----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/data-binding
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/data-binding.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor data binding

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains data binding features for Razor components and DOM elements in Blazor apps.

## Binding features

Razor components provide data binding features with the [`@bind`](../../mvc/views/razor?view=aspnetcore-10.0#bind) Razor directive attribute with a field, property, or Razor expression value.

The following example binds:

* An `<input>` element value to the C# `inputValue` field.
* A second `<input>` element value to the C# `InputValue` property.

When an `<input>` element loses focus, its bound field or property is updated.

`Bind.razor`:

```
@page "/bind"

<PageTitle>Bind</PageTitle>

<h1>Bind Example</h1>

<p>
    <label>
        inputValue: 
        <input @bind="inputValue" />
    </label>
</p>

<p>
    <label>
        InputValue: 
        <input @bind="InputValue" />
    </label>
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string? inputValue;

    private string? InputValue { get; set; }
}
```

```
@page "/bind"

<PageTitle>Bind</PageTitle>

<h1>Bind Example</h1>

<p>
    <label>
        inputValue: 
        <input @bind="inputValue" />
    </label>
</p>

<p>
    <label>
        InputValue: 
        <input @bind="InputValue" />
    </label>
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string? inputValue;

    private string? InputValue { get; set; }
}
```

```
@page "/bind"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <input @bind="InputValue" />
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string? inputValue;

    private string? InputValue { get; set; }
}
```

```
@page "/bind"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <input @bind="InputValue" />
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string? inputValue;

    private string? InputValue { get; set; }
}
```

```
@page "/bind"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <input @bind="InputValue" />
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string inputValue;

    private string InputValue { get; set; }
}
```

```
@page "/bind"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <input @bind="InputValue" />
</p>

<ul>
    <li><code>inputValue</code>: @inputValue</li>
    <li><code>InputValue</code>: @InputValue</li>
</ul>

@code {
    private string inputValue;

    private string InputValue { get; set; }
}
```

The text box is updated in the UI only when the component is rendered, not in response to changing the field's or property's value. Since components render themselves after event handler code executes, field and property updates are usually reflected in the UI immediately after an event handler is triggered.

As a demonstration of how data binding composes in HTML, the following example binds the `InputValue` property to the second `<input>` element's `value` and `onchange` attributes ([`change`](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)). *The second `<input>` element in the following example is a concept demonstration and isn't meant to suggest how you should bind data in Razor components.*

`BindTheory.razor`:

```
@page "/bind-theory"

<PageTitle>Bind Theory</PageTitle>

<h1>Bind Theory Example</h1>

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue" @onchange="@((ChangeEventArgs __e) =>
            InputValue = __e?.Value?.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
```

```
@page "/bind-theory"

<PageTitle>Bind Theory</PageTitle>

<h1>Bind Theory Example</h1>

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue" @onchange="@((ChangeEventArgs __e) =>
            InputValue = __e?.Value?.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
```

```
@page "/bind-theory"

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue"
            @onchange="@((ChangeEventArgs __e) => InputValue = __e?.Value?.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
```

```
@page "/bind-theory"

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue"
            @onchange="@((ChangeEventArgs __e) => InputValue = __e?.Value?.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
```

```
@page "/bind-theory"

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue"
            @onchange="@((ChangeEventArgs __e) => InputValue = __e.Value.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string InputValue { get; set; }
}
```

```
@page "/bind-theory"

<p>
    <label>
        Normal Blazor binding: 
        <input @bind="InputValue" />
    </label>
</p>

<p>
    <label>
        Demonstration of equivalent HTML binding: 
        <input value="@InputValue"
            @onchange="@((ChangeEventArgs __e) => InputValue = __e.Value.ToString())" />
    </label>
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string InputValue { get; set; }
}
```

When the `BindTheory` component is rendered, the `value` of the HTML demonstration `<input>` element comes from the `InputValue` property. When the user enters a value in the text box and changes element focus, the `onchange` event is fired and the `InputValue` property is set to the changed value. In reality, code execution is more complex because [`@bind`](../../mvc/views/razor?view=aspnetcore-10.0#bind) handles cases where type conversions are performed. In general, [`@bind`](../../mvc/views/razor?view=aspnetcore-10.0#bind) associates the current value of an expression with the `value` attribute of the `<input>` and handles changes using the registered handler.

Default binding with `@bind` uses the [`change`](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event) DOM event (`onchange`). Binding can also take place on the [`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event) DOM event using the `@bind:event="{EVENT}"` attribute, where the `{EVENT}` placeholder is either `oninput` or `onchange` (default).

The following example binds the `InputValue` property to the `<input>` element's value when the element's `oninput` event is triggered. Unlike the `onchange` event, which fires when the element loses focus, `oninput` fires when the value of the text box changes.

`Page/BindEvent.razor`:

```
@page "/bind-event"

<PageTitle>Bind Event</PageTitle>

<h1>Bind Event Example</h1>

<p>
    <label>
        InputValue: 
        <input @bind="InputValue" @bind:event="oninput" />
    </label>
    
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
```

```
@page "/bind-event"

<PageTitle>Bind Event</PageTitle>

<h1>Bind Event Example</h1>

<p>
    <label>
        InputValue: 
        <input @bind="InputValue" @bind:event="oninput" />
    </label>
    
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
```

```
@page "/bind-event"

<p>
    <input @bind="InputValue" @bind:event="oninput" />
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
```

```
@page "/bind-event"

<p>
    <input @bind="InputValue" @bind:event="oninput" />
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string? InputValue { get; set; }
}
```

```
@page "/bind-event"

<p>
    <input @bind="InputValue" @bind:event="oninput" />
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string InputValue { get; set; }
}
```

```
@page "/bind-event"

<p>
    <input @bind="InputValue" @bind:event="oninput" />
</p>

<p>
    <code>InputValue</code>: @InputValue
</p>

@code {
    private string InputValue { get; set; }
}
```

To bind on other DOM events, use either of the following approaches:

* [Delegate event handlers](event-handling?view=aspnetcore-10.0#delegate-event-handlers)
* [Custom event arguments](event-handling?view=aspnetcore-10.0#custom-event-arguments)

To bind on other DOM events, use [delegate event handlers](event-handling?view=aspnetcore-10.0#delegate-event-handlers).

Razor attribute binding is case-sensitive:

* `@bind` and `@bind:event` are valid.
* `@Bind`/`@Bind:Event` (capital letters `B` and `E`) or `@BIND`/`@BIND:EVENT` (all capital letters) **are invalid**.

To execute asynchronous logic after binding, use `@bind:after="{DELEGATE}"`, where the `{DELEGATE}` placeholder is a C# delegate (method). An assigned C# delegate isn't executed until the bound value is assigned synchronously.

Using an [event callback parameter (`EventCallback`/`EventCallback<T>`)](event-handling?view=aspnetcore-10.0#eventcallback) with `@bind:after` isn't supported. Instead, pass a method that returns an [Action](/en-us/dotnet/api/system.action) or [Task](/en-us/dotnet/api/system.threading.tasks.task) to `@bind:after`.

In the following example:

* Each `<input>` element's `value` is bound to the `searchText` field synchronously.

* The `PerformSearch` method executes asynchronously:

  * When the first box loses focus (`onchange` event) after the value is changed.
  * After each keystroke (`oninput` event) in the second box.

* `PerformSearch` calls a service with an asynchronous method (`FetchAsync`) to return search results.

```
@inject ISearchService SearchService

<input @bind="searchText" @bind:after="PerformSearch" />
<input @bind="searchText" @bind:event="oninput" @bind:after="PerformSearch" />

@code {
    private string? searchText;
    private string[]? searchResult;

    private async Task PerformSearch() => 
        searchResult = await SearchService.FetchAsync(searchText);
}
```

Additional examples

`BindAfter.razor`:

```
@page "/bind-after"
@using Microsoft.AspNetCore.Components.Forms

<h1>Bind After Examples</h1>

<h2>Elements</h2>

<input type="text" @bind="text" @bind:after="() => { }" />

<input type="text" @bind="text" @bind:after="After" />

<input type="text" @bind="text" @bind:after="AfterAsync" />

<h2>Components</h2>

<InputText @bind-Value="text" @bind-Value:after="() => { }" />

<InputText @bind-Value="text" @bind-Value:after="After" />

<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />

@code {
    private string text = "";

    private void After() {}
    private Task AfterAsync() { return Task.CompletedTask; }
}
```

For more information on the `InputText` component, see [ASP.NET Core Blazor input components](../forms/input-components?view=aspnetcore-10.0).

Components support two-way data binding by defining a pair of `@bind` attributes with either a `:get` or `:set` modifier. The `{PARAMETER}` placeholder in the following examples is used to bind a component parameter:

* `@bind:get`/`@bind-{PARAMETER}:get`: Specifies the value to bind.
* `@bind:set`/`@bind-{PARAMETER}:set`: Specifies a callback for when the value changes.

The `:get`/`:set` modifiers are always used together.

For binding to HTML elements, use `:get`/`:set` modifiers to ensure an `<input>` element stays synchronized with a bound value, even when the value is modified in the handler:

```
<input @bind:get="inputValue" @bind:set="HandleValueChange" />
```

Note

The legacy approach prior to the release of .NET 7 used the `value` attribute with `@onchange`:

```
<input value="@inputValue" @onchange="HandleValueChange" />
```

The preceding approach can cause synchronization errors, where the `<input>` element displays a different value than the value held in the bound variable. Migrate to `:get`/`:set` modifiers to avoid this issue.

For component parameters, the following syntaxes for a hypothetical `Child` component are functionally equivalent:

```
<Child Value="@inputValue" ValueChanged="@(v => inputValue = v)" />
<Child @bind-Value:get="inputValue" @bind-Value:set="@(v => inputValue = v)" />
```

With `:get`/`:set` modifier binding, you can react to a value change before it's applied to the DOM, and you can change the applied value, if necessary. Whereas with `@bind:event="{EVENT}"` attribute binding, where the `{EVENT}` placeholder is a DOM event, you receive the notification after the DOM is updated, and there's no capacity to modify the applied value while binding.

The following `BindGetSet` component demonstrates `:get`/`:set` syntax for `<input>` elements and the [`InputText` component](../forms/input-components?view=aspnetcore-10.0) used by [Blazor forms](../forms/?view=aspnetcore-10.0) in synchronous (`Set`) and asynchronous (`SetAsync`) scenarios.

`BindGetSet.razor`:

```
@page "/bind-get-set"
@using Microsoft.AspNetCore.Components.Forms

<h1>Bind Get Set Examples</h1>

<h2>Elements</h2>

<input type="text" @bind:get="text" @bind:set="(value) => { text = value; }" />
<input type="text" @bind:get="text" @bind:set="Set" />
<input type="text" @bind:get="text" @bind:set="SetAsync" />

<h2>Components</h2>

<InputText @bind-Value:get="text" @bind-Value:set="(value) => { text = value; }" />
<InputText @bind-Value:get="text" @bind-Value:set="Set" />
<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />

@code {
    private string text = string.Empty;

    private void Set(string value)
    {
        text = value;
    }

    private Task SetAsync(string value)
    {
        text = value;
        return Task.CompletedTask;
    }
}
```

For more information on the `InputText` component, see [ASP.NET Core Blazor input components](../forms/input-components?view=aspnetcore-10.0).

For another example use of `:get` and `:set` modifiers, see the [Bind across more than two components](#bind-across-more-than-two-components) section later in this article.

Razor attribute binding is case-sensitive:

* `@bind`, `@bind:event`, and `@bind:after` are valid.
* `@Bind`/`@bind:Event`/`@bind:aftEr` (capital letters) or `@BIND`/`@BIND:EVENT`/`@BIND:AFTER` (all capital letters) **are invalid**.

## Use `@bind:get`/`@bind:set` modifiers and avoid event handlers for two-way data binding

Two-way data binding isn't possible to implement with an event handler. Use `:get`/`:set` modifiers for two-way data binding.

❌ Consider the following ***dysfunctional approach*** for two-way data binding using an event handler:

```
<p>
    <input value="@inputValue" @oninput="OnInput" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private string? inputValue;

    private void OnInput(ChangeEventArgs args)
    {
        var newValue = args.Value?.ToString() ?? string.Empty;

        inputValue = newValue.Length > 4 ? "Long!" : newValue;
    }
}
```

The `OnInput` event handler updates the value of `inputValue` to `Long!` after a fourth character is provided. However, the user can continue adding characters to the element value in the UI. The value of `inputValue` isn't bound back to the element's value with each keystroke. The preceding example is only capable of one-way data binding.

The reason for this behavior is that Blazor isn't aware that your code intends to modify the value of `inputValue` in the event handler. Blazor doesn't try to force DOM element values and .NET variable values to match unless they're bound with `@bind` syntax. In earlier versions of Blazor, two-way data binding is implemented by [binding the element to a property and controlling the property's value with its setter](#binding-to-a-property-with-c-get-and-set-accessors). In ASP.NET Core in .NET 7 or later, `:get`/`:set` modifier syntax is used to implement two-way data binding, as the next example demonstrates.

✔️ Consider the following ***correct approach*** using `:get`/`:set` for two-way data binding:

```
<p>
    <input @bind:event="oninput" @bind:get="inputValue" @bind:set="OnInput" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private string? inputValue;

    private void OnInput(string value)
    {
        var newValue = value ?? string.Empty;

        inputValue = newValue.Length > 4 ? "Long!" : newValue;
    }
}
```

Using `:get`/`:set` modifiers both controls the underlying value of `inputValue` via `:set` and binds the value of `inputValue` to the element's value via `:get`. The preceding example demonstrates the correct approach for implementing two-way data binding.

## Binding to a property with C# `get` and `set` accessors

[C# `get` and `set` accessors](/en-us/dotnet/csharp/programming-guide/classes-and-structs/using-properties) can be used to create custom binding format behavior, as the following `DecimalBinding` component demonstrates. The component binds a positive or negative decimal with up to three decimal places to an `<input>` element by way of a `string` property (`DecimalValue`).

`DecimalBinding.razor`:

```
@page "/decimal-binding"
@using System.Globalization

<PageTitle>Decimal Binding</PageTitle>

<h1>Decimal Binding Example</h1>

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
```

```
@page "/decimal-binding"
@using System.Globalization

<PageTitle>Decimal Binding</PageTitle>

<h1>Decimal Binding Example</h1>

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
```

```
@page "/decimal-binding"
@using System.Globalization

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
```

```
@page "/decimal-binding"
@using System.Globalization

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
```

```
@page "/decimal-binding"
@using System.Globalization

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
```

```
@page "/decimal-binding"
@using System.Globalization

<p>
    <label>
        Decimal value (±0.000 format):
        <input @bind="DecimalValue" />
    </label>
</p>

<p>
    <code>decimalValue</code>: @decimalValue
</p>

@code {
    private decimal decimalValue = 1.1M;
    private NumberStyles style = 
        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
    private CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    private string DecimalValue
    {
        get => decimalValue.ToString("0.000", culture);
        set
        {
            if (Decimal.TryParse(value, style, culture, out var number))
            {
                decimalValue = Math.Round(number, 3);
            }
        }
    }
}
```

Note

Across multiple components, two-way binding to a property with `get`/`set` accessors requires discarding the [Task](/en-us/dotnet/api/system.threading.tasks.task) returned by [EventCallback.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback.invokeasync) in the property's setter. For two-way data binding, we recommend using `@bind:get`/`@bind:set` modifiers. For more information, see the [`@bind:get`/`@bind:set` guidance](#binding-features) earlier in this article.

To see an example of how the [Task](/en-us/dotnet/api/system.threading.tasks.task) returned by [EventCallback.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback.invokeasync) is discarded in .NET 6 or earlier before `@bind:get`/`@bind:set` modifiers became a framework feature, see [the `NestedChild` component of the *Bind across more than two components* section in the .NET 6 version of this article](?view=aspnetcore-6.0\&preserve-view=true#bind-across-more-than-two-components).

Note

Two-way binding to a property with `get`/`set` accessors requires discarding the [Task](/en-us/dotnet/api/system.threading.tasks.task) returned by [EventCallback.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback.invokeasync). For an example, see [the `NestedChild` component of the *Bind across more than two components* section](#bind-across-more-than-two-components). For two-way data binding in .NET 7 or later, we recommend using `@bind:get`/`@bind:set` modifiers, which are described in .NET 7 or later versions of this article.

## Multiple option selection with `<select>` elements

Binding supports [`multiple`](https://developer.mozilla.org/docs/Web/HTML/Attributes/multiple) option selection with `<select>` elements. The [`@onchange`](../../mvc/views/razor?view=aspnetcore-10.0#onevent) event provides an array of the selected elements via [event arguments (`ChangeEventArgs`)](event-handling?view=aspnetcore-10.0#event-arguments). The value must be bound to an array type.

`BindMultipleInput.razor`:

```
@page "/bind-multiple-input"

<h1>Bind Multiple <code>input</code>Example</h1>

<p>
    <label>
        Select one or more cars: 
        <select @onchange="SelectedCarsChanged" multiple>
            <option value="audi">Audi</option>
            <option value="jeep">Jeep</option>
            <option value="opel">Opel</option>
            <option value="saab">Saab</option>
            <option value="volvo">Volvo</option>
        </select>
    </label>
</p>

<p>
    Selected Cars: @string.Join(", ", SelectedCars)
</p>

<p>
    <label>
        Select one or more cities: 
        <select @bind="SelectedCities" multiple>
            <option value="bal">Baltimore</option>
            <option value="la">Los Angeles</option>
            <option value="pdx">Portland</option>
            <option value="sf">San Francisco</option>
            <option value="sea">Seattle</option>
        </select>
    </label>
</p>

<span>
    Selected Cities: @string.Join(", ", SelectedCities)
</span>

@code {
    public string[] SelectedCars { get; set; } = [];
    public string[] SelectedCities { get; set; } = [ "bal", "sea" ];

    private void SelectedCarsChanged(ChangeEventArgs e)
    {
        if (e.Value is not null)
        {
            SelectedCars = (string[])e.Value;
        }
    }
}
```

```
@page "/bind-multiple-input"

<h1>Bind Multiple <code>input</code>Example</h1>

<p>
    <label>
        Select one or more cars: 
        <select @onchange="SelectedCarsChanged" multiple>
            <option value="audi">Audi</option>
            <option value="jeep">Jeep</option>
            <option value="opel">Opel</option>
            <option value="saab">Saab</option>
            <option value="volvo">Volvo</option>
        </select>
    </label>
</p>

<p>
    Selected Cars: @string.Join(", ", SelectedCars)
</p>

<p>
    <label>
        Select one or more cities: 
        <select @bind="SelectedCities" multiple>
            <option value="bal">Baltimore</option>
            <option value="la">Los Angeles</option>
            <option value="pdx">Portland</option>
            <option value="sf">San Francisco</option>
            <option value="sea">Seattle</option>
        </select>
    </label>
</p>

<span>
    Selected Cities: @string.Join(", ", SelectedCities)
</span>

@code {
    public string[] SelectedCars { get; set; } = new string[] { };
    public string[] SelectedCities { get; set; } = new[] { "bal", "sea" };

    private void SelectedCarsChanged(ChangeEventArgs e)
    {
        if (e.Value is not null)
        {
            SelectedCars = (string[])e.Value;
        }
    }
}
```

For information on how empty strings and `null` values are handled in data binding, see the [Binding `<select>` element options to C# object `null` values](#binding-select-element-options-to-c-object-null-values) section.

## Binding `<select>` element options to C# object `null` values

There's no sensible way to represent a `<select>` element option value as a C# object `null` value, because:

* HTML attributes can't have `null` values. The closest equivalent to `null` in HTML is absence of the HTML `value` attribute from the `<option>` element.
* When selecting an `<option>` with no `value` attribute, the browser treats the value as the *text content* of that `<option>`'s element.

The Blazor framework doesn't attempt to suppress the default behavior because it would involve:

* Creating a chain of special-case workarounds in the framework.
* Breaking changes to current framework behavior.

The most plausible `null` equivalent in HTML is an *empty string* `value`. The Blazor framework handles `null` to empty string conversions for two-way binding to a `<select>`'s value.

## Unparsable values

When a user provides an unparsable value to a data-bound element, the unparsable value is automatically reverted to its previous value when the bind event is triggered.

Consider the following component, where an `<input>` element is bound to an `int` type with an initial value of `123`.

`UnparsableValues.razor`:

```
@page "/unparsable-values"

<PageTitle>Unparsable Values</PageTitle>

<h1>Unparsable Values Example</h1>

<p>
    <label>
        inputValue: 
        <input @bind="inputValue" />
    </label>
    
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
```

```
@page "/unparsable-values"

<PageTitle>Unparsable Values</PageTitle>

<h1>Unparsable Values Example</h1>

<p>
    <label>
        inputValue: 
        <input @bind="inputValue" />
    </label>
    
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
```

```
@page "/unparseable-values"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
```

```
@page "/unparseable-values"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
```

```
@page "/unparseable-values"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
```

```
@page "/unparseable-values"

<p>
    <input @bind="inputValue" />
</p>

<p>
    <code>inputValue</code>: @inputValue
</p>

@code {
    private int inputValue = 123;
}
```

Binding applies to the element's `onchange` event. If the user updates the value of the text box's entry to `123.45` and changes the focus, the element's value is reverted to `123` when `onchange` fires. When the value `123.45` is rejected in favor of the original value of `123`, the user understands that their value wasn't accepted.

For the `oninput` event (`@bind:event="oninput"`), a value reversion occurs after any keystroke that introduces an unparsable value. When targeting the `oninput` event with an `int`-bound type, a user is prevented from typing a dot (`.`) character. A dot (`.`) character is immediately removed, so the user receives immediate feedback that only whole numbers are permitted. There are scenarios where reverting the value on the `oninput` event isn't ideal, such as when the user should be allowed to clear an unparsable `<input>` value. Alternatives include:

* Don't use the `oninput` event. Use the default `onchange` event, where an invalid value isn't reverted until the element loses focus.

* Bind to a nullable type, such as `int?` or `string` and either use `@bind:get`/`@bind:set` modifiers (described earlier in this article) or [bind to a property with custom `get` and `set` accessor logic](#binding-to-a-property-with-c-get-and-set-accessors) to handle invalid entries.

* Use an [input component](../forms/input-components?view=aspnetcore-10.0), such as [InputNumber\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputnumber-1) or [InputDate\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputdate-1), with [form validation](../forms/validation?view=aspnetcore-10.0). Input components together with form validation components provide built-in support to manage invalid inputs:

  * Permit the user to provide invalid input and receive validation errors on the associated [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext).
  * Display validation errors in the UI without interfering with the user entering additional webform data.

## Format strings

Data binding works with a single [DateTime](/en-us/dotnet/api/system.datetime) format string using `@bind:format="{FORMAT STRING}"`, where the `{FORMAT STRING}` placeholder is the format string. Other format expressions, such as currency or number formats, aren't available at this time but might be added in a future release.

`DateBinding.razor`:

```
@page "/date-binding"

<PageTitle>Date Binding</PageTitle>

<h1>Date Binding Example</h1>

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new(2020, 1, 1);
}
```

```
@page "/date-binding"

<PageTitle>Date Binding</PageTitle>

<h1>Date Binding Example</h1>

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new(2020, 1, 1);
}
```

```
@page "/date-binding"

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new(2020, 1, 1);
}
```

```
@page "/date-binding"

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new(2020, 1, 1);
}
```

```
@page "/date-binding"

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new(2020, 1, 1);
}
```

```
@page "/date-binding"

<p>
    <label>
        <code>yyyy-MM-dd</code> format:
        <input @bind="startDate" @bind:format="yyyy-MM-dd" />
    </label>
</p>

<p>
    <code>startDate</code>: @startDate
</p>

@code {
    private DateTime startDate = new DateTime(2020, 1, 1);
}
```

In the preceding code, the `<input>` element's field type (`type` attribute) defaults to `text`.

Nullable [System.DateTime](/en-us/dotnet/api/system.datetime) and [System.DateTimeOffset](/en-us/dotnet/api/system.datetimeoffset) are supported:

```
private DateTime? date;
private DateTimeOffset? dateOffset;
```

Specifying a format for the `date` field type isn't recommended because Blazor has built-in support to format dates. In spite of the recommendation, only use the `yyyy-MM-dd` date format for binding to function correctly if a format is supplied with the `date` field type:

```
<input type="date" @bind="startDate" @bind:format="yyyy-MM-dd">
```

## Binding with component parameters

A common scenario is binding a property of a child component to a property in its parent component. This scenario is called a *chained bind* because multiple levels of binding occur simultaneously.

You can't implement chained binds with [`@bind`](../../mvc/views/razor?view=aspnetcore-10.0#bind) syntax in a child component. An event handler and value must be specified separately to support updating the property in the parent from the child component. The parent component still leverages [`@bind`](../../mvc/views/razor?view=aspnetcore-10.0#bind) syntax to set up data binding with the child component.

The following `ChildBind` component has a `Year` component parameter and an [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1). By convention, the [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1) for the parameter must be named as the component parameter name with a "`Changed`" suffix. The naming syntax is `{PARAMETER NAME}Changed`, where the `{PARAMETER NAME}` placeholder is the parameter name. In the following example, the [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1) is named `YearChanged`.

[EventCallback.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback.invokeasync) invokes the delegate associated with the binding with the provided argument and dispatches an event notification for the changed property.

`ChildBind.razor`:

```
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild() => 
        await YearChanged.InvokeAsync(Random.Shared.Next(1950, 2021));
}
```

```
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild() => 
        await YearChanged.InvokeAsync(Random.Shared.Next(1950, 2021));
}
```

```
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(Random.Shared.Next(1950, 2021));
    }
}
```

```
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(Random.Shared.Next(1950, 2021));
    }
}
```

```
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    private Random r = new();

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

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(r.Next(1950, 2021));
    }
}
```

```
<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    private Random r = new Random();

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

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(r.Next(1950, 2021));
    }
}
```

For more information on events and [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1), see the *EventCallback* section of the [ASP.NET Core Blazor event handling](event-handling?view=aspnetcore-10.0#eventcallback) article.

In the following `Parent1` component, the `year` field is bound to the `Year` parameter of the child component. The `Year` parameter is bindable because it has a companion `YearChanged` event that matches the type of the `Year` parameter.

`Parent1.razor`:

```
@page "/parent-1"

<PageTitle>Parent 1</PageTitle>

<h1>Parent Example 1</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private int year = 1979;

    private void UpdateYear() => year = Random.Shared.Next(1950, 2021);
}
```

```
@page "/parent-1"

<PageTitle>Parent 1</PageTitle>

<h1>Parent Example 1</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private int year = 1979;

    private void UpdateYear() => year = Random.Shared.Next(1950, 2021);
}
```

```
@page "/parent-1"

<h1>Parent Component</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private int year = 1979;

    private void UpdateYear()
    {
        year = Random.Shared.Next(1950, 2021);
    }
}
```

```
@page "/parent-1"

<h1>Parent Component</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private int year = 1979;

    private void UpdateYear()
    {
        year = Random.Shared.Next(1950, 2021);
    }
}
```

```
@page "/parent-1"

<h1>Parent Component</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private Random r = new();
    private int year = 1979;

    private void UpdateYear()
    {
        year = r.Next(1950, 2021);
    }
}
```

```
@page "/parent-1"

<h1>Parent Component</h1>

<p>Parent <code>year</code>: @year</p>

<button @onclick="UpdateYear">Update Parent <code>year</code></button>

<ChildBind @bind-Year="year" />

@code {
    private Random r = new Random();
    private int year = 1979;

    private void UpdateYear()
    {
        year = r.Next(1950, 2021);
    }
}
```

Component parameter binding can also trigger `@bind:after` events. In the following example, the `YearUpdated` method executes asynchronously after binding the `Year` component parameter.

```
<ChildBind @bind-Year="year" @bind-Year:after="YearUpdated" />

@code {
    ...

    private async Task YearUpdated()
    {
        ... = await ...;
    }
}
```

By convention, a property can be bound to a corresponding event handler by including an `@bind-{PROPERTY}:event` attribute assigned to the handler, where the `{PROPERTY}` placeholder is the property. `<ChildBind @bind-Year="year" />` is equivalent to writing:

```
<ChildBind @bind-Year="year" @bind-Year:event="YearChanged" />
```

In a more sophisticated and real-world example, the following `PasswordEntry` component:

* Sets an `<input>` element's value to a `password` field.
* Exposes changes of a `Password` property to a parent component with an [`EventCallback`](event-handling?view=aspnetcore-10.0#eventcallback) that passes in the current value of the child's `password` field as its argument.
* Uses the `onclick` event to trigger the `ToggleShowPassword` method. For more information, see [ASP.NET Core Blazor event handling](event-handling?view=aspnetcore-10.0).

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](../security/?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

`PasswordEntry.razor`:

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
        </p>
    </div>
</div>

@code {
    private string? password;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        await PasswordChanged.InvokeAsync(password);
    }
}
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
        </p>
    </div>
</div>

@code {
    private string? password;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        await PasswordChanged.InvokeAsync(password);
    }
}
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
        </p>
    </div>
</div>

@code {
    private string? password;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        await PasswordChanged.InvokeAsync(password);
    }
}
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
        </p>
    </div>
</div>

@code {
    private string? password;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        await PasswordChanged.InvokeAsync(password);
    }
}
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
        </p>
    </div>
</div>

@code {
    private string password;

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

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e.Value.ToString();

        await PasswordChanged.InvokeAsync(password);
    }
}
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
        </p>
    </div>
</div>

@code {
    private string password;

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

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private async Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e.Value.ToString();

        await PasswordChanged.InvokeAsync(password);
    }
}
```

The `PasswordEntry` component is used in another component, such as the following `PasswordBinding` component example.

`PasswordBinding.razor`:

```
@page "/password-binding"

<PageTitle>Password Binding</PageTitle>

<h1>Password Binding Example</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
```

```
@page "/password-binding"

<PageTitle>Password Binding</PageTitle>

<h1>Password Binding Example</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
```

```
@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
```

```
@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
```

```
@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
```

```
@page "/password-binding"

<h1>Password Binding</h1>

<PasswordEntry @bind-Password="password" />

<p>
    <code>password</code>: @password
</p>

@code {
    private string password = "Not set";
}
```

When the `PasswordBinding` component is initially rendered, the `password` value of `Not set` is displayed in the UI. After initial rendering, the value of `password` reflects changes made to the `Password` component parameter value in the `PasswordEntry` component.

Note

The preceding example binds the password one-way from the child `PasswordEntry` component to the parent `PasswordBinding` component. Two-way binding isn't a requirement in this scenario if the goal is for the app to have a shared password entry component for reuse around the app that merely passes the password to the parent. For an approach that permits two-way binding without [writing directly to the child component's parameter](overwriting-parameters?view=aspnetcore-10.0), see the `NestedChild` component example in the [Bind across more than two components](#bind-across-more-than-two-components) section of this article.

Perform checks or trap errors in the handler. The following revised `PasswordEntry` component provides immediate feedback to the user if a space is used in the password's value.

`PasswordEntry.razor`:

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
    </div>
</div>

@code {
    private string? password;
    private string? validationMessage;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        if (password != null && password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }
}
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
    </div>
</div>

@code {
    private string? password;
    private string? validationMessage;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        if (password != null && password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }
}
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
    </div>
</div>

@code {
    private string? password;
    private string? validationMessage;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        if (password != null && password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }
}
```

In the following example, the `PasswordUpdated` method executes asynchronously after binding the `Password` component parameter:

```
<PasswordEntry @bind-Password="password" @bind-Password:after="PasswordUpdated" />
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
    </div>
</div>

@code {
    private string? password;
    private string? validationMessage;

    [Parameter]
    public string? Password { get; set; }

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e?.Value?.ToString();

        if (password != null && password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }
}
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
    </div>
</div>

@code {
    private string password;
    private string validationMessage;

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

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e.Value.ToString();

        if (password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }
}
```

```
<div class="card bg-light mt-3" style="width:22rem ">
    <div class="card-body">
        <h3 class="card-title">Password Component</h3>
        <p class="card-text">
            <label>
                Password:
                <input @oninput="OnPasswordChanged" required type="password"
                    value="@password" />
            </label>
            <span class="text-danger">@validationMessage</span>
        </p>
    </div>
</div>

@code {
    private string password;
    private string validationMessage;

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

    [Parameter]
    public EventCallback<string> PasswordChanged { get; set; }

    private Task OnPasswordChanged(ChangeEventArgs e)
    {
        password = e.Value.ToString();

        if (password.Contains(' '))
        {
            validationMessage = "Spaces not allowed!";

            return Task.CompletedTask;
        }
        else
        {
            validationMessage = string.Empty;

            return PasswordChanged.InvokeAsync(password);
        }
    }
}
```

## Bind across more than two components

You can bind parameters through any number of nested components, but you must respect the one-way flow of data:

* Change notifications *flow up the hierarchy*.
* New parameter values *flow down the hierarchy*.

A common and recommended approach is to only store the underlying data in the parent component to avoid any confusion about what state must be updated, as shown in the following example.

`Parent2.razor`:

```
@page "/parent-2"

<PageTitle>Parent 2</PageTitle>

<h1>Parent Example 2</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue() => parentMessage = $"Set in Parent {DateTime.Now}";
}
```

```
@page "/parent-2"

<PageTitle>Parent 2</PageTitle>

<h1>Parent Example 2</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue() => parentMessage = $"Set in Parent {DateTime.Now}";
}
```

```
@page "/parent-2"

<h1>Parent Component</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue()
    {
        parentMessage = $"Set in Parent {DateTime.Now}";
    }
}
```

```
@page "/parent-2"

<h1>Parent Component</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue()
    {
        parentMessage = $"Set in Parent {DateTime.Now}";
    }
}
```

```
@page "/parent-2"

<h1>Parent Component</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue()
    {
        parentMessage = $"Set in Parent {DateTime.Now}";
    }
}
```

```
@page "/parent-2"

<h1>Parent Component</h1>

<p>Parent Message: <b>@parentMessage</b></p>

<p>
    <button @onclick="ChangeValue">Change from Parent</button>
</p>

<NestedChild @bind-ChildMessage="parentMessage" />

@code {
    private string parentMessage = "Initial value set in Parent";

    private void ChangeValue()
    {
        parentMessage = $"Set in Parent {DateTime.Now}";
    }
}
```

In the following `NestedChild` component, the `NestedGrandchild` component:

* Assigns the value of `ChildMessage` to `GrandchildMessage` with `@bind:get` syntax.
* Updates `GrandchildMessage` when `ChildMessageChanged` executes with `@bind:set` syntax.

Prior to the release of .NET 7, two-way binding across components uses `get`/`set` accessors with a third property that discards the [Task](/en-us/dotnet/api/system.threading.tasks.task) returned by [EventCallback.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback.invokeasync) in its setter. To see an example of this approach for .NET 6 or earlier before `@bind:get`/`@bind:set` modifiers became a framework feature, see [the `NestedChild` component of this section in the .NET 6 version of this article](?view=aspnetcore-6.0\&preserve-view=true#bind-across-more-than-two-components).

The reason to avoid directly changing the value of a component parameter is that it effectively mutates the parent's state from the child component. This can interfere with Blazor's change detection process and trigger extra render cycles because parameters are meant to be *inputs*, they're not meant to be mutable state. In chained scenarios where data is passed among components, directly writing to a component parameter can lead to unintended effects, such as infinite rerenders that hang the app.

`@bind:get`/`@bind:set` syntax allows you to:

* Avoid creating an extra property that only exists to forward values and callbacks across chained components, which was required prior to the release of .NET 7.
* Intercept and transform values before they're applied.
* Keep the parameter immutable in the child, while still supporting two-way binding.

A useful analogy is [HTML's `<input>` element](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/input) that tracks the following value states:

* `defaultValue`: Like a component parameter received from the parent.
* `value`: Like the current state inside the component.

If you mutate `defaultValue` directly, you're breaking the contract. Instead, these states are kept separate, and only the `value` is updated through controlled means after the initial render of the element. The same reasoning applies to component parameters, and using `@bind:get`/`@bind:set` syntax avoids potential unintended rendering effects associated with writing directly to component parameters.

`NestedChild.razor`:

```
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage:get="ChildMessage" 
        @bind-GrandchildMessage:set="ChildMessageChanged" />
</div>

@code {
    [Parameter]
    public string? ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string?> ChildMessageChanged { get; set; }

    private async Task ChangeValue() => 
        await ChildMessageChanged.InvokeAsync($"Set in Child {DateTime.Now}");
}
```

```
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage:get="ChildMessage" 
        @bind-GrandchildMessage:set="ChildMessageChanged" />
</div>

@code {
    [Parameter]
    public string? ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string?> ChildMessageChanged { get; set; }

    private async Task ChangeValue() => 
        await ChildMessageChanged.InvokeAsync($"Set in Child {DateTime.Now}");
}
```

```
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage:get="ChildMessage" 
        @bind-GrandchildMessage:set="ChildMessageChanged" />
</div>

@code {
    [Parameter]
    public string? ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string?> ChildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await ChildMessageChanged.InvokeAsync(
            $"Set in Child {DateTime.Now}");
    }
}
```

```
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage="BoundValue" />
</div>

@code {
    [Parameter]
    public string? ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string> ChildMessageChanged { get; set; }

    private string BoundValue
    {
        get => ChildMessage ?? string.Empty;
        set => ChildMessageChanged.InvokeAsync(value);
    }

    private async Task ChangeValue()
    {
        await ChildMessageChanged.InvokeAsync(
            $"Set in Child {DateTime.Now}");
    }
}
```

```
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage="BoundValue" />
</div>

@code {
    [Parameter]
    public string ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string> ChildMessageChanged { get; set; }

    private string BoundValue
    {
        get => ChildMessage;
        set => ChildMessageChanged.InvokeAsync(value);
    }

    private async Task ChangeValue()
    {
        await ChildMessageChanged.InvokeAsync(
            $"Set in Child {DateTime.Now}");
    }
}
```

```
<div class="border rounded m-1 p-1">
    <h2>Child Component</h2>

    <p>Child Message: <b>@ChildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Child</button>
    </p>

    <NestedGrandchild @bind-GrandchildMessage="BoundValue" />
</div>

@code {
    [Parameter]
    public string ChildMessage { get; set; }

    [Parameter]
    public EventCallback<string> ChildMessageChanged { get; set; }

    private string BoundValue
    {
        get => ChildMessage;
        set => ChildMessageChanged.InvokeAsync(value);
    }

    private async Task ChangeValue()
    {
        await ChildMessageChanged.InvokeAsync(
            $"Set in Child {DateTime.Now}");
    }
}
```

Warning

Generally, avoid creating components that write directly to their own component parameters. The preceding `NestedChild` component makes use of a `BoundValue` property instead of writing directly to its `ChildMessage` parameter. For more information, see [Avoid overwriting parameters in ASP.NET Core Blazor](overwriting-parameters?view=aspnetcore-10.0).

`NestedGrandchild.razor`:

```
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string? GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue() => 
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
}
```

```
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string? GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue() => 
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
}
```

```
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string? GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
    }
}
```

```
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string? GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
    }
}
```

```
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
    }
}
```

```
<div class="border rounded m-1 p-1">
    <h3>Grandchild Component</h3>

    <p>Grandchild Message: <b>@GrandchildMessage</b></p>

    <p>
        <button @onclick="ChangeValue">Change from Grandchild</button>
    </p>
</div>

@code {
    [Parameter]
    public string GrandchildMessage { get; set; }

    [Parameter]
    public EventCallback<string> GrandchildMessageChanged { get; set; }

    private async Task ChangeValue()
    {
        await GrandchildMessageChanged.InvokeAsync(
            $"Set in Grandchild {DateTime.Now}");
    }
}
```

For an alternative approach suited to sharing data in memory and across components that aren't necessarily nested, see [ASP.NET Core Blazor state management overview](../state-management/?view=aspnetcore-10.0).

## Bound field or property expression tree

To facilitate deeper interactions with a binding, Blazor allows you to capture of the [expression tree](/en-us/dotnet/csharp/advanced-topics/expression-trees/) of a bound field or property. This is achieved by defining a property with the field or property name suffixed with `Expression`. For any given field or property named `{FIELD OR PROPERTY NAME}`, the corresponding expression tree property is named `{FIELD OR PROPERTY NAME}Expression`.

The following `ChildParameterExpression` component identifies the `Year` expression's model and field name. A [FieldIdentifier](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.fieldidentifier), which is used to obtain the model and field name, uniquely identifies a single field that can be edited. This may correspond to a property on a model object or can be any other named value. Use of a parameter's expression is useful when creating custom validation components, which isn't covered by the Microsoft Blazor documentation but is addressed by numerous third-party resources.

`ChildParameterExpression.razor`:

```
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

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

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized() => 
        yearField = FieldIdentifier.Create(YearExpression);
}
```

```
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

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

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized() => 
        yearField = FieldIdentifier.Create(YearExpression);
}
```

```
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

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

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized()
    {
        yearField = FieldIdentifier.Create(YearExpression);
    }
}
```

```
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

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

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized()
    {
        yearField = FieldIdentifier.Create(YearExpression);
    }
}
```

```
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

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

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized()
    {
        yearField = FieldIdentifier.Create(YearExpression);
    }
}
```

```
@using System.Linq.Expressions

<ul>
    <li>Year model: @yearField.Model</li>
    <li>Year field name: @yearField.FieldName</li>
</ul>

@code {
    private FieldIdentifier yearField;

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

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    [Parameter]
    public Expression<Func<int>> YearExpression { get; set; } = default!;

    protected override void OnInitialized()
    {
        yearField = FieldIdentifier.Create(YearExpression);
    }
}
```

`Parent3.razor`:

```
@page "/parent-3"

<PageTitle>Parent 3</PageTitle>

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
```

```
@page "/parent-3"

<PageTitle>Parent 3</PageTitle>

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
```

```
@page "/parent-3"

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
```

```
@page "/parent-3"

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
```

```
@page "/parent-3"

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
```

```
@page "/parent-3"

<h1>Parent Example 3</h1>

<p>Parent <code>year</code>: @year</p>

<ChildParameterExpression @bind-Year="year" />

@code {
    private int year = 1979;
}
```

## Related content

* [Parameter change detection and additional guidance on Razor component rendering](rendering?view=aspnetcore-10.0)
* [ASP.NET Core Blazor forms overview](../forms/?view=aspnetcore-10.0)
* [Binding to radio buttons in a form](../forms/binding?view=aspnetcore-10.0#radio-buttons)
* [Binding `InputSelect` options to C# object `null` values](../forms/binding?view=aspnetcore-10.0#binding-inputselect-options-to-c-object-null-values)
* [ASP.NET Core Blazor event handling: `EventCallback` section](event-handling?view=aspnetcore-10.0#eventcallback)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/web-api/jsonpatch
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/web-api/jsonpatch.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# JSON Patch support in ASP.NET Core web API

This article explains how to handle JSON Patch requests in an ASP.NET Core web API.

JSON Patch support in ASP.NET Core web API is based on [System.Text.Json](/en-us/dotnet/api/system.text.json) serialization, and requires the [`Microsoft.AspNetCore.JsonPatch.SystemTextJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch.SystemTextJson) NuGet package.

## What is the JSON Patch standard?

The JSON Patch standard:

* Is a standard format for describing changes to apply to a JSON document.

* Is defined in [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) and is widely used in RESTful APIs to perform partial updates to JSON resources.

* Describes a sequence of operations that modify a JSON document such as:

  * `add`
  * `remove`
  * `replace`
  * `move`
  * `copy`
  * `test`

In web apps, JSON Patch is commonly used in a PATCH operation to perform partial updates of a resource. Rather than sending the entire resource for an update, clients can send a JSON Patch document containing only the changes. Patching reduces payload size and improves efficiency.

For an overview of the JSON Patch standard, see [jsonpatch.com](https://jsonpatch.com/).

## JSON Patch support in ASP.NET Core web API

JSON Patch support in ASP.NET Core web API is based on [System.Text.Json](/en-us/dotnet/api/system.text.json) serialization, starting with .NET 10. It implements [Microsoft.AspNetCore.JsonPatch](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch) based on [System.Text.Json](/en-us/dotnet/api/system.text.json) serialization. This feature:

* Requires the [`Microsoft.AspNetCore.JsonPatch.SystemTextJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch.SystemTextJson) NuGet package.
* Aligns with modern .NET practices by leveraging the [System.Text.Json](/en-us/dotnet/api/system.text.json) library, which is optimized for .NET.
* Provides improved performance and reduced memory usage compared to the legacy `Newtonsoft.Json`-based implementation. For more information on the legacy `Newtonsoft.Json`-based implementation, see the [.NET 9 version of this article](?view=aspnetcore-9.0\&preserve-view=true).

Note

The implementation of [Microsoft.AspNetCore.JsonPatch](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch) based on [System.Text.Json](/en-us/dotnet/api/system.text.json) serialization isn't a drop-in replacement for the legacy `Newtonsoft.Json`-based implementation. It doesn't support dynamic types, such as [ExpandoObject](/en-us/dotnet/api/system.dynamic.expandoobject).

Important

The JSON Patch standard has ***inherent security risks***. Since these risks are inherent to the JSON Patch standard, the ASP.NET Core implementation ***doesn't attempt to mitigate inherent security risks***. It's the responsibility of the developer to ensure that the JSON Patch document is safe to apply to the target object. For more information, see the [Mitigating Security Risks](#mitigating-security-risks) section.

## Enable JSON Patch support with [System.Text.Json](/en-us/dotnet/api/system.text.json)

To enable JSON Patch support with [System.Text.Json](/en-us/dotnet/api/system.text.json), install the [`Microsoft.AspNetCore.JsonPatch.SystemTextJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch.SystemTextJson) NuGet package.

```
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson
```

This package provides a [JsonPatchDocument\<TModel>](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument-1) class to represent a JSON Patch document for objects of type `TModel` and custom logic for serializing and deserializing JSON Patch documents using [System.Text.Json](/en-us/dotnet/api/system.text.json). The key method of the [JsonPatchDocument\<TModel>](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument-1) class is [ApplyTo(Object)](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument.applyto#microsoft-aspnetcore-jsonpatch-systemtextjson-jsonpatchdocument-applyto\(system-object\)), which applies the patch operations to a target object of type `TModel`.

## Minimal API PATCH endpoint applying JSON Patch

In a Minimal API, a PATCH endpoint for JSON Patch:

* Uses `MapPatch` to define the route.
* Accepts a [JsonPatchDocument\<TModel>](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument-1) parameter.
* Calls [ApplyTo(Object)](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument.applyto#microsoft-aspnetcore-jsonpatch-systemtextjson-jsonpatchdocument-applyto\(system-object\)) on the patch document to apply the changes.

### Example Minimal API PATCH endpoint

```
group.MapPatch("/{id}", async Task<Results<Ok<Customer>,ValidationProblem,NotFound<ProblemDetails>>> (AppDb db, string id,
    JsonPatchDocument<Customer> patchDoc) =>
{
    var customer = await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id);
    if (customer is null)
    {
        return TypedResults.NotFound<ProblemDetails>(new ());
    }
    if (patchDoc != null)
    {
        Dictionary<string, string[]>? errors = null;
        patchDoc.ApplyTo(customer, jsonPatchError =>
            {
                errors ??= new ();
                var key = jsonPatchError.AffectedObject.GetType().Name;
                if (!errors.ContainsKey(key))
                {
                    errors.Add(key, new string[] { });
                }
                errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
            });

        if (errors != null)
        {
            return TypedResults.ValidationProblem(errors);
        }

        // Only save if there are no errors
        await db.SaveChangesAsync();
    }

    return TypedResults.Ok(customer);
})
.Accepts<JsonPatchDocument<Customer>>("application/json-patch+json");
```

This code from the sample app works with the following `Customer` and `Order` models:

```
using System.ComponentModel.DataAnnotations;

namespace App.Models;

public class Customer
{
    public string Id { get; set; }
    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? PhoneNumber { get; set; }
    public string? Address { get; set; }
    public List<Order>? Orders { get; set; }
    public Customer()
    {
        Id = Guid.NewGuid().ToString();
    }
}
```

```
namespace App.Models;

public class Order
{
    public string Id { get; set; }
    public DateTime? OrderDate { get; set; }
    public DateTime? ShipDate { get; set; }
    public decimal TotalAmount { get; set; }

    public Order()
    {
        Id = Guid.NewGuid().ToString();
    }
}
```

The sample PATCH endpoint's key steps:

* **Retrieve the Customer**:

  * The endpoint retrieves a `Customer` object from the database `AppDb` using the provided `id`.
  * If no `Customer` object is found, it returns a `404 Not Found` response via `TypedResults.NotFound()`.

* **Apply JSON Patch**:

  * The [ApplyTo(Object)](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument.applyto#microsoft-aspnetcore-jsonpatch-systemtextjson-jsonpatchdocument-applyto\(system-object\)) method applies the JSON Patch operations from the `patchDoc` to the retrieved `Customer` object.
  * If errors occur during the patch application, such as invalid operations or conflicts, an error handling delegate captures them. This delegate collects error messages into a dictionary keyed by the type name of the affected object.

* **Return validation errors**:
  * If the error handling delegate captures any errors during the patch application, the endpoint returns a `ValidationProblem` response containing the error details via `TypedResults.ValidationProblem(errors)`.

* **Save and return the Updated Customer**:
  * If the patch is successfully applied with no errors, the changes are saved to the database and the endpoint returns the updated `Customer` object via `TypedResults.Ok(customer)`.

### Example error response

The following example shows the body of a validation problem response for a JSON Patch operation when the specified path is invalid:

```
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Customer": [
      "The target location specified by path segment 'foobar' was not found."
    ]
  }
}
```

## Apply a JSON Patch document to an object

The following examples demonstrate how to use the [ApplyTo(Object)](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument.applyto#microsoft-aspnetcore-jsonpatch-systemtextjson-jsonpatchdocument-applyto\(system-object\)) method to apply a JSON Patch document to an object.

### Example: Apply a [JsonPatchDocument\<TModel>](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument-1) to an object

The following example demonstrates:

* The `add`, `replace`, and `remove` operations.
* Operations on nested properties.
* Adding a new item to an array.
* Using a JSON String Enum Converter in a JSON patch document.

```
// Original object
var person = new Person {
    FirstName = "John",
    LastName = "Doe",
    Email = "johndoe@gmail.com",
    PhoneNumbers = [new() {Number = "123-456-7890", Type = PhoneNumberType.Mobile}],
    Address = new Address
    {
        Street = "123 Main St",
        City = "Anytown",
        State = "TX"
    }
};

// Raw JSON patch document
string jsonPatch = """
[
    { "op": "replace", "path": "/FirstName", "value": "Jane" },
    { "op": "remove", "path": "/Email"},
    { "op": "add", "path": "/Address/ZipCode", "value": "90210" },
    { "op": "add", "path": "/PhoneNumbers/-", "value": { "Number": "987-654-3210",
                                                                "Type": "Work" } }
]
""";

// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);

// Apply the JSON patch document
patchDoc!.ApplyTo(person);

// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
```

The previous example results in the following output of the updated object:

```
{
    "firstName": "Jane",
    "lastName": "Doe",
    "address": {
        "street": "123 Main St",
        "city": "Anytown",
        "state": "TX",
        "zipCode": "90210"
    },
    "phoneNumbers": [
        {
            "number": "123-456-7890",
            "type": "Mobile"
        },
        {
            "number": "987-654-3210",
            "type": "Work"
        }
    ]
}
```

The [ApplyTo(Object)](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument.applyto#microsoft-aspnetcore-jsonpatch-systemtextjson-jsonpatchdocument-applyto\(system-object\)) method generally follows the conventions and options of [System.Text.Json](/en-us/dotnet/api/system.text.json) for processing the [JsonPatchDocument\<TModel>](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument-1), including the behavior controlled by the following options:

* [JsonNumberHandling](/en-us/dotnet/api/system.text.json.serialization.jsonnumberhandling): Whether numeric properties are read from strings.
* [PropertyNameCaseInsensitive](/en-us/dotnet/api/system.text.json.jsonserializeroptions.propertynamecaseinsensitive#system-text-json-jsonserializeroptions-propertynamecaseinsensitive): Whether property names are case-sensitive.

Key differences between [System.Text.Json](/en-us/dotnet/api/system.text.json) and the new [JsonPatchDocument\<TModel>](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument-1) implementation:

* The runtime type of the target object, not the declared type, determines which properties [ApplyTo(Object)](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument.applyto#microsoft-aspnetcore-jsonpatch-systemtextjson-jsonpatchdocument-applyto\(system-object\)) patches.
* [System.Text.Json](/en-us/dotnet/api/system.text.json) deserialization relies on the declared type to identify eligible properties.

### Example: Apply a JsonPatchDocument with error handling

Various errors can occur when applying a JSON Patch document. For example, the target object might not have the specified property, or the value specified might be incompatible with the property type.

JSON `Patch` supports the `test` operation, which checks if a specified value equals the target property. If it doesn't, it returns an error.

The following example demonstrates how to handle these errors gracefully.

Important

The object passed to the [ApplyTo(Object)](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument.applyto#microsoft-aspnetcore-jsonpatch-systemtextjson-jsonpatchdocument-applyto\(system-object\)) method is modified in place. The caller is responsible for discarding changes if any operation fails.

```
// Original object
var person = new Person {
    FirstName = "John",
    LastName = "Doe",
    Email = "johndoe@gmail.com"
};

// Raw JSON patch document
string jsonPatch = """
[
    { "op": "replace", "path": "/Email", "value": "janedoe@gmail.com"},
    { "op": "test", "path": "/FirstName", "value": "Jane" },
    { "op": "replace", "path": "/LastName", "value": "Smith" }
]
""";

// Deserialize the JSON patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);

// Apply the JSON patch document, catching any errors
Dictionary<string, string[]>? errors = null;
patchDoc!.ApplyTo(person, jsonPatchError =>
    {
        errors ??= new ();
        var key = jsonPatchError.AffectedObject.GetType().Name;
        if (!errors.ContainsKey(key))
        {
            errors.Add(key, new string[] { });
        }
        errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
    });
if (errors != null)
{
    // Print the errors
    foreach (var error in errors)
    {
        Console.WriteLine($"Error in {error.Key}: {string.Join(", ", error.Value)}");
    }
}

// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));
```

The previous example results in the following output:

```
Error in Person: The current value 'John' at path 'FirstName' is not equal 
to the test value 'Jane'.
{
    "firstName": "John",
    "lastName": "Smith",              <<< Modified!
    "email": "janedoe@gmail.com",     <<< Modified!
    "phoneNumbers": []
}
```

### Example: Construct a JSON Patch document using strongly-typed lambda expressions

In addition to specifying paths as string literals, [JsonPatchDocument\<TModel>](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument-1) supports defining patch operations using strongly-typed lambda expressions. Strongly-typed lambda expressions:

* Provide compile-time verification that property paths and value types match the model.
* Eliminate runtime typographical errors, such a casing error, that can occur with string-based JSON Pointer paths.
* Automatically update property paths when renaming model properties during refactoring.

The following example demonstrates building a patch document using lambda expressions:

```
var person = new Person
{
    FirstName = "John",
    LastName = "Doe",
    Email = "johndoe@example.com"
};

// Create a strongly typed JSON Patch document
var patchDoc = new JsonPatchDocument<Person>();

// Add operations using lambda expressions
patchDoc.Replace(p => p.FirstName, "Jane");
patchDoc.Replace(p => p.LastName, "Smith");
patchDoc.Replace(p => p.Email, "janesmith@example.com");

// Apply the patch document to the target object
patchDoc.ApplyTo(person);

Console.WriteLine($"{person.FirstName} {person.LastName} ({person.Email})");
```

The previous example results in the following output:

```
Jane Smith (janesmith@example.com)
```

## Mitigating security risks

When using the `Microsoft.AspNetCore.JsonPatch.SystemTextJson` package, it's critical to understand and mitigate potential security risks. The following sections outline the identified security risks associated with JSON Patch and provide recommended mitigations to ensure secure usage of the package.

Important

***This is not an exhaustive list of threats.*** App developers must conduct their own threat model reviews to determine an app-specific comprehensive list and come up with appropriate mitigations as needed. For example, apps which expose collections to patch operations should consider the potential for algorithmic complexity attacks if those operations insert or remove elements at the beginning of the collection.

To minimize security risks when integrating JSON Patch functionality into their apps, developers should:

* Run comprehensive threat models for their own apps.
* Address identified threats.
* Follow the recommended mitigations in the following sections.

### Denial of Service (DoS) via memory amplification

* **Scenario**: A malicious client submits a `copy` operation that duplicates large object graphs multiple times, leading to excessive memory consumption.

* **Impact**: Potential Out-Of-Memory (OOM) conditions, causing service disruptions.

* **Mitigation**:

  * Validate incoming JSON Patch documents for size and structure before calling [ApplyTo(Object)](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson.jsonpatchdocument.applyto#microsoft-aspnetcore-jsonpatch-systemtextjson-jsonpatchdocument-applyto\(system-object\)).
  * The validation must be app specific, but an example validation can look similar to the following:

```
public void Validate(JsonPatchDocument<T> patch)
{
    // This is just an example. It's up to the developer to make sure that
    // this case is handled properly, based on the app needs.
    if (patch.Operations.Where(op=>op.OperationType == OperationType.Copy).Count()
                              > MaxCopyOperationsCount)
    {
        throw new InvalidOperationException();
    }
}
```

### Business logic subversion

* **Scenario**: Patch operations can manipulate fields with implicit invariants (for example, internal flags, IDs, or computed fields), violating business constraints.

* **Impact**: Data integrity issues and unintended app behavior.

* **Mitigation**:

  * Use POCOs (Plain Old CLR Objects) with explicitly defined properties that are safe to modify.

    * Avoid exposing sensitive or security-critical properties in the target object.
    * If a POCO object isn't used, validate the patched object after applying operations to ensure business rules and invariants aren't violated.

### Authentication and authorization

* **Scenario**: Unauthenticated or unauthorized clients send malicious JSON Patch requests.

* **Impact**: Unauthorized access to modify sensitive data or disrupt app behavior.

* **Mitigation**:

  * Protect endpoints that accept JSON Patch requests by using proper authentication and authorization mechanisms.
  * Restrict access to trusted clients or users with appropriate permissions.

## Get the code

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/jsonpatch/samples/10.x/JsonPatchSample). ([How to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

To test the sample, run the app and send HTTP requests by using the included `.http` file.

## Related content

* [IETF RFC 5789 PATCH method specification](https://tools.ietf.org/html/rfc5789)
* [IETF RFC 6902 JSON Patch specification](https://tools.ietf.org/html/rfc6902)
* [IETF RFC 6901 JSON Pointer](https://tools.ietf.org/html/rfc6901)
* [ASP.NET Core JSON Patch source code](https://github.com/dotnet/aspnetcore/tree/main/src/Features/JsonPatch.SystemTextJson/src)

This article explains how to handle JSON Patch requests in an ASP.NET Core web API.

Important

The JSON Patch standard has ***inherent security risks***. This implementation ***doesn't attempt to mitigate these inherent security risks***. It's the responsibility of the developer to ensure that the JSON Patch document is safe to apply to the target object. For more information, see the [Mitigating Security Risks](#mitigating-security-risks) section.

## Package installation

JSON Patch support in ASP.NET Core web API is based on `Newtonsoft.Json` and requires the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package.

To enable JSON Patch support:

* Install the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package.

* Call [AddNewtonsoftJson](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.newtonsoftjsonmvcbuilderextensions.addnewtonsoftjson). For example:

  ```
  var builder = WebApplication.CreateBuilder(args);

  builder.Services.AddControllers()
      .AddNewtonsoftJson();

  var app = builder.Build();

  app.UseHttpsRedirection();

  app.UseAuthorization();

  app.MapControllers();

  app.Run();
  ```

`AddNewtonsoftJson` replaces the default `System.Text.Json`-based input and output formatters used for formatting ***all*** JSON content. This extension method is compatible with the following MVC service registration methods:

* [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages)
* [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews)
* [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers)

JsonPatch requires setting the `Content-Type` header to `application/json-patch+json`.

## Add support for JSON Patch when using System.Text.Json

The `System.Text.Json`-based input formatter doesn't support JSON Patch. To add support for JSON Patch using `Newtonsoft.Json`, while leaving the other input and output formatters unchanged:

* Install the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package.

* Update `Program.cs`:

  ```
  using JsonPatchSample;
  using Microsoft.AspNetCore.Mvc.Formatters;

  var builder = WebApplication.CreateBuilder(args);

  builder.Services.AddControllers(options =>
  {
      options.InputFormatters.Insert(0, MyJPIF.GetJsonPatchInputFormatter());
  });

  var app = builder.Build();

  app.UseHttpsRedirection();

  app.UseAuthorization();

  app.MapControllers();

  app.Run();
  ```

  ```
  using Microsoft.AspNetCore.Mvc;
  using Microsoft.AspNetCore.Mvc.Formatters;
  using Microsoft.Extensions.Options;

  namespace JsonPatchSample;

  public static class MyJPIF
  {
      public static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
      {
          var builder = new ServiceCollection()
              .AddLogging()
              .AddMvc()
              .AddNewtonsoftJson()
              .Services.BuildServiceProvider();

          return builder
              .GetRequiredService<IOptions<MvcOptions>>()
              .Value
              .InputFormatters
              .OfType<NewtonsoftJsonPatchInputFormatter>()
              .First();
      }
  }
  ```

The preceding code creates an instance of [NewtonsoftJsonPatchInputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.newtonsoftjsonpatchinputformatter) and inserts it as the first entry in the [MvcOptions.InputFormatters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.inputformatters) collection. This order of registration ensures that:

* `NewtonsoftJsonPatchInputFormatter` processes JSON Patch requests.
* The existing `System.Text.Json`-based input and formatters process all other JSON requests and responses.

Use the `Newtonsoft.Json.JsonConvert.SerializeObject` method to serialize a [JsonPatchDocument](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.jsonpatchdocument).

## PATCH HTTP request method

The PUT and [PATCH](https://tools.ietf.org/html/rfc5789) methods are used to update an existing resource. The difference between them is that PUT replaces the entire resource, while PATCH specifies only the changes.

## JSON Patch

[JSON Patch](https://tools.ietf.org/html/rfc6902) is a format for specifying updates to be applied to a resource. A JSON Patch document has an array of *operations*. Each operation identifies a particular type of change. Examples of such changes include adding an array element or replacing a property value.

For example, the following JSON documents represent a resource, a JSON Patch document for the resource, and the result of applying the Patch operations.

### Resource example

```
{
  "customerName": "John",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    }
  ]
}
```

### JSON patch example

```
[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]
```

In the preceding JSON:

* The `op` property indicates the type of operation.
* The `path` property indicates the element to update.
* The `value` property provides the new value.

### Resource after patch

Here's the resource after applying the preceding JSON Patch document:

```
{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}
```

The changes made by applying a JSON Patch document to a resource are atomic. If any operation in the list fails, no operation in the list is applied.

## Path syntax

The [path](https://tools.ietf.org/html/rfc6901) property of an operation object has slashes between levels. For example, `"/address/zipCode"`.

Zero-based indexes are used to specify array elements. The first element of the `addresses` array would be at `/addresses/0`. To `add` to the end of an array, use a hyphen (`-`) rather than an index number: `/addresses/-`.

### Operations

The following table shows supported operations as defined in the [JSON Patch specification](https://tools.ietf.org/html/rfc6902).

| Operation | Notes                                                                                  |
| --------- | -------------------------------------------------------------------------------------- |
| `add`     | Add a property or array element. For existing property: set value.                     |
| `remove`  | Remove a property or array element.                                                    |
| `replace` | Same as `remove` followed by `add` at same location.                                   |
| `move`    | Same as `remove` from source followed by `add` to destination using value from source. |
| `copy`    | Same as `add` to destination using value from source.                                  |
| `test`    | Return success status code if value at `path` = provided `value`.                      |

## JSON Patch in ASP.NET Core

The ASP.NET Core implementation of JSON Patch is provided in the [Microsoft.AspNetCore.JsonPatch](https://www.nuget.org/packages/microsoft.aspnetcore.jsonpatch/) NuGet package.

## Action method code

In an API controller, an action method for JSON Patch:

* Is annotated with the `HttpPatch` attribute.
* Accepts a [JsonPatchDocument\<TModel>](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.jsonpatchdocument-1), typically with [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute).
* Calls [ApplyTo(Object)](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.jsonpatchdocument.applyto#microsoft-aspnetcore-jsonpatch-jsonpatchdocument-applyto\(system-object\)) on the patch document to apply the changes.

Here's an example:

```
[HttpPatch]
public IActionResult JsonPatchWithModelState(
    [FromBody] JsonPatchDocument<Customer> patchDoc)
{
    if (patchDoc != null)
    {
        var customer = CreateCustomer();

        patchDoc.ApplyTo(customer, ModelState);

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return new ObjectResult(customer);
    }
    else
    {
        return BadRequest(ModelState);
    }
}
```

This code from the sample app works with the following `Customer` model:

```
namespace JsonPatchSample.Models;

public class Customer
{
    public string? CustomerName { get; set; }
    public List<Order>? Orders { get; set; }
}
```

```
namespace JsonPatchSample.Models;

public class Order
{
    public string OrderName { get; set; }
    public string OrderType { get; set; }
}
```

The sample action method:

* Constructs a `Customer`.
* Applies the patch.
* Returns the result in the body of the response.

In a real app, the code would retrieve the data from a store such as a database and update the database after applying the patch.

### Model state

The preceding action method example calls an overload of `ApplyTo` that takes model state as one of its parameters. With this option, you can get error messages in responses. The following example shows the body of a 400 Bad Request response for a `test` operation:

```
{
  "Customer": [
    "The current value 'John' at path 'customerName' != test value 'Nancy'."
  ]
}
```

### Dynamic objects

The following action method example shows how to apply a patch to a dynamic object:

```
[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
    dynamic obj = new ExpandoObject();
    patch.ApplyTo(obj);

    return Ok(obj);
}
```

## The add operation

* If `path` points to an array element: inserts new element before the one specified by `path`.

* If `path` points to a property: sets the property value.

* If `path` points to a nonexistent location:

  * If the resource to patch is a dynamic object: adds a property.
  * If the resource to patch is a static object: the request fails.

The following sample patch document sets the value of `CustomerName` and adds an `Order` object to the end of the `Orders` array.

```
[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]
```

## The remove operation

* If `path` points to an array element: removes the element.

* If `path` points to a property:

  * If resource to patch is a dynamic object: removes the property.

  * If resource to patch is a static object:

    * If the property is nullable: sets it to null.
    * If the property is non-nullable, sets it to `default<T>`.

The following sample patch document sets `CustomerName` to null and deletes `Orders[0]`:

```
[
  {
    "op": "remove",
    "path": "/customerName"
  },
  {
    "op": "remove",
    "path": "/orders/0"
  }
]
```

## The replace operation

This operation is functionally the same as a `remove` followed by an `add`.

The following sample patch document sets the value of `CustomerName` and replaces `Orders[0]`with a new `Order` object:

```
[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]
```

## The move operation

* If `path` points to an array element: copies `from` element to location of `path` element, then runs a `remove` operation on the `from` element.

* If `path` points to a property: copies value of `from` property to `path` property, then runs a `remove` operation on the `from` property.

* If `path` points to a nonexistent property:

  * If the resource to patch is a static object: the request fails.
  * If the resource to patch is a dynamic object: copies `from` property to location indicated by `path`, then runs a `remove` operation on the `from` property.

The following sample patch document:

* Copies the value of `Orders[0].OrderName` to `CustomerName`.
* Sets `Orders[0].OrderName` to null.
* Moves `Orders[1]` to before `Orders[0]`.

```
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]
```

## The copy operation

This operation is functionally the same as a `move` operation without the final `remove` step.

The following sample patch document:

* Copies the value of `Orders[0].OrderName` to `CustomerName`.
* Inserts a copy of `Orders[1]` before `Orders[0]`.

```
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]
```

## The test operation

If the value at the location indicated by `path` is different from the value provided in `value`, the request fails. In that case, the whole PATCH request fails even if all other operations in the patch document would otherwise succeed.

The `test` operation is commonly used to prevent an update when there's a concurrency conflict.

The following sample patch document has no effect if the initial value of `CustomerName` is "John", because the test fails:

```
[
  {
    "op": "test",
    "path": "/customerName",
    "value": "Nancy"
  },
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  }
]
```

## Get the code

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/jsonpatch/samples). ([How to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

To test the sample, run the app and send HTTP requests with the following settings:

* URL: `http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate`
* HTTP method: `PATCH`
* Header: `Content-Type: application/json-patch+json`
* Body: Copy and paste one of the JSON patch document samples from the *JSON* project folder.

## Mitigating security risks

When using the `Microsoft.AspNetCore.JsonPatch` package with the `Newtonsoft.Json`-based implementation, it's critical to understand and mitigate potential security risks. The following sections outline the identified security risks associated with JSON Patch and provide recommended mitigations to ensure secure usage of the package.

Important

***This is not an exhaustive list of threats.*** App developers must conduct their own threat model reviews to determine an app-specific comprehensive list and come up with appropriate mitigations as needed. For example, apps which expose collections to patch operations should consider the potential for algorithmic complexity attacks if those operations insert or remove elements at the beginning of the collection.

By running comprehensive threat models for their own apps and addressing identified threats while following the recommended mitigations below, consumers of these packages can integrate JSON Patch functionality into their apps while minimizing security risks.

### Denial of Service (DoS) via memory amplification

* **Scenario**: A malicious client submits a `copy` operation that duplicates large object graphs multiple times, leading to excessive memory consumption.

* **Impact**: Potential Out-Of-Memory (OOM) conditions, causing service disruptions.

* **Mitigation**:

  * Validate incoming JSON Patch documents for size and structure before calling `ApplyTo`.
  * The validation needs to be app specific, but an example validation can look similar to the following:

```
public void Validate(JsonPatchDocument patch)
{
    // This is just an example. It's up to the developer to make sure that
    // this case is handled properly, based on the app needs.
    if (patch.Operations.Where(op => op.OperationType == OperationType.Copy).Count()
                              > MaxCopyOperationsCount)
    {
        throw new InvalidOperationException();
    }
}
```

### Business Logic Subversion

* **Scenario**: Patch operations can manipulate fields with implicit invariants (for example, internal flags, IDs, or computed fields), violating business constraints.

* **Impact**: Data integrity issues and unintended app behavior.

* **Mitigation**:

  * Use POCO objects with explicitly defined properties that are safe to modify.
  * Avoid exposing sensitive or security-critical properties in the target object.
  * If no POCO object is used, validate the patched object after applying operations to ensure business rules and invariants aren't violated.

### Authentication and authorization

* **Scenario**: Unauthenticated or unauthorized clients send malicious JSON Patch requests.

* **Impact**: Unauthorized access to modify sensitive data or disrupt app behavior.

* **Mitigation**:

  * Protect endpoints accepting JSON Patch requests with proper authentication and authorization mechanisms.
  * Restrict access to trusted clients or users with appropriate permissions.

## Related content

* [IETF RFC 5789 PATCH method specification](https://tools.ietf.org/html/rfc5789)
* [IETF RFC 6902 JSON Patch specification](https://tools.ietf.org/html/rfc6902)
* [IETF RFC 6901 JSON Pointer](https://tools.ietf.org/html/rfc6901)
* [ASP.NET Core JSON Patch source code](https://github.com/dotnet/AspNetCore/tree/main/src/Features/JsonPatch/src)

This article explains how to handle JSON Patch requests in an ASP.NET Core web API.

Important

The JSON Patch standard has ***inherent security risks***. Since these risks are inherent to the JSON Patch standard, this implementation ***doesn't attempt to mitigate inherent security risks***. It's the responsibility of the developer to ensure that the JSON Patch document is safe to apply to the target object. For more information, see the [Mitigating Security Risks](#mitigating-security-risks) section.

## Package installation

To enable JSON Patch support in your app, complete the following steps:

1. Install the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package.

2. Update the project's `Startup.ConfigureServices` method to call [AddNewtonsoftJson](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.newtonsoftjsonmvcbuilderextensions.addnewtonsoftjson). For example:

   ```
   services
       .AddControllersWithViews()
       .AddNewtonsoftJson();
   ```

`AddNewtonsoftJson` is compatible with the MVC service registration methods:

* [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages)
* [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews)
* [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers)

## JSON Patch, AddNewtonsoftJson, and System.Text.Json

`AddNewtonsoftJson` replaces the `System.Text.Json`-based input and output formatters used for formatting **all** JSON content. To add support for JSON Patch using `Newtonsoft.Json`, while leaving the other formatters unchanged, update the project's `Startup.ConfigureServices` method as follows:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        options.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
    });
}

private static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
{
    var builder = new ServiceCollection()
        .AddLogging()
        .AddMvc()
        .AddNewtonsoftJson()
        .Services.BuildServiceProvider();

    return builder
        .GetRequiredService<IOptions<MvcOptions>>()
        .Value
        .InputFormatters
        .OfType<NewtonsoftJsonPatchInputFormatter>()
        .First();
}
```

The preceding code requires the `Microsoft.AspNetCore.Mvc.NewtonsoftJson` package and the following `using` statements:

```
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System.Linq;
```

Use the `Newtonsoft.Json.JsonConvert.SerializeObject` method to serialize a JsonPatchDocument.

## PATCH HTTP request method

The PUT and [PATCH](https://tools.ietf.org/html/rfc5789) methods are used to update an existing resource. The difference between them is that PUT replaces the entire resource, while PATCH specifies only the changes.

## JSON Patch

[JSON Patch](https://tools.ietf.org/html/rfc6902) is a format for specifying updates to be applied to a resource. A JSON Patch document has an array of *operations*. Each operation identifies a particular type of change. Examples of such changes include adding an array element or replacing a property value.

For example, the following JSON documents represent a resource, a JSON Patch document for the resource, and the result of applying the Patch operations.

### Resource example

```
{
  "customerName": "John",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    }
  ]
}
```

### JSON patch example

```
[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]
```

In the preceding JSON:

* The `op` property indicates the type of operation.
* The `path` property indicates the element to update.
* The `value` property provides the new value.

### Resource after patch

Here's the resource after applying the preceding JSON Patch document:

```
{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}
```

The changes made by applying a JSON Patch document to a resource are atomic. If any operation in the list fails, no operation in the list is applied.

## Path syntax

The [path](https://tools.ietf.org/html/rfc6901) property of an operation object has slashes between levels. For example, `"/address/zipCode"`.

Zero-based indexes are used to specify array elements. The first element of the `addresses` array would be at `/addresses/0`. To `add` to the end of an array, use a hyphen (`-`) rather than an index number: `/addresses/-`.

### Operations

The following table shows supported operations as defined in the [JSON Patch specification](https://tools.ietf.org/html/rfc6902).

| Operation | Notes                                                                                  |
| --------- | -------------------------------------------------------------------------------------- |
| `add`     | Add a property or array element. For existing property: set value.                     |
| `remove`  | Remove a property or array element.                                                    |
| `replace` | Same as `remove` followed by `add` at same location.                                   |
| `move`    | Same as `remove` from source followed by `add` to destination using value from source. |
| `copy`    | Same as `add` to destination using value from source.                                  |
| `test`    | Return success status code if value at `path` = provided `value`.                      |

## JSON Patch in ASP.NET Core

The ASP.NET Core implementation of JSON Patch is provided in the [Microsoft.AspNetCore.JsonPatch](https://www.nuget.org/packages/microsoft.aspnetcore.jsonpatch/) NuGet package.

## Action method code

In an API controller, an action method for JSON Patch:

* Is annotated with the `HttpPatch` attribute.
* Accepts a `JsonPatchDocument<T>`, typically with `[FromBody]`.
* Calls `ApplyTo` on the patch document to apply the changes.

Here's an example:

```
[HttpPatch]
public IActionResult JsonPatchWithModelState(
    [FromBody] JsonPatchDocument<Customer> patchDoc)
{
    if (patchDoc != null)
    {
        var customer = CreateCustomer();

        patchDoc.ApplyTo(customer, ModelState);

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return new ObjectResult(customer);
    }
    else
    {
        return BadRequest(ModelState);
    }
}
```

This code from the sample app works with the following `Customer` model:

```
using System.Collections.Generic;

namespace JsonPatchSample.Models
{
    public class Customer
    {
        public string CustomerName { get; set; }
        public List<Order> Orders { get; set; }
    }
}
```

```
namespace JsonPatchSample.Models
{
    public class Order
    {
        public string OrderName { get; set; }
        public string OrderType { get; set; }
    }
}
```

The sample action method:

* Constructs a `Customer`.
* Applies the patch.
* Returns the result in the body of the response.

In a real app, the code would retrieve the data from a store such as a database and update the database after applying the patch.

### Model state

The preceding action method example calls an overload of `ApplyTo` that takes model state as one of its parameters. With this option, you can get error messages in responses. The following example shows the body of a 400 Bad Request response for a `test` operation:

```
{
    "Customer": [
        "The current value 'John' at path 'customerName' is not equal to the test value 'Nancy'."
    ]
}
```

### Dynamic objects

The following action method example shows how to apply a patch to a dynamic object:

```
[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
    dynamic obj = new ExpandoObject();
    patch.ApplyTo(obj);

    return Ok(obj);
}
```

## The add operation

* If `path` points to an array element: inserts new element before the one specified by `path`.

* If `path` points to a property: sets the property value.

* If `path` points to a nonexistent location:

  * If the resource to patch is a dynamic object: adds a property.
  * If the resource to patch is a static object: the request fails.

The following sample patch document sets the value of `CustomerName` and adds an `Order` object to the end of the `Orders` array.

```
[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]
```

## The remove operation

* If `path` points to an array element: removes the element.

* If `path` points to a property:

  * If resource to patch is a dynamic object: removes the property.

  * If resource to patch is a static object:

    * If the property is nullable: sets it to null.
    * If the property is non-nullable, sets it to `default<T>`.

The following sample patch document sets `CustomerName` to null and deletes `Orders[0]`:

```
[
  {
    "op": "remove",
    "path": "/customerName"
  },
  {
    "op": "remove",
    "path": "/orders/0"
  }
]
```

## The replace operation

This operation is functionally the same as a `remove` followed by an `add`.

The following sample patch document sets the value of `CustomerName` and replaces `Orders[0]`with a new `Order` object:

```
[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]
```

## The move operation

* If `path` points to an array element: copies `from` element to location of `path` element, then runs a `remove` operation on the `from` element.

* If `path` points to a property: copies value of `from` property to `path` property, then runs a `remove` operation on the `from` property.

* If `path` points to a nonexistent property:

  * If the resource to patch is a static object: the request fails.
  * If the resource to patch is a dynamic object: copies `from` property to location indicated by `path`, then runs a `remove` operation on the `from` property.

The following sample patch document:

* Copies the value of `Orders[0].OrderName` to `CustomerName`.
* Sets `Orders[0].OrderName` to null.
* Moves `Orders[1]` to before `Orders[0]`.

```
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]
```

## The copy operation

This operation is functionally the same as a `move` operation without the final `remove` step.

The following sample patch document:

* Copies the value of `Orders[0].OrderName` to `CustomerName`.
* Inserts a copy of `Orders[1]` before `Orders[0]`.

```
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]
```

## The test operation

If the value at the location indicated by `path` is different from the value provided in `value`, the request fails. In that case, the whole PATCH request fails even if all other operations in the patch document would otherwise succeed.

The `test` operation is commonly used to prevent an update when there's a concurrency conflict.

The following sample patch document has no effect if the initial value of `CustomerName` is "John", because the test fails:

```
[
  {
    "op": "test",
    "path": "/customerName",
    "value": "Nancy"
  },
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  }
]
```

## Get the code

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/jsonpatch/samples). ([How to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

To test the sample, run the app and send HTTP requests with the following settings:

* URL: `http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate`
* HTTP method: `PATCH`
* Header: `Content-Type: application/json-patch+json`
* Body: Copy and paste one of the JSON patch document samples from the *JSON* project folder.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/default-settings
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/configuration/default-settings.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Data Protection key management and lifetime in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

## Key management

The app attempts to detect its operational environment and handle key configuration on its own.

1. If the app is hosted in [Azure Apps](https://azure.microsoft.com/services/app-service/), keys are persisted to the *%HOME%\ASP.NET\DataProtection-Keys* folder. This folder is backed by network storage and is synchronized across all machines hosting the app.

   * Keys aren't protected at rest.
   * The *DataProtection-Keys* folder supplies the key ring to all instances of an app in a single deployment slot.
   * Separate deployment slots, such as Staging and Production, don't share a key ring. When you swap between deployment slots, for example swapping Staging to Production or using A/B testing, any app using Data Protection won't be able to decrypt stored data using the key ring inside the previous slot. This leads to users being logged out of an app that uses the standard ASP.NET Core cookie authentication, as it uses Data Protection to protect its cookies. If you desire slot-independent key rings, use an external key ring provider, such as Azure Blob Storage, Azure Key Vault, a SQL store, or Redis cache.

2. If the user profile is available, keys are persisted to the *%LOCALAPPDATA%\ASP.NET\DataProtection-Keys* folder. If the operating system is Windows, the keys are encrypted at rest using DPAPI.

   The app pool's [setProfileEnvironment attribute](/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel#configuration) must also be enabled. The default value of `setProfileEnvironment` is `true`. In some scenarios (for example, Windows OS), `setProfileEnvironment` is set to `false`. If keys aren't stored in the user profile directory as expected:

   1. Navigate to the *%windir%/system32/inetsrv/config* folder.
   2. Open the *applicationHost.config* file.
   3. Locate the `<system.applicationHost><applicationPools><applicationPoolDefaults><processModel>` element.
   4. Confirm that the `setProfileEnvironment` attribute isn't present, which defaults the value to `true`, or explicitly set the attribute's value to `true`.

3. If the app is hosted in IIS, keys are persisted to the HKLM registry in a special registry key that's ACLed only to the worker process account. Keys are encrypted at rest using DPAPI.

4. If none of these conditions match, keys aren't persisted outside of the current process. When the process shuts down, all generated keys are lost.

The developer is always in full control and can override how and where keys are stored. The first three options above should provide good defaults for most apps similar to how the ASP.NET **\<machineKey>** auto-generation routines worked in the past. The final, fallback option is the only scenario that requires the developer to specify [configuration](overview?view=aspnetcore-10.0) upfront if they want key persistence, but this fallback only occurs in rare situations.

When hosting in a Docker container, keys should be persisted in a folder that's a Docker volume (a shared volume or a host-mounted volume that persists beyond the container's lifetime) or in an external provider, such as [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) or [Redis](https://redis.io/). An external provider is also useful in web farm scenarios if apps can't access a shared network volume (see [PersistKeysToFileSystem](overview?view=aspnetcore-10.0#persist-keys-to-the-file-system-persistkeystofilesystem) for more information).

Warning

If the developer overrides the rules outlined above and points the Data Protection system at a specific key repository, automatic encryption of keys at rest is disabled. At-rest protection can be re-enabled via [configuration](overview?view=aspnetcore-10.0).

## Key lifetime

Keys have a 90-day lifetime by default. When a key expires, the app automatically generates a new key and sets the new key as the active key. As long as retired keys remain on the system, your app can decrypt any data protected with them. See [key management](../implementation/key-management?view=aspnetcore-10.0#key-expiration-and-rolling) for more information.

## Default algorithms

The default payload protection algorithm used is AES-256-CBC for confidentiality and HMACSHA256 for authenticity. A 512-bit master key, changed every 90 days, is used to derive the two sub-keys used for these algorithms on a per-payload basis. See [subkey derivation](../implementation/subkeyderivation?view=aspnetcore-10.0#additional-authenticated-data-and-subkey-derivation) for more information.

### Delete keys

Deleting a key makes its protected data permanently inaccessible. To mitigate that risk, we recommend not deleting keys. The resulting accumulation of keys generally has minimal impact because they're small. In exceptional cases, such as extremely long-running services, keys can be deleted. Only delete keys:

* That are old (no longer in use).
* When you can accept the risk of data loss in exchange for storage savings.

We recommend not deleting data protection keys.

```
using Microsoft.AspNetCore.DataProtection.KeyManagement;

var services = new ServiceCollection();
services.AddDataProtection();

var serviceProvider = services.BuildServiceProvider();

var keyManager = serviceProvider.GetService<IKeyManager>();

if (keyManager is IDeletableKeyManager deletableKeyManager)
{
    var utcNow = DateTimeOffset.UtcNow;
    var yearAgo = utcNow.AddYears(-1);

    if (!deletableKeyManager.DeleteKeys(key => key.ExpirationDate < yearAgo))
    {
        Console.WriteLine("Failed to delete keys.");
    }
    else
    {
        Console.WriteLine("Old keys deleted successfully.");
    }
}
else
{
    Console.WriteLine("Key manager does not support deletion.");
}
```

## Related content

* [Key management extensibility in ASP.NET Core](../extensibility/key-management?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../../host-and-deploy/web-farm?view=aspnetcore-10.0)

## Key management

The app attempts to detect its operational environment and handle key configuration on its own.

1. If the app is hosted in [Azure Apps](https://azure.microsoft.com/services/app-service/), keys are persisted to the *%HOME%\ASP.NET\DataProtection-Keys* folder. This folder is backed by network storage and is synchronized across all machines hosting the app.

   * Keys aren't protected at rest.
   * The *DataProtection-Keys* folder supplies the key ring to all instances of an app in a single deployment slot.
   * Separate deployment slots, such as Staging and Production, don't share a key ring. When you swap between deployment slots, for example swapping Staging to Production or using A/B testing, any app using Data Protection won't be able to decrypt stored data using the key ring inside the previous slot. This leads to users being logged out of an app that uses the standard ASP.NET Core cookie authentication, as it uses Data Protection to protect its cookies. If you desire slot-independent key rings, use an external key ring provider, such as Azure Blob Storage, Azure Key Vault, a SQL store, or Redis cache.

2. If the user profile is available, keys are persisted to the *%LOCALAPPDATA%\ASP.NET\DataProtection-Keys* folder. If the operating system is Windows, the keys are encrypted at rest using DPAPI.

   The app pool's [setProfileEnvironment attribute](/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel#configuration) must also be enabled. The default value of `setProfileEnvironment` is `true`. In some scenarios (for example, Windows OS), `setProfileEnvironment` is set to `false`. If keys aren't stored in the user profile directory as expected:

   1. Navigate to the *%windir%/system32/inetsrv/config* folder.
   2. Open the *applicationHost.config* file.
   3. Locate the `<system.applicationHost><applicationPools><applicationPoolDefaults><processModel>` element.
   4. Confirm that the `setProfileEnvironment` attribute isn't present, which defaults the value to `true`, or explicitly set the attribute's value to `true`.

3. If the app is hosted in IIS, keys are persisted to the HKLM registry in a special registry key that's ACLed only to the worker process account. Keys are encrypted at rest using DPAPI.

4. If none of these conditions match, keys aren't persisted outside of the current process. When the process shuts down, all generated keys are lost.

The developer is always in full control and can override how and where keys are stored. The first three options above should provide good defaults for most apps similar to how the ASP.NET **\<machineKey>** auto-generation routines worked in the past. The final, fallback option is the only scenario that requires the developer to specify [configuration](overview?view=aspnetcore-10.0) upfront if they want key persistence, but this fallback only occurs in rare situations.

When hosting in a Docker container, keys should be persisted in a folder that's a Docker volume (a shared volume or a host-mounted volume that persists beyond the container's lifetime) or in an external provider, such as [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) or [Redis](https://redis.io/). An external provider is also useful in web farm scenarios if apps can't access a shared network volume (see [PersistKeysToFileSystem](overview?view=aspnetcore-10.0#persist-keys-to-the-file-system-persistkeystofilesystem) for more information).

Warning

If the developer overrides the rules outlined above and points the Data Protection system at a specific key repository, automatic encryption of keys at rest is disabled. At-rest protection can be re-enabled via [configuration](overview?view=aspnetcore-10.0).

## Key lifetime

Keys have a 90-day lifetime by default. When a key expires, the app automatically generates a new key and sets the new key as the active key. As long as retired keys remain on the system, your app can decrypt any data protected with them. See [key management](../implementation/key-management?view=aspnetcore-10.0#key-expiration-and-rolling) for more information.

## Default algorithms

The default payload protection algorithm used is AES-256-CBC for confidentiality and HMACSHA256 for authenticity. A 512-bit master key, changed every 90 days, is used to derive the two sub-keys used for these algorithms on a per-payload basis. See [subkey derivation](../implementation/subkeyderivation?view=aspnetcore-10.0#additional-authenticated-data-and-subkey-derivation) for more information.

## Related content

* [Key management extensibility in ASP.NET Core](../extensibility/key-management?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../../host-and-deploy/web-farm?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-06-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/libman/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/libman/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Client-side library acquisition in ASP.NET Core with LibMan

By [Scott Addie](https://twitter.com/Scott_Addie)

Library Manager (LibMan) is a lightweight, client-side library acquisition tool. LibMan downloads popular libraries and frameworks from the file system or from a [content delivery network (CDN)](https://wikipedia.org/wiki/Content_delivery_network). The supported CDNs include [CDNJS](https://cdnjs.com/), [jsDelivr](https://www.jsdelivr.com/), and [unpkg](https://unpkg.com/#/). The selected library files are fetched and placed in the appropriate location within the ASP.NET Core project.

## LibMan use cases

LibMan offers the following benefits:

* Only the library files you need are downloaded.
* Additional tooling, such as [Node.js](https://nodejs.org), [npm](https://www.npmjs.com), and [WebPack](https://webpack.js.org), isn't necessary to acquire a subset of files in a library.
* Files can be placed in a specific location without resorting to build tasks or manual file copying.

LibMan isn't a package management system. If you're already using a package manager, such as npm or [yarn](https://yarnpkg.com), continue doing so. LibMan wasn't developed to replace those tools.

## Related content

* [Use LibMan with ASP.NET Core in Visual Studio](libman-vs?view=aspnetcore-10.0)
* [Use the LibMan CLI with ASP.NET Core](libman-cli?view=aspnetcore-10.0)
* [LibMan GitHub repository](https://github.com/aspnet/LibraryManager)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/read-related-data
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-rp/read-related-data.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 6, Razor Pages with EF Core in ASP.NET Core - Read Related Data

By [Tom Dykstra](https://github.com/tdykstra), [Jon P Smith](https://twitter.com/thereformedprog), and [Rick Anderson](https://twitter.com/RickAndMSFT)

The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. For information about the tutorial series, see [the first tutorial](intro?view=aspnetcore-10.0).

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) and compare that code to what you created by following the tutorial.

This tutorial shows how to read and display related data. Related data is data that EF Core loads into navigation properties.

The following illustrations show the completed pages for this tutorial:

## Eager, explicit, and lazy loading

EF Core can load related data into the navigation properties of an entity in several ways:

* [Eager loading](/en-us/ef/core/querying/related-data#eager-loading). Eager loading is when a query for one type of entity also loads related entities. When you read an entity, EF Core retrieves its related data. This approach typically results in a single join query that retrieves all of the data you need. EF Core issues multiple queries for some types of eager loading. Issuing multiple queries can be more efficient than a large single query. Specify eager loading with the [Include](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.include) and [ThenInclude](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.theninclude) methods.

  Eager loading sends multiple queries when a collection navigation is included:

  * One query for the main query
  * One query for each collection "edge" in the load tree.

* Separate queries with `Load`: You can retrieve the data in separate queries, and EF Core "fixes up" the navigation properties. "Fixes up" means that EF Core automatically populates the navigation properties. Separate queries with `Load` is more like explicit loading than eager loading.

  **Note:** EF Core automatically fixes up navigation properties to any other entities that it previously loaded into the context instance. Even if you don't explicitly include the data for a navigation property, the property might still be populated if some or all of the related entities were previously loaded.

* [Explicit loading](/en-us/ef/core/querying/related-data#explicit-loading). When you first read the entity, EF Core doesn't retrieve related data. You must write code to retrieve the related data when it's needed. Explicit loading with separate queries results in multiple queries sent to the database. With explicit loading, you specify the navigation properties to load. Use the `Load` method to do explicit loading. For example:

* [Lazy loading](/en-us/ef/core/querying/related-data#lazy-loading). When you first read the entity, EF Core doesn't retrieve related data. The first time you access a navigation property, EF Core automatically retrieves the data required for that navigation property. EF Core sends a query to the database each time you access a navigation property for the first time. Lazy loading can hurt performance, for example when developers use [N+1 queries](https://www.bing.com/search?q=N%2B1+queries). N+1 queries load a parent and enumerate through children.

## Create Course pages

The `Course` entity includes a navigation property that contains the related `Department` entity.

To display the name of the assigned department for a course:

* Load the related `Department` entity into the `Course.Department` navigation property.
* Get the name from the `Department` entity's `Name` property.

[]()

### Scaffold Course pages

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

- Follow the instructions in [Scaffold Student pages](intro?view=aspnetcore-10.0#scaffold-student-pages) with the following exceptions:

  * Create a *Pages/Courses* folder.
  * Use `Course` for the model class.
  * Use the existing context class instead of creating a new one.

* Create a *Pages/Courses* folder.

* Run the following command to scaffold the Course pages.

  **On Windows:**

  ```
  dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages\Courses --referenceScriptLibraries
  ```

  **On Linux or macOS:**

  ```
  dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages/Courses --referenceScriptLibraries
  ```

- Open `Pages/Courses/Index.cshtml.cs` and examine the `OnGetAsync` method. The scaffolding engine specified eager loading for the `Department` navigation property. The `Include` method specifies eager loading.

- Run the app and select the **Courses** link. The department column displays the `DepartmentID`, which isn't useful.

### Display the department name

Update Pages/Courses/Index.cshtml.cs with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public IList<Course> Courses { get; set; }

        public async Task OnGetAsync()
        {
            Courses = await _context.Courses
                .Include(c => c.Department)
                .AsNoTracking()
                .ToListAsync();
        }
    }
}
```

The preceding code changes the `Course` property to `Courses` and adds `AsNoTracking`.

No-tracking queries are useful when the results are used in a read-only scenario. They're generally quicker to execute because there's no need to set up the change tracking information. If the entities retrieved from the database don't need to be updated, then a no-tracking query is likely to perform better than a tracking query.

In some cases a tracking query is more efficient than a no-tracking query. For more information, see [Tracking vs. No-Tracking Queries](/en-us/ef/core/querying/tracking). In the preceding code, `AsNoTracking` is called because the entities aren't updated in the current context.

Update `Pages/Courses/Index.cshtml` with the following code.

```
@page
@model ContosoUniversity.Pages.Courses.IndexModel

@{
    ViewData["Title"] = "Courses";
}

<h1>Courses</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].CourseID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Credits)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Department)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Courses)
{
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.CourseID)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Credits)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Department.Name)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

The following changes have been made to the scaffolded code:

* Changed the `Course` property name to `Courses`.

* Added a **Number** column that shows the `CourseID` property value. By default, primary keys aren't scaffolded because normally they're meaningless to end users. However, in this case the primary key is meaningful.

* Changed the **Department** column to display the department name. The code displays the `Name` property of the `Department` entity that's loaded into the `Department` navigation property:

  ```
  @Html.DisplayFor(modelItem => item.Department.Name)
  ```

Run the app and select the **Courses** tab to see the list with department names.

[]()

### Loading related data with Select

The `OnGetAsync` method loads related data with the `Include` method. The `Select` method is an alternative that loads only the related data needed. For single items, like the `Department.Name` it uses a `SQL INNER JOIN`. For collections, it uses another database access, but so does the `Include` operator on collections.

Note

The `Select` method used here is a LINQ **projection** operator. It transforms each element in the source sequence into a new form—in this case, a `CourseViewModel`. Because the query projects into a non-entity type, EF Core generates a SQL query that retrieves only the columns needed for the projection, and no entity tracking is performed.

The following code loads related data with the `Select` method:

```
public IList<CourseViewModel> CourseVM { get; set; }

public async Task OnGetAsync()
{
    CourseVM = await _context.Courses
    .Select(p => new CourseViewModel
    {
        CourseID = p.CourseID,
        Title = p.Title,
        Credits = p.Credits,
        DepartmentName = p.Department.Name
    }).ToListAsync();
}
```

The preceding code doesn't return any entity types, therefore no tracking is done. For more information about the EF tracking, see [Tracking vs. No-Tracking Queries](/en-us/ef/core/querying/tracking).

The `CourseViewModel`:

```
public class CourseViewModel
{
    public int CourseID { get; set; }
    public string Title { get; set; }
    public int Credits { get; set; }
    public string DepartmentName { get; set; }
}
```

See [IndexSelectModel](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu50/Pages/Courses) for the complete Razor Pages.

## Create Instructor pages

This section scaffolds Instructor pages and adds related Courses and Enrollments to the Instructors Index page.

[]()

This page reads and displays related data in the following ways:

* The list of instructors displays related data from the `OfficeAssignment` entity (Office in the preceding image). The `Instructor` and `OfficeAssignment` entities are in a one-to-zero-or-one relationship. Eager loading is used for the `OfficeAssignment` entities. Eager loading is typically more efficient when the related data needs to be displayed. In this case, office assignments for the instructors are displayed.
* When the user selects an instructor, related `Course` entities are displayed. The `Instructor` and `Course` entities are in a many-to-many relationship. Eager loading is used for the `Course` entities and their related `Department` entities. In this case, separate queries might be more efficient because only courses for the selected instructor are needed. This example shows how to use eager loading for navigation properties in entities that are in navigation properties.
* When the user selects a course, related data from the `Enrollments` entity is displayed. In the preceding image, student name and grade are displayed. The `Course` and `Enrollment` entities are in a one-to-many relationship.

### Create a view model

The instructors page shows data from three different tables. A view model is needed that includes three properties representing the three tables.

Create `Models/SchoolViewModels/InstructorIndexData.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}
```

### Scaffold Instructor pages

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Follow the instructions in [Scaffold the student pages](intro?view=aspnetcore-10.0#scaffold-student-pages) with the following exceptions:

  * Create a *Pages/Instructors* folder.
  * Use `Instructor` for the model class.
  * Use the existing context class instead of creating a new one.

* Create a *Pages/Instructors* folder.

* Run the following command to scaffold the Instructor pages.

  **On Windows:**

  ```
  dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages\Instructors --referenceScriptLibraries
  ```

  **On Linux or macOS:**

  ```
  dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages/Instructors --referenceScriptLibraries
  ```

Run the app and navigate to the Instructors page.

Update `Pages/Instructors/Index.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;  // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public InstructorIndexData InstructorData { get; set; }
        public int InstructorID { get; set; }
        public int CourseID { get; set; }

        public async Task OnGetAsync(int? id, int? courseID)
        {
            InstructorData = new InstructorIndexData();
            InstructorData.Instructors = await _context.Instructors
                .Include(i => i.OfficeAssignment)                 
                .Include(i => i.Courses)
                    .ThenInclude(c => c.Department)
                .OrderBy(i => i.LastName)
                .ToListAsync();

            if (id != null)
            {
                InstructorID = id.Value;
                Instructor instructor = InstructorData.Instructors
                    .Where(i => i.ID == id.Value).Single();
                InstructorData.Courses = instructor.Courses;
            }

            if (courseID != null)
            {
                CourseID = courseID.Value;
                IEnumerable<Enrollment> Enrollments = await _context.Enrollments
                    .Where(x => x.CourseID == CourseID)                    
                    .Include(i=>i.Student)
                    .ToListAsync();                 
                InstructorData.Enrollments = Enrollments;
            }
        }
    }
}
```

The `OnGetAsync` method accepts optional route data for the ID of the selected instructor.

Examine the query in the `Pages/Instructors/Index.cshtml.cs` file:

```
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
    .Include(i => i.OfficeAssignment)                 
    .Include(i => i.Courses)
        .ThenInclude(c => c.Department)
    .OrderBy(i => i.LastName)
    .ToListAsync();
```

The code specifies eager loading for the following navigation properties:

* `Instructor.OfficeAssignment`
* `Instructor.Courses`
  * `Course.Department`

The following code executes when an instructor is selected, that is, `id != null`.

```
if (id != null)
{
    InstructorID = id.Value;
    Instructor instructor = InstructorData.Instructors
        .Where(i => i.ID == id.Value).Single();
    InstructorData.Courses = instructor.Courses;
}
```

The selected instructor is retrieved from the list of instructors in the view model. The view model's `Courses` property is loaded with the `Course` entities from the selected instructor's `Courses` navigation property.

The `Where` method returns a collection. In this case, the filter selects a single entity, so the `Single` method is called to convert the collection into a single `Instructor` entity. The `Instructor` entity provides access to the `Course` navigation property.

The [Single](/en-us/dotnet/api/system.linq.enumerable.single) method is used on a collection when the collection has only one item. The `Single` method throws an exception if the collection is empty or if there's more than one item. An alternative is [SingleOrDefault](/en-us/dotnet/api/system.linq.enumerable.singleordefault), which returns a default value if the collection is empty. For this query, `null` in the default returned.

The following code populates the view model's `Enrollments` property when a course is selected:

```
if (courseID != null)
{
    CourseID = courseID.Value;
    IEnumerable<Enrollment> Enrollments = await _context.Enrollments
        .Where(x => x.CourseID == CourseID)                    
        .Include(i=>i.Student)
        .ToListAsync();                 
    InstructorData.Enrollments = Enrollments;
}
```

### Update the instructors Index page

Update `Pages/Instructors/Index.cshtml` with the following code.

```
@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.InstructorData.Instructors)
        {
            string selectedRow = "";
            if (item.ID == Model.InstructorID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @{
                        foreach (var course in item.Courses)
                        {
                            @course.CourseID @:  @course.Title <br />
                        }
                    }
                </td>
                <td>
                    <a asp-page="./Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@if (Model.InstructorData.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.InstructorData.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == Model.CourseID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    <a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}

@if (Model.InstructorData.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.InstructorData.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}
```

The preceding code makes the following changes:

* Updates the `page` directive to `@page "{id:int?}"`. `"{id:int?}"` is a route template. The [route template](../../fundamentals/routing?view=aspnetcore-10.0#route-template-reference) changes integer query strings in the URL to route data. For example, clicking on the **Select** link for an instructor with only the `@page` directive produces a URL like the following:

  `https://localhost:5001/Instructors?id=2`

  When the page directive is `@page "{id:int?}"`, the URL is: `https://localhost:5001/Instructors/2`

* Adds an **Office** column that displays `item.OfficeAssignment.Location` only if `item.OfficeAssignment` isn't null. Because this is a one-to-zero-or-one relationship, there might not be a related OfficeAssignment entity.

  ```
  @if (item.OfficeAssignment != null)
  {
      @item.OfficeAssignment.Location
  }
  ```

* Adds a **Courses** column that displays courses taught by each instructor. See [Explicit line transition](../../mvc/views/razor?view=aspnetcore-10.0#explicit-line-transition) for more about this razor syntax.

* Adds code that dynamically adds `class="table-success"` to the `tr` element of the selected instructor and course. This sets a background color for the selected row using a Bootstrap class.

  ```
  string selectedRow = "";
  if (item.CourseID == Model.CourseID)
  {
      selectedRow = "table-success";
  }
  <tr class="@selectedRow">
  ```

* Adds a new hyperlink labeled **Select**. This link sends the selected instructor's ID to the `Index` method and sets a background color.

  ```
  <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
  ```

* Adds a table of courses for the selected Instructor.

* Adds a table of student enrollments for the selected course.

Run the app and select the **Instructors** tab. The page displays the `Location` (office) from the related `OfficeAssignment` entity. If `OfficeAssignment` is null, an empty table cell is displayed.

Click on the **Select** link for an instructor. The row style changes and courses assigned to that instructor are displayed.

Select a course to see the list of enrolled students and their grades.

## Next steps

The next tutorial shows how to update related data.

[Previous tutorial](complex-data-model?view=aspnetcore-10.0) [Next tutorial](update-related-data?view=aspnetcore-10.0)

This tutorial shows how to read and display related data. Related data is data that EF Core loads into navigation properties.

The following illustrations show the completed pages for this tutorial:

## Eager, explicit, and lazy loading

EF Core can load related data into the navigation properties of an entity in several ways:

* [Eager loading](/en-us/ef/core/querying/related-data#eager-loading). Eager loading is when a query for one type of entity also loads related entities. When an entity is read, its related data is retrieved. This typically results in a single join query that retrieves all of the data that's needed. EF Core will issue multiple queries for some types of eager loading. Issuing multiple queries can be more efficient than a giant single query. Eager loading is specified with the `Include` and `ThenInclude` methods.

  Eager loading sends multiple queries when a collection navigation is included:

  * One query for the main query
  * One query for each collection "edge" in the load tree.

* Separate queries with `Load`: You can retrieve the data in separate queries, and EF Core "fixes up" the navigation properties. "Fixes up" means that EF Core automatically populates the navigation properties. Separate queries with `Load` is more like explicit loading than eager loading.

  **Note:** EF Core automatically fixes up navigation properties to any other entities that it previously loaded into the context instance. Even if you don't explicitly include the data for a navigation property, the property might still be populated if some or all of the related entities were previously loaded.

* [Explicit loading](/en-us/ef/core/querying/related-data#explicit-loading). When you first read the entity, EF Core doesn't retrieve related data. You must write code to retrieve the related data when it's needed. Explicit loading with separate queries results in multiple queries sent to the database. With explicit loading, you specify the navigation properties to load. Use the `Load` method to do explicit loading. For example:

* [Lazy loading](/en-us/ef/core/querying/related-data#lazy-loading). When the entity is first read, related data isn't retrieved. The first time a navigation property is accessed, the data required for that navigation property is automatically retrieved. A query is sent to the database each time a navigation property is accessed for the first time. Lazy loading can hurt performance, for example when developers use N+1 patterns, loading a parent and enumerating through children.

## Create Course pages

The `Course` entity includes a navigation property that contains the related `Department` entity.

To display the name of the assigned department for a course:

* Load the related `Department` entity into the `Course.Department` navigation property.
* Get the name from the `Department` entity's `Name` property.

[]()

### Scaffold Course pages

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

- Follow the instructions in [Scaffold Student pages](intro?view=aspnetcore-10.0#scaffold-student-pages) with the following exceptions:

  * Create a *Pages/Courses* folder.
  * Use `Course` for the model class.
  * Use the existing context class instead of creating a new one.

* Create a *Pages/Courses* folder.

* Run the following command to scaffold the Course pages.

  **On Windows:**

  ```
  dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages\Courses --referenceScriptLibraries
  ```

  **On Linux or macOS:**

  ```
  dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages/Courses --referenceScriptLibraries
  ```

- Open `Pages/Courses/Index.cshtml.cs` and examine the `OnGetAsync` method. The scaffolding engine specified eager loading for the `Department` navigation property. The `Include` method specifies eager loading.

- Run the app and select the **Courses** link. The department column displays the `DepartmentID`, which isn't useful.

### Display the department name

Update Pages/Courses/Index.cshtml.cs with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public IList<Course> Courses { get; set; }

        public async Task OnGetAsync()
        {
            Courses = await _context.Courses
                .Include(c => c.Department)
                .AsNoTracking()
                .ToListAsync();
        }
    }
}
```

The preceding code changes the `Course` property to `Courses` and adds `AsNoTracking`. `AsNoTracking` improves performance because the entities returned are not tracked. The entities don't need to be tracked because they're not updated in the current context.

Update `Pages/Courses/Index.cshtml` with the following code.

```
@page
@model ContosoUniversity.Pages.Courses.IndexModel

@{
    ViewData["Title"] = "Courses";
}

<h1>Courses</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].CourseID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Credits)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Courses[0].Department)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Courses)
{
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.CourseID)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Credits)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Department.Name)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

The following changes have been made to the scaffolded code:

* Changed the `Course` property name to `Courses`.

* Added a **Number** column that shows the `CourseID` property value. By default, primary keys aren't scaffolded because normally they're meaningless to end users. However, in this case the primary key is meaningful.

* Changed the **Department** column to display the department name. The code displays the `Name` property of the `Department` entity that's loaded into the `Department` navigation property:

  ```
  @Html.DisplayFor(modelItem => item.Department.Name)
  ```

Run the app and select the **Courses** tab to see the list with department names.

[]()

### Loading related data with Select

The `OnGetAsync` method loads related data with the `Include` method. The `Select` method is an alternative that loads only the related data needed. For single items, like the `Department.Name` it uses a SQL INNER JOIN. For collections, it uses another database access, but so does the `Include` operator on collections.

Note

The `Select` method used here is a LINQ **projection** operator. It transforms each element in the source sequence into a new form—in this case, a `CourseViewModel`. Because the query projects into a non-entity type, EF Core generates a SQL query that retrieves only the columns needed for the projection, and no entity tracking is performed.

The following code loads related data with the `Select` method:

```
public IList<CourseViewModel> CourseVM { get; set; }

public async Task OnGetAsync()
{
    CourseVM = await _context.Courses
            .Select(p => new CourseViewModel
            {
                CourseID = p.CourseID,
                Title = p.Title,
                Credits = p.Credits,
                DepartmentName = p.Department.Name
            }).ToListAsync();
}
```

The preceding code doesn't return any entity types, therefore no tracking is done. For more information about the EF tracking, see [Tracking vs. No-Tracking Queries](/en-us/ef/core/querying/tracking).

The `CourseViewModel`:

```
public class CourseViewModel
{
    public int CourseID { get; set; }
    public string Title { get; set; }
    public int Credits { get; set; }
    public string DepartmentName { get; set; }
}
```

See [IndexSelect.cshtml](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu30snapshots/6-related/Pages/Courses/IndexSelect.cshtml) and [IndexSelect.cshtml.cs](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu30snapshots/6-related/Pages/Courses/IndexSelect.cshtml.cs) for a complete example.

## Create Instructor pages

This section scaffolds Instructor pages and adds related Courses and Enrollments to the Instructors Index page.

[]()

This page reads and displays related data in the following ways:

* The list of instructors displays related data from the `OfficeAssignment` entity (Office in the preceding image). The `Instructor` and `OfficeAssignment` entities are in a one-to-zero-or-one relationship. Eager loading is used for the `OfficeAssignment` entities. Eager loading is typically more efficient when the related data needs to be displayed. In this case, office assignments for the instructors are displayed.
* When the user selects an instructor, related `Course` entities are displayed. The `Instructor` and `Course` entities are in a many-to-many relationship. Eager loading is used for the `Course` entities and their related `Department` entities. In this case, separate queries might be more efficient because only courses for the selected instructor are needed. This example shows how to use eager loading for navigation properties in entities that are in navigation properties.
* When the user selects a course, related data from the `Enrollments` entity is displayed. In the preceding image, student name and grade are displayed. The `Course` and `Enrollment` entities are in a one-to-many relationship.

### Create a view model

The instructors page shows data from three different tables. A view model is needed that includes three properties representing the three tables.

Create `SchoolViewModels/InstructorIndexData.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}
```

### Scaffold Instructor pages

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

- Follow the instructions in [Scaffold the student pages](intro?view=aspnetcore-10.0#scaffold-student-pages) with the following exceptions:

  * Create a *Pages/Instructors* folder.
  * Use `Instructor` for the model class.
  * Use the existing context class instead of creating a new one.

* Create a *Pages/Instructors* folder.

* Run the following command to scaffold the Instructor pages.

  **On Windows:**

  ```
  dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages\Instructors --referenceScriptLibraries
  ```

  **On Linux or macOS:**

  ```
  dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages/Instructors --referenceScriptLibraries
  ```

To see what the scaffolded page looks like before you update it, run the app and navigate to the Instructors page.

Update `Pages/Instructors/Index.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;  // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public InstructorIndexData InstructorData { get; set; }
        public int InstructorID { get; set; }
        public int CourseID { get; set; }

        public async Task OnGetAsync(int? id, int? courseID)
        {
            InstructorData = new InstructorIndexData();
            InstructorData.Instructors = await _context.Instructors
                .Include(i => i.OfficeAssignment)                 
                .Include(i => i.CourseAssignments)
                    .ThenInclude(i => i.Course)
                        .ThenInclude(i => i.Department)
                .Include(i => i.CourseAssignments)
                    .ThenInclude(i => i.Course)
                        .ThenInclude(i => i.Enrollments)
                            .ThenInclude(i => i.Student)
                .AsNoTracking()
                .OrderBy(i => i.LastName)
                .ToListAsync();

            if (id != null)
            {
                InstructorID = id.Value;
                Instructor instructor = InstructorData.Instructors
                    .Where(i => i.ID == id.Value).Single();
                InstructorData.Courses = instructor.CourseAssignments.Select(s => s.Course);
            }

            if (courseID != null)
            {
                CourseID = courseID.Value;
                var selectedCourse = InstructorData.Courses
                    .Where(x => x.CourseID == courseID).Single();
                InstructorData.Enrollments = selectedCourse.Enrollments;
            }
        }
    }
}
```

The `OnGetAsync` method accepts optional route data for the ID of the selected instructor.

Examine the query in the `Pages/Instructors/Index.cshtml.cs` file:

```
InstructorData.Instructors = await _context.Instructors
    .Include(i => i.OfficeAssignment)                 
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
    .AsNoTracking()
    .OrderBy(i => i.LastName)
    .ToListAsync();
```

The code specifies eager loading for the following navigation properties:

* `Instructor.OfficeAssignment`

* `Instructor.CourseAssignments`

  * `CourseAssignments.Course`

    * `Course.Department`
    * `Course.Enrollments`
      * `Enrollment.Student`

Notice the repetition of `Include` and `ThenInclude` methods for `CourseAssignments` and `Course`. This repetition is necessary to specify eager loading for two navigation properties of the `Course` entity.

The following code executes when an instructor is selected (`id != null`).

```
if (id != null)
{
    InstructorID = id.Value;
    Instructor instructor = InstructorData.Instructors
        .Where(i => i.ID == id.Value).Single();
    InstructorData.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
```

The selected instructor is retrieved from the list of instructors in the view model. The view model's `Courses` property is loaded with the `Course` entities from that instructor's `CourseAssignments` navigation property.

The `Where` method returns a collection. But in this case, the filter will select a single entity, so the `Single` method is called to convert the collection into a single `Instructor` entity. The `Instructor` entity provides access to the `CourseAssignments` property. `CourseAssignments` provides access to the related `Course` entities.

The `Single` method is used on a collection when the collection has only one item. The `Single` method throws an exception if the collection is empty or if there's more than one item. An alternative is `SingleOrDefault`, which returns a default value (null in this case) if the collection is empty.

The following code populates the view model's `Enrollments` property when a course is selected:

```
if (courseID != null)
{
    CourseID = courseID.Value;
    var selectedCourse = InstructorData.Courses
        .Where(x => x.CourseID == courseID).Single();
    InstructorData.Enrollments = selectedCourse.Enrollments;
}
```

### Update the instructors Index page

Update `Pages/Instructors/Index.cshtml` with the following code.

```
@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.InstructorData.Instructors)
        {
            string selectedRow = "";
            if (item.ID == Model.InstructorID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @{
                        foreach (var course in item.CourseAssignments)
                        {
                            @course.Course.CourseID @:  @course.Course.Title <br />
                        }
                    }
                </td>
                <td>
                    <a asp-page="./Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@if (Model.InstructorData.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.InstructorData.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == Model.CourseID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    <a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}

@if (Model.InstructorData.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.InstructorData.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}
```

The preceding code makes the following changes:

* Updates the `page` directive from `@page` to `@page "{id:int?}"`. `"{id:int?}"` is a route template. The route template changes integer query strings in the URL to route data. For example, clicking on the **Select** link for an instructor with only the `@page` directive produces a URL like the following:

  `https://localhost:5001/Instructors?id=2`

  When the page directive is `@page "{id:int?}"`, the URL is:

  `https://localhost:5001/Instructors/2`

* Adds an **Office** column that displays `item.OfficeAssignment.Location` only if `item.OfficeAssignment` isn't null. Because this is a one-to-zero-or-one relationship, there might not be a related OfficeAssignment entity.

  ```
  @if (item.OfficeAssignment != null)
  {
      @item.OfficeAssignment.Location
  }
  ```

* Adds a **Courses** column that displays courses taught by each instructor. See [Explicit line transition](../../mvc/views/razor?view=aspnetcore-10.0#explicit-line-transition) for more about this razor syntax.

* Adds code that dynamically adds `class="table-success"` to the `tr` element of the selected instructor and course. This sets a background color for the selected row using a Bootstrap class.

  ```
  string selectedRow = "";
  if (item.CourseID == Model.CourseID)
  {
      selectedRow = "table-success";
  }
  <tr class="@selectedRow">
  ```

* Adds a new hyperlink labeled **Select**. This link sends the selected instructor's ID to the `Index` method and sets a background color.

  ```
  <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
  ```

* Adds a table of courses for the selected Instructor.

* Adds a table of student enrollments for the selected course.

Run the app and select the **Instructors** tab. The page displays the `Location` (office) from the related `OfficeAssignment` entity. If `OfficeAssignment` is null, an empty table cell is displayed.

Click on the **Select** link for an instructor. The row style changes and courses assigned to that instructor are displayed.

Select a course to see the list of enrolled students and their grades.

## Using Single

The `Single` method can pass in the `Where` condition instead of calling the `Where` method separately:

```
public async Task OnGetAsync(int? id, int? courseID)
{
    InstructorData = new InstructorIndexData();

    InstructorData.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
            .Include(i => i.CourseAssignments)
                .ThenInclude(i => i.Course)
                    .ThenInclude(i => i.Enrollments)
                        .ThenInclude(i => i.Student)
          .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();

    if (id != null)
    {
        InstructorID = id.Value;
        Instructor instructor = InstructorData.Instructors.Single(
            i => i.ID == id.Value);
        InstructorData.Courses = instructor.CourseAssignments.Select(
            s => s.Course);
    }

    if (courseID != null)
    {
        CourseID = courseID.Value;
        InstructorData.Enrollments = InstructorData.Courses.Single(
            x => x.CourseID == courseID).Enrollments;
    }
}
```

Use of `Single` with a Where condition is a matter of personal preference. It provides no benefits over using the `Where` method.

## Explicit loading

The current code specifies eager loading for `Enrollments` and `Students`:

```
InstructorData.Instructors = await _context.Instructors
    .Include(i => i.OfficeAssignment)                 
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
    .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
    .AsNoTracking()
    .OrderBy(i => i.LastName)
    .ToListAsync();
```

Suppose users rarely want to see enrollments in a course. In that case, an optimization would be to only load the enrollment data if it's requested. In this section, the `OnGetAsync` is updated to use explicit loading of `Enrollments` and `Students`.

Update `Pages/Instructors/Index.cshtml.cs` with the following code.

```
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;  // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public InstructorIndexData InstructorData { get; set; }
        public int InstructorID { get; set; }
        public int CourseID { get; set; }

        public async Task OnGetAsync(int? id, int? courseID)
        {
            InstructorData = new InstructorIndexData();
            InstructorData.Instructors = await _context.Instructors
                .Include(i => i.OfficeAssignment)                 
                .Include(i => i.CourseAssignments)
                    .ThenInclude(i => i.Course)
                        .ThenInclude(i => i.Department)
                //.Include(i => i.CourseAssignments)
                //    .ThenInclude(i => i.Course)
                //        .ThenInclude(i => i.Enrollments)
                //            .ThenInclude(i => i.Student)
                //.AsNoTracking()
                .OrderBy(i => i.LastName)
                .ToListAsync();

            if (id != null)
            {
                InstructorID = id.Value;
                Instructor instructor = InstructorData.Instructors
                    .Where(i => i.ID == id.Value).Single();
                InstructorData.Courses = instructor.CourseAssignments.Select(s => s.Course);
            }

            if (courseID != null)
            {
                CourseID = courseID.Value;
                var selectedCourse = InstructorData.Courses
                    .Where(x => x.CourseID == courseID).Single();
                await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
                foreach (Enrollment enrollment in selectedCourse.Enrollments)
                {
                    await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
                }
                InstructorData.Enrollments = selectedCourse.Enrollments;
            }
        }
    }
}
```

The preceding code drops the *ThenInclude* method calls for enrollment and student data. If a course is selected, the explicit loading code retrieves:

* The `Enrollment` entities for the selected course.
* The `Student` entities for each `Enrollment`.

Notice that the preceding code comments out `.AsNoTracking()`. Navigation properties can only be explicitly loaded for tracked entities.

Test the app. From a user's perspective, the app behaves identically to the previous version.

## Next steps

The next tutorial shows how to update related data.

[Previous tutorial](complex-data-model?view=aspnetcore-10.0) [Next tutorial](update-related-data?view=aspnetcore-10.0)

In this tutorial, related data is read and displayed. Related data is data that EF Core loads into navigation properties.

If you run into problems you can't solve, [download or view the completed app.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) [Download instructions](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

The following illustrations show the completed pages for this tutorial:

## Eager, explicit, and lazy Loading of related data

EF Core can load related data into the navigation properties of an entity in several ways:

* [Eager loading](/en-us/ef/core/querying/related-data#eager-loading). Eager loading is when a query for one type of entity also loads related entities. When the entity is read, its related data is retrieved. This typically results in a single join query that retrieves all of the data that's needed. EF Core will issue multiple queries for some types of eager loading. Issuing multiple queries can be more efficient than was the case for some queries in EF6 where there was a single query. Eager loading is specified with the `Include` and `ThenInclude` methods.

  Eager loading sends multiple queries when a collection navigation is included:

  * One query for the main query
  * One query for each collection "edge" in the load tree.

* Separate queries with `Load`: The data can be retrieved in separate queries, and EF Core "fixes up" the navigation properties. "fixes up" means that EF Core automatically populates the navigation properties. Separate queries with `Load` is more like explicit loading than eager loading.

  Note: EF Core automatically fixes up navigation properties to any other entities that were previously loaded into the context instance. Even if the data for a navigation property is *not* explicitly included, the property may still be populated if some or all of the related entities were previously loaded.

* [Explicit loading](/en-us/ef/core/querying/related-data#explicit-loading). When the entity is first read, related data isn't retrieved. Code must be written to retrieve the related data when it's needed. Explicit loading with separate queries results in multiple queries sent to the DB. With explicit loading, the code specifies the navigation properties to be loaded. Use the `Load` method to do explicit loading. For example:

* [Lazy loading](/en-us/ef/core/querying/related-data#lazy-loading). [Lazy loading was added to EF Core in version 2.1](/en-us/ef/core/querying/related-data#lazy-loading). When the entity is first read, related data isn't retrieved. The first time a navigation property is accessed, the data required for that navigation property is automatically retrieved. A query is sent to the DB each time a navigation property is accessed for the first time.

* The `Select` operator loads only the related data needed.

## Create a Course page that displays department name

The Course entity includes a navigation property that contains the `Department` entity. The `Department` entity contains the department that the course is assigned to.

To display the name of the assigned department in a list of courses:

* Get the `Name` property from the `Department` entity.
* The `Department` entity comes from the `Course.Department` navigation property.

[]()

### Scaffold the Course model

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

Follow the instructions in [Scaffold the student model](intro?view=aspnetcore-10.0#scaffold-the-student-model) and use `Course` for the model class.

Run the following command:

```
dotnet aspnet-codegenerator razorpage -m Course -dc SchoolContext -udl -outDir Pages\Courses --referenceScriptLibraries
```

The preceding command scaffolds the `Course` model. Open the project in Visual Studio.

Open `Pages/Courses/Index.cshtml.cs` and examine the `OnGetAsync` method. The scaffolding engine specified eager loading for the `Department` navigation property. The `Include` method specifies eager loading.

Run the app and select the **Courses** link. The department column displays the `DepartmentID`, which isn't useful.

Update the `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    Course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .ToListAsync();
}
```

The preceding code adds `AsNoTracking`. `AsNoTracking` improves performance because the entities returned are not tracked. The entities are not tracked because they're not updated in the current context.

Update `Pages/Courses/Index.cshtml` with the following highlighted markup:

```
@page
@model ContosoUniversity.Pages.Courses.IndexModel
@{
    ViewData["Title"] = "Courses";
}

<h2>Courses</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Course[0].CourseID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Course[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Course[0].Credits)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Course[0].Department)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Course)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.CourseID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Credits)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Department.Name)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

The following changes have been made to the scaffolded code:

* Changed the heading from Index to Courses.

* Added a **Number** column that shows the `CourseID` property value. By default, primary keys aren't scaffolded because normally they're meaningless to end users. However, in this case the primary key is meaningful.

* Changed the **Department** column to display the department name. The code displays the `Name` property of the `Department` entity that's loaded into the `Department` navigation property:

  ```
  @Html.DisplayFor(modelItem => item.Department.Name)
  ```

Run the app and select the **Courses** tab to see the list with department names.

[]()

### Loading related data with Select

The `OnGetAsync` method loads related data with the `Include` method:

```
public async Task OnGetAsync()
{
    Course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .ToListAsync();
}
```

The `Select` operator loads only the related data needed. For single items, like the `Department.Name` it uses a SQL INNER JOIN. For collections, it uses another database access, but so does the `Include` operator on collections.

The following code loads related data with the `Select` method:

```
public IList<CourseViewModel> CourseVM { get; set; }

public async Task OnGetAsync()
{
    CourseVM = await _context.Courses
            .Select(p => new CourseViewModel
            {
                CourseID = p.CourseID,
                Title = p.Title,
                Credits = p.Credits,
                DepartmentName = p.Department.Name
            }).ToListAsync();
}
```

The `CourseViewModel`:

```
public class CourseViewModel
{
    public int CourseID { get; set; }
    public string Title { get; set; }
    public int Credits { get; set; }
    public string DepartmentName { get; set; }
}
```

See [IndexSelect.cshtml](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu/Pages/Courses/IndexSelect.cshtml) and [IndexSelect.cshtml.cs](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu/Pages/Courses/IndexSelect.cshtml.cs) for a complete example.

## Create an Instructors page that shows Courses and Enrollments

In this section, the Instructors page is created.

[]()

This page reads and displays related data in the following ways:

* The list of instructors displays related data from the `OfficeAssignment` entity (Office in the preceding image). The `Instructor` and `OfficeAssignment` entities are in a one-to-zero-or-one relationship. Eager loading is used for the `OfficeAssignment` entities. Eager loading is typically more efficient when the related data needs to be displayed. In this case, office assignments for the instructors are displayed.
* When the user selects an instructor (Harui in the preceding image), related `Course` entities are displayed. The `Instructor` and `Course` entities are in a many-to-many relationship. Eager loading is used for the `Course` entities and their related `Department` entities. In this case, separate queries might be more efficient because only courses for the selected instructor are needed. This example shows how to use eager loading for navigation properties in entities that are in navigation properties.
* When the user selects a course (Chemistry in the preceding image), related data from the `Enrollments` entity is displayed. In the preceding image, student name and grade are displayed. The `Course` and `Enrollment` entities are in a one-to-many relationship.

### Create a view model for the Instructor Index view

The instructors page shows data from three different tables. A view model is created that includes the three entities representing the three tables.

In the *SchoolViewModels* folder, create `InstructorIndexData.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}
```

### Scaffold the Instructor model

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

Follow the instructions in [Scaffold the student model](intro?view=aspnetcore-10.0#scaffold-the-student-model) and use `Instructor` for the model class.

Run the following command:

```
dotnet aspnet-codegenerator razorpage -m Instructor -dc SchoolContext -udl -outDir Pages\Instructors --referenceScriptLibraries
```

The preceding command scaffolds the `Instructor` model. Run the app and navigate to the instructors page.

Replace `Pages/Instructors/Index.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;  // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class IndexModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public IndexModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public InstructorIndexData Instructor { get; set; }
        public int InstructorID { get; set; }

        public async Task OnGetAsync(int? id)
        {
            Instructor = new InstructorIndexData();
            Instructor.Instructors = await _context.Instructors
                  .Include(i => i.OfficeAssignment)
                  .Include(i => i.CourseAssignments)
                    .ThenInclude(i => i.Course)
                  .AsNoTracking()
                  .OrderBy(i => i.LastName)
                  .ToListAsync();

            if (id != null)
            {
                InstructorID = id.Value;
            }           
        }
    }
}
```

The `OnGetAsync` method accepts optional route data for the ID of the selected instructor.

Examine the query in the `Pages/Instructors/Index.cshtml.cs` file:

```
Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();
```

The query has two includes:

* `OfficeAssignment`: Displayed in the [instructors view](#IP).
* `CourseAssignments`: Which brings in the courses taught.

### Update the instructors Index page

Update `Pages/Instructors/Index.cshtml` with the following markup:

```
@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Instructor.Instructors)
        {
            string selectedRow = "";
            if (item.ID == Model.InstructorID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @{
                        foreach (var course in item.CourseAssignments)
                        {
                            @course.Course.CourseID @:  @course.Course.Title <br />
                        }
                    }
                </td>
                <td>
                    <a asp-page="./Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

The preceding markup makes the following changes:

* Updates the `page` directive from `@page` to `@page "{id:int?}"`. `"{id:int?}"` is a route template. The route template changes integer query strings in the URL to route data. For example, clicking on the **Select** link for an instructor with only the `@page` directive produces a URL like the following:

  `http://localhost:1234/Instructors?id=2`

  When the page directive is `@page "{id:int?}"`, the previous URL is:

  `http://localhost:1234/Instructors/2`

* Page title is **Instructors**.

* Added an **Office** column that displays `item.OfficeAssignment.Location` only if `item.OfficeAssignment` isn't null. Because this is a one-to-zero-or-one relationship, there might not be a related OfficeAssignment entity.

  ```
  @if (item.OfficeAssignment != null)
  {
      @item.OfficeAssignment.Location
  }
  ```

* Added a **Courses** column that displays courses taught by each instructor. See [Explicit line transition](../../mvc/views/razor?view=aspnetcore-10.0#explicit-line-transition) for more about this razor syntax.

* Added code that dynamically adds `class="table-success"` to the `tr` element of the selected instructor. This sets a background color for the selected row using a Bootstrap class.

  ```
  string selectedRow = "";
  if (item.CourseID == Model.CourseID)
  {
      selectedRow = "table-success";
  }
  <tr class="@selectedRow">
  ```

* Added a new hyperlink labeled **Select**. This link sends the selected instructor's ID to the `Index` method and sets a background color.

  ```
  <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
  ```

Run the app and select the **Instructors** tab. The page displays the `Location` (office) from the related `OfficeAssignment` entity. If OfficeAssignment\` is null, an empty table cell is displayed.

Click on the **Select** link. The row style changes.

### Add courses taught by selected instructor

Update the `OnGetAsync` method in `Pages/Instructors/Index.cshtml.cs` with the following code:

```
public async Task OnGetAsync(int? id, int? courseID)
{
    Instructor = new InstructorIndexData();
    Instructor.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
          .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();

    if (id != null)
    {
        InstructorID = id.Value;
        Instructor instructor = Instructor.Instructors.Where(
            i => i.ID == id.Value).Single();
        Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }

    if (courseID != null)
    {
        CourseID = courseID.Value;
        Instructor.Enrollments = Instructor.Courses.Where(
            x => x.CourseID == courseID).Single().Enrollments;
    }
}
```

Add `public int CourseID { get; set; }`

```
public class IndexModel : PageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public IndexModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    public InstructorIndexData Instructor { get; set; }
    public int InstructorID { get; set; }
    public int CourseID { get; set; }

    public async Task OnGetAsync(int? id, int? courseID)
    {
        Instructor = new InstructorIndexData();
        Instructor.Instructors = await _context.Instructors
              .Include(i => i.OfficeAssignment)
              .Include(i => i.CourseAssignments)
                .ThenInclude(i => i.Course)
                    .ThenInclude(i => i.Department)
              .AsNoTracking()
              .OrderBy(i => i.LastName)
              .ToListAsync();

        if (id != null)
        {
            InstructorID = id.Value;
            Instructor instructor = Instructor.Instructors.Where(
                i => i.ID == id.Value).Single();
            Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
        }

        if (courseID != null)
        {
            CourseID = courseID.Value;
            Instructor.Enrollments = Instructor.Courses.Where(
                x => x.CourseID == courseID).Single().Enrollments;
        }
    }
```

Examine the updated query:

```
Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();
```

The preceding query adds the `Department` entities.

The following code executes when an instructor is selected (`id != null`). The selected instructor is retrieved from the list of instructors in the view model. The view model's `Courses` property is loaded with the `Course` entities from that instructor's `CourseAssignments` navigation property.

```
if (id != null)
{
    InstructorID = id.Value;
    Instructor instructor = Instructor.Instructors.Where(
        i => i.ID == id.Value).Single();
    Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
```

The `Where` method returns a collection. In the preceding `Where` method, only a single `Instructor` entity is returned. The `Single` method converts the collection into a single `Instructor` entity. The `Instructor` entity provides access to the `CourseAssignments` property. `CourseAssignments` provides access to the related `Course` entities.

The `Single` method is used on a collection when the collection has only one item. The `Single` method throws an exception if the collection is empty or if there's more than one item. An alternative is `SingleOrDefault`, which returns a default value (null in this case) if the collection is empty. Using `SingleOrDefault` on an empty collection:

* Results in an exception (from trying to find a `Courses` property on a null reference).
* The exception message would less clearly indicate the cause of the problem.

The following code populates the view model's `Enrollments` property when a course is selected:

```
if (courseID != null)
{
    CourseID = courseID.Value;
    Instructor.Enrollments = Instructor.Courses.Where(
        x => x.CourseID == courseID).Single().Enrollments;
}
```

Add the following markup to the end of the `Pages/Instructors/Index.cshtml` Razor Page:

```
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@if (Model.Instructor.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Instructor.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == Model.CourseID)
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    <a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}
```

The preceding markup displays a list of courses related to an instructor when an instructor is selected.

Test the app. Click on a **Select** link on the instructors page.

### Show student data

In this section, the app is updated to show the student data for a selected course.

Update the query in the `OnGetAsync` method in `Pages/Instructors/Index.cshtml.cs` with the following code:

```
Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)                 
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
        .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Enrollments)
                    .ThenInclude(i => i.Student)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();
```

Update `Pages/Instructors/Index.cshtml`. Add the following markup to the end of the file:

```

@if (Model.Instructor.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Instructor.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}
```

The preceding markup displays a list of the students who are enrolled in the selected course.

Refresh the page and select an instructor. Select a course to see the list of enrolled students and their grades.

## Using Single

The `Single` method can pass in the `Where` condition instead of calling the `Where` method separately:

```
public async Task OnGetAsync(int? id, int? courseID)
{
    Instructor = new InstructorIndexData();

    Instructor.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
            .Include(i => i.CourseAssignments)
                .ThenInclude(i => i.Course)
                    .ThenInclude(i => i.Enrollments)
                        .ThenInclude(i => i.Student)
          .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();

    if (id != null)
    {
        InstructorID = id.Value;
        Instructor instructor = Instructor.Instructors.Single(
            i => i.ID == id.Value);
        Instructor.Courses = instructor.CourseAssignments.Select(
            s => s.Course);
    }

    if (courseID != null)
    {
        CourseID = courseID.Value;
        Instructor.Enrollments = Instructor.Courses.Single(
            x => x.CourseID == courseID).Enrollments;
    }
}
```

The preceding `Single` approach provides no benefits over using `Where`. Some developers prefer the `Single` approach style.

## Explicit loading

The current code specifies eager loading for `Enrollments` and `Students`:

```
Instructor.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)                 
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
        .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Enrollments)
                    .ThenInclude(i => i.Student)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();
```

Suppose users rarely want to see enrollments in a course. In that case, an optimization would be to only load the enrollment data if it's requested. In this section, the `OnGetAsync` is updated to use explicit loading of `Enrollments` and `Students`.

Update the `OnGetAsync` with the following code:

```
public async Task OnGetAsync(int? id, int? courseID)
{
    Instructor = new InstructorIndexData();
    Instructor.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)                 
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
            //.Include(i => i.CourseAssignments)
            //    .ThenInclude(i => i.Course)
            //        .ThenInclude(i => i.Enrollments)
            //            .ThenInclude(i => i.Student)
         // .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();


    if (id != null)
    {
        InstructorID = id.Value;
        Instructor instructor = Instructor.Instructors.Where(
            i => i.ID == id.Value).Single();
        Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }

    if (courseID != null)
    {
        CourseID = courseID.Value;
        var selectedCourse = Instructor.Courses.Where(x => x.CourseID == courseID).Single();
        await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
            await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
        }
        Instructor.Enrollments = selectedCourse.Enrollments;
    }
}
```

The preceding code drops the *ThenInclude* method calls for enrollment and student data. If a course is selected, the highlighted code retrieves:

* The `Enrollment` entities for the selected course.
* The `Student` entities for each `Enrollment`.

Notice the preceding code comments out `.AsNoTracking()`. Navigation properties can only be explicitly loaded for tracked entities.

Test the app. From a users perspective, the app behaves identically to the previous version.

The next tutorial shows how to update related data.

## Related content

* [YouTube version of this tutorial (part1)](https://www.youtube.com/watch?v=PzKimUDmrvE)
* [YouTube version of this tutorial (part2)](https://www.youtube.com/watch?v=xvDDrIHv5ko)

[Previous](complex-data-model?view=aspnetcore-10.0) [Next](update-related-data?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/extensibility/core-crypto
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/extensibility/core-crypto.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Core cryptography extensibility in ASP.NET Core

[]()

Warning

Types that implement any of the following interfaces should be thread-safe for multiple callers.

[]()

## IAuthenticatedEncryptor

The **IAuthenticatedEncryptor** interface is the basic building block of the cryptographic subsystem. There's generally one IAuthenticatedEncryptor per key, and the IAuthenticatedEncryptor instance wraps all cryptographic key material and algorithmic information necessary to perform cryptographic operations.

As its name suggests, the type is responsible for providing authenticated encryption and decryption services. It exposes the following two APIs.

* `Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData) : byte[]`

* `Encrypt(ArraySegment<byte> plaintext, ArraySegment<byte> additionalAuthenticatedData) : byte[]`

The Encrypt method returns a blob that includes the enciphered plaintext and an authentication tag. The authentication tag must encompass the additional authenticated data (AAD), though the AAD itself need not be recoverable from the final payload. The Decrypt method validates the authentication tag and returns the deciphered payload. All failures (except ArgumentNullException and similar) should be homogenized to CryptographicException.

Note

The IAuthenticatedEncryptor instance itself doesn't actually need to contain the key material. For example, the implementation could delegate to an HSM for all operations.

[]()[]()

## How to create an IAuthenticatedEncryptor

* [ASP.NET Core 2.x](#tabpanel_1_aspnetcore2x)
* [ASP.NET Core 1.x](#tabpanel_1_aspnetcore1x)

The **IAuthenticatedEncryptorFactory** interface represents a type that knows how to create an [IAuthenticatedEncryptor](core-crypto?view=aspnetcore-10.0#data-protection-extensibility-core-crypto-iauthenticatedencryptor) instance. Its API is as follows.

* CreateEncryptorInstance(IKey key) : IAuthenticatedEncryptor

For any given IKey instance, any authenticated encryptors created by its CreateEncryptorInstance method should be considered equivalent, as in the below code sample.

```
// we have an IAuthenticatedEncryptorFactory instance and an IKey instance
IAuthenticatedEncryptorFactory factory = ...;
IKey key = ...;

// get an encryptor instance and perform an authenticated encryption operation
ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("AAD"));
var encryptor1 = factory.CreateEncryptorInstance(key);
byte[] ciphertext = encryptor1.Encrypt(plaintext, aad);

// get another encryptor instance and perform an authenticated decryption operation
var encryptor2 = factory.CreateEncryptorInstance(key);
byte[] roundTripped = encryptor2.Decrypt(new ArraySegment<byte>(ciphertext), aad);


// the 'roundTripped' and 'plaintext' buffers should be equivalent
```

The **IAuthenticatedEncryptorDescriptor** interface represents a type that knows how to create an [IAuthenticatedEncryptor](core-crypto?view=aspnetcore-10.0#data-protection-extensibility-core-crypto-iauthenticatedencryptor) instance. Its API is as follows.

* CreateEncryptorInstance() : IAuthenticatedEncryptor

* ExportToXml() : XmlSerializedDescriptorInfo

Like IAuthenticatedEncryptor, an instance of IAuthenticatedEncryptorDescriptor is assumed to wrap one specific key. This means that for any given IAuthenticatedEncryptorDescriptor instance, any authenticated encryptors created by its CreateEncryptorInstance method should be considered equivalent, as in the below code sample.

```
// we have an IAuthenticatedEncryptorDescriptor instance
IAuthenticatedEncryptorDescriptor descriptor = ...;

// get an encryptor instance and perform an authenticated encryption operation
ArraySegment<byte> plaintext = new ArraySegment<byte>(Encoding.UTF8.GetBytes("plaintext"));
ArraySegment<byte> aad = new ArraySegment<byte>(Encoding.UTF8.GetBytes("AAD"));
var encryptor1 = descriptor.CreateEncryptorInstance();
byte[] ciphertext = encryptor1.Encrypt(plaintext, aad);

// get another encryptor instance and perform an authenticated decryption operation
var encryptor2 = descriptor.CreateEncryptorInstance();
byte[] roundTripped = encryptor2.Decrypt(new ArraySegment<byte>(ciphertext), aad);


// the 'roundTripped' and 'plaintext' buffers should be equivalent
```

[]()

## IAuthenticatedEncryptorDescriptor (ASP.NET Core 2.x only)

* [ASP.NET Core 2.x](#tabpanel_2_aspnetcore2x)
* [ASP.NET Core 1.x](#tabpanel_2_aspnetcore1x)

The **IAuthenticatedEncryptorDescriptor** interface represents a type that knows how to export itself to XML. Its API is as follows.

* ExportToXml() : XmlSerializedDescriptorInfo

## XML Serialization

The primary difference between IAuthenticatedEncryptor and IAuthenticatedEncryptorDescriptor is that the descriptor knows how to create the encryptor and supply it with valid arguments. Consider an IAuthenticatedEncryptor whose implementation relies on SymmetricAlgorithm and KeyedHashAlgorithm. The encryptor's job is to consume these types, but it doesn't necessarily know where these types came from, so it can't really write out a proper description of how to recreate itself if the application restarts. The descriptor acts as a higher level on top of this. Since the descriptor knows how to create the encryptor instance (for example it knows how to create the required algorithms), it can serialize that knowledge in XML form so that the encryptor instance can be recreated after an application reset.

[]()

The descriptor can be serialized via its ExportToXml routine. This routine returns an XmlSerializedDescriptorInfo which contains two properties: the XElement representation of the descriptor and the Type which represents an [IAuthenticatedEncryptorDescriptorDeserializer](core-crypto?view=aspnetcore-10.0#data-protection-extensibility-core-crypto-iauthenticatedencryptordescriptordeserializer) which can be used to resurrect this descriptor given the corresponding XElement.

The serialized descriptor may contain sensitive information such as cryptographic key material. The data protection system has built-in support for encrypting information before it's persisted to storage. To take advantage of this, the descriptor should mark the element which contains sensitive information with the attribute name "requiresEncryption" (xmlns `"<http://schemas.asp.net/2015/03/dataProtection>"`), value "true".

Tip

There's a helper API for setting this attribute. Call the extension method XElement.MarkAsRequiresEncryption() located in namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.

There can also be cases where the serialized descriptor doesn't contain sensitive information. Consider again the case of a cryptographic key stored in an HSM. The descriptor cannot write out the key material when serializing itself since the HSM won't expose the material in plaintext form. Instead, the descriptor might write out the key-wrapped version of the key (if the HSM allows export in this fashion) or the HSM's own unique identifier for the key.

[]()

## IAuthenticatedEncryptorDescriptorDeserializer

The **IAuthenticatedEncryptorDescriptorDeserializer** interface represents a type that knows how to deserialize an IAuthenticatedEncryptorDescriptor instance from an XElement. It exposes a single method:

* ImportFromXml(XElement element) : IAuthenticatedEncryptorDescriptor

The ImportFromXml method takes the XElement that was returned by [IAuthenticatedEncryptorDescriptor.ExportToXml](core-crypto?view=aspnetcore-10.0#data-protection-extensibility-core-crypto-exporttoxml) and creates an equivalent of the original IAuthenticatedEncryptorDescriptor.

Types which implement IAuthenticatedEncryptorDescriptorDeserializer should have one of the following two public constructors:

* .ctor(IServiceProvider)

* .ctor()

Note

The IServiceProvider passed to the constructor may be null.

## The top-level factory

* [ASP.NET Core 2.x](#tabpanel_3_aspnetcore2x)
* [ASP.NET Core 1.x](#tabpanel_3_aspnetcore1x)

The **AlgorithmConfiguration** class represents a type which knows how to create [IAuthenticatedEncryptorDescriptor](core-crypto?view=aspnetcore-10.0#data-protection-extensibility-core-crypto-iauthenticatedencryptordescriptor) instances. It exposes a single API.

* CreateNewDescriptor() : IAuthenticatedEncryptorDescriptor

Think of AlgorithmConfiguration as the top-level factory. The configuration serves as a template. It wraps algorithmic information (for example this configuration produces descriptors with an AES-128-GCM master key), but it's not yet associated with a specific key.

When CreateNewDescriptor is called, fresh key material is created solely for this call, and a new IAuthenticatedEncryptorDescriptor is produced which wraps this key material and the algorithmic information required to consume the material. The key material could be created in software (and held in memory), it could be created and held within an HSM, and so on. The crucial point is that any two calls to CreateNewDescriptor should never create equivalent IAuthenticatedEncryptorDescriptor instances.

The AlgorithmConfiguration type serves as the entry point for key creation routines such as [automatic key rolling](../implementation/key-management?view=aspnetcore-10.0#key-expiration-and-rolling). To change the implementation for all future keys, set the AuthenticatedEncryptorConfiguration property in KeyManagementOptions.

The **IAuthenticatedEncryptorConfiguration** interface represents a type which knows how to create [IAuthenticatedEncryptorDescriptor](core-crypto?view=aspnetcore-10.0#data-protection-extensibility-core-crypto-iauthenticatedencryptordescriptor) instances. It exposes a single API.

* CreateNewDescriptor() : IAuthenticatedEncryptorDescriptor

Think of IAuthenticatedEncryptorConfiguration as the top-level factory. The configuration serves as a template. It wraps algorithmic information (for example this configuration produces descriptors with an AES-128-GCM master key), but it's not yet associated with a specific key.

When CreateNewDescriptor is called, fresh key material is created solely for this call, and a new IAuthenticatedEncryptorDescriptor is produced which wraps this key material and the algorithmic information required to consume the material. The key material could be created in software (and held in memory), it could be created and held within an HSM, and so on. The crucial point is that any two calls to CreateNewDescriptor should never create equivalent IAuthenticatedEncryptorDescriptor instances.

The IAuthenticatedEncryptorConfiguration type serves as the entry point for key creation routines such as [automatic key rolling](../implementation/key-management?view=aspnetcore-10.0#key-expiration-and-rolling). To change the implementation for all future keys, register a singleton IAuthenticatedEncryptorConfiguration in the service container.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-interop/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/dotnet-interop/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# JavaScript `[JSImport]`/`[JSExport]` interop in .NET WebAssembly

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Aaron Shumaker](https://github.com/SerratedSharp)

This article explains how to interact with JavaScript (JS) in client-side WebAssembly using JS `[JSImport]`/`[JSExport]` interop ([System.Runtime.InteropServices.JavaScript](/en-us/dotnet/api/system.runtime.interopservices.javascript) API).

`[JSImport]`/`[JSExport]` interop is applicable when running a .NET WebAssembly module in a JS host in the following scenarios:

* [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop with a WebAssembly Browser App project](wasm-browser-app?view=aspnetcore-10.0).
* [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](../../blazor/javascript-interoperability/import-export-interop?view=aspnetcore-10.0).
* Other .NET WebAssembly platforms that support `[JSImport]`/`[JSExport]` interop.

## Prerequisites

[.NET SDK (latest version)](https://dotnet.microsoft.com/download/dotnet/)

Any of the following project types:

* A WebAssembly Browser App project created according to [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop with a WebAssembly Browser App project](wasm-browser-app?view=aspnetcore-10.0).
* A Blazor client-side project created according to [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](../../blazor/javascript-interoperability/import-export-interop?view=aspnetcore-10.0).
* A project created for a commercial or open-source platform that supports `[JSImport]`/`[JSExport]` interop ([System.Runtime.InteropServices.JavaScript](/en-us/dotnet/api/system.runtime.interopservices.javascript) API).

## Sample app

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../../blazor/fundamentals/?view=aspnetcore-10.0#sample-apps)): Select an 8.0 or later version folder that matches the version of .NET that you're adopting. Within the version folder, access the sample named `WASMBrowserAppImportExportInterop`.

## JS interop using `[JSImport]`/`[JSExport]` attributes

The `[JSImport]` attribute is applied to a .NET method to indicate that a corresponding JS method should be called when the .NET method is called. This allows .NET developers to define "imports" that enable .NET code to call into JS. Additionally, an [Action](/en-us/dotnet/api/system.action) can be passed as a parameter, and JS can invoke the action to support a callback or event subscription pattern.

The `[JSExport]` attribute is applied to a .NET method to expose it to JS code. This allows JS code to initiate calls to the .NET method.

## Importing JS methods

The following example imports a standard built-in JS method (`console.log`) into C#. `[JSImport]` is limited to importing methods of globally-accessible objects. For example, `log` is a method defined on the `console` object, which is defined on the globally-accessible object `globalThis`. The `console.log` method is mapped to a C# proxy method, `ConsoleLog`, which accepts a string for the log message:

```
public partial class GlobalInterop
{
    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog(string text);
}
```

In `Program.Main`, `ConsoleLog` is called with the message to log:

```
GlobalInterop.ConsoleLog("Hello World!");
```

The output appears in the browser's console.

The following demonstrates importing a method declared in JS.

The following custom JS method (`globalThis.callAlert`) spawns an [alert dialog (`window.alert`)](https://developer.mozilla.org/docs/Web/API/Window/alert) with the message passed in `text`:

```
globalThis.callAlert = function (text) {
  globalThis.window.alert(text);
}
```

The `globalThis.callAlert` method is mapped to a C# proxy method (`CallAlert`), which accepts a string for the message:

```
using System.Runtime.InteropServices.JavaScript;

public partial class GlobalInterop
{
	[JSImport("globalThis.callAlert")]
	public static partial void CallAlert(string text);
}
```

In `Program.Main`, `CallAlert` is called, passing the text for the alert dialog message:

```
GlobalInterop.CallAlert("Hello World");
```

The C# class declaring the `[JSImport]` method doesn't have an implementation. At compile time, a source-generated partial class contains the .NET code that implements the marshalling of the call and types to invoke the corresponding JS method. In Visual Studio, using the **Go To Definition** or **Go To Implementation** options respectively navigates to either the source-generated partial class or the developer-defined partial class.

In the preceding example, the intermediate `globalThis.callAlert` JS declaration is used to wrap existing JS code. This article informally refers to the intermediate JS declaration as a *JS shim*. JS shims fill the gap between the .NET implementation and existing JS capabilities/libraries. In many cases, such as the preceding trivial example, the JS shim isn't necessary, and methods could be imported directly, as demonstrated in the earlier `ConsoleLog` example. As this article demonstrates in the upcoming sections, a JS shim can:

* Encapsulate additional logic.
* Manually map types.
* Reduce the number of objects or calls crossing the interop boundary.
* Manually map static calls to instance methods.

## Loading JavaScript declarations

JS declarations which are intended to be imported with `[JSImport]` are typically loaded in the context of the same page or JS host that loaded .NET WebAssembly. This can be accomplished with:

* A `<script>...</script>` block declaring inline JS.
* A script source (`src`) declaration (`<script src="./some.js"></script>`) that loads an external JS file (`.js`).
* A JS ES6 module (`<script type='module' src="./moduleName.js"></script>`).
* A JS ES6 module loaded using [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync) from .NET WebAssembly.

Examples in this article use [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync). When calling [ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync), client-side .NET WebAssembly requests the file using the `moduleUrl` parameter, and thus it expects the file to be accessible as a static web asset, much the same way as a `<script>` tag retrieves a file with a `src` URL. For example, the following C# code within a WebAssembly Browser App project maintains the JS file (`.js`) at the path `/wwwroot/scripts/ExampleShim.js`:

```
await JSHost.ImportAsync("ExampleShim", "/scripts/ExampleShim.js");
```

Depending on the platform that's loading WebAssembly, a dot-prefixed URL, such as `./scripts/`, might refer to an incorrect subdirectory, such as `/_framework/scripts/`, because the WebAssembly package is initialized by framework scripts under `/_framework/`. In that case, prefixing the URL with `../scripts/` refers to the correct path. Prefixing with `/scripts/` works if the site is hosted at the root of the domain. A typical approach involves configuring the correct base path for the given environment with an HTML `<base>` tag and using the `/scripts/` prefix to refer to the path relative to the base path. Tilde notation `~/` prefixes aren't supported by [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync).

Important

If JS is loaded from a JavaScript module, then `[JSImport]` attributes must include the module name as the second parameter. For example, `[JSImport("globalThis.callAlert", "ExampleShim")]` indicates the imported method was declared in a JavaScript module named "`ExampleShim`."

[JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync) can take advantage of the following features:

* [Import map for module scripts (Blazor Web Apps)](../../blazor/fundamentals/static-files?view=aspnetcore-10.0#importmap-component)
* [Fingerprinting client-side static assets (standalone Blazor WebAssembly apps)](../../blazor/fundamentals/static-files?view=aspnetcore-10.0#fingerprint-client-side-static-assets-in-standalone-blazor-webassembly-apps)

[JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync) can take advantage of an [import map for module scripts](../../blazor/fundamentals/static-files?view=aspnetcore-10.0#importmap-component) in a Blazor Web App.

## Type mappings

Parameters and return types in the .NET method signature are automatically converted to or from appropriate JS types at runtime if a unique mapping is supported. This may result in values converted by value or references wrapped in a proxy type. This process is known as *type marshalling*. Use [JSMarshalAsAttribute\<T>](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsmarshalasattribute-1) to control how the imported method parameters and return types are marshalled.

Some types don't have a default type mapping. For example, a `long` can be marshalled as [System.Runtime.InteropServices.JavaScript.JSType.Number](/en-us/dotnet/api/system.runtime.interopservices.javascript.jstype.number) or [System.Runtime.InteropServices.JavaScript.JSType.BigInt](/en-us/dotnet/api/system.runtime.interopservices.javascript.jstype.bigint), so the [JSMarshalAsAttribute\<T>](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsmarshalasattribute-1) is required to avoid a compile-time error.

The following type mapping scenarios are supported:

* Passing [Action](/en-us/dotnet/api/system.action) or [Func\<TResult>](/en-us/dotnet/api/system.func-1) as parameters, which are marshalled as callable JS methods. This allows .NET code to invoke listeners in response to JS callbacks or events.
* Passing JS references and .NET managed object references in either direction, which as marshaled as proxy objects and kept alive across the interop boundary until the proxy is garbage collected.
* Marshalling asynchronous JS methods or a [JS `Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) with a [Task](/en-us/dotnet/api/system.threading.tasks.task) result, and vice versa.

Most of the marshalled types work in both directions, as parameters and as return values, on both imported and exported methods.

The following table indicates the supported type mappings.

| .NET                        | JavaScript   | `Nullable`    | `Task` ➔to `Promise` | `JSMarshalAs` optional | `Array of`    |
| --------------------------- | ------------ | ------------- | -------------------- | ---------------------- | ------------- |
| `Boolean`                   | `Boolean`    | ✅Supported    | ✅Supported           | ✅Supported             | Not supported |
| `Byte`                      | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | ✅Supported    |
| `Char`                      | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | Not supported |
| `Int16`                     | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | Not supported |
| `Int32`                     | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | ✅Supported    |
| `Int64`                     | `Number`     | ✅Supported    | ✅Supported           | Not supported          | Not supported |
| `Int64`                     | `BigInt`     | ✅Supported    | ✅Supported           | Not supported          | Not supported |
| `Single`                    | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | ✅Supported    |
| `Double`                    | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | ✅Supported    |
| `IntPtr`                    | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | Not supported |
| `DateTime`                  | `Date`       | ✅Supported    | ✅Supported           | Not supported          | Not supported |
| `DateTimeOffset`            | `Date`       | ✅Supported    | ✅Supported           | Not supported          | Not supported |
| `Exception`                 | `Error`      | Not supported | ✅Supported           | ✅Supported             | Not supported |
| `JSObject`                  | `Object`     | Not supported | ✅Supported           | ✅Supported             | ✅Supported    |
| `String`                    | `String`     | Not supported | ✅Supported           | ✅Supported             | ✅Supported    |
| `Object`                    | `Any`        | Not supported | ✅Supported           | Not supported          | ✅Supported    |
| `Span<Byte>`                | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `Span<Int32>`               | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `Span<Single>`              | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `Span<Double>`              | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `ArraySegment<Byte>`        | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `ArraySegment<Int32>`       | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `ArraySegment<Single>`      | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `ArraySegment<Double>`      | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `Task`                      | `Promise`    | Not supported | Not supported        | ✅Supported             | Not supported |
| `Action`                    | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Action<T1>`                | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Action<T1, T2>`            | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Action<T1, T2, T3>`        | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Func<TResult>`             | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Func<T1, TResult>`         | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Func<T1, T2, TResult>`     | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Func<T1, T2, T3, TResult>` | `Function`   | Not supported | Not supported        | Not supported          | Not supported |

| .NET                        | JavaScript   | `Nullable`    | `Task` ➔to `Promise` | `JSMarshalAs` optional | `Array of`    |
| --------------------------- | ------------ | ------------- | -------------------- | ---------------------- | ------------- |
| `Boolean`                   | `Boolean`    | ✅Supported    | ✅Supported           | ✅Supported             | Not supported |
| `Byte`                      | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | ✅Supported    |
| `Char`                      | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | Not supported |
| `Int16`                     | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | Not supported |
| `Int32`                     | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | ✅Supported    |
| `Int64`                     | `Number`     | ✅Supported    | ✅Supported           | Not supported          | Not supported |
| `Int64`                     | `BigInt`     | ✅Supported    | ✅Supported           | Not supported          | Not supported |
| `Single`                    | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | Not supported |
| `Double`                    | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | ✅Supported    |
| `IntPtr`                    | `Number`     | ✅Supported    | ✅Supported           | ✅Supported             | Not supported |
| `DateTime`                  | `Date`       | ✅Supported    | ✅Supported           | Not supported          | Not supported |
| `DateTimeOffset`            | `Date`       | ✅Supported    | ✅Supported           | Not supported          | Not supported |
| `Exception`                 | `Error`      | Not supported | ✅Supported           | ✅Supported             | Not supported |
| `JSObject`                  | `Object`     | Not supported | ✅Supported           | ✅Supported             | ✅Supported    |
| `String`                    | `String`     | Not supported | ✅Supported           | ✅Supported             | ✅Supported    |
| `Object`                    | `Any`        | Not supported | ✅Supported           | Not supported          | ✅Supported    |
| `Span<Byte>`                | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `Span<Int32>`               | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `Span<Double>`              | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `ArraySegment<Byte>`        | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `ArraySegment<Int32>`       | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `ArraySegment<Double>`      | `MemoryView` | Not supported | Not supported        | Not supported          | Not supported |
| `Task`                      | `Promise`    | Not supported | Not supported        | ✅Supported             | Not supported |
| `Action`                    | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Action<T1>`                | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Action<T1, T2>`            | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Action<T1, T2, T3>`        | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Func<TResult>`             | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Func<T1, TResult>`         | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Func<T1, T2, TResult>`     | `Function`   | Not supported | Not supported        | Not supported          | Not supported |
| `Func<T1, T2, T3, TResult>` | `Function`   | Not supported | Not supported        | Not supported          | Not supported |

The following conditions apply to type mapping and marshalled values:

* The `Array of` column indicates if the .NET type can be marshalled as a JS [`Array`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array). Example: C# `int[]` (`Int32`) mapped to JS `Array` of `Number`s.

* When passing a JS value to C# with a value of the wrong type, the framework throws an exception in most cases. The framework doesn't perform compile-time type checking in JS.

* `JSObject`, `Exception`, `Task` and `ArraySegment` create `GCHandle` and a proxy. You can trigger disposal in developer code or allow [.NET garbage collection (GC)](/en-us/dotnet/standard/garbage-collection/) to dispose of the objects later. These types carry significant performance overhead.

* `Array`: Marshaling an array creates a copy of the array in JS or .NET.

* `MemoryView`

  * `MemoryView` is a JS class for the .NET WebAssembly runtime to marshal `Span` and `ArraySegment`.
  * Unlike marshaling an array, marshaling a `Span` or `ArraySegment` doesn't create a copy of the underlying memory.
  * `MemoryView` can only be properly instantiated by the .NET WebAssembly runtime. Therefore, it isn't possible to import a JS method as a .NET method that has a parameter of `Span` or `ArraySegment`.
  * `MemoryView` created for a `Span` is only valid for the duration of the interop call. As `Span` is allocated on the call stack, which doesn't persist after the interop call, it isn't possible to export a .NET method that returns a `Span`.
  * `MemoryView` created for an `ArraySegment` survives after the interop call and is useful for sharing a buffer. Calling `dispose()` on a `MemoryView` created for an `ArraySegment` disposes the proxy and unpins the underlying .NET array. We recommend calling `dispose()` in a `try-finally` block for `MemoryView`.

Some combinations of type mappings that require nested generic types in [`JSMarshalAs`](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsmarshalasattribute-1) aren't currently supported. For example, attempting to materialize an array from a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) such as `[return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]` generates a compile-time error. An appropriate workaround varies depending on the scenario, but this specific scenario is further explored in the [Type mapping limitations](#type-mapping-limitations) section.

## JS primitives

The following example demonstrates `[JSImport]` leveraging type mappings of several primitive JS types and the use of [`JSMarshalAs`](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsmarshalasattribute-1), where explicit mappings are required at compile time.

`PrimitivesShim.js`:

```
globalThis.counter = 0;

// Takes no parameters and returns nothing.
export function incrementCounter() {
  globalThis.counter += 1;
};

// Returns an int.
export function getCounter() { return globalThis.counter; };

// Takes a parameter and returns nothing. JS doesn't restrict the parameter type, 
// but we can restrict it in the .NET proxy, if desired.
export function logValue(value) { console.log(value); };

// Called for various .NET types to demonstrate mapping to JS primitive types.
export function logValueAndType(value) { console.log(typeof value, value); };
```

`PrimitivesInterop.cs`:

```
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class PrimitivesInterop
{
    // Importing an existing JS method.
    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog([JSMarshalAs<JSType.Any>] object value);

    // Importing static methods from a JS module.
    [JSImport("incrementCounter", "PrimitivesShim")]
    public static partial void IncrementCounter();

    [JSImport("getCounter", "PrimitivesShim")]
    public static partial int GetCounter();

    // The JS shim method name isn't required to match the C# method name.
    [JSImport("logValue", "PrimitivesShim")]
    public static partial void LogInt(int value);

    // A second mapping to the same JS method with compatible type.
    [JSImport("logValue", "PrimitivesShim")]
    public static partial void LogString(string value);

    // Accept any type as parameter. .NET types are mapped to JS types where 
    // possible. Otherwise, they're marshalled as an untyped object reference 
    // to the .NET object proxy. The JS implementation logs to browser console 
    // the JS type and value to demonstrate results of marshalling.
    [JSImport("logValueAndType", "PrimitivesShim")]
    public static partial void LogValueAndType(
        [JSMarshalAs<JSType.Any>] object value);

    // Some types have multiple mappings and require explicit marshalling to the 
    // desired JS type. A long/Int64 can be mapped as either a Number or BigInt.
    // Passing a long value to the above method generates an error at runtime:
    // "ToJS for System.Int64 is not implemented." ("ToJS" means "to JavaScript")
    // If the parameter declaration `Method(JSMarshalAs<JSType.Any>] long value)` 
    // is used, a compile-time error is generated:
    // "Type long is not supported by source-generated JS interop...."
    // Instead, explicitly map the long parameter to either a JSType.Number or 
    // JSType.BigInt. Note that runtime overflow errors are possible in JS if the 
    // C# value is too large.
    [JSImport("logValueAndType", "PrimitivesShim")]
    public static partial void LogValueAndTypeForNumber(
        [JSMarshalAs<JSType.Number>] long value);

    [JSImport("logValueAndType", "PrimitivesShim")]
    public static partial void LogValueAndTypeForBigInt(
        [JSMarshalAs<JSType.BigInt>] long value);
}

public static class PrimitivesUsage
{
    public static async Task Run()
    {
        // Ensure JS module loaded.
        await JSHost.ImportAsync("PrimitivesShim", "/PrimitivesShim.js");

        // Call a proxy to a static JS method, console.log().
        PrimitivesInterop.ConsoleLog("Printed from JSImport of console.log()");

        // Basic examples of JS interop with an integer.
        PrimitivesInterop.IncrementCounter();
        int counterValue = PrimitivesInterop.GetCounter();
        PrimitivesInterop.LogInt(counterValue);
        PrimitivesInterop.LogString("I'm a string from .NET in your browser!");

        // Mapping some other .NET types to JS primitives.
        PrimitivesInterop.LogValueAndType(true);
        PrimitivesInterop.LogValueAndType(0x3A); // Byte literal
        PrimitivesInterop.LogValueAndType('C');
        PrimitivesInterop.LogValueAndType((Int16)12);
        // JS Number has a lower max value and can generate overflow errors.
        PrimitivesInterop.LogValueAndTypeForNumber(9007199254740990L); // Int64/Long
        // Next line: Int64/Long, JS BigInt supports larger numbers.
        PrimitivesInterop.LogValueAndTypeForBigInt(1234567890123456789L);// 
        PrimitivesInterop.LogValueAndType(3.14f); // Single floating point literal
        PrimitivesInterop.LogValueAndType(3.14d); // Double floating point literal
        PrimitivesInterop.LogValueAndType("A string");
    }
}
```

In `Program.Main`:

```
await PrimitivesUsage.Run();
```

The preceding example displays the following output in the browser's debug console:

> Printed from JSImport of console.log()\
> 1\
> I'm a string from .NET in your browser!\
> boolean true\
> number 58\
> number 67\
> number 12\
> number 9007199254740990\
> bigint 1234567890123456789n\
> number 3.140000104904175\
> number 3.14\
> string A string

## JS `Date` objects

The example in this section demonstrates importing methods which have a [JS `Date`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date) object as its return or parameter. Dates are marshalled across interop by-value, meaning they are copied in much the same way as JS primitives.

A `Date` object is timezone agnostic. A .NET [DateTime](/en-us/dotnet/api/system.datetime) is adjusted relative to its [DateTimeKind](/en-us/dotnet/api/system.datetimekind) when marshalled to a `Date`, but timezone information isn't preserved. Consider initializing a [DateTime](/en-us/dotnet/api/system.datetime) with a [DateTimeKind.Utc](/en-us/dotnet/api/system.datetimekind#system-datetimekind-utc) or [DateTimeKind.Local](/en-us/dotnet/api/system.datetimekind#system-datetimekind-local) consistent with the value it represents.

`DateShim.js`:

```
export function incrementDay(date) {
  date.setDate(date.getDate() + 1);
  return date;
}

export function logValueAndType(value) {
  console.log("Date:", value)
}
```

`DateInterop.cs`:

```
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class DateInterop
{
    [JSImport("incrementDay", "DateShim")]
    [return: JSMarshalAs<JSType.Date>] // Explicit JSMarshalAs for a return type
    public static partial DateTime IncrementDay(
        [JSMarshalAs<JSType.Date>] DateTime date);

    [JSImport("logValueAndType", "DateShim")]
    public static partial void LogValueAndType(
        [JSMarshalAs<JSType.Date>] DateTime value);
}

public static class DateUsage
{
    public static async Task Run()
    {
        // Ensure JS module loaded.
        await JSHost.ImportAsync("DateShim", "/DateShim.js");

        // Basic examples of interop with a C# DateTime and JS Date.
        DateTime date = new(1968, 12, 21, 12, 51, 0, DateTimeKind.Utc);
        DateInterop.LogValueAndType(date);
        date = DateInterop.IncrementDay(date);
        DateInterop.LogValueAndType(date);
    }
}
```

In `Program.Main`:

```
await DateUsage.Run();
```

The preceding example displays the following output in the browser's debug console:

> Date: Sat Dec 21 1968 07:51:00 GMT-0500 (Eastern Standard Time)\
> Date: Sun Dec 22 1968 07:51:00 GMT-0500 (Eastern Standard Time)

The preceding timezone information (`GMT-0500 (Eastern Standard Time)`) depends on local timezone of your computer/browser.

## JS object references

Whenever a JS method returns an object reference, it's represented in .NET as a [JSObject](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsobject). The original JS object continues its lifetime within the JS boundary, while .NET code can access and modify it by reference through the [JSObject](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsobject). While the type itself exposes a limited API, the ability to hold a JS object reference and return or pass it across the interop boundary enables support for several interop scenarios.

The [JSObject](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsobject) provides methods to access properties, but it doesn't provide direct access to instance methods. As the following `Summarize` method demonstrates, instance methods can be accessed indirectly by implementing a static method that takes the instance as a parameter.

`JSObjectShim.js`:

```
export function createObject() {
  return {
    name: "Example JS Object",
    answer: 41,
    question: null,
    summarize: function () {
      return `Question: "${this.question}" Answer: ${this.answer}`;
    }
  };
}

export function incrementAnswer(object) {
  object.answer += 1;
  // Don't return the modified object, since the reference is modified.
}

// Proxy an instance method call.
export function summarize(object) {
  return object.summarize();
}
```

`JSObjectInterop.cs`:

```
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class JSObjectInterop
{
    [JSImport("createObject", "JSObjectShim")]
    public static partial JSObject CreateObject();

    [JSImport("incrementAnswer", "JSObjectShim")]
    public static partial void IncrementAnswer(JSObject jsObject);

    [JSImport("summarize", "JSObjectShim")]
    public static partial string Summarize(JSObject jsObject);

    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog([JSMarshalAs<JSType.Any>] object value);
}

public static class JSObjectUsage
{
    public static async Task Run()
    {
        await JSHost.ImportAsync("JSObjectShim", "/JSObjectShim.js");

        JSObject jsObject = JSObjectInterop.CreateObject();
        JSObjectInterop.ConsoleLog(jsObject);
        JSObjectInterop.IncrementAnswer(jsObject);
        // An updated object isn't retrieved. The change is reflected in the 
        // existing instance.
        JSObjectInterop.ConsoleLog(jsObject);

        // JSObject exposes several methods for interacting with properties.
        jsObject.SetProperty("question", "What is the answer?");
        JSObjectInterop.ConsoleLog(jsObject);

        // We can't directly JSImport an instance method on the jsObject, but we 
        // can pass the object reference and have the JS shim call the instance 
        // method.
        string summary = JSObjectInterop.Summarize(jsObject);
        Console.WriteLine("Summary: " + summary);
    }
}
```

In `Program.Main`:

```
await JSObjectUsage.Run();
```

The preceding example displays the following output in the browser's debug console:

> {name: 'Example JS Object', answer: 41, question: null, Symbol(wasm cs\_owned\_js\_handle): 5, summarize: ƒ}\
> {name: 'Example JS Object', answer: 42, question: null, Symbol(wasm cs\_owned\_js\_handle): 5, summarize: ƒ}\
> {name: 'Example JS Object', answer: 42, question: 'What is the answer?', Symbol(wasm cs\_owned\_js\_handle): 5, summarize: ƒ}\
> Summary: Question: "What is the answer?" Answer: 42

## Asynchronous interop

Many JS APIs are asynchronous and signal completion through either a callback, a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise), or an async method. Ignoring asynchronous capabilities is often not an option, as subsequent code may depend upon the completion of the asynchronous operation and must be awaited.

JS methods using the `async` keyword or returning a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) can be awaited in C# by a method returning a [Task](/en-us/dotnet/api/system.threading.tasks.task). As demonstrated below, the `async` keyword isn't used on the C# method with the `[JSImport]` attribute because it doesn't use the `await` keyword within it. However, consuming code calling the method would typically use the `await` keyword and be marked as `async`, as demonstrated in the `PromisesUsage` example.

JS with a callback, such as a `setTimeout`, can be wrapped in a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) before returning from JS. Wrapping a callback in a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise), as demonstrated in the function assigned to `Wait2Seconds`, is only appropriate when the callback is called exactly once. Otherwise, a C# [Action](/en-us/dotnet/api/system.action) can be passed to listen for a callback that may be called zero or many times, which is demonstrated in the [Subscribing to JS events](#subscribing-to-js-events) section.

`PromisesShim.js`:

```
export function wait2Seconds() {
  // This also demonstrates wrapping a callback-based API in a promise to
  // make it awaitable.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(); // Resolve promise after 2 seconds
    }, 2000);
  });
}

// Return a value via resolve in a promise.
export function waitGetString() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("String From Resolve"); // Return a string via promise
    }, 500);
  });
}

export function waitGetDate() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date('1988-11-24')); // Return a date via promise
    }, 500);
  });
}

// Demonstrates an awaitable fetch.
export function fetchCurrentUrl() {
  // This method returns the promise returned by .then(*.text())
  // and .NET awaits the returned promise.
  return fetch(globalThis.window.location, { method: 'GET' })
    .then(response => response.text());
}

// .NET can await JS methods using the async/await JS syntax.
export async function asyncFunction() {
  await wait2Seconds();
}

// A Promise.reject can be used to signal failure and is bubbled to .NET code
// as a JSException.
export function conditionalSuccess(shouldSucceed) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldSucceed)
        resolve(); // Success
      else
        reject("Reject: ShouldSucceed == false"); // Failure
    }, 500);
  });
}
```

Don't use the `async` keyword in the C# method signature. Returning [Task](/en-us/dotnet/api/system.threading.tasks.task) or [Task\<TResult>](/en-us/dotnet/api/system.threading.tasks.task-1) is sufficient.

When calling asynchronous JS methods, we often want to wait until the JS method completes execution. If loading a resource or making a request, we likely want the following code to assume the action is completed.

If the JS shim returns a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise), then C# can treat it as an awaitable [Task](/en-us/dotnet/api/system.threading.tasks.task)/[Task\<TResult>](/en-us/dotnet/api/system.threading.tasks.task-1).

`PromisesInterop.cs`:

```
using System;
using System.Diagnostics;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class PromisesInterop
{
    // For a promise with void return type, declare a Task return type:
    [JSImport("wait2Seconds", "PromisesShim")]
    public static partial Task Wait2Seconds();

    [JSImport("waitGetString", "PromisesShim")]
    public static partial Task<string> WaitGetString();

    // Some return types require a [return: JSMarshalAs...] declaring the
    // Promise's return type corresponding to Task<T>.
    [JSImport("waitGetDate", "PromisesShim")]
    [return: JSMarshalAs<JSType.Promise<JSType.Date>>()]
    public static partial Task<DateTime> WaitGetDate();

    [JSImport("fetchCurrentUrl", "PromisesShim")]
    public static partial Task<string> FetchCurrentUrl();

    [JSImport("asyncFunction", "PromisesShim")]
    public static partial Task AsyncFunction();

    [JSImport("conditionalSuccess", "PromisesShim")]
    public static partial Task ConditionalSuccess(bool shouldSucceed);
}

public static class PromisesUsage
{
    public static async Task Run()
    {
        await JSHost.ImportAsync("PromisesShim", "/PromisesShim.js");

        Stopwatch sw = new();
        sw.Start();

        await PromisesInterop.Wait2Seconds(); // Await Promise
        Console.WriteLine($"Waited {sw.Elapsed.TotalSeconds:#.0}s.");

        sw.Restart();
        string str =
            await PromisesInterop.WaitGetString(); // Await promise (string return)
        Console.WriteLine(
            $"Waited {sw.Elapsed.TotalSeconds:#.0}s for WaitGetString: '{str}'");

        sw.Restart();
        // Await promise with string return.
        DateTime date = await PromisesInterop.WaitGetDate();
        Console.WriteLine(
            $"Waited {sw.Elapsed.TotalSeconds:#.0}s for WaitGetDate: '{date}'");

        // Await a JS fetch.
        string responseText = await PromisesInterop.FetchCurrentUrl();
        Console.WriteLine($"responseText.Length: {responseText.Length}");

        sw.Restart();

        await PromisesInterop.AsyncFunction(); // Await an async JS method
        Console.WriteLine(
            $"Waited {sw.Elapsed.TotalSeconds:#.0}s for AsyncFunction.");

        try
        {
            // Handle a promise rejection. Await an async JS method.
            await PromisesInterop.ConditionalSuccess(shouldSucceed: false);
        }
        catch (JSException ex) // Catch JS exception
        {
            Console.WriteLine($"JS Exception Caught: '{ex.Message}'");
        }
    }
}
```

In `Program.Main`:

```
await PromisesUsage.Run();
```

The preceding example displays the following output in the browser's debug console:

> Waited 2.0s.\
> Waited .5s for WaitGetString: 'String From Resolve'\
> Waited .5s for WaitGetDate: '11/24/1988 12:00:00 AM'\
> responseText.Length: 582\
> Waited 2.0s for AsyncFunction.\
> JS Exception Caught: 'Reject: ShouldSucceed == false'

## Type mapping limitations

Some type mappings requiring nested generic types in the [`JSMarshalAs`](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsmarshalasattribute-1) definition aren't currently supported. For example, returning a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) for an array such as `[return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]` generates a compile-time error. An appropriate workaround varies depending on the scenario, but one option is to represent the array as a [JSObject](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsobject) reference. This may be sufficient if accessing individual elements within .NET isn't necessary and the reference can be passed to other JS methods that act on the array. Alternatively, a dedicated method can take the [JSObject](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsobject) reference as a parameter and return the materialized array, as demonstrated by the following `UnwrapJSObjectAsIntArray` example. In this case, the JS method has no type checking, and the developer has the responsibility to ensure a [JSObject](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsobject) wrapping the appropriate array type is passed.

```
export function waitGetIntArrayAsObject() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([1, 2, 3, 4, 5]); // Return an array from the Promise
    }, 500);
  });
}

export function unwrapJSObjectAsIntArray(jsObject) {
  return jsObject;
}
```

```
// Not supported, generates compile-time error.
// [JSImport("waitGetArray", "PromisesShim")]
// [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
// public static partial Task<int[]> WaitGetIntArray();

// Workaround, take the return the call and pass it to UnwrapJSObjectAsIntArray.
// Return a JSObject reference to a JS number array.
[JSImport("waitGetIntArrayAsObject", "PromisesShim")]
[return: JSMarshalAs<JSType.Promise<JSType.Object>>()]
public static partial Task<JSObject> WaitGetIntArrayAsObject();

// Takes a JSObject reference to a JS number array, and returns the array as a C# 
// int array.
[JSImport("unwrapJSObjectAsIntArray", "PromisesShim")]
[return: JSMarshalAs<JSType.Array<JSType.Number>>()]
public static partial int[] UnwrapJSObjectAsIntArray(JSObject intArray);
//...
```

In `Program.Main`:

```
JSObject arrayAsJSObject = await PromisesInterop.WaitGetIntArrayAsObject();
int[] intArray = PromisesInterop.UnwrapJSObjectAsIntArray(arrayAsJSObject);
```

## Performance considerations

Marshalling of calls and the overhead of tracking objects across the interop boundary is more expensive than native .NET operations but should still demonstrate acceptable performance for a typical web app with moderate demand.

Object proxies, such as [JSObject](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsobject), which maintain references across the interop boundary, have additional memory overhead and impact how garbage collection affects these objects. Additionally, available memory might be exhausted without triggering garbage collection in some scenarios because memory pressure from JS and .NET isn't shared. This risk is significant when an excessive number of large objects are referenced across the interop boundary by relatively small JS objects, or vice versa where large .NET objects are referenced by JS proxies. In such cases, we recommend following deterministic disposal patterns with `using` scopes leveraging the [IDisposable](/en-us/dotnet/api/system.idisposable) interface on JS objects.

The following benchmarks, which leverage earlier example code, demonstrate that interop operations are roughly an order of magnitude slower than those that remain within the .NET boundary, but the interop operations remain relatively fast. Additionally, consider that a user's device capabilities impact performance.

`JSObjectBenchmark.cs`:

```
using System;
using System.Diagnostics;

public static class JSObjectBenchmark
{
    public static void Run()
    {
        Stopwatch sw = new();
        var jsObject = JSObjectInterop.CreateObject();

        sw.Start();

        for (int i = 0; i < 1000000; i++)
        {
            JSObjectInterop.IncrementAnswer(jsObject);
        }

        sw.Stop();

        Console.WriteLine(
            $"JS interop elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds " +
            $"at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per " +
            "operation");

        var pocoObject =
            new PocoObject { Question = "What is the answer?", Answer = 41 };
        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            pocoObject.IncrementAnswer();
        }

        sw.Stop();

        Console.WriteLine($".NET elapsed time: {sw.Elapsed.TotalSeconds:#.0000} " +
            $"seconds at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms " +
            "per operation");

        Console.WriteLine($"Begin Object Creation");

        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            var jsObject2 = JSObjectInterop.CreateObject();
            JSObjectInterop.IncrementAnswer(jsObject2);
        }

        sw.Stop();

        Console.WriteLine(
            $"JS interop elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds " +
            $"at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per " +
            "operation");

        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            var pocoObject2 =
                new PocoObject { Question = "What is the answer?", Answer = 0 };
            pocoObject2.IncrementAnswer();
        }

        sw.Stop();
        Console.WriteLine(
            $".NET elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds at " +
            $"{sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per operation");
    }

    public class PocoObject // Plain old CLR object
    {
        public string Question { get; set; }
        public int Answer { get; set; }

        public void IncrementAnswer() => Answer += 1;
    }
}
```

In `Program.Main`:

```
JSObjectBenchmark.Run();
```

The preceding example displays the following output in the browser's debug console:

> JS interop elapsed time: .2536 seconds at .000254 ms per operation\
> .NET elapsed time: .0210 seconds at .000021 ms per operation\
> Begin Object Creation\
> JS interop elapsed time: 2.1686 seconds at .002169 ms per operation\
> .NET elapsed time: .1089 seconds at .000109 ms per operation

## Subscribing to JS events

.NET code can subscribe to JS events and handle JS events by passing a C# [Action](/en-us/dotnet/api/system.action) to a JS function to act as a handler. The JS shim code handles subscribing to the event.

Warning

Interacting with individual properties of the DOM via JS interop, as the guidance in this section demonstrates, is relatively slow and may lead to the creation of many proxies that create high garbage collection pressure. The following pattern isn't generally recommended. Use the following pattern for no more than a few elements. For more information, see the [Performance considerations](#performance-considerations) section.

A nuance of `removeEventListener` is that it requires a reference to the function previously passed to `addEventListener`. When a C# [Action](/en-us/dotnet/api/system.action) is passed across the interop boundary, it's wrapped in a JS proxy object. Therefore, passing the same C# [Action](/en-us/dotnet/api/system.action) to both `addEventListener` and `removeEventListener` results in generating two different JS proxy objects wrapping the [Action](/en-us/dotnet/api/system.action). These references are different, thus `removeEventListener` isn't able to find the event listener to remove. To address this problem, the following examples wrap the C# [Action](/en-us/dotnet/api/system.action) in a JS function and return the reference as a [JSObject](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsobject) from the subscribe call to pass later to the unsubscribe call. Because the C# [Action](/en-us/dotnet/api/system.action) is returned and passed as a [JSObject](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsobject), the same reference is used for both calls, and the event listener can be removed.

`EventsShim.js`:

```
export function subscribeEventById(elementId, eventName, listenerFunc) {
  const elementObj = document.getElementById(elementId);

  // Need to wrap the Managed C# action in JS func (only because it is being 
  // returned).
  let handler = function (event) {
    listenerFunc(event.type, event.target.id); // Decompose object to primitives
  }.bind(elementObj);

  elementObj.addEventListener(eventName, handler, false);
  // Return JSObject reference so it can be used for removeEventListener later.
  return handler;
}

// Param listenerHandler must be the JSObject reference returned from the prior 
// SubscribeEvent call.
export function unsubscribeEventById(elementId, eventName, listenerHandler) {
  const elementObj = document.getElementById(elementId);
  elementObj.removeEventListener(eventName, listenerHandler, false);
}

export function triggerClick(elementId) {
  const elementObj = document.getElementById(elementId);
  elementObj.click();
}

export function getElementById(elementId) {
  return document.getElementById(elementId);
}

export function subscribeEvent(elementObj, eventName, listenerFunc) {
  let handler = function (e) {
    listenerFunc(e);
  }.bind(elementObj);

  elementObj.addEventListener(eventName, handler, false);
  return handler;
}

export function unsubscribeEvent(elementObj, eventName, listenerHandler) {
  return elementObj.removeEventListener(eventName, listenerHandler, false);
}

export function subscribeEventFailure(elementObj, eventName, listenerFunc) {
  // It's not strictly required to wrap the C# action listenerFunc in a JS 
  // function.
  elementObj.addEventListener(eventName, listenerFunc, false);
  // If you need to return the wrapped proxy object, you will receive an error 
  // when it tries to wrap the existing proxy in an additional proxy:
  // Error: "JSObject proxy of ManagedObject proxy is not supported."
  return listenerFunc;
}
```

`EventsInterop.cs`:

```
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class EventsInterop
{
    [JSImport("subscribeEventById", "EventsShim")]
    public static partial JSObject SubscribeEventById(string elementId,
        string eventName,
        [JSMarshalAs<JSType.Function<JSType.String, JSType.String>>]
        Action<string, string> listenerFunc);

    [JSImport("unsubscribeEventById", "EventsShim")]
    public static partial void UnsubscribeEventById(string elementId,
        string eventName, JSObject listenerHandler);

    [JSImport("triggerClick", "EventsShim")]
    public static partial void TriggerClick(string elementId);

    [JSImport("getElementById", "EventsShim")]
    public static partial JSObject GetElementById(string elementId);

    [JSImport("subscribeEvent", "EventsShim")]
    public static partial JSObject SubscribeEvent(JSObject htmlElement,
        string eventName,
        [JSMarshalAs<JSType.Function<JSType.Object>>]
        Action<JSObject> listenerFunc);

    [JSImport("unsubscribeEvent", "EventsShim")]
    public static partial void UnsubscribeEvent(JSObject htmlElement,
        string eventName, JSObject listenerHandler);
}

public static class EventsUsage
{
    public static async Task Run()
    {
        await JSHost.ImportAsync("EventsShim", "/EventsShim.js");

        Action<string, string> listenerFunc = (eventName, elementId) =>
            Console.WriteLine(
                $"In C# event listener: Event {eventName} from ID {elementId}");

        // Assumes two buttons exist on the page with ids of "btn1" and "btn2"
        JSObject listenerHandler1 =
            EventsInterop.SubscribeEventById("btn1", "click", listenerFunc);
        JSObject listenerHandler2 =
            EventsInterop.SubscribeEventById("btn2", "click", listenerFunc);
        Console.WriteLine("Subscribed to btn1 & 2.");
        EventsInterop.TriggerClick("btn1");
        EventsInterop.TriggerClick("btn2");

        EventsInterop.UnsubscribeEventById("btn2", "click", listenerHandler2);
        Console.WriteLine("Unsubscribed btn2.");
        EventsInterop.TriggerClick("btn1");
        EventsInterop.TriggerClick("btn2"); // Doesn't trigger because unsubscribed
        EventsInterop.UnsubscribeEventById("btn1", "click", listenerHandler1);
        // Pitfall: Using a different handler for unsubscribe silently fails.
        // EventsInterop.UnsubscribeEventById("btn1", "click", listenerHandler2);

        // With JSObject as event target and event object.
        Action<JSObject> listenerFuncForElement = (eventObj) =>
        {
            string eventType = eventObj.GetPropertyAsString("type");
            JSObject target = eventObj.GetPropertyAsJSObject("target");
            Console.WriteLine(
                $"In C# event listener: Event {eventType} from " +
                $"ID {target.GetPropertyAsString("id")}");
        };

        JSObject htmlElement = EventsInterop.GetElementById("btn1");
        JSObject listenerHandler3 = EventsInterop.SubscribeEvent(
            htmlElement, "click", listenerFuncForElement);
        Console.WriteLine("Subscribed to btn1.");
        EventsInterop.TriggerClick("btn1");
        EventsInterop.UnsubscribeEvent(htmlElement, "click", listenerHandler3);
        Console.WriteLine("Unsubscribed btn1.");
        EventsInterop.TriggerClick("btn1");
    }
}
```

In `Program.Main`:

```
await EventsUsage.Run();
```

The preceding example displays the following output in the browser's debug console:

> Subscribed to btn1 & 2.\
> In C# event listener: Event click from ID btn1\
> In C# event listener: Event click from ID btn2\
> Unsubscribed btn2.\
> In C# event listener: Event click from ID btn1\
> Subscribed to btn1.\
> In C# event listener: Event click from ID btn1\
> Unsubscribed btn1.

## JS `[JSImport]`/`[JSExport]` interop scenarios

The following articles focus on running a .NET WebAssembly module in a JS host, such as a browser:

* [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop with a WebAssembly Browser App project](wasm-browser-app?view=aspnetcore-10.0)
* [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](../../blazor/javascript-interoperability/import-export-interop?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/inc/remote-app-setup
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/inc/remote-app-setup.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Remote app setup

Important

Framework and Core applications must use identical virtual directory layouts.

The virtual directory setup is used for route generation, authorization, and other services within the system. At this point, no reliable method has been found to enable different virtual directories due to how ASP.NET Framework works.

In some incremental upgrade scenarios, it's useful for the new ASP.NET Core app to be able to communicate with the original ASP.NET app.

Common scenarios this enables:

* Fallback to the legacy application with [YARP](../../../fundamentals/servers/yarp/yarp-overview?view=aspnetcore-10.0)
* [Remote app authentication](../areas/authentication?view=aspnetcore-10.0#remote-authentication)
* [Remote session](../areas/session?view=aspnetcore-10.0#remote-app-session-state)

## Configuration Values

To enable the ASP.NET Core app to communicate with the ASP.NET app, it's necessary to make a couple small changes to each app.

You need to configure two configuration values in both applications:

* `RemoteAppApiKey`: A key (required to be parseable as a [GUID](/en-us/dotnet/api/system.guid)) that is shared between the two applications. This should be a GUID value like `12345678-1234-1234-1234-123456789012`.
* `RemoteAppUri`: The URI of the remote ASP.NET Framework application (only required in the ASP.NET Core application configuration). This should be the full URL where the ASP.NET Framework app is hosted, such as `https://localhost:44300` or `https://myapp.example.com`.

## Configure ASP.NET Framework Application

Important

The ASP.NET Framework application should be hosted with SSL enabled. In the remote app setup for incremental migration, it is not required to have direct access externally. It is recommended to only allow access from the client application via the proxy.

For ASP.NET Framework applications, add these values to your `web.config` in the `<appSettings>` section:

Important

ASP.NET Framework stores its appSettings in `web.config`. However, they can be retrieved from other sources (such as environment variables) with the use of [configuration Builders](/en-us/aspnet/config-builder). This makes sharing configuration values much easier between the local and remote applications in this setup.

```
<appSettings>
  <add key="RemoteAppApiKey" value="..." />
</appSettings>
```

To configure the application to be available to handle the requests from the ASP.NET Core client, set up the following:

1. Install the NuGet package [`Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices`](https://www.nuget.org/packages/Microsoft.AspNetCore.SystemWebAdapters)

2. Add the configuration code to the `Application_Start` method in your `Global.asax.cs` file:

   ```
   protected void Application_Start()
   {
       HttpApplicationHost.RegisterHost(builder =>
       {
           builder.Services.AddSystemWebAdapters()
               .AddRemoteAppServer(options =>
               {
                   // ApiKey is a string representing a GUID
                   options.ApiKey = System.Configuration.ConfigurationManager.AppSettings["RemoteAppApiKey"];
               });
       });

       // ...existing code...
   }
   ```

3. Add the `SystemWebAdapterModule` module to the `web.config` if it wasn't already added by NuGet. This module configuration is required for IIS hosting scenarios. The `SystemWebAdapterModule` module is not added automatically when using SDK style projects for ASP.NET Core.

   ```
     <system.webServer>
       <modules>
   +      <remove name="SystemWebAdapterModule" />
   +      <add name="SystemWebAdapterModule" type="Microsoft.AspNetCore.SystemWebAdapters.SystemWebAdapterModule, Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices" preCondition="managedHandler" />
       </modules>
   </system.webServer>
   ```

## Configure ASP.NET Core Application

For ASP.NET Core applications, add these values to your `appsettings.json`:

```
{
  "RemoteAppApiKey": "...",
  "RemoteAppUri": "https://localhost:44300"
}
```

To set up the ASP.NET Core app to be able to send requests to the ASP.NET app, configure the remote app client by calling `AddRemoteAppClient` after registering System.Web adapter services with `AddSystemWebAdapters`.

Add this configuration to your `Program.cs` file:

```
builder.Services.AddSystemWebAdapters()
    .AddRemoteAppClient(options =>
    {
        options.RemoteAppUrl = new(builder.Configuration["RemoteAppUri"]);
        options.ApiKey = builder.Configuration["RemoteAppApiKey"];
    });
```

With both the ASP.NET and ASP.NET Core apps updated, extension methods can now be used to set up [remote app authentication](../areas/authentication?view=aspnetcore-10.0#remote-authentication) or [remote session](../areas/session?view=aspnetcore-10.0#remote-app-session-state), as needed.

### Enable proxying

To enable proxying from the ASP.NET Core application to the ASP.NET Framework application, you can set up a fallback route that forwards unmatched requests to the legacy application. This allows for a gradual migration where the ASP.NET Core app handles migrated functionality while falling back to the original app for unmigrated features.

1. Install the YARP (Yet Another Reverse Proxy) NuGet package following the [latest guidance](../../../fundamentals/servers/yarp/getting-started?view=aspnetcore-10.0).

2. Add the required using statements to your `Program.cs`:

   ```
   using Microsoft.Extensions.Options;
   using Microsoft.AspNetCore.SystemWebAdapters;
   ```

3. Register the reverse proxy services in your `Program.cs`:

   ```
   builder.Services.AddReverseProxy();
   ```

4. After building the app and configuring other middleware, add the fallback route mapping:

   ```
   var app = builder.Build();

   // Configure your other middleware here (authentication, routing, etc.)

   // Map the fallback route
   app.MapForwarder("/{**catch-all}", app.Services.GetRequiredService<IOptions<RemoteAppClientOptions>>().Value.RemoteAppUrl.OriginalString)

       // Ensures this route has the lowest priority (runs last)
       .WithOrder(int.MaxValue)

       // Skips remaining middleware when this route matches
       .ShortCircuit();

   app.Run();
   ```

## Setup Aspire orchestration

Important

This integration is currently in preview and is published as a prerelease package on NuGet.org. When adding the package, enable prerelease packages in your tooling (for example, select `Include prerelease` in Visual Studio's NuGet Package Manager or use the equivalent prerelease option with the .NET CLI).

1. Add Aspire orchestration for the ASP.NET Framework application

2. Add a new ASP.NET Core application to the solution and add it to your Aspire orchestration

3. Add the `Aspire.Hosting.IncrementalMigration` package to your AppHost project.

4. Configure IIS Express to locally host your framework application and configure incremental migration fallback using the incremental migration extensions:

   ```
   var builder = DistributedApplication.CreateBuilder(args);

   // Add ASP.NET Framework app with IIS Express
   var frameworkApp = builder.AddIISExpressProject<Projects.FrameworkApplication>("framework");

   // Add ASP.NET Core app with fallback to Framework app
   var coreApp = builder.AddProject<Projects.CoreApplication>("core")
       .WithIncrementalMigrationFallback(frameworkApp, options =>
       {
           options.RemoteSession = RemoteSession.Enabled;
           options.RemoteAuthentication = RemoteAuthentication.DefaultScheme;
       });

   builder.Build().Run();
   ```

5. Configure the options of the incremental migration fallback for the scenarios you want to support.

## Configure ServiceDefaults to support ASP.NET Framework

1. Add the package `Aspire.Microsoft.AspNetCore.SystemWebAdapters` to your application.

2. Update the ServiceDefaults project to support .NET Framework. This is based off of the default ServiceDefaults and may differ if you have customized anything.

   * Update the target framework to multitarget:

     ```
     - <TargetFramework>net10.0</TargetFramework>
     + <TargetFrameworks>net481;net10.0</TargetFrameworks>
     ```

   * Update the PackageReferences to account for the different frameworks:

     ```
     <ItemGroup>
         <PackageReference Include="Microsoft.Extensions.Http.Resilience" />
         <PackageReference Include="Microsoft.Extensions.ServiceDiscovery" />
         <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
         <PackageReference Include="OpenTelemetry.Extensions.Hosting" />
         <PackageReference Include="OpenTelemetry.Instrumentation.Http" />
         <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
         <PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" />
     </ItemGroup>

     <ItemGroup Condition=" '$(TargetFramework)' == 'net10.0' ">
         <FrameworkReference Include="Microsoft.AspNetCore.App" />
         <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
     </ItemGroup>

     <ItemGroup Condition=" '$(TargetFramework)' == 'net481' ">
         <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
         <PackageReference Include="OpenTelemetry.Instrumentation.AspNet" />
     </ItemGroup>
     ```

   * To enable telemetry, update the metrics and tracing registrations:

     ```
             builder.Services.AddOpenTelemetry()
                 .WithMetrics(metrics =>
                 {
                     metrics
     + #if NET
                         .AddAspNetCoreInstrumentation()
     + #else
     +                   .AddAspNetInstrumentation()
     + #endif
                         .AddSqlClientInstrumentation()
                         .AddHttpClientInstrumentation()
                         .AddRuntimeInstrumentation();
                 })
                 .WithTracing(tracing =>
                 {
                     tracing.AddSource(builder.Environment.ApplicationName)
     + #if NET
                         .AddAspNetCoreInstrumentation()
     + #else
     +                   .AddAspNetInstrumentation()
     + #endif
                         .AddSqlClientInstrumentation()
                         .AddHttpClientInstrumentation();
                 });
     ```

   * Disable the default endpoints as that only applies for ASP.NET Core:

     ```
     + #if NET
         public static WebApplication MapDefaultEndpoints(this WebApplication app)
         {
             // Default endpoint registrations
         }
     + #endif
     ```

   For a complete, working example of a multi-targeted `ServiceDefaults` configuration that supports both ASP.NET Core and ASP.NET Framework, see the sample `Extensions.cs` file in the System.Web adapters repository: `https://github.com/dotnet/systemweb-adapters/blob/main/samples/ServiceDefaults/Extensions.cs`.

## Configure ASP.NET Framework Application

1. Reference the ServiceDefaults project

2. Add the configuration code to the `Application_Start` method in your `Global.asax.cs` file:

   ```
   protected void Application_Start()
   {
       HttpApplicationHost.RegisterHost(builder =>
       {
           builder.AddServiceDefaults();
           builder.Services.AddSystemWebAdapters();
       });
   }
   ```

## Configure ASP.NET Core Application

1. Reference the ServiceDefaults project

2. Add the System.Web adapters in Programs.cs:

   ```
   var builder = WebApplication.CreateBuilder();

   builder.AddServiceDefaults();
   + builder.AddSystemWebAdapters();

   ...

   var app = builder.Build();

   ...

   + // Must be placed after routing if manually added
   + app.UseSystemWebAdapters();

   ...

   + app.MapRemoteAppFallback()
   +
   +   // Optional, but recommended unless middleware is needed
   +   .ShortCircuit();

   app.Run();
   ```

With this configuration:

1. **Local routes take precedence**: If the ASP.NET Core application has a matching route, it will handle the request locally
2. **Fallback to legacy app**: Unmatched requests are automatically forwarded to the ASP.NET Framework application
3. **Middleware optimization**: The `.ShortCircuit()` method prevents unnecessary middleware execution when forwarding requests

This setup enables a seamless user experience during incremental migration, where users can access both migrated and legacy functionality through a single endpoint.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/azure-apps/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/azure-apps/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Deploy ASP.NET Core apps to Azure App Service

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[Azure App Service](https://azure.microsoft.com/services/app-service/) is a [Microsoft cloud computing platform service](https://azure.microsoft.com/) for hosting web apps, including ASP.NET Core.

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

## Useful resources

[App Service Documentation](/en-us/azure/app-service/) is the home for Azure Apps documentation, tutorials, samples, how-to guides, and other resources. Two notable tutorials that pertain to hosting ASP.NET Core apps are:

[Create an ASP.NET Core web app in Azure](/en-us/azure/app-service/app-service-web-get-started-dotnet)\
Use Visual Studio to create and deploy an ASP.NET Core web app to Azure App Service on Windows.

[Create an ASP.NET Core app in App Service on Linux](/en-us/azure/app-service/containers/quickstart-dotnetcore)\
Use the command line to create and deploy an ASP.NET Core web app to Azure App Service on Linux.

Subscribe to the [App Service Announcements](https://github.com/Azure/app-service-announcements/) repository and monitor the issues. The App Service team regularly posts announcements and scenarios arriving in App Service.

The following articles are available in ASP.NET Core documentation:

[Publish an ASP.NET Core app to Azure with Visual Studio](../../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0)\
Learn how to publish an ASP.NET Core app to Azure App Service using Visual Studio.

[Create your first pipeline](/en-us/azure/devops/pipelines/get-started-yaml)\
Set up a CI build for an ASP.NET Core app, then create a continuous deployment release to Azure App Service.

[Azure Web App sandbox](https://github.com/projectkudu/kudu/wiki/Azure-Web-App-sandbox)\
Discover Azure App Service runtime execution limitations enforced by the Azure Apps platform.

[Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)\
Understand and troubleshoot warnings and errors with ASP.NET Core projects.

## Application configuration

### Platform

The platform architecture (x86/x64) of an App Services app is set in the app's settings in the Azure portal for apps that are hosted on an A-series compute (Basic) or higher hosting tier. Confirm that the app's publish settings (for example, in the Visual Studio [publish profile (.pubxml)](../visual-studio-publish-profiles?view=aspnetcore-10.0)) match the setting in the app's service configuration in the Azure portal.

ASP.NET Core apps can be published [framework-dependent](/en-us/dotnet/core/deploying/) because the runtimes for 64-bit (x64) and 32-bit (x86) apps are present on Azure App Service. The [.NET Core SDK](/en-us/dotnet/core/sdk) available on App Service is 32-bit, but you can deploy 64-bit apps built locally using the [Kudu](https://github.com/projectkudu/kudu/wiki) console or the publish process in Visual Studio. For more information, see the [Publish and deploy the app](#publish-and-deploy-the-app) section.

For apps with native dependencies, runtimes for 32-bit (x86) apps are present on Azure App Service. The [.NET Core SDK](/en-us/dotnet/core/sdk) available on App Service is 32-bit.

For more information on .NET Core framework components and distribution methods, such as information on the .NET Core runtime and the .NET Core SDK, see [About .NET Core: Composition](/en-us/dotnet/core/about#composition).

### Packages

Include the following NuGet packages to provide automatic logging features for apps deployed to Azure App Service:

* [Microsoft.AspNetCore.AzureAppServices.HostingStartup](https://www.nuget.org/packages/Microsoft.AspNetCore.AzureAppServices.HostingStartup/) uses [IHostingStartup](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0) to provide ASP.NET Core logging integration with Azure App Service. The added logging features are provided by the `Microsoft.AspNetCore.AzureAppServicesIntegration` package.
* [Microsoft.AspNetCore.AzureAppServicesIntegration](https://www.nuget.org/packages/Microsoft.AspNetCore.AzureAppServicesIntegration/) executes [AddAzureWebAppDiagnostics](/en-us/dotnet/api/microsoft.extensions.logging.azureappservicesloggerfactoryextensions.addazurewebappdiagnostics) to add Azure App Service diagnostics logging providers in the `Microsoft.Extensions.Logging.AzureAppServices` package.
* [Microsoft.Extensions.Logging.AzureAppServices](https://www.nuget.org/packages/Microsoft.Extensions.Logging.AzureAppServices/) provides logger implementations to support Azure App Service diagnostics logs and log streaming features.

The preceding packages must be explicitly referenced in the app's project file.

## Override app configuration using the Azure portal

App settings in the Azure portal permit you to set environment variables for the app. For more information, see the following resources:

* [Configure an App Service app (Azure documentation)](/en-us/azure/app-service/configure-common)
* [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0#environment-variables-configuration-provider)

## Proxy server and load balancer scenarios

The [IIS integration middleware](../iis/?view=aspnetcore-10.0#enable-the-iisintegration-components), which configures forwarded headers middleware when hosting [out-of-process](../iis/?view=aspnetcore-10.0#out-of-process-hosting-model), and the ASP.NET Core Module are configured to forward the scheme (HTTP/HTTPS) and the remote IP address where the request originated. Additional configuration might be required for apps hosted behind additional proxy servers and load balancers. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0).

## Monitoring and logging

ASP.NET Core apps deployed to App Service automatically receive an App Service extension, **ASP.NET Core Logging Integration**. The extension enables logging integration for ASP.NET Core apps on Azure App Service.

ASP.NET Core apps deployed to App Service automatically receive an App Service extension, **ASP.NET Core Logging Extensions**. The extension enables logging integration for ASP.NET Core apps on Azure App Service.

For monitoring, logging, and troubleshooting information, see the following articles:

[Monitor apps in Azure App Service](/en-us/azure/app-service/web-sites-monitor)\
Learn how to review quotas and metrics for apps and App Service plans.

[Enable diagnostics logging for apps in Azure App Service](/en-us/azure/app-service/web-sites-enable-diagnostic-log)\
Discover how to enable and access diagnostic logging for HTTP status codes, failed requests, and web server activity.

[Handle errors in ASP.NET Core](../../fundamentals/error-handling?view=aspnetcore-10.0)\
Understand common approaches to handling errors in ASP.NET Core apps.

[Troubleshoot ASP.NET Core on Azure App Service and IIS](../../test/troubleshoot-azure-iis?view=aspnetcore-10.0)\
Learn how to diagnose issues with Azure App Service deployments with ASP.NET Core apps.

[Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../azure-iis-errors-reference?view=aspnetcore-10.0)\
See the common deployment configuration errors for apps hosted by Azure App Service/IIS with troubleshooting advice.

## Data Protection key ring and deployment slots

[ASP.NET Core Data Protection keys](../../security/data-protection/implementation/key-management?view=aspnetcore-10.0#data-protection-implementation-key-management) are persisted to the *%HOME%\ASP.NET\DataProtection-Keys* folder. This folder is backed by network storage and is synchronized across all machines hosting the app. Keys aren't protected at rest. This folder supplies the key ring to all instances of an app in a single deployment slot. Separate deployment slots, such as Staging and Production, don't share a key ring.

When swapping between deployment slots, any system using data protection won't be able to decrypt stored data using the key ring inside the previous slot. ASP.NET cookie middleware uses data protection to protect its cookies. This leads to users being signed out of an app that uses the standard ASP.NET cookie middleware. For a slot-independent key ring solution, use an external key ring provider, such as:

* Azure Blob Storage
* Azure Key Vault
* SQL store
* Redis cache

For more information, see [Key storage providers in ASP.NET Core](../../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0).[]()

## Deploy an ASP.NET Core app that uses a .NET Core preview

To deploy an app that uses a preview release of .NET Core, see the following resources. These approaches are also used when the runtime is available but the SDK hasn't been installed on Azure App Service.

* [Specify the .NET Core SDK Version using Azure Pipelines](#specify-the-net-core-sdk-version-using-azure-pipelines)
* [Deploy a self-contained preview app](#deploy-a-self-contained-preview-app)
* [Use Docker with Web Apps for containers](#use-docker-with-web-apps-for-containers)
* [Install the preview site extension](#install-the-preview-site-extension)

See [Select the .NET Core version to use](/en-us/dotnet/core/versions/selection) for information on selecting the version of the .NET SDK for self-contained deployments.

### Specify the .NET Core SDK Version using Azure Pipelines

Use [Azure App Service CI/CD scenarios](/en-us/azure/app-service/deploy-continuous-deployment) to set up a continuous integration build with Azure DevOps. After the Azure DevOps build is created, optionally configure the build to use a specific SDK version.

#### Specify the .NET Core SDK version

When using the App Service deployment center to create an Azure DevOps build, the default build pipeline includes steps for `Restore`, `Build`, `Test`, and `Publish`. To specify the SDK version, select the **Add (+)** button in the Agent job list to add a new step. Search for **.NET Core SDK** in the search bar.

Move the step into the first position in the build so that the steps following it use the specified version of the .NET Core SDK. Specify the version of the .NET Core SDK. In this example, the SDK is set to `3.0.100`.

To publish a [self-contained deployment (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd), configure SCD in the `Publish` step and provide the [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog).

### Deploy a self-contained preview app

A [self-contained deployment (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) that targets a preview runtime carries the preview runtime in the deployment.

When deploying a self-contained app:

* The site in Azure App Service doesn't require the [preview site extension](#install-the-preview-site-extension).
* The app must be published following a different approach than when publishing for a [framework-dependent deployment (FDD)](/en-us/dotnet/core/deploying#framework-dependent-deployments-fdd).

Follow the guidance in the [Deploy the app self-contained](#deploy-the-app-self-contained) section.

### Use Docker with Web Apps for containers

The Docker Hub at `https://hub.docker.com/_/microsoft-dotnet` contains the latest preview Docker images. The images can be used as a base image. Use the image and deploy to Web Apps for Containers normally.

### Install the preview site extension

If a problem occurs using the preview site extension, open an [dotnet/AspNetCore issue](https://github.com/dotnet/AspNetCore/issues).

1. From the Azure portal, navigate to the App Service.
2. Select the web app.
3. Type "ex" in the search box to filter for "Extensions" or scroll down the list of management tools.
4. Select **Extensions**.
5. Select **Add**.
6. Select the **ASP.NET Core {X.Y} ({x64|x86}) Runtime** extension from the list, where `{X.Y}` is the ASP.NET Core preview version and `{x64|x86}` specifies the platform.
7. Select **OK** to accept the legal terms.
8. Select **OK** to install the extension.

When the operation completes, the latest .NET Core preview is installed. Verify the installation:

1. Select **Advanced Tools**.

2. Select **Go** in **Advanced Tools**.

3. Select the **Debug console** > **PowerShell** menu item.

4. At the PowerShell prompt, execute the following command. Substitute the ASP.NET Core runtime version for `{X.Y}` and the platform for `{PLATFORM}` in the command:

   ```
   Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.{PLATFORM}\
   ```

   The command returns `True` when the x64 preview runtime is installed.

Note

The platform architecture (x86/x64) of an App Services app is set in the app's settings in the Azure portal for apps that are hosted on an A-series compute (Basic) or higher hosting tier. Confirm that the app's publish settings (for example, in the Visual Studio [publish profile (.pubxml)](../visual-studio-publish-profiles?view=aspnetcore-10.0)) match the setting in the app's service configuration in the Azure portal.

If the app is run in in-process mode and the platform architecture is configured for 64-bit (x64), the ASP.NET Core Module uses the 64-bit preview runtime, if present. Install the **ASP.NET Core {X.Y} (x64) Runtime** extension using the Azure portal.

After installing the x64 preview runtime, run the following command in the Azure Kudu PowerShell command window to verify the installation. Substitute the ASP.NET Core runtime version for `{X.Y}` in the following command:

```
Test-Path D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x64\
```

The command returns `True` when the x64 preview runtime is installed.

**Use the preview site extension with an ARM template**

If an ARM template is used to create and deploy apps, the `Microsoft.Web/sites/siteextensions` resource type can be used to add the site extension to a web app. In the following example, the .NET 5 (x64) Runtime site extension (`AspNetCoreRuntime.5.0.x64`) is added to the app:

```
{
    ...
    "parameters": {
        "site_name": {
            "defaultValue": "{SITE NAME}",
            "type": "String"
        },
        ...
    },       
    ...
    "resources": [
        ...
        {
            "type": "Microsoft.Web/sites/siteextensions",
            "apiVersion": "2018-11-01",
            "name": "[concat(parameters('site_name'), '/AspNetCoreRuntime.5.0.x64')]",
            "location": "[resourceGroup().location]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/sites', parameters('site_name'))]"
            ]
        }
    ]
}
    
```

For the placeholder `{SITE NAME}`, use the app's name in Azure App Service (for example, `contoso`).

## Publish and deploy the app

For a 64-bit deployment:

* Use a 64-bit .NET Core SDK to build a 64-bit app.
* Set the **Platform** to **64 Bit** in the App Service's **Configuration** > **General settings**. The app must use a Basic or higher service plan to enable the choice of platform bitness.

### Deploy the app framework-dependent

Apps published as framework-dependent are cross-platform and don't include the .NET runtime in the deployment. Azure App Service includes the .NET runtime.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. Right-click the project in **Solution Explorer** and select **Publish**. Alternatively, select **Build** > **Publish {Application Name}** from the Visual Studio toolbar.

2. In the **Publish** dialog, select **Azure** > **Next**.

3. Select the Azure service.

4. Select **Advanced**. The **Publish** dialog opens.

5. Select a Resource group and Hosting plan, or create new ones.

6. Select **Finish**.

7. In the **Publish** page:

   * For **Configuration**, select the pen icon **Edit Configuration**:

     * Confirm that the **Release** configuration is selected.
     * In the **Deployment Mode** drop-down list, select **Framework-Dependent**.
     * In the **Target Runtime** drop-down list, select the desired runtime. The default is `win-x86`.

   * To remove additional files upon deployment, open **File Publish Options** and select the checkbox to remove additional files at the destination.

   * Select **Save**.

   * Select **Publish**.

1) In the project file, don't specify a [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog).

2) From a command shell, publish the app in Release configuration with the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command. In the following example, the app is published as a framework-dependent app:

   ```
   dotnet publish --configuration Release
   ```

3) Move the contents of the *bin/Release/{TARGET FRAMEWORK}/publish* directory to the site in App Service. If dragging the *publish* folder contents from your local hard drive or network share directly to App Service in the [Kudu](https://github.com/projectkudu/kudu/wiki) console, drag the files to the `D:\home\site\wwwroot` folder in the Kudu console.

### Deploy the app self-contained

Publishing an app as self-contained produces a platform-specific executable. The output publishing folder contains all components of the app, including the .NET libraries and target runtime. For more information, see \[Publish self-contained]/dotnet/core/deploying/#publish-self-contained). Use Visual Studio or the .NET CLI for a [self-contained deployment (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. Right-click the project in **Solution Explorer** and select **Publish**. Alternatively, select **Build** > **Publish {Application Name}** from the Visual Studio toolbar.

2. In the **Publish** dialog, select **Azure** > **Next**.

3. Select the Azure service.

4. Select **Advanced**. The **Publish** dialog opens.

5. Select a Resource group and Hosting plan, or create new ones.

6. Select **Finish**.

7. In the **Publish** page:

   * For **Configuration**, select the pen icon **Edit Configuration**:

     * Confirm that the **Release** configuration is selected.
     * In the **Deployment Mode** drop-down list, select **Self-Contained**.
     * In the **Target Runtime** drop-down list, select the desired runtime. The default is `win-x86`.

   * To remove additional files upon deployment, open **File Publish Options** and select the checkbox to remove additional files at the destination.

   * Select **Save**.

   * Select **Publish**.

1) In the project file, specify one or more [Runtime Identifiers (RIDs)](/en-us/dotnet/core/rid-catalog). Use `<RuntimeIdentifier>` for a single RID, or use `<RuntimeIdentifiers>` to provide a semicolon-delimited list of multiple RIDs. In the following example, the `win-x86` RID is specified:

   ```
   <PropertyGroup>
     <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
     <RuntimeIdentifier>win-x86</RuntimeIdentifier>
   </PropertyGroup>
   ```

2) From a command shell, publish the app in Release configuration for the host's runtime with the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command. In the following example, the app is published for the `win-x86` RID. The RID supplied to the `--runtime` option must be provided in the `<RuntimeIdentifier>` (or `<RuntimeIdentifiers>`) property in the project file.

   ```
   dotnet publish --configuration Release --runtime win-x86 --self-contained
   ```

3) Move the contents of the *bin/Release/{TARGET FRAMEWORK}/{RUNTIME IDENTIFIER}/publish* directory to the site in App Service. If dragging the *publish* folder contents from your local hard drive or network share directly to App Service in the [Kudu console](https://github.com/projectkudu/kudu/wiki), drag the files to the `D:\home\site\wwwroot` folder in the Kudu console.

## Protocol settings (HTTPS)

Secure protocol bindings allow specifying a certificate to use when responding to requests over HTTPS. Binding requires a valid private certificate (*.pfx*) issued for the specific hostname. For more information, see [Tutorial: Bind an existing custom SSL certificate to Azure App Service](/en-us/azure/app-service/app-service-web-tutorial-custom-ssl).

## Transform web.config

If you need to transform *web.config* on publish (for example, set environment variables based on the configuration, profile, or environment), see [Transform web.config](../iis/transform-webconfig?view=aspnetcore-10.0).

## Related content

* [App Service overview](/en-us/azure/app-service/app-service-web-overview)
* [Azure App Service diagnostics overview](/en-us/azure/app-service/app-service-diagnostics)
* [Host ASP.NET Core in a web farm](../web-farm?view=aspnetcore-10.0)
* [Tutorial: Connect to SQL Database from .NET App Service without secrets using a managed identity](/en-us/azure/app-service/tutorial-connect-msi-sql-database?tabs=efcore%2Cdotnetcore)

Azure App Service on Windows Server uses [Internet Information Services (IIS)](https://www.iis.net/). [Kestrel and YARP](https://devblogs.microsoft.com/dotnet/bringing-kestrel-and-yarp-to-azure-app-services/) on the front end provides the load balancer. The following topics pertain to the underlying IIS technology:

* [Host ASP.NET Core on Windows with IIS](../iis/?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0)
* [IIS modules with ASP.NET Core](../iis/modules?view=aspnetcore-10.0)
* [Windows Server - IT administrator content for current and previous releases](/en-us/windows-server/windows-server-versions)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/parameter-binding.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Parameter Binding in Minimal API apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Note

[C# union types](/en-us/dotnet/csharp/language-reference/builtin-types/union) are supported only as the body (as JSON). Non-body sources—route values, query string, headers, and form values—bind string values without JSON parsing, so they can't dispatch to a union case.

The following `GET` route handler uses some of these parameter binding sources:

```
var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }
```

The following table shows the relationship between the parameters used in the preceding example and the associated binding sources.

| Parameter      | Binding Source                   |
| -------------- | -------------------------------- |
| `id`           | route value                      |
| `page`         | query string                     |
| `customHeader` | header                           |
| `service`      | Provided by dependency injection |

The HTTP methods `GET`, `HEAD`, `OPTIONS`, and `DELETE` don't implicitly bind from body. To bind from body (as JSON) for these HTTP methods, [bind explicitly](#explicit-parameter-binding) with [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) or read from the [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest).

The following example POST route handler uses a binding source of body (as JSON) for the `person` parameter:

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);
```

The parameters in the preceding examples are all bound from request data automatically. To demonstrate the convenience that parameter binding provides, the following route handlers show how to read request data directly from the request:

```
app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});
```

## Explicit Parameter Binding

Attributes can be used to explicitly declare where parameters are bound from.

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
```

| Parameter     | Binding Source                        |
| ------------- | ------------------------------------- |
| `id`          | route value with the name `id`        |
| `page`        | query string with the name `"p"`      |
| `service`     | Provided by dependency injection      |
| `contentType` | header with the name `"Content-Type"` |

### Explicit binding from form values

The [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) attribute binds form values:

```
app.MapPost("/todos", async ([FromForm] string name,
    [FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = name,
        Visibility = visibility
    };

    if (attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await attachment.CopyToAsync(stream);
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.
```

An alternative is to use the [`[AsParameters]`](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute) attribute with a custom type that has properties annotated with `[FromForm]`. For example, the following code binds from form values to properties of the `NewTodoRequest` record struct:

```
app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = request.Name,
        Visibility = request.Visibility
    };

    if (request.Attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await request.Attachment.CopyToAsync(stream);

        todo.Attachment = attachmentName;
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.
```

```
public record struct NewTodoRequest([FromForm] string Name,
    [FromForm] Visibility Visibility, IFormFile? Attachment);
```

For more information, see the section on [AsParameters](#parameter-binding-for-argument-lists-with-asparameters) later in this article.

The [complete sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/IFormFile) is in the [AspNetCore.Docs.Samples](https://github.com/dotnet/AspNetCore.Docs.Samples) repository.

### Secure binding from IFormFile and IFormFileCollection

Complex form binding is supported using [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection) using the [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute):

```
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
    ([FromForm] FileUploadForm fileUploadForm, HttpContext context,
                                                IAntiforgery antiforgery) =>
{
    // Server-side validation. The client-side pattern can be bypassed, so the
    // name must be validated here too.
    if (!MyUtils.IsValidFileName(fileUploadForm.Name))
    {
        return TypedResults.BadRequest("Invalid file name.");
    }

    await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
              fileUploadForm.Name!, app.Environment.ContentRootPath);
    return TypedResults.Ok($"Your file with the description:" +
        $" {fileUploadForm.Description} has been uploaded successfully");
});

app.Run();
```

Parameters bound to the request with `[FromForm]` include an [antiforgery token](../../security/anti-request-forgery?view=aspnetcore-10.0). The antiforgery token is validated when the request is processed. For more information, see [Antiforgery with Minimal APIs](../../security/anti-request-forgery?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#afwma).

For more information, see [Form binding in Minimal APIs](https://andrewlock.net/exploring-the-dotnet-8-preview-form-binding-in-minimal-apis/).

The [complete sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/FormBinding) is in the [AspNetCore.Docs.Samples](https://github.com/dotnet/AspNetCore.Docs.Samples) repository.

## Parameter binding with dependency injection

Parameter binding for Minimal APIs binds parameters through [dependency injection](../dependency-injection?view=aspnetcore-10.0) when the type is configured as a service. It's not necessary to explicitly apply the [`[FromServices]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) attribute to a parameter. In the following code, both actions return the time:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
```

## Optional parameters

Parameters declared in route handlers are treated as required:

* If a request matches the route, the route handler only runs if all required parameters are provided in the request.
* Failure to provide all required parameters results in an error.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
```

| URI                      | result                                                                                            |
| ------------------------ | ------------------------------------------------------------------------------------------------- |
| `/products?pageNumber=3` | 3 returned                                                                                        |
| `/products`              | `BadHttpRequestException`: Required parameter "int pageNumber" wasn't provided from query string. |
| `/products/1`            | HTTP 404 error, no matching route                                                                 |

To make `pageNumber` optional, define the type as optional or provide a default value:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
```

| URI                      | result     |
| ------------------------ | ---------- |
| `/products?pageNumber=3` | 3 returned |
| `/products`              | 1 returned |
| `/products2`             | 1 returned |

The preceding nullable and default value applies to all sources:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();
```

The preceding code calls the method with a null product if no request body is sent.

**NOTE**: If invalid data is provided and the parameter is nullable, the route handler is ***not*** run.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
```

| URI                        | result                                                                                       |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `/products?pageNumber=3`   | `3` returned                                                                                 |
| `/products`                | `1` returned                                                                                 |
| `/products?pageNumber=two` | `BadHttpRequestException`: Failed to bind parameter `"Nullable<int> pageNumber"` from "two". |
| `/products/two`            | HTTP 404 error, no matching route                                                            |

See the [Binding Failures](#bf) section for more information.

## Special types

The following types are bound without explicit attributes:

* [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext): The context which holds all the information about the current HTTP request or response:

  ```
  app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
  ```

* [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) and [HttpResponse](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse): The HTTP request and HTTP response:

  ```
  app.MapGet("/", (HttpRequest request, HttpResponse response) =>
      response.WriteAsync($"Hello World {request.Query["name"]}"));
  ```

* [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken): The cancellation token associated with the current HTTP request:

  ```
  app.MapGet("/", async (CancellationToken cancellationToken) => 
      await MakeLongRunningRequestAsync(cancellationToken));
  ```

* [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal): The user associated with the request, bound from [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user):

  ```
  app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
  ```

[]()

### Bind the request body as a `Stream` or `PipeReader`

The request body can bind as a [`Stream`](/en-us/dotnet/api/system.io.stream) or [`PipeReader`](/en-us/dotnet/api/system.io.pipelines.pipereader) to efficiently support scenarios where the user has to process data and:

* Store the data to blob storage or enqueue the data to a queue provider.
* Process the stored data with a worker process or cloud function.

For example, the data might be enqueued to [Azure Queue storage](/en-us/azure/storage/queues/storage-queues-introduction) or stored in [Azure Blob storage](/en-us/azure/storage/blobs/storage-blobs-introduction).

The following code implements a background queue:

```
using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}
```

The following code binds the request body to a `Stream`:

```
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
```

The following code shows the complete `Program.cs` file:

```
using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
```

* When reading data, the `Stream` is the same object as `HttpRequest.Body`.
* The request body isn't buffered by default. After the body is read, it's not rewindable. The stream can't be read multiple times.
* The `Stream` and `PipeReader` aren't usable outside of the minimal action handler as the underlying buffers will be disposed or reused.

### File uploads using IFormFile and IFormFileCollection

File uploads using `IFormFile` and `IFormFileCollection` in Minimal APIs require `multipart/form-data` encoding. The parameter name in the route handler must match the form field name in the request. Minimal APIs don't support binding the entire request body directly to an `IFormFile` parameter without form encoding.

If you need to bind the entire request body, for example, when working with JSON, binary data, or other content types, see:

* [Bind the request body as a Stream or PipeReader](#bind-the-request-body-as-a-stream-or-pipereader)
* [Explicit Parameter Binding](#explicit-parameter-binding)

The following code uses [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection) to upload file:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();
```

Authenticated file upload requests are supported using an [Authorization header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Authorization), a [client certificate](/en-us/aspnet/core/security/authentication/certauth), or a cookie header.

[]()

### Binding to forms with IFormCollection, IFormFile, and IFormFileCollection

Binding from form-based parameters using [IFormCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection), [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile), and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection) is supported. [OpenAPI](../openapi/aspnetcore-openapi?view=aspnetcore-10.0) metadata is inferred for form parameters to support integration with [Swagger UI](../../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0).

The following code uploads files using inferred binding from the `IFormFile` type:

```
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();
```

***Warning:*** When implementing forms, the app ***must prevent*** [Cross-Site Request Forgery (XSRF/CSRF) attacks](../../security/anti-request-forgery?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#afwma). In the preceding code, the [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) service is used to prevent XSRF attacks by generating and validation an antiforgery token:

```
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();
```

For more information on XSRF attacks, see [Antiforgery with Minimal APIs](../../security/anti-request-forgery?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#afwma)

For more information, see [Form binding in Minimal APIs](https://andrewlock.net/exploring-the-dotnet-8-preview-form-binding-in-minimal-apis/);

#### IFormFile collection binding behavior

The following table summarizes how different [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) collection parameter types are bound in Minimal APIs. The general guidance that the parameter name in the route handler must match the form field name applies to [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and supported named file collections such as [IReadOnlyList\<T>](/en-us/dotnet/api/system.collections.generic.ireadonlylist-1), but not to [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection), which binds all uploaded files.

| Parameter type                                                                                                                                                 | Bound value                                                                                                                                                        | Honors parameter name? |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------- |
| [`IFormFileCollection`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection)                                                                       | All files in [`HttpContext.Request.Form.Files`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection.files#microsoft-aspnetcore-http-iformcollection-files) | No                     |
| [`IFormFile`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile)                                                                                           | The single file whose form field name matches the parameter name                                                                                                   | Yes                    |
| [`IReadOnlyList<IFormFile>`](/en-us/dotnet/api/system.collections.generic.ireadonlylist-1)                                                                     | All files whose form field name matches the parameter name                                                                                                         | Yes                    |
| Other [`IFormFile`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) collection types (`IEnumerable<IFormFile>`, `List<IFormFile>`, `IFormFile[]`, etc.) | Not supported — parameter is not populated                                                                                                                         | N/A                    |

Use [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection) when you need all uploaded files regardless of form field name. Use [IReadOnlyList\<T>](/en-us/dotnet/api/system.collections.generic.ireadonlylist-1) when you need only the files whose form field name matches the parameter name.

Note

The [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) collection binding rules in the preceding table also apply to properties on [`[AsParameters]`](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute) types and form-mapped complex types, since the form-mapping layer is shared with Blazor.

[]()

## Bind to collections and complex types from forms

Binding is supported for:

* Collections, for example [List](/en-us/dotnet/api/system.collections.generic.list-1) and [Dictionary](/en-us/dotnet/api/system.collections.generic.dictionary-2)
* Complex types, for example, `Todo` or `Project`

Warning

Complex-type form mapping with [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) in Minimal APIs doesn't use MVC model binding. Attributes in the [Microsoft.AspNetCore.Mvc.ModelBinding](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding) namespace, such as [`[BindNever]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindneverattribute) and [`[BindRequired]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindrequiredattribute), aren't supported. Don't use these attributes to prevent overposting. Instead, use a dedicated input model or data transfer object (DTO) that includes only the properties clients are allowed to modify.

The following code shows:

* A minimal endpoint that binds a multi-part form input to a complex object.
* How to use the antiforgery services to support the generation and validation of antiforgery tokens.

```
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
        <html><body>
           <form action="/todo" method="POST" enctype="multipart/form-data">
               <input name="{token.FormFieldName}" 
                                type="hidden" value="{token.RequestToken}" />
               <input type="text" name="name" />
               <input type="date" name="dueDate" />
               <input type="checkbox" name="isCompleted" value="true" />
               <input type="submit" />
               <input name="isCompleted" type="hidden" value="false" /> 
           </form>
        </body></html>
    """;
    return Results.Content(html, "text/html");
});

app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>> 
               ([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
    try
    {
        await antiforgery.ValidateRequestAsync(context);
        return TypedResults.Ok(todo);
    }
    catch (AntiforgeryValidationException e)
    {
        return TypedResults.BadRequest("Invalid antiforgery token");
    }
});

app.Run();

class Todo
{
    public string Name { get; set; } = string.Empty;
    public bool IsCompleted { get; set; } = false;
    public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}
```

In the preceding code:

* The target parameter ***must*** be annotated with the [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) attribute to disambiguate from parameters that should be read from the JSON body.
* Binding from complex or collection types is ***not*** supported for Minimal APIs that are compiled with the Request Delegate Generator.
* The markup shows an additional hidden input with a name of `isCompleted` and a value of `false`. If the `isCompleted` checkbox is checked when the form is submitted, both values `true` and `false` are submitted as values. If the checkbox is unchecked, only the hidden input value `false` is submitted. The ASP.NET Core form-mapping process reads only the first value when binding to a `bool` value, which results in `true` for checked checkboxes and `false` for unchecked checkboxes.

An example of the form data submitted to the preceding endpoint looks as follows:

```
__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false
```

[]()

## Bind arrays and string values from headers and query strings

The following code demonstrates binding query strings to an array of primitive types, string arrays, and [StringValues](/en-us/dotnet/api/microsoft.extensions.primitives.stringvalues):

```
// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
```

Binding query strings or header values to an array of complex types is supported when the type has `TryParse` implemented. The following code binds to a string array and returns all the items with the specified tags:

```
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});
```

The following code shows the model and the required `TryParse` implementation:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}
```

The following code binds to an `int` array:

```
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});
```

To test the preceding code, add the following endpoint to populate the database with `Todo` items:

```
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});
```

Use a tool like [`HttpRepl`](../../web-api/http-repl/?view=aspnetcore-10.0) to pass the following data to the previous endpoint:

```
[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]
```

The following code binds to the header key `X-Todo-Id` and returns the `Todo` items with matching `Id` values:

```
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});
```

Note

When binding a non-nullable `string[]` from a query string, the absence of any matching query string value results in an empty array instead of `null`. If the parameter is declared as nullable (for example, `string[]?`) or has a default value of `null` (for example, `[FromQuery] string[]? names = null`), omitting the query parameter results in `null`.

[]()

## Parameter binding for argument lists with \[AsParameters]

[AsParametersAttribute](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute) enables simple parameter binding to types and not complex or recursive model binding.

Consider the following code:

```
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.
```

Consider the following `GET` endpoint:

```
app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
```

The following `struct` can be used to replace the preceding highlighted parameters:

```
struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}
```

The refactored `GET` endpoint uses the preceding `struct` with the [AsParameters](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute?view=aspnetcore-7.0\&preserve-view=true) attribute:

```
app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
```

The following code shows additional endpoints in the app:

```
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.Name
    };

    Db.Todos.Add(todoItem);
    await Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.IsComplete;

    await Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
    if (await Db.Todos.FindAsync(Id) is Todo todo)
    {
        Db.Todos.Remove(todo);
        await Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});
```

The following classes are used to refactor the parameter lists:

```
class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}
```

The following code shows the refactored endpoints using `AsParameters` and the preceding `struct` and classes:

```
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});
```

The following [`record`](/en-us/dotnet/csharp/language-reference/builtin-types/record) types can be used to replace the preceding parameters:

```
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
```

Using a `struct` with `AsParameters` can be more performant than using a `record` type.

The [complete sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/arg-lists) in the [AspNetCore.Docs.Samples](https://github.com/dotnet/AspNetCore.Docs.Samples) repository.

## Custom Binding

There are three ways to customize parameter binding:

1. For route, query, and header binding sources, bind custom types by adding a static `TryParse` method for the type.
2. Control the binding process by implementing a `BindAsync` method on a type.
3. For advanced scenarios, implement the [IBindableFromHttpContext\<TSelf>](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1) interface to provide custom binding logic directly from the `HttpContext`.

### TryParse

`TryParse` has two APIs:

```
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
```

The following code displays `Point: 12.3, 10.1` with the URI `/map?Point=12.3,10.1`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}
```

### BindAsync

`BindAsync` has the following APIs:

```
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
```

The following code displays `SortBy:xyz, SortDirection:Desc, CurrentPage:99` with the URI `/products?SortBy=xyz&SortDir=Desc&Page=99`:

```
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}
```

[]()

### Custom parameter binding with `IBindableFromHttpContext`

ASP.NET Core provides support for custom parameter binding in Minimal APIs using the [IBindableFromHttpContext\<TSelf>](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1) interface. This interface, introduced with C# 11's static abstract members, allows you to create types that can be bound from an HTTP context directly in route handler parameters.

```
public interface IBindableFromHttpContext<TSelf>
    where TSelf : class, IBindableFromHttpContext<TSelf>
{
    static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}
```

By implementing the [IBindableFromHttpContext\<TSelf>](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1), you can create custom types that handle their own binding logic from the `HttpContext`. When a route handler includes a parameter of this type, the framework automatically calls the static `BindAsync` method to create the instance:

```
using CustomBindingExample;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/", () => "Hello, IBindableFromHttpContext example!");

app.MapGet("/custom-binding", (CustomBoundParameter param) =>
{
    return $"Value from custom binding: {param.Value}";
});

app.MapGet("/combined/{id}", (int id, CustomBoundParameter param) =>
{
    return $"ID: {id}, Custom Value: {param.Value}";
});
```

The following is an example implementation of a custom parameter that binds from an HTTP header:

```
using System.Reflection;

namespace CustomBindingExample;

public class CustomBoundParameter : IBindableFromHttpContext<CustomBoundParameter>
{
    public string Value { get; init; } = default!;

    public static ValueTask<CustomBoundParameter?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        // Custom binding logic here
        // This example reads from a custom header
        var value = context.Request.Headers["X-Custom-Header"].ToString();
        
        // If no header was provided, you could fall back to a query parameter
        if (string.IsNullOrEmpty(value))
        {
            value = context.Request.Query["customValue"].ToString();
        }
        
        return ValueTask.FromResult<CustomBoundParameter?>(new CustomBoundParameter 
        {
            Value = value
        });
    }
}
```

You can also implement validation within your custom binding logic:

```
app.MapGet("/validated", (ValidatedParameter param) =>
{
    if (string.IsNullOrEmpty(param.Value))
    {
        return Results.BadRequest("Value cannot be empty");
    }
    
    return Results.Ok($"Validated value: {param.Value}");
});
```

[View or download the sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/minimal-apis/samples/10.0/CustomBindingExample) ([how to download](../../overview?view=aspnetcore-10.0#how-to-download-a-sample))

## Binding failures

When binding fails, the framework logs a debug message and returns various status codes to the client depending on the failure mode.

| Failure mode                                | Nullable Parameter Type | Binding Source     | Status code |
| ------------------------------------------- | ----------------------- | ------------------ | ----------- |
| `{ParameterType}.TryParse` returns `false`  | yes                     | route/query/header | 400         |
| `{ParameterType}.BindAsync` returns `null`  | yes                     | custom             | 400         |
| `{ParameterType}.BindAsync` throws          | doesn't matter          | custom             | 500         |
| Failure to deserialize JSON body            | doesn't matter          | body               | 400         |
| Wrong content type (not `application/json`) | doesn't matter          | body               | 415         |

## Binding Precedence

The rules for determining a binding source from a parameter:

1. Explicit attribute defined on parameter (From\* attributes) in the following order:

   1. Route values: [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute)
   2. Query string: [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute)
   3. Header: [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute)
   4. Body: [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute)
   5. Form: [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute)
   6. Service: [`[FromServices]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute)
   7. Parameter values: [`[AsParameters]`](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute)

2. Special types

   1. [`HttpContext`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext)
   2. [`HttpRequest`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) ([`HttpContext.Request`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.request#microsoft-aspnetcore-http-httpcontext-request))
   3. [`HttpResponse`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse) ([`HttpContext.Response`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.response#microsoft-aspnetcore-http-httpcontext-response))
   4. [`ClaimsPrincipal`](/en-us/dotnet/api/system.security.claims.claimsprincipal) ([`HttpContext.User`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user))
   5. [`CancellationToken`](/en-us/dotnet/api/system.threading.cancellationtoken) ([`HttpContext.RequestAborted`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted))
   6. [`IFormCollection`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection) ([`HttpContext.Request.Form`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection))
   7. [`IFormFileCollection`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection) ([`HttpContext.Request.Form.Files`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection.files#microsoft-aspnetcore-http-iformcollection-files))
   8. [`IFormFile`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) ([`HttpContext.Request.Form.Files[paramName]`](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection.item#microsoft-aspnetcore-http-iformfilecollection-item\(system-string\)))
   9. [`Stream`](/en-us/dotnet/api/system.io.stream) ([`HttpContext.Request.Body`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.body#microsoft-aspnetcore-http-httprequest-body))
   10. [`PipeReader`](/en-us/dotnet/api/system.io.pipelines.pipereader) ([`HttpContext.Request.BodyReader`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader#microsoft-aspnetcore-http-httprequest-bodyreader))

3. Parameter type has a valid static [`BindAsync`](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1.bindasync) method.

4. Parameter type is a string or has a valid static [`TryParse`](/en-us/dotnet/api/system.iparsable-1.tryparse) method.

   1. If the parameter name exists in the route template for example, `app.Map("/todo/{id}", (int id) => {});`, then it's bound from the route.
   2. Bound from the query string.

5. If the parameter type is a service provided by dependency injection, it uses that service as the source.

6. The parameter is from the body.

## Configure JSON deserialization options for body binding

The body binding source uses [System.Text.Json](/en-us/dotnet/api/system.text.json) for deserialization. It is ***not*** possible to change this default, but JSON serialization and deserialization options can be configured.

### Configure JSON deserialization options globally

Options that apply globally for an app can be configured by invoking [ConfigureHttpJsonOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpjsonserviceextensions.configurehttpjsonoptions). The following example includes public fields and formats JSON output.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }
```

Since the sample code configures both serialization and deserialization, it can read `NameField` and include `NameField` in the output JSON.

### Configure JSON deserialization options for an endpoint

[ReadFromJsonAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequestjsonextensions.readfromjsonasync) has overloads that accept a [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object. The following example includes public fields and formats JSON output.

```
using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }
```

Since the preceding code applies the customized options only to deserialization, the output JSON excludes `NameField`.

## Read the request body

Read the request body directly using a [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) or [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) parameter:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();
```

The preceding code:

* Accesses the request body using [HttpRequest.BodyReader](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader#microsoft-aspnetcore-http-httprequest-bodyreader).
* Copies the request body to a local file.

```
var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }
```

The following table shows the relationship between the parameters used in the preceding example and the associated binding sources.

| Parameter      | Binding Source                   |
| -------------- | -------------------------------- |
| `id`           | route value                      |
| `page`         | query string                     |
| `customHeader` | header                           |
| `service`      | Provided by dependency injection |

The HTTP methods `GET`, `HEAD`, `OPTIONS`, and `DELETE` don't implicitly bind from body. To bind from body (as JSON) for these HTTP methods, [bind explicitly](#explicit-parameter-binding) with [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) or read from the [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest).

The following example POST route handler uses a binding source of body (as JSON) for the `person` parameter:

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);
```

The parameters in the preceding examples are all bound from request data automatically. To demonstrate the convenience that parameter binding provides, the following route handlers show how to read request data directly from the request:

```
app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});
```

### Explicit Parameter Binding

Attributes can be used to explicitly declare where parameters are bound from.

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
```

| Parameter     | Binding Source                        |
| ------------- | ------------------------------------- |
| `id`          | route value with the name `id`        |
| `page`        | query string with the name `"p"`      |
| `service`     | Provided by dependency injection      |
| `contentType` | header with the name `"Content-Type"` |

Note

Binding from form values is ***not*** natively supported in .NET 6 and 7.

### Parameter binding with dependency injection

Parameter binding for Minimal APIs binds parameters through [dependency injection](../dependency-injection?view=aspnetcore-10.0) when the type is configured as a service. It's not necessary to explicitly apply the [`[FromServices]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) attribute to a parameter. In the following code, both actions return the time:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
```

### Optional parameters

Parameters declared in route handlers are treated as required:

* If a request matches the route, the route handler only runs if all required parameters are provided in the request.
* Failure to provide all required parameters results in an error.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
```

| URI                      | result                                                                                             |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `/products?pageNumber=3` | 3 returned                                                                                         |
| `/products`              | `BadHttpRequestException`: Required parameter "int pageNumber" was not provided from query string. |
| `/products/1`            | HTTP 404 error, no matching route                                                                  |

To make `pageNumber` optional, define the type as optional or provide a default value:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
```

| URI                      | result     |
| ------------------------ | ---------- |
| `/products?pageNumber=3` | 3 returned |
| `/products`              | 1 returned |
| `/products2`             | 1 returned |

The preceding nullable and default value applies to all sources:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();
```

The preceding code calls the method with a null product if no request body is sent.

**NOTE**: If invalid data is provided and the parameter is nullable, the route handler is ***not*** run.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
```

| URI                        | result                                                                                       |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `/products?pageNumber=3`   | `3` returned                                                                                 |
| `/products`                | `1` returned                                                                                 |
| `/products?pageNumber=two` | `BadHttpRequestException`: Failed to bind parameter `"Nullable<int> pageNumber"` from "two". |
| `/products/two`            | HTTP 404 error, no matching route                                                            |

See the [Binding Failures](#bf) section for more information.

### Special types

The following types are bound without explicit attributes:

* [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext): The context which holds all the information about the current HTTP request or response:

  ```
  app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
  ```

* [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) and [HttpResponse](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse): The HTTP request and HTTP response:

  ```
  app.MapGet("/", (HttpRequest request, HttpResponse response) =>
      response.WriteAsync($"Hello World {request.Query["name"]}"));
  ```

* [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken): The cancellation token associated with the current HTTP request:

  ```
  app.MapGet("/", async (CancellationToken cancellationToken) => 
      await MakeLongRunningRequestAsync(cancellationToken));
  ```

* [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal): The user associated with the request, bound from [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user):

  ```
  app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
  ```

```
using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}
```

The following code binds the request body to a `Stream`:

```
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
```

The following code shows the complete `Program.cs` file:

```
using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
```

* When reading data, the `Stream` is the same object as `HttpRequest.Body`.
* The request body isn't buffered by default. After the body is read, it's not rewindable. The stream can't be read multiple times.
* The `Stream` and `PipeReader` aren't usable outside of the minimal action handler as the underlying buffers will be disposed or reused.

#### File uploads using IFormFile and IFormFileCollection

The following code uses [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection) to upload file:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();
```

Authenticated file upload requests are supported using an [Authorization header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Authorization), a [client certificate](/en-us/aspnet/core/security/authentication/certauth), or a cookie header.

There is no built-in support for [antiforgery](/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-7.0\&preserve-view=true#anti7) in ASP.NET Core in .NET 7. [Antiforgery is available in ASP.NET Core in .NET 8](/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-8.0\&preserve-view=true#bind8) or later. However, it can be implemented using the [`IAntiforgery` service](/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-7.0\&preserve-view=true#antimin7).

[]()

### Bind arrays and string values from headers and query strings

The following code demonstrates binding query strings to an array of primitive types, string arrays, and [StringValues](/en-us/dotnet/api/microsoft.extensions.primitives.stringvalues):

```
// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
```

Binding query strings or header values to an array of complex types is supported when the type has `TryParse` implemented. The following code binds to a string array and returns all the items with the specified tags:

```
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});
```

The following code shows the model and the required `TryParse` implementation:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}
```

The following code binds to an `int` array:

```
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});
```

To test the preceding code, add the following endpoint to populate the database with `Todo` items:

```
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});
```

Use an API testing tool like [`HttpRepl`](../../web-api/http-repl/?view=aspnetcore-10.0) to pass the following data to the previous endpoint:

```
[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]
```

The following code binds to the header key `X-Todo-Id` and returns the `Todo` items with matching `Id` values:

```
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});
```

Note

When binding a non-nullable `string[]` from a query string, the absence of any matching query string value results in an empty array instead of `null`. If the parameter is declared as nullable (for example, `string[]?`) or has a default value of `null` (for example, `[FromQuery] string[]? names = null`), omitting the query parameter results in `null`.

[]()

### Parameter binding for argument lists with \[AsParameters]

[AsParametersAttribute](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute) enables simple parameter binding to types and not complex or recursive model binding.

Consider the following code:

```
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.
```

Consider the following `GET` endpoint:

```
app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
```

The following `struct` can be used to replace the preceding highlighted parameters:

```
struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}
```

The refactored `GET` endpoint uses the preceding `struct` with the [AsParameters](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute?view=aspnetcore-7.0\&preserve-view=true) attribute:

```
app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
```

The following code shows additional endpoints in the app:

```
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.Name
    };

    Db.Todos.Add(todoItem);
    await Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.IsComplete;

    await Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
    if (await Db.Todos.FindAsync(Id) is Todo todo)
    {
        Db.Todos.Remove(todo);
        await Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});
```

The following classes are used to refactor the parameter lists:

```
class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}
```

The following code shows the refactored endpoints using `AsParameters` and the preceding `struct` and classes:

```
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});
```

The following [`record`](/en-us/dotnet/csharp/language-reference/builtin-types/record) types can be used to replace the preceding parameters:

```
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
```

```
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
```

The following code displays `Point: 12.3, 10.1` with the URI `/map?Point=12.3,10.1`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}
```

#### BindAsync

`BindAsync` has the following APIs:

```
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
```

The following code displays `SortBy:xyz, SortDirection:Desc, CurrentPage:99` with the URI `/products?SortBy=xyz&SortDir=Desc&Page=99`:

```
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}
```

[]()

#### Custom parameter binding with `IBindableFromHttpContext`

ASP.NET Core provides support for custom parameter binding in Minimal APIs using the [IBindableFromHttpContext\<TSelf>](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1) interface. This interface, introduced with C# 11's static abstract members, allows you to create types that can be bound from an HTTP context directly in route handler parameters.

```
public interface IBindableFromHttpContext<TSelf>
    where TSelf : class, IBindableFromHttpContext<TSelf>
{
    static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}
```

By implementing the [IBindableFromHttpContext\<TSelf>](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1) interface, you can create custom types that handle their own binding logic from the HttpContext. When a route handler includes a parameter of this type, the framework automatically calls the static BindAsync method to create the instance:

```
using CustomBindingExample;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/", () => "Hello, IBindableFromHttpContext example!");

app.MapGet("/custom-binding", (CustomBoundParameter param) =>
{
    return $"Value from custom binding: {param.Value}";
});

app.MapGet("/combined/{id}", (int id, CustomBoundParameter param) =>
{
    return $"ID: {id}, Custom Value: {param.Value}";
});
```

The following is an example implementation of a custom parameter that binds from an HTTP header:

```
using System.Reflection;

namespace CustomBindingExample;

public class CustomBoundParameter : IBindableFromHttpContext<CustomBoundParameter>
{
    public string Value { get; init; } = default!;

    public static ValueTask<CustomBoundParameter?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        // Custom binding logic here
        // This example reads from a custom header
        var value = context.Request.Headers["X-Custom-Header"].ToString();
        
        // If no header was provided, you could fall back to a query parameter
        if (string.IsNullOrEmpty(value))
        {
            value = context.Request.Query["customValue"].ToString();
        }
        
        return ValueTask.FromResult<CustomBoundParameter?>(new CustomBoundParameter 
        {
            Value = value
        });
    }
}
```

You can also implement validation within your custom binding logic:

```
app.MapGet("/validated", (ValidatedParameter param) =>
{
    if (string.IsNullOrEmpty(param.Value))
    {
        return Results.BadRequest("Value cannot be empty");
    }
    
    return Results.Ok($"Validated value: {param.Value}");
});
```

[View or download the sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/minimal-apis/samples/7.0/CustomBindingExample) ([how to download](../../overview?view=aspnetcore-10.0#how-to-download-a-sample))

### Binding failures

When binding fails, the framework logs a debug message and returns various status codes to the client depending on the failure mode.

| Failure mode                                | Nullable Parameter Type | Binding Source     | Status code |
| ------------------------------------------- | ----------------------- | ------------------ | ----------- |
| `{ParameterType}.TryParse` returns `false`  | yes                     | route/query/header | 400         |
| `{ParameterType}.BindAsync` returns `null`  | yes                     | custom             | 400         |
| `{ParameterType}.BindAsync` throws          | does not matter         | custom             | 500         |
| Failure to deserialize JSON body            | does not matter         | body               | 400         |
| Wrong content type (not `application/json`) | does not matter         | body               | 415         |

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }
```

Since the sample code configures both serialization and deserialization, it can read `NameField` and include `NameField` in the output JSON.

#### Configure JSON deserialization options for an endpoint

[ReadFromJsonAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequestjsonextensions.readfromjsonasync) has overloads that accept a [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object. The following example includes public fields and formats JSON output.

```
using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }
```

Since the preceding code applies the customized options only to deserialization, the output JSON excludes `NameField`.

### Read the request body

Read the request body directly using a [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) or [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) parameter:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();
```

The preceding code:

* Accesses the request body using [HttpRequest.BodyReader](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader#microsoft-aspnetcore-http-httprequest-bodyreader).
* Copies the request body to a local file.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/route-handlers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/route-handlers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Route handlers in Minimal API apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

A configured `WebApplication` supports `Map{Verb}` and the [MapMethods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapmethods), where `{Verb}` is a Pascal-cased HTTP method like `Get`, `Post`, `Put`, or `Delete`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();
```

The [Delegate](/en-us/dotnet/api/system.delegate) arguments passed to these methods are called *route handlers*.

This article describes how to use route handlers, including examples, parameters, route groups, and route constraints.

## Work with route handlers

Route handlers are methods that execute when the route matches. Route handlers can be a lambda expression, a local function, an instance method, or a static method. Route handlers can be synchronous or asynchronous.

The following sections provide examples of different route handlers.

### Lambda expression

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();
```

### Local function

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();
```

### Instance method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}
```

### Static method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}
```

### Endpoint defined outside of Program.cs

Minimal APIs don't have to be located in the *Program.cs* file. For example, you can set up the structure in the *Program.cs* file, and define the endpoint in a separate file:

**Program.cs**

```
using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();
```

**TodoEndpoints.cs**

```
namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}
```

For more information, see the [Route groups](#route-groups) section later in this article.

### Named endpoints and link generation

You can supply a name for your endpoints to generate URLs that target the endpoint. Using a named endpoint avoids having to hard code paths in an app:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();
```

The preceding code displays the message *`The link to the hello route is /hello`* from the `/` (forward slash) endpoint.

#### Criteria for endpoint names

Endpoint names must satisfy the following criteria:

* Endpoint names are case sensitive.
* Endpoint names must be globally unique.
* Endpoint names are used as the OpenAPI operation identifier (ID) when OpenAPI support is enabled. For more information, see [Generate OpenAPI documents](../openapi/aspnetcore-openapi?view=aspnetcore-10.0).

### Route parameters

Route parameters can be captured as part of the route pattern definition:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();
```

The preceding code returns the message *The user id is 3 and book id is 7* from the URI `/users/3/books/7`.

The route handler can declare the parameters to capture. When a request is made to a route with parameters declared to capture, the parameters are parsed and passed to the handler. This approach makes it easy to capture the values in a type-safe way. In the preceding code, the `userId` and `bookId` parameters are both type `int`.

In the preceding code, if either route value can't be converted to an `int`, an exception is thrown. The GET request `/users/hello/books/3` throws the following exception:

```
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
```

### Wildcard and catch all routes

The following catch all route returns *Routing to hello* from the `/posts/hello` endpoint:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();
```

### Route constraints

Route constraints restrict the matching behavior of a route.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();
```

The following table demonstrates the preceding route templates and their behavior.

| Route template                       | Example matching URI |
| ------------------------------------ | -------------------- |
| `/todos/{id:int}`                    | `/todos/1`           |
| `/todos/{text}`                      | `/todos/something`   |
| `/posts/{slug:regex(^[a-z0-9_-]+$)}` | `/posts/mypost`      |

For more information, see [Route constraint reference](../routing?view=aspnetcore-10.0#route-constraints) in [Routing in ASP.NET Core](../routing?view=aspnetcore-10.0).

### Route groups

The [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) extension method helps organize groups of endpoints with a common prefix and reduces repetitive code. Use this method to customize entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata) that add [endpoint metadata](../routing?view=aspnetcore-10.0#endpoint-metadata).

For example, the following code creates two similar groups of endpoints:

```
app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
```

```
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}
```

In this scenario, you can use a relative address for the `Location` header in the `201 Created` result:

```
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}
```

The first group of endpoints matches only requests prefixed with `/public/todos` and are accessible without any authentication. The second group of endpoints matches only requests prefixed with `/private/todos` and require authentication.

`QueryPrivateTodos` is a local function that modifies the `TodoDb` parameters of the route handler, to enable them to access and store private todo data. `QueryPrivateTodos` serves as an [endpoint filter factory](min-api-filters?view=aspnetcore-10.0).

Route groups also support nested groups and complex prefix patterns with route parameters and constraints. In the following example, the route handler mapped to the `user` group can capture the `{org}` and `{group}` route parameters defined in the outer group prefixes.

The prefix can also be empty. This approach can be useful for adding endpoint metadata or filters to a group of endpoints without changing the route pattern.

```
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
```

Adding filters or metadata to a group results in the same behavior as adding them individually to each endpoint (before adding extra filters or metadata that might exist in an inner group or specific endpoint).

```
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});
```

In the preceding example, the outer filter logs the incoming request before the inner filter even though the outer filter is added second. Because the filters are applied to different groups, the order that they're added relative to each other doesn't matter. The order in which filters are added matters when applied to the same group or specific endpoint.

A request to `/outer/inner/` logs the following data:

```
/outer group filter
/inner group filter
MapGet filter
```

## Bind parameters in a route handler

[Parameter binding in Minimal API applications](parameter-binding?view=aspnetcore-10.0) describes the rules in detail for how route handler parameters are populated.

## Handle the response from the route handler

[Create responses in Minimal API applications](responses?view=aspnetcore-10.0) describes in detail how values returned from route handlers are converted into responses.

## Related content

* [Routing in ASP.NET Core](../routing?view=aspnetcore-10.0)
* [Parameter Binding in Minimal API apps](parameter-binding?view=aspnetcore-10.0)
* [Filters in Minimal API apps](min-api-filters?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-29

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/client-features
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/client-features.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core SignalR clients

## Versioning, support, and compatibility

The SignalR clients ship alongside the server components and are versioned to match. Any supported client can safely connect to any supported server, and any compatibility issues would be considered bugs to be fixed. SignalR clients are supported in the same support lifecycle as the rest of .NET. See the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core) for details.

Many features require a compatible client **and** server. See below for a table showing the minimum versions for various features.

The 1.x versions of SignalR map to the 2.1 and 2.2 .NET Core releases and have the same lifetime. For version 3.x and above, the SignalR version exactly matches the rest of .NET and has the same support lifecycle.

| SignalR version | .NET Core version         | Support level                                                                                                 | End of support    |
| --------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------- |
| 1.0.x           | 2.1.x                     | Long Term Support                                                                                             | August 21, 2021   |
| 1.1.x           | 2.2.x                     | End Of Life                                                                                                   | December 23, 2019 |
| 3.x or higher   | *same as SignalR version* | See the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core) |                   |

**NOTE:** In ASP.NET Core 3.0, the JavaScript client *moved* to the `@microsoft/signalr` npm package.

## Feature distribution

The table below shows the features and support for the clients that offer real-time support. For each feature, the *minimum* version supporting this feature is listed. If no version is listed, the feature isn't supported.

| Feature                                                                                                                                                            | Server | .NET client | JavaScript client | Java client | Swift client     |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | ----------- | ----------------- | ----------- | ---------------- |
| Azure SignalR Service Support                                                                                                                                      | 2.1.0  | 1.0.0       | 1.0.0             | 1.0.0       | 1.0.0-preview\.1 |
| [Server-to-client Streaming](streaming?view=aspnetcore-10.0)                                                                                                       | 2.1.0  | 1.0.0       | 1.0.0             | 1.0.0       | 1.0.0-preview\.1 |
| [Client-to-server Streaming](streaming?view=aspnetcore-10.0)                                                                                                       | 3.0.0  | 3.0.0       | 3.0.0             | 3.0.0       | ❌                |
| Automatic Reconnection ([.NET](dotnet-client?view=aspnetcore-10.0#handle-lost-connection), [JavaScript](javascript-client?view=aspnetcore-10.0#reconnect-clients)) | 3.0.0  | 3.0.0       | 3.0.0             | ❌           | 1.0.0-preview\.1 |
| WebSockets Transport                                                                                                                                               | 2.1.0  | 1.0.0       | 1.0.0             | 1.0.0       | 1.0.0-preview\.1 |
| Server-Sent Events Transport                                                                                                                                       | 2.1.0  | 1.0.0       | 1.0.0             | ❌           | 1.0.0-preview\.1 |
| Long Polling Transport                                                                                                                                             | 2.1.0  | 1.0.0       | 1.0.0             | 3.0.0       | 1.0.0-preview\.1 |
| JSON Hub Protocol                                                                                                                                                  | 2.1.0  | 1.0.0       | 1.0.0             | 1.0.0       | 1.0.0-preview\.1 |
| MessagePack Hub Protocol                                                                                                                                           | 2.1.0  | 1.0.0       | 1.0.0             | 5.0.0       | 1.0.0-preview\.1 |
| Client Results                                                                                                                                                     | 7.0.0  | 7.0.0       | 7.0.0             | 7.0.0       | 1.0.0-preview\.1 |
| [Authentication Refresh](authn-and-authz?view=aspnetcore-10.0#authentication-refresh)                                                                              | 11.0.0 | 11.0.0      | 11.0.0            | ❌           | ❌                |
| [Cancel a non-streaming invocation](dotnet-client?view=aspnetcore-10.0#cancel-a-hub-method-invocation)                                                             | 11.0.0 | 11.0.0      | ❌                 | ❌           | ❌                |

Support for enabling additional client features is tracked in [our issue tracker](https://github.com/dotnet/AspNetCore/issues).

[]()

## Browsers that don't support ECMAScript 6 (ES6)

SignalR targets ES6. For browsers that don't support ES6, transpile the library to ES5. For more information, see [Getting Started with ES6 – Transpiling ES6 to ES5 with Traceur and Babel](https://blog.codewithdan.com/getting-started-with-es6-transpiling-es6-to-es5-with-traceur-and-babel/).

## Related content

* [Get started with SignalR for ASP.NET Core](../tutorials/signalr?view=aspnetcore-10.0)
* [Supported platforms](supported-platforms?view=aspnetcore-10.0)
* [Hubs](hubs?view=aspnetcore-10.0)
* [JavaScript client](javascript-client?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-19

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/simple
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/simple.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Simple authorization in ASP.NET Core

Authorization in ASP.NET Core is controlled with the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and its various parameters. In its most basic form, applying the `[Authorize]` attribute to a Razor component, controller, action, or Razor Page, limits access to that component to authenticated users.

This article uses Blazor Razor component examples and focuses on Blazor authorization scenarios. For Razor Pages and MVC guidance, see the following resources after reading this article:

* [Simple authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/simple?view=aspnetcore-10.0)
* [Simple authorization in ASP.NET Core MVC](../../mvc/security/authorization/simple?view=aspnetcore-10.0)

## `[Authorize]` attribute

In Blazor apps, specify the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) at the top of a Razor component file (`.razor`). In the following example, only authenticated users can access the page:

```
@page "/"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

You can only see this if you're signed in.
```

Important

Only use the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) on `@page` components reached via the Blazor router. Authorization is only performed as an aspect of routing and *not* for child components rendered within a page. To authorize the display of specific parts within a page, use an [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component instead, which is described in [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0#authorizeview-component).

The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) can also be applied to all of the Razor components in a Blazor app or a subset of Razor components in a folder using an imports file (`_Imports.razor`). Add an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive for the [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization) namespace with an [`@attribute`](../../mvc/views/razor?view=aspnetcore-10.0#attribute) directive for the [`[Authorize]` attribute](../../blazor/security/?view=aspnetcore-10.0#authorize-attribute):

```
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
```

The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) also supports role-based or policy-based authorization. For role-based authorization, use the [Roles](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles#microsoft-aspnetcore-authorization-authorizeattribute-roles) parameter. In the following example, the user can only access the page if they're in the `Admin` or `Superuser` role:

```
@page "/"
@attribute [Authorize(Roles = "Admin, Superuser")]

<p>You can only see this if you're in the 'Admin' or 'Superuser' role.</p>
```

For policy-based authorization, use the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) parameter. In the following example, the user can only access the page if they satisfy the requirements of the `Over21` [authorization policy](policies?view=aspnetcore-10.0):

```
@page "/"
@attribute [Authorize(Policy = "Over21")]

<p>You can only see this if you satisfy the 'Over21' policy.</p>
```

If neither [Roles](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles#microsoft-aspnetcore-authorization-authorizeattribute-roles) nor [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) is specified, [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) uses the default policy:

* Authenticated (signed-in) users are authorized.
* Unauthenticated (signed-out) users are unauthorized.

Endpoints without authorization metadata don't use the default policy. Such endpoints don't require authorization unless the app configures a fallback policy. For a comparison of named, default, and fallback policies, see [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0#default-and-fallback-policies).

When the user isn't authorized and if the app doesn't [customize unauthorized content with the `Router` component](../../blazor/security/?view=aspnetcore-10.0#customize-unauthorized-content-with-the-router-component), the framework automatically displays the following fallback message:

```
Not authorized.
```

For more information on Blazor authentication and authorization, see [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0).

Use the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) to allow access by non-authenticated users to individual actions:

```
@using Microsoft.AspNetCore.Authorization
@attribute [AllowAnonymous]
```

For server-side apps where most endpoints require authentication, see [Require global user authentication](policies?view=aspnetcore-10.0#require-global-user-authentication).

## Related content

* [Simple authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/simple?view=aspnetcore-10.0)
* [Simple authorization in ASP.NET Core MVC](../../mvc/security/authorization/simple?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/blazor-web-app-with-entra
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/blazor-web-app-with-entra.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

This article describes how to secure a Blazor Web App with [Microsoft identity platform](/en-us/entra/identity-platform/) with [Microsoft Identity Web packages](/en-us/entra/msal/dotnet/microsoft-identity-web/) for [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra) using a sample app.

This version of the article covers implementing Entra with the [Backend for Frontend (BFF) pattern](/en-us/azure/architecture/patterns/backends-for-frontends) with [YARP](https://dotnet.github.io/yarp/) and [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview). Change the article version selector to **Without YARP and Aspire** if the app's specification doesn't call for adopting YARP and Aspire.

The following specification is covered:

* The Blazor Web App uses the [Auto render mode with global interactivity (`InteractiveAuto`)](../components/render-modes?view=aspnetcore-10.0).

* The server project calls [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization) to add a server-side authentication state provider that uses [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) to flow the authentication state to the client. The client calls [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization) to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application.

* The app uses [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra), based on [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) packages.

* Automatic non-interactive token refresh is managed by the framework.

* The [Backend for Frontend (BFF) pattern](/en-us/azure/architecture/patterns/backends-for-frontends) is adopted using [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview) for service discovery and [YARP](https://dotnet.github.io/yarp/) for proxying requests to a weather forecast endpoint on the backend app.

  * A backend web API uses JWT-bearer authentication to validate JWT tokens saved by the Blazor Web App in the sign-in cookie.
  * Aspire improves the experience of building .NET cloud-native apps. It provides a consistent, opinionated set of tools and patterns for building and running distributed apps.
  * YARP (Yet Another Reverse Proxy) is a library used to create a reverse proxy server.

* The app uses server-side and client-side service abstractions to display generated weather data.

  * When rendering the `Weather` component on the server to display weather data, the component uses the `ServerWeatherForecaster`. Microsoft Identity Web packages provide API to create a named downstream web service for making web API calls. [IDownstreamApi](/en-us/dotnet/api/microsoft.identity.abstractions.idownstreamapi) is injected into the `ServerWeatherForecaster`, which is used to call [CallApiForUserAsync](/en-us/dotnet/api/microsoft.identity.abstractions.idownstreamapi.callapiforuserasync) to obtain weather data from an external web API (`MinimalApiJwt` project).
  * When the `Weather` component is rendered on the client, the component uses the `ClientWeatherForecaster` service implementation, which uses a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) (in the client project's `Program` file) to make a web API call to the server project's Minimal API (`/weather-forecast`) for weather data. The Minimal API endpoint obtains an access token for the user by calling [GetAccessTokenForUserAsync](/en-us/dotnet/api/microsoft.identity.web.itokenacquisition.getaccesstokenforuserasync). Along with the correct scopes, a reverse proxy call is made to the external web API (`MinimalApiJwt` project) to obtain and return weather data to the client for rendering by the component.

## Prerequisites

[Aspire](/en-us/dotnet/aspire/get-started/aspire-overview) requires [Visual Studio](https://visualstudio.microsoft.com/) version 17.10 or later.

Also, see the *Prerequisites* section of [Quickstart: Build your first Aspire solution](/en-us/dotnet/aspire/get-started/build-your-first-aspire-app?tabs=visual-studio#prerequisites).

## Sample solution

The sample solution consists of the following projects:

* Aspire:

  * `Aspire.AppHost`: Used to manage the high-level orchestration concerns of the app.
  * `Aspire.ServiceDefaults`: Contains default Aspire app configurations that can be extended and customized as needed.

* `MinimalApiJwt`: Backend web API, containing an example [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.

* `BlazorWebAppEntra`: Server-side project of the Blazor Web App.

* `BlazorWebAppEntra.Client`: Client-side project of the Blazor Web App.

Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the `BlazorWebAppEntraBffYarpAspire` folder for .NET 9 or later.

Start the solution from the ***`Aspire/Aspire.AppHost` project***.

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

## Microsoft Entra ID app registrations

We recommend using separate registrations for apps and web APIs, even when the apps and web APIs are in the same solution. The following guidance is for the `BlazorWebAppEntra` app and `MinimalApiJwt` web API of the sample solution, but the same guidance applies generally to any Entra-based registrations for apps and web APIs.

For app and web API registration guidance, see [Register an application in Microsoft Entra ID](/en-us/entra/identity-platform/quickstart-register-app).

Register the web API (`MinimalApiJwt`) first so that you can then grant access to the web API when registering the app. The web API's tenant ID and client ID are used to configure the web API in its `Program` file. After registering the web API, expose the web API in **App registrations** > **Expose an API** with a scope name of `Weather.Get`. Record the App ID URI for use in the app's configuration.

Next, register the app (`BlazorWebAppEntra`) with a **Web** platform configuration with two entries under **Redirect URI**: `https://localhost/signin-oidc` and `https://localhost/signout-callback-oidc` (ports aren't required on these URIs). Set the **Front-channel logout URL** to `https://localhost/signout-callback-oidc` (a port isn't required). The app's tenant ID, tenant domain, and client ID, along with the web API's base address, App ID URI, and weather scope name, are used to configure the app in its `appsettings.json` file. Grant API permission to access the web API in **App registrations** > **API permissions**. If the app's security specification calls for it, you can grant admin consent for the organization to access the web API. Authorized users and groups are assigned to the app's registration in **App registrations** > **Enterprise applications**.

In the Entra or Azure portal's **Implicit grant and hybrid flows** app registration configuration, don't select either checkbox for the authorization endpoint to return **Access tokens** or **ID tokens**. The OpenID Connect handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.

Create a client secret in the app's registration in the Entra or Azure portal (**Manage** > **Certificates & secrets** > **New client secret**). Hold on to the client secret **Value** for use the next section.

Additional Entra configuration guidance for specific settings is provided later in this article.

## Aspire projects

For more information on using Aspire and details on the `.AppHost` and `.ServiceDefaults` projects of the sample app, see the [Aspire documentation](/en-us/dotnet/aspire/).

Confirm that you've met the prerequisites for Aspire. For more information, see the *Prerequisites* section of [Quickstart: Build your first Aspire solution](/en-us/dotnet/aspire/get-started/build-your-first-aspire-app?tabs=visual-studio#prerequisites).

The sample app only configures an insecure HTTP launch profile (`http`) for use during development testing.

## Server-side Blazor Web App project (`BlazorWebAppEntra`)

The `BlazorWebAppEntra` project is the server-side project of the Blazor Web App.

## Client-side Blazor Web App project (`BlazorWebAppEntra.Client`)

The `BlazorWebAppEntra.Client` project is the client-side project of the Blazor Web App.

If the user needs to log in or out during client-side rendering, a full page reload is initiated.

## Backend web API project (`MinimalApiJwt`)

The `MinimalApiJwt` project is a backend web API for multiple frontend projects. The project configures a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data. Requests from the Blazor Web App server-side project (`BlazorWebAppEntra`) are proxied to the `MinimalApiJwt` project.

The `MinimalApiJwt.http` file can be used for testing the weather data request. Note that the `MinimalApiJwt` project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see [Use .http files in Visual Studio 2022](../../test/http-files?view=aspnetcore-10.0).

The project includes packages and configuration to produce [OpenAPI documents](../../fundamentals/openapi/overview?view=aspnetcore-10.0).

A secure weather forecast data endpoint is in the project's `Program` file:

```
app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();
```

The [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) extension method requires authorization for the route definition. For any controllers that you add to the project, add the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the controller or action.

## Configure the backend web API project (`MinimalApiJwt`)

Configure the `MinimalApiJwt` project in the [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions) of the [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) call in the project's `Program` file.

For the web API app's registration, the `Weather.Get` scope is configured in the Entra or Azure portal in **Expose an API**.

[Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.authority) sets the Authority for making OIDC calls.

```
jwtOptions.Authority = "{AUTHORITY}";
```

The following examples use a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee` and a directory name of `contoso`.

If the app is registered in an ME-ID tenant, the authority should match the issuer (`iss`) of the JWT returned by the identity provider.

V1 STS token endpoint:

```
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
```

V2 STS token endpoint:

```
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
```

For more information on V2 STS tokens, see the [STS token version](#sts-token-version) section.

If the app is registered in a Microsoft Entra External ID tenant:

```
jwtOptions.Authority = "https://contoso.ciamlogin.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
```

If the app is registered in an AAD B2C tenant:

```
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
```

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. AAD B2C tenants are supported for customers with accounts established prior to May 1, 2025 until 2030. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

[Audience](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.audience) sets the Audience for any received JWT access token.

```
jwtOptions.Audience = "{AUDIENCE}";
```

Match the value to just the path of the **Application ID URI** configured when adding the `Weather.Get` scope under **Expose an API** in the Entra or Azure portal. Don't include the scope name, "`Weather.Get`," in the value.

The following examples use an Application (Client) Id of `11112222-bbbb-3333-cccc-4444dddd5555`. The third example uses a tenant domain of `contoso.onmicrosoft.com`.

ME-ID tenant example:

```
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";
```

Microsoft Entra External ID tenant:

```
jwtOptions.Audience = "11112222-bbbb-3333-cccc-4444dddd5555";
```

AAD B2C tenant example:

```
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";
```

## Configure the server project (`BlazorWebAppEntra`)

[AddMicrosoftIdentityWebApp](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapp) from [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) ([`Microsoft.Identity.Web` NuGet package](https://www.nuget.org/packages/Microsoft.Identity.Web), [API documentation](/en-us/dotnet/api/microsoft.identity.web)) is configured in the `BlazorWebAppEntra` project's `Program` file.

Obtain the application (client) ID, tenant (publisher) domain, and directory (tenant) ID from the app's registration in the Entra or Azure portal. The App ID URI is obtained for the `Weather.Get` scope from the web API's registration. Don't include the scope name when taking the App ID URI from the portal.

The authentication configuration depends on the type of tenant:

* [Configuration for an ME-ID](#configuration-for-an-me-id)
* [Configuration for Microsoft Entra External ID](#configuration-for-microsoft-entra-external-id)

### Configuration for an ME-ID

*This section applies to an app registered in a Microsoft Entra ID or Azure AAD B2C tenant.*

In the `BlazorWebAppEntra` project's `Program` file, provide the values for the following placeholders in Microsoft Identity Web configuration:

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(msIdentityOptions =>
    {
        msIdentityOptions.CallbackPath = "/signin-oidc";
        msIdentityOptions.ClientId = "{CLIENT ID (BLAZOR APP)}";
        msIdentityOptions.Domain = "{DIRECTORY NAME}.onmicrosoft.com";
        msIdentityOptions.Instance = "https://login.microsoftonline.com/";
        msIdentityOptions.ResponseType = "code";
        msIdentityOptions.TenantId = "{TENANT ID}";
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", configOptions =>
    {
        configOptions.BaseUrl = "{BASE ADDRESS}";
        configOptions.Scopes = ["{APP ID URI}/Weather.Get"];
    })
    .AddDistributedTokenCaches();
```

Provide the same downstream API scope to the request transformer:

```
List<string> scopes = ["{APP ID URI}/Weather.Get"];
```

Placeholders in the preceding configuration:

* `{CLIENT ID (BLAZOR APP)}`: The application (client) ID.

* `{DIRECTORY NAME}`: The directory name of the tenant (publisher) domain.

* `{TENANT ID}`: The directory (tenant) ID.

* `{BASE ADDRESS}`: The web API's base address.

* `{APP ID URI}`: The App ID URI for web API scopes. Either of the following formats are used, where the `{CLIENT ID (WEB API)}` placeholder is the Client Id of the web API's Entra registration, and the `{DIRECTORY NAME}` placeholder is the directory name of the tenant (publishers) domain (example: `contoso`).

  * ME-ID tenant format: `api://{CLIENT ID (WEB API)}`
  * B2C tenant format: `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}`

Example:

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(msIdentityOptions =>
    {
        msIdentityOptions.CallbackPath = "/signin-oidc";
        msIdentityOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
        msIdentityOptions.Domain = "contoso.onmicrosoft.com";
        msIdentityOptions.Instance = "https://login.microsoftonline.com/";
        msIdentityOptions.ResponseType = "code";
        msIdentityOptions.TenantId = "aaaabbbb-0000-cccc-1111-dddd2222eeee";
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", configOptions =>
    {
        configOptions.BaseUrl = "https://localhost:7277";
        configOptions.Scopes = 
            ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
    })
    .AddDistributedTokenCaches();
```

Example:

```
List<string> scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
```

### Configuration for Microsoft Entra External ID

*This section applies to an app registered in a Microsoft Entra External ID tenant.*

In the `BlazorWebAppEntra` project's `Program` file, provide the values for the following placeholders in Microsoft Identity Web configuration:

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(msIdentityOptions =>
    {
        msIdentityOptions.CallbackPath = "/signin-oidc";
        msIdentityOptions.Authority = "https://{DIRECTORY NAME}.ciamlogin.com/{TENANT ID}/v2.0";
        msIdentityOptions.ClientId = "{CLIENT ID (BLAZOR APP)}";
        msIdentityOptions.ResponseType = "code";
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", configOptions =>
    {
        configOptions.BaseUrl = "{BASE ADDRESS}";
        configOptions.Scopes = ["{APP ID URI}/Weather.Get"];
    })
    .AddDistributedTokenCaches();
```

Provide the same downstream API scope to the request transformer:

```
List<string> scopes = ["{APP ID URI}/Weather.Get"];
```

Placeholders in the preceding configuration:

* `{DIRECTORY NAME}`: The directory name of the tenant (publisher) domain.

* `{CLIENT ID (BLAZOR APP)}`: The application (client) ID.

* `{BASE ADDRESS}`: The web API's base address.

* `{APP ID URI}`: The App ID URI for web API scopes. Either of the following formats are used, where the `{CLIENT ID (WEB API)}` placeholder is the Client Id of the web API's Entra registration, and the `{DIRECTORY NAME}` placeholder is the directory name of the tenant (publishers) domain (example: `contoso`).

  * ME-ID or Microsoft Entra External ID tenant format: `api://{CLIENT ID (WEB API)}`
  * B2C tenant format: `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}`

Example:

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(msIdentityOptions =>
    {
        msIdentityOptions.CallbackPath = "/signin-oidc";
        msIdentityOptions.Authority = "https://contoso.ciamlogin.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
        msIdentityOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
        msIdentityOptions.ResponseType = "code";
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", configOptions =>
    {
        configOptions.BaseUrl = "https://localhost:7277";
        configOptions.Scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
    })
    .AddDistributedTokenCaches();
```

Example:

```
List<string> scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
```

This version of the article covers implementing Entra with the [Backend for Frontend (BFF) pattern](/en-us/azure/architecture/patterns/backends-for-frontends) without [YARP](https://dotnet.github.io/yarp/) and [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview). Change the article version selector to **With YARP and Aspire** if the app's specification calls for adopting YARP and Aspire.

The following specification is covered:

* The Blazor Web App uses the [Auto render mode with global interactivity (`InteractiveAuto`)](../components/render-modes?view=aspnetcore-10.0).

* The server project calls [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization) to add a server-side authentication state provider that uses [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) to flow the authentication state to the client. The client calls [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization) to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application.

* The app uses [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra), based on [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) packages.

* Automatic non-interactive token refresh is managed by the framework.

* The [Backend for Frontend (BFF) pattern](/en-us/azure/architecture/patterns/backends-for-frontends) is adopted for proxying requests to a weather forecast endpoint on the backend app. A backend web API uses JWT-bearer authentication to validate JWT tokens saved by the Blazor Web App in the sign-in cookie.

* The app uses server-side and client-side service abstractions to display generated weather data:

  * When rendering the `Weather` component on the server to display weather data, the component uses the `ServerWeatherForecaster`. Microsoft Identity Web packages provide API to create a named downstream web service for making web API calls. [IDownstreamApi](/en-us/dotnet/api/microsoft.identity.abstractions.idownstreamapi) is injected into the `ServerWeatherForecaster`, which is used to call [CallApiForUserAsync](/en-us/dotnet/api/microsoft.identity.abstractions.idownstreamapi.callapiforuserasync) to obtain weather data from an external web API (`MinimalApiJwt` project).
  * When the `Weather` component is rendered on the client, the component uses the `ClientWeatherForecaster` service implementation, which uses a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) (in the client project's `Program` file) to make a web API call to the server project's Minimal API (`/weather-forecast`) for weather data. The Minimal API endpoint obtains the weather data from the `ServerWeatherForecaster` class and returns it to the client for rendering by the component.

## Sample solution

The sample solution consists of the following projects:

* `BlazorWebAppEntra`: Server-side project of the Blazor Web App, containing an example [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.
* `BlazorWebAppEntra.Client`: Client-side project of the Blazor Web App.
* `MinimalApiJwt`: Backend web API, containing an example [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.

Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the `BlazorWebAppEntraBff` folder for .NET 9 or later.

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

## Microsoft Entra ID app registrations

We recommend using separate registrations for apps and web APIs, even when the apps and web APIs are in the same solution. The following guidance is for the `BlazorWebAppEntra` app and `MinimalApiJwt` web API of the sample solution, but the same guidance applies generally to any Entra-based registrations for apps and web APIs.

For app and web API registration guidance, see [Register an application in Microsoft Entra ID](/en-us/entra/identity-platform/quickstart-register-app).

Register the web API (`MinimalApiJwt`) first so that you can then grant access to the web API when registering the app. The web API's tenant ID and client ID are used to configure the web API in its `Program` file. After registering the web API, expose the web API in **App registrations** > **Expose an API** with a scope name of `Weather.Get`. Record the App ID URI for use in the app's configuration.

Next, register the app (`BlazorWebAppEntra`) with a **Web** platform configuration with two entries under **Redirect URI**: `https://localhost/signin-oidc` and `https://localhost/signout-callback-oidc` (ports aren't required on these URIs). Set the **Front-channel logout URL** to `https://localhost/signout-callback-oidc` (a port isn't required). The app's tenant ID, tenant domain, and client ID, along with the web API's base address, App ID URI, and weather scope name, are used to configure the app in its `appsettings.json` file. Grant API permission to access the web API in **App registrations** > **API permissions**. If the app's security specification calls for it, you can grant admin consent for the organization to access the web API. Authorized users and groups are assigned to the app's registration in **App registrations** > **Enterprise applications**.

In the Entra or Azure portal's **Implicit grant and hybrid flows** app registration configuration, don't select either checkbox for the authorization endpoint to return **Access tokens** or **ID tokens**. The OpenID Connect handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.

Create a client secret in the app's registration in the Entra or Azure portal (**Manage** > **Certificates & secrets** > **New client secret**). Hold on to the client secret **Value** for use the next section.

Additional Entra configuration guidance for specific settings is provided later in this article.

## Server-side Blazor Web App project (`BlazorWebAppEntra`)

The `BlazorWebAppEntra` project is the server-side project of the Blazor Web App.

## Client-side Blazor Web App project (`BlazorWebAppEntra.Client`)

The `BlazorWebAppEntra.Client` project is the client-side project of the Blazor Web App.

If the user needs to log in or out during client-side rendering, a full page reload is initiated.

## Backend web API project (`MinimalApiJwt`)

The `MinimalApiJwt` project is a backend web API for multiple frontend projects. The project configures a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.

The `MinimalApiJwt.http` file can be used for testing the weather data request. Note that the `MinimalApiJwt` project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see [Use .http files in Visual Studio 2022](../../test/http-files?view=aspnetcore-10.0).

The project includes packages and configuration to produce [OpenAPI documents](../../fundamentals/openapi/overview?view=aspnetcore-10.0).

A secure weather forecast data endpoint is in the project's `Program` file:

```
app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();
```

The [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) extension method requires authorization for the route definition. For any controllers that you add to the project, add the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the controller or action.

## Configure the backend web API project (`MinimalApiJwt`)

Configure the project in the [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions) of the [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) call in the `MinimalApiJwt` project's `Program` file.

For the web API app's registration, the `Weather.Get` scope is configured in the Entra or Azure portal in **Expose an API**.

[Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.authority) sets the Authority for making OIDC calls.

```
jwtOptions.Authority = "{AUTHORITY}";
```

The following examples use a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee` and a directory name of `contoso`.

If the app is registered in an ME-ID tenant, the authority should match the issuer (`iss`) of the JWT returned by the identity provider.

V1 STS token endpoint:

```
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
```

V2 STS token endpoint:

```
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
```

For more information on V2 STS tokens, see the [STS token version](#sts-token-version) section.

If the app is registered in a Microsoft Entra External ID tenant:

```
jwtOptions.Authority = "https://contoso.ciamlogin.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
```

If the app is registered in an AAD B2C tenant:

```
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
```

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. AAD B2C tenants are supported for customers with accounts established prior to May 1, 2025 until 2030. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

[Audience](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.audience) sets the Audience for any received JWT access token.

```
jwtOptions.Audience = "{AUDIENCE}";
```

Match the value to just the path of the **Application ID URI** configured when adding the `Weather.Get` scope under **Expose an API** in the Entra or Azure portal. Don't include the scope name, "`Weather.Get`," in the value.

The following examples use an Application (Client) Id of `11112222-bbbb-3333-cccc-4444dddd5555`. The third example uses a tenant domain of `contoso.onmicrosoft.com`.

ME-ID tenant example:

```
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";
```

Microsoft Entra External ID tenant:

```
jwtOptions.Audience = "11112222-bbbb-3333-cccc-4444dddd5555";
```

AAD B2C tenant example:

```
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";
```

## Configure the server project (`BlazorWebAppEntra`)

[AddMicrosoftIdentityWebApp](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapp) from [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) ([`Microsoft.Identity.Web` NuGet package](https://www.nuget.org/packages/Microsoft.Identity.Web), [API documentation](/en-us/dotnet/api/microsoft.identity.web)) is configured in the `BlazorWebAppEntra` project's `Program` file.

Obtain the application (client) ID, tenant (publisher) domain, and directory (tenant) ID from the app's registration in the Entra or Azure portal. The App ID URI is obtained for the `Weather.Get` scope from the web API's registration. Don't include the scope name when taking the App ID URI from the portal.

The authentication configuration depends on the type of tenant:

* [ME-ID tenant configuration](#me-id-tenant-configuration)
* [Microsoft Entra External ID configuration](#microsoft-entra-external-id-configuration)

### ME-ID tenant configuration

*This section applies to an app registered in a Microsoft Entra ID or Azure AAD B2C tenant.*

In the `BlazorWebAppEntra` project's `Program` file, provide the values for the following placeholders in Microsoft Identity Web configuration:

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(msIdentityOptions =>
    {
        msIdentityOptions.CallbackPath = "/signin-oidc";
        msIdentityOptions.ClientId = "{CLIENT ID (BLAZOR APP)}";
        msIdentityOptions.Domain = "{DIRECTORY NAME}.onmicrosoft.com";
        msIdentityOptions.Instance = "https://login.microsoftonline.com/";
        msIdentityOptions.ResponseType = "code";
        msIdentityOptions.TenantId = "{TENANT ID}";
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", configOptions =>
    {
        configOptions.BaseUrl = "{BASE ADDRESS}";
        configOptions.Scopes = ["{APP ID URI}/Weather.Get"];
    })
    .AddDistributedTokenCaches();
```

Placeholders in the preceding configuration:

* `{CLIENT ID (BLAZOR APP)}`: The application (client) ID.

* `{DIRECTORY NAME}`: The directory name of the tenant (publisher) domain.

* `{TENANT ID}`: The directory (tenant) ID.

* `{BASE ADDRESS}`: The web API's base address.

* `{APP ID URI}`: The App ID URI for web API scopes. Either of the following formats are used, where the `{CLIENT ID (WEB API)}` placeholder is the Client Id of the web API's Entra registration, and the `{DIRECTORY NAME}` placeholder is the directory name of the tenant (publishers) domain (example: `contoso`).

  * ME-ID tenant format: `api://{CLIENT ID (WEB API)}`
  * B2C tenant format: `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}`

Example:

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(msIdentityOptions =>
    {
        msIdentityOptions.CallbackPath = "/signin-oidc";
        msIdentityOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
        msIdentityOptions.Domain = "contoso.onmicrosoft.com";
        msIdentityOptions.Instance = "https://login.microsoftonline.com/";
        msIdentityOptions.ResponseType = "code";
        msIdentityOptions.TenantId = "aaaabbbb-0000-cccc-1111-dddd2222eeee";
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", configOptions =>
    {
        configOptions.BaseUrl = "https://localhost:7277";
        configOptions.Scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
    })
    .AddDistributedTokenCaches();
```

### Microsoft Entra External ID configuration

*This section applies to an app registered in a Microsoft Entra External ID tenant.*

In the `BlazorWebAppEntra` project's `Program` file, provide the values for the following placeholders in Microsoft Identity Web configuration:

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(msIdentityOptions =>
    {
        msIdentityOptions.CallbackPath = "/signin-oidc";
        msIdentityOptions.Authority = "https://{DIRECTORY NAME}.ciamlogin.com/{TENANT ID}/v2.0";
        msIdentityOptions.ClientId = "{CLIENT ID (BLAZOR APP)}";
        msIdentityOptions.ResponseType = "code";
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", configOptions =>
    {
        configOptions.BaseUrl = "{BASE ADDRESS}";
        configOptions.Scopes = ["{APP ID URI}/Weather.Get"];
    })
    .AddDistributedTokenCaches();
```

Placeholders in the preceding configuration:

* `{DIRECTORY NAME}`: The directory name of the tenant (publisher) domain.

* `{CLIENT ID (BLAZOR APP)}`: The application (client) ID.

* `{BASE ADDRESS}`: The web API's base address.

* `{APP ID URI}`: The App ID URI for web API scopes. Either of the following formats are used, where the `{CLIENT ID (WEB API)}` placeholder is the Client Id of the web API's Entra registration, and the `{DIRECTORY NAME}` placeholder is the directory name of the tenant (publisher) domain (example: `contoso`).

  * ME-ID or Microsoft Entra External ID tenant format: `api://{CLIENT ID (WEB API)}`
  * B2C tenant format: `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}`

Example:

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(msIdentityOptions =>
    {
        msIdentityOptions.CallbackPath = "/signin-oidc";
        msIdentityOptions.Authority = "https://contoso.ciamlogin.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
        msIdentityOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
        msIdentityOptions.ResponseType = "code";
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", configOptions =>
    {
        configOptions.BaseUrl = "https://localhost:7277";
        configOptions.Scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
    })
    .AddDistributedTokenCaches();
```

The callback path (`CallbackPath`) must match the redirect URI (login callback path) configured when registering the application in the Entra or Azure portal. Paths are configured in the **Authentication** blade of the app's registration. The default value of `CallbackPath` is `/signin-oidc` for a registered redirect URI of `https://localhost/signin-oidc` (a port isn't required).

The [SignedOutCallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutcallbackpath) is the request path within the app's base path intercepted by the OpenID Connect handler where the user agent is first returned after signing out from Entra. The sample app doesn't set a value for the path because the default value of "`/signout-callback-oidc`" is used. After intercepting the request, the OpenID Connect handler redirects to the [SignedOutRedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutredirecturi) or [RedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.redirecturi), if specified.

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](./?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

Warning

Production apps should use a production distributed token cache provider. Otherwise, the app may have poor performance in some scenarios. For more information, see the [Use a production distributed token cache provider](#use-a-production-distributed-token-cache-provider) section.

The callback path (`CallbackPath`) must match the redirect URI (login callback path) configured when registering the application in the Entra or Azure portal. Paths are configured in the **Authentication** blade of the app's registration. The default value of `CallbackPath` is `/signin-oidc` for a registered redirect URI of `https://localhost/signin-oidc` (a port isn't required).

The [SignedOutCallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutcallbackpath) is the request path within the app's base path intercepted by the OpenID Connect handler where the user agent is first returned after signing out from Entra. The sample app doesn't set a value for the path because the default value of "`/signout-callback-oidc`" is used. After intercepting the request, the OpenID Connect handler redirects to the [SignedOutRedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutredirecturi) or [RedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.redirecturi), if specified.

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](./?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

### Establish the client secret

*This section only applies to the server project of the Blazor Web App.*

Use either or both of the following approaches to supply the client secret to the app:

* **Secret Manager tool**: The Secret Manager tool stores private data on the local machine and is only used during local development.
* **Azure Key Vault**: You can store the client secret in a key vault for use in any environment, including for the `Development` environment when working locally. Some developers prefer to use key vaults for staging and production deployments and use the Secret Manager tool for local development.

We strongly recommend that you avoid storing client secrets in project code or configuration files. Use secure authentication flows, such as either or both of the approaches in this section.

### Secret Manager tool

The [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) can store the server app's client secret under the configuration key `AzureAd:ClientSecret`.

The Blazor server app hasn't been initialized for the Secret Manager tool. Use a command shell, such as the Developer PowerShell command shell in Visual Studio, to execute the following command. Before executing the command, change the directory with the `cd` command to the server project's directory. The command establishes a user secrets identifier (`<UserSecretsId>`) in the server app's project file, which is used internally by the tooling to track secrets for the app:

```
dotnet user-secrets init
```

Execute the following command to set the client secret. The `{SECRET}` placeholder is the client secret obtained from the app's Entra registration:

```
dotnet user-secrets set "AzureAd:ClientSecret" "{SECRET}"
```

If using Visual Studio, you can confirm that the secret is set by right-clicking the server project in **Solution Explorer** and selecting **Manage User Secrets**.

### Azure Key Vault

[Azure Key Vault](https://azure.microsoft.com/products/key-vault/) provides a safe approach for providing the app's client secret to the app.

To create a key vault and set a client secret, see [About Azure Key Vault secrets (Azure documentation)](/en-us/azure/key-vault/secrets/about-secrets), which cross-links resources to get started with Azure Key Vault. To implement the code in this section, record the key vault URI and the secret name from Azure when you create the key vault and secret. For the example in this section, the secret name is "`BlazorWebAppEntraClientSecret`."

When establishing the key vault in the Entra or Azure portal:

* Configure the key vault to use Azure role-based access control (RABC). If you aren't operating on an [Azure Virtual Network](/en-us/azure/virtual-network/virtual-networks-overview), including for local development and testing, confirm that public access on the **Networking** step is **enabled** (checked). Enabling public access only exposes the key vault endpoint. Authenticated accounts are still required for access.

* Create an Azure Managed Identity (or add a role to the existing Managed Identity that you plan to use) with the **Key Vault Secrets User** role. Assign the Managed Identity to the Azure App Service that's hosting the deployment: **Settings** > **Identity** > **User assigned** > **Add**.

  Note

  If you also plan to run an app locally with an authorized user for key vault access using the [Azure CLI](/en-us/cli/azure/) or Visual Studio's Azure Service Authentication, add your developer Azure user account in **Access Control (IAM)** with the **Key Vault Secrets User** role. If you want to use the Azure CLI through Visual Studio, execute the `az login` command from the Developer PowerShell panel and follow the prompts to authenticate with the tenant.

To implement the code in this section, record the key vault URI (example: "`https://contoso.vault.azure.net/`", trailing slash required) and the secret name (example: "`BlazorWebAppEntraClientSecret`") from Azure when you create the key vault and secret.

Important

A key vault secret is created with an expiration date. Be sure to track when a key vault secret is going to expire and create a new secret for the app prior to that date passing.

Add the following `AzureHelper` class to the server project. The `GetKeyVaultSecret` method retrieves a secret from a key vault. Adjust the namespace (`BlazorSample.Helpers`) to match your project namespace scheme.

`Helpers/AzureHelper.cs`:

```
using Azure.Core;
using Azure.Security.KeyVault.Secrets;

namespace BlazorWebAppEntra.Helpers;

public static class AzureHelper
{
    public static string GetKeyVaultSecret(string vaultUri, 
        TokenCredential credential, string secretName)
    {
        var client = new SecretClient(new Uri(vaultUri), credential);
        var secret = client.GetSecretAsync(secretName).Result;

        return secret.Value.Value;
    }
}
```

Note

The preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential). For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

Where services are registered in the server project's `Program` file, obtain and apply the client secret using the following code:

```
TokenCredential? credential;

if (builder.Environment.IsProduction())
{
    credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
    // Local development and testing only
    DefaultAzureCredentialOptions options = new()
    {
        // Specify the tenant ID to use the dev credentials when running the app locally
        // in Visual Studio.
        VisualStudioTenantId = "{TENANT ID}",
        SharedTokenCacheTenantId = "{TENANT ID}"
    };

    credential = new DefaultAzureCredential(options);
}
```

Where [MicrosoftIdentityOptions](/en-us/dotnet/api/microsoft.identity.web.microsoftidentityoptions) are set, call `GetKeyVaultSecret` to receive and assign the app's client secret:

```
msIdentityOptions.ClientSecret = AzureHelper.GetKeyVaultSecret("{VAULT URI}", 
    credential, "{SECRET NAME}");
```

`{MANAGED IDENTITY CLIENT ID}`: The Azure Managed Identity Client ID (GUID).

`{TENANT ID}`: The directory (tenant) ID. Example: `aaaabbbb-0000-cccc-1111-dddd2222eeee`

`{VAULT URI}`: Key vault URI. Include the trailing slash on the URI. Example: `https://contoso.vault.azure.net/`

`{SECRET NAME}`: Secret name. Example: `BlazorWebAppEntraClientSecret`

Configuration is used to facilitate supplying dedicated key vaults and secret names based on the app's environmental configuration files. For example, you can supply different configuration values for `appsettings.Development.json` in development, `appsettings.Staging.json` when staging, and `appsettings.Production.json` for the production deployment. For more information, see [ASP.NET Core Blazor configuration](../fundamentals/configuration?view=aspnetcore-10.0).

## Only serialize the name and role claims

In the `Program` file, all claims are serialized by setting [SerializeAllClaims](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.server.authenticationstateserializationoptions.serializeallclaims) to `true`. If you only want the name and role claims serialized for CSR, remove the option or set it to `false`.

## Supply configuration with the JSON configuration provider (app settings)

The [sample solution projects](#sample-solution) configure Microsoft Identity Web and JWT bearer authentication in their `Program` files in order to make configuration settings discoverable using C# autocompletion. Professional apps usually use a *configuration provider* to configure OIDC options, such as the default [JSON configuration provider](../../fundamentals/configuration/?view=aspnetcore-10.0). The JSON configuration provider loads configuration from app settings files `appsettings.json`/`appsettings.{ENVIRONMENT}.json`, where the `{ENVIRONMENT}` placeholder is the app's [runtime environment](../../fundamentals/environments?view=aspnetcore-10.0). Follow the guidance in this section to use app settings files for configuration.

In the app settings file (`appsettings.json`) of the `BlazorWebAppEntra` project, add the following JSON configuration:

```
{
  "AzureAd": {
    "CallbackPath": "/signin-oidc",
    "ClientId": "{CLIENT ID (BLAZOR APP)}",
    "Domain": "{DIRECTORY NAME}.onmicrosoft.com",
    "Instance": "https://login.microsoftonline.com/",
    "ResponseType": "code",
    "TenantId": "{TENANT ID}"
  },
  "DownstreamApi": {
    "BaseUrl": "{BASE ADDRESS}",
    "Scopes": ["{APP ID URI}/Weather.Get"]
  }
}
```

Update the placeholders in the preceding configuration to match the values that the app uses in the `Program` file:

* `{CLIENT ID (BLAZOR APP)}`: The application (client) ID.

* `{DIRECTORY NAME}`: The directory name of the tenant (publisher) domain.

* `{TENANT ID}`: The directory (tenant) ID.

* `{BASE ADDRESS}`: The web API's base address.

* `{APP ID URI}`: The App ID URI for web API scopes. Either of the following formats are used, where the `{CLIENT ID (WEB API)}` placeholder is the Client Id of the web API's Entra registration, and the `{DIRECTORY NAME}` placeholder is the directory name of the tenant (publishers) domain (example: `contoso`).

  * ME-ID tenant format: `api://{CLIENT ID (WEB API)}`
  * B2C tenant format: `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}`

Example:

```
"AzureAd": {
  "CallbackPath": "/signin-oidc",
  "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "Domain": "contoso.onmicrosoft.com",
  "Instance": "https://login.microsoftonline.com/",
  "ResponseType": "code",
  "TenantId": "aaaabbbb-0000-cccc-1111-dddd2222eeee"
},
"DownstreamApi": {
  "BaseUrl": "https://localhost:7277",
  "Scopes": ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"]
}
```

Update any other values in the preceding configuration to match custom/non-default values used in the `Program` file.

The configuration is automatically picked up by the authentication builder.

Make the following changes in the `Program` file:

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
-   .AddMicrosoftIdentityWebApp(msIdentityOptions =>
-   {
-       msIdentityOptions.CallbackPath = "...";
-       msIdentityOptions.ClientId = "...";
-       msIdentityOptions.Domain = "...";
-       msIdentityOptions.Instance = "...";
-       msIdentityOptions.ResponseType = "...";
-       msIdentityOptions.TenantId = "...";
-   })
+   .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
-   .AddDownstreamApi("DownstreamApi", configOptions =>
-   {
-       configOptions.BaseUrl = "...";
-       configOptions.Scopes = ["..."];
-   })
+   .AddDownstreamApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi"))
    .AddDistributedTokenCaches();
```

```
- List<string> scopes = ["{APP ID URI}/Weather.Get"];
- var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
+ var configuration = transformContext.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
+ var scopes = configuration.GetSection("DownstreamApi:Scopes").Get<IEnumerable<string>>();
+ var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes ??
+     throw new InvalidOperationException("No downstream API scopes!"));
```

Note

Production apps should use a production distributed token cache provider. Otherwise, the app may have poor performance in some scenarios. For more information, see the [Use a production distributed token cache provider](#use-a-production-distributed-token-cache-provider) section.

In the `MinimalApiJwt` project, add the following app settings configuration to the `appsettings.json` file:

```
"Authentication": {
  "Schemes": {
    "Bearer": {
      "Authority": "https://sts.windows.net/{TENANT ID (WEB API)}/",
      "ValidAudiences": ["{APP ID URI (WEB API)}"]
    }
  }
},
```

The preceding example uses the V1 STS token URL format. For guidance on V2 STS tokens, see the [STS token version](#sts-token-version) section.

The preceding example for the ME-ID tenant type uses the V1 STS token URL format. For guidance on V2 STS tokens, see the [STS token version](#sts-token-version) section.

Audience formats adopt the following patterns (`{CLIENT ID}` is the Client Id of the web API; `{DIRECTORY NAME}` is the directory name, for example, `contoso`):

* ME-ID tenant type: `api://{CLIENT ID}`
* Microsoft Entra External ID: `{CLIENT ID}`
* B2C tenant type: `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}`

The configuration is automatically picked up by the JWT bearer authentication builder.

Remove the following lines from the `Program` file:

```
- jwtOptions.Authority = "...";
- jwtOptions.Audience = "...";
```

For more information on configuration, see the following resources:

* [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor configuration](../fundamentals/configuration?view=aspnetcore-10.0)

## Use a production distributed token cache provider

In-memory distributed token caches are created when calling [AddDistributedTokenCaches](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.distributedtokencacheadapterextension.adddistributedtokencaches) to ensure that there's a base implementation available for distributed token caching.

Production web apps and web APIs should use a production distributed token cache (for example: [Redis](https://redis.io/), [Microsoft SQL Server](https://www.microsoft.com/sql-server), [Microsoft Azure Cosmos DB](https://azure.microsoft.com/products/cosmos-db)).

Note

For local development and testing on a single machine, you can use in-memory token caches instead of distributed token caches:

```
builder.Services.AddInMemoryTokenCaches();
```

Later in the development and testing period, adopt a production distributed token cache provider.

[AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache) adds a default implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) that stores cache items in memory, which is used by Microsoft Identity Web for token caching.

The distributed token cache is configured by [MsalDistributedTokenCacheAdapterOptions](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions):

* In development for debugging purposes, you can disable the L1 cache by setting [DisableL1Cache](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.disablel1cache) to `true`. ***Be sure to reset it back to `false` for production.***
* Set the maximum size of your L1 cache with [`L1CacheOptions.SizeLimit`](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions.sizelimit) to prevent the cache from overrunning the server's memory. The default value is 500 MB.
* In development for debugging purposes, you can disable token encryption at rest by setting [Encrypt](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.encrypt) to `false`, which is the default value. ***Be sure to reset it back to `true` for production.***
* Set token eviction from the cache with [SlidingExpiration](/en-us/dotnet/api/microsoft.extensions.caching.distributed.distributedcacheentryoptions.slidingexpiration). The default value is 1 hour.
* For more information, including guidance on the callback for L2 cache failures ([OnL2CacheFailure](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.onl2cachefailure)) and asynchronous L2 cache writes ([EnableAsyncL2Write](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.enableasyncl2write)), see [MsalDistributedTokenCacheAdapterOptions](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions) and [Token cache serialization: Distributed token caches](/en-us/entra/msal/dotnet/how-to/token-cache-serialization#distributed-token-caches).

```
builder.Services.AddDistributedMemoryCache();

builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(
    options => 
    {
      // The following lines that are commented out reflect
      // default values. We recommend overriding the default
      // value of Encrypt to encrypt tokens at rest.

      //options.DisableL1Cache = false;
      //options.L1CacheOptions.SizeLimit = 500 * 1024 * 1024;
      options.Encrypt = true;
      //options.SlidingExpiration = TimeSpan.FromHours(1);
    });
```

[AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache) requires a package reference to the [`Microsoft.Extensions.Caching.Memory` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory).

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

To configure a production distributed cache provider, see [Distributed caching in ASP.NET Core](../../performance/caching/distributed?view=aspnetcore-10.0).

Warning

Always replace the in-memory distributed token caches with a real token cache provider when deploying the app to a production environment. If you fail to adopt a production distributed token cache provider, the app may suffer significantly degraded performance.

For more information, see [Token cache serialization: Distributed caches](/en-us/entra/msal/dotnet/how-to/token-cache-serialization?tabs=msal#distributed-caches). However, the code examples shown don't apply to ASP.NET Core apps, which configure distributed caches via [AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache), not [AddDistributedTokenCache](/en-us/dotnet/api/microsoft.identity.web.tokencacheextensions.adddistributedtokencache).

Use a shared Data Protection key ring in production so that instances of the app across servers in a web farm can decrypt tokens when [MsalDistributedTokenCacheAdapterOptions.Encrypt](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.encrypt) is set to `true`.

Note

For early development and local testing on a single machine, you can set [Encrypt](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.encrypt) to `false` and configure a shared Data Protection key ring later:

```
options.Encrypt = false;
```

Later in the development and testing period, enable token encryption and adopt a shared Data Protection key ring.

The following example shows how to use [Azure Blob Storage and Azure Key Vault (`PersistKeysToAzureBlobStorage`/`ProtectKeysWithAzureKeyVault`)](../../security/data-protection/configuration/overview?view=aspnetcore-10.0#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault) for the shared key ring. The service configurations are base case scenarios for demonstration purposes. Before deploying production apps, familiarize yourself with the Azure services and adopt best practices using the Azure services' dedicated documentation sets, which are linked at the end of this section.

Confirm the presence of the following packages in the server project of the Blazor Web App:

* [`Azure.Extensions.AspNetCore.DataProtection.Blobs`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Blobs)
* [`Azure.Extensions.AspNetCore.DataProtection.Keys`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Keys)

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Note

Before proceeding with the following steps, confirm that the app is registered with Microsoft Entra.

The following code is typically implemented at the same time that a [production distributed token cache provider](../../performance/caching/distributed?view=aspnetcore-10.0) is implemented. Other options, both within Azure and outside of Azure, are available for managing data protection keys across multiple app instances, but the sample app demonstrates how to use Azure services.

Configure Azure Blob Storage to maintain data protection keys. Follow the guidance in [Key storage providers in ASP.NET Core](../../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0#azure-storage).

Configure Azure Key Vault to encrypt the data protection keys at rest. Follow the guidance in [Configure ASP.NET Core Data Protection](../../security/data-protection/configuration/overview?view=aspnetcore-10.0#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault).

Use the following code in the `Program` file where services are registered:

```
TokenCredential? credential;

if (builder.Environment.IsProduction())
{
    credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
    // Local development and testing only
    DefaultAzureCredentialOptions options = new()
    {
        // Specify the tenant ID to use the dev credentials when running the app locally
        // in Visual Studio.
        VisualStudioTenantId = "{TENANT ID}",
        SharedTokenCacheTenantId = "{TENANT ID}"
    };

    credential = new DefaultAzureCredential(options);
}

builder.Services.AddDataProtection()
    .SetApplicationName("BlazorWebAppEntra")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential)
    .ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), credential);
```

You can pass any app name to [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname). Just confirm that all app deployments use the same value.

`{MANAGED IDENTITY CLIENT ID}`: The Azure Managed Identity Client ID (GUID).

`{TENANT ID}`: Tenant ID.

`{BLOB URI}`: Full URI to the key file. The URI is generated by Azure Storage when you create the key file. Do not use a SAS.

`{KEY IDENTIFIER}`: Azure Key Vault key identifier used for key encryption. An access policy allows the application to access the key vault with `Get`, `Unwrap Key`, and `Wrap Key` permissions. The version of the key is obtained from the key in the Entra or Azure portal after it's created. If you enable autorotation of the key vault key, make sure that you use a versionless key identifier in the app's key vault configuration, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`).

Note

In non-`Production` environments, the preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

Alternatively, you can configure the app to supply the values from app settings files using the JSON Configuration Provider. Add the following to the app settings file:

```
"DistributedTokenCache": {
  "DisableL1Cache": false,
  "L1CacheSizeLimit": 524288000,
  "Encrypt": true,
  "SlidingExpirationInHours": 1
},
"DataProtection": {
  "BlobUri": "{BLOB URI}",
  "KeyIdentifier": "{KEY IDENTIFIER}"
}
```

Example `DataProtection` section:

```
"DataProtection": {
  "BlobUri": "https://contoso.blob.core.windows.net/data-protection/keys.xml",
  "KeyIdentifier": "https://contoso.vault.azure.net/keys/data-protection"
}
```

Note

The key identifier in the preceding example is *versionless*. There's no GUID key version on the end of the identifier. This is particularly important if you opt to configure automatic key rotation for the key. For more information, see [Configure cryptographic key auto-rotation in Azure Key Vault: Key rotation policy](/en-us/azure/key-vault/keys/how-to-configure-key-rotation#key-rotation-policy).

Make the following changes in the `Program` file:

```
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(
    options =>
    {
+       var config = builder.Configuration.GetSection("DistributedTokenCache");

-       options.DisableL1Cache = false;
+       options.DisableL1Cache = config.GetValue<bool>("DisableL1Cache");

-       options.L1CacheOptions.SizeLimit = 500 * 1024 * 1024;
+       options.L1CacheOptions.SizeLimit = config.GetValue<long>("L1CacheSizeLimit");

-       options.Encrypt = true;
+       options.Encrypt = config.GetValue<bool>("Encrypt");

-       options.SlidingExpiration = TimeSpan.FromHours(1);
+       options.SlidingExpiration = 
+           TimeSpan.FromHours(config.GetValue<int>("SlidingExpirationInHours"));
    });

- builder.Services.AddDataProtection()
-     .SetApplicationName("BlazorWebAppEntra")
-     .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential)
-     .ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), credential);
```

Add the following code where services are configured in the `Program` file:

```
var config = builder.Configuration.GetSection("DataProtection");

builder.Services.AddDataProtection()
    .SetApplicationName("BlazorWebAppEntra")
    .PersistKeysToAzureBlobStorage(
        new Uri(config.GetValue<string>("BlobUri") ??
        throw new Exception("Missing Blob URI")),
        credential)
    .ProtectKeysWithAzureKeyVault(
        new Uri(config.GetValue<string>("KeyIdentifier") ?? 
        throw new Exception("Missing Key Identifier")), 
        credential);
```

For more information on using a shared Data Protection key ring and key storage providers, see the following resources:

* [Key storage providers in ASP.NET Core](../../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0#azure-storage)
* [Configure ASP.NET Core Data Protection](../../security/data-protection/configuration/overview?view=aspnetcore-10.0#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault)
* [Use the Azure SDK for .NET in ASP.NET Core apps](/en-us/dotnet/azure/sdk/aspnetcore-guidance?tabs=api)
* [Host ASP.NET Core in a web farm: Data Protection](../../host-and-deploy/web-farm?view=aspnetcore-10.0#data-protection)
* [Configure ASP.NET Core Data Protection](../../security/data-protection/configuration/overview?view=aspnetcore-10.0)
* [Key storage providers in ASP.NET Core](../../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0)
* [Azure Key Vault documentation](/en-us/azure/key-vault/general/)
* [Azure Storage documentation](/en-us/azure/storage/)
* [Provide access to Key Vault keys, certificates, and secrets with Azure role-based access control](/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli)

## YARP forwarder destination prefix

The Blazor Web App server project's YARP forwarder, where the user's access token is attached to the `MinimalApiJwt` web API call, specifies a destination prefix of `https://weatherapi`. This value matches the project name passed to [`AddProject`](https://aspire.dev/reference/api/csharp/aspire.hosting/projectresourcebuilderextensions/methods/#addproject-idistributedapplicationbuilder-string) in the `AppHost.cs` file of the `Aspire.AppHost` project.

Forwarder in the Blazor Web App server project (`BlazorWebAppEntra`):

```
app.MapForwarder("/weather-forecast", "https://weatherapi", transformBuilder =>
{
    ...
}).RequireAuthorization();
```

Matching project name in the `AppHost.cs` file of the Aspire App Host project (`Aspire.AppHost`):

```
var weatherApi = builder.AddProject<Projects.MinimalApiJwt>("weatherapi");
```

There's no need to change the destination prefix of the YARP forwarder when deploying the Blazor Web App to production. The Microsoft Identity Web Downstream API package uses the base URI passed via configuration to make the web API call from the `ServerWeatherForecaster`, not the destination prefix of the YARP forwarder. In production, the YARP forwarder merely transforms the request, adding the user's access token.

## Redirect to the home page on logout

The `LogInOrOut` component (`Layout/LogInOrOut.razor`) sets a hidden field for the return URL (`ReturnUrl`) to the current URL (`currentURL`). When the user signs out of the app, the identity provider returns the user to the page from which they logged out. If the user logs out from a secure page, they're returned to the same secure page and sent back through the authentication process. This authentication flow is reasonable when users need to change accounts regularly.

Alternatively, use the following `LogInOrOut` component, which doesn't supply a return URL when logging out.

`Layout/LogInOrOut.razor`:

```
<div class="nav-item px-3">
    <AuthorizeView>
        <Authorized>
            <form action="authentication/logout" method="post">
                <AntiforgeryToken />
                <button type="submit" class="nav-link">
                    <span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true">
                    </span> Logout
                </button>
            </form>
        </Authorized>
        <NotAuthorized>
            <a class="nav-link" href="authentication/login">
                <span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span>
                Login
            </a>
        </NotAuthorized>
    </AuthorizeView>
</div>
```

## Weather data security

For more information on how this app secures its weather data, see [Secure data in Blazor Web Apps with Interactive Auto rendering](./?view=aspnetcore-10.0#secure-data-in-blazor-web-apps-with-interactive-auto-rendering).

## Host in a web farm or cluster

Server-side Blazor Web Apps hosted in a web farm or cluster of machines must adopt [*session affinity*](../fundamentals/signalr?view=aspnetcore-10.0#use-session-affinity-sticky-sessions-for-server-side-web-farm-hosting) to maintain Blazor circuits for users of the app.

We also recommend using a shared [Data Protection](../../security/data-protection/introduction?view=aspnetcore-10.0) key ring in production, even when the app uses the Interactive WebAssembly render mode exclusively for client-side rendering (no Blazor circuits).

## STS token version

There are two types of token URIs, named Version 1 (V1) and Version 2 (V2). In Azure's security token services (STS), the V1 endpoint uses the `sts.windows.net` domain as the issuer, while the V2 endpoint uses the `login.microsoftonline.com` domain as the issuer. V2 supports additional features, such as authenticating personal accounts and OpenID Connect (OIDC) protocols.

This article and its accompanying sample apps adopt V1 STS tokens. To adopt V2 tokens, make the following changes:

* The STS version must be changed in the web API's (`MinimalApiJwt`) app registration in the Azure portal. Set the value of `requestedAccessTokenVersion` to `2` in the web API app registration's manifest. Entra issues access tokens in the version requested by the resource (audience) app registration, so this setting on the Blazor Web App's client registration has no effect on the tokens that `MinimalApiJwt` receives and validates.

* Use the V2 authority URL endpoint (example: `https://login.microsoftonline.com/{TENANT ID}/v2.0`, where the `{TENANT ID}` placeholder is the tenant ID).

* In the web API (`MinimalApiJwt`), explicitly validate the issuer:

  ```
  jwtOptions.TokenValidationParameters = new TokenValidationParameters
  {
      ValidateIssuer = true,
      // Ensure the issuer ends with /v2.0 if using the V2 endpoint and that
      // {TENANT ID} is the tenant GUID (matching the token's tid claim), not a domain
      ValidIssuer = "https://login.microsoftonline.com/{TENANT ID}/v2.0",
      ValidateAudience = true,
      ValidAudiences = new[] { "{WEB API CLIENT ID 1}", "{WEB API CLIENT ID 2}", ... },
      ValidateLifetime = true
  };
  ```

  Instead of setting the [ValidAudiences](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.validaudiences) property, you can specify a single valid audience with [ValidAudience](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.validaudience):

  ```
  ValidAudience = "{WEB API CLIENT ID}",
  ```

  The `{WEB API CLIENT ID}` placeholders in the preceding examples are ***only the client IDs***, not the full values passed to the `Audience` property.

  To supply a collection of valid audiences in an app that [configures Identity from app settings](#supply-configuration-with-the-json-configuration-provider-app-settings), you can use the following code to obtain the valid audiences configuration:

  ```
  ValidAudiences = builder.Configuration.GetSection(
      "Authentication:Schemes:Bearer:ValidAudiences").Get<string[]>(),
  ```

For more information, see [Access tokens in the Microsoft identity platform: Token formats](/en-us/entra/identity-platform/access-tokens#token-formats).

  > Uncaught TypeError TypeError: Failed to execute 'postMessage' on 'Window': The provided value cannot be converted to a sequence.

    * [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)
    * [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)
    * [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

  > info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

```
@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

<PageTitle>User Claims</PageTitle>

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li><b>@claim.Type:</b> @claim.Value</li>
        }
    </ul>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    [CascadingParameter]
    private Task<AuthenticationState>? AuthState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (AuthState == null)
        {
            return;
        }

        var authState = await AuthState;
        claims = authState.User.Claims;
    }
}
```

### Inspect the access token

Obtaining the access token during development is often helpful when troubleshooting app and Azure configuration problems. In the following example for a weather forecast endpoint, the bearer token and token details are logged only when the app is compiled with the `DEBUG` symbol, which is typically a Debug build. You can decode the token using an online JWT token decoder, such as the [Microsoft JWT token decoder](https://jwt.ms/), or log details from the token in C#, as the following example demonstrates.

Caution

In production, avoid logging the token or its contents.

```
app.MapGet("/weather-forecast", (HttpContext context, ILogger<Program> logger) =>
{
#if DEBUG
    var authHeader = context.Request.Headers.Authorization.FirstOrDefault(v => 
        v != null && 
        v.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase));

    if (authHeader is not null)
    {
        var token = authHeader["Bearer ".Length..].Trim();
        logger.LogDebug("Token: {Token}", token);

        try
        {
                var handler = 
                    new Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler();
                var jwtToken = handler.ReadJsonWebToken(token);
                logger.LogDebug("Audience: {Audience}", 
                    string.Join(", ", jwtToken.Audiences));
                logger.LogDebug("Issuer: {Issuer}", jwtToken.Issuer);
            var jwtToken = handler.ReadJwtToken(token);
            logger.LogDebug("Audience: {Audience}", 
                string.Join(", ", jwtToken.Audiences));
            logger.LogDebug("Issuer: {Issuer}", jwtToken.Issuer);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to decode token.");
        }
    }
#endif

    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();
```

## Related content

* [Call a web API from an ASP.NET Core Blazor app: Microsoft identity platform for web API calls](../call-web-api?view=aspnetcore-10.0#microsoft-identity-platform-for-web-api-calls)

* [Microsoft identity platform documentation](/en-us/entra/identity-platform/)

* [Web API documentation | Microsoft identity platform](/en-us/entra/identity-platform/index-web-api)

* [A web API that calls web APIs: Call an API: Option 2: Call a downstream web API with the helper class](/en-us/entra/identity-platform/scenario-web-api-call-api-call-api?tabs=aspnetcore#option-2-call-a-downstream-web-api-with-the-helper-class)

* [`AzureAD/microsoft-identity-web` GitHub repository](https://github.com/AzureAD/microsoft-identity-web/wiki): Helpful guidance on implementing Microsoft Identity Web for Microsoft Entra ID for ASP.NET Core apps, including links to sample apps and related Azure documentation. Currently, Blazor Web Apps aren't explicitly addressed by the Azure documentation, but the setup and configuration of a Blazor Web App for ME-ID and Azure hosting is the same as it is for any ASP.NET Core web app.

* [`AuthenticationStateProvider` service](./?view=aspnetcore-10.0#authenticationstateprovider-service)

* [Manage authentication state in Blazor Web Apps](./?view=aspnetcore-10.0#manage-authentication-state-in-blazor-web-apps)

* [Service abstractions in Blazor Web Apps](../call-web-api?view=aspnetcore-10.0#service-abstractions-for-web-api-calls)

* Data Protection resources

  * [Configure ASP.NET Core Data Protection](../../security/data-protection/configuration/overview?view=aspnetcore-10.0)
  * [Key storage providers in ASP.NET Core](../../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0)
  * [Key encryption at rest in Windows and Azure using ASP.NET Core](../../security/data-protection/implementation/key-encryption-at-rest?view=aspnetcore-10.0)

* [Opaque (reference) access token support](additional-scenarios?view=aspnetcore-10.0#opaque-reference-access-token-support)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0035
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0035.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0035: Validatable property or its type on a `[ValidatableType]` is not accessible

|              | Value   |
| ------------ | ------- |
| **Rule ID**  | ASP0035 |
| **Category** | Usage   |
| **Severity** | Warning |

## Cause

A property on a type marked with [ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute) declares validation, but the property, its getter, or its type isn't accessible to the validation source generator.

## Rule description

The validation source generator can only generate validation for public properties with public getters and accessible property types. An inaccessible property that has validation attributes, implements `IValidatableObject`, or contains a type that declares validation is silently skipped.

The following code produces this diagnostic because `Child` isn't public:

```
[ValidatableType]
public class Order
{
    internal Customer Child { get; } = new();

    public class Customer
    {
        [Required]
        public string? Name { get; set; }
    }
}
```

## How to fix violations

Make the property and its getter public. Also make the property type and each of its containing types public or internal and don't declare them as file-local:

```
[ValidatableType]
public class Order
{
    public Customer Child { get; } = new();

    public class Customer
    {
        [Required]
        public string? Name { get; set; }
    }
}
```

If the property shouldn't be validated, apply [SkipValidationAttribute](/en-us/dotnet/api/microsoft.extensions.validation.skipvalidationattribute) to it.

For more information, see [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-12.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-encryption-at-rest
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/implementation/key-encryption-at-rest.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Key encryption at rest in Windows and Azure using ASP.NET Core

The data protection system [employs a discovery mechanism by default](../configuration/default-settings?view=aspnetcore-10.0) to determine how cryptographic keys should be encrypted at rest. The developer can override the discovery mechanism and manually specify how keys should be encrypted at rest.

Warning

If you specify an explicit [key persistence location](key-storage-providers?view=aspnetcore-10.0), the data protection system deregisters the default key encryption at rest mechanism. Consequently, keys are no longer encrypted at rest. We recommend that you [specify an explicit key encryption mechanism](key-encryption-at-rest?view=aspnetcore-10.0) for production deployments. The encryption-at-rest mechanism options are described in this topic.

## Azure Key Vault

For more information, see [Configure ASP.NET Core Data Protection](../configuration/overview?view=aspnetcore-10.0#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault).

## Windows DPAPI

**Only applies to Windows deployments.**

When Windows DPAPI is used, key material is encrypted with [CryptProtectData](/en-us/windows/desktop/api/dpapi/nf-dpapi-cryptprotectdata) before being persisted to storage. DPAPI is an appropriate encryption mechanism for data that's never read outside of the current machine (though it's possible to back these keys up to Active Directory). To configure DPAPI key-at-rest encryption, call one of the [ProtectKeysWithDpapi](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithdpapi)) extension methods:

```
// Only the local user account can decrypt the keys
services.AddDataProtection()
    .ProtectKeysWithDpapi();
```

If `ProtectKeysWithDpapi` is called with no parameters, only the current Windows user account can decipher the persisted key ring. You can optionally specify that any user account on the machine (not just the current user account) be able to decipher the key ring:

```
// All user accounts on the machine can decrypt the keys
services.AddDataProtection()
    .ProtectKeysWithDpapi(protectToLocalMachine: true);
```

## X.509 certificate

If the app is spread across multiple machines, it may be convenient to distribute a shared X.509 certificate (`.pfx` format) across the machines and configure the hosted apps to use the certificate for encryption of keys at rest.

In the following example, the certificate's thumbprint is passed to [ProtectKeysWithCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate):

```
services.AddDataProtection()
    .ProtectKeysWithCertificate("{CERTIFICATE THUMBPRINT}");
```

In the following example, an [X509Certificate2](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2) is passed to [ProtectKeysWithCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate):

```
var cert = new X509Certificate2(...);

services.AddDataProtection()
    .ProtectKeysWithCertificate(cert);
```

To create the certificate, use one of the following approaches or any other suitable tool or online service:

* [`dotnet dev-certs` command](/en-us/dotnet/core/tools/dotnet-dev-certs)
* [`New-SelfSignedCertificate` PowerShell command](/en-us/powershell/module/pki/new-selfsignedcertificate)
* [Azure Key Vault](/en-us/azure/key-vault/certificates/quick-create-portal#add-a-certificate-to-key-vault)
* [MakeCert on Windows](/en-us/windows/desktop/seccrypto/makecert)
* [OpenSSL](https://www.openssl.org)

For more information, see [Generate self-signed certificates with the .NET CLI](/en-us/dotnet/core/additional-tools/self-signed-certificates-guide).

Due to .NET Framework limitations, only certificates with CAPI private keys are supported. See the content below for possible workarounds to these limitations.

## Windows DPAPI-NG

**This mechanism is available only on Windows 8/Windows Server 2012 or later.**

Beginning with Windows 8, Windows OS supports DPAPI-NG (also called CNG DPAPI). For more information, see [About CNG DPAPI](/en-us/windows/desktop/SecCNG/cng-dpapi).

The principal is encoded as a protection descriptor rule. In the following example that calls [ProtectKeysWithDpapiNG](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithdpaping), only the domain-joined user with the specified SID can decrypt the key ring:

```
public void ConfigureServices(IServiceCollection services)
{
    // Uses the descriptor rule "SID=S-1-5-21-..."
    services.AddDataProtection()
        .ProtectKeysWithDpapiNG("SID=S-1-5-21-...",
        flags: DpapiNGProtectionDescriptorFlags.None);
}
```

There's also a parameterless overload of `ProtectKeysWithDpapiNG`. Use this convenience method to specify the rule "SID={CURRENT\_ACCOUNT\_SID}", where *CURRENT\_ACCOUNT\_SID* is the SID of the current Windows user account:

```
public void ConfigureServices(IServiceCollection services)
{
    // Use the descriptor rule "SID={current account SID}"
    services.AddDataProtection()
        .ProtectKeysWithDpapiNG();
}
```

In this scenario, the AD domain controller is responsible for distributing the encryption keys used by the DPAPI-NG operations. The target user can decipher the encrypted payload from any domain-joined machine (provided that the process is running under their identity).

## Certificate-based encryption with Windows DPAPI-NG

If the app is running on Windows 8.1/Windows Server 2012 R2 or later, you can use Windows DPAPI-NG to perform certificate-based encryption. Use the rule descriptor string "CERTIFICATE=HashId:{CERTIFICATE THUMBPRINT}", where the `{CERTIFICATE THUMBPRINT}` placeholder is the hex-encoded SHA1 thumbprint of the certificate:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .ProtectKeysWithDpapiNG("CERTIFICATE=HashId:{CERTIFICATE THUMBPRINT}",
            flags: DpapiNGProtectionDescriptorFlags.None);
}
```

Any app pointed at this repository must be running on Windows 8.1/Windows Server 2012 R2 or later to decipher the keys.

## Custom key encryption

If the in-box mechanisms aren't appropriate, the developer can specify their own key encryption mechanism by providing a custom [IXmlEncryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.xmlencryption.ixmlencryptor).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/directory-structure
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/directory-structure.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core directory structure

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The *publish* directory contains the app's deployable assets produced by the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command. The directory contains:

* Application files
* Configuration files
* Static assets
* Packages
* A runtime ([self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) only)

| App Type                                                                                                  | Directory Structure                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Framework-dependent Executable (FDE)](/en-us/dotnet/core/deploying/#framework-dependent-executables-fde) | - publish†

  * Views† MVC apps; if views aren't precompiled
  * Pages† MVC or Razor Pages apps, if pages aren't precompiled
  * wwwroot†
  * \*.dll files
  * {ASSEMBLY NAME}.deps.json
  * {ASSEMBLY NAME}.dll
  * {ASSEMBLY NAME}{.EXTENSION}.exe extension on Windows, no extension on macOS or Linux
  * {ASSEMBLY NAME}.pdb
  * {ASSEMBLY NAME}.runtimeconfig.json
  * web.config (IIS deployments)
  * createdump ([Linux createdump utility](https://github.com/dotnet/coreclr/blob/master/Documentation/botr/xplat-minidump-generation.md#configurationpolicy))
  * \*.so (Linux shared object library)
  * \*.a (macOS archive)
  * \*.dylib (macOS dynamic library) |
| [Self-contained Deployment (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd)           | * publish†

  * Views† MVC apps, if views aren't precompiled
  * Pages† MVC or Razor Pages apps, if pages aren't precompiled
  * wwwroot†
  * \*.dll files
  * {ASSEMBLY NAME}.deps.json
  * {ASSEMBLY NAME}.dll
  * {ASSEMBLY NAME}{.EXTENSION} .exe extension on Windows, no extension on macOS or Linux
  * {ASSEMBLY NAME}.pdb
  * {ASSEMBLY NAME}.runtimeconfig.json
  * web.config (IIS deployments)                                                                                                                                                                                                                                                                     |

†Indicates a directory

The *publish* directory represents the *content root path*, also called the *application base path*, of the deployment. Whatever name is given to the *publish* directory of the deployed app on the server, its location serves as the server's physical path to the hosted app.

The *wwwroot* directory, if present, only contains static assets.

## Additional resources

* [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish)
* [.NET application deployment](/en-us/dotnet/core/deploying/)
* [Target frameworks](/en-us/dotnet/standard/frameworks)
* [.NET RID Catalog](/en-us/dotnet/core/rid-catalog)

The *publish* directory contains the app's deployable assets produced by the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command. The directory contains:

* Application files
* Configuration files
* Static assets
* Packages
* A runtime ([self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) only)

| App Type                                                                                                  | Directory Structure                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Framework-dependent Executable (FDE)](/en-us/dotnet/core/deploying/#framework-dependent-executables-fde) | - publish†

  * Views† MVC apps; if views aren't precompiled
  * Pages† MVC or Razor Pages apps, if pages aren't precompiled
  * wwwroot†
  * \*.dll files
  * {ASSEMBLY NAME}.deps.json
  * {ASSEMBLY NAME}.dll
  * {ASSEMBLY NAME}{.EXTENSION}.exe extension on Windows, no extension on macOS or Linux
  * {ASSEMBLY NAME}.pdb
  * {ASSEMBLY NAME}.Views.dll
  * {ASSEMBLY NAME}.Views.pdb
  * {ASSEMBLY NAME}.runtimeconfig.json
  * web.config (IIS deployments)
  * createdump ([Linux createdump utility](https://github.com/dotnet/coreclr/blob/master/Documentation/botr/xplat-minidump-generation.md#configurationpolicy))
  * \*.so (Linux shared object library)
  * \*.a (macOS archive)
  * \*.dylib (macOS dynamic library) |
| [Self-contained Deployment (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd)           | * publish†

  * Views† MVC apps, if views aren't precompiled
  * Pages† MVC or Razor Pages apps, if pages aren't precompiled
  * wwwroot†
  * \*.dll files
  * {ASSEMBLY NAME}.deps.json
  * {ASSEMBLY NAME}.dll
  * {ASSEMBLY NAME}{.EXTENSION} .exe extension on Windows, no extension on macOS or Linux
  * {ASSEMBLY NAME}.pdb
  * {ASSEMBLY NAME}.Views.dll
  * {ASSEMBLY NAME}.Views.pdb
  * {ASSEMBLY NAME}.runtimeconfig.json
  * web.config (IIS deployments)                                                                                                                                                                                                                                                                     |

†Indicates a directory

The *publish* directory represents the *content root path*, also called the *application base path*, of the deployment. Whatever name is given to the *publish* directory of the deployed app on the server, its location serves as the server's physical path to the hosted app.

The *wwwroot* directory, if present, only contains static assets.

## Additional resources

* [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish)
* [.NET application deployment](/en-us/dotnet/core/deploying/)
* [Target frameworks](/en-us/dotnet/standard/frameworks)
* [.NET RID Catalog](/en-us/dotnet/core/rid-catalog)

The *publish* directory contains the app's deployable assets produced by the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command. The directory contains:

* Application files
* Configuration files
* Static assets
* Packages
* A runtime ([self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) only)

| App Type                                                                                                  | Directory Structure                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Framework-dependent Executable (FDE)](/en-us/dotnet/core/deploying/#framework-dependent-executables-fde) | - publish†

  * Views† MVC apps; if views aren't precompiled
  * Pages† MVC or Razor Pages apps, if pages aren't precompiled
  * wwwroot†
  * \*.dll files
  * {ASSEMBLY NAME}.deps.json
  * {ASSEMBLY NAME}.dll
  * {ASSEMBLY NAME}{.EXTENSION} .exe extension on Windows, no extension on macOS or Linux
  * {ASSEMBLY NAME}.pdb
  * {ASSEMBLY NAME}.Views.dll
  * {ASSEMBLY NAME}.Views.pdb
  * {ASSEMBLY NAME}.runtimeconfig.json
  * web.config (IIS deployments)
  * createdump ([Linux createdump utility](https://github.com/dotnet/coreclr/blob/master/Documentation/botr/xplat-minidump-generation.md#configurationpolicy))
  * \*.so (Linux shared object library)
  * \*.a (macOS archive)
  * \*.dylib (macOS dynamic library) |
| [Self-contained Deployment (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd)           | * publish†

  * Views† MVC apps, if views aren't precompiled
  * Pages† MVC or Razor Pages apps, if pages aren't precompiled
  * wwwroot†
  * \*.dll files
  * {ASSEMBLY NAME}.deps.json
  * {ASSEMBLY NAME}.dll
  * {ASSEMBLY NAME}.exe
  * {ASSEMBLY NAME}.pdb
  * {ASSEMBLY NAME}.Views.dll
  * {ASSEMBLY NAME}.Views.pdb
  * {ASSEMBLY NAME}.runtimeconfig.json
  * web.config (IIS deployments)                                                                                                                                                                                                                                                                                                                                        |

†Indicates a directory

The *publish* directory represents the *content root path*, also called the *application base path*, of the deployment. Whatever name is given to the *publish* directory of the deployed app on the server, its location serves as the server's physical path to the hosted app.

The *wwwroot* directory, if present, only contains static assets.

Creating a *Logs* folder is useful for [ASP.NET Core Module enhanced debug logging](iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs). Folders in the path provided to the `<handlerSetting>` value aren't created by the module automatically and should pre-exist in the deployment to allow the module to write the debug log.

A *Logs* directory can be created for the deployment using one of the following two approaches:

* Add the following `<Target>` element to the project file:

  ```
  <Target Name="CreateLogsFolder" AfterTargets="Publish">
     <MakeDir Directories="$(PublishDir)Logs" 
              Condition="!Exists('$(PublishDir)Logs')" />
     <WriteLinesToFile File="$(PublishDir)Logs\.log" 
                       Lines="Generated file" 
                       Overwrite="True" 
                       Condition="!Exists('$(PublishDir)Logs\.log')" />
  </Target>
  ```

  The `<MakeDir>` element creates an empty *Logs* folder in the published output. The element uses the `PublishDir` property to determine the target location for creating the folder. Several deployment methods, such as Web Deploy, skip empty folders during deployment. The `<WriteLinesToFile>` element generates a file in the *Logs* folder, which guarantees deployment of the folder to the server. Folder creation using this approach fails if the worker process doesn't have write access to the target folder.

* Physically create the *Logs* directory on the server in the deployment.

The deployment directory requires Read/Execute permissions. The *Logs* directory requires Read/Write permissions. Additional directories where files are written require Read/Write permissions.

## Additional resources

* [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish)
* [.NET application deployment](/en-us/dotnet/core/deploying/)
* [Target frameworks](/en-us/dotnet/standard/frameworks)
* [.NET RID Catalog](/en-us/dotnet/core/rid-catalog)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/logging
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/logging.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor logging

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article provides information on logging in Blazor apps, particularly in client-side logging scenarios.

For general ASP.NET Core logging guidance, including how to log from Razor components, see [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0).

## Configuration

Logging configuration can be loaded from app settings files. For more information, see [ASP.NET Core Blazor configuration](configuration?view=aspnetcore-10.0#logging-configuration).

At default log levels and without configuring additional logging providers:

* On the server, logging only occurs to the server-side .NET console in the `Development` environment at the [LogLevel.Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) level or higher.
* On the client, logging only occurs to the client-side [browser developer tools](https://developer.mozilla.org/docs/Glossary/Developer_Tools) console at the [LogLevel.Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) level or higher.

When the app is configured in the project file to use implicit namespaces (`<ImplicitUsings>enable</ImplicitUsings>`), a `using` directive for [Microsoft.Extensions.Logging](/en-us/dotnet/api/microsoft.extensions.logging) or any API in the [LoggerExtensions](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions) class isn't required to support API [Visual Studio IntelliSense](/en-us/visualstudio/ide/using-intellisense) completions or building apps. If implicit namespaces aren't enabled, Razor components must explicitly define [`@using` directives](../../mvc/views/razor?view=aspnetcore-10.0#using) for logging namespaces that aren't imported via the imports file (`_Imports.razor`).

## Log levels

Log levels conform to ASP.NET Core app log levels, which are listed in the API documentation at [LogLevel](/en-us/dotnet/api/microsoft.extensions.logging.loglevel).

## Client-side logging

Not every feature of [ASP.NET Core logging](../../fundamentals/logging/?view=aspnetcore-10.0) is supported client-side. For example, client-side components don't have access to the client's file system or network, so writing logs to the client's physical or network storage isn't possible. When using a third-party logging service designed to work with single-page apps (SPAs), follow the service's security guidance. Keep in mind that every piece of data, including keys or secrets stored client-side are ***insecure*** and can be easily discovered by malicious users.

Depending on the framework version and logging features, logging implementations may require adding the namespace for [Microsoft.Extensions.Logging](/en-us/dotnet/api/microsoft.extensions.logging) to the `Program` file:

```
using Microsoft.Extensions.Logging;
```

Configure logging in client-side apps with the [WebAssemblyHostBuilder.Logging](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.logging#microsoft-aspnetcore-components-webassembly-hosting-webassemblyhostbuilder-logging) property. The [Logging](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.logging#microsoft-aspnetcore-components-webassembly-hosting-webassemblyhostbuilder-logging) property is of type [ILoggingBuilder](/en-us/dotnet/api/microsoft.extensions.logging.iloggingbuilder), so the extension methods of [ILoggingBuilder](/en-us/dotnet/api/microsoft.extensions.logging.iloggingbuilder) are supported.

To set the minimum logging level, call [LoggingBuilderExtensions.SetMinimumLevel](/en-us/dotnet/api/microsoft.extensions.logging.loggingbuilderextensions.setminimumlevel) on the host builder in the `Program` file with the [LogLevel](/en-us/dotnet/api/microsoft.extensions.logging.loglevel). The following example sets the minimum log level to [Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning):

```
builder.Logging.SetMinimumLevel(LogLevel.Warning);
```

## Log in the client-side `Program` file

Logging is supported in client-side apps after the [WebAssemblyHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder) is built using the framework's internal console logger provider ([`WebAssemblyConsoleLoggerProvider` (reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyConsoleLoggerProvider.cs)).

In the `Program` file:

```
var host = builder.Build();

var logger = host.Services.GetRequiredService<ILoggerFactory>()
    .CreateLogger<Program>();

logger.LogInformation("Logged after the app is built in the Program file.");

await host.RunAsync();
```

Developer tools console output:

> info: Program\[0]\
> Logged after the app is built in the Program file.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Client-side log category

[Log categories](../../fundamentals/logging/?view=aspnetcore-10.0#log-category) are supported.

The following example shows how to use log categories with the `Counter` component of an app created from a Blazor project template.

In the `IncrementCount` method of the app's `Counter` component (`Counter.razor`) that injects an [ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory) as `LoggerFactory`:

```
var logger = LoggerFactory.CreateLogger("CustomCategory");
logger.LogWarning("Someone has clicked me!");
```

Developer tools console output:

> warn: CustomCategory\[0]\
> Someone has clicked me!

## Client-side log event ID

[Log event ID](../../fundamentals/logging/?view=aspnetcore-10.0#log-event-id) is supported.

The following example shows how to use log event IDs with the `Counter` component of an app created from a Blazor project template.

`LogEvent.cs`:

```
public class LogEvent
{
    public const int Event1 = 1000;
    public const int Event2 = 1001;
}
```

In the `IncrementCount` method of the app's `Counter` component (`Counter.razor`):

```
logger.LogInformation(LogEvent.Event1, "Someone has clicked me!");
logger.LogWarning(LogEvent.Event2, "Someone has clicked me!");
```

Developer tools console output:

> info: BlazorSample.Pages.Counter\[1000]\
> Someone has clicked me!\
> warn: BlazorSample.Pages.Counter\[1001]\
> Someone has clicked me!

## Client-side log message template

[Log message templates](../../fundamentals/logging/?view=aspnetcore-10.0#log-message-template) are supported:

The following example shows how to use log message templates with the `Counter` component of an app created from a Blazor project template.

In the `IncrementCount` method of the app's `Counter` component (`Counter.razor`):

```
logger.LogInformation("Someone clicked me at {CurrentDT}!", DateTime.UtcNow);
```

Developer tools console output:

> info: BlazorSample.Pages.Counter\[0]\
> Someone clicked me at 04/21/2022 12:15:57!

## Client-side log exception parameters

[Log exception parameters](../../fundamentals/logging/?view=aspnetcore-10.0#log-exceptions) are supported.

The following example shows how to use log exception parameters with the `Counter` component of an app created from a Blazor project template.

In the `IncrementCount` method of the app's `Counter` component (`Counter.razor`):

```
currentCount++;

try
{
    if (currentCount == 3)
    {
        currentCount = 4;
        throw new OperationCanceledException("Skip 3");
    }
}
catch (Exception ex)
{
    logger.LogWarning(ex, "Exception (currentCount: {Count})!", currentCount);
}
```

Developer tools console output:

> warn: BlazorSample.Pages.Counter\[0]\
> Exception (currentCount: 4)!\
> System.OperationCanceledException: Skip 3\
> at BlazorSample.Pages.Counter.IncrementCount() in C:UsersAlabaDesktopBlazorSamplePagesCounter.razor:line 28

## Client-side filter function

[Filter functions](../../fundamentals/logging/?view=aspnetcore-10.0#filter-function) are supported.

The following example shows how to use a filter with the `Counter` component of an app created from a Blazor project template.

In the `Program` file:

```
builder.Logging.AddFilter((provider, category, logLevel) =>
    category.Equals("CustomCategory2") && logLevel == LogLevel.Information);
```

In the `IncrementCount` method of the app's `Counter` component (`Counter.razor`) that injects an [ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory) as `LoggerFactory`:

```
var logger1 = LoggerFactory.CreateLogger("CustomCategory1");
logger1.LogInformation("Someone has clicked me!");

var logger2 = LoggerFactory.CreateLogger("CustomCategory1");
logger2.LogWarning("Someone has clicked me!");

var logger3 = LoggerFactory.CreateLogger("CustomCategory2");
logger3.LogInformation("Someone has clicked me!");

var logger4 = LoggerFactory.CreateLogger("CustomCategory2");
logger4.LogWarning("Someone has clicked me!");
```

In the developer tools console output, the filter only permits logging for the `CustomCategory2` category and [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) log level message:

> info: CustomCategory2\[0]\
> Someone has clicked me!

The app can also configure log filtering for specific namespaces. For example, set the log level to [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-trace) in the `Program` file:

```
builder.Logging.SetMinimumLevel(LogLevel.Trace);
```

Normally at the [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-trace) log level, developer tools console output at the **Verbose** level includes [Microsoft.AspNetCore.Components.RenderTree](/en-us/dotnet/api/microsoft.aspnetcore.components.rendertree) logging messages, such as the following:

> dbug: Microsoft.AspNetCore.Components.RenderTree.Renderer\[3]\
> Rendering component 14 of type Microsoft.AspNetCore.Components.Web.HeadOutlet

In the `Program` file, logging messages specific to [Microsoft.AspNetCore.Components.RenderTree](/en-us/dotnet/api/microsoft.aspnetcore.components.rendertree) can be disabled using ***either*** of the following approaches:

* ```
  builder.Logging.AddFilter("Microsoft.AspNetCore.Components.RenderTree.*", LogLevel.None);
  ```
* ```
  builder.Services.PostConfigure<LoggerFilterOptions>(options =>
      options.Rules.Add(
          new LoggerFilterRule(null, 
                               "Microsoft.AspNetCore.Components.RenderTree.*", 
                               LogLevel.None, 
                               null)
      ));
  ```

After ***either*** of the preceding filters is added to the app, the console output at the **Verbose** level doesn't show logging messages from the [Microsoft.AspNetCore.Components.RenderTree](/en-us/dotnet/api/microsoft.aspnetcore.components.rendertree) API.

## Client-side custom logger provider

The example in this section demonstrates a custom logger provider for further customization.

Add a package reference to the app for the [`Microsoft.Extensions.Logging.Configuration` package](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Configuration).

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Add the following custom logger configuration. The configuration establishes a `LogLevels` dictionary that sets a custom log format for three log levels: [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information), [Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning), and [Error](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-error). A `LogFormat` [`enum`](/en-us/dotnet/csharp/language-reference/builtin-types/enum) is used to describe short (`LogFormat.Short`) and long (`LogFormat.Long`) formats.

`CustomLoggerConfiguration.cs`:

```
using Microsoft.Extensions.Logging;

public class CustomLoggerConfiguration
{
    public int EventId { get; set; }

    public Dictionary<LogLevel, LogFormat> LogLevels { get; set; } = 
        new()
        {
            [LogLevel.Information] = LogFormat.Short,
            [LogLevel.Warning] = LogFormat.Short,
            [LogLevel.Error] = LogFormat.Long
        };

    public enum LogFormat
    {
        Short,
        Long
    }
}
```

Add the following custom logger to the app. The `CustomLogger` outputs custom log formats based on the `logLevel` values defined in the preceding `CustomLoggerConfiguration` configuration.

```
using Microsoft.Extensions.Logging;
using static CustomLoggerConfiguration;

public sealed class CustomLogger : ILogger
{
    private readonly string name;
    private readonly Func<CustomLoggerConfiguration> getCurrentConfig;

    public CustomLogger(
        string name,
        Func<CustomLoggerConfiguration> getCurrentConfig) =>
        (this.name, this.getCurrentConfig) = (name, getCurrentConfig);

    public IDisposable BeginScope<TState>(TState state) => default!;

    public bool IsEnabled(LogLevel logLevel) =>
        getCurrentConfig().LogLevels.ContainsKey(logLevel);

    public void Log<TState>(
        LogLevel logLevel,
        EventId eventId,
        TState state,
        Exception? exception,
        Func<TState, Exception?, string> formatter)
    {
        if (!IsEnabled(logLevel))
        {
            return;
        }

        CustomLoggerConfiguration config = getCurrentConfig();

        if (config.EventId == 0 || config.EventId == eventId.Id)
        {
            switch (config.LogLevels[logLevel])
            {
                case LogFormat.Short:
                    Console.WriteLine($"{name}: {formatter(state, exception)}");
                    break;
                case LogFormat.Long:
                    Console.WriteLine($"[{eventId.Id, 2}: {logLevel, -12}] {name} - {formatter(state, exception)}");
                    break;
                default:
                    // No-op
                    break;
            }
        }
    }
}
```

Add the following custom logger provider to the app. `CustomLoggerProvider` adopts an [`Options`-based approach](../../fundamentals/configuration/options?view=aspnetcore-10.0) to configure the logger via built-in logging configuration features. For example, the app can set or change log formats via an `appsettings.json` file without requiring code changes to the custom logger, which is demonstrated at the end of this section.

`CustomLoggerProvider.cs`:

```
using System.Collections.Concurrent;
using Microsoft.Extensions.Options;

[ProviderAlias("CustomLog")]
public sealed class CustomLoggerProvider : ILoggerProvider
{
    private readonly IDisposable onChangeToken;
    private CustomLoggerConfiguration config;
    private readonly ConcurrentDictionary<string, CustomLogger> loggers =
        new(StringComparer.OrdinalIgnoreCase);

    public CustomLoggerProvider(
        IOptionsMonitor<CustomLoggerConfiguration> config)
    {
        this.config = config.CurrentValue;
        onChangeToken = config.OnChange(updatedConfig => this.config = updatedConfig);
    }

    public ILogger CreateLogger(string categoryName) =>
        loggers.GetOrAdd(categoryName, name => new CustomLogger(name, GetCurrentConfig));

    private CustomLoggerConfiguration GetCurrentConfig() => config;

    public void Dispose()
    {
        loggers.Clear();
        onChangeToken.Dispose();
    }
}
```

Add the following custom logger extensions.

`CustomLoggerExtensions.cs`:

```
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Configuration;

public static class CustomLoggerExtensions
{
    public static ILoggingBuilder AddCustomLogger(
        this ILoggingBuilder builder)
    {
        builder.AddConfiguration();

        builder.Services.TryAddEnumerable(
            ServiceDescriptor.Singleton<ILoggerProvider, CustomLoggerProvider>());

        LoggerProviderOptions.RegisterProviderOptions
            <CustomLoggerConfiguration, CustomLoggerProvider>(builder.Services);

        return builder;
    }
}
```

In the `Program` file on the host builder, clear the existing provider by calling [ClearProviders](/en-us/dotnet/api/microsoft.extensions.logging.loggingbuilderextensions.clearproviders) and add the custom logging provider:

```
builder.Logging.ClearProviders().AddCustomLogger();
```

In the following `CustomLoggerExample` component:

* The debug message isn't logged.
* The information message is logged in the short format (`LogFormat.Short`).
* The warning message is logged in the short format (`LogFormat.Short`).
* The error message is logged in the long format (`LogFormat.Long`).
* The trace message isn't logged.

`CustomLoggerExample.razor`:

```
@page "/custom-logger-example"
@inject ILogger<CustomLoggerExample> Logger

<p>
    <button @onclick="LogMessages">Log Messages</button>
</p>

@code{
    private void LogMessages()
    {
        Logger.LogDebug(1, "This is a debug message.");
        Logger.LogInformation(3, "This is an information message.");
        Logger.LogWarning(5, "This is a warning message.");
        Logger.LogError(7, "This is an error message.");
        Logger.LogTrace(5!, "This is a trace message.");
    }
}
```

```
@page "/custom-logger-example"
@using Microsoft.Extensions.Logging
@inject ILogger<CustomLoggerExample> Logger

<p>
    <button @onclick="LogMessages">Log Messages</button>
</p>

@code{
    private void LogMessages()
    {
        Logger.LogDebug(1, "This is a debug message.");
        Logger.LogInformation(3, "This is an information message.");
        Logger.LogWarning(5, "This is a warning message.");
        Logger.LogError(7, "This is an error message.");
        Logger.LogTrace(5!, "This is a trace message.");
    }
}
```

The following output is seen in the browser's developer tools console when the **`Log Messages`** button is selected. The log entries reflect the appropriate formats applied by the custom logger (the client app is named `LoggingTest`):

> LoggingTest.Pages.CustomLoggerExample: This is an information message.\
> LoggingTest.Pages.CustomLoggerExample: This is a warning message.\
> \[ 7: Error ] LoggingTest.Pages.CustomLoggerExample - This is an error message.

From a casual inspection of the preceding example, it's apparent that setting the log line formats via the dictionary in `CustomLoggerConfiguration` isn't strictly necessary. The line formats applied by the custom logger (`CustomLogger`) could have been applied by merely checking the `logLevel` in the `Log` method. The purpose of assigning the log format via configuration is that the developer can change the log format easily via app configuration, as the following example demonstrates.

In the client-side app, add or update the `appsettings.json` file to include logging configuration. Set the log format to `Long` for all three log levels:

```
{
  "Logging": {
    "CustomLog": {
      "LogLevels": {
        "Information": "Long",
        "Warning": "Long",
        "Error": "Long"
      }
    }
  }
}
```

In the preceding example, notice that the entry for the custom logger configuration is `CustomLog`, which was applied to the custom logger provider (`CustomLoggerProvider`) as an alias with `[ProviderAlias("CustomLog")]`. The logging configuration could have been applied with the name `CustomLoggerProvider` instead of `CustomLog`, but use of the alias `CustomLog` is more user friendly.

In the `Program` file, consume the logging configuration. Add the following code:

```
builder.Logging.AddConfiguration(
    builder.Configuration.GetSection("Logging"));
```

The call to [LoggingBuilderConfigurationExtensions.AddConfiguration](/en-us/dotnet/api/microsoft.extensions.logging.configuration.loggingbuilderconfigurationextensions.addconfiguration) can be placed either before or after adding the custom logger provider.

Run the app again. Select the **`Log Messages`** button. Notice that the logging configuration is applied from the `appsettings.json` file. All three log entries are in the long (`LogFormat.Long`) format (the client app is named `LoggingTest`):

> \[ 3: Information ] LoggingTest.Pages.CustomLoggerExample - This is an information message.\
> \[ 5: Warning ] LoggingTest.Pages.CustomLoggerExample - This is a warning message.\
> \[ 7: Error ] LoggingTest.Pages.CustomLoggerExample - This is an error message.

## Client-side log scopes

The developer tools console logger doesn't support [log scopes](../../fundamentals/logging/?view=aspnetcore-10.0#log-scopes). However, a [custom logger](#client-side-custom-logger-provider) can support log scopes. For an unsupported example that you can further develop to suit your needs, see the `BlazorWebAssemblyScopesLogger` sample app in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples) ([how to download](./?view=aspnetcore-10.0#sample-apps)).

The sample app uses standard ASP.NET Core [BeginScope](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.beginscope) logging syntax to indicate scopes for logged messages. The `Logger` service in the following example is an `ILogger<CustomLoggerExample>`, which is injected into the app's `CustomLoggerExample` component (`CustomLoggerExample.razor`).

```
using (Logger.BeginScope("L1"))
{
    Logger.LogInformation(3, "INFO: ONE scope.");
}

using (Logger.BeginScope("L1"))
{
    using (Logger.BeginScope("L2"))
    {
        Logger.LogInformation(3, "INFO: TWO scopes.");
    }
}

using (Logger.BeginScope("L1"))
{
    using (Logger.BeginScope("L2"))
    {
        using (Logger.BeginScope("L3"))
        {
            Logger.LogInformation(3, "INFO: THREE scopes.");
        }
    }
}
```

Output:

> \[ 3: Information ] {CLASS} - INFO: ONE scope. => L1 blazor.webassembly.js:1:35542\
> \[ 3: Information ] {CLASS} - INFO: TWO scopes. => L1 => L2 blazor.webassembly.js:1:35542\
> \[ 3: Information ] {CLASS} - INFO: THREE scopes. => L1 => L2 => L3

The `{CLASS}` placeholder in the preceding example is `BlazorWebAssemblyScopesLogger.Pages.CustomLoggerExample`.

## Prerendered component logging

Prerendered components execute [component initialization code twice](../components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync). Logging takes place server-side on the first execution of initialization code and client-side on the second execution of initialization code. Depending on the goal of logging during initialization, check logs server-side, client-side, or both.

## SignalR client logging with the SignalR client builder

*This section applies to server-side apps.*

In Blazor script start configuration, pass in the `configureSignalR` configuration object that calls `configureLogging` with the log level.

For the `configureLogging` log level value, pass the argument as either the string or integer log level shown in the following table.

| [LogLevel](/en-us/dotnet/api/microsoft.extensions.logging.loglevel)                                                      | String setting | Integer setting |
| ------------------------------------------------------------------------------------------------------------------------ | -------------- | --------------- |
| [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-trace)             | `trace`        | 0               |
| [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug)             | `debug`        | 1               |
| [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) | `information`  | 2               |
| [Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning)         | `warning`      | 3               |
| [Error](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-error)             | `error`        | 4               |
| [Critical](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-critical)       | `critical`     | 5               |
| [None](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-none)               | `none`         | 6               |

Example 1: Set the [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) log level with a string value.

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      configureSignalR: function (builder) {
        builder.configureLogging("information");
      }
    }
  });
</script>
```

Blazor Server:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      builder.configureLogging("information");
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Example 2: Set the [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) log level with an integer value.

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      configureSignalR: function (builder) {
        builder.configureLogging(2); // LogLevel.Information
      }
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Blazor Server:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      builder.configureLogging(2); // LogLevel.Information
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Note

Using an integer to specify the logging level in Example 2, often referred to as a *magic number* or *magic constant*, is considered a poor coding practice because the integer doesn't clearly identify the logging level when viewing the source code. If minimizing the bytes transferred to the browser is a priority, using an integer might be justified (consider removing the comment in such cases).

For more information on Blazor startup (`Blazor.start()`), see [ASP.NET Core Blazor startup](startup?view=aspnetcore-10.0).

## SignalR client logging with app configuration

Set up app settings configuration as described in [ASP.NET Core Blazor configuration](configuration?view=aspnetcore-10.0#logging-configuration). Place app settings files in `wwwroot` that contain a `Logging:LogLevel:HubConnection` app setting.

Note

As an alternative to using app settings, you can pass the [LogLevel](/en-us/dotnet/api/microsoft.extensions.logging.loglevel) as the argument to [LoggingBuilderExtensions.SetMinimumLevel](/en-us/dotnet/api/microsoft.extensions.logging.loggingbuilderextensions.setminimumlevel) when the hub connection is created in a Razor component. However, accidentally deploying the app to a production hosting environment with verbose logging may result in a performance penalty. We recommend using app settings to set the log level.

Provide a `Logging:LogLevel:HubConnection` app setting in the default `appsettings.json` file and in the `Development` environment app settings file. Use a typical less-verbose log level for the default, such as [LogLevel.Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning). The default app settings value is what is used in `Staging` and `Production` environments if no app settings files for those environments are present. Use a verbose log level in the `Development` environment app settings file, such as [LogLevel.Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-trace).

`wwwroot/appsettings.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "HubConnection": "Warning"
    }
  }
}
```

`wwwroot/appsettings.Development.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "HubConnection": "Trace"
    }
  }
}
```

Important

Configuration in the preceding app settings files is only used by the app if the guidance in [ASP.NET Core Blazor configuration](configuration?view=aspnetcore-10.0#logging-configuration) is followed.

At the top of the Razor component file (`.razor`):

* Inject an [ILoggerProvider](/en-us/dotnet/api/microsoft.extensions.logging.iloggerprovider) to add a `WebAssemblyConsoleLogger` to the logging providers passed to [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). Unlike a [ConsoleLoggerProvider](/en-us/dotnet/api/microsoft.extensions.logging.console.consoleloggerprovider), `WebAssemblyConsoleLogger` is a wrapper around browser-specific logging APIs (for example, `console.log`). Use of `WebAssemblyConsoleLogger` makes logging possible within Mono inside a browser context.
* Inject an `IConfiguration` to read the `Logging:LogLevel:HubConnection` app setting.

Note

`WebAssemblyConsoleLogger` is [internal](/en-us/dotnet/csharp/language-reference/keywords/internal) and not supported for direct use in developer code.

```
@inject ILoggerProvider LoggerProvider
@inject IConfiguration Config
```

Note

The following example is based on the demonstration in the [SignalR with Blazor tutorial](../tutorials/signalr-blazor?view=aspnetcore-10.0). Consult the tutorial for further details.

In the component's [`OnInitializedAsync` method](../components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync), use [HubConnectionBuilderExtensions.ConfigureLogging](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilderextensions.configurelogging) to add the logging provider and set the minimum log level from configuration:

```
protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .ConfigureLogging(builder => 
        {
            builder.AddProvider(LoggerProvider);
            builder.SetMinimumLevel(
                Config.GetValue<LogLevel>("Logging:LogLevel:HubConnection"));
        })
        .Build();

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}
```

Note

In the preceding example, `Navigation` is an injected [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager).

For more information on setting the app's environment, see [ASP.NET Core Blazor environments](environments?view=aspnetcore-10.0).

## Client-side authentication logging

Log Blazor authentication messages at the [LogLevel.Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug) or [LogLevel.Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-trace) logging levels with a logging configuration in app settings or by using a log filter for [Microsoft.AspNetCore.Components.WebAssembly.Authentication](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication) in the `Program` file.

Use ***either*** of the following approaches:

* In an app settings file (for example, `wwwroot/appsettings.Development.json`):

  ```
  "Logging": {
    "LogLevel": {
      "Microsoft.AspNetCore.Components.WebAssembly.Authentication": "Debug"
    }
  }
  ```

  For more information on how to configure a client-side app to read app settings files, see [ASP.NET Core Blazor configuration](configuration?view=aspnetcore-10.0#logging-configuration).

* Using a log filter, the following example:

  * Activates logging for the `Debug` build configuration using a [C# preprocessor directive](/en-us/dotnet/csharp/language-reference/preprocessor-directives).
  * Logs Blazor authentication messages at the [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug) log level.

  ```
  #if DEBUG
      builder.Logging.AddFilter(
          "Microsoft.AspNetCore.Components.WebAssembly.Authentication", 
          LogLevel.Debug);
  #endif
  ```

Note

Razor components rendered on the client only log to the client-side [browser developer tools](https://developer.mozilla.org/docs/Glossary/Developer_Tools) console.

## Related content

* [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0)

* [`Loglevel` Enum (API documentation)](/en-us/dotnet/api/microsoft.extensions.logging.loglevel)

* [Implement a custom logging provider in .NET](/en-us/dotnet/core/extensions/custom-logging-provider)

* Browser developer tools documentation:

  * [Chrome DevTools](https://developer.chrome.com/docs/devtools/)
  * [Microsoft Edge Developer Tools overview](/en-us/microsoft-edge/devtools-guide-chromium/)

* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](./?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/tooling
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/tooling.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use tooling to help migrate ASP.NET Framework to ASP.NET Core

## The GitHub Copilot app modernization

To upgrade ASP.NET Framework applications (MVC, Web API, and Web Forms) to ASP.NET Core, use the [GitHub Copilot app modernization Visual Studio extension](/en-us/dotnet/core/porting/github-copilot-app-modernization/overview).

The GitHub Copilot app modernization agent is a Visual Studio extension that leverages AI to simplify the process of upgrading legacy .NET applications. By integrating with GitHub Copilot Chat, this tool analyzes your solution to generate upgrade plans and assists in rewriting code to support ASP.NET Core. It streamlines the migration workflow by reducing manual effort, identifying dependencies, and providing interactive, automated guidance for modernizing your codebase. To learn how to upgrade your ASP.NET apps using the recommended tooling, see [How to upgrade a .NET app with GitHub Copilot app modernization](/en-us/dotnet/core/porting/github-copilot-app-modernization/how-to-upgrade-with-github-copilot).

If your .NET Framework project has supporting libraries in the solution that are required, upgrade them to .NET Standard 2.0, if possible. For more information, see [Upgrade supporting libraries](start?view=aspnetcore-10.0#upgrade-supporting-libraries).

## See also

* [Quickstart: Assess and migrate a .NET project with GitHub Copilot app modernization for .NET](/en-us/dotnet/azure/migration/appmod/quickstart)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/misc
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/misc.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Technical differences between ASP.NET Framework and ASP.NET Core

This document outlines important technical differences between ASP.NET Framework and ASP.NET Core that you should understand before beginning your migration.

## URI decoding differences

ASP.NET Core handles URI encoding differently:

| Character | Encoded | ASP.NET Core | ASP.NET Framework |
| --------- | ------- | ------------ | ----------------- |
| `\`       | `%5C`   | `\`          | `/`               |
| `/`       | `%2F`   | `%2F`        | `/`               |

**Recommendation**: Use `new Uri(this.AspNetCoreHttpRequest.GetEncodedUrl())` for proper URL handling.

## User Secrets migration

User Secrets require special handling. See [GitHub issue #27611](https://github.com/dotnet/AspNetCore.Docs/issues/27611) for current guidance.

## CultureInfo.CurrentCulture differences

Note

ASP.NET Core does not automatically set `CultureInfo.CurrentCulture` for requests like ASP.NET Framework does. You must explicitly configure localization middleware.

In ASP.NET Framework, [CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture) was set for a request, but this is not done automatically in ASP.NET Core. Instead, you must add the appropriate middleware to your pipeline.

**Recommendation**: See [ASP.NET Core Localization](/en-us/aspnet/core/fundamentals/localization#localization-middleware) for details on how to enable this.

Simplest way to enable this with similar behavior as ASP.NET Framework would be to add the following to your pipeline:

```
app.UseRequestLocalization();
```

### Threading considerations

Note

This makes use of the [System.Web Adapters](../inc/systemweb-adapters?view=aspnetcore-10.0) to simplify migration.

ASP.NET Core does not guarantee thread affinity for requests. If your code requires thread affinity, you must ensure proper synchronization or use the `SingleThreadedRequest` attribute:

```
[SingleThreadedRequest]
public class LegacyController : Controller
{
    public IActionResult Index()
    {
        // Some code that requires to be run on a single thread
        return View();
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-apache
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/linux-nginx.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host ASP.NET Core on Linux with Nginx

By [Sourabh Shirhatti](https://twitter.com/sshirhatti)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This guide explains setting up a production-ready ASP.NET Core environment for Ubuntu, Red Hat Enterprise (RHEL), and SUSE Linux Enterprise Server.

For information on other Linux distributions supported by ASP.NET Core, see [Prerequisites for .NET on Linux](/en-us/dotnet/core/linux-prerequisites).

This guide:

* Places an existing ASP.NET Core app behind a reverse proxy server.
* Sets up the reverse proxy server to forward requests to the Kestrel web server.
* Ensures the web app runs on startup as a daemon.
* Configures a process management tool to help restart the web app.

## Prerequisites

* [Ubuntu](#tabpanel_1_linux-ubuntu)
* [Red Hat Enterprise Linux](#tabpanel_1_linux-rhel)
* [SUSE Linux Enterprise Server](#tabpanel_1_linux-sles)

- Access to Ubuntu 20.04 with a standard user account with sudo privilege.
- The latest stable [.NET runtime installed](/en-us/dotnet/core/install/linux) on the server.
- An existing ASP.NET Core app.

* Access to Red Hat Enterprise (RHEL) 8.0 or later with a standard user account with sudo privilege.
* The latest stable [.NET runtime installed](/en-us/dotnet/core/install/linux) on the server.
* An existing ASP.NET Core app.

- Access to SLES 12 or 15 with a standard user account with sudo privilege.
- The latest stable [.NET runtime installed](/en-us/dotnet/core/install/linux) on the server.
- An existing ASP.NET Core app.

At any point in the future after upgrading the shared framework, restart the ASP.NET Core apps hosted by the server.

## Publish and copy over the app

Configure the app for a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd).

If the app is run locally in the [`Development` environment](../fundamentals/environments?view=aspnetcore-10.0#configure-services-and-middleware-by-environment) and isn't configured by the server to make secure HTTPS connections, adopt either of the following approaches:

* Configure the app to handle secure local connections. For more information, see the [HTTPS configuration](#https-configuration) section.

* Configure the app to run at the insecure endpoint:

  * Deactivate HTTPS redirection middleware in the `Development` environment (`Program.cs`):

    ```
    if (!app.Environment.IsDevelopment())
    {
        app.UseHttpsRedirection();
    }
    ```

    For more information, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0#configure-services-and-middleware-by-environment).

  * Remove `https://localhost:5001` (if present) from the `applicationUrl` property in the `Properties/launchSettings.json` file.

For more information on configuration by environment, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0).

Run [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) from the `Development` environment to package an app into a directory (for example, `bin/Release/{TARGET FRAMEWORK MONIKER}/publish`, where the `{TARGET FRAMEWORK MONIKER}` placeholder is the [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks)) that can run on the server:

```
dotnet publish --configuration Release
```

The app can also be published as a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) if you prefer not to maintain the .NET runtime on the server.

Copy the ASP.NET Core app to the server using a tool that integrates into the organization's workflow (for example, `SCP`, `SFTP`). It's common to locate web apps under the `var` directory (for example, `var/www/helloapp`).

Note

Under a production deployment scenario, a continuous integration workflow does the work of publishing the app and copying the assets to the server.

Test the app:

1. From the command line, run the app: `dotnet <app_assembly>.dll`.
2. In a browser, navigate to `http://<serveraddress>:<port>` to verify the app works on Linux locally.

## Configure a reverse proxy server

A reverse proxy is a common setup for serving dynamic web apps. A reverse proxy terminates the HTTP request and forwards it to the ASP.NET Core app.

### Use a reverse proxy server

Kestrel is great for serving dynamic content from ASP.NET Core. However, the web serving capabilities aren't as feature rich as servers such as IIS, Apache, or Nginx. A reverse proxy server can offload work such as serving static content, caching requests, compressing requests, and HTTPS termination from the HTTP server. A reverse proxy server may reside on a dedicated machine or may be deployed alongside an HTTP server.

For the purposes of this guide, a single instance of Nginx is used. It runs on the same server, alongside the HTTP server. Based on requirements, a different setup may be chosen.

Because requests are forwarded by reverse proxy, use the [forwarded headers middleware](proxy-load-balancer?view=aspnetcore-10.0) from the [`Microsoft.AspNetCore.HttpOverrides`](https://www.nuget.org/packages/Microsoft.AspNetCore.HttpOverrides) package, which is automatically included in ASP.NET Core apps via the [shared framework's `Microsoft.AspNetCore.App` metapackage](../fundamentals/metapackage-app?view=aspnetcore-10.0). The middleware updates the `Request.Scheme`, using the `X-Forwarded-Proto` header, so that redirect URIs and other security policies work correctly.

Forwarded headers middleware should run before other middleware. This ordering ensures that the middleware relying on forwarded headers information can consume the header values for processing. To run forwarded headers middleware after diagnostics and error handling middleware, see [forwarded headers middleware order](proxy-load-balancer?view=aspnetcore-10.0#forwarded-headers-middleware-order).

Invoke the [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders) method before calling other middleware. Configure the middleware to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers:

```
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication();

var app = builder.Build();

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();

app.MapGet("/", () => "Hello ForwardedHeadersOptions!");

app.Run();
```

If no [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) are specified to the middleware, the default headers to forward are `None`.

Proxies running on loopback addresses (`127.0.0.0/8`, `[::1]`), including the standard localhost address (`127.0.0.1`), are trusted by default. If other trusted proxies or networks within the organization handle requests between the internet and the web server, add them to the list of [KnownProxies](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownproxies) or [KnownNetworks](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownnetworks) with [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions). The following example adds a trusted proxy server at IP address `10.0.0.100` to the forwarded headers middleware `KnownProxies`:

```
using Microsoft.AspNetCore.HttpOverrides;
using System.Net;

var builder = WebApplication.CreateBuilder(args);

// Configure forwarded headers
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});

builder.Services.AddAuthentication();

var app = builder.Build();

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();

app.MapGet("/", () => "10.0.0.100");

app.Run();
```

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0).

### Install Nginx

* [Ubuntu](#tabpanel_2_linux-ubuntu)
* [Red Hat Enterprise Linux](#tabpanel_2_linux-rhel)
* [SUSE Linux Enterprise Server](#tabpanel_2_linux-sles)

Use `apt-get` to install Nginx. The installer creates a [`systemd`](https://systemd.io/) init script that runs Nginx as daemon on system startup. Follow the installation instructions for Ubuntu at [Nginx: Linux packages - Ubuntu](https://nginx.org/en/linux_packages.html#Ubuntu).

Use `yum-utils` to select and install an Nginx module stream. Follow the installation instructions for RHEL at [Nginx: Linux packages - RHEL](https://nginx.org/en/linux_packages.html#RHEL).

Use `yum-utils` to select and install an Nginx module stream. Follow the installation instructions for SLES at [Nginx: Linux packages - SLES](https://nginx.org/en/linux_packages.html#SLES).

Note

If optional Nginx modules are required, building Nginx from source might be required.

Since Nginx was installed for the first time, explicitly start it by running:

```
sudo service nginx start
```

Verify a browser displays the default landing page for Nginx. The landing page is reachable at `http://<server_IP_address>/index.nginx-debian.html`.

### Configure Nginx

* [Ubuntu](#tabpanel_3_linux-ubuntu)
* [Red Hat Enterprise Linux](#tabpanel_3_linux-rhel)
* [SUSE Linux Enterprise Server](#tabpanel_3_linux-sles)

To configure Nginx as a reverse proxy to forward HTTP requests to the ASP.NET Core app, modify `/etc/nginx/sites-available/default` and recreate the symlink. After creating the `/etc/nginx/sites-available/default` file, use the following command to create the symlink:

```
sudo ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
```

Open `/etc/nginx/sites-available/default` in a text editor, and replace the contents with the following snippet:

To configure Nginx as a reverse proxy to forward HTTP requests to an ASP.NET Core app, modify `/etc/nginx.conf`. Update the `http{}` code block with the following snippet:

To configure Nginx as a reverse proxy to forward HTTP requests to an ASP.NET Core app, modify `/etc/nginx.conf`. Update the `http{}` code block with the following snippet:

```
map $http_connection $connection_upgrade {
  "~*Upgrade" $http_connection;
  default keep-alive;
}

server {
  listen        80;
  server_name   example.com *.example.com;
  location / {
      proxy_pass         http://127.0.0.1:5000/;
      proxy_http_version 1.1;
      proxy_set_header   Upgrade $http_upgrade;
      proxy_set_header   Connection $connection_upgrade;
      proxy_set_header   Host $host;
      proxy_cache_bypass $http_upgrade;
      proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header   X-Forwarded-Proto $scheme;
  }
}
```

If the app is a SignalR or Blazor Server app, see [ASP.NET Core SignalR production hosting and scaling](../signalr/scale?view=aspnetcore-10.0#linux-with-nginx) and [Host and deploy ASP.NET Core server-side Blazor apps](../blazor/host-and-deploy/server/?view=aspnetcore-10.0#linux-with-nginx) respectively for more information.

When no `server_name` matches, Nginx uses the default server. If no default server is defined, the first server in the configuration file is the default server. As a best practice, add a specific default server that returns a status code of 444 in your configuration file. A default server configuration example is:

```
server {
    listen   80 default_server;
    # listen [::]:80 default_server deferred;
    return   444;
}
```

With the preceding configuration file and default server, Nginx accepts public traffic on port 80 with host header `example.com` or `*.example.com`. Requests not matching these hosts won't get forwarded to Kestrel. Nginx forwards the matching requests to Kestrel at `http://127.0.0.1:5000/`. For more information, see [How nginx processes a request](https://nginx.org/docs/http/request_processing.html). To change Kestrel's IP/port, see [Kestrel: Endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Warning

Failure to specify a proper [server\_name directive](https://nginx.org/docs/http/server_names.html) exposes your app to security vulnerabilities. Subdomain wildcard binding (for example, `*.example.com`) doesn't pose this security risk if you control the entire parent domain (as opposed to `*.com`, which is vulnerable). For more information, see [RFC 9110: HTTP Semantics (Section 7.2: Host and :authority)](https://www.rfc-editor.org/rfc/rfc9110#field.host).

Once the Nginx configuration is established, run `sudo nginx -t` to verify the syntax of the configuration files. If the configuration file test is successful, force Nginx to pick up the changes by running `sudo nginx -s reload`.

To directly run the app on the server:

1. Navigate to the app's directory.
2. Run the app: `dotnet <app_assembly.dll>`, where `app_assembly.dll` is the assembly file name of the app.

* [Ubuntu](#tabpanel_4_linux-ubuntu)
* [Red Hat Enterprise Linux](#tabpanel_4_linux-rhel)
* [SUSE Linux Enterprise Server](#tabpanel_4_linux-sles)

If the app runs on the server but fails to respond over the internet, check the server's firewall and confirm port 80 is open. If using an Azure Ubuntu VM, add a Network Security Group (NSG) rule that enables inbound port 80 traffic. There's no need to enable an outbound port 80 rule, as the outbound traffic is automatically granted when the inbound rule is enabled.

If the app runs on the server but fails to respond over the internet, check the server's firewall and confirm port 80 is open.

If the app runs on the server but fails to respond over the internet, check the server's firewall and confirm port 80 is open.

When done testing the app, shut down the app with `Ctrl`+`C` in the command shell.

### Increase keepalive\_requests

[`keepalive_requests`](http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_requests) can be increased for [higher performance](https://www.nginx.com/blog/10-tips-for-10x-application-performance/#web-server-tuning), For more information, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/22141#issuecomment-1442435506).

## Monitor the app

The server is set up to forward requests made to `http://<serveraddress>:80` on to the ASP.NET Core app running on Kestrel at `http://127.0.0.1:5000`. However, Nginx isn't set up to manage the Kestrel process. [`systemd`](https://systemd.io/) can be used to create a service file to start and monitor the underlying web app. `systemd` is an init system that provides many powerful features for starting, stopping, and managing processes.

### Create the service file

Create the service definition file:

```
sudo nano /etc/systemd/system/kestrel-helloapp.service
```

The following example is an `.ini` service file for the app:

```
[Unit]
Description=Example .NET Web API App running on Linux

[Service]
WorkingDirectory=/var/www/helloapp
ExecStart=/usr/bin/dotnet /var/www/helloapp/helloapp.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGTERM
SyslogIdentifier=dotnet-example
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_NOLOGO=true

[Install]
WantedBy=multi-user.target
```

In the preceding example, the user that manages the service is specified by the `User` option. The user (`www-data`) must exist and have proper ownership of the app's files.

Setting `KillSignal=SIGTERM`, which is also `systemd`'s default `KillSignal`, allows .NET apps to perform a graceful shutdown and clean up resources properly. Avoid setting `KillSignal=SIGINT`, as .NET lifetime integrations listen for `SIGTERM` to initiate graceful shutdown, and other signals might result in abrupt termination.

Use `TimeoutStopSec` to configure the duration of time to wait for the app to shut down after it receives the initial stop signal. If the app doesn't shut down in this period, `SIGKILL` is issued to terminate the app. Provide the value as unitless seconds (for example, `150`), a time span value (for example, `2min 30s`), or infinity (`infinity`) to disable the timeout. `TimeoutStopSec` defaults to the value of `DefaultTimeoutStopSec` in the manager configuration file (`systemd-system.conf`, `system.conf.d`, `systemd-user.conf`, `user.conf.d`). The default timeout for most distributions is 90 seconds.

```
# The default value is 90 seconds for most distributions.
TimeoutStopSec=90
```

Linux has a case-sensitive file system. Setting `ASPNETCORE_ENVIRONMENT` to `Production` results in searching for the configuration file `appsettings.Production.json`, not `appsettings.production.json`.

Some values (for example, SQL connection strings) must be escaped for the configuration providers to read the environment variables. Use the following command to generate a properly escaped value for use in the configuration file:

```
systemd-escape "<value-to-escape>"
```

Colon (`:`) separators aren't supported in environment variable names. Use a double underscore (`__`) in place of a colon. The [Environment Variables configuration provider](../fundamentals/configuration/?view=aspnetcore-10.0#environment-variables) converts double-underscores into colons when environment variables are read into configuration. In the following example, the connection string key `ConnectionStrings:DefaultConnection` is set into the service definition file as `ConnectionStrings__DefaultConnection`:

```
Environment=ConnectionStrings__DefaultConnection={Connection String}
```

Save the file and enable the service.

```
sudo systemctl enable kestrel-helloapp.service
```

Start the service and verify that it's running.

```
sudo systemctl start kestrel-helloapp.service
sudo systemctl status kestrel-helloapp.service

◝ kestrel-helloapp.service - Example .NET Web API App running on Linux
    Loaded: loaded (/etc/systemd/system/kestrel-helloapp.service; enabled)
    Active: active (running) since Thu 2016-10-18 04:09:35 NZDT; 35s ago
Main PID: 9021 (dotnet)
    CGroup: /system.slice/kestrel-helloapp.service
            └─9021 /usr/local/bin/dotnet /var/www/helloapp/helloapp.dll
```

With the reverse proxy configured and Kestrel managed through `systemd`, the web app is fully configured and can be accessed from a browser on the local machine at `http://localhost`. It's also accessible from a remote machine, barring any firewall that might be blocking. Inspecting the response headers, the `Server` header shows the ASP.NET Core app being served by Kestrel.

```
HTTP/1.1 200 OK
Date: Tue, 11 Oct 2016 16:22:23 GMT
Server: Kestrel
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Transfer-Encoding: chunked
```

### View logs

Since the web app using Kestrel is managed using [`systemd`](https://systemd.io/), all events and processes are logged to a centralized journal. However, this journal includes all entries for all services and processes managed by `systemd`. To view the `kestrel-helloapp.service`-specific items, use the following command:

```
sudo journalctl -fu kestrel-helloapp.service
```

For further filtering, time options such as `--since today`, `--until 1 hour ago`, or a combination of these can reduce the number of entries returned.

```
sudo journalctl -fu kestrel-helloapp.service --since "2016-10-18" --until "2016-10-18 04:00"
```

## Data protection

The [ASP.NET Core Data Protection stack](../security/data-protection/introduction?view=aspnetcore-10.0) is used by several ASP.NET Core [middlewares](../fundamentals/middleware/?view=aspnetcore-10.0), including authentication middleware (for example, cookie middleware) and cross-site request forgery (CSRF) protections. Even if Data Protection APIs aren't called by user code, data protection should be configured to create a persistent cryptographic [key store](../security/data-protection/implementation/key-management?view=aspnetcore-10.0). If data protection isn't configured, the keys are held in memory and discarded when the app restarts.

If the key ring is stored in memory when the app restarts:

* All cookie-based authentication tokens are invalidated.
* Users are required to sign in again on their next request.
* Any data protected with the key ring can no longer be decrypted. This may include [CSRF tokens](../security/anti-request-forgery?view=aspnetcore-10.0#aspnet-core-antiforgery-configuration) and [ASP.NET Core MVC TempData cookies](../fundamentals/app-state?view=aspnetcore-10.0#tempdata).

To configure data protection to persist and encrypt the key ring, see:

* [Key storage providers in ASP.NET Core](../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0)
* [Key encryption at rest in Windows and Azure using ASP.NET Core](../security/data-protection/implementation/key-encryption-at-rest?view=aspnetcore-10.0)

## Long request header fields

Proxy server default settings typically limit request header fields to 4 K or 8 K depending on the platform. An app may require fields longer than the default (for example, apps that use [Microsoft Entra ID](https://azure.microsoft.com/services/active-directory/)). If longer fields are required, the proxy server's default settings require adjustment. The values to apply depend on the scenario. For more information, see your server's documentation.

* [proxy\_buffer\_size](https://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffer_size)
* [proxy\_buffers](https://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffers)
* [proxy\_busy\_buffers\_size](https://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_busy_buffers_size)
* [large\_client\_header\_buffers](https://nginx.org/docs/http/ngx_http_core_module.html#large_client_header_buffers)

Warning

Don't increase the default values of proxy buffers unless necessary. Increasing these values increases the risk of buffer overrun (overflow) and Denial of Service (DoS) attacks by malicious users.

## Secure the app

### Enable AppArmor

Linux Security Modules (LSM) is a framework that's part of the Linux kernel since Linux 2.6. LSM supports different implementations of security modules. [AppArmor](https://apparmor.net/) is an LSM that implements a Mandatory Access Control system, which allows confining the program to a limited set of resources. Ensure AppArmor is enabled and properly configured.

### Configure the firewall

Close off all external ports that aren't in use. Uncomplicated firewall (ufw) provides a front end for `iptables` by providing a CLI for configuring the firewall.

* [Ubuntu](#tabpanel_5_linux-ubuntu)
* [Red Hat Enterprise Linux](#tabpanel_5_linux-rhel)
* [SUSE Linux Enterprise Server](#tabpanel_5_linux-sles)

Warning

A firewall prevents access to the whole system if not configured correctly. Failure to specify the correct SSH port effectively locks you out of the system if you are using SSH to connect to it. The default port is 22. For more information, see the [introduction to ufw](https://help.ubuntu.com/community/UFW) and the [manual](https://manpages.ubuntu.com/manpages/resolute/man8/ufw.8.html).

Install `ufw` and configure it to allow traffic on any ports needed.

```
sudo apt-get install ufw

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

sudo ufw enable
```

Warning

A firewall prevents access to the whole system if not configured correctly. Failure to specify the correct SSH port effectively locks you out of the system if you are using SSH to connect to it. The default port is 22. For more information, see the [introduction to ufw](https://help.ubuntu.com/community/UFW).

Install `ufw` and configure it to allow traffic on any ports needed.

```
sudo yum -y install ufw

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

sudo ufw enable
```

Warning

A firewall prevents access to the whole system if not configured correctly. Failure to specify the correct SSH port effectively locks you out of the system if you are using SSH to connect to it. The default port is 22. For more information, see the [introduction to ufw](https://help.ubuntu.com/community/UFW).

Install `ufw` and configure it to allow traffic on any ports needed.

```
sudo yum -y install ufw

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

sudo ufw enable
```

### Secure Nginx

#### Change the Nginx response name

Edit `src/http/ngx_http_header_filter_module.c`:

```
static char ngx_http_server_string[] = "Server: Web Server" CRLF;
static char ngx_http_server_full_string[] = "Server: Web Server" CRLF;
```

#### Configure options

Configure the server with additional required modules. Consider using a web app firewall, such as [ModSecurity](https://www.modsecurity.org/), to harden the app.

#### HTTPS configuration

**Configure the app for secure (HTTPS) local connections**

The [dotnet run](/en-us/dotnet/core/tools/dotnet-run) command uses the app's `Properties/launchSettings.json` file, which configures the app to listen on the URLs provided by the `applicationUrl` property. For example, `https://localhost:5001;http://localhost:5000`.

Configure the app to use a certificate in development for the `dotnet run` command or development environment (`F5` or `Ctrl`+`F5` in Visual Studio Code) using one of the following approaches:

* [Replace the default certificate from configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#configuration) (*Recommended*)
* [KestrelServerOptions.ConfigureHttpsDefaults](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#configurehttpsdefaultsactionhttpsconnectionadapteroptions)

**Configure the reverse proxy for secure (HTTPS) client connections**

Warning

The security configuration in this section is a general configuration to be used as a starting point for further customization. We're unable to provide support for third-party tooling, servers, and operating systems. *Use the configuration in this section at your own risk.* For more information, access the following resources:

* [Configuring HTTPS servers](http://nginx.org/docs/http/configuring_https_servers.html) (Nginx documentation)
* [mozilla.org SSL Configuration Generator](https://ssl-config.mozilla.org/#server=nginx)

- Configure the server to listen to HTTPS traffic on port 443 by specifying a valid certificate issued by a trusted Certificate Authority (CA).

- Harden the security by employing some of the practices depicted in the following */etc/nginx/nginx.conf* file.

- The following example doesn't configure the server to redirect insecure requests. We recommend using HTTPS redirection middleware. For more information, see [Enforce HTTPS in ASP.NET Core](../security/enforcing-ssl?view=aspnetcore-10.0).

  Note

  For development environments where the server configuration handles secure redirection instead of HTTPS redirection middleware, we recommend using temporary redirects (302) rather than permanent redirects (301). Link caching can cause unstable behavior in development environments.

- Adding a `Strict-Transport-Security` (HSTS) header ensures all subsequent requests made by the client are over HTTPS. For guidance on setting the `Strict-Transport-Security` header, see [Enforce HTTPS in ASP.NET Core](../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol).

- If HTTPS will be disabled in the future, use one of the following approaches:

  * Don't add the HSTS header.
  * Choose a short `max-age` value.

Add the */etc/nginx/proxy.conf* configuration file:

```
proxy_redirect          off;
proxy_set_header        Host $host;
proxy_set_header        X-Real-IP $remote_addr;
proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header        X-Forwarded-Proto $scheme;
client_max_body_size    10m;
client_body_buffer_size 128k;
proxy_connect_timeout   90;
proxy_send_timeout      90;
proxy_read_timeout      90;
proxy_buffers           32 4k;
```

* [Ubuntu](#tabpanel_6_linux-ubuntu)
* [Red Hat Enterprise Linux](#tabpanel_6_linux-rhel)
* [SUSE Linux Enterprise Server](#tabpanel_6_linux-sles)

**Replace** the contents of the */etc/nginx/nginx.conf* configuration file with the following file. The example contains both `http` and `server` sections in one configuration file.

Modify `/etc/nginx/nginx.conf`. Open it in a text editor, and replace the `http{}` and `server{}` code blocks with contents with the following snippet:

Modify `/etc/nginx/nginx.conf`. Open it in a text editor, and replace the `http{}` and `server{}` code blocks with contents with the following snippet:

```
http {
    include        /etc/nginx/proxy.conf;
    limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
    server_tokens  off;

    sendfile on;
    # Adjust keepalive_timeout to the lowest possible value that makes sense 
    # for your use case.
    keepalive_timeout   29;
    client_body_timeout 10; client_header_timeout 10; send_timeout 10;

    upstream helloapp{
        server 127.0.0.1:5000;
    }

    server {
        listen                    443 ssl http2;
        listen                    [::]:443 ssl http2;
        server_name               example.com *.example.com;
        ssl_certificate           /etc/ssl/certs/testCert.crt;
        ssl_certificate_key       /etc/ssl/certs/testCert.key;
        ssl_session_timeout       1d;
        ssl_protocols             TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers off;
        ssl_ciphers               ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
        ssl_session_cache         shared:SSL:10m;
        ssl_session_tickets       off;
        ssl_stapling              off;

        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;

        #Redirects all traffic
        location / {
            proxy_pass http://helloapp;
            limit_req  zone=one burst=10 nodelay;
        }
    }
}
```

Note

Blazor WebAssembly apps require a larger `burst` parameter value to accommodate the larger number of requests made by an app. For more information, see [Host and deploy ASP.NET Core Blazor WebAssembly with Nginx](../blazor/host-and-deploy/webassembly/nginx?view=aspnetcore-10.0).

Note

The preceding example disables Online Certificate Status Protocol (OCSP) Stapling. If enabled, confirm that the certificate supports the feature. For more information and guidance on enabling OCSP, see the following properties in the [Module ngx\_http\_ssl\_module (Nginx documentation)](http://nginx.org/en/docs/http/ngx_http_ssl_module.html) article:

* `ssl_stapling`
* `ssl_stapling_file`
* `ssl_stapling_responder`
* `ssl_stapling_verify`

#### Secure Nginx from clickjacking

[Clickjacking](https://blog.qualys.com/securitylabs/2015/10/20/clickjacking-a-common-implementation-mistake-that-can-put-your-websites-in-danger), also known as a *UI redress attack*, is a malicious attack where a website visitor is tricked into clicking a link or button on a different page than they're currently visiting. Use `X-FRAME-OPTIONS` to secure the site.

To mitigate clickjacking attacks:

1. Edit the *nginx.conf* file:

   ```
   sudo nano /etc/nginx/nginx.conf
   ```

   Within the `http{}` code block, add the line: `add_header X-Frame-Options "SAMEORIGIN";`

2. Save the file.

3. Restart Nginx.

#### MIME-type sniffing

This header prevents most browsers from MIME-sniffing a response away from the declared content type, as the header instructs the browser not to override the response content type. With the `nosniff` option, if the server says the content is `text/html`, the browser renders it as `text/html`.

1. Edit the *nginx.conf* file:

   ```
   sudo nano /etc/nginx/nginx.conf
   ```

   Within the `http{}` code block, add the line: `add_header X-Content-Type-Options "nosniff";`

2. Save the file.

3. Restart Nginx.

## Additional Nginx suggestions

After upgrading the shared framework on the server, restart the ASP.NET Core apps hosted by the server.

## Related content

* [Prerequisites for .NET on Linux](/en-us/dotnet/core/linux-prerequisites)
* [Nginx: Linux packages - Ubuntu](https://nginx.org/en/linux_packages.html#Ubuntu)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)
* [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0)
* [NGINX: Using the Forwarded header](https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/)

This guide explains setting up a production-ready ASP.NET Core environment on an Ubuntu 16.04 server. These instructions likely work with newer versions of Ubuntu, but the instructions haven't been tested with newer versions.

For information on other Linux distributions supported by ASP.NET Core, see [Prerequisites for .NET Core on Linux](/en-us/dotnet/core/linux-prerequisites).

Note

For Ubuntu 14.04, `supervisord` is recommended as a solution for monitoring the Kestrel process. `systemd` isn't available on Ubuntu 14.04. For Ubuntu 14.04 instructions, see the [previous version of this topic](https://github.com/dotnet/AspNetCore.Docs/blob/e9c1419175c4dd7e152df3746ba1df5935aaafd5/aspnetcore/publishing/linuxproduction.md).

This guide:

* Places an existing ASP.NET Core app behind a reverse proxy server.
* Sets up the reverse proxy server to forward requests to the Kestrel web server.
* Ensures the web app runs on startup as a daemon.
* Configures a process management tool to help restart the web app.

## Prerequisites

* Access to an Ubuntu 16.04 server with a standard user account with sudo privilege.
* The latest non-preview [.NET runtime installed](/en-us/dotnet/core/install/linux) on the server.
* An existing ASP.NET Core app.

At any point in the future after upgrading the shared framework, restart the ASP.NET Core apps hosted by the server.

## Publish and copy over the app

Configure the app for a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd).

If the app is run locally in the [`Development` environment](../fundamentals/environments?view=aspnetcore-10.0#configure-services-and-middleware-by-environment) and isn't configured by the server to make secure HTTPS connections, adopt either of the following approaches:

* Configure the app to handle secure local connections. For more information, see the [HTTPS configuration](#https-configuration) section.

* Configure the app to run at the insecure endpoint:

  * Deactivate HTTPS redirection middleware in the `Development` environment (`Program.cs`):

    ```
    if (!app.Environment.IsDevelopment())
    {
        app.UseHttpsRedirection();
    }
    ```

    For more information, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0#configure-services-and-middleware-by-environment).

  * Remove `https://localhost:5001` (if present) from the `applicationUrl` property in the `Properties/launchSettings.json` file.

For more information on configuration by environment, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0).

Run [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) from the `Development` environment to package an app into a directory (for example, `bin/Release/{TARGET FRAMEWORK MONIKER}/publish`, where the placeholder `{TARGET FRAMEWORK MONIKER}` is the Target Framework Moniker/TFM) that can run on the server:

```
dotnet publish --configuration Release
```

The app can also be published as a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) if you prefer not to maintain the .NET Core runtime on the server.

Copy the ASP.NET Core app to the server using a tool that integrates into the organization's workflow (for example, `SCP`, `SFTP`). It's common to locate web apps under the `var` directory (for example, `var/www/helloapp`).

Note

Under a production deployment scenario, a continuous integration workflow does the work of publishing the app and copying the assets to the server.

Test the app:

1. From the command line, run the app: `dotnet <app_assembly>.dll`.
2. In a browser, navigate to `http://<serveraddress>:<port>` to verify the app works on Linux locally.

## Configure a reverse proxy server

A reverse proxy is a common setup for serving dynamic web apps. A reverse proxy terminates the HTTP request and forwards it to the ASP.NET Core app.

### Use a reverse proxy server

Kestrel is great for serving dynamic content from ASP.NET Core. However, the web serving capabilities aren't as feature rich as servers such as IIS, Apache, or Nginx. A reverse proxy server can offload work such as serving static content, caching requests, compressing requests, and HTTPS termination from the HTTP server. A reverse proxy server may reside on a dedicated machine or may be deployed alongside an HTTP server.

For the purposes of this guide, a single instance of Nginx is used. It runs on the same server, alongside the HTTP server. Based on requirements, a different setup may be chosen.

Because requests are forwarded by reverse proxy, use the [forwarded headers middleware](proxy-load-balancer?view=aspnetcore-10.0) from the [`Microsoft.AspNetCore.HttpOverrides`](https://www.nuget.org/packages/Microsoft.AspNetCore.HttpOverrides) package. The middleware updates the `Request.Scheme`, using the `X-Forwarded-Proto` header, so that redirect URIs and other security policies work correctly.

Forwarded headers middleware should run before other middleware. This ordering ensures that the middleware relying on forwarded headers information can consume the header values for processing. To run forwarded headers middleware after diagnostics and error handling middleware, see [forwarded headers middleware order](proxy-load-balancer?view=aspnetcore-10.0#forwarded-headers-middleware-order).

Invoke the [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders) method at the top of `Program.cs` before calling other middleware. Configure the middleware to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers:

```
// requires using Microsoft.AspNetCore.HttpOverrides;
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();
```

If no [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) are specified to the middleware, the default headers to forward are `None`.

Proxies running on loopback addresses (`127.0.0.0/8`, `[::1]`), including the standard localhost address (`127.0.0.1`), are trusted by default. If other trusted proxies or networks within the organization handle requests between the Internet and the web server, add them to the list of [KnownProxies](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownproxies) or [KnownNetworks](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownnetworks) with [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions). The following example adds a trusted proxy server at IP address 10.0.0.100 to the forwarded headers middleware `KnownProxies` in `Program.cs`:

```
using System.Net;

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});
```

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0).

### Install Nginx

Use `apt-get` to install Nginx. The installer creates a `systemd` init script that runs Nginx as daemon on system startup. Follow the installation instructions for Ubuntu at [Nginx: Official Debian/Ubuntu packages](https://www.nginx.com/resources/wiki/start/topics/tutorials/install/#official-debian-ubuntu-packages).

Note

If optional Nginx modules are required, building Nginx from source might be required.

Since Nginx was installed for the first time, explicitly start it by running:

```
sudo service nginx start
```

Verify a browser displays the default landing page for Nginx. The landing page is reachable at `http://<server_IP_address>/index.nginx-debian.html`.

### Configure Nginx

To configure Nginx as a reverse proxy to forward HTTP requests to your ASP.NET Core app, modify `/etc/nginx/sites-available/default`. Open it in a text editor, and replace the contents with the following snippet:

```
server {
    listen        80;
    server_name   example.com *.example.com;
    location / {
        proxy_pass         http://127.0.0.1:5000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection keep-alive;
        proxy_set_header   Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}
```

If the app is a SignalR or Blazor Server app, see [ASP.NET Core SignalR production hosting and scaling](../signalr/scale?view=aspnetcore-10.0#linux-with-nginx) and [Host and deploy ASP.NET Core server-side Blazor apps](../blazor/host-and-deploy/server/?view=aspnetcore-10.0#linux-with-nginx) respectively for more information.

When no `server_name` matches, Nginx uses the default server. If no default server is defined, the first server in the configuration file is the default server. As a best practice, add a specific default server that returns a status code of 444 in your configuration file. A default server configuration example is:

```
server {
    listen   80 default_server;
    # listen [::]:80 default_server deferred;
    return   444;
}
```

With the preceding configuration file and default server, Nginx accepts public traffic on port 80 with host header `example.com` or `*.example.com`. Requests not matching these hosts won't get forwarded to Kestrel. Nginx forwards the matching requests to Kestrel at `http://127.0.0.1:5000`. For more information, see [How nginx processes a request](https://nginx.org/docs/http/request_processing.html). To change Kestrel's IP/port, see [Kestrel: Endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Warning

Failure to specify a proper [server\_name directive](https://nginx.org/docs/http/server_names.html) exposes your app to security vulnerabilities. Subdomain wildcard binding (for example, `*.example.com`) doesn't pose this security risk if you control the entire parent domain (as opposed to `*.com`, which is vulnerable). For more information, see [RFC 9110: HTTP Semantics (Section 7.2: Host and :authority)](https://www.rfc-editor.org/rfc/rfc9110#field.host).

Once the Nginx configuration is established, run `sudo nginx -t` to verify the syntax of the configuration files. If the configuration file test is successful, force Nginx to pick up the changes by running `sudo nginx -s reload`.

To directly run the app on the server:

1. Navigate to the app's directory.
2. Run the app: `dotnet <app_assembly.dll>`, where `app_assembly.dll` is the assembly file name of the app.

If the app runs on the server but fails to respond over the Internet, check the server's firewall and confirm port 80 is open. If using an Azure Ubuntu VM, add a Network Security Group (NSG) rule that enables inbound port 80 traffic. There's no need to enable an outbound port 80 rule, as the outbound traffic is automatically granted when the inbound rule is enabled.

When done testing the app, shut down the app with `Ctrl`+`C` in the command shell.

## Monitor the app

The server is set up to forward requests made to `http://<serveraddress>:80` on to the ASP.NET Core app running on Kestrel at `http://127.0.0.1:5000`. However, Nginx isn't set up to manage the Kestrel process. `systemd` can be used to create a service file to start and monitor the underlying web app. `systemd` is an init system that provides many powerful features for starting, stopping, and managing processes.

### Create the service file

Create the service definition file:

```
sudo nano /etc/systemd/system/kestrel-helloapp.service
```

The following example is a service file for the app:

```
[Unit]
Description=Example .NET Web API App running on Ubuntu

[Service]
WorkingDirectory=/var/www/helloapp
ExecStart=/usr/bin/dotnet /var/www/helloapp/helloapp.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_NOLOGO=true

[Install]
WantedBy=multi-user.target
```

In the preceding example, the user that manages the service is specified by the `User` option. The user (`www-data`) must exist and have proper ownership of the app's files.

Use `TimeoutStopSec` to configure the duration of time to wait for the app to shut down after it receives the initial interrupt signal. If the app doesn't shut down in this period, SIGKILL is issued to terminate the app. Provide the value as unitless seconds (for example, `150`), a time span value (for example, `2min 30s`), or `infinity` to disable the timeout. `TimeoutStopSec` defaults to the value of `DefaultTimeoutStopSec` in the manager configuration file (`systemd-system.conf`, `system.conf.d`, `systemd-user.conf`, `user.conf.d`). The default timeout for most distributions is 90 seconds.

```
# The default value is 90 seconds for most distributions.
TimeoutStopSec=90
```

Linux has a case-sensitive file system. Setting `ASPNETCORE_ENVIRONMENT` to `Production` results in searching for the configuration file `appsettings.Production.json`, not `appsettings.production.json`.

Some values (for example, SQL connection strings) must be escaped for the configuration providers to read the environment variables. Use the following command to generate a properly escaped value for use in the configuration file:

```
systemd-escape "<value-to-escape>"
```

Colon (`:`) separators aren't supported in environment variable names. Use a double underscore (`__`) in place of a colon. The [Environment Variables configuration provider](../fundamentals/configuration/?view=aspnetcore-10.0#environment-variables) converts double-underscores into colons when environment variables are read into configuration. In the following example, the connection string key `ConnectionStrings:DefaultConnection` is set into the service definition file as `ConnectionStrings__DefaultConnection`:

```
Environment=ConnectionStrings__DefaultConnection={Connection String}
```

Save the file and enable the service.

```
sudo systemctl enable kestrel-helloapp.service
```

Start the service and verify that it's running.

```
sudo systemctl start kestrel-helloapp.service
sudo systemctl status kestrel-helloapp.service

◝ kestrel-helloapp.service - Example .NET Web API App running on Ubuntu
    Loaded: loaded (/etc/systemd/system/kestrel-helloapp.service; enabled)
    Active: active (running) since Thu 2016-10-18 04:09:35 NZDT; 35s ago
Main PID: 9021 (dotnet)
    CGroup: /system.slice/kestrel-helloapp.service
            └─9021 /usr/local/bin/dotnet /var/www/helloapp/helloapp.dll
```

With the reverse proxy configured and Kestrel managed through `systemd`, the web app is fully configured and can be accessed from a browser on the local machine at `http://localhost`. It's also accessible from a remote machine, barring any firewall that might be blocking. Inspecting the response headers, the `Server` header shows the ASP.NET Core app being served by Kestrel.

```
HTTP/1.1 200 OK
Date: Tue, 11 Oct 2016 16:22:23 GMT
Server: Kestrel
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Transfer-Encoding: chunked
```

### View logs

Since the web app using Kestrel is managed using `systemd`, all events and processes are logged to a centralized journal. However, this journal includes all entries for all services and processes managed by `systemd`. To view the `kestrel-helloapp.service`-specific items, use the following command:

```
sudo journalctl -fu kestrel-helloapp.service
```

For further filtering, time options such as `--since today`, `--until 1 hour ago`, or a combination of these can reduce the number of entries returned.

```
sudo journalctl -fu kestrel-helloapp.service --since "2016-10-18" --until "2016-10-18 04:00"
```

## Data protection

The [ASP.NET Core Data Protection stack](../security/data-protection/introduction?view=aspnetcore-10.0) is used by several ASP.NET Core [middlewares](../fundamentals/middleware/?view=aspnetcore-10.0), including authentication middleware (for example, cookie middleware) and cross-site request forgery (CSRF) protections. Even if Data Protection APIs aren't called by user code, data protection should be configured to create a persistent cryptographic [key store](../security/data-protection/implementation/key-management?view=aspnetcore-10.0). If data protection isn't configured, the keys are held in memory and discarded when the app restarts.

If the key ring is stored in memory when the app restarts:

* All cookie-based authentication tokens are invalidated.
* Users are required to sign in again on their next request.
* Any data protected with the key ring can no longer be decrypted. This may include [CSRF tokens](../security/anti-request-forgery?view=aspnetcore-10.0#aspnet-core-antiforgery-configuration) and [ASP.NET Core MVC TempData cookies](../fundamentals/app-state?view=aspnetcore-10.0#tempdata).

To configure data protection to persist and encrypt the key ring, see:

* [Key storage providers in ASP.NET Core](../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0)
* [Key encryption at rest in Windows and Azure using ASP.NET Core](../security/data-protection/implementation/key-encryption-at-rest?view=aspnetcore-10.0)

## Long request header fields

Proxy server default settings typically limit request header fields to 4 K or 8 K depending on the platform. An app may require fields longer than the default (for example, apps that use [Azure Active Directory](https://azure.microsoft.com/services/active-directory/)). If longer fields are required, the proxy server's default settings require adjustment. The values to apply depend on the scenario. For more information, see your server's documentation.

* [proxy\_buffer\_size](https://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffer_size)
* [proxy\_buffers](https://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffers)
* [proxy\_busy\_buffers\_size](https://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_busy_buffers_size)
* [large\_client\_header\_buffers](https://nginx.org/docs/http/ngx_http_core_module.html#large_client_header_buffers)

Warning

Don't increase the default values of proxy buffers unless necessary. Increasing these values increases the risk of buffer overrun (overflow) and Denial of Service (DoS) attacks by malicious users.

## Secure the app

### Enable AppArmor

Linux Security Modules (LSM) is a framework that's part of the Linux kernel since Linux 2.6. LSM supports different implementations of security modules. [AppArmor](https://apparmor.net/) is an LSM that implements a Mandatory Access Control system, which allows confining the program to a limited set of resources. Ensure AppArmor is enabled and properly configured.

### Configure the firewall

Close off all external ports that aren't in use. Uncomplicated firewall (ufw) provides a front end for `iptables` by providing a CLI for configuring the firewall.

Warning

A firewall will prevent access to the whole system if not configured correctly. Failure to specify the correct SSH port will effectively lock you out of the system if you are using SSH to connect to it. The default port is 22. For more information, see the [introduction to ufw](https://help.ubuntu.com/community/UFW) and the [manual](https://manpages.ubuntu.com/manpages/resolute/man8/ufw.8.html).

Install `ufw` and configure it to allow traffic on any ports needed.

```
sudo apt-get install ufw

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

sudo ufw enable
```

### Secure Nginx

#### Change the Nginx response name

Edit `src/http/ngx_http_header_filter_module.c`:

```
static char ngx_http_server_string[] = "Server: Web Server" CRLF;
static char ngx_http_server_full_string[] = "Server: Web Server" CRLF;
```

#### Configure options

Configure the server with additional required modules. Consider using a web app firewall, such as [ModSecurity](https://www.modsecurity.org/), to harden the app.

#### HTTPS configuration

**Configure the app for secure (HTTPS) local connections**

The [dotnet run](/en-us/dotnet/core/tools/dotnet-run) command uses the app's `Properties/launchSettings.json` file, which configures the app to listen on the URLs provided by the `applicationUrl` property. For example, `https://localhost:5001;http://localhost:5000`.

Configure the app to use a certificate in development for the `dotnet run` command or development environment (`F5` or `Ctrl`+`F5` in Visual Studio Code) using one of the following approaches:

* [Replace the default certificate from configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#configuration) (*Recommended*)
* [KestrelServerOptions.ConfigureHttpsDefaults](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#configurehttpsdefaultsactionhttpsconnectionadapteroptions)

**Configure the reverse proxy for secure (HTTPS) client connections**

Warning

The security configuration in this section is a general configuration to be used as a starting point for further customization. We're unable to provide support for third-party tooling, servers, and operating systems. *Use the configuration in this section at your own risk.* For more information, access the following resources:

* [Configuring HTTPS servers](http://nginx.org/docs/http/configuring_https_servers.html) (Nginx documentation)
* [mozilla.org SSL Configuration Generator](https://ssl-config.mozilla.org/#server=nginx)

- Configure the server to listen to HTTPS traffic on port 443 by specifying a valid certificate issued by a trusted Certificate Authority (CA).

- Harden the security by employing some of the practices depicted in the following */etc/nginx/nginx.conf* file.

- The following example doesn't configure the server to redirect insecure requests. We recommend using HTTPS redirection middleware. For more information, see [Enforce HTTPS in ASP.NET Core](../security/enforcing-ssl?view=aspnetcore-10.0).

  Note

  For development environments where the server configuration handles secure redirection instead of HTTPS redirection middleware, we recommend using temporary redirects (302) rather than permanent redirects (301). Link caching can cause unstable behavior in development environments.

- Adding a `Strict-Transport-Security` (HSTS) header ensures all subsequent requests made by the client are over HTTPS. For guidance on setting the `Strict-Transport-Security` header, see [Enforce HTTPS in ASP.NET Core](../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol).

- If HTTPS will be disabled in the future, use one of the following approaches:

  * Don't add the HSTS header.
  * Choose a short `max-age` value.

Add the */etc/nginx/proxy.conf* configuration file:

```
proxy_redirect          off;
proxy_set_header        Host $host;
proxy_set_header        X-Real-IP $remote_addr;
proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header        X-Forwarded-Proto $scheme;
client_max_body_size    10m;
client_body_buffer_size 128k;
proxy_connect_timeout   90;
proxy_send_timeout      90;
proxy_read_timeout      90;
proxy_buffers           32 4k;
```

**Replace** the contents of the */etc/nginx/nginx.conf* configuration file with the following file. The example contains both `http` and `server` sections in one configuration file.

```
http {
    include        /etc/nginx/proxy.conf;
    limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
    server_tokens  off;

    sendfile on;
    # Adjust keepalive_timeout to the lowest possible value that makes sense 
    # for your use case.
    keepalive_timeout   29;
    client_body_timeout 10; client_header_timeout 10; send_timeout 10;

    upstream helloapp{
        server 127.0.0.1:5000;
    }

    server {
        listen                    443 ssl http2;
        listen                    [::]:443 ssl http2;
        server_name               example.com *.example.com;
        ssl_certificate           /etc/ssl/certs/testCert.crt;
        ssl_certificate_key       /etc/ssl/certs/testCert.key;
        ssl_session_timeout       1d;
        ssl_protocols             TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers off;
        ssl_ciphers               ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
        ssl_session_cache         shared:SSL:10m;
        ssl_session_tickets       off;
        ssl_stapling              off;

        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;

        #Redirects all traffic
        location / {
            proxy_pass http://helloapp;
            limit_req  zone=one burst=10 nodelay;
        }
    }
}
```

Note

Blazor WebAssembly apps require a larger `burst` parameter value to accommodate the larger number of requests made by an app. For more information, see [Host and deploy ASP.NET Core Blazor WebAssembly with Nginx](../blazor/host-and-deploy/webassembly/nginx?view=aspnetcore-10.0).

Note

The preceding example disables Online Certificate Status Protocol (OCSP) Stapling. If enabled, confirm that the certificate supports the feature. For more information and guidance on enabling OCSP, see the following properties in the [Module ngx\_http\_ssl\_module (Nginx documentation)](http://nginx.org/en/docs/http/ngx_http_ssl_module.html) article:

* `ssl_stapling`
* `ssl_stapling_file`
* `ssl_stapling_responder`
* `ssl_stapling_verify`

#### Secure Nginx from clickjacking

[Clickjacking](https://blog.qualys.com/securitylabs/2015/10/20/clickjacking-a-common-implementation-mistake-that-can-put-your-websites-in-danger), also known as a *UI redress attack*, is a malicious attack where a website visitor is tricked into clicking a link or button on a different page than they're currently visiting. Use `X-FRAME-OPTIONS` to secure the site.

To mitigate clickjacking attacks:

1. Edit the *nginx.conf* file:

   ```
   sudo nano /etc/nginx/nginx.conf
   ```

   Add the line: `add_header X-Frame-Options "SAMEORIGIN";`

2. Save the file.

3. Restart Nginx.

#### MIME-type sniffing

This header prevents most browsers from MIME-sniffing a response away from the declared content type, as the header instructs the browser not to override the response content type. With the `nosniff` option, if the server says the content is `text/html`, the browser renders it as `text/html`.

1. Edit the *nginx.conf* file:

   ```
   sudo nano /etc/nginx/nginx.conf
   ```

   Add the line: `add_header X-Content-Type-Options "nosniff";`

2. Save the file.

3. Restart Nginx.

## Additional Nginx suggestions

After upgrading the shared framework on the server, restart the ASP.NET Core apps hosted by the server.

## Additional resources

* [Prerequisites for .NET Core on Linux](/en-us/dotnet/core/linux-prerequisites)
* [Nginx: Binary Releases: Official Debian/Ubuntu packages](https://www.nginx.com/resources/wiki/start/topics/tutorials/install/#official-debian-ubuntu-packages)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)
* [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0)
* [NGINX: Using the Forwarded header](https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/)

This guide explains setting up a production-ready ASP.NET Core environment on an Ubuntu 16.04 server. These instructions likely work with newer versions of Ubuntu, but the instructions haven't been tested with newer versions.

For information on other Linux distributions supported by ASP.NET Core, see [Prerequisites for .NET Core on Linux](/en-us/dotnet/core/linux-prerequisites).

Note

For Ubuntu 14.04, `supervisord` is recommended as a solution for monitoring the Kestrel process. `systemd` isn't available on Ubuntu 14.04. For Ubuntu 14.04 instructions, see the [previous version of this topic](https://github.com/dotnet/AspNetCore.Docs/blob/e9c1419175c4dd7e152df3746ba1df5935aaafd5/aspnetcore/publishing/linuxproduction.md).

This guide:

* Places an existing ASP.NET Core app behind a reverse proxy server.
* Sets up the reverse proxy server to forward requests to the Kestrel web server.
* Ensures the web app runs on startup as a daemon.
* Configures a process management tool to help restart the web app.

## Prerequisites

* Access to an Ubuntu 16.04 server with a standard user account with sudo privilege.
* The latest non-preview [.NET runtime installed](/en-us/dotnet/core/install/linux) on the server.
* An existing ASP.NET Core app.

At any point in the future after upgrading the shared framework, restart the ASP.NET Core apps hosted by the server.

## Publish and copy over the app

Configure the app for a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd).

If the app is run locally in the [`Development` environment](../fundamentals/environments?view=aspnetcore-10.0#configure-services-and-middleware-by-environment) and isn't configured by the server to make secure HTTPS connections, adopt either of the following approaches:

* Configure the app to handle secure local connections. For more information, see the [HTTPS configuration](#https-configuration) section.

* Configure the app to run at the insecure endpoint:

  * Deactivate HTTPS redirection middleware in the `Development` environment (`Program.cs`):

    ```
    if (!app.Environment.IsDevelopment())
    {
        app.UseHttpsRedirection();
    }
    ```

    For more information, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0#configure-services-and-middleware-by-environment).

  * Remove `https://localhost:5001` (if present) from the `applicationUrl` property in the `Properties/launchSettings.json` file.

For more information on configuration by environment, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0).

Run [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) from the `Development` environment to package an app into a directory (for example, `bin/Release/{TARGET FRAMEWORK MONIKER}/publish`, where the placeholder `{TARGET FRAMEWORK MONIKER}` is the Target Framework Moniker/TFM) that can run on the server:

```
dotnet publish --configuration Release
```

The app can also be published as a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) if you prefer not to maintain the .NET Core runtime on the server.

Copy the ASP.NET Core app to the server using a tool that integrates into the organization's workflow (for example, `SCP`, `SFTP`). It's common to locate web apps under the `var` directory (for example, `var/www/helloapp`).

Note

Under a production deployment scenario, a continuous integration workflow does the work of publishing the app and copying the assets to the server.

Test the app:

1. From the command line, run the app: `dotnet <app_assembly>.dll`.
2. In a browser, navigate to `http://<serveraddress>:<port>` to verify the app works on Linux locally.

## Configure a reverse proxy server

A reverse proxy is a common setup for serving dynamic web apps. A reverse proxy terminates the HTTP request and forwards it to the ASP.NET Core app.

### Use a reverse proxy server

Kestrel is great for serving dynamic content from ASP.NET Core. However, the web serving capabilities aren't as feature rich as servers such as IIS, Apache, or Nginx. A reverse proxy server can offload work such as serving static content, caching requests, compressing requests, and HTTPS termination from the HTTP server. A reverse proxy server may reside on a dedicated machine or may be deployed alongside an HTTP server.

For the purposes of this guide, a single instance of Nginx is used. It runs on the same server, alongside the HTTP server. Based on requirements, a different setup may be chosen.

Because requests are forwarded by reverse proxy, use the [forwarded headers middleware](proxy-load-balancer?view=aspnetcore-10.0) from the [`Microsoft.AspNetCore.HttpOverrides`](https://www.nuget.org/packages/Microsoft.AspNetCore.HttpOverrides) package. The middleware updates the `Request.Scheme`, using the `X-Forwarded-Proto` header, so that redirect URIs and other security policies work correctly.

Forwarded headers middleware should run before other middleware. This ordering ensures that the middleware relying on forwarded headers information can consume the header values for processing. To run forwarded headers middleware after diagnostics and error handling middleware, see [forwarded headers middleware order](proxy-load-balancer?view=aspnetcore-10.0#forwarded-headers-middleware-order).

Invoke the [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders) method at the top of the request processing pipeline before calling other middleware. Configure the middleware to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers:

```
using Microsoft.AspNetCore.HttpOverrides;

...

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();
```

If no [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) are specified to the middleware, the default headers to forward are `None`.

Proxies running on loopback addresses (`127.0.0.0/8`, `[::1]`), including the standard localhost address (`127.0.0.1`), are trusted by default. If other trusted proxies or networks within the organization handle requests between the Internet and the web server, add them to the list of [KnownProxies](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownproxies) or [KnownNetworks](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownnetworks) with [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions). The following example adds a trusted proxy server at IP address 10.0.0.100 to the forwarded headers middleware `KnownProxies` in `Startup.ConfigureServices`:

```
using System.Net;

...

services.Configure<ForwardedHeadersOptions>(options =>
{
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});
```

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0).

### Install Nginx

Use `apt-get` to install Nginx. The installer creates a `systemd` init script that runs Nginx as daemon on system startup. Follow the installation instructions for Ubuntu at [Nginx: Official Debian/Ubuntu packages](https://www.nginx.com/resources/wiki/start/topics/tutorials/install/#official-debian-ubuntu-packages).

Note

If optional Nginx modules are required, building Nginx from source might be required.

Since Nginx was installed for the first time, explicitly start it by running:

```
sudo service nginx start
```

Verify a browser displays the default landing page for Nginx. The landing page is reachable at `http://<server_IP_address>/index.nginx-debian.html`.

### Configure Nginx

To configure Nginx as a reverse proxy to forward HTTP requests to your ASP.NET Core app, modify `/etc/nginx/sites-available/default`. Open it in a text editor, and replace the contents with the following snippet:

```
server {
    listen        80;
    server_name   example.com *.example.com;
    location / {
        proxy_pass         http://127.0.0.1:5000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection keep-alive;
        proxy_set_header   Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}
```

If the app is a SignalR or Blazor Server app, see [ASP.NET Core SignalR production hosting and scaling](../signalr/scale?view=aspnetcore-10.0#linux-with-nginx) and [Host and deploy ASP.NET Core server-side Blazor apps](../blazor/host-and-deploy/server/?view=aspnetcore-10.0#linux-with-nginx) respectively for more information.

When no `server_name` matches, Nginx uses the default server. If no default server is defined, the first server in the configuration file is the default server. As a best practice, add a specific default server that returns a status code of 444 in your configuration file. A default server configuration example is:

```
server {
    listen   80 default_server;
    # listen [::]:80 default_server deferred;
    return   444;
}
```

With the preceding configuration file and default server, Nginx accepts public traffic on port 80 with host header `example.com` or `*.example.com`. Requests not matching these hosts won't get forwarded to Kestrel. Nginx forwards the matching requests to Kestrel at `http://127.0.0.1:5000`. For more information, see [How nginx processes a request](https://nginx.org/docs/http/request_processing.html). To change Kestrel's IP/port, see [Kestrel: Endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration).

Warning

Failure to specify a proper [server\_name directive](https://nginx.org/docs/http/server_names.html) exposes your app to security vulnerabilities. Subdomain wildcard binding (for example, `*.example.com`) doesn't pose this security risk if you control the entire parent domain (as opposed to `*.com`, which is vulnerable). For more information, see [RFC 9110: HTTP Semantics (Section 7.2: Host and :authority)](https://www.rfc-editor.org/rfc/rfc9110#field.host).

Once the Nginx configuration is established, run `sudo nginx -t` to verify the syntax of the configuration files. If the configuration file test is successful, force Nginx to pick up the changes by running `sudo nginx -s reload`.

To directly run the app on the server:

1. Navigate to the app's directory.
2. Run the app: `dotnet <app_assembly.dll>`, where `app_assembly.dll` is the assembly file name of the app.

If the app runs on the server but fails to respond over the Internet, check the server's firewall and confirm port 80 is open. If using an Azure Ubuntu VM, add a Network Security Group (NSG) rule that enables inbound port 80 traffic. There's no need to enable an outbound port 80 rule, as the outbound traffic is automatically granted when the inbound rule is enabled.

When done testing the app, shut down the app with `Ctrl`+`C` in the command shell.

## Monitor the app

The server is set up to forward requests made to `http://<serveraddress>:80` on to the ASP.NET Core app running on Kestrel at `http://127.0.0.1:5000`. However, Nginx isn't set up to manage the Kestrel process. `systemd` can be used to create a service file to start and monitor the underlying web app. `systemd` is an init system that provides many powerful features for starting, stopping, and managing processes.

### Create the service file

Create the service definition file:

```
sudo nano /etc/systemd/system/kestrel-helloapp.service
```

The following example is a service file for the app:

```
[Unit]
Description=Example .NET Web API App running on Ubuntu

[Service]
WorkingDirectory=/var/www/helloapp
ExecStart=/usr/bin/dotnet /var/www/helloapp/helloapp.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-example
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target
```

In the preceding example, the user that manages the service is specified by the `User` option. The user (`www-data`) must exist and have proper ownership of the app's files.

Use `TimeoutStopSec` to configure the duration of time to wait for the app to shut down after it receives the initial interrupt signal. If the app doesn't shut down in this period, SIGKILL is issued to terminate the app. Provide the value as unitless seconds (for example, `150`), a time span value (for example, `2min 30s`), or `infinity` to disable the timeout. `TimeoutStopSec` defaults to the value of `DefaultTimeoutStopSec` in the manager configuration file (`systemd-system.conf`, `system.conf.d`, `systemd-user.conf`, `user.conf.d`). The default timeout for most distributions is 90 seconds.

```
# The default value is 90 seconds for most distributions.
TimeoutStopSec=90
```

Linux has a case-sensitive file system. Setting `ASPNETCORE_ENVIRONMENT` to `Production` results in searching for the configuration file `appsettings.Production.json`, not `appsettings.production.json`.

Some values (for example, SQL connection strings) must be escaped for the configuration providers to read the environment variables. Use the following command to generate a properly escaped value for use in the configuration file:

```
systemd-escape "<value-to-escape>"
```

Colon (`:`) separators aren't supported in environment variable names. Use a double underscore (`__`) in place of a colon. The [Environment Variables configuration provider](../fundamentals/configuration/?view=aspnetcore-10.0#environment-variables) converts double-underscores into colons when environment variables are read into configuration. In the following example, the connection string key `ConnectionStrings:DefaultConnection` is set into the service definition file as `ConnectionStrings__DefaultConnection`:

```
Environment=ConnectionStrings__DefaultConnection={Connection String}
```

Save the file and enable the service.

```
sudo systemctl enable kestrel-helloapp.service
```

Start the service and verify that it's running.

```
sudo systemctl start kestrel-helloapp.service
sudo systemctl status kestrel-helloapp.service

◝ kestrel-helloapp.service - Example .NET Web API App running on Ubuntu
    Loaded: loaded (/etc/systemd/system/kestrel-helloapp.service; enabled)
    Active: active (running) since Thu 2016-10-18 04:09:35 NZDT; 35s ago
Main PID: 9021 (dotnet)
    CGroup: /system.slice/kestrel-helloapp.service
            └─9021 /usr/local/bin/dotnet /var/www/helloapp/helloapp.dll
```

With the reverse proxy configured and Kestrel managed through `systemd`, the web app is fully configured and can be accessed from a browser on the local machine at `http://localhost`. It's also accessible from a remote machine, barring any firewall that might be blocking. Inspecting the response headers, the `Server` header shows the ASP.NET Core app being served by Kestrel.

```
HTTP/1.1 200 OK
Date: Tue, 11 Oct 2016 16:22:23 GMT
Server: Kestrel
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Transfer-Encoding: chunked
```

### View logs

Since the web app using Kestrel is managed using `systemd`, all events and processes are logged to a centralized journal. However, this journal includes all entries for all services and processes managed by `systemd`. To view the `kestrel-helloapp.service`-specific items, use the following command:

```
sudo journalctl -fu kestrel-helloapp.service
```

For further filtering, time options such as `--since today`, `--until 1 hour ago`, or a combination of these can reduce the number of entries returned.

```
sudo journalctl -fu kestrel-helloapp.service --since "2016-10-18" --until "2016-10-18 04:00"
```

## Data protection

The [ASP.NET Core Data Protection stack](../security/data-protection/introduction?view=aspnetcore-10.0) is used by several ASP.NET Core [middlewares](../fundamentals/middleware/?view=aspnetcore-10.0), including authentication middleware (for example, cookie middleware) and cross-site request forgery (CSRF) protections. Even if Data Protection APIs aren't called by user code, data protection should be configured to create a persistent cryptographic [key store](../security/data-protection/implementation/key-management?view=aspnetcore-10.0). If data protection isn't configured, the keys are held in memory and discarded when the app restarts.

If the key ring is stored in memory when the app restarts:

* All cookie-based authentication tokens are invalidated.
* Users are required to sign in again on their next request.
* Any data protected with the key ring can no longer be decrypted. This may include [CSRF tokens](../security/anti-request-forgery?view=aspnetcore-10.0#aspnet-core-antiforgery-configuration) and [ASP.NET Core MVC TempData cookies](../fundamentals/app-state?view=aspnetcore-10.0#tempdata).

To configure data protection to persist and encrypt the key ring, see:

* [Key storage providers in ASP.NET Core](../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0)
* [Key encryption at rest in Windows and Azure using ASP.NET Core](../security/data-protection/implementation/key-encryption-at-rest?view=aspnetcore-10.0)

## Long request header fields

Proxy server default settings typically limit request header fields to 4 K or 8 K depending on the platform. An app may require fields longer than the default (for example, apps that use [Azure Active Directory](https://azure.microsoft.com/services/active-directory/)). If longer fields are required, the proxy server's default settings require adjustment. The values to apply depend on the scenario. For more information, see your server's documentation.

* [proxy\_buffer\_size](https://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffer_size)
* [proxy\_buffers](https://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffers)
* [proxy\_busy\_buffers\_size](https://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_busy_buffers_size)
* [large\_client\_header\_buffers](https://nginx.org/docs/http/ngx_http_core_module.html#large_client_header_buffers)

Warning

Don't increase the default values of proxy buffers unless necessary. Increasing these values increases the risk of buffer overrun (overflow) and Denial of Service (DoS) attacks by malicious users.

## Secure the app

### Enable AppArmor

Linux Security Modules (LSM) is a framework that's part of the Linux kernel since Linux 2.6. LSM supports different implementations of security modules. [AppArmor](https://apparmor.net/) is an LSM that implements a Mandatory Access Control system, which allows confining the program to a limited set of resources. Ensure AppArmor is enabled and properly configured.

### Configure the firewall

Close off all external ports that aren't in use. Uncomplicated firewall (ufw) provides a front end for `iptables` by providing a CLI for configuring the firewall.

Warning

A firewall will prevent access to the whole system if not configured correctly. Failure to specify the correct SSH port will effectively lock you out of the system if you are using SSH to connect to it. The default port is 22. For more information, see the [introduction to ufw](https://help.ubuntu.com/community/UFW) and the [manual](https://manpages.ubuntu.com/manpages/resolute/man8/ufw.8.html).

Install `ufw` and configure it to allow traffic on any ports needed.

```
sudo apt-get install ufw

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

sudo ufw enable
```

### Secure Nginx

#### Change the Nginx response name

Edit `src/http/ngx_http_header_filter_module.c`:

```
static char ngx_http_server_string[] = "Server: Web Server" CRLF;
static char ngx_http_server_full_string[] = "Server: Web Server" CRLF;
```

#### Configure options

Configure the server with additional required modules. Consider using a web app firewall, such as [ModSecurity](https://www.modsecurity.org/), to harden the app.

#### HTTPS configuration

**Configure the app for secure (HTTPS) local connections**

The [dotnet run](/en-us/dotnet/core/tools/dotnet-run) command uses the app's `Properties/launchSettings.json` file, which configures the app to listen on the URLs provided by the `applicationUrl` property. For example, `https://localhost:5001;http://localhost:5000`.

Configure the app to use a certificate in development for the `dotnet run` command or development environment (`F5` or `Ctrl`+`F5` in Visual Studio Code) using one of the following approaches:

* [Replace the default certificate from configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#configuration) (*Recommended*)
* [KestrelServerOptions.ConfigureHttpsDefaults](../fundamentals/servers/kestrel?view=aspnetcore-10.0#configurehttpsdefaultsactionhttpsconnectionadapteroptions)

**Configure the reverse proxy for secure (HTTPS) client connections**

Warning

The security configuration in this section is a general configuration to be used as a starting point for further customization. We're unable to provide support for third-party tooling, servers, and operating systems. *Use the configuration in this section at your own risk.* For more information, access the following resources:

* [Configuring HTTPS servers](http://nginx.org/docs/http/configuring_https_servers.html) (Nginx documentation)
* [mozilla.org SSL Configuration Generator](https://ssl-config.mozilla.org/#server=nginx)

- Configure the server to listen to HTTPS traffic on port 443 by specifying a valid certificate issued by a trusted Certificate Authority (CA).

- Harden the security by employing some of the practices depicted in the following */etc/nginx/nginx.conf* file.

- The following example doesn't configure the server to redirect insecure requests. We recommend using HTTPS redirection middleware. For more information, see [Enforce HTTPS in ASP.NET Core](../security/enforcing-ssl?view=aspnetcore-10.0).

  Note

  For development environments where the server configuration handles secure redirection instead of HTTPS redirection middleware, we recommend using temporary redirects (302) rather than permanent redirects (301). Link caching can cause unstable behavior in development environments.

- Adding a `Strict-Transport-Security` (HSTS) header ensures all subsequent requests made by the client are over HTTPS. For guidance on setting the `Strict-Transport-Security` header, see [Enforce HTTPS in ASP.NET Core](../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol).

- If HTTPS will be disabled in the future, use one of the following approaches:

  * Don't add the HSTS header.
  * Choose a short `max-age` value.

Add the */etc/nginx/proxy.conf* configuration file:

```
proxy_redirect          off;
proxy_set_header        Host $host;
proxy_set_header        X-Real-IP $remote_addr;
proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header        X-Forwarded-Proto $scheme;
client_max_body_size    10m;
client_body_buffer_size 128k;
proxy_connect_timeout   90;
proxy_send_timeout      90;
proxy_read_timeout      90;
proxy_buffers           32 4k;
```

**Replace** the contents of the */etc/nginx/nginx.conf* configuration file with the following file. The example contains both `http` and `server` sections in one configuration file.

```
http {
    include        /etc/nginx/proxy.conf;
    limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
    server_tokens  off;

    sendfile on;
    # Adjust keepalive_timeout to the lowest possible value that makes sense 
    # for your use case.
    keepalive_timeout   29;
    client_body_timeout 10; client_header_timeout 10; send_timeout 10;

    upstream helloapp{
        server 127.0.0.1:5000;
    }

    server {
        listen                    443 ssl http2;
        listen                    [::]:443 ssl http2;
        server_name               example.com *.example.com;
        ssl_certificate           /etc/ssl/certs/testCert.crt;
        ssl_certificate_key       /etc/ssl/certs/testCert.key;
        ssl_session_timeout       1d;
        ssl_protocols             TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers off;
        ssl_ciphers               ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
        ssl_session_cache         shared:SSL:10m;
        ssl_session_tickets       off;
        ssl_stapling              off;

        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;

        #Redirects all traffic
        location / {
            proxy_pass http://helloapp;
            limit_req  zone=one burst=10 nodelay;
        }
    }
}
```

Note

Blazor WebAssembly apps require a larger `burst` parameter value to accommodate the larger number of requests made by an app. For more information, see [Host and deploy ASP.NET Core Blazor WebAssembly with Nginx](../blazor/host-and-deploy/webassembly/nginx?view=aspnetcore-10.0).

Note

The preceding example disables Online Certificate Status Protocol (OCSP) Stapling. If enabled, confirm that the certificate supports the feature. For more information and guidance on enabling OCSP, see the following properties in the [Module ngx\_http\_ssl\_module (Nginx documentation)](http://nginx.org/en/docs/http/ngx_http_ssl_module.html) article:

* `ssl_stapling`
* `ssl_stapling_file`
* `ssl_stapling_responder`
* `ssl_stapling_verify`

#### Secure Nginx from clickjacking

[Clickjacking](https://blog.qualys.com/securitylabs/2015/10/20/clickjacking-a-common-implementation-mistake-that-can-put-your-websites-in-danger), also known as a *UI redress attack*, is a malicious attack where a website visitor is tricked into clicking a link or button on a different page than they're currently visiting. Use `X-FRAME-OPTIONS` to secure the site.

To mitigate clickjacking attacks:

1. Edit the *nginx.conf* file:

   ```
   sudo nano /etc/nginx/nginx.conf
   ```

   Add the line: `add_header X-Frame-Options "SAMEORIGIN";`

2. Save the file.

3. Restart Nginx.

#### MIME-type sniffing

This header prevents most browsers from MIME-sniffing a response away from the declared content type, as the header instructs the browser not to override the response content type. With the `nosniff` option, if the server says the content is `text/html`, the browser renders it as `text/html`.

1. Edit the *nginx.conf* file:

   ```
   sudo nano /etc/nginx/nginx.conf
   ```

   Add the line: `add_header X-Content-Type-Options "nosniff";`

2. Save the file.

3. Restart Nginx.

## Additional Nginx suggestions

After upgrading the shared framework on the server, restart the ASP.NET Core apps hosted by the server.

## Additional resources

* [Prerequisites for .NET Core on Linux](/en-us/dotnet/core/linux-prerequisites)
* [Nginx: Binary Releases: Official Debian/Ubuntu packages](https://www.nginx.com/resources/wiki/start/topics/tutorials/install/#official-debian-ubuntu-packages)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)
* [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0)
* [NGINX: Using the Forwarded header](https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Minimal APIs quick reference

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This document provides a quick reference for Minimal APIs. For a guided introduction, see [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0).

The Minimal APIs consist of:

* [`WebApplication` and `WebApplicationBuilder`](minimal-apis/webapplication?view=aspnetcore-10.0)
* [Route Handlers](minimal-apis/route-handlers?view=aspnetcore-10.0)

## `WebApplication`

The following code is generated by an ASP.NET Core template:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code can be created via `dotnet new web` on the command line or selecting the Empty Web template in Visual Studio.

The following code creates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`) without explicitly creating a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder):

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults:

[WebApplication](minimal-apis/webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

* [UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.developerexceptionpagemiddleware) is added first when the [HostingEnvironment](environments?view=aspnetcore-10.0) is `"Development"`.

* [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) is added second, if the user code didn't already call `UseRouting` and endpoints are configured, for example `app.MapGet`.

* [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) is added at the end of the middleware pipeline if endpoints are configured.

* [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) is added immediately after `UseRouting`, if user code didn't already call `UseAuthentication` and if [IAuthenticationSchemeProvider](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationschemeprovider) can be detected in the service provider. `IAuthenticationSchemeProvider` is added by default when you use [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), and services are detected by using [IServiceProviderIsService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iserviceproviderisservice).

* [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) is added next, if user code didn't already call `UseAuthorization` and if [IAuthorizationHandlerProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandlerprovider) can be detected in the service provider. `IAuthorizationHandlerProvider` is added by default when you use [AddAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), and services are detected by using `IServiceProviderIsService`.

* User configured middleware and endpoints are added between `UseRouting` and `UseEndpoints`.

The following code is effectively what the automatic middleware being added to the app produces:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// User middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// End user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case because it's automatically added as described earlier:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// Other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.

* The app needs to call `UseRouting` and `UseEndpoints` so the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

For information on antiforgery middleware in Minimal APIs, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-10.0#afwma).

### Working with ports

When a web app is created with Visual Studio or `dotnet new`, a `Properties/launchSettings.json` file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog `Unable to connect to web server 'AppName'`. Visual Studio returns an error because it's expecting the port specified in `Properties/launchSettings.json`, but the app is using the port specified by `app.Run("http://localhost:3000")`. Run the following port changing samples from the command line.

The following sections set the app's port.

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");
```

In the preceding code, the app responds to port `3000`.

#### Multiple ports

In the following code, the app responds to port `3000` and `4000`:

```
var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Set the port from the command line

The following command makes the app respond to port `7777`:

```
dotnet run --urls="https://localhost:7777"
```

If the Kestrel endpoint is also configured in the `appsettings.json` file, the `appsettings.json` file specified URL is used. For more information, see [Kestrel endpoint configuration](configuration/?view=aspnetcore-10.0#kestrel-endpoint-configuration)

#### Read the port from environment

The following code reads the port from the environment:

```
var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");
```

The preferred way to set the port from the environment is to use the `ASPNETCORE_URLS` environment variable, which is shown in the following section.

#### Set the ports via the `ASPNETCORE_URLS` environment variable

The `ASPNETCORE_URLS` environment variable is available to set the port:

```
ASPNETCORE_URLS=http://localhost:3000
```

`ASPNETCORE_URLS` supports multiple URLs:

```
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
```

### Listen on all interfaces

The following samples demonstrate listening on all interfaces.

#### `http://*:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://+:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://0.0.0.0:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Listen on all interfaces using `ASPNETCORE_URLS`

The preceding samples can use `ASPNETCORE_URLS`:

```
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
```

### Listen on all interfaces using `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`

The preceding samples can also use `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`, but `ASPNETCORE_URLS` (when set) overrides the following settings:

```
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
```

For more information, see [Configure endpoints for Kestrel web server](servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### Specify HTTPS with development certificate

```
var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information on the development certificate, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../security/enforcing-ssl?view=aspnetcore-10.0#trust).

### Specify HTTPS using a custom certificate

The following sections show how to specify the custom certificate using the `appsettings.json` file and via configuration.

#### Specify the custom certificate with `appsettings.json`

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}
```

#### Specify the custom certificate via configuration

```
var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Use the certificate APIs

```
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Read the environment

```
var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();
```

For more information using the environment, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).

### Configuration

The following code reads from the configuration system:

```
var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();
```

For more information, see [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

### Logging

The following code writes a message to the log on application startup:

```
var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information, see [Logging in .NET and ASP.NET Core](logging/?view=aspnetcore-10.0).

### Access the Dependency Injection (DI) container

The following code shows how to get services from the DI container during application startup:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();
```

The following code shows how to access keys from the DI container using the [`[FromKeyedServices]` attribute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}
```

For more information on DI, see [Dependency injection in ASP.NET Core](dependency-injection?view=aspnetcore-10.0).

## `WebApplicationBuilder`

This section contains sample code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

### Change the content root, application name, and environment

The following code sets the content root, application name, and environment:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

[WebApplication.Create](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.create) initializes a new instance of the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) class with preconfigured defaults.

For more information, see [ASP.NET Core fundamentals overview](./?view=aspnetcore-10.0)

### Change the content root, app name, and environment by using environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| Feature          | Environment variable         | Command-line argument |
| ---------------- | ---------------------------- | --------------------- |
| Application name | `ASPNETCORE_APPLICATIONNAME` | `--applicationName`   |
| Environment name | `ASPNETCORE_ENVIRONMENT`     | `--environment`       |
| Content root     | `ASPNETCORE_CONTENTROOT`     | `--contentRoot`       |

### Add configuration providers

The following sample adds the INI configuration provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](configuration/?view=aspnetcore-10.0#file-configuration-provider) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

### Read configuration

By default the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) reads configuration from multiple sources, including:

* `appSettings.json` and `appSettings.{environment}.json`
* Environment variables
* The command line

For a complete list of configuration sources read, see [Default configuration](configuration/?view=aspnetcore-10.0#default-configuration) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

The following code reads `HelloKey` from configuration and displays the value at the `/` endpoint. If the configuration value is null, "Hello" is assigned to `message`:

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

### Read the environment

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Add logging providers

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();
```

### Add services

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

### Customize the `IHostBuilder`

Existing extension methods on [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) can be accessed using the [Host property](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.properties#microsoft-extensions-hosting-ihostbuilder-properties):

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Customize the `IWebHostBuilder`

Extension methods on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) can be accessed using the [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost) property.

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();
```

### Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed with `WebHostOptions`, the command line, or with the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();
```

### Custom dependency injection (DI) container

The following example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

### Add middleware

Any existing ASP.NET Core middleware can be configured on the `WebApplication`:

```
var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0).

### Developer exception page

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the [development environment](environments?view=aspnetcore-10.0), navigating to `/` renders a friendly page that shows the exception.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();
```

## ASP.NET Core middleware

The following table lists some of the middleware frequently used with Minimal APIs.

| Middleware                                                                                 | Description                                                                                                          | API                                                                                                                                                                                                      |
| ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Authentication](../security/authentication/?view=aspnetcore-10.0)                         | Provides authentication support.                                                                                     | [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)                                                                                           |
| [Authorization](../security/authorization/introduction?view=aspnetcore-10.0)               | Provides authorization support.                                                                                      | [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)                                                                                    |
| [CORS](../security/cors?view=aspnetcore-10.0)                                              | Configures Cross-Origin Resource Sharing.                                                                            | [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors)                                                                                                               |
| [Exception Handler](error-handling-api?view=aspnetcore-10.0)                               | Globally handles exceptions thrown by the middleware pipeline.                                                       | [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)                                                                                     |
| [Forwarded Headers](middleware/?view=aspnetcore-10.0#forwarded-headers-middleware-order)   | Forwards proxied headers onto the current request.                                                                   | [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders)                                                                                     |
| [HTTPS Redirection](../security/enforcing-ssl?view=aspnetcore-10.0)                        | Redirects all HTTP requests to HTTPS.                                                                                | [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)                                                                                   |
| [HTTP Strict Transport Security (HSTS)](middleware/?view=aspnetcore-10.0#middleware-order) | Security enhancement middleware that adds a special response header.                                                 | [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)                                                                                                                  |
| [Request Logging](http-logging/?view=aspnetcore-10.0)                                      | Provides support for logging HTTP requests and responses.                                                            | [UseHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usehttplogging)                                                                                             |
| [Request Timeouts](../performance/timeouts?view=aspnetcore-10.0)                           | Provides support for configuring request timeouts, global default and per endpoint.                                  | [UseRequestTimeouts](/en-us/dotnet/api/microsoft.aspnetcore.builder.requesttimeoutsiapplicationbuilderextensions.userequesttimeouts)                                                                     |
| [W3C Request Logging](https://www.w3.org/TR/WD-logfile.html)                               | Provides support for logging HTTP requests and responses in the [W3C format](https://www.w3.org/TR/WD-logfile.html). | [UseW3CLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usew3clogging)                                                                                               |
| [Response Caching](../performance/caching/middleware?view=aspnetcore-10.0)                 | Provides support for caching responses.                                                                              | [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching)                                                                                        |
| [Response Compression](../performance/response-compression?view=aspnetcore-10.0)           | Provides support for compressing responses.                                                                          | [UseResponseCompression](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecompressionbuilderextensions.useresponsecompression)                                                                     |
| [Session](app-state?view=aspnetcore-10.0)                                                  | Provides support for managing user sessions.                                                                         | [UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)                                                                                                      |
| [Static Files](static-files?view=aspnetcore-10.0)                                          | Provides support for serving static files and directory browsing.                                                    | [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), [UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) |
| [WebSockets](websockets?view=aspnetcore-10.0)                                              | Enables the WebSockets protocol.                                                                                     | [UseWebSockets](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketmiddlewareextensions.usewebsockets)                                                                                              |

The following sections cover request handling: routing, parameter binding, and responses.

## Routing

A configured `WebApplication` supports `Map{Verb}` and [MapMethods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapmethods) where `{Verb}` is a camel-cased HTTP method like `Get`, `Post`, `Put`, or `Delete`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();
```

The [Delegate](/en-us/dotnet/api/system.delegate) arguments passed to these methods are called *route handlers*.

### Route Handlers

Route handlers are methods that execute when the route matches. Route handlers can be a lambda expression, a local function, an instance method, or a static method. Route handlers can be synchronous or asynchronous.

The following sections provide examples of different route handlers.

### Lambda expression

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();
```

### Local function

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();
```

### Instance method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}
```

### Static method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}
```

### Endpoint defined outside of Program.cs

Minimal APIs don't have to be located in the *Program.cs* file. For example, you can set up the structure in the *Program.cs* file, and define the endpoint in a separate file:

**Program.cs**

```
using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();
```

**TodoEndpoints.cs**

```
namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}
```

For more information, see the [Route groups](#route-groups) section later in this article.

### Named endpoints and link generation

You can supply a name for your endpoints to generate URLs that target the endpoint. Using a named endpoint avoids having to hard code paths in an app:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();
```

The preceding code displays the message *`The link to the hello route is /hello`* from the `/` (forward slash) endpoint.

#### Criteria for endpoint names

Endpoint names must satisfy the following criteria:

* Endpoint names are case sensitive.
* Endpoint names must be globally unique.
* Endpoint names are used as the OpenAPI operation identifier (ID) when OpenAPI support is enabled. For more information, see [Generate OpenAPI documents](openapi/aspnetcore-openapi?view=aspnetcore-10.0).

### Route parameters

Route parameters can be captured as part of the route pattern definition:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();
```

The preceding code returns the message *The user id is 3 and book id is 7* from the URI `/users/3/books/7`.

The route handler can declare the parameters to capture. When a request is made to a route with parameters declared to capture, the parameters are parsed and passed to the handler. This approach makes it easy to capture the values in a type-safe way. In the preceding code, the `userId` and `bookId` parameters are both type `int`.

In the preceding code, if either route value can't be converted to an `int`, an exception is thrown. The GET request `/users/hello/books/3` throws the following exception:

```
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
```

### Wildcard and catch all routes

The following catch all route returns *Routing to hello* from the `/posts/hello` endpoint:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();
```

### Route constraints

Route constraints restrict the matching behavior of a route.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();
```

The following table demonstrates the preceding route templates and their behavior.

| Route template                       | Example matching URI |
| ------------------------------------ | -------------------- |
| `/todos/{id:int}`                    | `/todos/1`           |
| `/todos/{text}`                      | `/todos/something`   |
| `/posts/{slug:regex(^[a-z0-9_-]+$)}` | `/posts/mypost`      |

For more information, see [Route constraint reference](routing?view=aspnetcore-10.0#route-constraints) in [Routing in ASP.NET Core](routing?view=aspnetcore-10.0).

### Route groups

The [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) extension method helps organize groups of endpoints with a common prefix and reduces repetitive code. Use this method to customize entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata) that add [endpoint metadata](routing?view=aspnetcore-10.0#endpoint-metadata).

For example, the following code creates two similar groups of endpoints:

```
app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
```

```
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}
```

In this scenario, you can use a relative address for the `Location` header in the `201 Created` result:

```
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}
```

The first group of endpoints matches only requests prefixed with `/public/todos` and are accessible without any authentication. The second group of endpoints matches only requests prefixed with `/private/todos` and require authentication.

`QueryPrivateTodos` is a local function that modifies the `TodoDb` parameters of the route handler, to enable them to access and store private todo data. `QueryPrivateTodos` serves as an [endpoint filter factory](minimal-apis/min-api-filters?view=aspnetcore-10.0).

Route groups also support nested groups and complex prefix patterns with route parameters and constraints. In the following example, the route handler mapped to the `user` group can capture the `{org}` and `{group}` route parameters defined in the outer group prefixes.

The prefix can also be empty. This approach can be useful for adding endpoint metadata or filters to a group of endpoints without changing the route pattern.

```
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
```

Adding filters or metadata to a group results in the same behavior as adding them individually to each endpoint (before adding extra filters or metadata that might exist in an inner group or specific endpoint).

```
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});
```

In the preceding example, the outer filter logs the incoming request before the inner filter even though the outer filter is added second. Because the filters are applied to different groups, the order that they're added relative to each other doesn't matter. The order in which filters are added matters when applied to the same group or specific endpoint.

A request to `/outer/inner/` logs the following data:

```
/outer group filter
/inner group filter
MapGet filter
```

## Parameter binding

The following GET route handler uses some of these parameter binding sources:

```
var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }
```

### Key parameter binding features

* **Explicit binding**: Use attributes like `[FromRoute]`, `[FromQuery]`, `[FromHeader]`, `[FromBody]`, `[FromForm]`, and `[FromServices]` to explicitly specify binding sources.
* **Form binding**: Bind form values using `[FromForm]` attribute, including support for `IFormFile` and `IFormFileCollection` for file uploads.
* **Complex types**: Bind to collections and complex types from forms, query strings, and headers.
* **Custom binding**: Implement custom binding logic using `TryParse`, `BindAsync`, or the `IBindableFromHttpContext<T>` interface.
* **Optional parameters**: Support nullable types and default values for optional parameters.
* **Dependency injection**: Parameters are automatically bound from services registered in the DI container.
* **Special types**: Automatic binding for `HttpContext`, `HttpRequest`, `HttpResponse`, `CancellationToken`, `ClaimsPrincipal`, `Stream`, and `PipeReader`.

***

**Learn more:** For detailed information on parameter binding including advanced scenarios, validation, binding precedence, and troubleshooting, see [Parameter binding in Minimal API applications](minimal-apis/parameter-binding?view=aspnetcore-10.0).

## Json+PipeReader deserialization in Minimal APIs

```
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
    // previous code
}
```

### Option 3: A more complicated but better performing fix

This fix involves setting up a separate code path for the `ReadOnlySequence` handling:

```
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    if (reader.HasValueSequence)
    {
        reader.ValueSequence;
        // ReadOnlySequence optimized path
    }
    else
    {
        reader.ValueSpan;
        // ReadOnlySpan optimized path
    }
}
```

For more information, see

* [System.Text.Json.Serialization.JsonConverter](/en-us/dotnet/api/system.text.json.serialization.jsonconverter)
* [github.com/dotnet/aspnetcore/pull/62895](https://github.com/dotnet/aspnetcore/pull/62895)

## Validation support in Minimal APIs

For an overview of [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation) validation, including how to register services for Minimal API projects, see [Validation in ASP.NET Core](validation?view=aspnetcore-10.0).

Enabling validation allows the ASP.NET Core runtime to perform validations defined on the:

* Query
* Header
* Request body

Validations are defined using attributes in the [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace.

When a parameter to a Minimal API endpoint is a class or record type, validation attributes are automatically applied. For example:

```
public record Product(
    [Required] string Name, 
    [Range(1, 1000)] int Quantity);
```

Developers customize the behavior of the validation system by:

* Creating custom [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) implementations.
* Implementing the [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) interface for complex validation logic.

If validation fails, the runtime returns a [`400 - Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400) response with details of the validation errors.

### Disable validation for specific endpoints

Validation can be disabled for specific endpoints by using the `DisableValidation` extension method, as in the following example:

```
app.MapPost("/products", 
        ([EvenNumber(ErrorMessage = "Product ID must be even")] int productId, 
        [Required] string name) => TypedResults.Ok(productId))
    .DisableValidation();
```

### Customize validation error responses using `IProblemDetailsService`

Customize error responses from Minimal API validation logic with an [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) implementation. Register this service in the app's service collection to enable more consistent and user-specific error responses.

To implement custom validation error responses:

* Implement [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) or use the default implementation.
* Register the service in the service container.
* The validation system automatically uses the registered service to format validation error responses.

For more information on customizing validation error responses with `IProblemDetailsService`, see [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0#customize-validation-error-responses-using-iproblemdetailsservice).

### Localizing validation messages

Validation error messages and the display names of validated parameters and properties are localized by [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation).

Register the standard ASP.NET Core localization services together with the validation pipeline in the `Program` file:

```
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddValidation();
```

By default, lookup keys are resolved against the resources of the type that declares the validated member. Top-level parameters on Minimal API endpoints don't have a containing type, so use `ValidationOptions.LocalizerProvider` to resolve messages for them from a shared resource file:

```
builder.Services.AddValidation(options =>
{
    options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationResources));
});
```

For the message lookup key conventions, custom message formatting, and loading messages from sources other than resource files, see [Validation in ASP.NET Core](validation?view=aspnetcore-10.0#localize-validation-messages).

## Responses

Route handlers support the following types of return values:

* `IResult` based: Includes `Task<IResult>` and `ValueTask<IResult>`.
* `string`: Includes `Task<string>` and `ValueTask<string>`.
* `T` (Any other type): Includes `Task<T>` and `ValueTask<T>`.

| Return value         | Behavior                                                                                                     | Content-Type                             |
| -------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------- |
| `IResult`            | The framework calls [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync) | Decided by the `IResult` implementation. |
| `string`             | The framework writes the string directly to the response.                                                    | `text/plain`                             |
| `T` (Any other type) | The framework JSON-serializes the response.                                                                  | `application/json`                       |

For more information on route handler return values, see [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0).

### Example return values

#### String return values

```
app.MapGet("/hello", () => "Hello World");
```

#### JSON return values

```
app.MapGet("/hello", () => new { Message = "Hello World" });
```

#### Return `TypedResults`

The following code returns [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults):

```
app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));
```

Returning `TypedResults` is preferred to returning [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results). For more information, see [`TypedResults` versus `Results`](minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

#### `IResult` return values

```
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
```

The following example uses the built-in result types to customize the response:

```
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
```

#### JSON

```
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
```

#### Custom Status Code

```
app.MapGet("/405", () => Results.StatusCode(405));
```

#### Text

```
app.MapGet("/text", () => Results.Text("This is some text"));
```

[]()

#### Stream

```
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});
```

See [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0#stream7) for more examples.

#### Redirect

```
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
```

#### File

```
app.MapGet("/download", () => Results.File("myfile.text"));
```

[]()

### Built-in results

Common result helpers exist in the [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes. Returning `TypedResults` is preferred to returning `Results`. For more information, see [`TypedResults` versus `Results`](minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

### Modifying Headers

Use the `HttpResponse` object to modify response headers:

```
app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});
```

### Customizing results

Applications can control responses by implementing a custom [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) type. The following code is an example of an HTML result type:

```
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}
```

We recommend adding an extension method to [Microsoft.AspNetCore.Http.IResultExtensions](/en-us/dotnet/api/microsoft.aspnetcore.http.iresultextensions) to make these custom results more discoverable.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();
```

### Typed results

The [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) interface can represent values returned from Minimal APIs that don't utilize the implicit support for JSON serializing the returned object to the HTTP response. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class is used to create varying `IResult` objects that represent different types of responses. For example, setting the response status code or redirecting to another URL.

The types implementing `IResult` are public, allowing for type assertions when testing. For example:

```
[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}
```

You can look at the return types of the corresponding methods on the static [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) class to find the correct public `IResult` type to cast to.

See [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0) for more examples.

## Filters

For more information, see [Filters in Minimal API apps](minimal-apis/min-api-filters?view=aspnetcore-10.0).

## Authorization

Routes can be protected using authorization policies. These can be declared via the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute or by using the [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) method:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

The preceding code can be written with [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization):

```
app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();
```

The following sample uses [policy-based authorization](../security/authorization/policies?view=aspnetcore-10.0):

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

### Allow unauthenticated users to access an endpoint

The [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) allows unauthenticated users to access endpoints:

```
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();
```

## CORS

Routes can be [CORS](../security/cors?view=aspnetcore-10.0) enabled using [CORS policies](../security/cors?view=aspnetcore-10.0#cors-policy-options). CORS can be declared via the [`[EnableCors]`](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute or by using the [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) method. The following samples enable CORS:

```
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
```

```
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();
```

For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0)

## ValidateScopes and ValidateOnBuild

[ValidateScopes](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validatescopes#microsoft-extensions-dependencyinjection-serviceprovideroptions-validatescopes) and [ValidateOnBuild](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validateonbuild#microsoft-extensions-dependencyinjection-serviceprovideroptions-validateonbuild) are enabled by default in the [Development](environments?view=aspnetcore-10.0) environment but disabled in other environments.

When `ValidateOnBuild` is `true`, the DI container validates the service configuration at build time. If the service configuration is invalid, the build fails at app startup, rather than at runtime when the service is requested.

When `ValidateScopes` is `true`, the DI container validates that a scoped service isn't resolved from the root scope. Resolving a scoped service from the root scope can result in a memory leak because the service is retained in memory longer than the scope of the request.

`ValidateScopes` and `ValidateOnBuild` are false by default in non-Development modes for performance reasons.

The following code shows `ValidateScopes` is enabled by default in development mode but disabled in release mode:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    // Intentionally getting service provider from app, not from the request
    // This causes an exception from attempting to resolve a scoped service
    // outside of a scope.
    // Throws System.InvalidOperationException:
    // 'Cannot resolve scoped service 'MyScopedService' from root provider.'
    var service = app.Services.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved");
});

app.Run();

public class MyScopedService { }
```

The following code shows `ValidateOnBuild` is enabled by default in development mode but disabled in release mode:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();

// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    var service = context.RequestServices.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved correctly!");
});

app.Run();

public class MyScopedService { }

public class AnotherService
{
    public AnotherService(BrokenService brokenService) { }
}

public class BrokenService { }
```

The following code disables `ValidateScopes` and `ValidateOnBuild` in `Development`:

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
    // Doesn't detect the validation problems because ValidateScopes is false.
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = false;
        options.ValidateOnBuild = false;
    });
}
```

## See also

* [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0)
* [Generate OpenAPI documents](openapi/aspnetcore-openapi?view=aspnetcore-10.0)
* [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0)
* [Filters in Minimal API apps](minimal-apis/min-api-filters?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0)
* [Authentication and authorization in Minimal APIs](minimal-apis/security?view=aspnetcore-10.0)
* [Test Minimal API apps](minimal-apis/test-min-api?view=aspnetcore-10.0)
* [Short-circuit routing](https://andrewlock.net/exploring-the-dotnet-8-preview-short-circuit-routing/)
* [Identity API endpoints](https://andrewlock.net/exploring-the-dotnet-8-preview-introducing-the-identity-api-endpoints/)
* [Keyed service dependency injection container support](https://andrewlock.net/exploring-the-dotnet-8-preview-keyed-services-dependency-injection-support/)
* [A look behind the scenes of Minimal API endpoints](https://andrewlock.net/behind-the-scenes-of-minimal-apis-1-a-first-look-behind-the-scenes-of-minimal-api-endpoints/)
* [Organizing ASP.NET Core Minimal APIs](https://www.tessferrandez.com/blog/2023/10/31/organizing-minimal-apis.html)
* [Fluent validation discussion on GitHub](https://github.com/dotnet/aspnetcore/issues/51834#issuecomment-1837180853)

This document provides a quick reference for Minimal APIs. For a guided introduction, see [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0).

The Minimal APIs consist of:

* [WebApplication and WebApplicationBuilder](minimal-apis/webapplication?view=aspnetcore-10.0)
* [Route Handlers](minimal-apis/route-handlers?view=aspnetcore-10.0)

## `WebApplication`

The following code is generated by an ASP.NET Core template:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code can be created via `dotnet new web` on the command line or selecting the Empty Web template in Visual Studio.

The following code creates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`) without explicitly creating a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder):

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults:

[WebApplication](minimal-apis/webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

* [UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.developerexceptionpagemiddleware) is added first when the [HostingEnvironment](environments?view=aspnetcore-10.0) is `"Development"`.

* [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) is added second, if the user code didn't already call `UseRouting` and endpoints are configured, for example `app.MapGet`.

* [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) is added at the end of the middleware pipeline if endpoints are configured.

* [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) is added immediately after `UseRouting`, if user code didn't already call `UseAuthentication` and if [IAuthenticationSchemeProvider](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationschemeprovider) can be detected in the service provider. `IAuthenticationSchemeProvider` is added by default when you use [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), and services are detected by using [IServiceProviderIsService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iserviceproviderisservice).

* [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) is added next, if user code didn't already call `UseAuthorization` and if [IAuthorizationHandlerProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandlerprovider) can be detected in the service provider. `IAuthorizationHandlerProvider` is added by default when you use [AddAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), and services are detected by using `IServiceProviderIsService`.

* User configured middleware and endpoints are added between `UseRouting` and `UseEndpoints`.

The following code is effectively what the automatic middleware being added to the app produces:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// User middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// End user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case because it's automatically added as described earlier:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// Other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.

* The app needs to call `UseRouting` and `UseEndpoints` so the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

For information on antiforgery middleware in Minimal APIs, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-10.0#afwma).

### Working with ports

When a web app is created with Visual Studio or `dotnet new`, a `Properties/launchSettings.json` file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog `Unable to connect to web server 'AppName'`. Visual Studio returns an error because it's expecting the port specified in `Properties/launchSettings.json`, but the app is using the port specified by `app.Run("http://localhost:3000")`. Run the following port changing samples from the command line.

The following sections set the app's port.

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");
```

In the preceding code, the app responds to port `3000`.

#### Multiple ports

In the following code, the app responds to port `3000` and `4000`:

```
var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Set the port from the command line

The following command makes the app respond to port `7777`:

```
dotnet run --urls="https://localhost:7777"
```

If the Kestrel endpoint is also configured in the `appsettings.json` file, the `appsettings.json` file specified URL is used. For more information, see [Kestrel endpoint configuration](configuration/?view=aspnetcore-10.0#kestrel-endpoint-configuration)

#### Read the port from environment

The following code reads the port from the environment:

```
var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");
```

The preferred way to set the port from the environment is to use the `ASPNETCORE_URLS` environment variable, which is shown in the following section.

#### Set the ports via the `ASPNETCORE_URLS` environment variable

The `ASPNETCORE_URLS` environment variable is available to set the port:

```
ASPNETCORE_URLS=http://localhost:3000
```

`ASPNETCORE_URLS` supports multiple URLs:

```
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
```

### Listen on all interfaces

The following samples demonstrate listening on all interfaces

#### `http://*:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://+:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://0.0.0.0:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Listen on all interfaces using `ASPNETCORE_URLS`

The preceding samples can use `ASPNETCORE_URLS`:

```
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
```

### Listen on all interfaces using `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`

The preceding samples can also use `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`, but `ASPNETCORE_URLS` (when set) overrides the following settings:

```
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
```

For more information, see [Configure endpoints for Kestrel web server](servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### Specify HTTPS with development certificate

```
var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information on the development certificate, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../security/enforcing-ssl?view=aspnetcore-10.0#trust).

### Specify HTTPS using a custom certificate

The following sections show how to specify the custom certificate using the `appsettings.json` file and via configuration.

#### Specify the custom certificate with `appsettings.json`

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}
```

#### Specify the custom certificate via configuration

```
var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Use the certificate APIs

```
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Read the environment

```
var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();
```

For more information using the environment, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).

### Configuration

The following code reads from the configuration system:

```
var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();
```

For more information, see [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

### Logging

The following code writes a message to the log on application startup:

```
var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information, see [Logging in .NET and ASP.NET Core](logging/?view=aspnetcore-10.0).

### Access the Dependency Injection (DI) container

The following code shows how to get services from the DI container during application startup:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();
```

The following code shows how to access keys from the DI container using the [`[FromKeyedServices]` attribute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}
```

For more information on DI, see [Dependency injection in ASP.NET Core](dependency-injection?view=aspnetcore-10.0).

## `WebApplicationBuilder`

This section contains sample code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

### Change the content root, application name, and environment

The following code sets the content root, application name, and environment:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

[WebApplication.Create](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.create) initializes a new instance of the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) class with preconfigured defaults.

For more information, see [ASP.NET Core fundamentals overview](./?view=aspnetcore-10.0)

### Change the content root, app name, and environment by using environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| Feature          | Environment variable         | Command-line argument |
| ---------------- | ---------------------------- | --------------------- |
| Application name | `ASPNETCORE_APPLICATIONNAME` | `--applicationName`   |
| Environment name | `ASPNETCORE_ENVIRONMENT`     | `--environment`       |
| Content root     | `ASPNETCORE_CONTENTROOT`     | `--contentRoot`       |

### Add configuration providers

The following sample adds the INI configuration provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](configuration/?view=aspnetcore-10.0#file-configuration-provider) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

### Read configuration

By default the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) reads configuration from multiple sources, including:

* `appSettings.json` and `appSettings.{environment}.json`
* Environment variables
* The command line

For a complete list of configuration sources read, see [Default configuration](configuration/?view=aspnetcore-10.0#default-configuration) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

The following code reads `HelloKey` from configuration and displays the value at the `/` endpoint. If the configuration value is null, "Hello" is assigned to `message`:

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

### Read the environment

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Add logging providers

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();
```

### Add services

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

### Customize the `IHostBuilder`

Existing extension methods on [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) can be accessed using the [Host property](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.properties#microsoft-extensions-hosting-ihostbuilder-properties):

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Customize the `IWebHostBuilder`

Extension methods on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) can be accessed using the [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost) property.

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();
```

### Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed with `WebHostOptions`, the command line, or with the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();
```

### Custom dependency injection (DI) container

The following example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

### Add middleware

Any existing ASP.NET Core middleware can be configured on the `WebApplication`:

```
var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0).

### Developer exception page

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the [development environment](environments?view=aspnetcore-10.0), navigating to `/` renders a friendly page that shows the exception.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();
```

## ASP.NET Core middleware

The following table lists some of the middleware frequently used with Minimal APIs.

| Middleware                                                                                | Description                                                                                                          | API                                                                                                                                                                                                      |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Authentication](../security/authentication/?view=aspnetcore-6.0)                         | Provides authentication support.                                                                                     | [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)                                                                                           |
| [Authorization](../security/authorization/introduction?view=aspnetcore-10.0)              | Provides authorization support.                                                                                      | [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)                                                                                    |
| [CORS](../security/cors?view=aspnetcore-6.0)                                              | Configures Cross-Origin Resource Sharing.                                                                            | [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors)                                                                                                               |
| [Exception Handler](error-handling-api?view=aspnetcore-6.0)                               | Globally handles exceptions thrown by the middleware pipeline.                                                       | [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)                                                                                     |
| [Forwarded Headers](middleware/?view=aspnetcore-6.0#forwarded-headers-middleware-order)   | Forwards proxied headers onto the current request.                                                                   | [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders)                                                                                     |
| [HTTPS Redirection](../security/enforcing-ssl?view=aspnetcore-6.0)                        | Redirects all HTTP requests to HTTPS.                                                                                | [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)                                                                                   |
| [HTTP Strict Transport Security (HSTS)](middleware/?view=aspnetcore-6.0#middleware-order) | Security enhancement middleware that adds a special response header.                                                 | [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)                                                                                                                  |
| [Request Logging](http-logging/?view=aspnetcore-10.0)                                     | Provides support for logging HTTP requests and responses.                                                            | [UseHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usehttplogging)                                                                                             |
| [Request Timeouts](../performance/timeouts?view=aspnetcore-10.0)                          | Provides support for configuring request timeouts, global default and per endpoint.                                  | `UseRequestTimeouts`                                                                                                                                                                                     |
| [W3C Request Logging](https://www.w3.org/TR/WD-logfile.html)                              | Provides support for logging HTTP requests and responses in the [W3C format](https://www.w3.org/TR/WD-logfile.html). | [UseW3CLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usew3clogging)                                                                                               |
| [Response Caching](../performance/caching/middleware?view=aspnetcore-10.0)                | Provides support for caching responses.                                                                              | [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching)                                                                                        |
| [Response Compression](../performance/response-compression?view=aspnetcore-10.0)          | Provides support for compressing responses.                                                                          | [UseResponseCompression](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecompressionbuilderextensions.useresponsecompression)                                                                     |
| [Session](app-state?view=aspnetcore-10.0)                                                 | Provides support for managing user sessions.                                                                         | [UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)                                                                                                      |
| [Static Files](static-files?view=aspnetcore-10.0)                                         | Provides support for serving static files and directory browsing.                                                    | [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), [UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) |
| [WebSockets](websockets?view=aspnetcore-10.0)                                             | Enables the WebSockets protocol.                                                                                     | [UseWebSockets](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketmiddlewareextensions.usewebsockets)                                                                                              |

The following sections cover request handling: routing, parameter binding, and responses.

## Routing

A configured `WebApplication` supports `Map{Verb}` and [MapMethods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapmethods) where `{Verb}` is a camel-cased HTTP method like `Get`, `Post`, `Put`, or `Delete`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();
```

The [Delegate](/en-us/dotnet/api/system.delegate) arguments passed to these methods are called "route handlers".

### Route Handlers

Route handlers are methods that execute when the route matches. Route handlers can be a lambda expression, a local function, an instance method, or a static method. Route handlers can be synchronous or asynchronous.

The following sections provide examples of different route handlers.

### Lambda expression

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();
```

### Local function

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();
```

### Instance method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}
```

### Static method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}
```

### Endpoint defined outside of Program.cs

Minimal APIs don't have to be located in the *Program.cs* file. For example, you can set up the structure in the *Program.cs* file, and define the endpoint in a separate file:

**Program.cs**

```
using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();
```

**TodoEndpoints.cs**

```
namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}
```

For more information, see the [Route groups](#route-groups) section later in this article.

### Named endpoints and link generation

You can supply a name for your endpoints to generate URLs that target the endpoint. Using a named endpoint avoids having to hard code paths in an app:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();
```

The preceding code displays the message *`The link to the hello route is /hello`* from the `/` (forward slash) endpoint.

#### Criteria for endpoint names

Endpoint names must satisfy the following criteria:

* Endpoint names are case sensitive.
* Endpoint names must be globally unique.
* Endpoint names are used as the OpenAPI operation identifier (ID) when OpenAPI support is enabled. For more information, see [Generate OpenAPI documents](openapi/aspnetcore-openapi?view=aspnetcore-10.0).

### Route parameters

Route parameters can be captured as part of the route pattern definition:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();
```

The preceding code returns the message *The user id is 3 and book id is 7* from the URI `/users/3/books/7`.

The route handler can declare the parameters to capture. When a request is made to a route with parameters declared to capture, the parameters are parsed and passed to the handler. This approach makes it easy to capture the values in a type-safe way. In the preceding code, the `userId` and `bookId` parameters are both type `int`.

In the preceding code, if either route value can't be converted to an `int`, an exception is thrown. The GET request `/users/hello/books/3` throws the following exception:

```
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
```

### Wildcard and catch all routes

The following catch all route returns *Routing to hello* from the `/posts/hello` endpoint:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();
```

### Route constraints

Route constraints restrict the matching behavior of a route.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();
```

The following table demonstrates the preceding route templates and their behavior.

| Route template                       | Example matching URI |
| ------------------------------------ | -------------------- |
| `/todos/{id:int}`                    | `/todos/1`           |
| `/todos/{text}`                      | `/todos/something`   |
| `/posts/{slug:regex(^[a-z0-9_-]+$)}` | `/posts/mypost`      |

For more information, see [Route constraint reference](routing?view=aspnetcore-10.0#route-constraints) in [Routing in ASP.NET Core](routing?view=aspnetcore-10.0).

### Route groups

The [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) extension method helps organize groups of endpoints with a common prefix and reduces repetitive code. Use this method to customize entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata) that add [endpoint metadata](routing?view=aspnetcore-10.0#endpoint-metadata).

For example, the following code creates two similar groups of endpoints:

```
app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
```

```
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}
```

In this scenario, you can use a relative address for the `Location` header in the `201 Created` result:

```
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}
```

The first group of endpoints matches only requests prefixed with `/public/todos` and are accessible without any authentication. The second group of endpoints matches only requests prefixed with `/private/todos` and require authentication.

`QueryPrivateTodos` is a local function that modifies the `TodoDb` parameters of the route handler, to enable them to access and store private todo data. `QueryPrivateTodos` serves as an [endpoint filter factory](minimal-apis/min-api-filters?view=aspnetcore-10.0).

Route groups also support nested groups and complex prefix patterns with route parameters and constraints. In the following example, the route handler mapped to the `user` group can capture the `{org}` and `{group}` route parameters defined in the outer group prefixes.

The prefix can also be empty. This approach can be useful for adding endpoint metadata or filters to a group of endpoints without changing the route pattern.

```
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
```

Adding filters or metadata to a group results in the same behavior as adding them individually to each endpoint (before adding extra filters or metadata that might exist in an inner group or specific endpoint).

```
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});
```

In the preceding example, the outer filter logs the incoming request before the inner filter even though the outer filter is added second. Because the filters are applied to different groups, the order that they're added relative to each other doesn't matter. The order in which filters are added matters when applied to the same group or specific endpoint.

A request to `/outer/inner/` logs the following data:

```
/outer group filter
/inner group filter
MapGet filter
```

## Parameter binding

The following GET route handler uses some of these parameter binding sources:

```
var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }
```

### Key parameter binding features

* **Explicit binding**: Use attributes like `[FromRoute]`, `[FromQuery]`, `[FromHeader]`, `[FromBody]`, `[FromForm]`, and `[FromServices]` to explicitly specify binding sources.
* **Form binding**: Bind form values using `[FromForm]` attribute, including support for `IFormFile` and `IFormFileCollection` for file uploads.
* **Complex types**: Bind to collections and complex types from forms, query strings, and headers.
* **Custom binding**: Implement custom binding logic using `TryParse`, `BindAsync`, or the `IBindableFromHttpContext<T>` interface.
* **Optional parameters**: Support nullable types and default values for optional parameters.
* **Dependency injection**: Parameters are automatically bound from services registered in the DI container.
* **Special types**: Automatic binding for `HttpContext`, `HttpRequest`, `HttpResponse`, `CancellationToken`, `ClaimsPrincipal`, `Stream`, and `PipeReader`.

***

**Learn more:** For detailed information on parameter binding including advanced scenarios, validation, binding precedence, and troubleshooting, see [Parameter binding in Minimal API applications](minimal-apis/parameter-binding?view=aspnetcore-10.0).

## Responses

Route handlers support the following types of return values:

1. `IResult` based - This includes `Task<IResult>` and `ValueTask<IResult>`
2. `string` - This includes `Task<string>` and `ValueTask<string>`
3. `T` (Any other type) - This includes `Task<T>` and `ValueTask<T>`

| Return value         | Behavior                                                                                                     | Content-Type                            |
| -------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| `IResult`            | The framework calls [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync) | Decided by the `IResult` implementation |
| `string`             | The framework writes the string directly to the response                                                     | `text/plain`                            |
| `T` (Any other type) | The framework JSON-serializes the response                                                                   | `application/json`                      |

For a more in-depth guide to route handler return values see [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0)

### Example return values

#### string return values

```
app.MapGet("/hello", () => "Hello World");
```

#### JSON return values

```
app.MapGet("/hello", () => new { Message = "Hello World" });
```

#### Return TypedResults

The following code returns a [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults):

```
app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));
```

Returning `TypedResults` is preferred to returning [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results). For more information, see [`TypedResults` versus `Results`](minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

#### IResult return values

```
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
```

The following example uses the built-in result types to customize the response:

```
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
```

#### JSON

```
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
```

#### Custom Status Code

```
app.MapGet("/405", () => Results.StatusCode(405));
```

#### Text

```
app.MapGet("/text", () => Results.Text("This is some text"));
```

[]()

#### Stream

```
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});
```

See [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0#stream7) for more examples.

#### Redirect

```
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
```

#### File

```
app.MapGet("/download", () => Results.File("myfile.text"));
```

[]()

### Built-in results

Common result helpers exist in the [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes. Returning `TypedResults` is preferred to returning `Results`. For more information, see [`TypedResults` versus `Results`](minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

### Modifying Headers

Use the `HttpResponse` object to modify response headers:

```
app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});
```

### Customizing results

Applications can control responses by implementing a custom [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) type. The following code is an example of an HTML result type:

```
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}
```

We recommend adding an extension method to [Microsoft.AspNetCore.Http.IResultExtensions](/en-us/dotnet/api/microsoft.aspnetcore.http.iresultextensions) to make these custom results more discoverable.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();
```

### Typed results

The [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) interface can represent values returned from Minimal APIs that don't utilize the implicit support for JSON serializing the returned object to the HTTP response. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class is used to create varying `IResult` objects that represent different types of responses. For example, setting the response status code or redirecting to another URL.

The types implementing `IResult` are public, allowing for type assertions when testing. For example:

```
[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}
```

You can look at the return types of the corresponding methods on the static [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) class to find the correct public `IResult` type to cast to.

See [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0) for more examples.

## Filters

For more information, see [Filters in Minimal API apps](minimal-apis/min-api-filters?view=aspnetcore-10.0).

## Authorization

Routes can be protected using authorization policies. These can be declared via the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute or by using the [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) method:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

The preceding code can be written with [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization):

```
app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();
```

The following sample uses [policy-based authorization](../security/authorization/policies?view=aspnetcore-10.0):

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

### Allow unauthenticated users to access an endpoint

The [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) allows unauthenticated users to access endpoints:

```
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();
```

## CORS

Routes can be [CORS](../security/cors?view=aspnetcore-6.0) enabled using [CORS policies](../security/cors?view=aspnetcore-6.0#cors-policy-options). CORS can be declared via the [`[EnableCors]`](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute or by using the [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) method. The following samples enable CORS:

```
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
```

```
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();
```

For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-6.0)

## ValidateScopes and ValidateOnBuild

[ValidateScopes](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validatescopes#microsoft-extensions-dependencyinjection-serviceprovideroptions-validatescopes) and [ValidateOnBuild](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validateonbuild#microsoft-extensions-dependencyinjection-serviceprovideroptions-validateonbuild) are enabled by default in the [Development](environments?view=aspnetcore-10.0) environment but disabled in other environments.

When `ValidateOnBuild` is `true`, the DI container validates the service configuration at build time. If the service configuration is invalid, the build fails at app startup, rather than at runtime when the service is requested.

When `ValidateScopes` is `true`, the DI container validates that a scoped service isn't resolved from the root scope. Resolving a scoped service from the root scope can result in a memory leak because the service is retained in memory longer than the scope of the request.

`ValidateScopes` and `ValidateOnBuild` are false by default in non-Development modes for performance reasons.

The following code shows `ValidateScopes` is enabled by default in development mode but disabled in release mode:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    // Intentionally getting service provider from app, not from the request
    // This causes an exception from attempting to resolve a scoped service
    // outside of a scope.
    // Throws System.InvalidOperationException:
    // 'Cannot resolve scoped service 'MyScopedService' from root provider.'
    var service = app.Services.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved");
});

app.Run();

public class MyScopedService { }
```

The following code shows `ValidateOnBuild` is enabled by default in development mode but disabled in release mode:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();

// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    var service = context.RequestServices.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved correctly!");
});

app.Run();

public class MyScopedService { }

public class AnotherService
{
    public AnotherService(BrokenService brokenService) { }
}

public class BrokenService { }
```

The following code disables `ValidateScopes` and `ValidateOnBuild` in `Development`:

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
    // Doesn't detect the validation problems because ValidateScopes is false.
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = false;
        options.ValidateOnBuild = false;
    });
}
```

## See also

* [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0)
* [Generate OpenAPI documents](openapi/aspnetcore-openapi?view=aspnetcore-10.0)
* [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0)
* [Filters in Minimal API apps](minimal-apis/min-api-filters?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0)
* [Authentication and authorization in Minimal APIs](minimal-apis/security?view=aspnetcore-10.0)
* [Test Minimal API apps](minimal-apis/test-min-api?view=aspnetcore-10.0)
* [Short-circuit routing](https://andrewlock.net/exploring-the-dotnet-8-preview-short-circuit-routing/)
* [Identity API endpoints](https://andrewlock.net/exploring-the-dotnet-8-preview-introducing-the-identity-api-endpoints/)
* [Keyed service dependency injection container support](https://andrewlock.net/exploring-the-dotnet-8-preview-keyed-services-dependency-injection-support/)
* [A look behind the scenes of Minimal API endpoints](https://andrewlock.net/behind-the-scenes-of-minimal-apis-1-a-first-look-behind-the-scenes-of-minimal-api-endpoints/)
* [Organizing ASP.NET Core Minimal APIs](https://www.tessferrandez.com/blog/2023/10/31/organizing-minimal-apis.html)
* [Fluent validation discussion on GitHub](https://github.com/dotnet/aspnetcore/issues/51834#issuecomment-1837180853)

This document provides a quick reference for Minimal APIs. For a guided introduction, see [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0).

The Minimal APIs consist of:

* [WebApplication and WebApplicationBuilder](minimal-apis/webapplication?view=aspnetcore-10.0)
* [Route Handlers](minimal-apis/route-handlers?view=aspnetcore-10.0)

## `WebApplication`

The following code is generated by an ASP.NET Core template:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code can be created via `dotnet new web` on the command line or selecting the Empty Web template in Visual Studio.

The following code creates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`) without explicitly creating a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder):

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults:

[WebApplication](minimal-apis/webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

* [UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.developerexceptionpagemiddleware) is added first when the [HostingEnvironment](environments?view=aspnetcore-10.0) is `"Development"`.

* [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) is added second, if the user code didn't already call `UseRouting` and endpoints are configured, for example `app.MapGet`.

* [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) is added at the end of the middleware pipeline if endpoints are configured.

* [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) is added immediately after `UseRouting`, if user code didn't already call `UseAuthentication` and if [IAuthenticationSchemeProvider](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationschemeprovider) can be detected in the service provider. `IAuthenticationSchemeProvider` is added by default when you use [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), and services are detected by using [IServiceProviderIsService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iserviceproviderisservice).

* [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) is added next, if user code didn't already call `UseAuthorization` and if [IAuthorizationHandlerProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandlerprovider) can be detected in the service provider. `IAuthorizationHandlerProvider` is added by default when you use [AddAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), and services are detected by using `IServiceProviderIsService`.

* User configured middleware and endpoints are added between `UseRouting` and `UseEndpoints`.

The following code is effectively what the automatic middleware being added to the app produces:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// User middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// End user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case because it's automatically added as described earlier:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// Other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.

* The app needs to call `UseRouting` and `UseEndpoints` so the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

For information on antiforgery middleware in Minimal APIs, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-10.0#afwma).

### Working with ports

When a web app is created with Visual Studio or `dotnet new`, a `Properties/launchSettings.json` file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog `Unable to connect to web server 'AppName'`. Visual Studio returns an error because it's expecting the port specified in `Properties/launchSettings.json`, but the app is using the port specified by `app.Run("http://localhost:3000")`. Run the following port changing samples from the command line.

The following sections set the app's port.

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");
```

In the preceding code, the app responds to port `3000`.

#### Multiple ports

In the following code, the app responds to port `3000` and `4000`:

```
var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Set the port from the command line

The following command makes the app respond to port `7777`:

```
dotnet run --urls="https://localhost:7777"
```

If the Kestrel endpoint is also configured in the `appsettings.json` file, the `appsettings.json` file specified URL is used. For more information, see [Kestrel endpoint configuration](configuration/?view=aspnetcore-10.0#kestrel-endpoint-configuration)

#### Read the port from environment

The following code reads the port from the environment:

```
var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");
```

The preferred way to set the port from the environment is to use the `ASPNETCORE_URLS` environment variable, which is shown in the following section.

#### Set the ports via the `ASPNETCORE_URLS` environment variable

The `ASPNETCORE_URLS` environment variable is available to set the port:

```
ASPNETCORE_URLS=http://localhost:3000
```

`ASPNETCORE_URLS` supports multiple URLs:

```
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
```

### Listen on all interfaces

The following samples demonstrate listening on all interfaces.

#### `http://*:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://+:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://0.0.0.0:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Listen on all interfaces using `ASPNETCORE_URLS`

The preceding samples can use `ASPNETCORE_URLS`:

```
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
```

### Listen on all interfaces using `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`

The preceding samples can also use `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`, but `ASPNETCORE_URLS` (when set) overrides the following settings:

```
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
```

For more information, see [Configure endpoints for Kestrel web server](servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### Specify HTTPS with development certificate

```
var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information on the development certificate, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../security/enforcing-ssl?view=aspnetcore-10.0#trust).

### Specify HTTPS using a custom certificate

The following sections show how to specify the custom certificate using the `appsettings.json` file and via configuration.

#### Specify the custom certificate with `appsettings.json`

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}
```

#### Specify the custom certificate via configuration

```
var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Use the certificate APIs

```
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Read the environment

```
var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();
```

For more information using the environment, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).

### Configuration

The following code reads from the configuration system:

```
var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();
```

For more information, see [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

### Logging

The following code writes a message to the log on application startup:

```
var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information, see [Logging in .NET and ASP.NET Core](logging/?view=aspnetcore-10.0).

### Access the Dependency Injection (DI) container

The following code shows how to get services from the DI container during application startup:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();
```

The following code shows how to access keys from the DI container using the [`[FromKeyedServices]` attribute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}
```

For more information on DI, see [Dependency injection in ASP.NET Core](dependency-injection?view=aspnetcore-10.0).

## `WebApplicationBuilder`

This section contains sample code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

### Change the content root, application name, and environment

The following code sets the content root, application name, and environment:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

[WebApplication.Create](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.create) initializes a new instance of the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) class with preconfigured defaults.

For more information, see [ASP.NET Core fundamentals overview](./?view=aspnetcore-10.0)

### Change the content root, app name, and environment by using environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| Feature          | Environment variable         | Command-line argument |
| ---------------- | ---------------------------- | --------------------- |
| Application name | `ASPNETCORE_APPLICATIONNAME` | `--applicationName`   |
| Environment name | `ASPNETCORE_ENVIRONMENT`     | `--environment`       |
| Content root     | `ASPNETCORE_CONTENTROOT`     | `--contentRoot`       |

### Add configuration providers

The following sample adds the INI configuration provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](configuration/?view=aspnetcore-10.0#file-configuration-provider) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

### Read configuration

By default the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) reads configuration from multiple sources, including:

* `appSettings.json` and `appSettings.{environment}.json`
* Environment variables
* The command line

For a complete list of configuration sources read, see [Default configuration](configuration/?view=aspnetcore-10.0#default-configuration) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

The following code reads `HelloKey` from configuration and displays the value at the `/` endpoint. If the configuration value is null, "Hello" is assigned to `message`:

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

### Read the environment

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Add logging providers

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();
```

### Add services

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

### Customize the `IHostBuilder`

Existing extension methods on [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) can be accessed using the [Host property](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.properties#microsoft-extensions-hosting-ihostbuilder-properties):

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Customize the `IWebHostBuilder`

Extension methods on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) can be accessed using the [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost) property.

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();
```

### Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed with `WebHostOptions`, the command line, or with the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();
```

### Custom dependency injection (DI) container

The following example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

### Add middleware

Any existing ASP.NET Core middleware can be configured on the `WebApplication`:

```
var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0).

### Developer exception page

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the [development environment](environments?view=aspnetcore-10.0), navigating to `/` renders a friendly page that shows the exception.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();
```

## ASP.NET Core middleware

The following table lists some of the middleware frequently used with Minimal APIs.

| Middleware                                                                                | Description                                                                                                          | API                                                                                                                                                                                                      |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Authentication](../security/authentication/?view=aspnetcore-6.0)                         | Provides authentication support.                                                                                     | [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)                                                                                           |
| [Authorization](../security/authorization/introduction?view=aspnetcore-10.0)              | Provides authorization support.                                                                                      | [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)                                                                                    |
| [CORS](../security/cors?view=aspnetcore-6.0)                                              | Configures Cross-Origin Resource Sharing.                                                                            | [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors)                                                                                                               |
| [Exception Handler](error-handling-api?view=aspnetcore-6.0)                               | Globally handles exceptions thrown by the middleware pipeline.                                                       | [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)                                                                                     |
| [Forwarded Headers](middleware/?view=aspnetcore-6.0#forwarded-headers-middleware-order)   | Forwards proxied headers onto the current request.                                                                   | [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders)                                                                                     |
| [HTTPS Redirection](../security/enforcing-ssl?view=aspnetcore-6.0)                        | Redirects all HTTP requests to HTTPS.                                                                                | [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)                                                                                   |
| [HTTP Strict Transport Security (HSTS)](middleware/?view=aspnetcore-6.0#middleware-order) | Security enhancement middleware that adds a special response header.                                                 | [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)                                                                                                                  |
| [Request Logging](http-logging/?view=aspnetcore-10.0)                                     | Provides support for logging HTTP requests and responses.                                                            | [UseHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usehttplogging)                                                                                             |
| [Request Timeouts](../performance/timeouts?view=aspnetcore-10.0)                          | Provides support for configuring request timeouts, global default and per endpoint.                                  | `UseRequestTimeouts`                                                                                                                                                                                     |
| [W3C Request Logging](https://www.w3.org/TR/WD-logfile.html)                              | Provides support for logging HTTP requests and responses in the [W3C format](https://www.w3.org/TR/WD-logfile.html). | [UseW3CLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usew3clogging)                                                                                               |
| [Response Caching](../performance/caching/middleware?view=aspnetcore-10.0)                | Provides support for caching responses.                                                                              | [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching)                                                                                        |
| [Response Compression](../performance/response-compression?view=aspnetcore-10.0)          | Provides support for compressing responses.                                                                          | [UseResponseCompression](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecompressionbuilderextensions.useresponsecompression)                                                                     |
| [Session](app-state?view=aspnetcore-10.0)                                                 | Provides support for managing user sessions.                                                                         | [UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)                                                                                                      |
| [Static Files](static-files?view=aspnetcore-10.0)                                         | Provides support for serving static files and directory browsing.                                                    | [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), [UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) |
| [WebSockets](websockets?view=aspnetcore-10.0)                                             | Enables the WebSockets protocol.                                                                                     | [UseWebSockets](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketmiddlewareextensions.usewebsockets)                                                                                              |

The following sections cover request handling: routing, parameter binding, and responses.

## Routing

A configured `WebApplication` supports `Map{Verb}` and [MapMethods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapmethods) where `{Verb}` is a camel-cased HTTP method like `Get`, `Post`, `Put`, or `Delete`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();
```

The [Delegate](/en-us/dotnet/api/system.delegate) arguments passed to these methods are called "route handlers".

### Route Handlers

Route handlers are methods that execute when the route matches. Route handlers can be a lambda expression, a local function, an instance method, or a static method. Route handlers can be synchronous or asynchronous.

The following sections provide examples of different route handlers.

### Lambda expression

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();
```

### Local function

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();
```

### Instance method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}
```

### Static method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}
```

### Endpoint defined outside of Program.cs

Minimal APIs don't have to be located in the *Program.cs* file. For example, you can set up the structure in the *Program.cs* file, and define the endpoint in a separate file:

**Program.cs**

```
using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();
```

**TodoEndpoints.cs**

```
namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}
```

For more information, see the [Route groups](#route-groups) section later in this article.

### Named endpoints and link generation

You can supply a name for your endpoints to generate URLs that target the endpoint. Using a named endpoint avoids having to hard code paths in an app:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();
```

The preceding code displays the message *`The link to the hello route is /hello`* from the `/` (forward slash) endpoint.

#### Criteria for endpoint names

Endpoint names must satisfy the following criteria:

* Endpoint names are case sensitive.
* Endpoint names must be globally unique.
* Endpoint names are used as the OpenAPI operation identifier (ID) when OpenAPI support is enabled. For more information, see [Generate OpenAPI documents](openapi/aspnetcore-openapi?view=aspnetcore-10.0).

### Route parameters

Route parameters can be captured as part of the route pattern definition:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();
```

The preceding code returns the message *The user id is 3 and book id is 7* from the URI `/users/3/books/7`.

The route handler can declare the parameters to capture. When a request is made to a route with parameters declared to capture, the parameters are parsed and passed to the handler. This approach makes it easy to capture the values in a type-safe way. In the preceding code, the `userId` and `bookId` parameters are both type `int`.

In the preceding code, if either route value can't be converted to an `int`, an exception is thrown. The GET request `/users/hello/books/3` throws the following exception:

```
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
```

### Wildcard and catch all routes

The following catch all route returns *Routing to hello* from the `/posts/hello` endpoint:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();
```

### Route constraints

Route constraints restrict the matching behavior of a route.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();
```

The following table demonstrates the preceding route templates and their behavior.

| Route template                       | Example matching URI |
| ------------------------------------ | -------------------- |
| `/todos/{id:int}`                    | `/todos/1`           |
| `/todos/{text}`                      | `/todos/something`   |
| `/posts/{slug:regex(^[a-z0-9_-]+$)}` | `/posts/mypost`      |

For more information, see [Route constraint reference](routing?view=aspnetcore-10.0#route-constraints) in [Routing in ASP.NET Core](routing?view=aspnetcore-10.0).

### Route groups

The [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) extension method helps organize groups of endpoints with a common prefix and reduces repetitive code. Use this method to customize entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata) that add [endpoint metadata](routing?view=aspnetcore-10.0#endpoint-metadata).

For example, the following code creates two similar groups of endpoints:

```
app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
```

```
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}
```

In this scenario, you can use a relative address for the `Location` header in the `201 Created` result:

```
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}
```

The first group of endpoints matches only requests prefixed with `/public/todos` and are accessible without any authentication. The second group of endpoints matches only requests prefixed with `/private/todos` and require authentication.

`QueryPrivateTodos` is a local function that modifies the `TodoDb` parameters of the route handler, to enable them to access and store private todo data. `QueryPrivateTodos` serves as an [endpoint filter factory](minimal-apis/min-api-filters?view=aspnetcore-10.0).

Route groups also support nested groups and complex prefix patterns with route parameters and constraints. In the following example, the route handler mapped to the `user` group can capture the `{org}` and `{group}` route parameters defined in the outer group prefixes.

The prefix can also be empty. This approach can be useful for adding endpoint metadata or filters to a group of endpoints without changing the route pattern.

```
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
```

Adding filters or metadata to a group results in the same behavior as adding them individually to each endpoint (before adding extra filters or metadata that might exist in an inner group or specific endpoint).

```
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});
```

In the preceding example, the outer filter logs the incoming request before the inner filter even though the outer filter is added second. Because the filters are applied to different groups, the order that they're added relative to each other doesn't matter. The order in which filters are added matters when applied to the same group or specific endpoint.

A request to `/outer/inner/` logs the following data:

```
/outer group filter
/inner group filter
MapGet filter
```

## Parameter binding

The following GET route handler uses some of these parameter binding sources:

```
var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }
```

### Key parameter binding features

* **Explicit binding**: Use attributes like `[FromRoute]`, `[FromQuery]`, `[FromHeader]`, `[FromBody]`, `[FromForm]`, and `[FromServices]` to explicitly specify binding sources.
* **Form binding**: Bind form values using `[FromForm]` attribute, including support for `IFormFile` and `IFormFileCollection` for file uploads.
* **Complex types**: Bind to collections and complex types from forms, query strings, and headers.
* **Custom binding**: Implement custom binding logic using `TryParse`, `BindAsync`, or the `IBindableFromHttpContext<T>` interface.
* **Optional parameters**: Support nullable types and default values for optional parameters.
* **Dependency injection**: Parameters are automatically bound from services registered in the DI container.
* **Special types**: Automatic binding for `HttpContext`, `HttpRequest`, `HttpResponse`, `CancellationToken`, `ClaimsPrincipal`, `Stream`, and `PipeReader`.

***

**Learn more:** For detailed information on parameter binding including advanced scenarios, validation, binding precedence, and troubleshooting, see [Parameter binding in Minimal API applications](minimal-apis/parameter-binding?view=aspnetcore-10.0).

## Responses

Route handlers support the following types of return values:

1. `IResult` based - This includes `Task<IResult>` and `ValueTask<IResult>`
2. `string` - This includes `Task<string>` and `ValueTask<string>`
3. `T` (Any other type) - This includes `Task<T>` and `ValueTask<T>`

| Return value         | Behavior                                                                                                     | Content-Type                            |
| -------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| `IResult`            | The framework calls [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync) | Decided by the `IResult` implementation |
| `string`             | The framework writes the string directly to the response                                                     | `text/plain`                            |
| `T` (Any other type) | The framework JSON-serializes the response                                                                   | `application/json`                      |

For a more in-depth guide to route handler return values see [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0)

### Example return values

#### string return values

```
app.MapGet("/hello", () => "Hello World");
```

#### JSON return values

```
app.MapGet("/hello", () => new { Message = "Hello World" });
```

#### Return TypedResults

The following code returns a [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults):

```
app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));
```

Returning `TypedResults` is preferred to returning [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results). For more information, see [`TypedResults` versus `Results`](minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

#### IResult return values

```
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
```

The following example uses the built-in result types to customize the response:

```
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
```

#### JSON

```
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
```

#### Custom Status Code

```
app.MapGet("/405", () => Results.StatusCode(405));
```

#### Text

```
app.MapGet("/text", () => Results.Text("This is some text"));
```

[]()

#### Stream

```
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});
```

See [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0#stream7) for more examples.

#### Redirect

```
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
```

#### File

```
app.MapGet("/download", () => Results.File("myfile.text"));
```

[]()

### Built-in results

Common result helpers exist in the [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes. Returning `TypedResults` is preferred to returning `Results`. For more information, see [`TypedResults` versus `Results`](minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

### Modifying Headers

Use the `HttpResponse` object to modify response headers:

```
app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});
```

### Customizing results

Applications can control responses by implementing a custom [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) type. The following code is an example of an HTML result type:

```
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}
```

We recommend adding an extension method to [Microsoft.AspNetCore.Http.IResultExtensions](/en-us/dotnet/api/microsoft.aspnetcore.http.iresultextensions) to make these custom results more discoverable.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();
```

### Typed results

The [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) interface can represent values returned from Minimal APIs that don't utilize the implicit support for JSON serializing the returned object to the HTTP response. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class is used to create varying `IResult` objects that represent different types of responses. For example, setting the response status code or redirecting to another URL.

The types implementing `IResult` are public, allowing for type assertions when testing. For example:

```
[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}
```

You can look at the return types of the corresponding methods on the static [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) class to find the correct public `IResult` type to cast to.

See [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0) for more examples.

## Filters

For more information, see [Filters in Minimal API apps](minimal-apis/min-api-filters?view=aspnetcore-10.0).

## Authorization

Routes can be protected using authorization policies. These can be declared via the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute or by using the [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) method:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

The preceding code can be written with [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization):

```
app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();
```

The following sample uses [policy-based authorization](../security/authorization/policies?view=aspnetcore-10.0):

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

### Allow unauthenticated users to access an endpoint

The [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) allows unauthenticated users to access endpoints:

```
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();
```

## CORS

Routes can be [CORS](../security/cors?view=aspnetcore-6.0) enabled using [CORS policies](../security/cors?view=aspnetcore-6.0#cors-policy-options). CORS can be declared via the [`[EnableCors]`](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute or by using the [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) method. The following samples enable CORS:

```
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
```

```
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();
```

For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-6.0)

## ValidateScopes and ValidateOnBuild

[ValidateScopes](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validatescopes#microsoft-extensions-dependencyinjection-serviceprovideroptions-validatescopes) and [ValidateOnBuild](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validateonbuild#microsoft-extensions-dependencyinjection-serviceprovideroptions-validateonbuild) are enabled by default in the [Development](environments?view=aspnetcore-10.0) environment but disabled in other environments.

When `ValidateOnBuild` is `true`, the DI container validates the service configuration at build time. If the service configuration is invalid, the build fails at app startup, rather than at runtime when the service is requested.

When `ValidateScopes` is `true`, the DI container validates that a scoped service isn't resolved from the root scope. Resolving a scoped service from the root scope can result in a memory leak because the service is retained in memory longer than the scope of the request.

`ValidateScopes` and `ValidateOnBuild` are false by default in non-Development modes for performance reasons.

The following code shows `ValidateScopes` is enabled by default in development mode but disabled in release mode:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    // Intentionally getting service provider from app, not from the request
    // This causes an exception from attempting to resolve a scoped service
    // outside of a scope.
    // Throws System.InvalidOperationException:
    // 'Cannot resolve scoped service 'MyScopedService' from root provider.'
    var service = app.Services.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved");
});

app.Run();

public class MyScopedService { }
```

The following code shows `ValidateOnBuild` is enabled by default in development mode but disabled in release mode:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();

// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    var service = context.RequestServices.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved correctly!");
});

app.Run();

public class MyScopedService { }

public class AnotherService
{
    public AnotherService(BrokenService brokenService) { }
}

public class BrokenService { }
```

The following code disables `ValidateScopes` and `ValidateOnBuild` in `Development`:

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
    // Doesn't detect the validation problems because ValidateScopes is false.
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = false;
        options.ValidateOnBuild = false;
    });
}
```

## See also

* [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0)
* [Generate OpenAPI documents](openapi/aspnetcore-openapi?view=aspnetcore-10.0)
* [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0)
* [Filters in Minimal API apps](minimal-apis/min-api-filters?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0)
* [Authentication and authorization in Minimal APIs](minimal-apis/security?view=aspnetcore-10.0)
* [Test Minimal API apps](minimal-apis/test-min-api?view=aspnetcore-10.0)
* [Short-circuit routing](https://andrewlock.net/exploring-the-dotnet-8-preview-short-circuit-routing/)
* [Identity API endpoints](https://andrewlock.net/exploring-the-dotnet-8-preview-introducing-the-identity-api-endpoints/)
* [Keyed service dependency injection container support](https://andrewlock.net/exploring-the-dotnet-8-preview-keyed-services-dependency-injection-support/)
* [A look behind the scenes of Minimal API endpoints](https://andrewlock.net/behind-the-scenes-of-minimal-apis-1-a-first-look-behind-the-scenes-of-minimal-api-endpoints/)
* [Organizing ASP.NET Core Minimal APIs](https://www.tessferrandez.com/blog/2023/10/31/organizing-minimal-apis.html)
* [Fluent validation discussion on GitHub](https://github.com/dotnet/aspnetcore/issues/51834#issuecomment-1837180853)

This document provides a quick reference for Minimal APIs. For a guided introduction, see [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0).

The Minimal APIs consist of:

* [WebApplication and WebApplicationBuilder](minimal-apis/webapplication?view=aspnetcore-10.0)
* [Route Handlers](minimal-apis/route-handlers?view=aspnetcore-10.0)

## `WebApplication`

The following code is generated by an ASP.NET Core template:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code can be created via `dotnet new web` on the command line or selecting the Empty Web template in Visual Studio.

The following code creates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`) without explicitly creating a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder):

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults:

[`WebApplication`](minimal-apis/webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

* [`UseDeveloperExceptionPage`](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.developerexceptionpagemiddleware) is added first when the [`HostingEnvironment`](environments?view=aspnetcore-10.0) is `"Development"`.
* [`UseRouting`](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) is added second if user code didn't already call `UseRouting` and if there are endpoints configured, for example `app.MapGet`.
* [`UseEndpoints`](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) is added at the end of the middleware pipeline if any endpoints are configured.
* [`UseAuthentication`](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) is added immediately after `UseRouting` if user code didn't already call `UseAuthentication` and if [`IAuthenticationSchemeProvider`](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationschemeprovider) can be detected in the service provider. `IAuthenticationSchemeProvider` is added by default when using [`AddAuthentication`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), and services are detected using [`IServiceProviderIsService`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iserviceproviderisservice).
* [`UseAuthorization`](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) is added next if user code didn't already call `UseAuthorization` and if [`IAuthorizationHandlerProvider`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandlerprovider) can be detected in the service provider. `IAuthorizationHandlerProvider` is added by default when using [`AddAuthorization`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), and services are detected using `IServiceProviderIsService`.
* User configured middleware and endpoints are added between `UseRouting` and `UseEndpoints`.

The following code is effectively what the automatic middleware being added to the app produces:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should be run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case as it is automatically added as described previously:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.
* The app needs to call `UseRouting` and `UseEndpoints` so that the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

### Working with ports

When a web app is created with Visual Studio or `dotnet new`, a `Properties/launchSettings.json` file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog `Unable to connect to web server 'AppName'`. Visual Studio returns an error because it's expecting the port specified in `Properties/launchSettings.json`, but the app is using the port specified by `app.Run("http://localhost:3000")`. Run the following port changing samples from the command line.

The following sections set the app's port.

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");
```

In the preceding code, the app responds to port `3000`.

#### Multiple ports

In the following code, the app responds to port `3000` and `4000`:

```
var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Set the port from the command line

The following command makes the app respond to port `7777`:

```
dotnet run --urls="https://localhost:7777"
```

If the Kestrel endpoint is also configured in the `appsettings.json` file, the `appsettings.json` file specified URL is used. For more information, see [Kestrel endpoint configuration](configuration/?view=aspnetcore-10.0#kestrel-endpoint-configuration)

#### Read the port from environment

The following code reads the port from the environment:

```
var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");
```

The preferred way to set the port from the environment is to use the `ASPNETCORE_URLS` environment variable, which is shown in the following section.

#### Set the ports via the `ASPNETCORE_URLS` environment variable

The `ASPNETCORE_URLS` environment variable is available to set the port:

```
ASPNETCORE_URLS=http://localhost:3000
```

`ASPNETCORE_URLS` supports multiple URLs:

```
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
```

For more information using the environment, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).

### Listen on all interfaces

The following samples demonstrate listening on all interfaces

#### `http://*:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://+:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://0.0.0.0:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Listen on all interfaces using `ASPNETCORE_URLS`

The preceding samples can use `ASPNETCORE_URLS`:

```
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
```

### Specify HTTPS with development certificate

```
var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information on the development certificate, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../security/enforcing-ssl?view=aspnetcore-10.0#trust).

### Specify HTTPS using a custom certificate

The following sections show how to specify the custom certificate using the `appsettings.json` file and via configuration.

#### Specify the custom certificate with `appsettings.json`

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}
```

#### Specify the custom certificate via configuration

```
var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Use the certificate APIs

```
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Configuration

The following code reads from the configuration system:

```
var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();
```

For more information, see [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

### Logging

The following code writes a message to the log on application startup:

```
var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information, see [Logging in .NET and ASP.NET Core](logging/?view=aspnetcore-10.0).

### Access the Dependency Injection (DI) container

The following code shows how to get services from the DI container during application startup:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();
```

For more information, see [Dependency injection in ASP.NET Core](dependency-injection?view=aspnetcore-10.0).

## `WebApplicationBuilder`

This section contains sample code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

### Change the content root, application name, and environment

The following code sets the content root, application name, and environment:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

[WebApplication.Create](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.create) initializes a new instance of the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) class with preconfigured defaults.

For more information, see [ASP.NET Core fundamentals overview](./?view=aspnetcore-10.0).

### Change the content root, app name, and environment by environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| Feature          | Environment variable         | Command-line argument |
| ---------------- | ---------------------------- | --------------------- |
| Application name | `ASPNETCORE_APPLICATIONNAME` | `--applicationName`   |
| Environment name | `ASPNETCORE_ENVIRONMENT`     | `--environment`       |
| Content root     | `ASPNETCORE_CONTENTROOT`     | `--contentRoot`       |

### Add configuration providers

The following sample adds the INI configuration provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](configuration/?view=aspnetcore-10.0#file-configuration-provider) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

### Read configuration

By default the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) reads configuration from multiple sources, including:

* `appSettings.json` and `appSettings.{environment}.json`
* Environment variables
* The command line

The following code reads `HelloKey` from configuration and displays the value at the `/` endpoint. If the configuration value is null, "Hello" is assigned to `message`:

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

For a complete list of configuration sources read, see [Default configuration](configuration/?view=aspnetcore-10.0#default-configuration) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).

### Add logging providers

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();
```

### Add services

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

### Customize the `IHostBuilder`

Existing extension methods on [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) can be accessed using the [Host property](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.properties#microsoft-extensions-hosting-ihostbuilder-properties):

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Customize the `IWebHostBuilder`

Extension methods on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) can be accessed using the [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost) property.

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();
```

### Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed with `WebHostOptions`, the command line, or with the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();
```

### Custom dependency injection (DI) container

The following example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

### Add middleware

Any existing ASP.NET Core middleware can be configured on the `WebApplication`:

```
var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0).

### Developer exception page

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the [development environment](environments?view=aspnetcore-10.0), navigating to `/` renders a friendly page that shows the exception.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();
```

## ASP.NET Core middleware

The following table lists some of the middleware frequently used with Minimal APIs.

| Middleware                                                                                | Description                                                                                                          | API                                                                                                                                                                                                      |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Authentication](../security/authentication/?view=aspnetcore-6.0)                         | Provides authentication support.                                                                                     | [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)                                                                                           |
| [Authorization](../security/authorization/introduction?view=aspnetcore-10.0)              | Provides authorization support.                                                                                      | [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)                                                                                    |
| [CORS](../security/cors?view=aspnetcore-6.0)                                              | Configures Cross-Origin Resource Sharing.                                                                            | [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors)                                                                                                               |
| [Exception Handler](error-handling-api?view=aspnetcore-6.0)                               | Globally handles exceptions thrown by the middleware pipeline.                                                       | [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)                                                                                     |
| [Forwarded Headers](middleware/?view=aspnetcore-6.0#forwarded-headers-middleware-order)   | Forwards proxied headers onto the current request.                                                                   | [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders)                                                                                     |
| [HTTPS Redirection](../security/enforcing-ssl?view=aspnetcore-6.0)                        | Redirects all HTTP requests to HTTPS.                                                                                | [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)                                                                                   |
| [HTTP Strict Transport Security (HSTS)](middleware/?view=aspnetcore-6.0#middleware-order) | Security enhancement middleware that adds a special response header.                                                 | [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)                                                                                                                  |
| [Request Logging](http-logging/?view=aspnetcore-10.0)                                     | Provides support for logging HTTP requests and responses.                                                            | [UseHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usehttplogging)                                                                                             |
| [Request Timeouts](../performance/timeouts?view=aspnetcore-10.0)                          | Provides support for configuring request timeouts, global default and per endpoint.                                  | `UseRequestTimeouts`                                                                                                                                                                                     |
| [W3C Request Logging](https://www.w3.org/TR/WD-logfile.html)                              | Provides support for logging HTTP requests and responses in the [W3C format](https://www.w3.org/TR/WD-logfile.html). | [UseW3CLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usew3clogging)                                                                                               |
| [Response Caching](../performance/caching/middleware?view=aspnetcore-10.0)                | Provides support for caching responses.                                                                              | [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching)                                                                                        |
| [Response Compression](../performance/response-compression?view=aspnetcore-10.0)          | Provides support for compressing responses.                                                                          | [UseResponseCompression](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecompressionbuilderextensions.useresponsecompression)                                                                     |
| [Session](app-state?view=aspnetcore-10.0)                                                 | Provides support for managing user sessions.                                                                         | [UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)                                                                                                      |
| [Static Files](static-files?view=aspnetcore-10.0)                                         | Provides support for serving static files and directory browsing.                                                    | [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), [UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) |
| [WebSockets](websockets?view=aspnetcore-10.0)                                             | Enables the WebSockets protocol.                                                                                     | [UseWebSockets](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketmiddlewareextensions.usewebsockets)                                                                                              |

The following sections cover request handling: routing, parameter binding, and responses.

## Routing

A configured `WebApplication` supports `Map{Verb}` and [MapMethods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapmethods) where `{Verb}` is a camel-cased HTTP method like `Get`, `Post`, `Put`, or `Delete`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();
```

The [Delegate](/en-us/dotnet/api/system.delegate) arguments passed to these methods are called "route handlers".

### Route Handlers

Route handlers are methods that execute when the route matches. Route handlers can be a lambda expression, a local function, an instance method, or a static method. Route handlers can be synchronous or asynchronous.

The following sections provide examples of different route handlers.

### Lambda expression

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();
```

### Local function

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();
```

### Instance method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}
```

### Static method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}
```

### Endpoint defined outside of Program.cs

Minimal APIs don't have to be located in the *Program.cs* file. For example, you can set up the structure in the *Program.cs* file, and define the endpoint in a separate file:

**Program.cs**

```
using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();
```

**TodoEndpoints.cs**

```
namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}
```

For more information, see the [Route groups](#route-groups) section later in this article.

### Named endpoints and link generation

You can supply a name for your endpoints to generate URLs that target the endpoint. Using a named endpoint avoids having to hard code paths in an app:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();
```

The preceding code displays the message *`The link to the hello route is /hello`* from the `/` (forward slash) endpoint.

#### Criteria for endpoint names

Endpoint names must satisfy the following criteria:

* Endpoint names are case sensitive.
* Endpoint names must be globally unique.
* Endpoint names are used as the OpenAPI operation identifier (ID) when OpenAPI support is enabled. For more information, see [Generate OpenAPI documents](openapi/aspnetcore-openapi?view=aspnetcore-10.0).

### Route parameters

Route parameters can be captured as part of the route pattern definition:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();
```

The preceding code returns the message *The user id is 3 and book id is 7* from the URI `/users/3/books/7`.

The route handler can declare the parameters to capture. When a request is made to a route with parameters declared to capture, the parameters are parsed and passed to the handler. This approach makes it easy to capture the values in a type-safe way. In the preceding code, the `userId` and `bookId` parameters are both type `int`.

In the preceding code, if either route value can't be converted to an `int`, an exception is thrown. The GET request `/users/hello/books/3` throws the following exception:

```
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
```

### Wildcard and catch all routes

The following catch all route returns *Routing to hello* from the `/posts/hello` endpoint:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();
```

### Route constraints

Route constraints restrict the matching behavior of a route.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();
```

The following table demonstrates the preceding route templates and their behavior.

| Route template                       | Example matching URI |
| ------------------------------------ | -------------------- |
| `/todos/{id:int}`                    | `/todos/1`           |
| `/todos/{text}`                      | `/todos/something`   |
| `/posts/{slug:regex(^[a-z0-9_-]+$)}` | `/posts/mypost`      |

For more information, see [Route constraint reference](routing?view=aspnetcore-10.0#route-constraints) in [Routing in ASP.NET Core](routing?view=aspnetcore-10.0).

### Route groups

The [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) extension method helps organize groups of endpoints with a common prefix and reduces repetitive code. Use this method to customize entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata) that add [endpoint metadata](routing?view=aspnetcore-10.0#endpoint-metadata).

For example, the following code creates two similar groups of endpoints:

```
app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
```

```
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}
```

In this scenario, you can use a relative address for the `Location` header in the `201 Created` result:

```
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}
```

The first group of endpoints matches only requests prefixed with `/public/todos` and are accessible without any authentication. The second group of endpoints matches only requests prefixed with `/private/todos` and require authentication.

`QueryPrivateTodos` is a local function that modifies the `TodoDb` parameters of the route handler, to enable them to access and store private todo data. `QueryPrivateTodos` serves as an [endpoint filter factory](minimal-apis/min-api-filters?view=aspnetcore-10.0).

Route groups also support nested groups and complex prefix patterns with route parameters and constraints. In the following example, the route handler mapped to the `user` group can capture the `{org}` and `{group}` route parameters defined in the outer group prefixes.

The prefix can also be empty. This approach can be useful for adding endpoint metadata or filters to a group of endpoints without changing the route pattern.

```
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
```

Adding filters or metadata to a group results in the same behavior as adding them individually to each endpoint (before adding extra filters or metadata that might exist in an inner group or specific endpoint).

```
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});
```

In the preceding example, the outer filter logs the incoming request before the inner filter even though the outer filter is added second. Because the filters are applied to different groups, the order that they're added relative to each other doesn't matter. The order in which filters are added matters when applied to the same group or specific endpoint.

A request to `/outer/inner/` logs the following data:

```
/outer group filter
/inner group filter
MapGet filter
```

## Parameter binding

```
var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }
```

The following table shows the relationship between the parameters used in the preceding example and the associated binding sources.

| Parameter      | Binding Source                   |
| -------------- | -------------------------------- |
| `id`           | route value                      |
| `page`         | query string                     |
| `customHeader` | header                           |
| `service`      | Provided by dependency injection |

The HTTP methods `GET`, `HEAD`, `OPTIONS`, and `DELETE` don't implicitly bind from body. To bind from body (as JSON) for these HTTP methods, [bind explicitly](#explicit-parameter-binding) with [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) or read from the [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest).

The following example POST route handler uses a binding source of body (as JSON) for the `person` parameter:

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);
```

The parameters in the preceding examples are all bound from request data automatically. To demonstrate the convenience that parameter binding provides, the following route handlers show how to read request data directly from the request:

```
app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});
```

### Explicit Parameter Binding

Attributes can be used to explicitly declare where parameters are bound from.

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
```

| Parameter     | Binding Source                        |
| ------------- | ------------------------------------- |
| `id`          | route value with the name `id`        |
| `page`        | query string with the name `"p"`      |
| `service`     | Provided by dependency injection      |
| `contentType` | header with the name `"Content-Type"` |

Note

Binding from form values is ***not*** natively supported in .NET 6 and 7.

### Parameter binding with dependency injection

Parameter binding for Minimal APIs binds parameters through [dependency injection](dependency-injection?view=aspnetcore-10.0) when the type is configured as a service. It's not necessary to explicitly apply the [`[FromServices]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) attribute to a parameter. In the following code, both actions return the time:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
```

### Optional parameters

Parameters declared in route handlers are treated as required:

* If a request matches the route, the route handler only runs if all required parameters are provided in the request.
* Failure to provide all required parameters results in an error.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
```

| URI                      | result                                                                                             |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `/products?pageNumber=3` | 3 returned                                                                                         |
| `/products`              | `BadHttpRequestException`: Required parameter "int pageNumber" was not provided from query string. |
| `/products/1`            | HTTP 404 error, no matching route                                                                  |

To make `pageNumber` optional, define the type as optional or provide a default value:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
```

| URI                      | result     |
| ------------------------ | ---------- |
| `/products?pageNumber=3` | 3 returned |
| `/products`              | 1 returned |
| `/products2`             | 1 returned |

The preceding nullable and default value applies to all sources:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();
```

The preceding code calls the method with a null product if no request body is sent.

**NOTE**: If invalid data is provided and the parameter is nullable, the route handler is ***not*** run.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
```

| URI                        | result                                                                                       |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `/products?pageNumber=3`   | `3` returned                                                                                 |
| `/products`                | `1` returned                                                                                 |
| `/products?pageNumber=two` | `BadHttpRequestException`: Failed to bind parameter `"Nullable<int> pageNumber"` from "two". |
| `/products/two`            | HTTP 404 error, no matching route                                                            |

See the [Binding Failures](#bf) section for more information.

### Special types

The following types are bound without explicit attributes:

* [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext): The context which holds all the information about the current HTTP request or response:

  ```
  app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
  ```

* [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) and [HttpResponse](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse): The HTTP request and HTTP response:

  ```
  app.MapGet("/", (HttpRequest request, HttpResponse response) =>
      response.WriteAsync($"Hello World {request.Query["name"]}"));
  ```

* [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken): The cancellation token associated with the current HTTP request:

  ```
  app.MapGet("/", async (CancellationToken cancellationToken) => 
      await MakeLongRunningRequestAsync(cancellationToken));
  ```

* [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal): The user associated with the request, bound from [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user):

  ```
  app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
  ```

```
using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}
```

The following code binds the request body to a `Stream`:

```
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
```

The following code shows the complete `Program.cs` file:

```
using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
```

* When reading data, the `Stream` is the same object as `HttpRequest.Body`.
* The request body isn't buffered by default. After the body is read, it's not rewindable. The stream can't be read multiple times.
* The `Stream` and `PipeReader` aren't usable outside of the minimal action handler as the underlying buffers will be disposed or reused.

#### File uploads using IFormFile and IFormFileCollection

The following code uses [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection) to upload file:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();
```

Authenticated file upload requests are supported using an [Authorization header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Authorization), a [client certificate](/en-us/aspnet/core/security/authentication/certauth), or a cookie header.

There is no built-in support for [antiforgery](/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-7.0\&preserve-view=true#anti7) in ASP.NET Core in .NET 7. [Antiforgery is available in ASP.NET Core in .NET 8](/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-8.0\&preserve-view=true#bind8) or later. However, it can be implemented using the [`IAntiforgery` service](/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-7.0\&preserve-view=true#antimin7).

[]()

### Bind arrays and string values from headers and query strings

The following code demonstrates binding query strings to an array of primitive types, string arrays, and [StringValues](/en-us/dotnet/api/microsoft.extensions.primitives.stringvalues):

```
// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
```

Binding query strings or header values to an array of complex types is supported when the type has `TryParse` implemented. The following code binds to a string array and returns all the items with the specified tags:

```
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});
```

The following code shows the model and the required `TryParse` implementation:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}
```

The following code binds to an `int` array:

```
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});
```

To test the preceding code, add the following endpoint to populate the database with `Todo` items:

```
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});
```

Use an API testing tool like [`HttpRepl`](../web-api/http-repl/?view=aspnetcore-10.0) to pass the following data to the previous endpoint:

```
[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]
```

The following code binds to the header key `X-Todo-Id` and returns the `Todo` items with matching `Id` values:

```
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});
```

Note

When binding a non-nullable `string[]` from a query string, the absence of any matching query string value results in an empty array instead of `null`. If the parameter is declared as nullable (for example, `string[]?`) or has a default value of `null` (for example, `[FromQuery] string[]? names = null`), omitting the query parameter results in `null`.

[]()

### Parameter binding for argument lists with \[AsParameters]

[AsParametersAttribute](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute) enables simple parameter binding to types and not complex or recursive model binding.

Consider the following code:

```
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.
```

Consider the following `GET` endpoint:

```
app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
```

The following `struct` can be used to replace the preceding highlighted parameters:

```
struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}
```

The refactored `GET` endpoint uses the preceding `struct` with the [AsParameters](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute?view=aspnetcore-7.0\&preserve-view=true) attribute:

```
app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
```

The following code shows additional endpoints in the app:

```
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.Name
    };

    Db.Todos.Add(todoItem);
    await Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.IsComplete;

    await Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
    if (await Db.Todos.FindAsync(Id) is Todo todo)
    {
        Db.Todos.Remove(todo);
        await Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});
```

The following classes are used to refactor the parameter lists:

```
class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}
```

The following code shows the refactored endpoints using `AsParameters` and the preceding `struct` and classes:

```
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});
```

The following [`record`](/en-us/dotnet/csharp/language-reference/builtin-types/record) types can be used to replace the preceding parameters:

```
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
```

```
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
```

The following code displays `Point: 12.3, 10.1` with the URI `/map?Point=12.3,10.1`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}
```

#### BindAsync

`BindAsync` has the following APIs:

```
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
```

The following code displays `SortBy:xyz, SortDirection:Desc, CurrentPage:99` with the URI `/products?SortBy=xyz&SortDir=Desc&Page=99`:

```
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}
```

[]()

#### Custom parameter binding with `IBindableFromHttpContext`

ASP.NET Core provides support for custom parameter binding in Minimal APIs using the [IBindableFromHttpContext\<TSelf>](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1) interface. This interface, introduced with C# 11's static abstract members, allows you to create types that can be bound from an HTTP context directly in route handler parameters.

```
public interface IBindableFromHttpContext<TSelf>
    where TSelf : class, IBindableFromHttpContext<TSelf>
{
    static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}
```

By implementing the [IBindableFromHttpContext\<TSelf>](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1) interface, you can create custom types that handle their own binding logic from the HttpContext. When a route handler includes a parameter of this type, the framework automatically calls the static BindAsync method to create the instance:

```
using CustomBindingExample;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/", () => "Hello, IBindableFromHttpContext example!");

app.MapGet("/custom-binding", (CustomBoundParameter param) =>
{
    return $"Value from custom binding: {param.Value}";
});

app.MapGet("/combined/{id}", (int id, CustomBoundParameter param) =>
{
    return $"ID: {id}, Custom Value: {param.Value}";
});
```

The following is an example implementation of a custom parameter that binds from an HTTP header:

```
using System.Reflection;

namespace CustomBindingExample;

public class CustomBoundParameter : IBindableFromHttpContext<CustomBoundParameter>
{
    public string Value { get; init; } = default!;

    public static ValueTask<CustomBoundParameter?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        // Custom binding logic here
        // This example reads from a custom header
        var value = context.Request.Headers["X-Custom-Header"].ToString();
        
        // If no header was provided, you could fall back to a query parameter
        if (string.IsNullOrEmpty(value))
        {
            value = context.Request.Query["customValue"].ToString();
        }
        
        return ValueTask.FromResult<CustomBoundParameter?>(new CustomBoundParameter 
        {
            Value = value
        });
    }
}
```

You can also implement validation within your custom binding logic:

```
app.MapGet("/validated", (ValidatedParameter param) =>
{
    if (string.IsNullOrEmpty(param.Value))
    {
        return Results.BadRequest("Value cannot be empty");
    }
    
    return Results.Ok($"Validated value: {param.Value}");
});
```

[View or download the sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/minimal-apis/samples/7.0/CustomBindingExample) ([how to download](../overview?view=aspnetcore-10.0#how-to-download-a-sample))

### Binding failures

When binding fails, the framework logs a debug message and returns various status codes to the client depending on the failure mode.

| Failure mode                                | Nullable Parameter Type | Binding Source     | Status code |
| ------------------------------------------- | ----------------------- | ------------------ | ----------- |
| `{ParameterType}.TryParse` returns `false`  | yes                     | route/query/header | 400         |
| `{ParameterType}.BindAsync` returns `null`  | yes                     | custom             | 400         |
| `{ParameterType}.BindAsync` throws          | does not matter         | custom             | 500         |
| Failure to deserialize JSON body            | does not matter         | body               | 400         |
| Wrong content type (not `application/json`) | does not matter         | body               | 415         |

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }
```

Since the sample code configures both serialization and deserialization, it can read `NameField` and include `NameField` in the output JSON.

#### Configure JSON deserialization options for an endpoint

[ReadFromJsonAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequestjsonextensions.readfromjsonasync) has overloads that accept a [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object. The following example includes public fields and formats JSON output.

```
using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }
```

Since the preceding code applies the customized options only to deserialization, the output JSON excludes `NameField`.

### Read the request body

Read the request body directly using a [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) or [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) parameter:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();
```

The preceding code:

* Accesses the request body using [HttpRequest.BodyReader](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader#microsoft-aspnetcore-http-httprequest-bodyreader).
* Copies the request body to a local file.

## Responses

Route handlers support the following types of return values:

1. `IResult` based - This includes `Task<IResult>` and `ValueTask<IResult>`
2. `string` - This includes `Task<string>` and `ValueTask<string>`
3. `T` (Any other type) - This includes `Task<T>` and `ValueTask<T>`

| Return value         | Behavior                                                                                                     | Content-Type                            |
| -------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| `IResult`            | The framework calls [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync) | Decided by the `IResult` implementation |
| `string`             | The framework writes the string directly to the response                                                     | `text/plain`                            |
| `T` (Any other type) | The framework JSON-serializes the response                                                                   | `application/json`                      |

For a more in-depth guide to route handler return values see [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0)

### Example return values

#### string return values

```
app.MapGet("/hello", () => "Hello World");
```

#### JSON return values

```
app.MapGet("/hello", () => new { Message = "Hello World" });
```

#### Return TypedResults

The following code returns a [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults):

```
app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));
```

Returning `TypedResults` is preferred to returning [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results). For more information, see [`TypedResults` versus `Results`](minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

#### IResult return values

```
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
```

The following example uses the built-in result types to customize the response:

```
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
```

#### JSON

```
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
```

#### Custom Status Code

```
app.MapGet("/405", () => Results.StatusCode(405));
```

#### Text

```
app.MapGet("/text", () => Results.Text("This is some text"));
```

[]()

#### Stream

```
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});
```

See [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0#stream7) for more examples.

#### Redirect

```
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
```

#### File

```
app.MapGet("/download", () => Results.File("myfile.text"));
```

[]()

### Built-in results

Common result helpers exist in the [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes. Returning `TypedResults` is preferred to returning `Results`. For more information, see [`TypedResults` versus `Results`](minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

### Customizing results

Applications can control responses by implementing a custom [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) type. The following code is an example of an HTML result type:

```
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}
```

We recommend adding an extension method to [Microsoft.AspNetCore.Http.IResultExtensions](/en-us/dotnet/api/microsoft.aspnetcore.http.iresultextensions) to make these custom results more discoverable.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();
```

### Typed results

The [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) interface can represent values returned from Minimal APIs that don't utilize the implicit support for JSON serializing the returned object to the HTTP response. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class is used to create varying `IResult` objects that represent different types of responses. For example, setting the response status code or redirecting to another URL.

The types implementing `IResult` are public, allowing for type assertions when testing. For example:

```
[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}
```

You can look at the return types of the corresponding methods on the static [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) class to find the correct public `IResult` type to cast to.

See [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0) for more examples.

## Filters

See [Filters in Minimal API apps](minimal-apis/min-api-filters?view=aspnetcore-10.0)

## Authorization

Routes can be protected using authorization policies. These can be declared via the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute or by using the [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) method:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

The preceding code can be written with [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization):

```
app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();
```

The following sample uses [policy-based authorization](../security/authorization/policies?view=aspnetcore-10.0):

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

### Allow unauthenticated users to access an endpoint

The [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) allows unauthenticated users to access endpoints:

```
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();
```

## CORS

Routes can be [CORS](../security/cors?view=aspnetcore-6.0) enabled using [CORS policies](../security/cors?view=aspnetcore-6.0#cors-policy-options). CORS can be declared via the [`[EnableCors]`](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute or by using the [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) method. The following samples enable CORS:

```
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
```

```
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();
```

For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-6.0)

[]()

## See also

* [Generate OpenAPI documents](openapi/aspnetcore-openapi?view=aspnetcore-10.0)
* [Create responses in Minimal API applications](minimal-apis/responses?view=aspnetcore-10.0)
* [Filters in Minimal API apps](minimal-apis/min-api-filters?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0)
* [Authentication and authorization in Minimal APIs](minimal-apis/security?view=aspnetcore-10.0)
* [Test Minimal API apps](minimal-apis/test-min-api?view=aspnetcore-10.0)

This document provides a quick reference for Minimal APIs. For a guided introduction, see [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0).

The Minimal APIs consist of:

* [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) and [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder)
* Route handlers

## `WebApplication`

The following code is generated by an ASP.NET Core template:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code can be created via `dotnet new web` on the command line or selecting the Empty Web template in Visual Studio.

The following code creates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`) without explicitly creating a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder):

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

[`WebApplication.Create`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.create) initializes a new instance of the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) class with preconfigured defaults.

### Working with ports

When a web app is created with Visual Studio or `dotnet new`, a `Properties/launchSettings.json` file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog `Unable to connect to web server 'AppName'`. Run the following port changing samples from the command line.

The following sections set the port the app responds to.

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");
```

In the preceding code, the app responds to port `3000`.

#### Multiple ports

In the following code, the app responds to port `3000` and `4000`.

```
var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Set the port from the command line

The following command makes the app respond to port `7777`:

```
dotnet run --urls="https://localhost:7777"
```

If the Kestrel endpoint is also configured in the `appsettings.json` file, the `appsettings.json` file specified URL is used. For more information, see [Kestrel endpoint configuration](configuration/?view=aspnetcore-6.0#kestrel)

#### Read the port from environment

The following code reads the port from the environment:

```
var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");
```

The preferred way to set the port from the environment is to use the `ASPNETCORE_URLS` environment variable, which is shown in the following section.

#### Set the ports via the ASPNETCORE\_URLS environment variable

The `ASPNETCORE_URLS` environment variable is available to set the port:

```
ASPNETCORE_URLS=http://localhost:3000
```

`ASPNETCORE_URLS` supports multiple URLs:

```
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
```

### Listen on all interfaces

The following samples demonstrate listening on all interfaces

#### http\://\*:3000

```
var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### http\://+:3000

```
var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://0.0.0.0:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Listen on all interfaces using ASPNETCORE\_URLS

The preceding samples can use `ASPNETCORE_URLS`

```
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
```

### Specify HTTPS with development certificate

```
var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information on the development certificate, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../security/enforcing-ssl?view=aspnetcore-10.0#trust).

### Specify HTTPS using a custom certificate

The following sections show how to specify the custom certificate using the `appsettings.json` file and via configuration.

#### Specify the custom certificate with `appsettings.json`

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}
```

#### Specify the custom certificate via configuration

```
var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Use the certificate APIs

```
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Read the environment

```
var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();
```

For more information using the environment, see [ASP.NET Core runtime environments](environments?view=aspnetcore-6.0)

### Configuration

The following code reads from the configuration system:

```
var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Hello";

app.MapGet("/", () => message);

app.Run();
```

For more information, see [Configuration in ASP.NET Core](configuration/?view=aspnetcore-6.0)

### Logging

The following code writes a message to the log on application startup:

```
var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information, see [Logging in .NET and ASP.NET Core](logging/?view=aspnetcore-6.0)

### Access the Dependency Injection (DI) container

The following code shows how to get services from the DI container during application startup:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();
```

For more information, see [Dependency injection in ASP.NET Core](dependency-injection?view=aspnetcore-6.0).

## WebApplicationBuilder

This section contains sample code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

### Change the content root, application name, and environment

The following code sets the content root, application name, and environment:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults.

For more information, see [ASP.NET Core fundamentals overview](./?view=aspnetcore-6.0)

### Change the content root, app name, and environment by environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| feature          | Environment variable        | Command-line argument |
| ---------------- | --------------------------- | --------------------- |
| Application name | ASPNETCORE\_APPLICATIONNAME | --applicationName     |
| Environment name | ASPNETCORE\_ENVIRONMENT     | --environment         |
| Content root     | ASPNETCORE\_CONTENTROOT     | --contentRoot         |

### Add configuration providers

The following sample adds the INI configuration provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](configuration/?view=aspnetcore-6.0#file-configuration-provider) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-6.0).

### Read configuration

By default the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) reads configuration from multiple sources, including:

* `appSettings.json` and `appSettings.{environment}.json`
* Environment variables
* The command line

For a complete list of configuration sources read, see [Default configuration](configuration/?view=aspnetcore-6.0#default-configuration) in [Configuration in ASP.NET Core](configuration/?view=aspnetcore-6.0)

The following code reads `HelloKey` from configuration and displays the value at the `/` endpoint. If the configuration value is null, "Hello" is assigned to `message`:

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

### Read the environment

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

### Add logging providers

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();
```

### Add services

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

### Customize the IHostBuilder

Existing extension methods on [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) can be accessed using the [Host property](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.properties#microsoft-extensions-hosting-ihostbuilder-properties):

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Customize the IWebHostBuilder

Extension methods on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) can be accessed using the [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost#microsoft-aspnetcore-builder-webapplicationbuilder-webhost) property.

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();
```

### Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed with `WebHostOptions`, the command line, or with the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();
```

### Custom dependency injection (DI) container

The following example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

### Add middleware

Any existing ASP.NET Core middleware can be configured on the `WebApplication`:

```
var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information, see [ASP.NET Core middleware](middleware/?view=aspnetcore-6.0)

### Developer exception page

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the [development environment](environments?view=aspnetcore-10.0), navigating to `/` renders a friendly page that shows the exception.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();
```

## ASP.NET Core middleware

The following table lists some of the middleware frequently used with Minimal APIs.

| Middleware                                                                                | Description                                                                                                          | API                                                                                                                                                                                                      |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Authentication](../security/authentication/?view=aspnetcore-6.0)                         | Provides authentication support.                                                                                     | [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)                                                                                           |
| [Authorization](../security/authorization/introduction?view=aspnetcore-10.0)              | Provides authorization support.                                                                                      | [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)                                                                                    |
| [CORS](../security/cors?view=aspnetcore-6.0)                                              | Configures Cross-Origin Resource Sharing.                                                                            | [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors)                                                                                                               |
| [Exception Handler](error-handling-api?view=aspnetcore-6.0)                               | Globally handles exceptions thrown by the middleware pipeline.                                                       | [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)                                                                                     |
| [Forwarded Headers](middleware/?view=aspnetcore-6.0#forwarded-headers-middleware-order)   | Forwards proxied headers onto the current request.                                                                   | [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders)                                                                                     |
| [HTTPS Redirection](../security/enforcing-ssl?view=aspnetcore-6.0)                        | Redirects all HTTP requests to HTTPS.                                                                                | [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)                                                                                   |
| [HTTP Strict Transport Security (HSTS)](middleware/?view=aspnetcore-6.0#middleware-order) | Security enhancement middleware that adds a special response header.                                                 | [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)                                                                                                                  |
| [Request Logging](http-logging/?view=aspnetcore-10.0)                                     | Provides support for logging HTTP requests and responses.                                                            | [UseHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usehttplogging)                                                                                             |
| [W3C Request Logging](https://www.w3.org/TR/WD-logfile.html)                              | Provides support for logging HTTP requests and responses in the [W3C format](https://www.w3.org/TR/WD-logfile.html). | [UseW3CLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usew3clogging)                                                                                               |
| [Response Caching](../performance/caching/middleware?view=aspnetcore-10.0)                | Provides support for caching responses.                                                                              | [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching)                                                                                        |
| [Response Compression](../performance/response-compression?view=aspnetcore-10.0)          | Provides support for compressing responses.                                                                          | [UseResponseCompression](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecompressionbuilderextensions.useresponsecompression)                                                                     |
| [Session](app-state?view=aspnetcore-10.0)                                                 | Provides support for managing user sessions.                                                                         | [UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)                                                                                                      |
| [Static Files](static-files?view=aspnetcore-10.0)                                         | Provides support for serving static files and directory browsing.                                                    | [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), [UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) |
| [WebSockets](websockets?view=aspnetcore-10.0)                                             | Enables the WebSockets protocol.                                                                                     | [UseWebSockets](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketmiddlewareextensions.usewebsockets)                                                                                              |

## Request handling

The following sections cover routing, parameter binding, and responses.

### Routing

A configured `WebApplication` supports `Map{Verb}` and [MapMethods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapmethods):

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();
```

### Route Handlers

Route handlers are methods that execute when the route matches. Route handlers can be a function of any shape, including synchronous or asynchronous. Route handlers can be a lambda expression, a local function, an instance method or a static method.

#### Lambda expression

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();
```

#### Local function

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();
```

#### Instance method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}
```

#### Static method

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}
```

### Named endpoints and link generation

Endpoints can be given names in order to generate URLs to the endpoint. Using a named endpoint avoids having to hard code paths in an app:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();
```

The preceding code displays `The link to the hello endpoint is /hello` from the `/` endpoint.

**NOTE**: Endpoint names are case sensitive.

Endpoint names:

* Must be globally unique.
* Are used as the OpenAPI operation id when OpenAPI support is enabled. For more information, see [OpenAPI](openapi/aspnetcore-openapi?view=aspnetcore-10.0).

### Route Parameters

Route parameters can be captured as part of the route pattern definition:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();
```

The preceding code returns `The user id is 3 and book id is 7` from the URI `/users/3/books/7`.

The route handler can declare the parameters to capture. When a request is made a route with parameters declared to capture, the parameters are parsed and passed to the handler. This makes it easy to capture the values in a type safe way. In the preceding code, `userId` and `bookId` are both `int`.

In the preceding code, if either route value cannot be converted to an `int`, an exception is thrown. The GET request `/users/hello/books/3` throws the following exception:

**`BadHttpRequestException: Failed to bind parameter "int userId" from "hello".`**

### Wildcard and catch all routes

The following catch all route returns `Routing to hello` from the \`/posts/hello' endpoint:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();
```

### Route constraints

Route constraints constrain the matching behavior of a route.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();
```

The following table demonstrates the preceding route templates and their behavior.

| Route Template                       | Example Matching URI |
| ------------------------------------ | -------------------- |
| `/todos/{id:int}`                    | `/todos/1`           |
| `/todos/{text}`                      | `/todos/something`   |
| `/posts/{slug:regex(^[a-z0-9_-]+$)}` | `/posts/mypost`      |

For more information, see [Route constraint reference](routing?view=aspnetcore-10.0) in [Routing in ASP.NET Core](routing?view=aspnetcore-10.0).

### Parameter Binding

Parameter binding is the process of converting request data into strongly typed parameters that are expressed by route handlers. A binding source determines where parameters are bound from. Binding sources can be explicit or inferred based on HTTP method and parameter type.

Supported binding sources:

* Route values
* Query string
* Header
* Body (as JSON)
* Services provided by dependency injection
* Custom

Note

Binding from form values is ***not*** natively supported in .NET.

The following example GET route handler uses some of these parameter binding sources:

```
var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }
```

The following table shows the relationship between the parameters used in the preceding example and the associated binding sources.

| Parameter      | Binding Source                   |
| -------------- | -------------------------------- |
| `id`           | route value                      |
| `page`         | query string                     |
| `customHeader` | header                           |
| `service`      | Provided by dependency injection |

The HTTP methods `GET`, `HEAD`, `OPTIONS`, and `DELETE` don't implicitly bind from body. To bind from body (as JSON) for these HTTP methods, [bind explicitly](#explicit-parameter-binding) with [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) or read from the [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest).

The following example POST route handler uses a binding source of body (as JSON) for the `person` parameter:

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);
```

The parameters in the preceding examples are all bound from request data automatically. To demonstrate the convenience that parameter binding provides, the following example route handlers show how to read request data directly from the request:

```
app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});
```

### Explicit Parameter Binding

Attributes can be used to explicitly declare where parameters are bound from.

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
```

| Parameter     | Binding Source                        |
| ------------- | ------------------------------------- |
| `id`          | route value with the name `id`        |
| `page`        | query string with the name `"p"`      |
| `service`     | Provided by dependency injection      |
| `contentType` | header with the name `"Content-Type"` |

Note

Binding from form values is ***not*** natively supported in .NET.

### Parameter binding with DI

Parameter binding for Minimal APIs binds parameters through [dependency injection](dependency-injection?view=aspnetcore-10.0) when the type is configured as a service. It's not necessary to explicitly apply the [`[FromServices]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) attribute to a parameter. In the following code, both actions return the time:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
```

### Optional parameters

Parameters declared in route handlers are treated as required:

* If a request matches the route, the route handler only runs if all required parameters are provided in the request.
* Failure to provide all required parameters results in an error.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
```

| URI                      | result                                                                                             |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `/products?pageNumber=3` | 3 returned                                                                                         |
| `/products`              | `BadHttpRequestException`: Required parameter "int pageNumber" was not provided from query string. |
| `/products/1`            | HTTP 404 error, no matching route                                                                  |

To make `pageNumber` optional, define the type as optional or provide a default value:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
```

| URI                      | result     |
| ------------------------ | ---------- |
| `/products?pageNumber=3` | 3 returned |
| `/products`              | 1 returned |
| `/products2`             | 1 returned |

The preceding nullable and default value applies to all sources:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();
```

The preceding code calls the method with a null product if no request body is sent.

**NOTE**: If invalid data is provided and the parameter is nullable, the route handler is ***not*** run.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
```

| URI                        | result                                                                                       |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `/products?pageNumber=3`   | `3` returned                                                                                 |
| `/products`                | `1` returned                                                                                 |
| `/products?pageNumber=two` | `BadHttpRequestException`: Failed to bind parameter `"Nullable<int> pageNumber"` from "two". |
| `/products/two`            | HTTP 404 error, no matching route                                                            |

See the [Binding Failures](#bf) section for more information.

### Special types

The following types are bound without explicit attributes:

* [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext): The context which holds all the information about the current HTTP request or response:

  ```
  app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
  ```

* [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) and [HttpResponse](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse): The HTTP request and HTTP response:

  ```
  app.MapGet("/", (HttpRequest request, HttpResponse response) =>
      response.WriteAsync($"Hello World {request.Query["name"]}"));
  ```

* [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken): The cancellation token associated with the current HTTP request:

  ```
  app.MapGet("/", async (CancellationToken cancellationToken) => 
      await MakeLongRunningRequestAsync(cancellationToken));
  ```

* [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal): The user associated with the request, bound from [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user):

  ```
  app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
  ```

### Custom Binding

There are two ways to customize parameter binding:

1. For route, query, and header binding sources, bind custom types by adding a static `TryParse` method for the type.
2. Control the binding process by implementing a `BindAsync` method on a type.

#### TryParse

`TryParse` has two APIs:

```
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
```

The following code displays `Point: 12.3, 10.1` with the URI `/map?Point=12.3,10.1`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}
```

#### BindAsync

`BindAsync` has the following APIs:

```
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
```

The following code displays `SortBy:xyz, SortDirection:Desc, CurrentPage:99` with the URI `/products?SortBy=xyz&SortDir=Desc&Page=99`:

```
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}
```

[]()

### Binding failures

When binding fails, the framework logs a debug message and returns various status codes to the client depending on the failure mode.

| Failure mode                                | Nullable Parameter Type | Binding Source     | Status code |
| ------------------------------------------- | ----------------------- | ------------------ | ----------- |
| `{ParameterType}.TryParse` returns `false`  | yes                     | route/query/header | 400         |
| `{ParameterType}.BindAsync` returns `null`  | yes                     | custom             | 400         |
| `{ParameterType}.BindAsync` throws          | does not matter         | custom             | 500         |
| Failure to deserialize JSON body            | does not matter         | body               | 400         |
| Wrong content type (not `application/json`) | does not matter         | body               | 415         |

### Binding Precedence

The rules for determining a binding source from a parameter:

1. Explicit attribute defined on parameter (From\* attributes) in the following order:

   1. Route values: [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute)
   2. Query string: [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute)
   3. Header: [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute)
   4. Body: [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute)
   5. Service: [`[FromServices]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute)

2. Special types

   1. [`HttpContext`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext)
   2. [`HttpRequest`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) ([`HttpContext.Request`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.request#microsoft-aspnetcore-http-httpcontext-request))
   3. [`HttpResponse`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse) ([`HttpContext.Response`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.response#microsoft-aspnetcore-http-httpcontext-response))
   4. [`ClaimsPrincipal`](/en-us/dotnet/api/system.security.claims.claimsprincipal) ([`HttpContext.User`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user))
   5. [`CancellationToken`](/en-us/dotnet/api/system.threading.cancellationtoken) ([`HttpContext.RequestAborted`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted))

3. Parameter type has a valid `BindAsync` method.

4. Parameter type is a string or has a valid `TryParse` method.

   1. If the parameter name exists in the route template. In `app.Map("/todo/{id}", (int id) => {});`, `id` is bound from the route.
   2. Bound from the query string.

5. If the parameter type is a service provided by dependency injection, it uses that service as the source.

6. The parameter is from the body.

### Customize JSON binding

The body binding source uses [System.Text.Json](/en-us/dotnet/api/system.text.json) for de-serialization. It is ***not*** possible to change this default, but the binding can be customized using other techniques described previously. To customize JSON serializer options, use code similar to the following:

```
using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/products", (Product product) => product);

app.Run();

class Product
{
    // These are public fields, not properties.
    public int Id;
    public string? Name;
}
```

The preceding code:

* Configures both the input and output default JSON options.

* Returns the following JSON

  ```
  {
    "id": 1,
    "name": "Joe Smith"
  }
  ```

  When posting

  ```
  {
    "Id": 1,
    "Name": "Joe Smith"
  }
  ```

### Read the request body

Read the request body directly using a [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) or [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) parameter:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();
```

The preceding code:

* Accesses the request body using [HttpRequest.BodyReader](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader#microsoft-aspnetcore-http-httprequest-bodyreader).
* Copies the request body to a local file.

## Responses

Route handlers support the following types of return values:

1. `IResult` based - This includes `Task<IResult>` and `ValueTask<IResult>`
2. `string` - This includes `Task<string>` and `ValueTask<string>`
3. `T` (Any other type) - This includes `Task<T>` and `ValueTask<T>`

| Return value         | Behavior                                                                                                     | Content-Type                            |
| -------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| `IResult`            | The framework calls [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync) | Decided by the `IResult` implementation |
| `string`             | The framework writes the string directly to the response                                                     | `text/plain`                            |
| `T` (Any other type) | The framework will JSON serialize the response                                                               | `application/json`                      |

### Example return values

#### string return values

```
app.MapGet("/hello", () => "Hello World");
```

#### JSON return values

```
app.MapGet("/hello", () => new { Message = "Hello World" });
```

#### IResult return values

```
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
```

The following example uses the built-in result types to customize the response:

```
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
```

##### JSON

```
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
```

##### Custom Status Code

```
app.MapGet("/405", () => Results.StatusCode(405));
```

##### Text

```
app.MapGet("/text", () => Results.Text("This is some text"));
```

##### Stream

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
```

##### Redirect

```
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
```

##### File

```
app.MapGet("/download", () => Results.File("myfile.text"));
```

### Built-in results

Common result helpers exist in the `Microsoft.AspNetCore.Http.Results` static class.

| Description                                                                    | Response type                                    | Status Code                 | API                                                                                                    |
| ------------------------------------------------------------------------------ | ------------------------------------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------ |
| Write a JSON response with advanced options                                    | application/json                                 | 200                         | [Results.Json](/en-us/dotnet/api/microsoft.aspnetcore.http.results.json)                               |
| Write a JSON response                                                          | application/json                                 | 200                         | [Results.Ok](/en-us/dotnet/api/microsoft.aspnetcore.http.results.ok)                                   |
| Write a text response                                                          | text/plain (default), configurable               | 200                         | [Results.Text](/en-us/dotnet/api/microsoft.aspnetcore.http.results.text)                               |
| Write the response as bytes                                                    | application/octet-stream (default), configurable | 200                         | [Results.Bytes](/en-us/dotnet/api/microsoft.aspnetcore.http.results.bytes)                             |
| Write a stream of bytes to the response                                        | application/octet-stream (default), configurable | 200                         | [Results.Stream](/en-us/dotnet/api/microsoft.aspnetcore.http.results.stream)                           |
| Stream a file to the response for download with the content-disposition header | application/octet-stream (default), configurable | 200                         | [Results.File](/en-us/dotnet/api/microsoft.aspnetcore.http.results.file)                               |
| Set the status code to 404, with an optional JSON response                     | N/A                                              | 404                         | [Results.NotFound](/en-us/dotnet/api/microsoft.aspnetcore.http.results.notfound)                       |
| Set the status code to 204                                                     | N/A                                              | 204                         | [Results.NoContent](/en-us/dotnet/api/microsoft.aspnetcore.http.results.nocontent)                     |
| Set the status code to 422, with an optional JSON response                     | N/A                                              | 422                         | [Results.UnprocessableEntity](/en-us/dotnet/api/microsoft.aspnetcore.http.results.unprocessableentity) |
| Set the status code to 400, with an optional JSON response                     | N/A                                              | 400                         | [Results.BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.results.badrequest)                   |
| Set the status code to 409, with an optional JSON response                     | N/A                                              | 409                         | [Results.Conflict](/en-us/dotnet/api/microsoft.aspnetcore.http.results.conflict)                       |
| Write a problem details JSON object to the response                            | N/A                                              | 500 (default), configurable | [Results.Problem](/en-us/dotnet/api/microsoft.aspnetcore.http.results.problem)                         |
| Write a problem details JSON object to the response with validation errors     | N/A                                              | N/A, configurable           | [Results.ValidationProblem](/en-us/dotnet/api/microsoft.aspnetcore.http.results.validationproblem)     |

### Customizing results

Applications can control responses by implementing a custom [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) type. The following code is an example of an HTML result type:

```
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}
```

We recommend adding an extension method to [Microsoft.AspNetCore.Http.IResultExtensions](/en-us/dotnet/api/microsoft.aspnetcore.http.iresultextensions) to make these custom results more discoverable.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();
```

## Authorization

Routes can be protected using authorization policies. These can be declared via the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute or by using the [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) method:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

The preceding code can be written with [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization):

```
app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();
```

The following sample uses [policy-based authorization](../security/authorization/policies?view=aspnetcore-10.0):

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();
```

### Allow unauthenticated users to access an endpoint

The [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) allows unauthenticated users to access endpoints:

```
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();
```

## CORS

Routes can be [CORS](../security/cors?view=aspnetcore-6.0) enabled using [CORS policies](../security/cors?view=aspnetcore-6.0#cors-policy-options). CORS can be declared via the [`[EnableCors]`](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute or by using the [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) method. The following samples enable CORS:

```
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
```

```
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();
```

For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-6.0)

## See also

[OpenAPI support in Minimal APIs](openapi/aspnetcore-openapi?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/routing.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor routing

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains Blazor app request routing with guidance on static versus interactive routing, ASP.NET Core endpoint routing integration, navigation events, and route templates and constraints for Razor components.

Routing in Blazor is achieved by providing a route template to each accessible component in the app with an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive. When a Razor file with an `@page` directive is compiled, the generated class is given a [RouteAttribute](/en-us/dotnet/api/microsoft.aspnetcore.components.routeattribute) specifying the route template. At runtime, the router searches for component classes with a [RouteAttribute](/en-us/dotnet/api/microsoft.aspnetcore.components.routeattribute) and renders whichever component has a route template that matches the requested URL.

The following `HelloWorld` component uses a route template of `/hello-world`, and the rendered webpage for the component is reached at the relative URL `/hello-world`.

`HelloWorld.razor`:

```
@page "/hello-world"

<h1>Hello World!</h1>
```

The preceding component loads in the browser at `/hello-world` regardless of whether or not you add the component to the app's UI navigation as a link.

## Static versus interactive routing

*This section applies to Blazor Web Apps.*

If [prerendering is enabled](../components/prerender?view=aspnetcore-10.0), the Blazor router (`Router` component, `<Router>` in `Routes.razor`) performs static routing to components during static server-side rendering (static SSR). This type of routing is called *static routing*.

When an interactive render mode is assigned to the `Routes` component, the Blazor router becomes interactive after static SSR with static routing on the server. This type of routing is called *interactive routing*.

Static routers use endpoint routing and the HTTP request path to determine which component to render. When the router becomes interactive, it uses the document's URL (the URL in the browser's address bar) to determine which component to render. This means that the interactive router can dynamically change which component is rendered if the document's URL dynamically changes to another valid internal URL, and it can do so without performing an HTTP request to fetch new page content.

Interactive routing also prevents prerendering because new page content isn't requested from the server with a normal page request. For more information, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0#interactive-routing-and-prerendering).

## ASP.NET Core endpoint routing integration

*This section applies to Blazor Web Apps operating over a circuit.*

A Blazor Web App is integrated into [ASP.NET Core Endpoint Routing](../../fundamentals/routing?view=aspnetcore-10.0). An ASP.NET Core app is configured with endpoints for routable components and the root component to render for those endpoints with [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) in the `Program` file. The default root component (first component loaded) is the `App` component (`App.razor`):

```
app.MapRazorComponents<App>();
```

*This section applies to Blazor Server apps operating over a circuit.*

Blazor Server is integrated into [ASP.NET Core Endpoint Routing](../../fundamentals/routing?view=aspnetcore-10.0). An ASP.NET Core app is configured to accept incoming connections for interactive components with [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) in the `Program` file:

```
app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
```

*This section applies to Blazor Server apps operating over a circuit.*

Blazor Server is integrated into [ASP.NET Core Endpoint Routing](../../fundamentals/routing?view=aspnetcore-10.0). An ASP.NET Core app is configured to accept incoming connections for interactive components with [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) in `Startup.Configure`.

The typical configuration is to route all requests to a Razor page, which acts as the host for the server-side part of the Blazor Server app. By convention, the *host* page is usually named `_Host.cshtml` in the `Pages` folder of the app.

The route specified in the host file is called a *fallback route* because it operates with a low priority in route matching. The fallback route is used when other routes don't match. This allows the app to use other controllers and pages without interfering with component routing in the Blazor Server app.

For information on configuring [MapFallbackToPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.mapfallbacktopage) for non-root URL server hosting, see [ASP.NET Core Blazor app base path](../host-and-deploy/app-base-path?view=aspnetcore-10.0).

## Route templates

The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component enables routing to Razor components and is located in the app's `Routes` component (`Components/Routes.razor`).

The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component enables routing to Razor components. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component is used in the `App` component (`App.razor`).

When a Razor component (`.razor`) with an [`@page` directive](../../mvc/views/razor?view=aspnetcore-10.0#page) is compiled, the generated component class is provided a [RouteAttribute](/en-us/dotnet/api/microsoft.aspnetcore.components.routeattribute) specifying the component's route template.

When the app starts, the assembly specified as the Router's `AppAssembly` is scanned to gather route information for the app's components that have a [RouteAttribute](/en-us/dotnet/api/microsoft.aspnetcore.components.routeattribute).

At runtime, the [RouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.routeview) component:

* Receives the [RouteData](/en-us/dotnet/api/microsoft.aspnetcore.components.routedata) from the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) along with any route parameters.
* Renders the specified component with its [layout](../components/layouts?view=aspnetcore-10.0), including any further nested layouts.

Optionally specify a [DefaultLayout](/en-us/dotnet/api/microsoft.aspnetcore.components.routeview.defaultlayout#microsoft-aspnetcore-components-routeview-defaultlayout) parameter with a layout class for components that don't specify a layout with the [`@layout` directive](../components/layouts?view=aspnetcore-10.0#apply-a-layout-to-a-component). The framework's [Blazor project templates](../project-structure?view=aspnetcore-10.0) specify the `MainLayout` component (`MainLayout.razor`) as the app's default layout. For more information on layouts, see [ASP.NET Core Blazor layouts](../components/layouts?view=aspnetcore-10.0).

Components support multiple route templates using multiple [`@page` directives](../../mvc/views/razor?view=aspnetcore-10.0#page). The following example component loads on requests for `/blazor-route` and `/different-blazor-route`.

`BlazorRoute.razor`:

```
@page "/blazor-route"
@page "/different-blazor-route"

<h1>Routing Example</h1>

<p>
    This page is reached at either <code>/blazor-route</code> or 
    <code>/different-blazor-route</code>.
</p>
```

Important

For URLs to resolve correctly, the app must include a `<base>` tag ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)) with the app base path specified in the `href` attribute. For more information, see [ASP.NET Core Blazor app base path](../host-and-deploy/app-base-path?view=aspnetcore-10.0).

The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) doesn't interact with query string values. To work with query strings, see [Query strings](navigation?view=aspnetcore-10.0#query-strings).

As an alternative to specifying the route template as a string literal with the `@page` directive, constant-based route templates can be specified with the [`@attribute` directive](../../mvc/views/razor?view=aspnetcore-10.0#attribute).

In the following example, the `@page` directive in a component is replaced with the `@attribute` directive and the constant-based route template in `Constants.CounterRoute`, which is set elsewhere in the app to "`/counter`":

```
- @page "/counter"
+ @attribute [Route(Constants.CounterRoute)]
```

Note

With the release of .NET 5.0.1 and for any additional 5.x releases, the `Router` component includes the `PreferExactMatches` parameter set to `@true`. For more information, see [Migrate from ASP.NET Core 3.1 to .NET 5](../../migration/31-to-50?view=aspnetcore-10.0#changes-to-blazor-app-routing-logic-in-501-and-further-5x-releases-up-to-60).

## Focus an element on navigation

The [FocusOnNavigate](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.focusonnavigate) component sets the UI focus to an element based on a CSS selector after navigating from one page to another.

```
<FocusOnNavigate RouteData="routeData" Selector="h1" />
```

When the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component navigates to a new page, the [FocusOnNavigate](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.focusonnavigate) component sets the focus to the page's top-level header (`<h1>`). This is a common strategy for ensuring that a page navigation is announced when using a screen reader.

## Provide custom content when content isn't found

For requests where content isn't found, a Razor component can be assigned to [Router.NotFoundPage](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfoundpage). The parameter works in concert with [`NavigationManager.NotFound`](navigation?view=aspnetcore-10.0#not-found-responses), a method called in developer code that triggers a Not Found response.

The Blazor project template includes a `NotFound.razor` page. This page automatically renders whenever [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called, making it possible to handle missing routes with a consistent user experience.

`NotFound.razor`:

```
@page "/not-found"
@layout MainLayout

<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
```

The `NotFound` component is assigned to the router's [Router.NotFoundPage](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfoundpage) parameter, which supports routing that can be used across status code pages re-execution middleware, including non-Blazor middleware.

In the following example, the preceding `NotFound` component is present in the app's `Pages` folder and passed to the [Router.NotFoundPage](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfoundpage) parameter:

```
<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
```

For more information, see the next article on [ASP.NET Core Blazor navigation](navigation?view=aspnetcore-10.0#not-found-responses).

The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component allows the app to specify custom content if content isn't found for the requested route.

Set custom content for the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component's [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfound#microsoft-aspnetcore-components-routing-router-notfound) parameter:

```
<Router ...>
    ...
    <NotFound>
        ...
    </NotFound>
</Router>
```

Arbitrary items are supported as content of the [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfound#microsoft-aspnetcore-components-routing-router-notfound) parameter, such as other interactive components. To apply a default layout to [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfound#microsoft-aspnetcore-components-routing-router-notfound) content, see [ASP.NET Core Blazor layouts](../components/layouts?view=aspnetcore-10.0#apply-a-layout-to-arbitrary-content-layoutview-component).

Blazor Web Apps don't use the [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfound#microsoft-aspnetcore-components-routing-router-notfound) parameter (`<NotFound>...</NotFound>` markup), but the parameter is supported† for backward compatibility in .NET 8/9 to avoid a breaking change in the framework. The server-side ASP.NET Core middleware pipeline processes requests on the server. Use server-side techniques to handle bad requests.

†*Supported* in this context means that placing `<NotFound>...</NotFound>` markup doesn't result in an exception, but using the markup isn't effective either.

For more information, see the following resources:

* [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0#static-server-side-rendering-static-ssr)
* [Blazor navigation: Not Found responses](navigation?view=aspnetcore-10.0#not-found-responses)

## Route to components from multiple assemblies

*This section applies to Blazor Web Apps.*

Use the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component's [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies#microsoft-aspnetcore-components-routing-router-additionalassemblies) parameter and the endpoint convention builder [AddAdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointconventionbuilderextensions.addadditionalassemblies) to discover routable components in additional assemblies. The following subsections explain when and how to use each API.

### Static routing

To discover routable components from additional assemblies for static server-side rendering (static SSR), even if the router later becomes interactive for interactive rendering, the assemblies must be disclosed to the Blazor framework. Call the [AddAdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointconventionbuilderextensions.addadditionalassemblies) method with the additional assemblies chained to [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) in the server project's `Program` file.

The following example includes the routable components in the `BlazorSample.Client` project's assembly using the project's imports file (`_Imports.razor`):

```
app.MapRazorComponents<App>()
    .AddAdditionalAssemblies(typeof(BlazorSample.Client._Imports).Assembly);
```

Note

The preceding guidance also applies in [component class library](../components/class-libraries?view=aspnetcore-10.0) scenarios. Additional important guidance for class libraries and static SSR is found in [ASP.NET Core Razor class libraries (RCLs) with static server-side rendering (static SSR)](../components/class-libraries-and-static-server-side-rendering?view=aspnetcore-10.0).

### Interactive routing

An interactive render mode can be assigned to the `Routes` component (`Routes.razor`) that makes the Blazor router become interactive after static SSR and static routing on the server. For example, `<Routes @rendermode="InteractiveServer" />` assigns interactive server-side rendering (interactive SSR) to the `Routes` component. The `Router` component inherits interactive server-side rendering (interactive SSR) from the `Routes` component. The router becomes interactive after static routing on the server.

Internal navigation for interactive routing doesn't involve requesting new page content from the server. Therefore, prerendering doesn't occur for internal page requests. For more information, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0#interactive-routing-and-prerendering).

If the `Routes` component is defined in the server project, the [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies#microsoft-aspnetcore-components-routing-router-additionalassemblies) parameter of the `Router` component should include the `.Client` project's assembly. This allows the router to work correctly when rendered interactively.

In the following example, the `Routes` component is in the server project, and the imports file (`_Imports.razor`) of the `BlazorSample.Client` project indicates the assembly to search for routable components:

```
<Router
    AppAssembly="..."
    AdditionalAssemblies="[ typeof(BlazorSample.Client._Imports).Assembly ]">
    ...
</Router>
```

Additional assemblies are scanned in addition to the assembly specified to [AppAssembly](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.appassembly).

Note

The preceding guidance also applies in [component class library](../components/class-libraries?view=aspnetcore-10.0) scenarios.

Alternatively, routable components only exist in the `.Client` project with global Interactive WebAssembly or Auto rendering applied, and the `Routes` component is defined in the `.Client` project, not the server project. In this case, there aren't external assemblies with routable components, so it isn't necessary to specify a value for [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies#microsoft-aspnetcore-components-routing-router-additionalassemblies).

*This section applies to Blazor Server apps.*

Use the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component's [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies#microsoft-aspnetcore-components-routing-router-additionalassemblies) parameter and the endpoint convention builder [AddAdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointconventionbuilderextensions.addadditionalassemblies) to discover routable components in additional assemblies.

In the following example, `Component1` is a routable component defined in a referenced [component class library](../components/class-libraries?view=aspnetcore-10.0) named `ComponentLibrary`:

```
<Router
    AppAssembly="..."
    AdditionalAssemblies="new[] { typeof(ComponentLibrary.Component1).Assembly }">
    ...
</Router>
```

Additional assemblies are scanned in addition to the assembly specified to [AppAssembly](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.appassembly).

## Route parameters

The router uses route parameters to populate the corresponding [component parameters](../components/?view=aspnetcore-10.0#component-parameters) with the same name. Route parameter names are case insensitive. In the following example, the `text` parameter assigns the value of the route segment to the component's `Text` property. When a request is made for `/route-parameter-1/amazing`, the content is rendered as `Blazor is amazing!`.

`RouteParameter1.razor`:

```
@page "/route-parameter-1/{text}"

<h1>Route Parameter Example 1</h1>

<p>Blazor is @Text!</p>

@code {
    [Parameter]
    public string? Text { get; set; }
}
```

Optional parameters are supported. In the following example, the `text` optional parameter assigns the value of the route segment to the component's `Text` property. If the segment isn't present, the value of `Text` is set to `fantastic`.

Optional parameters aren't supported. In the following example, two [`@page` directives](../../mvc/views/razor?view=aspnetcore-10.0#page) are applied. The first directive permits navigation to the component without a parameter. The second directive assigns the `{text}` route parameter value to the component's `Text` property.

`RouteParameter2.razor`:

```
@page "/route-parameter-2/{text?}"

<h1>Route Parameter Example 2</h1>

<p>Blazor is @Text!</p>

@code {
    [Parameter]
    public string? Text { get; set; }

    protected override void OnParametersSet() => Text = Text ?? "fantastic";
}
```

When the [`OnInitialized{Async}` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) is used instead of the [`OnParametersSet{Async}` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#after-parameters-are-set-onparameterssetasync), the default assignment of the `Text` property to `fantastic` doesn't occur if the user navigates within the same component. For example, this situation arises when the user navigates from `/route-parameter-2/amazing` to `/route-parameter-2`. As the component instance persists and accepts new parameters, the `OnInitialized` method isn't invoked again.

Note

Route parameters don't work with query string values. To work with query strings, see [Query strings](navigation?view=aspnetcore-10.0#query-strings).

Note

Route parameters can't be typed as a [C# union](/en-us/dotnet/csharp/language-reference/builtin-types/union). The router parses route segments as strings and converts them to the target type without JSON parsing, so it can't dispatch to a union case.

## Route constraints

A route constraint enforces type matching on a route segment to a component.

In the following example, the route to the `User` component only matches if:

* An `Id` route segment is present in the request URL.
* The `Id` segment is an integer (`int`) type.

`User.razor`:

```
@page "/user/{Id:int}"

<h1>User Example</h1>

<p>User Id: @Id</p>

@code {
    [Parameter]
    public int Id { get; set; }
}
```

Note

Route constraints don't work with query string values. To work with query strings, see [Query strings](navigation?view=aspnetcore-10.0#query-strings).

The route constraints shown in the following table are available. For the route constraints that match the invariant culture, see the warning below the table for more information.

| Constraint | Example               | Example Matches                                                                  | Invariant culture matching |
| ---------- | --------------------- | -------------------------------------------------------------------------------- | -------------------------- |
| `bool`     | `{active:bool}`       | `true`, `FALSE`                                                                  | No                         |
| `datetime` | `{dob:datetime}`      | `2016-12-31`, `2016-12-31 7:32pm`                                                | Yes                        |
| `decimal`  | `{price:decimal}`     | `49.99`, `-1,000.01`                                                             | Yes                        |
| `double`   | `{weight:double}`     | `1.234`, `-1,001.01e8`                                                           | Yes                        |
| `float`    | `{weight:float}`      | `1.234`, `-1,001.01e8`                                                           | Yes                        |
| `guid`     | `{id:guid}`           | `00001111-aaaa-2222-bbbb-3333cccc4444`, `{00001111-aaaa-2222-bbbb-3333cccc4444}` | No                         |
| `int`      | `{id:int}`            | `123456789`, `-123456789`                                                        | Yes                        |
| `long`     | `{ticks:long}`        | `123456789`, `-123456789`                                                        | Yes                        |
| `nonfile`  | `{parameter:nonfile}` | Not `BlazorSample.styles.css`, not `favicon.ico`                                 | Yes                        |

Warning

Route constraints that verify the URL and are converted to a CLR type (such as `int` or [DateTime](/en-us/dotnet/api/system.datetime)) always use the invariant culture. These constraints assume that the URL is non-localizable.

Route constraints also work with [optional parameters](#route-parameters). In the following example, `Id` is required, but `Option` is an optional boolean route parameter.

`User.razor`:

```
@page "/user/{id:int}/{option:bool?}"

<p>
    Id: @Id
</p>

<p>
    Option: @Option
</p>

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

    [Parameter]
    public bool Option { get; set; }
}
```

## Avoid file capture in a route parameter

The following route template inadvertently captures static asset paths in its optional route parameter (`Optional`). For example, the app's stylesheet (`.styles.css`) is captured, which breaks the app's styles:

```
@page "/{optional?}"

...

@code {
    [Parameter]
    public string? Optional { get; set; }
}
```

To restrict a route parameter to capturing non-file paths, use the [`:nonfile` constraint](#route-constraints) in the route template:

```
@page "/{optional:nonfile?}"
```

## Routing with URLs that contain dots

A ***server-side*** default route template assumes that if the last segment of a request URL contains a dot (`.`) that a file is requested. For example, the relative URL `/example/some.thing` is interpreted by the router as a request for a file named `some.thing`. Without additional configuration, an app returns a *404 - Not Found* response if `some.thing` was meant to route to a component with an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive and `some.thing` is a route parameter value. To use a route with one or more parameters that contain a dot, the app must configure the route with a custom template.

Consider the following `Example` component that can receive a route parameter from the last segment of the URL.

`Example.razor`:

```
@page "/example/{param?}"

<p>
    Param: @Param
</p>

@code {
    [Parameter]
    public string? Param { get; set; }
}
```

To permit the **Server** app of a hosted Blazor WebAssembly [solution](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) to route the request with a dot in the `param` route parameter, add a fallback file route template with the optional parameter in the `Program` file:

```
app.MapFallbackToFile("/example/{param?}", "index.html");
```

To configure a Blazor Server app to route the request with a dot in the `param` route parameter, add a fallback page route template with the optional parameter in the `Program` file:

```
app.MapFallbackToPage("/example/{param?}", "/_Host");
```

For more information, see [Routing in ASP.NET Core](../../fundamentals/routing?view=aspnetcore-10.0).

To permit the **Server** app of a hosted Blazor WebAssembly [solution](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) to route the request with a dot in the `param` route parameter, add a fallback file route template with the optional parameter in `Startup.Configure`.

`Startup.cs`:

```
endpoints.MapFallbackToFile("/example/{param?}", "index.html");
```

To configure a Blazor Server app to route the request with a dot in the `param` route parameter, add a fallback page route template with the optional parameter in `Startup.Configure`.

`Startup.cs`:

```
endpoints.MapFallbackToPage("/example/{param?}", "/_Host");
```

For more information, see [Routing in ASP.NET Core](../../fundamentals/routing?view=aspnetcore-10.0).

## Catch-all route parameters

Catch-all route parameters, which capture paths across multiple folder boundaries, are supported in components.

Catch-all route parameters are:

* Named to match the route segment name. Naming isn't case-sensitive.
* A `string` type. The framework doesn't provide automatic casting.
* At the end of the URL.

`CatchAll.razor`:

```
@page "/catch-all/{*pageRoute}"

<h1>Catch All Parameters Example</h1>

<p>Add some URI segments to the route and request the page again.</p>

<p>
    PageRoute: @PageRoute
</p>

@code {
    [Parameter]
    public string? PageRoute { get; set; }
}
```

For the URL `/catch-all/this/is/a/test` with a route template of `/catch-all/{*pageRoute}`, the value of `PageRoute` is set to `this/is/a/test`.

Slashes and segments of the captured path are decoded. For a route template of `/catch-all/{*pageRoute}`, the URL `/catch-all/this/is/a%2Ftest%2A` yields `this/is/a/test*`.

## Handle asynchronous navigation events with `OnNavigateAsync`

The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component supports an [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) feature. The [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) handler is invoked when the user:

* Visits a route for the first time by navigating to it directly in their browser.
* Navigates to a new route using a link or a [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) invocation, which is called in developer code to navigates to a URI. \[[NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) API is described in the next article, [ASP.NET Core Blazor navigation](navigation?view=aspnetcore-10.0).]

```
<Router AppAssembly="typeof(App).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        ...
    }
}
```

```
<Router AppAssembly="typeof(Program).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        ...
    }
}
```

For an example that uses [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync), see [Lazy load assemblies in ASP.NET Core Blazor WebAssembly](../webassembly-lazy-load-assemblies?view=aspnetcore-10.0).

When prerendering on the server, [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) is executed *twice*:

* Once when the requested endpoint component is initially rendered statically.
* A second time when the browser renders the endpoint component.

To prevent developer code in [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) from executing twice, the `Routes` component can store the [NavigationContext](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationcontext) for use in the [`OnAfterRender{Async}` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync), where `firstRender` can be checked. For more information, see [Prerendering with JavaScript interop](../components/lifecycle?view=aspnetcore-10.0#prerendering-with-javascript-interop).

To prevent developer code in [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) from executing twice, the `App` component can store the [NavigationContext](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationcontext) for use in [`OnAfterRender{Async}`](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync), where `firstRender` can be checked. For more information, see [Prerendering with JavaScript interop](../components/lifecycle?view=aspnetcore-10.0#prerendering-with-javascript-interop).

## Handle cancellations in `OnNavigateAsync`

The [NavigationContext](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationcontext) object passed to the [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) callback contains a [CancellationToken](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationcontext.cancellationtoken#microsoft-aspnetcore-components-routing-navigationcontext-cancellationtoken) that's set when a new navigation event occurs. The [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) callback must throw when this cancellation token is set to avoid continuing to run the [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) callback on an outdated navigation.

If a user navigates to an endpoint but then immediately navigates to a new endpoint, the app shouldn't continue running the [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) callback for the first endpoint.

In the following example:

* The cancellation token is passed in the call to `PostAsJsonAsync`, which can cancel the POST if the user navigates away from the `/about` endpoint.
* The cancellation token is set during a product prefetch operation if the user navigates away from the `/store` endpoint.

```
@inject HttpClient Http
@inject ProductCatalog Products

<Router AppAssembly="typeof(App).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext context)
    {
        if (context.Path == "/about") 
        {
            var stats = new Stats { Page = "/about" };
            await Http.PostAsJsonAsync("api/visited", stats, 
                context.CancellationToken);
        }
        else if (context.Path == "/store")
        {
            var productIds = new[] { 345, 789, 135, 689 };

            foreach (var productId in productIds) 
            {
                context.CancellationToken.ThrowIfCancellationRequested();
                Products.Prefetch(productId);
            }
        }
    }
}
```

```
@inject HttpClient Http
@inject ProductCatalog Products

<Router AppAssembly="typeof(Program).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext context)
    {
        if (context.Path == "/about") 
        {
            var stats = new Stats { Page = "/about" };
            await Http.PostAsJsonAsync("api/visited", stats, 
                context.CancellationToken);
        }
        else if (context.Path == "/store")
        {
            var productIds = new[] { 345, 789, 135, 689 };

            foreach (var productId in productIds) 
            {
                context.CancellationToken.ThrowIfCancellationRequested();
                Products.Prefetch(productId);
            }
        }
    }
}
```

Note

Not throwing if the cancellation token in [NavigationContext](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationcontext) is canceled can result in unintended behavior, such as rendering a component from a previous navigation.

## User interaction with `<Navigating>` content

If there's a significant delay during navigation, such as while [lazy-loading assemblies in a Blazor WebAssembly app](../webassembly-lazy-load-assemblies?view=aspnetcore-10.0) or for a slow network connection to a Blazor server-side app, the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component can indicate to the user that a page transition is occurring.

At the top of the component that specifies the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component, add an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive for the [Microsoft.AspNetCore.Components.Routing](/en-us/dotnet/api/microsoft.aspnetcore.components.routing) namespace:

```
@using Microsoft.AspNetCore.Components.Routing
```

Provide content to the [Navigating](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.navigating#microsoft-aspnetcore-components-routing-router-navigating) parameter for display during page transition events.

In the router element (`<Router>...</Router>`) content:

```
<Navigating>
    <p>Loading the requested page&hellip;</p>
</Navigating>
```

For an example that uses the [Navigating](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.navigating#microsoft-aspnetcore-components-routing-router-navigating) property, see [Lazy load assemblies in ASP.NET Core Blazor WebAssembly](../webassembly-lazy-load-assemblies?view=aspnetcore-10.0#user-interaction-with-navigating-content).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/identity-management-solutions
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/identity-management-solutions.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Identity management solutions for .NET web apps

The following table provides a nonexhaustive overview of identity management solutions for ASP.NET Core apps in alphabetical order. These solutions offer features and capabilities to manage [user authentication](authentication/?view=aspnetcore-10.0), [authorization](authorization/introduction?view=aspnetcore-10.0), and [user identity](authentication/identity?view=aspnetcore-10.0), including options for apps that are:

* Container-based.
* Self-hosted, where you manage the app's installation and infrastructure on your own hardware.
* Managed in a cloud-based service, such as [Microsoft Entra](authentication/azure-active-directory/?view=aspnetcore-10.0).

Depending on your company size and app requirements, many commercial licenses provide "community" or free options.

| Name                                                                                                 | Type      | License Type                                                                                                       | Documentation                                                                            |
| ---------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| [ASP.NET Core Identity](authentication/identity?view=aspnetcore-10.0)                                | Self host | [OSS (MIT)](https://github.com/dotnet/aspnetcore/blob/main/LICENSE.txt)                                            | [Introduction to Identity on ASP.NET Core](authentication/identity?view=aspnetcore-10.0) |
| [Auth0](https://auth0.com/)                                                                          | Managed   | [Commercial](https://auth0.com/pricing)                                                                            | [Get started](https://auth0.com/docs/get-started)                                        |
| [Duende IdentityServer](https://duendesoftware.com/products/identityserver)                          | Self host | [Commercial](https://duendesoftware.com/products/identityserver#pricing)                                           | [ASP.NET Identity integration](https://docs.duendesoftware.com)                          |
| [Keycloak](https://www.keycloak.org)                                                                 | Container | [OSS (Apache 2.0)](https://github.com/keycloak/keycloak/blob/master/LICENSE.txt)                                   | [Keycloak securing apps documentation](https://www.keycloak.org/guides#securing-apps)    |
| [Microsoft Entra ID](https://www.microsoft.com/security/business/identity-access/microsoft-entra-id) | Managed   | [Commercial](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id#Plansandpricing) | [Entra documentation](/en-us/entra/fundamentals/what-is-entra)                           |
| [Okta](https://www.okta.com)                                                                         | Managed   | [Commercial](https://www.okta.com/pricing/)                                                                        | [Okta for ASP.NET Core](https://developer.okta.com/code/)                                |
| [OpenIddict](https://github.com/openiddict/openiddict-core)                                          | Self host | [OSS (Apache 2.0)](https://github.com/openiddict/openiddict-core/blob/dev/LICENSE.md)                              | [OpenIddict documentation](https://documentation.openiddict.com/)                        |

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-microsoft-entra-id
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/standalone-with-microsoft-entra-id.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to create a [standalone Blazor WebAssembly app](../../hosting-models?view=aspnetcore-10.0#blazor-webassembly) that uses [Microsoft Entra ID (ME-ID)](https://azure.microsoft.com/services/active-directory/) for authentication.

1. Navigate to [**Microsoft Entra ID** in the Azure portal](https://entra.microsoft.com/#home). Select **Applications** > **App registrations** in the sidebar. Select the **New registration** button.
2. Provide a **Name** for the app (for example, **Blazor Standalone ME-ID**).
3. Choose a **Supported account types**. You may select **Accounts in this organizational directory only** for this experience.
4. Set the **Redirect URI** dropdown list to **Single-page application (SPA)** and provide the following redirect URI: `https://localhost/authentication/login-callback`. If you know the production redirect URI for the Azure default host (for example, `azurewebsites.net`) or the custom domain host (for example, `contoso.com`), you can also add the production redirect URI at the same time that you're providing the `localhost` redirect URI. Be sure to include the port number for non-`:443` ports in any production redirect URIs that you add.
5. If you're using an [unverified publisher domain](/en-us/entra/identity-platform/howto-configure-publisher-domain), clear the **Permissions** > **Grant admin consent to openid and offline\_access permissions** checkbox. If the publisher domain is verified, this checkbox isn't present.
6. Select **Register**.

Note

Supplying the port number for a `localhost` ME-ID redirect URI isn't required. For more information, see [Redirect URI (reply URL) restrictions and limitations: Localhost exceptions (Entra documentation)](/en-us/entra/identity-platform/reply-url#localhost-exceptions).

Record the following information:

* Application (client) ID (for example, `00001111-aaaa-2222-bbbb-3333cccc4444`)
* Directory (tenant) ID (for example, `aaaabbbb-0000-cccc-1111-dddd2222eeee`)

In **Authentication** > **Platform configurations** > **Single-page application**:

1. Confirm the redirect URI of `https://localhost/authentication/login-callback` is present.
2. In the **Implicit grant** section, ensure that the checkboxes for **Access tokens** and **ID tokens** aren't selected. **Implicit grant isn't recommended for Blazor apps using MSAL v2.0 or later.** For more information, see [Secure ASP.NET Core Blazor WebAssembly](./?view=aspnetcore-10.0#use-the-authorization-code-flow-with-pkce).
3. The remaining defaults for the app are acceptable for this experience.
4. Select the **Save** button if you made changes.

### Create the Blazor app

Create the app in an empty folder. Replace the placeholders in the following command with the information recorded earlier and execute the command in a command shell:

```
dotnet new blazorwasm -au SingleOrg --client-id "{CLIENT ID}" -o {PROJECT NAME} --tenant-id "{TENANT ID}"
```

| Placeholder      | Azure portal name       | Example                                |
| ---------------- | ----------------------- | -------------------------------------- |
| `{PROJECT NAME}` | —                       | `BlazorSample`                         |
| `{CLIENT ID}`    | Application (client) ID | `00001111-aaaa-2222-bbbb-3333cccc4444` |
| `{TENANT ID}`    | Directory (tenant) ID   | `aaaabbbb-0000-cccc-1111-dddd2222eeee` |

The output location specified with the `-o|--output` option creates a project folder if it doesn't exist and becomes part of the project's name.

Add a [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions) for `User.Read` permission with [DefaultAccessTokenScopes](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.defaultaccesstokenscopes#microsoft-authentication-webassembly-msal-models-msalprovideroptions-defaultaccesstokenscopes):

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes
        .Add("https://graph.microsoft.com/User.Read");
});
```

  ```
  @using Microsoft.Extensions.Options
  @inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> RemoteOptions
  ```

  Modify the component's redirect in the `OnInitialized` method:

  ```
  - Navigation.NavigateToLogin("authentication/login");
  + Navigation.NavigateToLogin(RemoteOptions.Get(Options.DefaultName)
  +     .AuthenticationPaths.LogInPath);
  ```

Support for authenticating users is registered in the service container with the [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) extension method provided by the [`Microsoft.Authentication.WebAssembly.Msal` package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal). This method sets up the services required for the app to interact with the Identity Provider (IP).

In the `Program` file:

```
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});
```

The [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) method accepts a callback to configure the parameters required to authenticate an app. The values required for configuring the app can be obtained from the ME-ID configuration when you register the app.

### `wwwroot/appsettings.json` configuration

Configuration is supplied by the `wwwroot/appsettings.json` file:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  }
}
```

Example:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/e86c78e2-...-918e0565a45e",
    "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
    "ValidateAuthority": true
  }
}
```

### Access token scopes

The Blazor WebAssembly template doesn't automatically configure the app to request an access token for a secure API. To provision an access token as part of the sign-in flow, add the scope to the default access token scopes of the [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions):

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
```

Specify additional scopes with `AdditionalScopesToConsent`:

```
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE URI}");
```

Note

[AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent) isn't able to provision delegated user permissions for Microsoft Graph via the Microsoft Entra ID consent UI when a user first uses an app registered in Microsoft Azure. For more information, see [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0\&pivots=graph-sdk-5#defaultaccesstokenscopes-versus-additionalscopestoconsent).

For more information, see the following resources:

* [Request additional access tokens](additional-scenarios?view=aspnetcore-10.0#request-additional-access-tokens)
* [Attach tokens to outgoing requests](additional-scenarios?view=aspnetcore-10.0#attach-tokens-to-outgoing-requests)
* [Quickstart: Configure an application to expose web APIs](/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis)
* [Access token scopes for Microsoft Graph API](graph-api?view=aspnetcore-10.0)

### Login mode

The framework defaults to pop-up login mode and falls back to redirect login mode if a pop-up can't be opened. Configure MSAL to use redirect login mode by setting the `LoginMode` property of [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions) to `redirect`:

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.LoginMode = "redirect";
});
```

The default setting is `popup`, and the string value isn't case-sensitive.

### Imports file

The [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespace is made available throughout the app via the imports file (`_Imports.razor`):

```
...
@using Microsoft.AspNetCore.Components.Authorization
...
```

### Index page

The Index page (`wwwroot/index.html`) page includes a script that defines the `AuthenticationService` in JavaScript. `AuthenticationService` handles the low-level details of the OIDC protocol. The app internally calls methods defined in the script to perform the authentication operations.

```
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
```

```
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string? Action { get; set; }
}
```

    * [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)
    * [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)
    * [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

  > info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

```
@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthenticationState { get; set; }

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}
```

### Inspect the content of a JSON Web Token (JWT)

To decode a JSON Web Token (JWT), use Microsoft's [jwt.ms](https://jwt.ms/) tool. Values in the UI never leave your browser.

Example encoded JWT (shortened for display):

> eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V\_bU55Ug\_PW4pLPr20tTS8Ct7\_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2\_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

```
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
  "sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]
```

## Related content

* [Identity and account types for single- and multitenant apps](/en-us/security/zero-trust/develop/identity-supported-account-types)
* [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0)
* [Build a custom version of the Authentication.MSAL JavaScript library](additional-scenarios?view=aspnetcore-10.0#build-a-custom-version-of-the-authenticationmsal-javascript-library)
* [Unauthenticated or unauthorized web API requests in an app with a secure default client](additional-scenarios?view=aspnetcore-10.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client)
* [ASP.NET Core Blazor WebAssembly with Microsoft Entra ID groups and roles](microsoft-entra-id-groups-and-roles?view=aspnetcore-10.0)
* [Microsoft identity platform and Microsoft Entra ID with ASP.NET Core](../../../security/authentication/azure-active-directory/?view=aspnetcore-10.0)
* [Microsoft identity platform documentation](/en-us/entra/identity-platform/)
* [Security best practices for application properties in Microsoft Entra ID](/en-us/entra/identity-platform/security-best-practices-for-app-registration)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/configure-jwt-bearer-authentication.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure JWT bearer authentication in ASP.NET Core

By [Damien Bowden](https://github.com/damienbod)

APIs commonly use JWT (JSON Web Token) Bearer Authentication. While it works similarly to cookie authentication, the identity provider issues a JWT or tokens when authentication succeeds. You can send these tokens to other servers to authenticate, unlike cookies which you only send back to the issuing domain. A JWT is a self-contained token that encapsulates information for an API resource or a client. The client that requests the JWT can request data from an API resource by using the Authorization header and a bearer token.

JWT bearer Authentication provides:

* **Authentication**: When using the `JwtBearerHandler`, bearer tokens are essential for authentication. The `JwtBearerHandler` validates the token and extracts the user's identity from its claims.
* **Authorization**: Bearer tokens enable authorization by providing a collection of claims that represent the user's or application's permissions, much like a cookie.
* **Delegated Authorization**: When a user-specific access token is used to authenticate between APIs instead of an application-wide access token, this process is known as *delegated authorization*.

For an introduction to JWT bearer Authentication, see [JSON Web Tokens.](https://en.wikipedia.org/wiki/JSON_Web_Token) [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/configure-jwt-bearer-authentication/samples/JwtBearer)

This article covers the following areas:

* Token types
* Using JWT tokens to secure an APIs
* How OIDC/OAuth fits into this?
* Implementing JWT bearer token authentication
* Recommended approaches to create a JWT

## Token types

There are many types of tokens and formats. Don't generate your own access tokens or ID tokens, except for testing purposes. Self-created tokens that don't follow established standards:

* Can lead to security vulnerabilities.
* Are only suitable for closed systems.

Use [OpenID Connect 1.0](https://openid.net/specs/openid-connect-core-1_0-final.html) or an OAuth standard to create access tokens for API access.

### Access tokens

Access tokens:

* Are strings that a client app uses to make requests to the server implementing an API.
* Can vary in format. Different APIs might use different formats for the tokens.
* Can be encrypted.
* Should never be read or interpreted by a web client or UI app that holds the access token.
* Are intended solely for making requests to an API.
* Are typically sent to the API in the **Authorization** request header as a bearer token.

See [The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749#section-1.4).

#### Application access tokens and delegated access tokens

Access tokens can be either *application access tokens* or *delegated access tokens*. These tokens have different claims and are managed and stored differently. An *application access token* is typically stored once in the app until it expires, while a *delegated access token* is stored per user, either in a cookie or in a secure server cache.

Use delegated user access tokens whenever a user is involved. Downstream APIs can request a delegated user access token on behalf of the authenticated user.

#### Sender-constrained access tokens

Access tokens can be used as [bearer tokens](https://cloud.google.com/docs/authentication/token-types#bearer) or [sender-constrained tokens](https://docs.verify.ibm.com/ibm-security-verify-access/docs/tasks-certboundaccesstoken) to access resources. Sender-constrained tokens require the requesting client to prove possession of a private key to use the token. Proving possession of a private key guarantees the token can't be used independently. You can implement sender-constrained tokens in two ways:

* [Demonstrating Proof of Possession (DPoP)](https://datatracker.ietf.org/doc/html/rfc9449)
* [Mutual-TLS (MTLS)](https://datatracker.ietf.org/doc/html/rfc8705)

### ID tokens

ID tokens are security tokens that confirm a user's successful authentication. The tokens allow the client to verify the user's identity. The JWT token server issues ID tokens containing claims with user information. ID tokens are always in [JWT](https://jwt.io/introduction) format.

ID tokens ***should never*** be used to access APIs.

### Other tokens

There are many types of tokens, including access and ID tokens, as specified by OpenID Connect and OAuth standards. Refresh tokens can be used to refresh a UI app without re-authenticating the user. [OAuth JAR tokens](https://datatracker.ietf.org/doc/rfc9101/) can securely send authorization requests. Verifiable credentials flows utilize JWT types for issuing or verifying credentials. It is crucial to use tokens according to the specifications. See the standards links provided later in this article for more information.

## Using JWT tokens to secure an API

When using JWT access tokens for API authorization, the API grants or denies access based on the provided token. If the request is not authorized, a 401 or 403 response is returned. The API shouldn't redirect the user to the identity provider to obtain a new token or request additional permissions. The app consuming the API is responsible for acquiring an appropriate token. This ensures a clear separation of concerns between the API (authorization) and the consuming client app (authentication).

Note

HTTP also allows returning 404 for not authorized, so as to not leak information about the existence of resources to unauthorized clients.

### 401 Unauthorized

A 401 Unauthorized response indicates that the provided access token doesn't meet the required standards. This condition could be due to several reasons, including:

* **Invalid signature**: The token's signature doesn't match, suggesting potential tampering.
* **Expiration**: The token has expired and is no longer valid.
* **Incorrect claims**: Critical claims within the token, such as the audience (`aud`) or issuer (`iss`), are missing or invalid.

Note

From the HTTP Semantics [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.2): The server generating a 401 response MUST send a `WWW-Authenticate` header field (Section 11.6.1) containing at least one challenge applicable to the target resource.

The [OAuth specifications](/en-us/entra/identity-platform/access-token-claims-reference) provide detailed guidelines on the required claims and their validation.

### 403 Forbidden

A [403 Forbidden](https://developer.mozilla.org/docs/Web/HTTP/Status/403) response typically indicates that the authenticated user lacks the necessary permissions to access the requested resource. This condition is distinct from authentication issues, such as an invalid token, and is unrelated to the standard claims within the access token.

In ASP.NET Core, you can enforce authorization using:

* [Requirements and policies](/en-us/aspnet/core/security/authorization/policies): Define custom requirements, such as "Must be an administrator," and associate them with policies.
* [Role-based authorization](/en-us/aspnet/core/security/authorization/roles): Assign users to roles, such as "Admin" or "Editor," and restrict access based on those roles.

## What role has OIDC and/or OAuth when using bearer tokens?

When an API uses JWT access tokens for authorization, the API only validates the access token and doesn't consider how the token was obtained.

OpenID Connect (OIDC) and OAuth 2.0 provide standardized, secure frameworks for token acquisition. Token acquisition varies depending on the type of app. Due to the complexity of secure token acquisition, it's highly recommended to rely on these standards:

* For apps acting on behalf of a user and an application: Use OIDC to enable delegated user access. In web apps, use the confidential code flow with [Proof Key for Code Exchange](https://oauth.net/2/pkce/) (PKCE) for enhanced security.
  * If the calling app is an ASP.NET Core app with server-side [OIDC authentication](/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication), use the [SaveTokens](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens) property to store the access token in a cookie for later use via [`HttpContext.GetTokenAsync("access_token")`](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.gettokenasync).
* If the app has no user: Use the OAuth 2.0 client credentials flow to obtain application access tokens.

## Implementing JWT bearer token authentication

The [`Microsoft.AspNetCore.Authentication.JwtBearer` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer) can be used to validate the JWT bearer tokens.

JWT bearer tokens should be fully validated in an API. The following should be validated:

* Signature, for trust and integrity. This ensures the token was created by the designated secure token service and has not been tampered with.
* Issuer claim with the expected value.
* Audience claim with the expected value.
* Token expiration.

The following claims are required for OAuth 2.0 access tokens: `iss`, `exp`, `aud`, `sub`, `client_id`, `iat`, and `jti`.

If any of these claims or values are incorrect, the API should return a 401 response.

### JWT bearer token basic validation

A basic implementation of the [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) can validate just the audience and the issuer. The signature must be validated so that the token can be trusted and that it hasn't been tampered with.

```
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(jwtOptions =>
{
	jwtOptions.Authority = "https://{--your-authority--}";
	jwtOptions.Audience = "https://{--your-audience--}";
});
```

### JWT bearer token explicit validation

The [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) method provides multiple configurations. Some secure token providers use a non-standard metadata address and the parameter can be setup explicitly. The API can accept multiple issuers or audiences.

Explicitly defining the parameters is not required. The definitions depends on the access token claim values and the secure token server used to validate the access token. You should use the default values if possible.

See [Mapping claims](claims?view=aspnetcore-10.0#claims-namespaces-default-namespaces) MapInboundClaims details.

```
builder.Services.AddAuthentication()
.AddJwtBearer("some-scheme", jwtOptions =>
{
	jwtOptions.MetadataAddress = builder.Configuration["Api:MetadataAddress"];
	// Optional if the MetadataAddress is specified
	jwtOptions.Authority = builder.Configuration["Api:Authority"];
	jwtOptions.Audience = builder.Configuration["Api:Audience"];
	jwtOptions.TokenValidationParameters = new TokenValidationParameters
	{
		ValidateIssuer = true,
		ValidateAudience = true,
		ValidateIssuerSigningKey = true,
		ValidAudiences = builder.Configuration.GetSection("Api:ValidAudiences").Get<string[]>(),
		ValidIssuers = builder.Configuration.GetSection("Api:ValidIssuers").Get<string[]>()
	};

	jwtOptions.MapInboundClaims = false;
});
```

### JWT with multiple schemes

APIs often need to accommodate access tokens from various issuers. You can support multiple token issuers in an API by using the following approaches:

* **Separate APIs**: Create distinct APIs with dedicated authentication schemes for each issuer.
* [AddPolicyScheme](policyschemes?view=aspnetcore-10.0): Define multiple authentication schemes and implement logic to select the appropriate scheme based on token properties, such as the issuer or claims. This approach offers greater flexibility within a single API.

### Require authentication for all API endpoints

Use [SetFallbackPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder.setfallbackpolicy) to require an authenticated user for requests processed by the authorization middleware when no authorization policy is produced from endpoint metadata:

```
var requireAuthPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();

builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(requireAuthPolicy);
```

The default policy applies to `[Authorize]` without a policy name and requires an authenticated user unless the app changes it. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

You can also use the [AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute to force authentication. If you use multiple schemes, set the bearer scheme as the default authentication scheme or specify it by using `[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme])`.

Authorization in controllers:

```
[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
```

Authorization in Minimal APIs:

```
app.MapGet("/hello", [Authorize] () => "Hi");
```

## Recommended approaches to create a JWT

Insecure handling of access tokens, such as weak authentication or storing tokens in vulnerable client-side storage, can lead to significant security vulnerabilities. For example, storing access tokens directly in the browser by using local storage, session storage, or web workers. The following section contains best practices for apps that use and create access tokens.

### Use standards

Use standards like OpenID Connect or OAuth when creating access tokens. Don't create access tokens in production apps without following the security precautions outlined in this article. Limit creating access tokens to test scenarios.

### Use asymmetric keys

Asymmetric keys should **always** be used when creating access tokens. The public key is available in the well known endpoints and the API clients can validate the signature of the access token using the public key.

### Never create an access token from a username/password request

You should **NOT** create an access token from a username/password request. Username/password requests aren't authenticated and are vulnerable to impersonation and phishing attacks. Access tokens should only be created using an OpenID Connect flow or an OAuth standard flow. Deviating from these standards can result in an insecure app.

### Use cookies

For secure web apps, a backend is required to store access tokens on a trusted server. Only a secure HTTP only cookie is shared on the client browser. See the [OIDC authentication documentation](/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication) for how to do this in an ASP.NET Core web app.

## Downstream APIs

APIs occasionally need to access user data from downstream APIs on behalf of the authenticated user in the calling app. While implementing an OAuth client credentials flow is an option, it necessitates full trust between the two API apps. A more secure approach involves using a zero-trust strategy with a delegated user access token. This approach:

* Enhances security by granting the API only the necessary permissions for that specific user.
* Requires the API to create the new access token for the user calling the app and the API.

There are several ways to implement a zero-trust strategy with a delegated user access token:

### Use OAuth 2.0 Token Exchange to request a new delegated access token

This is a good way to implement this requirement but it's complicated if you must implement the OAuth flow.

See [OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)

### Use Microsoft Identity Web on behalf of flow to request a new delegated access token

Using the [Microsoft Identity Web authentication library](/en-us/entra/msal/dotnet/microsoft-identity-web/) is the easiest and a secure approach. It only works with Microsoft Entra ID, Microsoft Entra External ID.

For more information, see [Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow](/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow).

### Use the same delegated access token sent to the API

This approach is not difficult to implement but the access token has access to all downstream APIs. Yarp reverse proxy can be used to implement this.

### Use OAuth client credentials flow and use an application access token

This is easy to implement but the client application has full application access and not a delegated access token. The token should be cached in the client API application.

Note

Any app-to-app security also works. Certificate authentication, or in Azure, a managed identity can be used.

## Handling access tokens

When using access tokens in a client application, the access tokens must be rotated, persisted, and stored on the server. In a web app, cookies are used to secure the session and can be used to store tokens via the [SaveTokens](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens) property.

[SaveTokens](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens) doesn't refresh access tokens automatically, but this functionality is planned for a future release. In the meantime, you can manually refresh the access token as [demonstrated in the Blazor Web App with OIDC documentation](/en-us/aspnet/core/blazor/security/blazor-web-app-with-oidc?pivots=with-yarp-and-aspire#token-refresh) or use a third-party NuGet package, such as [`Duende.AccessTokenManagement.OpenIdConnect`](https://www.nuget.org/packages/Duende.AccessTokenManagement.OpenIdConnect). For more information, see [Duende token management](https://docs.duendesoftware.com/identityserver/v7/quickstarts/3a_token_management/).

Note

If deploying to production, the cache should work in a multi-instance deployment. A persistent cache is normally required.

Some secure token servers encrypt access tokens. Access tokens don't require any format. When you use OAuth introspection, use a reference token instead of an access token. A client (UI) application should never open an access token because the access token isn't intended for this purpose. Only an API for which the access token was created should open the access token.

* Don't open access tokens in a UI application.
* Don't send the ID token to the APIs.
* Access tokens can have any format.
* Access tokens can be encrypted.
* Access tokens expire and need to be rotated.
* Persist access tokens on a secure backend server.

## YARP (Yet Another Reverse Proxy)

[YARP (Yet Another Reverse Proxy)](https://dotnet.github.io/yarp/) is a useful technology for handling HTTP requests and forwarding the requests to other APIs. YARP can implement security logic for acquiring new access credentials. YARP is frequently used when adopting [Backend for Frontend (BFF) security architecture](/en-us/azure/architecture/patterns/backends-for-frontends).

For Blazor examples that use YARP to implement the BFF pattern, see the following articles:

* [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](../../blazor/security/blazor-web-app-with-entra?view=aspnetcore-10.0\&pivots=with-yarp-and-aspire)
* [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](../../blazor/security/blazor-web-app-with-oidc?view=aspnetcore-10.0\&pivots=with-yarp-and-aspire)

For a Blazor example that uses YARP to implement the BFF pattern, see [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](../../blazor/security/blazor-web-app-with-oidc?view=aspnetcore-10.0\&pivots=with-yarp-and-aspire).

For more information, see [auth0: The Backend for Frontend Pattern](https://auth0.com/blog/the-backend-for-frontend-pattern-bff/).

## Testing APIs

Integration tests and containers with access tokens can be used to test secure APIs. Access tokens can be created using the [dotnet user-jwts tool](/en-us/aspnet/core/security/authentication/jwt-authn).

Warning

Ensure that security problems are **not** introduced into the API for testing purposes. Testing becomes more challenging when delegated access tokens are used, as these tokens can only be created through a UI and an OpenID Connect flow. If a test tool is used to create delegated access tokens, security features must be disabled for testing. It's essential that these features are only disabled in the test environment.

Create dedicated and isolated test environments where security features can safely be disabled or modified. Ensure these changes are strictly limited to the test environment.

### Use Swagger UI, Curl and other API UI tools

Swagger UI and Curl are great UI tools for testing APIs. For the tools to work, the API can produce an OpenAPI document and the client testing tool can load this document. You can add a security flow to acquire a new access token to the API OpenAPI file.

Warning

Don't deploy insecure security test flows to production.

When implementing a Swagger UI for an API, you should normally not deploy the UI to production as the security must be weakened to allow this to work.

## Map claims from OpenID Connect

Refer to the following document:

[Mapping, customizing, and transforming claims in ASP.NET Core](claims?view=aspnetcore-10.0)

## Standards

* [JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)
* [The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)
* [OAuth 2.0 Demonstrating Proof of Possession DPoP](https://datatracker.ietf.org/doc/html/rfc9449)
* [OAuth 2.0 JWT-Secured Authorization Request (JAR) RFC 9101](https://datatracker.ietf.org/doc/rfc9101/)
* [OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://datatracker.ietf.org/doc/html/rfc8705)
* [OpenID Connect 1.0](https://openid.net/specs/openid-connect-core-1_0-final.html)
* [Microsoft identity platform and OAuth 2.0 On-Behalf-Of flow](/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow)
* [OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)
* [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://datatracker.ietf.org/doc/html/rfc9068)
* [HTTP Semantics RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.2)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/context-headers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/implementation/context-headers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Context headers in ASP.NET Core

[]()

## Background and theory

In the data protection system, a "key" means an object that can provide authenticated encryption services. Each key is identified by a unique id (a GUID), and it carries with it algorithmic information and entropic material. It's intended that each key carry unique entropy, but the system cannot enforce that, and we also need to account for developers who might change the key ring manually by modifying the algorithmic information of an existing key in the key ring. To achieve our security requirements given these cases the data protection system has a concept of [cryptographic agility](https://www.microsoft.com/research/publication/cryptographic-agility-and-its-relation-to-circular-encryption), which allows securely using a single entropic value across multiple cryptographic algorithms.

Most systems which support cryptographic agility do so by including some identifying information about the algorithm inside the payload. The algorithm's OID is generally a good candidate for this. However, one problem that we ran into is that there are multiple ways to specify the same algorithm: "AES" (CNG) and the managed Aes, AesManaged, AesCryptoServiceProvider, AesCng, and RijndaelManaged (given specific parameters) classes are all actually the same thing, and we'd need to maintain a mapping of all of these to the correct OID. If a developer wanted to provide a custom algorithm (or even another implementation of AES!), they'd have to tell us its OID. This extra registration step makes system configuration particularly painful.

Stepping back, we decided that we were approaching the problem from the wrong direction. An OID tells you what the algorithm is, but we don't actually care about this. If we need to use a single entropic value securely in two different algorithms, it's not necessary for us to know what the algorithms actually are. What we actually care about is how they behave. Any decent symmetric block cipher algorithm is also a strong pseudorandom permutation (PRP): fix the inputs (key, chaining mode, IV, plaintext) and the ciphertext output will with overwhelming probability be distinct from any other symmetric block cipher algorithm given the same inputs. Similarly, any decent keyed hash function is also a strong pseudorandom function (PRF), and given a fixed input set its output will overwhelmingly be distinct from any other keyed hash function.

We use this concept of strong PRPs and PRFs to build up a context header. This context header essentially acts as a stable thumbprint over the algorithms in use for any given operation, and it provides the cryptographic agility needed by the data protection system. This header is reproducible and is used later as part of the [subkey derivation process](subkeyderivation?view=aspnetcore-10.0#data-protection-implementation-subkey-derivation). There are two different ways to build the context header depending on the modes of operation of the underlying algorithms.

## CBC-mode encryption + HMAC authentication

[]()

The context header consists of the following components:

* \[16 bits] The value 00 00, which is a marker meaning "CBC encryption + HMAC authentication".

* \[32 bits] The key length (in bytes, big-endian) of the symmetric block cipher algorithm.

* \[32 bits] The block size (in bytes, big-endian) of the symmetric block cipher algorithm.

* \[32 bits] The key length (in bytes, big-endian) of the HMAC algorithm. (Currently the key size always matches the digest size.)

* \[32 bits] The digest size (in bytes, big-endian) of the HMAC algorithm.

* `EncCBC(K_E, IV, "")`, which is the output of the symmetric block cipher algorithm given an empty string input and where IV is an all-zero vector. The construction of `K_E` is described below.

* `MAC(K_H, "")`, which is the output of the HMAC algorithm given an empty string input. The construction of `K_H` is described below.

Ideally, we could pass all-zero vectors for `K_E` and `K_H`. However, we want to avoid the situation where the underlying algorithm checks for the existence of weak keys before performing any operations (notably DES and 3DES), which precludes using a simple or repeatable pattern like an all-zero vector.

Instead, we use the NIST SP800-108 KDF in Counter Mode (see [NIST SP800-108](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-108.pdf), Sec. 5.1) with a zero-length key, label, and context and HMACSHA512 as the underlying PRF. We derive `| K_E | + | K_H |` bytes of output, then decompose the result into `K_E` and `K_H` themselves. Mathematically, this is represented as follows.

`( K_E || K_H ) = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = "")`

### Example: AES-192-CBC + HMACSHA256

As an example, consider the case where the symmetric block cipher algorithm is AES-192-CBC and the validation algorithm is HMACSHA256. The system would generate the context header using the following steps.

First, let `( K_E || K_H ) = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = "")`, where `| K_E | = 192 bits` and `| K_H | = 256 bits` per the specified algorithms. This leads to `K_E = 5BB6..21DD` and `K_H = A04A..00A9` in the example below:

```
5B B6 C9 83 13 78 22 1D 8E 10 73 CA CF 65 8E B0
61 62 42 71 CB 83 21 DD A0 4A 05 00 5B AB C0 A2
49 6F A5 61 E3 E2 49 87 AA 63 55 CD 74 0A DA C4
B7 92 3D BF 59 90 00 A9
```

Next, compute `Enc_CBC (K_E, IV, "")` for AES-192-CBC given `IV = 0*` and `K_E` as above.

`result := F474B1872B3B53E4721DE19C0841DB6F`

Next, compute `MAC(K_H, "")` for HMACSHA256 given `K_H` as above.

`result := D4791184B996092EE1202F36E8608FA8FBD98ABDFF5402F264B1D7211536220C`

This produces the full context header below:

```
00 00 00 00 00 18 00 00 00 10 00 00 00 20 00 00
00 20 F4 74 B1 87 2B 3B 53 E4 72 1D E1 9C 08 41
DB 6F D4 79 11 84 B9 96 09 2E E1 20 2F 36 E8 60
8F A8 FB D9 8A BD FF 54 02 F2 64 B1 D7 21 15 36
22 0C
```

This context header is the thumbprint of the authenticated encryption algorithm pair (AES-192-CBC encryption + HMACSHA256 validation). The components, as described [above](context-headers?view=aspnetcore-10.0#data-protection-implementation-context-headers-cbc-components) are:

* the marker `(00 00)`

* the block cipher key length `(00 00 00 18)`

* the block cipher block size `(00 00 00 10)`

* the HMAC key length `(00 00 00 20)`

* the HMAC digest size `(00 00 00 20)`

* the block cipher PRP output `(F4 74 - DB 6F)` and

* the HMAC PRF output `(D4 79 - end)`.

Note

The CBC-mode encryption + HMAC authentication context header is built the same way regardless of whether the algorithms implementations are provided by Windows CNG or by managed SymmetricAlgorithm and KeyedHashAlgorithm types. This allows applications running on different operating systems to reliably produce the same context header even though the implementations of the algorithms differ between OSes. (In practice, the KeyedHashAlgorithm doesn't have to be a proper HMAC. It can be any keyed hash algorithm type.)

### Example: 3DES-192-CBC + HMACSHA1

First, let `( K_E || K_H ) = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = "")`, where `| K_E | = 192 bits` and `| K_H | = 160 bits` per the specified algorithms. This leads to `K_E = A219..E2BB` and `K_H = DC4A..B464` in the example below:

```
A2 19 60 2F 83 A9 13 EA B0 61 3A 39 B8 A6 7E 22
61 D9 F8 6C 10 51 E2 BB DC 4A 00 D7 03 A2 48 3E
D1 F7 5A 34 EB 28 3E D7 D4 67 B4 64
```

Next, compute `Enc_CBC (K_E, IV, "")` for 3DES-192-CBC given `IV = 0*` and `K_E` as above.

`result := ABB100F81E53E10E`

Next, compute `MAC(K_H, "")` for HMACSHA1 given `K_H` as above.

`result := 76EB189B35CF03461DDF877CD9F4B1B4D63A7555`

This produces the full context header which is a thumbprint of the authenticated encryption algorithm pair (3DES-192-CBC encryption + HMACSHA1 validation), shown below:

```
00 00 00 00 00 18 00 00 00 08 00 00 00 14 00 00
00 14 AB B1 00 F8 1E 53 E1 0E 76 EB 18 9B 35 CF
03 46 1D DF 87 7C D9 F4 B1 B4 D6 3A 75 55
```

The components break down as follows:

* the marker `(00 00)`

* the block cipher key length `(00 00 00 18)`

* the block cipher block size `(00 00 00 08)`

* the HMAC key length `(00 00 00 14)`

* the HMAC digest size `(00 00 00 14)`

* the block cipher PRP output `(AB B1 - E1 0E)` and

* the HMAC PRF output `(76 EB - end)`.

## Galois/Counter Mode encryption + authentication

The context header consists of the following components:

* \[16 bits] The value 00 01, which is a marker meaning "GCM encryption + authentication".

* \[32 bits] The key length (in bytes, big-endian) of the symmetric block cipher algorithm.

* \[32 bits] The nonce size (in bytes, big-endian) used during authenticated encryption operations. (For our system, this is fixed at nonce size = 96 bits.)

* \[32 bits] The block size (in bytes, big-endian) of the symmetric block cipher algorithm. (For GCM, this is fixed at block size = 128 bits.)

* \[32 bits] The authentication tag size (in bytes, big-endian) produced by the authenticated encryption function. (For our system, this is fixed at tag size = 128 bits.)

* \[128 bits] The tag of `Enc_GCM (K_E, nonce, "")`, which is the output of the symmetric block cipher algorithm given an empty string input and where nonce is a 96-bit all-zero vector.

`K_E` is derived using the same mechanism as in the CBC encryption + HMAC authentication scenario. However, since there's no `K_H` in play here, we essentially have `| K_H | = 0`, and the algorithm collapses to the below form.

`K_E = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = "")`

### Example: AES-256-GCM

First, let `K_E = SP800_108_CTR(prf = HMACSHA512, key = "", label = "", context = "")`, where `| K_E | = 256 bits`.

`K_E := 22BC6F1B171C08C4AE2F27444AF8FC8B3087A90006CAEA91FDCFB47C1B8733B8`

Next, compute the authentication tag of `Enc_GCM (K_E, nonce, "")` for AES-256-GCM given `nonce = 096` and `K_E` as above.

`result := E7DCCE66DF855A323A6BB7BD7A59BE45`

This produces the full context header below:

```
00 01 00 00 00 20 00 00 00 0C 00 00 00 10 00 00
00 10 E7 DC CE 66 DF 85 5A 32 3A 6B B7 BD 7A 59
BE 45
```

The components break down as follows:

* the marker `(00 01)`

* the block cipher key length `(00 00 00 20)`

* the nonce size `(00 00 00 0C)`

* the block cipher block size `(00 00 00 10)`

* the authentication tag size `(00 00 00 10)` and

* the authentication tag from running the block cipher `(E7 DC - end)`.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/bl0006
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/bl0006.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# BL0006: Do not use RenderTree types

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | BL0006       |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A reference to a type in the [Microsoft.AspNetCore.Components.RenderTree](/en-us/dotnet/api/microsoft.aspnetcore.components.rendertree) was found.

## Rule description

Types in the `Microsoft.AspNetCore.Components.RenderTree` namespace are part of Blazor's implementation detail and are subject to breaking changes. Using these types is not recommended in user code.

## How to fix violations

Remove the reference to the type.

## When to suppress warnings

It is safe to suppress this rule if using this type is essential. Note that this these are subject to breaking changes between major releases.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-05-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/adding-controller
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/adding-controller.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 2, add a controller to an ASP.NET Core MVC app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The Model-View-Controller (MVC) architectural pattern separates an app into three main components: **M**odel, **V**iew, and **C**ontroller. The MVC pattern helps you create apps that are more testable and easier to update than traditional monolithic apps.

MVC-based apps contain:

* **M**odels: Classes that represent the data of the app. The model classes use validation logic to enforce business rules for that data. Typically, model objects retrieve and store model state in a database. In this tutorial, a `Movie` model retrieves movie data from a database, provides it to the view or updates it. Updated data is written to a database.

* **V**iews: Views are the components that display the app's user interface (UI). Generally, this UI displays the model data.

* **C**ontrollers: Classes that:

  * Handle browser requests.
  * Invoke model logic to retrieve or modify data.
  * Call view templates that return a response.

In an MVC app, the view only displays information. The controller handles and responds to user input and interaction. For example, the controller handles URL segments and query-string values, and passes these values to the model. The model might use these values to query the database. For example:

* `https://localhost:5001/Home/Privacy`: specifies the `Home` controller and the `Privacy` action.
* `https://localhost:5001/Movies/Edit/5`: is a request to edit the movie with ID=5 using the `Movies` controller and the `Edit` action, which are detailed later in the tutorial.

Route data is explained later in the tutorial.

The MVC architectural pattern separates an app into three main groups of components: Models, Views, and Controllers. This pattern helps to achieve separation of concerns: The UI logic belongs in the view. Input logic belongs in the controller. Business logic belongs in the model. This separation helps manage complexity when building an app, because it enables work on one aspect of the implementation at a time without impacting the code of another. For example, you can work on the view code without depending on the business logic code.

These concepts are introduced and demonstrated in this tutorial series while building a movie app. The MVC project contains folders for the *Controllers* and *Views*.

## Add a controller

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

In **Solution Explorer**, right-click **Controllers > Add > Controller**.

In the **Add New Scaffolded Item** dialog box, select **MVC Controller - Empty** > **Add**.

In the **Add New Item - MvcMovie** dialog, enter *`HelloWorldController.cs`* and select **Add**.

Select the **EXPLORER** icon and then control-click (right-click) **Controllers > New File** and name the new file `HelloWorldController.cs`.

Replace the contents of `Controllers/HelloWorldController.cs` with the following code:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    // 
    // GET: /HelloWorld/
    public string Index()
    {
        return "This is my default action...";
    }
    // 
    // GET: /HelloWorld/Welcome/ 
    public string Welcome()
    {
        return "This is the Welcome action method...";
    }
}
```

Every `public` method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a string. Note the comments preceding each method.

An HTTP endpoint:

* Is a targetable URL in the web application, such as `https://localhost:5001/HelloWorld`.

* Combines:

  * The protocol used: `HTTPS`.
  * The network location of the web server, including the TCP port: `localhost:5001`.
  * The target URI: `HelloWorld`.

The first comment states this is an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods/GET) method that's invoked by appending `/HelloWorld/` to the base URL.

The second comment specifies an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods) method that's invoked by appending `/HelloWorld/Welcome/` to the URL. Later on in the tutorial, the scaffolding engine is used to generate `HTTP POST` methods, which update data.

Run the app without the debugger by pressing `Ctrl`+`F5`.

Append `/HelloWorld` to the path in the address bar. The `Index` method returns a string.

MVC invokes controller classes, and the action methods within them, depending on the incoming URL. The default [URL routing logic](../../mvc/controllers/routing?view=aspnetcore-10.0) used by MVC, uses a format like this to determine what code to invoke:

`/[Controller]/[ActionName]/[Parameters]`

The routing format is set in the `Program.cs` file.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();
```

When you browse to the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index" method specified in the template line highlighted above. In the preceding URL segments:

* The first URL segment determines the controller class to run. So `localhost:5001/HelloWorld` maps to the **HelloWorld** Controller class.
* The second part of the URL segment determines the action method on the class. So `localhost:5001/HelloWorld/Index` causes the `Index` method of the `HelloWorldController` class to run. Notice that you only had to browse to `localhost:5001/HelloWorld` and the `Index` method was called by default. `Index` is the default method that will be called on a controller if a method name isn't explicitly specified.
* The third part of the URL segment ( `id`) is for route data. Route data is explained later in the tutorial.

Browse to: `https://localhost:{PORT}/HelloWorld/Welcome`. Replace `{PORT}` with your port number.

The `Welcome` method runs and returns the string `This is the Welcome action method...`. For this URL, the controller is `HelloWorld` and `Welcome` is the action method. You haven't used the `[Parameters]` part of the URL yet.

Modify the code to pass some parameter information from the URL to the controller. For example, `/HelloWorld/Welcome?name=Rick&numtimes=4`.

Change the `Welcome` method to include two parameters as shown in the following code.

```
// GET: /HelloWorld/Welcome/ 
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
```

The preceding code:

* Uses the C# optional-parameter feature to indicate that the `numTimes` parameter defaults to 1 if no value is passed for that parameter.
* Uses `HtmlEncoder.Default.Encode` to protect the app from malicious input, such as through JavaScript.
* Uses [Interpolated Strings](/en-us/dotnet/articles/csharp/language-reference/keywords/interpolated-strings) in `$"Hello {name}, NumTimes is: {numTimes}"`.

Run the app and browse to: `https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`. Replace `{PORT}` with your port number.

Try different values for `name` and `numtimes` in the URL. The MVC [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system automatically maps the named parameters from the query string to parameters in the method. See [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0) for more information.

In the previous image:

* The URL segment `Parameters` isn't used.
* The `name` and `numTimes` parameters are passed in the [query string](https://wikipedia.org/wiki/Query_string).
* The `?` (question mark) in the above URL is a separator, and the query string follows.
* The `&` character separates field-value pairs.

Replace the `Welcome` method with the following code:

```
public string Welcome(string name, int ID = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}
```

Run the app and enter the following URL: `https://localhost:{PORT}/HelloWorld/Welcome/3?name=Rick`

In the preceding URL:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` starts the [query string](https://wikipedia.org/wiki/Query_string).

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();
```

In the preceding example:

* The third URL segment matched the route parameter `id` as defined in the routing template in the `Program.cs` file.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` (in `id?`) indicates the `id` parameter is optional.

[Previous: Get Started](start-mvc?view=aspnetcore-10.0) [Next: Add a View](adding-view?view=aspnetcore-10.0)

The Model-View-Controller (MVC) architectural pattern separates an app into three main components: **M**odel, **V**iew, and **C**ontroller. The MVC pattern helps you create apps that are more testable and easier to update than traditional monolithic apps.

MVC-based apps contain:

* **M**odels: Classes that represent the data of the app. The model classes use validation logic to enforce business rules for that data. Typically, model objects retrieve and store model state in a database. In this tutorial, a `Movie` model retrieves movie data from a database, provides it to the view or updates it. Updated data is written to a database.

* **V**iews: Views are the components that display the app's user interface (UI). Generally, this UI displays the model data.

* **C**ontrollers: Classes that:

  * Handle browser requests.
  * Retrieve model data.
  * Call view templates that return a response.

In an MVC app, the view only displays information. The controller handles and responds to user input and interaction. For example, the controller handles URL segments and query-string values, and passes these values to the model. The model might use these values to query the database. For example:

* `https://localhost:5001/Home/Privacy`: specifies the `Home` controller and the `Privacy` action.
* `https://localhost:5001/Movies/Edit/5`: is a request to edit the movie with ID=5 using the `Movies` controller and the `Edit` action, which are detailed later in the tutorial.

Route data is explained later in the tutorial.

The MVC architectural pattern separates an app into three main groups of components: Models, Views, and Controllers. This pattern helps to achieve separation of concerns: The UI logic belongs in the view. Input logic belongs in the controller. Business logic belongs in the model. This separation helps manage complexity when building an app, because it enables work on one aspect of the implementation at a time without impacting the code of another. For example, you can work on the view code without depending on the business logic code.

These concepts are introduced and demonstrated in this tutorial series while building a movie app. The MVC project contains folders for the *Controllers* and *Views*.

## Add a controller

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

In **Solution Explorer**, right-click **Controllers > Add > Controller**.

In the **Add New Scaffolded Item** dialog box, select **MVC Controller - Empty** > **Add**.

In the **Add New Item - MvcMovie** dialog, enter *`HelloWorldController.cs`* and select **Add**.

Select the **EXPLORER** icon and then control-click (right-click) **Controllers > New File** and name the new file `HelloWorldController.cs`.

Replace the contents of `Controllers/HelloWorldController.cs` with the following code:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    // 
    // GET: /HelloWorld/
    public string Index()
    {
        return "This is my default action...";
    }
    // 
    // GET: /HelloWorld/Welcome/ 
    public string Welcome()
    {
        return "This is the Welcome action method...";
    }
}
```

Every `public` method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a string. Note the comments preceding each method.

An HTTP endpoint:

* Is a targetable URL in the web application, such as `https://localhost:5001/HelloWorld`.

* Combines:

  * The protocol used: `HTTPS`.
  * The network location of the web server, including the TCP port: `localhost:5001`.
  * The target URI: `HelloWorld`.

The first comment states this is an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods/GET) method that's invoked by appending `/HelloWorld/` to the base URL.

The second comment specifies an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods) method that's invoked by appending `/HelloWorld/Welcome/` to the URL. Later on in the tutorial, the scaffolding engine is used to generate `HTTP POST` methods, which update data.

Run the app without the debugger by pressing `Ctrl`+`F5`.

Append `/HelloWorld` to the path in the address bar. The `Index` method returns a string.

MVC invokes controller classes, and the action methods within them, depending on the incoming URL. The default [URL routing logic](../../mvc/controllers/routing?view=aspnetcore-10.0) used by MVC, uses a format like this to determine what code to invoke:

`/[Controller]/[ActionName]/[Parameters]`

The routing format is set in the `Program.cs` file.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

When you browse to the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index" method specified in the template line highlighted above. In the preceding URL segments:

* The first URL segment determines the controller class to run. So `localhost:5001/HelloWorld` maps to the **HelloWorld** Controller class.
* The second part of the URL segment determines the action method on the class. So `localhost:5001/HelloWorld/Index` causes the `Index` method of the `HelloWorldController` class to run. Notice that you only had to browse to `localhost:5001/HelloWorld` and the `Index` method was called by default. `Index` is the default method that will be called on a controller if a method name isn't explicitly specified.
* The third part of the URL segment ( `id`) is for route data. Route data is explained later in the tutorial.

Browse to: `https://localhost:{PORT}/HelloWorld/Welcome`. Replace `{PORT}` with your port number.

The `Welcome` method runs and returns the string `This is the Welcome action method...`. For this URL, the controller is `HelloWorld` and `Welcome` is the action method. You haven't used the `[Parameters]` part of the URL yet.

Modify the code to pass some parameter information from the URL to the controller. For example, `/HelloWorld/Welcome?name=Rick&numtimes=4`.

Change the `Welcome` method to include two parameters as shown in the following code.

```
// GET: /HelloWorld/Welcome/ 
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
```

The preceding code:

* Uses the C# optional-parameter feature to indicate that the `numTimes` parameter defaults to 1 if no value is passed for that parameter.
* Uses `HtmlEncoder.Default.Encode` to protect the app from malicious input, such as through JavaScript.
* Uses [Interpolated Strings](/en-us/dotnet/articles/csharp/language-reference/keywords/interpolated-strings) in `$"Hello {name}, NumTimes is: {numTimes}"`.

Run the app and browse to: `https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`. Replace `{PORT}` with your port number.

Try different values for `name` and `numtimes` in the URL. The MVC [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system automatically maps the named parameters from the query string to parameters in the method. See [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0) for more information.

In the previous image:

* The URL segment `Parameters` isn't used.
* The `name` and `numTimes` parameters are passed in the [query string](https://wikipedia.org/wiki/Query_string).
* The `?` (question mark) in the above URL is a separator, and the query string follows.
* The `&` character separates field-value pairs.

Replace the `Welcome` method with the following code:

```
public string Welcome(string name, int ID = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}
```

Run the app and enter the following URL: `https://localhost:{PORT}/HelloWorld/Welcome/3?name=Rick`

In the preceding URL:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` starts the [query string](https://wikipedia.org/wiki/Query_string).

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

In the preceding example:

* The third URL segment matched the route parameter `id` as defined in the routing template in the `Program.cs` file.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` (in `id?`) indicates the `id` parameter is optional.

[Previous: Get Started](start-mvc?view=aspnetcore-10.0) [Next: Add a View](adding-view?view=aspnetcore-10.0)

The Model-View-Controller (MVC) architectural pattern separates an app into three main components: **M**odel, **V**iew, and **C**ontroller. The MVC pattern helps you create apps that are more testable and easier to update than traditional monolithic apps.

MVC-based apps contain:

* **M**odels: Classes that represent the data of the app. The model classes use validation logic to enforce business rules for that data. Typically, model objects retrieve and store model state in a database. In this tutorial, a `Movie` model retrieves movie data from a database, provides it to the view or updates it. Updated data is written to a database.

* **V**iews: Views are the components that display the app's user interface (UI). Generally, this UI displays the model data.

* **C**ontrollers: Classes that:

  * Handle browser requests.
  * Retrieve model data.
  * Call view templates that return a response.

In an MVC app, the view only displays information. The controller handles and responds to user input and interaction. For example, the controller handles URL segments and query-string values, and passes these values to the model. The model might use these values to query the database. For example:

* `https://localhost:5001/Home/Privacy`: specifies the `Home` controller and the `Privacy` action.
* `https://localhost:5001/Movies/Edit/5`: is a request to edit the movie with ID=5 using the `Movies` controller and the `Edit` action, which are detailed later in the tutorial.

Route data is explained later in the tutorial.

The MVC architectural pattern separates an app into three main groups of components: Models, Views, and Controllers. This pattern helps to achieve separation of concerns: The UI logic belongs in the view. Input logic belongs in the controller. Business logic belongs in the model. This separation helps manage complexity when building an app, because it enables work on one aspect of the implementation at a time without impacting the code of another. For example, you can work on the view code without depending on the business logic code.

These concepts are introduced and demonstrated in this tutorial series while building a movie app. The MVC project contains folders for the *Controllers* and *Views*.

## Add a controller

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

In **Solution Explorer**, right-click **Controllers > Add > Controller**.

In the **Add New Scaffolded Item** dialog box, select **MVC Controller - Empty** > **Add**.

In the **Add New Item - MvcMovie** dialog, enter *`HelloWorldController.cs`* and select **Add**.

Select the **EXPLORER** icon and then control-click (right-click) **Controllers > New File** and name the new file `HelloWorldController.cs`.

Replace the contents of `Controllers/HelloWorldController.cs` with the following code:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    // 
    // GET: /HelloWorld/
    public string Index()
    {
        return "This is my default action...";
    }
    // 
    // GET: /HelloWorld/Welcome/ 
    public string Welcome()
    {
        return "This is the Welcome action method...";
    }
}
```

Every `public` method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a string. Note the comments preceding each method.

An HTTP endpoint:

* Is a targetable URL in the web application, such as `https://localhost:5001/HelloWorld`.

* Combines:

  * The protocol used: `HTTPS`.
  * The network location of the web server, including the TCP port: `localhost:5001`.
  * The target URI: `HelloWorld`.

The first comment states this is an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods/GET) method that's invoked by appending `/HelloWorld/` to the base URL.

The second comment specifies an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods) method that's invoked by appending `/HelloWorld/Welcome/` to the URL. Later on in the tutorial, the scaffolding engine is used to generate `HTTP POST` methods, which update data.

Run the app without the debugger by pressing `Ctrl`+`F5`.

Append `/HelloWorld` to the path in the address bar. The `Index` method returns a string.

MVC invokes controller classes, and the action methods within them, depending on the incoming URL. The default [URL routing logic](../../mvc/controllers/routing?view=aspnetcore-10.0) used by MVC, uses a format like this to determine what code to invoke:

`/[Controller]/[ActionName]/[Parameters]`

The routing format is set in the `Program.cs` file.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

When you browse to the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index" method specified in the template line highlighted above. In the preceding URL segments:

* The first URL segment determines the controller class to run. So `localhost:5001/HelloWorld` maps to the **HelloWorld** Controller class.
* The second part of the URL segment determines the action method on the class. So `localhost:5001/HelloWorld/Index` causes the `Index` method of the `HelloWorldController` class to run. Notice that you only had to browse to `localhost:5001/HelloWorld` and the `Index` method was called by default. `Index` is the default method that will be called on a controller if a method name isn't explicitly specified.
* The third part of the URL segment ( `id`) is for route data. Route data is explained later in the tutorial.

Browse to: `https://localhost:{PORT}/HelloWorld/Welcome`. Replace `{PORT}` with your port number.

The `Welcome` method runs and returns the string `This is the Welcome action method...`. For this URL, the controller is `HelloWorld` and `Welcome` is the action method. You haven't used the `[Parameters]` part of the URL yet.

Modify the code to pass some parameter information from the URL to the controller. For example, `/HelloWorld/Welcome?name=Rick&numtimes=4`.

Change the `Welcome` method to include two parameters as shown in the following code.

```
// GET: /HelloWorld/Welcome/ 
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
```

The preceding code:

* Uses the C# optional-parameter feature to indicate that the `numTimes` parameter defaults to 1 if no value is passed for that parameter.
* Uses `HtmlEncoder.Default.Encode` to protect the app from malicious input, such as through JavaScript.
* Uses [Interpolated Strings](/en-us/dotnet/articles/csharp/language-reference/keywords/interpolated-strings) in `$"Hello {name}, NumTimes is: {numTimes}"`.

Run the app and browse to: `https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`. Replace `{PORT}` with your port number.

Try different values for `name` and `numtimes` in the URL. The MVC [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system automatically maps the named parameters from the query string to parameters in the method. See [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0) for more information.

In the previous image:

* The URL segment `Parameters` isn't used.
* The `name` and `numTimes` parameters are passed in the [query string](https://wikipedia.org/wiki/Query_string).
* The `?` (question mark) in the above URL is a separator, and the query string follows.
* The `&` character separates field-value pairs.

Replace the `Welcome` method with the following code:

```
public string Welcome(string name, int ID = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}
```

Run the app and enter the following URL: `https://localhost:{PORT}/HelloWorld/Welcome/3?name=Rick`

In the preceding URL:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` starts the [query string](https://wikipedia.org/wiki/Query_string).

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

In the preceding example:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` (in `id?`) indicates the `id` parameter is optional.

[Previous: Get Started](start-mvc?view=aspnetcore-10.0) [Next: Add a View](adding-view?view=aspnetcore-10.0)

The Model-View-Controller (MVC) architectural pattern separates an app into three main components: **M**odel, **V**iew, and **C**ontroller. The MVC pattern helps you create apps that are more testable and easier to update than traditional monolithic apps.

MVC-based apps contain:

* **M**odels: Classes that represent the data of the app. The model classes use validation logic to enforce business rules for that data. Typically, model objects retrieve and store model state in a database. In this tutorial, a `Movie` model retrieves movie data from a database, provides it to the view or updates it. Updated data is written to a database.

* **V**iews: Views are the components that display the app's user interface (UI). Generally, this UI displays the model data.

* **C**ontrollers: Classes that:

  * Handle browser requests.
  * Retrieve model data.
  * Call view templates that return a response.

In an MVC app, the view only displays information. The controller handles and responds to user input and interaction. For example, the controller handles URL segments and query-string values, and passes these values to the model. The model might use these values to query the database. For example:

* `https://localhost:5001/Home/Privacy`: specifies the `Home` controller and the `Privacy` action.
* `https://localhost:5001/Movies/Edit/5`: is a request to edit the movie with ID=5 using the `Movies` controller and the `Edit` action, which are detailed later in the tutorial.

Route data is explained later in the tutorial.

The MVC architectural pattern separates an app into three main groups of components: Models, Views, and Controllers. This pattern helps to achieve separation of concerns: The UI logic belongs in the view. Input logic belongs in the controller. Business logic belongs in the model. This separation helps manage complexity when building an app, because it enables work on one aspect of the implementation at a time without impacting the code of another. For example, you can work on the view code without depending on the business logic code.

These concepts are introduced and demonstrated in this tutorial series while building a movie app. The MVC project contains folders for the *Controllers* and *Views*.

## Add a controller

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

In **Solution Explorer**, right-click **Controllers > Add > Controller**.

In the **Add New Scaffolded Item** dialog box, select **MVC Controller - Empty** > **Add**.

In the **Add New Item - MvcMovie** dialog, enter *`HelloWorldController.cs`* and select **Add**.

Select the **EXPLORER** icon and then control-click (right-click) **Controllers > New File** and name the new file `HelloWorldController.cs`.

In **Solution Explorer**, control-click **Controllers** and select **Add > New File**.

Select **ASP.NET Core** and **Controller Class**.

Name the controller **HelloWorldController**.

Replace the contents of `Controllers/HelloWorldController.cs` with the following code:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    // 
    // GET: /HelloWorld/
    public string Index()
    {
        return "This is my default action...";
    }
    // 
    // GET: /HelloWorld/Welcome/ 
    public string Welcome()
    {
        return "This is the Welcome action method...";
    }
}
```

Every `public` method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a string. Note the comments preceding each method.

An HTTP endpoint:

* Is a targetable URL in the web application, such as `https://localhost:5001/HelloWorld`.

* Combines:

  * The protocol used: `HTTPS`.
  * The network location of the web server, including the TCP port: `localhost:5001`.
  * The target URI: `HelloWorld`.

The first comment states this is an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods/GET) method that's invoked by appending `/HelloWorld/` to the base URL.

The second comment specifies an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods) method that's invoked by appending `/HelloWorld/Welcome/` to the URL. Later on in the tutorial, the scaffolding engine is used to generate `HTTP POST` methods, which update data.

Run the app without the debugger by pressing `Ctrl`+`F5`.

Append `/HelloWorld` to the path in the address bar. The `Index` method returns a string.

MVC invokes controller classes, and the action methods within them, depending on the incoming URL. The default [URL routing logic](../../mvc/controllers/routing?view=aspnetcore-10.0) used by MVC, uses a format like this to determine what code to invoke:

`/[Controller]/[ActionName]/[Parameters]`

The routing format is set in the `Program.cs` file.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

When you browse to the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index" method specified in the template line highlighted above. In the preceding URL segments:

* The first URL segment determines the controller class to run. So `localhost:5001/HelloWorld` maps to the **HelloWorld** Controller class.
* The second part of the URL segment determines the action method on the class. So `localhost:5001/HelloWorld/Index` causes the `Index` method of the `HelloWorldController` class to run. Notice that you only had to browse to `localhost:5001/HelloWorld` and the `Index` method was called by default. `Index` is the default method that will be called on a controller if a method name isn't explicitly specified.
* The third part of the URL segment ( `id`) is for route data. Route data is explained later in the tutorial.

Browse to: `https://localhost:{PORT}/HelloWorld/Welcome`. Replace `{PORT}` with your port number.

The `Welcome` method runs and returns the string `This is the Welcome action method...`. For this URL, the controller is `HelloWorld` and `Welcome` is the action method. You haven't used the `[Parameters]` part of the URL yet.

Modify the code to pass some parameter information from the URL to the controller. For example, `/HelloWorld/Welcome?name=Rick&numtimes=4`.

Change the `Welcome` method to include two parameters as shown in the following code.

```
// GET: /HelloWorld/Welcome/ 
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
```

The preceding code:

* Uses the C# optional-parameter feature to indicate that the `numTimes` parameter defaults to 1 if no value is passed for that parameter.
* Uses `HtmlEncoder.Default.Encode` to protect the app from malicious input, such as through JavaScript.
* Uses [Interpolated Strings](/en-us/dotnet/articles/csharp/language-reference/keywords/interpolated-strings) in `$"Hello {name}, NumTimes is: {numTimes}"`.

Run the app and browse to: `https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`. Replace `{PORT}` with your port number.

Try different values for `name` and `numtimes` in the URL. The MVC [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system automatically maps the named parameters from the query string to parameters in the method. See [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0) for more information.

In the previous image:

* The URL segment `Parameters` isn't used.
* The `name` and `numTimes` parameters are passed in the [query string](https://wikipedia.org/wiki/Query_string).
* The `?` (question mark) in the above URL is a separator, and the query string follows.
* The `&` character separates field-value pairs.

Replace the `Welcome` method with the following code:

```
public string Welcome(string name, int ID = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}
```

Run the app and enter the following URL: `https://localhost:{PORT}/HelloWorld/Welcome/3?name=Rick`

In the preceding URL:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` starts the [query string](https://wikipedia.org/wiki/Query_string).

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

In the preceding example:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` (in `id?`) indicates the `id` parameter is optional.

[Previous: Get Started](start-mvc?view=aspnetcore-10.0) [Next: Add a View](adding-view?view=aspnetcore-10.0)

The Model-View-Controller (MVC) architectural pattern separates an app into three main components: **M**odel, **V**iew, and **C**ontroller. The MVC pattern helps you create apps that are more testable and easier to update than traditional monolithic apps.

MVC-based apps contain:

* **M**odels: Classes that represent the data of the app. The model classes use validation logic to enforce business rules for that data. Typically, model objects retrieve and store model state in a database. In this tutorial, a `Movie` model retrieves movie data from a database, provides it to the view or updates it. Updated data is written to a database.

* **V**iews: Views are the components that display the app's user interface (UI). Generally, this UI displays the model data.

* **C**ontrollers: Classes that:

  * Handle browser requests.
  * Retrieve model data.
  * Call view templates that return a response.

In an MVC app, the view only displays information. The controller handles and responds to user input and interaction. For example, the controller handles URL segments and query-string values, and passes these values to the model. The model might use these values to query the database. For example:

* `https://localhost:5001/Home/Privacy`: specifies the `Home` controller and the `Privacy` action.
* `https://localhost:5001/Movies/Edit/5`: is a request to edit the movie with ID=5 using the `Movies` controller and the `Edit` action, which are detailed later in the tutorial.

Route data is explained later in the tutorial.

The MVC architectural pattern separates an app into three main groups of components: Models, Views, and Controllers. This pattern helps to achieve separation of concerns: The UI logic belongs in the view. Input logic belongs in the controller. Business logic belongs in the model. This separation helps manage complexity when building an app, because it enables work on one aspect of the implementation at a time without impacting the code of another. For example, you can work on the view code without depending on the business logic code.

These concepts are introduced and demonstrated in this tutorial series while building a movie app. The MVC project contains folders for the *Controllers* and *Views*.

## Add a controller

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

In **Solution Explorer**, right-click **Controllers > Add > Controller**.

In the **Add New Scaffolded Item** dialog box, select **MVC Controller - Empty** > **Add**.

In the **Add New Item - MvcMovie** dialog, enter *`HelloWorldController.cs`* and select **Add**.

Select the **EXPLORER** icon and then control-click (right-click) **Controllers > New File** and name the new file `HelloWorldController.cs`.

For Visual Studio for Mac, see the .NET 7 version of this tutorial.

Replace the contents of `Controllers/HelloWorldController.cs` with the following code:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
    public class HelloWorldController : Controller
    {
        // 
        // GET: /HelloWorld/

        public string Index()
        {
            return "This is my default action...";
        }

        // 
        // GET: /HelloWorld/Welcome/ 

        public string Welcome()
        {
            return "This is the Welcome action method...";
        }
    }
}
```

Every `public` method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a string. Note the comments preceding each method.

An HTTP endpoint:

* Is a targetable URL in the web application, such as `https://localhost:5001/HelloWorld`.

* Combines:

  * The protocol used: `HTTPS`.
  * The network location of the web server, including the TCP port: `localhost:5001`.
  * The target URI: `HelloWorld`.

The first comment states this is an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods/GET) method that's invoked by appending `/HelloWorld/` to the base URL.

The second comment specifies an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods) method that's invoked by appending `/HelloWorld/Welcome/` to the URL. Later on in the tutorial, the scaffolding engine is used to generate `HTTP POST` methods, which update data.

Run the app without the debugger.

Append "HelloWorld" to the path in the address bar. The `Index` method returns a string.

MVC invokes controller classes, and the action methods within them, depending on the incoming URL. The default [URL routing logic](../../mvc/controllers/routing?view=aspnetcore-10.0) used by MVC, uses a format like this to determine what code to invoke:

`/[Controller]/[ActionName]/[Parameters]`

The routing format is set in the `Program.cs` file.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

When you browse to the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index" method specified in the template line highlighted above. In the preceding URL segments:

* The first URL segment determines the controller class to run. So `localhost:5001/HelloWorld` maps to the **HelloWorld** Controller class.
* The second part of the URL segment determines the action method on the class. So `localhost:5001/HelloWorld/Index` causes the `Index` method of the `HelloWorldController` class to run. Notice that you only had to browse to `localhost:5001/HelloWorld` and the `Index` method was called by default. `Index` is the default method that will be called on a controller if a method name isn't explicitly specified.
* The third part of the URL segment ( `id`) is for route data. Route data is explained later in the tutorial.

Browse to: `https://localhost:{PORT}/HelloWorld/Welcome`. Replace `{PORT}` with your port number.

The `Welcome` method runs and returns the string `This is the Welcome action method...`. For this URL, the controller is `HelloWorld` and `Welcome` is the action method. You haven't used the `[Parameters]` part of the URL yet.

Modify the code to pass some parameter information from the URL to the controller. For example, `/HelloWorld/Welcome?name=Rick&numtimes=4`.

Change the `Welcome` method to include two parameters as shown in the following code.

```
// GET: /HelloWorld/Welcome/ 
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
```

The preceding code:

* Uses the C# optional-parameter feature to indicate that the `numTimes` parameter defaults to 1 if no value is passed for that parameter.
* Uses `HtmlEncoder.Default.Encode` to protect the app from malicious input, such as through JavaScript.
* Uses [Interpolated Strings](/en-us/dotnet/articles/csharp/language-reference/keywords/interpolated-strings) in `$"Hello {name}, NumTimes is: {numTimes}"`.

Run the app and browse to: `https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`. Replace `{PORT}` with your port number.

Try different values for `name` and `numtimes` in the URL. The MVC [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system automatically maps the named parameters from the query string to parameters in the method. See [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0) for more information.

In the previous image:

* The URL segment `Parameters` isn't used.
* The `name` and `numTimes` parameters are passed in the [query string](https://wikipedia.org/wiki/Query_string).
* The `?` (question mark) in the above URL is a separator, and the query string follows.
* The `&` character separates field-value pairs.

Replace the `Welcome` method with the following code:

```
public string Welcome(string name, int ID = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}
```

Run the app and enter the following URL: `https://localhost:{PORT}/HelloWorld/Welcome/3?name=Rick`

In the preceding URL:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` starts the [query string](https://wikipedia.org/wiki/Query_string).

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

In the preceding example:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` (in `id?`) indicates the `id` parameter is optional.

[Previous: Get Started](start-mvc?view=aspnetcore-10.0) [Next: Add a View](adding-view?view=aspnetcore-10.0)

The Model-View-Controller (MVC) architectural pattern separates an app into three main components: **M**odel, **V**iew, and **C**ontroller. The MVC pattern helps you create apps that are more testable and easier to update than traditional monolithic apps.

MVC-based apps contain:

* **M**odels: Classes that represent the data of the app. The model classes use validation logic to enforce business rules for that data. Typically, model objects retrieve and store model state in a database. In this tutorial, a `Movie` model retrieves movie data from a database, provides it to the view or updates it. Updated data is written to a database.

* **V**iews: Views are the components that display the app's user interface (UI). Generally, this UI displays the model data.

* **C**ontrollers: Classes that:

  * Handle browser requests.
  * Retrieve model data.
  * Call view templates that return a response.

In an MVC app, the view only displays information. The controller handles and responds to user input and interaction. For example, the controller handles URL segments and query-string values, and passes these values to the model. The model might use these values to query the database. For example:

* `https://localhost:5001/Home/Privacy`: specifies the `Home` controller and the `Privacy` action.
* `https://localhost:5001/Movies/Edit/5`: is a request to edit the movie with ID=5 using the `Movies` controller and the `Edit` action, which are detailed later in the tutorial.

Route data is explained later in the tutorial.

The MVC architectural pattern separates an app into three main groups of components: Models, Views, and Controllers. This pattern helps to achieve separation of concerns: The UI logic belongs in the view. Input logic belongs in the controller. Business logic belongs in the model. This separation helps manage complexity when building an app, because it enables work on one aspect of the implementation at a time without impacting the code of another. For example, you can work on the view code without depending on the business logic code.

These concepts are introduced and demonstrated in this tutorial series while building a movie app. The MVC project contains folders for the *Controllers* and *Views*.

## Add a controller

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

In the **Solution Explorer**, right-click **Controllers > Add > Controller**.

In the **Add Scaffold** dialog box, select **MVC Controller - Empty**.

In the **Add New Item - MvcMovie dialog**, enter *`HelloWorldController.cs`* and select **Add**.

Select the **EXPLORER** icon and then control-click (right-click) **Controllers > New File** and name the new file `HelloWorldController.cs`.

In **Solution Explorer**, right-click **Controllers > Add > New File**.

Select **ASP.NET Core** and **Controller Class**.

Name the controller **HelloWorldController**.

Replace the contents of `Controllers/HelloWorldController.cs` with the following:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
    public class HelloWorldController : Controller
    {
        // 
        // GET: /HelloWorld/

        public string Index()
        {
            return "This is my default action...";
        }

        // 
        // GET: /HelloWorld/Welcome/ 

        public string Welcome()
        {
            return "This is the Welcome action method...";
        }
    }
}
```

Every `public` method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a string. Note the comments preceding each method.

An HTTP endpoint:

* Is a targetable URL in the web application, such as `https://localhost:5001/HelloWorld`.

* Combines:

  * The protocol used: `HTTPS`.
  * The network location of the web server, including the TCP port: `localhost:5001`.
  * The target URI: `HelloWorld`.

The first comment states this is an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods/GET) method that's invoked by appending `/HelloWorld/` to the base URL.

The second comment specifies an [HTTP GET](https://developer.mozilla.org/docs/Web/HTTP/Methods) method that's invoked by appending `/HelloWorld/Welcome/` to the URL. Later on in the tutorial, the scaffolding engine is used to generate `HTTP POST` methods, which update data.

Run the app without the debugger.

Append "HelloWorld" to the path in the address bar. The `Index` method returns a string.

MVC invokes controller classes, and the action methods within them, depending on the incoming URL. The default [URL routing logic](../../mvc/controllers/routing?view=aspnetcore-10.0) used by MVC, uses a format like this to determine what code to invoke:

`/[Controller]/[ActionName]/[Parameters]`

The routing format is set in the `Configure` method in `Startup.cs` file.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

When you browse to the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index" method specified in the template line highlighted above. In the preceding URL segments:

* The first URL segment determines the controller class to run. So `localhost:5001/HelloWorld` maps to the **HelloWorld**Controller class.
* The second part of the URL segment determines the action method on the class. So `localhost:5001/HelloWorld/Index` causes the `Index` method of the `HelloWorldController` class to run. Notice that you only had to browse to `localhost:5001/HelloWorld` and the `Index` method was called by default. `Index` is the default method that will be called on a controller if a method name isn't explicitly specified.
* The third part of the URL segment ( `id`) is for route data. Route data is explained later in the tutorial.

Browse to: `https://localhost:{PORT}/HelloWorld/Welcome`. Replace `{PORT}` with your port number.

The `Welcome` method runs and returns the string `This is the Welcome action method...`. For this URL, the controller is `HelloWorld` and `Welcome` is the action method. You haven't used the `[Parameters]` part of the URL yet.

Modify the code to pass some parameter information from the URL to the controller. For example, `/HelloWorld/Welcome?name=Rick&numtimes=4`.

Change the `Welcome` method to include two parameters as shown in the following code.

```
// GET: /HelloWorld/Welcome/ 
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}
```

The preceding code:

* Uses the C# optional-parameter feature to indicate that the `numTimes` parameter defaults to 1 if no value is passed for that parameter.
* Uses `HtmlEncoder.Default.Encode` to protect the app from malicious input, such as through JavaScript.
* Uses [Interpolated Strings](/en-us/dotnet/articles/csharp/language-reference/keywords/interpolated-strings) in `$"Hello {name}, NumTimes is: {numTimes}"`.

Run the app and browse to: `https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`. Replace `{PORT}` with your port number.

Try different values for `name` and `numtimes` in the URL. The MVC [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system automatically maps the named parameters from the query string to parameters in the method. See [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0) for more information.

In the previous image:

* The URL segment `Parameters` isn't used.
* The `name` and `numTimes` parameters are passed in the [query string](https://wikipedia.org/wiki/Query_string).
* The `?` (question mark) in the above URL is a separator, and the query string follows.
* The `&` character separates field-value pairs.

Replace the `Welcome` method with the following code:

```
public string Welcome(string name, int ID = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}
```

Run the app and enter the following URL: `https://localhost:{PORT}/HelloWorld/Welcome/3?name=Rick`

In the preceding URL:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` starts the [query string](https://wikipedia.org/wiki/Query_string).

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

In the preceding example:

* The third URL segment matched the route parameter `id`.
* The `Welcome` method contains a parameter `id` that matched the URL template in the `MapControllerRoute` method.
* The trailing `?` (in `id?`) indicates the `id` parameter is optional.

[Previous: Get Started](start-mvc?view=aspnetcore-10.0) [Next: Add a View](adding-view?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/performance/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor performance best practices

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Blazor is optimized for high performance in most realistic application UI scenarios. However, the best performance depends on developers adopting the correct patterns and features.

Note

The code examples in this node of articles adopt [nullable reference types (NRTs) and .NET compiler null-state static analysis](../../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis), which are supported in ASP.NET Core in .NET 6 or later.

## Ahead-of-time (AOT) compilation

Ahead-of-time (AOT) compilation compiles a Blazor app's .NET code directly into native WebAssembly for direct execution by the browser. AOT-compiled apps result in larger apps that take longer to download, but AOT-compiled apps usually provide better runtime performance, especially for apps that execute CPU-intensive tasks. For more information, see [ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation](../webassembly-build-tools-and-aot?view=aspnetcore-10.0#ahead-of-time-aot-compilation).

## Metrics and tracing

Metrics and tracing capabilities help you monitor and diagnose app performance, track user interactions, and understand component behavior in production environments.

### Configuration

To enable Blazor metrics and tracing in your app, configure [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-dotnet) with the following meters and activity sources in the app's `Program` file where services are registered:

```
builder.Services.ConfigureOpenTelemetryMeterProvider(meterProvider =>
{
    meterProvider.AddMeter("Microsoft.AspNetCore.Components");
    meterProvider.AddMeter("Microsoft.AspNetCore.Components.Lifecycle");
    meterProvider.AddMeter("Microsoft.AspNetCore.Components.Server.Circuits");
});

builder.Services.ConfigureOpenTelemetryTracerProvider(tracerProvider =>
{
    tracerProvider.AddSource("Microsoft.AspNetCore.Components");
    tracerProvider.AddSource("Microsoft.AspNetCore.Components.Server.Circuits");
});
```

### Performance meters

For more information on the following performance meters, see [ASP.NET Core built-in metrics](../../metrics/built-in?view=aspnetcore-10.0).

`Microsoft.AspNetCore.Components` meter:

* `aspnetcore.components.navigate`: Tracks the total number of route changes in the app.
* `aspnetcore.components.handle_event.duration`: Measures the duration of processing browser events, including business logic.

`Microsoft.AspNetCore.Components.Lifecycle` meter:

* `aspnetcore.components.update_parameters.duration`: Measures the duration of processing component parameters, including business logic.
* `aspnetcore.components.render_diff.duration`: Tracks the duration of rendering batches.
* `aspnetcore.components.render_diff.size`: Tracks the size of rendering batches.

`Microsoft.AspNetCore.Components.Server.Circuits` meter:

In server-side Blazor apps, additional circuit-specific metrics include:

* `aspnetcore.components.circuit.active`: Shows the number of active circuits currently in memory.
* `aspnetcore.components.circuit.connected`: Tracks the number of circuits connected to clients.
* `aspnetcore.components.circuit.duration`: Measures circuit lifetime duration and provides total circuit count.

### Blazor tracing

For more information on the following tracing activities, see [ASP.NET Core built-in metrics](../../metrics/built-in?view=aspnetcore-10.0).

The new activity tracing capabilities use the `Microsoft.AspNetCore.Components` activity source and provide three main types of tracing activities: circuit lifecycle, navigation, and event handling.

Circuit lifecycle tracing:

`Microsoft.AspNetCore.Components.StartCircuit`: Traces circuit initialization with the format `Circuit {circuitId}`.

Tags:

* `aspnetcore.components.circuit.id`: Unique circuit identifier.
* `error.type`: Exception type full name (optional)

Links:

* HTTP trace
* SignalR trace

Usage: Links other Blazor traces of the same session/circuit to HTTP and SignalR contexts.

Navigation tracing:

`Microsoft.AspNetCore.Components.Navigate`: Tracks route changes with the format `Route {route} -> {componentType}`.

Tags:

* `aspnetcore.components.route`: URL path pattern of the page.
* `aspnetcore.components.type`: Class name of the Razor component.
* `error.type`: Exception type full name (optional).

Links:

* HTTP trace
* SignalR trace
* Circuit trace

Usage: Which Blazor pages this session visited?

Event handling tracing:

`Microsoft.AspNetCore.Components.HandleEvent`: Traces event handling with the format `Event {attributeName} -> {componentType}.{methodName}`.

Tags:

* `aspnetcore.components.attribute.name`: Name of the HTML attribute that triggers the event (example: `onClick`).
* `code.function.name`: C# method name of the handler.
* `aspnetcore.components.type`: Full name of target C# component that receives the event.
* `error.type`: Exception type full name (optional).

Links:

* Circuit trace
* Route trace

Usages:

* Click to which component caused exception and on which page?
* In which linked circuit and with what HTTP context it happened?

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/api-endpoint-auth
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/api-endpoint-auth.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# API endpoint authentication behavior in ASP.NET Core

When using cookie authentication, API endpoints return the appropriate HTTP status codes (401 or 403) for authentication failures instead of redirecting unauthenticated requests to login pages. This behavior, which is more suitable for programmatic API access, was introduced in ASP.NET Core in .NET 10.

## How ASP.NET Core identifies API endpoints

ASP.NET Core adds [IDisableCookieRedirectMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.idisablecookieredirectmetadata) to the endpoints that it recognizes as API endpoints, including:

* Controllers decorated with the `[ApiController]` attribute.
* Minimal API endpoints that read JSON request bodies or write JSON responses.
* Endpoints using [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) return types.
* SignalR hubs and endpoints.

Detection is based on metadata that's inferred when the app builds its endpoints. It isn't based on the `Accept` header of an incoming request, and it isn't based on which `Map{Verb}` method registered the route.

A minimal API handler whose declared return type is `void`, `string`, or the [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) interface doesn't contribute the metadata through its return type, although the concrete `TypedResults` types such as `Ok<TValue>` do. For example, `app.MapGet("/hello", () => "Hello").RequireAuthorization()` writes a `text/plain` response and takes no JSON request body, so unauthenticated requests to it still redirect to the login page.

## Default behavior

By default, ASP.NET Core applies cookie authentication logic based on the endpoint type:

* **Web pages**: Redirect to the login or access-denied page with a 302 status code.
* **API endpoints**: Return 401 or 403 status codes instead of a 302 redirect.

[XMLHttpRequests (XHRs)](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest) receive 401 and 403 responses regardless of the endpoint they target. That behavior predates .NET 10 and is unchanged.

Note

Endpoint metadata only affects the challenge (401) and forbid (403) paths. Sign-out redirects aren't affected. When a sign-out request specifies a redirect URI or a valid return URL, non-XHR requests redirect as they did before .NET 10. XHR behavior is unchanged.

## Configure the behavior for specific endpoints

Call [DisableCookieRedirect](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieredirectendpointconventionbuilderextensions.disablecookieredirect) to return 401 and 403 status codes for endpoints that aren't detected automatically:

```
var api = app.MapGroup("/api").DisableCookieRedirect();

api.MapGet("/status", () => "Ready")
   .RequireAuthorization();
```

Call [AllowCookieRedirect](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieredirectendpointconventionbuilderextensions.allowcookieredirect) to keep login redirects for endpoints that are detected as API endpoints:

```
app.MapGet("/reports/summary", () => new { Total = 1000 })
   .RequireAuthorization()
   .AllowCookieRedirect();
```

For controllers, apply the [AllowCookieRedirectAttribute](/en-us/dotnet/api/microsoft.aspnetcore.http.allowcookieredirectattribute) to an action method or to the controller class:

```
[ApiController]
[Authorize]
[AllowCookieRedirect]
[Route("[controller]")]
public class ReportsController : ControllerBase
{
    [HttpGet("summary")]
    public object GetSummary() => new { Total = 1000 };
}
```

Important

[IAllowCookieRedirectMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iallowcookieredirectmetadata) overrides [IDisableCookieRedirectMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.idisablecookieredirectmetadata) no matter the order in which the metadata is added. Calling `DisableCookieRedirect` after `AllowCookieRedirect` on the same endpoint doesn't restore the status code behavior.

## Opt out of the behavior app-wide

To restore the pre-.NET 10 behavior for an entire app, enable the `Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata` switch. When it's enabled, cookie authentication ignores endpoint metadata and only XHRs result in 401 and 403 responses.

Set the switch in the project file so that it applies before any app code runs:

```
<ItemGroup>
  <RuntimeHostConfigurationOption
    Include="Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata"
    Value="true" />
</ItemGroup>
```

The switch can also be set in code. It's read once, the first time cookie authentication is used, so call [SetSwitch](/en-us/dotnet/api/system.appcontext.setswitch) before the host is built:

```
AppContext.SetSwitch(
    "Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata", true);

var builder = WebApplication.CreateBuilder(args);
```

## Breaking change considerations

The change introduced in .NET 10 is a [behavioral change](/en-us/dotnet/core/compatibility/categories#behavioral-change). An app that combines cookie authentication with endpoints that ASP.NET Core detects as API endpoints returns 401 and 403 responses where it previously returned a 302 redirect to the login or access-denied page. For the full breaking change notice, see [Cookie login redirects are disabled for known API endpoints](/en-us/aspnet/core/breaking-changes/10/cookie-authentication-api-endpoints).

Consider the impact on each kind of app:

* **Web applications**: Page endpoints continue to redirect to the login page.
* **Mixed applications**: API endpoints return status codes while web pages get redirects, so browser code that followed the redirect must handle 401 and 403 instead.
* **API-only applications**: Detected API endpoints return proper HTTP status codes without additional configuration.

To keep the previous behavior, call `AllowCookieRedirect` on the affected endpoints, apply `[AllowCookieRedirect]` to the affected controllers, or enable the `Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata` switch for the whole app.

### Testing your API endpoints

After upgrading to ASP.NET Core in .NET 10, verify that your API endpoints return appropriate status codes:

```
[Fact]
public async Task UnauthorizedApiRequest_Returns401()
{
    var response = await client.GetAsync("/api/secure-data");

    Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);

    // The handler sets the Location header before setting the status code,
    // so the login URI is still present on the 401 response.
    Assert.NotNull(response.Headers.Location);
}
```

## Related topics

* [Use cookie authentication without ASP.NET Core Identity](cookie?view=aspnetcore-10.0)
* [Create web APIs with ASP.NET Core](../../web-api/?view=aspnetcore-10.0)
* [Create responses in Minimal API applications](../../fundamentals/minimal-apis/responses?view=aspnetcore-10.0)
* [SignalR authentication and authorization](../../signalr/authn-and-authz?view=aspnetcore-10.0)
* [Cookie login redirects are disabled for known API endpoints](/en-us/aspnet/core/breaking-changes/10/cookie-authentication-api-endpoints)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-16

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/interceptors
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/interceptors.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC interceptors on .NET

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Ernest Nguyen](https://github.com/erni27)

Interceptors are a gRPC concept that allows apps to interact with incoming or outgoing gRPC calls. They offer a way to enrich the request processing pipeline.

Interceptors are configured for a channel or service and executed automatically for each gRPC call. Since interceptors are transparent to the user's application logic, they're an excellent solution for common cases, such as logging, monitoring, authentication, and validation.

## `Interceptor` type

Interceptors can be implemented for both gRPC servers and clients by creating a class that inherits from the `Interceptor` type:

```
public class ExampleInterceptor : Interceptor
{
}
```

By default, the `Interceptor` base class doesn't do anything. Add behavior to an interceptor by overriding the appropriate base class methods in an interceptor implementation.

## Client interceptors

gRPC client interceptors intercept outgoing RPC invocations. They provide access to the sent request, the incoming response, and the context for a client-side call.

`Interceptor` methods to override for client:

* `BlockingUnaryCall`: Intercepts a blocking invocation of a unary RPC.
* `AsyncUnaryCall`: Intercepts an asynchronous invocation of an unary RPC.
* `AsyncClientStreamingCall`: Intercepts an asynchronous invocation of a client-streaming RPC.
* `AsyncServerStreamingCall`: Intercepts an asynchronous invocation of a server-streaming RPC.
* `AsyncDuplexStreamingCall`: Intercepts an asynchronous invocation of a bidirectional-streaming RPC.

Warning

Although both `BlockingUnaryCall` and `AsyncUnaryCall` refer to unary RPCs, they aren't interchangeable. A blocking invocation isn't intercepted by `AsyncUnaryCall`, and an asynchronous invocation isn't intercepted by a `BlockingUnaryCall`.

### Create a client gRPC interceptor

The following code presents a basic example of intercepting an asynchronous invocation of a unary call:

```
public class ClientLoggingInterceptor : Interceptor
{
    private readonly ILogger _logger;

    public ClientLoggingInterceptor(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<ClientLoggingInterceptor>();
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        _logger.LogInformation("Starting call. Type/Method: {Type} / {Method}",
            context.Method.Type, context.Method.Name);
        return continuation(request, context);
    }
}
```

Overriding `AsyncUnaryCall`:

* Intercepts an asynchronous unary call.
* Logs details about the call.
* Calls the `continuation` parameter passed into the method. This invokes the next interceptor in the chain or the underlying call invoker if this is the last interceptor.

Methods on `Interceptor` for each kind of service method have different signatures. However, the concept behind `continuation` and `context` parameters remains the same:

* `continuation` is a delegate which invokes the next interceptor in the chain or the underlying call invoker (if there is no interceptor left in the chain). It isn't an error to call it zero or multiple times. Interceptors aren't required to return a call representation (`AsyncUnaryCall` in case of unary RPC) returned from the `continuation` delegate. Omitting the delegate call and returning your own instance of call representation breaks the interceptors' chain and returns the associated response immediately.
* `context` carries scoped values associated with the client-side call. Use `context` to pass metadata, such as security principals, credentials, or tracing data. Moreover, `context` carries information about deadlines and cancellation. For more information, see [Reliable gRPC services with deadlines and cancellation](deadlines-cancellation?view=aspnetcore-10.0#deadlines).

### Awaiting response in client interceptor

An interceptor can await the response in unary and client streaming calls by updating the `AsyncUnaryCall<TResponse>.ResponseAsync` or `AsyncClientStreamingCall<TRequest, TResponse>.ResponseAsync` value.

```
public class ErrorHandlerInterceptor : Interceptor
{
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var call = continuation(request, context);

        return new AsyncUnaryCall<TResponse>(
            HandleResponse(call.ResponseAsync),
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose);
    }

    private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> inner)
    {
        try
        {
            return await inner;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("Custom error", ex);
        }
    }
}
```

The preceding code:

* Creates a new interceptor that overrides `AsyncUnaryCall`.

* Overriding `AsyncUnaryCall`:

  * Calls the `continuation` parameter to invoke the next item in the interceptor chain.
  * Creates a new `AsyncUnaryCall<TResponse>` instance based on the result of the continuation.
  * Wraps the `ResponseAsync` task using the `HandleResponse` method.
  * Awaits the response with `HandleResponse`. Awaiting the response allows logic to be added after the client received the response. By awaiting the response in a try-catch block, errors from calls can be logged.

For more information on how to create a client interceptor, see the [`ClientLoggerInterceptor.cs` example in the `grpc/grpc-dotnet` GitHub repository](https://github.com/grpc/grpc-dotnet/blob/master/examples/Interceptor/Client/ClientLoggerInterceptor.cs).

### Configure client interceptors

gRPC client interceptors are configured on a channel.

The following code:

* Creates a channel by using `GrpcChannel.ForAddress`.
* Uses the `Intercept` extension method to configure the channel to use the interceptor. Note that this method returns a `CallInvoker`. Strongly-typed gRPC clients can be created from an invoker just like a channel.
* Creates a client from the invoker. gRPC calls made by the client automatically execute the interceptor.

```
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new ClientLoggerInterceptor());

var client = new Greeter.GreeterClient(invoker);
```

The `Intercept` extension method can be chained to configure multiple interceptors for a channel. Alternatively, there is an `Intercept` overload that accepts multiple interceptors. Any number of interceptors can be executed for a single gRPC call, as the following example demonstrates:

```
var invoker = channel
    .Intercept(new ClientTokenInterceptor())
    .Intercept(new ClientMonitoringInterceptor())
    .Intercept(new ClientLoggerInterceptor());
```

Interceptors are invoked in reverse order of the chained `Intercept` extension methods. In the preceding code, interceptors are invoked in the following order:

1. `ClientLoggerInterceptor`
2. `ClientMonitoringInterceptor`
3. `ClientTokenInterceptor`

For information on how to configure interceptors with gRPC client factory, see [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0#configure-interceptors).

## Server interceptors

gRPC server interceptors intercept incoming RPC requests. They provide access to the incoming request, the outgoing response, and the context for a server-side call.

`Interceptor` methods to override for server:

* `UnaryServerHandler`: Intercepts a unary RPC.
* `ClientStreamingServerHandler`: Intercepts a client-streaming RPC.
* `ServerStreamingServerHandler`: Intercepts a server-streaming RPC.
* `DuplexStreamingServerHandler`: Intercepts a bidirectional-streaming RPC.

### Create a server gRPC interceptor

The following code presents an example of an intercepting an incoming unary RPC:

```
public class ServerLoggerInterceptor : Interceptor
{
    private readonly ILogger _logger;

    public ServerLoggerInterceptor(ILogger<ServerLoggerInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        _logger.LogInformation("Starting receiving call. Type/Method: {Type} / {Method}",
            MethodType.Unary, context.Method);
        try
        {
            return await continuation(request, context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Error thrown by {context.Method}.");
            throw;
        }
    }
}
```

Overriding `UnaryServerHandler`:

* Intercepts an incoming unary call.
* Logs details about the call.
* Calls the `continuation` parameter passed into the method. This invokes the next interceptor in the chain or the service handler if this is the last interceptor.
* Logs any exceptions. Awaiting the continuation allows logic to be added after the service method has executed. By awaiting the continuation in a try-catch block, errors from methods can be logged.

The signature of both client and server interceptors methods are similar:

* `continuation` stands for a delegate for an incoming RPC calling the next interceptor in the chain or the service handler (if there is no interceptor left in the chain). Similar to client interceptors, you can call it any time and there's no need to return a response directly from the continuation delegate. Outbound logic can be added after a service handler has executed by awaiting the continuation.
* `context` carries metadata associated with the server-side call, such as request metadata, deadlines and cancellation, or RPC result.

For more information on how to create a server interceptor, see the [`ServerLoggerInterceptor.cs` example in the `grpc/grpc-dotnet` GitHub repository](https://github.com/grpc/grpc-dotnet/blob/master/examples/Interceptor/Server/ServerLoggerInterceptor.cs).

### Configure server interceptors

gRPC server interceptors are configured at startup. The following code:

* Adds gRPC to the app with `AddGrpc`.
* Configures `ServerLoggerInterceptor` for all services by adding it to the service option's `Interceptors` collection.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });
}
```

An interceptor can also be configured for a specific service by using `AddServiceOptions` and specifying the service type.

```
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddGrpc()
        .AddServiceOptions<GreeterService>(options =>
        {
            options.Interceptors.Add<ServerLoggerInterceptor>();
        });
}
```

Interceptors are run in the order that they're added to the `InterceptorCollection`. If both global and single service interceptors are configured, then globally-configured interceptors are run before those configured for a single service.

By default, gRPC server interceptors have a per-request lifetime. Overriding this behavior is possible through registering the interceptor type with [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0). The following example registers the `ServerLoggerInterceptor` with a singleton lifetime:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<ServerLoggerInterceptor>();
    });

    services.AddSingleton<ServerLoggerInterceptor>();
}
```

### gRPC Interceptors versus middleware

ASP.NET Core [middleware](../fundamentals/middleware/?view=aspnetcore-10.0) offers similar functionalities compared to interceptors in C-core-based gRPC apps. ASP.NET Core middleware and interceptors are conceptually similar. Both:

* Are used to construct a pipeline that handles a gRPC request.

* Allow work to be performed before or after the next component in the pipeline.

* Provide access to `HttpContext`:

  * In middleware, the `HttpContext` is a parameter.
  * In interceptors, the `HttpContext` can be accessed using the `ServerCallContext` parameter with the `ServerCallContext.GetHttpContext` extension method. This feature is specific to interceptors running in ASP.NET Core.

gRPC Interceptor differences from ASP.NET Core middleware:

* Interceptors:

  * Operate on the gRPC layer of abstraction using the [`ServerCallContext`](https://grpc.io/grpc/csharp/api/Grpc.Core.ServerCallContext.html).

  * Provide access to:

    * The deserialized message sent to a call.
    * The message returned from the call before it's serialized.

  * Can catch and handle exceptions thrown from gRPC services.

* Middleware:

  * Runs for all HTTP requests.
  * Runs before gRPC interceptors.
  * Operates on the underlying HTTP/2 messages.
  * Can only access bytes from the request and response streams.

## Related content

* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [Create gRPC services and methods](services?view=aspnetcore-10.0)
* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)
* [Example of how to use gRPC on the client and server (`grpc/grpc-dotnet` GitHub repository)](https://github.com/grpc/grpc-dotnet/tree/master/examples#interceptor)
* [Configure interceptors in a gRPC client factory in .NET](clientfactory?view=aspnetcore-10.0#configure-interceptors)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/security/authorization/roles
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/security/authorization/roles.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Role-based authorization in ASP.NET Core MVC

When a user's identity is created after authentication, the user may belong to one or more *roles*, reflecting various authorizations that the user has to access data and perform operations. For example, Tracy may belong to the "Administrator" and "User" roles with access to administrative web pages in the app, while Scott may only belong to the "User" role and not have access to administrative data or operations. How these roles are created and managed depends on the backing store of the authorization process. Roles are exposed to the developer through [ClaimsPrincipal.IsInRole](/en-us/dotnet/api/system.security.claims.claimsprincipal.isinrole). [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) must be called to add Role services when setting up the app's identity system.

While roles are claims, not all claims are roles. Depending on the identity issuer, a role may be a collection of users that may apply claims for group members, as well as an actual claim on an identity. However, claims are meant to be information about an individual user. Using roles to add claims to a user can confuse the boundary between the user and their individual claims. This confusion is why the single-page application (SPA) templates aren't designed around roles. In addition, for organizations migrating from an on-premises legacy system, the proliferation of roles over the years can mean a role claim may be too large to be contained within a token usable by a SPA. To secure SPAs, see [Use Identity to secure a Web API backend for SPAs](../../../security/authentication/identity-api-authorization?view=aspnetcore-10.0).

This article uses MVC examples and focuses on MVC scenarios. For Blazor and Razor Pages guidance, see the following resources:

* [Role-based authorization in ASP.NET Core](../../../security/authorization/roles?view=aspnetcore-10.0)
* [Role-based authorization in ASP.NET Core Razor Pages](../../../razor-pages/security/authorization/roles?view=aspnetcore-10.0)

## Add Role services to Identity

Register role-based authorization services in the `Program` file by calling [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) with the role type in the app's Identity configuration. The role type in the following example is `IdentityRole`:

```
builder.Services.AddDefaultIdentity<IdentityUser>( ... )
    .AddRoles<IdentityRole>()
    ...
```

The preceding code requires the [`Microsoft.AspNetCore.Identity.UI` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.UI) and a `using` directive for [Microsoft.AspNetCore.Identity](/en-us/dotnet/api/microsoft.aspnetcore.identity).

In cases where the app takes granular control to build Identity manually, call [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) on [AddIdentityCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentitycore):

```
builder.Services.AddIdentityCore<IdentityUser>()
    .AddRoles<IdentityRole>()
    ...
```

Register role-based authorization services in `Startup.ConfigureServices` (`Startup.cs`) by calling [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) with the role type in the app's Identity configuration. The role type in the following example is `IdentityRole`:

```
services.AddDefaultIdentity<IdentityUser>()
    .AddRoles<IdentityRole>()
    ...
```

The preceding code requires the [`Microsoft.AspNetCore.Identity.UI` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.UI) and a `using` directive for [Microsoft.AspNetCore.Identity](/en-us/dotnet/api/microsoft.aspnetcore.identity).

In cases where the app takes granular control to build Identity manually, call [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) on [AddIdentityCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentitycore):

```
services.AddIdentityCore<IdentityUser>()
    .AddRoles<IdentityRole>()
    ...
```

## Role-based authorization checks

Role-based authorization checks:

* Are declarative and specify roles which the current user must be a member of to access the requested resource.
* Are applied to controllers or actions within a controller.

For example, the following code limits access to any actions on a controller to users who are a member of the `Administrator` role:

```
[Authorize(Roles = "Administrator")]
public class AdministrationController : Controller
{
    ...
}
```

Multiple roles can be specified as a comma-separated list. In the following example, access is limited to users who are members of the `HRManager` role *or* the `Finance` role:

```
[Authorize(Roles = "HRManager, Finance")]
public class SalaryController : Controller
{
    ...
}
```

When multiple attributes are applied, the user must be a member of *all* of the roles specified. The following example requires *both* `PowerUser` *and* `ControlPanelUser` roles to call the `SetTime` and `Shutdown` actions:

```
[Authorize(Roles = "PowerUser")]
[Authorize(Roles = "ControlPanelUser")]
public class ControlPanelController : Controller
{
    public IActionResult SetTime() { ... }
    public IActionResult ShutDown() { ... }
}
```

Access to an action can be limited by applying additional role authorization attributes at the action level:

```
[Authorize(Roles = "Administrator, PowerUser")]
public class ControlPanelController : Controller
{
    public IActionResult SetTime() { ... }

    [Authorize(Roles = "Administrator")]
    public IActionResult ShutDown() { ... }
}
```

In the preceding controller:

* Members of the `Administrator` role or the `PowerUser` role can access the controller and the `SetTime` action.
* Only members of the `Administrator` role can access the `ShutDown` action.

A controller can be secured but still allow anonymous, unauthenticated access to individual actions with the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute):

```
[Authorize]
public class Control3PanelController : Controller
{
    public IActionResult SetTime() { ... }

    [AllowAnonymous]
    public IActionResult Login() { ... }
}
```

Role matching is typically case-sensitive because role names are stored and compared using .NET string comparisons. For example, `Admin` (uppercase `A`) isn't treated as the same role as `admin` (lowercase `a`). For more information, see [Claim-based authorization in ASP.NET Core](../../../security/authorization/claims?view=aspnetcore-10.0#claim-case-sensitivity).

## Policy-based authorization checks

Role requirements can be expressed using policy syntax, where the app registers a policy at startup as part of the Authorization service configuration. In the following example, the `RequireAdministratorRole` policy specifies that all users must be in the `Administrator` role:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("RequireAdministratorRole",
         policy => policy.RequireRole("Administrator"));
```

Policies are applied using the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) property on the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
[Authorize(Policy = "RequireAdministratorRole")]
public IActionResult Shutdown() { ... }
```

In contrast to role matching, which is typically case-sensitive, ASP.NET Core policy name lookup is typically case-insensitive, so `RequireAdministratorRole` and `requireadministratorrole` refer to the same policy.

To specify multiple allowed roles in a requirement, specify the roles as parameters to the [RequireRole](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requirerole) method. In the following example, users are authorized if they belong to the `Administrator`, `PowerUser`, *or* `BackupAdministrator` roles:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ElevatedRights", policy =>
          policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
```

If you want the policy to require all of the preceding roles, either chain the roles to the policy builder or specify them to the policy builder individually in a [statement lambda](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#statement-lambdas).

Chained to the policy builder:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ElevatedRights", policy => 
        policy
            .RequireRole("Administrator")
            .RequireRole("PowerUser")
            .RequireRole("BackupAdministrator"));
```

Alternatively, use a statement lambda:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ElevatedRights",
        policy =>
        {
            policy.RequireRole("Administrator");
            policy.RequireRole("PowerUser");
            policy.RequireRole("BackupAdministrator");
        });
```

Role requirements can be expressed using policy syntax, where the app registers a policy at startup as part of the Authorization service configuration. In the following example, the `RequireAdministratorRole` policy specifies that all users must be in the `Administrator` role:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdministratorRole",
        policy => policy.RequireRole("Administrator"));
});
```

Policies are applied using the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) property on the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
[Authorize(Policy = "RequireAdministratorRole")]
public IActionResult Shutdown() { ... }
```

To specify multiple allowed roles in a requirement, specify the roles as parameters to the [RequireRole](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requirerole) method. In the following example, users are authorized if they belong to the `Administrator`, `PowerUser`, *or* `BackupAdministrator` roles:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy =>
        policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
});
```

If you want the policy to require all of the preceding roles, either chain the roles to the policy builder or specify them to the policy builder individually in a [statement lambda](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#statement-lambdas).

Chained to the policy builder:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy =>
        policy
            .RequireRole("Administrator")
            .RequireRole("PowerUser")
            .RequireRole("BackupAdministrator"));
});
```

Alternatively, use a statement lambda:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights",
        policy =>
        {
            policy.RequireRole("Administrator");
            policy.RequireRole("PowerUser");
            policy.RequireRole("BackupAdministrator");
        });
});
```

Role requirements can be expressed using policy syntax, where the app registers a policy at startup as part of the Authorization service configuration. In the following example, the `RequireAdministratorRole` policy specifies that all users must be in the `Administrator` role:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdministratorRole",
        policy => policy.RequireRole("Administrator"));
});
```

Policies are applied using the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) property on the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
[Authorize(Policy = "RequireAdministratorRole")]
public IActionResult Shutdown() { ... }
```

To specify multiple allowed roles in a requirement, specify the roles as parameters to the [RequireRole](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requirerole) method. In the following example, users are authorized if they belong to the `Administrator`, `PowerUser`, *or* `BackupAdministrator` roles:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy =>
        policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
});
```

If you want the policy to require all of the preceding roles, either chain the roles to the policy builder or specify them to the policy builder individually in a [statement lambda](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#statement-lambdas).

Chained to the policy builder:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy =>
        policy
            .RequireRole("Administrator")
            .RequireRole("PowerUser")
            .RequireRole("BackupAdministrator"));
});
```

Alternatively, use a statement lambda:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights",
        policy =>
        {
            policy.RequireRole("Administrator");
            policy.RequireRole("PowerUser");
            policy.RequireRole("BackupAdministrator");
        });
});
```

## Windows Authentication security groups as app roles

After the app is [configured for Windows Authentication](../../../security/authentication/windowsauth?view=aspnetcore-10.0) with the client and server machines part of the same Windows domain, user security groups are automatically included as claims in the user's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal).

The `User.Identity` is typically a [WindowsIdentity](/en-us/dotnet/api/system.security.principal.windowsidentity) when using Windows Authentication, and you can retrieve the SID group claims or check if a user is in a role with the following code, where the `{DOMAIN}` placeholder is the domain and the `{SID GROUP NAME}` is the SID group name:

```
if (User.Identity is WindowsIdentity windowsIdentity)
{
    var groups = windowsIdentity.Groups;

    // If needed, obtain a list of the SID groups
    var securityGroups = 
        groups.Select(g => g.Translate(typeof(NTAccount)).ToString()).ToList();

    // If needed, obtain the user's Windows identity name
    var windowsIdentityName = windowsIdentity.Name;

    // Check if the user is in a specific SID group
    if (User.IsInRole(@"{DOMAIN}\{SID GROUP NAME}"))
    {
        // User is in the specified group
    }
    else
    {
        // User isn't in the specified group
    }
}
else
{
    // The user isn't authenticated with Windows Authentication
}
```

For a demonstration of related code that translates SID group claims into human-readable values in a Blazor app, see the `UserClaims` component in [Secure an ASP.NET Core Blazor Web App with Windows Authentication](../../../blazor/security/blazor-web-app-with-windows-authentication?view=aspnetcore-10.0). Such an approach to retrieving SID group claims can be combined with [adding claims with an `IClaimsTransformation`](../../../security/authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation) to create custom role claims when a user is authenticated.

An approach similar to the preceding example for retrieving SID group claims can be combined with [adding claims with an `IClaimsTransformation`](../../../security/authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation) to create custom role claims when a user is authenticated.

## Related content

* [Role-based authorization in ASP.NET Core](../../../security/authorization/roles?view=aspnetcore-10.0)
* [ASP.NET Core Blazor authentication and authorization](../../../blazor/security/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor WebAssembly with Microsoft Entra ID groups and roles](../../../blazor/security/webassembly/microsoft-entra-id-groups-and-roles?view=aspnetcore-10.0)
* [Role-based authorization in ASP.NET Core Razor Pages](../../../razor-pages/security/authorization/roles?view=aspnetcore-10.0)
* [Extend or add custom claims, including role claims, using `IClaimsTransformation`](../../../security/authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-03-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hosting-models.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor hosting models

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains Blazor hosting models, primarily focused on Blazor Server and Blazor WebAssembly apps in versions of .NET earlier than .NET 8. The guidance in this article is relevant under all .NET releases for Blazor Hybrid apps that run on native mobile and desktop platforms. Blazor Web Apps in .NET 8 or later are better conceptualized by how Razor components are rendered, which is described as their *render mode*. Render modes are briefly touched on in the *Fundamentals* overview article and covered in detail in [ASP.NET Core Blazor render modes](components/render-modes?view=aspnetcore-10.0) of the *Components* node.

This article explains Blazor hosting models and how to choose which one to use.

Blazor is a web framework for building web UI components ([Razor components](components/?view=aspnetcore-10.0)) that can be hosted in different ways. Razor components can run server-side in ASP.NET Core (*Blazor Server*) versus client-side in the browser on a [WebAssembly](https://webassembly.org/)-based .NET runtime (*Blazor WebAssembly*, *Blazor Wasm*). You can also host Razor components in native mobile and desktop apps that render to an embedded Web View control (*Blazor Hybrid*). Regardless of the hosting model, the way you build Razor components *is the same*. The same Razor components can be used with any of the hosting models unchanged.

Blazor is a web framework for building web UI components ([Razor components](components/?view=aspnetcore-10.0)) that can be hosted in different ways. Razor components can run server-side in ASP.NET Core (*Blazor Server*) versus client-side in the browser on a [WebAssembly](https://webassembly.org/)-based .NET runtime (*Blazor WebAssembly*, *Blazor Wasm*). Regardless of the hosting model, the way you build Razor components *is the same*. The same Razor components can be used with any of the hosting models unchanged.

## Blazor Server

With the Blazor Server hosting model, components are executed on the server from within an ASP.NET Core app. UI updates, event handling, and JavaScript calls are handled over a [SignalR](../signalr/introduction?view=aspnetcore-10.0) connection using the [WebSockets protocol](../fundamentals/websockets?view=aspnetcore-10.0). The state on the server associated with each connected client is called a *circuit*. Circuits aren't tied to a specific network connection and can tolerate temporary network interruptions and attempts by the client to reconnect to the server when the connection is lost.

In a traditional server-rendered app, opening the same app in multiple browser screens (tabs or `iframes`) typically doesn't translate into additional resource demands on the server. For the Blazor Server hosting model, each browser screen requires a separate circuit and separate instances of server-managed component state. Blazor considers closing a browser tab or navigating to an external URL a *graceful* termination. In the event of a graceful termination, the circuit and associated resources are immediately released. A client may also disconnect non-gracefully, for instance due to a network interruption. Blazor Server stores disconnected circuits for a configurable interval to allow the client to reconnect.

On the client, the Blazor script establishes the SignalR connection with the server. The script is served as a static web asset with automatic compression and [fingerprinting](https://developer.mozilla.org/docs/Glossary/Fingerprinting).

On the client, the Blazor script establishes the SignalR connection with the server. The script is served from an embedded resource in the ASP.NET Core shared framework.

The Blazor Server hosting model offers several benefits:

* Download size is significantly smaller than when the Blazor WebAssembly hosting model is used, and the app loads much faster.
* The app takes full advantage of server capabilities, including the use of .NET APIs.
* .NET on the server is used to run the app, so existing .NET tooling, such as debugging, works as expected.
* Thin clients are supported. For example, Blazor Server works with browsers that don't support WebAssembly and on resource-constrained devices.
* The app's .NET/C# code base, including the app's component code, isn't served to clients.

The Blazor Server hosting model has the following limitations:

* Higher latency usually exists. Every user interaction involves a network hop.
* There's no offline support. If the client connection fails, interactivity fails.
* Scaling apps with many users requires server resources to handle multiple client connections and client state.
* An ASP.NET Core server is required to serve the app. Serverless deployment scenarios aren't possible, such as serving the app from a Content Delivery Network (CDN).

We recommend using the [Azure SignalR Service](/en-us/azure/azure-signalr) for apps that adopt the Blazor Server hosting model. The service allows for scaling up a Blazor Server app to a large number of concurrent SignalR connections.

## Blazor WebAssembly

The Blazor WebAssembly hosting model runs components client-side in the browser on a WebAssembly-based .NET runtime. Razor components, their dependencies, and the .NET runtime are downloaded to the browser. Components are executed directly on the browser UI thread. UI updates and event handling occur within the same process. Assets are deployed as static files to a web server or service capable of serving static content to clients.

Blazor Web Apps can use the Blazor WebAssembly hosting model to enable client-side interactivity. When an app is created that exclusively runs on the Blazor WebAssembly hosting model without server-side rendering and interactivity, the app is called a *standalone* Blazor WebAssembly app.

When the Blazor WebAssembly app is created for deployment without a backend ASP.NET Core app to serve its files, the app is called a *standalone* Blazor WebAssembly app.

When a standalone Blazor WebAssembly app uses a backend ASP.NET Core app to serve its files, the app is called a *hosted* Blazor WebAssembly app. Using hosted Blazor WebAssembly, you get a full-stack web development experience with .NET, including the ability to share code between the client and server apps, support for prerendering, and integration with MVC and Razor Pages. A hosted client app can interact with its backend server app over the network using a variety of messaging frameworks and protocols, such as [web API](../web-api/?view=aspnetcore-10.0), [gRPC-web](../grpc/?view=aspnetcore-10.0), and [SignalR](../signalr/introduction?view=aspnetcore-10.0) ([Use ASP.NET Core SignalR with Blazor](tutorials/signalr-blazor?view=aspnetcore-10.0)).

A Blazor WebAssembly app built as a [Progressive Web App (PWA)](progressive-web-app/?view=aspnetcore-10.0) uses modern browser APIs to enable many of the capabilities of a native client app, such as working offline, running in its own app window, launching from the host's operating system, receiving push notifications, and automatically updating in the background.

The Blazor script handles:

* Downloading the .NET runtime, Razor components, and dependencies.
* Runtime initialization.

The size of the published app, its *payload size*, is a critical performance factor for an app's usability. A large app takes a relatively long time to download to a browser, which diminishes the user experience. Blazor WebAssembly optimizes payload size to reduce download times:

* Unused code is stripped out of the app when it's published by the [Intermediate Language (IL) Trimmer](host-and-deploy/configure-trimmer?view=aspnetcore-10.0).
* HTTP responses are compressed.
* The .NET runtime and assemblies are cached in the browser.

The Blazor WebAssembly hosting model offers several benefits:

* For standalone Blazor WebAssembly apps, there's no .NET server-side dependency after the app is downloaded from the server, so the app remains functional if the server goes offline.
* Client resources and capabilities are fully leveraged.
* Work is offloaded from the server to the client.
* For standalone Blazor WebAssembly apps, an ASP.NET Core web server isn't required to host the app. Serverless deployment scenarios are possible, such as serving the app from a Content Delivery Network (CDN).

The Blazor WebAssembly hosting model has the following limitations:

* Razor components are restricted to the capabilities of the browser.
* Capable client hardware and software (for example, WebAssembly support) is required.
* Download size is larger, and components take longer to load.
* Code sent to the client can't be protected from inspection and tampering by users.

The .NET [Intermediate Language (IL)](/en-us/dotnet/standard/glossary#il) interpreter includes partial [just-in-time (JIT)](/en-us/dotnet/standard/glossary#jit) runtime support to achieve improved runtime performance. The JIT interpreter optimizes execution of interpreter bytecodes by replacing them with tiny blobs of WebAssembly code. The JIT interpreter is automatically enabled for Blazor WebAssembly apps except when debugging.

Blazor supports ahead-of-time (AOT) compilation, where you can compile your .NET code directly into WebAssembly. AOT compilation results in runtime performance improvements at the expense of a larger app size. For more information, see [ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation](webassembly-build-tools-and-aot?view=aspnetcore-10.0#ahead-of-time-aot-compilation).

The same [.NET WebAssembly build tools](webassembly-build-tools-and-aot?view=aspnetcore-10.0) used for AOT compilation also [relink the .NET WebAssembly runtime](webassembly-build-tools-and-aot?view=aspnetcore-10.0#runtime-relinking) to trim unused runtime code. Blazor also trims unused code from .NET framework libraries. The .NET compiler further precompresses a standalone Blazor WebAssembly app for a smaller app payload.

WebAssembly-rendered Razor components can use [native dependencies](webassembly-native-dependencies?view=aspnetcore-10.0) built to run on WebAssembly.

Blazor WebAssembly includes support for trimming unused code from .NET libraries. For more information, see [ASP.NET Core Blazor globalization and localization](globalization-localization?view=aspnetcore-10.0).

## Blazor Hybrid

Blazor can also be used to build native client apps using a hybrid approach. Hybrid apps are native apps that leverage web technologies for their functionality. In a Blazor Hybrid app, Razor components run directly in the native app (not on WebAssembly) along with any other .NET code and render web UI based on HTML and CSS to an embedded Web View control through a local interop channel.

Blazor Hybrid apps can be built using different .NET native app frameworks, including .NET MAUI, WPF, and Windows Forms. Blazor provides `BlazorWebView` controls for adding Razor components to apps built with these frameworks. Using Blazor with .NET MAUI offers a convenient way to build cross-platform Blazor Hybrid apps for mobile and desktop, while Blazor integration with WPF and Windows Forms can be a great way to modernize existing apps.

Because Blazor Hybrid apps are native apps, they can support functionality that isn't available with only the web platform. Blazor Hybrid apps have full access to native platform capabilities through normal .NET APIs. Blazor Hybrid apps can also share and reuse components with existing Blazor Server or Blazor WebAssembly apps. Blazor Hybrid apps combine the benefits of the web, native apps, and the .NET platform.

The Blazor Hybrid hosting model offers several benefits:

* Reuse existing components that can be shared across mobile, desktop, and web.
* Leverage web development skills, experience, and resources.
* Apps have full access to the native capabilities of the device.

The Blazor Hybrid hosting model has the following limitations:

* Separate native client apps must be built, deployed, and maintained for each target platform.
* Native client apps usually take longer to find, download, and install over accessing a web app in a browser.

For more information, see [ASP.NET Core Blazor Hybrid](hybrid/?view=aspnetcore-10.0).

For more information on Microsoft native client frameworks, see the following resources:

* [.NET Multi-platform App UI (.NET MAUI)](/en-us/dotnet/maui/what-is-maui)
* [Windows Presentation Foundation (WPF)](/en-us/dotnet/desktop/wpf/overview/)
* [Windows Forms](/en-us/dotnet/desktop/winforms/overview/)

## Which Blazor hosting model should I choose?

A component's hosting model is set by its *render mode*, either at compile time or runtime, which is described with examples in [ASP.NET Core Blazor render modes](components/render-modes?view=aspnetcore-10.0). The following table shows the primary considerations for setting the render mode to determine a component's hosting model. For standalone Blazor WebAssembly apps, all of the app's components are rendered on the client with the Blazor WebAssembly hosting model.

Select the Blazor hosting model based on the app's feature requirements. The following table shows the primary considerations for selecting the hosting model.

Blazor Hybrid apps include .NET MAUI, WPF, and Windows Forms framework apps.

| Feature                                                                                           | Blazor Server  | Blazor WebAssembly (Wasm) | Blazor Hybrid   |
| ------------------------------------------------------------------------------------------------- | -------------- | ------------------------- | --------------- |
| [Complete .NET API compatibility](#complete-net-api-compatibility)                                | ✔️Supported    | ❌Not supported            | ✔️Supported     |
| [Direct access to server and network resources](#direct-access-to-server-and-network-resources)   | ✔️Supported    | ❌Not supported†           | ❌Not supported† |
| [Small payload size with fast initial load time](#small-payload-size-with-fast-initial-load-time) | ✔️Supported    | ❌Not supported            | ❌Not supported  |
| [Near native execution speed](#near-native-execution-speed)                                       | ✔️Supported    | ✔️Supported‡              | ✔️Supported     |
| [App code secure and private on the server](#app-code-secure-and-private-on-the-server)           | ✔️Supported    | ❌Not supported†           | ❌Not supported† |
| [Run apps offline once downloaded](#run-apps-offline-once-downloaded)                             | ❌Not supported | ✔️Supported               | ✔️Supported     |
| [Static site hosting](#static-site-hosting)                                                       | ❌Not supported | ✔️Supported               | ❌Not supported  |
| [Offloads processing to clients](#offloads-processing-to-clients)                                 | ❌Not supported | ✔️Supported               | ✔️Supported     |
| [Full access to native client capabilities](#full-access-to-native-client-capabilities)           | ❌Not supported | ❌Not supported            | ✔️Supported     |
| [Web-based deployment](#web-based-deployment)                                                     | ✔️Supported    | ✔️Supported               | ❌Not supported  |

†Blazor WebAssembly and Blazor Hybrid apps can use server-based APIs to access server/network resources and access private and secure app code.\
‡Blazor WebAssembly only reaches near-native performance with [ahead-of-time (AOT) compilation](webassembly-build-tools-and-aot?view=aspnetcore-10.0#ahead-of-time-aot-compilation).

| Feature                                                                                           | Blazor Server  | Blazor WebAssembly (Wasm) |
| ------------------------------------------------------------------------------------------------- | -------------- | ------------------------- |
| [Complete .NET API compatibility](#complete-net-api-compatibility)                                | ✔️Supported    | ❌Not supported            |
| [Direct access to server and network resources](#direct-access-to-server-and-network-resources)   | ✔️Supported    | ❌Not supported†           |
| [Small payload size with fast initial load time](#small-payload-size-with-fast-initial-load-time) | ✔️Supported    | ❌Not supported            |
| [App code secure and private on the server](#app-code-secure-and-private-on-the-server)           | ✔️Supported    | ❌Not supported†           |
| [Run apps offline once downloaded](#run-apps-offline-once-downloaded)                             | ❌Not supported | ✔️Supported               |
| [Static site hosting](#static-site-hosting)                                                       | ❌Not supported | ✔️Supported               |
| [Offloads processing to clients](#offloads-processing-to-clients)                                 | ❌Not supported | ✔️Supported               |

†Blazor WebAssembly apps can use server-based APIs to access server/network resources and access private and secure app code.

After you choose the app's hosting model, you can generate a Blazor Server or Blazor WebAssembly app from a Blazor project template. For more information, see [Tooling for ASP.NET Core Blazor](tooling?view=aspnetcore-10.0#blazor-template-options).

To create a Blazor Hybrid app, see the articles under [ASP.NET Core Blazor Hybrid tutorials](hybrid/tutorials/?view=aspnetcore-10.0).

### Complete .NET API compatibility

Components rendered for the Blazor Server hosting model and Blazor Hybrid apps have complete .NET API compatibility, while components rendered for Blazor WebAssembly are limited to a [subset of .NET APIs](fundamentals/?view=aspnetcore-10.0#subset-of-net-apis-for-blazor-webassembly-apps). When an app's specification requires one or more .NET APIs that are unavailable to WebAssembly-rendered components, then choose to render components for Blazor Server or use Blazor Hybrid.

Blazor Server and Blazor Hybrid apps have complete .NET API compatibility, while Blazor WebAssembly apps are limited to a subset of .NET APIs. When an app's specification requires one or more .NET APIs that are unavailable to Blazor WebAssembly apps, then choose Blazor Server or Blazor Hybrid.

Blazor Server apps have complete .NET API compatibility, while Blazor WebAssembly apps are limited to a subset of .NET APIs. When an app's specification requires one or more .NET APIs that are unavailable to Blazor WebAssembly apps, then choose Blazor Server.

### Direct access to server and network resources

Components rendered for the Blazor Server hosting model have direct access to server and network resources where the app is executing. Because components hosted using Blazor WebAssembly or Blazor Hybrid execute on a client, they don't have direct access to server and network resources. Components can access server and network resources *indirectly* via protected server-based APIs. Server-based APIs might be available via third-party libraries, packages, and services. Take into account the following considerations:

* Third-party libraries, packages, and services might be costly to implement and maintain, weakly supported, or introduce security risks.
* If one or more server-based APIs are developed internally by your organization, additional resources are required to build and maintain them.

Use the Blazor Server hosting model to avoid the need to expose APIs from the server environment.

Blazor Server apps have direct access to server and network resources where the app is executing. Because Blazor WebAssembly and Blazor Hybrid apps execute on a client, they don't have direct access to server and network resources. Blazor WebAssembly and Blazor Hybrid apps can access server and network resources *indirectly* via protected server-based APIs. Server-based APIs might be available via third-party libraries, packages, and services. Take into account the following considerations:

* Third-party libraries, packages, and services might be costly to implement and maintain, weakly supported, or introduce security risks.
* If one or more server-based APIs are developed internally by your organization, additional resources are required to build and maintain them.

To avoid server-based APIs for Blazor WebAssembly or Blazor Hybrid apps, adopt Blazor Server, which can access server and network resources directly.

Blazor Server apps have direct access to server and network resources where the app is executing. Because Blazor WebAssembly apps execute on a client, they don't have direct access to server and network resources. Blazor WebAssembly apps can access server and network resources *indirectly* via protected server-based APIs. Server-based APIs might be available via third-party libraries, packages, and services. Take into account the following considerations:

* Third-party libraries, packages, and services might be costly to implement and maintain, weakly supported, or introduce security risks.
* If one or more server-based APIs are developed internally by your organization, additional resources are required to build and maintain them.

To avoid server-based APIs for Blazor WebAssembly apps, adopt Blazor Server, which can access server and network resources directly.

### Small payload size with fast initial load time

Rendering components from the server reduces the app payload size and improves initial load times. When a fast initial load time is desired, use the Blazor Server hosting model or consider static server-side rendering.

Blazor Server apps have relatively small payload sizes with faster initial load times. When a fast initial load time is desired, adopt Blazor Server.

### Near native execution speed

Blazor Hybrid apps run using the .NET runtime natively on the target platform, which offers the best possible speed.

Components rendered for the Blazor WebAssembly hosting model, including Progressive Web Apps (PWAs), and standalone Blazor WebAssembly apps run using the .NET runtime for WebAssembly, which is slower than running directly on the platform. Consider using [ahead-of-time (AOT) compiled](webassembly-build-tools-and-aot?view=aspnetcore-10.0#ahead-of-time-aot-compilation) to improve runtime performance when using Blazor WebAssembly.

Blazor Hybrid apps run using the .NET runtime natively on the target platform, which offers the best possible speed.

Blazor WebAssembly, including Progressive Web Apps (PWAs), apps run using the .NET runtime for WebAssembly, which is slower than running directly on the platform, even for apps that are [ahead-of-time (AOT) compiled](webassembly-build-tools-and-aot?view=aspnetcore-10.0#ahead-of-time-aot-compilation) for WebAssembly in the browser.

Blazor Server apps generally execute on the server quickly.

Blazor WebAssembly apps run using the .NET runtime for WebAssembly, which is slower than running directly on the platform.

### App code secure and private on the server

Maintaining app code securely and privately on the server is a built-in feature of components rendered for the Blazor Server hosting model. Components rendered using the Blazor WebAssembly or Blazor Hybrid hosting models can use server-based APIs to access functionality that must be kept private and secure. The considerations for developing and maintaining server-based APIs described in the [Direct access to server and network resources](#direct-access-to-server-and-network-resources) section apply. If the development and maintenance of server-based APIs isn't desirable for maintaining secure and private app code, render components for the Blazor Server hosting model.

Maintaining app code securely and privately on the server is a built-in feature of Blazor Server. Blazor WebAssembly and Blazor Hybrid apps can use server-based APIs to access functionality that must be kept private and secure. The considerations for developing and maintaining server-based APIs described in the [Direct access to server and network resources](#direct-access-to-server-and-network-resources) section apply. If the development and maintenance of server-based APIs isn't desirable for maintaining secure and private app code, adopt the Blazor Server hosting model.

Maintaining app code securely and privately on the server is a built-in feature of Blazor Server. Blazor WebAssembly apps can use server-based APIs to access functionality that must be kept private and secure. The considerations for developing and maintaining server-based APIs described in the [Direct access to server and network resources](#direct-access-to-server-and-network-resources) section apply. If the development and maintenance of server-based APIs isn't desirable for maintaining secure and private app code, adopt the Blazor Server hosting model.

### Run apps offline once downloaded

Standalone Blazor WebAssembly apps built as Progressive Web Apps (PWAs) and Blazor Hybrid apps can run offline, which is particularly useful when clients aren't able to connect to the Internet. Components rendered for the Blazor Server hosting model fail to run when the connection to the server is lost. If an app must run offline, standalone Blazor WebAssembly and Blazor Hybrid are the best choices.

Blazor WebAssembly apps built as Progressive Web Apps (PWAs) and Blazor Hybrid apps can run offline, which is particularly useful when clients aren't able to connect to the Internet. Blazor Server apps fail to run when the connection to the server is lost. If an app must run offline, Blazor WebAssembly and Blazor Hybrid are the best choices.

Blazor WebAssembly apps can run offline, which is particularly useful when clients aren't able to connect to the Internet. Blazor Server apps fail to run when the connection to the server is lost. If an app must run offline, Blazor WebAssembly is the best choice.

### Static site hosting

Static site hosting is possible with standalone Blazor WebAssembly apps because they're downloaded to clients as a set of static files. Standalone Blazor WebAssembly apps don't require a server to execute server-side code in order to download and run and can be delivered via a [Content Delivery Network (CDN)](https://developer.mozilla.org/docs/Glossary/CDN) (for example, [Azure CDN](https://azure.microsoft.com/services/cdn/)).

Although Blazor Hybrid apps are compiled into one or more self-contained deployment assets, the assets are usually provided to clients through a third-party app store. If static hosting is an app requirement, select standalone Blazor WebAssembly.

### Offloads processing to clients

Components rendered using the Blazor WebAssembly or Blazor Hybrid hosting models execute on clients and thus offload processing to clients. Components rendered for the Blazor Server hosting model execute on a server, so server resource demand typically increases with the number of users and the amount of processing required per user. When it's possible to offload most or all of an app's processing to clients and the app processes a significant amount of data, Blazor WebAssembly or Blazor Hybrid is the best choice.

Blazor WebAssembly and Blazor Hybrid apps execute on clients and thus offload processing to clients. Blazor Server apps execute on a server, so server resource demand typically increases with the number of users and the amount of processing required per user. When it's possible to offload most or all of an app's processing to clients and the app processes a significant amount of data, Blazor WebAssembly or Blazor Hybrid is the best choice.

Blazor WebAssembly apps execute on clients and thus offload processing to clients. Blazor Server apps execute on a server, so server resource demand typically increases with the number of users and the amount of processing required per user. When it's possible to offload most or all of an app's processing to clients and the app processes a significant amount of data, Blazor WebAssembly is the best choice.

### Full access to native client capabilities

Blazor Hybrid apps have full access to native client API capabilities via .NET native app frameworks. In Blazor Hybrid apps, Razor components run directly in the native app, not on [WebAssembly](https://developer.mozilla.org/docs/WebAssembly). When full client capabilities are a requirement, Blazor Hybrid is the best choice.

### Web-based deployment

Blazor Web Apps are updated on the next app refresh from the browser.

Blazor Hybrid apps are native client apps that typically require an installer and platform-specific deployment mechanism.

## Setting a component's hosting model

To set a component's hosting model to Blazor Server or Blazor WebAssembly at compile-time or dynamically at runtime, you set its *render mode*. Render modes are fully explained and demonstrated in the [ASP.NET Core Blazor render modes](components/render-modes?view=aspnetcore-10.0) article. We don't recommend that you jump from this article directly to the *Render modes* article without reading the content in the articles between these two articles. For example, render modes are more easily understood by looking at Razor component examples, but basic Razor component structure and function isn't covered until the [ASP.NET Core Blazor fundamentals](fundamentals/?view=aspnetcore-10.0) article is reached. It's also helpful to learn about Blazor's project templates and tooling before working with the component examples in the *Render modes* article.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/app-secrets.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Safe storage of app secrets in development in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Kirk Larkin](https://twitter.com/serpent5)

This article explains how to manage sensitive data for an ASP.NET Core app on a development machine. Never store passwords or other sensitive data in source code or configuration files. Production secrets shouldn't be used for development or test. Secrets shouldn't be deployed with the app. Production secrets should be accessed through a controlled means like Azure Key Vault. Azure test and production secrets can be stored and protected with the [Azure Key Vault configuration provider](key-vault-configuration?view=aspnetcore-10.0).

[View or download the sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/app-secrets/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

For more information on authentication for deployed test and production apps, see [Secure authentication flows](./?view=aspnetcore-10.0#secure-authentication-flows).

To use user secrets in a .NET console app, see [GitHub dotnet/entityframework.docs issue #3939](https://github.com/dotnet/EntityFramework.Docs/issues/3939#issuecomment-1191978026).

## Work with environment variables

Environment variables are used to avoid storage of app secrets in code or in local configuration files. Environment variables override configuration values for all previously specified configuration sources.

Consider an ASP.NET Core web app in which **Individual Accounts** security is enabled. A default database connection string is included in the project *appsettings.json* file with the `DefaultConnection` key. The default connection string is for LocalDB, which runs in user mode and doesn't require a password. During app deployment, you can override the `DefaultConnection` key value with the value from an environment variable. The environment variable might store the complete connection string with sensitive credentials.

Warning

Environment variables are commonly stored as plain, unencrypted text. If the machine or process is compromised, environment variables are accessible to untrusted parties. Extra measures to prevent disclosure of user secrets might be required.

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For example, [Bash](https://linuxhint.com/bash-environment-variables/) doesn't support colon (`:`) as a separator. All platforms support the double underscore (`__`) syntax and automatically replace it with a colon (`:`).

[]()

## Use the Secret Manager tool

Secret Manager is a tool that stores sensitive data during application development. In this context, a piece of sensitive data is an *app secret*.

* App secrets are stored in a separate location from the project tree.
* They're associated with a specific project or shared across several projects.
* They aren't checked into source control.

Warning

Secret Manager doesn't encrypt the stored secrets and shouldn't be treated as a trusted store. It's for development purposes only. The keys and values are stored in a JSON configuration file in the user profile directory.

Secret Manager hides implementation details, such as where and how the values are stored. You can use the tool without knowing these implementation details. The values are stored in a JSON file in the local machine's user profile folder:

* [Windows](#tabpanel_1_windows)
* [Linux / macOS](#tabpanel_1_linux+macos)

File system path:

`%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json`

File system path:

`~/.microsoft/usersecrets/<user_secrets_id>/secrets.json`

In the file system path, replace the `<user_secrets_id>` portion with the `UserSecretsId` value specified in your project file.

Don't write code that depends on the location or format of data saved with Secret Manager. These implementation details might change. For example, the secret values aren't encrypted.

## Enable secret storage

Secret Manager operates on project-specific configuration settings stored in your user profile.

### Use the CLI

Secret Manager includes an `init` command. To use user secrets, run the following command in the project directory:

```
dotnet user-secrets init
```

This command adds a `UserSecretsId` element within a `PropertyGroup` of the project file. By default, the inner text of `UserSecretsId` is a GUID. The inner text is arbitrary, but is unique to the project. The following example shows a GUID value of `0000a1a1-b2b2-c3c3-d4d4-eeeeee555555`.

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

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <UserSecretsId>0000a1a1-b2b2-c3c3-d4d4-eeeeee555555</UserSecretsId>
  </PropertyGroup>

</Project>
```

### Use Visual Studio

In Visual Studio, right-click the project in Solution Explorer, and select **Manage User Secrets** from the context menu. This gesture adds a `UserSecretsId` element, populated with a GUID, to the project file.

### If 'GenerateAssemblyInfo' is 'false'

If the generation of assembly info attributes (`GenerateAssemblyInfo`) is disabled (set to `false`), manually add the [UserSecretsIdAttribute](/en-us/dotnet/api/microsoft.extensions.configuration.usersecrets.usersecretsidattribute) in the *AssemblyInfo.cs* file. For example:

```
[assembly: UserSecretsId("your_user_secrets_id")]
```

When you manually add the `UserSecretsId` attribute to the *AssemblyInfo.cs* file, the `UserSecretsId` value must match the value in the project file.

## Set a secret

Define an app secret consisting of a key and its value. The secret is associated with the project's `UserSecretsId` value. For example, run the following command from the directory in which the project file exists:

```
dotnet user-secrets set "Movies:ServiceApiKey" "12345"
```

In this example, the colon indicates that `Movies` is an object literal with a `ServiceApiKey` property.

You can also use Secret Manager from other directories. Include the `--project` option to supply the file system path at which the project file exists. For example:

```
dotnet user-secrets set "Movies:ServiceApiKey" "12345" --project "C:\apps\WebApp1\src\WebApp1"
```

### JSON structure flattening in Visual Studio

The Visual Studio **Manage User Secrets** gesture opens a *secrets.json* file in the text editor. Replace the contents of the *secrets.json* file with the key-value pairs to store. For example:

```
{
  "Movies": {
    "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
    "ServiceApiKey": "12345"
  }
}
```

The JSON structure is flattened after modifications via the `dotnet user-secrets remove` or `dotnet user-secrets set` command. For example, running `dotnet user-secrets remove "Movies:ConnectionString"` collapses the `Movies` object literal. The modified file resembles the following JSON:

```
{
  "Movies:ServiceApiKey": "12345"
}
```

## Set multiple secrets

A batch of secrets can be set by piping JSON to the `set` command. In the following example, the contents of the *input.json* file is piped to the `set` command.

* [Windows](#tabpanel_2_windows)
* [Linux / macOS](#tabpanel_2_linux+macos)

Run the following command:

```
type .\input.json | dotnet user-secrets set
```

Run the following command in a terminal:

```
cat ./input.json | dotnet user-secrets set
```

## Access a secret

To access a secret, complete the following steps:

1. [Register the user secrets configuration source](#register-the-user-secrets-configuration-source).

2. [Read the secret via the Configuration API](#read-the-secret-via-the-configuration-api).

### Register the user secrets configuration source

The user secrets [configuration provider](/en-us/dotnet/core/extensions/configuration-providers) registers the appropriate configuration source with the .NET [Configuration API](../fundamentals/configuration/?view=aspnetcore-10.0).

ASP.NET Core web apps created with the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command or Visual Studio generate the following code:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) method initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The initialized `WebApplicationBuilder` (`builder`) provides default configuration and calls the [AddUserSecrets](/en-us/dotnet/api/microsoft.extensions.configuration.usersecretsconfigurationextensions.addusersecrets) method when the [EnvironmentName](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.environmentname#microsoft-extensions-hosting-ihostenvironment-environmentname) property is [Development](/en-us/dotnet/api/microsoft.extensions.hosting.environmentname.development#microsoft-extensions-hosting-environmentname-development).

### Read the secret via the Configuration API

The following examples demonstrate how to read the `Movies:ServiceApiKey` key:

**Program.cs file**

```
var builder = WebApplication.CreateBuilder(args);
var movieApiKey = builder.Configuration["Movies:ServiceApiKey"];

var app = builder.Build();

app.MapGet("/", () => movieApiKey);

app.Run();
```

**Razor Pages page model**

```
public class IndexModel : PageModel
{
    private readonly IConfiguration _config;

    public IndexModel(IConfiguration config)
    {
        _config = config;
    }

    public void OnGet()
    {
        var moviesApiKey = _config["Movies:ServiceApiKey"];

        // call Movies service with the API key
    }
}
```

For more information, see [Configuration in ASP.NET Core](../fundamentals/configuration/?view=aspnetcore-10.0).

## Map secrets to a POCO

Mapping an entire object literal to a POCO (a simple .NET class with properties) is useful for aggregating related properties.

Assume the application *secrets.json* file contains the following two secrets:

```
{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}
```

To map the preceding secrets to a POCO, use the .NET Configuration API's [object graph binding](../fundamentals/configuration/?view=aspnetcore-10.0#bind-an-array) feature. The following code binds to a custom `MovieSettings` POCO and accesses the `ServiceApiKey` property value:

```
var moviesConfig = 
    Configuration.GetSection("Movies").Get<MovieSettings>();
_moviesApiKey = moviesConfig.ServiceApiKey;
```

The `Movies:ConnectionString` and `Movies:ServiceApiKey` secrets are mapped to the respective properties in `MovieSettings`:

```
public class MovieSettings
{
    public string ConnectionString { get; set; }

    public string ServiceApiKey { get; set; }
}
```

## Use string replacement with secrets

Storing passwords in plain text is insecure. Never store secrets in a configuration file such as *appsettings.json*, which might get checked in to a source code repository.

For example, a database connection string stored in an *appsettings.json* file shouldn't include a password. Instead, store the password as a secret, and include the password in the connection string at runtime. For example:

```
dotnet user-secrets set "DbPassword" "`<secret value>`"
```

Replace the `<secret value>` placeholder in the example with the password value. Set the secret's value on a [SqlConnectionStringBuilder](/en-us/dotnet/api/system.data.sqlclient.sqlconnectionstringbuilder) object's [Password](/en-us/dotnet/api/system.data.sqlclient.sqlconnectionstringbuilder.password) property to include it as the password value in the connection string:

```
using System.Data.SqlClient;

var builder = WebApplication.CreateBuilder(args);

var conStrBuilder = new SqlConnectionStringBuilder(
        builder.Configuration.GetConnectionString("Movies"));
conStrBuilder.Password = builder.Configuration["DbPassword"];
var connection = conStrBuilder.ConnectionString;

var app = builder.Build();

app.MapGet("/", () => connection);

app.Run();
```

## List the secrets

Assume the application *secrets.json* file contains the following two secrets:

```
{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}
```

Run the following command from the directory in which the project file exists:

```
dotnet user-secrets list
```

The following output appears:

```
Movies:ConnectionString = Server=(localdb)\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true
Movies:ServiceApiKey = 12345
```

In the example, a colon (`:`) in the key names denotes the object hierarchy within the *secrets.json* file.

## Remove a single secret

Assume the application *secrets.json* file contains the following two secrets:

```
{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}
```

Run the following command from the directory in which the project file exists:

```
dotnet user-secrets remove "Movies:ConnectionString"
```

The application *secrets.json* file is modified to remove the key-value pair associated with the `Movies:ConnectionString` key:

```
{
  "Movies": {
    "ServiceApiKey": "12345"
  }
}
```

The `dotnet user-secrets list` command displays the following message:

```
Movies:ServiceApiKey = 12345
```

## Remove all secrets

Assume the application *secrets.json* file contains the following two secrets:

```
{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}
```

Run the following command from the directory in which the project file exists:

```
dotnet user-secrets clear
```

All user secrets for the app are deleted from the *secrets.json* file:

```
{}
```

Running the `dotnet user-secrets list` command displays the following message:

```
No secrets configured for this application.
```

## Manage user secrets with Visual Studio

To manage user secrets in Visual Studio, right-click the project in Solution Explorer and select **Manage User Secrets**:

## Migrate user secrets from ASP.NET Framework to ASP.NET Core

You can migrate your stored user secrets from ASP.NET Framework to ASP.NET Core. For more information, see [GitHub dotnet/aspnetcore.docs issue #27611](https://github.com/dotnet/AspNetCore.Docs/issues/27611) - *User Secrets documentation doesn't mention incompatibility with AssemblyInfo.cs*.

## Work with user secrets in non-web applications

Projects that target `Microsoft.NET.Sdk.Web` automatically include support for user secrets. For projects that target `Microsoft.NET.Sdk`, such as console applications, install the configuration extension and user secrets NuGet packages explicitly.

* [PowerShell](#tabpanel_3_powershell)
* [.NET CLI](#tabpanel_3_net-cli)

```
Install-Package Microsoft.Extensions.Configuration
Install-Package Microsoft.Extensions.Configuration.UserSecrets
```

```
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
```

After you install the packages, [initialize the project](#enable-secret-storage) and [set secrets](#set-a-secret) the same way as for a web app. The following example shows a console application that retrieves the value of a secret set with the `AppSecret` key:

```
using Microsoft.Extensions.Configuration;

namespace ConsoleApp;

class Program
{
    static void Main(string[] args)
    {
        IConfigurationRoot config = new ConfigurationBuilder()
            .AddUserSecrets<Program>()
            .Build();

        Console.WriteLine(config["AppSecret"]);
    }
}
```

## Related content

* [GitHub dotnet/aspnetcore.docs issue #30378 (Debugging under Internet Information Services (IIS))](https://github.com/dotnet/AspNetCore.Docs/issues/30378)
* [GitHub dotnet/aspnetcore.docs issue #16328 (Running in IIS)](https://github.com/dotnet/AspNetCore.Docs/issues/16328)
* [Configuration in ASP.NET Core](../fundamentals/configuration/?view=aspnetcore-10.0)
* [Azure Key Vault configuration provider](key-vault-configuration?view=aspnetcore-10.0)

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Kirk Larkin](https://twitter.com/serpent5), [Daniel Roth](https://github.com/danroth27), and [Scott Addie](https://github.com/scottaddie)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/app-secrets/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

This article explains how to manage sensitive data for an ASP.NET Core app on a development machine. Never store passwords or other sensitive data in source code or configuration files. Production secrets shouldn't be used for development or test. Secrets shouldn't be deployed with the app. Production secrets should be accessed through a controlled means like Azure Key Vault. Azure test and production secrets can be stored and protected with the [Azure Key Vault configuration provider](key-vault-configuration?view=aspnetcore-10.0).

For more information on authentication for test and production environments, see [Secure authentication flows](./?view=aspnetcore-10.0#secure-authentication-flows).

## Environment variables

Environment variables are used to avoid storage of app secrets in code or in local configuration files. Environment variables override configuration values for all previously specified configuration sources.

Consider an ASP.NET Core web app in which **Individual User Accounts** security is enabled. A default database connection string is included in the project's `appsettings.json` file with the key `DefaultConnection`. The default connection string is for LocalDB, which runs in user mode and doesn't require a password. During app deployment, the `DefaultConnection` key value can be overridden with an environment variable's value. The environment variable may store the complete connection string with sensitive credentials.

Warning

Environment variables are generally stored in plain, unencrypted text. If the machine or process is compromised, environment variables can be accessed by untrusted parties. Additional measures to prevent disclosure of user secrets may be required.

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For example, [Bash](https://linuxhint.com/bash-environment-variables/) doesn't support colon (`:`) as a separator. All platforms support the double underscore (`__`) syntax and automatically replace it with a colon (`:`).

## Secret Manager

The Secret Manager tool stores sensitive data during application development. In this context, a piece of sensitive data is an app secret. App secrets are stored in a separate location from the project tree. The app secrets are associated with a specific project or shared across several projects. The app secrets aren't checked into source control.

Warning

The Secret Manager tool doesn't encrypt the stored secrets and shouldn't be treated as a trusted store. It's for development purposes only. The keys and values are stored in a JSON configuration file in the user profile directory.

## How the Secret Manager tool works

The Secret Manager tool hides implementation details, such as where and how the values are stored. You can use the tool without knowing these implementation details. The values are stored in a JSON file in the local machine's user profile folder:

* [Windows](#tabpanel_1_windows)
* [Linux / macOS](#tabpanel_1_linux+macos)

File system path:

`%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json`

File system path:

`~/.microsoft/usersecrets/<user_secrets_id>/secrets.json`

In the preceding file paths, replace `<user_secrets_id>` with the `UserSecretsId` value specified in the project file.

Don't write code that depends on the location or format of data saved with the Secret Manager tool. These implementation details may change. For example, the secret values aren't encrypted, but could be in the future.

## Enable secret storage

The Secret Manager tool operates on project-specific configuration settings stored in your user profile.

The Secret Manager tool includes an `init` command in .NET Core SDK 3.0.100 or later. To use user secrets, run the following command in the project directory:

```
dotnet user-secrets init
```

The preceding command adds a `UserSecretsId` element within a `PropertyGroup` of the project file. By default, the inner text of `UserSecretsId` is a GUID. The inner text is arbitrary, but is unique to the project.

```
<PropertyGroup>
  <TargetFramework>netcoreapp3.1</TargetFramework>
  <UserSecretsId>79a3edd0-2092-40a2-a04d-dcb46d5ca9ed</UserSecretsId>
</PropertyGroup>
```

In Visual Studio, right-click the project in Solution Explorer, and select **Manage User Secrets** from the context menu. This gesture adds a `UserSecretsId` element, populated with a GUID, to the project file.

## Set a secret

Define an app secret consisting of a key and its value. The secret is associated with the project's `UserSecretsId` value. For example, run the following command from the directory in which the project file exists:

```
dotnet user-secrets set "Movies:ServiceApiKey" "12345"
```

In the preceding example, the colon denotes that `Movies` is an object literal with a `ServiceApiKey` property.

The Secret Manager tool can be used from other directories too. Use the `--project` option to supply the file system path at which the project file exists. For example:

```
dotnet user-secrets set "Movies:ServiceApiKey" "12345" --project "C:\apps\WebApp1\src\WebApp1"
```

### JSON structure flattening in Visual Studio

Visual Studio's **Manage User Secrets** gesture opens a `secrets.json` file in the text editor. Replace the contents of `secrets.json` with the key-value pairs to be stored. For example:

```
{
  "Movies": {
    "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
    "ServiceApiKey": "12345"
  }
}
```

The JSON structure is flattened after modifications via `dotnet user-secrets remove` or `dotnet user-secrets set`. For example, running `dotnet user-secrets remove "Movies:ConnectionString"` collapses the `Movies` object literal. The modified file resembles the following JSON:

```
{
  "Movies:ServiceApiKey": "12345"
}
```

## Set multiple secrets

A batch of secrets can be set by piping JSON to the `set` command. In the following example, the `input.json` file's contents are piped to the `set` command.

* [Windows](#tabpanel_2_windows)
* [Linux / macOS](#tabpanel_2_linux+macos)

Open a command shell, and execute the following command:

```
type .\input.json | dotnet user-secrets set
```

Open a command shell, and execute the following command:

```
cat ./input.json | dotnet user-secrets set
```

## Access a secret

To access a secret, complete the following steps:

1. [Register the user secrets configuration source](#register-the-user-secrets-configuration-source)
2. [Read the secret via the Configuration API](#read-the-secret-via-the-configuration-api)

### Register the user secrets configuration source

The user secrets [configuration provider](/en-us/dotnet/core/extensions/configuration-providers) registers the appropriate configuration source with the .NET [Configuration API](../fundamentals/configuration/?view=aspnetcore-10.0).

The user secrets configuration source is automatically added in Development mode when the project calls [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder). `CreateDefaultBuilder` calls [AddUserSecrets](/en-us/dotnet/api/microsoft.extensions.configuration.usersecretsconfigurationextensions.addusersecrets) when the [EnvironmentName](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.environmentname#microsoft-extensions-hosting-ihostenvironment-environmentname) is [Development](/en-us/dotnet/api/microsoft.extensions.hosting.environmentname.development#microsoft-extensions-hosting-environmentname-development):

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

When `CreateDefaultBuilder` isn't called, add the user secrets configuration source explicitly by calling [AddUserSecrets](/en-us/dotnet/api/microsoft.extensions.configuration.usersecretsconfigurationextensions.addusersecrets) in [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration). Call `AddUserSecrets` only when the app runs in the `Development` environment, as shown in the following example:

```
public class Program
{
    public static void Main(string[] args)
    {
        var host = new HostBuilder()
            .ConfigureAppConfiguration((hostContext, builder) =>
            {
                // Add other providers for JSON, etc.

                if (hostContext.HostingEnvironment.IsDevelopment())
                {
                    builder.AddUserSecrets<Program>();
                }
            })
            .Build();
        
        host.Run();
    }
}
```

### Read the secret via the Configuration API

If the user secrets configuration source is registered, the .NET Configuration API can read the secrets. [Constructor injection](/en-us/dotnet/core/extensions/dependency-injection/overview#constructor-injection-behavior) can be used to gain access to the .NET Configuration API. Consider the following examples of reading the `Movies:ServiceApiKey` key:

**Startup class:**

```
public class Startup
{
    private string _moviesApiKey = null;

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        _moviesApiKey = Configuration["Movies:ServiceApiKey"];
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Run(async (context) =>
        {
            var result = string.IsNullOrEmpty(_moviesApiKey) ? "Null" : "Not Null";
            await context.Response.WriteAsync($"Secret is {result}");
        });
    }
}
```

**Razor Pages page model:**

```
public class IndexModel : PageModel
{
    private readonly IConfiguration _config;

    public IndexModel(IConfiguration config)
    {
        _config = config;
    }

    public void OnGet()
    {
        var moviesApiKey = _config["Movies:ServiceApiKey"];

        // call Movies service with the API key
    }
}
```

For more information, see [Access configuration in Startup](../fundamentals/configuration/?view=aspnetcore-10.0#access-configuration-in-startup) and [Access configuration in Razor Pages](../fundamentals/configuration/?view=aspnetcore-10.0#access-configuration-in-razor-pages).

## Map secrets to a POCO

Mapping an entire object literal to a POCO (a simple .NET class with properties) is useful for aggregating related properties.

Assume the application *secrets.json* file contains the following two secrets:

```
{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}
```

To map the preceding secrets to a POCO, use the .NET Configuration API's [object graph binding](../fundamentals/configuration/?view=aspnetcore-10.0#bind-an-array) feature. The following code binds to a custom `MovieSettings` POCO and accesses the `ServiceApiKey` property value:

```
var moviesConfig = 
    Configuration.GetSection("Movies").Get<MovieSettings>();
_moviesApiKey = moviesConfig.ServiceApiKey;
```

The `Movies:ConnectionString` and `Movies:ServiceApiKey` secrets are mapped to the respective properties in `MovieSettings`:

```
public class MovieSettings
{
    public string ConnectionString { get; set; }

    public string ServiceApiKey { get; set; }
}
```

## String replacement with secrets

Storing passwords in plain text is insecure. Never store secrets in a configuration file such as `appsettings.json`, which might get checked in to a source code repository.

For example, a database connection string stored in `appsettings.json` should not include a password. Instead, store the password as a secret, and include the password in the connection string at runtime. For example:

```
dotnet user-secrets set "DbPassword" "<secret value>"
```

Replace the `<secret value>` placeholder in the preceding example with the password value. Set the secret's value on a [SqlConnectionStringBuilder](/en-us/dotnet/api/system.data.sqlclient.sqlconnectionstringbuilder) object's [Password](/en-us/dotnet/api/system.data.sqlclient.sqlconnectionstringbuilder.password) property to include it as the password value in the connection string:

```
using System.Data.SqlClient;

var builder = WebApplication.CreateBuilder(args);

var conStrBuilder = new SqlConnectionStringBuilder(
        builder.Configuration.GetConnectionString("Movies"));
conStrBuilder.Password = builder.Configuration["DbPassword"];
var connection = conStrBuilder.ConnectionString;

var app = builder.Build();

app.MapGet("/", () => connection);

app.Run();
```

## List the secrets

Assume the application *secrets.json* file contains the following two secrets:

```
{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}
```

Run the following command from the directory in which the project file exists:

```
dotnet user-secrets list
```

The following output appears:

```
Movies:ConnectionString = Server=(localdb)\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true
Movies:ServiceApiKey = 12345
```

In the preceding example, a colon in the key names denotes the object hierarchy within `secrets.json`.

## Remove a single secret

Assume the application *secrets.json* file contains the following two secrets:

```
{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}
```

Run the following command from the directory in which the project file exists:

```
dotnet user-secrets remove "Movies:ConnectionString"
```

The app's `secrets.json` file was modified to remove the key-value pair associated with the `MoviesConnectionString` key:

```
{
  "Movies": {
    "ServiceApiKey": "12345"
  }
}
```

`dotnet user-secrets list` displays the following message:

```
Movies:ServiceApiKey = 12345
```

## Remove all secrets

Assume the application *secrets.json* file contains the following two secrets:

```
{
  "Movies:ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=Movie-1;Trusted_Connection=True;MultipleActiveResultSets=true",
  "Movies:ServiceApiKey": "12345"
}
```

Run the following command from the directory in which the project file exists:

```
dotnet user-secrets clear
```

All user secrets for the app have been deleted from the `secrets.json` file:

```
{}
```

Running `dotnet user-secrets list` displays the following message:

```
No secrets configured for this application.
```

[]()

## Manage user secrets with Visual Studio

To manage user secrets in Visual Studio, right click the project in solution explorer and select **Manage User Secrets**:

## Migrating User Secrets from ASP.NET Framework to ASP.NET Core

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/27611).

## User secrets in non-web applications

Projects that target `Microsoft.NET.Sdk.Web` automatically include support for user secrets. For projects that target `Microsoft.NET.Sdk`, such as console applications, install the configuration extension and user secrets NuGet packages explicitly.

Using PowerShell:

```
Install-Package Microsoft.Extensions.Configuration
Install-Package Microsoft.Extensions.Configuration.UserSecrets
```

Using the .NET CLI:

```
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
```

Once the packages are installed, [initialize the project](#enable-secret-storage) and [set secrets](#set-a-secret) the same way as for a web app. The following example shows a console application that retrieves the value of a secret that was set with the key "AppSecret":

```
using Microsoft.Extensions.Configuration;

namespace ConsoleApp;

class Program
{
    static void Main(string[] args)
    {
        IConfigurationRoot config = new ConfigurationBuilder()
            .AddUserSecrets<Program>()
            .Build();

        Console.WriteLine(config["AppSecret"]);
    }
}
```

## Related content

* See [this issue](https://github.com/dotnet/AspNetCore.Docs/issues/30378) and [this issue](https://github.com/dotnet/AspNetCore.Docs/issues/16328) for information on accessing user secrets from IIS.
* [Configuration in ASP.NET Core](../fundamentals/configuration/?view=aspnetcore-10.0)
* [Azure Key Vault configuration provider](key-vault-configuration?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/http3
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel/http3.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use HTTP/3 with the ASP.NET Core Kestrel web server

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[HTTP/3](https://datatracker.ietf.org/doc/rfc9114/) is an approved standard and the third major version of HTTP. This article discusses the requirements for HTTP/3. HTTP/3 is fully supported in .NET 7 or later.

Important

Apps configured to take advantage of HTTP/3 should be designed to also support HTTP/1.1 and HTTP/2.

## HTTP/3 benefits

`HTTP/3`:

* Is the latest version of the Hypertext Transfer Protocol.
* Builds on the strengths of `HTTP/2` while addressing some of its limitations, particularly in terms of performance, latency, reliability, and security.

| Feature      | `HTTP/2`                                                    | `HTTP/3`                                                 |
| ------------ | ----------------------------------------------------------- | -------------------------------------------------------- |
| Transport    | Uses [TCP](https://developer.mozilla.org/docs/Glossary/TCP) | Uses [QUIC](https://www.rfc-editor.org/rfc/rfc9000.html) |
| Connection   | Slower due to TCP + TLS                                     | Combines transport and encryption handshakes             |
| Setup        | handshake                                                   | handshakes                                               |
| Head-of-Line | Affected by TCP-level                                       | Eliminated with QUIC                                     |
| Blocking     | blocking                                                    | stream multiplexing                                      |
| Encryption   | TLS over TCP                                                | TLS is built into QUIC                                   |

The key differences from `HTTP/2` to `HTTP/3` are:

* **Transport Protocol**: `HTTP/3` uses QUIC instead of TCP. QUIC offers improved performance, lower latency, and better reliability, especially on mobile and lossy networks.
* **Head-of-Line Blocking**: `HTTP/2` can suffer from head-of-line blocking at the TCP level, where a delay in one stream can affect others. `HTTP/3`, with QUIC, provides independent streams, so packet loss in one stream doesn't stall others.
* **Connection Establishment**: `HTTP/3` with QUIC can establish connections faster, as it combines transport and encryption handshakes.
* **Encryption**: `HTTP/3` mandates TLS 1.3 encryption, providing enhanced security by default, whereas it's optional in `HTTP/2`.
* **Multiplexing**: While both support multiplexing, `HTTP/3`'s implementation with QUIC is more efficient and avoids the TCP-level head-of-line blocking issues.
* **Connection Migration**: QUIC in `HTTP/3` allows connections to persist even when a client's IP address changes (like switching from Wi-Fi to cellular), improving mobile user experience.

## Early request processing

Kestrel can process HTTP/3 requests without first waiting for the control stream and the initial SETTINGS frame. This optimization reduces first-request latency on new HTTP/3 connections.

In ASP.NET Core versions earlier than .NET 11, Kestrel waited to receive the QUIC control stream and its initial SETTINGS frame before processing any request streams. This requirement is no longer necessary, which means the first request on a new connection completes faster.

## HTTP/3 requirements

HTTP/3 uses QUIC as its transport protocol. The ASP.NET Core implementation of HTTP/3 depends on [MsQuic](https://github.com/microsoft/msquic) to provide QUIC functionality. As a result, ASP.NET Core support of HTTP/3 depends on MsQuic platform requirements. For more information on how to install **MsQuic**, see [QUIC Platform dependencies](/en-us/dotnet/fundamentals/networking/quic/quic-overview#platform-dependencies). If the platform that Kestrel is running on doesn't have all the requirements for HTTP/3, Kestrel disables HTTP/3 and falls back to other HTTP protocols.

## Getting started

HTTP/3 isn't enabled by default. Add configuration to `Program.cs` to enable HTTP/3.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, options) =>
{
    options.ListenAnyIP(5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
        listenOptions.UseHttps();
    });
});
```

The preceding code configures port 5001 to:

* Use HTTP/3 alongside HTTP/1.1 and HTTP/2 by specifying `HttpProtocols.Http1AndHttp2AndHttp3`.
* Enable HTTPS by using `UseHttps`. HTTP/3 requires HTTPS.

Because not all routers, firewalls, and proxies properly support HTTP/3, configure HTTP/3 together with HTTP/1.1 and HTTP/2. Specify [`HttpProtocols.Http1AndHttp2AndHttp3`](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.httpprotocols#microsoft-aspnetcore-server-kestrel-core-httpprotocols-http1andhttp2andhttp3) as an endpoint's supported protocols.

For more information, see [Configure endpoints for Kestrel web server](endpoints?view=aspnetcore-10.0).

## Configure QuicTransportOptions

Configure QUIC transport options by calling the [UseQuic](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderquicextensions.usequic) extension method on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder).

```
        var builder = WebApplication.CreateBuilder(args);

        builder.WebHost.UseQuic(options =>
        {
#pragma warning disable CA2252 // Using preview features
            options.MaxBidirectionalStreamCount = 200;
#pragma warning restore CA2252
        });

        builder.WebHost.ConfigureKestrel((context, serverOptions) =>
        {
            serverOptions.ListenAnyIP(5001, listenOptions =>
            {
                listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
                listenOptions.UseHttps();
            });
        });
```

The following table describes the available [QuicTransportOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.quic.quictransportoptions).

| Option                                                                                                                                                                                                                                                      | Default                           | Description                                                                                   |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------- |
| [MaxBidirectionalStreamCount](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.quic.quictransportoptions.maxbidirectionalstreamcount#microsoft-aspnetcore-server-kestrel-transport-quic-quictransportoptions-maxbidirectionalstreamcount)    | `100`                             | The maximum number of concurrent bidirectional streams per connection.                        |
| [MaxUnidirectionalStreamCount](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.quic.quictransportoptions.maxunidirectionalstreamcount#microsoft-aspnetcore-server-kestrel-transport-quic-quictransportoptions-maxunidirectionalstreamcount) | `10`                              | The maximum number of concurrent inbound unidirectional streams per connection.               |
| [MaxReadBufferSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.quic.quictransportoptions.maxreadbuffersize#microsoft-aspnetcore-server-kestrel-transport-quic-quictransportoptions-maxreadbuffersize)                                  | `1024 * 1024` (1 MB)              | The maximum read buffer size in bytes.                                                        |
| [MaxWriteBufferSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.quic.quictransportoptions.maxwritebuffersize#microsoft-aspnetcore-server-kestrel-transport-quic-quictransportoptions-maxwritebuffersize)                               | `64 * 1024` (64 KB)               | The maximum write buffer size in bytes.                                                       |
| [Backlog](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.quic.quictransportoptions.backlog#microsoft-aspnetcore-server-kestrel-transport-quic-quictransportoptions-backlog)                                                                | `512`                             | The maximum length of the pending connection queue.                                           |
| [DefaultStreamErrorCode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.quic.quictransportoptions.defaultstreamerrorcode#microsoft-aspnetcore-server-kestrel-transport-quic-quictransportoptions-defaultstreamerrorcode)                   | `0x010c` (H3\_REQUEST\_CANCELLED) | Error code used when the stream should abort the read or write side of the stream internally. |
| [DefaultCloseErrorCode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.quic.quictransportoptions.defaultcloseerrorcode#microsoft-aspnetcore-server-kestrel-transport-quic-quictransportoptions-defaultcloseerrorcode)                      | `0x100` (H3\_NO\_ERROR)           | Error code used when an open connection is disposed.                                          |

## Alt-svc

HTTP/3 is discovered as an upgrade from HTTP/1.1 or HTTP/2 via the [`alt-svc`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Alt-Svc) header. That means the first request will normally use HTTP/1.1 or HTTP/2 before switching to HTTP/3. Kestrel automatically adds the `alt-svc` header if HTTP/3 is enabled.

## Localhost testing

* Browsers don't support self-signed certificates on HTTP/3, such as the Kestrel development certificate.

* Use `HttpClient` for localhost or loopback testing in .NET 6 or later. When you use `HttpClient` to make an HTTP/3 request, you need extra configuration:

  * Set [`HttpRequestMessage.Version`](/en-us/dotnet/api/system.net.http.httprequestmessage.version#system-net-http-httprequestmessage-version) to 3.0, or
  * Set [`HttpRequestMessage.VersionPolicy`](/en-us/dotnet/api/system.net.http.httprequestmessage.versionpolicy#system-net-http-httprequestmessage-versionpolicy) to [`HttpVersionPolicy.RequestVersionOrHigher`](/en-us/dotnet/api/system.net.http.httpversionpolicy#system-net-http-httpversionpolicy-requestversionorhigher).

For more information about how to use HTTP/3 with `HttpClient`, see [HTTP/3 with .NET](/en-us/dotnet/core/extensions/httpclient-http3).

[HTTP/3](https://datatracker.ietf.org/doc/rfc9114/) is an approved standard and the third major version of HTTP. This article discusses the requirements for HTTP/3. HTTP/3 is fully supported in .NET 7 or later.

Important

Apps configured to take advantage of HTTP/3 should be designed to also support HTTP/1.1 and HTTP/2.

## HTTP/3 benefits

`HTTP/3`:

* Is the latest version of the Hypertext Transfer Protocol.
* Builds on the strengths of `HTTP/2` while addressing some of its limitations, particularly in terms of performance, latency, reliability, and security.

| Feature      | `HTTP/2`                                                    | `HTTP/3`                                                 |
| ------------ | ----------------------------------------------------------- | -------------------------------------------------------- |
| Transport    | Uses [TCP](https://developer.mozilla.org/docs/Glossary/TCP) | Uses [QUIC](https://www.rfc-editor.org/rfc/rfc9000.html) |
| Connection   | Slower due to TCP + TLS                                     | Combines transport and encryption handshakes             |
| Setup        | handshake                                                   | handshakes                                               |
| Head-of-Line | Affected by TCP-level                                       | Eliminated with QUIC                                     |
| Blocking     | blocking                                                    | stream multiplexing                                      |
| Encryption   | TLS over TCP                                                | TLS is built into QUIC                                   |

The key differences from `HTTP/2` to `HTTP/3` are:

* **Transport Protocol**: `HTTP/3` uses QUIC instead of TCP. QUIC offers improved performance, lower latency, and better reliability, especially on mobile and lossy networks.
* **Head-of-Line Blocking**: `HTTP/2` can suffer from head-of-line blocking at the TCP level, where a delay in one stream can affect others. `HTTP/3`, with QUIC, provides independent streams, so packet loss in one stream doesn't stall others.
* **Connection Establishment**: `HTTP/3` with QUIC can establish connections faster, as it combines transport and encryption handshakes.
* **Encryption**: `HTTP/3` mandates TLS 1.3 encryption, providing enhanced security by default, whereas it's optional in `HTTP/2`.
* **Multiplexing**: While both support multiplexing, `HTTP/3`'s implementation with QUIC is more efficient and avoids the TCP-level head-of-line blocking issues.
* **Connection Migration**: QUIC in `HTTP/3` allows connections to persist even when a client's IP address changes (like switching from Wi-Fi to cellular), improving mobile user experience.

## HTTP/3 requirements

HTTP/3 uses QUIC as its transport protocol. The ASP.NET Core implementation of HTTP/3 depends on [MsQuic](https://github.com/microsoft/msquic) to provide QUIC functionality. As a result, ASP.NET Core support of HTTP/3 depends on MsQuic platform requirements. For more information on how to install **MsQuic**, see [QUIC Platform dependencies](/en-us/dotnet/fundamentals/networking/quic/quic-overview#platform-dependencies). If the platform that Kestrel is running on doesn't have all the requirements for HTTP/3, then it's disabled, and Kestrel will fall back to other HTTP protocols.

## Getting started

HTTP/3 is not enabled by default. Add configuration to `Program.cs` to enable HTTP/3.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, options) =>
{
    options.ListenAnyIP(5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
        listenOptions.UseHttps();
    });
});
```

The preceding code configures port 5001 to:

* Use HTTP/3 alongside HTTP/1.1 and HTTP/2 by specifying `HttpProtocols.Http1AndHttp2AndHttp3`.
* Enable HTTPS with `UseHttps`. HTTP/3 requires HTTPS.

Because not all routers, firewalls, and proxies properly support HTTP/3, HTTP/3 should be configured together with HTTP/1.1 and HTTP/2. This can be done by specifying [`HttpProtocols.Http1AndHttp2AndHttp3`](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.httpprotocols#microsoft-aspnetcore-server-kestrel-core-httpprotocols-http1andhttp2andhttp3) as an endpoint's supported protocols.

For more information, see [Configure endpoints for Kestrel web server](endpoints?view=aspnetcore-10.0).

## Configure QuicTransportOptions

QUIC transport options can be configured by calling the [UseQuic](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderquicextensions.usequic) extension method on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder).

```
        var builder = WebApplication.CreateBuilder(args);

        builder.WebHost.UseQuic(options =>
        {
#pragma warning disable CA2252 // Using preview features
            options.MaxBidirectionalStreamCount = 200;
#pragma warning restore CA2252
        });

        builder.WebHost.ConfigureKestrel((context, serverOptions) =>
        {
            serverOptions.ListenAnyIP(5001, listenOptions =>
            {
                listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
                listenOptions.UseHttps();
            });
        });
```

For the full list of available QUIC transport options and their descriptions, see [QuicTransportOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.quic.quictransportoptions).

## Alt-svc

HTTP/3 is discovered as an upgrade from HTTP/1.1 or HTTP/2 via the [`alt-svc`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Alt-Svc) header. That means the first request will normally use HTTP/1.1 or HTTP/2 before switching to HTTP/3. Kestrel automatically adds the `alt-svc` header if HTTP/3 is enabled.

## Localhost testing

* Browsers don't allow self-signed certificates on HTTP/3, such as the Kestrel development certificate.

* `HttpClient` can be used for localhost/loopback testing in .NET 6 or later. Extra configuration is required when using `HttpClient` to make an HTTP/3 request:

  * Set [`HttpRequestMessage.Version`](/en-us/dotnet/api/system.net.http.httprequestmessage.version#system-net-http-httprequestmessage-version) to 3.0, or
  * Set [`HttpRequestMessage.VersionPolicy`](/en-us/dotnet/api/system.net.http.httprequestmessage.versionpolicy#system-net-http-httprequestmessage-versionpolicy) to [`HttpVersionPolicy.RequestVersionOrHigher`](/en-us/dotnet/api/system.net.http.httpversionpolicy#system-net-http-httpversionpolicy-requestversionorhigher).

For more information on how to use HTTP/3 with `HttpClient`, see [HTTP/3 with .NET](/en-us/dotnet/core/extensions/httpclient-http3).

[HTTP/3](https://datatracker.ietf.org/doc/rfc9114/) is a proposed standard and the third major version of HTTP. This article discusses requirements for HTTP/3. HTTP/3 is fully supported in .NET 7 or later.

Important

Apps configured to take advantage of HTTP/3 should be designed to also support HTTP/1.1 and HTTP/2.

## HTTP/3 requirements

HTTP/3 has different requirements depending on the operating system. If the platform that Kestrel is running on doesn't have all the requirements for HTTP/3 then it's disabled, and Kestrel will fallback to other HTTP protocols.

### Windows

* Windows 11 Build 22000 or later OR Windows Server 2022.
* TLS 1.3 or later connection.

### Linux

* `libmsquic` package installed.

`libmsquic` is published via Microsoft's official Linux package repository at `packages.microsoft.com`. To install this package:

1. Add the `packages.microsoft.com` repository. See [Linux Software Repository for Microsoft Products](/en-us/windows-server/administration/linux-package-repository-for-microsoft-software) for instructions.
2. Install the `libmsquic` package using the distro's package manager. For example, `apt install libmsquic=1.9*` on Ubuntu.

**Note:** .NET 6 is only compatible with the 1.9.x versions of libmsquic. Libmsquic 2.x is not compatible due to breaking changes. Libmsquic receives updates to 1.9.x when needed to incorporate security fixes.

### macOS

HTTP/3 isn't currently supported on macOS and may be available in a future release.

## Getting started

HTTP/3 is not enabled by default. Add configuration to `Program.cs` to enable HTTP/3.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, options) =>
{
    options.ListenAnyIP(5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
        listenOptions.UseHttps();
    });
});
```

The preceding code configures port 5001 to:

* Use HTTP/3 alongside HTTP/1.1 and HTTP/2 by specifying `HttpProtocols.Http1AndHttp2AndHttp3`.
* Enable HTTPS with `UseHttps`. HTTP/3 requires HTTPS.

Because not all routers, firewalls, and proxies properly support HTTP/3, HTTP/3 should be configured together with HTTP/1.1 and HTTP/2. This can be done by specifying [`HttpProtocols.Http1AndHttp2AndHttp3`](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.httpprotocols#microsoft-aspnetcore-server-kestrel-core-httpprotocols-http1andhttp2andhttp3) as an endpoint's supported protocols.

For more information, see [Configure endpoints for Kestrel web server](endpoints?view=aspnetcore-10.0).

## Alt-svc

HTTP/3 is discovered as an upgrade from HTTP/1.1 or HTTP/2 via the [`alt-svc`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Alt-Svc) header. That means the first request will normally use HTTP/1.1 or HTTP/2 before switching to HTTP/3. Kestrel automatically adds the `alt-svc` header if HTTP/3 is enabled.

## Localhost testing

* Browsers don't allow self-signed certificates on HTTP/3 such as the Kestrel development certificate.

* `HttpClient` can be used for localhost/loopback testing in .NET 6 or later. Extra configuration is required when using `HttpClient` to make an HTTP/3 request:

  * Set [`HttpRequestMessage.Version`](/en-us/dotnet/api/system.net.http.httprequestmessage.version#system-net-http-httprequestmessage-version) to 3.0, or
  * Set [`HttpRequestMessage.VersionPolicy`](/en-us/dotnet/api/system.net.http.httprequestmessage.versionpolicy#system-net-http-httprequestmessage-versionpolicy) to [`HttpVersionPolicy.RequestVersionOrHigher`](/en-us/dotnet/api/system.net.http.httpversionpolicy#system-net-http-httpversionpolicy-requestversionorhigher).

## HTTP/3 benefits

HTTP/3 uses the same semantics as HTTP/1.1 and HTTP/2: the same request methods, status codes, and message fields apply to all versions. The differences are in the underlying transport. Both HTTP/1.1 and HTTP/2 use TCP as their transport. HTTP/3 uses a new transport technology developed alongside HTTP/3 called [QUIC](https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-34).

HTTP/3 and QUIC have a number of benefits compared to HTTP/1.1 and HTTP/2:

* Faster response time of the first request. QUIC and HTTP/3 negotiates the connection in fewer round-trips between the client and the server. The first request reaches the server faster.
* Improved experience when there is connection packet loss. HTTP/2 multiplexes multiple requests via one TCP connection. Packet loss on the connection affects all requests. This problem is called "head-of-line blocking". Because QUIC provides native multiplexing, lost packets only impact the requests where data has been lost.
* Supports transitioning between networks. This feature is useful for mobile devices where it is common to switch between WIFI and cellular networks as a mobile device changes location. Currently HTTP/1.1 and HTTP/2 connections fail with an error when switching networks. An app or web browsers must retry any failed HTTP requests. HTTP/3 allows the app or web browser to seamlessly continue when a network changes. Kestrel doesn't support network transitions in .NET 6. It may be available in a future release.

[HTTP/3](https://datatracker.ietf.org/doc/rfc9114/) is the third and upcoming major version of HTTP. This article discusses requirements for HTTP/3 and how to configure Kestrel to use it.

Important

HTTP/3 is available in .NET 6 as a *preview feature*. The HTTP/3 specification isn't finalized and behavioral or performance issues may exist in HTTP/3 with .NET 6.

For more information on preview feature support, see [the preview features supported section](https://github.com/dotnet/designs/blob/main/accepted/2021/preview-features/preview-features.md#are-preview-features-supported).

Apps configured to take advantage of HTTP/3 should be designed to also support HTTP/1.1 and HTTP/2. If issues are identified in HTTP/3, we recommend disabling HTTP/3 until the issues are resolved in a future release of ASP.NET Core. Significant issues are reported at the [Announcements GitHub repository](https://github.com/aspnet/Announcements/issues).

## HTTP/3 requirements

HTTP/3 has different requirements depending on the operating system. If the platform that Kestrel is running on doesn't have all the requirements for HTTP/3 then it's disabled, and Kestrel will fallback to other HTTP protocols.

### Windows

* Windows 11 Build 22000 or later OR Windows Server 2022.
* TLS 1.3 or later connection.

### Linux

* `libmsquic` package installed.

`libmsquic` is published via Microsoft's official Linux package repository at `packages.microsoft.com`. To install this package:

1. Add the `packages.microsoft.com` repository. See [Linux Software Repository for Microsoft Products](/en-us/windows-server/administration/linux-package-repository-for-microsoft-software) for instructions.
2. Install the `libmsquic` package using the distro's package manager. For example, `apt install libmsquic=1.9*` on Ubuntu.

**Note:** .NET 6 is only compatible with the 1.9.x versions of libmsquic. Libmsquic 2.x is not compatible due to breaking changes. Libmsquic receives updates to 1.9.x when needed to incorporate security fixes.

### macOS

HTTP/3 isn't currently supported on macOS and may be available in a future release.

## Getting started

HTTP/3 is not enabled by default. Add configuration to `Program.cs` to enable HTTP/3.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, options) =>
{
    options.ListenAnyIP(5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
        listenOptions.UseHttps();
    });
});
```

The preceding code configures port 5001 to:

* Use HTTP/3 alongside HTTP/1.1 and HTTP/2 by specifying `HttpProtocols.Http1AndHttp2AndHttp3`.
* Enable HTTPS with `UseHttps`. HTTP/3 requires HTTPS.

Because not all routers, firewalls, and proxies properly support HTTP/3, HTTP/3 should be configured together with HTTP/1.1 and HTTP/2. This can be done by specifying `HttpProtocols.Http1AndHttp2AndHttp3` as an endpoint's supported protocols.

For more information, see [Configure endpoints for Kestrel web server](endpoints?view=aspnetcore-10.0).

## Alt-svc

HTTP/3 is discovered as an upgrade from HTTP/1.1 or HTTP/2 via the `alt-svc` header. That means the first request will normally use HTTP/1.1 or HTTP/2 before switching to HTTP/3. Kestrel automatically adds the `alt-svc` header if HTTP/3 is enabled.

## Localhost testing

* Browsers don't allow self-signed certificates on HTTP/3 such as the Kestrel development certificate.

* `HttpClient` can be used for localhost/loopback testing in .NET 6 or later. Extra configuration is required when using `HttpClient` to make an HTTP/3 request:

  * Set `HttpRequestMessage.Version` to 3.0, or
  * Set `HttpRequestMessage.VersionPolicy` to `HttpVersionPolicy.RequestVersionOrHigher`.

## Limitations

Some HTTPS scenarios are not yet supported for HTTP/3 in Kestrel. When calling `Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps` with [HttpsConnectionAdapterOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions) while using HTTP/3, setting the following options on the [HttpsConnectionAdapterOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions) is a no-op (it does nothing):

* [HandshakeTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.handshaketimeout#microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions-handshaketimeout)
* [OnAuthenticate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.onauthenticate#microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions-onauthenticate)

Calling the following implementations of `Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps` throw an error when using HTTP/3:

* [UseHttps(this ListenOptions listenOptions, ServerOptionsSelectionCallback serverOptionsSelectionCallback, object state, TimeSpan handshakeTimeout)](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionshttpsextensions.usehttps#microsoft-aspnetcore-hosting-listenoptionshttpsextensions-usehttps\(microsoft-aspnetcore-server-kestrel-core-listenoptions-system-net-security-serveroptionsselectioncallback-system-object-system-timespan\))
* [UseHttps(this ListenOptions listenOptions, TlsHandshakeCallbackOptions callbackOptions)](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionshttpsextensions.usehttps#microsoft-aspnetcore-hosting-listenoptionshttpsextensions-usehttps\(microsoft-aspnetcore-server-kestrel-core-listenoptions-microsoft-aspnetcore-server-kestrel-https-tlshandshakecallbackoptions\))

## HTTP/3 benefits

HTTP/3 uses the same semantics as HTTP/1.1 and HTTP/2: the same request methods, status codes, and message fields apply to all versions. The differences are in the underlying transport. Both HTTP/1.1 and HTTP/2 use TCP as their transport. HTTP/3 uses a new transport technology developed alongside HTTP/3 called [QUIC](https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-34).

HTTP/3 and QUIC have a number of benefits compared to HTTP/1.1 and HTTP/2:

* Faster response time of the first request. QUIC and HTTP/3 negotiates the connection in fewer round-trips between the client and the server. The first request reaches the server faster.
* Improved experience when there is connection packet loss. HTTP/2 multiplexes multiple requests via one TCP connection. Packet loss on the connection affects all requests. This problem is called "head-of-line blocking". Because QUIC provides native multiplexing, lost packets only impact the requests where data has been lost.
* Supports transitioning between networks. This feature is useful for mobile devices where it is common to switch between WIFI and cellular networks as a mobile device changes location. Currently HTTP/1.1 and HTTP/2 connections fail with an error when switching networks. An app or web browsers must retry any failed HTTP requests. HTTP/3 allows the app or web browser to seamlessly continue when a network changes. Kestrel doesn't support network transitions in .NET 6. It may be available in a future release.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/middleware
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/caching/middleware.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Response caching middleware in ASP.NET Core

By [John Luo](https://github.com/JunTaoLuo) and [Rick Anderson](https://twitter.com/RickAndMSFT)

This article explains how to configure [response caching middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCaching/src/ResponseCachingMiddleware.cs) in an ASP.NET Core app. The middleware determines when responses are cacheable, stores responses, and serves responses from cache. For an introduction to HTTP caching and the [`[ResponseCache]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) attribute, see [Response Caching](response?view=aspnetcore-10.0).

## Configuration

In `Program.cs`, add the response caching middleware services [AddResponseCaching](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.responsecachingservicesextensions.addresponsecaching) to the service collection and configure the app to use the middleware with the [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching) extension method. `UseResponseCaching` adds the middleware to the request processing pipeline:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseHttpsRedirection();

// UseCors must be called before UseResponseCaching
//app.UseCors();

app.UseResponseCaching();
```

Warning

[UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) must be called before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching) when using [CORS middleware](../../security/cors?view=aspnetcore-10.0).

The sample app adds headers to control caching on subsequent requests:

* [Cache-Control](https://www.rfc-editor.org/rfc/rfc9111#field.cache-control): Caches cacheable responses for up to 10 seconds.
* [Vary](https://www.rfc-editor.org/rfc/rfc9110#field.vary): Configures the middleware to serve a cached response only if the [Accept-Encoding](https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding) header of subsequent requests matches that of the original request.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseHttpsRedirection();

// UseCors must be called before UseResponseCaching
//app.UseCors();

app.UseResponseCaching();

app.Use(async (context, next) =>
{
    context.Response.GetTypedHeaders().CacheControl =
        new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
        {
            Public = true,
            MaxAge = TimeSpan.FromSeconds(10)
        };
    context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] =
        new string[] { "Accept-Encoding" };

    await next();
});

app.MapGet("/", () => DateTime.Now.Millisecond);

app.Run();
```

The preceding headers are not written to the response and are overridden when a controller, action, or Razor Page:

* Has a [\[ResponseCache\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) attribute. This applies even if a property isn't set. For example, omitting the [VaryByHeader](response?view=aspnetcore-10.0#vary) property will cause the corresponding header to be removed from the response.

Response caching middleware only caches server responses that result in a 200 (OK) status code. Any other responses, including [error pages](../../fundamentals/error-handling?view=aspnetcore-10.0), are ignored by the middleware.

Warning

Responses containing content for authenticated clients must be marked as not cacheable to prevent the middleware from storing and serving those responses. See [Conditions for caching](#conditions-for-caching) for details on how the middleware determines if a response is cacheable.

The preceding code typically doesn't return a cached value to a browser. Use [Fiddler](https://www.telerik.com/fiddler) or another tool that can explicitly set request headers and is preferred for testing caching. For more information, see [Troubleshooting](#troubleshooting) in this article.

## Options

Response caching options are shown in the following table.

| Option                                                                                                                                                                                                         | Description                                                                                                   |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| [MaximumBodySize](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.maximumbodysize#microsoft-aspnetcore-responsecaching-responsecachingoptions-maximumbodysize)                   | The largest cacheable size for the response body in bytes. The default value is `64 * 1024 * 1024` (64 MB).   |
| [SizeLimit](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.sizelimit#microsoft-aspnetcore-responsecaching-responsecachingoptions-sizelimit)                                     | The size limit for the response cache middleware in bytes. The default value is `100 * 1024 * 1024` (100 MB). |
| [UseCaseSensitivePaths](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.usecasesensitivepaths#microsoft-aspnetcore-responsecaching-responsecachingoptions-usecasesensitivepaths) | Determines if responses are cached on case-sensitive paths. The default value is `false`.                     |

The following example configures the middleware to:

* Cache responses with a body size smaller than or equal to 1,024 bytes.
* Store the responses by case-sensitive paths. For example, `/page1` and `/Page1` are stored separately.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching(options =>
{
    options.MaximumBodySize = 1024;
    options.UseCaseSensitivePaths = true;
});

var app = builder.Build();

app.UseHttpsRedirection();

// UseCors must be called before UseResponseCaching
//app.UseCors();

app.UseResponseCaching();

app.Use(async (context, next) =>
{
    context.Response.GetTypedHeaders().CacheControl =
        new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
        {
            Public = true,
            MaxAge = TimeSpan.FromSeconds(10)
        };
    context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] =
        new string[] { "Accept-Encoding" };

    await next(context);
});

app.MapGet("/", () => DateTime.Now.Millisecond);

app.Run();
```

## VaryByQueryKeys

When using MVC, web API controllers, or Razor Pages page models, the [`[ResponseCache]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) attribute specifies the parameters necessary for setting the appropriate headers for response caching. The only parameter of the `[ResponseCache]` attribute that strictly requires the middleware is [VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute.varybyquerykeys#microsoft-aspnetcore-mvc-responsecacheattribute-varybyquerykeys), which doesn't correspond to an actual HTTP header. For more information, see [Response caching in ASP.NET Core](response?view=aspnetcore-10.0#responsecache-attribute).

When not using the `[ResponseCache]` attribute, response caching can be varied with `VaryByQueryKeys`. Use the [ResponseCachingFeature](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingfeature) directly from the [HttpContext.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features#microsoft-aspnetcore-http-httpcontext-features):

```
var responseCachingFeature = context.HttpContext.Features.Get<IResponseCachingFeature>();

if (responseCachingFeature != null)
{
    responseCachingFeature.VaryByQueryKeys = new[] { "MyKey" };
}
```

Using a single value equal to `*` in `VaryByQueryKeys` varies the cache by all request query parameters.

## HTTP headers used by response caching middleware

The following table provides information on HTTP headers that affect response caching.

| Header              | Details                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorization`     | The response isn't cached if the header exists.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `Cache-Control`     | The middleware only considers caching responses marked with the `public` cache directive. Control caching with the following parameters:- max-age
- max-stale†
- min-fresh
- must-revalidate
- no-cache
- no-store
- only-if-cached
- private
- public
- s-maxage
- proxy-revalidate‡†If no limit is specified to `max-stale`, the middleware takes no action. ‡`proxy-revalidate` has the same effect as `must-revalidate`. For more information, see [RFC 9111: Request Directives](https://www.rfc-editor.org/rfc/rfc9111.html#name-request-directives). |
| `Pragma`            | A `Pragma: no-cache` header in the request produces the same effect as `Cache-Control: no-cache`. This header is overridden by the relevant directives in the `Cache-Control` header, if present. Considered for backward compatibility with HTTP/1.0.                                                                                                                                                                                                                                                                                                      |
| `Set-Cookie`        | The response isn't cached if the header exists. Any middleware in the request processing pipeline that sets one or more cookies prevents the response caching middleware from caching the response (for example, the [cookie-based TempData provider](../../fundamentals/app-state?view=aspnetcore-10.0#tempdata)).                                                                                                                                                                                                                                         |
| `Vary`              | The `Vary` header is used to vary the cached response by another header. For example, cache responses by encoding by including the `Vary: Accept-Encoding` header, which caches responses for requests with headers `Accept-Encoding: gzip` and `Accept-Encoding: text/plain` separately. A response with a header value of `*` is never stored.                                                                                                                                                                                                            |
| `Expires`           | A response deemed stale by this header isn't stored or retrieved unless overridden by other `Cache-Control` headers.                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `If-None-Match`     | The full response is served from cache if the value isn't `*` and the `ETag` of the response doesn't match any of the values provided. Otherwise, a 304 (Not Modified) response is served.                                                                                                                                                                                                                                                                                                                                                                  |
| `If-Modified-Since` | If the `If-None-Match` header isn't present, a full response is served from cache if the cached response date is newer than the value provided. Otherwise, a *304 - Not Modified* response is served.                                                                                                                                                                                                                                                                                                                                                       |
| `Date`              | When serving from cache, the `Date` header is set by the middleware if it wasn't provided on the original response.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `Content-Length`    | When serving from cache, the `Content-Length` header is set by the middleware if it wasn't provided on the original response.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `Age`               | The `Age` header sent in the original response is ignored. The middleware computes a new value when serving a cached response.                                                                                                                                                                                                                                                                                                                                                                                                                              |

## Caching respects request Cache-Control directives

The middleware respects the rules of [RFC 9111: HTTP Caching (Section 5.2. Cache-Control)](https://www.rfc-editor.org/rfc/rfc9111#field.cache-control). The rules require a cache to honor a valid `Cache-Control` header sent by the client. Under the specification, a client can make requests with a `no-cache` header value and force the server to generate a new response for every request. Currently, there's no developer control over this caching behavior when using the middleware because the middleware adheres to the official caching specification.

For more control over caching behavior, explore other caching features of ASP.NET Core. See the following topics:

* [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0)
* [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0)
* [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0)
* [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0)

## Troubleshooting

The [response caching middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCaching/src/ResponseCachingMiddleware.cs) uses [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache), which has a limited capacity. When the capacity is exceeded, the [memory cache is compacted (`TriggerOvercapacityCompaction`)](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

If caching behavior isn't as expected, confirm that responses are cacheable and capable of being served from the cache. Examine the request's incoming headers and the response's outgoing headers. Enable [logging](../../fundamentals/logging/?view=aspnetcore-10.0) to help with debugging.

When testing and troubleshooting caching behavior, a browser typically sets request headers that prevent caching. For example, a browser may set the `Cache-Control` header to `no-cache` or `max-age=0` when refreshing a page. [Fiddler](https://www.telerik.com/fiddler) and other tools can explicitly set request headers and are preferred for testing caching.

[]()

### Conditions for caching

* The request must result in a server response with a 200 (OK) status code.
* The request method must be GET or HEAD.
* Response caching middleware must be placed before middleware that require caching. For more information, see [ASP.NET Core middleware](../../fundamentals/middleware/?view=aspnetcore-10.0).
* The `Authorization` header must not be present.
* `Cache-Control` header parameters must be valid, and the response must be marked `public` and not marked `private`.
* The `Pragma: no-cache` header must not be present if the `Cache-Control` header isn't present, as the `Cache-Control` header overrides the `Pragma` header when present.
* The `Set-Cookie` header must not be present.
* `Vary` header parameters must be valid and not equal to `*`.
* The `Content-Length` header value (if set) must match the size of the response body.
* The [IHttpSendFileFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpsendfilefeature) isn't used.
* The response must not be stale as specified by the `Expires` header and the `max-age` and `s-maxage` cache directives.
* Response buffering must be successful. The size of the response must be smaller than the configured or default [SizeLimit](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.sizelimit#microsoft-aspnetcore-responsecaching-responsecachingoptions-sizelimit). The body size of the response must be smaller than the configured or default [MaximumBodySize](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.maximumbodysize#microsoft-aspnetcore-responsecaching-responsecachingoptions-maximumbodysize).
* The response must be cacheable according to [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111). For example, the `no-store` directive must not exist in request or response header fields. See [RFC 9111: HTTP Caching (Section 3: *Storing Responses in Caches*](https://www.rfc-editor.org/rfc/rfc9111#name-storing-responses-in-caches) for details.

Note

The Antiforgery system for generating secure tokens to prevent Cross-Site Request Forgery (CSRF) attacks sets the `Cache-Control` and `Pragma` headers to `no-cache` so that responses aren't cached. For information on how to disable antiforgery tokens for HTML form elements, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0#aspnet-core-antiforgery-configuration).

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/middleware/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [GitHub source for `IResponseCachingPolicyProvider`](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCaching/src/Interfaces/IResponseCachingPolicyProvider.cs)
* [GitHub source for `IResponseCachingPolicyProvider`](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCaching/src/Interfaces/IResponseCachingPolicyProvider.cs)
* [App startup in ASP.NET Core](../../fundamentals/startup?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../../fundamentals/middleware/?view=aspnetcore-10.0)
* [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0)
* [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0)
* [Detect changes with change tokens in ASP.NET Core](../../fundamentals/change-tokens?view=aspnetcore-10.0)
* [Response caching in ASP.NET Core](response?view=aspnetcore-10.0)
* [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0)
* [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0)

This article explains how to configure response caching middleware in an ASP.NET Core app. The middleware determines when responses are cacheable, stores responses, and serves responses from cache. For an introduction to HTTP caching and the [`[ResponseCache]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) attribute, see [Response Caching](response?view=aspnetcore-10.0).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/middleware/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Configuration

Response caching middleware is implicitly available for ASP.NET Core apps via the shared framework.

In `Startup.ConfigureServices`, add the response caching middleware to the service collection:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCaching();
    services.AddRazorPages();
}
```

Configure the app to use the middleware with the [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching) extension method, which adds the middleware to the request processing pipeline in `Startup.Configure`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseStaticFiles();
    app.UseRouting();
    // UseCors must be called before UseResponseCaching
    // app.UseCors("myAllowSpecificOrigins");

    app.UseResponseCaching();

    app.Use(async (context, next) =>
    {
        context.Response.GetTypedHeaders().CacheControl = 
            new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
            {
                Public = true,
                MaxAge = TimeSpan.FromSeconds(10)
            };
        context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] = 
            new string[] { "Accept-Encoding" };

        await next();
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

Warning

[UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) must be called before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching) when using [CORS middleware](../../security/cors?view=aspnetcore-10.0).

The sample app adds headers to control caching on subsequent requests:

* [Cache-Control](https://www.rfc-editor.org/rfc/rfc9111#field.cache-control): Caches cacheable responses for up to 10 seconds.
* [Vary](https://www.rfc-editor.org/rfc/rfc9110#field.vary): Configures the middleware to serve a cached response only if the [Accept-Encoding](https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding) header of subsequent requests matches that of the original request.

```
// using Microsoft.AspNetCore.Http;

app.Use(async (context, next) =>
{
    context.Response.GetTypedHeaders().CacheControl = 
        new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
        {
            Public = true,
            MaxAge = TimeSpan.FromSeconds(10)
        };
    context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] = 
        new string[] { "Accept-Encoding" };

    await next();
});
```

The preceding headers are not written to the response and are overridden when a controller, action, or Razor Page:

* Has a [\[ResponseCache\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) attribute. This applies even if a property isn't set. For example, omitting the [VaryByHeader](response?view=aspnetcore-10.0#vary) property will cause the corresponding header to be removed from the response.

Response caching middleware only caches server responses that result in a 200 (OK) status code. Any other responses, including [error pages](../../fundamentals/error-handling?view=aspnetcore-10.0), are ignored by the middleware.

Warning

Responses containing content for authenticated clients must be marked as not cacheable to prevent the middleware from storing and serving those responses. See [Conditions for caching](#conditions-for-caching) for details on how the middleware determines if a response is cacheable.

## Options

Response caching options are shown in the following table.

| Option                                                                                                                                                                                                         | Description                                                                                                   |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| [MaximumBodySize](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.maximumbodysize#microsoft-aspnetcore-responsecaching-responsecachingoptions-maximumbodysize)                   | The largest cacheable size for the response body in bytes. The default value is `64 * 1024 * 1024` (64 MB).   |
| [SizeLimit](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.sizelimit#microsoft-aspnetcore-responsecaching-responsecachingoptions-sizelimit)                                     | The size limit for the response cache middleware in bytes. The default value is `100 * 1024 * 1024` (100 MB). |
| [UseCaseSensitivePaths](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.usecasesensitivepaths#microsoft-aspnetcore-responsecaching-responsecachingoptions-usecasesensitivepaths) | Determines if responses are cached on case-sensitive paths. The default value is `false`.                     |

The following example configures the middleware to:

* Cache responses with a body size smaller than or equal to 1,024 bytes.
* Store the responses by case-sensitive paths. For example, `/page1` and `/Page1` are stored separately.

```
services.AddResponseCaching(options =>
{
    options.MaximumBodySize = 1024;
    options.UseCaseSensitivePaths = true;
});
```

## VaryByQueryKeys

When using MVC / web API controllers or Razor Pages page models, the [`[ResponseCache]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) attribute specifies the parameters necessary for setting the appropriate headers for response caching. The only parameter of the `[ResponseCache]` attribute that strictly requires the middleware is [VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute.varybyquerykeys#microsoft-aspnetcore-mvc-responsecacheattribute-varybyquerykeys), which doesn't correspond to an actual HTTP header. For more information, see [Response caching in ASP.NET Core](response?view=aspnetcore-10.0#responsecache-attribute).

When not using the `[ResponseCache]` attribute, response caching can be varied with `VaryByQueryKeys`. Use the [ResponseCachingFeature](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingfeature) directly from the [HttpContext.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features#microsoft-aspnetcore-http-httpcontext-features):

```
var responseCachingFeature = context.HttpContext.Features.Get<IResponseCachingFeature>();

if (responseCachingFeature != null)
{
    responseCachingFeature.VaryByQueryKeys = new[] { "MyKey" };
}
```

Using a single value equal to `*` in `VaryByQueryKeys` varies the cache by all request query parameters.

## HTTP headers used by response caching middleware

The following table provides information on HTTP headers that affect response caching.

| Header              | Details                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorization`     | The response isn't cached if the header exists.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `Cache-Control`     | The middleware only considers caching responses marked with the `public` cache directive. Control caching with the following parameters:- max-age
- max-stale†
- min-fresh
- must-revalidate
- no-cache
- no-store
- only-if-cached
- private
- public
- s-maxage
- proxy-revalidate‡†If no limit is specified to `max-stale`, the middleware takes no action. ‡`proxy-revalidate` has the same effect as `must-revalidate`. For more information, see [RFC 9111: Request Directives](https://www.rfc-editor.org/rfc/rfc9111.html#name-request-directives). |
| `Pragma`            | A `Pragma: no-cache` header in the request produces the same effect as `Cache-Control: no-cache`. This header is overridden by the relevant directives in the `Cache-Control` header, if present. Considered for backward compatibility with HTTP/1.0.                                                                                                                                                                                                                                                                                                      |
| `Set-Cookie`        | The response isn't cached if the header exists. Any middleware in the request processing pipeline that sets one or more cookies prevents the response caching middleware from caching the response (for example, the [cookie-based TempData provider](../../fundamentals/app-state?view=aspnetcore-10.0#tempdata)).                                                                                                                                                                                                                                         |
| `Vary`              | The `Vary` header is used to vary the cached response by another header. For example, cache responses by encoding by including the `Vary: Accept-Encoding` header, which caches responses for requests with headers `Accept-Encoding: gzip` and `Accept-Encoding: text/plain` separately. A response with a header value of `*` is never stored.                                                                                                                                                                                                            |
| `Expires`           | A response deemed stale by this header isn't stored or retrieved unless overridden by other `Cache-Control` headers.                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `If-None-Match`     | The full response is served from cache if the value isn't `*` and the `ETag` of the response doesn't match any of the values provided. Otherwise, a 304 (Not Modified) response is served.                                                                                                                                                                                                                                                                                                                                                                  |
| `If-Modified-Since` | If the `If-None-Match` header isn't present, a full response is served from cache if the cached response date is newer than the value provided. Otherwise, a *304 - Not Modified* response is served.                                                                                                                                                                                                                                                                                                                                                       |
| `Date`              | When serving from cache, the `Date` header is set by the middleware if it wasn't provided on the original response.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `Content-Length`    | When serving from cache, the `Content-Length` header is set by the middleware if it wasn't provided on the original response.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `Age`               | The `Age` header sent in the original response is ignored. The middleware computes a new value when serving a cached response.                                                                                                                                                                                                                                                                                                                                                                                                                              |

## Caching respects request Cache-Control directives

The middleware respects the rules of [RFC 9111: HTTP Caching (Section 5.2. Cache-Control)](https://www.rfc-editor.org/rfc/rfc9111#field.cache-control). The rules require a cache to honor a valid `Cache-Control` header sent by the client. Under the specification, a client can make requests with a `no-cache` header value and force the server to generate a new response for every request. Currently, there's no developer control over this caching behavior when using the middleware because the middleware adheres to the official caching specification.

For more control over caching behavior, explore other caching features of ASP.NET Core. See the following topics:

* [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0)
* [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0)
* [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0)
* [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0)

## Troubleshooting

If caching behavior isn't as expected, confirm that responses are cacheable and capable of being served from the cache. Examine the request's incoming headers and the response's outgoing headers. Enable [logging](../../fundamentals/logging/?view=aspnetcore-10.0) to help with debugging.

When testing and troubleshooting caching behavior, a browser may set request headers that affect caching in undesirable ways. For example, a browser may set the `Cache-Control` header to `no-cache` or `max-age=0` when refreshing a page. The following tools can explicitly set request headers and are preferred for testing caching:

* [Fiddler](https://www.telerik.com/fiddler)

### Conditions for caching

* The request must result in a server response with a 200 (OK) status code.
* The request method must be GET or HEAD.
* In `Startup.Configure`, response caching middleware must be placed before middleware that require caching. For more information, see [ASP.NET Core middleware](../../fundamentals/middleware/?view=aspnetcore-10.0).
* The `Authorization` header must not be present.
* `Cache-Control` header parameters must be valid, and the response must be marked `public` and not marked `private`.
* The `Pragma: no-cache` header must not be present if the `Cache-Control` header isn't present, as the `Cache-Control` header overrides the `Pragma` header when present.
* The `Set-Cookie` header must not be present.
* `Vary` header parameters must be valid and not equal to `*`.
* The `Content-Length` header value (if set) must match the size of the response body.
* The [IHttpSendFileFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpsendfilefeature) isn't used.
* The response must not be stale as specified by the `Expires` header and the `max-age` and `s-maxage` cache directives.
* Response buffering must be successful. The size of the response must be smaller than the configured or default [SizeLimit](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.sizelimit#microsoft-aspnetcore-responsecaching-responsecachingoptions-sizelimit). The body size of the response must be smaller than the configured or default [MaximumBodySize](/en-us/dotnet/api/microsoft.aspnetcore.responsecaching.responsecachingoptions.maximumbodysize#microsoft-aspnetcore-responsecaching-responsecachingoptions-maximumbodysize).
* The response must be cacheable according to [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111). For example, the `no-store` directive must not exist in request or response header fields. See [RFC 9111: HTTP Caching (Section 3: *Storing Responses in Caches*](https://www.rfc-editor.org/rfc/rfc9111#name-storing-responses-in-caches) for details.

Note

The Antiforgery system for generating secure tokens to prevent Cross-Site Request Forgery (CSRF) attacks sets the `Cache-Control` and `Pragma` headers to `no-cache` so that responses aren't cached. For information on how to disable antiforgery tokens for HTML form elements, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0#aspnet-core-antiforgery-configuration).

## Additional resources

* [App startup in ASP.NET Core](../../fundamentals/startup?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../../fundamentals/middleware/?view=aspnetcore-10.0)
* [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0)
* [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0)
* [Detect changes with change tokens in ASP.NET Core](../../fundamentals/change-tokens?view=aspnetcore-10.0)
* [Response caching in ASP.NET Core](response?view=aspnetcore-10.0)
* [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0)
* [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/progressive-web-app/push-notifications
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/progressive-web-app/push-notifications.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Push notifications for ASP.NET Core Blazor Progressive Web Applications (PWAs)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Blazor PWAs can receive and display *push notifications* (data messages) from a backend server, even when the user isn't actively using the app. For example, push notifications can be sent when a different user performs an action in their installed PWA or when the app or users interacting directly with the backend server app perform an action.

Use push notifications to:

* Notify users that something important has happened, prompting them to return to the app.
* Update data stored in the app, such as a news feed, so the user has fresh data on their next return to the app, even if they're offline when the push notification is issued.

The mechanisms for sending, receiving, and displaying a push notification are independent of Blazor WebAssembly. Sending a push notification is implemented by the backend server, which can use any technology. Receiving and displaying a push notification on the client is implemented in the service worker JavaScript (JS) file.

The example in this article uses push notifications to provide order status updates to the customers of a pizza restaurant based on the [Blazing Pizza Workshop PWA demonstration app](https://github.com/dotnet-presentations/blazor-workshop). You aren't required to participate in the online workshop to use this article, but the workshop is a helpful introduction to Blazor PWA development.

Note

The Blazing Pizza app adopts the *repository pattern* to create an abstraction layer between the UI layer and the data access layer. For more information, see [Unit of Work (UoW) pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html) and [Designing the infrastructure persistence layer](/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design).

## Establish public and private keys

Generate the cryptographic public and private keys for securing push notifications either locally, for example with PowerShell or IIS, or using an online tool.

Placeholders used in this article's example code:

* `{PUBLIC KEY}`: The public key.
* `{PRIVATE KEY}`: The private key.

For this article's C# examples, update the `someone@example.com` email address to match the address used when creating the custom key pair.

When implementing push notifications, ensure that cryptographic keys are managed securely:

* **Key generation**: Use a trusted library or tool to generate the public and private keys. Avoid using weak or outdated algorithms.
* **Key storage**: Store private keys securely on the server, using a secure storage mechanism such as a hardware security module (HSM) or encrypted storage. Never expose private keys to the client.
* **Key usage**: Use the private key only for signing push notification payloads. Ensure that the public key is distributed securely to clients.

For more information on cryptographic best practices, see [Cryptographic Services](/en-us/dotnet/standard/security/cryptographic-services).

## Create a subscription

Before sending push notifications to a user, the app must ask the user for permission. If they grant permission to receive notifications, their browser generates a *subscription*, which includes a set of tokens the app can use to route notifications to the user.

Permission can be obtained at any time by the app, but we only recommend asking users for permission when it's clear why they would want to subscribe for notifications from the app. The following example asks users when they arrive on the checkout page (`Checkout` component) because at that point it's clear that the user is serious about placing an order.

If the user agrees to receive notifications, the following example sends the push notification subscription data to the server, where push notification tokens are stored in the database for later use.

Add a push notifications JS file to request a subscription:

* Call [`navigator.serviceWorker.getRegistration`](https://developer.mozilla.org/docs/Web/API/ServiceWorkerContainer/getRegistration) to get the service worker's registration.
* Call [`worker.pushManager.getSubscription`](https://developer.mozilla.org/docs/Web/API/PushManager/getSubscription) to determine if a subscription exists.
* If a subscription doesn't exist, create a new subscription using the [`PushManager.subscribe`](https://developer.mozilla.org/docs/Web/API/PushManager/subscribe) function and return the new subscription's URL and tokens.

In the Blazing Pizza app, the JS file is named `pushNotifications.js` and located in the public static assets folder (`wwwroot`) of the solution's Razor class library project (`BlazingPizza.ComponentsLibrary`). The `blazorPushNotifications.requestSubscription` function requests a subscription.

`BlazingPizza.ComponentsLibrary/wwwroot/pushNotifications.js`:

```
(function () {
    const applicationServerPublicKey = '{PUBLIC KEY}';

    window.blazorPushNotifications = {
        requestSubscription: async () => {
            const worker = await navigator.serviceWorker.getRegistration();
            const existingSubscription = await worker.pushManager.getSubscription();
            if (!existingSubscription) {
                const newSubscription = await subscribe(worker);
                if (newSubscription) {
                    return {
                        url: newSubscription.endpoint,
                        p256dh: arrayBufferToBase64(newSubscription.getKey('p256dh')),
                        auth: arrayBufferToBase64(newSubscription.getKey('auth'))
                    };
                }
            }
        }
    };

    async function subscribe(worker) {
        try {
            return await worker.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: applicationServerPublicKey
            });
        } catch (error) {
            if (error.name === 'NotAllowedError') {
                return null;
            }
            throw error;
        }
    }

    function arrayBufferToBase64(buffer) {
        var binary = '';
        var bytes = new Uint8Array(buffer);
        var len = bytes.byteLength;
        for (var i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return window.btoa(binary);
    }
})();
```

Note

For more information on the preceding `arrayBufferToBase64` function, see [How can I convert an ArrayBuffer to a base64-encoded string? (Stack Overflow)](https://stackoverflow.com/a/9458996).

A subscription object and notification subscription endpoint is created on the server. The endpoint receives client web API calls with push notification subscription data, including cryptographic tokens. The data is stored in the database for each app user.

In the Blazing Pizza app, the subscription object is the `NotificationSubscription` class. The `P256dh` and `Auth` properties are the user's cryptographic tokens.

`BlazingPizza.Shared/NotificationSubscription.cs`:

```
public class NotificationSubscription
{
    public int? NotificationSubscriptionId { get; set; }

    public string? UserId { get; set; }

    public string? Url { get; set; }

    public string? P256dh { get; set; }

    public string? Auth { get; set; }
}
```

The `notifications/subscribe` endpoint is defined in the app's `MapPizzaApi` extension method, which is called in the app's `Program` file to set up web API endpoints for the app. The user's notification subscription (`NotificationSubscription`), which includes their push notification tokens, is stored in the database. Only one subscription per user is stored. Alternatively, you could allow the user register multiple subscriptions from different browsers or devices.

```
app.MapPut("/notifications/subscribe", 
    [Authorize] async (
    HttpContext context,
    PizzaStoreContext db,
    NotificationSubscription subscription) => 
    {
        var userId = GetUserId(context);

        if (userId is null)
        {
            return Results.Unauthorized();
        }

        // Remove old subscriptions for this user
        var oldSubscriptions = db.NotificationSubscriptions.Where(
            e => e.UserId == userId);
        db.NotificationSubscriptions.RemoveRange(oldSubscriptions);

        // Store the new subscription
        subscription.UserId = userId;
        db.NotificationSubscriptions.Add(subscription);

        await db.SaveChangesAsync();

        return Results.Ok(subscription);
    });
```

In `BlazingPizza.Client/HttpRepository.cs`, the `SubscribeToNotifications` method issues an HTTP PUT to the subscriptions endpoint on the server:

```
public class HttpRepository : IRepository
{

	private readonly HttpClient _httpClient;

	public HttpRepository(HttpClient httpClient)
	{
		_httpClient = httpClient;
	}

    ...

    public async Task SubscribeToNotifications(NotificationSubscription subscription)
	    {
		    var response = await _httpClient.PutAsJsonAsync("notifications/subscribe", 
                subscription);
		    response.EnsureSuccessStatusCode();
	}
}
```

The repository interface (`BlazingPizza.Shared/IRepository.cs`) includes the method signature of `SubscribeToNotifications`:

```
public interface IRepository
{
    ...

	Task SubscribeToNotifications(NotificationSubscription subscription);
}
```

Define a method to request a subscription and subscribe to notifications if the subscription is established. Save the subscription in the database for later use.

In the `Checkout` component of the Blazing Pizza app, the `RequestNotificationSubscriptionAsync` method performs the following duties:

* The subscription is created via JS interop by calling `blazorPushNotifications.requestSubscription`. The component injects the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) service to invoke the JS function.
* The `SubscribeToNotifications` method is called to save the subscription.

In `BlazingPizza.Client/Components/Pages/Checkout.razor`:

```
async Task RequestNotificationSubscriptionAsync()
{
    var subscription = await JSRuntime.InvokeAsync<NotificationSubscription>(
        "blazorPushNotifications.requestSubscription");

    if (subscription is not null)
    {
        try
        {
            await Repository.SubscribeToNotifications(subscription);
        }
        catch (AccessTokenNotAvailableException ex)
        {
            ex.Redirect();
        }
    }
}
```

In the `Checkout` component, `RequestNotificationSubscriptionAsync` is called in the [`OnInitialized` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) and executes on component initialization. The method is asynchronous, but it can run in the background, and the [Task](/en-us/dotnet/api/system.threading.tasks.task) that it returns can be discarded. Therefore, the method isn't called in the asynchronous lifecycle method for component initialization (`OnInitializedAsync`). This approach renders the component faster.

```
protected override void OnInitialized()
{
    _ = RequestNotificationSubscriptionAsync();
}
```

To demonstrate how the code works, run the Blazing Pizza app and start placing an order. Go to the checkout screen to see the subscription request:

Choose **Allow** and check in the browser developer tools console for errors. You can set a breakpoint in the `PizzaApiExtensions`'s `MapPut("/notifications/subscribe"...)` code and run the app with debugging to inspect the incoming data from the browser. The data includes an endpoint URL and cryptographic tokens.

After the user has either allowed or blocked notifications for a given site, the browser won't ask again. To reset the permission for further testing for either Google Chrome or Microsoft Edge, select the "information" icon (🛈) to the left of the browser's address bar and change **Notifications** back to **Ask (default)**, as seen in the following image:

## Send a notification

Sending a notification involves performing some complex cryptographic operations on the server to protect the data in transit. The bulk of the complexity is handled for the app by a third-party NuGet package, [`WebPush`](https://www.nuget.org/packages/WebPush), which is used by the server project (`BlazingPizza.Server`) in the Blazing Pizza app.

The `SendNotificationAsync` method dispatches order notifications using the captured subscription. The following code makes uses of `WebPush` APIs for dispatching the notification. The payload of the notification is JSON serialized and includes a message and a URL. The message is displayed to the user, and the URL allows the user to reach the pizza order associated with the notification. Additional parameters can be serialized as required for other notification scenarios.

Caution

In the following example, we recommend using a secure approach for supplying the private key. When working locally in the `Development` environment, a private key can be provided to the app using the [Secret Manager](../../security/app-secrets?view=aspnetcore-10.0#secret-manager) tool. In `Development`, `Staging`, and `Production` environments, [Azure Key Vault](/en-us/azure/key-vault/) with [Azure Managed Identities](/en-us/entra/identity/managed-identities-azure-resources/overview) can be used, noting in passing that to obtain a certificate's private key from a key vault that the certificate must have an exportable private key.

```
private static async Task SendNotificationAsync(Order order, 
    NotificationSubscription subscription, string message)
{
    var publicKey = "{PUBLIC KEY}";
    var privateKey = "{PRIVATE KEY}";

    var pushSubscription = new PushSubscription(subscription.Url, 
        subscription.P256dh, subscription.Auth);
    var vapidDetails = new VapidDetails("mailto:<someone@example.com>", publicKey, 
        privateKey);
    var webPushClient = new WebPushClient();

    try
    {
        var payload = JsonSerializer.Serialize(new
        {
            message,
            url = $"myorders/{order.OrderId}",
        });

        await webPushClient.SendNotificationAsync(pushSubscription, payload, 
            vapidDetails);
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine($"Error sending push notification: {ex.Message}");
    }
}
```

The preceding example enables the server to send notifications, but the browser doesn't react to notifications without additional logic. Displaying notifications is covered in the [Display notifications](#display-notifications) section.

The browser's developer tools console indicates the arrival of notifications ten seconds after orders are placed in the Blazing Pizza app. On the **Application** tab, open the **Push Messaging** section. Select the circle to **Start recording**:

## Display notifications

The PWA's service worker (`service-worker.js`) must handle push notifications in order for the app to display them.

The following [`push` event handler](https://developer.mozilla.org/docs/Web/API/PushEvent) in the Blazing Pizza app calls [`showNotification`](https://developer.mozilla.org/docs/Web/API/ServiceWorkerRegistration/showNotification) to create a notification for the active service worker.

In `BlazingPizza/wwwroot/service-worker.js`:

```
self.addEventListener('push', event => {
  const payload = event.data.json();
  event.waitUntil(
    self.registration.showNotification('Blazing Pizza', {
      body: payload.message,
      icon: 'img/icon-512.png',
      vibrate: [100, 50, 100],
      data: { url: payload.url }
    })
  );
});
```

The preceding code doesn't take effect until after the next page load when the browser logs `Installing service worker...`. When struggling to get the service worker to update, use the **Application** tab in the browser's developer tools console. Under **Service Workers**, choose **Update** or use **Unregister** to force a new registration on the next load.

With the preceding code in place and new order placed by a user, the order moves into *Out for delivery* status after 10 seconds per the app's built-in demonstration logic. The browser receives a push notification:

When using the app in either Google Chrome or Microsoft Edge, the notification appears even if the user isn't actively using the Blazing Pizza app. However, the browser must be running, or the notification appears the next time that the browser is opened.

When using the installed PWA, the notification should be delivered even if the user isn't running the app.

## Handle notification clicks

Register a [`notificationclick` event handler](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope/notificationclick_event) to process a user selecting (clicking) a push notification on their device:

* Close the notification by calling [`event.notification.close`](https://developer.mozilla.org/docs/Web/API/Notification/close).
* Call [`clients.openWindow`](https://developer.mozilla.org/docs/Web/API/Clients/openWindow) to create a new top-level browsing context and load the URL passed to the method.

The following example in the Blazing Pizza app takes the user to the order status page for the order that pertains to the notification. The URL is provided by the `event.notification.data.url` parameter, which is sent by the server in the notification's payload.

In the service worker file (`service-worker.js`):

```
self.addEventListener('notificationclick', event => {
  event.notification.close();
  event.waitUntil(clients.openWindow(event.notification.data.url));
});
```

If the PWA is installed on the device, the PWA is shown on the device. If the PWA isn't installed, the user is taken to the app's page in their browser.

## Related content

* [Push API (MDN documentation)](https://developer.mozilla.org/docs/Web/API/Push_API)
* [Message Encryption for Web Push (draft-ietf-webpush-encryption-08)](https://datatracker.ietf.org/doc/html/draft-ietf-webpush-encryption-08)
* [Push API (W3C Working Draft 03 September 2024)](https://www.w3.org/TR/push-api/)
* [Service Worker API (MDN documentation)](https://developer.mozilla.org/docs/Web/API/Service_Worker_API)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/integration-tests.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Integration tests in ASP.NET Core

By [Jos van der Til](https://jvandertil.nl), [Martin Costello](https://martincostello.com/), and [Javier Calvarro Nelson](https://github.com/javiercn).

Integration tests ensure that an app's components function correctly at a level that includes the app's supporting infrastructure, such as the database, file system, and network. ASP.NET Core supports integration tests using a unit test framework with a test web host and an in-memory test server.

This article assumes a basic understanding of unit tests. If unfamiliar with test concepts, see the [Testing in .NET](/en-us/dotnet/core/testing/) article and its linked content.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/10.x/IntegrationTestsSample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. If you're unfamiliar with Razor Pages, see the following articles:

* [Introduction to Razor Pages](../razor-pages/?view=aspnetcore-10.0)
* [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Razor Pages unit tests](razor-pages-tests?view=aspnetcore-10.0)

**For testing SPAs**, we recommend a tool such as [Playwright for .NET](https://playwright.dev/dotnet/), which can automate a browser.

* Sets the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to the SUT's project root so that static files and pages/views are found when the tests are executed.
* Provides the [WebApplicationFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) class to streamline bootstrapping the SUT with `TestServer`.

The [unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test) documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.

**Separate unit tests from integration tests into different projects**. Separating the tests:

* Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
* Allows control over which set of tests are run.

There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, `IndexPageTests` to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, `HomeControllerTests` to test component integration for the Home controller).

## Test app prerequisites

The test project must:

* Reference the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package.
* Specify the Web SDK in the project file (`<Project Sdk="Microsoft.NET.Sdk.Web">`).

These prerequisites can be seen in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/10.x/IntegrationTestsSample). Inspect the `tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj` file. The sample app uses the [xUnit](https://xunit.net/) test framework and the [AngleSharp](https://anglesharp.github.io/) parser library, so the sample app also references:

* [`AngleSharp`](https://www.nuget.org/packages/AngleSharp)
* [`xunit`](https://www.nuget.org/packages/xunit)
* [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio)

In apps that use [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio) version 2.4.2 or later, the test project must reference the [`Microsoft.NET.Test.Sdk`](https://www.nuget.org/packages/Microsoft.NET.Test.Sdk) package.

Entity Framework Core is also used in the tests. See the [project file in GitHub](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/10.x/IntegrationTestsSample/src/RazorPagesProject/RazorPagesProject.csproj).

## SUT environment

If the SUT's [environment](../fundamentals/environments?view=aspnetcore-10.0) isn't set, the environment defaults to `Development`.

## Basic tests with the default WebApplicationFactory

[WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) is used to create a [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) for the integration tests. `TEntryPoint` is the entry point class of the SUT, usually `Program.cs`.

Test classes implement a *class fixture* interface ([`IClassFixture`](https://xunit.net/docs/shared-context#class-fixture)) to indicate the class contains tests and provide shared object instances across the tests in the class.

The following test class, `BasicTests`, uses the `WebApplicationFactory` to bootstrap the SUT and provide an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to a test method, `Get_EndpointsReturnSuccessAndCorrectContentType`. The method verifies the response status code is successful (200-299) and the `Content-Type` header is `text/html; charset=utf-8` for several app pages.

[CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) creates an instance of `HttpClient` that automatically follows redirects and handles cookies.

```
public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}
```

```
[TestClass]
public class BasicTests
{
    private static CustomWebApplicationFactory<Program> _factory;

    [ClassInitialize]
    public static void AssemblyInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
    public static void AssemblyCleanup(TestContext _)
    {
        _factory.Dispose();
    }

    [TestMethod]
    [DataRow("/")]
    [DataRow("/Index")]
    [DataRow("/About")]
    [DataRow("/Privacy")]
    [DataRow("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.AreEqual("text/html; charset=utf-8",
            response.Content.Headers.ContentType.ToString());
    }
}
```

```
public class BasicTests
{
    private CustomWebApplicationFactory<Program>
        _factory;

    [SetUp]
    public void SetUp()
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [TearDown]
    public void TearDown()
    {
        _factory.Dispose();
    }

    [DatapointSource]
    public string[] values = ["/", "/Index", "/About", "/Privacy", "/Contact"];

    [Theory]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.That(response.Content.Headers.ContentType.ToString(), Is.EqualTo("text/html; charset=utf-8"));
    }
}
```

By default, non-essential cookies aren't preserved across requests when the [General Data Protection Regulation consent policy](../security/gdpr?view=aspnetcore-10.0) is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see [Essential cookies](../security/gdpr?view=aspnetcore-10.0#essential-cookies).

[]()

## AngleSharp vs `Application Parts` for antiforgery checks

This article uses the [AngleSharp](https://anglesharp.github.io/) parser to handle the antiforgery checks by loading pages and parsing the HTML. For testing the endpoints of controller and Razor Pages views at a lower-level, without caring about how they render in the browser, consider using `Application Parts`. The [Application Parts](../mvc/advanced/app-parts?view=aspnetcore-10.0) approach injects a controller or Razor Page into the app that can be used to make JSON requests to get the required values. For more information, see the blog [Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts](https://blog.martincostello.com/integration-testing-antiforgery-with-application-parts/) and [associated GitHub repo](https://github.com/martincostello/antiforgery-testing-application-part) by [Martin Costello](https://github.com/martincostello).

## Customize WebApplicationFactory

Web host configuration can be created independently of the test classes by inheriting from [WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) to create one or more custom factories:

1. Inherit from `WebApplicationFactory` and override [ConfigureWebHost](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.configurewebhost). The [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) allows the configuration of the service collection with [`IWebHostBuilder.ConfigureServices`](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder.configureservices)

   ```
   public class CustomWebApplicationFactory<TProgram>
       : WebApplicationFactory<TProgram> where TProgram : class
   {
       protected override void ConfigureWebHost(IWebHostBuilder builder)
       {
           builder.ConfigureServices(services =>
           {
               var dbContextDescriptor = services.SingleOrDefault(
                   d => d.ServiceType == 
                       typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

               services.Remove(dbContextDescriptor);

               var dbConnectionDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbConnection));

               services.Remove(dbConnectionDescriptor);

               // Create open SqliteConnection so EF won't automatically close it.
               services.AddSingleton<DbConnection>(container =>
               {
                   var connection = new SqliteConnection("DataSource=:memory:");
                   connection.Open();

                   return connection;
               });

               services.AddDbContext<ApplicationDbContext>((container, options) =>
               {
                   var connection = container.GetRequiredService<DbConnection>();
                   options.UseSqlite(connection);
               });
           });

           builder.UseEnvironment("Development");
       }
   }
   ```

   ```
   public class CustomWebApplicationFactory<TProgram>
       : WebApplicationFactory<TProgram> where TProgram : class
   {
       protected override void ConfigureWebHost(IWebHostBuilder builder)
       {
           builder.ConfigureServices(services =>
           {
               var dbContextDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

               services.Remove(dbContextDescriptor);

               var dbConnectionDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbConnection));

               services.Remove(dbConnectionDescriptor);

               // Create open SqliteConnection so EF won't automatically close it.
               services.AddSingleton<DbConnection>(container =>
               {
                   var connection = new SqliteConnection("DataSource=:memory:");
                   connection.Open();

                   return connection;
               });

               services.AddDbContext<ApplicationDbContext>((container, options) =>
               {
                   var connection = container.GetRequiredService<DbConnection>();
                   options.UseSqlite(connection);
               });
           });

           builder.UseEnvironment("Development");
       }
   }
   ```

   ```
   public class CustomWebApplicationFactory<TProgram>
       : WebApplicationFactory<TProgram> where TProgram : class
   {
       protected override void ConfigureWebHost(IWebHostBuilder builder)
       {
           builder.ConfigureServices(services =>
           {
               var dbContextDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

               services.Remove(dbContextDescriptor);

               var dbConnectionDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbConnection));

               services.Remove(dbConnectionDescriptor);

               // Create open SqliteConnection so EF won't automatically close it.
               services.AddSingleton<DbConnection>(container =>
               {
                   var connection = new SqliteConnection("DataSource=:memory:");
                   connection.Open();

                   return connection;
               });

               services.AddDbContext<ApplicationDbContext>((container, options) =>
               {
                   var connection = container.GetRequiredService<DbConnection>();
                   options.UseSqlite(connection);
               });
           });

           builder.UseEnvironment("Development");
       }
   }
   ```

   Database seeding in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/10.x/IntegrationTestsSample) is performed by the `InitializeDbForTests` method. The method is described in the [Integration tests sample: Test app organization](#test-app-organization) section.

   The SUT's database context is registered in `Program.cs`. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Program.cs` code is executed. To use a different database for the tests than the app's database, the app's database context must be replaced in `builder.ConfigureServices`.

   The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. The factory then adds a new `ApplicationDbContext` that uses an in-memory database for the tests..

   To connect to a different database, change the `DbConnection`. To use a SQL Server test database:

   * Reference the [`Microsoft.EntityFrameworkCore.SqlServer`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer/) NuGet package in the project file.
   * Call `UseInMemoryDatabase`.

2) Use the custom `CustomWebApplicationFactory` in test classes. The following example uses the factory in the `IndexPageTests` class:

   ```
   public class IndexPageTests :
       IClassFixture<CustomWebApplicationFactory<Program>>
   {
       private readonly HttpClient _client;
       private readonly CustomWebApplicationFactory<Program>
           _factory;

       public IndexPageTests(
           CustomWebApplicationFactory<Program> factory)
       {
           _factory = factory;
           _client = factory.CreateClient(new WebApplicationFactoryClientOptions
           {
               AllowAutoRedirect = false
           });
       }
   ```

   ```
   [TestClass]
   public class IndexPageTests
   {
       private static HttpClient _client;
       private static CustomWebApplicationFactory<Program>
           _factory;

       [ClassInitialize]
       public static void AssemblyInitialize(TestContext _)
       {
           _factory = new CustomWebApplicationFactory<Program>();
           _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
           {
               AllowAutoRedirect = false
           });
       }

       [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
       public static void AssemblyCleanup(TestContext _)
       {
           _factory.Dispose();
       }
   ```

   ```
   public class IndexPageTests
   {

       private HttpClient _client;
       private CustomWebApplicationFactory<Program>
           _factory;

       [SetUp]
       public void SetUp()
       {
           _factory = new CustomWebApplicationFactory<Program>();
           _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
           {
               AllowAutoRedirect = false
           });
       }

       [TearDown]
       public void TearDown()
       {
           _factory.Dispose();
           _client.Dispose();
       }
   ```

   The sample app's client is configured to prevent the `HttpClient` from following redirects. As explained later in the [Mock authentication](#mock-authentication) section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a `Location` header.

3) A typical test uses the `HttpClient` and helper methods to process the request and the response:

   ```
   [Fact]
   public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
   {
       // Arrange
       var defaultPage = await _client.GetAsync("/");
       var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

       //Act
       var response = await _client.SendAsync(
           (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
           (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

       // Assert
       Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
       Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
       Assert.Equal("/", response.Headers.Location.OriginalString);
   }
   ```

   ```
   [TestMethod]
   public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
   {
       // Arrange
       var defaultPage = await _client.GetAsync("/");
       var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

       //Act
       var response = await _client.SendAsync(
           (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
           (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

       // Assert
       Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
       Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
       Assert.AreEqual("/", response.Headers.Location.OriginalString);
   }
   ```

   ```
   [Test]
   public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
   {
       // Arrange
       var defaultPage = await _client.GetAsync("/");
       var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

       //Act
       var response = await _client.SendAsync(
           (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
           (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

       // Assert
       Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
       Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
       Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
   }
   ```

Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's [data protection antiforgery system](../security/data-protection/introduction?view=aspnetcore-10.0). In order to arrange for a test's POST request, the test app must:

1. Make a request for the page.
2. Parse the antiforgery cookie and request validation token from the response.
3. Make the POST request with the antiforgery cookie and request validation token in place.

The `SendAsync` helper extension methods (`Helpers/HttpClientExtensions.cs`) and the `GetDocumentAsync` helper method (`Helpers/HtmlHelpers.cs`) in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/10.x/IntegrationTestsSample/) use the [AngleSharp](https://anglesharp.github.io/) parser to handle the antiforgery check with the following methods:

* `GetDocumentAsync`: Receives the [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage) and returns an `IHtmlDocument`. `GetDocumentAsync` uses a factory that prepares a *virtual response* based on the original `HttpResponseMessage`. For more information, see the [AngleSharp documentation](https://github.com/AngleSharp/AngleSharp#documentation).

* `SendAsync` extension methods for the `HttpClient` compose an [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) and call [SendAsync(HttpRequestMessage)](/en-us/dotnet/api/system.net.http.httpclient.sendasync#system-net-http-httpclient-sendasync\(system-net-http-httprequestmessage\)) to submit requests to the SUT. Overloads for `SendAsync` accept the HTML form (`IHtmlFormElement`) and the following:

  * Submit button of the form (`IHtmlElement`)
  * Form values collection (`IEnumerable<KeyValuePair<string, string>>`)
  * Submit button (`IHtmlElement`) and form values (`IEnumerable<KeyValuePair<string, string>>`)

[AngleSharp](https://anglesharp.github.io/) is a **third-party parsing library used for demonstration purposes** in this article and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the [Html Agility Pack (HAP)](https://html-agility-pack.net/). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly. See [AngleSharp vs `Application Parts` for antiforgery checks](#asap7) in this article for more information.

The [EF-Core in-memory database provider](/en-us/ef/core/testing/choosing-a-testing-strategy#in-memory-as-a-database-fake) can be used for limited and basic testing, however the ***[SQLite provider](/en-us/ef/core/testing/choosing-a-testing-strategy#sqlite-as-a-database-fake) is the recommended choice for in-memory testing***.

When a test requires a custom middleware, you can use an [IStartupFilter](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter). For more information, see [App startup in ASP.NET Core](../fundamentals/startup?view=aspnetcore-10.0#startup-filters).

When a test requires a custom service, see the [Inject mock services](#inject-mock-services) section.

## Customize the `WebApplicationFactory` with test configurations

There are various ways to supply test-specific configurations to the running web application via `WebApplicationFactory`.

1. Using `ConfigureHostConfiguration`: override the `CreateHost` method and call `builder.ConfigureHostConfiguration` as follows:

   ```
   private static readonly KeyValuePair<string, string?>[] s_inMemorySettings = new KeyValuePair<string, string?>[]
   {
       new("TestConfigKey", "TestConfigValue"),
   };

   protected override IHost CreateHost(IHostBuilder builder)
   {
       builder.ConfigureHostConfiguration(builder =>
       {
           builder.AddInMemoryCollection(s_inMemorySettings);
       });
       return base.CreateHost(builder);
   }
   ```

   Using this approach, the configuration source added via `ConfigureHostConfiguration` will be enumerated before the entry point is called, and the configurations will be passed to the entry point via `args`. The actual configuration source isn't preserved.

   Important

   Make sure you pass `args` to `WebApplication.CreateBuilder` call. Otherwise, the configurations will not take effect.

2. Using `ConfigureAppConfiguration`: override the `ConfigureWebHost` method and call `builder.ConfigureAppConfiguration` as follows:

   ```
   private static readonly KeyValuePair<string, string?>[] s_inMemorySettings = new KeyValuePair<string, string?>[]
   {
       new("TestConfigKey", "TestConfigValue"),
   };

   protected override void ConfigureWebHost(IWebHostBuilder builder)
   {
       base.ConfigureWebHost(builder);
       builder.ConfigureAppConfiguration(builder =>
       {
           builder.AddInMemoryCollection(s_inMemorySettings);
       });
   }
   ```

   Using this approach, the configuration source added via `ConfigureAppConfiguration` is preserved, but it's only available after the web application is built. If you try to access the configuration before `WebApplicationBuilder.Build` is called, the configurations will not be available.

3. Starting in .NET 11 Preview 7, you can override a new `ConfigureWebApplicationBuilder` method as follows:

   ```
   private static readonly KeyValuePair<string, string?>[] s_inMemorySettings = new KeyValuePair<string, string?>[]
   {
       new("TestConfigKey", "TestConfigValue"),
   };

   protected override void ConfigureWebApplicationBuilder(IHostApplicationBuilder hostApplicationBuilder)
   {
       hostApplicationBuilder.Configuration.AddInMemoryCollection(s_inMemorySettings);
       base.ConfigureWebApplicationBuilder(hostApplicationBuilder);
   }
   ```

   Using this approach, the configuration source added via `ConfigureWebApplicationBuilder` is preserved, and is also available immediately after `WebApplication.CreateBuilder` returns.

## Customize the client with WithWebHostBuilder

When additional configuration is required within a test method, [WithWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.withwebhostbuilder) creates a new `WebApplicationFactory` with an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) that is further customized by configuration.

The [sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/10.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests) calls `WithWebHostBuilder` to replace configured services with test stubs. For more information and example usage, see [Inject mock services](#inject-mock-services) in this article.

The `Post_DeleteMessageHandler_ReturnsRedirectToRoot` test method of the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/10.x/IntegrationTestsSample) demonstrates the use of `WithWebHostBuilder`. This test performs a record delete in the database by triggering a form submission in the SUT.

Because another test in the `IndexPageTests` class performs an operation that deletes all of the records in the database and may run before the `Post_DeleteMessageHandler_ReturnsRedirectToRoot` method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the `messages` form in the SUT is simulated in the request to the SUT:

```
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}
```

```
[TestMethod]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    Assert.AreEqual("/", response.Headers.Location.OriginalString);
}
```

```
[Test]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
    Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
}
```

## Client options

See the [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) page for defaults and available options when creating `HttpClient` instances.

Create the `WebApplicationFactoryClientOptions` class and pass it to the [CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) method:

```
public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }
```

```
[TestClass]
public class IndexPageTests
{
    private static HttpClient _client;
    private static CustomWebApplicationFactory<Program>
        _factory;

    [ClassInitialize]
    public static void AssemblyInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

    [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
    public static void AssemblyCleanup(TestContext _)
    {
        _factory.Dispose();
    }
```

```
public class IndexPageTests
{

    private HttpClient _client;
    private CustomWebApplicationFactory<Program>
        _factory;

    [SetUp]
    public void SetUp()
    {
        _factory = new CustomWebApplicationFactory<Program>();
        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

    [TearDown]
    public void TearDown()
    {
        _factory.Dispose();
        _client.Dispose();
    }
```

***NOTE:*** To avoid HTTPS redirection warnings in logs when using HTTPS redirection middleware, set `BaseAddress = new Uri("https://localhost")`

## Inject mock services

Services can be overridden in a test with a call to [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) on the host builder. To scope the overridden services to the test itself, the [WithWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.withwebhostbuilder) method is used to retrieve a host builder. This can be seen in the following tests:

* [Get\_QuoteService\_ProvidesQuoteInPage](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/10.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs#L166-L173)
* [Get\_GithubProfilePageCanGetAGithubUser](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/10.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs#L35-L38)
* [Get\_SecurePageIsReturnedForAnAuthenticatedUser](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/10.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs#L105-L117)

The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.

`Services/IQuoteService.cs`:

```
public interface IQuoteService
{
    Task<string> GenerateQuote();
}
```

`Services/QuoteService.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}
```

`Program.cs`:

```
services.AddScoped<IQuoteService, QuoteService>();
```

`Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }
```

`Pages/Index.cs`:

```
<input id="quote" type="hidden" value="@Model.Quote">
```

The following markup is generated when the SUT app is run:

```
<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">
```

To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's `QuoteService` with a service provided by the test app, called `TestQuoteService`:

`IntegrationTests.IndexPageTests.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
```

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
```

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
```

`ConfigureTestServices` is called, and the scoped service is registered:

```
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
```

```
[TestMethod]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.AreEqual("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
```

```
[Test]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.That(quoteElement.Attributes["value"].Value, Is.EqualTo(
        "Something's interfering with time, Mr. Scarman, " +
        "and time is my business."));
}
```

The markup produced during the test's execution reflects the quote text supplied by `TestQuoteService`, thus the assertion passes:

```
<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">
```

## Mock authentication

Tests in the `AuthTests` class check that a secure endpoint:

* Redirects an unauthenticated user to the app's sign in page.
* Returns content for an authenticated user.

In the SUT, the `/SecurePage` page uses an [AuthorizePage](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizepage) convention to apply an [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) to the page. For more information, see [Razor Pages authorization conventions](../razor-pages/security/authorization/conventions?view=aspnetcore-10.0#require-authorization-to-access-a-page).

```
services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});
```

In the `Get_SecurePageRedirectsAnUnauthenticatedUser` test, a [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) is set to disallow redirects by setting [AllowAutoRedirect](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions.allowautoredirect#microsoft-aspnetcore-mvc-testing-webapplicationfactoryclientoptions-allowautoredirect) to `false`:

```
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}
```

```
[TestMethod]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    StringAssert.StartsWith(response.Headers.Location.OriginalString, "http://localhost/Identity/Account/Login");
}
```

```
[Test]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
    Assert.That(response.Headers.Location.OriginalString, Does.StartWith("http://localhost/Identity/Account/Login"));
}
```

By disallowing the client to follow the redirect, the following checks can be made:

* The status code returned by the SUT can be checked against the expected [HttpStatusCode.Redirect](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-redirect) result, not the final status code after the redirect to the sign in page, which would be [HttpStatusCode.OK](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-ok).
* The `Location` header value in the response headers is checked to confirm that it starts with `http://localhost/Identity/Account/Login`, not the final sign in page response, where the `Location` header wouldn't be present.

The test app can mock an [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) in [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) in order to test aspects of authentication and authorization. A minimal scenario returns an [AuthenticateResult.Success](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticateresult.success):

```
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
```

```
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
```

```
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
```

The `TestAuthHandler` is called to authenticate a user when the authentication scheme is set to `TestScheme` where `AddAuthentication` is registered for `ConfigureTestServices`. It's important for the `TestScheme` scheme to match the scheme your app expects. Otherwise, authentication won't work.

```
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(options =>
                    {
                        options.DefaultAuthenticateScheme = "TestScheme";
                        options.DefaultChallengeScheme = "TestScheme";
                    })
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
```

```
[TestMethod]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = "TestScheme";
                    options.DefaultChallengeScheme = "TestScheme";
                })
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "TestScheme", options => { });
        });
    })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
```

```
[Test]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = "TestScheme";
                    options.DefaultChallengeScheme = "TestScheme";
                })
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "TestScheme", options => { });
        });
    })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
```

For more information on `WebApplicationFactoryClientOptions`, see the [Client options](#client-options) section.

### Basic tests for authentication middleware

See [this GitHub repository](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test) for basic tests of authentication middleware. It contains a [test server](https://github.com/blowdart/idunno.Authentication/blob/dev/test/idunno.Authentication.Basic.Test/BasicAuthenticationTests.cs#L331) that's specific to the test scenario.

## Set the environment

Set the [environment](../fundamentals/environments?view=aspnetcore-10.0) in the custom application factory:

```
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType == 
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
```

```
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
```

```
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
```

## How the test infrastructure infers the app content root path

The `WebApplicationFactory` constructor infers the app [content root](../fundamentals/?view=aspnetcore-10.0#content-root) path by searching for a [WebApplicationFactoryContentRootAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactorycontentrootattribute) on the assembly containing the integration tests with a key equal to the `TEntryPoint` assembly `System.Reflection.Assembly.FullName`. In case an attribute with the correct key isn't found, `WebApplicationFactory` falls back to searching for a solution file (*.sln*) and appends the `TEntryPoint` assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.

## Disable shadow copying

Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to `Assembly.Location` and you encounter issues, you might have to disable shadow copying.

To disable shadow copying when using xUnit, create a `xunit.runner.json` file in your test project directory, with the [correct configuration setting](https://xunit.net/docs/configuration-files#shadowCopy):

```
{
  "shadowCopy": false
}
```

## Disposal of objects

After the tests of the `IClassFixture` implementation are executed, [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are disposed when xUnit disposes of the [`WebApplicationFactory`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1). If objects instantiated by the developer require disposal, dispose of them in the `IClassFixture` implementation. For more information, see [Implementing a Dispose method](/en-us/dotnet/standard/garbage-collection/implementing-dispose).

After the tests of the `TestClass` are executed, [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are disposed when MSTest disposes of the [`WebApplicationFactory`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) in the `ClassCleanup` method. If objects instantiated by the developer require disposal, dispose of them in the `ClassCleanup` method. For more information, see [Implementing a Dispose method](/en-us/dotnet/standard/garbage-collection/implementing-dispose).

After the tests of the test class are executed, [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are disposed when NUnit disposes of the [`WebApplicationFactory`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) in the `TearDown` method. If objects instantiated by the developer require disposal, dispose of them in the `TearDown` method. For more information, see [Implementing a Dispose method](/en-us/dotnet/standard/garbage-collection/implementing-dispose).

## Integration tests sample

The [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/10.x/IntegrationTestsSample) is composed of two apps:

| App                   | Project directory               | Description                                                         |
| --------------------- | ------------------------------- | ------------------------------------------------------------------- |
| Message app (the SUT) | `src/RazorPagesProject`         | Allows a user to add, delete one, delete all, and analyze messages. |
| Test app              | `tests/RazorPagesProject.Tests` | Used to integration test the SUT.                                   |

The tests can be run using the built-in test features of an IDE, such as [Visual Studio](https://visualstudio.microsoft.com). If using [Visual Studio Code](https://code.visualstudio.com/) or the command line, execute the following command at a command prompt in the `tests/RazorPagesProject.Tests` directory:

```
dotnet test
```

### Message app (SUT) organization

The SUT is a Razor Pages message system with the following characteristics:

* The Index page of the app (`Pages/Index.cshtml` and `Pages/Index.cshtml.cs`) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
* A message is described by the `Message` class (`Data/Message.cs`) with two properties: `Id` (key) and `Text` (message). The `Text` property is required and limited to 200 characters.
* Messages are stored using [Entity Framework's in-memory database](/en-us/ef/core/providers/in-memory/)†.
* The app contains a data access layer (DAL) in its database context class, `AppDbContext` (`Data/AppDbContext.cs`).
* If the database is empty on app startup, the message store is initialized with three messages.
* The app includes a `/SecurePage` that can only be accessed by an authenticated user.

†The EF article, [Test with InMemory](/en-us/ef/core/miscellaneous/testing/in-memory), explains how to use an in-memory database for tests with MSTest. This topic uses the [xUnit](https://xunit.net/) test framework. Test concepts and test implementations across different test frameworks are similar but not identical.

Although the app doesn't use the repository pattern and isn't an effective example of the [Unit of Work (UoW) pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html), Razor Pages supports these patterns of development. For more information, see [Designing the infrastructure persistence layer](/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) and [Test controller logic](../mvc/controllers/testing?view=aspnetcore-10.0) (the sample implements the repository pattern).

### Test app organization

The test app is a console app inside the `tests/RazorPagesProject.Tests` directory.

| Test app directory  | Description                                                                                                                                                                                                                                                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AuthTests`         | Contains test methods for:- Accessing a secure page by an unauthenticated user.
- Accessing a secure page by an authenticated user with a mock [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1).
- Obtaining a GitHub user profile and checking the profile's user login. |
| `BasicTests`        | Contains a test method for routing and content type.                                                                                                                                                                                                                                                                                       |
| `IntegrationTests`  | Contains the integration tests for the Index page using custom `WebApplicationFactory` class.                                                                                                                                                                                                                                              |
| `Helpers/Utilities` | * `Utilities.cs` contains the `InitializeDbForTests` method used to seed the database with test data.
* `HtmlHelpers.cs` provides a method to return an AngleSharp `IHtmlDocument` for use by the test methods.
* `HttpClientExtensions.cs` provide overloads for `SendAsync` to submit requests to the SUT.                               |

The test framework is [xUnit](https://xunit.net/). Integration tests are conducted using the [Microsoft.AspNetCore.TestHost](/en-us/dotnet/api/microsoft.aspnetcore.testhost), which includes the [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver). Because the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package is used to configure the test host and test server, the `TestHost` and `TestServer` packages don't require direct package references in the test app's project file or developer configuration in the test app.

Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.

The sample app seeds the database with three messages in `Utilities.cs` that tests can use when they execute:

```
public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}
```

The SUT's database context is registered in `Program.cs`. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Program.cs` code is executed. To use a different database for the tests, the app's database context must be replaced in `builder.ConfigureServices`. For more information, see the [Customize WebApplicationFactory](#customize-webapplicationfactory) section.

## Related content

* [Unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test)
* [Razor Pages unit tests in ASP.NET Core](razor-pages-tests?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0)
* [Test controller logic in ASP.NET Core](../mvc/controllers/testing?view=aspnetcore-10.0)
* [Basic tests for authentication middleware](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test)

This topic assumes a basic understanding of unit tests. If unfamiliar with test concepts, see the [Unit Testing in .NET Core and .NET Standard](/en-us/dotnet/core/testing/) topic and its linked content.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/test/integration-tests/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. If unfamiliar with Razor Pages, see the following topics:

* [Introduction to Razor Pages](../razor-pages/?view=aspnetcore-10.0)
* [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Razor Pages unit tests](razor-pages-tests?view=aspnetcore-10.0)

Note

For testing SPAs, we recommend a tool such as [Playwright for .NET](https://playwright.dev/dotnet/), which can automate a browser.

* Sets the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to the SUT's project root so that static files and pages/views are found when the tests are executed.
* Provides the [WebApplicationFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) class to streamline bootstrapping the SUT with `TestServer`.

The [unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test) documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.

**Separate unit tests from integration tests into different projects**. Separating the tests:

* Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
* Allows control over which set of tests are run.

There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, `IndexPageTests` to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, `HomeControllerTests` to test component integration for the Home controller).

## Test app prerequisites

The test project must:

* Reference the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package.
* Specify the Web SDK in the project file (`<Project Sdk="Microsoft.NET.Sdk.Web">`).

These prerequisites can be seen in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/test/integration-tests/samples/). Inspect the `tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj` file. The sample app uses the [xUnit](https://xunit.net/) test framework and the [AngleSharp](https://anglesharp.github.io/) parser library, so the sample app also references:

* [`AngleSharp`](https://www.nuget.org/packages/AngleSharp)
* [`xunit`](https://www.nuget.org/packages/xunit)
* [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio)

In apps that use [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio) version 2.4.2 or later, the test project must reference the [`Microsoft.NET.Test.Sdk`](https://www.nuget.org/packages/Microsoft.NET.Test.Sdk) package.

Entity Framework Core is also used in the tests. The app references:

* [`Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore)
* [`Microsoft.AspNetCore.Identity.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.EntityFrameworkCore)
* [`Microsoft.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore)
* [`Microsoft.EntityFrameworkCore.InMemory`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory)
* [`Microsoft.EntityFrameworkCore.Tools`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Tools)

## SUT environment

If the SUT's [environment](../fundamentals/environments?view=aspnetcore-10.0) isn't set, the environment defaults to `Development`.

## Basic tests with the default WebApplicationFactory

[WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) is used to create a [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) for the integration tests. `TEntryPoint` is the entry point class of the SUT, usually the `Startup` class.

Test classes implement a *class fixture* interface ([`IClassFixture`](https://xunit.net/docs/shared-context#class-fixture)) to indicate the class contains tests and provide shared object instances across the tests in the class.

The following test class, `BasicTests`, uses the `WebApplicationFactory` to bootstrap the SUT and provide an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to a test method, `Get_EndpointsReturnSuccessAndCorrectContentType`. The method checks if the response status code is successful (status codes in the range 200-299) and the `Content-Type` header is `text/html; charset=utf-8` for several app pages.

[CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) creates an instance of `HttpClient` that automatically follows redirects and handles cookies.

```
public class BasicTests 
    : IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
    private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;

    public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}
```

By default, non-essential cookies aren't preserved across requests when the [GDPR consent policy](../security/gdpr?view=aspnetcore-10.0) is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see [Essential cookies](../security/gdpr?view=aspnetcore-10.0#essential-cookies).

## Customize WebApplicationFactory

Web host configuration can be created independently of the test classes by inheriting from `WebApplicationFactory` to create one or more custom factories:

1. Inherit from `WebApplicationFactory` and override [ConfigureWebHost](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.configurewebhost). The [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) allows the configuration of the service collection with [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartup.configureservices):

   ```
   public class CustomWebApplicationFactory<TStartup>
       : WebApplicationFactory<TStartup> where TStartup: class
   {
       protected override void ConfigureWebHost(IWebHostBuilder builder)
       {
           builder.ConfigureServices(services =>
           {
               var descriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbContextOptions<ApplicationDbContext>));

               services.Remove(descriptor);

               services.AddDbContext<ApplicationDbContext>(options =>
               {
                   options.UseInMemoryDatabase("InMemoryDbForTesting");
               });

               var sp = services.BuildServiceProvider();

               using (var scope = sp.CreateScope())
               {
                   var scopedServices = scope.ServiceProvider;
                   var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                   var logger = scopedServices
                       .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();

                   db.Database.EnsureCreated();

                   try
                   {
                       Utilities.InitializeDbForTests(db);
                   }
                   catch (Exception ex)
                   {
                       logger.LogError(ex, "An error occurred seeding the " +
                           "database with test messages. Error: {Message}", ex.Message);
                   }
               }
           });
       }
   }
   ```

   Database seeding in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/test/integration-tests/samples) is performed by the `InitializeDbForTests` method. The method is described in the [Integration tests sample: Test app organization](#test-app-organization) section.

   The SUT's database context is registered in its `Startup.ConfigureServices` method. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Startup.ConfigureServices` code is executed. The execution order is a breaking change for the [Generic Host](../fundamentals/host/generic-host?view=aspnetcore-10.0) with the release of ASP.NET Core 3.0. To use a different database for the tests than the app's database, the app's database context must be replaced in `builder.ConfigureServices`.

   For SUTs that still use the [Web Host](../fundamentals/host/web-host?view=aspnetcore-10.0), the test app's `builder.ConfigureServices` callback is executed *before* the SUT's `Startup.ConfigureServices` code. The test app's `builder.ConfigureTestServices` callback is executed *after*.

   The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. Next, the factory adds a new `ApplicationDbContext` that uses an in-memory database for the tests.

   To connect to a different database than the in-memory database, change the `UseInMemoryDatabase` call to connect the context to a different database. To use a SQL Server test database:

   * Reference the [`Microsoft.EntityFrameworkCore.SqlServer`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer/) NuGet package in the project file.
   * Call `UseSqlServer` with a connection string to the database.

   ```
   services.AddDbContext<ApplicationDbContext>((options, context) => 
   {
       context.UseSqlServer(
           Configuration.GetConnectionString("TestingDbConnectionString"));
   });
   ```

2. Use the custom `CustomWebApplicationFactory` in test classes. The following example uses the factory in the `IndexPageTests` class:

   ```
   public class IndexPageTests : 
       IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
   {
       private readonly HttpClient _client;
       private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> 
           _factory;

       public IndexPageTests(
           CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
       {
           _factory = factory;
           _client = factory.CreateClient(new WebApplicationFactoryClientOptions
               {
                   AllowAutoRedirect = false
               });
       }
   ```

   The sample app's client is configured to prevent the `HttpClient` from following redirects. As explained later in the [Mock authentication](#mock-authentication) section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a `Location` header.

3. A typical test uses the `HttpClient` and helper methods to process the request and the response:

   ```
   [Fact]
   public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
   {
       // Arrange
       var defaultPage = await _client.GetAsync("/");
       var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

       //Act
       var response = await _client.SendAsync(
           (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
           (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

       // Assert
       Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
       Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
       Assert.Equal("/", response.Headers.Location.OriginalString);
   }
   ```

Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's [data protection antiforgery system](../security/data-protection/introduction?view=aspnetcore-10.0). In order to arrange for a test's POST request, the test app must:

1. Make a request for the page.
2. Parse the antiforgery cookie and request validation token from the response.
3. Make the POST request with the antiforgery cookie and request validation token in place.

The `SendAsync` helper extension methods (`Helpers/HttpClientExtensions.cs`) and the `GetDocumentAsync` helper method (`Helpers/HtmlHelpers.cs`) in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/test/integration-tests/samples/) use the [AngleSharp](https://anglesharp.github.io/) parser to handle the antiforgery check with the following methods:

* `GetDocumentAsync`: Receives the [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage) and returns an `IHtmlDocument`. `GetDocumentAsync` uses a factory that prepares a *virtual response* based on the original `HttpResponseMessage`. For more information, see the [AngleSharp documentation](https://github.com/AngleSharp/AngleSharp#documentation).

* `SendAsync` extension methods for the `HttpClient` compose an [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) and call [SendAsync(HttpRequestMessage)](/en-us/dotnet/api/system.net.http.httpclient.sendasync#system-net-http-httpclient-sendasync\(system-net-http-httprequestmessage\)) to submit requests to the SUT. Overloads for `SendAsync` accept the HTML form (`IHtmlFormElement`) and the following:

  * Submit button of the form (`IHtmlElement`)
  * Form values collection (`IEnumerable<KeyValuePair<string, string>>`)
  * Submit button (`IHtmlElement`) and form values (`IEnumerable<KeyValuePair<string, string>>`)

Note

[AngleSharp](https://anglesharp.github.io/) is a third-party parsing library used for demonstration purposes in this topic and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the [Html Agility Pack (HAP)](https://html-agility-pack.net/). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly.

Note

The [EF-Core in-memory database provider](/en-us/ef/core/testing/choosing-a-testing-strategy#in-memory-as-a-database-fake) can be used for limited and basic testing, however the [SQLite provider](/en-us/ef/core/testing/choosing-a-testing-strategy#sqlite-as-a-database-fake) is the recommended choice for in-memory testing.

## Customize the client with WithWebHostBuilder

When additional configuration is required within a test method, [WithWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.withwebhostbuilder) creates a new `WebApplicationFactory` with an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) that is further customized by configuration.

The `Post_DeleteMessageHandler_ReturnsRedirectToRoot` test method of the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/test/integration-tests/samples) demonstrates the use of `WithWebHostBuilder`. This test performs a record delete in the database by triggering a form submission in the SUT.

Because another test in the `IndexPageTests` class performs an operation that deletes all of the records in the database and may run before the `Post_DeleteMessageHandler_ReturnsRedirectToRoot` method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the `messages` form in the SUT is simulated in the request to the SUT:

```
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                var serviceProvider = services.BuildServiceProvider();

                using (var scope = serviceProvider.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices
                        .GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<IndexPageTests>>();

                    try
                    {
                        Utilities.ReinitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding " +
                            "the database with test messages. Error: {Message}", 
                            ex.Message);
                    }
                }
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}
```

## Client options

The following table shows the default [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) available when creating `HttpClient` instances.

| Option                                                                                                                                                                                                                                  | Description                                                                                        | Default            |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------ |
| [AllowAutoRedirect](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions.allowautoredirect#microsoft-aspnetcore-mvc-testing-webapplicationfactoryclientoptions-allowautoredirect)                      | Gets or sets whether or not `HttpClient` instances should automatically follow redirect responses. | `true`             |
| [BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions.baseaddress#microsoft-aspnetcore-mvc-testing-webapplicationfactoryclientoptions-baseaddress)                                        | Gets or sets the base address of `HttpClient` instances.                                           | `http://localhost` |
| [HandleCookies](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions.handlecookies#microsoft-aspnetcore-mvc-testing-webapplicationfactoryclientoptions-handlecookies)                                  | Gets or sets whether `HttpClient` instances should handle cookies.                                 | `true`             |
| [MaxAutomaticRedirections](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions.maxautomaticredirections#microsoft-aspnetcore-mvc-testing-webapplicationfactoryclientoptions-maxautomaticredirections) | Gets or sets the maximum number of redirect responses that `HttpClient` instances should follow.   | 7                  |

Create the `WebApplicationFactoryClientOptions` class and pass it to the [CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) method (default values are shown in the code example):

```
// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;

_client = _factory.CreateClient(clientOptions);
```

## Inject mock services

Services can be overridden in a test with a call to [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) on the host builder. **To inject mock services, the SUT must have a `Startup` class with a `Startup.ConfigureServices` method.**

The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.

`Services/IQuoteService.cs`:

```
public interface IQuoteService
{
    Task<string> GenerateQuote();
}
```

`Services/QuoteService.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}
```

`Startup.cs`:

```
services.AddScoped<IQuoteService, QuoteService>();
```

`Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }
```

`Pages/Index.cs`:

```
<input id="quote" type="hidden" value="@Model.Quote">
```

The following markup is generated when the SUT app is run:

```
<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">
```

To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's `QuoteService` with a service provided by the test app, called `TestQuoteService`:

`IntegrationTests.IndexPageTests.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
```

`ConfigureTestServices` is called, and the scoped service is registered:

```
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
```

The markup produced during the test's execution reflects the quote text supplied by `TestQuoteService`, thus the assertion passes:

```
<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">
```

## Mock authentication

Tests in the `AuthTests` class check that a secure endpoint:

* Redirects an unauthenticated user to the app's Login page.
* Returns content for an authenticated user.

In the SUT, the `/SecurePage` page uses an [AuthorizePage](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizepage) convention to apply an [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) to the page. For more information, see [Razor Pages authorization conventions](../razor-pages/security/authorization/conventions?view=aspnetcore-10.0#require-authorization-to-access-a-page).

```
services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});
```

In the `Get_SecurePageRedirectsAnUnauthenticatedUser` test, a [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) is set to disallow redirects by setting [AllowAutoRedirect](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions.allowautoredirect#microsoft-aspnetcore-mvc-testing-webapplicationfactoryclientoptions-allowautoredirect) to `false`:

```
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login", 
        response.Headers.Location.OriginalString);
}
```

By disallowing the client to follow the redirect, the following checks can be made:

* The status code returned by the SUT can be checked against the expected [HttpStatusCode.Redirect](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-redirect) result, not the final status code after the redirect to the Login page, which would be [HttpStatusCode.OK](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-ok).
* The `Location` header value in the response headers is checked to confirm that it starts with `http://localhost/Identity/Account/Login`, not the final Login page response, where the `Location` header wouldn't be present.

The test app can mock an [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) in [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) in order to test aspects of authentication and authorization. A minimal scenario returns an [AuthenticateResult.Success](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticateresult.success):

```
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
```

The `TestAuthHandler` is called to authenticate a user when the authentication scheme is set to `Test` where `AddAuthentication` is registered for `ConfigureTestServices`. It's important for the `Test` scheme to match the scheme your app expects. Otherwise, authentication won't work.

```
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication("Test")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "Test", options => {});
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Test");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
```

For more information on `WebApplicationFactoryClientOptions`, see the [Client options](#client-options) section.

## Set the environment

By default, the SUT's host and app environment is configured to use the `Development` environment. To override the SUT's environment when using `IHostBuilder`:

* Set the `ASPNETCORE_ENVIRONMENT` environment variable (for example, `Staging`, `Production`, or other custom value, such as `Testing`).
* Override `CreateHostBuilder` in the test app to read environment variables prefixed with `ASPNETCORE`.

```
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));
```

If the SUT uses the Web Host (`IWebHostBuilder`), override `CreateWebHostBuilder`:

```
protected override IWebHostBuilder CreateWebHostBuilder() =>
    base.CreateWebHostBuilder().UseEnvironment("Testing");
```

## How the test infrastructure infers the app content root path

The `WebApplicationFactory` constructor infers the app [content root](../fundamentals/?view=aspnetcore-10.0#content-root) path by searching for a [WebApplicationFactoryContentRootAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactorycontentrootattribute) on the assembly containing the integration tests with a key equal to the `TEntryPoint` assembly `System.Reflection.Assembly.FullName`. In case an attribute with the correct key isn't found, `WebApplicationFactory` falls back to searching for a solution file (*.sln*) and appends the `TEntryPoint` assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.

## Disable shadow copying

Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to `Assembly.Location` and you encounter issues, you might have to disable shadow copying.

To disable shadow copying when using xUnit, create a `xunit.runner.json` file in your test project directory, with the [correct configuration setting](https://xunit.net/docs/configuration-files#shadowCopy):

```
{
  "shadowCopy": false
}
```

## Disposal of objects

After the tests of the `IClassFixture` implementation are executed, [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are disposed when xUnit disposes of the [`WebApplicationFactory`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1). If objects instantiated by the developer require disposal, dispose of them in the `IClassFixture` implementation. For more information, see [Implementing a Dispose method](/en-us/dotnet/standard/garbage-collection/implementing-dispose).

## Integration tests sample

The [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/test/integration-tests/samples) is composed of two apps:

| App                   | Project directory               | Description                                                         |
| --------------------- | ------------------------------- | ------------------------------------------------------------------- |
| Message app (the SUT) | `src/RazorPagesProject`         | Allows a user to add, delete one, delete all, and analyze messages. |
| Test app              | `tests/RazorPagesProject.Tests` | Used to integration test the SUT.                                   |

The tests can be run using the built-in test features of an IDE, such as [Visual Studio](https://visualstudio.microsoft.com). If using [Visual Studio Code](https://code.visualstudio.com/) or the command line, execute the following command at a command prompt in the `tests/RazorPagesProject.Tests` directory:

```
dotnet test
```

### Message app (SUT) organization

The SUT is a Razor Pages message system with the following characteristics:

* The Index page of the app (`Pages/Index.cshtml` and `Pages/Index.cshtml.cs`) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
* A message is described by the `Message` class (`Data/Message.cs`) with two properties: `Id` (key) and `Text` (message). The `Text` property is required and limited to 200 characters.
* Messages are stored using [Entity Framework's in-memory database](/en-us/ef/core/providers/in-memory/)†.
* The app contains a data access layer (DAL) in its database context class, `AppDbContext` (`Data/AppDbContext.cs`).
* If the database is empty on app startup, the message store is initialized with three messages.
* The app includes a `/SecurePage` that can only be accessed by an authenticated user.

†The EF topic, [Test with InMemory](/en-us/ef/core/miscellaneous/testing/in-memory), explains how to use an in-memory database for tests with MSTest. This topic uses the [xUnit](https://xunit.net/) test framework. Test concepts and test implementations across different test frameworks are similar but not identical.

Although the app doesn't use the repository pattern and isn't an effective example of the [Unit of Work (UoW) pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html), Razor Pages supports these patterns of development. For more information, see [Designing the infrastructure persistence layer](/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) and [Test controller logic](../mvc/controllers/testing?view=aspnetcore-10.0) (the sample implements the repository pattern).

### Test app organization

The test app is a console app inside the `tests/RazorPagesProject.Tests` directory.

| Test app directory  | Description                                                                                                                                                                                                                                                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AuthTests`         | Contains test methods for:- Accessing a secure page by an unauthenticated user.
- Accessing a secure page by an authenticated user with a mock [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1).
- Obtaining a GitHub user profile and checking the profile's user login. |
| `BasicTests`        | Contains a test method for routing and content type.                                                                                                                                                                                                                                                                                       |
| `IntegrationTests`  | Contains the integration tests for the Index page using custom `WebApplicationFactory` class.                                                                                                                                                                                                                                              |
| `Helpers/Utilities` | * `Utilities.cs` contains the `InitializeDbForTests` method used to seed the database with test data.
* `HtmlHelpers.cs` provides a method to return an AngleSharp `IHtmlDocument` for use by the test methods.
* `HttpClientExtensions.cs` provide overloads for `SendAsync` to submit requests to the SUT.                               |

The test framework is [xUnit](https://xunit.net/). Integration tests are conducted using the [Microsoft.AspNetCore.TestHost](/en-us/dotnet/api/microsoft.aspnetcore.testhost), which includes the [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver). Because the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package is used to configure the test host and test server, the `TestHost` and `TestServer` packages don't require direct package references in the test app's project file or developer configuration in the test app.

Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.

The sample app seeds the database with three messages in `Utilities.cs` that tests can use when they execute:

```
public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}
```

The SUT's database context is registered in its `Startup.ConfigureServices` method. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Startup.ConfigureServices` code is executed. To use a different database for the tests, the app's database context must be replaced in `builder.ConfigureServices`. For more information, see the [Customize WebApplicationFactory](#customize-webapplicationfactory) section.

For SUTs that still use the [Web Host](../fundamentals/host/web-host?view=aspnetcore-10.0), the test app's `builder.ConfigureServices` callback is executed *before* the SUT's `Startup.ConfigureServices` code. The test app's `builder.ConfigureTestServices` callback is executed *after*.

## Related content

* [Unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test)
* [Razor Pages unit tests in ASP.NET Core](razor-pages-tests?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0)
* [Test controller logic in ASP.NET Core](../mvc/controllers/testing?view=aspnetcore-10.0)

This article assumes a basic understanding of unit tests. If unfamiliar with test concepts, see the [Testing in .NET](/en-us/dotnet/core/testing/) article and its linked content.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/7.x/IntegrationTestsSample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. If you're unfamiliar with Razor Pages, see the following articles:

* [Introduction to Razor Pages](../razor-pages/?view=aspnetcore-10.0)
* [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Razor Pages unit tests](razor-pages-tests?view=aspnetcore-10.0)

**For testing SPAs**, we recommend a tool such as [Playwright for .NET](https://playwright.dev/dotnet/), which can automate a browser.

* Sets the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to the SUT's project root so that static files and pages/views are found when the tests are executed.
* Provides the [WebApplicationFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) class to streamline bootstrapping the SUT with `TestServer`.

The [unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test) documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.

**Separate unit tests from integration tests into different projects**. Separating the tests:

* Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
* Allows control over which set of tests are run.

There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, `IndexPageTests` to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, `HomeControllerTests` to test component integration for the Home controller).

## Test app prerequisites

The test project must:

* Reference the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package.
* Specify the Web SDK in the project file (`<Project Sdk="Microsoft.NET.Sdk.Web">`).

These prerequisites can be seen in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/7.x/IntegrationTestsSample). Inspect the `tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj` file. The sample app uses the [xUnit](https://xunit.net/) test framework and the [AngleSharp](https://anglesharp.github.io/) parser library, so the sample app also references:

* [`AngleSharp`](https://www.nuget.org/packages/AngleSharp)
* [`xunit`](https://www.nuget.org/packages/xunit)
* [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio)

In apps that use [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio) version 2.4.2 or later, the test project must reference the [`Microsoft.NET.Test.Sdk`](https://www.nuget.org/packages/Microsoft.NET.Test.Sdk) package.

Entity Framework Core is also used in the tests. See the [project file in GitHub](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/7.x/IntegrationTestsSample/src/RazorPagesProject/RazorPagesProject.csproj).

## SUT environment

If the SUT's [environment](../fundamentals/environments?view=aspnetcore-10.0) isn't set, the environment defaults to `Development`.

## Basic tests with the default WebApplicationFactory

Expose the implicitly defined `Program` class to the test project by doing one of the following:

* Expose internal types from the web app to the test project. This can be done in the SUT project's file (`.csproj`):

  ```
  <ItemGroup>
       <InternalsVisibleTo Include="MyTestProject" />
  </ItemGroup>
  ```

* Make the [`Program` class public using a partial class](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/7.x/IntegrationTestsSample/src/RazorPagesProject/Program.cs) declaration:

  ```
  var builder = WebApplication.CreateBuilder(args);
  // ... Configure services, routes, etc.
  app.Run();
  + public partial class Program { }
  ```

  The [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/7.x/IntegrationTestsSample) uses the `Program` partial class approach.

[WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) is used to create a [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) for the integration tests. `TEntryPoint` is the entry point class of the SUT, usually `Program.cs`.

Test classes implement a *class fixture* interface ([`IClassFixture`](https://xunit.net/docs/shared-context#class-fixture)) to indicate the class contains tests and provide shared object instances across the tests in the class.

The following test class, `BasicTests`, uses the `WebApplicationFactory` to bootstrap the SUT and provide an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to a test method, `Get_EndpointsReturnSuccessAndCorrectContentType`. The method verifies the response status code is successful (200-299) and the `Content-Type` header is `text/html; charset=utf-8` for several app pages.

[CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) creates an instance of `HttpClient` that automatically follows redirects and handles cookies.

```
public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}
```

By default, non-essential cookies aren't preserved across requests when the [General Data Protection Regulation consent policy](../security/gdpr?view=aspnetcore-10.0) is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see [Essential cookies](../security/gdpr?view=aspnetcore-10.0#essential-cookies).

[]()

## AngleSharp vs `Application Parts` for antiforgery checks

This article uses the [AngleSharp](https://anglesharp.github.io/) parser to handle the antiforgery checks by loading pages and parsing the HTML. For testing the endpoints of controller and Razor Pages views at a lower-level, without caring about how they render in the browser, consider using `Application Parts`. The [Application Parts](../mvc/advanced/app-parts?view=aspnetcore-10.0) approach injects a controller or Razor Page into the app that can be used to make JSON requests to get the required values. For more information, see the blog [Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts](https://blog.martincostello.com/integration-testing-antiforgery-with-application-parts/) and [associated GitHub repo](https://github.com/martincostello/antiforgery-testing-application-part) by [Martin Costello](https://github.com/martincostello).

## Customize WebApplicationFactory

Web host configuration can be created independently of the test classes by inheriting from [WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) to create one or more custom factories:

1. Inherit from `WebApplicationFactory` and override [ConfigureWebHost](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.configurewebhost). The [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) allows the configuration of the service collection with [`IWebHostBuilder.ConfigureServices`](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder.configureservices)

   ```
   public class CustomWebApplicationFactory<TProgram>
       : WebApplicationFactory<TProgram> where TProgram : class
   {
       protected override void ConfigureWebHost(IWebHostBuilder builder)
       {
           builder.ConfigureServices(services =>
           {
               var dbContextDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbContextOptions<ApplicationDbContext>));

               services.Remove(dbContextDescriptor);

               var dbConnectionDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbConnection));

               services.Remove(dbConnectionDescriptor);

               // Create open SqliteConnection so EF won't automatically close it.
               services.AddSingleton<DbConnection>(container =>
               {
                   var connection = new SqliteConnection("DataSource=:memory:");
                   connection.Open();

                   return connection;
               });

               services.AddDbContext<ApplicationDbContext>((container, options) =>
               {
                   var connection = container.GetRequiredService<DbConnection>();
                   options.UseSqlite(connection);
               });
           });

           builder.UseEnvironment("Development");
       }
   }
   ```

   Database seeding in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/7.x/IntegrationTestsSample) is performed by the `InitializeDbForTests` method. The method is described in the [Integration tests sample: Test app organization](#test-app-organization) section.

   The SUT's database context is registered in `Program.cs`. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Program.cs` code is executed. To use a different database for the tests than the app's database, the app's database context must be replaced in `builder.ConfigureServices`.

   The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. The factory then adds a new `ApplicationDbContext` that uses an in-memory database for the tests..

   To connect to a different database, change the `DbConnection`. To use a SQL Server test database:

* Reference the [`Microsoft.EntityFrameworkCore.SqlServer`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer/) NuGet package in the project file.
* Call `UseInMemoryDatabase`.

2. Use the custom `CustomWebApplicationFactory` in test classes. The following example uses the factory in the `IndexPageTests` class:

   ```
   public class IndexPageTests :
       IClassFixture<CustomWebApplicationFactory<Program>>
   {
       private readonly HttpClient _client;
       private readonly CustomWebApplicationFactory<Program>
           _factory;

       public IndexPageTests(
           CustomWebApplicationFactory<Program> factory)
       {
           _factory = factory;
           _client = factory.CreateClient(new WebApplicationFactoryClientOptions
           {
               AllowAutoRedirect = false
           });
       }
   ```

   The sample app's client is configured to prevent the `HttpClient` from following redirects. As explained later in the [Mock authentication](#mock-authentication) section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a `Location` header.

3. A typical test uses the `HttpClient` and helper methods to process the request and the response:

   ```
   [Fact]
   public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
   {
       // Arrange
       var defaultPage = await _client.GetAsync("/");
       var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

       //Act
       var response = await _client.SendAsync(
           (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
           (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

       // Assert
       Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
       Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
       Assert.Equal("/", response.Headers.Location.OriginalString);
   }
   ```

Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's [data protection antiforgery system](../security/data-protection/introduction?view=aspnetcore-10.0). In order to arrange for a test's POST request, the test app must:

1. Make a request for the page.
2. Parse the antiforgery cookie and request validation token from the response.
3. Make the POST request with the antiforgery cookie and request validation token in place.

The `SendAsync` helper extension methods (`Helpers/HttpClientExtensions.cs`) and the `GetDocumentAsync` helper method (`Helpers/HtmlHelpers.cs`) in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/7.x/IntegrationTestsSample/) use the [AngleSharp](https://anglesharp.github.io/) parser to handle the antiforgery check with the following methods:

* `GetDocumentAsync`: Receives the [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage) and returns an `IHtmlDocument`. `GetDocumentAsync` uses a factory that prepares a *virtual response* based on the original `HttpResponseMessage`. For more information, see the [AngleSharp documentation](https://github.com/AngleSharp/AngleSharp#documentation).

* `SendAsync` extension methods for the `HttpClient` compose an [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) and call [SendAsync(HttpRequestMessage)](/en-us/dotnet/api/system.net.http.httpclient.sendasync#system-net-http-httpclient-sendasync\(system-net-http-httprequestmessage\)) to submit requests to the SUT. Overloads for `SendAsync` accept the HTML form (`IHtmlFormElement`) and the following:

  * Submit button of the form (`IHtmlElement`)
  * Form values collection (`IEnumerable<KeyValuePair<string, string>>`)
  * Submit button (`IHtmlElement`) and form values (`IEnumerable<KeyValuePair<string, string>>`)

[AngleSharp](https://anglesharp.github.io/) is a **third-party parsing library used for demonstration purposes** in this article and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the [Html Agility Pack (HAP)](https://html-agility-pack.net/). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly. See [AngleSharp vs `Application Parts` for antiforgery checks](#asap7) in this article for more information.

The [EF-Core in-memory database provider](/en-us/ef/core/testing/choosing-a-testing-strategy#in-memory-as-a-database-fake) can be used for limited and basic testing, however the ***[SQLite provider](/en-us/ef/core/testing/choosing-a-testing-strategy#sqlite-as-a-database-fake) is the recommended choice for in-memory testing***.

When a test requires a custom middleware, you can use an [IStartupFilter](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter). For more information, see [App startup in ASP.NET Core](../fundamentals/startup?view=aspnetcore-10.0#startup-filters).

When a test requires a custom service, see the [Inject mock services](#inject-mock-services) section.

## Customize the client with WithWebHostBuilder

When additional configuration is required within a test method, [WithWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.withwebhostbuilder) creates a new `WebApplicationFactory` with an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) that is further customized by configuration.

The [sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/7.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests) calls `WithWebHostBuilder` to replace configured services with test stubs. For more information and example usage, see [Inject mock services](#inject-mock-services) in this article.

The `Post_DeleteMessageHandler_ReturnsRedirectToRoot` test method of the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/7.x/IntegrationTestsSample) demonstrates the use of `WithWebHostBuilder`. This test performs a record delete in the database by triggering a form submission in the SUT.

Because another test in the `IndexPageTests` class performs an operation that deletes all of the records in the database and may run before the `Post_DeleteMessageHandler_ReturnsRedirectToRoot` method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the `messages` form in the SUT is simulated in the request to the SUT:

```
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}
```

## Client options

See the [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) page for defaults and available options when creating `HttpClient` instances.

Create the `WebApplicationFactoryClientOptions` class and pass it to the [CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) method:

```
public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }
```

***NOTE:*** To avoid HTTPS redirection warnings in logs when using HTTPS redirection middleware, set `BaseAddress = new Uri("https://localhost")`

## Inject mock services

Services can be overridden in a test with a call to [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) on the host builder. To scope the overridden services to the test itself, the [WithWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.withwebhostbuilder) method is used to retrieve a host builder. This can be seen in the following tests:

* [Get\_QuoteService\_ProvidesQuoteInPage](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/7.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs#L166-L173)
* [Get\_GithubProfilePageCanGetAGithubUser](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/7.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs#L35-L38)
* [Get\_SecurePageIsReturnedForAnAuthenticatedUser](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/7.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs#L105-L117)

The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.

`Services/IQuoteService.cs`:

```
public interface IQuoteService
{
    Task<string> GenerateQuote();
}
```

`Services/QuoteService.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}
```

`Program.cs`:

```
services.AddScoped<IQuoteService, QuoteService>();
```

`Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }
```

`Pages/Index.cs`:

```
<input id="quote" type="hidden" value="@Model.Quote">
```

The following markup is generated when the SUT app is run:

```
<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">
```

To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's `QuoteService` with a service provided by the test app, called `TestQuoteService`:

`IntegrationTests.IndexPageTests.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
```

`ConfigureTestServices` is called, and the scoped service is registered:

```
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
```

The markup produced during the test's execution reflects the quote text supplied by `TestQuoteService`, thus the assertion passes:

```
<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">
```

## Mock authentication

Tests in the `AuthTests` class check that a secure endpoint:

* Redirects an unauthenticated user to the app's sign in page.
* Returns content for an authenticated user.

In the SUT, the `/SecurePage` page uses an [AuthorizePage](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizepage) convention to apply an [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) to the page. For more information, see [Razor Pages authorization conventions](../razor-pages/security/authorization/conventions?view=aspnetcore-10.0#require-authorization-to-access-a-page).

```
services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});
```

In the `Get_SecurePageRedirectsAnUnauthenticatedUser` test, a [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) is set to disallow redirects by setting [AllowAutoRedirect](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions.allowautoredirect#microsoft-aspnetcore-mvc-testing-webapplicationfactoryclientoptions-allowautoredirect) to `false`:

```
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}
```

By disallowing the client to follow the redirect, the following checks can be made:

* The status code returned by the SUT can be checked against the expected [HttpStatusCode.Redirect](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-redirect) result, not the final status code after the redirect to the sign in page, which would be [HttpStatusCode.OK](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-ok).
* The `Location` header value in the response headers is checked to confirm that it starts with `http://localhost/Identity/Account/Login`, not the final sign in page response, where the `Location` header wouldn't be present.

The test app can mock an [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) in [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) in order to test aspects of authentication and authorization. A minimal scenario returns an [AuthenticateResult.Success](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticateresult.success):

```
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
```

The `TestAuthHandler` is called to authenticate a user when the authentication scheme is set to `TestScheme` where `AddAuthentication` is registered for `ConfigureTestServices`. It's important for the `TestScheme` scheme to match the scheme your app expects. Otherwise, authentication won't work.

```
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
```

For more information on `WebApplicationFactoryClientOptions`, see the [Client options](#client-options) section.

### Basic tests for authentication middleware

See [this GitHub repository](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test) for basic tests of authentication middleware. It contains a [test server](https://github.com/blowdart/idunno.Authentication/blob/dev/test/idunno.Authentication.Basic.Test/BasicAuthenticationTests.cs#L331) that's specific to the test scenario.

## Set the environment

Set the [environment](../fundamentals/environments?view=aspnetcore-10.0) in the custom application factory:

```
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
```

## How the test infrastructure infers the app content root path

The `WebApplicationFactory` constructor infers the app [content root](../fundamentals/?view=aspnetcore-10.0#content-root) path by searching for a [WebApplicationFactoryContentRootAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactorycontentrootattribute) on the assembly containing the integration tests with a key equal to the `TEntryPoint` assembly `System.Reflection.Assembly.FullName`. In case an attribute with the correct key isn't found, `WebApplicationFactory` falls back to searching for a solution file (*.sln*) and appends the `TEntryPoint` assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.

## Disable shadow copying

Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to `Assembly.Location` and you encounter issues, you might have to disable shadow copying.

To disable shadow copying when using xUnit, create a `xunit.runner.json` file in your test project directory, with the [correct configuration setting](https://xunit.net/docs/configuration-files#shadowCopy):

```
{
  "shadowCopy": false
}
```

## Disposal of objects

After the tests of the `IClassFixture` implementation are executed, [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are disposed when xUnit disposes of the [`WebApplicationFactory`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1). If objects instantiated by the developer require disposal, dispose of them in the `IClassFixture` implementation. For more information, see [Implementing a Dispose method](/en-us/dotnet/standard/garbage-collection/implementing-dispose).

## Integration tests sample

The [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/7.x/IntegrationTestsSample) is composed of two apps:

| App                   | Project directory               | Description                                                         |
| --------------------- | ------------------------------- | ------------------------------------------------------------------- |
| Message app (the SUT) | `src/RazorPagesProject`         | Allows a user to add, delete one, delete all, and analyze messages. |
| Test app              | `tests/RazorPagesProject.Tests` | Used to integration test the SUT.                                   |

The tests can be run using the built-in test features of an IDE, such as [Visual Studio](https://visualstudio.microsoft.com). If using [Visual Studio Code](https://code.visualstudio.com/) or the command line, execute the following command at a command prompt in the `tests/RazorPagesProject.Tests` directory:

```
dotnet test
```

### Message app (SUT) organization

The SUT is a Razor Pages message system with the following characteristics:

* The Index page of the app (`Pages/Index.cshtml` and `Pages/Index.cshtml.cs`) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
* A message is described by the `Message` class (`Data/Message.cs`) with two properties: `Id` (key) and `Text` (message). The `Text` property is required and limited to 200 characters.
* Messages are stored using [Entity Framework's in-memory database](/en-us/ef/core/providers/in-memory/)†.
* The app contains a data access layer (DAL) in its database context class, `AppDbContext` (`Data/AppDbContext.cs`).
* If the database is empty on app startup, the message store is initialized with three messages.
* The app includes a `/SecurePage` that can only be accessed by an authenticated user.

†The EF article, [Test with InMemory](/en-us/ef/core/miscellaneous/testing/in-memory), explains how to use an in-memory database for tests with MSTest. This topic uses the [xUnit](https://xunit.net/) test framework. Test concepts and test implementations across different test frameworks are similar but not identical.

Although the app doesn't use the repository pattern and isn't an effective example of the [Unit of Work (UoW) pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html), Razor Pages supports these patterns of development. For more information, see [Designing the infrastructure persistence layer](/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) and [Test controller logic](../mvc/controllers/testing?view=aspnetcore-10.0) (the sample implements the repository pattern).

### Test app organization

The test app is a console app inside the `tests/RazorPagesProject.Tests` directory.

| Test app directory  | Description                                                                                                                                                                                                                                                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AuthTests`         | Contains test methods for:- Accessing a secure page by an unauthenticated user.
- Accessing a secure page by an authenticated user with a mock [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1).
- Obtaining a GitHub user profile and checking the profile's user login. |
| `BasicTests`        | Contains a test method for routing and content type.                                                                                                                                                                                                                                                                                       |
| `IntegrationTests`  | Contains the integration tests for the Index page using custom `WebApplicationFactory` class.                                                                                                                                                                                                                                              |
| `Helpers/Utilities` | * `Utilities.cs` contains the `InitializeDbForTests` method used to seed the database with test data.
* `HtmlHelpers.cs` provides a method to return an AngleSharp `IHtmlDocument` for use by the test methods.
* `HttpClientExtensions.cs` provide overloads for `SendAsync` to submit requests to the SUT.                               |

The test framework is [xUnit](https://xunit.net/). Integration tests are conducted using the [Microsoft.AspNetCore.TestHost](/en-us/dotnet/api/microsoft.aspnetcore.testhost), which includes the [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver). Because the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package is used to configure the test host and test server, the `TestHost` and `TestServer` packages don't require direct package references in the test app's project file or developer configuration in the test app.

Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.

The sample app seeds the database with three messages in `Utilities.cs` that tests can use when they execute:

```
public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}
```

The SUT's database context is registered in `Program.cs`. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Program.cs` code is executed. To use a different database for the tests, the app's database context must be replaced in `builder.ConfigureServices`. For more information, see the [Customize WebApplicationFactory](#customize-webapplicationfactory) section.

## Related content

* [Unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test)
* [Razor Pages unit tests in ASP.NET Core](razor-pages-tests?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0)
* [Test controller logic in ASP.NET Core](../mvc/controllers/testing?view=aspnetcore-10.0)
* [Basic tests for authentication middleware](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test)

This article assumes a basic understanding of unit tests. If unfamiliar with test concepts, see the [Testing in .NET](/en-us/dotnet/core/testing/) article and its linked content.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/8.x/IntegrationTestsSample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. If you're unfamiliar with Razor Pages, see the following articles:

* [Introduction to Razor Pages](../razor-pages/?view=aspnetcore-10.0)
* [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Razor Pages unit tests](razor-pages-tests?view=aspnetcore-10.0)

**For testing SPAs**, we recommend a tool such as [Playwright for .NET](https://playwright.dev/dotnet/), which can automate a browser.

* Sets the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to the SUT's project root so that static files and pages/views are found when the tests are executed.
* Provides the [WebApplicationFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) class to streamline bootstrapping the SUT with `TestServer`.

The [unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test) documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.

**Separate unit tests from integration tests into different projects**. Separating the tests:

* Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
* Allows control over which set of tests are run.

There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, `IndexPageTests` to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, `HomeControllerTests` to test component integration for the Home controller).

## Test app prerequisites

The test project must:

* Reference the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package.
* Specify the Web SDK in the project file (`<Project Sdk="Microsoft.NET.Sdk.Web">`).

These prerequisites can be seen in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/8.x/IntegrationTestsSample). Inspect the `tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj` file. The sample app uses the [xUnit](https://xunit.net/) test framework and the [AngleSharp](https://anglesharp.github.io/) parser library, so the sample app also references:

* [`AngleSharp`](https://www.nuget.org/packages/AngleSharp)
* [`xunit`](https://www.nuget.org/packages/xunit)
* [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio)

In apps that use [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio) version 2.4.2 or later, the test project must reference the [`Microsoft.NET.Test.Sdk`](https://www.nuget.org/packages/Microsoft.NET.Test.Sdk) package.

Entity Framework Core is also used in the tests. See the [project file in GitHub](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/8.x/IntegrationTestsSample/src/RazorPagesProject/RazorPagesProject.csproj).

## SUT environment

If the SUT's [environment](../fundamentals/environments?view=aspnetcore-10.0) isn't set, the environment defaults to `Development`.

## Basic tests with the default WebApplicationFactory

Expose the implicitly defined `Program` class to the test project by doing one of the following:

* Expose internal types from the web app to the test project. This can be done in the SUT project's file (`.csproj`):

  ```
  <ItemGroup>
       <InternalsVisibleTo Include="MyTestProject" />
  </ItemGroup>
  ```

* Make the [`Program` class public using a partial class](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/8.x/IntegrationTestsSample/src/RazorPagesProject/Program.cs) declaration:

  ```
  var builder = WebApplication.CreateBuilder(args);
  // ... Configure services, routes, etc.
  app.Run();
  + public partial class Program { }
  ```

  The [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/8.x/IntegrationTestsSample) uses the `Program` partial class approach.

[WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) is used to create a [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) for the integration tests. `TEntryPoint` is the entry point class of the SUT, usually `Program.cs`.

Test classes implement a *class fixture* interface ([`IClassFixture`](https://xunit.net/docs/shared-context#class-fixture)) to indicate the class contains tests and provide shared object instances across the tests in the class.

The following test class, `BasicTests`, uses the `WebApplicationFactory` to bootstrap the SUT and provide an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to a test method, `Get_EndpointsReturnSuccessAndCorrectContentType`. The method verifies the response status code is successful (200-299) and the `Content-Type` header is `text/html; charset=utf-8` for several app pages.

[CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) creates an instance of `HttpClient` that automatically follows redirects and handles cookies.

```
public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}
```

By default, non-essential cookies aren't preserved across requests when the [General Data Protection Regulation consent policy](../security/gdpr?view=aspnetcore-10.0) is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see [Essential cookies](../security/gdpr?view=aspnetcore-10.0#essential-cookies).

[]()

## AngleSharp vs `Application Parts` for antiforgery checks

This article uses the [AngleSharp](https://anglesharp.github.io/) parser to handle the antiforgery checks by loading pages and parsing the HTML. For testing the endpoints of controller and Razor Pages views at a lower-level, without caring about how they render in the browser, consider using `Application Parts`. The [Application Parts](../mvc/advanced/app-parts?view=aspnetcore-10.0) approach injects a controller or Razor Page into the app that can be used to make JSON requests to get the required values. For more information, see the blog [Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts](https://blog.martincostello.com/integration-testing-antiforgery-with-application-parts/) and [associated GitHub repo](https://github.com/martincostello/antiforgery-testing-application-part) by [Martin Costello](https://github.com/martincostello).

## Customize WebApplicationFactory

Web host configuration can be created independently of the test classes by inheriting from [WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) to create one or more custom factories:

1. Inherit from `WebApplicationFactory` and override [ConfigureWebHost](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.configurewebhost). The [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) allows the configuration of the service collection with [`IWebHostBuilder.ConfigureServices`](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder.configureservices)

   ```
   public class CustomWebApplicationFactory<TProgram>
       : WebApplicationFactory<TProgram> where TProgram : class
   {
       protected override void ConfigureWebHost(IWebHostBuilder builder)
       {
           builder.ConfigureServices(services =>
           {
               var dbContextDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbContextOptions<ApplicationDbContext>));

               services.Remove(dbContextDescriptor);

               var dbConnectionDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbConnection));

               services.Remove(dbConnectionDescriptor);

               // Create open SqliteConnection so EF won't automatically close it.
               services.AddSingleton<DbConnection>(container =>
               {
                   var connection = new SqliteConnection("DataSource=:memory:");
                   connection.Open();

                   return connection;
               });

               services.AddDbContext<ApplicationDbContext>((container, options) =>
               {
                   var connection = container.GetRequiredService<DbConnection>();
                   options.UseSqlite(connection);
               });
           });

           builder.UseEnvironment("Development");
       }
   }
   ```

   Database seeding in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/8.x/IntegrationTestsSample) is performed by the `InitializeDbForTests` method. The method is described in the [Integration tests sample: Test app organization](#test-app-organization) section.

   The SUT's database context is registered in `Program.cs`. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Program.cs` code is executed. To use a different database for the tests than the app's database, the app's database context must be replaced in `builder.ConfigureServices`.

   The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. The factory then adds a new `ApplicationDbContext` that uses an in-memory database for the tests..

   To connect to a different database, change the `DbConnection`. To use a SQL Server test database:

   * Reference the [`Microsoft.EntityFrameworkCore.SqlServer`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer/) NuGet package in the project file.
   * Call `UseInMemoryDatabase`.

2) Use the custom `CustomWebApplicationFactory` in test classes. The following example uses the factory in the `IndexPageTests` class:

   ```
   public class IndexPageTests :
       IClassFixture<CustomWebApplicationFactory<Program>>
   {
       private readonly HttpClient _client;
       private readonly CustomWebApplicationFactory<Program>
           _factory;

       public IndexPageTests(
           CustomWebApplicationFactory<Program> factory)
       {
           _factory = factory;
           _client = factory.CreateClient(new WebApplicationFactoryClientOptions
           {
               AllowAutoRedirect = false
           });
       }
   }
   ```

   The sample app's client is configured to prevent the `HttpClient` from following redirects. As explained later in the [Mock authentication](#mock-authentication) section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a `Location` header.

3) A typical test uses the `HttpClient` and helper methods to process the request and the response:

   ```
   [Fact]
   public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
   {
       // Arrange
       var defaultPage = await _client.GetAsync("/");
       var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

       //Act
       var response = await _client.SendAsync(
           (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
           (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

       // Assert
       Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
       Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
       Assert.Equal("/", response.Headers.Location.OriginalString);
   }
   ```

Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's [data protection antiforgery system](../security/data-protection/introduction?view=aspnetcore-10.0). In order to arrange for a test's POST request, the test app must:

1. Make a request for the page.
2. Parse the antiforgery cookie and request validation token from the response.
3. Make the POST request with the antiforgery cookie and request validation token in place.

The `SendAsync` helper extension methods (`Helpers/HttpClientExtensions.cs`) and the `GetDocumentAsync` helper method (`Helpers/HtmlHelpers.cs`) in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/8.x/IntegrationTestsSample/) use the [AngleSharp](https://anglesharp.github.io/) parser to handle the antiforgery check with the following methods:

* `GetDocumentAsync`: Receives the [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage) and returns an `IHtmlDocument`. `GetDocumentAsync` uses a factory that prepares a *virtual response* based on the original `HttpResponseMessage`. For more information, see the [AngleSharp documentation](https://github.com/AngleSharp/AngleSharp#documentation).

* `SendAsync` extension methods for the `HttpClient` compose an [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) and call [SendAsync(HttpRequestMessage)](/en-us/dotnet/api/system.net.http.httpclient.sendasync#system-net-http-httpclient-sendasync\(system-net-http-httprequestmessage\)) to submit requests to the SUT. Overloads for `SendAsync` accept the HTML form (`IHtmlFormElement`) and the following:

  * Submit button of the form (`IHtmlElement`)
  * Form values collection (`IEnumerable<KeyValuePair<string, string>>`)
  * Submit button (`IHtmlElement`) and form values (`IEnumerable<KeyValuePair<string, string>>`)

[AngleSharp](https://anglesharp.github.io/) is a **third-party parsing library used for demonstration purposes** in this article and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the [Html Agility Pack (HAP)](https://html-agility-pack.net/). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly. See [AngleSharp vs `Application Parts` for antiforgery checks](#asap7) in this article for more information.

The [EF-Core in-memory database provider](/en-us/ef/core/testing/choosing-a-testing-strategy#in-memory-as-a-database-fake) can be used for limited and basic testing, however the ***[SQLite provider](/en-us/ef/core/testing/choosing-a-testing-strategy#sqlite-as-a-database-fake) is the recommended choice for in-memory testing***.

When a test requires a custom middleware, you can use an [IStartupFilter](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter). For more information, see [App startup in ASP.NET Core](../fundamentals/startup?view=aspnetcore-10.0#startup-filters).

When a test requires a custom service, see the [Inject mock services](#inject-mock-services) section.

## Customize the client with WithWebHostBuilder

When additional configuration is required within a test method, [WithWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.withwebhostbuilder) creates a new `WebApplicationFactory` with an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) that is further customized by configuration.

The [sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/8.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests) calls `WithWebHostBuilder` to replace configured services with test stubs. For more information and example usage, see [Inject mock services](#inject-mock-services) in this article.

The `Post_DeleteMessageHandler_ReturnsRedirectToRoot` test method of the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/8.x/IntegrationTestsSample) demonstrates the use of `WithWebHostBuilder`. This test performs a record delete in the database by triggering a form submission in the SUT.

Because another test in the `IndexPageTests` class performs an operation that deletes all of the records in the database and may run before the `Post_DeleteMessageHandler_ReturnsRedirectToRoot` method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the `messages` form in the SUT is simulated in the request to the SUT:

```
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}
```

## Client options

See the [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) page for defaults and available options when creating `HttpClient` instances.

Create the `WebApplicationFactoryClientOptions` class and pass it to the [CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) method:

```
public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }
}
```

***NOTE:*** To avoid HTTPS redirection warnings in logs when using HTTPS redirection middleware, set `BaseAddress = new Uri("https://localhost")`

## Inject mock services

Services can be overridden in a test with a call to [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) on the host builder. To scope the overridden services to the test itself, the [WithWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.withwebhostbuilder) method is used to retrieve a host builder. This can be seen in the following tests:

* [Get\_QuoteService\_ProvidesQuoteInPage](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/8.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs#L166-L173)
* [Get\_GithubProfilePageCanGetAGithubUser](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/8.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs#L35-L38)
* [Get\_SecurePageIsReturnedForAnAuthenticatedUser](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/8.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs#L105-L117)

The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.

`Services/IQuoteService.cs`:

```
public interface IQuoteService
{
    Task<string> GenerateQuote();
}
```

`Services/QuoteService.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}
```

`Program.cs`:

```
services.AddScoped<IQuoteService, QuoteService>();
```

`Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }
```

`Pages/Index.cs`:

```
<input id="quote" type="hidden" value="@Model.Quote">
```

The following markup is generated when the SUT app is run:

```
<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">
```

To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's `QuoteService` with a service provided by the test app, called `TestQuoteService`:

`IntegrationTests.IndexPageTests.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
```

`ConfigureTestServices` is called, and the scoped service is registered:

```
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
```

The markup produced during the test's execution reflects the quote text supplied by `TestQuoteService`, thus the assertion passes:

```
<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">
```

## Mock authentication

Tests in the `AuthTests` class check that a secure endpoint:

* Redirects an unauthenticated user to the app's sign in page.
* Returns content for an authenticated user.

In the SUT, the `/SecurePage` page uses an [AuthorizePage](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizepage) convention to apply an [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) to the page. For more information, see [Razor Pages authorization conventions](../razor-pages/security/authorization/conventions?view=aspnetcore-10.0#require-authorization-to-access-a-page).

```
services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});
```

In the `Get_SecurePageRedirectsAnUnauthenticatedUser` test, a [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) is set to disallow redirects by setting [AllowAutoRedirect](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions.allowautoredirect#microsoft-aspnetcore-mvc-testing-webapplicationfactoryclientoptions-allowautoredirect) to `false`:

```
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}
```

By disallowing the client to follow the redirect, the following checks can be made:

* The status code returned by the SUT can be checked against the expected [HttpStatusCode.Redirect](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-redirect) result, not the final status code after the redirect to the sign in page, which would be [HttpStatusCode.OK](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-ok).
* The `Location` header value in the response headers is checked to confirm that it starts with `http://localhost/Identity/Account/Login`, not the final sign in page response, where the `Location` header wouldn't be present.

The test app can mock an [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) in [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) in order to test aspects of authentication and authorization. A minimal scenario returns an [AuthenticateResult.Success](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticateresult.success):

```
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
```

The `TestAuthHandler` is called to authenticate a user when the authentication scheme is set to `TestScheme` where `AddAuthentication` is registered for `ConfigureTestServices`. It's important for the `TestScheme` scheme to match the scheme your app expects. Otherwise, authentication won't work.

```
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
```

For more information on `WebApplicationFactoryClientOptions`, see the [Client options](#client-options) section.

### Basic tests for authentication middleware

See [this GitHub repository](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test) for basic tests of authentication middleware. It contains a [test server](https://github.com/blowdart/idunno.Authentication/blob/dev/test/idunno.Authentication.Basic.Test/BasicAuthenticationTests.cs#L331) that's specific to the test scenario.

## Set the environment

Set the [environment](../fundamentals/environments?view=aspnetcore-10.0) in the custom application factory:

```
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
```

## How the test infrastructure infers the app content root path

The `WebApplicationFactory` constructor infers the app [content root](../fundamentals/?view=aspnetcore-10.0#content-root) path by searching for a [WebApplicationFactoryContentRootAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactorycontentrootattribute) on the assembly containing the integration tests with a key equal to the `TEntryPoint` assembly `System.Reflection.Assembly.FullName`. In case an attribute with the correct key isn't found, `WebApplicationFactory` falls back to searching for a solution file (*.sln*) and appends the `TEntryPoint` assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.

## Disable shadow copying

Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to `Assembly.Location` and you encounter issues, you might have to disable shadow copying.

To disable shadow copying when using xUnit, create a `xunit.runner.json` file in your test project directory, with the [correct configuration setting](https://xunit.net/docs/configuration-files#shadowCopy):

```
{
  "shadowCopy": false
}
```

## Disposal of objects

After the tests of the `IClassFixture` implementation are executed, [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are disposed when xUnit disposes of the [`WebApplicationFactory`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1). If objects instantiated by the developer require disposal, dispose of them in the `IClassFixture` implementation. For more information, see [Implementing a Dispose method](/en-us/dotnet/standard/garbage-collection/implementing-dispose).

## Integration tests sample

The [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/8.x/IntegrationTestsSample) is composed of two apps:

| App                   | Project directory               | Description                                                         |
| --------------------- | ------------------------------- | ------------------------------------------------------------------- |
| Message app (the SUT) | `src/RazorPagesProject`         | Allows a user to add, delete one, delete all, and analyze messages. |
| Test app              | `tests/RazorPagesProject.Tests` | Used to integration test the SUT.                                   |

The tests can be run using the built-in test features of an IDE, such as [Visual Studio](https://visualstudio.microsoft.com). If using [Visual Studio Code](https://code.visualstudio.com/) or the command line, execute the following command at a command prompt in the `tests/RazorPagesProject.Tests` directory:

```
dotnet test
```

### Message app (SUT) organization

The SUT is a Razor Pages message system with the following characteristics:

* The Index page of the app (`Pages/Index.cshtml` and `Pages/Index.cshtml.cs`) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
* A message is described by the `Message` class (`Data/Message.cs`) with two properties: `Id` (key) and `Text` (message). The `Text` property is required and limited to 200 characters.
* Messages are stored using [Entity Framework's in-memory database](/en-us/ef/core/providers/in-memory/)†.
* The app contains a data access layer (DAL) in its database context class, `AppDbContext` (`Data/AppDbContext.cs`).
* If the database is empty on app startup, the message store is initialized with three messages.
* The app includes a `/SecurePage` that can only be accessed by an authenticated user.

†The EF article, [Test with InMemory](/en-us/ef/core/miscellaneous/testing/in-memory), explains how to use an in-memory database for tests with MSTest. This topic uses the [xUnit](https://xunit.net/) test framework. Test concepts and test implementations across different test frameworks are similar but not identical.

Although the app doesn't use the repository pattern and isn't an effective example of the [Unit of Work (UoW) pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html), Razor Pages supports these patterns of development. For more information, see [Designing the infrastructure persistence layer](/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) and [Test controller logic](../mvc/controllers/testing?view=aspnetcore-10.0) (the sample implements the repository pattern).

### Test app organization

The test app is a console app inside the `tests/RazorPagesProject.Tests` directory.

| Test app directory  | Description                                                                                                                                                                                                                                                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AuthTests`         | Contains test methods for:- Accessing a secure page by an unauthenticated user.
- Accessing a secure page by an authenticated user with a mock [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1).
- Obtaining a GitHub user profile and checking the profile's user login. |
| `BasicTests`        | Contains a test method for routing and content type.                                                                                                                                                                                                                                                                                       |
| `IntegrationTests`  | Contains the integration tests for the Index page using custom `WebApplicationFactory` class.                                                                                                                                                                                                                                              |
| `Helpers/Utilities` | * `Utilities.cs` contains the `InitializeDbForTests` method used to seed the database with test data.
* `HtmlHelpers.cs` provides a method to return an AngleSharp `IHtmlDocument` for use by the test methods.
* `HttpClientExtensions.cs` provide overloads for `SendAsync` to submit requests to the SUT.                               |

The test framework is [xUnit](https://xunit.net/). Integration tests are conducted using the [Microsoft.AspNetCore.TestHost](/en-us/dotnet/api/microsoft.aspnetcore.testhost), which includes the [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver). Because the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package is used to configure the test host and test server, the `TestHost` and `TestServer` packages don't require direct package references in the test app's project file or developer configuration in the test app.

Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.

The sample app seeds the database with three messages in `Utilities.cs` that tests can use when they execute:

```
public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}
```

The SUT's database context is registered in `Program.cs`. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Program.cs` code is executed. To use a different database for the tests, the app's database context must be replaced in `builder.ConfigureServices`. For more information, see the [Customize WebApplicationFactory](#customize-webapplicationfactory) section.

## Related content

* [Unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test)
* [Razor Pages unit tests in ASP.NET Core](razor-pages-tests?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0)
* [Test controller logic in ASP.NET Core](../mvc/controllers/testing?view=aspnetcore-10.0)
* [Basic tests for authentication middleware](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test)

This article assumes a basic understanding of unit tests. If unfamiliar with test concepts, see the [Testing in .NET](/en-us/dotnet/core/testing/) article and its linked content.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. If you're unfamiliar with Razor Pages, see the following articles:

* [Introduction to Razor Pages](../razor-pages/?view=aspnetcore-10.0)
* [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Razor Pages unit tests](razor-pages-tests?view=aspnetcore-10.0)

**For testing SPAs**, we recommend a tool such as [Playwright for .NET](https://playwright.dev/dotnet/), which can automate a browser.

* Sets the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to the SUT's project root so that static files and pages/views are found when the tests are executed.
* Provides the [WebApplicationFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) class to streamline bootstrapping the SUT with `TestServer`.

The [unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test) documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.

**Separate unit tests from integration tests into different projects**. Separating the tests:

* Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
* Allows control over which set of tests are run.

There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. The only difference is in how the tests are named. In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, `IndexPageTests` to test component integration for the Index page). In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, `HomeControllerTests` to test component integration for the Home controller).

## Test app prerequisites

The test project must:

* Reference the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package.
* Specify the Web SDK in the project file (`<Project Sdk="Microsoft.NET.Sdk.Web">`).

These prerequisites can be seen in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample). Inspect the `tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj` file. The sample app uses the [xUnit](https://xunit.net/) test framework and the [AngleSharp](https://anglesharp.github.io/) parser library, so the sample app also references:

* [`AngleSharp`](https://www.nuget.org/packages/AngleSharp)
* [`xunit`](https://www.nuget.org/packages/xunit)
* [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio)

In apps that use [`xunit.runner.visualstudio`](https://www.nuget.org/packages/xunit.runner.visualstudio) version 2.4.2 or later, the test project must reference the [`Microsoft.NET.Test.Sdk`](https://www.nuget.org/packages/Microsoft.NET.Test.Sdk) package.

Entity Framework Core is also used in the tests. See the [project file in GitHub](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/9.x/IntegrationTestsSample/src/RazorPagesProject/RazorPagesProject.csproj).

## SUT environment

If the SUT's [environment](../fundamentals/environments?view=aspnetcore-10.0) isn't set, the environment defaults to `Development`.

## Basic tests with the default WebApplicationFactory

Expose the implicitly defined `Program` class to the test project by doing one of the following:

* Expose internal types from the web app to the test project. This can be done in the SUT project's file (`.csproj`):

  ```
  <ItemGroup>
       <InternalsVisibleTo Include="MyTestProject" />
  </ItemGroup>
  ```

* Make the [`Program` class public using a partial class](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/9.x/IntegrationTestsSample/src/RazorPagesProject/Program.cs) declaration:

  ```
  var builder = WebApplication.CreateBuilder(args);
  // ... Configure services, routes, etc.
  app.Run();
  + public partial class Program { }
  ```

  The [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample) uses the `Program` partial class approach.

[WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) is used to create a [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) for the integration tests. `TEntryPoint` is the entry point class of the SUT, usually `Program.cs`.

Test classes implement a *class fixture* interface ([`IClassFixture`](https://xunit.net/docs/shared-context#class-fixture)) to indicate the class contains tests and provide shared object instances across the tests in the class.

The following test class, `BasicTests`, uses the `WebApplicationFactory` to bootstrap the SUT and provide an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to a test method, `Get_EndpointsReturnSuccessAndCorrectContentType`. The method verifies the response status code is successful (200-299) and the `Content-Type` header is `text/html; charset=utf-8` for several app pages.

[CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) creates an instance of `HttpClient` that automatically follows redirects and handles cookies.

```
public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}
```

```
[TestClass]
public class BasicTests
{
    private static CustomWebApplicationFactory<Program> _factory;

    [ClassInitialize]
    public static void AssemblyInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
    public static void AssemblyCleanup(TestContext _)
    {
        _factory.Dispose();
    }

    [TestMethod]
    [DataRow("/")]
    [DataRow("/Index")]
    [DataRow("/About")]
    [DataRow("/Privacy")]
    [DataRow("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.AreEqual("text/html; charset=utf-8",
            response.Content.Headers.ContentType.ToString());
    }
}
```

```
public class BasicTests
{
    private CustomWebApplicationFactory<Program>
        _factory;

    [SetUp]
    public void SetUp()
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [TearDown]
    public void TearDown()
    {
        _factory.Dispose();
    }

    [DatapointSource]
    public string[] values = ["/", "/Index", "/About", "/Privacy", "/Contact"];

    [Theory]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.That(response.Content.Headers.ContentType.ToString(), Is.EqualTo("text/html; charset=utf-8"));
    }
}
```

By default, non-essential cookies aren't preserved across requests when the [General Data Protection Regulation consent policy](../security/gdpr?view=aspnetcore-10.0) is enabled. To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. For instructions on marking a cookie as essential, see [Essential cookies](../security/gdpr?view=aspnetcore-10.0#essential-cookies).

[]()

## AngleSharp vs `Application Parts` for antiforgery checks

This article uses the [AngleSharp](https://anglesharp.github.io/) parser to handle the antiforgery checks by loading pages and parsing the HTML. For testing the endpoints of controller and Razor Pages views at a lower-level, without caring about how they render in the browser, consider using `Application Parts`. The [Application Parts](../mvc/advanced/app-parts?view=aspnetcore-10.0) approach injects a controller or Razor Page into the app that can be used to make JSON requests to get the required values. For more information, see the blog [Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts](https://blog.martincostello.com/integration-testing-antiforgery-with-application-parts/) and [associated GitHub repo](https://github.com/martincostello/antiforgery-testing-application-part) by [Martin Costello](https://github.com/martincostello).

## Customize WebApplicationFactory

Web host configuration can be created independently of the test classes by inheriting from [WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) to create one or more custom factories:

1. Inherit from `WebApplicationFactory` and override [ConfigureWebHost](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.configurewebhost). The [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) allows the configuration of the service collection with [`IWebHostBuilder.ConfigureServices`](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder.configureservices)

   ```
   public class CustomWebApplicationFactory<TProgram>
       : WebApplicationFactory<TProgram> where TProgram : class
   {
       protected override void ConfigureWebHost(IWebHostBuilder builder)
       {
           builder.ConfigureServices(services =>
           {
               var dbContextDescriptor = services.SingleOrDefault(
                   d => d.ServiceType == 
                       typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

               services.Remove(dbContextDescriptor);

               var dbConnectionDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbConnection));

               services.Remove(dbConnectionDescriptor);

               // Create open SqliteConnection so EF won't automatically close it.
               services.AddSingleton<DbConnection>(container =>
               {
                   var connection = new SqliteConnection("DataSource=:memory:");
                   connection.Open();

                   return connection;
               });

               services.AddDbContext<ApplicationDbContext>((container, options) =>
               {
                   var connection = container.GetRequiredService<DbConnection>();
                   options.UseSqlite(connection);
               });
           });

           builder.UseEnvironment("Development");
       }
   }
   ```

   ```
   public class CustomWebApplicationFactory<TProgram>
       : WebApplicationFactory<TProgram> where TProgram : class
   {
       protected override void ConfigureWebHost(IWebHostBuilder builder)
       {
           builder.ConfigureServices(services =>
           {
               var dbContextDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

               services.Remove(dbContextDescriptor);

               var dbConnectionDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbConnection));

               services.Remove(dbConnectionDescriptor);

               // Create open SqliteConnection so EF won't automatically close it.
               services.AddSingleton<DbConnection>(container =>
               {
                   var connection = new SqliteConnection("DataSource=:memory:");
                   connection.Open();

                   return connection;
               });

               services.AddDbContext<ApplicationDbContext>((container, options) =>
               {
                   var connection = container.GetRequiredService<DbConnection>();
                   options.UseSqlite(connection);
               });
           });

           builder.UseEnvironment("Development");
       }
   }
   ```

   ```
   public class CustomWebApplicationFactory<TProgram>
       : WebApplicationFactory<TProgram> where TProgram : class
   {
       protected override void ConfigureWebHost(IWebHostBuilder builder)
       {
           builder.ConfigureServices(services =>
           {
               var dbContextDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

               services.Remove(dbContextDescriptor);

               var dbConnectionDescriptor = services.SingleOrDefault(
                   d => d.ServiceType ==
                       typeof(DbConnection));

               services.Remove(dbConnectionDescriptor);

               // Create open SqliteConnection so EF won't automatically close it.
               services.AddSingleton<DbConnection>(container =>
               {
                   var connection = new SqliteConnection("DataSource=:memory:");
                   connection.Open();

                   return connection;
               });

               services.AddDbContext<ApplicationDbContext>((container, options) =>
               {
                   var connection = container.GetRequiredService<DbConnection>();
                   options.UseSqlite(connection);
               });
           });

           builder.UseEnvironment("Development");
       }
   }
   ```

   Database seeding in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample) is performed by the `InitializeDbForTests` method. The method is described in the [Integration tests sample: Test app organization](#test-app-organization) section.

   The SUT's database context is registered in `Program.cs`. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Program.cs` code is executed. To use a different database for the tests than the app's database, the app's database context must be replaced in `builder.ConfigureServices`.

   The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. The factory then adds a new `ApplicationDbContext` that uses an in-memory database for the tests..

   To connect to a different database, change the `DbConnection`. To use a SQL Server test database:

   * Reference the [`Microsoft.EntityFrameworkCore.SqlServer`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer/) NuGet package in the project file.
   * Call `UseInMemoryDatabase`.

2) Use the custom `CustomWebApplicationFactory` in test classes. The following example uses the factory in the `IndexPageTests` class:

   ```
   public class IndexPageTests :
       IClassFixture<CustomWebApplicationFactory<Program>>
   {
       private readonly HttpClient _client;
       private readonly CustomWebApplicationFactory<Program>
           _factory;

       public IndexPageTests(
           CustomWebApplicationFactory<Program> factory)
       {
           _factory = factory;
           _client = factory.CreateClient(new WebApplicationFactoryClientOptions
           {
               AllowAutoRedirect = false
           });
       }
   ```

   ```
   [TestClass]
   public class IndexPageTests
   {
       private static HttpClient _client;
       private static CustomWebApplicationFactory<Program>
           _factory;

       [ClassInitialize]
       public static void AssemblyInitialize(TestContext _)
       {
           _factory = new CustomWebApplicationFactory<Program>();
           _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
           {
               AllowAutoRedirect = false
           });
       }

       [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
       public static void AssemblyCleanup(TestContext _)
       {
           _factory.Dispose();
       }
   ```

   ```
   public class IndexPageTests
   {

       private HttpClient _client;
       private CustomWebApplicationFactory<Program>
           _factory;

       [SetUp]
       public void SetUp()
       {
           _factory = new CustomWebApplicationFactory<Program>();
           _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
           {
               AllowAutoRedirect = false
           });
       }

       [TearDown]
       public void TearDown()
       {
           _factory.Dispose();
           _client.Dispose();
       }
   ```

   The sample app's client is configured to prevent the `HttpClient` from following redirects. As explained later in the [Mock authentication](#mock-authentication) section, this permits tests to check the result of the app's first response. The first response is a redirect in many of these tests with a `Location` header.

3) A typical test uses the `HttpClient` and helper methods to process the request and the response:

   ```
   [Fact]
   public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
   {
       // Arrange
       var defaultPage = await _client.GetAsync("/");
       var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

       //Act
       var response = await _client.SendAsync(
           (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
           (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

       // Assert
       Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
       Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
       Assert.Equal("/", response.Headers.Location.OriginalString);
   }
   ```

   ```
   [TestMethod]
   public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
   {
       // Arrange
       var defaultPage = await _client.GetAsync("/");
       var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

       //Act
       var response = await _client.SendAsync(
           (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
           (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

       // Assert
       Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
       Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
       Assert.AreEqual("/", response.Headers.Location.OriginalString);
   }
   ```

   ```
   [Test]
   public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
   {
       // Arrange
       var defaultPage = await _client.GetAsync("/");
       var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

       //Act
       var response = await _client.SendAsync(
           (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
           (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));

       // Assert
       Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
       Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
       Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
   }
   ```

Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's [data protection antiforgery system](../security/data-protection/introduction?view=aspnetcore-10.0). In order to arrange for a test's POST request, the test app must:

1. Make a request for the page.
2. Parse the antiforgery cookie and request validation token from the response.
3. Make the POST request with the antiforgery cookie and request validation token in place.

The `SendAsync` helper extension methods (`Helpers/HttpClientExtensions.cs`) and the `GetDocumentAsync` helper method (`Helpers/HtmlHelpers.cs`) in the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample/) use the [AngleSharp](https://anglesharp.github.io/) parser to handle the antiforgery check with the following methods:

* `GetDocumentAsync`: Receives the [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage) and returns an `IHtmlDocument`. `GetDocumentAsync` uses a factory that prepares a *virtual response* based on the original `HttpResponseMessage`. For more information, see the [AngleSharp documentation](https://github.com/AngleSharp/AngleSharp#documentation).

* `SendAsync` extension methods for the `HttpClient` compose an [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) and call [SendAsync(HttpRequestMessage)](/en-us/dotnet/api/system.net.http.httpclient.sendasync#system-net-http-httpclient-sendasync\(system-net-http-httprequestmessage\)) to submit requests to the SUT. Overloads for `SendAsync` accept the HTML form (`IHtmlFormElement`) and the following:

  * Submit button of the form (`IHtmlElement`)
  * Form values collection (`IEnumerable<KeyValuePair<string, string>>`)
  * Submit button (`IHtmlElement`) and form values (`IEnumerable<KeyValuePair<string, string>>`)

[AngleSharp](https://anglesharp.github.io/) is a **third-party parsing library used for demonstration purposes** in this article and the sample app. AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. Other parsers can be used, such as the [Html Agility Pack (HAP)](https://html-agility-pack.net/). Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly. See [AngleSharp vs `Application Parts` for antiforgery checks](#asap7) in this article for more information.

The [EF-Core in-memory database provider](/en-us/ef/core/testing/choosing-a-testing-strategy#in-memory-as-a-database-fake) can be used for limited and basic testing, however the ***[SQLite provider](/en-us/ef/core/testing/choosing-a-testing-strategy#sqlite-as-a-database-fake) is the recommended choice for in-memory testing***.

When a test requires a custom middleware, you can use an [IStartupFilter](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter). For more information, see [App startup in ASP.NET Core](../fundamentals/startup?view=aspnetcore-10.0#startup-filters).

When a test requires a custom service, see the [Inject mock services](#inject-mock-services) section.

## Customize the client with WithWebHostBuilder

When additional configuration is required within a test method, [WithWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.withwebhostbuilder) creates a new `WebApplicationFactory` with an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) that is further customized by configuration.

The [sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests) calls `WithWebHostBuilder` to replace configured services with test stubs. For more information and example usage, see [Inject mock services](#inject-mock-services) in this article.

The `Post_DeleteMessageHandler_ReturnsRedirectToRoot` test method of the [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample) demonstrates the use of `WithWebHostBuilder`. This test performs a record delete in the database by triggering a form submission in the SUT.

Because another test in the `IndexPageTests` class performs an operation that deletes all of the records in the database and may run before the `Post_DeleteMessageHandler_ReturnsRedirectToRoot` method, the database is reseeded in this test method to ensure that a record is present for the SUT to delete. Selecting the first delete button of the `messages` form in the SUT is simulated in the request to the SUT:

```
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}
```

```
[TestMethod]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    Assert.AreEqual("/", response.Headers.Location.OriginalString);
}
```

```
[Test]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
    Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
}
```

## Client options

See the [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) page for defaults and available options when creating `HttpClient` instances.

Create the `WebApplicationFactoryClientOptions` class and pass it to the [CreateClient()](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createclient#microsoft-aspnetcore-mvc-testing-webapplicationfactory-1-createclient) method:

```
public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }
```

```
[TestClass]
public class IndexPageTests
{
    private static HttpClient _client;
    private static CustomWebApplicationFactory<Program>
        _factory;

    [ClassInitialize]
    public static void AssemblyInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

    [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
    public static void AssemblyCleanup(TestContext _)
    {
        _factory.Dispose();
    }
```

```
public class IndexPageTests
{

    private HttpClient _client;
    private CustomWebApplicationFactory<Program>
        _factory;

    [SetUp]
    public void SetUp()
    {
        _factory = new CustomWebApplicationFactory<Program>();
        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

    [TearDown]
    public void TearDown()
    {
        _factory.Dispose();
        _client.Dispose();
    }
```

***NOTE:*** To avoid HTTPS redirection warnings in logs when using HTTPS redirection middleware, set `BaseAddress = new Uri("https://localhost")`

## Inject mock services

Services can be overridden in a test with a call to [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) on the host builder. To scope the overridden services to the test itself, the [WithWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.withwebhostbuilder) method is used to retrieve a host builder. This can be seen in the following tests:

* [Get\_QuoteService\_ProvidesQuoteInPage](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/IndexPageTests.cs#L166-L173)
* [Get\_GithubProfilePageCanGetAGithubUser](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs#L35-L38)
* [Get\_SecurePageIsReturnedForAnAuthenticatedUser](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/test/integration-tests/9.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/IntegrationTests/AuthTests.cs#L105-L117)

The sample SUT includes a scoped service that returns a quote. The quote is embedded in a hidden field on the Index page when the Index page is requested.

`Services/IQuoteService.cs`:

```
public interface IQuoteService
{
    Task<string> GenerateQuote();
}
```

`Services/QuoteService.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}
```

`Program.cs`:

```
services.AddScoped<IQuoteService, QuoteService>();
```

`Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }
```

`Pages/Index.cs`:

```
<input id="quote" type="hidden" value="@Model.Quote">
```

The following markup is generated when the SUT app is run:

```
<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">
```

To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. The mock service replaces the app's `QuoteService` with a service provided by the test app, called `TestQuoteService`:

`IntegrationTests.IndexPageTests.cs`:

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
```

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
```

```
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
```

`ConfigureTestServices` is called, and the scoped service is registered:

```
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
```

```
[TestMethod]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.AreEqual("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
```

```
[Test]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.That(quoteElement.Attributes["value"].Value, Is.EqualTo(
        "Something's interfering with time, Mr. Scarman, " +
        "and time is my business."));
}
```

The markup produced during the test's execution reflects the quote text supplied by `TestQuoteService`, thus the assertion passes:

```
<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">
```

## Mock authentication

Tests in the `AuthTests` class check that a secure endpoint:

* Redirects an unauthenticated user to the app's sign in page.
* Returns content for an authenticated user.

In the SUT, the `/SecurePage` page uses an [AuthorizePage](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizepage) convention to apply an [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) to the page. For more information, see [Razor Pages authorization conventions](../razor-pages/security/authorization/conventions?view=aspnetcore-10.0#require-authorization-to-access-a-page).

```
services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});
```

In the `Get_SecurePageRedirectsAnUnauthenticatedUser` test, a [WebApplicationFactoryClientOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions) is set to disallow redirects by setting [AllowAutoRedirect](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactoryclientoptions.allowautoredirect#microsoft-aspnetcore-mvc-testing-webapplicationfactoryclientoptions-allowautoredirect) to `false`:

```
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}
```

```
[TestMethod]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    StringAssert.StartsWith(response.Headers.Location.OriginalString, "http://localhost/Identity/Account/Login");
}
```

```
[Test]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
    Assert.That(response.Headers.Location.OriginalString, Does.StartWith("http://localhost/Identity/Account/Login"));
}
```

By disallowing the client to follow the redirect, the following checks can be made:

* The status code returned by the SUT can be checked against the expected [HttpStatusCode.Redirect](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-redirect) result, not the final status code after the redirect to the sign in page, which would be [HttpStatusCode.OK](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-ok).
* The `Location` header value in the response headers is checked to confirm that it starts with `http://localhost/Identity/Account/Login`, not the final sign in page response, where the `Location` header wouldn't be present.

The test app can mock an [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) in [ConfigureTestServices](/en-us/dotnet/api/microsoft.aspnetcore.testhost.webhostbuilderextensions.configuretestservices) in order to test aspects of authentication and authorization. A minimal scenario returns an [AuthenticateResult.Success](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticateresult.success):

```
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
```

```
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
```

```
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
```

The `TestAuthHandler` is called to authenticate a user when the authentication scheme is set to `TestScheme` where `AddAuthentication` is registered for `ConfigureTestServices`. `DefaultAuthenticateScheme` and `DefaultChallengeScheme` are explicitly set to `TestScheme` to ensure the test handler overrides any authentication configuration set by the app. It's important for the `TestScheme` scheme to match the scheme your app expects. Otherwise, authentication won't work.

```
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(options =>
                    {
                        options.DefaultAuthenticateScheme = "TestScheme";
                        options.DefaultChallengeScheme = "TestScheme";
                    })
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
```

```
[TestMethod]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = "TestScheme";
                    options.DefaultChallengeScheme = "TestScheme";
                })
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "TestScheme", options => { });
        });
    })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
```

```
[Test]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = "TestScheme";
                    options.DefaultChallengeScheme = "TestScheme";
                })
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "TestScheme", options => { });
        });
    })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
```

For more information on `WebApplicationFactoryClientOptions`, see the [Client options](#client-options) section.

### Basic tests for authentication middleware

See [this GitHub repository](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test) for basic tests of authentication middleware. It contains a [test server](https://github.com/blowdart/idunno.Authentication/blob/dev/test/idunno.Authentication.Basic.Test/BasicAuthenticationTests.cs#L331) that's specific to the test scenario.

## Set the environment

Set the [environment](../fundamentals/environments?view=aspnetcore-10.0) in the custom application factory:

```
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType == 
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
```

```
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
```

```
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
```

## How the test infrastructure infers the app content root path

The `WebApplicationFactory` constructor infers the app [content root](../fundamentals/?view=aspnetcore-10.0#content-root) path by searching for a [WebApplicationFactoryContentRootAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactorycontentrootattribute) on the assembly containing the integration tests with a key equal to the `TEntryPoint` assembly `System.Reflection.Assembly.FullName`. In case an attribute with the correct key isn't found, `WebApplicationFactory` falls back to searching for a solution file (*.sln*) and appends the `TEntryPoint` assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.

## Disable shadow copying

Shadow copying causes the tests to execute in a different directory than the output directory. If your tests rely on loading files relative to `Assembly.Location` and you encounter issues, you might have to disable shadow copying.

To disable shadow copying when using xUnit, create a `xunit.runner.json` file in your test project directory, with the [correct configuration setting](https://xunit.net/docs/configuration-files#shadowCopy):

```
{
  "shadowCopy": false
}
```

## Disposal of objects

After the tests of the `IClassFixture` implementation are executed, [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are disposed when xUnit disposes of the [`WebApplicationFactory`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1). If objects instantiated by the developer require disposal, dispose of them in the `IClassFixture` implementation. For more information, see [Implementing a Dispose method](/en-us/dotnet/standard/garbage-collection/implementing-dispose).

After the tests of the `TestClass` are executed, [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are disposed when MSTest disposes of the [`WebApplicationFactory`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) in the `ClassCleanup` method. If objects instantiated by the developer require disposal, dispose of them in the `ClassCleanup` method. For more information, see [Implementing a Dispose method](/en-us/dotnet/standard/garbage-collection/implementing-dispose).

After the tests of the test class are executed, [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are disposed when NUnit disposes of the [`WebApplicationFactory`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) in the `TearDown` method. If objects instantiated by the developer require disposal, dispose of them in the `TearDown` method. For more information, see [Implementing a Dispose method](/en-us/dotnet/standard/garbage-collection/implementing-dispose).

## Integration tests sample

The [sample app](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/9.x/IntegrationTestsSample) is composed of two apps:

| App                   | Project directory               | Description                                                         |
| --------------------- | ------------------------------- | ------------------------------------------------------------------- |
| Message app (the SUT) | `src/RazorPagesProject`         | Allows a user to add, delete one, delete all, and analyze messages. |
| Test app              | `tests/RazorPagesProject.Tests` | Used to integration test the SUT.                                   |

The tests can be run using the built-in test features of an IDE, such as [Visual Studio](https://visualstudio.microsoft.com). If using [Visual Studio Code](https://code.visualstudio.com/) or the command line, execute the following command at a command prompt in the `tests/RazorPagesProject.Tests` directory:

```
dotnet test
```

### Message app (SUT) organization

The SUT is a Razor Pages message system with the following characteristics:

* The Index page of the app (`Pages/Index.cshtml` and `Pages/Index.cshtml.cs`) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
* A message is described by the `Message` class (`Data/Message.cs`) with two properties: `Id` (key) and `Text` (message). The `Text` property is required and limited to 200 characters.
* Messages are stored using [Entity Framework's in-memory database](/en-us/ef/core/providers/in-memory/)†.
* The app contains a data access layer (DAL) in its database context class, `AppDbContext` (`Data/AppDbContext.cs`).
* If the database is empty on app startup, the message store is initialized with three messages.
* The app includes a `/SecurePage` that can only be accessed by an authenticated user.

†The EF article, [Test with InMemory](/en-us/ef/core/miscellaneous/testing/in-memory), explains how to use an in-memory database for tests with MSTest. This topic uses the [xUnit](https://xunit.net/) test framework. Test concepts and test implementations across different test frameworks are similar but not identical.

Although the app doesn't use the repository pattern and isn't an effective example of the [Unit of Work (UoW) pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html), Razor Pages supports these patterns of development. For more information, see [Designing the infrastructure persistence layer](/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) and [Test controller logic](../mvc/controllers/testing?view=aspnetcore-10.0) (the sample implements the repository pattern).

### Test app organization

The test app is a console app inside the `tests/RazorPagesProject.Tests` directory.

| Test app directory  | Description                                                                                                                                                                                                                                                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AuthTests`         | Contains test methods for:- Accessing a secure page by an unauthenticated user.
- Accessing a secure page by an authenticated user with a mock [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1).
- Obtaining a GitHub user profile and checking the profile's user login. |
| `BasicTests`        | Contains a test method for routing and content type.                                                                                                                                                                                                                                                                                       |
| `IntegrationTests`  | Contains the integration tests for the Index page using custom `WebApplicationFactory` class.                                                                                                                                                                                                                                              |
| `Helpers/Utilities` | * `Utilities.cs` contains the `InitializeDbForTests` method used to seed the database with test data.
* `HtmlHelpers.cs` provides a method to return an AngleSharp `IHtmlDocument` for use by the test methods.
* `HttpClientExtensions.cs` provide overloads for `SendAsync` to submit requests to the SUT.                               |

The test framework is [xUnit](https://xunit.net/). Integration tests are conducted using the [Microsoft.AspNetCore.TestHost](/en-us/dotnet/api/microsoft.aspnetcore.testhost), which includes the [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver). Because the [`Microsoft.AspNetCore.Mvc.Testing`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Testing) package is used to configure the test host and test server, the `TestHost` and `TestServer` packages don't require direct package references in the test app's project file or developer configuration in the test app.

Integration tests usually require a small dataset in the database prior to the test execution. For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.

The sample app seeds the database with three messages in `Utilities.cs` that tests can use when they execute:

```
public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}
```

The SUT's database context is registered in `Program.cs`. The test app's `builder.ConfigureServices` callback is executed *after* the app's `Program.cs` code is executed. To use a different database for the tests, the app's database context must be replaced in `builder.ConfigureServices`. For more information, see the [Customize WebApplicationFactory](#customize-webapplicationfactory) section.

## Related content

* [Unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test)
* [Razor Pages unit tests in ASP.NET Core](razor-pages-tests?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0)
* [Test controller logic in ASP.NET Core](../mvc/controllers/testing?view=aspnetcore-10.0)
* [Basic tests for authentication middleware](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/configure-trimmer
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/configure-trimmer.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure the Trimmer for ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to control the Intermediate Language (IL) Trimmer when building a Blazor app.

Blazor WebAssembly performs [Intermediate Language (IL)](/en-us/dotnet/standard/glossary#il) trimming to reduce the size of the published output. Trimming occurs when publishing an app.

## Default trimmer granularity

The default trimmer granularity for Blazor apps is `partial`, which means that only core framework libraries and libraries that have explicitly enabled trimming support are trimmed. Full trimming isn't supported.

For more information, see [Trimming options (.NET documentation)](/en-us/dotnet/core/deploying/trimming/trimming-options#trimming-granularity).

## Configuration

To configure the IL Trimmer, see the [Trimming options](/en-us/dotnet/core/deploying/trimming/trimming-options) article in the .NET Fundamentals documentation, which includes guidance on the following subjects:

* Disable trimming for the entire app with the `<PublishTrimmed>` property in the project file.
* Control how aggressively unused IL is discarded by the IL Trimmer.
* Stop the IL Trimmer from trimming specific assemblies.
* "Root" assemblies for trimming.
* Surface warnings for reflected types by setting the `<SuppressTrimAnalysisWarnings>` property to `false` in the project file.
* Control symbol trimming and debugger support.
* Set IL Trimmer features for trimming framework library features.

When the [trimmer granularity](#default-trimmer-granularity) is `partial`, which is the default value, the IL Trimmer trims the base class library and any other assemblies marked as trimmable. To opt into trimming in any of the app's class library projects, set the `<IsTrimmable>` MSBuild property to `true` in those projects:

```
<PropertyGroup>
  <IsTrimmable>true</IsTrimmable>
</PropertyGroup>
```

For guidance that pertains to .NET libraries, see [Prepare .NET libraries for trimming](/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming).

## Failure to preserve types used by a published app

Trimming may have detrimental effects for a published app leading to runtime errors, even in spite of setting the [`<PublishTrimmed>` property](#configuration) to `false` in the project file. In apps that use [reflection](/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/), the IL Trimmer often can't determine the required types for runtime reflection and trims them away or trims away parameter names from methods. This can happen with complex framework types used for JS interop, JSON serialization/deserialization, and other operations.

The IL Trimmer is also unable to react to an app's dynamic behavior at runtime. To ensure the trimmed app works correctly once deployed, test published output frequently while developing.

Consider the following example that performs JSON deserialization into a [Tuple\<T1,T2>](/en-us/dotnet/api/system.tuple-2) collection (`List<Tuple<string, string>>`).

`TrimExample.razor`:

```
@page "/trim-example"
@using System.Diagnostics.CodeAnalysis
@using System.Text.Json

<h1>Trim Example</h1>

<ul>
    @foreach (var item in @items)
    {
        <li>@item.Item1, @item.Item2</li>
    }
</ul>

@code {
    private List<Tuple<string, string>> items = [];

    [StringSyntax(StringSyntaxAttribute.Json)]
    private const string data =
        """[{"item1":"1:T1","item2":"1:T2"},{"item1":"2:T1","item2":"2:T2"}]""";

    protected override void OnInitialized()
    {
        JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true };

        items = JsonSerializer
            .Deserialize<List<Tuple<string, string>>>(data, options)!;
    }
}
```

The preceding component executes normally when the app is run locally and produces the following rendered list:

> • 1:T1, 1:T2\
> • 2:T2, 2:T2

When the app is published, [Tuple\<T1,T2>](/en-us/dotnet/api/system.tuple-2) is trimmed from the app, even in spite of setting the `<PublishTrimmed>` property to `false` in the project file. Accessing the component throws the following exception:

> crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer\[100]\
> Unhandled exception rendering component: ConstructorContainsNullParameterNames, System.Tuple\`2\[System.String,System.String]\
> System.NotSupportedException: ConstructorContainsNullParameterNames, System.Tuple\`2\[System.String,System.String]

To address lost types, consider adopting one of the following approaches.

### Custom types

To avoid issues with .NET trimming in scenarios that rely on reflection, such as JS interop and JSON serialization, use custom types defined in non-trimmable libraries or preserve the types via linker configuration.

The following modifications create a `StringTuple` type for use by the component.

`StringTuple.cs`:

```
[method: SetsRequiredMembers]
public sealed class StringTuple(string item1, string item2)
{
    public required string Item1 { get; init; } = item1;
    public required string Item2 { get; init; } = item2;
}
```

The component is modified to use the `StringTuple` type:

```
- private List<Tuple<string, string>> items = [];
+ private List<StringTuple> items = [];
```

```
- items = JsonSerializer.Deserialize<List<Tuple<string, string>>>(data, options)!;
+ items = JsonSerializer.Deserialize<List<StringTuple>>(data, options)!;
```

Because custom types defined in non-trimmable assemblies aren't trimmed by Blazor when an app is published, the component works as designed after the app is published.

If you prefer to use framework types in spite of our recommendation, use either of the following approaches:

* [Preserve the type as a dynamic dependency](#preserve-the-type-as-a-dynamic-dependency)
* [Use a Root Descriptor](#use-a-root-descriptor)

If you prefer to use framework types in spite of our recommendation, [preserve the type as a dynamic dependency](#preserve-the-type-as-a-dynamic-dependency).

### Preserve the type as a dynamic dependency

Create a dynamic dependency to preserve the type with the [`[DynamicDependency]` attribute](/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute).

If not already present, add an `@using` directive for [System.Diagnostics.CodeAnalysis](/en-us/dotnet/api/system.diagnostics.codeanalysis):

```
@using System.Diagnostics.CodeAnalysis
```

Add a [`[DynamicDependency]` attribute](/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute) to preserve the [Tuple\<T1,T2>](/en-us/dotnet/api/system.tuple-2):

```
+ [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, 
+     typeof(Tuple<string, string>))]
private List<Tuple<string, string>> items = [];
```

### Use a Root Descriptor

A [Root Descriptor](/en-us/dotnet/core/deploying/trimming/trimming-options#root-descriptors) can preserve the type.

Add an `ILLink.Descriptors.xml` file to the root of the app† with the type:

```
<linker>
  <assembly fullname="System.Private.CoreLib">
    <type fullname="System.Tuple`2" preserve="all" />
  </assembly>
</linker>
```

†The root of the app refers to the root of the Blazor WebAssembly app or the root of the `.Client` project of a Blazor Web App (.NET 8 or later).

Add a `TrimmerRootDescriptor` item to the app's project file‡ referencing the `ILLink.Descriptors.xml` file:

```
<ItemGroup>
  <TrimmerRootDescriptor Include="$(MSBuildThisFileDirectory)ILLink.Descriptors.xml" />
</ItemGroup>
```

‡The project file is either the project file of the Blazor WebAssembly app or the project file of the `.Client` project of a Blazor Web App (.NET 8 or later).

### Workaround in .NET 8

As a workaround in .NET 8, you can add the `_ExtraTrimmerArgs` MSBuild property set to `--keep-metadata parametername` in the app's project file to preserve parameter names during trimming:

```
<PropertyGroup>
  <_ExtraTrimmerArgs>--keep-metadata parametername</_ExtraTrimmerArgs>
</PropertyGroup>
```

## Related content

* [Trim self-contained deployments and executables](/en-us/dotnet/core/deploying/trimming/trim-self-contained)
* [Prepare .NET libraries for trimming](/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming)
* [ASP.NET Core Blazor app download size performance best practices](../performance/app-download-size?view=aspnetcore-10.0#intermediate-language-il-trimming)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0018
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0018.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0018: Unused route parameter

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0018      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A route parameter is specified but not used.

## Rule description

A route parameter is specified but not used. In the example below, the `id` parameter is defined in the route but not in the route handler.

```
var app = WebApplication.Create();

app.MapGet("/{id}", () => ...);
```

## How to fix violations

To fix a violation of this rule, remove the route parameter or add code that uses the parameter.

```
var app = WebApplication.Create();

app.MapGet("/{id}", (string id) => ...);
```

## When to suppress warnings

In general, do ***not*** suppress a warning from this rule without validating the route parameter is used. Currently properties within the bound model for [FromRoute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute) attributes may not be analyzed. For more information, see [GitHub issue `#54212`](https://github.com/dotnet/aspnetcore/issues/54212).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/integration-hosted-webassembly
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/integration-hosted-webassembly.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Integrate ASP.NET Core Razor components with MVC or Razor Pages in hosted Blazor WebAssembly solutions

Note

Hosted Blazor WebAssembly solutions remain supported, but the project template was removed and is no longer supported in .NET 8 or later. This article appears in the table of contents up to .NET 7 for reference, but note that .NET 7 is a [Standard Support Term](https://dotnet.microsoft.com/platform/support/policy/dotnet-core) release that's no longer supported.

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core).

This article explains Razor component integration scenarios for hosted Blazor WebAssembly apps, including prerendering of Razor components on the server.

Important

Framework changes across ASP.NET Core releases led to different sets of instructions in this article. Before using this article's guidance, confirm that the document version selector at the top of this article matches the version of ASP.NET Core that you intend to use for your app.

Prerendering can improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines can use to calculate page rank.

## Solution configuration

### Prerendering configuration

To set up prerendering for a hosted Blazor WebAssembly app:

1. Host the Blazor WebAssembly app in an ASP.NET Core app. A standalone Blazor WebAssembly app can be added to an ASP.NET Core solution, or you can use a hosted Blazor WebAssembly app created from the [Blazor WebAssembly project template](../tooling?view=aspnetcore-7.0) with the hosted option:

   * Visual Studio: In the **Additional information** dialog, select the **ASP.NET Core Hosted** checkbox when creating the Blazor WebAssembly app. In this article's examples, the solution is named `BlazorHosted`.
   * Visual Studio Code/.NET CLI command shell: `dotnet new blazorwasm -ho` (use the `-ho|--hosted` option). Use the `-o|--output {LOCATION}` option to create a folder for the solution and set the solution's project namespaces. In this article's examples, the solution is named `BlazorHosted` (`dotnet new blazorwasm -ho -o BlazorHosted`).

   For the examples in this article, the hosted solution's name (assembly name) is `BlazorHosted`. The client project's namespace is `BlazorHosted.Client`, and the server project's namespace is `BlazorHosted.Server`.

2. **Delete** the `wwwroot/index.html` file from the Blazor WebAssembly **Client** project.

3. In the **Client** project, **delete** the following lines in `Program.cs`:

   ```
   - builder.RootComponents.Add<App>("#app");
   - builder.RootComponents.Add<HeadOutlet>("head::after");
   ```

4. Add `_Host.cshtml` file to the **Server** project's `Pages` folder. You can obtain the files from a project created from the Blazor Server template using Visual Studio or using the .NET CLI with the `dotnet new blazorserver -o BlazorServer` command in a command shell (the `-o BlazorServer` option creates a folder for the project). After placing the files into the **Server** project's `Pages` folder, make the following changes to the files.

   Make the following changes to the `_Host.cshtml` file:

   * Update the `Pages` namespace at the top of the file to match the namespace of the **Server** app's pages. The `{APP NAMESPACE}` placeholder in the following example represents the namespace of the donor app's pages that provided the `_Host.cshtml` file:

     Delete:

     ```
     - @namespace {APP NAMESPACE}.Pages
     ```

     Add:

     ```
     @namespace BlazorHosted.Server.Pages
     ```

   * Add an [`@using`](../../mvc/views/razor?view=aspnetcore-7.0#using) directive for the **Client** project at the top of the file:

     ```
     @using BlazorHosted.Client
     ```

   * Update the stylesheet links to point to the WebAssembly project's stylesheets. In the following example, the client project's namespace is `BlazorHosted.Client`. The `{APP NAMESPACE}` placeholder represents the namespace of the donor app that provided the `_Host.cshtml` file. Update the Component Tag Helper (`<component>` tag) for the `HeadOutlet` component to prerender the component.

     Delete:

     ```
     - <link href="css/site.css" rel="stylesheet" />
     - <link href="{APP NAMESPACE}.styles.css" rel="stylesheet" />
     - <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
     ```

     Add:

     ```
     <link href="css/app.css" rel="stylesheet" />
     <link href="BlazorHosted.Client.styles.css" rel="stylesheet" />
     <component type="typeof(HeadOutlet)" render-mode="WebAssemblyPrerendered" />
     ```

     Note

     Leave the `<link>` element that requests the Bootstrap stylesheet (`css/bootstrap/bootstrap.min.css`) in place.

   * Update the Blazor script source to use the client-side Blazor WebAssembly script:

     Delete:

     ```
     - <script src="_framework/blazor.server.js"></script>
     ```

     Add:

     ```
     <script src="_framework/blazor.webassembly.js"></script>
     ```

   * Update the `render-mode` of the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) to prerender the root `App` component with [WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered):

     Delete:

     ```
     - <component type="typeof(App)" render-mode="ServerPrerendered" />
     ```

     Add:

     ```
     <component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
     ```

     Important

     Prerendering isn't supported for authentication endpoints (`/authentication/` path segment). For more information, see [ASP.NET Core Blazor WebAssembly additional security scenarios](../security/webassembly/additional-scenarios?view=aspnetcore-7.0#prerendering-with-authentication).

5. In the `Program.cs` file of the **Server** project, change the fallback endpoint from the `index.html` file to the `_Host.cshtml` page:

   Delete:

   ```
   - app.MapFallbackToFile("index.html");
   ```

   Add:

   ```
   app.MapFallbackToPage("/_Host");
   ```

6. If the **Client** and **Server** projects use one or more common services during prerendering, factor the service registrations into a method that can be called from both projects. For more information, see [ASP.NET Core Blazor dependency injection](../fundamentals/dependency-injection?view=aspnetcore-7.0#register-common-services).

7. Run the **Server** project. The hosted Blazor WebAssembly app is prerendered by the **Server** project for clients.

### Configuration for embedding Razor components into pages or views

The following sections and examples for embedding Razor components from the **Client** Blazor WebAssembly app into pages or views of the server app require additional configuration.

The **Server** project must have the following files and folders.

Razor Pages:

* `Pages/Shared/_Layout.cshtml`
* `Pages/Shared/_Layout.cshtml.css`
* `Pages/_ViewImports.cshtml`
* `Pages/_ViewStart.cshtml`

MVC:

* `Views/Shared/_Layout.cshtml`
* `Views/Shared/_Layout.cshtml.css`
* `Views/_ViewImports.cshtml`
* `Views/_ViewStart.cshtml`

The preceding files can be obtained by generating an app from the ASP.NET Core project templates using:

* Visual Studio's new project creation tools.
* Opening a command shell and executing `dotnet new webapp -o {PROJECT NAME}` (Razor Pages) or `dotnet new mvc -o {PROJECT NAME}` (MVC). The option `-o|--output` with a value for the `{PROJECT NAME}` placeholder provides a name for the app and creates a folder for the app.

Update the namespaces in the imported `_ViewImports.cshtml` file to match those in use by the **Server** project receiving the files.

`Pages/_ViewImports.cshtml` (Razor Pages):

```
@using BlazorHosted.Server
@namespace BlazorHosted.Server.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

`Views/_ViewImports.cshtml` (MVC):

```
@using BlazorHosted.Server
@using BlazorHosted.Server.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

Update the imported layout file, which is `Pages/Shared/_Layout.cshtml` for Razor Pages or `Views/Shared/_Layout.cshtml` for MVC.

First, delete the title and the stylesheet from the donor project, which is `RPDonor.styles.css` in the following example. The `{PROJECT NAME}` placeholder represents the donor project's app name.

```
- <title>@ViewData["Title"] - {PROJECT NAME}</title>
- <link rel="stylesheet" href="~/RPDonor.styles.css" asp-append-version="true" />
```

Include the **Client** project's styles in the layout file. In the following example, the **Client** project's namespace is `BlazorHosted.Client`. The `<title>` element can be updated at the same time.

Place the following lines in the `<head>` content of the layout file:

```
<title>@ViewData["Title"] - BlazorHosted</title>
<link href="css/app.css" rel="stylesheet" />
<link rel="stylesheet" href="BlazorHosted.Client.styles.css" asp-append-version="true" />
<component type="typeof(HeadOutlet)" render-mode="WebAssemblyPrerendered" />
```

The imported layout contains two `Home` (`Index` page) and `Privacy` navigation links. To make the `Home` links point to the hosted Blazor WebAssembly app, change the hyperlinks:

```
- <a class="navbar-brand" asp-area="" asp-page="/Index">{PROJECT NAME}</a>
+ <a class="navbar-brand" href="/">BlazorHosted</a>
```

```
- <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
+ <a class="nav-link text-dark" href="/">Home</a>
```

In an MVC layout file:

```
- <a class="navbar-brand" asp-area="" asp-controller="Home" 
-     asp-action="Index">{PROJECT NAME}</a>
+ <a class="navbar-brand" href="/">BlazorHosted</a>
```

```
- <a class="nav-link text-dark" asp-area="" asp-controller="Home" 
-     asp-action="Index">Home</a>
+ <a class="nav-link text-dark" href="/">Home</a>
```

Update the `<footer>` element's app name. The following example uses the app name `BlazorHosted`:

```
- &copy; {DATE} - {DONOR NAME} - <a asp-area="" asp-page="/Privacy">Privacy</a>
+ &copy; {DATE} - BlazorHosted - <a asp-area="" asp-page="/Privacy">Privacy</a>
```

In the preceding example, the `{DATE}` placeholder represents the copyright date in an app generated from the Razor Pages or MVC project template.

To make the `Privacy` link lead to a privacy page (Razor Pages), add a privacy page to the **Server** project.

`Pages/Privacy.cshtml` in the **Server** project:

```
@page
@model PrivacyModel
@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>
```

For an MVC-based privacy view, create a privacy view in the **Server** project.

`View/Home/Privacy.cshtml` in the **Server** project:

```
@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>
```

In the `Home` controller of the MVC app, return the view.

Add the following code to `Controllers/HomeController.cs`:

```
public IActionResult Privacy()
{
    return View();
}
```

If you import files from a donor app, be sure to update any namespaces in the files to match that of the **Server** project (for example, `BlazorHosted.Server`).

Import static assets to the **Server** project from the donor project's `wwwroot` folder:

* `wwwroot/css` folder and contents
* `wwwroot/js` folder and contents
* `wwwroot/lib` folder and contents

If the donor project is created from an ASP.NET Core project template and the files aren't modified, you can copy the entire `wwwroot` folder from the donor project into the **Server** project and remove the favicon icon file.

Warning

Avoid placing the static asset into both the **Client** and **Server** `wwwroot` folders. If the same file is present in both folders, an exception is thrown because the static assets share the same web root path. Therefore, host a static asset in either of the `wwwroot` folders, not both.

After adopting the preceding configuration, embed Razor components into pages or views of the **Server** project. Use the guidance in the following sections of this article:

* *Render components in a page or view with the Component Tag Helper*
* *Render components in a page or view with a CSS selector*

## Render components in a page or view with the Component Tag Helper

After [configuring the solution](#solution-configuration), including the [additional configuration](#configuration-for-embedding-razor-components-into-pages-or-views), the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) supports two render modes for rendering a component from a Blazor WebAssembly app in a page or view:

* [WebAssembly](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassembly)
* [WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered)

In the following Razor Pages example, the `Counter` component is rendered in a page. To make the component interactive, the Blazor WebAssembly script is included in the page's [render section](../../mvc/views/layout?view=aspnetcore-7.0#sections). To avoid using the full namespace for the `Counter` component with the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) (`{ASSEMBLY NAME}.Pages.Counter`), add an [`@using`](../../mvc/views/razor?view=aspnetcore-7.0#using) directive for the client project's `Pages` namespace. In the following example, the **Client** project's namespace is `BlazorHosted.Client`.

In the **Server** project, `Pages/RazorPagesCounter1.cshtml`:

```
@page
@using BlazorHosted.Client.Pages

<component type="typeof(Counter)" render-mode="WebAssemblyPrerendered" />

@section Scripts {
    <script src="_framework/blazor.webassembly.js"></script>
}
```

Run the **Server** project. Navigate to the Razor page at `/razorpagescounter1`. The prerendered `Counter` component is embedded in the page.

[RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the component:

* Is prerendered into the page.
* Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0).

Additional work might be required depending on the static resources that components use and how layout pages are organized in an app. Typically, scripts are added to a page or view's `Scripts` render section and stylesheets are added to the layout's `<head>` element content.

### Set child content through a render fragment

The [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) doesn't support receiving a [`RenderFragment` delegate for child content](./?view=aspnetcore-7.0#child-content-render-fragments) (for example, `param-ChildContent="..."`). We recommend creating a Razor component (`.razor`) that references the component you want to render with the child content you want to pass and then invoke the Razor component from the page or view.

### Ensure that top-level prerendered components aren't trimmed out on publish

If a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) directly references a component from a library that's subject to trimming on publish, the component might be trimmed out during publish because there are no references to it from client-side app code. As a result, the component isn't prerendered, leaving a blank spot in the output. If this occurs, instruct the trimmer to preserve the library component by adding a [`DynamicDependency` attribute](/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute) to any class in the client-side app. To preserve a component called `SomeLibraryComponentToBePreserved`, add the following to any component:

```
@using System.Diagnostics.CodeAnalysis
@attribute [DynamicDependency(DynamicallyAccessedMemberTypes.All, 
    typeof(SomeLibraryComponentToBePreserved))]
```

The preceding approach usually isn't required because the app usually prerenders its components (which are not trimmed), which in turn references components from libraries (causing them also not to be trimmed). Only use `DynamicDependency` explicitly for prerendering a library component directly when the library is subject to trimming.

## Render components in a page or view with a CSS selector

After [configuring the solution](#solution-configuration), including the [additional configuration](#configuration-for-embedding-razor-components-into-pages-or-views), add root components to the **Client** project of a hosted Blazor WebAssembly solution in the `Program.cs` file. In the following example, the `Counter` component is declared as a root component with a CSS selector that selects the element with the `id` that matches `counter-component`. In the following example, the **Client** project's namespace is `BlazorHosted.Client`.

In `Program.cs` file of the **Client** project, add the namespace for the project's Razor components to the top of the file:

```
using BlazorHosted.Client.Pages;
```

After the `builder` is established in `Program.cs`, add the `Counter` component as a root component:

```
builder.RootComponents.Add<Counter>("#counter-component");
```

In the following Razor Pages example, the `Counter` component is rendered in a page. To make the component interactive, the Blazor WebAssembly script is included in the page's [render section](../../mvc/views/layout?view=aspnetcore-7.0#sections).

In the **Server** project, `Pages/RazorPagesCounter2.cshtml`:

```
@page

<div id="counter-component">Loading...</div>

@section Scripts {
    <script src="_framework/blazor.webassembly.js"></script>
}
```

Run the **Server** project. Navigate to the Razor page at `/razorpagescounter2`. The prerendered `Counter` component is embedded in the page.

Additional work might be required depending on the static resources that components use and how layout pages are organized in an app. Typically, scripts are added to a page or view's `Scripts` render section and stylesheets are added to the layout's `<head>` element content.

Note

The preceding example throws a [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) if a Blazor WebAssembly app is prerendered and integrated into a Razor Pages or MVC app **simultaneously** with the use of a CSS selector. Navigating to one of the **Client** project's Razor components or navigating to a page or view of the **Server** with an embedded component throws one or more [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception).

This is normal behavior because prerendering and integrating a Blazor WebAssembly app with routable Razor components is incompatible with the use of CSS selectors.

If you've been working with the examples in the preceding sections and just wish to see the CSS selector work in your sample app, comment out the specification of the `App` root component of the **Client** project's `Program.cs` file:

```
- builder.RootComponents.Add<App>("#app");
+ //builder.RootComponents.Add<App>("#app");
```

Navigate to the page or view with the embedded Razor component that uses a CSS selector (for example, `/razorpagescounter2` of the preceding example). The page or view loads with the embedded component, and the embedded component functions as expected.

## Persist prerendered state

Without persisting prerendered state, state used during prerendering is lost and must be recreated when the app is fully loaded. If any state is setup asynchronously, the UI may flicker as the prerendered UI is replaced with temporary placeholders and then fully rendered again.

To persist state for prerendered components, use the [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-7.0) ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs)). Add the Tag Helper's tag, `<persist-component-state />`, inside the closing `</body>` tag of the `_Host` page in an app that prerenders components.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

In `Pages/_Host.cshtml` of Blazor apps that are WebAssembly prerendered (`WebAssemblyPrerendered`) in a hosted Blazor WebAssembly app:

```
<body>
    ...

    <persist-component-state />
</body>
```

Decide what state to persist using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service. [PersistentComponentState.RegisterOnPersisting](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) registers a callback to persist the component state before the app is paused. The state is retrieved when the application resumes. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown.

In the following example:

* The `{TYPE}` placeholder represents the type of data to persist (for example, `WeatherForecast[]`).
* The `{TOKEN}` placeholder is a state identifier string (for example, `fetchdata`).

```
@implements IDisposable
@inject PersistentComponentState ApplicationState

...

@code {
    private {TYPE} data;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<{TYPE}>(
            "{TOKEN}", out var restored))
        {
            data = await ...;
        }
        else
        {
            data = restored!;
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson("{TOKEN}", data);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}
```

The following example is an updated version of the `FetchData` component in a hosted Blazor WebAssembly app based on the Blazor project template. The `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-7.0) persists the component state after all component invocations.

`Pages/WeatherForecastPreserveState.razor`:

```
@page "/weather-forecast-preserve-state"
@using BlazorSample.Shared
@implements IDisposable
@inject IWeatherForecastService WeatherForecastService
@inject PersistentComponentState ApplicationState

<PageTitle>Weather Forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts = Array.Empty<WeatherForecast>();
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<WeatherForecast[]>(
            nameof(forecasts), out var restored))
        {
            forecasts = await WeatherForecastService.GetForecastAsync(
                DateOnly.FromDateTime(DateTime.Now));
        }
        else
        {
            forecasts = restored!;
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson(nameof(forecasts), forecasts);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}
```

By initializing components with the same state used during prerendering, any expensive initialization steps are only executed once. The rendered UI also matches the prerendered UI, so no flicker occurs in the browser.

The persisted prerendered state is transferred to the client, where it's used to restore the component state. For prerendering in a hosted Blazor WebAssembly app, the data is exposed to the browser and must not contain sensitive, private information.

## Additional Blazor WebAssembly resources

* [Prerendering support with assembly lazy loading](../webassembly-lazy-load-assemblies?view=aspnetcore-7.0#lazy-load-assemblies-in-a-hosted-blazor-webassembly-solution)

* Razor component lifecycle subjects that pertain to prerendering

  * [Component initialization (`OnInitialized{Async}`)](lifecycle?view=aspnetcore-7.0#component-initialization-oninitializedasync)
  * [After component render (`OnAfterRender{Async}`)](lifecycle?view=aspnetcore-7.0#after-component-render-onafterrenderasync)
  * [Stateful reconnection after prerendering](lifecycle?view=aspnetcore-7.0#stateful-reconnection-after-prerendering): Although the content in the section focuses on Blazor Server and stateful SignalR *reconnection*, the scenario for prerendering in hosted Blazor WebAssembly apps ([WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered)) involves similar conditions and approaches to prevent executing developer code twice. To preserve state during the execution of initialization code while prerendering, see *Persist prerendered state* section of this article.
  * [Prerendering with JavaScript interop](lifecycle?view=aspnetcore-7.0#prerendering-with-javascript-interop)

* Authentication and authorization subjects that pertain to prerendering

  * [General aspects](../security/?view=aspnetcore-7.0)
  * [Prerendering with authentication in hosted Blazor WebAssembly apps](../security/webassembly/additional-scenarios?view=aspnetcore-7.0#prerendering-with-authentication)

* [Host and deploy ASP.NET Core Blazor WebAssembly](../host-and-deploy/webassembly/?view=aspnetcore-7.0)

* [Handle errors: Prerendering](../fundamentals/handle-errors?view=aspnetcore-7.0#prerendering)

* [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) is executed *twice* when prerendering: [Handle asynchronous navigation events with `OnNavigateAsync`](../fundamentals/routing?view=aspnetcore-7.0#handle-asynchronous-navigation-events-with-onnavigateasync)

Prerendering can improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines can use to calculate page rank.

## Solution configuration

### Prerendering configuration

To set up prerendering for a hosted Blazor WebAssembly app:

1. Host the Blazor WebAssembly app in an ASP.NET Core app. A standalone Blazor WebAssembly app can be added to an ASP.NET Core solution, or you can use a hosted Blazor WebAssembly app created from the [Blazor WebAssembly project template](../tooling?view=aspnetcore-7.0) with the hosted option:

   * Visual Studio: In the **Additional information** dialog, select the **ASP.NET Core Hosted** checkbox when creating the Blazor WebAssembly app. In this article's examples, the solution is named `BlazorHosted`.
   * Visual Studio Code/.NET CLI command shell: `dotnet new blazorwasm -ho` (use the `-ho|--hosted` option). Use the `-o|--output {LOCATION}` option to create a folder for the solution and set the solution's project namespaces. In this article's examples, the solution is named `BlazorHosted` (`dotnet new blazorwasm -ho -o BlazorHosted`).

   For the examples in this article, the client project's namespace is `BlazorHosted.Client`, and the server project's namespace is `BlazorHosted.Server`.

2. **Delete** the `wwwroot/index.html` file from the Blazor WebAssembly **Client** project.

3. In the **Client** project, **delete** the following lines in `Program.cs`:

   ```
   - builder.RootComponents.Add<App>("#app");
   - builder.RootComponents.Add<HeadOutlet>("head::after");
   ```

4. Add `_Host.cshtml` and `_Layout.cshtml` files to the **Server** project's `Pages` folder. You can obtain the files from a project created from the Blazor Server template using Visual Studio or using the .NET CLI with the `dotnet new blazorserver -o BlazorServer` command in a command shell (the `-o BlazorServer` option creates a folder for the project). After placing the files into the **Server** project's `Pages` folder, make the following changes to the files.

   Important

   The use of a layout page (`_Layout.cshtml`) with a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) for a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is required to control `<head>` content, such as the page's title ([PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component) and other head elements ([HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component). For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-7.0#control-head-content-during-prerendering).

   Make the following changes to the `_Layout.cshtml` file:

   * Update the `Pages` namespace at the top of the file to match the namespace of the **Server** app's pages. The `{APP NAMESPACE}` placeholder in the following example represents the namespace of the donor app's pages that provided the `_Layout.cshtml` file:

     Delete:

     ```
     - @namespace {APP NAMESPACE}.Pages
     ```

     Add:

     ```
     @namespace BlazorHosted.Server.Pages
     ```

   * Add an [`@using`](../../mvc/views/razor?view=aspnetcore-7.0#using) directive for the **Client** project at the top of the file:

     ```
     @using BlazorHosted.Client
     ```

   * Update the stylesheet links to point to the WebAssembly project's stylesheets. In the following example, the client project's namespace is `BlazorHosted.Client`. The `{APP NAMESPACE}` placeholder represents the namespace of the donor app that provided the `_Layout.cshtml` file. Update the Component Tag Helper (`<component>` tag) for the `HeadOutlet` component to prerender the component.

     Delete:

     ```
     - <link href="css/site.css" rel="stylesheet" />
     - <link href="{APP NAMESPACE}.styles.css" rel="stylesheet" />
     - <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
     ```

     Add:

     ```
     <link href="css/app.css" rel="stylesheet" />
     <link href="BlazorHosted.Client.styles.css" rel="stylesheet" />
     <component type="typeof(HeadOutlet)" render-mode="WebAssemblyPrerendered" />
     ```

     Note

     Leave the `<link>` element that requests the Bootstrap stylesheet (`css/bootstrap/bootstrap.min.css`) in place.

   * Update the Blazor script source to use the client-side Blazor WebAssembly script:

     Delete:

     ```
     - <script src="_framework/blazor.server.js"></script>
     ```

     Add:

     ```
     <script src="_framework/blazor.webassembly.js"></script>
     ```

   In the `_Host.cshtml` file:

   * Change the `Pages` namespace to that of the **Client** project. The `{APP NAMESPACE}` placeholder represents the namespace of the donor app's pages that provided the `_Host.cshtml` file:

     Delete:

     ```
     - @namespace {APP NAMESPACE}.Pages
     ```

     Add:

     ```
     @namespace BlazorHosted.Client
     ```

   * Update the `render-mode` of the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) to prerender the root `App` component with [WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered):

     Delete:

     ```
     - <component type="typeof(App)" render-mode="ServerPrerendered" />
     ```

     Add:

     ```
     <component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
     ```

     Important

     Prerendering isn't supported for authentication endpoints (`/authentication/` path segment). For more information, see [ASP.NET Core Blazor WebAssembly additional security scenarios](../security/webassembly/additional-scenarios?view=aspnetcore-7.0#prerendering-with-authentication).

5. In endpoint mapping of the **Server** project in `Program.cs`, change the fallback from the `index.html` file to the `_Host.cshtml` page:

   Delete:

   ```
   - app.MapFallbackToFile("index.html");
   ```

   Add:

   ```
   app.MapFallbackToPage("/_Host");
   ```

6. If the **Client** and **Server** projects use one or more common services during prerendering, factor the service registrations into a method that can be called from both projects. For more information, see [ASP.NET Core Blazor dependency injection](../fundamentals/dependency-injection?view=aspnetcore-7.0#register-common-services).

7. Run the **Server** project. The hosted Blazor WebAssembly app is prerendered by the **Server** project for clients.

### Configuration for embedding Razor components into pages or views

The following sections and examples for embedding Razor components from the **Client** Blazor WebAssembly app into pages or views of the server app require additional configuration.

The **Server** project must have the following files and folders.

Razor Pages:

* `Pages/Shared/_Layout.cshtml`
* `Pages/Shared/_Layout.cshtml.css`
* `Pages/_ViewImports.cshtml`
* `Pages/_ViewStart.cshtml`

MVC:

* `Views/Shared/_Layout.cshtml`
* `Views/Shared/_Layout.cshtml.css`
* `Views/_ViewImports.cshtml`
* `Views/_ViewStart.cshtml`

Important

The use of a layout page (`_Layout.cshtml`) with a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) for a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is required to control `<head>` content, such as the page's title ([PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component) and other head elements ([HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component). For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-7.0#control-head-content-during-prerendering).

The preceding files can be obtained by generating an app from the ASP.NET Core project templates using:

* Visual Studio's new project creation tools.
* Opening a command shell and executing `dotnet new webapp -o {PROJECT NAME}` (Razor Pages) or `dotnet new mvc -o {PROJECT NAME}` (MVC). The option `-o|--output` with a value for the `{PROJECT NAME}` placeholder provides a name for the app and creates a folder for the app.

Update the namespaces in the imported `_ViewImports.cshtml` file to match those in use by the **Server** project receiving the files.

`Pages/_ViewImports.cshtml` (Razor Pages):

```
@using BlazorHosted.Server
@namespace BlazorHosted.Server.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

`Views/_ViewImports.cshtml` (MVC):

```
@using BlazorHosted.Server
@using BlazorHosted.Server.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

Update the imported layout file, which is `Pages/Shared/_Layout.cshtml` for Razor Pages or `Views/Shared/_Layout.cshtml` for MVC.

First, delete the title and the stylesheet from the donor project, which is `RPDonor.styles.css` in the following example. The `{PROJECT NAME}` placeholder represents the donor project's app name.

```
- <title>@ViewData["Title"] - {PROJECT NAME}</title>
- <link rel="stylesheet" href="~/RPDonor.styles.css" asp-append-version="true" />
```

Include the **Client** project's styles in the layout file. In the following example, the **Client** project's namespace is `BlazorHosted.Client`. The `<title>` element can be updated at the same time.

Place the following lines in the `<head>` content of the layout file:

```
<title>@ViewData["Title"] - BlazorHosted</title>
<link href="css/app.css" rel="stylesheet" />
<link rel="stylesheet" href="BlazorHosted.Client.styles.css" asp-append-version="true" />
<component type="typeof(HeadOutlet)" render-mode="WebAssemblyPrerendered" />
```

The imported layout contains two `Home` (`Index` page) and `Privacy` navigation links. To make the `Home` links point to the hosted Blazor WebAssembly app, change the hyperlinks:

```
- <a class="navbar-brand" asp-area="" asp-page="/Index">{PROJECT NAME}</a>
+ <a class="navbar-brand" href="/">BlazorHosted</a>
```

```
- <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
+ <a class="nav-link text-dark" href="/">Home</a>
```

In an MVC layout file:

```
- <a class="navbar-brand" asp-area="" asp-controller="Home" 
-     asp-action="Index">{PROJECT NAME}</a>
+ <a class="navbar-brand" href="/">BlazorHosted</a>
```

```
- <a class="nav-link text-dark" asp-area="" asp-controller="Home" 
-     asp-action="Index">Home</a>
+ <a class="nav-link text-dark" href="/">Home</a>
```

Update the `<footer>` element's app name. The following example uses the app name `BlazorHosted`:

```
- &copy; {DATE} - {DONOR NAME} - <a asp-area="" asp-page="/Privacy">Privacy</a>
+ &copy; {DATE} - BlazorHosted - <a asp-area="" asp-page="/Privacy">Privacy</a>
```

In the preceding example, the `{DATE}` placeholder represents the copyright date in an app generated from the Razor Pages or MVC project template.

To make the `Privacy` link lead to a privacy page (Razor Pages), add a privacy page to the **Server** project.

`Pages/Privacy.cshtml` in the **Server** project:

```
@page
@model PrivacyModel
@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>
```

For an MVC-based privacy view, create a privacy view in the **Server** project.

`View/Home/Privacy.cshtml` in the **Server** project:

```
@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>
```

In the `Home` controller of the MVC app, return the view.

Add the following code to `Controllers/HomeController.cs`:

```
public IActionResult Privacy()
{
    return View();
}
```

If you import files from a donor app, be sure to update any namespaces in the files to match that of the **Server** project (for example, `BlazorHosted.Server`).

Import static assets to the **Server** project from the donor project's `wwwroot` folder:

* `wwwroot/css` folder and contents
* `wwwroot/js` folder and contents
* `wwwroot/lib` folder and contents

If the donor project is created from an ASP.NET Core project template and the files aren't modified, you can copy the entire `wwwroot` folder from the donor project into the **Server** project and remove the favicon icon file.

Warning

Avoid placing the static asset into both the **Client** and **Server** `wwwroot` folders. If the same file is present in both folders, an exception is thrown because the static asset in each folder shares the same web root path. Therefore, host a static asset in either `wwwroot` folder, not both.

After adopting the preceding configuration, embed Razor components into pages or views of the **Server** project. Use the guidance in the following sections of this article:

* *Render components in a page or view with the Component Tag Helper*
* *Render components in a page or view with a CSS selector*

## Render components in a page or view with the Component Tag Helper

After [configuring the solution](#solution-configuration), including the [additional configuration](#configuration-for-embedding-razor-components-into-pages-or-views), the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) supports two render modes for rendering a component from a Blazor WebAssembly app in a page or view:

* [WebAssembly](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassembly)
* [WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered)

In the following Razor Pages example, the `Counter` component is rendered in a page. To make the component interactive, the Blazor WebAssembly script is included in the page's [render section](../../mvc/views/layout?view=aspnetcore-7.0#sections). To avoid using the full namespace for the `Counter` component with the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) (`{ASSEMBLY NAME}.Pages.Counter`), add an [`@using`](../../mvc/views/razor?view=aspnetcore-7.0#using) directive for the client project's `Pages` namespace. In the following example, the **Client** project's namespace is `BlazorHosted.Client`.

In the **Server** project, `Pages/RazorPagesCounter1.cshtml`:

```
@page
@using BlazorHosted.Client.Pages

<component type="typeof(Counter)" render-mode="WebAssemblyPrerendered" />

@section Scripts {
    <script src="_framework/blazor.webassembly.js"></script>
}
```

Run the **Server** project. Navigate to the Razor page at `/razorpagescounter1`. The prerendered `Counter` component is embedded in the page.

[RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the component:

* Is prerendered into the page.
* Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0).

Additional work might be required depending on the static resources that components use and how layout pages are organized in an app. Typically, scripts are added to a page or view's `Scripts` render section and stylesheets are added to the layout's `<head>` element content.

### Set child content through a render fragment

The [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) doesn't support receiving a [`RenderFragment` delegate for child content](./?view=aspnetcore-7.0#child-content-render-fragments) (for example, `param-ChildContent="..."`). We recommend creating a Razor component (`.razor`) that references the component you want to render with the child content you want to pass and then invoke the Razor component from the page or view.

### Ensure that top-level prerendered components aren't trimmed out on publish

If a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) directly references a component from a library that's subject to trimming on publish, the component might be trimmed out during publish because there are no references to it from client-side app code. As a result, the component isn't prerendered, leaving a blank spot in the output. If this occurs, instruct the trimmer to preserve the library component by adding a [`DynamicDependency` attribute](/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute) to any class in the client-side app. To preserve a component called `SomeLibraryComponentToBePreserved`, add the following to any component:

```
@using System.Diagnostics.CodeAnalysis
@attribute [DynamicDependency(DynamicallyAccessedMemberTypes.All, 
    typeof(SomeLibraryComponentToBePreserved))]
```

The preceding approach usually isn't required because the app usually prerenders its components (which are not trimmed), which in turn references components from libraries (causing them also not to be trimmed). Only use `DynamicDependency` explicitly for prerendering a library component directly when the library is subject to trimming.

## Render components in a page or view with a CSS selector

After [configuring the solution](#solution-configuration), including the [additional configuration](#configuration-for-embedding-razor-components-into-pages-or-views), add root components to the **Client** project of a hosted Blazor WebAssembly solution in the `Program.cs` file. In the following example, the `Counter` component is declared as a root component with a CSS selector that selects the element with the `id` that matches `counter-component`. In the following example, the **Client** project's namespace is `BlazorHosted.Client`.

In `Program.cs` file of the **Client** project, add the namespace for the project's Razor components to the top of the file:

```
using BlazorHosted.Client.Pages;
```

After the `builder` is established in `Program.cs`, add the `Counter` component as a root component:

```
builder.RootComponents.Add<Counter>("#counter-component");
```

In the following Razor Pages example, the `Counter` component is rendered in a page. To make the component interactive, the Blazor WebAssembly script is included in the page's [render section](../../mvc/views/layout?view=aspnetcore-7.0#sections).

In the **Server** project, `Pages/RazorPagesCounter2.cshtml`:

```
@page

<div id="counter-component">Loading...</div>

@section Scripts {
    <script src="_framework/blazor.webassembly.js"></script>
}
```

Run the **Server** project. Navigate to the Razor page at `/razorpagescounter2`. The prerendered `Counter` component is embedded in the page.

Additional work might be required depending on the static resources that components use and how layout pages are organized in an app. Typically, scripts are added to a page or view's `Scripts` render section and stylesheets are added to the layout's `<head>` element content.

Note

The preceding example throws a [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) if a Blazor WebAssembly app is prerendered and integrated into a Razor Pages or MVC app **simultaneously** with the use of a CSS selector. Navigating to one of the **Client** project's Razor components or navigating to a page or view of the **Server** with an embedded component throws one or more [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception).

This is normal behavior because prerendering and integrating a Blazor WebAssembly app with routable Razor components is incompatible with the use of CSS selectors.

If you've been working with the examples in the preceding sections and just wish to see the CSS selector work in your sample app, comment out the specification of the `App` root component of the **Client** project's `Program.cs` file:

```
- builder.RootComponents.Add<App>("#app");
+ //builder.RootComponents.Add<App>("#app");
```

Navigate to the page or view with the embedded Razor component that uses a CSS selector (for example, `/razorpagescounter2` of the preceding example). The page or view loads with the embedded component, and the embedded component functions as expected.

## Persist prerendered state

Without persisting prerendered state, state used during prerendering is lost and must be recreated when the app is fully loaded. If any state is setup asynchronously, the UI may flicker as the prerendered UI is replaced with temporary placeholders and then fully rendered again.

To solve these problems, Blazor supports persisting state in a prerendered page using the [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-7.0). Add the Tag Helper's tag, `<persist-component-state />`, inside the closing `</body>` tag.

`Pages/_Layout.cshtml`:

```
<body>
    ...

    <persist-component-state />
</body>
```

Decide what state to persist using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service. [PersistentComponentState.RegisterOnPersisting](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) registers a callback to persist the component state before the app is paused. The state is retrieved when the application resumes. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown.

The following example is an updated version of the `FetchData` component in a hosted Blazor WebAssembly app based on the Blazor project template. The `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-7.0) persists the component state after all component invocations.

`Pages/WeatherForecastPreserveState.razor`:

```
@page "/weather-forecast-preserve-state"
@implements IDisposable
@using BlazorSample.Shared
@inject IWeatherForecastService WeatherForecastService
@inject PersistentComponentState ApplicationState

<PageTitle>Weather Forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts = Array.Empty<WeatherForecast>();
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<WeatherForecast[]>(
            nameof(forecasts), out var restored))
        {
            forecasts = 
                await WeatherForecastService.GetForecastAsync(DateTime.Now);
        }
        else
        {
            forecasts = restored!;
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson(nameof(forecasts), forecasts);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}
```

By initializing components with the same state used during prerendering, any expensive initialization steps are only executed once. The rendered UI also matches the prerendered UI, so no flicker occurs in the browser.

The persisted prerendered state is transferred to the client, where it's used to restore the component state. For prerendering in a hosted Blazor WebAssembly app, the data is exposed to the browser and must not contain sensitive, private information.

## Additional Blazor WebAssembly resources

* [Prerendering support with assembly lazy loading](../webassembly-lazy-load-assemblies?view=aspnetcore-7.0#lazy-load-assemblies-in-a-hosted-blazor-webassembly-solution)

* Razor component lifecycle subjects that pertain to prerendering

  * [Component initialization (`OnInitialized{Async}`)](lifecycle?view=aspnetcore-7.0#component-initialization-oninitializedasync)
  * [After component render (`OnAfterRender{Async}`)](lifecycle?view=aspnetcore-7.0#after-component-render-onafterrenderasync)
  * [Stateful reconnection after prerendering](lifecycle?view=aspnetcore-7.0#stateful-reconnection-after-prerendering): Although the content in the section focuses on Blazor Server and stateful SignalR *reconnection*, the scenario for prerendering in hosted Blazor WebAssembly apps ([WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered)) involves similar conditions and approaches to prevent executing developer code twice. To preserve state during the execution of initialization code while prerendering, see *Persist prerendered state* section of this article.
  * [Prerendering with JavaScript interop](lifecycle?view=aspnetcore-7.0#prerendering-with-javascript-interop)

* Authentication and authorization subjects that pertain to prerendering

  * [General aspects](../security/?view=aspnetcore-7.0#aspnet-core-blazor-authentication-and-authorization)
  * [Prerendering with authentication](../security/webassembly/additional-scenarios?view=aspnetcore-7.0#prerendering-with-authentication)

* [Host and deploy ASP.NET Core Blazor WebAssembly](../host-and-deploy/webassembly/?view=aspnetcore-7.0)

Prerendering can improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines can use to calculate page rank.

## Solution configuration

### Prerendering configuration

To set up prerendering for a hosted Blazor WebAssembly app:

1. Host the Blazor WebAssembly app in an ASP.NET Core app. A standalone Blazor WebAssembly app can be added to an ASP.NET Core solution, or you can use a hosted Blazor WebAssembly app created from the [Blazor WebAssembly project template](../tooling?view=aspnetcore-7.0) with the hosted option:

   * Visual Studio: In the **Additional information** dialog, select the **ASP.NET Core Hosted** checkbox when creating the Blazor WebAssembly app. In this article's examples, the solution is named `BlazorHosted`.
   * Visual Studio Code/.NET CLI command shell: `dotnet new blazorwasm -ho` (use the `-ho|--hosted` option). Use the `-o|--output {LOCATION}` option to create a folder for the solution and set the solution's project namespaces. In this article's examples, the solution is named `BlazorHosted` (`dotnet new blazorwasm -ho -o BlazorHosted`).

   For the examples in this article, the client project's namespace is `BlazorHosted.Client`, and the server project's namespace is `BlazorHosted.Server`.

2. **Delete** the `wwwroot/index.html` file from the Blazor WebAssembly **Client** project.

3. In the **Client** project, **delete** the following line in `Program.cs`:

   ```
   - builder.RootComponents.Add<App>("#app");
   ```

4. Add a `Pages/_Host.cshtml` file to the **Server** project's `Pages` folder. You can obtain a `_Host.cshtml` file from a project created from the Blazor Server template with the `dotnet new blazorserver -o BlazorServer` command in a command shell (the `-o BlazorServer` option creates a folder for the project). After placing the `Pages/_Host.cshtml` file into the **Server** project of the hosted Blazor WebAssembly solution, make the following changes to the file:

   * Provide an [`@using`](../../mvc/views/razor?view=aspnetcore-7.0#using) directive for the **Client** project (for example, `@using BlazorHosted.Client`).

   * Update the stylesheet links to point to the WebAssembly project's stylesheets. In the following example, the client project's namespace is `BlazorHosted.Client`:

     ```
     - <link href="css/site.css" rel="stylesheet" />
     - <link href="_content/BlazorServer/_framework/scoped.styles.css" rel="stylesheet" />
     + <link href="css/app.css" rel="stylesheet" />
     + <link href="BlazorHosted.Client.styles.css" rel="stylesheet" />
     ```

     Note

     Leave the `<link>` element that requests the Bootstrap stylesheet (`css/bootstrap/bootstrap.min.css`) in place.

   * Update the `render-mode` of the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) to prerender the root `App` component with [WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered):

     ```
     - <component type="typeof(App)" render-mode="ServerPrerendered" />
     + <component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
     ```

   * Update the Blazor script source to use the client-side Blazor WebAssembly script:

     ```
     - <script src="_framework/blazor.server.js"></script>
     + <script src="_framework/blazor.webassembly.js"></script>
     ```

5. In `Startup.Configure` of the **Server** project, change the fallback from the `index.html` file to the `_Host.cshtml` page.

   `Startup.cs`:

   ```
   - endpoints.MapFallbackToFile("index.html");
   + endpoints.MapFallbackToPage("/_Host");
   ```

6. If the **Client** and **Server** projects use one or more common services during prerendering, factor the service registrations into a method that can be called from both projects. For more information, see [ASP.NET Core Blazor dependency injection](../fundamentals/dependency-injection?view=aspnetcore-7.0#register-common-services).

7. Run the **Server** project. The hosted Blazor WebAssembly app is prerendered by the **Server** project for clients.

### Configuration for embedding Razor components into pages or views

The following sections and examples in this article for embedding Razor components of the client Blazor WebAssembly app into pages or views of the server app require additional configuration.

Use a default Razor Pages or MVC layout file in the **Server** project. The **Server** project must have the following files and folders.

Razor Pages:

* `Pages/Shared/_Layout.cshtml`
* `Pages/_ViewImports.cshtml`
* `Pages/_ViewStart.cshtml`

MVC:

* `Views/Shared/_Layout.cshtml`
* `Views/_ViewImports.cshtml`
* `Views/_ViewStart.cshtml`

Obtain the preceding files from an app created from the Razor Pages or MVC project template. For more information, see [Tutorial: Get started with Razor Pages in ASP.NET Core](../../tutorials/razor-pages/razor-pages-start?view=aspnetcore-7.0) or [Get started with ASP.NET Core MVC](../../tutorials/first-mvc-app/start-mvc?view=aspnetcore-7.0).

Update the namespaces in the imported `_ViewImports.cshtml` file to match those in use by the **Server** project receiving the files.

Update the imported layout file (`_Layout.cshtml`) to include the **Client** project's styles. In the following example, the **Client** project's namespace is `BlazorHosted.Client`. The `<title>` element can be updated at the same time.

`Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC):

```
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-   <title>@ViewData["Title"] - DonorProject</title>
+   <title>@ViewData["Title"] - BlazorHosted</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
+   <link href="css/app.css" rel="stylesheet" />
+   <link href="BlazorHosted.Client.styles.css" rel="stylesheet" />
</head>
```

The imported layout contains `Home` and `Privacy` navigation links. To make the `Home` link point to the hosted Blazor WebAssembly app, change the hyperlink:

```
- <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
+ <a class="nav-link text-dark" href="/">Home</a>
```

In an MVC layout file:

```
- <a class="nav-link text-dark" asp-area="" asp-controller="Home" 
-     asp-action="Index">Home</a>
+ <a class="nav-link text-dark" href="/">Home</a>
```

To make the `Privacy` link lead to a privacy page, add a privacy page to the **Server** project.

`Pages/Privacy.cshtml` in the **Server** project:

```
@page
@model BlazorHosted.Server.Pages.PrivacyModel
@{
}

<h1>Privacy Policy</h1>
```

If an MVC-based privacy view is preferred, create a privacy view in the **Server** project.

`View/Home/Privacy.cshtml`:

```
@{
    ViewData["Title"] = "Privacy Policy";
}

<h1>@ViewData["Title"]</h1>
```

In the `Home` controller, return the view.

`Controllers/HomeController.cs`:

```
public IActionResult Privacy()
{
    return View();
}
```

Import static assets to the **Server** project from the donor project's `wwwroot` folder:

* `wwwroot/css` folder and contents
* `wwwroot/js` folder and contents
* `wwwroot/lib` folder and contents

If the donor project is created from an ASP.NET Core project template and the files aren't modified, you can copy the entire `wwwroot` folder from the donor project into the **Server** project and remove the favicon icon file.

Warning

Avoid placing the static asset into both the **Client** and **Server** `wwwroot` folders. If the same file is present in both folders, an exception is thrown because the static asset in each folder shares the same web root path. Therefore, host a static asset in either `wwwroot` folder, not both.

## Render components in a page or view with the Component Tag Helper

After [configuring the solution](#solution-configuration), including the [additional configuration](#configuration-for-embedding-razor-components-into-pages-or-views), the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) supports two render modes for rendering a component from a Blazor WebAssembly app in a page or view:

* [WebAssembly](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassembly)
* [WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered)

In the following Razor Pages example, the `Counter` component is rendered in a page. To make the component interactive, the Blazor WebAssembly script is included in the page's [render section](../../mvc/views/layout?view=aspnetcore-7.0#sections). To avoid using the full namespace for the `Counter` component with the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0) (`{ASSEMBLY NAME}.Pages.Counter`), add an [`@using`](../../mvc/views/razor?view=aspnetcore-7.0#using) directive for the client project's `Pages` namespace. In the following example, the **Client** project's namespace is `BlazorHosted.Client`.

In the **Server** project, `Pages/RazorPagesCounter1.cshtml`:

```
@page
@using BlazorHosted.Client.Pages

<component type="typeof(Counter)" render-mode="WebAssemblyPrerendered" />

@section Scripts {
    <script src="_framework/blazor.webassembly.js"></script>
}
```

Run the **Server** project. Navigate to the Razor page at `/razorpagescounter1`. The prerendered `Counter` component is embedded in the page.

[RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the component:

* Is prerendered into the page.
* Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-7.0).

Additional work might be required depending on the static resources that components use and how layout pages are organized in an app. Typically, scripts are added to a page or view's `Scripts` render section and stylesheets are added to the layout's `<head>` element content.

## Render components in a page or view with a CSS selector

After [configuring the solution](#solution-configuration), including the [additional configuration](#configuration-for-embedding-razor-components-into-pages-or-views), add root components to the **Client** project of a hosted Blazor WebAssembly solution in `Program.cs`. In the following example, the `Counter` component is declared as a root component with a CSS selector that selects the element with the `id` that matches `counter-component`. In the following example, the **Client** project's namespace is `BlazorHosted.Client`.

In `Program.cs` of the **Client** project, add the namespace for the project's Razor components to the top of the file:

```
using BlazorHosted.Client.Pages;
```

After the `builder` is established in `Program.cs`, add the `Counter` component as a root component:

```
builder.RootComponents.Add<Counter>("#counter-component");
```

In the following Razor Pages example, the `Counter` component is rendered in a page. To make the component interactive, the Blazor WebAssembly script is included in the page's [render section](../../mvc/views/layout?view=aspnetcore-7.0#sections).

In the **Server** project, `Pages/RazorPagesCounter2.cshtml`:

```
@page

<div id="counter-component">Loading...</div>

@section Scripts {
    <script src="_framework/blazor.webassembly.js"></script>
}
```

Run the **Server** project. Navigate to the Razor page at `/razorpagescounter2`. The prerendered `Counter` component is embedded in the page.

Additional work might be required depending on the static resources that components use and how layout pages are organized in an app. Typically, scripts are added to a page or view's `Scripts` render section and stylesheets are added to the layout's `<head>` element content.

Note

The preceding example throws a [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) if a Blazor WebAssembly app is prerendered and integrated into a Razor Pages or MVC app **simultaneously** with a CSS selector. Navigating to one of the **Client** project's Razor components throws the following exception:

> Microsoft.JSInterop.JSException: Could not find any element matching selector '#counter-component'.

This is normal behavior because prerendering and integrating a Blazor WebAssembly app with routable Razor components is incompatible with the use of CSS selectors.

## Additional Blazor WebAssembly resources

* [Prerendering support with assembly lazy loading](../webassembly-lazy-load-assemblies?view=aspnetcore-7.0#lazy-load-assemblies-in-a-hosted-blazor-webassembly-solution)

* Razor component lifecycle subjects that pertain to prerendering

  * [Component initialization (`OnInitialized{Async}`)](lifecycle?view=aspnetcore-7.0#component-initialization-oninitializedasync)
  * [After component render (`OnAfterRender{Async}`)](lifecycle?view=aspnetcore-7.0#after-component-render-onafterrenderasync)
  * [Stateful reconnection after prerendering](lifecycle?view=aspnetcore-7.0#stateful-reconnection-after-prerendering): Although the content in the section focuses on Blazor Server and stateful SignalR *reconnection*, the scenario for prerendering in hosted Blazor WebAssembly apps ([WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered)) involves similar conditions and approaches to prevent executing developer code twice. To preserve state during the execution of initialization code while prerendering, see *Persist prerendered state* section of this article.
  * [Prerendering with JavaScript interop](lifecycle?view=aspnetcore-7.0#prerendering-with-javascript-interop)

* Authentication and authorization subjects that pertain to prerendering

  * [General aspects](../security/?view=aspnetcore-7.0#aspnet-core-blazor-authentication-and-authorization)
  * [Prerendering with authentication](../security/webassembly/additional-scenarios?view=aspnetcore-7.0#prerendering-with-authentication)

* [Host and deploy ASP.NET Core Blazor WebAssembly](../host-and-deploy/webassembly/?view=aspnetcore-7.0)

Integrating Razor components into Razor Pages or MVC apps in a hosted Blazor WebAssembly [solution](../tooling?view=aspnetcore-7.0#visual-studio-solution-file-sln) is supported in ASP.NET Core in .NET 5 or later. Select a .NET 5 or later version of this article.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-11-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/logging/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Logging in .NET and ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes logging in ASP.NET Core apps. For general guidance on logging in .NET, see [Logging in C# and .NET](/en-us/dotnet/core/extensions/logging). For Blazor logging guidance, which adds to or supersedes this guidance, see [ASP.NET Core Blazor logging](../../blazor/fundamentals/logging?view=aspnetcore-10.0).

## Logging providers

ASP.NET Core supports high performance, structured logging via the [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) API to help you monitor app behavior and diagnose problems. Logs are written to different destinations by configuring *logging providers*. A set of logging providers are built into the framework, and there are many third-party providers available. Multiple providers can be enabled in an app.

Most logging providers write log messages to a data storage system. For example, the Azure Application Insights logging provider stores logs in [Azure Application Insights](/en-us/azure/azure-monitor/app/app-insights-overview). One provider, the `Console` provider, only displays log messages. The `Console` provider is useful when running an app locally for monitoring and debugging in real time.

Apps created from an ASP.NET Core web app project template call [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) in the app's `Program` file, which adds the following default logging providers:

* [`Console`](#console)
* [`Debug`](#debug)
* [`EventSource`](#eventsource)
* [Windows `EventLog`](#windows-eventlog)

```
var builder = WebApplication.CreateBuilder(args);
```

To override the default logging providers, call [ClearProviders](/en-us/dotnet/api/microsoft.extensions.logging.loggingbuilderextensions.clearproviders) on [WebApplicationBuilder.Logging](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.logging) and use logging provider extension methods to add logging providers. The following example only sets up the [`Console`](#console) logging provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddConsole();
```

Alternatively, the preceding code can be written as follows with [ILoggingBuilder](/en-us/dotnet/api/microsoft.extensions.logging.iloggingbuilder) of [ConfigureLogging](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.configurelogging):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.ConfigureLogging(logging =>
{
    logging.ClearProviders();
    logging.AddConsole();
});
```

Apps created from an ASP.NET Core web app project template call [Host.CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder), which adds the following default logging providers:

* [`Console`](#console)
* [`Debug`](#debug)
* [`EventSource`](#eventsource)
* [Windows `EventLog`](#windows-eventlog)

```
Host.CreateDefaultBuilder(args)
```

To override the default logging providers, call [ClearProviders](/en-us/dotnet/api/microsoft.extensions.logging.loggingbuilderextensions.clearproviders) to remove all the [ILoggerProvider](/en-us/dotnet/api/microsoft.extensions.logging.iloggerprovider) instances from the [ILoggingBuilder](/en-us/dotnet/api/microsoft.extensions.logging.iloggingbuilder) and use logging provider extension methods to add logging providers. The following example only sets up the [`Console`](#console) logging provider:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.ClearProviders();
            logging.AddConsole();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

Additional providers are covered in the [Built-in logging providers](#built-in-logging-providers) and [Third-party logging providers](#third-party-logging-providers) sections.

## Logging output

Logs created by the [default logging providers](#logging-providers) are displayed:

* In Visual Studio

  * In the **Debug** output window when debugging.
  * In the **ASP.NET Core Web Server** window.

* In the command shell when the app is run with the [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) command.

.NET in general and ASP.NET Core use the same logging API and providers. More information can be found in [Logging in C# and .NET](/en-us/dotnet/core/extensions/logging), which covers general logging scenarios for C# and .NET. This article focuses on ASP.NET Core app logging.

## Create log messages

To create log messages, use an [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) object from [dependency injection (DI)](../dependency-injection?view=aspnetcore-10.0).

The following examples:

* Create an [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) that specifies a log *category* based on the fully qualified name of the type. The log category is a string that is associated with each log, which is useful for identifying, sorting, and filtering log messages. More information on [log categories](#log-category) is provided later in this article.
* Calls [LogInformation](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.loginformation) to log at the [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) level. The log *level* indicates the severity of the logged event. More information on [log levels](#log-level) is provided later in this article.

In the following counter page (`Counter` Razor component) in a Blazor app, an `ILogger<Counter>` is injected with the [`@inject` directive](../../mvc/views/razor?view=aspnetcore-10.0#inject). The logger instance (`Logger`) is used to log information when the `IncrementCount` method is called.

`Pages/Counter.razor`:

```
@page "/counter"
@inject ILogger<Counter> Logger

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        Logger.LogInformation("Someone incremented the counter!");

        currentCount++;
    }
}
```

Note

In .NET 5 or earlier, an [`@using` directive](../../mvc/views/razor?view=aspnetcore-10.0#using) for [Microsoft.Extensions.Logging](/en-us/dotnet/api/microsoft.extensions.logging) is required to support [IntelliSense](/en-us/visualstudio/ide/using-intellisense) logging API completions in Razor components.

Log message:

> BlazorSample.Components.Pages.Counter: Information: Someone incremented the counter!

The log category is `BlazorSample.Components.Pages.Counter`, and the log level (severity) is `Information`. The message is `Someone incremented the counter!`.

In the following Razor Pages privacy page class file, an `ILogger<PrivacyModel>` is injected into the class's constructor to log when the page is visited. Note in this example that the message is a *template* that takes the current UTC date and time (`DateTime.UtcNow`) and writes it into the log message. Log message templates are covered in the [Log message template](#log-message-template) section later in this article.

`Pages/Privacy.cshtml.cs`:

```
public class PrivacyModel(ILogger<PrivacyModel> logger) : PageModel
{
    public void OnGet() => logger.LogInformation("Privacy page visited at {DT}", 
        DateTime.UtcNow);
}
```

## Log message template

The log message template can contain placeholders for provided arguments. Use names for the placeholders, not numbers.

In the following examples, `{Id}` is an identifier placeholder for an item ID, and `id` is the identifier parameter.

```
Logger.LogInformation(LogEvent.GetItem, "Getting item {Id}", id);
```

```
Logger.LogWarning(LogEvent.GetItemNotFound, "Get({Id}) NOT FOUND", id);
```

The *order of the parameters*, not their placeholder names, determines which parameters are used to provide placeholder values in log messages. In the following code, the parameter names are out of sequence in the placeholders of the message template:

```
var apples = 1;
var pears = 2;
var bananas = 3;

Logger.LogInformation("{Pears}, {Bananas}, {Apples}", apples, pears, bananas);
```

However, the parameters are assigned to the placeholders in the order: `apples`, `pears`, `bananas`. The log message reflects the *order of the parameters*:

```
1, 2, 3
```

This approach allows logging providers to implement [semantic or structured logging](https://github.com/NLog/NLog/wiki/How-to-use-structured-logging). The arguments themselves are passed to the logging system, not just the formatted message template. This enables logging providers to store the parameter values as fields. For example, consider the following logger method:

```
Logger.LogInformation("Getting item {Id} at {RequestTime}", id, DateTime.Now);
```

When logging to Azure Table Storage:

* Each Azure Table entity can have `ID` and `RequestTime` properties.
* Tables with properties simplify queries on logged data. For example, a query can find all logs within a particular `RequestTime` range without having to parse the time out of the text message.

## Log at app startup

The following example calls [WebApplication.Logger](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.logger) in the `Program` file to log informational messages:

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.Logger.LogInformation("Adding Routes");

app.MapGet("/", () => "Hello World!");

app.Logger.LogInformation("Starting the app");

app.Run();
```

The following example calls [AddConsole](/en-us/dotnet/api/microsoft.extensions.logging.consoleloggerextensions.addconsole) and logs at the `/Test` endpoint:

```
var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/Test", async (ILogger<Program> logger, HttpResponse response) =>
{
    logger.LogInformation("'Test' logging in the Program file");
    await response.WriteAsync("Testing");
});

app.Run();
```

The following example calls [AddSimpleConsole](/en-us/dotnet/api/microsoft.extensions.logging.consoleloggerextensions.addsimpleconsole), disables color output with a [console formatter option](/en-us/dotnet/api/microsoft.extensions.logging.console.simpleconsoleformatteroptions), and logs at the `/Test` endpoint:

```
using Microsoft.Extensions.Logging.Console;

var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddSimpleConsole(
    option => option.ColorBehavior = LoggerColorBehavior.Disabled);

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/Test", async (ILogger<Program> logger, HttpResponse response) =>
{
    logger.LogInformation("'Test' logging in the Program file");
    await response.WriteAsync("Testing");
});

app.Run();
```

The following code logs in `Program.Main` by obtaining an [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) instance from DI after building the host:

```
public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();

    var logger = host.Services.GetRequiredService<ILogger<Program>>();
    logger.LogInformation("Host created.");

    host.Run();
}
```

The following example shows how to inject an [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) into `Startup.Configure`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
    logger.LogInformation("'Startup'.Configure' logging");

    ...
}
```

Logger injection into the `Startup` constructor or into the `Startup.ConfigureServices` method isn't supported because logging depends on dependency injection (DI) and on configuration, which in turns depends on DI. The DI container isn't set up until `ConfigureServices` finishes executing.

For information on configuring a service that depends on [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) or why constructor injection of a logger into `Startup` worked in earlier versions, see the [Configure a service that depends on `ILogger`](#configure-a-service-that-depends-on-ilogger) section.

## Configure logging

When an [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) object is created, a *category* is specified. That category is included with each log message created by that instance of the logger.

The log level determines the level of detail for log messages at a default level for the app as a whole and for specific app assemblies. The log level can be set by any of the [configuration providers](../configuration/?view=aspnetcore-10.0).

### App settings

Logging configuration is commonly provided by the `Logging` section of `appsettings.{ENVIRONMENT}.json` files, where the `{ENVIRONMENT}` placeholder is the [environment](../environments?view=aspnetcore-10.0). The following `appsettings.Development.json` file is generated by the ASP.NET Core web app templates:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
```

In the preceding JSON:

* The `"Default"` and `"Microsoft.AspNetCore"` categories are specified.
* The `"Microsoft.AspNetCore"` category applies to all categories that start with `"Microsoft.AspNetCore"`. For example, this setting applies to the `"Microsoft.AspNetCore.Routing.EndpointMiddleware"` category.
* The `"Microsoft.AspNetCore"` category logs at log level `Warning` and higher (more severe).
* A specific log provider isn't specified, so `LogLevel` applies to all the enabled logging providers except for the [Windows `EventLog`](#windows-eventlog).

The `Logging` property can have [LogLevel](/en-us/dotnet/api/microsoft.extensions.logging.loglevel) and log provider properties. The `LogLevel` specifies the minimum [level](#log-level) to log for selected categories. In the preceding JSON, `Information` and `Warning` log levels are specified. `LogLevel`s indicate the severity of the log, which are shown in the following table with their corresponding `enum` values.

| Log Level     | Value |
| ------------- | ----- |
| `Trace`       | 0     |
| `Debug`       | 1     |
| `Information` | 2     |
| `Warning`     | 3     |
| `Error`       | 4     |
| `Critical`    | 5     |
| `None`        | 6     |

When a `LogLevel` is specified, logging is enabled for messages at the specified level and higher (more severe). In the preceding JSON, the `Default` category is logged for `Information` and higher. For example, `Information`, `Warning`, `Error`, and `Critical` messages are logged. If no `LogLevel` is specified, logging defaults to the `Information` level. For more information, see [Log levels](#log-level).

A provider property can specify a `LogLevel` property. `LogLevel` under a provider specifies levels to log for that provider, and overrides the non-provider log settings. Consider the following `appsettings.json` file:

```
{
  "Logging": {
    "LogLevel": { // All providers, LogLevel applies to all the enabled providers.
      "Default": "Error", // Default logging, Error and higher.
      "Microsoft": "Warning" // All Microsoft* categories, Warning and higher.
    },
    "Debug": { // Debug provider.
      "LogLevel": {
        "Default": "Information", // Overrides preceding LogLevel:Default setting.
        "Microsoft.Hosting": "Trace" // Debug:Microsoft.Hosting category.
      }
    },
    "EventSource": { // EventSource provider
      "LogLevel": {
        "Default": "Warning" // All categories of EventSource provider.
      }
    }
  }
}
```

Settings in `Logging.{PROVIDER NAME}.LogLevel` override settings in `Logging.LogLevel`, where the `{PROVIDER NAME}` placeholder is the provider name. In the preceding JSON, the `Debug` provider's default log level is set to `Information`:

`Logging:Debug:LogLevel:Default:Information`

The preceding setting specifies the `Information` log level for every `Logging:Debug:` category except `Microsoft.Hosting`. When a specific category is listed, the specific category overrides the default category. In the preceding JSON, the `Logging:Debug:LogLevel` categories `"Microsoft.Hosting"` and `"Default"` override the settings in `Logging:LogLevel`.

The minimum log level can be specified for any of:

* Specific providers example: `Logging:EventSource:LogLevel:Default:Information`
* Specific categories example: `Logging:LogLevel:Microsoft:Warning`
* All providers and all categories: `Logging:LogLevel:Default:Warning`

Any logs below the minimum level are ***not***:

* Passed to the provider.
* Logged or displayed.

To suppress all logs, specify [LogLevel.None](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-none). `LogLevel.None` has a value of 6, which is higher than `LogLevel.Critical` (5).

If a provider supports [log scopes](#log-scopes), `IncludeScopes` indicates whether they're enabled. For more information, see [log scopes](#log-scopes).

The following `appsettings.json` file contains all the providers enabled by default:

```
{
  "Logging": {
    "LogLevel": { // No provider, LogLevel applies to all the enabled providers.
      "Default": "Error",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Warning"
    },
    "Debug": { // Debug provider.
      "LogLevel": {
        "Default": "Information" // Overrides preceding LogLevel:Default setting.
      }
    },
    "Console": {
      "IncludeScopes": true,
      "LogLevel": {
        "Microsoft.AspNetCore.Mvc.Razor.Internal": "Warning",
        "Microsoft.AspNetCore.Mvc.Razor.Razor": "Debug",
        "Microsoft.AspNetCore.Mvc.Razor": "Error",
        "Default": "Information"
      }
    },
    "EventSource": {
      "LogLevel": {
        "Microsoft": "Information"
      }
    },
    "EventLog": {
      "LogLevel": {
        "Microsoft": "Information"
      }
    },
    "AzureAppServicesFile": {
      "IncludeScopes": true,
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "AzureAppServicesBlob": {
      "IncludeScopes": true,
      "LogLevel": {
        "Microsoft": "Information"
      }
    },
    "ApplicationInsights": {
      "LogLevel": {
        "Default": "Information"
      }
    }
  }
}
```

In the preceding sample:

* The categories and levels aren't suggested values. The sample is provided to show all of the default providers.

* Settings in `Logging.{PROVIDER NAME}.LogLevel` override settings in `Logging.LogLevel`, where the `{PROVIDER NAME}` placeholder is the provider name. For example, the level in `Debug.LogLevel.Default` overrides the level in `LogLevel.Default`.

* Each default provider *alias* is used. Each provider defines an *alias* that can be used in configuration in place of the fully qualified type name. The built-in providers aliases are:

  * `Console`
  * `Debug`
  * `EventSource`
  * `EventLog`
  * `AzureAppServicesFile`
  * `AzureAppServicesBlob`
  * `ApplicationInsights`

### Command shell

Environment variables for logging configuration can be set via a command shell.

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For example, [Bash](https://linuxhint.com/bash-environment-variables/) doesn't support colon (`:`) as a separator. All platforms support the double underscore (`__`) syntax and automatically replace it with a colon (`:`).

Set an environment variable with the [`set`](/en-us/windows-server/administration/windows-commands/set) command on Windows for the current command shell. In the following example, the environment key `Logging:LogLevel:Microsoft` is set to a value of `Information`. You can test the setting with any app created from an ASP.NET Core web app project template.

```
set Logging__LogLevel__Microsoft=Information
```

Execute the [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) command in the project directory after executing the preceding `set` command:

```
dotnet run
```

The preceding environment variable:

* Is only set for apps launched from the current command shell.
* Isn't read by browsers launched by Visual Studio or Visual Studio Code.

Use the [`setx`](/en-us/windows-server/administration/windows-commands/setx) to persist the environment variable across command shell instances. The `/M` switch sets the variable in the system environment. If `/M` isn't used, a user environment variable is set.

```
setx Logging__LogLevel__Microsoft Information /M
```

Note

When configuring environment variables with names that contain `.` (periods) in macOS and Linux, consider the "Exporting a variable with a dot (.) in it" question on **Stack Exchange** and its corresponding [accepted answer](https://unix.stackexchange.com/a/93533).

### Configure Azure App Service

On [Azure App Service](https://azure.microsoft.com/services/app-service/), follow the guidance in [Configure an App Service app](/en-us/azure/app-service/configure-common?tabs=portal#configure-app-settings) to set logging environment variables.

For more information, see [Azure Apps: Override app configuration using the Azure portal](../../host-and-deploy/azure-apps/?view=aspnetcore-10.0#override-app-configuration-using-the-azure-portal).

## Log category

When an [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) object is created, a *category* is specified. The category is included with each log message created by that instance of the logger. The category string is arbitrary, but the convention is to use the fully qualified class name. The ASP.NET Core web apps use [`ILogger<T>`](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) to create a logger instance that uses the fully qualified type name of `T` as the category.

Log messages with a category name that begins with "Microsoft" are from .NET. Typically, log messages that begin with the app's assembly name are from the app. Packages outside of .NET usually have a category based on an assembly name from the package. For a list of common log categories, see the [Common log categories](#common-log-categories) section.

In a Razor component of a Blazor app, where the type `T` is `Counter` for a counter page rendered by a `Counter` component (`Pages/Counter.razor`):

```
@inject ILogger<Counter> Logger
```

In a Razor Pages page class model, where the type `T` is `PrivacyModel` for a privacy page (`Pages/Privacy.cshtml.cs`):

```
public class PrivacyModel(ILogger<PrivacyModel> logger) : PageModel
```

If further categorization is desired, the convention is to use a hierarchical name by appending a subcategory to the fully qualified class name using [ILoggerFactory.CreateLogger](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory.createlogger). This approach is useful for scoping log messages to component or class methods.

The following `Counter` component logs from the `IncrementByOne` method with the `BlazorSample.Components.Pages.Counter.IncrementByOne` category and from the `IncrementByTen` method with the `BlazorSample.Components.Pages.Privacy.IncrementByTen` category.

`Pages/Counter.razor`:

```
@page "/counter"
@inject ILogger<Counter> Logger

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementByOne">Click me (+1)</button>
<button class="btn btn-primary" @onclick="IncrementByTen">Click me (+10)</button>

@code {
    private int currentCount = 0;

    private void IncrementByOne()
    {
        var logger = Logger.CreateLogger($"{typeof(Counter)}.IncrementByOne");
        Logger.LogInformation("Someone incremented the counter!");
        currentCount++;
    }

    private void IncrementByTen()
    {
        var logger = Logger.CreateLogger($"{typeof(Counter)}.IncrementByTen");
        Logger.LogInformation("Someone incremented the counter!");
        currentCount += 10;
    }
}
```

Log messages:

> BlazorSample.Components.Pages.Counter.IncrementByOne: Information: Someone incremented the counter!\
> BlazorSample.Components.Pages.Counter.IncrementByTen: Information: Someone incremented the counter!

In a Razor Pages page class model that uses a custom category ("`CustomCategory`") for the entire page model:

```
public class PrivacyModel(ILoggerFactory logger) : PageModel
{
    private readonly ILogger _logger = 
        logger.CreateLogger($"{typeof(PrivacyModel)}.CustomCategory");

    public void OnGet() =>
        _logger.LogInformation("Privacy page visited");
}
```

## Log event ID

Each log message can specify an *event ID*. The following example creates a set of custom event IDs for use by an app. Notice how the IDs are in the 1,000 range for create, read, update, and delete (CRUD) operations, 3,000 for test logging, and in the 4,000 range for not found scenarios:

```
public class LogEvent
{
    public const int GenerateItems      = 1000;
    public const int ListItems          = 1001;
    public const int GetItem            = 1002;
    public const int InsertItem         = 1003;
    public const int UpdateItem         = 1004;
    public const int DeleteItem         = 1005;

    public const int TestItem           = 3000;

    public const int GetItemNotFound    = 4000;
    public const int UpdateItemNotFound = 4001;
}
```

Used in Razor component code, where an `ILogger<T>` instance (`Logger`) is injected:

* The `LogEvent.GetItem` ID (`1002`) is used with the informational log message for retrieving an item by its identifier (`id`).
* The `LogEvent.GetItemNotFound` ID (`4000`) is used with the warning log message if the item isn't found.

```
Logger.LogInformation(LogEvent.GetItem, "Getting item {Id}", id);

var todoItem = await TodoItemService.FindAsync(id);

if (todoItem == null)
{
    Logger.LogWarning(LogEvent.GetItemNotFound, "Get({Id}) NOT FOUND", id);

    return NotFound();
}
```

The logging provider may store the event ID in an ID (allows filtering on the ID), in the log message, or not at all. The [`Debug` provider](#debug) doesn't show event IDs. The console provider shows event IDs in brackets after the category:

```
info: BlazorSample.Components.Pages.Items[1002]
      Getting item 1
warn: BlazorSample.Components.Pages.Items[4000]
      Get(1) NOT FOUND
```

## Log level

The following table describes the logging levels from lowest to highest severity, their corresponding `enum` values, and their convenience extension methods.

| `LogLevel`                                                                                                               | Value | Method                                                                                           | Description                                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------------------------ | ----- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-trace)             | 0     | [LogTrace](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.logtrace)             | Contain the most detailed messages. These messages may contain sensitive app data. These messages are disabled by default and should ***not*** be enabled in production. |
| [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug)             | 1     | [LogDebug](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.logdebug)             | For debugging and development. Use with caution in production due to the high volume of messages logged.                                                                 |
| [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) | 2     | [LogInformation](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.loginformation) | Tracks the general flow of the app.                                                                                                                                      |
| [Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning)         | 3     | [LogWarning](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.logwarning)         | For abnormal or unexpected events. Typically includes errors or conditions that don't cause the app to fail.                                                             |
| [Error](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-error)             | 4     | [LogError](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.logerror)             | Typically used for unhandled errors and exceptions. These messages indicate a failure in the current operation or request, not an app-wide failure.                      |
| [Critical](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-critical)       | 5     | [LogCritical](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.logcritical)       | For failures that require immediate attention, such as data loss or out of disk space.                                                                                   |
| [None](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-none)               | 6     | —                                                                                                | Specifies that a logging category shouldn't write messages.                                                                                                              |

The [Log](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.log) method's first parameter, [LogLevel](/en-us/dotnet/api/microsoft.extensions.logging.loglevel), indicates the severity of the log. Rather than calling `Log(LogLevel, ...)`, most developers call [LoggerExtensions](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions) methods. For example, the following two logging calls are functionally equivalent and produce the same based on an injected `ILogger<T>` instance (`Logger`) in a Razor component:

```
Logger.Log(LogLevel.Information, LogEvent.TestItem, routeInfo);

Logger.LogInformation(LogEvent.TestItem, routeInfo);
```

Note

`LogEvent.TestItem` is a [log event ID](#log-event-id).

Log at an appropriate level to control how much log output is written to a particular storage medium:

* In production:

  * Logging at the [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-trace), [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug), or [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) levels produces a high-volume of detailed log messages. To control costs and not exceed data storage limits, log at these levels to a high-volume, low-cost data store. Consider limiting these levels to specific categories.

  * Logging at [Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning) through [Critical](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-critical) levels usually produces few log messages.

    * Costs and storage limits usually aren't a concern.
    * Few logs allow more flexibility in data store choices.

* In development:

  * We recommend the [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) level (`"Default": "Information"`) for default logging and the [Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning) level for Microsoft ASP.NET Core assemblies (`"Microsoft.AspNetCore": "Warning"`).
  * Add [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-trace) and [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug), or [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) messages when troubleshooting. To limit output, only set these logging levels for the categories under investigation.

## Change log levels in a running app

The Logging API doesn't include support for changing log levels while an app is running. However, some configuration providers are capable of reloading configuration, which takes immediate effect on logging configuration. For example, the [File Configuration Provider](../configuration/?view=aspnetcore-10.0#file-configuration-provider), reloads logging configuration by default. If configuration is changed in code while an app is running, the app can call [IConfigurationRoot.Reload](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationroot.reload) to update the app's logging configuration.

## How filtering rules are applied

When an [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) object is created, the [ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory) object selects a single rule per provider to apply to that logger. All messages written by an [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) instance are filtered based on the selected rules. The most specific rule for each provider and category pair is selected from the available rules.

The following algorithm is used for each provider when an [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) is created for a given category:

* Select all rules that match the provider or its alias. If no match is found, select all rules with an empty provider.
* From the result of the preceding step, select rules with longest matching category prefix. If no match is found, select all rules that don't specify a category.
* If multiple rules are selected, take the *last* one.
* If no rules are selected, use `MinimumLevel`.

## `ILogger` and `ILoggerFactory`

The [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) and [ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory) interfaces and implementations are included in the .NET SDK. They are also available in the following NuGet packages:

* The interfaces are in the [`Microsoft.Extensions.Logging.Abstractions` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions).
* The default implementations are in the [`Microsoft.Extensions.Logging` NuGet package](https://www.nuget.org/packages/microsoft.extensions.logging).

## Log exceptions

The logger methods have overloads that take an exception parameter:

```
try
{
   ...

   throw new Exception("Test exception");
}
catch (Exception ex)
{
    Logger.LogWarning(LogEvent.GetItemNotFound, ex, "Test exception at {DT}", 
        DateTime.UtcNow);
}
```

Exception logging is provider-specific.

### Default log level

If the default log level isn't explicitly set, the default log level is [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information).

If the default log level isn't set in configuration, it can be set with [LoggingBuilderExtensions.SetMinimumLevel](/en-us/dotnet/api/microsoft.extensions.logging.loggingbuilderextensions.setminimumlevel). The following example sets the [Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning) level as the default logging level:

```
var builder = WebApplication.CreateBuilder();

builder.Logging.SetMinimumLevel(LogLevel.Warning);
```

We recommend setting the minimum default log level in configuration, not in C# code.

### Filter function

A filter function is invoked for all providers and categories that don't have rules assigned to them by configuration or code. The following example displays console logs when the category contains `Page` or `Microsoft` and the log level is [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) or higher:

```
var builder = WebApplication.CreateBuilder();

builder.Logging.AddFilter((provider, category, logLevel) =>
{
    if (provider is not null && category is not null)
    {
        return provider.Contains("ConsoleLoggerProvider")
            && (category.Contains("Page") || category.Contains("Microsoft"))
            && logLevel >= LogLevel.Information;
    }

    return false;
});
```

We recommend specifying log levels in configuration and not in code with filter functions.

## ASP.NET Core categories

The following table contains some of the logging categories used by ASP.NET Core.

| Category                              | Notes                                                                                                             |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `Microsoft.AspNetCore`                | General ASP.NET Core diagnostics.                                                                                 |
| `Microsoft.AspNetCore.DataProtection` | Which data protection keys were considered, found, and used.                                                      |
| `Microsoft.AspNetCore.HostFiltering`  | Hosts allowed.                                                                                                    |
| `Microsoft.AspNetCore.Hosting`        | How long HTTP requests took to complete and what time they started. Which hosting startup assemblies were loaded. |
| `Microsoft.AspNetCore.Mvc`            | MVC and Razor diagnostics. Model binding, filter execution, view compilation, and action selection.               |
| `Microsoft.AspNetCore.Routing`        | Route matching information.                                                                                       |
| `Microsoft.AspNetCore.Server`         | Connection start, stop, and keep alive responses. HTTPS certificate information.                                  |
| `Microsoft.AspNetCore.StaticFiles`    | Files served.                                                                                                     |

To study more categories using the [`Console`](#console) logger, set `appsettings.Development.json` in a test app:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Trace",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
```

Warning

Reset the logging configuration back to its prior levels in `appsettings.Development.json` after studying the [`Console`](#console) logger output.

For a list of Entity Framework categories, see [Simple Logging: Message categories (EF Core documentation)](/en-us/ef/core/logging-events-diagnostics/simple-logging#message-categories).

## Log scopes

A *scope* can group a set of logical operations. This grouping can be used to attach the same data to each log that's created as part of a set. For example, every log created as part of processing a transaction can include the transaction ID.

A scope:

* Is an [IDisposable](/en-us/dotnet/api/system.idisposable) type that's returned by the [BeginScope](/en-us/dotnet/api/microsoft.extensions.logging.ilogger.beginscope) method.
* Lasts until it's disposed.

The following providers support scopes:

* [`Console`](#console)
* [`AzureAppServicesFile` and `AzureAppServicesBlob`](/en-us/dotnet/api/microsoft.extensions.logging.azureappservices.batchingloggeroptions.includescopes#microsoft-extensions-logging-azureappservices-batchingloggeroptions-includescopes)

Use a scope by wrapping logger calls in a `using` block:

```
public async Task<TodoItem> GetTodoItem(long id)
{
    TodoItem todoItem;
    var transactionId = Guid.NewGuid().ToString();

    using (Logger.BeginScope(new List<KeyValuePair<string, object>>
    {
        new("TransactionId", transactionId),
    }))
    {
        Logger.LogInformation(LogEvent.GetItem, "Getting item {Id}", id);

        todoItem = await TodoItemsService.FindAsync(id);

        if (todoItem == null)
        {
            Logger.LogWarning(LogEvent.GetItemNotFound, "Get({Id}) NOT FOUND", id);

            return NotFound();
        }
    }

    return todoItem;
}
```

## Built-in logging providers

ASP.NET Core includes the following logging providers:

* [`Console`](#console)
* [`Debug`](#debug)
* [`EventSource`](#eventsource)
* [Windows `EventLog`](#windows-eventlog)

The following logging providers are provided by Microsoft, but not as part of the .NET shared framework. They must be installed as an additional NuGet package added to the app.

* [`AzureAppServicesFile` and `AzureAppServicesBlob`](#azure-app-service)
* [`ApplicationInsights`](#azure-application-insights)

ASP.NET Core doesn't include a logging provider for writing logs to files. To write logs to files from an ASP.NET Core app, consider using a [third-party logging provider](#third-party-logging-providers).

For information on `stdout` and debug logging with the ASP.NET Core Module, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../../test/troubleshoot-azure-iis?view=aspnetcore-10.0) and [ASP.NET Core Module (ANCM) for IIS](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

### `Console`

The `Console` provider logs output to the console. For more information on viewing `Console` logs in development, see the [Logging output](#logging-output) section.

### `Debug`

The `Debug` provider writes log output by using the [System.Diagnostics.Debug](/en-us/dotnet/api/system.diagnostics.debug) class. Calls to `System.Diagnostics.Debug.WriteLine` write to the `Debug` provider.

On Linux, the `Debug` provider log location is distribution-dependent and may be one of the following:

* `/var/log/message`
* `/var/log/syslog`

### `EventSource`

[EventSourceLoggerProvider](/en-us/dotnet/api/microsoft.extensions.logging.eventsource.eventsourceloggerprovider) writes to a cross-platform event source with the name `Microsoft-Extensions-Logging`. On Windows, the provider uses [ETW](/en-us/windows/win32/etw/event-tracing-portal).

#### `ServerReady` event to measure startup time

The ASP.NET Core hosting [EventSource](/en-us/dotnet/api/system.diagnostics.tracing.eventsource) emits the [`ServerReady`](https://source.dot.net/#Microsoft.AspNetCore.Hosting/Internal/HostingEventSource.cs,76) event in [Microsoft.AspNetCore.Hosting](/en-us/dotnet/api/microsoft.aspnetcore.hosting), which represents the point where the server is ready to respond to requests and can be used to measure startup time.

#### `dotnet-trace` tooling

The [`dotnet-trace`](/en-us/dotnet/core/diagnostics/dotnet-trace) tool is a cross-platform CLI global tool that enables the collection of .NET traces of a running process. The tool collects [Microsoft.Extensions.Logging.EventSource](/en-us/dotnet/api/microsoft.extensions.logging.eventsource) provider data using a [LoggingEventSource](/en-us/dotnet/api/microsoft.extensions.logging.eventsource.loggingeventsource).

For installation instructions, see [`dotnet-trace`](/en-us/dotnet/core/diagnostics/dotnet-trace).

Use the `dotnet-trace` tooling to collect a trace from an app:

1. Run the app with the `dotnet run` command.

2. Determine the process identifier (PID) of the .NET app:

   ```
   dotnet-trace ps
   ```

   Find the PID for the process that has the same name as the app's assembly.

3. Execute the `dotnet-trace` command.

   General command syntax:

   * `{PID}`: Process identifier
   * `{KEYWORD}`: Keyword
   * `{PROVIDER LEVEL}`: Provider level
   * `{LOGGER CATEGORY ...}`: Logger category
   * `{CATEGORY LEVEL ...}`: Category level

   ```
   dotnet-trace collect -p {PID} 
       --providers Microsoft-Extensions-Logging:{KEYWORD}:{PROVIDER LEVEL}
           :FilterSpecs=\"
               {LOGGER CATEGORY 1}:{CATEGORY LEVEL 1};
               {LOGGER CATEGORY 2}:{CATEGORY LEVEL 2};
               ...
               {LOGGER CATEGORY N}:{CATEGORY LEVEL N}\"
   ```

   When using a PowerShell command shell, enclose the `--providers` value in single quotes (`'`):

   ```
   dotnet-trace collect -p {PID} 
       --providers 'Microsoft-Extensions-Logging:{KEYWORD}:{PROVIDER LEVEL}
           :FilterSpecs=\"
               {LOGGER CATEGORY 1}:{CATEGORY LEVEL 1};
               {LOGGER CATEGORY 2}:{CATEGORY LEVEL 2};
               ...
               {LOGGER CATEGORY N}:{CATEGORY LEVEL N}\"'
   ```

   On non-Windows platforms, add the `-f speedscope` option to change the format of the output trace file to `speedscope`.

   The following table defines the keyword (`{KEYWORD}` placeholder).

   | Keyword | Description                                                                                                                                |
   | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
   | 1       | Log meta events about the `LoggingEventSource`. Doesn't log events from [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger). |
   | 2       | Turns on the `Message` event when `ILogger.Log()` is called. Provides information in a programmatic (not formatted) way.                   |
   | 4       | Turns on the `FormatMessage` event when `ILogger.Log()` is called. Provides the formatted string version of the information.               |
   | 8       | Turns on the `MessageJson` event when `ILogger.Log()` is called. Provides a JSON representation of the arguments.                          |

   The following table defines the provider levels.

   | Provider Level | Description     |
   | -------------- | --------------- |
   | 0              | `LogAlways`     |
   | 1              | `Critical`      |
   | 2              | `Error`         |
   | 3              | `Warning`       |
   | 4              | `Informational` |
   | 5              | `Verbose`       |

   The parsing for a category level can be either a string or a number, as indicated in the following table.

   | Category named value | Numeric value |
   | -------------------- | ------------- |
   | `Trace`              | 0             |
   | `Debug`              | 1             |
   | `Information`        | 2             |
   | `Warning`            | 3             |
   | `Error`              | 4             |
   | `Critical`           | 5             |

   The provider level and category level:

   * Are in reverse order.
   * The string constants aren't all identical.

   If no `FilterSpecs` are specified, the `EventSourceLogger` implementation attempts to convert the provider level to a category level and applies it to all categories.

   | Provider Level     | Category Level   |
   | ------------------ | ---------------- |
   | `Verbose`(5)       | `Debug`(1)       |
   | `Informational`(4) | `Information`(2) |
   | `Warning`(3)       | `Warning`(3)     |
   | `Error`(2)         | `Error`(4)       |
   | `Critical`(1)      | `Critical`(5)    |

   If `FilterSpecs` are provided, any category that's included in the list uses the category level encoded there, all other categories are filtered out.

   The following examples assume:

   * An app is running and calling `Logger.LogDebug("12345")`.
   * The process ID (PID) is set via `set PID=12345`, where `12345` is the actual PID.

   Consider the following command:

   ```
   dotnet-trace collect -p %PID% --providers Microsoft-Extensions-Logging:4:5
   ```

   The preceding command:

   * Captures debug messages.
   * Doesn't apply a `FilterSpecs`.
   * Specifies level 5 which maps category [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug).

   Consider the following command:

   ```
   dotnet-trace collect -p %PID%  --providers Microsoft-Extensions-Logging:4:5:\"FilterSpecs=*:5\"
   ```

   The preceding command:

   * Doesn't capture debug messages because the category level 5 is [Critical](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-critical).
   * Provides a `FilterSpecs`.

   The following command captures debug messages because category level 1 specifies [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug):

   ```
   dotnet-trace collect -p %PID%  --providers Microsoft-Extensions-Logging:4:5:\"FilterSpecs=*:1\"
   ```

   The following command captures debug messages because category specifies [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug):

   ```
   dotnet-trace collect -p %PID%  --providers Microsoft-Extensions-Logging:4:5:\"FilterSpecs=*:Debug\"
   ```

   `FilterSpecs` entries for the logger category and category level represent additional log filtering conditions. Separate `FilterSpecs` entries with the `;` semicolon character.

   Example using a Windows command shell:

   ```
   dotnet-trace collect -p %PID% --providers Microsoft-Extensions-Logging:4:2:FilterSpecs=\"Microsoft.AspNetCore.Hosting*:4\"
   ```

   The preceding command activates:

   * The [`EventSource` provider](#eventsource) to produce formatted strings (`4`) for errors (`2`).
   * `Microsoft.AspNetCore.Hosting` logging at the [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) logging level (`4`).

4. Stop the `dotnet-trace` tooling by pressing the `Enter` key or `Ctrl`+`C`.

   The trace is saved with the name `trace.nettrace` in the folder where the `dotnet-trace` command is executed.

5. Open the trace with [Perfview](#perfview). Open the `trace.nettrace` file and explore the trace events.

If the app doesn't build the host with [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder), add the [`EventSource` provider](#eventsource) to the app's logging configuration.

For more information, see:

* [Trace for performance analysis utility (`dotnet-trace`)](/en-us/dotnet/core/diagnostics/dotnet-trace) (.NET documentation)
* [Trace for performance analysis utility (`dotnet-trace`)](https://github.com/dotnet/diagnostics/blob/main/documentation/dotnet-trace-instructions.md) (`dotnet/diagnostics` GitHub repository documentation)
* [LoggingEventSource](/en-us/dotnet/api/microsoft.extensions.logging.eventsource.loggingeventsource)
* [EventLevel](/en-us/dotnet/api/system.diagnostics.tracing.eventlevel)
* [Perfview](#perfview) for viewing `EventSource` traces

#### Perfview

Use the [PerfView utility](https://github.com/Microsoft/perfview) to collect and view logs. There are other tools for viewing ETW logs, but PerfView provides the best experience for working with the ETW events emitted by ASP.NET Core.

To configure PerfView for collecting events logged by this provider, add the string `*Microsoft-Extensions-Logging` to the **Additional Providers** list. Don't miss the `*` at the start of the string.

### Windows EventLog

The Windows `EventLog` provider sends log output to the Windows Event Log. Unlike the other providers, the `EventLog` provider doesn't inherit the default non-provider settings. If `EventLog` log settings aren't specified, they default to [LogLevel.Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning).

To log events lower than [LogLevel.Warning](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning), explicitly set the log level. The following example sets the Event Log default log level to [LogLevel.Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information):

```
"Logging": {
  "EventLog": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}
```

[AddEventLog](/en-us/dotnet/api/microsoft.extensions.logging.eventloggerfactoryextensions.addeventlog) overloads can pass in [EventLogSettings](/en-us/dotnet/api/microsoft.extensions.logging.eventlog.eventlogsettings). If `null` or not specified, the following default settings are used:

* `LogName`: "`Application`"
* `SourceName`: "`.NET Runtime`"
* `MachineName`: The local machine name is used.

The following code changes the `SourceName` from the default value of `".NET Runtime"` to "`CustomLogs`":

```
var builder = WebApplication.CreateBuilder();

builder.Logging.AddEventLog(eventLogSettings =>
{
    eventLogSettings.SourceName = "CustomLogs";
});
```

When the app calls the [AddEventLog](/en-us/dotnet/api/microsoft.extensions.logging.eventloggerfactoryextensions.addeventlog) overload with [EventLogSettings](/en-us/dotnet/api/microsoft.extensions.logging.eventlog.eventlogsettings), a new instance of [EventLogLoggerProvider](/en-us/dotnet/api/microsoft.extensions.logging.eventlog.eventlogloggerprovider) is created with the provided settings. If there's already an [EventLogLoggerProvider](/en-us/dotnet/api/microsoft.extensions.logging.eventlog.eventlogloggerprovider) instance registered, which is the case if the app doesn't call [ClearProviders](/en-us/dotnet/api/microsoft.extensions.logging.loggingbuilderextensions.clearproviders) to remove all the [ILoggerProvider](/en-us/dotnet/api/microsoft.extensions.logging.iloggerprovider) instances, the new settings don't replace the existing ones. If you want to ensure that the [EventLogSettings](/en-us/dotnet/api/microsoft.extensions.logging.eventlog.eventlogsettings) are used, call [ClearProviders](/en-us/dotnet/api/microsoft.extensions.logging.loggingbuilderextensions.clearproviders) before calling [AddEventLog](/en-us/dotnet/api/microsoft.extensions.logging.eventloggerfactoryextensions.addeventlog).

#### Azure App Service

The [`Microsoft.Extensions.Logging.AzureAppServices` provider NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Logging.AzureAppServices) writes logs to text files in an Azure App Service app's file system and to [blob storage](/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet#what-is-blob-storage) in an Azure Storage account. The provider only logs when the project runs in the Azure environment.

The provider package isn't included in the shared framework. To use the provider, add the provider package to the project.

To configure provider settings, use [AzureFileLoggerOptions](/en-us/dotnet/api/microsoft.extensions.logging.azureappservices.azurefileloggeroptions) and [AzureBlobLoggerOptions](/en-us/dotnet/api/microsoft.extensions.logging.azureappservices.azureblobloggeroptions), as shown in the following example:

```
using Microsoft.Extensions.Logging.AzureAppServices;

var builder = WebApplication.CreateBuilder();

builder.Logging.AddAzureWebAppDiagnostics();

builder.Services.Configure<AzureFileLoggerOptions>(options =>
{
    options.FileName = "azure-diagnostics-";
    options.FileSizeLimit = 50 * 1024;
    options.RetainedFileCountLimit = 5;
});

builder.Services.Configure<AzureBlobLoggerOptions>(options =>
{
    options.BlobName = "log.txt";
});
```

```
public class Scopes
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureLogging(logging => logging.AddAzureWebAppDiagnostics())
                .ConfigureServices(serviceCollection => serviceCollection
                    .Configure<AzureFileLoggerOptions>(options =>
                    {
                        options.FileName = "azure-diagnostics-";
                        options.FileSizeLimit = 50 * 1024;
                        options.RetainedFileCountLimit = 5;
                    })
                    .Configure<AzureBlobLoggerOptions>(options =>
                    {
                        options.BlobName = "log.txt";
                    }))
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

When deployed to Azure App Service, the app uses the settings in the [App Service logs](/en-us/azure/app-service/troubleshoot-diagnostic-logs) section of the **App Service** page of the Azure portal. When the following settings are updated, the changes take effect immediately without requiring a restart or redeployment of the app.

* **Application Logging (Filesystem)**
* **Application Logging (Blob)**

The default location for log files is `D:\home\LogFiles\Application`. The default file size limit is 10 MB, and the default maximum number of files retained is two files.

#### Azure log streaming

Azure log streaming supports viewing log activity in real time from:

* The app server
* The web server
* Failed request tracing

To configure Azure log streaming:

* Navigate to the **App Service logs** page from the app's portal page.
* Set **Application Logging (Filesystem)** to **On**.
* Choose the log **Level**. This setting only applies to Azure log streaming.

Navigate to the **Log Stream** page to view logs. The logged messages are logged with the [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) interface.

### Azure Application Insights

Application Insights is a service that monitors a web app and provides tools for querying and analyzing the telemetry data. If you use this provider, you can query and analyze your logs by using the Application Insights tools.

The [`Microsoft.Extensions.Logging.ApplicationInsights` provider NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Logging.ApplicationInsights) writes logs to [Azure Application Insights](/en-us/azure/azure-monitor/app/cloudservices). The logging provider is included as a dependency of the [`Microsoft.ApplicationInsights.AspNetCore` NuGet package](https://www.nuget.org/packages/Microsoft.ApplicationInsights.AspNetCore), which is the package that provides all available telemetry for ASP.NET Core. If you use the `Microsoft.ApplicationInsights.AspNetCore` NuGet package, you aren't required to install the `Microsoft.Extensions.Logging.ApplicationInsights` provider package.

Note

The [`Microsoft.ApplicationInsights.Web` NuGet package](https://www.nuget.org/packages/Microsoft.ApplicationInsights.Web) is for ASP.NET 4.x, not ASP.NET Core, and shouldn't be used in an ASP.NET Core app.

For more information, see the following resources:

* [Application Insights overview](/en-us/azure/application-insights/app-insights-overview)
* [Application Insights for ASP.NET Core applications](/en-us/azure/azure-monitor/app/asp-net-core): Start here if you want to implement the full range of Application Insights telemetry along with logging.
* [ApplicationInsightsLoggerProvider for .NET ILogger logs](/en-us/azure/azure-monitor/app/ilogger): Start here if you want to implement the logging provider without the rest of Application Insights telemetry.
* [Application Insights logging adapters](/en-us/azure/azure-monitor/app/asp-net-trace-logs)
* [Install, configure, and initialize the Application Insights SDK](/en-us/training/modules/instrument-web-app-code-with-application-insights) interactive tutorial.

## Third-party logging providers

Third-party logging frameworks that work with ASP.NET Core:

* [elmah.io](https://elmah.io/) ([GitHub repository](https://github.com/elmahio/Elmah.Io.Extensions.Logging))
* [Gelf](https://go2docs.graylog.org/5-0/getting_in_log_data/gelf.html) ([GitHub repository](https://github.com/mattwcole/gelf-extensions-logging))
* [JSNLog](https://jsnlog.com/) ([GitHub repository](https://github.com/mperdeck/jsnlog))
* [KissLog.net](https://kisslog.net/) ([GitHub repository](https://github.com/catalingavan/KissLog-net))
* [Log4Net](https://logging.apache.org/log4net/) ([GitHub repository](https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore))
* [NLog](https://nlog-project.org/) ([GitHub repository](https://github.com/NLog/NLog.Extensions.Logging))
* [PLogger](https://www.nuget.org/packages/InvertedSoftware.PLogger.Core/) ([GitHub repository](https://github.com/invertedsoftware/InvertedSoftware.PLogger.Core))
* [Sentry](https://sentry.io/welcome/) ([GitHub repository](https://github.com/getsentry/sentry-dotnet))
* [Serilog](https://serilog.net/) ([GitHub repository](https://github.com/serilog/serilog-aspnetcore))
* [Stackdriver](https://cloud.google.com/dotnet/docs/stackdriver#logging) ([GitHub repository](https://github.com/googleapis/google-cloud-dotnet))

Some third-party frameworks can perform [semantic logging, also known as structured logging](https://softwareengineering.stackexchange.com/questions/312197/benefits-of-structured-logging-vs-basic-logging).

Using a third-party framework is similar to using one of the built-in providers:

1. Add the provider's NuGet package to the project.
2. Call an [ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory) extension method provided by the logging framework.

For more information, see the provider's documentation. Third-party logging providers aren't owned, maintained, or supported by Microsoft.

## Non-host console app logging

For logging in a console app without the Generic Host, see [Logging in C# and .NET](/en-us/dotnet/core/extensions/logging). For an additional example, see the [Background Tasks sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/hosted-services/samples), which is covered by [Background tasks with hosted services in ASP.NET Core](../host/hosted-services?view=aspnetcore-10.0).

## No asynchronous logger methods

Logging should be so fast that it isn't worth the performance cost of asynchronous code. If a logging data store is slow, don't write to it directly. Consider writing the log messages to a fast store initially, then move the logs to the slower data store later. For example, don't write log messages directly to a SQL Server data store in a `Log` method because `Log` methods are synchronous. Instead, synchronously add log messages to an in-memory queue and have a background worker pull the messages out of the queue to push the data to SQL Server asynchronously. For more information, see [Guidance on how to log to a message queue for slow data stores (dotnet/AspNetCore.Docs #11801)](https://github.com/dotnet/AspNetCore.Docs/issues/11801).

## Apply log filter rules in code

The preferred approach for setting log filter rules is by [app configuration](../configuration/?view=aspnetcore-10.0).

The following example shows how to register filter rules in code by calling [AddFilter](/en-us/dotnet/api/microsoft.extensions.logging.filterloggingbuilderextensions.addfilter) on [WebApplicationBuilder.Logging](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.logging):

```
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Debug;

var builder = WebApplication.CreateBuilder();

builder.Logging.AddFilter("System", LogLevel.Debug);
builder.Logging.AddFilter<DebugLoggerProvider>("Microsoft", LogLevel.Information);
builder.Logging.AddFilter<ConsoleLoggerProvider>("Microsoft", LogLevel.Trace);
```

In the preceding example:

* The first filter specifies:

  * Log filtering rules for all providers because a specific provider isn't configured.
  * All categories starting with "`System`".
  * Log level [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-debug) and higher.

* The [`Debug` provider](#debug) ([DebugLoggerProvider](/en-us/dotnet/api/microsoft.extensions.logging.debug.debugloggerprovider)) specifies:

  * All categories starting with "`Microsoft`".
  * Log level [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-information) and higher.

* The [`Console` provider](#console) ([ConsoleLoggerProvider](/en-us/dotnet/api/microsoft.extensions.logging.console.consoleloggerprovider)) specifies:

  * All categories starting with "`Microsoft`".
  * Log level [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-trace) and higher.

## Specify the trace context for logging scopes

The logging libraries implicitly create a scope object with [ActivityTrackingOptions](/en-us/dotnet/api/microsoft.extensions.logging.loggerfactoryoptions.activitytrackingoptions#microsoft-extensions-logging-loggerfactoryoptions-activitytrackingoptions). The following fields indicate the options ([ActivityTrackingOptions](/en-us/dotnet/api/microsoft.extensions.logging.activitytrackingoptions)):

* `SpanId`
* `TraceId`
* `ParentId`
* `Baggage`
* `Tags`

`SpanId`, `TraceId`, `ParentId` are enabled by default.

In the following example, only the `SpanId` and `TraceId` are specified:

```
var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddSimpleConsole(options =>
{
    options.IncludeScopes = true;
});

builder.Logging.Configure(options =>
{
    options.ActivityTrackingOptions = 
        ActivityTrackingOptions.SpanId | ActivityTrackingOptions.TraceId;
});

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

## Automatically log scope with `SpanId`, `TraceId`, and `ParentId`

The logging libraries implicitly create a scope object with `SpanId`, `TraceId`, and `ParentId`. This behavior is configured via [ActivityTrackingOptions](/en-us/dotnet/api/microsoft.extensions.logging.loggerfactoryoptions.activitytrackingoptions#microsoft-extensions-logging-loggerfactoryoptions-activitytrackingoptions).

The logging libraries implicitly create a scope object with [ActivityTrackingOptions](/en-us/dotnet/api/microsoft.extensions.logging.loggerfactoryoptions.activitytrackingoptions#microsoft-extensions-logging-loggerfactoryoptions-activitytrackingoptions). The following fields indicate the options ([ActivityTrackingOptions](/en-us/dotnet/api/microsoft.extensions.logging.activitytrackingoptions)):

* `SpanId`
* `TraceId`
* `ParentId`

`SpanId`, `TraceId`, `ParentId` are enabled by default.

In the following example, only the `SpanId` and `TraceId` are specified:

```
var loggerFactory = LoggerFactory.Create(logging =>
{
    logging.Configure(options =>
    {
        options.ActivityTrackingOptions = 
            ActivityTrackingOptions.SpanId | ActivityTrackingOptions.TraceId;
    }).AddSimpleConsole(options =>
    {
        options.IncludeScopes = true;
    });
});
```

If the [W3C Trace Context specification](https://www.w3.org/TR/trace-context/) `traceparent` http request header is set:

* The [`ParentId`](/en-us/dotnet/api/microsoft.extensions.logging.activitytrackingoptions) in the log scope shows the `parent-id` from in-bound `traceparent` header.
* The [`SpanId`](/en-us/dotnet/api/microsoft.extensions.logging.activitytrackingoptions) in the log scope shows the updated `parent-id` for the next out-bound step/span.

For more information, see [Mutating the traceparent Field](https://www.w3.org/TR/trace-context/#mutating-the-traceparent-field).

## Create a custom logger

To create a custom logger, see [Implement a custom logging provider in .NET](/en-us/dotnet/core/extensions/custom-logging-provider).

## Log during host construction

Logging during host construction isn't directly supported. However, a separate logger can be used. In the following example, a [Serilog](https://serilog.net/) logger is used to log in [CreateHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createhostbuilder). `AddSerilog` uses the static configuration specified in `Log.Logger`, which is provided by the [Serilog NuGet package](https://www.nuget.org/packages/serilog).

In the [CreateHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1.createhostbuilder) method of the app's `Program` file:

```
var builtConfig = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddCommandLine(args)
    .Build();

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.File(builtConfig["Logging:FilePath"])
    .CreateLogger();

try
{
    return Host.CreateDefaultBuilder(args)
        .ConfigureServices((context, services) =>
        {
            services.AddRazorPages();
        })
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.AddConfiguration(builtConfig);
        })
        .ConfigureLogging(logging =>
        {   
            logging.AddSerilog();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
}
catch (Exception ex)
{
    Log.Fatal(ex, "Host builder error");

    throw;
}
finally
{
    Log.CloseAndFlush();
}
```

## Configure a service that depends on `ILogger`

Constructor injection of a logger into `Startup` works in earlier versions of ASP.NET Core because a separate DI container is created for the [Web Host](../host/web-host?view=aspnetcore-10.0). For information about why only one container is created for the [Generic Host](../host/generic-host?view=aspnetcore-10.0), see the [breaking change announcement](https://github.com/aspnet/Announcements/issues/353).

To configure a service that depends on [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger), use constructor injection or provide a factory method. The factory method approach is recommended only if there's no other option. For example, consider a service that requires a logger instance provided by dependency injection (DI):

```
services.AddSingleton<ILoggingService>((container) =>
{
    var logger = container.GetRequiredService<ILogger<LoggingService>>();
    return new LoggingService() { Logger = logger };
});
```

The preceding code is a [Func\<T,TResult>](/en-us/dotnet/api/system.func-2) that runs the first time the DI container constructs an instance of `LoggerService`. Access any registered service using this pattern.

## Logging bug reports

File a logging bug report in the [`dotnet/runtime` GitHub repository issues](https://github.com/dotnet/runtime/issues).

## Common log categories

This section describes common log categories seen in ASP.NET Core app logs. The following isn't a comprehensive list.

`Microsoft.AspNetCore`: Logs from the ASP.NET Core framework components, such as hosting, routing, and middleware.

Authentication

* `Microsoft.AspNetCore.Authentication`: Logs from the authentication middleware and services, including authentication scheme handling.
* `Microsoft.AspNetCore.Authentication.Cookies`: Logs specific to cookie-based authentication.
* `Microsoft.AspNetCore.Authentication.JwtBearer`: Logs related to JWT bearer token authentication.
* `Microsoft.AspNetCore.Authentication.OpenIdConnect`: Logs concerning OpenID Connect authentication processes.
* `Microsoft.AspNetCore.Authentication.OAuth`: Logs related to OAuth authentication and authorization flows.

Authorization

* `Microsoft.AspNetCore.Authorization`: Logs related to authorization operations, including policy evaluation and decision making.
* `Microsoft.AspNetCore.Authorization.DefaultAuthorizationService`: Logs about the default.

Configuration

* `Microsoft.Extensions.Configuration.Json`: Logs from classes that obtain configuration data from JSON files.
* `Microsoft.Extensions.Configuration.UserSecrets`: Logs related to loading user secrets configuration data.

CORS

* `Microsoft.AspNetCore.Cors`: Logs related to Cross-Origin Resource Sharing (CORS) middleware and policy evaluation.
* `Microsoft.AspNetCore.Cors.Infrastructure`: Logs concerning CORS policy configuration and enforcement.

Data Protection:

* `Microsoft.AspNetCore.DataProtection`: Logs from the data protection system, including key management, encryption operations, and which keys were considered, found, and used.
* `Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager`: Logs specific to the XML key manager, including key storage and retrieval.

Diagnostics

* `Microsoft.AspNetCore.Diagnostics`: Logs about diagnostics and error handling middleware, including exception handling and status code pages.
* `Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware`: Logs specific to developer exception page middleware processing.
* `Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware`: Logs related to exception handling and error response generation.
* `Microsoft.AspNetCore.Diagnostics.StatusCodePageMiddleware`: Logs related to status code page middleware and response handling.

Host Filtering

* `Microsoft.AspNetCore.HostFiltering`: Hosts allowed and denied by the host-filtering middleware.
* `Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware`: Logs related to the host-filtering middleware, including allowed and denied hosts.
* `Microsoft.AspNetCore.HostFiltering.HostFilteringOptions`: Logs concerning options for the host-filtering middleware.

Hosting

* `Microsoft.AspNetCore.Hosting.Lifetime`: Logs related to the lifecycle of the web host, including starting and stopping events.
* `Microsoft.AspNetCore.Hosting.Diagnostics`: Logs diagnostics information, such as application startup and shutdown.
* `Microsoft.AspNetCore.Hosting.RequestDelegate`: Logs related to the handling of HTTP requests by the application pipeline.
* `Microsoft.AspNetCore.Hosting.Internal.WebHost`: Internal logs from the web host, useful for debugging host-related issues.
* `Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor`: Logs concerning the execution of hosted services by the web host.

HTTP

* `Microsoft.AspNetCore.Http.ConnectionLogging`: Related to HTTP connections, including connection establishment and termination.
* `Microsoft.AspNetCore.Http.DefaultHttpContext`: Logs related to the creation and usage of HttpContext instances.
* `Microsoft.AspNetCore.Http.Endpoints.EndpointMiddleware`: Logs about endpoint routing and middleware execution.
* `Microsoft.AspNetCore.Http.Response`: Logs related to HTTP response processing.

HTTPS

* `Microsoft.AspNetCore.HttpsPolicy`: Logs from HTTPS redirection middleware, policy enforcement and HTTP Strict-Transport-Security (HSTS).
* `Microsoft.AspNetCore.HttpsPolicy.HstsMiddleware`: Logs specific to HTTP Strict-Transport-Security (HSTS) middleware processing.
* `Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware`: Logs related to HTTPS redirection middleware execution.
* `Microsoft.AspNetCore.HttpsPolicy.HstsOptions`: Logs concerning HSTS policy configuration and enforcement.

Identity

* `Microsoft.AspNetCore.Identity`: Logs from the ASP.NET Core Identity framework, including user management and identity operations.
* `Microsoft.AspNetCore.Identity.RoleManager`: Logs related to role management operations.
* `Microsoft.AspNetCore.Identity.UserManager`: Logs concerning user management activities and lifecycle events.

Kestrel

* `Microsoft.AspNetCore.Server.Kestrel`: Logs from the Kestrel web server, covering connection handling and request processing.
* `Microsoft.AspNetCore.Server.Kestrel.Core`: Logs related to core Kestrel operations, such as configuration and resource management.
* `Microsoft.AspNetCore.Server.Kestrel.Transport`: Logs specific to network transport layers used by Kestrel.

Logging

* `Microsoft.Extensions.Logging`: Logs from the logging extensions API.
* `Microsoft.Extensions.Logging.Console`: Logs specific to the Console logger.

MVC

* `Microsoft.AspNetCore.Mvc`: General logs from MVC framework components, including controller and action execution.
* `Microsoft.AspNetCore.Mvc.Infrastructure`: Logs related to the infrastructure and support services for MVC, such as model binding and action filters.
* `Microsoft.AspNetCore.Mvc.ModelBinding`: Logs concerning model binding operations and data validation.
* `Microsoft.AspNetCore.Mvc.Filters`: Logs about the execution of action filters and filter pipelines.
* `Microsoft.AspNetCore.Mvc.Razor`: Logs specific to Razor view rendering and compilation.
* `Microsoft.AspNetCore.Mvc.ViewFeatures`: Logs concerning view rendering and related features like view components and tag helpers.

Routing

* `Microsoft.AspNetCore.Routing.EndpointMiddleware`: Logs related to the routing of HTTP requests to endpoints.
* `Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware`: Logs about the endpoint routing middleware handling requests.
* `Microsoft.AspNetCore.Routing.Matching.DataSourceDependentMatcher`: Logs concerning route matching and selection of endpoints.
* `Microsoft.AspNetCore.Routing.Matching.DfaMatcher`: Logs specific to the DFA (Deterministic Finite Automaton) routing matcher.

SignalR

* `Microsoft.AspNetCore.SignalR`: Logs from the SignalR framework, including hub connections and message handling.
* `Microsoft.AspNetCore.SignalR.Hub`: Logs specific to hub invocation and message dispatching.
* `Microsoft.AspNetCore.SignalR.Transports`: Logs related to transport mechanisms used by SignalR.

Static files

* `Microsoft.AspNetCore.StaticFiles`: Logs from the static files middleware, including file serving and cache operations.
* `Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware`: Logs related to static file middleware execution and file response handling.

## Related content

* [ASP.NET Core Blazor logging](../../blazor/fundamentals/logging?view=aspnetcore-10.0)
* [Improving logging performance with source generators](https://andrewlock.net/exploring-dotnet-6-part-8-improving-logging-performance-with-source-generators/)
* [Behind `[LogProperties]` and the new telemetry logging source generator](https://andrewlock.net/behind-logproperties-and-the-new-telemetry-logging-source-generator/)
* [`Microsoft.Extensions.Logging` reference source (`dotnet/runtime`) GitHub repository](https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.Logging)
* [High performance logging](/en-us/dotnet/core/extensions/high-performance-logging)
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/logging) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor Hybrid

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains ASP.NET Core Blazor Hybrid, a way to build interactive client-side web UI with .NET in an ASP.NET Core app.

Use *Blazor Hybrid* to blend desktop and mobile native client frameworks with .NET and Blazor.

In a Blazor Hybrid app, [Razor components](../components/?view=aspnetcore-10.0) run natively on the device. Components render to an embedded Web View control through a local interop channel. Components don't run in the browser, and WebAssembly isn't involved. Razor components load and execute code quickly, and components have full access to the native capabilities of the device through the .NET platform. Component styles rendered in a Web View are platform dependent and may require you to account for rendering differences across platforms using custom stylesheets.

Blazor Hybrid articles cover subjects pertaining to integrating [Razor components](../components/?view=aspnetcore-10.0) into native client frameworks.

## Blazor Hybrid apps with .NET MAUI

Blazor Hybrid support is built into the [.NET Multi-platform App UI (.NET MAUI)](/en-us/dotnet/maui/what-is-maui) framework. .NET MAUI includes the [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) control that permits rendering [Razor components](../components/?view=aspnetcore-10.0) into an embedded Web View. By using .NET MAUI and Blazor together, you can reuse one set of web UI components across mobile, desktop, and web.

## Blazor Hybrid apps with WPF and Windows Forms

Blazor Hybrid apps can be built with [Windows Presentation Foundation (WPF)](/en-us/dotnet/desktop/wpf/overview/) and [Windows Forms](/en-us/dotnet/desktop/winforms/overview/). Blazor provides `BlazorWebView` controls for both of these frameworks ([WPF `BlazorWebView`](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.wpf.blazorwebview), [Windows Forms `BlazorWebView`](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.windowsforms.blazorwebview)). Razor components run natively in the Windows desktop and render to an embedded Web View. Using Blazor in WPF and Windows Forms enables you to add new UI to your existing Windows desktop apps that can be reused across platforms with .NET MAUI or on the web.

## Web View configuration

Blazor Hybrid exposes the underlying Web View configuration for different platforms through events of the `BlazorWebView` control:

* `BlazorWebViewInitializing` provides access to the settings used to create the Web View on each platform, if settings are available.
* `BlazorWebViewInitialized` provides access to the Web View to allow further configuration of the settings.

Use the preferred patterns on each platform to attach event handlers to the events to execute your custom code.

API documentation:

* .NET MAUI

  * [BlazorWebViewInitializing](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview.blazorwebviewinitializing#microsoft-aspnetcore-components-webview-maui-blazorwebview-blazorwebviewinitializing)
  * [BlazorWebViewInitialized](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview.blazorwebviewinitialized#microsoft-aspnetcore-components-webview-maui-blazorwebview-blazorwebviewinitialized)

* WPF

  * [BlazorWebViewInitializing](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.wpf.blazorwebview.blazorwebviewinitializing#microsoft-aspnetcore-components-webview-wpf-blazorwebview-blazorwebviewinitializing)
  * [BlazorWebViewInitialized](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.wpf.blazorwebview.blazorwebviewinitialized#microsoft-aspnetcore-components-webview-wpf-blazorwebview-blazorwebviewinitialized)

* Windows Forms

  * [BlazorWebViewInitializing](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.windowsforms.blazorwebview.blazorwebviewinitializing#microsoft-aspnetcore-components-webview-windowsforms-blazorwebview-blazorwebviewinitializing)
  * [BlazorWebViewInitialized](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.windowsforms.blazorwebview.blazorwebviewinitialized#microsoft-aspnetcore-components-webview-windowsforms-blazorwebview-blazorwebviewinitialized)

## Unhandled exceptions in Windows Forms and WPF apps

*This section only applies to Windows Forms and WPF Blazor Hybrid apps.*

Create a callback for `UnhandledException` on the [System.AppDomain.CurrentDomain](/en-us/dotnet/api/system.appdomain.currentdomain#system-appdomain-currentdomain) property. The following example uses a [compiler directive](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-if) to display a [MessageBox](/en-us/dotnet/api/system.windows.forms.messagebox) that either alerts the user that an error has occurred or shows the error information to the developer. Log the error information in `error.ExceptionObject`.

```
AppDomain.CurrentDomain.UnhandledException += (sender, error) =>
{
#if DEBUG
    MessageBox.Show(text: error.ExceptionObject.ToString(), caption: "Error");
#else
    MessageBox.Show(text: "An error has occurred.", caption: "Error");
#endif
    
    // Log the error information (error.ExceptionObject)
};
```

## Globalization and localization

*This section only applies to .NET MAUI Blazor Hybrid apps.*

.NET MAUI configures the [CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture) and [CurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentuiculture#system-globalization-cultureinfo-currentuiculture) based on the device's ambient information.

[IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer) and other API in the [Microsoft.Extensions.Localization](/en-us/dotnet/api/microsoft.extensions.localization) namespace generally work as expected, along with globalization formatting, parsing, and binding that relies on the user's culture.

When dynamically changing the app culture at runtime, the app must be reloaded to reflect the change in culture, which takes care of rerendering the root component and passing the new culture to rerendered child components.

.NET's resource system supports embedding localized images (as blobs) into an app, but Blazor Hybrid can't display the embedded images in Razor components at this time. Even if a user reads an image's bytes into a [Stream](/en-us/dotnet/api/system.io.stream) using [ResourceManager](/en-us/dotnet/api/system.resources.resourcemanager), the framework doesn't currently support rendering the retrieved image in a Razor component.

For more information, see the following resources:

* [Localization (.NET MAUI documentation)](/en-us/dotnet/maui/fundamentals/localization)
* [Blazor Image component to display images that are not accessible through HTTP endpoints (dotnet/aspnetcore #25274)](https://github.com/dotnet/aspnetcore/issues/25274)

## Access scoped services from native UI

[BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) has a [TryDispatchAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.wpf.blazorwebview.trydispatchasync) method that calls a specified `Action<ServiceProvider>` asynchronously and passes in the scoped services available in Razor components. This enables code from the native UI to access scoped services such as [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager):

```
private async void MyMauiButtonHandler(object sender, EventArgs e)
{
    var wasDispatchCalled = await _blazorWebView.TryDispatchAsync(sp =>
    {
        var navMan = sp.GetRequiredService<NavigationManager>();
        navMan.CallSomeNavigationApi(...);
    });

    if (!wasDispatchCalled)
    {
        ...
    }
}
```

When `wasDispatchCalled` is `false`, consider what to do if the call wasn't dispatched. Generally, the dispatch shouldn't fail. If it fails, OS resources might be exhausted. If resources are exhausted, consider logging a message, throwing an exception, and perhaps alerting the user.

## Related content

* [ASP.NET Core Blazor Hybrid tutorials](tutorials/?view=aspnetcore-10.0)
* [.NET Multi-platform App UI (.NET MAUI)](/en-us/dotnet/maui/what-is-maui)
* [Windows Presentation Foundation (WPF)](/en-us/dotnet/desktop/wpf/overview/)
* [Windows Forms](/en-us/dotnet/desktop/winforms/overview/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/platform-specific-configuration
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/host/platform-specific-configuration.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use hosting startup assemblies in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Pavel Krymets](https://github.com/pakrym)

An [IHostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup) (hosting startup) implementation adds enhancements to an app at startup from an external assembly. For example, an external library can use a hosting startup implementation to provide additional configuration providers or services to an app.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/platform-specific-configuration/samples/) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## HostingStartup attribute

A [HostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingstartupattribute) attribute indicates the presence of a hosting startup assembly to activate at runtime.

The entry assembly or the assembly containing the `Startup` class is automatically scanned for the `HostingStartup` attribute. The list of assemblies to search for `HostingStartup` attributes is loaded at runtime from configuration in the [WebHostDefaults.HostingStartupAssembliesKey](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostdefaults.hostingstartupassemblieskey#microsoft-aspnetcore-hosting-webhostdefaults-hostingstartupassemblieskey). The list of assemblies to exclude from discovery is loaded from the [WebHostDefaults.HostingStartupExcludeAssembliesKey](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostdefaults.hostingstartupexcludeassemblieskey#microsoft-aspnetcore-hosting-webhostdefaults-hostingstartupexcludeassemblieskey).

In the following example, the namespace of the hosting startup assembly is `StartupEnhancement`. The class containing the hosting startup code is `StartupEnhancementHostingStartup`:

```
[assembly: HostingStartup(typeof(StartupEnhancement.StartupEnhancementHostingStartup))]
```

The `HostingStartup` attribute is typically located in the hosting startup assembly's `IHostingStartup` implementation class file.

## Discover loaded hosting startup assemblies

To discover loaded hosting startup assemblies, enable logging and check the app's logs. Errors that occur when loading assemblies are logged. Loaded hosting startup assemblies are logged at the Debug level, and all errors are logged.

## Disable automatic loading of hosting startup assemblies

To disable automatic loading of hosting startup assemblies, use one of the following approaches:

* To prevent all hosting startup assemblies from loading, set one of the following to `true` or `1`:

  * Prevent Hosting Startup host configuration setting:

    ```
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseSetting(
                        WebHostDefaults.PreventHostingStartupKey, "true")
                    .UseStartup<Startup>();
            });
    ```

  * `ASPNETCORE_PREVENTHOSTINGSTARTUP` environment variable.

* To prevent specific hosting startup assemblies from loading, set one of the following to a semicolon-delimited string of hosting startup assemblies to exclude at startup:

  * Hosting Startup Exclude Assemblies host configuration setting:

    ```
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseSetting(
                        WebHostDefaults.HostingStartupExcludeAssembliesKey, 
                        "{ASSEMBLY1;ASSEMBLY2; ...}")
                    .UseStartup<Startup>();
            });
    ```

    The `{ASSEMBLY1;ASSEMBLY2; ...}` placeholder represents the semicolon-separated list of assemblies.

  * `ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES` environment variable.

If both the host configuration setting and the environment variable are set, the host setting controls the behavior.

Disabling hosting startup assemblies using the host setting or environment variable disables the assembly globally and may disable several characteristics of an app.

## Project

Create a hosting startup with either of the following project types:

* [Class library](#class-library)
* [Console app without an entry point](#console-app-without-an-entry-point)

### Class library

A hosting startup enhancement can be provided in a class library. The library contains a `HostingStartup` attribute.

The [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/platform-specific-configuration/samples/) includes a Razor Pages app, *HostingStartupApp*, and a class library, *HostingStartupLibrary*. The class library:

* Contains a hosting startup class, `ServiceKeyInjection`, which implements `IHostingStartup`. `ServiceKeyInjection` adds a pair of service strings to the app's configuration using the in-memory configuration provider ([AddInMemoryCollection](/en-us/dotnet/api/microsoft.extensions.configuration.memoryconfigurationbuilderextensions.addinmemorycollection)).
* Includes a `HostingStartup` attribute that identifies the hosting startup's namespace and class.

The `ServiceKeyInjection` class's [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup.configure) method uses an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) to add enhancements to an app.

`HostingStartupLibrary/ServiceKeyInjection.cs`:

```
[assembly: HostingStartup(typeof(HostingStartupLibrary.ServiceKeyInjection))]

namespace HostingStartupLibrary
{
    public class ServiceKeyInjection : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureAppConfiguration(config =>
            {
                var dict = new Dictionary<string, string>
                {
                    {"DevAccount_FromLibrary", "DEV_1111111-1111"},
                    {"ProdAccount_FromLibrary", "PROD_2222222-2222"}
                };

                config.AddInMemoryCollection(dict);
            });
        }
    }
}
```

The app's Index page reads and renders the configuration values for the two keys set by the class library's hosting startup assembly:

`HostingStartupApp/Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    public IndexModel(IConfiguration config)
    {
        ServiceKey_Development_Library = config["DevAccount_FromLibrary"];
        ServiceKey_Production_Library = config["ProdAccount_FromLibrary"];
        ServiceKey_Development_Package = config["DevAccount_FromPackage"];
        ServiceKey_Production_Package = config["ProdAccount_FromPackage"];
    }

    public string ServiceKey_Development_Library { get; private set; }
    public string ServiceKey_Production_Library { get; private set; }
    public string ServiceKey_Development_Package { get; private set; }
    public string ServiceKey_Production_Package { get; private set; }

    public void OnGet()
    {
    }
}
```

The [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/platform-specific-configuration/samples/) also includes a NuGet package project that provides a separate hosting startup, *HostingStartupPackage*. The package has the same characteristics of the class library described earlier. The package:

* Contains a hosting startup class, `ServiceKeyInjection`, which implements `IHostingStartup`. `ServiceKeyInjection` adds a pair of service strings to the app's configuration.
* Includes a `HostingStartup` attribute.

`HostingStartupPackage/ServiceKeyInjection.cs`:

```
[assembly: HostingStartup(typeof(HostingStartupPackage.ServiceKeyInjection))]

namespace HostingStartupPackage
{
    public class ServiceKeyInjection : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureAppConfiguration(config =>
            {
                var dict = new Dictionary<string, string>
                {
                    {"DevAccount_FromPackage", "DEV_3333333-3333"},
                    {"ProdAccount_FromPackage", "PROD_4444444-4444"}
                };

                config.AddInMemoryCollection(dict);
            });
        }
    }
}
```

The app's Index page reads and renders the configuration values for the two keys set by the package's hosting startup assembly:

`HostingStartupApp/Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    public IndexModel(IConfiguration config)
    {
        ServiceKey_Development_Library = config["DevAccount_FromLibrary"];
        ServiceKey_Production_Library = config["ProdAccount_FromLibrary"];
        ServiceKey_Development_Package = config["DevAccount_FromPackage"];
        ServiceKey_Production_Package = config["ProdAccount_FromPackage"];
    }

    public string ServiceKey_Development_Library { get; private set; }
    public string ServiceKey_Production_Library { get; private set; }
    public string ServiceKey_Development_Package { get; private set; }
    public string ServiceKey_Production_Package { get; private set; }

    public void OnGet()
    {
    }
}
```

### Console app without an entry point

*This approach is only available for .NET apps, not .NET Framework.*

A dynamic hosting startup enhancement that doesn't require a compile-time reference for activation can be provided in a console app without an entry point that contains a `HostingStartup` attribute. Publishing the console app produces a hosting startup assembly that can be consumed from the runtime store.

A console app without an entry point is used in this process because:

* A dependencies file is required to consume the hosting startup in the hosting startup assembly. A dependencies file is a runnable app asset that's produced by publishing an app, not a library.
* A library can't be added directly to the [runtime package store](/en-us/dotnet/core/deploying/runtime-store), which requires a runnable project that targets the shared runtime.

In the creation of a dynamic hosting startup:

* A hosting startup assembly is created from the console app without an entry point that:

  * Includes a class that contains the `IHostingStartup` implementation.
  * Includes a [HostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingstartupattribute) attribute to identify the `IHostingStartup` implementation class.

* The console app is published to obtain the hosting startup's dependencies. A consequence of publishing the console app is that unused dependencies are trimmed from the dependencies file.

* The dependencies file is modified to set the runtime location of the hosting startup assembly.

* The hosting startup assembly and its dependencies file is placed into the runtime package store. To discover the hosting startup assembly and its dependencies file, they're listed in a pair of environment variables.

The console app references the [Microsoft.AspNetCore.Hosting.Abstractions](https://www.nuget.org/packages/Microsoft.AspNetCore.Hosting.Abstractions/) package:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" 
                      Version="3.0.0" />
  </ItemGroup>

</Project>
```

A [HostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingstartupattribute) attribute identifies a class as an implementation of `IHostingStartup` for loading and execution when building the [IWebHost](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhost). In the following example, the namespace is `StartupEnhancement`, and the class is `StartupEnhancementHostingStartup`:

```
[assembly: HostingStartup(typeof(StartupEnhancement.StartupEnhancementHostingStartup))]
```

A class implements `IHostingStartup`. The class's [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup.configure) method uses an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) to add enhancements to an app. `IHostingStartup.Configure` in the hosting startup assembly is called by the runtime before `Startup.Configure` in user code, which allows user code to overwrite any configuration provided by the hosting startup assembly.

```
namespace StartupEnhancement
{
    public class StartupEnhancementHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            // Use the IWebHostBuilder to add app enhancements.
        }
    }
}
```

When building an `IHostingStartup` project, the dependencies file (`.deps.json`) sets the `runtime` location of the assembly to the *bin* folder:

```
"targets": {
  ".NETCoreApp,Version=v3.0": {
    "StartupEnhancement/1.0.0": {
      "dependencies": {
        "Microsoft.AspNetCore.Hosting.Abstractions": "3.0.0"
      },
      "runtime": {
        "StartupEnhancement.dll": {}
      }
    }
  }
}
```

Only part of the file is shown. The assembly name in the example is `StartupEnhancement`.

## Configuration provided by the hosting startup

There are two approaches to handling configuration depending on whether you want the hosting startup's configuration to take precedence or the app's configuration to take precedence:

1. Provide configuration to the app using [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder.configureappconfiguration) to load the configuration after the app's [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder.configureappconfiguration) delegates execute. Hosting startup configuration takes priority over the app's configuration using this approach.
2. Provide configuration to the app using [UseConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useconfiguration) to load the configuration before the app's [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder.configureappconfiguration) delegates execute. The app's configuration values take priority over those provided by the hosting startup using this approach.

```
public class ConfigurationInjection : IHostingStartup
{
    public void Configure(IWebHostBuilder builder)
    {
        Dictionary<string, string> dict;

        builder.ConfigureAppConfiguration(config =>
        {
            dict = new Dictionary<string, string>
            {
                {"ConfigurationKey1", 
                    "From IHostingStartup: Higher priority " +
                    "than the app's configuration."},
            };

            config.AddInMemoryCollection(dict);
        });

        dict = new Dictionary<string, string>
        {
            {"ConfigurationKey2", 
                "From IHostingStartup: Lower priority " +
                "than the app's configuration."},
        };

        var builtConfig = new ConfigurationBuilder()
            .AddInMemoryCollection(dict)
            .Build();

        builder.UseConfiguration(builtConfig);
    }
}
```

## Specify the hosting startup assembly

For either a class library- or console app-supplied hosting startup, specify the hosting startup assembly's name in the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable. The environment variable is a semicolon-delimited list of assemblies.

Only hosting startup assemblies are scanned for the `HostingStartup` attribute. For the sample app, *HostingStartupApp*, to discover the hosting startups described earlier, the environment variable is set to the following value:

```
HostingStartupLibrary;HostingStartupPackage;StartupDiagnostics
```

A hosting startup assembly can also be set using the Hosting Startup Assemblies host configuration setting:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseSetting(
                    WebHostDefaults.HostingStartupAssembliesKey, 
                    "{ASSEMBLY1;ASSEMBLY2; ...}")
                .UseStartup<Startup>();
        });
```

The `{ASSEMBLY1;ASSEMBLY2; ...}` placeholder represents the semicolon-separated list of assemblies.

When multiple hosting startup assembles are present, their [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup.configure) methods are executed in the order that the assemblies are listed.

## Activation

Options for hosting startup activation are:

* [Runtime store](#runtime-store) (**deprecated**): Activation doesn't require a compile-time reference for activation. The sample app places the hosting startup assembly and dependencies files into a folder, *deployment*, to facilitate deployment of the hosting startup in a multimachine environment. The *deployment* folder also includes a PowerShell script that creates or modifies environment variables on the deployment system to enable the hosting startup. The [runtime package store](/en-us/dotnet/core/deploying/runtime-store) is deprecated, so we recommend the compile-time reference options that follow.

* Compile-time reference required for activation

  * [NuGet package](#nuget-package)
  * [Project bin folder](#project-bin-folder)

### Runtime store

Warning

The runtime package store is deprecated. It's no longer supported or under active development, and the `dotnet store` command has known issues with .NET 6 and later versions. Use a [NuGet package](#nuget-package) or the [project bin folder](#project-bin-folder) for hosting startup activation instead. For more information, see [Runtime package store](/en-us/dotnet/core/deploying/runtime-store).

The hosting startup implementation is placed in the [runtime store](/en-us/dotnet/core/deploying/runtime-store). A compile-time reference to the assembly isn't required by the enhanced app.

After the hosting startup is built, a runtime store is generated using the manifest project file and the [dotnet store](/en-us/dotnet/core/tools/dotnet-store) command.

```
dotnet store --manifest {MANIFEST FILE} --runtime {RUNTIME IDENTIFIER} --output {OUTPUT LOCATION} --skip-optimization
```

In the sample app (*RuntimeStore* project) the following command is used:

```
dotnet store --manifest store.manifest.csproj --runtime win7-x64 --output ./deployment/store --skip-optimization
```

For the runtime to discover the runtime store, the runtime store's location is added to the `DOTNET_SHARED_STORE` environment variable.

**Modify and place the hosting startup's dependencies file**

To activate the enhancement without a package reference to the enhancement, specify additional dependencies to the runtime with `additionalDeps`. `additionalDeps` allows you to:

* Extend the app's library graph by providing a set of additional `.deps.json` files to merge with the app's own `.deps.json` file on startup.
* Make the hosting startup assembly discoverable and loadable.

The recommended approach for generating the additional dependencies file is to:

1. Execute `dotnet publish` on the runtime store manifest file referenced in the previous section.
2. Remove the manifest reference from libraries and the `runtime` section of the resulting `.deps.json` file.

In the example project, the `store.manifest/1.0.0` property is removed from the `targets` and `libraries` section:

```
{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v3.0",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v3.0": {
      "store.manifest/1.0.0": {
        "dependencies": {
          "StartupDiagnostics": "1.0.0"
        },
        "runtime": {
          "store.manifest.dll": {}
        }
      },
      "StartupDiagnostics/1.0.0": {
        "runtime": {
          "lib/netcoreapp3.0/StartupDiagnostics.dll": {
            "assemblyVersion": "1.0.0.0",
            "fileVersion": "1.0.0.0"
          }
        }
      }
    }
  },
  "libraries": {
    "store.manifest/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    },
    "StartupDiagnostics/1.0.0": {
      "type": "package",
      "serviceable": true,
      "sha512": "sha512-xrhzuNSyM5/f4ZswhooJ9dmIYLP64wMnqUJSyTKVDKDVj5T+qtzypl8JmM/aFJLLpYrf0FYpVWvGujd7/FfMEw==",
      "path": "startupdiagnostics/1.0.0",
      "hashPath": "startupdiagnostics.1.0.0.nupkg.sha512"
    }
  }
}
```

Place the `.deps.json` file into the following location:

```
{ADDITIONAL DEPENDENCIES PATH}/shared/{SHARED FRAMEWORK NAME}/{SHARED FRAMEWORK VERSION}/{ENHANCEMENT ASSEMBLY NAME}.deps.json
```

* `{ADDITIONAL DEPENDENCIES PATH}`: Location added to the `DOTNET_ADDITIONAL_DEPS` environment variable.
* `{SHARED FRAMEWORK NAME}`: Shared framework required for this additional dependencies file.
* `{SHARED FRAMEWORK VERSION}`: Minimum shared framework version.
* `{ENHANCEMENT ASSEMBLY NAME}`: The enhancement's assembly name.

In the sample app (*RuntimeStore* project), the additional dependencies file is placed into the following location:

```
deployment/additionalDeps/shared/Microsoft.AspNetCore.App/3.0.0/StartupDiagnostics.deps.json
```

For runtime to discover the runtime store location, the additional dependencies file location is added to the `DOTNET_ADDITIONAL_DEPS` environment variable.

Note

The `DOTNET_ADDITIONAL_DEPS` environment variable only works with framework-dependent applications. This environment variable is ignored for self-contained applications.

In the sample app (*RuntimeStore* project), building the runtime store and generating the additional dependencies file is accomplished using a [PowerShell](/en-us/powershell/scripting/overview) script.

For examples of how to set environment variables for various operating systems, see [Use multiple environments](../environments?view=aspnetcore-10.0).

**Deployment**

To facilitate the deployment of a hosting startup in a multimachine environment, the sample app creates a *deployment* folder in published output that contains:

* The hosting startup runtime store.
* The hosting startup dependencies file.
* A PowerShell script that creates or modifies the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES`, `DOTNET_SHARED_STORE`, and `DOTNET_ADDITIONAL_DEPS` to support the activation of the hosting startup. Run the script from an administrative PowerShell command prompt on the deployment system.

### NuGet package

A hosting startup enhancement can be provided in a NuGet package. The package has a `HostingStartup` attribute. The hosting startup types provided by the package are made available to the app using either of the following approaches:

* The enhanced app's project file makes a package reference for the hosting startup in the app's project file (a compile-time reference). With the compile-time reference in place, the hosting startup assembly and all of its dependencies are incorporated into the app's dependency file (`.deps.json`). This approach applies to a hosting startup assembly package published to [nuget.org](https://www.nuget.org/).
* The hosting startup's dependencies file is made available to the enhanced app as described in the [Runtime store](#runtime-store) section (without a compile-time reference).

For more information on NuGet packages and the runtime store, see the following topics:

* [How to Create a NuGet Package with Cross Platform Tools](/en-us/dotnet/core/deploying/creating-nuget-packages)
* [Publishing packages](/en-us/nuget/create-packages/publish-a-package)
* [Runtime package store](/en-us/dotnet/core/deploying/runtime-store)

### Project bin folder

A hosting startup enhancement can be provided by a *bin*-deployed assembly in the enhanced app. The hosting startup types provided by the assembly are made available to the app using one of the following approaches:

* The enhanced app's project file makes an assembly reference to the hosting startup (a compile-time reference). With the compile-time reference in place, the hosting startup assembly and all of its dependencies are incorporated into the app's dependency file (`.deps.json`). This approach applies when the deployment scenario calls for making a compile-time reference to the hosting startup's assembly (*.dll* file) and moving the assembly to either:

  * The consuming project.
  * A location accessible by the consuming project.

* The hosting startup's dependencies file is made available to the enhanced app as described in the [Runtime store](#runtime-store) section (without a compile-time reference).

* When targeting the .NET Framework, the assembly is loadable in the default load context, which on .NET Framework means that the assembly is located at either of the following locations:

  * Application base path: The *bin* folder where the app's executable (*.exe*) is located.
  * Global Assembly Cache (GAC): The GAC stores assemblies that several .NET Framework apps share. For more information, see [How to: Install an assembly into the global assembly cache](/en-us/dotnet/framework/app-domains/how-to-install-an-assembly-into-the-gac) in the .NET Framework documentation.

## Sample code

The [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/platform-specific-configuration/samples/) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample)) demonstrates hosting startup implementation scenarios:

* Two hosting startup assemblies (class libraries) set a pair of in-memory configuration key-value pairs each:

  * NuGet package (*HostingStartupPackage*)
  * Class library (*HostingStartupLibrary*)

* A hosting startup is activated from a runtime store-deployed assembly (*StartupDiagnostics*). The assembly adds two middlewares to the app at startup that provide diagnostic information on:

  * Registered services
  * Address (scheme, host, path base, path, query string)
  * Connection (remote IP, remote port, local IP, local port, client certificate)
  * Request headers
  * Environment variables

To run the sample:

**Activation from a NuGet package**

1. Compile the *HostingStartupPackage* package with the [dotnet pack](/en-us/dotnet/core/tools/dotnet-pack) command.

2. Add the package's assembly name of the *HostingStartupPackage* to the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable.

3. Compile and run the app. A package reference is present in the enhanced app (a compile-time reference). A `<PropertyGroup>` in the app's project file specifies the package project's output (*../HostingStartupPackage/bin/Debug*) as a package source. This allows the app to use the package without uploading the package to [nuget.org](https://www.nuget.org/). For more information, see the notes in the HostingStartupApp's project file.

   ```
   <PropertyGroup>
     <RestoreSources>$(RestoreSources);https://api.nuget.org/v3/index.json;../HostingStartupPackage/bin/Debug</RestoreSources>
   </PropertyGroup>
   ```

4. Observe that the service configuration key values rendered by the Index page match the values set by the package's `ServiceKeyInjection.Configure` method.

If you make changes to the *HostingStartupPackage* project and recompile it, clear the local NuGet package caches to ensure that the *HostingStartupApp* receives the updated package and not a stale package from the local cache. To clear the local NuGet caches, execute the following [dotnet nuget locals](/en-us/dotnet/core/tools/dotnet-nuget-locals) command:

```
dotnet nuget locals all --clear
```

**Activation from a class library**

1. Compile the *HostingStartupLibrary* class library with the [dotnet build](/en-us/dotnet/core/tools/dotnet-build) command.

2. Add the class library's assembly name of *HostingStartupLibrary* to the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable.

3. *bin*-deploy the class library's assembly to the app by copying the *HostingStartupLibrary.dll* file from the class library's compiled output to the app's *bin/Debug* folder.

4. Compile and run the app. An `<ItemGroup>` in the app's project file references the class library's assembly (*.\bin\Debug\netcoreapp3.0\HostingStartupLibrary.dll*) (a compile-time reference). For more information, see the notes in the HostingStartupApp's project file.

   ```
   <ItemGroup>
     <Reference Include=".\\bin\\Debug\\netcoreapp3.0\\HostingStartupLibrary.dll">
       <HintPath>.\bin\Debug\netcoreapp3.0\HostingStartupLibrary.dll</HintPath>
       <SpecificVersion>False</SpecificVersion> 
     </Reference>
   </ItemGroup>
   ```

5. Observe that the service configuration key values rendered by the Index page match the values set by the class library's `ServiceKeyInjection.Configure` method.

**Activation from a runtime store-deployed assembly**

1. The *StartupDiagnostics* project uses [PowerShell](/en-us/powershell/scripting/overview) to modify its `StartupDiagnostics.deps.json` file. PowerShell is installed by default on Windows starting with Windows 7 SP1 and Windows Server 2008 R2 SP1. To obtain PowerShell on other platforms, see [Installing various versions of PowerShell](/en-us/powershell/scripting/install/installing-powershell).

2. Execute the *build.ps1* script in the *RuntimeStore* folder. The script:

   * Generates the `StartupDiagnostics` package in the *obj\packages* folder.
   * Generates the runtime store for `StartupDiagnostics` in the *store* folder. The `dotnet store` command in the script uses the `win7-x64` [runtime identifier (RID)](/en-us/dotnet/core/rid-catalog) for a hosting startup deployed to Windows. When providing the hosting startup for a different runtime, substitute the correct RID on line 37 of the script. The runtime store for `StartupDiagnostics` would later be moved to the user's or system's runtime store on the machine where the assembly will be consumed. The user runtime store install location for the `StartupDiagnostics` assembly is *.dotnet/store/x64/netcoreapp3.0/startupdiagnostics/1.0.0/lib/netcoreapp3.0/StartupDiagnostics.dll*.
   * Generates the `additionalDeps` for `StartupDiagnostics` in the *additionalDeps* folder. The additional dependencies would later be moved to the user's or system's additional dependencies. The user `StartupDiagnostics` additional dependencies install location is `.dotnet/x64/additionalDeps/StartupDiagnostics/shared/Microsoft.NETCore.App/3.0.0/StartupDiagnostics.deps.json`.
   * Places the *deploy.ps1* file in the *deployment* folder.

3. Run the *deploy.ps1* script in the *deployment* folder. The script appends:

   * `StartupDiagnostics` to the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable.
   * The hosting startup dependencies path (in the RuntimeStore project's *deployment* folder) to the `DOTNET_ADDITIONAL_DEPS` environment variable.
   * The runtime store path (in the RuntimeStore project's *deployment* folder) to the `DOTNET_SHARED_STORE` environment variable.

4. Run the sample app.

5. Request the `/services` endpoint to see the app's registered services. Request the `/diag` endpoint to see the diagnostic information.

An [IHostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup) (hosting startup) implementation adds enhancements to an app at startup from an external assembly. For example, an external library can use a hosting startup implementation to provide additional configuration providers or services to an app.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/platform-specific-configuration/samples/) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## HostingStartup attribute

A [HostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingstartupattribute) attribute indicates the presence of a hosting startup assembly to activate at runtime.

The entry assembly or the assembly containing the `Startup` class is automatically scanned for the `HostingStartup` attribute. The list of assemblies to search for `HostingStartup` attributes is loaded at runtime from configuration in the [WebHostDefaults.HostingStartupAssembliesKey](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostdefaults.hostingstartupassemblieskey#microsoft-aspnetcore-hosting-webhostdefaults-hostingstartupassemblieskey). The list of assemblies to exclude from discovery is loaded from the [WebHostDefaults.HostingStartupExcludeAssembliesKey](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostdefaults.hostingstartupexcludeassemblieskey#microsoft-aspnetcore-hosting-webhostdefaults-hostingstartupexcludeassemblieskey). For more information, see [Web Host: Hosting Startup Assemblies](web-host?view=aspnetcore-10.0#hosting-startup-assemblies) and [Web Host: Hosting Startup Exclude Assemblies](web-host?view=aspnetcore-10.0#hosting-startup-exclude-assemblies).

In the following example, the namespace of the hosting startup assembly is `StartupEnhancement`. The class containing the hosting startup code is `StartupEnhancementHostingStartup`:

```
[assembly: HostingStartup(typeof(StartupEnhancement.StartupEnhancementHostingStartup))]
```

The `HostingStartup` attribute is typically located in the hosting startup assembly's `IHostingStartup` implementation class file.

## Discover loaded hosting startup assemblies

To discover loaded hosting startup assemblies, enable logging and check the app's logs. Errors that occur when loading assemblies are logged. Loaded hosting startup assemblies are logged at the Debug level, and all errors are logged.

## Disable automatic loading of hosting startup assemblies

To disable automatic loading of hosting startup assemblies, use one of the following approaches:

* To prevent all hosting startup assemblies from loading, set one of the following to `true` or `1`:

  * [Prevent Hosting Startup](web-host?view=aspnetcore-10.0#prevent-hosting-startup) host configuration setting.
  * `ASPNETCORE_PREVENTHOSTINGSTARTUP` environment variable.

* To prevent specific hosting startup assemblies from loading, set one of the following to a semicolon-delimited string of hosting startup assemblies to exclude at startup:

  * [Hosting Startup Exclude Assemblies](web-host?view=aspnetcore-10.0#hosting-startup-exclude-assemblies) host configuration setting.
  * `ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES` environment variable.

If both the host configuration setting and the environment variable are set, the host setting controls the behavior.

Disabling hosting startup assemblies using the host setting or environment variable disables the assembly globally and may disable several characteristics of an app.

## Project

Create a hosting startup with either of the following project types:

* [Class library](#class-library)
* [Console app without an entry point](#console-app-without-an-entry-point)

### Class library

A hosting startup enhancement can be provided in a class library. The library contains a `HostingStartup` attribute.

The [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/platform-specific-configuration/samples/) includes a Razor Pages app, *HostingStartupApp*, and a class library, *HostingStartupLibrary*. The class library:

* Contains a hosting startup class, `ServiceKeyInjection`, which implements `IHostingStartup`. `ServiceKeyInjection` adds a pair of service strings to the app's configuration using the in-memory configuration provider ([AddInMemoryCollection](/en-us/dotnet/api/microsoft.extensions.configuration.memoryconfigurationbuilderextensions.addinmemorycollection)).
* Includes a `HostingStartup` attribute that identifies the hosting startup's namespace and class.

The `ServiceKeyInjection` class's [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup.configure) method uses an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) to add enhancements to an app.

`HostingStartupLibrary/ServiceKeyInjection.cs`:

```
[assembly: HostingStartup(typeof(HostingStartupLibrary.ServiceKeyInjection))]

namespace HostingStartupLibrary
{
    public class ServiceKeyInjection : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureAppConfiguration(config =>
            {
                var dict = new Dictionary<string, string>
                {
                    {"DevAccount_FromLibrary", "DEV_1111111-1111"},
                    {"ProdAccount_FromLibrary", "PROD_2222222-2222"}
                };

                config.AddInMemoryCollection(dict);
            });
        }
    }
}
```

The app's Index page reads and renders the configuration values for the two keys set by the class library's hosting startup assembly:

`HostingStartupApp/Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    public IndexModel(IConfiguration config)
    {
        ServiceKey_Development_Library = config["DevAccount_FromLibrary"];
        ServiceKey_Production_Library = config["ProdAccount_FromLibrary"];
        ServiceKey_Development_Package = config["DevAccount_FromPackage"];
        ServiceKey_Production_Package = config["ProdAccount_FromPackage"];
    }

    public string ServiceKey_Development_Library { get; private set; }
    public string ServiceKey_Production_Library { get; private set; }
    public string ServiceKey_Development_Package { get; private set; }
    public string ServiceKey_Production_Package { get; private set; }

    public void OnGet()
    {
    }
}
```

The [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/platform-specific-configuration/samples/) also includes a NuGet package project that provides a separate hosting startup, *HostingStartupPackage*. The package has the same characteristics of the class library described earlier. The package:

* Contains a hosting startup class, `ServiceKeyInjection`, which implements `IHostingStartup`. `ServiceKeyInjection` adds a pair of service strings to the app's configuration.
* Includes a `HostingStartup` attribute.

`HostingStartupPackage/ServiceKeyInjection.cs`:

```
[assembly: HostingStartup(typeof(HostingStartupPackage.ServiceKeyInjection))]

namespace HostingStartupPackage
{
    public class ServiceKeyInjection : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureAppConfiguration(config =>
            {
                var dict = new Dictionary<string, string>
                {
                    {"DevAccount_FromPackage", "DEV_3333333-3333"},
                    {"ProdAccount_FromPackage", "PROD_4444444-4444"}
                };

                config.AddInMemoryCollection(dict);
            });
        }
    }
}
```

The app's Index page reads and renders the configuration values for the two keys set by the package's hosting startup assembly:

`HostingStartupApp/Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    public IndexModel(IConfiguration config)
    {
        ServiceKey_Development_Library = config["DevAccount_FromLibrary"];
        ServiceKey_Production_Library = config["ProdAccount_FromLibrary"];
        ServiceKey_Development_Package = config["DevAccount_FromPackage"];
        ServiceKey_Production_Package = config["ProdAccount_FromPackage"];
    }

    public string ServiceKey_Development_Library { get; private set; }
    public string ServiceKey_Production_Library { get; private set; }
    public string ServiceKey_Development_Package { get; private set; }
    public string ServiceKey_Production_Package { get; private set; }

    public void OnGet()
    {
    }
}
```

### Console app without an entry point

*This approach is only available for .NET apps, not .NET Framework.*

A dynamic hosting startup enhancement that doesn't require a compile-time reference for activation can be provided in a console app without an entry point that contains a `HostingStartup` attribute. Publishing the console app produces a hosting startup assembly that can be consumed from the runtime store.

A console app without an entry point is used in this process because:

* A dependencies file is required to consume the hosting startup in the hosting startup assembly. A dependencies file is a runnable app asset that's produced by publishing an app, not a library.
* A library can't be added directly to the [runtime package store](/en-us/dotnet/core/deploying/runtime-store), which requires a runnable project that targets the shared runtime.

In the creation of a dynamic hosting startup:

* A hosting startup assembly is created from the console app without an entry point that:

  * Includes a class that contains the `IHostingStartup` implementation.
  * Includes a [HostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingstartupattribute) attribute to identify the `IHostingStartup` implementation class.

* The console app is published to obtain the hosting startup's dependencies. A consequence of publishing the console app is that unused dependencies are trimmed from the dependencies file.

* The dependencies file is modified to set the runtime location of the hosting startup assembly.

* The hosting startup assembly and its dependencies file is placed into the runtime package store. To discover the hosting startup assembly and its dependencies file, they're listed in a pair of environment variables.

The console app references the [Microsoft.AspNetCore.Hosting.Abstractions](https://www.nuget.org/packages/Microsoft.AspNetCore.Hosting.Abstractions/) package:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" 
                      Version="2.1.1" />
  </ItemGroup>

</Project>
```

A [HostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingstartupattribute) attribute identifies a class as an implementation of `IHostingStartup` for loading and execution when building the [IWebHost](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhost). In the following example, the namespace is `StartupEnhancement`, and the class is `StartupEnhancementHostingStartup`:

```
[assembly: HostingStartup(typeof(StartupEnhancement.StartupEnhancementHostingStartup))]
```

A class implements `IHostingStartup`. The class's [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup.configure) method uses an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) to add enhancements to an app. `IHostingStartup.Configure` in the hosting startup assembly is called by the runtime before `Startup.Configure` in user code, which allows user code to overwrite any configuration provided by the hosting startup assembly.

```
namespace StartupEnhancement
{
    public class StartupEnhancementHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            // Use the IWebHostBuilder to add app enhancements.
        }
    }
}
```

When building an `IHostingStartup` project, the dependencies file (`.deps.json`) sets the `runtime` location of the assembly to the *bin* folder:

```
"targets": {
  ".NETCoreApp,Version=v2.1": {
    "StartupEnhancement/1.0.0": {
      "dependencies": {
        "Microsoft.AspNetCore.Hosting.Abstractions": "2.1.1"
      },
      "runtime": {
        "StartupEnhancement.dll": {}
      }
    }
  }
}
```

Only part of the file is shown. The assembly name in the example is `StartupEnhancement`.

## Configuration provided by the hosting startup

There are two approaches to handling configuration depending on whether you want the hosting startup's configuration to take precedence or the app's configuration to take precedence:

1. Provide configuration to the app using [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder.configureappconfiguration) to load the configuration after the app's [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder.configureappconfiguration) delegates execute. Hosting startup configuration takes priority over the app's configuration using this approach.
2. Provide configuration to the app using [UseConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useconfiguration) to load the configuration before the app's [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder.configureappconfiguration) delegates execute. The app's configuration values take priority over those provided by the hosting startup using this approach.

```
public class ConfigurationInjection : IHostingStartup
{
    public void Configure(IWebHostBuilder builder)
    {
        Dictionary<string, string> dict;

        builder.ConfigureAppConfiguration(config =>
        {
            dict = new Dictionary<string, string>
            {
                {"ConfigurationKey1", 
                    "From IHostingStartup: Higher priority " +
                    "than the app's configuration."},
            };

            config.AddInMemoryCollection(dict);
        });

        dict = new Dictionary<string, string>
        {
            {"ConfigurationKey2", 
                "From IHostingStartup: Lower priority " +
                "than the app's configuration."},
        };

        var builtConfig = new ConfigurationBuilder()
            .AddInMemoryCollection(dict)
            .Build();

        builder.UseConfiguration(builtConfig);
    }
}
```

## Specify the hosting startup assembly

For either a class library- or console app-supplied hosting startup, specify the hosting startup assembly's name in the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable. The environment variable is a semicolon-delimited list of assemblies.

Only hosting startup assemblies are scanned for the `HostingStartup` attribute. For the sample app, *HostingStartupApp*, to discover the hosting startups described earlier, the environment variable is set to the following value:

```
HostingStartupLibrary;HostingStartupPackage;StartupDiagnostics
```

A hosting startup assembly can also be set using the [Hosting Startup Assemblies](web-host?view=aspnetcore-10.0#hosting-startup-assemblies) host configuration setting.

When multiple hosting startup assembles are present, their [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup.configure) methods are executed in the order that the assemblies are listed.

## Activation

Options for hosting startup activation are:

* [Runtime store](#runtime-store) (**deprecated**): Activation doesn't require a compile-time reference for activation. The sample app places the hosting startup assembly and dependencies files into a folder, *deployment*, to facilitate deployment of the hosting startup in a multimachine environment. The *deployment* folder also includes a PowerShell script that creates or modifies environment variables on the deployment system to enable the hosting startup. The [runtime package store](/en-us/dotnet/core/deploying/runtime-store) is deprecated, so we recommend the compile-time reference options that follow.

* Compile-time reference required for activation

  * [NuGet package](#nuget-package)
  * [Project bin folder](#project-bin-folder)

### Runtime store

Warning

The runtime package store is deprecated. It's no longer supported or under active development, and the `dotnet store` command has known issues with .NET 6 and later versions. Use a [NuGet package](#nuget-package) or the [project bin folder](#project-bin-folder) for hosting startup activation instead. For more information, see [Runtime package store](/en-us/dotnet/core/deploying/runtime-store).

The hosting startup implementation is placed in the [runtime store](/en-us/dotnet/core/deploying/runtime-store). A compile-time reference to the assembly isn't required by the enhanced app.

After the hosting startup is built, a runtime store is generated using the manifest project file and the [dotnet store](/en-us/dotnet/core/tools/dotnet-store) command.

```
dotnet store --manifest {MANIFEST FILE} --runtime {RUNTIME IDENTIFIER} --output {OUTPUT LOCATION} --skip-optimization
```

In the sample app (*RuntimeStore* project) the following command is used:

```
dotnet store --manifest store.manifest.csproj --runtime win7-x64 --output ./deployment/store --skip-optimization
```

For the runtime to discover the runtime store, the runtime store's location is added to the `DOTNET_SHARED_STORE` environment variable.

**Modify and place the hosting startup's dependencies file**

To activate the enhancement without a package reference to the enhancement, specify additional dependencies to the runtime with `additionalDeps`. `additionalDeps` allows you to:

* Extend the app's library graph by providing a set of additional `.deps.json` files to merge with the app's own `.deps.json` file on startup.
* Make the hosting startup assembly discoverable and loadable.

The recommended approach for generating the additional dependencies file is to:

1. Execute `dotnet publish` on the runtime store manifest file referenced in the previous section.
2. Remove the manifest reference from libraries and the `runtime` section of the resulting `.deps.json` file.

In the example project, the `store.manifest/1.0.0` property is removed from the `targets` and `libraries` section:

```
{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v2.1",
    "signature": "4ea77c7b75ad1895ae1ea65e6ba2399010514f99"
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v2.1": {
      "store.manifest/1.0.0": {
        "dependencies": {
          "StartupDiagnostics": "1.0.0"
        },
        "runtime": {
          "store.manifest.dll": {}
        }
      },
      "StartupDiagnostics/1.0.0": {
        "runtime": {
          "lib/netcoreapp2.1/StartupDiagnostics.dll": {
            "assemblyVersion": "1.0.0.0",
            "fileVersion": "1.0.0.0"
          }
        }
      }
    }
  },
  "libraries": {
    "store.manifest/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    },
    "StartupDiagnostics/1.0.0": {
      "type": "package",
      "serviceable": true,
      "sha512": "sha512-oiQr60vBQW7+nBTmgKLSldj06WNLRTdhOZpAdEbCuapoZ+M2DJH2uQbRLvFT8EGAAv4TAKzNtcztpx5YOgBXQQ==",
      "path": "startupdiagnostics/1.0.0",
      "hashPath": "startupdiagnostics.1.0.0.nupkg.sha512"
    }
  }
}
```

Place the `.deps.json` file into the following location:

```
{ADDITIONAL DEPENDENCIES PATH}/shared/{SHARED FRAMEWORK NAME}/{SHARED FRAMEWORK VERSION}/{ENHANCEMENT ASSEMBLY NAME}.deps.json
```

* `{ADDITIONAL DEPENDENCIES PATH}`: Location added to the `DOTNET_ADDITIONAL_DEPS` environment variable.
* `{SHARED FRAMEWORK NAME}`: Shared framework required for this additional dependencies file.
* `{SHARED FRAMEWORK VERSION}`: Minimum shared framework version.
* `{ENHANCEMENT ASSEMBLY NAME}`: The enhancement's assembly name.

In the sample app (*RuntimeStore* project), the additional dependencies file is placed into the following location:

```
deployment/additionalDeps/shared/Microsoft.AspNetCore.App/2.1.0/StartupDiagnostics.deps.json
```

For runtime to discover the runtime store location, the additional dependencies file location is added to the `DOTNET_ADDITIONAL_DEPS` environment variable.

Note

The `DOTNET_ADDITIONAL_DEPS` environment variable only works with framework-dependent applications. This environment variable is ignored for self-contained applications.

In the sample app (*RuntimeStore* project), building the runtime store and generating the additional dependencies file is accomplished using a [PowerShell](/en-us/powershell/scripting/overview) script.

For examples of how to set environment variables for various operating systems, see [Use multiple environments](../environments?view=aspnetcore-10.0).

**Deployment**

To facilitate the deployment of a hosting startup in a multimachine environment, the sample app creates a *deployment* folder in published output that contains:

* The hosting startup runtime store.
* The hosting startup dependencies file.
* A PowerShell script that creates or modifies the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES`, `DOTNET_SHARED_STORE`, and `DOTNET_ADDITIONAL_DEPS` to support the activation of the hosting startup. Run the script from an administrative PowerShell command prompt on the deployment system.

### NuGet package

A hosting startup enhancement can be provided in a NuGet package. The package has a `HostingStartup` attribute. The hosting startup types provided by the package are made available to the app using either of the following approaches:

* The enhanced app's project file makes a package reference for the hosting startup in the app's project file (a compile-time reference). With the compile-time reference in place, the hosting startup assembly and all of its dependencies are incorporated into the app's dependency file (`.deps.json`). This approach applies to a hosting startup assembly package published to [nuget.org](https://www.nuget.org/).
* The hosting startup's dependencies file is made available to the enhanced app as described in the [Runtime store](#runtime-store) section (without a compile-time reference).

For more information on NuGet packages and the runtime store, see the following topics:

* [How to Create a NuGet Package with Cross Platform Tools](/en-us/dotnet/core/deploying/creating-nuget-packages)
* [Publishing packages](/en-us/nuget/create-packages/publish-a-package)
* [Runtime package store](/en-us/dotnet/core/deploying/runtime-store)

### Project bin folder

A hosting startup enhancement can be provided by a *bin*-deployed assembly in the enhanced app. The hosting startup types provided by the assembly are made available to the app using one of the following approaches:

* The enhanced app's project file makes an assembly reference to the hosting startup (a compile-time reference). With the compile-time reference in place, the hosting startup assembly and all of its dependencies are incorporated into the app's dependency file (`.deps.json`). This approach applies when the deployment scenario calls for making a compile-time reference to the hosting startup's assembly (*.dll* file) and moving the assembly to either:

  * The consuming project.
  * A location accessible by the consuming project.

* The hosting startup's dependencies file is made available to the enhanced app as described in the [Runtime store](#runtime-store) section (without a compile-time reference).

* When targeting the .NET Framework, the assembly is loadable in the default load context, which on .NET Framework means that the assembly is located at either of the following locations:

  * Application base path: The *bin* folder where the app's executable (*.exe*) is located.
  * Global Assembly Cache (GAC): The GAC stores assemblies that several .NET Framework apps share. For more information, see [How to: Install an assembly into the global assembly cache](/en-us/dotnet/framework/app-domains/how-to-install-an-assembly-into-the-gac) in the .NET Framework documentation.

## Sample code

The [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/platform-specific-configuration/samples/) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample)) demonstrates hosting startup implementation scenarios:

* Two hosting startup assemblies (class libraries) set a pair of in-memory configuration key-value pairs each:

  * NuGet package (*HostingStartupPackage*)
  * Class library (*HostingStartupLibrary*)

* A hosting startup is activated from a runtime store-deployed assembly (*StartupDiagnostics*). The assembly adds two middlewares to the app at startup that provide diagnostic information on:

  * Registered services
  * Address (scheme, host, path base, path, query string)
  * Connection (remote IP, remote port, local IP, local port, client certificate)
  * Request headers
  * Environment variables

To run the sample:

**Activation from a NuGet package**

1. Compile the *HostingStartupPackage* package with the [dotnet pack](/en-us/dotnet/core/tools/dotnet-pack) command.

2. Add the package's assembly name of the *HostingStartupPackage* to the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable.

3. Compile and run the app. A package reference is present in the enhanced app (a compile-time reference). A `<PropertyGroup>` in the app's project file specifies the package project's output (*../HostingStartupPackage/bin/Debug*) as a package source. This allows the app to use the package without uploading the package to [nuget.org](https://www.nuget.org/). For more information, see the notes in the HostingStartupApp's project file.

   ```
   <PropertyGroup>
     <RestoreSources>$(RestoreSources);https://api.nuget.org/v3/index.json;../HostingStartupPackage/bin/Debug</RestoreSources>
   </PropertyGroup>
   ```

4. Observe that the service configuration key values rendered by the Index page match the values set by the package's `ServiceKeyInjection.Configure` method.

If you make changes to the *HostingStartupPackage* project and recompile it, clear the local NuGet package caches to ensure that the *HostingStartupApp* receives the updated package and not a stale package from the local cache. To clear the local NuGet caches, execute the following [dotnet nuget locals](/en-us/dotnet/core/tools/dotnet-nuget-locals) command:

```
dotnet nuget locals all --clear
```

**Activation from a class library**

1. Compile the *HostingStartupLibrary* class library with the [dotnet build](/en-us/dotnet/core/tools/dotnet-build) command.

2. Add the class library's assembly name of *HostingStartupLibrary* to the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable.

3. *bin*-deploy the class library's assembly to the app by copying the *HostingStartupLibrary.dll* file from the class library's compiled output to the app's *bin/Debug* folder.

4. Compile and run the app. An `<ItemGroup>` in the app's project file references the class library's assembly (*.\bin\Debug\netcoreapp2.1\HostingStartupLibrary.dll*) (a compile-time reference). For more information, see the notes in the HostingStartupApp's project file.

   ```
   <ItemGroup>
     <Reference Include=".\\bin\\Debug\\netcoreapp2.1\\HostingStartupLibrary.dll">
       <HintPath>.\bin\Debug\netcoreapp2.1\HostingStartupLibrary.dll</HintPath>
       <SpecificVersion>False</SpecificVersion>
     </Reference>
   </ItemGroup>
   ```

5. Observe that the service configuration key values rendered by the Index page match the values set by the class library's `ServiceKeyInjection.Configure` method.

**Activation from a runtime store-deployed assembly**

1. The *StartupDiagnostics* project uses [PowerShell](/en-us/powershell/scripting/overview) to modify its `StartupDiagnostics.deps.json` file. PowerShell is installed by default on Windows starting with Windows 7 SP1 and Windows Server 2008 R2 SP1. To obtain PowerShell on other platforms, see [Installing various versions of PowerShell](/en-us/powershell/scripting/install/installing-powershell).

2. Execute the *build.ps1* script in the *RuntimeStore* folder. The script:

   * Generates the `StartupDiagnostics` package in the *obj\packages* folder.
   * Generates the runtime store for `StartupDiagnostics` in the *store* folder. The `dotnet store` command in the script uses the `win7-x64` [runtime identifier (RID)](/en-us/dotnet/core/rid-catalog) for a hosting startup deployed to Windows. When providing the hosting startup for a different runtime, substitute the correct RID on line 37 of the script. The runtime store for `StartupDiagnostics` would later be moved to the user's or system's runtime store on the machine where the assembly will be consumed. The user runtime store install location for the `StartupDiagnostics` assembly is *.dotnet/store/x64/netcoreapp2.2/startupdiagnostics/1.0.0/lib/netcoreapp2.2/StartupDiagnostics.dll*.
   * Generates the `additionalDeps` for `StartupDiagnostics` in the *additionalDeps* folder. The additional dependencies would later be moved to the user's or system's additional dependencies. The user `StartupDiagnostics` additional dependencies install location is `.dotnet/x64/additionalDeps/StartupDiagnostics/shared/Microsoft.NETCore.App/2.2.0/StartupDiagnostics.deps.json`.
   * Places the *deploy.ps1* file in the *deployment* folder.

3. Run the *deploy.ps1* script in the *deployment* folder. The script appends:

   * `StartupDiagnostics` to the `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable.
   * The hosting startup dependencies path (in the RuntimeStore project's *deployment* folder) to the `DOTNET_ADDITIONAL_DEPS` environment variable.
   * The runtime store path (in the RuntimeStore project's *deployment* folder) to the `DOTNET_SHARED_STORE` environment variable.

4. Run the sample app.

5. Request the `/services` endpoint to see the app's registered services. Request the `/diag` endpoint to see the diagnostic information.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/cascading-values-and-parameters
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/cascading-values-and-parameters.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor cascading values and parameters

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to flow data from an ancestor Razor component to descendent components.

*Cascading values and parameters* provide a convenient way to flow data down a component hierarchy from an ancestor component to any number of descendent components. Unlike [Component parameters](./?view=aspnetcore-10.0#component-parameters), cascading values and parameters don't require an attribute assignment for each descendent component where the data is consumed. Cascading values and parameters also allow components to coordinate with each other across a component hierarchy.

Note

The code examples in this article adopt [nullable reference types (NRTs) and .NET compiler null-state static analysis](../../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis), which are supported in ASP.NET Core in .NET 6 or later. When targeting .NET 5 or earlier, remove the null type designation (`?`) from the `CascadingType?`, `@ActiveTab?`, `RenderFragment?`, `ITab?`, `TabSet?`, and `string?` types in the article's examples.

## Root-level cascading values

Root-level cascading values can be registered for the entire component hierarchy. Named cascading values and subscriptions for update notifications are supported.

The following class is used in this section's examples.

`Dalek.cs`:

```
// "Dalek" ©Terry Nation https://www.imdb.com/name/nm0622334/
// "Doctor Who" ©BBC https://www.bbc.co.uk/programmes/b006q2x0

namespace BlazorSample;

public class Dalek
{
    public int Units { get; set; }
}
```

```
// "Dalek" ©Terry Nation https://www.imdb.com/name/nm0622334/
// "Doctor Who" ©BBC https://www.bbc.co.uk/programmes/b006q2x0

namespace BlazorSample;

public class Dalek
{
    public int Units { get; set; }
}
```

The following registrations are made in the app's `Program` file with [AddCascadingValue](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cascadingvalueservicecollectionextensions.addcascadingvalue):

* `Dalek` with a property value for `Units` is registered as a fixed cascading value.
* A second `Dalek` registration with a different property value for `Units` is named "`AlphaGroup`".

```
builder.Services.AddCascadingValue(sp => new Dalek { Units = 123 });
builder.Services.AddCascadingValue("AlphaGroup", sp => new Dalek { Units = 456 });
```

The following `Daleks` component displays the cascaded values.

`Daleks.razor`:

```
@page "/daleks"

<PageTitle>Daleks</PageTitle>

<h1>Root-level Cascading Value Example</h1>

<ul>
    <li>Dalek Units: @Dalek?.Units</li>
    <li>Alpha Group Dalek Units: @AlphaGroupDalek?.Units</li>
</ul>

<p>
    Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
    Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
</p>

@code {
    [CascadingParameter]
    private Dalek? Dalek { get; set; }

    [CascadingParameter(Name = "AlphaGroup")]
    public Dalek? AlphaGroupDalek { get; set; }
}
```

```
@page "/daleks"

<PageTitle>Daleks</PageTitle>

<h1>Root-level Cascading Value Example</h1>

<ul>
    <li>Dalek Units: @Dalek?.Units</li>
    <li>Alpha Group Dalek Units: @AlphaGroupDalek?.Units</li>
</ul>

<p>
    Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
    Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
</p>

@code {
    [CascadingParameter]
    private Dalek? Dalek { get; set; }

    [CascadingParameter(Name = "AlphaGroup")]
    public Dalek? AlphaGroupDalek { get; set; }
}
```

In the following example, `Dalek` is registered as a cascading value using [`CascadingValueSource<T>`](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvaluesource-1), where `<T>` is the type. The `isFixed` flag indicates whether the value is fixed. If `false`, all recipients are subscribed for update notifications. Subscriptions create overhead and reduce performance, so set `isFixed` to `true` if the value doesn't change.

```
builder.Services.AddCascadingValue(sp =>
{
    var dalek = new Dalek { Units = 789 };
    var source = new CascadingValueSource<Dalek>(dalek, isFixed: false);

    return source;
});
```

Warning

Registering a component type as a root-level cascading value doesn't register additional services for the type or permit service activation in the component.

Treat required services separately from cascading values, registering them separately from the cascaded type.

Avoid using [AddCascadingValue](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cascadingvalueservicecollectionextensions.addcascadingvalue) to register a component type as a cascading value. Instead, wrap the `<Router>...</Router>` in the `Routes` component (`Components/Routes.razor`) with the component and adopt global interactive server-side rendering (interactive SSR). For an example, see the [`CascadingValue` component](#cascadingvalue-component) section.

## Root-level cascading values with notifications

Calling [NotifyChangedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvaluesource-1.notifychangedasync) to issue update notifications can be used to signal multiple Razor component subscribers that a cascading value has changed. Notifications aren't possible for subscribers that adopt static server-side rendering (static SSR), so subscribers must adopt an interactive render mode.

In the following example:

* `NotifyingDalek` implements [INotifyPropertyChanged](/en-us/dotnet/api/system.componentmodel.inotifypropertychanged) to notify clients that a property value has changed. When the `Units` property is set, the [PropertyChangedEventHandler](/en-us/dotnet/api/system.componentmodel.propertychangedeventhandler) (`PropertyChanged`) is invoked.
* The `SetUnitsToOneThousandAsync` method can be triggered by subscribers to set `Units` to 1,000 with a simulated processing delay.

Keep in mind for production code that any change in state (any property value change of the class) causes all subscribed components to rerender, regardless of which part of the state they use. We recommend creating granular classes, cascading them separately with specific subscriptions to ensure that only components subscribed to a specific portion of the application state are affected by changes.

Note

For a Blazor Web App solution consisting of server and client (`.Client`) projects, the following `NotifyingDalek.cs` file is placed in the `.Client` project.

`NotifyingDalek.cs`:

```
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class NotifyingDalek : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    private int units;

    public int Units
    {
        get => units;
        set
        {
            if (units != value)
            {
                units = value;
                OnPropertyChanged();
            }
        }
    }

    protected virtual void OnPropertyChanged(
        [CallerMemberName] string? propertyName = default)
            => PropertyChanged?.Invoke(this, new(propertyName));

    public async Task SetUnitsToOneThousandAsync()
    {
        // Simulate a three second delay in processing
        await Task.Delay(3000);

        Units = 1000;
    }
}
```

The following `CascadingStateServiceCollectionExtensions` creates a [CascadingValueSource\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvaluesource-1) from a type that implements [INotifyPropertyChanged](/en-us/dotnet/api/system.componentmodel.inotifypropertychanged).

Note

For a Blazor Web App solution consisting of server and client (`.Client`) projects, the following `CascadingStateServiceCollectionExtensions.cs` file is placed in the `.Client` project.

`CascadingStateServiceCollectionExtensions.cs`:

```
using System.ComponentModel;
using Microsoft.AspNetCore.Components;

namespace Microsoft.Extensions.DependencyInjection;

public static class CascadingStateServiceCollectionExtensions
{
    public static IServiceCollection AddNotifyingCascadingValue<T>(
        this IServiceCollection services, T state, bool isFixed = false)
        where T : INotifyPropertyChanged
    {
        return services.AddCascadingValue<T>(sp =>
        {
            return new CascadingStateValueSource<T>(state, isFixed);
        });
    }

    private sealed class CascadingStateValueSource<T>
        : CascadingValueSource<T>, IDisposable where T : INotifyPropertyChanged
    {
        private readonly T state;
        private readonly CascadingValueSource<T> source;

        public CascadingStateValueSource(T state, bool isFixed = false)
            : base(state, isFixed = false)
        {
            this.state = state;
            source = new CascadingValueSource<T>(state, isFixed);
            this.state.PropertyChanged += HandlePropertyChanged;
        }

        private void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
        {
            _ = NotifyChangedAsync();
        }

        public void Dispose()
        {
            state.PropertyChanged -= HandlePropertyChanged;
        }
    }
}
```

The type's [PropertyChangedEventHandler](/en-us/dotnet/api/system.componentmodel.propertychangedeventhandler) (`HandlePropertyChanged`) calls the [CascadingValueSource\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvaluesource-1)'s [NotifyChangedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvaluesource-1.notifychangedasync) method to notify subscribers that the cascading value has changed. The [Task](/en-us/dotnet/api/system.threading.tasks.task) is discarded when calling [NotifyChangedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvaluesource-1.notifychangedasync) because the call only represents the duration of the dispatch to the synchronous context. Exceptions are handled internally by dispatching them to the renderer within the context of whichever component threw when receiving the update. This is the same way that exceptions are processed with a [CascadingValue\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1), which isn't notified about exceptions that happen inside notification recipients. The event handler is disconnected in the `Dispose` method to prevent a memory leak.

In the `Program` file, `NotifyingDalek` is passed to create a [CascadingValueSource\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvaluesource-1) with an initial `Unit` value of 888 units:

```
builder.Services.AddNotifyingCascadingValue(new NotifyingDalek() { Units = 888 });
```

Note

For a Blazor Web App solution consisting of server and client (`.Client`) projects, the preceding code is placed into each project's `Program` file.

The following component is used to demonstrate how changing the value of `NotifyingDalek.Units` notifies subscribers.

`Daleks.razor`:

```
<h2>Daleks component</h2>

<div>
    <b>Dalek Units:</b> @Dalek?.Units
</div>

<div>
    <label>
        <span style="font-weight:bold">New Unit Count:</span>
        <input @bind="dalekCount" />
    </label>
    <button @onclick="Update">Update</button>
</div>

<div>
    <button @onclick="SetOneThousandUnits">Set Units to 1,000</button>
</div>

<p>
    Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
    Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
</p>

@code {
    private int dalekCount;

    [CascadingParameter]
    private NotifyingDalek? Dalek { get; set; }

    private void Update()
    {
        if (Dalek is not null)
        {
            Dalek.Units = dalekCount;
            dalekCount = 0;
        }
    }

    private async Task SetOneThousandUnits()
    {
        if (Dalek is not null)
        {
            await Dalek.SetUnitsToOneThousandAsync();
        }
    }
}
```

To demonstrate multiple subscriber notifications, the following `DaleksMain` component renders three `Daleks` components. When the unit count (`Units`) of one `Dalek` component is updated, the other two `Dalek` component subscribers are updated.

`DaleksMain.razor`:

```
@page "/daleks-main"

<PageTitle>Daleks Main</PageTitle>

<h1>Daleks Main</h1>

<Daleks />

<Daleks />

<Daleks />
```

Add a navigation link to the `DaleksMain` component in `NavMenu.razor`:

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="daleks-main">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Daleks
    </NavLink>
</div>
```

Because the [CascadingValueSource\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvaluesource-1)'s type in this example (`NotifyingDalek`) is a class type, you can meet virtually any state management feature specification requirement. However, subscriptions create overhead and reduce performance, so benchmark the performance of this approach in your app and compare it to other [state management approaches](../state-management/?view=aspnetcore-10.0) before adopting it in a production app with constrained processing and memory resources.

Any change in state (any property value change of the class) causes all subscribed components to rerender, regardless of which part of the state they use. **Avoid creating a single large class representing the entire global application state.** Instead, create granular classes and cascade them separately with specific subscriptions to cascading parameters, ensuring that only components subscribed to a specific portion of the application state are affected by changes.

## `CascadingValue` component

An ancestor component provides a cascading value using the Blazor framework's [`CascadingValue`](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1) component, which wraps a subtree of a component hierarchy and supplies a single value to all of the components within its subtree.

The following example demonstrates the flow of theme information down the component hierarchy to provide a CSS style class to buttons in child components.

The following `ThemeInfo` C# class specifies the theme information.

Note

For the examples in this section, the app's namespace is `BlazorSample`. When experimenting with the code in your own sample app, change the app's namespace to your sample app's namespace.

`ThemeInfo.cs`:

```
namespace BlazorSample;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
```

```
namespace BlazorSample;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
```

```
namespace BlazorSample.UIThemeClasses;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
```

```
namespace BlazorSample.UIThemeClasses;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
```

```
namespace BlazorSample.UIThemeClasses
{
    public class ThemeInfo
    {
        public string ButtonClass { get; set; }
    }
}
```

```
namespace BlazorSample.UIThemeClasses
{
    public class ThemeInfo
    {
        public string ButtonClass { get; set; }
    }
}
```

The following [layout component](layouts?view=aspnetcore-10.0) specifies theme information (`ThemeInfo`) as a cascading value for all components that make up the layout body of the [Body](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase.body#microsoft-aspnetcore-components-layoutcomponentbase-body) property. `ButtonClass` is assigned a value of [`btn-success`](https://getbootstrap.com/docs/5.0/components/buttons/), which is a Bootstrap button style. Any descendent component in the component hierarchy can use the `ButtonClass` property through the `ThemeInfo` cascading value.

`MainLayout.razor`:

```
@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <CascadingValue Value="theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="." class="reload">Reload</a>
    <span class="dismiss">🗙</span>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
```

```
@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <CascadingValue Value="theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
```

```
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <CascadingValue Value="@theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
```

```
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <CascadingValue Value="@theme">
            <div class="content px-4">
                @Body
            </div>
        </CascadingValue>
    </main>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
```

```
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <CascadingValue Value="@theme">
            <div class="content px-4">
                @Body
            </div>
        </CascadingValue>
    </div>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
```

```
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <CascadingValue Value="theme">
        <div class="content px-4">
            @Body
        </div>
    </CascadingValue>
</div>

@code {
    private ThemeInfo theme = new ThemeInfo { ButtonClass = "btn-success" };
}
```

Blazor Web Apps provide alternative approaches for cascading values that apply more broadly to the app than furnishing them via a single layout file:

* Wrap the markup of the `Routes` component in a [`CascadingValue`](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1) component to specify the data as a cascading value for all of the app's components.

  The following example cascades `ThemeInfo` data from the `Routes` component.

  `Routes.razor`:

  ```
  <CascadingValue Value="theme">
      <Router ...>
          ...
      </Router>
  </CascadingValue>

  @code {
      private ThemeInfo theme = new() { ButtonClass = "btn-success" };
  }
  ```

  Note

  Wrapping the `Routes` component instance in the `App` component (`Components/App.razor`) with a [`CascadingValue`](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1) component isn't supported.

* Specify a *root-level cascading value* as a service by calling the [AddCascadingValue](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cascadingvalueservicecollectionextensions.addcascadingvalue) extension method on the service collection builder.

  The following example cascades `ThemeInfo` data from the `Program` file.

  `Program.cs`

  ```
  builder.Services.AddCascadingValue(sp => 
      new ThemeInfo() { ButtonClass = "btn-primary" });
  ```

For more information, see the following sections of this article:

* [Root-level cascading values](#root-level-cascading-values)
* [Root-level cascading values with notifications](#root-level-cascading-values-with-notifications)
* [Cascading values/parameters and render mode boundaries](#cascading-valuesparameters-and-render-mode-boundaries)

## `[CascadingParameter]` attribute

To make use of cascading values, descendent components declare cascading parameters using the [`[CascadingParameter]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingparameterattribute). Cascading values are bound to cascading parameters **by type**. Cascading multiple values of the same type is covered in the [Cascade multiple values](#cascade-multiple-values) section later in this article.

The `private` access modifier is recommended for cascading parameters because the parameter should be scoped for use only within the component's class in most cases. When subclassing is required, use the `protected` access modifier.

The following component binds the `ThemeInfo` cascading value to a cascading parameter, optionally using the same name of `ThemeInfo`. The parameter is used to set the CSS class for the **`Increment Counter (Themed)`** button.

`ThemedCounter.razor`:

```
@page "/themed-counter"

<PageTitle>Themed Counter</PageTitle>

<h1>Themed Counter Example</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    private ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount() => currentCount++;
}
```

```
@page "/themed-counter"

<PageTitle>Themed Counter</PageTitle>

<h1>Themed Counter Example</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    private ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount() => currentCount++;
}
```

```
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    private ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    private ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    private ThemeInfo ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    private ThemeInfo ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

Similar to a regular component parameter, components accepting a cascading parameter are rerendered when the cascading value is changed. For instance, configuring a different theme instance causes the `ThemedCounter` component from the [`CascadingValue` component](#cascadingvalue-component) section to rerender.

`MainLayout.razor`:

```
<main>
    <div class="top-row px-4">
        <a href="https://learn.microsoft.com/aspnet/" target="_blank">About</a>
    </div>

    <CascadingValue Value="theme">
        <article class="content px-4">
            @Body
        </article>
    </CascadingValue>
    <button @onclick="ChangeToDarkTheme">Dark mode</button>
</main>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };

    private void ChangeToDarkTheme()
    {
        theme = new() { ButtonClass = "btn-secondary" };
    }
}
```

[CascadingValue\<TValue>.IsFixed](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1.isfixed) can be used to indicate that a cascading parameter doesn't change after initialization.

## Cascading values/parameters and render mode boundaries

Cascading parameters don't pass data across render mode boundaries:

* Interactive sessions run in a different context than the pages that use static server-side rendering (static SSR). There's no requirement that the server producing the page is even the same machine that hosts some later Interactive Server session, including for WebAssembly components where the server is a different machine to the client. The benefit of static server-side rendering (static SSR) is to gain the full performance of pure stateless HTML rendering.

* State crossing the boundary between static and interactive rendering must be serializable. Components are arbitrary objects that reference a vast chain of other objects, including the renderer, the DI container, and every DI service instance. You must explicitly cause state to be serialized from static SSR to make it available in subsequent interactively-rendered components. Two approaches are adopted:

  * Via the Blazor framework, parameters passed across a static SSR to interactive rendering boundary are serialized automatically if they're JSON-serializable, or an error is thrown.
  * State stored in [Persistent Component State](../state-management/prerendered-state-persistence?view=aspnetcore-10.0) is serialized and recovered automatically if it's JSON-serializable, or an error is thrown.

Cascading parameters aren't JSON-serializable because the typical usage patterns for cascading parameters are somewhat like DI services. There are often platform-specific variants of cascading parameters, so it would be unhelpful to developers if the framework stopped developers from having server-interactive-specific versions or WebAssembly-specific versions. Also, many cascading parameter values in general aren't serializable, so it would be impractical to update existing apps if you had to stop using all nonserializable cascading parameter values.

Recommendations:

* If you need to make state available to all interactive components as a cascading parameter, we recommend using [root-level cascading values](#root-level-cascading-values) or [root-level cascading values with notifications](#root-level-cascading-values-with-notifications). A factory pattern is available, and the app can emit updated values after app startup. Root-level cascading values are available to all components, including interactive components, since they're processed as DI services.

* For component library authors, you can create an extension method for library consumers similar to the following:

  ```
  builder.Services.AddLibraryCascadingParameters();
  ```

  Instruct developers to call your extension method. This is a sound alternative to instructing them to add a `<RootComponent>` component in their `MainLayout` component.

## Cascade multiple values

To cascade multiple values of the same type within the same subtree, provide a unique [Name](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1.name) string to each [`CascadingValue`](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1) component and their corresponding [`[CascadingParameter]` attributes](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingparameterattribute).

In the following example, two [`CascadingValue`](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1) components cascade different instances of `CascadingType`:

```
<CascadingValue Value="parentCascadeParameter1" Name="CascadeParam1">
    <CascadingValue Value="ParentCascadeParameter2" Name="CascadeParam2">
        ...
    </CascadingValue>
</CascadingValue>

@code {
    private CascadingType? parentCascadeParameter1;

    [Parameter]
    public CascadingType? ParentCascadeParameter2 { get; set; }
}
```

In a descendant component, the cascaded parameters receive their cascaded values from the ancestor component by [Name](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1.name):

```
@code {
    [CascadingParameter(Name = "CascadeParam1")]
    protected CascadingType? ChildCascadeParameter1 { get; set; }

    [CascadingParameter(Name = "CascadeParam2")]
    protected CascadingType? ChildCascadeParameter2 { get; set; }
}
```

## Pass data across a component hierarchy

Cascading parameters also enable components to pass data across a component hierarchy. Consider the following UI tab set example, where a tab set component maintains a series of individual tabs.

Note

For the examples in this section, the app's namespace is `BlazorSample`. When experimenting with the code in your own sample app, change the namespace to your sample app's namespace.

Create an `ITab` interface that tabs implement in a folder named `UIInterfaces`.

`UIInterfaces/ITab.cs`:

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample.UIInterfaces;

public interface ITab
{
    RenderFragment ChildContent { get; }
}
```

Note

For more information on [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), see [ASP.NET Core Razor components](./?view=aspnetcore-10.0#child-content-render-fragments).

The following `TabSet` component maintains a set of tabs. The tab set's `Tab` components, which are created later in this section, supply the list items (`<li>...</li>`) for the list (`<ul>...</ul>`).

Child `Tab` components aren't explicitly passed as parameters to the `TabSet`. Instead, the child `Tab` components are part of the child content of the `TabSet`. However, the `TabSet` still needs a reference to each `Tab` component so that it can render the headers and the active tab. To enable this coordination without requiring additional code, the `TabSet` component *can provide itself as a cascading value* that is then picked up by the descendent `Tab` components.

`TabSet.razor`:

```
@using BlazorSample.UIInterfaces

<!-- Display the tab headers -->

<CascadingValue Value="this">
    <ul class="nav nav-tabs">
        @ChildContent
    </ul>
</CascadingValue>

<!-- Display body for only the active tab -->

<div class="nav-tabs-body p-4">
    @ActiveTab?.ChildContent
</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public ITab? ActiveTab { get; private set; }

    public void AddTab(ITab tab)
    {
        if (ActiveTab is null)
        {
            SetActiveTab(tab);
        }
    }

    public void SetActiveTab(ITab tab)
    {
        if (ActiveTab != tab)
        {
            ActiveTab = tab;
            StateHasChanged();
        }
    }
}
```

Descendent `Tab` components capture the containing `TabSet` as a cascading parameter. The `Tab` components add themselves to the `TabSet` and coordinate to set the active tab.

`Tab.razor`:

```
@using BlazorSample.UIInterfaces
@implements ITab

<li>
    <a @onclick="ActivateTab" class="nav-link @TitleCssClass" role="button">
        @Title
    </a>
</li>

@code {
    [CascadingParameter]
    private TabSet? ContainerTabSet { get; set; }

    [Parameter]
    public string? Title { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private string? TitleCssClass => 
        ContainerTabSet?.ActiveTab == this ? "active" : null;

    protected override void OnInitialized()
    {
        ContainerTabSet?.AddTab(this);
    }

    private void ActivateTab()
    {
        ContainerTabSet?.SetActiveTab(this);
    }
}
```

The following `ExampleTabSet` component uses the `TabSet` component, which contains three `Tab` components.

`ExampleTabSet.razor`:

```
@page "/example-tab-set"

<TabSet>
    <Tab Title="First tab">
        <h4>Greetings from the first tab!</h4>

        <label>
            <input type="checkbox" @bind="showThirdTab" />
            Toggle third tab
        </label>
    </Tab>

    <Tab Title="Second tab">
        <h4>Hello from the second tab!</h4>
    </Tab>

    @if (showThirdTab)
    {
        <Tab Title="Third tab">
            <h4>Welcome to the disappearing third tab!</h4>
            <p>Toggle this tab from the first tab.</p>
        </Tab>
    }
</TabSet>

@code {
    private bool showThirdTab;
}
```

## Related content

* [Generic type support: Explicit generic types based on ancestor components](generic-type-support?view=aspnetcore-10.0#explicit-generic-types-based-on-ancestor-components)
* [State management: Protected browser storage: Factor out state preservation to a common provider](../state-management/protected-browser-storage?view=aspnetcore-10.0#factor-out-state-preservation-to-a-common-provider)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/sort-filter-page
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-rp/sort-filter-page.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 3, Razor Pages with EF Core in ASP.NET Core - Sort, Filter, Paging

By [Tom Dykstra](https://github.com/tdykstra), [Jeremy Likness](https://twitter.com/jeremylikness), and [Jon P Smith](https://twitter.com/thereformedprog)

The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. For information about the tutorial series, see [the first tutorial](intro?view=aspnetcore-10.0).

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) and compare that code to what you created by following the tutorial.

This tutorial adds sorting, filtering, and paging functionality to the Students pages.

The following illustration shows a completed page. The column headings are clickable links to sort the column. Click a column heading repeatedly to switch between ascending and descending sort order.

## Add sorting

Replace the code in `Pages/Students/Index.cshtml.cs` with the following code to add sorting.

```
public class IndexModel : PageModel
{
    private readonly SchoolContext _context;
    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

    public IList<Student> Students { get; set; }

    public async Task OnGetAsync(string sortOrder)
    {
        // using System;
        NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        DateSort = sortOrder == "Date" ? "date_desc" : "Date";

        IQueryable<Student> studentsIQ = from s in _context.Students
                                        select s;

        switch (sortOrder)
        {
            case "name_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                break;
            case "Date":
                studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                break;
            case "date_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                break;
            default:
                studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                break;
        }

        Students = await studentsIQ.AsNoTracking().ToListAsync();
    }
}
```

The preceding code:

* Requires adding `using System;`.
* Adds properties to contain the sorting parameters.
* Changes the name of the `Student` property to `Students`.
* Replaces the code in the `OnGetAsync` method.

The `OnGetAsync` method receives a `sortOrder` parameter from the query string in the URL. The URL and query string is generated by the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0).

The `sortOrder` parameter is either `Name` or `Date`. The `sortOrder` parameter is optionally followed by `_desc` to specify descending order. The default sort order is ascending.

When the Index page is requested from the **Students** link, there's no query string. The students are displayed in ascending order by last name. Ascending order by last name is the `default` in the `switch` statement. When the user clicks a column heading link, the appropriate `sortOrder` value is provided in the query string value.

`NameSort` and `DateSort` are used by the Razor Page to configure the column heading hyperlinks with the appropriate query string values:

```
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
```

The code uses the C# [conditional operator ?:](/en-us/dotnet/csharp/language-reference/operators/conditional-operator). The `?:` operator is a ternary operator, it takes three operands. The first line specifies that when `sortOrder` is null or empty, `NameSort` is set to `name_desc`. If `sortOrder` is ***not*** null or empty, `NameSort` is set to an empty string.

These two statements enable the page to set the column heading hyperlinks as follows:

| Current sort order   | Last Name Hyperlink | Date Hyperlink |
| -------------------- | ------------------- | -------------- |
| Last Name ascending  | descending          | ascending      |
| Last Name descending | ascending           | ascending      |
| Date ascending       | ascending           | descending     |
| Date descending      | ascending           | ascending      |

The method uses LINQ to Entities to specify the column to sort by. The code initializes an `IQueryable<Student>` before the switch statement, and modifies it in the switch statement:

```
IQueryable<Student> studentsIQ = from s in _context.Students
                                select s;

switch (sortOrder)
{
    case "name_desc":
        studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
        break;
    case "Date":
        studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
        break;
    case "date_desc":
        studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
        break;
    default:
        studentsIQ = studentsIQ.OrderBy(s => s.LastName);
        break;
}

Students = await studentsIQ.AsNoTracking().ToListAsync();
```

When an `IQueryable` is created or modified, no query is sent to the database. The query isn't executed until the `IQueryable` object is converted into a collection. `IQueryable` are converted to a collection by calling a method such as `ToListAsync`. Therefore, the `IQueryable` code results in a single query that's not executed until the following statement:

```
Students = await studentsIQ.AsNoTracking().ToListAsync();
```

`OnGetAsync` could get verbose with a large number of sortable columns. For information about an alternative way to code this functionality, see [Use dynamic LINQ to simplify code](../ef-mvc/advanced?view=aspnetcore-10.0#dynamic-linq) in the MVC version of this tutorial series.

### Add column heading hyperlinks to the Student Index page

Replace the code in `Students/Index.cshtml`, with the following code. The changes are highlighted.

```
@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>
<p>
    <a asp-page="Create">Create New</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

The preceding code:

* Adds hyperlinks to the `LastName` and `EnrollmentDate` column headings.
* Uses the information in `NameSort` and `DateSort` to set up hyperlinks with the current sort order values.
* Changes the page heading from Index to Students.
* Changes `Model.Student` to `Model.Students`.

To verify that sorting works:

* Run the app and select the **Students** tab.
* Click the column headings.

## Add filtering

To add filtering to the Students Index page:

* A text box and a submit button is added to the Razor Page. The text box supplies a search string on the first or last name.
* The page model is updated to use the text box value.

### Update the OnGetAsync method

Replace the code in `Students/Index.cshtml.cs` with the following code to add filtering:

```
public class IndexModel : PageModel
{
    private readonly SchoolContext _context;

    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

    public IList<Student> Students { get; set; }

    public async Task OnGetAsync(string sortOrder, string searchString)
    {
        NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        DateSort = sortOrder == "Date" ? "date_desc" : "Date";

        CurrentFilter = searchString;
        
        IQueryable<Student> studentsIQ = from s in _context.Students
                                        select s;
        if (!String.IsNullOrEmpty(searchString))
        {
            studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
                                   || s.FirstMidName.Contains(searchString));
        }

        switch (sortOrder)
        {
            case "name_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                break;
            case "Date":
                studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                break;
            case "date_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                break;
            default:
                studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                break;
        }

        Students = await studentsIQ.AsNoTracking().ToListAsync();
    }
}
```

The preceding code:

* Adds the `searchString` parameter to the `OnGetAsync` method, and saves the parameter value in the `CurrentFilter` property. The search string value is received from a text box that's added in the next section.
* Adds to the LINQ statement a `Where` clause. The `Where` clause selects only students whose first name or last name contains the search string. The LINQ statement is executed only if there's a value to search for.

### IQueryable vs. IEnumerable

The code calls the [Where](/en-us/dotnet/api/system.linq.queryable.where) method on an `IQueryable` object, and the filter is processed on the server. In some scenarios, the app might be calling the `Where` method as an extension method on an in-memory collection. For example, suppose `_context.Students` changes from EF Core `DbSet` to a repository method that returns an `IEnumerable` collection. The result would normally be the same but in some cases may be different.

For example, the .NET Framework implementation of `Contains` performs a case-sensitive comparison by default. In SQL Server, `Contains` case-sensitivity is determined by the collation setting of the SQL Server instance. SQL Server defaults to case-insensitive. SQLite defaults to case-sensitive. `ToUpper` could be called to make the test explicitly case-insensitive:

```
Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`
```

The preceding code would ensure that the filter is case-insensitive even if the `Where` method is called on an `IEnumerable` or runs on SQLite.

When `Contains` is called on an `IEnumerable` collection, the .NET implementation is used. When `Contains` is called on an `IQueryable` object, the database implementation is used.

Calling `Contains` on an `IQueryable` is usually preferable for performance reasons. With `IQueryable`, the filtering is done by the database server. If an `IEnumerable` is created first, all the rows have to be returned from the database server.

There's a performance penalty for calling `ToUpper`. The `ToUpper` code adds a function in the WHERE clause of the TSQL SELECT statement. The added function prevents the optimizer from using an index. Given that SQL is installed as case-insensitive, it's best to avoid the `ToUpper` call when it's not needed.

For more information, see [How to use case-insensitive query with Sqlite provider](https://github.com/aspnet/EntityFrameworkCore/issues/11414).

### Update the Razor page

Replace the code in `Pages/Students/Index.cshtml` to add a **Search** button.

```
@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name:
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-primary" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

The preceding code uses the `<form>` [tag helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) to add the search text box and button. By default, the `<form>` tag helper submits form data with a POST. With POST, the parameters are passed in the HTTP message body and not in the URL. When HTTP GET is used, the form data is passed in the URL as query strings. Passing the data with query strings enables users to bookmark the URL. The [W3C guidelines](https://www.w3.org/2001/tag/doc/whenToUseGet.html) recommend that GET should be used when the action doesn't result in an update.

Test the app:

* Select the **Students** tab and enter a search string. If you're using SQLite, the filter is case-insensitive only if you implemented the optional `ToUpper` code shown earlier.

* Select **Search**.

Notice that the URL contains the search string. For example:

```
https://localhost:5001/Students?SearchString=an
```

If the page is bookmarked, the bookmark contains the URL to the page and the `SearchString` query string. The `method="get"` in the `form` tag is what caused the query string to be generated.

Currently, when a column heading sort link is selected, the filter value from the **Search** box is lost. The lost filter value is fixed in the next section.

## Add paging

In this section, a `PaginatedList` class is created to support paging. The `PaginatedList` class uses `Skip` and `Take` statements to filter data on the server instead of retrieving all rows of the table. The following illustration shows the paging buttons.

### Create the PaginatedList class

In the project folder, create `PaginatedList.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(
            IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip(
                (pageIndex - 1) * pageSize)
                .Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}
```

The `CreateAsync` method in the preceding code takes page size and page number and applies the appropriate `Skip` and `Take` statements to the `IQueryable`. When `ToListAsync` is called on the `IQueryable`, it returns a List containing only the requested page. The properties `HasPreviousPage` and `HasNextPage` are used to enable or disable **Previous** and **Next** paging buttons.

The `CreateAsync` method is used to create the `PaginatedList<T>`. A constructor can't create the `PaginatedList<T>` object; constructors can't run asynchronous code.

### Add page size to configuration

Add `PageSize` to the `appsettings.json` [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) file:

```
{
  "PageSize": 3,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

### Add paging to IndexModel

Replace the code in `Students/Index.cshtml.cs` to add paging.

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class IndexModel : PageModel
    {
        private readonly SchoolContext _context;
        private readonly IConfiguration Configuration;

        public IndexModel(SchoolContext context, IConfiguration configuration)
        {
            _context = context;
            Configuration = configuration;
        }

        public string NameSort { get; set; }
        public string DateSort { get; set; }
        public string CurrentFilter { get; set; }
        public string CurrentSort { get; set; }

        public PaginatedList<Student> Students { get; set; }

        public async Task OnGetAsync(string sortOrder,
            string currentFilter, string searchString, int? pageIndex)
        {
            CurrentSort = sortOrder;
            NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
            DateSort = sortOrder == "Date" ? "date_desc" : "Date";
            if (searchString != null)
            {
                pageIndex = 1;
            }
            else
            {
                searchString = currentFilter;
            }

            CurrentFilter = searchString;

            IQueryable<Student> studentsIQ = from s in _context.Students
                                             select s;
            if (!String.IsNullOrEmpty(searchString))
            {
                studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
                                       || s.FirstMidName.Contains(searchString));
            }
            switch (sortOrder)
            {
                case "name_desc":
                    studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                    break;
                case "Date":
                    studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                    break;
                case "date_desc":
                    studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                    break;
                default:
                    studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                    break;
            }

            var pageSize = Configuration.GetValue("PageSize", 4);
            Students = await PaginatedList<Student>.CreateAsync(
                studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
        }
    }
}
```

The preceding code:

* Changes the type of the `Students` property from `IList<Student>` to `PaginatedList<Student>`.
* Adds the page index, the current `sortOrder`, and the `currentFilter` to the `OnGetAsync` method signature.
* Saves the sort order in the `CurrentSort` property.
* Resets page index to 1 when there's a new search string.
* Uses the `PaginatedList` class to get Student entities.
* Sets `pageSize` to 3 from [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0), 4 if configuration fails.

All the parameters that `OnGetAsync` receives are null when:

* The page is called from the **Students** link.
* The user hasn't clicked a paging or sorting link.

When a paging link is clicked, the page index variable contains the page number to display.

The `CurrentSort` property provides the Razor Page with the current sort order. The current sort order must be included in the paging links to keep the sort order while paging.

The `CurrentFilter` property provides the Razor Page with the current filter string. The `CurrentFilter` value:

* Must be included in the paging links in order to maintain the filter settings during paging.
* Must be restored to the text box when the page is redisplayed.

If the search string is changed while paging, the page is reset to 1. The page has to be reset to 1 because the new filter can result in different data to display. When a search value is entered and **Submit** is selected:

* The search string is changed.
* The `searchString` parameter isn't null.

The `PaginatedList.CreateAsync` method converts the student query to a single page of students in a collection type that supports paging. That single page of students is passed to the Razor Page.

The two question marks after `pageIndex` in the `PaginatedList.CreateAsync` call represent the [null-coalescing operator](/en-us/dotnet/csharp/language-reference/operators/null-conditional-operator). The null-coalescing operator defines a default value for a nullable type. The expression `pageIndex ?? 1` returns the value of `pageIndex` if it has a value, otherwise, it returns 1.

### Add paging links

Replace the code in `Students/Index.cshtml` with the following code. The changes are highlighted:

```
@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: 
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-primary" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.Students.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.Students.HasNextPage ? "disabled" : "";
}

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @nextDisabled">
    Next
</a>
```

The column header links use the query string to pass the current search string to the `OnGetAsync` method:

```
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
```

The paging buttons are displayed by tag helpers:

```

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @nextDisabled">
    Next
</a>
```

Run the app and navigate to the students page.

* To make sure paging works, click the paging links in different sort orders.
* To verify that paging works correctly with sorting and filtering, enter a search string and try paging.

## Grouping

This section creates an `About` page that displays how many students have enrolled for each enrollment date. The update uses grouping and includes the following steps:

* Create a view model for the data used by the `About` page.
* Update the `About` page to use the view model.

### Create the view model

Create a *Models/SchoolViewModels* folder.

Create `SchoolViewModels/EnrollmentDateGroup.cs` with the following code:

```
using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}
```

### Create the Razor Page

Create a `Pages/About.cshtml` file with the following code:

```
@page
@model ContosoUniversity.Pages.AboutModel

@{
    ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
    <tr>
        <th>
            Enrollment Date
        </th>
        <th>
            Students
        </th>
    </tr>

    @foreach (var item in Model.Students)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>
```

### Create the page model

Update the `Pages/About.cshtml.cs` file with the following code:

```
using ContosoUniversity.Models.SchoolViewModels;
using ContosoUniversity.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;

namespace ContosoUniversity.Pages
{
    public class AboutModel : PageModel
    {
        private readonly SchoolContext _context;

        public AboutModel(SchoolContext context)
        {
            _context = context;
        }

        public IList<EnrollmentDateGroup> Students { get; set; }

        public async Task OnGetAsync()
        {
            IQueryable<EnrollmentDateGroup> data =
                from student in _context.Students
                group student by student.EnrollmentDate into dateGroup
                select new EnrollmentDateGroup()
                {
                    EnrollmentDate = dateGroup.Key,
                    StudentCount = dateGroup.Count()
                };

            Students = await data.AsNoTracking().ToListAsync();
        }
    }
}
```

The LINQ statement groups the student entities by enrollment date, calculates the number of entities in each group, and stores the results in a collection of `EnrollmentDateGroup` view model objects.

Run the app and navigate to the About page. The count of students for each enrollment date is displayed in a table.

## Next steps

In the next tutorial, the app uses migrations to update the data model.

[Previous tutorial](crud?view=aspnetcore-10.0) [Next tutorial](migrations?view=aspnetcore-10.0)

In this tutorial, sorting, filtering, grouping, and paging, functionality is added.

The following illustration shows a completed page. The column headings are clickable links to sort the column. Clicking a column heading repeatedly switches between ascending and descending sort order.

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples).

## Add sorting to the Index page

Add strings to the `Students/Index.cshtml.cs` `PageModel` to contain the sorting parameters:

```
public class IndexModel : PageModel
{
    private readonly SchoolContext _context;

    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }
```

Update the `Students/Index.cshtml.cs` `OnGetAsync` with the following code:

```
public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}
```

The preceding code receives a `sortOrder` parameter from the query string in the URL. The URL (including the query string) is generated by the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0)

The `sortOrder` parameter is either "Name" or "Date." The `sortOrder` parameter is optionally followed by "\_desc" to specify descending order. The default sort order is ascending.

When the Index page is requested from the **Students** link, there's no query string. The students are displayed in ascending order by last name. Ascending order by last name is the default (fall-through case) in the `switch` statement. When the user clicks a column heading link, the appropriate `sortOrder` value is provided in the query string value.

`NameSort` and `DateSort` are used by the Razor Page to configure the column heading hyperlinks with the appropriate query string values:

```
public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}
```

The following code contains the C# conditional [?: operator](/en-us/dotnet/csharp/language-reference/operators/conditional-operator):

```
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
```

The first line specifies that when `sortOrder` is null or empty, `NameSort` is set to "name\_desc." If `sortOrder` is **not** null or empty, `NameSort` is set to an empty string.

The `?: operator` is also known as the ternary operator.

These two statements enable the page to set the column heading hyperlinks as follows:

| Current sort order   | Last Name Hyperlink | Date Hyperlink |
| -------------------- | ------------------- | -------------- |
| Last Name ascending  | descending          | ascending      |
| Last Name descending | ascending           | ascending      |
| Date ascending       | ascending           | descending     |
| Date descending      | ascending           | ascending      |

The method uses LINQ to Entities to specify the column to sort by. The code initializes an `IQueryable<Student>` before the switch statement, and modifies it in the switch statement:

```
public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}
```

When an`IQueryable` is created or modified, no query is sent to the database. The query isn't executed until the `IQueryable` object is converted into a collection. `IQueryable` are converted to a collection by calling a method such as `ToListAsync`. Therefore, the `IQueryable` code results in a single query that's not executed until the following statement:

```
Student = await studentIQ.AsNoTracking().ToListAsync();
```

`OnGetAsync` could get verbose with a large number of sortable columns.

### Add column heading hyperlinks to the Student Index page

Replace the code in `Students/Index.cshtml`, with the following highlighted code:

```
@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>
<p>
    <a asp-page="Create">Create New</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Student[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Student[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Student)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

The preceding code:

* Adds hyperlinks to the `LastName` and `EnrollmentDate` column headings.
* Uses the information in `NameSort` and `DateSort` to set up hyperlinks with the current sort order values.

To verify that sorting works:

* Run the app and select the **Students** tab.
* Click **Last Name**.
* Click **Enrollment Date**.

To get a better understanding of the code:

* In `Students/Index.cshtml.cs`, set a breakpoint on `switch (sortOrder)`.
* Add a watch for `NameSort` and `DateSort`.
* In `Students/Index.cshtml`, set a breakpoint on `@Html.DisplayNameFor(model => model.Student[0].LastName)`.

Step through the debugger.

## Add a Search Box to the Students Index page

To add filtering to the Students Index page:

* A text box and a submit button is added to the Razor Page. The text box supplies a search string on the first or last name.
* The page model is updated to use the text box value.

### Add filtering functionality to the Index method

Update the `Students/Index.cshtml.cs` `OnGetAsync` with the following code:

```
public async Task OnGetAsync(string sortOrder, string searchString)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";
    CurrentFilter = searchString;

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}
```

The preceding code:

* Adds the `searchString` parameter to the `OnGetAsync` method. The search string value is received from a text box that's added in the next section.
* Added to the LINQ statement a `Where` clause. The `Where` clause selects only students whose first name or last name contains the search string. The LINQ statement is executed only if there's a value to search for.

Note: The preceding code calls the `Where` method on an `IQueryable` object, and the filter is processed on the server. In some scenarios, the app might be calling the `Where` method as an extension method on an in-memory collection. For example, suppose `_context.Students` changes from EF Core `DbSet` to a repository method that returns an `IEnumerable` collection. The result would normally be the same but in some cases may be different.

For example, the .NET Framework implementation of `Contains` performs a case-sensitive comparison by default. In SQL Server, `Contains` case-sensitivity is determined by the collation setting of the SQL Server instance. SQL Server defaults to case-insensitive. `ToUpper` could be called to make the test explicitly case-insensitive:

`Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`

The preceding code would ensure that results are case-insensitive if the code changes to use `IEnumerable`. When `Contains` is called on an `IEnumerable` collection, the .NET implementation is used. When `Contains` is called on an `IQueryable` object, the database implementation is used. Returning an `IEnumerable` from a repository can have a significant performance penalty:

1. All the rows are returned from the DB server.
2. The filter is applied to all the returned rows in the application.

There's a performance penalty for calling `ToUpper`. The `ToUpper` code adds a function in the WHERE clause of the TSQL SELECT statement. The added function prevents the optimizer from using an index. Given that SQL is installed as case-insensitive, it's best to avoid the `ToUpper` call when it's not needed.

### Add a Search Box to the Student Index page

In `Pages/Students/Index.cshtml`, add the following highlighted code to create a **Search** button and assorted chrome.

```
@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name:
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
```

The preceding code uses the `<form>` [tag helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) to add the search text box and button. By default, the `<form>` tag helper submits form data with a POST. With POST, the parameters are passed in the HTTP message body and not in the URL. When HTTP GET is used, the form data is passed in the URL as query strings. Passing the data with query strings enables users to bookmark the URL. The [W3C guidelines](https://www.w3.org/2001/tag/doc/whenToUseGet.html) recommend that GET should be used when the action doesn't result in an update.

Test the app:

* Select the **Students** tab and enter a search string.
* Select **Search**.

Notice that the URL contains the search string.

```
http://localhost:5000/Students?SearchString=an
```

If the page is bookmarked, the bookmark contains the URL to the page and the `SearchString` query string. The `method="get"` in the `form` tag is what caused the query string to be generated.

Currently, when a column heading sort link is selected, the filter value from the **Search** box is lost. The lost filter value is fixed in the next section.

## Add paging functionality to the Students Index page

In this section, a `PaginatedList` class is created to support paging. The `PaginatedList` class uses `Skip` and `Take` statements to filter data on the server instead of retrieving all rows of the table. The following illustration shows the paging buttons.

In the project folder, create `PaginatedList.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(
            IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip(
                (pageIndex - 1) * pageSize)
                .Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}
```

The `CreateAsync` method in the preceding code takes page size and page number and applies the appropriate `Skip` and `Take` statements to the `IQueryable`. When `ToListAsync` is called on the `IQueryable`, it returns a List containing only the requested page. The properties `HasPreviousPage` and `HasNextPage` are used to enable or disable **Previous** and **Next** paging buttons.

The `CreateAsync` method is used to create the `PaginatedList<T>`. A constructor can't create the `PaginatedList<T>` object, constructors can't run asynchronous code.

## Add paging functionality to the Index method

In `Students/Index.cshtml.cs`, update the type of `Student` from `IList<Student>` to `PaginatedList<Student>`:

```
public PaginatedList<Student> Student { get; set; }
```

Update the `Students/Index.cshtml.cs` `OnGetAsync` with the following code:

```
public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)
{
    CurrentSort = sortOrder;
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";
    if (searchString != null)
    {
        pageIndex = 1;
    }
    else
    {
        searchString = currentFilter;
    }

    CurrentFilter = searchString;

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    int pageSize = 3;
    Student = await PaginatedList<Student>.CreateAsync(
        studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
```

The preceding code adds the page index, the current `sortOrder`, and the `currentFilter` to the method signature.

```
public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)
```

All the parameters are null when:

* The page is called from the **Students** link.
* The user hasn't clicked a paging or sorting link.

When a paging link is clicked, the page index variable contains the page number to display.

`CurrentSort` provides the Razor Page with the current sort order. The current sort order must be included in the paging links to keep the sort order while paging.

`CurrentFilter` provides the Razor Page with the current filter string. The `CurrentFilter` value:

* Must be included in the paging links in order to maintain the filter settings during paging.
* Must be restored to the text box when the page is redisplayed.

If the search string is changed while paging, the page is reset to 1. The page has to be reset to 1 because the new filter can result in different data to display. When a search value is entered and **Submit** is selected:

* The search string is changed.
* The `searchString` parameter isn't null.

```
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}
```

The `PaginatedList.CreateAsync` method converts the student query to a single page of students in a collection type that supports paging. That single page of students is passed to the Razor Page.

```
Student = await PaginatedList<Student>.CreateAsync(
    studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
```

The two question marks in `PaginatedList.CreateAsync` represent the [null-coalescing operator](/en-us/dotnet/csharp/language-reference/operators/null-conditional-operator). The null-coalescing operator defines a default value for a nullable type. The expression `(pageIndex ?? 1)` means return the value of `pageIndex` if it has a value. If `pageIndex` doesn't have a value, return 1.

## Add paging links to the student Razor Page

Update the markup in `Students/Index.cshtml`. The changes are highlighted:

```
@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            <label>Find by name: <input type="text" name="SearchString" value="@Model.CurrentFilter" /></label>
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Student[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Student[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Student)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.Student.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.Student.HasNextPage ? "disabled" : "";
}

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @nextDisabled">
    Next
</a>
```

The column header links use the query string to pass the current search string to the `OnGetAsync` method so that the user can sort within filter results:

```
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
```

The paging buttons are displayed by tag helpers:

```

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @nextDisabled">
    Next
</a>
```

Run the app and navigate to the students page.

* To make sure paging works, click the paging links in different sort orders.
* To verify that paging works correctly with sorting and filtering, enter a search string and try paging.

To get a better understanding of the code:

* In `Students/Index.cshtml.cs`, set a breakpoint on `switch (sortOrder)`.
* Add a watch for `NameSort`, `DateSort`, `CurrentSort`, and `Model.Student.PageIndex`.
* In `Students/Index.cshtml`, set a breakpoint on `@Html.DisplayNameFor(model => model.Student[0].LastName)`.

Step through the debugger.

## Update the About page to show student statistics

In this step, `Pages/About.cshtml` is updated to display how many students have enrolled for each enrollment date. The update uses grouping and includes the following steps:

* Create a view model for the data used by the **About** Page.
* Update the About page to use the view model.

### Create the view model

Create a *SchoolViewModels* folder in the *Models* folder.

In the *SchoolViewModels* folder, add a `EnrollmentDateGroup.cs` with the following code:

```
using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}
```

### Update the About page model

The web templates in ASP.NET Core 2.2 do not include the About page. If you are using ASP.NET Core 2.2, create the About Razor Page.

Update the `Pages/About.cshtml.cs` file with the following code:

```
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;

namespace ContosoUniversity.Pages
{
    public class AboutModel : PageModel
    {
        private readonly SchoolContext _context;

        public AboutModel(SchoolContext context)
        {
            _context = context;
        }

        public IList<EnrollmentDateGroup> Student { get; set; }

        public async Task OnGetAsync()
        {
            IQueryable<EnrollmentDateGroup> data =
                from student in _context.Student
                group student by student.EnrollmentDate into dateGroup
                select new EnrollmentDateGroup()
                {
                    EnrollmentDate = dateGroup.Key,
                    StudentCount = dateGroup.Count()
                };

            Student = await data.AsNoTracking().ToListAsync();
        }
    }
}
```

The LINQ statement groups the student entities by enrollment date, calculates the number of entities in each group, and stores the results in a collection of `EnrollmentDateGroup` view model objects.

### Modify the About Razor Page

Replace the code in the `Pages/About.cshtml` file with the following code:

```
@page
@model ContosoUniversity.Pages.AboutModel

@{
    ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
    <tr>
        <th>
            Enrollment Date
        </th>
        <th>
            Students
        </th>
    </tr>

    @foreach (var item in Model.Student)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>
```

Run the app and navigate to the About page. The count of students for each enrollment date is displayed in a table.

If you run into problems you can't solve, download the [completed app for this stage](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu20snapshots/cu-part3-sorting).

## Related content

* [Debugging ASP.NET Core 2.x source](https://github.com/dotnet/AspNetCore.Docs/issues/4155)
* [YouTube version of this tutorial](https://www.youtube.com/watch?v=MDs7PFpoMqI)

In the next tutorial, the app uses migrations to update the data model.

[Previous](crud?view=aspnetcore-10.0) [Next](migrations?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/th-components
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/th-components.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tag Helper Components in ASP.NET Core

By [Scott Addie](https://twitter.com/Scott_Addie) and [Fiyaz Bin Hasan](https://github.com/fiyazbinhasan)

A Tag Helper Component is a Tag Helper that allows you to conditionally modify or add HTML elements from server-side code. This feature is available in ASP.NET Core 2.0 or later.

ASP.NET Core includes two built-in Tag Helper Components: `head` and `body`. They're located in the [Microsoft.AspNetCore.Mvc.Razor.TagHelpers](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.taghelpers) namespace and can be used in both MVC and Razor Pages. Tag Helper Components don't require registration with the app in `_ViewImports.cshtml`.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/tag-helpers/th-components/samples) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Use cases

Two common use cases of Tag Helper Components include:

1. [Injecting a `<link>` into the `<head>`.](#inject-into-html-head-element)
2. [Injecting a `<script>` into the `<body>`.](#inject-into-html-body-element)

The following sections describe these use cases.

### Inject into HTML head element

Inside the HTML `<head>` element, CSS files are commonly imported with the HTML `<link>` element. The following code injects a `<link>` element into the `<head>` element using the `head` Tag Helper Component:

```
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace RazorPagesSample.TagHelpers
{
    public class AddressStyleTagHelperComponent : TagHelperComponent
    {
        private readonly string _style = 
            @"<link rel=""stylesheet"" href=""/css/address.css"" />";

        public override int Order => 1;

        public override Task ProcessAsync(TagHelperContext context,
                                          TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "head", 
                              StringComparison.OrdinalIgnoreCase))
            {
                output.PostContent.AppendHtml(_style);
            }

            return Task.CompletedTask;
        }
    }
}
```

In the preceding code:

* `AddressStyleTagHelperComponent` implements [TagHelperComponent](/en-us/dotnet/api/microsoft.aspnetcore.razor.taghelpers.taghelpercomponent). The abstraction:

  * Allows initialization of the class with a [TagHelperContext](/en-us/dotnet/api/microsoft.aspnetcore.razor.taghelpers.taghelpercontext).
  * Enables the use of Tag Helper Components to add or modify HTML elements.

* The [Order](/en-us/dotnet/api/microsoft.aspnetcore.razor.taghelpers.taghelpercomponent.order) property defines the order in which the Components are rendered. `Order` is necessary when there are multiple usages of Tag Helper Components in an app.

* [ProcessAsync](/en-us/dotnet/api/microsoft.aspnetcore.razor.taghelpers.taghelpercomponent.processasync) compares the execution context's [TagName](/en-us/dotnet/api/microsoft.aspnetcore.razor.taghelpers.taghelpercontext.tagname) property value to `head`. If the comparison evaluates to true, the content of the `_style` field is injected into the HTML `<head>` element.

### Inject into HTML body element

The `body` Tag Helper Component can inject a `<script>` element into the `<body>` element. The following code demonstrates this technique:

```
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace RazorPagesSample.TagHelpers
{
    public class AddressScriptTagHelperComponent : TagHelperComponent
    {
        public override int Order => 2;
        
        public override async Task ProcessAsync(TagHelperContext context,
                                                TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "body",
                              StringComparison.OrdinalIgnoreCase))
            {
                var script = await File.ReadAllTextAsync(
                    "TagHelpers/Templates/AddressToolTipScript.html");
                output.PostContent.AppendHtml(script);
            }
        }
    }
}
```

A separate HTML file is used to store the `<script>` element. The HTML file makes the code cleaner and more maintainable. The preceding code reads the contents of `TagHelpers/Templates/AddressToolTipScript.html` and appends it with the Tag Helper output. The `AddressToolTipScript.html` file includes the following markup:

```
<script>
$("address[printable]").hover(function() {
    $(this).attr({
        "data-toggle": "tooltip",
        "data-placement": "right",
        "title": "Home of Microsoft!"
    });
});
</script>
```

The preceding code binds a [Bootstrap tooltip widget](https://getbootstrap.com/docs/3.3/javascript/#tooltips) to any `<address>` element that includes a `printable` attribute. The effect is visible when a mouse pointer hovers over the element.

## Register a Component

A Tag Helper Component must be added to the app's Tag Helper Components collection. There are three ways to add to the collection:

* [Registration via services container](#registration-via-services-container)
* [Registration via Razor file](#registration-via-razor-file)
* [Registration via Page Model or controller](#registration-via-page-model-or-controller)

### Registration via services container

If the Tag Helper Component class isn't managed with [ITagHelperComponentManager](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.taghelpers.itaghelpercomponentmanager), it must be registered with the [dependency injection (DI)](../../../fundamentals/dependency-injection?view=aspnetcore-10.0) system. The following `Startup.ConfigureServices` code registers the `AddressStyleTagHelperComponent` and `AddressScriptTagHelperComponent` classes with a [transient lifetime](../../../fundamentals/dependency-injection?view=aspnetcore-10.0#lifetime-and-registration-options):

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddTransient<ITagHelperComponent, 
        AddressScriptTagHelperComponent>();
    services.AddTransient<ITagHelperComponent, 
        AddressStyleTagHelperComponent>();
}
```

### Registration via Razor file

If the Tag Helper Component isn't registered with DI, it can be registered from a Razor Pages page or an MVC view. This technique is used for controlling the injected markup and the component execution order from a Razor file.

`ITagHelperComponentManager` is used to add Tag Helper Components or remove them from the app. The following code demonstrates this technique with `AddressTagHelperComponent`:

```
@using RazorPagesSample.TagHelpers;
@using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
@inject ITagHelperComponentManager manager;

@{
    string markup;

    if (Model.IsWeekend)
    {
        markup = "<em class='text-warning'>Office closed today!</em>";
    }
    else
    {
        markup = "<em class='text-info'>Office open today!</em>";
    }

    manager.Components.Add(new AddressTagHelperComponent(markup, 1));
}
```

In the preceding code:

* The `@inject` directive provides an instance of `ITagHelperComponentManager`. The instance is assigned to a variable named `manager` for access downstream in the Razor file.
* An instance of `AddressTagHelperComponent` is added to the app's Tag Helper Components collection.

`AddressTagHelperComponent` is modified to accommodate a constructor that accepts the `markup` and `order` parameters:

```
private readonly string _markup;

public override int Order { get; }

public AddressTagHelperComponent(string markup = "", int order = 1)
{
    _markup = markup;
    Order = order;
}
```

The provided `markup` parameter is used in `ProcessAsync` as follows:

```
public override async Task ProcessAsync(TagHelperContext context,
                                        TagHelperOutput output)
{
    if (string.Equals(context.TagName, "address",
            StringComparison.OrdinalIgnoreCase) &&
        output.Attributes.ContainsName("printable"))
    {
        TagHelperContent childContent = await output.GetChildContentAsync();
        string content = childContent.GetContent();
        output.Content.SetHtmlContent(
            $"<div>{content}<br>{_markup}</div>{_printableButton}");
    }
}
```

### Registration via Page Model or controller

If the Tag Helper Component isn't registered with DI, it can be registered from a Razor Pages page model or an MVC controller. This technique is useful for separating C# logic from Razor files.

Constructor injection is used to access an instance of `ITagHelperComponentManager`. The Tag Helper Component is added to the instance's Tag Helper Components collection. The following Razor Pages page model demonstrates this technique with `AddressTagHelperComponent`:

```
using System;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesSample.TagHelpers;

public class IndexModel : PageModel
{
    private readonly ITagHelperComponentManager _tagHelperComponentManager;

    public bool IsWeekend
    {
        get
        {
            var dayOfWeek = DateTime.Now.DayOfWeek;

            return dayOfWeek == DayOfWeek.Saturday ||
                   dayOfWeek == DayOfWeek.Sunday;
        }
    }

    public IndexModel(ITagHelperComponentManager tagHelperComponentManager)
    {
        _tagHelperComponentManager = tagHelperComponentManager;
    }

    public void OnGet()
    {
        string markup;

        if (IsWeekend)
        {
            markup = "<em class='text-warning'>Office closed today!</em>";
        }
        else
        {
            markup = "<em class='text-info'>Office open today!</em>";
        }

        _tagHelperComponentManager.Components.Add(
            new AddressTagHelperComponent(markup, 1));
    }
}
```

In the preceding code:

* Constructor injection is used to access an instance of `ITagHelperComponentManager`.
* An instance of `AddressTagHelperComponent` is added to the app's Tag Helper Components collection.

## Create a Component

To create a custom Tag Helper Component:

* Create a public class deriving from [TagHelperComponentTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.taghelpers.taghelpercomponenttaghelper).
* Apply an [`[HtmlTargetElement]`](/en-us/dotnet/api/microsoft.aspnetcore.razor.taghelpers.htmltargetelementattribute) attribute to the class. Specify the name of the target HTML element.
* *Optional*: Apply an [`[EditorBrowsable(EditorBrowsableState.Never)]`](/en-us/dotnet/api/system.componentmodel.editorbrowsableattribute) attribute to the class to suppress the type's display in IntelliSense.

The following code creates a custom Tag Helper Component that targets the `<address>` HTML element:

```
using System.ComponentModel;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;

namespace RazorPagesSample.TagHelpers
{
    [HtmlTargetElement("address")]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class AddressTagHelperComponentTagHelper : TagHelperComponentTagHelper
    {
        public AddressTagHelperComponentTagHelper(
            ITagHelperComponentManager componentManager, 
            ILoggerFactory loggerFactory) : base(componentManager, loggerFactory)
        {
        }
    }
}
```

Use the custom `address` Tag Helper Component to inject HTML markup as follows:

```
public class AddressTagHelperComponent : TagHelperComponent
{
    private readonly string _printableButton =
        "<button type='button' class='btn btn-info' onclick=\"window.open(" +
        "'https://binged.it/2AXRRYw')\">" +
        "<span class='glyphicon glyphicon-road' aria-hidden='true'></span>" +
        "</button>";

    public override int Order => 3;

    public override async Task ProcessAsync(TagHelperContext context,
                                            TagHelperOutput output)
    {
        if (string.Equals(context.TagName, "address",
                StringComparison.OrdinalIgnoreCase) &&
            output.Attributes.ContainsName("printable"))
        {
            var content = await output.GetChildContentAsync();
            output.Content.SetHtmlContent(
                $"<div>{content.GetContent()}</div>{_printableButton}");
        }
    }
}
```

The preceding `ProcessAsync` method injects the HTML provided to [SetHtmlContent](/en-us/dotnet/api/microsoft.aspnetcore.razor.taghelpers.taghelpercontent.sethtmlcontent) into the matching `<address>` element. The injection occurs when:

* The execution context's `TagName` property value equals `address`.
* The corresponding `<address>` element has a `printable` attribute.

For example, the `if` statement evaluates to true when processing the following `<address>` element:

```
<address printable>
    One Microsoft Way<br />
    Redmond, WA 98052-6399<br />
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>
```

## Related content

* [Dependency injection in ASP.NET Core](../../../fundamentals/dependency-injection?view=aspnetcore-10.0)
* [Dependency injection into views in ASP.NET Core](../dependency-injection?view=aspnetcore-10.0)
* [ASP.NET Core built-in Tag Helpers](built-in/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/handle-errors
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/handle-errors.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Handle errors in ASP.NET Core Blazor apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes how Blazor manages unhandled exceptions and how to develop apps that detect and handle errors.

## Detailed errors during development

When a Blazor app isn't functioning properly during development, receiving detailed error information from the app assists in troubleshooting and fixing the issue. When an error occurs, Blazor apps display a light yellow bar at the bottom of the screen:

* During development, the bar directs you to the browser console, where you can see the exception.
* In production, the bar notifies the user that an error has occurred and recommends refreshing the browser.

The UI for this error handling experience is part of the [Blazor project templates](../project-structure?view=aspnetcore-10.0). Not all versions of the Blazor project templates use the [`data-nosnippet` attribute](https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag) to signal to browsers not to cache the contents of the error UI, but all versions of the Blazor documentation apply the attribute.

In a Blazor Web App, customize the experience in the `MainLayout` component. Because the [Environment Tag Helper](../../mvc/views/tag-helpers/built-in/environment-tag-helper?view=aspnetcore-10.0) (for example, `<environment include="Production">...</environment>`) isn't supported in Razor components, the following example injects [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment) to configure error messages for different environments.

At the top of `MainLayout.razor`:

```
@inject IHostEnvironment HostEnvironment
```

Create or modify the Blazor error UI markup:

```
<div id="blazor-error-ui" data-nosnippet>
    @if (HostEnvironment.IsProduction())
    {
        <span>An error has occurred.</span>
    }
    else
    {
        <span>An unhandled exception occurred.</span>
    }
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
```

In a Blazor Server app, customize the experience in the `Pages/_Host.cshtml` file. The following example uses the [Environment Tag Helper](../../mvc/views/tag-helpers/built-in/environment-tag-helper?view=aspnetcore-10.0) to configure error messages for different environments.

In a Blazor Server app, customize the experience in the `Pages/_Layout.cshtml` file. The following example uses the [Environment Tag Helper](../../mvc/views/tag-helpers/built-in/environment-tag-helper?view=aspnetcore-10.0) to configure error messages for different environments.

In a Blazor Server app, customize the experience in the `Pages/_Host.cshtml` file. The following example uses the [Environment Tag Helper](../../mvc/views/tag-helpers/built-in/environment-tag-helper?view=aspnetcore-10.0) to configure error messages for different environments.

Create or modify the Blazor error UI markup:

```
<div id="blazor-error-ui" data-nosnippet>
    <environment include="Staging,Production">
        An error has occurred.
    </environment>
    <environment include="Development">
        An unhandled exception occurred.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
```

In a Blazor WebAssembly app, customize the experience in the `wwwroot/index.html` file:

```
<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
```

The `blazor-error-ui` element is normally hidden due to the presence of the `display: none` style of the `blazor-error-ui` CSS class in the app's auto-generated stylesheet. When an error occurs, the framework applies `display: block` to the element.

The `blazor-error-ui` element is normally hidden due to the presence of the `display: none` style of the `blazor-error-ui` CSS class in the site's stylesheet in the `wwwroot/css` folder. When an error occurs, the framework applies `display: block` to the element.

## Detailed circuit errors

*This section applies to Blazor Web Apps operating over a circuit.*

*This section applies to Blazor Server apps.*

Client-side errors don't include the call stack and don't provide detail on the cause of the error, but server logs do contain such information. For development purposes, sensitive circuit error information can be made available to the client by enabling detailed errors.

Set [CircuitOptions.DetailedErrors](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.detailederrors#microsoft-aspnetcore-components-server-circuitoptions-detailederrors) to `true`. For more information and an example, see [ASP.NET Core Blazor SignalR guidance](signalr?view=aspnetcore-10.0#server-side-circuit-handler-options).

An alternative to setting [CircuitOptions.DetailedErrors](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.detailederrors#microsoft-aspnetcore-components-server-circuitoptions-detailederrors) is to set the `DetailedErrors` configuration key to `true` in the app's `Development` environment settings file (`appsettings.Development.json`). Additionally, set [SignalR server-side logging](../../signalr/diagnostics?view=aspnetcore-10.0#server-side-logging) (`Microsoft.AspNetCore.SignalR`) to [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel) or [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel) for detailed SignalR logging.

`appsettings.Development.json`:

```
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}
```

The [DetailedErrors](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.detailederrors#microsoft-aspnetcore-components-server-circuitoptions-detailederrors) configuration key can also be set to `true` using the `ASPNETCORE_DETAILEDERRORS` environment variable with a value of `true` on `Development`/`Staging` environment servers or on your local system.

Warning

Always avoid exposing error information to clients on the Internet, which is a security risk.

## Detailed errors for Razor component server-side rendering

*This section applies to Blazor Web Apps.*

Use the [RazorComponentsServiceOptions.DetailedErrors](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions.detailederrors#microsoft-aspnetcore-components-endpoints-razorcomponentsserviceoptions-detailederrors) option to control producing detailed information on errors for Razor component server-side rendering. The default value is `false`.

The following example enables detailed errors:

```
builder.Services.AddRazorComponents(options => 
    options.DetailedErrors = builder.Environment.IsDevelopment());
```

Warning

**Only enable detailed errors in the `Development` environment.** Detailed errors may contain sensitive information about the app that malicious users can use in an attack.

The preceding example provides a degree of safety by setting the value of [DetailedErrors](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions.detailederrors#microsoft-aspnetcore-components-endpoints-razorcomponentsserviceoptions-detailederrors) based on the value returned by [IsDevelopment](/en-us/dotnet/api/microsoft.extensions.hosting.hostenvironmentenvextensions.isdevelopment). When the app is in the `Development` environment, [DetailedErrors](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions.detailederrors#microsoft-aspnetcore-components-endpoints-razorcomponentsserviceoptions-detailederrors) is set to `true`. This approach isn't foolproof because it's possible to host a production app on a public server in the `Development` environment.

## Manage unhandled exceptions in developer code

For an app to continue after an error, the app must have error handling logic. Later sections of this article describe potential sources of unhandled exceptions.

In production, don't render framework exception messages or stack traces in the UI. Rendering exception messages or stack traces could:

* Disclose sensitive information to end users.
* Help a malicious user discover weaknesses in an app that can compromise the security of the app, server, or network.

## Unhandled exceptions for circuits

*This section applies to server-side apps operating over a circuit.*

Razor components with server interactivity enabled are stateful on the server. While users interact with the component on the server, they maintain a connection to the server known as a *circuit*. The circuit holds active component instances, plus many other aspects of state, such as:

* The most recent rendered output of components.
* The current set of event-handling delegates that could be triggered by client-side events.

If a user opens the app in multiple browser tabs, the user creates multiple independent circuits.

Blazor treats most unhandled exceptions as fatal to the circuit where they occur. If a circuit is terminated due to an unhandled exception, the user can only continue to interact with the app by reloading the page to create a new circuit. Circuits outside of the one that's terminated, which are circuits for other users or other browser tabs, aren't affected. This scenario is similar to a desktop app that crashes. The crashed app must be restarted, but other apps aren't affected.

The framework terminates a circuit when an unhandled exception occurs for the following reasons:

* An unhandled exception often leaves the circuit in an undefined state.
* The app's normal operation can't be guaranteed after an unhandled exception.
* Security vulnerabilities may appear in the app if the circuit continues in an undefined state.

## Global exception handling

For approaches to handling exceptions globally, see the following sections:

* [Error boundaries](#error-boundaries): Applies to all Blazor apps.
* [Alternative global exception handling](#alternative-global-exception-handling): Applies to Blazor Server, Blazor WebAssembly, and Blazor Web Apps (8.0 or later) that adopt a global interactive render mode.

## Error boundaries

*Error boundaries* provide a convenient approach for handling exceptions. The [ErrorBoundary](/en-us/dotnet/api/microsoft.aspnetcore.components.web.errorboundary) component:

* Renders its child content when an error hasn't occurred.
* Renders error UI when an unhandled exception is thrown by any component within the error boundary.

To define an error boundary, use the [ErrorBoundary](/en-us/dotnet/api/microsoft.aspnetcore.components.web.errorboundary) component to wrap one or more other components. The error boundary manages unhandled exceptions thrown by the components that it wraps.

```
<ErrorBoundary>
    ...
</ErrorBoundary>
```

To implement an error boundary in a global fashion, add the boundary around the body content of the app's main layout.

In `MainLayout.razor`:

```
<article class="content px-4">
    <ErrorBoundary>
        @Body
    </ErrorBoundary>
</article>
```

In Blazor Web Apps with the error boundary only applied to a static `MainLayout` component, the boundary is only active during static server-side rendering (static SSR). The boundary doesn't activate just because a component further down the component hierarchy is interactive.

An interactive render mode can't be applied to the `MainLayout` component because the component's `Body` parameter is a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) delegate, which is arbitrary code and can't be serialized. To enable interactivity broadly for the `MainLayout` component and the rest of the components further down the component hierarchy, the app must adopt a global interactive render mode by applying the interactive render mode to the `HeadOutlet` and `Routes` component instances in the app's root component, which is typically the `App` component. The following example adopts the Interactive Server (`InteractiveServer`) render mode globally.

In `Components/App.razor`:

```
<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />
```

If you prefer not to enable global interactivity, place the error boundary farther down the component hierarchy. The important concepts to keep in mind are that wherever the error boundary is placed:

* If the component where the error boundary is placed isn't interactive, the error boundary is only capable of activating on the server during static SSR. For example, the boundary can activate when an error is thrown in a component lifecycle method but not for an event triggered by user interactivity within the component, such as an error thrown by a button click handler.
* If the component where the error boundary is placed is interactive, the error boundary is capable of activating for interactive components that it wraps.

Note

The preceding considerations aren't relevant for standalone Blazor WebAssembly apps because the client-side rendering (CSR) of a Blazor WebAssembly app is completely interactive.

Consider the following example, where an exception thrown by an embedded counter component is caught by an error boundary in the `Home` component, which adopts an interactive render mode.

`EmbeddedCounter.razor`:

```
<h1>Embedded Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        if (currentCount > 5)
        {
            throw new InvalidOperationException("Current count is too big!");
        }
    }
}
```

`Home.razor`:

```
@page "/"
@rendermode InteractiveServer

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<ErrorBoundary>
    <EmbeddedCounter />
</ErrorBoundary>
```

Consider the following example, where an exception thrown by an embedded counter component is caught by an error boundary in the `Home` component.

`EmbeddedCounter.razor`:

```
<h1>Embedded Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        if (currentCount > 5)
        {
            throw new InvalidOperationException("Current count is too big!");
        }
    }
}
```

`Home.razor`:

```
@page "/"

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<ErrorBoundary>
    <EmbeddedCounter />
</ErrorBoundary>
```

If the unhandled exception is thrown for a `currentCount` over five:

* The error is logged normally (`System.InvalidOperationException: Current count is too big!`).
* The exception is handled by the error boundary.
* The default error UI is rendered by the error boundary.

The [ErrorBoundary](/en-us/dotnet/api/microsoft.aspnetcore.components.web.errorboundary) component renders an empty `<div>` element using the `blazor-error-boundary` CSS class for its error content. The colors, text, and icon for the default UI are defined in the app's stylesheet in the `wwwroot` folder, so you're free to customize the error UI.

To change the default error content:

* Wrap the components of the error boundary in the [ChildContent](/en-us/dotnet/api/microsoft.aspnetcore.components.errorboundarybase.childcontent#microsoft-aspnetcore-components-errorboundarybase-childcontent) property.
* Set the [ErrorContent](/en-us/dotnet/api/microsoft.aspnetcore.components.errorboundarybase.errorcontent#microsoft-aspnetcore-components-errorboundarybase-errorcontent) property to the error content.

The following example wraps the `EmbeddedCounter` component and supplies custom error content:

```
<ErrorBoundary>
    <ChildContent>
        <EmbeddedCounter />
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
    </ErrorContent>
</ErrorBoundary>
```

For the preceding example, the app's stylesheet presumably includes an `errorUI` CSS class to style the content. The error content is rendered from the [ErrorContent](/en-us/dotnet/api/microsoft.aspnetcore.components.errorboundarybase.errorcontent#microsoft-aspnetcore-components-errorboundarybase-errorcontent) property without a block-level element. A block-level element, such as a division (`<div>`) or a paragraph (`<p>`) element, can wrap the error content markup, but it isn't required.

Optionally, use the context (`@context`) of the [ErrorContent](/en-us/dotnet/api/microsoft.aspnetcore.components.errorboundarybase.errorcontent#microsoft-aspnetcore-components-errorboundarybase-errorcontent) to obtain error data:

```
<ErrorContent>
    @context.HelpLink
</ErrorContent>
```

The [ErrorContent](/en-us/dotnet/api/microsoft.aspnetcore.components.errorboundarybase.errorcontent#microsoft-aspnetcore-components-errorboundarybase-errorcontent) can also name the context. In the following example, the context is named `exception`:

```
<ErrorContent Context="exception">
    @exception.HelpLink
</ErrorContent>
```

Warning

Always avoid exposing error information to clients on the Internet, which is a security risk.

If the error boundary is defined in the app's layout, the error UI is seen regardless of which page the user navigates to after the error occurs. We recommend narrowly scoping error boundaries in most scenarios. If you broadly scope an error boundary, you can reset it to a non-error state on subsequent page navigation events by calling the error boundary's [Recover](/en-us/dotnet/api/microsoft.aspnetcore.components.errorboundarybase.recover) method.

In `MainLayout.razor`:

* Add a field for the [ErrorBoundary](/en-us/dotnet/api/microsoft.aspnetcore.components.web.errorboundary) to [capture a reference](../components/?view=aspnetcore-10.0#capture-references-to-components) to it with the [`@ref`](../../mvc/views/razor?view=aspnetcore-10.0#ref) attribute directive.
* In the [`OnParameterSet` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#after-parameters-are-set-onparameterssetasync), you can trigger a recovery on the error boundary with [Recover](/en-us/dotnet/api/microsoft.aspnetcore.components.errorboundarybase.recover) to clear the error when the user navigates to a different component.

```
...

<ErrorBoundary @ref="errorBoundary">
    @Body
</ErrorBoundary>

...

@code {
    private ErrorBoundary? errorBoundary;

    protected override void OnParametersSet()
    {
        errorBoundary?.Recover();
    }
}
```

To avoid the infinite loop where recovering merely rerenders a component that throws the error again, don't call [Recover](/en-us/dotnet/api/microsoft.aspnetcore.components.errorboundarybase.recover) from rendering logic. Only call [Recover](/en-us/dotnet/api/microsoft.aspnetcore.components.errorboundarybase.recover) when:

* The user performs a UI gesture, such as selecting a button to indicate that they want to retry a procedure or when the user navigates to a new component.
* Additional logic that executes also clears the exception. When the component is rerendered, the error doesn't reoccur.

The following example permits the user to recover from the exception with a button:

```
<ErrorBoundary @ref="errorBoundary">
    <ChildContent>
        <EmbeddedCounter />
    </ChildContent>
    <ErrorContent>
        <div class="alert alert-danger" role="alert">
            <p class="fs-3 fw-bold">😈 A rotten gremlin got us. Sorry!</p>
            <p>@context.HelpLink</p>
            <button class="btn btn-info" @onclick="_ => errorBoundary?.Recover()">
                Clear
            </button>
        </div>
    </ErrorContent>
</ErrorBoundary>

@code {
    private ErrorBoundary? errorBoundary;
}
```

You can also subclass [ErrorBoundary](/en-us/dotnet/api/microsoft.aspnetcore.components.web.errorboundary) for custom processing by overriding [OnErrorAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.web.errorboundary.onerrorasync). The following example merely logs the error, but you can implement any error handling code you wish. You can remove the line that returns a [CompletedTask](/en-us/dotnet/api/system.threading.tasks.task.completedtask#system-threading-tasks-task-completedtask) if your code awaits an asynchronous task.

`CustomErrorBoundary.razor`:

```
@inherits ErrorBoundary
@inject ILogger<CustomErrorBoundary> Logger

@if (CurrentException is null)
{
    @ChildContent
}
else if (ErrorContent is not null)
{
    @ErrorContent(CurrentException)
}

@code {
    protected override Task OnErrorAsync(Exception ex)
    {
        Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
        return Task.CompletedTask;
    }
}
```

The preceding example can also be implemented as a class.

`CustomErrorBoundary.cs`:

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace BlazorSample;

public class CustomErrorBoundary : ErrorBoundary
{
    [Inject]
    ILogger<CustomErrorBoundary> Logger {  get; set; } = default!;

    protected override Task OnErrorAsync(Exception ex)
    {
        Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
        return Task.CompletedTask;
    }
}
```

Either of the preceding implementations used in a component:

```
<CustomErrorBoundary>
    ...
</CustomErrorBoundary>
```

## Alternative global exception handling

The approach described in this section applies to Blazor Server, Blazor WebAssembly, and Blazor Web Apps that adopt a global interactive render mode (`InteractiveServer`, `InteractiveWebAssembly`, or `InteractiveAuto`). The approach doesn't work with Blazor Web Apps that adopt per-page/component render modes or static server-side rendering (static SSR) because the approach relies on a [`CascadingValue`](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component)/[`CascadingParameter`](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingparameter-attribute), which don't work across render mode boundaries or with components that adopt static SSR.

An alternative to using [Error boundaries](#error-boundaries) ([ErrorBoundary](/en-us/dotnet/api/microsoft.aspnetcore.components.web.errorboundary)) is to pass a custom error component as a [`CascadingValue`](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component) to child components. An advantage of using a component over using an [injected service](dependency-injection?view=aspnetcore-10.0) or a custom logger implementation is that a cascaded component can render content and apply CSS styles when an error occurs.

The following `ProcessError` component example merely logs errors, but methods of the component can process errors in any way required by the app, including through the use of multiple error processing methods.

`ProcessError.razor`:

```
@inject ILogger<ProcessError> Logger

<CascadingValue Value="this" IsFixed="true">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public void LogError(Exception ex)
    {
        Logger.LogError("ProcessError.LogError: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);

        // Call StateHasChanged if LogError directly participates in 
        // rendering. If LogError only logs or records the error,
        // there's no need to call StateHasChanged.
        //StateHasChanged();
    }
}
```

Note

For more information on [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), see [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#child-content-render-fragments).

[CascadingValue\<TValue>.IsFixed](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1.isfixed) is used to indicate that a cascading parameter doesn't change after initialization.

When using this approach in a Blazor Web App, open the `Routes` component and wrap the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component (`<Router>...</Router>`) with the `ProcessError` component. This permits the `ProcessError` component to cascade down to any component of the app where the `ProcessError` component is received as a [`CascadingParameter`](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingparameter-attribute).

In `Routes.razor`:

```
<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>
```

When using this approach in a Blazor Server or Blazor WebAssembly app, open the `App` component, wrap the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component (`<Router>...</Router>`) with the `ProcessError` component. This permits the `ProcessError` component to cascade down to any component of the app where the `ProcessError` component is received as a [`CascadingParameter`](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingparameter-attribute).

In `App.razor`:

```
<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>
```

To process errors in a component:

* Designate the `ProcessError` component as a [`CascadingParameter`](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingparameter-attribute) in the [`@code`](../../mvc/views/razor?view=aspnetcore-10.0#code) block. In an example `Counter` component in an app based on a Blazor project template, add the following `ProcessError` property:

  ```
  [CascadingParameter]
  private ProcessError? ProcessError { get; set; }
  ```

* Call an error processing method in any `catch` block with an appropriate exception type. The example `ProcessError` component only offers a single `LogError` method, but the error processing component can provide any number of error processing methods to address alternative error processing requirements throughout the app. The following `Counter` component `@code` block example includes the `ProcessError` cascading parameter and traps an exception for logging when the count is greater than five:

  ```
  @code {
      private int currentCount = 0;

      [CascadingParameter]
      private ProcessError? ProcessError { get; set; }

      private void IncrementCount()
      {
          try
          {
              currentCount++;

              if (currentCount > 5)
              {
                  throw new InvalidOperationException("Current count is over five!");
              }
          }
          catch (Exception ex)
          {
              ProcessError?.LogError(ex);
          }
      }
  }
  ```

The logged error:

> fail: {COMPONENT NAMESPACE}.ProcessError\[0]\
> ProcessError.LogError: System.InvalidOperationException Message: Current count is over five!

If the `LogError` method directly participates in rendering, such as showing a custom error message bar or changing the CSS styles of the rendered elements, call [`StateHasChanged`](../components/lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged) at the end of the `LogError` method to rerender the UI.

Because the approaches in this section handle errors with a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement, an app's SignalR connection between the client and server isn't broken when an error occurs and the circuit remains alive. Other unhandled exceptions remain fatal to a circuit. For more information, see the section on [how a circuit reacts to unhandled exceptions](#unhandled-exceptions-for-circuits).

An app can use an error processing component as a cascading value to process errors in a centralized way.

The following `ProcessError` component passes itself as a [`CascadingValue`](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component) to child components. The following example merely logs the error, but methods of the component can process errors in any way required by the app, including through the use of multiple error processing methods. An advantage of using a component over using an [injected service](dependency-injection?view=aspnetcore-10.0) or a custom logger implementation is that a cascaded component can render content and apply CSS styles when an error occurs.

`ProcessError.razor`:

```
@using Microsoft.Extensions.Logging
@inject ILogger<ProcessError> Logger

<CascadingValue Value="this" IsFixed="true">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public void LogError(Exception ex)
    {
        Logger.LogError("ProcessError.LogError: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);
    }
}
```

Note

For more information on [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), see [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#child-content-render-fragments).

[CascadingValue\<TValue>.IsFixed](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1.isfixed) is used to indicate that a cascading parameter doesn't change after initialization.

In the `App` component, wrap the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component with the `ProcessError` component. This permits the `ProcessError` component to cascade down to any component of the app where the `ProcessError` component is received as a [`CascadingParameter`](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingparameter-attribute).

`App.razor`:

```
<ProcessError>
    <Router ...>
        ...
    </Router>
</ProcessError>
```

To process errors in a component:

* Designate the `ProcessError` component as a [`CascadingParameter`](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingparameter-attribute) in the [`@code`](../../mvc/views/razor?view=aspnetcore-10.0#code) block:

  ```
  [CascadingParameter]
  private ProcessError ProcessError { get; set; }
  ```

* Call an error processing method in any `catch` block with an appropriate exception type. The example `ProcessError` component only offers a single `LogError` method, but the error processing component can provide any number of error processing methods to address alternative error processing requirements throughout the app.

  ```
  try
  {
      ...
  }
  catch (Exception ex)
  {
      ProcessError.LogError(ex);
  }
  ```

Using the preceding example `ProcessError` component and `LogError` method, the browser's developer tools console indicates the trapped, logged error:

> fail: {COMPONENT NAMESPACE}.Shared.ProcessError\[0]\
> ProcessError.LogError: System.NullReferenceException Message: Object reference not set to an instance of an object.

If the `LogError` method directly participates in rendering, such as showing a custom error message bar or changing the CSS styles of the rendered elements, call [`StateHasChanged`](../components/lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged) at the end of the `LogError` method to rerender the UI.

Because the approaches in this section handle errors with a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement, a Blazor app's SignalR connection between the client and server isn't broken when an error occurs and the circuit remains alive. Any unhandled exception is fatal to a circuit. For more information, see the section on [how a circuit reacts to unhandled exceptions](#unhandled-exceptions-for-circuits).

## Log errors with a persistent provider

If an unhandled exception occurs, the exception is logged to [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) instances configured in the service container. Blazor apps log console output with the Console Logging Provider. Consider logging to a location on the server (or backend web API for client-side apps) with a provider that manages log size and log rotation. Alternatively, the app can use an Application Performance Management (APM) service, such as [Azure Application Insights (Azure Monitor)](/en-us/azure/azure-monitor/app/app-insights-overview).

Note

Native [Application Insights](/en-us/azure/azure-monitor/app/app-insights-overview) features to support client-side apps and native Blazor framework support for [Google Analytics](https://analytics.google.com/analytics/web/) might become available in future releases of these technologies. For more information, see [Support App Insights in Blazor WASM Client Side (`microsoft/ApplicationInsights-dotnet` #2143)](https://github.com/microsoft/ApplicationInsights-dotnet/issues/2143) and [Web analytics and diagnostics (includes links to community implementations) (`dotnet/aspnetcore` #5461)](https://github.com/dotnet/aspnetcore/issues/5461). In the meantime, a client-side app can use the [Application Insights JavaScript SDK](/en-us/azure/azure-monitor/app/javascript) with [JS interop](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0) to log errors directly to Application Insights from a client-side app.

During development in a Blazor app operating over a circuit, the app usually sends the full details of exceptions to the browser's console to aid in debugging. In production, detailed errors aren't sent to clients, but an exception's full details are logged on the server.

You must decide which incidents to log and the level of severity of logged incidents. Hostile users might be able to trigger errors deliberately. For example, don't log an incident from an error where an unknown `ProductId` is supplied in the URL of a component that displays product details. Not all errors should be treated as incidents for logging.

For more information, see the following articles:

* [ASP.NET Core Blazor logging](logging?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core](../../fundamentals/error-handling?view=aspnetcore-10.0)‡
* [Create web APIs with ASP.NET Core](../../web-api/?view=aspnetcore-10.0)

‡Applies to server-side Blazor apps and other server-side ASP.NET Core apps that are web API backend apps for Blazor. Client-side apps can trap and send error information on the client to a web API, which logs the error information to a persistent logging provider.

If an unhandled exception occurs, the exception is logged to [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) instances configured in the service container. Blazor apps log console output with the Console Logging Provider. Consider logging to a more permanent location on the server by sending error information to a backend web API that uses a logging provider with log size management and log rotation. Alternatively, the backend web API app can use an Application Performance Management (APM) service, such as [Azure Application Insights (Azure Monitor)†](/en-us/azure/azure-monitor/app/app-insights-overview), to record error information that it receives from clients.

You must decide which incidents to log and the level of severity of logged incidents. Hostile users might be able to trigger errors deliberately. For example, don't log an incident from an error where an unknown `ProductId` is supplied in the URL of a component that displays product details. Not all errors should be treated as incidents for logging.

For more information, see the following articles:

* [ASP.NET Core Blazor logging](logging?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core](../../fundamentals/error-handling?view=aspnetcore-10.0)‡
* [Create web APIs with ASP.NET Core](../../web-api/?view=aspnetcore-10.0)

†Native [Application Insights](/en-us/azure/azure-monitor/app/app-insights-overview) features to support client-side apps and native Blazor framework support for [Google Analytics](https://analytics.google.com/analytics/web/) might become available in future releases of these technologies. For more information, see [Support App Insights in Blazor WASM Client Side (`microsoft/ApplicationInsights-dotnet` #2143)](https://github.com/microsoft/ApplicationInsights-dotnet/issues/2143) and [Web analytics and diagnostics (includes links to community implementations) (`dotnet/aspnetcore` #5461)](https://github.com/dotnet/aspnetcore/issues/5461). In the meantime, a client-side app can use the [Application Insights JavaScript SDK](/en-us/azure/azure-monitor/app/javascript) with [JS interop](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0) to log errors directly to Application Insights from a client-side app.

‡Applies to server-side ASP.NET Core apps that are web API backend apps for Blazor apps. Client-side apps trap and send error information to a web API, which logs the error information to a persistent logging provider.

## Places where errors may occur

Framework and app code may trigger unhandled exceptions in any of the following locations, which are described further in the following sections of this article:

* [Component instantiation](#component-instantiation)
* [Lifecycle methods](#lifecycle-methods)
* [Rendering logic](#rendering-logic)
* [Event handlers](#event-handlers)
* [Component disposal](#component-disposal)
* [JavaScript interop](#javascript-interop)
* [Prerendering](#prerendering)

- [Component instantiation](#component-instantiation)
- [Lifecycle methods](#lifecycle-methods)
- [Rendering logic](#rendering-logic)
- [Event handlers](#event-handlers)
- [Component disposal](#component-disposal)
- [JavaScript interop](#javascript-interop)
- [Prerendering](#prerendering)

### Component instantiation

When Blazor creates an instance of a component:

* The component's constructor is invoked.
* The constructors of DI services supplied to the component's constructor via the [`@inject`](../../mvc/views/razor?view=aspnetcore-10.0#inject) directive or the [`[Inject]` attribute](dependency-injection?view=aspnetcore-10.0#request-a-service-in-a-component) are invoked.

An error in an executed constructor or a setter for any `[Inject]` property results in an unhandled exception and stops the framework from instantiating the component. If the app is operating over a circuit, the circuit fails. If constructor logic may throw exceptions, the app should trap the exceptions using a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement with error handling and logging.

### Lifecycle methods

During the lifetime of a component, Blazor invokes [lifecycle methods](../components/lifecycle?view=aspnetcore-10.0). If any lifecycle method throws an exception, synchronously or asynchronously, the exception is fatal to a circuit. For components to deal with errors in lifecycle methods, add error handling logic.

In the following example where [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) calls a method to obtain a product:

* An exception thrown in the `ProductRepository.GetProductByIdAsync` method is handled by a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement.

* When the `catch` block is executed:

  * `loadFailed` is set to `true`, which is used to display an error message to the user.
  * The error is logged.

```
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product

<PageTitle>Product Details</PageTitle>

<h1>Product Details Example</h1>

@if (details != null)
{
    <h2>@details.ProductName</h2>
    <p>
        @details.Description
        <a href="@details.Url">Company Link</a>
    </p>
    
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

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

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await Product.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
        public string? Url { get; set; }
    }

    /*
    * Register the service in Program.cs:
    * using static BlazorSample.Components.Pages.ProductDetails;
    * builder.Services.AddScoped<IProductRepository, ProductRepository>();
    */

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }

    public class ProductRepository : IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id)
        {
            return Task.FromResult(
                new ProductDetail()
                {
                    ProductName = "Flowbee ",
                    Description = "The Revolutionary Haircutting System You've Come to Love!",
                    Url = "https://flowbee.com/"
                });
        }
    }
}
```

```
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product

<PageTitle>Product Details</PageTitle>

<h1>Product Details Example</h1>

@if (details != null)
{
    <h2>@details.ProductName</h2>
    <p>
        @details.Description
        <a href="@details.Url">Company Link</a>
    </p>
    
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

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

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await Product.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
        public string? Url { get; set; }
    }

    /*
    * Register the service in Program.cs:
    * using static BlazorSample.Components.Pages.ProductDetails;
    * builder.Services.AddScoped<IProductRepository, ProductRepository>();
    */

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }

    public class ProductRepository : IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id)
        {
            return Task.FromResult(
                new ProductDetail()
                {
                    ProductName = "Flowbee ",
                    Description = "The Revolutionary Haircutting System You've Come to Love!",
                    Url = "https://flowbee.com/"
                });
        }
    }
}
```

```
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

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

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
```

```
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

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

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
```

```
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

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

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
```

```
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

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

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
```

### Rendering logic

The declarative markup in a Razor component file (`.razor`) is compiled into a C# method called [BuildRenderTree](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.buildrendertree). When a component renders, [BuildRenderTree](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.buildrendertree) executes and builds up a data structure describing the elements, text, and child components of the rendered component.

Rendering logic can throw an exception. An example of this scenario occurs when `@someObject.PropertyName` is evaluated but `@someObject` is `null`. For Blazor apps operating over a circuit, an unhandled exception thrown by rendering logic is fatal to the app's circuit.

To prevent a [NullReferenceException](/en-us/dotnet/api/system.nullreferenceexception) in rendering logic, check for a `null` object before accessing its members. In the following example, `person.Address` properties aren't accessed if `person.Address` is `null`:

```
@if (person.Address != null)
{
    <div>@person.Address.Line1</div>
    <div>@person.Address.Line2</div>
    <div>@person.Address.City</div>
    <div>@person.Address.Country</div>
}
```

The preceding code assumes that `person` isn't `null`. Often, the structure of the code guarantees that an object exists at the time the component is rendered. In those cases, it isn't necessary to check for `null` in rendering logic. In the prior example, `person` might be guaranteed to exist because `person` is created when the component is instantiated, as the following example shows:

```
@code {
    private Person person = new();

    ...
}
```

### Event handlers

Client-side code triggers invocations of C# code when event handlers are created using:

* `@onclick`
* `@onchange`
* Other `@on...` attributes
* `@bind`

Event handler code might throw an unhandled exception in these scenarios.

If the app calls code that could fail for external reasons, trap exceptions using a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement with error handling and logging.

If an event handler throws an unhandled exception (for example, a database query fails) that isn't trapped and handled by developer code:

* The framework logs the exception.
* In a Blazor app operating over a circuit, the exception is fatal to the app's circuit.

### Component disposal

A component may be removed from the UI, for example, because the user has navigated to another page. When a component that implements [System.IDisposable](/en-us/dotnet/api/system.idisposable) is removed from the UI, the framework calls the component's [Dispose](/en-us/dotnet/api/system.idisposable.dispose) method.

If the component's `Dispose` method throws an unhandled exception in a Blazor app operating over a circuit, the exception is fatal to the app's circuit.

If disposal logic may throw exceptions, the app should trap the exceptions using a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement with error handling and logging.

For more information on component disposal, see [ASP.NET Core Razor component disposal](../components/component-disposal?view=aspnetcore-10.0).

### JavaScript interop

[IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) is registered by the Blazor framework. [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) allows .NET code to make asynchronous calls to the JavaScript (JS) runtime in the user's browser.

The following conditions apply to error handling with [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync):

* If a call to [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) fails synchronously, a .NET exception occurs. A call to [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) may fail, for example, because the supplied arguments can't be serialized. Developer code must catch the exception. If app code in an event handler or component lifecycle method doesn't handle an exception in a Blazor app operating over a circuit, the resulting exception is fatal to the app's circuit.
* If a call to [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) fails asynchronously, the .NET [Task](/en-us/dotnet/api/system.threading.tasks.task) fails. A call to [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) may fail, for example, because the JS-side code throws an exception or returns a `Promise` that completed as `rejected`. Developer code must catch the exception. If using the [`await`](/en-us/dotnet/csharp/language-reference/keywords/await) operator, consider wrapping the method call in a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement with error handling and logging. Otherwise in a Blazor app operating over a circuit, the failing code results in an unhandled exception that's fatal to the app's circuit.
* Calls to [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) must complete within a certain period or else the call times out. The default timeout period is one minute. The timeout protects the code against a loss in network connectivity or JS code that never sends back a completion message. If the call times out, the resulting [System.Threading.Tasks](/en-us/dotnet/api/system.threading.tasks) fails with an [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception). Trap and process the exception with logging.

Similarly, JS code may initiate calls to .NET methods indicated by the [`[JSInvokable]` attribute](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0). If these .NET methods throw an unhandled exception:

* In a Blazor app operating over a circuit, the exception isn't treated as fatal to the app's circuit.
* The JS-side `Promise` is rejected.

You have the option of using error handling code on either the .NET side or the JS side of the method call.

For more information, see the following articles:

* [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0)
* [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0)

### Prerendering

Razor components are prerendered by default so that their rendered HTML markup is returned as part of the user's initial HTTP request.

In a Blazor app operating over a circuit, prerendering works by:

* Creating a new circuit for all of the prerendered components that are part of the same page.
* Generating the initial HTML.
* Treating the circuit as `disconnected` until the user's browser establishes a SignalR connection back to the same server. When the connection is established, interactivity on the circuit is resumed and the components' HTML markup is updated.

For prerendered client-side components, prerendering works by:

* Generating initial HTML on the server for all of the prerendered components that are part of the same page.
* Making the component interactive on the client after the browser has loaded the app's compiled code and the .NET runtime (if not already loaded) in the background.

If a component throws an unhandled exception during prerendering, for example, during a lifecycle method or in rendering logic:

* In a Blazor app operating over a circuit, the exception is fatal to the circuit. For prerendered client-side components, the exception prevents rendering the component.
* The exception is thrown up the call stack from the [ComponentTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper).

Under normal circumstances when prerendering fails, continuing to build and render the component doesn't make sense because a working component can't be rendered.

To tolerate errors that may occur during prerendering, error handling logic must be placed inside a component that may throw exceptions. Use [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statements with error handling and logging. Instead of wrapping the [ComponentTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper) in a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement, place error handling logic in the component rendered by the [ComponentTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper).

## Advanced scenarios

### Recursive rendering

Components can be nested recursively. This is useful for representing recursive data structures. For example, a `TreeNode` component can render more `TreeNode` components for each of the node's children.

When rendering recursively, avoid coding patterns that result in infinite recursion:

* Don't recursively render a data structure that contains a cycle. For example, don't render a tree node whose children includes itself.
* Don't create a chain of layouts that contain a cycle. For example, don't create a layout whose layout is itself.
* Don't allow an end user to violate recursion invariants (rules) through malicious data entry or JavaScript interop calls.

Infinite loops during rendering:

* Causes the rendering process to continue forever.
* Is equivalent to creating an unterminated loop.

In these scenarios, Blazor fails and usually attempts to:

* Consume as much CPU time as permitted by the operating system, indefinitely.
* Consume an unlimited amount of memory. Consuming unlimited memory is equivalent to the scenario where an unterminated loop adds entries to a collection on every iteration.

To avoid infinite recursion patterns, ensure that recursive rendering code contains suitable stopping conditions.

### Custom render tree logic

Most Razor components are implemented as Razor component files (`.razor`) and are compiled by the framework to produce logic that operates on a [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) to render their output. However, a developer may manually implement [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) logic using procedural C# code. For more information, see [ASP.NET Core Blazor advanced scenarios (render tree construction)](../advanced-scenarios?view=aspnetcore-10.0#manually-build-a-render-tree-rendertreebuilder).

Warning

Use of manual render tree builder logic is considered an advanced and unsafe scenario, not recommended for general component development.

If [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) code is written, the developer must guarantee the correctness of the code. For example, the developer must ensure that:

* Calls to [OpenElement](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder.openelement) and [CloseElement](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder.closeelement) are correctly balanced.
* Attributes are only added in the correct places.

Incorrect manual render tree builder logic can cause arbitrary undefined behavior, including crashes, the app or server to stop responding, and security vulnerabilities.

Consider manual render tree builder logic on the same level of complexity and with the same level of *danger* as writing assembly code or [Microsoft Intermediate Language (MSIL)](/en-us/dotnet/standard/managed-code) instructions by hand.

## Related content

* [Handle caught exceptions outside of a Razor component's lifecycle](../components/synchronization-context?view=aspnetcore-10.0#handle-caught-exceptions-outside-of-a-razor-components-lifecycle)
* [Not Found responses](navigation?view=aspnetcore-10.0#not-found-responses)
* [ASP.NET Core Blazor logging](logging?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core](../../fundamentals/error-handling?view=aspnetcore-10.0)†
* [Create web APIs with ASP.NET Core](../../web-api/?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](./?view=aspnetcore-10.0#sample-apps))

- [Handle caught exceptions outside of a Razor component's lifecycle](../components/synchronization-context?view=aspnetcore-10.0#handle-caught-exceptions-outside-of-a-razor-components-lifecycle)
- [ASP.NET Core Blazor logging](logging?view=aspnetcore-10.0)
- [Handle errors in ASP.NET Core](../../fundamentals/error-handling?view=aspnetcore-10.0)†
- [Create web APIs with ASP.NET Core](../../web-api/?view=aspnetcore-10.0)
- [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](./?view=aspnetcore-10.0#sample-apps))

* [ASP.NET Core Blazor logging](logging?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core](../../fundamentals/error-handling?view=aspnetcore-10.0)†
* [Create web APIs with ASP.NET Core](../../web-api/?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](./?view=aspnetcore-10.0#sample-apps))

†Applies to backend ASP.NET Core web API apps that client-side Blazor apps use for logging.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/clientfactory
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/clientfactory.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC client factory integration in .NET

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

gRPC integration with `HttpClientFactory` offers a centralized way to create gRPC clients. It can be used as an alternative to [configuring stand-alone gRPC client instances](client?view=aspnetcore-10.0). Factory integration is available in the [Grpc.Net.ClientFactory](https://www.nuget.org/packages/Grpc.Net.ClientFactory) NuGet package.

The factory offers the following benefits:

* Provides a central location for configuring logical gRPC client instances.
* Manages the lifetime of the underlying `HttpClientMessageHandler`.
* Automatic propagation of deadline and cancellation in an ASP.NET Core gRPC service.

## Register gRPC clients

To register a gRPC client, the generic `AddGrpcClient` extension method can be used within an instance of [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) at the app's entry point in `Program.cs`, specifying the gRPC typed client class and service address:

```
builder.Services.AddGrpcClient<Greeter.GreeterClient>(o =>
{
    o.Address = new Uri("https://localhost:5001");
});
```

The gRPC client type is registered as transient with dependency injection (DI). The client can now be injected and consumed directly in types created by DI. ASP.NET Core MVC controllers, SignalR hubs and gRPC services are places where gRPC clients can automatically be injected:

```
public class AggregatorService : Aggregator.AggregatorBase
{
    private readonly Greeter.GreeterClient _client;

    public AggregatorService(Greeter.GreeterClient client)
    {
        _client = client;
    }

    public override async Task SayHellos(HelloRequest request,
        IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
    {
        // Forward the call on to the greeter service
        using (var call = _client.SayHellos(request))
        {
            await foreach (var response in call.ResponseStream.ReadAllAsync())
            {
                await responseStream.WriteAsync(response);
            }
        }
    }
}
```

## Configure HttpHandler

`HttpClientFactory` creates the `HttpMessageHandler` used by the gRPC client. Standard `HttpClientFactory` methods can be used to add outgoing request middleware or to configure the underlying `HttpClientHandler` of the `HttpClient`:

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        var handler = new HttpClientHandler();
        handler.ClientCertificates.Add(LoadCertificate());
        return handler;
    });
```

For more information, see [Make HTTP requests using IHttpClientFactory](../fundamentals/http-requests?view=aspnetcore-10.0).

## Configure Interceptors

gRPC interceptors can be added to clients using the `AddInterceptor` method.

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddInterceptor<LoggingInterceptor>();
```

The preceding code:

* Registers the `GreeterClient` type.
* Configures a `LoggingInterceptor` for this client. `LoggingInterceptor` is created once and shared between `GreeterClient` instances.

By default, an interceptor is created once and shared between clients. This behavior can be overridden by specifying a scope when registering an interceptor. The client factory can be configured to create a new interceptor for each client by specifying `InterceptorScope.Client`.

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddInterceptor<LoggingInterceptor>(InterceptorScope.Client);
```

Creating client scoped interceptors is useful when an interceptor requires [scoped or transient scoped services from DI](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes).

A gRPC interceptor or channel credentials can be used to send `Authorization` metadata with each request. For more information about configuring authentication, see [Send a bearer token with gRPC client factory](authn-and-authz?view=aspnetcore-10.0#bearer-token-with-grpc-client-factory).

## Configure Channel

Additional configuration can be applied to a channel using the `ConfigureChannel` method:

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigureChannel(o =>
    {
        o.Credentials = new CustomCredentials();
    });
```

`ConfigureChannel` is passed a `GrpcChannelOptions` instance. For more information, see [configure client options](configuration?view=aspnetcore-10.0#configure-client-options).

Note

Some properties are set on `GrpcChannelOptions` before the `ConfigureChannel` callback is run:

* `HttpHandler` is set to the result from [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler).
* `LoggerFactory` is set to the [ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory) resolved from DI.

These values can be overridden by `ConfigureChannel`.

## Call credentials

An authentication header can be added to gRPC calls using the `AddCallCredentials` method:

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddCallCredentials((context, metadata) =>
    {
        if (!string.IsNullOrEmpty(_token))
        {
            metadata.Add("Authorization", $"Bearer {_token}");
        }
        return Task.CompletedTask;
    });
```

For more information about configuring call credentials, see [Bearer token with gRPC client factory](authn-and-authz?view=aspnetcore-10.0#bearer-token-with-grpc-client-factory).

## Deadline and cancellation propagation

gRPC clients created by the factory in a gRPC service can be configured with `EnableCallContextPropagation()` to automatically propagate the deadline and cancellation token to child calls. The `EnableCallContextPropagation()` extension method is available in the [Grpc.AspNetCore.Server.ClientFactory](https://www.nuget.org/packages/Grpc.AspNetCore.Server.ClientFactory) NuGet package.

Call context propagation works by reading the deadline and cancellation token from the current gRPC request context and automatically propagating them to outgoing calls made by the gRPC client. Call context propagation is an excellent way of ensuring that complex, nested gRPC scenarios always propagate the deadline and cancellation.

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .EnableCallContextPropagation();
```

By default, `EnableCallContextPropagation` raises an error if the client is used outside the context of a gRPC call. The error is designed to alert you that there isn't a call context to propagate. If you want to use the client outside of a call context, suppress the error when the client is configured with `SuppressContextNotFoundErrors`:

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .EnableCallContextPropagation(o => o.SuppressContextNotFoundErrors = true);
```

For more information about deadlines and RPC cancellation, see [Reliable gRPC services with deadlines and cancellation](deadlines-cancellation?view=aspnetcore-10.0).

## Named clients

Typically, a gRPC client type is registered once and then injected directly into a type's constructor by DI. However, there are scenarios where it's useful to have multiple configurations for one client. For example, a client that makes gRPC calls with and without authentication.

Multiple clients with the same type can be registered by giving each client a name. Each named client can have its own configuration. The generic `AddGrpcClient` extension method has an overload that includes a name parameter:

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>("Greeter", o =>
    {
        o.Address = new Uri("https://localhost:5001");
    });

builder.Services
    .AddGrpcClient<Greeter.GreeterClient>("GreeterAuthenticated", o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigureChannel(o =>
    {
        o.Credentials = new CustomCredentials();
    });
```

The preceding code:

* Registers the `GreeterClient` type twice, specifying a unique name for each.
* Configures different settings for each named client. The `GreeterAuthenticated` registration adds credentials to the channel so that gRPC calls made with it are authenticated.

A named gRPC client is created in app code using `GrpcClientFactory`. The type and name of the desired client is specified using the generic `GrpcClientFactory.CreateClient` method:

```
public class AggregatorService : Aggregator.AggregatorBase
{
    private readonly Greeter.GreeterClient _client;

    public AggregatorService(GrpcClientFactory grpcClientFactory)
    {
        _client = grpcClientFactory.CreateClient<Greeter.GreeterClient>("GreeterAuthenticated");
    }
}
```

## Additional resources

* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)
* [Reliable gRPC services with deadlines and cancellation](deadlines-cancellation?view=aspnetcore-10.0)
* [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0)

gRPC integration with `HttpClientFactory` offers a centralized way to create gRPC clients. It can be used as an alternative to [configuring stand-alone gRPC client instances](client?view=aspnetcore-10.0). Factory integration is available in the [Grpc.Net.ClientFactory](https://www.nuget.org/packages/Grpc.Net.ClientFactory) NuGet package.

The factory offers the following benefits:

* Provides a central location for configuring logical gRPC client instances
* Manages the lifetime of the underlying `HttpClientMessageHandler`
* Automatic propagation of deadline and cancellation in an ASP.NET Core gRPC service

## Register gRPC clients

To register a gRPC client, the generic `AddGrpcClient` extension method can be used within `Startup.ConfigureServices`, specifying the gRPC typed client class and service address:

```
services.AddGrpcClient<Greeter.GreeterClient>(o =>
{
    o.Address = new Uri("https://localhost:5001");
});
```

The gRPC client type is registered as transient with dependency injection (DI). The client can now be injected and consumed directly in types created by DI. ASP.NET Core MVC controllers, SignalR hubs and gRPC services are places where gRPC clients can automatically be injected:

```
public class AggregatorService : Aggregator.AggregatorBase
{
    private readonly Greeter.GreeterClient _client;

    public AggregatorService(Greeter.GreeterClient client)
    {
        _client = client;
    }

    public override async Task SayHellos(HelloRequest request,
        IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
    {
        // Forward the call on to the greeter service
        using (var call = _client.SayHellos(request))
        {
            await foreach (var response in call.ResponseStream.ReadAllAsync())
            {
                await responseStream.WriteAsync(response);
            }
        }
    }
}
```

## Configure HttpHandler

`HttpClientFactory` creates the `HttpMessageHandler` used by the gRPC client. Standard `HttpClientFactory` methods can be used to add outgoing request middleware or to configure the underlying `HttpClientHandler` of the `HttpClient`:

```
services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        var handler = new HttpClientHandler();
        handler.ClientCertificates.Add(LoadCertificate());
        return handler;
    });
```

For more information, see [Make HTTP requests using IHttpClientFactory](../fundamentals/http-requests?view=aspnetcore-10.0).

## Configure Interceptors

gRPC interceptors can be added to clients using the `AddInterceptor` method.

```
services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddInterceptor<LoggingInterceptor>();
```

The preceding code:

* Registers the `GreeterClient` type.
* Configures a `LoggingInterceptor` for this client. `LoggingInterceptor` is created once and shared between `GreeterClient` instances.

By default, an interceptor is created once and shared between clients. This behavior can be overridden by specifying a scope when registering an interceptor. The client factory can be configured to create a new interceptor for each client by specifying `InterceptorScope.Client`.

```
services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddInterceptor<LoggingInterceptor>(InterceptorScope.Client);
```

Creating client scoped interceptors is useful when an interceptor requires [scoped or transient scoped services from DI](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes).

A gRPC interceptor or channel credentials can be used to send `Authorization` metadata with each request. For more information about configuring authentication, see [Send a bearer token with gRPC client factory](authn-and-authz?view=aspnetcore-10.0#bearer-token-with-grpc-client-factory).

## Configure Channel

Additional configuration can be applied to a channel using the `ConfigureChannel` method:

```
services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigureChannel(o =>
    {
        o.Credentials = new CustomCredentials();
    });
```

`ConfigureChannel` is passed a `GrpcChannelOptions` instance. For more information, see [configure client options](configuration?view=aspnetcore-10.0#configure-client-options).

Note

Some properties are set on `GrpcChannelOptions` before the `ConfigureChannel` callback is run:

* `HttpHandler` is set to the result from [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler).
* `LoggerFactory` is set to the [ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory) resolved from DI.

These values can be overridden by `ConfigureChannel`.

## Deadline and cancellation propagation

gRPC clients created by the factory in a gRPC service can be configured with `EnableCallContextPropagation()` to automatically propagate the deadline and cancellation token to child calls. The `EnableCallContextPropagation()` extension method is available in the [Grpc.AspNetCore.Server.ClientFactory](https://www.nuget.org/packages/Grpc.AspNetCore.Server.ClientFactory) NuGet package.

Call context propagation works by reading the deadline and cancellation token from the current gRPC request context and automatically propagating them to outgoing calls made by the gRPC client. Call context propagation is an excellent way of ensuring that complex, nested gRPC scenarios always propagate the deadline and cancellation.

```
services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .EnableCallContextPropagation();
```

By default, `EnableCallContextPropagation` raises an error if the client is used outside the context of a gRPC call. The error is designed to alert you that there isn't a call context to propagate. If you want to use the client outside of a call context, suppress the error when the client is configured with `SuppressContextNotFoundErrors`:

```
services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .EnableCallContextPropagation(o => o.SuppressContextNotFoundErrors = true);
```

For more information about deadlines and RPC cancellation, see [Reliable gRPC services with deadlines and cancellation](deadlines-cancellation?view=aspnetcore-10.0).

## Named clients

Typically, a gRPC client type is registered once and then injected directly into a type's constructor by DI. However, there are scenarios where it is useful to have multiple configurations for one client. For example, a client that makes gRPC calls with and without authentication.

Multiple clients with the same type can be registered by giving each client a name. Each named client can have its own configuration. The generic `AddGrpcClient` extension method has an overload that includes a name parameter:

```
services
    .AddGrpcClient<Greeter.GreeterClient>("Greeter", o =>
    {
        o.Address = new Uri("https://localhost:5001");
    });

services
    .AddGrpcClient<Greeter.GreeterClient>("GreeterAuthenticated", o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigureChannel(o =>
    {
        o.Credentials = new CustomCredentials();
    });
```

The preceding code:

* Registers the `GreeterClient` type twice, specifying a unique name for each.
* Configures different settings for each named client. The `GreeterAuthenticated` registration adds credentials to the channel so that gRPC calls made with it are authenticated.

A named gRPC client is created in app code using `GrpcClientFactory`. The type and name of the desired client is specified using the generic `GrpcClientFactory.CreateClient` method:

```
public class AggregatorService : Aggregator.AggregatorBase
{
    private readonly Greeter.GreeterClient _client;

    public AggregatorService(GrpcClientFactory grpcClientFactory)
    {
        _client = grpcClientFactory.CreateClient<Greeter.GreeterClient>("GreeterAuthenticated");
    }
}
```

## Additional resources

* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)
* [Reliable gRPC services with deadlines and cancellation](deadlines-cancellation?view=aspnetcore-10.0)
* [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/templated-components
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/templated-components.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor templated components

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Robert Haken](https://havit.blazor.eu)

This article explains how templated components can accept one or more UI templates as parameters, which can then be used as part of the component's rendering logic.

## Templated components

Templated components are components that receive one or more UI templates as parameters, which can be utilized in the rendering logic of the component. By using templated components, you can create higher-level components that are more reusable. Examples include:

* A table component that allows a user to specify templates for the table's header, rows, and footer.
* A list component that allows a user to specify a template for rendering items in a list.
* A navigation bar component that allows a user to specify a template for start content and navigation links.

A templated component is defined by specifying one or more component parameters of type [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) or [RenderFragment\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment-1). A render fragment represents a segment of UI to render. [RenderFragment\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment-1) takes a type parameter that can be specified when the render fragment is invoked.

Note

For more information on [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), see [ASP.NET Core Razor components](./?view=aspnetcore-10.0#child-content-render-fragments).

Often, templated components are generically typed, as the following `TemplatedNavBar` component (`TemplatedNavBar.razor`) demonstrates. The generic type (`<T>`) in the following example is used to render [IReadOnlyList\<T>](/en-us/dotnet/api/system.collections.generic.ireadonlylist-1) values, which in this case is a list of pets for a component that displays a navigation bar with links to a pet detail component.

`TemplatedNavBar.razor`:

```
@typeparam TItem

<nav class="navbar navbar-expand navbar-light bg-light">
    <div class="container justify-content-start">
        @StartContent
        <div class="navbar-nav">
            @foreach (var item in Items)
            {
                @ItemTemplate(item)
            }
        </div>
    </div>
</nav>

@code {
    [Parameter]
    public RenderFragment? StartContent { get; set; }

    [Parameter, EditorRequired]
    public RenderFragment<TItem> ItemTemplate { get; set; } = default!;

    [Parameter, EditorRequired]
    public IReadOnlyList<TItem> Items { get; set; } = default!;
}
```

```
@typeparam TItem

<nav class="navbar navbar-expand navbar-light bg-light">
    <div class="container justify-content-start">
        @StartContent
        <div class="navbar-nav">
            @foreach (var item in Items)
            {
                @ItemTemplate(item)
            }
        </div>
    </div>
</nav>

@code {
    [Parameter]
    public RenderFragment? StartContent { get; set; }

    [Parameter, EditorRequired]
    public RenderFragment<TItem> ItemTemplate { get; set; } = default!;

    [Parameter, EditorRequired]
    public IReadOnlyList<TItem> Items { get; set; } = default!;
}
```

```
@typeparam TItem

<nav class="navbar navbar-expand navbar-light bg-light">
    <div class="container justify-content-start">
        @StartContent
        <div class="navbar-nav">
            @foreach (var item in Items)
            {
                @ItemTemplate(item)
            }
        </div>
    </div>
</nav>

@code {
    [Parameter]
    public RenderFragment? StartContent { get; set; }

    [Parameter, EditorRequired]
    public RenderFragment<TItem> ItemTemplate { get; set; } = default!;

    [Parameter, EditorRequired]
    public IReadOnlyList<TItem> Items { get; set; } = default!;
}
```

```
@typeparam TItem

<nav class="navbar navbar-expand navbar-light bg-light">
    <div class="container justify-content-start">
        @StartContent
        <div class="navbar-nav">
            @foreach (var item in Items)
            {
                @ItemTemplate(item)
            }
        </div>
    </div>
</nav>

@code {
    [Parameter]
    public RenderFragment? StartContent { get; set; }

    [Parameter, EditorRequired]
    public RenderFragment<TItem> ItemTemplate { get; set; } = default!;

    [Parameter, EditorRequired]
    public IReadOnlyList<TItem> Items { get; set; } = default!;
}
```

```
@typeparam TItem

<nav class="navbar navbar-expand navbar-light bg-light">
    <div class="container justify-content-start">
        @StartContent
        <div class="navbar-nav">
            @foreach (var item in Items)
            {
                @ItemTemplate(item)
            }
        </div>
    </div>
</nav>

@code {
    [Parameter]
    public RenderFragment StartContent { get; set; }

    [Parameter]
    public RenderFragment<TItem> ItemTemplate { get; set; } = default;

    [Parameter]
    public IReadOnlyList<TItem> Items { get; set; } = default;
}
```

```
@typeparam TItem

<nav class="navbar navbar-expand navbar-light bg-light">
    <div class="container justify-content-start">
        @StartContent
        <div class="navbar-nav">
            @foreach (var item in Items)
            {
                @ItemTemplate(item)
            }
        </div>
    </div>
</nav>

@code {
    [Parameter]
    public RenderFragment StartContent { get; set; }

    [Parameter]
    public RenderFragment<TItem> ItemTemplate { get; set; } = default;

    [Parameter]
    public IReadOnlyList<TItem> Items { get; set; } = default;
}
```

When using a templated component, the template parameters can be specified using child elements that match the names of the parameters. In the following example, `<StartContent>...</StartContent>` and `<ItemTemplate>...</ItemTemplate>` supply [RenderFragment\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment-1) templates for `StartContent` and `ItemTemplate` of the `TemplatedNavBar` component.

Specify the `Context` attribute on the component element when you want to specify the content parameter name for implicit child content (without any wrapping child element). In the following example, the `Context` attribute appears on the `TemplatedNavBar` element and applies to all [RenderFragment\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment-1) template parameters.

`Pets1.razor`:

```
@page "/pets-1"

<PageTitle>Pets 1</PageTitle>

<h1>Pets Example 1</h1>

<TemplatedNavBar Items="pets" Context="pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}?ReturnUrl=%2Fpets-1")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-1"

<PageTitle>Pets 1</PageTitle>

<h1>Pets Example 1</h1>

<TemplatedNavBar Items="pets" Context="pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}?ReturnUrl=%2Fpets-1")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-1"

<PageTitle>Pets 1</PageTitle>

<h1>Pets Example 1</h1>

<TemplatedNavBar Items="pets" Context="pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}?ReturnUrl=%2Fpets-1")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-1"

<PageTitle>Pets 1</PageTitle>

<h1>Pets Example 1</h1>

<TemplatedNavBar Items="pets" Context="pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-1"

<h1>Pets Example 1</h1>

<TemplatedNavBar Items="pets" Context="pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

```
@page "/pets-1"

<h1>Pets Example 1</h1>

<TemplatedNavBar Items="pets" Context="pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

Alternatively, you can change the parameter name using the `Context` attribute on the [RenderFragment\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment-1) child element. In the following example, the `Context` is set on `ItemTemplate` rather than `TemplatedNavBar`.

`Pets2.razor`:

```
@page "/pets-2"

<PageTitle>Pets 2</PageTitle>

<h1>Pets Example 2</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate Context="pet">
        <NavLink href="@($"/pet-detail/{pet.PetId}?ReturnUrl=%2Fpets-2")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-2"

<PageTitle>Pets 2</PageTitle>

<h1>Pets Example 2</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate Context="pet">
        <NavLink href="@($"/pet-detail/{pet.PetId}?ReturnUrl=%2Fpets-2")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-2"

<PageTitle>Pets 2</PageTitle>

<h1>Pets Example 2</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate Context="pet">
        <NavLink href="@($"/pet-detail/{pet.PetId}?ReturnUrl=%2Fpets-2")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-2"

<PageTitle>Pets 2</PageTitle>

<h1>Pets Example 2</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate Context="pet">
        <NavLink href="@($"/pet-detail/{pet.PetId}")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-2"

<h1>Pets Example 2</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate Context="pet">
        <NavLink href="@($"/pet-detail/{pet.PetId}")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

```
@page "/pets-2"

<h1>Pets Example 2</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate Context="pet">
        <NavLink href="@($"/pet-detail/{pet.PetId}")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

Component arguments of type [RenderFragment\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment-1) have an implicit parameter named `context`, which can be used. In the following example, `Context` isn't set. `@context.{PROPERTY}` supplies pet values to the template, where `{PROPERTY}` is a `Pet` property:

`Pets3.razor`:

```
@page "/pets-3"

<PageTitle>Pets 3</PageTitle>

<h1>Pets Example 3</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{context.PetId}?ReturnUrl=%2Fpets-3")" class="nav-link">
            @context.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-3"

<PageTitle>Pets 3</PageTitle>

<h1>Pets Example 3</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{context.PetId}?ReturnUrl=%2Fpets-3")" class="nav-link">
            @context.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-3"

<PageTitle>Pets 3</PageTitle>

<h1>Pets Example 3</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{context.PetId}?ReturnUrl=%2Fpets-3")" class="nav-link">
            @context.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-3"

<PageTitle>Pets 3</PageTitle>

<h1>Pets Example 3</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{context.PetId}")" class="nav-link">
            @context.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-3"

<h1>Pets Example 3</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{context.PetId}")" class="nav-link">
            @context.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

```
@page "/pets-3"

<h1>Pets Example 3</h1>

<TemplatedNavBar Items="pets">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{context.PetId}")" class="nav-link">
            @context.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

When using generic-typed components, the type parameter is inferred if possible. However, you can explicitly specify the type with an attribute that has a name matching the type parameter, which is `TItem` in the preceding example:

`Pets4.razor`:

```
@page "/pets-4"

<PageTitle>Pets 4</PageTitle>

<h1>Pets Example 4</h1>

<TemplatedNavBar Items="pets" Context="pet" TItem="Pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}?ReturnUrl=%2Fpets-4")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 4, Name = "Salem Saberhagen" },
        new Pet { PetId = 7, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-4"

<PageTitle>Pets 4</PageTitle>

<h1>Pets Example 4</h1>

<TemplatedNavBar Items="pets" Context="pet" TItem="Pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}?ReturnUrl=%2Fpets-4")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 4, Name = "Salem Saberhagen" },
        new Pet { PetId = 7, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-4"

<PageTitle>Pets 4</PageTitle>

<h1>Pets Example 4</h1>

<TemplatedNavBar Items="pets" Context="pet" TItem="Pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}?ReturnUrl=%2Fpets-4")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 4, Name = "Salem Saberhagen" },
        new Pet { PetId = 7, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-4"

<PageTitle>Pets 4</PageTitle>

<h1>Pets Example 4</h1>

<TemplatedNavBar Items="pets" Context="pet" TItem="Pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 4, Name = "Salem Saberhagen" },
        new Pet { PetId = 7, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-4"

<h1>Pets Example 4</h1>

<TemplatedNavBar Items="pets" Context="pet" TItem="Pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 4, Name = "Salem Saberhagen" },
        new Pet { PetId = 7, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

```
@page "/pets-4"

<h1>Pets Example 4</h1>

<TemplatedNavBar Items="pets" Context="pet" TItem="Pet">
    <StartContent>
        <a href="/" class="navbar-brand">PetsApp</a>
    </StartContent>
    <ItemTemplate>
        <NavLink href="@($"/pet-detail/{pet.PetId}")" class="nav-link">
            @pet.Name
        </NavLink>
    </ItemTemplate>
</TemplatedNavBar>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 4, Name = "Salem Saberhagen" },
        new Pet { PetId = 7, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

The example provided in the `TemplatedNavBar` component (`TemplatedNavBar.razor`) assumes that the `Items` collection doesn't change after the initial render; or that if it does change, maintaining the state of components/elements used in `ItemTemplate` isn't necessary. For templated components where such usage can't be anticipated, see the [Preserve relationships with `@key`](#preserve-relationships-with-key) section.

## Preserve relationships with `@key`

Templated components are often used to render collections of items, such as tables or lists. In such general scenarios, we can't assume that the user will avoid stateful components/elements in the item template definition or that there won't be additional changes to the `Items` collection. For such templated components, it's necessary to preserve the relationships with the `@key` directive attribute.

Note

For more information on the `@key` directive attribute, see [Retain element, component, and model relationships in ASP.NET Core Blazor](element-component-model-relationships?view=aspnetcore-10.0).

The following `TableTemplate` component (`TableTemplate.razor`) demonstrates a templated component that preserves relationships with `@key`.

`TableTemplate.razor`:

```
@typeparam TItem

<table class="table">
    <thead>
        <tr>@TableHeader</tr>
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            <tr @key="@item">@RowTemplate(item)</tr>
        }
    </tbody>
</table>

@code {
    [Parameter]
    public RenderFragment? TableHeader { get; set; }

    [Parameter, EditorRequired]
    public RenderFragment<TItem> RowTemplate { get; set; } = default!;

    [Parameter, EditorRequired]
    public IReadOnlyList<TItem> Items { get; set; } = default!;
}
```

```
@typeparam TItem

<table class="table">
    <thead>
        <tr>@TableHeader</tr>
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            <tr @key="@item">@RowTemplate(item)</tr>
        }
    </tbody>
</table>

@code {
    [Parameter]
    public RenderFragment? TableHeader { get; set; }

    [Parameter, EditorRequired]
    public RenderFragment<TItem> RowTemplate { get; set; } = default!;

    [Parameter, EditorRequired]
    public IReadOnlyList<TItem> Items { get; set; } = default!;
}
```

```
@typeparam TItem

<table class="table">
    <thead>
        <tr>@TableHeader</tr>
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            <tr @key="@item">@RowTemplate(item)</tr>
        }
    </tbody>
</table>

@code {
    [Parameter]
    public RenderFragment? TableHeader { get; set; }

    [Parameter, EditorRequired]
    public RenderFragment<TItem> RowTemplate { get; set; } = default!;

    [Parameter, EditorRequired]
    public IReadOnlyList<TItem> Items { get; set; } = default!;
}
```

```
@typeparam TItem

<table class="table">
    <thead>
        <tr>@TableHeader</tr>
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            <tr @key="@item">@RowTemplate(item)</tr>
        }
    </tbody>
</table>

@code {
    [Parameter]
    public RenderFragment? TableHeader { get; set; }

    [Parameter, EditorRequired]
    public RenderFragment<TItem> RowTemplate { get; set; } = default!;

    [Parameter, EditorRequired]
    public IReadOnlyList<TItem> Items { get; set; } = default!;
}
```

```
@typeparam TItem

<table class="table">
    <thead>
        <tr>@TableHeader</tr>
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            <tr @key="@item">@RowTemplate(item)</tr>
        }
    </tbody>
</table>

@code {
    [Parameter]
    public RenderFragment TableHeader { get; set; }

    [Parameter]
    public RenderFragment<TItem> RowTemplate { get; set; } = default;

    [Parameter]
    public IReadOnlyList<TItem> Items { get; set; } = default;
}
```

```
@typeparam TItem

<table class="table">
    <thead>
        <tr>@TableHeader</tr>
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            <tr @key="@item">@RowTemplate(item)</tr>
        }
    </tbody>
</table>

@code {
    [Parameter]
    public RenderFragment TableHeader { get; set; }

    [Parameter]
    public RenderFragment<TItem> RowTemplate { get; set; } = default;

    [Parameter]
    public IReadOnlyList<TItem> Items { get; set; } = default;
}
```

Consider the following `Pets5` component (`Pets5.razor`), which demonstrates the importance of keying data to preserve model relationships. In the component, each iteration of adding a pet in [`OnAfterRenderAsync`](lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync) results in Blazor rerendering the `TableTemplate` component.

`Pets5.razor`:

```
@page "/pets-5"

<PageTitle>Pets 5</PageTitle>

<h1>Pets Example 5</h1>

<TableTemplate Items="pets" Context="pet">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate>
        <td><input value="@pet.PetId" /></td>
        <td><input value="@pet.Name" /></td>
    </RowTemplate>
</TableTemplate>


@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // Insert new pet every 5 seconds
        if (pets.Count < 10)
        {
            await Task.Delay(5000);
            pets.Insert(0, new Pet() { PetId = pets.Count + 1, Name = "<new pet>" });
            StateHasChanged();
        }
    }

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-5"

<PageTitle>Pets 5</PageTitle>

<h1>Pets Example 5</h1>

<TableTemplate Items="pets" Context="pet">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate>
        <td><input value="@pet.PetId" /></td>
        <td><input value="@pet.Name" /></td>
    </RowTemplate>
</TableTemplate>


@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // Insert new pet every 5 seconds
        if (pets.Count < 10)
        {
            await Task.Delay(5000);
            pets.Insert(0, new Pet() { PetId = pets.Count + 1, Name = "<new pet>" });
            StateHasChanged();
        }
    }

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-5"

<PageTitle>Pets 5</PageTitle>

<h1>Pets Example 5</h1>

<TableTemplate Items="pets" Context="pet">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate>
        <td><input value="@pet.PetId" /></td>
        <td><input value="@pet.Name" /></td>
    </RowTemplate>
</TableTemplate>


@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // Insert new pet every 5 seconds
        if (pets.Count < 10)
        {
            await Task.Delay(5000);
            pets.Insert(0, new Pet() { PetId = pets.Count + 1, Name = "<new pet>" });
            StateHasChanged();
        }
    }

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-5"

<PageTitle>Pets 5</PageTitle>

<h1>Pets Example 5</h1>

<TableTemplate Items="pets" Context="pet">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate>
        <td><input value="@pet.PetId" /></td>
        <td><input value="@pet.Name" /></td>
    </RowTemplate>
</TableTemplate>


@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // Insert new pet every 5 seconds
        if (pets.Count < 10)
        {
            await Task.Delay(5000);
            pets.Insert(0, new Pet() { PetId = pets.Count + 1, Name = "<new pet>" });
            StateHasChanged();
        }
    }

    private class Pet
    {
        public int PetId { get; set; }
        public string? Name { get; set; }
    }
}
```

```
@page "/pets-5"

<h1>Pets Example 5</h1>

<TableTemplate Items="pets" Context="pet">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate>
        <td><input value="@pet.PetId" /></td>
        <td><input value="@pet.Name" /></td>
    </RowTemplate>
</TableTemplate>


@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // Insert new pet every 5 seconds
        if (pets.Count < 10)
        {
            await Task.Delay(5000);
            pets.Insert(0, new Pet() { PetId = pets.Count + 1, Name = "<new pet>" });
            StateHasChanged();
        }
    }

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

```
@page "/pets-5"

<h1>Pets Example 5</h1>

<TableTemplate Items="pets" Context="pet">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate>
        <td><input value="@pet.PetId" /></td>
        <td><input value="@pet.Name" /></td>
    </RowTemplate>
</TableTemplate>


@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 1, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 2, Name = "Salem Saberhagen" },
        new Pet { PetId = 3, Name = "K-9" }
    };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // Insert new pet every 5 seconds
        if (pets.Count < 10)
        {
            await Task.Delay(5000);
            pets.Insert(0, new Pet() { PetId = pets.Count + 1, Name = "<new pet>" });
            StateHasChanged();
        }
    }

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
```

This demonstration allows you to:

* Select an `<input>` from among several rendered table rows.
* Study the behavior of the page's focus as the pets collection automatically grows.

Without using the `@key` directive attribute in the `TableTemplate` component, the page's focus remains on the same index position (row) of the table, causing the focus to shift each time a pet is added. To demonstrate this, remove the `@key` directive attribute and value, restart the app, and attempt to modify a field value as items are added.

## Related content

* [ASP.NET Core Blazor rendering performance best practices](../performance/rendering?view=aspnetcore-10.0#define-reusable-renderfragments-in-code)
* [Retain element, component, and model relationships in ASP.NET Core Blazor](element-component-model-relationships?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/windows-service
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/windows-service.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host ASP.NET Core in a Windows Service

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

An ASP.NET Core app can be hosted on Windows as a [Windows Service](/en-us/dotnet/framework/windows-services/introduction-to-windows-service-applications) without using IIS. When hosted as a Windows Service, the app automatically starts after server reboots.

## Prerequisites

* [ASP.NET Core SDK 7.0 or later](https://dotnet.microsoft.com/download)
* [PowerShell 6.2 or later](https://github.com/PowerShell/PowerShell)

## Worker Service template

The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. To use the template as a basis for a Windows Service app:

1. Create a Worker Service app from the .NET template.
2. Install the NuGet package [Microsoft.Extensions.Hosting.WindowsServices](https://www.nuget.org/packages/Microsoft.Extensions.Hosting.WindowsServices).
3. Follow the guidance in the [App configuration](#app-configuration) section to update the Worker Service app so that it can run as a Windows Service.

```
dotnet new worker -o ContosoWorker
```

## App configuration

Update Program.cs to call [AddWindowsService](https://source.dot.net/#Microsoft.Extensions.Hosting.WindowsServices/WindowsServiceLifetimeHostBuilderExtensions.cs,f8bfb38e255ef3b6,references). When the app is running as a Windows Service, `AddWindowsService`:

* Sets the host lifetime to `WindowsServiceLifetime`.

* Sets the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory#system-appcontext-basedirectory). For more information, see the [Current directory and content root](#current-directory-and-content-root) section.

* Enables logging to the event log:

  * The application name is used as the default source name.
  * The default log level is *Warning* or higher for an app based on an ASP.NET Core template that calls `CreateDefaultBuilder` to build the host.
  * Override the default log level with the `Logging:EventLog:LogLevel:Default` key in `appsettings.json`/`appsettings.{Environment}.json` or other configuration provider.
  * Only administrators can create new event sources. When an event source can't be created using the application name, a warning is logged to the *Application* source and event logs are disabled.

Consider the following `ServiceA` class:

```
namespace SampleApp.Services;

public class ServiceA : BackgroundService
{
    public ServiceA(ILoggerFactory loggerFactory)
    {
        Logger = loggerFactory.CreateLogger<ServiceA>();
    }

    public ILogger Logger { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Logger.LogInformation("ServiceA is starting.");

        stoppingToken.Register(() => Logger.LogInformation("ServiceA is stopping."));

        while (!stoppingToken.IsCancellationRequested)
        {
            Logger.LogInformation("ServiceA is doing background work.");

            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }

        Logger.LogInformation("ServiceA has stopped.");
    }
}
```

The following `Program.cs` calls [`AddHostedService`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionhostedserviceextensions.addhostedservice) to register `ServiceA`:

```
using SampleApp.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddWindowsService();
builder.Services.AddHostedService<ServiceA>();

var app = builder.Build();

app.MapRazorPages();

app.Run();
```

The following sample apps accompany this article:

* Background Worker Service Sample: A non-web app sample based on the [Worker Service template](#worker-service-template) that uses [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0) for background tasks.
* Web App Service Sample: A Razor Pages web app sample that runs as a Windows Service with [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0) for background tasks.

For MVC guidance, see the articles under [Overview of ASP.NET Core MVC](../mvc/overview?view=aspnetcore-10.0) and [Migrate from ASP.NET Core 2.2 to 3.0](../migration/22-to-30?view=aspnetcore-10.0).

## Deployment type

For information and advice on deployment scenarios, see [.NET application deployment](/en-us/dotnet/core/deploying/).

### SDK

For a web app-based service that uses the Razor Pages or MVC frameworks, specify the Web SDK in the project file:

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

If the service only executes background tasks (for example, [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0)), specify the Worker SDK in the project file:

```
<Project Sdk="Microsoft.NET.Sdk.Worker">
```

### Framework-dependent deployment (FDD)

Framework-dependent deployment (FDD) relies on the presence of a shared system-wide version of .NET on the target system. When the FDD scenario is adopted following the guidance in this article, the SDK produces an executable (*.exe*), called a *framework-dependent executable*.

If using the [Web SDK](#sdk), a *web.config* file, which is normally produced when publishing an ASP.NET Core app, is unnecessary for a Windows Services app. To disable the creation of the *web.config* file, add the `<IsTransformWebConfigDisabled>` property set to `true`.

```
<PropertyGroup>
  <TargetFramework>net7.0</TargetFramework>
  <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
```

### Self-contained deployment (SCD)

Self-contained deployment (SCD) doesn't rely on the presence of a shared framework on the host system. The runtime and the app's dependencies are deployed with the app.

A Windows [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog) is included in the `<PropertyGroup>` that contains the target framework:

```
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
```

To publish for multiple RIDs:

* Provide the RIDs in a semicolon-delimited list.
* Use the property name [\<RuntimeIdentifiers>](/en-us/dotnet/core/project-sdk/msbuild-props#runtimeidentifiers) (plural).

For more information, see [.NET RID Catalog](/en-us/dotnet/core/rid-catalog).

## Service user account

To create a user account for a service, use the [New-LocalUser](/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser) cmdlet from an administrative PowerShell 6 command shell.

On Windows 10 October 2018 Update (version 1809/build 10.0.17763) or later:

```
New-LocalUser -Name {SERVICE NAME}
```

On Windows OS earlier than the Windows 10 October 2018 Update (version 1809/build 10.0.17763):

```
powershell -Command "New-LocalUser -Name {SERVICE NAME}"
```

Provide a [strong password](/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements) when prompted.

Unless the `-AccountExpires` parameter is supplied to the [New-LocalUser](/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser) cmdlet with an expiration [DateTime](/en-us/dotnet/api/system.datetime), the account doesn't expire.

For more information, see [Microsoft.PowerShell.LocalAccounts](/en-us/powershell/module/microsoft.powershell.localaccounts/) and [Service User Accounts](/en-us/windows/win32/services/service-user-accounts).

An alternative approach to managing users when using Active Directory is to use Managed Service Accounts. For more information, see [Group Managed Service Accounts Overview](/en-us/windows-server/identity/ad-ds/manage/group-managed-service-accounts/group-managed-service-accounts/group-managed-service-accounts-overview).

## Log on as a service rights

To establish *Log on as a service* rights for a service user account:

1. Open the Local Security Policy editor by running *secpol.msc*.

2. Expand the **Local Policies** node and select **User Rights Assignment**.

3. Open the **Log on as a service** policy.

4. Select **Add User or Group**.

5. Provide the object name (user account) using either of the following approaches:

   1. Type the user account (`{DOMAIN OR COMPUTER NAME\USER}`) in the object name field and select **OK** to add the user to the policy.
   2. Select **Advanced**. Select **Find Now**. Select the user account from the list. Select **OK**. Select **OK** again to add the user to the policy.

6. Select **OK** or **Apply** to accept the changes.

## Create and manage the Windows Service

### Create a service

Use PowerShell commands to register a service. From an administrative PowerShell 6 command shell, execute the following commands:

```
$acl = Get-Acl "{EXE PATH}"
$aclRuleArgs = "{DOMAIN OR COMPUTER NAME\USER}", "Read,Write,ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($aclRuleArgs)
$acl.SetAccessRule($accessRule)
$acl | Set-Acl "{EXE PATH}"

New-Service -Name {SERVICE NAME} -BinaryPathName "{EXE FILE PATH} --contentRoot {EXE FOLDER PATH}" -Credential "{DOMAIN OR COMPUTER NAME\USER}" -Description "{DESCRIPTION}" -DisplayName "{DISPLAY NAME}" -StartupType Automatic
```

* `{EXE PATH}`: Path of the app's executable on the host (for example, `d:\myservice`). Don't include the app's executable file name in the path. A trailing slash isn't required.
* `{DOMAIN OR COMPUTER NAME\USER}`: Service user account (for example, `Contoso\ServiceUser`).
* `{SERVICE NAME}`: Service name (for example, `MyService`).
* `{EXE FILE PATH}`: The app's full executable path (for example, `d:\myservice\myservice.exe`). Include the executable's file name with extension.
* `{EXE FOLDER PATH}`: The app's full executable folder path (for example `d:\myservice`).
* `{DESCRIPTION}`: Service description (for example, `My sample service`).
* `{DISPLAY NAME}`: Service display name (for example, `My Service`).

### Start a service

Start a service with the following PowerShell 6 command:

```
Start-Service -Name {SERVICE NAME}
```

The command takes a few seconds to start the service.

### Determine a service's status

To check the status of a service, use the following PowerShell 6 command:

```
Get-Service -Name {SERVICE NAME}
```

The status is reported as one of the following values:

* `Starting`
* `Running`
* `Stopping`
* `Stopped`

### Stop a service

Stop a service with the following PowerShell 6 command:

```
Stop-Service -Name {SERVICE NAME}
```

### Remove a service

After a short delay to stop a service, remove a service with the following PowerShell 6 command:

```
Remove-Service -Name {SERVICE NAME}
```

## Proxy server and load balancer scenarios

Services that interact with requests from the Internet or a corporate network and are behind a proxy or load balancer might require additional configuration. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0).

## Configure endpoints

By default, ASP.NET Core binds to `http://localhost:5000`. Configure the URL and port by setting the `ASPNETCORE_URLS` environment variable.

For additional URL and port configuration approaches, see the relevant server article:

* [Configure endpoints for Kestrel web server](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0)
* [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0#configure-windows-server)

The preceding guidance covers support for HTTPS endpoints. For example, configure the app for HTTPS when authentication is used with a Windows Service.

Note

Use of the ASP.NET Core HTTPS development certificate to secure a service endpoint isn't supported.

## Current directory and content root

The current working directory returned by calling [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) for a Windows Service is the *C:\WINDOWS\system32* folder. The *system32* folder isn't a suitable location to store a service's files (for example, settings files). Use one of the following approaches to maintain and access a service's assets and settings files.

### Use ContentRootPath or ContentRootFileProvider

Use [IHostEnvironment.ContentRootPath](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath#microsoft-extensions-hosting-ihostenvironment-contentrootpath) or [ContentRootFileProvider](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootfileprovider#microsoft-extensions-hosting-ihostenvironment-contentrootfileprovider) to locate an app's resources.

When the app runs as a service, [UseWindowsService](/en-us/dotnet/api/microsoft.extensions.hosting.windowsservicelifetimehostbuilderextensions.usewindowsservice) sets the [ContentRootPath](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath#microsoft-extensions-hosting-ihostenvironment-contentrootpath) to [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory#system-appcontext-basedirectory).

The app's default settings files, `appsettings.json` and `appsettings.{Environment}.json`, are loaded from the app's content root by calling [CreateDefaultBuilder during host construction](../fundamentals/host/generic-host?view=aspnetcore-10.0#set-up-a-host).

For other settings files loaded by developer code in [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration), there's no need to call [SetBasePath](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationextensions.setbasepath). In the following example, the `custom_settings.json` file exists in the app's content root and is loaded without explicitly setting a base path:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                config.AddJsonFile("custom_settings.json");
            })
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
            });
}
```

Don't attempt to use [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) to obtain a resource path because a Windows Service app returns the *C:\WINDOWS\system32* folder as its current directory.

### Store a service's files in a suitable location on disk

Specify an absolute path with [SetBasePath](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationextensions.setbasepath) when using an [IConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationbuilder) to the folder containing the files.

## Troubleshoot

To troubleshoot a Windows Service app, see [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0).

### Common errors

* An old or pre-release version of PowerShell is in use.

* The registered service doesn't use the app's **published** output from the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command. Output of the [dotnet build](/en-us/dotnet/core/tools/dotnet-build) command isn't supported for app deployment. Published assets are found in either of the following folders depending on the deployment type:

  * *bin/Release/{TARGET FRAMEWORK}/publish* (FDD)
  * *bin/Release/{TARGET FRAMEWORK}/{RUNTIME IDENTIFIER}/publish* (SCD)

* The service isn't in the RUNNING state.

* The paths to resources that the app uses (for example, certificates) are incorrect. The base path of a Windows Service is *c:\Windows\System32*.

* The user doesn't have *Log on as a service* rights.

* The user's password is expired or incorrectly passed when executing the `New-Service` PowerShell command.

* The app requires ASP.NET Core authentication but isn't configured for secure connections (HTTPS).

* The request URL port is incorrect or not configured correctly in the app.

### System and Application Event Logs

Access the System and Application Event Logs:

1. Open the Start menu, search for *Event Viewer*, and select the **Event Viewer** app.
2. In **Event Viewer**, open the **Windows Logs** node.
3. Select **System** to open the System Event Log. Select **Application** to open the Application Event Log.
4. Search for errors associated with the failing app.

### Run the app at a command prompt

Many startup errors don't produce useful information in the event logs. You can find the cause of some errors by running the app at a command prompt on the hosting system. To log additional detail from the app, lower the [log level](../fundamentals/logging/?view=aspnetcore-10.0#log-level) or run the app in the [`Development` environment](../fundamentals/environments?view=aspnetcore-10.0).

### Clear package caches

A functioning app might fail immediately after upgrading either the .NET SDK on the development machine or changing package versions within the app. In some cases, incoherent packages might break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:

1. Delete the *bin* and *obj* folders.

2. Clear the package caches by executing [dotnet nuget locals all --clear](/en-us/dotnet/core/tools/dotnet-nuget-locals) from a command shell.

   Clearing package caches can also be accomplished with the [nuget.exe](https://www.nuget.org/downloads) tool and executing the command `nuget locals all -clear`. *nuget.exe* isn't a bundled install with the Windows desktop operating system and must be obtained separately from the [NuGet website](https://www.nuget.org/downloads).

3. Restore and rebuild the project.

4. Delete all of the files in the deployment folder on the server prior to redeploying the app.

### Slow or unresponsive app

A *crash dump* is a snapshot of the system's memory and can help determine the cause of an app crash, startup failure, or slow app.

#### App crashes or encounters an exception

Obtain and analyze a dump from [Windows Error Reporting (WER)](/en-us/windows/win32/wer/windows-error-reporting):

1. Create a folder to hold crash dump files at `c:\dumps`.

2. Run the [EnableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/windows-service/samples/scripts/EnableDumps.ps1) with the application executable name:

   ```
   .\EnableDumps {APPLICATION EXE} c:\dumps
   ```

3. Run the app under the conditions that cause the crash to occur.

4. After the crash has occurred, run the [DisableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/windows-service/samples/scripts/DisableDumps.ps1):

   ```
   .\DisableDumps {APPLICATION EXE}
   ```

After an app crashes and dump collection is complete, the app is allowed to terminate normally. The PowerShell script configures WER to collect up to five dumps per app.

Warning

Crash dumps might take up a large amount of disk space (up to several gigabytes each).

#### App is unresponsive, fails during startup, or runs normally

When an app stops responding but doesn't crash, fails during startup, or runs normally, see [User-Mode Dump Files: Choosing the Best Tool](/en-us/windows-hardware/drivers/debugger/user-mode-dump-files#choosing-the-best-tool) to select an appropriate tool to produce the dump.

#### Analyze the dump

A dump can be analyzed using several approaches. For more information, see [Analyzing a User-Mode Dump File](/en-us/windows-hardware/drivers/debugger/analyzing-a-user-mode-dump-file).

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/windows-service/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Kestrel endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0) (includes HTTPS configuration and SNI support)
* [.NET Generic Host in ASP.NET Core](../fundamentals/host/generic-host?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)

An ASP.NET Core app can be hosted on Windows as a [Windows Service](/en-us/dotnet/framework/windows-services/introduction-to-windows-service-applications) without using IIS. When hosted as a Windows Service, the app automatically starts after server reboots.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/windows-service/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

* [ASP.NET Core SDK 6.0 or later](https://dotnet.microsoft.com/download)
* [PowerShell 6.2 or later](https://github.com/PowerShell/PowerShell)

## Worker Service template

The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. To use the template as a basis for a Windows Service app:

1. Create a Worker Service app from the .NET template.
2. Follow the guidance in the [App configuration](#app-configuration) section to update the Worker Service app so that it can run as a Windows Service.

```
dotnet new worker -o ContosoWorker
```

## App configuration

The app requires a package reference for [Microsoft.Extensions.Hosting.WindowsServices](https://www.nuget.org/packages/Microsoft.Extensions.Hosting.WindowsServices).

`IHostBuilder.UseWindowsService` is called when building the host. If the app is running as a Windows Service, the method:

* Sets the host lifetime to `WindowsServiceLifetime`.

* Sets the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory#system-appcontext-basedirectory). For more information, see the [Current directory and content root](#current-directory-and-content-root) section.

* Enables logging to the event log:

  * The application name is used as the default source name.
  * The default log level is *Warning* or higher for an app based on an ASP.NET Core template that calls `CreateDefaultBuilder` to build the host.
  * Override the default log level with the `Logging:EventLog:LogLevel:Default` key in `appsettings.json`/`appsettings.{Environment}.json` or other configuration provider.
  * Only administrators can create new event sources. When an event source can't be created using the application name, a warning is logged to the *Application* source and event logs are disabled.

In `Program.cs`:

* Set `ContentRootPath`
* Call `UseWindowsService`

```
using Microsoft.Extensions.Hosting.WindowsServices;
using SampleApp.Services;

var options = new WebApplicationOptions
{
    Args = args,
    ContentRootPath = WindowsServiceHelpers.IsWindowsService() 
                                     ? AppContext.BaseDirectory : default
};

var builder = WebApplication.CreateBuilder(options);
builder.Services.AddRazorPages();
builder.Services.AddHostedService<ServiceA>();
builder.Services.AddHostedService<ServiceB>();

builder.Host.UseWindowsService();

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();
app.MapRazorPages();
await app.RunAsync();
```

The following sample apps accompany this topic:

* Background Worker Service Sample: A non-web app sample based on the [Worker Service template](#worker-service-template) that uses [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0) for background tasks.
* Web App Service Sample: A Razor Pages web app sample that runs as a Windows Service with [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0) for background tasks.

For MVC guidance, see the articles under [Overview of ASP.NET Core MVC](../mvc/overview?view=aspnetcore-10.0) and [Migrate from ASP.NET Core 2.2 to 3.0](../migration/22-to-30?view=aspnetcore-10.0).

## Deployment type

For information and advice on deployment scenarios, see [.NET application deployment](/en-us/dotnet/core/deploying/).

### SDK

For a web app-based service that uses the Razor Pages or MVC frameworks, specify the Web SDK in the project file:

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

If the service only executes background tasks (for example, [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0)), specify the Worker SDK in the project file:

```
<Project Sdk="Microsoft.NET.Sdk.Worker">
```

### Framework-dependent deployment (FDD)

Framework-dependent deployment (FDD) relies on the presence of a shared system-wide version of .NET on the target system. When the FDD scenario is adopted following the guidance in this article, the SDK produces an executable (*.exe*), called a *framework-dependent executable*.

If using the [Web SDK](#sdk), a *web.config* file, which is normally produced when publishing an ASP.NET Core app, is unnecessary for a Windows Services app. To disable the creation of the *web.config* file, add the `<IsTransformWebConfigDisabled>` property set to `true`.

```
<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
```

### Self-contained deployment (SCD)

Self-contained deployment (SCD) doesn't rely on the presence of a shared framework on the host system. The runtime and the app's dependencies are deployed with the app.

A Windows [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog) is included in the `<PropertyGroup>` that contains the target framework:

```
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
```

To publish for multiple RIDs:

* Provide the RIDs in a semicolon-delimited list.
* Use the property name [\<RuntimeIdentifiers>](/en-us/dotnet/core/tools/csproj#runtimeidentifiers) (plural).

For more information, see [.NET RID Catalog](/en-us/dotnet/core/rid-catalog).

## Service user account

To create a user account for a service, use the [New-LocalUser](/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser) cmdlet from an administrative PowerShell 6 command shell.

On Windows 10 October 2018 Update (version 1809/build 10.0.17763) or later:

```
New-LocalUser -Name {SERVICE NAME}
```

On Windows OS earlier than the Windows 10 October 2018 Update (version 1809/build 10.0.17763):

```
powershell -Command "New-LocalUser -Name {SERVICE NAME}"
```

Provide a [strong password](/en-us/windows/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements) when prompted.

Unless the `-AccountExpires` parameter is supplied to the [New-LocalUser](/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser) cmdlet with an expiration [DateTime](/en-us/dotnet/api/system.datetime), the account doesn't expire.

For more information, see [Microsoft.PowerShell.LocalAccounts](/en-us/powershell/module/microsoft.powershell.localaccounts/) and [Service User Accounts](/en-us/windows/desktop/services/service-user-accounts).

An alternative approach to managing users when using Active Directory is to use Managed Service Accounts. For more information, see [Group Managed Service Accounts Overview](/en-us/windows-server/security/group-managed-service-accounts/group-managed-service-accounts-overview).

## Log on as a service rights

To establish *Log on as a service* rights for a service user account:

1. Open the Local Security Policy editor by running *secpol.msc*.

2. Expand the **Local Policies** node and select **User Rights Assignment**.

3. Open the **Log on as a service** policy.

4. Select **Add User or Group**.

5. Provide the object name (user account) using either of the following approaches:

   1. Type the user account (`{DOMAIN OR COMPUTER NAME\USER}`) in the object name field and select **OK** to add the user to the policy.
   2. Select **Advanced**. Select **Find Now**. Select the user account from the list. Select **OK**. Select **OK** again to add the user to the policy.

6. Select **OK** or **Apply** to accept the changes.

## Create and manage the Windows Service

### Create a service

Use PowerShell commands to register a service. From an administrative PowerShell 6 command shell, execute the following commands:

```
$acl = Get-Acl "{EXE PATH}"
$aclRuleArgs = "{DOMAIN OR COMPUTER NAME\USER}", "Read,Write,ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($aclRuleArgs)
$acl.SetAccessRule($accessRule)
$acl | Set-Acl "{EXE PATH}"

New-Service -Name {SERVICE NAME} -BinaryPathName "{EXE FILE PATH} --contentRoot {EXE FOLDER PATH}" -Credential "{DOMAIN OR COMPUTER NAME\USER}" -Description "{DESCRIPTION}" -DisplayName "{DISPLAY NAME}" -StartupType Automatic
```

* `{EXE PATH}`: Path of the app's executable on the host (for example, `d:\myservice`). Don't include the app's executable file name in the path. A trailing slash isn't required.
* `{DOMAIN OR COMPUTER NAME\USER}`: Service user account (for example, `Contoso\ServiceUser`).
* `{SERVICE NAME}`: Service name (for example, `MyService`).
* `{EXE FILE PATH}`: The app's full executable path (for example, `d:\myservice\myservice.exe`). Include the executable's file name with extension.
* `{EXE FOLDER PATH}`: The app's full executable folder path (for example `d:\myservice`).
* `{DESCRIPTION}`: Service description (for example, `My sample service`).
* `{DISPLAY NAME}`: Service display name (for example, `My Service`).

### Start a service

Start a service with the following PowerShell 6 command:

```
Start-Service -Name {SERVICE NAME}
```

The command takes a few seconds to start the service.

### Determine a service's status

To check the status of a service, use the following PowerShell 6 command:

```
Get-Service -Name {SERVICE NAME}
```

The status is reported as one of the following values:

* `Starting`
* `Running`
* `Stopping`
* `Stopped`

### Stop a service

Stop a service with the following PowerShell 6 command:

```
Stop-Service -Name {SERVICE NAME}
```

### Remove a service

After a short delay to stop a service, remove a service with the following PowerShell 6 command:

```
Remove-Service -Name {SERVICE NAME}
```

## Proxy server and load balancer scenarios

Services that interact with requests from the Internet or a corporate network and are behind a proxy or load balancer might require additional configuration. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0).

## Configure endpoints

By default, ASP.NET Core binds to `http://localhost:5000`. Configure the URL and port by setting the `ASPNETCORE_URLS` environment variable.

For additional URL and port configuration approaches, see the relevant server article:

* [Configure endpoints for Kestrel web server](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0)
* [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0#configure-windows-server)

The preceding guidance covers support for HTTPS endpoints. For example, configure the app for HTTPS when authentication is used with a Windows Service.

Note

Use of the ASP.NET Core HTTPS development certificate to secure a service endpoint isn't supported.

## Current directory and content root

The current working directory returned by calling [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) for a Windows Service is the *C:\WINDOWS\system32* folder. The *system32* folder isn't a suitable location to store a service's files (for example, settings files). Use one of the following approaches to maintain and access a service's assets and settings files.

### Use ContentRootPath or ContentRootFileProvider

Use [IHostEnvironment.ContentRootPath](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath#microsoft-extensions-hosting-ihostenvironment-contentrootpath) or [ContentRootFileProvider](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootfileprovider#microsoft-extensions-hosting-ihostenvironment-contentrootfileprovider) to locate an app's resources.

When the app runs as a service, [UseWindowsService](/en-us/dotnet/api/microsoft.extensions.hosting.windowsservicelifetimehostbuilderextensions.usewindowsservice) sets the [ContentRootPath](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath#microsoft-extensions-hosting-ihostenvironment-contentrootpath) to [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory#system-appcontext-basedirectory).

The app's default settings files, `appsettings.json` and `appsettings.{Environment}.json`, are loaded from the app's content root by calling [CreateDefaultBuilder during host construction](../fundamentals/host/generic-host?view=aspnetcore-10.0#set-up-a-host).

For other settings files loaded by developer code in [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration), there's no need to call [SetBasePath](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationextensions.setbasepath). In the following example, the `custom_settings.json` file exists in the app's content root and is loaded without explicitly setting a base path:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                config.AddJsonFile("custom_settings.json");
            })
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
            });
}
```

Don't attempt to use [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) to obtain a resource path because a Windows Service app returns the *C:\WINDOWS\system32* folder as its current directory.

### Store a service's files in a suitable location on disk

Specify an absolute path with [SetBasePath](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationextensions.setbasepath) when using an [IConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationbuilder) to the folder containing the files.

## Troubleshoot

To troubleshoot a Windows Service app, see [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0).

### Common errors

* An old or pre-release version of PowerShell is in use.

* The registered service doesn't use the app's **published** output from the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command. Output of the [dotnet build](/en-us/dotnet/core/tools/dotnet-build) command isn't supported for app deployment. Published assets are found in either of the following folders depending on the deployment type:

  * *bin/Release/{TARGET FRAMEWORK}/publish* (FDD)
  * *bin/Release/{TARGET FRAMEWORK}/{RUNTIME IDENTIFIER}/publish* (SCD)

* The service isn't in the RUNNING state.

* The paths to resources that the app uses (for example, certificates) are incorrect. The base path of a Windows Service is *c:\Windows\System32*.

* The user doesn't have *Log on as a service* rights.

* The user's password is expired or incorrectly passed when executing the `New-Service` PowerShell command.

* The app requires ASP.NET Core authentication but isn't configured for secure connections (HTTPS).

* The request URL port is incorrect or not configured correctly in the app.

### System and Application Event Logs

Access the System and Application Event Logs:

1. Open the Start menu, search for *Event Viewer*, and select the **Event Viewer** app.
2. In **Event Viewer**, open the **Windows Logs** node.
3. Select **System** to open the System Event Log. Select **Application** to open the Application Event Log.
4. Search for errors associated with the failing app.

### Run the app at a command prompt

Many startup errors don't produce useful information in the event logs. You can find the cause of some errors by running the app at a command prompt on the hosting system. To log additional detail from the app, lower the [log level](../fundamentals/logging/?view=aspnetcore-10.0#log-level) or run the app in the [`Development` environment](../fundamentals/environments?view=aspnetcore-10.0).

### Clear package caches

A functioning app may fail immediately after upgrading either the .NET SDK on the development machine or changing package versions within the app. In some cases, incoherent packages may break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:

1. Delete the *bin* and *obj* folders.

2. Clear the package caches by executing [dotnet nuget locals all --clear](/en-us/dotnet/core/tools/dotnet-nuget-locals) from a command shell.

   Clearing package caches can also be accomplished with the [nuget.exe](https://www.nuget.org/downloads) tool and executing the command `nuget locals all -clear`. *nuget.exe* isn't a bundled install with the Windows desktop operating system and must be obtained separately from the [NuGet website](https://www.nuget.org/downloads).

3. Restore and rebuild the project.

4. Delete all of the files in the deployment folder on the server prior to redeploying the app.

### Slow or unresponsive app

A *crash dump* is a snapshot of the system's memory and can help determine the cause of an app crash, startup failure, or slow app.

#### App crashes or encounters an exception

Obtain and analyze a dump from [Windows Error Reporting (WER)](/en-us/windows/desktop/wer/windows-error-reporting):

1. Create a folder to hold crash dump files at `c:\dumps`.

2. Run the [EnableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/windows-service/samples/scripts/EnableDumps.ps1) with the application executable name:

   ```
   .\EnableDumps {APPLICATION EXE} c:\dumps
   ```

3. Run the app under the conditions that cause the crash to occur.

4. After the crash has occurred, run the [DisableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/windows-service/samples/scripts/DisableDumps.ps1):

   ```
   .\DisableDumps {APPLICATION EXE}
   ```

After an app crashes and dump collection is complete, the app is allowed to terminate normally. The PowerShell script configures WER to collect up to five dumps per app.

Warning

Crash dumps might take up a large amount of disk space (up to several gigabytes each).

#### App is unresponsive, fails during startup, or runs normally

When an app stops responding but doesn't crash, fails during startup, or runs normally, see [User-Mode Dump Files: Choosing the Best Tool](/en-us/windows-hardware/drivers/debugger/user-mode-dump-files#choosing-the-best-tool) to select an appropriate tool to produce the dump.

#### Analyze the dump

A dump can be analyzed using several approaches. For more information, see [Analyzing a User-Mode Dump File](/en-us/windows-hardware/drivers/debugger/analyzing-a-user-mode-dump-file).

## Additional resources

* [Kestrel endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0) (includes HTTPS configuration and SNI support)
* [.NET Generic Host in ASP.NET Core](../fundamentals/host/generic-host?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)

An ASP.NET Core app can be hosted on Windows as a [Windows Service](/en-us/dotnet/framework/windows-services/introduction-to-windows-service-applications) without using IIS. When hosted as a Windows Service, the app automatically starts after server reboots.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/windows-service/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

* [ASP.NET Core SDK 5.0](https://dotnet.microsoft.com/download)
* [PowerShell 6.2 or later](https://github.com/PowerShell/PowerShell)

## Worker Service template

The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. To use the template as a basis for a Windows Service app:

1. Create a Worker Service app from the .NET template.
2. Follow the guidance in the [App configuration](#app-configuration) section to update the Worker Service app so that it can run as a Windows Service.

```
dotnet new worker -o ContosoWorker
```

## App configuration

The app requires a package reference for [Microsoft.Extensions.Hosting.WindowsServices](https://www.nuget.org/packages/Microsoft.Extensions.Hosting.WindowsServices).

`IHostBuilder.UseWindowsService` is called when building the host. If the app is running as a Windows Service, the method:

* Sets the host lifetime to `WindowsServiceLifetime`.

* Sets the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory#system-appcontext-basedirectory). For more information, see the [Current directory and content root](#current-directory-and-content-root) section.

* Enables logging to the event log:

  * The application name is used as the default source name.
  * The default log level is *Warning* or higher for an app based on an ASP.NET Core template that calls `CreateDefaultBuilder` to build the host.
  * Override the default log level with the `Logging:EventLog:LogLevel:Default` key in `appsettings.json`/`appsettings.{Environment}.json` or other configuration provider.
  * Only administrators can create new event sources. When an event source can't be created using the application name, a warning is logged to the *Application* source and event logs are disabled.

In `CreateHostBuilder` of `Program.cs`:

```
Host.CreateDefaultBuilder(args)
    .UseWindowsService()
    ...
```

The following sample apps accompany this topic:

* Background Worker Service Sample: A non-web app sample based on the [Worker Service template](#worker-service-template) that uses [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0) for background tasks.
* Web App Service Sample: A Razor Pages web app sample that runs as a Windows Service with [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0) for background tasks.

For MVC guidance, see the articles under [Overview of ASP.NET Core MVC](../mvc/overview?view=aspnetcore-10.0) and [Migrate from ASP.NET Core 2.2 to 3.0](../migration/22-to-30?view=aspnetcore-10.0).

## Deployment type

For information and advice on deployment scenarios, see [.NET application deployment](/en-us/dotnet/core/deploying/).

### SDK

For a web app-based service that uses the Razor Pages or MVC frameworks, specify the Web SDK in the project file:

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

If the service only executes background tasks (for example, [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0)), specify the Worker SDK in the project file:

```
<Project Sdk="Microsoft.NET.Sdk.Worker">
```

### Framework-dependent deployment (FDD)

Framework-dependent deployment (FDD) relies on the presence of a shared system-wide version of .NET on the target system. When the FDD scenario is adopted following the guidance in this article, the SDK produces an executable (*.exe*), called a *framework-dependent executable*.

If using the [Web SDK](#sdk), a *web.config* file, which is normally produced when publishing an ASP.NET Core app, is unnecessary for a Windows Services app. To disable the creation of the *web.config* file, add the `<IsTransformWebConfigDisabled>` property set to `true`.

```
<PropertyGroup>
  <TargetFramework>netcoreapp3.0</TargetFramework>
  <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
```

### Self-contained deployment (SCD)

Self-contained deployment (SCD) doesn't rely on the presence of a shared framework on the host system. The runtime and the app's dependencies are deployed with the app.

A Windows [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog) is included in the `<PropertyGroup>` that contains the target framework:

```
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
```

To publish for multiple RIDs:

* Provide the RIDs in a semicolon-delimited list.
* Use the property name [\<RuntimeIdentifiers>](/en-us/dotnet/core/tools/csproj#runtimeidentifiers) (plural).

For more information, see [.NET RID Catalog](/en-us/dotnet/core/rid-catalog).

## Service user account

To create a user account for a service, use the [New-LocalUser](/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser) cmdlet from an administrative PowerShell 6 command shell.

On Windows 10 October 2018 Update (version 1809/build 10.0.17763) or later:

```
New-LocalUser -Name {SERVICE NAME}
```

On Windows OS earlier than the Windows 10 October 2018 Update (version 1809/build 10.0.17763):

```
powershell -Command "New-LocalUser -Name {SERVICE NAME}"
```

Provide a [strong password](/en-us/windows/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements) when prompted.

Unless the `-AccountExpires` parameter is supplied to the [New-LocalUser](/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser) cmdlet with an expiration [DateTime](/en-us/dotnet/api/system.datetime), the account doesn't expire.

For more information, see [Microsoft.PowerShell.LocalAccounts](/en-us/powershell/module/microsoft.powershell.localaccounts/) and [Service User Accounts](/en-us/windows/desktop/services/service-user-accounts).

An alternative approach to managing users when using Active Directory is to use Managed Service Accounts. For more information, see [Group Managed Service Accounts Overview](/en-us/windows-server/security/group-managed-service-accounts/group-managed-service-accounts-overview).

## Log on as a service rights

To establish *Log on as a service* rights for a service user account:

1. Open the Local Security Policy editor by running *secpol.msc*.

2. Expand the **Local Policies** node and select **User Rights Assignment**.

3. Open the **Log on as a service** policy.

4. Select **Add User or Group**.

5. Provide the object name (user account) using either of the following approaches:

   1. Type the user account (`{DOMAIN OR COMPUTER NAME\USER}`) in the object name field and select **OK** to add the user to the policy.
   2. Select **Advanced**. Select **Find Now**. Select the user account from the list. Select **OK**. Select **OK** again to add the user to the policy.

6. Select **OK** or **Apply** to accept the changes.

## Create and manage the Windows Service

### Create a service

Use PowerShell commands to register a service. From an administrative PowerShell 6 command shell, execute the following commands:

```
$acl = Get-Acl "{EXE PATH}"
$aclRuleArgs = "{DOMAIN OR COMPUTER NAME\USER}", "Read,Write,ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($aclRuleArgs)
$acl.SetAccessRule($accessRule)
$acl | Set-Acl "{EXE PATH}"

New-Service -Name {SERVICE NAME} -BinaryPathName "{EXE FILE PATH}" -Credential "{DOMAIN OR COMPUTER NAME\USER}" -Description "{DESCRIPTION}" -DisplayName "{DISPLAY NAME}" -StartupType Automatic
```

* `{EXE PATH}`: Path of the app's executable on the host (for example, `d:\myservice`). Don't include the app's executable file name in the path. A trailing slash isn't required.
* `{DOMAIN OR COMPUTER NAME\USER}`: Service user account (for example, `Contoso\ServiceUser`).
* `{SERVICE NAME}`: Service name (for example, `MyService`).
* `{EXE FILE PATH}`: The app's full executable path (for example, `d:\myservice\myservice.exe`). Include the executable's file name with extension.
* `{DESCRIPTION}`: Service description (for example, `My sample service`).
* `{DISPLAY NAME}`: Service display name (for example, `My Service`).

### Start a service

Start a service with the following PowerShell 6 command:

```
Start-Service -Name {SERVICE NAME}
```

The command takes a few seconds to start the service.

### Determine a service's status

To check the status of a service, use the following PowerShell 6 command:

```
Get-Service -Name {SERVICE NAME}
```

The status is reported as one of the following values:

* `Starting`
* `Running`
* `Stopping`
* `Stopped`

### Stop a service

Stop a service with the following PowerShell 6 command:

```
Stop-Service -Name {SERVICE NAME}
```

### Remove a service

After a short delay to stop a service, remove a service with the following PowerShell 6 command:

```
Remove-Service -Name {SERVICE NAME}
```

## Proxy server and load balancer scenarios

Services that interact with requests from the Internet or a corporate network and are behind a proxy or load balancer might require additional configuration. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0).

## Configure endpoints

By default, ASP.NET Core binds to `http://localhost:5000`. Configure the URL and port by setting the `ASPNETCORE_URLS` environment variable.

For additional URL and port configuration approaches, see the relevant server article:

* [Configure endpoints for Kestrel web server](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0)
* [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0#configure-windows-server)

The preceding guidance covers support for HTTPS endpoints. For example, configure the app for HTTPS when authentication is used with a Windows Service.

Note

Use of the ASP.NET Core HTTPS development certificate to secure a service endpoint isn't supported.

## Current directory and content root

The current working directory returned by calling [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) for a Windows Service is the *C:\WINDOWS\system32* folder. The *system32* folder isn't a suitable location to store a service's files (for example, settings files). Use one of the following approaches to maintain and access a service's assets and settings files.

### Use ContentRootPath or ContentRootFileProvider

Use [IHostEnvironment.ContentRootPath](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath#microsoft-extensions-hosting-ihostenvironment-contentrootpath) or [ContentRootFileProvider](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootfileprovider#microsoft-extensions-hosting-ihostenvironment-contentrootfileprovider) to locate an app's resources.

When the app runs as a service, [UseWindowsService](/en-us/dotnet/api/microsoft.extensions.hosting.windowsservicelifetimehostbuilderextensions.usewindowsservice) sets the [ContentRootPath](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath#microsoft-extensions-hosting-ihostenvironment-contentrootpath) to [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory#system-appcontext-basedirectory).

The app's default settings files, `appsettings.json` and `appsettings.{Environment}.json`, are loaded from the app's content root by calling [CreateDefaultBuilder during host construction](../fundamentals/host/generic-host?view=aspnetcore-10.0#set-up-a-host).

For other settings files loaded by developer code in [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration), there's no need to call [SetBasePath](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationextensions.setbasepath). In the following example, the `custom_settings.json` file exists in the app's content root and is loaded without explicitly setting a base path:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                config.AddJsonFile("custom_settings.json");
            })
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
            });
}
```

Don't attempt to use [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) to obtain a resource path because a Windows Service app returns the *C:\WINDOWS\system32* folder as its current directory.

### Store a service's files in a suitable location on disk

Specify an absolute path with [SetBasePath](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationextensions.setbasepath) when using an [IConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationbuilder) to the folder containing the files.

## Troubleshoot

To troubleshoot a Windows Service app, see [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0).

### Common errors

* An old or pre-release version of PowerShell is in use.

* The registered service doesn't use the app's **published** output from the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command. Output of the [dotnet build](/en-us/dotnet/core/tools/dotnet-build) command isn't supported for app deployment. Published assets are found in either of the following folders depending on the deployment type:

  * *bin/Release/{TARGET FRAMEWORK}/publish* (FDD)
  * *bin/Release/{TARGET FRAMEWORK}/{RUNTIME IDENTIFIER}/publish* (SCD)

* The service isn't in the RUNNING state.

* The paths to resources that the app uses (for example, certificates) are incorrect. The base path of a Windows Service is *c:\Windows\System32*.

* The user doesn't have *Log on as a service* rights.

* The user's password is expired or incorrectly passed when executing the `New-Service` PowerShell command.

* The app requires ASP.NET Core authentication but isn't configured for secure connections (HTTPS).

* The request URL port is incorrect or not configured correctly in the app.

### System and Application Event Logs

Access the System and Application Event Logs:

1. Open the Start menu, search for *Event Viewer*, and select the **Event Viewer** app.
2. In **Event Viewer**, open the **Windows Logs** node.
3. Select **System** to open the System Event Log. Select **Application** to open the Application Event Log.
4. Search for errors associated with the failing app.

### Run the app at a command prompt

Many startup errors don't produce useful information in the event logs. You can find the cause of some errors by running the app at a command prompt on the hosting system. To log additional detail from the app, lower the [log level](../fundamentals/logging/?view=aspnetcore-10.0#log-level) or run the app in the [`Development` environment](../fundamentals/environments?view=aspnetcore-10.0).

### Clear package caches

A functioning app may fail immediately after upgrading either the .NET SDK on the development machine or changing package versions within the app. In some cases, incoherent packages may break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:

1. Delete the *bin* and *obj* folders.

2. Clear the package caches by executing [dotnet nuget locals all --clear](/en-us/dotnet/core/tools/dotnet-nuget-locals) from a command shell.

   Clearing package caches can also be accomplished with the [nuget.exe](https://www.nuget.org/downloads) tool and executing the command `nuget locals all -clear`. *nuget.exe* isn't a bundled install with the Windows desktop operating system and must be obtained separately from the [NuGet website](https://www.nuget.org/downloads).

3. Restore and rebuild the project.

4. Delete all of the files in the deployment folder on the server prior to redeploying the app.

### Slow or unresponsive app

A *crash dump* is a snapshot of the system's memory and can help determine the cause of an app crash, startup failure, or slow app.

#### App crashes or encounters an exception

Obtain and analyze a dump from [Windows Error Reporting (WER)](/en-us/windows/desktop/wer/windows-error-reporting):

1. Create a folder to hold crash dump files at `c:\dumps`.

2. Run the [EnableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/windows-service/samples/scripts/EnableDumps.ps1) with the application executable name:

   ```
   .\EnableDumps {APPLICATION EXE} c:\dumps
   ```

3. Run the app under the conditions that cause the crash to occur.

4. After the crash has occurred, run the [DisableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/windows-service/samples/scripts/DisableDumps.ps1):

   ```
   .\DisableDumps {APPLICATION EXE}
   ```

After an app crashes and dump collection is complete, the app is allowed to terminate normally. The PowerShell script configures WER to collect up to five dumps per app.

Warning

Crash dumps might take up a large amount of disk space (up to several gigabytes each).

#### App is unresponsive, fails during startup, or runs normally

When an app stops responding but doesn't crash, fails during startup, or runs normally, see [User-Mode Dump Files: Choosing the Best Tool](/en-us/windows-hardware/drivers/debugger/user-mode-dump-files#choosing-the-best-tool) to select an appropriate tool to produce the dump.

#### Analyze the dump

A dump can be analyzed using several approaches. For more information, see [Analyzing a User-Mode Dump File](/en-us/windows-hardware/drivers/debugger/analyzing-a-user-mode-dump-file).

## Additional resources

* [Kestrel endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0) (includes HTTPS configuration and SNI support)
* [.NET Generic Host in ASP.NET Core](../fundamentals/host/generic-host?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)

An ASP.NET Core app can be hosted on Windows as a [Windows Service](/en-us/dotnet/framework/windows-services/introduction-to-windows-service-applications) without using IIS. When hosted as a Windows Service, the app automatically starts after server reboots.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/windows-service/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

* [ASP.NET Core SDK 2.1 or later](https://dotnet.microsoft.com/download)
* [PowerShell 6.2 or later](https://github.com/PowerShell/PowerShell)

## Worker Service template

The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. To use the template as a basis for a Windows Service app:

1. Create a Worker Service app from the .NET template.
2. Follow the guidance in the [App configuration](#app-configuration) section to update the Worker Service app so that it can run as a Windows Service.

```
dotnet new worker -o ContosoWorker
```

## App configuration

The app requires a package reference for [Microsoft.Extensions.Hosting.WindowsServices](https://www.nuget.org/packages/Microsoft.Extensions.Hosting.WindowsServices).

`IHostBuilder.UseWindowsService` is called when building the host. If the app is running as a Windows Service, the method:

* Sets the host lifetime to `WindowsServiceLifetime`.

* Sets the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory#system-appcontext-basedirectory). For more information, see the [Current directory and content root](#current-directory-and-content-root) section.

* Enables logging to the event log:

  * The application name is used as the default source name.
  * The default log level is *Warning* or higher for an app based on an ASP.NET Core template that calls `CreateDefaultBuilder` to build the host.
  * Override the default log level with the `Logging:EventLog:LogLevel:Default` key in `appsettings.json`/`appsettings.{Environment}.json` or other configuration provider.
  * Only administrators can create new event sources. When an event source can't be created using the application name, a warning is logged to the *Application* source and event logs are disabled.

In `CreateHostBuilder` of `Program.cs`:

```
Host.CreateDefaultBuilder(args)
    .UseWindowsService()
    ...
```

The following sample apps accompany this topic:

* Background Worker Service Sample: A non-web app sample based on the [Worker Service template](#worker-service-template) that uses [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0) for background tasks.
* Web App Service Sample: A Razor Pages web app sample that runs as a Windows Service with [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0) for background tasks.

For MVC guidance, see the articles under [Overview of ASP.NET Core MVC](../mvc/overview?view=aspnetcore-10.0) and [Migrate from ASP.NET Core 2.2 to 3.0](../migration/22-to-30?view=aspnetcore-10.0).

## Deployment type

For information and advice on deployment scenarios, see [.NET application deployment](/en-us/dotnet/core/deploying/).

### SDK

For a web app-based service that uses the Razor Pages or MVC frameworks, specify the Web SDK in the project file:

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

If the service only executes background tasks (for example, [hosted services](../fundamentals/host/hosted-services?view=aspnetcore-10.0)), specify the Worker SDK in the project file:

```
<Project Sdk="Microsoft.NET.Sdk.Worker">
```

### Framework-dependent deployment (FDD)

Framework-dependent deployment (FDD) relies on the presence of a shared system-wide version of .NET on the target system. When the FDD scenario is adopted following the guidance in this article, the SDK produces an executable (*.exe*), called a *framework-dependent executable*.

If using the [Web SDK](#sdk), a *web.config* file, which is normally produced when publishing an ASP.NET Core app, is unnecessary for a Windows Services app. To disable the creation of the *web.config* file, add the `<IsTransformWebConfigDisabled>` property set to `true`.

```
<PropertyGroup>
  <TargetFramework>netcoreapp3.0</TargetFramework>
  <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
```

### Self-contained deployment (SCD)

Self-contained deployment (SCD) doesn't rely on the presence of a shared framework on the host system. The runtime and the app's dependencies are deployed with the app.

A Windows [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog) is included in the `<PropertyGroup>` that contains the target framework:

```
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>
```

To publish for multiple RIDs:

* Provide the RIDs in a semicolon-delimited list.
* Use the property name [\<RuntimeIdentifiers>](/en-us/dotnet/core/tools/csproj#runtimeidentifiers) (plural).

For more information, see [.NET RID Catalog](/en-us/dotnet/core/rid-catalog).

## Service user account

To create a user account for a service, use the [New-LocalUser](/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser) cmdlet from an administrative PowerShell 6 command shell.

On Windows 10 October 2018 Update (version 1809/build 10.0.17763) or later:

```
New-LocalUser -Name {SERVICE NAME}
```

On Windows OS earlier than the Windows 10 October 2018 Update (version 1809/build 10.0.17763):

```
powershell -Command "New-LocalUser -Name {SERVICE NAME}"
```

Provide a [strong password](/en-us/windows/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements) when prompted.

Unless the `-AccountExpires` parameter is supplied to the [New-LocalUser](/en-us/powershell/module/microsoft.powershell.localaccounts/new-localuser) cmdlet with an expiration [DateTime](/en-us/dotnet/api/system.datetime), the account doesn't expire.

For more information, see [Microsoft.PowerShell.LocalAccounts](/en-us/powershell/module/microsoft.powershell.localaccounts/) and [Service User Accounts](/en-us/windows/desktop/services/service-user-accounts).

An alternative approach to managing users when using Active Directory is to use Managed Service Accounts. For more information, see [Group Managed Service Accounts Overview](/en-us/windows-server/security/group-managed-service-accounts/group-managed-service-accounts-overview).

## Log on as a service rights

To establish *Log on as a service* rights for a service user account:

1. Open the Local Security Policy editor by running *secpol.msc*.

2. Expand the **Local Policies** node and select **User Rights Assignment**.

3. Open the **Log on as a service** policy.

4. Select **Add User or Group**.

5. Provide the object name (user account) using either of the following approaches:

   1. Type the user account (`{DOMAIN OR COMPUTER NAME\USER}`) in the object name field and select **OK** to add the user to the policy.
   2. Select **Advanced**. Select **Find Now**. Select the user account from the list. Select **OK**. Select **OK** again to add the user to the policy.

6. Select **OK** or **Apply** to accept the changes.

## Create and manage the Windows Service

### Create a service

Use PowerShell commands to register a service. From an administrative PowerShell 6 command shell, execute the following commands:

```
$acl = Get-Acl "{EXE PATH}"
$aclRuleArgs = "{DOMAIN OR COMPUTER NAME\USER}", "Read,Write,ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($aclRuleArgs)
$acl.SetAccessRule($accessRule)
$acl | Set-Acl "{EXE PATH}"

New-Service -Name {SERVICE NAME} -BinaryPathName "{EXE FILE PATH}" -Credential "{DOMAIN OR COMPUTER NAME\USER}" -Description "{DESCRIPTION}" -DisplayName "{DISPLAY NAME}" -StartupType Automatic
```

* `{EXE PATH}`: Path of the app's executable on the host (for example, `d:\myservice`). Don't include the app's executable file name in the path. A trailing slash isn't required.
* `{DOMAIN OR COMPUTER NAME\USER}`: Service user account (for example, `Contoso\ServiceUser`).
* `{SERVICE NAME}`: Service name (for example, `MyService`).
* `{EXE FILE PATH}`: The app's full executable path (for example, `d:\myservice\myservice.exe`). Include the executable's file name with extension.
* `{DESCRIPTION}`: Service description (for example, `My sample service`).
* `{DISPLAY NAME}`: Service display name (for example, `My Service`).

### Start a service

Start a service with the following PowerShell 6 command:

```
Start-Service -Name {SERVICE NAME}
```

The command takes a few seconds to start the service.

### Determine a service's status

To check the status of a service, use the following PowerShell 6 command:

```
Get-Service -Name {SERVICE NAME}
```

The status is reported as one of the following values:

* `Starting`
* `Running`
* `Stopping`
* `Stopped`

### Stop a service

Stop a service with the following PowerShell 6 command:

```
Stop-Service -Name {SERVICE NAME}
```

### Remove a service

After a short delay to stop a service, remove a service with the following PowerShell 6 command:

```
Remove-Service -Name {SERVICE NAME}
```

## Proxy server and load balancer scenarios

Services that interact with requests from the Internet or a corporate network and are behind a proxy or load balancer might require additional configuration. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0).

## Configure endpoints

By default, ASP.NET Core binds to `http://localhost:5000`. Configure the URL and port by setting the `ASPNETCORE_URLS` environment variable.

For additional URL and port configuration approaches, see the relevant server article:

* [Kestrel web server in ASP.NET Core](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration)
* [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0#configure-windows-server)

The preceding guidance covers support for HTTPS endpoints. For example, configure the app for HTTPS when authentication is used with a Windows Service.

Note

Use of the ASP.NET Core HTTPS development certificate to secure a service endpoint isn't supported.

## Current directory and content root

The current working directory returned by calling [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) for a Windows Service is the *C:\WINDOWS\system32* folder. The *system32* folder isn't a suitable location to store a service's files (for example, settings files). Use one of the following approaches to maintain and access a service's assets and settings files.

### Use ContentRootPath or ContentRootFileProvider

Use [IHostEnvironment.ContentRootPath](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath#microsoft-extensions-hosting-ihostenvironment-contentrootpath) or [ContentRootFileProvider](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootfileprovider#microsoft-extensions-hosting-ihostenvironment-contentrootfileprovider) to locate an app's resources.

When the app runs as a service, [UseWindowsService](/en-us/dotnet/api/microsoft.extensions.hosting.windowsservicelifetimehostbuilderextensions.usewindowsservice) sets the [ContentRootPath](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath#microsoft-extensions-hosting-ihostenvironment-contentrootpath) to [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory#system-appcontext-basedirectory).

The app's default settings files, `appsettings.json` and `appsettings.{Environment}.json`, are loaded from the app's content root by calling [CreateDefaultBuilder during host construction](../fundamentals/host/generic-host?view=aspnetcore-10.0#set-up-a-host).

For other settings files loaded by developer code in [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration), there's no need to call [SetBasePath](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationextensions.setbasepath). In the following example, the `custom_settings.json` file exists in the app's content root and is loaded without explicitly setting a base path:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                config.AddJsonFile("custom_settings.json");
            })
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
            });
}
```

Don't attempt to use [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) to obtain a resource path because a Windows Service app returns the *C:\WINDOWS\system32* folder as its current directory.

### Store a service's files in a suitable location on disk

Specify an absolute path with [SetBasePath](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationextensions.setbasepath) when using an [IConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationbuilder) to the folder containing the files.

## Troubleshoot

To troubleshoot a Windows Service app, see [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0).

### Common errors

* An old or pre-release version of PowerShell is in use.

* The registered service doesn't use the app's **published** output from the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command. Output of the [dotnet build](/en-us/dotnet/core/tools/dotnet-build) command isn't supported for app deployment. Published assets are found in either of the following folders depending on the deployment type:

  * *bin/Release/{TARGET FRAMEWORK}/publish* (FDD)
  * *bin/Release/{TARGET FRAMEWORK}/{RUNTIME IDENTIFIER}/publish* (SCD)

* The service isn't in the RUNNING state.

* The paths to resources that the app uses (for example, certificates) are incorrect. The base path of a Windows Service is *c:\Windows\System32*.

* The user doesn't have *Log on as a service* rights.

* The user's password is expired or incorrectly passed when executing the `New-Service` PowerShell command.

* The app requires ASP.NET Core authentication but isn't configured for secure connections (HTTPS).

* The request URL port is incorrect or not configured correctly in the app.

### System and Application Event Logs

Access the System and Application Event Logs:

1. Open the Start menu, search for *Event Viewer*, and select the **Event Viewer** app.
2. In **Event Viewer**, open the **Windows Logs** node.
3. Select **System** to open the System Event Log. Select **Application** to open the Application Event Log.
4. Search for errors associated with the failing app.

### Run the app at a command prompt

Many startup errors don't produce useful information in the event logs. You can find the cause of some errors by running the app at a command prompt on the hosting system. To log additional detail from the app, lower the [log level](../fundamentals/logging/?view=aspnetcore-10.0#log-level) or run the app in the [`Development` environment](../fundamentals/environments?view=aspnetcore-10.0).

### Clear package caches

A functioning app may fail immediately after upgrading either the .NET SDK on the development machine or changing package versions within the app. In some cases, incoherent packages may break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:

1. Delete the *bin* and *obj* folders.

2. Clear the package caches by executing [dotnet nuget locals all --clear](/en-us/dotnet/core/tools/dotnet-nuget-locals) from a command shell.

   Clearing package caches can also be accomplished with the [nuget.exe](https://www.nuget.org/downloads) tool and executing the command `nuget locals all -clear`. *nuget.exe* isn't a bundled install with the Windows desktop operating system and must be obtained separately from the [NuGet website](https://www.nuget.org/downloads).

3. Restore and rebuild the project.

4. Delete all of the files in the deployment folder on the server prior to redeploying the app.

### Slow or unresponsive app

A *crash dump* is a snapshot of the system's memory and can help determine the cause of an app crash, startup failure, or slow app.

#### App crashes or encounters an exception

Obtain and analyze a dump from [Windows Error Reporting (WER)](/en-us/windows/desktop/wer/windows-error-reporting):

1. Create a folder to hold crash dump files at `c:\dumps`.

2. Run the [EnableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/windows-service/samples/scripts/EnableDumps.ps1) with the application executable name:

   ```
   .\EnableDumps {APPLICATION EXE} c:\dumps
   ```

3. Run the app under the conditions that cause the crash to occur.

4. After the crash has occurred, run the [DisableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/windows-service/samples/scripts/DisableDumps.ps1):

   ```
   .\DisableDumps {APPLICATION EXE}
   ```

After an app crashes and dump collection is complete, the app is allowed to terminate normally. The PowerShell script configures WER to collect up to five dumps per app.

Warning

Crash dumps might take up a large amount of disk space (up to several gigabytes each).

#### App is unresponsive, fails during startup, or runs normally

When an app stops responding but doesn't crash, fails during startup, or runs normally, see [User-Mode Dump Files: Choosing the Best Tool](/en-us/windows-hardware/drivers/debugger/user-mode-dump-files#choosing-the-best-tool) to select an appropriate tool to produce the dump.

#### Analyze the dump

A dump can be analyzed using several approaches. For more information, see [Analyzing a User-Mode Dump File](/en-us/windows-hardware/drivers/debugger/analyzing-a-user-mode-dump-file).

## Additional resources

* [Kestrel endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration) (includes HTTPS configuration and SNI support)
* [.NET Generic Host in ASP.NET Core](../fundamentals/host/generic-host?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/interactive-server-side-rendering
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/interactive-server-side-rendering.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to mitigate security threats in interactive server-side Blazor.

Apps adopt a *stateful* data processing model, where the server and client maintain a long-lived relationship. The persistent state is maintained by a circuit, which can span connections that are also potentially long-lived.

When a user visits a site, the server creates a circuit in the server's memory. The circuit indicates to the browser what content to render and responds to events, such as when the user selects a button in the UI. To perform these actions, a circuit invokes JavaScript functions in the user's browser and .NET methods on the server. This two-way JavaScript-based interaction is referred to as [JavaScript interop (JS interop)](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0).

Because JS interop occurs over the Internet and the client uses a remote browser, apps share most web app security concerns. This topic describes common threats to server-side Blazor apps and provides threat mitigation guidance focused on Internet-facing apps.

In constrained environments, such as inside corporate networks or intranets, some of the mitigation guidance either:

* Doesn't apply in the constrained environment.
* Isn't worth the cost to implement because the security risk is low in a constrained environment.

## Interactive Server Components with WebSocket compression enabled

[Compression](../fundamentals/signalr?view=aspnetcore-10.0#websocket-compression-for-interactive-server-components) can expose the app to side-channel attacks against the TLS encryption of the connection, such as [CRIME](https://wikipedia.org/wiki/CRIME_\(security_exploit\)) and [BREACH](https://wikipedia.org/wiki/BREACH_\(security_exploit\)) attacks. These types of attacks require that the cyberattacker:

* Force a browser to issue requests with a payload the cyberattacker controls to a vulnerable site via cross-site form posting or by embedding the site inside an iframe of another site.
* Observe the length of the compressed and encrypted response over the network.

For the app to be vulnerable, it must reflect the payload from the cyberattacker in the response, for example, by writing out the path or the query string into the response. Using the length of the response, the cyberattacker can "guess" any information on the response, bypassing the encryption of the connection.

Generally speaking, Blazor apps can enable compression over the WebSocket connection with appropriate security measures:

* The app can be vulnerable when it takes content from the request (for example, the path or query string) that can be influenced by a cyberattacker and reproduces it into the HTML of the page or otherwise makes it part of the response.

* Blazor applies the following security measures automatically:

  * When compression is configured, Blazor automatically blocks embedding the app into an iframe, which blocks the initial (uncompressed) response from the server from rendering and precludes the WebSocket connection from ever starting.

  * The restriction on embedding the app into an iframe can be relaxed. However, relaxing the restriction exposes the app to attack if the embedding document becomes compromised via a cross-site scripting vulnerability, as that gives the cyberattacker a way to execute the attack.

* Normally for this type of attack to take place, the app must repeatedly reproduce the content in the responses so that the cyberattacker can guess the response. Given how Blazor renders (it renders once and then produces diffs of the content only for the elements that changed) this is hard for a cyberattacker to accomplish. However, it isn't impossible for a cyberattacker, so care must be taken to avoid rendering sensitive information alongside external information that can be manipulated by a cyberattacker. Some examples of this are:

  * Render [Personally Identifiable Information (PII)](./?view=aspnetcore-10.0#personally-identifiable-information-pii) on the page at the same time as rendering database data that was added by another user.

  * Rendering PII information on to the page at the same time as data coming from another user via JS interop or a local singleton service on the server.

In general, we recommend that you avoid rendering components that contain sensitive information alongside components that can render data from untrusted sources as part of the same render batch. Untrusted sources include route parameters, query strings, data from JS interop, and any other source of data that a third-party user can control (databases, external services).

## Shared state

Server-side Blazor apps live in server memory, and multiple app sessions are hosted within the same process. For each app session, Blazor starts a circuit with its own dependency injection container scope, thus scoped services are unique per Blazor session.

Warning

We don't recommend apps on the same server share state using singleton services unless extreme care is taken, as this can introduce security vulnerabilities, such as leaking user state across circuits.

You can use stateful singleton services in Blazor apps if they're specifically designed for it. For example, use of a singleton memory cache is acceptable because a memory cache requires a key to access a given entry. Assuming users don't have control over the cache keys that are used with the cache, state stored in the cache doesn't leak across circuits.

For general guidance on state management, see [ASP.NET Core Blazor state management overview](../state-management/?view=aspnetcore-10.0).

## `IHttpContextAccessor`/`HttpContext`

For more information, see [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../components/httpcontext?view=aspnetcore-10.0).

## Resource exhaustion

Resource exhaustion can occur when a client interacts with the server and causes the server to consume excessive resources. Excessive resource consumption primarily affects:

* [CPU](#cpu)
* [Memory](#memory)
* [Client connections](#client-connections)

Denial of Service (DoS) attacks usually seek to exhaust an app or server's resources. However, resource exhaustion isn't necessarily the result of an attack on the system. For example, finite resources can be exhausted due to high user demand. DoS is covered further in the [DoS section](#denial-of-service-dos-attacks).

Resources external to the Blazor framework, such as databases and file handles (used to read and write files), may also experience resource exhaustion. For more information, see [ASP.NET Core Best Practices](../../fundamentals/best-practices?view=aspnetcore-10.0).

### CPU

CPU exhaustion can occur when one or more clients force the server to perform intensive CPU work.

For example, consider an app that calculates a *Fibonnacci number*. A Fibonnacci number is produced from a Fibonnacci sequence, where each number in the sequence is the sum of the two preceding numbers. The amount of work required to reach the answer depends on the length of the sequence and the size of the initial value. If the app doesn't place limits on a client's request, the CPU-intensive calculations may dominate the CPU's time and diminish the performance of other tasks. Excessive resource consumption is a security concern impacting availability.

CPU exhaustion is a concern for all public-facing apps. In regular web apps, requests and connections time out as a safeguard, but Blazor apps don't provide the same safeguards. Blazor apps must include appropriate checks and limits before performing potentially CPU-intensive work.

### Memory

Memory exhaustion can occur when one or more clients force the server to consume a large amount of memory.

For example, consider an app with a component that accepts and displays a list of items. If the Blazor app doesn't place limits on the number of items allowed or the number of items rendered back to the client, the memory-intensive processing and rendering may dominate the server's memory to the point where performance of the server suffers. The server may crash or slow to the point that it appears to have crashed.

Consider the following scenario for maintaining and displaying a list of items that pertain to a potential memory exhaustion scenario on the server:

* The items in a `List<T>` property or field use the server's memory. If the app allows the list of items to grow unbounded, there's a risk of the server running out of memory. Running out of memory causes the current session to end (crash) and all of the concurrent sessions in that server instance receive an out-of-memory exception. To prevent this scenario from occurring, the app must use a data structure that imposes an item limit on concurrent users.

* If a paging scheme isn't used for rendering, the server uses additional memory for objects that aren't visible in the UI. Without a limit on the number of items, memory demands may exhaust the available server memory. To prevent this scenario, use one of the following approaches:

  * Use paginated lists when rendering.
  * Only display the first 100 to 1,000 items and require the user to enter search criteria to find items beyond the items displayed.
  * For a more advanced rendering scenario, implement lists or grids that support *virtualization*. Using virtualization, lists only render a subset of items currently visible to the user. When the user interacts with the scrollbar in the UI, the component renders only those items required for display. The items that aren't currently required for display can be held in secondary storage, which is the ideal approach. Undisplayed items can also be held in memory, which is less ideal.

Note

Blazor has built-in support for virtualization. For more information, see [ASP.NET Core Razor component virtualization](../components/virtualization?view=aspnetcore-10.0).

Blazor apps offer a similar programming model to other UI frameworks for stateful apps, such as WPF, Windows Forms, or Blazor WebAssembly. The main difference is that in several of the UI frameworks the memory consumed by the app belongs to the client and only affects that individual client. For example, a Blazor WebAssembly app runs entirely on the client and only uses client memory resources. For a server-side Blazor app, the memory consumed by the app belongs to the server and is shared among clients on the server instance.

Server-side memory demands are a consideration for all server-side Blazor apps. However, most web apps are stateless, and the memory used while processing a request is released when the response is returned. As a general recommendation, don't permit clients to allocate an unbound amount of memory as in any other server-side app that persists client connections. The memory consumed by a server-side Blazor app persists for a longer time than a single request.

Note

During development, a profiler can be used or a trace captured to assess memory demands of clients. A profiler or trace won't capture the memory allocated to a specific client. To capture the memory use of a specific client during development, capture a dump and examine the memory demand of all the objects rooted at a user's circuit.

### Client connections

Connection exhaustion can occur when one or more clients open too many concurrent connections to the server, preventing other clients from establishing new connections.

Blazor clients establish a single connection per session and keep the connection open for as long as the browser window is open. Given the persistent nature of the connections and the stateful nature of server-side Blazor apps, connection exhaustion is a greater risk to availability of the app.

There's no limit on the number of connections per user for an app. If the app requires a connection limit, take one or more of the following approaches:

* Require authentication, which naturally limits the ability of unauthorized users to connect to the app. For this scenario to be effective, users must be prevented from provisioning new users on demand.

* Limit the number of connections per user. Limiting connections can be accomplished via the following approaches. Exercise care to allow legitimate users to access the app (for example, when a connection limit is established based on the client's IP address).

  * Application level

    * Endpoint routing extensibility.
    * Require authentication to connect to the app and keep track of the active sessions per user.
    * Reject new sessions upon reaching a limit.
    * Proxy WebSocket connections to an app through the use of a proxy, such as the [Azure SignalR Service](/en-us/azure/azure-signalr/signalr-overview) that multiplexes connections from clients to an app. This provides an app with greater connection capacity than a single client can establish, preventing a client from exhausting the connections to the server.

  * Server level: Use a proxy/gateway in front of the app.

    * [Azure Application Gateway](/en-us/azure/application-gateway/overview) is a web traffic (OSI layer 7) load balancer that enables you to manage traffic to your web applications. For more information, see [Overview of WebSocket support in Application Gateway](/en-us/azure/application-gateway/application-gateway-websocket).
    * [Azure Front Door](/en-us/azure/frontdoor/front-door-overview) is a load-balancing service for distributing your workloads across multiple computing resources.

- Require authentication, which naturally limits the ability of unauthorized users to connect to the app. For this scenario to be effective, users must be prevented from provisioning new users on demand.

- Limit the number of connections per user. Limiting connections can be accomplished via the following approaches. Exercise care to allow legitimate users to access the app (for example, when a connection limit is established based on the client's IP address).

  * Application level

    * Endpoint routing extensibility.
    * Require authentication to connect to the app and keep track of the active sessions per user.
    * Reject new sessions upon reaching a limit.
    * Proxy WebSocket connections to an app through the use of a proxy, such as the [Azure SignalR Service](/en-us/azure/azure-signalr/signalr-overview) that multiplexes connections from clients to an app. This provides an app with greater connection capacity than a single client can establish, preventing a client from exhausting the connections to the server.

  * Server level

    * Use a proxy/gateway in front of the app.
    * Although Long Polling is supported for Blazor apps, [WebSockets is the recommended transport protocol](../host-and-deploy/server/?view=aspnetcore-10.0#azure-signalr-service). We recommend selecting a proxy/gateway that supports WebSockets.

## Denial of Service (DoS) attacks

[Denial of Service (DoS) attacks](https://developer.mozilla.org/docs/Glossary/DOS_attack) involve a client causing the server to exhaust one or more of its resources making the app unavailable. Blazor apps include default limits and rely on other ASP.NET Core and SignalR limits that are set on [CircuitOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions) to protect against DoS attacks:

* [CircuitOptions.DisconnectedCircuitMaxRetained](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.disconnectedcircuitmaxretained#microsoft-aspnetcore-components-server-circuitoptions-disconnectedcircuitmaxretained)
* [CircuitOptions.DisconnectedCircuitRetentionPeriod](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.disconnectedcircuitretentionperiod#microsoft-aspnetcore-components-server-circuitoptions-disconnectedcircuitretentionperiod)
* [CircuitOptions.JSInteropDefaultCallTimeout](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.jsinteropdefaultcalltimeout#microsoft-aspnetcore-components-server-circuitoptions-jsinteropdefaultcalltimeout)
* [CircuitOptions.MaxBufferedUnacknowledgedRenderBatches](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.maxbufferedunacknowledgedrenderbatches#microsoft-aspnetcore-components-server-circuitoptions-maxbufferedunacknowledgedrenderbatches)
* [HubConnectionContextOptions.MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubconnectioncontextoptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-hubconnectioncontextoptions-maximumreceivemessagesize)

For more information and configuration coding examples, see the following articles:

* [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0)
* [ASP.NET Core SignalR configuration](../../signalr/configuration?view=aspnetcore-10.0)

## Interactions with the browser (client)

A client interacts with the server through JS interop event dispatching and render completion. JS interop communication goes both ways between JavaScript and .NET:

* Browser events are dispatched from the client to the server in an asynchronous fashion.
* The server responds asynchronously rerendering the UI as necessary.

### JavaScript functions invoked from .NET

For calls from .NET methods to JavaScript:

* All invocations have a configurable timeout after which they fail, returning a [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception) to the caller.

  * There's a default timeout for the calls ([CircuitOptions.JSInteropDefaultCallTimeout](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.jsinteropdefaultcalltimeout#microsoft-aspnetcore-components-server-circuitoptions-jsinteropdefaultcalltimeout)) of one minute. To configure this limit, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#harden-javascript-interop-calls).
  * A cancellation token can be provided to control the cancellation on a per-call basis. Rely on the default call timeout where possible and time-bound any call to the client if a cancellation token is provided.

* The result of a JavaScript call can't be trusted. The Blazor app client running in the browser searches for the JavaScript function to invoke. The function is invoked, and either the result or an error is produced. A malicious client can attempt to:

  * Cause an issue in the app by returning an error from the JavaScript function.
  * Induce an unintended behavior on the server by returning an unexpected result from the JavaScript function.

Take the following precautions to protect against the preceding scenarios:

* Wrap JS interop calls within [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statements to account for errors that might occur during the invocations. For more information, see [Handle errors in ASP.NET Core Blazor apps](../fundamentals/handle-errors?view=aspnetcore-10.0#javascript-interop).
* Validate data returned from JS interop invocations, including error messages, before taking any action.

### .NET methods invoked from the browser

Don't trust calls from JavaScript to .NET methods. When a .NET method is exposed to JavaScript, consider how the .NET method is invoked:

* Treat any .NET method exposed to JavaScript as you would a public endpoint to the app.

  * Validate input.

    * Ensure that values are within expected ranges.
    * Ensure that the user has permission to perform the action requested.

  * Don't allocate an excessive quantity of resources as part of the .NET method invocation. For example, perform checks and place limits on CPU and memory use.

  * Take into account that static and instance methods can be exposed to JavaScript clients. Avoid sharing state across sessions unless the design calls for sharing state with appropriate constraints.

    * For instance methods exposed through [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) objects that are originally created through dependency injection (DI), the objects should be registered as scoped objects. This applies to any DI service that the app uses.
    * For static methods, avoid establishing state that can't be scoped to the client unless the app is explicitly sharing state by-design across all users on a server instance.

  * Avoid passing user-supplied data in parameters to JavaScript calls. If passing data in parameters is absolutely required, ensure that the JavaScript code handles passing the data without introducing [Cross-site scripting (XSS)](#cross-site-scripting-xss) vulnerabilities. For example, don't write user-supplied data to the DOM by setting the `innerHTML` property of an element. Consider using [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP) to disable `eval` and other unsafe JavaScript primitives. For more information, see [Enforce a Content Security Policy for ASP.NET Core Blazor](content-security-policy?view=aspnetcore-10.0) and [MDN's CSP guide](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP).

* Avoid implementing custom dispatching of .NET invocations on top of the framework's dispatching implementation. Exposing .NET methods to the browser is an advanced scenario, not recommended for general Blazor development.

### Events

Events provide an entry point to an app. The same rules for safeguarding endpoints in web apps apply to event handling in Blazor apps. A malicious client can send any data it wishes to send as the payload for an event.

For example:

* A change event for a `<select>` could send a value that isn't within the options that the app presented to the client.
* An `<input>` could send any text data to the server, bypassing client-side validation.

The app must validate the data for any event that the app handles. The Blazor framework [forms components](../forms/input-components?view=aspnetcore-10.0) perform basic validations. If the app uses custom forms components, custom code must be written to validate event data as appropriate.

Events are asynchronous, so multiple events can be dispatched to the server before the app has time to react by producing a new render. This has some security implications to consider. Limiting client actions in the app must be performed inside event handlers and not depend on the current rendered view state.

Consider a counter component that should allow a user to increment a counter a maximum of three times. The button to increment the counter is conditionally based on the value of `count`:

```
<p>Count: @count</p>

@if (count < 3)
{
    <button @onclick="IncrementCount" value="Increment count" />
}

@code 
{
    private int count = 0;

    private void IncrementCount()
    {
        count++;
    }
}
```

A client can dispatch one or more increment events before the framework produces a new render of this component. The result is that the `count` can be incremented *over three times* by the user because the button isn't removed by the UI quickly enough. The correct way to achieve the limit of three `count` increments is shown in the following example:

```
<p>Count: @count</p>

@if (count < 3)
{
    <button @onclick="IncrementCount" value="Increment count" />
}

@code 
{
    private int count = 0;

    private void IncrementCount()
    {
        if (count < 3)
        {
            count++;
        }
    }
}
```

By adding the `if (count < 3) { ... }` check inside the handler, the decision to increment `count` is based on the current app state. The decision isn't based on the state of the UI as it was in the previous example, which might be temporarily stale.

### Protect against multiple dispatches

If an event callback invokes a long running operation asynchronously, such as fetching data from an external service or database, consider using a safeguard. The safeguard can prevent the user from enqueueing multiple operations while the operation is in progress with visual feedback. The following component code sets `isLoading` to `true` while `DataService.GetDataAsync` obtains data from the server. While `isLoading` is `true`, the button is disabled in the UI:

```
<button disabled="@isLoading" @onclick="UpdateData">Update</button>

@code {
    private bool isLoading;
    private Data[] data = Array.Empty<Data>();

    private async Task UpdateData()
    {
        if (!isLoading)
        {
            isLoading = true;
            data = await DataService.GetDataAsync(DateTime.Now);
            isLoading = false;
        }
    }
}
```

The safeguard pattern demonstrated in the preceding example works if the background operation is executed asynchronously with the `async`-`await` pattern.

### Cancel early and avoid use-after-dispose

In addition to using a safeguard as described in the [Protect against multiple dispatches](#protect-against-multiple-dispatches) section, consider using a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) to cancel long-running operations when the component is disposed. This approach has the added benefit of avoiding *use-after-dispose* in components:

```
@implements IDisposable

...

@code {
    private readonly CancellationTokenSource TokenSource = 
        new CancellationTokenSource();

    private async Task UpdateData()
    {
        ...

        data = await DataService.GetDataAsync(DateTime.Now, TokenSource.Token);

        if (TokenSource.Token.IsCancellationRequested)
        {
           return;
        }

        ...
    }

    public void Dispose()
    {
        TokenSource.Cancel();
    }
}
```

### Avoid events that produce large amounts of data

Some DOM events, such as `oninput` or `onscroll`, can produce a large amount of data. Avoid using these events in server-side Blazor server.

## Additional security guidance

The guidance for securing ASP.NET Core apps apply to server-side Blazor apps and are covered in the following sections of this article:

* [Logging and sensitive data](#logging-and-sensitive-data)
* [Protect information in transit with HTTPS](#protect-information-in-transit-with-https)
* [Cross-site scripting (XSS)](#cross-site-scripting-xss)
* [Cross-origin protection](#cross-origin-protection)
* [Click-jacking](#click-jacking)
* [Open redirects](#open-redirects)

### Logging and sensitive data

JS interop interactions between the client and server are recorded in the server's logs with [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) instances. Blazor avoids logging sensitive information, such as actual events or JS interop inputs and outputs.

When an error occurs on the server, the framework notifies the client and tears down the session. The client receives a generic error message that can be seen in the browser's developer tools.

The client-side error doesn't include the call stack and doesn't provide detail on the cause of the error, but server logs do contain such information. For development purposes, sensitive error information can be made available to the client by [enabling detailed errors](../fundamentals/handle-errors?view=aspnetcore-10.0#detailed-circuit-errors).

Warning

Exposing error information to clients on the Internet is a security risk that should always be avoided.

### Protect information in transit with HTTPS

Blazor uses SignalR for communication between the client and the server. Blazor normally uses the transport that SignalR negotiates, which is typically WebSockets.

Blazor doesn't ensure the integrity and confidentiality of the data sent between the server and the client. Always use HTTPS.

### Cross-site scripting (XSS)

Cross-site scripting (XSS) allows an unauthorized party to execute arbitrary logic in the context of the browser. A compromised app could potentially run arbitrary code on the client. The vulnerability could be used to potentially perform a number of malicious actions against the server:

* Dispatch fake/invalid events to the server.
* Dispatch fail/invalid render completions.
* Avoid dispatching render completions.
* Dispatch interop calls from JavaScript to .NET.
* Modify the response of interop calls from .NET to JavaScript.
* Avoid dispatching .NET to JS interop results.

The Blazor framework takes steps to protect against some of the preceding threats:

* Stops producing new UI updates if the client isn't acknowledging render batches. Configured with [CircuitOptions.MaxBufferedUnacknowledgedRenderBatches](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.maxbufferedunacknowledgedrenderbatches#microsoft-aspnetcore-components-server-circuitoptions-maxbufferedunacknowledgedrenderbatches).

* Times out any .NET to JavaScript call after one minute without receiving a response from the client. Configured with [CircuitOptions.JSInteropDefaultCallTimeout](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.jsinteropdefaultcalltimeout#microsoft-aspnetcore-components-server-circuitoptions-jsinteropdefaultcalltimeout).

* Performs basic validation on all input coming from the browser during JS interop:

  * .NET references are valid and of the type expected by the .NET method.
  * The data isn't malformed.
  * The correct number of arguments for the method are present in the payload.
  * The arguments or result can be deserialized correctly before invoking the method.

* Performs basic validation in all input coming from the browser from dispatched events:

  * The event has a valid type.
  * The data for the event can be deserialized.
  * There's an event handler associated with the event.

In addition to the safeguards that the framework implements, the app must be coded by the developer to safeguard against threats and take appropriate actions:

* Always validate data when handling events.

* Take appropriate action upon receiving invalid data:

  * Ignore the data and return. This allows the app to continue processing requests.
  * If the app determines that the input is illegitimate and couldn't be produced by legitimate client, throw an exception. Throwing an exception tears down the circuit and ends the session.

* Don't trust the error message provided by render batch completions included in the logs. The error is ***provided by the client*** and can't generally be trusted, as the client might be compromised.

* Don't trust the input on JS interop calls in either direction between JavaScript and .NET methods.

* The app is responsible for validating that the content of arguments and results are valid, even if the arguments or results are correctly deserialized.

For a XSS vulnerability to exist, the app must incorporate user input in the rendered page. Blazor executes a compile-time step where the markup in a `.razor` file is transformed into procedural C# logic. At runtime, the C# logic builds a *render tree* describing the elements, text, and child components. This is applied to the browser's DOM via a sequence of JavaScript instructions (or is serialized to HTML in the case of prerendering):

* User input rendered via normal Razor syntax (for example, `@someStringValue`) doesn't expose a XSS vulnerability because the Razor syntax is added to the DOM via commands that can only write text. Even if the value includes HTML markup, the value is displayed as static text. When prerendering, the output is HTML-encoded, which also displays the content as static text.
* Component authors can author components in C# without using Razor. The component author is responsible for using the correct APIs when emitting output. For example, use `builder.AddContent(0, someUserSuppliedString)` and *not* `builder.AddMarkupContent(0, someUserSuppliedString)`, as the latter could create a XSS vulnerability.

- User input rendered via normal Razor syntax (for example, `@someStringValue`) doesn't expose a XSS vulnerability because the Razor syntax is added to the DOM via commands that can only write text. Even if the value includes HTML markup, the value is displayed as static text. When prerendering, the output is HTML-encoded, which also displays the content as static text.
- Script tags aren't allowed and shouldn't be included in the app's component render tree. If a script tag is included in a component's markup, a compile-time error is generated.
- Component authors can author components in C# without using Razor. The component author is responsible for using the correct APIs when emitting output. For example, use `builder.AddContent(0, someUserSuppliedString)` and *not* `builder.AddMarkupContent(0, someUserSuppliedString)`, as the latter could create a XSS vulnerability.

Consider further mitigating XSS vulnerabilities. For example, implement a restrictive [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP). For more information, see [Enforce a Content Security Policy for ASP.NET Core Blazor](content-security-policy?view=aspnetcore-10.0) and [MDN's CSP guide](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP).

For more information, see [Prevent Cross-Site Scripting (XSS) in ASP.NET Core](../../security/cross-site-scripting?view=aspnetcore-10.0).

### Cross-origin protection

Cross-origin attacks involve a client from a different origin performing an action against the server. The malicious action is typically a GET request or a form POST (Cross-Site Request Forgery, CSRF), but opening a malicious WebSocket is also possible. Blazor apps offer [the same guarantees that any other SignalR app using the hub protocol offer](../../signalr/security?view=aspnetcore-10.0):

* Apps can be accessed cross-origin unless additional measures are taken to prevent it. To disable cross-origin access, either disable CORS in the endpoint by adding the CORS middleware to the pipeline and adding the [DisableCorsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.cors.disablecorsattribute) to the Blazor endpoint metadata or limit the set of allowed origins by [configuring SignalR for Cross-Origin Resource Sharing](../../signalr/security?view=aspnetcore-10.0#cross-origin-resource-sharing). For guidance on WebSocket origin restrictions, see [WebSockets support in ASP.NET Core](../../fundamentals/websockets?view=aspnetcore-10.0#websocket-origin-restriction).
* If CORS is enabled, extra steps might be required to protect the app depending on the CORS configuration. If CORS is globally enabled, CORS can be disabled for the Blazor SignalR hub by adding the [DisableCorsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.cors.disablecorsattribute) metadata to the endpoint metadata after calling [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) on the endpoint route builder.

For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0).

### Click-jacking

Click-jacking involves rendering a site as an `<iframe>` inside a site from a different origin in order to trick the user into performing actions on the site under attack.

To protect an app from rendering inside of an `<iframe>`, use [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP) and the `X-Frame-Options` header. For CSP syntax, see [MDN's CSP guide](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP).

For more information, see the following resources:

* [Enforce a Content Security Policy for ASP.NET Core Blazor](content-security-policy?view=aspnetcore-10.0)
* [MDN web docs: X-Frame-Options](https://developer.mozilla.org/docs/Web/HTTP/Headers/X-Frame-Options)

### Open redirects

When an app session starts, the server performs basic validation of the URLs sent as part of starting the session. The framework checks that the base URL is a parent of the current URL before establishing the circuit. No additional checks are performed by the framework.

When a user selects a link on the client, the URL for the link is sent to the server, which determines what action to take. For example, the app may perform a client-side navigation or indicate to the browser to go to the new location.

Components can also trigger navigation requests programmatically through the use of [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager). In such scenarios, the app might perform a client-side navigation or indicate to the browser to go to the new location.

Components must:

* Avoid using user input as part of the navigation call arguments.
* Validate arguments to ensure that the target is allowed by the app.

Otherwise, a malicious user can force the browser to go to a cyberattacker-controlled site. In this scenario, the cyberattacker tricks the app into using some user input as part of the invocation of the [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) method.

This advice also applies when rendering links as part of the app:

* If possible, use relative links.
* Validate that absolute link destinations are valid before including them in a page.

For more information, see [Prevent open redirect attacks in ASP.NET Core](../../security/preventing-open-redirects?view=aspnetcore-10.0).

## Security checklist

The following list of security considerations isn't comprehensive:

* Validate arguments from events.

* Validate inputs and results from JS interop calls.

* Avoid using (or validate beforehand) user input for .NET to JS interop calls.

* Prevent the client from allocating an unbound amount of memory.

  * Data within the component.
  * [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) objects returned to the client.

* Protect against multiple dispatches.

* Cancel long-running operations when the component is disposed.

* Avoid events that produce large amounts of data.

* Avoid using user input as part of calls to [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) and validate user input for URLs against a set of allowed origins first if unavoidable.

* Don't make authorization decisions based on the state of the UI but only from component state.

* Consider using [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP) to protect against XSS attacks. For more information, see [Enforce a Content Security Policy for ASP.NET Core Blazor](content-security-policy?view=aspnetcore-10.0) and [MDN's CSP guide](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP).

* Consider using CSP and [X-Frame-Options](https://developer.mozilla.org/docs/Web/HTTP/Headers/X-Frame-Options) to protect against click-jacking.

* Ensure CORS settings are appropriate when enabling CORS or explicitly disable CORS for Blazor apps.

* Test to ensure that the server-side limits for the Blazor app provide an acceptable user experience without unacceptable levels of risk.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app/part-6
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/movie-database-app/part-6.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor movie database app (Part 6 - Add search)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article is the sixth part of the Blazor movie database app tutorial that teaches you the basics of building an ASP.NET Core Blazor Web App with features to manage a movie database.

This part of the tutorial series covers adding a search feature to the movies `Index` component to filter movies by title.

## Implement a filter feature for the `QuickGrid` component

The [`QuickGrid` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid) is used by the movie `Index` component (`Components/MoviePages/Index.razor`) to display movies from the database:

```
<QuickGrid Class="table" Items="context.Movie">
    ...
</QuickGrid>
```

The [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.items) parameter receives an `IQueryable<TGridItem>`, where `TGridItem` is the type of data represented by each row in the grid (`Movie`). [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.items) is assigned a collection of movie entities (`DbSet<Movie>`) obtained from the created database context ([CreateDbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1.createdbcontext)) of the injected database context factory (`DbFactory`).

To make the `QuickGrid` component filter on the movie title, the `Index` component should:

* Set a filter string as a *component parameter* from the query string.
* If the parameter has a value, filter the movies returned from the database.
* Provide an input for the user to provide the filter string and a button to trigger a reload using the filter.

Start by adding the following code to the `@code` block of the `Index` component (`MoviePages/Index.razor`):

```
[SupplyParameterFromQuery]
private string? TitleFilter { get; set; }

private IQueryable<Movie> FilteredMovies => 
    context.Movie.Where(m => m.Title!.Contains(TitleFilter ?? string.Empty));
```

`TitleFilter` is the filter string. The property is provided the [`[SupplyParameterFromQuery]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromqueryattribute), which lets Blazor know that the value of `TitleFilter` should be assigned from the query string when the query string contains a field of the same name (for example, `?titleFilter=road+warrior` yields a `TitleFilter` value of `road warrior`). Note that query string field names, such as `titleFilter`, aren't case sensitive.

The `FilteredMovies` property is an `IQueryable<Movie>`, which is the type for assignment to the QuickGrid's [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.items) parameter. The property filters the list of movies based on the supplied `TitleFilter`. If a `TitleFilter` isn't assigned a value from the query string (`TitleFilter` is `null`), an empty string (`string.Empty`) is used for the [Contains](/en-us/dotnet/api/system.string.contains) clause. Therefore, no movies are filtered for display.

Change the `QuickGrid` component's [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.items) parameter to use the `movies` collection:

```
- <QuickGrid Class="table" Items="context.Movie">
+ <QuickGrid Class="table" Items="FilteredMovies">
```

The `movie => movie.Title!.Contains(...)` code is a *lambda expression*. Lambdas are used in method-based LINQ queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/api/system.linq.queryable.where) or [Contains](/en-us/dotnet/api/system.string.contains) methods. LINQ queries aren't executed when they're defined or when they're modified by calling a method, such as [Where](/en-us/dotnet/api/system.linq.queryable.where), [Contains](/en-us/dotnet/api/system.string.contains), or [OrderBy](/en-us/dotnet/api/system.linq.queryable.orderby). Rather, query execution is deferred. The evaluation of an expression is delayed until its realized value is iterated.

The [Where](/en-us/dotnet/api/system.linq.queryable.where) method is run on the database, not in the C# code. The case sensitivity of the query depends on the database and the collation. For SQL Server, [Contains](/en-us/dotnet/api/system.string.contains) maps to [SQL `LIKE`](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with default collation provides a mixture of case-sensitive and case-insensitive filtering, depending on the query.

Run the app and navigate to the movies `Index` page at `/movies`. The movies in the database load:

Append a query string to the URL in the address bar: `?titleFilter=road+warrior`. For example, the full URL appears as `https://localhost:7073/movies?titleFilter=road+warrior`, assuming the port number is `7073`. The filtered movie is displayed:

Append a query string to the URL in the address bar: `?titleFilter=Road+Warrior`. For example, the full URL appears as `https://localhost:7073/movies?titleFilter=Road+Warrior`, assuming the port number is `7073`. The filtered movie is displayed:

Append a query string to the URL in the address bar: `?titleFilter=Road+Warrior`. For example, the full URL appears as `https://localhost:7073/movies?titleFilter=Road+Warrior`, assuming the port number is `7073`. The filtered movie is displayed:

Next, give users a way to provide the `titleFilter` filter string via the component's UI. Add the following HTML under the H1 heading (`<h1>Index</h1>`). The following HTML reloads the page with the contents of the textbox as a query string value:

```
<div>
    <form action="/movies" data-enhance>
        <input type="search" name="titleFilter" />
        <input type="submit" value="Search" />
    </form>
</div>
```

The `data-enhance` attribute applies *enhanced navigation* to the component, where Blazor intercepts the GET request and performs a fetch request instead. Blazor then patches the response content into the page, which avoids a full-page reload and preserves more of the page state. The page loads faster, usually without losing the user's scroll position.

Save the file that you're working on. Apply the change by either restarting the app or using [Hot Reload](/en-us/visualstudio/debugger/hot-reload) to apply the change to the running app.

Close the browser window and in VS Code select **Run** > **Restart Debugging** or press `Ctrl`+`Shift`+`F5` on the keyboard. VS Code recompiles and runs the app with your saved changes and spawns a new browser window for the app.

Because the app is currently running with `dotnet watch`, saved changes are detected automatically and reflected in the existing browser window.

Type "`road warrior`" into the search box and select the **Search** button to filter the movies:

The result after searching on `road warrior`:

Notice that the search box loses the search value ("`road warrior`") when the movies are filtered. If you want to preserve the searched value, add the `data-permanent` attribute:

Type "`Road Warrior`" into the search box and select the **Search** button to filter the movies:

The result after searching on `Road Warrior`:

Notice that the search box loses the search value ("`Road Warrior`") when the movies are filtered. If you want to preserve the searched value, add the `data-permanent` attribute:

Type "`Road Warrior`" into the search box and select the **Search** button to filter the movies:

The result after searching on `Road Warrior`:

Notice that the search box loses the search value ("`Road Warrior`") when the movies are filtered. If you want to preserve the searched value, add the `data-permanent` attribute:

```
- <form action="/movies" data-enhance>
+ <form action="/movies" data-enhance data-permanent>
```

## Stop the app

Stop the app by closing the browser's window.

Stop the app by closing the browser's window and pressing `Shift`+`F5` on the keyboard in VS Code.

Stop the app by closing the browser's window and pressing `Ctrl`+`C` in the command shell.

## Troubleshoot with the completed sample

If you run into a problem while following the tutorial that you can't resolve from the text, compare your code to the completed project in the Blazor samples repository:

[Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)

Select the latest version folder. The sample folder for this tutorial's project is named `BlazorWebAppMovies`.

## Related content

* [LINQ documentation](/en-us/dotnet/csharp/programming-guide/concepts/linq/)
* [Write C# LINQ queries to query data (C# documentation)](/en-us/dotnet/csharp/programming-guide/concepts/linq/query-syntax-and-method-syntax-in-linq)
* [Lambda Expression (C# documentation](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions)

## Legal

[*Mad Max*, *The Road Warrior*, *Mad Max: Beyond Thunderdome*, *Mad Max: Fury Road*, and *Furiosa: A Mad Max Saga*](https://warnerbros.fandom.com/wiki/Mad_Max_\(franchise\)) are trademarks and copyrights of [Warner Bros. Entertainment](https://www.warnerbros.com/).

## Next steps

[Previous: Add validation](part-5?view=aspnetcore-10.0) [Next: Add a new field](part-7?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/samesite/rp31
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/samesite/rp31.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core 3.1 Razor Pages SameSite cookie sample

ASP.NET Core 3.0 has built-in support for the [SameSite](https://owasp.org/www-community/SameSite) attribute, including a `SameSiteMode` attribute value of `Unspecified` to suppress writing the attribute.

ASP.NET Core [Identity](../authentication/identity?view=aspnetcore-3.1) is largely unaffected by [SameSite cookies](../samesite?view=aspnetcore-3.1) except for advanced scenarios like `IFrames` or `OpenIdConnect` integration.

When using `Identity`, do ***not*** add any cookie providers or call `services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)`, `Identity` takes care of that.

## Writing the SameSite attribute

Following is an example of how to write a SameSite attribute on a cookie;

```
var cookieOptions = new CookieOptions
{
    // Set the secure flag, which Chrome's changes will require for SameSite none.
    // Note this will also require you to be running on HTTPS
    Secure = true,

    // Set the cookie to HTTP only which is good practice unless you really do need
    // to access it client side in scripts.
    HttpOnly = true,

    // Add the SameSite attribute, this will emit the attribute with a value of none.
    // To not emit the attribute at all set the SameSite property to SameSiteMode.Unspecified.
    SameSite = SameSiteMode.None
};

// Add the cookie to the response cookie collection
Response.Cookies.Append(CookieName, "cookieValue", cookieOptions);
```

## Setting Cookie Authentication and Session State cookies

Cookie authentication, session state and [various other components](../samesite?view=aspnetcore-3.1) set their sameSite options via Cookie options, for example

```
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.SameSite = SameSiteMode.None;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.IsEssential = true;
    });

services.AddSession(options =>
{
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.IsEssential = true;
});
```

In the code shown above both cookie authentication and session state set their sameSite attribute to None, emitting the attribute with a `None` value, and also set the Secure attribute to true.

### Run the sample

If you run the [sample project](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore31RazorPages), load your browser debugger on the initial page and use it to view the cookie collection for the site. To do so in Edge and Chrome press `F12` then select the `Application` tab and click the site URL under the `Cookies` option in the `Storage` section.

You can see from the image above that the cookie created by the sample when you click the "Create SameSite Cookie" button has a SameSite attribute value of `Lax`, matching the value set in the [sample code](#sampleCode).

## Intercepting cookies

In order to intercept cookies, to adjust the none value according to its support in the user's browser agent you must use the `CookiePolicy` middleware. This must be placed into the http request pipeline **before** any components that write cookies and configured within `ConfigureServices()`.

To insert it into the pipeline use `app.UseCookiePolicy()` in the `Configure(IApplicationBuilder, IHostingEnvironment)` method in your [Startup.cs](https://github.com/blowdart/AspNetSameSiteSamples/blob/master/AspNetCore21MVC/Startup.cs). For example

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
       app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();
    app.UseAuthentication();
    app.UseSession();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

Then in the `ConfigureServices(IServiceCollection services)` configure the cookie policy to call out to a helper class when cookies are appended or deleted, like so;

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
        options.OnAppendCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
        options.OnDeleteCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    });
}

private void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
    if (options.SameSite == SameSiteMode.None)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        if (SameSite.BrowserDetection.DisallowsSameSiteNone(userAgent))
        {
            options.SameSite = SameSiteMode.Unspecified;
        }
    }
}
```

The helper function `CheckSameSite(HttpContext, CookieOptions)`:

* Is called when cookies are appended to the request or deleted from the request.
* Checks to see if the `SameSite` property is set to `None`.
* If `SameSite` is set to `None` and the current user agent is known to not support the none attribute value. The check is done using the [SameSiteSupport](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/samesite/snippets/SameSiteSupport.cs) class:
  * Sets `SameSite` to not emit the value by setting the property to `(SameSiteMode)(-1)`

### More Information

[Chrome Updates](https://www.chromium.org/updates/same-site) [ASP.NET Core SameSite Documentation](../samesite?view=aspnetcore-3.1)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2022-08-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/50-to-60-samples
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/50-to-60-samples.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Code samples migrated to the new minimal hosting model in ASP.NET Core in .NET 6

This article provides samples of code migrated to ASP.NET Core in .NET 6. ASP.NET Core in .NET 6 uses a new minimal hosting model. For more information, see [New hosting model](50-to-60?view=aspnetcore-10.0#new-hosting-model).

## Middleware

The following code adds the static file middleware to a .NET 5 app:

```
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseStaticFiles();
    }
}
```

The following code adds the static file middleware to an ASP.NET Core in .NET 6 app:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseStaticFiles();

app.Run();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. For more information, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-6.0)

## Routing

The following code adds an endpoint to an .NET 5 app:

```
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", () => "Hello World");
        });
    }
}
```

In .NET 6, routes can be added directly to the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) without an explicit call to [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) or [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting). The following code adds an endpoint to an ASP.NET Core in .NET 6 app:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

**Note:** Routes added directly to the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) execute at the ***end*** of the pipeline.

[]()

## Change the content root, app name, and environment

### ASP.NET Core in .NET 5

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseEnvironment(Environments.Staging)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>()
                      .UseSetting(WebHostDefaults.ApplicationKey,
                                  typeof(Program).Assembly.FullName);
        });
```

### ASP.NET Core in .NET 6

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

For more information, see [ASP.NET Core fundamentals overview](../fundamentals/?view=aspnetcore-10.0)

#### Change the content root, app name, and environment by environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| feature          | Environment variable        | Command-line argument |
| ---------------- | --------------------------- | --------------------- |
| Application name | ASPNETCORE\_APPLICATIONNAME | --applicationName     |
| Environment name | ASPNETCORE\_ENVIRONMENT     | --environment         |
| Content root     | ASPNETCORE\_CONTENTROOT     | --contentRoot         |

## Add configuration providers

### ASP.NET Core in .NET 5

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration(config =>
        {
            config.AddIniFile("appsettings.ini");
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

### ASP.NET Core in .NET 6

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](../fundamentals/configuration/?view=aspnetcore-6.0#file-configuration-provider) in [Configuration in ASP.NET Core](../fundamentals/configuration/?view=aspnetcore-6.0).

## Add logging providers

### ASP.NET Core in .NET 5

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.AddJsonConsole();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

### ASP.NET Core in .NET 6

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();
```

For more information, see [Logging in .NET and ASP.NET Core](../fundamentals/logging/?view=aspnetcore-6.0#).

## Add services

### ASP.NET Core in .NET 5

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Add the memory cache services
        services.AddMemoryCache();

        // Add a custom scoped service
        services.AddScoped<ITodoRepository, TodoRepository>();
    }
}
```

### ASP.NET Core in .NET 6

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

For more information, see [Dependency injection in ASP.NET Core](../fundamentals/dependency-injection?view=aspnetcore-6.0#).

[]()

## Customize IHostBuilder or IWebHostBuilder

### Customize IHostBuilder

#### ASP.NET Core in .NET 5

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

#### ASP.NET Core in .NET 6

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();
```

### Customize IWebHostBuilder

### ASP.NET Core in .NET 5

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            // Change the HTTP server implementation to be HTTP.sys based.
            webBuilder.UseHttpSys()
                      .UseStartup<Startup>();
        });
```

### ASP.NET Core in .NET 6

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implementation to be HTTP.sys based.
// Windows only.
builder.WebHost.UseHttpSys();

var app = builder.Build();
```

## Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed by setting the [WebRootPath](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationoptions.webrootpath#microsoft-aspnetcore-builder-webapplicationoptions-webrootpath) property on [WebApplicationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationoptions):

### ASP.NET Core in .NET 5

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            // Look for static files in webroot.
            webBuilder.UseWebRoot("webroot")
                      .UseStartup<Startup>();
        });
```

### ASP.NET Core in .NET 6

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();
```

[]()

## Custom dependency injection (DI) container

The following .NET 5 and .NET 6 samples use [Autofac](https://docs.autofac.org/en/latest/integration/aspnetcore.html)

### ASP.NET Core in .NET 5

**Program class**

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory())
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

**Startup**

```
public class Startup
{
    public void ConfigureContainer(ContainerBuilder containerBuilder)
    {
    }
}
```

### ASP.NET Core in .NET 6

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

## Access additional services

`Startup.Configure` can inject any service added via the [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection).

### ASP.NET Core in .NET 5

```
public class Startup
{
    // This method gets called by the runtime. Use this method to add services
    // to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IService, Service>();
    }

    // Anything added to the service collection can be injected into Configure.
    public void Configure(IApplicationBuilder app,
                          IWebHostEnvironment env,
                          IHostApplicationLifetime lifetime,
                          IService service,
                          ILogger<Startup> logger)
    {
        lifetime.ApplicationStarted.Register(() =>
            logger.LogInformation(
                "The application {Name} started in the injected {Service}",
                env.ApplicationName, service));
    }
}
```

### ASP.NET Core in .NET 6

In ASP.NET Core in .NET 6:

* There are a few common services available as top level properties on [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication).
* Additional services need to be manually resolved from the `IServiceProvider` via [WebApplication.Services](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.services#microsoft-aspnetcore-builder-webapplication-services).

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IService, Service>();

var app = builder.Build();

IService service = app.Services.GetRequiredService<IService>();
ILogger logger = app.Logger;
IHostApplicationLifetime lifetime = app.Lifetime;
IWebHostEnvironment env = app.Environment;

lifetime.ApplicationStarted.Register(() =>
    logger.LogInformation(
        $"The application {env.ApplicationName} started" +
        $" with injected {service}"));
```

[]()

## Test with WebApplicationFactory or TestServer

### ASP.NET Core in .NET 5

In the following samples, the test project uses `TestServer` and [WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1). These ship as separate packages that require explicit reference:

#### WebApplicationFactory

```
<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="{Version}" />
</ItemGroup>
```

#### TestServer

```
<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="{Version}" />
</ItemGroup>
```

#### ASP.NET Core in .NET 5 code

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IHelloService, HelloService>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHelloService helloService)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync(helloService.HelloMessage);
            });
        });
    }
}
```

#### With TestServer

```
[Fact]
public async Task HelloWorld()
{
    using var host = Host.CreateDefaultBuilder()
        .ConfigureWebHostDefaults(builder =>
        {
            // Use the test server and point to the application's startup
            builder.UseTestServer()
                    .UseStartup<WebApplication1.Startup>();
        })
        .ConfigureServices(services =>
        {
            // Replace the service
            services.AddSingleton<IHelloService, MockHelloService>();
        })
        .Build();

    await host.StartAsync();

    var client = host.GetTestClient();

    var response = await client.GetStringAsync("/");

    Assert.Equal("Test Hello", response);
}

class MockHelloService : IHelloService
{
    public string HelloMessage => "Test Hello";
}
```

#### With WebApplicationFactory

```
[Fact]
public async Task HelloWorld()
{
    var application = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton<IHelloService, MockHelloService>();
            });
        });

    var client = application.CreateClient();

    var response = await client.GetStringAsync("/");

    Assert.Equal("Test Hello", response);
}

class MockHelloService : IHelloService
{
    public string HelloMessage => "Test Hello";
}
```

### ASP.NET Core in .NET 6

```

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IHelloService, HelloService>();

var app = builder.Build();

var helloService = app.Services.GetRequiredService<IHelloService>();

app.MapGet("/", async context =>
{
    await context.Response.WriteAsync(helloService.HelloMessage);
});

app.Run();
```

#### Project file (.csproj)

The project file can contain one of the following:

```
<ItemGroup>
    <InternalsVisibleTo Include="MyTestProject" />
</ItemGroup>
```

Or

```
[assembly: InternalsVisibleTo("MyTestProject")]
```

An alternative solution is to make the `Program` class public. `Program` can be made public with [Top-level statements](/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) by defining a `public partial Program` class in the project or in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

// ... Configure services, routes, etc.

app.Run();

public partial class Program { }
```

```
[Fact]
public async Task HelloWorld()
{
    var application = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton<IHelloService, MockHelloService>();
            });
        });

    var client = application.CreateClient();

    var response = await client.GetStringAsync("/");

    Assert.Equal("Test Hello", response);
}

class MockHelloService : IHelloService
{
    public string HelloMessage => "Test Hello";
}
```

The .NET 5 version and .NET 6 version with the `WebApplicationFactory` are identical by design.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/performance-best-practices
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/best-practices.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Best Practices

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Mike Rousos](https://github.com/mjrousos)

This article provides guidelines for maximizing performance and reliability of ASP.NET Core apps.

## Cache aggressively

Caching is discussed in several parts of this article. For more information, see [Overview of caching in ASP.NET Core](../performance/caching/overview?view=aspnetcore-10.0).

## Understand hot code paths

In this article, a *hot code path* is defined as a code path that is frequently called and where much of the execution time occurs. Hot code paths typically limit app scale-out and performance and are discussed in several parts of this article.

## Avoid blocking calls

ASP.NET Core apps should be designed to process many requests simultaneously. Asynchronous APIs allow a small pool of threads to handle thousands of concurrent requests by not waiting on blocking calls. Rather than waiting on a long-running synchronous task to complete, the thread can work on another request.

A common performance problem in ASP.NET Core apps is blocking calls that could be asynchronous. Many synchronous blocking calls lead to [Thread Pool starvation](/en-us/dotnet/core/diagnostics/debug-threadpool-starvation) and degraded response times.

**Do not** block asynchronous execution by calling [Task.Wait](/en-us/dotnet/api/system.threading.tasks.task.wait) or [Task\<TResult>.Result](/en-us/dotnet/api/system.threading.tasks.task-1.result). **Do not** acquire locks in common code paths. ASP.NET Core apps perform best when architected to run code in parallel. **Do not** call [Task.Run](/en-us/dotnet/api/system.threading.tasks.task.run) and immediately await it. ASP.NET Core already runs app code on normal Thread Pool threads, so calling `Task.Run` only results in extra unnecessary Thread Pool scheduling. Even if the scheduled code would block a thread, `Task.Run` does not prevent that.

* **Do** make [hot code paths](#understand-hot-code-paths) asynchronous.
* **Do** call data access, I/O, and long-running operations APIs asynchronously if an asynchronous API is available.
* **Do not** use [Task.Run](/en-us/dotnet/api/system.threading.tasks.task.run) to make a synchronous API asynchronous.
* **Do** make controller/Razor Page actions asynchronous. The entire call stack is asynchronous in order to benefit from [async/await](/en-us/dotnet/csharp/programming-guide/concepts/async/) patterns.
* **Consider** using message brokers like [Azure Service Bus](/en-us/azure/service-bus-messaging/service-bus-messaging-overview) to offload long-running calls

A profiler, such as [PerfView](https://github.com/Microsoft/perfview), can be used to find threads frequently added to the [Thread Pool](/en-us/windows/desktop/procthread/thread-pools). The `Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start` event indicates a thread added to the thread pool.

## Return large collections across multiple smaller pages

A webpage shouldn't load large amounts of data all at once. When returning a collection of objects, consider whether it could lead to performance issues. Determine if the design could produce the following poor outcomes:

* [OutOfMemoryException](/en-us/dotnet/api/system.outofmemoryexception) or high memory consumption
* Thread pool starvation (see the following remarks on [IAsyncEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1))
* Slow response times
* Frequent garbage collection

**Do** add pagination to mitigate the preceding scenarios. Using page size and page index parameters, developers should favor the design of returning a partial result. When an exhaustive result is required, pagination should be used to asynchronously populate batches of results to avoid locking server resources.

For more information on paging and limiting the number of returned records, see:

* [Performance considerations](../data/ef-rp/intro?view=aspnetcore-10.0#performance-considerations)
* [Add paging to an ASP.NET Core app](../data/ef-rp/sort-filter-page?view=aspnetcore-10.0#add-paging)

### Return `IEnumerable<T>` or `IAsyncEnumerable<T>`

Returning `IEnumerable<T>` from an action results in synchronous collection iteration by the serializer. The result is the blocking of calls and a potential for thread pool starvation. To avoid synchronous enumeration, use `ToListAsync` before returning the enumerable.

Beginning with ASP.NET Core 3.0, `IAsyncEnumerable<T>` can be used as an alternative to `IEnumerable<T>` that enumerates asynchronously. For more information, see [Controller action return types](../web-api/action-return-types?view=aspnetcore-10.0#return-ienumerablet-or-iasyncenumerablet).

## Minimize large object allocations

The [.NET garbage collector](/en-us/dotnet/standard/garbage-collection/) manages allocation and release of memory automatically in ASP.NET Core apps. Automatic garbage collection generally means that developers don't need to worry about how or when memory is freed. However, cleaning up unreferenced objects takes CPU time, so developers should minimize allocating objects in [hot code paths](#understand-hot-code-paths). Garbage collection is especially expensive on large objects (>= 85,000 bytes). Large objects are stored on the [large object heap](/en-us/dotnet/standard/garbage-collection/large-object-heap) and require a full (generation 2) garbage collection to clean up. Unlike generation 0 and generation 1 collections, a generation 2 collection requires a temporary suspension of app execution. Frequent allocation and de-allocation of large objects can cause inconsistent performance.

Recommendations:

* **Do** consider caching large objects that are frequently used. Caching large objects prevents expensive allocations.
* **Do** pool buffers by using an [ArrayPool\<T>](/en-us/dotnet/api/system.buffers.arraypool-1) to store large arrays.
* **Do not** allocate many, short-lived large objects on [hot code paths](#understand-hot-code-paths).

Memory issues, such as the preceding, can be diagnosed by reviewing garbage collection (GC) stats in [PerfView](https://github.com/Microsoft/perfview) and examining:

* Garbage collection pause time.
* What percentage of the processor time is spent in garbage collection.
* How many garbage collections are generation 0, 1, and 2.

For more information, see [Garbage Collection and Performance](/en-us/dotnet/standard/garbage-collection/performance).

## Optimize data access and I/O

Interactions with a data store and other remote services are often the slowest parts of an ASP.NET Core app. Reading and writing data efficiently is critical for good performance.

Recommendations:

* **Do** call all data access APIs asynchronously.
* **Do not** retrieve more data than is necessary. Write queries to return just the data that's necessary for the current HTTP request.
* **Do** consider caching frequently accessed data retrieved from a database or remote service if slightly out-of-date data is acceptable. Depending on the scenario, use a [MemoryCache](../performance/caching/memory?view=aspnetcore-10.0) or a [DistributedCache](../performance/caching/distributed?view=aspnetcore-10.0). For more information, see [Response caching in ASP.NET Core](../performance/caching/response?view=aspnetcore-10.0).
* **Do** minimize network round trips. The goal is to retrieve the required data in a single call rather than several calls.
* **Do** use [no-tracking queries](/en-us/ef/core/querying/tracking#no-tracking-queries) in Entity Framework Core when accessing data for read-only purposes. EF Core can return the results of no-tracking queries more efficiently.
* **Do** filter and aggregate LINQ queries (with `.Where`, `.Select`, or `.Sum` statements, for example) so that the filtering is performed by the database.
* **Do** consider that EF Core resolves some query operators on the client, which may lead to inefficient query execution. For more information, see [Client evaluation performance issues](/en-us/ef/core/querying/client-eval#client-evaluation-performance-issues).
* **Do not** use projection queries on collections, which can result in executing "N + 1" SQL queries. For more information, see [Optimization of correlated subqueries](/en-us/ef/core/what-is-new/ef-core-2.1#optimization-of-correlated-subqueries).

The following approaches may improve performance in high-scale apps:

* [DbContext pooling](/en-us/ef/core/what-is-new/ef-core-2.0#dbcontext-pooling)
* [Explicitly compiled queries](/en-us/ef/core/what-is-new/ef-core-2.0#explicitly-compiled-queries)

We recommend measuring the impact of the preceding high-performance approaches before committing the code base. The additional complexity of compiled queries may not justify the performance improvement.

Query issues can be detected by reviewing the time spent accessing data with [Application Insights](/en-us/azure/application-insights/app-insights-overview) or with profiling tools. Most databases also make statistics available concerning frequently executed queries.

## Pool HTTP connections with HttpClientFactory

Although [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) implements the `IDisposable` interface, it's designed for reuse. Closed `HttpClient` instances leave sockets open in the `TIME_WAIT` state for a short period of time. If a code path that creates and disposes of `HttpClient` objects is frequently used, the app may exhaust available sockets. `HttpClientFactory` was introduced in ASP.NET Core 2.1 as a solution to this problem. It handles pooling HTTP connections to optimize performance and reliability. For more information, see [Use `HttpClientFactory` to implement resilient HTTP requests](/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests).

Recommendations:

* **Do not** create and dispose of `HttpClient` instances directly.
* **Do** use [HttpClientFactory](/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) to retrieve `HttpClient` instances. For more information, see [Use HttpClientFactory to implement resilient HTTP requests](/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests).

## Keep common code paths fast

You want all of your code to be fast. Frequently-called code paths are the most critical to optimize. These include:

* Middleware components in the app's request processing pipeline, especially middleware run early in the pipeline. These components have a large impact on performance.
* Code that's executed for every request or multiple times per request. For example, custom logging, authorization handlers, or initialization of transient services.

Recommendations:

* **Do not** use custom middleware components with long-running tasks.
* **Do** use performance profiling tools, such as [Visual Studio Diagnostic Tools](/en-us/visualstudio/profiling/profiling-feature-tour) or [PerfView](https://github.com/Microsoft/perfview)), to identify [hot code paths](#understand-hot-code-paths).

## Complete long-running Tasks outside of HTTP requests

Most requests to an ASP.NET Core app can be handled by a controller or page model calling necessary services and returning an HTTP response. For some requests that involve long-running tasks, it's better to make the entire request-response process asynchronous.

Recommendations:

* **Do not** wait for long-running tasks to complete as part of ordinary HTTP request processing.
* **Do** consider handling long-running requests with [background services](host/hosted-services?view=aspnetcore-10.0) or out of process possibly with an [Azure Function](/en-us/azure/azure-functions/) and/or using a message broker like [Azure Service Bus](/en-us/azure/service-bus-messaging/service-bus-messaging-overview). Completing work out-of-process is especially beneficial for CPU-intensive tasks.
* **Do** use real-time communication options, such as [SignalR](../signalr/introduction?view=aspnetcore-10.0), to communicate with clients asynchronously.

## Minify client assets

ASP.NET Core apps with complex front-ends frequently serve many JavaScript, CSS, or image files. Performance of initial load requests can be improved by:

* Bundling, which combines multiple files into one.
* Minifying, which reduces the size of files by removing whitespace and comments.

Recommendations:

* **Do** use the [bundling and minification guidelines](../client-side/bundling-and-minification?view=aspnetcore-10.0), which mention compatible tools and show how to use ASP.NET Core's `environment` tag to handle both `Development` and `Production` environments.
* **Do** consider other third-party tools, such as [Webpack](https://webpack.js.org/), for complex client asset management.

## Compress responses

Reducing the size of the response usually increases the responsiveness of an app, often dramatically. One way to reduce payload sizes is to compress an app's responses. For more information, see [Response compression](../performance/response-compression?view=aspnetcore-10.0).

## Use the latest ASP.NET Core release

Each new release of ASP.NET Core includes performance improvements. Optimizations in .NET and ASP.NET Core mean that newer versions generally outperform older versions. For example, .NET Core 2.1 added support for compiled regular expressions and benefitted from [Span\<T>](/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines). ASP.NET Core 2.2 added support for HTTP/2. [ASP.NET Core 3.0 adds many improvements](../release-notes/aspnetcore-3.0?view=aspnetcore-10.0) that reduce memory usage and improve throughput. If performance is a priority, consider upgrading to the current version of ASP.NET Core.

## Minimize exceptions

Exceptions should be rare. Throwing and catching exceptions is slow relative to other code flow patterns. Because of this, exceptions shouldn't be used to control normal program flow.

Recommendations:

* **Do not** use throwing or catching exceptions as a means of normal program flow, especially in [hot code paths](#understand-hot-code-paths).
* **Do** include logic in the app to detect and handle conditions that would cause an exception.
* **Do** throw or catch exceptions for unusual or unexpected conditions.

App diagnostic tools, such as Application Insights, can help to identify common exceptions in an app that may affect performance.

## Avoid synchronous read or write on HttpRequest/HttpResponse body

All I/O in ASP.NET Core is asynchronous. Servers implement the `Stream` interface, which has both synchronous and asynchronous overloads. The asynchronous ones should be preferred to avoid blocking thread pool threads. Blocking threads can lead to thread pool starvation.

**Do not do this:** The following example uses the [ReadToEnd](/en-us/dotnet/api/system.io.streamreader.readtoend). It blocks the current thread to wait for the result. This is an example of [sync over async](https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#warning-sync-over-async).

```
public class BadStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public ActionResult<ContosoData> Get()
    {
        var json = new StreamReader(Request.Body).ReadToEnd();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }
}
```

In the preceding code, `Get` synchronously reads the entire HTTP request body into memory. If the client is slowly uploading, the app is doing sync over async. The app does sync over async because Kestrel does **NOT** support synchronous reads.

**Do this:** The following example uses [ReadToEndAsync](/en-us/dotnet/api/system.io.streamreader.readtoendasync) and does not block the thread while reading.

```
public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        var json = await new StreamReader(Request.Body).ReadToEndAsync();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }

}
```

The preceding code asynchronously reads the entire HTTP request body into memory.

Warning

If the request is large, reading the entire HTTP request body into memory could lead to an out of memory (OOM) condition. OOM can result in a Denial Of Service. For more information, see [Avoid reading large request bodies or response bodies into memory](#arlb) in this article.

**Do this:** The following example is fully asynchronous using a non-buffered request body:

```
public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);
    }
}
```

The preceding code asynchronously de-serializes the request body into a C# object.

## Prefer ReadFormAsync over Request.Form

Use `HttpContext.Request.ReadFormAsync` instead of `HttpContext.Request.Form`. `HttpContext.Request.Form` can be safely read only with the following conditions:

* The form has been read by a call to `ReadFormAsync`, and
* The cached form value is being read using `HttpContext.Request.Form`

**Do not do this:** The following example uses `HttpContext.Request.Form`. `HttpContext.Request.Form` uses [sync over async](https://devblogs.microsoft.com/pfxteam/should-i-expose-synchronous-wrappers-for-asynchronous-methods/) and can lead to thread pool starvation.

```
public class BadReadController : Controller
{
    [HttpPost("/form-body")]
    public IActionResult Post()
    {
        var form =  HttpContext.Request.Form;

        Process(form["id"], form["name"]);

        return Accepted();
    }
```

**Do this:** The following example uses `HttpContext.Request.ReadFormAsync` to read the form body asynchronously.

```
public class GoodReadController : Controller
{
    [HttpPost("/form-body")]
    public async Task<IActionResult> Post()
    {
       var form = await HttpContext.Request.ReadFormAsync();

        Process(form["id"], form["name"]);

        return Accepted();
    }
```

[]()

## Avoid reading large request bodies or response bodies into memory

In .NET, every object allocation greater than or equal to 85,000 bytes ends up in the [large object heap (LOH)](/en-us/dotnet/standard/garbage-collection/large-object-heap). Large objects are expensive in two ways:

* The allocation cost is high because the memory for a newly allocated large object has to be cleared. The CLR guarantees that memory for all newly allocated objects is cleared.
* LOH is collected with the rest of the heap. LOH requires a full [garbage collection](/en-us/dotnet/standard/garbage-collection/fundamentals) or [Gen2 collection](/en-us/dotnet/standard/garbage-collection/fundamentals#generations).

This [blog post](https://adamsitnik.com/Array-Pool/#the-problem) describes the problem succinctly:

> When a large object is allocated, it's marked as Gen 2 object. Not Gen 0 as for small objects. The consequences are that if you run out of memory in LOH, GC cleans up the whole managed heap, not only LOH. So it cleans up Gen 0, Gen 1 and Gen 2 including LOH. This is called full garbage collection and is the most time-consuming garbage collection. For many applications, it can be acceptable. But definitely not for high-performance web servers, where few big memory buffers are needed to handle an average web request (read from a socket, decompress, decode JSON, and more).

Storing a large request or response body into a single `byte[]` or `string`:

* May result in quickly running out of space in the LOH.
* May cause performance issues for the app because of full GCs running.

## Working with a synchronous data processing API

When using a serializer/de-serializer that only supports synchronous reads and writes (for example, [Json.NET](https://www.newtonsoft.com/json/help/html/Introduction.htm)):

* Buffer the data into memory asynchronously before passing it into the serializer/de-serializer.

Warning

If the request is large, it could lead to an out of memory (OOM) condition. OOM can result in a Denial Of Service. For more information, see [Avoid reading large request bodies or response bodies into memory](#arlb) in this article.

ASP.NET Core 3.0 uses [System.Text.Json](/en-us/dotnet/api/system.text.json) by default for JSON serialization. [System.Text.Json](/en-us/dotnet/api/system.text.json):

* Reads and writes JSON asynchronously.
* Is optimized for UTF-8 text.
* Typically is higher performance than `Newtonsoft.Json`.

## Do not store IHttpContextAccessor.HttpContext in a field

The [IHttpContextAccessor.HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor.httpcontext#microsoft-aspnetcore-http-ihttpcontextaccessor-httpcontext) returns the `HttpContext` of the active request when accessed from the request thread. The `IHttpContextAccessor.HttpContext` should **not** be stored in a field or variable.

**Do not do this:** The following example stores the `HttpContext` in a field and then attempts to use it later.

```
public class MyBadType
{
    private readonly HttpContext _context;
    public MyBadType(IHttpContextAccessor accessor)
    {
        _context = accessor.HttpContext;
    }

    public void CheckAdmin()
    {
        if (!_context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}
```

The preceding code frequently captures a null or incorrect `HttpContext` in the constructor.

**Do this:** The following example:

* Stores the [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) in a field.
* Uses the `HttpContext` field at the correct time and checks for `null`.

```
public class MyGoodType
{
    private readonly IHttpContextAccessor _accessor;
    public MyGoodType(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }

    public void CheckAdmin()
    {
        var context = _accessor.HttpContext;
        if (context != null && !context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}
```

## Do not access HttpContext from multiple threads

`HttpContext` is **not** thread-safe. Accessing `HttpContext` from multiple threads in parallel can result in unexpected behavior such as the server to stop responding, crashes, and data corruption.

**Do not do this:** The following example makes three parallel requests and logs the incoming request path before and after the outgoing HTTP request. The request path is accessed from multiple threads, potentially in parallel.

```
public class AsyncBadSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        var query1 = SearchAsync(SearchEngine.Google, query);
        var query2 = SearchAsync(SearchEngine.Bing, query);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }       

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.", 
                                    HttpContext.Request.Path);
            searchResults = _searchService.Search(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", 
                                    HttpContext.Request.Path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", 
                             HttpContext.Request.Path);
        }

        return await searchResults;
    }
```

**Do this:** The following example copies all data from the incoming request before making the three parallel requests.

```
public class AsyncGoodSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        string path = HttpContext.Request.Path;
        var query1 = SearchAsync(SearchEngine.Google, query,
                                 path);
        var query2 = SearchAsync(SearchEngine.Bing, query, path);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query,
                                                  string path)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.",
                                   path);
            searchResults = await _searchService.SearchAsync(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", path);
        }

        return await searchResults;
    }
```

## Do not use the HttpContext after the request is complete

`HttpContext` is only valid as long as there is an active HTTP request in the ASP.NET Core pipeline. The entire ASP.NET Core pipeline is an asynchronous chain of delegates that executes every request. When the `Task` returned from this chain completes, the `HttpContext` is recycled.

**Do not do this:** The following example uses `async void` which makes the HTTP request complete when the first `await` is reached:

* Using `async void` is **ALWAYS** a bad practice in ASP.NET Core apps.
* The example code accesses the `HttpResponse` after the HTTP request is complete.
* The late access crashes the process.

```
public class AsyncBadVoidController : Controller
{
    [HttpGet("/async")]
    public async void Get()
    {
        await Task.Delay(1000);

        // The following line will crash the process because of writing after the 
        // response has completed on a background thread. Notice async void Get()

        await Response.WriteAsync("Hello World");
    }
}
```

**Do this:** The following example returns a `Task` to the framework, so the HTTP request doesn't complete until the action completes.

```
public class AsyncGoodTaskController : Controller
{
    [HttpGet("/async")]
    public async Task Get()
    {
        await Task.Delay(1000);

        await Response.WriteAsync("Hello World");
    }
}
```

## Do not capture the HttpContext in background threads

**Do not do this:** The following example shows a closure is capturing the `HttpContext` from the `Controller` property. This is a bad practice because the work item could:

* Run outside of the request scope.
* Attempt to read the wrong `HttpContext`.

```
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        var path = HttpContext.Request.Path;
        Log(path);
    });

    return Accepted();
}
```

**Do this:** The following example:

* Copies the data required in the background task during the request.
* Doesn't reference anything from the controller.

```
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}
```

Background tasks should be implemented as hosted services. For more information, see [Background tasks with hosted services](host/hosted-services?view=aspnetcore-10.0).

## Do not capture services injected into the controllers on background threads

**Do not do this:** The following example shows a closure that is capturing the `DbContext` from the `Controller` action parameter. This is a bad practice. The work item could run outside of the request scope. The `ContosoDbContext` is scoped to the request, resulting in an `ObjectDisposedException`.

```
[HttpGet("/fire-and-forget-1")]
public IActionResult FireAndForget1([FromServices]ContosoDbContext context)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        context.Contoso.Add(new Contoso());
        await context.SaveChangesAsync();
    });

    return Accepted();
}
```

**Do this:** The following example:

* Injects an [IServiceScopeFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicescopefactory) in order to create a scope in the background work item. `IServiceScopeFactory` is a singleton.
* Creates a new dependency injection scope in the background thread.
* Doesn't reference anything from the controller.
* Doesn't capture the `ContosoDbContext` from the incoming request.

```
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        await using (var scope = serviceScopeFactory.CreateAsyncScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}
```

The following highlighted code:

* Creates a scope for the lifetime of the background operation and resolves services from it.
* Uses `ContosoDbContext` from the correct scope.

```
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        await using (var scope = serviceScopeFactory.CreateAsyncScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}
```

## Do not modify the status code or headers after the response body has started

ASP.NET Core does not buffer the HTTP response body. The first time the response is written:

* The headers are sent along with that chunk of the body to the client.
* It's no longer possible to change response headers.

**Do not do this:** The following code tries to add response headers after the response has already started:

```
app.Use(async (context, next) =>
{
    await next();

    context.Response.Headers["test"] = "test value";
});
```

In the preceding code, `context.Response.Headers["test"] = "test value";` will throw an exception if `next()` has written to the response.

**Do this:** The following example checks if the HTTP response has started before modifying the headers.

```
app.Use(async (context, next) =>
{
    await next();

    if (!context.Response.HasStarted)
    {
        context.Response.Headers["test"] = "test value";
    }
});
```

**Do this:** The following example uses `HttpResponse.OnStarting` to set the headers before the response headers are flushed to the client.

Checking if the response has not started allows registering a callback that will be invoked just before response headers are written. Checking if the response has not started:

* Provides the ability to append or override headers just in time.
* Doesn't require knowledge of the next middleware in the pipeline.

```
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});
```

## Do not call next() if you have already started writing to the response body

Components only expect to be called if it's possible for them to handle and manipulate the response.

## Use In-process hosting with IIS

Using in-process hosting, an ASP.NET Core app runs in the same process as its IIS worker process. In-process hosting provides improved performance over out-of-process hosting because requests aren't proxied over the loopback adapter. The loopback adapter is a network interface that returns outgoing network traffic back to the same machine. IIS handles process management with the [Windows Process Activation Service (WAS)](/en-us/iis/manage/provisioning-and-managing-iis/features-of-the-windows-process-activation-service-was).

Projects default to the in-process hosting model in ASP.NET Core 3.0 or later.

For more information, see [Host ASP.NET Core on Windows with IIS](../host-and-deploy/iis/?view=aspnetcore-10.0)

## Don't assume that HttpRequest.ContentLength is not null

`HttpRequest.ContentLength` is null if the `Content-Length` header is not received. Null in that case means the length of the request body is not known; it doesn't mean the length is zero. Because all comparisons with null (except `==`) return false, the comparison `Request.ContentLength > 1024`, for example, might return `false` when the request body size is more than 1024. Not knowing this can lead to security holes in apps. You might think you're protecting against too-large requests when you aren't.

For more information, see [this StackOverflow answer](https://stackoverflow.com/a/73201538/652224).

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/transforms-response
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/transforms-response.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Response and Response Trailers

All response headers and trailers are copied from the proxied response to the outgoing client response by default. Response and response trailer transforms may specify if they should be applied only for successful responses or for all responses.

In code these are implemented as derivations of the abstract classes [ResponseTransform](/en-us/dotnet/api/yarp.reverseproxy.transforms.responsetransform) and [ResponseTrailersTransform](/en-us/dotnet/api/yarp.reverseproxy.transforms.responsetrailerstransform).

## ResponseHeadersCopy

**Sets whether destination response headers are copied to the client**

| Key                 | Value      | Default | Required |
| ------------------- | ---------- | ------- | -------- |
| ResponseHeadersCopy | true/false | true    | yes      |

Config:

```
{ "ResponseHeadersCopy": "false" }
```

Code:

```
routeConfig = routeConfig.WithTransformCopyResponseHeaders(copy: false);
```

```
transformBuilderContext.CopyResponseHeaders = false;
```

This sets if all proxy response headers are copied to the client response. This setting is enabled by default and can be disabled by configuring the transform with a `false` value. Transforms that reference specific headers will still be run if this is disabled.

## ResponseHeader

**Adds or replaces response headers**

| Key            | Value                  | Default | Required |
| -------------- | ---------------------- | ------- | -------- |
| ResponseHeader | The header name        | (none)  | yes      |
| Set/Append     | The header value       | (none)  | yes      |
| When           | Success/Always/Failure | Success | no       |

Config:

```
{
  "ResponseHeader": "HeaderName",
  "Append": "value",
  "When": "Success"
}
```

Code:

```
routeConfig = routeConfig.WithTransformResponseHeader(
    headerName: "HeaderName", value: "value", append: true, ResponseCondition.Success);
```

```
transformBuilderContext.AddResponseHeader(
    headerName: "HeaderName", value: "value", append: true, always: ResponseCondition.Success);
```

Example:

```
HeaderName: value
```

This sets or appends the value for the named response header. Set replaces any existing header. Append adds an additional header with the given value. Note: setting "" as a header value is not recommended and can cause an undefined behavior.

`When` specifies if the response header should be included for all, successful, or failure responses. Any response with a status code less than 400 is considered a success.

## ResponseHeaderRemove

**Removes response headers**

| Key                  | Value                  | Default | Required |
| -------------------- | ---------------------- | ------- | -------- |
| ResponseHeaderRemove | The header name        | (none)  | yes      |
| When                 | Success/Always/Failure | Success | no       |

Config:

```
{
  "ResponseHeaderRemove": "HeaderName",
  "When": "Success"
}
```

Code:

```
routeConfig = routeConfig.WithTransformResponseHeaderRemove(
    headerName: "HeaderName", ResponseCondition.Success);
```

```
transformBuilderContext.AddResponseHeaderRemove(
    headerName: "HeaderName", ResponseCondition.Success);
```

Example:

```
HeaderName: value
AnotherHeader: another-value
```

This removes the named response header.

`When` specifies if the response header should be removed for all, successful, or failure responses. Any response with a status code less than 400 is considered a success.

## ResponseHeadersAllowed

| Key                    | Value                                               | Required |
| ---------------------- | --------------------------------------------------- | -------- |
| ResponseHeadersAllowed | A semicolon separated list of allowed header names. | yes      |

Config:

```
{
  "ResponseHeadersAllowed": "Header1;header2"
}
```

Code:

```
routeConfig = routeConfig.WithTransformResponseHeadersAllowed("Header1", "header2");
```

```
transformBuilderContext.AddResponseHeadersAllowed("Header1", "header2");
```

YARP copies most response headers from the proxy response by default (see [ResponseHeadersCopy](transforms?view=aspnetcore-10.0#responseheaderscopy)). Some security models only allow specific headers to be proxied. This transform disables ResponseHeadersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list.

Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities.

Example:

```
Header1: value1
Header2: value2
AnotherHeader: AnotherValue
```

Only header1 and header2 are copied from the proxy response.

## ResponseTrailersCopy

**Sets whether destination trailing response headers are copied to the client**

| Key                  | Value      | Default | Required |
| -------------------- | ---------- | ------- | -------- |
| ResponseTrailersCopy | true/false | true    | yes      |

Config:

```
{ "ResponseTrailersCopy": "false" }
```

Code:

```
routeConfig = routeConfig.WithTransformCopyResponseTrailers(copy: false);
```

```
transformBuilderContext.CopyResponseTrailers = false;
```

This sets if all proxy response trailers are copied to the client response. This setting is enabled by default and can be disabled by configuring the transform with a `false` value. Transforms that reference specific headers will still be run if this is disabled.

## ResponseTrailer

**Adds or replaces trailing response headers**

| Key             | Value                  | Default | Required |
| --------------- | ---------------------- | ------- | -------- |
| ResponseTrailer | The header name        | (none)  | yes      |
| Set/Append      | The header value       | (none)  | yes      |
| When            | Success/Always/Failure | Success | no       |

Config:

```
{
  "ResponseTrailer": "HeaderName",
  "Append": "value",
  "When": "Success"
}
```

Code:

```
routeConfig = routeConfig.WithTransformResponseTrailer(
    headerName: "HeaderName", value: "value", append: true, ResponseCondition.Success);
```

```
transformBuilderContext.AddResponseTrailer(
    headerName: "HeaderName", value: "value", append: true, ResponseCondition.Success);
```

Example:

```
HeaderName: value
```

Response trailers are headers sent at the end of the response body. Support for trailers is uncommon in HTTP/1.1 implementations but is becoming common in HTTP/2 implementations. Check your client and server for support.

ResponseTrailer follows the same structure and guidance as [ResponseHeader](transforms?view=aspnetcore-10.0#responseheader).

## ResponseTrailerRemove

**Removes trailing response headers**

| Key                   | Value                  | Default | Required |
| --------------------- | ---------------------- | ------- | -------- |
| ResponseTrailerRemove | The header name        | (none)  | yes      |
| When                  | Success/Always/Failure | Success | no       |

Config:

```
{
  "ResponseTrailerRemove": "HeaderName",
  "When": "Success"
}
```

Code:

```
routeConfig = routeConfig.WithTransformResponseTrailerRemove(headerName: "HeaderName", ResponseCondition.Success);
```

```
transformBuilderContext.AddResponseTrailerRemove(headerName: "HeaderName", ResponseCondition.Success);
```

Example:

```
HeaderName: value
AnotherHeader: another-value
```

This removes the named trailing header.

ResponseTrailerRemove follows the same structure and guidance as [ResponseHeaderRemove](transforms?view=aspnetcore-10.0#responseheaderremove).

## ResponseTrailersAllowed

| Key                     | Value                                               | Required |
| ----------------------- | --------------------------------------------------- | -------- |
| ResponseTrailersAllowed | A semicolon separated list of allowed header names. | yes      |

Config:

```
{
  "ResponseTrailersAllowed": "Header1;header2"
}
```

Code:

```
routeConfig = routeConfig.WithTransformResponseTrailersAllowed("Header1", "header2");
```

```
transformBuilderContext.AddResponseTrailersAllowed("Header1", "header2");
```

YARP copies most response trailers from the proxy response by default (see [ResponseTrailersCopy](transforms?view=aspnetcore-10.0#responsetrailerscopy)). Some security models only allow specific headers to be proxied. This transform disables ResponseTrailersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list.

Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities.

Example:

```
Header1: value1
Header2: value2
AnotherHeader: AnotherValue
```

Only header1 and header2 are copied from the proxy response.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Views in ASP.NET Core MVC

By [Steve Smith](https://ardalis.com/) and [Dave Brock](https://twitter.com/daveabrock)

This document explains views used in ASP.NET Core MVC applications. For information on Razor Pages, see [Razor Pages architecture and concepts in ASP.NET Core](../../razor-pages/?view=aspnetcore-10.0).

In the Model-View-Controller (MVC) pattern, the *view* handles the app's data presentation and user interaction. A view is an HTML template with embedded [Razor markup](razor?view=aspnetcore-10.0). Razor markup is code that interacts with HTML markup to produce a webpage that's sent to the client.

In ASP.NET Core MVC, views are `.cshtml` files that use the [C# programming language](/en-us/dotnet/csharp/) in Razor markup. Usually, view files are grouped into folders named for each of the app's [controllers](../controllers/actions?view=aspnetcore-10.0). The folders are stored in a `Views` folder at the root of the app:

A `Home` folder inside the `Views` folder represents the `Home` controller. The `Home` folder contains the views for the `About`, `Contact`, and `Index` (homepage) webpages. When a user requests one of these three webpages, controller actions in the `Home` controller determine which of the three views is used to build and return a webpage to the user.

Use [layouts](layout?view=aspnetcore-10.0) to provide consistent webpage sections and reduce code repetition. Layouts often contain the header, navigation and menu elements, and the footer. The header and footer usually contain boilerplate markup for many metadata elements and links to script and style assets. Layouts help you avoid this boilerplate markup in your views.

[Partial views](partial?view=aspnetcore-10.0) reduce code duplication by managing reusable parts of views. For example, a partial view is useful for an author biography on a blog website that appears in several views. An author biography is ordinary view content and doesn't require code to execute in order to produce the content for the webpage. Author biography content is available to the view by model binding alone, so using a partial view for this type of content is ideal.

[View components](view-components?view=aspnetcore-10.0) are similar to partial views in that they allow you to reduce repetitive code, but they're appropriate for view content that requires code to run on the server in order to render the webpage. View components are useful when the rendered content requires database interaction, such as for a website shopping cart. View components aren't limited to model binding in order to produce webpage output.

## Benefits of using views

Views help to establish [separation of concerns](/en-us/dotnet/architecture/modern-web-apps-azure/architectural-principles#separation-of-concerns) within an MVC app by separating the user interface markup from other parts of the app. Following SoC design makes your app modular, which provides several benefits:

* The app is easier to maintain because it's better organized. Views are generally grouped by app feature. This makes it easier to find related views when working on a feature.
* The parts of the app are loosely coupled. You can build and update the app's views separately from the business logic and data access components. You can modify the views of the app without necessarily having to update other parts of the app.
* It's easier to test the user interface parts of the app because the views are separate units.
* Due to better organization, it's less likely that you'll accidentally repeat sections of the user interface.

## Creating a view

Views that are specific to a controller are created in the `Views/[ControllerName]` folder. Views that are shared among controllers are placed in the `Views/Shared` folder. To create a view, add a new file and give it the same name as its associated controller action with the `.cshtml` file extension. To create a view that corresponds with the `About` action in the `Home` controller, create an `About.cshtml` file in the `Views/Home` folder:

```
@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>Use this area to provide additional information.</p>
```

[Razor markup](razor?view=aspnetcore-10.0) starts with the `@` symbol. Run C# statements by placing C# code within [Razor code blocks](razor?view=aspnetcore-10.0#razor-code-blocks) set off by curly braces (`{ ... }`). For example, see the assignment of "About" to `ViewData["Title"]` shown previously. You can display values within HTML by simply referencing the value with the `@` symbol. See the contents of the `<h2>` and `<h3>` elements previously.

The view content shown previously is only part of the entire webpage that's rendered to the user. The rest of the page's layout and other common aspects of the view are specified in other view files. To learn more, see the [Layout topic](layout?view=aspnetcore-10.0).

## How controllers specify views

Views are typically returned from actions as a [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult), which is a type of [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult). Your action method can create and return a `ViewResult` directly, but that isn't commonly done. Since most controllers inherit from [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller), you simply use the `View` helper method to return the `ViewResult`:

`HomeController.cs`:

```
public IActionResult About()
{
    ViewData["Message"] = "Your application description page.";

    return View();
}
```

When this action returns, the `About.cshtml` view shown in the last section is rendered as the following webpage:

The `View` helper method has several overloads. You can optionally specify:

* An explicit view to return:

  ```
  return View("Orders");
  ```

* A [model](../models/model-binding?view=aspnetcore-10.0) to pass to the view:

  ```
  return View(Orders);
  ```

* Both a view and a model:

  ```
  return View("Orders", Orders);
  ```

### View discovery

When an action returns a view, a process called *view discovery* takes place. This process determines which view file is used based on the view name.

The default behavior of the `View` method (`return View();`) is to return a view with the same name as the action method from which it's called. For example, the `About` `ActionResult` method name of the controller is used to search for a view file named `About.cshtml`. First, the runtime looks in the `Views/[ControllerName]` folder for the view. If it doesn't find a matching view there, it searches the `Shared` folder for the view.

It doesn't matter if you implicitly return the `ViewResult` with `return View();` or explicitly pass the view name to the `View` method with `return View("<ViewName>");`. In both cases, view discovery searches for a matching view file in this order:

1. `Views/\[ControllerName]/\[ViewName].cshtml`
2. `Views/Shared/\[ViewName].cshtml`

A view file path can be provided instead of a view name. If using an absolute path starting at the app root (optionally starting with "/" or "\~/"), the `.cshtml` extension must be specified:

```
return View("Views/Home/About.cshtml");
```

You can also use a relative path to specify views in different directories without the `.cshtml` extension. Inside the `HomeController`, you can return the `Index` view of your `Manage` views with a relative path:

```
return View("../Manage/Index");
```

Similarly, you can indicate the current controller-specific directory with the "./" prefix:

```
return View("./About");
```

[Partial views](partial?view=aspnetcore-10.0) and [view components](view-components?view=aspnetcore-10.0) use similar (but not identical) discovery mechanisms.

You can customize the default convention for how views are located within the app by using a custom [IViewLocationExpander](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.iviewlocationexpander).

View discovery relies on finding view files by file name. If the underlying file system is case sensitive, view names are probably case sensitive. For compatibility across operating systems, match case between controller and action names and associated view folders and file names. If you encounter an error that a view file can't be found while working with a case-sensitive file system, confirm that the casing matches between the requested view file and the actual view file name.

Follow the best practice of organizing the file structure for your views to reflect the relationships among controllers, actions, and views for maintainability and clarity.

## Pass data to views

Pass data to views using several approaches:

* Strongly typed data: viewmodel

* Weakly typed data

  * `ViewData` (`ViewDataAttribute`)
  * `ViewBag`

### Strongly-typed data (viewmodel)

The most robust approach is to specify a [model](../models/model-binding?view=aspnetcore-10.0) type in the view. This model is commonly referred to as a *viewmodel*. You pass an instance of the viewmodel type to the view from the action.

Using a viewmodel to pass data to a view allows the view to take advantage of *strong* type checking. *Strong typing* (or *strongly typed*) means that every variable and constant has an explicitly defined type (for example, `string`, `int`, or `DateTime`). The validity of types used in a view is checked at compile time.

[Visual Studio](https://visualstudio.microsoft.com) and [Visual Studio Code](https://code.visualstudio.com/) list strongly typed class members using a feature called [IntelliSense](/en-us/visualstudio/ide/using-intellisense). When you want to see the properties of a viewmodel, type the variable name for the viewmodel followed by a period (`.`). This helps you write code faster with fewer errors.

Specify a model using the `@model` directive. Use the model with `@Model`:

```
@model WebApplication1.ViewModels.Address

<h2>Contact</h2>
<address>
    @Model.Street<br>
    @Model.City, @Model.State @Model.PostalCode<br>
    <abbr title="Phone">P:</abbr> 425.555.0100
</address>
```

To provide the model to the view, the controller passes it as a parameter:

```
public IActionResult Contact()
{
    ViewData["Message"] = "Your contact page.";

    var viewModel = new Address()
    {
        Name = "Microsoft",
        Street = "One Microsoft Way",
        City = "Redmond",
        State = "WA",
        PostalCode = "98052-6399"
    };

    return View(viewModel);
}
```

There are no restrictions on the model types that you can provide to a view. We recommend using Plain Old CLR Object (POCO) viewmodels with little or no behavior (methods) defined. Usually, viewmodel classes are either stored in the `Models` folder or a separate `ViewModels` folder at the root of the app. The `Address` viewmodel used in the previous example is a POCO viewmodel stored in a file named `Address.cs`:

```
namespace WebApplication1.ViewModels
{
    public class Address
    {
        public string Name { get; set; }
        public string Street { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string PostalCode { get; set; }
    }
}
```

Nothing prevents you from using the same classes for both your viewmodel types and your business model types. However, using separate models allows your views to vary independently from the business logic and data access parts of your app. Separation of models and viewmodels also offers security benefits when models use [model binding](../models/model-binding?view=aspnetcore-10.0) and [validation](../models/validation?view=aspnetcore-10.0) for data sent to the app by the user.

[]()

### Weakly typed data (`ViewData`, `[ViewData]` attribute, and `ViewBag`)

`ViewBag` *isn't available by default for use in Razor Pages* `PageModel` *classes.*

In addition to strongly typed views, views have access to a *weakly typed* (also called *loosely typed*) collection of data. Unlike strong types, *weak types* (or *loose types*) means that you don't explicitly declare the type of data you're using. You can use the collection of weakly typed data for passing small amounts of data in and out of controllers and views.

| Passing data between a ...                              | Example                                                                    |
| ------------------------------------------------------- | -------------------------------------------------------------------------- |
| Controller and a view                                   | Populating a dropdown list with data.                                      |
| View and a [layout view](layout?view=aspnetcore-10.0)   | Setting the `<title>` element content in the layout view from a view file. |
| [Partial view](partial?view=aspnetcore-10.0) and a view | A widget that displays data based on the webpage that the user requested.  |

This collection can be referenced through either the `ViewData` or `ViewBag` properties on controllers and views. The `ViewData` property is a dictionary of weakly typed objects. The `ViewBag` property is a wrapper around `ViewData` that provides dynamic properties for the underlying `ViewData` collection. Note: Key lookups are case-insensitive for both `ViewData` and `ViewBag`.

`ViewData` and `ViewBag` are dynamically resolved at runtime. Since they don't offer compile-time type checking, both are generally more error-prone than using a viewmodel. For that reason, some developers prefer to minimally or never use `ViewData` and `ViewBag`.

[]()

#### `ViewData`

`ViewData` is a [ViewDataDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.viewdatadictionary) object accessed through `string` keys. String data can be stored and used directly without the need for a cast, but you must cast other `ViewData` object values to specific types when you extract them. You can use `ViewData` to pass data from controllers to views and within views, including [partial views](partial?view=aspnetcore-10.0) and [layouts](layout?view=aspnetcore-10.0).

The following is an example that sets values for a greeting and an address using `ViewData` in an action:

```
public IActionResult SomeAction()
{
    ViewData["Greeting"] = "Hello";
    ViewData["Address"]  = new Address()
    {
        Name = "Steve",
        Street = "123 Main St",
        City = "Hudson",
        State = "OH",
        PostalCode = "44236"
    };

    return View();
}
```

Work with the data in a view:

```
@{
    // Since Address isn't a string, it requires a cast.
    var address = ViewData["Address"] as Address;
}

@ViewData["Greeting"] World!

<address>
    @address.Name<br>
    @address.Street<br>
    @address.City, @address.State @address.PostalCode
</address>
```

#### `[ViewData]` attribute

Another approach that uses the [ViewDataDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.viewdatadictionary) is [ViewDataAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewdataattribute). Properties on controllers or Razor Page models marked with the `[ViewData]` attribute have their values stored and loaded from the dictionary.

In the following example, the Home controller contains a `Title` property marked with `[ViewData]`. The `About` method sets the title for the About view:

```
public class HomeController : Controller
{
    [ViewData]
    public string Title { get; set; }

    public IActionResult About()
    {
        Title = "About Us";
        ViewData["Message"] = "Your application description page.";

        return View();
    }
}
```

In the layout, the title is read from the ViewData dictionary:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...
```

#### `ViewBag`

`ViewBag` *isn't available by default for use in Razor Pages* `PageModel` *classes.*

`ViewBag` is a `Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.DynamicViewData` object that provides dynamic access to the objects stored in `ViewData`. `ViewBag` can be more convenient to work with, since it doesn't require casting. The following example shows how to use `ViewBag` with the same result as using `ViewData` previously:

```
public IActionResult SomeAction()
{
    ViewBag.Greeting = "Hello";
    ViewBag.Address  = new Address()
    {
        Name = "Steve",
        Street = "123 Main St",
        City = "Hudson",
        State = "OH",
        PostalCode = "44236"
    };

    return View();
}
```

```
@ViewBag.Greeting World!

<address>
    @ViewBag.Address.Name<br>
    @ViewBag.Address.Street<br>
    @ViewBag.Address.City, @ViewBag.Address.State @ViewBag.Address.PostalCode
</address>
```

#### Using `ViewData` and `ViewBag` simultaneously

`ViewBag` *isn't available by default for use in Razor Pages* `PageModel` *classes.*

Since `ViewData` and `ViewBag` refer to the same underlying `ViewData` collection, you can use both `ViewData` and `ViewBag` and mix and match between them when reading and writing values.

Set the title using `ViewBag` and the description using `ViewData` at the top of an `About.cshtml` view:

```
@{
    Layout = "/Views/Shared/_Layout.cshtml";
    ViewBag.Title = "About Contoso";
    ViewData["Description"] = "Let us tell you about Contoso's philosophy and mission.";
}
```

Read the properties but reverse the use of `ViewData` and `ViewBag`. In the `_Layout.cshtml` file, obtain the title using `ViewData` and obtain the description using `ViewBag`:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"]</title>
    <meta name="description" content="@ViewBag.Description">
    ...
```

Remember that strings don't require a cast for `ViewData`. You can use `@ViewData["Title"]` without casting.

Using both `ViewData` and `ViewBag` at the same time works, as does mixing and matching reading and writing the properties. The following markup is rendered:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <title>About Contoso</title>
    <meta name="description" content="Let us tell you about Contoso's philosophy and mission.">
    ...
```

#### Summary of the differences between `ViewData` and `ViewBag`

`ViewBag` *isn't available by default for use in Razor Pages* `PageModel` *classes.*

* `ViewData`

  * Derives from [ViewDataDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.viewdatadictionary), so it has dictionary properties that can be useful, such as `ContainsKey`, `Add`, `Remove`, and `Clear`.
  * Keys in the dictionary are strings, so whitespace is allowed. Example: `ViewData["Some Key With Whitespace"]`
  * Any type other than a `string` must be cast in the view to use `ViewData`.

* `ViewBag`

  * Derives from `Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.DynamicViewData`, so it allows the creation of dynamic properties using dot notation (`@ViewBag.SomeKey = <value or object>`), and no casting is required. The syntax of `ViewBag` makes it quicker to add to controllers and views.
  * Simpler to check for null values. Example: `@ViewBag.Person?.Name`

#### When to use `ViewData` or `ViewBag`

Both `ViewData` and `ViewBag` are equally valid approaches for passing small amounts of data among controllers and views. The choice of which one to use is based on preference. You can mix and match `ViewData` and `ViewBag` objects, however, the code is easier to read and maintain with one approach used consistently. Both approaches are dynamically resolved at runtime and thus prone to causing runtime errors. Some development teams avoid them.

### Dynamic views

Views that don't declare a model type using `@model` but that have a model instance passed to them (for example, `return View(Address);`) can reference the instance's properties dynamically:

```
<address>
    @Model.Street<br>
    @Model.City, @Model.State @Model.PostalCode<br>
    <abbr title="Phone">P:</abbr> 425.555.0100
</address>
```

This feature offers flexibility but doesn't offer compilation protection or IntelliSense. If the property doesn't exist, webpage generation fails at runtime.

## More view features

[Tag Helpers](tag-helpers/intro?view=aspnetcore-10.0) make it easy to add server-side behavior to existing HTML tags. Using Tag Helpers avoids the need to write custom code or helpers within your views. Tag helpers are applied as attributes to HTML elements and are ignored by editors that can't process them. This allows you to edit and render view markup in a variety of tools.

Generating custom HTML markup can be achieved with many built-in HTML Helpers. [View Components](view-components?view=aspnetcore-10.0) can handle more complex user interface logic. View components provide the same SoC that controllers and views offer. They can eliminate the need for actions and views that deal with data used by common user interface elements.

Like many other aspects of ASP.NET Core, views support [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0), allowing services to be [injected into views](dependency-injection?view=aspnetcore-10.0).

## CSS isolation

Isolate CSS styles to individual pages, views, and components to reduce or avoid:

* Dependencies on global styles that can be challenging to maintain.
* Style conflicts in nested content.

To add a *scoped CSS file* for a page or view, place the CSS styles in a companion `.cshtml.css` file matching the name of the `.cshtml` file. In the following example, an `Index.cshtml.css` file supplies CSS styles that are only applied to the `Index.cshtml` page or view.

`Pages/Index.cshtml.css` (Razor Pages) or `Views/Index.cshtml.css` (MVC):

```
h1 {
    color: red;
}
```

CSS isolation occurs at build time. The framework rewrites CSS selectors to match markup rendered by the app's pages or views. The rewritten CSS styles are bundled and produced as a static asset, `{APP ASSEMBLY}.styles.css`. The placeholder `{APP ASSEMBLY}` is the assembly name of the project. A link to the bundled CSS styles is placed in the app's layout.

In the `<head>` content of the app's `Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC), add or confirm the presence of the link to the bundled CSS styles:

```
<link rel="stylesheet" href="~/{APP ASSEMBLY}.styles.css" />
```

In the following example, the app's assembly name is `WebApp`:

```
<link rel="stylesheet" href="WebApp.styles.css" />
```

The styles defined in a scoped CSS file are only applied to the rendered output of the matching file. In the preceding example, any `h1` CSS declarations defined elsewhere in the app don't conflict with the `Index`'s heading style. CSS style cascading and inheritance rules remain in effect for scoped CSS files. For example, styles applied directly to an `<h1>` element in the `Index.cshtml` file override the scoped CSS file's styles in `Index.cshtml.css`.

Note

In order to guarantee CSS style isolation when bundling occurs, importing CSS in Razor code blocks isn't supported.

CSS isolation only applies to HTML elements. CSS isolation isn't supported for [Tag Helpers](tag-helpers/intro?view=aspnetcore-10.0).

Within the bundled CSS file, each page, view, or Razor component is associated with a scope identifier in the format `b-{STRING}`, where the `{STRING}` placeholder is a ten-character string generated by the framework. The following example provides the style for the preceding `<h1>` element in the `Index` page of a Razor Pages app:

```
/* /Pages/Index.cshtml.rz.scp.css */
h1[b-3xxtam6d07] {
    color: red;
}
```

In the `Index` page where the CSS style is applied from the bundled file, the scope identifier is appended as an HTML attribute:

```
<h1 b-3xxtam6d07>
```

The identifier is unique to an app. At build time, a project bundle is created with the convention `{STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css`, where the placeholder `{STATIC WEB ASSETS BASE PATH}` is the static web assets base path.

If other projects are utilized, such as NuGet packages or [Razor class libraries](../../razor-pages/ui-class?view=aspnetcore-10.0), the bundled file:

```
<ItemGroup>
  <None Update="{Pages|Views}/Index.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

In the preceding example, the CSS generated for `Index.cshtml.css` changes its scope identifier from `b-{STRING}` to `custom-scope-identifier`.

Use scope identifiers to achieve inheritance with scoped CSS files. In the following project file example, a `BaseView.cshtml.css` file contains common styles across views. A `DerivedView.cshtml.css` file inherits these styles.

```
<ItemGroup>
  <None Update="{Pages|Views}/BaseView.cshtml.css" CssScope="custom-scope-identifier" />
  <None Update="{Pages|Views}/DerivedView.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

Use the wildcard (`*`) operator to share scope identifiers across multiple files:

```
<ItemGroup>
  <None Update="{Pages|Views}/*.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

### Change base path for static web assets

The scoped CSS file is generated at the root of the app. In the project file, use the `StaticWebAssetBasePath` property to change the default path. The following example places the scoped CSS file, and the rest of the app's assets, at the `_content` path:

```
<PropertyGroup>
  <StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>
```

### Disable automatic bundling

To opt out of how framework publishes and loads scoped files at runtime, use the `DisableScopedCssBundling` property. When using this property, other tools or processes are responsible for taking the isolated CSS files from the `obj` directory and publishing and loading them at runtime:

```
<PropertyGroup>
  <DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>
```

## Razor class library (RCL) support

When a [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0) provides isolated styles, the `<link>` tag's `href` attribute points to `{STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css`, where the placeholders are:

* `{STATIC WEB ASSET BASE PATH}`: The static web asset base path.
* `{PACKAGE ID}`: The library's [package identifier](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties). The package identifier defaults to the project's assembly name if the package identifier isn't specified in the project file.

In the following example:

* The static web asset base path is `_content/ClassLib`.
* The class library's assembly name is `ClassLib`.

`Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC):

```
<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">
```

For more information on RCLs, see the following articles:

* [Reusable Razor UI in class libraries with ASP.NET Core](../../razor-pages/ui-class?view=aspnetcore-10.0)
* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../../blazor/components/class-libraries?view=aspnetcore-10.0)

For information on Blazor CSS isolation, see [ASP.NET Core Blazor CSS isolation](../../blazor/components/css-isolation?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-04

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/memory
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/memory.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Memory management and garbage collection in ASP.NET Core

By [Sébastien Ros](https://github.com/sebastienros) and [Rick Anderson](https://twitter.com/RickAndMSFT)

Memory management is complex, even in a managed framework like .NET. Analyzing and resolving memory issues can be challenging. Memory leaks and garbage collection (GC) issues are commonly due to a lack of understanding about how memory consumption works in .NET, or not understanding the process for measuring usage.

This article demonstrates common memory use patterns that can be problematic, and suggests alternate approaches.

## Explore garbage collection (GC) in .NET

The GC process allocates heap segments where each segment is a contiguous range of memory. Objects placed in the heap are categorized into one of three generations: 0, 1, or 2. The generation determines the frequency of the GC attempts to release memory on managed objects that the app no longer references. The GC addresses the lower-numbered generations more frequently.

Objects are moved from one generation to another based on their lifetime. As objects live longer, they're moved into a higher generation. As mentioned previously, GC runs less often on higher generations. Short-lived objects always remain in Gen 0. For example, objects that are referenced during the life of a web request are short lived. Application level [singletons](../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes) generally migrate to Gen 2.

When an ASP.NET Core app starts, the GC process:

* Reserves some memory for the initial heap segments.
* Commits a small portion of memory when the runtime loads.

The preceding memory allocations are done for performance reasons. The performance benefit comes from heap segments in contiguous memory.

### Review caveats when using GC.Collect

In general, ASP.NET Core apps in production should **not** use the [GC.Collect](/en-us/dotnet/api/system.gc.collect) method explicitly. Inducing garbage collections at suboptimal times can decrease performance significantly.

`GC.Collect` is useful when investigating memory leaks. Calling `GC.Collect()` triggers a blocking garbage collection cycle that tries to reclaim all objects inaccessible from managed code. It's a useful way to understand the size of the reachable live objects in the heap, and track growth of memory size over time.

## Analyze the memory usage of an app

Dedicated tools can help analyze memory usage, including:

* Counting object references.
* Measuring how much effect the GC has on CPU usage.
* Measuring memory space used for each generation.

Use the following tools to analyze memory usage:

* [dotnet-trace utility](/en-us/dotnet/core/diagnostics/dotnet-trace) (can use on production machines)
* [Analyze memory usage without the Visual Studio debugger](/en-us/visualstudio/profiling/memory-usage-without-debugging2)
* [Measure memory usage in Visual Studio](/en-us/visualstudio/profiling/memory-usage)

### Detect memory issues

Task Manager can be used to get an idea of how much memory an ASP.NET app is using. The Task Manager memory value:

* Represents the amount of memory used by the ASP.NET process.
* Includes the app's living objects and other memory consumers such as native memory usage.

If the Task Manager memory value increases indefinitely and never flattens out, the app has a memory leak. The following sections demonstrate and explain several memory usage patterns.

## Explore the sample display memory usage app

The [MemoryLeak sample app](https://github.com/sebastienros/memoryleak) is available on GitHub. The MemoryLeak app:

* Includes a diagnostic controller that gathers real-time memory and GC data for the app.
* Has an Index page that displays the memory and GC data. The Index page is refreshed every second.
* Contains an API controller that provides various memory load patterns.
* Can be used to display memory usage patterns of ASP.NET Core apps, but it isn't a supported tool.

Run MemoryLeak. Allocated memory slowly increases until a GC occurs. Memory increases because the tool allocates a custom object to capture data. The following image shows the MemoryLeak Index page when a Gen 0 GC occurs. The chart shows 0 RPS (Requests per second) because no API endpoints from the API controller have been called.

The chart displays two values for the memory usage:

* **Allocated**: The amount of memory occupied by managed objects.
* **Working Set**: The set of pages in the virtual address space of the process that are currently resident in physical memory. The working set shown is the same value Task Manager displays. For more information, see [Working Set](/en-us/windows/win32/memory/working-set).

### Transient objects

The following API creates a 20-KB String instance and returns it to the client. On each request, a new object is allocated in memory and written to the response. Strings are stored as UTF-16 characters in .NET so each character takes 2 bytes in memory.

```
[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
    return new String('x', 10 * 1024);
}
```

The following chart is generated with a relatively small load that shows how GC affects memory allocations.

The chart illustrates the following details:

* 4K RPS (Requests per second)
* Gen 0 GC collections occur about every 2 seconds
* Constant Working Set, approximately 500 MB
* CPU is 12%
* Stable memory consumption and release (through GC)

The following chart is taken at the max throughput that the machine can handle.

The chart illustrates the following details:

* 22 K RPS
* Gen 0 GC collections occur several times per second
* Gen 1 collections trigger because the app allocates significantly more memory per second
* Constant Working Set, approximately 500 MB
* CPU is 33%
* Stable memory consumption and release (through GC)
* CPU (33%) isn't over-utilized, so GC can keep up with a high number of allocations

### Workstation GC versus Server GC

The .NET Garbage Collector has two different modes:

* **Workstation GC**: Optimized for the desktop.
* **Server GC**: The default GC for ASP.NET Core apps. Optimized for the server.

The GC mode can be set explicitly in the project file or in the *runtimeconfig.json* file of the published app. The following markup shows setting `ServerGarbageCollection` in the project file:

```
<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
```

Changing `ServerGarbageCollection` in the project file requires the app to be rebuilt.

Note

Server garbage collection is **not** available on machines with a single core. For more information, see the [IsServerGC](/en-us/dotnet/api/system.runtime.gcsettings.isservergc#system-runtime-gcsettings-isservergc) property.

The following image shows the memory profile under a 5K RPS using the Workstation GC.

The differences between this chart and the server version are significant:

* Working Set drops from 500 MB to 70 MB
* GC does Gen 0 collections multiple times per second instead of every 2 seconds
* GC drops from 300 MB to 10 MB

On a typical web server environment, CPU usage is more important than memory, therefore the Server GC is better. If memory utilization is high and CPU usage is relatively low, the Workstation GC might be more performant. For example, high density hosting several web apps where memory is scarce.

### GC using Docker and small containers

When multiple containerized apps run on the same machine, Workstation GC might be more performant than Server GC. For more information, see [Running with Server GC in a Small Container (blog)](https://devblogs.microsoft.com/dotnet/running-with-server-gc-in-a-small-container-scenario-part-0/) and [Running with Server GC in a Small Container Scenario Part 1 – Hard Limit for the GC Heap (blog)](https://devblogs.microsoft.com/dotnet/running-with-server-gc-in-a-small-container-scenario-part-1-hard-limit-for-the-gc-heap/).

### Persistent object references

The GC can't free objects that are referenced. Objects that are referenced but no longer needed result in a memory leak. If the app frequently allocates objects and fails to free them after they are no longer needed, memory usage increases over time.

The following API creates a 20-KB String instance and returns it to the client. The difference with the previous example is that a static member references this instance, which means the instance is never available for collection.

```
private static ConcurrentBag<string> _staticStrings = new ConcurrentBag<string>();

[HttpGet("staticstring")]
public ActionResult<string> GetStaticString()
{
    var bigString = new String('x', 10 * 1024);
    _staticStrings.Add(bigString);
    return bigString;
}
```

The preceding code:

* Demonstrates a typical memory leak.
* With frequent calls, causes app memory to increase until the process crashes with an `OutOfMemory` exception.

The chart illustrates the following details:

* Load testing the `/api/staticstring` endpoint causes a linear increase in memory
* GC tries to free memory as memory pressure grows by calling a Gen 2 collection
* GC can't free the leaked memory; Allocated and Working Set increase with time

Some scenarios, such as caching, require object references to be held until memory pressure forces them to be released. The [WeakReference](/en-us/dotnet/api/system.weakreference) class can be used for this type of caching code. A `WeakReference` object is collected under memory pressures. The default implementation of the [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) interface uses `WeakReference`.

### Native memory

Some .NET objects rely on native memory, but native memory **isn't collectable** by the GC. The .NET object that uses native memory must free it by using native code.

.NET provides the [IDisposable](/en-us/dotnet/api/system.idisposable) interface to let developers release native memory. Even if the [Dispose](/en-us/dotnet/api/system.idisposable.dispose) method isn't called, correctly implemented classes call `Dispose` when the [finalizer](/en-us/dotnet/csharp/programming-guide/classes-and-structs/finalizers) runs.

Consider the following code:

```
[HttpGet("fileprovider")]
public void GetFileProvider()
{
    var fp = new PhysicalFileProvider(TempPath);
    fp.Watch("*.*");
}
```

[PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider) is a managed class, so any instance is collected at the end of the request.

The following image shows the memory profile while invoking the `fileprovider` API continuously.

The preceding chart shows an obvious issue with the implementation of this class, as it keeps increasing memory usage. This result is a known problem tracked in [GitHub dotnet/aspnetcore issue #844](https://github.com/dotnet/aspnetcore/issues/844).

The same leak occurs in user code in the following scenarios:

* Not releasing the class correctly
* Forgetting to invoke the `Dispose` method of the dependent objects that should be disposed

### Large object heap

Frequent memory allocation/free cycles result in fragmented memory, especially when allocating large chunks of memory. Objects are allocated in contiguous blocks of memory. To mitigate fragmentation, when the GC frees memory, it tries to defragment it. This process is called **compaction**. Compaction involves moving objects. Moving large objects imposes a performance penalty. For this reason, the GC creates a special memory zone for *large* objects, called the [large object heap](/en-us/dotnet/standard/garbage-collection/large-object-heap) (LOH). Objects that are greater than 85,000 bytes (approximately 83 KB) are:

* Placed on the LOH
* Not compacted
* Processed during Gen 2 collection

When the LOH is full, the GC triggers a Gen 2 collection.

* Gen 2 collections are inherently slow.
* They can incur the cost of triggering a collection on all other generations.

The following code compacts the LOH immediately:

```
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
```

For information on compacting the LOH, see the [LargeObjectHeapCompactionMode](/en-us/dotnet/api/system.runtime.gcsettings.largeobjectheapcompactionmode#system-runtime-gcsettings-largeobjectheapcompactionmode) property.

In containers that use .NET Core 3.0 or later, the LOH is automatically compacted.

The following API that illustrates this behavior:

```
[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
   return new byte[size].Length;
}
```

The following chart shows the memory profile of calling the `/api/loh/84975` endpoint, under maximum load:

The following chart shows the memory profile of calling the `/api/loh/84976` endpoint, allocating *just one more byte*:

Note

The `byte[]` structure has overhead bytes, which is why 84,976 bytes triggers the 85,000 limit.

Comparing the two preceding charts:

* Working Set is similar for both scenarios, about 450 MB
* Under LOH requests (84,975 bytes) show mostly Gen 0 collections
* Over LOH requests generate constant Gen 2 collections, which are expensive. More CPU is required and throughput drops \~50%.

Temporary large objects are problematic because they cause Gen 2 collections.

For maximum performance, minimize the use of large objects. If possible, split up large objects. For example, [response caching middleware](caching/response?view=aspnetcore-10.0) in ASP.NET Core splits the cache entries into blocks less than 85,000 bytes.

The following links show the ASP.NET Core approach to keeping objects under the LOH limit:

* [ResponseCaching/Streams/StreamUtilities.cs](https://github.com/dotnet/AspNetCore/blob/v3.0.0/src/Middleware/ResponseCaching/src/Streams/StreamUtilities.cs#L16)
* [ResponseCaching/MemoryResponseCache.cs](https://github.com/aspnet/ResponseCaching/blob/c1cb7576a0b86e32aec990c22df29c780af29ca5/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs#L55)

For more information, see:

* [Large Object Heap Uncovered](https://devblogs.microsoft.com/dotnet/large-object-heap-uncovered-from-an-old-msdn-article/)
* [Large object heap](/en-us/dotnet/standard/garbage-collection/large-object-heap)

### HttpClient

Incorrectly using the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) class can result in a resource leak.

System resources (such as database connections, sockets, file handles, and so on) present two issues:

* They're more scarce than memory.
* They're more problematic when leaked than memory.

Experienced .NET developers know to call the [Dispose](/en-us/dotnet/api/system.idisposable.dispose) method on objects that implement the [IDisposable](/en-us/dotnet/api/system.idisposable) interface. Not disposing objects that implement `IDisposable` typically results in leaked memory or leaked system resources.

`HttpClient` implements `IDisposable`, but should **not** be disposed on every invocation. Rather, `HttpClient` should be reused.

The following endpoint creates and disposes a new `HttpClient` instance on every request:

```
[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
    using (var httpClient = new HttpClient())
    {
        var result = await httpClient.GetAsync(url);
        return (int)result.StatusCode;
    }
}
```

Under load, the following error messages are logged:

```
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HLG70PBE1CR1", Request id "0HLG70PBE1CR1:00000031":
      An unhandled exception was thrown by the application.
System.Net.Http.HttpRequestException: Only one usage of each socket address
    (protocol/network address/port) is normally permitted --->
    System.Net.Sockets.SocketException: Only one usage of each socket address
    (protocol/network address/port) is normally permitted
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port,
    CancellationToken cancellationToken)
```

Even though the `HttpClient` instances are disposed, it takes some time for the operating system to release the actual network connection. The process of continuously creating new connections results in *ports exhaustion*. Each client connection requires its own client port.

One way to prevent port exhaustion is to reuse the same `HttpClient` instance:

```
private static readonly HttpClient _httpClient = new HttpClient();

[HttpGet("httpclient2")]
public async Task<int> GetHttpClient2(string url)
{
    var result = await _httpClient.GetAsync(url);
    return (int)result.StatusCode;
}
```

The `HttpClient` instance is released when the app stops. This example shows that not every disposable resource should be disposed after each use.

The following articles describe a better way to handle the lifetime of an `HttpClient` instance:

* [HttpClient and lifetime management](../fundamentals/http-requests?view=aspnetcore-10.0#httpclient-and-lifetime-management)
* [HTTPClient factory (blog)](https://devblogs.microsoft.com/dotnet/asp-net-core-2-1-preview1-introducing-httpclient-factory/)

### Object pooling

The previous example showed how the `HttpClient` instance can be made static and reused by all requests. Reuse prevents running out of resources.

Object pooling is an alternative:

* It uses the reuse pattern.
* The design is ideal for objects that are expensive to create.

A pool is a collection of preinitialized objects that can be reserved and released across threads. Pools can define allocation rules such as limits, predefined sizes, or growth rate.

The NuGet package [Microsoft.Extensions.ObjectPool](https://www.nuget.org/packages/Microsoft.Extensions.ObjectPool/) contains classes that help to manage such pools.

The following API endpoint instantiates a `byte` buffer that is filled with random numbers on each request:

```
        [HttpGet("array/{size}")]
        public byte[] GetArray(int size)
        {
            var random = new Random();
            var array = new byte[size];
            random.NextBytes(array);

            return array;
        }
```

The following chart displays calling the preceding API with moderate load:

The chart reveals that Gen 0 collections happen about once per second.

The code can be optimized by pooling the `byte` buffer by using the [ArrayPool\<T>](/en-us/dotnet/api/system.buffers.arraypool-1) class. A static instance is reused across requests.

What's different with this approach is that a pooled object is returned from the API:

* The object is out of your control as soon as you return from the method.
* You can't release the object.

To set up disposal of the object:

* Encapsulate the pooled array in a disposable object.
* Register the pooled object by using the [HttpContext.Response.RegisterForDispose](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.registerfordispose) method.

`RegisterForDispose` takes care of calling `Dispose` on the target object, so the object is released only after the HTTP request completes.

```
private static ArrayPool<byte> _arrayPool = ArrayPool<byte>.Create();

private class PooledArray : IDisposable
{
    public byte[] Array { get; private set; }

    public PooledArray(int size)
    {
        Array = _arrayPool.Rent(size);
    }

    public void Dispose()
    {
        _arrayPool.Return(Array);
    }
}

[HttpGet("pooledarray/{size}")]
public byte[] GetPooledArray(int size)
{
    var pooledArray = new PooledArray(size);

    var random = new Random();
    random.NextBytes(pooledArray.Array);

    HttpContext.Response.RegisterForDispose(pooledArray);

    return pooledArray.Array;
}
```

Applying the same load as the nonpooled version results in the following chart:

The main difference is allocated bytes, and as a consequence, fewer Gen 0 collections.

## Related content

* [ASP.NET Core Blazor performance best practices](../blazor/performance/?view=aspnetcore-10.0)
* [Garbage collection](/en-us/dotnet/standard/garbage-collection/)
* [Understanding different GC modes with Concurrency Visualizer (blog)](https://devblogs.microsoft.com/premier-developer/understanding-different-gc-modes-with-concurrency-visualizer/)
* [Large Object Heap Uncovered (blog)](https://devblogs.microsoft.com/dotnet/large-object-heap-uncovered-from-an-old-msdn-article/)
* [Large object heap on Windows systems](/en-us/dotnet/standard/garbage-collection/large-object-heap)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/deadlines-cancellation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/deadlines-cancellation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Reliable gRPC services with deadlines and cancellation

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

Deadlines and cancellation are features used by gRPC clients to abort in-progress calls. This article discusses why deadlines and cancellation are important, and how to use them in .NET gRPC apps.

## Deadlines

A deadline allows a gRPC client to specify how long it will wait for a call to complete. When a deadline is exceeded, the call is canceled. Setting a deadline is important because it provides an upper limit on how long a call can run for. It stops misbehaving services from running forever and exhausting server resources. Deadlines are a useful tool for building reliable apps and should be configured.

Deadline configuration:

* A deadline is configured using `CallOptions.Deadline` when a call is made.
* There is no default deadline value. gRPC calls aren't time limited unless a deadline is specified.
* A deadline is the UTC time of when the deadline is exceeded. For example, `DateTime.UtcNow.AddSeconds(5)` is a deadline of 5 seconds from now.
* If a past or current time is used then the call immediately exceeds the deadline.
* The deadline is sent with the gRPC call to the service and is independently tracked by both the client and the service. It is possible that a gRPC call completes on one machine, but by the time the response has returned to the client the deadline has been exceeded.

If a deadline is exceeded, the client and service have different behavior:

* The client immediately aborts the underlying HTTP request and throws a `DeadlineExceeded` error. The client app can choose to catch the error and display a timeout message to the user.
* On the server, the executing HTTP request is aborted and [ServerCallContext.CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) is raised. Although the HTTP request is aborted, the gRPC call continues to run on the server until the method completes. It's important that the cancellation token is passed to async methods so they are cancelled along with the call. For example, passing a cancellation token to async database queries and HTTP requests. Passing a cancellation token allows the canceled call to complete quickly on the server and free up resources for other calls.

Configure `CallOptions.Deadline` to set a deadline for a gRPC call:

```
var client = new Greet.GreeterClient(channel);

try
{
    var response = await client.SayHelloAsync(
        new HelloRequest { Name = "World" },
        deadline: DateTime.UtcNow.AddSeconds(5));
    
    // Greeting: Hello World
    Console.WriteLine("Greeting: " + response.Message);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
    Console.WriteLine("Greeting timeout.");
}
```

Using `ServerCallContext.CancellationToken` in a gRPC service:

```
public override async Task<HelloReply> SayHello(HelloRequest request,
    ServerCallContext context)
{
    var user = await _databaseContext.GetUserAsync(request.Name,
        context.CancellationToken);

    return new HelloReply { Message = "Hello " + user.DisplayName };
}
```

### Deadlines and retries

When a gRPC call is configured with [retry fault handling](retries?view=aspnetcore-10.0) and a deadline, the deadline tracks time across all retries for a gRPC call. If the deadline is exceeded, a gRPC call immediately aborts the underlying HTTP request, skips any remaining retries, and throws a `DeadlineExceeded` error.

### Propagating deadlines

When a gRPC call is made from an executing gRPC service, the deadline should be propagated. For example:

1. Client app calls `FrontendService.GetUser` with a deadline.
2. `FrontendService` calls `UserService.GetUser`. The deadline specified by the client should be specified with the new gRPC call.
3. `UserService.GetUser` receives the deadline. It correctly times-out if the client app's deadline is exceeded.

The call context provides the deadline with `ServerCallContext.Deadline`:

```
public override async Task<UserResponse> GetUser(UserRequest request,
    ServerCallContext context)
{
    var client = new User.UserServiceClient(_channel);
    var response = await client.GetUserAsync(
        new UserRequest { Id = request.Id },
        deadline: context.Deadline);

    return response;
}
```

Manually propagating deadlines can be cumbersome. The deadline needs to be passed to every call, and it's easy to accidentally miss. An automatic solution is available with gRPC client factory. Specifying `EnableCallContextPropagation`:

* Automatically propagates the deadline and cancellation token to child calls.
* Doesn't propagate the deadline if the child call specifies a smaller deadline. For example, a propagated deadline of 10 seconds isn't used if a child call specifies a new deadline of 5 seconds using `CallOptions.Deadline`. When multiple deadlines are available, the smallest deadline is used.
* Is an excellent way of ensuring that complex, nested gRPC scenarios always propagate the deadline and cancellation.

```
services
    .AddGrpcClient<User.UserServiceClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .EnableCallContextPropagation();
```

For more information, see [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0#deadline-and-cancellation-propagation).

## Cancellation

Cancellation allows a gRPC client to cancel long running calls that are no longer needed. For example, a gRPC call that streams realtime updates is started when the user visits a page on a website. The stream should be canceled when the user navigates away from the page.

A gRPC call can be canceled in the client by passing a cancellation token with [CallOptions.CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) or calling `Dispose` on the call.

```
private AsyncServerStreamingCall<HelloReply> _call;

public void StartStream()
{
    _call = client.SayHellos(new HelloRequest { Name = "World" });

    // Read response in background task.
    _ = Task.Run(async () =>
    {
        await foreach (var response in _call.ResponseStream.ReadAllAsync())
        {
            Console.WriteLine("Greeting: " + response.Message);
        }
    });
}

public void StopStream()
{
    _call.Dispose();
}
```

gRPC services that can be cancelled should:

* Pass `ServerCallContext.CancellationToken` to async methods. Canceling async methods allows the call on the server to complete quickly.
* Propagate the cancellation token to child calls. Propagating the cancellation token ensures that child calls are canceled with their parent. [gRPC client factory](clientfactory?view=aspnetcore-10.0) and `EnableCallContextPropagation()` automatically propagates the cancellation token.

## Related content

* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)
* [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/azure-ad-b2c
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/azure-ad-b2c.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Cloud authentication with Azure Active Directory B2C in ASP.NET Core

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

## Recommended path for new projects

[Microsoft Entra External ID for customers](/en-us/entra/external-id/customers/overview-customers-ciam) is the recommended customer identity and access management (CIAM) solution for new ASP.NET Core applications, replacing Azure AD B2C with the latest identity platform features.

To get started with a new project, see [Sign in users in a sample ASP.NET Core web app](/en-us/entra/external-id/customers/sample-web-app-dotnet-sign-in).

## Migrate from Azure AD B2C to Entra External ID

To migrate an existing Azure AD B2C application to the newer platform, see [Plan and execute a migration to Microsoft Entra External ID](/en-us/entra/external-id/customers/migrate-to-external-id).

For detailed guidance on moving user accounts, including password migration strategies, see [Migrating users to Microsoft Entra External ID](/en-us/entra/external-id/customers/how-to-migrate-users).

## Guidance for existing Azure AD B2C projects

Azure AD B2C remains supported for existing applications. Authoritative setup and configuration guidance is maintained in the Azure AD B2C documentation. The following articles cover the topics that ASP.NET Core developers typically need:

| Topic                                           | Article                                                                                                                                |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Create a tenant                                 | [Tutorial: Create an Azure AD B2C tenant](/en-us/azure/active-directory-b2c/tutorial-create-tenant)                                    |
| Register a web application                      | [Tutorial: Register a web application in Azure AD B2C](/en-us/azure/active-directory-b2c/tutorial-register-applications)               |
| Configure authentication in an ASP.NET Core app | [Configure authentication in a sample ASP.NET Core web app](/en-us/azure/active-directory-b2c/configure-authentication-sample-web-app) |
| Enable multi-factor authentication              | [Enable MFA in Azure AD B2C](/en-us/azure/active-directory-b2c/multi-factor-authentication)                                            |

## ASP.NET Core integration checklist

After completing identity provider setup using the Entra or Azure AD B2C documentation, complete the following ASP.NET Core-specific steps:

1. **Install NuGet packages:** Add the [`Microsoft.Identity.Web`](https://www.nuget.org/packages/Microsoft.Identity.Web) and [`Microsoft.Identity.Web.UI`](https://www.nuget.org/packages/Microsoft.Identity.Web.UI) packages:

   ```
   dotnet add package Microsoft.Identity.Web
   dotnet add package Microsoft.Identity.Web.UI
   ```

2. **Configure services in `Program.cs`:** Call `AddMicrosoftIdentityWebApp` to configure OpenID Connect authentication for the Microsoft identity platform, and call `AddMicrosoftIdentityUI` to add the required sign-in/sign-out UI components. For the full API reference, see the [Microsoft Identity Web documentation](https://github.com/AzureAD/microsoft-identity-web/wiki).

3. **Add configuration:** Add the `AzureADB2C` section in `appsettings.json` with values that match your tenant and app registration. See [Configure authentication in a sample ASP.NET Core web app](/en-us/azure/active-directory-b2c/configure-authentication-sample-web-app) for the complete schema and sample.

## Related content

* [Microsoft Entra External ID for customers overview](/en-us/entra/external-id/customers/overview-customers-ciam)
* [Azure AD B2C documentation](/en-us/azure/active-directory-b2c/)
* [Customize the Azure AD B2C user interface](/en-us/azure/active-directory-b2c/customize-ui)
* [Configure password complexity in Azure AD B2C](/en-us/azure/active-directory-b2c/password-complexity)
* [Microsoft Graph API with Azure AD B2C](/en-us/azure/active-directory-b2c/microsoft-graph-operations)
* [Overview of ASP.NET Core Authentication](./?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/partial
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/partial.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Partial views in ASP.NET Core

By [Steve Smith](https://ardalis.com/), [Maher JENDOUBI](https://twitter.com/maherjend), [Rick Anderson](https://twitter.com/RickAndMSFT), and [Scott Sauber](https://twitter.com/scottsauber)

A partial view is a [Razor](razor?view=aspnetcore-10.0) markup file (`.cshtml`) without a [`@page`](razor?view=aspnetcore-10.0#page) directive that renders HTML output *within* another markup file's rendered output.

The term *partial view* is used when developing either an MVC app, where markup files are called *views*, or a Razor Pages app, where markup files are called *pages*. This article generically refers to MVC views and Razor Pages pages as *markup files*.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/partial/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## When to use partial views

Partial views are an effective way to:

* Break up large markup files into smaller components.

  In a large, complex markup file composed of several logical pieces, there's an advantage to working with each piece isolated into a partial view. The code in the markup file is manageable because the markup only contains the overall page structure and references to partial views.

* Reduce the duplication of common markup content across markup files.

  When the same markup elements are used across markup files, a partial view removes the duplication of markup content into one partial view file. When the markup is changed in the partial view, it updates the rendered output of the markup files that use the partial view.

Partial views shouldn't be used to maintain common layout elements. Common layout elements should be specified in [\_Layout.cshtml](layout?view=aspnetcore-10.0) files.

Don't use a partial view where complex rendering logic or code execution is required to render the markup. Instead of a partial view, use a [view component](view-components?view=aspnetcore-10.0).

## Declare partial views

A partial view is a `.cshtml` markup file without an [`@page`](razor?view=aspnetcore-10.0#page) directive maintained within the *Views* folder (MVC) or *Pages* folder (Razor Pages).

In ASP.NET Core MVC, a controller's [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult) is capable of returning either a view or a partial view. In Razor Pages, a [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) can return a partial view represented as a [PartialViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.partialviewresult) object. Referencing and rendering partial views is described in the [Reference a partial view](#reference-a-partial-view) section.

Unlike MVC view or page rendering, a partial view doesn't run `_ViewStart.cshtml`. For more information on `_ViewStart.cshtml`, see [Layout in ASP.NET Core](layout?view=aspnetcore-10.0).

Partial view file names often begin with an underscore (`_`). This naming convention isn't required, but it helps to visually differentiate partial views from views and pages.

A partial view is a `.cshtml` markup file maintained within the *Views* folder.

A controller's [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult) is capable of returning either a view or a partial view. Referencing and rendering partial views is described in the [Reference a partial view](#reference-a-partial-view) section.

Unlike MVC view rendering, a partial view doesn't run `_ViewStart.cshtml`. For more information on `_ViewStart.cshtml`, see [Layout in ASP.NET Core](layout?view=aspnetcore-10.0).

Partial view file names often begin with an underscore (`_`). This naming convention isn't required, but it helps to visually differentiate partial views from views.

## Reference a partial view

### Use a partial view in a Razor Pages PageModel

In ASP.NET Core 2.0 or 2.1, the following handler method renders the *\_AuthorPartialRP.cshtml* partial view to the response:

```
public IActionResult OnGetPartial() =>
    new PartialViewResult
    {
        ViewName = "_AuthorPartialRP",
        ViewData = ViewData,
    };
```

In ASP.NET Core 2.2 or later, a handler method can alternatively call the [Partial](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagebase.partial) method to produce a `PartialViewResult` object:

```
public IActionResult OnGetPartial() =>
    Partial("_AuthorPartialRP");
```

### Use a partial view in a markup file

Within a markup file, there are several ways to reference a partial view. We recommend that apps use one of the following asynchronous rendering approaches:

* [Partial Tag Helper](#partial-tag-helper)
* [Asynchronous HTML Helper](#asynchronous-html-helper)

Within a markup file, there are two ways to reference a partial view:

* [Asynchronous HTML Helper](#asynchronous-html-helper)
* [Synchronous HTML Helper](#synchronous-html-helper)

We recommend that apps use the [Asynchronous HTML Helper](#asynchronous-html-helper).

### Partial Tag Helper

The [Partial Tag Helper](tag-helpers/built-in/partial-tag-helper?view=aspnetcore-10.0) requires ASP.NET Core 2.1 or later.

The Partial Tag Helper renders content asynchronously and uses an HTML-like syntax:

```
<partial name="_PartialName" />
```

When a file extension is present, the Tag Helper references a partial view that must be in the same folder as the markup file calling the partial view:

```
<partial name="_PartialName.cshtml" />
```

The following example references a partial view from the app root. Paths that start with a tilde-slash (`~/`) or a slash (`/`) refer to the app root:

**Razor Pages**

```
<partial name="~/Pages/Folder/_PartialName.cshtml" />
<partial name="/Pages/Folder/_PartialName.cshtml" />
```

**MVC**

```
<partial name="~/Views/Folder/_PartialName.cshtml" />
<partial name="/Views/Folder/_PartialName.cshtml" />
```

The following example references a partial view with a relative path:

```
<partial name="../Account/_PartialName.cshtml" />
```

For more information, see [Partial Tag Helper in ASP.NET Core](tag-helpers/built-in/partial-tag-helper?view=aspnetcore-10.0).

### Asynchronous HTML Helper

When using an HTML Helper, the best practice is to use [PartialAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.partialasync). `PartialAsync` returns an [IHtmlContent](/en-us/dotnet/api/microsoft.aspnetcore.html.ihtmlcontent) type wrapped in a [Task\<TResult>](/en-us/dotnet/api/system.threading.tasks.task-1). The method is referenced by prefixing the awaited call with an `@` character:

```
@await Html.PartialAsync("_PartialName")
```

When the file extension is present, the HTML Helper references a partial view that must be in the same folder as the markup file calling the partial view:

```
@await Html.PartialAsync("_PartialName.cshtml")
```

The following example references a partial view from the app root. Paths that start with a tilde-slash (`~/`) or a slash (`/`) refer to the app root:

**Razor Pages**

```
@await Html.PartialAsync("~/Pages/Folder/_PartialName.cshtml")
@await Html.PartialAsync("/Pages/Folder/_PartialName.cshtml")
```

**MVC**

```
@await Html.PartialAsync("~/Views/Folder/_PartialName.cshtml")
@await Html.PartialAsync("/Views/Folder/_PartialName.cshtml")
```

The following example references a partial view with a relative path:

```
@await Html.PartialAsync("../Account/_LoginPartial.cshtml")
```

Alternatively, you can render a partial view with [RenderPartialAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.renderpartialasync). This method doesn't return an [IHtmlContent](/en-us/dotnet/api/microsoft.aspnetcore.html.ihtmlcontent). It streams the rendered output directly to the response. Because the method doesn't return a result, it must be called within a Razor code block:

```
@{
    await Html.RenderPartialAsync("_AuthorPartial");
}
```

Since `RenderPartialAsync` streams rendered content, it provides better performance in some scenarios. In performance-critical situations, benchmark the page using both approaches and use the approach that generates a faster response.

### Synchronous HTML Helper

[Partial](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.partial) and [RenderPartial](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.renderpartial) are the synchronous equivalents of `PartialAsync` and `RenderPartialAsync`, respectively. The synchronous equivalents aren't recommended because there are scenarios in which they deadlock. The synchronous methods are targeted for removal in a future release.

Important

If you need to execute code, use a [view component](view-components?view=aspnetcore-10.0) instead of a partial view.

Calling `Partial` or `RenderPartial` results in a Visual Studio analyzer warning. For example, the presence of `Partial` yields the following warning message:

> Use of IHtmlHelper.Partial may result in application deadlocks. Consider using \<partial> Tag Helper or IHtmlHelper.PartialAsync.

Replace calls to `@Html.Partial` with `@await Html.PartialAsync` or the [Partial Tag Helper](tag-helpers/built-in/partial-tag-helper?view=aspnetcore-10.0). For more information on Partial Tag Helper migration, see [Migrate from an HTML Helper](tag-helpers/built-in/partial-tag-helper?view=aspnetcore-10.0#migrate-from-an-html-helper).

## Partial view discovery

When a partial view is referenced by name without a file extension, the following locations are searched in the stated order:

**Razor Pages**

1. Currently executing page's folder
2. Directory graph above the page's folder
3. `/Shared`
4. `/Pages/Shared`
5. `/Views/Shared`

**MVC**

1. `/Areas/<Area-Name>/Views/<Controller-Name>`
2. `/Areas/<Area-Name>/Views/Shared`
3. `/Views/Shared`
4. `/Pages/Shared`

1) `/Areas/<Area-Name>/Views/<Controller-Name>`
2) `/Areas/<Area-Name>/Views/Shared`
3) `/Views/Shared`

The following conventions apply to partial view discovery:

* Different partial views with the same file name are allowed when the partial views are in different folders.
* When referencing a partial view by name without a file extension and the partial view is present in both the caller's folder and the *Shared* folder, the partial view in the caller's folder supplies the partial view. If the partial view isn't present in the caller's folder, the partial view is provided from the *Shared* folder. Partial views in the *Shared* folder are called *shared partial views* or *default partial views*.
* Partial views can be *chained*—a partial view can call another partial view if a circular reference isn't formed by the calls. Relative paths are always relative to the current file, not to the root or parent of the file.

Note

A [Razor](razor?view=aspnetcore-10.0) `section` defined in a partial view is invisible to parent markup files. The `section` is only visible to the partial view in which it's defined.

## Access data from partial views

When a partial view is instantiated, it receives a *copy* of the parent's `ViewData` dictionary. Updates made to the data within the partial view aren't persisted to the parent view. `ViewData` changes in a partial view are lost when the partial view returns.

The following example demonstrates how to pass an instance of [ViewDataDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.viewdatadictionary) to a partial view:

```
@await Html.PartialAsync("_PartialName", customViewData)
```

You can pass a model into a partial view. The model can be a custom object. You can pass a model with `PartialAsync` (renders a block of content to the caller) or `RenderPartialAsync` (streams the content to the output):

```
@await Html.PartialAsync("_PartialName", model)
```

**Razor Pages**

The following markup in the sample app is from the `Pages/ArticlesRP/ReadRP.cshtml` page. The page contains two partial views. The second partial view passes in a model and `ViewData` to the partial view. The `ViewDataDictionary` constructor overload is used to pass a new `ViewData` dictionary while retaining the existing `ViewData` dictionary.

```
@model ReadRPModel

<h2>@Model.Article.Title</h2>
@* Pass the author's name to Pages\Shared\_AuthorPartialRP.cshtml *@
@await Html.PartialAsync("../Shared/_AuthorPartialRP", Model.Article.AuthorName)
@Model.Article.PublicationDate

@* Loop over the Sections and pass in a section and additional ViewData to 
   the strongly typed Pages\ArticlesRP\_ArticleSectionRP.cshtml partial view. *@
@{
    var index = 0;

    foreach (var section in Model.Article.Sections)
    {
        await Html.PartialAsync("_ArticleSectionRP", 
                                section,
                                new ViewDataDictionary(ViewData)
                                {
                                    { "index", index }
                                });

        index++;
    }
}
```

`Pages/Shared/_AuthorPartialRP.cshtml` is the first partial view referenced by the `ReadRP.cshtml` markup file:

```
@model string
<div>
    <h3>@Model</h3>
    This partial view from /Pages/Shared/_AuthorPartialRP.cshtml.
</div>
```

`Pages/ArticlesRP/_ArticleSectionRP.cshtml` is the second partial view referenced by the `ReadRP.cshtml` markup file:

```
@using PartialViewsSample.ViewModels
@model ArticleSection

<h3>@Model.Title Index: @ViewData["index"]</h3>
<div>
    @Model.Content
</div>
```

**MVC**

The following markup in the sample app shows the `Views/Articles/Read.cshtml` view. The view contains two partial views. The second partial view passes in a model and `ViewData` to the partial view. The `ViewDataDictionary` constructor overload is used to pass a new `ViewData` dictionary while retaining the existing `ViewData` dictionary.

```
@model PartialViewsSample.ViewModels.Article

<h2>@Model.Title</h2>
@* Pass the author's name to Views\Shared\_AuthorPartial.cshtml *@
@await Html.PartialAsync("_AuthorPartial", Model.AuthorName)
@Model.PublicationDate

@* Loop over the Sections and pass in a section and additional ViewData to 
   the strongly typed Views\Articles\_ArticleSection.cshtml partial view. *@
@{
    var index = 0;

    foreach (var section in Model.Sections)
    {
        @(await Html.PartialAsync("_ArticleSection", 
                                section,
                                new ViewDataDictionary(ViewData)
                                {
                                    { "index", index }
                                }))

        index++;
    }
}
```

`Views/Shared/_AuthorPartial.cshtml` is the first partial view referenced by the `Read.cshtml` markup file:

```
@model string
<div>
    <h3>@Model</h3>
    This partial view from /Views/Shared/_AuthorPartial.cshtml.
</div>
```

`Views/Articles/_ArticleSection.cshtml` is the second partial view referenced by the `Read.cshtml` markup file:

```
@using PartialViewsSample.ViewModels
@model ArticleSection

<h3>@Model.Title Index: @ViewData["index"]</h3>
<div>
    @Model.Content
</div>
```

At runtime, the partials are rendered into the parent markup file's rendered output, which itself is rendered within the shared `_Layout.cshtml`. The first partial view renders the article author's name and publication date:

> Abraham Lincoln
>
> This partial view from \<shared partial view file path>. 11/19/1863 12:00:00 AM

The second partial view renders the article's sections:

> Section One Index: 0
>
> Four score and seven years ago ...
>
> Section Two Index: 1
>
> Now we are engaged in a great civil war, testing ...
>
> Section Three Index: 2
>
> But, in a larger sense, we can not dedicate ...

## Related content

* [Razor syntax reference for ASP.NET Core](razor?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](tag-helpers/intro?view=aspnetcore-10.0)
* [Partial Tag Helper in ASP.NET Core](tag-helpers/built-in/partial-tag-helper?view=aspnetcore-10.0)
* [View components in ASP.NET Core](view-components?view=aspnetcore-10.0)
* [Areas in ASP.NET Core](../controllers/areas?view=aspnetcore-10.0)

- [Razor syntax reference for ASP.NET Core](razor?view=aspnetcore-10.0)
- [View components in ASP.NET Core](view-components?view=aspnetcore-10.0)
- [Areas in ASP.NET Core](../controllers/areas?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/windowsauth.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure Windows Authentication in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Kirk Larkin](https://twitter.com/serpent5)

Windows Authentication (also known as Negotiate, Kerberos, or NTLM authentication) can be configured for ASP.NET Core apps hosted with [IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0), [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0), or [HTTP.sys](../../fundamentals/servers/httpsys?view=aspnetcore-10.0).

Windows Authentication relies on the operating system to authenticate users of ASP.NET Core apps. Windows Authentication is used for servers that run on a corporate network using Active Directory domain identities or Windows accounts to identify users. Windows Authentication is best suited to intranet environments where users, client apps, and web servers belong to the same Windows domain.

## When to use Windows Authentication

Windows Authentication is suitable for web applications that operate within an organization's private internal network, accessible only to employees (and other authorized users) within the same network. The user management is done within Active Directory (AD), and users use their existing Windows domain account to authenticate.

Windows Authentication provides several benefits for intranet applications:

* **Seamless user experience** - Users are automatically authenticated based on their active Windows session, or are prompted to enter their Windows credentials via a standard browser dialog box.
* **Integration with Active Directory** - Leverages existing Windows infrastructure and security policies, including user groups, account lockouts, and multi-factor authentication (MFA).
* **Secure credential handling** - Authentication is handled through secure protocols like Kerberos, with no need to manage separate user credentials.
* **Role-based authorization** - Applications can access user and group information from Active Directory, enabling role-based access control (RBAC) within the application.
* **Reduced administrative overhead** - No need to maintain a separate user database or credential management system.

This makes Windows Authentication ideal for organizations that want to make use of their existing Windows infrastructure, such as intranet portals.

Note

Windows Authentication isn't supported with HTTP/2. While authentication challenges can be sent over HTTP/2 responses, the client must downgrade to HTTP/1.1 to complete the authentication process. This is a protocol limitation, not a deprecation of Windows Authentication. Once authenticated, normal HTTP/2 communication can resume for subsequent requests.

For public-facing applications Windows Authentication isn't recommended due to security and usability concerns. These reasons include:

* Windows Authentication is best kept internal to protect Active Directory, exposing it outside an internal network introduces security risks.
* External users don't have Windows domain accounts.
* It's complex to configure the necessary network infrastructure securely, and firewalls or proxies may interfere with the authentication process.
* It's not cross-platform and doesn't provide customization options for designs and user experiences.

### Alternatives for different scenarios

Depending on your application requirements, consider these alternatives:

**For public-facing applications:**

* [OpenID Connect](../how-to-choose-identity-solution?view=aspnetcore-10.0) with external identity providers
* ASP.NET Core Identity with local user accounts ([Introduction to Identity on ASP.NET Core](identity?view=aspnetcore-10.0))
* Azure Active Directory (AAD) for Microsoft 365 environments

**For mixed environments** with both intranet and external users:

* Active Directory Federation Services (ADFS) with OpenID Connect
* Azure Active Directory with hybrid configuration

**For corporate environments using modern authentication:**

* Azure Active Directory with single sign-on
* SAML-based solutions with third-party identity providers

## Proxy and load balancer scenarios

Windows Authentication is a stateful scenario primarily used in an intranet, where a proxy or load balancer doesn't usually handle traffic between clients and servers. If a proxy or load balancer is used, Windows Authentication only works if the proxy or load balancer:

* Handles the authentication.
* Passes the user authentication information to the app (for example, in a request header), which acts on the authentication information.

An alternative to Windows Authentication in environments where proxies and load balancers are used is Active Directory Federated Services (ADFS) with OpenID Connect (OIDC).

## IIS/IIS Express

Add the [`Microsoft.AspNetCore.Authentication.Negotiate` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Negotiate) and authentication services by calling [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) in `Program.cs`:

```
using Microsoft.AspNetCore.Authentication.Negotiate;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages();

var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code was generated by the ASP.NET Core Razor Pages template with **Windows Authentication** specified.

### Launch settings (debugger)

Configuration for launch settings only affects the `Properties/launchSettings.json` file for IIS Express and doesn't configure IIS for Windows Authentication. Server configuration is explained in the [IIS](#iis) section.

The **Web Application** templates available via Visual Studio or the .NET CLI can be configured to support Windows Authentication, which updates the `Properties/launchSettings.json` file automatically.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

**New project**

Create a new Razor Pages or MVC app. In the **Additional information** dialog, set the **Authentication type** to **Windows**.

Run the app. The username appears in the rendered app's user interface.

**Existing project**

The project's properties enable Windows Authentication and disable Anonymous Authentication. Open the launch profiles dialog:

1. In Solution Explorer, right click the project and select **Properties**.
2. Select the **Debug > General** tab and select **Open debug launch profiles UI**.
3. Clear the checkbox for **Enable Anonymous Authentication**.
4. Select the checkbox for **Enable Windows Authentication**.

Alternatively, the properties can be configured in the `iisSettings` node of the `launchSettings.json` file:

```
"iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": false,
    "iisExpress": {
        "applicationUrl": "http://localhost:52171/",
        "sslPort": 44308
    }
}
```

**New project**

Execute the [`dotnet new` command](/en-us/dotnet/core/tools/dotnet-new) with the `webapp` argument (ASP.NET Core Web App) and `--auth Windows` switch:

```
dotnet new webapp --auth Windows
```

**Existing project**

Update the `iisSettings` node of the `launchSettings.json` file:

```
"iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": false,
    "iisExpress": {
        "applicationUrl": "http://localhost:52171/",
        "sslPort": 44308
    }
}
```

### IIS

IIS uses the [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) to host ASP.NET Core apps. Windows Authentication is configured for IIS via the *web.config* file. The following sections show how to:

* Provide a local *web.config* file that activates Windows Authentication on the server when the app is deployed.
* Use the IIS Manager to configure the *web.config* file of an ASP.NET Core app that has already been deployed to the server.

If you haven't already done so, enable IIS to host ASP.NET Core apps. For more information, see [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0).

Enable the IIS Role Service for Windows Authentication. For more information, see [Enable Windows Authentication in IIS Role Services (see Step 2)](../../host-and-deploy/iis/advanced?view=aspnetcore-10.0#iis-configuration).

[IIS integration middleware](../../host-and-deploy/iis/?view=aspnetcore-10.0#enable-the-iisintegration-components) is configured to automatically authenticate requests by default. For more information, see [Host ASP.NET Core on Windows with IIS: IIS options (AutomaticAuthentication)](../../host-and-deploy/iis/?view=aspnetcore-10.0#iis-options).

The ASP.NET Core Module is configured to forward the Windows Authentication token to the app by default. For more information, see [ASP.NET Core Module configuration reference: Attributes of the aspNetCore element](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#attributes-of-the-aspnetcore-element).

Use **either** of the following approaches:

* **Before publishing and deploying the project,** add the following *web.config* file to the project root:

  ```
  <?xml version="1.0" encoding="utf-8"?>
  <configuration>
    <location path="." inheritInChildApplications="false">
      <system.webServer>
        <security>
          <authentication>
            <anonymousAuthentication enabled="false" />
            <windowsAuthentication enabled="true" />
          </authentication>
        </security>
      </system.webServer>
    </location>
  </configuration>
  ```

  When the project is published by the .NET SDK (without the `<IsTransformWebConfigDisabled>` property set to `true` in the project file), the published *web.config* file includes the `<location><system.webServer><security><authentication>` section. For more information on the `<IsTransformWebConfigDisabled>` property, see [web.config file](../../host-and-deploy/iis/web-config?view=aspnetcore-10.0).

* **After publishing and deploying the project,** perform server-side configuration with the IIS Manager:

  1. In IIS Manager, select the IIS site under the **Sites** node of the **Connections** sidebar.
  2. Double-click **Authentication** in the **IIS** area.
  3. Select **Anonymous Authentication**. Select **Disable** in the **Actions** sidebar.
  4. Select **Windows Authentication**. Select **Enable** in the **Actions** sidebar.

  When these actions are taken, IIS Manager modifies the app's *web.config* file. A `<system.webServer><security><authentication>` node is added with updated settings for `anonymousAuthentication` and `windowsAuthentication`:

  ```
  <system.webServer>
    <security>
      <authentication>
        <anonymousAuthentication enabled="false" />
        <windowsAuthentication enabled="true" />
      </authentication>
    </security>
  </system.webServer>
  ```

  The `<system.webServer>` section added to the *web.config* file by IIS Manager is outside of the app's `<location>` section added by the .NET SDK when the app is published. Because the section is added outside of the `<location>` node, the settings are inherited by any [sub-apps](../../host-and-deploy/iis/?view=aspnetcore-10.0#sub-applications) to the current app. To prevent inheritance, move the added `<security>` section inside of the `<location><system.webServer>` section that the .NET SDK provided.

  When IIS Manager is used to add the IIS configuration, it only affects the app's *web.config* file on the server. A subsequent deployment of the app may overwrite the settings on the server if the server's copy of *web.config* is replaced by the project's *web.config* file. Use **either** of the following approaches to manage the settings:

  * Use IIS Manager to reset the settings in the *web.config* file after the file is overwritten on deployment.
  * Add a *web.config file* to the app locally with the settings.

## Kestrel

The [`Microsoft.AspNetCore.Authentication.Negotiate` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Negotiate) can be used with [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) to enable Windows Authentication using Negotiate and Kerberos on Windows, Linux, and macOS.

Warning

Credentials can be persisted across requests on a connection. *Negotiate authentication must not be used with proxies unless the proxy maintains a 1:1 connection affinity (a persistent connection) with Kestrel.*

Note

The Negotiate handler detects if the underlying server supports Windows Authentication natively and if it is enabled. If the server supports Windows Authentication but it is disabled, an error is thrown asking you to enable the server implementation. When Windows Authentication is enabled in the server, the Negotiate handler transparently forwards authentication requests to it.

Authentication and a fallback authorization policy are enabled by the following highlighted code in `Program.cs`:

```
using Microsoft.AspNetCore.Authentication.Negotiate;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages();

var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code was generated by the ASP.NET Core Razor Pages template with **Windows Authentication** specified.

Highlighted lines:

* 5–6: [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) and [AddNegotiate](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.negotiateextensions.addnegotiate) register and configure the Negotiate authentication handler.
* 8–11: [AddAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authorizationservicecollectionextensions.addauthorization) with a fallback policy enforces authenticated users by default.
* 26: [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) executes authentication handlers for each request and populates [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user).
* 27: [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) evaluates authorization policies, including the fallback policy.

For the rules that determine when the fallback policy is selected instead of a named or default policy, see [Policy-based authorization in ASP.NET Core](../authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

Note

Calling [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) and [AddNegotiate](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.negotiateextensions.addnegotiate) registers and configures the Negotiate handler; it does not run authentication per request. The Authentication middleware ([UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)) invokes the handler and populates [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user), and must appear before [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) for policy evaluation to work.

[]()

### Kerberos authentication and role-based access control (RBAC)

Kerberos authentication on Linux or macOS doesn't provide any role information for an authenticated user. To add role and group information to a Kerberos user, the authentication handler must be configured to retrieve the roles from an LDAP domain. The most basic configuration only specifies an LDAP domain to query against and uses the authenticated user's context to query the LDAP domain:

```
using Microsoft.AspNetCore.Authentication.Negotiate;
using System.Runtime.InteropServices;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
    .AddNegotiate(options =>
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            options.EnableLdap("contoso.com");
        }
    });
```

Some configurations may require specific credentials to query the LDAP domain. The credentials can be specified in the following highlighted options:

```
using Microsoft.AspNetCore.Authentication.Negotiate;
using System.Runtime.InteropServices;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
        .AddNegotiate(options =>
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                options.EnableLdap(settings =>
                {
                    settings.Domain = "contoso.com";
                    settings.MachineAccountName = "machineName";
                    settings.MachineAccountPassword =
                                      builder.Configuration["Password"];
                });
            }
        });

builder.Services.AddRazorPages();
```

By default, the negotiate authentication handler resolves nested domains. In a large or complicated LDAP environment, resolving nested domains may result in a slow lookup or a lot of memory being used for each user. Nested domain resolution can be disabled using the `IgnoreNestedGroups` option.

Anonymous requests are allowed. Use [ASP.NET Core Authorization](../authorization/introduction?view=aspnetcore-10.0) to challenge anonymous requests for authentication.

### Windows environment configuration

The [`Microsoft.AspNetCore.Authentication.Negotiate` API](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Negotiate) performs [User Mode](/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode) authentication. Service Principal Names (SPNs) must be added to the user account running the service, not the machine account. Execute `setspn -S HTTP/myservername.mydomain.com myuser` in an administrative command shell.

#### Kerberos vs NTLM

The Negotiate package on Kestrel for ASP.NET Core attempts to use Kerberos, which is a more secure and performant authentication scheme than [NTLM](/en-us/troubleshoot/windows-server/windows-security/ntlm-user-authentication):

```
using Microsoft.AspNetCore.Authentication.Negotiate;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages();

var app = builder.Build();
```

[NegotiateDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.negotiate.negotiatedefaults.authenticationscheme) specifies Kerberos because it's the default.

IIS, IISExpress, and Kestrel support both Kerberos and [NTLM](/en-us/dotnet/framework/wcf/feature-details/understanding-http-authentication).

Examining the [WWW-Authenticate:](https://developer.mozilla.org/docs/Web/HTTP/Headers/WWW-Authenticate) header using IIS or IISExpress with a tool like Fiddler shows either `Negotiate` or NTLM.

Kestrel only shows `WWW-Authenticate: Negotiate`

The `WWW-Authenticate: Negotiate` header means that the server can use NTLM or Kerberos. Kestrel requires the [`Negotiate` header prefix](https://www.ietf.org/rfc/rfc4559.txt), it doesn't support directly specifying `NTLM` in the request or response auth headers. NTLM is supported in Kestrel, but it must be sent as `Negotiate`.

On Kestrel, to see if NTLM or Kerberos is used, Base64 decode the header and it shows either `NTLM` or `HTTP`. `HTTP` indicates Kerberos was used.

### Linux and macOS environment configuration

Instructions for joining a Linux or macOS machine to a Windows domain are available in the [Connect Azure Data Studio to your SQL Server using Windows authentication - Kerberos](/en-us/sql/azure-data-studio/enable-kerberos?view=sql-server-2017#join-your-os-to-the-active-directory-domain-controller\&preserve-view=true) article. The instructions create a machine account for the Linux machine on the domain. SPNs must be added to that machine account.

Note

When following the guidance in the [Connect Azure Data Studio to your SQL Server using Windows authentication - Kerberos](/en-us/sql/azure-data-studio/enable-kerberos?view=sql-server-2017#join-your-os-to-the-active-directory-domain-controller\&preserve-view=true) article, replace `python-software-properties` with `python3-software-properties` if needed.

Once the Linux or macOS machine is joined to the domain, additional steps are required to provide a [keytab file](/en-us/archive/blogs/pie/all-you-need-to-know-about-keytab-files) with the SPNs:

* On the domain controller, add new web service SPNs to the machine account:

  * `setspn -S HTTP/mywebservice.mydomain.com mymachine`
  * `setspn -S HTTP/mywebservice@MYDOMAIN.COM mymachine`

* Use [ktpass](/en-us/windows-server/administration/windows-commands/ktpass) to generate a keytab file:

  * `ktpass -princ HTTP/mywebservice.mydomain.com@MYDOMAIN.COM -pass myKeyTabFilePassword -mapuser MYDOMAIN\mymachine$ -pType KRB5_NT_PRINCIPAL -out c:\temp\mymachine.HTTP.keytab -crypto AES256-SHA1`
  * Some fields must be specified in uppercase as indicated.

* Copy the keytab file to the Linux or macOS machine.

* Select the keytab file via an environment variable: `export KRB5_KTNAME=/tmp/mymachine.HTTP.keytab`

* Invoke `klist` to show the SPNs currently available for use.

Note

A keytab file contains domain access credentials and must be protected accordingly.

## HTTP.sys

[HTTP.sys](../../fundamentals/servers/httpsys?view=aspnetcore-10.0) supports [Kernel Mode](/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode) Windows Authentication using Negotiate, NTLM, or Basic authentication.

The following code adds authentication and configures the app's web host to use HTTP.sys with Windows Authentication:

```
using Microsoft.AspNetCore.Server.HttpSys;
using System.Runtime.InteropServices;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
    builder.WebHost.UseHttpSys(options =>
        {
            options.Authentication.Schemes =
                AuthenticationSchemes.NTLM |
                AuthenticationSchemes.Negotiate;
            options.Authentication.AllowAnonymous = false;
        });
}
```

Note

HTTP.sys delegates to [Kernel Mode](/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode) authentication with the Kerberos authentication protocol. [User Mode](/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode) authentication isn't supported with Kerberos and HTTP.sys. The machine account must be used to decrypt the Kerberos token/ticket that's obtained from Active Directory and forwarded by the client to the server to authenticate the user. Register the Service Principal Name (SPN) for the host, not the user of the app.

Tip

To further protect Windows authentication over HTTPS, consider enabling channel binding token (CBT) hardening. For details, see [Enable channel binding token (CBT) hardening](../../fundamentals/servers/httpsys?view=aspnetcore-10.0#enable-channel-binding-token-cbt-hardening).

Note

HTTP.sys isn't supported on Nano Server version 1709 or later. To use Windows Authentication and HTTP.sys with Nano Server, use a Server Core (microsoft/windowsservercore) container (see `https://hub.docker.com/_/microsoft-windows-servercore`). For more information on Server Core, see [What is the Server Core installation option in Windows Server?](/en-us/windows-server/administration/server-core/what-is-server-core).

## Authorize users

The configuration state of anonymous access determines the way in which the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attributes are used in the app. The following two sections explain how to handle the disallowed and allowed configuration states of anonymous access.

### Disallow anonymous access

When Windows Authentication is enabled and anonymous access is disabled, the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attributes have no effect. If an IIS site is configured to disallow anonymous access, the request never reaches the app. For this reason, the [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attribute isn't applicable.

### Allow anonymous access

When both Windows Authentication and anonymous access are enabled, use the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attributes. The [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute allows you to secure endpoints of the app which require authentication. The [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attribute overrides the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute in apps that allow anonymous access. For attribute usage details, see [Simple authorization in ASP.NET Core](../authorization/simple?view=aspnetcore-10.0).

Note

By default, users who lack authorization to access a page are presented with an empty HTTP 403 response. The [status code pages middleware](../../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepages) can be configured to provide users with a better "Access Denied" experience.

## Impersonation

ASP.NET Core doesn't implement impersonation. Apps run with the app's identity for all requests, using app pool or process identity. If the app should perform an action on behalf of a user, use [WindowsIdentity.RunImpersonated](/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonated) or [RunImpersonatedAsync](/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonatedasync) in a [terminal inline middleware](../../fundamentals/middleware/?view=aspnetcore-10.0#create-a-middleware-pipeline-with-iapplicationbuilder) in `Program.cs`. Run a single action in this context and then close the context.

```
app.Run(async (context) =>
{
    try
    {
        var user = (WindowsIdentity)context.User.Identity!;

        await context.Response
            .WriteAsync($"User: {user.Name}\tState: {user.ImpersonationLevel}\n");

        await WindowsIdentity.RunImpersonatedAsync(user.AccessToken, async () =>
        {
            var impersonatedUser = WindowsIdentity.GetCurrent();
            var message =
                $"User: {impersonatedUser.Name}\t" +
                $"State: {impersonatedUser.ImpersonationLevel}";

            var bytes = Encoding.UTF8.GetBytes(message);
            await context.Response.Body.WriteAsync(bytes, 0, bytes.Length);
        });
    }
    catch (Exception e)
    {
        await context.Response.WriteAsync(e.ToString());
    }
});
```

While the [`Microsoft.AspNetCore.Authentication.Negotiate` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Negotiate) enables authentication on Windows, Linux, and macOS, impersonation is only supported on Windows.

## Claims transformations

When hosting with IIS, [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationservice.authenticateasync) isn't called internally to initialize a user. Therefore, an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation used to transform claims after every authentication isn't activated by default. For more information and a code example that activates claims transformations, see [Differences between in-process and out-of-process hosting](../../host-and-deploy/iis/in-process-hosting?view=aspnetcore-10.0#differences-between-in-process-and-out-of-process-hosting).

## Additional resources

* [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish)
* [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)
* [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](../../host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-10.0)

Windows Authentication (also known as Negotiate, Kerberos, or NTLM authentication) can be configured for ASP.NET Core apps hosted with [IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0), [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0), or [HTTP.sys](../../fundamentals/servers/httpsys?view=aspnetcore-10.0).

Windows Authentication relies on the operating system to authenticate users of ASP.NET Core apps. You can use Windows Authentication when your server runs on a corporate network using Active Directory domain identities or Windows accounts to identify users. Windows Authentication is best suited to intranet environments where users, client apps, and web servers belong to the same Windows domain.

## When to use Windows Authentication

Windows Authentication is suitable for web applications that operate within an organization's private internal network, accessible only to employees (and other authorized users) within the same network. The user management is done within Active Directory (AD), and users use their existing Windows domain account to authenticate.

Windows Authentication provides several benefits for intranet applications:

* **Seamless user experience** - Users are automatically authenticated based on their active Windows session, or are prompted to enter their Windows credentials via a standard browser dialog box.
* **Integration with Active Directory** - Leverages existing Windows infrastructure and security policies, including user groups, account lockouts, and multi-factor authentication (MFA).
* **Secure credential handling** - Authentication is handled through secure protocols like Kerberos, with no need to manage separate user credentials.
* **Role-based authorization** - Applications can access user and group information from Active Directory, enabling role-based access control (RBAC) within the application.
* **Reduced administrative overhead** - No need to maintain a separate user database or credential management system.

This makes Windows Authentication ideal for organizations that want to make use of their existing Windows infrastructure, such as intranet portals.

Note

Windows Authentication isn't supported with HTTP/2. While authentication challenges can be sent over HTTP/2 responses, the client must downgrade to HTTP/1.1 to complete the authentication process. This is a protocol limitation, not a deprecation of Windows Authentication. Once authenticated, normal HTTP/2 communication can resume for subsequent requests.

For public-facing applications Windows Authentication isn't recommended due to security and usability concerns. These reasons include:

* Windows Authentication is best kept internal to protect Active Directory, exposing it outside an internal network introduces security risks.
* External users don't have Windows domain accounts.
* It's complex to configure the necessary network infrastructure securely, and firewalls or proxies may interfere with the authentication process.
* It's not cross-platform and doesn't provide customization options for designs and user experiences.

### Alternatives for different scenarios

Depending on your application requirements, consider these alternatives:

**For public-facing applications:**

* [OpenID Connect](../how-to-choose-identity-solution?view=aspnetcore-10.0) with external identity providers
* ASP.NET Core Identity with local user accounts ([Introduction to Identity on ASP.NET Core](identity?view=aspnetcore-10.0))
* Azure Active Directory (AAD) for Microsoft 365 environments

**For mixed environments** with both intranet and external users:

* Active Directory Federation Services (ADFS) with OpenID Connect
* Azure Active Directory with hybrid configuration

**For corporate environments using modern authentication:**

* Azure Active Directory with single sign-on
* SAML-based solutions with third-party identity providers

## Proxy and load balancer scenarios

Windows Authentication is a stateful scenario primarily used in an intranet, where a proxy or load balancer doesn't usually handle traffic between clients and servers. If a proxy or load balancer is used, Windows Authentication only works if the proxy or load balancer:

* Handles the authentication.
* Passes the user authentication information to the app (for example, in a request header), which acts on the authentication information.

An alternative to Windows Authentication in environments where proxies and load balancers are used is Active Directory Federated Services (ADFS) with OpenID Connect (OIDC).

## IIS/IIS Express

Add authentication services by invoking [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) ([Microsoft.AspNetCore.Server.IISIntegration](/en-us/dotnet/api/microsoft.aspnetcore.server.iisintegration) namespace) in `Startup.ConfigureServices`:

```
services.AddAuthentication(IISDefaults.AuthenticationScheme);
```

### Launch settings (debugger)

Configuration for launch settings only affects the `Properties/launchSettings.json` file for IIS Express and doesn't configure IIS for Windows Authentication. Server configuration is explained in the [IIS](#iis) section.

The **Web Application** template available via Visual Studio or the .NET CLI can be configured to support Windows Authentication, which updates the `Properties/launchSettings.json` file automatically.

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

**New project**

1. Create a new project.
2. Select **ASP.NET Core Web Application**. Select **Next**.
3. Provide a name in the **Project name** field. Confirm the **Location** entry is correct or provide a location for the project. Select **Create**.
4. Select **Change** under **Authentication**.
5. In the **Change Authentication** window, select **Windows Authentication**. Select **OK**.
6. Select **Web Application**.
7. Select **Create**.

Run the app. The username appears in the rendered app's user interface.

**Existing project**

The project's properties enable Windows Authentication and disable Anonymous Authentication:

1. Right-click the project in **Solution Explorer** and select **Properties**.
2. Select the **Debug** tab.
3. Clear the checkbox for **Enable Anonymous Authentication**.
4. Select the checkbox for **Enable Windows Authentication**.
5. Save and close the property page.

Alternatively, the properties can be configured in the `iisSettings` node of the `launchSettings.json` file:

```
"iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": false,
    "iisExpress": {
        "applicationUrl": "http://localhost:52171/",
        "sslPort": 44308
    }
}
```

**New project**

Execute the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command with the `webapp` argument (ASP.NET Core Web App) and `--auth Windows` switch:

```
dotnet new webapp --auth Windows
```

**Existing project**

Update the `iisSettings` node of the `launchSettings.json` file:

```
"iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": false,
    "iisExpress": {
        "applicationUrl": "http://localhost:52171/",
        "sslPort": 44308
    }
}
```

When modifying an existing project, confirm that the project file includes a package reference for the [`Microsoft.AspNetCore.App` metapackage](../../fundamentals/metapackage-app?view=aspnetcore-10.0) **or** the [`Microsoft.AspNetCore.Authentication` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication/).

### IIS

IIS uses the [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) to host ASP.NET Core apps. Windows Authentication is configured for IIS via the *web.config* file. The following sections show how to:

* Provide a local *web.config* file that activates Windows Authentication on the server when the app is deployed.
* Use the IIS Manager to configure the *web.config* file of an ASP.NET Core app that has already been deployed to the server.

If you haven't already done so, enable IIS to host ASP.NET Core apps. For more information, see [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0).

Enable the IIS Role Service for Windows Authentication. For more information, see [Enable Windows Authentication in IIS Role Services (see Step 2)](../../host-and-deploy/iis/?view=aspnetcore-10.0#iis-configuration).

[IIS integration middleware](../../host-and-deploy/iis/?view=aspnetcore-10.0#enable-the-iisintegration-components) is configured to automatically authenticate requests by default. For more information, see [Host ASP.NET Core on Windows with IIS: IIS options (AutomaticAuthentication)](../../host-and-deploy/iis/?view=aspnetcore-10.0#iis-options).

The ASP.NET Core Module is configured to forward the Windows Authentication token to the app by default. For more information, see [ASP.NET Core Module configuration reference: Attributes of the aspNetCore element](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#attributes-of-the-aspnetcore-element).

Use **either** of the following approaches:

* **Before publishing and deploying the project,** add the following *web.config* file to the project root:

  ```
  <?xml version="1.0" encoding="utf-8"?>
  <configuration>
    <location path="." inheritInChildApplications="false">
      <system.webServer>
        <security>
          <authentication>
            <anonymousAuthentication enabled="false" />
            <windowsAuthentication enabled="true" />
          </authentication>
        </security>
      </system.webServer>
    </location>
  </configuration>
  ```

  When the project is published by the .NET SDK (without the `<IsTransformWebConfigDisabled>` property set to `true` in the project file), the published *web.config* file includes the `<location><system.webServer><security><authentication>` section. For more information on the `<IsTransformWebConfigDisabled>` property, see [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0#webconfig-file).

* **After publishing and deploying the project,** perform server-side configuration with the IIS Manager:

  1. In IIS Manager, select the IIS site under the **Sites** node of the **Connections** sidebar.
  2. Double-click **Authentication** in the **IIS** area.
  3. Select **Anonymous Authentication**. Select **Disable** in the **Actions** sidebar.
  4. Select **Windows Authentication**. Select **Enable** in the **Actions** sidebar.

  When these actions are taken, IIS Manager modifies the app's *web.config* file. A `<system.webServer><security><authentication>` node is added with updated settings for `anonymousAuthentication` and `windowsAuthentication`:

  ```
  <system.webServer>
    <security>
      <authentication>
        <anonymousAuthentication enabled="false" />
        <windowsAuthentication enabled="true" />
      </authentication>
    </security>
  </system.webServer>
  ```

  The `<system.webServer>` section added to the *web.config* file by IIS Manager is outside of the app's `<location>` section added by the .NET SDK when the app is published. Because the section is added outside of the `<location>` node, the settings are inherited by any [sub-apps](../../host-and-deploy/iis/?view=aspnetcore-10.0#sub-applications) to the current app. To prevent inheritance, move the added `<security>` section inside of the `<location><system.webServer>` section that the .NET SDK provided.

  When IIS Manager is used to add the IIS configuration, it only affects the app's *web.config* file on the server. A subsequent deployment of the app may overwrite the settings on the server if the server's copy of *web.config* is replaced by the project's *web.config* file. Use **either** of the following approaches to manage the settings:

  * Use IIS Manager to reset the settings in the *web.config* file after the file is overwritten on deployment.
  * Add a *web.config file* to the app locally with the settings.

## Kestrel

The [`Microsoft.AspNetCore.Authentication.Negotiate` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Negotiate) can be used with [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) to support Windows Authentication using Negotiate and Kerberos on Windows, Linux, and macOS.

Warning

Credentials can be persisted across requests on a connection. *Negotiate authentication must not be used with proxies unless the proxy maintains a 1:1 connection affinity (a persistent connection) with Kestrel.*

Note

The Negotiate handler detects if the underlying server supports Windows Authentication natively and if it is enabled. If the server supports Windows Authentication but it is disabled, an error is thrown asking you to enable the server implementation. When Windows Authentication is enabled in the server, the Negotiate handler transparently forwards authentication requests to it.

Add authentication services by invoking [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) and [AddNegotiate](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.negotiateextensions.addnegotiate) in `Startup.ConfigureServices`:

```
// using Microsoft.AspNetCore.Authentication.Negotiate;
// using Microsoft.Extensions.DependencyInjection;

services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();
```

Add authentication middleware by calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) in `Startup.Configure`:

```
app.UseAuthentication();
```

For more information on middleware, see [ASP.NET Core middleware](../../fundamentals/middleware/?view=aspnetcore-10.0).

[]()

### Kerberos authentication and role-based access control (RBAC)

Kerberos authentication on Linux or macOS doesn't provide any role information for an authenticated user. To add role and group information to a Kerberos user, the authentication handler must be configured to retrieve the roles from an LDAP domain. The most basic configuration only specifies an LDAP domain to query against and will use the authenticated user's context to query the LDAP domain:

```
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
    .AddNegotiate(options =>
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            options.EnableLdap("contoso.com");
        }
    });
```

Some configurations may require specific credentials to query the LDAP domain. The credentials can be specified in the following highlighted options:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDatabaseDeveloperPageExceptionFilter();
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
        .AddNegotiate(options =>
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                options.EnableLdap(settings =>
                {
                    settings.Domain = "contoso.com";
                    settings.MachineAccountName = "machineName";
                    settings.MachineAccountPassword = Configuration["Password"]
                });
            }
        });

    services.AddRazorPages();
}
```

By default, the negotiate authentication handler resolves nested domains. In a large or complicated LDAP environment, resolving nested domains may result in a slow lookup or a lot of memory being used for each user. Nested domain resolution can be disabled using the `IgnoreNestedGroups` option.

Anonymous requests are allowed. Use [ASP.NET Core Authorization](../authorization/introduction?view=aspnetcore-10.0) to challenge anonymous requests for authentication.

[AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.negotiate.negotiatedefaults.authenticationscheme#microsoft-aspnetcore-authentication-negotiate-negotiatedefaults-authenticationscheme) requires the [`Microsoft.AspNetCore.Authentication.Negotiate` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Negotiate).

### Windows environment configuration

The [`Microsoft.AspNetCore.Authentication.Negotiate` API](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Negotiate) performs [User Mode](/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode) authentication. Service Principal Names (SPNs) must be added to the user account running the service, not the machine account. Execute `setspn -S HTTP/myservername.mydomain.com myuser` in an administrative command shell.

### Linux and macOS environment configuration

Instructions for joining a Linux or macOS machine to a Windows domain are available in the [Connect Azure Data Studio to your SQL Server using Windows authentication - Kerberos](/en-us/sql/azure-data-studio/enable-kerberos?view=sql-server-2017#join-your-os-to-the-active-directory-domain-controller\&preserve-view=true) article. The instructions create a machine account for the Linux machine on the domain. SPNs must be added to that machine account.

Note

When following the guidance in the [Connect Azure Data Studio to your SQL Server using Windows authentication - Kerberos](/en-us/sql/azure-data-studio/enable-kerberos?view=sql-server-2017#join-your-os-to-the-active-directory-domain-controller\&preserve-view=true) article, replace `python-software-properties` with `python3-software-properties` if needed.

Once the Linux or macOS machine is joined to the domain, additional steps are required to provide a [keytab file](/en-us/archive/blogs/pie/all-you-need-to-know-about-keytab-files) with the SPNs:

* On the domain controller, add new web service SPNs to the machine account:

  * `setspn -S HTTP/mywebservice.mydomain.com mymachine`
  * `setspn -S HTTP/mywebservice@MYDOMAIN.COM mymachine`

* Use [ktpass](/en-us/windows-server/administration/windows-commands/ktpass) to generate a keytab file:

  * `ktpass -princ HTTP/mywebservice.mydomain.com@MYDOMAIN.COM -pass myKeyTabFilePassword -mapuser MYDOMAIN\mymachine$ -pType KRB5_NT_PRINCIPAL -out c:\temp\mymachine.HTTP.keytab -crypto AES256-SHA1`
  * Some fields must be specified in uppercase as indicated.

* Copy the keytab file to the Linux or macOS machine.

* Select the keytab file via an environment variable: `export KRB5_KTNAME=/tmp/mymachine.HTTP.keytab`

* Invoke `klist` to show the SPNs currently available for use.

Note

A keytab file contains domain access credentials and must be protected accordingly.

## HTTP.sys

[HTTP.sys](../../fundamentals/servers/httpsys?view=aspnetcore-10.0) supports [Kernel Mode](/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode) Windows Authentication using Negotiate, NTLM, or Basic authentication.

Add authentication services by invoking [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) ([Microsoft.AspNetCore.Server.HttpSys](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys) namespace) in `Startup.ConfigureServices`:

```
services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);
```

Configure the app's web host to use HTTP.sys with Windows Authentication (`Program.cs`). [UseHttpSys](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderhttpsysextensions.usehttpsys) is in the [Microsoft.AspNetCore.Server.HttpSys](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys) namespace.

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>()
                    .UseHttpSys(options =>
                    {
                        options.Authentication.Schemes = 
                            AuthenticationSchemes.NTLM | 
                            AuthenticationSchemes.Negotiate;
                        options.Authentication.AllowAnonymous = false;
                    });
            });
}
```

Note

HTTP.sys delegates to [Kernel Mode](/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode) authentication with the Kerberos authentication protocol. [User Mode](/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode) authentication isn't supported with Kerberos and HTTP.sys. The machine account must be used to decrypt the Kerberos token/ticket that's obtained from Active Directory and forwarded by the client to the server to authenticate the user. Register the Service Principal Name (SPN) for the host, not the user of the app.

Note

HTTP.sys isn't supported on Nano Server version 1709 or later. To use Windows Authentication and HTTP.sys with Nano Server, use a Server Core (microsoft/windowsservercore) container (see `https://hub.docker.com/_/microsoft-windows-servercore`). For more information on Server Core, see [What is the Server Core installation option in Windows Server?](/en-us/windows-server/administration/server-core/what-is-server-core).

## Authorize users

The configuration state of anonymous access determines the way in which the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attributes are used in the app. The following two sections explain how to handle the disallowed and allowed configuration states of anonymous access.

### Disallow anonymous access

When Windows Authentication is enabled and anonymous access is disabled, the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attributes have no effect. If an IIS site is configured to disallow anonymous access, the request never reaches the app. For this reason, the [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attribute isn't applicable.

### Allow anonymous access

When both Windows Authentication and anonymous access are enabled, use the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attributes. The [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute allows you to secure endpoints of the app which require authentication. The [`[AllowAnonymous]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attribute overrides the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute in apps that allow anonymous access. For attribute usage details, see [Simple authorization in ASP.NET Core](../authorization/simple?view=aspnetcore-10.0).

Note

By default, users who lack authorization to access a page are presented with an empty HTTP 403 response. The [status code pages middleware](../../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepages) can be configured to provide users with a better "Access Denied" experience.

## Impersonation

ASP.NET Core doesn't implement impersonation. Apps run with the app's identity for all requests, using app pool or process identity. If the app should perform an action on behalf of a user, use [WindowsIdentity.RunImpersonated](/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonated) or [RunImpersonatedAsync](/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonatedasync) in a [terminal inline middleware](../../fundamentals/middleware/?view=aspnetcore-10.0#create-a-middleware-pipeline-with-iapplicationbuilder) in `Startup.Configure`. Run a single action in this context and then close the context.

```
app.Run(async (context) =>
{
    try
    {
        var user = (WindowsIdentity)context.User.Identity;

        await context.Response
            .WriteAsync($"User: {user.Name}\tState: {user.ImpersonationLevel}\n");

        WindowsIdentity.RunImpersonated(user.AccessToken, () =>
        {
            var impersonatedUser = WindowsIdentity.GetCurrent();
            var message =
                $"User: {impersonatedUser.Name}\t" +
                $"State: {impersonatedUser.ImpersonationLevel}";

            var bytes = Encoding.UTF8.GetBytes(message);
            context.Response.Body.Write(bytes, 0, bytes.Length);
        });
    }
    catch (Exception e)
    {
        await context.Response.WriteAsync(e.ToString());
    }
});
```

While the [`Microsoft.AspNetCore.Authentication.Negotiate` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Negotiate) enables authentication on Windows, Linux, and macOS, impersonation is only supported on Windows.

## Claims transformations

When hosting with IIS, [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationservice.authenticateasync) isn't called internally to initialize a user. Therefore, an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation used to transform claims after every authentication isn't activated by default. For more information and a code example that activates claims transformations, see [Differences between in-process and out-of-process hosting](../../host-and-deploy/iis/in-process-hosting?view=aspnetcore-10.0#differences-between-in-process-and-out-of-process-hosting).

## Additional resources

* [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish)
* [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)
* [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](../../host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/troubleshoot
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/forms/troubleshoot.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Troubleshoot ASP.NET Core Blazor forms

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article provides troubleshooting guidance for Blazor forms.

## Large form payloads and the SignalR message size limit

*This section only applies to Blazor Web Apps, Blazor Server apps, and hosted Blazor WebAssembly solutions that implement SignalR.*

If form processing fails because the component's form payload has exceeded the maximum incoming SignalR message size permitted for hub methods, the form can adopt [streaming JS interop](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0#stream-from-javascript-to-net) without increasing the message size limit. For more information on the size limit and the error thrown, see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#maximum-receive-message-size).

In the following example a text area (`<textarea>`) is used with streaming JS interop to move up to 50,000 bytes of data to the server.

Add a JavaScript (JS) `getText` function to the app:

```
window.getText = (elem) => {
  const textValue = elem.value;
  const utf8Encoder = new TextEncoder();
  const encodedTextValue = utf8Encoder.encode(textValue);
  return encodedTextValue;
};
```

For information on where to place JS in a Blazor app, see [JavaScript location in ASP.NET Core Blazor apps](../javascript-interoperability/location-of-javascript?view=aspnetcore-10.0).

Due to security considerations, zero-length streams aren't permitted for streaming JS Interop. Therefore, the following `StreamFormData` component traps a [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) and returns an empty string if the text area is blank when the form is submitted.

`StreamFormData.razor`:

```
@page "/stream-form-data"
@inject IJSRuntime JS
@inject ILogger<StreamFormData> Logger

<h1>Stream form data with JS interop</h1>

<EditForm FormName="StreamFormData" Model="this" OnSubmit="Submit">
    <div>
        <label>
            &lt;textarea&gt; value streamed for assignment to
            <code>TextAreaValue (&lt;= 50,000 characters)</code>:
            <textarea @ref="largeTextArea" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    Length: @TextAreaValue?.Length
</div>

@code {
    private ElementReference largeTextArea;

    public string? TextAreaValue { get; set; }

    protected override void OnInitialized() =>
        TextAreaValue ??= string.Empty;

    private async Task Submit()
    {
        TextAreaValue = await GetTextAsync();

        Logger.LogInformation("TextAreaValue length: {Length}",
            TextAreaValue.Length);
    }

    public async Task<string> GetTextAsync()
    {
        try
        {
            var streamRef =
                await JS.InvokeAsync<IJSStreamReference>("getText", largeTextArea);
            var stream = await streamRef.OpenReadStreamAsync(maxAllowedSize: 50_000);
            var streamReader = new StreamReader(stream);

            return await streamReader.ReadToEndAsync();
        }
        catch (JSException jsException)
        {
            if (jsException.InnerException is
                    ArgumentOutOfRangeException outOfRangeException &&
                outOfRangeException.ActualValue is not null &&
                outOfRangeException.ActualValue is long actualLength &&
                actualLength == 0)
            {
                return string.Empty;
            }

            throw;
        }
    }
}
```

```
@page "/stream-form-data"
@inject IJSRuntime JS
@inject ILogger<StreamFormData> Logger

<h1>Stream form data with JS interop</h1>

<EditForm Model="this" OnSubmit="Submit">
    <div>
        <label>
            &lt;textarea&gt; value streamed for assignment to
            <code>TextAreaValue (&lt;= 50,000 characters)</code>:
            <textarea @ref="largeTextArea" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    Length: @TextAreaValue?.Length
</div>

@code {
    private ElementReference largeTextArea;

    public string? TextAreaValue { get; set; }

    protected override void OnInitialized() => 
        TextAreaValue ??= string.Empty;

    private async Task Submit()
    {
        TextAreaValue = await GetTextAsync();

        Logger.LogInformation("TextAreaValue length: {Length}",
            TextAreaValue.Length);
    }

    public async Task<string> GetTextAsync()
    {
        try
        {
            var streamRef =
                await JS.InvokeAsync<IJSStreamReference>("getText", largeTextArea);
            var stream = await streamRef.OpenReadStreamAsync(maxAllowedSize: 50_000);
            var streamReader = new StreamReader(stream);

            return await streamReader.ReadToEndAsync();
        }
        catch (JSException jsException)
        {
            if (jsException.InnerException is
                    ArgumentOutOfRangeException outOfRangeException &&
                outOfRangeException.ActualValue is not null &&
                outOfRangeException.ActualValue is long actualLength &&
                actualLength == 0)
            {
                return string.Empty;
            }

            throw;
        }
    }
}
```

If form processing fails because the component's form payload has exceeded the maximum incoming SignalR message size permitted for hub methods, the message size limit can be increased. For more information on the size limit and the error thrown, see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#maximum-receive-message-size).

## EditForm parameter error

> InvalidOperationException: EditForm requires a Model parameter, or an EditContext parameter, but not both.

Confirm that the [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) assigns a [Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model#microsoft-aspnetcore-components-forms-editform-model) **or** an [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.editcontext#microsoft-aspnetcore-components-forms-editform-editcontext). Don't use both for the same form.

When assigning to [Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model#microsoft-aspnetcore-components-forms-editform-model), confirm that the model type is instantiated.

## Connection disconnected

> Error: Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'.

> System.IO.InvalidDataException: The maximum message size of 32768B was exceeded. The message size can be configured in AddHubOptions.

For more information and guidance, see the following resources:

* [Large form payloads and the SignalR message size limit](#large-form-payloads-and-the-signalr-message-size-limit)
* [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#maximum-receive-message-size)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/nginx
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/nginx.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host and deploy ASP.NET Core Blazor WebAssembly with Nginx

By [Andrii Annenko](https://github.com/aannenko)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to host and deploy Blazor WebAssembly that targets .NET 8, 9 or 10 using [Nginx](https://nginx.org/).

This article covers deploying the app either directly on a host or in a Docker container that includes Nginx and the published app's files. The Docker hosting section covers publishing the app, copying the published output to Nginx's web root in the container image, configuring Nginx for client-side routing, and applying common production settings.

## Minimal Nginx configuration

The following `nginx.conf` file is a minimal Nginx configuration example showing how to configure Nginx to send the `index.html` file whenever it can't find a corresponding file on disk. It also directs Nginx to serve correct MIME types by defining a `types` block (alternatively, include a [`mime.types`](https://github.com/nginx/nginx/blob/7fa941a55e211ebd57f512fbfb24d59dbb97940d/conf/mime.types) file).

```
events { }
http {
    types {
        text/html                    html;
        text/css                     css;
        application/javascript       js mjs;
        application/json             json;
        application/manifest+json    webmanifest;

        application/wasm             wasm;

        application/octet-stream     dll pdb webcil;
    }

    server {
        listen 80;

        location / {
            root /usr/share/nginx/html;
            try_files $uri $uri/ /index.html =404;
        }
    }
}
```

## Complete Nginx configuration

The following `nginx.conf` example builds on the minimal configuration. It includes optional settings for caching, compression, rate limiting, and security headers. Review the comments and remove or adjust settings based on your app's requirements.

```
events { }
http {
    # Security hardening: Don't reveal the Nginx version in responses.
    server_tokens off;

    # Optional: Rate limiting to prevent a single client from sending too 
    # many requests.
    # Blazor WebAssembly apps can generate many requests during startup.
    limit_req_zone $binary_remote_addr zone=one:10m rate=60r/s;

    # Optional: Cache-Control policy by URI.
    # - index.html and service-worker.js: avoid caching so app updates are picked up.
    # - /_framework/*: fingerprinted assets can be cached long-term.
    map $uri $cache_control {
        default             "max-age=3600";
        /index.html         "no-cache";
        /service-worker.js  "no-cache";

        # IMPORTANT: .NET 8/9 Blazor WASM apps do not fingerprint these files
        # Uncomment if the Blazor WASM app targets .NET 8/9, remove for .NET 10
        # /_framework/blazor.boot.json        "no-cache";
        # /_framework/blazor.webassembly.js   "max-age=3600";
        # /_framework/dotnet.js               "max-age=3600";

        ~^/_framework/      "public, max-age=31536000, immutable";
        ~\.woff2?$          "public, max-age=31536000, immutable";
    }

    # MIME types for Blazor WebAssembly assets.
    types {
        text/html                           html htm;
        text/css                            css;
        application/javascript              js mjs;
        application/json                    json;
        application/manifest+json           webmanifest;
        application/wasm                    wasm;
        text/plain                          txt;
        text/xml                            xml;
        application/octet-stream            bin dll exe pdb blat dat webcil;

        # Images
        image/png                           png;
        image/jpeg                          jpg jpeg;
        image/gif                           gif;
        image/webp                          webp;
        image/avif                          avif;
        image/svg+xml                       svg;
        image/x-icon                        ico;

        # Fonts
        font/woff                           woff;
        font/woff2                          woff2;

        # Fallback for other MIME types
        application/octet-stream            *;
    }

    server {
        listen 80;

        location / {
            # Root path for static site content.
            root      /usr/share/nginx/html;

            # SPA/Blazor routing:
            # - Serve a file if it exists.
            # - Otherwise fall back to index.html.
            try_files $uri $uri/ /index.html =404;

            # Optional: Use the rate limit zone defined above.
            limit_req zone=one burst=60 nodelay;

            # Optional: Serve precompressed *.gz files (when present) instead of 
            # compressing on the fly.
            gzip_static on;

            # Optional: Caching policy based on "map $uri $cache_control" above.
            add_header Cache-Control $cache_control always;

            # Optional: Mitigate MIME sniffing.
            add_header X-Content-Type-Options "nosniff" always;
        }
    }
}
```

If browser developer tools or a network traffic tool indicates that requests are receiving a *503 - Service Unavailable* status code, increase the `rate` and `burst` values.

For more information on production Nginx web server configuration, see [Creating NGINX Plus and NGINX Configuration Files](https://docs.nginx.com/nginx/admin-guide/basic-functionality/managing-configuration-files/).

## Docker deployment

The following Dockerfile publishes a Blazor WebAssembly app and serves the app's static web assets from an Nginx image.

The example assumes that:

* The app's project file is named `BlazorSample.csproj` and located at the Docker build context root.
* An `nginx.conf` file is available at the Docker build context root.

For example, `Dockerfile`, `nginx.conf` and `BlazorSample.csproj` are all located in the same directory where the rest of Blazor code is also located. In this case, `docker build` is launched in this working directory.

```
# Build stage
# IMPORTANT: change the dotnet/sdk version to the one that your Blazor app targets
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build
WORKDIR /source

RUN apt update \
    && apt install -y python3 \
    && dotnet workload install wasm-tools

COPY . .
RUN dotnet publish BlazorSample.csproj --output /app

# Runtime stage
FROM nginx:latest
COPY --from=build /app/wwwroot/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/nginx.conf
```

Build and run the image:

```
docker build -t blazor-wasm-nginx .
docker run --rm -p {PORT}:80 blazor-wasm-nginx
```

For more information, refer to [Nginx Docker guide](https://hub.docker.com/_/nginx).

## Hosted deployment on Linux (Nginx)

Configure the app with [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers by following the guidance in [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

For more information on setting the app's base path, including sub-app path configuration, see [ASP.NET Core Blazor app base path](../app-base-path?view=aspnetcore-10.0).

Follow the guidance for an [ASP.NET Core SignalR app](../../../signalr/scale?view=aspnetcore-10.0#linux-with-nginx) with the following changes:

* Remove the configuration for proxy buffering (`proxy_buffering off;`) because the setting only applies to [Server-Sent Events (SSE)](https://developer.mozilla.org/docs/Web/API/Server-sent_events), which aren't relevant to Blazor app client-server interactions.

* Change the `location` path from `/hubroute` (`location /hubroute { ... }`) to the sub-app path `/{PATH}` (`location /{PATH} { ... }`), where the `{PATH}` placeholder is the sub-app path.

  The following example configures the server for an app that responds to requests at the root path `/`:

  ```
  http {
      server {
          ...
          location / {
              ...
          }
      }
  }
  ```

  The following example configures the sub-app path of `/blazor`:

  ```
  http {
      server {
          ...
          location /blazor {
              ...
          }
      }
  }
  ```

## Related content

* [Host ASP.NET Core on Linux with Nginx](../../../host-and-deploy/linux-nginx?view=aspnetcore-10.0)

* Nginx documentation:

  * [NGINX as a WebSocket Proxy](https://www.nginx.com/blog/websocket-nginx/)
  * [WebSocket proxying](http://nginx.org/docs/http/websocket.html)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/bundling-and-minification
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/bundling-and-minification.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Bundle and minify static assets in ASP.NET Core

By [Scott Addie](https://twitter.com/Scott_Addie) and [David Pine](https://twitter.com/davidpine7)

This article explains the benefits of applying bundling and minification, including how these features can be used with ASP.NET Core web apps.

## What is bundling and minification

Bundling and minification are two distinct performance optimizations you can apply in a web app. Used together, bundling and minification improve performance by reducing the number of server requests and reducing the size of the requested static assets.

Bundling and minification primarily improve the first page request load time. Once a web page has been requested, the browser caches the static assets (JavaScript, CSS, and images). So, bundling and minification don't improve performance when requesting the same page, or pages, on the same site requesting the same assets. If the expires header isn't set correctly on the assets and if bundling and minification aren't used, the browser's freshness heuristics mark the assets stale after a few days. Additionally, the browser requires a validation request for each asset. In this case, bundling and minification provide a performance improvement even after the first page request.

### Bundling

Bundling combines multiple files into a single file. Bundling reduces the number of server requests necessary to render a web asset, such as a web page. You can create any number of individual bundles specifically for CSS, JavaScript, etc. Fewer files mean fewer HTTP requests from the browser to the server or from the service providing your application. This results in improved first page load performance.

### Minification

Minification removes unnecessary characters from code without altering functionality. The result is a significant size reduction in requested assets (such as CSS, images, and JavaScript files). Common effects of minification include shortening variable names to one character and removing comments and unnecessary whitespace.

Consider the following JavaScript function:

```
AddAltToImg = function (imageTagAndImageID, imageContext) {
  ///<signature>
  ///<summary> Adds an alt tab to the image
  // </summary>
  //<param name="imgElement" type="String">The image selector.</param>
  //<param name="ContextForImage" type="String">The image context.</param>
  ///</signature>
  var imageElement = $(imageTagAndImageID, imageContext);
  imageElement.attr('alt', imageElement.attr('id').replace(/ID/, ''));
}
```

Minification reduces the function to the following:

```
AddAltToImg=function(t,a){var r=$(t,a);r.attr("alt",r.attr("id").replace(/ID/,""))};
```

In addition to removing the comments and unnecessary whitespace, the following parameter and variable names were renamed as follows:

| Original             | Renamed |
| -------------------- | ------- |
| `imageTagAndImageID` | `t`     |
| `imageContext`       | `a`     |
| `imageElement`       | `r`     |

## Impact of bundling and minification

The following table outlines differences between individually loading assets and using bundling and minification for a typical web app.

| Action                 | Without B/M | With B/M | Reduction |
| ---------------------- | ----------- | -------- | --------- |
| File Requests          | 18          | 7        | 61%       |
| Bytes Transferred (KB) | 265         | 156      | 41%       |
| Load Time (ms)         | 2360        | 885      | 63%       |

The load time improved, but this example ran locally. Greater performance gains are realized when using bundling and minification with assets transferred over a network.

The test app used to generate the figures in the preceding table demonstrates typical improvements that might not apply to a given app. We recommend testing an app to determine if bundling and minification yields an improved load time.

## Choose a bundling and minification strategy

ASP.NET Core doesn't provide a native bundling and minification solution. Third-party tools, such as [Gulp](https://gulpjs.com) and [Webpack](https://webpack.js.org), provide workflow automation for bundling and minification, as well as linting and image optimization. Bundling and minifying before deployment provides the advantage of reduced server load. However, bundling and minification increase build complexity and only work with static files.

## Environment-based bundling and minification

As a best practice, the bundled and minified files of your app should be used in a production environment. During development, the original files make for easier debugging of the app.

Specify which files to include in your pages by using the [Environment Tag Helper](../mvc/views/tag-helpers/built-in/environment-tag-helper?view=aspnetcore-10.0) in your views. The Environment Tag Helper only renders its contents when running in specific [environments](../fundamentals/environments?view=aspnetcore-10.0).

The following `environment` tag renders the unprocessed CSS files when running in the `Development` environment:

```
<environment include="Development">
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</environment>
```

```
<environment names="Staging,Production">
    <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
          asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
          asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
    <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
```

The following `environment` tag renders the bundled and minified CSS files when running in an environment other than `Development`. For example, running in `Production` or `Staging` triggers the rendering of these stylesheets:

```
<environment exclude="Development">
    <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
          asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
          asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
    <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
```

```
<environment names="Staging,Production">
    <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
          asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
          asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
    <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
```

## Related content

* [Use multiple environments](../fundamentals/environments?view=aspnetcore-10.0)
* [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/environment-tag-helper
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/environment-tag-helper.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Environment Tag Helper in ASP.NET Core

By [Peter Kellner](https://peterkellner.net) and [Hisham Bin Ateya](https://twitter.com/hishambinateya)

The Environment Tag Helper conditionally renders its enclosed content based on the current [hosting environment](../../../../fundamentals/environments?view=aspnetcore-10.0). The Environment Tag Helper's single attribute, `names`, is a comma-separated list of environment names. If any of the provided environment names match the current environment, the enclosed content is rendered.

For an overview of Tag Helpers, see [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0).

## Environment Tag Helper Attributes

### names

`names` accepts a single hosting environment name or a comma-separated list of hosting environment names that trigger the rendering of the enclosed content.

Environment values are compared to the current value returned by [IWebHostEnvironment.EnvironmentName](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingenvironment.environmentname). The comparison ignores case.

The following example uses an Environment Tag Helper. The content is rendered if the hosting environment is Staging or Production:

```
<environment names="Staging,Production">
    <strong>IWebHostEnvironment.EnvironmentName is Staging or Production</strong>
</environment>
```

## include and exclude attributes

`include` & `exclude` attributes control rendering the enclosed content based on the included or excluded hosting environment names.

### include

The `include` property exhibits similar behavior to the `names` attribute. An environment listed in the `include` attribute value must match the app's hosting environment ([IWebHostEnvironment.EnvironmentName](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingenvironment.environmentname)) to render the content of the `<environment>` tag.

```
<environment include="Staging,Production">
    <strong>IWebHostEnvironment.EnvironmentName is Staging or Production</strong>
</environment>
```

### exclude

In contrast to the `include` attribute, the content of the `<environment>` tag is rendered when the hosting environment doesn't match an environment listed in the `exclude` attribute value.

```
<environment exclude="Development">
    <strong>IWebHostEnvironment.EnvironmentName is not Development</strong>
</environment>
```

## Related content

* [ASP.NET Core runtime environments](../../../../fundamentals/environments?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/logging-and-diagnostics
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/logging-and-diagnostics.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# IIS log creation and redirection

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The ASP.NET Core Module redirects stdout and stderr console output to disk if the `stdoutLogEnabled` and `stdoutLogFile` attributes of the `aspNetCore` element are set. Any folders in the `stdoutLogFile` path are created by the module when the log file is created. The app pool must have write access to the location where the logs are written (use `IIS AppPool\{APP POOL NAME}` to provide write permission, where the placeholder `{APP POOL NAME}` is the app pool name).

Logs aren't rotated, unless process recycling/restart occurs. It's the responsibility of the hoster to limit the disk space the logs consume.

Using the stdout log is only recommended for troubleshooting app startup issues when hosting on IIS or when using [development-time support for IIS with Visual Studio](development-time-iis-support?view=aspnetcore-10.0), not while debugging locally and running the app with IIS Express.

Don't use the stdout log for general app logging purposes. For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

A timestamp and file extension are added automatically when the log file is created. The log file name is composed by appending the timestamp, process ID, and file extension (`.log`) to the last segment of the `stdoutLogFile` path (typically `stdout`) delimited by underscores. If the `stdoutLogFile` path ends with `stdout`, a log for an app with a PID of 1934 created on 2/5/2018 at 19:42:32 has the file name `stdout_20180205194132_1934.log`.

If `stdoutLogEnabled` is false, errors that occur on app startup are captured and emitted to the event log up to 30 KB. After startup, all additional logs are discarded.

The following sample `aspNetCore` element configures stdout logging at the relative path `.\log\`. Confirm that the AppPool user identity has permission to write to the path provided.

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="true"
    stdoutLogFile=".\logs\stdout"
    hostingModel="inprocess">
</aspNetCore>
```

When publishing an app for Azure App Service deployment, the Web SDK sets the `stdoutLogFile` value to `\\?\%home%\LogFiles\stdout`. The `%home` environment variable is predefined for apps hosted by Azure App Service.

To create logging filter rules, see the [Apply log filter rules in code](../../fundamentals/logging/?view=aspnetcore-10.0#apply-log-filter-rules-in-code) section of the ASP.NET Core logging documentation.

For more information on path formats, see [File path formats on Windows systems](/en-us/dotnet/standard/io/file-path-formats).

## Enhanced diagnostic logs

The ASP.NET Core Module is configurable to provide enhanced diagnostics logs. Add the `<handlerSettings>` element to the `<aspNetCore>` element in `web.config`. Setting the `debugLevel` to `TRACE` exposes a higher fidelity of diagnostic information:

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="false"
    stdoutLogFile="\\?\%home%\LogFiles\stdout"
    hostingModel="inprocess">
  <handlerSettings>
    <handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />
    <handlerSetting name="debugLevel" value="FILE,TRACE" />
  </handlerSettings>
</aspNetCore>
```

Any folders in the path (`logs` in the preceding example) are created by the module when the log file is created. The app pool must have write access to the location where the logs are written (use `IIS AppPool\{APP POOL NAME}` to provide write permission, where the placeholder `{APP POOL NAME}` is the app pool name).

See [Configuration of ASP.NET Core Module with `web.config`](web-config?view=aspnetcore-10.0#configuration-of-aspnet-core-module-with-webconfig) for an example of the `aspNetCore` element in the `web.config` file.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/url-rewriting
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/url-rewriting.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# URL rewriting middleware in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Kirk Larkin](https://twitter.com/serpent5) and [Rick Anderson](https://twitter.com/RickAndMSFT)

This article introduces URL rewriting with instructions on how to use URL rewriting middleware in ASP.NET Core apps.

URL rewriting is the act of modifying request URLs based on one or more predefined rules. URL rewriting creates an abstraction between resource locations and their addresses so that the locations and addresses aren't tightly linked. URL rewriting is valuable in several scenarios to:

* Move or replace server resources temporarily or permanently and maintain stable locators for those resources.
* Split request processing across different apps or across areas of one app.
* Remove, add, or reorganize URL segments on incoming requests.
* Optimize public URLs for Search Engine Optimization (SEO).
* Permit the use of friendly public URLs to help visitors predict the content returned by requesting a resource.
* Redirect insecure requests to secure endpoints.
* Prevent hotlinking, where an external site uses a hosted static asset on another site by linking the asset into its own content.

***URL rewriting can reduce the performance of an app***. Limit the number and complexity of rules.

## URL redirect and URL rewrite

The difference in wording between *URL redirect* and *URL rewrite* is subtle but has important implications for providing resources to clients. ASP.NET Core's URL rewriting middleware is capable of meeting the need for both.

A *URL redirect* involves a client-side operation, where the client is instructed to access a resource at a different address than the client originally requested. This requires a round trip to the server. The redirect URL returned to the client appears in the browser's address bar when the client makes a new request for the resource.

If `/resource` is *redirected* to `/different-resource`, the server responds that the client should obtain the resource at `/different-resource` with a status code indicating that the redirect is either temporary or permanent.

When redirecting requests to a different URL, indicate whether the redirect is permanent or temporary by specifying the status code with the response:

* The [`301 - Moved Permanently`](https://developer.mozilla.org/docs/Web/HTTP/Status/301) status code is used where the resource has a new, permanent URL and that all future requests for the resource should use the new URL. *The client may cache and reuse the response when a 301 status code is received.*

* The [`302 - Found`](https://developer.mozilla.org/docs/Web/HTTP/Status/302) status code is used where the redirection is temporary or generally subject to change. The 302 status code indicates to the client not to store the URL and use it in the future.

For more information on status codes, see [RFC 9110: Status Code Definitions](https://www.rfc-editor.org/rfc/rfc9110#name-status-codes).

A *URL rewrite* is a server-side operation that provides a resource from a different resource address than the client requested. Rewriting a URL doesn't require a round trip to the server. The rewritten URL isn't returned to the client and doesn't appear in the browser's address bar.

If `/resource` is *rewritten* to `/different-resource`, the server *internally* fetches and returns the resource at `/different-resource`.

Although the client might be able to retrieve the resource at the rewritten URL, the client isn't informed that the resource exists at the rewritten URL when it makes its request and receives the response.

## URL rewriting sample app

Explore the features of the URL rewriting middleware with the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/url-rewriting/samples/). The app applies redirect and rewrite rules and shows the redirected or rewritten URL for several scenarios.

## When to use URL rewriting middleware

Use URL rewriting middleware when the following approaches aren't satisfactory:

* [URL Rewrite module with IIS on Windows Server](https://www.iis.net/downloads/microsoft/url-rewrite)
* [Apache mod\_rewrite module on Apache Server](https://httpd.apache.org/docs/2.4/rewrite/)
* [URL rewriting on Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/)

Use the URL rewriting middleware when the app is hosted on [HTTP.sys server](servers/httpsys?view=aspnetcore-10.0).

The main reasons to use the server-based URL rewriting technologies in IIS, Apache, and Nginx are:

* The middleware doesn't support the full features of these modules.

  Some of the features of the server modules don't work with ASP.NET Core projects, such as the `IsFile` and `IsDirectory` constraints of the IIS Rewrite module. In these scenarios, use the middleware instead.

* The performance of the middleware probably doesn't match that of the modules.

  Benchmarking is the only way to know with certainty which approach degrades performance the most or if degraded performance is negligible.

## Extension and options

Establish URL rewrite and redirect rules by creating an instance of the [RewriteOptions](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptions) class with extension methods for each of the rewrite rules. Chain multiple rules ***in the order that they should be processed***. The `RewriteOptions` are passed into the URL rewriting middleware as it's added to the request pipeline with [UseRewriter](/en-us/dotnet/api/microsoft.aspnetcore.builder.rewritebuilderextensions.userewriter):

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

In the preceding code, [`MethodRules`](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/url-rewriting/samples/6.x/SampleApp/RewriteRules.cs) is a user defined class. See [`RewriteRules.cs`](#rrr) in this article for more information.

### Authorization and URL rewriting

When a rule changes the request path, routing evaluates the new path and can select a different endpoint than the one that matched the original path. Authorization must run for the endpoint that's finally selected. Otherwise, the authorization requirements of that endpoint aren't applied.

An app that calls `UseRewriter` and protects any endpoint with authorization must add the authentication and authorization middleware explicitly, and must add it after `UseRewriter`:

```
app.UseRewriter(options);
app.UseAuthentication();
app.UseAuthorization();
```

When the app calls `UseRouting` explicitly, use the following order:

```
app.UseRewriter(options);
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
```

Both of the following configurations evaluate authorization against the endpoint selected for the original path rather than the rewritten path:

* Relying on the authentication and authorization middleware that's added automatically when the corresponding services are registered, without calling `UseAuthentication` and `UseAuthorization`.
* Calling `UseAuthentication` and `UseAuthorization` before `UseRewriter`.

Important

Explicitly add and order the authentication and authorization middleware after `UseRewriter` as shown earlier. Keep the app updated to the latest supported patch release to ensure the framework's built-in authorization behavior includes the newest fixes.

### Redirect non-www to www

Three options permit the app to redirect non-`www` requests to `www`:

* [AddRedirectToWwwPermanent](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttowwwpermanent): Permanently redirect the request to the `www` subdomain if the request is non-`www`. Redirects with a [Status308PermanentRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status308permanentredirect#microsoft-aspnetcore-http-statuscodes-status308permanentredirect) status code.

* [AddRedirectToWww](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttowww): Redirect the request to the `www` subdomain if the incoming request is non-`www`. Redirects with a [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) status code. An overload permits providing the status code for the response. Use a field of the [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) class for a status code assignment.

### URL redirect

Use [AddRedirect](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirect) to redirect requests. The first parameter contains the [.NET regular expression](/en-us/dotnet/standard/base-types/regular-expressions) (Regex) for matching on the path of the incoming URL. The second parameter is the replacement string. The third parameter, if present, specifies the status code. If the status code isn't specified, the status code defaults to [302 - Found](https://developer.mozilla.org/docs/Web/HTTP/Status/302), which indicates that the resource is temporarily moved or replaced.

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

In a browser with developer tools enabled, make a request to the sample app with the path `/redirect-rule/1234/5678`. The regular expression matches the request path on `redirect-rule/(.*)`, and the path is replaced with `/redirected/1234/5678`. The redirect URL is sent back to the client with a *302 - Found* status code. The browser makes a new request at the redirect URL, which appears in the browser's address bar. Since no rules in the sample app match on the redirect URL:

* The second request receives a *200 - OK* response from the app.
* The body of the response shows the redirect URL.

A round trip is made to the server when a URL is *redirected*.

Warning

Be cautious when establishing redirect rules. Redirect rules are evaluated on every request to the app, including after a redirect. It's easy to accidentally create a loop of ***infinite*** redirects.

The part of the expression contained within parentheses is called a [capture group](/en-us/dotnet/standard/base-types/grouping-constructs-in-regular-expressions). The dot (`.`) of the expression means *match any character*. The asterisk (`*`) indicates *match the preceding character zero or more times*. Therefore, the last two path segments of the URL, `1234/5678`, are captured by capture group `(.*)`. Any value provided in the request URL after `redirect-rule/` is captured by this single capture group.

In the replacement string, captured groups are injected into the string with the dollar sign (`$`) followed by the sequence number of the capture. The first capture group value is obtained with `$1`, the second with `$2`, and they continue in sequence for the capture groups in the regular expression. There's only one captured group in the redirect rule regular expression in `redirect-rule/(.*)`, so there's only one injected group in the replacement string, which is `$1`. When the rule is applied, the URL becomes `/redirected/1234/5678`.

Try `/redirect-rule/1234/5678` with the browser tools on the network tab.

#### Redirect rule safety

The middleware expands capture groups and variables in a replacement string and then interprets the result as the redirect destination. A broad capture such as `(.*)` isn't inherently unsafe when a trusted local prefix constrains the destination. A rule is unsafe when request-derived text can control the complete destination, construct the `scheme://` prefix, or determine the authority (host) of an absolute URI.

```
// Unsafe: $1 can supply a complete redirect destination.
options.AddRedirect("legacy/(.*)", "$1");

// Safe: The trusted prefix keeps the destination within the app.
options.AddRedirect("legacy/(.*)", "/archive/$1");

// Safe: The scheme and authority are fixed.
options.AddRedirect("legacy/(.*)", "https://example.com/$1");

// Unsafe unless $1 is validated against an allow list of hosts.
options.AddRedirect("go/(.*)", "https://$1/path");
```

Use a fixed local prefix when redirecting within the app. For external redirects, use a fixed host or validate the host against an allow list. Imported IIS URL Rewrite and Apache `mod_rewrite` rules also expand back-references and variables. Importing a rule doesn't validate its resulting substitution, so review imported rules by the same criteria.

Depending on the API, imported-rule options, and replacement, the original query string might be preserved or appended to the destination. Include query parameters when reviewing whether request-derived data can affect a redirect. For more information about validating redirect destinations, see [Prevent open redirect attacks in ASP.NET Core](../security/preventing-open-redirects?view=aspnetcore-10.0).

### URL redirect to a secure endpoint

Use [AddRedirectToHttps](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttohttps) to redirect HTTP requests to the same host and path using the HTTPS protocol. If the status code isn't supplied, the middleware defaults to *302 - Found*. If the port isn't supplied:

* The middleware defaults to `null`.
* The scheme changes to `https` (HTTPS protocol), and the client accesses the resource on port 443.

The following example shows how to set the status code to `301 - Moved Permanently` and change the port to the HTTPS port used by Kestrel on localhost. In production, the HTTPS port is set to null:

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

int? localhostHTTPSport = null;
if (app.Environment.IsDevelopment())
{
    localhostHTTPSport = Int32.Parse(Environment.GetEnvironmentVariable(
                   "ASPNETCORE_URLS")!.Split(new Char[] { ':', ';' })[2]);
}

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        // localhostHTTPport not needed for production, used only with localhost.
        .AddRedirectToHttps(301, localhostHTTPSport)
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

Use [AddRedirectToHttpsPermanent](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttohttpspermanent) to redirect insecure requests to the same host and path with secure HTTPS protocol on port 443. The middleware sets the status code to `301 - Moved Permanently`.

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

Note

When redirecting to a secure endpoint without the requirement for additional redirect rules, we recommend using HTTPS redirection middleware. For more information, see [Enforce HTTPS](../security/enforcing-ssl?view=aspnetcore-10.0#require-https).

The sample app demonstrates how to use `AddRedirectToHttps` or `AddRedirectToHttpsPermanent`. Make an insecure HTTP request to the app at `http://redirect6.azurewebsites.net/iis-rules-rewrite/xyz`. When testing HTTP to HTTPS redirection with localhost:

* Use the HTTP URL, which has a different port than the HTTPS URL. The HTTP URL is in the `Properties/launchSettings.json` file.
* Removing the `s` from `https://localhost/{port}` fails because localhost doesn't respond on HTTP to the HTTPS port.

The following image shows the F12 browser tools image of a request to `http://redirect6.azurewebsites.net/iis-rules-rewrite/xyz` using the preceding code:

### URL rewrite

Use [AddRewrite](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addrewrite) to create a rule for rewriting URLs. The first parameter contains the regular expression for matching on the incoming URL path. The second parameter is the replacement string. The third parameter, `skipRemainingRules: {true|false}`, indicates to the middleware whether or not to skip additional rewrite rules if the current rule is applied.

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

Try the request to `https://redirect6.azurewebsites.net/rewrite-rule/1234/5678`

The caret (`^`) at the beginning of the expression means that matching starts at the beginning of the URL path.

In the earlier example with the redirect rule, `redirect-rule/(.*)`, there's no caret (`^`) at the start of the regular expression. Therefore, any characters may precede `redirect-rule/` in the path for a successful match.

| Path                               | Match |
| ---------------------------------- | ----- |
| `/redirect-rule/1234/5678`         | Yes   |
| `/my-cool-redirect-rule/1234/5678` | Yes   |
| `/anotherredirect-rule/1234/5678`  | Yes   |

The rewrite rule, `^rewrite-rule/(\d+)/(\d+)`, only matches paths if they start with `rewrite-rule/`. In the following table, note the difference in matching.

| Path                              | Match |
| --------------------------------- | ----- |
| `/rewrite-rule/1234/5678`         | Yes   |
| `/my-cool-rewrite-rule/1234/5678` | No    |
| `/anotherrewrite-rule/1234/5678`  | No    |

Following the `^rewrite-rule/` portion of the expression, there are two capture groups, `(\d+)/(\d+)`. The `\d` signifies *match a digit (number)*. The plus sign (`+`) means *match one or more of the preceding character*. Therefore, the URL must contain a number followed by a forward-slash followed by another number. These capture groups are injected into the rewritten URL as `$1` and `$2`. The rewrite rule replacement string places the captured groups into the query string. The requested path `/rewrite-rule/1234/5678` is rewritten to return the resource at `/rewritten?var1=1234&var2=5678`. If a query string is present on the original request, it's preserved when the URL is rewritten.

There's no round trip to the server to return the resource. If the resource exists, it's fetched and returned to the client with a *200 - OK* status code. Because the client isn't redirected, the URL in the browser's address bar doesn't change. Clients can't detect that a URL rewrite operation occurred on the server.

### Performance tips for URL rewrite and redirect

For the fastest response:

* Order rewrite rules from the most frequently matched rule to the least frequently matched rule.
* Use `skipRemainingRules: true` whenever possible because matching rules is computationally expensive and increases app response time. Skip the processing of the remaining rules when a match occurs and no additional rule processing is required.

Warning

A malicious user can provide expensive-to-process input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` institute a timeout. For example, the Regex timeout (`_regexTimeout`) of the [`RedirectRule` class](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Rewrite/src/RedirectRule.cs) and [`RewriteRule` class](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Rewrite/src/RewriteRule.cs) classes both pass in a one second timeout.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Apache mod\_rewrite

Apply Apache mod\_rewrite rules with [AddApacheModRewrite](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.apachemodrewriteoptionsextensions.addapachemodrewrite). Make sure that the rules file is deployed with the app. For more information and examples of mod\_rewrite rules, see [Apache mod\_rewrite](https://httpd.apache.org/docs/2.4/rewrite/).

A [StreamReader](/en-us/dotnet/api/system.io.streamreader) is used to read the rules from the *ApacheModRewrite.txt* rules file:

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

The sample app redirects requests from `/apache-mod-rules-redirect/(.\*)` to `/redirected?id=$1`. The response status code is *302 - Found*.

```
# Rewrite path with additional sub directory
RewriteRule ^/apache-mod-rules-redirect/(.*) /redirected?id=$1 [L,R=302]
```

Try the request to `https://redirect6.azurewebsites.net/apache-mod-rules-redirect/1234`

The [Apache middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Rewrite/src/ApacheModRewrite/ServerVariables.cs) supports the following Apache mod\_rewrite server variables:

* CONN\_REMOTE\_ADDR
* HTTP\_ACCEPT
* HTTP\_CONNECTION
* HTTP\_COOKIE
* HTTP\_FORWARDED
* HTTP\_HOST
* HTTP\_REFERER
* HTTP\_USER\_AGENT
* HTTPS
* IPV6
* QUERY\_STRING
* REMOTE\_ADDR
* REMOTE\_PORT
* REQUEST\_FILENAME
* REQUEST\_METHOD
* REQUEST\_SCHEME
* REQUEST\_URI
* SCRIPT\_FILENAME
* SERVER\_ADDR
* SERVER\_PORT
* SERVER\_PROTOCOL
* TIME
* TIME\_DAY
* TIME\_HOUR
* TIME\_MIN
* TIME\_MON
* TIME\_SEC
* TIME\_WDAY
* TIME\_YEAR

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### IIS URL Rewrite Module rules

To use the same rule set that applies to the IIS URL Rewrite Module, use [AddIISUrlRewrite](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.iisurlrewriteoptionsextensions.addiisurlrewrite). Make sure that the rules file is deployed with the app. Don't direct the middleware to use the app's *web.config* file when running on Windows Server IIS. With IIS, these rules should be stored outside of the app's *web.config* file in order to avoid conflicts with the IIS Rewrite module. For more information and examples of IIS URL Rewrite Module rules, see [Using Url Rewrite Module 2.0](/en-us/iis/extensions/url-rewrite-module/using-url-rewrite-module-20) and [URL Rewrite Module Configuration Reference](/en-us/iis/extensions/url-rewrite-module/url-rewrite-module-configuration-reference).

A [StreamReader](/en-us/dotnet/api/system.io.streamreader) is used to read the rules from the `IISUrlRewrite.xml` rules file:

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

The sample app rewrites requests from `/iis-rules-rewrite/(.*)` to `/rewritten?id=$1`. The response is sent to the client with a *200 - OK* status code.

```
<rewrite>
  <rules>
    <rule name="Rewrite segment to id querystring" stopProcessing="true">
      <match url="^iis-rules-rewrite/(.*)$" />
      <action type="Rewrite" url="rewritten?id={R:1}" appendQueryString="false"/>
    </rule>
  </rules>
</rewrite>
```

Try the request to `https://redirect6.azurewebsites.net/iis-rules-rewrite/xyz`

Apps that have an active IIS Rewrite Module with server-level rules configured that impacts the app in undesirable ways:

* Consider disabling the IIS Rewrite Module for the app.
* For more information, see [Disabling IIS modules](../host-and-deploy/iis/modules?view=aspnetcore-10.0#disabling-iis-modules).

#### Unsupported features

The middleware doesn't support the following IIS URL Rewrite Module features:

* Outbound Rules
* Custom Server Variables
* Wildcards
* LogRewrittenUrl

#### Supported server variables

The [middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Rewrite/src/IISUrlRewrite/ServerVariables.cs) supports the following IIS URL Rewrite Module server variables:

* CONTENT\_LENGTH
* CONTENT\_TYPE
* HTTP\_ACCEPT
* HTTP\_CONNECTION
* HTTP\_COOKIE
* HTTP\_HOST
* HTTP\_REFERER
* HTTP\_URL
* HTTP\_USER\_AGENT
* HTTPS
* LOCAL\_ADDR
* QUERY\_STRING
* REMOTE\_ADDR
* REMOTE\_PORT
* REQUEST\_FILENAME
* REQUEST\_URI

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

[IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider) can be obtained via a [PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider). This approach may provide greater flexibility for the location of rewrite rules files. Make sure that the rewrite rules files are deployed to the server at the path provided.

```
var fileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory());
```

### Method-based rule

Use [Add](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.add) to implement custom rule logic in a method. `Add` exposes the [RewriteContext](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewritecontext), which makes available the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) for use in redirect methods. The [RewriteContext.Result](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewritecontext.result) property determines how additional pipeline processing is handled. Set the value to one of the [RuleResult](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.ruleresult) fields described in the following table.

| Rewrite context result               | Action                                                           |
| ------------------------------------ | ---------------------------------------------------------------- |
| `RuleResult.ContinueRules` (default) | Continue applying rules.                                         |
| `RuleResult.EndResponse`             | Stop applying rules and send the response.                       |
| `RuleResult.SkipRemainingRules`      | Stop applying rules and send the context to the next middleware. |

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

The sample app demonstrates a method that redirects requests for paths that end with `.xml`. When a request is made for `/file.xml`:

* The request is redirected to `/xmlfiles/file.xml`
* The status code is set to `301 - Moved Permanently`. When the browser makes a new request for `/xmlfiles/file.xml`, static file middleware serves the file to the client from the *wwwroot/xmlfiles* folder. For a redirect, explicitly set the status code of the response. Otherwise, a *200 - OK* status code is returned, and the redirect doesn't occur on the client.

[]()

`RewriteRules.cs`:

```
public static void RedirectXmlFileRequests(RewriteContext context)
{
    var request = context.HttpContext.Request;

    // Because the client is redirecting back to the same app, stop 
    // processing if the request has already been redirected.
    if (request.Path.StartsWithSegments(new PathString("/xmlfiles")) ||
        request.Path.Value==null)
    {
        return;
    }

    if (request.Path.Value.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
    {
        var response = context.HttpContext.Response;
        response.StatusCode = (int) HttpStatusCode.MovedPermanently;
        context.Result = RuleResult.EndResponse;
        response.Headers[HeaderNames.Location] = 
            "/xmlfiles" + request.Path + request.QueryString;
    }
}
```

This approach can also rewrite requests. The sample app demonstrates rewriting the path for any text file request to serve the *file.txt* text file from the *wwwroot* folder. Static file middleware serves the file based on the updated request path:

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

`RewriteRules.cs`:

```
public static void RewriteTextFileRequests(RewriteContext context)
{
    var request = context.HttpContext.Request;

    if (request.Path.Value != null &&
        request.Path.Value.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
    {
        context.Result = RuleResult.SkipRemainingRules;
        request.Path = "/file.txt";
    }
}
```

### IRule-based rule

Use [Add](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.add) to use rule logic in a class that implements the [IRule](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.irule) interface. `IRule` provides greater flexibility over using the method-based rule approach. The implementation class may include a constructor that allows passing in parameters for the [ApplyRule](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.irule.applyrule) method.

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)
        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

The values of the parameters in the sample app for the `extension` and the `newPath` are checked to meet several conditions. The `extension` must contain a value, and the value must be `.png`, `.jpg`, or `.gif`. If the `newPath` isn't valid, an [ArgumentException](/en-us/dotnet/api/system.argumentexception) is thrown. If a request is made for `image.png`, the request is redirected to `/png-images/image.png`. If a request is made for `image.jpg`, the request is redirected to `/jpg-images/image.jpg`. The status code is set to `301 - Moved Permanently`, and the `context.Result` is set to stop processing rules and send the response.

```
public class RedirectImageRequests : IRule
{
    private readonly string _extension;
    private readonly PathString _newPath;

    public RedirectImageRequests(string extension, string newPath)
    {
        if (string.IsNullOrEmpty(extension))
        {
            throw new ArgumentException(nameof(extension));
        }

        if (!Regex.IsMatch(extension, @"^\.(png|jpg|gif)$"))
        {
            throw new ArgumentException("Invalid extension", nameof(extension));
        }

        if (!Regex.IsMatch(newPath, @"(/[A-Za-z0-9]+)+?"))
        {
            throw new ArgumentException("Invalid path", nameof(newPath));
        }

        _extension = extension;
        _newPath = new PathString(newPath);
    }

    public void ApplyRule(RewriteContext context)
    {
        var request = context.HttpContext.Request;

        // Because we're redirecting back to the same app, stop 
        // processing if the request has already been redirected
        if (request.Path.StartsWithSegments(new PathString(_newPath)) ||
            request.Path.Value == null)
        {
            return;
        }

        if (request.Path.Value.EndsWith(_extension, StringComparison.OrdinalIgnoreCase))
        {
            var response = context.HttpContext.Response;
            response.StatusCode = (int) HttpStatusCode.MovedPermanently;
            context.Result = RuleResult.EndResponse;
            response.Headers[HeaderNames.Location] = 
                _newPath + request.Path + request.QueryString;
        }
    }
}
```

Try:

* PNG request: `https://redirect6.azurewebsites.net/image.png`
* JPG request: `https://redirect6.azurewebsites.net/image.jpg`

[]()

## Regex examples

| Goal                              | Regex String & Match Example                                                                    | Replacement String & Output Example                        |
| --------------------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| Rewrite path into query string    | `^path/(.*)/(.*)` `/path/abc/123`                                                               | `path?var1=$1&var2=$2` `/path?var1=abc&var2=123`           |
| Strip trailing slash              | `^path2/(.*)/$` `/path2/xyz/`                                                                   | `$1` `/path2/xyz`                                          |
| Enforce trailing slash            | `^path3/(.*[^/])$` `/path3/xyz`                                                                 | `$1/` `/path3/xyz/`                                        |
| Avoid rewriting specific requests | `^(.*)(?<!\.axd)$` or `^(?!.*\.axd$)(.*)$` Yes: `/path4/resource.htm` No: `/path4/resource.axd` | `rewritten/$1` `/rewritten/resource.htm` `/resource.axd`   |
| Rearrange URL segments            | `path5/(.*)/(.*)/(.*)` `path5/1/2/3`                                                            | `path5/$3/$2/$1` `path5/3/2/1`                             |
| Replace a URL segment             | `^path6/(.*)/segment2/(.*)` `^path6/segment1/segment2/segment3`                                 | `path6/$1/replaced/$2` `/path6/segment1/replaced/segment3` |

The links in the preceding table use the following code deployed to Azure:

```
using Microsoft.AspNetCore.Rewrite;
using RewriteRules;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

using (StreamReader apacheModRewriteStreamReader =
    File.OpenText("ApacheModRewrite.txt"))
using (StreamReader iisUrlRewriteStreamReader =
    File.OpenText("IISUrlRewrite.xml"))
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent()
        .AddRedirect("redirect-rule/(.*)", "redirected/$1")
        .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2",
            skipRemainingRules: true)

        // Rewrite path to QS.
        .AddRewrite(@"^path/(.*)/(.*)", "path?var1=$1&var2=$2",
            skipRemainingRules: true)
        // Skip trailing slash.
        .AddRewrite(@"^path2/(.*)/$", "path2/$1",
            skipRemainingRules: true)
         // Enforce trailing slash.
         .AddRewrite(@"^path3/(.*[^/])$", "path3/$1/",
            skipRemainingRules: true)
         // Avoid rewriting specific requests.
         .AddRewrite(@"^path4/(.*)(?<!\.axd)$", "rewritten/$1",
            skipRemainingRules: true)
         // Rearrange URL segments
         .AddRewrite(@"^path5/(.*)/(.*)/(.*)", "path5/$3/$2/$1",
            skipRemainingRules: true)
          // Replace a URL segment
          .AddRewrite(@"^path6/(.*)/segment2/(.*)", "path6/$1/replaced/$2",
            skipRemainingRules: true)

        .AddApacheModRewrite(apacheModRewriteStreamReader)
        .AddIISUrlRewrite(iisUrlRewriteStreamReader)
        .Add(MethodRules.RedirectXmlFileRequests)
        .Add(MethodRules.RewriteTextFileRequests)
        .Add(new RedirectImageRequests(".png", "/png-images"))
        .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

    app.UseRewriter(options);
}

app.UseStaticFiles();

app.Run(context => context.Response.WriteAsync(
    $"Rewritten or Redirected Url: " +
    $"{context.Request.Path + context.Request.QueryString}"));

app.Run();
```

In most of the preceding regular expression samples, the literal `path` is used to make unique testable rewrite rules for the deployed sample. Typically the regular expression wouldn't include `path`. For example, see these [regular expression examples](?view=aspnetcore-5.0\&preserve-view=true#regex5) table.

This document introduces URL rewriting with instructions on how to use URL rewriting middleware in ASP.NET Core apps.

URL rewriting is the act of modifying request URLs based on one or more predefined rules. URL rewriting creates an abstraction between resource locations and their addresses so that the locations and addresses aren't tightly linked. URL rewriting is valuable in several scenarios to:

* Move or replace server resources temporarily or permanently and maintain stable locators for those resources.
* Split request processing across different apps or across areas of one app.
* Remove, add, or reorganize URL segments on incoming requests.
* Optimize public URLs for Search Engine Optimization (SEO).
* Permit the use of friendly public URLs to help visitors predict the content returned by requesting a resource.
* Redirect insecure requests to secure endpoints.
* Prevent hotlinking, where an external site uses a hosted static asset on another site by linking the asset into its own content.

Note

URL rewriting can reduce the performance of an app. Where feasible, limit the number and complexity of rules.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/url-rewriting/samples/) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## URL redirect and URL rewrite

The difference in wording between *URL redirect* and *URL rewrite* is subtle but has important implications for providing resources to clients. ASP.NET Core's URL rewriting middleware is capable of meeting the need for both.

A *URL redirect* involves a client-side operation, where the client is instructed to access a resource at a different address than the client originally requested. This requires a round trip to the server. The redirect URL returned to the client appears in the browser's address bar when the client makes a new request for the resource.

If `/resource` is *redirected* to `/different-resource`, the server responds that the client should obtain the resource at `/different-resource` with a status code indicating that the redirect is either temporary or permanent.

When redirecting requests to a different URL, indicate whether the redirect is permanent or temporary by specifying the status code with the response:

* The `301 - Moved Permanently` status code is used where the resource has a new, permanent URL and you wish to instruct the client that all future requests for the resource should use the new URL. *The client may cache and reuse the response when a 301 status code is received.*

* The *302 - Found* status code is used where the redirection is temporary or generally subject to change. The 302 status code indicates to the client not to store the URL and use it in the future.

For more information on status codes, see [RFC 9110: Status Code Definitions](https://www.rfc-editor.org/rfc/rfc9110#name-status-codes).

A *URL rewrite* is a server-side operation that provides a resource from a different resource address than the client requested. Rewriting a URL doesn't require a round trip to the server. The rewritten URL isn't returned to the client and doesn't appear in the browser's address bar.

If `/resource` is *rewritten* to `/different-resource`, the server *internally* fetches and returns the resource at `/different-resource`.

Although the client might be able to retrieve the resource at the rewritten URL, the client isn't informed that the resource exists at the rewritten URL when it makes its request and receives the response.

## URL rewriting sample app

You can explore the features of the URL rewriting middleware with the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/url-rewriting/samples/). The app applies redirect and rewrite rules and shows the redirected or rewritten URL for several scenarios.

## When to use URL rewriting middleware

Use URL rewriting middleware when you're unable to use the following approaches:

* [URL Rewrite module with IIS on Windows Server](https://www.iis.net/downloads/microsoft/url-rewrite)
* [Apache mod\_rewrite module on Apache Server](https://httpd.apache.org/docs/2.4/rewrite/)
* [URL rewriting on Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/)

Use the URL rewriting middleware when the app is hosted on [HTTP.sys server](servers/httpsys?view=aspnetcore-10.0).

The main reasons to use the server-based URL rewriting technologies in IIS, Apache, and Nginx are:

* The middleware doesn't support the full features of these modules.

  Some of the features of the server modules don't work with ASP.NET Core projects, such as the `IsFile` and `IsDirectory` constraints of the IIS Rewrite module. In these scenarios, use the middleware instead.

* The performance of the middleware probably doesn't match that of the modules.

  Benchmarking is the only way to know for sure which approach degrades performance the most or if degraded performance is negligible.

## Package

URL rewriting middleware is provided by the [Microsoft.AspNetCore.Rewrite](https://www.nuget.org/packages/Microsoft.AspNetCore.Rewrite) package, which is implicitly included in ASP.NET Core apps.

## Extension and options

Establish URL rewrite and redirect rules by creating an instance of the [RewriteOptions](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptions) class with extension methods for each of your rewrite rules. Chain multiple rules in the order that you would like them processed. The `RewriteOptions` are passed into the URL rewriting middleware as it's added to the request pipeline with [UseRewriter](/en-us/dotnet/api/microsoft.aspnetcore.builder.rewritebuilderextensions.userewriter):

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

### Redirect non-www to www

Three options permit the app to redirect non-`www` requests to `www`:

* [AddRedirectToWwwPermanent](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttowwwpermanent): Permanently redirect the request to the `www` subdomain if the request is non-`www`. Redirects with a [Status308PermanentRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status308permanentredirect#microsoft-aspnetcore-http-statuscodes-status308permanentredirect) status code.

* [AddRedirectToWww](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttowww): Redirect the request to the `www` subdomain if the incoming request is non-`www`. Redirects with a [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) status code. An overload permits you to provide the status code for the response. Use a field of the [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) class for a status code assignment.

### URL redirect

Use [AddRedirect](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirect) to redirect requests. The first parameter contains your Regex for matching on the path of the incoming URL. The second parameter is the replacement string. The third parameter, if present, specifies the status code. If you don't specify the status code, the status code defaults to *302 - Found*, which indicates that the resource is temporarily moved or replaced.

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

In a browser with developer tools enabled, make a request to the sample app with the path `/redirect-rule/1234/5678`. The regex matches the request path on `redirect-rule/(.*)`, and the path is replaced with `/redirected/1234/5678`. The redirect URL is sent back to the client with a *302 - Found* status code. The browser makes a new request at the redirect URL, which appears in the browser's address bar. Since no rules in the sample app match on the redirect URL:

* The second request receives a *200 - OK* response from the app.
* The body of the response shows the redirect URL.

A round trip is made to the server when a URL is *redirected*.

Warning

Be cautious when establishing redirect rules. Redirect rules are evaluated on every request to the app, including after a redirect. It's easy to accidentally create a *loop of infinite redirects*.

Original Request: `/redirect-rule/1234/5678`

The part of the expression contained within parentheses is called a *capture group*. The dot (`.`) of the expression means *match any character*. The asterisk (`*`) indicates *match the preceding character zero or more times*. Therefore, the last two path segments of the URL, `1234/5678`, are captured by capture group `(.*)`. Any value you provide in the request URL after `redirect-rule/` is captured by this single capture group.

In the replacement string, captured groups are injected into the string with the dollar sign (`$`) followed by the sequence number of the capture. The first capture group value is obtained with `$1`, the second with `$2`, and they continue in sequence for the capture groups in your regex. There's only one captured group in the redirect rule regex in the sample app, so there's only one injected group in the replacement string, which is `$1`. When the rule is applied, the URL becomes `/redirected/1234/5678`.

### URL redirect to a secure endpoint

Use [AddRedirectToHttps](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttohttps) to redirect HTTP requests to the same host and path using the HTTPS protocol. If the status code isn't supplied, the middleware defaults to *302 - Found*. If the port isn't supplied:

* The middleware defaults to `null`.
* The scheme changes to `https` (HTTPS protocol), and the client accesses the resource on port 443.

The following example shows how to set the status code to `301 - Moved Permanently` and change the port to 5001.

```
public void Configure(IApplicationBuilder app)
{
    var options = new RewriteOptions()
        .AddRedirectToHttps(301, 5001);

    app.UseRewriter(options);
}
```

Use [AddRedirectToHttpsPermanent](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttohttpspermanent) to redirect insecure requests to the same host and path with secure HTTPS protocol on port 443. The middleware sets the status code to `301 - Moved Permanently`.

```
public void Configure(IApplicationBuilder app)
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent();

    app.UseRewriter(options);
}
```

Note

When redirecting to a secure endpoint without the requirement for additional redirect rules, we recommend using HTTPS redirection middleware. For more information, see the [Enforce HTTPS](../security/enforcing-ssl?view=aspnetcore-10.0#require-https) topic.

The sample app is capable of demonstrating how to use `AddRedirectToHttps` or `AddRedirectToHttpsPermanent`. Add the extension method to the `RewriteOptions`. Make an insecure request to the app at any URL. Dismiss the browser security warning that the self-signed certificate is untrusted or create an exception to trust the certificate.

Original Request using `AddRedirectToHttps(301, 5001)`: `http://localhost:5000/secure`

Original Request using `AddRedirectToHttpsPermanent`: `http://localhost:5000/secure`

### URL rewrite

Use [AddRewrite](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addrewrite) to create a rule for rewriting URLs. The first parameter contains the regex for matching on the incoming URL path. The second parameter is the replacement string. The third parameter, `skipRemainingRules: {true|false}`, indicates to the middleware whether or not to skip additional rewrite rules if the current rule is applied.

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

Original Request: `/rewrite-rule/1234/5678`

The carat (`^`) at the beginning of the expression means that matching starts at the beginning of the URL path.

In the earlier example with the redirect rule, `redirect-rule/(.*)`, there's no carat (`^`) at the start of the regex. Therefore, any characters may precede `redirect-rule/` in the path for a successful match.

| Path                               | Match |
| ---------------------------------- | ----- |
| `/redirect-rule/1234/5678`         | Yes   |
| `/my-cool-redirect-rule/1234/5678` | Yes   |
| `/anotherredirect-rule/1234/5678`  | Yes   |

The rewrite rule, `^rewrite-rule/(\d+)/(\d+)`, only matches paths if they start with `rewrite-rule/`. In the following table, note the difference in matching.

| Path                              | Match |
| --------------------------------- | ----- |
| `/rewrite-rule/1234/5678`         | Yes   |
| `/my-cool-rewrite-rule/1234/5678` | No    |
| `/anotherrewrite-rule/1234/5678`  | No    |

Following the `^rewrite-rule/` portion of the expression, there are two capture groups, `(\d+)/(\d+)`. The `\d` signifies *match a digit (number)*. The plus sign (`+`) means *match one or more of the preceding character*. Therefore, the URL must contain a number followed by a forward-slash followed by another number. These capture groups are injected into the rewritten URL as `$1` and `$2`. The rewrite rule replacement string places the captured groups into the query string. The requested path of `/rewrite-rule/1234/5678` is rewritten to obtain the resource at `/rewritten?var1=1234&var2=5678`. If a query string is present on the original request, it's preserved when the URL is rewritten.

There's no round trip to the server to obtain the resource. If the resource exists, it's fetched and returned to the client with a *200 - OK* status code. Because the client isn't redirected, the URL in the browser's address bar doesn't change. Clients can't detect that a URL rewrite operation occurred on the server.

Note

Use `skipRemainingRules: true` whenever possible because matching rules is computationally expensive and increases app response time. For the fastest app response:

* Order rewrite rules from the most frequently matched rule to the least frequently matched rule.
* Skip the processing of the remaining rules when a match occurs and no additional rule processing is required.

### Apache mod\_rewrite

Apply Apache mod\_rewrite rules with [AddApacheModRewrite](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.apachemodrewriteoptionsextensions.addapachemodrewrite). Make sure that the rules file is deployed with the app. For more information and examples of mod\_rewrite rules, see [Apache mod\_rewrite](https://httpd.apache.org/docs/2.4/rewrite/).

A [StreamReader](/en-us/dotnet/api/system.io.streamreader) is used to read the rules from the *ApacheModRewrite.txt* rules file:

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

The sample app redirects requests from `/apache-mod-rules-redirect/(.\*)` to `/redirected?id=$1`. The response status code is *302 - Found*.

```
# Rewrite path with additional sub directory
RewriteRule ^/apache-mod-rules-redirect/(.*) /redirected?id=$1 [L,R=302]
```

Original Request: `/apache-mod-rules-redirect/1234`

The middleware supports the following Apache mod\_rewrite server variables:

* CONN\_REMOTE\_ADDR
* HTTP\_ACCEPT
* HTTP\_CONNECTION
* HTTP\_COOKIE
* HTTP\_FORWARDED
* HTTP\_HOST
* HTTP\_REFERER
* HTTP\_USER\_AGENT
* HTTPS
* IPV6
* QUERY\_STRING
* REMOTE\_ADDR
* REMOTE\_PORT
* REQUEST\_FILENAME
* REQUEST\_METHOD
* REQUEST\_SCHEME
* REQUEST\_URI
* SCRIPT\_FILENAME
* SERVER\_ADDR
* SERVER\_PORT
* SERVER\_PROTOCOL
* TIME
* TIME\_DAY
* TIME\_HOUR
* TIME\_MIN
* TIME\_MON
* TIME\_SEC
* TIME\_WDAY
* TIME\_YEAR

### IIS URL Rewrite Module rules

To use the same rule set that applies to the IIS URL Rewrite Module, use [AddIISUrlRewrite](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.iisurlrewriteoptionsextensions.addiisurlrewrite). Make sure that the rules file is deployed with the app. Don't direct the middleware to use the app's *web.config* file when running on Windows Server IIS. With IIS, these rules should be stored outside of the app's *web.config* file in order to avoid conflicts with the IIS Rewrite module. For more information and examples of IIS URL Rewrite Module rules, see [Using Url Rewrite Module 2.0](/en-us/iis/extensions/url-rewrite-module/using-url-rewrite-module-20) and [URL Rewrite Module Configuration Reference](/en-us/iis/extensions/url-rewrite-module/url-rewrite-module-configuration-reference).

A [StreamReader](/en-us/dotnet/api/system.io.streamreader) is used to read the rules from the `IISUrlRewrite.xml` rules file:

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

The sample app rewrites requests from `/iis-rules-rewrite/(.*)` to `/rewritten?id=$1`. The response is sent to the client with a *200 - OK* status code.

```
<rewrite>
  <rules>
    <rule name="Rewrite segment to id querystring" stopProcessing="true">
      <match url="^iis-rules-rewrite/(.*)$" />
      <action type="Rewrite" url="rewritten?id={R:1}" appendQueryString="false"/>
    </rule>
  </rules>
</rewrite>
```

Original Request: `/iis-rules-rewrite/1234`

If you have an active IIS Rewrite Module with server-level rules configured that would impact your app in undesirable ways, you can disable the IIS Rewrite Module for an app. For more information, see [Disabling IIS modules](../host-and-deploy/iis/modules?view=aspnetcore-10.0#disabling-iis-modules).

#### Unsupported features

The middleware doesn't support the following IIS URL Rewrite Module features:

* Outbound Rules
* Custom Server Variables
* Wildcards
* LogRewrittenUrl

#### Supported server variables

The middleware supports the following IIS URL Rewrite Module server variables:

* CONTENT\_LENGTH
* CONTENT\_TYPE
* HTTP\_ACCEPT
* HTTP\_CONNECTION
* HTTP\_COOKIE
* HTTP\_HOST
* HTTP\_REFERER
* HTTP\_URL
* HTTP\_USER\_AGENT
* HTTPS
* LOCAL\_ADDR
* QUERY\_STRING
* REMOTE\_ADDR
* REMOTE\_PORT
* REQUEST\_FILENAME
* REQUEST\_URI

Note

You can also obtain an [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider) via a [PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider). This approach may provide greater flexibility for the location of your rewrite rules files. Make sure that your rewrite rules files are deployed to the server at the path you provide.

```
PhysicalFileProvider fileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory());
```

### Method-based rule

Use [Add](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.add) to implement your own rule logic in a method. `Add` exposes the [RewriteContext](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewritecontext), which makes available the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) for use in your method. The [RewriteContext.Result](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewritecontext.result) determines how additional pipeline processing is handled. Set the value to one of the [RuleResult](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.ruleresult) fields described in the following table.

| Rewrite context result               | Action                                                           |
| ------------------------------------ | ---------------------------------------------------------------- |
| `RuleResult.ContinueRules` (default) | Continue applying rules.                                         |
| `RuleResult.EndResponse`             | Stop applying rules and send the response.                       |
| `RuleResult.SkipRemainingRules`      | Stop applying rules and send the context to the next middleware. |

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

The sample app demonstrates a method that redirects requests for paths that end with `.xml`. If a request is made for `/file.xml`, the request is redirected to `/xmlfiles/file.xml`. The status code is set to `301 - Moved Permanently`. When the browser makes a new request for `/xmlfiles/file.xml`, static file middleware serves the file to the client from the *wwwroot/xmlfiles* folder. For a redirect, explicitly set the status code of the response. Otherwise, a *200 - OK* status code is returned, and the redirect doesn't occur on the client.

`RewriteRules.cs`:

```
public static void RedirectXmlFileRequests(RewriteContext context)
{
    var request = context.HttpContext.Request;

    // Because the client is redirecting back to the same app, stop 
    // processing if the request has already been redirected.
    if (request.Path.StartsWithSegments(new PathString("/xmlfiles")))
    {
        return;
    }

    if (request.Path.Value.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
    {
        var response = context.HttpContext.Response;
        response.StatusCode = (int) HttpStatusCode.MovedPermanently;
        context.Result = RuleResult.EndResponse;
        response.Headers[HeaderNames.Location] = 
            "/xmlfiles" + request.Path + request.QueryString;
    }
}
```

This approach can also rewrite requests. The sample app demonstrates rewriting the path for any text file request to serve the *file.txt* text file from the *wwwroot* folder. Static file middleware serves the file based on the updated request path:

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

`RewriteRules.cs`:

```
public static void RewriteTextFileRequests(RewriteContext context)
{
    var request = context.HttpContext.Request;

    if (request.Path.Value.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
    {
        context.Result = RuleResult.SkipRemainingRules;
        request.Path = "/file.txt";
    }
}
```

### IRule-based rule

Use [Add](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.add) to use rule logic in a class that implements the [IRule](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.irule) interface. `IRule` provides greater flexibility over using the method-based rule approach. Your implementation class may include a constructor that allows you can pass in parameters for the [ApplyRule](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.irule.applyrule) method.

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

The values of the parameters in the sample app for the `extension` and the `newPath` are checked to meet several conditions. The `extension` must contain a value, and the value must be `.png`, `.jpg`, or *.gif*. If the `newPath` isn't valid, an [ArgumentException](/en-us/dotnet/api/system.argumentexception) is thrown. If a request is made for `image.png`, the request is redirected to `/png-images/image.png`. If a request is made for `image.jpg`, the request is redirected to `/jpg-images/image.jpg`. The status code is set to `301 - Moved Permanently`, and the `context.Result` is set to stop processing rules and send the response.

```
public class RedirectImageRequests : IRule
{
    private readonly string _extension;
    private readonly PathString _newPath;

    public RedirectImageRequests(string extension, string newPath)
    {
        if (string.IsNullOrEmpty(extension))
        {
            throw new ArgumentException(nameof(extension));
        }

        if (!Regex.IsMatch(extension, @"^\.(png|jpg|gif)$"))
        {
            throw new ArgumentException("Invalid extension", nameof(extension));
        }

        if (!Regex.IsMatch(newPath, @"(/[A-Za-z0-9]+)+?"))
        {
            throw new ArgumentException("Invalid path", nameof(newPath));
        }

        _extension = extension;
        _newPath = new PathString(newPath);
    }

    public void ApplyRule(RewriteContext context)
    {
        var request = context.HttpContext.Request;

        // Because we're redirecting back to the same app, stop 
        // processing if the request has already been redirected
        if (request.Path.StartsWithSegments(new PathString(_newPath)))
        {
            return;
        }

        if (request.Path.Value.EndsWith(_extension, StringComparison.OrdinalIgnoreCase))
        {
            var response = context.HttpContext.Response;
            response.StatusCode = (int) HttpStatusCode.MovedPermanently;
            context.Result = RuleResult.EndResponse;
            response.Headers[HeaderNames.Location] = 
                _newPath + request.Path + request.QueryString;
        }
    }
}
```

Original Request: `/image.png`

Original Request: `/image.jpg`

[]()

## Regex examples

| Goal                              | Regex String & Match Example                                                        | Replacement String & Output Example                      |
| --------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------- |
| Rewrite path into query string    | `^path/(.*)/(.*)` `/path/abc/123`                                                   | `path?var1=$1&var2=$2` `/path?var1=abc&var2=123`         |
| Strip trailing slash              | `(.*)/$` `/path/`                                                                   | `$1` `/path`                                             |
| Enforce trailing slash            | `(.*[^/])$` `/path`                                                                 | `$1/` `/path/`                                           |
| Avoid rewriting specific requests | `^(.*)(?<!\.axd)$` or `^(?!.*\.axd$)(.*)$` Yes: `/resource.htm` No: `/resource.axd` | `rewritten/$1` `/rewritten/resource.htm` `/resource.axd` |
| Rearrange URL segments            | `path/(.*)/(.*)/(.*)` `path/1/2/3`                                                  | `path/$3/$2/$1` `path/3/2/1`                             |
| Replace a URL segment             | `^(.*)/segment2/(.*)` `/segment1/segment2/segment3`                                 | `$1/replaced/$2` `/segment1/replaced/segment3`           |

This document introduces URL rewriting with instructions on how to use URL rewriting middleware in ASP.NET Core apps.

URL rewriting is the act of modifying request URLs based on one or more predefined rules. URL rewriting creates an abstraction between resource locations and their addresses so that the locations and addresses aren't tightly linked. URL rewriting is valuable in several scenarios to:

* Move or replace server resources temporarily or permanently and maintain stable locators for those resources.
* Split request processing across different apps or across areas of one app.
* Remove, add, or reorganize URL segments on incoming requests.
* Optimize public URLs for Search Engine Optimization (SEO).
* Permit the use of friendly public URLs to help visitors predict the content returned by requesting a resource.
* Redirect insecure requests to secure endpoints.
* Prevent hotlinking, where an external site uses a hosted static asset on another site by linking the asset into its own content.

Note

URL rewriting can reduce the performance of an app. Where feasible, limit the number and complexity of rules.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/url-rewriting/samples/) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## URL redirect and URL rewrite

The difference in wording between *URL redirect* and *URL rewrite* is subtle but has important implications for providing resources to clients. ASP.NET Core's URL rewriting middleware is capable of meeting the need for both.

A *URL redirect* involves a client-side operation, where the client is instructed to access a resource at a different address than the client originally requested. This requires a round trip to the server. The redirect URL returned to the client appears in the browser's address bar when the client makes a new request for the resource.

If `/resource` is *redirected* to `/different-resource`, the server responds that the client should obtain the resource at `/different-resource` with a status code indicating that the redirect is either temporary or permanent.

When redirecting requests to a different URL, indicate whether the redirect is permanent or temporary by specifying the status code with the response:

* The `301 - Moved Permanently` status code is used where the resource has a new, permanent URL and you wish to instruct the client that all future requests for the resource should use the new URL. *The client may cache and reuse the response when a 301 status code is received.*

* The *302 - Found* status code is used where the redirection is temporary or generally subject to change. The 302 status code indicates to the client not to store the URL and use it in the future.

For more information on status codes, see [RFC 9110: Status Code Definitions](https://www.rfc-editor.org/rfc/rfc9110#name-status-codes).

A *URL rewrite* is a server-side operation that provides a resource from a different resource address than the client requested. Rewriting a URL doesn't require a round trip to the server. The rewritten URL isn't returned to the client and doesn't appear in the browser's address bar.

If `/resource` is *rewritten* to `/different-resource`, the server *internally* fetches and returns the resource at `/different-resource`.

Although the client might be able to retrieve the resource at the rewritten URL, the client isn't informed that the resource exists at the rewritten URL when it makes its request and receives the response.

## URL rewriting sample app

You can explore the features of the URL rewriting middleware with the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/url-rewriting/samples/). The app applies redirect and rewrite rules and shows the redirected or rewritten URL for several scenarios.

## When to use URL rewriting middleware

Use URL rewriting middleware when you're unable to use the following approaches:

* [URL Rewrite module with IIS on Windows Server](https://www.iis.net/downloads/microsoft/url-rewrite)
* [Apache mod\_rewrite module on Apache Server](https://httpd.apache.org/docs/2.4/rewrite/)
* [URL rewriting on Nginx](https://www.nginx.com/blog/creating-nginx-rewrite-rules/)

Also, use the middleware when the app is hosted on [HTTP.sys server](servers/httpsys?view=aspnetcore-10.0) (formerly called WebListener).

The main reasons to use the server-based URL rewriting technologies in IIS, Apache, and Nginx are:

* The middleware doesn't support the full features of these modules.

  Some of the features of the server modules don't work with ASP.NET Core projects, such as the `IsFile` and `IsDirectory` constraints of the IIS Rewrite module. In these scenarios, use the middleware instead.

* The performance of the middleware probably doesn't match that of the modules.

  Benchmarking is the only way to know for sure which approach degrades performance the most or if degraded performance is negligible.

## Package

To include the middleware in your project, add a package reference to the [Microsoft.AspNetCore.App metapackage](metapackage-app?view=aspnetcore-10.0) in the project file, which contains the [Microsoft.AspNetCore.Rewrite](https://www.nuget.org/packages/Microsoft.AspNetCore.Rewrite) package.

When not using the `Microsoft.AspNetCore.App` metapackage, add a project reference to the `Microsoft.AspNetCore.Rewrite` package.

## Extension and options

Establish URL rewrite and redirect rules by creating an instance of the [RewriteOptions](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptions) class with extension methods for each of your rewrite rules. Chain multiple rules in the order that you would like them processed. The `RewriteOptions` are passed into the URL rewriting middleware as it's added to the request pipeline with [UseRewriter](/en-us/dotnet/api/microsoft.aspnetcore.builder.rewritebuilderextensions.userewriter):

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

### Redirect non-www to www

Three options permit the app to redirect non-`www` requests to `www`:

* [AddRedirectToWwwPermanent](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttowwwpermanent): Permanently redirect the request to the `www` subdomain if the request is non-`www`. Redirects with a [Status308PermanentRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status308permanentredirect#microsoft-aspnetcore-http-statuscodes-status308permanentredirect) status code.

* [AddRedirectToWww](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttowww): Redirect the request to the `www` subdomain if the incoming request is non-`www`. Redirects with a [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) status code. An overload permits you to provide the status code for the response. Use a field of the [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) class for a status code assignment.

### URL redirect

Use [AddRedirect](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirect) to redirect requests. The first parameter contains your regex for matching on the path of the incoming URL. The second parameter is the replacement string. The third parameter, if present, specifies the status code. If you don't specify the status code, the status code defaults to *302 - Found*, which indicates that the resource is temporarily moved or replaced.

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

In a browser with developer tools enabled, make a request to the sample app with the path `/redirect-rule/1234/5678`. The regex matches the request path on `redirect-rule/(.*)`, and the path is replaced with `/redirected/1234/5678`. The redirect URL is sent back to the client with a *302 - Found* status code. The browser makes a new request at the redirect URL, which appears in the browser's address bar. Since no rules in the sample app match on the redirect URL:

* The second request receives a *200 - OK* response from the app.
* The body of the response shows the redirect URL.

A round trip is made to the server when a URL is *redirected*.

Warning

Be cautious when establishing redirect rules. Redirect rules are evaluated on every request to the app, including after a redirect. It's easy to accidentally create a *loop of infinite redirects*.

Original Request: `/redirect-rule/1234/5678`

The part of the expression contained within parentheses is called a *capture group*. The dot (`.`) of the expression means *match any character*. The asterisk (`*`) indicates *match the preceding character zero or more times*. Therefore, the last two path segments of the URL, `1234/5678`, are captured by capture group `(.*)`. Any value you provide in the request URL after `redirect-rule/` is captured by this single capture group.

In the replacement string, captured groups are injected into the string with the dollar sign (`$`) followed by the sequence number of the capture. The first capture group value is obtained with `$1`, the second with `$2`, and they continue in sequence for the capture groups in your regex. There's only one captured group in the redirect rule regex in the sample app, so there's only one injected group in the replacement string, which is `$1`. When the rule is applied, the URL becomes `/redirected/1234/5678`.

### URL redirect to a secure endpoint

Use [AddRedirectToHttps](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttohttps) to redirect HTTP requests to the same host and path using the HTTPS protocol. If the status code isn't supplied, the middleware defaults to *302 - Found*. If the port isn't supplied:

* The middleware defaults to `null`.
* The scheme changes to `https` (HTTPS protocol), and the client accesses the resource on port 443.

The following example shows how to set the status code to `301 - Moved Permanently` and change the port to 5001.

```
public void Configure(IApplicationBuilder app)
{
    var options = new RewriteOptions()
        .AddRedirectToHttps(301, 5001);

    app.UseRewriter(options);
}
```

Use [AddRedirectToHttpsPermanent](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addredirecttohttpspermanent) to redirect insecure requests to the same host and path with secure HTTPS protocol on port 443. The middleware sets the status code to `301 - Moved Permanently`.

```
public void Configure(IApplicationBuilder app)
{
    var options = new RewriteOptions()
        .AddRedirectToHttpsPermanent();

    app.UseRewriter(options);
}
```

Note

When redirecting to a secure endpoint without the requirement for additional redirect rules, we recommend using HTTPS redirection middleware. For more information, see the [Enforce HTTPS](../security/enforcing-ssl?view=aspnetcore-10.0#require-https) topic.

The sample app is capable of demonstrating how to use `AddRedirectToHttps` or `AddRedirectToHttpsPermanent`. Add the extension method to the `RewriteOptions`. Make an insecure request to the app at any URL. Dismiss the browser security warning that the self-signed certificate is untrusted or create an exception to trust the certificate.

Original Request using `AddRedirectToHttps(301, 5001)`: `http://localhost:5000/secure`

Original Request using `AddRedirectToHttpsPermanent`: `http://localhost:5000/secure`

### URL rewrite

Use [AddRewrite](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.addrewrite) to create a rule for rewriting URLs. The first parameter contains the regex for matching on the incoming URL path. The second parameter is the replacement string. The third parameter, `skipRemainingRules: {true|false}`, indicates to the middleware whether or not to skip additional rewrite rules if the current rule is applied.

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

Original Request: `/rewrite-rule/1234/5678`

The carat (`^`) at the beginning of the expression means that matching starts at the beginning of the URL path.

In the earlier example with the redirect rule, `redirect-rule/(.*)`, there's no carat (`^`) at the start of the regex. Therefore, any characters may precede `redirect-rule/` in the path for a successful match.

| Path                               | Match |
| ---------------------------------- | ----- |
| `/redirect-rule/1234/5678`         | Yes   |
| `/my-cool-redirect-rule/1234/5678` | Yes   |
| `/anotherredirect-rule/1234/5678`  | Yes   |

The rewrite rule, `^rewrite-rule/(\d+)/(\d+)`, only matches paths if they start with `rewrite-rule/`. In the following table, note the difference in matching.

| Path                              | Match |
| --------------------------------- | ----- |
| `/rewrite-rule/1234/5678`         | Yes   |
| `/my-cool-rewrite-rule/1234/5678` | No    |
| `/anotherrewrite-rule/1234/5678`  | No    |

Following the `^rewrite-rule/` portion of the expression, there are two capture groups, `(\d+)/(\d+)`. The `\d` signifies *match a digit (number)*. The plus sign (`+`) means *match one or more of the preceding character*. Therefore, the URL must contain a number followed by a forward-slash followed by another number. These capture groups are injected into the rewritten URL as `$1` and `$2`. The rewrite rule replacement string places the captured groups into the query string. The requested path of `/rewrite-rule/1234/5678` is rewritten to obtain the resource at `/rewritten?var1=1234&var2=5678`. If a query string is present on the original request, it's preserved when the URL is rewritten.

There's no round trip to the server to obtain the resource. If the resource exists, it's fetched and returned to the client with a *200 - OK* status code. Because the client isn't redirected, the URL in the browser's address bar doesn't change. Clients can't detect that a URL rewrite operation occurred on the server.

Note

Use `skipRemainingRules: true` whenever possible because matching rules is computationally expensive and increases app response time. For the fastest app response:

* Order rewrite rules from the most frequently matched rule to the least frequently matched rule.
* Skip the processing of the remaining rules when a match occurs and no additional rule processing is required.

### Apache mod\_rewrite

Apply Apache mod\_rewrite rules with [AddApacheModRewrite](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.apachemodrewriteoptionsextensions.addapachemodrewrite). Make sure that the rules file is deployed with the app. For more information and examples of mod\_rewrite rules, see [Apache mod\_rewrite](https://httpd.apache.org/docs/2.4/rewrite/).

A [StreamReader](/en-us/dotnet/api/system.io.streamreader) is used to read the rules from the *ApacheModRewrite.txt* rules file:

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

The sample app redirects requests from `/apache-mod-rules-redirect/(.\*)` to `/redirected?id=$1`. The response status code is *302 - Found*.

```
# Rewrite path with additional sub directory
RewriteRule ^/apache-mod-rules-redirect/(.*) /redirected?id=$1 [L,R=302]
```

Original Request: `/apache-mod-rules-redirect/1234`

The middleware supports the following Apache mod\_rewrite server variables:

* CONN\_REMOTE\_ADDR
* HTTP\_ACCEPT
* HTTP\_CONNECTION
* HTTP\_COOKIE
* HTTP\_FORWARDED
* HTTP\_HOST
* HTTP\_REFERER
* HTTP\_USER\_AGENT
* HTTPS
* IPV6
* QUERY\_STRING
* REMOTE\_ADDR
* REMOTE\_PORT
* REQUEST\_FILENAME
* REQUEST\_METHOD
* REQUEST\_SCHEME
* REQUEST\_URI
* SCRIPT\_FILENAME
* SERVER\_ADDR
* SERVER\_PORT
* SERVER\_PROTOCOL
* TIME
* TIME\_DAY
* TIME\_HOUR
* TIME\_MIN
* TIME\_MON
* TIME\_SEC
* TIME\_WDAY
* TIME\_YEAR

### IIS URL Rewrite Module rules

To use the same rule set that applies to the IIS URL Rewrite Module, use [AddIISUrlRewrite](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.iisurlrewriteoptionsextensions.addiisurlrewrite). Make sure that the rules file is deployed with the app. Don't direct the middleware to use the app's *web.config* file when running on Windows Server IIS. With IIS, these rules should be stored outside of the app's *web.config* file in order to avoid conflicts with the IIS Rewrite module. For more information and examples of IIS URL Rewrite Module rules, see [Using Url Rewrite Module 2.0](/en-us/iis/extensions/url-rewrite-module/using-url-rewrite-module-20) and [URL Rewrite Module Configuration Reference](/en-us/iis/extensions/url-rewrite-module/url-rewrite-module-configuration-reference).

A [StreamReader](/en-us/dotnet/api/system.io.streamreader) is used to read the rules from the `IISUrlRewrite.xml` rules file:

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

The sample app rewrites requests from `/iis-rules-rewrite/(.*)` to `/rewritten?id=$1`. The response is sent to the client with a *200 - OK* status code.

```
<rewrite>
  <rules>
    <rule name="Rewrite segment to id querystring" stopProcessing="true">
      <match url="^iis-rules-rewrite/(.*)$" />
      <action type="Rewrite" url="rewritten?id={R:1}" appendQueryString="false"/>
    </rule>
  </rules>
</rewrite>
```

Original Request: `/iis-rules-rewrite/1234`

If you have an active IIS Rewrite Module with server-level rules configured that would impact your app in undesirable ways, you can disable the IIS Rewrite Module for an app. For more information, see [Disabling IIS modules](../host-and-deploy/iis/modules?view=aspnetcore-10.0#disabling-iis-modules).

#### Unsupported features

The middleware released with ASP.NET Core 2.x doesn't support the following IIS URL Rewrite Module features:

* Outbound Rules
* Custom Server Variables
* Wildcards
* LogRewrittenUrl

#### Supported server variables

The middleware supports the following IIS URL Rewrite Module server variables:

* CONTENT\_LENGTH
* CONTENT\_TYPE
* HTTP\_ACCEPT
* HTTP\_CONNECTION
* HTTP\_COOKIE
* HTTP\_HOST
* HTTP\_REFERER
* HTTP\_URL
* HTTP\_USER\_AGENT
* HTTPS
* LOCAL\_ADDR
* QUERY\_STRING
* REMOTE\_ADDR
* REMOTE\_PORT
* REQUEST\_FILENAME
* REQUEST\_URI

Note

You can also obtain an [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider) via a [PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider). This approach may provide greater flexibility for the location of your rewrite rules files. Make sure that your rewrite rules files are deployed to the server at the path you provide.

```
PhysicalFileProvider fileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory());
```

### Method-based rule

Use [Add](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.add) to implement your own rule logic in a method. `Add` exposes the [RewriteContext](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewritecontext), which makes available the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) for use in your method. The [RewriteContext.Result](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewritecontext.result) determines how additional pipeline processing is handled. Set the value to one of the [RuleResult](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.ruleresult) fields described in the following table.

| Rewrite context result               | Action                                                           |
| ------------------------------------ | ---------------------------------------------------------------- |
| `RuleResult.ContinueRules` (default) | Continue applying rules.                                         |
| `RuleResult.EndResponse`             | Stop applying rules and send the response.                       |
| `RuleResult.SkipRemainingRules`      | Stop applying rules and send the context to the next middleware. |

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

The sample app demonstrates a method that redirects requests for paths that end with `.xml`. If a request is made for `/file.xml`, the request is redirected to `/xmlfiles/file.xml`. The status code is set to `301 - Moved Permanently`. When the browser makes a new request for `/xmlfiles/file.xml`, static file middleware serves the file to the client from the *wwwroot/xmlfiles* folder. For a redirect, explicitly set the status code of the response. Otherwise, a *200 - OK* status code is returned, and the redirect doesn't occur on the client.

`RewriteRules.cs`:

```
public static void RedirectXmlFileRequests(RewriteContext context)
{
    var request = context.HttpContext.Request;

    // Because the client is redirecting back to the same app, stop 
    // processing if the request has already been redirected.
    if (request.Path.StartsWithSegments(new PathString("/xmlfiles")))
    {
        return;
    }

    if (request.Path.Value.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
    {
        var response = context.HttpContext.Response;
        response.StatusCode = (int) HttpStatusCode.MovedPermanently;
        context.Result = RuleResult.EndResponse;
        response.Headers[HeaderNames.Location] = 
            "/xmlfiles" + request.Path + request.QueryString;
    }
}
```

This approach can also rewrite requests. The sample app demonstrates rewriting the path for any text file request to serve the *file.txt* text file from the *wwwroot* folder. Static file middleware serves the file based on the updated request path:

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

`RewriteRules.cs`:

```
public static void RewriteTextFileRequests(RewriteContext context)
{
    var request = context.HttpContext.Request;

    if (request.Path.Value.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
    {
        context.Result = RuleResult.SkipRemainingRules;
        request.Path = "/file.txt";
    }
}
```

### IRule-based rule

Use [Add](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.rewriteoptionsextensions.add) to use rule logic in a class that implements the [IRule](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.irule) interface. `IRule` provides greater flexibility over using the method-based rule approach. Your implementation class may include a constructor that allows you can pass in parameters for the [ApplyRule](/en-us/dotnet/api/microsoft.aspnetcore.rewrite.irule.applyrule) method.

```
public void Configure(IApplicationBuilder app)
{
    using (StreamReader apacheModRewriteStreamReader = 
        File.OpenText("ApacheModRewrite.txt"))
    using (StreamReader iisUrlRewriteStreamReader = 
        File.OpenText("IISUrlRewrite.xml")) 
    {
        var options = new RewriteOptions()
            .AddRedirect("redirect-rule/(.*)", "redirected/$1")
            .AddRewrite(@"^rewrite-rule/(\d+)/(\d+)", "rewritten?var1=$1&var2=$2", 
                skipRemainingRules: true)
            .AddApacheModRewrite(apacheModRewriteStreamReader)
            .AddIISUrlRewrite(iisUrlRewriteStreamReader)
            .Add(MethodRules.RedirectXmlFileRequests)
            .Add(MethodRules.RewriteTextFileRequests)
            .Add(new RedirectImageRequests(".png", "/png-images"))
            .Add(new RedirectImageRequests(".jpg", "/jpg-images"));

        app.UseRewriter(options);
    }

    app.UseStaticFiles();

    app.Run(context => context.Response.WriteAsync(
        $"Rewritten or Redirected Url: " +
        $"{context.Request.Path + context.Request.QueryString}"));
}
```

The values of the parameters in the sample app for the `extension` and the `newPath` are checked to meet several conditions. The `extension` must contain a value, and the value must be `.png`, `.jpg`, or *.gif*. If the `newPath` isn't valid, an [ArgumentException](/en-us/dotnet/api/system.argumentexception) is thrown. If a request is made for `image.png`, the request is redirected to `/png-images/image.png`. If a request is made for `image.jpg`, the request is redirected to `/jpg-images/image.jpg`. The status code is set to `301 - Moved Permanently`, and the `context.Result` is set to stop processing rules and send the response.

```
public class RedirectImageRequests : IRule
{
    private readonly string _extension;
    private readonly PathString _newPath;

    public RedirectImageRequests(string extension, string newPath)
    {
        if (string.IsNullOrEmpty(extension))
        {
            throw new ArgumentException(nameof(extension));
        }

        if (!Regex.IsMatch(extension, @"^\.(png|jpg|gif)$"))
        {
            throw new ArgumentException("Invalid extension", nameof(extension));
        }

        if (!Regex.IsMatch(newPath, @"(/[A-Za-z0-9]+)+?"))
        {
            throw new ArgumentException("Invalid path", nameof(newPath));
        }

        _extension = extension;
        _newPath = new PathString(newPath);
    }

    public void ApplyRule(RewriteContext context)
    {
        var request = context.HttpContext.Request;

        // Because we're redirecting back to the same app, stop 
        // processing if the request has already been redirected
        if (request.Path.StartsWithSegments(new PathString(_newPath)))
        {
            return;
        }

        if (request.Path.Value.EndsWith(_extension, StringComparison.OrdinalIgnoreCase))
        {
            var response = context.HttpContext.Response;
            response.StatusCode = (int) HttpStatusCode.MovedPermanently;
            context.Result = RuleResult.EndResponse;
            response.Headers[HeaderNames.Location] = 
                _newPath + request.Path + request.QueryString;
        }
    }
}
```

Original Request: `/image.png`

Original Request: `/image.jpg`

## Regex examples

| Goal                              | Regex String & Match Example                                                        | Replacement String & Output Example                      |
| --------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------- |
| Rewrite path into query string    | `^path/(.*)/(.*)` `/path/abc/123`                                                   | `path?var1=$1&var2=$2` `/path?var1=abc&var2=123`         |
| Strip trailing slash              | `(.*)/$` `/path/`                                                                   | `$1` `/path`                                             |
| Enforce trailing slash            | `(.*[^/])$` `/path`                                                                 | `$1/` `/path/`                                           |
| Avoid rewriting specific requests | `^(.*)(?<!\.axd)$` or `^(?!.*\.axd$)(.*)$` Yes: `/resource.htm` No: `/resource.axd` | `rewritten/$1` `/rewritten/resource.htm` `/resource.axd` |
| Rearrange URL segments            | `path/(.*)/(.*)/(.*)` `path/1/2/3`                                                  | `path/$3/$2/$1` `path/3/2/1`                             |
| Replace a URL segment             | `^(.*)/segment2/(.*)` `/segment1/segment2/segment3`                                 | `$1/replaced/$2` `/segment1/replaced/segment3`           |

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/url-rewriting/samples/) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))
* [`RewriteMiddleware` (reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Rewrite/src/RewriteMiddleware.cs)
* [App startup in ASP.NET Core](startup?view=aspnetcore-10.0)
* [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0)
* [Regular expressions in .NET](/en-us/dotnet/articles/standard/base-types/regular-expressions)
* [Regular expression language - quick reference](/en-us/dotnet/articles/standard/base-types/quick-ref)
* [Apache mod\_rewrite](https://httpd.apache.org/docs/2.4/rewrite/)
* [Using Url Rewrite Module 2.0 (for IIS)](/en-us/iis/extensions/url-rewrite-module/using-url-rewrite-module-20)
* [URL Rewrite Module Configuration Reference](/en-us/iis/extensions/url-rewrite-module/url-rewrite-module-configuration-reference)
* [Keep a simple URL structure](https://support.google.com/webmasters/answer/76329?hl=en)
* [10 URL Rewriting Tips and Tricks](https://ruslany.net/2009/04/10-url-rewriting-tips-and-tricks/)
* [To slash or not to slash](https://developers.google.com/search/blog/2010/04/to-slash-or-not-to-slash)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-18

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/social/google-logins.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Google external login setup in ASP.NET Core

By [Valeriy Novytskyy](https://github.com/01binary), [Rick Anderson](https://twitter.com/RickAndMSFT), and [Sharaf Abacery](https://github.com/sharafabacery)

This tutorial shows how to enable user sign in with Google accounts using a sample ASP.NET Core project created in [Using external login providers with Identity in ASP.NET Core](./?view=aspnetcore-10.0). Follow Google's official guidance in [Sign in with Google for Web: Setup](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid) to create a Google API client ID.

## Create the app in Google

* Navigate to the [Google API & Services](https://console.cloud.google.com/apis) page of the Google Cloud platform.

* If no project exists, create a new project by selecting the **Create Project** button. To select a different project than an existing project that loads, select the loaded project's button in the top-left corner of the UI, followed by the project. To add a new project, select the loaded project's button in the top-left corner of the UI, followed by the **New project** button.

* When creating a new project:

  * Enter a **Project name**.
  * Optionally, select an **Organization** for the project.
  * Select the **Create** button.

After creating the project, the **Dashboard** page of the project loads, where it's possible to configure the project.

Open the **Credentials** tab to create the OAuth client.

The prerequisite to creating the credentials is to configure the OAuth consent screen. If the consent isn't configured, there's a prompt to configure the consent screen.

* Select **Configure consent screen** or select **OAuth consent screen** in the sidebar.
* In the **OAuth consent screen**, select **Get started**.
* Set the **App name** and **User support email**.
* Set the audience type to **External**.
* Add **Contact information** by entering a contact email address.
* Agree to the terms.
* Select **Create**.

Create the client credentials for the app by opening the **Clients** sidebar menu item:

* Select the **Create client** button.
* Select **Web application** as the **Application type**.
* Enter a **Name** for the client.
* Add an **Authorized redirect URI**. For local testing, use the default address `https://localhost:{PORT}/signin-google`, where the `{PORT}` placeholder is the app's port.
* Select the **Create** button to create the client.
* Save the **Client ID** and **Client secret**, which are used later in the ASP.NET app configuration.

Note

The URI segment `/signin-google` is set as the default callback of the Google authentication provider. It's possible to change the default callback URI while configuring the Google authentication middleware via the inherited [RemoteAuthenticationOptions.CallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.callbackpath) property.

When deploying the app, either:

* Update the app's redirect URI in the **Google Console** to the app's deployed redirect URI.
* Create a new Google API registration in the **Google Console** for the production app with its production redirect URI.

## Store the Google client ID and secret

Store sensitive settings, such as the Google client ID and secret values, with [Secret Manager](../../app-secrets?view=aspnetcore-10.0). For this sample, follow these steps:

1. Initialize the project for secret storage according to the instructions in [Safe storage of app secrets in development](../../app-secrets?view=aspnetcore-10.0).

2. Store the sensitive settings in the local secret store with the secret keys `Authentication:Google:ClientId` (value: `{CLIENT ID}` placeholder) and `Authentication:Google:ClientSecret` (value: `{CLIENT SECRET}` placeholder):

   ```
   dotnet user-secrets set "Authentication:Google:ClientId" "{CLIENT ID}"
   dotnet user-secrets set "Authentication:Google:ClientSecret" "{CLIENT SECRET}"
   ```

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For example, [Bash](https://linuxhint.com/bash-environment-variables/) doesn't support colon (`:`) as a separator. All platforms support the double underscore (`__`) syntax and automatically replace it with a colon (`:`).

Manage API credentials and usage in the [API Console](https://console.developers.google.com/apis/dashboard).

## Configure Google authentication

To use Google authentication with OpenID Connect, add the [`Google.Apis.Auth.AspNetCore3` NuGet package](https://www.nuget.org/packages/Google.Apis.Auth.AspNetCore3) to the app:

```
dotnet add package Google.Apis.Auth.AspNetCore3
```

Add the authentication service to the `Program` file:

```
services.AddAuthentication().AddGoogleOpenIdConnect(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];
});
```

Alternatively, to use Google OAuth authentication with `AddGoogle`, add the [`Microsoft.AspNetCore.Authentication.Google` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Google) to the app:

```
dotnet add package Microsoft.AspNetCore.Authentication.Google
```

Add the authentication service to the `Program` file:

```
builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = builder.Configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
});
```

Add the [`Microsoft.AspNetCore.Authentication.Google` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Google) to the app:

```
dotnet add package Microsoft.AspNetCore.Authentication.Google
```

Add the authentication service to `Startup.ConfigureServices`:

```
services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];
});
```

The [AddAuthentication(IServiceCollection, String)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-string\)) overload sets the [DefaultScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultscheme) property. The [AddAuthentication(IServiceCollection, Action\<AuthenticationOptions>)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-action\(\(microsoft-aspnetcore-authentication-authenticationoptions\)\)\)) overload allows configuring authentication options, which can be used to set up default authentication schemes for different purposes. Subsequent calls to `AddAuthentication` override previously configured [AuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.authenticationoptions) properties.

[AuthenticationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder) extension methods that register an authentication handler may only be called once per authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display name.

## Sign in with Google

* Run the app and select **Log in**.
* Under **Use another service to log in.**, select Google.
* The browser is redirected to **Google** for authentication.
* Select the Google account to log in or enter Google credentials.
* If this is the first time signing in, there's a prompt to allow the app to access the Google account information.
* The browser is redirected back to the app, where it's possible to set the email.

The user is now logged in using Google credentials.

## Troubleshooting

* If the sign-in doesn't work without receiving any errors, switch to development mode to make the app and Google registration easier to debug.
* If the site's database hasn't been created by applying the initial migration, the following error occurs: *A database operation failed while processing the request*. Select **Apply Migrations** to create the database and refresh the page to continue past the error.
* For information about an HTTP 500 error after successfully authenticating the request by the OAuth 2.0 provider, such as Google, and information on how to implement external authentication with Google for React and other SPA apps, see [Middleware not handling 'signin-google' route after successful authentication in Asp.Net Core Web Api External Login Authentication (`dotnet/AspNetCore.Docs` #14169)](https://github.com/dotnet/AspNetCore.Docs/issues/14169).

## Next steps

* This article demonstrates authentication with Google. For information on authenticating with other external providers, see [Using external login providers with Identity in ASP.NET Core](./?view=aspnetcore-10.0).
* After the app is deployed to Azure, reset the `ClientSecret` in the Google API console.
* Set the `Authentication:Google:ClientId` and `Authentication:Google:ClientSecret` as app settings in the Azure portal. The configuration system is set up to read keys from the environment variables.

## Related content

[Multiple authentication providers](./?view=aspnetcore-10.0#multiple-authentication-providers)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-09

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/hosting
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/hosting.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Framework HttpApplicationHost

The System.Web adapters library enables ASP.NET Framework applications to use the .NET generic host pattern, bringing modern application infrastructure capabilities to traditional ASP.NET Framework projects. This approach provides access to dependency injection, logging, configuration, and other services that are standard in modern .NET applications, while maintaining compatibility with existing ASP.NET Framework code.

## Why use the host pattern

Adopting the generic host pattern in ASP.NET Framework applications provides several key benefits:

* **Dependency Injection**: Access to the built-in dependency injection container used in modern .NET, enabling better testability, maintainability, and separation of concerns
* **Unified Logging**: Integration with `Microsoft.Extensions.Logging`, providing a consistent logging experience across your application and access to various logging providers
* **Modern Configuration**: Use the configuration system from modern .NET, including support for JSON files, environment variables, user secrets, and other configuration sources
* **Migration Path**: Prepare your codebase for migration to ASP.NET Core by adopting modern patterns incrementally while maintaining your existing ASP.NET Framework application
* **Service Defaults**: Leverage service defaults and standardized application infrastructure patterns commonly used in modern .NET applications

## Setting up the generic host

To configure the generic host pattern in an ASP.NET Framework application, register the host in the `Application_Start` method of your `Global.asax.cs` file:

```
using System.Web;
using Microsoft.AspNetCore.SystemWebAdapters.Hosting;
using Microsoft.Extensions.Hosting;

namespace MvcApp
{
    public class MvcApplication : HttpApplication
    {
        protected void Application_Start()
        {
            HttpApplicationHost.RegisterHost(builder =>
            {
                builder.AddServiceDefaults();
                builder.AddSystemWebDependencyInjection();
                builder.AddSystemWebAdapters();
            });

            // Existing ASP.NET Framework configuration
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
    }
}
```

The `HttpApplicationHost.RegisterHost` method configures a generic host that runs alongside your ASP.NET Framework application. The host builder accepts configuration through extension methods that add various capabilities.

## Dependency injection

The `AddSystemWebDependencyInjection` method enables dependency injection throughout your ASP.NET Framework application. Services registered with the generic host's service collection become available to controllers, handlers, and other components.

This extension method is an internal method that will be source generated depending on what you have referenced. The following frameworks are supported:

* WebForms and handlers using the [WebObjectActivator](/en-us/dotnet/api/system.web.httpruntime.webobjectactivator#system-web-httpruntime-webobjectactivator)
* ASP.NET MVC using [DependencyResolver](/en-us/dotnet/api/system.web.mvc.dependencyresolver)
* ASP.NET WebApi using [DependencyResolver](/en-us/previous-versions/aspnet/hh969140\(v=vs.108\))

Note

The `builder.AddSystemWebDependencyInjection()` extension method is generated by a C# source generator and is only available in C# projects. For VB.NET apps, create an intermediate C# class library that references the ASP.NET MVC/Web API and SystemWebAdapters packages. Configure dependency injection in that library and expose a public method that your VB.NET project can call.

### Registering services

Register services with the dependency injection container by accessing the `Services` property on the host builder:

```
HttpApplicationHost.RegisterHost(builder =>
{    
    builder.AddSystemWebDependencyInjection();
    builder.AddSystemWebAdapters();

    builder.Services.AddScoped<IMyService, MyService>();
    builder.Services.AddSingleton<IDataRepository, DataRepository>();
});
```

For details on how to customize the dependency injection system, including replacing the service container, see the full documentation: [Dependency injection in ASP.NET Core](../../../fundamentals/dependency-injection?view=aspnetcore-10.0).

If you are currently overriding the dependency injection hooks in the ASP.NET Framework, you will need to update to the current pattern for your dependency injection and remove the existing integrations for WebForms/MVC4/WebAPI.

### Consuming services

Once registered, services can be injected into MVC controllers, Web API controllers, and other components that support dependency injection:

```
public class HomeController : Controller
{
    private readonly IMyService _myService;
    private readonly ILogger<HomeController> _logger;

    public HomeController(IMyService myService, ILogger<HomeController> logger)
    {
        _myService = myService;
        _logger = logger;
    }

    public ActionResult Index()
    {
        _logger.LogInformation("Loading home page");
        var data = _myService.GetData();
        return View(data);
    }
}
```

Within a request, a scoped set of services may be retrieved by a special extension method:

```
var myService = HttpContext.Current.GetRequestServices().GetService(typeof(ISomeType));
```

Warning

[HttpContext](/en-us/dotnet/api/system.web.httpcontext) implements [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) so you may see extension methods to get services from it. However, it is not implemented in a way to retrieve any useful services from a registered container. You will see an error if you attempt to use it and you should instead use the `GetRequestServices()` extension method first.

## Logging

The generic host integrates `Microsoft.Extensions.Logging`, providing a modern logging infrastructure for your ASP.NET Framework application.

### Using loggers

Inject `ILogger<T>` into your classes to log messages:

```
public class OrderService : IOrderService
{
    private readonly ILogger<OrderService> _logger;

    public OrderService(ILogger<OrderService> logger)
    {
        _logger = logger;
    }

    public void ProcessOrder(int orderId)
    {
        _logger.LogInformation("Processing order {OrderId}", orderId);
        
        try
        {
            // Process order logic
            _logger.LogDebug("Order {OrderId} processed successfully", orderId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing order {OrderId}", orderId);
            throw;
        }
    }
}
```

For details on customizing and using logging, including integrating with your own providers, see [Logging in .NET and ASP.NET Core](../../../fundamentals/logging/?view=aspnetcore-10.0).

## Configuration

The generic host supports the modern .NET configuration system, including JSON files, environment variables, and other configuration sources. By default, the `HttpApplicationHost` uses standard configuration patterns (such as `appsettings.json` and `appsettings.{Environment}.json`), and automatically includes values from [AppSettings](/en-us/dotnet/api/system.configuration.configurationmanager.appsettings#system-configuration-configurationmanager-appsettings) and [ConnectionStrings](/en-us/dotnet/api/system.configuration.configurationmanager.connectionstrings#system-configuration-configurationmanager-connectionstrings) for compatibility with existing ASP.NET Framework applications.

### Accessing configuration

Inject `IConfiguration` to access configuration values:

```
public class EmailService : IEmailService
{
    private readonly string _smtpServer;
    private readonly ILogger<EmailService> _logger;

    public EmailService(IConfiguration configuration, ILogger<EmailService> logger)
    {
        _smtpServer = configuration["Email:SmtpServer"];
        _logger = logger;
    }

    public void SendEmail(string to, string subject, string body)
    {
        _logger.LogInformation("Sending email to {Recipient} via {SmtpServer}", to, _smtpServer);
        // Email sending logic
    }
}
```

For code that can't use dependency injection, use the static `AppConfiguration` helper class to access configuration values:

```
using Microsoft.AspNetCore.SystemWebAdapters;

var configValue = AppConfiguration.GetSetting("SomeSettings");
var connStr = AppConfiguration.GetConnectionString("connection-string-name");
```

This will work on both ASP.NET Core and ASP.NET Framework applications that are referencing `Microsoft.AspNetCore.SystemWebAdapters` and will provide access to the current `IConfiguration`.

### Using options pattern

The options pattern provides strongly-typed access to configuration sections:

```
public class EmailSettings
{
    public string SmtpServer { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
}

// Register options
HttpApplicationHost.RegisterHost(builder =>
{
    builder.Services.Configure<EmailSettings>(
        builder.Configuration.GetSection("Email"));

    builder.AddSystemWebDependencyInjection();
    builder.AddSystemWebAdapters();
});

// Consume options
public class EmailService : IEmailService
{
    private readonly EmailSettings _settings;
    private readonly ILogger<EmailService> _logger;

    public EmailService(IOptions<EmailSettings> options, ILogger<EmailService> logger)
    {
        _settings = options.Value;
        _logger = logger;
    }
}
```

For advanced configuration scenarios, including custom configuration sources and environment-specific settings, see [Configuration in ASP.NET Core](../../../fundamentals/configuration/?view=aspnetcore-10.0).

## Migration strategies

Completely converting to the generic host pattern may not be needed for smaller projects, but can make migration much easier for larger projects. If you have successfully converted to using this, then you can swap the `HttpApplicationHost` usage for the `WebApplication` in ASP.NET Core.

While migrating to this setup, small incremental changes will help ensure a successful conversion. A recommended order to use for migrating here would be the following:

1. Add an initial host registration:

   ```
   HttpApplicationHost.RegisterHost(builder =>
   {
   });
   ```

2. Update to use Microsoft Extensions dependency injection:

   ```
   HttpApplicationHost.RegisterHost(builder =>
   {
       builder.AddSystemWebDependencyInjection();
   });
   ```

   While doing this, you may need to adapt your current containers according to their own documentation for integrating in with the Microsoft Extensions dependency injection

3. Identify your logging system and integrate it into the Microsoft Extension logging infrastructure. Decide if you want to continue using the logger types from your existing system or migrating to the `ILogger<>` types the HttpApplicationHost will provide.

4. Replace all calls to [AppSettings](/en-us/dotnet/api/system.configuration.configurationmanager.appsettings#system-configuration-configurationmanager-appsettings) and [ConnectionStrings](/en-us/dotnet/api/system.configuration.configurationmanager.connectionstrings#system-configuration-configurationmanager-connectionstrings) with the new equivalent `AppConfiguration` call. You must be referencing `Microsoft.AspNetCore.SystemWebAdapters` in the project where you want to do this.

5. Start using the options pattern to convert configuration into POCO that can be passed to consuming services using the integrated DI system

6. Move your settings from `web.config` to `appsettings.json`

## Related content

* [.NET Generic Host in ASP.NET Core](../../../fundamentals/host/generic-host?view=aspnetcore-10.0)
* [Dependency injection in ASP.NET Core](../../../fundamentals/dependency-injection?view=aspnetcore-10.0)
* [Logging in .NET and ASP.NET Core](../../../fundamentals/logging/?view=aspnetcore-10.0)
* [Configuration in ASP.NET Core](../../../fundamentals/configuration/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/dotnet-client
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/dotnet-client.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core SignalR .NET client

The ASP.NET Core SignalR .NET client library lets you communicate with SignalR hubs from .NET apps. This article describes how to use the APIs to connect to a SignalR hub, and call the .NET hub and client methods. The code sample in this article is a Windows Presentation Foundation (WPF) app that uses the ASP.NET Core SignalR .NET client.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/dotnet-client/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Install the SignalR .NET client package

The [Microsoft.AspNetCore.SignalR.Client](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client) package is required for .NET clients to connect to SignalR hubs. You can install the client library from the Visual Studio **Package Manager Console** or by using the .NET CLI.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

Run the following command in the **Package Manager Console** window:

```
Install-Package Microsoft.AspNetCore.SignalR.Client
```

Run the following command in a command shell:

```
dotnet add package Microsoft.AspNetCore.SignalR.Client
```

## Connect to a hub

To establish a connection, create a `HubConnectionBuilder` and call `Build`. The hub URL, protocol, transport type, log level, headers, and other options can be configured while building a connection. Configure any required options by inserting any of the `HubConnectionBuilder` methods into `Build`. Start the connection with `StartAsync`.

```
using System;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.AspNetCore.SignalR.Client;

namespace SignalRChatClient
{
    public partial class MainWindow : Window
    {
        HubConnection connection;
        public MainWindow()
        {
            InitializeComponent();

            connection = new HubConnectionBuilder()
                .WithUrl("http://localhost:53353/ChatHub")
                .Build();

            connection.Closed += async (error) =>
            {
                await Task.Delay(new Random().Next(0,5) * 1000);
                await connection.StartAsync();
            };
        }

        private async void connectButton_Click(object sender, RoutedEventArgs e)
        {
            connection.On<string, string>("ReceiveMessage", (user, message) =>
            {
                this.Dispatcher.Invoke(() =>
                {
                   var newMessage = $"{user}: {message}";
                   messagesList.Items.Add(newMessage);
                });
            });

            try
            {
                await connection.StartAsync();
                messagesList.Items.Add("Connection started");
                connectButton.IsEnabled = false;
                sendButton.IsEnabled = true;
            }
            catch (Exception ex)
            {
                messagesList.Items.Add(ex.Message);
            }
        }

        private async void sendButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                await connection.InvokeAsync("SendMessage", 
                    userTextBox.Text, messageTextBox.Text);
            }
            catch (Exception ex)
            {                
                messagesList.Items.Add(ex.Message);                
            }
        }
    }
}
```

## Handle lost connection

To reconnect with clients, you can set up automatic reconnection or configure the reconnection manually.

### Automatically reconnect

The [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection) can be configured to automatically reconnect by using the `WithAutomaticReconnect` method on the [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). It doesn't automatically reconnect by default.

```
HubConnection connection= new HubConnectionBuilder()
    .WithUrl(new Uri("http://127.0.0.1:5000/chathub"))
    .WithAutomaticReconnect()
    .Build();
```

Without any parameters, `WithAutomaticReconnect()` configures the client to wait 0, 2, 10, and 30 seconds respectively before trying each reconnect attempt. It stops after four failed attempts.

Before starting any reconnect attempts, the `HubConnection` transitions to the `HubConnectionState.Reconnecting` state and fires the `Reconnecting` event. This approach provides an opportunity to warn users that the connection is lost and to disable UI elements. Non-interactive apps can start queuing or dropping messages.

```
connection.Reconnecting += error =>
{
    Debug.Assert(connection.State == HubConnectionState.Reconnecting);

    // Notify users the connection was lost and the client is reconnecting.
    // Start queuing or dropping messages.

    return Task.CompletedTask;
};
```

If the client successfully reconnects within its first four attempts, the `HubConnection` transitions back to the `Connected` state and fires the `Reconnected` event. This approach provides an opportunity to inform users the connection is now reestablished and to dequeue any queued messages.

Because the connection looks entirely new to the server, a new `ConnectionId` is provided to the `Reconnected` event handlers.

Warning

The `Reconnected` event handler's `connectionId` parameter is `null` if the `HubConnection` is configured to [skip negotiation](configuration?view=aspnetcore-10.0#configure-client-options).

```
connection.Reconnected += connectionId =>
{
    Debug.Assert(connection.State == HubConnectionState.Connected);

    // Notify users the connection was reestablished.
    // Start dequeuing messages queued while reconnecting if any.

    return Task.CompletedTask;
};
```

`WithAutomaticReconnect()` doesn't configure the `HubConnection` to retry initial start failures, so start failures need to be handled manually:

```
public static async Task<bool> ConnectWithRetryAsync(HubConnection connection, CancellationToken token)
{
    // Keep trying to until we can start or the token is canceled.
    while (true)
    {
        try
        {
            await connection.StartAsync(token);
            Debug.Assert(connection.State == HubConnectionState.Connected);
            return true;
        }
        catch when (token.IsCancellationRequested)
        {
            return false;
        }
        catch
        {
            // Failed to connect, trying again in 5000 ms.
            Debug.Assert(connection.State == HubConnectionState.Disconnected);
            await Task.Delay(5000);
        }
    }
}
```

If the client doesn't successfully reconnect within its first four attempts, the `HubConnection` transitions to the `Disconnected` state and fires the [Closed](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.closed#microsoft-aspnetcore-signalr-client-hubconnection-closed) event. This approach provides an opportunity to attempt to restart the connection manually or inform users the connection is now permanently lost.

```
connection.Closed += error =>
{
    Debug.Assert(connection.State == HubConnectionState.Disconnected);

    // Notify users the connection has been closed or manually try to restart the connection.

    return Task.CompletedTask;
};
```

To configure a custom number of reconnect attempts before disconnecting or change the reconnect timing, `WithAutomaticReconnect` accepts an array of numbers representing the delay in milliseconds to wait before starting each reconnect attempt.

```
HubConnection connection = new HubConnectionBuilder()
    .WithUrl(new Uri("http://127.0.0.1:5000/chathub"))
    .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.Zero, TimeSpan.FromSeconds(10) })
    .Build();

    // .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30) }) yields the default behavior.
```

The preceding example configures the `HubConnection` to start attempting reconnects immediately after the connection is lost. This approach is also true for the default configuration.

* If the first reconnect attempt fails, the second reconnect attempt also starts immediately instead of waiting 2 seconds as defined in the default configuration.

* If the second reconnect attempt fails, the third reconnect attempt starts in 10 seconds, which is the same behavior defined in the default configuration.

* The custom behavior then diverges again from the default behavior by stopping after the third reconnect attempt failure. In the default configuration, one more reconnect attempt is made after another 30 seconds.

For more control over the timing and number of automatic reconnect attempts, `WithAutomaticReconnect` accepts an object implementing the `IRetryPolicy` interface, which has a single method named `NextRetryDelay`. `NextRetryDelay` takes a single argument with the type `RetryContext`. The `RetryContext` has three properties: `PreviousRetryCount` (type `long`), `ElapsedTime` (type `TimeSpan`), and `RetryReason` (type `Exception`).

* Before the first reconnect attempt, `PreviousRetryCount` and `ElapsedTime` are both zero (0), and `RetryReason` is the Exception that caused the lost connection.

* After each failed retry attempt, `PreviousRetryCount` increments by one, `ElapsedTime` updates to reflect the amount of time spent reconnecting so far, and `RetryReason` is the Exception that caused the last reconnect attempt to fail.

`NextRetryDelay` must return either a `TimeSpan` value that represents the time to wait before the next reconnect attempt or `null` if the `HubConnection` should stop reconnecting.

```
public class RandomRetryPolicy : IRetryPolicy
{
    private readonly Random _random = new Random();

    public TimeSpan? NextRetryDelay(RetryContext retryContext)
    {
        // If we've been reconnecting for less than 60 seconds so far,
        // wait between 0 and 10 seconds before the next reconnect attempt.
        if (retryContext.ElapsedTime < TimeSpan.FromSeconds(60))
        {
            return TimeSpan.FromSeconds(_random.NextDouble() * 10);
        }
        else
        {
            // If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
            return null;
        }
    }
}
```

```
HubConnection connection = new HubConnectionBuilder()
    .WithUrl(new Uri("http://127.0.0.1:5000/chathub"))
    .WithAutomaticReconnect(new RandomRetryPolicy())
    .Build();
```

Alternatively, you can write code to reconnect your client manually, as demonstrated in the next section.

### Manually reconnect

Warning

In versions earlier than 3.0, the .NET client for SignalR doesn't automatically reconnect. You must write code to reconnect your client manually.

Use the [Closed](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.closed#microsoft-aspnetcore-signalr-client-hubconnection-closed) event to respond to a lost connection. For example, you might want to automate reconnection.

The `Closed` event requires a delegate that returns a `Task`, which allows async code to run without using `async void`. To satisfy the delegate signature in a `Closed` event handler that runs synchronously, return `Task.CompletedTask`:

```
connection.Closed += (error) => {
    // Do your close logic.
    return Task.CompletedTask;
};
```

The main reason for the async support is so you can restart the connection. Starting a connection is an async action.

In a `Closed` handler that restarts the connection, consider waiting for some random delay to prevent overloading the server, as shown in the following example:

```
connection.Closed += async (error) =>
{
    await Task.Delay(new Random().Next(0,5) * 1000);
    await connection.StartAsync();
};
```

## Call hub methods from client

`InvokeAsync` calls methods on the hub. Pass the hub method name and any arguments defined in the hub method to `InvokeAsync`. SignalR is asynchronous, so use `async` and `await` when making the calls.

```
await connection.InvokeAsync("SendMessage", 
    userTextBox.Text, messageTextBox.Text);
```

The `InvokeAsync` method returns a `Task` that completes when the server method returns. The return value, if any, is provided as the result of the `Task`. Any exceptions thrown by the method on the server produce a faulted `Task`. Use `await` syntax to wait for the server method to complete and `try...catch` syntax to handle errors.

The `SendAsync` method returns a `Task` that completes when the message is sent to the server. No return value is provided because this `Task` doesn't wait until the server method completes. Any exceptions thrown on the client while sending the message produce a faulted `Task`. Use `await` and `try...catch` syntax to handle message send errors.

Note

Calling hub methods from a client is supported only when using the Azure SignalR Service in *Default* mode. For more information, see [Frequently Asked Questions](/en-us/azure/azure-signalr/signalr-resource-faq).

### Cancel a hub method invocation

Pass a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) to `InvokeAsync` to cancel a long-running hub method. Canceling the token sends a cancellation message to the server, which triggers the `CancellationToken` parameter of the running hub method:

```
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
    var report = await hubConnection.InvokeAsync<Report>(
        "GenerateReport", reportId, cts.Token);
}
catch (OperationCanceledException)
{
    // The invocation was canceled.
}
```

The hub method must declare a `CancellationToken` parameter to observe the cancellation. The server supplies that parameter; don't pass an argument for it from the client:

```
public class ReportHub : Hub
{
    public async Task<Report> GenerateReport(
        int reportId, CancellationToken cancellationToken)
    {
        // Pass cancellationToken to the work being performed.
    }
}
```

Note the following about cancellation:

* Cancellation is cooperative. The server requests cancellation, but the hub method decides whether and how quickly to stop. The server sends a completion message for the invocation either way.
* If the hub method allows an [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception) to propagate, the invocation completes with an error rather than as a canceled invocation.
* The `CancellationToken` supplied to the hub method is also canceled when the connection closes, so it isn't only triggered by an explicit client cancellation.
* The caller's `Task` is canceled on the client as soon as the cancellation message is sent. It doesn't wait for the hub method to stop.
* Cancellation applies to `InvokeAsync`, which tracks an invocation ID. `SendAsync` doesn't wait for the hub method, so there's nothing for it to cancel.

Before ASP.NET Core 11.0, a `CancellationToken` passed to a non-streaming `InvokeAsync` only canceled the wait on the client. The hub method ran to completion on the server. Only streaming invocations could be canceled. This is also the behavior against a server running an earlier version of ASP.NET Core.

Canceling a non-streaming invocation is supported by the .NET client only. The JavaScript, Java, and Swift clients can cancel streaming invocations, but their `invoke` equivalents don't accept a cancellation argument. See [ASP.NET Core SignalR clients](client-features?view=aspnetcore-10.0).

## Call client methods from hub

Define the methods that the hub calls by using `connection.On` after building, but before starting the connection:

```
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
    this.Dispatcher.Invoke(() =>
    {
       var newMessage = $"{user}: {message}";
       messagesList.Items.Add(newMessage);
    });
});
```

The preceding code in `connection.On` runs when server-side code calls it by using the `SendAsync` method:

```
public async Task SendMessage(string user, string message)
{
    await Clients.All.SendAsync("ReceiveMessage", user, message);
}
```

Note

While the hub side of the connection supports strongly typed messaging, the client must register by using the generic method [HubConnection.On](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.on) with the method name. For an example, see [Host ASP.NET Core SignalR in background services](background-services?view=aspnetcore-10.0#call-a-signalr-hub-from-a-background-service).

## Error handling and logging

Handle errors with a `try...catch` statement. Inspect the `Exception` object to determine the proper action to take after an error occurs:

```
try
{
    await connection.InvokeAsync("SendMessage", 
        userTextBox.Text, messageTextBox.Text);
}
catch (Exception ex)
{                
    messagesList.Items.Add(ex.Message);                
}
```

## Refresh authentication without reconnecting

When the server enables authentication refresh, the .NET client can replace the credentials on an open connection instead of letting the connection close when its access token expires. Automatic refresh is enabled by default, and [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection) exposes a `RefreshAuthenticationAsync` method that requests an immediate refresh. To change the refresh timing or turn automatic refresh off, call `WithAuthenticationRefresh` when building the connection. For the client options, the refresh events, and the server options that enable the feature, see [SignalR authentication and authorization](authn-and-authz?view=aspnetcore-10.0#authentication-refresh).

## Related content

* [Hubs](hubs?view=aspnetcore-10.0)
* [JavaScript client](javascript-client?view=aspnetcore-10.0)
* [Publish to Azure](publish-to-azure-web-app?view=aspnetcore-10.0)
* [Azure Functions development and configuration with Azure SignalR Service](/en-us/azure/azure-signalr/signalr-concept-serverless-development-config)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-19

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/account-confirmation-and-password-recovery
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Account confirmation and password recovery in ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure an ASP.NET Core Blazor Web App with email confirmation and password recovery.

Note

This article only applies to Blazor Web Apps. To implement email confirmation and password recovery for standalone Blazor WebAssembly apps with ASP.NET Core Identity, see [Account confirmation and password recovery in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](webassembly/standalone-with-identity/account-confirmation-and-password-recovery?view=aspnetcore-10.0).

## Namespace

The app's namespace used by the example in this article is `BlazorSample`. Update the code examples to use the namespace of your app.

## Select and configure an email provider

In this article, [Mailchimp's Transactional API](https://mailchimp.com/developer/transactional/api/) is used via [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) to send email. We recommend using an email service to send email rather than SMTP. SMTP is difficult to configure and secure properly. Whichever email service you use, access their guidance for .NET apps, create an account, configure an API key for their service, and install any NuGet packages required.

Create a class to hold the secret email provider API key. The example in this article uses a class named `AuthMessageSenderOptions` with an `EmailAuthKey` property to hold the key.

`AuthMessageSenderOptions.cs`:

```
namespace BlazorSample;

public class AuthMessageSenderOptions
{
    public string? EmailAuthKey { get; set; }
}
```

Register the `AuthMessageSenderOptions` configuration instance in the `Program` file:

```
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);
```

## Configure a secret for the email provider's security key

Receive the email provider's security key from the provider and use it in the following guidance.

Use either or both of the following approaches to supply the secret to the app:

* [Secret Manager tool](#secret-manager-tool): The Secret Manager tool stores private data on the local machine and is only used during local development.
* [Azure Key Vault](#azure-key-vault): You can store the secret in a key vault for use in any environment, including for the `Development` environment when working locally. Some developers prefer to use key vaults for staging and production deployments and use the [Secret Manager tool](#secret-manager-tool) for local development.

We strongly recommend that you avoid storing secrets in project code or configuration files. Use secure authentication flows, such as either or both of the approaches in this section.

### Secret Manager tool

If the project has already been initialized for the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0), it will already have a user secrets identifier (`<UserSecretsId>`) in its project file (`.csproj`). In Visual Studio, you can tell if the user secrets ID is present by looking at the **Properties** panel when the project is selected in **Solution Explorer**. If the app hasn't been initialized, execute the following command in a command shell opened to the project's directory. In Visual Studio, you can use the Developer PowerShell command prompt.

```
dotnet user-secrets init
```

Set the API key with the Secret Manager tool. In the following example, the key name is `EmailAuthKey` to match `AuthMessageSenderOptions.EmailAuthKey`, and the key is represented by the `{KEY}` placeholder. Execute the following command with the API key:

```
dotnet user-secrets set "EmailAuthKey" "{KEY}"
```

If using Visual Studio, you can confirm the secret is set by right-clicking the server project in **Solution Explorer** and selecting **Manage User Secrets**.

For more information, see [Safe storage of app secrets in development](../../security/app-secrets?view=aspnetcore-10.0).

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](./?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

### Azure Key Vault

[Azure Key Vault](https://azure.microsoft.com/products/key-vault/) provides a safe approach for providing the app's client secret to the app.

To create a key vault and set a secret, see [About Azure Key Vault secrets (Azure documentation)](/en-us/azure/key-vault/secrets/about-secrets), which cross-links resources to get started with Azure Key Vault. For the example in this section, the secret name is "`EmailAuthKey`."

When establishing the key vault in the Entra or Azure portal:

* Configure the key vault to use Azure role-based access control (RABC). If you aren't operating on an [Azure Virtual Network](/en-us/azure/virtual-network/virtual-networks-overview), including for local development and testing, confirm that public access on the **Networking** step is **enabled** (checked). Enabling public access only exposes the key vault endpoint. Authenticated accounts are still required for access.

* Create an Azure Managed Identity (or add a role to the existing Managed Identity that you plan to use) with the **Key Vault Secrets User** role. Assign the Managed Identity to the Azure App Service that's hosting the deployment: **Settings** > **Identity** > **User assigned** > **Add**.

  Note

  If you also plan to run an app locally with an authorized user for key vault access using the [Azure CLI](/en-us/cli/azure/) or Visual Studio's Azure Service Authentication, add your developer Azure user account in **Access Control (IAM)** with the **Key Vault Secrets User** role. If you want to use the Azure CLI through Visual Studio, execute the `az login` command from the Developer PowerShell panel and follow the prompts to authenticate with the tenant.

To implement the code in this section, record the key vault URI (example: "`https://contoso.vault.azure.net/`", trailing slash required) and the secret name (example: "`EmailAuthKey`") from Azure when you create the key vault and secret.

Important

A key vault secret is created with an expiration date. Be sure to track when a key vault secret is going to expire and create a new secret for the app prior to that date passing.

Add the following `AzureHelper` class to the server project. The `GetKeyVaultSecret` method retrieves a secret from a key vault. Adjust the namespace (`BlazorSample.Helpers`) to match your project namespace scheme.

`Helpers/AzureHelper.cs`:

```
using Azure.Core;
using Azure.Security.KeyVault.Secrets;

namespace BlazorSample.Helpers;

public static class AzureHelper
{
    public static string GetKeyVaultSecret(string vaultUri, 
        TokenCredential credential, string secretName)
    {
        var client = new SecretClient(new Uri(vaultUri), credential);
        var secret = client.GetSecretAsync(secretName).Result;

        return secret.Value.Value;
    }
}
```

Where services are registered in the server project's `Program` file, obtain and bind the secret with [Options configuration](../../fundamentals/configuration/options?view=aspnetcore-10.0):

```
TokenCredential? credential;

if (builder.Environment.IsProduction())
{
    credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
    // Local development and testing only
    DefaultAzureCredentialOptions options = new()
    {
        // Specify the tenant ID to use the dev credentials when running the app locally
        // in Visual Studio.
        VisualStudioTenantId = "{TENANT ID}",
        SharedTokenCacheTenantId = "{TENANT ID}"
    };

    credential = new DefaultAzureCredential(options);
}

var emailAuthKey = AzureHelper.GetKeyVaultSecret("{VAULT URI}", credential, 
    "EmailAuthKey");

var authMessageSenderOptions = 
    new AuthMessageSenderOptions() { EmailAuthKey = emailAuthKey };
builder.Configuration.GetSection(authMessageSenderOptions.EmailAuthKey)
    .Bind(authMessageSenderOptions);
```

Note

In non-`Production` environments, the preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

The preceding example implies that the Managed Identity Client ID (`{MANAGED IDENTITY CLIENT ID}`), directory (tenant) ID (`{TENANT ID}`), and key vault URI (`{VAULT URI}`, example: `https://contoso.vault.azure.net/`, trailing slash required) are supplied by hard-coded values. Any or all of these values can be supplied from app settings configuration. For example, the following obtains the vault URI from the `AzureAd` node of an app settings file, and `vaultUri` can be used in the call to `GetKeyVaultSecret` in the preceding example:

```
var vaultUri = builder.Configuration.GetValue<string>("AzureAd:VaultUri")!;
```

For more information, see [ASP.NET Core Blazor configuration](../fundamentals/configuration?view=aspnetcore-10.0).

## Implement `IEmailSender`

The following example is based on Mailchimp's Transactional API using [Mandrill.net](https://www.nuget.org/packages/Mandrill.net). For a different provider, refer to their documentation on how to implement sending an email message.

Add the [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) NuGet package to the project.

Add the following `EmailSender` class to implement [IEmailSender\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.iemailsender-1). In the following example, `ApplicationUser` is an [IdentityUser](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser). The message HTML markup can be further customized. As long as the `message` passed to `MandrillMessage` starts with the `<` character, the Mandrill.net API assumes that the message body is composed in HTML.

`Components/Account/EmailSender.cs`:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Mandrill;
using Mandrill.Model;
using BlazorSample.Data;

namespace BlazorSample.Components.Account;

public class EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
    ILogger<EmailSender> logger) : IEmailSender<ApplicationUser>
{
    private readonly ILogger logger = logger;

    public AuthMessageSenderOptions Options { get; } = optionsAccessor.Value;

    public Task SendConfirmationLinkAsync(ApplicationUser user, string email,
        string confirmationLink) => SendEmailAsync(email, "Confirm your email",
        "<html lang=\"en\"><head></head><body>Please confirm your account by " +
        $"<a href='{confirmationLink}'>clicking here</a>.</body></html>");

    public Task SendPasswordResetLinkAsync(ApplicationUser user, string email,
        string resetLink) => SendEmailAsync(email, "Reset your password",
        "<html lang=\"en\"><head></head><body>Please reset your password by " +
        $"<a href='{resetLink}'>clicking here</a>.</body></html>");

    public Task SendPasswordResetCodeAsync(ApplicationUser user, string email,
        string resetCode) => SendEmailAsync(email, "Reset your password",
        "<html lang=\"en\"><head></head><body>Please reset your password " +
        $"using the following code:<br>{resetCode}</body></html>");

    public async Task SendEmailAsync(string toEmail, string subject, string message)
    {
        if (string.IsNullOrEmpty(Options.EmailAuthKey))
        {
            throw new Exception("Null EmailAuthKey");
        }

        await Execute(Options.EmailAuthKey, subject, message, toEmail);
    }

    public async Task Execute(string apiKey, string subject, string message, 
        string toEmail)
    {
        var api = new MandrillApi(apiKey);
        var mandrillMessage = new MandrillMessage("sarah@contoso.com", toEmail, 
            subject, message);
        await api.Messages.SendAsync(mandrillMessage);

        logger.LogInformation("Email to {EmailAddress} sent!", toEmail);
    }
}
```

Note

Body content for messages might require special encoding for the email service provider. If links in the message body can't be followed in the email message, consult the service provider's documentation to troubleshoot the problem.

## Configure app to support email

In the `Program` file, change the email sender implementation to the `EmailSender`:

```
- builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
+ builder.Services.AddSingleton<IEmailSender<ApplicationUser>, EmailSender>();
```

Remove the `IdentityNoOpEmailSender` (`Components/Account/IdentityNoOpEmailSender.cs`) from the app.

In the `RegisterConfirmation` component (`Components/Account/Pages/RegisterConfirmation.razor`), remove the conditional block in the `@code` block that checks if the `EmailSender` is an `IdentityNoOpEmailSender`:

```
- else if (EmailSender is IdentityNoOpEmailSender)
- {
-     ...
- }
```

Also in the `RegisterConfirmation` component, remove the Razor markup and code for checking the `emailConfirmationLink` field, leaving just the line instructing the user to check their email ...

```
- @if (emailConfirmationLink is not null)
- {
-     ...
- }
- else
- {
     <p>Please check your email to confirm your account.</p>
- }

@code {
-    private string? emailConfirmationLink;

     ...
}
```

## Enable account confirmation after a site has users

Enabling account confirmation on a site with users locks out all the existing users. Existing users are locked out because their accounts aren't confirmed. To work around existing user lockout, use one of the following approaches:

* Update the database to mark all existing users as confirmed.
* Confirm existing users. For example, batch-send emails with confirmation links.

## Email and activity timeout

The default inactivity timeout is 14 days. The following code sets the inactivity timeout to five days with sliding expiration:

```
builder.Services.ConfigureApplicationCookie(options => {
    options.ExpireTimeSpan = TimeSpan.FromDays(5);
    options.SlidingExpiration = true;
});
```

## Change all ASP.NET Core Data Protection token lifespans

The following code changes Data Protection tokens' timeout period to three hours:

```
builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
    options.TokenLifespan = TimeSpan.FromHours(3));
```

To change the email token lifespan, add a custom [DataProtectorTokenProvider\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.dataprotectortokenprovider-1) and [DataProtectionTokenProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.dataprotectiontokenprovideroptions).

`CustomTokenProvider.cs`:

```
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

namespace BlazorSample;

public class CustomEmailConfirmationTokenProvider<TUser>
    : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomEmailConfirmationTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<EmailConfirmationTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
        : base(dataProtectionProvider, options, logger)
    {
    }
}

public class EmailConfirmationTokenProviderOptions 
    : DataProtectionTokenProviderOptions
{
    public EmailConfirmationTokenProviderOptions()
    {
        Name = "EmailDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(4);
    }
}

public class CustomPasswordResetTokenProvider<TUser> 
    : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomPasswordResetTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<PasswordResetTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
        : base(dataProtectionProvider, options, logger)
    {
    }
}

public class PasswordResetTokenProviderOptions : 
    DataProtectionTokenProviderOptions
{
    public PasswordResetTokenProviderOptions()
    {
        Name = "PasswordResetDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(3);
    }
}
```

Configure the services to use the custom token provider in the `Program` file:

```
builder.Services.AddIdentityCore<ApplicationUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<ApplicationUser>)));
        options.Tokens.EmailConfirmationTokenProvider = 
            "CustomEmailConfirmation";
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

builder.Services
    .AddTransient<CustomEmailConfirmationTokenProvider<ApplicationUser>>();
```

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-16

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/sdk
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/sdk.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Razor SDK

By [Rick Anderson](https://twitter.com/RickAndMSFT)

## Overview

The [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) includes the `Microsoft.NET.Sdk.Razor` MSBuild SDK (Razor SDK). The Razor SDK:

* Is required to build, package, and publish projects containing [Razor](../mvc/views/razor?view=aspnetcore-10.0) files for ASP.NET Core MVC-based or [Blazor](../blazor/?view=aspnetcore-10.0) projects.
* Includes a set of predefined properties, and items that allow customizing the compilation of Razor (`.cshtml` or `.razor`) files.

The Razor SDK includes `Content` items with `Include` attributes set to the `**\*.cshtml` and `**\*.razor` globbing patterns. Matching files are published.

## Prerequisites

[.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

## Use the Razor SDK

Most web apps aren't required to explicitly reference the Razor SDK.

To use the Razor SDK to build class libraries containing Razor views or Razor Pages, we recommend starting with the Razor class library (RCL) project template. An RCL that's used to build Blazor (`.razor`) files minimally requires a reference to the [Microsoft.AspNetCore.Components](https://www.nuget.org/packages/Microsoft.AspNetCore.Components) package. An RCL that's used to build Razor views or pages (`.cshtml` files) minimally requires targeting `netcoreapp3.0` or later and has a `FrameworkReference` to the [Microsoft.AspNetCore.App metapackage](../fundamentals/metapackage-app?view=aspnetcore-10.0) in its project file.

### Properties

The following properties control the Razor's SDK behavior as part of a project build:

* `RazorCompileOnBuild`: When `true`, compiles and emits the Razor assembly as part of building the project. Defaults to `true`.

* `RazorCompileOnPublish`: When `true`, compiles and emits the Razor assembly as part of publishing the project. Defaults to `true`.

* `UseRazorSourceGenerator`: Defaults to `true`. When `true`:

  * Compiles using source generation.
  * Doesn't create `<app_name>.Views.dll`. Views are included in `<app_name>.dll`.
  * Supports [.NET Hot Reload](../test/hot-reload?view=aspnetcore-10.0).

The properties and items in the following table are used to configure inputs and output to the Razor SDK.

| Items                   | Description                                                                                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `RazorGenerate`         | Item elements (`.cshtml` files) that are inputs to code generation.                                                                                                |
| `RazorComponent`        | Item elements (`.razor` files) that are inputs to Razor component code generation.                                                                                 |
| `RazorCompile`          | Item elements (`.cs` files) that are inputs to Razor compilation targets. Use this `ItemGroup` to specify additional files to be compiled into the Razor assembly. |
| `RazorEmbeddedResource` | Item elements added as embedded resources to the generated Razor assembly.                                                                                         |

| Property                                                                                                                                          | Description                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `RazorOutputPath`                                                                                                                                 | The Razor output directory.                                                                                                                                                                                                                                                                                                                                                                            |
| `RazorCompileToolset`                                                                                                                             | Used to determine the toolset used to build the Razor assembly. Valid values are `Implicit`, `RazorSDK`, and `PrecompilationTool`.                                                                                                                                                                                                                                                                     |
| [EnableDefaultContentItems](https://github.com/dotnet/sdk/blob/main/src/WebSdk/ProjectSystem/Targets/Microsoft.NET.Sdk.Web.ProjectSystem.targets) | Default is `true`. When `true`, includes *web.config*, `.json`, and `.cshtml` files as content in the project. When referenced via `Microsoft.NET.Sdk.Web`, files under *wwwroot* and config files are also included.                                                                                                                                                                                  |
| `EnableDefaultRazorGenerateItems`                                                                                                                 | When `true`, includes `.cshtml` files from `Content` items in `RazorGenerate` items.                                                                                                                                                                                                                                                                                                                   |
| `GenerateRazorTargetAssemblyInfo`                                                                                                                 | Not used in .NET 6 or later.                                                                                                                                                                                                                                                                                                                                                                           |
| `EnableDefaultRazorTargetAssemblyInfoAttributes`                                                                                                  | Not used in .NET 6 or later.                                                                                                                                                                                                                                                                                                                                                                           |
| `CopyRazorGenerateFilesToPublishDirectory`                                                                                                        | When `true`, copies `RazorGenerate` items (`.cshtml`) files to the publish directory. Typically, Razor files aren't required for a published app if they participate in compilation at build-time or publish-time. Defaults to `false`.                                                                                                                                                                |
| `PreserveCompilationReferences`                                                                                                                   | When `true`, copy reference assembly items to the publish directory. Typically, reference assemblies aren't required for a published app if Razor compilation occurs at build-time or publish-time. Set to `true` if your published app requires runtime compilation. For example, set the value to `true` if the app modifies `.cshtml` files at runtime or uses embedded views. Defaults to `false`. |
| `IncludeRazorContentInPack`                                                                                                                       | When `true`, all Razor content items (`.cshtml` files) are marked for inclusion in the generated NuGet package. Defaults to `false`.                                                                                                                                                                                                                                                                   |
| `EmbedRazorGenerateSources`                                                                                                                       | When `true`, adds RazorGenerate (`.cshtml`) items as embedded files to the generated Razor assembly. Defaults to `false`.                                                                                                                                                                                                                                                                              |
| `GenerateMvcApplicationPartsAssemblyAttributes`                                                                                                   | Not used in .NET 6 or later.                                                                                                                                                                                                                                                                                                                                                                           |
| `DefaultWebContentItemExcludes`                                                                                                                   | A globbing pattern for item elements that are to be excluded from the `Content` item group in projects targeting the Web or Razor SDK                                                                                                                                                                                                                                                                  |
| `ExcludeConfigFilesFromBuildOutput`                                                                                                               | When `true`, *.config* and `.json` files do not get copied to the build output directory.                                                                                                                                                                                                                                                                                                              |
| `AddRazorSupportForMvc`                                                                                                                           | When `true`, configures the Razor SDK to add support for the MVC configuration that is required when building applications containing MVC views or Razor Pages. This property is implicitly set for .NET Core 3.0 or later projects targeting the Web SDK                                                                                                                                              |
| `RazorLangVersion`                                                                                                                                | The version of the Razor Language to target.                                                                                                                                                                                                                                                                                                                                                           |
| `EmitCompilerGeneratedFiles`                                                                                                                      | When set to `true`, the generated source files are written to disk. Setting to `true` is useful when debugging the compiler. The default is `false`.                                                                                                                                                                                                                                                   |

For more information on properties, see [MSBuild properties](/en-us/visualstudio/msbuild/msbuild-properties).

### Runtime compilation of Razor views

* By default, the Razor SDK doesn't publish reference assemblies that are required to perform runtime compilation. This results in compilation failures when the application model relies on runtime compilation—for example, the app uses embedded views or changes views after the app is published. Set `CopyRefAssembliesToPublishDirectory` to `true` to continue publishing reference assemblies. Both code generation and compilation are supported by a single call to the compiler. A single assembly is produced that contains the app types and the generated views.

* For a web app, ensure your app is targeting the `Microsoft.NET.Sdk.Web` SDK.

## Razor language version

When targeting the `Microsoft.NET.Sdk.Web` SDK, the Razor language version is inferred from the app's target framework version. For projects targeting the `Microsoft.NET.Sdk.Razor` SDK or in the rare case that the app requires a different Razor language version than the inferred value, a version can be configured by setting the `<RazorLangVersion>` property in the app's project file:

```
<PropertyGroup>
  <RazorLangVersion>{VERSION}</RazorLangVersion>
</PropertyGroup>
```

Razor's language version is tightly integrated with the version of the runtime that it was built for. Targeting a language version that isn't designed for the runtime is unsupported and likely produces build errors.

## Additional resources

* [MSBuild reference for .NET SDK projects](/en-us/dotnet/core/tools/csproj)
* [Common MSBuild project items](/en-us/visualstudio/msbuild/common-msbuild-project-items)

The [.NET Core 2.1 or later SDK](https://dotnet.microsoft.com/download/dotnet-core) includes the `Microsoft.NET.Sdk.Razor` MSBuild SDK (Razor SDK). The Razor SDK:

* Is required to build, package, and publish projects containing [Razor](../mvc/views/razor?view=aspnetcore-10.0) files for ASP.NET Core MVC-based or [Blazor](../blazor/?view=aspnetcore-10.0) projects.
* Includes a set of predefined targets, properties, and items that allow customizing the compilation of Razor (`.cshtml` or `.razor`) files.

The Razor SDK includes `Content` items with `Include` attributes set to the `**\*.cshtml` and `**\*.razor` globbing patterns. Matching files are published.

## Prerequisites

[.NET Core 2.1 or later SDK](https://dotnet.microsoft.com/download/dotnet-core)

## Use the Razor SDK

Most web apps aren't required to explicitly reference the Razor SDK.

To use the Razor SDK to build class libraries containing Razor views or Razor Pages, we recommend starting with the Razor class library (RCL) project template. An RCL that's used to build Blazor (`.razor`) files minimally requires a reference to the [Microsoft.AspNetCore.Components](https://www.nuget.org/packages/Microsoft.AspNetCore.Components) package. An RCL that's used to build Razor views or pages (`.cshtml` files) minimally requires targeting `netcoreapp3.0` or later and has a `FrameworkReference` to the [Microsoft.AspNetCore.App metapackage](../fundamentals/metapackage-app?view=aspnetcore-10.0) in its project file.

### Properties

The following properties control the Razor's SDK behavior as part of a project build:

* `RazorCompileOnBuild`: When `true`, compiles and emits the Razor assembly as part of building the project. Defaults to `true`.
* `RazorCompileOnPublish`: When `true`, compiles and emits the Razor assembly as part of publishing the project. Defaults to `true`.

The properties and items in the following table are used to configure inputs and output to the Razor SDK.

Warning

Starting with ASP.NET Core 3.0, MVC Views or Razor Pages aren't served by default if the `RazorCompileOnBuild` or `RazorCompileOnPublish` MSBuild properties in the project file are disabled. Applications must add an explicit reference to the [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation) package if the app relies on runtime compilation to process `.cshtml` files.

| Items                          | Description                                                                                                                                                                                                                                  |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RazorGenerate`                | Item elements (`.cshtml` files) that are inputs to code generation.                                                                                                                                                                          |
| `RazorComponent`               | Item elements (`.razor` files) that are inputs to Razor component code generation.                                                                                                                                                           |
| `RazorCompile`                 | Item elements (`.cs` files) that are inputs to Razor compilation targets. Use this `ItemGroup` to specify additional files to be compiled into the Razor assembly.                                                                           |
| `RazorTargetAssemblyAttribute` | Item elements used to code generate attributes for the Razor assembly. For example: `RazorAssemblyAttribute` `Include="System.Reflection.AssemblyMetadataAttribute"` `_Parameter1="BuildSource" _Parameter2="https://learn.microsoft.com/">` |
| `RazorEmbeddedResource`        | Item elements added as embedded resources to the generated Razor assembly.                                                                                                                                                                   |

| Property                                                                                                                                                                                                    | Description                                                                                                                                                                                                                                                                                                                                                                                            |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `RazorTargetName`                                                                                                                                                                                           | File name (without extension) of the assembly produced by Razor.                                                                                                                                                                                                                                                                                                                                       |
| `RazorOutputPath`                                                                                                                                                                                           | The Razor output directory.                                                                                                                                                                                                                                                                                                                                                                            |
| `RazorCompileToolset`                                                                                                                                                                                       | Used to determine the toolset used to build the Razor assembly. Valid values are `Implicit`, `RazorSDK`, and `PrecompilationTool`.                                                                                                                                                                                                                                                                     |
| [EnableDefaultContentItems](https://github.com/dotnet/websdk/blob/release/2.1/src/ProjectSystem/Microsoft.NET.Sdk.Web.ProjectSystem.Targets/netstandard1.0/Microsoft.NET.Sdk.Web.ProjectSystem.targets#L21) | Default is `true`. When `true`, includes *web.config*, `.json`, and `.cshtml` files as content in the project. When referenced via `Microsoft.NET.Sdk.Web`, files under *wwwroot* and config files are also included.                                                                                                                                                                                  |
| `EnableDefaultRazorGenerateItems`                                                                                                                                                                           | When `true`, includes `.cshtml` files from `Content` items in `RazorGenerate` items.                                                                                                                                                                                                                                                                                                                   |
| `GenerateRazorTargetAssemblyInfo`                                                                                                                                                                           | When `true`, generates a `.cs` file containing attributes specified by `RazorAssemblyAttribute` and includes the file in the compile output.                                                                                                                                                                                                                                                           |
| `EnableDefaultRazorTargetAssemblyInfoAttributes`                                                                                                                                                            | When `true`, adds a default set of assembly attributes to `RazorAssemblyAttribute`.                                                                                                                                                                                                                                                                                                                    |
| `CopyRazorGenerateFilesToPublishDirectory`                                                                                                                                                                  | When `true`, copies `RazorGenerate` items (`.cshtml`) files to the publish directory. Typically, Razor files aren't required for a published app if they participate in compilation at build-time or publish-time. Defaults to `false`.                                                                                                                                                                |
| `PreserveCompilationReferences`                                                                                                                                                                             | When `true`, copy reference assembly items to the publish directory. Typically, reference assemblies aren't required for a published app if Razor compilation occurs at build-time or publish-time. Set to `true` if your published app requires runtime compilation. For example, set the value to `true` if the app modifies `.cshtml` files at runtime or uses embedded views. Defaults to `false`. |
| `IncludeRazorContentInPack`                                                                                                                                                                                 | When `true`, all Razor content items (`.cshtml` files) are marked for inclusion in the generated NuGet package. Defaults to `false`.                                                                                                                                                                                                                                                                   |
| `EmbedRazorGenerateSources`                                                                                                                                                                                 | When `true`, adds RazorGenerate (`.cshtml`) items as embedded files to the generated Razor assembly. Defaults to `false`.                                                                                                                                                                                                                                                                              |
| `UseRazorBuildServer`                                                                                                                                                                                       | When `true`, uses a persistent build server process to offload code generation work. Defaults to the value of `UseSharedCompilation`.                                                                                                                                                                                                                                                                  |
| `GenerateMvcApplicationPartsAssemblyAttributes`                                                                                                                                                             | When `true`, the SDK generates additional attributes used by MVC at runtime to perform application part discovery.                                                                                                                                                                                                                                                                                     |
| `DefaultWebContentItemExcludes`                                                                                                                                                                             | A globbing pattern for item elements that are to be excluded from the `Content` item group in projects targeting the Web or Razor SDK                                                                                                                                                                                                                                                                  |
| `ExcludeConfigFilesFromBuildOutput`                                                                                                                                                                         | When `true`, *.config* and `.json` files do not get copied to the build output directory.                                                                                                                                                                                                                                                                                                              |
| `AddRazorSupportForMvc`                                                                                                                                                                                     | When `true`, configures the Razor SDK to add support for the MVC configuration that is required when building applications containing MVC views or Razor Pages. This property is implicitly set for .NET Core 3.0 or later projects targeting the Web SDK                                                                                                                                              |
| `RazorLangVersion`                                                                                                                                                                                          | The version of the Razor Language to target.                                                                                                                                                                                                                                                                                                                                                           |

For more information on properties, see [MSBuild properties](/en-us/visualstudio/msbuild/msbuild-properties).

### Targets

The Razor SDK defines two primary targets:

* `RazorGenerate`: Code generates `.cs` files from `RazorGenerate` item elements. Use the `RazorGenerateDependsOn` property to specify additional targets that can run before or after this target.
* `RazorCompile`: Compiles generated `.cs` files in to a Razor assembly. Use the `RazorCompileDependsOn` to specify additional targets that can run before or after this target.
* `RazorComponentGenerate`: Code generates `.cs` files for `RazorComponent` item elements. Use the `RazorComponentGenerateDependsOn` property to specify additional targets that can run before or after this target.

### Runtime compilation of Razor views

* By default, the Razor SDK doesn't publish reference assemblies that are required to perform runtime compilation. This results in compilation failures when the application model relies on runtime compilation—for example, the app uses embedded views or changes views after the app is published. Set `CopyRefAssembliesToPublishDirectory` to `true` to continue publishing reference assemblies.

* For a web app, ensure your app is targeting the `Microsoft.NET.Sdk.Web` SDK.

## Razor language version

When targeting the `Microsoft.NET.Sdk.Web` SDK, the Razor language version is inferred from the app's target framework version. For projects targeting the `Microsoft.NET.Sdk.Razor` SDK or in the rare case that the app requires a different Razor language version than the inferred value, a version can be configured by setting the `<RazorLangVersion>` property in the app's project file:

```
<PropertyGroup>
  <RazorLangVersion>{VERSION}</RazorLangVersion>
</PropertyGroup>
```

Razor's language version is tightly integrated with the version of the runtime that it was built for. Targeting a language version that isn't designed for the runtime is unsupported and likely produces build errors.

## Additional resources

* [MSBuild reference for .NET SDK projects](/en-us/dotnet/core/tools/csproj)
* [Common MSBuild project items](/en-us/visualstudio/msbuild/common-msbuild-project-items)

- Standardizes the experience around building, packaging, and publishing projects containing [Razor](../mvc/views/razor?view=aspnetcore-10.0) files for ASP.NET Core MVC-based projects.
- Includes a set of predefined targets, properties, and items that allow customizing the compilation of Razor files.

The Razor SDK includes a `Content` item with an `Include` attribute set to the `**\*.cshtml` globbing pattern. Matching files are published.

## Prerequisites

[.NET Core 2.1 or later SDK](https://dotnet.microsoft.com/download/dotnet-core)

## Use the Razor SDK

Most web apps aren't required to explicitly reference the Razor SDK.

To use the Razor SDK to build class libraries containing Razor views or Razor Pages:

* Use `Microsoft.NET.Sdk.Razor` instead of `Microsoft.NET.Sdk`:

  ```
  <Project SDK="Microsoft.NET.Sdk.Razor">
    <!-- omitted for brevity -->
  </Project>
  ```

* Typically, a package reference to `Microsoft.AspNetCore.Mvc` is required to receive additional dependencies that are required to build and compile Razor Pages and Razor views. At a minimum, your project should add package references to:

  * `Microsoft.AspNetCore.Razor.Design`
  * `Microsoft.AspNetCore.Mvc.Razor.Extensions`
  * `Microsoft.AspNetCore.Mvc.Razor`

  The `Microsoft.AspNetCore.Razor.Design` package provides the Razor compilation tasks and targets for the project.

  The preceding packages are included in `Microsoft.AspNetCore.Mvc`. The following markup shows a project file that uses the Razor SDK to build Razor files for an ASP.NET Core Razor Pages app:

  ```
  <Project Sdk="Microsoft.NET.Sdk.Razor">

    <PropertyGroup>
      <TargetFramework>netcoreapp2.1</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
    </ItemGroup>

  </Project>
  ```

Warning

The `Microsoft.AspNetCore.Razor.Design` and `Microsoft.AspNetCore.Mvc.Razor.Extensions` packages are included in the [Microsoft.AspNetCore.App metapackage](../fundamentals/metapackage-app?view=aspnetcore-10.0). However, the version-less `Microsoft.AspNetCore.App` package reference provides a metapackage to the app that doesn't include the latest version of `Microsoft.AspNetCore.Razor.Design`. Projects must reference a consistent version of `Microsoft.AspNetCore.Razor.Design` (or `Microsoft.AspNetCore.Mvc`) so that the latest build-time fixes for Razor are included. For more information, see [this GitHub issue](https://github.com/aspnet/Razor/issues/2553).

### Properties

The following properties control the Razor's SDK behavior as part of a project build:

* `RazorCompileOnBuild`: When `true`, compiles and emits the Razor assembly as part of building the project. Defaults to `true`.
* `RazorCompileOnPublish`: When `true`, compiles and emits the Razor assembly as part of publishing the project. Defaults to `true`.

The properties and items in the following table are used to configure inputs and output to the Razor SDK.

| Items                          | Description                                                                                                                                                                                                                                  |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RazorGenerate`                | Item elements (`.cshtml` files) that are inputs to code generation.                                                                                                                                                                          |
| `RazorComponent`               | Item elements (`.razor` files) that are inputs to Razor component code generation.                                                                                                                                                           |
| `RazorCompile`                 | Item elements (`.cs` files) that are inputs to Razor compilation targets. Use this `ItemGroup` to specify additional files to be compiled into the Razor assembly.                                                                           |
| `RazorTargetAssemblyAttribute` | Item elements used to code generate attributes for the Razor assembly. For example: `RazorAssemblyAttribute` `Include="System.Reflection.AssemblyMetadataAttribute"` `_Parameter1="BuildSource" _Parameter2="https://learn.microsoft.com/">` |
| `RazorEmbeddedResource`        | Item elements added as embedded resources to the generated Razor assembly.                                                                                                                                                                   |

| Property                                                                                                                                                                                                    | Description                                                                                                                                                                                                                                                                                                                                                                                            |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `RazorTargetName`                                                                                                                                                                                           | File name (without extension) of the assembly produced by Razor.                                                                                                                                                                                                                                                                                                                                       |
| `RazorOutputPath`                                                                                                                                                                                           | The Razor output directory.                                                                                                                                                                                                                                                                                                                                                                            |
| `RazorCompileToolset`                                                                                                                                                                                       | Used to determine the toolset used to build the Razor assembly. Valid values are `Implicit`, `RazorSDK`, and `PrecompilationTool`.                                                                                                                                                                                                                                                                     |
| [EnableDefaultContentItems](https://github.com/dotnet/websdk/blob/release/2.1/src/ProjectSystem/Microsoft.NET.Sdk.Web.ProjectSystem.Targets/netstandard1.0/Microsoft.NET.Sdk.Web.ProjectSystem.targets#L21) | Default is `true`. When `true`, includes *web.config*, `.json`, and `.cshtml` files as content in the project. When referenced via `Microsoft.NET.Sdk.Web`, files under *wwwroot* and config files are also included.                                                                                                                                                                                  |
| `EnableDefaultRazorGenerateItems`                                                                                                                                                                           | When `true`, includes `.cshtml` files from `Content` items in `RazorGenerate` items.                                                                                                                                                                                                                                                                                                                   |
| `GenerateRazorTargetAssemblyInfo`                                                                                                                                                                           | When `true`, generates a `.cs` file containing attributes specified by `RazorAssemblyAttribute` and includes the file in the compile output.                                                                                                                                                                                                                                                           |
| `EnableDefaultRazorTargetAssemblyInfoAttributes`                                                                                                                                                            | When `true`, adds a default set of assembly attributes to `RazorAssemblyAttribute`.                                                                                                                                                                                                                                                                                                                    |
| `CopyRazorGenerateFilesToPublishDirectory`                                                                                                                                                                  | When `true`, copies `RazorGenerate` items (`.cshtml`) files to the publish directory. Typically, Razor files aren't required for a published app if they participate in compilation at build-time or publish-time. Defaults to `false`.                                                                                                                                                                |
| `CopyRefAssembliesToPublishDirectory`                                                                                                                                                                       | When `true`, copy reference assembly items to the publish directory. Typically, reference assemblies aren't required for a published app if Razor compilation occurs at build-time or publish-time. Set to `true` if your published app requires runtime compilation. For example, set the value to `true` if the app modifies `.cshtml` files at runtime or uses embedded views. Defaults to `false`. |
| `IncludeRazorContentInPack`                                                                                                                                                                                 | When `true`, all Razor content items (`.cshtml` files) are marked for inclusion in the generated NuGet package. Defaults to `false`.                                                                                                                                                                                                                                                                   |
| `EmbedRazorGenerateSources`                                                                                                                                                                                 | When `true`, adds RazorGenerate (`.cshtml`) items as embedded files to the generated Razor assembly. Defaults to `false`.                                                                                                                                                                                                                                                                              |
| `UseRazorBuildServer`                                                                                                                                                                                       | When `true`, uses a persistent build server process to offload code generation work. Defaults to the value of `UseSharedCompilation`.                                                                                                                                                                                                                                                                  |
| `GenerateMvcApplicationPartsAssemblyAttributes`                                                                                                                                                             | When `true`, the SDK generates additional attributes used by MVC at runtime to perform application part discovery.                                                                                                                                                                                                                                                                                     |
| `DefaultWebContentItemExcludes`                                                                                                                                                                             | A globbing pattern for item elements that are to be excluded from the `Content` item group in projects targeting the Web or Razor SDK                                                                                                                                                                                                                                                                  |
| `ExcludeConfigFilesFromBuildOutput`                                                                                                                                                                         | When `true`, *.config* and `.json` files do not get copied to the build output directory.                                                                                                                                                                                                                                                                                                              |
| `AddRazorSupportForMvc`                                                                                                                                                                                     | When `true`, configures the Razor SDK to add support for the MVC configuration that is required when building applications containing MVC views or Razor Pages. This property is implicitly set for .NET Core 3.0 or later projects targeting the Web SDK                                                                                                                                              |
| `RazorLangVersion`                                                                                                                                                                                          | The version of the Razor Language to target.                                                                                                                                                                                                                                                                                                                                                           |

For more information on properties, see [MSBuild properties](/en-us/visualstudio/msbuild/msbuild-properties).

### Targets

The Razor SDK defines two primary targets:

* `RazorGenerate`: Code generates `.cs` files from `RazorGenerate` item elements. Use the `RazorGenerateDependsOn` property to specify additional targets that can run before or after this target.
* `RazorCompile`: Compiles generated `.cs` files in to a Razor assembly. Use the `RazorCompileDependsOn` to specify additional targets that can run before or after this target.
* `RazorComponentGenerate`: Code generates `.cs` files for `RazorComponent` item elements. Use the `RazorComponentGenerateDependsOn` property to specify additional targets that can run before or after this target.

### Runtime compilation of Razor views

* By default, the Razor SDK doesn't publish reference assemblies that are required to perform runtime compilation. This results in compilation failures when the application model relies on runtime compilation—for example, the app uses embedded views or changes views after the app is published. Set `CopyRefAssembliesToPublishDirectory` to `true` to continue publishing reference assemblies.

* For a web app, ensure your app is targeting the `Microsoft.NET.Sdk.Web` SDK.

## Razor language version

When targeting the `Microsoft.NET.Sdk.Web` SDK, the Razor language version is inferred from the app's target framework version. For projects targeting the `Microsoft.NET.Sdk.Razor` SDK or in the rare case that the app requires a different Razor language version than the inferred value, a version can be configured by setting the `<RazorLangVersion>` property in the app's project file:

```
<PropertyGroup>
  <RazorLangVersion>{VERSION}</RazorLangVersion>
</PropertyGroup>
```

Razor's language version is tightly integrated with the version of the runtime that it was built for. Targeting a language version that isn't designed for the runtime is unsupported and likely produces build errors.

## Additional resources

* [MSBuild reference for .NET SDK projects](/en-us/dotnet/core/tools/csproj)
* [Common MSBuild project items](/en-us/visualstudio/msbuild/common-msbuild-project-items)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/dependency-injection.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor dependency injection

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rainer Stropek](https://www.timecockpit.com) and [Mike Rousos](https://github.com/mjrousos)

This article explains how Blazor apps can inject services into components.

[Dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0) is a technique for accessing services configured in a central location:

* Framework-registered services can be injected directly into Razor components.
* Blazor apps define and register custom services and make them available throughout the app via DI.

Note

We recommend reading [Dependency injection in ASP.NET Core](../../fundamentals/dependency-injection?view=aspnetcore-10.0) before reading this topic.

## Default services

The services shown in the following table are commonly used in Blazor apps.

| Service                                                                                  | Lifetime                                                                                                                                                                                                 | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [HttpClient](/en-us/dotnet/api/system.net.http.httpclient)                               | Scoped                                                                                                                                                                                                   | Provides methods for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.Client-side, an instance of [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is registered by the app in the `Program` file and uses the browser for handling the HTTP traffic in the background.Server-side, an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) isn't configured as a service by default. In server-side code, provide an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).For more information, see [Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0).An [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is registered as a scoped service, not singleton. For more information, see the [Service lifetime](#service-lifetime) section. |
| [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime)                           | **Client-side**: Singleton**Server-side**: ScopedThe Blazor framework registers [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) in the app's service container.                           | Represents an instance of a JavaScript runtime where JavaScript calls are dispatched. For more information, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0).When seeking to inject the service into a singleton service on the server, take either of the following approaches:- Change the service registration to scoped to match [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime)'s registration, which is appropriate if the service deals with user-specific state.
- Pass the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) into the singleton service's implementation as an argument of its method calls instead of injecting it into the singleton.                                                |
| [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) | **Client-side**: Singleton**Server-side**: ScopedThe Blazor framework registers [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) in the app's service container. | Contains helpers for working with URIs and navigation state. For more information, see [URI and navigation state helpers](navigation?view=aspnetcore-10.0#uri-and-navigation-state-helpers).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |

Additional services registered by the Blazor framework are described in the documentation where they're used to describe Blazor features, such as configuration and logging.

A custom service provider doesn't automatically provide the default services listed in the table. If you use a custom service provider and require any of the services shown in the table, add the required services to the new service provider.

## Add client-side services

Configure services for the app's service collection in the `Program` file. In the following example, the `ExampleDependency` implementation is registered for `IExampleDependency`:

```
var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
...

await builder.Build().RunAsync();
```

After the host is built, services are available from the root DI scope before any components are rendered. This can be useful for running initialization logic before rendering content:

```
var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync();

await host.RunAsync();
```

The host provides a central configuration instance for the app. Building on the preceding example, the weather service's URL is passed from a default configuration source (for example, `appsettings.json`) to `InitializeWeatherAsync`:

```
var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync(
    host.Configuration["WeatherServiceUrl"]);

await host.RunAsync();
```

## Add server-side services

After creating a new app, examine part of the `Program` file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
```

The `builder` variable represents a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) with an [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection), which is a list of [service descriptor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor) objects. Services are added by providing service descriptors to the service collection. The following example demonstrates the concept with the `IDataAccess` interface and its concrete implementation `DataAccess`:

```
builder.Services.AddSingleton<IDataAccess, DataAccess>();
```

After creating a new app, examine the `Startup.ConfigureServices` method in `Startup.cs`:

```
using Microsoft.Extensions.DependencyInjection;

...

public void ConfigureServices(IServiceCollection services)
{
    ...
}
```

The [ConfigureServices](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.configureservices) method is passed an [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection), which is a list of [service descriptor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor) objects. Services are added in the `ConfigureServices` method by providing service descriptors to the service collection. The following example demonstrates the concept with the `IDataAccess` interface and its concrete implementation `DataAccess`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDataAccess, DataAccess>();
}
```

## Register common services

If one or more common services are required client- and server-side, you can place the common service registrations in a method client-side and call the method to register the services in both projects.

First, factor common service registrations into a separate method. For example, create a `ConfigureCommonServices` method client-side:

```
public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}
```

For the client-side `Program` file, call `ConfigureCommonServices` to register the common services:

```
var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);
```

In the server-side `Program` file, call `ConfigureCommonServices` to register the common services:

```
var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);
```

For an example of this approach, see [ASP.NET Core Blazor WebAssembly additional security scenarios](../security/webassembly/additional-scenarios?view=aspnetcore-10.0#prerendering-with-authentication).

## Client-side services that fail during prerendering

*This section only applies to WebAssembly components in Blazor Web Apps.*

Blazor Web Apps normally prerender client-side WebAssembly components. If an app is run with a required service only registered in the `.Client` project, executing the app results in a runtime error similar to the following when a component attempts to use the required service during prerendering:

> InvalidOperationException: Cannot provide a value for {PROPERTY} on type '{ASSEMBLY}}.Client.Pages.{COMPONENT NAME}'. There is no registered service of type '{SERVICE}'.

Use ***either*** of the following approaches to resolve this problem:

* Register the service in the main project to make it available during component prerendering.
* If prerendering isn't required for the component, disable prerendering by following the guidance in [Prerender ASP.NET Core Razor components](../components/prerender?view=aspnetcore-10.0#disable-prerendering). If you adopt this approach, you don't need to register the service in the main project.

For more information, see the [Client-side services fail to resolve during prerendering](../components/prerender?view=aspnetcore-10.0#client-side-services-fail-to-resolve-during-prerendering) section of the *Prerendering* article, which appears later in the Blazor documentation.

## Service lifetime

Services can be configured with the lifetimes shown in the following table.

| Lifetime                                                                                            | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Scoped](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor.scoped)       | Client-side doesn't currently have a concept of DI scopes. `Scoped`-registered services behave like `Singleton` services.Server-side development supports the `Scoped` lifetime across HTTP requests but not across SignalR connection/circuit messages among components that are loaded on the client. The Razor Pages or MVC portion of the app treats scoped services normally and recreates the services on *each HTTP request* when navigating among pages or views or from a page or view to a component. Scoped services aren't reconstructed when navigating among components on the client, where the communication to the server takes place over the SignalR connection of the user's circuit, not via HTTP requests. In the following component scenarios on the client, scoped services are reconstructed because a new circuit is created for the user:- The user closes the browser's window. The user opens a new window and navigates back to the app.
- The user closes a tab of the app in a browser window. The user opens a new tab and navigates back to the app.
- The user selects the browser's reload/refresh button.For more information on preserving user state in server-side apps, see [ASP.NET Core Blazor state management overview](../state-management/?view=aspnetcore-10.0) and [ASP.NET Core Blazor server-side state management](../state-management/server?view=aspnetcore-10.0). |
| [Singleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor.singleton) | DI creates a *single instance* of the service. All components requiring a `Singleton` service receive the same instance of the service.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| [Transient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor.transient) | Whenever a component obtains an instance of a `Transient` service from the service container, it receives a *new instance* of the service.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |

The DI system is based on the DI system in ASP.NET Core. For more information, see [Dependency injection in ASP.NET Core](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

## Request a service in a component

For injecting services into components, Blazor supports [constructor injection](#constructor-injection) and [property injection](#property-injection).

### Constructor injection

After services are added to the service collection, inject one or more services into components with constructor injection. The following example injects the `NavigationManager` service.

`ConstructorInjection.razor`:

```
@page "/constructor-injection"

<button @onclick="HandleClick">
    Take me to the Counter component
</button>
```

`ConstructorInjection.razor.cs`:

```
using Microsoft.AspNetCore.Components;

public partial class ConstructorInjection(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}
```

### Property injection

After services are added to the service collection, inject one or more services into components with the [`@inject`](../../mvc/views/razor?view=aspnetcore-10.0#inject) Razor directive, which has two parameters:

* Type: The type of the service to inject.
* Property: The name of the property receiving the injected app service. The property doesn't require manual creation. The compiler creates the property.

For more information, see [Dependency injection into views in ASP.NET Core](../../mvc/views/dependency-injection?view=aspnetcore-10.0).

Use multiple [`@inject`](../../mvc/views/razor?view=aspnetcore-10.0#inject) statements to inject different services.

The following example demonstrates shows how to use the [`@inject`](../../mvc/views/razor?view=aspnetcore-10.0#inject) directive. The service implementing `Services.NavigationManager` is injected into the component's property `Navigation`. Note how the code is only using the `NavigationManager` abstraction.

`PropertyInjection.razor`:

```
@page "/property-injection"
@inject NavigationManager Navigation

<button @onclick="@(() => Navigation.NavigateTo("/counter"))">
    Take me to the Counter component
</button>
```

Internally, the generated property (`Navigation`) uses the [`[Inject]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.injectattribute). Typically, this attribute isn't used directly. If a base class is required for components and injected properties are also required for the base class, manually add the [`[Inject]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.injectattribute):

```
using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent
{
    [Inject]
    protected NavigationManager Navigation { get; set; } = default!;

    ...
}
```

Note

Since injected services are expected to be available, the default literal with the null-forgiving operator (`default!`) is assigned in .NET 6 or later. For more information, see [Nullable reference types (NRTs) and .NET compiler null-state static analysis](../../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis).

In components derived from a base class, the [`@inject`](../../mvc/views/razor?view=aspnetcore-10.0#inject) directive isn't required. The [InjectAttribute](/en-us/dotnet/api/microsoft.aspnetcore.components.injectattribute) of the base class is sufficient. The component only requires the [`@inherits`](../../mvc/views/razor?view=aspnetcore-10.0#inherits) directive. In the following example, any injected services of `CustomComponentBase` are available to the `Demo` component:

```
@page "/demo"
@inherits CustomComponentBase
```

## Service injection via a top-level imports file (`_Imports.razor`)

*This section only applies to Blazor Web Apps.*

A top-level imports file in the `Components` folder (`Components/_Imports.razor`) injects its references into all of the components in the folder hierarchy, which includes the `App` component (`App.razor`). The `App` component is always rendered statically even if [prerendering of a page component is disabled](../components/prerender?view=aspnetcore-10.0#disable-prerendering). Therefore, injecting services via the top-level imports file results in resolving *two instances* of the service in page components.

To address this scenario, inject the service in a new imports file placed in the `Pages` folder (`Components/Pages/_Imports.razor`). From that location, the service is only resolved once in page components.

## Use DI in services

Complex services might require additional services. In the following example, `DataAccess` requires the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) default service. [`@inject`](../../mvc/views/razor?view=aspnetcore-10.0#inject) (or the [`[Inject]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.injectattribute)) isn't available for use in services. *Constructor injection* must be used instead. Required services are added by adding parameters to the service's constructor. When DI creates the service, it recognizes the services it requires in the constructor and provides them accordingly. In the following example, the constructor receives an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) via DI. [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is a default service.

```
using System.Net.Http;

public class DataAccess : IDataAccess
{
    public DataAccess(HttpClient http)
    {
        ...
    }

    ...
}
```

Constructor injection is supported with [primary constructors](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) in C# 12 (.NET 8) or later:

```
using System.Net.Http;

public class DataAccess(HttpClient http) : IDataAccess
{
    ...
}
```

Prerequisites for constructor injection:

* One constructor must exist whose arguments can all be fulfilled by DI. Additional parameters not covered by DI are allowed if they specify default values.
* The applicable constructor must be `public`.
* One applicable constructor must exist. In case of an ambiguity, DI throws an exception.

## Inject keyed services into components

Blazor supports injecting keyed services using the `[Inject]` attribute. Keys allow for scoping of registration and consumption of services when using dependency injection. Use the [InjectAttribute.Key](/en-us/dotnet/api/microsoft.aspnetcore.components.injectattribute.key#microsoft-aspnetcore-components-injectattribute-key) property to specify the key for the service to inject:

```
[Inject(Key = "my-service")]
public IMyService MyService { get; set; }
```

## Utility base component classes to manage a DI scope

In non-Blazor ASP.NET Core apps, scoped and transient services are typically scoped to the current request. After the request completes, scoped and transient services are disposed by the DI system.

In interactive server-side Blazor apps, the DI scope lasts for the duration of the circuit (the SignalR connection between the client and server), which can result in scoped and disposable transient services living much longer than the lifetime of a single component. Therefore, don't directly inject a scoped service into a component if you intend the service lifetime to match the lifetime of the component. Transient services injected into a component that don't implement [IDisposable](/en-us/dotnet/api/system.idisposable) are garbage collected when the component is disposed. However, injected transient services *that implement [IDisposable](/en-us/dotnet/api/system.idisposable)* are maintained by the DI container for the lifetime of the circuit, which prevents service garbage collection when the component is disposed and results in a memory leak. An alternative approach for scoped services based on the [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase) type is described later in this section, and disposable transient services shouldn't be used at all. For more information, see [Design for solving transient disposables on Blazor Server (`dotnet/aspnetcore` #26676)](https://github.com/dotnet/aspnetcore/issues/26676).

Even in client-side Blazor apps that don't operate over a circuit, services registered with a scoped lifetime are treated as singletons, so they live longer than scoped services in typical ASP.NET Core apps. Client-side disposable transient services also live longer than the components where they're injected because the DI container, which holds references to disposable services, persists for the lifetime of the app, preventing garbage collection on the services. Although long-lived disposable transient services are of greater concern on the server, they should be avoided as client service registrations as well. Use of the [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase) type is also recommended for client-side scoped services to control service lifetime, and disposable transient services shouldn't be used at all.

An approach that limits a service lifetime is use of the [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase) type. [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase) is an abstract type derived from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) that creates a DI scope corresponding to the *lifetime of the component*. Using this scope, a component can inject services with a scoped lifetime and have them live as long as the component. When the component is destroyed, services from the component's scoped service provider are disposed as well. This can be useful for services reused within a component but not shared across components.

Two versions of [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase) type are available and described in the next two sections:

* [`OwningComponentBase`](#owningcomponentbase)
* [`OwningComponentBase<TService>`](#owningcomponentbasetservice)

### `OwningComponentBase`

[OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase) is an abstract, disposable child of the [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) type with a protected [ScopedServices](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase.scopedservices#microsoft-aspnetcore-components-owningcomponentbase-scopedservices) property of type [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider). The provider can be used to resolve services that are scoped to the lifetime of the component.

DI services injected into the component using [`@inject`](../../mvc/views/razor?view=aspnetcore-10.0#inject) or the [`[Inject]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.injectattribute) aren't created in the component's scope. To use the component's scope, services must be resolved using [ScopedServices](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase.scopedservices#microsoft-aspnetcore-components-owningcomponentbase-scopedservices) with either [GetRequiredService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.getrequiredservice) or [GetService](/en-us/dotnet/api/system.iserviceprovider.getservice). Any services resolved using the [ScopedServices](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase.scopedservices#microsoft-aspnetcore-components-owningcomponentbase-scopedservices) provider have their dependencies provided in the component's scope.

The following example demonstrates the difference between injecting a scoped service directly and resolving a service using [ScopedServices](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase.scopedservices#microsoft-aspnetcore-components-owningcomponentbase-scopedservices) on the server. The following interface and implementation for a time travel class include a `DT` property to hold a [DateTime](/en-us/dotnet/api/system.datetime) value. The implementation calls [DateTime.Now](/en-us/dotnet/api/system.datetime.now#system-datetime-now) to set `DT` when the `TimeTravel` class is instantiated.

`ITimeTravel.cs`:

```
public interface ITimeTravel
{
    public DateTime DT { get; set; }
}
```

`TimeTravel.cs`:

```
public class TimeTravel : ITimeTravel
{
    public DateTime DT { get; set; } = DateTime.Now;
}
```

The service is registered as scoped in the server-side `Program` file. Server-side, scoped services have a lifetime equal to the duration of the [circuit](../hosting-models?view=aspnetcore-10.0#blazor-server).

In the `Program` file:

```
builder.Services.AddScoped<ITimeTravel, TimeTravel>();
```

In the following `TimeTravel` component:

* The time travel service is directly injected with `@inject` as `TimeTravel1`.
* The service is also resolved separately with [ScopedServices](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase.scopedservices#microsoft-aspnetcore-components-owningcomponentbase-scopedservices) and [GetRequiredService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.getrequiredservice) as `TimeTravel2`.

`TimeTravel.razor`:

```
@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}
```

```
@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}
```

Initially navigating to the `TimeTravel` component, the time travel service is instantiated twice when the component loads, and `TimeTravel1` and `TimeTravel2` have the same initial value:

> TimeTravel1.DT: 8/31/2022 2:54:45 PM\
> TimeTravel2.DT: 8/31/2022 2:54:45 PM

When navigating away from the `TimeTravel` component to another component and back to the `TimeTravel` component:

* `TimeTravel1` is provided the same service instance that was created when the component first loaded, so the value of `DT` remains the same.
* `TimeTravel2` obtains a new `ITimeTravel` service instance in `TimeTravel2` with a new DT value.

> TimeTravel1.DT: 8/31/2022 2:54:45 PM\
> TimeTravel2.DT: 8/31/2022 2:54:48 PM

`TimeTravel1` is tied to the user's circuit, which remains intact and isn't disposed until the underlying circuit is deconstructed. For example, the service is disposed if the circuit is disconnected for the [disconnected circuit retention period](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.disconnectedcircuitretentionperiod#microsoft-aspnetcore-components-server-circuitoptions-disconnectedcircuitretentionperiod).

In spite of the scoped service registration in the `Program` file and the longevity of the user's circuit, `TimeTravel2` receives a new `ITimeTravel` service instance each time the component is initialized.

### `OwningComponentBase<TService>`

[OwningComponentBase\<TService>](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase-1) derives from [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase) and adds a [Service](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase-1.service) property that returns an instance of `T` from the scoped DI provider. This type is a convenient way to access scoped services without using an instance of [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) when there's one primary service the app requires from the DI container using the component's scope. The [ScopedServices](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase.scopedservices#microsoft-aspnetcore-components-owningcomponentbase-scopedservices) property is available, so the app can get services of other types, if necessary.

```
@page "/users"
@attribute [Authorize]
@inherits OwningComponentBase<AppDbContext>

<h1>Users (@Service.Users.Count())</h1>

<ul>
    @foreach (var user in Service.Users)
    {
        <li>@user.UserName</li>
    }
</ul>
```

### Detect client-side transient disposables

Custom code can be added to a client-side Blazor app to detect disposable transient services in an app that should use [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase). This approach is useful if you're concerned that code added to the app in the future consumes one or more transient disposable services, including services added by libraries. Demonstration code is available in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples/tree/main) ([how to download](./?view=aspnetcore-10.0#sample-apps)).

Inspect the following in .NET 6 or later versions of the `BlazorSample_WebAssembly` sample:

* `DetectIncorrectUsagesOfTransientDisposables.cs`

* `Services/TransientDisposableService.cs`

* In `Program.cs`:

  * The app's `Services` namespace is provided at the top of the file (`using BlazorSample.Services;`).
  * `DetectIncorrectUsageOfTransients` is called immediately after the `builder` is assigned from [WebAssemblyHostBuilder.CreateDefault](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.createdefault).
  * The `TransientDisposableService` is registered (`builder.Services.AddTransient<TransientDisposableService>();`).
  * `EnableTransientDisposableDetection` is called on the built host in the processing pipeline of the app (`host.EnableTransientDisposableDetection();`).

* The app registers the `TransientDisposableService` service without throwing an exception. However, attempting to resolve the service in `TransientService.razor` throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) when the framework attempts to construct an instance of `TransientDisposableService`.

### Detect server-side transient disposables

Custom code can be added to a server-side Blazor app to detect server-side disposable transient services in an app that should use [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase). This approach is useful if you're concerned that code added to the app in the future consumes one or more transient disposable services, including services added by libraries. Demonstration code is available in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples/tree/main) ([how to download](./?view=aspnetcore-10.0#sample-apps)).

Inspect the following in .NET 8 or later versions of the `BlazorSample_BlazorWebApp` sample:

Inspect the following in .NET 6 or .NET 7 versions of the `BlazorSample_Server` sample:

* `DetectIncorrectUsagesOfTransientDisposables.cs`

* `Services/TransitiveTransientDisposableDependency.cs`:

* In `Program.cs`:

  * The app's `Services` namespace is provided at the top of the file (`using BlazorSample.Services;`).
  * `DetectIncorrectUsageOfTransients` is called on the host builder (`builder.DetectIncorrectUsageOfTransients();`).
  * The `TransientDependency` service is registered (`builder.Services.AddTransient<TransientDependency>();`).
  * The `TransitiveTransientDisposableDependency` is registered for `ITransitiveTransientDisposableDependency` (`builder.Services.AddTransient<ITransitiveTransientDisposableDependency, TransitiveTransientDisposableDependency>();`).

* The app registers the `TransientDependency` service without throwing an exception. However, attempting to resolve the service in `TransientService.razor` throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) when the framework attempts to construct an instance of `TransientDependency`.

### Transient service registrations for `IHttpClientFactory`/`HttpClient` handlers

Transient service registrations for [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory)/[HttpClient](/en-us/dotnet/api/system.net.http.httpclient) handlers are recommended. If the app contains [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory)/[HttpClient](/en-us/dotnet/api/system.net.http.httpclient) handlers and uses the [IRemoteAuthenticationBuilder\<TRemoteAuthenticationState,TAccount>](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iremoteauthenticationbuilder-2) to add support for authentication, the following transient disposables for client-side authentication are also discovered, which is expected and can be ignored:

* [BaseAddressAuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.baseaddressauthorizationmessagehandler)
* [AuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler)

Other instances of [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory)/[HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are also discovered. These instances can also be ignored.

The Blazor sample apps in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples/tree/main) ([how to download](./?view=aspnetcore-10.0#sample-apps)) demonstrate the code to detect transient disposables. However, the code is deactivated because the sample apps include [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory)/[HttpClient](/en-us/dotnet/api/system.net.http.httpclient) handlers.

To activate the demonstration code and witness its operation:

* Uncomment the transient disposable lines in `Program.cs`.

* Remove the conditional check in `NavMenu.razor` that prevents the `TransientService` component from displaying in the app's navigation sidebar:

  ```
  - && (c.Name != "TransientService")
  ```

* Run the sample app and navigate to the `TransientService` component at `/transient-service`.

The Blazor sample apps in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples/tree/main) ([how to download](./?view=aspnetcore-10.0#sample-apps)) demonstrate the code to detect transient disposables. Run the sample app and navigate to the `TransientService` component at `/transient-service`.

## Use of an Entity Framework Core (EF Core) DbContext from DI

For more information, see [ASP.NET Core Blazor with Entity Framework Core (EF Core)](../blazor-ef-core?view=aspnetcore-10.0).

## Access server-side Blazor services from a different DI scope

[Circuit activity handlers](signalr?view=aspnetcore-10.0#monitor-server-side-circuit-activity) provide an approach for accessing scoped Blazor services from other non-Blazor dependency injection (DI) scopes.

Prior to the release of ASP.NET Core in .NET 8, accessing circuit-scoped services from other dependency injection scopes required using a custom base component type. With circuit activity handlers, a custom base component type isn't required, as the following example demonstrates:

```
public class CircuitServicesAccessor
{
    static readonly AsyncLocal<IServiceProvider> blazorServices = new();

    public IServiceProvider? Services
    {
        get => blazorServices.Value;
        set => blazorServices.Value = value!;
    }
}

public class ServicesAccessorCircuitHandler(
    IServiceProvider services, CircuitServicesAccessor servicesAccessor) 
    : CircuitHandler
{
    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next) => 
            async context =>
            {
                servicesAccessor.Services = services;
                await next(context);
                servicesAccessor.Services = null;
            };
}

public static class CircuitServicesServiceCollectionExtensions
{
    public static IServiceCollection AddCircuitServicesAccessor(
        this IServiceCollection services)
    {
        services.AddScoped<CircuitServicesAccessor>();
        services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>();

        return services;
    }
}
```

Call `AddCircuitServicesAccessor` in the app's `Program` file:

```
builder.Services.AddCircuitServicesAccessor();
```

Access the circuit-scoped services by injecting the `CircuitServicesAccessor` where it's needed.

For an example that shows how to access the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) from a [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) set up using [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory), see the circuit activity handler approach in [ASP.NET Core Blazor additional server-side security scenarios](../security/additional-scenarios?view=aspnetcore-10.0#access-authenticationstateprovider-in-outgoing-request-middleware).

There may be times when a Razor component invokes asynchronous methods that execute code in a different DI scope. Without the correct approach, these DI scopes don't have access to Blazor's services, such as [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) and [Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage](/en-us/dotnet/api/microsoft.aspnetcore.components.server.protectedbrowserstorage).

For example, [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances created using [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) have their own DI service scope. As a result, [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) instances configured on the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) aren't able to directly inject Blazor services.

Create a class `BlazorServiceAccessor` that defines an [`AsyncLocal`](/en-us/dotnet/api/system.threading.asynclocal-1), which stores the Blazor [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) for the current asynchronous context. A `BlazorServiceAccessor` instance can be acquired from within a different DI service scope to access Blazor services.

`BlazorServiceAccessor.cs`:

```
internal sealed class BlazorServiceAccessor
{
    private static readonly AsyncLocal<BlazorServiceHolder> s_currentServiceHolder = new();

    public IServiceProvider? Services
    {
        get => s_currentServiceHolder.Value?.Services;
        set
        {
            if (s_currentServiceHolder.Value is { } holder)
            {
                // Clear the current IServiceProvider trapped in the AsyncLocal.
                holder.Services = null;
            }

            if (value is not null)
            {
                // Use object indirection to hold the IServiceProvider in an AsyncLocal
                // so it can be cleared in all ExecutionContexts when it's cleared.
                s_currentServiceHolder.Value = new() { Services = value };
            }
        }
    }

    private sealed class BlazorServiceHolder
    {
        public IServiceProvider? Services { get; set; }
    }
}
```

To set the value of `BlazorServiceAccessor.Services` automatically when an `async` component method is invoked, create a custom base component that re-implements the three primary asynchronous entry points into Razor component code:

* [IComponent.SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.icomponent.setparametersasync)
* [IHandleEvent.HandleEventAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.ihandleevent.handleeventasync)
* [IHandleAfterRender.OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.ihandleafterrender.onafterrenderasync)

The following class demonstrates the implementation for the base component.

`CustomComponentBase.cs`:

```
using Microsoft.AspNetCore.Components;

public class CustomComponentBase : ComponentBase, IHandleEvent, IHandleAfterRender
{
    private bool hasCalledOnAfterRender;

    [Inject]
    private IServiceProvider Services { get; set; } = default!;

    [Inject]
    private BlazorServiceAccessor BlazorServiceAccessor { get; set; } = default!;

    public override Task SetParametersAsync(ParameterView parameters)
        => InvokeWithBlazorServiceContext(() => base.SetParametersAsync(parameters));

    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        => InvokeWithBlazorServiceContext(() =>
        {
            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                task.Status != TaskStatus.Canceled;

            StateHasChanged();

            return shouldAwaitTask ?
                CallStateHasChangedOnAsyncCompletion(task) :
                Task.CompletedTask;
        });

    Task IHandleAfterRender.OnAfterRenderAsync()
        => InvokeWithBlazorServiceContext(() =>
        {
            var firstRender = !hasCalledOnAfterRender;
            hasCalledOnAfterRender |= true;

            OnAfterRender(firstRender);

            return OnAfterRenderAsync(firstRender);
        });

    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch
        {
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        StateHasChanged();
    }

    private async Task InvokeWithBlazorServiceContext(Func<Task> func)
    {
        try
        {
            BlazorServiceAccessor.Services = Services;
            await func();
        }
        finally
        {
            BlazorServiceAccessor.Services = null;
        }
    }
}
```

Any components extending `CustomComponentBase` automatically have `BlazorServiceAccessor.Services` set to the [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) in the current Blazor DI scope.

Finally, in the `Program` file, add the `BlazorServiceAccessor` as a scoped service:

```
builder.Services.AddScoped<BlazorServiceAccessor>();
```

Finally, in `Startup.ConfigureServices` of `Startup.cs`, add the `BlazorServiceAccessor` as a scoped service:

```
services.AddScoped<BlazorServiceAccessor>();
```

## Related content

* [Dependency injection in ASP.NET Core](../../fundamentals/dependency-injection?view=aspnetcore-10.0)
* [`IDisposable` guidance for Transient and shared instances](../../fundamentals/dependency-injection?view=aspnetcore-10.0#idisposable-guidance-for-transient-and-shared-instances)
* [Dependency injection into views in ASP.NET Core](../../mvc/views/dependency-injection?view=aspnetcore-10.0)
* [Primary constructors (C# Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors)

- [Dependency injection in ASP.NET Core](../../fundamentals/dependency-injection?view=aspnetcore-10.0)
- [`IDisposable` guidance for Transient and shared instances](../../fundamentals/dependency-injection?view=aspnetcore-10.0#idisposable-guidance-for-transient-and-shared-instances)
- [Dependency injection into views in ASP.NET Core](../../mvc/views/dependency-injection?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/http-context
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/http-context.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate ASP.NET Framework HttpContext to ASP.NET Core

HttpContext is a fundamental component of web applications, providing access to HTTP request and response information. When migrating from ASP.NET Framework to ASP.NET Core, HttpContext presents unique challenges because the two frameworks have different APIs and approaches.

## Why HttpContext migration is complex

ASP.NET Framework and ASP.NET Core have fundamentally different HttpContext implementations:

* **ASP.NET Framework** uses [System.Web.HttpContext](/en-us/dotnet/api/system.web.httpcontext) with built-in properties and methods
* **ASP.NET Core** uses [Microsoft.AspNetCore.Http.HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) with a more modular, extensible design

These differences mean you can't simply move your HttpContext code from Framework to Core without changes.

## Migration strategies overview

You have two main approaches for handling HttpContext during migration:

1. **Complete rewrite** - Rewrite all HttpContext code to use ASP.NET Core's native HttpContext implementation
2. **System.Web adapters** - Use adapters to minimize code changes while migrating incrementally

For most applications, migrating to ASP.NET Core's native HttpContext provides the best performance and maintainability. However, larger applications or those with extensive HttpContext usage may benefit from using System.Web adapters during incremental migration.

## Choose your migration approach

You have two main options for migrating HttpContext from ASP.NET Framework to ASP.NET Core. Your choice depends on your migration timeline, whether you need to run both applications simultaneously, and how much code you're willing to rewrite.

### Quick decision guide

**Answer these questions to choose your approach:**

1. **Are you doing a complete rewrite or incremental migration?**

   * Complete rewrite → [Complete rewrite to ASP.NET Core HttpContext](#complete-rewrite-to-aspnet-core-httpcontext)
   * Incremental migration → Continue to question 2

2. **Do you have extensive HttpContext usage across shared libraries?**

   * Yes, lots of shared code → [System.Web adapters](#systemweb-adapters)
   * No, isolated HttpContext usage → [Complete rewrite to ASP.NET Core HttpContext](#complete-rewrite-to-aspnet-core-httpcontext)

### Migration approaches comparison

| Approach                                                             | Code Changes                        | Performance | Shared Libraries         | When to Use                                         |
| -------------------------------------------------------------------- | ----------------------------------- | ----------- | ------------------------ | --------------------------------------------------- |
| **[Complete rewrite](#complete-rewrite-to-aspnet-core-httpcontext)** | High - Rewrite all HttpContext code | Best        | Requires updates         | Complete rewrites, performance-critical apps        |
| **[System.Web adapters](#systemweb-adapters)**                       | Low - Keep existing patterns        | Good        | Works with existing code | Incremental migrations, extensive HttpContext usage |

## Important differences

### HttpContext lifetime

The adapters are backed by [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) which cannot be used past the lifetime of a request. Thus, [HttpContext](/en-us/dotnet/api/system.web.httpcontext) when run on ASP.NET Core cannot be used past a request as well, while on ASP.NET Framework it would work at times. An [ObjectDisposedException](/en-us/dotnet/api/system.objectdisposedexception) will be thrown in cases where it is used past a request end.

**Recommendation**: Store the values needed into a POCO and hold onto that.

### Request threading considerations

Warning

ASP.NET Core does not guarantee thread affinity for requests. If your code requires thread-safe access to `HttpContext`, you must ensure proper synchronization.

In ASP.NET Framework, a request had thread-affinity and [Current](/en-us/dotnet/api/system.web.httpcontext.current#system-web-httpcontext-current) would only be available if on that thread. ASP.NET Core does not have this guarantee so [Current](/en-us/dotnet/api/system.web.httpcontext.current#system-web-httpcontext-current) will be available within the same async context, but no guarantees about threads are made.

**Recommendation**: If reading/writing to the [HttpContext](/en-us/dotnet/api/system.web.httpcontext), you must ensure you are doing so in a single-threaded way. You can force a request to never run concurrently on any async context by setting the `ISingleThreadedRequestMetadata`. This will have performance implications and should only be used if you can't refactor usage to ensure non-concurrent access. There is an implementation available to add to controllers with `SingleThreadedRequestAttribute`:

```
[SingleThreadedRequest]
public class SomeController : Controller
{
    ...
} 
```

### Request stream buffering

By default, the incoming request is not always seekable nor fully available. In order to get behavior seen in .NET Framework, you can opt into prebuffering the input stream. This will fully read the incoming stream and buffer it to memory or disk (depending on settings).

**Recommendation**: This can be enabled by applying endpoint metadata that implements the `IPreBufferRequestStreamMetadata` interface. This is available as an attribute `PreBufferRequestStreamAttribute` that can be applied to controllers or methods.

To enable this on all MVC endpoints, there is an extension method that can be used as follows:

```
app.MapDefaultControllerRoute()
    .PreBufferRequestStream();
```

### Response stream buffering

Some APIs on [Response](/en-us/dotnet/api/system.web.httpcontext.response#system-web-httpcontext-response) require that the output stream is buffered, such as [Output](/en-us/dotnet/api/system.web.httpresponse.output#system-web-httpresponse-output), [End()](/en-us/dotnet/api/system.web.httpresponse.end#system-web-httpresponse-end), [Clear()](/en-us/dotnet/api/system.web.httpresponse.clear#system-web-httpresponse-clear), and [SuppressContent](/en-us/dotnet/api/system.web.httpresponse.suppresscontent#system-web-httpresponse-suppresscontent).

**Recommendation**: In order to support behavior for [Response](/en-us/dotnet/api/system.web.httpcontext.response#system-web-httpcontext-response) that requires buffering the response before sending, endpoints must opt-into it with endpoint metadata implementing `IBufferResponseStreamMetadata`.

To enable this on all MVC endpoints, there is an extension method that can be used as follows:

```
app.MapDefaultControllerRoute()
    .BufferResponseStream();
```

## Complete rewrite to ASP.NET Core HttpContext

Choose this approach when you're performing a complete migration and can rewrite HttpContext-related code to use ASP.NET Core's native implementation.

ASP.NET Core's HttpContext provides a more modular and extensible design compared to ASP.NET Framework. This approach offers the best performance but requires more code changes during migration.

### Overview

`HttpContext` has significantly changed in ASP.NET Core. When migrating HTTP modules or handlers to middleware, you'll need to update your code to work with the new `HttpContext` API.

In ASP.NET Core middleware, the `Invoke` method takes a parameter of type `HttpContext`:

```
public async Task Invoke(HttpContext context)
```

This `HttpContext` is different from the ASP.NET Framework version and requires different approaches to access request and response information.

### Property translations

This section shows how to translate the most commonly used properties of [System.Web.HttpContext](/en-us/dotnet/api/system.web.httpcontext) to the equivalent [Microsoft.AspNetCore.Http.HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) in ASP.NET Core.

#### HttpContext properties

* **[HttpContext.Items](/en-us/dotnet/api/system.web.httpcontext.items#system-web-httpcontext-items)** → **[HttpContext.Items](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.items#microsoft-aspnetcore-http-httpcontext-items)**

  ```
  IDictionary<object, object> items = httpContext.Items;
  ```

* ***No equivalent*** → **[HttpContext.TraceIdentifier](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.traceidentifier#microsoft-aspnetcore-http-httpcontext-traceidentifier)**

  ```
  string requestId = httpContext.TraceIdentifier;
  ```

  Unique request ID for logging

#### HttpRequest properties

* **[HttpRequest.HttpMethod](/en-us/dotnet/api/system.web.httprequest.httpmethod#system-web-httprequest-httpmethod)** → **[HttpRequest.Method](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.method#microsoft-aspnetcore-http-httprequest-method)**

  ```
  string httpMethod = httpContext.Request.Method;
  ```

* **[HttpRequest.QueryString](/en-us/dotnet/api/system.web.httprequest.querystring#system-web-httprequest-querystring)** → **[HttpRequest.QueryString](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.querystring#microsoft-aspnetcore-http-httprequest-querystring)**

  ```
  IQueryCollection queryParameters = httpContext.Request.Query;

  // If no query parameter "key" used, values will have 0 items
  // If single value used for a key (...?key=v1), values will have 1 item ("v1")
  // If key has multiple values (...?key=v1&key=v2), values will have 2 items ("v1" and "v2")
  IList<string> values = queryParameters["key"];

  // If no query parameter "key" used, value will be ""
  // If single value used for a key (...?key=v1), value will be "v1"
  // If key has multiple values (...?key=v1&key=v2), value will be "v1,v2"
  string value = queryParameters["key"].ToString();
  ```

* **[HttpRequest.Url](/en-us/dotnet/api/system.web.httprequest.url#system-web-httprequest-url)** / **[HttpRequest.RawUrl](/en-us/dotnet/api/system.web.httprequest.rawurl#system-web-httprequest-rawurl)** → **Multiple properties**

  ```
  // using Microsoft.AspNetCore.Http.Extensions;
  var url = httpContext.Request.GetDisplayUrl();
  ```

  Use Request.Scheme, Host, PathBase, Path, QueryString

* **[HttpRequest.IsSecureConnection](/en-us/dotnet/api/system.web.httprequest.issecureconnection#system-web-httprequest-issecureconnection)** → **[HttpRequest.IsHttps](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.ishttps#microsoft-aspnetcore-http-httprequest-ishttps)**

  ```
  var isSecureConnection = httpContext.Request.IsHttps;
  ```

* **[HttpRequest.UserHostAddress](/en-us/dotnet/api/system.web.httprequest.userhostaddress#system-web-httprequest-userhostaddress)** → **[ConnectionInfo.RemoteIpAddress](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.remoteipaddress#microsoft-aspnetcore-http-connectioninfo-remoteipaddress)**

  ```
  var userHostAddress = httpContext.Connection.RemoteIpAddress?.ToString();
  ```

* **[HttpRequest.Cookies](/en-us/dotnet/api/system.web.httprequest.cookies#system-web-httprequest-cookies)** → **[HttpRequest.Cookies](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.cookies#microsoft-aspnetcore-http-httprequest-cookies)**

  ```
  IRequestCookieCollection cookies = httpContext.Request.Cookies;
  string unknownCookieValue = cookies["unknownCookie"]; // will be null (no exception)
  string knownCookieValue = cookies["cookie1name"];     // will be actual value
  ```

* **[HttpRequest.RequestContext](/en-us/dotnet/api/system.web.httprequest.requestcontext#system-web-httprequest-requestcontext)** → **[RoutingHttpContextExtensions.GetRouteData](/en-us/dotnet/api/microsoft.aspnetcore.routing.routinghttpcontextextensions.getroutedata)**

  ```
  var routeValue = httpContext.GetRouteValue("key");
  ```

* **[HttpRequest.Headers](/en-us/dotnet/api/system.web.httprequest.headers#system-web-httprequest-headers)** → **[HttpRequest.Headers](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.headers#microsoft-aspnetcore-http-httprequest-headers)**

  ```
  // using Microsoft.AspNetCore.Http.Headers;
  // using Microsoft.Net.Http.Headers;

  IHeaderDictionary headersDictionary = httpContext.Request.Headers;

  // GetTypedHeaders extension method provides strongly typed access to many headers
  var requestHeaders = httpContext.Request.GetTypedHeaders();
  CacheControlHeaderValue cacheControlHeaderValue = requestHeaders.CacheControl;

  // For unknown header, unknownheaderValues has zero items and unknownheaderValue is ""
  IList<string> unknownheaderValues = headersDictionary["unknownheader"];
  string unknownheaderValue = headersDictionary["unknownheader"].ToString();

  // For known header, knownheaderValues has 1 item and knownheaderValue is the value
  IList<string> knownheaderValues = headersDictionary[HeaderNames.AcceptLanguage];
  string knownheaderValue = headersDictionary[HeaderNames.AcceptLanguage].ToString();
  ```

* **[HttpRequest.UserAgent](/en-us/dotnet/api/system.web.httprequest.useragent#system-web-httprequest-useragent)** → **[HttpRequest.Headers](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.headers#microsoft-aspnetcore-http-httprequest-headers)**

  ```
  string userAgent = headersDictionary[HeaderNames.UserAgent].ToString();
  ```

* **[HttpRequest.UrlReferrer](/en-us/dotnet/api/system.web.httprequest.urlreferrer#system-web-httprequest-urlreferrer)** → **[HttpRequest.Headers](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.headers#microsoft-aspnetcore-http-httprequest-headers)**

  ```
  string urlReferrer = headersDictionary[HeaderNames.Referer].ToString();
  ```

* **[HttpRequest.ContentType](/en-us/dotnet/api/system.web.httprequest.contenttype#system-web-httprequest-contenttype)** → **[HttpRequest.ContentType](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.contenttype#microsoft-aspnetcore-http-httprequest-contenttype)**

  ```
  // using Microsoft.Net.Http.Headers;

  MediaTypeHeaderValue mediaHeaderValue = requestHeaders.ContentType;
  string contentType = mediaHeaderValue?.MediaType.ToString();   // ex. application/x-www-form-urlencoded
  string contentMainType = mediaHeaderValue?.Type.ToString();    // ex. application
  string contentSubType = mediaHeaderValue?.SubType.ToString();  // ex. x-www-form-urlencoded

  System.Text.Encoding requestEncoding = mediaHeaderValue?.Encoding;
  ```

* **[HttpRequest.Form](/en-us/dotnet/api/system.web.httprequest.form#system-web-httprequest-form)** → **[HttpRequest.Form](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.form#microsoft-aspnetcore-http-httprequest-form)**

  ```
  if (httpContext.Request.HasFormContentType)
  {
      IFormCollection form;

      form = httpContext.Request.Form; // sync
      // Or
      form = await httpContext.Request.ReadFormAsync(); // async

      string firstName = form["firstname"];
      string lastName = form["lastname"];
  }
  ```

  **Warning**: Read form values only if content type is *x-www-form-urlencoded* or *form-data*

* **[HttpRequest.InputStream](/en-us/dotnet/api/system.web.httprequest.inputstream#system-web-httprequest-inputstream)** → **[HttpRequest.Body](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.body#microsoft-aspnetcore-http-httprequest-body)**

  ```
  string inputBody;
  using (var reader = new System.IO.StreamReader(
      httpContext.Request.Body, System.Text.Encoding.UTF8))
  {
      inputBody = reader.ReadToEnd();
  }
  ```

  **Warning**: Use only in handler middleware at end of pipeline. Body can only be read once per request

#### HttpResponse properties

* **[HttpResponse.Status](/en-us/dotnet/api/system.web.httpresponse.status#system-web-httpresponse-status)** / **[HttpResponse.StatusDescription](/en-us/dotnet/api/system.web.httpresponse.statusdescription#system-web-httpresponse-statusdescription)** → **[HttpResponse.StatusCode](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.statuscode#microsoft-aspnetcore-http-httpresponse-statuscode)**

  ```
  // using Microsoft.AspNetCore.Http;
  httpContext.Response.StatusCode = StatusCodes.Status200OK;
  ```

* **[HttpResponse.ContentEncoding](/en-us/dotnet/api/system.web.httpresponse.contentencoding#system-web-httpresponse-contentencoding)** / **[HttpResponse.ContentType](/en-us/dotnet/api/system.web.httpresponse.contenttype#system-web-httpresponse-contenttype)** → **[HttpResponse.ContentType](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.contenttype#microsoft-aspnetcore-http-httpresponse-contenttype)**

  ```
  // using Microsoft.Net.Http.Headers;
  var mediaType = new MediaTypeHeaderValue("application/json");
  mediaType.Encoding = System.Text.Encoding.UTF8;
  httpContext.Response.ContentType = mediaType.ToString();
  ```

* **[HttpResponse.ContentType](/en-us/dotnet/api/system.web.httpresponse.contenttype#system-web-httpresponse-contenttype)** → **[HttpResponse.ContentType](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.contenttype#microsoft-aspnetcore-http-httpresponse-contenttype)**

  ```
  httpContext.Response.ContentType = "text/html";
  ```

* **[HttpResponse.Output](/en-us/dotnet/api/system.web.httpresponse.output#system-web-httpresponse-output)** → **[HttpResponseWritingExtensions.WriteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponsewritingextensions.writeasync)**

  ```
  string responseContent = GetResponseContent();
  await httpContext.Response.WriteAsync(responseContent);
  ```

* **[HttpResponse.TransmitFile](/en-us/dotnet/api/system.web.httpresponse.transmitfile)** → **See request features**

  Serving files is discussed in [Request Features in ASP.NET Core](../../../fundamentals/request-features?view=aspnetcore-10.0)

* **[HttpResponse.Headers](/en-us/dotnet/api/system.web.httpresponse.headers#system-web-httpresponse-headers)** → **[HttpResponse.OnStarting](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.onstarting)**

  ```
  // using Microsoft.AspNet.Http.Headers;
  // using Microsoft.Net.Http.Headers;

  private Task SetHeaders(object context)
  {
      var httpContext = (HttpContext)context;

      // Set header with single value
      httpContext.Response.Headers["ResponseHeaderName"] = "headerValue";

      // Set header with multiple values
      string[] responseHeaderValues = new string[] { "headerValue1", "headerValue1" };
      httpContext.Response.Headers["ResponseHeaderName"] = responseHeaderValues;

      // Translating ASP.NET 4's HttpContext.Response.RedirectLocation  
      httpContext.Response.Headers[HeaderNames.Location] = "http://www.example.com";
      // Or
      httpContext.Response.Redirect("http://www.example.com");

      // GetTypedHeaders extension method provides strongly typed access to many headers
      var responseHeaders = httpContext.Response.GetTypedHeaders();

      // Translating ASP.NET 4's HttpContext.Response.CacheControl 
      responseHeaders.CacheControl = new CacheControlHeaderValue
      {
          MaxAge = new System.TimeSpan(365, 0, 0, 0)
          // Many more properties available 
      };

      // If you use .NET Framework 4.6+, Task.CompletedTask will be a bit faster
      return Task.FromResult(0);
  }
  ```

  Must use callback pattern to set headers before response starts

* **[HttpResponse.Cookies](/en-us/dotnet/api/system.web.httpresponse.cookies#system-web-httpresponse-cookies)** → **[HttpResponse.OnStarting](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.onstarting)**

  ```
  private Task SetCookies(object context)
  {
      var httpContext = (HttpContext)context;

      IResponseCookies responseCookies = httpContext.Response.Cookies;

      responseCookies.Append("cookie1name", "cookie1value");
      responseCookies.Append("cookie2name", "cookie2value",
          new CookieOptions { Expires = System.DateTime.Now.AddDays(5), HttpOnly = true });

      // If you use .NET Framework 4.6+, Task.CompletedTask will be a bit faster
      return Task.FromResult(0); 
  }
  ```

  Must use callback pattern to set cookies before response starts

* Setting response headers:

  ```
  public async Task Invoke(HttpContext httpContext)
  {
      // Set callback to execute before response starts
      httpContext.Response.OnStarting(SetHeaders, state: httpContext);
      // ... rest of middleware logic
  }
  ```

* Setting response cookies:

```
public async Task Invoke(HttpContext httpContext)
{
    // Set callbacks to execute before response starts
    httpContext.Response.OnStarting(SetCookies, state: httpContext);
    httpContext.Response.OnStarting(SetHeaders, state: httpContext);
    // ... rest of middleware logic
}
```

## System.Web adapters

Note

This makes use of the [System.Web Adapters](../inc/systemweb-adapters?view=aspnetcore-10.0) to simplify migration.

Choose this approach when you have extensive HttpContext usage across shared libraries or when performing an incremental migration where you want to minimize code changes.

The [System.Web adapters](../inc/systemweb-adapters?view=aspnetcore-10.0) provide a compatibility layer that allows you to use familiar [HttpContext](/en-us/dotnet/api/system.web.httpcontext) APIs in ASP.NET Core applications. This approach is particularly useful when:

* You have shared libraries that use [HttpContext](/en-us/dotnet/api/system.web.httpcontext)
* You're performing an incremental migration
* You want to minimize code changes during the migration process

### Benefits of using System.Web adapters

* **Minimal code changes**: Keep your existing `System.Web.HttpContext` usage patterns
* **Shared libraries**: Libraries can work with both ASP.NET Framework and ASP.NET Core
* **Incremental migration**: Migrate applications piece by piece without breaking shared dependencies
* **Faster migration**: Reduce the time needed to migrate complex applications

### Considerations

* **Performance**: While good, adapters introduce some overhead compared to native ASP.NET Core APIs
* **Feature parity**: Not all [HttpContext](/en-us/dotnet/api/system.web.httpcontext) features are available through adapters
* **Long-term strategy**: Consider eventually migrating to native ASP.NET Core APIs for best performance

For more information about System.Web adapters, see the [System.Web adapters documentation](../inc/systemweb-adapters?view=aspnetcore-10.0).

## Related content

* [HTTP Handlers and HTTP Modules Overview](/en-us/iis/configuration/system.webserver/)
* [HttpContext in ASP.NET Core](../../../fundamentals/http-context?view=aspnetcore-10.0)
* [Middleware](../../../fundamentals/middleware/?view=aspnetcore-10.0)
* [Configuration](../../../fundamentals/configuration/?view=aspnetcore-10.0)
* [System.Web adapters](../inc/systemweb-adapters?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/partial-tag-helper
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/partial-tag-helper.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Partial Tag Helper in ASP.NET Core

By [Scott Addie](https://github.com/scottaddie)

For an overview of Tag Helpers, see [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/tag-helpers/built-in/samples) ([how to download](../../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Overview

The Partial Tag Helper is used for rendering a [partial view](../../partial?view=aspnetcore-10.0) in Razor Pages and MVC apps. Consider that it:

* Requires ASP.NET Core 2.1 or later.
* Is an alternative to [HTML Helper syntax](../../partial?view=aspnetcore-10.0#reference-a-partial-view).
* Renders the partial view asynchronously.

The HTML Helper options for rendering a partial view include:

* [`@await Html.PartialAsync`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.partialasync)
* [`@await Html.RenderPartialAsync`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.renderpartialasync)
* [`@Html.Partial`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.partial)
* [`@Html.RenderPartial`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.renderpartial)

The *Product* model is used in samples throughout this document:

```
namespace TagHelpersBuiltIn.Models
{
    public class Product
    {
        public int Number { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }
    }
}
```

An inventory of the Partial Tag Helper attributes follows.

## name

The `name` attribute is required. It indicates the name or the path of the partial view to be rendered. When a partial view name is provided, the [view discovery](../../overview?view=aspnetcore-10.0#view-discovery) process is initiated. That process is bypassed when an explicit path is provided. For all acceptable `name` values, see [Partial view discovery](../../partial?view=aspnetcore-10.0#partial-view-discovery).

The following markup uses an explicit path, indicating that `_ProductPartial.cshtml` is to be loaded from the *Shared* folder. Using the [for](#for) attribute, a model is passed to the partial view for binding.

```
<partial name="Shared/_ProductPartial.cshtml" for="Product">
```

## for

The `for` attribute assigns a [ModelExpression](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.modelexpression) to be evaluated against the current model. A `ModelExpression` infers the `@Model.` syntax. For example, `for="Product"` can be used instead of `for="@Model.Product"`. This default inference behavior is overridden by using the `@` symbol to define an inline expression.

The following markup loads `_ProductPartial.cshtml`:

```
<partial name="_ProductPartial" for="Product">
```

The partial view is bound to the associated page model's `Product` property:

```
using Microsoft.AspNetCore.Mvc.RazorPages;
using TagHelpersBuiltIn.Models;

namespace TagHelpersBuiltIn.Pages
{
    public class ProductModel : PageModel
    {
        public Product Product { get; set; }

        public void OnGet()
        {
            Product = new Product
            {
                Number = 1,
                Name = "Test product",
                Description = "This is a test product"
            };
        }
    }
}
```

## model

The `model` attribute assigns a model instance to pass to the partial view. The `model` attribute can't be used with the [for](#for) attribute.

When neither `for` nor `model` is specified, the Partial Tag Helper falls back to passing the calling view's model (`ViewContext.ViewData.Model`) to the partial view.

In the following markup, a new `Product` object is instantiated and passed to the `model` attribute for binding:

```
<partial name="_ProductPartial"
         model='new Product { Number = 1, Name = "Test product", Description = "This is a test" }'>
```

## view-data

The `view-data` attribute assigns a [ViewDataDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.viewdatadictionary) to pass to the partial view. The following markup makes the entire ViewData collection accessible to the partial view:

```
@{
    ViewData["IsNumberReadOnly"] = true;
}

<partial name="_ProductViewDataPartial" for="Product" view-data="ViewData">
```

In the preceding code, the `IsNumberReadOnly` key value is set to `true` and added to the ViewData collection. Consequently, `ViewData["IsNumberReadOnly"]` is made accessible within the following partial view:

```
@model TagHelpersBuiltIn.Models.Product

<div class="form-group">
    <label asp-for="Number"></label>
    @if ((bool)ViewData["IsNumberReadOnly"])
    {
        <input asp-for="Number" type="number" class="form-control" readonly />
    }
    else
    {
        <input asp-for="Number" type="number" class="form-control" />
    }
</div>
<div class="form-group">
    <label asp-for="Name"></label>
    <input asp-for="Name" type="text" class="form-control" />
</div>
<div class="form-group">
    <label asp-for="Description"></label>
    <textarea asp-for="Description" rows="4" cols="50" class="form-control"></textarea>
</div>
```

In this example, the value of `ViewData["IsNumberReadOnly"]` determines whether the *Number* field is displayed as read only.

## Migrate from an HTML Helper

Consider the following asynchronous HTML Helper example. A collection of products is iterated and displayed. Per the `PartialAsync` method's first parameter, the `_ProductPartial.cshtml` partial view is loaded. An instance of the `Product` model is passed to the partial view for binding.

```
@foreach (var product in Model.Products)
{
    @await Html.PartialAsync("_ProductPartial", product)
}
```

The following Partial Tag Helper achieves the same asynchronous rendering behavior as the `PartialAsync` HTML Helper. The `model` attribute is assigned a `Product` model instance for binding to the partial view.

```
@foreach (var product in Model.Products)
{
    <partial name="_ProductPartial" model="@product" />
}
```

## Related content

* [Partial views in ASP.NET Core](../../partial?view=aspnetcore-10.0)
* [Views in ASP.NET Core MVC](../../overview?view=aspnetcore-10.0#weakly-typed-data-viewdata-viewdata-attribute-and-viewbag)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-09

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/inc/systemweb-adapters
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/inc/systemweb-adapters.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# System.Web adapters

The main use case of the adapters in the [dotnet/systemweb-adapters](https://github.com/dotnet/systemweb-adapters) repository is to help developers who have taken a reliance on `System.Web` types within their class libraries as they want to move to ASP.NET Core.

An important feature of the adapters is the adapters allow the library to be used from ***both*** ASP.NET Framework and ASP.NET Core projects. Updating multiple ASP.NET Framework apps to ASP.NET Core often involves intermediate states where not all the apps have been fully updated. By using the `System.Web` adapters, the library can be used both from ASP.NET Core callers and ASP.NET Framework callers that haven't been upgraded.

Let's take a look at an example using the adapters moving from .NET Framework to ASP.NET Core.

## Packages

* `Microsoft.AspNetCore.SystemWebAdapters`: This package is used in supporting libraries and provides the System.Web APIs you may have taken a dependency on, such as `HttpContext` and others. This package targets .NET Standard 2.0, .NET Framework 4.5+, and .NET 5+.
* `Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices`: This package only targets .NET Framework and is intended to provide services to ASP.NET Framework applications that may need to provide incremental migrations. This is generally not expected to be referenced from libraries, but rather from the applications themselves.
* `Microsoft.AspNetCore.SystemWebAdapters.CoreServices`: This package only targets .NET 6+ and is intended to provide services to ASP.NET Core applications to configure behavior of `System.Web` APIs as well as opting into any behaviors for incremental migration. This is generally not expected to be referenced from libraries, but rather from the applications themselves.
* `Microsoft.AspNetCore.SystemWebAdapters.Abstractions`: This package is a supporting package that provides abstractions for services used by both the ASP.NET Core and ASP.NET Framework application such as session state serialization.

### Converting to System.Web.HttpContext

To convert between the two representations of HttpContext, you may do the following:

For [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) to [HttpContext](/en-us/dotnet/api/system.web.httpcontext):

* Implicit casting
* `HttpContext.AsSystemWeb()`

For [HttpContext](/en-us/dotnet/api/system.web.httpcontext) to [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext)

* Implicit casting
* `HttpContext.AsAspNetCore()`

Both of these methods will use a cached [HttpContext](/en-us/dotnet/api/system.web.httpcontext) representation for the duration of a request. This allows for targeted rewrites to [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) as needed.

## Example

### ASP.NET Framework

Consider a controller that does something such as:

```
public class SomeController : Controller
{
  public ActionResult Index()
  {
    SomeOtherClass.SomeMethod(HttpContext.Current);
  }
}
```

which then has logic in a separate assembly passing that [HttpContext](/en-us/dotnet/api/system.web.httpcontext) around until finally, some inner method does some logic on it such as:

```
public class Class2
{
  public bool PerformSomeCheck(HttpContext context)
  {
    return context.Request.Headers["SomeHeader"] == "ExpectedValue";
  }
}
```

### ASP.NET Core

In order to run the above logic in ASP.NET Core, a developer will need to add the `Microsoft.AspNetCore.SystemWebAdapters` package, that will enable the projects to work on both platforms.

The libraries would need to be updated to understand the adapters, but it will be as simple as adding the package and recompiling. If these are the only dependencies a system has on `System.Web.dll`, then the libraries will be able to target [.NET Standard 2.0](/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) to facilitate a simpler building process while migrating.

The controller in ASP.NET Core will now look like this:

```
public class SomeController : Controller
{
  [Route("/")]
  public IActionResult Index()
  {
    SomeOtherClass.SomeMethod(HttpContext);
  }
}
```

Notice that since there's a [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.httpcontext#microsoft-aspnetcore-mvc-controllerbase-httpcontext) property, they can pass that through, but it generally looks the same. Using implicit conversions, the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) can be converted into the adapter that could then be passed around through the levels utilizing the code in the same way.

## Unit Testing

When unit testing code that uses the System.Web adapters, there are some special considerations to keep in mind.

In most cases, there's no need to set up additional components for running tests. But if the component being tested uses [HttpRuntime](/en-us/dotnet/api/system.web.httpruntime), it might be necessary to start up the `SystemWebAdapters` service, as shown in the following example:

```
namespace TestProject1;

/// <summary>
/// This demonstrates an xUnit feature that ensures all tests
/// in classes marked with this collection are run sequentially.
/// </summary>
[CollectionDefinition(nameof(SystemWebAdaptersHostedTests),
    DisableParallelization = true)]
public class SystemWebAdaptersHostedTests
{
}
[Collection(nameof(SystemWebAdaptersHostedTests))]
public class RuntimeTests
{
    /// <summary>
    /// This method starts up a host in the background that
    /// makes it possible to initialize <see cref="HttpRuntime"/>
    /// and <see cref="HostingEnvironment"/> with values needed 
    /// for testing with the <paramref name="configure"/> option.
    /// </summary>
    /// <param name="configure">
    /// Configuration for the hosting and runtime options.
    /// </param>
    public static async Task<IDisposable> EnableRuntimeAsync(
        Action<SystemWebAdaptersOptions>? configure = null,
        CancellationToken token = default)
        => await new HostBuilder()
           .ConfigureWebHost(webBuilder =>
           {
               webBuilder
                   .UseTestServer()
                   .ConfigureServices(services =>
                   {
                       services.AddSystemWebAdapters();
                       if (configure is not null)
                       {
                           services.AddOptions
                               <SystemWebAdaptersOptions>()
                               .Configure(configure);
                       }
                   })
                   .Configure(app =>
                   {
                       // No need to configure pipeline for tests
                   });
           })
           .StartAsync(token);
    [Fact]
    public async Task RuntimeEnabled()
    {
        using (await EnableRuntimeAsync(options =>
            options.AppDomainAppPath = "path"))
        {
            Assert.True(HostingEnvironment.IsHosted);
            Assert.Equal("path", HttpRuntime.AppDomainAppPath);
        }
        Assert.False(HostingEnvironment.IsHosted);
    }
}
```

The tests must be executed in sequence, not in parallel. The preceding example illustrates how to achieve this by setting [XUnit's `DisableParallelization` option](https://xunit.net/docs/running-tests-in-parallel#parallelism-in-test-frameworks) to `true`. This setting disables parallel execution for a specific test collection, ensuring that the tests within that collection run one after the other, without concurrency.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-23

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0025
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0025.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0025: Use AddAuthorizationBuilder to register authorization services and construct policies.

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0025      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

The use of [AddAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.policyservicecollectionextensions.addauthorization) can be converted to the new [AddAuthorizationBuilder](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.policyservicecollectionextensions.addauthorizationbuilder).

## Rule description

Use `AddAuthorizationBuilder` to register authorization services and construct policies.

## How to fix violations

To fix a violation of this rule, replace the usage of `AddAuthorization` with `AddAuthorizationBuilder`.

The code fix converts any usage of the setters for the following properties of [AuthorizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions):

* [DefaultPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.defaultpolicy#microsoft-aspnetcore-authorization-authorizationoptions-defaultpolicy)
* [FallbackPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.fallbackpolicy#microsoft-aspnetcore-authorization-authorizationoptions-fallbackpolicy)
* [InvokeHandlersAfterFailure](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.invokehandlersafterfailure#microsoft-aspnetcore-authorization-authorizationoptions-invokehandlersafterfailure)

These setter usages are converted to equivalent method calls on [AuthorizationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder):

* [SetDefaultPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder.setdefaultpolicy)
* [SetFallbackPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder.setfallbackpolicy)
* [SetInvokeHandlersAfterFailure](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder.setinvokehandlersafterfailure)

No diagnostic is reported when the configure action passed to `AddAuthorization` uses any of the following members of `AuthorizationOptions`:

* The [GetPolicy(String)](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.getpolicy#microsoft-aspnetcore-authorization-authorizationoptions-getpolicy\(system-string\)) method
* The [DefaultPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.defaultpolicy#microsoft-aspnetcore-authorization-authorizationoptions-defaultpolicy) getter
* The [FallbackPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.fallbackpolicy#microsoft-aspnetcore-authorization-authorizationoptions-fallbackpolicy) getter
* The [InvokeHandlersAfterFailure](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.invokehandlersafterfailure#microsoft-aspnetcore-authorization-authorizationoptions-invokehandlersafterfailure) getter

`AuthorizationBuilder` doesn't have equivalents for these members of `AuthorizationOptions`, so they can't be converted.

No diagnostic is reported if the configure action passed to `AddAuthorization` contains operations unrelated to `AuthorizationOptions`. The code fix would not be able to automatically map unrelated operations to the fluent API of `AddAuthorizationBuilder`.

The following example shows code that triggers this diagnostic:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

var app = builder.Build();

app.UseAuthorization();

app.Run();
```

The following example shows the result of applying the code fix:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorizationBuilder()
  .AddPolicy("AtLeast21", policy =>
  {
        policy.Requirements.Add(new MinimumAgeRequirement(21));
  });

var app = builder.Build();

app.UseAuthorization();

app.Run();
```

## When to suppress warnings

The severity level of this diagnostic is Information. Suppress warnings if you don't want to use the new syntax.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-11.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# What's new in ASP.NET Core in .NET 11

This article highlights the most significant changes in ASP.NET Core in .NET 11 with links to relevant documentation.

This article will be updated as new preview releases are made available.

## Blazor

This section describes new features for Blazor.

### New `DisplayName` component and support for `[Display]` and `[DisplayName]` attributes

The `DisplayName` component can be used to display property names from metadata attributes:

```
[Required, DisplayName("Production Date")]
public DateTime ProductionDate { get; set; }
```

The [`[Display]` attribute](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) on the model class property is supported:

```
[Required, Display(Name = "Production Date")]
public DateTime ProductionDate { get; set; }
```

Of the two approaches, the `[Display]` attribute is recommended, which makes additional properties available. The `[Display]` attribute also enables assigning a resource type for localization. When both attributes are present, `[Display]` takes precedence over `[DisplayName]`. If neither attribute is present, the component falls back to the property name.

Use the `DisplayName` component in labels or table headers:

```
<label>
    <DisplayName For="@(() => Model!.ProductionDate)" />
    <InputDate @bind-Value="Model!.ProductionDate" />
</label>
```

### Blazor Web script startup options format now supported for Blazor Server and Blazor WebAssembly scripts

The Blazor Web App script (`blazor.web.js`) options object passed to `Blazor.start()` uses the following format since the release of .NET 8:

```
Blazor.start({
  ssr: { ... },
  circuit: { ... },
  webAssembly: { ... },
});
```

Now, Blazor Server (`blazor.server.js`) and Blazor WebAssembly (`blazor.webassembly.js`) scripts can use the same options format.

The following example shows the prior options format, which remains supported:

```
Blazor.start({
  loadBootResource: function (...) {
      ...
    },
  });
```

The newly supported options format for the preceding example:

```
Blazor.start({
  webAssembly: {
    loadBootResource: function (...) {
      ...
    },
  },
});
```

For more information, see [ASP.NET Core Blazor startup](../blazor/fundamentals/startup?view=aspnetcore-11.0#startup-process-and-configuration).

### New `BasePath` component

Blazor Web Apps can use the new `BasePath` component (`<BasePath />`) to render the app's app base path (`<base href>`) HTML tag automatically. For more information, see [ASP.NET Core Blazor app base path](../blazor/host-and-deploy/app-base-path?view=aspnetcore-11.0).

### Inline JS event handler removed from the `NavMenu` component

The inline JS event handler that toggles the display of navigation links is no longer present in the `NavMenu` component of the Blazor Web App project template. Apps generated from the project template now use a [collocated JS module](../blazor/javascript-interoperability/location-of-javascript?view=aspnetcore-11.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) approach to show or hide the navigation bar on the rendered page. The new approach improves [Content Security Policy (CSP) compliance](../blazor/security/content-security-policy?view=aspnetcore-11.0) because it doesn't require the CSP to include an unsafe hash for the inline JS.

To migrate an existing app to .NET 11, including adopting the new JS module approach for the navigation bar toggler, see [Migrate from ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11](../migration/100-to-110?view=aspnetcore-10.0).

### `NavigateTo` and `NavLink` support for relative navigation

The new `RelativeToCurrentUri` parameter (default: `false`) for [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) and the [`NavLink` component](../blazor/fundamentals/routing?view=aspnetcore-11.0#navlink-component) allows you to navigate to URIs relative to the current page path rather than the app's base URI.

Consider the following nested endpoints:

* `/docs`

  * `/getting-started`

    * `/installation`
    * `/configuration`

When the browser's URI is `/docs/getting-started/installation` and you want to navigate the user to `/docs/getting-started/configuration`, `NavigateTo("/configuration")` redirects to `/configuration` at the app's root instead of the relative path at `/docs/getting-started/configuration`. Set the `RelativeToCurrentUri` with [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) or the [`NavLink` component](../blazor/fundamentals/routing?view=aspnetcore-11.0#navlink-component) for the desired navigation:

```
Navigation.NavigateTo("/configuration", new NavigationOptions
{
    RelativeToCurrentUri = true
});
```

```
<NavLink href="configuration" RelativeToCurrentUri="true">Configuration</NavLink>
```

### Persist temporary data between HTTP requests during static server-side rendering (static SSR)

To persist temporary data between HTTP requests during static server-side rendering (static SSR), Blazor supports TempData. TempData is ideal for scenarios such as flash messages after form submissions, passing data during redirects (POST-Redirect-GET pattern), and one-time notifications.

TempData is available when [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) is called in the app's `Program` file and is provided as a cascading value with the [`[CascadingParameter]` attribute](../blazor/components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingparameter-attribute).

```
[CascadingParameter]
public ITempData? TempData { get; set; }
```

When supplied to a parameter for simple read/write of a single value, use the `[SupplyParameterFromTempData]` attribute:

```
[SupplyParameterFromTempData]
public string? Message { get; set; }
```

For more information, see [ASP.NET Core Blazor server-side state management](../blazor/state-management/server?view=aspnetcore-11.0#temporary-data-persistence).

### New Blazor Web Worker template (`blazorwebworker`)

The .NET Web Worker project template, which contains a Web Worker client for offloading long-running work to a background thread, has been renamed to the **Blazor Web Worker** project template (`blazorwebworker`). The name change makes it clearer that the template is part of the Blazor stack for use in Blazor WebAssembly and Blazor Web apps (for client-side rendering, CSR).

Two often-requested capabilities have been added to the generated `WebWorkerClient`:

* `InvokeVoidAsync` for fire-and-forget worker calls that don't return a value, mirroring the shape on `IJSRuntime`.
* Cancellation and timeout support on both worker creation and worker invocations, so callers can pass a `CancellationToken` and tear down a stuck worker cleanly.

Existing projects created with the old template continue to work. The rename only affects the template name shown in `dotnet new list` and in Visual Studio's list of **Create a new project** templates.

For more information, see the following resources:

* [ASP.NET Core Blazor with .NET on Web Workers](../blazor/blazor-with-dotnet-on-web-workers?view=aspnetcore-11.0)
* [.NET Web Worker template update to Blazor Web Worker template (`dotnet/aspnetcore` #66070)](https://github.com/dotnet/aspnetcore/pull/66070) (Please don't comment on closed issues and PRs.)

### Virtualization enhancements

* The [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component no longer assumes every item has the same height. Previously, the component disabled the browser's native scroll anchoring (to avoid an infinite rendering loop), which meant any height change above the viewport—item expansion, data updates, lazy-loaded content—caused visible items to jump on screen. The `Virtualize` component now adapts to measured item sizes at runtime, which reduces incorrect spacing and scrolling when item heights vary.

  The updates use a hybrid approach: native CSS scroll anchoring on browsers that support it for non-`<table>` layouts with a manual `ResizeObserver`-based scroll-compensation fallback for `<table>` layouts and Safari, where native anchoring miscalculates positions on `<tr>` elements.

  Apps using the `Virtualize` component receive the benefits of these updates automatically. No developer API changes are required.

  These updates include an update to the default value of [Virtualize\<TItem>.OverscanCount](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.overscancount), which was `3` in .NET 10 or earlier and now changes to `15` in .NET 11 or later. The change in default value increases the precision of average item height calculations.

  For more information, see the following resources:

  * [*Item size* section](../blazor/components/virtualization?view=aspnetcore-11.0#item-size) and [*Overscan count* section](../blazor/components/virtualization?view=aspnetcore-11.0#overscan-count) of the *Virtualization* article.
  * [\[Virtualization\] Visible content does not shift when in-DOM items above the viewport change height (`dotnet/aspnetcore` #65951)](https://github.com/dotnet/aspnetcore/pull/65951) (Please don't comment on closed issues and PRs).

* Use the new `AnchorMode` parameter to control how the viewport behaves at list edges when items are dynamically added:

  * `None`: No edge pinning. The viewport stays at the current scroll position regardless of item changes.
  * `Start` (default): Pins the viewport to the beginning of the list. For example, this pinning behavior is useful for a news feed user experience.
  * `End`: Pins the viewport to the end of the list. For example, this pinning behavior is useful for a chat or logging user experience.

  In the following example, the virtualized content is pinned to the beginning of the list:

  ```
  <Virtualize AnchorMode="Start" ...>
      ...
  </Virtualize>
  ```

  For more information, see the following resources:

  * [ASP.NET Core Razor component virtualization](../blazor/components/virtualization?view=aspnetcore-11.0#control-viewport-scroll-position-behavior-when-items-are-dynamically-added)
  * [\[release/11.0-preview4\] Virtualization AnchorMode with variable-height support (`dotnet/aspnetcore` #66521)](https://github.com/dotnet/aspnetcore/pull/66521) (Please don't comment on closed issues and PRs.)

* Content Security Policy (CSP) compliance

  The `Virtualize` component renders dynamic inline `style` attributes on its spacer and placeholder elements (for example, `style="height: 478896px; flex-shrink: 0;"`) because spacer heights are calculated at runtime based on scroll position, item count, and average item size, which change on every scroll interaction. These are blocked by a Content Security Policy (CSP) when `style-src 'self'` is set, breaking virtualization entirely for apps with strict CSP policies.

  Now, CSP violations are avoided because `Virtualize` components:

  * Render calculated spacer and placeholder heights as numeric values in `data-blazor-virtualize-reserved-height` attributes.
  * When required, render the trailing spacer's vertical offset as a numeric value in a `data-blazor-virtualize-loop-breaker-transform` attribute to hide the spacer.

### New service defaults library project template for Blazor WebAssembly apps

The `blazor-wasm-servicedefaults` project template creates a service defaults library for Blazor WebAssembly apps with [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview) integration. For more information, see [Tooling for ASP.NET Core Blazor](../blazor/tooling?view=aspnetcore-11.0#service-defaults-library-for-blazor-webassembly-apps).

### New development server for Blazor WebAssembly apps

[`Microsoft.AspNetCore.Components.Gateway`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Gateway) is a lightweight ASP.NET Core host that replaces [`Microsoft.AspNetCore.Components.WebAssembly.DevServer`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.DevServer) for serving standalone Blazor WebAssembly apps during development and production.

To adopt the Gateway in an existing standalone Blazor WebAssembly app, reference the `Microsoft.AspNetCore.Components.Gateway` package in the app's project file.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Custom routing code and middleware aren't required by the app. Fallback endpoints come from the static web assets manifest the SDK emits when the `StaticWebAssetSpaFallbackEnabled` property is set in the app's project file, which is present by default in standalone Blazor WebAssembly apps created from the project template:

```
<StaticWebAssetSpaFallbackEnabled>true</StaticWebAssetSpaFallbackEnabled>
```

Prior to the release of .NET 11, the `inspectUri` property of the `Properties/launchSettings.json` file:

* Enables the IDE to detect that the app is a Blazor app.
* Instructs the script debugging infrastructure to connect to the browser through Blazor's debugging proxy.

The property is no longer required when using the new development server.

Open the `Properties/launchSettings.json` file of the startup project. Remove the `inspectUri` property in each launch profile of the file's `profiles` node:

```
- "inspectUri": "..."
```

For more information, see [\[Blazor\] Replace DevServer with BlazorGateway for standalone WASM apps (`dotnet/aspnetcore` #65982)](https://github.com/dotnet/aspnetcore/pull/65982) (Please don't comment on closed issues and PRs).

### Server-triggered circuit pause

*This feature applies to server-side Blazor apps.*

Blazor already supports graceful circuit pause and resume with [`Blazor.pauseCircuit()` and `Blazor.resumeCircuit()`](../blazor/state-management/server?view=aspnetcore-10.0#pause-and-resume-circuits). .NET 11 introduces a symmetric server-side pause and resume capability, where the server can request that connected clients begin the graceful circuit-pause flow.

`Circuit.RequestCircuitPauseAsync(CancellationToken)` is used to request that the connected client begin the graceful circuit-pause flow. The `CancellationToken` cancels the request before it is accepted by the framework. The method returns `true` if the request was accepted and the client was asked to begin pausing.

This feature is useful in the following scenarios:

* Planned shutdowns and deployments.
* Instance draining.
* App maintenance windows.

For more information and an implementation example for server restarts, see [ASP.NET Core Blazor server-side state management](../blazor/state-management/server?view=aspnetcore-11.0#server-triggered-circuit-pause).

### Smaller Blazor WebAssembly publish output

Two trimming changes shrink published Blazor WebAssembly apps that don't use [OpenTelemetry (OTEL)](https://opentelemetry.io/) or Hot Reload:

* The `ComponentsMetrics` and `ComponentsActivitySource` types are now gated behind a `[FeatureSwitchDefinition]` attribute, so the trimmer can drop the metrics and tracing call paths from `Renderer` and friends when `System.Diagnostics.Metrics.Meter.IsSupported` is `false` (the default for trimmed apps) \[[browser\]\[wasm\] Implement IL trimming for OTEL (`dotnet/aspnetcore` #65901)](https://github.com/dotnet/aspnetcore/pull/65901) (Please don't comment on closed issues and PRs).
* `HotReloadManager` now exposes a feature-switched `IsSupported` property tied to `System.Reflection.Metadata.MetadataUpdater.IsSupported`, so the trimmer can eliminate hot-reload caches and metadata-update handler registrations across the renderer when published \[[blazor\]\[wasm\] Fix hot reload IL trimming (`dotnet/aspnetcore` #65903)](https://github.com/dotnet/aspnetcore/pull/65903) (Please don't comment on closed issues and PRs).

Apps that use OTEL or Hot Reload aren't affected by the preceding updates.

### `QuickGrid` improvements

The [`QuickGrid` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid) receives several new features in .NET 11.

For more information on the following features, see [ASP.NET Core Blazor \`QuickGrid\` component](../blazor/components/quickgrid?view=aspnetcore-11.0).

#### Pagination modes

Prior to the release of .NET 11, pagination and sort state is managed in memory inside the `QuickGrid` component without changing the URL, called *inner-state navigation*. An interactive render mode is required.

With the release of .NET 11, `QuickGrid` supports *URL-based navigation*.

Pagination and sort state is persisted in the URL query string. When users paginate or sort, the URL updates (example: `?page=2&sort=Name&direction=asc`). This enables link sharing, browser back/forward, and static SSR without interactivity.

Sortable column headers and paginator controls render as `<a>` elements with `href` attributes. The `StaticHtmlRenderer` renders these anchors. On each request, the server reads the query string to determine current page and sort state—no JavaScript runtime required.

Query string parameters:

* `page`: One-based page number. The first page omits the parameter for clean URLs.
* `sort`: Column title for sorting the grid.
* `direction`: Ascending (`asc`) or descending (`desc`).

The `sort` column is identified by the column's `Title` property. Columns without a `Title` render a non-clickable `<div>` header.

`QuickGrid` reads the URL on initialization and subscribes to `NavigationManager.LocationChanged`, so browser back/forward and direct URL entry work. When sort parameters are removed from the URL, it falls back to the default sort column/direction.

Disabled paginator links use `aria-disabled="true"` and `pointer-events: none` instead of the HTML `disabled` attribute, which doesn't exist on `<a>` elements.

#### Query parameter names

The new `QueryParameterNameOptions` parameter of the `QuickGrid` component controls the names of the query string parameters that persist grid state in the URL. The `QueryParameterNameOptions` class has three settable properties:

* `Sort`: Name of the query string parameter that holds the sort column. The default value is `sort`.
* `Direction`: Name of the query string parameter that holds the sort direction. The default value is `direction`.
* `Page`: Name of the query string parameter that holds the page number. The default value is `page`.

The constructor takes an optional prefix argument that's prepended to all three default names. The prefix must include any separator character that you want to appear between the prefix and the name. In the following example, the query string parameters are named `products_sort`, `products_direction`, and `products_page`:

```
@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid ... 
    QueryParameterNameOptions="@(new QueryParameterNameOptions("products_"))">
    ...
</QuickGrid>
```

To control the names individually, set the properties of the class. Properties set explicitly take precedence over a prefix passed to the constructor, so the two approaches can be combined:

```
@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid ... QueryParameterNameOptions="@queryParameterNames">
    ...
</QuickGrid>

@code {
    private QueryParameterNameOptions queryParameterNames = new()
    {
        Sort = "orderBy",
        Direction = "orderDir",
        Page = "p"
    };
}
```

#### Multiple grids on the same page

Multiple `QuickGrid` components on the same page require unique query parameter names to avoid query string conflicts. Assign a `QueryParameterNameOptions` parameter to all but one of the grids.

Each `QuickGrid` must have its own `PaginationState` instance. Multiple grids must not share a `PaginationState` if they use different query parameter names—the last grid to render overwrites the query parameter name on the shared state, causing the `Paginator` to read from the wrong parameter.

In releases prior to .NET 11, the following `QuickGrid` components worked implicitly:

```
<QuickGrid ... Pagination="@pagination1">
    ...
</QuickGrid>

<QuickGrid ... Pagination="@pagination2">
    ...
</QuickGrid>
```

With the release of .NET 11, the following `QuickGrid` components require unique query parameter names. The first `QuickGrid` uses the default names, while the second one uses a `cities_` prefix:

```
<QuickGrid ... Pagination="@pagination1">
    ...
</QuickGrid>

<QuickGrid ... Pagination="@pagination2" 
    QueryParameterNameOptions="@(new QueryParameterNameOptions("cities_"))">
    ...
</QuickGrid>
```

Example query string for the preceding `QuickGrid` components:

```
?page=2&sort=Name&direction=asc&cities_page=3&cities_sort=Population&cities_direction=desc
```

#### Sort by column

Add `Sortable="true"` to a `PropertyColumn`. With URL-based navigation, selecting a header navigates to a URL with updated `sort` and `direction` parameters. With inner-state navigation, selecting a header triggers `@onclick`, which calls `SortByColumnAsync`. In both cases, `SortByColumnAsync` navigates via `NavigationManager.NavigateTo(GetSortQueryStringUrl(...))`, so the URL always reflects the sort state.

#### Title-based sort identification

Sort state in the URL uses the column's `Title` property as the identifier. The `sort` query parameter is set to `column.Title` (example for column title `Name`: `?sort=Name&direction=asc`). On a URL change, `QuickGrid` matches the `sort` value back to a column by executing `_columns.FirstOrDefault(c => c.Title == sort.ColumnTitle)`. If no column title matches, the sort is ignored and the grid falls back to its default sort.

Renaming a column's `Title` is a URL-breaking change. Any bookmarked or shared URLs containing the old title in the `sort` parameter stop matching, and the grid silently falls back to the default sort instead of sorting by the intended column. For `PropertyColumn`, the `Title` defaults to the property name (example: `Property="@(p => p.FirstName)"` produces `Title="First Name"`), so renaming the property or explicitly changing the `Title` parameter both break existing URLs.

#### Paginator

`Paginator` injects `NavigationManager`, subscribes to `LocationChanged`, and reads the page index from the query string on every location change. `GoToPageAsync` navigates to the target URL rather than directly mutating `PaginationState`. State is updated through the `LocationChanged` callback flow.

`GetPageUrl` returns a URL with the one-based page number. Page index 0 (page 1) omits the query parameter entirely.

#### CSS breaking change

When URL-based navigation is enabled, selectors targeting `button.col-title` must also target `a.col-title`, and `nav button`/`nav button:disabled` require `nav a`/`nav a[aria-disabled="true"]`. The built-in QuickGrid stylesheet provides both by default.

#### How to disable URL-based navigation

To disable URL-based navigation, set the `AppContext` switch for the feature to `false`:

```
AppContext.SetSwitch(
    "Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting",
    false);
```

This restores `<button>` elements with `@onclick` handlers. An interactive render mode is required.

The switch only controls the rendered HTML element (`<a>` versus `<button>`). Even when disabled, `QuickGrid` still reads and writes state to the URL query string internally. `SortByColumnAsync` and `Paginator.GoToPageAsync` navigate via `NavigationManager.NavigateTo` regardless of the flag.

#### Row click event (`OnRowClick`)

The `QuickGrid` component now supports row click events through the new [OnRowClick](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.onrowclick) parameter. When set, the grid automatically applies appropriate styling (cursor pointer) and invokes the callback with the clicked item:

```
@using Microsoft.AspNetCore.Components.QuickGrid
@inject NavigationManager NavigationManager

<QuickGrid Items="@people.AsQueryable()" 
    OnRowClick="@((Person args) => HandleRowClick(args))">
    <PropertyColumn Property="@(p => p.Name)" />
    <PropertyColumn Property="@(p => p.Email)" />
</QuickGrid>

@code {
    private List<Person> people = new()
    {
        new(1, "Alice Smith", "alice@example.com", "Engineering"),
        new(2, "Bob Johnson", "bob@example.com", "Marketing"),
        new(3, "Carol Williams", "carol@example.com", "Engineering"),
    };

    private void HandleRowClick(Person person)
    {
        NavigationManager.NavigateTo($"/person/{person.Id}");
    }

    private record Person(int Id, string Name, string Email, string Department);
}
```

The feature includes built-in CSS styling that applies a pointer cursor to clickable rows through the row-clickable CSS class, providing clear visual feedback to users.

### Client-side prerendering in a Blazor Web App preserves the server's culture

By default, client-side prerendering on the server (`.Client` project in a Blazor Web App) persists the server's [CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture) and [CurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentuiculture#system-globalization-cultureinfo-currentuiculture) into component state and applies them on the client before satellite assemblies load.

Apps that require the client to choose a culture independently of the server can opt out with `WebAssemblyComponentsOptions.UseCultureFromServer` in the Blazor Web App's `Program` file:

```
builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents(options =>
    {
        options.UseCultureFromServer = false;
    });
```

### Persist session data between HTTP requests during static server-side rendering (static SSR)

Session data persistence reads and writes cookie-based HTTP session values during static server-side rendering (static SSR), which is useful for scenarios such as shopping cart IDs or multi-step form progress. Unlike [temporary data persistence (`ITempData`)](#persist-temporary-data-between-http-requests-during-static-server-side-rendering-static-ssr), session values aren't cleared after reading. Values persist across multiple requests for the session lifetime.

Session storage configuration requires adding services by calling `AddSession` and request pipeline configuration with `UseSession`:

```
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession();
builder.Services.AddRazorComponents();

var app = builder.Build();

app.UseSession();
```

When supplied to a parameter, use the `[SupplyParameterFromSession]` attribute without or with a key (string):

```
[SupplyParameterFromSession]
public string? Message { get; set; }

[SupplyParameterFromSession(Name = "flash_message")]
public string? FlashMessage { get; set; }
```

For more information, see [ASP.NET Core Blazor server-side state management](../blazor/state-management/server?view=aspnetcore-11.0#session-data-persistence).

### `GetUriWithFragment` extension method

A new `GetUriWithFragment` extension method permits `NavigationManager` to easily construct URIs with hash fragments. This helper method provides an efficient, zero-allocation way to append hash fragments to the current URI. The following example demonstrates two use cases:

* Inline call that jumps to Section 1 (`id="section-1"`) of the rendered page.
* Method call that receives a section Id (`sectionId`) and navigates to the section of the page.

```
@inject NavigationManager Navigation

<a href="@Navigation.GetUriWithFragment("section-1")">
    Jump to Section 1
</a>

@code {
    private void NavigateToSection(string sectionId)
    {
        var uri = Navigation.GetUriWithFragment(sectionId);
        Navigation.NavigateTo(uri);
    }
}
```

The method uses `string.Create` for optimal performance and works correctly with non-root base URIs (for example, when using `<base href="/app/">`).

### `EnvironmentView` component

Blazor now includes a built-in `EnvironmentView` component for conditional rendering based on the hosting environment. This component provides a consistent way to render content based on the current environment across both server-side and client-side hosting models.

The `EnvironmentView` component accepts `Include` and `Exclude` parameters for specifying environment names. The component performs case-insensitive matching and follows the same semantics as MVC's `EnvironmentTagHelper`.

```
@using Microsoft.AspNetCore.Components.Web

<EnvironmentView Include="Development">
    <div class="alert alert-warning">
        Debug mode enabled
    </div>
</EnvironmentView>

<EnvironmentView Include="Development,Staging">
    <p>Pre-production environment</p>
</EnvironmentView>

<EnvironmentView Exclude="Production">
    <p>@DateTime.Now</p>
</EnvironmentView>
```

### MathML namespace support

Blazor now supports MathML elements in interactive rendering. MathML elements, such as `<math>`, `<mrow>`, `<mi>`, and `<mn>`, are created with the correct namespace (<http://www.w3.org/1998/Math/MathML>) using `document.createElementNS()`, similar to how SVG elements are handled:

```
<math>
    <mrow>
        <mi>x</mi>
        <mo>=</mo>
        <mfrac>
            <mrow>
                <mo>−</mo>
                <mi>b</mi>
                <mo>±</mo>
                <msqrt>
                    <mrow>
                        <msup><mi>b</mi><mn>2</mn></msup>
                        <mo>−</mo>
                        <mn>4</mn>
                        <mi>a</mi>
                        <mi>c</mi>
                    </mrow>
                </msqrt>
            </mrow>
            <mrow>
                <mn>2</mn>
                <mi>a</mi>
            </mrow>
        </mfrac>
    </mrow>
</math>
```

This fix ensures that MathML content renders correctly in browsers when added dynamically through Blazor's renderer, resolving issues where MathML elements were previously created as regular HTML elements without the proper namespace.

### `InvokeVoidAsync()` analyzer

A new Blazor analyzer (BL0010) has been added that recommends using `InvokeVoidAsync` instead of `InvokeAsync<object>` when calling JavaScript functions that don't return values. This analyzer helps developers write more efficient JSInterop code.

**Problematic code:**

```
// ⚠️ BL0010: Use InvokeVoidAsync for JavaScript functions that don't return a value
await JSRuntime.InvokeAsync<object>("console.log", "Hello");
```

**Recommended code:**

```
// ✅ Correct: Use InvokeVoidAsync
await JSRuntime.InvokeVoidAsync("console.log", "Hello");
```

The analyzer helps catch performance issues where `InvokeAsync` is unnecessarily used with `object` or ignored return values, guiding developers toward the more appropriate `InvokeVoidAsync` method.

### `IComponentPropertyActivator`

Blazor now provides `IComponentPropertyActivator` for customizing how `[Inject]` properties are populated on components. This enables advanced scenarios such as:

* Providing additional context for property resolution.
* Support for custom DI containers that need to intercept property injection.
* Advanced scenarios requiring property injection customization.

```
public interface IComponentPropertyActivator
{
    Action<IServiceProvider, IComponent> GetActivator(
        [DynamicallyAccessedMembers(Component)] Type componentType);
}
```

The default implementation caches activators per component type, supports keyed services via `[Inject(Key = "...")]`, integrates with Hot Reload for cache invalidation, and includes proper trimming annotations for AOT compatibility.

### SignalR `ConfigureConnection` for Interactive Server components

Blazor now provides access to configure the underlying SignalR connection options when using Interactive Server components through the new `ConfigureConnection` property on `ServerComponentsEndpointOptions`. This enables configuration of `HttpConnectionDispatcherOptions` properties that were previously only accessible through workarounds.

```
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(options =>
    {
        options.ConfigureConnection = dispatcherOptions =>
        {
            dispatcherOptions.CloseOnAuthenticationExpiration = true;
            dispatcherOptions.AllowStatefulReconnects = true;
            dispatcherOptions.ApplicationMaxBufferSize = 1024 * 1024;
        };
    });
```

This provides a clean, type-safe API for configuring SignalR connection settings without needing to inspect endpoint metadata.

### `IHostedService` support in Blazor WebAssembly

Blazor WebAssembly now supports `IHostedService` for running background services in the browser. This brings feature parity with Blazor Server and enables scenarios like periodic data refresh, real-time updates, and background processing.

```
public class DataRefreshService : IHostedService
{
    private Timer? _timer;
    
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _timer = new Timer(RefreshData, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
        return Task.CompletedTask;
    }

    private void RefreshData(object? state)
    {
        // Refresh data periodically
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Dispose();
        return Task.CompletedTask;
    }
}

// Registration
builder.Services.AddHostedService<DataRefreshService>();
```

Hosted services are started when the app starts and stopped when it shuts down, providing a clean lifecycle for background operations in Blazor WebAssembly apps.

### Configure Blazor client behavior from the server

Blazor apps can now configure client-side startup behavior from the server in C# when mapping Razor components instead of hand-writing `Blazor.start` JavaScript. `WithBrowserOptions` sets options that the server serializes into the rendered page and the Blazor script applies in the browser, across the Server, WebAssembly, and Auto render modes. The options cover the client log level, interactive Server reconnection, whether enhanced navigation preserves the DOM, and a WebAssembly runtime's environment name, culture, and environment variables:

```
app.MapRazorComponents<App>()
    .WithBrowserOptions(options =>
    {
        options.InteractiveServer.ReconnectionDialogId = "reconnect-dialog";
        options.InteractiveServer.ReconnectionMaxRetries = 10;
        options.InteractiveServer.ReconnectionRetryInterval = TimeSpan.FromSeconds(1.5);
        options.InteractiveWebAssembly.EnvironmentVariables["OTEL_EXPORTER_OTLP_ENDPOINT"] =
            "https://localhost:4318";
        options.StaticServer.CircuitInactivityTimeout = TimeSpan.FromSeconds(1.5);
        options.StaticServer.PreserveDom = true;
        options.InteractiveWebAssembly.ApplicationCulture = "en-ca";
        options.InteractiveWebAssembly.EnvironmentName = "Staging";
        options.LogLevel = LogLevel.Warning;
    });
```

You can also set the options in a Razor component with the `ConfigureBrowser` component:

```
<ConfigureBrowser Options="RequestBrowserOptions" />

...
    
@code {
    private BrowserOptions RequestBrowserOptions => new()
    {
        LogLevel = LogLevel.Trace,
        StaticServer = { PreserveDom = true }
    };
}
```

Read the resolved options from `HttpContext` with `GetBrowserOptions()`:

```
@BrowserOptions.GetBrowserOptions(HttpContext).LogLevel
```

For more information, see the following resources:

* [API Proposal: BrowserOptions for server-to-client configuration (`dotnet/aspnetcore` #66393)](https://github.com/dotnet/aspnetcore/issues/66393)
* [Reshape BrowserConfiguration API per review (BrowserOptions) while preserving the JS wire format (`dotnet/aspnetcore` #67337)](https://github.com/dotnet/aspnetcore/pull/67337)
* [Reshape BrowserOptions server-to-client configuration API per review (`dotnet/aspnetcore` #67918)](https://github.com/dotnet/aspnetcore/pull/67918)

Please don't comment on closed issues and PRs. Open a new issue to provide feedback on this API.

### Environment variables in Blazor WebAssembly configuration

Blazor WebAssembly applications can now access environment variables through `IConfiguration`. This enables runtime configuration without rebuilding the application, making it easier to deploy the same build to different environments.

In the following example, the `API_ENDPOINT` and `ENABLE_FEATURE_X` environment variables are automatically included in configuration:

```
var builder = WebAssemblyHostBuilder.CreateDefault(args);

var apiEndpoint = builder.Configuration["API_ENDPOINT"];
var featureFlag = builder.Configuration["ENABLE_FEATURE_X"];
```

Environment variables are loaded into the configuration system alongside other configuration sources, such as app settings (`appsettings.json`), providing a unified way to access configuration values regardless of their source.

### Blazor WebAssembly component metrics and tracing

Blazor WebAssembly apps now provide component specific metrics and tracing when support for metrics has been enabled in the runtime.

### Enable container support in Blazor Web App template

The Blazor Web App project template now supports the **Enable container support** option in Visual Studio. This makes it easier to containerize Blazor Web Apps and deploy them to container orchestration platforms, such as Kubernetes or Azure Container Apps.

### Static SSR supports client-side validation

Blazor static server-side rendering (static SSR) forms now get instant, in-browser validation feedback without a server round-trip, matching the experience provided by interactive Blazor apps and MVC apps with unobtrusive validation. The .NET model remains the single source of truth for validation rules. The server renders metadata for the validation rules which are then enforced by the Blazor JS code on the client-side.

The feature is enabled by default for all static SSR forms that include the `DataAnnotationsValidator` component. Both enhanced and non-enhanced forms are supported.

Complete feature coverage is available in [ASP.NET Core Blazor client-side form validation in static SSR](../blazor/forms/validation-client-side?view=aspnetcore-11.0).

For more information, see the following resources:

* [Add .NET support for client-side validation in Blazor SSR (`dotnet/aspnetcore` #66441)](https://github.com/dotnet/aspnetcore/pull/66441)
* [Add JS library for client-side validation in Blazor SSR (`dotnet/aspnetcore` #66420)](https://github.com/dotnet/aspnetcore/pull/66420)

Please don't comment on closed issues and PRs. If you have feedback on this feature, please open a new issue on the `dotnet/aspnetcore` GitHub repository.

### Asynchronous form validation support

Blazor forms receive support for async validation rules, such as database lookups or remote API calls. In any rendering mode, `EditForm` submit validation awaits async validators end-to-end.

The built-in `DataAnnotationsValidator` component runs the asynchronous `DataAnnotations` APIs (`AsyncValidationAttribute` and `IAsyncValidatableObject`), so asynchronous rules declared on the model work without additional configuration.

Validator components register asynchronous work with `ValidationRequestedEventArgs.AddAsyncValidator` for the whole form and `EditContext.RegisterAsyncFieldValidator` for a single field. The framework owns the cancellation token source, cancels superseded validations, and exposes progress with `IsValidationPending(field)` and `IsValidationFaulted(field)`.

```
<EditForm EditContext="editContext" OnValidSubmit="HandleSubmit">
    <InputText @bind-Value="model.Username" />
    @if (editContext.IsValidationPending(() => model.Username))
    {
        <span>Checking availability...</span>
    }
    <ValidationMessage For="() => model.Username" />
    <button type="submit">Register</button>
</EditForm>

@code {
    [Inject] public UserService Users { get; set; } = default!;

    private readonly RegistrationModel model = new();
    private EditContext editContext = default!;
    private ValidationMessageStore messages = default!;

    protected override void OnInitialized()
    {
        editContext = new EditContext(model);
        messages = new ValidationMessageStore(editContext);
        editContext.OnFieldChanged += (_, e) =>
        {
            if (e.FieldIdentifier.FieldName == nameof(model.Username))
            {
                editContext.RegisterAsyncFieldValidator(e.FieldIdentifier,
                    token => CheckAsync(e.FieldIdentifier, model.Username, token));
            }
        };
    }

    private async Task CheckAsync(FieldIdentifier field, string value, CancellationToken ct)
    {
        messages.Clear(field);
        if (await Users.IsUsernameTakenAsync(value, ct))
        {
            messages.Add(field, "Username is taken.");
        }
        editContext.NotifyValidationStateChanged();
    }

    private Task HandleSubmit() => RegisterAsync();
}
```

Complete feature coverage is available in [ASP.NET Core Blazor advanced form validation](../blazor/forms/validation-advanced?view=aspnetcore-11.0#asynchronous-validation).

For more information, see [Add built-in support for async form validation in Blazor (`dotnet/aspnetcore` #66526)](https://github.com/dotnet/aspnetcore/pull/66526).

Please don't comment on closed issues and PRs. If you have feedback on this feature, please open a new issue on the `dotnet/aspnetcore` GitHub repository.

### Fixes to TempData and `[SupplyParameterFromSession]` persistence for streaming SSR

When a page uses session-backed features, where a component has a `[SupplyParameterFromSession]` parameter (which creates a subscription) or the session-storage TempData provider is active, the session cookie (`.AspNetCore.Session`) is now issued before streaming begins, even if no value is ultimately written. Pages that don't use session-backed features are unaffected.

For more information, see [Fix TempData and SupplyParameterFromSession persistence for streaming SSR case (`dotnet/aspnetcore` #66832)](https://github.com/dotnet/aspnetcore/pull/66832). (Please don't comment on closed issues and PRs.)

### Antiforgery middleware (`app.UseAntiforgery()`) optional in Blazor Web Apps

[CSRF protection](../security/anti-request-forgery?view=aspnetcore-10.0#automatic-csrf-protection-in-aspnet-core) is enabled by default via the auto-injected CSRF protection middleware, so an explicit `app.UseAntiforgery()` call in a Blazor Web App is usually unnecessary and should only be added for specific use cases. The call no longer appears in apps created from the Blazor Web App project template.

For more information, see the following resources:

* [Migrate from ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11](../migration/100-to-110?view=aspnetcore-10.0)
* [Blazor server-side rendering defers antiforgery validation to middleware (breaking change announcement)](/en-us/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection)

General coverage for the new automatic CSRF protection in ASP.NET Core:

* [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-11.0)
* [ASP.NET Core Blazor forms overview](../blazor/forms/?view=aspnetcore-11.0#antiforgery-support)
* [ASP.NET Core Blazor authentication and authorization](../blazor/security/?view=aspnetcore-11.0#antiforgery-support)

### Blazor Virtualize can scroll to an item

The `Virtualize<TItem>` component can now open at a specific item and scroll to any item on demand. Two new public APIs make this possible:

* `InitialItemIndex` positions the list at a given item on the first interactive render, so the list opens at that item without a flash of the first item.
* `ScrollToItemAsync(int itemIndex, CancellationToken cancellationToken = default)` scrolls to an item at any time after the first render and returns a `Task` that completes when the target is aligned to the top of the viewport.

```
<Virtualize TItem="Product" Items="products" InitialItemIndex="500" @ref="list">
    <div class="product">@context.Name</div>
</Virtualize>

<button @onclick="GoToTop">Back to top</button>

@code {
    private Virtualize<Product> list = default!;
    private List<Product> products = ProductCatalog.All;

    private async Task GoToTop() => await list.ScrollToItemAsync(0);
}
```

Out-of-range indexes are clamped to the valid range. If a second `ScrollToItemAsync` call starts while one is still in flight, the last call wins. Calling `ScrollToItemAsync` before the first interactive render throws `InvalidOperationException`; use `InitialItemIndex` to set the starting position instead.

For more information, see the following resources:

* [ASP.NET Core Razor component virtualization](../blazor/components/virtualization?view=aspnetcore-10.0#scroll-to-a-specific-item)
* [Add `InitialIndex` (sic†) parameter and `ScrollToIndexAsync` (sic†) API to `Virtualize<TItem>` (`dotnet/aspnetcore` #66753)](https://github.com/dotnet/aspnetcore/pull/66753). (Please don't comment on closed issues and PRs. *sic*†: API naming was changed without updating the title of the PR.)

### Automatic circuit pause on tab inactivity

Auto-pause can pause a circuit when the browser tab becomes hidden, freeing server memory and SignalR connections held by inactive users. It's an opt-in feature provided by the `Microsoft.AspNetCore.Components.Server.AutoPause` package. After adding a package reference, enable the feature by calling `AddAutoPause` when the app's root component is mapped:

```
app.MapRazorComponents<App>()
    .WithBrowserOptions(options => options.AddAutoPause(p => p.HiddenDelay = TimeSpan.FromSeconds(30)));
```

After the tab is hidden for a configurable delay period (default: 2 minutes), the circuit pauses. If the user returns before the delay elapses, the pause doesn't occur.

For more information, see [ASP.NET Core Blazor server-side state management](../blazor/state-management/server?view=aspnetcore-11.0#automatic-circuit-pause-on-tab-inactivity).

### Wider support for `AuthorizationPolicy` and `IAuthorizationRequirementData`

Starting in .NET 11, you can apply [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) attributes to SignalR hubs and hub methods, MVC controllers and actions, and Blazor's [`AuthorizeView`](../blazor/security/?view=aspnetcore-10.0#authorizeview-component) and [`AuthorizeRouteView`](../blazor/security/authentication-state?view=aspnetcore-10.0\&pivots=server#implement-a-custom-authenticationstateprovider) components, not just to endpoints. For apps that target releases earlier than .NET 11, these attributes are only enforced on Minimal API and routed endpoints.

For more information, see [Custom authorization policies with \`IAuthorizationRequirementData\`](../security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata?view=aspnetcore-11.0).

### QuickGrid APIs from Virtualize are exposed

The following new [`QuickGrid` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid) APIs are exposed when a grid is virtualized ([Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize) set to `true`):

* `InitialItemIndex`: Scrolls the grid to the given zero-based row index on the first interactive render. The value is applied once and clamped to the valid range. This forwards to the inner `Virtualize` component. For more information, see [ASP.NET Core Razor component virtualization](../blazor/components/virtualization?view=aspnetcore-11.0#scroll-to-a-specific-item).
* `ScrollToItemAsync`: Programmatically scrolls the grid to the given zero-based row index, aligning it to the top. The last call wins, and the method throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) when virtualization is disabled or the grid isn't rendered yet. This forwards to the inner `Virtualize` component. For more information, see [ASP.NET Core Razor component virtualization](../blazor/components/virtualization?view=aspnetcore-11.0#scroll-to-a-specific-item).
* `AnchorMode`: Controls how the viewport behaves at list edges when items are dynamically added (default: `Start`). This is an experimental API that requires opting in to the `ASP0030` diagnostic, and it forwards to the inner `Virtualize` component. For more information, see [ASP.NET Core Razor component virtualization](../blazor/components/virtualization?view=aspnetcore-11.0#control-viewport-scroll-position-behavior-when-items-are-dynamically-added).
* `ItemComparer`: A comparer used to detect whether items were prepended or appended between data loads, which is useful for class-typed items supplied by an [ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsprovider). This is an experimental API that requires opting in to the `ASP0030` diagnostic, and it forwards to the inner `Virtualize` component. For more information, see [ASP.NET Core Razor component virtualization](../blazor/components/virtualization?view=aspnetcore-11.0#control-viewport-scroll-position-behavior-when-items-are-dynamically-added).

For more information, see [ASP.NET Core Blazor \`QuickGrid\` component](../blazor/components/quickgrid?view=aspnetcore-11.0#quickgrid-implementation).

### Cache rendered output of a component subtree during static SSR

The new `CacheView` component caches the rendered output of a Razor component subtree during static server-side rendering (static SSR). On a cache hit, cached markup is replayed without instantiating or running the lifecycle of the child components that were included in the cached output.

`CacheView` is useful for expensive, mostly static sections of a page that don't require the entire response to be cached:

```
<CacheView VaryByQuery="category" ExpiresAfter="TimeSpan.FromMinutes(5)">
    <ProductList Category="@Category" />
</CacheView>
```

For more information, see [ASP.NET Core Blazor CacheView component](../blazor/state-management/cacheview-component?view=aspnetcore-11.0).

### Blazor Server circuits update after authentication refresh

Interactive Server components can now receive the refreshed `ClaimsPrincipal` without reconnecting the circuit. The Blazor component hub and client enable authentication refresh automatically, so no additional configuration is required.

After the connection refreshes its authentication, Blazor updates the authentication state and raises `AuthenticationStateChanged`. Components that consume `AuthenticationStateProvider`, including `AuthorizeView`, rerender using the refreshed identity and claims. This behavior is useful when a user's roles or permissions change during an active circuit or when a component should reload user-specific content after claims are refreshed. The UI can reflect the new authentication state without forcing the user to reconnect or reload the page.

For more information, see the following resources:

* [\[Blazor\] Propagate SignalR authentication refresh to server circuits (`dotnet/aspnetcore` #68221)](https://github.com/dotnet/aspnetcore/pull/68221)
* [\[release/11.0-rc1\] Harden SignalR authentication refresh (`dotnet/aspnetcore` #68593)](https://github.com/dotnet/aspnetcore/pull/68593)

Please don't comment on closed issues and PRs. If you have feedback on this feature, please open a new issue on the `dotnet/aspnetcore` GitHub repository.

### Experimental Blazor AI components for agentic user interfaces

Modern AI apps increasingly provide rich interactions with agents. A complete agentic user interface may need to stream ongoing work, visualize agent reasoning and progress, request approval before tools act, accept multimodal input, and synchronize state between the app and the agent. The Blazor AI components are designed to provide building blocks for creating these experiences using Blazor's component model.

The new [`Microsoft.AspNetCore.Components.AI` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.AI) includes an initial set of Blazor AI components for streaming chat, rich-text and tool rendering, human approval flows, and typed, shared, and predictive UI state.

#### Get started

Important

The `Microsoft.AspNetCore.Components.AI` package is a prerelease, experimental package throughout .NET 11.

Add the package to a Blazor app:

```
dotnet add package Microsoft.AspNetCore.Components.AI --prerelease
```

Basic chat and the Components.AI block model work with any `IChatClient`. To connect the Blazor app to a remote agent over the [Agent User Interaction Protocol (AG-UI)](https://ag-ui.com), install the `AGUI.Client` package:

```
dotnet add package AGUI.Client
```

`AGUI.Client` includes a transitive reference to `AGUI.Abstractions`, which provides the AG-UI event types used in later examples. Register an [`AGUIChatClient`](https://docs.ag-ui.com/sdk/dotnet/client/chat-client) as the app's `IChatClient`:

```
using AGUI.Client;
using Microsoft.Extensions.AI;

builder.Services.AddHttpClient<IChatClient>(httpClient =>
    new AGUIChatClient(new(httpClient, "https://api.example.com/agent")));
```

`AGUIChatClient` streams AG-UI events as `ChatResponseUpdate` values. The Blazor AI components render the conversational content from these updates, while apps can use the additional AG-UI event information to build richer agentic interactions. While basic chat functionality is supported with any `IChatClient`, AG-UI is required when a remote server and the Blazor client must exchange frontend tool declarations, backend tool events, approval interrupts, shared-state events, or AG-UI conversation identifiers.

Microsoft Agent Framework (MAF) can expose an `AIAgent` through an ASP.NET Core AG-UI endpoint. For the server-side setup, see [AG-UI integration with Agent Framework](/en-us/agent-framework/integrations/by-component/ui/ag-ui/) and its [.NET getting-started guide](/en-us/agent-framework/integrations/by-component/ui/ag-ui/getting-started).

#### Build a basic streaming conversation

The first step in an agentic UI is often a basic conversation that streams responses and retains message history across turns. The [initial chat support](https://github.com/dotnet/aspnetcore/pull/68323) is provider- and protocol-neutral. Apps supply an `IChatClient` from `Microsoft.Extensions.AI`, and `UIAgent` converts its streaming responses into observable content blocks.

`ChatPage` is a complete chat shell that combines three lower-level components:

* `AgentBoundary` creates and cascades the conversation state.
* `MessageList` renders each turn as it streams and provides default typing, error, and retry UI.
* `MessageInput` sends messages from a text area and disables input while a response is streaming.

The following component creates a `UIAgent` over an app-provided `IChatClient` and renders the conversation with `ChatPage`:

```
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.AI
@using Microsoft.Extensions.AI
@implements IDisposable
@inject IChatClient ChatClient

<ChatPage Agent="agent" Placeholder="Type a message...">
    <WelcomeContent>
        <p>Ask the agent a question.</p>
    </WelcomeContent>
</ChatPage>

@code {
    private UIAgent agent = default!;

    protected override void OnInitialized()
    {
        agent = new UIAgent(ChatClient);
    }

    public void Dispose() => agent.Dispose();
}
```

`Placeholder` sets the hint shown in the empty message input. `WelcomeContent` supplies the content shown before the first message is sent.

Include the component styles in the `App` component (`Components/App.razor`):

```
<link rel="stylesheet" href="@Assets["_content/Microsoft.AspNetCore.Components.AI/ai-chat.css"]" />
```

#### Render content blocks

An `IChatClient` streams model-facing content, such as `TextContent`, `RichTextContent`, and `FunctionCallContent`, in `ChatResponseUpdate` values. `UIAgent` maps this response content into UI-facing `ContentBlock` objects that retain rendering state and can update in place while the response streams. For example, both plain-text fragments and structured rich-text snapshots map to a `RichContentBlock`.

The built-in block types include:

* `RichContentBlock` for streamed text and structured rich content.
* `FunctionInvocationContentBlock` for a server function call and its eventual result.
* `UIActionBlock` for a function that runs in the Blazor app.
* `FunctionApprovalBlock` for a function call that is waiting for user approval.
* `ActivityContentBlock` for application-defined progress that updates in place.

`ChatPage` and `MessageList` include default rendering for `RichContentBlock` and `FunctionApprovalBlock`. Add a `BlockRenderer<TBlock>` to `ChatPage.MessageListContent` to replace this default rendering or render another block type. Its child content receives the matching block as `context`, including its current properties as they change during streaming.

The following example replaces the default rendering for conversational content:

```
<ChatPage Agent="agent">
    <MessageListContent>
        <BlockRenderer TBlock="RichContentBlock" Context="block">
            <div class="agent-response">@block.RawText</div>
        </BlockRenderer>
    </MessageListContent>
</ChatPage>
```

Use the renderer's `When` predicate to only handle selected blocks of a type. If multiple renderers match, the most recently registered renderer takes precedence. Apps can also define custom `ContentBlock` types and map response content to them with a `ContentBlockHandler<TState>`.

#### Render structured rich text

The [rich-text support](https://github.com/dotnet/aspnetcore/pull/68324) permits an agent to return a structured presentation model instead of plain text. `RichTextContent` is response content that contains both plain text and `RichTextNode` values for headings, paragraphs, emphasis, links, lists, code blocks, tables, and other presentation elements. `UIAgent` maps it to the same `RichContentBlock` used for plain `TextContent` but uses the supplied node tree instead of creating simple paragraphs.

Apps can produce `RichTextContent` directly or use `IChatClient` middleware to transform streamed `TextContent`, such as by parsing markdown into `RichTextNode` values. The package doesn't require or include a particular markdown implementation. Each `RichTextContent` is a complete snapshot, so it atomically replaces the previous content for the same message as streaming progresses. `ChatPage` and `MessageList` then render the structured content without requiring a custom `BlockRenderer`.

#### Render server tool calls

An agent can call a tool that runs on its server while the Blazor app renders the operation using app-specific UI. For example, the agent can call a weather tool, and the app can show the requested location immediately, followed by a weather card when the server returns the result.

Server tool calls become `FunctionInvocationContentBlock` instances, which pair the `FunctionCallContent` with its eventual `FunctionResultContent` and expose the tool name, arguments, and completion state.

The package's source generator creates a strongly typed block handler from a class annotated with `ToolBlock`, `ToolParameter`, and `ToolResult` ([\[Blazor\] Add Components.AI server tool rendering (`dotnet/aspnetcore` #68327)](https://github.com/dotnet/aspnetcore/pull/68327)):

```
[ToolBlock("get_weather")]
public partial class WeatherToolBlock : FunctionInvocationContentBlock
{
    [ToolParameter(Name = "location")]
    public string? Location { get; set; }

    [ToolResult]
    public WeatherInfo? Weather { get; set; }
}
```

Register the generated handlers when constructing the `UIAgent`:

```
var agent = new UIAgent(
    chatClient,
    options => options.AddGeneratedToolBlocks());
```

Render the generated block in `MessageListContent`:

```
<ChatPage Agent="agent">
    <MessageListContent>
        <BlockRenderer TBlock="WeatherToolBlock">
            @if (context.HasResult)
            {
                <p>@context.Location: @context.Weather?.Temperature&deg;C</p>
            }
            else
            {
                <p>Checking the weather for @context.Location...</p>
            }
        </BlockRenderer>
    </MessageListContent>
</ChatPage>
```

As the call arguments stream, the generated handler updates `Location` while `HasResult` remains `false`. When the result arrives, it populates `Weather`, sets `HasResult` to `true`, and rerenders the same block as the completed weather card.

When MAF hosts the remote agent, backend tools use its normal tool pipeline and AG-UI transports the call and result to the client. See [Backend tool rendering with AG-UI](/en-us/agent-framework/integrations/by-component/ui/ag-ui/backend-tool-rendering).

#### Run frontend tools

Frontend tools run in the client app rather than on the agent server. For example, a Blazor app can expose a tool that changes UI state, reads a local preference, or asks the user for input. Create the tool with `AIFunctionFactory` from `Microsoft.Extensions.AI`, then register it with `UIAgentOptions.RegisterUIAction` ([\[Blazor\] Add Components.AI client tool rendering (`dotnet/aspnetcore` #68325)](https://github.com/dotnet/aspnetcore/pull/68325)).

`RegisterUIAction` advertises the `AIFunction` to the agent. When the agent requests it, `UIAgent` creates a `UIActionBlock` instead of executing the function immediately:

```
var setAccentColor = AIFunctionFactory.Create(
    async (string color) =>
    {
        await InvokeAsync(() =>
        {
            accentColor = color;
            StateHasChanged();
        });
        return $"Changed the accent color to {color}.";
    },
    name: "set_accent_color");

var agent = new UIAgent(
    chatClient,
    options => options.RegisterUIAction(setAccentColor))
```

Place a `BlockRenderer<UIActionBlock>` in `MessageListContent` to handle the function call requested by the agent. For example, the renderer can use a component that automatically invokes the function and displays its progress:

```
<ChatPage Agent="agent">
    <MessageListContent>
        <BlockRenderer TBlock="UIActionBlock"
                       When='@(action => action.ToolName == "set_accent_color")'
                       Context="action">
            <AutoInvokeAction Action="action" />
        </BlockRenderer>
    </MessageListContent>
</ChatPage>
```

The `AutoInvokeAction` component calls `InvokeAsync` when it receives the block:

```
@if (Action.IsComplete)
{
    <span>Accent color updated</span>
}
else
{
    <span>Updating accent color...</span>
}

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

    protected override async Task OnInitializedAsync()
    {
        if (!Action.IsComplete)
        {
            await Action.InvokeAsync();
        }
    }
}
```

Calling `InvokeAsync` executes the registered function with the arguments supplied by the agent. The function runs wherever the Blazor UI runs: in the server-side circuit for Blazor Server or in the browser for WebAssembly. When the function completes, `UIAgent` sends its result back to the agent and continues the conversation. Use the renderer's `When` predicate to provide different handling for each `action.ToolName`. A renderer can invoke the action automatically, as shown here, or present UI that collects input or confirmation first.

#### Require approval before tools run

An app can require the user to approve a consequential tool call, such as scheduling a meeting, before the agent proceeds. Tool approval requests become `FunctionApprovalBlock` instances. The conversation pauses until the UI calls `Approve` or `Reject` ([\[Blazor\] Add Components.AI human approval flows (`dotnet/aspnetcore` #68329)](https://github.com/dotnet/aspnetcore/pull/68329)):

```
<ChatPage Agent="agent">
    <MessageListContent>
        <BlockRenderer TBlock="FunctionApprovalBlock" Context="approval">
            <p>Allow <code>@approval.ToolName</code> to run?</p>
            <button @onclick="approval.Approve">Approve</button>
            <button @onclick="() => approval.Reject()">Reject</button>
        </BlockRenderer>
    </MessageListContent>
</ChatPage>
```

For a MAF agent, the server decides which functions require approval, and AG-UI transports the request and decision. See [Human-in-the-loop with AG-UI](/en-us/agent-framework/integrations/by-component/ui/ag-ui/human-in-the-loop).

#### Display activities

An activity is an app-defined progress item that updates in place while an agent performs longer-running work. For example, a research agent can show that it's searching sources, comparing results, and completing the research without adding a separate message for every update.

`ActivityHandler<TBlock>` is a protocol-neutral extension point that maps provider- or app-specific progress updates into a mutable `ActivityContentBlock` ([\[Blazor\] Add agentic generative UI state rendering (`dotnet/aspnetcore` #68333)](https://github.com/dotnet/aspnetcore/pull/68333)). `TryCreateBlock` initializes and emits the block for the first matching update. `TryUpdateBlock` mutates the same block as later updates arrive and indicates when the activity is complete. Register the handler with `UIAgentOptions.AddBlockHandler`, then provide a `BlockRenderer` for the app-specific block. Activities don't have a default visual representation.

AG-UI's `ACTIVITY_SNAPSHOT` and `ACTIVITY_DELTA` events are one possible source of these updates. `AGUIChatClient` exposes the original event through `ChatResponseUpdate.RawRepresentation`. For example, an app can handle replacing snapshots whose payload includes an app-defined `complete` property:

```
using System.Text.Json;
using Microsoft.AspNetCore.Components.AI;
using AGUI.Abstractions;

public sealed class ResearchActivityBlock : ActivityContentBlock
{
    public string ActivityMessageId { get; set; } = "";
}

public sealed class ResearchActivityHandler
    : ActivityHandler<ResearchActivityBlock>
{
    protected override bool TryCreateBlock(
        BlockMappingContext context,
        ResearchActivityBlock state)
        => TryApplySnapshot(context, state, out _);

    protected override bool TryUpdateBlock(
        BlockMappingContext context,
        ResearchActivityBlock state,
        out bool isCompleted)
        => TryApplySnapshot(context, state, out isCompleted);

    private static bool TryApplySnapshot(
        BlockMappingContext context,
        ResearchActivityBlock state,
        out bool isCompleted)
    {
        isCompleted = false;

        if (context.Update.RawRepresentation is not ActivitySnapshotEvent snapshot ||
            (state.ActivityMessageId.Length > 0 &&
             (state.ActivityMessageId != snapshot.MessageId ||
              snapshot.Replace == false)))
        {
            return false;
        }

        state.ActivityMessageId = snapshot.MessageId;
        state.ActivityType = snapshot.ActivityType;
        state.Content = snapshot.Content;
        isCompleted =
            snapshot.Content.ValueKind == JsonValueKind.Object &&
            snapshot.Content.TryGetProperty("complete", out var complete) &&
            complete.ValueKind == JsonValueKind.True;
        context.MarkUpdateHandled();
        return true;
    }
}
```

The handler stores the AG-UI message ID on the block to correlate later snapshots. Apps that consume `ActivityDeltaEvent` instead apply its RFC 6902 JSON Patch operations to the current `Content` before returning from `TryUpdateBlock`. The app defines the activity payload and completion semantics; Components.AI doesn't include an AG-UI-specific activity handler or JSON Patch implementation.

#### Shared state

Agentic UIs often show a shared workspace alongside the conversation, such as a recipe, document, form, or plan that the agent can update. `UIAgent<TState>` exposes this data as typed, observable UI state separately from conversational content ([\[Blazor\] Add agentic generative UI state rendering (`dotnet/aspnetcore` #68333)](https://github.com/dotnet/aspnetcore/pull/68333)).

The app configures a state mapper for the `ChatResponseUpdate` values produced by its `IChatClient`. In an AG-UI integration, the agent server explicitly maps selected tool results to `STATE_SNAPSHOT` or `STATE_DELTA` events. `AGUIChatClient` then exposes those events through `ChatResponseUpdate.RawRepresentation`, where the Blazor app can deserialize them and call `SetState`:

```
using System.Text.Json;
using AGUI.Abstractions;
using Microsoft.AspNetCore.Components.AI;

var agent = new UIAgent<RecipeState>(chatClient, options =>
{
    options.StateMapper = context =>
    {
        if (context.Update.RawRepresentation is StateSnapshotEvent snapshot &&
            snapshot.Snapshot.Deserialize<RecipeState>(
                JsonSerializerOptions.Web) is { } state)
        {
            context.SetState(state);
        }
    };
});
```

Read the current value from `agent.State.Value` and subscribe to `agent.State.OnChanged` when the surrounding component needs to rerender. State mappers can also handle app-specific `AIContent` from other `IChatClient` implementations.

For the corresponding MAF server configuration, including mapping tool results to state snapshots and deltas, see [State management with AG-UI](/en-us/agent-framework/integrations/by-component/ui/ag-ui/state-management).

#### Show predictive UI state

Predictive state permits an app to render an agent's proposed state change while the model is still generating it without replacing the committed state. For example, as an agent generates the complete contents of an edited document in a tool argument, the UI can progressively display the proposed document and a diff. When generation finishes, the user can accept the completed proposal or reject it and restore the committed document.

An AG-UI server integration can map streamed arguments for a state-writing tool to provisional state events. The completed tool-call arguments are the authoritative proposal. When creating the `UIAgent<TState>`, configure its state mapper to deserialize those events and call `SetPredictiveState`. `AgentState<TState>` then retains the prior committed value for rollback ([\[Blazor\] Add predictive state updates (`dotnet/aspnetcore` #68335)](https://github.com/dotnet/aspnetcore/pull/68335)):

```
using System.Text.Json;
using AGUI.Abstractions;
using Microsoft.AspNetCore.Components.AI;

var agent = new UIAgent<DocumentState>(chatClient, options =>
{
    options.StateMapper = context =>
    {
        if (context.Update.RawRepresentation is StateSnapshotEvent snapshot &&
            snapshot.Snapshot.Deserialize<DocumentState>(
                JsonSerializerOptions.Web) is { } predictedState)
        {
            context.SetPredictiveState(predictedState);
        }
    };

    options.RegisterUIAction(AIFunctionFactory.Create(
        ConfirmChanges,
        name: "confirm_changes",
        description: "Confirm the proposed document changes."));
});
```

The example also registers a `confirm_changes` frontend action. When the action appears, a custom block renderer displays the accept and reject controls. The renderer adds the user's choice as the `accepted` argument and calls `UIActionBlock.InvokeAsync`, which runs the registered callback:

```
private string ConfirmChanges(bool accepted)
{
    if (accepted)
    {
        agent.State.AcceptPredictiveState();
    }
    else
    {
        agent.State.RejectPredictiveState();
    }

    return accepted
        ? "The user accepted the changes."
        : "The user rejected the changes.";
}
```

The provisional value is immediately available from `agent.State.Value`, and `HasPendingPredictiveState` indicates that it isn't committed. The callback commits the completed proposal or restores the baseline, and its return value reports the decision to the agent in a follow-up run. If generation fails, is canceled, or ends without a decision, the provisional value is automatically rolled back. The server-side extraction and mapping of streamed tool arguments must be configured explicitly; see [State management with AG-UI](/en-us/agent-framework/integrations/by-component/ui/ag-ui/state-management).

#### Persist and restore conversations

An `IConversationThread` stores completed turns so a UI can rebuild its conversation after the component or app restarts. A thread can also retain protocol metadata, such as the `threadId` and previous `runId` used to continue a server-owned AG-UI conversation.

Pass the thread when constructing `UIAgent`, then call `UIAgent.RestoreAsync` or `AgentContext.RestoreAsync` to explicitly replay the stored updates into content blocks and typed state ([\[Blazor\] Add shared agent and UI state (`dotnet/aspnetcore` #68334)](https://github.com/dotnet/aspnetcore/pull/68334)):

```
var agent = new UIAgent(
    chatClient,
    options => options.Thread = conversationThread);

var restoredBlocks = await agent.RestoreAsync();
```

Passing a thread to `UIAgent` enables new completed turns to be persisted but doesn't automatically restore earlier turns. For MAF-hosted AG-UI agents, see [AG-UI conversation continuity](/en-us/agent-framework/integrations/by-component/ui/ag-ui/getting-started#conversation-continuity).

### Validation attributes are no longer experimental

[ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute) and [SkipValidationAttribute](/en-us/dotnet/api/microsoft.extensions.validation.skipvalidationattribute) from the [`Microsoft.Extensions.Validation` package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) are no longer marked experimental. If you suppressed `ASP0029` to use either attribute, remove the suppression.

For more information, see the following resources:

* [Upgrade from the experimental `ValidatableTypeAttribute` and `SkipValidationAttribute` in .NET 10](../migration/100-to-110?view=aspnetcore-10.0#upgrade-from-the-experimental-validatabletypeattribute-and-skipvalidationattribute-in-net-10)
* [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-11.0#nested-objects-and-collections)
* [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-11.0#force-generate-validatable-type-information)

## Blazor Hybrid

This section describes new features for Blazor Hybrid.

*Release notes appear in this section as preview features become available.*

## SignalR

This section describes new features for SignalR.

### SignalR authentication refresh

SignalR connections can refresh authentication without dropping the connection when the access token expires. The server exposes a `/refresh` endpoint alongside `/negotiate` and reports the token lifetime in the negotiate response. A client re-authenticates before the token expires, so a hub connection that previously closed when its bearer token aged out can stay open.

Enable the feature per hub on the server. The server can inspect or reject a refreshed identity by returning a value from `OnAuthenticationRefresh`:

```
using System.Security.Claims;

app.MapHub<ChatHub>("/chat", options =>
{
    options.EnableAuthenticationRefresh = true;
    options.CloseOnAuthenticationExpiration = true;

    // Optional: inspect the refreshed identity and decide whether to accept it.
    options.OnAuthenticationRefresh = context =>
    {
        var previousSubject = context.PreviousUser.FindFirstValue("sub")
            ?? context.PreviousUser.FindFirstValue(ClaimTypes.NameIdentifier);
        var newSubject = context.NewUser.FindFirstValue("sub")
            ?? context.NewUser.FindFirstValue(ClaimTypes.NameIdentifier);

        return Task.FromResult(
            previousSubject is not null &&
            string.Equals(previousSubject, newSubject, StringComparison.Ordinal));
    };
});
```

A hub can react to a refreshed identity by overriding `OnAuthenticationRefreshedAsync`:

```
public class ChatHub : Hub
{
    public override Task OnAuthenticationRefreshedAsync()
    {
        // The connection's User has been updated with the refreshed token.
        return Task.CompletedTask;
    }
}
```

Automatic refresh is on by default in the .NET client and is configurable with `WithAuthenticationRefresh`. Refresh notifications are events on [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection), and `RefreshAuthenticationAsync` requests an immediate refresh after the app obtains new claims:

```
await using var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chat")
    .WithAuthenticationRefresh(options =>
    {
        // EnableAutoRefresh is true by default.
        options.RefreshBeforeExpiration = TimeSpan.FromMinutes(1);
    })
    .Build();

connection.AuthenticationRefreshed += context => Task.CompletedTask;
connection.AuthenticationRefreshFailed += context => Task.CompletedTask;

await connection.StartAsync();

// Refresh immediately after acquiring a token with updated claims.
await connection.RefreshAuthenticationAsync();
```

### Cancel hub invocations from the client

The SignalR client can cancel a regular, non-streaming hub method invocation. Previously only streaming invocations could be canceled from the client. Now, when you pass a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) to [InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionextensions.invokeasync) and cancel it, the client sends a cancellation message, and the hub method's `CancellationToken` parameter is triggered on the server.

```
// Client — canceling the token cancels the server-side invocation.
using var cts = new CancellationTokenSource();
var work = connection.InvokeAsync("LongRunningWork", cts.Token);
// ...
cts.Cancel();
```

```
// Hub — accept a CancellationToken to observe client cancellation.
public class WorkHub : Hub
{
    public async Task LongRunningWork(CancellationToken cancellationToken)
    {
        await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);
    }
}
```

### SignalR .NET client supports authentication refresh after redirects

The SignalR .NET client extends [SignalR authentication refresh](#signalr-authentication-refresh) so it works when negotiate redirects to another server, contributed by [@MoChilia](https://github.com/MoChilia). This client change enables support for redirecting servers such as Azure SignalR Service, which hasn't enabled the feature yet.

The client preserves the app-token provider across the redirect, adopts a refreshed transport token from the response, and retains `tokenLifetimeSeconds` so automatic refresh remains scheduled after the original token expires.

Thank you [@MoChilia](https://github.com/MoChilia) for this contribution!

### SignalR TypeScript client supports authentication refresh

The SignalR TypeScript client supports refreshing an access token without reconnecting. It can schedule a refresh based on the token lifetime that the server reports or refresh immediately after the app obtains updated claims.

Configure automatic refresh by using `withAuthenticationRefresh`. Register success and failure handlers on the built connection, and call `refreshAuthentication` to request a manual refresh:

```
const connection = new signalR.HubConnectionBuilder()
  .withUrl("/clock", { accessTokenFactory: getAccessToken })
  .withAuthenticationRefresh({
    enableAutoRefresh: true,
    refreshBeforeExpirationInMilliseconds: 120_000,
  })
  .build();

connection.onAuthenticationRefreshed((context) => {
  console.log(`New token lifetime: ${context.newTokenLifetimeInSeconds}`);
});

connection.onAuthenticationRefreshFailed((context) => {
  console.error(context.error);
});

await connection.start();

// Refresh immediately after acquiring a token with updated claims.
await connection.refreshAuthentication();
```

## Minimal APIs

This section describes new features for Minimal APIs.

### Endpoint filters observe parameter-binding failures

When a Minimal API endpoint has any filters or filter factories configured, the filter pipeline now runs even if parameter binding fails. Filters can read `HttpContext.Response.StatusCode == 400` and substitute their own response body.

In the `Development` environment, set `RouteHandlerOptions.ThrowOnBadRequest = false` so the framework returns a 400 that the filter can observe instead of throwing [BadHttpRequestException](/en-us/dotnet/api/microsoft.aspnetcore.http.badhttprequestexception) to the developer exception page. This is already the default in non-`Development` environments.

Thank you [@marcominerva](https://github.com/marcominerva) for this contribution!

### C# union types

ASP.NET Core supports [C# union types](/en-us/dotnet/csharp/whats-new/csharp-15#union-types) ([C# language reference](/en-us/dotnet/csharp/language-reference/builtin-types/union)), which are new in .NET 11, anywhere [`System.Text.Json`](/en-us/dotnet/standard/serialization/system-text-json/overview) is used: JSON request and response bodies in Minimal APIs and MVC, SignalR's `JsonHubProtocol`, Blazor JavaScript interop, persistent component state, and prerendered component parameters.

```
public union UnionIntString(int, string);

app.MapGet("/value", () => new UnionIntString(42));
```

Union types aren't supported for non-body binding sources such as route values, query strings, headers, and form fields.

For OpenAPI, an endpoint that returns a union is described with an `anyOf` schema listing each case type. Unlike polymorphic types, union cases don't carry a `$type` discriminator, so each case reuses its standalone component (for example, `#/components/schemas/Dog`) instead of a duplicated, prefixed one. ApiExplorer detects a union through `JsonTypeInfoKind.Union`, so the schema also flows through to Swashbuckle and NSwag. When multiple cases serialize to the same JSON shape, disambiguate them with a `[JsonUnion]` classifier. SignalR unions require the JSON hub protocol; the MessagePack and `Newtonsoft.Json` protocols don't support unions.

For examples and additional information that apply to Blazor apps, see the [Component parameters section in the Components overview article](../blazor/components/?view=aspnetcore-10.0#component-parameters) and the [Pass parameters section of the Dynamically-rendered ASP.NET Core Razor components article](../blazor/components/dynamiccomponent?view=aspnetcore-10.0).

### Async validation for Minimal APIs

Minimal API validation now supports asynchronous validators end-to-end ([dotnet/aspnetcore #66487](https://github.com/dotnet/aspnetcore/pull/66487), [dotnet/aspnetcore #67183](https://github.com/dotnet/aspnetcore/pull/67183)). Preview 5 shipped the building blocks for asynchronous form validation in Blazor. Preview 6 adds new asynchronous `DataAnnotations` APIs in the base libraries (`AsyncValidationAttribute` and `IAsyncValidatableObject`), and `Microsoft.Extensions.Validation` now runs them when an endpoint validates a request.

The simplest way to add an asynchronous rule is a custom validation attribute. Derive from `AsyncValidationAttribute` and implement `IsValidAsync` to query a database or call a remote API without blocking a thread. The synchronous `IsValid` is abstract too; throw from it when the attribute validates asynchronously only:

```
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;

public sealed class UniqueEmailAttribute : AsyncValidationAttribute
{
    // Synchronous IsValid. This attribute validates asynchronously only.
    protected override ValidationResult? IsValid(object? value, ValidationContext context) =>
        throw new InvalidOperationException("Validate this attribute with IsValidAsync.");

    protected override async Task<ValidationResult?> IsValidAsync(
        object? value, ValidationContext context, CancellationToken cancellationToken)
    {
        var users = context.GetRequiredService<IUserService>();
        
        if (value is string email && await users.EmailExistsAsync(email, cancellationToken))
        {
            return new ValidationResult("That email is already registered.");
        }

        return ValidationResult.Success;
    }
}
```

Apply `[UniqueEmail]` to a property like any built-in validation attribute.

For validation that spans several properties or the whole object, implement `IAsyncValidatableObject` and return results as an `IAsyncEnumerable<ValidationResult>`. Because `IAsyncValidatableObject` extends `IValidatableObject`, also implement the synchronous `Validate` method. When a type validates asynchronously only, throw from `Validate` so its validation isn't silently skipped by the synchronous APIs:

```
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;

public class ReservationRequest : IAsyncValidatableObject
{
    [Required]
    public string Email { get; set; } = "";

    public DateOnly Date { get; set; }

    // Synchronous IValidatableObject. This type validates asynchronously only.
    public IEnumerable<ValidationResult> Validate(ValidationContext context) =>
        throw new InvalidOperationException("Validate this type with ValidateAsync.");

    public async IAsyncEnumerable<ValidationResult> ValidateAsync(
        ValidationContext context,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        var rooms = context.GetRequiredService<IRoomService>();

        if (!await rooms.HasAvailabilityAsync(Date, cancellationToken))
        {
            yield return new ValidationResult(
                "No rooms are available on that date.", [nameof(Date)]);
        }
    }
}
```

Register validation and the framework validates the request before the endpoint runs:

```
builder.Services.AddValidation();

app.MapPost("/reservations", (ReservationRequest request) =>
    Results.Ok(request));
```

Validators run concurrently where possible: asynchronous attributes on the same member start together, collection items validate in parallel, and the framework preserves the existing ordering between member, type, and `IValidatableObject` validation.

### Short-circuit endpoints with an attribute

The new `[ShortCircuit]` attribute marks an endpoint to run immediately after routing, skipping the rest of the middleware pipeline. This is the attribute form of the existing `ShortCircuit()` endpoint convention, so it can be applied directly to MVC controllers and actions.

Short-circuiting is useful for endpoints that don't need authentication, CORS, or other middleware—for example a health check or a `robots.txt` response, and it avoids the cost of running that middleware. The endpoint still runs and produces its response. Pass an optional status code, such as `[ShortCircuit(404)]`, to set the response status code.

```
[ApiController]
[Route("robots.txt")]
[ShortCircuit]
public class RobotsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Content("User-agent: *\nDisallow:", "text/plain");
}
```

The same attribute works on Minimal API endpoints, and the existing `ShortCircuit()` convention continues to work unchanged:

```
app.MapGet("/health", [ShortCircuit] () => "Healthy");
```

Thank you [@Porozhniakov](https://github.com/Porozhniakov) for contributing this feature!

### Validation localization is built in

`Microsoft.Extensions.Validation` localizes validation messages and display names without a separate package. Calling `AddLocalization` to register an `IStringLocalizerFactory`, followed by `AddValidation`, activates localization automatically. The validation source generator emits the localization lookup into your assembly.

```
builder.Services.AddLocalization();
builder.Services.AddValidation();
```

```
[ValidatableType]
public class CustomerModel
{
    [Display(Name = "CustomerName")]          // resource key for the display name
    [Required(ErrorMessage = "NameRequired")] // resource key for the message
    public string? Name { get; set; }
}
```

By default, keys resolve against the resources of the type that declares the validated member. An explicit `ErrorMessage` value, such as `NameRequired` in the preceding example, is the first resource key that localization tries. When an attribute doesn't specify `ErrorMessage`, localization instead tries built-in resource-name conventions from most to least specific:

1. `{DeclaringType}_{MemberName}_{AttributeType}_Error`
2. `{DeclaringType}_{AttributeType}_Error`
3. `{AttributeType}_Error`

For example, a `[Required]` attribute on `CustomerModel.Name` resolves against `CustomerModel_Name_RequiredAttribute_Error`, `CustomerModel_RequiredAttribute_Error`, or the shared `RequiredAttribute_Error` resource. This allows the default message of an attribute to be translated once for the whole app. If no resource resolves, validation falls back to the attribute's built-in message. Use `ValidationOptions.LocalizerProvider` to resolve keys from a shared resource file instead:

```
builder.Services.AddValidation(options =>
{
    options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationMessages));
});
```

Localized strings don't have to come from resource files. Registering a custom `IStringLocalizerFactory` switches validation messages to that factory's backing store, such as a database or JSON files. A user-registered factory takes precedence over the default resource file implementation:

```
builder.Services.AddSingleton<IStringLocalizerFactory, DbStringLocalizerFactory>();
builder.Services.AddValidation();
```

Attributes that already localize themselves (`ErrorMessageResourceType`, `[Display(ResourceType = ...)]`) bypass the pipeline entirely. A custom attribute that needs to substitute its own values into the message template can implement `IValidationMessageFormatter`:

```
public sealed class DivisibleByAttribute : ValidationAttribute, IValidationMessageFormatter
{
    public int Divisor { get; init; }

    public string FormatMessage(CultureInfo culture, string template, string displayName)
        => string.Format(culture, template, displayName, Divisor); // {0} = name, {1} = divisor
}
```

The same localization rules apply to validation for minimal APIs and Blazor, so a message localizes identically wherever the model is used.

Complete feature coverage is available in the following articles:

* [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-11.0#localize-validation-messages)
* [Minimal APIs quick reference](../fundamentals/minimal-apis?view=aspnetcore-11.0#localizing-validation-messages)

For more information, see [Add localization support to Microsoft.Extensions.Validation (`dotnet/aspnetcore` #66646)](https://github.com/dotnet/aspnetcore/pull/66646).

## OpenAPI

This section describes new features for OpenAPI.

### Describe binary file responses

ASP.NET Core 11 introduces support for generating OpenAPI descriptions for operations that return binary file responses. This support maps the `FileContentResult` result type to an OpenAPI schema with `type: string` and `format: binary`.

* [Minimal APIs](#tabpanel_1_minimal-apis)
* [Controllers](#tabpanel_1_controllers)

Use the `Produces<T>` extension method with `T` of `FileContentResult` to specify the response type and content type:

```
app.MapPost("/filecontentresult", () =>
{
    var content = "This endpoint returns a FileContentResult!"u8.ToArray();
    return TypedResults.File(content);
})
.Produces<FileContentResult>(contentType: MediaTypeNames.Application.Octet);
```

Use the `ProducesResponseType<T>` attribute with `T` of `FileContentResult` to specify the response type and content type:

```
[HttpPost("filecontentresult")]
[ProducesResponseType<FileContentResult>(StatusCodes.Status200OK, MediaTypeNames.Application.Octet)]
public IActionResult PostFileContentResult()
{
    var content = "This endpoint returns a FileContentResult!"u8.ToArray();
    return new FileContentResult(content, MediaTypeNames.Application.Octet);
}
```

The generated OpenAPI document describes the endpoint response as:

```
responses:
  '200':
    description: OK
    content:
      application/octet-stream:
        schema:
          $ref: '#/components/schemas/FileContentResult'
```

The `FileContentResult` is defined in `components/schemas` as:

```
components:
  schemas:
    FileContentResult:
      type: string
      format: binary
```

### OpenAPI 3.2.0 support (Breaking Change)

`Microsoft.AspNetCore.OpenApi` now supports OpenAPI 3.2.0 through an updated dependency on `Microsoft.OpenApi` 3.3.1. This update includes breaking changes from the underlying library. For more information, see the [Microsoft.OpenApi upgrade guide](https://github.com/microsoft/OpenAPI.NET/blob/main/docs/upgrade-guide-3.md).

To generate an OpenAPI 3.2.0 document, specify the version when calling [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi):

```
builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_2;
});
```

Subsequent updates take advantage of new capabilities in the 3.2.0 specification, such as item schema support for streaming events.

Thank you [@baywet](https://github.com/baywet) for this contribution!

### HTTP QUERY in generated OpenAPI documents

OpenAPI document generation now recognizes [HTTP QUERY](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/) as a known operation type. QUERY is a proposed safe, idempotent method that lets clients send a request body when describing a search, useful when a query is too large or too structured to fit in a URL. Routing already accepts arbitrary verb strings via `MapMethods`, and OpenAPI 3.2 adds a [`query` field to the Path Item Object](https://spec.openapis.org/oas/v3.2.0.html#fixed-fields-6) so this can be described in the OpenAPI document.

Note that `query` is only valid in an OpenAPI 3.2 document, so set the `OpenApiVersion` in the `OpenApiOptions`. In earlier OpenAPI versions, the `query` operation is generated within an `x-oai-additionalOperations` specification extension in the Path Item Object.

```
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_2;
});

var app = builder.Build();

app.MapOpenApi();

app.MapMethods("/search", ["QUERY"], (SearchRequest request) =>
    SearchService.Run(request));

app.Run();
```

In an OpenAPI 3.2 document, the QUERY operation is described inline as a sibling of `get`, `post`, and other standard operations:

```
"paths": {
  "/search": {
    "query": {
      "requestBody": { ... },
      "responses": { "200": { ... } }
    }
  }
}
```

In OpenAPI 3.0 and 3.1 documents, the same operation is represented under the `x-oai-additionalOperations` extension on the Path Item:

```
"paths": {
  "/search": {
    "x-oai-additionalOperations": {
      "QUERY": {
        "requestBody": { ... },
        "responses": { "200": { ... } }
      }
    }
  }
}
```

Thank you [@kilifu](https://github.com/kilifu) for this contribution!

### File stream result types appear in OpenAPI documents

`FileStreamResult`, `FileContentHttpResult`, and `FileStreamHttpResult` are now described as binary string schemas in generated OpenAPI documents, so clients see accurate response shapes for endpoints that stream files. Annotate the endpoint with `.Produces<FileContentHttpResult>(contentType: "application/pdf")` (or the equivalent `FileStreamHttpResult`/`FileStreamResult` type) so OpenAPI sees the result type and emits the binary schema.

Thank you [@marcominerva](https://github.com/marcominerva) for this contribution!

### OpenAPI schemas better match ASP.NET Core behavior

OpenAPI generation now handles several schema cases more accurately. Non-body enum parameters keep the original C# enum member names even when HTTP JSON options configure a [JsonStringEnumConverter](/en-us/dotnet/api/system.text.json.serialization.jsonstringenumconverter) naming policy, because query, route, header, and form binding use `Enum.TryParse` rather than JSON serialization. Array schema reference IDs now use valid component names such as `stringArray` and `TodoArray` instead of names with array syntax.

```
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.Converters.Add(
        new JsonStringEnumConverter(JsonNamingPolicy.KebabCaseLower));
});

app.MapGet("/orders", (OrderStatus status) => Results.Ok(status));
```

With this configuration, a body schema can still describe `OrderStatus.PendingReview` as `pending-review`, while the query parameter schema describes the accepted value as `PendingReview`.

Minimal API endpoints can support multiple [Produces](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.produces) extension method calls for the same status code—for example, to specify that a 200 response may arrive as `application/json` or `text/plain` with different schemas. The same support applies to MVC controllers via multiple [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attributes.

In prior releases, the framework collapsed each status code to a single response type and silently dropped the rest, making it impossible to describe endpoints that serve multiple content types. [Microsoft.AspNetCore.Mvc.ApiExplorer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer) now preserves every declared response type with deterministic ordering, and the generated OpenAPI document emits separate content entries per media type—or an `anyOf` schema when multiple types share the same content type.

Thank you [@marcominerva](https://github.com/marcominerva) for the array schema reference contribution!

### OpenAPI 3.2 by default

Generated OpenAPI documents now target OpenAPI 3.2 by default. Documents continue to generate as before. Set the document version explicitly if you need to target an earlier version for tooling that doesn't yet support OpenAPI 3.2.

To target an earlier version, specify it when calling [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi):

```
builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});
```

### Server-Sent Events support in OpenAPI 3.2

Endpoints that return `SseItem<T>` are described in the generated OpenAPI document with the OpenAPI 3.2 `itemSchema` shape for `text/event-stream` responses. The `itemSchema` describes a stream's per-event payload shape instead of falling back to a plain `string` schema.

```
app.MapGet("/todos/stream", (CancellationToken ct) =>
    TypedResults.ServerSentEvents(GetTodosAsync(ct)))
   .WithName("StreamTodos");

static async IAsyncEnumerable<SseItem<Todo>> GetTodosAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    foreach (var todo in Todos.All)
    {
        yield return new SseItem<Todo>(todo) { EventId = todo.Id.ToString() };
        await Task.Delay(1000, ct);
    }
}
```

Return the stream through `TypedResults.ServerSentEvents`. A handler that returns `IAsyncEnumerable<SseItem<T>>` directly is serialized as JSON instead of SSE. Use the dedicated `SseItem<T>` overload without `eventType`. To use one event name for the whole stream, pass a plain `IAsyncEnumerable<T>` with `eventType`.

The generated 3.2 document describes the event payload with `itemSchema` referencing `#/components/schemas/Todo`, plus the standard SSE `event` and `id` string fields:

```
responses:
  '200':
    description: OK
    content:
      text/event-stream:
        itemSchema:
          type: object
          required: [data]
          properties:
            data:
              $ref: '#/components/schemas/Todo'
            event: { type: string }
            id: { type: string }
```

If the event payload is a discriminated union (a preview C# 14 feature), OpenAPI also emits the union's case names as an `enum` on the `event` field.

### Select an environment for build-time OpenAPI document generation

Build-time OpenAPI document generation supports selecting the app environment with the `OpenApiGenerationEnvironment` MSBuild property. The property sets the host's environment for the generation process, equivalent to setting the `ASPNETCORE_ENVIRONMENT` or `DOTNET_ENVIRONMENT` environment variable. Environment-specific configuration and document transformations can therefore affect the generated OpenAPI document without requiring the environment variable to be set before running `dotnet build`.

Set the property in the project file:

```
<PropertyGroup>
  <OpenApiGenerationEnvironment>Development</OpenApiGenerationEnvironment>
</PropertyGroup>
```

For more information, see [Generate OpenAPI documents](../fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0#customize-build-time-document-generation).

Thank you [@ldsenow](https://github.com/ldsenow) for this contribution!

### OpenAPI reflects obsolete APIs

ASP.NET Core OpenAPI generation maps `[Obsolete]` to `deprecated: true` automatically for operations, schema types, and schema properties. API clients and documentation tools can therefore surface the same deprecation information as .NET callers without a custom OpenAPI transformer.

```
app.MapGet("/catalog/{id}", GetCatalogItem);

#pragma warning disable CS0618 // This example intentionally declares and maps obsolete APIs.
app.MapGet("/catalog/legacy/{id}", GetLegacyCatalogItem);

[Obsolete("Use /catalog/{id}.")]
static LegacyCatalogItem GetLegacyCatalogItem(int id) =>
    new(id, $"Product {id}", $"SKU-{id:D4}");

static CatalogItem GetCatalogItem(int id) =>
    new(id, $"Product {id}", $"SKU-{id:D4}");

public sealed record CatalogItem(
    int Id,
    string Name,
    string StockKeepingUnit);

[Obsolete("Use CatalogItem.")]
public sealed record LegacyCatalogItem(
    int Id,
    string Name,
    [property: Obsolete("Use StockKeepingUnit.")] string Sku);

#pragma warning restore CS0618
```

The generated document marks the legacy operation, its response schema, and the `Sku` property as deprecated:

```
{
  "paths": {
    "/catalog/legacy/{id}": {
      "get": {
        "deprecated": true
      }
    }
  },
  "components": {
    "schemas": {
      "LegacyCatalogItem": {
        "deprecated": true,
        "properties": {
          "sku": {
            "deprecated": true
          }
        }
      }
    }
  }
}
```

An [IOpenApiOperationTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapioperationtransformer) or [IOpenApiSchemaTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapischematransformer) can override the generated value for a specific API.

Thank you [@fickleEfrit](https://github.com/fickleEfrit) for this contribution!

## Authentication and authorization

This section describes new features for authentication and authorization.

### `TimeProvider` support in ASP.NET Core Identity

ASP.NET Core Identity now uses `TimeProvider` instead of `DateTime` and `DateTimeOffset` for all time-related operations. This change makes Identity components more testable and provides better control over time in tests and specialized scenarios.

The following example shows how to use a fake `TimeProvider` for testing Identity features:

```
// In tests
var fakeTimeProvider = new FakeTimeProvider(
    new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero));

services.AddSingleton<TimeProvider>(fakeTimeProvider);
services.AddIdentity<IdentityUser, IdentityRole>();

// Identity will now use the fake time provider
```

By using `TimeProvider`, you can more easily write deterministic tests for time-sensitive Identity features such as token expiration, lockout durations, and security stamp validation.

### Infer passkey display name from authenticator

ASP.NET Core Identity now automatically infers friendly display names for passkeys based on their AAGUID (Authenticator Attestation GUID). Built-in mappings are included for the most commonly used passkey authenticators, including Google Password Manager, iCloud Keychain, Windows Hello, 1Password, and Bitwarden.

For known authenticators, the name is automatically assigned without prompting the user. For unknown authenticators, the user is redirected to a rename page. Extend the mappings by adding entries to the `PasskeyAuthenticators` dictionary in the project.

### `dotnet user-jwts` supports file-based apps

The `dotnet user-jwts` tool creates signed development JWTs so you can call an app's authenticated endpoints without setting up a real identity provider. The `create` command generates a token, stores its signing key in the app's user secrets, and prints the token to use as a bearer token. It now works with file-based apps (a single `app.cs` with no project file) through the new `--file` option:

```
dotnet user-jwts create --file app.cs
```

### Consistent authorization metadata across the stack

Authorization metadata can be expressed as [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata), an [AuthorizationPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicy), or an [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) attribute. MVC filters, SignalR hub methods, and Blazor's `AuthorizeView` and `AuthorizeRouteView` apply all three forms consistently.

A new `AuthorizationPolicy.CombineAsync` overload is the shared implementation:

```
public class AuthorizationPolicy
{
    public static Task<AuthorizationPolicy?> CombineAsync(
        IAuthorizationPolicyProvider policyProvider,
        IEnumerable<object> metadata);
}
```

MVC, SignalR, and Blazor use this overload internally. A custom attribute that implements both [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata) and [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) contributes to the decision once. The legacy MVC path with `EnableEndpointRouting = false` is unchanged.

### Negotiate authentication uses TLS channel binding

Negotiate authentication on Kestrel uses the TLS endpoint channel binding token for HTTPS connections. The authentication handler supplies the token to the underlying Kerberos or NTLM exchange and retains it across multi-round authentication.

No configuration changes are required. Non-HTTPS connections and HTTPS connections where a channel binding token isn't available continue to use the existing behavior.

### Experimental Device Bound Session Credentials support

Important

The `Microsoft.AspNetCore.Authentication.DeviceBoundSessions` package is experimental and remains prerelease throughout .NET 11 and until the specification stabilizes.

The [Device Bound Session Credentials (DBSC) specification](https://w3c.github.io/webappsec-dbsc/) defines a protocol that binds session refresh to a private key held by the browser. The app issues a short-lived session cookie, and the browser must provide a signed proof of possession to refresh it. A copied session cookie might remain usable until it expires, but an attacker without the device key can't use it to extend the session.

ASP.NET Core adds an experimental server-side DBSC implementation in the `Microsoft.AspNetCore.Authentication.DeviceBoundSessions` package. The authentication component layers over an existing cookie authentication scheme and manages the registration and refresh endpoints, a path-scoped refresh cookie, and the short-lived session cookie.

After adding the `Microsoft.AspNetCore.Authentication.DeviceBoundSessions` package, configure DBSC over an existing cookie authentication scheme:

```
builder.Services
    .AddAuthentication("Application")
    .AddCookie("Application")
    .AddDeviceBoundSession("Application", options =>
    {
        options.ShortLivedCookieExpiration = TimeSpan.FromMinutes(10);
    });
```

Browser support currently requires an experimental DBSC implementation. For more information, see [Chrome's DBSC documentation](https://developer.chrome.com/docs/web-platform/device-bound-session-credentials).

## Miscellaneous

This section describes miscellaneous new features in .NET 11.

### `IOutputCachePolicyProvider` interface

ASP.NET Core in .NET 11 provides the [IOutputCachePolicyProvider](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.ioutputcachepolicyprovider) interface for implementing custom output caching policy selection logic. By using this interface, apps can determine the default base caching policy, check for the existence of named policies, and support advanced scenarios where policies must be resolved dynamically. Examples include loading policies from external configuration sources, databases, or applying tenant-specific caching rules.

The following code shows the `IOutputCachePolicyProvider` interface:

```
public interface IOutputCachePolicyProvider
{
    IReadOnlyList<IOutputCachePolicy> GetBasePolicies();
    ValueTask<IOutputCachePolicy?> GetPolicyAsync(string policyName);
}
```

Thank you [@lqlive](https://github.com/lqlive) for this contribution!

### Auto-trust development certificates in WSL

The development certificate setup now automatically trusts certificates in WSL (Windows Subsystem for Linux) environments. When you run `dotnet dev-certs https --trust` in WSL, the certificate is automatically installed and trusted in both the WSL environment and Windows, eliminating manual trust configuration.

```
# Automatically trusts certificates in both WSL and Windows
dotnet dev-certs https --trust
```

This improvement streamlines the development experience when using WSL, removing a common friction point for developers working in Linux environments on Windows.

Thank you [@StickFun](https://github.com/StickFun) for this contribution!

### Native OpenTelemetry tracing for ASP.NET Core

ASP.NET Core now natively adds OpenTelemetry semantic convention attributes to the HTTP server activity, aligning with the [OpenTelemetry HTTP server span specification](https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server-span). All required attributes are included by default, matching the metadata previously only available through the `OpenTelemetry.Instrumentation.AspNetCore` library.

To collect the built-in tracing data, subscribe to the `Microsoft.AspNetCore` activity source in your OpenTelemetry configuration:

```
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource("Microsoft.AspNetCore")
        .AddConsoleExporter());
```

No additional instrumentation library (such as `OpenTelemetry.Instrumentation.AspNetCore`) is needed. The framework now directly populates semantic convention attributes on the request activity, such as `http.request.method`, `url.path`, `http.response.status_code`, and `server.address`.

If you don't want OpenTelemetry attributes added to the activity, you can turn it off by setting the `Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData` AppContext switch to `true`.

### Performance improvements

Kestrel's HTTP/1.1 request parser now uses a non-throwing code path for handling malformed requests. Instead of throwing [BadHttpRequestException](/en-us/dotnet/api/microsoft.aspnetcore.http.badhttprequestexception) on every parse failure, the parser returns a result struct indicating success, incomplete, or error states. In scenarios with many malformed requests — such as port scanning, malicious traffic, or misconfigured clients — this eliminates expensive exception handling overhead and improves throughput by up to 20-40%. There's no impact on valid request processing.

The HTTP logging middleware now pools its `ResponseBufferingStream` instances, reducing per-request allocations when response body logging or interceptors are enabled.

### Zstandard response compression and request decompression

ASP.NET Core now supports [Zstandard (zstd)](https://facebook.github.io/zstd/) for both response compression and request decompression. This adds zstd support to the existing response-compression and request-decompression middleware and enables zstd by default.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression();
builder.Services.AddRequestDecompression();
builder.Services.Configure<ZstandardCompressionProviderOptions>(options =>
{
    options.CompressionOptions = new ZstandardCompressionOptions
    {
        Quality = 6 // 1-22, higher = better compression, slower
    };
});
```

Thank you [@manandre](https://github.com/manandre) for this contribution!

### HTTP/3 starts processing requests earlier

Kestrel now starts processing HTTP/3 requests without waiting for the control stream and SETTINGS frame first, which reduces first-request latency on new connections.

### MCP Server template ships with the .NET SDK

The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that AI applications and agents, such as those in Visual Studio, Visual Studio Code, and GitHub Copilot, use to discover and call external tools, data, and services through a consistent interface. An *MCP server* exposes your own functionality, such as custom tools or access to a data source, so an AI host can invoke it on the user's behalf.

Use the `mcpserver` template when you want to build a C# MCP server that integrates your code or services with AI-powered tools. The generated project uses the [official C# SDK for MCP](https://github.com/modelcontextprotocol/csharp-sdk) and includes a working sample tool, so you have a runnable starting point to extend with your own tools.

The `mcpserver` project template, previously available only by installing `Microsoft.McpServer.ProjectTemplates`, now ships as a bundled template in the .NET SDK:

```
dotnet new mcpserver -o MyMcpServer
```

Moving the template into ASP.NET Core makes it discoverable from `dotnet new list` without a separate install step, and aligns its servicing with the rest of the web stack.

For more information, see [Build a Model Context Protocol (MCP) server in C#](/en-us/dotnet/ai/quickstarts/build-mcp-server).

### TLS handshake observability in Kestrel

Two related changes make it easier to diagnose and customize TLS connections in Kestrel.

`ITlsHandshakeFeature` now exposes an `Exception` property containing the exception thrown during a failed TLS handshake, so middleware and logging can record why a connection failed instead of seeing a bare `IOException` further up the stack. The feature continues to work after the handshake fails — Kestrel snapshots the relevant fields off the underlying `SslStream` before it is disposed.

The `TlsClientHelloBytesCallback` option on `HttpsConnectionAdapterOptions` was reworked as a connection middleware. The previous callback shape is now obsolete; configure ClientHello inspection via the new `ListenOptions.UseTlsClientHelloListener` extension instead. The example below uses both features together — connection middleware reads `ITlsHandshakeFeature.Exception` after the handshake, and `UseTlsClientHelloListener` inspects the ClientHello before TLS:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(5001, listenOptions =>
    {
        listenOptions.Use(next => async context =>
        {
            await next(context);

            var tlsHandshakeFeature = context.Features.Get<ITlsHandshakeFeature>();
            if (tlsHandshakeFeature?.Exception is { } ex)
            {
                Console.WriteLine($"[TLS Handshake Failed] ConnectionId={context.ConnectionId}, Exception={ex.GetType().Name}: {ex.Message}");
            }
        });

        // UseTlsClientHelloListener must be called before UseHttps()
        listenOptions.UseTlsClientHelloListener((connection, clientHelloBytes) =>
        {
            Console.WriteLine($"TLS Client Hello received on {connection.ConnectionId}, {clientHelloBytes.Length} bytes");
        });
        listenOptions.UseHttps();
    });
});
```

### Response compression always emits `Vary: Accept-Encoding`

The response-compression middleware now adds `Vary: Accept-Encoding` to every response when compression is enabled, even when the response itself isn't compressed. This prevents shared caches and CDNs from serving a compressed payload to a client that didn't ask for one (or vice versa).

Thank you [@pedrobsaila](https://github.com/pedrobsaila) for this contribution!

### Runtime-async enabled for shared framework libraries

ASP.NET Core's shared-framework-only libraries are now compiled with the `runtime-async` feature on `net11.0+`. Runtime-async lets the runtime, rather than the C# compiler, generate the state machine for `async`/`await`, which can reduce per-await allocations and improve diagnostics. This is an internal codegen change with no public API impact — apps targeting `net11.0` automatically benefit when they call into the affected ASP.NET Core libraries.

Libraries that ship as both shared-framework members and standalone NuGet packages are excluded, because runtime-async is incompatible with WebAssembly and would otherwise break Wasm consumers of those packages.

Because runtime-async changes how `async`/`await` is generated for a large portion of the ASP.NET Core stack, try your apps against this preview and [file an issue](https://github.com/dotnet/aspnetcore/issues/new/choose) if you hit unexpected behavior, particularly around exception stacks, `ExecutionContext`/`AsyncLocal` flow, or anything that looks like a regression from .NET 10.

### Rate-limiting middleware returns accurate `Retry-After` headers

The [FixedWindowRateLimiter](/en-us/dotnet/api/system.threading.ratelimiting.fixedwindowratelimiter) now reports a [RetryAfter](/en-us/dotnet/api/system.threading.ratelimiting.metadataname.retryafter#system-threading-ratelimiting-metadataname-retryafter) metadata value that accurately reflects the next window boundary. Apps that propagate this metadata to the `Retry-After` response header in their [OnRejected](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.ratelimiteroptions.onrejected#microsoft-aspnetcore-ratelimiting-ratelimiteroptions-onrejected) callback now produce correct retry intervals automatically, with no code changes required.

Additional fixes in `System.Threading.RateLimiting` resolve an issue where [TokenBucketRateLimiter](/en-us/dotnet/api/system.threading.ratelimiting.tokenbucketratelimiter) mishandled partial token refills during zero-permit acquisition, and improve the chained rate limiter returned by [CreateChained](/en-us/dotnet/api/system.threading.ratelimiting.ratelimiter.createchained) to correctly forward idle-duration and replenishment behavior from its inner limiters.

For an overview of the rate limiting middleware, see [Rate limiting middleware in ASP.NET Core](/en-us/aspnet/core/performance/rate-limit).

Thank you [@asbjornvad](https://github.com/asbjornvad) and [@apoorvdarshan](https://github.com/apoorvdarshan) for these contributions!

### Kestrel applies trailer header timeouts

Kestrel now applies [RequestHeadersTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.requestheaderstimeout#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-requestheaderstimeout) to fragmented HTTP/2 and HTTP/3 trailer headers that don't finish sending the header block. The same timeout that protects initial request headers now also prevents connections from staying open indefinitely while Kestrel waits for trailer `HEADERS` frames to complete.

```
builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(10);
});
```

### TLS channel-binding token access from `ITlsConnectionFeature`

Applications using TLS can read the connection's channel binding token to defend against relay attacks:

```
using System.Security.Authentication.ExtendedProtection;

app.Use(async (context, next) =>
{
    var tls = context.Features.Get<ITlsConnectionFeature>();
    if (tls is not null && tls.TryGetChannelBindingBytes(
            ChannelBindingKind.Endpoint,
            out ReadOnlyMemory<byte> cbt))
    {
        // Compare cbt against the token the client presented during authentication.
    }

    await next(context);
});
```

Kestrel returns the binding from `SslStream.TransportContext.GetChannelBinding`. IIS and HTTP.sys return it from the request. On HTTP.sys, `HttpSysOptions.HttpAuthenticationHardeningLevel` controls Extended Protection and channel-binding token exposure:

* `Legacy` disables channel-binding validation and doesn't expose the token.
* `Medium`, the default, exposes the token and validates it when supplied, but tolerates its absence.
* `Strict` requires the token for authenticated requests and rejects requests without one. It also fails startup if the OS can't apply the configuration, while `Legacy` and `Medium` log the configuration failure and continue.

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-19

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/tools/dotnet-aspnet-codegenerator
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/tools/dotnet-aspnet-codegenerator.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core code generator tool (`aspnet-codegenerator`)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The `dotnet aspnet-codegenerator` command runs the ASP.NET Core scaffolding engine. Running the `dotnet aspnet-codegenerator` command is required to scaffold from the command line or when using Visual Studio Code. The command isn't required to use scaffolding with Visual Studio, which includes the scaffolding engine by default.

## Install and update the code generator tool

Install the [.NET SDK](https://dotnet.microsoft.com/download).

`dotnet aspnet-codegenerator` is a [global tool](/en-us/dotnet/core/tools/global-tools) that must be installed. The following command installs the latest stable version of the ASP.NET Core code generator tool:

```
dotnet tool install -g dotnet-aspnet-codegenerator
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

If the tool is already installed, the following command updates the tool to the latest stable version available from the installed .NET SDKs:

```
dotnet tool update -g dotnet-aspnet-codegenerator
```

## Uninstall the code generator tool

It may be necessary to uninstall the ASP.NET Core code generator tool to resolve problems. For example, if you installed a preview version of the tool, uninstall it before installing the released version.

The following commands uninstall the ASP.NET Core code generator tool and install the latest stable version:

```
dotnet tool uninstall -g dotnet-aspnet-codegenerator
dotnet tool install -g dotnet-aspnet-codegenerator
```

## Synopsis

```
dotnet aspnet-codegenerator [arguments] [-b|--build-base-path] [-c|--configuration] [-n|--nuget-package-dir] [--no-build] [-p|--project] [-tfm|--target-framework]
dotnet aspnet-codegenerator [-h|--help]
```

## Description

The `dotnet aspnet-codegenerator` global command runs the ASP.NET Core code generator and scaffolding engine.

## Arguments

`generator`

The code generator to run. The available generators are shown in the following table.

| Generator         | Operation                                                                                                                                     |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `area`            | [Scaffolds an area](../../mvc/controllers/areas?view=aspnetcore-10.0).                                                                        |
| `blazor`          | [Scaffolds Blazor create, read, update, delete, and list pages](../../blazor/components/quickgrid?view=aspnetcore-10.0#quickgrid-scaffolder). |
| `blazor-identity` | Generates Blazor Identity files.                                                                                                              |
| `controller`      | [Scaffolds a controller](../../tutorials/first-mvc-app/adding-model?view=aspnetcore-10.0).                                                    |
| `identity`        | [Scaffolds Identity](../../security/authentication/scaffold-identity?view=aspnetcore-10.0).                                                   |
| `minimalapi`      | Generates an endpoints file (with CRUD API endpoints) given a model and optional database context.                                            |
| `razorpage`       | [Scaffolds Razor Pages](../../tutorials/razor-pages/model?view=aspnetcore-10.0).                                                              |
| `view`            | [Scaffolds a view](../../mvc/views/overview?view=aspnetcore-10.0).                                                                            |

| Generator    | Operation                                                                                          |
| ------------ | -------------------------------------------------------------------------------------------------- |
| `area`       | [Scaffolds an area](../../mvc/controllers/areas?view=aspnetcore-10.0).                             |
| `controller` | [Scaffolds a controller](../../tutorials/first-mvc-app/adding-model?view=aspnetcore-10.0).         |
| `identity`   | [Scaffolds Identity](../../security/authentication/scaffold-identity?view=aspnetcore-10.0).        |
| `minimalapi` | Generates an endpoints file (with CRUD API endpoints) given a model and optional database context. |
| `razorpage`  | [Scaffolds Razor Pages](../../tutorials/razor-pages/model?view=aspnetcore-10.0).                   |
| `view`       | [Scaffolds a view](../../mvc/views/overview?view=aspnetcore-10.0).                                 |

## Options

`-b|--build-base-path`

The build base path.

`-c|--configuration {Debug|Release}`

Defines the build configuration. The default value is `Debug`.

`-h|--help`

Prints out a short help for the command.

`-n|--nuget-package-dir`

Specifies the NuGet package directory.

`--no-build`

Doesn't build the project before running. Passing `--no-build` also implicitly sets the `--no-restore` flag.

`-p|--project <PATH>`

Specifies the path of the project file to run (folder name or full path). If not specified, the tool defaults to the current directory.

`-tfm|--target-framework`

The target [framework](/en-us/dotnet/standard/frameworks) to use.

## Generator options

The following sections detail the options available for the supported generators:

* [Area (`area`)](#area-options)
* [Controller (`controller`)](#controller-options)
* [Blazor (`blazor`)](#blazor-options)
* [Blazor Identity (`blazor-identity`)](#blazor-identity-options)
* [Identity (`identity`)](#identity-options)
* [Minimal API (`minimalapi`)](#minimal-api-options)
* [Razor page (`razorpage`)](#razor-page-options)
* [View (`view`)](#view-options)

- [Area (`area`)](#area-options)
- [Controller (`controller`)](#controller-options)
- [Identity (`identity`)](#identity-options)
- [Minimal API (`minimalapi`)](#minimal-api-options)
- [Razor page (`razorpage`)](#razor-page-options)
- [View (`view`)](#view-options)

### Area options

Usage: `dotnet aspnet-codegenerator area {AREA NAME}`

The `{AREA NAME}` placeholder is the name of the area to generate.

The preceding command generates the following folders:

* `Areas`

  * `{AREA NAME}`

    * `Controllers`
    * `Data`
    * `Models`
    * `Views`

Use the `-h|--help` option for help:

```
dotnet aspnet-codegenerator area -h
```

### Blazor options

Razor components can be individually scaffolded for Blazor apps by specifying the name of the template to use. The supported templates are:

* `Empty`
* `Create`
* `Edit`
* `Delete`
* `Details`
* `List`
* `CRUD`: *CRUD* is an acronym for Create, Read, Update, and Delete. The `CRUD` template produces `Create`, `Edit`, `Delete`, `Details`, and `Index` (`List`) components for the app.

The options for the `blazor` generator are shown in the following table.

| Option                            | Description                                                                                         |
| --------------------------------- | --------------------------------------------------------------------------------------------------- |
| `-dbProvider\|--databaseProvider` | Database provider to use. Options include `sqlserver` (default), `sqlite`, `cosmos`, or `postgres`. |
| `-dc\|--dataContext`              | Database context class to use.                                                                      |
| `-m\|--model`                     | Model class to use.                                                                                 |
| `-ns\|--namespaceName`            | Specify the name of the namespace to use for the generated Endpoints file.                          |
| `--relativeFolderPath\|-outDir`   | Relative output folder path. If not specified, files are generated in the project folder.           |

The following example:

* Uses the `Edit` template to generate an `Edit` component (`Edit.razor`) in the `Components/Pages/MoviePages` folder of the app. If the `MoviePages` folder doesn't exist, the tool creates the folder automatically.
* Uses the SQLite database provider.
* Uses `BlazorWebAppMovies.Data.BlazorWebAppMoviesContext` for the database context.
* Uses the `Movie` model.

```
dotnet aspnet-codegenerator blazor Edit -dbProvider sqlite -dc BlazorWebAppMovies.Data.BlazorWebAppMoviesContext -m Movie -outDir Components/Pages
```

Use the `-h|--help` option for help:

```
dotnet aspnet-codegenerator blazor -h
```

For an example that uses the `blazor` generator, see [Build a Blazor movie database app (Overview)](../../blazor/tutorials/movie-database-app/?view=aspnetcore-10.0).

For more information, see [ASP.NET Core Blazor \`QuickGrid\` component](../../blazor/components/quickgrid?view=aspnetcore-10.0#quickgrid-scaffolder).

### Blazor Identity options

Scaffold Identity Razor components into a Blazor app with the `blazor-identity` generator.

The options for the `blazor-identity` template are shown in the following table.

| Option                            | Description                                                                                                    |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `-dbProvider\|--databaseProvider` | Database provider to use. Options include `sqlserver` (default) and `sqlite`.                                  |
| `-dc\|--dataContext`              | Database context class to use.                                                                                 |
| `-f\|--force`                     | Use this option to overwrite existing files.                                                                   |
| `-fi\|--files`                    | List of semicolon separated files to scaffold. Use the `-lf\|--listFiles` option to see the available options. |
| `-lf\|--listFiles`                | Lists the files that can be scaffolded by using the `-fi\|--files` option.                                     |
| `-rn\|--rootNamespace`            | Root namespace to use for generating Identity code.                                                            |
| `-u\|--userClass`                 | Name of the user class to generate.                                                                            |

Use the `-h|--help` option for help:

```
dotnet aspnet-codegenerator blazor-identity -h
```

### Controller options

General options are shown in the following table.

| Option                                 | Description                                                                                                          |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `-b\|--bootstrapVersion`               | Specifies the bootstrap version and creates a `wwwroot` folder for the Bootstrap assets if the folder isn't present. |
| `-dbProvider\|--databaseProvider`      | Database provider to use. Options include `sqlserver` (default), `sqlite`, `cosmos`, `postgres`.                     |
| `-dc\|--dataContext`                   | The database context class to use or the name of the class to generate.                                              |
| `-f\|--force`                          | Overwrite existing files.                                                                                            |
| `-l\|--layout`                         | Custom layout page to use.                                                                                           |
| `-m\|--model`                          | Model class to use.                                                                                                  |
| `-outDir\|--relativeFolderPath`        | Relative output folder path. If not specified, files are generated in the project folder.                            |
| `-scripts\|--referenceScriptLibraries` | Reference script libraries in the generated views. Adds `_ValidationScriptsPartial` to `Edit` and `Create` pages.    |
| `-sqlite\|--useSqlite`                 | Flag to specify if the database context should use SQLite instead of SQL Server.                                     |
| `-udl\|--useDefaultLayout`             | Use the default layout for the views.                                                                                |

The options unique to `controller` are shown in the following table.

| Option                              | Description                                                                                               |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `-actions\|--readWriteActions`      | Generate controller with read/write actions without a model.                                              |
| `-api\|--restWithNoViews`           | Generate a controller with REST style API. `noViews` is assumed and any view related options are ignored. |
| `-async\|--useAsyncActions`         | Generate asynchronous controller actions.                                                                 |
| `-name\|--controllerName`           | Name of the controller.                                                                                   |
| `-namespace\|--controllerNamespace` | Specify the name of the namespace to use for the generated controller.                                    |
| `-nv\|--noViews`                    | Generate **no** views.                                                                                    |

Use the `-h|--help` option for help:

```
dotnet aspnet-codegenerator controller -h
```

For an example that uses the `controller` generator, see [Part 4, add a model to an ASP.NET Core MVC app](../../tutorials/first-mvc-app/adding-model?view=aspnetcore-10.0).

### Identity options

For more information, see [Scaffold Identity in ASP.NET Core projects](../../security/authentication/scaffold-identity?view=aspnetcore-10.0).

### Minimal API options

Scaffold a Minimal API backend with the `minimalapi` template.

The options for `minimalapi` are shown in the following table.

| Option                             | Description                                                                                         |
| ---------------------------------- | --------------------------------------------------------------------------------------------------- |
| `-dbProvider\|--databaseProvider`  | Database provider to use. Options include `sqlserver` (default), `sqlite`, `cosmos`, or `postgres`. |
| `-dc\|--dataContext`               | Database context class to use.                                                                      |
| `-e\|--endpoints`                  | Endpoints class to use (not the file name).                                                         |
| `-m\|--model`                      | Model class to use.                                                                                 |
| `-namespace\|--endpointsNamespace` | Specify the name of the namespace to use for the generated endpoints file.                          |
| `-o\|--open`                       | Use this option to enable OpenAPI.                                                                  |
| `-outDir\|--relativeFolderPath`    | Relative output folder path. If not specified, files are generated in the project folder.           |
| `-sqlite\|--useSqlite`             | Flag to specify if the database context should use SQLite instead of SQL Server.                    |

The following example:

* Generates an endpoints class named `SpeakersEndpoints` with API endpoints that map to database operations using the `ApplicationDbContext` database context class and the `BackEnd.Models.Speaker` model.
* Adds `app.MapSpeakerEndpoints();` to the `Program` file (`Program.cs`) to register the endpoints class.

```
dotnet aspnet-codegenerator minimalapi -dc ApplicationDbContext -e SpeakerEndpoints -m BackEnd.Models.Speaker -o
```

Use the `-h|--help` option for help:

```
dotnet aspnet-codegenerator minimalapi -h
```

### Razor page options

Razor Pages can be individually scaffolded by specifying the name of the new page and the template to use. The supported templates are:

* `Empty`
* `Create`
* `Edit`
* `Delete`
* `Details`
* `List`

Typically, the template and generated file name isn't specified, which creates the following templates:

* `Create`
* `Edit`
* `Delete`
* `Details`
* `List`

General options are shown in the following table.

| Option                                 | Description                                                                                                          |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `-b\|--bootstrapVersion`               | Specifies the bootstrap version and creates a `wwwroot` folder for the Bootstrap assets if the folder isn't present. |
| `-dbProvider\|--databaseProvider`      | Database provider to use. Options include `sqlserver` (default), `sqlite`, `cosmos`, `postgres`.                     |
| `-dc\|--dataContext`                   | The database context class to use or the name of the class to generate.                                              |
| `-f\|--force`                          | Overwrite existing files.                                                                                            |
| `-l\|--layout`                         | Custom layout page to use.                                                                                           |
| `-m\|--model`                          | Model class to use.                                                                                                  |
| `-outDir\|--relativeFolderPath`        | Relative output folder path. If not specified, files are generated in the project folder.                            |
| `-scripts\|--referenceScriptLibraries` | Reference script libraries in the generated views. Adds `_ValidationScriptsPartial` to `Edit` and `Create` pages.    |
| `-sqlite\|--useSqlite`                 | Flag to specify if the database context should use SQLite instead of SQL Server.                                     |
| `-udl\|--useDefaultLayout`             | Use the default layout for the views.                                                                                |

The options unique to `razorpage` are shown in the following table.

| Option                        | Description                                                                               |
| ----------------------------- | ----------------------------------------------------------------------------------------- |
| `-namespace\|--namespaceName` | The name of the namespace to use for the generated `PageModel` class.                     |
| `-npm\|--noPageModel`         | Don't generate a `PageModel` class for the `Empty` template.                              |
| `-partial\|--partialView`     | Generate a partial view. Layout options `-l` and `-udl` are ignored if this is specified. |

The following example uses the `Edit` template to generate `CustomEditPage.cshtml` and `CustomEditPage.cshtml.cs` in the `Pages/Movies` folder:

```
dotnet aspnet-codegenerator razorpage CustomEditPage Edit -dc RazorPagesMovieContext -m Movie -outDir Pages/Movies
```

Use the `-h|--help` option for help:

```
dotnet aspnet-codegenerator razorpage -h
```

For an example that uses the `razorpage` generator, see [Part 2, add a model](../../tutorials/razor-pages/model?view=aspnetcore-10.0).

### View options

Views can be individually scaffolded by specifying the name of the view and the template. The supported templates are:

* `Empty`
* `Create`
* `Edit`
* `Delete`
* `Details`
* `List`

General options are shown in the following table.

| Option                                 | Description                                                                                                          |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `-b\|--bootstrapVersion`               | Specifies the bootstrap version and creates a `wwwroot` folder for the Bootstrap assets if the folder isn't present. |
| `-dbProvider\|--databaseProvider`      | Database provider to use. Options include `sqlserver` (default), `sqlite`, `cosmos`, `postgres`.                     |
| `-dc\|--dataContext`                   | The database context class to use or the name of the class to generate.                                              |
| `-f\|--force`                          | Overwrite existing files.                                                                                            |
| `-l\|--layout`                         | Custom layout page to use.                                                                                           |
| `-m\|--model`                          | Model class to use.                                                                                                  |
| `-outDir\|--relativeFolderPath`        | Relative output folder path. If not specified, files are generated in the project folder.                            |
| `-scripts\|--referenceScriptLibraries` | Reference script libraries in the generated views. Adds `_ValidationScriptsPartial` to `Edit` and `Create` pages.    |
| `-sqlite\|--useSqlite`                 | Flag to specify if the database context should use SQLite instead of SQL Server.                                     |
| `-udl\|--useDefaultLayout`             | Use the default layout for the views.                                                                                |

The options unique to `view` are shown in the following table.

| Option                              | Description                                                                                       |
| ----------------------------------- | ------------------------------------------------------------------------------------------------- |
| `-namespace\|--controllerNamespace` | Specify the name of the namespace to use for the generated controller.                            |
| `-partial\|--partialView`           | Generate a partial view. Other layout options (`-l` and `-udl`) are ignored if this is specified. |

The following example uses the `Edit` template to generate `CustomEditView.cshtml` in the `Views/Movies` folder:

```
dotnet aspnet-codegenerator view CustomEditView Edit -dc MovieContext -m Movie -outDir Views/Movies
```

Use the `-h|--help` option for help:

```
dotnet aspnet-codegenerator view -h
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/http-requests.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Make HTTP requests with IHttpClientFactory in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Kirk Larkin](https://github.com/serpent5), [Steve Gordon](https://github.com/stevejgordon), [Glenn Condron](https://github.com/glennc), and [Ryan Nowak](https://github.com/rynowak).

You can register the [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) and use it to configure and create [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances in an app. The `IHttpClientFactory` interface offers the following benefits:

* Provides a central location for naming and configuring logical `HttpClient` instances. For example, a client named *github* can be registered and configured to access [GitHub](https://github.com/). A default client can be registered for general access.

* Codifies the concept of outgoing middleware via delegating handlers in the `HttpClient` instance. Provides extensions for Polly-based middleware to take advantage of delegating handlers in `HttpClient`.

* Manages the pooling and lifetime of underlying `HttpClientMessageHandler` instances. Automatic management avoids common DNS (Domain Name System) problems that occur when manually managing `HttpClient` lifetimes.

* Adds a configurable logging experience (via the `ILogger`) for all requests sent through clients created by the factory.

This article describes how to make HTTP requests by using `IHttpClientFactory` in ASP.NET Core applications. The sample code version uses [System.Text.Json](/en-us/dotnet/api/system.text.json) to deserialize JSON content returned in HTTP responses. For samples that use `Json.NET` and `ReadAsAsync<T>`, use the browser **Version** selector to select a 2.x version of this article.

## Review consumption patterns

There are several ways `IHttpClientFactory` can be used in an app:

* [Basic usage](#basic-usage)
* [Named clients](#named-clients)
* [Typed clients](#typed-clients)
* [Generated clients](#generated-clients)

The best approach depends upon the app's requirements.

### Basic usage

Register `IHttpClientFactory` by calling the `AddHttpClient` method in the *Program.cs* file:

```
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHttpClient();
```

An `IHttpClientFactory` can be requested by using [dependency injection (DI)](dependency-injection?view=aspnetcore-10.0). The following code uses `IHttpClientFactory` to create an `HttpClient` instance:

```
public class BasicModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public BasicModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { HeaderNames.Accept, "application/vnd.github.v3+json" },
                { HeaderNames.UserAgent, "HttpRequestsSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}
```

In the example, the call to `IHttpClientFactory` is a good way to refactor an existing app. This approach has no effect on how `HttpClient` is used.

In scenarios where `HttpClient` instances are created in an existing app, replace the occurrences with calls to the [CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient).

### Named clients

Named clients are suitable for the following scenarios:

* The app requires many distinct uses of `HttpClient`.
* There are many `HttpClient` instances with different configurations.

Specify the configuration for a named `HttpClient` instance during its registration in the *Program.cs* file:

```
builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // using Microsoft.Net.Http.Headers;
    // The GitHub API requires two headers.
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.UserAgent, "HttpRequestsSample");
});
```

In the code snippet, the client is configured with:

* The base address `https://api.github.com/`.
* Two headers required to work with the GitHub API.

#### CreateClient method

For each call to the [CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient):

* A new instance of `HttpClient` is created.
* The configuration action is called.

To create a named client, pass the client name into the `CreateClient` method:

```
public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public NamedClientModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var httpResponseMessage = await httpClient.GetAsync(
            "repos/dotnet/AspNetCore.Docs/branches");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}
```

In the code snippet, the request doesn't need to specify a hostname. The code can pass the path only because the base address configured for the client is used.

### Typed clients

Typed clients provide several benefits:

* Access to the same capabilities as named clients without the need to use strings as keys.
* IntelliSense and compiler help when consuming clients.
* The ability to define a single location to configure and interact with a particular `HttpClient` instance. For example, a single typed client might be used for a single backend endpoint, or to encapsulate all logic dealing with the endpoint.
* Support for DI and injection where the app requires the functionality.

A typed client accepts an `HttpClient` parameter in its constructor:

```
public class GitHubService
{
    private readonly HttpClient _httpClient;

    public GitHubService(HttpClient httpClient)
    {
        _httpClient = httpClient;

        _httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    }

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
        await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
            "repos/dotnet/AspNetCore.Docs/branches");
}
```

In the code snippet:

* The configuration is moved into the typed client.
* The provided `HttpClient` instance is stored as a private field.

API-specific methods can be created that expose `HttpClient` functionality. For example, the `GetAspNetCoreDocsBranches` method encapsulates code to retrieve docs GitHub branches.

The following code calls [AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) in the *Program.cs* file to register the `GitHubService` typed client class:

```
builder.Services.AddHttpClient<GitHubService>();
```

The typed client is registered as transient with DI. In the snippet, the `AddHttpClient` method registers `GitHubService` as a transient service. This registration uses a factory method to:

* Create an instance of `HttpClient`.
* Create an instance of `GitHubService` and passing in the instance of `HttpClient` to its constructor.

The typed client can be injected and consumed directly:

```
public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public TypedClientModel(GitHubService gitHubService) =>
        _gitHubService = gitHubService;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
        }
        catch (HttpRequestException)
        {
            // ...
        }
    }
}
```

The configuration for a typed client can also be specified during its registration in the *Program.cs* file, rather than in the typed client's constructor:

```
builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // ...
});
```

### Generated clients

The `IHttpClientFactory` interface can be used in combination with third-party libraries such as [Refit](https://github.com/reactiveui/refit). Refit is a REST library for .NET that converts REST APIs into live interfaces. Call the `AddRefitClient` method to generate a dynamic implementation of an interface. The method uses `HttpClient` to make the external HTTP calls.

A custom interface represents the external API:

```
public interface IGitHubClient
{
    [Get("/repos/dotnet/AspNetCore.Docs/branches")]
    Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}
```

Call `AddRefitClient` to generate the dynamic implementation, and then call the `ConfigureHttpClient` method to configure the underlying `HttpClient` instance:

```
builder.Services.AddRefitClient<IGitHubClient>()
    .ConfigureHttpClient(httpClient =>
    {
        httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    });
```

Use DI to access the dynamic implementation of `IGitHubClient`:

```
public class RefitModel : PageModel
{
    private readonly IGitHubClient _gitHubClient;

    public RefitModel(IGitHubClient gitHubClient) =>
        _gitHubClient = gitHubClient;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
        }
        catch (ApiException)
        {
            // ...
        }
    }
}
```

## Make POST, PUT, and DELETE requests

In the preceding examples, all HTTP requests use the GET HTTP verb. `HttpClient` also supports other HTTP verbs, including:

* POST
* PUT
* DELETE
* PATCH

For a complete list of supported HTTP verbs, see the [HttpMethod](/en-us/dotnet/api/system.net.http.httpmethod).

The following example shows how to make an HTTP POST request:

```
public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json); // using static System.Net.Mime.MediaTypeNames;

    using var httpResponseMessage =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}
```

In the preceding code, the `CreateItemAsync` method:

* Serializes the `TodoItem` parameter to JSON by using `System.Text.Json`.
* Creates an instance of [StringContent](/en-us/dotnet/api/system.net.http.stringcontent) to package the serialized JSON for sending in the HTTP request body.
* Calls the [PostAsync](/en-us/dotnet/api/system.net.http.httpclient.postasync) to send the JSON content to the specified URL. The value is a relative URL that is added to the [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress).
* Calls the [EnsureSuccessStatusCode](/en-us/dotnet/api/system.net.http.httpresponsemessage.ensuresuccessstatuscode) to throw an exception if the response status code doesn't indicate success.

`HttpClient` also supports other types of content. For example, [MultipartContent](/en-us/dotnet/api/system.net.http.multipartcontent) and [StreamContent](/en-us/dotnet/api/system.net.http.streamcontent). For a complete list of supported content, see [HttpContent](/en-us/dotnet/api/system.net.http.httpcontent).

The following example shows an HTTP PUT request:

```
public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json);

    using var httpResponseMessage =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}
```

The code snippet is similar to the POST example. The `SaveItemAsync` method calls the [PutAsync](/en-us/dotnet/api/system.net.http.httpclient.putasync) instead of `PostAsync`.

The following example shows an HTTP DELETE request:

```
public async Task DeleteItemAsync(long itemId)
{
    using var httpResponseMessage =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponseMessage.EnsureSuccessStatusCode();
}
```

In the code snippet, the `DeleteItemAsync` method calls [DeleteAsync](/en-us/dotnet/api/system.net.http.httpclient.deleteasync). Because HTTP DELETE requests typically contain no body, the `DeleteAsync` method doesn't provide an overload that accepts an instance of `HttpContent`.

To learn more about using different HTTP verbs with `HttpClient`, see [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).

## Use outgoing request middleware

`HttpClient` has the concept of delegating handlers that can be linked together for outgoing HTTP requests. The `IHttpClientFactory` interface provides the following benefits:

* Define the handlers to apply for each named client by using a simplified process.
* Register and chain multiple handlers to build an outgoing request middleware pipeline. Each handler can perform work before and after the outgoing request. This pattern is similar to the inbound middleware pipeline in ASP.NET Core.
* Manage cross-cutting concerns around HTTP requests, such as caching, error handling, serialization, and logging.

To create a delegating handler:

* Derive from the [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler).
* Override the [SendAsync](/en-us/dotnet/api/system.net.http.delegatinghandler.sendasync). Execute code before passing the request to the next handler in the pipeline:

```
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "The API key header X-API-KEY is required.")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}
```

The code snippet checks if the `X-API-KEY` header is in the request. If `X-API-KEY` is missing, the code returns the [BadRequest](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-badrequest) status code.

More than one handler can be added to the configuration for an `HttpClient` instance by using the [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler):

```
builder.Services.AddTransient<ValidateHeaderHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<ValidateHeaderHandler>();
```

In the code snippet, `ValidateHeaderHandler` is registered with DI as a transient service. A delegating handler **must** be registered in DI as a transient service, rather than singleton or scoped. Delegating handlers can't be reused across multiple HTTP client pipelines, and registering a handler as a singleton results in an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) when the handler is reused. After registration, [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler) can be called, passing in the type for the handler.

Multiple handlers can be registered in the order they should execute. Each handler wraps the next handler until the final `HttpClientHandler` executes the request:

```
builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();

builder.Services.AddHttpClient("MultipleHttpMessageHandlers")
    .AddHttpMessageHandler<SampleHandler1>()
    .AddHttpMessageHandler<SampleHandler2>();
```

In the code snippet, `SampleHandler1` runs first, before `SampleHandler2`.

### Use DI in outgoing request middleware

When `IHttpClientFactory` creates a new delegating handler, it uses DI to fulfill the handler's constructor parameters. `IHttpClientFactory` creates a **separate** DI scope for each handler, which can lead to surprising behavior when a handler consumes a *scoped* service.

Consider the following interface and its implementation, which represents a task as an operation with an identifier, `OperationId`:

```
public interface IOperationScoped
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
```

As the name suggests, `IOperationScoped` is registered with DI by using a *scoped* lifetime:

```
builder.Services.AddScoped<IOperationScoped, OperationScoped>();
```

The following delegating handler consumes and uses `IOperationScoped` to set the `X-OPERATION-ID` header for the outgoing request:

```
public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationScoped;

    public OperationHandler(IOperationScoped operationScoped) =>
        _operationScoped = operationScoped;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationScoped.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}
```

In the [HttpRequestsSample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/http-requests/samples/6.x/HttpRequestsSample), run the app and browse to the /Operation page, then refresh. The request scope value changes for each request, but the handler scope value only changes every 5 seconds.

Handlers can depend upon services of any scope. Services that handlers depend upon are disposed when the handler is disposed.

Use one of the following approaches to share per-request state with message handlers:

* Pass data into the handler by using the [Options](/en-us/dotnet/api/system.net.http.httprequestmessage.options#system-net-http-httprequestmessage-options).
* Use the [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) to access the current request.
* Create a custom [AsyncLocal\<T>](/en-us/dotnet/api/system.threading.asynclocal-1) storage object for passing the data.

## Use Polly-based handlers

The `IHttpClientFactory` interface integrates with the third-party library [Polly](https://github.com/App-vNext/Polly). Polly is a comprehensive resilience and transient fault-handling library for .NET. It allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.

Extension methods are provided to enable the use of Polly policies with configured `HttpClient` instances. The Polly extensions support adding Polly-based handlers to clients. Polly requires the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly/) NuGet package.

### Handle transient faults

Faults typically occur when external HTTP calls are transient. [AddTransientHttpErrorPolicy](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addtransienthttperrorpolicy) allows a policy to be defined to handle transient errors. Policies configured with the `AddTransientHttpErrorPolicy` method handle the following responses:

* [HttpRequestException](/en-us/dotnet/api/system.net.http.httprequestexception)
* HTTP 5xx
* HTTP 408

`AddTransientHttpErrorPolicy` provides access to a `PolicyBuilder` object configured to handle errors representing a possible transient fault:

```
builder.Services.AddHttpClient("PollyWaitAndRetry")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.WaitAndRetryAsync(
            3, retryNumber => TimeSpan.FromMilliseconds(600)));
```

In the code snippet, a `WaitAndRetryAsync` policy is defined. Failed requests are retried up to three times with a delay of 600 ms between attempts.

### Dynamically select policies

Extension methods are provided to add Polly-based handlers, for example, the [AddPolicyHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addpolicyhandler). The following `AddPolicyHandler` overload inspects the request to decide which policy to apply:

```
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient("PollyDynamic")
    .AddPolicyHandler(httpRequestMessage =>
        httpRequestMessage.Method == HttpMethod.Get ? timeoutPolicy : longTimeoutPolicy);
```

In the code snippet, if the outgoing request is an HTTP GET, a 10-second timeout is applied. For any other HTTP method, a 30-second timeout is used.

### Add multiple Polly handlers

It's common practice to nest Polly policies:

```
builder.Services.AddHttpClient("PollyMultiple")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.RetryAsync(3))
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
```

This example adds two handlers:

* The first handler uses the [AddTransientHttpErrorPolicy](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addtransienthttperrorpolicy) to add a retry policy. Failed requests are retried up to three times.

* The second `AddTransientHttpErrorPolicy` call adds a circuit breaker policy. Further external requests are blocked for 30 seconds if five failed attempts occur sequentially. Circuit breaker policies are stateful. All calls through this client share the same circuit state.

### Add policies from the Polly registry

An approach to managing regularly used policies is to define them once and register them with a `PolicyRegistry`. For example:

```
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

var policyRegistry = builder.Services.AddPolicyRegistry();

policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);

builder.Services.AddHttpClient("PollyRegistryRegular")
    .AddPolicyHandlerFromRegistry("Regular");

builder.Services.AddHttpClient("PollyRegistryLong")
    .AddPolicyHandlerFromRegistry("Long");
```

The code snippet adds two policies to the Polly registry, `Regular` and `Long`. The code calls the [AddPolicyHandlerFromRegistry](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addpolicyhandlerfromregistry) to configure individual named clients to use these policies from the Polly registry.

For more information on `IHttpClientFactory` and Polly integrations, see the [Polly wiki](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory).

## HttpClient and lifetime management

A new `HttpClient` instance is returned each time `CreateClient` is called on the `IHttpClientFactory`. An [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) is created for each named client. The factory manages the lifetimes of the `HttpMessageHandler` instances.

`IHttpClientFactory` pools the `HttpMessageHandler` instances created by the factory to reduce resource consumption. An `HttpMessageHandler` instance might be reused from the pool when creating a new `HttpClient` instance, if its lifetime isn't expired.

Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connections. Creating more handlers than necessary can result in connection delays. Some handlers also keep connections open indefinitely, which can prevent the handler from reacting to DNS changes.

The default handler lifetime is 2 minutes. The default value can be overridden on a per-named client basis:

```
builder.Services.AddHttpClient("HandlerLifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));
```

`HttpClient` instances can generally be treated as .NET objects that **don't** require disposal. Disposal cancels outgoing requests and guarantees the given `HttpClient` instance can't be used after calling [Dispose](/en-us/dotnet/api/system.idisposable.dispose). `IHttpClientFactory` tracks and disposes resources used by `HttpClient` instances.

Keeping a single `HttpClient` instance alive for a long duration is a common pattern used before the inception of `IHttpClientFactory`. This pattern becomes unnecessary after migrating to `IHttpClientFactory`.

### Explore alternatives to IHttpClientFactory

Using `IHttpClientFactory` in a DI-enabled app avoids:

* Resource exhaustion problems by pooling `HttpMessageHandler` instances.
* Stale DNS problems by cycling `HttpMessageHandler` instances at regular intervals.

There are alternative ways to solve the preceding problems by using a long-lived [SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler) instance.

* Create an instance of `SocketsHttpHandler` when the app starts and use it for the life of the app.
* Configure [PooledConnectionLifetime](/en-us/dotnet/api/system.net.http.socketshttphandler.pooledconnectionlifetime#system-net-http-socketshttphandler-pooledconnectionlifetime) to an appropriate value based on DNS refresh times.
* Create `HttpClient` instances by using `new HttpClient(handler, disposeHandler: false)` as needed.

The alternate approaches solve the resource management problems that `IHttpClientFactory` solves in a similar way.

* The `SocketsHttpHandler` shares connections across `HttpClient` instances. This sharing prevents socket exhaustion.
* The `SocketsHttpHandler` cycles connections according to `PooledConnectionLifetime` to avoid stale DNS problems.

## Log messages and response status

Clients created via `IHttpClientFactory` record log messages for all requests. Enable the appropriate information level in the logging configuration so you can see the default log messages. Other logging, such as the logging of request headers, is only included at trace level.

The log category used for each client includes the name of the client. A client named *MyNamedClient*, for example, logs messages with a category of "System.Net.Http.HttpClient.**MyNamedClient**.LogicalHandler." Messages that have the *LogicalHandler* suffix occur outside the request handler pipeline. On the request, messages are logged before any other handlers in the pipeline process them. On the response, messages are logged after any other pipeline handlers receive the response.

Logging also occurs inside the request handler pipeline. In the *MyNamedClient* example, those messages are logged with the log category "System.Net.Http.HttpClient.**MyNamedClient**.ClientHandler." For the request, the logging occurs after all other handlers run and immediately before the request is sent. On the response, the logging includes the state of the response before it passes back through the handler pipeline.

Enabling logging outside and inside the pipeline enables inspection of the changes made by the other pipeline handlers. The logging might include changes to request headers or to the response status code.

Including the name of the client in the log category enables log filtering for specific named clients.

## Configure the HttpMessageHandler

It might be necessary to control the configuration of the inner `HttpMessageHandler` used by a client.

An `IHttpClientBuilder` is returned when adding named or typed clients. The [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method can be used to define a delegate. The delegate is used to create and configure the primary `HttpMessageHandler` used by that client:

```
builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            AllowAutoRedirect = true,
            UseDefaultCredentials = true
        });
```

## Work with cookies

The pooled `HttpMessageHandler` instances results in `CookieContainer` objects being shared. Unanticipated `CookieContainer` object sharing often results in incorrect code.

For apps that require cookies, consider:

* Disabling automatic cookie handling
* Avoiding `IHttpClientFactory`

Call [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) to disable automatic cookie handling:

```
builder.Services.AddHttpClient("NoAutomaticCookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            UseCookies = false
        });
```

## Use IHttpClientFactory in a console app

In a console app, add the following package references to the project:

* [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting)
* [Microsoft.Extensions.Http](https://www.nuget.org/packages/Microsoft.Extensions.Http)

In the following example:

* [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) and `GitHubService` are registered in the [Generic Host's](host/generic-host?view=aspnetcore-10.0) service container.
* `GitHubService` is requested from DI, which in-turn requests an instance of `IHttpClientFactory`.
* `GitHubService` uses `IHttpClientFactory` to create an instance of `HttpClient`, which it uses to retrieve docs GitHub branches.

```
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()
    .ConfigureServices(services =>
    {
        services.AddHttpClient();
        services.AddTransient<GitHubService>();
    })
    .Build();

try
{
    var gitHubService = host.Services.GetRequiredService<GitHubService>();
    var gitHubBranches = await gitHubService.GetAspNetCoreDocsBranchesAsync();

    Console.WriteLine($"{gitHubBranches?.Count() ?? 0} GitHub Branches");

    if (gitHubBranches is not null)
    {
        foreach (var gitHubBranch in gitHubBranches)
        {
            Console.WriteLine($"- {gitHubBranch.Name}");
        }
    }
}
catch (Exception ex)
{
    host.Services.GetRequiredService<ILogger<Program>>()
        .LogError(ex, "Unable to load branches from GitHub.");
}

public class GitHubService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public GitHubService(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { "Accept", "application/vnd.github.v3+json" },
                { "User-Agent", "HttpRequestsConsoleSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        httpResponseMessage.EnsureSuccessStatusCode();

        using var contentStream =
            await httpResponseMessage.Content.ReadAsStreamAsync();
        
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubBranch>>(contentStream);
    }
}

public record GitHubBranch(
    [property: JsonPropertyName("name")] string Name);
```

## Use header propagation middleware

Header propagation is an ASP.NET Core middleware to propagate HTTP headers from the incoming request to the outgoing `HttpClient` requests. To use header propagation:

* Install the [Microsoft.AspNetCore.HeaderPropagation](https://www.nuget.org/packages/Microsoft.AspNetCore.HeaderPropagation) package.

* Configure the `HttpClient` instance and middleware pipeline in the *Program.cs* file:

  ```
  // Add services to the container.
  builder.Services.AddControllers();

  builder.Services.AddHttpClient("PropagateHeaders")
      .AddHeaderPropagation();

  builder.Services.AddHeaderPropagation(options =>
  {
      options.Headers.Add("X-TraceId");
  });

  var app = builder.Build();

  // Configure the HTTP request pipeline.
  app.UseHttpsRedirection();

  app.UseHeaderPropagation();

  app.MapControllers();
  ```

* Make outbound requests by using the configured `HttpClient` instance, which includes the added headers.

By [Kirk Larkin](https://github.com/serpent5), [Steve Gordon](https://github.com/stevejgordon), [Glenn Condron](https://github.com/glennc), and [Ryan Nowak](https://github.com/rynowak).

An [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) can be registered and used to configure and create [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances in an app. `IHttpClientFactory` offers the following benefits:

* Provides a central location for naming and configuring logical `HttpClient` instances. For example, a client named *github* could be registered and configured to access [GitHub](https://github.com/). A default client can be registered for general access.
* Codifies the concept of outgoing middleware via delegating handlers in `HttpClient`. Provides extensions for Polly-based middleware to take advantage of delegating handlers in `HttpClient`.
* Manages the pooling and lifetime of underlying `HttpClientMessageHandler` instances. Automatic management avoids common DNS (Domain Name System) problems that occur when manually managing `HttpClient` lifetimes.
* Adds a configurable logging experience (via `ILogger`) for all requests sent through clients created by the factory.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/http-requests/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample)).

The sample code in this topic version uses [System.Text.Json](/en-us/dotnet/api/system.text.json) to deserialize JSON content returned in HTTP responses. For samples that use `Json.NET` and `ReadAsAsync<T>`, use the version selector to select a 2.x version of this topic.

## Consumption patterns

There are several ways `IHttpClientFactory` can be used in an app:

* [Basic usage](#basic-usage)
* [Named clients](#named-clients)
* [Typed clients](#typed-clients)
* [Generated clients](#generated-clients)

The best approach depends upon the app's requirements.

### Basic usage

`IHttpClientFactory` can be registered by calling `AddHttpClient`:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.
```

An `IHttpClientFactory` can be requested using [dependency injection (DI)](dependency-injection?view=aspnetcore-10.0). The following code uses `IHttpClientFactory` to create an `HttpClient` instance:

```
public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}
```

Using `IHttpClientFactory` like in the preceding example is a good way to refactor an existing app. It has no effect on how `HttpClient` is used. In places where `HttpClient` instances are created in an existing app, replace those occurrences with calls to [CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient).

### Named clients

Named clients are a good choice when:

* The app requires many distinct uses of `HttpClient`.
* Many `HttpClient`s have different configuration.

Configuration for a named `HttpClient` can be specified during registration in `Startup.ConfigureServices`:

```
services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
```

In the preceding code the client is configured with:

* The base address `https://api.github.com/`.
* Two headers required to work with the GitHub API.

#### CreateClient

Each time [CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient) is called:

* A new instance of `HttpClient` is created.
* The configuration action is called.

To create a named client, pass its name into `CreateClient`:

```
public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}
```

In the preceding code, the request doesn't need to specify a hostname. The code can pass just the path, since the base address configured for the client is used.

### Typed clients

Typed clients:

* Provide the same capabilities as named clients without the need to use strings as keys.

* Provides IntelliSense and compiler help when consuming clients.

* Provide a single location to configure and interact with a particular `HttpClient`. For example, a single typed client might be used:

  * For a single backend endpoint.
  * To encapsulate all logic dealing with the endpoint.

* Work with DI and can be injected where required in the app.

A typed client accepts an `HttpClient` parameter in its constructor:

```
public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}
```

In the preceding code:

* The configuration is moved into the typed client.
* The `HttpClient` object is exposed as a `public` property.

API-specific methods can be created that expose `HttpClient` functionality. For example, the `GetAspNetDocsIssues` method encapsulates code to retrieve open issues.

The following code calls [AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) in `Startup.ConfigureServices` to register a typed client class:

```
services.AddHttpClient<GitHubService>();
```

The typed client is registered as transient with DI. In the preceding code, `AddHttpClient` registers `GitHubService` as a transient service. This registration uses a factory method to:

1. Create an instance of `HttpClient`.
2. Create an instance of `GitHubService`, passing in the instance of `HttpClient` to its constructor.

The typed client can be injected and consumed directly:

```
public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}
```

The configuration for a typed client can be specified during registration in `Startup.ConfigureServices`, rather than in the typed client's constructor:

```
services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
```

The `HttpClient` can be encapsulated within a typed client. Rather than exposing it as a property, define a method that calls the `HttpClient` instance internally:

```
public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}
```

In the preceding code, the `HttpClient` is stored in a private field. Access to the `HttpClient` is by the public `GetRepos` method.

### Generated clients

`IHttpClientFactory` can be used in combination with third-party libraries such as [Refit](https://github.com/paulcbetts/refit). Refit is a REST library for .NET. It converts REST APIs into live interfaces. An implementation of the interface is generated dynamically by the `RestService`, using `HttpClient` to make the external HTTP calls.

An interface and a reply are defined to represent the external API and its response:

```
public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}
```

A typed client can be added, using Refit to generate the implementation:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}
```

The defined interface can be consumed where necessary, with the implementation provided by DI and Refit:

```
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}
```

## Make POST, PUT, and DELETE requests

In the preceding examples, all HTTP requests use the GET HTTP verb. `HttpClient` also supports other HTTP verbs, including:

* POST
* PUT
* DELETE
* PATCH

For a complete list of supported HTTP verbs, see [HttpMethod](/en-us/dotnet/api/system.net.http.httpmethod).

The following example shows how to make an HTTP POST request:

```
public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}
```

In the preceding code, the `CreateItemAsync` method:

* Serializes the `TodoItem` parameter to JSON using `System.Text.Json`. This uses an instance of [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) to configure the serialization process.
* Creates an instance of [StringContent](/en-us/dotnet/api/system.net.http.stringcontent) to package the serialized JSON for sending in the HTTP request's body.
* Calls [PostAsync](/en-us/dotnet/api/system.net.http.httpclient.postasync) to send the JSON content to the specified URL. This is a relative URL that gets added to the [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress).
* Calls [EnsureSuccessStatusCode](/en-us/dotnet/api/system.net.http.httpresponsemessage.ensuresuccessstatuscode) to throw an exception if the response status code doesn't indicate success.

`HttpClient` also supports other types of content. For example, [MultipartContent](/en-us/dotnet/api/system.net.http.multipartcontent) and [StreamContent](/en-us/dotnet/api/system.net.http.streamcontent). For a complete list of supported content, see [HttpContent](/en-us/dotnet/api/system.net.http.httpcontent).

The following example shows an HTTP PUT request:

```
public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}
```

The preceding code is similar to the POST example. The `SaveItemAsync` method calls [PutAsync](/en-us/dotnet/api/system.net.http.httpclient.putasync) instead of `PostAsync`.

The following example shows an HTTP DELETE request:

```
public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}
```

In the preceding code, the `DeleteItemAsync` method calls [DeleteAsync](/en-us/dotnet/api/system.net.http.httpclient.deleteasync). Because HTTP DELETE requests typically contain no body, the `DeleteAsync` method doesn't provide an overload that accepts an instance of `HttpContent`.

To learn more about using different HTTP verbs with `HttpClient`, see [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).

## Outgoing request middleware

`HttpClient` has the concept of delegating handlers that can be linked together for outgoing HTTP requests. `IHttpClientFactory`:

* Simplifies defining the handlers to apply for each named client.

* Supports registration and chaining of multiple handlers to build an outgoing request middleware pipeline. Each of these handlers is able to perform work before and after the outgoing request. This pattern:

  * Is similar to the inbound middleware pipeline in ASP.NET Core.

  * Provides a mechanism to manage cross-cutting concerns around HTTP requests, such as:

    * caching
    * error handling
    * serialization
    * logging

To create a delegating handler:

* Derive from [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler).
* Override [SendAsync](/en-us/dotnet/api/system.net.http.delegatinghandler.sendasync). Execute code before passing the request to the next handler in the pipeline:

```
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}
```

The preceding code checks if the `X-API-KEY` header is in the request. If `X-API-KEY` is missing, [BadRequest](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-badrequest) is returned.

More than one handler can be added to the configuration for an `HttpClient` with [Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler):

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.
```

In the preceding code, `ValidateHeaderHandler` is registered with DI as a transient service. A delegating handler **must** be registered in DI as a transient service, rather than singleton or scoped. Delegating handlers can't be reused across multiple HTTP client pipelines, and registering a handler as a singleton results in an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) when the handler is reused. Once registered, [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler) can be called, passing in the type for the handler.

Multiple handlers can be registered in the order that they should execute. Each handler wraps the next handler until the final `HttpClientHandler` executes the request:

```
services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();
```

### Use DI in outgoing request middleware

When `IHttpClientFactory` creates a new delegating handler, it uses DI to fulfill the handler's constructor parameters. `IHttpClientFactory` creates a **separate** DI scope for each handler, which can lead to surprising behavior when a handler consumes a *scoped* service.

For example, consider the following interface and its implementation, which represents a task as an operation with an identifier, `OperationId`:

```
public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
```

As its name suggests, `IOperationScoped` is registered with DI using a *scoped* lifetime:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}
```

The following delegating handler consumes and uses `IOperationScoped` to set the `X-OPERATION-ID` header for the outgoing request:

```
public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}
```

In the [`HttpRequestsSample` download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/http-requests/samples/3.x/HttpRequestsSample)], navigate to `/Operation` and refresh the page. The request scope value changes for each request, but the handler scope value only changes every 5 seconds.

Handlers can depend upon services of any scope. Services that handlers depend upon are disposed when the handler is disposed.

Use one of the following approaches to share per-request state with message handlers:

* Pass data into the handler using [HttpRequestMessage.Options](/en-us/dotnet/api/system.net.http.httprequestmessage.options).
* Use [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) to access the current request.
* Create a custom [AsyncLocal\<T>](/en-us/dotnet/api/system.threading.asynclocal-1) storage object to pass the data.

## Use Polly-based handlers

`IHttpClientFactory` integrates with the third-party library [Polly](https://github.com/App-vNext/Polly). Polly is a comprehensive resilience and transient fault-handling library for .NET. It allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.

Extension methods are provided to enable the use of Polly policies with configured `HttpClient` instances. The Polly extensions support adding Polly-based handlers to clients. Polly requires the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly/) NuGet package.

### Handle transient faults

Faults typically occur when external HTTP calls are transient. [AddTransientHttpErrorPolicy](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addtransienthttperrorpolicy) allows a policy to be defined to handle transient errors. Policies configured with `AddTransientHttpErrorPolicy` handle the following responses:

* [HttpRequestException](/en-us/dotnet/api/system.net.http.httprequestexception)
* HTTP 5xx
* HTTP 408

`AddTransientHttpErrorPolicy` provides access to a `PolicyBuilder` object configured to handle errors representing a possible transient fault:

```
public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.
```

In the preceding code, a `WaitAndRetryAsync` policy is defined. Failed requests are retried up to three times with a delay of 600 ms between attempts.

### Dynamically select policies

Extension methods are provided to add Polly-based handlers, for example, [AddPolicyHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addpolicyhandler). The following `AddPolicyHandler` overload inspects the request to decide which policy to apply:

```
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);
```

In the preceding code, if the outgoing request is an HTTP GET, a 10-second timeout is applied. For any other HTTP method, a 30-second timeout is used.

### Add multiple Polly handlers

It's common to nest Polly policies:

```
services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
```

In the preceding example:

* Two handlers are added.
* The first handler uses [AddTransientHttpErrorPolicy](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addtransienthttperrorpolicy) to add a retry policy. Failed requests are retried up to three times.
* The second `AddTransientHttpErrorPolicy` call adds a circuit breaker policy. Further external requests are blocked for 30 seconds if five failed attempts occur sequentially. Circuit breaker policies are stateful. All calls through this client share the same circuit state.

### Add policies from the Polly registry

An approach to managing regularly used policies is to define them once and register them with a `PolicyRegistry`.

In the following code:

* The "regular" and "long" policies are added.
* [AddPolicyHandlerFromRegistry](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addpolicyhandlerfromregistry) adds the "regular" and "long" policies from the registry.

```
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.
```

For more information on `IHttpClientFactory` and Polly integrations, see the [Polly wiki](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory).

## HttpClient and lifetime management

A new `HttpClient` instance is returned each time `CreateClient` is called on the `IHttpClientFactory`. An [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) is created for each named client. The factory manages the lifetimes of the `HttpMessageHandler` instances.

`IHttpClientFactory` pools the `HttpMessageHandler` instances created by the factory to reduce resource consumption. An `HttpMessageHandler` instance might be reused from the pool when creating a new `HttpClient` instance, if its lifetime isn't expired.

Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connections. Creating more handlers than necessary can result in connection delays. Some handlers also keep connections open indefinitely, which can prevent the handler from reacting to DNS (Domain Name System) changes.

The default handler lifetime is two minutes. The default value can be overridden on a per-named client basis:

```
public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.
```

`HttpClient` instances can generally be treated as .NET objects **not** requiring disposal. Disposal cancels outgoing requests and guarantees the given `HttpClient` instance can't be used after calling [Dispose](/en-us/dotnet/api/system.idisposable.dispose). `IHttpClientFactory` tracks and disposes resources used by `HttpClient` instances.

Keeping a single `HttpClient` instance alive for a long duration is a common pattern used before the inception of `IHttpClientFactory`. This pattern becomes unnecessary after migrating to `IHttpClientFactory`.

### Alternatives to IHttpClientFactory

Using `IHttpClientFactory` in a DI-enabled app avoids:

* Resource exhaustion problems by pooling `HttpMessageHandler` instances.
* Stale DNS problems by cycling `HttpMessageHandler` instances at regular intervals.

There are alternative ways to solve the preceding problems using a long-lived [SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler) instance.

* Create an instance of `SocketsHttpHandler` when the app starts and use it for the life of the app.
* Configure [PooledConnectionLifetime](/en-us/dotnet/api/system.net.http.socketshttphandler.pooledconnectionlifetime#system-net-http-socketshttphandler-pooledconnectionlifetime) to an appropriate value based on DNS refresh times.
* Create `HttpClient` instances using `new HttpClient(handler, disposeHandler: false)` as needed.

The preceding approaches solve the resource management problems that `IHttpClientFactory` solves in a similar way.

* The `SocketsHttpHandler` shares connections across `HttpClient` instances. This sharing prevents socket exhaustion.
* The `SocketsHttpHandler` cycles connections according to `PooledConnectionLifetime` to avoid stale DNS problems.

### Cookies

The pooled `HttpMessageHandler` instances results in `CookieContainer` objects being shared. Unanticipated `CookieContainer` object sharing often results in incorrect code. For apps that require cookies, consider either:

* Disabling automatic cookie handling
* Avoiding `IHttpClientFactory`

Call [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) to disable automatic cookie handling:

```
services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });
```

## Logging

Clients created via `IHttpClientFactory` record log messages for all requests. Enable the appropriate information level in the logging configuration to see the default log messages. Other logging, such as the logging of request headers, is only included at trace level.

The log category used for each client includes the name of the client. A client named *MyNamedClient*, for example, logs messages with a category of "System.Net.Http.HttpClient.**MyNamedClient**.LogicalHandler." Messages suffixed with *LogicalHandler* occur outside the request handler pipeline. On the request, messages are logged before any other handlers in the pipeline have processed it. On the response, messages are logged after any other pipeline handlers have received the response.

Logging also occurs inside the request handler pipeline. In the *MyNamedClient* example, those messages are logged with the log category "System.Net.Http.HttpClient.**MyNamedClient**.ClientHandler." For the request, this occurs after all other handlers have run and immediately before the request is sent. On the response, this logging includes the state of the response before it passes back through the handler pipeline.

Enabling logging outside and inside the pipeline enables inspection of the changes made by the other pipeline handlers. The logging might include changes to request headers or to the response status code.

Including the name of the client in the log category enables log filtering for specific named clients.

## Configure the HttpMessageHandler

It might be necessary to control the configuration of the inner `HttpMessageHandler` used by a client.

An `IHttpClientBuilder` is returned when adding named or typed clients. The [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method can be used to define a delegate. The delegate is used to create and configure the primary `HttpMessageHandler` used by that client:

```
public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.
```

## Use IHttpClientFactory in a console app

In a console app, add the following package references to the project:

* [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting)
* [Microsoft.Extensions.Http](https://www.nuget.org/packages/Microsoft.Extensions.Http)

In the following example:

* [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) is registered in the [Generic Host's](host/generic-host?view=aspnetcore-10.0) service container.
* `MyService` creates a client factory instance from the service, which is used to create an `HttpClient`. `HttpClient` is used to retrieve a webpage.
* `Main` creates a scope to execute the service's `GetPage` method and write the first 500 characters of the webpage content to the console.

```
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}
```

## Header propagation middleware

Header propagation is an ASP.NET Core middleware to propagate HTTP headers from the incoming request to the outgoing HTTP Client requests. To use header propagation:

* Reference the [Microsoft.AspNetCore.HeaderPropagation](https://www.nuget.org/packages/Microsoft.AspNetCore.HeaderPropagation) package.

* Configure the middleware and `HttpClient` in `Startup`:

  ```
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddControllers();

      services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
      services.AddHeaderPropagation(options =>
      {
          options.Headers.Add("X-TraceId");
      });
  }

  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  {
      if (env.IsDevelopment())
      {
          app.UseDeveloperExceptionPage();
      }

      app.UseHttpsRedirection();

      app.UseHeaderPropagation();

      app.UseRouting();

      app.UseAuthorization();

      app.UseEndpoints(endpoints =>
      {
          endpoints.MapControllers();
      });
  }
  ```

* The client includes the configured headers on outbound requests:

  ```
  var client = clientFactory.CreateClient("MyForwardingClient");
  var response = client.GetAsync(...);
  ```

By [Kirk Larkin](https://github.com/serpent5), [Steve Gordon](https://github.com/stevejgordon), [Glenn Condron](https://github.com/glennc), and [Ryan Nowak](https://github.com/rynowak).

An [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) can be registered and used to configure and create [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances in an app. `IHttpClientFactory` offers the following benefits:

* Provides a central location for naming and configuring logical `HttpClient` instances. For example, a client named *github* could be registered and configured to access [GitHub](https://github.com/). A default client can be registered for general access.
* Codifies the concept of outgoing middleware via delegating handlers in `HttpClient`. Provides extensions for Polly-based middleware to take advantage of delegating handlers in `HttpClient`.
* Manages the pooling and lifetime of underlying `HttpClientMessageHandler` instances. Automatic management avoids common DNS (Domain Name System) problems that occur when manually managing `HttpClient` lifetimes.
* Adds a configurable logging experience (via `ILogger`) for all requests sent through clients created by the factory.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/http-requests/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample)).

The sample code in this topic version uses [System.Text.Json](/en-us/dotnet/api/system.text.json) to deserialize JSON content returned in HTTP responses. For samples that use `Json.NET` and `ReadAsAsync<T>`, use the version selector to select a 2.x version of this topic.

## Consumption patterns

There are several ways `IHttpClientFactory` can be used in an app:

* [Basic usage](#basic-usage)
* [Named clients](#named-clients)
* [Typed clients](#typed-clients)
* [Generated clients](#generated-clients)

The best approach depends upon the app's requirements.

### Basic usage

`IHttpClientFactory` can be registered by calling `AddHttpClient`:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.
```

An `IHttpClientFactory` can be requested using [dependency injection (DI)](dependency-injection?view=aspnetcore-10.0). The following code uses `IHttpClientFactory` to create an `HttpClient` instance:

```
public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}
```

Using `IHttpClientFactory` like in the preceding example is a good way to refactor an existing app. It has no effect on how `HttpClient` is used. In places where `HttpClient` instances are created in an existing app, replace those occurrences with calls to [CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient).

### Named clients

Named clients are a good choice when:

* The app requires many distinct uses of `HttpClient`.
* Many `HttpClient`s have different configuration.

Configuration for a named `HttpClient` can be specified during registration in `Startup.ConfigureServices`:

```
services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
```

In the preceding code the client is configured with:

* The base address `https://api.github.com/`.
* Two headers required to work with the GitHub API.

#### CreateClient

Each time [CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient) is called:

* A new instance of `HttpClient` is created.
* The configuration action is called.

To create a named client, pass its name into `CreateClient`:

```
public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}
```

In the preceding code, the request doesn't need to specify a hostname. The code can pass just the path, since the base address configured for the client is used.

### Typed clients

Typed clients:

* Provide the same capabilities as named clients without the need to use strings as keys.

* Provides IntelliSense and compiler help when consuming clients.

* Provide a single location to configure and interact with a particular `HttpClient`. For example, a single typed client might be used:

  * For a single backend endpoint.
  * To encapsulate all logic dealing with the endpoint.

* Work with DI and can be injected where required in the app.

A typed client accepts an `HttpClient` parameter in its constructor:

```
public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubIssue>>(responseStream);
    }
}
```

In the preceding code:

* The configuration is moved into the typed client.
* The `HttpClient` object is exposed as a `public` property.

API-specific methods can be created that expose `HttpClient` functionality. For example, the `GetAspNetDocsIssues` method encapsulates code to retrieve open issues.

The following code calls [AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) in `Startup.ConfigureServices` to register a typed client class:

```
services.AddHttpClient<GitHubService>();
```

The typed client is registered as transient with DI. In the preceding code, `AddHttpClient` registers `GitHubService` as a transient service. This registration uses a factory method to:

1. Create an instance of `HttpClient`.
2. Create an instance of `GitHubService`, passing in the instance of `HttpClient` to its constructor.

The typed client can be injected and consumed directly:

```
public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}
```

The configuration for a typed client can be specified during registration in `Startup.ConfigureServices`, rather than in the typed client's constructor:

```
services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
```

The `HttpClient` can be encapsulated within a typed client. Rather than exposing it as a property, define a method that calls the `HttpClient` instance internally:

```
public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}
```

In the preceding code, the `HttpClient` is stored in a private field. Access to the `HttpClient` is by the public `GetRepos` method.

### Generated clients

`IHttpClientFactory` can be used in combination with third-party libraries such as [Refit](https://github.com/paulcbetts/refit). Refit is a REST library for .NET. It converts REST APIs into live interfaces. An implementation of the interface is generated dynamically by the `RestService`, using `HttpClient` to make the external HTTP calls.

An interface and a reply are defined to represent the external API and its response:

```
public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}
```

A typed client can be added, using Refit to generate the implementation:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}
```

The defined interface can be consumed where necessary, with the implementation provided by DI and Refit:

```
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}
```

## Make POST, PUT, and DELETE requests

In the preceding examples, all HTTP requests use the GET HTTP verb. `HttpClient` also supports other HTTP verbs, including:

* POST
* PUT
* DELETE
* PATCH

For a complete list of supported HTTP verbs, see [HttpMethod](/en-us/dotnet/api/system.net.http.httpmethod).

The following example shows how to make an HTTP POST request:

```
public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}
```

In the preceding code, the `CreateItemAsync` method:

* Serializes the `TodoItem` parameter to JSON using `System.Text.Json`. This uses an instance of [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) to configure the serialization process.
* Creates an instance of [StringContent](/en-us/dotnet/api/system.net.http.stringcontent) to package the serialized JSON for sending in the HTTP request's body.
* Calls [PostAsync](/en-us/dotnet/api/system.net.http.httpclient.postasync) to send the JSON content to the specified URL. This is a relative URL that gets added to the [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress).
* Calls [EnsureSuccessStatusCode](/en-us/dotnet/api/system.net.http.httpresponsemessage.ensuresuccessstatuscode) to throw an exception if the response status code doesn't indicate success.

`HttpClient` also supports other types of content. For example, [MultipartContent](/en-us/dotnet/api/system.net.http.multipartcontent) and [StreamContent](/en-us/dotnet/api/system.net.http.streamcontent). For a complete list of supported content, see [HttpContent](/en-us/dotnet/api/system.net.http.httpcontent).

The following example shows an HTTP PUT request:

```
public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}
```

The preceding code is similar to the POST example. The `SaveItemAsync` method calls [PutAsync](/en-us/dotnet/api/system.net.http.httpclient.putasync) instead of `PostAsync`.

The following example shows an HTTP DELETE request:

```
public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}
```

In the preceding code, the `DeleteItemAsync` method calls [DeleteAsync](/en-us/dotnet/api/system.net.http.httpclient.deleteasync). Because HTTP DELETE requests typically contain no body, the `DeleteAsync` method doesn't provide an overload that accepts an instance of `HttpContent`.

To learn more about using different HTTP verbs with `HttpClient`, see [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).

## Outgoing request middleware

`HttpClient` has the concept of delegating handlers that can be linked together for outgoing HTTP requests. `IHttpClientFactory`:

* Simplifies defining the handlers to apply for each named client.

* Supports registration and chaining of multiple handlers to build an outgoing request middleware pipeline. Each of these handlers is able to perform work before and after the outgoing request. This pattern:

  * Is similar to the inbound middleware pipeline in ASP.NET Core.

  * Provides a mechanism to manage cross-cutting concerns around HTTP requests, such as:

    * caching
    * error handling
    * serialization
    * logging

To create a delegating handler:

* Derive from [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler).
* Override [SendAsync](/en-us/dotnet/api/system.net.http.delegatinghandler.sendasync). Execute code before passing the request to the next handler in the pipeline:

```
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}
```

The preceding code checks if the `X-API-KEY` header is in the request. If `X-API-KEY` is missing, [BadRequest](/en-us/dotnet/api/system.net.httpstatuscode#system-net-httpstatuscode-badrequest) is returned.

More than one handler can be added to the configuration for an `HttpClient` with [Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler):

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.
```

In the preceding code, `ValidateHeaderHandler` is registered with DI as a transient service. A delegating handler **must** be registered in DI as a transient service, rather than singleton or scoped. Delegating handlers can't be reused across multiple HTTP client pipelines, and registering a handler as a singleton results in an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) when the handler is reused. Once registered, [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler) can be called, passing in the type for the handler.

Multiple handlers can be registered in the order that they should execute. Each handler wraps the next handler until the final `HttpClientHandler` executes the request:

```
services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();
```

### Use DI in outgoing request middleware

When `IHttpClientFactory` creates a new delegating handler, it uses DI to fulfill the handler's constructor parameters. `IHttpClientFactory` creates a **separate** DI scope for each handler, which can lead to surprising behavior when a handler consumes a *scoped* service.

For example, consider the following interface and its implementation, which represents a task as an operation with an identifier, `OperationId`:

```
public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
```

As its name suggests, `IOperationScoped` is registered with DI using a *scoped* lifetime:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}
```

The following delegating handler consumes and uses `IOperationScoped` to set the `X-OPERATION-ID` header for the outgoing request:

```
public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}
```

In the [`HttpRequestsSample` download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/http-requests/samples/3.x/HttpRequestsSample)], navigate to `/Operation` and refresh the page. The request scope value changes for each request, but the handler scope value only changes every 5 seconds.

Handlers can depend upon services of any scope. Services that handlers depend upon are disposed when the handler is disposed.

Use one of the following approaches to share per-request state with message handlers:

* Pass data into the handler using [HttpRequestMessage.Properties](/en-us/dotnet/api/system.net.http.httprequestmessage.properties).
* Use [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) to access the current request.
* Create a custom [AsyncLocal\<T>](/en-us/dotnet/api/system.threading.asynclocal-1) storage object to pass the data.

## Use Polly-based handlers

`IHttpClientFactory` integrates with the third-party library [Polly](https://github.com/App-vNext/Polly). Polly is a comprehensive resilience and transient fault-handling library for .NET. It allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.

Extension methods are provided to enable the use of Polly policies with configured `HttpClient` instances. The Polly extensions support adding Polly-based handlers to clients. Polly requires the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly/) NuGet package.

### Handle transient faults

Faults typically occur when external HTTP calls are transient. [AddTransientHttpErrorPolicy](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addtransienthttperrorpolicy) allows a policy to be defined to handle transient errors. Policies configured with `AddTransientHttpErrorPolicy` handle the following responses:

* [HttpRequestException](/en-us/dotnet/api/system.net.http.httprequestexception)
* HTTP 5xx
* HTTP 408

`AddTransientHttpErrorPolicy` provides access to a `PolicyBuilder` object configured to handle errors representing a possible transient fault:

```
public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.
```

In the preceding code, a `WaitAndRetryAsync` policy is defined. Failed requests are retried up to three times with a delay of 600 ms between attempts.

### Dynamically select policies

Extension methods are provided to add Polly-based handlers, for example, [AddPolicyHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addpolicyhandler). The following `AddPolicyHandler` overload inspects the request to decide which policy to apply:

```
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);
```

In the preceding code, if the outgoing request is an HTTP GET, a 10-second timeout is applied. For any other HTTP method, a 30-second timeout is used.

### Add multiple Polly handlers

It's common to nest Polly policies:

```
services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
```

In the preceding example:

* Two handlers are added.
* The first handler uses [AddTransientHttpErrorPolicy](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addtransienthttperrorpolicy) to add a retry policy. Failed requests are retried up to three times.
* The second `AddTransientHttpErrorPolicy` call adds a circuit breaker policy. Further external requests are blocked for 30 seconds if five failed attempts occur sequentially. Circuit breaker policies are stateful. All calls through this client share the same circuit state.

### Add policies from the Polly registry

An approach to managing regularly used policies is to define them once and register them with a `PolicyRegistry`.

In the following code:

* The "regular" and "long" policies are added.
* [AddPolicyHandlerFromRegistry](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pollyhttpclientbuilderextensions.addpolicyhandlerfromregistry) adds the "regular" and "long" policies from the registry.

```
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.
```

For more information on `IHttpClientFactory` and Polly integrations, see the [Polly wiki](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory).

## HttpClient and lifetime management

A new `HttpClient` instance is returned each time `CreateClient` is called on the `IHttpClientFactory`. An [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) is created for each named client. The factory manages the lifetimes of the `HttpMessageHandler` instances.

`IHttpClientFactory` pools the `HttpMessageHandler` instances created by the factory to reduce resource consumption. An `HttpMessageHandler` instance might be reused from the pool when creating a new `HttpClient` instance, if its lifetime isn't expired.

Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connections. Creating more handlers than necessary can result in connection delays. Some handlers also keep connections open indefinitely, which can prevent the handler from reacting to DNS (Domain Name System) changes.

The default handler lifetime is two minutes. The default value can be overridden on a per-named client basis:

```
public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.
```

`HttpClient` instances can generally be treated as .NET objects **not** requiring disposal. Disposal cancels outgoing requests and guarantees the given `HttpClient` instance can't be used after calling [Dispose](/en-us/dotnet/api/system.idisposable.dispose). `IHttpClientFactory` tracks and disposes resources used by `HttpClient` instances.

Keeping a single `HttpClient` instance alive for a long duration is a common pattern used before the inception of `IHttpClientFactory`. This pattern becomes unnecessary after migrating to `IHttpClientFactory`.

### Alternatives to IHttpClientFactory

Using `IHttpClientFactory` in a DI-enabled app avoids:

* Resource exhaustion problems by pooling `HttpMessageHandler` instances.
* Stale DNS problems by cycling `HttpMessageHandler` instances at regular intervals.

There are alternative ways to solve the preceding problems using a long-lived [SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler) instance.

* Create an instance of `SocketsHttpHandler` when the app starts and use it for the life of the app.
* Configure [PooledConnectionLifetime](/en-us/dotnet/api/system.net.http.socketshttphandler.pooledconnectionlifetime#system-net-http-socketshttphandler-pooledconnectionlifetime) to an appropriate value based on DNS refresh times.
* Create `HttpClient` instances using `new HttpClient(handler, disposeHandler: false)` as needed.

The preceding approaches solve the resource management problems that `IHttpClientFactory` solves in a similar way.

* The `SocketsHttpHandler` shares connections across `HttpClient` instances. This sharing prevents socket exhaustion.
* The `SocketsHttpHandler` cycles connections according to `PooledConnectionLifetime` to avoid stale DNS problems.

### Cookies

The pooled `HttpMessageHandler` instances results in `CookieContainer` objects being shared. Unanticipated `CookieContainer` object sharing often results in incorrect code. For apps that require cookies, consider either:

* Disabling automatic cookie handling
* Avoiding `IHttpClientFactory`

Call [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) to disable automatic cookie handling:

```
services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });
```

## Logging

Clients created via `IHttpClientFactory` record log messages for all requests. Enable the appropriate information level in the logging configuration to see the default log messages. Other logging, such as the logging of request headers, is only included at trace level.

The log category used for each client includes the name of the client. A client named *MyNamedClient*, for example, logs messages with a category of "System.Net.Http.HttpClient.**MyNamedClient**.LogicalHandler." Messages suffixed with *LogicalHandler* occur outside the request handler pipeline. On the request, messages are logged before any other handlers in the pipeline process them. On the response, messages are logged after any other pipeline handlers receive the response.

Logging also occurs inside the request handler pipeline. In the *MyNamedClient* example, those messages are logged with the log category "System.Net.Http.HttpClient.**MyNamedClient**.ClientHandler." For the request, this occurs after all other handlers have run and immediately before the request is sent. On the response, this logging includes the state of the response before it passes back through the handler pipeline.

Enabling logging outside and inside the pipeline enables inspection of the changes made by the other pipeline handlers. The logging might include changes to request headers or to the response status code.

Including the name of the client in the log category enables log filtering for specific named clients.

## Configure the HttpMessageHandler

It might be necessary to control the configuration of the inner `HttpMessageHandler` used by a client.

An `IHttpClientBuilder` is returned when adding named or typed clients. The [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method can be used to define a delegate. The delegate is used to create and configure the primary `HttpMessageHandler` used by that client:

```
public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.
```

## Use IHttpClientFactory in a console app

In a console app, add the following package references to the project:

* [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting)
* [Microsoft.Extensions.Http](https://www.nuget.org/packages/Microsoft.Extensions.Http)

In the following example:

* [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) is registered in the [Generic Host's](host/generic-host?view=aspnetcore-10.0) service container.
* `MyService` creates a client factory instance from the service, which is used to create an `HttpClient`. `HttpClient` is used to retrieve a webpage.
* `Main` creates a scope to execute the service's `GetPage` method and write the first 500 characters of the webpage content to the console.

```
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}
```

## Header propagation middleware

Header propagation is an ASP.NET Core middleware to propagate HTTP headers from the incoming request to the outgoing HTTP Client requests. To use header propagation:

* Reference the [Microsoft.AspNetCore.HeaderPropagation](https://www.nuget.org/packages/Microsoft.AspNetCore.HeaderPropagation) package.

* Configure the middleware and `HttpClient` in `Startup`:

  ```
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddControllers();

      services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
      services.AddHeaderPropagation(options =>
      {
          options.Headers.Add("X-TraceId");
      });
  }

  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  {
      if (env.IsDevelopment())
      {
          app.UseDeveloperExceptionPage();
      }

      app.UseHttpsRedirection();

      app.UseHeaderPropagation();

      app.UseRouting();

      app.UseAuthorization();

      app.UseEndpoints(endpoints =>
      {
          endpoints.MapControllers();
      });
  }
  ```

* The client includes the configured headers on outbound requests:

  ```
  var client = clientFactory.CreateClient("MyForwardingClient");
  var response = client.GetAsync(...);
  ```

By [Glenn Condron](https://github.com/glennc), [Ryan Nowak](https://github.com/rynowak), and [Steve Gordon](https://github.com/stevejgordon)

An [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) can be registered and used to configure and create [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances in an app. It offers the following benefits:

* Provides a central location for naming and configuring logical `HttpClient` instances. For example, a *github* client can be registered and configured to access [GitHub](https://github.com/). A default client can be registered for other purposes.
* Codifies the concept of outgoing middleware via delegating handlers in `HttpClient` and provides extensions for Polly-based middleware to take advantage of that.
* Manages the pooling and lifetime of underlying `HttpClientMessageHandler` instances to avoid common DNS problems that occur when manually managing `HttpClient` lifetimes.
* Adds a configurable logging experience (via `ILogger`) for all requests sent through clients created by the factory.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/http-requests/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

Projects targeting .NET Framework require installation of the [Microsoft.Extensions.Http](https://www.nuget.org/packages/Microsoft.Extensions.Http/) NuGet package. Projects that target .NET Core and reference the [Microsoft.AspNetCore.App metapackage](metapackage-app?view=aspnetcore-10.0) already include the `Microsoft.Extensions.Http` package.

## Consumption patterns

There are several ways `IHttpClientFactory` can be used in an app:

* [Basic usage](#basic-usage)
* [Named clients](#named-clients)
* [Typed clients](#typed-clients)
* [Generated clients](#generated-clients)

None of them are strictly superior to another. The best approach depends upon the app's constraints.

### Basic usage

The `IHttpClientFactory` can be registered by calling the `AddHttpClient` extension method on the `IServiceCollection`, inside the `Startup.ConfigureServices` method.

```
services.AddHttpClient();
```

Once registered, code can accept an `IHttpClientFactory` anywhere services can be injected with [dependency injection (DI)](dependency-injection?view=aspnetcore-10.0). The `IHttpClientFactory` can be used to create an `HttpClient` instance:

```
public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            Branches = await response.Content
                .ReadAsAsync<IEnumerable<GitHubBranch>>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }                               
    }
}
```

Using `IHttpClientFactory` in this fashion is a good way to refactor an existing app. It has no effect on the way `HttpClient` is used. In places where `HttpClient` instances are currently created, replace those occurrences with a call to [CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient).

### Named clients

If an app requires many distinct uses of `HttpClient`, each with a different configuration, an option is to use **named clients**. Configuration for a named `HttpClient` can be specified during registration in `Startup.ConfigureServices`.

```
services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
```

In the preceding code, `AddHttpClient` is called, providing the name *github*. This client has some default configuration applied—namely the base address and two headers required to work with the GitHub API.

Each time `CreateClient` is called, a new instance of `HttpClient` is created and the configuration action is called.

To consume a named client, a string parameter can be passed to `CreateClient`. Specify the name of the client to be created:

```
public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            PullRequests = await response.Content
                .ReadAsAsync<IEnumerable<GitHubPullRequest>>();
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}
```

In the preceding code, the request doesn't need to specify a hostname. It can pass just the path, since the base address configured for the client is used.

### Typed clients

Typed clients:

* Provide the same capabilities as named clients without the need to use strings as keys.
* Provides IntelliSense and compiler help when consuming clients.
* Provide a single location to configure and interact with a particular `HttpClient`. For example, a single typed client might be used for a single backend endpoint and encapsulate all logic dealing with that endpoint.
* Work with DI and can be injected where required in your app.

A typed client accepts an `HttpClient` parameter in its constructor:

```
public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept", 
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent", 
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<GitHubIssue>>();

        return result;
    }
}
```

In the preceding code, the configuration is moved into the typed client. The `HttpClient` object is exposed as a `public` property. It's possible to define API-specific methods that expose `HttpClient` functionality. The `GetAspNetDocsIssues` method encapsulates the code needed to query for and parse out the latest open issues from a GitHub repository.

To register a typed client, the generic [AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) extension method can be used within `Startup.ConfigureServices`, specifying the typed client class:

```
services.AddHttpClient<GitHubService>();
```

The typed client is registered as transient with DI. The typed client can be injected and consumed directly:

```
public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}
```

If preferred, the configuration for a typed client can be specified during registration in `Startup.ConfigureServices`, rather than in the typed client's constructor:

```
services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
```

It's possible to entirely encapsulate the `HttpClient` within a typed client. Rather than exposing it as a property, public methods can be provided which call the `HttpClient` instance internally.

```
public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<string>>();

        return result;
    }
}
```

In the preceding code, the `HttpClient` is stored as a private field. All access to make external calls goes through the `GetRepos` method.

### Generated clients

`IHttpClientFactory` can be used in combination with other third-party libraries such as [Refit](https://github.com/paulcbetts/refit). Refit is a REST library for .NET. It converts REST APIs into live interfaces. An implementation of the interface is generated dynamically by the `RestService`, using `HttpClient` to make the external HTTP calls.

An interface and a reply are defined to represent the external API and its response:

```
public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}
```

A typed client can be added, using Refit to generate the implementation:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddMvc();
}
```

The defined interface can be consumed where necessary, with the implementation provided by DI and Refit:

```
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}
```

## Outgoing request middleware

`HttpClient` already has the concept of delegating handlers that can be linked together for outgoing HTTP requests. The `IHttpClientFactory` makes it easy to define the handlers to apply for each named client. It supports registration and chaining of multiple handlers to build an outgoing request middleware pipeline. Each of these handlers is able to perform work before and after the outgoing request. This pattern is similar to the inbound middleware pipeline in ASP.NET Core. The pattern provides a mechanism to manage cross-cutting concerns around HTTP requests, including caching, error handling, serialization, and logging.

To create a handler, define a class deriving from [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler). Override the `SendAsync` method to execute code before passing the request to the next handler in the pipeline:

```
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}
```

The preceding code defines a basic handler. It checks to see if an `X-API-KEY` header has been included on the request. If the header is missing, it can avoid the HTTP call and return a suitable response.

During registration, one or more handlers can be added to the configuration for an `HttpClient`. This task is accomplished via extension methods on the [IHttpClientBuilder](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.ihttpclientbuilder).

```
services.AddTransient<ValidateHeaderHandler>();

services.AddHttpClient("externalservice", c =>
{
    // Assume this is an "external" service which requires an API KEY
    c.BaseAddress = new Uri("https://localhost:5000/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();
```

In the preceding code, the `ValidateHeaderHandler` is registered with DI. The handler **must** be registered in DI as a transient service, never scoped. If the handler is registered as a scoped service and any services that the handler depends upon are disposable:

* The handler's services could be disposed before the handler goes out of scope.
* The disposed handler services cause the handler to fail.

Once registered, [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler) can be called, passing in the handler type.

Multiple handlers can be registered in the order that they should execute. Each handler wraps the next handler until the final `HttpClientHandler` executes the request:

```
services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();
```

Use one of the following approaches to share per-request state with message handlers:

* Pass data into the handler using `HttpRequestMessage.Properties`.
* Use `IHttpContextAccessor` to access the current request.
* Create a custom `AsyncLocal` storage object to pass the data.

## Use Polly-based handlers

`IHttpClientFactory` integrates with a popular third-party library called [Polly](https://github.com/App-vNext/Polly). Polly is a comprehensive resilience and transient fault-handling library for .NET. It allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.

Extension methods are provided to enable the use of Polly policies with configured `HttpClient` instances. The Polly extensions:

* Support adding Polly-based handlers to clients.
* Can be used after installing the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly/) NuGet package. The package isn't included in the ASP.NET Core shared framework.

### Handle transient faults

Most common faults occur when external HTTP calls are transient. A convenient extension method called `AddTransientHttpErrorPolicy` is included which allows a policy to be defined to handle transient errors. Policies configured with this extension method handle `HttpRequestException`, HTTP 5xx responses, and HTTP 408 responses.

The `AddTransientHttpErrorPolicy` extension can be used within `Startup.ConfigureServices`. The extension provides access to a `PolicyBuilder` object configured to handle errors representing a possible transient fault:

```
services.AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));
```

In the preceding code, a `WaitAndRetryAsync` policy is defined. Failed requests are retried up to three times with a delay of 600 ms between attempts.

### Dynamically select policies

Other extension methods exist which can be used to add Polly-based handlers. One such extension is `AddPolicyHandler`, which has multiple overloads. One overload allows the request to be inspected when defining which policy to apply:

```
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);
```

In the preceding code, if the outgoing request is an HTTP GET, a 10-second timeout is applied. For any other HTTP method, a 30-second timeout is used.

### Add multiple Polly handlers

It's common to nest Polly policies to provide enhanced functionality:

```
services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
```

In the preceding example, two handlers are added. The first uses the `AddTransientHttpErrorPolicy` extension to add a retry policy. Failed requests are retried up to three times. The second call to `AddTransientHttpErrorPolicy` adds a circuit breaker policy. Further external requests are blocked for 30 seconds if five failed attempts occur sequentially. Circuit breaker policies are stateful. All calls through this client share the same circuit state.

### Add policies from the Polly registry

An approach to managing regularly used policies is to define them once and register them with a `PolicyRegistry`. An extension method is provided which allows a handler to be added using a policy from the registry:

```
var registry = services.AddPolicyRegistry();

registry.Add("regular", timeout);
registry.Add("long", longTimeout);

services.AddHttpClient("regulartimeouthandler")
    .AddPolicyHandlerFromRegistry("regular");
```

In the preceding code, two policies are registered when the `PolicyRegistry` is added to the `ServiceCollection`. To use a policy from the registry, the `AddPolicyHandlerFromRegistry` method is used, passing the name of the policy to apply.

Further information about `IHttpClientFactory` and Polly integrations can be found on the [Polly wiki](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory).

## HttpClient and lifetime management

A new `HttpClient` instance is returned each time `CreateClient` is called on the `IHttpClientFactory`. There's an [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) for each named client. The factory manages the lifetimes of the `HttpMessageHandler` instances.

`IHttpClientFactory` pools the `HttpMessageHandler` instances created by the factory to reduce resource consumption. An `HttpMessageHandler` instance might be reused from the pool when creating a new `HttpClient` instance, if its lifetime isn't expired.

Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connections. Creating more handlers than necessary can result in connection delays. Some handlers also keep connections open indefinitely, which can prevent the handler from reacting to DNS changes.

The default handler lifetime is two minutes. The default value can be overridden on a per-named client basis. To override it, call [SetHandlerLifetime](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.sethandlerlifetime) on the `IHttpClientBuilder` that is returned when creating the client:

```
services.AddHttpClient("extendedhandlerlifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));
```

Disposal of the client isn't required. Disposal cancels outgoing requests and guarantees the given `HttpClient` instance can't be used after calling [Dispose](/en-us/dotnet/api/system.idisposable.dispose). `IHttpClientFactory` tracks and disposes resources used by `HttpClient` instances. The `HttpClient` instances can generally be treated as .NET objects not requiring disposal.

Keeping a single `HttpClient` instance alive for a long duration is a common pattern used before the inception of `IHttpClientFactory`. This pattern becomes unnecessary after migrating to `IHttpClientFactory`.

### Alternatives to IHttpClientFactory

Using `IHttpClientFactory` in a DI-enabled app avoids:

* Resource exhaustion problems by pooling `HttpMessageHandler` instances.
* Stale DNS problems by cycling `HttpMessageHandler` instances at regular intervals.

There are alternative ways to solve the preceding problems using a long-lived [SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler) instance.

* Create an instance of `SocketsHttpHandler` when the app starts and use it for the life of the app.
* Configure [PooledConnectionLifetime](/en-us/dotnet/api/system.net.http.socketshttphandler.pooledconnectionlifetime#system-net-http-socketshttphandler-pooledconnectionlifetime) to an appropriate value based on DNS refresh times.
* Create `HttpClient` instances using `new HttpClient(handler, disposeHandler: false)` as needed.

The preceding approaches solve the resource management problems that `IHttpClientFactory` solves in a similar way.

* The `SocketsHttpHandler` shares connections across `HttpClient` instances. This sharing prevents socket exhaustion.
* The `SocketsHttpHandler` cycles connections according to `PooledConnectionLifetime` to avoid stale DNS problems.

### Cookies

The pooled `HttpMessageHandler` instances results in `CookieContainer` objects being shared. Unanticipated `CookieContainer` object sharing often results in incorrect code. For apps that require cookies, consider either:

* Disabling automatic cookie handling
* Avoiding `IHttpClientFactory`

Call [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) to disable automatic cookie handling:

```
services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });
```

## Logging

Clients created via `IHttpClientFactory` record log messages for all requests. Enable the appropriate information level in your logging configuration to see the default log messages. Other logging, such as the logging of request headers, is only included at trace level.

The log category used for each client includes the name of the client. A client named *MyNamedClient*, for example, logs messages with a category of `System.Net.Http.HttpClient.MyNamedClient.LogicalHandler`. Messages suffixed with *LogicalHandler* occur outside the request handler pipeline. On the request, messages are logged before any other handlers in the pipeline have processed it. On the response, messages are logged after any other pipeline handlers have received the response.

Logging also occurs inside the request handler pipeline. In the *MyNamedClient* example, those messages are logged against the log category `System.Net.Http.HttpClient.MyNamedClient.ClientHandler`. For the request, this occurs after all other handlers have run and immediately before the request is sent out on the network. On the response, this logging includes the state of the response before it passes back through the handler pipeline.

Enabling logging outside and inside the pipeline enables inspection of the changes made by the other pipeline handlers. The logging might include changes to request headers, for example, or to the response status code.

Including the name of the client in the log category enables log filtering for specific named clients where necessary.

## Configure the HttpMessageHandler

It might be necessary to control the configuration of the inner `HttpMessageHandler` used by a client.

An `IHttpClientBuilder` is returned when adding named or typed clients. The [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method can be used to define a delegate. The delegate is used to create and configure the primary `HttpMessageHandler` used by that client:

```
services.AddHttpClient("configured-inner-handler")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            AllowAutoRedirect = false,
            UseDefaultCredentials = true
        };
    });
```

## Use IHttpClientFactory in a console app

In a console app, add the following package references to the project:

* [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting)
* [Microsoft.Extensions.Http](https://www.nuget.org/packages/Microsoft.Extensions.Http)

In the following example:

* [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) is registered in the [Generic Host's](host/generic-host?view=aspnetcore-10.0) service container.
* `MyService` creates a client factory instance from the service, which is used to create an `HttpClient`. `HttpClient` is used to retrieve a webpage.
* The service's `GetPage` method is executed to write the first 500 characters of the webpage content to the console. For more information on calling services from `Program.Main`, see [Dependency injection in ASP.NET Core](dependency-injection?view=aspnetcore-10.0#call-services-from-main).

```
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}
```

## Header propagation middleware

Header propagation is a community supported middleware to propagate HTTP headers from the incoming request to the outgoing HTTP Client requests. To use header propagation:

* Reference the community supported port of the package [HeaderPropagation](https://www.nuget.org/packages/HeaderPropagation). ASP.NET Core 3.1 or later supports [Microsoft.AspNetCore.HeaderPropagation](https://www.nuget.org/packages/Microsoft.AspNetCore.HeaderPropagation).

* Configure the middleware and `HttpClient` in `Startup`:

  ```
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

      services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
      services.AddHeaderPropagation(options =>
      {
          options.Headers.Add("X-TraceId");
      });
  }

  public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  {
      if (env.IsDevelopment())
      {
          app.UseDeveloperExceptionPage();
      }
      else
      {
          app.UseHsts();
      }

      app.UseHttpsRedirection();

      app.UseHeaderPropagation();

      app.UseMvc();
  }
  ```

* The client includes the configured headers on outbound requests:

  ```
  var client = clientFactory.CreateClient("MyForwardingClient");
  var response = client.GetAsync(...);
  ```

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/http-requests/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

- [Use HttpClientFactory to implement resilient HTTP requests](/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests)

- [Implement HTTP call retries with exponential backoff with HttpClientFactory and Polly policies](/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly)

- [Implement the Circuit Breaker pattern](/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/implement-circuit-breaker-pattern)

- [How to serialize and deserialize JSON in .NET](/en-us/dotnet/standard/serialization/system-text-json-how-to)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/security/authorization/roles
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/security/authorization/roles.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Role-based authorization in ASP.NET Core Razor Pages

When a user's identity is created after authentication, the user may belong to one or more *roles*, reflecting various authorizations that the user has to access data and perform operations. For example, Tracy may belong to the "Administrator" and "User" roles with access to administrative web pages in the app, while Scott may only belong to the "User" role and not have access to administrative data or operations. How these roles are created and managed depends on the backing store of the authorization process. Roles are exposed to the developer through [ClaimsPrincipal.IsInRole](/en-us/dotnet/api/system.security.claims.claimsprincipal.isinrole). [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) must be called to add Role services when setting up the app's identity system.

While roles are claims, not all claims are roles. Depending on the identity issuer, a role may be a collection of users that may apply claims for group members, as well as an actual claim on an identity. However, claims are meant to be information about an individual user. Using roles to add claims to a user can confuse the boundary between the user and their individual claims. This confusion is why the single-page application (SPA) templates aren't designed around roles. In addition, for organizations migrating from an on-premises legacy system, the proliferation of roles over the years can mean a role claim may be too large to be contained within a token usable by a SPA. To secure SPAs, see [Use Identity to secure a Web API backend for SPAs](../../../security/authentication/identity-api-authorization?view=aspnetcore-10.0).

This article uses Razor Pages examples and focuses on Razor Pages authorization scenarios. For Blazor and MVC guidance, see the following resources:

* [Role-based authorization in ASP.NET Core](../../../security/authorization/roles?view=aspnetcore-10.0)
* [Role-based authorization in ASP.NET Core MVC](../../../mvc/security/authorization/roles?view=aspnetcore-10.0)

Examples throughout this article apply role-based authorization via one or more [`[Authorize]` attributes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) on `PageModel` classes. Alternatively, role-based authorization can be applied using *conventions*. For more information, see [Razor Pages route and app conventions in ASP.NET Core](../../razor-pages-conventions?view=aspnetcore-10.0#page-model-action-conventions).

## Add Role services to Identity

Register role-based authorization services in the `Program` file by calling [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) with the role type in the app's Identity configuration. The role type in the following example is `IdentityRole`:

```
builder.Services.AddDefaultIdentity<IdentityUser>( ... )
    .AddRoles<IdentityRole>()
    ...
```

The preceding code requires the [`Microsoft.AspNetCore.Identity.UI` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.UI) and a `using` directive for [Microsoft.AspNetCore.Identity](/en-us/dotnet/api/microsoft.aspnetcore.identity).

In cases where the app takes granular control to build Identity manually, call [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) on [AddIdentityCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentitycore):

```
builder.Services.AddIdentityCore<IdentityUser>()
    .AddRoles<IdentityRole>()
    ...
```

Register role-based authorization services in `Startup.ConfigureServices` (`Startup.cs`) by calling [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) with the role type in the app's Identity configuration. The role type in the following example is `IdentityRole`:

```
services.AddDefaultIdentity<IdentityUser>()
    .AddRoles<IdentityRole>()
    ...
```

The preceding code requires the [`Microsoft.AspNetCore.Identity.UI` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.UI) and a `using` directive for [Microsoft.AspNetCore.Identity](/en-us/dotnet/api/microsoft.aspnetcore.identity).

In cases where the app takes granular control to build Identity manually, call [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) on [AddIdentityCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentitycore):

```
services.AddIdentityCore<IdentityUser>()
    .AddRoles<IdentityRole>()
    ...
```

## Role-based authorization checks

Role-based authorization checks:

* Are declarative and specify roles which the current user must be a member of to access the requested resource.
* Are applied to Razor Pages `PageModel` classes.
* Can't be applied at the Razor Page handler level. They must be applied to the entire `PageModel` class.

For example, the following code limits access to any `PageModel` handlers to users who are a member of the `Administrator` role:

```
[Authorize(Roles = "Administrator")]
public class AdministrationModel : PageModel
{
    ...
}
```

Multiple roles can be specified as a comma-separated list. In the following example, access is limited to users who are members of the `HRManager` role *or* the `Finance` role:

```
[Authorize(Roles = "HRManager, Finance")]
public class SalaryModel : PageModel
{
    ...
}
```

When multiple attributes are applied, the user must be a member of *all* of the roles specified. The following example requires *both* `PowerUser` *and* `ControlPanelUser` roles to call the `OnPostSetTime` and `OnPostShutdown` handlers:

```
[Authorize(Roles = "PowerUser")]
[Authorize(Roles = "ControlPanelUser")]
public class ControlPanelModel : PageModel
{
    public IActionResult OnPostSetTime() { ... }
    public IActionResult OnPostShutdown() { ... }
}
```

Filter attributes, including the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute), can only be applied to the entire `PageModel` class and can't be applied to specific page handler methods. If you need to implement different authorization rules for different page handlers, adopt either of the following approaches.

* Use separate Razor Pages for operations requiring different authorization levels, using [partial views](../../../mvc/views/partial?view=aspnetcore-10.0) for shared content.

* Inject [IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice) and manually check the authorization policy by calling [IAuthorizationService.AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) within handler methods. If authorization fails, the handler returns a `Forbid` result ([ForbidResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.forbidresult)).

  The following example demonstrates the approach:

  * The page's `OnGet` handler requires the `User` role via the `RequireUserRole` policy.
  * The page's `OnPostAsync` handler requires the `Admin` role via the `RequireAdministratorRole` policy.

  Note

  For guidance on registering the following policies at app startup, see the [Policy-based authorization checks](#policy-based-authorization-checks) section later in this article.

  Constructor injection of [IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice) in the following example is supported with [primary constructors](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) in C# 12 (.NET 8) or later.

  ```
  public class AuthPageHandlersExampleModel(
      IAuthorizationService authorizationService) : PageModel
  {
      public async Task<IActionResult> OnGet()
      {
          var authResult = 
              await authorizationService.AuthorizeAsync(User, "RequireUserRole");

          if (!authResult.Succeeded)
          {
              return Forbid();
          }

          // Authorized logic

          return Page();
      }

      public async Task<IActionResult> OnPostAsync()
      {
          var authResult = await authorizationService.AuthorizeAsync(
              User, "RequireAdministratorRole");

          if (!authResult.Succeeded)
          {
              return Forbid();
          }

          // Authorized logic

          return Page();
      }
  }
  ```

  Alternatively, page handler methods can check roles directly by calling [IsInRole](/en-us/dotnet/api/system.security.claims.claimsprincipal.isinrole):

  ```
  public IActionResult OnGet()
  {
      if (!User.IsInRole("User"))
      {
          return Forbid();
      }

      // Authorized logic

      return Page();
  }

  public IActionResult OnPost()
  {
      if (!User.IsInRole("Admin"))
      {
          return Forbid();
      }

      // Authorized logic

      return Page();
  }
  ```

Role matching is typically case-sensitive because role names are stored and compared using .NET string comparisons. For example, `Admin` (uppercase `A`) isn't treated as the same role as `admin` (lowercase `a`). For more information, see [Claim-based authorization in ASP.NET Core](../../../security/authorization/claims?view=aspnetcore-10.0#claim-case-sensitivity).

## Policy-based authorization checks

Role requirements can be expressed using policy syntax, where the app registers a policy at startup as part of the Authorization service configuration. In the following example, the `RequireAdministratorRole` policy specifies that all users must be in the `Administrator` role:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("RequireAdministratorRole",
         policy => policy.RequireRole("Administrator"));
```

Policies are applied using the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) property on the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
[Authorize(Policy = "RequireAdministratorRole")]
public class AdministrationModel : PageModel
{
    ...
}
```

In contrast to role matching, which is typically case-sensitive, ASP.NET Core policy name lookup is typically case-insensitive, so `RequireAdministratorRole` and `requireadministratorrole` refer to the same policy.

To specify multiple allowed roles in a requirement, specify the roles as parameters to the [RequireRole](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requirerole) method. In the following example, users are authorized if they belong to the `Administrator`, `PowerUser`, *or* `BackupAdministrator` roles:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ElevatedRights", policy =>
          policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
```

If you want the policy to require all of the preceding roles, either chain the roles to the policy builder or specify them to the policy builder individually in a [statement lambda](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#statement-lambdas).

Chained to the policy builder:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ElevatedRights", policy => 
        policy
            .RequireRole("Administrator")
            .RequireRole("PowerUser")
            .RequireRole("BackupAdministrator"));
```

Alternatively, use a statement lambda:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ElevatedRights",
        policy =>
        {
            policy.RequireRole("Administrator");
            policy.RequireRole("PowerUser");
            policy.RequireRole("BackupAdministrator");
        });
```

Role requirements can be expressed using policy syntax, where the app registers a policy at startup as part of the Authorization service configuration. In the following example, the `RequireAdministratorRole` policy specifies that all users must be in the `Administrator` role:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdministratorRole",
        policy => policy.RequireRole("Administrator"));
});
```

Policies are applied using the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) property on the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
[Authorize(Policy = "RequireAdministratorRole")]
public class AdministrationModel : PageModel
{
    ...
}
```

To specify multiple allowed roles in a requirement, specify the roles as parameters to the [RequireRole](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requirerole) method. In the following example, users are authorized if they belong to the `Administrator`, `PowerUser`, *or* `BackupAdministrator` roles:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy =>
          policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
});
```

If you want the policy to require all of the preceding roles, either chain the roles to the policy builder or specify them to the policy builder individually in a [statement lambda](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#statement-lambdas).

Chained to the policy builder:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy => 
        policy
            .RequireRole("Administrator")
            .RequireRole("PowerUser")
            .RequireRole("BackupAdministrator"));
});
```

Alternatively, use a statement lambda:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights",
        policy =>
        {
            policy.RequireRole("Administrator");
            policy.RequireRole("PowerUser");
            policy.RequireRole("BackupAdministrator");
        });
});
```

Role requirements can be expressed using policy syntax, where the app registers a policy at startup as part of the Authorization service configuration. In the following example, the `RequireAdministratorRole` policy specifies that all users must be in the `Administrator` role:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdministratorRole",
        policy => policy.RequireRole("Administrator"));
});
```

Policies are applied using the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) property on the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
[Authorize(Policy = "RequireAdministratorRole")]
public class AdministrationModel : PageModel
{
    ...
}
```

To specify multiple allowed roles in a requirement, specify the roles as parameters to the [RequireRole](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requirerole) method. In the following example, users are authorized if they belong to the `Administrator`, `PowerUser`, *or* `BackupAdministrator` roles:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy =>
          policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
});
```

If you want the policy to require all of the preceding roles, either chain the roles to the policy builder or specify them to the policy builder individually in a [statement lambda](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#statement-lambdas).

Chained to the policy builder:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy => 
        policy
            .RequireRole("Administrator")
            .RequireRole("PowerUser")
            .RequireRole("BackupAdministrator"));
});
```

Alternatively, use a statement lambda:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights",
        policy =>
        {
            policy.RequireRole("Administrator");
            policy.RequireRole("PowerUser");
            policy.RequireRole("BackupAdministrator");
        });
});
```

## Windows Authentication security groups as app roles

After the app is [configured for Windows Authentication](../../../security/authentication/windowsauth?view=aspnetcore-10.0) with the client and server machines part of the same Windows domain, user security groups are automatically included as claims in the user's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal).

The `User.Identity` is typically a [WindowsIdentity](/en-us/dotnet/api/system.security.principal.windowsidentity) when using Windows Authentication, and you can retrieve the SID group claims or check if a user is in a role with the following code, where the `{DOMAIN}` placeholder is the domain and the `{SID GROUP NAME}` is the SID group name:

```
if (User.Identity is WindowsIdentity windowsIdentity)
{
    var groups = windowsIdentity.Groups;

    // If needed, obtain a list of the SID groups
    var securityGroups = 
        groups.Select(g => g.Translate(typeof(NTAccount)).ToString()).ToList();

    // If needed, obtain the user's Windows identity name
    var windowsIdentityName = windowsIdentity.Name;

    // Check if the user is in a specific SID group
    if (User.IsInRole(@"{DOMAIN}\{SID GROUP NAME}"))
    {
        // User is in the specified group
    }
    else
    {
        // User isn't in the specified group
    }
}
else
{
    // The user isn't authenticated with Windows Authentication
}
```

For a demonstration of related code that translates SID group claims into human-readable values in a Blazor app, see the `UserClaims` component in [Secure an ASP.NET Core Blazor Web App with Windows Authentication](../../../blazor/security/blazor-web-app-with-windows-authentication?view=aspnetcore-10.0). Such an approach to retrieving SID group claims can be combined with [adding claims with an `IClaimsTransformation`](../../../security/authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation) to create custom role claims when a user is authenticated.

An approach similar to the preceding example for retrieving SID group claims can be combined with [adding claims with an `IClaimsTransformation`](../../../security/authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation) to create custom role claims when a user is authenticated.

## Related content

* [Role-based authorization in ASP.NET Core](../../../security/authorization/roles?view=aspnetcore-10.0)
* [ASP.NET Core Blazor authentication and authorization](../../../blazor/security/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor WebAssembly with Microsoft Entra ID groups and roles](../../../blazor/security/webassembly/microsoft-entra-id-groups-and-roles?view=aspnetcore-10.0)
* [Role-based authorization in ASP.NET Core MVC](../../../mvc/security/authorization/roles?view=aspnetcore-10.0)
* [Extend or add custom claims, including role claims, using `IClaimsTransformation`](../../../security/authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/bl0007
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/bl0007.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# BL0007: Component parameter '{0}' should be auto property

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | BL0007       |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A [component parameter](../blazor/components/?view=aspnetcore-10.0#component-parameters), which is a public [C# property](/en-us/dotnet/csharp/programming-guide/classes-and-structs/properties) of a component class with the [`[Parameter]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute), isn't an [automatically-implemented property (*auto property*)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties).

## Rule description

A component parameter is a framework-managed communication channel between a parent and a child component. Developers shouldn't read or write to the parameter in a [`get` (getter) or `set` (setter) accessor](/en-us/dotnet/csharp/programming-guide/classes-and-structs/using-properties), either from inside or outside the component.

Possible side effects of interacting directly with a component parameter include infinite rendering loops, unexpected extra renderings, and parameter value overwrites.

## How to fix violations

Implement the component parameter as an auto property and override [OnParametersSet](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparametersset) or [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) in the component class to read or transform the parameter's value. For more information, see the [`OnParametersSet{Async}` lifecycle method](../blazor/components/lifecycle?view=aspnetcore-10.0#after-parameters-are-set-onparameterssetasync).

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-05-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core security topics

ASP.NET Core enables developers to configure and manage security. The following list provides links to articles about working with security in ASP.NET Core:

* [Authentication](authentication/?view=aspnetcore-10.0)
* [Authorization](authorization/introduction?view=aspnetcore-10.0)
* [Data protection](data-protection/introduction?view=aspnetcore-10.0)
* [HTTPS enforcement](enforcing-ssl?view=aspnetcore-10.0)
* [Safe storage of app secrets in development](app-secrets?view=aspnetcore-10.0)
* [XSRF/CSRF prevention](anti-request-forgery?view=aspnetcore-10.0)
* [Cross Origin Resource Sharing (CORS)](cors?view=aspnetcore-10.0)
* [Cross-Site Scripting (XSS) attacks](cross-site-scripting?view=aspnetcore-10.0)

These security features allow you to build robust and secure ASP.NET Core apps.

For Blazor security coverage, which adds to or supersedes the guidance in this node, see [ASP.NET Core Blazor authentication and authorization](../blazor/security/?view=aspnetcore-10.0) and the other articles in Blazor's *Security and Identity* node.

## ASP.NET Core security features

ASP.NET Core provides many tools and libraries to secure ASP.NET Core apps, such as built-in identity providers and non-Microsoft identity services like Facebook, Twitter, and LinkedIn. ASP.NET Core provides several approaches to store app secrets.

## Authentication vs. Authorization

[Authentication](authentication/?view=aspnetcore-10.0) is a process where a user provides credentials that are compared to credentials stored in an operating system, database, app, or resource. When the two sets of credentials match, the user authenticates successfully. They can then perform actions for which they're authorized. The [authorization](authorization/introduction?view=aspnetcore-10.0) process determines the actions the user is allowed to do.

Another way to think of authentication is to consider it as a way to **enter** a space, where the space is a server, database, app, or resource. Authorization defines **what actions** the user can perform to which objects inside that space (server, database, or app).

## Common vulnerabilities in software

ASP.NET Core and Entity Framework contain features that help you secure your apps and prevent security breaches. The following list of links takes you to documentation detailing techniques to avoid the most common security vulnerabilities in web apps:

* [Cross-Site Scripting (XSS) attacks](cross-site-scripting?view=aspnetcore-10.0)
* [SQL queries > SQL injection attacks](/en-us/ef/core/querying/sql-queries#passing-parameters)
* [Cross-Site Request Forgery (XSRF/CSRF) attacks](anti-request-forgery?view=aspnetcore-10.0)
* [Open redirect attacks](preventing-open-redirects?view=aspnetcore-10.0)

There are more vulnerabilities that you should be aware of. For more information, see the other articles in the **Security and Identity** section of the table of contents.

## Secure authentication flows

We recommend using the most secure authentication option. For Azure services, the most secure authentication is [managed identities](/en-us/entra/identity/managed-identities-azure-resources/overview).

Avoid using the Resource Owner Password Credentials (ROPG) grant:

* It exposes the user's password to the client.
* It's a significant security risk.
* Use it only when other authentication flows aren't possible.

Managed identities are a secure way to authenticate to services without needing to store credentials in code, environment variables, or configuration files. Managed identities are available for Azure services, and can be used with Azure SQL, Azure Storage, and other Azure services:

* [Managed identities in Microsoft Entra for Azure SQL](/en-us/azure/azure-sql/database/authentication-azure-ad-user-assigned-managed-identity)
* [Managed identities for App Service and Azure Functions](/en-us/azure/app-service/overview-managed-identity)
* [Secure authentication flows](/en-us/entra/identity-platform/authentication-flows-app-scenarios#web-app-that-signs-in-a-user)

When the app is deployed to a test server, an environment variable can be used to set the connection string to a test database server. For more information, see [Configuration](../fundamentals/configuration/?view=aspnetcore-10.0). Environment variables are commonly stored in plain, unencrypted text. If the machine or process is compromised, environment variables might be accessible to untrusted parties. We recommend against using environment variables to store a production connection string as it's not the most secure approach.

Configuration data guidelines:

* Never store passwords or other sensitive data in configuration provider code or in plain text configuration files. The [Secret Manager](app-secrets?view=aspnetcore-10.0) tool can be used to store secrets in development.
* Don't use production secrets in development or test environments.
* Specify secrets outside of the project so that they can't be accidentally committed to a source code repository.

For more information, see:

* [Managed identity best practice recommendations](/en-us/entra/identity/managed-identities-azure-resources/managed-identity-best-practice-recommendations)
* [Connecting from your application to resources without handling credentials in your code](/en-us/entra/identity/managed-identities-azure-resources/overview-for-developers?tabs=portal%2Cdotnet)
* [Azure services that can use managed identities to access other services](/en-us/entra/identity/managed-identities-azure-resources/managed-identities-status)
* [IETF OAuth 2.0 Security Best Current Practice (Section 2.4. Resource Owner Password Credentials Grant)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.4)

For information on other cloud providers, see:

* [AWS (Amazon Web Services): AWS Key Management Service (KMS)](https://aws.amazon.com/kms/)
* [Google Cloud Key Management Service overview](https://docs.cloud.google.com/kms/docs/key-management-service)

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

## Related content

* [Introduction to Identity on ASP.NET Core](authentication/identity?view=aspnetcore-10.0)
* [Enable QR code generation for TOTP authentication](authentication/identity-enable-qrcodes?view=aspnetcore-10.0)
* [Using external login providers with Identity in ASP.NET Core](authentication/social/?view=aspnetcore-10.0)
* [Identity management solutions for .NET web apps](identity-management-solutions?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/movie-database-app/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor movie database app (Overview)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This tutorial explains the basics of building a Blazor Web App with a database, Entity Framework (EF) Core, and user interactivity.

Parts of this series include:

1. [Create a Blazor Web App](part-1?view=aspnetcore-10.0)
2. [Add and scaffold a model](part-2?view=aspnetcore-10.0)
3. [Learn about Razor components](part-3?view=aspnetcore-10.0)
4. [Work with a database](part-4?view=aspnetcore-10.0)
5. [Add validation](part-5?view=aspnetcore-10.0)
6. [Add search](part-6?view=aspnetcore-10.0)
7. [Add a new field](part-7?view=aspnetcore-10.0)
8. [Add interactivity](part-8?view=aspnetcore-10.0)

At the end of the tutorial, you'll have a Blazor Web App that can display and manage movies in a movie database.

## Secure authentication flow required for production apps

This tutorial uses a local database that doesn't require user authentication. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production Blazor Web Apps, see [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0) and the following articles in the *Server* security node:


## Sample app

If you don't intend to create the demonstration app while reading the article, you can refer to the completed sample app in the [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples). Select the latest version folder in the repository. The sample folder for this tutorial's project is named `BlazorWebAppMovies`.

To run the sample locally, apply the pending migrations, which include a migration to create the database. Use the following instructions:

If the [`dotnet-ef` tool](/en-us/ef/core/miscellaneous/cli/dotnet) isn't installed, install the tool with the following command from either a command shell, the Developer PowerShell command shell in Visual Studio, or the **Terminal** PowerShell command shell in VS Code:

```
dotnet tool install --global dotnet-ef
```

From a command shell opened to the project's root folder (the project directory containing the `BlazorWebAppMovies.csproj` file), execute the following command:

```
dotnet ef database update
```

After executing the preceding command, run the sample using any of the following approaches:

* Visual Studio

  * Select the **Run** button.
  * Use **Debug** > **Start Debugging** from the menu.
  * Press `F5`.

* .NET CLI command shell: Execute the `dotnet watch` (or `dotnet run`) command from the project's root folder.

## Article code examples

The line breaks of code examples shown in the ASP.NET Core documentation often don't match line breaks in scaffolded code generated by tooling for an app. This is due to an article publishing limitation. Lines of code in articles are generally limited to 85 characters in length, and we manually adjust the line length using line breaks to satisfy our publishing guidelines.

As you work through this tutorial or use any other ASP.NET Core article's code examples, you never need to adjust scaffolded code in your app to match the line breaks displayed in article code examples.

## Report a tutorial issue

To open a documentation GitHub issue for an article of the series, use the **Open a documentation issue** link at the bottom of the article. Using the link to create your issue adds important tracking metadata to the issue and automatically pings the author of the article.

## Support requests

We welcome feedback on the tutorial's articles, such as bug reports and comments on the article's text, but we're often unable to provide product support. If you run into a problem while following the tutorial, don't immediately open a documentation issue. Check the steps that you've taken against the article and compare your code to the [sample app](#sample-app) before opening an issue because many problems can be traced to missing a step or not following a step correctly.

For general questions about .NET and Blazor beyond the tutorial and reference documentation or to obtain assistance from the .NET community, converse with developers in [public forums](../../fundamentals/?view=aspnetcore-10.0#support-requests).

## Next steps

[Next: Create a Blazor Web App](part-1?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/http2
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel/http2.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use HTTP/2 with the ASP.NET Core Kestrel web server

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is available for ASP.NET Core apps if the following base requirements are met:

* Operating system

  * Windows Server 2016/Windows 10 or later‡
  * Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later)
  * macOS 10.15 or later

* Target framework: .NET Core 2.2 or later

* [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) connection

* TLS 1.2 or later connection

‡Kestrel has limited support for HTTP/2 on Windows Server 2012 R2 and Windows 8.1. Support is limited because the list of supported TLS cipher suites available on these operating systems is limited. A certificate generated using an Elliptic Curve Digital Signature Algorithm (ECDSA) may be required to secure TLS connections.

If an HTTP/2 connection is established, [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) reports `HTTP/2`.

Starting with .NET Core 3.0, HTTP/2 is enabled by default. For more information on configuration, see the [Kestrel HTTP/2 limits](options?view=aspnetcore-10.0#http2-limits) and [ListenOptions.Protocols](endpoints?view=aspnetcore-10.0#listenoptionsprotocols) sections.

## Advanced HTTP/2 features

Additional HTTP/2 features in Kestrel support gRPC, including support for response trailers and sending reset frames.

### Trailers

HTTP Trailers are similar to HTTP Headers, except they are sent after the response body is sent. For IIS and HTTP.sys, only HTTP/2 response trailers are supported.

```
if (httpContext.Response.SupportsTrailers())
{
    httpContext.Response.DeclareTrailer("trailername");	

    // Write body
    httpContext.Response.WriteAsync("Hello world");

    httpContext.Response.AppendTrailer("trailername", "TrailerValue");
}
```

In the preceding example code:

* `SupportsTrailers` ensures that trailers are supported for the response.
* `DeclareTrailer` adds the given trailer name to the `Trailer` response header. Declaring a response's trailers is optional, but recommended. If `DeclareTrailer` is called, it must be before the response headers are sent.
* `AppendTrailer` appends the trailer.

### Reset

Reset allows for the server to reset a HTTP/2 request with a specified error code. A reset request is considered aborted.

```
var resetFeature = httpContext.Features.Get<IHttpResetFeature>();
resetFeature.Reset(errorCode: 2);
```

`Reset` in the preceding code example specifies the `INTERNAL_ERROR` error code. For more information about HTTP/2 error codes, visit the [HTTP/2 specification error code section](https://tools.ietf.org/html/rfc7540#page-50).

Starting with .NET Core 3.0, HTTP/2 is enabled by default. For more information on configuration, see the [Kestrel HTTP/2 limits](options?view=aspnetcore-10.0#http2-limits) and [ListenOptions.Protocols](endpoints?view=aspnetcore-10.0#listenoptionsprotocols) sections.

## Advanced HTTP/2 features

Additional HTTP/2 features in Kestrel support gRPC, including support for response trailers and sending reset frames.

### Trailers

HTTP Trailers are similar to HTTP Headers, except they are sent after the response body is sent. For IIS and HTTP.sys, only HTTP/2 response trailers are supported.

```
if (httpContext.Response.SupportsTrailers())
{
    httpContext.Response.DeclareTrailer("trailername");	

    // Write body
    httpContext.Response.WriteAsync("Hello world");

    httpContext.Response.AppendTrailer("trailername", "TrailerValue");
}
```

In the preceding example code:

* `SupportsTrailers` ensures that trailers are supported for the response.
* `DeclareTrailer` adds the given trailer name to the `Trailer` response header. Declaring a response's trailers is optional, but recommended. If `DeclareTrailer` is called, it must be before the response headers are sent.
* `AppendTrailer` appends the trailer.

### Reset

Reset allows for the server to reset a HTTP/2 request with a specified error code. A reset request is considered aborted.

```
var resetFeature = httpContext.Features.Get<IHttpResetFeature>();
resetFeature.Reset(errorCode: 2);
```

`Reset` in the preceding code example specifies the `INTERNAL_ERROR` error code. For more information about HTTP/2 error codes, visit the [HTTP/2 specification error code section](https://tools.ietf.org/html/rfc7540#page-50).

Starting with .NET Core 3.0, HTTP/2 is enabled by default. For more information on configuration, see the [Kestrel HTTP/2 limits](options?view=aspnetcore-10.0#http2-limits) and [ListenOptions.Protocols](endpoints?view=aspnetcore-10.0#listenoptionsprotocols) sections.

## Advanced HTTP/2 features

Additional HTTP/2 features in Kestrel support gRPC, including support for response trailers and sending reset frames.

### Trailers

HTTP Trailers are similar to HTTP Headers, except they are sent after the response body is sent. For IIS and HTTP.sys, only HTTP/2 response trailers are supported.

```
if (httpContext.Response.SupportsTrailers())
{
    httpContext.Response.DeclareTrailer("trailername");	

    // Write body
    httpContext.Response.WriteAsync("Hello world");

    httpContext.Response.AppendTrailer("trailername", "TrailerValue");
}
```

In the preceding example code:

* `SupportsTrailers` ensures that trailers are supported for the response.
* `DeclareTrailer` adds the given trailer name to the `Trailer` response header. Declaring a response's trailers is optional, but recommended. If `DeclareTrailer` is called, it must be before the response headers are sent.
* `AppendTrailer` appends the trailer.

### Reset

Reset allows for the server to reset a HTTP/2 request with a specified error code. A reset request is considered aborted.

```
var resetFeature = httpContext.Features.Get<IHttpResetFeature>();
resetFeature.Reset(errorCode: 2);
```

`Reset` in the preceding code example specifies the `INTERNAL_ERROR` error code. For more information about HTTP/2 error codes, visit the [HTTP/2 specification error code section](https://tools.ietf.org/html/rfc7540#page-50).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure ASP.NET Core Blazor WebAssembly

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Blazor WebAssembly apps are secured in the same manner as single-page applications (SPAs). There are several approaches for authenticating users to SPAs, but the most common and comprehensive approach is to use an implementation based on the [OAuth 2.0 protocol](https://oauth.net/), such as [OpenID Connect (OIDC)](https://openid.net/developers/how-connect-works/).

The Blazor WebAssembly security documentation primarily focuses on how to accomplish user authentication and authorization tasks. For OAuth 2.0/OIDC general concept coverage, see the resources in the [main overview article's *Related content* section](../?view=aspnetcore-10.0#related-content).

## Client-side/SPA security of sensitive data and credentials

A Blazor WebAssembly app's .NET/C# codebase is served to clients, and the app's code can't be protected from inspection and tampering by users. Never place sensitive data into a Blazor WebAssembly app, such as app secrets, connection strings, passwords, security keys, and private .NET/C# code.

The following technologies are useful for storing sensitive data, which can be used together in the same app to split responsibilities for storing data among `Development`, `Staging`, and `Production` environments:

* [Secret Manager tool](../../../security/app-secrets?view=aspnetcore-10.0): Only used on the local development system.
* [Azure Key Vault](https://azure.microsoft.com/products/key-vault/): Can be used for locally-running apps in the `Development` environment and for `Staging`/`Production` deployments.

For examples of the preceding approaches, see [Account confirmation and password recovery in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](standalone-with-identity/account-confirmation-and-password-recovery?view=aspnetcore-10.0#configure-a-secret-for-the-email-providers-security-key).

## Web API requests

To protect .NET/C# code and data, use [ASP.NET Core Data Protection](../../../security/data-protection/introduction?view=aspnetcore-10.0) features with a server-side ASP.NET Core backend web API. The client-side Blazor WebAssembly app calls the server-side web API for secure app features and data processing. For more information, see [Call a web API from an ASP.NET Core Blazor app](../../call-web-api?view=aspnetcore-10.0\&pivots=webassembly) and the articles and examples in this documentation node.

Blazor WebAssembly apps are often prevented from making direct calls across origins to web APIs due to [Cross-Origin Request Sharing (CORS) security](../../call-web-api?view=aspnetcore-10.0#cross-origin-resource-sharing-cors). A typical exception looks like the following:

> Access to fetch at '{URL}' from origin 'https\://localhost:{PORT}' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Even if you call [SetBrowserRequestMode](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserrequestmode) with a [BrowserRequestMode](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.browserrequestmode) field of `NoCors` (1) seeking to circumvent the preceding exception, the request usually fails due to CORS restrictions on the web API's origin, such as a restriction that only allows calls from specific origins or a restriction that prevents JavaScript [`fetch`](https://developer.mozilla.org/docs/Web/API/Fetch_API/Using_Fetch) requests from a browser. The only way for such calls to succeed is for the web API that you're calling to allow your origin to call its origin with the correct CORS configuration. Most external web APIs don't allow you to configure their CORS policies. To deal with this restriction, adopt either of the following strategies:

* Maintain your own server-side ASP.NET Core backend web API. The client-side Blazor WebAssembly app calls your server-side web API, and your web API makes the request from its server-based C# code (not a browser) to the external web API with the correct CORS headers, returning the result to your client-side Blazor WebAssembly app.

* Use a proxy service to proxy the request from the client-side Blazor WebAssembly app to the external web API. The proxy service uses a server-side app to make the request on the client's behalf and returns the result after the call succeeds. In the following example based on [CloudFlare's CORS PROXY](https://corsproxy.io/), the `{REQUEST URI}` placeholder is the request URI:

  ```
  @using System.Net
  @inject IHttpClientFactory ClientFactory

  ...

  @code {
      public async Task CallApi()
      {
          var client = ClientFactory.CreateClient();

          var urlEncodedRequestUri = WebUtility.UrlEncode("{REQUEST URI}");

          using var request = new HttpRequestMessage(HttpMethod.Get, 
              $"https://corsproxy.io/?{urlEncodedRequestUri}");

          using var response = await client.SendAsync(request);

          ...
      }
  }
  ```

## Authentication library

Blazor WebAssembly supports authenticating and authorizing apps using OIDC via the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) library using the [Microsoft identity platform](/en-us/entra/identity-platform/). The library provides a set of primitives for seamlessly authenticating against ASP.NET Core backends. The library can authenticate against any third-party Identity Provider (IP) that supports OIDC, which are called OpenID Providers (OP).

The authentication support in the Blazor WebAssembly Library (`Authentication.js`) is built on top of the [Microsoft Authentication Library (MSAL, `msal.js`)](/en-us/entra/identity-platform/msal-overview), which is used to handle the underlying authentication protocol details. The Blazor WebAssembly Library only supports the Proof Key for Code Exchange (PKCE) authorization code flow. Implicit grant isn't supported.

Other options for authenticating SPAs exist, such as the use of SameSite cookies. However, the engineering design of Blazor WebAssembly uses OAuth and OIDC as the best option for authentication in Blazor WebAssembly apps. [Token-based authentication](../../../security/anti-request-forgery?view=aspnetcore-10.0#token-based-authentication) based on [JSON Web Tokens (JWTs)](https://datatracker.ietf.org/doc/html/rfc7519) was chosen over [cookie-based authentication](../../../security/anti-request-forgery?view=aspnetcore-10.0#cookie-based-authentication) for functional and security reasons:

* Using a token-based protocol offers fewer vulnerabilities, as the tokens aren't sent in all requests.
* The tokens are explicitly sent to the server, so server endpoints don't require protection against [Cross-Site Request Forgery (CSRF)](../../../security/anti-request-forgery?view=aspnetcore-10.0). This allows you to host Blazor WebAssembly apps alongside MVC or Razor pages apps.
* Tokens have narrower permissions than cookies. For example, tokens can't be used to manage the user account or change a user's password unless such functionality is explicitly implemented.
* Tokens have a short lifetime, one hour, which limits the attack window. Tokens can also be revoked at any time.
* Self-contained JWTs offer guarantees to the client and server about the authentication process. For example, a client has the means to detect and validate that the tokens it receives are legitimate and were emitted as part of a given authentication process. If a third party attempts to switch a token in the middle of the authentication process, the client can detect the switched token and avoid using it.
* Tokens with OAuth and OIDC don't rely on the user agent behaving correctly to ensure that the app is secure.
* Token-based protocols, such as OAuth and OIDC, allow for authenticating and authorizing users in standalone Blazor Webassembly apps with the same set of security characteristics.

- Using a token-based protocol offers fewer vulnerabilities, as the tokens aren't sent in all requests.
- The tokens are explicitly sent to the server, so server endpoints don't require protection against [Cross-Site Request Forgery (CSRF)](../../../security/anti-request-forgery?view=aspnetcore-10.0). This allows you to host Blazor WebAssembly apps alongside MVC or Razor pages apps.
- Tokens have narrower permissions than cookies. For example, tokens can't be used to manage the user account or change a user's password unless such functionality is explicitly implemented.
- Tokens have a short lifetime, one hour, which limits the attack window. Tokens can also be revoked at any time.
- Self-contained JWTs offer guarantees to the client and server about the authentication process. For example, a client has the means to detect and validate that the tokens it receives are legitimate and were emitted as part of a given authentication process. If a third party attempts to switch a token in the middle of the authentication process, the client can detect the switched token and avoid using it.
- Tokens with OAuth and OIDC don't rely on the user agent behaving correctly to ensure that the app is secure.
- Token-based protocols, such as OAuth and OIDC, allow for authenticating and authorizing users of hosted Blazor WebAssembly solution clients and standalone Blazor Webassembly apps with the same set of security characteristics.

Important

For versions of ASP.NET Core that adopt Duende Identity Server in Blazor project templates, [Duende Software](https://duendesoftware.com/) might require you to pay a license fee for production use of Duende Identity Server. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../../../migration/50-to-60?view=aspnetcore-10.0#project-templates-use-duende-identity-server).

## Authentication process with OIDC

The [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) library offers several primitives to implement authentication and authorization using OIDC. In broad terms, authentication works as follows:

* When an anonymous user selects the login button or requests a Razor component or page with the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) applied, the user is redirected to the app's login page (`/authentication/login`).

* In the login page, the authentication library prepares for a redirect to the authorization endpoint. The authorization endpoint is outside of the Blazor WebAssembly app and can be hosted at a separate origin. The endpoint is responsible for determining whether the user is authenticated and for issuing one or more tokens in response. The authentication library provides a login callback to receive the authentication response.

  * If the user isn't authenticated, the user is redirected to the underlying authentication system, which is usually ASP.NET Core Identity.
  * If the user was already authenticated, the authorization endpoint generates the appropriate tokens and redirects the browser back to the login callback endpoint (`/authentication/login-callback`).

* When the Blazor WebAssembly app loads the login callback endpoint (`/authentication/login-callback`), the authentication response is processed.

  * If the authentication process completes successfully, the user is authenticated and optionally sent back to the original protected URL that the user requested.
  * If the authentication process fails for any reason, the user is sent to the login failed page (`/authentication/login-failed`), where an error is displayed.

## `Authentication` component

The `Authentication` component (`Authentication.razor`) handles remote authentication operations and permits the app to:

* Configure app routes for authentication states.
* Set UI content for authentication states.
* Manage authentication state.

Authentication actions, such as registering or signing in a user, are passed to the Blazor framework's [RemoteAuthenticatorViewCore\<TAuthenticationState>](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticatorviewcore-1) component, which persists and controls state across authentication operations.

For more information and examples, see [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0).

## Authorization

In Blazor WebAssembly apps, authorization checks can be bypassed because all client-side code can be modified by users. The same is true for all client-side app technologies, including JavaScript SPA frameworks or native apps for any operating system.

**Always perform authorization checks on the server within any API endpoints accessed by your client-side app.**

## Customize authentication

Blazor WebAssembly provides methods to add and retrieve additional parameters for the underlying Authentication library to conduct remote authentication operations with external identity providers.

To pass additional parameters, [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) supports passing and retrieving history entry state when performing external location changes. For more information, see the following resources:

* Blazor *Fundamentals* > *Navigation* article

  * [Navigation history state](../../fundamentals/navigation?view=aspnetcore-10.0#navigation-history-state)
  * [Navigation options](../../fundamentals/navigation?view=aspnetcore-10.0#navigation-options)

* MDN documentation: [History API](https://developer.mozilla.org/docs/Web/API/History_API)

The state stored by the History API provides the following benefits for remote authentication:

* The state passed to the secured app endpoint is tied to the navigation performed to authenticate the user at the `authentication/login` endpoint.
* Extra work encoding and decoding data is avoided.
* Vulnerabilities are reduced. Unlike using the query string to store navigation state, a top-level navigation or influence from a different origin can't set the state stored by the History API.
* The history entry is replaced upon successful authentication, so the state attached to the history entry is removed and doesn't require clean up.

[InteractiveRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions) represents the request to the identity provider for logging in or provisioning an access token.

[NavigationManagerExtensions](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions) provides the [NavigateToLogin](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.navigationmanagerextensions.navigatetologin) method for a login operation and [NavigateToLogout](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.navigationmanagerextensions.navigatetologout) for a logout operation. The methods call [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto), setting the history entry state with a passed [InteractiveRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions) or a new [InteractiveRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions) instance created by the method for:

* A user signing in ([InteractionType.SignIn](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiontype#microsoft-aspnetcore-components-webassembly-authentication-interactiontype-signin)) with the current URI for the return URL.
* A user signing out ([InteractionType.SignOut](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiontype#microsoft-aspnetcore-components-webassembly-authentication-interactiontype-signout)) with the return URL.

The following authentication scenarios are covered in the [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0#custom-authentication-request-scenarios) article:

* Customize the login process
* Logout with a custom return URL
* Customize options before obtaining a token interactively
* Customize options when using an [IAccessTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.iaccesstokenprovider)
* Obtain the login path from authentication options

## Blazor WebAssembly authorization patterns

*For patterns that apply to server-side Blazor apps (Blazor Web Apps, Blazor Server apps), see [ASP.NET Core Blazor additional server-side security scenarios](../additional-scenarios?view=aspnetcore-10.0#server-side-blazor-app-authorization-patterns).*

Unlike server-side Blazor apps, Blazor WebAssembly apps don't support setting an [AuthorizationOptions.FallbackPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.fallbackpolicy#microsoft-aspnetcore-authorization-authorizationoptions-fallbackpolicy) to a policy with [RequireAuthenticatedUser](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireauthenticateduser). The [server-side fallback policy](../../../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies) is enforced by authorization middleware and isn't a client-side authorization mechanism. Therefore, the only supported pattern for Blazor WebAssembly apps is to apply the [`[Authorize]` attribute](../?view=aspnetcore-10.0#authorize-attribute) ([API documentation](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute)) to Razor components using ***one*** of the following approaches:

* In the app's imports file, add an [`@using`](../../../mvc/views/razor?view=aspnetcore-10.0#using) directive for the [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization) namespace with an [`@attribute`](../../../mvc/views/razor?view=aspnetcore-10.0#attribute) directive for the [`[Authorize]` attribute](../?view=aspnetcore-10.0#authorize-attribute).

  `_Imports.razor`:

  ```
  @using Microsoft.AspNetCore.Authorization
  @attribute [Authorize]
  ```

  Allow anonymous access to the `Authentication` component to permit redirection to the identity provider. Add the following Razor code to the `Authentication` component under its [`@page`](../../../mvc/views/razor?view=aspnetcore-10.0#page) directive.

  `Authentication.razor`:

  ```
  @using Microsoft.AspNetCore.Components.WebAssembly.Authentication
  @attribute [AllowAnonymous]
  ```

* Add the attribute to each Razor component under the [`@page`](../../../mvc/views/razor?view=aspnetcore-10.0#page) directive:

  ```
  @using Microsoft.AspNetCore.Authorization
  @attribute [Authorize]
  ```

## Use one identity provider app registration per app

Some of the articles under this *Overview* pertain to Blazor hosting scenarios that involve two or more apps. A standalone Blazor WebAssembly app uses web API with authenticated users to access server resources and data provided by a server app.

When this scenario is implemented in documentation examples, ***two*** identity provider registrations are used, one for the client app and one for the server app. Using separate registrations, for example in Microsoft Entra ID, isn't strictly required. However, using two registrations is a security best practice because it isolates the registrations by app. Using separate registrations also allows independent configuration of the client and server registrations.

Some of the articles under this *Overview* pertain to either of the following Blazor hosting scenarios that involve two or more apps:

* A hosted Blazor WebAssembly solution, which is composed of two apps: a client-side Blazor WebAssembly app and a server-side ASP.NET Core host app. Authenticated users to the client app access server resources and data provided by the server app.
* A standalone Blazor WebAssembly app that uses web API with authenticated users to access server resources and data provided by a server app. This scenario is similar to using a hosted Blazor WebAssembly solution; but in this case, the client app isn't hosted by the server app.

When these scenarios are implemented in documentation examples, ***two*** identity provider registrations are used, one for the client app and one for the server app. Using separate registrations, for example in Microsoft Entra ID, isn't strictly required. However, using two registrations is a security best practice because it isolates the registrations by app. Using separate registrations also allows independent configuration of the client and server registrations.

## Refresh tokens

Although refresh tokens can't be secured in Blazor WebAssembly apps, they can be used if you implement them with appropriate security strategies.

For standalone Blazor WebAssembly apps in ASP.NET Core in .NET 6 or later, we recommend using:

* The [OAuth 2.0 Authorization Code flow (Code) with Proof Key for Code Exchange (PKCE)](https://oauth.net/2/pkce/).
* A refresh token that has a short expiration.
* A [rotated](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation) refresh token.
* A refresh token with an expiration after which a new interactive authorization flow is required to refresh the user's credentials.

For hosted Blazor WebAssembly solutions, refresh tokens can be maintained and used by the server-side app in order to access third-party APIs. For more information, see [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0#authenticate-users-with-a-third-party-provider-and-call-protected-apis-on-the-host-server-and-the-third-party).

For more information, see the following resources:

* [Microsoft identity platform refresh tokens: Refresh token lifetime](/en-us/entra/identity-platform/refresh-tokens#refresh-token-lifetime)
* [OAuth 2.0 for Browser-Based Apps (IETF specification)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps-11#section-4)

## Establish claims for users

Apps often require claims for users based on a web API call to a server. For example, claims are frequently used to [establish authorization](../?view=aspnetcore-10.0#authorization) in an app. In these scenarios, the app requests an access token to access the service and uses the token to obtain user data for creating claims.

For examples, see the following resources:

* [Additional scenarios: Customize the user](additional-scenarios?view=aspnetcore-10.0#customize-the-user)
* [ASP.NET Core Blazor WebAssembly with Microsoft Entra ID groups and roles](microsoft-entra-id-groups-and-roles?view=aspnetcore-10.0)

## Prerendering support

[Prerendering](../../components/prerender?view=aspnetcore-10.0) isn't supported for authentication endpoints (`/authentication/` path segment).

[Prerendering](../../components/integration?view=aspnetcore-10.0) isn't supported for authentication endpoints (`/authentication/` path segment).

For more information, see [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0#prerendering-with-authentication).

## Azure App Service on Linux with Identity Server

Specify the issuer explicitly when deploying to Azure App Service on Linux with Identity Server.

For more information, see [Use Identity to secure a Web API backend for SPAs](../../../security/authentication/identity-api-authorization?view=aspnetcore-10.0#azure-app-service-on-linux).

## Windows Authentication

We don't recommend using Windows Authentication with Blazor Webassembly or with any other SPA framework. We recommend using token-based protocols instead of Windows Authentication, such as OIDC with Active Directory Federation Services (ADFS).

If Windows Authentication is used with Blazor Webassembly or with any other SPA framework, additional measures are required to protect the app from cross-site request forgery (CSRF) tokens. The same concerns that apply to cookies apply to Windows Authentication with the addition that Windows Authentication doesn't offer a mechanism to prevent sharing of the authentication context across origins. Apps using Windows Authentication without additional protection from CSRF should at least be restricted to an organization's intranet and not be used on the open Internet.

For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../../security/anti-request-forgery?view=aspnetcore-10.0).

## Secure a SignalR hub

To secure a SignalR hub in the server API project, apply the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the hub class or to methods of the hub class.

In a client project with prerendering, such as hosted Blazor WebAssembly (ASP.NET Core in .NET 7 or earlier) or a Blazor Web App (ASP.NET Core in .NET 8 or later), see the guidance in [ASP.NET Core Blazor SignalR guidance](../../fundamentals/signalr?view=aspnetcore-10.0#client-side-signalr-cross-origin-negotiation-for-authentication).

In a client project component without prerendering, such as standalone Blazor WebAssembly, or non-browser apps, supply an access token to the hub connection, as the following example demonstrates. For more information, see [SignalR authentication and authorization](../../../signalr/authn-and-authz?view=aspnetcore-10.0#bearer-token-authentication).

```
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject NavigationManager Navigation

...

var tokenResult = await TokenProvider.RequestAccessToken();

if (tokenResult.TryGetToken(out var token))
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"), 
            options => { options.AccessTokenProvider = () => Task.FromResult(token?.Value); })
        .Build();

  ...
}
```

## Logging

*This section applies to Blazor WebAssembly apps in ASP.NET Core in .NET 7 or later.*

To enable debug or trace logging, see the *Authentication logging (Blazor WebAssembly)* section in a .NET 7 or later version of the [ASP.NET Core Blazor logging](../../fundamentals/logging?view=aspnetcore-10.0) article.

## The WebAssembly sandbox

The WebAssembly *sandbox* restricts access to the environment of the system executing WebAssembly code, including access to I/O subsystems, system storage and resources, and the operating system. The isolation between WebAssembly code and the system that executes the code makes WebAssembly a safe coding framework for systems. However, WebAssembly is vulnerable to side-channel attacks at the hardware level. Normal precautions and due diligence in sourcing hardware and placing limitations on accessing hardware apply.

*WebAssembly isn't owned or maintained by Microsoft.*

For more information, see the following W3C resources:

* [WebAssembly: Security](https://webassembly.org/docs/security/)
* [WebAssembly Specification: Security Considerations](https://webassembly.github.io/spec/core/intro/introduction.html#security-considerations)
* [W3C WebAssembly Community Group: Feedback and issues](https://webassembly.org/community/feedback/): The W3C WebAssembly Community Group link is only provided for reference, making it clear that WebAssembly security vulnerabilities and bugs are patched on an ongoing basis, often reported and addressed by browser. ***Don't send feedback or bug reports on Blazor to the W3C WebAssembly Community Group.*** Blazor feedback should be reported to the [Microsoft ASP.NET Core product unit](https://github.com/dotnet/aspnetcore/issues). If the Microsoft product unit determines that an underlying problem with WebAssembly exists, they take the appropriate steps to report the problem to the W3C WebAssembly Community Group.

## Implementation guidance

Articles under this *Overview* provide information on authenticating users in Blazor WebAssembly apps against specific providers.

Standalone Blazor WebAssembly apps:

* [General guidance for OIDC providers and the WebAssembly Authentication Library](standalone-with-authentication-library?view=aspnetcore-10.0)
* [Microsoft Accounts](standalone-with-microsoft-accounts?view=aspnetcore-10.0)
* [Microsoft Entra ID (ME-ID)](standalone-with-microsoft-entra-id?view=aspnetcore-10.0)
* [Azure Active Directory (AAD) B2C](standalone-with-azure-active-directory-b2c?view=aspnetcore-10.0)

Hosted Blazor WebAssembly apps:

* [Microsoft Entra ID (ME-ID)](hosted-with-microsoft-entra-id?view=aspnetcore-10.0)
* [Azure Active Directory (AAD) B2C](hosted-with-azure-active-directory-b2c?view=aspnetcore-10.0)
* [Identity Server](hosted-with-identity-server?view=aspnetcore-10.0)

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

Further configuration guidance is found in the following articles:

* [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0)
* [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0)

## Use the Authorization Code flow with PKCE

Microsoft identity platform's [Microsoft Authentication Library for JavaScript (MSAL)](/en-us/entra/identity-platform/msal-overview) v2.0 or later provides support for the [Authorization Code flow](/en-us/entra/identity-platform/v2-oauth2-auth-code-flow) with [Proof Key for Code Exchange (PKCE)](https://oauth.net/2/pkce/) and [Cross-Origin Resource Sharing (CORS)](../../../security/cors?view=aspnetcore-10.0) for single-page applications, including Blazor.

**Microsoft doesn't recommend using Implicit grant.**

For more information, see the following resources:

* [Authentication flow support in MSAL: Implicit grant](/en-us/entra/identity-platform/msal-authentication-flows#implicit-grant)
* [Microsoft identity platform and implicit grant flow: Prefer the auth code flow](/en-us/entra/identity-platform/v2-oauth2-implicit-grant-flow#prefer-the-auth-code-flow)
* [Microsoft identity platform and OAuth 2.0 authorization code flow](/en-us/entra/identity-platform/v2-oauth2-auth-code-flow)

## Related content

* Microsoft identity platform documentation

  * [General documentation](/en-us/entra/identity-platform/)
  * [Access tokens](/en-us/entra/identity-platform/access-tokens)

* [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)

  * Using forwarded headers middleware to preserve HTTPS scheme information across proxy servers and internal networks.
  * Additional scenarios and use cases, including manual scheme configuration, request path changes for correct request routing, and forwarding the request scheme for Linux and non-IIS reverse proxies.

* [Prerendering with authentication](additional-scenarios?view=aspnetcore-10.0#prerendering-with-authentication)

* [WebAssembly: Security](https://webassembly.org/docs/security/)

* [WebAssembly Specification: Security Considerations](https://webassembly.github.io/spec/core/intro/introduction.html#security-considerations)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/security/authorization/policies
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/security/authorization/policies.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Policy-based authorization in ASP.NET Core Razor Pages

This article provides additional Razor Pages policy-based authorization scenarios following [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0), which should be read before this article when learning about policy-based authorization.

## Apply policies to Razor Pages

Apply policies to Razor Pages using the `[Authorize]` attribute with the policy name:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace AuthorizationPoliciesSample.Pages;

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Model : PageModel { }
```

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseModel : PageModel
{
}
```

Policies can't be applied at the page handler level, they must be applied to the [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class.

Policies can also be applied to pages using an [authorization convention](conventions?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/samesite/rp21
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/samesite/rp21.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core 2.1 Razor Pages SameSite cookie sample

This sample targets .NET Framework Targeted

ASP.NET Core 2.1 has built-in support for the [SameSite](https://owasp.org/www-community/SameSite) attribute, but it was written to the original standard. The [patched behavior](https://github.com/dotnet/aspnetcore/issues/8212) changed the meaning of `SameSite.None` to emit the sameSite attribute with a value of `None`, rather than not emit the value at all. If you want to not emit the value you can set the `SameSite` property on a cookie to -1.

ASP.NET Core [Identity](../authentication/identity?view=aspnetcore-2.2) is largely unaffected by [SameSite cookies](../samesite?view=aspnetcore-2.2) except for advanced scenarios like `IFrames` or `OpenIdConnect` integration.

When using `Identity`, do ***not*** add any cookie providers or call `services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)`, `Identity` takes care of that.

## Writing the SameSite attribute

The following code is an example of how to write a SameSite attribute on a cookie:

```
var cookieOptions = new CookieOptions
{
    // Set the secure flag, which Chrome's changes will require for SameSite none.
    // Note this will also require you to be running on HTTPS
    Secure = true,

    // Set the cookie to HTTP only which is good practice unless you really do need
    // to access it client side in scripts.
    HttpOnly = true,

    // Add the SameSite attribute, this will emit the attribute with a value of none.
    // To not emit the attribute at all set the SameSite property to (SameSiteMode)(-1).
    SameSite = SameSiteMode.None
};

// Add the cookie to the response cookie collection
Response.Cookies.Append(CookieName, "cookieValue", cookieOptions);
```

## Setting Cookie Authentication and Session State cookies

Cookie authentication, session state and [various other components](../samesite?view=aspnetcore-2.2) set their sameSite options via Cookie options, for example

```
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.SameSite = SameSiteMode.None;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.IsEssential = true;
    });

services.AddSession(options =>
{
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.IsEssential = true;
});
```

In the preceding code, both cookie authentication and session state set their sameSite attribute to `None`, emitting the attribute with a `None` value, and also set the Secure attribute to `true`.

### Run the sample

If you run the [sample project](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore21RazorPages), load your browser debugger on the initial page and use it to view the cookie collection for the site. To do so in Edge and Chrome press `F12` then select the `Application` tab and click the site URL under the `Cookies` option in the `Storage` section.

You can see from the image above that the cookie created by the sample when you click the "Create SameSite Cookie" button has a SameSite attribute value of `Lax`, matching the value set in the [sample code](#sampleCode).

## Intercepting cookies

In order to intercept cookies, to adjust the none value according to its support in the user's browser agent you must use the `CookiePolicy` middleware. This must be placed into the http request pipeline **before** any components that write cookies and configured within `ConfigureServices()`.

To insert it into the pipeline use `app.UseCookiePolicy()` in the `Configure(IApplicationBuilder, IHostingEnvironment)` method in your [Startup.cs](https://github.com/blowdart/AspNetSameSiteSamples/blob/master/AspNetCore21MVC/Startup.cs). For example

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
       app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();
    app.UseAuthentication();
    app.UseSession();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

Then in the `ConfigureServices(IServiceCollection services)` configure the cookie policy to call out to a helper class when cookies are appended or deleted, like so;

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
        options.OnAppendCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
        options.OnDeleteCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    });
}

private void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
    if (options.SameSite == SameSiteMode.None)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        if (SameSite.BrowserDetection.DisallowsSameSiteNone(userAgent))
        {
            options.SameSite = (SameSiteMode)(-1);
        }
    }
}
```

```
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.1.14" />
<PackageReference Include="Microsoft.AspNetCore.CookiePolicy" Version="2.1.14" />
```

### More Information

[Chrome Updates](https://www.chromium.org/updates/same-site) [ASP.NET Core SameSite Documentation](../samesite?view=aspnetcore-2.2) [ASP.NET Core 2.1 SameSite Change Announcement](https://github.com/dotnet/aspnetcore/issues/8212)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2022-08-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/start-mvc
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/start-mvc.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Get started with ASP.NET Core MVC

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET Core web development, consider the [Razor Pages](../razor-pages/razor-pages-start?view=aspnetcore-10.0) version of this tutorial, which provides an easier starting point. See [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0), which compares Razor Pages, MVC, and Blazor for UI development.

This is the first tutorial of a series that teaches ASP.NET Core MVC web development with controllers and views.

At the end of the series, you'll have an app that manages, validates, and displays movie data. You learn how to:

* Create a web app.
* Add and scaffold a model.
* Work with a database.
* Add search and validation.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-mvc-app/start-mvc/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Create a web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio and select **Create a new project**.

- In the **Create a new project** dialog, select **ASP.NET Core Web App (Model-View-Controller)** > **Next**.

- In the **Configure your new project** dialog:

  * Enter `MvcMovie` for **Project name**. It's important to name the project *MvcMovie*. Capitalization needs to match each `namespace` when code is copied.
  * The **Location** for the project can be set to anywhere.

- Select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 10.0 (Standard Term Support)**.
  * Verify that **Do not use top-level statements** is unchecked.

- Select **Create**.

For more information, including alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

Visual Studio uses the default project template for the created MVC project. The created project:

* Is a working app.
* Is a basic starter project.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs) and [Visual Studio Code help](#visual-studio-code-help).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project. The project can be located anywhere.

* Run the following commands:

  ```
  dotnet new mvc -o MvcMovie
  code -r MvcMovie
  ```

  The `dotnet new` command creates a new ASP.NET Core MVC project in the *MvcMovie* folder.

  The `code` command opens the *MvcMovie* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

- Press `Ctrl`+`F5` to run the app without the debugger.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio runs the app and opens the default browser.

The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. When Visual Studio creates a web project, a random port is used for the web server.

Launching the app without debugging by pressing `Ctrl`+`F5` allows you to:

* Make code changes.
* Save the file.
* Quickly refresh the browser and see the code changes.

You can launch the app in debug or non-debug mode from the **Debug** menu:

You can debug the app by selecting the **https** button in the toolbar:

The following image shows the app:

* Close the browser window. Visual Studio will stop the application.

- Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

- Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* In Visual Studio Code, press `Ctrl`+`F5` (Windows)/`^`+`F5` (macOS) to run the app without debugging.

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0)
  * Launches a browser.
  * Navigates to `https://localhost:<port#>`.

  The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. Localhost only serves web requests from the local computer.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.

* Save the file.

* Quickly refresh the browser and see the code changes.

* Close the browser window.

* In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)

## Visual Studio help

* [Learn to debug C# code using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)
* [Introduction to the Visual Studio IDE](/en-us/visualstudio/ide/visual-studio-ide)

## Visual Studio Code help

* [Getting started](https://code.visualstudio.com/docs)

* [Debugging](https://code.visualstudio.com/docs/editor/debugging)

* [Integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)

* [Keyboard shortcuts](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference)

  * [macOS keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf)
  * [Linux keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf)
  * [Windows keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf)

In the next tutorial in this series, you learn about MVC and start writing some code.

[Next: Add a controller](adding-controller?view=aspnetcore-10.0)

This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET Core web development, consider the [Razor Pages](../razor-pages/razor-pages-start?view=aspnetcore-10.0) version of this tutorial, which provides an easier starting point. See [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0), which compares Razor Pages, MVC, and Blazor for UI development.

This is the first tutorial of a series that teaches ASP.NET Core MVC web development with controllers and views.

At the end of the series, you'll have an app that manages, validates, and displays movie data. You learn how to:

* Create a web app.
* Add and scaffold a model.
* Work with a database.
* Add search and validation.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-mvc-app/start-mvc/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Create a web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio and select **Create a new project**.

- In the **Create a new project** dialog, select **ASP.NET Core Web App (Model-View-Controller)** > **Next**.

- In the **Configure your new project** dialog:

  * Enter `MvcMovie` for **Project name**. It's important to name the project *MvcMovie*. Capitalization needs to match each `namespace` when code is copied.
  * The **Location** for the project can be set to anywhere.

- Select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 9.0 (Standard Term Support)**.
  * Verify that **Do not use top-level statements** is unchecked.

- Select **Create**.

For more information, including alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

Visual Studio uses the default project template for the created MVC project. The created project:

* Is a working app.
* Is a basic starter project.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs) and [Visual Studio Code help](#visual-studio-code-help).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project. The project can be located anywhere.

* Run the following commands:

  ```
  dotnet new mvc -o MvcMovie
  code -r MvcMovie
  ```

  The `dotnet new` command creates a new ASP.NET Core MVC project in the *MvcMovie* folder.

  The `code` command opens the *MvcMovie* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

- Press `Ctrl`+`F5` to run the app without the debugger.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio runs the app and opens the default browser.

The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. When Visual Studio creates a web project, a random port is used for the web server.

Launching the app without debugging by pressing `Ctrl`+`F5` allows you to:

* Make code changes.
* Save the file.
* Quickly refresh the browser and see the code changes.

You can launch the app in debug or non-debug mode from the **Debug** menu:

You can debug the app by selecting the **https** button in the toolbar:

The following image shows the app:

* Close the browser window. Visual Studio will stop the application.

- Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

- Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* In Visual Studio Code, press `Ctrl`+`F5` (Windows)/`^`+`F5` (macOS) to run the app without debugging.

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0)
  * Launches a browser.
  * Navigates to `https://localhost:<port#>`.

  The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. Localhost only serves web requests from the local computer.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.

* Save the file.

* Quickly refresh the browser and see the code changes.

* Close the browser window.

* In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)

## Visual Studio help

* [Learn to debug C# code using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)
* [Introduction to the Visual Studio IDE](/en-us/visualstudio/ide/visual-studio-ide)

## Visual Studio Code help

* [Getting started](https://code.visualstudio.com/docs)

* [Debugging](https://code.visualstudio.com/docs/editor/debugging)

* [Integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)

* [Keyboard shortcuts](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference)

  * [macOS keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf)
  * [Linux keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf)
  * [Windows keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf)

In the next tutorial in this series, you learn about MVC and start writing some code.

[Next: Add a controller](adding-controller?view=aspnetcore-10.0)

This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET Core web development, consider the [Razor Pages](../razor-pages/razor-pages-start?view=aspnetcore-10.0) version of this tutorial, which provides an easier starting point. See [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0), which compares Razor Pages, MVC, and Blazor for UI development.

This is the first tutorial of a series that teaches ASP.NET Core MVC web development with controllers and views.

At the end of the series, you'll have an app that manages and displays movie data. You learn how to:

* Create a web app.
* Add and scaffold a model.
* Work with a database.
* Add search and validation.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-mvc-app/start-mvc/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Create a web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio and select **Create a new project**.

- In the **Create a new project** dialog, select **ASP.NET Core Web App (Model-View-Controller)** > **Next**.

- In the **Configure your new project** dialog:

  * Enter `MvcMovie` for **Project name**. It's important to name the project *MvcMovie*. Capitalization needs to match each `namespace` when code is copied.
  * The **Location** for the project can be set to anywhere.

- Select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 8.0 (Long Term Support)**.
  * Verify that **Do not use top-level statements** is unchecked.

- Select **Create**.

For more information, including alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

Visual Studio uses the default project template for the created MVC project. The created project:

* Is a working app.
* Is a basic starter project.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs) and [Visual Studio Code help](#visual-studio-code-help).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project. The project can be located anywhere.

* Run the following commands:

  ```
  dotnet new mvc -o MvcMovie
  code -r MvcMovie
  ```

  The `dotnet new` command creates a new ASP.NET Core MVC project in the *MvcMovie* folder.

  The `code` command opens the *MvcMovie* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

- Press `Ctrl`+`F5` to run the app without the debugger.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio runs the app and opens the default browser.

The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. When Visual Studio creates a web project, a random port is used for the web server.

Launching the app without debugging by pressing `Ctrl`+`F5` allows you to:

* Make code changes.
* Save the file.
* Quickly refresh the browser and see the code changes.

You can launch the app in debug or non-debug mode from the **Debug** menu:

You can debug the app by selecting the **https** button in the toolbar:

The following image shows the app:

* Close the browser window. Visual Studio will stop the application.

- Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

- Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* In Visual Studio Code, press `Ctrl`+`F5` (Windows)/`^`+`F5` (macOS) to run the app without debugging.

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0)
  * Launches a browser.
  * Navigates to `https://localhost:<port#>`.

  The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. Localhost only serves web requests from the local computer.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.

* Save the file.

* Quickly refresh the browser and see the code changes.

* Close the browser window.

* In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

## Visual Studio help

* [Learn to debug C# code using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)
* [Introduction to the Visual Studio IDE](/en-us/visualstudio/ide/visual-studio-ide)

## Visual Studio Code help

* [Getting started](https://code.visualstudio.com/docs)

* [Debugging](https://code.visualstudio.com/docs/editor/debugging)

* [Integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)

* [Keyboard shortcuts](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference)

  * [macOS keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf)
  * [Linux keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf)
  * [Windows keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf)

## Visual Studio for Mac help

* [Visual Studio for Mac Tour](/en-us/visualstudio/mac/ide-tour)
* [Introducing Visual Studio for Mac](/en-us/visualstudio/mac/)

In the next tutorial in this series, you learn about MVC and start writing some code.

[Next: Add a controller](adding-controller?view=aspnetcore-10.0)

This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET Core web development, consider the [Razor Pages](../razor-pages/razor-pages-start?view=aspnetcore-10.0) version of this tutorial, which provides an easier starting point. See [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0), which compares Razor Pages, MVC, and Blazor for UI development.

This is the first tutorial of a series that teaches ASP.NET Core MVC web development with controllers and views.

At the end of the series, you'll have an app that manages and displays movie data. You learn how to:

* Create a web app.
* Add and scaffold a model.
* Work with a database.
* Add search and validation.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-mvc-app/start-mvc/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Create a web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- Start Visual Studio and select **Create a new project**.

- In the **Create a new project** dialog, select **ASP.NET Core Web App (Model-View-Controller)** > **Next**.

- In the **Configure your new project** dialog:

  * Enter `MvcMovie` for **Project name**. It's important to name the project *MvcMovie*. Capitalization needs to match each `namespace` when code is copied.
  * The **Location** for the project can be set to anywhere.

- Select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 7.0**.
  * Verify that **Do not use top-level statements** is unchecked.

- Select **Create**.

For more information, including alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

Visual Studio uses the default project template for the created MVC project. The created project:

* Is a working app.
* Is a basic starter project.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project. The project can be located anywhere.

* Run the following commands:

  ```
  dotnet new mvc -o MvcMovie
  code -r MvcMovie
  ```

  The `dotnet new` command creates a new ASP.NET Core MVC project in the *MvcMovie* folder.

  The `code` command opens the *MvcMovie* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

* Start Visual Studio for Mac and select **File** > **New Project...**.

* In Visual Studio for Mac select **Web and Console** > **App** > **Web Application (Model-View-Controller)** > **Continue**.

* In the **Configure your new Web Application (Model-View-Controller)** dialog:

  * Select **.NET 7.0** for the **Target Framework**.
  * Verify that **Do not use top-level statements** is unchecked.

* Select **Continue**.

* Enter `MvcMovie` for **Project name**. It's important to name the project *MvcMovie*. Capitalization needs to match each `namespace` when code is copied.

* The **Location** for the project can be set to anywhere.

* Select **Create**.

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

- Press `Ctrl`+`F5` to run the app without the debugger.Visual Studio displays the following dialog when a project is not yet configured to use SSL:Select **Yes** if you trust the IIS Express SSL certificate.The following dialog is displayed:Select **Yes** if you agree to trust the development certificate.For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio runs the app and opens the default browser.

The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. When Visual Studio creates a web project, a random port is used for the web server.

Launching the app without debugging by pressing `Ctrl`+`F5` allows you to:

* Make code changes.
* Save the file.
* Quickly refresh the browser and see the code changes.

You can launch the app in debug or non-debug mode from the **Debug** menu:

You can debug the app by selecting the **https** button in the toolbar:

The following image shows the app:

* Close the browser window. Visual Studio will stop the application.

- Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

- Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* In Visual Studio Code, press `Ctrl`+`F5` (Windows)/`^`+`F5` (macOS) to run the app without debugging.

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0)
  * Launches a browser.
  * Navigates to `https://localhost:<port#>`.

  The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. Localhost only serves web requests from the local computer.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.

* Save the file.

* Quickly refresh the browser and see the code changes.

* Close the browser window.

* In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

- Select Option+Command+Return to run the app without the debugger.

  Visual Studio for Mac:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.
  * Launches a browser.
  * Navigates to `http://localhost:port`, where *port* is a randomly chosen port number, set when Visual Studio creates a web project.

  Visual Studio for Mac displays the following popup:

  * Select **Install and Trust** if you trust the development certificate.

  The following dialog is displayed:

  * Enter your password and select **Update Settings**.

  See [Trust the ASP.NET Core HTTPS development certificate](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos) for more information.

  The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. Localhost only serves web requests from the local computer.

You can launch the app in debug or non-debug mode from the **Debug** menu.

The following image shows the app:

* Close the browser window. Visual Studio for Mac will stop the application.

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

## Visual Studio help

* [Learn to debug C# code using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)
* [Introduction to the Visual Studio IDE](/en-us/visualstudio/ide/visual-studio-ide)

## Visual Studio Code help

* [Getting started](https://code.visualstudio.com/docs)

* [Debugging](https://code.visualstudio.com/docs/editor/debugging)

* [Integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)

* [Keyboard shortcuts](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference)

  * [macOS keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf)
  * [Linux keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf)
  * [Windows keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf)

## Visual Studio for Mac help

* [Visual Studio for Mac Tour](/en-us/visualstudio/mac/ide-tour)
* [Introducing Visual Studio for Mac](/en-us/visualstudio/mac/)

In the next tutorial in this series, you learn about MVC and start writing some code.

[Next: Add a controller](adding-controller?view=aspnetcore-10.0)

This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET Core web development, consider the [Razor Pages](../razor-pages/razor-pages-start?view=aspnetcore-10.0) version of this tutorial, which provides an easier starting point. See [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0), which compares Razor Pages, MVC, and Blazor for UI development.

This is the first tutorial of a series that teaches ASP.NET Core MVC web development with controllers and views.

At the end of the series, you'll have an app that manages and displays movie data. You learn how to:

* Create a web app.
* Add and scaffold a model.
* Work with a database.
* Add search and validation.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-mvc-app/start-mvc/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Prerequisites

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

- [Visual Studio 2022](https://visualstudio.microsoft.com/vs/#download) with the **ASP.NET and web development** workload.
- [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use something other than Visual Studio Code.

For Visual Studio for Mac, see the .NET 7 version of this tutorial.

>

## Create a web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- Start Visual Studio and select **Create a new project**.
- In the **Create a new project** dialog, select **ASP.NET Core Web App (Model-View-Controller)** > **Next**.
- In the **Configure your new project** dialog, enter `MvcMovie` for **Project name**. It's important to name the project *MvcMovie*. Capitalization needs to match each `namespace` when code is copied.
- Select **Next**.
- In the **Additional information** dialog, select **.NET 6.0 (Long-term support)**.
- Select **Create**.

For alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

Visual Studio uses the default project template for the created MVC project. The created project:

* Is a working app.
* Is a basic starter project.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs) and [Visual Studio Code help](#visual-studio-code-help).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new mvc -o MvcMovie
  code -r MvcMovie
  ```

  The `dotnet new` command creates a new ASP.NET Core MVC project in the *MvcMovie* folder.

  The `code` command opens the *MvcMovie* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

For Visual Studio for Mac, see the .NET 7 version of this tutorial.

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

- Select Ctrl+F5 to run the app without the debugger.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio runs the app and opens the default browser.

The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. When Visual Studio creates a web project, a random port is used for the web server.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.
* Save the file.
* Quickly refresh the browser and see the code changes.

You can launch the app in debug or non-debug mode from the **Debug** menu:

You can debug the app by selecting the **MvcMovie** button in the toolbar:

The following image shows the app:

* Select Ctrl+F5 to run without the debugger.

  * Trust the HTTPS development certificate by running the following command:

    ```
    dotnet dev-certs https --trust
    ```

    The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

    The preceding command displays the following dialog, provided the certificate was not previously trusted:

  * Select **Yes** if you agree to trust the development certificate.

    For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0)
  * Launches a browser.
  * Navigates to `https://localhost:<port#>`.

  The address bar shows `localhost:<port#>` and not something like `example.com`. The standard hostname for your local computer is `localhost`. Localhost only serves web requests from the local computer.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.

* Save the file.

* Quickly refresh the browser and see the code changes.

For Visual Studio for Mac, see the .NET 7 version of this tutorial.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

## Visual Studio help

* [Learn to debug C# code using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)
* [Introduction to the Visual Studio IDE](/en-us/visualstudio/ide/visual-studio-ide)

## Visual Studio Code help

* [Getting started](https://code.visualstudio.com/docs)

* [Debugging](https://code.visualstudio.com/docs/editor/debugging)

* [Integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)

* [Keyboard shortcuts](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference)

  * [macOS keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf)
  * [Linux keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf)
  * [Windows keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf)

## Visual Studio for Mac help

* [Visual Studio for Mac Tour](/en-us/visualstudio/mac/ide-tour)
* [Introducing Visual Studio for Mac](/en-us/visualstudio/mac/)

In the next tutorial in this series, you learn about MVC and start writing some code.

[Next: Add a controller](adding-controller?view=aspnetcore-10.0)

This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET Core web development, consider the [Razor Pages](../razor-pages/razor-pages-start?view=aspnetcore-10.0) version of this tutorial, which provides an easier starting point. See [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0), which compares Razor Pages, MVC, and Blazor for UI development.

This is the first tutorial of a series that teaches ASP.NET Core MVC web development with controllers and views.

At the end of the series, you'll have an app that manages and displays movie data. You learn how to:

* Create a web app.
* Add and scaffold a model.
* Work with a database.
* Add search and validation.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-mvc-app/start-mvc/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Create a web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- Start Visual Studio and select **Create a new project**.

- In the **Create a new project** dialog, select **ASP.NET Core Web Application** > **Next**.

- In the **Configure your new project** dialog, enter `MvcMovie` for **Project name**. It's important to name the project *MvcMovie*. Capitalization needs to match each `namespace` matches when code is copied.

- Select **Create**.

- In the **Create a new ASP.NET Core web application** dialog, select:

  * **.NET Core** and **ASP.NET Core 5.0** in the dropdowns.
  * **ASP.NET Core Web App (Model-View-Controller)**.
  * **Create**.

For alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

Visual Studio used the default project template for the created MVC project. The created project:

* Is a working app.
* Is a basic starter project.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new mvc -o MvcMovie
  code -r MvcMovie
  ```

  The `dotnet new` command creates a new ASP.NET Core MVC project in the *MvcMovie* folder.

  The `code` command opens the *MvcMovie* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

* Select **File** > **New Solution**.

* In Visual Studio for Mac earlier than version 8.6, select **.NET Core** > **App** > **Web Application (Model-View-Controller)** > **Next**. In version 8.6 or later, select **Web and Console** > **App** > **Web Application (Model-View-Controller)** > **Next**.

* In the **Configure your new Web Application** dialog:

  * Confirm that **Authentication** is set to **No Authentication**.
  * If an option to select a **Target Framework** is presented, select the latest 5.x version.
  * Select **Next**.

* Name the project **MvcMovie**, and then select **Create**.

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

- Select Ctrl+F5 to run the app without the debugger.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio:

  * Starts [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview).
  * Runs the app.

  The address bar shows `localhost:port#` and not something like `example.com`. The standard hostname for your local computer is `localhost`. When Visual Studio creates a web project, a random port is used for the web server.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.
* Save the file.
* Quickly refresh the browser and see the code changes.

You can launch the app in debug or non-debug mode from the **Debug** menu item:

You can debug the app by selecting the **IIS Express** button

The following image shows the app:

* Select Ctrl+F5 to run without the debugger.

  * Trust the HTTPS development certificate by running the following command:

    ```
    dotnet dev-certs https --trust
    ```

    The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

    The preceding command displays the following dialog, provided the certificate was not previously trusted:

  * Select **Yes** if you agree to trust the development certificate.

    For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0)
  * Launches a browser.
  * Navigates to `https://localhost:5001`.

  The address bar shows `localhost:port:5001` and not something like `example.com`. The standard hostname for your local computer is `localhost`. Localhost only serves web requests from the local computer.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.

* Save the file.

* Quickly refresh the browser and see the code changes.

- Select **Run** > **Start Without Debugging** to launch the app.

  Visual Studio for Mac:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.
  * Launches a browser.
  * Navigates to `http://localhost:port`, where *port* is a randomly chosen port number.

  Visual Studio for Mac displays the following popup:

  Select **Yes** if you trust the development certificate.

  The following dialog is displayed:

  Enter your password and select **OK**

  Select **Yes** if you agree to trust the development certificate.

  See [Trust the ASP.NET Core HTTPS development certificate](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos) for more information.

  The address bar shows `localhost:port#` and not something like `example.com`. The standard hostname for your local computer is `localhost`. When Visual Studio creates a web project, a random port is used for the web server.

You can launch the app in debug or non-debug mode from the **Run** menu.

The following image shows the app:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

## Visual Studio help

* [Learn to debug C# code using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)
* [Introduction to the Visual Studio IDE](/en-us/visualstudio/ide/visual-studio-ide)

## Visual Studio Code help

* [Getting started](https://code.visualstudio.com/docs)

* [Debugging](https://code.visualstudio.com/docs/editor/debugging)

* [Integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)

* [Keyboard shortcuts](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference)

  * [macOS keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf)
  * [Linux keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf)
  * [Windows keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf)

## Visual Studio for Mac help

* [Visual Studio for Mac Tour](/en-us/visualstudio/mac/ide-tour)
* [Introducing Visual Studio for Mac](/en-us/visualstudio/mac/)

In the next part of this tutorial, you learn about MVC and start writing some code.

[Next](adding-controller?view=aspnetcore-10.0)

This tutorial teaches ASP.NET Core MVC web development with controllers and views. If you're new to ASP.NET Core web development, consider the [Razor Pages](../razor-pages/razor-pages-start?view=aspnetcore-10.0) version of this tutorial, which provides an easier starting point. See [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0), which compares Razor Pages, MVC, and Blazor for UI development.

This is the first tutorial of a series that teaches ASP.NET Core MVC web development with controllers and views.

At the end of the series, you'll have an app that manages and displays movie data. You learn how to:

* Create a web app.
* Add and scaffold a model.
* Work with a database.
* Add search and validation.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-mvc-app/start-mvc/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Create a web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- From the Visual Studio, select **Create a new project**.

- Select **ASP.NET Core Web Application** > **Next**.

- Name the project **MvcMovie** and select **Create**. It's important to name the project **MvcMovie** so when you copy code, the namespace will match.

- Select **Web Application(Model-View-Controller)**. From the dropdown boxes, select **.NET Core** and **ASP.NET Core 3.1**, then select **Create**.

Visual Studio used the default project template for the created MVC project. The created project:

* Is a working app.
* Is a basic starter project.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs) and [Visual Studio Code help](#visual-studio-code-help).

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to a folder that will contain the project.

* Run the following command:

  ```
  dotnet new mvc -o MvcMovie
  code -r MvcMovie
  ```

  * A dialog box appears with **Required assets to build and debug are missing from 'MvcMovie'. Add them?**, select **Yes**.

  * `dotnet new mvc -o MvcMovie`: Creates a new ASP.NET Core MVC project in the *MvcMovie* folder.

  * `code -r MvcMovie`: Loads the `MvcMovie.csproj` project file in Visual Studio Code.

- Select **File** > **New Solution**.

- In Visual Studio for Mac earlier than version 8.6, select **.NET Core** > **App** > **Web Application (Model-View-Controller)** > **Next**. In version 8.6 or later, select **Web and Console** > **App** > **Web Application (Model-View-Controller)** > **Next**.

- In the **Configure your new Web Application** dialog:

  * Confirm that **Authentication** is set to **No Authentication**.
  * If an option to select a **Target Framework** is presented, select the latest 3.x version.
  * Select **Next**.

- Name the project **MvcMovie**, and then select **Create**.

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

- Select Ctrl+F5 to run the app without debugging.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio:

  * Starts [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview).
  * Runs the app.

  The address bar shows `localhost:port#` and not something like `example.com`. The standard hostname for your local computer is `localhost`. When Visual Studio creates a web project, a random port is used for the web server.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.
* Save the file.
* Quickly refresh the browser and see the code changes.

You can launch the app in debug or non-debug mode from the **Debug** menu item:

You can debug the app by selecting the **IIS Express** button

The following image shows the app:

* Select Ctrl+F5 to run the app without debugging.

  * Trust the HTTPS development certificate by running the following command:

    ```
    dotnet dev-certs https --trust
    ```

    The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

    The preceding command displays the following dialog, provided the certificate was not previously trusted:

  * Select **Yes** if you agree to trust the development certificate.

    For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0)
  * Launches a browser.
  * Navigates to `https://localhost:5001`.

  The address bar shows `localhost:port:5001` and not something like `example.com`. The standard hostname for your local computer is `localhost`. Localhost only serves web requests from the local computer.

Launching the app without debugging by selecting Ctrl+F5 allows you to:

* Make code changes.

* Save the file.

* Quickly refresh the browser and see the code changes.

- Select **Run** > **Start Without Debugging** to launch the app.

  Visual Studio for Mac: starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server, launches a browser, and navigates to `http://localhost:port`, where *port* is a randomly chosen port number.

Visual Studio for Mac displays the following popup:

Select **Yes** if you trust the development certificate.

The following dialog is displayed:

Enter your password and select **OK**

Select **Yes** if you agree to trust the development certificate.

See [Trust the ASP.NET Core HTTPS development certificate](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos) for more information.

The address bar shows `localhost:port#` and not something like `example.com`. The standard hostname for your local computer is `localhost`. When Visual Studio creates a web project, a random port is used for the web server. When you run the app, you'll see a different port number.

You can launch the app in debug or non-debug mode from the **Run** menu.

The following image shows the app:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

## Visual Studio help

* [Learn to debug C# code using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)
* [Introduction to the Visual Studio IDE](/en-us/visualstudio/ide/visual-studio-ide)

## Visual Studio Code help

* [Getting started](https://code.visualstudio.com/docs)

* [Debugging](https://code.visualstudio.com/docs/editor/debugging)

* [Integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)

* [Keyboard shortcuts](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-reference)

  * [macOS keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf)
  * [Linux keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf)
  * [Windows keyboard shortcuts](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf)

## Visual Studio for Mac help

* [Visual Studio for Mac Tour](/en-us/visualstudio/mac/ide-tour)
* [Introducing Visual Studio for Mac](/en-us/visualstudio/mac/)

In the next part of this tutorial, you learn about MVC and start writing some code.

[Next](adding-controller?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app/part-5
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/movie-database-app/part-5.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor movie database app (Part 5 - Add validation)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article is the fifth part of the Blazor movie database app tutorial that teaches you the basics of building an ASP.NET Core Blazor Web App with features to manage a movie database.

This part of the tutorial series explains how metadata of the `Movie` model is used to validate user input in the forms that create and edit movies.

## Validation using data annotations

Validation rules are specified on a model class using *data annotations*. The following list shows some of the [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) attributes for user input validation of `public` properties in a form's model:

* [`[Required]`](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute): Require that the user provide a value.
* [`[StringLength]`](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute): Specifies the minimum and maximum length of characters. Note that a `MinimumLength` passed to the attribute doesn't make the string required (apply the [`[Required]` attribute](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute)).
* [`[RegularExpression]`](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute): Specify a pattern to match for the user's input.
* [`[Range]`](/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute): Specify the minimum and maximum values.

Value types, such as `decimal`, `int`, `float`, `DateOnly`, `TimeOnly`, and `DateTime`, are inherently required. Placing a [`[Required]` attribute](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute) on value types isn't necessary.

Additional data annotations that you can use in your forms are covered by the Blazor reference documentation.

## Add validation to the `Movie` model

Add the following data annotations to the `Movie` class properties. To update all of the properties at once, you can copy and paste the entire `Models/Movie.cs` file, which appears after the following code example.

```
+ [Required]
+ [StringLength(60, MinimumLength = 3)]
  public string? Title { get; set; }

+ [Required]
+ [StringLength(30)]
+ [RegularExpression(@"^[A-Z]+[a-zA-Z()\s-]*$")]
  public string? Genre { get; set; }

+ [Range(0, 100)]
  [DataType(DataType.Currency)]
  [Column(TypeName = "decimal(18, 2)")]
  public decimal Price { get; set; }
```

`Models/Movie.cs` file after the preceding data annotations are applied to the properties:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorWebAppMovies.Models;

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(60, MinimumLength = 3)]
    public string? Title { get; set; }

    public DateOnly ReleaseDate { get; set; }

    [Required]
    [StringLength(30)]
    [RegularExpression(@"^[A-Z]+[a-zA-Z()\s-]*$")]
    public string? Genre { get; set; }

    [Range(0, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

The preceding validation rules are merely for demonstration and aren't optimal for a production system. For example, the preceding validation prevents entering a movie with only one or two characters and doesn't allow additional special characters for a movie's genre.

## Create an EF Core migration and update the database

A data model schema defines how data is organized and connected within a relational database.

Adding the data annotations to the `Movie` class in the preceding section doesn't automatically result in matching changes to the database's schema.

Review the annotations applied to the `Title` property:

```
[Required]
[StringLength(60, MinimumLength = 3)]
public string? Title { get; set; }
```

The difference between the model property and the database's schema are summarized in the following table. Neither constraint matches after the data annotation is applied to the `Movie` model.

| Constraint     | Model `Title` property                                                                          | Database `Title` column                                                                                  |
| -------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Maximum length | 60 characters                                                                                   | Byte pairs up to \~2 GB† [`NVARCHAR (MAX)`](/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql) |
| Required       | ✔️Yes [`[Required]`](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute) | ❌No `NULL` is permitted in the column.                                                                   |

†Database character columns are defined by *size* (byte pairs). One byte-pair per character is used for characters defined in the Unicode range 0 to 65,535. However, individual characters outside of that Unicode range take multiple byte-pairs to store, so the actual number of characters that a column can store is arbitrary. The important concept for our purposes in comparing the `Title` property of the `Movie` model and the database schema for the `Title` column is that \~2 GB of stored byte-pairs in the database far exceeds the 60 character limit set for the property. The database schema *should be adjusted downward* to match the app's constraint.

To match the `Movie` model's `Title` property length in the app, the database should indicate `NVARCHAR (60)` for the size of the `Title` column. The schema difference doesn't cause EF Core to throw an exception when the app is used because a user posting a 60 character movie title fits within the database's \~2 GB byte-pair limit for a movie title. However, consider the reverse situation where a model property is given a constraint larger than what the database permits and the user posts a string too long for a database character column: An exception is thrown by the database or data is truncated when the user posts the value. You should always keep the app's models aligned with the database's schema because a misaligned schema can cause exceptions and the storage of incorrect data.

Although the `Title` property is a [nullable reference type (NRT)](/en-us/dotnet/csharp/nullable-references#nullable-variable-annotations), as indicated by the `?` on the `string` type (`string?`), the database shouldn't store a `NULL` value in its `Title` column due to the model's `Required` constraint. When the database's schema is updated in the next step, the database's `Title` column should reflect `NOT NULL` for the `Title` column to match the property. The important concept is that just because a model property is an NRT and can hold a `null` value in code doesn't mean that the database column's schema should be nullable (`NULL` permitted). These are independent conditions used for different purposes: NRTs are used to prevent coding errors with nullable types, while the database schema reflects the exact type and size of stored data.

To align the model and the database schema, create and apply an EF Core *database migration* with a migration name that identifies the migration changes. The migration name is similar to a commit message in a version control system. In the following command examples, the migration name "`NewMovieDataAnnotations`" reflects that new data annotations are added to the `Movie` model.

Important

Make sure that the app isn't running for the next steps.

Stopping the app when using Visual Studio only requires you to close the browser's window.

When using VS Code, close the browser's window and stop the app in VS Code with **Run** > **Stop Debugging** or by pressing `Shift`+`F5` on the keyboard.

When using the .NET CLI, close the browser's window and stop the app in the command shell with `Ctrl`+`C`.

In Visual Studio **Solution Explorer**, double-click **Connected Services**. In the **Service Dependencies** area, select the ellipsis (`...`) followed by **Add migration** in the **SQL Server Express LocalDB** area.

Give the migration a **Migration name** of `NewMovieDataAnnotations` to describe the migration. Wait for the database context to load in the **DbContext class names** field. Select **Finish** to create the migration. Select the **Close** button when the operation completes.

Select the ellipsis (`...`) again followed by the **Update database** command.

The **Update database with the latest migration** dialog opens. Wait for the **DbContext class names** field to update and for prior migrations to load. Select the **Finish** button. Select the **Close** button when the operation completes.

Use the following command in the **Terminal** (**Terminal** menu > **New Terminal**) to add a migration for the new data annotations:

```
dotnet ef migrations add NewMovieDataAnnotations
```

To apply the migration to the database, execute the following command:

```
dotnet ef database update
```

To add a migration for the new data annotations, execute the following command in a command shell opened to the project's root folder:

```
dotnet ef migrations add NewMovieDataAnnotations
```

To apply the migration to the database, execute the following command:

```
dotnet ef database update
```

After applying the migration, the model property and the database's schema match, as summarized in the following table.

| Constraint     | Model `Title` property                                                                          | Database `Title` column                                                                                              |
| -------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Maximum length | 60 characters                                                                                   | Sixty (60) byte pairs are permitted†. [`NVARCHAR (60)`](/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql) |
| Required       | ✔️Yes [`[Required]`](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute) | ✔️Yes `NOT NULL` is indicated for the column.                                                                        |

†Sixty (60) byte pairs is 60 characters if one byte-pair per character is used to store the movie title, which is true when using characters defined in the Unicode range 0 to 65,535.

## Troubleshoot with the completed sample

If you run into a problem while following the tutorial that you can't resolve from the text, compare your code to the completed project in the Blazor samples repository:

[Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)

Select the latest version folder. The sample folder for this tutorial's project is named `BlazorWebAppMovies`.

## Related content

* [Tag Helpers in forms in ASP.NET Core](../../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization in ASP.NET Core](../../../fundamentals/localization?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers in ASP.NET Core](../../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)
* [Migrations overview (EF Core documentation)](/en-us/ef/core/managing-schemas/migrations/)
* [nchar and nvarchar (Transact-SQL) (SQL Server documentation)](/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql#remarks)
* [Blazor enhanced forms](../../forms/?view=aspnetcore-10.0#enhanced-form-handling)

## Next steps

[Previous: Work with a database](part-4?view=aspnetcore-10.0) [Next: Add search](part-6?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/development-time-iis-support
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/development-time-iis-support.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Development-time IIS support in Visual Studio for ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Sourabh Shirhatti](https://twitter.com/sshirhatti)

This article describes [Visual Studio](https://visualstudio.microsoft.com) support for debugging ASP.NET Core apps running with IIS on Windows Server. This topic walks through enabling this scenario and setting up a project.

## Prerequisites

* [Visual Studio for Windows](https://visualstudio.microsoft.com/downloads/)
* **ASP.NET and web development** workload
* **.NET Core cross-platform development** workload
* X.509 security certificate (for HTTPS support)

## Enable IIS

1. In Windows, navigate to **Control Panel** > **Programs** > **Programs and Features** > **Turn Windows features on or off** (left side of the screen).
2. Select the **Internet Information Services** checkbox. Select **OK**.

The IIS installation may require a system restart.

## Configure IIS

IIS must have a website configured with the following:

* **Host name**: Typically, the **Default Web Site** is used with a **Host name** of `localhost`. However, any valid IIS website with a unique host name works.

* **Site Binding**

  * For apps that require HTTPS, create a binding to port 443 with a certificate. Typically, the **IIS Express Development Certificate** is used, but any valid certificate works.
  * For apps that use HTTP, confirm the existence of a binding to port 80 or create a binding to port 80 for a new site.
  * Use a single binding for either HTTP or HTTPS. **Binding to both HTTP and HTTPS ports simultaneously isn't supported.**

## Enable development-time IIS support in Visual Studio

1. Launch the Visual Studio installer.

2. Select **Modify** for the Visual Studio installation that you plan to use for IIS development-time support.

3. For the **ASP.NET and web development** workload, locate and install the **Development time IIS support** component.

   The component is listed in the **Optional** section under **Development time IIS support** in the **Installation details** panel to the right of the workloads. The component installs the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0), which is a native IIS module required to run ASP.NET Core apps with IIS.

## Configure the project

### HTTPS redirection

For a new project that requires HTTPS, select the checkbox to **Configure for HTTPS** in the **Create a new ASP.NET Core Web Application** window. Selecting the checkbox adds [HTTPS Redirection and HSTS middleware](../../security/enforcing-ssl?view=aspnetcore-10.0) to the app when it's created.

For an existing project that requires HTTPS, use HTTPS Redirection and HSTS middleware in `Startup.Configure`. For more information, see [Enforce HTTPS in ASP.NET Core](../../security/enforcing-ssl?view=aspnetcore-10.0).

For a project that uses HTTP, [HTTPS Redirection and HSTS middleware](../../security/enforcing-ssl?view=aspnetcore-10.0) aren't added to the app. No app configuration is required.

### IIS launch profile

Create a new launch profile to add development-time IIS support:

1. Right-click the project in **Solution Explorer**. Select **Properties**. Open the **Debug** tab.

2. For **Profile**, select the **New** button. Name the profile "IIS" in the popup window. Select **OK** to create the profile.

3. For the **Launch** setting, select **IIS** from the list.

4. Select the checkbox for **Launch browser** and provide the endpoint URL.

   When the app requires HTTPS, use an HTTPS endpoint (`https://`). For HTTP, use an HTTP (`http://`) endpoint.

   Provide the same host name and port as the [IIS configuration specified earlier uses](#configure-iis), typically `localhost`.

   Provide the name of the app at the end of the URL.

   For example, `https://localhost/WebApplication1` (HTTPS) or `http://localhost/WebApplication1` (HTTP) are valid endpoint URLs.

5. In the **Environment variables** section, select the **Add** button. Provide an environment variable with a **Name** of `ASPNETCORE_ENVIRONMENT` and a **Value** of `Development`.

6. In the **Web Server Settings** area, set the **App URL** to the same value used for the **Launch browser** endpoint URL.

7. For the **Hosting Model** setting in Visual Studio 2019 or later, select **Default** to use the hosting model used by the project. If the project sets the `<AspNetCoreHostingModel>` property in its project file, the value of the property (`InProcess` or `OutOfProcess`) is used. If the property isn't present, the default hosting model of the app is used, which is in-process. If the app requires an explicit hosting model setting different from the app's normal hosting model, set the **Hosting Model** to either `In Process` or `Out Of Process` as needed.

8. Save the profile.

When not using Visual Studio, manually add a launch profile to the [launchSettings.json](https://json.schemastore.org/launchsettings) file in the *Properties* folder. The following example configures the profile to use the HTTPS protocol:

```
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iis": {
      "applicationUrl": "https://localhost/WebApplication1",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS": {
      "commandName": "IIS",
      "launchBrowser": true,
      "launchUrl": "https://localhost/WebApplication1",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
```

Confirm that the `applicationUrl` and `launchUrl` endpoints match and use the same protocol as the IIS binding configuration, either HTTP or HTTPS.

## Run the project

Run Visual Studio as an administrator:

* Confirm that the build configuration drop-down list is set to **Debug**.
* Set the [Start Debugging button](/en-us/visualstudio/debugger/debugger-feature-tour) to the **IIS** profile and select the button to start the app.

Visual Studio may prompt a restart if not running as an administrator. If prompted, restart Visual Studio.

If an untrusted development certificate is used, the browser may require you to create an exception for the untrusted certificate.

Note

Debugging a Release build configuration with [Just My Code](/en-us/visualstudio/debugger/just-my-code) and compiler optimizations results in a degraded experience. For example, break points aren't hit.

## Additional resources

* [Getting Started with the IIS Manager in IIS](/en-us/iis/get-started/getting-started-with-iis/getting-started-with-the-iis-manager-in-iis-7-and-iis-8)
* [Enforce HTTPS in ASP.NET Core](../../security/enforcing-ssl?view=aspnetcore-10.0)

This article describes [Visual Studio](https://visualstudio.microsoft.com) support for debugging ASP.NET Core apps running with IIS on Windows Server. This topic walks through enabling this scenario and setting up a project.

## Prerequisites

* [Visual Studio for Windows](https://visualstudio.microsoft.com/downloads/)
* **ASP.NET and web development** workload
* **.NET Core cross-platform development** workload
* X.509 security certificate (for HTTPS support)

## Enable IIS

1. In Windows, navigate to **Control Panel** > **Programs** > **Programs and Features** > **Turn Windows features on or off** (left side of the screen).
2. Select the **Internet Information Services** checkbox. Select **OK**.

The IIS installation may require a system restart.

## Configure IIS

IIS must have a website configured with the following:

* **Host name**: Typically, the **Default Web Site** is used with a **Host name** of `localhost`. However, any valid IIS website with a unique host name works.

* **Site Binding**

  * For apps that require HTTPS, create a binding to port 443 with a certificate. Typically, the **IIS Express Development Certificate** is used, but any valid certificate works.
  * For apps that use HTTP, confirm the existence of a binding to post 80 or create a binding to port 80 for a new site.
  * Use a single binding for either HTTP or HTTPS. **Binding to both HTTP and HTTPS ports simultaneously isn't supported.**

## Enable development-time IIS support in Visual Studio

1. Launch the Visual Studio installer.

2. Select **Modify** for the Visual Studio installation that you plan to use for IIS development-time support.

3. For the **ASP.NET and web development** workload, locate and install the **Development time IIS support** component.

   The component is listed in the **Optional** section under **Development time IIS support** in the **Installation details** panel to the right of the workloads. The component installs the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0), which is a native IIS module required to run ASP.NET Core apps with IIS.

## Configure the project

### HTTPS redirection

For a new project that requires HTTPS, select the checkbox to **Configure for HTTPS** in the **Create a new ASP.NET Core Web Application** window. Selecting the checkbox adds [HTTPS Redirection and HSTS middleware](../../security/enforcing-ssl?view=aspnetcore-10.0) to the app when it's created.

For an existing project that requires HTTPS, use HTTPS Redirection and HSTS middleware in `Startup.Configure`. For more information, see [Enforce HTTPS in ASP.NET Core](../../security/enforcing-ssl?view=aspnetcore-10.0).

For a project that uses HTTP, [HTTPS Redirection and HSTS middleware](../../security/enforcing-ssl?view=aspnetcore-10.0) aren't added to the app. No app configuration is required.

### IIS launch profile

Create a new launch profile to add development-time IIS support:

1. Right-click the project in **Solution Explorer**. Select **Properties**. Open the **Debug** tab.

2. For **Profile**, select the **New** button. Name the profile "IIS" in the popup window. Select **OK** to create the profile.

3. For the **Launch** setting, select **IIS** from the list.

4. Select the checkbox for **Launch browser** and provide the endpoint URL.

   When the app requires HTTPS, use an HTTPS endpoint (`https://`). For HTTP, use an HTTP (`http://`) endpoint.

   Provide the same host name and port as the [IIS configuration specified earlier uses](#configure-iis), typically `localhost`.

   Provide the name of the app at the end of the URL.

   For example, `https://localhost/WebApplication1` (HTTPS) or `http://localhost/WebApplication1` (HTTP) are valid endpoint URLs.

5. In the **Environment variables** section, select the **Add** button. Provide an environment variable with a **Name** of `ASPNETCORE_ENVIRONMENT` and a **Value** of `Development`.

6. In the **Web Server Settings** area, set the **App URL** to the same value used for the **Launch browser** endpoint URL.

7. For the **Hosting Model** setting in Visual Studio 2019 or later, select **Default** to use the hosting model used by the project. If the project sets the `<AspNetCoreHostingModel>` property in its project file, the value of the property (`InProcess` or `OutOfProcess`) is used. If the property isn't present, the default hosting model of the app is used, which is out-of-process. If the app requires an explicit hosting model setting different from the app's normal hosting model, set the **Hosting Model** to either `In Process` or `Out Of Process` as needed.

8. Save the profile.

When not using Visual Studio, manually add a launch profile to the [launchSettings.json](https://json.schemastore.org/launchsettings) file in the *Properties* folder. The following example configures the profile to use the HTTPS protocol:

```
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iis": {
      "applicationUrl": "https://localhost/WebApplication1",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS": {
      "commandName": "IIS",
      "launchBrowser": true,
      "launchUrl": "https://localhost/WebApplication1",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
```

Confirm that the `applicationUrl` and `launchUrl` endpoints match and use the same protocol as the IIS binding configuration, either HTTP or HTTPS.

## Run the project

Run Visual Studio as an administrator:

* Confirm that the build configuration drop-down list is set to **Debug**.
* Set the [Start Debugging button](/en-us/visualstudio/debugger/debugger-feature-tour) to the **IIS** profile and select the button to start the app.

Visual Studio may prompt a restart if not running as an administrator. If prompted, restart Visual Studio.

If an untrusted development certificate is used, the browser may require you to create an exception for the untrusted certificate.

Note

Debugging a Release build configuration with [Just My Code](/en-us/visualstudio/debugger/just-my-code) and compiler optimizations results in a degraded experience. For example, break points aren't hit.

## Additional resources

* [Getting Started with the IIS Manager in IIS](/en-us/iis/get-started/getting-started-with-iis/getting-started-with-the-iis-manager-in-iis-7-and-iis-8)
* [Enforce HTTPS in ASP.NET Core](../../security/enforcing-ssl?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/10/cookie-authentication-api-endpoints
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/breaking-changes/10/cookie-authentication-api-endpoints.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Cookie login redirects are disabled for known API endpoints

By default, unauthenticated and unauthorized requests made to known API endpoints protected by cookie authentication now result in 401 and 403 responses rather than redirecting to a login or access-denied URI.

Known API [endpoints](/en-us/aspnet/core/fundamentals/routing) are identified using the new [IDisableCookieRedirectMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.idisablecookieredirectmetadata) interface, and metadata implementing the new interface has been added automatically to the following:

* [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) endpoints.
* Minimal API endpoints that read JSON request bodies or write JSON responses.
* Endpoints using [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) return types.
* SignalR endpoints.

The companion [IAllowCookieRedirectMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iallowcookieredirectmetadata) interface opts an endpoint back in to redirects. It overrides `IDisableCookieRedirectMetadata` no matter the order in which the metadata is added.

The 401 and 403 responses still include a `Location` header holding the login or access-denied URI. Only the status code changes.

Only the challenge (401) and forbid (403) paths consider endpoint metadata. Sign-out and return-URL redirects are unaffected.

## Version introduced

.NET 10 Preview 7

## Previous behavior

Previously, the cookie authentication handler redirected unauthenticated and unauthorized requests to a login or access-denied URI by default for all requests other than [XMLHttpRequests (XHRs)](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest).

## New behavior

Starting in .NET 10, unauthenticated and unauthorized requests made to known API endpoints result in 401 and 403 responses rather than redirecting to a login or access-denied URI. XHRs continue to result in 401 and 403 responses regardless of the target endpoint.

## Type of breaking change

This change is a [behavioral change](/en-us/dotnet/core/compatibility/categories#behavioral-change).

## Reason for change

This change was highly requested. Redirecting unauthenticated requests to a login page doesn't usually make sense for API endpoints, which typically rely on 401 and 403 status codes rather than HTML redirects to communicate auth failures.

## Recommended action

To restore the previous behavior for the whole app, enable the `Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata` switch. Cookie authentication then ignores endpoint metadata, and only XHRs result in 401 and 403 responses, which matches the pre-.NET 10 behavior. Set the switch in the project file so that it applies before any app code runs:

```
<ItemGroup>
  <RuntimeHostConfigurationOption
    Include="Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata"
    Value="true" />
</ItemGroup>
```

The switch is read once, the first time cookie authentication is used, so if you set it in code instead, call [SetSwitch](/en-us/dotnet/api/system.appcontext.setswitch) before the host is built:

```
AppContext.SetSwitch(
    "Microsoft.AspNetCore.Authentication.Cookies.IgnoreRedirectMetadata", true);

var builder = WebApplication.CreateBuilder(args);
```

To restore redirects for individual endpoints instead, call [AllowCookieRedirect](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieredirectendpointconventionbuilderextensions.allowcookieredirect) or apply the [AllowCookieRedirectAttribute](/en-us/dotnet/api/microsoft.aspnetcore.http.allowcookieredirectattribute) to an action method or controller class:

```
app.MapGet("/reports/summary", () => new { Total = 1000 })
   .RequireAuthorization()
   .AllowCookieRedirect();
```

Conversely, call [DisableCookieRedirect](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieredirectendpointconventionbuilderextensions.disablecookieredirect) to return 401 and 403 status codes for endpoints that aren't detected as API endpoints automatically.

If you want to always redirect to the login and access-denied URIs for unauthenticated or unauthorized requests regardless of the target endpoint or whether the source of the request is an XHR, you can override [RedirectToLogin](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.redirecttologin) and [RedirectToAccessDenied](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.redirecttoaccessdenied) as follows:

```
builder.Services.AddAuthentication()
    .AddCookie(options =>
    {
        options.Events.OnRedirectToLogin = context =>
        {
            context.Response.Redirect(context.RedirectUri);
            return Task.CompletedTask;
        };

        options.Events.OnRedirectToAccessDenied = context =>
        {
            context.Response.Redirect(context.RedirectUri);
            return Task.CompletedTask;
        };
    });
```

If you want to revert to the exact previous behavior that avoids redirecting for only XHRs, you can override the events with this slightly more complicated logic:

```
builder.Services.AddAuthentication()
    .AddCookie(options =>
    {
        bool IsXhr(HttpRequest request)
        {
            return string.Equals(request.Query[HeaderNames.XRequestedWith], "XMLHttpRequest", StringComparison.Ordinal) ||
                string.Equals(request.Headers.XRequestedWith, "XMLHttpRequest", StringComparison.Ordinal);
        }

        options.Events.OnRedirectToLogin = context =>
        {
            if (IsXhr(context.Request))
            {
                context.Response.Headers.Location = context.RedirectUri;
                context.Response.StatusCode = 401;
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }

            return Task.CompletedTask;
        };

        options.Events.OnRedirectToAccessDenied = context =>
        {
            if (IsXhr(context.Request))
            {
                context.Response.Headers.Location = context.RedirectUri;
                context.Response.StatusCode = 403;
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }

            return Task.CompletedTask;
        };
    });
```

## Affected APIs

* [Microsoft.AspNetCore.Http.Metadata.IDisableCookieRedirectMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.idisablecookieredirectmetadata)
* [Microsoft.AspNetCore.Http.Metadata.IAllowCookieRedirectMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iallowcookieredirectmetadata)
* [Microsoft.AspNetCore.Http.AllowCookieRedirectAttribute](/en-us/dotnet/api/microsoft.aspnetcore.http.allowcookieredirectattribute)
* [Microsoft.AspNetCore.Builder.CookieRedirectEndpointConventionBuilderExtensions.DisableCookieRedirect](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieredirectendpointconventionbuilderextensions.disablecookieredirect)
* [Microsoft.AspNetCore.Builder.CookieRedirectEndpointConventionBuilderExtensions.AllowCookieRedirect](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieredirectendpointconventionbuilderextensions.allowcookieredirect)
* [Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents.RedirectToLogin](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.redirecttologin)
* [Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents.RedirectToAccessDenied](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.redirecttoaccessdenied)

## See also

* [API endpoint authentication behavior in ASP.NET Core](/en-us/aspnet/core/security/authentication/api-endpoint-auth?view=aspnetcore-10.0\&preserve-view=true)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-16

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/grpc/grpc-start.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Create a gRPC client and server in ASP.NET Core

This tutorial shows how to create a .NET [gRPC](../../grpc/?view=aspnetcore-10.0) client and an ASP.NET Core gRPC Server. At the end, you'll have a gRPC client that communicates with the gRPC Greeter service.

In this tutorial, you:

* Create a gRPC Server.
* Create a gRPC client.
* Test the gRPC client with the gRPC Greeter service.

## Create a gRPC service

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio and select **Create a new Project**.
- In the **Create a new project** dialog, search for `gRPC`. Select **ASP.NET Core gRPC Service** and select **Next**.
- In the **Configure your new project** dialog, enter `GrpcGreeter` for **Project name**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.
- Select **Next**.
- In the **Additional information** dialog, select **.NET 10.0 (Long Term Support)** and then select **Create**.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/terminal/basics).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new grpc -o GrpcGreeter
  code -r GrpcGreeter
  ```

  The `dotnet new` command creates a new gRPC service in the *GrpcGreeter* folder.

  The `code` command opens the *GrpcGreeter* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

### Run the service

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

- Press Ctrl+F5 to run without the debugger.

  Visual Studio displays the following dialog:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* Press **Ctrl-F5** to run without the debugger.

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

The logs show the service listening on `https://localhost:<port>`, where `<port>` is the localhost port number randomly assigned when the project is created and set in `Properties/launchSettings.json`.

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

Note

The gRPC template is configured to use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246). gRPC clients need to use HTTPS to call the server. The gRPC service localhost port number is randomly assigned when the project is created and set in the *Properties\launchSettings.json* file of the gRPC service project.

### Examine the project files

*GrpcGreeter* project files:

* `Protos/greet.proto`: defines the `Greeter` gRPC and is used to generate the gRPC server assets. For more information, see [Introduction to gRPC](../../grpc/?view=aspnetcore-10.0).

* `Services` folder: Contains the implementation of the `Greeter` service.

* `appsettings.json`: Contains configuration data such as the protocol used by Kestrel. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

* `Program.cs`, which contains:

  * The entry point for the gRPC service. For more information, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0).
  * Code that configures app behavior. For more information, see [App startup](../../fundamentals/startup?view=aspnetcore-10.0).

## Create the gRPC client in a .NET console app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

- Open a second instance of Visual Studio and select **Create a new project**.
- In the **Create a new project** dialog, select **Console App**, and select **Next**.
- In the **Project name** text box, enter **GrpcGreeterClient** and select **Next**.
- In the **Additional information** dialog, select **.NET 10.0 (Long Term Support)** and then select **Create**.

* Open the [integrated terminal](https://code.visualstudio.com/docs/terminal/basics).

* Change directories (`cd`) to a folder for the project.

* Run the following commands:

  ```
  dotnet new console -o GrpcGreeterClient
  code -r GrpcGreeterClient
  ```

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

### Add required NuGet packages

The gRPC client project requires the following NuGet packages:

* [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client), which contains the .NET client.
* [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/), which contains protobuf message APIs for C#.
* [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), which contain C# tooling support for protobuf files. The tooling package isn't required at runtime, so the dependency is marked with `PrivateAssets="All"`.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)

Install the packages using either the Package Manager Console (PMC) or Manage NuGet Packages.

#### PMC option to install packages

* From Visual Studio, select **Tools** > **NuGet Package Manager** > **Package Manager Console**

* From the **Package Manager Console** window, run `cd GrpcGreeterClient` to change directories to the folder containing the `GrpcGreeterClient.csproj` files.

* Run the following commands:

  ```
  Install-Package Grpc.Net.Client
  Install-Package Google.Protobuf
  Install-Package Grpc.Tools
  ```

#### Manage NuGet Packages option to install packages

* Right-click the project in **Solution Explorer** > **Manage NuGet Packages**.
* Select the **Browse** tab.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the **Browse** tab and select **Install**.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

Run the following commands from the **Integrated Terminal**:

```
dotnet add GrpcGreeterClient.csproj package Grpc.Net.Client
dotnet add GrpcGreeterClient.csproj package Google.Protobuf
dotnet add GrpcGreeterClient.csproj package Grpc.Tools
```

### Add greet.proto

* Create a *Protos* folder in the gRPC client project.

* Copy the *Protos\greet.proto* file from the gRPC Greeter service to the *Protos* folder in the gRPC client project.

* Update the namespace inside the `greet.proto` file to the project's namespace:

  ```
  option csharp_namespace = "GrpcGreeterClient";
  ```

* Edit the `GrpcGreeterClient.csproj` project file:

- [Visual Studio](#tabpanel_5_visual-studio)
- [Visual Studio Code](#tabpanel_5_visual-studio-code)

Right-click the project and select **Edit Project File**.

Select the `GrpcGreeterClient.csproj` file.

* Add an item group with a `<Protobuf>` element that refers to the *greet.proto* file:

  ```
  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>
  ```

### Create the Greeter client

* Build the client project to create the types in the `GrpcGreeterClient` namespace.

Note

The `GrpcGreeterClient` types are generated automatically by the build process. The tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) generates the following files based on the *greet.proto* file:

* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs`: The protocol buffer code which populates, serializes and retrieves the request and response message types.
* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs`: Contains the generated client classes.

For more information on the C# assets automatically generated by [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), see [gRPC services with C#: Generated C# assets](../../grpc/basics?view=aspnetcore-10.0#generated-c-assets).

* Update the gRPC client `Program.cs` file with the following code.

  ```
  using Grpc.Net.Client;
  using GrpcGreeterClient;

  // The port number must match the port of the gRPC server.
  using var channel = GrpcChannel.ForAddress("https://localhost:7042");
  var client = new Greeter.GreeterClient(channel);
  var reply = await client.SayHelloAsync(
      new HelloRequest { Name = "GreeterClient" });
  Console.WriteLine("Greeting: " + reply.Message);
  Console.WriteLine("Press any key to exit...");
  Console.ReadKey();
  ```

* In the preceding highlighted code, replace the localhost port number `7042` with the `HTTPS` port number specified in `Properties/launchSettings.json` within the `GrpcGreeter` service project.

`Program.cs` contains the entry point and logic for the gRPC client.

The Greeter client is created by:

* Instantiating a `GrpcChannel` containing the information for creating the connection to the gRPC service.
* Using the `GrpcChannel` to construct the Greeter client:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
    new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

The Greeter client calls the asynchronous `SayHello` method. The result of the `SayHello` call is displayed:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
    new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

## Test the gRPC client with the gRPC Greeter service

Update the `appsettings.Development.json` file by adding the following highlighted lines:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.AspNetCore.Hosting": "Information",
      "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Information"
    }
  }
}
```

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

- In the `GrpcGreeter` service project, press `Ctrl+F5` to start the server without the debugger.
- In the `GrpcGreeterClient` console project, press `Ctrl+F5` to start the client without the debugger.

* Start the Greeter service.
* Start the client.

The client sends a greeting to the service with a message containing its name, *GreeterClient*. The service sends the message "Hello GreeterClient" as a response. The "Hello GreeterClient" response is displayed in the command prompt:

```
Greeting: Hello GreeterClient
Press any key to exit...
```

The gRPC service records the details of the successful call in the logs written to the command prompt:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpc-start\samples\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:<port>/greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 POST https://localhost:7042/greet.Greeter/SayHello - 200 - application/grpc 40.4615ms
```

Note

The code in this article requires the ASP.NET Core HTTPS development certificate to secure the gRPC service. If the .NET gRPC client fails with the message `The remote certificate is invalid according to the validation procedure.` or `The SSL connection could not be established.`, the development certificate isn't trusted. To fix this issue, see [Call a gRPC service with an untrusted/invalid certificate](../../grpc/troubleshoot?view=aspnetcore-10.0#call-a-grpc-service-with-an-untrustedinvalid-certificate).

### Next steps

* View or download [the completed sample code for this tutorial](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).
* [Overview for gRPC on .NET](../../grpc/?view=aspnetcore-10.0)
* [gRPC Services with C#](../../grpc/basics?view=aspnetcore-10.0)
* [Migrate gRPC from C-core to gRPC for .NET](../../grpc/migration?view=aspnetcore-10.0)

This tutorial shows how to create a .NET [gRPC](../../grpc/?view=aspnetcore-10.0) client and an ASP.NET Core gRPC Server. At the end, you'll have a gRPC client that communicates with the gRPC Greeter service.

In this tutorial, you:

* Create a gRPC Server.
* Create a gRPC client.
* Test the gRPC client with the gRPC Greeter service.

## Create a gRPC service

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio 2022 and select **New Project**.
- In the **Create a new project** dialog, search for `gRPC`. Select **ASP.NET Core gRPC Service** and select **Next**.
- In the **Configure your new project** dialog, enter `GrpcGreeter` for **Project name**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.
- Select **Next**.
- In the **Additional information** dialog, select **.NET 9.0 (Standard Term Support)** and then select **Create**.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/terminal/basics).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new grpc -o GrpcGreeter
  code -r GrpcGreeter
  ```

  The `dotnet new` command creates a new gRPC service in the *GrpcGreeter* folder.

  The `code` command opens the *GrpcGreeter* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

### Run the service

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

- Press Ctrl+F5 to run without the debugger.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* Press **Ctrl-F5** to run without the debugger.

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

The logs show the service listening on `https://localhost:<port>`, where `<port>` is the localhost port number randomly assigned when the project is created and set in `Properties/launchSettings.json`.

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

Note

The gRPC template is configured to use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246). gRPC clients need to use HTTPS to call the server. The gRPC service localhost port number is randomly assigned when the project is created and set in the *Properties\launchSettings.json* file of the gRPC service project.

### Examine the project files

*GrpcGreeter* project files:

* `Protos/greet.proto`: defines the `Greeter` gRPC and is used to generate the gRPC server assets. For more information, see [Introduction to gRPC](../../grpc/?view=aspnetcore-10.0).

* `Services` folder: Contains the implementation of the `Greeter` service.

* `appsettings.json`: Contains configuration data such as the protocol used by Kestrel. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

* `Program.cs`, which contains:

  * The entry point for the gRPC service. For more information, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0).
  * Code that configures app behavior. For more information, see [App startup](../../fundamentals/startup?view=aspnetcore-10.0).

## Create the gRPC client in a .NET console app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

- Open a second instance of Visual Studio and select **New Project**.
- In the **Create a new project** dialog, select **Console App**, and select **Next**.
- In the **Project name** text box, enter **GrpcGreeterClient** and select **Next**.
- In the **Additional information** dialog, select **.NET 9.0 (Standard Term Support)** and then select **Create**.

* Open the [integrated terminal](https://code.visualstudio.com/docs/terminal/basics).

* Change directories (`cd`) to a folder for the project.

* Run the following commands:

  ```
  dotnet new console -o GrpcGreeterClient
  code -r GrpcGreeterClient
  ```

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

### Add required NuGet packages

The gRPC client project requires the following NuGet packages:

* [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client), which contains the .NET client.
* [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/), which contains protobuf message APIs for C#.
* [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), which contain C# tooling support for protobuf files. The tooling package isn't required at runtime, so the dependency is marked with `PrivateAssets="All"`.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)

Install the packages using either the Package Manager Console (PMC) or Manage NuGet Packages.

#### PMC option to install packages

* From Visual Studio, select **Tools** > **NuGet Package Manager** > **Package Manager Console**

* From the **Package Manager Console** window, run `cd GrpcGreeterClient` to change directories to the folder containing the `GrpcGreeterClient.csproj` files.

* Run the following commands:

  ```
  Install-Package Grpc.Net.Client
  Install-Package Google.Protobuf
  Install-Package Grpc.Tools
  ```

#### Manage NuGet Packages option to install packages

* Right-click the project in **Solution Explorer** > **Manage NuGet Packages**.
* Select the **Browse** tab.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the **Browse** tab and select **Install**.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

Run the following commands from the **Integrated Terminal**:

```
dotnet add GrpcGreeterClient.csproj package Grpc.Net.Client
dotnet add GrpcGreeterClient.csproj package Google.Protobuf
dotnet add GrpcGreeterClient.csproj package Grpc.Tools
```

### Add greet.proto

* Create a *Protos* folder in the gRPC client project.

* Copy the *Protos\greet.proto* file from the gRPC Greeter service to the *Protos* folder in the gRPC client project.

* Update the namespace inside the `greet.proto` file to the project's namespace:

  ```
  option csharp_namespace = "GrpcGreeterClient";
  ```

* Edit the `GrpcGreeterClient.csproj` project file:

- [Visual Studio](#tabpanel_5_visual-studio)
- [Visual Studio Code](#tabpanel_5_visual-studio-code)

Right-click the project and select **Edit Project File**.

Select the `GrpcGreeterClient.csproj` file.

* Add an item group with a `<Protobuf>` element that refers to the *greet.proto* file:

  ```
  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>
  ```

### Create the Greeter client

* Build the client project to create the types in the `GrpcGreeterClient` namespace.

Note

The `GrpcGreeterClient` types are generated automatically by the build process. The tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) generates the following files based on the *greet.proto* file:

* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs`: The protocol buffer code which populates, serializes and retrieves the request and response message types.
* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs`: Contains the generated client classes.

For more information on the C# assets automatically generated by [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), see [gRPC services with C#: Generated C# assets](../../grpc/basics?view=aspnetcore-10.0#generated-c-assets).

* Update the gRPC client `Program.cs` file with the following code.

  ```
  using Grpc.Net.Client;
  using GrpcGreeterClient;

  // The port number must match the port of the gRPC server.
  using var channel = GrpcChannel.ForAddress("https://localhost:7042");
  var client = new Greeter.GreeterClient(channel);
  var reply = await client.SayHelloAsync(
      new HelloRequest { Name = "GreeterClient" });
  Console.WriteLine("Greeting: " + reply.Message);
  Console.WriteLine("Press any key to exit...");
  Console.ReadKey();
  ```

* In the preceding highlighted code, replace the localhost port number `7042` with the `HTTPS` port number specified in `Properties/launchSettings.json` within the `GrpcGreeter` service project.

`Program.cs` contains the entry point and logic for the gRPC client.

The Greeter client is created by:

* Instantiating a `GrpcChannel` containing the information for creating the connection to the gRPC service.
* Using the `GrpcChannel` to construct the Greeter client:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
    new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

The Greeter client calls the asynchronous `SayHello` method. The result of the `SayHello` call is displayed:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
    new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

## Test the gRPC client with the gRPC Greeter service

Update the `appsettings.Development.json` file by adding the following highlighted lines:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.AspNetCore.Hosting": "Information",
      "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Information"
    }
  }
}
```

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

- In the `GrpcGreeter` service project, press `Ctrl+F5` to start the server without the debugger.
- In the `GrpcGreeterClient` console project, press `Ctrl+F5` to start the client without the debugger.

* Start the Greeter service.
* Start the client.

The client sends a greeting to the service with a message containing its name, *GreeterClient*. The service sends the message "Hello GreeterClient" as a response. The "Hello GreeterClient" response is displayed in the command prompt:

```
Greeting: Hello GreeterClient
Press any key to exit...
```

The gRPC service records the details of the successful call in the logs written to the command prompt:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpc-start\samples\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:<port>/greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 POST https://localhost:7042/greet.Greeter/SayHello - 200 - application/grpc 40.4615ms
```

Note

The code in this article requires the ASP.NET Core HTTPS development certificate to secure the gRPC service. If the .NET gRPC client fails with the message `The remote certificate is invalid according to the validation procedure.` or `The SSL connection could not be established.`, the development certificate isn't trusted. To fix this issue, see [Call a gRPC service with an untrusted/invalid certificate](../../grpc/troubleshoot?view=aspnetcore-10.0#call-a-grpc-service-with-an-untrustedinvalid-certificate).

### Next steps

* View or download [the completed sample code for this tutorial](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).
* [Overview for gRPC on .NET](../../grpc/?view=aspnetcore-10.0)
* [gRPC Services with C#](../../grpc/basics?view=aspnetcore-10.0)
* [Migrate gRPC from C-core to gRPC for .NET](../../grpc/migration?view=aspnetcore-10.0)

This tutorial shows how to create a .NET [gRPC](../../grpc/?view=aspnetcore-10.0) client and an ASP.NET Core gRPC Server. At the end, you'll have a gRPC client that communicates with the gRPC Greeter service.

In this tutorial, you:

* Create a gRPC Server.
* Create a gRPC client.
* Test the gRPC client with the gRPC Greeter service.

## Create a gRPC service

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio 2022 and select **New Project**.
- In the **Create a new project** dialog, search for `gRPC`. Select **ASP.NET Core gRPC Service** and select **Next**.
- In the **Configure your new project** dialog, enter `GrpcGreeter` for **Project name**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.
- Select **Next**.
- In the **Additional information** dialog, select **.NET 8.0 (Long Term Support)** and then select **Create**.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new grpc -o GrpcGreeter
  code -r GrpcGreeter
  ```

  The `dotnet new` command creates a new gRPC service in the *GrpcGreeter* folder.

  The `code` command opens the *GrpcGreeter* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

### Run the service

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

- Press Ctrl+F5 to run without the debugger.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* Press **Ctrl-F5** to run without the debugger.

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

- Select **Debug** > **Start Without Debugging** to launch the app.

  Visual Studio for Mac:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

  Visual Studio for Mac displays the following popup:

  Select **Install and Trust** if you trust the development certificate.

  The following dialog is displayed:

  Enter your password and select **Update Settings**

  Select **Yes** if you agree to trust the development certificate.

  See [Trust the ASP.NET Core HTTPS development certificate](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos) for more information.

- From Visual Studio, press **Opt-Cmd-Return** to run without the debugger. Alternatively, navigate to the menu bar and go to **Run>Start Without Debugging**.

  Visual Studio starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0), launches a browser, and navigates to a randomly assigned port such as `http://localhost:7042`.

The logs show the service listening on `https://localhost:<port>`, where `<port>` is the localhost port number randomly assigned when the project is created and set in `Properties/launchSettings.json`.

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

Note

The gRPC template is configured to use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246). gRPC clients need to use HTTPS to call the server. The gRPC service localhost port number is randomly assigned when the project is created and set in the *Properties\launchSettings.json* file of the gRPC service project.

### Examine the project files

*GrpcGreeter* project files:

* `Protos/greet.proto`: defines the `Greeter` gRPC and is used to generate the gRPC server assets. For more information, see [Introduction to gRPC](../../grpc/?view=aspnetcore-10.0).

* `Services` folder: Contains the implementation of the `Greeter` service.

* `appSettings.json`: Contains configuration data such as the protocol used by Kestrel. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

* `Program.cs`, which contains:

  * The entry point for the gRPC service. For more information, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0).
  * Code that configures app behavior. For more information, see [App startup](../../fundamentals/startup?view=aspnetcore-10.0).

## Create the gRPC client in a .NET console app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

- Open a second instance of Visual Studio and select **New Project**.
- In the **Create a new project** dialog, select **Console App**, and select **Next**.
- In the **Project name** text box, enter **GrpcGreeterClient** and select **Next**.
- In the **Additional information** dialog, select **.NET 8.0 (Long Term Support)** and then select **Create**.

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to a folder for the project.

* Run the following commands:

  ```
  dotnet new console -o GrpcGreeterClient
  code -r GrpcGreeterClient
  ```

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

### Add required NuGet packages

The gRPC client project requires the following NuGet packages:

* [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client), which contains the .NET client.
* [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/), which contains protobuf message APIs for C#.
* [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), which contain C# tooling support for protobuf files. The tooling package isn't required at runtime, so the dependency is marked with `PrivateAssets="All"`.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)

Install the packages using either the Package Manager Console (PMC) or Manage NuGet Packages.

#### PMC option to install packages

* From Visual Studio, select **Tools** > **NuGet Package Manager** > **Package Manager Console**

* From the **Package Manager Console** window, run `cd GrpcGreeterClient` to change directories to the folder containing the `GrpcGreeterClient.csproj` files.

* Run the following commands:

  ```
  Install-Package Grpc.Net.Client
  Install-Package Google.Protobuf
  Install-Package Grpc.Tools
  ```

#### Manage NuGet Packages option to install packages

* Right-click the project in **Solution Explorer** > **Manage NuGet Packages**.
* Select the **Browse** tab.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the **Browse** tab and select **Install**.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

Run the following commands from the **Integrated Terminal**:

```
dotnet add GrpcGreeterClient.csproj package Grpc.Net.Client
dotnet add GrpcGreeterClient.csproj package Google.Protobuf
dotnet add GrpcGreeterClient.csproj package Grpc.Tools
```

### Add greet.proto

* Create a *Protos* folder in the gRPC client project.

* Copy the *Protos\greet.proto* file from the gRPC Greeter service to the *Protos* folder in the gRPC client project.

* Update the namespace inside the `greet.proto` file to the project's namespace:

  ```
  option csharp_namespace = "GrpcGreeterClient";
  ```

* Edit the `GrpcGreeterClient.csproj` project file:

- [Visual Studio](#tabpanel_5_visual-studio)
- [Visual Studio Code](#tabpanel_5_visual-studio-code)

Right-click the project and select **Edit Project File**.

Select the `GrpcGreeterClient.csproj` file.

* Add an item group with a `<Protobuf>` element that refers to the *greet.proto* file:

  ```
  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>
  ```

### Create the Greeter client

* Build the client project to create the types in the `GrpcGreeterClient` namespace.

Note

The `GrpcGreeterClient` types are generated automatically by the build process. The tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) generates the following files based on the *greet.proto* file:

* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs`: The protocol buffer code which populates, serializes and retrieves the request and response message types.
* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs`: Contains the generated client classes.

For more information on the C# assets automatically generated by [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), see [gRPC services with C#: Generated C# assets](../../grpc/basics?view=aspnetcore-10.0#generated-c-assets).

* Update the gRPC client `Program.cs` file with the following code.

  ```
  using System.Threading.Tasks;
  using Grpc.Net.Client;
  using GrpcGreeterClient;

  // The port number must match the port of the gRPC server.
  using var channel = GrpcChannel.ForAddress("https://localhost:7042");
  var client = new Greeter.GreeterClient(channel);
  var reply = await client.SayHelloAsync(
                    new HelloRequest { Name = "GreeterClient" });
  Console.WriteLine("Greeting: " + reply.Message);
  Console.WriteLine("Press any key to exit...");
  Console.ReadKey();
  ```

* In the preceding highlighted code, replace the localhost port number `7042` with the `HTTPS` port number specified in `Properties/launchSettings.json` within the `GrpcGreeter` service project.

`Program.cs` contains the entry point and logic for the gRPC client.

The Greeter client is created by:

* Instantiating a `GrpcChannel` containing the information for creating the connection to the gRPC service.
* Using the `GrpcChannel` to construct the Greeter client:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

The Greeter client calls the asynchronous `SayHello` method. The result of the `SayHello` call is displayed:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

## Test the gRPC client with the gRPC Greeter service

Update the `appsettings.Development.json` file by adding the following highlighted lines:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
      ,"Microsoft.AspNetCore.Hosting": "Information",
      "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Information"
    }
  }
}
```

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

- In the Greeter service, press `Ctrl+F5` to start the server without the debugger.
- In the `GrpcGreeterClient` project, press `Ctrl+F5` to start the client without the debugger.

* Start the Greeter service.
* Start the client.

The client sends a greeting to the service with a message containing its name, *GreeterClient*. The service sends the message "Hello GreeterClient" as a response. The "Hello GreeterClient" response is displayed in the command prompt:

```
Greeting: Hello GreeterClient
Press any key to exit...
```

The gRPC service records the details of the successful call in the logs written to the command prompt:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpc-start\samples\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:<port>/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 78.32260000000001ms 200 application/grpc
```

Note

The code in this article requires the ASP.NET Core HTTPS development certificate to secure the gRPC service. If the .NET gRPC client fails with the message `The remote certificate is invalid according to the validation procedure.` or `The SSL connection could not be established.`, the development certificate isn't trusted. To fix this issue, see [Call a gRPC service with an untrusted/invalid certificate](../../grpc/troubleshoot?view=aspnetcore-10.0#call-a-grpc-service-with-an-untrustedinvalid-certificate).

### Next steps

* View or download [the completed sample code for this tutorial](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).
* [Overview for gRPC on .NET](../../grpc/?view=aspnetcore-10.0)
* [gRPC Services with C#](../../grpc/basics?view=aspnetcore-10.0)
* [Migrate gRPC from C-core to gRPC for .NET](../../grpc/migration?view=aspnetcore-10.0)

This tutorial shows how to create a .NET [gRPC](../../grpc/?view=aspnetcore-10.0) client and an ASP.NET Core gRPC Server. At the end, you'll have a gRPC client that communicates with the gRPC Greeter service.

In this tutorial, you:

* Create a gRPC Server.
* Create a gRPC client.
* Test the gRPC client with the gRPC Greeter service.

## Create a gRPC service

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- Start Visual Studio 2022 and select **Create a new project**.
- In the **Create a new project** dialog, search for `gRPC`. Select **ASP.NET Core gRPC Service** and select **Next**.
- In the **Configure your new project** dialog, enter `GrpcGreeter` for **Project name**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.
- Select **Next**.
- In the **Additional information** dialog, select **.NET 6.0 (Long-term support)** and then select **Create**.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new grpc -o GrpcGreeter
  code -r GrpcGreeter
  ```

  The `dotnet new` command creates a new gRPC service in the *GrpcGreeter* folder.

  The `code` command opens the *GrpcGreeter* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

* Start Visual Studio for Mac and select **File** > **New Project**.
* In the **Choose a template for your new project** dialog, select **Web and Console** > **App** > **gRPC Service** and select **Continue**.
* Select **.NET 6.0** for the target framework and select **Continue**.
* Name the project **GrpcGreeter**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.
* Select **Continue**.

### Run the service

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

- Press Ctrl+F5 to run without the debugger.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* Press **Ctrl-F5** to run without the debugger.

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

- Select **Debug** > **Start Without Debugging** to launch the app.

  Visual Studio for Mac:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

  Visual Studio for Mac displays the following popup:

  Select **Install and Trust** if you trust the development certificate.

  The following dialog is displayed:

  Enter your password and select **Update Settings**

  Select **Yes** if you agree to trust the development certificate.

  See [Trust the ASP.NET Core HTTPS development certificate](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos) for more information.

- From Visual Studio, press **Opt-Cmd-Return** to run without the debugger. Alternatively, navigate to the menu bar and go to **Run>Start Without Debugging**.

  Visual Studio starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0), launches a browser, and navigates to a randomly assigned port such as `http://localhost:7042`.

The logs show the service listening on `https://localhost:<port>`, where `<port>` is the localhost port number randomly assigned when the project is created and set in `Properties/launchSettings.json`.

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

Note

The gRPC template is configured to use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246). gRPC clients need to use HTTPS to call the server. The gRPC service localhost port number is randomly assigned when the project is created and set in the *Properties\launchSettings.json* file of the gRPC service project.

macOS doesn't support ASP.NET Core gRPC with TLS. Additional configuration is required to successfully run gRPC services on macOS. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](../../grpc/troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

### Examine the project files

*GrpcGreeter* project files:

* `Protos/greet.proto`: defines the `Greeter` gRPC and is used to generate the gRPC server assets. For more information, see [Introduction to gRPC](../../grpc/?view=aspnetcore-10.0).

* `Services` folder: Contains the implementation of the `Greeter` service.

* `appSettings.json`: Contains configuration data such as the protocol used by Kestrel. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

* `Program.cs`, which contains:

  * The entry point for the gRPC service. For more information, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0).
  * Code that configures app behavior. For more information, see [App startup](../../fundamentals/startup?view=aspnetcore-10.0).

## Create the gRPC client in a .NET console app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

- Open a second instance of Visual Studio and select **Create a new project**.
- In the **Create a new project** dialog, select **Console Application**, and select **Next**.
- In the **Project name** text box, enter **GrpcGreeterClient** and select **Next**.
- In the **Additional information** dialog, select **.NET 6.0 (Long-term support)** and then select **Create**.

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to a folder for the project.

* Run the following commands:

  ```
  dotnet new console -o GrpcGreeterClient
  code -r GrpcGreeterClient
  ```

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Follow the instructions in [Building a complete .NET solution on macOS using Visual Studio for Mac](/en-us/dotnet/core/tutorials/using-on-mac-vs-full-solution) to create a console app with the name *GrpcGreeterClient*.

### Add required NuGet packages

The gRPC client project requires the following NuGet packages:

* [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client), which contains the .NET client.
* [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/), which contains protobuf message APIs for C#.
* [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), which contain C# tooling support for protobuf files. The tooling package isn't required at runtime, so the dependency is marked with `PrivateAssets="All"`.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)

Install the packages using either the Package Manager Console (PMC) or Manage NuGet Packages.

#### PMC option to install packages

* From Visual Studio, select **Tools** > **NuGet Package Manager** > **Package Manager Console**

* From the **Package Manager Console** window, run `cd GrpcGreeterClient` to change directories to the folder containing the `GrpcGreeterClient.csproj` files.

* Run the following commands:

  ```
  Install-Package Grpc.Net.Client
  Install-Package Google.Protobuf
  Install-Package Grpc.Tools
  ```

#### Manage NuGet Packages option to install packages

* Right-click the project in **Solution Explorer** > **Manage NuGet Packages**.
* Select the **Browse** tab.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the **Browse** tab and select **Install**.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

Run the following commands from the **Integrated Terminal**:

```
dotnet add GrpcGreeterClient.csproj package Grpc.Net.Client
dotnet add GrpcGreeterClient.csproj package Google.Protobuf
dotnet add GrpcGreeterClient.csproj package Grpc.Tools
```

* Right-click **GrpcGreeterClient** project in the **Solution Pad** and select **Manage NuGet Packages**.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the results pane and select **Add Package**.
* In **Select Projects** select **OK**.
* If the **License Acceptance** dialog appears, select **Accept** if you agree to the license terms.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

### Add greet.proto

* Create a *Protos* folder in the gRPC client project.

* Copy the *Protos\greet.proto* file from the gRPC Greeter service to the *Protos* folder in the gRPC client project.

* Update the namespace inside the `greet.proto` file to the project's namespace:

  ```
  option csharp_namespace = "GrpcGreeterClient";
  ```

* Edit the `GrpcGreeterClient.csproj` project file:

- [Visual Studio](#tabpanel_5_visual-studio)
- [Visual Studio Code](#tabpanel_5_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_5_visual-studio-mac)

Right-click the project and select **Edit Project File**.

Select the `GrpcGreeterClient.csproj` file.

Right-click the project and select **Edit Project File**.

* Add an item group with a `<Protobuf>` element that refers to the *greet.proto* file:

  ```
  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>
  ```

### Create the Greeter client

* Build the client project to create the types in the `GrpcGreeterClient` namespace.

Note

The `GrpcGreeterClient` types are generated automatically by the build process. The tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) generates the following files based on the *greet.proto* file:

* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs`: The protocol buffer code which populates, serializes and retrieves the request and response message types.
* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs`: Contains the generated client classes.

For more information on the C# assets automatically generated by [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), see [gRPC services with C#: Generated C# assets](../../grpc/basics?view=aspnetcore-10.0#generated-c-assets).

* Update the gRPC client `Program.cs` file with the following code.

  ```
  using System.Threading.Tasks;
  using Grpc.Net.Client;
  using GrpcGreeterClient;

  // The port number must match the port of the gRPC server.
  using var channel = GrpcChannel.ForAddress("https://localhost:7042");
  var client = new Greeter.GreeterClient(channel);
  var reply = await client.SayHelloAsync(
                    new HelloRequest { Name = "GreeterClient" });
  Console.WriteLine("Greeting: " + reply.Message);
  Console.WriteLine("Press any key to exit...");
  Console.ReadKey();
  ```

* In the preceding highlighted code, replace the localhost port number `7042` with the `HTTPS` port number specified in `Properties/launchSettings.json` within the `GrpcGreeter` service project.

`Program.cs` contains the entry point and logic for the gRPC client.

The Greeter client is created by:

* Instantiating a `GrpcChannel` containing the information for creating the connection to the gRPC service.
* Using the `GrpcChannel` to construct the Greeter client:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

The Greeter client calls the asynchronous `SayHello` method. The result of the `SayHello` call is displayed:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

## Test the gRPC client with the gRPC Greeter service

Update the `appsettings.Development.json` file by adding the following highlighted lines:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
      ,"Microsoft.AspNetCore.Hosting": "Information",
      "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Information"
    }
  }
}
```

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_6_visual-studio-mac)

- In the Greeter service, press `Ctrl+F5` to start the server without the debugger.
- In the `GrpcGreeterClient` project, press `Ctrl+F5` to start the client without the debugger.

* Start the Greeter service.
* Start the client.

- Due to the previously mentioned [HTTP/2 TLS issue on macOS workaround](../../grpc/troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos), you'll need to update the channel address in the client to match port in launchSetting.json of the GrpcGreeter service "http\://localhost:5000". Update line 13 of *`GrpcGreeterClient/Program.cs`* to read:

  ```
  using var channel = GrpcChannel.ForAddress("http://localhost:5000");
  ```

- Start the Greeter service.

- Start the client.

The client sends a greeting to the service with a message containing its name, *GreeterClient*. The service sends the message "Hello GreeterClient" as a response. The "Hello GreeterClient" response is displayed in the command prompt:

```
Greeting: Hello GreeterClient
Press any key to exit...
```

The gRPC service records the details of the successful call in the logs written to the command prompt:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpc-start\samples\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:<port>/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 78.32260000000001ms 200 application/grpc
```

Note

The code in this article requires the ASP.NET Core HTTPS development certificate to secure the gRPC service. If the .NET gRPC client fails with the message `The remote certificate is invalid according to the validation procedure.` or `The SSL connection could not be established.`, the development certificate isn't trusted. To fix this issue, see [Call a gRPC service with an untrusted/invalid certificate](../../grpc/troubleshoot?view=aspnetcore-10.0#call-a-grpc-service-with-an-untrustedinvalid-certificate).

### Next steps

* View or download [the completed sample code for this tutorial](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/samples/6) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).
* [Overview for gRPC on .NET](../../grpc/?view=aspnetcore-10.0)
* [gRPC Services with C#](../../grpc/basics?view=aspnetcore-10.0)
* [Migrate gRPC from C-core to gRPC for .NET](../../grpc/migration?view=aspnetcore-10.0)

This tutorial shows how to create a .NET [gRPC](../../grpc/?view=aspnetcore-10.0) client and an ASP.NET Core gRPC Server. At the end, you'll have a gRPC client that communicates with the gRPC Greeter service.

In this tutorial, you:

* Create a gRPC Server.
* Create a gRPC client.
* Test the gRPC client with the gRPC Greeter service.

## Create a gRPC service

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- Start Visual Studio 2022 and select **Create a new project**.
- In the **Create a new project** dialog, search for `gRPC`. Select **ASP.NET Core gRPC Service** and select **Next**.
- In the **Configure your new project** dialog, enter `GrpcGreeter` for **Project name**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.
- Select **Next**.
- In the **Additional information** dialog, select **.NET 6.0 (Long-term support)** and then select **Create**.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new grpc -o GrpcGreeter
  code -r GrpcGreeter
  ```

  The `dotnet new` command creates a new gRPC service in the *GrpcGreeter* folder.

  The `code` command opens the *GrpcGreeter* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

* Start Visual Studio for Mac and select **File** > **New Project**.
* In the **Choose a template for your new project** dialog, select **Web and Console** > **App** > **gRPC Service** and select **Continue**.
* Select **.NET 6.0** for the target framework and select **Continue**.
* Name the project **GrpcGreeter**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.
* Select **Continue**.

### Run the service

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

- Press Ctrl+F5 to run without the debugger.

  Visual Studio displays the following dialog when a project is not yet configured to use SSL:

  Select **Yes** if you trust the IIS Express SSL certificate.

  The following dialog is displayed:

  Select **Yes** if you agree to trust the development certificate.

  For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

  Visual Studio:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* Press **Ctrl-F5** to run without the debugger.

  Visual Studio Code:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

- Select **Debug** > **Start Without Debugging** to launch the app.

  Visual Studio for Mac:

  * Starts [Kestrel](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel) server.

  * Launches a browser.

  * Navigates to `http://localhost:port`, such as `http://localhost:7042`.

    * *port*: A randomly assigned port number for the app.
    * `localhost`: The standard hostname for the local computer. Localhost only serves web requests from the local computer.

  Visual Studio for Mac displays the following popup:

  Select **Install and Trust** if you trust the development certificate.

  The following dialog is displayed:

  Enter your password and select **Update Settings**

  Select **Yes** if you agree to trust the development certificate.

  See [Trust the ASP.NET Core HTTPS development certificate](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos) for more information.

- From Visual Studio, press **Opt-Cmd-Return** to run without the debugger. Alternatively, navigate to the menu bar and go to **Run>Start Without Debugging**.

  Visual Studio starts [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0), launches a browser, and navigates to a randomly assigned port such as `http://localhost:7042`.

The logs show the service listening on `https://localhost:<port>`, where `<port>` is the localhost port number randomly assigned when the project is created and set in `Properties/launchSettings.json`.

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

Note

The gRPC template is configured to use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246). gRPC clients need to use HTTPS to call the server. The gRPC service localhost port number is randomly assigned when the project is created and set in the *Properties\launchSettings.json* file of the gRPC service project.

macOS doesn't support ASP.NET Core gRPC with TLS. Additional configuration is required to successfully run gRPC services on macOS. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](../../grpc/troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

### Examine the project files

*GrpcGreeter* project files:

* `Protos/greet.proto`: defines the `Greeter` gRPC and is used to generate the gRPC server assets. For more information, see [Introduction to gRPC](../../grpc/?view=aspnetcore-10.0).

* `Services` folder: Contains the implementation of the `Greeter` service.

* `appSettings.json`: Contains configuration data such as the protocol used by Kestrel. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

* `Program.cs`, which contains:

  * The entry point for the gRPC service. For more information, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0).
  * Code that configures app behavior. For more information, see [App startup](../../fundamentals/startup?view=aspnetcore-10.0).

## Create the gRPC client in a .NET console app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

- Open a second instance of Visual Studio and select **Create a new project**.
- In the **Create a new project** dialog, select **Console Application**, and select **Next**.
- In the **Project name** text box, enter **GrpcGreeterClient** and select **Next**.
- In the **Additional information** dialog, select **.NET 6.0 (Long-term support)** and then select **Create**.

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to a folder for the project.

* Run the following commands:

  ```
  dotnet new console -o GrpcGreeterClient
  code -r GrpcGreeterClient
  ```

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Follow the instructions in [Building a complete .NET solution on macOS using Visual Studio for Mac](/en-us/dotnet/core/tutorials/using-on-mac-vs-full-solution) to create a console app with the name *GrpcGreeterClient*.

### Add required NuGet packages

The gRPC client project requires the following NuGet packages:

* [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client), which contains the .NET client.
* [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/), which contains protobuf message APIs for C#.
* [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), which contain C# tooling support for protobuf files. The tooling package isn't required at runtime, so the dependency is marked with `PrivateAssets="All"`.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)

Install the packages using either the Package Manager Console (PMC) or Manage NuGet Packages.

#### PMC option to install packages

* From Visual Studio, select **Tools** > **NuGet Package Manager** > **Package Manager Console**

* From the **Package Manager Console** window, run `cd GrpcGreeterClient` to change directories to the folder containing the `GrpcGreeterClient.csproj` files.

* Run the following commands:

  ```
  Install-Package Grpc.Net.Client
  Install-Package Google.Protobuf
  Install-Package Grpc.Tools
  ```

#### Manage NuGet Packages option to install packages

* Right-click the project in **Solution Explorer** > **Manage NuGet Packages**.
* Select the **Browse** tab.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the **Browse** tab and select **Install**.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

Run the following commands from the **Integrated Terminal**:

```
dotnet add GrpcGreeterClient.csproj package Grpc.Net.Client
dotnet add GrpcGreeterClient.csproj package Google.Protobuf
dotnet add GrpcGreeterClient.csproj package Grpc.Tools
```

* Right-click **GrpcGreeterClient** project in the **Solution Pad** and select **Manage NuGet Packages**.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the results pane and select **Add Package**.
* In **Select Projects** select **OK**.
* If the **License Acceptance** dialog appears, select **Accept** if you agree to the license terms.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

### Add greet.proto

* Create a *Protos* folder in the gRPC client project.

* Copy the *Protos\greet.proto* file from the gRPC Greeter service to the *Protos* folder in the gRPC client project.

* Update the namespace inside the `greet.proto` file to the project's namespace:

  ```
  option csharp_namespace = "GrpcGreeterClient";
  ```

* Edit the `GrpcGreeterClient.csproj` project file:

- [Visual Studio](#tabpanel_5_visual-studio)
- [Visual Studio Code](#tabpanel_5_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_5_visual-studio-mac)

Right-click the project and select **Edit Project File**.

Select the `GrpcGreeterClient.csproj` file.

Right-click the project and select **Edit Project File**.

* Add an item group with a `<Protobuf>` element that refers to the *greet.proto* file:

  ```
  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>
  ```

### Create the Greeter client

* Build the client project to create the types in the `GrpcGreeterClient` namespace.

Note

The `GrpcGreeterClient` types are generated automatically by the build process. The tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) generates the following files based on the *greet.proto* file:

* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs`: The protocol buffer code which populates, serializes and retrieves the request and response message types.
* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs`: Contains the generated client classes.

For more information on the C# assets automatically generated by [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), see [gRPC services with C#: Generated C# assets](../../grpc/basics?view=aspnetcore-10.0#generated-c-assets).

* Update the gRPC client `Program.cs` file with the following code.

  ```
  using System.Threading.Tasks;
  using Grpc.Net.Client;
  using GrpcGreeterClient;

  // The port number must match the port of the gRPC server.
  using var channel = GrpcChannel.ForAddress("https://localhost:7042");
  var client = new Greeter.GreeterClient(channel);
  var reply = await client.SayHelloAsync(
                    new HelloRequest { Name = "GreeterClient" });
  Console.WriteLine("Greeting: " + reply.Message);
  Console.WriteLine("Press any key to exit...");
  Console.ReadKey();
  ```

* In the preceding highlighted code, replace the localhost port number `7042` with the `HTTPS` port number specified in `Properties/launchSettings.json` within the `GrpcGreeter` service project.

`Program.cs` contains the entry point and logic for the gRPC client.

The Greeter client is created by:

* Instantiating a `GrpcChannel` containing the information for creating the connection to the gRPC service.
* Using the `GrpcChannel` to construct the Greeter client:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

The Greeter client calls the asynchronous `SayHello` method. The result of the `SayHello` call is displayed:

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

## Test the gRPC client with the gRPC Greeter service

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_6_visual-studio-mac)

- In the Greeter service, press `Ctrl+F5` to start the server without the debugger.
- In the `GrpcGreeterClient` project, press `Ctrl+F5` to start the client without the debugger.

* Start the Greeter service.
* Start the client.

- Due to the previously mentioned [HTTP/2 TLS issue on macOS workaround](../../grpc/troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos), you'll need to update the channel address in the client to match port in launchSetting.json of the GrpcGreeter service "http\://localhost:5000". Update line 13 of *`GrpcGreeterClient/Program.cs`* to read:

  ```
  using var channel = GrpcChannel.ForAddress("http://localhost:5000");
  ```

- Start the Greeter service.

- Start the client.

The client sends a greeting to the service with a message containing its name, *GreeterClient*. The service sends the message "Hello GreeterClient" as a response. The "Hello GreeterClient" response is displayed in the command prompt:

```
Greeting: Hello GreeterClient
Press any key to exit...
```

The gRPC service records the details of the successful call in the logs written to the command prompt:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpc-start\samples\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:<port>/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 78.32260000000001ms 200 application/grpc
```

Update the `appsettings.Development.json` file by adding the following lines:

```
"Microsoft.AspNetCore.Hosting": "Information",
"Microsoft.AspNetCore.Routing.EndpointMiddleware": "Information"
```

Note

The code in this article requires the ASP.NET Core HTTPS development certificate to secure the gRPC service. If the .NET gRPC client fails with the message `The remote certificate is invalid according to the validation procedure.` or `The SSL connection could not be established.`, the development certificate isn't trusted. To fix this issue, see [Call a gRPC service with an untrusted/invalid certificate](../../grpc/troubleshoot?view=aspnetcore-10.0#call-a-grpc-service-with-an-untrustedinvalid-certificate).

### Next steps

* View or download [the completed sample code for this tutorial](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/samples/6) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).
* [Overview for gRPC on .NET](../../grpc/?view=aspnetcore-10.0)
* [gRPC Services with C#](../../grpc/basics?view=aspnetcore-10.0)
* [Migrate gRPC from C-core to gRPC for .NET](../../grpc/migration?view=aspnetcore-10.0)

This tutorial shows how to create a .NET [gRPC](../../grpc/?view=aspnetcore-10.0) client and an ASP.NET Core gRPC Server.

At the end, you'll have a gRPC client that communicates with the gRPC Greeter service.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

In this tutorial, you:

* Create a gRPC Server.
* Create a gRPC client.
* Test the gRPC client with the gRPC Greeter service.

## Prerequisites

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

- [Visual Studio 2019 16.8 or later](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the **ASP.NET and web development** workload
- [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use something other than Visual Studio Code.

* [Visual Studio for Mac version 8.7 or later](/en-us/visualstudio/releasenotes/vs2019-mac-relnotes)
* [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)

## Create a gRPC service

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- Start Visual Studio and select **Create a new project**.
- In the **Create a new project** dialog, select **gRPC Service** and select **Next**.
- In the **Configure your new project** dialog, enter `GrpcGreeter` for **Project name**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.
- Select **Next**.
- In the **Additional information** dialog, select **.NET 5.0** in the **Target Framework** dropdown.
- Select **Create**.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new grpc -o GrpcGreeter
  code -r GrpcGreeter
  ```

  The `dotnet new` command creates a new gRPC service in the *GrpcGreeter* folder.

  The `code` command opens the *GrpcGreeter* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

* Start Visual Studio for Mac and select **New**. Alternatively, from the Visual Studio **File** menu, select **New Solution**.
* In the **Choose a template for your new project** dialog, select **Web and Console** > **App** > **gRPC Service** and select **Next**.
* Select **.NET 5.0** for the target framework and select **Next**.
* Name the project **GrpcGreeter**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.
* Select **Create**.

### Run the service

  ```
  dotnet dev-certs https --trust
  ```

The logs show the service listening on `https://localhost:5001`.

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

Note

The gRPC template is configured to use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246). gRPC clients need to use HTTPS to call the server.

macOS doesn't support ASP.NET Core gRPC with TLS. Additional configuration is required to successfully run gRPC services on macOS. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](../../grpc/troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

### Examine the project files

*GrpcGreeter* project files:

* *greet.proto*: The *Protos/greet.proto* file defines the `Greeter` gRPC and is used to generate the gRPC server assets. For more information, see [Introduction to gRPC](../../grpc/?view=aspnetcore-10.0).
* *Services* folder: Contains the implementation of the `Greeter` service.
* `appsettings.json`: Contains configuration data, such as protocol used by Kestrel. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).
* `Program.cs`: Contains the entry point for the gRPC service. For more information, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0).
* `Startup.cs`: Contains code that configures app behavior. For more information, see [App startup](../../fundamentals/startup?view=aspnetcore-10.0).

## Create the gRPC client in a .NET console app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

- Open a second instance of Visual Studio and select **Create a new project**.
- In the **Create a new project** dialog, select **Console App (.NET)** and select **Next**.
- In the **Project name** text box, enter **GrpcGreeterClient** and select **Create**.

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to a folder for the project.

* Run the following commands:

  ```
  dotnet new console -o GrpcGreeterClient
  code -r GrpcGreeterClient
  ```

* Visual Studio Code requests that you add assets to build and debug the project, select **Yes**.

  Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Follow the instructions in [Building a complete .NET solution on macOS using Visual Studio for Mac](/en-us/dotnet/core/tutorials/using-on-mac-vs-full-solution) to create a console app with the name *GrpcGreeterClient*.

### Add required packages

The gRPC client project requires the following packages:

* [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client), which contains the .NET client.
* [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/), which contains protobuf message APIs for C#.
* [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), which contains C# tooling support for protobuf files. The tooling package isn't required at runtime, so the dependency is marked with `PrivateAssets="All"`.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)

Install the packages using either the Package Manager Console (PMC) or Manage NuGet Packages.

#### PMC option to install packages

* From Visual Studio, select **Tools** > **NuGet Package Manager** > **Package Manager Console**

* From the **Package Manager Console** window, run `cd GrpcGreeterClient` to change directories to the folder containing the `GrpcGreeterClient.csproj` files.

* Run the following commands:

  ```
  Install-Package Grpc.Net.Client
  Install-Package Google.Protobuf
  Install-Package Grpc.Tools
  ```

#### Manage NuGet Packages option to install packages

* Right-click the project in **Solution Explorer** > **Manage NuGet Packages**.
* Select the **Browse** tab.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the **Browse** tab and select **Install**.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

Run the following commands from the **Integrated Terminal**:

```
dotnet add GrpcGreeterClient.csproj package Grpc.Net.Client
dotnet add GrpcGreeterClient.csproj package Google.Protobuf
dotnet add GrpcGreeterClient.csproj package Grpc.Tools
```

* Right-click **GrpcGreeterClient** project in the **Solution Pad** and select **Manage NuGet Packages**.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the results pane and select **Add Package**.
* Select the **Accept** button on the **Accept License** dialog.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

### Add greet.proto

* Create a *Protos* folder in the gRPC client project.

* Copy the *Protos\greet.proto* file from the gRPC Greeter service to the *Protos* folder in the gRPC client project.

* Update the namespace inside the `greet.proto` file to the project's namespace:

  ```
  option csharp_namespace = "GrpcGreeterClient";
  ```

* Edit the `GrpcGreeterClient.csproj` project file:

  * [Visual Studio](#tabpanel_5_visual-studio)
  * [Visual Studio Code](#tabpanel_5_visual-studio-code)
  * [Visual Studio for Mac](#tabpanel_5_visual-studio-mac)

  Right-click the project and select **Edit Project File**.

  Select the `GrpcGreeterClient.csproj` file.

  Right-click the project and select **Edit Project File**.

***

* Add an item group with a `<Protobuf>` element that refers to the *greet.proto* file:

  ```
  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>
  ```

### Create the Greeter client

* Build the client project to create the types in the `GrpcGreeterClient` namespace.

Note

The `GrpcGreeterClient` types are generated automatically by the build process. The tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) generates the following files based on the *greet.proto* file:

* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs`: The protocol buffer code which populates, serializes and retrieves the request and response message types.
* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs`: Contains the generated client classes.

For more information on the C# assets automatically generated by [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), see [gRPC services with C#: Generated C# assets](../../grpc/basics?view=aspnetcore-10.0#generated-c-assets).

* Update the gRPC client `Program.cs` file with the following code:

  ```
  using System;
  using System.Net.Http;
  using System.Threading.Tasks;
  using Grpc.Net.Client;

  namespace GrpcGreeterClient
  {
      class Program
      {
          static async Task Main(string[] args)
          {
              // The port number(5001) must match the port of the gRPC server.
              using var channel = GrpcChannel.ForAddress("https://localhost:5001");
              var client = new Greeter.GreeterClient(channel);
              var reply = await client.SayHelloAsync(
                                new HelloRequest { Name = "GreeterClient" });
              Console.WriteLine("Greeting: " + reply.Message);
              Console.WriteLine("Press any key to exit...");
              Console.ReadKey();
          }
      }
  }
  ```

`Program.cs` contains the entry point and logic for the gRPC client.

The Greeter client is created by:

* Instantiating a `GrpcChannel` containing the information for creating the connection to the gRPC service.
* Using the `GrpcChannel` to construct the Greeter client:

```
static async Task Main(string[] args)
{
    // The port number(5001) must match the port of the gRPC server.
    using var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greeter.GreeterClient(channel);
    var reply = await client.SayHelloAsync(
                      new HelloRequest { Name = "GreeterClient" });
    Console.WriteLine("Greeting: " + reply.Message);
    Console.WriteLine("Press any key to exit...");
    Console.ReadKey();
}
```

The Greeter client calls the asynchronous `SayHello` method. The result of the `SayHello` call is displayed:

```
static async Task Main(string[] args)
{
    // The port number(5001) must match the port of the gRPC server.
    using var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greeter.GreeterClient(channel);
    var reply = await client.SayHelloAsync(
                      new HelloRequest { Name = "GreeterClient" });
    Console.WriteLine("Greeting: " + reply.Message);
    Console.WriteLine("Press any key to exit...");
    Console.ReadKey();
}
```

## Test the gRPC client with the gRPC Greeter service

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_6_visual-studio-mac)

- In the Greeter service, press `Ctrl+F5` to start the server without the debugger.
- In the `GrpcGreeterClient` project, press `Ctrl+F5` to start the client without the debugger.

* Start the Greeter service.
* Start the client.

- Due to the previously mentioned [HTTP/2 TLS issue on macOS workaround](../../grpc/troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos), you'll need to update the channel address in the client to "http\://localhost:5000". Update line 13 of *`GrpcGreeterClient/Program.cs`* to read:

  ```
  using var channel = GrpcChannel.ForAddress("http://localhost:5000");
  ```

- Start the Greeter service.

- Start the client.

The client sends a greeting to the service with a message containing its name, *GreeterClient*. The service sends the message "Hello GreeterClient" as a response. The "Hello GreeterClient" response is displayed in the command prompt:

```
Greeting: Hello GreeterClient
Press any key to exit...
```

The gRPC service records the details of the successful call in the logs written to the command prompt:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpc-start\samples\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:5001/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 78.32260000000001ms 200 application/grpc
```

Note

The code in this article requires the ASP.NET Core HTTPS development certificate to secure the gRPC service. If the .NET gRPC client fails with the message `The remote certificate is invalid according to the validation procedure.` or `The SSL connection could not be established.`, the development certificate isn't trusted. To fix this issue, see [Call a gRPC service with an untrusted/invalid certificate](../../grpc/troubleshoot?view=aspnetcore-10.0#call-a-grpc-service-with-an-untrustedinvalid-certificate).

### Next steps

* [Overview for gRPC on .NET](../../grpc/?view=aspnetcore-10.0)
* [gRPC Services with C#](../../grpc/basics?view=aspnetcore-10.0)
* [Migrate gRPC from C-core to gRPC for .NET](../../grpc/migration?view=aspnetcore-10.0)

This tutorial shows how to create a .NET Core [gRPC](../../grpc/?view=aspnetcore-10.0) client and an ASP.NET Core gRPC Server.

At the end, you'll have a gRPC client that communicates with the gRPC Greeter service.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

In this tutorial, you:

* Create a gRPC Server.
* Create a gRPC client.
* Test the gRPC client with the gRPC Greeter service.

## Prerequisites

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

- [Visual Studio 2019 16.4 or later](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the **ASP.NET and web development** workload
- [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on any platform (macOS, Linux, or Windows) and with any code editor. Minor changes may be required if you use something other than Visual Studio Code. For more information on installing Visual Studio Code on macOS, see [Visual Studio Code on macOS](https://code.visualstudio.com/docs/setup/mac).

* [Visual Studio for Mac version 8.7 or later](/en-us/visualstudio/releasenotes/vs2019-mac-relnotes)
* [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)

## Create a gRPC service

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- Start Visual Studio and select **Create a new project**. Alternatively, from the Visual Studio **File** menu, select **New** > **Project**.

- In the **Create a new project** dialog, select **gRPC Service** and select **Next**:

- Name the project **GrpcGreeter**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.

- Select **Create**.

- In the **Create a new gRPC service** dialog:

  * The **gRPC Service** template is selected.
  * Select **Create**.

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to a folder for the project.

* Run the following commands:

  ```
  dotnet new grpc -o GrpcGreeter
  code -r GrpcGreeter
  ```

  * The `dotnet new` command creates a new gRPC service in the *GrpcGreeter* folder.
  * The `code` command opens the *GrpcGreeter* folder in a new instance of Visual Studio Code.

  A dialog box appears with **Required assets to build and debug are missing from 'GrpcGreeter'. Add them?**

* Select **Yes**.

- Start Visual Studio for Mac and select **Create a new project**. Alternatively, from the Visual Studio **File** menu, select **New** > **Project**.

- In the **Create a new project** dialog, select **Web and Console** > **App** > **gRPC Service** and select **Next**:

- Select **.NET Core 3.1** for the target framework and select **Next**.

- Name the project **GrpcGreeter**. It's important to name the project *GrpcGreeter* so the namespaces match when you copy and paste code.

- Select **Create**.

### Run the service

  ```
  dotnet dev-certs https --trust
  ```

The logs show the service listening on `https://localhost:5001`.

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

Note

The gRPC template is configured to use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246). gRPC clients need to use HTTPS to call the server.

macOS doesn't support ASP.NET Core gRPC with TLS. Additional configuration is required to successfully run gRPC services on macOS. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](../../grpc/troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

### Examine the project files

*GrpcGreeter* project files:

* *greet.proto*: The *Protos/greet.proto* file defines the `Greeter` gRPC and is used to generate the gRPC server assets. For more information, see [Introduction to gRPC](../../grpc/?view=aspnetcore-10.0).
* *Services* folder: Contains the implementation of the `Greeter` service.
* `appsettings.json`: Contains configuration data, such as protocol used by Kestrel. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).
* `Program.cs`: Contains the entry point for the gRPC service. For more information, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0).
* `Startup.cs`: Contains code that configures app behavior. For more information, see [App startup](../../fundamentals/startup?view=aspnetcore-10.0).

## Create the gRPC client in a .NET console app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

- Open a second instance of Visual Studio and select **Create a new project**.
- In the **Create a new project** dialog, select **Console App (.NET Core)** and select **Next**.
- In the **Project name** text box, enter **GrpcGreeterClient** and select **Create**.

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to a folder for the project.

* Run the following commands:

  ```
  dotnet new console -o GrpcGreeterClient
  code -r GrpcGreeterClient
  ```

Follow the instructions in [Building a complete .NET Core solution on macOS using Visual Studio for Mac](/en-us/dotnet/core/tutorials/using-on-mac-vs-full-solution) to create a console app with the name *GrpcGreeterClient*.

### Add required packages

The gRPC client project requires the following packages:

* [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client), which contains the .NET Core client.
* [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/), which contains protobuf message APIs for C#.
* [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), which contains C# tooling support for protobuf files. The tooling package isn't required at runtime, so the dependency is marked with `PrivateAssets="All"`.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)

Install the packages using either the Package Manager Console (PMC) or Manage NuGet Packages.

#### PMC option to install packages

* From Visual Studio, select **Tools** > **NuGet Package Manager** > **Package Manager Console**

* From the **Package Manager Console** window, run `cd GrpcGreeterClient` to change directories to the folder containing the `GrpcGreeterClient.csproj` files.

* Run the following commands:

  ```
  Install-Package Grpc.Net.Client
  Install-Package Google.Protobuf
  Install-Package Grpc.Tools
  ```

#### Manage NuGet Packages option to install packages

* Right-click the project in **Solution Explorer** > **Manage NuGet Packages**.
* Select the **Browse** tab.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the **Browse** tab and select **Install**.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

Run the following commands from the **Integrated Terminal**:

```
dotnet add GrpcGreeterClient.csproj package Grpc.Net.Client
dotnet add GrpcGreeterClient.csproj package Google.Protobuf
dotnet add GrpcGreeterClient.csproj package Grpc.Tools
```

* Right-click **GrpcGreeterClient** project in the **Solution Pad** and select **Manage NuGet Packages**.
* Enter **Grpc.Net.Client** in the search box.
* Select the **Grpc.Net.Client** package from the results pane and select **Add Package**.
* Select the **Accept** button on the **Accept License** dialog.
* Repeat for `Google.Protobuf` and `Grpc.Tools`.

### Add greet.proto

* Create a *Protos* folder in the gRPC client project.

* Copy the *Protos\greet.proto* file from the gRPC Greeter service to the *Protos* folder in the gRPC client project.

* Update the namespace inside the `greet.proto` file to the project's namespace:

  ```
  option csharp_namespace = "GrpcGreeterClient";
  ```

* Edit the `GrpcGreeterClient.csproj` project file:

  * [Visual Studio](#tabpanel_5_visual-studio)
  * [Visual Studio Code](#tabpanel_5_visual-studio-code)
  * [Visual Studio for Mac](#tabpanel_5_visual-studio-mac)

  Right-click the project and select **Edit Project File**.

  Select the `GrpcGreeterClient.csproj` file.

  Right-click the project and select **Edit Project File**.

***

* Add an item group with a `<Protobuf>` element that refers to the *greet.proto* file:

  ```
  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>
  ```

### Create the Greeter client

* Build the client project to create the types in the `GrpcGreeterClient` namespace.

Note

The `GrpcGreeterClient` types are generated automatically by the build process. The tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) generates the following files based on the *greet.proto* file:

* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs`: The protocol buffer code which populates, serializes and retrieves the request and response message types.
* `GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs`: Contains the generated client classes.

For more information on the C# assets automatically generated by [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/), see [gRPC services with C#: Generated C# assets](../../grpc/basics?view=aspnetcore-10.0#generated-c-assets).

Update the gRPC client `Program.cs` file with the following code:

```
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Grpc.Net.Client;

namespace GrpcGreeterClient
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // The port number(5001) must match the port of the gRPC server.
            using var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = new Greeter.GreeterClient(channel);
            var reply = await client.SayHelloAsync(
                              new HelloRequest { Name = "GreeterClient" });
            Console.WriteLine("Greeting: " + reply.Message);
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }
}
```

`Program.cs` contains the entry point and logic for the gRPC client.

The Greeter client is created by:

* Instantiating a `GrpcChannel` containing the information for creating the connection to the gRPC service.
* Using the `GrpcChannel` to construct the Greeter client:

```
static async Task Main(string[] args)
{
    // The port number(5001) must match the port of the gRPC server.
    using var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greeter.GreeterClient(channel);
    var reply = await client.SayHelloAsync(
                      new HelloRequest { Name = "GreeterClient" });
    Console.WriteLine("Greeting: " + reply.Message);
    Console.WriteLine("Press any key to exit...");
    Console.ReadKey();
}
```

The Greeter client calls the asynchronous `SayHello` method. The result of the `SayHello` call is displayed:

```
static async Task Main(string[] args)
{
    // The port number(5001) must match the port of the gRPC server.
    using var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greeter.GreeterClient(channel);
    var reply = await client.SayHelloAsync(
                      new HelloRequest { Name = "GreeterClient" });
    Console.WriteLine("Greeting: " + reply.Message);
    Console.WriteLine("Press any key to exit...");
    Console.ReadKey();
}
```

## Test the gRPC client with the gRPC Greeter service

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_6_visual-studio-mac)

- In the Greeter service, press `Ctrl+F5` to start the server without the debugger.
- In the `GrpcGreeterClient` project, press `Ctrl+F5` to start the client without the debugger.

* Start the Greeter service.
* Start the client.

- Due to the previously mentioned [HTTP/2 TLS issue on macOS workaround](../../grpc/troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos), you'll need to update the channel address in the client to "http\://localhost:5000". Update line 13 of *`GrpcGreeterClient/Program.cs`* to read:

  ```
  using var channel = GrpcChannel.ForAddress("http://localhost:5000");
  ```

- Start the Greeter service.

- Start the client.

The client sends a greeting to the service with a message containing its name, *GreeterClient*. The service sends the message "Hello GreeterClient" as a response. The "Hello GreeterClient" response is displayed in the command prompt:

```
Greeting: Hello GreeterClient
Press any key to exit...
```

The gRPC service records the details of the successful call in the logs written to the command prompt:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpc-start\samples\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:5001/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 78.32260000000001ms 200 application/grpc
```

Note

The code in this article requires the ASP.NET Core HTTPS development certificate to secure the gRPC service. If the .NET gRPC client fails with the message `The remote certificate is invalid according to the validation procedure.` or `The SSL connection could not be established.`, the development certificate isn't trusted. To fix this issue, see [Call a gRPC service with an untrusted/invalid certificate](../../grpc/troubleshoot?view=aspnetcore-10.0#call-a-grpc-service-with-an-untrustedinvalid-certificate).

### Next steps

* [Overview for gRPC on .NET](../../grpc/?view=aspnetcore-10.0)
* [gRPC Services with C#](../../grpc/basics?view=aspnetcore-10.0)
* [Migrate gRPC from C-core to gRPC for .NET](../../grpc/migration?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/80-90
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/80-to-90.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core in .NET 8 to ASP.NET Core in .NET 9

This article explains how to update an ASP.NET Core in .NET 8 to ASP.NET Core in .NET 9.

## Update the .NET SDK version in `global.json`

If you rely on a [`global.json`](/en-us/dotnet/core/tools/global-json) file to target a specific .NET SDK version, update the `version` property to the .NET 9 SDK version that's installed. For example:

```
{
  "sdk": {
-    "version": "8.0.100"
+    "version": "9.0.100"
  }
}
```

## Update the target framework

Update the project file's [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks) to `net9.0`:

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

  <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>

</Project>
```

## Update package references

In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.org/packages?q=Microsoft.AspNetCore.*), [`Microsoft.EntityFrameworkCore.*`](https://www.nuget.org/packages?q=Microsoft.EntityFrameworkCore.*), [`Microsoft.Extensions.*`](https://www.nuget.org/packages?q=Microsoft.Extensions.*), and [`System.Net.Http.Json`](https://www.nuget.org/packages/System.Net.Http.Json) package reference's `Version` attribute to 9.0.0 or later. For example:

```
<ItemGroup>
-   <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="8.0.2" />
-   <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2" />
-   <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
-   <PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
+   <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="9.0.0" />
+   <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
+   <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.0" />
+   <PackageReference Include="System.Net.Http.Json" Version="9.0.0" />
</ItemGroup>
```

## Replace `UseStaticFiles` with `MapStaticAssets`

Optimize the handling of static files in your web apps by replacing [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) with [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) in the app's `Program` file:

```
- app.UseStaticFiles();
+ app.MapStaticAssets();
```

In MVC & Razor Pages apps, you additionally need to chain a call to `.WithStaticAssets` after `MapRazorPages` or `MapControllerRoute` in `Program.cs`. For an example, see the [Serve static files in ASP.NET Core apps](../fundamentals/static-files?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-9.0).

ASP.NET Core automatically fingerprints and precompresses your static files at build and publish time, and then [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) surfaces the optimized files as endpoints using endpoint routing with appropriate caching headers.

To resolve the fingerprinted file names from your app:

* In Blazor apps, use the [ComponentBase.Assets](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.assets#microsoft-aspnetcore-components-componentbase-assets) property. Update explicit references to static assets in Razor component files (`.razor`) to use `@Assets["{ASSET PATH}"]`, where the `{ASSET PATH}` placeholder is the path to the asset. Note that this should ***NOT*** be done for the Blazor framework scripts (`blazor.*.js`). In the following example, Bootstrap, the Blazor project template app stylesheet (`app.css`), and the [CSS isolation stylesheet](../blazor/components/css-isolation?view=aspnetcore-10.0) (based on an app's namespace of `BlazorSample`) are linked in a root component, typically the `App` component (`Components/App.razor`):

  ```
  <link rel="stylesheet" href="@Assets["bootstrap/bootstrap.min.css"]" />
  <link rel="stylesheet" href="@Assets["app.css"]" />
  <link rel="stylesheet" href="@Assets["BlazorSample.styles.css"]" />
  ```

* In MVC & Razor Pages apps, the script and link tag helpers will automatically resolve the fingerprinted file names.

To resolve the fingerprinted file names when importing JavaScript modules, add a generated [import map](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/script/type/importmap):

* In Blazor apps, add the ([ImportMap](/en-us/dotnet/api/microsoft.aspnetcore.components.importmap)) component to the `<head>` content of the app's root component, typically in the `App` component (`App.razor`):

  ```
  <ImportMap />
  ```

* In MVC & Razor pages apps, add `<script type="importmap"></script>` to the head of the main layout file, which is updated by the Import Map Tag Helper.

For more information, see the following resources:

* [What's new in ASP.NET Core in .NET 9](../release-notes/aspnetcore-9.0?view=aspnetcore-10.0#static-asset-delivery-optimization)
* [ASP.NET Core Blazor static files](../blazor/fundamentals/static-files?view=aspnetcore-10.0)

## Blazor

### Adopt simplified authentication state serialization for Blazor Web Apps

Blazor Web Apps can optionally adopt [simplified authentication state serialization](../release-notes/aspnetcore-9.0?view=aspnetcore-10.0#simplified-authentication-state-serialization-for-blazor-web-apps).

In the server project:

* Remove the Persisting Authentication State Provider (`PersistingAuthenticationStateProvider.cs`).

* Remove the service registration from the `Program` file. Instead, chain a call to [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization) on [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents):

  ```
  - builder.Services.AddScoped<AuthenticationStateProvider, PersistingAuthenticationStateProvider>();

    builder.Services.AddRazorComponents()
        .AddInteractiveServerComponents()
        .AddInteractiveWebAssemblyComponents()
  +     .AddAuthenticationStateSerialization();
  ```

The API only serializes the server-side name and role claims for access in the browser. To include all claims, set [SerializeAllClaims](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.server.authenticationstateserializationoptions.serializeallclaims#microsoft-aspnetcore-components-webassembly-server-authenticationstateserializationoptions-serializeallclaims) to `true`:

```
.AddAuthenticationStateSerialization(options => options.SerializeAllClaims = true);
```

In the client project (`.Client`):

* Remove the Persistent Authentication State Provider (`PersistentAuthenticationStateProvider.cs`).

* Remove the service registration from the `Program` file. Instead, call [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization) on the service collection:

  ```
  - builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
  + builder.Services.AddAuthenticationStateDeserialization();
  ```

For more information, see [What's new in ASP.NET Core in .NET 9](../release-notes/aspnetcore-9.0?view=aspnetcore-10.0#simplified-authentication-state-serialization-for-blazor-web-apps).

### Streaming rendering attribute no longer requires the `true` parameter

In .NET 8, [streaming rendering](../blazor/components/rendering?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#streaming-rendering) required you to pass `true` for the `enabled` parameter:

```
@attribute [StreamRendering(true)]
```

In .NET 9 or later, `true` can optionally be removed, as `true` is now the default for the `enabled` parameter:

```
@attribute [StreamRendering]
```

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/extensibility
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/middleware/extensibility.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Factory-based middleware activation in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory)/[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) is an extensibility point for [middleware](./?view=aspnetcore-10.0) activation that offers the following benefits:

* Activation per client request (injection of scoped services)
* Strong typing of middleware

[UseMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.builder.usemiddlewareextensions.usemiddleware) extension methods check if a middleware's registered type implements [IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware). If it does, the [IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) instance registered in the container is used to resolve the [IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) implementation instead of using the convention-based middleware activation logic. The middleware is registered as a [scoped or transient service](../dependency-injection?view=aspnetcore-10.0#service-lifetimes) in the app's service container.

[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) is activated per client request (connection), so scoped services can be injected into the middleware's constructor.

## IMiddleware

[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) defines middleware for the app's request pipeline. The [InvokeAsync(HttpContext, RequestDelegate)](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware.invokeasync) method handles requests and returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) that represents the execution of the middleware.

Middleware activated by convention:

```
public class ConventionalMiddleware
{
    private readonly RequestDelegate _next;

    public ConventionalMiddleware(RequestDelegate next)
        => _next = next;

    public async Task InvokeAsync(HttpContext context, SampleDbContext dbContext)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            dbContext.Requests.Add(new Request("Conventional", keyValue));

            await dbContext.SaveChangesAsync();
        }

        await _next(context);
    }
}
```

Middleware activated by [MiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.middlewarefactory):

```
public class FactoryActivatedMiddleware : IMiddleware
{
    private readonly SampleDbContext _dbContext;

    public FactoryActivatedMiddleware(SampleDbContext dbContext)
        => _dbContext = dbContext;

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            _dbContext.Requests.Add(new Request("Factory", keyValue));

            await _dbContext.SaveChangesAsync();
        }

        await next(context);
    }
}
```

Extensions are created for the middleware:

```
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseConventionalMiddleware(
        this IApplicationBuilder app)
        => app.UseMiddleware<ConventionalMiddleware>();

    public static IApplicationBuilder UseFactoryActivatedMiddleware(
        this IApplicationBuilder app)
        => app.UseMiddleware<FactoryActivatedMiddleware>();
}
```

It isn't possible to pass objects to the factory-activated middleware with [UseMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.builder.usemiddlewareextensions.usemiddleware):

```
public static IApplicationBuilder UseFactoryActivatedMiddleware(
    this IApplicationBuilder app, bool option)
{
    // Passing 'option' as an argument throws a NotSupportedException at runtime.
    return app.UseMiddleware<FactoryActivatedMiddleware>(option);
}
```

The factory-activated middleware is added to the built-in container in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<SampleDbContext>
    (options => options.UseInMemoryDatabase("SampleDb"));

builder.Services.AddTransient<FactoryActivatedMiddleware>();
```

Both middleware are registered in the request processing pipeline, also in `Program.cs`:

```
var app = builder.Build();

app.UseConventionalMiddleware();
app.UseFactoryActivatedMiddleware();
```

## IMiddlewareFactory

[IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) provides methods to create middleware. The middleware factory implementation is registered in the container as a scoped service.

The default [IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) implementation, [MiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.middlewarefactory), is found in the [Microsoft.AspNetCore.Http](https://www.nuget.org/packages/Microsoft.AspNetCore.Http/) package.

[IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory)/[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) is an extensibility point for [middleware](./?view=aspnetcore-10.0) activation.

[UseMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.builder.usemiddlewareextensions.usemiddleware) extension methods check if a middleware's registered type implements [IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware). If it does, the [IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) instance registered in the container is used to resolve the [IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) implementation instead of using the convention-based middleware activation logic. The middleware is registered as a [scoped or transient service](../dependency-injection?view=aspnetcore-10.0#service-lifetimes) in the app's service container.

Benefits:

* Activation per client request (injection of scoped services)
* Strong typing of middleware

[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) is activated per client request (connection), so scoped services can be injected into the middleware's constructor.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/middleware/extensibility/samples) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## IMiddleware

[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) defines middleware for the app's request pipeline. The [InvokeAsync(HttpContext, RequestDelegate)](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware.invokeasync) method handles requests and returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) that represents the execution of the middleware.

Middleware activated by convention:

```
public class ConventionalMiddleware
{
    private readonly RequestDelegate _next;

    public ConventionalMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, AppDbContext db)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            db.Add(new Request()
                {
                    DT = DateTime.UtcNow, 
                    MiddlewareActivation = "ConventionalMiddleware", 
                    Value = keyValue
                });

            await db.SaveChangesAsync();
        }

        await _next(context);
    }
}
```

Middleware activated by [MiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.middlewarefactory):

```
public class FactoryActivatedMiddleware : IMiddleware
{
    private readonly AppDbContext _db;

    public FactoryActivatedMiddleware(AppDbContext db)
    {
        _db = db;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            _db.Add(new Request()
                {
                    DT = DateTime.UtcNow, 
                    MiddlewareActivation = "FactoryActivatedMiddleware", 
                    Value = keyValue
                });

            await _db.SaveChangesAsync();
        }

        await next(context);
    }
}
```

Extensions are created for the middleware:

```
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseConventionalMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ConventionalMiddleware>();
    }

    public static IApplicationBuilder UseFactoryActivatedMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<FactoryActivatedMiddleware>();
    }
}
```

It isn't possible to pass objects to the factory-activated middleware with [UseMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.builder.usemiddlewareextensions.usemiddleware):

```
public static IApplicationBuilder UseFactoryActivatedMiddleware(
    this IApplicationBuilder builder, bool option)
{
    // Passing 'option' as an argument throws a NotSupportedException at runtime.
    return builder.UseMiddleware<FactoryActivatedMiddleware>(option);
}
```

The factory-activated middleware is added to the built-in container in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options =>
        options.UseInMemoryDatabase("InMemoryDb"));

    services.AddTransient<FactoryActivatedMiddleware>();

    services.AddRazorPages();
}
```

Both middleware are registered in the request processing pipeline in `Startup.Configure`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseConventionalMiddleware();
    app.UseFactoryActivatedMiddleware();

    app.UseStaticFiles();
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

## IMiddlewareFactory

[IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) provides methods to create middleware. The middleware factory implementation is registered in the container as a scoped service.

The default [IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) implementation, [MiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.middlewarefactory), is found in the [Microsoft.AspNetCore.Http](https://www.nuget.org/packages/Microsoft.AspNetCore.Http/) package.

[IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory)/[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) is an extensibility point for [middleware](./?view=aspnetcore-10.0) activation.

[UseMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.builder.usemiddlewareextensions.usemiddleware) extension methods check if a middleware's registered type implements [IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware). If it does, the [IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) instance registered in the container is used to resolve the [IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) implementation instead of using the convention-based middleware activation logic. The middleware is registered as a [scoped or transient service](../dependency-injection?view=aspnetcore-10.0#service-lifetimes) in the app's service container.

Benefits:

* Activation per client request (injection of scoped services)
* Strong typing of middleware

[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) is activated per client request (connection), so scoped services can be injected into the middleware's constructor.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/middleware/extensibility/samples) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## IMiddleware

[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) defines middleware for the app's request pipeline. The [InvokeAsync(HttpContext, RequestDelegate)](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware.invokeasync) method handles requests and returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) that represents the execution of the middleware.

Middleware activated by convention:

```
public class ConventionalMiddleware
{
    private readonly RequestDelegate _next;

    public ConventionalMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, AppDbContext db)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            db.Add(new Request()
                {
                    DT = DateTime.UtcNow, 
                    MiddlewareActivation = "ConventionalMiddleware", 
                    Value = keyValue
                });

            await db.SaveChangesAsync();
        }

        await _next(context);
    }
}
```

Middleware activated by [MiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.middlewarefactory):

```
public class FactoryActivatedMiddleware : IMiddleware
{
    private readonly AppDbContext _db;

    public FactoryActivatedMiddleware(AppDbContext db)
    {
        _db = db;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            _db.Add(new Request()
                {
                    DT = DateTime.UtcNow, 
                    MiddlewareActivation = "FactoryActivatedMiddleware", 
                    Value = keyValue
                });

            await _db.SaveChangesAsync();
        }

        await next(context);
    }
}
```

Extensions are created for the middleware:

```
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseConventionalMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ConventionalMiddleware>();
    }

    public static IApplicationBuilder UseFactoryActivatedMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<FactoryActivatedMiddleware>();
    }
}
```

It isn't possible to pass objects to the factory-activated middleware with [UseMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.builder.usemiddlewareextensions.usemiddleware):

```
public static IApplicationBuilder UseFactoryActivatedMiddleware(
    this IApplicationBuilder builder, bool option)
{
    // Passing 'option' as an argument throws a NotSupportedException at runtime.
    return builder.UseMiddleware<FactoryActivatedMiddleware>(option);
}
```

The factory-activated middleware is added to the built-in container in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options =>
        options.UseInMemoryDatabase("InMemoryDb"));

    services.AddTransient<FactoryActivatedMiddleware>();

    services.AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
```

Both middleware are registered in the request processing pipeline in `Startup.Configure`:

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseConventionalMiddleware();
    app.UseFactoryActivatedMiddleware();

    app.UseStaticFiles();
    app.UseMvc();
}
```

## IMiddlewareFactory

[IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) provides methods to create middleware. The middleware factory implementation is registered in the container as a scoped service.

The default [IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) implementation, [MiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.middlewarefactory), is found in the [Microsoft.AspNetCore.Http](https://www.nuget.org/packages/Microsoft.AspNetCore.Http/) package.

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/middleware/extensibility/samples) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))
* [ASP.NET Core middleware](./?view=aspnetcore-10.0)
* [Middleware activation with a third-party container in ASP.NET Core](extensibility-third-party-container?view=aspnetcore-10.0)

- [ASP.NET Core middleware](./?view=aspnetcore-10.0)
- [Middleware activation with a third-party container in ASP.NET Core](extensibility-third-party-container?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/supported-platforms
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/supported-platforms.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC on .NET supported platforms

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

This article discusses the requirements and supported platforms for using gRPC with .NET. There are different requirements for the two major gRPC workloads:

* [Hosting gRPC services in ASP.NET Core](#aspnet-core-grpc-server-requirements)
* [Calling gRPC from .NET client apps](#net-grpc-client-requirements)

## Wire-formats

gRPC takes advantage of advanced features available in HTTP/2. HTTP/2 isn't supported everywhere, but a second wire-format using HTTP/1.1 is available for gRPC:

* [`application/grpc`](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) - gRPC over HTTP/2 is how gRPC is typically used.
* [`application/grpc-web`](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) - gRPC-Web modifies the gRPC protocol to be compatible with HTTP/1.1. gRPC-Web can be used in more places. gRPC-Web can be used by browser apps and in networks without complete support for HTTP/2. Two advanced gRPC features are no longer supported: client streaming and bidirectional streaming.

gRPC on .NET supports both wire-formats. `application/grpc` is used by default. gRPC-Web must be configured on the client and the server for successful gRPC-Web calls. For information on setting up gRPC-Web, see [gRPC-Web in ASP.NET Core gRPC apps](grpcweb?view=aspnetcore-10.0).

## ASP.NET Core gRPC server requirements

Hosting gRPC services with ASP.NET Core requires .NET Core 3.x or later.

* .NET 5 or later
* .NET Core 3

ASP.NET Core gRPC services can be hosted on all operating systems that .NET supports.

* Windows
* Linux
* macOS

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). For production hosting, use Windows or Linux server distributions.

* Windows
* Linux
* macOS†

†[macOS doesn't support hosting ASP.NET Core apps with HTTPS](troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). In addition, macOS [doesn't support hosting ASP.NET Core apps with HTTPS](troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos). For production hosting, use Windows or Linux server distributions.

### Supported ASP.NET Core servers

All built-in ASP.NET Core servers are supported.

* Kestrel
* TestServer
* IIS†
* HTTP.sys†

†Requires .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later.

For information about configuring ASP.NET Core servers to run gRPC, see [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0#server-options).

### Azure services

* [Azure Kubernetes Service (AKS)](https://azure.microsoft.com/services/kubernetes-service/)
* [Azure Container Apps](https://azure.microsoft.com/services/container-apps/)
* [Azure App Service](https://azure.microsoft.com/services/app-service/)†

†gRPC requires a Linux-based environment on Azure App Service. See [How-to deploy a .NET 6 gRPC app on App Service](https://github.com/Azure/app-service-linux-docs/blob/master/HowTo/gRPC/Linux/.NET/use_gRPC_with_dotnet.md) for Azure App Service deployment information.

## .NET gRPC client requirements

The [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client/) package supports gRPC calls over HTTP/2 on .NET Core 3 and .NET 5 or later.

Limited support is available for gRPC over HTTP/2 on .NET Framework. Other .NET versions such as UWP and Unity don't have required HTTP/2 support, and must use gRPC-Web instead.

The following table lists .NET implementations and their gRPC client support.

| .NET implementation                   | gRPC over HTTP/2 | gRPC-Web |
| ------------------------------------- | ---------------- | -------- |
| .NET 5 or later                       | ✔️               | ✔️       |
| .NET Core 3                           | ✔️               | ✔️       |
| .NET Core 2.1                         | ❌                | ✔️       |
| .NET Framework 4.6.1                  | ⚠️†              | ✔️       |
| Blazor WebAssembly                    | ❌                | ✔️       |
| Mono 5.4                              | ❌                | ✔️       |
| Universal Windows Platform 10.0.16299 | ❌                | ✔️       |
| Unity 2018.1                          | ❌                | ✔️       |

†.NET Framework requires configuration of [WinHttpHandler](/en-us/dotnet/api/system.net.http.winhttphandler) and Windows 11 or later, Windows Server 2019 or later. For more information, see [Make gRPC calls on .NET Framework](netstandard?view=aspnetcore-10.0#net-framework).

Using `Grpc.Net.Client` with gRPC-Web requires additional configuration. For more information:

* [Configure gRPC-Web with the .NET gRPC client](grpcweb?view=aspnetcore-10.0#configure-grpc-web-with-the-net-grpc-client)
* [Use gRPC client with .NET Standard 2.0](netstandard?view=aspnetcore-10.0)

Important

gRPC-Web requires the client ***and*** server to support it. gRPC-Web can be [quickly configured by an ASP.NET Core gRPC server](grpcweb?view=aspnetcore-10.0#configure-grpc-web-in-aspnet-core). Other gRPC server implementations require a proxy to support gRPC-Web.

## Related content

* [Use gRPC client with .NET Standard 2.0](netstandard?view=aspnetcore-10.0)
* [gRPC C# core-library](https://grpc.io/docs/languages/csharp/quickstart/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/dangerous-unprotect
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/consumer-apis/dangerous-unprotect.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Unprotect payloads whose keys have been revoked in ASP.NET Core

[]()

The ASP.NET Core data protection APIs are not primarily intended for indefinite persistence of confidential payloads. Other technologies like [Windows CNG DPAPI](/en-us/windows/win32/seccng/cng-dpapi) and [Azure Rights Management](/en-us/rights-management/) are more suited to the scenario of indefinite storage, and they have correspondingly strong key management capabilities. That said, there's nothing prohibiting a developer from using the ASP.NET Core data protection APIs for long-term protection of confidential data. Keys are never removed from the key ring, so `IDataProtector.Unprotect` can always recover existing payloads as long as the keys are available and valid.

However, an issue arises when the developer tries to unprotect data that has been protected with a revoked key, as `IDataProtector.Unprotect` will throw an exception in this case. This might be fine for short-lived or transient payloads (like authentication tokens), as these kinds of payloads can easily be recreated by the system, and at worst the site visitor might be required to log in again. But for persisted payloads, having `Unprotect` throw could lead to unacceptable data loss.

## IPersistedDataProtector

To support the scenario of allowing payloads to be unprotected even in the face of revoked keys, the data protection system contains an `IPersistedDataProtector` type. To get an instance of `IPersistedDataProtector`, simply get an instance of `IDataProtector` in the normal fashion and try casting the `IDataProtector` to `IPersistedDataProtector`.

Note

Not all `IDataProtector` instances can be cast to `IPersistedDataProtector`. Developers should use the C# as operator or similar to avoid runtime exceptions caused by invalid casts, and they should be prepared to handle the failure case appropriately.

`IPersistedDataProtector` exposes the following API surface:

```
DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors,
     out bool requiresMigration, out bool wasRevoked) : byte[]
```

This API takes the protected payload (as a byte array) and returns the unprotected payload. There's no string-based overload. The two out parameters are as follows.

* `requiresMigration`: Is set to `true` if the key used to protect this payload is no longer the active default key. For example, the key used to protect this payload is old and a key rolling operation has since taken place. The caller may wish to consider reprotecting the payload depending on their business needs.

* `wasRevoked`: Is set to `true` if the key used to protect this payload was revoked.

Warning

Exercise extreme caution when passing `ignoreRevocationErrors: true` to the `DangerousUnprotect` method. If after calling this method the `wasRevoked` value is true, then the key used to protect this payload was revoked, and the payload's authenticity should be treated as suspect. In this case, only continue operating on the unprotected payload if you have some separate assurance that it's authentic, for example that it's coming from a secure database rather than being sent by an untrusted web client.

```
using System;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;

public class Program
{
    public static void Main(string[] args)
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddDataProtection()
            // point at a specific folder and use DPAPI to encrypt keys
            .PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys"))
            .ProtectKeysWithDpapi();
        var services = serviceCollection.BuildServiceProvider();

        // get a protector and perform a protect operation
        var protector = services.GetDataProtector("Sample.DangerousUnprotect");
        Console.Write("Input: ");
        byte[] input = Encoding.UTF8.GetBytes(Console.ReadLine());
        var protectedData = protector.Protect(input);
        Console.WriteLine($"Protected payload: {Convert.ToBase64String(protectedData)}");

        // demonstrate that the payload round-trips properly
        var roundTripped = protector.Unprotect(protectedData);
        Console.WriteLine($"Round-tripped payload: {Encoding.UTF8.GetString(roundTripped)}");

        // get a reference to the key manager and revoke all keys in the key ring
        var keyManager = services.GetService<IKeyManager>();
        Console.WriteLine("Revoking all keys in the key ring...");
        keyManager.RevokeAllKeys(DateTimeOffset.Now, "Sample revocation.");

        // try calling Protect - this should throw
        Console.WriteLine("Calling Unprotect...");
        try
        {
            var unprotectedPayload = protector.Unprotect(protectedData);
            Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
        }

        // try calling DangerousUnprotect
        Console.WriteLine("Calling DangerousUnprotect...");
        try
        {
            IPersistedDataProtector persistedProtector = protector as IPersistedDataProtector;
            if (persistedProtector == null)
            {
                throw new Exception("Can't call DangerousUnprotect.");
            }

            bool requiresMigration, wasRevoked;
            var unprotectedPayload = persistedProtector.DangerousUnprotect(
                protectedData: protectedData,
                ignoreRevocationErrors: true,
                requiresMigration: out requiresMigration,
                wasRevoked: out wasRevoked);
            Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
            Console.WriteLine($"Requires migration = {requiresMigration}, was revoked = {wasRevoked}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
        }
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Input: Hello!
 * Protected payload: CfDJ8LHIzUCX1ZVBn2BZ...
 * Round-tripped payload: Hello!
 * Revoking all keys in the key ring...
 * Calling Unprotect...
 * CryptographicException: The key {...} has been revoked.
 * Calling DangerousUnprotect...
 * Unprotected payload: Hello!
 * Requires migration = True, was revoked = True
 */
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/security/authorization/conventions
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/security/authorization/conventions.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Razor Pages authorization conventions in ASP.NET Core

One way to control access in a Razor Pages app is to use authorization conventions at startup. These conventions allow the app to authorize users and allow anonymous users to access individual pages or folders of pages. The conventions described in this article automatically apply [authorization filters](../../../mvc/controllers/filters?view=aspnetcore-10.0#authorization-filters) to control access.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/RazorPagesAuthorization) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app uses [cookie authentication without ASP.NET Core Identity](../../../security/authentication/cookie?view=aspnetcore-10.0). To use ASP.NET Core Identity, follow the guidance in [Introduction to Identity on ASP.NET Core](../../../security/authentication/identity?view=aspnetcore-10.0).

## Require authorization to access a page

Use the [AuthorizePage](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizepage) convention to add an [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) to the page at the specified path:

```
builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Contact");
    options.Conventions.AuthorizeFolder("/Private");
    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});
```

```
services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Contact");
    options.Conventions.AuthorizeFolder("/Private");
    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});
```

```
services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AuthorizePage("/Contact");
        options.Conventions.AuthorizeFolder("/Private");
        options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
        options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
```

The specified path is the View Engine path, which is the Razor Pages root relative path without an extension and containing only forward slashes.

To specify an [authorization policy](../../../security/authorization/policies?view=aspnetcore-10.0), use an [`AuthorizePage` overload](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizepage):

```
options.Conventions.AuthorizePage("/Contact", "AtLeast21");
```

Note

An [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) can be applied to a page model class with the `[Authorize]` filter attribute. For more information, see [Filter methods for Razor Pages in ASP.NET Core](../../filter?view=aspnetcore-10.0#authorize-filter-attribute).

## Require authorization to access a folder of pages

Use the [AuthorizeFolder](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizefolder) convention to add an [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) to all of the pages in a folder at the specified path:

```
builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Contact");
    options.Conventions.AuthorizeFolder("/Private");
    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});
```

```
services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Contact");
    options.Conventions.AuthorizeFolder("/Private");
    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});
```

```
services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AuthorizePage("/Contact");
        options.Conventions.AuthorizeFolder("/Private");
        options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
        options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
```

The specified path is the View Engine path, which is the Razor Pages root relative path containing only forward slashes.

To specify an [authorization policy](../../../security/authorization/policies?view=aspnetcore-10.0), use an [`AuthorizeFolder` overload](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizefolder):

```
options.Conventions.AuthorizeFolder("/Private", "AtLeast21");
```

## Require authorization to access an area page

Use the [AuthorizeAreaPage](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizeareapage) convention to add an [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) to the area page at the specified path:

```
options.Conventions.AuthorizeAreaPage("Identity", "/Manage/Accounts");
```

The page name is the path of the file without an extension relative to the pages root directory for the specified area. For example, the page name for the file `Areas/Identity/Pages/Manage/Accounts.cshtml` is `/Manage/Accounts`.

To specify an [authorization policy](../../../security/authorization/policies?view=aspnetcore-10.0), use an [`AuthorizeAreaPage` overload](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizeareapage):

```
options.Conventions.AuthorizeAreaPage("Identity", "/Manage/Accounts", "AtLeast21");
```

## Require authorization to access a folder of areas

Use the [AuthorizeAreaFolder](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizeareafolder) convention to add an [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) to all of the areas in a folder at the specified path:

```
options.Conventions.AuthorizeAreaFolder("Identity", "/Manage");
```

The folder path is the path of the folder relative to the pages root directory for the specified area. For example, the folder path for the files under `Areas/Identity/Pages/Manage/` is `/Manage`.

To specify an [authorization policy](../../../security/authorization/policies?view=aspnetcore-10.0), use an [`AuthorizeAreaFolder` overload](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.authorizeareafolder):

```
options.Conventions.AuthorizeAreaFolder("Identity", "/Manage", "AtLeast21");
```

## Allow anonymous access to a page

Use the [AllowAnonymousToPage](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.allowanonymoustopage) convention to add an [AllowAnonymousFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.allowanonymousfilter) to a page at the specified path:

```
builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Contact");
    options.Conventions.AuthorizeFolder("/Private");
    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});
```

```
services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Contact");
    options.Conventions.AuthorizeFolder("/Private");
    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});
```

```
services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AuthorizePage("/Contact");
        options.Conventions.AuthorizeFolder("/Private");
        options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
        options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
```

The specified path is the View Engine path, which is the Razor Pages root relative path without an extension and containing only forward slashes.

## Allow anonymous access to a folder of pages

Use the [AllowAnonymousToFolder](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.allowanonymoustofolder) convention to add an [AllowAnonymousFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.allowanonymousfilter) to all of the pages in a folder at the specified path:

```
builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Contact");
    options.Conventions.AuthorizeFolder("/Private");
    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});
```

```
services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Contact");
    options.Conventions.AuthorizeFolder("/Private");
    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});
```

```
services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AuthorizePage("/Contact");
        options.Conventions.AuthorizeFolder("/Private");
        options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
        options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
```

The specified path is the View Engine path, which is the Razor Pages root relative path without an extension and containing only forward slashes.

## Note on combining authorized and anonymous access

The app can specify that a folder of pages requires authorization and that a page within that folder allows anonymous access:

```
// This works.
.AuthorizeFolder("/Private").AllowAnonymousToPage("/Private/Public")
```

The reverse, however, isn't valid. The app can't declare a folder of pages for anonymous access and specify a page within that folder that requires authorization:

```
// This doesn't work!
.AllowAnonymousToFolder("/Public").AuthorizePage("/Public/Private")
```

Requiring authorization on the Private page fails. When both the [AllowAnonymousFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.allowanonymousfilter) and [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) are applied to the page, the [AllowAnonymousFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.allowanonymousfilter) takes precedence and controls access.

## Related content

* [Razor Pages route and app conventions in ASP.NET Core](../../razor-pages-conventions?view=aspnetcore-10.0)
* [PageConventionCollection](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization/make-content-localizable
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/localization/make-content-localizable.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Make an ASP.NET Core app's content localizable

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Hisham Bin Ateya](https://twitter.com/hishambinateya), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax) and [Nadeem Afana](https://afana.me/)

One task for localizing an app is to wrap localizable content with code that facilitates replacing that content for different cultures.

## `IStringLocalizer`

[IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer) and [IStringLocalizer\<T>](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer-1) were architected to improve productivity when developing localized apps. `IStringLocalizer` uses the [ResourceManager](/en-us/dotnet/api/system.resources.resourcemanager) and [ResourceReader](/en-us/dotnet/api/system.resources.resourcereader) to provide culture-specific resources at run time. The interface has an indexer and an `IEnumerable` for returning localized strings. `IStringLocalizer` doesn't require storing the default language strings in a resource file. You can develop an app targeted for localization and not need to create resource files early in development.

The following code example shows how to wrap the string "About Title" for localization.

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Localization.Controllers;

[Route("api/[controller]")]
public class AboutController : Controller
{
    private readonly IStringLocalizer<AboutController> _localizer;

    public AboutController(IStringLocalizer<AboutController> localizer)
    {
        _localizer = localizer;
    }

    [HttpGet]
    public string Get()
    {
        return _localizer["About Title"];
    }
}
```

In the preceding code, the `IStringLocalizer<T>` implementation comes from [Dependency Injection](../dependency-injection?view=aspnetcore-10.0). If the localized value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title".

You can leave the default language literal strings in the app and wrap them in the localizer, so that you can focus on developing the app. You develop an app with your default language and prepare it for the localization step without first creating a default resource file.

Alternatively, you can use the traditional approach and provide a key to retrieve the default language string. For many developers, the new workflow of not having a default language *.resx* file and simply wrapping the string literals can reduce the overhead of localizing an app. Other developers prefer the traditional work flow as it can be easier to work with long string literals and easier to update localized strings.

## `IHtmlLocalizer`

Use the [IHtmlLocalizer\<TResource>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.localization.ihtmllocalizer-1) implementation for resources that contain HTML. [IHtmlLocalizer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.localization.ihtmllocalizer) HTML-encodes arguments that are formatted in the resource string, but doesn't HTML-encode the resource string itself. In the following highlighted code, only the value of the `name` parameter is HTML-encoded.

```
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;

namespace Localization.Controllers;

public class BookController : Controller
{
    private readonly IHtmlLocalizer<BookController> _localizer;

    public BookController(IHtmlLocalizer<BookController> localizer)
    {
        _localizer = localizer;
    }

    public IActionResult Hello(string name)
    {
        ViewData["Message"] = _localizer["<b>Hello</b><i> {0}</i>", name];

        return View();
    }
```

***NOTE:*** Generally, only localize text, not HTML.

## `IStringLocalizerFactory`

At the lowest level, [IStringLocalizerFactory](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizerfactory) can be retrieved from of [Dependency Injection](../dependency-injection?view=aspnetcore-10.0):

```
public class TestController : Controller
{
    private readonly IStringLocalizer _localizer;
    private readonly IStringLocalizer _localizer2;

    public TestController(IStringLocalizerFactory factory)
    {
        var type = typeof(SharedResource);
        var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
        _localizer = factory.Create(type);
        _localizer2 = factory.Create("SharedResource", assemblyName.Name);
    }       

    public IActionResult About()
    {
        ViewData["Message"] = _localizer["Your application description page."] 
            + " loc 2: " + _localizer2["Your application description page."];

        return View();
    }
```

The preceding code demonstrates each of the two factory create methods.

## Shared resources

You can partition your localized strings by controller or area, or have just one container. In the sample app, a marker class named `SharedResource` is used for shared resources. The marker class is never called:

```
// Dummy class to group shared resources

namespace Localization;

public class SharedResource
{
}
```

In the following sample, the `InfoController` and the `SharedResource` localizers are used:

```
public class InfoController : Controller
{
    private readonly IStringLocalizer<InfoController> _localizer;
    private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

    public InfoController(IStringLocalizer<InfoController> localizer,
                   IStringLocalizer<SharedResource> sharedLocalizer)
    {
        _localizer = localizer;
        _sharedLocalizer = sharedLocalizer;
    }

    public string TestLoc()
    {
        string msg = "Shared resx: " + _sharedLocalizer["Hello!"] +
                     " Info resx " + _localizer["Hello!"];
        return msg;
    }
```

## View localization

The [IViewLocalizer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.localization.iviewlocalizer) service provides localized strings for a [view](../../mvc/views/overview?view=aspnetcore-10.0). The `ViewLocalizer` class implements this interface and finds the resource location from the view file path. The following code shows how to use the default implementation of `IViewLocalizer`:

```
@using Microsoft.AspNetCore.Mvc.Localization

@inject IViewLocalizer Localizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>@Localizer["Use this area to provide additional information."]</p>
```

The default implementation of `IViewLocalizer` finds the resource file based on the view's file name. There's no option to use a global shared resource file. `ViewLocalizer` implements the localizer using `IHtmlLocalizer`, so Razor doesn't HTML-encode the localized string. You can parameterize resource strings, and `IViewLocalizer` HTML-encodes the parameters but not the resource string. Consider the following Razor markup:

```
@Localizer["<i>Hello</i> <b>{0}!</b>", UserManager.GetUserName(User)]
```

A French resource file could contain the following values:

| Key                        | Value                         |
| -------------------------- | ----------------------------- |
| `<i>Hello</i> <b>{0}!</b>` | `<i>Bonjour</i> <b>{0} !</b>` |

The rendered view would contain the HTML markup from the resource file.

Generally, ***only localize text***, not HTML.

To use a shared resource file in a view, inject `IHtmlLocalizer<T>`:

```
@using Microsoft.AspNetCore.Mvc.Localization
@using Localization.Services

@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedLocalizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>

<h1>@SharedLocalizer["Hello!"]</h1>
```

## DataAnnotations localization

DataAnnotations error messages are localized with `IStringLocalizer<T>`. Using the option `ResourcesPath = "Resources"`, the error messages in `RegisterViewModel` can be stored in either of the following paths:

* *Resources/ViewModels.Account.RegisterViewModel.fr.resx*
* *Resources/ViewModels/Account/RegisterViewModel.fr.resx*

```
using System.ComponentModel.DataAnnotations;

namespace Localization.ViewModels.Account;

public class RegisterViewModel
{
    [Required(ErrorMessage = "The Email field is required.")]
    [EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required(ErrorMessage = "The Password field is required.")]
    [StringLength(8, ErrorMessage = "The {0} must be at least {2} characters long.",
                                                                 MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage =
                            "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}
```

Non-validation attributes are localized.

[]()

### How to use one resource string for multiple classes

The following code shows how to use one resource string for validation attributes with multiple classes:

```
    services.AddMvc()
        .AddDataAnnotationsLocalization(options => {
            options.DataAnnotationLocalizerProvider = (type, factory) =>
                factory.Create(typeof(SharedResource));
        });
```

In the preceding code, `SharedResource` is the class corresponding to the *.resx* file where the validation messages are stored. With this approach, DataAnnotations only uses `SharedResource`, rather than the resource for each class.

## DataAnnotations localization in Minimal APIs and Blazor

Validation error messages and the display names of validated members are localized by [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation), which is the validation pipeline used by Minimal APIs and Blazor forms.

Localization activates automatically when an [IStringLocalizerFactory](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizerfactory) is registered. Call [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization) together with [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation):

```
builder.Services.AddLocalization();
builder.Services.AddValidation();
```

For the message lookup key conventions, shared resource files, custom message formatting, and the full set of options, see [Validation in ASP.NET Core](../validation?view=aspnetcore-10.0#localize-validation-messages).

The integration doesn't apply to MVC and Razor Pages apps. For those frameworks, see [Model validation in ASP.NET Core MVC](../../mvc/models/validation?view=aspnetcore-10.0).

## Configure localization services

Localization services are configured in `Program.cs`:

```
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

builder.Services.AddMvc()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization();
```

* [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization) adds the localization services to the services container, including implementations for `IStringLocalizer<T>` and `IStringLocalizerFactory`. The preceding code also sets the resources path to "Resources".

* [AddViewLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvclocalizationmvcbuilderextensions.addviewlocalization) adds support for localized view files. In this sample, view localization is based on the view file suffix. For example "fr" in the `Index.fr.cshtml` file.

* [AddDataAnnotationsLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcdataannotationsmvcbuilderextensions.adddataannotationslocalization) adds support for localized `DataAnnotations` validation messages through `IStringLocalizer` abstractions.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

## Next steps

Localizing an app also involves the following tasks:

* [Provide localized resources for the languages and cultures the app supports](provide-resources?view=aspnetcore-10.0)
* [Implement a strategy to select the language/culture for each request](select-language-culture?view=aspnetcore-10.0)

## Related content

* [Url culture provider using middleware as filters in ASP.NET Core](https://andrewlock.net/url-culture-provider-using-middleware-as-mvc-filter-in-asp-net-core-1-1-0/)
* [Applying the RouteDataRequest CultureProvider globally with middleware as filters](https://andrewlock.net/applying-the-routedatarequest-cultureprovider-globally-with-middleware-as-filters/)
* [Globalization and localization in ASP.NET Core](../localization?view=aspnetcore-10.0)
* [Provide localized resources for languages and cultures in an ASP.NET Core app](provide-resources?view=aspnetcore-10.0)

One task for localizing an app is to wrap localizable content with code that facilitates replacing that content for different cultures.

## `IStringLocalizer`

[IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer) and [IStringLocalizer\<T>](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer-1) were architected to improve productivity when developing localized apps. `IStringLocalizer` uses the [ResourceManager](/en-us/dotnet/api/system.resources.resourcemanager) and [ResourceReader](/en-us/dotnet/api/system.resources.resourcereader) to provide culture-specific resources at run time. The interface has an indexer and an `IEnumerable` for returning localized strings. `IStringLocalizer` doesn't require storing the default language strings in a resource file. You can develop an app targeted for localization and not need to create resource files early in development.

The following code example shows how to wrap the string "About Title" for localization.

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Localization.Controllers
{
    [Route("api/[controller]")]
    public class AboutController : Controller
    {
        private readonly IStringLocalizer<AboutController> _localizer;

        public AboutController(IStringLocalizer<AboutController> localizer)
        {
            _localizer = localizer;
        }

        [HttpGet]
        public string Get()
        {
            return _localizer["About Title"];
        }
    }
}
```

In the preceding code, the `IStringLocalizer<T>` implementation comes from [Dependency Injection](../dependency-injection?view=aspnetcore-10.0). If the localized value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title".

You can leave the default language literal strings in the app and wrap them in the localizer, so that you can focus on developing the app. You develop an app with your default language and prepare it for the localization step without first creating a default resource file.

Alternatively, you can use the traditional approach and provide a key to retrieve the default language string. For many developers, the new workflow of not having a default language *.resx* file and simply wrapping the string literals can reduce the overhead of localizing an app. Other developers prefer the traditional work flow as it can be easier to work with long string literals and easier to update localized strings.

## `IHtmlLocalizer`

Use the `IHtmlLocalizer<T>` implementation for resources that contain HTML. `IHtmlLocalizer` HTML-encodes arguments that are formatted in the resource string, but doesn't HTML-encode the resource string itself. In the following highlighted code, only the value of the `name` parameter is HTML-encoded.

```
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;

namespace Localization.Controllers
{
    public class BookController : Controller
    {
        private readonly IHtmlLocalizer<BookController> _localizer;

        public BookController(IHtmlLocalizer<BookController> localizer)
        {
            _localizer = localizer;
        }

        public IActionResult Hello(string name)
        {
            ViewData["Message"] = _localizer["<b>Hello</b><i> {0}</i>", name];

            return View();
        }
```

Note

Generally, only localize text, not HTML.

## `IStringLocalizerFactory`

At the lowest level, you can get `IStringLocalizerFactory` out of [Dependency Injection](../dependency-injection?view=aspnetcore-10.0):

```
{
    public class TestController : Controller
    {
        private readonly IStringLocalizer _localizer;
        private readonly IStringLocalizer _localizer2;

        public TestController(IStringLocalizerFactory factory)
        {
            var type = typeof(SharedResource);
            var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
            _localizer = factory.Create(type);
            _localizer2 = factory.Create("SharedResource", assemblyName.Name);
        }       

        public IActionResult About()
        {
            ViewData["Message"] = _localizer["Your application description page."] 
                + " loc 2: " + _localizer2["Your application description page."];
```

The preceding code demonstrates each of the two factory create methods.

## Shared resources

You can partition your localized strings by controller or area, or have just one container. In the sample app, a dummy class named `SharedResource` is used for shared resources.

```
// Dummy class to group shared resources

namespace Localization
{
    public class SharedResource
    {
    }
}
```

Some developers use the `Startup` class to contain global or shared strings. In the following sample, the `InfoController` and the `SharedResource` localizers are used:

```
public class InfoController : Controller
{
    private readonly IStringLocalizer<InfoController> _localizer;
    private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

    public InfoController(IStringLocalizer<InfoController> localizer,
                   IStringLocalizer<SharedResource> sharedLocalizer)
    {
        _localizer = localizer;
        _sharedLocalizer = sharedLocalizer;
    }

    public string TestLoc()
    {
        string msg = "Shared resx: " + _sharedLocalizer["Hello!"] +
                     " Info resx " + _localizer["Hello!"];
        return msg;
    }
```

## View localization

The `IViewLocalizer` service provides localized strings for a [view](../../mvc/views/overview?view=aspnetcore-10.0). The `ViewLocalizer` class implements this interface and finds the resource location from the view file path. The following code shows how to use the default implementation of `IViewLocalizer`:

```
@using Microsoft.AspNetCore.Mvc.Localization

@inject IViewLocalizer Localizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>@Localizer["Use this area to provide additional information."]</p>
```

The default implementation of `IViewLocalizer` finds the resource file based on the view's file name. There's no option to use a global shared resource file. `ViewLocalizer` implements the localizer using `IHtmlLocalizer`, so Razor doesn't HTML-encode the localized string. You can parameterize resource strings, and `IViewLocalizer` HTML-encodes the parameters but not the resource string. Consider the following Razor markup:

```
@Localizer["<i>Hello</i> <b>{0}!</b>", UserManager.GetUserName(User)]
```

A French resource file could contain the following values:

| Key                        | Value                         |
| -------------------------- | ----------------------------- |
| `<i>Hello</i> <b>{0}!</b>` | `<i>Bonjour</i> <b>{0} !</b>` |

The rendered view would contain the HTML markup from the resource file.

Note

Generally, only localize text, not HTML.

To use a shared resource file in a view, inject `IHtmlLocalizer<T>`:

```
@using Microsoft.AspNetCore.Mvc.Localization
@using Localization.Services

@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedLocalizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>

<h1>@SharedLocalizer["Hello!"]</h1>
```

## DataAnnotations localization

DataAnnotations error messages are localized with `IStringLocalizer<T>`. Using the option `ResourcesPath = "Resources"`, the error messages in `RegisterViewModel` can be stored in either of the following paths:

* *Resources/ViewModels.Account.RegisterViewModel.fr.resx*
* *Resources/ViewModels/Account/RegisterViewModel.fr.resx*

```
public class RegisterViewModel
{
    [Required(ErrorMessage = "The Email field is required.")]
    [EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required(ErrorMessage = "The Password field is required.")]
    [StringLength(8, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}
```

In ASP.NET Core MVC 1.1.0 or later, non-validation attributes are localized.

[]()

### How to use one resource string for multiple classes

The following code shows how to use one resource string for validation attributes with multiple classes:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddDataAnnotationsLocalization(options => {
            options.DataAnnotationLocalizerProvider = (type, factory) =>
                factory.Create(typeof(SharedResource));
        });
}
```

In the preceding code, `SharedResource` is the class corresponding to the *.resx* file where the validation messages are stored. With this approach, DataAnnotations only uses `SharedResource`, rather than the resource for each class.

## Configure localization services

Localization services are configured in the `Startup.ConfigureServices` method:

```
services.AddLocalization(options => options.ResourcesPath = "Resources");

services.AddMvc()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization();
```

* `AddLocalization` adds the localization services to the services container, including implementations for `IStringLocalizer<T>` and `IStringLocalizerFactory`. The preceding code also sets the resources path to "Resources".

* `AddViewLocalization` adds support for localized view files. In this sample, view localization is based on the view file suffix. For example "fr" in the `Index.fr.cshtml` file.

* `AddDataAnnotationsLocalization` adds support for localized `DataAnnotations` validation messages through `IStringLocalizer` abstractions.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

## Next steps

Localizing an app also involves the following tasks:

* [Provide localized resources for the languages and cultures the app supports](provide-resources?view=aspnetcore-10.0)
* [Implement a strategy to select the language/culture for each request](select-language-culture?view=aspnetcore-10.0)

## Related content

* [Globalization and localization in ASP.NET Core](../localization?view=aspnetcore-10.0)
* [Provide localized resources for languages and cultures in an ASP.NET Core app](provide-resources?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/crud
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/crud.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Implement basic CRUD functionality - ASP.NET MVC with EF Core

In the previous tutorial, you created an MVC application that stores and displays data by using the [Entity Framework (EF) Core](/en-us/ef/core/) and a SQL Server local database. In this exercise, you review and customize the CRUD (create, read, update, delete) code that the MVC scaffolding automatically creates for you in controllers and views.

Note

It's a common practice to implement the repository pattern in order to create an abstraction layer between your controller and the data access layer. To keep the examples simple and focused on demonstrating how to use the Entity Framework itself, the tutorials don't use repositories. For information about repositories with Entity Framework, see [the last tutorial in this series](advanced?view=aspnetcore-10.0).

In this tutorial, you:

* Customize the Details page by adding enrollment data with eager loading
* Update the Create page with security and error handling
* Update the Edit page to prevent overposting
* Update the Delete page with error reporting
* Close database connections

## Prerequisites

* Complete the previous tutorial, [Get started with EF Core in an ASP.NET MVC web app](intro?view=aspnetcore-10.0).

## Customize the Details page

The scaffolded code for the **Students Index** page left out the `Enrollments` property because the property holds a collection. The **Details** page displays the contents of the collection as an HTML table.

In the *Controllers/StudentsController.cs* file, the action method for the Details view uses the `FirstOrDefaultAsync` method to retrieve a single `Student` entity. You need to add code that calls the `Include`, `ThenInclude`, and `AsNoTracking` methods, as shown in the following highlighted code.

```
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var student = await _context.Students
        .Include(s => s.Enrollments)
            .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (student == null)
    {
        return NotFound();
    }

    return View(student);
}
```

The `Include` and `ThenInclude` methods cause the context to load the `Student.Enrollments` navigation property and the `Enrollment.Course` navigation property within each enrollment. You learn more about these methods in the [read related data](read-related-data?view=aspnetcore-10.0) tutorial.

The `AsNoTracking` method improves performance in scenarios where the returned entities aren't updated in the current context's lifetime. You learn more about the `AsNoTracking` method at the end of this tutorial.

### Configure the route data

The key value passed to the `Details` method comes from the *route data*. Route data is data that the model binder finds in a segment of the URL. For example, the default route specifies `controller`, `action`, and `id` (identifier, ID) segments:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

In the following URL, the default route maps *Instructor* as the `controller`, *Index* as the `action`, and 1 as the `id`. These values compose the route data.

```
http://localhost:1230/Instructor/Index/1?courseID=2021
```

The last part of the URL (`?courseID=2021`) is a query string value. The model binder also passes the ID value to the `Index` method `id` parameter, if you pass it as a query string value:

```
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
```

In the **Index** page, hyperlink URLs are created by tag helper statements in the Razor view. In the following Razor code, the `id` parameter matches the default route, so the `id` value is added to the route data.

```
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>
```

This change generates the following HTML when the `item.ID` value is 6:

```
<a href="/Students/Edit/6">Edit</a>
```

In the following Razor code, the `studentID` text doesn't match a parameter in the default route, so the text is added as a query string.

```
<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>
```

This change generates the following HTML when the `item.ID` value is 6:

```
<a href="/Students/Edit?studentID=6">Edit</a>
```

For more information about tag helpers, see [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

### Add enrollments to the Details view

Open the *Views/Students/Details.cshtml* file. Each field is displayed by using the `DisplayNameFor` and `DisplayFor` helpers, as shown in the following example.

```
<dt class="col-sm-2">
    @Html.DisplayNameFor(model => model.LastName)
</dt>
<dd class="col-sm-10">
    @Html.DisplayFor(model => model.LastName)
</dd>
```

After the last field, and immediately before the closing `</dl>` tag, add the following code to display a list of enrollments:

```
<dt class="col-sm-2">
    @Html.DisplayNameFor(model => model.Enrollments)
</dt>
<dd class="col-sm-10">
    <table class="table">
        <tr>
            <th>Course Title</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Course.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
</dd>
```

If the code indentation is misaligned after you paste the snippet, use the **CTRL**+**K**+**D** keyboard shortcut to correct it.

The new code loops through the entities in the `Enrollments` navigation property. For each enrollment, the code displays the course title and the grade. The course title is retrieved from the `Course` entity stored in the `Course` navigation property of the `Enrollments` entity.

Run the app, select the **Students** tab, and select the **Details** link for a student. You see the list of courses and grades for the selected student:

## Update the Create page

In the *StudentsController.cs* file, modify the HttpPost `Create` method by adding a `try-catch` block and removing the `ID` property from the `Bind` attribute.

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
    [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
    try
    {
        if (ModelState.IsValid)
        {
            _context.Add(student);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
    }
    catch (DbUpdateException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.
        ModelState.AddModelError("", "Unable to save changes. " +
            "Try again, and if the problem persists " +
            "see your system administrator.");
    }
    return View(student);
}
```

This code adds the `Student` entity created by the ASP.NET Core MVC model binder to the `Students` entity set, and then saves the changes to the database.

*Model binder* refers to the ASP.NET Core MVC functionality that makes it easier for you to work with data submitted by a form. A model binder converts posted form values to CLR types and passes them to the action method in parameters. In this case, the model binder instantiates a `Student` entity for you by using property values from the `Form` collection.

You removed the `ID` property from the `Bind` attribute because the ID is the primary key value that SQL Server sets automatically when it inserts the row. Input from the user doesn't set the ID value.

Other than the `Bind` attribute, the `try-catch` block is the only change made to the scaffolded code. If an exception that derives from `DbUpdateException` is caught while the changes are being saved, a generic error message is displayed. The cause for a `DbUpdateException` exception can be external to the application rather than a programming error, so the user is advised to try again. A production quality application logs such exception, but that functionality isn't implemented in this sample. For more information, see [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0).

The `ValidateAntiForgeryToken` attribute helps prevent cross-site request forgery (CSRF) attacks. The [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper) method automatically injects the token into the view. The token is also included when the user submits the form. The `ValidateAntiForgeryToken` attribute checks the token. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0) and [FormTagHelper Class](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.formtaghelper) reference.

[]()

### Protect against overposting

The `Bind` attribute that the scaffolded code includes on the `Create` method is one way to protect against overposting in create scenarios. For example, suppose the `Student` entity includes a `Secret` property that you don't want this web page to set.

```
public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}
```

Even if you don't have a `Secret` field on the web page, a hacker might use a tool such as Fiddler, or write some JavaScript, to post a `Secret` form value. Without the `Bind` attribute limiting the fields that the model binder uses when it creates a `Student` instance, the model binder can pick up that `Secret` form value and use it to create the `Student` entity instance. Then, whatever value the hacker specified for the `Secret` form field is updated in your database. The following image shows the Fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.

The value "OverPost" is then successfully added to the `Secret` property of the inserted row, although you never intended that the web page set the property.

You can prevent overposting in edit scenarios by reading the entity from the database first and then calling the `TryUpdateModel` method. Pass in an explicit allowed properties list, which is how the method is used in these tutorials.

An alternate way to prevent overposting preferred by many developers is to use view models rather than entity classes with model binding. Include only the properties you want to update in the view model. After the MVC model binder completes, copy the view model properties to the entity instance, optionally by using a tool such as AutoMapper. Use the `_context.Entry` setting on the entity instance to set its state to `Unchanged`, and then set the `Property("PropertyName").IsModified` setting to true on each entity property included in the view model. This method works in both edit and create scenarios.

### Test the Create page

The code in the *Views/Students/Create.cshtml* file uses the `label`, `input`, and `span` (for validation messages) tag helpers for each field.

Run the app, select the **Students** tab, and then select **Create New**.

Enter names and a date. Try entering an invalid date, as your browser allows. (Some browsers force you to use a date picker.) Select **Create** to see the error message.

This scenario demonstrates the default server-side validation. In a later tutorial, you see how to add attributes that generate code for client-side validation. The following highlighted code shows the model validation check in the `Create` method.

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
    [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
    try
    {
        if (ModelState.IsValid)
        {
            _context.Add(student);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
    }
    catch (DbUpdateException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.
        ModelState.AddModelError("", "Unable to save changes. " +
            "Try again, and if the problem persists " +
            "see your system administrator.");
    }
    return View(student);
}
```

Change the date to a valid value, and select **Create** to see the new student appear in the **Index** page.

## Update the Edit page

In the *StudentController.cs* file, the HttpGet `Edit` method (the one without the `HttpPost` attribute) uses the `FirstOrDefaultAsync` method to retrieve the selected `Student` entity, as you reviewed earlier in the `Details` method. You don't need to change this method.

### Use recommended HttpPost Edit code: Read and update

Replace the HttpPost `Edit` method with the following code.

```
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
    if (id == null)
    {
        return NotFound();
    }
    var studentToUpdate = await _context.Students.FirstOrDefaultAsync(s => s.ID == id);
    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
    }
    return View(studentToUpdate);
}
```

These changes implement a security best practice to prevent overposting. The scaffolder generated a `Bind` attribute and added the entity created by the model binder to the entity set with a `Modified` flag. That code isn't recommended for many scenarios because the `Bind` attribute clears out any preexisting data in fields not listed in the `Include` parameter.

The new code reads the existing entity and calls the `TryUpdateModel` method to update fields in the retrieved entity based on user input in the posted form data. (For more information, see [Model Binding in ASP.NET Core](../../mvc/models/model-binding?view=aspnetcore-10.0).) The Entity Framework's automatic change tracking sets the `Modified` flag on fields that the form input updates. When the `SaveChanges` method is called, the Entity Framework creates SQL statements to update the database row. Concurrency conflicts are ignored, and only the table columns updated by the user are also updated in the database. (A later tutorial shows how to handle concurrency conflicts.)

As a best practice to prevent overposting, the fields that you want to be updateable by the **Edit** page are declared in the `TryUpdateModel` parameters. (The empty string preceding the list of fields in the parameter list is for a prefix to use with the form fields names.) Currently, there are no extra fields that you're protecting. If you list the fields you want the model binder to bind, you can prevent overposting for fields added in the future. The fields in the list are automatically protected until you explicitly add them.

As a result of these changes, the method signature of the HttpPost `Edit` method is the same as the HttpGet `Edit` method. The changes basically rename the method to `EditPost`.

### Use alternate HttpPost Edit code: Create and attach

The recommended HttpPost `Edit` code ensures that only changed columns are updated and preserves data in properties that you don't want included for model binding. However, the read-first approach requires an extra database read, and can result in more complex code for handling concurrency conflicts. An alternative is to attach an entity created by the model binder to the Entity Framework context and mark it as modified. (Don't update your project with the following code. The code illustrates an optional approach.)

```
public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
{
    if (id != student.ID)
    {
        return NotFound();
    }
    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(student);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
    }
    return View(student);
}
```

You can use this approach when the web page UI includes all of the fields in the entity and can update any of them.

The scaffolded code uses the *create-and-attach* approach but only catches `DbUpdateConcurrencyException` exceptions and returns 404 error codes. The example shown catches any database update exception and displays an error message.

### Understand entity states

The database context keeps track of whether entities in memory are in sync with their corresponding rows in the database. This information determines what happens when you call the `SaveChanges` method. For example, when you pass a new entity to the `Add` method, that entity's state is set to `Added`. When you call the `SaveChanges` method, the database context issues a SQL INSERT command.

An entity can be in one of the following states:

* `Added`: The entity doesn't yet exist in the database. The `SaveChanges` method issues an `INSERT` statement.

* `Unchanged`: Nothing needs to be done with this entity by the `SaveChanges` method. When you read an entity from the database, the entity starts out with this status.

* `Modified`: Some or all of the entity's property values are modified. The `SaveChanges` method issues an `UPDATE` statement.

* `Deleted`: The entity is marked for deletion. The `SaveChanges` method issues a `DELETE` statement.

* `Detached`: The database context doesn't track the entity.

In a desktop application, state changes are typically set automatically. You read an entity and make changes to some of its property values. This behavior causes its entity state to automatically change to `Modified`. When you call the `SaveChanges` method, the Entity Framework generates a SQL `UPDATE` statement that updates only the actual properties that you changed.

In a web app, the `DbContext` object that initially reads an entity and displays its data to be edited is disposed after a page is rendered. When the HttpPost `Edit` action method is called, a new web request is made and you have a new instance of the `DbContext` class. If you re-read the entity in that new context, you simulate desktop processing.

If you don't want to do the extra read operation, you have to use the entity object created by the model binder. The easiest approach is to set the entity state to `Modified` as is done in the alternative HttpPost `Edit` code shown earlier. When you call the `SaveChanges` method, the Entity Framework updates all columns of the database row because the context has no way to know which properties you changed.

If you want to avoid the *read-first* approach, but you also want the SQL `UPDATE` statement to update only the fields the user changes, the code is more complex. You have to save the original values in some way, such as by using hidden fields. The values must be available when the HttpPost `Edit` method is called. You can create a `Student` entity by using the original values, call the `Attach` method with the original version of the entity, update the entity's values to the new values, and then call the `SaveChanges` method.

### Test the Edit page

Run the app, select the **Students** tab, then select an **Edit** hyperlink.

Change some of the data and select **Save**. The **Index** page opens and you see the changed data.

## Update the Delete page

In the *StudentController.cs* file, the template code for the HttpGet `Delete` method uses the `FirstOrDefaultAsync` method to retrieve the selected `Student` entity, as you saw in the `Details` and `Edit` methods. However, to implement a custom error message when the call to the `SaveChanges` method fails, you need to add some functionality to this method and its corresponding view.

As you saw for the update and create operations, delete operations require two action methods. The method called in response to a GET request displays a view that gives the user a chance to approve or cancel the delete operation. If the user approves, a POST request is created. The HttpPost `Delete` method is then called and this method actually performs the delete operation.

You need to add a try-catch block to the HttpPost `Delete` method to handle any errors that might occur when the database is updated. If an error occurs, the HttpPost `Delete` method calls the HttpGet `Delete` method, passing a parameter that indicates an error. The HttpGet `Delete` method redisplays the confirmation page along with the error message, giving the user an opportunity to cancel or try again.

Replace the HttpGet `Delete` action method with the following code, which manages error reporting.

```
public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
{
    if (id == null)
    {
        return NotFound();
    }

    var student = await _context.Students
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);
    if (student == null)
    {
        return NotFound();
    }

    if (saveChangesError.GetValueOrDefault())
    {
        ViewData["ErrorMessage"] =
            "Delete failed. Try again, and if the problem persists " +
            "see your system administrator.";
    }

    return View(student);
}
```

This code accepts an optional parameter that indicates whether the method call is after a failure to save changes. This parameter is false when the HttpGet `Delete` method is called without a previous failure. When the method is called by the HttpPost `Delete` method in response to a database update error, the parameter is true and an error message is passed to the view.

### Use read-first approach to HttpPost Delete

Replace the HttpPost `Delete` action method (named `DeleteConfirmed`) with the following code. This code performs the actual delete operation and catches any database update errors.

```
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var student = await _context.Students.FindAsync(id);
    if (student == null)
    {
        return RedirectToAction(nameof(Index));
    }

    try
    {
        _context.Students.Remove(student);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    catch (DbUpdateException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
    }
}
```

The code retrieves the selected entity, and then calls the `Remove` method to set the entity's status to `Deleted`. When the `SaveChanges` method is called, a SQL DELETE command is generated.

### Use create-and-attach approach to HttpPost Delete

If improving performance in a high-volume application is a priority, you might avoid an unnecessary SQL query by instantiating a `Student` entity by using only the primary key value and setting the entity state to `Deleted`. This information is all the Entity Framework needs to delete the entity. (Don't update your project with the following code. The code illustrates an alternate approach.)

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    try
    {
        Student studentToDelete = new Student() { ID = id };
        _context.Entry(studentToDelete).State = EntityState.Deleted;
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    catch (DbUpdateException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
    }
}
```

If the entity also contains related data to delete, make sure that cascade delete is configured in the database. With this approach to entity deletion, Entity Framework might not realize there are related entities to delete.

### Update the Delete view

In the *Views/Student/Delete.cshtml* file, add an error message between the h2 heading and the h3 heading, as shown in the following example:

```
<h2>Delete</h2>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>
```

Run the app, select the **Students** tab, and select a **Delete** hyperlink:

Select **Delete**. The **Index** page displays without the deleted student. (You see an example of the error handling code in action in the concurrency tutorial.)

## Close database connections

To free up the resources that a database connection holds, the context instance must be disposed as soon as possible when you're finished. The ASP.NET Core built-in [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) takes care of the clean-up task.

In the *Startup.cs* file, you call the [AddDbContext extension method](https://github.com/dotnet/efcore/blob/03bcb5122e3f577a84498545fcf130ba79a3d987/src/Microsoft.EntityFrameworkCore/EntityFrameworkServiceCollectionExtensions.cs) to create the `DbContext` class in the ASP.NET Core DI container. The method sets the service lifetime to `Scoped` by default. `Scoped` means the context object lifetime coincides with the web request life time, and the `Dispose` method is called automatically at the end of the web request.

## Handle transactions

By default, the Entity Framework implicitly implements transactions. In scenarios where you make changes to multiple rows or tables and then call the `SaveChanges` method, the Entity Framework automatically makes sure that either all of your changes succeed or they all fail. If some changes are finished first and then an error occurs, the completed changes are automatically rolled back. In scenarios where you need more control, see [Transactions](/en-us/ef/core/saving/transactions). The examples cover how to include operations completed outside of Entity Framework in a transaction.

## Disable tracking of entity objects (no-tracking queries)

When a database context retrieves table rows and creates entity objects that represent them, it tracks whether the entities in memory are in sync with the database, by default. The data in memory acts as a cache and is used when you update an entity. This caching is often unnecessary in a web application because context instances are typically short-lived (a new one is created and disposed for each request). The context that reads an entity is typically disposed before that entity is used again.

You can disable tracking of entity objects in memory by calling the `AsNoTracking` method. Here are some of the common scenarios for this action:

* During the context lifetime, you don't need to update any entities, and you don't need Entity Framework to [automatically load navigation properties with entities retrieved by separate queries](read-related-data?view=aspnetcore-10.0). These conditions are frequently met in a controller's HttpGet action methods.

* You're running a query that retrieves a large volume of data, and only a small portion of the returned data gets updated. It can be more efficient to turn off tracking for the large query, and run a query later for the few entities that need updates.

* You want to attach an entity to make updates, but earlier you retrieved the same entity for a different purpose. Because the database context already tracks the entity, you can't attach the entity you want to change. One way to handle this situation is to call the `AsNoTracking` method on the earlier query.

For more information, see [Tracking versus no-tracking queries](/en-us/ef/core/querying/tracking).

## Get the code

[Download or view the completed application](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final),

## Next step

[Tutorial: Add sorting, filtering, and paging - ASP.NET MVC with EF Core](sort-filter-page?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/graph-api
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/graph-api.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use Graph API with ASP.NET Core Blazor WebAssembly

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to use [Microsoft Graph](/en-us/graph/) in Blazor WebAssembly apps, which enables apps to access Microsoft Cloud resources.

Two approaches are covered:

* **Graph SDK**: The [Microsoft Graph SDK](/en-us/graph/sdks/sdks-overview) simplifies building high-quality, efficient, and resilient apps that access Microsoft Graph. Select the **Graph SDK** button at the top of this article to adopt this approach.

* **Named HttpClient with Graph API**: A [named `HttpClient`](../../call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory) can issue [Microsoft Graph API](/en-us/graph/use-the-api) requests directly to Microsoft Graph. Select the **Named HttpClient with Graph API** button at the top of this article to adopt this approach.

The guidance in this article isn't meant to replace the [Microsoft Graph documentation](/en-us/graph/) and Azure security guidance in other Microsoft documentation sets. Assess the security guidance in the [Related content](#related-content) section of this article before implementing Microsoft Graph in a production environment. Follow Microsoft's best practices to limit the vulnerabilities of your apps.

Additional approaches for working with Microsoft Graph and Blazor WebAssembly are provided by the following Microsoft Graph and Azure samples:

* [Microsoft Graph sample Blazor WebAssembly app](https://github.com/microsoftgraph/msgraph-sample-blazor-clientside): The sample adopts a factory-based approach with the [Microsoft Graph SDK](/en-us/graph/sdks/sdks-overview) to obtain Office 365 data. The sample uses a default [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to make Graph client requests. If you need to use the default [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) with the base address of the app (`BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)`) for other purposes, for example to load data from the web root of the app, consider refactoring the Graph client factory to use a [named `HttpClient`](../../call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory) dedicated to Graph requests.
* [ASP.NET Core in .NET 8 Blazor WebAssembly | standalone app | user sign-in, protected web API access (Microsoft Graph) | Microsoft identity platform](https://github.com/Azure-Samples/ms-identity-docs-code-dotnet/tree/main/spa-blazor-wasm): The sample is a [Progressive Web Application (PWA)](../../progressive-web-app/?view=aspnetcore-10.0) that uses an authorization message handler and [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to obtain Graph data. As with the preceding sample, this sample also uses a default [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to make Graph client requests. Instead of using the Microsoft Graph SDK, the sample retrieves user account data as JSON via a [Microsoft Graph API](/en-us/graph/use-the-api) request in a component. This is a similar technique to the **Named HttpClient with Graph API** approach in this article (use the button at the top of this article to see the guidance), except that this article's guidance uses a [named `HttpClient`](../../call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory) dedicated to Graph requests.

To provide feedback on either of the preceding two samples, open an issue on the sample's GitHub repository. If you're opening an issue for the Azure sample, provide a link to the sample in your opening comment because the Azure sample repository (`Azure-Samples`) contains many samples. Describe the problem in detail and include sample code as needed. Place a minimal app into GitHub that reproduces the problem or error. Be sure to remove Azure account configuration data from the sample before you commit it to the public repository.

To provide feedback or seek assistance with this article or ASP.NET Core, see [ASP.NET Core Blazor fundamentals](../../fundamentals/?view=aspnetcore-10.0#support-requests).

Important

The scenarios described in this article apply to using Microsoft Entra (ME-ID) as the identity provider, not AAD B2C. Using Microsoft Graph with a client-side Blazor WebAssembly app and the AAD B2C identity provider isn't supported at this time because the app would require a client secret, which can't be secured in the client-side Blazor app. For an AAD B2C standalone Blazor WebAssembly app use Graph API, create a backend server (web) API to access Graph API on behalf of users. The client-side app authenticates and authorizes users to [call the web API](../../call-web-api?view=aspnetcore-10.0) to securely access Microsoft Graph and return data to the client-side Blazor app from your server-based web API. The client secret is safely maintained in the server-based web API, not in the Blazor app on the client. **Never store a client secret in a client-side Blazor app.**

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

Using a hosted Blazor WebAssembly app is supported, where the **Server** app uses the Graph SDK/API to provide Graph data to the **Client** app via web API. For more information, see the [Hosted Blazor WebAssembly solutions](#hosted-blazor-webassembly-solutions) section of this article.

The examples in this article take advantage of new .NET/C# features. When using the examples with .NET 7 or earlier, minor modifications are required. However, the text and code examples that pertain to interacting with Microsoft Graph are the same for all versions of ASP.NET Core.

*The following guidance applies to Microsoft Graph v5 or later.*

The Microsoft Graph SDK for use in Blazor apps is called the *Microsoft Graph .NET Client Library*.

The Graph SDK examples require the following package references in the standalone Blazor WebAssembly app. The first two packages are already referenced if the app has been enabled for MSAL authentication, for example when creating the app by following the guidance in [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0).

The Graph SDK examples require the following package references in the standalone Blazor WebAssembly app or the **Client** app of a hosted Blazor WebAssembly solution. The first two packages are already referenced if the app has been enabled for MSAL authentication, for example when creating the app by following the guidance in [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0).

* [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication)
* [`Microsoft.Authentication.WebAssembly.Msal`](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal)
* [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http)
* [`Microsoft.Graph`](https://www.nuget.org/packages/Microsoft.Graph)

Note

The [`Microsoft.Authentication.WebAssembly.Msal` package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal) transitively adds the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the Azure portal, grant delegated permissions (scopes)† for Microsoft Graph data that the app should be able to access on behalf of a user. For the example in this article, the app's registration should include delegated permission to read user data (`Microsoft.Graph` > `User.Read` scope in **API permissions**, Type: Delegated). The `User.Read` scope allows users to sign in to the app and allows the app to read the profile and company information of signed-in users. For more information, see [Overview of permissions and consent in the Microsoft identity platform](/en-us/entra/identity-platform/permissions-consent-overview) and [Overview of Microsoft Graph permissions](/en-us/graph/permissions-overview).

†*Permissions* and *scopes* mean the same thing and are used interchangeably in security documentation and the Azure portal. Unless the text is referring to the Azure portal, this article uses *scope*/*scopes* when referring to Graph permissions.

Scopes are case insensitive, so `User.Read` is the same as `user.read`. Feel free to use either format, but we recommend a consistent choice across application code.

After adding the Microsoft Graph API scopes to the app's registration in the Azure portal, add the following app settings configuration to the `wwwroot/appsettings.json` file in the app, which includes the Graph base URL with the Microsoft Graph version and scopes. In the following example, the `User.Read` scope is specified for the examples in later sections of this article. Scopes aren't case sensitive.

```
"MicrosoftGraph": {
  "BaseUrl": "https://graph.microsoft.com",
  "Version": "{VERSION}",
  "Scopes": [
    "user.read"
  ]
}
```

In the preceding example, the `{VERSION}` placeholder is the version of the Microsoft Graph API (for example: `v1.0`).

The following is an example of a complete `wwwroot/appsettings.json` configuration file for an app that uses ME-ID as its identity provider, where reading user data (`user.read` scope) is specified for Microsoft Graph:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "user.read"
    ]
  }
}
```

In the preceding example, the `{TENANT ID}` placeholder is the Directory (tenant) ID, and the `{CLIENT ID}` placeholder is the Application (client) ID. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0).

Add the following `GraphClientExtensions` class to the standalone app. The scopes are provided to the [Scopes](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenrequestoptions.scopes#microsoft-aspnetcore-components-webassembly-authentication-accesstokenrequestoptions-scopes) property of the [AccessTokenRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenrequestoptions) in the `AuthenticateRequestAsync` method.

Add the following `GraphClientExtensions` class to the standalone app or **Client** app of a hosted Blazor WebAssembly [solution](../../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln). The scopes are provided to the [Scopes](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenrequestoptions.scopes#microsoft-aspnetcore-components-webassembly-authentication-accesstokenrequestoptions-scopes) property of the [AccessTokenRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenrequestoptions) in the `AuthenticateRequestAsync` method.

When an access token isn't obtained, the following code doesn't set a Bearer authorization header for Graph requests.

`GraphClientExtensions.cs`:

```
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;
using IAccessTokenProvider = 
    Microsoft.AspNetCore.Components.WebAssembly.Authentication.IAccessTokenProvider;

namespace BlazorSample;

internal static class GraphClientExtensions
{
    public static IServiceCollection AddGraphClient(
            this IServiceCollection services, string? baseUrl, List<string>? scopes)
    {
        if (string.IsNullOrEmpty(baseUrl) || scopes?.Count == 0)
        {
            return services;
        }

        services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(
            options =>
            {
                scopes?.ForEach((scope) =>
                {
                    options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
                });
            });

        services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();

        services.AddScoped(sp =>
        {
            return new GraphServiceClient(
                new HttpClient(),
                sp.GetRequiredService<IAuthenticationProvider>(),
                baseUrl);
        });

        return services;
    }

    private class GraphAuthenticationProvider(IAccessTokenProvider tokenProvider, 
        IConfiguration config) : IAuthenticationProvider
    {
        private readonly IConfiguration config = config;

        public IAccessTokenProvider TokenProvider { get; } = tokenProvider;

        public async Task AuthenticateRequestAsync(RequestInformation request, 
            Dictionary<string, object>? additionalAuthenticationContext = null, 
            CancellationToken cancellationToken = default)
        {
            var result = await TokenProvider.RequestAccessToken(
                new AccessTokenRequestOptions()
                {
                    Scopes = 
                        config.GetSection("MicrosoftGraph:Scopes").Get<string[]>() ??
                        [ "user.read" ]
                });

            if (result.TryGetToken(out var token))
            {
                request.Headers.Add("Authorization", 
                    $"{CoreConstants.Headers.Bearer} {token.Value}");
            }
        }
    }
}
```

Important

See the [`DefaultAccessTokenScopes` versus `AdditionalScopesToConsent`](#defaultaccesstokenscopes-versus-additionalscopestoconsent) section for an explanation on why the preceding code uses [DefaultAccessTokenScopes](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.defaultaccesstokenscopes) to add the scopes rather than [AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent).

In the `Program` file, add the Graph client services and configuration with the `AddGraphClient` extension method. The following code defaults to the Version 1.0 Microsoft Graph base address and `User.Read` scopes if these settings aren't found in the app settings file:

```
var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);
```

## Call Graph API from a component using the Graph SDK

The following `UserData` component uses an injected `GraphServiceClient` to obtain the user's ME-ID profile data and display their mobile phone number.

For any test user that you create in ME-ID, make sure that you give the user's ME-ID profile a mobile phone number in the Azure portal.

`UserData.razor`:

```
@page "/user-data"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@attribute [Authorize]
@inject GraphServiceClient Client

<PageTitle>User Data</PageTitle>

<h1>Microsoft Graph User Data</h1>

@if (!string.IsNullOrEmpty(user?.MobilePhone))
{
    <p>Mobile Phone: @user.MobilePhone</p>
}

@code {
    private Microsoft.Graph.Models.User? user;

    protected override async Task OnInitializedAsync()
    {
        user = await Client.Me.GetAsync();
    }
}
```

Add a link to the component's page in the `NavMenu` component (`Layout/NavMenu.razor`):

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-data">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Data
    </NavLink>
</div>
```

Tip

To add users to an app, see the [Assign users to an app registration with or without app roles](#assign-users-to-an-app-registration-with-or-without-app-roles) section.

When testing with the Graph SDK locally, we recommend using a new InPrivate/incognito browser session for each test to prevent lingering cookies from interfering with tests. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0#cookies-and-site-data).

## Customize user claims using the Graph SDK

In the following example, the app creates mobile phone number and office location claims for a user from their ME-ID user profile's data. The app must have the `User.Read` Graph API scope configured in ME-ID. Any test users for this scenario must have a mobile phone number and office location in their ME-ID profile, which can be added via the Azure portal.

In the following custom user account factory:

* An [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) (`logger`) is included for convenience in case you wish to log information or errors in the `CreateUserAsync` method.
* In the event that an [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) is thrown, the user is redirected to the identity provider to sign into their account. Additional or different actions can be taken when requesting an access token fails. For example, the app can log the [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) and create a support ticket for further investigation.
* The framework's [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) represents the user's account. If the app requires a custom user account class that extends [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount), swap your custom user account class for [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) in the following code.

`CustomAccountFactory.cs`:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor,
        IServiceProvider serviceProvider, ILogger<CustomAccountFactory> logger,
        IConfiguration config) 
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IServiceProvider serviceProvider = serviceProvider;
    private readonly string? baseUrl = string.Join("/",
        config.GetSection("MicrosoftGraph")["BaseUrl"] ?? 
            "https://graph.microsoft.com",
        config.GetSection("MicrosoftGraph")["Version"] ??
            "v1.0");

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity is not null &&
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null && !string.IsNullOrEmpty(baseUrl))
            {
                try
                {
                    var client = new GraphServiceClient(
                        new HttpClient(),
                        serviceProvider
                            .GetRequiredService<IAuthenticationProvider>(),
                        baseUrl);

                    var user = await client.Me.GetAsync();

                    if (user is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            user.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            user.OfficeLocation ?? "Not set"));
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}
```

Configure the MSAL authentication to use the custom user account factory.

Confirm that the `Program` file uses the [Microsoft.AspNetCore.Components.WebAssembly.Authentication](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication) namespace:

```
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
```

The example in this section builds on the approach of reading the base URL with version and scopes from app configuration via the `MicrosoftGraph` section in `wwwroot/appsettings.json` file. The following lines should already be present in the `Program` file from following the guidance earlier in this article:

```
var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);
```

In the `Program` file, find the call to the [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) extension method. Update the code to the following, which includes a call to [AddAccountClaimsPrincipalFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.remoteauthenticationbuilderextensions.addaccountclaimsprincipalfactory) that adds an account claims principal factory with the `CustomAccountFactory`.

If the app uses a custom user account class that extends [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount), swap the custom user account class for [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) in the following code.

```
builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount,
        CustomAccountFactory>();
```

You can use the following `UserClaims` component to study the user's claims after the user authenticates with ME-ID:

`UserClaims.razor`:

```
@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}
else
{
    <p>No claims found.</p>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}
```

Add a link to the component's page in the `NavMenu` component (`Layout/NavMenu.razor`):

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-claims">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
    </NavLink>
</div>
```

When testing with the Graph SDK locally, we recommend using a new InPrivate/incognito browser session for each test to prevent lingering cookies from interfering with tests. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0#cookies-and-site-data).

*The following guidance applies to Microsoft Graph v4. If you're upgrading an app from SDK v4 to v5 or later, see the following resources:*

* *[Microsoft Graph .NET SDK v5 changelog and upgrade guide](https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/main/docs/upgrade-to-v5.md)*
* *[Microsoft Graph .NET SDK releases (`microsoftgraph/msgraph-sdk-dotnet` GitHub repository)](https://github.com/microsoftgraph/msgraph-sdk-dotnet/releases) (see the 6.0.0 breaking changes remarks)*

The Microsoft Graph SDK for use in Blazor apps is called the *Microsoft Graph .NET Client Library*.

The Graph SDK examples require the following package references in the standalone Blazor WebAssembly app. The first two packages are already referenced if the app has been enabled for MSAL authentication, for example when creating the app by following the guidance in [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0).

The Graph SDK examples require the following package references in the standalone Blazor WebAssembly app or the **Client** app of a hosted Blazor WebAssembly solution. The first two packages are already referenced if the app has been enabled for MSAL authentication, for example when creating the app by following the guidance in [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0).

* [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication)
* [`Microsoft.Authentication.WebAssembly.Msal`](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal)
* [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http)
* [`Microsoft.Graph`](https://www.nuget.org/packages/Microsoft.Graph)

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the Azure portal, grant delegated permissions (scopes)† for Microsoft Graph data that the app should be able to access on behalf of a user. For the example in this article, the app's registration should include delegated permission to read user data (`Microsoft.Graph` > `User.Read` scope in **API permissions**, Type: Delegated). The `User.Read` scope allows users to sign in to the app and allows the app to read the profile and company information of signed-in users. For more information, see [Overview of permissions and consent in the Microsoft identity platform](/en-us/entra/identity-platform/permissions-consent-overview) and [Overview of Microsoft Graph permissions](/en-us/graph/permissions-overview).

†*Permissions* and *scopes* mean the same thing and are used interchangeably in security documentation and the Azure portal. Unless the text is referring to the Azure portal, this article uses *scope*/*scopes* when referring to Graph permissions.

Scopes are case insensitive, so `User.Read` is the same as `user.read`. Feel free to use either format, but we recommend a consistent choice across application code.

After adding the Microsoft Graph API scopes to the app's registration in the Azure portal, add the following app settings configuration to the `wwwroot/appsettings.json` file in the app, which includes the Graph base URL with the Microsoft Graph version and scopes. In the following example, the `User.Read` scope is specified for the examples in later sections of this article. Scopes aren't case sensitive.

```
"MicrosoftGraph": {
  "BaseUrl": "https://graph.microsoft.com",
  "Version": "{VERSION}",
  "Scopes": [
    "user.read"
  ]
}
```

In the preceding example, the `{VERSION}` placeholder is the version of the Microsoft Graph API (for example: `v1.0`).

The following is an example of a complete `wwwroot/appsettings.json` configuration file for an app that uses ME-ID as its identity provider, where reading user data (`user.read` scope) is specified for Microsoft Graph:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "user.read"
    ]
  }
}
```

In the preceding example, the `{TENANT ID}` placeholder is the Directory (tenant) ID, and the `{CLIENT ID}` placeholder is the Application (client) ID. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0).

Add the following `GraphClientExtensions` class to the standalone app. The scopes are provided to the [Scopes](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenrequestoptions.scopes#microsoft-aspnetcore-components-webassembly-authentication-accesstokenrequestoptions-scopes) property of the [AccessTokenRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenrequestoptions) in the `AuthenticateRequestAsync` method. The [IHttpProvider.OverallTimeout](/en-us/dotnet/api/microsoft.graph.ihttpprovider.overalltimeout#microsoft-graph-ihttpprovider-overalltimeout) is extended from the default value of 100 seconds to 300 seconds to give the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) more time to receive a response from Microsoft Graph.

Add the following `GraphClientExtensions` class to the standalone app or **Client** app of a hosted Blazor WebAssembly [solution](../../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln). The scopes are provided to the [Scopes](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenrequestoptions.scopes#microsoft-aspnetcore-components-webassembly-authentication-accesstokenrequestoptions-scopes) property of the [AccessTokenRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenrequestoptions) in the `AuthenticateRequestAsync` method. The [IHttpProvider.OverallTimeout](/en-us/dotnet/api/microsoft.graph.ihttpprovider.overalltimeout#microsoft-graph-ihttpprovider-overalltimeout) is extended from the default value of 100 seconds to 300 seconds to give the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) more time to receive a response from Microsoft Graph.

When an access token isn't obtained, the following code doesn't set a Bearer authorization header for Graph requests.

`GraphClientExtensions.cs`:

```
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Graph;

namespace BlazorSample;

internal static class GraphClientExtensions
{
    public static IServiceCollection AddGraphClient(
        this IServiceCollection services, string? baseUrl, List<string>? scopes)
    {
        if (string.IsNullOrEmpty(baseUrl) || scopes?.Count == 0)
        {
            return services;
        }

        services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(
            options =>
            {
                scopes?.ForEach((scope) =>
                {
                    options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
                });
            });

        services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();

        services.AddScoped<IHttpProvider, HttpClientHttpProvider>(sp =>
            new HttpClientHttpProvider(new HttpClient()));

        services.AddScoped(sp =>
        {
            return new GraphServiceClient(
                baseUrl,
                sp.GetRequiredService<IAuthenticationProvider>(),
                sp.GetRequiredService<IHttpProvider>());
        });

        return services;
    }

    private class GraphAuthenticationProvider(IAccessTokenProvider tokenProvider, 
        IConfiguration config) : IAuthenticationProvider
    {
        private readonly IConfiguration config = config;

        public IAccessTokenProvider TokenProvider { get; } = tokenProvider;

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            var result = await TokenProvider.RequestAccessToken(
                new AccessTokenRequestOptions()
                { 
                    Scopes = config.GetSection("MicrosoftGraph:Scopes").Get<string[]>()
                });

            if (result.TryGetToken(out var token))
            {
                request.Headers.Authorization ??= new AuthenticationHeaderValue(
                    "Bearer", token.Value);
            }
        }
    }

    private class HttpClientHttpProvider(HttpClient client) : IHttpProvider
    {
        private readonly HttpClient client = client;

        public ISerializer Serializer { get; } = new Serializer();

        public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
        {
            return client.SendAsync(request);
        }

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            HttpCompletionOption completionOption,
            CancellationToken cancellationToken)
        {
            return client.SendAsync(request, completionOption, cancellationToken);
        }

        public void Dispose()
        {
        }
    }
}
```

Important

See the [`DefaultAccessTokenScopes` versus `AdditionalScopesToConsent`](#defaultaccesstokenscopes-versus-additionalscopestoconsent) section for an explanation on why the preceding code uses [DefaultAccessTokenScopes](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.defaultaccesstokenscopes) to add the scopes rather than [AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent).

In the `Program` file, add the Graph client services and configuration with the `AddGraphClient` extension method:

```
var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);
```

## Call Graph API from a component using the Graph SDK

The following `UserData` component uses an injected `GraphServiceClient` to obtain the user's ME-ID profile data and display their mobile phone number. For any test user that you create in ME-ID, make sure that you give the user's ME-ID profile a mobile phone number in the Azure portal.

`UserData.razor`:

```
@page "/user-data"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@attribute [Authorize]
@inject GraphServiceClient Client

<PageTitle>User Data</PageTitle>

<h1>Microsoft Graph User Data</h1>

@if (!string.IsNullOrEmpty(user?.MobilePhone))
{
    <p>Mobile Phone: @user.MobilePhone</p>
}

@code {
    private Microsoft.Graph.User? user;

    protected override async Task OnInitializedAsync()
    {
        var request = Client.Me.Request();
        user = await request.GetAsync();
    }
}
```

Add a link to the component's page in the `NavMenu` component (`Layout/NavMenu.razor`):

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-data">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Data
    </NavLink>
</div>
```

Tip

To add users to an app, see the [Assign users to an app registration with or without app roles](#assign-users-to-an-app-registration-with-or-without-app-roles) section.

When testing with the Graph SDK locally, we recommend using a new InPrivate/incognito browser session for each test to prevent lingering cookies from interfering with tests. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0#cookies-and-site-data).

## Customize user claims using the Graph SDK

In the following example, the app creates mobile phone number and office location claims for a user from their ME-ID user profile's data. The app must have the `User.Read` Graph API scope configured in ME-ID. Any test users for this scenario must have a mobile phone number and office location in their ME-ID profile, which can be added via the Azure portal.

In the following custom user account factory:

* An [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) (`logger`) is included for convenience in case you wish to log information or errors in the `CreateUserAsync` method.
* In the event that an [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) is thrown, the user is redirected to the identity provider to sign into their account. Additional or different actions can be taken when requesting an access token fails. For example, the app can log the [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) and create a support ticket for further investigation.
* The framework's [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) represents the user's account. If the app requires a custom user account class that extends [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount), swap your custom user account class for [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) in the following code.

`CustomAccountFactory.cs`:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor, 
        IServiceProvider serviceProvider, ILogger<CustomAccountFactory> logger)
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IServiceProvider serviceProvider = serviceProvider;

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity is not null && 
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null)
            {
                try
                {
                    var client = ActivatorUtilities
                        .CreateInstance<GraphServiceClient>(serviceProvider);
                    var request = client.Me.Request();
                    var user = await request.GetAsync();

                    if (user is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            user.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            user.OfficeLocation ?? "Not set"));
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}
```

Configure the MSAL authentication to use the custom user account factory.

Confirm that the `Program` file uses the [Microsoft.AspNetCore.Components.WebAssembly.Authentication](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication) namespace:

```
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
```

The example in this section builds on the approach of reading the base URL with version and scopes from app configuration via the `MicrosoftGraph` section in `wwwroot/appsettings.json` file. The following lines should already be present in the `Program` file from following the guidance earlier in this article:

```
var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);
```

In the `Program` file, find the call to the [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) extension method. Update the code to the following, which includes a call to [AddAccountClaimsPrincipalFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.remoteauthenticationbuilderextensions.addaccountclaimsprincipalfactory) that adds an account claims principal factory with the `CustomAccountFactory`.

If the app uses a custom user account class that extends [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount), swap the custom user account class for [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) in the following code.

```
builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount,
        CustomAccountFactory>();
```

You can use the following `UserClaims` component to study the user's claims after the user authenticates with ME-ID:

`UserClaims.razor`:

```
@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}
else
{
    <p>No claims found.</p>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}
```

Add a link to the component's page in the `NavMenu` component (`Layout/NavMenu.razor`):

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-claims">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
    </NavLink>
</div>
```

When testing with the Graph SDK locally, we recommend using a new InPrivate/incognito browser session for each test to prevent lingering cookies from interfering with tests. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0#cookies-and-site-data).

The following examples use a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) for Graph API calls to obtain a user's mobile phone number to process a call or to customize a user's claims to include a mobile phone number claim and an office location claim.

The examples require a package reference for [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http) for the standalone Blazor WebAssembly app.

The examples require a package reference for [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http) for the standalone Blazor WebAssembly app or the **Client** app of a hosted Blazor WebAssembly solution.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the Azure portal, grant delegated permissions (scopes)† for Microsoft Graph data that the app should be able to access on behalf of a user. For the example in this article, the app's registration should include delegated permission to read user data (`Microsoft.Graph` > `User.Read` scope in **API permissions**, Type: Delegated). The `User.Read` scope allows users to sign in to the app and allows the app to read the profile and company information of signed-in users. For more information, see [Overview of permissions and consent in the Microsoft identity platform](/en-us/entra/identity-platform/permissions-consent-overview) and [Overview of Microsoft Graph permissions](/en-us/graph/permissions-overview).

†*Permissions* and *scopes* mean the same thing and are used interchangeably in security documentation and the Azure portal. Unless the text is referring to the Azure portal, this article uses *scope*/*scopes* when referring to Graph permissions.

Scopes are case insensitive, so `User.Read` is the same as `user.read`. Feel free to use either format, but we recommend a consistent choice across application code.

After adding the Microsoft Graph API scopes to the app's registration in the Azure portal, add the following app settings configuration to the `wwwroot/appsettings.json` file in the app, which includes the Graph base URL with the Microsoft Graph version and scopes. In the following example, the `User.Read` scope is specified for the examples in later sections of this article. Scopes aren't case sensitive.

```
"MicrosoftGraph": {
  "BaseUrl": "https://graph.microsoft.com",
  "Version": "{VERSION}",
  "Scopes": [
    "user.read"
  ]
}
```

In the preceding example, the `{VERSION}` placeholder is the version of the Microsoft Graph API (for example: `v1.0`).

The following is an example of a complete `wwwroot/appsettings.json` configuration file for an app that uses ME-ID as its identity provider, where reading user data (`user.read` scope) is specified for Microsoft Graph:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "user.read"
    ]
  }
}
```

In the preceding example, the `{TENANT ID}` placeholder is the Directory (tenant) ID, and the `{CLIENT ID}` placeholder is the Application (client) ID. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0).

Create the following `GraphAuthorizationMessageHandler` class and project configuration in the `Program` file for working with Graph API. The base URL and scopes are provided to the handler from configuration.

`GraphAuthorizationMessageHandler.cs`:

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

namespace BlazorSample;

public class GraphAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public GraphAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigation, IConfiguration config)
        : base(provider, navigation)
    {
        ConfigureHandler(
            authorizedUrls: [ 
                string.Join("/",
                    config.GetSection("MicrosoftGraph")["BaseUrl"] ??
                        "https://graph.microsoft.com",
                    config.GetSection("MicrosoftGraph")["Version"] ??
                        "v1.0")
            ],
            scopes: config.GetSection("MicrosoftGraph:Scopes")
                        .Get<List<string>>() ?? [ "user.read" ]);
    }
}
```

The trailing slash (`/`) of the authorized URL is required. The preceding code builds the following authorized URL from app settings configuration or defaults to the following authorized URL if the app settings configuration is missing: `https://graph.microsoft.com/v1.0/`.

In the `Program` file, configure the named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) for Graph API:

```
builder.Services.AddTransient<GraphAuthorizationMessageHandler>();

builder.Services.AddHttpClient("GraphAPI",
        client => client.BaseAddress = new Uri(
            string.Join("/",
                builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
                    "https://graph.microsoft.com",
                builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
                    "v1.0",
                string.Empty)))
    .AddHttpMessageHandler<GraphAuthorizationMessageHandler>();
```

In the preceding example, the `GraphAuthorizationMessageHandler` [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) is registered as a transient service for [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler). Transient registration is recommended for [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory), which manages its own DI scopes. For more information, see the following resources:

* [Utility base component classes to manage a DI scope](../../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope)
* [Detect client-side transient disposables](../../fundamentals/dependency-injection?view=aspnetcore-10.0#detect-client-side-transient-disposables)
* [`DelegatingHandler` instances](../../../fundamentals/http-requests?view=aspnetcore-10.0#outgoing-request-middleware)

A trailing slash (`/`) on the base address is required. In the preceding code, the third argument to `string.Join` is `string.Empty` to ensure the trailing slash is present: `https://graph.microsoft.com/v1.0/`.

## Call Graph API from a component using a named `HttpClient`

The `UserInfo.cs` class designates the required user profile properties with the [JsonPropertyNameAttribute](/en-us/dotnet/api/system.text.json.serialization.jsonpropertynameattribute) attribute and the JSON name used by ME-ID. The following example sets up properties for the user's mobile phone number and office location.

`UserInfo.cs`:

```
using System.Text.Json.Serialization;

namespace BlazorSample;

public class UserInfo
{
    [JsonPropertyName("mobilePhone")]
    public string? MobilePhone { get; set; }

    [JsonPropertyName("officeLocation")]
    public string? OfficeLocation { get; set; }
}
```

In the following `UserData` component, an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is created for Graph API to issue a request for the user's profile data. The `me` resource (`me`) are added to the base URL with version for the Graph API request. JSON data returned by Graph is deserialized into the `UserInfo` class properties. In the following example, the mobile phone number is obtained. You can add similar code to include the user's ME-ID profile office location if you wish (`userInfo.OfficeLocation`). If the access token request fails, the user is redirected to sign into the app for a new access token.

`UserData.razor`:

```
@page "/user-data"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@attribute [Authorize]
@inject IConfiguration Config
@inject IHttpClientFactory ClientFactory

<PageTitle>User Data</PageTitle>

<h1>Microsoft Graph User Data</h1>

@if (!string.IsNullOrEmpty(userInfo?.MobilePhone))
{
    <p>Mobile Phone: @userInfo.MobilePhone</p>
}

@code {
    private UserInfo? userInfo;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            var client = ClientFactory.CreateClient("GraphAPI");

            userInfo = await client.GetFromJsonAsync<UserInfo>("me");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}
```

Add a link to the component's page in the `NavMenu` component (`Layout/NavMenu.razor`):

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-data">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Data
    </NavLink>
</div>
```

Tip

To add users to an app, see the [Assign users to an app registration with or without app roles](#assign-users-to-an-app-registration-with-or-without-app-roles) section.

The following sequence describes the new user flow for Graph API scopes:

1. The new user signs into the app for the first time.
2. The user consents to using the app in the Azure consent UI.
3. The user accesses a component page that requests Graph API data for the first time.
4. The user is redirected to the Azure consent UI to consent to Graph API scopes.
5. Graph API user data is returned.

If you prefer that scope provisioning (consent for Graph API scopes) take place on the initial sign in, supply the scopes to MSAL authentication as default access token scopes in the `Program` file:

```
+ var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
+     .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);

+   foreach (var scope in scopes)
+   {
+       options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
+   }
});
```

Important

See the [`DefaultAccessTokenScopes` versus `AdditionalScopesToConsent`](#defaultaccesstokenscopes-versus-additionalscopestoconsent) section for an explanation on why the preceding code uses [DefaultAccessTokenScopes](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.defaultaccesstokenscopes) to add the scopes rather than [AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent).

When the preceding changes are made to the app, the user flow adopts the following sequence:

1. The new user signs into the app for the first time.
2. The user consents to using the app and Graph API scopes in the Azure consent UI.
3. The user accesses a component page that requests Graph API data for the first time.
4. Graph API user data is returned.

When testing with the Graph API locally, we recommend using a new InPrivate/incognito browser session for each test to prevent lingering cookies from interfering with testing. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0#troubleshoot).

## Customize user claims using a named `HttpClient`

In the following example, the app creates mobile phone number and office location claims for the user from their ME-ID user profile's data. The app must have the `User.Read` Graph API scope configured in ME-ID. Test user accounts in ME-ID require an entry for the mobile phone number and office location, which can be added via the Azure portal to their user profiles.

If you haven't already added the `UserInfo` class to the app by following the guidance earlier in this article, add the following class and designate the required user profile properties with the [JsonPropertyNameAttribute](/en-us/dotnet/api/system.text.json.serialization.jsonpropertynameattribute) attribute and the JSON name used by ME-ID. The following example sets up properties for the user's mobile phone number and office location.

`UserInfo.cs`:

```
using System.Text.Json.Serialization;

namespace BlazorSample;

public class UserInfo
{
    [JsonPropertyName("mobilePhone")]
    public string? MobilePhone { get; set; }

    [JsonPropertyName("officeLocation")]
    public string? OfficeLocation { get; set; }
}
```

In the following custom user account factory:

* An [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) (`logger`) is included for convenience in case you wish to log information or errors in the `CreateUserAsync` method.
* In the event that an [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) is thrown, the user is redirected to the identity provider to sign into their account. Additional or different actions can be taken when requesting an access token fails. For example, the app can log the [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) and create a support ticket for further investigation.
* The framework's [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) represents the user's account. If the app requires a custom user account class that extends [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount), swap the custom user account class for [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) in the following code.

`CustomAccountFactory.cs`:

```
using System.Net.Http.Json;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor,
        IHttpClientFactory clientFactory,
        ILogger<CustomAccountFactory> logger)
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IHttpClientFactory clientFactory = clientFactory;

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity is not null && 
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null)
            {
                try
                {
                    var client = clientFactory.CreateClient("GraphAPI");

                    var userInfo = await client.GetFromJsonAsync<UserInfo>("me");

                    if (userInfo is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            userInfo.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            userInfo.OfficeLocation ?? "Not set"));
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}
```

The MSAL authentication is configured to use the custom user account factory. Start by confirming that the `Program` file uses the [Microsoft.AspNetCore.Components.WebAssembly.Authentication](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication) namespace:

```
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
```

In the `Program` file, find the call to the [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) extension method. Update the code to the following, which includes a call to [AddAccountClaimsPrincipalFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.remoteauthenticationbuilderextensions.addaccountclaimsprincipalfactory) that adds an account claims principal factory with the `CustomAccountFactory`.

If the app uses a custom user account class that extends [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount), swap your app's custom user account class for [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) in the following code.

```
builder.Services.AddMsalAuthentication<RemoteAuthenticationState, 
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount, 
        CustomAccountFactory>();
```

The preceding example is for an app that uses ME-ID authentication with MSAL. Similar patterns exist for OIDC and API authentication. For more information, see the examples in the [Customize the user with a payload claim](additional-scenarios?view=aspnetcore-10.0#customize-the-user-with-a-payload-claim) section of the [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0) article.

You can use the following `UserClaims` component to study the user's claims after the user authenticates with ME-ID:

`UserClaims.razor`:

```
@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}
else
{
    <p>No claims found.</p>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}
```

Add a link to the component's page in the `NavMenu` component (`Layout/NavMenu.razor`):

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-claims">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
    </NavLink>
</div>
```

When testing with the Graph API locally, we recommend using a new InPrivate/incognito browser session for each test to prevent lingering cookies from interfering with testing. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0#troubleshoot).

## Assign users to an app registration with or without app roles

You can add users to an app registration and assign roles to users with the following steps in the Azure portal.

To add a user, select **Users** from the ME-ID area of the Azure portal:

1. Select **New user** > **Create new user**.
2. Use the **Create user** template.
3. Supply the user's information in the **Identity** area.
4. You can generate an initial password or assign an initial password that the user changes when they sign in for the first time. If you use the password generated by the portal, make a note of it now.
5. Select **Create** to create the user. When **Create new user** interface closes, select **Refresh** to update the user list and show the new user.
6. For the examples in this article, assign a mobile phone number to the new user by selecting their name from the users list, selecting **Properties**, and editing the contact information to provide a mobile phone number.

To assign users to the app *without app roles*:

1. In the ME-ID area of the Azure portal, open **Enterprise applications**.
2. Select the app from the list.
3. Select **Users and groups**.
4. Select **Add user/group**.
5. Select a user.
6. Select the **Assign** button.

To assign users to the app *with app roles*:

1. Add roles to the app's registration in the Azure portal following the guidance in [ASP.NET Core Blazor WebAssembly with Microsoft Entra ID groups and roles](microsoft-entra-id-groups-and-roles?view=aspnetcore-10.0#app-roles).
2. In the ME-ID area of the Azure portal, open **Enterprise applications**.
3. Select the app from the list.
4. Select **Users and groups**.
5. Select **Add user/group**.
6. Select a user and select their role for accessing the app. Multiple roles are assigned to a user by repeating the process of adding the user to the app until all of the roles for a user are assigned. Users with multiple roles are listed once for each assigned role in the **Users and groups** list of users for the app.
7. Select the **Assign** button.

## `DefaultAccessTokenScopes` versus `AdditionalScopesToConsent`

The examples in this article provision Graph API scopes with [DefaultAccessTokenScopes](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.defaultaccesstokenscopes), not [AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent).

[AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent) isn't used because it's unable to provision Graph API scopes for users when they sign in to the app for the first time with MSAL via the Azure consent UI. When the user attempts to access Graph API for the first time with the Graph SDK, they're confronted with an exception:

> Microsoft.Graph.Models.ODataErrors.ODataError: Access token is empty.

After a user provisions Graph API scopes provided via [DefaultAccessTokenScopes](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.defaultaccesstokenscopes), the app can use [AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent) for a subsequent user sign in. However, changing app code makes no sense for a production app that requires the periodic addition of new users with delegated Graph scopes or adding new delegated Graph API scopes to the app.

The preceding discussion of how to provision scopes for Graph API access when the user first signs into the app only applies to:

* Apps that adopt the Graph SDK.
* Apps that use a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) for Graph API access that asks users to consent to Graph scopes on their first sign in to the app.

When using a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) that doesn't ask users to consent to Graph scopes on their first sign in, users are redirected to the Azure consent UI for Graph API scopes consent *when they first request access to Graph API* via the [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) of the preconfigured, named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient). When Graph scopes aren't consented initially with the named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) approach, neither [DefaultAccessTokenScopes](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.defaultaccesstokenscopes) nor [AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent) are called by the app. For more information, see the [named `HttpClient` coverage in this article](?view=aspnetcore-10.0\&pivots=named-client-graph-api).

## Hosted Blazor WebAssembly solutions

The examples in this article pertain to using the Graph SDK or a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) with Graph API directly from a standalone Blazor WebAssembly app or directly from the **Client** app of a hosted Blazor WebAssembly [solution](../../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln). An additional scenario that isn't covered by this article is for a **Client** app of a hosted solution to call the **Server** app of the solution via web API, and then the **Server** app uses the Graph SDK/API to call Microsoft Graph and return data to the **Client** app. Although this is a supported approach, it isn't covered by this article. If you wish to adopt this approach:

* Follow the guidance in [Call a web API from an ASP.NET Core Blazor app](../../call-web-api?view=aspnetcore-10.0) for the web API aspects on issuing requests to the **Server** app from the **Client** app and returning data to the **Client** app.
* Follow the guidance in the primary [Microsoft Graph documentation](/en-us/graph/) to use the Graph SDK with a typical ASP.NET Core app, which in this scenario is the **Server** app of the solution. If you use the Blazor WebAssembly project template to the create the hosted Blazor WebAssembly solution (**ASP.NET Core Hosted**/`-h|--hosted`) with organizational authorization (single organization/`SingleOrg` or multiple organization/`MultiOrg`) and the Microsoft Graph option (**Microsoft identity platform** > **Connected Services** > **Add Microsoft Graph permissions** in Visual Studio or the `--calls-graph` option with the .NET CLI `dotnet new` command), the **Server** app of the solution is configured to use the Graph SDK when the solution is created from the project template.

## Related content

### General guidance

* [Microsoft Graph documentation](/en-us/graph/)
* [Microsoft Graph sample Blazor WebAssembly app](https://github.com/microsoftgraph/msgraph-sample-blazor-clientside): This sample demonstrates how to use the Microsoft Graph .NET SDK to access data in Office 365 from Blazor WebAssembly apps.
* [Build .NET apps with Microsoft Graph tutorial](/en-us/graph/tutorials/dotnet?tabs=aad) and [Microsoft Graph sample ASP.NET Core app](https://github.com/microsoftgraph/msgraph-sample-aspnet-core/tree/main/): Although these resources don't directly apply to calling Graph from *client-side* Blazor WebAssembly apps, the ME-ID app configuration and Microsoft Graph coding practices in the linked resources are relevant for standalone Blazor WebAssembly apps and should be consulted for general best practices.

- [Microsoft Graph documentation](/en-us/graph/)
- [Microsoft Graph sample Blazor WebAssembly app](https://github.com/microsoftgraph/msgraph-sample-blazor-clientside): This sample demonstrates how to use the Microsoft Graph .NET SDK to access data in Office 365 from Blazor WebAssembly apps.
- [Build .NET apps with Microsoft Graph tutorial](/en-us/graph/tutorials/dotnet?tabs=aad) and [Microsoft Graph sample ASP.NET Core app](https://github.com/microsoftgraph/msgraph-sample-aspnet-core/tree/main/): These resources are most appropriate for ***hosted*** Blazor WebAssembly solutions, where the **Server** app is configured to access Microsoft Graph as a typical ASP.NET Core app on behalf of the **Client** app. The **Client** app uses web API to make requests to the **Server** app for Graph data. Although these resources don't directly apply to calling Graph from *client-side* Blazor WebAssembly apps, the ME-ID app configuration and Microsoft Graph coding practices in the linked resources are relevant for standalone Blazor WebAssembly apps and should be consulted for general best practices.

### Security guidance

* [Microsoft Graph auth overview](/en-us/graph/auth/)
* [Overview of Microsoft Graph permissions](/en-us/graph/permissions-overview)
* [Microsoft Graph permissions reference](/en-us/graph/permissions-reference)
* [Overview of permissions and consent in the Microsoft identity platform](/en-us/entra/identity-platform/permissions-consent-overview)
* [Enhance security with the principle of least privilege](/en-us/entra/identity-platform/secure-least-privileged-access)
* [Azure privilege escalation articles on the Internet (Google search result)](https://www.google.com/search?q=%22Azure+Privilege+Escalation%22)
* [Microsoft Security Best Practices: Securing privileged access](/en-us/security/privileged-access-workstations/overview)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-10

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-identity/qrcodes-for-authenticator-apps
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/standalone-with-identity/qrcodes-for-authenticator-apps.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Enable QR code generation for TOTP authenticator apps in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure an ASP.NET Core Blazor WebAssembly app with Identity for two-factor authentication (2FA) with QR codes generated by Time-based One-time Password Algorithm (TOTP) authenticator apps.

For an introduction to 2FA with TOTP authenticator apps, see [Enable QR code generation for TOTP authentication](../../../../security/authentication/identity-enable-qrcodes?view=aspnetcore-10.0).

Warning

TOTP codes should be kept secret because they can be used to authenticate multiple times before they expire.

## Namespaces and article code examples

The namespaces used by the examples in this article are:

* `Backend` for the backend server web API project, described as the "server project" in this article.
* `BlazorWasmAuth` for the front-end client standalone Blazor WebAssembly app, described as the "client project" in this article.

These namespaces correspond to the projects in the `BlazorWebAssemblyStandaloneWithIdentity` sample solution in the [`dotnet/blazor-samples` GitHub repository](https://github.com/dotnet/blazor-samples). For more information, see [Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](./?view=aspnetcore-10.0#sample-apps).

If you aren't using the `BlazorWebAssemblyStandaloneWithIdentity` sample, change the namespaces in the code examples to use the namespaces of your projects.

All of the changes to the solution covered by this article take place in the `BlazorWasmAuth` project of the `BlazorWebAssemblyStandaloneWithIdentity` solution.

In article examples, code lines are split to reduce horizontal scrolling. These breaks don't affect execution but can be removed when pasting into your project.

## Optional account confirmation and password recovery

Although apps that implement 2FA usually adopt account confirmation and password recovery features, 2FA doesn't require it. The guidance in this article can be followed to implement 2FA without following the guidance in [Account confirmation and password recovery in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](account-confirmation-and-password-recovery?view=aspnetcore-10.0).

## Add a QR code library to the app

A QR code generated by the app to set up 2FA with an TOTP authenticator app must be generated by a QR code library.

The guidance in this article uses [`manuelbl/QrCodeGenerator`](https://github.com/manuelbl/QrCodeGenerator), but you can use any QR code generation library.

Add a package reference to the client project for the [`Net.Codecrete.QrCodeGenerator`](https://www.nuget.org/packages/Net.Codecrete.QrCodeGenerator) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

## Set the TOTP organization name

Set the site name in the app settings file of the client project. Use a meaningful site name that users can identify easily in their authenticator app. Developers usually set a site name that matches the company's name. We recommend limiting the site name length to 30 characters or less to allow the site name to display on narrow mobile device screens.

In the following example, the company name is `Weyland-Yutani Corporation` (©1986 20th Century Studios [*Aliens*](https://www.20thcenturystudios.com/movies/aliens)).

Added to `wwwroot/appsettings.json`:

```
"TotpOrganizationName": "Weyland-Yutani Corporation"
```

The app settings file after the TOTP organization name configuration is added:

```
{
  "BackendUrl": "https://localhost:7211",
  "FrontendUrl": "https://localhost:7171",
  "TotpOrganizationName": "Weyland-Yutani Corporation"
}
```

## Add model classes

Add the following `LoginResponse` class to the `Models` folder. This class is populated for requests to the `/login` endpoint of [MapIdentityApi](/en-us/dotnet/api/microsoft.aspnetcore.routing.identityapiendpointroutebuilderextensions.mapidentityapi) in the server app.

`Identity/Models/LoginResponse.cs`:

```
namespace BlazorWasmAuth.Identity.Models;

public class LoginResponse
{
    public string? Type { get; set; }
    public string? Title { get; set; }
    public int Status { get; set; }
    public string? Detail { get; set; }
}
```

Add the following `TwoFactorRequest` class to the `Models` folder. This class is populated for requests to the `/manage/2fa` endpoint of [MapIdentityApi](/en-us/dotnet/api/microsoft.aspnetcore.routing.identityapiendpointroutebuilderextensions.mapidentityapi) in the server app.

`Identity/Models/TwoFactorRequest.cs`:

```
namespace BlazorWasmAuth.Identity.Models;

public class TwoFactorRequest
{
    public bool? Enable { get; set; }
    public string? TwoFactorCode { get; set; }
    public bool? ResetSharedKey { get; set; }
    public bool? ResetRecoveryCodes { get; set; }
    public bool? ForgetMachine {  get; set; }
}
```

Add the following `TwoFactorResponse` class to the `Models` folder. This class is populated by the response to a 2FA request made to the `/manage/2fa` endpoint of [MapIdentityApi](/en-us/dotnet/api/microsoft.aspnetcore.routing.identityapiendpointroutebuilderextensions.mapidentityapi) in the server app.

`Identity/Models/TwoFactorResponse.cs`:

```
namespace BlazorWasmAuth.Identity.Models;

public class TwoFactorResponse
{
    public string SharedKey { get; set; } = string.Empty;
    public int RecoveryCodesLeft { get; set; } = 0;
    public string[] RecoveryCodes { get; set; } = [];
    public bool IsTwoFactorEnabled { get; set; }
    public bool IsMachineRemembered { get; set; }
    public string[] ErrorList { get; set; } = [];
}
```

## `IAccountManagement` interface

Add the following class signatures to the `IAccountManagement` interface. The class signatures represent methods added to the cookie authentication state provider for the following client requests:

* Log in with a 2FA TOTP code (`/login` endpoint): `LoginTwoFactorCodeAsync`
* Log in with a 2FA recovery code (`/login` endpoint): `LoginTwoFactorRecoveryCodeAsync`
* Make a 2FA management request (`/manage/2fa` endpoint): `TwoFactorRequestAsync`

`Identity/IAccountManagement.cs` (paste the following code at the bottom of the file):

```
public Task<FormResult> LoginTwoFactorCodeAsync(
    string email, 
    string password, 
    string twoFactorCode);

public Task<FormResult> LoginTwoFactorRecoveryCodeAsync(
    string email, 
    string password, 
    string twoFactorRecoveryCode);

public Task<TwoFactorResponse> TwoFactorRequestAsync(
    TwoFactorRequest twoFactorRequest);
```

## Update the cookie authentication state provider

Update the `CookieAuthenticationStateProvider` with features to add the following features:

* Authenticate users with either a TOTP authenticator app code or a recovery code.
* Manage 2FA in the app.

At the top of the `CookieAuthenticationStateProvider.cs` file, add a `using` statement for [System.Text.Json.Serialization](/en-us/dotnet/api/system.text.json.serialization):

```
using System.Text.Json.Serialization;
```

In the [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions), add the [DefaultIgnoreCondition](/en-us/dotnet/api/system.text.json.jsonserializeroptions.defaultignorecondition#system-text-json-jsonserializeroptions-defaultignorecondition) option set to [JsonIgnoreCondition.WhenWritingNull](/en-us/dotnet/api/system.text.json.serialization.jsonignorecondition#system-text-json-serialization-jsonignorecondition-whenwritingnull), which avoids serializing null properties:

```
private readonly JsonSerializerOptions jsonSerializerOptions =
    new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+       DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    };
```

The `LoginAsync` method is updated with the following logic:

* Attempt a normal login at the `/login` endpoint with an email address and password.
* If the server responds with a success status code, the method returns a `FormResult` with the `Succeeded` property set to `true`.
* If the server responds with the *401 - Unauthorized* status code and a detail code of "`RequiresTwoFactor`," a `FormResult` is returned with `Succeeded` set to `false` and the `RequiresTwoFactor` detail in the error list.

In `Identity/CookieAuthenticationStateProvider.cs`, replace the `LoginAsync` method with the following code:

```
public async Task<FormResult> LoginAsync(string email, string password)
{
    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "login?useCookies=true", new
            {
                email,
                password
            });

        if (result.IsSuccessStatusCode)
        {
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());

            return new FormResult { Succeeded = true };
        }
        else if (result.StatusCode == HttpStatusCode.Unauthorized)
        {
            using var responseJson = await result.Content.ReadAsStringAsync();
            var response = JsonSerializer.Deserialize<LoginResponse>(
                responseJson, jsonSerializerOptions);

            if (response?.Detail == "RequiresTwoFactor")
            {
                return new FormResult
                {
                    Succeeded = false,
                    ErrorList = [ "RequiresTwoFactor" ]
                };
            }
        }
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "App error");
    }

    return new FormResult
    {
        Succeeded = false,
        ErrorList = [ "Invalid email and/or password." ]
    };
}
```

A `LoginTwoFactorCodeAsync` method is added, which sends a request to the `/login` endpoint with a 2FA TOTP code (`twoFactorCode`). The method processes the response in a similar fashion to a normal, non-2FA login request.

Add the following method and class to `Identity/CookieAuthenticationStateProvider.cs` (paste the following code at the bottom of the class file):

```
public async Task<FormResult> LoginTwoFactorCodeAsync(
    string email, string password, string twoFactorCode)
{
    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "login?useCookies=true", new
            {
                email,
                password,
                twoFactorCode
            });

        if (result.IsSuccessStatusCode)
        {
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());

            return new FormResult { Succeeded = true };
        }
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "App error");
    }

    return new FormResult
    {
        Succeeded = false,
        ErrorList = [ "Invalid two-factor code." ]
    };
}
```

A `LoginTwoFactorRecoveryCodeAsync` method is added, which sends a request to the `/login` endpoint with a 2FA recovery code (`twoFactorRecoveryCode`). The method processes the response in a similar fashion to a normal, non-2FA login request.

Add the following method and class to `Identity/CookieAuthenticationStateProvider.cs` (paste the following code at the bottom of the class file):

```
public async Task<FormResult> LoginTwoFactorRecoveryCodeAsync(string email, 
    string password, string twoFactorRecoveryCode)
{
    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "login?useCookies=true", new
            {
                email,
                password,
                twoFactorRecoveryCode
            });

        if (result.IsSuccessStatusCode)
        {
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());

            return new FormResult { Succeeded = true };
        }
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "App error");
    }

    return new FormResult
    {
        Succeeded = false,
        ErrorList = [ "Invalid recovery code." ]
    };
}
```

A `TwoFactorRequestAsync` method is added, which is used to manage 2FA for the user:

* Reset the shared 2FA key when `TwoFactorRequest.ResetSharedKey` is `true`. Resetting the shared key implicitly disables 2FA. This forces the user to prove that they can provide a valid TOTP code from their authenticator app to enable 2FA after receiving a new shared key.
* Reset the user's recovery codes when `TwoFactorRequest.ResetRecoveryCodes` is `true`.
* Forget the machine when `TwoFactorRequest.ForgetMachine` is `true`, which means that a new 2FA TOTP code is required on the next login attempt.
* Enable 2FA using a TOTP code from a TOTP authenticator app when `TwoFactorRequest.Enable` is `true` and `TwoFactorRequest.TwoFactorCode` has a valid TOTP value.
* Obtain 2FA status with an empty request when all of `TwoFactorRequest`'s properties are `null`.

Add the following `TwoFactorRequestAsync` method to `Identity/CookieAuthenticationStateProvider.cs` (paste the following code at the bottom of the class file):

```
public async Task<TwoFactorResponse> TwoFactorRequestAsync(TwoFactorRequest twoFactorRequest)
{
    string[] defaultDetail = 
        [ "An unknown error prevented two-factor authentication." ];

    using var response = await httpClient.PostAsJsonAsync("manage/2fa", twoFactorRequest, 
        jsonSerializerOptions);

    // successful?
    if (response.IsSuccessStatusCode)
    {
        return await response.Content
            .ReadFromJsonAsync<TwoFactorResponse>() ??
            new()
            { 
                ErrorList = [ "There was an error processing the request." ]
            };
    }

    // body should contain details about why it failed
    var details = await response.Content.ReadAsStringAsync();
    var problemDetails = JsonDocument.Parse(details);
    var errors = new List<string>();
    var errorList = problemDetails.RootElement.GetProperty("errors");

    foreach (var errorEntry in errorList.EnumerateObject())
    {
        if (errorEntry.Value.ValueKind == JsonValueKind.String)
        {
            errors.Add(errorEntry.Value.GetString()!);
        }
        else if (errorEntry.Value.ValueKind == JsonValueKind.Array)
        {
            errors.AddRange(
                errorEntry.Value.EnumerateArray().Select(
                    e => e.GetString() ?? string.Empty)
                .Where(e => !string.IsNullOrEmpty(e)));
        }
    }

    // return the error list
    return new TwoFactorResponse
    {
        ErrorList = problemDetails == null ? defaultDetail : [.. errors]
    };
}
```

## Replace `Login` component

Replace the `Login` component. The following version of the `Login` component:

* Accepts a user's email address and password for an initial login attempt.
* If login is successful (2FA is disabled), the component notifies the user that they're authenticated.
* If the login attempt results in a response indicating that 2FA is required, a 2FA input element appears to receive either a 2FA TOTP code from an authenticator app or a recovery code. Depending on which code the user enters, login is attempted again by calling either `LoginTwoFactorCodeAsync` for a TOTP code or `LoginTwoFactorRecoveryCodeAsync` for a recovery code.

`Components/Identity/Login.razor`:

```
@page "/login"
@using System.ComponentModel.DataAnnotations
@using BlazorWasmAuth.Identity
@using BlazorWasmAuth.Identity.Models
@inject IAccountManagement Acct
@inject ILogger<Login> Logger
@inject NavigationManager Navigation

<PageTitle>Login</PageTitle>

<h1>Login</h1>

<AuthorizeView>
    <Authorized>
        <div class="alert alert-success">
            You're logged in as @context.User.Identity?.Name.
        </div>
    </Authorized>
    <NotAuthorized>
        @foreach (var error in formResult.ErrorList)
        {
            <div class="alert alert-danger">@error</div>
        }
        <div class="row">
            <div class="col">
                <section>
                    <EditForm Model="Input" method="post" OnValidSubmit="LoginUser" 
                            FormName="login" Context="editform_context">
                        <DataAnnotationsValidator />
                        <h2>Use a local account to log in.</h2>
                        <hr />
                        <div style="display:@(requiresTwoFactor ? "none" : "block")">
                            <div class="form-floating mb-3">
                                <InputText @bind-Value="Input.Email" 
                                    id="Input.Email" 
                                    class="form-control" 
                                    autocomplete="username" 
                                    aria-required="true" 
                                    placeholder="name@example.com" />
                                <label for="Input.Email" class="form-label">
                                    Email
                                </label>
                                <ValidationMessage For="() => Input.Email" 
                                    class="text-danger" />
                            </div>
                            <div class="form-floating mb-3">
                                <InputText type="password" 
                                    @bind-Value="Input.Password" 
                                    id="Input.Password" 
                                    class="form-control" 
                                    autocomplete="current-password" 
                                    aria-required="true" 
                                    placeholder="password" />
                                <label for="Input.Password" class="form-label">
                                    Password
                                </label>
                                <ValidationMessage For="() => Input.Password" 
                                    class="text-danger" />
                            </div>
                        </div>
                        <div style="display:@(requiresTwoFactor ? "block" : "none")">
                            <div class="form-floating mb-3">
                                <InputText @bind-Value="Input.TwoFactorCodeOrRecoveryCode" 
                                    id="Input.TwoFactorCodeOrRecoveryCode" 
                                    class="form-control" 
                                    autocomplete="off" 
                                    placeholder="###### or #####-#####" />
                                <label for="Input.TwoFactorCodeOrRecoveryCode" class="form-label">
                                    Two-factor Code or Recovery Code
                                </label>
                                <ValidationMessage For="() => Input.TwoFactorCodeOrRecoveryCode" 
                                    class="text-danger" />
                            </div>
                        </div>
                        <div>
                            <button type="submit" class="w-100 btn btn-lg btn-primary">
                                Log in
                            </button>
                        </div>
                        <div class="mt-3">
                            <p>
                                <a href="forgot-password">Forgot password</a>
                            </p>
                            <p>
                                <a href="register">Register as a new user</a>
                            </p>
                        </div>
                    </EditForm>
                </section>
            </div>
        </div>
    </NotAuthorized>
</AuthorizeView>

@code {
    private FormResult formResult = new();
    private bool requiresTwoFactor;

    [SupplyParameterFromForm]
    private InputModel Input { get; set; } = new();

    [SupplyParameterFromQuery]
    private string? ReturnUrl { get; set; }

    public async Task LoginUser()
    {
        if (requiresTwoFactor)
        {
            if (!string.IsNullOrEmpty(Input.TwoFactorCodeOrRecoveryCode))
            {
                // The [RegularExpression] data annotation ensures that the input 
                // is either a six-digit authenticator code (######) or an 
                // eleven-character alphanumeric recovery code (#####-#####)
                if (Input.TwoFactorCodeOrRecoveryCode.Length == 6)
                {
                    formResult = await Acct.LoginTwoFactorCodeAsync(
                        Input.Email, Input.Password, 
                        Input.TwoFactorCodeOrRecoveryCode);
                }
                else
                {
                    formResult = await Acct.LoginTwoFactorRecoveryCodeAsync(
                        Input.Email, Input.Password, 
                        Input.TwoFactorCodeOrRecoveryCode);

                    if (formResult.Succeeded)
                    {
                        var twoFactorResponse = await Acct.TwoFactorRequestAsync(new());
                    }
                }
            }
            else
            {
                formResult = 
                    new FormResult
                    {
                        Succeeded = false,
                        ErrorList = [ "Invalid two-factor code." ]
                    };
            }
        }
        else
        {
            formResult = await Acct.LoginAsync(Input.Email, Input.Password);
            requiresTwoFactor = formResult.ErrorList.Contains("RequiresTwoFactor");
            Input.TwoFactorCodeOrRecoveryCode = string.Empty;

            if (requiresTwoFactor)
            {
                formResult.ErrorList = [];
            }
        }

        if (formResult.Succeeded && !string.IsNullOrEmpty(ReturnUrl))
        {
            Navigation.NavigateTo(ReturnUrl);
        }
    }

    private sealed class InputModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; } = string.Empty;

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; } = string.Empty;

        [RegularExpression(@"^([0-9]{6})|([A-Z0-9]{5}[-]{1}[A-Z0-9]{5})$", 
            ErrorMessage = "Must be a six-digit authenticator code (######) or " +
            "eleven-character alphanumeric recovery code (#####-#####, dash " +
            "required)")]
        [Display(Name = "Two-factor Code or Recovery Code")]
        public string TwoFactorCodeOrRecoveryCode { get; set; } = string.Empty;
    }
}
```

Using the preceding component, the user is remembered after a successful login with a valid TOTP code from an authenticator app. If you want to always require a TOTP code for login and not remember the machine, call the `TwoFactorRequestAsync` method with `TwoFactorRequest.ForgetMachine` set to `true` immediately after a successful two-factor login:

```
if (Input.TwoFactorCodeOrRecoveryCode.Length == 6)
{
    formResult = await Acct.LoginTwoFactorCodeAsync(Input.Email, Input.Password, 
        Input.TwoFactorCodeOrRecoveryCode);

+    if (formResult.Succeeded)
+    {
+        var forgetMachine = 
+            await Acct.TwoFactorRequestAsync(new() { ForgetMachine = true });
+    }
}
```

## Add a component to display recovery codes

Add the following `ShowRecoveryCodes` component to the app to display recovery codes to the user.

`Components/Identity/ShowRecoveryCodes.razor`:

```
<h3>Recovery codes</h3>

<div class="alert alert-warning" role="alert">
    <p>
        <strong>Put these codes in a safe place.</strong>
    </p>
    <p>
        If you lose your device and don't have an unused 
        recovery code, you can't access your account.
    </p>
</div>
<div class="row">
    <div class="col-md-12">
        @foreach (var recoveryCode in RecoveryCodes)
        {
            <div>
                <code class="recovery-code">@recoveryCode</code>
            </div>
        }
    </div>
</div>

@code {
    [Parameter]
    public string[] RecoveryCodes { get; set; } = [];
}
```

## Manage 2FA page

Add the following `Manage2fa` component to the app to manage 2FA for users.

If 2FA isn't enabled, the component loads a form with a QR code to enable 2FA with a TOTP authenticator app. The user adds the app to their authenticator app and then verifies the authenticator app and enables 2FA by providing a TOTP code from the authenticator app.

If 2FA is enabled, buttons appear to disable 2FA and regenerate recovery codes.

`Components/Identity/Manage2fa.razor`:

```
@page "/manage-2fa"
@using System.ComponentModel.DataAnnotations
@using System.Globalization
@using System.Text
@using System.Text.Encodings.Web
@using Net.Codecrete.QrCodeGenerator
@using BlazorWasmAuth.Identity
@using BlazorWasmAuth.Identity.Models
@attribute [Authorize]
@inject IAccountManagement Acct
@inject IAuthorizationService AuthorizationService
@inject IConfiguration Config
@inject ILogger<Manage2fa> Logger

<PageTitle>Manage 2FA</PageTitle>

<h1>Manage Two-factor Authentication</h1>
<hr />
<div class="row">
    <div class="col">
        @if (loading)
        {
            <p>Loading ...</p>
        }
        else
        {
            @if (twoFactorResponse is not null)
            {
                @foreach (var error in twoFactorResponse.ErrorList)
                {
                    <div class="alert alert-danger">@error</div>
                }
                @if (twoFactorResponse.IsTwoFactorEnabled)
                {
                    <div class="alert alert-success" role="alert">
                        Two-factor authentication is enabled for your account.
                    </div>

                    <div class="m-1">
                        <button @onclick="Disable2FA" class="btn btn-lg btn-primary">
                            Disable 2FA
                        </button>
                    </div>

                    @if (twoFactorResponse.RecoveryCodes is null)
                    {
                        <div class="m-1">
                            Recovery Codes Remaining: 
                            @twoFactorResponse.RecoveryCodesLeft
                        </div>
                        <div class="m-1">
                            <button @onclick="GenerateNewCodes" 
                                    class="btn btn-lg btn-primary">
                                Generate New Recovery Codes
                            </button>
                        </div>
                    }
                    else
                    {
                        <ShowRecoveryCodes 
                            RecoveryCodes="twoFactorResponse.RecoveryCodes" />
                    }
                }
                else
                {
                    <h3>Configure authenticator app</h3>
                    <div>
                        <p>To use an authenticator app:</p>
                        <ol class="list">
                            <li>
                                <p>
                                    Download a two-factor authenticator app, such 
                                    as either of the following:
                                    <ul>
                                        <li>
                                            Microsoft Authenticator for
                                            <a href="https://go.microsoft.com/fwlink/?Linkid=825072">
                                                Android
                                            </a> and
                                            <a href="https://go.microsoft.com/fwlink/?Linkid=825073">
                                                iOS
                                            </a>
                                        </li>
                                        <li>
                                            Google Authenticator for
                                            <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">
                                                Android
                                            </a> and
                                            <a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">
                                                iOS
                                            </a>
                                        </li>
                                    </ul>
                                </p>
                            </li>
                            <li>
                                <p>
                                    Scan the QR Code or enter this key 
                                    <kbd>@twoFactorResponse.SharedKey</kbd> into your 
                                    two-factor authenticator app. Spaces and casing 
                                    don't matter.
                                </p>
                                <div>
                                    <svg xmlns="http://www.w3.org/2000/svg" height="300" 
                                            width="300" stroke="none" version="1.1" 
                                            viewBox="0 0 50 50">
                                        <rect width="300" height="300" fill="#ffffff" />
                                        <path d="@svgGraphicsPath" fill="#000000" />
                                    </svg>
                                </div>
                            </li>
                            <li>
                                <p>
                                    After you have scanned the QR code or input the 
                                    key above, your two-factor authenticator app 
                                    will provide you with a unique two-factor code. 
                                    Enter the code in the confirmation box below.
                                </p>
                                <div class="row">
                                    <div class="col-xl-6">
                                        <EditForm Model="Input" 
                                                FormName="send-code" 
                                                OnValidSubmit="OnValidSubmitAsync" 
                                                method="post">
                                            <DataAnnotationsValidator />
                                            <div class="form-floating mb-3">
                                                <InputText 
                                                    @bind-Value="Input.Code" 
                                                    id="Input.Code" 
                                                    class="form-control" 
                                                    autocomplete="off" 
                                                    placeholder="Enter the code" />
                                                <label for="Input.Code" 
                                                        class="control-label form-label">
                                                    Verification Code
                                                </label>
                                                <ValidationMessage 
                                                    For="() => Input.Code" 
                                                    class="text-danger" />
                                            </div>
                                            <button type="submit" 
                                                    class="w-100 btn btn-lg btn-primary">
                                                Verify
                                            </button>
                                        </EditForm>
                                    </div>
                                </div>
                            </li>
                        </ol>
                    </div>
                }
            }
        }
    </div>
</div>

@code {
    private TwoFactorResponse twoFactorResponse = new();
    private bool loading = true;
    private string? svgGraphicsPath;

    [SupplyParameterFromForm]
    private InputModel Input { get; set; } = new();

    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        twoFactorResponse = await Acct.TwoFactorRequestAsync(new());
        svgGraphicsPath = await GetQrCode(twoFactorResponse.SharedKey);
        loading = false;
    }

    private async Task<string> GetQrCode(string sharedKey)
    {
        if (authenticationState is not null && !string.IsNullOrEmpty(sharedKey))
        {
            var authState = await authenticationState;
            var email = authState?.User?.Identity?.Name!;
            var uri = string.Format(
                CultureInfo.InvariantCulture,
                "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6",
                UrlEncoder.Default.Encode(Config["TotpOrganizationName"]!),
                email,
                twoFactorResponse.SharedKey);
            var qr = QrCode.EncodeText(uri, QrCode.Ecc.Medium);

            return qr.ToGraphicsPath();
        }

        return string.Empty;
    }

    private async Task Disable2FA()
    {
        await Acct.TwoFactorRequestAsync(new() { ForgetMachine = true });
        twoFactorResponse = 
            await Acct.TwoFactorRequestAsync(new() { ResetSharedKey = true });
        svgGraphicsPath = await GetQrCode(twoFactorResponse.SharedKey);
    }

    private async Task GenerateNewCodes()
    {
        twoFactorResponse = 
            await Acct.TwoFactorRequestAsync(new() { ResetRecoveryCodes = true });
    }

    private async Task OnValidSubmitAsync()
    {
        twoFactorResponse = await Acct.TwoFactorRequestAsync(
            new() 
            { 
                Enable = true, 
                TwoFactorCode = Input.Code 
            });
        Input.Code = string.Empty;

        // When 2FA is first enabled, recovery codes are returned.
        // However, subsequently disabling and re-enabling 2FA
        // leaves the existing codes in place and doesn't generate
        // a new set of recovery codes. The following code ensures
        // that a new set of recovery codes is generated each
        // time 2FA is enabled.
        if (twoFactorResponse.RecoveryCodes is null || 
            twoFactorResponse.RecoveryCodes.Length == 0)
        {
            await GenerateNewCodes();
        }
    }

    private sealed class InputModel
    {
        [Required]
        [RegularExpression(@"^([0-9]{6})$", 
            ErrorMessage = "Must be a six-digit authenticator code (######)")]
        [DataType(DataType.Text)]
        [Display(Name = "Verification Code")]
        public string Code { get; set; } = string.Empty;
    }
}
```

## Link to the Manage 2FA page

Add a link to the navigation menu for users to reach the `Manage2fa` component page.

In the `<Authorized>` content of the `<AuthorizeView>` in `Components/Layout/NavMenu.razor`, add the following markup:

```
<AuthorizeView>
    <Authorized>

        ...

+       <div class="nav-item px-3">
+           <NavLink class="nav-link" href="manage-2fa">
+               <span class="bi bi-key" aria-hidden="true"></span> Manage 2FA
+           </NavLink>
+       </div>

        ...

    </Authorized>
</AuthorizeView>
```

## Failures due to TOTP time skew

TOTP authentication depends on accurate time keeping on the TOTP authenticator app device and the app's host. TOTP tokens are only valid for 30 seconds. If logins are failing due to rejected TOTP codes, confirm accurate time is maintained, preferably synchronized to an accurate NTP service.

## Related content

* [`nimiq/qr-creator`](https://github.com/nimiq/qr-creator)
* [MapIdentityApi](/en-us/dotnet/api/microsoft.aspnetcore.routing.identityapiendpointroutebuilderextensions.mapidentityapi)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-11-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0027
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0027.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0027: Unnecessary `public Program` class declaration

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0027      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

An unnecessary `public Program` class declaration within the `Program.cs` file.

## Rule description

Suggests to remove the unnecessary `public Program` class declaration as this is no longer required to make the generated `Program` class public in ASP.NET Core apps.

## How to fix violations

To fix a violation of this rule, remove the `public Program` class declaration within the `Program.cs` file.

## When to suppress warnings

The severity level of this diagnostic is Information. While the warning can be suppressed if necessary, it is recommended to remove the declaration as it is no longer required.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/metrics/security
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/metrics/security.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core built-in authentication and authorization metrics

This article describes the built-in authentication and authorization metrics for ASP.NET Core produced using the [System.Diagnostics.Metrics](/en-us/dotnet/api/system.diagnostics.metrics) API. These metrics cover authorization attempts and authentication operations. They're available in ASP.NET Core 10.0 or later.

For an overview of all built-in metrics reference pages and how to read this reference, see [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0). For information on how to collect, report, enrich, and test with ASP.NET Core metrics, see [ASP.NET Core metrics](overview?view=aspnetcore-10.0).

## `Microsoft.AspNetCore.Authorization`

The `Microsoft.AspNetCore.Authorization` metrics report information about [authorization attempts](../security/authorization/introduction?view=aspnetcore-10.0) in ASP.NET Core apps:

* [`aspnetcore.authorization.attempts`](#metric-aspnetcoreauthorizationattempts)

### Metric: `aspnetcore.authorization.attempts`

| Name                                | Instrument Type | Unit (UCUM) | Description                                                         |
| ----------------------------------- | --------------- | ----------- | ------------------------------------------------------------------- |
| `aspnetcore.authorization.attempts` | Counter         | `{request}` | The total number of requests for which authorization was attempted. |

| Attribute                         | Type    | Description                                          | Examples                                                  | Presence                                                                     |
| --------------------------------- | ------- | ---------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `user.is_authenticated`           | boolean | Whether the request came from an authenticated user. | `true`                                                    | `Required`                                                                   |
| `aspnetcore.authorization.policy` | string  | The name of the authorization policy.                | `AtLeast21`; `EmployeeOnly`                               | `Conditionally Required` if an authorization policy is used.                 |
| `aspnetcore.authorization.result` | string  | Whether the authorization succeeded or failed.       | `success`; `failure`                                      | `Conditionally Required` if an exception is not thrown during authorization. |
| `error.type`                      | string  | The full name of the exception type.                 | `System.InvalidOperationException`; `Contoso.MyException` | `Conditionally Required` if the request has ended with an error.             |

## `Microsoft.AspNetCore.Authentication`

The `Microsoft.AspNetCore.Authentication` metrics report information about [Authentication](../security/authentication/?view=aspnetcore-10.0) in ASP.NET Core apps:

* [`aspnetcore.authentication.authenticate.duration`](#metric-aspnetcoreauthenticationauthenticateduration)
* [`aspnetcore.authentication.challenges`](#metric-aspnetcoreauthenticationchallenges)
* [`aspnetcore.authentication.forbids`](#metric-aspnetcoreauthenticationforbids)
* [`aspnetcore.authentication.sign_ins`](#metric-aspnetcoreauthenticationsign_ins)
* [`aspnetcore.authentication.sign_outs`](#metric-aspnetcoreauthenticationsign_outs)

### Metric: `aspnetcore.authentication.authenticate.duration`

| Name                                              | Instrument Type | Unit (UCUM) | Description                                |
| ------------------------------------------------- | --------------- | ----------- | ------------------------------------------ |
| `aspnetcore.authentication.authenticate.duration` | Histogram       | `s`         | The authentication duration for a request. |

| Attribute                          | Type   | Description                            | Examples                                                  | Presence                                                                                  |
| ---------------------------------- | ------ | -------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `aspnetcore.authentication.result` | string | The authentication result.             | `success`; `failure`; `none`; `_OTHER`                    | `Conditionally Required` if the request did not end with an error.                        |
| `aspnetcore.authentication.scheme` | string | The name of the authentication scheme. | `Bearer`; `Cookies`                                       | `Conditionally Required` if the request did not end with an error.                        |
| `error.type`                       | string | The full name of the exception type.   | `System.InvalidOperationException`; `Contoso.MyException` | `Conditionally Required` if authentication failed or the request has ended with an error. |

### Metric: `aspnetcore.authentication.challenges`

| Name                                   | Instrument Type | Unit (UCUM) | Description                                       |
| -------------------------------------- | --------------- | ----------- | ------------------------------------------------- |
| `aspnetcore.authentication.challenges` | Counter         | `{request}` | The total number of times a scheme is challenged. |

| Attribute                          | Type   | Description                            | Examples                                                  | Presence                                                           |
| ---------------------------------- | ------ | -------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------ |
| `aspnetcore.authentication.scheme` | string | The name of the authentication scheme. | `Bearer`; `Cookies`                                       | `Conditionally Required` if the request did not end with an error. |
| `error.type`                       | string | The full name of the exception type.   | `System.InvalidOperationException`; `Contoso.MyException` | `Conditionally Required` if the request has ended with an error.   |

### Metric: `aspnetcore.authentication.forbids`

| Name                                | Instrument Type | Unit (UCUM) | Description                                                                                                    |
| ----------------------------------- | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------- |
| `aspnetcore.authentication.forbids` | Counter         | `{request}` | The total number of times an authenticated user attempts to access a resource they aren't permitted to access. |

| Attribute                          | Type   | Description                            | Examples                                                  | Presence                                                           |
| ---------------------------------- | ------ | -------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------ |
| `aspnetcore.authentication.scheme` | string | The name of the authentication scheme. | `Bearer`; `Cookies`                                       | `Conditionally Required` if the request did not end with an error. |
| `error.type`                       | string | The full name of the exception type.   | `System.InvalidOperationException`; `Contoso.MyException` | `Conditionally Required` if the request has ended with an error.   |

### Metric: `aspnetcore.authentication.sign_ins`

| Name                                 | Instrument Type | Unit (UCUM) | Description                                                       |
| ------------------------------------ | --------------- | ----------- | ----------------------------------------------------------------- |
| `aspnetcore.authentication.sign_ins` | Counter         | `{request}` | The total number of times a principal is signed in with a scheme. |

| Attribute                          | Type   | Description                            | Examples                                                  | Presence                                                           |
| ---------------------------------- | ------ | -------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------ |
| `aspnetcore.authentication.scheme` | string | The name of the authentication scheme. | `Bearer`; `Cookies`                                       | `Conditionally Required` if the request did not end with an error. |
| `error.type`                       | string | The full name of the exception type.   | `System.InvalidOperationException`; `Contoso.MyException` | `Conditionally Required` if the request has ended with an error.   |

### Metric: `aspnetcore.authentication.sign_outs`

| Name                                  | Instrument Type | Unit (UCUM) | Description                                                        |
| ------------------------------------- | --------------- | ----------- | ------------------------------------------------------------------ |
| `aspnetcore.authentication.sign_outs` | Counter         | `{request}` | The total number of times a principal is signed out with a scheme. |

| Attribute                          | Type   | Description                            | Examples                                                  | Presence                                                           |
| ---------------------------------- | ------ | -------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------ |
| `aspnetcore.authentication.scheme` | string | The name of the authentication scheme. | `Bearer`; `Cookies`                                       | `Conditionally Required` if the request did not end with an error. |
| `error.type`                       | string | The full name of the exception type.   | `System.InvalidOperationException`; `Contoso.MyException` | `Conditionally Required` if the request has ended with an error.   |

## Related content

* [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0)
* [ASP.NET Core built-in HTTP metrics](http?view=aspnetcore-10.0)
* [ASP.NET Core built-in diagnostics metrics](diagnostics?view=aspnetcore-10.0)
* [ASP.NET Core built-in Blazor (Razor components) metrics](blazor?view=aspnetcore-10.0)
* [ASP.NET Core metrics](overview?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/aspnet-core-module
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/aspnet-core-module.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Module (ANCM) for IIS

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The ASP.NET Core Module (ANCM) is a native IIS module that plugs into the IIS pipeline, allowing ASP.NET Core applications to work with IIS. Run ASP.NET Core apps with IIS by either:

* Hosting an ASP.NET Core app inside of the IIS worker process (`w3wp.exe`), called the [in-process hosting model](iis/in-process-hosting?view=aspnetcore-10.0).
* Forwarding web requests to a backend ASP.NET Core app running the Kestrel server, called the [out-of-process hosting model](iis/out-of-process-hosting?view=aspnetcore-10.0).

There are trade-offs between each of the hosting models. By default, the in-process hosting model is used due to better performance and diagnostics.

For more information and configuration guidance, see the following topics:

* [Web server implementations in ASP.NET Core](../fundamentals/servers/?view=aspnetcore-10.0)

## Install ASP.NET Core Module (ANCM)

The ASP.NET Core Module (ANCM) is installed with the .NET Core Runtime from the [.NET Core Hosting Bundle](iis/hosting-bundle?view=aspnetcore-10.0). The ASP.NET Core Module is forward and backward compatible with [in-support releases of .NET](https://dotnet.microsoft.com/platform/support/policy/dotnet-core#lifecycle).

Breaking changes and security advisories are reported on the [Announcements repo](https://github.com/aspnet/Announcements/issues). Announcements can be limited to a specific version by selecting a **Label** filter.

Download the installer using the following link:

[Current .NET Core Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

For more information, including installing an earlier version of the module, see [Hosting Bundle](iis/hosting-bundle?view=aspnetcore-10.0).

For a tutorial experience on publishing an ASP.NET Core app to an IIS server, see [Publish an ASP.NET Core app to IIS](../tutorials/publish-to-iis?view=aspnetcore-10.0).

The ASP.NET Core Module (ANCM) is a native IIS module that plugs into the IIS pipeline to either:

* Host an ASP.NET Core app inside of the IIS worker process (`w3wp.exe`), called the [in-process hosting model](#in-process-hosting-model).
* Forward web requests to a backend ASP.NET Core app running the [Kestrel server](../fundamentals/servers/kestrel?view=aspnetcore-10.0), called the [out-of-process hosting model](#out-of-process-hosting-model).

Supported Windows versions:

* Windows 7 or later
* Windows Server 2012 R2 or later

When hosting in-process, the module uses an in-process server implementation for IIS, called IIS HTTP Server (`IISHttpServer`).

When hosting out-of-process, the module only works with Kestrel. The module doesn't function with [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

## Hosting models

### In-process hosting model

ASP.NET Core apps default to the in-process hosting model.

The following characteristics apply when hosting in-process:

* IIS HTTP Server (`IISHttpServer`) is used instead of [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server. For in-process, [CreateDefaultBuilder](../fundamentals/host/generic-host?view=aspnetcore-10.0#default-builder-settings) calls [UseIIS](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiis) to:

  * Register the `IISHttpServer`.
  * Configure the port and base path the server should listen on when running behind the ASP.NET Core Module.
  * Configure the host to capture startup errors.

* The [requestTimeout attribute](#attributes-of-the-aspnetcore-element) doesn't apply to in-process hosting.

* Sharing an app pool among apps isn't supported. Use one app pool per app.

* When using [Web Deploy](/en-us/iis/publish/using-web-deploy/introduction-to-web-deploy) or manually placing an [`app_offline.htm` file in the deployment](iis/?view=aspnetcore-10.0#locked-deployment-files), the app might not shut down immediately if there's an open connection. For example, a WebSocket connection may delay app shut down.

* The architecture (bitness) of the app and installed runtime (x64 or x86) must match the architecture of the app pool.

* Client disconnects are detected. The [`HttpContext.RequestAborted`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted) cancellation token is cancelled when the client disconnects.

* In ASP.NET Core 2.2.1 or earlier, [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) returns the worker directory of the process started by IIS rather than the app's directory (for example, `C:\Windows\System32\inetsrv` for `w3wp.exe`).

  For sample code that sets the app's current directory, see the [`CurrentDirectoryHelpers` class](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/aspnet-core-module/samples/snapshot/3.x/CurrentDirectoryHelpers.cs). Call the `SetCurrentDirectory` method. Subsequent calls to [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) provide the app's directory.

* When hosting in-process, [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationservice.authenticateasync) isn't called internally to initialize a user. Therefore, an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation used to transform claims after every authentication isn't activated by default. When transforming claims with an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation, call [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) to add authentication services:

  ```
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
      services.AddAuthentication(IISServerDefaults.AuthenticationScheme);
  }

  public void Configure(IApplicationBuilder app)
  {
      app.UseAuthentication();
  }
  ```

  * [Web Package (single-file) deployments](/en-us/aspnet/web-forms/overview/deployment/web-deployment-in-the-enterprise/deploying-web-packages) aren't supported.

### Out-of-process hosting model

To configure an app for out-of-process hosting, set the value of the `<AspNetCoreHostingModel>` property to `OutOfProcess` in the project file (`.csproj`):

```
<PropertyGroup>
  <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>
```

In-process hosting is set with `InProcess`, which is the default value.

The value of `<AspNetCoreHostingModel>` is case insensitive, so `inprocess` and `outofprocess` are valid values.

[Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server is used instead of IIS HTTP Server (`IISHttpServer`).

For out-of-process, [`CreateDefaultBuilder`](../fundamentals/host/generic-host?view=aspnetcore-10.0#default-builder-settings) calls [UseIISIntegration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiisintegration) to:

* Configure the port and base path the server should listen on when running behind the ASP.NET Core Module.
* Configure the host to capture startup errors.

### Hosting model changes

If the `hostingModel` setting is changed in the `web.config` file (explained in the [Configuration with `web.config`](#configuration-with-webconfig) section), the module recycles the worker process for IIS.

For IIS Express, the module doesn't recycle the worker process but instead triggers a graceful shutdown of the current IIS Express process. The next request to the app spawns a new IIS Express process.

### Process name

`Process.GetCurrentProcess().ProcessName` reports `w3wp`/`iisexpress` (in-process) or `dotnet` (out-of-process).

Many native modules, such as Windows Authentication, remain active. To learn more about IIS modules active with the ASP.NET Core Module, see [IIS modules with ASP.NET Core](iis/modules?view=aspnetcore-10.0).

The ASP.NET Core Module can also:

* Set environment variables for the worker process.
* Log stdout output to file storage for troubleshooting startup issues.
* Forward Windows authentication tokens.

## How to install and use the ASP.NET Core Module (ANCM)

For instructions on how to install the ASP.NET Core Module, see [Install the .NET Core Hosting Bundle](iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle). The ASP.NET Core Module is forward and backward compatible with [in-support releases of .NET](https://dotnet.microsoft.com/platform/support/policy/dotnet-core#lifecycle).

Breaking changes and security advisories are reported on the [Announcements repo](https://github.com/aspnet/Announcements/issues). Announcements can be limited to a specific version by selecting a **Label** filter.

## Configuration with web.config

The ASP.NET Core Module is configured with the `aspNetCore` section of the `system.webServer` node in the site's *web.config* file.

The following `web.config` file is published for a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd) and configures the ASP.NET Core Module to handle site requests:

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet"
                  arguments=".\MyApp.dll"
                  stdoutLogEnabled="false"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>
```

The following *web.config* is published for a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath=".\MyApp.exe"
                  stdoutLogEnabled="false"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>
```

The [InheritInChildApplications](/en-us/dotnet/api/system.configuration.sectioninformation.inheritinchildapplications) property is set to `false` to indicate that apps that reside in a subdirectory of the app don't inherit the settings specified within the [`<location>`](/en-us/iis/manage/managing-your-configuration-settings/understanding-iis-configuration-delegation#the-concept-of-location) element.

When an app is deployed to [Azure App Service](https://azure.microsoft.com/products/app-service/), the `stdoutLogFile` path is set to `\\?\%home%\LogFiles\stdout`. The path saves stdout logs to the `LogFiles` folder, which is a location automatically created by the service.

For information on IIS sub-application configuration, see [Host ASP.NET Core on Windows with IIS](iis/?view=aspnetcore-10.0#sub-applications).

### Attributes of the aspNetCore element

| Attribute                 | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Default                                              |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `arguments`               | Optional string attribute.Arguments to the executable specified in **processPath**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |                                                      |
| `disableStartUpErrorPage` | Optional Boolean attribute.If true, the **502.5 - Process Failure** page is suppressed, and the 502 status code page configured in the *web.config* takes precedence.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `false`                                              |
| `forwardWindowsAuthToken` | Optional Boolean attribute.If true, the token is forwarded to the child process listening on `%ASPNETCORE_PORT%` as a header `'MS-ASPNETCORE-WINAUTHTOKEN'` per request. It's the responsibility of that process to call CloseHandle on this token per request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `true`                                               |
| `hostingModel`            | Optional string attribute.Specifies the hosting model as in-process (`InProcess`/`inprocess`) or out-of-process (`OutOfProcess`/`outofprocess`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `InProcess` `inprocess`                              |
| `processesPerApplication` | Optional integer attribute.Specifies the number of instances of the process specified in the **processPath** setting that can be spun up per app.†For in-process hosting, the value is limited to `1`.Setting `processesPerApplication` is discouraged. This attribute will be removed in a future release.                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Default: `1` Min: `1` Max: `100`†                    |
| `processPath`             | Required string attribute.Path to the executable that launches a process listening for HTTP requests. Relative paths are supported. If the path begins with `.`, the path is considered to be relative to the site root.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |                                                      |
| `rapidFailsPerMinute`     | Optional integer attribute.Specifies the number of times the process specified in **processPath** is allowed to crash per minute. If this limit is exceeded, the module stops launching the process for the remainder of the minute.Not supported with in-process hosting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Default: `10` Min: `0` Max: `100`                    |
| `requestTimeout`          | Optional timespan attribute.Specifies the duration for which the ASP.NET Core Module waits for a response from the process listening on %ASPNETCORE\_PORT%.In versions of the ASP.NET Core Module that shipped with the release of ASP.NET Core 2.1 or later, the `requestTimeout` is specified in hours, minutes, and seconds.Doesn't apply to in-process hosting. For in-process hosting, the module waits for the app to process the request.Valid values for minutes and seconds segments of the string are in the range 0-59. Use of **60** in the value for minutes or seconds results in a *500 - Internal Server Error*.                                                                                                                                        | Default: `00:02:00` Min: `00:00:00` Max: `360:00:00` |
| `shutdownTimeLimit`       | Optional integer attribute.Duration in seconds that the module waits for the executable to gracefully shutdown when the `app_offline.htm` file is detected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Default: `10` Min: `0` Max: `600`                    |
| `startupTimeLimit`        | Optional integer attribute.Duration in seconds that the module waits for the executable to start a process listening on the port. If this time limit is exceeded, the module kills the process.When hosting *in-process*: The process is **not** restarted and does **not** use the **rapidFailsPerMinute** setting.When hosting *out-of-process*: The module attempts to relaunch the process when it receives a new request and continues to attempt to restart the process on subsequent incoming requests unless the app fails to start **rapidFailsPerMinute** number of times in the last rolling minute.A value of 0 (zero) is **not** considered an infinite timeout.                                                                                           | Default: `120` Min: `0` Max: `3600`                  |
| `stdoutLogEnabled`        | Optional Boolean attribute.If true, **stdout** and **stderr** for the process specified in **processPath** are redirected to the file specified in **stdoutLogFile**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `false`                                              |
| `stdoutLogFile`           | Optional string attribute.Specifies the relative or absolute file path for which **stdout** and **stderr** from the process specified in **processPath** are logged. Relative paths are relative to the root of the site. Any path starting with `.` are relative to the site root and all other paths are treated as absolute paths. The module creates any folders provided in the path when it creates the log file. Using underscore delimiters, a timestamp, process ID, and file extension (`.log`) are added to the last segment of the **stdoutLogFile** path. If `.\logs\stdout` is supplied as a value, an example stdout log is saved as `stdout_20180205194132_1934.log` in the `logs` folder when saved on 2/5/2018 at 19:41:32 with a process ID of 1934. | `aspnetcore-stdout`                                  |

### Set environment variables

Environment variables can be specified for the process in the `processPath` attribute. Specify an environment variable with the `<environmentVariable>` child element of an `<environmentVariables>` collection element. Environment variables set in this section take precedence over system environment variables.

The following example sets two environment variables in `web.config`. `ASPNETCORE_ENVIRONMENT` configures the app's environment to `Development`. A developer might temporarily set this value in the `web.config` file in order to force the [Developer Exception Page](../fundamentals/error-handling?view=aspnetcore-10.0) to load when debugging an app exception. `CONFIG_DIR` is an example of a user-defined environment variable, where the developer has written code that reads the value on startup to form a path for loading the app's configuration file.

```
<aspNetCore processPath="dotnet"
      arguments=".\MyApp.dll"
      stdoutLogEnabled="false"
      stdoutLogFile=".\logs\stdout"
      hostingModel="inprocess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
    <environmentVariable name="CONFIG_DIR" value="f:\application_config" />
  </environmentVariables>
</aspNetCore>
```

Note

An alternative to setting the environment directly in `web.config` is to include the `<EnvironmentName>` property in the [publish profile (`.pubxml`)](visual-studio-publish-profiles?view=aspnetcore-10.0) or project file. This approach sets the environment in `web.config` when the project is published:

```
<PropertyGroup>
  <EnvironmentName>Development</EnvironmentName>
</PropertyGroup>
```

Warning

Only set the `ASPNETCORE_ENVIRONMENT` environment variable to `Development` on staging and testing servers that aren't accessible to untrusted networks, such as the Internet.

## `app_offline.htm`

If a file with the name `app_offline.htm` is detected in the root directory of an app, the ASP.NET Core Module attempts to gracefully shutdown the app and stop processing incoming requests. If the app is still running after the number of seconds defined in `shutdownTimeLimit`, the ASP.NET Core Module kills the running process.

While the `app_offline.htm` file is present, the ASP.NET Core Module responds to requests by sending back the contents of the `app_offline.htm` file. When the `app_offline.htm` file is removed, the next request starts the app.

When using the out-of-process hosting model, the app might not shut down immediately if there's an open connection. For example, a WebSocket connection might delay app shut down.

## Start-up error page

Both in-process and out-of-process hosting produce custom error pages when they fail to start the app.

If the ASP.NET Core Module fails to find either the in-process or out-of-process request handler, a *500.0 - In-Process/Out-Of-Process Handler Load Failure* status code page appears.

For in-process hosting, if the ASP.NET Core Module fails to start the app, a *500.30 - Start Failure* status code page appears.

For out-of-process hosting, if the ASP.NET Core Module fails to launch the backend process or the backend process starts but fails to listen on the configured port, a *502.5 - Process Failure* status code page appears.

To suppress this page and revert to the default IIS 5xx status code page, use the `disableStartUpErrorPage` attribute. For more information on configuring custom error messages, see [HTTP Errors `<httpErrors>`](/en-us/iis/configuration/system.webServer/httpErrors/).

## Log creation and redirection

The ASP.NET Core Module redirects stdout and stderr console output to disk if the `stdoutLogEnabled` and `stdoutLogFile` attributes of the `aspNetCore` element are set. The module creates any folders in the `stdoutLogFile` path when it creates the log file. The app pool must have write access to the location where the logs are written (use `IIS AppPool\<app_pool_name>` to provide write permission).

Logs aren't rotated, unless process recycling/restart occurs. It's the responsibility of the hoster to limit the disk space the logs consume.

Using the stdout log is only recommended for troubleshooting app startup issues when hosting on IIS or when using [development-time support for IIS with Visual Studio](iis/development-time-iis-support?view=aspnetcore-10.0), not while debugging locally and running the app with IIS Express.

Don't use the stdout log for general app logging purposes. For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

A timestamp and file extension are added automatically when the log file is created. The log file name is composed by appending the timestamp, process ID, and file extension (`.log`) to the last segment of the `stdoutLogFile` path (typically `stdout`) delimited by underscores. If the `stdoutLogFile` path ends with `stdout`, a log for an app with a PID of 1934 created on February 5, 2018 at 19:42:32 has the file name `stdout_20180205194132_1934.log`.

If `stdoutLogEnabled` is false, errors that occur on app startup are captured and emitted to the event log up to 30 KB. After startup, all additional logs are discarded.

The following sample `aspNetCore` element configures stdout logging at the relative path `.\log\`. Confirm that the AppPool user identity has permission to write to the path provided.

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="true"
    stdoutLogFile=".\logs\stdout"
    hostingModel="inprocess">
</aspNetCore>
```

When publishing an app for Azure App Service deployment, the Web SDK sets the `stdoutLogFile` value to `\\?\%home%\LogFiles\stdout`. The `%home` environment variable is predefined for apps hosted by Azure App Service.

To create logging filter rules, see the [Apply log filter rules in code](../fundamentals/logging/?view=aspnetcore-10.0#apply-log-filter-rules-in-code) section of the ASP.NET Core logging documentation.

For more information on path formats, see [File path formats on Windows systems](/en-us/dotnet/standard/io/file-path-formats).

## Enhanced diagnostic logs

The ASP.NET Core Module is configurable to provide enhanced diagnostics logs. Add the `<handlerSettings>` element to the `<aspNetCore>` element in `web.config`. Setting the `debugLevel` to `TRACE` exposes a higher fidelity of diagnostic information:

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="false"
    stdoutLogFile="\\?\%home%\LogFiles\stdout"
    hostingModel="inprocess">
  <handlerSettings>
    <handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />
    <handlerSetting name="debugLevel" value="FILE,TRACE" />
  </handlerSettings>
</aspNetCore>
```

The module creates any folders in the path (`logs` in the preceding example) when it creates the log file. The app pool must have write access to the location where the logs are written (use `IIS AppPool\{APP POOL NAME}`, where the placeholder `{APP POOL NAME}` is the app pool name, to provide write permission).

See [Configuration with web.config](#configuration-with-webconfig) for an example of the `aspNetCore` element in the `web.config` file.

## Modify the stack size

*Only applies when using the in-process hosting model.*

Configure the managed stack size using the `stackSize` setting in bytes in `web.config`. The default size is 1,048,576 bytes (1 MB).

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="false"
    stdoutLogFile="\\?\%home%\LogFiles\stdout"
    hostingModel="inprocess">
  <handlerSettings>
    <handlerSetting name="stackSize" value="2097152" />
  </handlerSettings>
</aspNetCore>
```

## Proxy configuration uses HTTP protocol and a pairing token

*Only applies to out-of-process hosting.*

The proxy created between the ASP.NET Core Module and Kestrel uses the HTTP protocol. There's no risk of eavesdropping the traffic between the module and Kestrel from a location off of the server.

A pairing token is used to guarantee that IIS proxied the requests received by Kestrel and didn't come from some other source. The pairing token is created and set into an environment variable (`ASPNETCORE_TOKEN`) by the module. The pairing token is also set into a header (`MS-ASPNETCORE-TOKEN`) on every proxied request. IIS middleware checks each request it receives to confirm that the pairing token header value matches the environment variable value. If the token values are mismatched, the request is logged and rejected. The pairing token environment variable and the traffic between the module and Kestrel aren't accessible from a location off of the server. Without knowing the pairing token value, a cyberattacker can't submit requests that bypass the check in the IIS middleware.

## ASP.NET Core Module with an IIS Shared Configuration

The ASP.NET Core Module installer runs with the privileges of the **TrustedInstaller** account. Because the local system account doesn't have modify permission for the share path used by the IIS Shared Configuration, the installer throws an access denied error when attempting to configure the module settings in the `applicationHost.config` file on the share.

When using an IIS Shared Configuration on the same machine as the IIS installation, run the ASP.NET Core Hosting Bundle installer with the `OPT_NO_SHARED_CONFIG_CHECK` parameter set to `1`:

```
dotnet-hosting-{VERSION}.exe OPT_NO_SHARED_CONFIG_CHECK=1
```

When the path to the shared configuration isn't on the same machine as the IIS installation, follow these steps:

1. Disable the IIS Shared Configuration.
2. Run the installer.
3. Export the updated `applicationHost.config` file to the share.
4. Re-enable the IIS Shared Configuration.

## Module version and Hosting Bundle installer logs

To determine the version of the installed ASP.NET Core Module:

1. On the hosting system, navigate to `%windir%\System32\inetsrv`.
2. Locate the `aspnetcore.dll` file.
3. Right-click the file and select **Properties** from the contextual menu.
4. Select the **Details** tab. The **File version** and **Product version** represent the installed version of the module.

The Hosting Bundle installer logs for the module are found at `C:\Users\%UserName%\AppData\Local\Temp`. The file is named `dd_DotNetCoreWinSvrHosting__{TIMESTAMP}_000_AspNetCoreModule_x64.log`.

## Module, schema, and configuration file locations

### Module

**IIS (x86/amd64):**

* `%windir%\System32\inetsrv\aspnetcore.dll`

* `%windir%\SysWOW64\inetsrv\aspnetcore.dll`

* `%ProgramFiles%\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll`

* `%ProgramFiles(x86)%\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll`

**IIS Express (x86/amd64):**

* `%ProgramFiles%\IIS Express\aspnetcore.dll`

* `%ProgramFiles(x86)%\IIS Express\aspnetcore.dll`

* `%ProgramFiles%\IIS Express\Asp.Net Core Module\V2\aspnetcorev2.dll`

* `%ProgramFiles(x86)%\IIS Express\Asp.Net Core Module\V2\aspnetcorev2.dll`

### Schema

**IIS**

* `%windir%\System32\inetsrv\config\schema\aspnetcore_schema.xml`

* `%windir%\System32\inetsrv\config\schema\aspnetcore_schema_v2.xml`

**IIS Express**

* `%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema.xml`

* `%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema_v2.xml`

### Configuration

**IIS**

* `%windir%\System32\inetsrv\config\applicationHost.config`

**IIS Express**

* Visual Studio: `{APPLICATION ROOT}\.vs\config\applicationHost.config`

* *iisexpress.exe* CLI: `%USERPROFILE%\Documents\IISExpress\config\applicationhost.config`

The files can be found by searching for `aspnetcore` in the `applicationHost.config` file.

The ASP.NET Core Module (ANCM) is a native IIS module that plugs into the IIS pipeline to either:

* Host an ASP.NET Core app inside of the IIS worker process (`w3wp.exe`), called the [in-process hosting model](#in-process-hosting-model).
* Forward web requests to a backend ASP.NET Core app running the [Kestrel server](../fundamentals/servers/kestrel?view=aspnetcore-10.0), called the [out-of-process hosting model](#out-of-process-hosting-model).

Supported Windows versions:

* Windows 7 or later
* Windows Server 2008 R2 or later

When hosting in-process, the module uses an in-process server implementation for IIS, called IIS HTTP Server (`IISHttpServer`).

When hosting out-of-process, the module only works with Kestrel. The module doesn't function with [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

## Hosting models

### In-process hosting model

To configure an app for in-process hosting, add the `<AspNetCoreHostingModel>` property to the app's project file with a value of `InProcess` (out-of-process hosting is set with `OutOfProcess`):

```
<PropertyGroup>
  <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
```

The in-process hosting model isn't supported for ASP.NET Core apps that target the .NET Framework.

The value of `<AspNetCoreHostingModel>` is case insensitive, so `inprocess` and `outofprocess` are valid values.

If the `<AspNetCoreHostingModel>` property isn't present in the file, the default value is `OutOfProcess`.

The following characteristics apply when hosting in-process:

* IIS HTTP Server (`IISHttpServer`) is used instead of [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server. For in-process, [CreateDefaultBuilder](../fundamentals/host/web-host?view=aspnetcore-10.0#set-up-a-host) calls [UseIIS](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiis) to:

  * Register the `IISHttpServer`.
  * Configure the port and base path the server should listen on when running behind the ASP.NET Core Module.
  * Configure the host to capture startup errors.

* The [requestTimeout attribute](#attributes-of-the-aspnetcore-element) doesn't apply to in-process hosting.

* Sharing an app pool among apps isn't supported. Use one app pool per app.

* When using [Web Deploy](/en-us/iis/publish/using-web-deploy/introduction-to-web-deploy) or manually placing an [app\_offline.htm file in the deployment](iis/?view=aspnetcore-10.0#locked-deployment-files), the app might not shut down immediately if there's an open connection. For example, a websocket connection might delay app shut down.

* The architecture (bitness) of the app and installed runtime (x64 or x86) must match the architecture of the app pool.

* Client disconnects are detected. The [HttpContext.RequestAborted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted) cancellation token is cancelled when the client disconnects.

* In ASP.NET Core 2.2.1 or earlier, [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) returns the worker directory of the process started by IIS rather than the app's directory (for example, *C:\Windows\System32\inetsrv* for *w3wp.exe*).

  For sample code that sets the app's current directory, see the [CurrentDirectoryHelpers class](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/aspnet-core-module/samples/snapshot/2.x/CurrentDirectoryHelpers.cs). Call the `SetCurrentDirectory` method. Subsequent calls to [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory) provide the app's directory.

* When hosting in-process, [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationservice.authenticateasync) isn't called internally to initialize a user. Therefore, an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation used to transform claims after every authentication isn't activated by default. When transforming claims with an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation, call [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) to add authentication services:

  ```
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
      services.AddAuthentication(IISServerDefaults.AuthenticationScheme);
  }

  public void Configure(IApplicationBuilder app)
  {
      app.UseAuthentication();
  }
  ```

### Out-of-process hosting model

To configure an app for out-of-process hosting, use either of the following approaches in the project file:

* Don't specify the `<AspNetCoreHostingModel>` property. If the `<AspNetCoreHostingModel>` property isn't present in the file, the default value is `OutOfProcess`.
* Set the value of the `<AspNetCoreHostingModel>` property to `OutOfProcess` (in-process hosting is set with `InProcess`):

```
<PropertyGroup>
  <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>
```

The value is case insensitive, so `inprocess` and `outofprocess` are valid values.

[Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server is used instead of IIS HTTP Server (`IISHttpServer`).

For out-of-process, [CreateDefaultBuilder](../fundamentals/host/web-host?view=aspnetcore-10.0#set-up-a-host) calls [UseIISIntegration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiisintegration) to:

* Configure the port and base path the server should listen on when running behind the ASP.NET Core Module.
* Configure the host to capture startup errors.

### Hosting model changes

If the `hostingModel` setting is changed in the *web.config* file (explained in the [Configuration with web.config](#configuration-with-webconfig) section), the module recycles the worker process for IIS.

For IIS Express, the module doesn't recycle the worker process but instead triggers a graceful shutdown of the current IIS Express process. The next request to the app spawns a new IIS Express process.

### Process name

`Process.GetCurrentProcess().ProcessName` reports `w3wp`/`iisexpress` (in-process) or `dotnet` (out-of-process).

Many native modules, such as Windows Authentication, remain active. To learn more about IIS modules active with the ASP.NET Core Module, see [IIS modules with ASP.NET Core](iis/modules?view=aspnetcore-10.0).

The ASP.NET Core Module can also:

* Set environment variables for the worker process.
* Log stdout output to file storage for troubleshooting startup issues.
* Forward Windows authentication tokens.

## How to install and use the ASP.NET Core Module (ANCM)

For instructions on how to install the ASP.NET Core Module, see [Install the .NET Core Hosting Bundle](iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle). The ASP.NET Core Module is forward and backward compatible with [in-support releases of .NET](https://dotnet.microsoft.com/platform/support/policy/dotnet-core#lifecycle).

Breaking changes and security advisories are reported on the [Announcements repo](https://github.com/aspnet/Announcements/issues). Announcements can be limited to a specific version by selecting a **Label** filter.

## Configuration with web.config

The ASP.NET Core Module is configured with the `aspNetCore` section of the `system.webServer` node in the site's *web.config* file.

The following *web.config* file is published for a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd) and configures the ASP.NET Core Module to handle site requests:

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet"
                  arguments=".\MyApp.dll"
                  stdoutLogEnabled="false"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>
```

The following *web.config* is published for a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath=".\MyApp.exe"
                  stdoutLogEnabled="false"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>
```

The [InheritInChildApplications](/en-us/dotnet/api/system.configuration.sectioninformation.inheritinchildapplications) property is set to `false` to indicate that apps that reside in a subdirectory of the app don't inherit the settings specified within the [\<location>](/en-us/iis/manage/managing-your-configuration-settings/understanding-iis-configuration-delegation#the-concept-of-location) element.

When an app is deployed to [Azure App Service](https://azure.microsoft.com/products/app-service/), the `stdoutLogFile` path is set to `\\?\%home%\LogFiles\stdout`. The path saves stdout logs to the *LogFiles* folder, which is a location automatically created by the service.

For information on IIS sub-application configuration, see [Host ASP.NET Core on Windows with IIS](iis/?view=aspnetcore-10.0#sub-applications).

### Attributes of the aspNetCore element

| Attribute                 | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Default                                              |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `arguments`               | Optional string attribute.Arguments to the executable specified in `processPath`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |                                                      |
| `disableStartUpErrorPage` | Optional Boolean attribute.If true, the **502.5 - Process Failure** page is suppressed, and the 502 status code page configured in the *web.config* takes precedence.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `false`                                              |
| `forwardWindowsAuthToken` | Optional Boolean attribute.If true, the token is forwarded to the child process listening on %ASPNETCORE\_PORT% as a header 'MS-ASPNETCORE-WINAUTHTOKEN' per request. It's the responsibility of that process to call CloseHandle on this token per request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `true`                                               |
| `hostingModel`            | Optional string attribute.Specifies the hosting model as in-process (`InProcess`/`inprocess`) or out-of-process (`OutOfProcess`/`outofprocess`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `OutOfProcess` `outofprocess`                        |
| `processesPerApplication` | Optional integer attribute.Specifies the number of instances of the process specified in the `processPath` setting that can be spun up per app.†For in-process hosting, the value is limited to `1`.Setting `processesPerApplication` is discouraged. This attribute will be removed in a future release.                                                                                                                                                                                                                                                                                                                                                                                                                                                       | Default: `1` Min: `1` Max: `100`†                    |
| `processPath`             | Required string attribute.Path to the executable that launches a process listening for HTTP requests. Relative paths are supported. If the path begins with `.`, the path is considered to be relative to the site root.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |                                                      |
| `rapidFailsPerMinute`     | Optional integer attribute.Specifies the number of times the process specified in `processPath` is allowed to crash per minute. If this limit is exceeded, the module stops launching the process for the remainder of the minute.Not supported with in-process hosting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Default: `10` Min: `0` Max: `100`                    |
| `requestTimeout`          | Optional timespan attribute.Specifies the duration for which the ASP.NET Core Module waits for a response from the process listening on %ASPNETCORE\_PORT%.In versions of the ASP.NET Core Module that shipped with the release of ASP.NET Core 2.1 or later, the `requestTimeout` is specified in hours, minutes, and seconds.Doesn't apply to in-process hosting. For in-process hosting, the module waits for the app to process the request.Valid values for minutes and seconds segments of the string are in the range 0-59. Use of **60** in the value for minutes or seconds results in a *500 - Internal Server Error*.                                                                                                                                | Default: `00:02:00` Min: `00:00:00` Max: `360:00:00` |
| `shutdownTimeLimit`       | Optional integer attribute.Duration in seconds that the module waits for the executable to gracefully shutdown when the `app_offline.htm` file is detected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Default: `10` Min: `0` Max: `600`                    |
| `startupTimeLimit`        | Optional integer attribute.Duration in seconds that the module waits for the executable to start a process listening on the port. If this time limit is exceeded, the module kills the process.When hosting *in-process*: The process is **not** restarted and does **not** use the `rapidFailsPerMinute` setting.When hosting *out-of-process*: The module attempts to relaunch the process when it receives a new request and continues to attempt to restart the process on subsequent incoming requests unless the app fails to start `rapidFailsPerMinute` number of times in the last rolling minute.A value of 0 (zero) is **not** considered an infinite timeout.                                                                                       | Default: `120` Min: `0` Max: `3600`                  |
| `stdoutLogEnabled`        | Optional Boolean attribute.If true, **stdout** and **stderr** for the process specified in `processPath` are redirected to the file specified in **stdoutLogFile**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `false`                                              |
| `stdoutLogFile`           | Optional string attribute.Specifies the relative or absolute file path for which `stdout` and `stderr` from the process specified in `processPath` are logged. Relative paths are relative to the root of the site. Any path starting with `.` are relative to the site root and all other paths are treated as absolute paths. The module creates any folders provided in the path when it creates the log file. Using underscore delimiters, a timestamp, process ID, and file extension (`.log`) are added to the last segment of the `stdoutLogFile` path. If `.\logs\stdout` is supplied as a value, an example stdout log is saved as `stdout_20180205194132_1934.log` in the `logs` folder when saved on 2/5/2018 at 19:41:32 with a process ID of 1934. | `aspnetcore-stdout`                                  |

### Setting environment variables

Environment variables can be specified for the process in the `processPath` attribute. Specify an environment variable with the `<environmentVariable>` child element of an `<environmentVariables>` collection element. Environment variables set in this section take precedence over system environment variables.

The following example sets two environment variables. `ASPNETCORE_ENVIRONMENT` configures the app's environment to `Development`. A developer might temporarily set this value in the `web.config` file in order to force the [Developer Exception Page](../fundamentals/error-handling?view=aspnetcore-10.0) to load when debugging an app exception. `CONFIG_DIR` is an example of a user-defined environment variable, where the developer has written code that reads the value on startup to form a path for loading the app's configuration file.

```
<aspNetCore processPath="dotnet"
      arguments=".\MyApp.dll"
      stdoutLogEnabled="false"
      stdoutLogFile=".\logs\stdout"
      hostingModel="inprocess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
    <environmentVariable name="CONFIG_DIR" value="f:\application_config" />
  </environmentVariables>
</aspNetCore>
```

Note

An alternative to setting the environment directly in `web.config` is to include the `<EnvironmentName>` property in the [publish profile (.pubxml)](visual-studio-publish-profiles?view=aspnetcore-10.0) or project file. This approach sets the environment in `web.config` when the project is published:

```
<PropertyGroup>
  <EnvironmentName>Development</EnvironmentName>
</PropertyGroup>
```

Warning

Only set the `ASPNETCORE_ENVIRONMENT` environment variable to `Development` on staging and testing servers that aren't accessible to untrusted networks, such as the Internet.

## app\_offline.htm

If a file with the name `app_offline.htm` is detected in the root directory of an app, the ASP.NET Core Module attempts to gracefully shutdown the app and stop processing incoming requests. If the app is still running after the number of seconds defined in `shutdownTimeLimit`, the ASP.NET Core Module kills the running process.

While the `app_offline.htm` file is present, the ASP.NET Core Module responds to requests by sending back the contents of the `app_offline.htm` file. When the `app_offline.htm` file is removed, the next request starts the app.

When using the out-of-process hosting model, the app might not shut down immediately if there's an open connection. For example, a websocket connection might delay app shut down.

## Start-up error page

Both in-process and out-of-process hosting produce custom error pages when they fail to start the app.

If the ASP.NET Core Module fails to find either the in-process or out-of-process request handler, a *500.0 - In-Process/Out-Of-Process Handler Load Failure* status code page appears.

For in-process hosting if the ASP.NET Core Module fails to start the app, a *500.30 - Start Failure* status code page appears.

For out-of-process hosting if the ASP.NET Core Module fails to launch the backend process or the backend process starts but fails to listen on the configured port, a *502.5 - Process Failure* status code page appears.

To suppress this page and revert to the default IIS 5xx status code page, use the `disableStartUpErrorPage` attribute. For more information on configuring custom error messages, see [HTTP Errors \<httpErrors>](/en-us/iis/configuration/system.webServer/httpErrors/).

## Log creation and redirection

The ASP.NET Core Module redirects stdout and stderr console output to disk if the `stdoutLogEnabled` and `stdoutLogFile` attributes of the `aspNetCore` element are set. The module creates any folders in the `stdoutLogFile` path when it creates the log file. The app pool must have write access to the location where the logs are written (use `IIS AppPool\{APP POOL NAME}` to provide write permission, where the placeholder `{APP POOL NAME}` is the app pool name).

Logs aren't rotated, unless process recycling/restart occurs. It's the responsibility of the hoster to limit the disk space the logs consume.

Using the stdout log is only recommended for troubleshooting app startup issues when hosting on IIS or when using [development-time support for IIS with Visual Studio](iis/development-time-iis-support?view=aspnetcore-10.0), not while debugging locally and running the app with IIS Express.

Don't use the stdout log for general app logging purposes. For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

A timestamp and file extension are added automatically when the log file is created. The log file name is composed by appending the timestamp, process ID, and file extension (`.log`) to the last segment of the `stdoutLogFile` path (typically `stdout`) delimited by underscores. If the `stdoutLogFile` path ends with `stdout`, a log for an app with a PID of 1934 created on 2/5/2018 at 19:42:32 has the file name `stdout_20180205194132_1934.log`.

If `stdoutLogEnabled` is false, errors that occur on app startup are captured and emitted to the event log up to 30 KB. After startup, all additional logs are discarded.

The following sample `aspNetCore` element configures stdout logging at the relative path `.\log\`. Confirm that the app pool user identity has permission to write to the path provided.

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="true"
    stdoutLogFile=".\logs\stdout"
    hostingModel="inprocess">
</aspNetCore>
```

When publishing an app for Azure App Service deployment, the Web SDK sets the `stdoutLogFile` value to `\\?\%home%\LogFiles\stdout`. The `%home` environment variable is predefined for apps hosted by Azure App Service.

For more information on path formats, see [File path formats on Windows systems](/en-us/dotnet/standard/io/file-path-formats).

## Enhanced diagnostic logs

The ASP.NET Core Module is configurable to provide enhanced diagnostics logs. Add the `<handlerSettings>` element to the `<aspNetCore>` element in `web.config`. Setting the `debugLevel` to `TRACE` exposes a higher fidelity of diagnostic information:

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="false"
    stdoutLogFile="\\?\%home%\LogFiles\stdout"
    hostingModel="inprocess">
  <handlerSettings>
    <handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />
    <handlerSetting name="debugLevel" value="FILE,TRACE" />
  </handlerSettings>
</aspNetCore>
```

The module doesn't automatically create folders in the path provided to the `<handlerSetting>` value (`logs` in the preceding example) and should pre-exist in the deployment. The app pool must have write access to the location where the logs are written (use `IIS AppPool\{APP POOL NAME}` to provide write permission, where the placeholder `{APP POOL NAME}` is the app pool name).

See [Configuration with web.config](#configuration-with-webconfig) for an example of the `aspNetCore` element in the `web.config` file.

```
dotnet-hosting-{VERSION}.exe OPT_NO_SHARED_CONFIG_CHECK=1
```

When the path to the shared configuration isn't on the same machine as the IIS installation, follow these steps:

1. Disable the IIS Shared Configuration.
2. Run the installer.
3. Export the updated `applicationHost.config` file to the share.
4. Re-enable the IIS Shared Configuration.

## Module version and Hosting Bundle installer logs

To determine the version of the installed ASP.NET Core Module:

1. On the hosting system, navigate to `%windir%\System32\inetsrv`.
2. Locate the `aspnetcore.dll` file.
3. Right-click the file and select **Properties** from the contextual menu.
4. Select the **Details** tab. The **File version** and **Product version** represent the installed version of the module.

The Hosting Bundle installer logs for the module are found at `C:\\Users\\%UserName%\\AppData\\Local\\Temp`. The file is named `dd_DotNetCoreWinSvrHosting__\{TIMESTAMP}_000_AspNetCoreModule_x64.log`, where the placeholder `{TIMESTAMP}` is the timestamp.

**IIS**

* `%windir%\System32\inetsrv\config\schema\aspnetcore_schema.xml`

* `%windir%\System32\inetsrv\config\schema\aspnetcore_schema_v2.xml`

**IIS Express**

* `%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema.xml`

* `%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema_v2.xml`

### Configuration

**IIS**

* `%windir%\System32\inetsrv\config\applicationHost.config`

**IIS Express**

* Visual Studio: `{APPLICATION ROOT}\.vs\config\applicationHost.config`

* *iisexpress.exe* CLI: `%USERPROFILE%\Documents\IISExpress\config\applicationhost.config`

The files can be found by searching for `aspnetcore` in the `applicationHost.config` file.

The ASP.NET Core Module (ANCM) is a native IIS module that plugs into the IIS pipeline to forward web requests to backend ASP.NET Core apps.

Supported Windows versions:

* Windows 7 or later
* Windows Server 2008 R2 or later

The module only works with Kestrel. The module is incompatible with [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

Because ASP.NET Core apps run in a process separate from the IIS worker process, the module also handles process management. The module starts the process for the ASP.NET Core app when the first request arrives and restarts the app if it crashes. This is essentially the same behavior as seen with ASP.NET 4.x apps that run in-process in IIS that are managed by the [Windows Process Activation Service (WAS)](/en-us/iis/manage/provisioning-and-managing-iis/features-of-the-windows-process-activation-service-was).

The following diagram illustrates the relationship between IIS, the ASP.NET Core Module, and an app:

Requests arrive from the web to the kernel-mode HTTP.sys driver. The driver routes the requests to IIS on the website's configured port, usually 80 (HTTP) or 443 (HTTPS). The module forwards the requests to Kestrel on a random port for the app, which isn't port 80 or 443.

The module specifies the port via an environment variable at startup, and the [IIS integration middleware](iis/?view=aspnetcore-10.0#enable-the-iisintegration-components) configures the server to listen on `http://localhost:{port}`. Additional checks are performed, and requests that don't originate from the module are rejected. The module doesn't support HTTPS forwarding, so requests are forwarded over HTTP even if received by IIS over HTTPS.

After Kestrel picks up the request from the module, the request is pushed into the ASP.NET Core middleware pipeline. The middleware pipeline handles the request and passes it on as an `HttpContext` instance to the app's logic. Middleware added by IIS Integration updates the scheme, remote IP, and pathbase to account for forwarding the request to Kestrel. The app's response is passed back to IIS, which pushes it back out to the HTTP client that initiated the request.

Many native modules, such as Windows Authentication, remain active. To learn more about IIS modules active with the ASP.NET Core Module, see [IIS modules with ASP.NET Core](iis/modules?view=aspnetcore-10.0).

The ASP.NET Core Module can also:

* Set environment variables for the worker process.
* Log stdout output to file storage for troubleshooting startup issues.
* Forward Windows authentication tokens.

## How to install and use the ASP.NET Core Module (ANCM)

For instructions on how to install the ASP.NET Core Module, see [Install the .NET Core Hosting Bundle](iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle).

## Configuration with web.config

The ASP.NET Core Module is configured with the `aspNetCore` section of the `system.webServer` node in the site's *web.config* file.

The following *web.config* file is published for a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd) and configures the ASP.NET Core Module to handle site requests:

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="dotnet"
                arguments=".\MyApp.dll"
                stdoutLogEnabled="false"
                stdoutLogFile=".\logs\stdout" />
  </system.webServer>
</configuration>
```

The following *web.config* is published for a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath=".\MyApp.exe"
                stdoutLogEnabled="false"
                stdoutLogFile=".\logs\stdout" />
  </system.webServer>
</configuration>
```

When an app is deployed to [Azure App Service](https://azure.microsoft.com/products/app-service/), the `stdoutLogFile` path is set to `\\?\%home%\LogFiles\stdout`. The path saves stdout logs to the *LogFiles* folder, which is a location automatically created by the service.

For information on IIS sub-application configuration, see [Host ASP.NET Core on Windows with IIS](iis/?view=aspnetcore-10.0#sub-applications).

### Attributes of the aspNetCore element

| Attribute                 | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Default                                              |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `arguments`               | Optional string attribute.Arguments to the executable specified in **processPath**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |                                                      |
| `disableStartUpErrorPage` | Optional Boolean attribute.If true, the **502.5 - Process Failure** page is suppressed, and the 502 status code page configured in the *web.config* takes precedence.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `false`                                              |
| `forwardWindowsAuthToken` | Optional Boolean attribute.If true, the token is forwarded to the child process listening on %ASPNETCORE\_PORT% as a header 'MS-ASPNETCORE-WINAUTHTOKEN' per request. It's the responsibility of that process to call CloseHandle on this token per request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `true`                                               |
| `processesPerApplication` | Optional integer attribute.Specifies the number of instances of the process specified in the **processPath** setting that can be spun up per app.Setting `processesPerApplication` is discouraged. This attribute will be removed in a future release.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Default: `1` Min: `1` Max: `100`                     |
| `processPath`             | Required string attribute.Path to the executable that launches a process listening for HTTP requests. Relative paths are supported. If the path begins with `.`, the path is considered to be relative to the site root.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |                                                      |
| `rapidFailsPerMinute`     | Optional integer attribute.Specifies the number of times the process specified in **processPath** is allowed to crash per minute. If this limit is exceeded, the module stops launching the process for the remainder of the minute.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Default: `10` Min: `0` Max: `100`                    |
| `requestTimeout`          | Optional timespan attribute.Specifies the duration for which the ASP.NET Core Module waits for a response from the process listening on %ASPNETCORE\_PORT%.In versions of the ASP.NET Core Module that shipped with the release of ASP.NET Core 2.1 or later, the `requestTimeout` is specified in hours, minutes, and seconds.                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Default: `00:02:00` Min: `00:00:00` Max: `360:00:00` |
| `shutdownTimeLimit`       | Optional integer attribute.Duration in seconds that the module waits for the executable to gracefully shutdown when the `app_offline.htm` file is detected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Default: `10` Min: `0` Max: `600`                    |
| `startupTimeLimit`        | Optional integer attribute.Duration in seconds that the module waits for the executable to start a process listening on the port. If this time limit is exceeded, the module kills the process. The module attempts to relaunch the process when it receives a new request and continues to attempt to restart the process on subsequent incoming requests unless the app fails to start **rapidFailsPerMinute** number of times in the last rolling minute.A value of 0 (zero) is **not** considered an infinite timeout.                                                                                                                                                                                                                                                          | Default: `120` Min: `0` Max: `3600`                  |
| `stdoutLogEnabled`        | Optional Boolean attribute.If true, **stdout** and **stderr** for the process specified in **processPath** are redirected to the file specified in **stdoutLogFile**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `false`                                              |
| `stdoutLogFile`           | Optional string attribute.Specifies the relative or absolute file path for which **stdout** and **stderr** from the process specified in **processPath** are logged. Relative paths are relative to the root of the site. Any path starting with `.` are relative to the site root and all other paths are treated as absolute paths. Any folders provided in the path must exist in order for the module to create the log file. Using underscore delimiters, a timestamp, process ID, and file extension (*.log*) are added to the last segment of the **stdoutLogFile** path. If `.\logs\stdout` is supplied as a value, an example stdout log is saved as *stdout\_20180205194132\_1934.log* in the *logs* folder when saved on 2/5/2018 at 19:41:32 with a process ID of 1934. | `aspnetcore-stdout`                                  |

### Setting environment variables

Environment variables can be specified for the process in the `processPath` attribute. Specify an environment variable with the `<environmentVariable>` child element of an `<environmentVariables>` collection element.

Warning

Environment variables set in this section conflict with system environment variables set with the same name. If an environment variable is set in both the *web.config* file and at the system level in Windows, the value from the *web.config* file becomes appended to the system environment variable value (for example, `ASPNETCORE_ENVIRONMENT: Development;Development`), which prevents the app from starting.

The following example sets two environment variables. `ASPNETCORE_ENVIRONMENT` configures the app's environment to `Development`. A developer might temporarily set this value in the *web.config* file in order to force the [Developer Exception Page](../fundamentals/error-handling?view=aspnetcore-10.0) to load when debugging an app exception. `CONFIG_DIR` is an example of a user-defined environment variable, where the developer has written code that reads the value on startup to form a path for loading the app's configuration file.

```
<aspNetCore processPath="dotnet"
      arguments=".\MyApp.dll"
      stdoutLogEnabled="false"
      stdoutLogFile="\\?\%home%\LogFiles\stdout">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
    <environmentVariable name="CONFIG_DIR" value="f:\application_config" />
  </environmentVariables>
</aspNetCore>
```

Warning

Only set the `ASPNETCORE_ENVIRONMENT` environment variable to `Development` on staging and testing servers that aren't accessible to untrusted networks, such as the Internet.

## app\_offline.htm

If a file with the name `app_offline.htm` is detected in the root directory of an app, the ASP.NET Core Module attempts to gracefully shutdown the app and stop processing incoming requests. If the app is still running after the number of seconds defined in `shutdownTimeLimit`, the ASP.NET Core Module kills the running process.

While the `app_offline.htm` file is present, the ASP.NET Core Module responds to requests by sending back the contents of the `app_offline.htm` file. When the `app_offline.htm` file is removed, the next request starts the app.

## Start-up error page

If the ASP.NET Core Module fails to launch the backend process or the backend process starts but fails to listen on the configured port, a *502.5 - Process Failure* status code page appears. To suppress this page and revert to the default IIS 502 status code page, use the `disableStartUpErrorPage` attribute. For more information on configuring custom error messages, see [HTTP Errors \<httpErrors>](/en-us/iis/configuration/system.webServer/httpErrors/).

## Log creation and redirection

The ASP.NET Core Module redirects stdout and stderr console output to disk if the `stdoutLogEnabled` and `stdoutLogFile` attributes of the `aspNetCore` element are set. The module creates any folders in the `stdoutLogFile` path when it creates the log file. The app pool must have write access to the location where the logs are written (use `IIS AppPool\<app_pool_name>` to provide write permission).

Logs aren't rotated, unless process recycling/restart occurs. It's the responsibility of the hoster to limit the disk space the logs consume.

Using the stdout log is only recommended for troubleshooting app startup issues when hosting on IIS or when using [development-time support for IIS with Visual Studio](iis/development-time-iis-support?view=aspnetcore-10.0), not while debugging locally and running the app with IIS Express.

Don't use the stdout log for general app logging purposes. For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

A timestamp and file extension are added automatically when the log file is created. The log file name is composed by appending the timestamp, process ID, and file extension (*.log*) to the last segment of the `stdoutLogFile` path (typically *stdout*) delimited by underscores. If the `stdoutLogFile` path ends with *stdout*, a log for an app with a PID of 1934 created on 2/5/2018 at 19:42:32 has the file name *stdout\_20180205194132\_1934.log*.

The following sample `aspNetCore` element configures stdout logging at the relative path `.\log\`. Confirm that the AppPool user identity has permission to write to the path provided.

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="true"
    stdoutLogFile=".\logs\stdout">
</aspNetCore>
```

When publishing an app for Azure App Service deployment, the Web SDK sets the `stdoutLogFile` value to `\\?\%home%\LogFiles\stdout`. The `%home` environment variable is predefined for apps hosted by Azure App Service.

To create logging filter rules, see the [Apply log filter rules in code](../fundamentals/logging/?view=aspnetcore-10.0#apply-log-filter-rules-in-code) section of the ASP.NET Core logging documentation.

For more information on path formats, see [File path formats on Windows systems](/en-us/dotnet/standard/io/file-path-formats).

## Proxy configuration uses HTTP protocol and a pairing token

The proxy created between the ASP.NET Core Module and Kestrel uses the HTTP protocol. There's no risk of eavesdropping the traffic between the module and Kestrel from a location off of the server.

A pairing token is used to guarantee that IIS proxied the requests received by Kestrel and didn't come from some other source. The pairing token is created and set into an environment variable (`ASPNETCORE_TOKEN`) by the module. The pairing token is also set into a header (`MS-ASPNETCORE-TOKEN`) on every proxied request. IIS middleware checks each request it receives to confirm that the pairing token header value matches the environment variable value. If the token values are mismatched, the request is logged and rejected. The pairing token environment variable and the traffic between the module and Kestrel aren't accessible from a location off of the server. Without knowing the pairing token value, a cyberattacker can't submit requests that bypass the check in the IIS middleware.

## ASP.NET Core Module with an IIS Shared Configuration

The ASP.NET Core Module installer runs with the privileges of the **TrustedInstaller** account. Because the local system account doesn't have modify permission for the share path used by the IIS Shared Configuration, the installer throws an access denied error when attempting to configure the module settings in the *applicationHost.config* file on the share.

When using an IIS Shared Configuration, follow these steps:

1. Disable the IIS Shared Configuration.
2. Run the installer.
3. Export the updated *applicationHost.config* file to the share.
4. Re-enable the IIS Shared Configuration.

## Module version and Hosting Bundle installer logs

To determine the version of the installed ASP.NET Core Module:

1. On the hosting system, navigate to *%windir%\System32\inetsrv*.
2. Locate the *aspnetcore.dll* file.
3. Right-click the file and select **Properties** from the contextual menu.
4. Select the **Details** tab. The **File version** and **Product version** represent the installed version of the module.

The Hosting Bundle installer logs for the module are found at *C:\Users\\%UserName%\AppData\Local\Temp*. The file is named *dd\_DotNetCoreWinSvrHosting\_\_\<timestamp>\_000\_AspNetCoreModule\_x64.log*.

## Module, schema, and configuration file locations

### Module

**IIS (x86/amd64):**

* %windir%\System32\inetsrv\aspnetcore.dll

* %windir%\SysWOW64\inetsrv\aspnetcore.dll

**IIS Express (x86/amd64):**

* %ProgramFiles%\IIS Express\aspnetcore.dll

* %ProgramFiles(x86)%\IIS Express\aspnetcore.dll

### Schema

**IIS**

* %windir%\System32\inetsrv\config\schema\aspnetcore\_schema.xml

**IIS Express**

* %ProgramFiles%\IIS Express\config\schema\aspnetcore\_schema.xml

### Configuration

**IIS**

* %windir%\System32\inetsrv\config\applicationHost.config

**IIS Express**

* Visual Studio: {APPLICATION ROOT}\\.vs\config\applicationHost.config

* *iisexpress.exe* CLI: %USERPROFILE%\Documents\IISExpress\config\applicationhost.config

The files can be found by searching for *aspnetcore* in the *applicationHost.config* file.

## Related content

* [Host ASP.NET Core on Windows with IIS](iis/?view=aspnetcore-10.0)
* [Deploy ASP.NET Core apps to Azure App Service](azure-apps/?view=aspnetcore-10.0)
* [ASP.NET Core Module reference source \[default branch (main)\]](https://github.com/dotnet/aspnetcore/tree/main/src/Servers/IIS/AspNetCoreModuleV2): Use the **Branch** drop down list to select a specific release (for example, `release/3.1`).
* [IIS modules with ASP.NET Core](iis/modules?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/security
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/security.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Authentication and authorization in Minimal APIs

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Minimal APIs support all authentication and authorization options available in ASP.NET Core, and provide extra functionality to improve the experience for managing authentication.

This article describes the support for authentication and authorization in Minimal API applications, and how to configure and test the functionality.

## Review authentication and authorization concepts

Authentication is the process of determining a user's identity while authorization is the process of determining whether a user has access to a resource. Both authentication and authorization scenarios share similar implementation semantics in ASP.NET Core.

* The authentication service, [IAuthenticationService](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationservice), handles all authentication and is used by authentication [middleware](../middleware/?view=aspnetcore-10.0#middleware-added-automatically-by-webapplication).
* The authorization service, [IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice), manages all authorization and is used by the authorization middleware.

### Authentication service

The authentication service uses registered authentication handlers to complete authentication-related actions. For example, an authentication-related action is authenticating a user or signing out a user. Authentication schemes are names that are used to uniquely identify an authentication handler and its configuration options. Authentication handlers are responsible for implementing the strategies for authentication and generating a user's claims given a particular authentication strategy, such as OAuth or OIDC. The configuration options are unique to the strategy as well and provide the handler with configuration that affects authentication behavior, such as redirect URIs.

### Authorization service

In the authorization layer, there are two strategies for determining user access to resources:

* **Role-based strategies** determine a user's access based on their assigned role, such as `Administrator` or `User`. For more information, see [Role-based authorization in ASP.NET Core](../../security/authorization/roles?view=aspnetcore-10.0).

* **Claim-based strategies** determine a user's access based on claims issued by a central authority. For more information, see [Claim-based authorization in ASP.NET Core](../../security/authorization/claims?view=aspnetcore-10.0).

In ASP.NET Core, both strategies are captured into an authorization requirement. The authorization service uses authorization handlers to determine whether or not a particular user meets the authorization requirements for a resource.

## Enable authentication in minimal apps

To enable authentication, call the [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) method to register the required authentication services on the app's service provider.

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication();
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.Run();
```

Typically, a specific authentication strategy is used. In the following sample, the app is configured with support for JSON Web Token (JWT) bearer-based authentication. This example makes use of the APIs available in the [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer) NuGet package.

```
var builder = WebApplication.CreateBuilder(args);
// Requires Microsoft.AspNetCore.Authentication.JwtBearer
builder.Services.AddAuthentication().AddJwtBearer();
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.Run();
```

By default, the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) automatically registers the authentication and authorization middleware if certain authentication and authorization services are enabled. In the following sample, it's not necessary to invoke the [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) or [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) methods to register the middleware. [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) automatically completes the registration after calling the `AddAuthentication` or `AddAuthorization` method.

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.Run();
```

In some cases, such as controlling middleware order, it's necessary to explicitly register authentication and authorization. In the following sample, the authentication middleware runs *after* the CORS middleware runs. For more information on middleware and this automatic behavior, see [Middleware in ASP.NET Core apps](../middleware/?view=aspnetcore-10.0#middleware-added-automatically-by-webapplication).

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/", () => "Hello World!");
app.Run();
```

### Configure authentication strategy

Authentication strategies typically support various configurations that are loaded via options. Minimal apps support loading options from configuration for the following authentication strategies:

* [JWT bearer-based authentication](https://jwt.io/introduction#what-is-json-web-token)
* [OpenID Connect-based authentication](https://openid.net/developers/how-connect-works/)

The ASP.NET Core framework expects to find these options under the `Authentication:Schemes:{SchemeName}` section in the [configuration](../configuration/?view=aspnetcore-10.0). In the following sample, two different schemes, `Bearer` and `LocalAuthIssuer`, are defined with their respective options. The `Authentication:DefaultScheme` option can be used to configure the default authentication strategy.

```
{
  "Authentication": {
    "DefaultScheme":  "LocalAuthIssuer",
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "https://localhost:7259",
          "http://localhost:5259"
        ],
        "ValidIssuer": "dotnet-user-jwts"
      },
      "LocalAuthIssuer": {
        "ValidAudiences": [
          "https://localhost:7259",
          "http://localhost:5259"
        ],
        "ValidIssuer": "local-auth"
      }
    }
  }
}
```

In the *Program.cs* file, two JWT bearer-based authentication strategies are registered with the following scheme names:

* "Bearer"
* "LocalAuthIssuer"

"Bearer" is the typical default scheme in JWT-bearer based enabled apps. However, you can override the default scheme by setting the `DefaultScheme` property as shown in the previous example.

The scheme name is used to uniquely identify an authentication strategy. The name is also used as the lookup key when resolving authentication options from config, as shown in the following example:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
  .AddJwtBearer()
  .AddJwtBearer("LocalAuthIssuer");
  
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.Run();
```

## Configure authorization policies in minimal apps

Authentication identifies and validates the identity of users against an API. Authorization validates and verifies access to resources in an API. The `IAuthorizationService` service registered by the `AddAuthorization` extension method facilitates the authorization. In the following scenario, a `/hello` resource is added that requires a user to present an `admin` role claim with a `greetings_api` scope claim.

Configuration of the authorization requirements on a resource is a two-step process:

1. Define the authorization requirements in a policy globally.
2. Apply individual policies to resources.

In the following code, the [AddAuthorizationBuilder](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.policyservicecollectionextensions.addauthorizationbuilder) method is invoked, which:

* Adds authorization-related services to the DI container.
* Returns an [AuthorizationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder) object that can be used to directly register authorization policies.

The code creates a new authorization policy named `admin_greetings` that encapsulates two authorization requirements:

* A role-based requirement via the [RequireRole](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requirerole) for users with an `admin` role.
* A claim-based requirement via the [RequireClaim](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireclaim) for which the user must provide a `greetings_api` scope claim.

The `admin_greetings` policy is provided as a required policy to the `/hello` endpoint:

```
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorizationBuilder()
  .AddPolicy("admin_greetings", policy =>
        policy
            .RequireRole("admin")
            .RequireClaim("scope", "greetings_api"));

var app = builder.Build();

app.MapGet("/hello", () => "Hello world!")
  .RequireAuthorization("admin_greetings");

app.Run();
```

## Use 'dotnet user-jwts' for development testing

The examples in this article use an app configured with JWT-bearer based authentication. JWT bearer-based authentication requires clients to present a token in the request header, which is used to validate their identity and claims. Typically, a central authority like an identity server issues the tokens.

For development on the local machine, the [dotnet user-jwts](../../security/authentication/jwt-authn?view=aspnetcore-10.0) command-line tool can be used to create bearer tokens.

```
dotnet user-jwts create
```

Note

When invoked on a project, the tool automatically adds the authentication options that match the generated token to the *appsettings.json* file.

Tokens can be configured with various customizations. For example, to create a token for the `admin` role and `greetings_api` scope expected by the authorization policy in the preceding code, run the tool as follows:

```
dotnet user-jwts create --scope "greetings_api" --role "admin"
```

The generated token can then be sent as part of the header in the testing tool of choice. For example, to send the token with curl:

```
curl -i -H "Authorization: Bearer {token}" https://localhost:{port}/hello
```

## Related content

* [The authentication service (`IAuthenticationService`)](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationservice)
* [The authorization service (`IAuthorizationService`)](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice)
* [ASP.NET Core middleware](../middleware/?view=aspnetcore-10.0)
* [Generate tokens with dotnet user-jwts](../../security/authentication/jwt-authn?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-19

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/using-data-protection
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/using-data-protection.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Get started with the Data Protection APIs in ASP.NET Core

[]()

Basically, protecting data consists of the following steps:

1. Create a data protector from a data protection provider.
2. Call the `Protect` method with the data you want to protect.
3. Call the `Unprotect` method with the data you want to turn back into plain text.

Most frameworks and app models, such as ASP.NET Core or SignalR, already configure the data protection system and add it to a service container that is accessed via [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). The following sample demonstrates:

* Configuring a service container for dependency injection and registering the data protection stack.
* Receiving the data protection provider via DI.
* Creating a protector.
* Protecting then unprotecting data.

```
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;

public class Program
{
    public static void Main(string[] args)
    {
        // add data protection services
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddDataProtection();
        var services = serviceCollection.BuildServiceProvider();

        // create an instance of MyClass using the service provider
        var instance = ActivatorUtilities.CreateInstance<MyClass>(services);
        instance.RunSample();
    }

    public class MyClass
    {
        IDataProtector _protector;

        // the 'provider' parameter is provided by DI
        public MyClass(IDataProtectionProvider provider)
        {
            _protector = provider.CreateProtector("Contoso.MyClass.v1");
        }

        public void RunSample()
        {
            Console.Write("Enter input: ");
            string input = Console.ReadLine();

            // protect the payload
            string protectedPayload = _protector.Protect(input);
            Console.WriteLine($"Protect returned: {protectedPayload}");

            // unprotect the payload
            string unprotectedPayload = _protector.Unprotect(protectedPayload);
            Console.WriteLine($"Unprotect returned: {unprotectedPayload}");
        }
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Enter input: Hello world!
 * Protect returned: CfDJ8ICcgQwZZhlAlTZT...OdfH66i1PnGmpCR5e441xQ
 * Unprotect returned: Hello world!
 */
```

When you create a protector you must provide one or more [Purpose Strings](consumer-apis/purpose-strings?view=aspnetcore-10.0). A purpose string provides isolation between consumers. For example, a protector created with a purpose string of "green" wouldn't be able to unprotect data provided by a protector with a purpose of "purple".

Tip

Instances of `IDataProtectionProvider` and `IDataProtector` are thread-safe for multiple callers. It's intended that once a component gets a reference to an `IDataProtector` via a call to `CreateProtector`, it will use that reference for multiple calls to `Protect` and `Unprotect`.

A call to `Unprotect` will throw CryptographicException if the protected payload cannot be verified or deciphered. Some components may wish to ignore errors during unprotect operations; a component which reads authentication cookies might handle this error and treat the request as if it had no cookie at all rather than fail the request outright. Components which want this behavior should specifically catch CryptographicException instead of swallowing all exceptions.

[]()

## Use AddOptions to configure custom repository

Consider the following code which uses a service provider because the implementation of `IXmlRepository` has a dependency on a singleton service:

```
public void ConfigureServices(IServiceCollection services)
{
    // ...

    var sp = services.BuildServiceProvider();
    services.AddDataProtection()
      .AddKeyManagementOptions(o => o.XmlRepository = sp.GetService<IXmlRepository>());
}
```

The preceding code logs the following warning:

*Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'.*

The following code provides the `IXmlRepository` implementation without having to build the service provider and therefore making additional copies of singleton services:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<DataProtectionDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));

    // Register XmlRepository for data protection.
    services.AddOptions<KeyManagementOptions>()
    .Configure<IServiceScopeFactory>((options, factory) =>
    {
        options.XmlRepository = new CustomXmlRepository(factory);
    });

    services.AddRazorPages();
}
```

The preceding code removes the call to `GetService` and hides `IConfigureOptions<T>`.

The following code shows the custom XML repository:

```
using CustomXMLrepo.Data;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

public class CustomXmlRepository : IXmlRepository
{
    private readonly IServiceScopeFactory factory;

    public CustomXmlRepository(IServiceScopeFactory factory)
    {
        this.factory = factory;
    }

    public IReadOnlyCollection<XElement> GetAllElements()
    {
        using (var scope = factory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<DataProtectionDbContext>();
            var keys = context.XmlKeys.ToList()
                .Select(x => XElement.Parse(x.Xml))
                .ToList();
            return keys;
        }
    }

    public void StoreElement(XElement element, string friendlyName)
    {
        var key = new XmlKey
        {
            Xml = element.ToString(SaveOptions.DisableFormatting)
        };

        using (var scope = factory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<DataProtectionDbContext>();
            context.XmlKeys.Add(key);
            context.SaveChanges();
        }
    }
}
```

The following code shows the XmlKey class:

```
public class XmlKey
{
    public Guid Id { get; set; }
    public string Xml { get; set; }

    public XmlKey()
    {
        this.Id = Guid.NewGuid();
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-06-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0003
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0003.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0003: Do not use model binding attributes with route handlers

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0003      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A route handler delegate includes a parameter that has a [`[Bind]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindattribute) attribute.

## Rule description

Route handler endpoints only support a subset of MVC attributes such as [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute), [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute), etc. Unsupported attributes are ignored and result in unexpected binding behavior. For example, the following code results in an analyzer warning:

```
app.MapGet("/todos/{id}", ([Bind] int id) => new Todo { Id = id });
```

## How to fix violations

To fix a violation of this rule, make sure that the endpoint uses one of the allowed model binding attributes.

```
app.MapGet("/todos/{id}", ([FromRoute] int id) => new Todo { Id = id });
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule. An incorrect model binding setup can result in unexpected behavior when resolving parameters at runtime.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core MVC with EF Core - tutorial series

This tutorial has **not** been updated to ASP.NET Core 3.0. It has been updated for ASP.NET Core in .NET 5.

1. [Get started](intro?view=aspnetcore-10.0)
2. [Create, Read, Update, and Delete operations](crud?view=aspnetcore-10.0)
3. [Sorting, filtering, paging, and grouping](sort-filter-page?view=aspnetcore-10.0)
4. [Migrations](migrations?view=aspnetcore-10.0)
5. [Create a complex data model](complex-data-model?view=aspnetcore-10.0)
6. [Reading related data](read-related-data?view=aspnetcore-10.0)
7. [Updating related data](update-related-data?view=aspnetcore-10.0)
8. [Handle concurrency conflicts](concurrency?view=aspnetcore-10.0)
9. [Inheritance](inheritance?view=aspnetcore-10.0)
10. [Advanced topics](advanced?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-05-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/authn-authz
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/authn-authz.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP authentication and authorization

## Introduction

The reverse proxy can be used to authenticate and authorize requests before they are proxied to the destination servers. This can reduce load on the destination servers, add a layer of protection, and ensure consistent policies are implemented across your applications.

## Defaults

No authentication or authorization is performed on requests unless enabled in the route or application configuration.

## Configuration

Authorization policies can be specified per route via [RouteConfig.AuthorizationPolicy](/en-us/dotnet/api/yarp.reverseproxy.configuration.routeconfig) and can be bound from the `Routes` sections of the config file. As with other route properties, this can be modified and reloaded without restarting the proxy. Policy names are case insensitive.

Example:

```
{
  "ReverseProxy": {
    "Routes": {
      "route1" : {
        "ClusterId": "cluster1",
        "AuthorizationPolicy": "customPolicy",
        "Match": {
          "Hosts": [ "localhost" ]
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "cluster1/destination1": {
            "Address": "https://localhost:10001/"
          }
        }
      }
    }
  }
}
```

[Authorization policies](/en-us/aspnet/core/security/authorization/policies) are an ASP.NET Core concept that the proxy utilizes. The proxy provides the above configuration to specify a policy per route and the rest is handled by existing ASP.NET Core authentication and authorization components.

Authorization policies can be configured in the application as follows:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("customPolicy", policy =>
        policy.RequireAuthenticatedUser());
});
```

In Program.cs add the Authorization and Authentication middleware.

```
app.UseAuthentication();
app.UseAuthorization();

app.MapReverseProxy();
```

See [Overview of ASP.NET Core Authentication](../../../security/authentication/?view=aspnetcore-10.0) for setting up your preferred type of authentication.

### Special values

In addition to custom policy names, YARP supports two special values for a route's authorization parameter: `default` and `anonymous`. When a route doesn't specify the parameter, ASP.NET Core uses the fallback policy if one is configured.

#### `default`

The `default` value uses the [AuthorizationOptions.DefaultPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.defaultpolicy). By default, this policy requires an authenticated user.

#### `anonymous`

The `anonymous` value allows anonymous access to the route regardless of the configured fallback policy.

#### Fallback policy

The [AuthorizationOptions.FallbackPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.fallbackpolicy) is used for a route that doesn't specify an authorization parameter. By default, the fallback policy is `null`, so such a route doesn't require authorization. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

## Flowing Credentials

Even after a request has been authorized in the proxy, the destination server may still need to know who the user is (authentication) and what they're allowed to do (authorization). How you flow that information will depend on the type of authentication being used.

### Cookie, bearer, API keys

These authentication types already pass their values in the request headers and these will flow to the destination server by default. That server will still need to verify and interpret those values, causing some double work.

### OAuth2, OpenIdConnect, WsFederation

These protocols are commonly used with remote identity providers. The authentication process can be configured in the proxy application and will result in an authentication cookie. That cookie will flow to the destination server as a normal request header.

### Windows, Negotiate, NTLM, Kerberos

These authentication types are often bound to a specific connection. They are not supported as means of authenticating a user in a destination server behind the YARP proxy (see [#166](https://github.com/microsoft/reverse-proxy/issues/166). They can be used to authenticate an incoming request to the proxy, but that identity information will have to be communicated to the destination server in another form. They can also be used to authenticate the proxy to the destination servers, but only as the proxy's own user, impersonating the client is not supported.

### Client Certificates

Client certificates are a TLS feature and are negotiated as part of a connection. See [these docs](/en-us/aspnet/core/security/authentication/certauth) for additional information. The certificate can be forwarded to the destination server as an HTTP header using the [ClientCert](transforms?view=aspnetcore-10.0#clientcert) transform.

### Swapping authentication types

Authentication types like Windows that don't flow naturally to the destination server will need to be converted in the proxy to an alternate form. For example a JWT bearer token can be created with the user information and set on the proxy request.

These swaps can be performed using [custom request transforms](transforms?view=aspnetcore-10.0#from-code). Detailed examples can be developed for specific scenarios if there is enough community interest. We need more community feedback on how you want to convert and flow identity information.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Web server implementations in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Tom Dykstra](https://github.com/tdykstra), [Steve Smith](https://ardalis.com/), [Stephen Halter](https://twitter.com/halter73), and [Chris Ross](https://github.com/Tratcher)

An ASP.NET Core app runs with an in-process HTTP server implementation. The server implementation listens for HTTP requests and surfaces them to the app as a set of [request features](../request-features?view=aspnetcore-10.0) composed into an [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext).

* [Windows](#tabpanel_1_windows)
* [macOS](#tabpanel_1_macos)
* [Linux](#tabpanel_1_linux)

ASP.NET Core ships with the following:

* [Kestrel server](kestrel?view=aspnetcore-10.0) is the default, cross-platform HTTP server implementation. Kestrel provides the best performance and memory utilization, but it doesn't have some of the advanced features in HTTP.sys. For more information, see [Kestrel vs. HTTP.sys](./?view=aspnetcore-10.0\&tabs=windows#kestrel-vs-httpsys) in the Windows tab.
* IIS HTTP Server is an [in-process server](#hosting-models) for IIS.
* [HTTP.sys server](httpsys?view=aspnetcore-10.0) is a Windows-only HTTP server based on the [HTTP.sys kernel driver and HTTP Server API](/en-us/windows/desktop/Http/http-api-start-page).

When using [IIS](/en-us/iis/get-started/introduction-to-iis/introduction-to-iis-architecture) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), the app either runs:

* In the same process as the IIS worker process (the [in-process hosting model](#hosting-models)) with the IIS HTTP Server. *In-process* is the recommended configuration.
* In a process separate from the IIS worker process (the [out-of-process hosting model](#hosting-models)) with the [Kestrel server](#kestrel).

The [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) is a native IIS module that handles native IIS requests between IIS and the in-process IIS HTTP Server or Kestrel. For more information, see [ASP.NET Core Module (ANCM) for IIS](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0).

## Kestrel vs. HTTP.sys

Kestrel has the following advantages over HTTP.sys:

* Better performance and memory utilization.
* Cross platform
* Agility, it's developed and patched independent of the OS.
* Programmatic port and TLS configuration
* Extensibility that allows for protocols like [PPv2](https://www.haproxy.org/download/3.1/doc/proxy-protocol.txt) and alternate transports.

Http.Sys operates as a shared kernel mode component with the following features that kestrel does not have:

* Port sharing
* Kernel mode windows authentication. [Kestrel supports only user-mode authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0#kestrel).
* Fast proxying via queue transfers
* Direct file transmission
* Response caching

## Hosting models

Several hosting models are available:

* Kestrel self-hosting: The Kestrel web server runs without requiring any other external web server such as IIS or HTTP.sys.

* HTTP.sys self-hosting is an alternative to Kestrel. Kestrel is recommended over HTTP.sys unless the app requires features not available in Kestrel.

* IIS in-process hosting: An ASP.NET Core app runs in the same process as its IIS worker process. IIS in-process hosting provides improved performance over IIS out-of-process hosting because requests aren't proxied over the loopback adapter, a network interface that returns outgoing network traffic back to the same machine. IIS handles process management with the [Windows Process Activation Service (WAS)](/en-us/iis/manage/provisioning-and-managing-iis/features-of-the-windows-process-activation-service-was).

* IIS out-of-process hosting: ASP.NET Core apps run in a process separate from the IIS worker process, and the module handles process management. The module starts the process for the ASP.NET Core app when the first request arrives and restarts the app if it shuts down or crashes. This is essentially the same behavior as seen with apps that run in-process that are managed by the [Windows Process Activation Service (WAS)](/en-us/iis/manage/provisioning-and-managing-iis/features-of-the-windows-process-activation-service-was). Using a separate process also enables hosting more than one app from the same app pool.

For more information, see the following:

* [Kestrel vs. HTTP.sys](#kestrel-vs-httpsys)
* [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)

ASP.NET Core ships with [Kestrel server](kestrel?view=aspnetcore-10.0), which is the default, cross-platform HTTP server.

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). For production hosting, use Windows or Linux server distributions.

ASP.NET Core ships with [Kestrel server](kestrel?view=aspnetcore-10.0), which is the default, cross-platform HTTP server.

## Kestrel

[Kestrel server](kestrel?view=aspnetcore-10.0) is the default, cross-platform HTTP server implementation. Kestrel provides the best performance and memory utilization, but it doesn't have some of the advanced features in HTTP.sys. For more information, see [Kestrel vs. HTTP.sys](#kestrel-vs-httpsys) in this document.

Use Kestrel:

* By itself as an edge server processing requests directly from a network, including the Internet.

* With a *reverse proxy server*, such as [Internet Information Services (IIS)](https://www.iis.net/), [Nginx](https://nginx.org), or [Apache](https://httpd.apache.org/). A reverse proxy server receives HTTP requests from the Internet and forwards them to Kestrel.

Either hosting configuration—with or without a reverse proxy server—is supported.

For Kestrel configuration guidance and information on when to use Kestrel in a reverse proxy configuration, see [Kestrel web server in ASP.NET Core](kestrel?view=aspnetcore-10.0).

* [Windows](#tabpanel_2_windows)
* [macOS](#tabpanel_2_macos)
* [Linux](#tabpanel_2_linux)

ASP.NET Core ships with the following:

* [Kestrel server](kestrel?view=aspnetcore-10.0) is the default, cross-platform HTTP server.
* [HTTP.sys server](httpsys?view=aspnetcore-10.0) is a Windows-only HTTP server based on the [HTTP.sys kernel driver and HTTP Server API](/en-us/windows/desktop/Http/http-api-start-page).

When using [IIS](/en-us/iis/get-started/introduction-to-iis/introduction-to-iis-architecture) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), the app runs in a process separate from the IIS worker process (*out-of-process*) with the [Kestrel server](#kestrel).

Because ASP.NET Core apps run in a process separate from the IIS worker process, the module handles process management. The module starts the process for the ASP.NET Core app when the first request arrives and restarts the app if it shuts down or crashes. This is essentially the same behavior as seen with apps that run in-process that are managed by the [Windows Process Activation Service (WAS)](/en-us/iis/manage/provisioning-and-managing-iis/features-of-the-windows-process-activation-service-was).

The following diagram illustrates the relationship between IIS, the ASP.NET Core Module, and an app hosted out-of-process:

Requests arrive from the web to the kernel-mode HTTP.sys driver. The driver routes the requests to IIS on the website's configured port, usually 80 (HTTP) or 443 (HTTPS). The module forwards the requests to Kestrel on a random port for the app, which isn't port 80 or 443.

The module specifies the port via an environment variable at startup, and the [IIS integration middleware](../../host-and-deploy/iis/?view=aspnetcore-10.0#enable-the-iisintegration-components) configures the server to listen on `http://localhost:{port}`. Additional checks are performed, and requests that don't originate from the module are rejected. The module doesn't support HTTPS forwarding, so requests are forwarded over HTTP even if received by IIS over HTTPS.

After Kestrel picks up the request from the module, the request is pushed into the ASP.NET Core middleware pipeline. The middleware pipeline handles the request and passes it on as an `HttpContext` instance to the app's logic. Middleware added by IIS Integration updates the scheme, remote IP, and pathbase to account for forwarding the request to Kestrel. The app's response is passed back to IIS, which pushes it back out to the HTTP client that initiated the request.

For IIS and ASP.NET Core Module configuration guidance, see the following topics:

* [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)

ASP.NET Core ships with [Kestrel server](kestrel?view=aspnetcore-10.0), which is the default, cross-platform HTTP server.

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). For production hosting, use Windows or Linux server distributions.

ASP.NET Core ships with [Kestrel server](kestrel?view=aspnetcore-10.0), which is the default, cross-platform HTTP server.

### Nginx with Kestrel

For information on how to use Nginx on Linux as a reverse proxy server for Kestrel, see [Host ASP.NET Core on Linux with Nginx](../../host-and-deploy/linux-nginx?view=aspnetcore-10.0).

## HTTP.sys

If ASP.NET Core apps are run on Windows, HTTP.sys is an alternative to Kestrel. Kestrel is recommended over HTTP.sys unless the app requires features not available in Kestrel. For more information, see [HTTP.sys web server implementation in ASP.NET Core](httpsys?view=aspnetcore-10.0).

HTTP.sys can also be used for apps that are only exposed to an internal network.

For HTTP.sys configuration guidance, see [HTTP.sys web server implementation in ASP.NET Core](httpsys?view=aspnetcore-10.0).

## ASP.NET Core server infrastructure

The [IApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder) available in the `Startup.Configure` method exposes the [ServerFeatures](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder.serverfeatures#microsoft-aspnetcore-builder-iapplicationbuilder-serverfeatures) property of type [IFeatureCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection). Kestrel and HTTP.sys only expose a single feature each, [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature), but different server implementations may expose additional functionality.

`IServerAddressesFeature` can be used to find out which port the server implementation has bound at runtime.

## Custom servers

If the built-in servers don't meet the app's requirements, a custom server implementation can be created. The [Open Web Interface for .NET (OWIN) guide](../owin?view=aspnetcore-10.0) demonstrates how to write a [Nowin](https://github.com/Bobris/Nowin)-based [IServer](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.iserver) implementation. Only the feature interfaces that the app uses require implementation, though at a minimum [IHttpRequestFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttprequestfeature) and [IHttpResponseFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpresponsefeature) must be supported.

## Server startup

The server is launched when the Integrated Development Environment (IDE) or editor starts the app:

* [Visual Studio](https://visualstudio.microsoft.com): Launch profiles can be used to start the app and server with either [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview)/[ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) or the console.
* [Visual Studio Code](https://code.visualstudio.com/): The app and server are started by [Omnisharp](https://github.com/OmniSharp/omnisharp-vscode), which activates the CoreCLR debugger.

When launching the app from a command prompt in the project's folder, [dotnet run](/en-us/dotnet/core/tools/dotnet-run) launches the app and server (Kestrel and HTTP.sys only). The configuration is specified by the `-c|--configuration` option, which is set to either `Debug` (default) or `Release`.

A `launchSettings.json` file provides configuration when launching an app with `dotnet run` or with a debugger built into tooling, such as Visual Studio. If launch profiles are present in a `launchSettings.json` file, use the `--launch-profile {PROFILE NAME}` option with the `dotnet run` command or select the profile in Visual Studio. For more information, see [dotnet run](/en-us/dotnet/core/tools/dotnet-run) and [.NET distribution packaging](/en-us/dotnet/core/build/distribution-packaging).

## HTTP/2 support

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is supported with ASP.NET Core in the following deployment scenarios:

* [Kestrel](kestrel/http2?view=aspnetcore-10.0)

  * Operating system

    * Windows Server 2016/Windows 10 or later†
    * Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later)
    * macOS 10.15 or later

  * Target framework: .NET Core 2.2 or later

* [HTTP.sys](httpsys?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later
  * Target framework: Not applicable to HTTP.sys deployments.

* [IIS (in-process)](../../host-and-deploy/iis/?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later; IIS 10 or later
  * Target framework: .NET Core 2.2 or later

* [IIS (out-of-process)](../../host-and-deploy/iis/?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later; IIS 10 or later
  * Public-facing edge server connections use HTTP/2, but the reverse proxy connection to Kestrel uses HTTP/1.1.
  * Target framework: Not applicable to IIS out-of-process deployments.

†Kestrel has limited support for HTTP/2 on Windows Server 2012 R2 and Windows 8.1. Support is limited because the list of supported TLS cipher suites available on these operating systems is limited. A certificate generated using an Elliptic Curve Digital Signature Algorithm (ECDSA) may be required to secure TLS connections.

* [Kestrel](kestrel/http2?view=aspnetcore-10.0)

  * Operating system

    * Windows Server 2016/Windows 10 or later†
    * Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later)
    * HTTP/2 will be supported on macOS in a future release.

  * Target framework: .NET Core 2.2 or later

* [HTTP.sys](httpsys?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later
  * Target framework: Not applicable to HTTP.sys deployments.

* [IIS (in-process)](../../host-and-deploy/iis/?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later; IIS 10 or later
  * Target framework: .NET Core 2.2 or later

* [IIS (out-of-process)](../../host-and-deploy/iis/?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later; IIS 10 or later
  * Public-facing edge server connections use HTTP/2, but the reverse proxy connection to Kestrel uses HTTP/1.1.
  * Target framework: Not applicable to IIS out-of-process deployments.

†Kestrel has limited support for HTTP/2 on Windows Server 2012 R2 and Windows 8.1. Support is limited because the list of supported TLS cipher suites available on these operating systems is limited. A certificate generated using an Elliptic Curve Digital Signature Algorithm (ECDSA) may be required to secure TLS connections.

* [Kestrel](kestrel?view=aspnetcore-10.0#http2-support)

  * Operating system

    * Windows Server 2016/Windows 10 or later†
    * Linux with OpenSSL 1.0.2 or later (for example, Ubuntu 16.04 or later)
    * HTTP/2 will be supported on macOS in a future release.

  * Target framework: .NET Core 2.2 or later

* [HTTP.sys](httpsys?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later
  * Target framework: Not applicable to HTTP.sys deployments.

* [IIS (in-process)](../../host-and-deploy/iis/?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later; IIS 10 or later
  * Target framework: .NET Core 2.2 or later

* [IIS (out-of-process)](../../host-and-deploy/iis/?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later; IIS 10 or later
  * Public-facing edge server connections use HTTP/2, but the reverse proxy connection to Kestrel uses HTTP/1.1.
  * Target framework: Not applicable to IIS out-of-process deployments.

†Kestrel has limited support for HTTP/2 on Windows Server 2012 R2 and Windows 8.1. Support is limited because the list of supported TLS cipher suites available on these operating systems is limited. A certificate generated using an Elliptic Curve Digital Signature Algorithm (ECDSA) may be required to secure TLS connections.

* [HTTP.sys](httpsys?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later
  * Target framework: Not applicable to HTTP.sys deployments.

* [IIS (out-of-process)](../../host-and-deploy/iis/?view=aspnetcore-10.0#http2-support)

  * Windows Server 2016/Windows 10 or later; IIS 10 or later
  * Public-facing edge server connections use HTTP/2, but the reverse proxy connection to Kestrel uses HTTP/1.1.
  * Target framework: Not applicable to IIS out-of-process deployments.

An HTTP/2 connection must use [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) and TLS 1.2 or later. For more information, see the topics that pertain to your server deployment scenarios.

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

## Related content

* [Kestrel web server in ASP.NET Core](kestrel?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)
* [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0)
* [Deploy ASP.NET Core apps to Azure App Service](../../host-and-deploy/azure-apps/?view=aspnetcore-10.0)
* [Host ASP.NET Core on Linux with Nginx](../../host-and-deploy/linux-nginx?view=aspnetcore-10.0)
* [HTTP.sys web server implementation in ASP.NET Core](httpsys?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/webassembly-native-dependencies
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/webassembly-native-dependencies.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor WebAssembly native dependencies

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Blazor WebAssembly apps can use native dependencies built to run on WebAssembly. You can statically link native dependencies into the .NET WebAssembly runtime using the **.NET WebAssembly build tools**, the same tools used to [ahead-of-time (AOT) compile](webassembly-build-tools-and-aot?view=aspnetcore-10.0#ahead-of-time-aot-compilation) a Blazor app to WebAssembly and to [relink the runtime to remove unused features](webassembly-build-tools-and-aot?view=aspnetcore-10.0#runtime-relinking).

*This article only applies to Blazor WebAssembly.*

## .NET WebAssembly build tools

The .NET WebAssembly build tools are based on [Emscripten](https://emscripten.org/), a compiler toolchain for the web platform. For more information on the build tools, including installation, see [ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation](webassembly-build-tools-and-aot?view=aspnetcore-10.0).

Add native dependencies to a Blazor WebAssembly app by adding `NativeFileReference` items in the app's project file. When the project is built, each `NativeFileReference` is passed to Emscripten by the .NET WebAssembly build tools so that they are compiled and linked into the runtime. Next, [`p/invoke`](/en-us/dotnet/standard/native-interop/pinvoke) into the native code from the app's .NET code.

Generally, any portable native code can be used as a native dependency with Blazor WebAssembly. You can add native dependencies to C/C++ code or code previously compiled using Emscripten:

* Object files (`.o`)
* Archive files (`.a`)
* Bitcode (`.bc`)
* Standalone WebAssembly modules (`.wasm`)

Prebuilt dependencies typically must be built using the same version of Emscripten used to build the .NET WebAssembly runtime.

Note

For [Mono](https://github.com/mono/mono)/WebAssembly MSBuild properties and targets, see [`WasmApp.targets` (`dotnet/runtime` GitHub repository)](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/build/WasmApp.Common.targets). Official documentation for common MSBuild properties is planned per [Document blazor msbuild configuration options (`dotnet/docs` #27395)](https://github.com/dotnet/docs/issues/27395).

## Use native code

This section demonstrates how to add a simple native C function to a Blazor WebAssembly app.

Create a new Blazor WebAssembly project.

Add a `Test.c` file to the project with a C function for computing factorials.

`Test.c`:

```
int fact(int n)
{
    if (n == 0) return 1;
    return n * fact(n - 1);
}
```

Add a `NativeFileReference` MSBuild item for `Test.c` in the app's project file (`.csproj`):

```
<ItemGroup>
  <NativeFileReference Include="Test.c" />
</ItemGroup>
```

In a Razor component, add a [`[DllImport]` attribute](/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute) for the `fact` function in the generated `Test` library and call the `fact` method from .NET code in the component.

`Pages/NativeCTest.razor`:

```
@page "/native-c-test"
@using System.Runtime.InteropServices

<PageTitle>Native C</PageTitle>

<h1>Native C Test</h1>

<p>
    @@fact(3) result: @fact(3)
</p>

@code {
    [DllImport("Test")]
    static extern int fact(int n);
}
```

When you build the app with the .NET WebAssembly build tools installed, the native C code is compiled and linked into the .NET WebAssembly runtime (`dotnet.wasm`). After the app is built, run the app to see the rendered factorial value.

## C++ managed method callbacks

Label managed methods that are passed to C++ with the [`[UnmanagedCallersOnly]` attribute](/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute). The method marked with the attribute must be `static`. To call an instance method in a Razor component, pass a [GCHandle](/en-us/dotnet/api/system.runtime.interopservices.gchandle) for the instance to C++ and then pass it back to native. Alternatively, use some other method to identify the instance of the component.

The method marked with the [`[DllImport]` attribute](/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute) must use a [function pointer (C# 9 or later)](/en-us/dotnet/csharp/language-reference/language-specification/unsafe-code#2433-function-pointers) rather than a delegate type for the callback argument.

Note

For C# function pointer types in [`[DllImport]`](/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute) methods, use [IntPtr](/en-us/dotnet/api/system.intptr) in the method signature on the managed side instead of `delegate *unmanaged<int, void>`. For more information, see [\[WASM\] callback from native code to .NET: Parsing function pointer types in signatures is not supported (`dotnet/runtime` #56145)](https://github.com/dotnet/runtime/issues/56145).

## Package native dependencies in a NuGet package

NuGet packages can contain native dependencies for use on WebAssembly. These libraries and their native functionality are then available to any Blazor WebAssembly app. The files for the native dependencies should be built for WebAssembly and packaged in the `browser-wasm` [architecture-specific folder](/en-us/nuget/create-packages/supporting-multiple-target-frameworks#architecture-specific-folders). WebAssembly-specific dependencies aren't referenced automatically and must be referenced manually as `NativeFileReference`s. Package authors can choose to add the native references by including a `.props` file in the package with the references.

## SkiaSharp example library use

[SkiaSharp](https://github.com/mono/SkiaSharp) is a cross-platform 2D graphics library for .NET based on the native [Skia graphics library](https://skia.org/) with support for Blazor WebAssembly.

The section demonstrates how to implement SkiaSharp in a Blazor WebAssembly app.

Add a package reference to the [`SkiaSharp.Views.Blazor` package](https://www.nuget.org/packages/SkiaSharp.Views.Blazor) in a Blazor WebAssembly project. Use Visual Studio's process for adding packages to an app (**Manage NuGet Packages** with **Include prerelease** selected) or execute the [`dotnet add package`](/en-us/dotnet/core/tools/dotnet-add-package) command in a command shell with the `--prerelease` option:

```
dotnet add package –-prerelease SkiaSharp.Views.Blazor
```

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Add a `SKCanvasView` component to the app with the following:

* `SkiaSharp` and `SkiaSharp.Views.Blazor` namespaces.
* Logic to draw in the SkiaSharp Canvas View component (`SKCanvasView`).

`Pages/NativeDependencyExample.razor`:

```
@page "/native-dependency-example"
@using SkiaSharp
@using SkiaSharp.Views.Blazor

<PageTitle>Native dependency</PageTitle>

<h1>Native dependency example with SkiaSharp</h1>

<SKCanvasView OnPaintSurface="OnPaintSurface" />

@code {
    private void OnPaintSurface(SKPaintSurfaceEventArgs e)
    {
        var canvas = e.Surface.Canvas;

        canvas.Clear(SKColors.White);

        using var paint = new SKPaint
        {
            Color = SKColors.Black,
            IsAntialias = true,
            TextSize = 24
        };

        canvas.DrawText("SkiaSharp", 0, 24, paint);
    }
}
```

Build the app, which might take several minutes. Run the app and navigate to the `NativeDependencyExample` component at `/native-dependency-example`.

## Related content

* [.NET WebAssembly build tools](webassembly-build-tools-and-aot?view=aspnetcore-10.0)
* [Mono/WebAssembly MSBuild properties and targets (`WasmApp.targets`, `dotnet/runtime` GitHub repository)](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/build/WasmApp.Common.targets)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/http-caching-issues
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/http-caching-issues.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Avoid HTTP caching issues when upgrading ASP.NET Core Blazor apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to avoid HTTP caching issues when upgrading Blazor apps.

When Blazor apps are incorrectly upgraded or configured, it can result in non-seamless upgrades for existing users. This article discusses some of the common HTTP caching issues that can occur when upgrading Blazor apps across major versions. It also provides some recommended actions to ensure a smooth transition for your users.

While future Blazor releases might provide better solutions for dealing with HTTP caching issues, it's ultimately up to the app to correctly configure caching. Proper caching configuration ensures that the app's users always have the most up-to-date version of the app, improving their experience and reducing the likelihood of encountering errors.

Common problems that negatively impact the user upgrade experience include:

* **Incorrect handling of project and package updates**: This happens if you don't update all of the app's deployed projects to use the same major framework version or if you use packages from a previous version when a newer version is available as part of the major upgrade.
* **Incorrect configuration of caching headers**: HTTP caching headers control how, where, and for how long the app's responses are cached. If headers aren't configured correctly, users might receive stale or mismatched files. This includes [Blazor bundle assets caching](bundle-caching-and-integrity-check-failures?view=aspnetcore-10.0), where server caching headers must be properly set to avoid caching problems on the client.
* **Incorrect configuration of other layers**: Content Delivery Networks (CDNs) and other layers of the deployed app can cause issues if incorrectly configured. For example, CDNs are designed to cache and deliver content to improve performance and reduce latency. If a CDN is incorrectly serving cached versions of assets, it can lead to stale content delivery to the user.

## Detect and diagnose upgrade issues

Upgrade issues typically appear as a failure to start the app in the browser. Normally, a warning indicates the presence of a stale asset or an asset that's missing or inconsistent with the app.

* First, check if the app loads successfully within a clean browser instance. Use a private browser mode to load the app, such as Microsoft Edge InPrivate mode or Google Chrome Incognito mode. If the app fails to load, it likely means that one or more packages or the framework wasn't correctly updated.
* If the app loads correctly in a clean browser instance, then it's likely that the app is being served from a stale cache. In most cases, a hard browser refresh with `Ctrl`+`F5` flushes the cache, which permits the app to load and run with the latest assets.
* If the app continues to fail, then it's likely that a stale CDN cache is serving the app. Try to flush the DNS cache via whatever mechanism your CDN provider offers.

## Recommended actions before an upgrade

The prior process for serving the app might make the update process more challenging. For example, avoiding or incorrectly using caching headers in the past can lead to current caching problems for users. You can take the actions in the following sections to mitigate the issue and improve the upgrade process for users.

### Align framework packages with the framework version

Ensure that framework packages line up with the framework version. Using packages from a previous version when a newer version is available can lead to compatibility issues. It's also important to ensure that all of the app's deployed projects use the same major framework version. This consistency helps to avoid unexpected behavior and errors.

### Verify the presence of correct caching headers

The correct caching headers should be present on responses to resource requests. This includes `ETag`, `Cache-Control`, and other caching headers. The configuration of these headers is dependent on the hosting service or hosting server platform. They are particularly important for assets such as the [Blazor script](../../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) and anything the script downloads.

Incorrect HTTP caching headers may also impact service workers. Service workers rely on caching headers to manage cached resources effectively. Therefore, incorrect or missing headers can disrupt the service worker's functionality.

### Use `Clear-Site-Data` to delete state in the browser

Consider using the [`Clear-Site-Data` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Clear-Site-Data) to delete state in the browser.

Usually the source of cache state problems is limited to the HTTP browser cache, so use of the `cache` directive should be sufficient. This action can help to ensure that the browser fetches the latest resources from the server, rather than serving stale content from the cache.

You can optionally include the `storage` directive to clear local storage caches at the same time that you're clearing the HTTP browser cache. However, apps that use client storage might experience a loss of important information if the `storage` directive is used.

### Append a query string to the Blazor script tag

If none of the previous recommended actions are effective, possible to use for your deployment, or apply to your app, consider temporarily appending a query string to the Blazor script's `<script>` tag source. This action should be enough in most situations to force the browser to bypass the local HTTP cache and download a new version of the app. There's no need to read or use the query string in the app.

In the following example, the query string `temporaryQueryString=1` is temporarily applied to the `<script>` tag's relative external source URI:

```
<script src="_framework/blazor.webassembly.js?temporaryQueryString=1"></script>
```

After all of the app's users have reloaded the app, the query string can be removed.

Alternatively, you can apply a persistent query string with relevant versioning. The following example assumes that the version of the app matches the .NET release version (`8` for .NET 8):

```
<script src="_framework/blazor.webassembly.js?version=8"></script>
```

For the location of the Blazor script `<script>` tag, see [ASP.NET Core Blazor project structure](../../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/image-tag-helper
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/image-tag-helper.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Image Tag Helper in ASP.NET Core

By [Peter Kellner](https://peterkellner.net)

The Image Tag Helper enhances the `<img>` tag to provide cache-busting behavior for static image files.

A cache-busting string is a unique value representing the hash of the static image file appended to the asset's URL. The unique string prompts clients (and some proxies) to reload the image from the host web server and not from the client's cache.

If the image source (`src`) is a static file on the host web server:

* A unique cache-busting string is appended as a query parameter to the image source.
* If the file on the host web server changes, a unique request URL is generated that includes the updated request parameter.

For an overview of Tag Helpers, see [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0).

## Image Tag Helper Attributes

### src

To activate the Image Tag Helper, the `src` attribute is required on the `<img>` element.

The image source (`src`) must point to a physical static file on the server. If the `src` is a remote URI, the cache-busting query string parameter isn't generated.

### asp-append-version

When `asp-append-version` is specified with a `true` value along with a `src` attribute, the Image Tag Helper is invoked.

The following example uses an Image Tag Helper:

```
<img src="~/images/asplogo.png" asp-append-version="true">
```

If the static file exists in the directory */wwwroot/images/*, the generated HTML is similar to the following (the hash will be different):

```
<img src="/images/asplogo.png?v=Kl_dqr9NVtnMdsM2MUg4qthUnWZm5T1fCEimBPWDNgM">
```

The value assigned to the parameter `v` is the hash value of the `asplogo.png` file on disk. If the web server is unable to obtain read access to the static file, no `v` parameter is added to the `src` attribute in the rendered markup.

For a Tag Helper to generate a version for a static file outside `wwwroot`, see [Serve files from multiple locations](../../../../fundamentals/static-files?view=aspnetcore-10.0#serve-files-from-multiple-locations)

## Hash caching behavior

The Image Tag Helper uses the cache provider on the local web server to store the calculated `Sha512` hash of a given file. If the file is requested multiple times, the hash isn't recalculated. The cache is invalidated by a file watcher that's attached to the file when the file's `Sha512` hash is calculated. When the file changes on disk, a new hash is calculated and cached.

## Related content

* [Cache in-memory in ASP.NET Core](../../../../performance/caching/memory?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/spa/intro
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/spa/intro.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Overview of Single Page Apps (SPAs) in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Visual Studio provides project templates for creating single-page apps (SPAs) based on JavaScript technologies, such as [Angular](https://angular.dev/), [React](https://react.dev/), and [Vue](https://vuejs.org/) that have an ASP.NET Core backend. These templates:

* Create a Visual Studio solution with a frontend project and a backend project.
* Use the Visual Studio project type for JavaScript and TypeScript (*.esproj*) for the frontend.
* Use an ASP.NET Core project for the backend.

Projects created by using the Visual Studio templates can be run from the command line on Windows, Linux, and macOS. To run the app, use `dotnet run --launch-profile https` to run the server project. Running the server project automatically starts the frontend JavaScript development server. The `https` launch profile is currently required.

## Visual Studio tutorials

To get started, follow one of the tutorials in the Visual Studio documentation:

* [Create an ASP.NET Core app with Angular](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-angular)
* [Create an ASP.NET Core app with React](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-react)
* [Create an ASP.NET Core app with Vue](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-vue)

## Related content

* [Developing Single Page Apps](/en-us/aspnet/core/client-side/spa/intro?view=aspnetcore-7.0\&preserve-view=true#developing-single-page-apps)

## Architecture of Single Page Application templates

The Single Page Application (SPA) templates for [Angular](https://angular.dev/) and [React](https://reactjs.org/) offer the ability to develop Angular and React apps that are hosted inside a .NET backend server.

At publish time, the files of the Angular and React app are copied to the `wwwroot` folder and are served via the [static file middleware](../../fundamentals/static-files?view=aspnetcore-10.0).

Rather than returning HTTP 404 (Not Found), a fallback route handles unknown requests to the backend and serves the `index.html` for the SPA.

During development, the app is configured to use the frontend proxy. React and Angular use the same frontend proxy.

When the app launches, the `index.html` page is opened in the browser. A special middleware that is only enabled in development:

* Intercepts the incoming requests.
* Checks whether the proxy is running.
* Redirects to the URL for the proxy if it's running or launches a new instance of the proxy.
* Returns a page to the browser that auto refreshes every few seconds until the proxy is up and the browser is redirected.

The primary benefit the ASP.NET Core SPA templates provide:

* Launches a proxy if it's not already running.
* Setting up HTTPS.
* Configuring some requests to be proxied to the backend ASP.NET Core server.

When the browser sends a request for a backend endpoint, for example `/weatherforecast` in the templates. The SPA proxy receives the request and sends it back to the server transparently. The server responds and the SPA proxy sends the request back to the browser:

## Published Single Page Apps

When the app is published, the SPA becomes a collection of files in the `wwwroot` folder.

There is no runtime component required to serve the app:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();


app.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action=Index}/{id?}");

app.MapFallbackToFile("index.html");

app.Run();
```

In the preceding template generated `Program.cs` file:

* `app.`[UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) allows the files to be served.
* `app.`[MapFallbackToFile](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfilesendpointroutebuilderextensions.mapfallbacktofile)`("index.html")` enables serving the default document for any unknown request the server receives.

When the app is published with [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish), the following tasks in the `csproj` file ensures that [`npm restore`](https://www.npmjs.com/package/restore) runs and that the appropriate npm script runs to generate the production artifacts:

```
  <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
    <!-- Ensure Node.js is installed -->
    <Exec Command="node --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
    </Exec>
    <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
    <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
  </Target>

  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
      <DistFiles Include="$(SpaRoot)build\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
        <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>
</Project>
```

## Developing Single Page Apps

The project file defines a few properties that control the behavior of the app during development:

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

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
    <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
    <IsPackable>false</IsPackable>
    <SpaRoot>ClientApp\</SpaRoot>
    <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
    <SpaProxyServerUrl>https://localhost:44414</SpaProxyServerUrl>
    <SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.1" />
  </ItemGroup>

  <ItemGroup>
    <!-- Don't publish the SPA source files, but do show them in the project files list -->
    <Content Remove="$(SpaRoot)**" />
    <None Remove="$(SpaRoot)**" />
    <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
  </ItemGroup>

  <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
    <!-- Ensure Node.js is installed -->
    <Exec Command="node --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
    </Exec>
    <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
    <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
  </Target>

  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
      <DistFiles Include="$(SpaRoot)build\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
        <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>
</Project>
```

* `SpaProxyServerUrl`: Controls the URL where the server expects the SPA proxy to be running. This is the URL:

  * The server pings after launching the proxy to know if it's ready.
  * Where it redirects the browser after a successful response.

* `SpaProxyLaunchCommand`: The command the server uses to launch the SPA proxy when it detects the proxy is not running.

The package `Microsoft.AspNetCore.SpaProxy` is responsible for the preceding logic to detect the proxy and redirect the browser.

The [hosting startup assembly](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0) defined in `Properties/launchSettings.json` is used to automatically add the required components during development necessary to detect if the proxy is running and launch it otherwise:

```
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:51783",
      "sslPort": 44329
    }
  },
  "profiles": {
    "MyReact": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:7145;http://localhost:5273",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
      }
    }
  }
}
```

### Setup for the client app

This setup is specific to the frontend framework the app is using, however many aspects of the configuration are similar.

#### Angular setup

The template generated `ClientApp/package.json` file:

```
{
  "name": "myangular",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "prestart": "node aspnetcore-https",
    "start": "run-script-os",
    "start:windows": "ng serve --port 44483 --ssl --ssl-cert \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.pem\" --ssl-key \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.key\"",
    "start:default": "ng serve --port 44483 --ssl --ssl-cert \"$HOME/.aspnet/https/${npm_package_name}.pem\" --ssl-key \"$HOME/.aspnet/https/${npm_package_name}.key\"",
    "build": "ng build",
    "build:ssr": "ng run MyAngular:server:dev",
    "watch": "ng build --watch --configuration development",
    "test": "ng test"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^14.1.3",
    "@angular/common": "^14.1.3",
    "@angular/compiler": "^14.1.3",
    "@angular/core": "^14.1.3",
    "@angular/forms": "^14.1.3",
    "@angular/platform-browser": "^14.1.3",
    "@angular/platform-browser-dynamic": "^14.1.3",
    "@angular/platform-server": "^14.1.3",
    "@angular/router": "^14.1.3",
    "bootstrap": "^5.2.0",
    "jquery": "^3.6.0",
    "oidc-client": "^1.11.5",
    "popper.js": "^1.16.0",
    "run-script-os": "^1.1.6",
    "rxjs": "~7.5.6",
    "tslib": "^2.4.0",
    "zone.js": "~0.11.8"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^14.1.3",
    "@angular/cli": "^14.1.3",
    "@angular/compiler-cli": "^14.1.3",
    "@types/jasmine": "~4.3.0",
    "@types/jasminewd2": "~2.0.10",
    "@types/node": "^18.7.11",
    "jasmine-core": "~4.3.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.1.1",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "^2.0.0",
    "typescript": "~4.7.4"
  },
  "overrides": {
    "autoprefixer": "10.4.5"
  },
  "optionalDependencies": {}
}
```

* Contains scripts that launching the angular development server:

* The `prestart` script invokes `ClientApp/aspnetcore-https.js`, which is responsible for ensuring the development server HTTPS certificate is available to the SPA proxy server.

* The `start:windows` and `start:default`:

  * Launch the Angular development server via [`ng serve`](https://angular.dev/cli/serve).
  * Provide the port, the options to use HTTPS, and the path to the certificate and the associated key. The provide port number matches the port number specified in the `.csproj` file.

The template generated `ClientApp/angular.json` file contains:

* The `serve` command.

* A `proxyconfig` element in the `development` configuration to indicate that `proxy.conf.js` should be used to configure the frontend proxy, as shown in the following highlighted JSON:

  ```
  {
    "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
    "version": 1,
    "newProjectRoot": "projects",
    "projects": {
      "MyAngular": {
        "projectType": "application",
        "schematics": {
          "@schematics/angular:application": {
            "strict": true
          }
        },
        "root": "",
        "sourceRoot": "src",
        "prefix": "app",
        "architect": {
          "build": {
            "builder": "@angular-devkit/build-angular:browser",
            "options": {
              "progress": false,
              "outputPath": "dist",
              "index": "src/index.html",
              "main": "src/main.ts",
              "polyfills": "src/polyfills.ts",
              "tsConfig": "tsconfig.app.json",
              "allowedCommonJsDependencies": [
                "oidc-client"
              ],
              "assets": [
                "src/assets"
              ],
              "styles": [
                "node_modules/bootstrap/dist/css/bootstrap.min.css",
                "src/styles.css"
              ],
              "scripts": []
            },
            "configurations": {
              "production": {
                "budgets": [
                  {
                    "type": "initial",
                    "maximumWarning": "500kb",
                    "maximumError": "1mb"
                  },
                  {
                    "type": "anyComponentStyle",
                    "maximumWarning": "2kb",
                    "maximumError": "4kb"
                  }
                ],
                "fileReplacements": [
                  {
                    "replace": "src/environments/environment.ts",
                    "with": "src/environments/environment.prod.ts"
                  }
                ],
                "outputHashing": "all"
              },
              "development": {
                "buildOptimizer": false,
                "optimization": false,
                "vendorChunk": true,
                "extractLicenses": false,
                "sourceMap": true,
                "namedChunks": true
              }
            },
            "defaultConfiguration": "production"
          },
          "serve": {
            "builder": "@angular-devkit/build-angular:dev-server",
            "configurations": {
              "production": {
                "browserTarget": "MyAngular:build:production"
              },
              "development": {
                "browserTarget": "MyAngular:build:development",
                "proxyConfig": "proxy.conf.js"
              }
            },
            "defaultConfiguration": "development"
          },
          "extract-i18n": {
            "builder": "@angular-devkit/build-angular:extract-i18n",
            "options": {
              "browserTarget": "MyAngular:build"
            }
          },
          "test": {
            "builder": "@angular-devkit/build-angular:karma",
            "options": {
              "main": "src/test.ts",
              "polyfills": "src/polyfills.ts",
              "tsConfig": "tsconfig.spec.json",
              "karmaConfig": "karma.conf.js",
              "assets": [
                "src/assets"
              ],
              "styles": [
                "src/styles.css"
              ],
              "scripts": []
            }
          },
          "server": {
            "builder": "@angular-devkit/build-angular:server",
            "options": {
              "outputPath": "dist-server",
              "main": "src/main.ts",
              "tsConfig": "tsconfig.server.json"
            },
            "configurations": {
              "dev": {
                "optimization": true,
                "outputHashing": "all",
                "sourceMap": false,
                "namedChunks": false,
                "extractLicenses": true,
                "vendorChunk": true
              },
              "production": {
                "optimization": true,
                "outputHashing": "all",
                "sourceMap": false,
                "namedChunks": false,
                "extractLicenses": true,
                "vendorChunk": false
              }
            }
          }
        }
      }
    },
    "defaultProject": "MyAngular"
  }
  ```

`ClientApp/proxy.conf.js` defines the routes that need to be proxied back to the server backend. The general set of options is defined at [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) for react and angular since they both use the same proxy.

The following highlighted code from `ClientApp/proxy.conf.js` uses logic based on the environment variables set during development to determine the port the backend is running on:

```
const { env } = require('process');

const target = env.ASPNETCORE_HTTPS_PORTS ? `https://localhost:${env.ASPNETCORE_HTTPS_PORTS}` :
  env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:51951';

const PROXY_CONFIG = [
  {
    context: [
      "/weatherforecast",
   ],
    target: target,
    secure: false,
    headers: {
      Connection: 'Keep-Alive'
    }
  }
]

module.exports = PROXY_CONFIG;
```

#### React setup

* The `package.json` scripts section contains the following scripts that launches the react app during development, as shown in the following highlighted code:

  ```
  {
    "name": "myreact",
    "version": "0.1.0",
    "private": true,
    "dependencies": {
      "bootstrap": "^5.2.0",
      "http-proxy-middleware": "^2.0.6",
      "jquery": "^3.6.0",
      "merge": "^2.1.1",
      "oidc-client": "^1.11.5",
      "react": "^18.2.0",
      "react-dom": "^18.2.0",
      "react-router-bootstrap": "^0.26.2",
      "react-router-dom": "^6.3.0",
      "react-scripts": "^5.0.1",
      "reactstrap": "^9.1.3",
      "rimraf": "^3.0.2",
      "web-vitals": "^2.1.4",
      "workbox-background-sync": "^6.5.4",
      "workbox-broadcast-update": "^6.5.4",
      "workbox-cacheable-response": "^6.5.4",
      "workbox-core": "^6.5.4",
      "workbox-expiration": "^6.5.4",
      "workbox-google-analytics": "^6.5.4",
      "workbox-navigation-preload": "^6.5.4",
      "workbox-precaching": "^6.5.4",
      "workbox-range-requests": "^6.5.4",
      "workbox-routing": "^6.5.4",
      "workbox-strategies": "^6.5.4",
      "workbox-streams": "^6.5.4"
    },
    "devDependencies": {
      "ajv": "^8.11.0",
      "cross-env": "^7.0.3",
      "eslint": "^8.22.0",
      "eslint-config-react-app": "^7.0.1",
      "eslint-plugin-flowtype": "^8.0.3",
      "eslint-plugin-import": "^2.26.0",
      "eslint-plugin-jsx-a11y": "^6.6.1",
      "eslint-plugin-react": "^7.30.1",
      "nan": "^2.16.0",
      "typescript": "^4.7.4"
    },
    "overrides": {
      "autoprefixer": "10.4.5"
    },
    "resolutions": {
      "css-what": "^5.0.1",
      "nth-check": "^3.0.1"
    },
    "scripts": {
      "prestart": "node aspnetcore-https && node aspnetcore-react",
      "start": "rimraf ./build && react-scripts start",
      "build": "react-scripts build",
      "test": "cross-env CI=true react-scripts test --env=jsdom",
      "eject": "react-scripts eject",
      "lint": "eslint ./src/"
    },
    "eslintConfig": {
      "extends": [
        "react-app"
      ]
    },
    "browserslist": {
      "production": [
        ">0.2%",
        "not dead",
        "not op_mini all"
      ],
      "development": [
        "last 1 chrome version",
        "last 1 firefox version",
        "last 1 safari version"
      ]
    }
  }
  ```

* The `prestart` script invokes:

  * `aspnetcore-https.js`, which is responsible for ensuring the development server HTTPS certificate is available to the SPA proxy server.
  * Invokes `aspnetcore-react.js` to setup the appropriate `.env.development.local` file to use the HTTPS local development certificate. `aspnetcore-react.js` configures the HTTPS local development certificate by adding `SSL_CRT_FILE=<certificate-path>` and `SSL_KEY_FILE=<key-path>` to the file.

* The `.env.development` file defines the port for the development server and specifies HTTPS.

The `src/setupProxy.js` configures the SPA proxy to forward the requests to the backend. The general set of options is defined in [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware).

The following highlighted code in `ClientApp/src/setupProxy.js` uses logic based on the environment variables set during development to determine the port the backend is running on:

```
const { createProxyMiddleware } = require('http-proxy-middleware');
const { env } = require('process');

const target = env.ASPNETCORE_HTTPS_PORTS ? `https://localhost:${env.ASPNETCORE_HTTPS_PORTS}` :
  env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:51783';

const context = [
  "/weatherforecast",
];

const onError = (err, req, resp, target) => {
    console.error(`${err.message}`);
}

module.exports = function (app) {
  const appProxy = createProxyMiddleware(context, {
    target: target,
    // Handle errors to prevent the proxy middleware from crashing when
    // the ASP NET Core webserver is unavailable
    onError: onError,
    secure: false,
    // Uncomment this line to add support for proxying websockets
    //ws: true, 
    headers: {
      Connection: 'Keep-Alive'
    }
  });

  app.use(appProxy);
};
```

## Supported SPA framework version in ASP.NET Core SPA templates

The SPA project templates that ship with each ASP.NET Core release reference the latest version of the appropriate SPA framework.

SPA frameworks typically have a shorter release cycle than .NET. Because of the two different release cycles, the supported version of the SPA framework and .NET can get out of sync: the major SPA framework version, that a .NET major release depends on, can go out of support, while the .NET version the SPA framework shipped with is still supported.

The ASP.NET Core SPA templates can be updated in a patch release to a new SPA framework version to keep the templates in a supported and safe state.

## Related content

* [Use Identity to secure a Web API backend for SPAs](../../security/authentication/identity-api-authorization?view=aspnetcore-10.0)
* [Use Angular with ASP.NET Core](angular?view=aspnetcore-10.0)
* [Use React with ASP.NET Core](react?view=aspnetcore-10.0)
* [Hosting Startup Assemblies](../../fundamentals/host/web-host?view=aspnetcore-10.0#hosting-startup-assemblies)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Kestrel web server in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Tom Dykstra](https://github.com/tdykstra), [Chris Ross](https://github.com/Tratcher), and [Stephen Halter](https://twitter.com/halter73)

Kestrel is a cross-platform [web server for ASP.NET Core](./?view=aspnetcore-10.0). Kestrel is the recommended server for ASP.NET Core, and it's configured by default in ASP.NET Core project templates.

Kestrel's features include:

* **Cross-platform:** Kestrel is a cross-platform web server that runs on Windows, Linux, and macOS.

* **High performance:** Kestrel is optimized to handle a large number of concurrent connections efficiently.

* **Lightweight:** Optimized for running in resource-constrained environments, such as containers and edge devices.

* **Security hardened:** Kestrel supports HTTPS and is hardened against web server vulnerabilities.

* **Wide protocol support:** Kestrel supports common web protocols, including:

  * HTTP/1.1, [HTTP/2](kestrel/http2?view=aspnetcore-10.0) and [HTTP/3](kestrel/http3?view=aspnetcore-10.0). Starting in .NET 11, HTTP/3 uses [early request processing](kestrel/http3?view=aspnetcore-10.0#early-request-processing) to reduce first-request latency.
  * [WebSockets](../websockets?view=aspnetcore-10.0)

* **Integration with ASP.NET Core:** Seamless integration with other ASP.NET Core components, such as the middleware pipeline, dependency injection, and configuration system.

* **Flexible workloads**: Kestrel supports many workloads:

  * ASP.NET app frameworks such as Minimal APIs, MVC, Razor pages, SignalR, Blazor, and gRPC.
  * Building a reverse proxy with [YARP](https://github.com/microsoft/reverse-proxy).

* **Extensibility:** Customize Kestrel through configuration, middleware, and custom transports.

* **Performance diagnostics:** Kestrel provides built-in performance diagnostics features, such as logging and metrics.

* **Memory management:** Kestrel includes features for efficient memory management. For more information, see [Memory management in Kestrel](kestrel/memory-management?view=aspnetcore-10.0).

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). For production hosting, use Windows or Linux server distributions.

## Get started

ASP.NET Core project templates use Kestrel by default when not hosted with IIS. In the following template-generated `Program.cs`, the [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) method calls [UseKestrel](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.usekestrel) internally:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information on configuring `WebApplication` and `WebApplicationBuilder`, see [Minimal APIs quick reference](../minimal-apis?view=aspnetcore-10.0).

## Related content

[]()

* [Configure endpoints for Kestrel web server](kestrel/endpoints?view=aspnetcore-10.0)[]()
* [Configure options for the ASP.NET Core Kestrel web server](kestrel/options?view=aspnetcore-10.0)[]()
* [Use HTTP/2 with the ASP.NET Core Kestrel web server](kestrel/http2?view=aspnetcore-10.0)[]()
* [When to use a reverse proxy with the ASP.NET Core Kestrel web server](kestrel/when-to-use-a-reverse-proxy?view=aspnetcore-10.0)[]()
* [Host filtering with ASP.NET Core Kestrel web server](kestrel/host-filtering?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)
* [Enforce HTTPS in ASP.NET Core](../../security/enforcing-ssl?view=aspnetcore-10.0)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [RFC 9110: HTTP Semantics (Section 7.2: Host and :authority)](https://www.rfc-editor.org/rfc/rfc9110#field.host)
* When using UNIX sockets on Linux, the socket isn't automatically deleted on app shutdown. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/14134).

Kestrel is a cross-platform [web server for ASP.NET Core](./?view=aspnetcore-10.0). Kestrel is the recommended server for ASP.NET Core, and it's configured by default in ASP.NET Core project templates.

Kestrel's features include:

* **Cross-platform:** Kestrel is a cross-platform web server that runs on Windows, Linux, and macOS.

* **High performance:** Kestrel is optimized to handle a large number of concurrent connections efficiently.

* **Lightweight:** Optimized for running in resource-constrained environments, such as containers and edge devices.

* **Security hardened:** Kestrel supports HTTPS and is hardened against web server vulnerabilities.

* **Wide protocol support:** Kestrel supports common web protocols, including:

  * HTTP/1.1, [HTTP/2](kestrel/http2?view=aspnetcore-10.0) and [HTTP/3](kestrel/http3?view=aspnetcore-10.0)
  * [WebSockets](../websockets?view=aspnetcore-10.0)

* **Integration with ASP.NET Core:** Seamless integration with other ASP.NET Core components, such as the middleware pipeline, dependency injection, and configuration system.

* **Flexible workloads**: Kestrel supports many workloads:

  * ASP.NET app frameworks such as Minimal APIs, MVC, Razor pages, SignalR, Blazor, and gRPC.
  * Building a reverse proxy with [YARP](https://github.com/microsoft/reverse-proxy).

* **Extensibility:** Customize Kestrel through configuration, middleware, and custom transports.

* **Performance diagnostics:** Kestrel provides built-in performance diagnostics features, such as logging and metrics.

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). For production hosting, use Windows or Linux server distributions.

## Get started

ASP.NET Core project templates use Kestrel by default when not hosted with IIS. In the following template-generated `Program.cs`, the [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) method calls [UseKestrel](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.usekestrel) internally:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information on configuring `WebApplication` and `WebApplicationBuilder`, see [Minimal APIs quick reference](../minimal-apis?view=aspnetcore-10.0).

## Behavior with debugger attached

The following timeouts and rate limits aren't enforced when a debugger is attached to a Kestrel process:

## Additional resources

[]()

* [Configure endpoints for Kestrel web server](kestrel/endpoints?view=aspnetcore-10.0)[]()
* [Configure options for the ASP.NET Core Kestrel web server](kestrel/options?view=aspnetcore-10.0)[]()
* [Use HTTP/2 with the ASP.NET Core Kestrel web server](kestrel/http2?view=aspnetcore-10.0)[]()
* [When to use a reverse proxy with the ASP.NET Core Kestrel web server](kestrel/when-to-use-a-reverse-proxy?view=aspnetcore-10.0)[]()
* [Host filtering with ASP.NET Core Kestrel web server](kestrel/host-filtering?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)
* [Enforce HTTPS in ASP.NET Core](../../security/enforcing-ssl?view=aspnetcore-10.0)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [RFC 9110: HTTP Semantics (Section 7.2: Host and :authority)](https://www.rfc-editor.org/rfc/rfc9110#field.host)
* When using UNIX sockets on Linux, the socket isn't automatically deleted on app shutdown. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/14134).

Kestrel is a cross-platform [web server for ASP.NET Core](./?view=aspnetcore-10.0). Kestrel is the web server that's included and enabled by default in ASP.NET Core project templates.

Kestrel supports the following scenarios:

* HTTPS
* [HTTP/2](kestrel/http2?view=aspnetcore-10.0) (except on macOS†)
* Opaque upgrade used to enable [WebSockets](../websockets?view=aspnetcore-10.0)
* Unix sockets for high performance behind Nginx

†HTTP/2 will be supported on macOS in a future release.

Kestrel is supported on all platforms and versions that .NET supports.

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). For production hosting, use Windows or Linux server distributions.

## Get started

ASP.NET Core project templates use Kestrel by default when not hosted with IIS. In the following template-generated `Program.cs`, the [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) method calls [UseKestrel](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.usekestrel) internally:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information on configuring `WebApplication` and `WebApplicationBuilder`, see [Minimal APIs quick reference](../minimal-apis?view=aspnetcore-10.0).

## Behavior with debugger attached

The following timeouts and rate limits aren't enforced when a debugger is attached to a Kestrel process:

## Additional resources

[]()

* [Configure endpoints for Kestrel web server](kestrel/endpoints?view=aspnetcore-10.0)[]()
* [Configure options for the ASP.NET Core Kestrel web server](kestrel/options?view=aspnetcore-10.0)[]()
* [Use HTTP/2 with the ASP.NET Core Kestrel web server](kestrel/http2?view=aspnetcore-10.0)[]()
* [When to use a reverse proxy with the ASP.NET Core Kestrel web server](kestrel/when-to-use-a-reverse-proxy?view=aspnetcore-10.0)[]()
* [Host filtering with ASP.NET Core Kestrel web server](kestrel/host-filtering?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)
* [Enforce HTTPS in ASP.NET Core](../../security/enforcing-ssl?view=aspnetcore-10.0)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [RFC 9110: HTTP Semantics (Section 7.2: Host and :authority)](https://www.rfc-editor.org/rfc/rfc9110#field.host)
* When using UNIX sockets on Linux, the socket isn't automatically deleted on app shutdown. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/14134).

Kestrel is a cross-platform [web server for ASP.NET Core](./?view=aspnetcore-10.0). Kestrel is the web server that's included and enabled by default in ASP.NET Core project templates.

Kestrel supports the following scenarios:

* HTTPS
* [HTTP/2](kestrel/http2?view=aspnetcore-10.0) (except on macOS†)
* Opaque upgrade used to enable [WebSockets](../websockets?view=aspnetcore-10.0)
* Unix sockets for high performance behind Nginx

†HTTP/2 will be supported on macOS in a future release.

Kestrel is supported on all platforms and versions that .NET supports.

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). For production hosting, use Windows or Linux server distributions.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/servers/kestrel/samples/5.x) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## Get started

ASP.NET Core project templates use Kestrel by default when not hosted with IIS. In `Program.cs`, the [ConfigureWebHostDefaults](/en-us/dotnet/api/microsoft.extensions.hosting.generichostbuilderextensions.configurewebhostdefaults) method calls [UseKestrel](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.usekestrel):

```
public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

For more information on building the host, see the *Set up a host* and *Default builder settings* sections of [.NET Generic Host in ASP.NET Core](../host/generic-host?view=aspnetcore-10.0#set-up-a-host).

## Additional resources

[]()

* [Configure endpoints for Kestrel web server](kestrel/endpoints?view=aspnetcore-10.0)[]()
* [Configure options for the ASP.NET Core Kestrel web server](kestrel/options?view=aspnetcore-10.0)[]()
* [Use HTTP/2 with the ASP.NET Core Kestrel web server](kestrel/http2?view=aspnetcore-10.0)[]()
* [When to use a reverse proxy with the ASP.NET Core Kestrel web server](kestrel/when-to-use-a-reverse-proxy?view=aspnetcore-10.0)[]()
* [Host filtering with ASP.NET Core Kestrel web server](kestrel/host-filtering?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)
* [Enforce HTTPS in ASP.NET Core](../../security/enforcing-ssl?view=aspnetcore-10.0)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [RFC 9110: HTTP Semantics (Section 7.2: Host and :authority)](https://www.rfc-editor.org/rfc/rfc9110#field.host)
* When using UNIX sockets on Linux, the socket is not automatically deleted on app shut down. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/14134).

Kestrel is a cross-platform [web server for ASP.NET Core](./?view=aspnetcore-10.0). Kestrel is the web server that's included by default in ASP.NET Core project templates.

Kestrel supports the following scenarios:

* HTTPS
* Opaque upgrade used to enable [WebSockets](https://github.com/aspnet/websockets)
* Unix sockets for high performance behind Nginx
* HTTP/2 (except on macOS†)

†HTTP/2 will be supported on macOS in a future release.

Kestrel is supported on all platforms and versions that .NET supports.

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). For production hosting, use Windows or Linux server distributions.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/servers/kestrel/samples/3.x) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## HTTP/2 support

Starting with .NET Core 3.0, HTTP/2 is enabled by default. For more information on configuration, see the [Kestrel options](#kestrel-options) and [ListenOptions.Protocols](#listenoptionsprotocols) sections.

## When to use Kestrel with a reverse proxy

Kestrel used as an edge server without a reverse proxy server doesn't support sharing the same IP and port among multiple processes. When Kestrel is configured to listen on a port, Kestrel handles all of the traffic for that port regardless of requests' `Host` headers. A reverse proxy that can share ports has the ability to forward requests to Kestrel on a unique IP and port.

Even if a reverse proxy server isn't required, using a reverse proxy server might be a good choice.

A reverse proxy:

* Can limit the exposed public surface area of the apps that it hosts.
* Provide an additional layer of configuration and defense-in-depth cybersecurity.
* Might integrate better with existing infrastructure.
* Simplify load balancing and secure communication (HTTPS) configuration. Only the reverse proxy server requires an X.509 certificate, and that server can communicate with the app's servers on the internal network using plain HTTP.

Warning

Hosting in a reverse proxy configuration requires [forwarded headers middleware configuration](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

## Kestrel in ASP.NET Core apps

ASP.NET Core project templates use Kestrel by default. In `Program.cs`, the [ConfigureWebHostDefaults](/en-us/dotnet/api/microsoft.extensions.hosting.generichostbuilderextensions.configurewebhostdefaults) method calls [UseKestrel](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.usekestrel):

```
public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

For more information on building the host, see the *Set up a host* and *Default builder settings* sections of [.NET Generic Host in ASP.NET Core](../host/generic-host?view=aspnetcore-10.0#set-up-a-host).

To provide additional configuration after calling `ConfigureWebHostDefaults`, use `ConfigureKestrel`:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(serverOptions =>
            {
                // Set properties and call methods on options
            })
            .UseStartup<Startup>();
        });
```

## Kestrel options

The Kestrel web server has constraint configuration options that are especially useful in Internet-facing deployments.

Set constraints on the [Limits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.limits#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-limits) property of the [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions) class. The `Limits` property holds an instance of the [KestrelServerLimits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits) class.

The following examples use the [Microsoft.AspNetCore.Server.Kestrel.Core](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core) namespace:

```
using Microsoft.AspNetCore.Server.Kestrel.Core;
```

In examples shown later in this article, Kestrel options are configured in C# code. Kestrel options can also be set using a [configuration provider](../configuration/?view=aspnetcore-10.0). For example, the [File Configuration Provider](../configuration/?view=aspnetcore-10.0#file-configuration-provider) can load Kestrel configuration from an `appsettings.json` or `appsettings.{Environment}.json` file:

```
{
  "Kestrel": {
    "Limits": {
      "MaxConcurrentConnections": 100,
      "MaxConcurrentUpgradedConnections": 100
    },
    "DisableStringReuse": true
  }
}
```

Note

[KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions) and [endpoint configuration](#endpoint-configuration) are configurable from configuration providers. Remaining Kestrel configuration must be configured in C# code.

Use **one** of the following approaches:

* Configure Kestrel in `Startup.ConfigureServices`:

  1. Inject an instance of `IConfiguration` into the `Startup` class. The following example assumes that the injected configuration is assigned to the `Configuration` property.

  2. In `Startup.ConfigureServices`, load the `Kestrel` section of configuration into Kestrel's configuration:

     ```
     using Microsoft.Extensions.Configuration

     public class Startup
     {
         public Startup(IConfiguration configuration)
         {
             Configuration = configuration;
         }

         public IConfiguration Configuration { get; }

         public void ConfigureServices(IServiceCollection services)
         {
             services.Configure<KestrelServerOptions>(
                 Configuration.GetSection("Kestrel"));
         }

         public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
         {
             ...
         }
     }
     ```

* Configure Kestrel when building the host:

  In `Program.cs`, load the `Kestrel` section of configuration into Kestrel's configuration:

  ```
  // using Microsoft.Extensions.DependencyInjection;

  public static IHostBuilder CreateHostBuilder(string[] args) =>
      Host.CreateDefaultBuilder(args)
          .ConfigureServices((context, services) =>
          {
              services.Configure<KestrelServerOptions>(
                  context.Configuration.GetSection("Kestrel"));
          })
          .ConfigureWebHostDefaults(webBuilder =>
          {
              webBuilder.UseStartup<Startup>();
          });
  ```

Both of the preceding approaches work with any [configuration provider](../configuration/?view=aspnetcore-10.0).

### Keep-alive timeout

[KeepAliveTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.keepalivetimeout#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-keepalivetimeout)

Gets or sets the [keep-alive timeout](https://www.rfc-editor.org/rfc/rfc9112.html#name-keep-alive-connections). Defaults to 2 minutes.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

### Maximum client connections

[MaxConcurrentConnections](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentconnections#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxconcurrentconnections) [MaxConcurrentUpgradedConnections](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentupgradedconnections#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxconcurrentupgradedconnections)

The maximum number of concurrent open TCP connections can be set for the entire app with the following code:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

There's a separate limit for connections that have been upgraded from HTTP or HTTPS to another protocol (for example, on a WebSockets request). After a connection is upgraded, it isn't counted against the `MaxConcurrentConnections` limit.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

The maximum number of connections is unlimited (null) by default.

### Maximum request body size

[MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxrequestbodysize#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxrequestbodysize)

The default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB.

The recommended approach to override the limit in an ASP.NET Core MVC app is to use the [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) attribute on an action method:

```
[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()
```

Here's an example that shows how to configure the constraint for the app on every request:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

Override the setting on a specific request in middleware:

```
app.Run(async (context) =>
{
    context.Features.Get<IHttpMaxRequestBodySizeFeature>()
        .MaxRequestBodySize = 10 * 1024;

    var minRequestRateFeature = 
        context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
    var minResponseRateFeature = 
        context.Features.Get<IHttpMinResponseDataRateFeature>();

    if (minRequestRateFeature != null)
    {
        minRequestRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }

    if (minResponseRateFeature != null)
    {
        minResponseRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }
```

An exception is thrown if the app configures the limit on a request after the app has started to read the request. There's an `IsReadOnly` property that indicates if the `MaxRequestBodySize` property is in read-only state, meaning it's too late to configure the limit.

When an app is run [out-of-process](../../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model) behind the [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0), Kestrel's request body size limit is disabled because IIS already sets the limit.

### Minimum request body data rate

[MinRequestBodyDataRate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.minrequestbodydatarate#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-minrequestbodydatarate) [MinResponseDataRate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.minresponsedatarate#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-minresponsedatarate)

Kestrel checks every second if data is arriving at the specified rate in bytes/second. If the rate drops below the minimum, the connection is timed out. The grace period is the amount of time that Kestrel gives the client to increase its send rate up to the minimum; the rate isn't checked during that time. The grace period helps avoid dropping connections that are initially sending data at a slow rate due to TCP slow-start.

The default minimum rate is 240 bytes/second with a 5 second grace period.

A minimum rate also applies to the response. The code to set the request limit and the response limit is the same except for having `RequestBody` or `Response` in the property and interface names.

Here's an example that shows how to configure the minimum data rates in `Program.cs`:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

Override the minimum rate limits per request in middleware:

```
app.Run(async (context) =>
{
    context.Features.Get<IHttpMaxRequestBodySizeFeature>()
        .MaxRequestBodySize = 10 * 1024;

    var minRequestRateFeature = 
        context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
    var minResponseRateFeature = 
        context.Features.Get<IHttpMinResponseDataRateFeature>();

    if (minRequestRateFeature != null)
    {
        minRequestRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }

    if (minResponseRateFeature != null)
    {
        minResponseRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }
```

The [IHttpMinResponseDataRateFeature](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminresponsedataratefeature) referenced in the prior sample is not present in `HttpContext.Features` for HTTP/2 requests because modifying rate limits on a per-request basis is generally not supported for HTTP/2 due to the protocol's support for request multiplexing. However, the [IHttpMinRequestBodyDataRateFeature](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminrequestbodydataratefeature) is still present `HttpContext.Features` for HTTP/2 requests, because the read rate limit can still be *disabled entirely* on a per-request basis by setting `IHttpMinRequestBodyDataRateFeature.MinDataRate` to `null` even for an HTTP/2 request. Attempting to read `IHttpMinRequestBodyDataRateFeature.MinDataRate` or attempting to set it to a value other than `null` will result in a `NotSupportedException` being thrown given an HTTP/2 request.

Server-wide rate limits configured via `KestrelServerOptions.Limits` still apply to both HTTP/1.x and HTTP/2 connections.

### Request headers timeout

[RequestHeadersTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.requestheaderstimeout#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-requestheaderstimeout)

Gets or sets the maximum amount of time the server spends receiving request headers. Defaults to 30 seconds.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

### Maximum streams per connection

`Http2.MaxStreamsPerConnection` limits the number of concurrent request streams per HTTP/2 connection. Excess streams are refused.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.MaxStreamsPerConnection = 100;
});
```

The default value is 100.

### Header table size

The HPACK decoder decompresses HTTP headers for HTTP/2 connections. `Http2.HeaderTableSize` limits the size of the header compression table that the HPACK decoder uses. The value is provided in octets and must be greater than zero (0).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.HeaderTableSize = 4096;
});
```

The default value is 4096.

### Maximum frame size

`Http2.MaxFrameSize` indicates the maximum allowed size of an HTTP/2 connection frame payload received or sent by the server. The value is provided in octets and must be between 2^14 (16,384) and 2^24-1 (16,777,215).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.MaxFrameSize = 16384;
});
```

The default value is 2^14 (16,384).

### Maximum request header size

`Http2.MaxRequestHeaderFieldSize` indicates the maximum allowed size in octets of request header values. This limit applies to both name and value in their compressed and uncompressed representations. The value must be greater than zero (0).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.MaxRequestHeaderFieldSize = 8192;
});
```

The default value is 8,192.

### Initial connection window size

`Http2.InitialConnectionWindowSize` indicates the maximum request body data in bytes the server buffers at one time aggregated across all requests (streams) per connection. Requests are also limited by `Http2.InitialStreamWindowSize`. The value must be greater than or equal to 65,535 and less than 2^31 (2,147,483,648).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.InitialConnectionWindowSize = 131072;
});
```

The default value is 128 KB (131,072).

### Initial stream window size

`Http2.InitialStreamWindowSize` indicates the maximum request body data in bytes the server buffers at one time per request (stream). Requests are also limited by `Http2.InitialConnectionWindowSize`. The value must be greater than or equal to 65,535 and less than 2^31 (2,147,483,648).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.InitialStreamWindowSize = 98304;
});
```

The default value is 96 KB (98,304).

### Trailers

HTTP Trailers are similar to HTTP Headers, except they are sent after the response body is sent. For IIS and HTTP.sys, only HTTP/2 response trailers are supported.

```
if (httpContext.Response.SupportsTrailers())
{
    httpContext.Response.DeclareTrailer("trailername");	

    // Write body
    httpContext.Response.WriteAsync("Hello world");

    httpContext.Response.AppendTrailer("trailername", "TrailerValue");
}
```

In the preceding example code:

* `SupportsTrailers` ensures that trailers are supported for the response.
* `DeclareTrailer` adds the given trailer name to the `Trailer` response header. Declaring a response's trailers is optional, but recommended. If `DeclareTrailer` is called, it must be before the response headers are sent.
* `AppendTrailer` appends the trailer.

### Reset

Reset allows for the server to reset a HTTP/2 request with a specified error code. A reset request is considered aborted.

```
var resetFeature = httpContext.Features.Get<IHttpResetFeature>();
resetFeature.Reset(errorCode: 2);
```

`Reset` in the preceding code example specifies the `INTERNAL_ERROR` error code. For more information about HTTP/2 error codes, visit the [HTTP/2 specification error code section](https://tools.ietf.org/html/rfc7540#page-50).

### Synchronous I/O

[AllowSynchronousIO](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.allowsynchronousio#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-allowsynchronousio) controls whether synchronous I/O is allowed for the request and response. The default value is `false`.

Warning

A large number of blocking synchronous I/O operations can lead to thread pool starvation, which makes the app unresponsive. Only enable `AllowSynchronousIO` when using a library that doesn't support asynchronous I/O.

The following example enables synchronous I/O:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.AllowSynchronousIO = true;
})
```

For information about other Kestrel options and limits, see:

* [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions)
* [KestrelServerLimits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits)
* [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions)

## Endpoint configuration

* `UseUrls` extension method.

The value provided using these approaches can be one or more HTTP and HTTPS endpoints (HTTPS if a default cert is available). Configure the value as a semicolon-separated list (for example, `"Urls": "http://localhost:8000;http://localhost:8001"`).

For more information on these approaches, see [Server URLs](../host/web-host?view=aspnetcore-10.0#server-urls) and [Override configuration](../host/web-host?view=aspnetcore-10.0#override-configuration).

A development certificate is created:

* When the [.NET SDK](/en-us/dotnet/core/sdk) is installed.
* The [dev-certs tool](/en-us/dotnet/core/tools/dotnet-dev-certs) is used to create a certificate.

Some browsers require granting explicit permission to trust the local development certificate.

Project templates configure apps to run on HTTPS by default and include [HTTPS redirection and HSTS support](../../security/enforcing-ssl?view=aspnetcore-10.0).

Call [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) or [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) methods on [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions) to configure URL prefixes and ports for Kestrel.

`UseUrls`, the `--urls` command-line argument, `urls` host configuration key, and the `ASPNETCORE_URLS` environment variable also work but have the limitations noted later in this section (a default certificate must be available for HTTPS endpoint configuration).

`KestrelServerOptions` configuration:

### ConfigureEndpointDefaults(Action\<ListenOptions>)

Specifies a configuration `Action` to run for each specified endpoint. Calling `ConfigureEndpointDefaults` multiple times replaces prior `Action`s with the last `Action` specified.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // Configure endpoint defaults
    });
});
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureEndpointDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configureendpointdefaults) won't have the defaults applied.

### ConfigureHttpsDefaults(Action\<HttpsConnectionAdapterOptions>)

Specifies a configuration `Action` to run for each HTTPS endpoint. Calling `ConfigureHttpsDefaults` multiple times replaces prior `Action`s with the last `Action` specified.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        // certificate is an X509Certificate2
        listenOptions.ServerCertificate = certificate;
    });
});
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureHttpsDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults) won't have the defaults applied.

### Configure(IConfiguration)

Creates a configuration loader for setting up Kestrel that takes an [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) as input. The configuration must be scoped to the configuration section for Kestrel.

### ListenOptions.UseHttps

*No configuration*

Kestrel listens on `http://localhost:5000` and `https://localhost:5001` (if a default cert is available).

[]()

*Replace the default certificate from configuration*

`CreateDefaultBuilder` calls `Configure(context.Configuration.GetSection("Kestrel"))` by default to load Kestrel configuration. A default HTTPS app settings configuration schema is available for Kestrel. Configure multiple endpoints, including the URLs and the certificates to use, either from a file on disk or from a certificate store.

In the following `appsettings.json` example:

* Set **AllowInvalid** to `true` to permit the use of invalid certificates (for example, self-signed certificates).
* Any HTTPS endpoint that doesn't specify a certificate (**HttpsDefaultCert** in the example that follows) falls back to the cert defined under **Certificates** > **Default** or the development certificate.

```
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "<certificate password>"
        }
      },
      "HttpsInlineCertStore": {
        "Url": "https://localhost:5002",
        "Certificate": {
          "Subject": "<subject; required>",
          "Store": "<certificate store; required>",
          "Location": "<location; defaults to CurrentUser>",
          "AllowInvalid": "<true or false; defaults to false>"
        }
      },
      "HttpsDefaultCert": {
        "Url": "https://localhost:5003"
      },
      "Https": {
        "Url": "https://*:5004",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "<certificate password>"
        }
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "<certificate password>"
      }
    }
  }
}
```

An alternative to using **Path** and **Password** for any certificate node is to specify the certificate using certificate store fields. For example, the **Certificates** > **Default** certificate can be specified as:

```
"Default": {
  "Subject": "<subject; required>",
  "Store": "<cert store; required>",
  "Location": "<location; defaults to CurrentUser>",
  "AllowInvalid": "<true or false; defaults to false>"
}
```

Schema notes:

* Endpoints names are case-insensitive. For example, `HTTPS` and `Https` are valid.
* The `Url` parameter is required for each endpoint. The format for this parameter is the same as the top-level `Urls` configuration parameter except that it's limited to a single value.
* These endpoints replace those defined in the top-level `Urls` configuration rather than adding to them. Endpoints defined in code via `Listen` are cumulative with the endpoints defined in the configuration section.
* The `Certificate` section is optional. If the `Certificate` section isn't specified, the defaults defined in earlier scenarios are used. If no defaults are available, the server throws an exception and fails to start.
* The `Certificate` section supports both **Path**–**Password** and **Subject**–**Store** certificates.
* Any number of endpoints may be defined in this way so long as they don't cause port conflicts.
* `options.Configure(context.Configuration.GetSection("{SECTION}"))` returns a `KestrelConfigurationLoader` with an `.Endpoint(string name, listenOptions => { })` method that can be used to supplement a configured endpoint's settings:

```
webBuilder.UseKestrel((context, serverOptions) =>
{
    serverOptions.Configure(context.Configuration.GetSection("Kestrel"))
        .Endpoint("HTTPS", listenOptions =>
        {
            listenOptions.HttpsOptions.SslProtocols = SslProtocols.Tls12;
        });
});
```

`KestrelServerOptions.ConfigurationLoader` can be directly accessed to continue iterating on the existing loader, such as the one provided by [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder).

* The configuration section for each endpoint is available on the options in the `Endpoint` method so that custom settings may be read.
* Multiple configurations may be loaded by calling `options.Configure(context.Configuration.GetSection("{SECTION}"))` again with another section. Only the last configuration is used, unless `Load` is explicitly called on prior instances. The metapackage doesn't call `Load` so that its default configuration section may be replaced.
* `KestrelConfigurationLoader` mirrors the `Listen` family of APIs from `KestrelServerOptions` as `Endpoint` overloads, so code and config endpoints may be configured in the same place. These overloads don't use names and only consume default settings from configuration.

*Change the defaults in code*

`ConfigureEndpointDefaults` and `ConfigureHttpsDefaults` can be used to change default settings for `ListenOptions` and `HttpsConnectionAdapterOptions`, including overriding the default certificate specified in the prior scenario. `ConfigureEndpointDefaults` and `ConfigureHttpsDefaults` should be called before any endpoints are configured.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // Configure endpoint defaults
    });

    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.SslProtocols = SslProtocols.Tls12;
    });
});
```

*Kestrel support for SNI*

[Server Name Indication (SNI)](https://tools.ietf.org/html/rfc6066#section-3) can be used to host multiple domains on the same IP address and port. For SNI to function, the client sends the host name for the secure session to the server during the TLS handshake so that the server can provide the correct certificate. The client uses the furnished certificate for encrypted communication with the server during the secure session that follows the TLS handshake.

Kestrel supports SNI via the `ServerCertificateSelector` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate.

SNI support requires:

* Running on target framework `netcoreapp2.1` or later. On `net461` or later, the callback is invoked but the `name` is always `null`. The `name` is also `null` if the client doesn't provide the host name parameter in the TLS handshake.
* All websites run on the same Kestrel instance. Kestrel doesn't support sharing an IP address and port across multiple instances without a reverse proxy.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var localhostCert = CertificateLoader.LoadFromStoreCert(
                "localhost", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var exampleCert = CertificateLoader.LoadFromStoreCert(
                "example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var subExampleCert = CertificateLoader.LoadFromStoreCert(
                "sub.example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var certs = new Dictionary<string, X509Certificate2>(
                StringComparer.OrdinalIgnoreCase);
            certs["localhost"] = localhostCert;
            certs["example.com"] = exampleCert;
            certs["sub.example.com"] = subExampleCert;

            httpsOptions.ServerCertificateSelector = (connectionContext, name) =>
            {
                if (name != null && certs.TryGetValue(name, out var cert))
                {
                    return cert;
                }

                return exampleCert;
            };
        });
    });
});
```

### Connection logging

Call [UseConnectionLogging](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionsconnectionloggingextensions.useconnectionlogging) to emit Debug level logs for byte-level communication on a connection. Connection logging is helpful for troubleshooting problems in low-level communication, such as during TLS encryption and behind proxies. If `UseConnectionLogging` is placed before `UseHttps`, encrypted traffic is logged. If `UseConnectionLogging` is placed after `UseHttps`, decrypted traffic is logged.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseConnectionLogging();
    });
});
```

### Bind to a TCP socket

The [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) method binds to a TCP socket, and an options lambda permits X.509 certificate configuration:

```
public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(serverOptions =>
            {
                serverOptions.Listen(IPAddress.Loopback, 5000);
                serverOptions.Listen(IPAddress.Loopback, 5001, 
                    listenOptions =>
                    {
                        listenOptions.UseHttps("testCert.pfx", 
                            "testPassword");
                    });
            })
            .UseStartup<Startup>();
        });
```

The example configures HTTPS for an endpoint with [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions). Use the same API to configure other Kestrel settings for specific endpoints.

On Windows, self-signed certificates can be created by using the [New-SelfSignedCertificate PowerShell cmdlet](/en-us/powershell/module/pki/new-selfsignedcertificate). For an unsupported example, see the [UpdateIISExpressSSLForChrome.ps1](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/includes/make-x509-cert/UpdateIISExpressSSLForChrome.ps1) certificate file on GitHub.

On macOS, Linux, and Windows, create certificates can be created by using [OpenSSL](https://www.openssl.org/).

### Bind to a Unix socket

Listen on a Unix socket with [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) for improved performance with Nginx, as shown in this example:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenUnixSocket("/tmp/kestrel-test.sock");
    serverOptions.ListenUnixSocket("/tmp/kestrel-test.sock", 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testpassword");
        });
})
```

* In the Nginx configuration file, set the `server` > `location` > `proxy_pass` entry to `http://unix:/tmp/{KESTREL SOCKET}:/;`. `{KESTREL SOCKET}` is the name of the socket provided to [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) (for example, `kestrel-test.sock` in the preceding example).
* Ensure that the socket is writeable by Nginx (for example, `chmod go+w /tmp/kestrel-test.sock`).

### Port 0

When the port number `0` is specified, Kestrel dynamically binds to an available port. The following example shows how to determine which port Kestrel actually bound at runtime:

```
public void Configure(IApplicationBuilder app)
{
    var serverAddressesFeature = 
        app.ServerFeatures.Get<IServerAddressesFeature>();

    app.UseStaticFiles();

    app.Run(async (context) =>
    {
        context.Response.ContentType = "text/html";
        await context.Response
            .WriteAsync("<!DOCTYPE html><html lang=\"en\"><head>" +
                "<title></title></head><body><p>Hosted by Kestrel</p>");

        if (serverAddressesFeature != null)
        {
            await context.Response
                .WriteAsync("<p>Listening on the following addresses: " +
                    string.Join(", ", serverAddressesFeature.Addresses) +
                    "</p>");
        }

        await context.Response.WriteAsync("<p>Request URL: " +
            $"{context.Request.GetDisplayUrl()}<p>");
    });
}
```

When the app is run, the console window output indicates the dynamic port where the app can be reached:

```
Listening on the following addresses: http://127.0.0.1:48508
```

### Limitations

Configure endpoints with the following approaches:

* [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls)
* `--urls` command-line argument
* `urls` host configuration key
* `ASPNETCORE_URLS` environment variable

These methods are useful for making code work with servers other than Kestrel. However, be aware of the following limitations:

* HTTPS can't be used with these approaches unless a default certificate is provided in the HTTPS endpoint configuration (for example, using `KestrelServerOptions` configuration or a configuration file as shown earlier in this topic).
* When both the `Listen` and `UseUrls` approaches are used simultaneously, the `Listen` endpoints override the `UseUrls` endpoints.

### IIS endpoint configuration

When using IIS, the URL bindings for IIS override bindings are set by either `Listen` or `UseUrls`. For more information, see the [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) topic.

### ListenOptions.Protocols

The `Protocols` property establishes the HTTP protocols (`HttpProtocols`) enabled on a connection endpoint or for the server. Assign a value to the `Protocols` property from the `HttpProtocols` enum.

| `HttpProtocols` enum value | Connection protocol permitted                                                                                                                                                                                                         |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Http1`                    | HTTP/1.1 only. Can be used with or without TLS.                                                                                                                                                                                       |
| `Http2`                    | HTTP/2 only. May be used without TLS only if the client supports a [Prior Knowledge mode](https://tools.ietf.org/html/rfc7540#section-3.4).                                                                                           |
| `Http1AndHttp2`            | HTTP/1.1 and HTTP/2. HTTP/2 requires the client to select HTTP/2 in the TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake; otherwise, the connection defaults to HTTP/1.1. |

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
    });
});
```

Use connection middleware to filter TLS handshakes on a per-connection basis for specific ciphers if required.

The following example throws [NotSupportedException](/en-us/dotnet/api/system.notsupportedexception) for any cipher algorithm that the app doesn't support. Alternatively, define and compare [ITlsHandshakeFeature.CipherAlgorithm](/en-us/dotnet/api/microsoft.aspnetcore.connections.features.itlshandshakefeature.cipheralgorithm#microsoft-aspnetcore-connections-features-itlshandshakefeature-cipheralgorithm) to a list of acceptable cipher suites.

No encryption is used with a [CipherAlgorithmType.Null](/en-us/dotnet/api/system.security.authentication.cipheralgorithmtype) cipher algorithm.

```
// using System.Net;
// using Microsoft.AspNetCore.Connections;

webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
        listenOptions.UseTlsFilter();
    });
});
```

```
using System;
using System.Security.Authentication;
using Microsoft.AspNetCore.Connections.Features;

namespace Microsoft.AspNetCore.Connections
{
    public static class TlsFilterConnectionMiddlewareExtensions
    {
        public static IConnectionBuilder UseTlsFilter(
            this IConnectionBuilder builder)
        {
            return builder.Use((connection, next) =>
            {
                var tlsFeature = connection.Features.Get<ITlsHandshakeFeature>();

                if (tlsFeature.CipherAlgorithm == CipherAlgorithmType.Null)
                {
                    throw new NotSupportedException("Prohibited cipher: " +
                        tlsFeature.CipherAlgorithm);
                }

                return next();
            });
        }
    }
}
```

Connection filtering can also be configured via an [IConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.connections.iconnectionbuilder) lambda:

```
// using System;
// using System.Net;
// using System.Security.Authentication;
// using Microsoft.AspNetCore.Connections;
// using Microsoft.AspNetCore.Connections.Features;

webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
        listenOptions.Use((context, next) =>
        {
            var tlsFeature = context.Features.Get<ITlsHandshakeFeature>();

            if (tlsFeature.CipherAlgorithm == CipherAlgorithmType.Null)
            {
                throw new NotSupportedException(
                    $"Prohibited cipher: {tlsFeature.CipherAlgorithm}");
            }

            return next();
        });
    });
});
```

On Linux, [CipherSuitesPolicy](/en-us/dotnet/api/system.net.security.ciphersuitespolicy) can be used to filter TLS handshakes on a per-connection basis:

```
// using System.Net.Security;
// using Microsoft.AspNetCore.Hosting;
// using Microsoft.AspNetCore.Server.Kestrel.Core;
// using Microsoft.Extensions.DependencyInjection;
// using Microsoft.Extensions.Hosting;

webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.OnAuthenticate = (context, sslOptions) =>
        {
            sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(
                new[]
                {
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
                    // ...
                });
        };
    });
});
```

*Set the protocol from configuration*

`CreateDefaultBuilder` calls `serverOptions.Configure(context.Configuration.GetSection("Kestrel"))` by default to load Kestrel configuration.

The following `appsettings.json` example establishes HTTP/1.1 as the default connection protocol for all endpoints:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1"
    }
  }
}
```

The following `appsettings.json` example establishes the HTTP/1.1 connection protocol for a specific endpoint:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsDefaultCert": {
        "Url": "https://localhost:5001",
        "Protocols": "Http1"
      }
    }
  }
}
```

Protocols specified in code override values set by configuration.

### URL prefixes

When using `UseUrls`, `--urls` command-line argument, `urls` host configuration key, or `ASPNETCORE_URLS` environment variable, the URL prefixes can be in any of the following formats.

Only HTTP URL prefixes are valid. Kestrel doesn't support HTTPS when configuring URL bindings using `UseUrls`.

* IPv4 address with port number

  ```
  http://65.55.39.10:80/
  ```

  `0.0.0.0` is a special case that binds to all IPv4 addresses.

* IPv6 address with port number

  ```
  http://[0:0:0:0:0:ffff:4137:270a]:80/
  ```

  `[::]` is the IPv6 equivalent of IPv4 `0.0.0.0`.

* Host name with port number

  ```
  http://contoso.com:80/
  http://*:80/
  ```

  Host names, `*`, and `+`, aren't special. Anything not recognized as a valid IP address or `localhost` binds to all IPv4 and IPv6 IPs. To bind different host names to different ASP.NET Core apps on the same port, use [HTTP.sys](httpsys?view=aspnetcore-10.0) or a reverse proxy server, such as IIS, Nginx, or Apache.

  Warning

  Hosting in a reverse proxy configuration requires [forwarded headers middleware configuration](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

* Host `localhost` name with port number or loopback IP with port number

  ```
  http://localhost:5000/
  http://127.0.0.1:5000/
  http://[::1]:5000/
  ```

  When `localhost` is specified, Kestrel attempts to bind to both IPv4 and IPv6 loopback interfaces. If the requested port is in use by another service on either loopback interface, Kestrel fails to start. If either loopback interface is unavailable for any other reason (most commonly because IPv6 isn't supported), Kestrel logs a warning.

## Host filtering

While Kestrel supports configuration based on prefixes such as `http://example.com:5000`, Kestrel largely ignores the host name. Host `localhost` is a special case used for binding to loopback addresses. Any host other than an explicit IP address binds to all public IP addresses. `Host` headers aren't validated.

As a workaround, use host-filtering middleware. Host-filtering middleware is provided by the [Microsoft.AspNetCore.HostFiltering](https://www.nuget.org/packages/Microsoft.AspNetCore.HostFiltering) package, which is implicitly provided for ASP.NET Core apps. The middleware is added by [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder), which calls [AddHostFiltering](/en-us/dotnet/api/microsoft.aspnetcore.builder.hostfilteringservicesextensions.addhostfiltering):

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}
```

Host-filtering middleware is disabled by default. To enable the middleware, define an `AllowedHosts` key in `appsettings.json`/`appsettings.{Environment}.json`. The value is a semicolon-delimited list of host names without port numbers:

`appsettings.json`:

```
{
  "AllowedHosts": "example.com;localhost"
}
```

Note

[Forwarded headers middleware](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0) also has an [AllowedHosts](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.allowedhosts#microsoft-aspnetcore-builder-forwardedheadersoptions-allowedhosts) option. Forwarded headers middleware and host-filtering middleware have similar functionality for different scenarios. Setting `AllowedHosts` with forwarded headers middleware is appropriate when the `Host` header isn't preserved while forwarding requests with a reverse proxy server or load balancer. Setting `AllowedHosts` with host-filtering middleware is appropriate when Kestrel is used as a public-facing edge server or when the `Host` header is directly forwarded.

For more information on forwarded headers middleware, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

## Libuv transport configuration

For projects that require the use of Libuv ([UseLibuv](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderlibuvextensions.uselibuv)):

* Add a dependency for the [`Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv`](https://www.nuget.org/packages/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv) package to the app's project file:

  ```
  <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv"
                    Version="{VERSION}" />
  ```

* Call [UseLibuv](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderlibuvextensions.uselibuv) on the `IWebHostBuilder`:

  ```
  public class Program
  {
      public static void Main(string[] args)
      {
          CreateHostBuilder(args).Build().Run();
      }

      public static IHostBuilder CreateHostBuilder(string[] args) =>
          Host.CreateDefaultBuilder(args)
              .ConfigureWebHostDefaults(webBuilder =>
              {
                  webBuilder.UseLibuv();
                  webBuilder.UseStartup<Startup>();
              });
  }
  ```

## HTTP/1.1 request draining

Opening HTTP connections is time consuming. For HTTPS, it's also resource intensive. Therefore, Kestrel tries to reuse connections per the HTTP/1.1 protocol. A request body must be fully consumed to allow the connection to be reused. The app doesn't always consume the request body, such as a `POST` requests where the server returns a redirect or 404 response. In the `POST`-redirect case:

* The client may already have sent part of the `POST` data.
* The server writes the 301 response.
* The connection can't be used for a new request until the `POST` data from the previous request body has been fully read.
* Kestrel tries to drain the request body. Draining the request body means reading and discarding the data without processing it.

The draining process makes a tradeoff between allowing the connection to be reused and the time it takes to drain any remaining data:

* Draining has a timeout of five seconds, which isn't configurable.
* If all of the data specified by the `Content-Length` or `Transfer-Encoding` header hasn't been read before the timeout, the connection is closed.

Sometimes you may want to terminate the request immediately, before or after writing the response. For example, clients may have restrictive data caps, so limiting uploaded data might be a priority. In such cases to terminate a request, call [HttpContext.Abort](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.abort) from a controller, Razor Page, or middleware.

There are caveats to calling `Abort`:

* Creating new connections can be slow and expensive.

* There's no guarantee that the client has read the response before the connection closes.

* Calling `Abort` should be rare and reserved for severe error cases, not common errors.

  * Only call `Abort` when a specific problem needs to be solved. For example, call `Abort` if malicious clients are trying to `POST` data or when there's a bug in client code that causes large or numerous requests.
  * Don't call `Abort` for common error situations, such as HTTP 404 (Not Found).

Calling [HttpResponse.CompleteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.completeasync) before calling `Abort` ensures that the server has completed writing the response. However, client behavior isn't predictable and they may not read the response before the connection is aborted.

This process is different for HTTP/2 because the protocol supports aborting individual request streams without closing the connection. The five second drain timeout doesn't apply. If there's any unread request body data after completing a response, then the server sends an HTTP/2 RST frame. Additional request body data frames are ignored.

If possible, it's better for clients to utilize the [Expect: 100-continue](https://developer.mozilla.org/docs/Web/HTTP/Status/100) request header and wait for the server to respond before starting to send the request body. That gives the client an opportunity to examine the response and abort before sending unneeded data.

## Additional resources

* When using UNIX sockets on Linux, the socket is not automatically deleted on app shut down. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/14134).
* [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)
* [Enforce HTTPS in ASP.NET Core](../../security/enforcing-ssl?view=aspnetcore-10.0)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [RFC 9110: HTTP Semantics (Section 7.2: Host and :authority)](https://www.rfc-editor.org/rfc/rfc9110#field.host)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor WebAssembly .NET bundle caching and integrity check failures

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains asset caching for Blazor WebAssembly and how to diagnose and resolve integrity failures.

When a Blazor WebAssembly app loads in the browser, the app downloads boot resources from the server:

* JavaScript code to bootstrap the app
* .NET runtime and assemblies
* Locale specific data

The Blazor boot configuration, inlined into `dotnet.js`, contains a fingerprinted manifest of the files that make up the app that must be downloaded along with a hash of each file's content. The app's files are preloaded and cached by the browser.

Note

†In .NET 10 or later, the `blazor.boot.json` manifest file is no longer present. If upgrading an app that relies on the manifest file for custom processing, we recommend collecting the information directly from the build.

When Blazor WebAssembly downloads an app's startup files, it instructs the browser to perform integrity checks on the responses. Blazor sends SHA-256 hash values for DLL (`.dll`), WebAssembly (`.wasm`), and other files. The file hashes of cached files are compared to the hashes in the boot configuration. An error is generated by the browser if any downloaded file's integrity check fails.

For more information, see the following sections of the *Fundamentals: Static files* article:

* [Preloaded Blazor framework static assets](../../fundamentals/static-files?view=aspnetcore-10.0#preloaded-blazor-framework-static-assets)
* [Fingerprint client-side static assets in standalone Blazor WebAssembly apps](../../fundamentals/static-files?view=aspnetcore-10.0#fingerprint-client-side-static-assets-in-standalone-blazor-webassembly-apps)

Except for Blazor's boot manifest file (`blazor.boot.json`), WebAssembly .NET runtime and app bundle files are cached on clients. The Blazor boot configuration contains a manifest of the files that make up the app that must be downloaded along with a hash of the file's content that's used to detect whether any of the boot resources have changed. Blazor caches downloaded files using the browser [Cache](https://developer.mozilla.org/docs/Web/API/Cache) API.

When Blazor WebAssembly downloads an app's startup files, it instructs the browser to perform integrity checks on the responses. Blazor sends SHA-256 hash values for DLL (`.dll`), WebAssembly (`.wasm`), and other files in the Blazor boot configuration, which isn't cached on clients. The file hashes of cached files are compared to the hashes in the Blazor boot configuration. For cached files with a matching hash, Blazor uses the cached files. Otherwise, files are requested from the server. After a file is downloaded, its hash is checked again for integrity validation. An error is generated by the browser if any downloaded file's integrity check fails.

Blazor's algorithm for managing file integrity:

* Ensures that the app doesn't risk loading an inconsistent set of files, for example if a new deployment is applied to your web server while the user is in the process of downloading the application files. Inconsistent files can result in a malfunctioning app.
* Ensures the user's browser never caches inconsistent or invalid responses, which can prevent the app from starting even if the user manually refreshes the page.
* Makes it safe to cache the responses and not check for server-side changes until the expected SHA-256 hashes themselves change, so subsequent page loads involve fewer requests and complete faster.

If the web server returns responses that don't match the expected SHA-256 hashes, an error similar to the following example appears in the browser's developer console:

> Failed to find a valid digest in the 'integrity' attribute for resource 'https\://myapp.example.com/\_framework/MyBlazorApp.dll' with computed SHA-256 integrity 'IIa70iwvmEg5WiDV17OpQ5eCztNYqL186J56852RpJY='. The resource has been blocked.

In most cases, the warning doesn't indicate a problem with integrity checking. Instead, the warning usually means that some other problem exists.

For Blazor WebAssembly's boot reference source, see [the `Boot.WebAssembly.ts` file in the `dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web.JS/src/Boot.WebAssembly.ts).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Diagnose integrity problems

When an app is built, the generated boot manifest describes the SHA-256 hashes of boot resources at the time that the build output is produced. The integrity check passes as long as the SHA-256 hashes in the boot manifest match the files delivered to the browser.

Common reasons why this fails include:

* The web server's response is an error (for example, a *404 - Not Found* or a *500 - Internal Server Error*) instead of the file the browser requested. This is reported by the browser as an integrity check failure and not as a response failure.

* Something has changed the contents of the files between the build and delivery of the files to the browser. This might happen:

  * If you or build tools manually modify the build output.
  * If some aspect of the deployment process modified the files. For example if you use a Git-based deployment mechanism, bear in mind that Git transparently converts Windows-style line endings to Unix-style line endings if you commit files on Windows and check them out on Linux. Changing file line endings change the SHA-256 hashes. To avoid this problem, consider [using `.gitattributes` to treat build artifacts as `binary` files](https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes).
  * The web server modifies the file contents as part of serving them. For example, some content distribution networks (CDNs) automatically attempt to [minify](../../../client-side/bundling-and-minification?view=aspnetcore-10.0#minification) HTML, thereby modifying it. You may need to disable such features.

* The boot manifest fails to load properly or is improperly cached on the client. Common causes include either of the following:

  * Misconfigured or malfunctioning custom developer code.
  * One or more misconfigured intermediate caching layers.

To diagnose which of these applies in your case:

1. Note which file is triggering the error by reading the error message.
2. Open your browser's developer tools and look in the *Network* tab. If necessary, reload the page to see the list of requests and responses. Find the file that is triggering the error in that list.
3. Check the HTTP status code in the response. If the server returns anything other than *200 - OK* (or another 2xx status code), then you have a server-side problem to diagnose. For example, status code 403 means there's an authorization problem, whereas status code 500 means the server is failing in an unspecified manner. Consult server-side logs to diagnose and fix the app.
4. If the status code is *200 - OK* for the resource, look at the response content in browser's developer tools and check that the content matches up with the data expected. For example, a common problem is to misconfigure routing so that requests return your `index.html` data even for other files. Make sure that responses to `.wasm` requests are WebAssembly binaries and that responses to `.dll` requests are .NET assembly binaries. If not, you have a server-side routing problem to diagnose.

1) Note which file is triggering the error by reading the error message.
2) Open your browser's developer tools and look in the *Network* tab. If necessary, reload the page to see the list of requests and responses. Find the file that is triggering the error in that list.
3) Check the HTTP status code in the response. If the server returns anything other than *200 - OK* (or another 2xx status code), then you have a server-side problem to diagnose. For example, status code 403 means there's an authorization problem, whereas status code 500 means the server is failing in an unspecified manner. Consult server-side logs to diagnose and fix the app.
4) If the status code is *200 - OK* for the resource, look at the response content in browser's developer tools and check that the content matches up with the data expected. For example, a common problem is to misconfigure routing so that requests return your `index.html` data even for other files. Make sure that responses to `.wasm` requests are WebAssembly binaries and that responses to `.dll` requests are .NET assembly binaries. If not, you have a server-side routing problem to diagnose.
5) Seek to validate the app's published and deployed output with the [Troubleshoot integrity PowerShell script](#troubleshoot-integrity-powershell-script).

If you confirm that the server is returning plausibly correct data, there must be something else modifying the contents in between build and delivery of the file. To investigate this:

* Examine the build toolchain and deployment mechanism in case they're modifying files after the files are built. An example of this is when Git transforms file line endings, as described earlier.
* Examine the web server or CDN configuration in case they're set up to modify responses dynamically (for example, trying to minify HTML). It's fine for the web server to implement HTTP compression (for example, returning `content-encoding: br` or `content-encoding: gzip`), since this doesn't affect the result after decompression. However, it's *not* fine for the web server to modify the uncompressed data.

## Troubleshoot integrity PowerShell script

Use the [`integrity.ps1`](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/samples/integrity.ps1?raw=true) PowerShell script to validate a published and deployed Blazor app. The script is provided for PowerShell Core 7 or later as a starting point when the app has integrity issues that the Blazor framework can't identify. Customization of the script might be required for your apps, including if running on version of PowerShell later than version 7.2.0.

The script checks the files in the `publish` folder and downloaded from the deployed app to detect issues in the different manifests that contain integrity hashes. These checks should detect the most common problems:

* You modified a file in the published output without realizing it.
* The app wasn't correctly deployed to the deployment target, or something changed within the deployment target's environment.
* There are differences between the deployed app and the output from publishing the app.

Invoke the script with the following command in a PowerShell command shell:

```
.\integrity.ps1 {BASE URL} {PUBLISH OUTPUT FOLDER}
```

In the following example, the script is executed on a locally-running app at `https://localhost:5001/`:

```
.\integrity.ps1 https://localhost:5001/ C:\TestApps\BlazorSample\bin\Release\net6.0\publish\
```

Placeholders:

* `{BASE URL}`: The URL of the deployed app. A trailing slash (`/`) is required.
* `{PUBLISH OUTPUT FOLDER}`: The path to the app's `publish` folder or location where the app is published for deployment.

Note

When cloning the `dotnet/AspNetCore.Docs` GitHub repository, the `integrity.ps1` script might be quarantined by [Bitdefender](https://www.bitdefender.com) or another virus scanner present on the system. Usually, the file is trapped by a virus scanner's *heuristic scanning* technology, which merely looks for patterns in files that might indicate the presence of malware. To prevent the virus scanner from quarantining the file, add an exception to the virus scanner prior to cloning the repository. The following example is a typical path to the script on a Windows system. Adjust the path as needed for other systems. The `{USER}` placeholder is the user's path segment.

```
C:\Users\{USER}\Documents\GitHub\AspNetCore.Docs\aspnetcore\blazor\host-and-deploy\webassembly\samples\integrity.ps1
```

**Warning**: *Creating virus scanner exceptions is dangerous and should only be performed when you're certain that the file is safe.*

Comparing the checksum of a file to a valid checksum value doesn't guarantee file safety, but modifying a file in a way that maintains a checksum value isn't trivial for malicious users. Therefore, checksums are useful as a general security approach. Compare the checksum of the local `integrity.ps1` file to one of the following values:

* SHA256: `32c24cb667d79a701135cb72f6bae490d81703323f61b8af2c7e5e5dc0f0c2bb`
* MD5: `9cee7d7ec86ee809a329b5406fbf21a8`

Obtain the file's checksum on Windows OS with the following command. Provide the path and file name for the `{PATH AND FILE NAME}` placeholder and indicate the type of checksum to produce for the `{SHA512|MD5}` placeholder, either `SHA256` or `MD5`:

```
CertUtil -hashfile {PATH AND FILE NAME} {SHA256|MD5}
```

If you have any cause for concern that checksum validation isn't secure enough in your environment, consult your organization's security leadership for guidance.

For more information, see [Microsoft Defender for Business](/en-us/defender-business/).

## Disable integrity checking for non-PWA apps

In most cases, don't disable integrity checking. Disabling integrity checking doesn't solve the underlying problem that has caused the unexpected responses and results in losing the benefits listed earlier.

There may be cases where the web server can't be relied upon to return consistent responses, and you have no choice but to temporarily disable integrity checks until the underlying problem is resolved.

To disable integrity checks, [manually load client-side boot resources](../../fundamentals/startup?view=aspnetcore-10.0#load-client-side-boot-resources) and avoid passing the `integrity` parameter to the `fetch` call.

To disable integrity checks, add the following to a property group in the Blazor WebAssembly app's project file (`.csproj`):

```
<BlazorCacheBootResources>false</BlazorCacheBootResources>
```

`BlazorCacheBootResources` also disables Blazor's default behavior of caching the `.dll`, `.wasm`, and other files based on their SHA-256 hashes because the property indicates that the SHA-256 hashes can't be relied upon for correctness. Even with this setting, the browser's normal HTTP cache may still cache those files, but whether or not this happens depends on your web server configuration and the `cache-control` headers that it serves.

Note

The `BlazorCacheBootResources` property doesn't disable integrity checks for [Progressive Web Applications (PWAs)](../../progressive-web-app/?view=aspnetcore-10.0). For guidance pertaining to PWAs, see the [Disable integrity checking for PWAs](#disable-integrity-checking-for-pwas) section.

We can't provide an exhaustive list of scenarios where disabling integrity checking is required. Servers can answer a request in arbitrary ways outside of the scope of the Blazor framework. The framework permits the preceding approach to make the app runnable at the cost of *losing a guarantee of integrity that the app can provide*. Again, we don't recommend disabling integrity checking, especially for production deployments. Developers should seek to solve the underlying integrity problem that's causing integrity checking to fail.

A few general cases that can cause integrity issues are:

* Running on HTTP where integrity can't be checked.
* If your deployment process modifies the files after publish in any way.
* If your host modifies the files in any way.

## Disable integrity checking for PWAs

Blazor's Progressive Web Application (PWA) template contains a suggested `service-worker.published.js` file that's responsible for fetching and storing application files for offline use. This is a separate process from the normal app startup mechanism and has its own separate integrity checking logic.

Inside the `service-worker.published.js` file, following line is present:

```
.map(asset => new Request(asset.url, { integrity: asset.hash }));
```

To disable integrity checking, remove the `integrity` parameter by changing the line to the following:

```
.map(asset => new Request(asset.url));
```

Again, disabling integrity checking means that you lose the safety guarantees offered by integrity checking. For example, there is a risk that if the user's browser is caching the app at the exact moment that you deploy a new version, it could cache some files from the old deployment and some from the new deployment. If that happens, the app becomes stuck in a broken state until you deploy a further update.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/prerendered-state-persistence
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/state-management/prerendered-state-persistence.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor prerendered state persistence

This article explains how to persist component state across prerendering in Blazor apps using the Persistent Component State service. You'll learn how to use the `[PersistentState]` attribute, the `PersistentComponentState` service directly, and how to create custom serializers for persistent state.

Without persisting component state, state used during prerendering is lost and must be recreated when the app is fully loaded. If any state is created asynchronously, the UI may flicker as the prerendered UI is replaced with temporary loading content and then fully rendered again.

Consider the following `PrerenderedCounter1` counter component. The component sets an initial random counter value during prerendering in [`OnInitialized` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync). When the component then renders interactively, the initial count value is replaced when `OnInitialized` executes a second time.

`PrerenderedCounter1.razor`:

```
@page "/prerendered-counter-1"
@inject ILogger<PrerenderedCounter1> Logger

<PageTitle>Prerendered Counter 1</PageTitle>

<h1>Prerendered Counter 1</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;

    protected override void OnInitialized()
    {
        currentCount = Random.Shared.Next(100);
        Logger.LogInformation("currentCount set to {Count}", currentCount);
    }

    private void IncrementCount() => currentCount++;
}
```

Note

If the app adopts [interactive routing](../fundamentals/routing?view=aspnetcore-10.0#static-versus-interactive-routing) and the page is reached via an internal [enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling), prerendering doesn't occur. Therefore, you must perform a full page reload for the `PrerenderedCounter1` component to see the following output. For more information, see the [Interactive routing and prerendering](#interactive-routing-and-prerendering) section.

Run the app and inspect logging from the component. The following is example output.

> info: BlazorSample.Components.Pages.PrerenderedCounter1\[0]\
> currentCount set to 41\
> info: BlazorSample.Components.Pages.PrerenderedCounter1\[0]\
> currentCount set to 92

The first logged count occurs during prerendering. The count is set again after prerendering when the component is rerendered. There's also a flicker in the UI when the count updates from 41 to 92.

To retain the initial value of the counter during prerendering, Blazor supports persisting state in a prerendered page using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service (and for components embedded into pages or views of Razor Pages or MVC apps, the [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-10.0)).

By initializing components with the same state used during prerendering, any expensive initialization steps are only executed once. The rendered UI also matches the prerendered UI, so no flicker occurs in the browser.

The persisted prerendered state is transferred to the client, where it's used to restore the component state. During client-side rendering (CSR, `InteractiveWebAssembly`), the data is exposed to the browser and must not contain sensitive, private information. During interactive server-side rendering (interactive SSR, `InteractiveServer`), [ASP.NET Core Data Protection](../../security/data-protection/introduction?view=aspnetcore-10.0) ensures that the data is transferred securely. The `InteractiveAuto` render mode combines WebAssembly and Server interactivity, so it's necessary to consider data exposure to the browser, as in the CSR case.

To persist prerendered state using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service, apply the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute) to `public` properties. The state is retrieved when the component renders interactively or the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service is instantiated.

Use `public` properties because reflection is used by the framework for tasks such as [trimming unused code](../performance/app-download-size?view=aspnetcore-10.0#intermediate-language-il-trimming) and [source generation](/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview).

By default, properties are serialized using the [System.Text.Json](/en-us/dotnet/api/system.text.json) serializer with default settings and persisted in the prerendered HTML. Serialization isn't trimmer safe and requires preservation of the types used. For more information, see [Configure the Trimmer for ASP.NET Core Blazor](../host-and-deploy/configure-trimmer?view=aspnetcore-10.0).

The following counter component persists counter state during prerendering and retrieves the state to initialize the component:

* The [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute) is applied to the public nullable `CurrentCount` property of type `int?`.
* The counter's state is assigned when `null` in `OnInitialized` and restored automatically when the component renders interactively.

`PrerenderedCounter2.razor`:

```
@page "/prerendered-counter-2"
@inject ILogger<PrerenderedCounter2> Logger

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [PersistentState]
    public int? CurrentCount { get; set; }

    protected override void OnInitialized()
    {
        if (CurrentCount is null)
        {
            CurrentCount = Random.Shared.Next(100);
            Logger.LogInformation("CurrentCount set to {Count}", CurrentCount);
        }
        else
        {
            Logger.LogInformation("CurrentCount restored to {Count}", CurrentCount);
        }
    }

    private void IncrementCount() => CurrentCount++;
}
```

When the component executes, `CurrentCount` is only set once during prerendering. The value is restored when the component is rerendered. The following is example output.

Note

If the app adopts [interactive routing](../fundamentals/routing?view=aspnetcore-10.0#static-versus-interactive-routing) and the page is reached via an internal [enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling), prerendering doesn't occur. Therefore, you must perform a full page reload for the component to see the following output. For more information, see the [Interactive routing and prerendering](#interactive-routing-and-prerendering) section.

> info: BlazorSample.Components.Pages.PrerenderedCounter2\[0]\
> CurrentCount set to 96\
> info: BlazorSample.Components.Pages.PrerenderedCounter2\[0]\
> CurrentCount restored to 96

In the following example that serializes state for multiple components of the same type:

* Public properties annotated with the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute) are serialized during prerendering.
* The [`@key` directive attribute](../components/element-component-model-relationships?view=aspnetcore-10.0#use-of-the-key-directive-attribute) is used to ensure that the state is correctly associated with the component instance.
* The `Element` property is initialized in the [`OnInitialized` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) to avoid null reference exceptions, similarly to how null references are avoided for query parameters and form data.

`PersistentChild.razor`:

```
<div>
    <p>Current count: @Element.CurrentCount</p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</div>

@code {
    [PersistentState]
    public State Element { get; set; }

    protected override void OnInitialized()
    {
        Element ??= new State();
    }

    private void IncrementCount()
    {
        Element.CurrentCount++;
    }

    private class State
    {
        public int CurrentCount { get; set; }
    }
}
```

`Parent.razor`:

```
@page "/parent"

@foreach (var element in elements)
{
    <PersistentChild @key="element.Name" />
}
```

Note

A user-configured [JsonSerializerContext](/en-us/dotnet/api/system.text.json.serialization.jsonserializercontext) doesn't flow into [C# union](/en-us/dotnet/csharp/language-reference/builtin-types/union) deserialization. Instead, use `[JsonUnion(TypeClassifier = typeof({TYPE CLASSIFIER}))]`, where the `{TYPE CLASSIFIER}` placeholder is the type classifier.

## Serialize state for services

In the following example that serializes state for a dependency injection service:

* Properties annotated with the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute) are serialized during prerendering and deserialized when the app becomes interactive.

* The [RegisterPersistentService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsrazorcomponentbuilderextensions.registerpersistentservice) extension method is used to register the service for persistence. The render mode is required because the render mode can't be inferred from the service type. Use any of the following values:

  * `RenderMode.Server`: The service is available for the Interactive Server render mode.
  * `RenderMode.Webassembly`: The service is available for the Interactive Webassembly render mode.
  * `RenderMode.InteractiveAuto`: The service is available for both the Interactive Server and Interactive Webassembly render modes if a component renders in either of those modes.

* The service is resolved during the initialization of an interactive render mode, and the properties annotated with the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute) are deserialized.

Note

Only persisting scoped services is supported on the **server**. On the WebAssembly client (`InteractiveAuto` or `InteractiveWebAssembly` render modes), the service must be registered as a **singleton**. See [Client project service registration](#client-project-service-registration) for details.

Serialized properties are identified from the actual service instance:

* This approach allows marking an abstraction as a persistent service.
* Enables actual implementations to be internal or different types.
* Supports shared code in different assemblies.
* Results in each instance exposing the same properties.

The following counter service, `CounterTracker`, marks its current count property, `CurrentCount` with the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute). The public property is serialized during prerendering and deserialized when the app becomes interactive wherever the service is injected.

`CounterTracker.cs`:

```
public class CounterTracker
{
    [PersistentState]
    public int CurrentCount { get; set; }

    public void IncrementCount()
    {
        CurrentCount++;
    }
}
```

### Server project service registration

In the server project's `Program` file, register the scoped service and register the service for persistence with `RegisterPersistentService`. In the following example, the `CounterTracker` service is available for both the Interactive Server and Interactive WebAssembly render modes if a component renders in either of those modes because it's registered with `RenderMode.InteractiveAuto`.

If the `Program` file doesn't already use the [Microsoft.AspNetCore.Components.Web](/en-us/dotnet/api/microsoft.aspnetcore.components.web) namespace, add the following `using` statement to the top of the file:

```
using Microsoft.AspNetCore.Components.Web;
```

Where services are registered in the server project's `Program` file:

```
builder.Services.AddScoped<CounterTracker>();

builder.Services.AddRazorComponents()
    .RegisterPersistentService<CounterTracker>(RenderMode.InteractiveAuto);
```

### Client project service registration

For `RenderMode.InteractiveAuto` or `RenderMode.InteractiveWebAssembly`, the service must also be registered in the `.Client` project's `Program` file. Register it as a **singleton**, not scoped:

```
builder.Services.AddSingleton<CounterTracker>();
```

The client-side `ComponentStatePersistenceManager` is registered as a singleton, so it resolves its dependencies from the root service provider. *Avoid a scoped service registration.* If the service is incorrectly registered as scoped on the client, resolving it from the root provider throws a `DirectScopedResolvedFromRootException` in Development environments. Beyond the exception, a scoped registration produces two separate instances on the client: one resolved from the root during state restoration and another from the app scope for injected components. This means the restored state doesn't reach the instance the components use. Registering the service as a singleton on the client ensures that the same instance is used for both state restoration and component injection.

Inject the `CounterTracker` service into a component and use it to increment a counter. For demonstration purposes in the following example, the value of the service's `CurrentCount` property is set to 10 only during prerendering.

`Pages/Counter.razor`:

```
@page "/counter"
@inject CounterTracker CounterTracker

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p>Rendering: @RendererInfo.Name</p>

<p role="status">Current count: @CounterTracker.CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    protected override void OnInitialized()
    {
        if (!RendererInfo.IsInteractive)
        {
            CounterTracker.CurrentCount = 10;
        }
    }

    private void IncrementCount()
    {
        CounterTracker.IncrementCount();
    }
}
```

To use preceding component to demonstrate persisting the count of 10 in `CounterTracker.CurrentCount`, navigate to the component and refresh the browser, which triggers prerendering. When prerendering occurs, you briefly see [RendererInfo.Name](/en-us/dotnet/api/microsoft.aspnetcore.components.rendererinfo.name) indicate "`Static`" before displaying "`Server`" after final rendering. The counter starts at 10.

## Use the `PersistentComponentState` service directly instead of the declarative model

As an alternative to using the declarative model for persisting state with the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute), you can use the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service directly, which offers greater flexibility for complex state persistence scenarios. Call [PersistentComponentState.RegisterOnPersisting](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) to register a callback to persist the component state during prerendering. The state is retrieved when the component renders interactively. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown.

The following counter component example persists counter state during prerendering and retrieves the state to initialize the component.

`PrerenderedCounter3.razor`:

```
@page "/prerendered-counter-3"
@implements IDisposable
@inject ILogger<PrerenderedCounter3> Logger
@inject PersistentComponentState ApplicationState

<PageTitle>Prerendered Counter 3</PageTitle>

<h1>Prerendered Counter 3</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override void OnInitialized()
    {
        if (!ApplicationState.TryTakeFromJson<int>(
            nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
            Logger.LogInformation("currentCount set to {Count}", currentCount);
        }
        else
        {
            currentCount = restoredCount!;
            Logger.LogInformation("currentCount restored to {Count}", currentCount);
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount);
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }

    private void IncrementCount() => currentCount++;

    void IDisposable.Dispose() => persistingSubscription.Dispose();
}
```

When the component executes, `currentCount` is only set once during prerendering. The value is restored when the component is rerendered. The following is example output.

Note

If the app adopts [interactive routing](../fundamentals/routing?view=aspnetcore-10.0#static-versus-interactive-routing) and the page is reached via an internal [enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling), prerendering doesn't occur. Therefore, you must perform a full page reload for the component to see the following output. For more information, see the [Interactive routing and prerendering](#interactive-routing-and-prerendering) section.

> info: BlazorSample.Components.Pages.PrerenderedCounter3\[0]\
> currentCount set to 96\
> info: BlazorSample.Components.Pages.PrerenderedCounter3\[0]\
> currentCount restored to 96

To preserve prerendered state, decide what state to persist using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service. [PersistentComponentState.RegisterOnPersisting](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) registers a callback to persist the component state during prerendering. The state is retrieved when the component renders interactively. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown.

The following counter component example persists counter state during prerendering and retrieves the state to initialize the component.

`PrerenderedCounter2.razor`:

```
@page "/prerendered-counter-2"
@implements IDisposable
@inject ILogger<PrerenderedCounter2> Logger
@inject PersistentComponentState ApplicationState

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override void OnInitialized()
    {
        if (!ApplicationState.TryTakeFromJson<int>(
            nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
            Logger.LogInformation("currentCount set to {Count}", currentCount);
        }
        else
        {
            currentCount = restoredCount!;
            Logger.LogInformation("currentCount restored to {Count}", currentCount);
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount);
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose() => persistingSubscription.Dispose();

    private void IncrementCount() => currentCount++;
}
```

When the component executes, `currentCount` is only set once during prerendering. The value is restored when the component is rerendered. The following is example output.

Note

If the app adopts [interactive routing](../fundamentals/routing?view=aspnetcore-10.0#static-versus-interactive-routing) and the page is reached via an internal [enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling), prerendering doesn't occur. Therefore, you must perform a full page reload for the component to see the following output. For more information, see the [Interactive routing and prerendering](#interactive-routing-and-prerendering) section.

> info: BlazorSample.Components.Pages.PrerenderedCounter2\[0]\
> currentCount set to 96\
> info: BlazorSample.Components.Pages.PrerenderedCounter2\[0]\
> currentCount restored to 96

## Serialization extensibility for persistent component state

Implement a custom serializer with [PersistentComponentStateSerializer\<T>](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstateserializer-1). Without a registered custom serializer, serialization falls back to the existing JSON serialization.

The following example creates a custom serializer for `int?` types that uses a custom format to demonstrate serialization extensibility. The serializer prefixes integer values with "`CUSTOM:`" to clearly distinguish them from JSON serialization.

The serializer executes on nullable and non-nullable types *independently*, so the following serializer doesn't execute on `int` types, only nullable integer types (`int?`) are processed.

Logging in the following example is for demonstration purposes and isn't normally implemented in a production app.

`CustomIntSerializer.cs`:

```
using System;
using System.Buffers;
using System.Text;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorSample;

public class CustomIntSerializer(ILogger<CustomIntSerializer> logger) 
    : PersistentComponentStateSerializer<int?>
{
    public override void Persist(int? value, IBufferWriter<byte> writer)
    {
        string customFormat = 
            value is not null ? $"CUSTOM:{value}" : $"CUSTOM:null";

        logger.LogInformation(
            "Persisting value {Value} with custom format: {CustomFormat}", value, 
            customFormat);

        byte[] bytes = Encoding.UTF8.GetBytes(customFormat);
        writer.Write(bytes);
    }

    public override int? Restore(ReadOnlySequence<byte> data)
    {
        byte[] bytes = data.ToArray();
        string text = Encoding.UTF8.GetString(bytes);

        logger.LogInformation("Restoring value from custom format: {CustomFormat}", 
            text);

        if (text.StartsWith("CUSTOM:", StringComparison.Ordinal))
        {
            var remainingText = text.AsSpan(7);

            if (!remainingText.SequenceEqual("null"))
            {
                if (int.TryParse(remainingText, out int value))
                {
                    return value;
                }
            }
            else
            {
                return null;
            }
        }

        // Fallback to direct parsing if format is unexpected
        return int.TryParse(text, out int fallbackValue) ? fallbackValue : null;
    }
}
```

The custom serializer is registered in the app's `Program` file:

```
builder.Services.AddSingleton<PersistentComponentStateSerializer<int?>, 
    CustomIntSerializer>();
```

The `int?` type is automatically persisted and restored with the custom serializer:

```
[PersistentState]
public int? CurrentCount { get; set; }
```

Using the preceding serializer with the `PrerenderedCounter2` component (`PrerenderedCounter2.razor`) shown in the [introduction](#aspnet-core-blazor-prerendered-state-persistence) of this article, output similar to the following is logged.

Note

If the app adopts [interactive routing](../fundamentals/routing?view=aspnetcore-10.0#static-versus-interactive-routing) and the page is reached via an internal [enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling), prerendering doesn't occur. Therefore, you must perform a full page reload for the component to see the following output. For more information, see the [Interactive routing and prerendering](#interactive-routing-and-prerendering) section.

> info: BlazorSample.Components.Pages.PrerenderedCounter2\[0]\
> CurrentCount set to 49\
> info: BlazorSample.CustomIntSerializer\[0]\
> Persisting value 49 with custom format: CUSTOM:49\
> info: BlazorSample.CustomIntSerializer\[0]\
> Restoring value from custom format: CUSTOM:49\
> info: BlazorSample.Components.Pages.PrerenderedCounter2\[0]\
> CurrentCount restored to 49

## Components embedded into pages and views (Razor Pages/MVC)

For components embedded into a page or view of a Razor Pages or MVC app, you must add the [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-10.0) with the `<persist-component-state />` HTML tag inside the closing `</body>` tag of the app's layout. **This is only required for Razor Pages and MVC apps.** For more information, see [Persist Component State Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-10.0).

`Pages/Shared/_Layout.cshtml`:

```
<body>
    ...

    <persist-component-state />
</body>
```

## Interactive routing and prerendering

When the `Routes` component doesn't define a render mode, the app is using per-page/component interactivity and navigation. Using per-page/component navigation, internal navigation is handled by [enhanced routing](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling) after the app becomes interactive. "Internal navigation" in this context means that the URL destination of the navigation event is a Blazor endpoint inside the app.

Blazor supports handling persistent component state during [enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling). State persisted during enhanced navigation can be read by interactive components on the page.

By default, persistent component state is only loaded by interactive components when they're initially loaded on the page. This prevents important state, such as data in an edited webform, from being overwritten if additional enhanced navigation events to the same page occur after the component is loaded.

If the data is read-only and doesn't change frequently, opt-in to allow updates during enhanced navigation by setting [AllowUpdates](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute.allowupdates) to `true` on the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute). This is useful for scenarios such as displaying cached data that's expensive to fetch but doesn't change often, such as weather forecast data in the following example:

```
[PersistentState(AllowUpdates = true)]
public WeatherForecast[]? Forecasts { get; set; }

protected override async Task OnInitializedAsync()
{
    Forecasts ??= await ForecastService.GetForecastAsync();
}
```

Note

[PersistentStateAttribute.AllowUpdates](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute.allowupdates) doesn't control whether the current property value is captured by [circuit state persistence](server?view=aspnetcore-10.0#circuit-state-persistence) when the circuit pauses, including an automatic pause caused by [tab inactivity](server?view=aspnetcore-10.0#automatic-circuit-pause-on-tab-inactivity).

Note

[PersistentStateAttribute.AllowUpdates](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute.allowupdates) doesn't control whether the current property value is captured by [circuit state persistence](server?view=aspnetcore-10.0#circuit-state-persistence) when the circuit pauses.

To skip restoring state during prerendering, set `RestoreBehavior` to `SkipInitialValue`:

```
[PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)]
public string NoPrerenderedData { get; set; }
```

To skip restoring state during reconnection, set `RestoreBehavior` to `SkipLastSnapshot`. This can be useful to ensure fresh data after reconnection:

```
[PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
public int CounterNotRestoredOnReconnect { get; set; }
```

Call [PersistentComponentState.RegisterOnRestoring](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronrestoring) to register a callback for imperatively controlling how state is restored, similar to how [PersistentComponentState.RegisterOnPersisting](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) provides full control of how state is persisted.

The [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service only works on the initial page load and not across internal enhanced page navigation events.

If the app performs a full (non-enhanced) navigation to a page utilizing persistent component state, the persisted state is made available for the app to use when it becomes interactive.

If an interactive circuit has already been established and an enhanced navigation is performed to a page utilizing persistent component state, the state *isn't made available in the existing circuit for the component to use*. There's no prerendering for the internal page request, and the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service isn't aware that an enhanced navigation has occurred. There's no mechanism to deliver state updates to components that are already running on an existing circuit. The reason for this is that Blazor only supports passing state from the server to the client at the time the runtime initializes, not after the runtime has started.

Disabling enhanced navigation, which reduces performance but also avoids the problem of loading state with [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) for internal page requests, is covered in [ASP.NET Core Blazor navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling). Alternatively, update the app to .NET 10 or later, where Blazor supports handling persistent component state when during enhanced navigation.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/host-filtering
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel/host-filtering.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host filtering with ASP.NET Core Kestrel web server

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

While Kestrel supports configuration based on prefixes such as `http://example.com:5000`, Kestrel largely ignores the host name. Host `localhost` is a special case used for binding to loopback addresses. Any host other than an explicit IP address binds to all public IP addresses. `Host` headers aren't validated.

As a workaround, use host-filtering middleware. The middleware is added by [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder), which calls [AddHostFiltering](/en-us/dotnet/api/microsoft.aspnetcore.builder.hostfilteringservicesextensions.addhostfiltering):

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}
```

Host-filtering middleware is disabled by default. To enable the middleware, define an `AllowedHosts` key in `appsettings.json`/`appsettings.{Environment}.json`. The value is a semicolon-delimited list of host names without port numbers:

`appsettings.json`:

```
{
  "AllowedHosts": "example.com;localhost"
}
```

Note

[Forwarded headers middleware](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0) also has an [AllowedHosts](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.allowedhosts#microsoft-aspnetcore-builder-forwardedheadersoptions-allowedhosts) option. Forwarded headers middleware and host-filtering middleware have similar functionality for different scenarios. Setting `AllowedHosts` with forwarded headers middleware is appropriate when the `Host` header isn't preserved while forwarding requests with a reverse proxy server or load balancer. Setting `AllowedHosts` with host-filtering middleware is appropriate when Kestrel is used as a public-facing edge server or when the `Host` header is directly forwarded.

For more information on forwarded headers middleware, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tooling
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tooling.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tooling for ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes tools for building Blazor apps using several tools:

* [Visual Studio (VS)](https://visualstudio.microsoft.com): The most comprehensive integrated development environment (IDE) for .NET developers on Windows. Includes an array of tools and features to elevate and enhance every stage of software development.
* [Visual Studio Code (VS Code)](https://code.visualstudio.com) is an open source, cross-platform code editor that can be used to develop Blazor apps.
* [.NET CLI](/en-us/dotnet/core/tools/): The .NET command-line interface (CLI) is a cross-platform toolchain for developing, building, running, and publishing .NET applications. The .NET CLI is included with the [.NET SDK](/en-us/dotnet/core/sdk) and runs on any platform supported by the SDK.

Select the pivot of this article that matches your tooling choice.

To create a Blazor app with Visual Studio, use the following guidance:

* Install the latest version of [Visual Studio](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2022) with the **ASP.NET and web development** workload.

* Create a new project using one of the available Blazor templates:

  * **Blazor Web App**: Creates an app that supports interactive server-side rendering (interactive SSR) and client-side rendering (CSR). The Blazor Web App template is recommended for getting started with Blazor to learn about server-side and client-side Blazor features.
  * **Blazor WebAssembly Standalone App**: Creates a standalone client web app that can be deployed as a static site.

Select **Next**.

* Install the latest version of [Visual Studio](https://visualstudio.microsoft.com) with the **ASP.NET and web development** workload.

* Create a new project:

  * For a Blazor Server experience, choose the **Blazor Server App** template, which includes demonstration code and [Bootstrap](https://getbootstrap.com/), or the **Blazor Server App Empty** template without demonstration code and Bootstrap. Select **Next**.
  * For a standalone Blazor WebAssembly experience, choose the **Blazor WebAssembly App** template, which includes demonstration code and Bootstrap, or the **Blazor WebAssembly App Empty** template without demonstration code and Bootstrap. Select **Next**.

- Install the latest version of [Visual Studio](https://visualstudio.microsoft.com) with the **ASP.NET and web development** workload.

- Create a new project:

  * For a Blazor Server experience, choose the **Blazor Server App** template. Select **Next**.
  * For a Blazor WebAssembly experience, choose the **Blazor WebAssembly App** template. Select **Next**.

* Provide a **Project name** and confirm that the **Location** is correct.

* For more information on the options in the **Additional information** dialog, see the [Blazor project templates and template options](#blazor-project-templates-and-template-options) section.

- For a *hosted* Blazor WebAssembly app, select the **ASP.NET Core Hosted** checkbox in the **Additional information** dialog.

* Select **Create**.

[Visual Studio Code](https://code.visualstudio.com) is an open source, cross-platform Integrated Development Environment (IDE) that can be used to develop Blazor apps.

Install the latest version of [Visual Studio Code](https://code.visualstudio.com/Download) for your platform.

Install the [C# Dev Kit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit). For more information, see [Debug ASP.NET Core Blazor apps](debug?view=aspnetcore-10.0#visual-studio-code-prerequisites).

If you're new to VS Code, see the [VS Code documentation](https://code.visualstudio.com/docs). If you're new to the .NET SDK, see [What is the .NET SDK?](/en-us/dotnet/core/sdk) and the associated articles in the .NET SDK documentation.

Create a new project:

* Open VS Code.

* Go to the **Explorer** view and select the **Create .NET Project** button. Alternatively, you can bring up the **Command Palette** using `Ctrl`+`Shift`+`P`, and then type "`.NET`" and find and select the **.NET: New Project** command.

* Select the Blazor project template from the list.

* In the **Project Location** dialog, create or select a folder for the project.

* In the **Command Palette**, provide a name for the project or accept the default name.

* Select **Create project** to create the project or adjust the project's options by selecting **Show all template options**. For more information on the templates and options, see the [Blazor project templates and template options](#blazor-project-templates-and-template-options) section.

* Press `F5` on the keyboard to run the app with the debugger or `Ctrl`+`F5` to run the app without the debugger.

  The **Command Palette** asks you to select a debugger. Select **C#** from the list.

  Next, select the **https** launch configuration.

* To stop the app, press `Shift`+`F5` on the keyboard.

The Visual Studio Code (VS Code) instructions for ASP.NET Core development in some parts of the Blazor documentation use the [.NET CLI](/en-us/dotnet/core/tools/), which is part of the .NET SDK. .NET CLI commands are issued in VS Code's integrated [**Terminal**](https://code.visualstudio.com/docs/editor/integrated-terminal), which defaults to a [PowerShell command shell](/en-us/powershell/). The **Terminal** is opened by selecting **New Terminal** from the **Terminal** menu in the menu bar.

For more information on Visual Studio Code configuration and use, see the [Visual Studio Code documentation](https://code.visualstudio.com/docs).

**Hosted Blazor WebAssembly launch and task configuration**

For hosted Blazor WebAssembly [solutions](#visual-studio-solution-file-sln), add (or move) the `.vscode` folder with `launch.json` and `tasks.json` files to the solution's parent folder, which is the folder that contains the typical project folders: Client, Server, and `Shared`. Update or confirm that the configuration in the `launch.json` and `tasks.json` files execute a hosted Blazor WebAssembly app from the **Server** project.

Examine the `Properties/launchSettings.json` file and determine the URL of the app from the `applicationUrl` property. Depending on the framework version, the URL protocol is either secure (HTTPS) `https://localhost:{PORT}` or insecure (HTTP) `http://localhost:{PORT}`, where the `{PORT}` placeholder is an assigned port. Note the URL for use in the `launch.json` file.

In the launch configuration of the `.vscode/launch.json` file:

* Set the current working directory (`cwd`) to the **Server** project folder.
* Indicate the app's URL with the `url` property. Use the value recorded earlier from the `Properties/launchSettings.json` file.

```
"cwd": "${workspaceFolder}/{SERVER APP FOLDER}",
"url": "{URL}"
```

In the preceding configuration:

* The `{SERVER APP FOLDER}` placeholder is the **Server** project's folder, typically Server.
* The `{URL}` placeholder is the app's URL, which is specified in the app's `Properties/launchSettings.json` file in the `applicationUrl` property.

If Google Chrome is preferred over Microsoft Edge, update or add an additional property of `"browser": "chrome"` to the configuration.

The following example `.vscode/launch.json` file:

* Sets the current working directory to the Server folder.
* Sets the URL for the app to `http://localhost:7268`.
* Changes the default browser from Microsoft Edge to Google Chrome.

```
"cwd": "${workspaceFolder}/Server",
"url": "http://localhost:7268",
"browser": "chrome"
```

The complete `.vscode/launch.json` file:

```
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "blazorwasm",
      "name": "Launch and Debug Blazor WebAssembly Application",
      "request": "launch",
      "cwd": "${workspaceFolder}/Server",
      "url": "http://localhost:7268",
      "browser": "chrome"
    }
  ]
}
```

In `.vscode/tasks.json`, add a `build` argument that specifies the path to the **Server** app's project file:

```
"${workspaceFolder}/{SERVER APP FOLDER}/{PROJECT NAME}.csproj",
```

In the preceding argument:

* The `{SERVER APP FOLDER}` placeholder is the **Server** project's folder, typically Server.
* The `{PROJECT NAME}` placeholder is the app's name, typically based on the solution's name followed by `.Server` in an app generated from the Blazor WebAssembly project template.

An example `.vscode/tasks.json` file with a **Server** project named `BlazorHosted` in the Server folder of the solution:

```
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build",
      "command": "dotnet",
      "type": "process",
        "args": [
          "build",
          "${workspaceFolder}/Server/BlazorHosted.Server.csproj",
          "/property:GenerateFullPaths=true",
          "/consoleloggerparameters:NoSummary",
        ],
        "group": "build",
        "presentation": {
          "reveal": "silent"
        },
        "problemMatcher": "$msCompile"
    }
  ]
}
```

Note

Only [browser debugging](debug?view=aspnetcore-10.0#debug-in-the-browser) is supported at this time.

You can't automatically rebuild the backend **Server** app of a hosted Blazor WebAssembly solution during debugging, for example by running the app with [`dotnet watch run`](../tutorials/dotnet-watch?view=aspnetcore-10.0).

**`.vscode/launch.json`** (`launch` configuration):

```
...
"cwd": "${workspaceFolder}/{SERVER APP FOLDER}",
...
```

In the preceding configuration for the current working directory (`cwd`), the `{SERVER APP FOLDER}` placeholder is the **Server** project's folder, typically "Server".

If Microsoft Edge is used and Google Chrome isn't installed on the system, add an additional property of `"browser": "edge"` to the configuration.

Example for a project folder of Server and that spawns Microsoft Edge as the browser for debug runs instead of the default browser Google Chrome:

```
...
"cwd": "${workspaceFolder}/Server",
"browser": "edge"
...
```

**`.vscode/tasks.json`** ([`dotnet` command](/en-us/dotnet/core/tools/dotnet) arguments):

```
...
"${workspaceFolder}/{SERVER APP FOLDER}/{PROJECT NAME}.csproj",
...
```

In the preceding argument:

* The `{SERVER APP FOLDER}` placeholder is the **Server** project's folder, typically "Server".
* The `{PROJECT NAME}` placeholder is the app's name, typically based on the solution's name followed by "`.Server`" in an app generated from the [Blazor project template](project-structure?view=aspnetcore-10.0).

The following example from the [tutorial for using SignalR with a Blazor WebAssembly app](tutorials/signalr-blazor?view=aspnetcore-10.0) uses a project folder name of Server and a project name of `BlazorWebAssemblySignalRApp.Server`:

```
...
"args": [
  "build",
    "${workspaceFolder}/Server/BlazorWebAssemblySignalRApp.Server.csproj",
    ...
],
...
```

The .NET SDK is a set of libraries and tools that developers use to create .NET applications and libraries.

Install the [.NET SDK](https://dotnet.microsoft.com/download). Commands are issued in a command shell using the [.NET CLI](/en-us/dotnet/core/tools/).

If you previously installed one or more .NET SDKs and want to see your active version, execute the following command in a command shell:

```
dotnet --version
```

If you're new to the .NET SDK, see [What is the .NET SDK?](/en-us/dotnet/core/sdk) and the associated articles in the .NET SDK documentation.

Create a new project:

* Change to the directory using the `cd` command to where you want to create the project folder (for example, `cd c:/users/Bernie_Kopell/Documents`).

* For a Blazor Web App experience with default interactive server-side rendering (interactive SSR), execute the following command:

  ```
  dotnet new blazor -o BlazorApp
  ```

* For a standalone Blazor WebAssembly experience, execute the following command in a command shell that uses the `blazorwasm` template:

  ```
  dotnet new blazorwasm -o BlazorApp
  ```

Create a new project:

* Change to the directory using the `cd` command to where you want to create the project folder (for example, `cd c:/users/Bernie_Kopell/Documents`).

* For a Blazor Server experience with demonstration code and [Bootstrap](https://getbootstrap.com/), execute the following command:

  ```
  dotnet new blazorserver -o BlazorApp
  ```

* For a standalone Blazor WebAssembly experience with demonstration code and Bootstrap, execute the following command:

  ```
  dotnet new blazorwasm -o BlazorApp
  ```

* For a hosted Blazor WebAssembly experience with demonstration code and Bootstrap, add the hosted option (`-ho`/`--hosted`) to the command:

  ```
  dotnet new blazorwasm -o BlazorApp -ho
  ```

  Note

  The Hosted Blazor WebAssembly project template isn't available in .NET 8 or later. To create a hosted Blazor WebAssembly app using a .NET 8 or later SDK, pass the `-f|--framework` option with a .NET 7 target framework (`net7.0`):

  ```
  dotnet new blazorwasm -o BlazorApp -ho -f net7.0
  ```

Create a new project:

* Change to the directory using the `cd` command to where you want to create the project folder (for example, `cd c:/users/Bernie_Kopell/Documents`).

* For a Blazor WebAssembly experience, execute the following command:

  ```
  dotnet new blazorwasm -o BlazorApp
  ```

* For a hosted Blazor WebAssembly experience, add the hosted option (`-ho` or `--hosted`) option to the command:

  ```
  dotnet new blazorwasm -o BlazorApp -ho
  ```

  Note

  The hosted Blazor WebAssembly project template isn't available in .NET 8 or later. To create a hosted Blazor WebAssembly app using a .NET 8 or later SDK, pass the `-f|--framework` option with the target framework moniker (for example, `net6.0`):

  ```
  dotnet new blazorwasm -o BlazorApp -ho -f net6.0
  ```

* For a Blazor Server experience, execute the following command:

  ```
  dotnet new blazorserver -o BlazorApp
  ```

For more information on the templates and options, see the [Blazor project templates and template options](#blazor-project-templates-and-template-options) section.

## Run the app

Important

When executing a Blazor Web App, run the app from the solution's server project, which is the project with a name that doesn't end in `.Client`.

Important

When executing a hosted Blazor WebAssembly app, run the app from the solution's **Server** project.

Press `Ctrl`+`F5` on the keyboard to run the app without the debugger.

Visual Studio displays the following dialog when a project isn't configured to use SSL:

Select **Yes** if you trust the ASP.NET Core SSL certificate.

The following dialog is displayed:

Select **Yes** to acknowledge the risk and install the certificate.

Visual Studio:

* Compiles and runs the app.
* Launches the default browser at `https://localhost:{PORT}`, which displays the app's UI. The `{PORT}` placeholder is the random port assigned at app creation. If you need to change the port due to a local port conflict, change the port in the project's `Properties/launchSettings.json` file.

In VS Code, press `Ctrl`+`F5` to run the app without debugging.

At the **Select debugger** prompt in the **Command Palette** at the top of the VS Code UI, select **C#**. At the next prompt, select the HTTPS profile (`[https]`).

The default browser is launched at `https://localhost:{PORT}`, which displays the app's UI. The `{PORT}` placeholder is the random port assigned at app creation. If you need to change the port due to a local port conflict, change the port in the project's `Properties/launchSettings.json` file.

In a command shell opened to the project's root folder, execute the [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) command to compile and start the app:

```
dotnet watch
```

The default browser is launched at `https://localhost:{PORT}`, which displays the app's UI. The `{PORT}` placeholder is the random port assigned at app creation. If you need to change the port due to a local port conflict, change the port in the project's `Properties/launchSettings.json` file.

Note

When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

One approach for running the app with SSL/HTTPS is to pass the [`-lp`|`--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) with the `https` profile name to the `dotnet watch` command:

```
dotnet watch -lp https
```

An alternative approach is to move the `https` profile above the `http` profile in the `Properties/launchSettings.json` file and save the change. After changing the profile order in the file, the `dotnet watch` command always uses the `https` profile by default.

## Visual Studio solution file (`.sln`)

A *solution* is a container to organize one or more related code projects. Solution files use a unique format and aren't intended to be edited directly.

[Visual Studio](https://visualstudio.microsoft.com/vs/) and [Visual Studio Code (VS Code)](https://code.visualstudio.com) use a solution file (`.sln`) to store settings for a solution. The [.NET CLI](/en-us/dotnet/core/tools/) doesn't organize projects using a solution file, but it can create solution files and list/modify the projects in solution files via the [`dotnet sln` command](/en-us/dotnet/core/tools/dotnet-sln). Other .NET CLI commands use the path of the solution file for various publishing, testing, and packaging commands.

Throughout the Blazor documentation, *solution* is used to describe apps created from the Blazor WebAssembly project template with the **ASP.NET Core Hosted** option enabled or from a Blazor Hybrid project template. Apps produced from these project templates include a solution file (`.sln`). For hosted Blazor WebAssembly apps where the developer isn't using Visual Studio, the solution file can be ignored or deleted if it isn't used with .NET CLI commands.

For more information, see the following resources:

* [Introduction to projects and solutions (Visual Studio documentation)](/en-us/visualstudio/get-started/tutorial-projects-solutions)
* [What are solutions and projects in Visual Studio? (Visual Studio documentation)](/en-us/visualstudio/ide/solutions-and-projects-in-visual-studio)
* [Project management (VS Code documentation)](https://code.visualstudio.com/docs/csharp/project-management)

## Blazor project templates and template options

* Blazor Web App project template: `blazor`
* Standalone Blazor WebAssembly app project template: `blazorwasm`
* [Service defaults library for Blazor WebAssembly apps](#service-defaults-library-for-blazor-webassembly-apps): `blazor-wasm-servicedefaults`

Note

The "Hosted" Blazor WebAssembly project template option isn't available in .NET 8 or later. To create a hosted Blazor WebAssembly app, a **Framework** option earlier than .NET 8 must be selected with the **ASP.NET Core Hosted** checkbox. However, we recommend a Blazor Web App for all new Blazor development in .NET 8 or later. For more information, see the following resources:

* [ASP.NET Core Blazor](./?view=aspnetcore-10.0#build-a-full-stack-web-app-with-blazor)
* [What's new in ASP.NET Core in .NET 8](../release-notes/aspnetcore-8.0?view=aspnetcore-10.0#new-blazor-web-app-template)
* [ASP.NET Core Blazor project structure](project-structure?view=aspnetcore-10.0#blazor-web-app)
* [Migrate from ASP.NET Core in .NET 7 to .NET 8](../migration/70-to-80?view=aspnetcore-10.0#convert-a-hosted-blazor-webassembly-app-into-a-blazor-web-app)

The Blazor framework provides project templates for creating new apps. The templates are used to create new Blazor projects and solutions regardless of the tooling that you select for Blazor development (Visual Studio, Visual Studio Code, or the [.NET command-line interface (CLI)](/en-us/dotnet/core/tools/)):

* Blazor Web App project template: `blazor`
* Standalone Blazor WebAssembly app project template: `blazorwasm`

Note

The "Hosted" Blazor WebAssembly project template option isn't available in .NET 8 or later. To create a hosted Blazor WebAssembly app, a **Framework** option earlier than .NET 8 must be selected with the **ASP.NET Core Hosted** checkbox. However, we recommend a Blazor Web App for all new Blazor development in .NET 8 or later. For more information, see the following resources:

* [ASP.NET Core Blazor](./?view=aspnetcore-10.0#build-a-full-stack-web-app-with-blazor)
* [What's new in ASP.NET Core in .NET 8](../release-notes/aspnetcore-8.0?view=aspnetcore-10.0#new-blazor-web-app-template)
* [ASP.NET Core Blazor project structure](project-structure?view=aspnetcore-10.0#blazor-web-app)
* [Migrate from ASP.NET Core in .NET 7 to .NET 8](../migration/70-to-80?view=aspnetcore-10.0#convert-a-hosted-blazor-webassembly-app-into-a-blazor-web-app)

- Blazor Server project templates: `blazorserver`, `blazorserver-empty`
- Blazor WebAssembly project templates: `blazorwasm`, `blazorwasm-empty`

* Blazor Server project template: `blazorserver`
* Blazor WebAssembly project template: `blazorwasm`

For more information on Blazor project templates, see [ASP.NET Core Blazor project structure](project-structure?view=aspnetcore-10.0).

Rendering terms and concepts used in the following subsections are introduced in the following sections of the *Fundamentals* overview article:

* [Client and server rendering concepts](fundamentals/?view=aspnetcore-10.0#client-and-server-rendering-concepts)
* [Static and interactive rendering concepts](fundamentals/?view=aspnetcore-10.0#static-and-interactive-rendering-concepts)
* [Render modes](fundamentals/?view=aspnetcore-10.0#render-modes)

Detailed guidance on render modes is provided by the [ASP.NET Core Blazor render modes](components/render-modes?view=aspnetcore-10.0) article.

### Interactive render mode

* Interactive server-side rendering (interactive SSR) is enabled with the **Server** option.
* To only enable interactivity with client-side rendering (CSR), use the **WebAssembly** option.
* To enable both interactive rendering modes and the ability to automatically switch between them at runtime, use the **Auto (Server and WebAssembly)** (automatic) render mode option.
* If interactivity is set to `None`, the generated app has no interactivity. The app is only configured for static server-side rendering.

The Interactive Auto render mode initially uses interactive SSR while the .NET app bundle and runtime are downloaded to the browser. After the .NET WebAssembly runtime is activated, the render mode switches to Interactive WebAssembly rendering.

The Blazor Web App template enables both static and interactive SSR using a single project. If you also enable CSR, the project includes an additional client project (`.Client`) for your WebAssembly-based components. The built output from the client project is downloaded to the browser and executed on the client. Any components using the WebAssembly or automatic render modes must be built from the client project.

Important

When using a Blazor Web App, most of the Blazor documentation example components ***require*** interactivity to function and demonstrate the concepts covered by the articles. When you test an example component provided by an article, make sure that either the app adopts global interactivity or the component adopts an interactive render mode.

### Interactivity location

Interactivity location options:

* **Per page/component**: The default sets up interactivity per page or per component.
* **Global**: Using this option sets up interactivity globally for the entire app.

Interactivity location can only be set if **Interactive render mode** isn't `None` and authentication isn't enabled.

### Sample pages

To include sample pages and a layout based on Bootstrap styling, use the **Include sample pages** option. Disable this option for project without sample pages and Bootstrap styling.

### Additional guidance on template options

* [ASP.NET Core Blazor render modes](components/render-modes?view=aspnetcore-10.0)

* The *.NET default templates for dotnet new* article in the .NET documentation:

  * [`blazor`](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazor)
  * [`blazorwasm`](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazorwasm)

* Passing the help option (`-h` or `--help`) to the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) CLI command in a command shell:

  * `dotnet new blazor -h`
  * `dotnet new blazorwasm -h`

For more information on template options, see the following resources:

* The *.NET default templates for dotnet new* article in the .NET documentation:

  * [`blazorserver`](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazorserver) (includes `blazorserver-empty` options)
  * [`blazorwasm`](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazorwasm) (includes `blazorwasm-empty` options)

* Passing the help option (`-h` or `--help`) to the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) CLI command in a command shell:

  * `dotnet new blazorserver -h`
  * `dotnet new blazorserver-empty -h`
  * `dotnet new blazorwasm -h`
  * `dotnet new blazorwasm-empty -h`

For more information on template options, see the following resources:

* The *.NET default templates for dotnet new* article in the .NET documentation:

  * [`blazorserver`](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazorserver)
  * [`blazorwasm`](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazorwasm)

* Passing the help option (`-h` or `--help`) to the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) CLI command in a command shell:

  * `dotnet new blazorserver -h`
  * `dotnet new blazorwasm -h`

## Service defaults library for Blazor WebAssembly apps

The `blazor-wasm-servicedefaults` project template creates a service defaults library for Blazor WebAssembly apps with [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview) integration.

The template features:

* [OpenTelemetry (OTEL)](https://opentelemetry.io/) ([OpenTelemetry .NET](https://github.com/open-telemetry/opentelemetry-dotnet)) support: Logging, metrics, and tracing with standard OTEL Protocol (OTLP) exporter.
* Service discovery integration via the [`Microsoft.Extensions.ServiceDiscovery` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.ServiceDiscovery).
* HTTP resilience using the standard resilience handler via the [`Microsoft.Extensions.Http.Resilience` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Http.Resilience).

Example template usage with an output name of `BlazorSample.ServiceDefaults`:

```
dotnet new blazor-wasm-servicedefaults -o BlazorSample.ServiceDefaults
```

Reference the library from the Blazor WebAssembly client and make the following call in the app's `Program` file:

```
builder.AddBlazorClientServiceDefaults();
```

## Related content

* [Visual Studio](https://visualstudio.microsoft.com)
* [Visual Studio Code](https://code.visualstudio.com)
* [ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation](webassembly-build-tools-and-aot?view=aspnetcore-10.0)
* [.NET command-line interface (CLI)](/en-us/dotnet/core/tools/)
* [.NET SDK](/en-us/dotnet/core/sdk)
* [.NET Hot Reload support for ASP.NET Core](../test/hot-reload?view=aspnetcore-10.0)
* [ASP.NET Core Blazor hosting models](hosting-models?view=aspnetcore-10.0)
* [ASP.NET Core Blazor project structure](project-structure?view=aspnetcore-10.0)
* [ASP.NET Core Blazor Hybrid tutorials](hybrid/tutorials/?view=aspnetcore-10.0)

- [Visual Studio](https://visualstudio.microsoft.com)
- [Visual Studio Code](https://code.visualstudio.com)
- [.NET command-line interface (CLI)](/en-us/dotnet/core/tools/)
- [.NET SDK](/en-us/dotnet/core/sdk)
- [ASP.NET Core Blazor hosting models](hosting-models?view=aspnetcore-10.0)
- [ASP.NET Core Blazor project structure](project-structure?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/configuration/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configuration in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

App configuration in ASP.NET Core is performed using one or more [configuration providers](#configuration-providers). Configuration providers read configuration data from key-value pairs using a variety of configuration sources:

* Settings files, such as `appsettings.json`
* Environment variables, including Azure App configuration
* Azure Key Vault
* Command-line arguments
* Custom providers, installed or created
* In-memory .NET objects

This article provides information on configuration in ASP.NET Core. For information on using configuration in non-ASP.NET Core apps, see [.NET Configuration](/en-us/dotnet/core/extensions/configuration).

For additional Blazor configuration guidance, which adds to or supersedes the guidance here, see [ASP.NET Core Blazor configuration](../../blazor/fundamentals/configuration?view=aspnetcore-10.0).

This article primarily pertains to app configuration. Other types of configuration, such as launch settings files and the `web.config` file, are mentioned here, but their primary documentation is elsewhere:

* Launch settings files (`launch.json`/`launchSettings.json`), which are tooling configuration files for the `Development` environment. For more information, see [ASP.NET Core runtime environments](../environments?view=aspnetcore-10.0#development). Some details on launch settings are covered by this article in the [Launch settings override environment variable settings](#launch-settings-override-environment-variable-settings) section.
* `web.config`, which is a server configuration file for [Internet Information Services (IIS)](https://www.iis.net/). For more information, see [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0) and [ASP.NET Core Module (ANCM) for IIS](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0).

For more information on migrating app configuration from earlier versions of ASP.NET, see [Migrate configuration to ASP.NET Core](../../migration/fx-to-core/examples/configuration?view=aspnetcore-10.0).

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

Examples in this article use *primary constructors*, available in C# 12 (.NET 8) or later. For more information, see [Declare primary constructors for classes and structs (C# documentation tutorial)](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) and [Primary constructors (C# Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors).

## Service manager working directory configuration

Service managers, such as macOS `launchd` and Linux `systemd`, can set the app's working directory to a location outside of the directory that contains the app's configuration files. For example, a service manager on a Unix system might set the working directory to the system's root directory (`/`).

If the host uses a working directory broader than the app's directory as its content root, the file system watcher for configuration providers using reload-on-change behavior, where configuration is automatically reloaded when configuration files update, might monitor an excessively large portion of the file system. This can lead to unintended side effects, including permission errors when accessing files outside the app's directory and unnecessary performance overhead due to the volume of observed files. To avoid these issues, configure the service manager to use the directory containing the app and its configuration files as its working directory, or explicitly set that directory as the host's content root.

## Read configuration values

Configuration is typically read by resolving the [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) service ([Microsoft.Extensions.Configuration](/en-us/dotnet/api/microsoft.extensions.configuration) namespace) and using the key of configuration key-value pairs to obtain a configuration value.

The following Razor component code shows how a configuration value, a technical contact email address, is obtained from configuration by the key `TechnicalContactEmail`.

```
@inject IConfiguration Config

Technical Contact: @Config["TechnicalContactEmail"]
```

## App and host configuration

ASP.NET Core apps configure and launch a *host*. The host is responsible for app startup and lifetime management. Host configuration key-value pairs are included in the app's configuration. Although you can perform some app configuration with host configuration providers, we only recommend performing configuration that's necessary for the host in host configuration.

App configuration is the highest priority. For more information on how the configuration providers are used when the host is built and how configuration sources affect host configuration, see [ASP.NET Core fundamentals overview](../?view=aspnetcore-10.0#host).

## Default app configuration sources

ASP.NET Core web apps call [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) to initialize a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults:

```
var builder = WebApplication.CreateBuilder(args);
```

For more information, see [.NET Generic Host in ASP.NET Core](../host/generic-host?view=aspnetcore-10.0#default-builder-settings).

Apps created from an ASP.NET Core web app project template call [Host.CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder) to initialize a new instance of the [HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder) class with preconfigured defaults:

```
Host.CreateDefaultBuilder(args)
```

Default app configuration is loaded in the following order, from highest to lowest priority:

1. Command-line arguments using the [Command-line Configuration Provider](#command-line).
2. Environment variables ***not*** prefixed by `ASPNETCORE_` or `DOTNET_` using the [Environment Variables Configuration Provider](#environment-variables-configuration-provider).
3. [User secrets](../../security/app-secrets?view=aspnetcore-10.0) when the app runs in the `Development` environment using the [File Configuration Provider](#file-configuration-provider).
4. [Environmental app settings file configuration](#app-settings-file-configuration-appsettingsjson-appsettingsenvironmentjson) via `appsettings.{ENVIRONMENT}.json`, where the `{ENVIRONMENT}` placeholder is the app's [environment](../environments?view=aspnetcore-10.0), using the [JSON Configuration Provider](#json-configuration-provider). For example, `appsettings.Production.json` is used in production, and `appsettings.Development.json` is used during development.
5. [General app settings file configuration](#app-settings-file-configuration-appsettingsjson-appsettingsenvironmentjson) via `appsettings.json` using the [JSON Configuration Provider](#json-configuration-provider).
6. Fallback [host configuration](#default-host-configuration-sources).

Note

We don't recommend calling [CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) more than once solely for the purpose of obtaining configuration values at runtime. We recommend using a [ConfigurationManager](/en-us/dotnet/api/microsoft.extensions.configuration.configurationmanager) (for example: `builder.Configuration`, [WebApplicationBuilder.Configuration](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.configuration)) or using a [ConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbuilder) from the appropriate configuration source.

To permit command-line arguments to control settings such as the environment name, which is important for determining which environment-based app settings file to load, the [Command-line Configuration Provider](#command-line) is used twice as a configuration source, at the start and end of configuration. Because the provider is used at the end, it has the highest priority.

When a configuration value is set in host and app configuration, the app configuration is used.

## Default host configuration sources

Default host configuration sources from highest to lowest priority when applied to the web app's configuration ([WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder)):

1. Command-line arguments using the [Command-line Configuration Provider](#command-line).
2. `DOTNET_`-prefixed environment variables using the [Environment Variables Configuration Provider](/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariables.environmentvariablesconfigurationprovider).
3. `ASPNETCORE_`-prefixed environment variables using the [Environment Variables Configuration Provider](/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariables.environmentvariablesconfigurationprovider).

Default host configuration sources from highest to lowest priority applied to either the [Generic Host](../host/generic-host?view=aspnetcore-10.0) or [Web Host](../host/web-host?view=aspnetcore-10.0):

1. `ASPNETCORE_`-prefixed environment variables using the [Environment Variables Configuration Provider](/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariables.environmentvariablesconfigurationprovider).
2. Command-line arguments using the [Command-line Configuration Provider](#command-line).
3. `DOTNET_`-prefixed environment variables using the [Environment Variables Configuration Provider](/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariables.environmentvariablesconfigurationprovider).

For more information on host configuration, see the following resources:

* [Generic Host](../host/generic-host?view=aspnetcore-10.0): Recommended for ASP.NET Core apps targeting .NET 6 or later that adopt the [minimal hosting model](../../migration/50-to-60?view=aspnetcore-10.0#new-hosting-model).
* [Web Host](../host/web-host?view=aspnetcore-10.0): Required for ASP.NET Core apps that target releases prior to .NET 6 and only maintained by the framework for backward compatibility in .NET 6 or later.

Default host configuration sources from highest to lowest priority for the [Web Host](../host/web-host?view=aspnetcore-10.0):

* Host configuration

  * Environment variables prefixed with `DOTNET_` (for example, `DOTNET_ENVIRONMENT`) using the [Environment Variables Configuration Provider](/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariables.environmentvariablesconfigurationprovider). The prefix (`DOTNET_`) is stripped when the configuration key-value pairs are loaded.
  * Command-line arguments using the [Command-line Configuration Provider](#command-line).

* Web Host default configuration ([ConfigureWebHostDefaults](/en-us/dotnet/api/microsoft.extensions.hosting.generichostbuilderextensions.configurewebhostdefaults))

  * Kestrel is used as the web server and configured using the app's configuration providers.
  * Add [host-filtering middleware](../servers/kestrel/host-filtering?view=aspnetcore-10.0).
  * Add [forwarded headers middleware](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0#forwarded-headers) if the `ASPNETCORE_FORWARDEDHEADERS_ENABLED` environment variable is set to `true`.
  * Enable IIS integration.

## Host variables

The following variables are set early in host builder initialization and can't be influenced by app configuration:

* [Application name](../minimal-apis?view=aspnetcore-10.0#change-the-content-root-application-name-and-environment).
* [Environment name](../environments?view=aspnetcore-10.0).
* [Content root](../?view=aspnetcore-10.0#content-root).
* [Web root](../?view=aspnetcore-10.0#web-root).
* Whether to scan for [hosting startup assemblies](../host/platform-specific-configuration?view=aspnetcore-10.0) and which assemblies to scan for.
* Variables read by app and library code from [HostBuilderContext.Configuration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuildercontext.configuration) in [IHostBuilder.ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.configureappconfiguration) callbacks.

Other host settings are read from app configuration instead of host configuration.

`URLS` is one of the many common host settings that isn't bootstrapped by host configuration. `URLS` is read later from app configuration. Host configuration is a fallback for app configuration, so host configuration can be used to set `URLS`, but the value is overridden by any configuration source that sets `URLS` in app configuration, such as app settings files (`appsettings.{ENVIRONMENT}.json`, where the `{ENVIRONMENT}` placeholder is the environment name, or `appsettings.json`).

For more information, see [Change the content root, app name, and environment](../../migration/50-to-60-samples?view=aspnetcore-10.0#change-the-content-root-app-name-and-environment) and [Change the content root, app name, and environment by environment variables or command line](../../migration/50-to-60-samples?view=aspnetcore-10.0#change-the-content-root-app-name-and-environment-by-environment-variables-or-command-line).

## Security and user secrets

Configuration data guidelines:

* Never store passwords or other sensitive data in configuration provider code or in plain text configuration files. The [Secret Manager](../../security/app-secrets?view=aspnetcore-10.0) tool can be used to store secrets in development.
* Don't use production secrets in development or test environments.
* Specify secrets outside of the project so that they can't be accidentally committed to a source code repository.
* Production apps should use the most secure authentication flow available. For more information, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

The User Secrets file configuration source of the [default configuration sources](#default-app-configuration-sources) is registered after the JSON configuration sources for app settings files. Therefore, user secrets keys take precedence over keys in `appsettings.json` and `appsettings.{ENVIRONMENT}.json`.

For more information on storing passwords or other sensitive data:

* [ASP.NET Core runtime environments](../environments?view=aspnetcore-10.0)
* [Safe storage of app secrets in development](../../security/app-secrets?view=aspnetcore-10.0): Includes advice on using environment variables to store sensitive data. The Secret Manager tool uses the [File Configuration Provider](#file-configuration-provider) to store user secrets in a JSON file on the local system.
* [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) safely stores app secrets for ASP.NET Core apps. For more information, see [Azure Key Vault configuration provider](../../security/key-vault-configuration?view=aspnetcore-10.0).

## Access configuration with Dependency Injection (DI)

Configuration can be injected into services using [Dependency Injection (DI)](../dependency-injection?view=aspnetcore-10.0) by resolving the [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) service. In the following example, the configuration value stored for the configuration key represented by the `{KEY}` placeholder is assigned to `value`. If the key isn't found, `null` is assigned to `value`:

```
public class CustomService(IConfiguration config)
{
    public void CustomMethod()
    {
        var value = config["{KEY}"];
    }
}
```

## Access configuration in the `Program` file

The following code accesses configuration in the `Program` file using [WebApplicationBuilder.Configuration](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.configuration) (`builder.Configuration`):

```
var defaultConnectionString = 
   builder.Configuration.GetValue<string>("ConnectionStrings:DefaultConnection");
```

After the app is built (after the line `var app = builder.Build();`), use [WebApplication.Configuration](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.configuration) (`app.Configuration`):

```
var defaultLogLevel = app.Configuration.GetValue<string>("Logging:LogLevel:Default");
```

## Access configuration in the `Startup` class

*This section generally applies to ASP.NET Core apps prior to the release of .NET 6.*

The following code displays configuration data in `Startup` methods:

```
public class Startup
{
    public Startup(IConfiguration config)
    {
        Config = config;
    }

    public IConfiguration Config { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var connectionString = Config["ConnectionStrings.DefaultConnection"];

        ...
    }

    public void Configure(...)
    {
        var defaultLogLevel = Config["Logging:LogLevel:Default"];

        ...
    }
}
```

## Display configuration settings on startup for debugging

The following code displays the app's configuration key-value pairs at app startup.

After the app is built in the `Program` file (after the line `var app = builder.Build();`), place the following code, which includes a compiler directive for the DEBUG configuration:

```
#if DEBUG
foreach (var c in app.Configuration.AsEnumerable())
{
    Console.WriteLine($"CONFIG: Key: {c.Key} Value: {c.Value}");
}
#endif
```

In the app's `Startup` class constructor, inject [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration), `config` in the following example, to write the configuration key-value pairs to the console. The following example includes a compiler directive for the DEBUG configuration:

```
#if DEBUG
foreach (var c in config.AsEnumerable())
{
    Console.WriteLine($"CONFIG: Key: {c.Key} Value: {c.Value}");
}
#endif
```

## Configuration keys and values

Configuration keys:

* Are case-insensitive. For example, `ConnectionString` and `connectionstring` are treated as equivalent keys.

* If a key and value is set by more than one configuration provider, the value from the last provider added is used. For more information, see [Default configuration](#default-app-configuration-sources).

* Hierarchical keys

  * Within the Configuration API, a colon separator (`:`) works on all platforms.
  * In environment variables, a colon separator doesn't work on all platforms. A double underscore (`__`) is supported by all platforms and is automatically converted into a colon (`:`) when the configuration is read by the app.
  * In Azure Key Vault, hierarchical keys use double dashes (`--`) as a separator. The [Azure Key Vault Configuration Provider](../../security/key-vault-configuration?view=aspnetcore-10.0) automatically replaces the double dashes (`--`) with a colon (`:`) when the secrets are loaded into the app's configuration.

* The [ConfigurationBinder](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder) supports binding arrays to objects using array indices in configuration keys. Array binding is described in the [Bind an array](#bind-an-array) section.

Configuration values are strings. Null values can't be stored in configuration or bound to objects.

## How hierarchical configuration data is organized

The Configuration API reads hierarchical configuration data by flattening the hierarchical data with the use of a delimiter in the configuration keys, which are usually colons (`:`). Double underscores (`__`) are usually used with environment variable configuration for cross-platform support.

Note

In complex app configuration scenarios, it's best to group and read related hierarchical configuration data using the [options pattern](options?view=aspnetcore-10.0).

Consider the following hierarchical configuration data:

* ConnectionStrings
  * DefaultConnection (Value = 'Data Source=LocalSqlServer\MSSQLDev;')

* Logging

  * LogLevel

    * Default (Value = 'Information')
    * Microsoft (Value = 'Warning')
    * Microsoft.Hosting.Lifetime (Value = 'Information')

* AllowedHosts (Value = '\*')

The following table displays the keys used to recover the values in the preceding configuration data. The delimiter isn't required for AllowedHosts.

| Key (colon delimiter)                         | Key (double-underscore delimiter)               |
| --------------------------------------------- | ----------------------------------------------- |
| `ConnectionStrings:DefaultConnection`         | `ConnectionStrings__DefaultConnection`          |
| `Logging:LogLevel:Default`                    | `Logging__LogLevel__Default`                    |
| `Logging:LogLevel:Microsoft`                  | `Logging__LogLevel__Microsoft`                  |
| `Logging:LogLevel:Microsoft.Hosting.Lifetime` | `Logging__LogLevel__Microsoft.Hosting.Lifetime` |
| `AllowedHosts`                                | `AllowedHosts`                                  |

Note

In complex app configuration scenarios, we recommend grouping and reading related hierarchical configuration data using the [Options pattern](options?view=aspnetcore-10.0).

[GetSection](/en-us/dotnet/api/microsoft.extensions.configuration.configurationsection.getsection) and [GetChildren](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getchildren) methods are available to isolate sections and children of a section in the configuration data. These methods are described where [`GetSection`, `GetChildren`, and `Exists` are covered](#work-with-sections-get-a-sections-children-and-determine-if-a-section-exists).

When the element structure includes an array, the array index should be treated as an additional element name in the path. Consider the following hierarchical configuration data as an array.

MainObject (an array):

* First item in the array

  * Object0

  * Object1

    * SubObject0
    * SubObject1

* Second item in the array

  * Object0

  * Object1

    * SubObject0
    * SubObject1

Keys with colon separators:

* MainObject:0:Object0
* MainObject:0:Object1:SubObject0
* MainObject:0:Object1:SubObject1
* MainObject:1:Object0
* MainObject:1:Object1:SubObject0
* MainObject:1:Object1:SubObject1

Keys with underscore separators, which is recommended for cross-platform compatibility when configuration is provided by environment variables:

* MainObject\_\_0\_\_Object0
* MainObject\_\_0\_\_Object1:SubObject0
* MainObject\_\_0\_\_Object1:SubObject1
* MainObject\_\_1\_\_Object0
* MainObject\_\_1\_\_Object1:SubObject0
* MainObject\_\_1\_\_Object1:SubObject1

Because configuration that comes from arrays is flattened and numbered sequentially for each configuration source that an app uses, values can be unexpectedly overwritten if care isn't taken when structuring and reading the data from multiple sources. Consider the following configuration key-value pairs:

Modules values (an array):

* Module1
* Module2
* Module3

The array is flattened and indexed sequentially yielding the configuration key-value pairs in the following table.

| Key         | Value     |
| ----------- | --------- |
| `Modules:0` | `Module1` |
| `Modules:1` | `Module2` |
| `Modules:2` | `Module3` |

After the preceding configuration is established, another configuration source loads the following configuration:

Modules values (an array):

* Module4
* Module5

This array is also flattened and indexed sequentially.

| Key         | Value     |
| ----------- | --------- |
| `Modules:0` | `Module4` |
| `Modules:1` | `Module5` |

Recalling that the last configuration source for a given key sets the value of that key, the final set of configuration key-value pairs are shown in the following table.

| Key         | Value     |
| ----------- | --------- |
| `Modules:0` | `Module4` |
| `Modules:1` | `Module5` |
| `Modules:2` | `Module3` |

This isn't a surprising result given how the framework flattens and indexes array data from configuration sources, but it should be kept in mind to avoid unexpected overwrites.

To avoid such overwrites, structure array indexing to match across various configuration sources that provide the same array data. Alternatively, a workaround approach is to delimit array values in a string value of a single key-value pair, for example using a comma, semicolon, or pipe as the delimiter. Write custom code to split the string and assign the delimited values to your array.

## Configuration providers

The following table shows the configuration providers available to ASP.NET Core apps.

| Provider                                                                                              | Provides configuration from…       |
| ----------------------------------------------------------------------------------------------------- | ---------------------------------- |
| [Azure Key Vault Configuration Provider](../../security/key-vault-configuration?view=aspnetcore-10.0) | Azure Key Vault                    |
| [Azure App Configuration Provider](/en-us/azure/azure-app-configuration/quickstart-aspnet-core-app)   | Azure App Configuration            |
| [Command-line Configuration Provider](#command-line)                                                  | Command-line parameters            |
| [Custom configuration provider](#custom-configuration-provider)                                       | Custom source                      |
| [Environment Variables Configuration Provider](#environment-variables-configuration-provider)         | Environment variables              |
| [File Configuration Provider](#file-configuration-provider)                                           | INI, JSON, and XML files           |
| [Key-Per-File Configuration Provider](#key-per-file-configuration-provider)                           | Directory files                    |
| [Memory Configuration Provider](#memory-configuration-provider)                                       | In-memory collections              |
| [User secrets](../../security/app-secrets?view=aspnetcore-10.0)                                       | File in the user profile directory |

Configuration sources are read in the order that their configuration providers are specified. Order configuration providers in code to suit the priorities for the underlying configuration sources that the app requires.

A typical sequence of configuration providers is:

1. General app settings via `appsettings.json`.
2. Environmental app settings via `appsettings.{ENVIRONMENT}.json`, where the `{ENVIRONMENT}` placeholder is the app's environment (examples: `Development`, `Production`).
3. [User secrets](../../security/app-secrets?view=aspnetcore-10.0).
4. Environment variables using the [Environment Variables Configuration Provider](#environment-variables-configuration-provider).
5. Command-line arguments using the [Command-line Configuration Provider](#command-line).

A common practice is to add the Command-line Configuration Provider last in a series of providers to allow command-line arguments to override configuration set by the other providers.

The preceding sequence of providers is used in the [default configuration](#default-app-configuration-sources).

To inspect the app's configuration providers, [inject](../dependency-injection?view=aspnetcore-10.0) [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration), cast it to [IConfigurationRoot](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationroot), and read the [Providers](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationroot.providers) property.

In the following `ConfigurationProviders` Razor component displays the enabled configuration providers in the order that they're added to the app.

`Pages/ConfigurationProviders.razor`:

```
@page "/configuration-providers"
@inject IConfiguration Config

<h1>Configuration Providers</h1>

@if (ConfigRoot is not null)
{
    <ul>
        @foreach (var provider in ConfigRoot.Providers)
        {
            <li>@provider</li>
        }
    </ul>
}

@code {
    private IConfigurationRoot? ConfigRoot;

    protected override void OnInitialized()
    {
        ConfigRoot = (IConfigurationRoot)Config;
    }
}
```

The preceding Razor component produces the following output, where the `{APP NAMESPACE}` placeholder is the app's namespace:

> MemoryConfigurationProvider\
> EnvironmentVariablesConfigurationProvider Prefix: 'ASPNETCORE\_'\
> MemoryConfigurationProvider\
> EnvironmentVariablesConfigurationProvider Prefix: 'DOTNET\_'\
> JsonConfigurationProvider for 'appsettings.json' (Optional)\
> JsonConfigurationProvider for 'appsettings.Development.json' (Optional)\
> JsonConfigurationProvider for '{APP NAMESPACE}.settings.json' (Optional)\
> JsonConfigurationProvider for '{APP NAMESPACE}.settings.Development.json' (Optional)\
> EnvironmentVariablesConfigurationProvider\
> Microsoft.Extensions.Configuration.ChainedConfigurationProvider

In the [Default app configuration sources](#default-app-configuration-sources) section earlier in this article, configuration sources are listed from highest to lowest priority. The preceding `ConfigurationProviders` component displays the sources *in the order that the app reads them*. For example, the [JSON Configuration Provider](#json-configuration-provider) for the non-environmental app settings file (`appsettings.json`) is earlier in the preceding list because it's added before the provider for the Development environment app settings file (`appsettings.Development.json`). The configuration providers are executed from the top of the list to the bottom of the list. For a matching configuration key between the two app settings JSON Configuration Providers, the last setting takes precedence, which is the value from `appsettings.Development.json`.

## App settings file configuration (`appsettings.json`, `appsettings.{ENVIRONMENT}.json`)

Read configuration loaded from app settings files using the [JSON Configuration Provider](#json-configuration-provider).

Consider the following `appsettings.json` file:

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=LocalSqlServer\\MSSQLDev;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

[Inject](../dependency-injection?view=aspnetcore-10.0) an instance of [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) to read configuration values.

The following `AppSettingsConfiguration` Razor component reads the default database connection string and default logging level configuration. [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) is injected at the top of the component and used to read configuration values. A colon (`:`) separator is used in string-typed configuration keys to locate the appropriate JSON properties. For example, the JSON structure for the default connection string (`DefaultConnection`) object is nested under the connection strings (`ConnectionStrings`) object, so the string notation to access the default connection string uses a colon to separate `DefaultConnection` and `ConnectionStrings` in the order that the objects appear in the app settings file: `ConnectionStrings:DefaultConnection`.

`Pages/AppSettingsConfiguration.razor`:

```
@page "/app-settings-configuration"
@inject IConfiguration Config

<h1>App Settings Configuration</h1>

<ul>
    <li>Default Connection String: @Config["ConnectionStrings:DefaultConnection"]
    <li>Default Log Level: @Config["Logging:LogLevel:Default"]
</ul>
```

The same approach is taken in a Razor Pages page model:

```
using Microsoft.Extensions.Configuration;

...

public class AppSettingsPageModel(IConfiguration config) : PageModel
{
    public ContentResult OnGet()
    {
        var defaultConnectionString = config["ConnectionStrings:DefaultConnection"];
        var defaultLogLevel = config["Logging:LogLevel:Default"];

        return Content(
            $"Default Connection String: {defaultConnectionString}\n" +
            $"Default Log Level: {defaultLogLevel}");
    }
}
```

The default [JsonConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.json.jsonconfigurationprovider) instances load configuration in the following order:

1. `appsettings.json`
2. `appsettings.{ENVIRONMENT}.json`, where the `{ENVIRONMENT}` placeholder is the app's [environment](../environments?view=aspnetcore-10.0) (examples: `appsettings.Production.json`, `appsettings.Development.json`). The environment version of the file is loaded based on the [IHostingEnvironment.EnvironmentName](/en-us/dotnet/api/microsoft.extensions.hosting.ihostingenvironment.environmentname).

`appsettings.{ENVIRONMENT}.json` values override keys in `appsettings.json`.

By default:

* In the Development environment, `appsettings.Development.json` configuration overwrites values found in `appsettings.json`.
* In the Production environment, `appsettings.Production.json` configuration overwrites values found in `appsettings.json`.

The preceding example only reads strings and doesn't support a default value. If a configuration value must be guaranteed with a default value, see the [Extract a single value from configuration with type conversion (`GetValue`)](#extract-a-single-value-from-configuration-with-type-conversion-getvalue) section.

Using the [default app configuration sources](#default-app-configuration-sources), the `appsettings.json` and `appsettings.{ENVIRONMENT}.json` files are enabled with [`reloadOnChange` set to `true`](#json-configuration-provider), which means that changes made to either the `appsettings.json` or `appsettings.{ENVIRONMENT}.json` files after the app starts take effect immediately after the file is saved.

Comments in `appsettings.json` and `appsettings.{ENVIRONMENT}.json` files are supported using JavaScript or [C# style comments](/en-us/dotnet/csharp/language-reference/tokens/comments). Some integrated development environments (IDEs) display errors when editing a JSON file that contains comments because the [official JSON specification (RFC 7159)](https://datatracker.ietf.org/doc/html/rfc7159) makes no allowance for comments in JSON files. You can generally ignore comment errors and warnings, but you can also usually disable warnings or errors with a setting in the IDE. In Visual Studio Code, for example, add the following to the `settings.json` file to disable the errors:

```
"files.associations": {
  "appsettings*.json": "jsonc"
}
```

The preceding setting indicates to VS Code that app settings files, including environmental-based files, are associated with the [JSONC ("JSON with Comments") file format](https://jsonc.org/), which supports comments.

For other IDEs, check the IDE's documentation and product support channels to determine how to silence errors or warnings about comments in JSON files.

## Environment Variables Configuration Provider

The [default Environment Variables Configuration Provider](#default-app-configuration-sources) ([EnvironmentVariablesConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariables.environmentvariablesconfigurationprovider)) loads configuration from environment variables that aren't prefixed with `ASPNETCORE_` or `DOTNET_`. For more information on `ASPNETCORE_` and `DOTNET_` environment variables, see the [Default host configuration sources](#default-host-configuration-sources) section and [`DOTNET_` environment variables (.NET Core documentation)](/en-us/dotnet/core/tools/dotnet-environment-variables).

Using the default Environment Variables Configuration Provider, the app loads configuration from environment variable key-value pairs after reading `appsettings.json`, `appsettings.{ENVIRONMENT}.json`, and [user secrets](../../security/app-secrets?view=aspnetcore-10.0). Therefore, key values read from the environment override values read from `appsettings.json`, `appsettings.{ENVIRONMENT}.json`, and user secrets.

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For example, [Bash](https://linuxhint.com/bash-environment-variables/) doesn't support colon (`:`) as a separator. All platforms support the double underscore (`__`) syntax and automatically replace it with a colon (`:`).

For guidance on setting environment variables in a command shell on Windows or in a cross-platform PowerShell command shell, see the following resources:

* [set (environment variable)](/en-us/windows-server/administration/windows-commands/set_1)
* [setx](/en-us/windows-server/administration/windows-commands/setx)
* [about\_Environment\_Variables (PowerShell documentation)](/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables)

### Custom prefix for environment variables

You can add a configuration provider for custom-prefixed environment variables. In the `Program` file, call [AddEnvironmentVariables](/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariablesextensions.addenvironmentvariables) with a string to specify a prefix after [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) is called. The provider is added after the [default configuration providers](#default-app-configuration-sources), thus the added provider has a higher priority, including over environment variables of the same name without the prefix.

In the following example, environment variables are added with the `CustomPrefix_` prefix:

```
builder.Configuration.AddEnvironmentVariables(prefix: "CustomPrefix_");
```

You can add a configuration provider for custom-prefixed environment variables. In the `Program` file, call [AddEnvironmentVariables](/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariablesextensions.addenvironmentvariables) with a string to specify a prefix on the [IConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationbuilder) of [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration). The provider is added after the [default configuration providers](#default-app-configuration-sources), thus the added provider has a higher priority, including over environment variables of the same name without the prefix.

In the following example, environment variables are added with the `CustomPrefix_` prefix:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
    .ConfigureAppConfiguration(config =>
    { 
        config.AddEnvironmentVariables("CustomPrefix_");
    });
```

The prefix is stripped off when the configuration key-value pairs are read.

### Launch settings override environment variable settings

Environment variables set in `launchSettings.json` override those set in the system environment. For example, the ASP.NET Core web templates generate a `launchSettings.json` file that sets the endpoint configuration to:

```
"applicationUrl": "https://localhost:5001;http://localhost:5000"
```

Configuring the `applicationUrl` sets the `ASPNETCORE_URLS` environment variable and overrides values set in the environment.

### Escape environment variables on Linux

On Linux, the value of URL environment variables must be escaped so `systemd` can parse it. In the following example, the Linux tool `systemd-escape` is used to yield `http:--localhost:5001` from `http://localhost:5001`:

```
groot@terminus:~$ systemd-escape http://localhost:5001
http:--localhost:5001
```

### Azure App Service app settings (environment variables)

For [Azure App Service](https://azure.microsoft.com/services/app-service/) app settings (environment variable) guidance, see the following resources:

* [Configure an App Service app (Azure documentation)](/en-us/azure/app-service/configure-common)
* [Azure database connection string prefixes](#connection-string-prefixes)

### Connection string prefixes

The Configuration API has special processing rules for connection string environment variables. These connection strings are involved in configuring Azure connection strings for the app environment. Environment variables with the prefixes shown in the following table are loaded into the app with the [default configuration](#default-app-configuration-sources) or when no prefix is supplied to [AddEnvironmentVariables](/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariablesextensions.addenvironmentvariables).

| Connection string prefix | Provider                                                                 |
| ------------------------ | ------------------------------------------------------------------------ |
| `CUSTOMCONNSTR_`         | Custom provider                                                          |
| `MYSQLCONNSTR_`          | [MySQL](https://www.mysql.com/)                                          |
| `SQLAZURECONNSTR_`       | [Azure SQL Database](https://azure.microsoft.com/services/sql-database/) |
| `SQLCONNSTR_`            | [SQL Server](https://www.microsoft.com/sql-server/)                      |

When an environment variable is discovered and loaded into configuration with any of the four prefixes shown in the preceding table:

* The configuration key is created by removing the environment variable prefix and adding a configuration key section (`ConnectionStrings`).
* A new configuration key-value pair is created that represents the database connection provider, except for `CUSTOMCONNSTR_`, which has no stated provider.

| Environment variable key | Converted configuration key | Provider configuration entry                                                 |
| ------------------------ | --------------------------- | ---------------------------------------------------------------------------- |
| `CUSTOMCONNSTR_{KEY}`    | `ConnectionStrings:{KEY}`   | Configuration entry not created.                                             |
| `MYSQLCONNSTR_{KEY}`     | `ConnectionStrings:{KEY}`   | Key: `ConnectionStrings:{KEY}_ProviderName`: Value: `MySql.Data.MySqlClient` |
| `SQLAZURECONNSTR_{KEY}`  | `ConnectionStrings:{KEY}`   | Key: `ConnectionStrings:{KEY}_ProviderName`: Value: `System.Data.SqlClient`  |
| `SQLCONNSTR_{KEY}`       | `ConnectionStrings:{KEY}`   | Key: `ConnectionStrings:{KEY}_ProviderName`: Value: `System.Data.SqlClient`  |

| Connection string prefix  | Provider                                                                           |
| ------------------------- | ---------------------------------------------------------------------------------- |
| `APIHUBCONNSTR_`          | Azure API hubs                                                                     |
| `CUSTOMCONNSTR_`          | Custom provider                                                                    |
| `DOCDBCONNSTR_`           | [Azure Cosmos DB](https://azure.microsoft.com/services/cosmos-db/)                 |
| `EVENTHUBCONNSTR_`        | [Azure Event Hubs](https://azure.microsoft.com/services/event-hubs/)               |
| `MYSQLCONNSTR_`           | [MySQL](https://www.mysql.com/)                                                    |
| `NOTIFICATIONHUBCONNSTR_` | [Azure Notification Hubs](https://azure.microsoft.com/services/notification-hubs/) |
| `POSTGRESQLCONNSTR_`      | [PostgreSQL](https://www.postgresql.org/)                                          |
| `REDISCACHECONNSTR_`      | [Azure Cache for Redis](https://azure.microsoft.com/services/cache/)               |
| `SERVICEBUSCONNSTR_`      | [Azure Service Bus](https://azure.microsoft.com/services/service-bus/)             |
| `SQLAZURECONNSTR_`        | [Azure SQL Database](https://azure.microsoft.com/services/sql-database/)           |
| `SQLCONNSTR_`             | [SQL Server](https://www.microsoft.com/sql-server/)                                |

When an environment variable is discovered and loaded into configuration with any of the recognized prefixes:

* The configuration key is created by removing the environment variable prefix and adding a configuration key section (`ConnectionStrings`).
* A new configuration key-value pair is created that represents the database connection provider when a provider name is associated with the prefix.

| Environment variable key       | Converted configuration key | Provider configuration entry                                                 |
| ------------------------------ | --------------------------- | ---------------------------------------------------------------------------- |
| `APIHUBCONNSTR_{KEY}`          | `ConnectionStrings:{KEY}`   | Configuration entry not created.                                             |
| `CUSTOMCONNSTR_{KEY}`          | `ConnectionStrings:{KEY}`   | Configuration entry not created.                                             |
| `DOCDBCONNSTR_{KEY}`           | `ConnectionStrings:{KEY}`   | Configuration entry not created.                                             |
| `EVENTHUBCONNSTR_{KEY}`        | `ConnectionStrings:{KEY}`   | Configuration entry not created.                                             |
| `MYSQLCONNSTR_{KEY}`           | `ConnectionStrings:{KEY}`   | Key: `ConnectionStrings:{KEY}_ProviderName`: Value: `MySql.Data.MySqlClient` |
| `NOTIFICATIONHUBCONNSTR_{KEY}` | `ConnectionStrings:{KEY}`   | Configuration entry not created.                                             |
| `POSTGRESQLCONNSTR_{KEY}`      | `ConnectionStrings:{KEY}`   | Key: `ConnectionStrings:{KEY}_ProviderName`: Value: `Npgsql`                 |
| `REDISCACHECONNSTR_{KEY}`      | `ConnectionStrings:{KEY}`   | Configuration entry not created.                                             |
| `SERVICEBUSCONNSTR_{KEY}`      | `ConnectionStrings:{KEY}`   | Configuration entry not created.                                             |
| `SQLAZURECONNSTR_{KEY}`        | `ConnectionStrings:{KEY}`   | Key: `ConnectionStrings:{KEY}_ProviderName`: Value: `System.Data.SqlClient`  |
| `SQLCONNSTR_{KEY}`             | `ConnectionStrings:{KEY}`   | Key: `ConnectionStrings:{KEY}_ProviderName`: Value: `System.Data.SqlClient`  |

## Command-line

Using the [default configuration sources](#default-app-configuration-sources), the [CommandLineConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.commandline.commandlineconfigurationprovider) loads configuration from command-line argument key-value pairs after the following configuration sources:

* `appsettings.json` and `appsettings.{ENVIRONMENT}.json` files.
* [App secrets](../../security/app-secrets?view=aspnetcore-10.0) in the `Development` environment.
* Environment variables.

By default, configuration values set on the command-line override configuration values set by all of the other configuration providers.

### Command-line arguments

The following [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) command sets keys and values using equals signs (`=`):

```
dotnet run ConnectionStrings:DefaultConnection="Data Source=LocalSqlServer\\MSSQLDev;" Logging:LogLevel:Default=Information
```

The following command sets keys and values using forward slashes (`/`):

```
dotnet run /ConnectionStrings:DefaultConnection "Data Source=LocalSqlServer\\MSSQLDev;" /Logging:LogLevel:Default Information
```

The following command sets keys and values using double dashes (`--`):

```
dotnet run --ConnectionStrings:DefaultConnection "Data Source=LocalSqlServer\\MSSQLDev;" --Logging:LogLevel:Default Information
```

Argument conventions:

* The key value must follow the equals sign (`=`), or the key must have a prefix of a double dash (`--`) or forward slash (`/`) when the value follows a space.
* Not assigning a value after an equals sign (`=`) results in assigning an empty string for the configuration setting. For example, specifying `ConnectionStrings:DefaultConnection=` is valid and results in assigning an empty string to the default connection string.
* Within the same command, don't mix argument key-value pairs separated by an equals sign (`=`) with key-value pairs separated by a space.

### Switch mappings

Switch mappings allow *key* name replacement logic via a dictionary of switch replacements passed to the [AddCommandLine](/en-us/dotnet/api/microsoft.extensions.configuration.commandlineconfigurationextensions.addcommandline) method.

When the switch mappings dictionary is used, the dictionary is checked for a key that matches the key provided by a command-line argument. If the command-line key is found in the dictionary, the dictionary value is passed back to set the key-value pair into the app's configuration. A switch mapping is required for any command-line key prefixed with a single dash (`-`).

Switch mappings dictionary key rules:

* Switches must start with a single dash (`-`) or double dash (`--`).
* The switch mappings dictionary must not contain duplicate keys.

In the following example, a switch mapping dictionary (`switchMappings`) is passed to [AddCommandLine](/en-us/dotnet/api/microsoft.extensions.configuration.commandlineconfigurationextensions.addcommandline) in the app's `Program` file:

```
var switchMappings = 
    new Dictionary<string, string>(){ { "-k1", "key1" }, { "-k2", "key2" } };

builder.Configuration.AddCommandLine(args, switchMappings);
```

```
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    })
.ConfigureAppConfiguration(config =>
{ 
    var switchMappings =
        new Dictionary<string, string>() { { "-k1", "key1" }, { "-k2", "key2" } };

    config.AddCommandLine(args, switchMappings);
});
```

The following [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) commands demonstrate key replacement (`value1` into `key1` and `value2` into `key2`):

* `dotnet run -k1 value1 -k2 value2`
* `dotnet run --k1=value1 --k2=value2`
* `dotnet run --k1 value1 --k2 value2`
* `dotnet run /k1=value1 /k2=value2`
* `dotnet run /k1 value1 /k2 value2`

For apps that use switch mappings, the call to [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder) shouldn't pass arguments. The [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder) method's [AddCommandLine](/en-us/dotnet/api/microsoft.extensions.configuration.commandlineconfigurationextensions.addcommandline) call doesn't include mapped switches, and there's no way to pass the switch-mapping dictionary to [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder). The solution isn't to pass the arguments to [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder) but instead to allow the [ConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbuilder) method's [AddCommandLine](/en-us/dotnet/api/microsoft.extensions.configuration.commandlineconfigurationextensions.addcommandline) method to process both the arguments and the switch-mapping dictionary.

For apps that use switch mappings, the call to [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder) shouldn't pass arguments. The [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder) method's [AddCommandLine](/en-us/dotnet/api/microsoft.extensions.configuration.commandlineconfigurationextensions.addcommandline) call doesn't include mapped switches, and there's no way to pass the switch-mapping dictionary to [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder). The solution isn't to pass the arguments to [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder) but instead to allow the [IConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationbuilder) method's [AddCommandLine](/en-us/dotnet/api/microsoft.extensions.configuration.commandlineconfigurationextensions.addcommandline) method to process both the arguments and the switch-mapping dictionary.

## Set environment and command-line arguments with Visual Studio

Environment and command-line arguments can be set in Visual Studio from the launch profiles dialog:

* In Solution Explorer, right click the project and select **Properties**.
* Select the **Debug > General** tab and select **Open debug launch profiles UI**.

## File Configuration Provider

[FileConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationprovider) is the base class for loading configuration from the file system. The following configuration providers derive from `FileConfigurationProvider`:

* [INI Configuration Provider](#ini-configuration-provider)
* [JSON Configuration Provider](#json-configuration-provider)
* [XML Configuration Provider](#xml-configuration-provider)

### INI Configuration Provider

The [IniConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.ini.iniconfigurationprovider) loads configuration from INI file key-value pairs. The following example demonstrates how to use the provider. Overloads can specify whether the file is optional and if the configuration is reloaded on file changes.

`IniConfig.ini`:

```
[ConnectionStrings]
DefaultConnection="Data Source=LocalSqlServer\\MSSQLDev;"

[Logging:LogLevel]
Default=Debug
Microsoft=Debug
```

`IniConfig.Production.ini`:

```
[ConnectionStrings]
DefaultConnection="Data Source=LocalSqlServer\\MSSQLProd;"

[Logging:LogLevel]
Default=Information
Microsoft=Warning
```

Load the configuration by calling [AddIniFile](/en-us/dotnet/api/microsoft.extensions.configuration.iniconfigurationextensions.addinifile). The following example creates a configuration source for each of the preceding files. The non-environmental file reloads configuration if the file is changed (`reloadOnChange` parameter, default: `false`). The environmental version of the file specifies that the file is optional (`optional` parameter, default: `false`). If you want to specify reloading the file on change (`reloadOnChange: true`), then you must also specify whether or not the file is optional (`optional`).

```
builder.Configuration
    .AddIniFile("IniConfig.ini", optional: false, reloadOnChange: true);
    .AddIniFile($"IniConfig.{builder.Environment.EnvironmentName}.ini", optional: true);
```

```
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    })
.ConfigureAppConfiguration(config =>
{ 
    config
        .AddIniFile("IniConfig.ini", optional:false, reloadOnChange: true)
        .AddIniFile("IniConfig.Production.ini", optional: true);
});
```

### JSON Configuration Provider

The [JsonConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.json.jsonconfigurationprovider) loads configuration from JSON file key-value pairs. The following example demonstrates how to use the provider. Overloads can specify whether the file is optional and if the configuration is reloaded on file changes.

Call [AddJsonFile](/en-us/dotnet/api/microsoft.extensions.configuration.jsonconfigurationextensions.addjsonfile) with the file path (or file name if the file is at the root of the app). The following makes the file optional (`optional` parameter, default: `false`) and specifies that the configuration is reloaded if the file is changed (`reloadOnChange` parameter, default: `false`). If you want to specify reloading the file on change (`reloadOnChange: true`), then you must also specify whether or not the file is optional (`optional`).

```
builder.Configuration.AddJsonFile("config.json", optional: true, reloadOnChange: true);
```

```
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    })
.ConfigureAppConfiguration(config =>
{ 
    config.AddJsonFile("config.json", optional: true, reloadOnChange: true);
});
```

### XML Configuration Provider

The [XmlConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.xml.xmlconfigurationprovider) loads configuration from XML file key-value pairs. The following example demonstrates how to use the provider. Overloads can specify whether the file is optional and if the configuration is reloaded on file changes.

`XmlFile.xml`:

```
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <ConnectionStrings>
    <DefaultConnection>Data Source=LocalSqlServer\\MSSQLDev;</DefaultConnectionString>
  </ConnectionStrings>
  <Logging>
    <LogLevel>
      <Default>Debug</Default>
      <Microsoft>Debug</Microsoft>
    </LogLevel>
  </Logging>
</configuration>
```

`XmlFile.Production.xml`:

```
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <ConnectionStrings>
    <DefaultConnectionString>Data Source=LocalSqlServer\\MSSQLProd;</DefaultConnectionString>
  </ConnectionStrings>
  <Logging>
    <LogLevel>
      <Default>Information</Default>
      <Microsoft>Warning</Microsoft>
    </LogLevel>
  </Logging>
</configuration>
```

Load the configuration by calling [AddXmlFile](/en-us/dotnet/api/microsoft.extensions.configuration.xmlconfigurationextensions.addxmlfile). The following example creates a configuration source for each of the preceding files. The non-environmental file reloads configuration if the file is changed (`reloadOnChange` parameter, default: `false`). The environmental version of the file specifies that the file is optional (`optional` parameter, default: `false`). If you want to specify reloading the file on change (`reloadOnChange: true`), then you must also specify whether or not the file is optional (`optional`).

```
builder.Configuration
    .AddXmlFile("XmlFile.xml", optional: false, reloadOnChange: true);
    .AddXmlFile($"XmlFile.{builder.Environment.EnvironmentName}.xml", optional: true);
```

```
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    })
.ConfigureAppConfiguration(config =>
{ 
    config
        .AddXmlFile("XmlFile.xml", optional:false, reloadOnChange: true)
        .AddXmlFile("XmlFile.Production.xml", optional: true);
});
```

Repeating elements that use the same element name work if the `name` attribute is used to distinguish the elements:

```
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <section name="section0">
    <key name="key0">value 00</key>
    <key name="key1">value 01</key>
  </section>
  <section name="section1">
    <key name="key0">value 10</key>
    <key name="key1">value 11</key>
  </section>
</configuration>
```

```
var value_00 = Config["section:section0:key:key0"];
var value_01 = Config["section:section0:key:key1"];
var value_10 = Config["section:section1:key:key0"];
var value_11 = Config["section:section1:key:key1"];
```

Attributes that supply values are supported:

```
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <key attribute="value" />
  <section>
    <key attribute="value" />
  </section>
</configuration>
```

The previous configuration loads the following keys with `value`:

* `key:attribute`
* `section:key:attribute`

## Key-Per-File Configuration Provider

The [KeyPerFileConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.keyperfile.keyperfileconfigurationprovider) uses a directory's files as configuration key-value pairs. The key is the file name. The value contains the file's contents. The Key-Per-File Configuration Provider is used in Docker hosting scenarios.

To activate key-per-file configuration, call the [AddKeyPerFile](/en-us/dotnet/api/microsoft.extensions.configuration.keyperfileconfigurationbuilderextensions.addkeyperfile) extension method on an instance of [ConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbuilder). The `directoryPath` to the files must be an absolute path.

Overloads permit specifying:

* An `Action<KeyPerFileConfigurationSource>` delegate that configures the source.
* Whether the directory is optional and the path to the directory.

The double-underscore (`__`) is used as a configuration key delimiter in file names. For example, the file name `Logging__LogLevel__System` produces the configuration key `Logging:LogLevel:System`.

```
var path = Path.Combine(Directory.GetCurrentDirectory(), "path/to/files");
builder.Configuration.AddKeyPerFile(directoryPath: path, optional: true);
```

```
.ConfigureAppConfiguration((hostingContext, config) =>
{
    var path = Path.Combine(
        Directory.GetCurrentDirectory(), "path/to/files");
    config.AddKeyPerFile(directoryPath: path, optional: true);
})
```

## Memory Configuration Provider

The [MemoryConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.memory.memoryconfigurationprovider) uses an in-memory collection as configuration key-value pairs.

The following code adds a memory collection to the configuration system:

```
var configSettings = new Dictionary<string, string>
{
    { "ConnectionStrings:DefaultConnection", "Data Source=LocalSqlServer\\MSSQLDev;" },
    { "Logging:LogLevel:Default", "Information" }
};

builder.Configuration.AddInMemoryCollection(configSettings);
```

```
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    })
    .ConfigureAppConfiguration(config =>
    { 
        var configSettings = new Dictionary<string, string>
        {
            { "ConnectionStrings:DefaultConnection", "Data Source=LocalSqlServer\\MSSQLDev;" },
            { "Logging:LogLevel:Default", "Information" }
        };

        config.AddInMemoryCollection(configSettings);
    });
```

## Kestrel endpoint configuration

Kestrel-specific endpoint configuration overrides all [cross-server](../servers/?view=aspnetcore-10.0) endpoint configurations. Cross-server endpoint configurations include:

* [`UseUrls`](../host/web-host?view=aspnetcore-10.0#server-urls).
* `--urls` on the [command line](./?view=aspnetcore-10.0#command-line).
* The `ASPNETCORE_URLS` [environment variable](./?view=aspnetcore-10.0#environment-variables).

Consider the following Kestrel configuration section in an `appsettings.json` file:

```
"Kestrel": {
  "Endpoints": {
    "Https": {
      "Url": "https://localhost:9999"
    }
  }
},
```

The app is launched on the command line with [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) and the following cross-server endpoint configuration:

```
dotnet run --urls="https://localhost:7777"
```

Kestrel binds to the endpoint configured specifically for Kestrel in the `appsettings.json` file (`https://localhost:9999`) and not the cross-server endpoint configuration passed to the `dotnet run` command (`https://localhost:7777`).

However, consider the Kestrel-specific endpoint configured as an environment variable:

* Key: `Kestrel__Endpoints__Https__Url`
* Value: `https://localhost:8888`

In the preceding environment variable, "`Https`" is the name of the Kestrel-specific endpoint. The preceding app settings configuration also defines a Kestrel-specific endpoint named `Https`. Per the [default host configuration providers](#default-host-configuration-sources), environment variables read by the [Environment Variables Configuration Provider](#environment-variables-configuration-provider) are read after `appsettings.{ENVIRONMENT}.json`. Therefore, the preceding environment variable (`Kestrel__Endpoints__Https__Url`) is used for the `Https` endpoint.

## Extract a single value from configuration with type conversion (`GetValue`)

[ConfigurationBinder.GetValue](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.getvalue) extracts a single value from configuration with a specified key and converts it to the specified type:

```
var number = Config.GetValue<int>("NumberKey", 99);
```

In the preceding code:

* `Config` is an injected [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration).
* If `NumberKey` isn't found in the configuration, the default value of `99` is used.

## Work with sections, get a section's children, and determine if a section exists

For the examples that follow, consider the following `subsection.json` file:

```
{
  "section0": {
    "key0": "value00",
    "key1": "value01"
  },
  "section1": {
    "key0": "value10",
    "key1": "value11"
  },
  "section2": {
    "subsection0": {
      "key0": "value200",
      "key1": "value201"
    },
    "subsection1": {
      "key0": "value210",
      "key1": "value211"
    }
  }
}
```

A configuration provider is added for `subsection.json` by calling [`AddJsonFile`](#json-configuration-provider).

### `GetSection`

[IConfiguration.GetSection](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getsection) returns a configuration subsection with the specified subsection key.

The following code returns values for `section1`, where `Config` is an injected [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration):

```
var subsection = Config.GetSection("section1");
var value1 = subsection["key0"];
var value2 = subsection["key1"];
```

The following code returns values for `section2:subsection0`, where `Config` is an injected [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration):

```
var subsection = Config.GetSection("section2:subsection0");
var value1 = subsection["key0"];
var value2 = subsection["key1"];
```

[GetSection](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getsection) never returns `null`. If a matching section isn't found, an empty [IConfigurationSection](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationsection) is returned.

When [GetSection](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getsection) returns a matching section, [Value](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationsection.value#microsoft-extensions-configuration-iconfigurationsection-value) isn't populated. A [Key](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationsection.key#microsoft-extensions-configuration-iconfigurationsection-key) and [Path](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationsection.path#microsoft-extensions-configuration-iconfigurationsection-path) are returned when the section exists.

### `GetChildren` and `Exists`

The following code calls:

* [ConfigurationExtensions.Exists](/en-us/dotnet/api/microsoft.extensions.configuration.configurationextensions.exists) to verify that `section2` exists.
* [IConfiguration.GetChildren](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getchildren) and returns values for the subsections of `section2`.

```
var section = Config.GetSection("section2");

if (!section.Exists())
{
    throw new Exception("section2 doesn't exist!");
}

var children = section.GetChildren();

foreach (var subSection in children)
{
    int i = 0;
    var key1 = subSection.Key + ":key" + i++.ToString();
    var key2 = subSection.Key + ":key" + i.ToString();
    Console.WriteLine($"{key1} value: {section[key1]}");
    Console.WriteLine($"{key2} value: {section[key2]}");
}
```

Output:

> subsection0:key0 value: value200\
> subsection0:key1 value: value201\
> subsection1:key0 value: value210\
> subsection1:key1 value: value211

## Bind an array

The [ConfigurationBinder.Bind](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.bind) supports binding arrays to objects using array indices in configuration keys. Any array format that exposes a numeric key segment is capable of array binding to a [POCO](https://wikipedia.org/wiki/Plain_Old_CLR_Object) class array.

`array.json`:

```
{
  "array": {
    "entries": {
      "0": "value00",
      "1": "value10",
      "2": "value20",
      "4": "value40",
      "5": "value50"
    }
  }
}
```

A configuration provider is added for `array.json` by calling [`AddJsonFile`](#json-configuration-provider).

The following code reads the configuration values.

`ArrayExample.cs`:

```
public class ArrayExample
{
    public string[]? Entries { get; set; } 
}
```

```
var array = Config.GetSection("array").Get<ArrayExample>();

if (array is null)
{
    throw new ArgumentNullException(nameof(array));
}

for (int j = 0; j < array.Entries?.Length; j++)
{
    Console.WriteLine($"Index: {j} Value: {array.Entries[j]}");
}
```

Output:

```
Index: 0 Value: value00
Index: 1 Value: value10
Index: 2 Value: value20
Index: 3 Value: value40
Index: 4 Value: value50
```

In the preceding output, Index 3 has value `value40`, corresponding to `"4": "value40",` in `array.json`. The bound array indices are continuous and not bound to the configuration key index. The configuration binder isn't capable of binding `null` values or creating `null` entries in bound objects.

The missing configuration item for Index 3 can be supplied before binding to the `ArrayExample` instance by any configuration provider that reads the Index 3 key/value pair. In the following example, assume that the values provided by `array.json` in the preceding example are provided by an [in-memory collection](#memory-configuration-provider). The example shows how to load the Index 3 value from the [JSON Configuration Provider](#json-configuration-provider) before the collection is bound.

`value3.json`:

```
{
  "array:entries:3": "value30"
}
```

```
var configSettings = new Dictionary<string, string>
{
    { "array:entries:0", "value00" },
    { "array:entries:1", "value10" },
    { "array:entries:2", "value20" },
    { "array:entries:4", "value40" },
    { "array:entries:5", "value50" }
};

builder.Configuration.AddInMemoryCollection(configSettings);
builder.Configuration.AddJsonFile("value3.json", optional: false, 
    reloadOnChange: false);
```

```
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    })
    .ConfigureAppConfiguration(config =>
    { 
        var configSettings = new Dictionary<string, string>
        {
            { "array:entries:0", "value00" },
            { "array:entries:1", "value10" },
            { "array:entries:2", "value20" },
            { "array:entries:4", "value40" },
            { "array:entries:5", "value50" }
        };

        config.AddInMemoryCollection(configSettings);
        config.AddJsonFile("value3.json", optional: false, reloadOnChange: false);
    });
```

The preceding code results in the following bound array:

```
Index: 0 Value: value00
Index: 1 Value: value10
Index: 2 Value: value20
Index: 3 Value: value30
Index: 4 Value: value40
Index: 5 Value: value50
```

## Custom configuration provider

The sample app demonstrates how to create a basic configuration provider that reads configuration key-value pairs from a database using [Entity Framework (EF)](/en-us/ef/core/).

The provider has the following characteristics:

* The EF in-memory database is used for demonstration purposes. To use a database that requires a connection string, implement a secondary [ConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbuilder) to supply the connection string from another configuration provider.
* The provider reads a database table into configuration at startup. The provider doesn't query the database on a per-key basis.
* Reload-on-change isn't implemented, so updating the database after the app starts has no effect on the app's configuration.

Define an `EFConfigurationValue` entity for storing configuration values in the database.

`Models/EFConfigurationValue.cs`:

```
public class EFConfigurationValue
{
    public string Id { get; set; } = string.Empty;
    public string Value { get; set; } = string.Empty;
}
```

Add an `EFConfigurationContext` to store and access the configured values.

`EFConfigurationProvider/EFConfigurationContext.cs`:

```
public class EFConfigurationContext : DbContext
{
    public EFConfigurationContext(
        DbContextOptions<EFConfigurationContext> options) : base(options)
    {
    }

    public DbSet<EFConfigurationValue> Values => Set<EFConfigurationValue>();
}
```

```
// using Microsoft.EntityFrameworkCore;

public class EFConfigurationContext : DbContext
{
    public EFConfigurationContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<EFConfigurationValue> Values { get; set; }
}
```

Create a class that implements [IConfigurationSource](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationsource).

`EFConfigurationProvider/EFConfigurationSource.cs`:

Note

The example requires the following `using` statements:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
```

```
public class EFConfigurationSource : IConfigurationSource
{
    private readonly Action<DbContextOptionsBuilder> _optionsAction;

    public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction) => 
        _optionsAction = optionsAction;

    public IConfigurationProvider Build(IConfigurationBuilder builder) => 
        new EFConfigurationProvider(_optionsAction);
}
```

Create the custom configuration provider by inheriting from [ConfigurationProvider](/en-us/dotnet/api/microsoft.extensions.configuration.configurationprovider). The configuration provider initializes the database when it's empty. Since configuration keys are case-insensitive, the dictionary used to initialize the database is created with the case-insensitive comparer, [StringComparer.OrdinalIgnoreCase](/en-us/dotnet/api/system.stringcomparer.ordinalignorecase).

`EFConfigurationProvider/EFConfigurationProvider.cs`:

```
using Microsoft.EntityFrameworkCore;

public class EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction) : ConfigurationProvider
{
    public override void Load()
    {
        var builder = new DbContextOptionsBuilder<EFConfigurationContext>();

        optionsAction(builder);

        using var dbContext = new EFConfigurationContext(builder.Options);

        if (dbContext == null || dbContext.Values == null)
        {
            throw new Exception("Null DB context");
        }

        dbContext.Database.EnsureCreated();

        Data = !dbContext.Values.Any()
            ? CreateAndSaveDefaultValues(dbContext)
            : dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
    }

    private static Dictionary<string, string> CreateAndSaveDefaultValues(
        EFConfigurationContext dbContext)
    {
        // Quotes (c)2005 Universal Pictures: Serenity
        // https://www.uphe.com/movies/serenity-2005
        var configValues =
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                { "quote1", "I aim to misbehave." },
                { "quote2", "I swallowed a bug." },
                { "quote3", "You can't stop the signal, Mal." }
            };

        if (dbContext == null || dbContext.Values == null)
        {
            throw new Exception("Null DB context");
        }

        dbContext.Values.AddRange(configValues
            .Select(kvp => new EFConfigurationValue
            {
                Id = kvp.Key,
                Value = kvp.Value
            })
            .ToArray());

        dbContext.SaveChanges();

        return configValues;
    }
}
```

```
// using Microsoft.EntityFrameworkCore;
// using Microsoft.Extensions.Configuration;

public class EFConfigurationProvider : ConfigurationProvider
{
    public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
    {
        OptionsAction = optionsAction;
    }

    Action<DbContextOptionsBuilder> OptionsAction { get; }

    public override void Load()
    {
        var builder = new DbContextOptionsBuilder<EFConfigurationContext>();

        OptionsAction(builder);

        using (var dbContext = new EFConfigurationContext(builder.Options))
        {
            dbContext.Database.EnsureCreated();

            Data = !dbContext.Values.Any()
                ? CreateAndSaveDefaultValues(dbContext)
                : dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
        }
    }

    private static IDictionary<string, string> CreateAndSaveDefaultValues(
        EFConfigurationContext dbContext)
    {
        // Quotes (c)2005 Universal Pictures: Serenity
        // https://www.uphe.com/movies/serenity-2005
        var configValues = 
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                { "quote1", "I aim to misbehave." },
                { "quote2", "I swallowed a bug." },
                { "quote3", "You can't stop the signal, Mal." }
            };

        dbContext.Values.AddRange(configValues
            .Select(kvp => new EFConfigurationValue 
                {
                    Id = kvp.Key,
                    Value = kvp.Value
                })
            .ToArray());

        dbContext.SaveChanges();

        return configValues;
    }
}
```

An `AddEFConfiguration` extension method permits adding the configuration source to a [ConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbuilder).

`Extensions/EntityFrameworkExtensions.cs`:

Note

The example requires the following `using` statements:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
```

```
public static class EntityFrameworkExtensions
{
    public static IConfigurationBuilder AddEFConfiguration(
        this IConfigurationBuilder builder,
        Action<DbContextOptionsBuilder> optionsAction)
    {
        return builder.Add(new EFConfigurationSource(optionsAction));
    }
}
```

The following code shows how to use the custom `EFConfigurationProvider` in the app's `Program` file:

```
builder.Configuration.AddEFConfiguration(
    opt => opt.UseInMemoryDatabase("InMemoryDb"));
```

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.AddEFConfiguration(
                options => options.UseInMemoryDatabase("InMemoryDb"));
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

For an example of accessing configuration using startup convenience methods, see [App startup: Convenience methods](../startup?view=aspnetcore-10.0#configure-services-without-a-startup-class).

## Add configuration from an external assembly

An [IHostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup) implementation allows adding enhancements to an app at startup from an external assembly outside of the app's `Startup` class. For more information, see [Use hosting startup assemblies in ASP.NET Core](../host/platform-specific-configuration?view=aspnetcore-10.0).

## Configuration-binding source generator

The [Configuration-binding source generator](/en-us/dotnet/core/whats-new/dotnet-8/runtime#configuration-binding-source-generator) provides AOT and trim-friendly configuration. For more information, see [Configuration-binding source generator](/en-us/dotnet/core/whats-new/dotnet-8/runtime#configuration-binding-source-generator).

## Related content

* [ASP.NET Core Blazor configuration](../../blazor/fundamentals/configuration?view=aspnetcore-10.0)
* [Configuration source code](https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.Configuration)
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/configuration) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))
* [Options pattern in ASP.NET Core](options?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-09

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/overwriting-parameters
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/overwriting-parameters.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Avoid overwriting parameters in ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Robert Haken](https://havit.blazor.eu)

This article explains how to avoid overwriting parameters in Blazor apps during rerendering.

## Overwritten parameters

The Blazor framework generally imposes safe parent-to-child parameter assignment:

* Parameters aren't overwritten unexpectedly.
* Side effects are minimized. For example, additional renders are avoided because they may create infinite rendering loops.

A child component receives new parameter values that possibly overwrite existing values when the parent component rerenders. Accidentally overwriting parameter values in a child component often occurs when developing the component with one or more data-bound parameters and the developer writes directly to a parameter in the child:

* The child component is rendered with one or more parameter values from the parent component.
* The child writes directly to the value of a parameter.
* The parent component rerenders and overwrites the value of the child's parameter.

The potential for overwriting parameter values extends into the child component's property `set` accessors, too.

Important

Our general guidance is not to create components that directly write to their own parameters after the component is rendered for the first time.

Consider the following `ShowMoreExpander` component that:

* Renders the title.
* Shows the child content when selected.
* Allows you to set initial state with a component parameter (`InitiallyExpanded`).

After the following `ShowMoreExpander` component demonstrates an overwritten parameter, a modified `ShowMoreExpander` component is shown to demonstrate the correct approach for this scenario. The following examples can be placed in a local sample app to experience the behaviors described.

`ShowMoreExpander.razor`:

```
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private void ShowMore() => InitiallyExpanded = true;
}
```

```
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private void ShowMore() => InitiallyExpanded = true;
}
```

```
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
```

```
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
```

```
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
```

```
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
```

The `ShowMoreExpander` component is added to the following `Expanders` parent component that may call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged):

* Calling [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) in developer code notifies a component that its state has changed and typically enqueues component rerendering to update the UI. [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is covered in more detail later in [ASP.NET Core Razor component lifecycle](lifecycle?view=aspnetcore-10.0) and [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0#statehaschanged).
* The button's `@onclick` directive attribute attaches an event handler to the button's `onclick` event. Event handling is covered in more detail later in [ASP.NET Core Blazor event handling](event-handling?view=aspnetcore-10.0).

`Expanders.razor`:

```
@page "/expanders"

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
```

```
@page "/expanders"

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
```

```
@page "/expanders"

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
```

```
@page "/expanders"

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
```

```
@page "/expanders"

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
```

```
@page "/expanders"

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
```

Initially, the `ShowMoreExpander` components behave independently when their `InitiallyExpanded` properties are set. The child components maintain their states as expected.

If [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is called in a parent component, the Blazor framework rerenders child components if their parameters might have changed:

* For a group of parameter types that Blazor explicitly checks, Blazor rerenders a child component if it detects that any of the parameters have changed.
* For unchecked parameter types, Blazor rerenders the child component *regardless of whether or not the parameters have changed*. Child content falls into this category of parameter types because child content is of type [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), which is a delegate that refers to other mutable objects.

For the `Expanders` component:

* The first `ShowMoreExpander` component sets child content in a potentially mutable [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), so a call to [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) in the parent component automatically rerenders the component and potentially overwrites the value of `InitiallyExpanded` to its initial value of `false`.
* The second `ShowMoreExpander` component doesn't set child content. Therefore, a potentially mutable [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) doesn't exist. A call to [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) in the parent component doesn't automatically rerender the child component, so the component's `InitiallyExpanded` value isn't overwritten.

To maintain state in the preceding scenario, use a *private field* in the `ShowMoreExpander` component to maintain its state.

The following revised `ShowMoreExpander` component:

* Accepts the `InitiallyExpanded` component parameter value from the parent.
* Assigns the component parameter value to a *private field* (`expanded`) in the [`OnInitialized` event](lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync).
* Uses the private field to maintain its internal toggle state, which demonstrates how to avoid writing directly to a parameter.

Note

The advice in this section extends to similar logic in component parameter `set` accessors, which can result in similar undesirable side effects.

`ShowMoreExpander.razor`:

```
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    protected override void OnInitialized() => expanded = InitiallyExpanded;

    private void Expand() => expanded = true;
}
```

```
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    protected override void OnInitialized() => expanded = InitiallyExpanded;

    private void Expand() => expanded = true;
}
```

```
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
```

```
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
```

```
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
```

```
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
```

Note

The revised `ShowMoreExpander` doesn't reflect changes to the `InitiallyExpanded` parameter after initialization (`OnInitialized`). In certain scenarios, an already initialized component might receive new parameter values. This can happen, for example, in a primary-subordinate view where the same component is used to render different detail views or when the `/item/{id}` route parameter changes to display a different item.

Consider following `ToggleExpander` component that:

* Allows you to change the state both from inside and outside.
* Handles new parameter values even if the same component instance is reused.

`ToggleExpander.razor`:

```
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private bool expanded;

    protected override void OnParametersSet() => expanded = Expanded;

    private async Task Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
```

```
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private bool expanded;

    protected override void OnParametersSet() => expanded = Expanded;

    private async Task Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
```

```
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async Task Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
```

```
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async Task Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
```

```
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async Task Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
```

```
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async Task Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
```

The `ToggleExpander` component should be used with the `@bind-Expanded="{field}"` binding syntax, allowing two-way synchronization of the parameter.

`ExpandersToggle.razor`:

```
@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle() => expanded = !expanded;
}
```

```
@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle() => expanded = !expanded;
}
```

```
@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
```

```
@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
```

```
@page "/expanders-toggle"

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
```

```
@page "/expanders-toggle"

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
```

For more information on parent-child binding, see the following resources:

* [Binding with component parameters](data-binding?view=aspnetcore-10.0#binding-with-component-parameters)
* [Bind across more than two components](data-binding?view=aspnetcore-10.0#bind-across-more-than-two-components)
* [Blazor Two Way Binding Error (dotnet/aspnetcore #24599)](https://github.com/dotnet/aspnetcore/issues/24599)

For more information on change detection, including information on the exact types that Blazor checks, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0#rendering-conventions-for-componentbase).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/migration
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/migration.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate gRPC from C-core to gRPC for .NET

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Due to the implementation of the underlying stack, not all features work in the same way between [C-core-based gRPC](https://grpc.io/blog/grpc-stacks) apps and gRPC for .NET. This document highlights the key differences for migrating between the two stacks.

Important

gRPC C-core is in maintenance mode and [will be deprecated in favor of gRPC for .NET](https://grpc.io/blog/grpc-csharp-future/). gRPC C-core is not recommended for new apps.

## Platform support

gRPC C-core and gRPC for .NET have different platform support:

* **gRPC C-core**: A C++ gRPC implementation with its own TLS and HTTP/2 stacks. The `Grpc.Core` package is a .NET wrapper around gRPC C-core and contains a gRPC client and server. It supports .NET Framework, .NET Core, and .NET 5 or later.
* **gRPC for .NET**: Designed for .NET Core 3.x and .NET 5 or later. It uses TLS and HTTP/2 stacks built into modern .NET releases. The `Grpc.AspNetCore` package contains a gRPC server that is hosted in ASP.NET Core and requires .NET Core 3.x or .NET 5 or later. The `Grpc.Net.Client` package contains a gRPC client. The client in `Grpc.Net.Client` has limited support for .NET Framework using [WinHttpHandler](/en-us/dotnet/api/system.net.http.winhttphandler).

For more information, see [gRPC on .NET supported platforms](supported-platforms?view=aspnetcore-10.0).

## Configure server and channel

NuGet packages, configuration, and startup code must be modified when migrating from gRPC C-Core to gRPC for .NET.

gRPC for .NET has separate NuGet packages for its client and server. The packages added depend upon whether an app is hosting gRPC services or calling them:

* [**`Grpc.AspNetCore`**](https://www.nuget.org/packages/Grpc.AspNetCore): Services are hosted by ASP.NET Core. For server configuration information, see [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0).
* [**`Grpc.Net.Client`**](https://www.nuget.org/packages/Grpc.Net.Client): Clients use `GrpcChannel`, which internally uses networking functionality built into .NET. For client configuration information, see [Call gRPC services with the .NET client](client?view=aspnetcore-10.0).

When migration is complete, the `Grpc.Core` package should be removed from the app. `Grpc.Core` contains large native binaries, and removing the package reduces NuGet restore time and app size.

## Code generated services and clients

gRPC C-Core and gRPC for .NET share many APIs, and code generated from `.proto` files is compatible with both gRPC implementations. Most clients and service can be migrated from C-Core to gRPC for .NET without changes.

## gRPC service implementation lifetime

In the ASP.NET Core stack, gRPC services, by default, are created with a [scoped lifetime](../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes). In contrast, gRPC C-core by default binds to a service with a [singleton lifetime](../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes).

A scoped lifetime allows the service implementation to resolve other services with scoped lifetimes. For example, a scoped lifetime can also resolve `DbContext` from the DI container through constructor injection. Using scoped lifetime:

* A new instance of the service implementation is constructed for each request.
* It isn't possible to share state between requests via instance members on the implementation type.
* The expectation is to store shared states in a singleton service in the DI container. The stored shared states are resolved in the constructor of the gRPC service implementation.

For more information on service lifetimes, see [Dependency injection in ASP.NET Core](../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes).

### Add a singleton service

To facilitate the transition from a gRPC C-core implementation to ASP.NET Core, it's possible to change the service lifetime of the service implementation from scoped to singleton. This involves adding an instance of the service implementation to the DI container:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();
    services.AddSingleton(new GreeterService());
}
```

However, a service implementation with a singleton lifetime is no longer able to resolve scoped services through constructor injection.

## Configure gRPC services options

In C-core-based apps, settings such as `grpc.max_receive_message_length` and `grpc.max_send_message_length` are configured with `ChannelOption` when [constructing the Server instance](https://grpc.io/grpc/csharp/api/Grpc.Core.Server.html#Grpc_Core_Server__ctor_System_Collections_Generic_IEnumerable_Grpc_Core_ChannelOption__).

In ASP.NET Core, gRPC provides configuration through the `GrpcServiceOptions` type. For example, a gRPC service's the maximum incoming message size can be configured via `AddGrpc`. The following example changes the default `MaxReceiveMessageSize` of 4 MB to 16 MB:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16 MB
    });
}
```

For more information on configuration, see [gRPC for .NET configuration](configuration?view=aspnetcore-10.0).

## Logging

C-core-based apps rely on the `GrpcEnvironment` to [configure the logger](https://grpc.io/grpc/csharp/api/Grpc.Core.GrpcEnvironment.html?q=size#Grpc_Core_GrpcEnvironment_SetLogger_Grpc_Core_Logging_ILogger_) for debugging purposes. The ASP.NET Core stack provides this functionality through the [Logging API](../fundamentals/logging/?view=aspnetcore-10.0). For example, a logger can be added to the gRPC service.

Constructor injection:

```
public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }
}
```

Primary constructor injection (.NET 8 or later):

```
public class GreeterService(ILogger<GreeterService> logger) : Greeter.GreeterBase
{
    ...
}
```

For more information on gRPC logging and diagnostics, see [Logging and diagnostics in gRPC on .NET](diagnostics?view=aspnetcore-10.0).

## HTTPS

C-core-based apps configure HTTPS through the [Server.Ports property](https://grpc.io/grpc/csharp/api/Grpc.Core.Server.html#Grpc_Core_Server_Ports). A similar concept is used to configure servers in ASP.NET Core. For example, Kestrel uses [endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0) for this functionality.

C-core-based apps configure HTTPS through the [Server.Ports property](https://grpc.io/grpc/csharp/api/Grpc.Core.Server.html#Grpc_Core_Server_Ports). A similar concept is used to configure servers in ASP.NET Core. For example, Kestrel uses [endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration) for this functionality.

## gRPC Interceptors

ASP.NET Core [middleware](../fundamentals/middleware/?view=aspnetcore-10.0) offers similar functionalities compared to interceptors in C-core-based gRPC apps. Both are supported by ASP.NET Core gRPC apps, so there's no need to rewrite interceptors.

For more information on how these features compare to each other, see [gRPC Interceptors versus middleware](interceptors?view=aspnetcore-10.0#grpc-interceptors-versus-middleware).

## Host gRPC in non-ASP.NET Core projects

A C-core-based server can be added to any project type. gRPC for .NET server requires ASP.NET Core. ASP.NET Core is usually available because the project file specifies `Microsoft.NET.SDK.Web` as the SDK.

A gRPC server can be hosted to non-ASP.NET Core projects by adding `<FrameworkReference Include="Microsoft.AspNetCore.App" />` to a project. The framework reference makes ASP.NET Core APIs available and they can be used to start an ASP.NET Core server.

For more information, see [Host gRPC in non-ASP.NET Core projects](aspnetcore?view=aspnetcore-10.0#host-grpc-in-non-aspnet-core-projects).

## Related content

* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0)
* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/security/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/security/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor Hybrid authentication and authorization

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes ASP.NET Core's support for the configuration and management of security and ASP.NET Core Identity in Blazor Hybrid apps.

Authentication in Blazor Hybrid apps is handled by native platform libraries, as they offer enhanced security guarantees that the browser sandbox can't offer. Authentication of native apps uses an OS-specific mechanism or via a federated protocol, such as [OpenID Connect (OIDC)](https://openid.net/developers/how-connect-works/). Follow the guidance for the identity provider that you've selected for the app and then further integrate identity with Blazor using the guidance in this article.

Integrating authentication must achieve the following goals for Razor components and services:

* Use the abstractions in the [`Microsoft.AspNetCore.Components.Authorization` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization), such as [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview).
* React to changes in the authentication context.
* Access credentials provisioned by the app from the identity provider, such as access tokens to perform authorized API calls.

After authentication is added to a .NET MAUI, WPF, or Windows Forms app and users are able to log in and log out successfully, integrate authentication with Blazor to make the authenticated user available to Razor components and services. Perform the following steps:

* Reference the [`Microsoft.AspNetCore.Components.Authorization` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization).

  Note

  For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

* Implement a custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider), which is the abstraction that Razor components use to access information about the authenticated user and to receive updates when the authentication state changes.

* Register the custom authentication state provider in the dependency injection container.

.NET MAUI apps use the `WebAuthenticator` class to initiate browser-based authentication flows that listen for a callback to a specific URL registered with the app.

For additional guidance, see the following resources:

* [Web authenticator (.NET MAUI documentation)](/en-us/dotnet/maui/platform-integration/communication/authentication)
* [`Sample.Server.WebAuthenticator` sample app](https://github.com/dotnet/maui/tree/main/src/Essentials/samples/Sample.Server.WebAuthenticator)

WPF apps use the [Microsoft identity platform](/en-us/entra/identity-platform/) to integrate with Microsoft Entra (ME-ID) and AAD B2C. For guidance and examples, see the following resources:

* [Overview of the Microsoft Authentication Library (MSAL)](/en-us/entra/identity-platform/msal-overview)
* [Add authentication to your Windows (WPF) app](/en-us/azure/developer/mobile-apps/azure-mobile-apps/quickstarts/wpf/authentication)
* [Tutorial: Sign in users and call Microsoft Graph in Windows Presentation Foundation (WPF) desktop app](/en-us/entra/identity-platform/tutorial-v2-windows-desktop)
* [Quickstart: Acquire a token and call Microsoft Graph API from a desktop application](/en-us/entra/identity-platform/index-desktop)
* [Quickstart: Set up sign in for a desktop app using Azure Active Directory B2C](/en-us/azure/active-directory-b2c/quickstart-native-app-desktop)
* [Configure authentication in a sample WPF desktop app by using Azure AD B2C](/en-us/azure/active-directory-b2c/configure-authentication-sample-wpf-desktop-app)

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

Windows Forms apps use the [Microsoft identity platform](/en-us/entra/identity-platform/) to integrate with Microsoft Entra (ME-ID) and AAD B2C. For more information, see [Overview of the Microsoft Authentication Library (MSAL)](/en-us/entra/identity-platform/msal-overview).

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

## Create a custom `AuthenticationStateProvider` without user change updates

If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. In this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following `ExternalAuthStateProvider` is an example implementation of a custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) for this authentication scenario.

Note

The following custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

`ExternalAuthStateProvider.cs`:

```
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private readonly Task<AuthenticationState> authenticationState;

    public ExternalAuthStateProvider(AuthenticatedUser user) => 
        authenticationState = Task.FromResult(new AuthenticationState(user.Principal));

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        authenticationState;
}

public class AuthenticatedUser
{
    public ClaimsPrincipal Principal { get; set; } = new();
}
```

The following steps describe how to:

* Add required namespaces.
* Add the authorization services and Blazor abstractions to the service collection.
* Build the service collection.
* Resolve the `AuthenticatedUser` service to set the authenticated user's claims principal. See your identity provider's documentation for details.
* Return the built host.

In the `MauiProgram.CreateMauiApp` method of `MauiProgram.cs`, add namespaces for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) and [System.Security.Claims](/en-us/dotnet/api/system.security.claims):

```
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
```

Remove the following line of code that returns a built [Microsoft.Maui.Hosting.MauiApp](/en-us/dotnet/api/microsoft.maui.hosting.mauiapp):

```
- return builder.Build();
```

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

```
builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<AuthenticatedUser>();
var host = builder.Build();

var authenticatedUser = host.Services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

return host;
```

The following steps describe how to:

* Add required namespaces.
* Add the authorization services and Blazor abstractions to the service collection.
* Build the service collection and add the built service collection as a resource to the app's `ResourceDictionary`.
* Resolve the `AuthenticatedUser` service to set the authenticated user's claims principal. See your identity provider's documentation for details.
* Return the built host.

In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add namespaces for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) and [System.Security.Claims](/en-us/dotnet/api/system.security.claims):

```
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
```

Remove the following line of code that adds the built service collection as a resource to the app's `ResourceDictionary`:

```
- Resources.Add("services", serviceCollection.BuildServiceProvider());
```

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

```
serviceCollection.AddAuthorizationCore();
serviceCollection.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<AuthenticatedUser>();
var services = serviceCollection.BuildServiceProvider();
Resources.Add("services", services);

var authenticatedUser = services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;
```

The following steps describe how to:

* Add required namespaces.
* Add the authorization services and Blazor abstractions to the service collection.
* Build the service collection and add the built service collection to the app's service provider.
* Resolve the `AuthenticatedUser` service to set the authenticated user's claims principal. See your identity provider's documentation for details.

In the `Form1`'s constructor (`Form1.cs`), add namespaces for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) and [System.Security.Claims](/en-us/dotnet/api/system.security.claims):

```
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
```

Remove the following line of code that sets the built service collection to the app's service provider:

```
- blazorWebView1.Services = services.BuildServiceProvider();
```

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

```
services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<AuthenticatedUser>();
var serviceCollection = services.BuildServiceProvider();
blazorWebView1.Services = serviceCollection;

var authenticatedUser = serviceCollection.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;
```

## Create a custom `AuthenticationStateProvider` with user change updates

To update the user while the Blazor app is running, call [NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged) within the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) implementation using ***either*** of the following approaches:

* [Signal an authentication update from outside of the `BlazorWebView`](#signal-an-authentication-update-from-outside-of-the-blazorwebview-option-1))
* [Handle authentication within the `BlazorWebView`](#handle-authentication-within-the-blazorwebview-option-2)

### Signal an authentication update from outside of the `BlazorWebView` (Option 1)

A custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) can use a global service to signal an authentication update. We recommend that the service offer an event that the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) can subscribe to, where the event invokes [NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged).

Note

The following custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

`ExternalAuthStateProvider.cs`:

```
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState currentUser;

    public ExternalAuthStateProvider(ExternalAuthService service)
    {
        currentUser = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            currentUser = new AuthenticationState(newUser);
            NotifyAuthenticationStateChanged(Task.FromResult(currentUser));
        };
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(currentUser);
}

public class ExternalAuthService
{
    public event Action<ClaimsPrincipal>? UserChanged;
    private ClaimsPrincipal? currentUser;

    public ClaimsPrincipal CurrentUser
    {
        get { return currentUser ?? new(); }
        set
        {
            currentUser = value;

            if (UserChanged is not null)
            {
                UserChanged(currentUser);
            }
        }
    }
}
```

In the `MauiProgram.CreateMauiApp` method of `MauiProgram.cs`, add a namespace for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization):

```
using Microsoft.AspNetCore.Components.Authorization;
```

Add the authorization services and Blazor abstractions to the service collection:

```
builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<ExternalAuthService>();
```

In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add a namespace for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization):

```
using Microsoft.AspNetCore.Components.Authorization;
```

Add the authorization services and the Blazor abstractions to the service collection:

```
serviceCollection.AddAuthorizationCore();
serviceCollection.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<ExternalAuthService>();
```

In the `Form1`'s constructor (`Form1.cs`), add a namespace for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization):

```
using Microsoft.AspNetCore.Components.Authorization;
```

Add the authorization services and Blazor abstractions to the service collection:

```
services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<ExternalAuthService>();
```

Wherever the app authenticates a user, resolve the `ExternalAuthService` service:

```
var authService = host.Services.GetRequiredService<ExternalAuthService>();
```

Execute your custom OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details. The authenticated user (`authenticatedUser` in the following example) is a new [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) based on a new [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity).

Set the current user to the authenticated user:

```
authService.CurrentUser = authenticatedUser;
```

An alternative to the preceding approach is to set the user's principal on [System.Threading.Thread.CurrentPrincipal](/en-us/dotnet/api/system.threading.thread.currentprincipal#system-threading-thread-currentprincipal) instead of setting it via a service, which avoids use of the dependency injection container:

```
public class CurrentThreadUserAuthenticationStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(
            new AuthenticationState(Thread.CurrentPrincipal as ClaimsPrincipal ?? 
                new ClaimsPrincipal(new ClaimsIdentity())));
}
```

Using the alternative approach, only authorization services ([AddAuthorizationCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authorizationservicecollectionextensions.addauthorizationcore)) and `CurrentThreadUserAuthenticationStateProvider` (`.TryAddScoped<AuthenticationStateProvider, CurrentThreadUserAuthenticationStateProvider>()`) are added to the service collection.

### Handle authentication within the `BlazorWebView` (Option 2)

A custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) can include additional methods to trigger log in and log out and update the user.

Note

The following custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

`ExternalAuthStateProvider.cs`:

```
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(new AuthenticationState(currentUser));

    public Task LogInAsync()
    {
        var loginTask = LogInAsyncCore();
        NotifyAuthenticationStateChanged(loginTask);

        return loginTask;

        async Task<AuthenticationState> LogInAsyncCore()
        {
            var user = await LoginWithExternalProviderAsync();
            currentUser = user;

            return new AuthenticationState(currentUser);
        }
    }

    private Task<ClaimsPrincipal> LoginWithExternalProviderAsync()
    {
        /*
            Provide OpenID/MSAL code to authenticate the user. See your identity 
            provider's documentation for details.

            Return a new ClaimsPrincipal based on a new ClaimsIdentity.
        */
        var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity());

        return Task.FromResult(authenticatedUser);
    }

    public void Logout()
    {
        currentUser = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(currentUser)));
    }
}
```

In the preceding example:

* The call to `LogInAsyncCore` triggers the login process.
* The call to [NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged) notifies that an update is in progress, which allows the app to provide a temporary UI during the login or logout process.
* Returning `loginTask` returns the task so that the component that triggered the login can await and react after the task is complete.
* The `LoginWithExternalProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see your identity provider's documentation. The authenticated user (`authenticatedUser`) is a new [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) based on a new [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity).

In the `MauiProgram.CreateMauiApp` method of `MauiProgram.cs`, add the authorization services and the Blazor abstraction to the service collection:

```
builder.Services.AddAuthorizationCore();
builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
```

In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add the authorization services and the Blazor abstraction to the service collection:

```
serviceCollection.AddAuthorizationCore();
serviceCollection.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
```

In the `Form1`'s constructor (`Form1.cs`), add the authorization services and the Blazor abstraction to the service collection:

```
services.AddAuthorizationCore();
services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
```

The following `LoginComponent` component demonstrates how to log in a user. In a typical app, the `LoginComponent` component is only shown in a parent component if the user isn't logged into the app.

`Shared/LoginComponent.razor`:

```
@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Login">Log in</button>

@code
{
    public async Task Login()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .LogInAsync();
    }
}
```

The following `LogoutComponent` component demonstrates how to log out a user. In a typical app, the `LogoutComponent` component is only shown in a parent component if the user is logged into the app.

`Shared/LogoutComponent.razor`:

```
@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Logout">Log out</button>

@code
{
    public async Task Logout()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .Logout();
    }
}
```

## Accessing other authentication information

Blazor doesn't define an abstraction to deal with other credentials, such as access tokens to use for HTTP requests to web APIs. We recommend following the identity provider's guidance to manage the user's credentials with the primitives that the identity provider's SDK provides.

It's common for identity provider SDKs to use a token store for user credentials stored in the device. If the SDK's token store primitive is added to the service container, consume the SDK's primitive within the app.

The Blazor framework isn't aware of a user's authentication credentials and doesn't interact with credentials in any way, so the app's code is free to follow whatever approach you deem most convenient. However, follow the general security guidance in the next section, [Other authentication security considerations](#other-authentication-security-considerations), when implementing authentication code in an app.

## Other authentication security considerations

The authentication process is external to Blazor, and we recommend that developers access the identity provider's guidance for additional security guidance.

When implementing authentication:

* Avoid authentication in the context of the Web View. For example, avoid using a JavaScript OAuth library to perform the authentication flow. In a single-page app, authentication tokens aren't hidden in JavaScript and can be easily discovered by malicious users and used for nefarious purposes. Native apps don't suffer this risk because native apps are only able to obtain tokens outside of the browser context, which means that rogue third-party scripts can't steal the tokens and compromise the app.

* Avoid implementing the authentication workflow yourself. In most cases, platform libraries securely handle the authentication workflow, using the system's browser instead of using a custom Web View that can be hijacked.

* Avoid using the platform's Web View control to perform authentication. Instead, rely on the system's browser when possible.

* Avoid passing the tokens to the document context (JavaScript). In some situations, a JavaScript library within the document is required to perform an authorized call to an external service. Instead of making the token available to JavaScript via JS interop:

  * Provide a generated temporary token to the library and within the Web View.
  * Intercept the outgoing network request in code.
  * Replace the temporary token with the real token and confirm that the destination of the request is valid.

Authentication in Blazor Hybrid apps is handled by native platform libraries, as they offer enhanced security guarantees that the browser sandbox can't offer. Authentication of native apps uses an OS-specific mechanism or via a federated protocol, such as [OpenID Connect (OIDC)](https://openid.net/developers/how-connect-works/). Follow the guidance for the identity provider that you've selected for the app and then further integrate identity with Blazor using the guidance in this article.

Integrating authentication must achieve the following goals for Razor components and services:

* Use the abstractions in the [`Microsoft.AspNetCore.Components.Authorization` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization), such as [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview).
* React to changes in the authentication context.
* Access credentials provisioned by the app from the identity provider, such as access tokens to perform authorized API calls.

After authentication is added to a .NET MAUI, WPF, or Windows Forms app and users are able to log in and log out successfully, integrate authentication with Blazor to make the authenticated user available to Razor components and services. Perform the following steps:

* Reference the [`Microsoft.AspNetCore.Components.Authorization` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization).

  Note

  For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

* Implement a custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider), which is the abstraction that Razor components use to access information about the authenticated user and to receive updates when the authentication state changes.

* Register the custom authentication state provider in the dependency injection container.

.NET MAUI apps use the `WebAuthenticator` class to initiate browser-based authentication flows that listen for a callback to a specific URL registered with the app.

For additional guidance, see the following resources:

* [Web authenticator (.NET MAUI documentation)](/en-us/dotnet/maui/platform-integration/communication/authentication)
* [`Sample.Server.WebAuthenticator` sample app](https://github.com/dotnet/maui/tree/main/src/Essentials/samples/Sample.Server.WebAuthenticator)

WPF apps use the [Microsoft identity platform](/en-us/entra/identity-platform/) to integrate with Microsoft Entra (ME-ID) and AAD B2C. For guidance and examples, see the following resources:

* [Overview of the Microsoft Authentication Library (MSAL)](/en-us/entra/identity-platform/msal-overview)
* [Add authentication to your Windows (WPF) app](/en-us/azure/developer/mobile-apps/azure-mobile-apps/quickstarts/wpf/authentication)
* [Tutorial: Sign in users and call Microsoft Graph in Windows Presentation Foundation (WPF) desktop app](/en-us/entra/identity-platform/tutorial-v2-windows-desktop)
* [Quickstart: Acquire a token and call Microsoft Graph API from a desktop application](/en-us/entra/identity-platform/index-desktop)
* [Quickstart: Set up sign in for a desktop app using Azure Active Directory B2C](/en-us/azure/active-directory-b2c/quickstart-native-app-desktop)
* [Configure authentication in a sample WPF desktop app by using Azure AD B2C](/en-us/azure/active-directory-b2c/configure-authentication-sample-wpf-desktop-app)

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

Windows Forms apps use the [Microsoft identity platform](/en-us/entra/identity-platform/) to integrate with Microsoft Entra (ME-ID) and AAD B2C. For more information, see [Overview of the Microsoft Authentication Library (MSAL)](/en-us/entra/identity-platform/msal-overview).

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

## Create a custom `AuthenticationStateProvider` without user change updates

If the app authenticates the user immediately after the app launches and the authenticated user remains the same for the entirety of the app lifetime, user change notifications aren't required, and the app only provides information about the authenticated user. In this scenario, the user logs into the app when the app is opened, and the app displays the login screen again after the user logs out. The following `ExternalAuthStateProvider` is an example implementation of a custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) for this authentication scenario.

Note

The following custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

`ExternalAuthStateProvider.cs`:

```
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private readonly Task<AuthenticationState> authenticationState;

    public ExternalAuthStateProvider(AuthenticatedUser user) => 
        authenticationState = Task.FromResult(new AuthenticationState(user.Principal));

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        authenticationState;
}

public class AuthenticatedUser
{
    public ClaimsPrincipal Principal { get; set; } = new();
}
```

The following steps describe how to:

* Add required namespaces.
* Add the authorization services and Blazor abstractions to the service collection.
* Build the service collection.
* Resolve the `AuthenticatedUser` service to set the authenticated user's claims principal. See your identity provider's documentation for details.
* Return the built host.

In the `MauiProgram.CreateMauiApp` method of `MauiProgram.cs`, add namespaces for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) and [System.Security.Claims](/en-us/dotnet/api/system.security.claims):

```
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
```

Remove the following line of code that returns a built [Microsoft.Maui.Hosting.MauiApp](/en-us/dotnet/api/microsoft.maui.hosting.mauiapp):

```
- return builder.Build();
```

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

```
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<AuthenticatedUser>();
var host = builder.Build();

var authenticatedUser = host.Services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;

return host;
```

The following steps describe how to:

* Add required namespaces.
* Add the authorization services and Blazor abstractions to the service collection.
* Build the service collection and add the built service collection as a resource to the app's `ResourceDictionary`.
* Resolve the `AuthenticatedUser` service to set the authenticated user's claims principal. See your identity provider's documentation for details.
* Return the built host.

In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add namespaces for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) and [System.Security.Claims](/en-us/dotnet/api/system.security.claims):

```
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
```

Remove the following line of code that adds the built service collection as a resource to the app's `ResourceDictionary`:

```
- Resources.Add("services", serviceCollection.BuildServiceProvider());
```

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

```
serviceCollection.AddAuthorizationCore();
serviceCollection.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<AuthenticatedUser>();
var services = serviceCollection.BuildServiceProvider();
Resources.Add("services", services);

var authenticatedUser = services.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;
```

The following steps describe how to:

* Add required namespaces.
* Add the authorization services and Blazor abstractions to the service collection.
* Build the service collection and add the built service collection to the app's service provider.
* Resolve the `AuthenticatedUser` service to set the authenticated user's claims principal. See your identity provider's documentation for details.

In the `Form1`'s constructor (`Form1.cs`), add namespaces for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) and [System.Security.Claims](/en-us/dotnet/api/system.security.claims):

```
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
```

Remove the following line of code that sets the built service collection to the app's service provider:

```
- blazorWebView1.Services = services.BuildServiceProvider();
```

Replace the preceding line of code with the following code. Add OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details.

```
services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<AuthenticatedUser>();
var serviceCollection = services.BuildServiceProvider();
blazorWebView1.Services = serviceCollection;

var authenticatedUser = serviceCollection.GetRequiredService<AuthenticatedUser>();

/*
Provide OpenID/MSAL code to authenticate the user. See your identity provider's 
documentation for details.

The user is represented by a new ClaimsPrincipal based on a new ClaimsIdentity.
*/
var user = new ClaimsPrincipal(new ClaimsIdentity());

authenticatedUser.Principal = user;
```

## Create a custom `AuthenticationStateProvider` with user change updates

To update the user while the Blazor app is running, call [NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged) within the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) implementation using ***either*** of the following approaches:

* [Signal an authentication update from outside of the `BlazorWebView`](#signal-an-authentication-update-from-outside-of-the-blazorwebview-option-1))
* [Handle authentication within the `BlazorWebView`](#handle-authentication-within-the-blazorwebview-option-2)

### Signal an authentication update from outside of the `BlazorWebView` (Option 1)

A custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) can use a global service to signal an authentication update. We recommend that the service offer an event that the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) can subscribe to, where the event invokes [NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged).

Note

The following custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

`ExternalAuthStateProvider.cs`:

```
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState currentUser;

    public ExternalAuthStateProvider(ExternalAuthService service)
    {
        currentUser = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            currentUser = new AuthenticationState(newUser);
            NotifyAuthenticationStateChanged(Task.FromResult(currentUser));
        };
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(currentUser);
}

public class ExternalAuthService
{
    public event Action<ClaimsPrincipal>? UserChanged;
    private ClaimsPrincipal? currentUser;

    public ClaimsPrincipal CurrentUser
    {
        get { return currentUser ?? new(); }
        set
        {
            currentUser = value;

            if (UserChanged is not null)
            {
                UserChanged(currentUser);
            }
        }
    }
}
```

In the `MauiProgram.CreateMauiApp` method of `MauiProgram.cs`, add a namespace for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization):

```
using Microsoft.AspNetCore.Components.Authorization;
```

Add the authorization services and Blazor abstractions to the service collection:

```
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
builder.Services.AddSingleton<ExternalAuthService>();
```

In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add a namespace for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization):

```
using Microsoft.AspNetCore.Components.Authorization;
```

Add the authorization services and the Blazor abstractions to the service collection:

```
serviceCollection.AddAuthorizationCore();
serviceCollection.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
serviceCollection.AddSingleton<ExternalAuthService>();
```

In the `Form1`'s constructor (`Form1.cs`), add a namespace for [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization):

```
using Microsoft.AspNetCore.Components.Authorization;
```

Add the authorization services and Blazor abstractions to the service collection:

```
services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
services.AddSingleton<ExternalAuthService>();
```

Wherever the app authenticates a user, resolve the `ExternalAuthService` service:

```
var authService = host.Services.GetRequiredService<ExternalAuthService>();
```

Execute your custom OpenID/MSAL code to authenticate the user. See your identity provider's documentation for details. The authenticated user (`authenticatedUser` in the following example) is a new [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) based on a new [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity).

Set the current user to the authenticated user:

```
authService.CurrentUser = authenticatedUser;
```

An alternative to the preceding approach is to set the user's principal on [System.Threading.Thread.CurrentPrincipal](/en-us/dotnet/api/system.threading.thread.currentprincipal#system-threading-thread-currentprincipal) instead of setting it via a service, which avoids use of the dependency injection container:

```
public class CurrentThreadUserAuthenticationStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(
            new AuthenticationState(Thread.CurrentPrincipal as ClaimsPrincipal ?? 
                new ClaimsPrincipal(new ClaimsIdentity())));
}
```

Using the alternative approach, only authorization services ([AddAuthorizationCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authorizationservicecollectionextensions.addauthorizationcore)) and `CurrentThreadUserAuthenticationStateProvider` (`.AddScoped<AuthenticationStateProvider, CurrentThreadUserAuthenticationStateProvider>()`) are added to the service collection.

### Handle authentication within the `BlazorWebView` (Option 2)

A custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) can include additional methods to trigger log in and log out and update the user.

Note

The following custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) doesn't declare a namespace in order to make the code example applicable to any Blazor Hybrid app. However, a best practice is to provide your app's namespace when you implement the example in a production app.

`ExternalAuthStateProvider.cs`:

```
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;

public class ExternalAuthStateProvider : AuthenticationStateProvider
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(new AuthenticationState(currentUser));

    public Task LogInAsync()
    {
        var loginTask = LogInAsyncCore();
        NotifyAuthenticationStateChanged(loginTask);

        return loginTask;

        async Task<AuthenticationState> LogInAsyncCore()
        {
            var user = await LoginWithExternalProviderAsync();
            currentUser = user;

            return new AuthenticationState(currentUser);
        }
    }

    private Task<ClaimsPrincipal> LoginWithExternalProviderAsync()
    {
        /*
            Provide OpenID/MSAL code to authenticate the user. See your identity 
            provider's documentation for details.

            Return a new ClaimsPrincipal based on a new ClaimsIdentity.
        */
        var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity());

        return Task.FromResult(authenticatedUser);
    }

    public void Logout()
    {
        currentUser = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(currentUser)));
    }
}
```

In the preceding example:

* The call to `LogInAsyncCore` triggers the login process.
* The call to [NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged) notifies that an update is in progress, which allows the app to provide a temporary UI during the login or logout process.
* Returning `loginTask` returns the task so that the component that triggered the login can await and react after the task is complete.
* The `LoginWithExternalProviderAsync` method is implemented by the developer to log in the user with the identity provider's SDK. For more information, see your identity provider's documentation. The authenticated user (`authenticatedUser`) is a new [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) based on a new [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity).

In the `MauiProgram.CreateMauiApp` method of `MauiProgram.cs`, add the authorization services and the Blazor abstraction to the service collection:

```
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
```

In the `MainWindow`'s constructor (`MainWindow.xaml.cs`), add the authorization services and the Blazor abstraction to the service collection:

```
serviceCollection.AddAuthorizationCore();
serviceCollection.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
```

In the `Form1`'s constructor (`Form1.cs`), add the authorization services and the Blazor abstraction to the service collection:

```
services.AddAuthorizationCore();
services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
```

The following `LoginComponent` component demonstrates how to log in a user. In a typical app, the `LoginComponent` component is only shown in a parent component if the user isn't logged into the app.

`Shared/LoginComponent.razor`:

```
@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Login">Log in</button>

@code
{
    public async Task Login()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .LogInAsync();
    }
}
```

The following `LogoutComponent` component demonstrates how to log out a user. In a typical app, the `LogoutComponent` component is only shown in a parent component if the user is logged into the app.

`Shared/LogoutComponent.razor`:

```
@inject AuthenticationStateProvider AuthenticationStateProvider

<button @onclick="Logout">Log out</button>

@code
{
    public async Task Logout()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .Logout();
    }
}
```

## Accessing other authentication information

Blazor doesn't define an abstraction to deal with other credentials, such as access tokens to use for HTTP requests to web APIs. We recommend following the identity provider's guidance to manage the user's credentials with the primitives that the identity provider's SDK provides.

It's common for identity provider SDKs to use a token store for user credentials stored in the device. If the SDK's token store primitive is added to the service container, consume the SDK's primitive within the app.

The Blazor framework isn't aware of a user's authentication credentials and doesn't interact with credentials in any way, so the app's code is free to follow whatever approach you deem most convenient. However, follow the general security guidance in the next section, [Other authentication security considerations](#other-authentication-security-considerations), when implementing authentication code in an app.

## Other authentication security considerations

The authentication process is external to Blazor, and we recommend that developers access the identity provider's guidance for additional security guidance.

When implementing authentication:

* Avoid authentication in the context of the Web View. For example, avoid using a JavaScript OAuth library to perform the authentication flow. In a single-page app, authentication tokens aren't hidden in JavaScript and can be easily discovered by malicious users and used for nefarious purposes. Native apps don't suffer this risk because native apps are only able to obtain tokens outside of the browser context, which means that rogue third-party scripts can't steal the tokens and compromise the app.

* Avoid implementing the authentication workflow yourself. In most cases, platform libraries securely handle the authentication workflow, using the system's browser instead of using a custom Web View that can be hijacked.

* Avoid using the platform's Web View control to perform authentication. Instead, rely on the system's browser when possible.

* Avoid passing the tokens to the document context (JavaScript). In some situations, a JavaScript library within the document is required to perform an authorized call to an external service. Instead of making the token available to JavaScript via JS interop:

  * Provide a generated temporary token to the library and within the Web View.
  * Intercept the outgoing network request in code.
  * Replace the temporary token with the real token and confirm that the destination of the request is valid.

## Related content

* [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0)

* [ASP.NET Core Blazor Hybrid security considerations](security-considerations?view=aspnetcore-10.0)

* Entra ID documentation for .NET MAUI

  * [Tutorial: Register and configure .NET MAUI mobile app in an external tenant](/en-us/entra/external-id/customers/tutorial-mobile-app-maui-sign-in-prepare-tenant)
  * [Sign in users in a sample .NET MAUI Android application](/en-us/entra/external-id/customers/how-to-mobile-app-maui-sample-sign-in)
  * [Sign in users in a sample .NET MAUI desktop application](/en-us/entra/external-id/customers/how-to-desktop-app-maui-sample-sign-in)

* Azure documentation for .NET MAUI

  * [Add authentication to your .NET MAUI app](/en-us/azure/developer/mobile-apps/azure-mobile-apps/quickstarts/maui/authentication)
  * [MAUI mobile or desktop application using Microsoft Entra ID for authentication](https://github.com/Azure-Samples/ms-identity-dotnetcore-maui)

- [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0)
- [ASP.NET Core Blazor Hybrid security considerations](security-considerations?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/60-70
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/60-to-70.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core in .NET 6 to .NET 7

This article explains how to update an existing ASP.NET Core in .NET 6 project to .NET 7.

## Update .NET SDK version in global.json

If you rely on a [global.json](/en-us/dotnet/core/tools/global-json) file to target a specific .NET SDK version, update the `version` property to the .NET 7 SDK version that's installed. For example:

```
{
  "sdk": {
-    "version": "6.0.200"
+    "version": "7.0.100"
  }
}
```

## Update the target framework

Update the project file's [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks) to `net7.0`:

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

  <PropertyGroup>
-    <TargetFramework>net6.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
  </PropertyGroup>

</Project>
```

## Update package references

In the project file, update each [Microsoft.AspNetCore.\*](https://www.nuget.org/packages?q=Microsoft.AspNetCore.*), [Microsoft.EntityFrameworkCore.\*](https://www.nuget.org/packages?q=Microsoft.EntityFrameworkCore.*), [Microsoft.Extensions.\*](https://www.nuget.org/packages?q=Microsoft.Extensions.*), and [System.Net.Http.Json](https://www.nuget.org/packages/System.Net.Http.Json) package reference's `Version` attribute to 7.0.0 or later. For example:

```
<ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="6.0.9" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.9" />
-    <PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
+    <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="7.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
+    <PackageReference Include="System.Net.Http.Json" Version="7.0.0" />
</ItemGroup>
```

## Blazor

### Adopt .NET 7 features

After following the guidance earlier in this article to update an app to .NET 7, adopt specific features by following the links in [What's new in ASP.NET Core in .NET 7](../release-notes/aspnetcore-7.0?view=aspnetcore-10.0#blazor).

To adopt all of the [new 7.0 features for Blazor apps](../release-notes/aspnetcore-7.0?view=aspnetcore-10.0#blazor), we recommend the following process:

* Create a new 7.0 Blazor project from one of the Blazor project templates. For more information, see [Tooling for ASP.NET Core Blazor](../blazor/tooling?view=aspnetcore-10.0).
* Move the app's components and code to the 7.0 app making modifications to adopt the new 7.0 features.

### Simplify component parameter binding

In prior Blazor releases, binding across multiple components required binding to properties with `get`/`set` accessors.

In .NET 6 or earlier:

```
<NestedGrandchild @bind-GrandchildMessage="BoundValue" />

@code {
    ...

    private string BoundValue
    {
        get => ChildMessage ?? string.Empty;
        set => ChildMessageChanged.InvokeAsync(value);
    }
}
```

In .NET 7, you can use the new `@bind:get` and `@bind:set` modifiers to support two-way data binding and simplify the binding syntax:

```
<NestedGrandchild @bind-GrandchildMessage:get="ChildMessage" 
    @bind-GrandchildMessage:set="ChildMessageChanged" />
```

For more information, see the following content in the *Data binding* article:

* [Introduction](../blazor/components/data-binding?view=aspnetcore-7.0)
* [Bind across more than two components](../blazor/components/data-binding?view=aspnetcore-7.0#bind-across-more-than-two-components)

### Migrate unmarshalled JavaScript interop

Unmarshalled interop using the [IJSUnmarshalledRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) interface is obsolete and should be replaced with JavaScript `[JSImport]`/`[JSExport]` interop.

For more information, see [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](../blazor/javascript-interoperability/import-export-interop?view=aspnetcore-10.0).

### Blazor WebAssembly authentication uses history state for redirects

The support for authentication in Blazor WebAssembly apps changed to rely on [navigation history state](../blazor/fundamentals/routing?view=aspnetcore-7.0#navigation-history-state) instead of query strings in the URL. As a result, passing the return URL through the query string fails to redirect back to the original page after a successful login in .NET 7.

The following example demonstrates **the prior redirection approach for apps that target .NET 6 or earlier** in `RedirectToLogin.razor`, which is based on a redirect URL (`?returnUrl=`) with [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto):

```
@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo(
            $"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
    }
}
```

The following example demonstrates **the new redirection approach for apps that target .NET 7 or later** in `RedirectToLogin.razor`, which is based on [navigation history state](../blazor/fundamentals/routing?view=aspnetcore-7.0#navigation-history-state) with [NavigateToLogin](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.navigationmanagerextensions.navigatetologin):

```
@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Options

@inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> OptionsSnapshot
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateToLogin(OptionsSnapshot.Get(Options.DefaultName).AuthenticationPaths.LogInPath);
    }
}
```

As part of this change, [SignOutSessionStateManager](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.signoutsessionstatemanager) is obsolete in .NET 7 or later and replaced with [NavigateToLogout](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.navigationmanagerextensions.navigatetologout).

The following example demonstrates **the prior approach** in `Shared/LoginDisplay.razor` of an app generated from the Blazor WebAssembly project template:

```
@inject SignOutSessionStateManager SignOutManager

...

@code{
    private async Task BeginLogout(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}
```

The following example demonstrates **the new approach** in `Shared/LoginDisplay.razor` that calls [NavigateToLogout](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.navigationmanagerextensions.navigatetologout). The injection (`@inject`) of the [SignOutSessionStateManager](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.signoutsessionstatemanager) is removed from the component's directives at the top of the file, and the `BeginLogOut` method is updated to the following code:

```
@code{
    public void BeginLogOut()
    {
        Navigation.NavigateToLogout("authentication/logout");
    }
}
```

The [SignOutSessionStateManager](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.signoutsessionstatemanager) service registration is removed in `Program.cs`:

```
- builder.Services.AddScoped<SignOutSessionStateManager>();
```

For more information, see the following resources:

* [\[Breaking change\]: Updates to Authentication in webassembly applications](https://github.com/aspnet/Announcements/issues/497)
* [ASP.NET Core Blazor navigation](../blazor/fundamentals/navigation?view=aspnetcore-7.0#navigation-history-state)
* [Secure ASP.NET Core Blazor WebAssembly](../blazor/security/webassembly/?view=aspnetcore-7.0#customize-authorization)
* [ASP.NET Core Blazor authentication and authorization](../blazor/security/?view=aspnetcore-7.0#authorizeview-component)

### .NET WebAssembly build tools for .NET 6 projects

You can now use the .NET WebAssembly build tools with a .NET 6 project when working with the .NET 7 SDK. The new `wasm-tools-net6` workload includes the .NET WebAssembly build tools for .NET 6 projects so that they can be used with the .NET 7 SDK. The existing `wasm-tools` workload installs the .NET WebAssembly build tools for .NET 7 projects. However, the .NET 7 version of the .NET WebAssembly build tools are incompatible with existing projects built with .NET 6. Projects using the .NET WebAssembly build tools that need to support both .NET 6 and .NET 7 must use multi-targeting.

## Update Docker images

For apps using Docker, update your *Dockerfile* `FROM` statements and scripts. Use a base image that includes the ASP.NET Core in .NET 7 runtime. Consider the following `docker pull` command difference between ASP.NET Core in .NET 6 and .NET 7:

```
- docker pull mcr.microsoft.com/dotnet/aspnet:6.0
+ docker pull mcr.microsoft.com/dotnet/aspnet:7.0
```

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/web-api/action-return-types.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Controller action return types in ASP.NET Core web API

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/mvc/action-return-types/) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

ASP.NET Core provides the following options for web API controller action return types:

* [Specific type](#specific-type)
* [IActionResult](#iactionresult-type)
* [ActionResult\<T>](#actionresultt-type)
* [HttpResults](#httpresults-type)

This article explains when it's most appropriate to use each return type.

## Specific type

The most basic action returns a primitive or complex data type, for example, `string` or a custom object. Consider the following action, which returns a collection of custom `Product` objects:

```
[HttpGet]
public Task<List<Product>> Get() =>
    _productContext.Products.OrderBy(p => p.Name).ToListAsync();
```

Without known conditions to safeguard against, returning a specific type could suffice. The preceding action accepts no parameters, so parameter constraints validation isn't needed.

When multiple return types are possible, it's common to mix an [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult) return type with the primitive or complex return type. Either [IActionResult](#iactionresult-type) or [ActionResult\<T>](#actionresultt-type) are necessary to accommodate this type of action. Several samples of multiple return types are provided in this article.

### Return IEnumerable\<T> or IAsyncEnumerable\<T>

See [Return `IEnumerable<T>` or `IAsyncEnumerable<T>`](../fundamentals/best-practices?view=aspnetcore-10.0#return-ienumerablet-or-iasyncenumerablet) for performance considerations.

ASP.NET Core buffers the result of actions that return [IEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.ienumerable-1) before writing them to the response. Consider declaring the action signature's return type as [IAsyncEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1) to guarantee asynchronous iteration. Ultimately, the iteration mode is based on the underlying concrete type being returned and the selected formatter affects how the result is processed:

* When using `System.Text.Json` formatter, MVC relies on the support that `System.Text.Json` added to **stream** the result.
* When using `Newtonsoft.Json` or with `XML-based` formatters the result is buffered.

Consider the following action, which returns sale-priced product records as `IEnumerable<Product>`:

```
[HttpGet("syncsale")]
public IEnumerable<Product> GetOnSaleProducts()
{
    var products = _productContext.Products.OrderBy(p => p.Name).ToList();

    foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}
```

The `IAsyncEnumerable<Product>` equivalent of the preceding action is:

```
[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable();

    await foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}
```

## IActionResult type

The [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) return type is appropriate when multiple `ActionResult` return types are possible in an action. The `ActionResult` types represent various HTTP status codes. Any non-abstract class deriving from `ActionResult` qualifies as a valid return type. Some common return types in this category are [BadRequestResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.badrequestresult) (400), [NotFoundResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.notfoundresult) (404), and [OkObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.okobjectresult) (200). Alternatively, convenience methods in the [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase) class can be used to return `ActionResult` types from an action. For example, `return BadRequest();` is a shorthand form of `return new BadRequestResult();`.

Because there are multiple return types and paths in this type of action, liberal use of the [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attribute is necessary. This attribute produces more descriptive response details for web API help pages generated by tools like [Swagger](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0). `[ProducesResponseType]` indicates the known types and HTTP status codes to be returned by the action.

### Synchronous action

Consider the following synchronous action in which there are two possible return types:

```
[HttpGet("{id}")]
[ProducesResponseType<Product>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById_IActionResult(int id)
{
    var product = _productContext.Products.Find(id);
    return product == null ? NotFound() : Ok(product);
}
```

In the preceding action:

* A 404 status code is returned when the product represented by `id` doesn't exist in the underlying data store. The [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) convenience method is invoked as shorthand for `return new NotFoundResult();`.
* A 200 status code is returned with the `Product` object when the product does exist. The [Ok](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.ok) convenience method is invoked as shorthand for `return new OkObjectResult(product);`.

### Asynchronous action

Consider the following asynchronous action in which there are two possible return types:

```
[HttpPost()]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync_IActionResult(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return BadRequest();
    }

    _productContext.Products.Add(product);
    await _productContext.SaveChangesAsync();

    return CreatedAtAction(nameof(GetById_IActionResult), new { id = product.Id }, product);
}
```

In the preceding action:

* A 400 status code is returned when the product description contains "XYZ Widget". The [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest) convenience method is invoked as shorthand for `return new BadRequestResult();`.

* A 201 status code is generated by the [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) convenience method when a product is created. The following code is an alternative to calling `CreatedAtAction`:

  ```
  return new CreatedAtActionResult(nameof(GetByIdAsync), 
                                  "Products", 
                                  new { id = product.Id }, 
                                  product);
  ```

  In the preceding code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

For example, the following model indicates that requests must include the `Name` and `Description` properties. Failure to provide `Name` and `Description` in the request causes model validation to fail.

```
public class Product
{
    public int Id { get; set; }

    [Required]
    public string Name { get; set; } = string.Empty;

    [Required]
    public string Description { get; set; } = string.Empty;

    public bool IsOnSale { get; set; }
}
```

If the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute is applied, model validation errors result in a 400 status code. For more information, see [Automatic HTTP 400 responses](./?view=aspnetcore-10.0#automatic-http-400-responses).

## ActionResult vs IActionResult

The following section compares `ActionResult` to `IActionResult`.

### ActionResult\<T> type

ASP.NET Core includes the [ActionResult\<T>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1) return type for web API controller actions. It enables returning a type deriving from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult) or return a [specific type](#specific-type). `ActionResult<T>` offers the following benefits over the [IActionResult type](#iactionresult-type):

* The [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attribute's `Type` property can be excluded. For example, `[ProducesResponseType(200, Type = typeof(Product))]` is simplified to `[ProducesResponseType(200)]`. The action's expected return type is inferred from the `T` in `ActionResult<T>`.
* [Implicit cast operators](/en-us/dotnet/csharp/language-reference/keywords/implicit) support the conversion of both `T` and `ActionResult` to `ActionResult<T>`. `T` converts to [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult), which means `return new ObjectResult(T);` is simplified to `return T;`.

C# doesn't support implicit cast operators on interfaces. Consequently, conversion of the interface to a concrete type is necessary to use `ActionResult<T>`. For example, use of `IEnumerable` in the following example doesn't work:

```
[HttpGet]
public ActionResult<IEnumerable<Product>> Get() =>
    _repository.GetProducts();
```

One option to fix the preceding code is to return `_repository.GetProducts().ToList();`.

Most actions have a specific return type. Unexpected conditions can occur during action execution, in which case the specific type isn't returned. For example, an action's input parameter may fail model validation. In such a case, it's common to return the appropriate `ActionResult` type instead of the specific type.

### Synchronous action

Consider a synchronous action in which there are two possible return types:

```
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Product> GetById_ActionResultOfT(int id)
{
    var product = _productContext.Products.Find(id);
    return product == null ? NotFound() : product;
}
```

In the preceding action:

* A 404 status code is returned when the product doesn't exist in the database.
* A 200 status code is returned with the corresponding `Product` object when the product does exist.

### Asynchronous action

Consider an asynchronous action in which there are two possible return types:

```
[HttpPost()]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Product>> CreateAsync_ActionResultOfT(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return BadRequest();
    }

    _productContext.Products.Add(product);
    await _productContext.SaveChangesAsync();

    return CreatedAtAction(nameof(GetById_ActionResultOfT), new { id = product.Id }, product);
}
```

In the preceding action:

* A 400 status code ([BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest)) is returned by the ASP.NET Core runtime when:

  * The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute has been applied and model validation fails.
  * The product description contains "XYZ Widget".

* A 201 status code is generated by the [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) method when a product is created. In this code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

## HttpResults type

In addition to the MVC-specific built-in result types ([IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) and [ActionResult\<T>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1)), ASP.NET Core includes the [HttpResults](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults) types that can be used in both [Minimal APIs](../fundamentals/minimal-apis?view=aspnetcore-10.0) and Web API.

Different than the MVC-specific result types, the `HttpResults`:

* Are a results implementation that is processed by a call to [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync).

* Does ***not*** leverage the configured [Formatters](advanced/formatting?view=aspnetcore-10.0#format-specific-action-results). Not leveraging the configured formatters means:

  * Some features like `Content negotiation` aren't available.
  * The produced `Content-Type` is decided by the `HttpResults` implementation.

The `HttpResults` can be useful when sharing code between Minimal APIs and Web API.

### IResult type

The [Microsoft.AspNetCore.Http.HttpResults](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults) namespace contains classes that implement the [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) interface. The `IResult` interface defines a contract that represents the result of an HTTP endpoint. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class is used to create varying `IResult` objects that represent different types of responses.

The [Built-in results](../fundamentals/minimal-apis?view=aspnetcore-10.0#built-in-results) table shows the common result helpers.

Consider the following code:

```
[HttpGet("{id}")]
[ProducesResponseType<Product>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IResult GetById(int id)
{
    var product = _productContext.Products.Find(id);
    return product == null ? Results.NotFound() : Results.Ok(product);
}
```

In the preceding action:

* A 404 status code is returned when the product doesn't exist in the database.
* A 200 status code is returned with the corresponding `Product` object when the product does exist, generated by the [Results.Ok\<T>()](/en-us/dotnet/api/microsoft.aspnetcore.http.results.ok).

Consider the following code:

```
[HttpPost]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType<Product>(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IResult> CreateAsync(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return Results.BadRequest();
    }

    _productContext.Products.Add(product);
    await _productContext.SaveChangesAsync();

    var location = Url.Action(nameof(CreateAsync), new { id = product.Id }) ?? $"/{product.Id}";
    return Results.Created(location, product);
}
```

In the preceding action:

* A 400 status code is returned when:

  * The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute has been applied and model validation fails.
  * The product description contains "XYZ Widget".

* A 201 status code is generated by the [`Results.Created`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.created) method when a product is created. In this code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

### Results\<TResult1, TResultN> type

The static [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) class returns the concrete `IResult` implementation that allows using `IResult` as return type. The usage of the concrete `IResult` implementation offers the following benefit over the [IResult type](#iresult-type):

* All the [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attributes can be excluded, since the `HttpResult` implementation contributes automatically to the endpoint metadata.

When multiple `IResult` return types are needed, returning [`Results<TResult1, TResultN>`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.results-2) is preferred over returning `IResult`. Returning `Results<TResult1, TResultN>` is preferred because generic union types automatically retain the endpoint metadata.

The `Results<TResult1, TResultN>` union types implement implicit cast operators so that the compiler can automatically convert the types specified in the generic arguments to an instance of the union type. This has the added benefit of providing compile-time checking that a route handler actually only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to `Results<>` results in a compilation error.

Consider the following code:

```
[HttpGet("{id}")]
public Results<NotFound, Ok<Product>> GetById(int id)
{
    var product = _productContext.Products.Find(id);
    return product == null ? TypedResults.NotFound() : TypedResults.Ok(product);
}
```

In the preceding action:

* A 404 status code is returned when the product doesn't exist in the database.
* A 200 status code is returned with the corresponding `Product` object when the product does exist, generated by the [TypedResults.Ok\<T>](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.ok#microsoft-aspnetcore-http-typedresults-ok).

```
[HttpPost]
public async Task<Results<BadRequest, Created<Product>>> CreateAsync(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return TypedResults.BadRequest();
    }

    _productContext.Products.Add(product);
    await _productContext.SaveChangesAsync();

    var location = Url.Action(nameof(CreateAsync), new { id = product.Id }) ?? $"/{product.Id}";
    return TypedResults.Created(location, product);
}
```

In the preceding action:

* A 400 status code is returned when:

  * The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute was applied and model validation fails.
  * The product description contains "XYZ Widget".

* A 201 status code is generated by the [`TypedResults.Created`](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.created) method when a product is created. In this code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

## Related content

* [Handle requests with controllers in ASP.NET Core MVC](../mvc/controllers/actions?view=aspnetcore-10.0)
* [Model validation in ASP.NET Core MVC](../mvc/models/validation?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/mvc/action-return-types/7.x/WebApiSample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

ASP.NET Core provides the following options for web API controller action return types:

* [Specific type](#specific-type)
* [IActionResult](#iactionresult-type)
* [ActionResult\<T>](#actionresultt-type)
* [HttpResults](#httpresults-type)

This article explains when it's most appropriate to use each return type.

## Specific type

The most basic action returns a primitive or complex data type, for example, `string` or a custom object. Consider the following action, which returns a collection of custom `Product` objects:

```
[HttpGet]
public Task<List<Product>> Get() =>
    _productContext.Products.OrderBy(p => p.Name).ToListAsync();
```

Without known conditions to safeguard against, returning a specific type could suffice. The preceding action accepts no parameters, so parameter constraints validation isn't needed.

When multiple return types are possible, it's common to mix an [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult) return type with the primitive or complex return type. Either [IActionResult](#iactionresult-type) or [ActionResult\<T>](#actionresultt-type) are necessary to accommodate this type of action. Several samples of multiple return types are provided in this article.

### Return IEnumerable\<T> or IAsyncEnumerable\<T>

See [Return `IEnumerable<T>` or `IAsyncEnumerable<T>`](../fundamentals/best-practices?view=aspnetcore-10.0#return-ienumerablet-or-iasyncenumerablet) for performance considerations.

ASP.NET Core buffers the result of actions that return [IEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.ienumerable-1) before writing them to the response. Consider declaring the action signature's return type as [IAsyncEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1) to guarantee asynchronous iteration. Ultimately, the iteration mode is based on the underlying concrete type being returned and the selected formatter affects how the result is processed:

* When using `System.Text.Json` formatter, MVC relies on the support that `System.Text.Json` added to **stream** the result.
* When using `Newtonsoft.Json` or with `XML-based` formatters the result is buffered.

Consider the following action, which returns sale-priced product records as `IEnumerable<Product>`:

```
[HttpGet("syncsale")]
public IEnumerable<Product> GetOnSaleProducts()
{
    var products = _productContext.Products.OrderBy(p => p.Name).ToList();

    foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}
```

The `IAsyncEnumerable<Product>` equivalent of the preceding action is:

```
[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable();

    await foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}
```

## IActionResult type

The [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) return type is appropriate when multiple `ActionResult` return types are possible in an action. The `ActionResult` types represent various HTTP status codes. Any non-abstract class deriving from `ActionResult` qualifies as a valid return type. Some common return types in this category are [BadRequestResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.badrequestresult) (400), [NotFoundResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.notfoundresult) (404), and [OkObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.okobjectresult) (200). Alternatively, convenience methods in the [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase) class can be used to return `ActionResult` types from an action. For example, `return BadRequest();` is a shorthand form of `return new BadRequestResult();`.

Because there are multiple return types and paths in this type of action, liberal use of the [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attribute is necessary. This attribute produces more descriptive response details for web API help pages generated by tools like [Swagger](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0). `[ProducesResponseType]` indicates the known types and HTTP status codes to be returned by the action.

### Synchronous action

Consider the following synchronous action in which there are two possible return types:

```
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Product))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById_IActionResult(int id)
{
    var product = _productContext.Products.Find(id);
    return product == null ? NotFound() : Ok(product);
}
```

In the preceding action:

* A 404 status code is returned when the product represented by `id` doesn't exist in the underlying data store. The [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) convenience method is invoked as shorthand for `return new NotFoundResult();`.
* A 200 status code is returned with the `Product` object when the product does exist. The [Ok](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.ok) convenience method is invoked as shorthand for `return new OkObjectResult(product);`.

### Asynchronous action

Consider the following asynchronous action in which there are two possible return types:

```
[HttpPost()]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync_IActionResult(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return BadRequest();
    }

    _productContext.Products.Add(product);
    await _productContext.SaveChangesAsync();

    return CreatedAtAction(nameof(GetById_IActionResult), new { id = product.Id }, product);
}
```

In the preceding action:

* A 400 status code is returned when the product description contains "XYZ Widget". The [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest) convenience method is invoked as shorthand for `return new BadRequestResult();`.

* A 201 status code is generated by the [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) convenience method when a product is created. The following code is an alternative to calling `CreatedAtAction`:

  ```
  return new CreatedAtActionResult(nameof(GetById), 
                                  "Products", 
                                  new { id = product.Id }, 
                                  product);
  ```

  In the preceding code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

For example, the following model indicates that requests must include the `Name` and `Description` properties. Failure to provide `Name` and `Description` in the request causes model validation to fail.

```
public class Product
{
    public int Id { get; set; }

    [Required]
    public string Name { get; set; } = string.Empty;

    [Required]
    public string Description { get; set; } = string.Empty;

    public bool IsOnSale { get; set; }
}
```

If the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute is applied, model validation errors result in a 400 status code. For more information, see [Automatic HTTP 400 responses](./?view=aspnetcore-10.0#automatic-http-400-responses).

## ActionResult vs IActionResult

The following section compares `ActionResult` to `IActionResult`.

### ActionResult\<T> type

ASP.NET Core includes the [ActionResult\<T>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1) return type for web API controller actions. It enables returning a type deriving from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult) or return a [specific type](#specific-type). `ActionResult<T>` offers the following benefits over the [IActionResult type](#iactionresult-type):

* The [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attribute's `Type` property can be excluded. For example, `[ProducesResponseType(200, Type = typeof(Product))]` is simplified to `[ProducesResponseType(200)]`. The action's expected return type is inferred from the `T` in `ActionResult<T>`.
* [Implicit cast operators](/en-us/dotnet/csharp/language-reference/keywords/implicit) support the conversion of both `T` and `ActionResult` to `ActionResult<T>`. `T` converts to [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult), which means `return new ObjectResult(T);` is simplified to `return T;`.

C# doesn't support implicit cast operators on interfaces. Consequently, conversion of the interface to a concrete type is necessary to use `ActionResult<T>`. For example, use of `IEnumerable` in the following example doesn't work:

```
[HttpGet]
public ActionResult<IEnumerable<Product>> Get() =>
    _repository.GetProducts();
```

One option to fix the preceding code is to return `_repository.GetProducts().ToList();`.

Most actions have a specific return type. Unexpected conditions can occur during action execution, in which case the specific type isn't returned. For example, an action's input parameter may fail model validation. In such a case, it's common to return the appropriate `ActionResult` type instead of the specific type.

### Synchronous action

Consider a synchronous action in which there are two possible return types:

```
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Product> GetById_ActionResultOfT(int id)
{
    var product = _productContext.Products.Find(id);
    return product == null ? NotFound() : product;
}
```

In the preceding action:

* A 404 status code is returned when the product doesn't exist in the database.
* A 200 status code is returned with the corresponding `Product` object when the product does exist.

### Asynchronous action

Consider an asynchronous action in which there are two possible return types:

```
[HttpPost()]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Product>> CreateAsync_ActionResultOfT(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return BadRequest();
    }

    _productContext.Products.Add(product);
    await _productContext.SaveChangesAsync();

    return CreatedAtAction(nameof(GetById_ActionResultOfT), new { id = product.Id }, product);
}
```

In the preceding action:

* A 400 status code ([BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest)) is returned by the ASP.NET Core runtime when:

  * The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute has been applied and model validation fails.
  * The product description contains "XYZ Widget".

* A 201 status code is generated by the [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) method when a product is created. In this code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

## HttpResults type

In addition to the MVC-specific built-in result types ([IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) and [ActionResult\<T>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1)), ASP.NET Core includes the [HttpResults](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults) types that can be used in both [Minimal APIs](../fundamentals/minimal-apis?view=aspnetcore-10.0) and Web API.

Different than the MVC-specific result types, the `HttpResults`:

* Are a results implementation that is processed by a call to [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync).

* Does ***not*** leverage the configured [Formatters](advanced/formatting?view=aspnetcore-10.0#format-specific-action-results). Not leveraging the configured formatters means:

  * Some features like `Content negotiation` aren't available.
  * The produced `Content-Type` is decided by the `HttpResults` implementation.

The `HttpResults` can be useful when sharing code between Minimal APIs and Web API.

### IResult type

The [Microsoft.AspNetCore.Http.HttpResults](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults) namespace contains classes that implement the [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) interface. The `IResult` interface defines a contract that represents the result of an HTTP endpoint. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class is used to create varying `IResult` objects that represent different types of responses.

The [Built-in results](/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-7.0\&preserve-view=true#binr7) table shows the common result helpers.

Consider the following code:

```
[HttpGet("{id}")]
[ProducesResponseType(typeof(Product), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IResult GetById(int id)
{
    var product = _productContext.Products.Find(id);
    return product == null ? Results.NotFound() : Results.Ok(product);
}
```

In the preceding action:

* A 404 status code is returned when the product doesn't exist in the database.
* A 200 status code is returned with the corresponding `Product` object when the product does exist, generated by the [Results.Ok\<T>()](/en-us/dotnet/api/microsoft.aspnetcore.http.results.ok).

Consider the following code:

```
[HttpPost]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(typeof(Product), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IResult> CreateAsync(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return Results.BadRequest();
    }

    _productContext.Products.Add(product);
    await _productContext.SaveChangesAsync();

    var location = Url.Action(nameof(GetById), new { id = product.Id }) ?? $"/{product.Id}";
    return Results.Created(location, product);
}
```

In the preceding action:

* A 400 status code is returned when:

  * The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute has been applied and model validation fails.
  * The product description contains "XYZ Widget".

* A 201 status code is generated by the [`Results.Create`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.created) method when a product is created. In this code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

### Results\<TResult1, TResultN> type

The static [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) class returns the concrete `IResult` implementation that allows using `IResult` as return type. The usage of the concrete `IResult` implementation offers the following benefit over the [IResult type](#iresult-type):

* All the [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attributes can be excluded, since the `HttpResult` implementation contributes automatically to the endpoint metadata.

When multiple `IResult` return types are needed, returning [`Results<TResult1, TResultN>`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.results-2) is preferred over returning `IResult`. Returning `Results<TResult1, TResultN>` is preferred because generic union types automatically retain the endpoint metadata.

The `Results<TResult1, TResultN>` union types implement implicit cast operators so that the compiler can automatically convert the types specified in the generic arguments to an instance of the union type. This has the added benefit of providing compile-time checking that a route handler actually only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to `Results<>` results in a compilation error.

Consider the following code:

```
[HttpGet("{id}")]
public Results<NotFound, Ok<Product>> GetById(int id)
{
    var product = _productContext.Products.Find(id);
    return product == null ? TypedResults.NotFound() : TypedResults.Ok(product);
}
```

In the preceding action:

* A 404 status code is returned when the product doesn't exist in the database.
* A 200 status code is returned with the corresponding `Product` object when the product does exist, generated by the [TypedResults.Ok\<T>](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.ok#microsoft-aspnetcore-http-typedresults-ok).

```
[HttpPost]
public async Task<Results<BadRequest, Created<Product>>> CreateAsync(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return TypedResults.BadRequest();
    }

    _productContext.Products.Add(product);
    await _productContext.SaveChangesAsync();

    var location = Url.Action(nameof(GetById), new { id = product.Id }) ?? $"/{product.Id}";
    return TypedResults.Created(location, product);
}
```

In the preceding action:

* A 400 status code is returned when:

  * The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute was applied and model validation fails.
  * The product description contains "XYZ Widget".

* A 201 status code is generated by the [`TypedResults.Create`](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.created) method when a product is created. In this code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

## Additional resources

* [Handle requests with controllers in ASP.NET Core MVC](../mvc/controllers/actions?view=aspnetcore-10.0)
* [Model validation in ASP.NET Core MVC](../mvc/models/validation?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/action-return-types/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

ASP.NET Core offers the following options for web API controller action return types:

* [Specific type](#specific-type)
* [IActionResult](#iactionresult-type)
* [ActionResult\<T>](#actionresultt-type)

This document explains when it's most appropriate to use each return type.

## Specific type

The simplest action returns a primitive or complex data type (for example, `string` or a custom object type). Consider the following action, which returns a collection of custom `Product` objects:

```
[HttpGet]
public List<Product> Get() =>
    _repository.GetProducts();
```

Without known conditions to safeguard against during action execution, returning a specific type could suffice. The preceding action accepts no parameters, so parameter constraints validation isn't needed.

When multiple return types are possible, it's common to mix an [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult) return type with the primitive or complex return type. Either [IActionResult](#iactionresult-type) or [ActionResult\<T>](#actionresultt-type) are necessary to accommodate this type of action. Several samples of multiple return types are provided in this document.

### Return IEnumerable\<T> or IAsyncEnumerable\<T>

ASP.NET Core buffers the result of actions that return [IEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.ienumerable-1) before writing them to the response. Consider declaring the action signature's return type as [IAsyncEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1) to guarantee asynchronous iteration. Ultimately, the iteration mode is based on the underlying concrete type being returned. MVC automatically buffers any concrete type that implements `IAsyncEnumerable<T>`.

Consider the following action, which returns sale-priced product records as `IEnumerable<Product>`:

```
[HttpGet("syncsale")]
public IEnumerable<Product> GetOnSaleProducts()
{
    var products = _repository.GetProducts();

    foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}
```

The `IAsyncEnumerable<Product>` equivalent of the preceding action is:

```
[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _repository.GetProductsAsync();

    await foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}
```

## IActionResult type

The [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) return type is appropriate when multiple `ActionResult` return types are possible in an action. The `ActionResult` types represent various HTTP status codes. Any non-abstract class deriving from `ActionResult` qualifies as a valid return type. Some common return types in this category are [BadRequestResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.badrequestresult) (400), [NotFoundResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.notfoundresult) (404), and [OkObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.okobjectresult) (200). Alternatively, convenience methods in the [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase) class can be used to return `ActionResult` types from an action. For example, `return BadRequest();` is a shorthand form of `return new BadRequestResult();`.

Because there are multiple return types and paths in this type of action, liberal use of the [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attribute is necessary. This attribute produces more descriptive response details for web API help pages generated by tools like [Swagger](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0). `[ProducesResponseType]` indicates the known types and HTTP status codes to be returned by the action.

### Synchronous action

Consider the following synchronous action in which there are two possible return types:

```
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Product))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
    if (!_repository.TryGetProduct(id, out var product))
    {
        return NotFound();
    }

    return Ok(product);
}
```

In the preceding action:

* A 404 status code is returned when the product represented by `id` doesn't exist in the underlying data store. The [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) convenience method is invoked as shorthand for `return new NotFoundResult();`.
* A 200 status code is returned with the `Product` object when the product does exist. The [Ok](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.ok) convenience method is invoked as shorthand for `return new OkObjectResult(product);`.

### Asynchronous action

Consider the following asynchronous action in which there are two possible return types:

```
[HttpPost]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return BadRequest();
    }

    await _repository.AddProductAsync(product);

    return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
```

In the preceding action:

* A 400 status code is returned when the product description contains "XYZ Widget". The [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest) convenience method is invoked as shorthand for `return new BadRequestResult();`.
* A 201 status code is generated by the [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) convenience method when a product is created. An alternative to calling `CreatedAtAction` is `return new CreatedAtActionResult(nameof(GetById), "Products", new { id = product.Id }, product);`. In this code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

For example, the following model indicates that requests must include the `Name` and `Description` properties. Failure to provide `Name` and `Description` in the request causes model validation to fail.

```
public class Product
{
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    public string Description { get; set; }

    public bool IsOnSale { get; set; }
}
```

If the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute is applied, model validation errors result in a 400 status code. For more information, see [Automatic HTTP 400 responses](./?view=aspnetcore-10.0#automatic-http-400-responses).

## ActionResult vs IActionResult

The following section compares `ActionResult` to `IActionResult`.

### ActionResult\<T> type

ASP.NET Core includes the [ActionResult\<T>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1) return type for web API controller actions. It enables you to return a type deriving from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult) or return a [specific type](#specific-type). `ActionResult<T>` offers the following benefits over the [IActionResult type](#iactionresult-type):

* The [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attribute's `Type` property can be excluded. For example, `[ProducesResponseType(200, Type = typeof(Product))]` is simplified to `[ProducesResponseType(200)]`. The action's expected return type is instead inferred from the `T` in `ActionResult<T>`.
* [Implicit cast operators](/en-us/dotnet/csharp/language-reference/keywords/implicit) support the conversion of both `T` and `ActionResult` to `ActionResult<T>`. `T` converts to [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult), which means `return new ObjectResult(T);` is simplified to `return T;`.

C# doesn't support implicit cast operators on interfaces. Consequently, conversion of the interface to a concrete type is necessary to use `ActionResult<T>`. For example, use of `IEnumerable` in the following example doesn't work:

```
[HttpGet]
public ActionResult<IEnumerable<Product>> Get() =>
    _repository.GetProducts();
```

One option to fix the preceding code is to return `_repository.GetProducts().ToList();`.

Most actions have a specific return type. Unexpected conditions can occur during action execution, in which case the specific type isn't returned. For example, an action's input parameter may fail model validation. In such a case, it's common to return the appropriate `ActionResult` type instead of the specific type.

### Synchronous action

Consider a synchronous action in which there are two possible return types:

```
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Product> GetById(int id)
{
    if (!_repository.TryGetProduct(id, out var product))
    {
        return NotFound();
    }

    return product;
}
```

In the preceding action:

* A 404 status code is returned when the product doesn't exist in the database.
* A 200 status code is returned with the corresponding `Product` object when the product does exist.

### Asynchronous action

Consider an asynchronous action in which there are two possible return types:

```
[HttpPost]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Product>> CreateAsync(Product product)
{
    if (product.Description.Contains("XYZ Widget"))
    {
        return BadRequest();
    }

    await _repository.AddProductAsync(product);

    return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
```

In the preceding action:

* A 400 status code ([BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest)) is returned by the ASP.NET Core runtime when:

  * The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute has been applied and model validation fails.
  * The product description contains "XYZ Widget".

* A 201 status code is generated by the [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) method when a product is created. In this code path, the `Product` object is provided in the response body. A `Location` response header containing the newly created product's URL is provided.

## Additional resources

* [Handle requests with controllers in ASP.NET Core MVC](../mvc/controllers/actions?view=aspnetcore-10.0)
* [Model validation in ASP.NET Core MVC](../mvc/models/validation?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/getting-started-with-swashbuckle.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Get started with Swashbuckle and ASP.NET Core

Note

In .NET 9 and later, ASP.NET Core includes built-in OpenAPI support. Swashbuckle is no longer included by default, but it remains available as a community package you can add manually to ASP.NET Core projects targeting .NET 9 or later.

• To understand the built‑in OpenAPI features, see [Overview of OpenAPI support in ASP.NET Core API apps](../fundamentals/openapi/overview?view=aspnetcore-8.0\&preserve-view=true\&view=aspnetcore-9.0).\
• To add and use the Swagger UI provided by the `Swashbuckle.AspNetCore.SwaggerUI` package for interactive exploration or local ad‑hoc testing, see [Use the generated OpenAPI documents](../fundamentals/openapi/using-openapi-documents?view=aspnetcore-8.0#use-swagger-ui-for-local-ad-hoc-testing).

The following instructions apply when using Swashbuckle with .NET versions earlier than 9.

There are three main components to Swashbuckle:

* [Swashbuckle.AspNetCore.Swagger](https://www.nuget.org/packages/Swashbuckle.AspNetCore.Swagger/): a Swagger object model and middleware to expose `SwaggerDocument` objects as JSON endpoints.

* [Swashbuckle.AspNetCore.SwaggerGen](https://www.nuget.org/packages/Swashbuckle.AspNetCore.SwaggerGen/): a Swagger generator that builds `SwaggerDocument` objects directly from your routes, controllers, and models. It's typically combined with the Swagger endpoint middleware to automatically expose Swagger JSON.

* [Swashbuckle.AspNetCore.SwaggerUI](https://www.nuget.org/packages/Swashbuckle.AspNetCore.SwaggerUI/): an embedded version of the Swagger UI tool. It interprets Swagger JSON to build a rich, customizable experience for describing the web API functionality. It includes built-in test harnesses for the public methods.

## Package installation

Swashbuckle can be added with the following approaches:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [.NET CLI](#tabpanel_1_net-cli)

- From the **Package Manager Console** window:

  * Go to **View** > **Other Windows** > **Package Manager Console**

  * Navigate to the directory in which the `.csproj` file exists

  * Execute the following command:

    ```
    Install-Package Swashbuckle.AspNetCore -Version 6.6.2
    ```

- From the **Manage NuGet Packages** dialog:

  * Right-click the project in **Solution Explorer** > **Manage NuGet Packages**
  * Set the **Package source** to "nuget.org"
  * Ensure the "Include prerelease" option is enabled
  * Enter "Swashbuckle.AspNetCore" in the search box
  * Select the latest "Swashbuckle.AspNetCore" package from the **Browse** tab and click **Install**

Run the following command from the **Integrated Terminal**:

```
dotnet add TodoApi.csproj package Swashbuckle.AspNetCore -v 6.6.2
```

Run the following command:

```
dotnet add TodoApi.csproj package Swashbuckle.AspNetCore -v 6.6.2
```

## Add and configure Swagger middleware

Add the Swagger generator to the services collection in `Program.cs`:

```
builder.Services.AddControllers();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
```

The call to [AddEndpointsApiExplorer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.endpointmetadataapiexplorerservicecollectionextensions.addendpointsapiexplorer) shown in the preceding example is required only for [Minimal APIs](/en-us/aspnet/core/fundamentals/apis). For more information, see [this StackOverflow post](https://stackoverflow.com/a/71933535).

Enable the middleware for serving the generated JSON document and the Swagger UI, also in `Program.cs`:

```
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
```

The preceding code adds the Swagger middleware only if the current environment is set to `Development`. The `UseSwaggerUI` method call enables an embedded version of the Swagger UI tool.

Launch the app and navigate to `https://localhost:<port>/swagger/v1/swagger.json`. The generated document describing the endpoints appears as shown in [OpenAPI specification (openapi.json)](web-api-help-pages-using-swagger?view=aspnetcore-8.0#openapi-specification-openapijson).

The Swagger UI can be found at `https://localhost:<port>/swagger`. Explore the API via Swagger UI and incorporate it in other programs.

Tip

To serve the Swagger UI at the app's root (`https://localhost:<port>/`), set the `RoutePrefix` property to an empty string:

```
if (builder.Environment.IsDevelopment())
{
    app.UseSwaggerUI(options => // UseSwaggerUI is called only in Development.
    {
        options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
        options.RoutePrefix = string.Empty;
    });
}
```

If using directories with IIS or a reverse proxy, set the Swagger endpoint to a relative path using the `./` prefix. For example, `./swagger/v1/swagger.json`. Using `/swagger/v1/swagger.json` instructs the app to look for the JSON file at the true root of the URL (plus the route prefix, if used). For example, use `https://localhost:<port>/<route_prefix>/swagger/v1/swagger.json` instead of `https://localhost:<port>/<virtual_directory>/<route_prefix>/swagger/v1/swagger.json`.

Note

By default, Swashbuckle generates and exposes Swagger JSON in version 3.0 of the specification—officially called the OpenAPI Specification. To support backwards compatibility, you can opt into exposing JSON in the 2.0 format instead. This 2.0 format is important for integrations such as Microsoft Power Apps and Microsoft Flow that currently support OpenAPI version 2.0. To opt into the 2.0 format, set the `SerializeAsV2` property in `Program.cs`:

```
app.UseSwagger(options =>
{
    options.SerializeAsV2 = true;
});
```

## Customize and extend

Swagger provides options for documenting the object model and customizing the UI to match your theme.

### API info and description

The configuration action passed to the `AddSwaggerGen` method adds information such as the author, license, and description.

In `Program.cs`, import the following namespace to use the `OpenApiInfo` class:

```
using Microsoft.OpenApi.Models;
```

Using the `OpenApiInfo` class, modify the information displayed in the UI:

```
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "ToDo API",
        Description = "An ASP.NET Core Web API for managing ToDo items",
        TermsOfService = new Uri("https://example.com/terms"),
        Contact = new OpenApiContact
        {
            Name = "Example Contact",
            Url = new Uri("https://example.com/contact")
        },
        License = new OpenApiLicense
        {
            Name = "Example License",
            Url = new Uri("https://example.com/license")
        }
    });
});
```

The Swagger UI displays the version's information:

### XML comments

XML comments can be enabled with the following approaches:

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [.NET CLI](#tabpanel_2_net-cli)

- Right-click the project in **Solution Explorer** and select *`Edit <project_name>.csproj`*.
- Add [GenerateDocumentationFile](/en-us/dotnet/core/project-sdk/msbuild-props#generatedocumentationfile) to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```

Add [GenerateDocumentationFile](/en-us/dotnet/core/project-sdk/msbuild-props#generatedocumentationfile) to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```

Add [GenerateDocumentationFile](/en-us/dotnet/core/project-sdk/msbuild-props#generatedocumentationfile) to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```

Enabling XML comments provides debug information for undocumented public types and members. Undocumented types and members are indicated by the warning message. For example, the following message indicates a violation of warning code 1591:

```
warning CS1591: Missing XML comment for publicly visible type or member 'TodoController'
```

To suppress warnings project-wide, define a semicolon-delimited list of warning codes to ignore in the project file. Appending the warning codes to `$(NoWarn);` applies the [C# default values](https://github.com/dotnet/sdk/blob/2eb6c546931b5bcb92cd3128b93932a980553ea1/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.CSharp.props#L16) too.

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

To suppress warnings only for specific members, enclose the code in [#pragma warning](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning) preprocessor directives. This approach is useful for code that shouldn't be exposed via the API docs. In the following example, warning code CS1591 is ignored for the entire `TodoContext` class. Enforcement of the warning code is restored at the close of the class definition. Specify multiple warning codes with a comma-delimited list.

```
namespace SwashbuckleSample.Models;

#pragma warning disable CS1591
public class TodoContext : DbContext
{
    public TodoContext(DbContextOptions<TodoContext> options) : base(options) { }

    public DbSet<TodoItem> TodoItems => Set<TodoItem>();
}
#pragma warning restore CS1591
```

Configure Swagger to use the XML file that's generated with the preceding instructions. For Linux or non-Windows operating systems, file names and paths can be case-sensitive. For example, a `TodoApi.XML` file is valid on Windows but not Ubuntu.

```
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "ToDo API",
        Description = "An ASP.NET Core Web API for managing ToDo items",
        TermsOfService = new Uri("https://example.com/terms"),
        Contact = new OpenApiContact
        {
            Name = "Example Contact",
            Url = new Uri("https://example.com/contact")
        },
        License = new OpenApiLicense
        {
            Name = "Example License",
            Url = new Uri("https://example.com/license")
        }
    });

    // using System.Reflection;
    var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});
```

In the preceding code, [Reflection](/en-us/dotnet/csharp/programming-guide/concepts/reflection) is used to build an XML file name matching that of the web API project. The [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory) property is used to construct a path to the XML file. Some Swagger features (for example, schemata of input parameters or HTTP methods and response codes from the respective attributes) work without the use of an XML documentation file. For most features, namely method summaries and the descriptions of parameters and response codes, the use of an XML file is mandatory.

Adding triple-slash comments to an action enhances the Swagger UI by adding the description to the section header. Add a [\<summary>](/en-us/dotnet/csharp/programming-guide/xmldoc/summary) element above the `Delete` action:

```
/// <summary>
/// Deletes a specific TodoItem.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
    var item = await _context.TodoItems.FindAsync(id);

    if (item is null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(item);
    await _context.SaveChangesAsync();

    return NoContent();
}
```

The Swagger UI displays the inner text of the preceding code's `<summary>` element:

The UI is driven by the generated JSON schema:

```
"delete": {
    "tags": [
        "Todo"
    ],
    "summary": "Deletes a specific TodoItem.",
    "parameters": [
        {
            "name": "id",
            "in": "path",
            "description": "",
            "required": true,
            "schema": {
                "type": "integer",
                "format": "int64"
            }
        }
    ],
    "responses": {
        "200": {
            "description": "Success"
        }
    }
},
```

Add a [\<remarks>](/en-us/dotnet/csharp/programming-guide/xmldoc/remarks) element to the `Create` action method documentation. It supplements information specified in the `<summary>` element and provides a more robust Swagger UI. The `<remarks>` element content can consist of text, JSON, or XML.

```
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <remarks>
/// Sample request:
///
///     POST /Todo
///     {
///        "id": 1,
///        "name": "Item #1",
///        "isComplete": true
///     }
///
/// </remarks>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(TodoItem item)
{
    _context.TodoItems.Add(item);
    await _context.SaveChangesAsync();

    return CreatedAtAction(nameof(Get), new { id = item.Id }, item);
}
```

Notice the UI enhancements with these additional comments:

### Data annotations

Mark the model with attributes, found in the [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace, to help drive the Swagger UI components.

Add the `[Required]` attribute to the `Name` property of the `TodoItem` class:

```
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace SwashbuckleSample.Models;

public class TodoItem
{
    public long Id { get; set; }

    [Required]
    public string Name { get; set; } = null!;

    [DefaultValue(false)]
    public bool IsComplete { get; set; }
}
```

The presence of this attribute changes the UI behavior and alters the underlying JSON schema:

```
"schemas": {
    "TodoItem": {
        "required": [
            "name"
        ],
        "type": "object",
        "properties": {
            "id": {
                "type": "integer",
                "format": "int64"
            },
            "name": {
                "type": "string"
            },
            "isComplete": {
                "type": "boolean",
                "default": false
            }
        },
        "additionalProperties": false
    }
},
```

Add the `[Produces("application/json")]` attribute to the API controller. Its purpose is to declare that the controller's actions support a response content type of *application/json*:

```
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class TodoController : ControllerBase
{
```

The **Media type** drop-down selects this content type as the default for the controller's GET actions:

As the usage of data annotations in the web API increases, the UI and API help pages become more descriptive and useful.

### Describe response types

Developers consuming a web API are most concerned with what's returned—specifically response types and error codes (if not standard). The response types and error codes are denoted in the XML comments and data annotations.

The `Create` action returns an HTTP 201 status code on success. An HTTP 400 status code is returned when the posted request body is null. Without proper documentation in the Swagger UI, the consumer lacks knowledge of these expected outcomes. Fix that problem by adding the highlighted lines in the following example:

```
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <remarks>
/// Sample request:
///
///     POST /Todo
///     {
///        "id": 1,
///        "name": "Item #1",
///        "isComplete": true
///     }
///
/// </remarks>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(TodoItem item)
{
    _context.TodoItems.Add(item);
    await _context.SaveChangesAsync();

    return CreatedAtAction(nameof(Get), new { id = item.Id }, item);
}
```

The Swagger UI now clearly documents the expected HTTP response codes:

Conventions can be used as an alternative to explicitly decorating individual actions with `[ProducesResponseType]`. For more information, see [Use web API conventions](../web-api/advanced/conventions?view=aspnetcore-8.0).

To support the `[ProducesResponseType]` decoration, the [Swashbuckle.AspNetCore.Annotations](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/README.md#swashbuckleaspnetcoreannotations) package offers extensions to enable and enrich the response, schema, and parameter metadata.

### Customize the UI

The default UI is both functional and presentable. However, API documentation pages should represent your brand or theme. Branding the Swashbuckle components requires adding the resources to serve static files and building the folder structure to host those files.

Enable static file middleware:

```
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapControllers();
```

To inject additional CSS stylesheets, add them to the project's *wwwroot* folder and specify the relative path in the middleware options:

```
if (app.Environment.IsDevelopment())
{
    app.UseSwaggerUI(options => // UseSwaggerUI is called only in Development.
    {
        options.InjectStylesheet("/swagger-ui/custom.css");
    });
}
```

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/web-api-help-pages-using-swagger/samples/) ([how to download](../fundamentals/?view=aspnetcore-8.0#how-to-download-a-sample))
* [Improve the developer experience of an API with Swagger documentation](/en-us/training/modules/improve-api-developer-experience-with-swagger/)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/web-api-help-pages-using-swagger/samples/) ([how to download](../fundamentals/?view=aspnetcore-8.0#how-to-download-a-sample))

There are three main components to Swashbuckle:

* [Swashbuckle.AspNetCore.Swagger](https://www.nuget.org/packages/Swashbuckle.AspNetCore.Swagger/): a Swagger object model and middleware to expose `SwaggerDocument` objects as JSON endpoints.

* [Swashbuckle.AspNetCore.SwaggerGen](https://www.nuget.org/packages/Swashbuckle.AspNetCore.SwaggerGen/): a Swagger generator that builds `SwaggerDocument` objects directly from your routes, controllers, and models. It's typically combined with the Swagger endpoint middleware to automatically expose Swagger JSON.

* [Swashbuckle.AspNetCore.SwaggerUI](https://www.nuget.org/packages/Swashbuckle.AspNetCore.SwaggerUI/): an embedded version of the Swagger UI tool. It interprets Swagger JSON to build a rich, customizable experience for describing the web API functionality. It includes built-in test harnesses for the public methods.

## Package installation

Swashbuckle can be added with the following approaches:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [.NET CLI](#tabpanel_3_net-cli)

- From the **Package Manager Console** window:

  * Go to **View** > **Other Windows** > **Package Manager Console**

  * Navigate to the directory in which the `TodoApi.csproj` file exists

  * Execute the following command:

    ```
    Install-Package Swashbuckle.AspNetCore -Version 5.6.3
    ```

- From the **Manage NuGet Packages** dialog:

  * Right-click the project in **Solution Explorer** > **Manage NuGet Packages**
  * Set the **Package source** to "nuget.org"
  * Ensure the "Include prerelease" option is enabled
  * Enter "Swashbuckle.AspNetCore" in the search box
  * Select the latest "Swashbuckle.AspNetCore" package from the **Browse** tab and click **Install**

Run the following command from the **Integrated Terminal**:

```
dotnet add TodoApi.csproj package Swashbuckle.AspNetCore -v 5.6.3
```

Run the following command:

```
dotnet add TodoApi.csproj package Swashbuckle.AspNetCore -v 5.6.3
```

## Add and configure Swagger middleware

Add the Swagger generator to the services collection in the `Startup.ConfigureServices` method:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(opt =>
        opt.UseInMemoryDatabase("TodoList"));
    services.AddControllers();

    // Register the Swagger generator, defining 1 or more Swagger documents
    services.AddSwaggerGen();
}
```

In the `Startup.Configure` method, enable the middleware for serving the generated JSON document and the Swagger UI:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        // Enable middleware to serve generated Swagger as a JSON endpoint.
        app.UseSwagger();

        // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.)
        app.UseSwaggerUI();
    }

    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

Note

Swashbuckle relies on MVC's [Microsoft.AspNetCore.Mvc.ApiExplorer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer) to discover the routes and endpoints. If the project calls [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc), routes and endpoints are discovered automatically. When calling [AddMvcCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccoreservicecollectionextensions.addmvccore), the [AddApiExplorer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcapiexplorermvccorebuilderextensions.addapiexplorer) method must be explicitly called. For more information, see [Swashbuckle, ApiExplorer, and Routing](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#swashbuckle-apiexplorer-and-routing).

In development, the preceding `UseSwaggerUI` method call enables an embedded version of the Swagger UI tool. This depends on the [static file middleware](../fundamentals/static-files?view=aspnetcore-8.0). If targeting .NET Framework or .NET Core 1.x, add the [Microsoft.AspNetCore.StaticFiles](https://www.nuget.org/packages/Microsoft.AspNetCore.StaticFiles/) NuGet package to the project.

Launch the app, and navigate to `http://localhost:<port>/swagger/v1/swagger.json`. The generated document describing the endpoints appears as shown in [OpenAPI specification (openapi.json)](web-api-help-pages-using-swagger?view=aspnetcore-8.0#openapi-specification-openapijson).

The Swagger UI can be found at `http://localhost:<port>/swagger`. Explore the API via Swagger UI and incorporate it in other programs.

Tip

To serve the Swagger UI at the app's root (`http://localhost:<port>/`), set the `RoutePrefix` property to an empty string:

```
// // UseSwaggerUI Protected by if (env.IsDevelopment())
app.UseSwaggerUI(c => // UseSwaggerUI Protected by if (env.IsDevelopment())
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    c.RoutePrefix = string.Empty;
});
```

If using directories with IIS or a reverse proxy, set the Swagger endpoint to a relative path using the `./` prefix. For example, `./swagger/v1/swagger.json`. Using `/swagger/v1/swagger.json` instructs the app to look for the JSON file at the true root of the URL (plus the route prefix, if used). For example, use `http://localhost:<port>/<route_prefix>/swagger/v1/swagger.json` instead of `http://localhost:<port>/<virtual_directory>/<route_prefix>/swagger/v1/swagger.json`.

Note

By default, Swashbuckle generates and exposes Swagger JSON in version 3.0 of the specification—officially called the OpenAPI Specification. To support backwards compatibility, you can opt into exposing JSON in the 2.0 format instead. This 2.0 format is important for integrations such as Microsoft Power Apps and Microsoft Flow that currently support OpenAPI version 2.0. To opt into the 2.0 format, set the `SerializeAsV2` property in `Startup.Configure`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        // Enable middleware to serve generated Swagger as a JSON endpoint.
        app.UseSwagger(c =>
        {
            c.SerializeAsV2 = true;
        });

        // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
        // specifying the Swagger JSON endpoint.
        // UseSwaggerUI is called only in Development.
        app.UseSwaggerUI(c => // UseSwaggerUI Protected by if (env.IsDevelopment())
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
        });
    }

    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

## Customize and extend

Swagger provides options for documenting the object model and customizing the UI to match your theme.

In the `Startup` class, add the following namespaces:

```
using System;
using System.Reflection;
using System.IO;
```

### API info and description

The configuration action passed to the `AddSwaggerGen` method adds information such as the author, license, and description:

In the `Startup` class, import the following namespace to use the `OpenApiInfo` class:

```
using Microsoft.OpenApi.Models;
```

Using the `OpenApiInfo` class, modify the information displayed in the UI:

```
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "ToDo API",
        Description = "A simple example ASP.NET Core Web API",
        TermsOfService = new Uri("https://example.com/terms"),
        Contact = new OpenApiContact
        {
            Name = "Shayne Boyer",
            Email = string.Empty,
            Url = new Uri("https://twitter.com/spboyer"),
        },
        License = new OpenApiLicense
        {
            Name = "Use under LICX",
            Url = new Uri("https://example.com/license"),
        }
    });
});
```

The Swagger UI displays the version's information:

### XML comments

XML comments can be enabled with the following approaches:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)
* [.NET CLI](#tabpanel_4_net-cli)

- Right-click the project in **Solution Explorer** and select *`Edit <project_name>.csproj`*.
- Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

Enabling XML comments provides debug information for undocumented public types and members. Undocumented types and members are indicated by the warning message. For example, the following message indicates a violation of warning code 1591:

```
warning CS1591: Missing XML comment for publicly visible type or member 'TodoController.GetAll()'
```

To suppress warnings project-wide, define a semicolon-delimited list of warning codes to ignore in the project file. Appending the warning codes to `$(NoWarn);` applies the [C# default values](https://github.com/dotnet/sdk/blob/2eb6c546931b5bcb92cd3128b93932a980553ea1/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.CSharp.props#L16) too.

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

To suppress warnings only for specific members, enclose the code in [#pragma warning](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning) preprocessor directives. This approach is useful for code that shouldn't be exposed via the API docs. In the following example, warning code CS1591 is ignored for the entire `Program` class. Enforcement of the warning code is restored at the close of the class definition. Specify multiple warning codes with a comma-delimited list.

```
namespace TodoApi
{
#pragma warning disable CS1591
    public class Program
    {
        public static void Main(string[] args) =>
            BuildWebHost(args).Run();

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }
#pragma warning restore CS1591
}
```

Configure Swagger to use the XML file that's generated with the preceding instructions. For Linux or non-Windows operating systems, file names and paths can be case-sensitive. For example, a `TodoApi.XML` file is valid on Windows but not Ubuntu.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(opt =>
        opt.UseInMemoryDatabase("TodoList"));
    services.AddControllers();

    // Register the Swagger generator, defining 1 or more Swagger documents
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo
        {
            Version = "v1",
            Title = "ToDo API",
            Description = "A simple example ASP.NET Core Web API",
            TermsOfService = new Uri("https://example.com/terms"),
            Contact = new OpenApiContact
            {
                Name = "Shayne Boyer",
                Email = string.Empty,
                Url = new Uri("https://twitter.com/spboyer"),
            },
            License = new OpenApiLicense
            {
                Name = "Use under LICX",
                Url = new Uri("https://example.com/license"),
            }
        });

        // Set the comments path for the Swagger JSON and UI.
        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        c.IncludeXmlComments(xmlPath);
    });
}
```

In the preceding code, [Reflection](/en-us/dotnet/csharp/programming-guide/concepts/reflection) is used to build an XML file name matching that of the web API project. The [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory) property is used to construct a path to the XML file. Some Swagger features (for example, schemata of input parameters or HTTP methods and response codes from the respective attributes) work without the use of an XML documentation file. For most features, namely method summaries and the descriptions of parameters and response codes, the use of an XML file is mandatory.

Adding triple-slash comments to an action enhances the Swagger UI by adding the description to the section header. Add a [\<summary>](/en-us/dotnet/csharp/programming-guide/xmldoc/summary) element above the `Delete` action:

```
/// <summary>
/// Deletes a specific TodoItem.
/// </summary>
/// <param name="id"></param>        
[HttpDelete("{id}")]
public IActionResult Delete(long id)
{
    var todo = _context.TodoItems.Find(id);

    if (todo == null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(todo);
    _context.SaveChanges();

    return NoContent();
}
```

The Swagger UI displays the inner text of the preceding code's `<summary>` element:

The UI is driven by the generated JSON schema:

```
"delete": {
    "tags": [
        "Todo"
    ],
    "summary": "Deletes a specific TodoItem.",
    "operationId": "ApiTodoByIdDelete",
    "consumes": [],
    "produces": [],
    "parameters": [
        {
            "name": "id",
            "in": "path",
            "description": "",
            "required": true,
            "type": "integer",
            "format": "int64"
        }
    ],
    "responses": {
        "200": {
            "description": "Success"
        }
    }
}
```

Add a [\<remarks>](/en-us/dotnet/csharp/programming-guide/xmldoc/remarks) element to the `Create` action method documentation. It supplements information specified in the `<summary>` element and provides a more robust Swagger UI. The `<remarks>` element content can consist of text, JSON, or XML.

```
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
///     POST /Todo
///     {
///        "id": 1,
///        "name": "Item1",
///        "isComplete": true
///     }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>            
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<TodoItem> Create(TodoItem item)
{
    _context.TodoItems.Add(item);
    _context.SaveChanges();

    return CreatedAtRoute("GetTodo", new { id = item.Id }, item);
}
```

Notice the UI enhancements with these additional comments:

### Data annotations

Mark the model with attributes, found in the [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace, to help drive the Swagger UI components.

Add the `[Required]` attribute to the `Name` property of the `TodoItem` class:

```
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace TodoApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }

        [Required]
        public string Name { get; set; }

        [DefaultValue(false)]
        public bool IsComplete { get; set; }
    }
}
```

The presence of this attribute changes the UI behavior and alters the underlying JSON schema:

```
"definitions": {
    "TodoItem": {
        "required": [
            "name"
        ],
        "type": "object",
        "properties": {
            "id": {
                "format": "int64",
                "type": "integer"
            },
            "name": {
                "type": "string"
            },
            "isComplete": {
                "default": false,
                "type": "boolean"
            }
        }
    }
},
```

Add the `[Produces("application/json")]` attribute to the API controller. Its purpose is to declare that the controller's actions support a response content type of *application/json*:

```
[Produces("application/json")]
[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
    private readonly TodoContext _context;
```

The **Response Content Type** drop-down selects this content type as the default for the controller's GET actions:

As the usage of data annotations in the web API increases, the UI and API help pages become more descriptive and useful.

### Describe response types

Developers consuming a web API are most concerned with what's returned—specifically response types and error codes (if not standard). The response types and error codes are denoted in the XML comments and data annotations.

The `Create` action returns an HTTP 201 status code on success. An HTTP 400 status code is returned when the posted request body is null. Without proper documentation in the Swagger UI, the consumer lacks knowledge of these expected outcomes. Fix that problem by adding the highlighted lines in the following example:

```
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <remarks>
/// Sample request:
///
///     POST /Todo
///     {
///        "id": 1,
///        "name": "Item1",
///        "isComplete": true
///     }
///
/// </remarks>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>            
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<TodoItem> Create(TodoItem item)
{
    _context.TodoItems.Add(item);
    _context.SaveChanges();

    return CreatedAtRoute("GetTodo", new { id = item.Id }, item);
}
```

The Swagger UI now clearly documents the expected HTTP response codes:

In ASP.NET Core 2.2 or later, conventions can be used as an alternative to explicitly decorating individual actions with `[ProducesResponseType]`. For more information, see [Use web API conventions](../web-api/advanced/conventions?view=aspnetcore-8.0).

To support the `[ProducesResponseType]` decoration, the [Swashbuckle.AspNetCore.Annotations](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/README.md#swashbuckleaspnetcoreannotations) package offers extensions to enable and enrich the response, schema, and parameter metadata.

### Customize the UI

The default UI is both functional and presentable. However, API documentation pages should represent your brand or theme. Branding the Swashbuckle components requires adding the resources to serve static files and building the folder structure to host those files.

If targeting .NET Framework or .NET Core 1.x, add the [Microsoft.AspNetCore.StaticFiles](https://www.nuget.org/packages/Microsoft.AspNetCore.StaticFiles) NuGet package to the project:

```
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
```

The preceding NuGet package is already installed if targeting .NET Core 2.x and using the [metapackage](../fundamentals/metapackage?view=aspnetcore-8.0).

Enable static file middleware:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();

    if (env.IsDevelopment())
    {
        // Enable middleware to serve generated Swagger as a JSON endpoint.
        app.UseSwagger();

        // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
        // specifying the Swagger JSON endpoint.
        app.UseSwaggerUI(c => // UseSwaggerUI Protected by if (env.IsDevelopment())
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
        });
    }

    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

To inject additional CSS stylesheets, add them to the project's *wwwroot* folder and specify the relative path in the middleware options:

```
if (env.IsDevelopment())
{
    app.UseSwaggerUI(c =>
    {
        c.InjectStylesheet("/swagger-ui/custom.css");
    }
}
```

## Additional resources

* [Improve the developer experience of an API with Swagger documentation](/en-us/training/modules/improve-api-developer-experience-with-swagger/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-10-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/web-farm
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/web-farm.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host ASP.NET Core in a web farm

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Chris Ross](https://github.com/Tratcher)

A *web farm* is a group of two or more web servers (or *nodes*) that host multiple instances of an app. When requests from users arrive to a web farm, a *load balancer* distributes the requests to the web farm's nodes. Web farms improve:

* **Reliability/availability**: When one or more nodes fail, the load balancer can route requests to other functioning nodes to continue processing requests.
* **Capacity/performance**: Multiple nodes can process more requests than a single server. The load balancer balances the workload by distributing requests to the nodes.
* **Scalability**: When more or less capacity is required, the number of active nodes can be increased or decreased to match the workload. Web farm platform technologies, such as [Azure App Service](https://azure.microsoft.com/services/app-service/), can automatically add or remove nodes at the request of the system administrator or automatically without human intervention.
* **Maintainability**: Nodes of a web farm can rely on a set of shared services, which results in easier system management. For example, the nodes of a web farm can rely upon a single database server and a common network location for static resources, such as images and downloadable files.

This topic describes configuration and dependencies for ASP.NET core apps hosted in a web farm that rely upon shared resources.

## General configuration

[Host and deploy ASP.NET Core](./?view=aspnetcore-10.0)\
Learn how to set up hosting environments and deploy ASP.NET Core apps. Configure a process manager on each node of the web farm to automate app starts and restarts. Each node requires the ASP.NET Core runtime. For more information, see the topics in the [Host and deploy](./?view=aspnetcore-10.0) area of the documentation.

[Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0)\
Learn about configuration for apps hosted behind proxy servers and load balancers, which often obscure important request information.

[Deploy ASP.NET Core apps to Azure App Service](azure-apps/?view=aspnetcore-10.0)\
[Azure App Service](https://azure.microsoft.com/services/app-service/) is a [Microsoft cloud computing platform service](https://azure.microsoft.com/) for hosting web apps, including ASP.NET Core. App Service is a fully managed platform that provides automatic scaling, load balancing, patching, and continuous deployment.

## App data

When an app is scaled to multiple instances, there might be app state that requires sharing across nodes. If the state is transient, consider sharing an [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache). If the shared state requires persistence, consider storing the shared state in a database.

## Required configuration

Data Protection and Caching require configuration for apps deployed to a web farm.

### Data Protection

The [ASP.NET Core Data Protection system](../security/data-protection/introduction?view=aspnetcore-10.0) is used by apps to protect data. Data Protection relies upon a set of cryptographic keys stored in a *key ring*. When the Data Protection system is initialized, it applies [default settings](../security/data-protection/configuration/default-settings?view=aspnetcore-10.0) that store the key ring locally. Under the default configuration, a unique key ring is stored on each node of the web farm. Consequently, each web farm node can't decrypt data that's encrypted by an app on any other node. The default configuration isn't generally appropriate for hosting apps in a web farm. An alternative to implementing a shared key ring is to always route user requests to the same node. For more information on Data Protection system configuration for web farm deployments, see [Configure ASP.NET Core Data Protection](../security/data-protection/configuration/overview?view=aspnetcore-10.0).

### Caching

In a web farm environment, the caching mechanism must share cached items across the web farm's nodes. Caching must either rely upon a common Redis cache, a shared SQL Server database, or a custom caching implementation that shares cached items across the web farm. For more information, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0).

## Dependent components

The following scenarios don't require additional configuration, but they depend on technologies that require configuration for web farms.

| Scenario       | Depends on …                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Authentication | Data Protection (see [Configure ASP.NET Core Data Protection](../security/data-protection/configuration/overview?view=aspnetcore-10.0)). For more information, see [Use cookie authentication without ASP.NET Core Identity](../security/authentication/cookie?view=aspnetcore-10.0) and [Share authentication cookies among ASP.NET apps](../security/cookie-sharing?view=aspnetcore-10.0).                           |
| Identity       | Authentication and database configuration. For more information, see [Introduction to Identity on ASP.NET Core](../security/authentication/identity?view=aspnetcore-10.0).                                                                                                                                                                                                                                             |
| Session        | Data Protection (encrypted cookies) (see [Configure ASP.NET Core Data Protection](../security/data-protection/configuration/overview?view=aspnetcore-10.0)) and Caching (see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0)). For more information, see [Session and state management: Session state](../fundamentals/app-state?view=aspnetcore-10.0#session-state).   |
| TempData       | Data Protection (encrypted cookies) (see [Configure ASP.NET Core Data Protection](../security/data-protection/configuration/overview?view=aspnetcore-10.0)) or Session (see [Session and state management: Session state](../fundamentals/app-state?view=aspnetcore-10.0#session-state)). For more information, see [Session and state management: TempData](../fundamentals/app-state?view=aspnetcore-10.0#tempdata). |
| Antiforgery    | Data Protection (see [Configure ASP.NET Core Data Protection](../security/data-protection/configuration/overview?view=aspnetcore-10.0)). For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-10.0).                                                                                                                    |

## Troubleshoot

### Data Protection and caching

When Data Protection or caching isn't configured for a web farm environment, intermittent errors occur when requests are processed. This occurs because nodes don't share the same resources and user requests aren't always routed back to the same node.

Consider a user who signs into the app using cookie authentication. The user signs into the app on one web farm node. If their next request arrives at the same node where they signed in, the app is able to decrypt the authentication cookie and allows access to the app's resource. If their next request arrives at a different node, the app can't decrypt the authentication cookie from the node where the user signed in, and authorization for the requested resource fails.

When any of the following symptoms occur **intermittently**, the problem is usually traced to improper Data Protection or caching configuration for a web farm environment:

* Authentication breaks: The authentication cookie is misconfigured or can't be decrypted. OAuth (Facebook, Microsoft, Twitter) or OpenIdConnect logins fail with the error "Correlation failed."
* Authorization breaks: Identity is lost.
* Session state loses data.
* Cached items disappear.
* TempData fails.
* POSTs fail: The antiforgery check fails.

For more information on Data Protection configuration for web farm deployments, see [Configure ASP.NET Core Data Protection](../security/data-protection/configuration/overview?view=aspnetcore-10.0). For more information on caching configuration for web farm deployments, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0).

## Obtain data from apps

If the web farm apps are capable of responding to requests, obtain request, connection, and additional data from the apps using terminal inline middleware. For more information and sample code, see [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0#obtain-data-from-an-app).

## Related content

* [Custom Script Extension for Windows](/en-us/azure/virtual-machines/extensions/custom-script-windows): Downloads and executes scripts on Azure virtual machines, which is useful for post-deployment configuration and software installation.
* [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/error-handling.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Handle errors in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article covers common approaches to handling errors in ASP.NET Core web apps. See also [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0).

For Blazor error handling guidance, which adds to or supersedes the guidance in this article, see [Handle errors in ASP.NET Core Blazor apps](../blazor/fundamentals/handle-errors?view=aspnetcore-10.0).

## Developer exception page

```
Status: 500 Internal Server Error
Time: 9.39 msSize: 480 bytes
FormattedRawHeadersRequest
Body
text/plain; charset=utf-8, 480 bytes
System.InvalidOperationException: Sample Exception
   at WebApplicationMinimal.Program.<>c.<Main>b__0_0() in C:\Source\WebApplicationMinimal\Program.cs:line 12
   at lambda_method1(Closure, Object, HttpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: text/plain
Host: localhost:7267
traceparent: 00-0eab195ea19d07b90a46cd7d6bf2f
```

## Exception handler page

To configure a custom error handling page for the [`Production` environment](environments?view=aspnetcore-10.0), call [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). This exception handling middleware:

* Catches and logs unhandled exceptions.
* Re-executes the request in an alternate pipeline using the path indicated. The request isn't re-executed if the response has started. The template-generated code re-executes the request using the `/Error` path.

Warning

If the alternate pipeline throws an exception of its own, exception handling middleware rethrows the original exception.

Since this middleware can re-execute the request pipeline:

* Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling `_next` or caching their processing on the `HttpContext` to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
* For the [UseExceptionHandler(IApplicationBuilder, String)](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler#microsoft-aspnetcore-builder-exceptionhandlerextensions-useexceptionhandler\(microsoft-aspnetcore-builder-iapplicationbuilder-system-string\)) overload that is used in templates, only the request path is modified, and the route data is cleared. Request data such as headers, method, and items are all reused as-is.
* Scoped services remain the same.

In the following example, [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) adds the exception handling middleware in non-`Development` environments:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
```

The Razor Pages app template provides an Error page (`.cshtml`) and [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class (`ErrorModel`) in the *Pages* folder. For an MVC app, the project template includes an `Error` action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the *original* HTTP method. If an error handler endpoint is restricted to a specific set of HTTP methods, it runs only for those HTTP methods. For example, an MVC controller action that uses the `[HttpGet]` attribute runs only for GET requests. To ensure that *all* requests reach the custom error handling page, don't restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

* For Razor Pages, create multiple handler methods. For example, use `OnGet` to handle GET exceptions and use `OnPost` to handle POST exceptions.
* For MVC, apply HTTP verb attributes to multiple actions. For example, use `[HttpGet]` to handle GET exceptions and use `[HttpPost]` to handle POST exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it supports anonymous access.

### Access the exception

Use [IExceptionHandlerPathFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerpathfeature) to access the exception and the original request path in an error handler. The following example uses `IExceptionHandlerPathFeature` to get more information about the exception that was thrown:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

## Exception handler lambda

An alternative to a [custom exception handler page](#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}
```

Another way to use a lambda is to set the status code based on the exception type, as in the following example:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(new ExceptionHandlerOptions
    {
        StatusCodeSelector = ex => ex is TimeoutException
            ? StatusCodes.Status503ServiceUnavailable
            : StatusCodes.Status500InternalServerError
    });
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

## IExceptionHandler

[IExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) is an interface that gives the developer a callback for handling known exceptions in a central location. The interface contains a single method, [`TryHandleAsync`](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler.tryhandleasync), which receives an `HttpContext` and an `Exception` parameter.

`IExceptionHandler` implementations are registered by calling [`IServiceCollection.AddExceptionHandler<T>`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.exceptionhandlerservicecollectionextensions.addexceptionhandler). The lifetime of an `IExceptionHandler` instance is singleton. Multiple implementations can be added, and they're called in the order registered.

Important

Registering an `IExceptionHandler` implementation isn't sufficient on its own. Registered handlers are invoked by the exception handling middleware, so the app must also add that middleware by calling [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). If `UseExceptionHandler` isn't called, the registered `IExceptionHandler` implementations are never called.

Exception handling middleware iterates through registered exception handlers in order until one returns `true` from `TryHandleAsync`, indicating that the exception has been handled. If an exception handler handles an exception, it can return `true` to stop processing. If an exception isn't handled by any exception handler, then control falls back to the default behavior and options from the middleware.

Starting in .NET 10, the default behavior is to suppress emission of diagnostics such as logs and metrics for handled exceptions (when `TryHandleAsync` returns `true`). This differs from earlier versions (.NET 8 and 9) where diagnostics were always emitted regardless of whether the exception was handled. The default behavior can be changed by setting [SuppressDiagnosticsCallback](#suppressdiagnosticscallback).

The following example shows an `IExceptionHandler` implementation:

```
using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}
```

The following example shows how to register an `IExceptionHandler` implementation for dependency injection and add the exception handling middleware that calls it:

```
using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity
```

### Configure `UseExceptionHandler` when using `IExceptionHandler`

The exception handling middleware requires a fallback for exceptions that no `IExceptionHandler` handles. If none is configured, calling `app.UseExceptionHandler()` with no arguments throws the following exception at startup:

`System.InvalidOperationException: An error occurred when configuring the exception handler middleware. Either the 'ExceptionHandlingPath' or the 'ExceptionHandler' property must be set in 'UseExceptionHandler()'.`

Use any one of the following to satisfy the requirement:

* Specify an error path: `app.UseExceptionHandler("/Error");`
* Enable problem details: call `builder.Services.AddProblemDetails();` and then `app.UseExceptionHandler();`
* Supply a fallback handler: `app.UseExceptionHandler(exceptionHandlerApp => { ... });`

The `IExceptionHandler` implementations still run first in all of these cases. The configured path, problem details response, or fallback handler is used only when every `TryHandleAsync` returns `false`.

If an `IExceptionHandler` returns `true` without writing a response or setting a status code, the response ends up as `404 Not Found` and the middleware logs:

`The exception handler configured on ExceptionHandlerOptions produced a 404 status response.`

An `IExceptionHandler` that returns `true` should write the complete response, including the status code. For example, set `httpContext.Response.StatusCode` and write the body, or call `IProblemDetailsService.TryWriteAsync`. If a 404 response is intentional, set [AllowStatusCode404Response](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandleroptions.allowstatuscode404response#microsoft-aspnetcore-builder-exceptionhandleroptions-allowstatuscode404response) to `true`.

When the preceding code runs in the `Development` environment:

* The `CustomExceptionHandler` is called first to handle an exception.
* After logging the exception, the `TryHandleAsync` method returns `false`, so the [developer exception page](#developer-exception-page) is shown.

In other environments:

* The `CustomExceptionHandler` is called first to handle an exception.
* After logging the exception, the `TryHandleAsync` method returns `false`, so the [`/Error` page](#exception-handler-page) is shown.

If `TryHandleAsync` returns `true` instead:

* The remaining `IExceptionHandler` implementations aren't called.
* The exception handler page, path, or lambda configured on `UseExceptionHandler` isn't used. The handler is responsible for writing the entire response.

### SuppressDiagnosticsCallback

Starting in .NET 10, you can control whether the exception handling middleware writes diagnostics for handled exceptions by configuring the `SuppressDiagnosticsCallback` property on `ExceptionHandlerOptions`. This callback receives the exception context and allows you to determine whether diagnostics should be suppressed based on the specific exception or request.

To revert to the .NET 8 and 9 behavior where diagnostics are always emitted for handled exceptions, set the callback to always return `false`:

```
app.UseExceptionHandler(new ExceptionHandlerOptions
{
    SuppressDiagnosticsCallback = context => false
});
```

You can also conditionally suppress diagnostics based on the exception type or other context:

```
app.UseExceptionHandler(new ExceptionHandlerOptions
{
    SuppressDiagnosticsCallback = context => context.Exception is ArgumentException
});
```

When an exception isn't handled by any `IExceptionHandler` implementation (all handlers return `false` from `TryHandleAsync`), control falls back to the default behavior and options from the middleware, and diagnostics are emitted according to the middleware's standard behavior.

[]()

## UseStatusCodePages

By default, an ASP.NET Core app doesn't provide a status code page for HTTP error status codes, such as *404 - Not Found*. When the app sets an HTTP 400-599 error status code that doesn't have a body, it returns the status code and an empty response body. To enable default text-only handlers for common error status codes, call [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) in `Program.cs`:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();
```

Call `UseStatusCodePages` before request handling middleware. For example, call `UseStatusCodePages` before the static file middleware and the endpoints middleware.

When `UseStatusCodePages` isn't used, navigating to a URL without an endpoint returns a browser-dependent error message indicating the endpoint can't be found. When `UseStatusCodePages` is called, the browser returns the following response:

```
Status Code: 404; Not Found
```

`UseStatusCodePages` isn't typically used in production because it returns a message that isn't useful to users.

Note

The status code pages middleware does **not** catch exceptions. To provide a custom error handling page, use the [exception handler page](#exception-handler-page).

### UseStatusCodePages with format string

To customize the response content type and text, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a content type and format string:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
```

In the preceding code, `{0}` is a placeholder for the error code.

`UseStatusCodePages` with a format string isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a lambda expression:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
```

`UseStatusCodePages` with a lambda isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePagesWithRedirects

The [UseStatusCodePagesWithRedirects](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithredirects) extension method:

* Sends a [302 - Found](https://developer.mozilla.org/docs/Web/HTTP/Status/302) status code to the client.
* Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
```

The URL template can include a `{0}` placeholder for the status code, as shown in the preceding code. If the URL template starts with `~` (tilde), the `~` is replaced by the app's `PathBase`. When specifying an endpoint in the app, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app:

* Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client's browser address bar reflects the redirected endpoint.
* Shouldn't preserve and return the original status code with the initial redirect response.

### UseStatusCodePagesWithReExecute

The [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) extension method:

* Generates the response body by re-executing the request pipeline using an alternate path.
* Does not alter the status code before or after re-executing the pipeline.

The new pipeline execution may alter the response's status code, as the new pipeline has full control of the status code. If the new pipeline does not alter the status code, the original status code will be sent to the client.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
```

If an endpoint within the app is specified, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app should:

* Process the request without redirecting to a different endpoint. For web apps, the client's browser address bar reflects the originally requested endpoint.
* Preserve and return the original status code with the response.

The URL template must start with `/` and may include a placeholder `{0}` for the status code. To pass the status code as a query-string parameter, pass a second argument into `UseStatusCodePagesWithReExecute`. For example:

```
var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");
```

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}
```

Similar to `UseExceptionHandler`, only the request path is modified, and the route data is cleared. Request data such as headers (including the referrer), HTTP method, and items are reused as-is on `HttpContext.Request`. Original path and query string details can be retrieved using [IStatusCodeReExecuteFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodereexecutefeature).

Since this middleware can re-execute the request pipeline:

* Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling `_next` or caching their processing on the `HttpContext` to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
* Scoped services remain the same.

## Disable status code pages

To disable status code pages for an MVC controller or action method, use the [\[SkipStatusCodePages\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.skipstatuscodepagesattribute) attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use [IStatusCodePagesFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodepagesfeature):

```
public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}
```

## Exception-handling code

Code in exception handling pages can also throw exceptions. Production error pages should be tested thoroughly and take extra care to avoid throwing exceptions of their own.

### Response headers

Once the headers for a response are sent:

* The app can't change the response's status code.
* Any exception pages or handlers can't run. The response must be completed or the connection aborted.

## Server exception handling

In addition to the exception handling logic in an app, the [HTTP server implementation](servers/?view=aspnetcore-10.0) can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a `500 - Internal Server Error` response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren't handled by the app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server's exception handling. The app's custom error pages, exception handling middleware, and filters don't affect this behavior.

## Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to [capture startup errors](host/web-host?view=aspnetcore-10.0#capture-startup-errors) and [capture detailed errors](host/web-host?view=aspnetcore-10.0#detailed-errors).

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

* The hosting layer logs a critical exception.
* The dotnet process crashes.
* No error page is displayed when the HTTP server is [Kestrel](servers/kestrel?view=aspnetcore-10.0).

When running on [IIS](/en-us/iis) (or Azure App Service) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), a *502.5 - Process Failure* is returned by the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) if the process can't start. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

## Database error page

The Database developer page exception filter [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) captures database-related exceptions that can be resolved by using Entity Framework Core migrations. When these exceptions occur, an HTML response is generated with details of possible actions to resolve the issue. This page is enabled only in the `Development` environment. The following code adds the Database developer page exception filter:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
```

## Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. For more information, see [Filters in ASP.NET Core](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters).

Exception filters are useful for trapping exceptions that occur within MVC actions, but they're not as flexible as the built-in [exception handling middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs), [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). We recommend using `UseExceptionHandler`, unless you need to perform error handling differently based on which MVC action is chosen.

## Model state errors

For information about how to handle model state errors, see [Model binding](../mvc/models/model-binding?view=aspnetcore-10.0) and [Model validation](../mvc/models/validation?view=aspnetcore-10.0).

[]()

The following code configures the app to generate a problem details response for all HTTP client and server error responses that ***don't have body content yet***:

```
builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();
```

The next section shows how to customize the problem details response body.

[]()

### Customize problem details

The automatic creation of a `ProblemDetails` can be customized using any of the following options:

1. Use [`ProblemDetailsOptions.CustomizeProblemDetails`](#customizeproblemdetails-operation)
2. Use a custom [`IProblemDetailsWriter`](#custom-iproblemdetailswriter)
3. Call the [`IProblemDetailsService` in a middleware](#problem-details-from-middleware)

#### `CustomizeProblemDetails` operation

The generated problem details can be customized using [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails), and the customizations are applied to all auto-generated problem details.

The following code uses [ProblemDetailsOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions) to set [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails):

```
builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();
```

For example, an [`HTTP Status 400 Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400) endpoint result produces the following problem details response body:

```
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}
```

#### Custom `IProblemDetailsWriter`

An [IProblemDetailsWriter](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailswriter) implementation can be created for advanced customizations.

```
public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}
```

***Note:*** When using a custom `IProblemDetailsWriter`, the custom `IProblemDetailsWriter` must be registered before calling [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages), [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers), [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews), or [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();
```

#### Problem details from middleware

An alternative approach to using [ProblemDetailsOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions) with [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails) is to set the [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailscontext.problemdetails#microsoft-aspnetcore-http-problemdetailscontext-problemdetails) in middleware. A problem details response can be written by calling [`IProblemDetailsService.WriteAsync`](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice.writeasync?view=aspnetcore-7.0\&preserve-view=true):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();
```

In the preceding code, the Minimal API endpoints `/divide` and `/squareroot` return the expected custom problem response on error input.

The API controller endpoints return the default problem response on error input, not the custom problem response. The default problem response is returned because the API controller has written to the response stream, [Problem details for error status codes](/en-us/aspnet/core/web-api/#problem-details-for-error-status-codes), before [`IProblemDetailsService.WriteAsync`](https://github.com/dotnet/aspnetcore/blob/ce2db7ea0b161fc5eb35710fca6feeafeeac37bc/src/Http/Http.Extensions/src/ProblemDetailsService.cs#L24) is called and the response is **not** written again.

The following `ValuesController` returns [BadRequestResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.badrequestresult), which writes to the response stream and therefore prevents the custom problem response from being returned.

```
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}
```

The following `Values3Controller` returns [`ControllerBase.Problem`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.problem) so the expected custom problem result is returned:

```
[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}
```

## Produce a ProblemDetails payload for exceptions

Consider the following app:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();
```

In non-development environments, when an exception occurs, the following is a standard [ProblemDetails response](https://datatracker.ietf.org/doc/html/rfc7807) that is returned to the client:

```
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}
```

For most apps, the preceding code is all that's needed for exceptions. However, the following section shows how to get more detailed problem responses.

An alternative to a [custom exception handler page](error-handling?view=aspnetcore-10.0#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error and writing a problem details response with [`IProblemDetailsService.WriteAsync`](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice.writeasync?view=aspnetcore-7.0\&preserve-view=true):

```
using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

An alternative approach to generate problem details is to use the third-party NuGet package [Hellang.Middleware.ProblemDetails](https://www.nuget.org/packages/Hellang.Middleware.ProblemDetails/) that can be used to map exceptions and client errors to problem details.

This article covers common approaches to handling errors in ASP.NET Core web apps. See also [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0).

For Blazor error handling guidance, which adds to or supersedes the guidance in this article, see [Handle errors in ASP.NET Core Blazor apps](../blazor/fundamentals/handle-errors?view=aspnetcore-10.0).

## Developer exception page

```
Status: 500 Internal Server Error
Time: 9.39 msSize: 480 bytes
FormattedRawHeadersRequest
Body
text/plain; charset=utf-8, 480 bytes
System.InvalidOperationException: Sample Exception
   at WebApplicationMinimal.Program.<>c.<Main>b__0_0() in C:\Source\WebApplicationMinimal\Program.cs:line 12
   at lambda_method1(Closure, Object, HttpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: text/plain
Host: localhost:7267
traceparent: 00-0eab195ea19d07b90a46cd7d6bf2f
```

## Exception handler page

To configure a custom error handling page for the [`Production` environment](environments?view=aspnetcore-10.0), call [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). This exception handling middleware:

* Catches and logs unhandled exceptions.
* Re-executes the request in an alternate pipeline using the path indicated. The request isn't re-executed if the response has started. The template-generated code re-executes the request using the `/Error` path.

Warning

If the alternate pipeline throws an exception of its own, exception handling middleware rethrows the original exception.

Since this middleware can re-execute the request pipeline:

* Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling `_next` or caching their processing on the `HttpContext` to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
* For the [UseExceptionHandler(IApplicationBuilder, String)](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler#microsoft-aspnetcore-builder-exceptionhandlerextensions-useexceptionhandler\(microsoft-aspnetcore-builder-iapplicationbuilder-system-string\)) overload that is used in templates, only the request path is modified, and the route data is cleared. Request data such as headers, method, and items are all reused as-is.
* Scoped services remain the same.

In the following example, [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) adds the exception handling middleware in non-`Development` environments:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
```

The Razor Pages app template provides an Error page (`.cshtml`) and [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class (`ErrorModel`) in the *Pages* folder. For an MVC app, the project template includes an `Error` action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the *original* HTTP method. If an error handler endpoint is restricted to a specific set of HTTP methods, it runs only for those HTTP methods. For example, an MVC controller action that uses the `[HttpGet]` attribute runs only for GET requests. To ensure that *all* requests reach the custom error handling page, don't restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

* For Razor Pages, create multiple handler methods. For example, use `OnGet` to handle GET exceptions and use `OnPost` to handle POST exceptions.
* For MVC, apply HTTP verb attributes to multiple actions. For example, use `[HttpGet]` to handle GET exceptions and use `[HttpPost]` to handle POST exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it supports anonymous access.

### Access the exception

Use [IExceptionHandlerPathFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerpathfeature) to access the exception and the original request path in an error handler. The following example uses `IExceptionHandlerPathFeature` to get more information about the exception that was thrown:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

## Exception handler lambda

An alternative to a [custom exception handler page](#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}
```

Another way to use a lambda is to set the status code based on the exception type, as in the following example:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(new ExceptionHandlerOptions
    {
        StatusCodeSelector = ex => ex is TimeoutException
            ? StatusCodes.Status503ServiceUnavailable
            : StatusCodes.Status500InternalServerError
    });
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

## IExceptionHandler

[IExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) is an interface that gives the developer a callback for handling known exceptions in a central location.

`IExceptionHandler` implementations are registered by calling [`IServiceCollection.AddExceptionHandler<T>`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.exceptionhandlerservicecollectionextensions.addexceptionhandler). The lifetime of an `IExceptionHandler` instance is singleton. Multiple implementations can be added, and they're called in the order registered.

Important

Registering an `IExceptionHandler` implementation isn't sufficient on its own. Registered handlers are invoked by the exception handling middleware, so the app must also add that middleware by calling [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). If `UseExceptionHandler` isn't called, the registered `IExceptionHandler` implementations are never called.

Exception handling middleware iterates through registered exception handlers in order until one returns `true` from `TryHandleAsync`, indicating that the exception has been handled. If an exception handler handles an exception, it can return `true` to stop processing. If an exception isn't handled by any exception handler, then control falls back to the default behavior and options from the middleware.

Note

In .NET 8 and .NET 9, the exception handling middleware logs the exception and emits metrics even when an `IExceptionHandler` implementation returns `true` from `TryHandleAsync`. If the handler does its own logging, the exception is recorded twice: once under the `Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware` category and once under the handler's category. To log it only from the handler, filter out the middleware category in configuration:

```
{
  "Logging": {
    "LogLevel": {
      "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None"
    }
  }
}
```

Starting in .NET 10, diagnostics are suppressed by default for handled exceptions. See [SuppressDiagnosticsCallback](error-handling?view=aspnetcore-10.0#suppressdiagnosticscallback).

The following example shows an `IExceptionHandler` implementation:

```
using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}
```

The following example shows how to register an `IExceptionHandler` implementation for dependency injection and add the exception handling middleware that calls it:

```
using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity
```

### Configure `UseExceptionHandler` when using `IExceptionHandler`

The exception handling middleware requires a fallback for exceptions that no `IExceptionHandler` handles. If none is configured, calling `app.UseExceptionHandler()` with no arguments throws at startup:

`System.InvalidOperationException: An error occurred when configuring the exception handler middleware. Either the 'ExceptionHandlingPath' or the 'ExceptionHandler' property must be set in 'UseExceptionHandler()'.`

Use any one of the following to satisfy the requirement:

* Specify an error path: `app.UseExceptionHandler("/Error");`
* Enable problem details: call `builder.Services.AddProblemDetails();` and then `app.UseExceptionHandler();`
* Supply a fallback handler: `app.UseExceptionHandler(exceptionHandlerApp => { ... });`

The `IExceptionHandler` implementations still run first in all of these cases. The configured path, problem details response, or fallback handler is used only when every `TryHandleAsync` returns `false`.

If an `IExceptionHandler` returns `true` without writing a response or setting a status code, the response ends up as `404 Not Found` and the middleware logs:

`The exception handler configured on ExceptionHandlerOptions produced a 404 status response.`

An `IExceptionHandler` that returns `true` should write the complete response, including the status code. For example, set `httpContext.Response.StatusCode` and write the body, or call `IProblemDetailsService.TryWriteAsync`. If a 404 response is intentional, set [AllowStatusCode404Response](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandleroptions.allowstatuscode404response#microsoft-aspnetcore-builder-exceptionhandleroptions-allowstatuscode404response) to `true`.

When the preceding code runs in the `Development` environment:

* The `CustomExceptionHandler` is called first to handle an exception.
* After logging the exception, the `TryHandleAsync` method returns `false`, so the [developer exception page](#developer-exception-page) is shown.

In other environments:

* The `CustomExceptionHandler` is called first to handle an exception.
* After logging the exception, the `TryHandleAsync` method returns `false`, so the [`/Error` page](#exception-handler-page) is shown.

If `TryHandleAsync` returns `true` instead:

* The remaining `IExceptionHandler` implementations aren't called.
* The exception handler page, path, or lambda configured on `UseExceptionHandler` isn't used. The handler is responsible for writing the entire response.

[]()

## UseStatusCodePages

By default, an ASP.NET Core app doesn't provide a status code page for HTTP error status codes, such as *404 - Not Found*. When the app sets an HTTP 400-599 error status code that doesn't have a body, it returns the status code and an empty response body. To enable default text-only handlers for common error status codes, call [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) in `Program.cs`:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();
```

Call `UseStatusCodePages` before request handling middleware. For example, call `UseStatusCodePages` before the static file middleware and the endpoints middleware.

When `UseStatusCodePages` isn't used, navigating to a URL without an endpoint returns a browser-dependent error message indicating the endpoint can't be found. When `UseStatusCodePages` is called, the browser returns the following response:

```
Status Code: 404; Not Found
```

`UseStatusCodePages` isn't typically used in production because it returns a message that isn't useful to users.

Note

The status code pages middleware does **not** catch exceptions. To provide a custom error handling page, use the [exception handler page](#exception-handler-page).

### UseStatusCodePages with format string

To customize the response content type and text, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a content type and format string:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
```

In the preceding code, `{0}` is a placeholder for the error code.

`UseStatusCodePages` with a format string isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a lambda expression:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
```

`UseStatusCodePages` with a lambda isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePagesWithRedirects

The [UseStatusCodePagesWithRedirects](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithredirects) extension method:

* Sends a [302 - Found](https://developer.mozilla.org/docs/Web/HTTP/Status/302) status code to the client.
* Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
```

The URL template can include a `{0}` placeholder for the status code, as shown in the preceding code. If the URL template starts with `~` (tilde), the `~` is replaced by the app's `PathBase`. When specifying an endpoint in the app, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app:

* Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client's browser address bar reflects the redirected endpoint.
* Shouldn't preserve and return the original status code with the initial redirect response.

### UseStatusCodePagesWithReExecute

The [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) extension method:

* Generates the response body by re-executing the request pipeline using an alternate path.
* Does not alter the status code before or after re-executing the pipeline.

The new pipeline execution may alter the response's status code, as the new pipeline has full control of the status code. If the new pipeline does not alter the status code, the original status code will be sent to the client.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
```

If an endpoint within the app is specified, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app should:

* Process the request without redirecting to a different endpoint. For web apps, the client's browser address bar reflects the originally requested endpoint.
* Preserve and return the original status code with the response.

The URL template must start with `/` and may include a placeholder `{0}` for the status code. To pass the status code as a query-string parameter, pass a second argument into `UseStatusCodePagesWithReExecute`. For example:

```
var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");
```

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}
```

Similar to `UseExceptionHandler`, only the request path is modified, and the route data is cleared. Request data such as headers (including the referrer), HTTP method, and items are reused as-is on `HttpContext.Request`. Original path and query string details can be retrieved using [IStatusCodeReExecuteFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodereexecutefeature).

Since this middleware can re-execute the request pipeline:

* Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling `_next` or caching their processing on the `HttpContext` to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
* Scoped services remain the same.

## Disable status code pages

To disable status code pages for an MVC controller or action method, use the [\[SkipStatusCodePages\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.skipstatuscodepagesattribute) attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use [IStatusCodePagesFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodepagesfeature):

```
public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}
```

## Exception-handling code

Code in exception handling pages can also throw exceptions. Production error pages should be tested thoroughly and take extra care to avoid throwing exceptions of their own.

### Response headers

Once the headers for a response are sent:

* The app can't change the response's status code.
* Any exception pages or handlers can't run. The response must be completed or the connection aborted.

## Server exception handling

In addition to the exception handling logic in an app, the [HTTP server implementation](servers/?view=aspnetcore-10.0) can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a `500 - Internal Server Error` response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren't handled by the app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server's exception handling. The app's custom error pages, exception handling middleware, and filters don't affect this behavior.

## Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to [capture startup errors](host/web-host?view=aspnetcore-10.0#capture-startup-errors) and [capture detailed errors](host/web-host?view=aspnetcore-10.0#detailed-errors).

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

* The hosting layer logs a critical exception.
* The dotnet process crashes.
* No error page is displayed when the HTTP server is [Kestrel](servers/kestrel?view=aspnetcore-10.0).

When running on [IIS](/en-us/iis) (or Azure App Service) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), a *502.5 - Process Failure* is returned by the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) if the process can't start. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

## Database error page

The Database developer page exception filter [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) captures database-related exceptions that can be resolved by using Entity Framework Core migrations. When these exceptions occur, an HTML response is generated with details of possible actions to resolve the issue. This page is enabled only in the `Development` environment. The following code adds the Database developer page exception filter:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
```

## Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. For more information, see [Filters in ASP.NET Core](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters).

Exception filters are useful for trapping exceptions that occur within MVC actions, but they're not as flexible as the built-in [exception handling middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs), [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). We recommend using `UseExceptionHandler`, unless you need to perform error handling differently based on which MVC action is chosen.

## Model state errors

For information about how to handle model state errors, see [Model binding](../mvc/models/model-binding?view=aspnetcore-10.0) and [Model validation](../mvc/models/validation?view=aspnetcore-10.0).

[]()

The following code configures the app to generate a problem details response for all HTTP client and server error responses that ***don't have body content yet***:

```
builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();
```

The next section shows how to customize the problem details response body.

[]()

### Customize problem details

The automatic creation of a `ProblemDetails` can be customized using any of the following options:

1. Use [`ProblemDetailsOptions.CustomizeProblemDetails`](#customizeproblemdetails-operation)
2. Use a custom [`IProblemDetailsWriter`](#custom-iproblemdetailswriter)
3. Call the [`IProblemDetailsService` in a middleware](#problem-details-from-middleware)

#### `CustomizeProblemDetails` operation

The generated problem details can be customized using [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails), and the customizations are applied to all auto-generated problem details.

The following code uses [ProblemDetailsOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions) to set [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails):

```
builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();
```

For example, an [`HTTP Status 400 Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400) endpoint result produces the following problem details response body:

```
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}
```

#### Custom `IProblemDetailsWriter`

An [IProblemDetailsWriter](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailswriter) implementation can be created for advanced customizations.

```
public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}
```

***Note:*** When using a custom `IProblemDetailsWriter`, the custom `IProblemDetailsWriter` must be registered before calling [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages), [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers), [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews), or [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();
```

#### Problem details from Middleware

An alternative approach to using [ProblemDetailsOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions) with [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails) is to set the [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailscontext.problemdetails#microsoft-aspnetcore-http-problemdetailscontext-problemdetails) in middleware. A problem details response can be written by calling [`IProblemDetailsService.WriteAsync`](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice.writeasync?view=aspnetcore-7.0\&preserve-view=true):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();
```

In the preceding code, the Minimal API endpoints `/divide` and `/squareroot` return the expected custom problem response on error input.

The API controller endpoints return the default problem response on error input, not the custom problem response. The default problem response is returned because the API controller has written to the response stream, [Problem details for error status codes](/en-us/aspnet/core/web-api/#problem-details-for-error-status-codes), before [`IProblemDetailsService.WriteAsync`](https://github.com/dotnet/aspnetcore/blob/ce2db7ea0b161fc5eb35710fca6feeafeeac37bc/src/Http/Http.Extensions/src/ProblemDetailsService.cs#L24) is called and the response is **not** written again.

The following `ValuesController` returns [BadRequestResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.badrequestresult), which writes to the response stream and therefore prevents the custom problem response from being returned.

```
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}
```

The following `Values3Controller` returns [`ControllerBase.Problem`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.problem) so the expected custom problem result is returned:

```
[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}
```

## Produce a ProblemDetails payload for exceptions

Consider the following app:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();
```

In non-development environments, when an exception occurs, the following is a standard [ProblemDetails response](https://datatracker.ietf.org/doc/html/rfc7807) that is returned to the client:

```
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}
```

For most apps, the preceding code is all that's needed for exceptions. However, the following section shows how to get more detailed problem responses.

An alternative to a [custom exception handler page](error-handling?view=aspnetcore-10.0#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error and writing a problem details response with [`IProblemDetailsService.WriteAsync`](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice.writeasync?view=aspnetcore-7.0\&preserve-view=true):

```
using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

An alternative approach to generate problem details is to use the third-party NuGet package [Hellang.Middleware.ProblemDetails](https://www.nuget.org/packages/Hellang.Middleware.ProblemDetails/) that can be used to map exceptions and client errors to problem details.

By [Tom Dykstra](https://github.com/tdykstra/)

This article covers common approaches to handling errors in ASP.NET Core web apps. See also [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0).

## Developer exception page

The *Developer Exception Page* displays detailed information about unhandled request exceptions. ASP.NET Core apps enable the developer exception page by default when both:

* Running in the [`Development` environment](environments?view=aspnetcore-10.0).
* App created with the current templates, that is, using [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder). Apps created using the [`WebHost.CreateDefaultBuilder`](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder#microsoft-aspnetcore-webhost-createdefaultbuilder) must enable the developer exception page by calling `app.UseDeveloperExceptionPage` in `Configure`.

The developer exception page runs early in the middleware pipeline, so that it can catch unhandled exceptions thrown in middleware that follows.

Detailed exception information shouldn't be displayed publicly when the app runs in the `Production` environment. For more information on configuring environments, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).

The Developer Exception Page can include the following information about the exception and the request:

* Stack trace
* Query string parameters, if any
* Cookies, if any
* Headers

The Developer Exception Page isn't guaranteed to provide any information. Use [Logging](logging/?view=aspnetcore-10.0) for complete error information.

## Exception handler page

To configure a custom error handling page for the [`Production` environment](environments?view=aspnetcore-10.0), call [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). This exception handling middleware:

* Catches and logs unhandled exceptions.
* Re-executes the request in an alternate pipeline using the path indicated. The request isn't re-executed if the response has started. The template-generated code re-executes the request using the `/Error` path.

Warning

If the alternate pipeline throws an exception of its own, exception handling middleware rethrows the original exception.

Since this middleware can re-execute the request pipeline:

* Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling `_next` or caching their processing on the `HttpContext` to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
* For the [UseExceptionHandler(IApplicationBuilder, String)](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler#microsoft-aspnetcore-builder-exceptionhandlerextensions-useexceptionhandler\(microsoft-aspnetcore-builder-iapplicationbuilder-system-string\)) overload that is used in templates, only the request path is modified, and the route data is cleared. Request data such as headers, method, and items are all reused as-is.
* Scoped services remain the same.

In the following example, [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) adds the exception handling middleware in non-`Development` environments:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
```

The Razor Pages app template provides an Error page (`.cshtml`) and [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class (`ErrorModel`) in the *Pages* folder. For an MVC app, the project template includes an `Error` action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the *original* HTTP method. If an error handler endpoint is restricted to a specific set of HTTP methods, it runs only for those HTTP methods. For example, an MVC controller action that uses the `[HttpGet]` attribute runs only for GET requests. To ensure that *all* requests reach the custom error handling page, don't restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

* For Razor Pages, create multiple handler methods. For example, use `OnGet` to handle GET exceptions and use `OnPost` to handle POST exceptions.
* For MVC, apply HTTP verb attributes to multiple actions. For example, use `[HttpGet]` to handle GET exceptions and use `[HttpPost]` to handle POST exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it supports anonymous access.

### Access the exception

Use [IExceptionHandlerPathFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerpathfeature) to access the exception and the original request path in an error handler. The following example uses `IExceptionHandlerPathFeature` to get more information about the exception that was thrown:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

## Exception handler lambda

An alternative to a [custom exception handler page](#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

## IExceptionHandler

[IExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) is an interface that gives the developer a callback for handling known exceptions in a central location.

`IExceptionHandler` implementations are registered by calling [`IServiceCollection.AddExceptionHandler<T>`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.exceptionhandlerservicecollectionextensions.addexceptionhandler). The lifetime of an `IExceptionHandler` instance is singleton. Multiple implementations can be added, and they're called in the order registered.

Important

Registering an `IExceptionHandler` implementation isn't sufficient on its own. Registered handlers are invoked by the exception handling middleware, so the app must also add that middleware by calling [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). If `UseExceptionHandler` isn't called, the registered `IExceptionHandler` implementations are never called.

Exception handling middleware iterates through registered exception handlers in order until one returns `true` from `TryHandleAsync`, indicating that the exception has been handled. If an exception handler handles an exception, it can return `true` to stop processing. If an exception isn't handled by any exception handler, then control falls back to the default behavior and options from the middleware.

Note

In .NET 8 and .NET 9, the exception handling middleware logs the exception and emits metrics even when an `IExceptionHandler` implementation returns `true` from `TryHandleAsync`. If the handler does its own logging, the exception is recorded twice: once under the `Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware` category and once under the handler's category. To log it only from the handler, filter out the middleware category in configuration:

```
{
  "Logging": {
    "LogLevel": {
      "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None"
    }
  }
}
```

Starting in .NET 10, diagnostics are suppressed by default for handled exceptions. See [SuppressDiagnosticsCallback](error-handling?view=aspnetcore-10.0#suppressdiagnosticscallback).

The following example shows an `IExceptionHandler` implementation:

```
using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}
```

The following example shows how to register an `IExceptionHandler` implementation for dependency injection and add the exception handling middleware that calls it:

```
using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity
```

### Configure `UseExceptionHandler` when using `IExceptionHandler`

The exception handling middleware requires a fallback for exceptions that no `IExceptionHandler` handles. If none is configured, calling `app.UseExceptionHandler()` with no arguments throws at startup:

`System.InvalidOperationException: An error occurred when configuring the exception handler middleware. Either the 'ExceptionHandlingPath' or the 'ExceptionHandler' property must be set in 'UseExceptionHandler()'.`

Use any one of the following to satisfy the requirement:

* Specify an error path: `app.UseExceptionHandler("/Error");`
* Enable problem details: call `builder.Services.AddProblemDetails();` and then `app.UseExceptionHandler();`
* Supply a fallback handler: `app.UseExceptionHandler(exceptionHandlerApp => { ... });`

The `IExceptionHandler` implementations still run first in all of these cases. The configured path, problem details response, or fallback handler is used only when every `TryHandleAsync` returns `false`.

If an `IExceptionHandler` returns `true` without writing a response or setting a status code, the response ends up as `404 Not Found` and the middleware logs:

`The exception handler configured on ExceptionHandlerOptions produced a 404 status response.`

An `IExceptionHandler` that returns `true` should write the complete response, including the status code. For example, set `httpContext.Response.StatusCode` and write the body, or call `IProblemDetailsService.TryWriteAsync`. If a 404 response is intentional, set [AllowStatusCode404Response](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandleroptions.allowstatuscode404response#microsoft-aspnetcore-builder-exceptionhandleroptions-allowstatuscode404response) to `true`.

When the preceding code runs in the `Development` environment:

* The `CustomExceptionHandler` is called first to handle an exception.
* After logging the exception, the `TryHandleAsync` method returns `false`, so the [developer exception page](#developer-exception-page) is shown.

In other environments:

* The `CustomExceptionHandler` is called first to handle an exception.
* After logging the exception, the `TryHandleAsync` method returns `false`, so the [`/Error` page](#exception-handler-page) is shown.

If `TryHandleAsync` returns `true` instead:

* The remaining `IExceptionHandler` implementations aren't called.
* The exception handler page, path, or lambda configured on `UseExceptionHandler` isn't used. The handler is responsible for writing the entire response.

[]()

## UseStatusCodePages

By default, an ASP.NET Core app doesn't provide a status code page for HTTP error status codes, such as *404 - Not Found*. When the app sets an HTTP 400-599 error status code that doesn't have a body, it returns the status code and an empty response body. To enable default text-only handlers for common error status codes, call [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) in `Program.cs`:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();
```

Call `UseStatusCodePages` before request handling middleware. For example, call `UseStatusCodePages` before the static file middleware and the endpoints middleware.

When `UseStatusCodePages` isn't used, navigating to a URL without an endpoint returns a browser-dependent error message indicating the endpoint can't be found. When `UseStatusCodePages` is called, the browser returns the following response:

```
Status Code: 404; Not Found
```

`UseStatusCodePages` isn't typically used in production because it returns a message that isn't useful to users.

Note

The status code pages middleware does **not** catch exceptions. To provide a custom error handling page, use the [exception handler page](#exception-handler-page).

### UseStatusCodePages with format string

To customize the response content type and text, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a content type and format string:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
```

In the preceding code, `{0}` is a placeholder for the error code.

`UseStatusCodePages` with a format string isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a lambda expression:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
```

`UseStatusCodePages` with a lambda isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePagesWithRedirects

The [UseStatusCodePagesWithRedirects](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithredirects) extension method:

* Sends a [302 - Found](https://developer.mozilla.org/docs/Web/HTTP/Status/302) status code to the client.
* Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
```

The URL template can include a `{0}` placeholder for the status code, as shown in the preceding code. If the URL template starts with `~` (tilde), the `~` is replaced by the app's `PathBase`. When specifying an endpoint in the app, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app:

* Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client's browser address bar reflects the redirected endpoint.
* Shouldn't preserve and return the original status code with the initial redirect response.

### UseStatusCodePagesWithReExecute

The [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) extension method:

* Generates the response body by re-executing the request pipeline using an alternate path.
* Does not alter the status code before or after re-executing the pipeline.

The new pipeline execution may alter the response's status code, as the new pipeline has full control of the status code. If the new pipeline does not alter the status code, the original status code will be sent to the client.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
```

If an endpoint within the app is specified, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app should:

* Process the request without redirecting to a different endpoint. For web apps, the client's browser address bar reflects the originally requested endpoint.
* Preserve and return the original status code with the response.

The URL template must start with `/` and may include a placeholder `{0}` for the status code. To pass the status code as a query-string parameter, pass a second argument into `UseStatusCodePagesWithReExecute`. For example:

```
var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");
```

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}
```

Similar to `UseExceptionHandler`, only the request path is modified, and the route data is cleared. Request data such as headers (including the referrer), HTTP method, and items are reused as-is on `HttpContext.Request`. Original path and query string details can be retrieved using [IStatusCodeReExecuteFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodereexecutefeature).

Since this middleware can re-execute the request pipeline:

* Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling `_next` or caching their processing on the `HttpContext` to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
* Scoped services remain the same.

## Disable status code pages

To disable status code pages for an MVC controller or action method, use the [\[SkipStatusCodePages\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.skipstatuscodepagesattribute) attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use [IStatusCodePagesFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodepagesfeature):

```
public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}
```

## Exception-handling code

Code in exception handling pages can also throw exceptions. Production error pages should be tested thoroughly and take extra care to avoid throwing exceptions of their own.

### Response headers

Once the headers for a response are sent:

* The app can't change the response's status code.
* Any exception pages or handlers can't run. The response must be completed or the connection aborted.

## Server exception handling

In addition to the exception handling logic in an app, the [HTTP server implementation](servers/?view=aspnetcore-10.0) can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a `500 - Internal Server Error` response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren't handled by the app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server's exception handling. The app's custom error pages, exception handling middleware, and filters don't affect this behavior.

## Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to [capture startup errors](host/web-host?view=aspnetcore-10.0#capture-startup-errors) and [capture detailed errors](host/web-host?view=aspnetcore-10.0#detailed-errors).

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

* The hosting layer logs a critical exception.
* The dotnet process crashes.
* No error page is displayed when the HTTP server is [Kestrel](servers/kestrel?view=aspnetcore-10.0).

When running on [IIS](/en-us/iis) (or Azure App Service) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), a *502.5 - Process Failure* is returned by the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) if the process can't start. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

## Database error page

The Database developer page exception filter [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) captures database-related exceptions that can be resolved by using Entity Framework Core migrations. When these exceptions occur, an HTML response is generated with details of possible actions to resolve the issue. This page is enabled only in the `Development` environment. The following code adds the Database developer page exception filter:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
```

## Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. For more information, see [Filters in ASP.NET Core](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters).

Exception filters are useful for trapping exceptions that occur within MVC actions, but they're not as flexible as the built-in [exception handling middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs), [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). We recommend using `UseExceptionHandler`, unless you need to perform error handling differently based on which MVC action is chosen.

## Model state errors

For information about how to handle model state errors, see [Model binding](../mvc/models/model-binding?view=aspnetcore-10.0) and [Model validation](../mvc/models/validation?view=aspnetcore-10.0).

[]()

The following code configures the app to generate a problem details response for all HTTP client and server error responses that ***don't have a body content yet***:

```
builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();
```

The next section shows how to customize the problem details response body.

[]()

### Customize problem details

The automatic creation of a `ProblemDetails` can be customized using any of the following options:

1. Use [`ProblemDetailsOptions.CustomizeProblemDetails`](#customizeproblemdetails-operation)
2. Use a custom [`IProblemDetailsWriter`](#custom-iproblemdetailswriter)
3. Call the [`IProblemDetailsService` in a middleware](#problem-details-from-middleware)

#### `CustomizeProblemDetails` operation

The generated problem details can be customized using [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails), and the customizations are applied to all auto-generated problem details.

The following code uses [ProblemDetailsOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions) to set [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails):

```
builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();
```

For example, an [`HTTP Status 400 Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400) endpoint result produces the following problem details response body:

```
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}
```

#### Custom `IProblemDetailsWriter`

An [IProblemDetailsWriter](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailswriter) implementation can be created for advanced customizations.

```
public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}
```

***Note:*** When using a custom `IProblemDetailsWriter`, the custom `IProblemDetailsWriter` must be registered before calling [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages), [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers), [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews), or [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();
```

#### Problem details from Middleware

An alternative approach to using [ProblemDetailsOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions) with [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails) is to set the [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailscontext.problemdetails#microsoft-aspnetcore-http-problemdetailscontext-problemdetails) in middleware. A problem details response can be written by calling [`IProblemDetailsService.WriteAsync`](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice.writeasync?view=aspnetcore-7.0\&preserve-view=true):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();
```

In the preceding code, the Minimal API endpoints `/divide` and `/squareroot` return the expected custom problem response on error input.

The API controller endpoints return the default problem response on error input, not the custom problem response. The default problem response is returned because the API controller has written to the response stream, [Problem details for error status codes](/en-us/aspnet/core/web-api/#problem-details-for-error-status-codes-1), before [`IProblemDetailsService.WriteAsync`](https://github.com/dotnet/aspnetcore/blob/ce2db7ea0b161fc5eb35710fca6feeafeeac37bc/src/Http/Http.Extensions/src/ProblemDetailsService.cs#L24) is called and the response is **not** written again.

The following `ValuesController` returns [BadRequestResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.badrequestresult), which writes to the response stream and therefore prevents the custom problem response from being returned.

```
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}
```

The following `Values3Controller` returns [`ControllerBase.Problem`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.problem) so the expected custom problem result is returned:

```
[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}
```

## Produce a ProblemDetails payload for exceptions

Consider the following app:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();
```

In non-development environments, when an exception occurs, the following is a standard [ProblemDetails response](https://datatracker.ietf.org/doc/html/rfc7807) that is returned to the client:

```
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}
```

For most apps, the preceding code is all that's needed for exceptions. However, the following section shows how to get more detailed problem responses.

An alternative to a [custom exception handler page](error-handling?view=aspnetcore-10.0#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error and writing a problem details response with [`IProblemDetailsService.WriteAsync`](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice.writeasync?view=aspnetcore-7.0\&preserve-view=true):

```
using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

An alternative approach to generate problem details is to use the third-party NuGet package [Hellang.Middleware.ProblemDetails](https://www.nuget.org/packages/Hellang.Middleware.ProblemDetails/) that can be used to map exceptions and client errors to problem details.

By [Tom Dykstra](https://github.com/tdykstra/)

This article covers common approaches to handling errors in ASP.NET Core web apps. See also [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0).

## Developer exception page

The *Developer Exception Page* displays detailed information about unhandled request exceptions. ASP.NET Core apps enable the developer exception page by default when both:

* Running in the [`Development` environment](environments?view=aspnetcore-10.0).
* App created with the current templates, that is, using [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder). Apps created using the [`WebHost.CreateDefaultBuilder`](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder#microsoft-aspnetcore-webhost-createdefaultbuilder) must enable the developer exception page by calling `app.UseDeveloperExceptionPage` in `Configure`.

The developer exception page runs early in the middleware pipeline, so that it can catch unhandled exceptions thrown in middleware that follows.

Detailed exception information shouldn't be displayed publicly when the app runs in the `Production` environment. For more information on configuring environments, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).

The Developer Exception Page can include the following information about the exception and the request:

* Stack trace
* Query string parameters, if any
* Cookies, if any
* Headers

The Developer Exception Page isn't guaranteed to provide any information. Use [Logging](logging/?view=aspnetcore-10.0) for complete error information.

## Exception handler page

To configure a custom error handling page for the [`Production` environment](environments?view=aspnetcore-10.0), call [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). This exception handling middleware:

* Catches and logs unhandled exceptions.
* Re-executes the request in an alternate pipeline using the path indicated. The request isn't re-executed if the response has started. The template-generated code re-executes the request using the `/Error` path.

Warning

If the alternate pipeline throws an exception of its own, exception handling middleware rethrows the original exception.

Since this middleware can re-execute the request pipeline:

* Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling `_next` or caching their processing on the `HttpContext` to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
* For the [UseExceptionHandler(IApplicationBuilder, String)](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler#microsoft-aspnetcore-builder-exceptionhandlerextensions-useexceptionhandler\(microsoft-aspnetcore-builder-iapplicationbuilder-system-string\)) overload that is used in templates, only the request path is modified, and the route data is cleared. Request data such as headers, method, and items are all reused as-is.
* Scoped services remain the same.

In the following example, [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) adds the exception handling middleware in non-`Development` environments:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
```

The Razor Pages app template provides an Error page (`.cshtml`) and [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class (`ErrorModel`) in the *Pages* folder. For an MVC app, the project template includes an `Error` action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the *original* HTTP method. If an error handler endpoint is restricted to a specific set of HTTP methods, it runs only for those HTTP methods. For example, an MVC controller action that uses the `[HttpGet]` attribute runs only for GET requests. To ensure that *all* requests reach the custom error handling page, don't restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

* For Razor Pages, create multiple handler methods. For example, use `OnGet` to handle GET exceptions and use `OnPost` to handle POST exceptions.
* For MVC, apply HTTP verb attributes to multiple actions. For example, use `[HttpGet]` to handle GET exceptions and use `[HttpPost]` to handle POST exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it supports anonymous access.

### Access the exception

Use [IExceptionHandlerPathFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerpathfeature) to access the exception and the original request path in an error handler. The following example uses `IExceptionHandlerPathFeature` to get more information about the exception that was thrown:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

## Exception handler lambda

An alternative to a [custom exception handler page](#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

[]()

## UseStatusCodePages

By default, an ASP.NET Core app doesn't provide a status code page for HTTP error status codes, such as *404 - Not Found*. When the app sets an HTTP 400-599 error status code that doesn't have a body, it returns the status code and an empty response body. To enable default text-only handlers for common error status codes, call [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) in `Program.cs`:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();
```

Call `UseStatusCodePages` before request handling middleware. For example, call `UseStatusCodePages` before the static file middleware and the endpoints middleware.

When `UseStatusCodePages` isn't used, navigating to a URL without an endpoint returns a browser-dependent error message indicating the endpoint can't be found. When `UseStatusCodePages` is called, the browser returns the following response:

```
Status Code: 404; Not Found
```

`UseStatusCodePages` isn't typically used in production because it returns a message that isn't useful to users.

Note

The status code pages middleware does **not** catch exceptions. To provide a custom error handling page, use the [exception handler page](#exception-handler-page).

### UseStatusCodePages with format string

To customize the response content type and text, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a content type and format string:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
```

In the preceding code, `{0}` is a placeholder for the error code.

`UseStatusCodePages` with a format string isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a lambda expression:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
```

`UseStatusCodePages` with a lambda isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePagesWithRedirects

The [UseStatusCodePagesWithRedirects](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithredirects) extension method:

* Sends a [302 - Found](https://developer.mozilla.org/docs/Web/HTTP/Status/302) status code to the client.
* Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
```

The URL template can include a `{0}` placeholder for the status code, as shown in the preceding code. If the URL template starts with `~` (tilde), the `~` is replaced by the app's `PathBase`. When specifying an endpoint in the app, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app:

* Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client's browser address bar reflects the redirected endpoint.
* Shouldn't preserve and return the original status code with the initial redirect response.

### UseStatusCodePagesWithReExecute

The [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) extension method:

* Generates the response body by re-executing the request pipeline using an alternate path.
* Does not alter the status code before or after re-executing the pipeline.

The new pipeline execution may alter the response's status code, as the new pipeline has full control of the status code. If the new pipeline does not alter the status code, the original status code will be sent to the client.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
```

If an endpoint within the app is specified, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app should:

* Process the request without redirecting to a different endpoint. For web apps, the client's browser address bar reflects the originally requested endpoint.
* Preserve and return the original status code with the response.

The URL template must start with `/` and may include a placeholder `{0}` for the status code. To pass the status code as a query-string parameter, pass a second argument into `UseStatusCodePagesWithReExecute`. For example:

```
var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");
```

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = $"{statusCodeReExecuteFeature.OriginalPathBase}"
                                    + $"{statusCodeReExecuteFeature.OriginalPath}"
                                    + $"{statusCodeReExecuteFeature.OriginalQueryString}";

        }
    }
}
```

Similar to `UseExceptionHandler`, only the request path is modified, and the route data is cleared. Request data such as headers (including the referrer), HTTP method, and items are reused as-is on `HttpContext.Request`. Original path and query string details can be retrieved using [IStatusCodeReExecuteFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodereexecutefeature).

Since this middleware can re-execute the request pipeline:

* Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling `_next` or caching their processing on the `HttpContext` to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
* Scoped services remain the same.

## Disable status code pages

To disable status code pages for an MVC controller or action method, use the [\[SkipStatusCodePages\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.skipstatuscodepagesattribute) attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use [IStatusCodePagesFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodepagesfeature):

```
public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}
```

## Exception-handling code

Code in exception handling pages can also throw exceptions. Production error pages should be tested thoroughly and take extra care to avoid throwing exceptions of their own.

### Response headers

Once the headers for a response are sent:

* The app can't change the response's status code.
* Any exception pages or handlers can't run. The response must be completed or the connection aborted.

## Server exception handling

In addition to the exception handling logic in an app, the [HTTP server implementation](servers/?view=aspnetcore-10.0) can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a `500 - Internal Server Error` response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren't handled by the app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server's exception handling. The app's custom error pages, exception handling middleware, and filters don't affect this behavior.

## Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to [capture startup errors](host/web-host?view=aspnetcore-10.0#capture-startup-errors) and [capture detailed errors](host/web-host?view=aspnetcore-10.0#detailed-errors).

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

* The hosting layer logs a critical exception.
* The dotnet process crashes.
* No error page is displayed when the HTTP server is [Kestrel](servers/kestrel?view=aspnetcore-10.0).

When running on [IIS](/en-us/iis) (or Azure App Service) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), a *502.5 - Process Failure* is returned by the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) if the process can't start. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

## Database error page

The Database developer page exception filter [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) captures database-related exceptions that can be resolved by using Entity Framework Core migrations. When these exceptions occur, an HTML response is generated with details of possible actions to resolve the issue. This page is enabled only in the `Development` environment. The following code adds the Database developer page exception filter:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
```

## Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. For more information, see [Filters in ASP.NET Core](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters).

Exception filters are useful for trapping exceptions that occur within MVC actions, but they're not as flexible as the built-in [exception handling middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs), [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). We recommend using `UseExceptionHandler`, unless you need to perform error handling differently based on which MVC action is chosen.

## Model state errors

For information about how to handle model state errors, see [Model binding](../mvc/models/model-binding?view=aspnetcore-10.0) and [Model validation](../mvc/models/validation?view=aspnetcore-10.0).

[]()

The following code configures the app to generate a problem details response for all HTTP client and server error responses that ***don't have a body content yet***:

```
builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();
```

The next section shows how to customize the problem details response body.

[]()

### Customize problem details

The automatic creation of a `ProblemDetails` can be customized using any of the following options:

1. Use [`ProblemDetailsOptions.CustomizeProblemDetails`](#customizeproblemdetails-operation)
2. Use a custom [`IProblemDetailsWriter`](#custom-iproblemdetailswriter)
3. Call the [`IProblemDetailsService` in a middleware](#problem-details-from-middleware)

#### `CustomizeProblemDetails` operation

The generated problem details can be customized using [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails), and the customizations are applied to all auto-generated problem details.

The following code uses [ProblemDetailsOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions) to set [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails):

```
builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();
```

For example, an [`HTTP Status 400 Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400) endpoint result produces the following problem details response body:

```
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}
```

#### Custom `IProblemDetailsWriter`

An [IProblemDetailsWriter](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailswriter) implementation can be created for advanced customizations.

```
public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}
```

***Note:*** When using a custom `IProblemDetailsWriter`, the custom `IProblemDetailsWriter` must be registered before calling [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages), [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers), [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews), or [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

            if (problemDetailsService.CanWrite(new ProblemDetailsContext() { HttpContext = context }))
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                        "https://en.wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                        "https://en.wikipedia.org/wiki/Square_root")
                };

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.Run();
```

#### Problem details from Middleware

An alternative approach to using [ProblemDetailsOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions) with [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails) is to set the [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailscontext.problemdetails#microsoft-aspnetcore-http-problemdetailscontext-problemdetails) in middleware. A problem details response can be written by calling [`IProblemDetailsService.WriteAsync`](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice.writeasync?view=aspnetcore-7.0\&preserve-view=true):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(Math.Sqrt(radicand));
});

app.MapControllers();

app.Run();
```

In the preceding code, the Minimal API endpoints `/divide` and `/squareroot` return the expected custom problem response on error input.

The API controller endpoints return the default problem response on error input, not the custom problem response. The default problem response is returned because the API controller has written to the response stream, [Problem details for error status codes](/en-us/aspnet/core/web-api/#problem-details-for-error-status-codes-1), before [`IProblemDetailsService.WriteAsync`](https://github.com/dotnet/aspnetcore/blob/ce2db7ea0b161fc5eb35710fca6feeafeeac37bc/src/Http/Http.Extensions/src/ProblemDetailsService.cs#L24) is called and the response is **not** written again.

The following `ValuesController` returns [BadRequestResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.badrequestresult), which writes to the response stream and therefore prevents the custom problem response from being returned.

```
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}
```

The following `Values3Controller` returns [`ControllerBase.Problem`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.problem) so the expected custom problem result is returned:

```
[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Math.Sqrt(radicand));
    }

}
```

## Produce a ProblemDetails payload for exceptions

Consider the following app:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();
```

In non-development environments, when an exception occurs, the following is a standard [ProblemDetails response](https://datatracker.ietf.org/doc/html/rfc7807) that is returned to the client:

```
{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}
```

For most apps, the preceding code is all that's needed for exceptions. However, the following section shows how to get more detailed problem responses.

An alternative to a [custom exception handler page](error-handling?view=aspnetcore-10.0#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error and writing a problem details response with [`IProblemDetailsService.WriteAsync`](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice.writeasync?view=aspnetcore-7.0\&preserve-view=true):

```
using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

app.MapControllers();
app.Run();
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

An alternative approach to generate problem details is to use the third-party NuGet package [Hellang.Middleware.ProblemDetails](https://www.nuget.org/packages/Hellang.Middleware.ProblemDetails/) that can be used to map exceptions and client errors to problem details.

By [Tom Dykstra](https://github.com/tdykstra/)

This article covers common approaches to handling errors in ASP.NET Core web apps. See [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0) for web APIs.

## Developer exception page

The *Developer Exception Page* displays detailed information about unhandled request exceptions. ASP.NET Core apps enable the developer exception page by default when both:

* Running in the [`Development` environment](environments?view=aspnetcore-10.0).
* App created with the current templates, that is, using [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder). Apps created using the [`WebHost.CreateDefaultBuilder`](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder#microsoft-aspnetcore-webhost-createdefaultbuilder) must enable the developer exception page by calling `app.UseDeveloperExceptionPage` in `Configure`.

The developer exception page runs early in the middleware pipeline, so that it can catch unhandled exceptions thrown in middleware that follows.

Detailed exception information shouldn't be displayed publicly when the app runs in the `Production` environment. For more information on configuring environments, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).

The Developer Exception Page can include the following information about the exception and the request:

* Stack trace
* Query string parameters, if any
* Cookies, if any
* Headers

The Developer Exception Page isn't guaranteed to provide any information. Use [Logging](logging/?view=aspnetcore-10.0) for complete error information.

## Exception handler page

To configure a custom error handling page for the [`Production` environment](environments?view=aspnetcore-10.0), call [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). This exception handling middleware:

* Catches and logs unhandled exceptions.
* Re-executes the request in an alternate pipeline using the path indicated. The request isn't re-executed if the response has started. The template-generated code re-executes the request using the `/Error` path.

Warning

If the alternate pipeline throws an exception of its own, exception handling middleware rethrows the original exception.

In the following example, [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) adds the exception handling middleware in non-`Development` environments:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
```

The Razor Pages app template provides an Error page (`.cshtml`) and [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class (`ErrorModel`) in the *Pages* folder. For an MVC app, the project template includes an `Error` action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the *original* HTTP method. If an error handler endpoint is restricted to a specific set of HTTP methods, it runs only for those HTTP methods. For example, an MVC controller action that uses the `[HttpGet]` attribute runs only for GET requests. To ensure that *all* requests reach the custom error handling page, don't restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

* For Razor Pages, create multiple handler methods. For example, use `OnGet` to handle GET exceptions and use `OnPost` to handle POST exceptions.
* For MVC, apply HTTP verb attributes to multiple actions. For example, use `[HttpGet]` to handle GET exceptions and use `[HttpPost]` to handle POST exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it supports anonymous access.

### Access the exception

Use [IExceptionHandlerPathFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerpathfeature) to access the exception and the original request path in an error handler. The following example uses `IExceptionHandlerPathFeature` to get more information about the exception that was thrown:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

## Exception handler lambda

An alternative to a [custom exception handler page](#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

## UseStatusCodePages

By default, an ASP.NET Core app doesn't provide a status code page for HTTP error status codes, such as *404 - Not Found*. When the app sets an HTTP 400-599 error status code that doesn't have a body, it returns the status code and an empty response body. To enable default text-only handlers for common error status codes, call [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) in `Program.cs`:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();
```

Call `UseStatusCodePages` before request handling middleware. For example, call `UseStatusCodePages` before the static file middleware and the endpoints middleware.

When `UseStatusCodePages` isn't used, navigating to a URL without an endpoint returns a browser-dependent error message indicating the endpoint can't be found. When `UseStatusCodePages` is called, the browser returns the following response:

```
Status Code: 404; Not Found
```

`UseStatusCodePages` isn't typically used in production because it returns a message that isn't useful to users.

Note

The status code pages middleware does **not** catch exceptions. To provide a custom error handling page, use the [exception handler page](#exception-handler-page).

### UseStatusCodePages with format string

To customize the response content type and text, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a content type and format string:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");
```

In the preceding code, `{0}` is a placeholder for the error code.

`UseStatusCodePages` with a format string isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a lambda expression:

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});
```

`UseStatusCodePages` with a lambda isn't typically used in production because it returns a message that isn't useful to users.

### UseStatusCodePagesWithRedirects

The [UseStatusCodePagesWithRedirects](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithredirects) extension method:

* Sends a [302 - Found](https://developer.mozilla.org/docs/Web/HTTP/Status/302) status code to the client.
* Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
```

The URL template can include a `{0}` placeholder for the status code, as shown in the preceding code. If the URL template starts with `~` (tilde), the `~` is replaced by the app's `PathBase`. When specifying an endpoint in the app, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app:

* Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client's browser address bar reflects the redirected endpoint.
* Shouldn't preserve and return the original status code with the initial redirect response.

### UseStatusCodePagesWithReExecute

The [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) extension method:

* Returns the original status code to the client.
* Generates the response body by re-executing the request pipeline using an alternate path.

```
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
```

If an endpoint within the app is specified, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app should:

* Process the request without redirecting to a different endpoint. For web apps, the client's browser address bar reflects the originally requested endpoint.
* Preserve and return the original status code with the response.

The URL template must start with `/` and may include a placeholder `{0}` for the status code. To pass the status code as a query-string parameter, pass a second argument into `UseStatusCodePagesWithReExecute`. For example:

```
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");
```

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = string.Join(
                statusCodeReExecuteFeature.OriginalPathBase,
                statusCodeReExecuteFeature.OriginalPath,
                statusCodeReExecuteFeature.OriginalQueryString);
        }
    }
}
```

## Disable status code pages

To disable status code pages for an MVC controller or action method, use the [\[SkipStatusCodePages\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.skipstatuscodepagesattribute) attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use [IStatusCodePagesFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodepagesfeature):

```
public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}
```

## Exception-handling code

Code in exception handling pages can also throw exceptions. Production error pages should be tested thoroughly and take extra care to avoid throwing exceptions of their own.

### Response headers

Once the headers for a response are sent:

* The app can't change the response's status code.
* Any exception pages or handlers can't run. The response must be completed or the connection aborted.

## Server exception handling

In addition to the exception handling logic in an app, the [HTTP server implementation](servers/?view=aspnetcore-10.0) can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a `500 - Internal Server Error` response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren't handled by the app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server's exception handling. The app's custom error pages, exception handling middleware, and filters don't affect this behavior.

## Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to [capture startup errors](host/web-host?view=aspnetcore-10.0#capture-startup-errors) and [capture detailed errors](host/web-host?view=aspnetcore-10.0#detailed-errors).

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

* The hosting layer logs a critical exception.
* The dotnet process crashes.
* No error page is displayed when the HTTP server is [Kestrel](servers/kestrel?view=aspnetcore-10.0).

When running on [IIS](/en-us/iis) (or Azure App Service) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), a *502.5 - Process Failure* is returned by the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) if the process can't start. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

## Database error page

The Database developer page exception filter [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) captures database-related exceptions that can be resolved by using Entity Framework Core migrations. When these exceptions occur, an HTML response is generated with details of possible actions to resolve the issue. This page is enabled only in the `Development` environment. The following code adds the Database developer page exception filter:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
```

## Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. For more information, see [Filters in ASP.NET Core](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters).

Exception filters are useful for trapping exceptions that occur within MVC actions, but they're not as flexible as the built-in [exception handling middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs), [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). We recommend using `UseExceptionHandler`, unless you need to perform error handling differently based on which MVC action is chosen.

## Model state errors

For information about how to handle model state errors, see [Model binding](../mvc/models/model-binding?view=aspnetcore-10.0) and [Model validation](../mvc/models/validation?view=aspnetcore-10.0).

By [Kirk Larkin](https://twitter.com/serpent5), [Tom Dykstra](https://github.com/tdykstra/), and [Steve Smith](https://ardalis.com/)

This article covers common approaches to handling errors in ASP.NET Core web apps. See [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0) for web APIs.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples). ([How to download](./?view=aspnetcore-10.0#how-to-download-a-sample).) The network tab on the F12 browser developer tools is useful when testing the sample app.

## Developer Exception Page

The *Developer Exception Page* displays detailed information about unhandled request exceptions. The ASP.NET Core templates generate the following code:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

The preceding highlighted code enables the developer exception page when the app is running in the [`Development` environment](environments?view=aspnetcore-10.0).

The templates place [UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage) early in the middleware pipeline so that it can catch unhandled exceptions thrown in middleware that follows.

The preceding code enables the Developer Exception Page ***only*** when the app runs in the `Development` environment. Detailed exception information shouldn't be displayed publicly when the app runs in the `Production` environment. For more information on configuring environments, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).

The Developer Exception Page can include the following information about the exception and the request:

* Stack trace
* Query string parameters if any
* Cookies if any
* Headers

The Developer Exception Page isn't guaranteed to provide any information. Use [Logging](logging/?view=aspnetcore-10.0) for complete error information.

## Exception handler page

To configure a custom error handling page for the [`Production` environment](environments?view=aspnetcore-10.0), call [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). This exception handling middleware:

* Catches and logs unhandled exceptions.
* Re-executes the request in an alternate pipeline using the path indicated. The request isn't re-executed if the response has started. The template-generated code re-executes the request using the `/Error` path.

Warning

If the alternate pipeline throws an exception of its own, exception handling middleware rethrows the original exception.

In the following example, [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) adds the exception handling middleware in non-`Development` environments:

```
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
```

The Razor Pages app template provides an Error page (`.cshtml`) and [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class (`ErrorModel`) in the *Pages* folder. For an MVC app, the project template includes an `Error` action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the *original* HTTP method. If an error handler endpoint is restricted to a specific set of HTTP methods, it runs only for those HTTP methods. For example, an MVC controller action that uses the `[HttpGet]` attribute runs only for GET requests. To ensure that *all* requests reach the custom error handling page, don't restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

* For Razor Pages, create multiple handler methods. For example, use `OnGet` to handle GET exceptions and use `OnPost` to handle POST exceptions.
* For MVC, apply HTTP verb attributes to multiple actions. For example, use `[HttpGet]` to handle GET exceptions and use `[HttpPost]` to handle POST exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it supports anonymous access.

### Access the exception

Use [IExceptionHandlerPathFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerpathfeature) to access the exception and the original request path in an error handler. The following code adds `ExceptionMessage` to the default `Pages/Error.cshtml.cs` generated by the ASP.NET Core templates:

```
[ResponseCache(Duration=0, Location=ResponseCacheLocation.None, NoStore=true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string ExceptionMessage { get; set; }
    private readonly ILogger<ErrorModel> _logger;

    public ErrorModel(ILogger<ErrorModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
        HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "File error thrown";
            _logger.LogError(ExceptionMessage);
        }
        if (exceptionHandlerPathFeature?.Path == "/index")
        {
            ExceptionMessage += " from home page";
        }
    }
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

To test the exception in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x):

* Set the environment to production.
* Remove the comments from `webBuilder.UseStartup<Startup>();` in `Program.cs`.
* Select **Trigger an exception** on the home page.

## Exception handler lambda

An alternative to a [custom exception handler page](#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler(errorApp =>
        {
            errorApp.Run(async context =>
            {
                context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;;
                context.Response.ContentType = "text/html";

                await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
                await context.Response.WriteAsync("ERROR!<br><br>\r\n");

                var exceptionHandlerPathFeature =
                    context.Features.Get<IExceptionHandlerPathFeature>();

                if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
                {
                    await context.Response.WriteAsync(
                                              "File error thrown!<br><br>\r\n");
                }

                await context.Response.WriteAsync(
                                              "<a href=\"/\">Home</a><br>\r\n");
                await context.Response.WriteAsync("</body></html>\r\n");
                await context.Response.WriteAsync(new string(' ', 512)); 
            });
        });
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

Warning

Do **not** serve sensitive error information from [IExceptionHandlerFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerfeature) or [IExceptionHandlerPathFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerpathfeature) to clients. Serving errors is a security risk.

To test the exception handling lambda in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x):

* Set the environment to production.
* Remove the comments from `webBuilder.UseStartup<StartupLambda>();` in `Program.cs`.
* Select **Trigger an exception** on the home page.

## UseStatusCodePages

By default, an ASP.NET Core app doesn't provide a status code page for HTTP error status codes, such as *404 - Not Found*. When the app sets an HTTP 400-599 error status code that doesn't have a body, it returns the status code and an empty response body. To provide status code pages, use the status code pages middleware. To enable default text-only handlers for common error status codes, call [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) in the `Startup.Configure` method:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages();

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

Call `UseStatusCodePages` before request handling middleware. For example, call `UseStatusCodePages` before the static file middleware and the endpoints middleware.

When `UseStatusCodePages` isn't used, navigating to a URL without an endpoint returns a browser-dependent error message indicating the endpoint can't be found. For example, navigating to `Home/Privacy2`. When `UseStatusCodePages` is called, the browser returns:

```
Status Code: 404; Not Found
```

`UseStatusCodePages` isn't typically used in production because it returns a message that isn't useful to users.

To test `UseStatusCodePages` in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x):

* Set the environment to production.
* Remove the comments from `webBuilder.UseStartup<StartupUseStatusCodePages>();` in `Program.cs`.
* Select the links on the home page on the home page.

Note

The status code pages middleware does **not** catch exceptions. To provide a custom error handling page, use the [exception handler page](#exception-handler-page).

### UseStatusCodePages with format string

To customize the response content type and text, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a content type and format string:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(
        "text/plain", "Status code page, status code: {0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

In the preceding code, `{0}` is a placeholder for the error code.

`UseStatusCodePages` with a format string isn't typically used in production because it returns a message that isn't useful to users.

To test `UseStatusCodePages` in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x), remove the comments from `webBuilder.UseStartup<StartupFormat>();` in `Program.cs`.

### UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a lambda expression:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(async context =>
    {
        context.HttpContext.Response.ContentType = "text/plain";

        await context.HttpContext.Response.WriteAsync(
            "Status code page, status code: " +
            context.HttpContext.Response.StatusCode);
    });

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

`UseStatusCodePages` with a lambda isn't typically used in production because it returns a message that isn't useful to users.

To test `UseStatusCodePages` in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x), remove the comments from `webBuilder.UseStartup<StartupStatusLambda>();` in `Program.cs`.

### UseStatusCodePagesWithRedirects

The [UseStatusCodePagesWithRedirects](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithredirects) extension method:

* Sends a [302 - Found](https://developer.mozilla.org/docs/Web/HTTP/Status/302) status code to the client.
* Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200.

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePagesWithRedirects("/MyStatusCode?code={0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

The URL template can include a `{0}` placeholder for the status code, as shown in the preceding code. If the URL template starts with `~` (tilde), the `~` is replaced by the app's `PathBase`. When specifying an endpoint in the app, create an MVC view or Razor page for the endpoint. For a Razor Pages example, see [Pages/MyStatusCode.cshtml](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x/ErrorHandlingSample/Pages) in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x).

This method is commonly used when the app:

* Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client's browser address bar reflects the redirected endpoint.
* Shouldn't preserve and return the original status code with the initial redirect response.

To test `UseStatusCodePages` in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x), remove the comments from `webBuilder.UseStartup<StartupSCredirect>();` in `Program.cs`.

### UseStatusCodePagesWithReExecute

The [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) extension method:

* Returns the original status code to the client.
* Generates the response body by re-executing the request pipeline using an alternate path.

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePagesWithReExecute("/MyStatusCode2", "?code={0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

If an endpoint within the app is specified, create an MVC view or Razor page for the endpoint. Ensure `UseStatusCodePagesWithReExecute` is placed before `UseRouting` so the request can be rerouted to the status page. For a Razor Pages example, see [Pages/MyStatusCode2.cshtml](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x/ErrorHandlingSample/Pages) in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x).

This method is commonly used when the app should:

* Process the request without redirecting to a different endpoint. For web apps, the client's browser address bar reflects the originally requested endpoint.
* Preserve and return the original status code with the response.

The URL and query string templates may include a placeholder `{0}` for the status code. The URL template must start with `/`.

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class MyStatusCode2Model : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string ErrorStatusCode { get; set; }

    public string OriginalURL { get; set; }
    public bool ShowOriginalURL => !string.IsNullOrEmpty(OriginalURL);

    public void OnGet(string code)
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
        ErrorStatusCode = code;

        var statusCodeReExecuteFeature = HttpContext.Features.Get<
                                               IStatusCodeReExecuteFeature>();
        if (statusCodeReExecuteFeature != null)
        {
            OriginalURL =
                statusCodeReExecuteFeature.OriginalPathBase
                + statusCodeReExecuteFeature.OriginalPath
                + statusCodeReExecuteFeature.OriginalQueryString;
        }
    }
}
```

For a Razor Pages example, see [Pages/MyStatusCode2.cshtml](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x/ErrorHandlingSample/Pages) in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x).

To test `UseStatusCodePages` in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples/5.x), remove the comments from `webBuilder.UseStartup<StartupSCreX>();` in `Program.cs`.

## Disable status code pages

To disable status code pages for an MVC controller or action method, use the [\[SkipStatusCodePages\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.skipstatuscodepagesattribute) attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use [IStatusCodePagesFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodepagesfeature):

```
public void OnGet()
{
    // using Microsoft.AspNetCore.Diagnostics;
    var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature != null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}
```

## Exception-handling code

Code in exception handling pages can also throw exceptions. Production error pages should be tested thoroughly and take extra care to avoid throwing exceptions of their own.

### Response headers

Once the headers for a response are sent:

* The app can't change the response's status code.
* Any exception pages or handlers can't run. The response must be completed or the connection aborted.

## Server exception handling

In addition to the exception handling logic in an app, the [HTTP server implementation](servers/?view=aspnetcore-10.0) can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a `500 - Internal Server Error` response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren't handled by the app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server's exception handling. The app's custom error pages, exception handling middleware, and filters don't affect this behavior.

## Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to [capture startup errors](host/web-host?view=aspnetcore-10.0#capture-startup-errors) and [capture detailed errors](host/web-host?view=aspnetcore-10.0#detailed-errors).

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

* The hosting layer logs a critical exception.
* The dotnet process crashes.
* No error page is displayed when the HTTP server is [Kestrel](servers/kestrel?view=aspnetcore-10.0).

When running on [IIS](/en-us/iis) (or Azure App Service) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), a *502.5 - Process Failure* is returned by the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) if the process can't start. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

## Database error page

The Database developer page exception filter `AddDatabaseDeveloperPageExceptionFilter` captures database-related exceptions that can be resolved by using Entity Framework Core migrations. When these exceptions occur, an HTML response is generated with details of possible actions to resolve the issue. This page is enabled only in the `Development` environment. The following code was generated by the ASP.NET Core Razor Pages templates when individual user accounts were specified:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDatabaseDeveloperPageExceptionFilter();
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}
```

## Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. For more information, see [Filters in ASP.NET Core](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters).

Exception filters are useful for trapping exceptions that occur within MVC actions, but they're not as flexible as the built-in [exception handling middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs), `UseExceptionHandler`. We recommend using `UseExceptionHandler`, unless you need to perform error handling differently based on which MVC action is chosen.

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

## Model state errors

For information about how to handle model state errors, see [Model binding](../mvc/models/model-binding?view=aspnetcore-10.0) and [Model validation](../mvc/models/validation?view=aspnetcore-10.0).

By [Tom Dykstra](https://github.com/tdykstra/), and [Steve Smith](https://ardalis.com/)

This article covers common approaches to handling errors in ASP.NET Core web apps. See [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0) for web APIs.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples). ([How to download](./?view=aspnetcore-10.0#how-to-download-a-sample).)

## Developer Exception Page

The *Developer Exception Page* displays detailed information about request exceptions. The ASP.NET Core templates generate the following code:

```
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
```

The preceding code enables the developer exception page when the app is running in the [`Development` environment](environments?view=aspnetcore-10.0).

The templates place [UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage) before any middleware so exceptions are caught in the middleware that follows.

The preceding code enables the Developer Exception Page **only when the app is running in the `Development` environment**. Detailed exception information should not be displayed publicly when the app runs in production. For more information on configuring environments, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).

The Developer Exception Page includes the following information about the exception and the request:

* Stack trace
* Query string parameters if any
* Cookies if any
* Headers

## Exception handler page

To configure a custom error handling page for the `Production` environment, use the exception handling middleware. The middleware:

* Catches and logs exceptions.
* Re-executes the request in an alternate pipeline for the page or controller indicated. The request isn't re-executed if the response has started. The template generated code re-executes the request to `/Error`.

In the following example, [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) adds the exception handling middleware in non-`Development` environments:

```
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
```

The Razor Pages app template provides an Error page (`.cshtml`) and [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class (`ErrorModel`) in the *Pages* folder. For an MVC app, the project template includes an Error action method and an Error view in the Home controller.

Don't mark the error handler action method with HTTP method attributes, such as `HttpGet`. Explicit verbs prevent some requests from reaching the method. Allow anonymous access to the method if unauthenticated users should see the error view.

### Access the exception

Use [IExceptionHandlerPathFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerpathfeature) to access the exception and the original request path in an error handler controller or page:

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "File error thrown";
        }
        if (exceptionHandlerPathFeature?.Path == "/index")
        {
            ExceptionMessage += " from home page";
        }
    }
}
```

Warning

Do **not** serve sensitive error information to clients. Serving errors is a security risk.

To trigger the preceding exception handling page, set the environment to productions and force an exception.

## Exception handler lambda

An alternative to a [custom exception handler page](#exception-handler-page) is to provide a lambda to [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). Using a lambda allows access to the error before returning the response.

Here's an example of using a lambda for exception handling:

```
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
   app.UseExceptionHandler(errorApp =>
   {
        errorApp.Run(async context =>
        {
            context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
            context.Response.ContentType = "text/html";

            await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
            await context.Response.WriteAsync("ERROR!<br><br>\r\n");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync("File error thrown!<br><br>\r\n");
            }

            await context.Response.WriteAsync("<a href=\"/\">Home</a><br>\r\n");
            await context.Response.WriteAsync("</body></html>\r\n");
            await context.Response.WriteAsync(new string(' ', 512)); // IE padding
        });
    });
    app.UseHsts();
}
```

In the preceding code, `await context.Response.WriteAsync(new string(' ', 512));` is added so the Internet Explorer browser displays the error message rather than an IE error message. For more information, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/16144).

Warning

Do **not** serve sensitive error information from [IExceptionHandlerFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerfeature) or [IExceptionHandlerPathFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandlerpathfeature) to clients. Serving errors is a security risk.

To see the result of the exception handling lambda in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples), use the `ProdEnvironment` and `ErrorHandlerLambda` preprocessor directives, and select **Trigger an exception** on the home page.

## UseStatusCodePages

By default, an ASP.NET Core app doesn't provide a status code page for HTTP status codes, such as *404 - Not Found*. The app returns a status code and an empty response body. To provide status code pages, use Status Code Pages middleware.

The middleware is made available by the [Microsoft.AspNetCore.Diagnostics](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics/) package.

To enable default text-only handlers for common error status codes, call [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) in the `Startup.Configure` method:

```
app.UseStatusCodePages();
```

Call `UseStatusCodePages` before request handling middleware (for example, static file middleware and MVC middleware).

When `UseStatusCodePages` isn't used, navigating to a URL without an endpoint returns a browser dependent error message indicating the endpoint can't be found. For example, navigating to `Home/Privacy2`. When `UseStatusCodePages` is called, the browser returns:

```
Status Code: 404; Not Found
```

## UseStatusCodePages with format string

To customize the response content type and text, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a content type and format string:

```
app.UseStatusCodePages(
    "text/plain", "Status code page, status code: {0}");
```

## UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) that takes a lambda expression:

```
app.UseStatusCodePages(async context =>
{
    context.HttpContext.Response.ContentType = "text/plain";

    await context.HttpContext.Response.WriteAsync(
        "Status code page, status code: " +
        context.HttpContext.Response.StatusCode);
});
```

## UseStatusCodePagesWithRedirects

The [UseStatusCodePagesWithRedirects](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithredirects) extension method:

* Sends a *302 - Found* status code to the client.
* Redirects the client to the location provided in the URL template.

```
app.UseStatusCodePagesWithRedirects("/StatusCode?code={0}");
```

The URL template can include a `{0}` placeholder for the status code, as shown in the example. If the URL template starts with `~` (tilde), the `~` is replaced by the app's `PathBase`. If you point to an endpoint within the app, create an MVC view or Razor page for the endpoint. For a Razor Pages example, see `Pages/StatusCode.cshtml` in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples).

This method is commonly used when the app:

* Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client's browser address bar reflects the redirected endpoint.
* Shouldn't preserve and return the original status code with the initial redirect response.

## UseStatusCodePagesWithReExecute

The [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) extension method:

* Returns the original status code to the client.
* Generates the response body by re-executing the request pipeline using an alternate path.

```
app.UseStatusCodePagesWithReExecute("/StatusCode","?code={0}");
```

If you point to an endpoint within the app, create an MVC view or Razor page for the endpoint. Ensure `UseStatusCodePagesWithReExecute` is placed before `UseRouting` so the request can be rerouted to the status page. For a Razor Pages example, see `Pages/StatusCode.cshtml` in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples).

This method is commonly used when the app should:

* Process the request without redirecting to a different endpoint. For web apps, the client's browser address bar reflects the originally requested endpoint.
* Preserve and return the original status code with the response.

The URL and query string templates may include a placeholder (`{0}`) for the status code. The URL template must start with a slash (`/`). When using a placeholder in the path, confirm that the endpoint (page or controller) can process the path segment. For example, a Razor Page for errors should accept the optional path segment value with the `@page` directive:

```
@page "{code?}"
```

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

```
var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature != null)
{
    OriginalURL =
        statusCodeReExecuteFeature.OriginalPathBase
        + statusCodeReExecuteFeature.OriginalPath
        + statusCodeReExecuteFeature.OriginalQueryString;
}
```

Don't mark the error handler action method with HTTP method attributes, such as `HttpGet`. Explicit verbs prevent some requests from reaching the method. Allow anonymous access to the method if unauthenticated users should see the error view.

## Disable status code pages

To disable status code pages for an MVC controller or action method, use the [`[SkipStatusCodePages]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.skipstatuscodepagesattribute) attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use [IStatusCodePagesFeature](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.istatuscodepagesfeature):

```
var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

if (statusCodePagesFeature != null)
{
    statusCodePagesFeature.Enabled = false;
}
```

## Exception-handling code

Code in exception handling pages can throw exceptions. It's often a good idea for production error pages to consist of purely static content.

### Response headers

Once the headers for a response are sent:

* The app can't change the response's status code.
* Any exception pages or handlers can't run. The response must be completed or the connection aborted.

## Server exception handling

In addition to the exception handling logic in your app, the [HTTP server implementation](servers/?view=aspnetcore-10.0) can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a *500 - Internal Server Error* response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren't handled by your app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server's exception handling. The app's custom error pages, exception handling middleware, and filters don't affect this behavior.

## Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to [capture startup errors](host/web-host?view=aspnetcore-10.0#capture-startup-errors) and [capture detailed errors](host/web-host?view=aspnetcore-10.0#detailed-errors).

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

* The hosting layer logs a critical exception.
* The dotnet process crashes.
* No error page is displayed when the HTTP server is [Kestrel](servers/kestrel?view=aspnetcore-10.0).

When running on [IIS](/en-us/iis) (or Azure App Service) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), a *502.5 - Process Failure* is returned by the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) if the process can't start. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

## Database error page

Database error page middleware captures database-related exceptions that can be resolved by using Entity Framework migrations. When these exceptions occur, an HTML response with details of possible actions to resolve the issue is generated. This page should be enabled only in the `Development` environment. Enable the page by adding code to `Startup.Configure`:

```
if (env.IsDevelopment())
{
    app.UseDatabaseErrorPage();
}
```

[UseDatabaseErrorPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.databaseerrorpageextensions.usedatabaseerrorpage) requires the [Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore/) NuGet package.

## Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exception that occurs during the execution of a controller action or another filter. For more information, see [Filters in ASP.NET Core](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters).

Tip

Exception filters are useful for trapping exceptions that occur within MVC actions, but they're not as flexible as the exception handling middleware. We recommend using the middleware. Use filters only where you need to perform error handling differently based on which MVC action is chosen.

## Model state errors

For information about how to handle model state errors, see [Model binding](../mvc/models/model-binding?view=aspnetcore-10.0) and [Model validation](../mvc/models/validation?view=aspnetcore-10.0).

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))
* [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core APIs](error-handling-api?view=aspnetcore-10.0)

- [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/error-handling/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))
- [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0)
- [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-microsoft-accounts
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/standalone-with-microsoft-accounts.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Accounts

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to create a [standalone Blazor WebAssembly app](../../hosting-models?view=aspnetcore-10.0#blazor-webassembly) that uses [Microsoft Accounts with Microsoft Entra (ME-ID)](/en-us/security/zero-trust/develop/identity-supported-account-types) for authentication.

1. Navigate to **Microsoft Entra ID** in the Azure portal. Select **App registrations** in the sidebar. Select the **New registration** button.
2. Provide a **Name** for the app (for example, **Blazor Standalone ME-ID MS Accounts**).
3. In **Supported account types**, select **Accounts in any organizational directory (Any Microsoft Entra ID directory – Multitenant)**.
4. Set the **Redirect URI** dropdown list to **Single-page application (SPA)** and provide the following redirect URI: `https://localhost/authentication/login-callback`. If you know the production redirect URI for the Azure default host (for example, `azurewebsites.net`) or the custom domain host (for example, `contoso.com`), you can also add the production redirect URI at the same time that you're providing the `localhost` redirect URI. Be sure to include the port number for non-`:443` ports in any production redirect URIs that you add.
5. If you're using an [unverified publisher domain](/en-us/entra/identity-platform/howto-configure-publisher-domain), clear the **Permissions** > **Grant admin consent to openid and offline\_access permissions** checkbox. If the publisher domain is verified, this checkbox isn't present.
6. Select **Register**.

Note

Supplying the port number for a `localhost` ME-ID redirect URI isn't required. For more information, see [Redirect URI (reply URL) restrictions and limitations: Localhost exceptions (Entra documentation)](/en-us/entra/identity-platform/reply-url#localhost-exceptions).

Record the Application (client) ID (for example, `00001111-aaaa-2222-bbbb-3333cccc4444`).

In **Authentication** > **Platform configurations** > **Single-page application**:

1. Confirm the redirect URI of `https://localhost/authentication/login-callback` is present.
2. In the **Implicit grant** section, ensure that the checkboxes for **Access tokens** and **ID tokens** aren't selected. **Implicit grant isn't recommended for Blazor apps using MSAL v2.0 or later.** For more information, see [Secure ASP.NET Core Blazor WebAssembly](./?view=aspnetcore-10.0#use-the-authorization-code-flow-with-pkce).
3. The remaining defaults for the app are acceptable for this experience.
4. Select the **Save** button if you made changes.

### Create the Blazor app

Create the app. Replace the placeholders in the following command with the information recorded earlier and execute the following command in a command shell:

```
dotnet new blazorwasm -au SingleOrg --client-id "{CLIENT ID}" --tenant-id "common" -o {PROJECT NAME}
```

| Placeholder      | Azure portal name       | Example                                |
| ---------------- | ----------------------- | -------------------------------------- |
| `{PROJECT NAME}` | —                       | `BlazorSample`                         |
| `{CLIENT ID}`    | Application (client) ID | `00001111-aaaa-2222-bbbb-3333cccc4444` |

The output location specified with the `-o|--output` option creates a project folder if it doesn't exist and becomes part of the project's name.

Add a pair of [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions) for `openid` and `offline_access` [DefaultAccessTokenScopes](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.defaultaccesstokenscopes#microsoft-authentication-webassembly-msal-models-msalprovideroptions-defaultaccesstokenscopes):

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
    options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
});
```

  ```
  @using Microsoft.Extensions.Options
  @inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> RemoteOptions
  ```

  Modify the component's redirect in the `OnInitialized` method:

  ```
  - Navigation.NavigateToLogin("authentication/login");
  + Navigation.NavigateToLogin(RemoteOptions.Get(Options.DefaultName)
  +     .AuthenticationPaths.LogInPath);
  ```

Support for authenticating users is registered in the service container with the [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) extension method provided by the [`Microsoft.Authentication.WebAssembly.Msal` package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal). This method sets up all of the services required for the app to interact with the Identity Provider (IP).

In the `Program` file:

```
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});
```

The [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) method accepts a callback to configure the parameters required to authenticate an app. The values required for configuring the app can be obtained from the ME-ID configuration when you register the app.

### `wwwroot/appsettings.json` configuration

Configuration is supplied by the `wwwroot/appsettings.json` file:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/common",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  }
}
```

Example:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/common",
    "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
    "ValidateAuthority": true
  }
}
```

### Access token scopes

The Blazor WebAssembly template doesn't automatically configure the app to request an access token for a secure API. To provision an access token as part of the sign-in flow, add the scope to the default access token scopes of the [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions):

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
```

Specify additional scopes with `AdditionalScopesToConsent`:

```
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE URI}");
```

Note

[AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent) for Microsoft Graph via the Microsoft Entra ID consent UI when a user first uses an app registered in Microsoft Azure. For more information, see [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0\&pivots=graph-sdk-5#defaultaccesstokenscopes-versus-additionalscopestoconsent).

For more information, see the following sections of the *Additional scenarios* article:

* [Request additional access tokens](additional-scenarios?view=aspnetcore-10.0#request-additional-access-tokens)
* [Attach tokens to outgoing requests](additional-scenarios?view=aspnetcore-10.0#attach-tokens-to-outgoing-requests)
* [Quickstart: Configure an application to expose web APIs](/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis)

### Login mode

The framework defaults to pop-up login mode and falls back to redirect login mode if a pop-up can't be opened. Configure MSAL to use redirect login mode by setting the `LoginMode` property of [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions) to `redirect`:

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.LoginMode = "redirect";
});
```

The default setting is `popup`, and the string value isn't case-sensitive.

### Imports file

The [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespace is made available throughout the app via the imports file (`_Imports.razor`):

```
...
@using Microsoft.AspNetCore.Components.Authorization
...
```

### Index page

The Index page (`wwwroot/index.html`) page includes a script that defines the `AuthenticationService` in JavaScript. `AuthenticationService` handles the low-level details of the OIDC protocol. The app internally calls methods defined in the script to perform the authentication operations.

```
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
```

```
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string? Action { get; set; }
}
```

    * [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)
    * [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)
    * [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

  > info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

```
@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthenticationState { get; set; }

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}
```

### Inspect the content of a JSON Web Token (JWT)

To decode a JSON Web Token (JWT), use Microsoft's [jwt.ms](https://jwt.ms/) tool. Values in the UI never leave your browser.

Example encoded JWT (shortened for display):

> eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V\_bU55Ug\_PW4pLPr20tTS8Ct7\_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2\_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

```
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
  "sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]
```

## Related content

* [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0)
* [Build a custom version of the Authentication.MSAL JavaScript library](additional-scenarios?view=aspnetcore-10.0#build-a-custom-version-of-the-authenticationmsal-javascript-library)
* [Unauthenticated or unauthorized web API requests in an app with a secure default client](additional-scenarios?view=aspnetcore-10.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client)
* [ASP.NET Core Blazor WebAssembly with Microsoft Entra ID groups and roles](microsoft-entra-id-groups-and-roles?view=aspnetcore-10.0)
* [Quickstart: Register an application with the Microsoft identity platform](/en-us/entra/identity-platform/quickstart-register-app#register-a-new-application-using-the-azure-portal)
* [Quickstart: Configure an application to expose web APIs](/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/machine-wide-policy
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/configuration/machine-wide-policy.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Data Protection machine-wide policy support in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

When running on Windows, the Data Protection system has limited support for setting a default machine-wide policy for all apps that consume ASP.NET Core Data Protection. The general idea is that an administrator might wish to change a default setting, such as the algorithms used or key lifetime, without the need to manually update every app on the machine.

Warning

The system administrator can set default policy, but they can't enforce it. The app developer can always override any value with one of their own choosing. The default policy only affects apps where the developer hasn't specified an explicit value for a setting.

## Setting default policy

To set default policy, an administrator can set known values in the system registry under the following registry key:

**HKLM\SOFTWARE\Microsoft\DotNetPackages\Microsoft.AspNetCore.DataProtection**

If you're on a 64-bit operating system and want to affect the behavior of 32-bit apps, remember to configure the Wow6432Node equivalent of the above key.

The supported values are shown below.

| Value              | Type   | Description                                                                                                                                                                                                                                                                                                  |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| EncryptionType     | string | Specifies which algorithms should be used for data protection. The value must be CNG-CBC, CNG-GCM, or Managed and is described in more detail below.                                                                                                                                                         |
| DefaultKeyLifetime | DWORD  | Specifies the lifetime for newly-generated keys. The value is specified in days and must be >= 7.                                                                                                                                                                                                            |
| KeyEscrowSinks     | string | Specifies the types that are used for key escrow. The value is a semicolon-delimited list of key escrow sinks, where each element in the list is the assembly-qualified name of a type that implements [IKeyEscrowSink](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.keymanagement.ikeyescrowsink). |

## Encryption types

If EncryptionType is CNG-CBC, the system is configured to use a CBC-mode symmetric block cipher for confidentiality and HMAC for authenticity with services provided by Windows CNG (see [Specifying custom Windows CNG algorithms](overview?view=aspnetcore-10.0#specifying-custom-windows-cng-algorithms) for more details). The following additional values are supported, each of which corresponds to a property on the CngCbcAuthenticatedEncryptionSettings type.

| Value                       | Type   | Description                                                                                             |
| --------------------------- | ------ | ------------------------------------------------------------------------------------------------------- |
| EncryptionAlgorithm         | string | The name of a symmetric block cipher algorithm understood by CNG. This algorithm is opened in CBC mode. |
| EncryptionAlgorithmProvider | string | The name of the CNG provider implementation that can produce the algorithm EncryptionAlgorithm.         |
| EncryptionAlgorithmKeySize  | DWORD  | The length (in bits) of the key to derive for the symmetric block cipher algorithm.                     |
| HashAlgorithm               | string | The name of a hash algorithm understood by CNG. This algorithm is opened in HMAC mode.                  |
| HashAlgorithmProvider       | string | The name of the CNG provider implementation that can produce the algorithm HashAlgorithm.               |

If EncryptionType is CNG-GCM, the system is configured to use a Galois/Counter Mode symmetric block cipher for confidentiality and authenticity with services provided by Windows CNG (see [Specifying custom Windows CNG algorithms](overview?view=aspnetcore-10.0#specifying-custom-windows-cng-algorithms) for more details). The following additional values are supported, each of which corresponds to a property on the CngGcmAuthenticatedEncryptionSettings type.

| Value                       | Type   | Description                                                                                                        |
| --------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------ |
| EncryptionAlgorithm         | string | The name of a symmetric block cipher algorithm understood by CNG. This algorithm is opened in Galois/Counter Mode. |
| EncryptionAlgorithmProvider | string | The name of the CNG provider implementation that can produce the algorithm EncryptionAlgorithm.                    |
| EncryptionAlgorithmKeySize  | DWORD  | The length (in bits) of the key to derive for the symmetric block cipher algorithm.                                |

If EncryptionType is Managed, the system is configured to use a managed SymmetricAlgorithm for confidentiality and KeyedHashAlgorithm for authenticity (see [Specifying custom managed algorithms](overview?view=aspnetcore-10.0#specifying-custom-managed-algorithms) for more details). The following additional values are supported, each of which corresponds to a property on the ManagedAuthenticatedEncryptionSettings type.

| Value                      | Type   | Description                                                                       |
| -------------------------- | ------ | --------------------------------------------------------------------------------- |
| EncryptionAlgorithmType    | string | The assembly-qualified name of a type that implements SymmetricAlgorithm.         |
| EncryptionAlgorithmKeySize | DWORD  | The length (in bits) of the key to derive for the symmetric encryption algorithm. |
| ValidationAlgorithmType    | string | The assembly-qualified name of a type that implements KeyedHashAlgorithm.         |

If EncryptionType has any other value other than null or empty, the Data Protection system throws an exception at startup.

Warning

When configuring a default policy setting that involves type names (EncryptionAlgorithmType, ValidationAlgorithmType, KeyEscrowSinks), the types must be available to the app. For ASP.NET Core apps, the packages that contain these types should be installed.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/publish-to-azure-webapp-using-vs
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/publish-to-azure-webapp-using-vs.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Publish an ASP.NET Core app to Azure with Visual Studio

Important

**ASP.NET Core preview releases with Azure App Service**

ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core preview release, see [Deploy ASP.NET Core preview release to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service).

To troubleshoot an App Service deployment issue, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

## Set up

Open a [free Azure account](https://azure.microsoft.com/pricing/purchase-options/azure-account?cid=msft_learn) if you don't have one.

## Create a web app

Start Visual Studio 2022 and select **Create a new project**.

In the **Create a new project** dialog, select **ASP.NET Core Web App**, and then select **Next**.

In the **Configure your new project** dialog, name your project, and then select **Next**.

In the **Additional information** dialog:

* In the **Framework** input, select **.NET 7.0 (Standard Term Support)**.
* In the **Authentication type** input, select **Individual Accounts**, and then select **Create**.

Visual Studio creates the solution.

## Run the app

Press `F5` to run the project.

### Register a user

* Select **Register** and register a new user. You can use a fictitious email address. When you submit, the page displays the following error:

  *"A database operation failed while processing the request. Applying existing migrations may resolve this issue"*

* Select **Apply Migrations** and, once the page updates, refresh the page.

* A **Register confirmation** page is displayed. Select **Click here to confirm your account**.

* A **Confirm email** page is displayed.

* Log in as the new user.

The app displays the email used to register the new user and a **Logout** link.

* Stop the application by closing the browser, or in Visual Studio select **Debug** > **Stop Debugging**.
* In Visual Studio select **Build** > **Clean Solution** to clean project items and avoid file contention.

## Deploy the app to Azure

Right-click on the project in Solution Explorer and select **Publish**.

In the **Publish** dialog:

* Select **Azure**.
* Select **Next**.

In the **Publish** dialog:

* Select **Azure App Service (Windows)**.
* Select **Next**.

In the **Publish** dialog, in the **App Service** tab, select **Create new**.

The **Create App Service** dialog appears:

* The **Name**, **Resource Group**, and **Hosting Plan** entry fields are populated. You can keep these names or change them.
* Select **Create**.

After creation is completed the dialog is automatically closed and the **Publish** dialog gets focus again:

* The new instance that was just created is automatically selected.
* Select **Finish**.

The **Publish profile creation progress** dialog confirms the publish profile was created. Select **Close**.

Next you see the **Publish Profile summary** page. Visual Studio has detected that this application requires a SQL Server database which it has listed in the Service Dependencies pane. Select the ellipsis (**...**) and then **Connect**.

The **Connect to dependency** dialog appears:

* Select **Azure SQL Database**.
* Select **Next**.

In the **Connect to Azure SQL database** dialog, select **Create new**.

The **Create Azure SQL Database** appears:

* The **Database name**, **Resource Group**, **Database server** and **App Service Plan** entry fields are populated. You can keep these values or change them.
* Enter the **Database administrator username** and **Database administrator password** for the selected **Database server** (note the account you use must have the necessary permissions to create the new Azure SQL database)
* Select **Create**.

After creation is completed the dialog is automatically closed and the **Connect to Azure SQL Database** dialog gets focus again:

* The new instance that was just created is automatically selected.
* Select **Next**.

In the next step of the **Connect to Azure SQL Database** dialog:

* Enter the **Database connection user name** and **Database connection password** fields. These are the details your application will use to connect to the database at runtime. Best practice is to avoid using the same details as the admin username and password used in the previous step.
* Select **Finish**.

The **Dependency configuration progress** dialog confirms the Azure SQL Database is configured. Select **Close**.

In the **Publish Profile summary** page select **More actions** > **Edit**:

On the **Settings** tab of the **Publish** dialog:

* Expand **Databases** and check **Use this connection string at runtime**.

* Expand **Entity Framework Migrations** and select **Apply this migration on publish**.

* Select **Save**. Visual Studio returns to the **Publish** dialog.

Click **Publish**. Visual Studio publishes your app to Azure. When the deployment completes.

The app is opened in a browser. Register a new user and log in as the new user to validate the database deployment and run-time connection.

### Update the app

* Edit the `Pages/Index.cshtml` Razor page and change its contents, then save the changes. For example, you can modify the paragraph to say "Hello ASP.NET Core!":

  ```
  @page
  @model IndexModel
  @{
      ViewData["Title"] = "Home page";
  }

  <div class="text-center">
      <h1 class="display-4">Welcome</h1>
      <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
      <p>Hello ASP.NET Core!</p>
  </div>
  ```

* Select **Publish** from the **Publish Profile summary** page again.

* After the app is published, refresh the page and verify that the changes you made are available on Azure.

### Clean up

When you have finished testing the app, go to the [Azure portal](https://portal.azure.com/) and delete the app.

* Select **Resource groups**, then select the resource group you created.

* In the **Resource group** page, select **Delete resource group**.

* Enter the name of the resource group and select **Delete**. Your app and all other resources created in this tutorial are now deleted from Azure.

## Related content

* [Azure App Service](/en-us/azure/app-service/app-service-web-overview)
* [Azure resource groups](/en-us/azure/azure-resource-manager/resource-group-overview#resource-groups)
* [Azure SQL Database](/en-us/azure/sql-database/)
* [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](../host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-10.0)
* [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/compatibility/replacing-machinekey
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/compatibility/replacing-machinekey.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Replace the ASP.NET machineKey in ASP.NET Core

[]()

The implementation of the `<machineKey>` element in ASP.NET [is replaceable](https://blogs.msdn.microsoft.com/webdev/2012/10/23/cryptographic-improvements-in-asp-net-4-5-pt-2/). This allows most calls to ASP.NET cryptographic routines to be routed through a replacement data protection mechanism, including the new data protection system.

## Package installation

Note

The new data protection system can only be installed into an existing ASP.NET application targeting .NET Framework 4.5.1 or later. Installation will fail if the application targets .NET Framework 4.5 or lower.

To install the new data protection system into an existing .NET Framework 4.5.1+ project, install the package Microsoft.AspNetCore.DataProtection.SystemWeb. This will instantiate the data protection system using the [default configuration](../configuration/default-settings?view=aspnetcore-10.0) settings.

When you install the package, it inserts a line into *Web.config* that tells ASP.NET to use it for [most cryptographic operations](https://blogs.msdn.microsoft.com/webdev/2012/10/23/cryptographic-improvements-in-asp-net-4-5-pt-2/), including forms authentication, view state, and calls to MachineKey.Protect. It does not use the [data protection API](../using-data-protection?view=aspnetcore-10.0). The line that's inserted reads as follows.

```
<machineKey compatibilityMode="Framework45" dataProtectorType="..." />
```

Tip

You can tell if the new data protection system is active by inspecting fields like `__VIEWSTATE`, which should begin with "CfDJ8" as in the example below. "CfDJ8" is the base64 representation of the magic "09 F0 C9 F0" header that identifies a payload protected by the data protection system.

```
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="CfDJ8AWPr2EQPTBGs3L2GCZOpk...">
```

## Package configuration

The data protection system is instantiated with a default zero-setup configuration. However, since by default keys are persisted to the local file system, this won't work for applications which are deployed in a farm. To resolve this, you can provide configuration by creating a type which subclasses DataProtectionStartup and overrides its ConfigureServices method.

Below is an example of a custom data protection startup type which configured both where keys are persisted and how they're encrypted at rest. It also overrides the default app isolation policy by providing its own application name.

```
using System;
using System.IO;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.SystemWeb;
using Microsoft.Extensions.DependencyInjection;

namespace DataProtectionDemo
{
    public class MyDataProtectionStartup : DataProtectionStartup
    {
        public override void ConfigureServices(IServiceCollection services)
        {
            services.AddDataProtection()
                .SetApplicationName("my-app")
                .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\myapp-keys\"))
                .ProtectKeysWithCertificate("thumbprint");
        }
    }
}
```

Tip

You can also use `<machineKey applicationName="my-app" ... />` in place of an explicit call to SetApplicationName. This is a convenience mechanism to avoid forcing the developer to create a DataProtectionStartup-derived type if all they wanted to configure was setting the application name.

To enable this custom configuration, go back to Web.config and look for the `<appSettings>` element that the package install added to the config file. It will look like the following markup:

```
<appSettings>
  <!--
  If you want to customize the behavior of the ASP.NET Core Data Protection stack, set the
  "aspnet:dataProtectionStartupType" switch below to be the fully-qualified name of a
  type which subclasses Microsoft.AspNetCore.DataProtection.SystemWeb.DataProtectionStartup.
  -->
  <add key="aspnet:dataProtectionStartupType" value="" />
</appSettings>
```

Fill in the blank value with the assembly-qualified name of the DataProtectionStartup-derived type you just created. If the name of the application is DataProtectionDemo, this would look like the below.

```
<add key="aspnet:dataProtectionStartupType"
     value="DataProtectionDemo.MyDataProtectionStartup, DataProtectionDemo" />
```

The newly-configured data protection system is now ready for use inside the application.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/consumer-apis/overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Consumer APIs overview for ASP.NET Core

The `IDataProtectionProvider` and `IDataProtector` interfaces are the basic interfaces through which consumers use the data protection system. They're located in the [Microsoft.AspNetCore.DataProtection.Abstractions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Abstractions/) package.

## IDataProtectionProvider

The provider interface represents the root of the data protection system. It cannot directly be used to protect or unprotect data. Instead, the consumer must get a reference to an `IDataProtector` by calling `IDataProtectionProvider.CreateProtector(purpose)`, where purpose is a string that describes the intended consumer use case. See [Purpose Strings](purpose-strings?view=aspnetcore-10.0) for much more information on the intent of this parameter and how to choose an appropriate value.

## IDataProtector

The protector interface is returned by a call to `CreateProtector`, and it's this interface which consumers can use to perform protect and unprotect operations.

To protect a piece of data, pass the data to the `Protect` method. The basic interface defines a method which converts byte\[] -> byte\[], but there's also an overload (provided as an extension method) which converts string -> string. The security offered by the two methods is identical; the developer should choose whichever overload is most convenient for their use case. Irrespective of the overload chosen, the value returned by the Protect method is now protected (enciphered and tamper-proofed), and the application can send it to an untrusted client.

To unprotect a previously-protected piece of data, pass the protected data to the `Unprotect` method. (There are byte\[]-based and string-based overloads for developer convenience.) If the protected payload was generated by an earlier call to `Protect` on this same `IDataProtector`, the `Unprotect` method will return the original unprotected payload. If the protected payload has been tampered with or was produced by a different `IDataProtector`, the `Unprotect` method will throw CryptographicException.

The concept of same vs. different `IDataProtector` ties back to the concept of purpose. If two `IDataProtector` instances were generated from the same root `IDataProtectionProvider` but via different purpose strings in the call to `IDataProtectionProvider.CreateProtector`, then they're considered [different protectors](purpose-strings?view=aspnetcore-10.0), and one won't be able to unprotect payloads generated by the other.

## Consuming these interfaces

For a DI-aware component, the intended usage is that the component takes an `IDataProtectionProvider` parameter in its constructor and that the DI system automatically provides this service when the component is instantiated.

Note

Some applications (such as console applications or ASP.NET 4.x applications) might not be DI-aware so cannot use the mechanism described here. For these scenarios consult the [Non DI Aware Scenarios](../configuration/non-di-scenarios?view=aspnetcore-10.0) document for more information on getting an instance of an `IDataProtection` provider without going through DI.

The following sample demonstrates three concepts:

1. [Add the data protection system](../configuration/overview?view=aspnetcore-10.0) to the service container,

2. Using DI to receive an instance of an `IDataProtectionProvider`, and

3. Creating an `IDataProtector` from an `IDataProtectionProvider` and using it to protect and unprotect data.

## Console app

```
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;

public class Program
{
    public static void Main(string[] args)
    {
        // add data protection services
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddDataProtection();
        var services = serviceCollection.BuildServiceProvider();

        // create an instance of MyClass using the service provider
        var instance = ActivatorUtilities.CreateInstance<MyClass>(services);
        instance.RunSample();
    }

    public class MyClass
    {
        IDataProtector _protector;

        // the 'provider' parameter is provided by DI
        public MyClass(IDataProtectionProvider provider)
        {
            _protector = provider.CreateProtector("Contoso.MyClass.v1");
        }

        public void RunSample()
        {
            Console.Write("Enter input: ");
            string input = Console.ReadLine();

            // protect the payload
            string protectedPayload = _protector.Protect(input);
            Console.WriteLine($"Protect returned: {protectedPayload}");

            // unprotect the payload
            string unprotectedPayload = _protector.Unprotect(protectedPayload);
            Console.WriteLine($"Unprotect returned: {unprotectedPayload}");
        }
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Enter input: Hello world!
 * Protect returned: CfDJ8ICcgQwZZhlAlTZT...OdfH66i1PnGmpCR5e441xQ
 * Unprotect returned: Hello world!
 */
```

## Web app

Call [AddDataProtection(IServiceCollection, Action\<DataProtectionOptions>)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.dataprotectionservicecollectionextensions.adddataprotection#microsoft-extensions-dependencyinjection-dataprotectionservicecollectionextensions-adddataprotection\(microsoft-extensions-dependencyinjection-iservicecollection-system-action\(\(microsoft-aspnetcore-dataprotection-dataprotectionoptions\)\)\)) in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDataProtection();

var app = builder.Build();
```

The following highlighted code shows how to use [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector) in a controller:

```
public class HomeController : Controller
{
    private readonly IDataProtector _dataProtector;

    public HomeController(IDataProtectionProvider dataProtectionProvider)
    {
        _dataProtector = dataProtectionProvider.CreateProtector("HomeControllerPurpose");
    }

    // ...

    public IActionResult Privacy()
    {
        // The original data to protect
        string originalData = "original data";

        // Protect the data (encrypt)
        string protectedData = _dataProtector.Protect(originalData);
        Console.WriteLine($"Protected Data: {protectedData}");

        // Unprotect the data (decrypt)
        string unprotectedData = _dataProtector.Unprotect(protectedData);
        Console.WriteLine($"Unprotected Data: {unprotectedData}");

        return View();
    }
    
    // ...
```

The package `Microsoft.AspNetCore.DataProtection.Abstractions` contains an extension method [GetDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectioncommonextensions.getdataprotector) as a developer convenience. It encapsulates as a single operation both retrieving an [IDataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotectionprovider) from the service provider and calling [`IDataProtectionProvider.CreateProtector`](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotectionprovider.createprotector). The following sample demonstrates its usage:

```
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
 
public class Program
{
    public static void Main(string[] args)
    {
        // add data protection services
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddDataProtection();
        var services = serviceCollection.BuildServiceProvider();
 
        // get an IDataProtector from the IServiceProvider
        var protector = services.GetDataProtector("Contoso.Example.v2");
        Console.Write("Enter input: ");
        string input = Console.ReadLine();
 
        // protect the payload
        string protectedPayload = protector.Protect(input);
        Console.WriteLine($"Protect returned: {protectedPayload}");
 
        // unprotect the payload
        string unprotectedPayload = protector.Unprotect(protectedPayload);
        Console.WriteLine($"Unprotect returned: {unprotectedPayload}");
    }
}
```

Tip

Instances of `IDataProtectionProvider` and `IDataProtector` are thread-safe for multiple callers. It's intended that once a component gets a reference to an `IDataProtector` via a call to `CreateProtector`, it will use that reference for multiple calls to `Protect` and `Unprotect`. A call to `Unprotect` will throw CryptographicException if the protected payload cannot be verified or deciphered. Some components may wish to ignore errors during unprotect operations; a component which reads authentication cookies might handle this error and treat the request as if it had no cookie at all rather than fail the request outright. Components which want this behavior should specifically catch CryptographicException instead of swallowing all exceptions.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-06-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/timeouts
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/timeouts.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Request timeouts middleware in ASP.NET Core

By [Tom Dykstra](https://github.com/tdykstra)

Apps can apply timeout limits selectively to requests. ASP.NET Core servers don't do this by default since request processing times vary widely by scenario. For example, WebSockets, static files, and calling expensive APIs would each require a different timeout limit. So ASP.NET Core provides middleware that configures timeouts per endpoint as well as a global timeout.

When a timeout limit is hit, a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) in [HttpContext.RequestAborted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted) has [IsCancellationRequested](/en-us/dotnet/api/system.threading.cancellationtoken.iscancellationrequested#system-threading-cancellationtoken-iscancellationrequested) set to `true`. [Abort()](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.abort#microsoft-aspnetcore-http-httpcontext-abort) isn't automatically called on the request, so the application may still produce a success or failure response. The default behavior if the app doesn't handle the exception and produce a response is to return status code 504.

This article explains how to configure the timeout middleware. The timeout middleware can be used in all types of ASP.NET Core apps: Minimal API, Web API with controllers, MVC, and Razor Pages. The sample app is a Minimal API, but every timeout feature it illustrates is also supported in the other app types.

Request timeouts are in the [Microsoft.AspNetCore.Http.Timeouts](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts) namespace.

***Note:*** When an app is running in debug mode, the timeout middleware doesn't trigger. This behavior is the same as for [Kestrel timeouts](../fundamentals/servers/kestrel/options?view=aspnetcore-10.0#behavior-with-debugger-attached). To test timeouts, run the app without the debugger attached.

## Add the middleware to the app

Add the request timeouts middleware to the service collection by calling [AddRequestTimeouts](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.requesttimeoutsiservicecollectionextensions.addrequesttimeouts).

Add the middleware to the request processing pipeline by calling [UseRequestTimeouts](/en-us/dotnet/api/microsoft.aspnetcore.builder.requesttimeoutsiapplicationbuilderextensions.userequesttimeouts).

Note

* In apps that explicitly call [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting), `UseRequestTimeouts` must be called after `UseRouting`.

Adding the middleware to the app doesn't automatically start triggering timeouts. Timeout limits have to be explicitly configured.

## Configure one endpoint or page

For Minimal API apps, configure an endpoint to timeout by calling [WithRequestTimeout](/en-us/dotnet/api/microsoft.aspnetcore.builder.requesttimeoutsiendpointconventionbuilderextensions.withrequesttimeout), or by applying the [`[RequestTimeout]`](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts.requesttimeoutattribute) attribute, as shown in the following example:

```
using Microsoft.AspNetCore.Http.Timeouts;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRequestTimeouts();

var app = builder.Build();
app.UseRequestTimeouts();

app.MapGet("/", async (HttpContext context) => {
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    }
    catch (TaskCanceledException)
    {
        return Results.Content("Timeout!", "text/plain");
    }

    return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout(TimeSpan.FromSeconds(2));
// Returns "Timeout!"

app.MapGet("/attribute",
    [RequestTimeout(milliseconds: 2000)] async (HttpContext context) => {
        try
        {
            await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
        }
        catch (TaskCanceledException)
        {
            return Results.Content("Timeout!", "text/plain");
        }

        return Results.Content("No timeout!", "text/plain");
    });
// Returns "Timeout!"

app.Run();
```

For apps with controllers, apply the `[RequestTimeout]` attribute to the action method or the controller class. For Razor Pages apps, apply the attribute to the Razor page class.

## Configure multiple endpoints or pages

Create named *policies* to specify timeout configuration that applies to multiple endpoints. Add a policy by calling [AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts.requesttimeoutoptions.addpolicy):

```
builder.Services.AddRequestTimeouts(options => {
    options.DefaultPolicy =
        new RequestTimeoutPolicy { Timeout = TimeSpan.FromMilliseconds(1500) };
    options.AddPolicy("MyPolicy", TimeSpan.FromSeconds(2));
});
```

A timeout can be specified for an endpoint by policy name:

```
app.MapGet("/namedpolicy", async (HttpContext context) => {
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    }
    catch (TaskCanceledException)
    {
        return Results.Content("Timeout!", "text/plain");
    }

    return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout("MyPolicy");
// Returns "Timeout!"
```

The `[RequestTimeout]` attribute can also be used to specify a named policy.

### Set global default timeout policy

Specify a policy for the global default timeout configuration:

```
builder.Services.AddRequestTimeouts(options => {
    options.DefaultPolicy =
        new RequestTimeoutPolicy { Timeout = TimeSpan.FromMilliseconds(1500) };
    options.AddPolicy("MyPolicy", TimeSpan.FromSeconds(2));
});
```

The default timeout applies to endpoints that don't have a timeout specified. The following endpoint code checks for a timeout although it doesn't call the extension method or apply the attribute. The global timeout configuration applies, so the code checks for a timeout:

```
app.MapGet("/", async (HttpContext context) => {
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    }
    catch
    {
        return Results.Content("Timeout!", "text/plain");
    }

    return Results.Content("No timeout!", "text/plain");
});
// Returns "Timeout!" due to default policy.
```

## Specify the status code in a policy

The [RequestTimeoutPolicy](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts.requesttimeoutpolicy) class has a property that can automatically set the status code when a timeout is triggered.

```
builder.Services.AddRequestTimeouts(options => {
    options.DefaultPolicy = new RequestTimeoutPolicy {
        Timeout = TimeSpan.FromMilliseconds(1000),
        TimeoutStatusCode = 503
    };
    options.AddPolicy("MyPolicy2", new RequestTimeoutPolicy {
        Timeout = TimeSpan.FromMilliseconds(1000),
        WriteTimeoutResponse = async (HttpContext context) => {
            context.Response.ContentType = "text/plain";
            await context.Response.WriteAsync("Timeout from MyPolicy2!");
        }
    });
});
```

```
app.MapGet("/", async (HttpContext context) => {
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    }
    catch (TaskCanceledException)
    {
        throw;
    }

    return Results.Content("No timeout!", "text/plain");
});
// Returns status code 503 due to default policy.
```

## Use a delegate in a policy

The `RequestTimeoutPolicy` class has a [WriteTimeoutResponse](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts.requesttimeoutpolicy.writetimeoutresponse#microsoft-aspnetcore-http-timeouts-requesttimeoutpolicy-writetimeoutresponse) property that can be used to customize the response when a timeout is triggered.

```
builder.Services.AddRequestTimeouts(options => {
    options.DefaultPolicy = new RequestTimeoutPolicy {
        Timeout = TimeSpan.FromMilliseconds(1000),
        TimeoutStatusCode = 503
    };
    options.AddPolicy("MyPolicy2", new RequestTimeoutPolicy {
        Timeout = TimeSpan.FromMilliseconds(1000),
        WriteTimeoutResponse = async (HttpContext context) => {
            context.Response.ContentType = "text/plain";
            await context.Response.WriteAsync("Timeout from MyPolicy2!");
        }
    });
});
```

```
app.MapGet("/usepolicy2", async (HttpContext context) => {
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    }
    catch (TaskCanceledException)
    {
        throw;
    }

    return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout("MyPolicy2");
// Returns "Timeout from MyPolicy2!" due to WriteTimeoutResponse in MyPolicy2.
```

## Disable timeouts

To disable all timeouts including the default global timeout, use the [`[DisableRequestTimeout]`](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts.disablerequesttimeoutattribute) attribute or the [DisableRequestTimeout](/en-us/dotnet/api/microsoft.aspnetcore.builder.requesttimeoutsiendpointconventionbuilderextensions.disablerequesttimeout) extension method:

```
app.MapGet("/disablebyattr", [DisableRequestTimeout] async (HttpContext context) => {
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    }
    catch
    {
        return Results.Content("Timeout!", "text/plain");
    }

    return Results.Content("No timeout!", "text/plain");
});
// Returns "No timeout!", ignores default timeout.
```

```
app.MapGet("/disablebyext", async (HttpContext context) => {
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    }
    catch
    {
        return Results.Content("Timeout!", "text/plain");
    }

    return Results.Content("No timeout!", "text/plain");
}).DisableRequestTimeout();
// Returns "No timeout!", ignores default timeout.
```

## Cancel a timeout

To cancel a timeout that has already been started, use the [DisableTimeout()](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts.ihttprequesttimeoutfeature.disabletimeout#microsoft-aspnetcore-http-timeouts-ihttprequesttimeoutfeature-disabletimeout) method on [IHttpRequestTimeoutFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts.ihttprequesttimeoutfeature). Timeouts cannot be canceled after they've expired.

```
app.MapGet("/canceltimeout", async (HttpContext context) => {
    var timeoutFeature = context.Features.Get<IHttpRequestTimeoutFeature>();
    timeoutFeature?.DisableTimeout();

    try
    {
        await Task.Delay(TimeSpan.FromSeconds(10), context.RequestAborted);
    } 
    catch (TaskCanceledException)
    {
        return Results.Content("Timeout!", "text/plain");
    }

    return Results.Content("No timeout!", "text/plain");
}).WithRequestTimeout(TimeSpan.FromSeconds(1));
// Returns "No timeout!" since the default timeout is not triggered.
```

## See also

* [Microsoft.AspNetCore.Http.Timeouts](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts)
* [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/10/razor-runtime-compilation-obsolete
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/breaking-changes/10/razor-runtime-compilation-obsolete.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Razor runtime compilation is obsolete

Razor runtime compilation is obsolete and is not recommended for production scenarios. For production scenarios, use the default build-time compilation. For development scenarios, use [Hot Reload](/en-us/aspnet/core/test/hot-reload) instead.

## Version introduced

.NET 10 Preview 7

## Previous behavior

Previously, you could use [Razor runtime compilation](/en-us/aspnet/core/mvc/views/view-compilation) to recompile `.cshtml` files while the application was running. This meant you didn't need to restart the application for changes to take effect.

## New behavior

Starting in .NET 10, use of the [affected APIs](#affected-apis) produces a compiler warning with diagnostic ID `ASPDEPR003`:

> warning ASPDEPR003: Razor runtime compilation is obsolete and is not recommended for production scenarios. For production scenarios, use the default build time compilation. For development scenarios, use Hot Reload instead. For more information, visit <https://aka.ms/aspnet/deprecate/003>.

## Type of breaking change

This change can affect [source compatibility](/en-us/dotnet/core/compatibility/categories#source-compatibility).

## Reason for change

Razor runtime compilation has been replaced by [Hot Reload](/en-us/aspnet/core/test/hot-reload), which has been the recommended approach for a few years now. This change makes it clearer that Razor runtime compilation doesn't get support for new features and should no longer be used.

## Recommended action

Remove calls to [AddRazorRuntimeCompilation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorruntimecompilationmvcbuilderextensions.addrazorruntimecompilation) and use [Hot Reload](/en-us/aspnet/core/test/hot-reload) instead.

## Affected APIs

* [Microsoft.AspNetCore.Mvc.ApplicationParts.AssemblyPartExtensions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.assemblypartextensions)
* [Microsoft.Extensions.DependencyInjection.RazorRuntimeCompilationMvcBuilderExtensions.AddRazorRuntimeCompilation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorruntimecompilationmvcbuilderextensions.addrazorruntimecompilation)
* [Microsoft.Extensions.DependencyInjection.RazorRuntimeCompilationMvcCoreBuilderExtensions.AddRazorRuntimeCompilation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorruntimecompilationmvccorebuilderextensions.addrazorruntimecompilation)
* [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.FileProviderRazorProjectItem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.runtimecompilation.fileproviderrazorprojectitem)
* [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.MvcRazorRuntimeCompilationOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.runtimecompilation.mvcrazorruntimecompilationoptions)

## See also

* [.NET Hot Reload support for ASP.NET Core](/en-us/aspnet/core/test/hot-reload)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/web-config
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/web-config.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# `web.config` file

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The `web.config` is a file that's read by IIS and the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0) to configure an app hosted with IIS.

## `web.config` file location

In order to set up the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0) correctly, the `web.config` file must be present at the [content root](../../fundamentals/?view=aspnetcore-10.0#content-root) path (typically the app base path) of the deployed app. This is the same location as the website physical path provided to IIS. The `web.config` file is required at the root of the app to enable the publishing of multiple apps using Web Deploy.

Sensitive files exist on the app's physical path, such as `{ASSEMBLY}.runtimeconfig.json`, `{ASSEMBLY}.xml` (XML Documentation comments), and `{ASSEMBLY}.deps.json`, where the placeholder `{ASSEMBLY}` is the assembly name. When the `web.config` file is present and the site starts normally, IIS doesn't serve these sensitive files if they're requested. If the `web.config` file is missing, incorrectly named, or unable to configure the site for normal startup, IIS might serve sensitive files publicly.

**The `web.config` file must be present in the deployment at all times, correctly named, and able to configure the site for normal start up. Never remove the `web.config` file from a production deployment.**

If a `web.config` file isn't present in the project, the file is created with the correct `processPath` and `arguments` to configure the ASP.NET Core Module and moved to [published output](../directory-structure?view=aspnetcore-10.0).

If a `web.config` file is present in the project, the file is transformed with the correct `processPath` and `arguments` to configure the ASP.NET Core Module and moved to published output. The transformation doesn't modify IIS configuration settings in the file.

The `web.config` file might provide additional IIS configuration settings that control active IIS modules. For information on IIS modules that are capable of processing requests with ASP.NET Core apps, see the [IIS modules](modules?view=aspnetcore-10.0) article.

An MSBuild target (`_TransformWebConfig`) handles creating, transforming, and publishing the `web.config` file when the project is published. This target is present in the Web SDK targets (`Microsoft.NET.Sdk.Web`). The SDK is set at the top of the project file:

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

To prevent the Web SDK from transforming the `web.config` file, use the `<IsTransformWebConfigDisabled>` property in the project file:

```
<PropertyGroup>
  <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
```

When disabling the Web SDK from transforming the file, the developer should manually set `processPath` and `arguments`. For more information, see [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0).

## Configuration of ASP.NET Core Module with `web.config`

The ASP.NET Core Module is configured with the `aspNetCore` section of the `system.webServer` node in the site's `web.config` file.

The following `web.config` file is published for a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd) and configures the ASP.NET Core Module to handle site requests:

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet"
                  arguments=".\MyApp.dll"
                  stdoutLogEnabled="false"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>
```

The following `web.config` is published for a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath=".\MyApp.exe"
                  stdoutLogEnabled="false"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>
```

The [InheritInChildApplications](/en-us/dotnet/api/system.configuration.sectioninformation.inheritinchildapplications) property is set to `false` to indicate that the settings specified within the [`<location>`](/en-us/iis/manage/managing-your-configuration-settings/understanding-iis-configuration-delegation#the-concept-of-location) element aren't inherited by apps that reside in a subdirectory of the app.

When an app is deployed to [Azure App Service](https://azure.microsoft.com/products/app-service/), the `stdoutLogFile` path is set to `\\?\%home%\LogFiles\stdout`. The path saves stdout logs to the `LogFiles` folder, which is a location automatically created by the service.

For information on IIS sub-application configuration, see [Advanced configuration](advanced?view=aspnetcore-10.0#sub-applications).

### Attributes of the `aspNetCore` element

| Attribute                 | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Default                                              |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `arguments`               | Optional string attribute.Arguments to the executable specified in `processPath`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |                                                      |
| `disableStartUpErrorPage` | Optional Boolean attribute.If true, the **502.5 - Process Failure** page is suppressed, and the 502 status code page configured in the `web.config` takes precedence.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `false`                                              |
| `forwardWindowsAuthToken` | Optional Boolean attribute.If true, the token is forwarded to the child process listening on `%ASPNETCORE_PORT%` as a header 'MS-ASPNETCORE-WINAUTHTOKEN' per request. It's the responsibility of that process to call CloseHandle on this token per request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `true`                                               |
| `hostingModel`            | Optional string attribute.Specifies the hosting model as in-process (`InProcess`/`inprocess`) or out-of-process (`OutOfProcess`/`outofprocess`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `OutOfProcess`/`outofprocess` when not present       |
| `processesPerApplication` | Optional integer attribute.Specifies the number of instances of the process specified in the `processPath` setting that can be spun up per app.†For in-process hosting, the value is limited to `1`.Setting `processesPerApplication` is discouraged. This attribute will be removed in a future release.                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Default: `1` Min: `1` Max: `100`†                    |
| `processPath`             | Required string attribute.Path to the executable that launches a process listening for HTTP requests. Relative paths are supported. If the path begins with `.`, the path is considered to be relative to the site root.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |                                                      |
| `rapidFailsPerMinute`     | Optional integer attribute.Specifies the number of times the process specified in `processPath` is allowed to crash per minute. If this limit is exceeded, the module stops launching the process for the remainder of the minute.Not supported with in-process hosting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Default: `10` Min: `0` Max: `100`                    |
| `requestTimeout`          | Optional timespan attribute.Specifies the duration for which the ASP.NET Core Module waits for a response from the process listening on %ASPNETCORE\_PORT%.In versions of the ASP.NET Core Module that shipped with the release of ASP.NET Core 2.1 or later, the `requestTimeout` is specified in hours, minutes, and seconds.Doesn't apply to in-process hosting. For in-process hosting, the module waits for the app to process the request.Valid values for minutes and seconds segments of the string are in the range 0-59. Use of `60` in the value for minutes or seconds results in a *500 - Internal Server Error*.                                                                                                                                          | Default: `00:02:00` Min: `00:00:00` Max: `360:00:00` |
| `shutdownTimeLimit`       | Optional integer attribute.Duration in seconds that the module waits for the executable to gracefully shutdown when the `app_offline.htm` file is detected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Default: `10` Min: `0` Max: `600`                    |
| `startupTimeLimit`        | Optional integer attribute.Duration in seconds that the module waits for the executable to start a process listening on the port. If this time limit is exceeded, the module kills the process.When hosting *in-process*: The process is **not** restarted and does **not** use the `rapidFailsPerMinute` setting.When hosting *out-of-process*: The module attempts to relaunch the process when it receives a new request and continues to attempt to restart the process on subsequent incoming requests unless the app fails to start `rapidFailsPerMinute` number of times in the last rolling minute.A value of 0 (zero) is **not** considered an infinite timeout.                                                                                               | Default: `120` Min: `0` Max: `3600`                  |
| `stdoutLogEnabled`        | Optional Boolean attribute.If true, `stdout` and `stderr` for the process specified in `processPath` are redirected to the file specified in `stdoutLogFile`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `false`                                              |
| `stdoutLogFile`           | Optional string attribute.Specifies the relative or absolute file path for which `stdout` and `stderr` from the process specified in `processPath` are logged. Relative paths are relative to the root of the site. Any path starting with `.` are relative to the site root and all other paths are treated as absolute paths. The module creates any folders provided in the path when the log file is created. Using underscore delimiters, a timestamp, process ID, and file extension (`.log`) are added to the last segment of the `stdoutLogFile` path. If `.\logs\stdout` is supplied as a value, an example stdout log is saved as `stdout_20180205194132_1934.log` in the `logs` folder when saved on February 5, 2018 at 19:41:32 with a process ID of 1934. | `aspnetcore-stdout`                                  |

### Set environment variables

Environment variables can be specified for the process in the `processPath` attribute. Specify an environment variable with the `<environmentVariable>` child element of an `<environmentVariables>` collection element. Environment variables set in this section take precedence over system environment variables.

The following example sets two environment variables in `web.config`. `ASPNETCORE_ENVIRONMENT` configures the app's environment to `Development`. A developer might temporarily set this value in the `web.config` file in order to force the [Developer Exception Page](../../fundamentals/error-handling?view=aspnetcore-10.0) to load when debugging an app exception. `CONFIG_DIR` is an example of a user-defined environment variable, where the developer has written code that reads the value on startup to form a path for loading the app's configuration file.

```
<aspNetCore processPath="dotnet"
      arguments=".\MyApp.dll"
      stdoutLogEnabled="false"
      stdoutLogFile=".\logs\stdout"
      hostingModel="inprocess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
    <environmentVariable name="CONFIG_DIR" value="f:\application_config" />
  </environmentVariables>
</aspNetCore>
```

Note

An alternative to setting the environment directly in `web.config` is to include the `<EnvironmentName>` property in the [publish profile (`.pubxml`)](../visual-studio-publish-profiles?view=aspnetcore-10.0) or project file. This approach sets the environment in `web.config` when the project is published:

```
<PropertyGroup>
  <EnvironmentName>Development</EnvironmentName>
</PropertyGroup>
```

Warning

Only set the `ASPNETCORE_ENVIRONMENT` environment variable to `Development` on staging and testing servers that aren't accessible to untrusted networks, such as the Internet.

## Configuration of IIS with `web.config`

IIS configuration is influenced by the `<system.webServer>` section of `web.config` for IIS scenarios that are functional for ASP.NET Core apps with the ASP.NET Core Module. For example, IIS configuration is functional for dynamic compression. If IIS is configured at the server level to use dynamic compression, the `<urlCompression>` element in the app's `web.config` file can disable it for an ASP.NET Core app.

For more information, see the following articles:

* [Configuration reference for `<system.webServer>`](/en-us/iis/configuration/system.webServer/)
* [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0)
* [IIS modules with ASP.NET Core](modules?view=aspnetcore-10.0)

To set environment variables for individual apps running in isolated app pools (supported for IIS 10.0 or later), see the *`AppCmd.exe` command* section of the [Environment Variables `<environmentVariables>`](/en-us/iis/configuration/system.applicationHost/applicationPools/add/environmentVariables/#appcmdexe) article in the IIS reference documentation.

## Configuration sections of `web.config`

Configuration sections of ASP.NET 4.x apps in `web.config` aren't used by ASP.NET Core apps for configuration:

* `<system.web>`
* `<appSettings>`
* `<connectionStrings>`
* `<location>`

ASP.NET Core apps are configured using other configuration providers. For more information, see [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0).

## Transform web.config

If you need to transform `web.config` on publish, see [Transform web.config](transform-webconfig?view=aspnetcore-10.0). You might need to transform `web.config` on publish to set environment variables based on the configuration, profile, or environment.

## Related content

* [IIS \<system.webServer>](/en-us/iis/configuration/system.webServer/)
* [IIS modules with ASP.NET Core](modules?view=aspnetcore-10.0)
* [Transform web.config](transform-webconfig?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0008
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0008.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0008: Do not use ConfigureWebHost with WebApplicationBuilder.Host

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0008      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

`ConfigureWebHost` can't be used with the `Host` property on `WebApplicationBuilder`.

## Rule description

The `WebApplicationBuilder` doesn't support configuring the `WebHost` before build using the `ConfigureWebHost` extension method.

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.ConfigureWebHost(webHostBuilder => {
    webHostBuilder.UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "myContentRoot"));
});

var app = builder.Build();

app.Run();
```

## How to fix violations

To fix a violation of this rule, configure the `WebHost` directly on the `WebApplicationBuilder`. For example, instead of setting the content root path via `ConfigureWebHost`.

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.ConfigureWebHost(webHostBuilder =>
{
    webHostBuilder.UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "myContentRoot"));
});

var app = builder.Build();

app.Run();
```

Configure the content root path directly on the `WebApplicationBuilder.WebHost`.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "foobar"));

var app = builder.Build();

app.Run();
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule. A misconfigured application can result in unexpected behavior at runtime.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/protobuf
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/protobuf.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Create Protobuf messages for .NET apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk) and [Mark Rendle](https://twitter.com/markrendle)

gRPC uses [Protobuf](https://developers.google.com/protocol-buffers) as its Interface Definition Language (IDL). Protobuf IDL is a language neutral format for specifying the messages sent and received by gRPC services. Protobuf messages are defined in `.proto` files. This document explains how Protobuf concepts map to .NET.

## Protobuf messages

Messages are the main data transfer object in Protobuf. They are conceptually similar to .NET classes.

```
syntax = "proto3";

option csharp_namespace = "Contoso.Messages";

message Person {
    int32 id = 1;
    string first_name = 2;
    string last_name = 3;
}  
```

The preceding message definition specifies three fields as name-value pairs. Like properties on .NET types, each field has a name and a type. The field type can be a [Protobuf scalar value type](#scalar-value-types), for example `int32`, or another message.

The [Protobuf style guide](https://developers.google.com/protocol-buffers/docs/style) recommends using `underscore_separated_names` for field names. New Protobuf messages created for .NET apps should follow the Protobuf style guidelines. .NET tooling automatically generates .NET types that use .NET naming standards. For example, a `first_name` Protobuf field generates a `FirstName` .NET property.

In addition to a name, each field in the message definition has a unique number. Field numbers are used to identify fields when the message is serialized to Protobuf. Serializing a small number is faster than serializing the entire field name. Because field numbers identify a field it is important to take care when changing them. For more information about changing Protobuf messages see [Versioning gRPC services](versioning?view=aspnetcore-10.0).

When an app is built the Protobuf tooling generates .NET types from `.proto` files. The `Person` message generates a .NET class:

```
public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
```

For more information about Protobuf messages see the [Protobuf language guide](https://developers.google.com/protocol-buffers/docs/proto3#simple).

## Scalar Value Types

Protobuf supports a range of native scalar value types. The following table lists them all with their equivalent C# type.

| Protobuf type | C# type      |
| ------------- | ------------ |
| `double`      | `double`     |
| `float`       | `float`      |
| `int32`       | `int`        |
| `int64`       | `long`       |
| `uint32`      | `uint`       |
| `uint64`      | `ulong`      |
| `sint32`      | `int`        |
| `sint64`      | `long`       |
| `fixed32`     | `uint`       |
| `fixed64`     | `ulong`      |
| `sfixed32`    | `int`        |
| `sfixed64`    | `long`       |
| `bool`        | `bool`       |
| `string`      | `string`     |
| `bytes`       | `ByteString` |

Scalar values always have a default value and can't be set to `null`. This constraint includes `string` and `ByteString` which are C# classes. `string` defaults to an empty string value and `ByteString` defaults to an empty bytes value. Attempting to set them to `null` throws an error.

[Nullable wrapper types](#nullable-types) can be used to support null values.

### Dates and times

The native scalar types don't provide for date and time values, equivalent to .NET's [DateTimeOffset](/en-us/dotnet/api/system.datetimeoffset), [DateTime](/en-us/dotnet/api/system.datetime), and [TimeSpan](/en-us/dotnet/api/system.timespan). These types can be specified by using some of Protobuf's *Well-Known Types* extensions. These extensions provide code generation and runtime support for complex field types across the supported platforms.

The following table shows the date and time types:

| .NET type        | Protobuf Well-Known Type    |
| ---------------- | --------------------------- |
| `DateTimeOffset` | `google.protobuf.Timestamp` |
| `DateTime`       | `google.protobuf.Timestamp` |
| `TimeSpan`       | `google.protobuf.Duration`  |

```
syntax = "proto3";

import "google/protobuf/duration.proto";  
import "google/protobuf/timestamp.proto";

message Meeting {
    string subject = 1;
    google.protobuf.Timestamp start = 2;
    google.protobuf.Duration duration = 3;
}  
```

The generated properties in the C# class aren't the .NET date and time types. The properties use the `Timestamp` and `Duration` classes in the `Google.Protobuf.WellKnownTypes` namespace. These classes provide methods for converting to and from `DateTimeOffset`, `DateTime`, and `TimeSpan`.

```
// Create Timestamp and Duration from .NET DateTimeOffset and TimeSpan.
var meeting = new Meeting
{
    Start = Timestamp.FromDateTimeOffset(meetingTime), // also FromDateTime()
    Duration = Duration.FromTimeSpan(meetingLength)
};

// Convert Timestamp and Duration to .NET DateTimeOffset and TimeSpan.
var time = meeting.Start.ToDateTimeOffset();
var duration = meeting.Duration?.ToTimeSpan();
```

Note

The `Timestamp` type works with UTC times. `DateTimeOffset` values always have an offset of zero, and the `DateTime.Kind` property is always `DateTimeKind.Utc`.

### Nullable types

The Protobuf code generation for C# uses the native types, such as `int` for `int32`. So the values are always included and can't be `null`.

For values that require explicit `null`, such as using `int?` in C# code, Protobuf's Well-Known Types include wrappers that are compiled to nullable C# types. To use them, import `wrappers.proto` into your `.proto` file, like the following code:

```
syntax = "proto3";

import "google/protobuf/wrappers.proto";

message Person {
    // ...
    google.protobuf.Int32Value age = 5;
}
```

`wrappers.proto` types aren't exposed in generated properties. Protobuf automatically maps them to appropriate .NET nullable types in C# messages. For example, a `google.protobuf.Int32Value` field generates an `int?` property. Reference type properties like `string` and `ByteString` are unchanged except `null` can be assigned to them without error.

The following table shows the complete list of wrapper types with their equivalent C# type.

| C# type      | Well-Known Type wrapper       |
| ------------ | ----------------------------- |
| `bool?`      | `google.protobuf.BoolValue`   |
| `double?`    | `google.protobuf.DoubleValue` |
| `float?`     | `google.protobuf.FloatValue`  |
| `int?`       | `google.protobuf.Int32Value`  |
| `long?`      | `google.protobuf.Int64Value`  |
| `uint?`      | `google.protobuf.UInt32Value` |
| `ulong?`     | `google.protobuf.UInt64Value` |
| `string`     | `google.protobuf.StringValue` |
| `ByteString` | `google.protobuf.BytesValue`  |

### Bytes

Binary payloads are supported in Protobuf with the `bytes` scalar value type. A generated property in C# uses `ByteString` as the property type.

Use `ByteString.CopyFrom(byte[] data)` to create a new instance from a byte array:

```
var data = await File.ReadAllBytesAsync(path);

var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
```

`ByteString` data is accessed directly using `ByteString.Span` or `ByteString.Memory`. Or call `ByteString.ToByteArray()` to convert an instance back into a byte array:

```
var payload = await client.GetPayload(new PayloadRequest());

await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
```

### Decimals

Protobuf doesn't natively support the .NET `decimal` type, just `double` and `float`. There's an ongoing discussion in the Protobuf project about the possibility of adding a standard decimal type to the Well-Known Types, with platform support for languages and frameworks that support it. Nothing has been implemented yet.

It's possible to create a message definition to represent the `decimal` type that works for safe serialization between .NET clients and servers. But developers on other platforms would have to understand the format being used and implement their own handling for it.

#### Creating a custom decimal type for Protobuf

```
package CustomTypes;

// Example: 12345.6789 -> { units = 12345, nanos = 678900000 }
message DecimalValue {

    // Whole units part of the amount
    int64 units = 1;

    // Nano units of the amount (10^-9)
    // Must be same sign as units
    sfixed32 nanos = 2;
}
```

The `nanos` field represents values from `0.999_999_999` to `-0.999_999_999`. For example, the `decimal` value `1.5m` would be represented as `{ units = 1, nanos = 500_000_000 }`. This is why the `nanos` field in this example uses the `sfixed32` type, which encodes more efficiently than `int32` for larger values. If the `units` field is negative, the `nanos` field should also be negative.

Note

Additional algorithms are available for encoding `decimal` values as byte strings. The algorithm used by `DecimalValue`:

* Is easy to understand.
* Isn't affected by big-endian or little-endian on different platforms.
* Supports decimal numbers ranging from positive `9,223,372,036,854,775,807.999999999` to negative `9,223,372,036,854,775,808.999999999` with a maximum precision of nine decimal places, which isn't the full range of a `decimal`.

Conversion between this type and the BCL `decimal` type might be implemented in C# like this:

```
namespace CustomTypes
{
    public partial class DecimalValue
    {
        private const decimal NanoFactor = 1_000_000_000;
        public DecimalValue(long units, int nanos)
        {
            Units = units;
            Nanos = nanos;
        }

        public static implicit operator decimal(CustomTypes.DecimalValue grpcDecimal)
        {
            return grpcDecimal.Units + grpcDecimal.Nanos / NanoFactor;
        }

        public static implicit operator CustomTypes.DecimalValue(decimal value)
        {
            var units = decimal.ToInt64(value);
            var nanos = decimal.ToInt32((value - units) * NanoFactor);
            return new CustomTypes.DecimalValue(units, nanos);
        }
    }
}
```

The preceding code:

* Adds a partial class for `DecimalValue`. The partial class is combined with `DecimalValue` generated from the `.proto` file. The generated class declares the `Units` and `Nanos` properties.
* Has implicit operators for converting between `DecimalValue` and the BCL `decimal` type.

## Collections

### Lists

Lists in Protobuf are specified by using the `repeated` prefix keyword on a field. The following example shows how to create a list:

```
message Person {
    // ...
    repeated string roles = 8;
}
```

In the generated code, `repeated` fields are represented by the `Google.Protobuf.Collections.RepeatedField<T>` generic type.

```
public class Person
{
    // ...
    public RepeatedField<string> Roles { get; }
}
```

`RepeatedField<T>` implements [IList\<T>](/en-us/dotnet/api/system.collections.generic.ilist-1). So you can use LINQ queries or convert it to an array or a list. `RepeatedField<T>` properties don't have a public setter. Items should be added to the existing collection.

```
var person = new Person();

// Add one item.
person.Roles.Add("user");

// Add all items from another collection.
var roles = new [] { "admin", "manager" };
person.Roles.Add(roles);
```

### Dictionaries

The .NET [IDictionary\<TKey,TValue>](/en-us/dotnet/api/system.collections.generic.idictionary-2) type is represented in Protobuf using `map<key_type, value_type>`.

```
message Person {
    // ...
    map<string, string> attributes = 9;
}
```

In generated .NET code, `map` fields are represented by the `Google.Protobuf.Collections.MapField<TKey, TValue>` generic type. `MapField<TKey, TValue>` implements [IDictionary\<TKey,TValue>](/en-us/dotnet/api/system.collections.generic.idictionary-2). Like `repeated` properties, `map` properties don't have a public setter. Items should be added to the existing collection.

```
var person = new Person();

// Add one item.
person.Attributes["created_by"] = "James";

// Add all items from another collection.
var attributes = new Dictionary<string, string>
{
    ["last_modified"] = DateTime.UtcNow.ToString()
};
person.Attributes.Add(attributes);
```

## Unstructured and conditional messages

Protobuf is a contract-first messaging format. An app's messages, including its fields and types, must be specified in `.proto` files when the app is built. Protobuf's contract-first design is great at enforcing message content but can limit scenarios where a strict contract isn't required:

* Messages with unknown payloads. For example, a message with a field that could contain any message.
* Conditional messages. For example, a message returned from a gRPC service might be a success result or an error result.
* Dynamic values. For example, a message with a field that contains an unstructured collection of values, similar to JSON.

Protobuf offers language features and types to support these scenarios.

### Any

The `Any` type lets you use messages as embedded types without having their `.proto` definition. To use the `Any` type, import `any.proto`.

```
import "google/protobuf/any.proto";

message Status {
    string message = 1;
    google.protobuf.Any detail = 2;
}
```

```
// Create a status with a Person message set to detail.
var status = new ErrorStatus();
status.Detail = Any.Pack(new Person { FirstName = "James" });

// Read Person message from detail.
if (status.Detail.Is(Person.Descriptor))
{
    var person = status.Detail.Unpack<Person>();
    // ...
}
```

### Oneof

`oneof` fields are a language feature. The compiler handles the `oneof` keyword when it generates the message class. Using `oneof` to specify a response message that could either return a `Person` or `Error` might look like this:

```
message Person {
    // ...
}

message Error {
    // ...
}

message ResponseMessage {
  oneof result {
    Error error = 1;
    Person person = 2;
  }
}
```

Fields within the `oneof` set must have unique field numbers in the overall message declaration.

When using `oneof`, the generated C# code includes an enum that specifies which of the fields has been set. You can test the enum to find which field is set. Fields that aren't set return `null` or the default value, rather than throwing an exception.

```
var response = await client.GetPersonAsync(new RequestMessage());

switch (response.ResultCase)
{
    case ResponseMessage.ResultOneofCase.Person:
        HandlePerson(response.Person);
        break;
    case ResponseMessage.ResultOneofCase.Error:
        HandleError(response.Error);
        break;
    default:
        throw new ArgumentException("Unexpected result.");
}
```

### Value

The `Value` type represents a dynamically typed value. It can be either `null`, a number, a string, a boolean, a dictionary of values (`Struct`), or a list of values (`ValueList`). `Value` is a Protobuf Well-Known Type that uses the previously discussed `oneof` feature. To use the `Value` type, import `struct.proto`.

```
import "google/protobuf/struct.proto";

message Status {
    // ...
    google.protobuf.Value data = 3;
}
```

```
// Create dynamic values.
var status = new Status();
status.Data = Value.ForStruct(new Struct
{
    Fields =
    {
        ["enabled"] = Value.ForBool(true),
        ["metadata"] = Value.ForList(
            Value.ForString("value1"),
            Value.ForString("value2"))
    }
});

// Read dynamic values.
switch (status.Data.KindCase)
{
    case Value.KindOneofCase.StructValue:
        foreach (var field in status.Data.StructValue.Fields)
        {
            // Read struct fields...
        }
        break;
    // ...
}
```

Using `Value` directly can be verbose. An alternative way to use `Value` is with Protobuf's built-in support for mapping messages to JSON. Protobuf's `JsonFormatter` and `JsonWriter` types can be used with any Protobuf message. `Value` is particularly well suited to being converted to and from JSON.

This is the JSON equivalent of the previous code:

```
// Create dynamic values from JSON.
var status = new Status();
status.Data = Value.Parser.ParseJson(@"{
    ""enabled"": true,
    ""metadata"": [ ""value1"", ""value2"" ]
}");

// Convert dynamic values to JSON.
// JSON can be read with a library like System.Text.Json or Newtonsoft.Json
var json = JsonFormatter.Default.Format(status.Data);
var document = JsonDocument.Parse(json);
```

## Related content

* [Protobuf language guide](https://developers.google.com/protocol-buffers/docs/proto3#simple)
* [Versioning gRPC services](versioning?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0012
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0012.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0012: Suggest using builder.Services over Host.ConfigureServices or WebHost.ConfigureServices

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0012      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

`ConfigureServices` isn't the recommended strategy for registering services in DI in a Minimal API application.

## Rule description

`ConfigureServices` isn't the recommended strategy for configuring logging in a Minimal API application.

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.ConfigureServices(services =>
{
    services.AddAntiforgery();
})

var app = builder.Build();

app.Run();
```

## How to fix violations

To fix a violation of this rule, use the `Services` property on the `WebApplicationBuilder` to modify the DI container directly without the need for an additional `ConfigureServices` call.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAntiforgery();

var app = builder.Build();

app.Run();
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-10.0
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-10.0.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# What's new in ASP.NET Core in .NET 10

This article highlights the most significant changes in ASP.NET Core in .NET 10 with links to relevant documentation.

## Blazor

This section describes new features for Blazor.

### New and updated Blazor Web App security samples

We've added and updated the Blazor Web App security samples linked in the following articles:

* [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](../blazor/security/blazor-web-app-with-oidc?view=aspnetcore-10.0)
* [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](../blazor/security/blazor-web-app-with-entra?view=aspnetcore-10.0)
* [Secure an ASP.NET Core Blazor Web App with Windows Authentication](../blazor/security/blazor-web-app-with-windows-authentication?view=aspnetcore-10.0)

All of our OIDC and Entra sample solutions now include a separate web API project (`MinimalApiJwt`) to demonstrate how to configure and call an external web API securely. Calling web APIs is demonstrated with a token handler and named HTTP client for an OIDC identity provider or Microsoft Identity Web packages/API for Microsoft Entra ID.

The sample solutions are configured in C# code in their `Program` files. To configure the solutions from app settings files (for example, `appsettings.json`) see the ***new*** *Supply configuration with the JSON configuration provider (app settings)* section of the OIDC or Entra articles.

Our Entra article and sample apps also include new guidance on the following approaches:

* How to use an encrypted distributed token cache for web farm hosting scenarios.
* How to use [Azure Key Vault](https://azure.microsoft.com/products/key-vault/) with [Azure Managed Identities](/en-us/entra/identity/managed-identities-azure-resources/overview) for data protection.

### QuickGrid `RowClass` parameter

Apply a stylesheet class to a row of the grid based on the row item using the new `RowClass` parameter. In the following example, the `GetRowCssClass` method is called on each row to conditionally apply a stylesheet class based on the row item:

```
<QuickGrid ... RowClass="GetRowCssClass">
    ...
</QuickGrid>

@code {
    private string GetRowCssClass(MyGridItem item) =>
        item.IsArchived ? "row-archived" : null;
}
```

For more information, see [ASP.NET Core Blazor \`QuickGrid\` component](../blazor/components/quickgrid?view=aspnetcore-10.0#style-a-table-row-based-on-the-row-item).

### Blazor script as static web asset

In prior releases of .NET, the Blazor script is served from an embedded resource in the ASP.NET Core shared framework. In .NET 10 or later, the Blazor script is served as a static web asset with automatic compression and fingerprinting.

The Blazor script (`blazor.web.js` or `blazor.server.js`) is included by the framework if the project contains at least one Razor component file (`.razor`). If your app requires the Blazor script but doesn't contain at least one component, add the following MSBuild property to the app's project file to force unconditional script inclusion:

```
<RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>
```

For more information, see the following resources:

* [ASP.NET Core Blazor project structure](../blazor/project-structure?view=aspnetcore-10.0#location-of-the-blazor-script)
* [ASP.NET Core Blazor static files](../blazor/fundamentals/static-files?view=aspnetcore-10.0)

### Route template highlights

The [`[Route]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.routeattribute) now supports route syntax highlighting to help visualize the structure of the route template:

### `NavigateTo` no longer scrolls to the top for same-page navigations

Previously, [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) scrolled to the top of the page for same-page navigations. This behavior has been changed in .NET 10 so that the browser no longer scrolls to the top of the page when navigating to the same page. This means the viewport is no longer reset when making updates to the address for the current page, such as changing the query string or fragment.

### Reconnection UI component added to the Blazor Web App project template

The Blazor Web App project template now includes a `ReconnectModal` component, including collocated stylesheet and JavaScript files, for improved developer control over the reconnection UI when the client loses the WebSocket connection to the server. The component doesn't insert styles programmatically, ensuring compliance with stricter Content Security Policy (CSP) settings for the `style-src` policy. In prior releases, the default reconnection UI was created by the framework in a way that could cause CSP violations. Note that the default reconnection UI is still used as fallback when the app doesn't define the reconnection UI, such as by using the project template's `ReconnectModal` component or a similar custom component.

New reconnection UI features:

* Apart from indicating the reconnection state by setting a specific CSS class on the reconnection UI element, the new `components-reconnect-state-changed` event is dispatched for reconnection state changes.
* Code can better differentiate the stages of the reconnection process with the new reconnection state "`retrying`," indicated by both the CSS class and the new event.

For more information, see [ASP.NET Core Blazor SignalR guidance](../blazor/fundamentals/signalr?view=aspnetcore-10.0#reflect-the-server-side-connection-state-in-the-ui).

### Ignore the query string and fragment when using `NavLinkMatch.All`

The `NavLink` component now ignores the query string and fragment when using the `NavLinkMatch.All` value for the `Match` parameter. This means that the link retains the `active` class if the URL path matches but the query string or fragment change. To revert to the original behavior, use the `Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragment` [`AppContext` switch](/en-us/dotnet/fundamentals/runtime-libraries/system-appcontext) set to `true`.

You can also override the `ShouldMatch` method on `NavLink` to customize the matching behavior:

```
public class CustomNavLink : NavLink
{
    protected override bool ShouldMatch(string currentUriAbsolute)
    {
        // Custom matching logic
    }
}
```

For more information, see [ASP.NET Core Blazor navigation](../blazor/fundamentals/navigation?view=aspnetcore-10.0#navlink-component).

### Close `QuickGrid` column options

You can now close the `QuickGrid` column options UI using the new `HideColumnOptionsAsync` method.

The following example uses the `HideColumnOptionsAsync` method to close the column options UI as soon as the title filter is applied:

```
<QuickGrid @ref="movieGrid" Items="movies">
    <PropertyColumn Property="@(m => m.Title)" Title="Title">
        <ColumnOptions>
            <input type="search" @bind="titleFilter" placeholder="Filter by title" 
                @bind:after="@(() => movieGrid.HideColumnOptionsAsync())" />
        </ColumnOptions>
    </PropertyColumn>
    <PropertyColumn Property="@(m => m.Genre)" Title="Genre" />
    <PropertyColumn Property="@(m => m.ReleaseYear)" Title="Release Year" />
</QuickGrid>

@code {
    private QuickGrid<Movie>? movieGrid;
    private string titleFilter = string.Empty;
    private IQueryable<Movie> movies = new List<Movie> { ... }.AsQueryable();
    private IQueryable<Movie> filteredMovies => 
        movies.Where(m => m.Title!.Contains(titleFilter));
}
```

### HttpClient response streaming enabled by default

In prior Blazor releases, response streaming for [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) requests was opt-in. Now, response streaming is enabled by default.

This is a breaking change because calling [HttpContent.ReadAsStreamAsync](/en-us/dotnet/api/system.net.http.httpcontent.readasstreamasync) for an [HttpResponseMessage.Content](/en-us/dotnet/api/system.net.http.httpresponsemessage.content) (`response.Content.ReadAsStreamAsync()`) returns a `BrowserHttpReadStream` and no longer a [MemoryStream](/en-us/dotnet/api/system.io.memorystream). `BrowserHttpReadStream` doesn't support synchronous operations, such as `Stream.Read(Span<Byte>)`. If your code uses synchronous operations, you can opt-out of response streaming or copy the [Stream](/en-us/dotnet/api/system.io.stream) into a [MemoryStream](/en-us/dotnet/api/system.io.memorystream) yourself.

To opt-out of response streaming globally, use either of the following approaches:

* Add the `<WasmEnableStreamingResponse>` property to the project file with a value of `false`:

  ```
  <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
  ```

* Set the `DOTNET_WASM_ENABLE_STREAMING_RESPONSE` environment variable to `false` or `0`.

To opt-out of response streaming for an individual request, set [SetBrowserResponseStreamingEnabled](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserresponsestreamingenabled) to `false` on the [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) (`requestMessage` in the following example):

```
requestMessage.SetBrowserResponseStreamingEnabled(false);
```

For more information, see [`HttpClient` and `HttpRequestMessage` with Fetch API request options (*Call web API* article)](../blazor/call-web-api?view=aspnetcore-10.0#httpclient-and-httprequestmessage-with-fetch-api-request-options).

### Client-side fingerprinting

The release of .NET 9 introduced [server-side fingerprinting](https://en.wikipedia.org/wiki/Fingerprint_\(computing\)) of static assets in Blazor Web Apps with the introduction of [Map Static Assets routing endpoint conventions (`MapStaticAssets`)](../fundamentals/static-files?view=aspnetcore-10.0), the [`ImportMap` component](../blazor/fundamentals/static-files?view=aspnetcore-10.0#importmap-component), and the [ComponentBase.Assets](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.assets#microsoft-aspnetcore-components-componentbase-assets) property (`@Assets["..."]`) to resolve fingerprinted JavaScript (JS) modules. For .NET 10, you can opt-into client-side fingerprinting of JS modules for standalone Blazor WebAssembly apps.

In standalone Blazor WebAssembly apps during build and publish, the framework overrides placeholders in `index.html` with values computed during build to fingerprint static assets. A fingerprint is placed into the `blazor.webassembly.js` script file name.

The following markup must be present in the `wwwroot/index.html` file to adopt the fingerprinting feature:

```
<head>
    ...
+   <script type="importmap"></script>
</head>

<body>
    ...
-   <script src="_framework/blazor.webassembly.js"></script>
+   <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>

</html>
```

In the project file (`.csproj`), add the `<OverrideHtmlAssetPlaceholders>` property set to `true`:

```
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
+   <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
  </PropertyGroup>
</Project>
```

In the following example, all developer-supplied JS files are modules with a `.js` file extension.

A module named `scripts.js` in the app's `wwwroot/js` folder is fingerprinted by adding `#[.{fingerprint}]` before the file extension (`.js`):

```
<script type="module" src="js/scripts#[.{fingerprint}].js"></script>
```

Specify the fingerprint expression with the `<StaticWebAssetFingerprintPattern>` property in the app's project file (`.csproj`):

```
<ItemGroup>
  <StaticWebAssetFingerprintPattern Include="JSModule" Pattern="*.js" 
    Expression="#[.{fingerprint}]!" />
</ItemGroup>
```

Any JS file (`*.js`) in `index.html` with the fingerprint marker is fingerprinted by the framework, including when the app is published.

If you adopt the `.mjs` file extension for JS modules, set the file extension with the `Pattern` parameter:

```
<ItemGroup>
  <StaticWebAssetFingerprintPattern Include="JSModule" Pattern="*.mjs" 
    Expression="#[.{fingerprint}]!" />
</ItemGroup>
```

Files are placed into the import map:

* Automatically for Blazor Web App client-side rendering (CSR).
* When opting-into module fingerprinting in standalone Blazor WebAssembly apps per the preceding instructions.

When resolving the import for JavaScript interop, the import map is used by the browser resolve fingerprinted files.

### Preloaded Blazor framework static assets

In Blazor Web Apps, framework static assets are automatically preloaded using [`Link` headers](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Link), which allows the browser to preload resources before the initial page is fetched and rendered.

In standalone Blazor WebAssembly apps, framework assets are scheduled for high priority downloading and caching early in browser `index.html` page processing when:

* The `OverrideHtmlAssetPlaceholders` MSBuild property in the app's project file (`.csproj`) is set to `true`:

  ```
  <PropertyGroup>
    <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
  </PropertyGroup>
  ```

* The following `<link>` element containing [`rel="preload"`](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/rel/preload) is present in the `<head>` content of `wwwroot/index.html`:

  ```
  <link rel="preload" id="webassembly" />
  ```

For more information, see [ASP.NET Core Blazor static files](../blazor/fundamentals/static-files?view=aspnetcore-10.0#preloaded-blazor-framework-static-assets).

Note

This feature is disabled for Blazor Web App enhanced navigation in .NET 11 or later. For more information, see [Blazor enhanced navigation no longer preloads resources](/en-us/aspnet/core/breaking-changes/11/blazor-enhanced-nav-preloading-disabled).

### Set the environment in standalone Blazor WebAssembly apps

The `Blazor-Environment` header and `Properties/launchSettings.json` file (`ASPNETCORE_ENVIRONMENT` environment variable) are no longer used to control the environment in standalone Blazor WebAssembly apps.

Starting in .NET 10, set the environment with the `<WasmApplicationEnvironmentName>` property in the app's project file (`.csproj`).

The following example sets the app's environment to `Staging`:

```
<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>
```

The default environments are:

* `Development` for build.
* `Production` for publish.

For more information, see [ASP.NET Core Blazor environments](../blazor/fundamentals/environments?view=aspnetcore-10.0#set-the-environment).

### Boot configuration file inlined

Blazor's boot configuration, which prior to the release of .NET 10 existed in a file named `blazor.boot.json`, has been inlined into the `dotnet.js` script. This only affects developers who are interacting directly with the `blazor.boot.json` file, such as when developers are:

* Checking file integrity for published assets with the troubleshoot integrity PowerShell script per the guidance in [ASP.NET Core Blazor WebAssembly caching and integrity check failures](../blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures?view=aspnetcore-9.0#troubleshoot-integrity-powershell-script).
* Changing the file name extension of DLL files when not using the default Webcil file format per the guidance in [Host and deploy ASP.NET Core Blazor WebAssembly](../blazor/host-and-deploy/webassembly/?view=aspnetcore-9.0#customize-how-boot-resources-are-loaded).

Currently, there's no documented replacement strategy for the preceding approaches. If you require either of the preceding strategies, open a new documentation issue describing your scenario using the **Open a documentation issue** link at the bottom of either article.

### Declarative model for persisting state from components and services

You can now declaratively specify state to persist from components and services using the `[PersistentState]` attribute. Public (`public`) properties with this attribute are automatically persisted using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service during prerendering. The state is retrieved when the component renders interactively or the service is instantiated.

In previous Blazor releases, persisting component state during prerendering using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service involved a significant amount of code, as the following example demonstrates:

```
@page "/movies"
@implements IDisposable
@inject IMovieService MovieService
@inject PersistentComponentState ApplicationState

@if (MoviesList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <QuickGrid Items="MoviesList.AsQueryable()">
        ...
    </QuickGrid>
}

@code {
    public List<Movie>? MoviesList { get; set; }
    private PersistingComponentStateSubscription? persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<List<Movie>>(nameof(MoviesList), 
            out var movies))
        {
            MoviesList = await MovieService.GetMoviesAsync();
        }
        else
        {
            MoviesList = movies;
        }

        persistingSubscription = ApplicationState.RegisterOnPersisting(() =>
        {
            ApplicationState.PersistAsJson(nameof(MoviesList), MoviesList);
            return Task.CompletedTask;
        });
    }

    public void Dispose() => persistingSubscription?.Dispose();
}
```

This code can now be simplified using the new declarative model:

```
@page "/movies"
@inject IMovieService MovieService

@if (MoviesList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <QuickGrid Items="MoviesList.AsQueryable()">
        ...
    </QuickGrid>
}

@code {
    [PersistentState]
    public List<Movie>? MoviesList { get; set; }

    protected override async Task OnInitializedAsync()
    {
        MoviesList ??= await MovieService.GetMoviesAsync();
    }
}
```

Use `public` properties because reflection is used by the framework for tasks such as [trimming unused code](../blazor/performance/app-download-size?view=aspnetcore-10.0#intermediate-language-il-trimming) and [source generation](/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview).

State can be serialized for multiple components of the same type, and you can establish declarative state in a service for use around the app by calling `RegisterPersistentService` on the Razor components builder ([AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents)) with a custom service type and render mode. For more information, see [ASP.NET Core Blazor prerendered state persistence](../blazor/state-management/prerendered-state-persistence?view=aspnetcore-10.0).

### New JavaScript interop features

Blazor adds support for the following JS interop features:

* Create an instance of a JS object using a constructor function and get the [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference)/[IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) .NET handle for referencing the instance.
* Read or modify the value of a JS object property, both data and accessor properties.

The following asynchronous methods are available on [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) and [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) with the same scoping behavior as the existing [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) method:

* `InvokeConstructorAsync(string identifier, object?[]? args)`: Invokes the specified JS constructor function asynchronously. The function is invoked with the `new` operator. In the following example, `jsInterop.TestClass` is a class with a constructor function, and `classRef` is an [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference):

  ```
  var classRef = await JSRuntime.InvokeConstructorAsync("jsInterop.TestClass", "Blazor!");
  var text = await classRef.GetValueAsync<string>("text");
  var textLength = await classRef.InvokeAsync<int>("getTextLength");
  ```

* `GetValueAsync<TValue>(string identifier)`: Reads the value of the specified JS property asynchronously. The property can't be a `set`-only property. A [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) is thrown if the property doesn't exist. The following example returns a value from a data property:

  ```
  var valueFromDataPropertyAsync = await JSRuntime.GetValueAsync<int>(
    "jsInterop.testObject.num");
  ```

* `SetValueAsync<TValue>(string identifier, TValue value)`: Updates the value of the specified JS property asynchronously. The property can't be a `get`-only property. If the property isn't defined on the target object, the property is created. A [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) is thrown if the property exists but isn't writable or when a new property can't be added to the object. In the following example, `num` is created on `testObject` with a value of 30 if it doesn't exist:

  ```
  await JSRuntime.SetValueAsync("jsInterop.testObject.num", 30);
  ```

Overloads are available for each of the preceding methods that take a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) argument or [TimeSpan](/en-us/dotnet/api/system.timespan) timeout argument.

The following synchronous methods are available on [IJSInProcessRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime) and [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) with the same scoping behavior as the existing [IJSInProcessObjectReference.Invoke](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference.invoke) method:

* `InvokeConstructor(string identifier, object?[]? args)`: Invokes the specified JS constructor function synchronously. The function is invoked with the `new` operator. In the following example, `jsInterop.TestClass` is a class with a constructor function, and `classRef` is an [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference):

  ```
  var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
  var classRef = inProcRuntime.InvokeConstructor("jsInterop.TestClass", "Blazor!");
  var text = classRef.GetValue<string>("text");
  var textLength = classRef.Invoke<int>("getTextLength");
  ```

* `GetValue<TValue>(string identifier)`: Reads the value of the specified JS property synchronously. The property can't be a `set`-only property. A [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) is thrown if the property doesn't exist. The following example returns a value from a data property:

  ```
  var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
  var valueFromDataProperty = inProcRuntime.GetValue<int>(
    "jsInterop.testObject.num");
  ```

* `SetValue<TValue>(string identifier, TValue value)`: Updates the value of the specified JS property synchronously. The property can't be a `get`-only property. If the property isn't defined on the target object, the property is created. A [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) is thrown if the property exists but isn't writable or when a new property can't be added to the object. In the following example, `num` is created on `testObject` with a value of 20 if it doesn't exist:

  ```
  var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
  inProcRuntime.SetValue("jsInterop.testObject.num", 20);
  ```

For more information, see the following sections of the *Call JavaScript functions from .NET methods* article:

* [Create an instance of a JS object using a constructor function](../blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#create-an-instance-of-a-js-object-using-a-constructor-function)
* [Read or modify the value of a JS object property](../blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#read-or-modify-the-value-of-a-js-object-property)

### Blazor WebAssembly performance profiling and diagnostic counters

New performance profiling and diagnostic counters are available for Blazor WebAssembly apps. For more information, see the following articles:

* [ASP.NET Core Blazor WebAssembly browser developer tools diagnostics](../blazor/performance/webassembly-browser-developer-tools-diagnostics?view=aspnetcore-10.0)
* [ASP.NET Core Blazor WebAssembly Event Pipe diagnostics](../blazor/performance/webassembly-event-pipe-diagnostics?view=aspnetcore-10.0)

### Opt-in to avoiding a `NavigationException` during static server-side rendering with `NavigationManager.NavigateTo`

Calling [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) during static server-side rendering (static SSR) throws a [NavigationException](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationexception), interrupting execution before being converted to a redirection response. This can cause confusion during debugging and is inconsistent with interactive rendering behavior, where code after [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) continues to execute normally.

In .NET 10, you can set the `<BlazorDisableThrowNavigationException>` MSBuild property to `true` in the app's project file in order to avoid throwing the exception during static SSR:

```
<PropertyGroup>
  <BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
```

With the MSBuild property set, calling [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) during static SSR no longer throws a [NavigationException](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationexception). Instead, it behaves consistently with interactive rendering by performing the navigation without throwing an exception. Code after [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) executes before the redirection occurs.

The .NET 10 Blazor Web App project template sets the MSBuild property to `true` by default. We recommend that apps updating to .NET 10 use the new MSBuild property and avoid the prior behavior.

If the MSBuild property is used, code that relied on [NavigationException](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationexception) being thrown should be updated. In the default Blazor Identity UI of the Blazor Web App project template before the release of .NET 10, the `IdentityRedirectManager` throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) after calling `RedirectTo` to ensure that the method wasn't invoked during interactive rendering. This exception and the [`[DoesNotReturn]` attributes](/en-us/dotnet/api/system.diagnostics.codeanalysis.doesnotreturnattribute) should now be removed when the MSBuild property is used. For more information, see [Migrate from ASP.NET Core in .NET 9 to ASP.NET Core in .NET 10](../migration/90-to-100?view=aspnetcore-10.0#when-navigation-errors-are-disabled-in-a-blazor-web-app-with-individual-accounts).

### Blazor router has a `NotFoundPage` parameter

Blazor now provides an improved way to display a "Not Found" page when navigating to a non-existent page. You can specify a page to render when [NavigationManager.NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) (described in the next section) is invoked by passing a page type to the `Router` component using the `NotFoundPage` parameter. The feature supports routing, works across status code pages re-execution middleware, and is compatible even with non-Blazor scenarios.

The [`NotFound` render fragment](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfound) (`<NotFound>...</NotFound>`) isn't supported in .NET 10 or later.

```
<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>This content is ignored because NotFoundPage is defined.</NotFound>
</Router>
```

The Blazor project template now includes a `NotFound.razor` page by default. This page automatically renders whenever [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called in your app, making it easier to handle missing routes with a consistent user experience.

For more information, see [ASP.NET Core Blazor navigation](../blazor/fundamentals/navigation?view=aspnetcore-10.0#not-found-responses).

### Not Found responses using `NavigationManager` for static SSR and global interactive rendering

The [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) now includes a [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) method to handle scenarios where a requested resource isn't found during static server-side rendering (static SSR) or global interactive rendering:

* **Static server-side rendering (static SSR)**: Calling [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) sets the HTTP status code to 404.

* **Interactive rendering**: Signals the Blazor router ([`Router` component](../blazor/fundamentals/routing?view=aspnetcore-10.0#route-templates)) to render Not Found content.

* **Streaming rendering**: If [enhanced navigation](../blazor/fundamentals/routing?view=aspnetcore-10.0#enhanced-navigation-and-form-handling) is active, [streaming rendering](../blazor/components/rendering?view=aspnetcore-10.0#streaming-rendering) renders Not Found content without reloading the page. When enhanced navigation is blocked, the framework redirects to Not Found content with a page refresh.

Streaming rendering can only render components that have a route, such as a [`NotFoundPage` assignment](#blazor-router-has-a-notfoundpage-parameter) (`NotFoundPage="..."`) or a [status code pages re-execution middleware page assignment](../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepageswithreexecute) ([UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute)). `DefaultNotFound` 404 content ("`Not found`" plain text) doesn't have a route, so it can't be used during streaming rendering.

[NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) content rendering uses the following, regardless if the response has started or not (in order):

* If [NotFoundEventArgs.Path](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.notfoundeventargs.path) is set, render the contents of the assigned page.
* If `Router.NotFoundPage` is set, render the assigned page.
* A status code pages re-execution middleware page, if configured.
* No action if none of the preceding approaches are adopted.

[Status code pages re-execution middleware](../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepageswithreexecute) with [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) takes precedence for browser-based address routing problems, such as an incorrect URL typed into the browser's address bar or selecting a link that has no endpoint in the app.

You can use the [NavigationManager.OnNotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.onnotfound) event for notifications when [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is invoked.

For more information and examples, see [ASP.NET Core Blazor navigation](../blazor/fundamentals/navigation?view=aspnetcore-10.0#not-found-responses).

### Support for Not Found responses in apps without Blazor's router

Apps that implement a custom router can use [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound). There are two ways to inform the renderer what page should be rendered when [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called.

The recommended approach that works regardless of the response state is to call [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute). When [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called, the middleware renders the path passed to the method:

```
app.UseStatusCodePagesWithReExecute(
    "/not-found", createScopeForStatusCodePages: true);
```

If you don't want to use [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute), the app can still support [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) for responses that have already started. Subscribe to `OnNotFoundEvent` in the router and assign the Not Found page path to `NotFoundEventArgs.Path` to inform the renderer what content to render when [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called.

`CustomRouter.razor`:

```
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Http
@implements IDisposable
@inject NavigationManager NavigationManager

@code {
    protected override void OnInitialized() =>
        NavigationManager.OnNotFound += OnNotFoundEvent;

    [CascadingParameter]
    public HttpContext? HttpContext { get; set; }

    private void OnNotFoundEvent(object sender, NotFoundEventArgs e)
    {
        // Only execute the logic if HTTP response has started
        // because setting NotFoundEventArgs.Path blocks re-execution
        if (HttpContext?.Response.HasStarted == false)
        {
            return;
        }

        e.Path = GetNotFoundRoutePath();
    }

    // Return the path of the Not Found page that you want to display
    private string GetNotFoundRoutePath()
    {
        ...
    }

    public void Dispose() => NavigationManager.OnNotFound -= OnNotFoundEvent;
}
```

If you use both approaches in your app, the Not Found path specified in the `OnNotFoundEvent` handler takes precedence over the path configured in the re-execution middleware.

### Metrics and tracing

This release introduces comprehensive metrics and tracing capabilities for Blazor apps, providing detailed observability of the component lifecycle, navigation, event handling, and circuit management.

For more information, see [ASP.NET Core Blazor performance best practices](../blazor/performance/?view=aspnetcore-10.0#metrics-and-tracing).

### JavaScript bundler support

Blazor's build output isn't compatible with JavaScript bundlers, such as [Gulp](https://gulpjs.com), [Webpack](https://webpack.js.org), and [Rollup](https://rollupjs.org/). Blazor can now produce bundler-friendly output during publish by setting the `WasmBundlerFriendlyBootConfig` MSBuild property to `true`.

For more information, see [Host and deploy ASP.NET Core Blazor](../blazor/host-and-deploy/?view=aspnetcore-10.0#javascript-bundler-support).

### Blazor WebAssembly static asset preloading in Blazor Web Apps

We replaced `<link>` headers with a `ResourcePreloader` component (`<ResourcePreloader />`) for preloading WebAssembly assets in Blazor Web Apps. This permits the app base path configuration (`<base href="..." />`) to correctly identify the app's root.

Removing the component disables the feature if the app is using a [`loadBootResource` callback](../blazor/fundamentals/startup?view=aspnetcore-10.0#load-client-side-boot-resources) to modify URLs.

The Blazor Web App template adopts the feature by default in .NET 10, and apps upgrading to .NET 10 can implement the feature by placing the `ResourcePreloader` component after the base URL tag (`<base>`) in the `App` component's head content (`App.razor`):

```
<head>
    ...
    <base href="/" />
+   <ResourcePreloader />
    ...
</head>
```

For more information, see [Host and deploy ASP.NET Core server-side Blazor apps](../blazor/host-and-deploy/server/?view=aspnetcore-10.0#static-asset-preloading).

### Improved form validation

Blazor now has improved form validation capabilities, including support for validating properties of nested objects and collection items.

To create a validated form, use a [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component inside an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component, just as before.

To opt into the new validation feature:

1. Call the `AddValidation` extension method in the `Program` file where services are registered.
2. Declare the form model types in a C# class file, not in a Razor component (`.razor`).
3. Annotate the root form model type with the `[ValidatableType]` attribute.

Without following the preceding steps, the validation behavior remains the same as in previous .NET releases.

The following example demonstrates customer orders with the improved form validation (details omitted for brevity):

In `Program.cs`, call `AddValidation` on the service collection to enable the new validation behavior:

```
builder.Services.AddValidation();
```

In the following `Order` class, the `[ValidatableType]` attribute is required on the top-level model type. The other types are discovered automatically. `OrderItem` and `ShippingAddress` aren't shown for brevity, but nested and collection validation works the same way in those types if they were shown.

`Order.cs`:

```
[ValidatableType]
public class Order
{
    public Customer Customer { get; set; } = new();
    public List<OrderItem> OrderItems { get; set; } = [];
}

public class Customer
{
    [Required(ErrorMessage = "Name is required.")]
    public string? FullName { get; set; }

    [Required(ErrorMessage = "Email is required.")]
    public string? Email { get; set; }

    public ShippingAddress ShippingAddress { get; set; } = new();
}
```

In the following `OrderPage` component, the [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component is present in the [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component.

`OrderPage.razor`:

```
<EditForm Model="Model">
    <DataAnnotationsValidator />

    <h3>Customer Details</h3>
    <div class="mb-3">
        <label>
            Full Name
            <InputText @bind-Value="Model!.Customer.FullName" />
        </label>
        <ValidationMessage For="@(() => Model!.Customer.FullName)" />
    </div>

    @* ... form continues ... *@
</EditForm>

@code {
    public Order? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    // ... code continues ...
}
```

The requirement to declare the model types outside of Razor components (`.razor` files) is due to the fact that both the new validation feature and the Razor compiler itself are using a source generator. Currently, output of one source generator can't be used as an input for another source generator.

Validation support now includes:

* Validation of nested complex objects and collections is now supported.

  * This includes validation rules defined by property attributes, class attributes, and the [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) implementation.
  * The `[SkipValidation]` attribute can exclude properties or types from validation.

* Validation now uses a source generator-based implementation instead of reflection-based implementation for improved performance and compatibility with ahead-of-time (AOT) compilation.

The [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component now has the same validation order and short-circuiting behavior as [System.ComponentModel.DataAnnotations.Validator](/en-us/dotnet/api/system.componentmodel.dataannotations.validator). The following rules are applied when validating an instance of type `T`:

1. Member properties of `T` are validated, including recursively validating nested objects.
2. Type-level attributes of `T` are validated.
3. The [IValidatableObject.Validate](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject.validate) method is executed, if `T` implements it.

If one of the preceding steps produces a validation error, the remaining steps are skipped.

### Use validation models from a different assembly

You can validate forms with models defined in a different assembly, such as a library or the `.Client` project of a Blazor Web App, by creating a method in the library or `.Client` project that receives an [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection) instance as an argument and calls `AddValidation` on it.

* In the app, call both the method and `AddValidation`.

For more information, see [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-10.0#register-validation-in-multi-assembly-apps).

### Custom Blazor cache and `BlazorCacheBootResources` MSBuild property removed

Now that all Blazor client-side files are fingerprinted and cached by the browser, Blazor's custom caching mechanism and the `BlazorCacheBootResources` MSBuild property have been removed from the framework. If the client-side project's project file contains the MSBuild property, remove the property, as it no longer has any effect:

```
- <BlazorCacheBootResources>...</BlazorCacheBootResources>
```

For more information, see [ASP.NET Core Blazor WebAssembly caching and integrity check failures](../blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures?view=aspnetcore-10.0).

### Web Authentication API (passkey) support for ASP.NET Core Identity

[Web Authentication (WebAuthn) API](https://developer.mozilla.org/docs/Web/API/Web_Authentication_API) support, known widely as *passkeys*, is a modern, phishing-resistant authentication method that improves security and user experience by leveraging public key cryptography and device-based authentication. ASP.NET Core Identity now supports passkey authentication based on WebAuthn and FIDO2 standards. This feature allows users to sign in without passwords, using secure, device-based authentication methods, such as biometrics or security keys.

The Blazor Web App project template provides out-of-the-box passkey management and login functionality.

For more information, see the following articles:

* [Enable Web Authentication API (WebAuthn) passkeys](../security/authentication/passkeys/?view=aspnetcore-10.0)
* [Implement passkeys in ASP.NET Core Blazor Web Apps](../security/authentication/passkeys/blazor?view=aspnetcore-10.0)

### Circuit state persistence

During server-side rendering, Blazor Web Apps can now persist a user's session (circuit) state when the connection to the server is lost for an extended period of time or proactively paused, as long as a full-page refresh isn't triggered. This allows users to resume their session without losing unsaved work in the following scenarios:

* Browser tab throttling
* Mobile device users switching apps
* Network interruptions
* Proactive resource management (pausing inactive circuits)
* [Enhanced navigation](../blazor/fundamentals/routing?view=aspnetcore-10.0#enhanced-navigation-and-form-handling)

For more information, see [ASP.NET Core Blazor server-side state management](../blazor/state-management/server?view=aspnetcore-10.0).

### Hot Reload for Blazor WebAssembly and .NET on WebAssembly

The SDK migrated to a general purpose [Hot Reload](../test/hot-reload?view=aspnetcore-10.0) for WebAssembly scenarios. There's a new MSBuild property `WasmEnableHotReload` that's `true` by default for the `Debug` configuration (`Configuration == "Debug"`) that enables Hot Reload.

For other configurations with custom configuration names, set the value to `true` in the app's project file to enable Hot Reload:

```
<PropertyGroup>
  <WasmEnableHotReload>true</WasmEnableHotReload>
</PropertyGroup>
```

To disable Hot Reload for the `Debug` configuration, set the value to `false`:

```
<PropertyGroup>
  <WasmEnableHotReload>false</WasmEnableHotReload>
</PropertyGroup>
```

### Updated PWA service worker registration to prevent caching issues

The service worker registration in the [Blazor Progressive Web Application (PWA)](../blazor/progressive-web-app/?view=aspnetcore-10.0) project template now includes the [`updateViaCache: 'none'` option](https://developer.mozilla.org/docs/Web/API/ServiceWorkerRegistration/updateViaCache), which prevents caching issues during service worker updates.

```
- navigator.serviceWorker.register('service-worker.js');
+ navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });
```

The option ensures that:

* The browser doesn't use cached versions of the service worker script.
* Service worker updates are applied reliably without being blocked by HTTP caching.
* PWA applications can update their service workers more predictably.

This addresses caching issues that can prevent service worker updates from being applied correctly, which is particularly important for PWAs that rely on service workers for offline functionality.

We recommend using the option set to `none` in all PWAs, including those that target .NET 9 or earlier.

### Serialization extensibility for persistent component state

Implement a custom serializer with [PersistentComponentStateSerializer\<T>](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstateserializer-1). Without a registered custom serializer, serialization falls back to the existing JSON serialization.

The custom serializer is registered in the app's `Program` file. In the following example, the `CustomUserSerializer` is registered for the `TUser` type:

```
builder.Services.AddSingleton<PersistentComponentStateSerializer<TUser>, 
    CustomUserSerializer>();
```

The type is automatically persisted and restored with the custom serializer:

```
[PersistentState] 
public User? CurrentUser { get; set; } = new();
```

### `OwningComponentBase` now implements `IAsyncDisposable`

[`OwningComponentBase`](../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope) now includes support for asynchronous disposal, improving resource management. There are new `DisposeAsync` and `DisposeAsyncCore` methods with an updated `Dispose` method to handle both synchronous and asynchronous disposal of the service scope.

### New `InputHidden` component to handle hidden input fields in forms

The new `InputHidden` component provides a hidden input field for storing string values.

In the following example, a hidden input field is created for the form's `Parameter` property. When the form is submitted, the value of the hidden field is displayed:

```
<EditForm Model="Parameter" OnValidSubmit="Submit" FormName="InputHidden Example">
    <InputHidden id="hidden" @bind-Value="Parameter" />
    <button type="submit">Submit</button>
</EditForm>

@if (submitted)
{
    <p>Hello @Parameter!</p>
}

@code {
    private bool submitted;

    [SupplyParameterFromForm] 
    public string Parameter { get; set; } = "stranger";

    private void Submit() => submitted = true;
}
```

### Persistent component state support for enhanced navigation

Blazor now supports handling persistent component state during [enhanced navigation](../blazor/fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling). State persisted during enhanced navigation can be read by interactive components on the page.

By default, persistent component state is only loaded by interactive components when they're initially loaded on the page. This prevents important state, such as data in an edited webform, from being overwritten if additional enhanced navigation events to the same page occur after the component is loaded.

If the data is read-only and doesn't change frequently, opt-in to allow updates during enhanced navigation by setting `AllowUpdates = true` on the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute). This is useful for scenarios such as displaying cached data that's expensive to fetch but doesn't change often. The following example demonstrates the use of `AllowUpdates` for weather forecast data:

```
[PersistentState(AllowUpdates = true)]
public WeatherForecast[]? Forecasts { get; set; }

protected override async Task OnInitializedAsync()
{
    Forecasts ??= await ForecastService.GetForecastAsync();
}
```

To skip restoring state during prerendering, set `RestoreBehavior` to `SkipInitialValue`:

```
[PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)]
public string NoPrerenderedData { get; set; }
```

To skip restoring state during reconnection, set `RestoreBehavior` to `SkipLastSnapshot`. This can be useful to ensure fresh data after reconnection:

```
[PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
public int CounterNotRestoredOnReconnect { get; set; }
```

Call `PersistentComponentState.RegisterOnRestoring` to register a callback for imperatively controlling how state is restored, similar to how [PersistentComponentState.RegisterOnPersisting](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) provides full control of how state is persisted.

### Blazor WebAssembly respects the current UI culture setting

In .NET 9 or earlier, standalone Blazor WebAssembly apps load UI globalization resources based on [CultureInfo.DefaultThreadCurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentculture#system-globalization-cultureinfo-defaultthreadcurrentculture). If you want to additionally load globalization data for your localization culture defined by [CultureInfo.DefaultThreadCurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentuiculture#system-globalization-cultureinfo-defaultthreadcurrentuiculture), [upgrade the app to .NET 10 or later](../migration/?view=aspnetcore-10.0).

## Blazor Hybrid

This section describes new features for Blazor Hybrid.

### New .NET MAUI Blazor Hybrid with a Blazor Web App and ASP.NET Core Identity article and sample

A new article and sample app has been added for .NET MAUI Blazor Hybrid and Web App using ASP.NET Core Identity.

For more information, see the following resources:

* [.NET MAUI Blazor Hybrid and Web App with ASP.NET Core Identity](../blazor/hybrid/security/maui-blazor-web-identity?view=aspnetcore-10.0)
* [`MauiBlazorWebIdentity` sample app (`dotnet/blazor-samples` GitHub repository)](https://github.com/dotnet/blazor-samples/tree/main/9.0/MauiBlazorWebIdentity)

## SignalR

This section describes new features for SignalR.

## Minimal APIs

This section describes new features for Minimal APIs.

### Treating empty string in form post as null for nullable value types

When using the `[FromForm]` attribute with a complex object in Minimal APIs, empty string values in a form post are now converted to `null` rather than causing a parse failure. This behavior matches the processing logic for form posts not associated with complex objects in Minimal APIs.

```
using Microsoft.AspNetCore.Http;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/todo", ([FromForm] Todo todo) => TypedResults.Ok(todo));

app.Run();

public class Todo
{
  public int Id { get; set; }
  public DateOnly? DueDate { get; set; } // Empty strings map to `null`
  public string Title { get; set; }
  public bool IsCompleted { get; set; }
}
```

Thanks to [@nvmkpk](https://github.com/nvmkpk) for contributing this change!

### Validation support in Minimal APIs

Support for validation in Minimal APIs is now available. This feature allows you to request validation of data sent to your API endpoints. Enabling validation allows the ASP.NET Core runtime to perform any validations defined on the:

* Query
* Header
* Request body

Validations are defined using attributes in the [`DataAnnotations`](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace. Developers customize the behavior of the validation system by:

* Creating custom [`[Validation]`](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) attribute implementations.
* Implementing the [`IValidatableObject`](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) interface for complex validation logic.

If validation fails, the runtime returns a 400 Bad Request response with details of the validation errors.

#### Enable built-in validation support for Minimal APIs

Enable the built-in validation support for Minimal APIs by calling the `AddValidation` extension method to register the required services in the service container for your application:

```
builder.Services.AddValidation();
```

The implementation automatically discovers types that are defined in Minimal API handlers or as base types of types defined in Minimal API handlers. An endpoint filter performs validation on these types and is added for each endpoint.

Note

`AddValidation` uses a source generator that only discovers validatable types within the assembly where `AddValidation` is called. If the Minimal API endpoints are defined in a different assembly, call `AddValidation` from within that assembly. For more information, see [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-10.0#register-validation-in-multi-assembly-apps).

Validation can be disabled for specific endpoints by using the `DisableValidation` extension method, as in the following example:

```
app.MapPost("/products",
    ([EvenNumber(ErrorMessage = "Product ID must be even")] int productId, [Required] string name)
        => TypedResults.Ok(productId))
    .DisableValidation();
```

Note

Several small improvements and fixes have been made to the Minimal APIs validation generator introduced in ASP.NET Core for .NET 10. To support future enhancements, the underlying validation resolver APIs are now marked as experimental. The top-level `AddValidation` APIs and the built-in validation filter remain stable and non-experimental.

#### Validation with record types

Minimal APIs also support validation with C# record types. Record types can be validated using attributes from the [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace, similar to classes. For example:

```
public record Product(
    [Required] string Name,
    [Range(1, 1000)] int Quantity);
```

When using record types as parameters in Minimal API endpoints, validation attributes are automatically applied in the same way as class types:

```
app.MapPost("/products", (Product product) =>
{
    // Endpoint logic here
    return TypedResults.Ok(product);
});
```

### Minimal API Validation integration with IProblemDetailsService

Error responses from the validation logic for Minimal APIs can now be customized by an `IProblemDetailsService` implementation provided in the application services collection (Dependency Injection container). This enables more consistent and user-specific error responses.

### Support for Server-Sent Events (SSE)

ASP.NET Core now supports returning a [ServerSentEvents](/en-us/dotnet/api/system.net.serversentevents) result using the [TypedResults.ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,051e6796e1492f84) API. This feature is supported in both Minimal APIs and controller-based apps.

Server-Sent Events is a server push technology that allows a server to send a stream of event messages to a client over a single HTTP connection. In .NET the event messages are represented as [`SseItem<T>`](/en-us/dotnet/api/system.net.serversentevents.sseitem-1) objects, which may contain an event type, an ID, and a data payload of type `T`.

The [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) class has a new static method called [ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,ceb980606eb9e295) that can be used to return a `ServerSentEvents` result. The first parameter to this method is an `IAsyncEnumerable<SseItem<T>>` that represents the stream of event messages to be sent to the client.

The following example illustrates how to use the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as JSON objects to the client:

```
app.MapGet("/json-item", (CancellationToken cancellationToken) =>
{
    async IAsyncEnumerable<HeartRateRecord> GetHeartRate(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var heartRate = Random.Shared.Next(60, 100);
            yield return HeartRateRecord.Create(heartRate);
            await Task.Delay(2000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken),
                                                  eventType: "heartRate");
});
```

For more information, see:

* [Server-Sent Events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) on MDN.
* [Minimal API sample app](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/samples/10.0/MinimalServerSentEvents/Program.cs) using the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as string, `ServerSentEvents`, and JSON objects to the client.
* [Controller API sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE) using the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as string, `ServerSentEvents`, and JSON objects to the client.

### Validation APIs moved to Microsoft.Extensions.Validation

The validation APIs have moved to the `Microsoft.Extensions.Validation` namespace and NuGet package. This change makes the APIs usable outside of ASP.NET Core HTTP scenarios. The public APIs and behavior remain unchanged—only the package and namespace are different. Existing projects don't require code changes, as old references redirect to the new implementation.

### Enhanced validation for classes and records

Validation attributes can now be applied to both classes and records with consistent code generation and validation behavior. This enhancement improves flexibility when designing models using records in ASP.NET Core apps.

Community contribution: Thanks to [@marcominerva](https://github.com/marcominerva)!

## OpenAPI

This section describes new features for OpenAPI.

### OpenAPI 3.1 support

ASP.NET Core has added support for generating [OpenAPI version 3.1](https://spec.openapis.org/oas/v3.1.1.html) documents in .NET 10. Despite the minor version bump, OpenAPI 3.1 is a significant update to the OpenAPI specification, in particular with full support for [JSON Schema draft 2020-12](https://json-schema.org/specification-links#2020-12).

Some of the changes you will see in the generated OpenAPI document include:

* Nullable types no longer have the `nullable: true` property in the schema.
* Instead of a `nullable: true` property, they have a `type` keyword whose value is an array that includes `null` as one of the types.
* Properties or parameters defined as a C# `int` or `long` now appear in the generated OpenAPI document without the `type: integer` field and have a `pattern` field limiting the value to digits. This happens when the [NumberHandling](/en-us/dotnet/api/system.text.json.jsonserializeroptions.numberhandling#system-text-json-jsonserializeroptions-numberhandling) property in the [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) is set to `AllowReadingFromString`, the default for ASP.NET Core Web apps. To enable C# `int` and `long` to be represented in the OpenAPI document as `type: integer`, set the [NumberHandling](/en-us/dotnet/api/system.text.json.jsonserializeroptions.numberhandling#system-text-json-jsonserializeroptions-numberhandling) property to `Strict`.

With this feature, the default OpenAPI version for generated documents is`3.1`. The version can be changed by explicitly setting the [OpenApiVersion](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.openapiversion) property of the [OpenApiOptions](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions) in the `configureOptions` delegate parameter of [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi):

```
builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});
```

When generating the OpenAPI document at build time, the OpenAPI version can be selected by setting the `--openapi-version` in the `OpenApiGenerateDocumentsOptions` MSBuild item:

```
<PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
    <!-- Configure build-time OpenAPI generation to produce an OpenAPI 3.1 document. -->
    <OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_1</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
```

OpenAPI 3.1 support was primarily added in the following [PR](https://github.com/dotnet/aspnetcore/pull/59480).

### OpenAPI 3.1 breaking changes

Support for OpenAPI 3.1 requires an update to the underlying OpenAPI.NET library to a new major version, 2.0. This new version has some breaking changes from the previous version. The breaking changes may impact apps if they have any document, operation, or schema transformers. Breaking changes in this iteration include the following:

* Entities within the OpenAPI document, like operations and parameters, are typed as interfaces. Concrete implementations exist for the inlined and referenced variants of an entity. For example, an `IOpenApiSchema` can either be an inlined `OpenApiSchema` or an `OpenApiSchemaReference` that points to a schema defined elsewhere in the document.
* The `Nullable` property has been removed from the `OpenApiSchema` type. To determine if a type is nullable, evaluate if the `OpenApiSchema.Type` property sets `JsonSchemaType.Null`.

One of the most significant changes is that the `OpenApiAny` class has been dropped in favor of using `JsonNode` directly. Transformers that use `OpenApiAny` need to be updated to use `JsonNode`. The following diff shows the changes in schema transformer from .NET 9 to .NET 10:

```
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
    if (context.JsonTypeInfo.Type == typeof(WeatherForecast))
    {
-       schema.Example = new OpenApiObject
+       schema.Example = new JsonObject
        {
-           ["date"] = new OpenApiString(DateTime.Now.AddDays(1).ToString("yyyy-MM-dd")),
+           ["date"] = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd"),
-           ["temperatureC"] = new OpenApiInteger(0),
+           ["temperatureC"] = 0,
-           ["temperatureF"] = new OpenApiInteger(32),
+           ["temperatureF"] = 32,
-           ["summary"] = new OpenApiString("Bracing"),
+           ["summary"] = "Bracing",
        };
    }
    return Task.CompletedTask;
});
```

Note that these changes are necessary even when only configuring the OpenAPI version to 3.0.

### OpenAPI in YAML

ASP.NET now supports serving the generated OpenAPI document in YAML format. YAML can be more concise than JSON, eliminating curly braces and quotation marks when these can be inferred. YAML also supports multi-line strings, which can be useful for long descriptions.

To configure an app to serve the generated OpenAPI document in YAML format, specify the endpoint in the MapOpenApi call with a ".yaml" or ".yml" suffix, as shown in the following example:

```
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi("/openapi/{documentName}.yaml");
}
```

Support for:

* YAML is currently only available for the OpenAPI served from the OpenAPI endpoint.
* Generating OpenAPI documents in YAML format at build time is added in a future preview.

See [this PR](https://github.com/dotnet/aspnetcore/pull/58616) which added support for serving the generated OpenAPI document in YAML format.

### Response description on `ProducesResponseType` for API controllers

The [ProducesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute), [ProducesResponseTypeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute), and [ProducesDefaultResponseTypeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesdefaultresponsetypeattribute) now accept an optional string parameter, `Description`, that sets the description of the response:

```
[HttpGet(Name = "GetWeatherForecast")]
[ProducesResponseType<IEnumerable<WeatherForecast>>(StatusCodes.Status200OK,
    Description = "The weather forecast for the next 5 days.")]
public IEnumerable<WeatherForecast> Get()
{
```

Generated OpenAPI data:

```
"responses": {
  "200": {
    "description": "The weather forecast for the next 5 days.",
    "content": {
```

This functionality is supported in both [API controllers](../web-api/?view=aspnetcore-10.0#apicontroller-attribute) and [Minimal APIs](../fundamentals/apis?view=aspnetcore-10.0). For Minimal APIs, the `Description` property is correctly set even when the attribute's type and the inferred return type aren't an exact match.

[Community contribution (`dotnet/aspnetcore` #58193)](https://github.com/dotnet/aspnetcore/pull/58193) by [Sander ten Brinke](https://github.com/sander1095).

### Populate XML doc comments into OpenAPI document

ASP.NET Core OpenAPI document generation will now include metadata from XML doc comments on method, class, and member definitions in the OpenAPI document. You must enable XML doc comments in your project file to use this feature. You can do this by adding the following property to your project file:

```
  <PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
```

At build-time, the OpenAPI package will leverage a source generator to discover XML comments in the current application assembly and any project references and emit source code to insert them into the document via an OpenAPI document transformer.

Note that the C# build process does not capture XML doc comments placed on lambda expressions, so to use XML doc comments to add metadata to a Minimal API endpoint, you must define the endpoint handler as a method, put the XML doc comments on the method, and then reference that method from the `MapXXX` method. For example, to use XML doc comments to add metadata to a Minimal API endpoint originally defined as a lambda expression:

```
app.MapGet("/hello", (string name) =>$"Hello, {name}!");
```

Change the `MapGet` call to reference a method:

```
app.MapGet("/hello", Hello);
```

Define the `Hello` method with XML doc comments:

```
static partial class Program
{
    /// <summary>
    /// Sends a greeting.
    /// </summary>
    /// <remarks>
    /// Greeting a person by their name.
    /// </remarks>
    /// <param name="name">The name of the person to greet.</param>
    /// <returns>A greeting.</returns>
    public static string Hello(string name)
    {
        return $"Hello, {name}!";
    }
}
```

In the previous example the `Hello` method is added to the `Program` class, but you can add it to any class in your project.

The previous example illustrates the `<summary>`, `<remarks>`, and `<param>` XML doc comments. For more information about XML doc comments, including all the supported tags, see the [C# documentation](/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags).

Since the core functionality is provided via a source generator, it can be disabled by adding the following MSBuild to your project file.

```
<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.2.*" GeneratePathProperty="true" />
</ItemGroup>

<Target Name="DisableCompileTimeOpenApiXmlGenerator" BeforeTargets="CoreCompile">
  <ItemGroup>
    <Analyzer Remove="$(PkgMicrosoft_AspNetCore_OpenApi)/analyzers/dotnet/cs/Microsoft.AspNetCore.OpenApi.SourceGenerators.dll" />
  </ItemGroup>
</Target>
```

The source generator process XML files included in the `AdditionalFiles` property. To add (or remove), sources modify the property as follows:

```
<Target Name="AddXmlSources" BeforeTargets="CoreCompile">
  <ItemGroup>
    <AdditionalFiles Include="$(PkgSome_Package)/lib/net10.0/Some.Package.xml" />
  </ItemGroup>
</Target>
```

### Microsoft.AspNetCore.OpenApi added to the ASP.NET Core web API (Native AOT) template

The **ASP.NET Core Web API (Native AOT)** project template (short name `webapiaot`) now includes support for OpenAPI document generation using the [`Microsoft.AspNetCore.OpenApi` package](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi) by default. This support is disabled by using the `--no-openapi` flag when creating a new project.

[Community contribution (`dotnet/aspnetcore` #60337)](https://github.com/dotnet/aspnetcore/pull/60337) by [Sander ten Brinke](https://github.com/sander1095).

### Support for `IOpenApiDocumentProvider` in the DI container

ASP.NET Core in .NET 10 supports [IOpenApiDocumentProvider](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapidocumentprovider) in the dependency injection (DI) container. Inject the interface to access the OpenAPI document. This approach is useful for accessing OpenAPI documents outside the context of HTTP requests, such as in background services or custom middleware.

Previously, running app startup logic without launching an HTTP server could be accomplished using [`HostFactoryResolver`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs) with a no-op [IServer](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.iserver) implementation. The new feature simplifies this process.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

For more information, see [Add IOpenApiDocumentProvider interface and implementation (`dotnet/aspnetcore` #61463)](https://github.com/dotnet/aspnetcore/pull/61463).

### Improvements to the XML comment generator

XML comment generation handles complex types in .NET 10 better than earlier versions of .NET.

* It produces accurate and complete XML comments for a wider range of types.
* It handles more complex scenarios.
* It gracefully bypasses processing for complex types that cause build errors in earlier versions.

These improvements change the failure mode for certain scenarios from build errors to missing metadata.

In addition, XML doc comment processing can now be configured to access XML comments in other assemblies. This is useful for generating documentation for types that are defined outside the current assembly, such as the `ProblemDetails` type in the `Microsoft.AspNetCore.Http` namespace.

This configuration is done with directives in the project build file. The following example shows how to configure the XML comment generator to access XML comments for types in the `Microsoft.AspNetCore.Http` assembly, which includes the `ProblemDetails` class.

```
<Target Name="AddOpenApiDependencies" AfterTargets="ResolveReferences">
  <ItemGroup>
  <!-- Include XML documentation from Microsoft.AspNetCore.Http.Abstractions
    to get metadata for ProblemDetails -->
    <AdditionalFiles
          Include="@(ReferencePath->'
            %(RootDir)%(Directory)%(Filename).xml')"
          Condition="'%(ReferencePath.Filename)' ==
           'Microsoft.AspNetCore.Http.Abstractions'"
          KeepMetadata="Identity;HintPath" />
  </ItemGroup>
</Target>
```

We expect to include XML comments from a selected set of assemblies in the shared framework in future previews to avoid the need for this configuration in most cases.

#### Unified handling of documentation IDs in OpenAPI XML comment generator

XML documentation comments from referenced assemblies are correctly merged even when their documentation IDs include return type suffixes. As a result, all valid XML comments are reliably included in generated OpenAPI documentation, improving documentation accuracy and completeness for APIs using referenced assemblies.

### Form data enum parameters use actual enum type in OpenAPI

Form data parameters in MVC controller actions now generate OpenAPI metadata using the actual enum type instead of defaulting to string.

Community contribution: Thanks to [@ascott18](https://github.com/ascott18)!

### Support for generating OpenApiSchemas in transformers

Developers can now generate a schema for a C# type using the same logic as ASP.NET Core OpenAPI document generation and add it to the OpenAPI document. The schema can then be referenced from elsewhere in the OpenAPI document.

The context passed to document, operation, and schema transformers includes a new `GetOrCreateSchemaAsync` method that can be used to generate a schema for a type. This method also has an optional `ApiParameterDescription` parameter to specify additional metadata for the generated schema.

To support adding the schema to the OpenAPI document, a `Document` property has been added to the Operation and Schema transformer contexts. This allows any transformer to add a schema to the OpenAPI document using the document's `AddComponent` method.

#### Example

To use this feature in a document, operation, or schema transformer, create the schema using the `GetOrCreateSchemaAsync` method provided in the context and add it to the OpenAPI document using the document's `AddComponent` method.

```
builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer(async (operation, context, cancellationToken) =>
    {
        // Generate schema for error responses
        var errorSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), null, cancellationToken);
        context.Document?.AddComponent("Error", errorSchema);

        operation.Responses ??= new OpenApiResponses();
        // Add a "4XX" response to the operation with the newly created schema
        operation.Responses["4XX"] = new OpenApiResponse
        {
            Description = "Bad Request",
            Content = new Dictionary<string, OpenApiMediaType>
            {
                ["application/problem+json"] = new OpenApiMediaType
                {
                    Schema = new OpenApiSchemaReference("Error", context.Document)
                }
            }
        };
    });
});
```

### Endpoint-specific OpenAPI operation transformers

Endpoint-specific operation transformers enable fine-grained customization of OpenAPI documentation for individual route endpoints. This feature allows developers to tailor Swagger/OpenAPI metadata and descriptions on a per-action or per-route basis, enhancing extensibility for advanced API scenarios.

For implementation details and code samples, see [Customize OpenAPI documents](../fundamentals/openapi/customize-openapi?view=aspnetcore-10.0#use-operation-transformers).

### Upgrade Microsoft.OpenApi to 2.0.0

The [`Microsoft.OpenApi`](https://www.nuget.org/packages/Microsoft.OpenApi/) library used for OpenAPI document generation in ASP.NET Core has been upgraded to version 2.0.0 (GA).

#### Breaking changes in 2.0.0

The following breaking changes were introduced in the preview releases and remain in the GA version. These primarily affect users who implement document, operation, or schema transformers:

* [Ephemeral object properties are now in Metadata](https://github.com/microsoft/OpenAPI.NET/blob/main/docs/upgrade-guide-2.md#ephemeral-object-properties-are-now-in-metadata)
* [Use HTTP Method Object Instead of Enum](https://github.com/microsoft/OpenAPI.NET/blob/main/docs/upgrade-guide-2.md#use-http-method-object-instead-of-enum)

With the update to the GA version, no further breaking changes are expected in OpenAPI document generation.

### OpenAPI Schema Generation Enhancements

#### Model nullable types using oneOf in OpenAPI schema

OpenAPI schema generation for nullable types was improved by using the `oneOf` pattern instead of the nullable property for complex types and collections. The implementation:

* Uses `oneOf` with `null` and the actual type schema for nullable complex types in request and response schemas.
* Detects nullability for parameters, properties, and return types using reflection and `NullabilityInfoContext`.
* Removes null types from componentized schemas to avoid duplication.

#### Fixes and improvements to schema reference resolution

This release improves the handling of JSON schemas for OpenAPI document generation by properly resolving relative JSON schema references (`$ref`) in the root schema document.

#### Include property descriptions as siblings of $ref in OpenAPI schema

Prior to .NET 10, ASP.NET Core discarded descriptions on properties defined with `$ref` in the generated OpenAPI document because OpenAPI v3.0 didn't allow sibling properties alongside `$ref` in schema definitions. OpenAPI 3.1 now lets you include descriptions alongside `$ref`. RC1 adds support for including property descriptions as siblings of `$ref` in the generated OpenAPI schema.

This was a community contribution. Thanks @desjoerd!

#### Add metadata from XML comments on `[AsParameters]` types to OpenAPI schema

OpenAPI schema generation now processes XML comments on properties of `[AsParameters]` parameter classes to extract metadata for documentation.

#### Exclude unknown HTTP methods from OpenAPI

OpenAPI schema generation now excludes unknown HTTP methods from the generated OpenAPI document. Query methods, which are standard HTTP methods but not recognized by OpenAPI, are now gracefully excluded from the generated OpenAPI document.

This was a community contribution. Thanks @martincostello!

#### Improve the description of JSON Patch request bodies

The OpenAPI schema generation for JSON Patch operations now correctly applies the `application/json-patch+json` media type to request bodies that use JSON Patch. This ensures that the generated OpenAPI document accurately reflects the expected media type for JSON Patch operations. In addition, the JSON Patch request body has a detailed schema that describes the structure of the JSON Patch document, including the operations that can be performed.

This was a community contribution. Thanks @martincostello!

#### Use invariant culture for OpenAPI document generation

OpenAPI document generation now uses invariant culture for formatting numbers and dates in the generated OpenAPI document. This ensures that the generated document is consistent and does not vary based on the server's culture settings.

This was a community contribution. Thanks @martincostello!

## Authentication and authorization

### Authentication and authorization metrics

Metrics have been added for certain authentication and authorization events in ASP.NET Core. With this change, you can now obtain metrics for the following events:

* Authentication:

  * Authenticated request duration
  * Challenge count
  * Forbid count
  * Sign in count
  * Sign out count

* Authorization:
  * Count of requests requiring authorization

The following image shows an example of the Authenticated request duration metric in the Aspire dashboard:

For more information, see [ASP.NET Core built-in authentication and authorization metrics](../metrics/security?view=aspnetcore-10.0#microsoftaspnetcoreauthorization).

### ASP.NET Core Identity metrics

[ASP.NET Core Identity](../security/authentication/identity?view=aspnetcore-10.0) observability has been improved in .NET 10 with metrics. Metrics are counters, histograms, and gauges that provide time-series measurements of system or application behavior.

For example, use the new ASP.NET Core Identity metrics to observe:

* **User management**: New user creations, password changes, and role assignments.
* **Login/session handling**: Login attempts, sign ins, sign outs, and users using two factor authentication.

The new metrics are in the `Microsoft.AspNetCore.Identity` meter:

* `aspnetcore.identity.user.create.duration`
* `aspnetcore.identity.user.update.duration`
* `aspnetcore.identity.user.delete.duration`
* `aspnetcore.identity.user.check_password_attempts`
* `aspnetcore.identity.user.generated_tokens`
* `aspnetcore.identity.user.verify_token_attempts`
* `aspnetcore.identity.sign_in.authenticate.duration`
* `aspnetcore.identity.sign_in.check_password_attempts`
* `aspnetcore.identity.sign_in.sign_ins`
* `aspnetcore.identity.sign_in.sign_outs`
* `aspnetcore.identity.sign_in.two_factor_clients_remembered`
* `aspnetcore.identity.sign_in.two_factor_clients_forgotten`

For more information about using metrics in ASP.NET Core, see [ASP.NET Core metrics](../metrics/overview?view=aspnetcore-10.0).

### Avoid cookie login redirects for known API endpoints

By default, unauthenticated and unauthorized requests made to known API endpoints protected by cookie authentication now result in 401 and 403 responses rather than redirecting to a login or access denied URI.

This change was [highly requested](https://github.com/dotnet/aspnetcore/issues/9039), because redirecting unauthenticated requests to a login page doesn't usually make sense for API endpoints which typically rely on 401 and 403 status codes rather than HTML redirects to communicate auth failures.

Known API [Endpoints](../fundamentals/routing?view=aspnetcore-10.0) are identified using the new `IApiEndpointMetadata` interface, and metadata implementing the new interface has been added automatically to the following:

* `[ApiController]` endpoints
* Minimal API endpoints that read JSON request bodies or write JSON responses
* Endpoints using `TypedResults` return types
* SignalR endpoints

When `IApiEndpointMetadata` is present, the cookie authentication handler now returns appropriate HTTP status codes (401 for unauthenticated requests, 403 for forbidden requests) instead of redirecting.

If you want to prevent this new behavior, and always redirect to the login and access denied URIs for unauthenticated or unauthorized requests regardless of the target endpoint, you can override the `RedirectToLogin` and `RedirectToAccessDenied` events as follows:

```
builder.Services.AddAuthentication()
    .AddCookie(options =>
    {
        options.Events.OnRedirectToLogin = context =>
        {
            context.Response.Redirect(context.RedirectUri);
            return Task.CompletedTask;
        };

        options.Events.OnRedirectToAccessDenied = context =>
        {
            context.Response.Redirect(context.RedirectUri);
            return Task.CompletedTask;
        };
    });
```

For more information about this breaking change, see [ASP.NET Core breaking changes announcement](https://github.com/aspnet/Announcements/issues/525).

## Miscellaneous

This section describes miscellaneous new features in .NET 10.

### Configure suppressing exception handler diagnostics

A new configuration option has been added to the [ASP.NET Core exception handler middleware](../fundamentals/error-handling?view=aspnetcore-10.0#exception-handler-page) to control diagnostic output: `ExceptionHandlerOptions.SuppressDiagnosticsCallback`. This callback is passed context about the request and exception, allowing you to add logic that determines whether the middleware should write exception logs and other telemetry.

This setting is useful when you know an exception is transient or has been handled by the exception handler middleware, and you don't want error logs written to your observability platform.

The middleware's default behavior has also changed: it no longer writes exception diagnostics for exceptions handled by `IExceptionHandler`. Based on user feedback, logging handled exceptions at the error level was often undesirable when `IExceptionHandler.TryHandleAsync` returned `true`.

You can revert to the previous behavior by configuring `SuppressDiagnosticsCallback`:

```
app.UseExceptionHandler(new ExceptionHandlerOptions
{
    SuppressDiagnosticsCallback = context => false;
});
```

For more information about this breaking change, see <https://github.com/aspnet/Announcements/issues/524>.

### Support for the .localhost Top-Level Domain

The `.localhost` top-level domain (TLD) is defined in [RFC2606](https://www.rfc-editor.org/rfc/rfc2606) and [RFC6761](https://www.rfc-editor.org/rfc/rfc6761) as being reserved for testing purposes and available for users to use locally as they would any other domain name. This means using a name like `myapp.localhost` locally that resolves to the IP loopback address is allowed and expected according to these RFCs. Additionally, modern evergreen browsers already automatically resolve any `*.localhost` name to the IP loopback address (`127.0.0.1`/`::1`), effectively making them an alias for any service already being hosted at `localhost` on the local machine, that is, any service responding to `http://localhost:6789` will also respond to `http://anything-here.localhost:6789`, assuming no further specific hostname verification or enforcement is being performed by the service.

ASP.NET Core has been updated in .NET 10 Preview 7 to better support the `.localhost` TLD, such that it can now be easily used when creating and running ASP.NET Core applications in your local development environment. Having different apps running locally be resolvable via different names allows for better separation of some domain-name-associated website assets, e.g. cookies, and makes it easier to identify which app you're browsing via the name displayed in the browser address bar.

ASP.NET Core's built-in HTTP server, Kestrel, will now correctly treat any `*.localhost` name set via [supported endpoint configuration mechanisms](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#configure-endpoints) as the local loopback address and thus bind to it rather than all external address (i.e. bind to `127.0.0.1`/`::1` rather than `0.0.0.0`/`::`). This includes the `"applicationUrl"` property in [launch profiles configured in a *launchSettings.json* file](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson), and the `ASPNETCORE_URLS` environment variable. When configured to listen on a `.localhost` address, Kestrel will log an information message for both the `.localhost` **and** `localhost` addresses, to make it clear that both names can be used.

While web browsers automatically resolve `*.localhost` names to the local loopback address, other apps might treat `*.localhost` names as regular domain names and attempt to resolve them via their corresponding DNS stack. If your DNS configuration doesn't resolve `*.localhost` names to an address, they fail to connect. You can continue to use the regular `localhost` name to address your apps when not in a web browser.

The [ASP.NET Core HTTPS development certificate](../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate) (including the `dotnet dev-certs https` command) have been updated to ensure the certificate is valid for use with the `*.dev.localhost` domain name. After installing .NET 10 SDK Preview 7, trust the new developer certificate by running `dotnet dev-certs https --trust` at the command line to ensure your system is configured to trust the new certificate.

The certificate lists the `*.dev.localhost` name as a Subject Alternative Name (SAN) rather than `*.localhost` because using a wildcard certificate for a top-level domain name is invalid.

The project templates for *ASP.NET Core Empty* (`web`) and *Blazor Web App* (`blazor`) have been updated with a new option that when specified configures the created project to use the `.dev.localhost` domain name suffix, combining it with the project name to allow the app to be browsed to at an address like `https://myapp.dev.localhost:5036`:

```
$ dotnet new web -n MyApp --localhost-tld
The template "ASP.NET Core Empty" was created successfully.

Processing post-creation actions...
Restoring D:\src\MyApp\MyApp.csproj:
Restore succeeded.

$ cd .\MyApp\
$ dotnet run --launch-profile https
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://myapp.dev.localhost:7099
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7099/
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://myapp.dev.localhost:5036
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5036/
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\src\local\10.0.1xx\MyApp
```

### Json+PipeReader deserialization support in MVC and Minimal APIs

PR: <https://github.com/dotnet/aspnetcore/pull/62895>

See <https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview7/libraries.md#pipereader-support-for-json-serializer>

MVC, Minimal APIs, and the `HttpRequestJsonExtensions.ReadFromJsonAsync` methods have all been updated to use the new Json+PipeReader support without requiring any code changes from applications.

For most applications, the addition of this support has no effect on their behavior. However, if the application is using a custom [JsonConverter](/en-us/dotnet/api/system.text.json.serialization.jsonconverter), there's a chance that the converter doesn't handle [Utf8JsonReader.HasValueSequence](/en-us/dotnet/api/system.text.json.utf8jsonreader.hasvaluesequence) correctly. This can result in missing data and errors, such as [ArgumentOutOfRangeException](/en-us/dotnet/api/system.argumentoutofrangeexception), when deserializing.

The quick workaround (especially if you don't own the custom `JsonConverter` being used) is to set the `"Microsoft.AspNetCore.UseStreamBasedJsonParsing"` [AppContext](/en-us/dotnet/api/system.appcontext) switch to `"true"`. This should be a temporary workaround, and the `JsonConverter` should be updated to support [HasValueSequence](/en-us/dotnet/api/system.text.json.utf8jsonreader.hasvaluesequence).

To fix `JsonConverter` implementations, there's a quick fix that allocates an array from the `ReadOnlySequence` and would look like the following example:

```
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
    // previous code
}
```

There's also a more complicated (but performant) fix, which would involve having a separate code path for the `ReadOnlySequence` handling:

```
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    if (reader.HasValueSequence)
    {
        reader.ValueSequence;
        // ReadOnlySequence optimized path
    }
    else
    {
        reader.ValueSpan;
        // ReadOnlySpan optimized path
    }
}
```

### Automatic eviction from memory pool

The memory pools used by Kestrel, IIS, and HTTP.sys now automatically evict memory blocks when the application is idle or under less load. The feature runs automatically and doesn't need to be enabled or configured manually.

#### Why memory eviction matters

Previously, memory allocated by the pool would remain reserved, even when not in use. This feature releases memory back to the system when the app is idle for a period of time. This eviction reduces overall memory usage and helps applications stay responsive under varying workloads.

#### Use memory eviction metrics

Metrics have been added to the default memory pool used by our server implementations. The new metrics are under the name `"Microsoft.AspNetCore.MemoryPool"`.

For information about metrics and how to use them, see [ASP.NET Core metrics](../metrics/overview?view=aspnetcore-10.0).

#### Manage memory pools

Besides using memory pools more efficiently by evicting unneeded memory blocks, .NET 10 improves the experience of creating memory pools. It does this by providing a built-in [IMemoryPoolFactory](https://source.dot.net/#Microsoft.AspNetCore.Connections.Abstractions/IMemoryPoolFactory.cs) and a `MemoryPoolFactory` implementation. It makes the implementation available to your application through dependency injection.

The following code example shows a simple background service that uses the built-in memory pool factory implementation to create memory pools. These pools benefit from the automatic eviction feature:

```
public class MyBackgroundService : BackgroundService
{
    private readonly MemoryPool<byte> _memoryPool;

    public MyBackgroundService(IMemoryPoolFactory<byte> factory)
    {
        _memoryPool = factory.Create();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(20, stoppingToken);
                // do work that needs memory
                var rented = _memoryPool.Rent(100);
                rented.Dispose();
            }
            catch (OperationCanceledException)
            {
                return;
            }
        }
    }
}
```

To use your own memory pool factory, make a class that implements `IMemoryPoolFactory` and register it with dependency injection, as the following example does. Memory pools created this way do not benefit from the automatic eviction feature unless you implement similar eviction logic in your custom factory:

```
services.AddSingleton<IMemoryPoolFactory<byte>,
CustomMemoryPoolFactory>();

public class CustomMemoryPoolFactory : IMemoryPoolFactory<byte>
{
    public MemoryPool<byte> Create()
    {
        // Return a custom MemoryPool implementation
        // or the default, as is shown here.
        return MemoryPool<byte>.Shared;
    }
}
```

### Customizable security descriptors for HTTP.sys

You can now specify a custom security descriptor for HTTP.sys request queues. The new [RequestQueueSecurityDescriptor](https://source.dot.net/#Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs,a556950881fd2d87) property on [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions) enables more granular control over access rights for the request queue. This granular control lets you tailor security to your application's needs.

#### What you can do with the new property

A *request queue* in HTTP.sys is a kernel-level structure that temporarily stores incoming HTTP requests until your application is ready to process them. By customizing the security descriptor, you can allow or deny specific users or groups access to the request queue. This is useful in scenarios where you want to restrict or delegate HTTP.sys request handling at the operating system level.

#### How to use the new property

The `RequestQueueSecurityDescriptor` property applies only when creating a new request queue. The property doesn't affect existing request queues. To use this property, set it to a [GenericSecurityDescriptor](/en-us/dotnet/api/system.security.accesscontrol.genericsecuritydescriptor) instance when configuring your HTTP.sys server.

For example, the following code allows all authenticated users but denies guests:

```
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.AspNetCore.Server.HttpSys;

// Create a new security descriptor
var securityDescriptor = new CommonSecurityDescriptor(isContainer: false, isDS: false, sddlForm: string.Empty);

// Create a discretionary access control list (DACL)
var dacl = new DiscretionaryAcl(isContainer: false, isDS: false, capacity: 2);
dacl.AddAccess(
    AccessControlType.Allow,
    new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
    -1,
    InheritanceFlags.None,
    PropagationFlags.None
);
dacl.AddAccess(
    AccessControlType.Deny,
    new SecurityIdentifier(WellKnownSidType.BuiltinGuestsSid, null),
    -1,
    InheritanceFlags.None,
    PropagationFlags.None
);

// Assign the DACL to the security descriptor
securityDescriptor.DiscretionaryAcl = dacl;

// Configure HTTP.sys options
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseHttpSys(options =>
{
    options.RequestQueueSecurityDescriptor = securityDescriptor;
});
```

For more information, see [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

### Better support for testing apps with top-level statements

.NET 10 now has better support for testing apps that use [top-level statements](/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements). Previously developers had to manually add `public partial class Program` to the `Program.cs` file so that the test project could reference the `Program class`. `public partial class Program` was required because the top-level statement feature in C# 9 generated a `Program class` that was declared as [internal](/en-us/dotnet/csharp/language-reference/keywords/internal).

In .NET 10, a [source generator](/en-us/shows/on-dotnet/c-source-generators) is used to generate the `public partial class Program` declaration if the programmer didn't declare it explicitly. Additionally, an analyzer was added to detect when `public partial class Program` is declared explicitly and advise the developer to remove it.

The following PRs contribited to this feature:

* [PR 58199](https://github.com/dotnet/aspnetcore/pull/58199)
* [PR 58482](https://github.com/dotnet/aspnetcore/pull/58482)

### New JSON Patch implementation with `System.Text.Json`

**[JSON Patch](https://jsonpatch.com/)**:

* Is a standard format for describing changes to apply to a JSON document.
* Is defined in RFC 6902 and is widely used in RESTful APIs to perform partial updates to JSON resources.
* Represents a sequence of operations (for example, Add, Remove, Replace, Move, Copy, Test) that can be applied to modify a JSON document.

In web apps, JSON Patch is commonly used in a PATCH operation to perform partial updates of a resource. Rather than sending the entire resource for an update, clients can send a JSON Patch document containing only the changes. Patching reduces payload size and improves efficiency.

This release introduces a new implementation of [Microsoft.AspNetCore.JsonPatch](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch) based on [System.Text.Json](/en-us/dotnet/api/system.text.json) serialization. This feature:

* Aligns with modern .NET practices by leveraging the `System.Text.Json` library, which is optimized for .NET.
* Provides improved performance and reduced memory usage compared to the legacy `Newtonsoft.Json`-based implementation.

The following benchmarks compare the performance of the new `System.Text.Json` implementation with the legacy `Newtonsoft.Json` implementation.

| Scenario                       | Implementation        | Mean       | Allocated Memory |
| ------------------------------ | --------------------- | ---------- | ---------------- |
| **Application Benchmarks**     | Newtonsoft.JsonPatch  | 271.924 µs | 25 KB            |
|                                | System.Text.JsonPatch | 1.584 µs   | 3 KB             |
| **Deserialization Benchmarks** | Newtonsoft.JsonPatch  | 19.261 µs  | 43 KB            |
|                                | System.Text.JsonPatch | 7.917 µs   | 7 KB             |

These benchmarks highlight significant performance gains and reduced memory usage with the new implementation.

Notes:

* The new implementation isn't a drop-in replacement for the legacy implementation. In particular, the new implementation doesn't support dynamic types, for example, [ExpandoObject](/en-us/dotnet/api/system.dynamic.expandoobject).
* The JSON Patch standard has ***inherent security risks***. Since these risks are inherent to the JSON Patch standard, the new implementation ***doesn't attempt to mitigate inherent security risks***. It's the responsibility of the developer to ensure that the JSON Patch document is safe to apply to the target object. For more information, see the [Mitigating Security Risks](#mitigating-security-risks) section.

#### Usage

To enable JSON Patch support with `System.Text.Json`, install the [`Microsoft.AspNetCore.JsonPatch.SystemTextJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch.SystemTextJson) NuGet package.

```
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease
```

This package provides a `JsonPatchDocument<T>` class to represent a JSON Patch document for objects of type `T` and custom logic for serializing and deserializing JSON Patch documents using `System.Text.Json`. The key method of the `JsonPatchDocument<T>` class is `ApplyTo`, which applies the patch operations to a target object of type `T`.

The following examples demonstrate how to use the `ApplyTo` method to apply a JSON Patch document to an object.

#### Example: Applying a `JsonPatchDocument`

The following example demonstrates:

1. The `add`, `replace`, and `remove` operations.
2. Operations on nested properties.
3. Adding a new item to an array.
4. Using a JSON String Enum Converter in a JSON Patch document.

```
// Original object
var person = new Person {
  FirstName = "John",
  LastName = "Doe",
  Email = "johndoe@gmail.com",
  PhoneNumbers = [new() {Number = "123-456-7890", Type = PhoneNumberType.Mobile}],
  Address = new Address
  {
    Street = "123 Main St",
    City = "Anytown",
    State = "TX"
  }
};

// Raw JSON Patch document
var jsonPatch = """
[
  { "op": "replace", "path": "/FirstName", "value": "Jane" },
  { "op": "remove", "path": "/Email"},
  { "op": "add", "path": "/Address/ZipCode", "value": "90210" },
  {
    "op": "add",
    "path": "/PhoneNumbers/-",
    "value": { "Number": "987-654-3210", "Type": "Work" }
  }
]
""";

// Deserialize the JSON Patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);

// Apply the JSON Patch document
patchDoc!.ApplyTo(person);

// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));

// Output:
// {
//   "firstName": "Jane",
//   "lastName": "Doe",
//   "address": {
//     "street": "123 Main St",
//     "city": "Anytown",
//     "state": "TX",
//     "zipCode": "90210"
//   },
//   "phoneNumbers": [
//     {
//       "number": "123-456-7890",
//       "type": "Mobile"
//     },
//     {
//       "number": "987-654-3210",
//       "type": "Work"
//     }
//   ]
// }
```

The `ApplyTo` method generally follows the conventions and options of `System.Text.Json` for processing the `JsonPatchDocument`, including the behavior controlled by the following options:

* `NumberHandling`: Whether numeric properties are read from strings.
* `PropertyNameCaseInsensitive`: Whether property names are case-sensitive.

Key differences between `System.Text.Json` and the new `JsonPatchDocument<T>` implementation:

* The runtime type of the target object, not the declared type, determines which properties `ApplyTo` patches.
* `System.Text.Json` deserialization relies on the declared type to identify eligible properties.

#### Example: Applying a `JsonPatchDocument` with error handling

There are various errors that can occur when applying a JSON Patch document. For example, the target object may not have the specified property, or the value specified might be incompatible with the property type.

JSON Patch also supports the `test` operation. The `test` operation checks if a specified value is equal to the target property, and if not, returns an error.

The following example demonstrates how to handle these errors gracefully.

Important

The object passed to the `ApplyTo` method is modified in place. It is the caller's responsibility to discard these changes if any operation fails.

```
// Original object
var person = new Person {
  FirstName = "John",
  LastName = "Doe",
  Email = "johndoe@gmail.com"
};

// Raw JSON Patch document
var jsonPatch = """
[
  { "op": "replace", "path": "/Email", "value": "janedoe@gmail.com"},
  { "op": "test", "path": "/FirstName", "value": "Jane" },
  { "op": "replace", "path": "/LastName", "value": "Smith" }
]
""";

// Deserialize the JSON Patch document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);

// Apply the JSON Patch document, catching any errors
Dictionary<string, string[]>? errors = null;
patchDoc!.ApplyTo(person, jsonPatchError =>
    {
        errors ??= new ();
        var key = jsonPatchError.AffectedObject.GetType().Name;
        if (!errors.ContainsKey(key))
        {
            errors.Add(key, new string[] { });
        }
        errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
    });
if (errors != null)
{
    // Print the errors
    foreach (var error in errors)
    {
        Console.WriteLine($"Error in {error.Key}: {string.Join(", ", error.Value)}");
    }
}

// Output updated object
Console.WriteLine(JsonSerializer.Serialize(person, serializerOptions));

// Output:
// Error in Person: The current value 'John' at path 'FirstName' is not equal 
// to the test value 'Jane'.
// {
//   "firstName": "John",
//   "lastName": "Smith",              <<< Modified!
//   "email": "janedoe@gmail.com",     <<< Modified!
//   "phoneNumbers": []
// }
```

#### Mitigating security risks

When using the `Microsoft.AspNetCore.JsonPatch.SystemTextJson` package, it's critical to understand and mitigate potential security risks. The following sections outline the identified security risks associated with JSON Patch and provide recommended mitigations to ensure secure usage of the package.

Important

***This is not an exhaustive list of threats.*** App developers must conduct their own threat model reviews to determine an app-specific comprehensive list and come up with appropriate mitigations as needed. For example, apps which expose collections to patch operations should consider the potential for algorithmic complexity attacks if those operations insert or remove elements at the beginning of the collection.

By running comprehensive threat models for their own apps and addressing identified threats while following the recommended mitigations below, consumers of these packages can integrate JSON Patch functionality into their apps while minimizing security risks.

Consumers of these packages can integrate JSON Patch functionality into their apps while minimizing security risks, including:

* Run comprehensive threat models for their own apps.
* Address identified threats.
* Follow the recommended mitigations in the following sections.

##### Denial of Service (DoS) via memory amplification

* **Scenario**: A malicious client submits a `copy` operation that duplicates large object graphs multiple times, leading to excessive memory consumption.

* **Impact**: Potential Out-Of-Memory (OOM) conditions, causing service disruptions.

* **Mitigation**:

  * Validate incoming JSON Patch documents for size and structure before calling `ApplyTo`.
  * The validation must be app specific, but an example validation can look similar to the following:

```
public void Validate(JsonPatchDocument<T> patch)
{
    // This is just an example. It's up to the developer to make sure that
    // this case is handled properly, based on the app's requirements.
    if (patch.Operations.Where(op=>op.OperationType == OperationType.Copy).Count()
        > MaxCopyOperationsCount)
    {
        throw new InvalidOperationException();
    }
}
```

##### Business Logic Subversion

* **Scenario**: Patch operations can manipulate fields with implicit invariants, (for example, internal flags, IDs, or computed fields), violating business constraints.

* **Impact**: Data integrity issues and unintended app behavior.

* **Mitigation**:

  * Use POCO objects with explicitly defined properties that are safe to modify.
  * Avoid exposing sensitive or security-critical properties in the target object.
  * If no POCO object is used, validate the patched object after applying operations to ensure business rules and invariants aren't violated.

##### Authentication and authorization

* **Scenario**: Unauthenticated or unauthorized clients send malicious JSON Patch requests.

* **Impact**: Unauthorized access to modify sensitive data or disrupt app behavior.

* **Mitigation**:

  * Protect endpoints accepting JSON Patch requests with proper authentication and authorization mechanisms.
  * Restrict access to trusted clients or users with appropriate permissions.

### Detect if URL is local using `RedirectHttpResult.IsLocalUrl`

Use the new [`RedirectHttpResult.IsLocalUrl(url)`](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/RedirectHttpResult.cs,c0ece2e6266cb369) helper method to detect if a URL is local. A URL is considered local if the following are true:

* It doesn't have the [host](https://developer.mozilla.org/docs/Web/API/URL/host) or [authority](https://developer.mozilla.org/docs/Web/URI/Authority) section.
* It has an [absolute path](https://developer.mozilla.org/docs/Learn_web_development/Howto/Web_mechanics/What_is_a_URL#absolute_urls_vs._relative_urls).

URLs using [virtual paths](/en-us/previous-versions/aspnet/ms178116\(v=vs.100\)) `"~/"` are also local.

`IsLocalUrl` is useful for validating URLs before redirecting to them to prevent [open redirection attacks](https://brightsec.com/blog/open-redirect-vulnerabilities/).

```
if (RedirectHttpResult.IsLocalUrl(url))
{
    return Results.LocalRedirect(url);
}
```

Thank you [@martincostello](https://github.com/martincostello) for this contribution!

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/session
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/session.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate ASP.NET Framework Session to ASP.NET Core

Session state is a critical component of many web applications, storing user-specific data across HTTP requests. When migrating from ASP.NET Framework to ASP.NET Core, session state presents unique challenges because the two frameworks handle sessions very differently.

## Why session migration is complex

ASP.NET Framework and ASP.NET Core have fundamentally different approaches to session management:

* **ASP.NET Framework** provides automatic object serialization and built-in session locking
* **ASP.NET Core** requires manual serialization and offers no session locking guarantees

These differences mean you can't simply move your session code from Framework to Core without changes.

## Migration strategies overview

You have three main approaches for handling session state during migration:

1. **Complete rewrite** - Rewrite all session code to use ASP.NET Core's native session implementation
2. **Incremental with separate sessions** - Migrate components piece by piece, with each app maintaining its own session state
3. **Incremental with shared sessions** - Migrate components while sharing session data between Framework and Core applications

For most applications, migrating to [ASP.NET Core session](../../../fundamentals/app-state?view=aspnetcore-10.0) provides the best performance and maintainability. However, larger applications or those with complex session requirements may benefit from an incremental approach using the System.Web adapters.

## Choose your migration approach

You have three main options for migrating session state from ASP.NET Framework to ASP.NET Core. Your choice depends on your migration timeline, whether you need to run both applications simultaneously, and how much code you're willing to rewrite.

### Quick decision guide

**Answer these questions to choose your approach:**

1. **Are you doing a complete rewrite or incremental migration?**

   * Complete rewrite → [Built-in ASP.NET Core session](#built-in-aspnet-core-session-state)
   * Incremental migration → Continue to question 2

2. **Do both your ASP.NET Framework and ASP.NET Core apps need to access the same session data?**

   * Yes, shared session needed → [Remote app session](#remote-app-session-state)
   * No, separate sessions are fine → [Wrapped ASP.NET Core session](#wrapped-aspnet-core-session-state)

### Understanding the differences

Before diving into implementation details, it's important to understand how ASP.NET Framework and ASP.NET Core handle session state differently:

* **Object serialization**

  * ASP.NET Framework automatically serializes and deserializes objects (unless using in-memory storage)
  * ASP.NET Core requires manual serialization/deserialization and stores data as `byte[]`

* **Session locking**

  * ASP.NET Framework locks session usage within a session, handling subsequent requests serially
  * ASP.NET Core provides no session locking guarantees

### Migration approaches comparison

| Approach                                                         | Code Changes                         | Performance | Session Sharing | When to Use                                    |
| ---------------------------------------------------------------- | ------------------------------------ | ----------- | --------------- | ---------------------------------------------- |
| **[Built-in ASP.NET Core](#built-in-aspnet-core-session-state)** | High - Rewrite all session code      | Best        | None            | Complete rewrites, performance-critical apps   |
| **[Wrapped ASP.NET Core](#wrapped-aspnet-core-session-state)**   | Low - Keep existing session patterns | Good        | None            | Incremental migrations, no shared state needed |
| **[Remote app](#remote-app-session-state)**                      | Low - Keep existing session patterns | Fair        | Full            | Running both apps simultaneously               |

The [System.Web adapters](../inc/systemweb-adapters?view=aspnetcore-10.0) enable the "Wrapped" and "Remote app" approaches by bridging the differences between ASP.NET Framework and Core session implementations through two key interfaces:

* `Microsoft.AspNetCore.SystemWebAdapters.ISessionManager`: Accepts an [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) and session metadata, returns an `ISessionState` object
* `Microsoft.AspNetCore.SystemWebAdapters.ISessionState`: Describes session object state and backs the [HttpSessionState](/en-us/dotnet/api/system.web.sessionstate.httpsessionstate) type

## Built-in ASP.NET Core session state

Choose this approach when you're performing a complete migration and can rewrite session-related code to use ASP.NET Core's native session implementation.

ASP.NET Core provides a lightweight, high-performance session state implementation that stores data as `byte[]` and requires explicit serialization. This approach offers the best performance but requires more code changes during migration.

For details on how to set this up and use it, see the \[ASP.NET session documentation]\((xref:fundamentals/app-state.md).

### Pros and cons

| Pros                                         | Cons                                                  |
| -------------------------------------------- | ----------------------------------------------------- |
| Best performance and lowest memory footprint | Requires rewriting all session access code            |
| Native ASP.NET Core implementation           | No automatic object serialization                     |
| Full control over serialization strategy     | No session locking (concurrent requests may conflict) |
| No additional dependencies                   | Breaking change from ASP.NET Framework patterns       |
| Supports all ASP.NET Core session providers  | Session keys are case-sensitive (unlike Framework)    |

### Migration considerations

When migrating to built-in ASP.NET Core session:

**Code changes required:**

* Replace `Session["key"]` with `HttpContext.Session.GetString("key")`
* Replace `Session["key"] = value` with `HttpContext.Session.SetString("key", value)`
* Add explicit serialization/deserialization for complex objects
* Handle null values explicitly (no automatic type conversion)

**Data migration:**

* Session data structure changes require careful planning
* Consider running both systems in parallel during migration
* Implement session data import/export utilities if needed

**Testing strategy:**

* Unit test session serialization/deserialization logic
* Integration test session behavior across requests
* Load test concurrent session access patterns

**When to choose this approach:**

* You can afford to rewrite session-related code
* Performance is a top priority
* You're not sharing session data with legacy applications
* You want to eliminate System.Web dependencies completely

## System.Web Adapter Session

Note

This makes use of the [System.Web Adapters](../inc/systemweb-adapters?view=aspnetcore-10.0) to simplify migration.

### Serialization configuration

The [HttpSessionState](/en-us/dotnet/api/system.web.sessionstate.httpsessionstate) object requires serialization for remote app session state.

In ASP.NET Framework, [BinaryFormatter](/en-us/dotnet/api/system.runtime.serialization.formatters.binary.binaryformatter) was used to automatically serialize session value contents. In order to serialize these with for use with the System.Web adapters, the serialization must be explicitly configured using `ISessionKeySerializer` implementations.

Out of the box, there is a simple JSON serializer that allows each session key to be registered to a known type using `JsonSessionSerializerOptions`:

* `RegisterKey<T>(string)` - Registers a session key to a known type. This registration is required for correct serialization/deserialization. Missing registrations cause errors and prevent session access.

```
builder.Services.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        // Serialization/deserialization requires each session key to be registered to a type
        options.RegisterKey<int>("test-value");
    });
```

If more customization is needed, then `ISessionKeySerializer` can be implemented:

```
builder.Services.AddSystemWebAdapters()
    .AddSessionSerializer();

builder.Services.AddSingleton<ISessionKeySerializer, CustomSessionKeySerializer>();
```

```
sealed class CustomSessionKeySerializer : ISessionKeySerializer
{
    public bool TryDeserialize(string key, byte[] bytes, out object? obj)
    {
        // Custom deserialization logic
    }

    public bool TrySerialize(string key, object? value, out byte[] bytes)
    {
        // Custom serialization logic
    }
}
```

Note

When using the `AddJsonSessionSerializer` registration pattern, there is no need to call `AddSessionSerializer` as it will automatically be added. If you only want to use a customimplementation, then you must manually add it.

### Enable session

Session support requires explicit activation. Configure it per-route or globally using ASP.NET Core metadata:

* Annotate controllers

```
[Session]
public class SomeController : Controller
{
}
```

* Enable globally for all endpoints

```
app.MapDefaultControllerRoute()
    .RequireSystemWebAdapterSession();
```

## Wrapped ASP.NET Core session state

Choose this approach when your migrated components don't need to share session data with your legacy application.

The `Microsoft.Extensions.DependencyInjection.WrappedSessionExtensions.AddWrappedAspNetCoreSession` extension method adds a wrapped ASP.NET Core session to work with the adapters. It uses the same backing store as [ISession](/en-us/dotnet/api/microsoft.aspnetcore.http.isession) while providing strongly-typed access.

**Configuration for ASP.NET Core:**

```
builder.Services.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        // Serialization/deserialization requires each session key to be registered to a type
        options.RegisterKey<int>("test-value");
        options.RegisterKey<SessionDemoModel>("SampleSessionItem");
    })
    .AddWrappedAspNetCoreSession();
```

Your Framework application requires no changes.

For more information, see the [wrapped session state sample app](https://github.com/dotnet/systemweb-adapters/blob/main/samples/SessionLocal/SessionLocalCore/Program.cs)

## Remote app session state

Note

This makes use of the [System.Web Adapters](../inc/systemweb-adapters?view=aspnetcore-10.0) to simplify migration.

Choose this approach when you need to share session state between your ASP.NET Framework and ASP.NET Core applications during incremental migration.

Remote app session enables communication between applications to retrieve and set session state by exposing an endpoint on the ASP.NET Framework app.

### Prerequisites

Complete the [remote app setup](../inc/remote-app-setup?view=aspnetcore-10.0) instructions to connect your ASP.NET Core and ASP.NET Framework applications.

### Application configuration

**ASP.NET Core configuration:**

Call `AddRemoteAppSession` and `AddJsonSessionSerializer` to register known session item types:

```
builder.Services.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        // Serialization/deserialization requires each session key to be registered to a type
        options.RegisterKey<int>("test-value");
        options.RegisterKey<SessionDemoModel>("SampleSessionItem");
    })
    .AddRemoteAppClient(options =>
    {
        // Provide the URL for the remote app that has enabled session querying
        options.RemoteAppUrl = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);

        // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session
        options.ApiKey = builder.Configuration["RemoteAppApiKey"];
    })
    .AddSessionClient();
```

**ASP.NET Framework configuration:**

Add this change to `Global.asax.cs`:

```
public class Global : HttpApplication
{
        protected void Application_Start()
        {
            HttpApplicationHost.RegisterHost(builder =>
            {
                builder.AddSystemWebAdapters()
                    .AddJsonSessionSerializer(options =>
                    {
                        // Serialization/deserialization requires each session key to be registered to a type
                        options.RegisterKey<int>("test-value");
                        options.RegisterKey<SessionDemoModel>("SampleSessionItem");
                    })
                    // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session
                    // ApiKey is a string representing a GUID
                    .AddRemoteAppServer(options => options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"])
                    .AddSessionServer();
            });
        }
}
```

When using Aspire, the configuration will be done via environment variables and are set by the AppHost. To enable remote session, the option must be enabled:

```
...

var coreApp = builder.AddProject<Projects.CoreApplication>("core")
    .WithHttpHealthCheck()
    .WaitFor(frameworkApp)
    .WithIncrementalMigrationFallback(frameworkApp, options => options.RemoteSession = RemoteSession.Enabled);

...
```

### Communication protocol

#### Readonly sessions

Readonly sessions retrieve session state without locking. The process uses a single `GET` request that returns session state and closes immediately.

#### Writeable sessions

Writeable sessions require additional steps:

* Start with the same `GET` request as readonly sessions
* Keep the initial `GET` request open until the session completes
* Use an additional `PUT` request to update state
* Close the initial request only after updating is complete

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-23

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/cacheview-component
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/state-management/cacheview-component.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor `CacheView` component

The `CacheView` component caches the rendered output of a Razor component subtree during static server-side rendering (static SSR). On a cache hit, cached markup is replayed without instantiating or running the lifecycle of the child components that were included in the cached output.

`CacheView` is useful for expensive, mostly static sections of a page that don't require the entire response to be cached.

```
<CacheView VaryByQuery="category" ExpiresAfter="TimeSpan.FromMinutes(5)">
    <ProductList Category="@Category" />
</CacheView>
```

Caching is enabled by default. Set `Enabled="false"` to render the content normally without reading or writing a cache entry.

## Cache keys and vary-by values

Each `CacheView` has a key based on its position in the component tree. Set `CacheKey` when the same component containing a `CacheView` is rendered multiple times, such as in a loop, so that each instance has a distinct entry.

For example, every `CacheView` produced by this loop has the same position in the render tree:

```
@for (var i = 0; i < 3; i++)
{
    <CacheView ExpiresAfter="TimeSpan.FromHours(1)"
               VaryByQuery="testId">
        <p>@Guid.NewGuid()</p>
    </CacheView>
}
```

The instances therefore resolve to the same key during the request, and `CacheView` throws an `InvalidOperationException`. Assign a unique `CacheKey` to each iteration, as demonstrated by the CacheView E2E tests:

```
@for (var i = 0; i < 3; i++)
{
    <CacheView CacheKey="@($"loop-{i}")"
               ExpiresAfter="TimeSpan.FromHours(1)"
               VaryByQuery="testId">
        <p class="cached-value">@Guid.NewGuid()</p>
    </CacheView>
}
```

Each iteration now creates and reuses an independent cache entry.

The default tree-position key is computed from the immediate parent component type and sequence number, not the full ancestor chain. Multiple `CacheView` instances with the same immediate parent component, such as `<ErrorBoundary>` or a custom wrapper component, and the same position in its render tree produce the same tree-position key. Unless `VaryBy*` parameters, such as `VaryByRoute`, already differentiate the calculated cache key, the instances resolve to the same cache key:

* Within the same request (on the same page), `CacheView` throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) (`Multiple CacheView components resolved to the same cache key`).
* Across different pages or requests, no exception is thrown, and the instances silently share the same cache entry.

When the calculated keys would otherwise be the same, set `CacheKey` to distinguish between them.

Page 1:

```
<ErrorBoundary>
    <CacheView>
        <p>Component One</p>
    </CacheView>
</ErrorBoundary>
```

Page 2:

```
<ErrorBoundary>
    <CacheView CacheKey="SecondComponent">
        <p>Component Two</p>
    </CacheView>
</ErrorBoundary>
```

The following parameters add request-specific values to the cache key:

| Parameter       | Cache varies by                                                                      |
| --------------- | ------------------------------------------------------------------------------------ |
| `VaryByQuery`   | A comma-separated list of query parameter names. Use `"*"` for all query parameters. |
| `VaryByRoute`   | A comma-separated list of route parameter names.                                     |
| `VaryByHeader`  | A comma-separated list of HTTP header names.                                         |
| `VaryByCookie`  | A comma-separated list of cookie names.                                              |
| `VaryByUser`    | The authenticated user identity.                                                     |
| `VaryByCulture` | The current culture and UI culture.                                                  |
| `VaryBy`        | An application-defined string value.                                                 |

`VaryByUser="true"` separates authenticated identities, but it doesn't automatically vary by every claim or permission. When a nonempty name identifier claim is present, other claims aren't included in the user portion of the key. If cached content depends on claims or permissions, also set `VaryBy` to an application-defined string that reflects all relevant values and changes when they do:

```
<CacheView VaryByUser="true" VaryBy="@currentPermission">
    <PermissionSensitiveContent />
</CacheView>
```

Here, `currentPermission` is the current permission value used by the child content. Authorization logic inside cached child content isn't rerun on a cache hit. Move content that must recheck authorization on every request outside the `CacheView`.

Without a matching vary-by parameter, requests with different values share the same cached output.

If a `<form>` or [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) without an explicit `action` attribute is rendered inside a `CacheView`, its [automatically generated `action` URL](../forms/?view=aspnetcore-12.0#handle-form-submission), including the query string, is cached with the markup. Configure `VaryByQuery` on the `CacheView` so that requests with different query strings don't share a cached form `action`.

## Expiration and cache storage

`CacheView` supports three expiration parameters:

* `ExpiresAfter` sets an absolute lifetime relative to entry creation.
* `ExpiresOn` sets an absolute expiration date and time.
* `ExpiresSliding` expires an entry after a period without access.

When no expiration is specified, entries expire after 30 seconds.

### Sliding expiration is bounded by an absolute expiration

`ExpiresSliding` doesn't keep an entry alive indefinitely. Every entry also carries an absolute expiration in the following order:

1. `ExpiresOn`, if set.
2. `ExpiresAfter`, if set.
3. The 30-second default.

An entry expires when either the sliding window elapses without access or the absolute expiration is reached, whichever comes first.

For example, consider `ExpiresSliding` set to 10 seconds with `ExpiresAfter` set to two minutes. Requests spaced less than 10 seconds apart reuse the cached output, and each access restarts the sliding window. A gap longer than 10 seconds expires the entry, so the next request creates a new entry. No matter how often the entry is accessed, it's never served more than two minutes after it's created.

Now consider `ExpiresSliding` set to 10 seconds without `ExpiresAfter` or `ExpiresOn`. The 30-second default absolute expiration applies, so repeated access within the 10-second window only keeps the entry alive until it's 30 seconds old. A gap longer than 10 seconds expires the entry earlier.

When you measure this behavior, report the configured expiration options together with the observed expiry because the sliding window alone doesn't determine when an entry is evicted.

The default in-memory store has a 100 MB cache size limit. Configure the limit with `RazorComponentsServiceOptions.CacheViewSizeLimit`. A value of `0` prevents entries from being cached. When the limit is reached, no new entries are cached until existing entries expire. The content still renders, but it isn't saved in the cache.

```
builder.Services.AddRazorComponents(options =>
{
    options.CacheViewSizeLimit = 50 * 1024 * 1024;
});
```

If a `HybridCache` service is registered in dependency injection, `CacheView` uses it automatically. For example, register it in the `Program` file:

```
builder.Services.AddHybridCache();
```

To use a specific `HybridCache` instance for `CacheView` without registering it in dependency injection, create the instance and assign it to `RazorComponentsServiceOptions.CacheViewHybridCache`. `CacheViewSizeLimit` applies only to the default in-memory store; a configured `HybridCache` uses its own backing-store limits and eviction behavior. Sliding expiration isn't supported with `HybridCache`; use `ExpiresAfter` or `ExpiresOn`.

Concurrent requests for the same key are coalesced so that only one request creates the cache entry.

## Declare component cache compatibility

Some components contain per-request content that must not be baked into shared cached markup. Component authors can apply `CacheBehaviorAttribute` and `CacheConditionAttribute` to control how their component behaves inside a `CacheView`.

The following table describes how the attributes work together.

| Attributes                                                             | Condition isn't satisfied                                                                        | Condition is satisfied                      |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------- |
| No attributes                                                          | The component is included in cached output.                                                      | Not applicable.                             |
| `[CacheBehavior(CacheBehavior.Rerender)]`                              | The component renders live on every request.                                                     | Not applicable.                             |
| `[CacheBehavior(CacheBehavior.Throw)]`                                 | The component throws an `InvalidOperationException`.                                             | Not applicable.                             |
| `[CacheCondition(...)]`                                                | The component renders live on every request using the default `CacheBehavior.Rerender` behavior. | The component is included in cached output. |
| `[CacheBehavior(CacheBehavior.Rerender)]` with `[CacheCondition(...)]` | The component renders live on every request.                                                     | The component is included in cached output. |
| `[CacheBehavior(CacheBehavior.Throw)]` with `[CacheCondition(...)]`    | The component throws an `InvalidOperationException`.                                             | The component is included in cached output. |

```
[CacheBehavior(CacheBehavior.Rerender)]
public sealed class CurrentRequestTime : ComponentBase
{
}
```

`CacheBehavior.Rerender` keeps the component live: its lifecycle runs on every request while the surrounding markup is served from the cache. The component's parameters are captured when the cache entry is created and replayed unchanged on cache hits.

`CacheBehavior.Throw` rejects use inside a `CacheView` unless a matching `CacheConditionAttribute` is satisfied.

Component authors can use `CacheBehavior.Throw` for components that are never safe to cache:

```
@attribute [CacheBehavior(CacheBehavior.Throw)]

<span class="user-badge">User: @UserName</span>

@code {
    [Parameter]
    public string? UserName { get; set; }
}
```

Adding vary-by parameters to the enclosing `CacheView` doesn't make this component cacheable because the component doesn't declare a cache condition. Move the component outside the cache boundary.

Combine `CacheBehavior.Throw` with `CacheConditionAttribute` for components that are cacheable only when the enclosing `CacheView` varies by specific request dimensions:

```
[CacheBehavior(CacheBehavior.Throw)]
[CacheCondition(CacheVaryBy.User)]
public sealed class UserSpecificComponent : ComponentBase
{
}
```

In this example, the component can be included in cached output only when the enclosing `CacheView` sets `VaryByUser="true"`. Otherwise, rendering throws an `InvalidOperationException`.

Combine `CacheBehavior.Rerender` with `CacheConditionAttribute` when a component can render live if a required vary-by dimension isn't active and can be included in cached output when the dimension is active:

```
@using Microsoft.AspNetCore.Http
@attribute [CacheBehavior(CacheBehavior.Rerender)]
@attribute [CacheCondition(CacheVaryBy.Cookie)]

<p>Price selection: @PriceSelection</p>

@code {
    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    private string PriceSelection =>
        HttpContext?.Request.Cookies["price-selection"] ?? "standard";
}
```

Without `VaryByCookie`, this component runs on every request. The following cache boundary satisfies the condition, so the component is included in cached output:

```
<CacheView VaryByCookie="price-selection">
    <PricePanel />
</CacheView>
```

`CacheConditionAttribute` checks vary-by dimensions, not individual query parameter, route parameter, header, or cookie names. For example, `[CacheCondition(CacheVaryBy.Cookie)]` is satisfied when `VaryByCookie` contains any value. It doesn't verify that `VaryByCookie` contains the correct cookie names. A component author must document every name that affects the component's output, and the consumer must include those exact names in the corresponding `CacheView` parameter. In the preceding example, specifying a cookie other than `price-selection` satisfies the declared condition but results in an unsafe cache key.

`CacheVaryBy` is a flags enum in which each value represents a request dimension. Combine dimensions with the logical OR operator (`|`):

```
[CacheCondition(CacheVaryBy.User | CacheVaryBy.Query)]
```

In this example, both user variation and query string variation must be active to satisfy the condition.

You can only apply one `CacheConditionAttribute` to a component. Conditions don't have an evaluation order.

Built-in components use these policies:

| Component                                                                                    | Behavior inside `CacheView`                                             |
| -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `AuthorizeView`                                                                              | Requires `VaryByUser="true"` or throws.                                 |
| `QuickGrid`                                                                                  | Requires `VaryByQuery` or throws.                                       |
| `Virtualize`                                                                                 | Always throws.                                                          |
| Antiforgery tokens, `HeadOutlet`, interactive render mode boundaries, and streaming children | Render fresh on every request while surrounding content remains cached. |

When `CacheBehavior.Throw` rejects a component, the exception identifies the component and either lists the required vary-by dimensions or directs the developer to move the component outside the `CacheView`.

## Limitations

### Request and streaming rendering restrictions

`CacheView` only caches static SSR output for `GET` requests. Caching is skipped for other HTTP methods or interactive renders.

A `CacheView` rendered inside a streaming rendering subtree also isn't cached. However, a streaming child inside a `CacheView` is supported: the streaming child renders fresh on each request while the surrounding content is cached.

### Nested cache views

A `CacheView` can't be nested inside another `CacheView`. The inner output would become part of the outer cache entry, which could freeze per-request content such as antiforgery tokens, authentication-dependent output, or interactive component markers.

Move the inner `CacheView` outside the outer cached subtree.

### Live component parameters are captured once

Components marked with `CacheBehavior.Rerender` run their lifecycle on every request, but their parameter values are captured when the cache entry is created and replayed unchanged on cache hits.

For example, this live component receives the current user's name as a parameter:

```
@attribute [CacheBehavior(CacheBehavior.Rerender)]

<p>Welcome, @UserName!</p>

@code {
    [Parameter]
    public string? UserName { get; set; }
}
```

The following usage is unsafe:

```
<CacheView>
    <UserGreeting UserName="@CurrentUserName" />
</CacheView>

@code {
    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    private string? CurrentUserName => HttpContext?.User.Identity?.Name;
}
```

If Alice creates the cache entry, `"Alice"` is captured as the `UserName` parameter. When Bob requests the page, `UserGreeting` runs its lifecycle again, but it receives the captured `"Alice"` value. Vary the cache by user so that each identity has a separate entry:

```
<CacheView VaryByUser="true">
    <UserGreeting UserName="@CurrentUserName" />
</CacheView>
```

Alternatively, move `UserGreeting` outside the `CacheView`.

### Live components can't have render fragment parameters

A live component can't have a `RenderFragment` or `RenderFragment<T>` parameter, including `ChildContent`. The parameter would capture content and references from the request that created the cache entry and couldn't be safely replayed on later requests.

For example, the following live component exposes a `ChildContent` parameter:

```
@attribute [CacheBehavior(CacheBehavior.Rerender)]

<div class="current-request-panel">
    @ChildContent
</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

Using the component inside a `CacheView` isn't supported:

```
<CacheView>
    <CurrentRequestPanel>
        <p>Content for @DateTimeOffset.Now</p>
    </CurrentRequestPanel>
</CacheView>
```

`CacheView` throws an `InvalidOperationException` instead of creating the cache entry. Remove the `ChildContent` parameter, move `CurrentRequestPanel` outside the `CacheView`, or introduce a live wrapper without render fragment parameters.

For example, the wrapper can render `CurrentRequestPanel` and its child content internally:

```
@attribute [CacheBehavior(CacheBehavior.Rerender)]

<CurrentRequestPanel>
    <p>Content for @DateTimeOffset.Now</p>
</CurrentRequestPanel>
```

The wrapper itself has no `ChildContent` or other `RenderFragment` parameter, so it can be used as the live component inside the cache:

```
<CacheView>
    <CurrentRequestPanelWrapper />
</CacheView>
```

On a cache hit, `CurrentRequestPanelWrapper` and its subtree render fresh. The wrapper provides a component boundary between the cached content and the component that receives `ChildContent`.

### Sliding expiration with HybridCache

`ExpiresSliding` isn't supported when the backing store uses `HybridCache`. Use `ExpiresAfter` or `ExpiresOn` for absolute expiration.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/adding-view
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/adding-view.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 3, add a view to an ASP.NET Core MVC app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

In this section, you modify the `HelloWorldController` class to use [Razor](../../mvc/views/razor?view=aspnetcore-10.0) view files. This cleanly encapsulates the process of generating HTML responses to a client.

View templates are created using Razor. Razor-based view templates:

* Have a `.cshtml` file extension.
* Provide an elegant way to create HTML output with C#.

Currently the `Index` method returns a string with a message in the controller class. In the `HelloWorldController` class, replace the `Index` method with the following code:

```
public IActionResult Index()
{
    return View();
}
```

The preceding code:

* Calls the controller's [View](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.view) method.
* Uses a view template to generate an HTML response.

Controller methods:

* Are referred to as *action methods*. For example, the `Index` action method in the preceding code.
* Generally return an [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or a class derived from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult), not a type like `string`.

## Add a view

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Right-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Right-click on the *Views/HelloWorld* folder, and then **Add > New Item**.

In the **Add New Item** dialog, if the option to **Show All Templates** is available, select it.

In the **Add New Item - MvcMovie** dialog:

* In the search box in the upper-right, enter *view*
* Select **Razor View - Empty**
* Keep the **Name** box value, `Index.cshtml`.
* Select **Add**

Add an `Index` view for the `HelloWorldController`:

* Add a new folder named *Views/HelloWorld*.
* Add a new file to the *Views/HelloWorld* folder, and name it `Index.cshtml`.

Replace the contents of the `Views/HelloWorld/Index.cshtml` Razor view file with the following:

```
@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>
```

Navigate to `https://localhost:{PORT}/HelloWorld`:

* The `Index` method in the `HelloWorldController` ran the statement `return View();`, which specified that the method should use a view template file to render a response to the browser.

* A view template file name wasn't specified, so MVC defaulted to using the default view file. When the view file name isn't specified, the default view is returned. The default view has the same name as the action method, `Index` in this example. The view template `/Views/HelloWorld/Index.cshtml` is used.

* The following image shows the string "Hello from our View Template!" hard-coded in the view:

## Change views and layout pages

Select the menu links **MvcMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Views/Shared/_Layout.cshtml` file.

Open the `Views/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow:

* Specifying the HTML container layout of a site in one place.
* Applying the HTML container layout across multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the view-specific pages you create show up, *wrapped* in the layout page. For example, if you select the **Privacy** link, the *`Views/Home/Privacy.cshtml`* view is rendered inside the `RenderBody` method.

## Change the title, footer, and menu link in the layout file

Replace the content of the `Views/Shared/_Layout.cshtml` file with the following markup. The changes are highlighted:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Movie App</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/MvcMovie.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="HelloWorld" asp-action="Index">Movie App</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2025 - Movie App - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

The preceding markup made the following changes:

* Three occurrences of `MvcMovie` to `Movie App`.
* The anchor element `<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a>` to `<a class="navbar-brand" asp-area="" asp-controller="HelloWorld" asp-action="Index">Movie App</a>`.

In the preceding markup, the `asp-area=""` [anchor Tag Helper attribute](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) and attribute value was omitted because this app isn't using [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0).

Save the changes and select the **Privacy** link. Notice how the title on the browser tab displays **Privacy Policy - Movie App** instead of **Privacy Policy - MvcMovie**

Select the **Home** link.

Notice that the title and anchor text display **Movie App**. The changes were made once in the layout template and all pages on the site reflect the new link text and new title.

Examine the `Views/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The `Views/_ViewStart.cshtml` file brings in the `Views/Shared/_Layout.cshtml` file to each view. The `Layout` property can be used to set a different layout view, or set it to `null` so no layout file will be used.

Open the `Views/HelloWorld/Index.cshtml` view file.

Change the title and `<h2>` element as highlighted in the following:

```
@{
    ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>
```

The title and `<h2>` element are slightly different so it's clear which part of the code changes the display.

`ViewData["Title"] = "Movie List";` in the code above sets the `Title` property of the `ViewData` dictionary to "Movie List". The `Title` property is used in the `<title>` HTML element in the layout page:

```
<title>@ViewData["Title"] - Movie App</title>
```

Save the change and navigate to `https://localhost:{PORT}/HelloWorld`.

Notice that the following have changed:

* Browser title.
* Primary heading.
* Secondary headings.

If there are no changes in the browser, it could be cached content that is being viewed. Press Ctrl+F5 in the browser to force the response from the server to be loaded. The browser title is created with `ViewData["Title"]` we set in the `Index.cshtml` view template and the additional "- Movie App" added in the layout file.

The content in the `Index.cshtml` view template is merged with the `Views/Shared/_Layout.cshtml` view template. A single HTML response is sent to the browser. Layout templates make it easy to make changes that apply across all of the pages in an app. To learn more, see [Layout](../../mvc/views/layout?view=aspnetcore-10.0).

The small bit of "data", the "Hello from our View Template!" message, is hard-coded however. The MVC application has a "V" (view), a "C" (controller), but no "M" (model) yet.

## Passing Data from the Controller to the View

Controller actions are invoked in response to an incoming URL request. A controller class is where the code is written that handles the incoming browser requests. The controller retrieves data from a data source and decides what type of response to send back to the browser. View templates can be used from a controller to generate and format an HTML response to the browser.

Controllers are responsible for providing the data required in order for a view template to render a response.

View templates should **not**:

* Do business logic
* Interact with a database directly.

A view template should work only with the data that's provided to it by the controller. Maintaining this "separation of concerns" helps keep the code:

* Clean.
* Testable.
* Maintainable.

Currently, the `Welcome` method in the `HelloWorldController` class takes a `name` and an `ID` parameter and then outputs the values directly to the browser.

Rather than have the controller render this response as a string, change the controller to use a view template instead. The view template generates a dynamic response, which means that appropriate data must be passed from the controller to the view to generate the response. Do this by having the controller put the dynamic data (parameters) that the view template needs in a `ViewData` dictionary. The view template can then access the dynamic data.

In `HelloWorldController.cs`, change the `Welcome` method to add a `Message` and `NumTimes` value to the `ViewData` dictionary.

The `ViewData` dictionary is a dynamic object, which means any type can be used. The `ViewData` object has no defined properties until something is added. The [MVC model binding system](../../mvc/models/model-binding?view=aspnetcore-10.0) automatically maps the named parameters `name` and `numTimes` from the query string to parameters in the method. The complete `HelloWorldController`:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
    public IActionResult Welcome(string name, int numTimes = 1)
    {
        ViewData["Message"] = "Hello " + name;
        ViewData["NumTimes"] = numTimes;
        return View();
    }
}
```

The `ViewData` dictionary object contains data that will be passed to the view.

Create a Welcome view template named `Views/HelloWorld/Welcome.cshtml`.

You'll create a loop in the `Welcome.cshtml` view template that displays "Hello" `NumTimes`. Replace the contents of `Views/HelloWorld/Welcome.cshtml` with the following:

```
@{
    ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
    @for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
    {
        <li>@ViewData["Message"]</li>
    }
</ul>
```

Save your changes and browse to the following URL:

`https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`

Data is taken from the URL and passed to the controller using the [MVC model binder](../../mvc/models/model-binding?view=aspnetcore-10.0). The controller packages the data into a `ViewData` dictionary and passes that object to the view. The view then renders the data as HTML to the browser.

In the preceding sample, the `ViewData` dictionary was used to pass data from the controller to a view. Later in the tutorial, a view model is used to pass data from a controller to a view. The view model approach to passing data is preferred over the `ViewData` dictionary approach.

In the next tutorial, a database of movies is created.

[Previous: Add a Controller](adding-controller?view=aspnetcore-10.0) [Next: Add a Model](adding-model?view=aspnetcore-10.0)

In this section, you modify the `HelloWorldController` class to use [Razor](../../mvc/views/razor?view=aspnetcore-10.0) view files. This cleanly encapsulates the process of generating HTML responses to a client.

View templates are created using Razor. Razor-based view templates:

* Have a `.cshtml` file extension.
* Provide an elegant way to create HTML output with C#.

Currently the `Index` method returns a string with a message in the controller class. In the `HelloWorldController` class, replace the `Index` method with the following code:

```
public IActionResult Index()
{
    return View();
}
```

The preceding code:

* Calls the controller's [View](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.view) method.
* Uses a view template to generate an HTML response.

Controller methods:

* Are referred to as *action methods*. For example, the `Index` action method in the preceding code.
* Generally return an [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or a class derived from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult), not a type like `string`.

## Add a view

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Right-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Right-click on the *Views/HelloWorld* folder, and then **Add > New Item**.

In the **Add New Item** dialog, if the option to **Show All Templates** is available, select it.

In the **Add New Item - MvcMovie** dialog:

* In the search box in the upper-right, enter *view*
* Select **Razor View - Empty**
* Keep the **Name** box value, `Index.cshtml`.
* Select **Add**

Add an `Index` view for the `HelloWorldController`:

* Add a new folder named *Views/HelloWorld*.
* Add a new file to the *Views/HelloWorld* folder, and name it `Index.cshtml`.

Replace the contents of the `Views/HelloWorld/Index.cshtml` Razor view file with the following:

```
@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>
```

Navigate to `https://localhost:{PORT}/HelloWorld`:

* The `Index` method in the `HelloWorldController` ran the statement `return View();`, which specified that the method should use a view template file to render a response to the browser.

* A view template file name wasn't specified, so MVC defaulted to using the default view file. When the view file name isn't specified, the default view is returned. The default view has the same name as the action method, `Index` in this example. The view template `/Views/HelloWorld/Index.cshtml` is used.

* The following image shows the string "Hello from our View Template!" hard-coded in the view:

## Change views and layout pages

Select the menu links **MvcMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Views/Shared/_Layout.cshtml` file.

Open the `Views/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow:

* Specifying the HTML container layout of a site in one place.
* Applying the HTML container layout across multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the view-specific pages you create show up, *wrapped* in the layout page. For example, if you select the **Privacy** link, the *`Views/Home/Privacy.cshtml`* view is rendered inside the `RenderBody` method.

## Change the title, footer, and menu link in the layout file

Replace the content of the `Views/Shared/_Layout.cshtml` file with the following markup. The changes are highlighted:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Movie App</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/MvcMovie.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="HelloWorld" asp-action="Index">Movie App</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2025 - Movie App - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

The preceding markup made the following changes:

* Three occurrences of `MvcMovie` to `Movie App`.
* The anchor element `<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a>` to `<a class="navbar-brand" asp-area="" asp-controller="HelloWorld" asp-action="Index">Movie App</a>`.

In the preceding markup, the `asp-area=""` [anchor Tag Helper attribute](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) and attribute value was omitted because this app isn't using [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0).

Save the changes and select the **Privacy** link. Notice how the title on the browser tab displays **Privacy Policy - Movie App** instead of **Privacy Policy - MvcMovie**

Select the **Home** link.

Notice that the title and anchor text display **Movie App**. The changes were made once in the layout template and all pages on the site reflect the new link text and new title.

Examine the `Views/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The `Views/_ViewStart.cshtml` file brings in the `Views/Shared/_Layout.cshtml` file to each view. The `Layout` property can be used to set a different layout view, or set it to `null` so no layout file will be used.

Open the `Views/HelloWorld/Index.cshtml` view file.

Change the title and `<h2>` element as highlighted in the following:

```
@{
    ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>
```

The title and `<h2>` element are slightly different so it's clear which part of the code changes the display.

`ViewData["Title"] = "Movie List";` in the code above sets the `Title` property of the `ViewData` dictionary to "Movie List". The `Title` property is used in the `<title>` HTML element in the layout page:

```
<title>@ViewData["Title"] - Movie App</title>
```

Save the change and navigate to `https://localhost:{PORT}/HelloWorld`.

Notice that the following have changed:

* Browser title.
* Primary heading.
* Secondary headings.

If there are no changes in the browser, it could be cached content that is being viewed. Press Ctrl+F5 in the browser to force the response from the server to be loaded. The browser title is created with `ViewData["Title"]` we set in the `Index.cshtml` view template and the additional "- Movie App" added in the layout file.

The content in the `Index.cshtml` view template is merged with the `Views/Shared/_Layout.cshtml` view template. A single HTML response is sent to the browser. Layout templates make it easy to make changes that apply across all of the pages in an app. To learn more, see [Layout](../../mvc/views/layout?view=aspnetcore-10.0).

The small bit of "data", the "Hello from our View Template!" message, is hard-coded however. The MVC application has a "V" (view), a "C" (controller), but no "M" (model) yet.

## Passing Data from the Controller to the View

Controller actions are invoked in response to an incoming URL request. A controller class is where the code is written that handles the incoming browser requests. The controller retrieves data from a data source and decides what type of response to send back to the browser. View templates can be used from a controller to generate and format an HTML response to the browser.

Controllers are responsible for providing the data required in order for a view template to render a response.

View templates should **not**:

* Do business logic
* Interact with a database directly.

A view template should work only with the data that's provided to it by the controller. Maintaining this "separation of concerns" helps keep the code:

* Clean.
* Testable.
* Maintainable.

Currently, the `Welcome` method in the `HelloWorldController` class takes a `name` and an `ID` parameter and then outputs the values directly to the browser.

Rather than have the controller render this response as a string, change the controller to use a view template instead. The view template generates a dynamic response, which means that appropriate data must be passed from the controller to the view to generate the response. Do this by having the controller put the dynamic data (parameters) that the view template needs in a `ViewData` dictionary. The view template can then access the dynamic data.

In `HelloWorldController.cs`, change the `Welcome` method to add a `Message` and `NumTimes` value to the `ViewData` dictionary.

The `ViewData` dictionary is a dynamic object, which means any type can be used. The `ViewData` object has no defined properties until something is added. The [MVC model binding system](../../mvc/models/model-binding?view=aspnetcore-10.0) automatically maps the named parameters `name` and `numTimes` from the query string to parameters in the method. The complete `HelloWorldController`:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
    public IActionResult Welcome(string name, int numTimes = 1)
    {
        ViewData["Message"] = "Hello " + name;
        ViewData["NumTimes"] = numTimes;
        return View();
    }
}
```

The `ViewData` dictionary object contains data that will be passed to the view.

Create a Welcome view template named `Views/HelloWorld/Welcome.cshtml`.

You'll create a loop in the `Welcome.cshtml` view template that displays "Hello" `NumTimes`. Replace the contents of `Views/HelloWorld/Welcome.cshtml` with the following:

```
@{
    ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
    @for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
    {
        <li>@ViewData["Message"]</li>
    }
</ul>
```

Save your changes and browse to the following URL:

`https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`

Data is taken from the URL and passed to the controller using the [MVC model binder](../../mvc/models/model-binding?view=aspnetcore-10.0). The controller packages the data into a `ViewData` dictionary and passes that object to the view. The view then renders the data as HTML to the browser.

In the preceding sample, the `ViewData` dictionary was used to pass data from the controller to a view. Later in the tutorial, a view model is used to pass data from a controller to a view. The view model approach to passing data is preferred over the `ViewData` dictionary approach.

In the next tutorial, a database of movies is created.

[Previous: Add a Controller](adding-controller?view=aspnetcore-10.0) [Next: Add a Model](adding-model?view=aspnetcore-10.0)

In this section, you modify the `HelloWorldController` class to use [Razor](../../mvc/views/razor?view=aspnetcore-10.0) view files. This cleanly encapsulates the process of generating HTML responses to a client.

View templates are created using Razor. Razor-based view templates:

* Have a `.cshtml` file extension.
* Provide an elegant way to create HTML output with C#.

Currently the `Index` method returns a string with a message in the controller class. In the `HelloWorldController` class, replace the `Index` method with the following code:

```
public IActionResult Index()
{
    return View();
}
```

The preceding code:

* Calls the controller's [View](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.view) method.
* Uses a view template to generate an HTML response.

Controller methods:

* Are referred to as *action methods*. For example, the `Index` action method in the preceding code.
* Generally return an [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or a class derived from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult), not a type like `string`.

## Add a view

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Right-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Right-click on the *Views/HelloWorld* folder, and then **Add > New Item**.

In the **Add New Item** dialog select **Show All Templates**.

In the **Add New Item - MvcMovie** dialog:

* In the search box in the upper-right, enter *view*
* Select **Razor View - Empty**
* Keep the **Name** box value, `Index.cshtml`.
* Select **Add**

Add an `Index` view for the `HelloWorldController`:

* Add a new folder named *Views/HelloWorld*.
* Add a new file to the *Views/HelloWorld* folder, and name it `Index.cshtml`.

Replace the contents of the `Views/HelloWorld/Index.cshtml` Razor view file with the following:

```
@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>
```

Navigate to `https://localhost:{PORT}/HelloWorld`:

* The `Index` method in the `HelloWorldController` ran the statement `return View();`, which specified that the method should use a view template file to render a response to the browser.

* A view template file name wasn't specified, so MVC defaulted to using the default view file. When the view file name isn't specified, the default view is returned. The default view has the same name as the action method, `Index` in this example. The view template `/Views/HelloWorld/Index.cshtml` is used.

* The following image shows the string "Hello from our View Template!" hard-coded in the view:

## Change views and layout pages

Select the menu links **MvcMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Views/Shared/_Layout.cshtml` file.

Open the `Views/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow:

* Specifying the HTML container layout of a site in one place.
* Applying the HTML container layout across multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the view-specific pages you create show up, *wrapped* in the layout page. For example, if you select the **Privacy** link, the *`Views/Home/Privacy.cshtml`* view is rendered inside the `RenderBody` method.

## Change the title, footer, and menu link in the layout file

Replace the content of the `Views/Shared/_Layout.cshtml` file with the following markup. The changes are highlighted:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Movie App</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Movies" asp-action="Index">Movie App</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2023 - Movie App - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

The preceding markup made the following changes:

* Three occurrences of `MvcMovie` to `Movie App`.
* The anchor element `<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a>` to `<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>`.

In the preceding markup, the `asp-area=""` [anchor Tag Helper attribute](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) and attribute value was omitted because this app isn't using [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0).

**Note**: The `Movies` controller hasn't been implemented. At this point, the `Movie App` link isn't functional.

Save the changes and select the **Privacy** link. Notice how the title on the browser tab displays **Privacy Policy - Movie App** instead of **Privacy Policy - MvcMovie**

Select the **Home** link.

Notice that the title and anchor text display **Movie App**. The changes were made once in the layout template and all pages on the site reflect the new link text and new title.

Examine the `Views/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The `Views/_ViewStart.cshtml` file brings in the `Views/Shared/_Layout.cshtml` file to each view. The `Layout` property can be used to set a different layout view, or set it to `null` so no layout file will be used.

Open the `Views/HelloWorld/Index.cshtml` view file.

Change the title and `<h2>` element as highlighted in the following:

```
@{
    ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>
```

The title and `<h2>` element are slightly different so it's clear which part of the code changes the display.

`ViewData["Title"] = "Movie List";` in the code above sets the `Title` property of the `ViewData` dictionary to "Movie List". The `Title` property is used in the `<title>` HTML element in the layout page:

```
<title>@ViewData["Title"] - Movie App</title>
```

Save the change and navigate to `https://localhost:{PORT}/HelloWorld`.

Notice that the following have changed:

* Browser title.
* Primary heading.
* Secondary headings.

If there are no changes in the browser, it could be cached content that is being viewed. Press Ctrl+F5 in the browser to force the response from the server to be loaded. The browser title is created with `ViewData["Title"]` we set in the `Index.cshtml` view template and the additional "- Movie App" added in the layout file.

The content in the `Index.cshtml` view template is merged with the `Views/Shared/_Layout.cshtml` view template. A single HTML response is sent to the browser. Layout templates make it easy to make changes that apply across all of the pages in an app. To learn more, see [Layout](../../mvc/views/layout?view=aspnetcore-10.0).

The small bit of "data", the "Hello from our View Template!" message, is hard-coded however. The MVC application has a "V" (view), a "C" (controller), but no "M" (model) yet.

## Passing Data from the Controller to the View

Controller actions are invoked in response to an incoming URL request. A controller class is where the code is written that handles the incoming browser requests. The controller retrieves data from a data source and decides what type of response to send back to the browser. View templates can be used from a controller to generate and format an HTML response to the browser.

Controllers are responsible for providing the data required in order for a view template to render a response.

View templates should **not**:

* Do business logic
* Interact with a database directly.

A view template should work only with the data that's provided to it by the controller. Maintaining this "separation of concerns" helps keep the code:

* Clean.
* Testable.
* Maintainable.

Currently, the `Welcome` method in the `HelloWorldController` class takes a `name` and an `ID` parameter and then outputs the values directly to the browser.

Rather than have the controller render this response as a string, change the controller to use a view template instead. The view template generates a dynamic response, which means that appropriate data must be passed from the controller to the view to generate the response. Do this by having the controller put the dynamic data (parameters) that the view template needs in a `ViewData` dictionary. The view template can then access the dynamic data.

In `HelloWorldController.cs`, change the `Welcome` method to add a `Message` and `NumTimes` value to the `ViewData` dictionary.

The `ViewData` dictionary is a dynamic object, which means any type can be used. The `ViewData` object has no defined properties until something is added. The [MVC model binding system](../../mvc/models/model-binding?view=aspnetcore-10.0) automatically maps the named parameters `name` and `numTimes` from the query string to parameters in the method. The complete `HelloWorldController`:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
    public IActionResult Welcome(string name, int numTimes = 1)
    {
        ViewData["Message"] = "Hello " + name;
        ViewData["NumTimes"] = numTimes;
        return View();
    }
}
```

The `ViewData` dictionary object contains data that will be passed to the view.

Create a Welcome view template named `Views/HelloWorld/Welcome.cshtml`.

You'll create a loop in the `Welcome.cshtml` view template that displays "Hello" `NumTimes`. Replace the contents of `Views/HelloWorld/Welcome.cshtml` with the following:

```
@{
    ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
    @for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
    {
        <li>@ViewData["Message"]</li>
    }
</ul>
```

Save your changes and browse to the following URL:

`https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`

Data is taken from the URL and passed to the controller using the [MVC model binder](../../mvc/models/model-binding?view=aspnetcore-10.0). The controller packages the data into a `ViewData` dictionary and passes that object to the view. The view then renders the data as HTML to the browser.

In the preceding sample, the `ViewData` dictionary was used to pass data from the controller to a view. Later in the tutorial, a view model is used to pass data from a controller to a view. The view model approach to passing data is preferred over the `ViewData` dictionary approach.

In the next tutorial, a database of movies is created.

[Previous: Add a Controller](adding-controller?view=aspnetcore-10.0) [Next: Add a Model](adding-model?view=aspnetcore-10.0)

In this section, you modify the `HelloWorldController` class to use [Razor](../../mvc/views/razor?view=aspnetcore-10.0) view files. This cleanly encapsulates the process of generating HTML responses to a client.

View templates are created using Razor. Razor-based view templates:

* Have a `.cshtml` file extension.
* Provide an elegant way to create HTML output with C#.

Currently the `Index` method returns a string with a message in the controller class. In the `HelloWorldController` class, replace the `Index` method with the following code:

```
public IActionResult Index()
{
    return View();
}
```

The preceding code:

* Calls the controller's [View](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.view) method.
* Uses a view template to generate an HTML response.

Controller methods:

* Are referred to as *action methods*. For example, the `Index` action method in the preceding code.
* Generally return an [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or a class derived from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult), not a type like `string`.

## Add a view

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Right-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Right-click on the *Views/HelloWorld* folder, and then **Add > New Item**.

In the **Add New Item - MvcMovie** dialog:

* In the search box in the upper-right, enter *view*
* Select **Razor View - Empty**
* Keep the **Name** box value, `Index.cshtml`.
* Select **Add**

Add an `Index` view for the `HelloWorldController`:

* Add a new folder named *Views/HelloWorld*.
* Add a new file to the *Views/HelloWorld* folder, and name it `Index.cshtml`.

Control-click the *Views* folder and select **Add > New Folder** and name the folder *HelloWorld*.

Control-click the *Views/HelloWorld* folder and select **Add > New File**.

In the **New File** dialog:

* Select **ASP.NET Core** in the left pane.
* Select **Razor View** in the center pane.
* Type *Index* in the **Name** box.
* Select **Create**.

Replace the contents of the `Views/HelloWorld/Index.cshtml` Razor view file with the following:

```
@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>
```

Navigate to `https://localhost:{PORT}/HelloWorld`:

* The `Index` method in the `HelloWorldController` ran the statement `return View();`, which specified that the method should use a view template file to render a response to the browser.

* A view template file name wasn't specified, so MVC defaulted to using the default view file. When the view file name isn't specified, the default view is returned. The default view has the same name as the action method, `Index` in this example. The view template `/Views/HelloWorld/Index.cshtml` is used.

* The following image shows the string "Hello from our View Template!" hard-coded in the view:

## Change views and layout pages

Select the menu links **MvcMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Views/Shared/_Layout.cshtml` file.

Open the `Views/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow:

* Specifying the HTML container layout of a site in one place.
* Applying the HTML container layout across multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the view-specific pages you create show up, *wrapped* in the layout page. For example, if you select the **Privacy** link, the *`Views/Home/Privacy.cshtml`* view is rendered inside the `RenderBody` method.

## Change the title, footer, and menu link in the layout file

Replace the content of the `Views/Shared/_Layout.cshtml` file with the following markup. The changes are highlighted:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Movie App</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Movies" asp-action="Index">Movie App</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2022 - Movie App - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

The preceding markup made the following changes:

* Three occurrences of `MvcMovie` to `Movie App`.
* The anchor element `<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a>` to `<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>`.

In the preceding markup, the `asp-area=""` [anchor Tag Helper attribute](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) and attribute value was omitted because this app isn't using [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0).

**Note**: The `Movies` controller hasn't been implemented. At this point, the `Movie App` link isn't functional.

Save the changes and select the **Privacy** link. Notice how the title on the browser tab displays **Privacy Policy - Movie App** instead of **Privacy Policy - MvcMovie**

Select the **Home** link.

Notice that the title and anchor text display **Movie App**. The changes were made once in the layout template and all pages on the site reflect the new link text and new title.

Examine the `Views/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The `Views/_ViewStart.cshtml` file brings in the `Views/Shared/_Layout.cshtml` file to each view. The `Layout` property can be used to set a different layout view, or set it to `null` so no layout file will be used.

Open the `Views/HelloWorld/Index.cshtml` view file.

Change the title and `<h2>` element as highlighted in the following:

```
@{
    ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>
```

The title and `<h2>` element are slightly different so it's clear which part of the code changes the display.

`ViewData["Title"] = "Movie List";` in the code above sets the `Title` property of the `ViewData` dictionary to "Movie List". The `Title` property is used in the `<title>` HTML element in the layout page:

```
<title>@ViewData["Title"] - Movie App</title>
```

Save the change and navigate to `https://localhost:{PORT}/HelloWorld`.

Notice that the following have changed:

* Browser title.
* Primary heading.
* Secondary headings.

If there are no changes in the browser, it could be cached content that is being viewed. Press Ctrl+F5 in the browser to force the response from the server to be loaded. The browser title is created with `ViewData["Title"]` we set in the `Index.cshtml` view template and the additional "- Movie App" added in the layout file.

The content in the `Index.cshtml` view template is merged with the `Views/Shared/_Layout.cshtml` view template. A single HTML response is sent to the browser. Layout templates make it easy to make changes that apply across all of the pages in an app. To learn more, see [Layout](../../mvc/views/layout?view=aspnetcore-10.0).

The small bit of "data", the "Hello from our View Template!" message, is hard-coded however. The MVC application has a "V" (view), a "C" (controller), but no "M" (model) yet.

## Passing Data from the Controller to the View

Controller actions are invoked in response to an incoming URL request. A controller class is where the code is written that handles the incoming browser requests. The controller retrieves data from a data source and decides what type of response to send back to the browser. View templates can be used from a controller to generate and format an HTML response to the browser.

Controllers are responsible for providing the data required in order for a view template to render a response.

View templates should **not**:

* Do business logic
* Interact with a database directly.

A view template should work only with the data that's provided to it by the controller. Maintaining this "separation of concerns" helps keep the code:

* Clean.
* Testable.
* Maintainable.

Currently, the `Welcome` method in the `HelloWorldController` class takes a `name` and an `ID` parameter and then outputs the values directly to the browser.

Rather than have the controller render this response as a string, change the controller to use a view template instead. The view template generates a dynamic response, which means that appropriate data must be passed from the controller to the view to generate the response. Do this by having the controller put the dynamic data (parameters) that the view template needs in a `ViewData` dictionary. The view template can then access the dynamic data.

In `HelloWorldController.cs`, change the `Welcome` method to add a `Message` and `NumTimes` value to the `ViewData` dictionary.

The `ViewData` dictionary is a dynamic object, which means any type can be used. The `ViewData` object has no defined properties until something is added. The [MVC model binding system](../../mvc/models/model-binding?view=aspnetcore-10.0) automatically maps the named parameters `name` and `numTimes` from the query string to parameters in the method. The complete `HelloWorldController`:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
    public IActionResult Welcome(string name, int numTimes = 1)
    {
        ViewData["Message"] = "Hello " + name;
        ViewData["NumTimes"] = numTimes;
        return View();
    }
}
```

The `ViewData` dictionary object contains data that will be passed to the view.

Create a Welcome view template named `Views/HelloWorld/Welcome.cshtml`.

You'll create a loop in the `Welcome.cshtml` view template that displays "Hello" `NumTimes`. Replace the contents of `Views/HelloWorld/Welcome.cshtml` with the following:

```
@{
    ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
    @for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
    {
        <li>@ViewData["Message"]</li>
    }
</ul>
```

Save your changes and browse to the following URL:

`https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`

Data is taken from the URL and passed to the controller using the [MVC model binder](../../mvc/models/model-binding?view=aspnetcore-10.0). The controller packages the data into a `ViewData` dictionary and passes that object to the view. The view then renders the data as HTML to the browser.

In the preceding sample, the `ViewData` dictionary was used to pass data from the controller to a view. Later in the tutorial, a view model is used to pass data from a controller to a view. The view model approach to passing data is preferred over the `ViewData` dictionary approach.

In the next tutorial, a database of movies is created.

[Previous: Add a Controller](adding-controller?view=aspnetcore-10.0) [Next: Add a Model](adding-model?view=aspnetcore-10.0)

In this section, you modify the `HelloWorldController` class to use [Razor](../../mvc/views/razor?view=aspnetcore-10.0) view files. This cleanly encapsulates the process of generating HTML responses to a client.

View templates are created using Razor. Razor-based view templates:

* Have a `.cshtml` file extension.
* Provide an elegant way to create HTML output with C#.

Currently the `Index` method returns a string with a message in the controller class. In the `HelloWorldController` class, replace the `Index` method with the following code:

```
public IActionResult Index()
{
    return View();
}
```

The preceding code:

* Calls the controller's [View](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.view) method.
* Uses a view template to generate an HTML response.

Controller methods:

* Are referred to as *action methods*. For example, the `Index` action method in the preceding code.
* Generally return an [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or a class derived from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult), not a type like `string`.

## Add a view

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Right-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Right-click on the *Views/HelloWorld* folder, and then **Add > New Item**.

In the **Add New Item - MvcMovie** dialog:

* In the search box in the upper-right, enter *view*
* Select **Razor View - Empty**
* Keep the **Name** box value, `Index.cshtml`.
* Select **Add**

Add an `Index` view for the `HelloWorldController`:

* Add a new folder named *Views/HelloWorld*.
* Add a new file to the *Views/HelloWorld* folder, and name it `Index.cshtml`.

For Visual Studio for Mac, see the .NET 7 version of this tutorial.

Control-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Control-click on the *Views/HelloWorld* folder, and then **Add > New File**.

In the **New File** dialog:

* Select **ASP.NET Core** in the left pane.

* Select **Razor View** in the center pane.

* Type *Index* in the **Name** box.

* Select **Create**.

Replace the contents of the `Views/HelloWorld/Index.cshtml` Razor view file with the following:

```
@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>
```

Navigate to `https://localhost:{PORT}/HelloWorld`:

* The `Index` method in the `HelloWorldController` ran the statement `return View();`, which specified that the method should use a view template file to render a response to the browser.

* A view template file name wasn't specified, so MVC defaulted to using the default view file. When the view file name isn't specified, the default view is returned. The default view has the same name as the action method, `Index` in this example. The view template `/Views/HelloWorld/Index.cshtml` is used.

* The following image shows the string "Hello from our View Template!" hard-coded in the view:

## Change views and layout pages

Select the menu links **MvcMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Views/Shared/_Layout.cshtml` file.

Open the `Views/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow:

* Specifying the HTML container layout of a site in one place.
* Applying the HTML container layout across multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the view-specific pages you create show up, *wrapped* in the layout page. For example, if you select the **Privacy** link, the *`Views/Home/Privacy.cshtml`* view is rendered inside the `RenderBody` method.

## Change the title, footer, and menu link in the layout file

Replace the content of the `Views/Shared/_Layout.cshtml` file with the following markup. The changes are highlighted:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Movie App</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Movies" asp-action="Index">Movie App</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2021 - Movie App - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

The preceding markup made the following changes:

* Three occurrences of `MvcMovie` to `Movie App`.
* The anchor element `<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a>` to `<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>`.

In the preceding markup, the `asp-area=""` [anchor Tag Helper attribute](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) and attribute value was omitted because this app isn't using [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0).

**Note**: The `Movies` controller hasn't been implemented. At this point, the `Movie App` link isn't functional.

Save the changes and select the **Privacy** link. Notice how the title on the browser tab displays **Privacy Policy - Movie App** instead of **Privacy Policy - MvcMovie**

Select the **Home** link.

Notice that the title and anchor text display **Movie App**. The changes were made once in the layout template and all pages on the site reflect the new link text and new title.

Examine the `Views/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The `Views/_ViewStart.cshtml` file brings in the `Views/Shared/_Layout.cshtml` file to each view. The `Layout` property can be used to set a different layout view, or set it to `null` so no layout file will be used.

Open the `Views/HelloWorld/Index.cshtml` view file.

Change the title and `<h2>` element as highlighted in the following:

```
@{
    ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>
```

The title and `<h2>` element are slightly different so it's clear which part of the code changes the display.

`ViewData["Title"] = "Movie List";` in the code above sets the `Title` property of the `ViewData` dictionary to "Movie List". The `Title` property is used in the `<title>` HTML element in the layout page:

```
<title>@ViewData["Title"] - Movie App</title>
```

Save the change and navigate to `https://localhost:{PORT}/HelloWorld`.

Notice that the following have changed:

* Browser title.
* Primary heading.
* Secondary headings.

If there are no changes in the browser, it could be cached content that is being viewed. Press Ctrl+F5 in the browser to force the response from the server to be loaded. The browser title is created with `ViewData["Title"]` we set in the `Index.cshtml` view template and the additional "- Movie App" added in the layout file.

The content in the `Index.cshtml` view template is merged with the `Views/Shared/_Layout.cshtml` view template. A single HTML response is sent to the browser. Layout templates make it easy to make changes that apply across all of the pages in an app. To learn more, see [Layout](../../mvc/views/layout?view=aspnetcore-10.0).

The small bit of "data", the "Hello from our View Template!" message, is hard-coded however. The MVC application has a "V" (view), a "C" (controller), but no "M" (model) yet.

## Passing Data from the Controller to the View

Controller actions are invoked in response to an incoming URL request. A controller class is where the code is written that handles the incoming browser requests. The controller retrieves data from a data source and decides what type of response to send back to the browser. View templates can be used from a controller to generate and format an HTML response to the browser.

Controllers are responsible for providing the data required in order for a view template to render a response.

View templates should **not**:

* Do business logic
* Interact with a database directly.

A view template should work only with the data that's provided to it by the controller. Maintaining this "separation of concerns" helps keep the code:

* Clean.
* Testable.
* Maintainable.

Currently, the `Welcome` method in the `HelloWorldController` class takes a `name` and an `ID` parameter and then outputs the values directly to the browser.

Rather than have the controller render this response as a string, change the controller to use a view template instead. The view template generates a dynamic response, which means that appropriate data must be passed from the controller to the view to generate the response. Do this by having the controller put the dynamic data (parameters) that the view template needs in a `ViewData` dictionary. The view template can then access the dynamic data.

In `HelloWorldController.cs`, change the `Welcome` method to add a `Message` and `NumTimes` value to the `ViewData` dictionary.

The `ViewData` dictionary is a dynamic object, which means any type can be used. The `ViewData` object has no defined properties until something is added. The [MVC model binding system](../../mvc/models/model-binding?view=aspnetcore-10.0) automatically maps the named parameters `name` and `numTimes` from the query string to parameters in the method. The complete `HelloWorldController`:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
    public class HelloWorldController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult Welcome(string name, int numTimes = 1)
        {
            ViewData["Message"] = "Hello " + name;
            ViewData["NumTimes"] = numTimes;

            return View();
        }
    }
}
```

The `ViewData` dictionary object contains data that will be passed to the view.

Create a Welcome view template named `Views/HelloWorld/Welcome.cshtml`.

You'll create a loop in the `Welcome.cshtml` view template that displays "Hello" `NumTimes`. Replace the contents of `Views/HelloWorld/Welcome.cshtml` with the following:

```
@{
    ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
    @for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
    {
        <li>@ViewData["Message"]</li>
    }
</ul>
```

Save your changes and browse to the following URL:

`https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`

Data is taken from the URL and passed to the controller using the [MVC model binder](../../mvc/models/model-binding?view=aspnetcore-10.0). The controller packages the data into a `ViewData` dictionary and passes that object to the view. The view then renders the data as HTML to the browser.

In the preceding sample, the `ViewData` dictionary was used to pass data from the controller to a view. Later in the tutorial, a view model is used to pass data from a controller to a view. The view model approach to passing data is preferred over the `ViewData` dictionary approach.

In the next tutorial, a database of movies is created.

[Previous: Add a Controller](adding-controller?view=aspnetcore-10.0) [Next: Add a Model](adding-model?view=aspnetcore-10.0)

In this section, you modify the `HelloWorldController` class to use [Razor](../../mvc/views/razor?view=aspnetcore-10.0) view files. This cleanly encapsulates the process of generating HTML responses to a client.

View templates are created using Razor. Razor-based view templates:

* Have a `.cshtml` file extension.
* Provide an elegant way to create HTML output with C#.

Currently the `Index` method returns a string with a message in the controller class. In the `HelloWorldController` class, replace the `Index` method with the following code:

```
public IActionResult Index()
{
    return View();
}
```

The preceding code:

* Calls the controller's [View](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.view) method.
* Uses a view template to generate an HTML response.

Controller methods:

* Are referred to as *action methods*. For example, the `Index` action method in the preceding code.
* Generally return an [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or a class derived from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult), not a type like `string`.

## Add a view

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Right-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Right-click on the *Views/HelloWorld* folder, and then **Add > New Item**.

In the **Add New Item - MvcMovie** dialog:

* In the search box in the upper-right, enter *view*
* Select **Razor View - Empty**
* Keep the **Name** box value, `Index.cshtml`.
* Select **Add**

Add an `Index` view for the `HelloWorldController`:

* Add a new folder named *Views/HelloWorld*.
* Add a new file to the *Views/HelloWorld* folder name `Index.cshtml`.

Control-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Control-click on the *Views/HelloWorld* folder, and then **Add > New File**.

In the **New File** dialog:

* Select **ASP.NET Core** in the left pane.

* Select **Razor View - Empty** in the center pane.

* Type *Index* in the **Name** box.

* Select **New**.

Replace the contents of the `Views/HelloWorld/Index.cshtml` Razor view file with the following:

```
@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>
```

Navigate to `https://localhost:{PORT}/HelloWorld`:

* The `Index` method in the `HelloWorldController` ran the statement `return View();`, which specified that the method should use a view template file to render a response to the browser.

* A view template file name wasn't specified, so MVC defaulted to using the default view file. When the view file name isn't specified, the default view is returned. The default view has the same name as the action method, `Index` in this example. The view template `/Views/HelloWorld/Index.cshtml` is used.

* The following image shows the string "Hello from our View Template!" hard-coded in the view:

## Change views and layout pages

Select the menu links **MvcMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Views/Shared/_Layout.cshtml` file.

Open the `Views/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allows:

* Specifying the HTML container layout of a site in one place.
* Applying the HTML container layout across multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the view-specific pages you create show up, *wrapped* in the layout page. For example, if you select the **Privacy** link, the *`Views/Home/Privacy.cshtml`* view is rendered inside the `RenderBody` method.

## Change the title, footer, and menu link in the layout file

Replace the content of the `Views/Shared/_Layout.cshtml` file with the following markup. The changes are highlighted:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Movie App</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2020 - Movie App - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

The preceding markup made the following changes:

* Three occurrences of `MvcMovie` to `Movie App`.
* The anchor element `<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a>` to `<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>`.

In the preceding markup, the `asp-area=""` [anchor Tag Helper attribute](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) and attribute value was omitted because this app isn't using [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0).

**Note**: The `Movies` controller hasn't been implemented. At this point, the `Movie App` link isn't functional.

Save the changes and select the **Privacy** link. Notice how the title on the browser tab displays **Privacy Policy - Movie App** instead of **Privacy Policy - MvcMovie**

Select the **Home** link.

Notice that the title and anchor text display **Movie App**. The changes were made once in the layout template and all pages on the site reflect the new link text and new title.

Examine the `Views/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The `Views/_ViewStart.cshtml` file brings in the `Views/Shared/_Layout.cshtml` file to each view. The `Layout` property can be used to set a different layout view, or set it to `null` so no layout file will be used.

Open the `Views/HelloWorld/Index.cshtml` view file.

Change the title and `<h2>` element as highlighted in the following:

```
@{
    ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>
```

The title and `<h2>` element are slightly different so it's clear which part of the code changes the display.

`ViewData["Title"] = "Movie List";` in the code above sets the `Title` property of the `ViewData` dictionary to "Movie List". The `Title` property is used in the `<title>` HTML element in the layout page:

```
<title>@ViewData["Title"] - Movie App</title>
```

Save the change and navigate to `https://localhost:{PORT}/HelloWorld`.

Notice that the following have changed:

* Browser title.
* Primary heading.
* Secondary headings.

If there are no changes in the browser, it could be cached content that is being viewed. Press Ctrl+F5 in the browser to force the response from the server to be loaded. The browser title is created with `ViewData["Title"]` we set in the `Index.cshtml` view template and the additional "- Movie App" added in the layout file.

The content in the `Index.cshtml` view template is merged with the `Views/Shared/_Layout.cshtml` view template. A single HTML response is sent to the browser. Layout templates make it easy to make changes that apply across all of the pages in an app. To learn more, see [Layout](../../mvc/views/layout?view=aspnetcore-10.0).

The small bit of "data", the "Hello from our View Template!" message, is hard-coded however. The MVC application has a "V" (view), a "C" (controller), but no "M" (model) yet.

## Passing Data from the Controller to the View

Controller actions are invoked in response to an incoming URL request. A controller class is where the code is written that handles the incoming browser requests. The controller retrieves data from a data source and decides what type of response to send back to the browser. View templates can be used from a controller to generate and format an HTML response to the browser.

Controllers are responsible for providing the data required in order for a view template to render a response.

View templates should **not**:

* Do business logic
* Interact with a database directly.

A view template should work only with the data that's provided to it by the controller. Maintaining this "separation of concerns" helps keep the code:

* Clean.
* Testable.
* Maintainable.

Currently, the `Welcome` method in the `HelloWorldController` class takes a `name` and an `ID` parameter and then outputs the values directly to the browser.

Rather than have the controller render this response as a string, change the controller to use a view template instead. The view template generates a dynamic response, which means that appropriate data must be passed from the controller to the view to generate the response. Do this by having the controller put the dynamic data (parameters) that the view template needs in a `ViewData` dictionary. The view template can then access the dynamic data.

In `HelloWorldController.cs`, change the `Welcome` method to add a `Message` and `NumTimes` value to the `ViewData` dictionary.

The `ViewData` dictionary is a dynamic object, which means any type can be used. The `ViewData` object has no defined properties until something is added. The [MVC model binding system](../../mvc/models/model-binding?view=aspnetcore-10.0) automatically maps the named parameters `name` and `numTimes` from the query string to parameters in the method. The complete `HelloWorldController`:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
    public class HelloWorldController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult Welcome(string name, int numTimes = 1)
        {
            ViewData["Message"] = "Hello " + name;
            ViewData["NumTimes"] = numTimes;

            return View();
        }
    }
}
```

The `ViewData` dictionary object contains data that will be passed to the view.

Create a Welcome view template named `Views/HelloWorld/Welcome.cshtml`.

You'll create a loop in the `Welcome.cshtml` view template that displays "Hello" `NumTimes`. Replace the contents of `Views/HelloWorld/Welcome.cshtml` with the following:

```
@{
    ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
    @for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
    {
        <li>@ViewData["Message"]</li>
    }
</ul>
```

Save your changes and browse to the following URL:

`https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`

Data is taken from the URL and passed to the controller using the [MVC model binder](../../mvc/models/model-binding?view=aspnetcore-10.0). The controller packages the data into a `ViewData` dictionary and passes that object to the view. The view then renders the data as HTML to the browser.

In the preceding sample, the `ViewData` dictionary was used to pass data from the controller to a view. Later in the tutorial, a view model is used to pass data from a controller to a view. The view model approach to passing data is preferred over the `ViewData` dictionary approach.

In the next tutorial, a database of movies is created.

[Previous: Add a Controller](adding-controller?view=aspnetcore-10.0) [Next: Add a Model](adding-model?view=aspnetcore-10.0)

In this section, you modify the `HelloWorldController` class to use [Razor](../../mvc/views/razor?view=aspnetcore-10.0) view files. This cleanly encapsulates the process of generating HTML responses to a client.

View templates are created using Razor. Razor-based view templates:

* Have a `.cshtml` file extension.
* Provide an elegant way to create HTML output with C#.

Currently the `Index` method returns a string with a message in the controller class. In the `HelloWorldController` class, replace the `Index` method with the following code:

```
public IActionResult Index()
{
    return View();
}
```

The preceding code:

* Calls the controller's [View](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.view) method.
* Uses a view template to generate an HTML response.

Controller methods:

* Are referred to as *action methods*. For example, the `Index` action method in the preceding code.
* Generally return an [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or a class derived from [ActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult), not a type like `string`.

## Add a view

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Right-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Right-click on the *Views/HelloWorld* folder, and then **Add > New Item**.

In the **Add New Item - MvcMovie** dialog:

* In the search box in the upper-right, enter *view*
* Select **Razor View - Empty**
* Keep the **Name** box value, `Index.cshtml`.
* Select **Add**

Add an `Index` view for the `HelloWorldController`:

* Add a new folder named *Views/HelloWorld*.
* Add a new file to the *Views/HelloWorld* folder name `Index.cshtml`.

Control-click on the *Views* folder, and then **Add > New Folder** and name the folder *HelloWorld*.

Control-click on the *Views/HelloWorld* folder, and then **Add > New File**.

In the **New File** dialog:

* Select **ASP.NET Core** in the left pane.

* Select **Razor View - Empty** in the center pane.

* Type *Index* in the **Name** box.

* Select **New**.

Replace the contents of the `Views/HelloWorld/Index.cshtml` Razor view file with the following:

```
@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>Hello from our View Template!</p>
```

Navigate to `https://localhost:{PORT}/HelloWorld`:

* The `Index` method in the `HelloWorldController` ran the statement `return View();`, which specified that the method should use a view template file to render a response to the browser.

* A view template file name wasn't specified, so MVC defaulted to using the default view file. When the view file name isn't specified, the default view is returned. The default view has the same name as the action method, `Index` in this example. The view template `/Views/HelloWorld/Index.cshtml` is used.

* The following image shows the string "Hello from our View Template!" hard-coded in the view:

## Change views and layout pages

Select the menu links **MvcMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Views/Shared/_Layout.cshtml` file.

Open the `Views/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allows:

* Specifying the HTML container layout of a site in one place.
* Applying the HTML container layout across multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the view-specific pages you create show up, *wrapped* in the layout page. For example, if you select the **Privacy** link, the *`Views/Home/Privacy.cshtml`* view is rendered inside the `RenderBody` method.

## Change the title, footer, and menu link in the layout file

Replace the content of the `Views/Shared/_Layout.cshtml` file with the following markup. The changes are highlighted:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Movie App</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2020 - Movie App - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>
```

The preceding markup made the following changes:

* Three occurrences of `MvcMovie` to `Movie App`.
* The anchor element `<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a>` to `<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>`.

In the preceding markup, the `asp-area=""` [anchor Tag Helper attribute](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) and attribute value was omitted because this app isn't using [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0).

**Note**: The `Movies` controller hasn't been implemented. At this point, the `Movie App` link isn't functional.

Save the changes and select the **Privacy** link. Notice how the title on the browser tab displays **Privacy Policy - Movie App** instead of **Privacy Policy - MvcMovie**

Select the **Home** link.

Notice that the title and anchor text display **Movie App**. The changes were made once in the layout template and all pages on the site reflect the new link text and new title.

Examine the `Views/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The `Views/_ViewStart.cshtml` file brings in the `Views/Shared/_Layout.cshtml` file to each view. The `Layout` property can be used to set a different layout view, or set it to `null` so no layout file will be used.

Open the `Views/HelloWorld/Index.cshtml` view file.

Change the title and `<h2>` element as highlighted in the following:

```
@{
    ViewData["Title"] = "Movie List";
}

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>
```

The title and `<h2>` element are slightly different so it's clear which part of the code changes the display.

`ViewData["Title"] = "Movie List";` in the code above sets the `Title` property of the `ViewData` dictionary to "Movie List". The `Title` property is used in the `<title>` HTML element in the layout page:

```
<title>@ViewData["Title"] - Movie App</title>
```

Save the change and navigate to `https://localhost:{PORT}/HelloWorld`.

Notice that the following have changed:

* Browser title.
* Primary heading.
* Secondary headings.

If there are no changes in the browser, it could be cached content that is being viewed. Press Ctrl+F5 in the browser to force the response from the server to be loaded. The browser title is created with `ViewData["Title"]` we set in the `Index.cshtml` view template and the additional "- Movie App" added in the layout file.

The content in the `Index.cshtml` view template is merged with the `Views/Shared/_Layout.cshtml` view template. A single HTML response is sent to the browser. Layout templates make it easy to make changes that apply across all of the pages in an app. To learn more, see [Layout](../../mvc/views/layout?view=aspnetcore-10.0).

The small bit of "data", the "Hello from our View Template!" message, is hard-coded however. The MVC application has a "V" (view), a "C" (controller), but no "M" (model) yet.

## Passing Data from the Controller to the View

Controller actions are invoked in response to an incoming URL request. A controller class is where the code is written that handles the incoming browser requests. The controller retrieves data from a data source and decides what type of response to send back to the browser. View templates can be used from a controller to generate and format an HTML response to the browser.

Controllers are responsible for providing the data required in order for a view template to render a response.

View templates should **not**:

* Do business logic
* Interact with a database directly.

A view template should work only with the data that's provided to it by the controller. Maintaining this "separation of concerns" helps keep the code:

* Clean.
* Testable.
* Maintainable.

Currently, the `Welcome` method in the `HelloWorldController` class takes a `name` and an `ID` parameter and then outputs the values directly to the browser.

Rather than have the controller render this response as a string, change the controller to use a view template instead. The view template generates a dynamic response, which means that appropriate data must be passed from the controller to the view to generate the response. Do this by having the controller put the dynamic data (parameters) that the view template needs in a `ViewData` dictionary. The view template can then access the dynamic data.

In `HelloWorldController.cs`, change the `Welcome` method to add a `Message` and `NumTimes` value to the `ViewData` dictionary.

The `ViewData` dictionary is a dynamic object, which means any type can be used. The `ViewData` object has no defined properties until something is added. The [MVC model binding system](../../mvc/models/model-binding?view=aspnetcore-10.0) automatically maps the named parameters `name` and `numTimes` from the query string to parameters in the method. The complete `HelloWorldController`:

```
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
    public class HelloWorldController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult Welcome(string name, int numTimes = 1)
        {
            ViewData["Message"] = "Hello " + name;
            ViewData["NumTimes"] = numTimes;

            return View();
        }
    }
}
```

The `ViewData` dictionary object contains data that will be passed to the view.

Create a Welcome view template named `Views/HelloWorld/Welcome.cshtml`.

You'll create a loop in the `Welcome.cshtml` view template that displays "Hello" `NumTimes`. Replace the contents of `Views/HelloWorld/Welcome.cshtml` with the following:

```
@{
    ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
    @for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
    {
        <li>@ViewData["Message"]</li>
    }
</ul>
```

Save your changes and browse to the following URL:

`https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4`

Data is taken from the URL and passed to the controller using the [MVC model binder](../../mvc/models/model-binding?view=aspnetcore-10.0). The controller packages the data into a `ViewData` dictionary and passes that object to the view. The view then renders the data as HTML to the browser.

In the preceding sample, the `ViewData` dictionary was used to pass data from the controller to a view. Later in the tutorial, a view model is used to pass data from a controller to a view. The view model approach to passing data is preferred over the `ViewData` dictionary approach.

In the next tutorial, a database of movies is created.

[Previous: Add a Controller](adding-controller?view=aspnetcore-10.0) [Next: Add a Model](adding-model?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-09

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app/part-7
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/movie-database-app/part-7.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor movie database app (Part 7 - Add a new field)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article is the seventh part of the Blazor movie database app tutorial that teaches you the basics of building an ASP.NET Core Blazor Web App with features to manage a movie database.

This part of the tutorial series covers adding a new field to the movie class, CRUD pages, and database.

The database update is handled by EF Core migrations. EF Core transparently tracks changes to the database in a migration history table and automatically throws an exception if the app's model classes aren't synchronized with the database's tables and columns. EF Core migrations make it possible to quickly troubleshoot database consistency problems.

Important

Confirm that the app isn't running for the next steps.

## Add a movie rating to the app's model

Open the `Models/Movie.cs` file and add a `Rating` property with a regular expression that limits the value of `Rating` to the exact [Motion Picture Association](https://www.motionpictures.org/) film rating designations:

```
[Required]
[RegularExpression(@"^(G|PG|PG-13|R|NC-17)$")]
public string? Rating { get; set; }
```

## Add the movie rating to the app's CRUD components

Open the `Create` component definition file (`Components/Pages/MoviePages/Create.razor`).

Add the following `<div>` block between the `<div>` block for `Price` and the create button (`<button>`):

```
<div class="mb-3">
    <label for="rating" class="form-label">Rating:</label> 
    <InputText id="rating" @bind-Value="Movie.Rating" class="form-control" /> 
    <ValidationMessage For="() => Movie.Rating" class="text-danger" /> 
</div>
```

Open the `Delete` component definition file (`Components/Pages/MoviePages/Delete.razor`).

Add the following description list (`<dl>`) block between the description list block for `Price` and the `EditForm` component:

```
<dl class="row">
    <dt class="col-sm-2">Rating</dt>
    <dd class="col-sm-10">@movie.Rating</dd>
</dl>
```

Open the `Details` component definition file (`Components/Pages/MoviePages/Details.razor`).

Add the following description list term (`<dt>`) and description list element (`<dl>`) after the term and element for `Price` just inside the closing `</dl>` tag:

```
<dt class="col-sm-2">Rating</dt>
<dd class="col-sm-10">@movie.Rating</dd>
```

Open the `Edit` component definition file (`Components/Pages/MoviePages/Edit.razor`).

Add the following `<div>` block between the `<div>` block for `Price` and the save button (`<button>`):

```
<div class="mb-3">
    <label for="rating" class="form-label">Rating:</label>
    <InputText id="rating" @bind-Value="Movie.Rating" class="form-control" />
    <ValidationMessage For="() => Movie.Rating" class="text-danger" />
</div>
```

Open the `Index` component definition file (`Components/Pages/MoviePages/Index.razor`).

Update the `QuickGrid` component to include the movie rating. Add the following [PropertyColumn\<TGridItem,TProp>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2) immediately after the column for `Price`:

```
<PropertyColumn Property="movie => movie.Rating" />
```

Update the `SeedData` class (`Data/SeedData.cs`) to provide a default value for the new `Rating` property for reseeding operations.

The following change is for the *Mad Max* `new Movie` block. `Rating = "R",` is added to the block:

```
new Movie
{
    Title = "Mad Max",
    ReleaseDate = DateOnly.Parse("1979-4-12"),
    Genre = "Sci-fi (Cyberpunk)",
    Price = 2.51M,
+   Rating = "R",
},
```

Add the `Rating` property to each of the other `new Movie` blocks in the same fashion. Here are the ratings of the remaining *Mad Max* movies when you add the new `Rating` lines:

* *The Road Warrior*: R
* *Mad Max: Beyond Thunderdome*: PG-13
* *Mad Max: Fury Road*: R
* *Furiosa: A Mad Max Saga*: R

**Save all of the updated files.**

Don't run the app yet. Build the app to confirm that there are no errors.

Select **Build** > **Rebuild Solution** from the menu bar.

In the **Command Palette**, use the `.NET: Build` command.

In a command shell opened to the project's root folder, execute the following command:

```
dotnet build
```

Fix any errors in the app that were introduced by pasting the preceding markup and code before proceeding to the next step.

## Update the database

If you were to try and run the app at this point, the app would fail with a SQL exception because the database doesn't include a `Rating` column in its `Movie` table. There are three approaches that you can take to resolve the discrepancy between the database's schema and the model's schema:

* Modify the schema of the database so that it matches the model class. The advantage of this approach is that it maintains the database's data. Adopt this approach using either database tooling or by creating a database change script, which are approaches not covered by this tutorial but can be learned using other articles. The downside to this approach is that it requires more time and is more prone to errors due to its increased complexity. *This tutorial doesn't adopt this approach.*
* Use EF Core to automatically drop and recreate the database using the new model class schema, losing the data stored in the database in the process. The database is reseeded with fresh data when the app is run. This approach allows you to quickly evolve the model and database schema together. Don't use this approach on a production database with data that must be preserved. *This tutorial doesn't adopt this approach.*
* Use an EF Core migration to update the database schema after the model is changed in the app. This approach is efficient and preserves the database's data. ***This tutorial adopts this approach.***

Create a migration to update the database's schema. The movie rating, as a `Rating` column, is added to the database's `Movie` table.

In Visual Studio **Solution Explorer**, double-click **Connected Services**. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select the ellipsis (`...`) followed by **Add migration**.

Give the migration a **Migration name** of `AddRatingField` to describe the migration. Wait for the database context to load in the **DbContext class names** field. Select **Finish** to create the migration. Select **Close** when the operation is complete.

The migration:

* Compares the `Movie` model with the `Movie` database table schema.
* Creates code to migrate the database schema to match the model.

Creating the migration doesn't automatically provision a default value for the rating when the database is updated. However, you can manually make a change to the migration file to apply a default movie rating value, which can be helpful when there are many records that require a default value. In this case, all but one of the *Mad Max* movies is rated *R*, so a default value of "`R`" for the `Rating` column is an appropriate choice. The one movie that doesn't have an *R* rating can be updated later in the running app.

Examine the files in the `Migrations` folder of the project in Visual Studio's **Solution Explorer**. Open the migration file that adds the movie rating field, which has a file name of `{TIME STAMP}_AddRatingField.cs`, where the `{TIME STAMP}` placeholder is a time stamp (for example, `20240530123755_AddRatingField.cs`).

Find the `AddColumn` block that adds the rating column to the `Movie` table in the database. Modify the last line that applies a default value (`defaultValue`). Change it from an empty string (`""`) to an *R* movie rating (`"R"`):

```
migrationBuilder.AddColumn<string>(
    name: "Rating",
    table: "Movie",
    type: "nvarchar(max)",
    nullable: false,
-   defaultValue: "");
+   defaultValue: "R");
```

Save the migration file.

In the **SQL Server Express LocalDB** area of **Service Dependencies**, select the ellipsis (`...`) again followed by the **Update database** command.

The **Update database with the latest migration** dialog opens. Wait for the **DbContext class names** field to update and for prior migrations to load. Select the **Finish** button. Select the **Close** button when the operation completes.

Modify the one movie that isn't rated *R*:

1. Run the app.
2. Edit the *Mad Max: Beyond Thunderdome* movie.
3. Update the movie rating from `R` to `PG-13`. Save the change.

Note

An alternative to modifying the migration file is to delete the records in the database and rerun the app to reseed the database. The seeding code was modified earlier to supply default values. This approach is useful in cases where the assignment of default values to fields is better controlled or faster for you to implement with C# code during seeding. For more information see the [Delete all database records and reseed the database](#delete-all-database-records-and-reseed-the-database) section at the end of this article.

In the **Terminal** (**Terminal** menu > **New Terminal**), execute the following command to add a migration. The migration name (`AddRatingField`) is an arbitrary description for the migration:

```
dotnet ef migrations add AddRatingField
```

The `dotnet-ef migrations` command:

* Compares the `Movie` model with the `Movie` database table schema.
* Creates code to migrate the database schema to match the model.

Creating the migration doesn't automatically provision a default value for the rating when the database is updated. However, you can manually make a small change to the migration file to apply a default movie rating value, which can be helpful when there are many records that require a default value. In this case, all but one of the *Mad Max* movies is rated *R*, so a default value of "`R`" for the `Rating` column is appropriate. The one movie that doesn't have an *R* rating can be updated later in the running app.

Examine the files in the `Migrations` folder of the project. Open the migration file that adds the movie rating field, which has a file name of `{TIME STAMP}_AddRatingField.cs`, where the `{TIME STAMP}` placeholder is a time stamp (for example, `20240530123755_AddRatingField.cs`).

Find the `AddColumn` block that adds the rating column to the `Movie` table in the database. Modify the last line that applies a default value (`defaultValue`). Change it from an empty string (`""`) to an *R* movie rating (`"R"`):

```
migrationBuilder.AddColumn<string>(
    name: "Rating",
    table: "Movie",
    type: "nvarchar(max)",
    nullable: false,
-   defaultValue: "");
+   defaultValue: "R");
```

Save the migration file.

In the **Terminal**, execute the following command to update the database, which preserves the existing data while it adds the movie rating column with a default value:

```
dotnet ef database update
```

Modify the one movie that isn't rated *R*:

1. Run the app.
2. Edit the *Mad Max: Beyond Thunderdome* movie.
3. Update the movie rating from `R` to `PG-13`. Save the change.

Note

An alternative to modifying the migration file is to delete the records in the database and rerun the app to reseed the database. The seeding code was modified earlier to supply default values. This approach is useful in cases where the assignment of default values to fields is better controlled or faster with C# code during seeding. For more information see the [Delete all database records and reseed the database](#delete-all-database-records-and-reseed-the-database) section at the end of this article.

In a command shell opened to the project's root folder, execute the following command to add a migration. The migration name (`AddRatingField`) is an arbitrary description for the migration:

```
dotnet ef migrations add AddRatingField
```

The `dotnet-ef migrations` command:

* Compares the `Movie` model with the `Movie` database table schema.
* Creates code to migrate the database schema to match the model.

Creating the migration doesn't automatically provision a default value for the rating when the database is updated. However, you can manually make a small change to the migration file to apply a default movie rating value, which can be helpful when there are many records that require a default value. In this case, all but one of the *Mad Max* movies is rated *R*, so a default value of "`R`" for the `Rating` column is appropriate. The one movie that doesn't have an *R* rating can be updated later in the running app.

Examine the files in the `Migrations` folder of the project. Open the migration file that adds the movie rating field, which has a file name of `{TIME STAMP}_AddRatingField.cs`, where the `{TIME STAMP}` placeholder is a time stamp (for example, `20240530123755_AddRatingField.cs`).

Find the `AddColumn` block that adds the rating column to the `Movie` table in the database. Modify the last line that applies a default value (`defaultValue`). Change it from an empty string (`""`) to an *R* movie rating (`"R"`):

```
migrationBuilder.AddColumn<string>(
    name: "Rating",
    table: "Movie",
    type: "nvarchar(max)",
    nullable: false,
-   defaultValue: "");
+   defaultValue: "R");
```

Save the migration file.

Execute the following command to update the database, which preserves the existing data while it adds the movie rating column with a default value:

```
dotnet ef database update
```

Modify the one movie that isn't rated *R*:

1. Run the app.
2. Edit the *Mad Max: Beyond Thunderdome* movie.
3. Update the movie rating from `R` to `PG-13`. Save the change.

Note

An alternative to modifying the migration file is to delete the records in the database and rerun the app to reseed the database. The seeding code was modified earlier to supply default values. This approach is useful in cases where the assignment of default values to fields is better controlled or faster with C# code during seeding. For more information see the [Delete all database records and reseed the database](#delete-all-database-records-and-reseed-the-database) section at the end of this article.

Run the app and verify you can create, edit, and display movies with the new movie rating field.

## Troubleshoot

In the event that the database becomes corrupted, delete the database and use migrations to recreate the database:

1. Select the database in **SQL Server Object Explorer** (SSOX).
2. Right-click on the database, and select **Delete**. *Make sure that you select the correct database in the list.*
3. Check **Close existing connections**.
4. Select **OK**.
5. In **Solution Explorer**, double-click **Connected Services**. In the **Service Dependencies** area, select the ellipsis (`...`) followed by **Update database** in the **SQL Server Express LocalDB** area. Updating the database executes the existing migrations that recreate the database.

1) Delete the database. If your database tooling has a connection to the database, close the tooling first or use the tool's features to close the database connection and delete the database. Consult your tool's documentation for guidance. *Make sure that you select the correct database in the list.*

2) In the **Terminal** (**Terminal** menu > **New Terminal**), execute the following command to run the existing migrations that recreate the database:

   ```
   dotnet ef database update
   ```

1. Delete the database. If your database tooling has a connection to the database, close the tooling first or use the tool's features to close the database connection and delete the database. Consult your tool's documentation for guidance. *Make sure that you select the correct database in the list.*

2. In a command shell opened to the project's root folder, execute the following command to run the existing migrations that recreate the database:

   ```
   dotnet ef database update
   ```

## Delete all database records and reseed the database

*This section describes an alternative process for updating the database with a default value for a new model property without modifying the migration file. Following the guidance in this section isn't necessary if you followed all of the steps in the [Update the database](#update-the-database) section earlier in this article.*

To delete all of the records in the database, use one of the following approaches:

* Run the app and use the delete links in the browser. This approach is reasonably fast when there are only a few records to delete.
* In Visual Studio from **SQL Server Object Explorer** (SSOX), delete the database records. With the database table visible, right-click the table and select **View Data**. When the table opens to show the movie records, select the first record. Hold the `Shift` key and select the last record to select all of the records in the table. With all of the records selected, press the `Delete` key to delete the records. This approach is reasonably fast when there are many records to delete.
* In Visual Studio from SSOX, right-click the database and select **New Query**. A blank SQL file (`.sql`) opens. Paste the following command into the file: `DELETE FROM dbo.Movie;`. Select the green **Execute** triangle to execute the query or press `Ctrl`+`Shift`+`E` on the keyboard. In the **Message** tab, SSOX reports the number of rows affected, deleted in this case, by the query. You can save the query for later use if you wish. Otherwise, close the file without saving it. This approach is especially useful when the table is large and contains hundreds to thousands of records.

- Run the app and use the delete links in the browser. This approach is reasonably fast when there are only a few records to delete.
- Using database tooling†, delete the database records. In most database tools that permit direct manipulation of data, open the database table's data view in the tooling. When the table opens to show the movie records, select the first record. Hold the `Shift` key and select the last record to select all of the records in the database. With all of the records selected, press the `Delete` key to delete the records. You may need to adjust these instructions depending on the database tooling in use. Consult the tool's documentation for more information. This approach is reasonably fast when there are many records to delete.
- Using database tooling†, execute a SQL query against the movie table to delete the table's records. The SQL command is `DELETE FROM dbo.Movie;`. You can save the query for later use if you wish. Otherwise, close the file without saving it. Consult the tool's documentation for the exact steps. This approach is especially useful when the table is large and contains hundreds to thousands of records.

†*database tooling*: There are many free and retail database tools available on the open market that can work with SQL databases. Consult Internet resources to find one suitable for your database and platform.

* Run the app and use the delete links in the browser. This approach is reasonably fast when there are only a few records to delete.
* Using database tooling†, delete the database records. In most database tools that permit direct manipulation of data, open the database table's data view in the tooling. When the table opens to show the movie records, select the first record. Hold the `Shift` key and select the last record to select all of the records in the database. With all of the records selected, press the `Delete` key to delete the records. You may need to adjust these instructions depending on the database tooling in use. Consult the tool's documentation for more information. This approach is reasonably fast when there are many records to delete.
* Using database tooling†, execute a SQL query against the movie table to delete the table's records. The SQL command is `DELETE FROM dbo.Movie;`. You can save the query for later use if you wish. Otherwise, close the file without saving it. Consult the tool's documentation for the exact steps. This approach is especially useful when the table is large and contains hundreds to thousands of records.

†*database tooling*: There are many free and retail database tools available on the open market that can work with SQL databases. Consult Internet resources to find one suitable for your database and platform.

Warning

Use extreme caution when deleting records from a database. Deleting records is permanent without taking additional data loss mitigation steps. Production databases often provision automatic backup copies of data, either instantaneously as the database is modified or periodically, including with off-site copies and permanent physical storage of data.

After deleting all of the records, run the app. The initializer reseeds the database and includes the correct movie ratings for the `Rating` field based on the seeding code.

## Troubleshoot with the completed sample

If you run into a problem while following the tutorial that you can't resolve from the text, compare your code to the completed project in the Blazor samples repository:

[Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)

Select the latest version folder. The sample folder for this tutorial's project is named `BlazorWebAppMovies`.

## Related content

* [Migrations (EF Core documentation)](/en-us/ef/core/managing-schemas/migrations/)
* [Customize migration code](/en-us/ef/core/managing-schemas/migrations/#customize-migration-code)

## Legal

[*Mad Max*, *The Road Warrior*, *Mad Max: Beyond Thunderdome*, *Mad Max: Fury Road*, and *Furiosa: A Mad Max Saga*](https://warnerbros.fandom.com/wiki/Mad_Max_\(franchise\)) are trademarks and copyrights of [Warner Bros. Entertainment](https://www.warnerbros.com/).

## Next steps

[Previous: Add Search](part-6?view=aspnetcore-10.0) [Next: Add interactivity](part-8?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/concurrency
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/concurrency.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Handle concurrency - ASP.NET MVC with EF Core

In earlier tutorials, you learned how to update data. This tutorial shows how to handle conflicts when multiple users update the same entity at the same time.

You'll create web pages that work with the `Department` entity and handle concurrency errors. The following illustrations show the Edit and Delete pages, including some messages that are displayed if a concurrency conflict occurs.

In this tutorial, you:

* Learn about concurrency conflicts
* Add a tracking property
* Create Departments controller and views
* Update Index view
* Update Edit methods
* Update Edit view
* Test concurrency conflicts
* Update the Delete page
* Update Details and Create views

## Prerequisites

* [Update related data](update-related-data?view=aspnetcore-10.0)

## Concurrency conflicts

A concurrency conflict occurs when one user displays an entity's data in order to edit it, and then another user updates the same entity's data before the first user's change is written to the database. If you don't enable the detection of such conflicts, whoever updates the database last overwrites the other user's changes. In many applications, this risk is acceptable: if there are few users, or few updates, or if isn't really critical if some changes are overwritten, the cost of programming for concurrency might outweigh the benefit. In that case, you don't have to configure the application to handle concurrency conflicts.

### Pessimistic concurrency (locking)

If your application does need to prevent accidental data loss in concurrency scenarios, one way to do that is to use database locks. This is called pessimistic concurrency. For example, before you read a row from a database, you request a lock for read-only or for update access. If you lock a row for update access, no other users are allowed to lock the row either for read-only or update access, because they would get a copy of data that's in the process of being changed. If you lock a row for read-only access, others can also lock it for read-only access but not for update.

Managing locks has disadvantages. It can be complex to program. It requires significant database management resources, and it can cause performance problems as the number of users of an application increases. For these reasons, not all database management systems support pessimistic concurrency. Entity Framework Core provides no built-in support for it, and this tutorial doesn't show you how to implement it.

### Optimistic Concurrency

The alternative to pessimistic concurrency is optimistic concurrency. Optimistic concurrency means allowing concurrency conflicts to happen, and then reacting appropriately if they do. For example, Jane visits the Department Edit page and changes the Budget amount for the English department from $350,000.00 to $0.00.

Before Jane clicks **Save**, John visits the same page and changes the Start Date field from 9/1/2007 to 9/1/2013.

Jane clicks **Save** first and sees her change when the browser returns to the Index page.

Then John clicks **Save** on an Edit page that still shows a budget of $350,000.00. What happens next is determined by how you handle concurrency conflicts.

Some of the options include the following:

* You can keep track of which property a user has modified and update only the corresponding columns in the database.

  In the example scenario, no data would be lost, because different properties were updated by the two users. The next time someone browses the English department, they will see both Jane's and John's changes -- a start date of 9/1/2013 and a budget of zero dollars. This method of updating can reduce the number of conflicts that could result in data loss, but it can't avoid data loss if competing changes are made to the same property of an entity. Whether the Entity Framework works this way depends on how you implement your update code. It's often not practical in a web application, because it can require that you maintain large amounts of state in order to keep track of all original property values for an entity as well as new values. Maintaining large amounts of state can affect application performance because it either requires server resources or must be included in the web page itself (for example, in hidden fields) or in a cookie.

* You can let John's change overwrite Jane's change.

  The next time someone browses the English department, they will see 9/1/2013 and the restored $350,000.00 value. This is called a *Client Wins* or *Last in Wins* scenario. (All values from the client take precedence over what's in the data store.) As noted in the introduction to this section, if you don't do any coding for concurrency handling, this will happen automatically.

* You can prevent John's change from being updated in the database.

  Typically, you would display an error message, show him the current state of the data, and allow him to reapply his changes if he still wants to make them. This is called a *Store Wins* scenario. (The data-store values take precedence over the values submitted by the client.) You'll implement the Store Wins scenario in this tutorial. This method ensures that no changes are overwritten without a user being alerted to what's happening.

### Detecting concurrency conflicts

You can resolve conflicts by handling `DbConcurrencyException` exceptions that the Entity Framework throws. In order to know when to throw these exceptions, the Entity Framework must be able to detect conflicts. Therefore, you must configure the database and the data model appropriately. Some options for enabling conflict detection include the following:

* In the database table, include a tracking column that can be used to determine when a row has been changed. You can then configure the Entity Framework to include that column in the Where clause of SQL Update or Delete commands.

  The data type of the tracking column is typically `rowversion`. The `rowversion` value is a sequential number that's incremented each time the row is updated. In an Update or Delete command, the Where clause includes the original value of the tracking column (the original row version) . If the row being updated has been changed by another user, the value in the `rowversion` column is different than the original value, so the Update or Delete statement can't find the row to update because of the Where clause. When the Entity Framework finds that no rows have been updated by the Update or Delete command (that is, when the number of affected rows is zero), it interprets that as a concurrency conflict.

* Configure the Entity Framework to include the original values of every column in the table in the Where clause of Update and Delete commands.

  As in the first option, if anything in the row has changed since the row was first read, the Where clause won't return a row to update, which the Entity Framework interprets as a concurrency conflict. For database tables that have many columns, this approach can result in very large Where clauses, and can require that you maintain large amounts of state. As noted earlier, maintaining large amounts of state can affect application performance. Therefore this approach is generally not recommended, and it isn't the method used in this tutorial.

  If you do want to implement this approach to concurrency, you have to mark all non-primary-key properties in the entity you want to track concurrency for by adding the `ConcurrencyCheck` attribute to them. That change enables the Entity Framework to include all columns in the SQL Where clause of Update and Delete statements.

In the remainder of this tutorial you'll add a `rowversion` tracking property to the Department entity, create a controller and views, and test to verify that everything works correctly.

## Add a tracking property

In `Models/Department.cs`, add a tracking property named RowVersion:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        [Timestamp]
        public byte[] RowVersion { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}
```

The `Timestamp` attribute specifies that this column will be included in the Where clause of Update and Delete commands sent to the database. The attribute is called `Timestamp` because previous versions of SQL Server used a SQL `timestamp` data type before the SQL `rowversion` replaced it. The .NET type for `rowversion` is a byte array.

If you prefer to use the fluent API, you can use the `IsRowVersion()` method (in `Data/SchoolContext.cs`) to specify the tracking property, as shown in the following example:

```
modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsRowVersion();
```

By adding a property you changed the database model, so you need to do another migration.

Save your changes and build the project, and then enter the following commands in the command window:

```
dotnet ef migrations add RowVersion
```

```
dotnet ef database update
```

## Create Departments controller and views

Scaffold a Departments controller and views as you did earlier for Students, Courses, and Instructors.

In the `DepartmentsController.cs` file, change all four occurrences of "FirstMidName" to "FullName" so that the department administrator drop-down lists will contain the full name of the instructor rather than just the last name.

```
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
```

## Update Index view

The scaffolding engine created a `RowVersion` column in the Index view, but that field shouldn't be displayed.

Replace the code in `Views/Departments/Index.cshtml` with the following code.

```
@model IEnumerable<ContosoUniversity.Models.Department>

@{
    ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Budget)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.StartDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Administrator)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

This changes the heading to "Departments", deletes the `RowVersion` column, and shows full name instead of first name for the administrator.

## Update Edit methods

In both the HttpGet `Edit` method and the `Details` method, add `AsNoTracking`. In the HttpGet `Edit` method, add eager loading for the Administrator.

```
var department = await _context.Departments
    .Include(d => d.Administrator)
    .AsNoTracking()
    .FirstOrDefaultAsync(m => m.DepartmentID == id);
```

Replace the existing code for the HttpPost `Edit` method with the following code:

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
    if (id == null)
    {
        return NotFound();
    }

    var departmentToUpdate = await _context.Departments.Include(d => d.Administrator).FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        Department deletedDepartment = new Department();
        await TryUpdateModelAsync(deletedDepartment);
        ModelState.AddModelError(string.Empty,
            "Unable to save changes. The department was deleted by another user.");
        ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
        return View(deletedDepartment);
    }

    _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;

    if (await TryUpdateModelAsync<Department>(
        departmentToUpdate,
        "",
        s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var exceptionEntry = ex.Entries.Single();
            var clientValues = (Department)exceptionEntry.Entity;
            var databaseEntry = exceptionEntry.GetDatabaseValues();
            if (databaseEntry == null)
            {
                ModelState.AddModelError(string.Empty,
                    "Unable to save changes. The department was deleted by another user.");
            }
            else
            {
                var databaseValues = (Department)databaseEntry.ToObject();

                if (databaseValues.Name != clientValues.Name)
                {
                    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
                }
                if (databaseValues.Budget != clientValues.Budget)
                {
                    ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
                }
                if (databaseValues.StartDate != clientValues.StartDate)
                {
                    ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
                }
                if (databaseValues.InstructorID != clientValues.InstructorID)
                {
                    Instructor databaseInstructor = await _context.Instructors.FirstOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
                    ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
                }

                ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                        + "was modified by another user after you got the original value. The "
                        + "edit operation was canceled and the current values in the database "
                        + "have been displayed. If you still want to edit this record, click "
                        + "the Save button again. Otherwise click the Back to List hyperlink.");
                departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
                ModelState.Remove("RowVersion");
            }
        }
    }
    ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}
```

The code begins by trying to read the department to be updated. If the `FirstOrDefaultAsync` method returns null, the department was deleted by another user. In that case the code uses the posted form values to create a `Department` entity so that the Edit page can be redisplayed with an error message. As an alternative, you wouldn't have to re-create the `Department` entity if you display only an error message without redisplaying the department fields.

The view stores the original `RowVersion` value in a hidden field, and this method receives that value in the `rowVersion` parameter. Before you call `SaveChanges`, you have to put that original `RowVersion` property value in the `OriginalValues` collection for the entity.

```
_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
```

Then when the Entity Framework creates a SQL UPDATE command, that command will include a WHERE clause that looks for a row that has the original `RowVersion` value. If no rows are affected by the UPDATE command (no rows have the original `RowVersion` value), the Entity Framework throws a `DbUpdateConcurrencyException` exception.

The code in the catch block for that exception gets the affected Department entity that has the updated values from the `Entries` property on the exception object.

```
var exceptionEntry = ex.Entries.Single();
```

The `Entries` collection will have just one `EntityEntry` object. You can use that object to get the new values entered by the user and the current database values.

```
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
```

The code adds a custom error message for each column that has database values different from what the user entered on the Edit page (only one field is shown here for brevity).

```
var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)
{
    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
```

Finally, the code sets the `RowVersion` value of the `departmentToUpdate` to the new value retrieved from the database. This new `RowVersion` value will be stored in the hidden field when the Edit page is redisplayed, and the next time the user clicks **Save**, only concurrency errors that happen since the redisplay of the Edit page will be caught.

```
departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
```

The `ModelState.Remove` statement is required because `ModelState` has the old `RowVersion` value. In the view, the `ModelState` value for a field takes precedence over the model property values when both are present.

## Update Edit view

In `Views/Departments/Edit.cshtml`, make the following changes:

* Add a hidden field to save the `RowVersion` property value, immediately following the hidden field for the `DepartmentID` property.

* Add a "Select Administrator" option to the drop-down list.

```
@model ContosoUniversity.Models.Department

@{
    ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="DepartmentID" />
            <input type="hidden" asp-for="RowVersion" />
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
                <span asp-validation-for="InstructorID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

## Test concurrency conflicts

Run the app and go to the Departments Index page. Right-click the **Edit** hyperlink for the English department and select **Open in new tab**, then click the **Edit** hyperlink for the English department. The two browser tabs now display the same information.

Change a field in the first browser tab and click **Save**.

The browser shows the Index page with the changed value.

Change a field in the second browser tab.

Click **Save**. You see an error message:

Click **Save** again. The value you entered in the second browser tab is saved. You see the saved values when the Index page appears.

## Update the Delete page

For the Delete page, the Entity Framework detects concurrency conflicts caused by someone else editing the department in a similar manner. When the HttpGet `Delete` method displays the confirmation view, the view includes the original `RowVersion` value in a hidden field. That value is then available to the HttpPost `Delete` method that's called when the user confirms the deletion. When the Entity Framework creates the SQL DELETE command, it includes a WHERE clause with the original `RowVersion` value. If the command results in zero rows affected (meaning the row was changed after the Delete confirmation page was displayed), a concurrency exception is thrown, and the HttpGet `Delete` method is called with an error flag set to true in order to redisplay the confirmation page with an error message. It's also possible that zero rows were affected because the row was deleted by another user, so in that case no error message is displayed.

### Update the Delete methods in the Departments controller

In `DepartmentsController.cs`, replace the HttpGet `Delete` method with the following code:

```
public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
{
    if (id == null)
    {
        return NotFound();
    }

    var department = await _context.Departments
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.DepartmentID == id);
    if (department == null)
    {
        if (concurrencyError.GetValueOrDefault())
        {
            return RedirectToAction(nameof(Index));
        }
        return NotFound();
    }

    if (concurrencyError.GetValueOrDefault())
    {
        ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
            + "was modified by another user after you got the original values. "
            + "The delete operation was canceled and the current values in the "
            + "database have been displayed. If you still want to delete this "
            + "record, click the Delete button again. Otherwise "
            + "click the Back to List hyperlink.";
    }

    return View(department);
}
```

The method accepts an optional parameter that indicates whether the page is being redisplayed after a concurrency error. If this flag is true and the department specified no longer exists, it was deleted by another user. In that case, the code redirects to the Index page. If this flag is true and the department does exist, it was changed by another user. In that case, the code sends an error message to the view using `ViewData`.

Replace the code in the HttpPost `Delete` method (named `DeleteConfirmed`) with the following code:

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
    try
    {
        if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
        {
            _context.Departments.Remove(department);
            await _context.SaveChangesAsync();
        }
        return RedirectToAction(nameof(Index));
    }
    catch (DbUpdateConcurrencyException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
    }
}
```

In the scaffolded code that you just replaced, this method accepted only a record ID:

```
public async Task<IActionResult> DeleteConfirmed(int id)
```

You've changed this parameter to a `Department` entity instance created by the model binder. This gives EF access to the RowVers\`ion property value in addition to the record key.

```
public async Task<IActionResult> Delete(Department department)
```

You have also changed the action method name from `DeleteConfirmed` to `Delete`. The scaffolded code used the name `DeleteConfirmed` to give the HttpPost method a unique signature. (The CLR requires overloaded methods to have different method parameters.) Now that the signatures are unique, you can stick with the MVC convention and use the same name for the HttpPost and HttpGet delete methods.

If the department is already deleted, the `AnyAsync` method returns false and the application just goes back to the Index method.

If a concurrency error is caught, the code redisplays the Delete confirmation page and provides a flag that indicates it should display a concurrency error message.

### Update the Delete view

In `Views/Departments/Delete.cshtml`, replace the scaffolded code with the following code that adds an error message field and hidden fields for the DepartmentID and RowVersion properties. The changes are highlighted.

```
@model ContosoUniversity.Models.Department

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <input type="hidden" asp-for="DepartmentID" />
        <input type="hidden" asp-for="RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-action="Index">Back to List</a>
        </div>
    </form>
</div>
```

This makes the following changes:

* Adds an error message between the `h2` and `h3` headings.

* Replaces FirstMidName with FullName in the **Administrator** field.

* Removes the RowVersion field.

* Adds a hidden field for the `RowVersion` property.

Run the app and go to the Departments Index page. Right-click the **Delete** hyperlink for the English department and select **Open in new tab**, then in the first tab click the **Edit** hyperlink for the English department.

In the first window, change one of the values, and click **Save**:

In the second tab, click **Delete**. You see the concurrency error message, and the Department values are refreshed with what's currently in the database.

If you click **Delete** again, you're redirected to the Index page, which shows that the department has been deleted.

## Update Details and Create views

You can optionally clean up scaffolded code in the Details and Create views.

Replace the code in `Views/Departments/Details.cshtml` to delete the RowVersion column and show the full name of the Administrator.

```
@model ContosoUniversity.Models.Department

@{
    ViewData["Title"] = "Details";
}

<h2>Details</h2>

<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>
```

Replace the code in `Views/Departments/Create.cshtml` to add a Select option to the drop-down list.

```
@model ContosoUniversity.Models.Department

@{
    ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

## Get the code

[Download or view the completed application.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final)

## Related content

For more information about how to handle concurrency in EF Core, see [Concurrency conflicts](/en-us/ef/core/saving/concurrency).

## Next steps

In this tutorial, you:

* Learned about concurrency conflicts
* Added a tracking property
* Created Departments controller and views
* Updated Index view
* Updated Edit methods
* Updated Edit view
* Tested concurrency conflicts
* Updated the Delete page
* Updated Details and Create views

Advance to the next tutorial to learn how to implement table-per-hierarchy inheritance for the Instructor and Student entities.

[Next: Implement table-per-hierarchy inheritance](inheritance?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/authentication
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/authentication.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate ASP.NET Framework Authentication to ASP.NET Core

Authentication is a critical component of web applications, handling user identity verification across HTTP requests. When migrating from ASP.NET Framework to ASP.NET Core, authentication presents unique challenges because the two frameworks handle authentication very differently.

For general information about authentication in ASP.NET Core, see [Overview of ASP.NET Core Authentication](../../../security/authentication/?view=aspnetcore-10.0). For information about authorization, see [Introduction to authorization in ASP.NET Core](../../../security/authorization/introduction?view=aspnetcore-10.0).

## Why authentication migration is complex

ASP.NET Framework and ASP.NET Core have fundamentally different approaches to authentication:

* **ASP.NET Framework** provides built-in authentication modules with automatic configuration and tight integration with System.Web
* **ASP.NET Core** uses a middleware-based approach with explicit configuration and dependency injection

These differences mean you can't simply move your authentication code from Framework to Core without changes.

## Authentication types and migration challenges

Different authentication types present varying levels of migration complexity:

### Cookie-based authentication

* **ASP.NET Framework**: Uses `Microsoft.Owin` cookie authentication middleware
* **ASP.NET Core**: Uses `CookieAuthentication` middleware with different configuration
* **Migration challenge**: Cookie format, encryption keys, and configuration differences

### JWT bearer tokens

* **ASP.NET Framework**: Often handled through custom modules or OWIN middleware
* **ASP.NET Core**: Native support through `Microsoft.AspNetCore.Authentication.JwtBearer`
* **Migration challenge**: Token validation parameter differences and middleware registration

### Windows authentication

* **ASP.NET Framework**: Built-in IIS integration
* **ASP.NET Core**: Requires explicit configuration and may need hosting model changes
* **Migration challenge**: Server configuration and authentication flow differences

### Forms authentication

* **ASP.NET Framework**: Built-in `System.Web.Security.FormsAuthentication`
* **ASP.NET Core**: No direct equivalent, requires migration to cookie authentication
* **Migration challenge**: Complete authentication system rewrite needed

### Custom authentication

* **ASP.NET Framework**: Custom modules implementing `IHttpModule`
* **ASP.NET Core**: Custom middleware or authentication handlers
* **Migration challenge**: Complete architecture change from modules to middleware

## Migration strategies overview

You have three main approaches for handling authentication during migration:

1. **Complete rewrite** - Rewrite all authentication code to use ASP.NET Core's native authentication
2. **Remote authentication** - Use System.Web adapters to delegate authentication to the ASP.NET Framework app
3. **Shared cookie authentication** - Share authentication cookies between Framework and Core applications (for OWIN-based scenarios)

For most applications, migrating to native ASP.NET Core authentication provides the best performance and maintainability. However, larger applications or those with complex authentication requirements may benefit from an incremental approach using the System.Web adapters.

## Choose your migration approach

You have three main options for migrating authentication from ASP.NET Framework to ASP.NET Core. Your choice depends on your authentication type, migration timeline, whether you need to run both applications simultaneously, and how much code you're willing to rewrite.

### Quick decision guide

**Answer these questions to choose your approach:**

1. **Are you doing a complete rewrite or incremental migration?**

   * Complete rewrite → [Complete rewrite to ASP.NET Core authentication](#complete-rewrite-to-aspnet-core-authentication)
   * Incremental migration → Continue to question 2

2. **What type of authentication does your ASP.NET Framework app use?**

   * OWIN cookie authentication → Continue to question 3
   * Forms authentication, JWT bearer tokens, Windows auth, or Custom authentication → [Remote authentication](#remote-authentication)

3. **Do both your ASP.NET Framework and ASP.NET Core apps need to access the same authentication state?**

   * Yes, shared authentication needed → Continue to question 4
   * No, separate authentication is fine → [Complete rewrite to ASP.NET Core authentication](#complete-rewrite-to-aspnet-core-authentication)

4. **Can you configure matching data protection settings between both apps?**

   * Yes → [Shared cookie authentication](#shared-cookie-authentication) (see Alternatives section, best performance)
   * No or unsure → [Remote authentication](#remote-authentication)

### Migration approaches comparison

| Approach                                                                | Code Changes                  | Performance | Auth Sharing | When to Use                                                           |
| ----------------------------------------------------------------------- | ----------------------------- | ----------- | ------------ | --------------------------------------------------------------------- |
| **[Complete rewrite](#complete-rewrite-to-aspnet-core-authentication)** | High - Rewrite all auth code  | Best        | None         | Complete rewrites, performance-critical apps, non-OWIN auth           |
| **[Remote authentication](#remote-authentication)**                     | Low - Keep existing patterns  | Fair        | Full         | Incremental migrations, complex auth, Windows auth                    |
| **[Shared cookies](#shared-cookie-authentication)**                     | Medium - Update configuration | Good        | Full         | OWIN cookie auth, performance-critical shared auth (see Alternatives) |

## Complete rewrite to ASP.NET Core authentication

Choose this approach when you're performing a complete migration, have non-OWIN authentication, or want the best performance and maintainability.

ASP.NET Core provides comprehensive authentication support with high performance and extensive customization options. This approach requires rewriting authentication code but offers the most benefits in the long term.

### Complete rewrite pros and cons

| Pros                                        | Cons                                           |
| ------------------------------------------- | ---------------------------------------------- |
| Best performance and security               | Requires rewriting all authentication code     |
| Native ASP.NET Core implementation          | No automatic migration path                    |
| Full control over authentication flow       | Learning curve for new authentication patterns |
| No additional dependencies                  | Breaking change from Framework patterns        |
| Access to latest ASP.NET Core auth features | Potential downtime during migration            |

### Migration considerations

When migrating to native ASP.NET Core authentication:

**Choose based on your Framework authentication type:**

* **Forms authentication** → Migrate to [Cookie authentication](../../../security/authentication/cookie?view=aspnetcore-10.0)
* **JWT bearer tokens** → Migrate to [JWT bearer authentication](../../../security/authentication/configure-jwt-bearer-authentication?view=aspnetcore-10.0)
* **Windows authentication** → Use [Windows authentication](../../../security/authentication/windowsauth?view=aspnetcore-10.0)
* **OWIN OAuth providers** → Use corresponding ASP.NET Core OAuth providers
* **Custom authentication** → Implement [custom authentication handlers](../../../security/authentication/?view=aspnetcore-10.0#authentication-handler)

**Code changes required:**

* Replace `HttpContext.User` access patterns (mostly compatible)
* Update authentication middleware registration in `Program.cs`
* Migrate custom authentication logic to ASP.NET Core patterns
* Update authorization attributes and policies

**When to choose this approach:**

* You can afford to rewrite authentication-related code
* Performance is a top priority
* You're not sharing authentication state with legacy applications
* You want to eliminate System.Web dependencies completely
* Your Framework app uses Forms authentication, custom modules, or JWT tokens

## Remote authentication

Note

This makes use of the [System.Web Adapters](../inc/systemweb-adapters?view=aspnetcore-10.0) to simplify migration.

Choose this approach when you need to share authentication between your ASP.NET Framework and ASP.NET Core applications during incremental migration, or when you have complex authentication that's difficult to migrate.

The System.Web adapters' remote authentication feature allows an ASP.NET Core app to determine a user's identity by deferring to an ASP.NET app. This enables gradual migration while maintaining a single authentication system.

### How remote authentication works

1. When requests are processed by the ASP.NET Core app, if remote app authentication is the default scheme or specified by the request's endpoint, the `RemoteAuthenticationAuthHandler` will attempt to authenticate the user.
2. The handler makes an HTTP request to the ASP.NET app's authenticate endpoint, forwarding configured headers from the current request (by default, `Authorization` and `Cookie` headers).
3. The ASP.NET app processes the authentication and returns either a serialized `ClaimsPrincipal` or an HTTP status code indicating failure.
4. The ASP.NET Core app uses the result to establish the user's identity or handle authentication challenges.

### Remote authentication pros and cons

| Pros                               | Cons                             |
| ---------------------------------- | -------------------------------- |
| Minimal code changes required      | Additional HTTP request overhead |
| Works with any Framework auth type | Network dependency between apps  |
| Gradual migration capability       | More complex debugging           |
| Preserves existing auth logic      | Requires both apps to be running |
| Handles complex auth scenarios     | Limited Windows auth support     |

### When to choose remote authentication

**Choose Remote Authentication when:**

* Your ASP.NET app doesn't use `Microsoft.Owin` cookie authentication
* You want a flexible solution that works with various authentication mechanisms
* You need to gradually migrate authentication logic to ASP.NET Core
* You have complex custom authentication that's difficult to rewrite
* You're doing an incremental migration and need shared authentication state

### Remote authentication configuration

There are just a few small code changes needed to enable remote authentication in a solution that's already set up according to the [Getting Started](../start?view=aspnetcore-10.0).

First, follow the [remote app setup](../inc/remote-app-setup?view=aspnetcore-10.0) instructions to connect the ASP.NET Core and ASP.NET apps. Then, there are just a couple extra extension methods to call to enable remote app authentication.

#### ASP.NET app configuration

The ASP.NET app needs to be configured to add the authentication endpoint. Adding the authentication endpoint is done by calling the `AddAuthenticationServer` extension method to set up the HTTP module that watches for requests to the authentication endpoint. Note that remote authentication scenarios typically want to add proxy support as well, so that any authentication related redirects correctly route to the ASP.NET Core app rather than the ASP.NET one.

```
HttpApplicationHost.RegisterHost(builder =>
{
    builder.AddSystemWebAdapters()
        .AddProxySupport(options => options.UseForwardedHeaders = true)
        .AddRemoteAppServer(options =>
        {
            options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"];
        })
        .AddAuthenticationServer();
});
```

#### ASP.NET Core app configuration

Next, the ASP.NET Core app needs to be configured to enable the authentication handler that will authenticate users by making an HTTP request to the ASP.NET app. Again, this is done by calling `AddAuthenticationClient` when registering System.Web adapters services:

```
builder.Services.AddSystemWebAdapters()
    .AddRemoteAppClient(options =>
    {
        options.RemoteAppUrl = new Uri(builder.Configuration
            ["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);
        options.ApiKey = builder.Configuration["RemoteAppApiKey"];
    })
    .AddAuthenticationClient(true);
```

The boolean that is passed to the `AddAuthenticationClient` call specifies whether remote app authentication should be the default authentication scheme. Passing `true` will cause the user to be authenticated via remote app authentication for all requests, whereas passing `false` means that the user will only be authenticated with remote app authentication if the remote app scheme is specifically requested (with `[Authorize(AuthenticationSchemes = RemoteAppAuthenticationDefaults.AuthenticationScheme)]` on a controller or action method, for example). Passing false for this parameter has the advantage of only making HTTP requests to the original ASP.NET app for authentication for endpoints that require remote app authentication but has the disadvantage of requiring annotating all such endpoints to indicate that they will use remote app auth.

When using Aspire, the configuration will be done via environment variables and are set by the AppHost. To enable remote session, the option must be enabled:

```
...

var coreApp = builder.AddProject<Projects.AuthRemoteIdentityCore>("core")
    .WithHttpHealthCheck()
    .WaitFor(frameworkApp)
    .WithIncrementalMigrationFallback(frameworkApp, options => options.RemoteAuthentication = RemoteAuthentication.DefaultScheme);

...
```

Once this is done, it will be automatically hooked up in both the framework and core applications.

#### Remote authentication with YARP fallback

When using remote authentication with YARP-based fallback to the ASP.NET Framework app, make sure that fallback requests don't invoke remote authentication. If remote authentication runs during fallback, the app makes unnecessary extra calls back to the Framework app and can lead to confusing behavior.

To avoid running remote authentication for fallback requests, use one of these approaches:

* Use routing short-circuit metadata on the YARP fallback route so that the route bypasses the rest of the middleware pipeline. For example, call `ShortCircuit` on the fallback endpoint as shown in the remote app setup guidance and in [Short-circuit middleware after routing](../../../fundamentals/routing?view=aspnetcore-10.0#short-circuit-middleware-after-routing).
* Don't use remote authentication as the default authentication scheme. Instead, configure remote authentication only for the endpoints that need it. For example, pass `false` to `AddAuthenticationClient` and specify the remote authentication scheme explicitly on those endpoints.

#### Using Remote Authentication with Specific Endpoints

When you set the default scheme to `false`, you can specify remote authentication for specific controllers or actions:

```
[Authorize(AuthenticationSchemes = RemoteAppAuthenticationDefaults.AuthenticationScheme)]
public class SecureController : Controller
{
    // This controller uses remote authentication
    public IActionResult Index()
    {
        return View();
    }
}

// Or on specific actions
public class HomeController : Controller
{
    [Authorize(AuthenticationSchemes = RemoteAppAuthenticationDefaults.AuthenticationScheme)]
    public IActionResult SecureAction()
    {
        return View();
    }
}
```

#### Implementing Custom Authentication Result Processors

You can implement custom processors to modify authentication results before they are used:

```
public class CustomAuthResultProcessor : IRemoteAuthenticationResultProcessor
{
    public Task ProcessAsync(RemoteAuthenticationResult result, HttpContext context)
    {
        // Custom logic to process authentication results
        if (result.Headers.ContainsKey("Location"))
        {
            // Modify redirect URLs or other logic
        }
        
        return Task.CompletedTask;
    }
}

// Register the custom processor
builder.Services.AddScoped<IRemoteAuthenticationResultProcessor, CustomAuthResultProcessor>();
```

Finally, if the ASP.NET Core app didn't previously include authentication middleware, that will need to be enabled (after routing middleware, but before authorization middleware). For more information about middleware ordering, see [ASP.NET Core middleware](../../../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order):

```
app.UseAuthentication();
```

#### Security Considerations

When implementing remote authentication, consider the following security aspects:

* **API Key Security**: The API key used for communication between the ASP.NET Core and ASP.NET apps should be stored securely using [configuration providers](../../../fundamentals/configuration/?view=aspnetcore-10.0) and not hardcoded.
* **Network Security**: Communication between the apps should occur over secure channels (HTTPS) in production environments.
* **Header Forwarding**: Be cautious about which headers you forward to avoid exposing sensitive information. Only forward headers that are necessary for authentication.
* **Endpoint Protection**: The authentication endpoint on the ASP.NET app should only be accessible to the ASP.NET Core app, not external clients.

#### Troubleshooting

Common issues when configuring remote authentication:

* **Authentication failures**: Verify that the API keys match between both applications and that the authentication endpoint path is correctly configured.
* **Redirect loops**: Ensure that the `RedirectUrlProcessor` is properly configured to redirect to the ASP.NET Core app rather than the ASP.NET app.
* **Missing claims**: Verify that the ASP.NET app is properly serializing the `ClaimsPrincipal` and that all required claims are included.

#### Design

1. When requests are processed by the ASP.NET Core app, if remote app authentication is the default scheme or specified by the request's endpoint, the `RemoteAuthenticationAuthHandler` will attempt to authenticate the user.
   1. The handler will make an HTTP request to the ASP.NET app's authenticate endpoint. It will copy configured headers from the current request onto this new one in order to forward auth-relevant data. As mentioned above, default behavior is to copy the `Authorization` and `Cookie` headers. The API key header is also added for security purposes.

2. The ASP.NET app will serve requests sent to the authenticate endpoint. As long as the API keys match, the ASP.NET app will return either the current user's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) serialized into the response body or it will return an HTTP status code (like 401 or 302) and response headers indicating failure.

3. When the ASP.NET Core app's `RemoteAuthenticationAuthHandler` receives the response from the ASP.NET app:

   1. If a ClaimsPrincipal was successfully returned, the auth handler will deserialize it and use it as the current user's identity.
   2. If a ClaimsPrincipal was not successfully returned, the handler will store the result and if authentication is challenged (because the user is accessing a protected resource, for example), the request's response will be updated with the status code and selected response headers from the response from the authenticate endpoint. This enables challenge responses (like redirects to a login page) to be propagated to end users.
      1. Because results from the ASP.NET app's authenticate endpoint may include data specific to that endpoint, users can register `IRemoteAuthenticationResultProcessor` implementations with the ASP.NET Core app which will run on any authentication results before they are used. As an example, the one built-in `IRemoteAuthenticationResultProcessor` is `RedirectUrlProcessor` which looks for `Location` response headers returned from the authenticate endpoint and ensures that they redirect back to the host of the ASP.NET Core app and not the ASP.NET app directly.

#### Known limitations

This remote authentication approach has a couple known limitations:

1. **Windows Authentication**: Because Windows authentication depends on a handle to a Windows identity, Windows authentication is not supported by this feature. Future work is planned to explore how shared Windows authentication might work. See [dotnet/systemweb-adapters#246](https://github.com/dotnet/systemweb-adapters/issues/246) for more information. For Windows authentication scenarios, consider using [Configure Windows Authentication in ASP.NET Core](../../../security/authentication/windowsauth?view=aspnetcore-10.0) in your ASP.NET Core application instead.
2. **User Management Actions**: This feature allows the ASP.NET Core app to make use of an identity authenticated by the ASP.NET app, but all actions related to users (logging on, logging off, etc.) still need to be routed through the ASP.NET app.
3. **Performance Considerations**: Each authentication request requires an HTTP call to the ASP.NET app, which may impact performance. Consider using shared cookie authentication (described in the Alternatives section) if performance is critical.

## Shared cookie authentication

If authentication in the ASP.NET app is done using `Microsoft.Owin` cookie authentication middleware, an alternative solution to remote authentication is to configure the ASP.NET and ASP.NET Core apps so that they are able to share an authentication cookie.

### How shared cookies work

Sharing an authentication cookie enables:

* Both apps to determine the user identity from the same cookie.
* Signing in or out of one app signs the user in or out of the other app.

Both applications are configured to:

* Use the same cookie name and domain settings
* Share data protection keys for cookie encryption/decryption
* Use compatible cookie authentication middleware

This allows users authenticated in one app to be automatically authenticated in the other app when they make requests.

### Shared cookie authentication pros and cons

| Pros                                  | Cons                                    |
| ------------------------------------- | --------------------------------------- |
| Best performance for shared auth      | Only works with OWIN cookie auth        |
| No additional HTTP requests           | Requires matching data protection setup |
| Both apps can handle sign-in/sign-out | More complex initial configuration      |
| Seamless user experience              | Limited to cookie-based authentication  |
| Lower network overhead                | Requires coordination of deployments    |

### Authentication limitations with shared cookies

Note that because signing in typically depends on a specific database, not all authentication functionality will work in both apps:

* Users should sign in through only one of the apps, either the ASP.NET or ASP.NET Core app, whichever the database is setup to work with.
* Both apps are able to see the users' identity and claims.
* Both apps are able to sign the user out.

Details on how to configure sharing auth cookies between ASP.NET and ASP.NET Core apps are available in [cookie sharing documentation](../../../security/cookie-sharing?view=aspnetcore-10.0). For more information about cookie authentication in ASP.NET Core, see [Use cookie authentication without ASP.NET Core Identity](../../../security/authentication/cookie?view=aspnetcore-10.0). For guidance on configuring `<machineKey>` and data protection when sharing protected values between Framework and Core apps, see [ASP.NET machineKey to ASP.NET Core migration](machine-key?view=aspnetcore-10.0).

The following samples in the [System.Web adapters](https://github.com/dotnet/systemweb-adapters) GitHub repo demonstrates remote app authentication with shared cookie configuration enabling both apps to sign users in and out:

* [ASP.NET app](https://github.com/dotnet/systemweb-adapters/tree/main/samples/AuthRemoteIdentity/AuthRemoteIdentityFramework)
* [ASP.NET Core app](https://github.com/dotnet/systemweb-adapters/tree/main/samples/AuthRemoteIdentity/AuthRemoteIdentityCore)

### When to choose shared cookie authentication

**Choose Shared Cookie Authentication when:**

* Your ASP.NET app already uses `Microsoft.Owin` cookie authentication
* You can configure matching data protection settings between both apps
* Performance is critical and you want to minimize HTTP requests
* You want both apps to handle user sign-in/sign-out operations
* You're comfortable managing shared encryption keys

Sharing authentication is a good option if both the following are true:

* The ASP.NET app is already using `Microsoft.Owin` cookie authentication.
* It's possible to update the ASP.NET app and ASP.NET Core apps to use matching data protection settings. Matching shared data protection settings includes a shared file path, Redis cache, or Azure Blob Storage for storing data protection keys. For more information, see [ASP.NET Core Data Protection Overview](../../../security/data-protection/introduction?view=aspnetcore-10.0).

For other scenarios, the remote authentication approach described previously in this doc is more flexible and is probably a better fit.

## See also

* [Overview of ASP.NET Core Authentication](../../../security/authentication/?view=aspnetcore-10.0)
* [Introduction to authorization in ASP.NET Core](../../../security/authorization/introduction?view=aspnetcore-10.0)
* [Share authentication cookies among ASP.NET apps](../../../security/cookie-sharing?view=aspnetcore-10.0)
* [Use cookie authentication without ASP.NET Core Identity](../../../security/authentication/cookie?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../../../fundamentals/middleware/?view=aspnetcore-10.0)
* [Migrate from ASP.NET Framework to ASP.NET Core](../?view=aspnetcore-10.0)
* [Remote app setup](../inc/remote-app-setup?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/basics
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/basics.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC services with C\#

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article provides an overview of the concepts required to write [gRPC](https://grpc.io/docs/guides/) apps in C#. The information presented applies to both [C-core](https://grpc.io/blog/grpc-stacks)-based and ASP.NET Core-based gRPC apps.

## Review the .proto file

gRPC uses a contract-first approach to API development. Protocol buffers (protobuf) are used as the Interface Definition Language (IDL) by default. The *.proto* file contains:

* The definition of the gRPC service.
* The messages sent between clients and servers.

For more information on the syntax of protobuf files, see [Create Protobuf messages for .NET apps](protobuf?view=aspnetcore-10.0).

Consider the *greet.proto* file used in the tutorial, [Create a gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0):

* The file defines a `Greeter` service.
* The `Greeter` service defines a `SayHello` call.
* The `SayHello` call sends a `HelloRequest` message and receives a `HelloReply` message.

The file contains the following code:

```
syntax = "proto3";

option csharp_namespace = "GrpcGreeter";

package greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}
```

## Add a .proto file to a C# app

The *.proto* file is included in a project by adding it to the `<Protobuf>` item group:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
```

By default, a `<Protobuf>` reference generates a concrete client and a service base class. The reference element's `GrpcServices` attribute can be used to limit C# asset generation. Valid `GrpcServices` options are:

* `Both` (default when not present)
* `Server`
* `Client`
* `None`

## C# tooling support for .proto files

The tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) is required to generate the C# assets from *.proto* files.

The generated assets (files) have the following characteristics:

* They're generated on an as-needed basis each time the project is built.
* They aren't added to the project or checked into source control.
* They're a build artifact contained in the *obj* directory.

Both the server and client projects require this package. The `Grpc.AspNetCore` metapackage includes a reference to `Grpc.Tools`. Server projects can add `Grpc.AspNetCore` by using the Package Manager in Visual Studio or by adding a `<PackageReference>` to the project file:

```
<PackageReference Include="Grpc.AspNetCore" Version="2.32.0" />
```

Client projects should directly reference `Grpc.Tools` alongside the other packages required to use the gRPC client. The tooling package isn't required at runtime, so the dependency is marked with the `PrivateAssets="All"` setting:

```
<PackageReference Include="Google.Protobuf" Version="3.18.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.52.0" />
<PackageReference Include="Grpc.Tools" Version="2.40.0">
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  <PrivateAssets>all</PrivateAssets>
</PackageReference>
```

## Generated C# assets

The tooling package generates the C# types representing the messages defined in the included *.proto* files.

For server-side assets, an abstract service base type is generated. The base type contains the definitions of all the gRPC calls contained in the *.proto* file. Create a concrete service implementation that derives from this base type and implements the logic for the gRPC calls. For the *greet.proto* file used in a [previous example](#review-the-proto-file), an abstract `GreeterBase` type that contains a virtual `SayHello` method is generated. A concrete implementation `GreeterService` overrides the method and implements the logic handling the gRPC call.

```
public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;
    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}
```

For client-side assets, a concrete client type is generated. The gRPC calls in the *.proto* file are translated into methods on the concrete type, which can be called. For the *greet.proto* file used in a [previous example](#review-the-proto-file), a concrete `GreeterClient` type is generated. Call `GreeterClient.SayHelloAsync` to initiate a gRPC call to the server.

```
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
```

By default, server and client assets are generated for each *.proto* file included in the `<Protobuf>` item group. To ensure only the server assets are generated in a server project, the `GrpcServices` attribute is set to `Server`.

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
```

Similarly, the attribute is set to `Client` in client projects.

## Related content

* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0)
* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)

This document outlines the concepts needed to write [gRPC](https://grpc.io/docs/guides/) apps in C#. The topics covered here apply to both [C-core](https://grpc.io/blog/grpc-stacks)-based and ASP.NET Core-based gRPC apps.

## proto file

gRPC uses a contract-first approach to API development. Protocol buffers (protobuf) are used as the Interface Definition Language (IDL) by default. The `.proto` file contains:

* The definition of the gRPC service.
* The messages sent between clients and servers.

For more information on the syntax of protobuf files, see [Create Protobuf messages for .NET apps](protobuf?view=aspnetcore-10.0).

For example, consider the *greet.proto* file used in [Get started with gRPC service](../tutorials/grpc/grpc-start?view=aspnetcore-10.0):

* Defines a `Greeter` service.
* The `Greeter` service defines a `SayHello` call.
* `SayHello` sends a `HelloRequest` message and receives a `HelloReply` message:

```
syntax = "proto3";

option csharp_namespace = "GrpcGreeter";

package greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}
```

## Add a `.proto` file to a C# app

The `.proto` file is included in a project by adding it to the `<Protobuf>` item group:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
```

By default, a `<Protobuf>` reference generates a concrete client and a service base class. The reference element's `GrpcServices` attribute can be used to limit C# asset generation. Valid `GrpcServices` options are:

* `Both` (default when not present)
* `Server`
* `Client`
* `None`

## C# Tooling support for `.proto` files

The tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) is required to generate the C# assets from `.proto` files. The generated assets (files):

* Are generated on an as-needed basis each time the project is built.
* Aren't added to the project or checked into source control.
* Are a build artifact contained in the *obj* directory.

This package is required by both the server and client projects. The `Grpc.AspNetCore` metapackage includes a reference to `Grpc.Tools`. Server projects can add `Grpc.AspNetCore` using the Package Manager in Visual Studio or by adding a `<PackageReference>` to the project file:

```
<PackageReference Include="Grpc.AspNetCore" Version="2.28.0" />
```

Client projects should directly reference `Grpc.Tools` alongside the other packages required to use the gRPC client. The tooling package isn't required at runtime, so the dependency is marked with `PrivateAssets="All"`:

```
<PackageReference Include="Google.Protobuf" Version="3.11.4" />
<PackageReference Include="Grpc.Net.Client" Version="2.52.0" />
<PackageReference Include="Grpc.Tools" Version="2.28.1">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
```

## Generated C# assets

The tooling package generates the C# types representing the messages defined in the included `.proto` files.

For server-side assets, an abstract service base type is generated. The base type contains the definitions of all the gRPC calls contained in the `.proto` file. Create a concrete service implementation that derives from this base type and implements the logic for the gRPC calls. For the `greet.proto`, the example described previously, an abstract `GreeterBase` type that contains a virtual `SayHello` method is generated. A concrete implementation `GreeterService` overrides the method and implements the logic handling the gRPC call.

```
public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;
    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}
```

For client-side assets, a concrete client type is generated. The gRPC calls in the `.proto` file are translated into methods on the concrete type, which can be called. For the `greet.proto`, the example described previously, a concrete `GreeterClient` type is generated. Call `GreeterClient.SayHelloAsync` to initiate a gRPC call to the server.

```
static async Task Main(string[] args)
{
    // The port number(5001) must match the port of the gRPC server.
    using var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greeter.GreeterClient(channel);
    var reply = await client.SayHelloAsync(
                      new HelloRequest { Name = "GreeterClient" });
    Console.WriteLine("Greeting: " + reply.Message);
    Console.WriteLine("Press any key to exit...");
    Console.ReadKey();
}
```

By default, server and client assets are generated for each `.proto` file included in the `<Protobuf>` item group. To ensure only the server assets are generated in a server project, the `GrpcServices` attribute is set to `Server`.

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
```

Similarly, the attribute is set to `Client` in client projects.

## Related content

* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0)
* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/spa/angular
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/spa/angular.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use the Angular project template with ASP.NET Core

To get started with an Angular project, follow the [Create an ASP.NET Core app with Angular](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-angular) tutorial in the Visual Studio documentation.

The ASP.NET Core with Angular project template provides a convenient starting point for ASP.NET Core apps using Angular and the Angular CLI to implement a rich, client-side user interface (UI).

The project template is equivalent to creating both an ASP.NET Core project to act as a web API and an Angular CLI project to act as a UI. This project combination offers the convenience of hosting both projects in a single ASP.NET Core project that can be built and published as a single unit.

The project template isn't meant for server-side rendering (SSR).

## Create a new app

Create a new project from a command prompt using the command `dotnet new angular` in an empty directory. For example, the following commands create the app in a `my-new-app` directory and switch to that directory:

```
dotnet new angular -o my-new-app
cd my-new-app
```

Run the app from either Visual Studio or the .NET CLI:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

Open the generated `.csproj` file, and run the app as normal from there.

The build process restores npm dependencies on the first run, which can take several minutes. Subsequent builds are much faster.

Ensure you have an environment variable called `ASPNETCORE_ENVIRONMENT` with a value of `Development`. On Windows (in non-PowerShell prompts), run `SET ASPNETCORE_ENVIRONMENT=Development`. On Linux or macOS, run `export ASPNETCORE_ENVIRONMENT=Development`.

Run [dotnet build](/en-us/dotnet/core/tools/dotnet-build) to verify the app builds correctly. On the first run, the build process restores npm dependencies, which can take several minutes. Subsequent builds are much faster.

Run [dotnet run](/en-us/dotnet/core/tools/dotnet-run) to start the app.

The project template creates an ASP.NET Core app and an Angular app. The ASP.NET Core app is intended to be used for data access, authorization, and other server-side concerns. The Angular app, residing in the `ClientApp` subdirectory, is intended to be used for all UI concerns.

## Add pages, images, styles, and modules

The `ClientApp` directory contains a standard Angular CLI app. See the official [Angular documentation](https://angular.dev) for more information.

There are slight differences between the Angular app created by this template and the one created by Angular CLI itself (via `ng new`); however, the app's capabilities are unchanged. The app created by the template contains a [Bootstrap](https://getbootstrap.com/)-based layout and a basic routing example.

## Run ng commands

In a command prompt, switch to the `ClientApp` subdirectory:

```
cd ClientApp
```

If you have the `ng` tool installed globally, you can run any of its commands. For example, you can run `ng lint`, `ng test`, or any of the other [Angular CLI commands](https://angular.dev/cli). There's no need to run `ng serve` though, because your ASP.NET Core app deals with serving both server-side and client-side parts of your app. Internally, it uses `ng serve` in development.

If you don't have the `ng` tool installed, run `npm run ng` instead. For example, you can run `npm run ng lint` or `npm run ng test`.

## Install npm packages

To install third-party npm packages, use a command prompt in the `ClientApp` subdirectory. For example:

```
cd ClientApp
npm install <package_name>
```

## Publish and deploy

In development, the app runs in a mode optimized for developer convenience. For example, JavaScript bundles include source maps (so that when debugging, you can see your original TypeScript code). The app watches for TypeScript, HTML, and CSS file changes on disk and automatically recompiles and reloads when it sees those files change.

In production, serve a version of your app that's optimized for performance. This is configured to happen automatically. When you publish, the build configuration emits a minified, ahead-of-time (AoT) compiled build of your client-side code. Unlike the development build, the production build doesn't require Node.js to be installed on the server (unless you have enabled server-side rendering (SSR)).

You can use standard [ASP.NET Core hosting and deployment methods](../../host-and-deploy/?view=aspnetcore-10.0).

## Run "ng serve" independently

The project is configured to start its own instance of the Angular CLI server in the background when the ASP.NET Core app starts in development mode. This is convenient because you don't have to run a separate server manually.

There's a drawback to this default setup. Each time you modify your C# code and your ASP.NET Core app needs to restart, the Angular CLI server restarts. Around 10 seconds is required to start back up. If you're making frequent C# code edits and don't want to wait for Angular CLI to restart, run the Angular CLI server externally, independently of the ASP.NET Core process.

To run the Angular CLI server externally, switch to the `ClientApp` subdirectory in a command prompt and launch the Angular CLI development server:

```
cd ClientApp
npm start
```

When you start your ASP.NET Core app, it won't launch an Angular CLI server. The instance you started manually is used instead. This enables it to start and restart faster. It's no longer waiting for Angular CLI to rebuild your client app each time.

When the proxy is launched, the target URL and port is inferred from the environment variables set by .NET, `ASPNETCORE_URLS` and `ASPNETCORE_HTTPS_PORTS`. To set the URLs or HTTPS port, use one of the environment variables or change the value in `proxy.conf.json`.

## Configure proxy middleware for SignalR

For more information, see [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware).

The updated Angular project template provides a convenient starting point for ASP.NET Core apps using Angular and the Angular CLI to implement a rich, client-side user interface (UI).

The template is equivalent to creating an ASP.NET Core project to act as an API backend and an Angular CLI project to act as a UI. The template offers the convenience of hosting both project types in a single app project. Consequently, the app project can be built and published as a single unit.

## Create a new app

Create a new project from a command prompt using the command `dotnet new angular` in an empty directory. For example, the following commands create the app in a `my-new-app` directory and switch to that directory:

```
dotnet new angular -o my-new-app
cd my-new-app
```

Run the app from either Visual Studio or the .NET CLI:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

Open the generated `.csproj` file, and run the app as normal from there.

The build process restores npm dependencies on the first run, which can take several minutes. Subsequent builds are much faster.

Ensure you have an environment variable called `ASPNETCORE_ENVIRONMENT` with a value of `Development`. On Windows (in non-PowerShell prompts), run `SET ASPNETCORE_ENVIRONMENT=Development`. On Linux or macOS, run `export ASPNETCORE_ENVIRONMENT=Development`.

Run [dotnet build](/en-us/dotnet/core/tools/dotnet-build) to verify the app builds correctly. On the first run, the build process restores npm dependencies, which can take several minutes. Subsequent builds are much faster.

Run [dotnet run](/en-us/dotnet/core/tools/dotnet-run) to start the app. A message similar to the following is logged:

```
Now listening on: http://localhost:<port>
```

Navigate to this URL in a browser.

Warning

The app starts up an instance of the Angular CLI server in the background. A message similar to the following is logged: *NG Live Development Server is listening on localhost:\<otherport>, open a browser to http\://localhost:\<otherport>/*. Ignore this message—it's **not** the URL for the combined ASP.NET Core and Angular CLI app.

The project template creates an ASP.NET Core app and an Angular app. The ASP.NET Core app is intended to be used for data access, authorization, and other server-side concerns. The Angular app, residing in the `ClientApp` subdirectory, is intended to be used for all UI concerns.

## Add pages, images, styles, and modules

The `ClientApp` directory contains a standard Angular CLI app. See the official [Angular documentation](https://angular.dev) for more information.

There are slight differences between the Angular app created by this template and the one created by Angular CLI itself (via `ng new`); however, the app's capabilities are unchanged. The app created by the template contains a [Bootstrap](https://getbootstrap.com/)-based layout and a basic routing example.

## Run ng commands

In a command prompt, switch to the `ClientApp` subdirectory:

```
cd ClientApp
```

If you have the `ng` tool installed globally, you can run any of its commands. For example, you can run `ng lint`, `ng test`, or any of the other [Angular CLI commands](https://angular.dev/cli). There's no need to run `ng serve` though, because your ASP.NET Core app deals with serving both server-side and client-side parts of your app. Internally, it uses `ng serve` in development.

If you don't have the `ng` tool installed, run `npm run ng` instead. For example, you can run `npm run ng lint` or `npm run ng test`.

## Install npm packages

To install third-party npm packages, use a command prompt in the `ClientApp` subdirectory. For example:

```
cd ClientApp
npm install --save <package_name>
```

## Publish and deploy

In development, the app runs in a mode optimized for developer convenience. For example, JavaScript bundles include source maps (so that when debugging, you can see your original TypeScript code). The app watches for TypeScript, HTML, and CSS file changes on disk and automatically recompiles and reloads when it sees those files change.

In production, serve a version of your app that's optimized for performance. This is configured to happen automatically. When you publish, the build configuration emits a minified, ahead-of-time (AoT) compiled build of your client-side code. Unlike the development build, the production build doesn't require Node.js to be installed on the server (unless you have enabled server-side rendering (SSR)).

You can use standard [ASP.NET Core hosting and deployment methods](../../host-and-deploy/?view=aspnetcore-10.0).

## Run "ng serve" independently

The project is configured to start its own instance of the Angular CLI server in the background when the ASP.NET Core app starts in development mode. This is convenient because you don't have to run a separate server manually.

There's a drawback to this default setup. Each time you modify your C# code and your ASP.NET Core app needs to restart, the Angular CLI server restarts. Around 10 seconds is required to start back up. If you're making frequent C# code edits and don't want to wait for Angular CLI to restart, run the Angular CLI server externally, independently of the ASP.NET Core process. To do so:

1. In a command prompt, switch to the `ClientApp` subdirectory, and launch the Angular CLI development server:

   ```
   cd ClientApp
   npm start
   ```

   Important

   Use `npm start` to launch the Angular CLI development server, not `ng serve`, so that the configuration in `package.json` is respected. To pass additional parameters to the Angular CLI server, add them to the relevant `scripts` line in your `package.json` file.

2. Modify your ASP.NET Core app to use the external Angular CLI instance instead of launching one of its own. In your *Startup* class, replace the `spa.UseAngularCliServer` invocation with the following:

   ```
   spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
   ```

When you start your ASP.NET Core app, it won't launch an Angular CLI server. The instance you started manually is used instead. This enables it to start and restart faster. It's no longer waiting for Angular CLI to rebuild your client app each time.

When the proxy is launched, the target URL and port is inferred from the environment variables set by .NET, `ASPNETCORE_URLS` and `ASPNETCORE_HTTPS_PORTS`. To set the URLs or HTTPS port, use one of the environment variables or change the value in `proxy.conf.json`.

### Pass data from .NET code into TypeScript code

During SSR, you might want to pass per-request data from your ASP.NET Core app into your Angular app. For example, you could pass cookie information or something read from a database. To do this, edit your *Startup* class. In the callback for `UseSpaPrerendering`, set a value for `options.SupplyData` such as the following:

```
options.SupplyData = (context, data) =>
{
    // Creates a new value called isHttpsRequest that's passed to TypeScript code
    data["isHttpsRequest"] = context.Request.IsHttps;
};
```

The `SupplyData` callback lets you pass arbitrary, per-request, JSON-serializable data (for example, strings, booleans, or numbers). Your `main.server.ts` code receives this as `params.data`. For example, the preceding code sample passes a boolean value as `params.data.isHttpsRequest` into the `createServerRenderer` callback. You can pass this to other parts of your app in any way supported by Angular. For example, see how `main.server.ts` passes the `BASE_URL` value to any component whose constructor is declared to receive it.

### Drawbacks of SSR

Not all apps benefit from SSR. The primary benefit is perceived performance. Visitors reaching your app over a slow network connection or on slow mobile devices see the initial UI quickly, even if it takes a while to fetch or parse the JavaScript bundles. However, many SPAs are mainly used over fast, internal company networks on fast computers where the app appears almost instantly.

At the same time, there are significant drawbacks to enabling SSR. It adds complexity to your development process. Your code must run in two different environments: client-side and server-side (in a Node.js environment invoked from ASP.NET Core). Here are some things to bear in mind:

* SSR requires a Node.js installation on your production servers. This is automatically the case for some deployment scenarios, such as Azure App Services, but not for others, such as Azure Service Fabric.
* Enabling the `BuildServerSideRenderer` build flag causes your *node\_modules* directory to publish. This folder contains 20,000+ files, which increases deployment time.
* To run your code in a Node.js environment, it can't rely on the existence of browser-specific JavaScript APIs such as `window` or `localStorage`. If your code (or some third-party library you reference) tries to use these APIs, you'll get an error during SSR. For example, don't use jQuery because it references browser-specific APIs in many places. To prevent errors, you must either avoid SSR or avoid browser-specific APIs or libraries. You can wrap any calls to such APIs in checks to ensure they aren't invoked during SSR. For example, use a check such as the following in JavaScript or TypeScript code:

```
if (typeof window !== 'undefined') {
    // Call browser-specific APIs here
}
```

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/background-services
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/background-services.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host ASP.NET Core SignalR in background services

By [Dave Pringle](https://github.com/UncleDave) and [Brady Gaster](https://twitter.com/bradygaster)

This article provides guidance for:

* Hosting SignalR Hubs using a background worker process hosted with ASP.NET Core.
* Sending messages to connected clients from within a .NET [BackgroundService](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/background-service/samples/6.0) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

## Enable SignalR at app startup

Hosting ASP.NET Core SignalR Hubs in the context of a background worker process is identical to hosting a Hub in an ASP.NET Core web app. In `Program.cs`, calling `builder.Services.AddSignalR` adds the required services to the ASP.NET Core Dependency Injection (DI) layer to support SignalR. The `MapHub` method is called on the `WebApplication` `app` to connect the Hub endpoints in the ASP.NET Core request pipeline.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();
builder.Services.AddHostedService<Worker>();

var app = builder.Build();

app.MapHub<ClockHub>("/hubs/clock");

app.Run();
```

In the preceding example, the `ClockHub` class implements the `Hub<T>` class to create a strongly typed Hub. The `ClockHub` has been configured in `Program.cs` to respond to requests at the endpoint `/hubs/clock`.

For more information on strongly typed Hubs, see [Use hubs in SignalR for ASP.NET Core](hubs?view=aspnetcore-10.0#use-strongly-typed-hubs).

Note

This functionality isn't limited to the [Hub\<T>](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub-1) class. Any class that inherits from [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub), such as [DynamicHub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.dynamichub), works.

```
public class ClockHub : Hub<IClock>
{
    public async Task SendMyLocalTimeToOtherClients(DateTime dateTime)
    {
        await Clients.All.ShowTime(dateTime);
    }
}
```

The interface used by the strongly typed `ClockHub` is the `IClock` interface.

```
public interface IClock
{
    Task ShowTime(DateTime currentTime);
}
```

## Call a SignalR Hub from a background service

During startup, the `Worker` class, a `BackgroundService`, is enabled using `AddHostedService`.

```
builder.Services.AddHostedService<Worker>();
```

Since SignalR is also enabled up during the startup phase, in which each Hub is attached to an individual endpoint in ASP.NET Core's HTTP request pipeline, each Hub is represented by an `IHubContext<T>` on the server. Using ASP.NET Core's DI features, other classes instantiated by the hosting layer, like `BackgroundService` classes, MVC Controller classes, or Razor page models, can get references to server-side Hubs by accepting instances of `IHubContext<ClockHub, IClock>` during construction.

```
public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IHubContext<ClockHub, IClock> _clockHub;

    public Worker(ILogger<Worker> logger, IHubContext<ClockHub, IClock> clockHub)
    {
        _logger = logger;
        _clockHub = clockHub;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {Time}", DateTime.Now);
            await _clockHub.Clients.All.ShowTime(DateTime.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}
```

As the `ExecuteAsync` method is called iteratively in the background service, the server's current date and time are sent to the connected clients using the `ClockHub`.

## React to SignalR events with background services

Like a Single Page App using the JavaScript client for SignalR, or a .NET desktop app using the [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0), a `BackgroundService` or `IHostedService` implementation can also be used to connect to SignalR Hubs and respond to events.

The `ClockHubClient` class implements both the `IClock` interface and the `IHostedService` interface. This way it can be enabled during startup to run continuously and respond to Hub events from the server.

```
public partial class ClockHubClient : IClock, IHostedService
{
}
```

During initialization, the `ClockHubClient` creates an instance of a `HubConnection` and enables the `IClock.ShowTime` method as the handler for the Hub's `ShowTime` event.

```
private readonly ILogger<ClockHubClient> _logger;
private HubConnection _connection;

public ClockHubClient(ILogger<ClockHubClient> logger)
{
    _logger = logger;
    
    _connection = new HubConnectionBuilder()
        .WithUrl(Strings.HubUrl)
        .Build();

    _connection.On<DateTime>(Strings.Events.TimeSent, ShowTime);
}

public Task ShowTime(DateTime currentTime)
{
    _logger.LogInformation("{CurrentTime}", currentTime.ToShortTimeString());

    return Task.CompletedTask;
}
```

In the `IHostedService.StartAsync` implementation, the `HubConnection` is started asynchronously.

```
public async Task StartAsync(CancellationToken cancellationToken)
{
    // Loop is here to wait until the server is running
    while (true)
    {
        try
        {
            await _connection.StartAsync(cancellationToken);

            break;
        }
        catch
        {
            await Task.Delay(1000, cancellationToken);
        }
    }
}
```

During the `IHostedService.StopAsync` method, the `HubConnection` is disposed of asynchronously.

```
public async Task StopAsync(CancellationToken cancellationToken)
{
    await _connection.DisposeAsync();
}
```

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/background-service/samples/3.x) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

## Enable SignalR in startup

Hosting ASP.NET Core SignalR Hubs in the context of a background worker process is identical to hosting a Hub in an ASP.NET Core web app. In the `Startup.ConfigureServices` method, calling `services.AddSignalR` adds the required services to the ASP.NET Core Dependency Injection (DI) layer to support SignalR. In `Startup.Configure`, the `MapHub` method is called in the `UseEndpoints` callback to connect the Hub endpoints in the ASP.NET Core request pipeline.

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSignalR();
        services.AddHostedService<Worker>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<ClockHub>("/hubs/clock");
        });
    }
}
```

In the preceding example, the `ClockHub` class implements the `Hub<T>` class to create a strongly typed Hub. The `ClockHub` has been configured in the `Startup` class to respond to requests at the endpoint `/hubs/clock`.

For more information on strongly typed Hubs, see [Use hubs in SignalR for ASP.NET Core](hubs?view=aspnetcore-10.0#use-strongly-typed-hubs).

Note

This functionality isn't limited to the [Hub\<T>](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub-1) class. Any class that inherits from [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub), such as [DynamicHub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.dynamichub), works.

```
public class ClockHub : Hub<IClock>
{
    public async Task SendTimeToClients(DateTime dateTime)
    {
        await Clients.All.ShowTime(dateTime);
    }
}
```

The interface used by the strongly typed `ClockHub` is the `IClock` interface.

```
public interface IClock
{
    Task ShowTime(DateTime currentTime);
}
```

## Call a SignalR Hub from a background service

During startup, the `Worker` class, a `BackgroundService`, is enabled using `AddHostedService`.

```
services.AddHostedService<Worker>();
```

Since SignalR is also enabled up during the `Startup` phase, in which each Hub is attached to an individual endpoint in ASP.NET Core's HTTP request pipeline, each Hub is represented by an `IHubContext<T>` on the server. Using ASP.NET Core's DI features, other classes instantiated by the hosting layer, like `BackgroundService` classes, MVC Controller classes, or Razor page models, can get references to server-side Hubs by accepting instances of `IHubContext<ClockHub, IClock>` during construction.

```
public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IHubContext<ClockHub, IClock> _clockHub;

    public Worker(ILogger<Worker> logger, IHubContext<ClockHub, IClock> clockHub)
    {
        _logger = logger;
        _clockHub = clockHub;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {Time}", DateTime.Now);
            await _clockHub.Clients.All.ShowTime(DateTime.Now);
            await Task.Delay(1000);
        }
    }
}
```

As the `ExecuteAsync` method is called iteratively in the background service, the server's current date and time are sent to the connected clients using the `ClockHub`.

## React to SignalR events with background services

Like a Single Page App using the JavaScript client for SignalR, or a .NET desktop app using the [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0), a `BackgroundService` or `IHostedService` implementation can also be used to connect to SignalR Hubs and respond to events.

The `ClockHubClient` class implements both the `IClock` interface and the `IHostedService` interface. This way it can be enabled during `Startup` to run continuously and respond to Hub events from the server.

```
public partial class ClockHubClient : IClock, IHostedService
{
}
```

During initialization, the `ClockHubClient` creates an instance of a `HubConnection` and enables the `IClock.ShowTime` method as the handler for the Hub's `ShowTime` event.

```
private readonly ILogger<ClockHubClient> _logger;
private HubConnection _connection;

public ClockHubClient(ILogger<ClockHubClient> logger)
{
    _logger = logger;
    
    _connection = new HubConnectionBuilder()
        .WithUrl(Strings.HubUrl)
        .Build();

    _connection.On<DateTime>(Strings.Events.TimeSent, ShowTime);
}

public Task ShowTime(DateTime currentTime)
{
    _logger.LogInformation("{CurrentTime}", currentTime.ToShortTimeString());

    return Task.CompletedTask;
}
```

In the `IHostedService.StartAsync` implementation, the `HubConnection` is started asynchronously.

```
public async Task StartAsync(CancellationToken cancellationToken)
{
    // Loop is here to wait until the server is running
    while (true)
    {
        try
        {
            await _connection.StartAsync(cancellationToken);

            break;
        }
        catch
        {
            await Task.Delay(1000);
        }
    }
}
```

During the `IHostedService.StopAsync` method, the `HubConnection` is disposed of asynchronously.

```
public Task StopAsync(CancellationToken cancellationToken)
{
    return _connection.DisposeAsync();
}
```

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/background-service/samples/2.2) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

## Enable SignalR in startup

Hosting ASP.NET Core SignalR Hubs in the context of a background worker process is identical to hosting a Hub in an ASP.NET Core web app. In the `Startup.ConfigureServices` method, calling `services.AddSignalR` adds the required services to the ASP.NET Core Dependency Injection (DI) layer to support SignalR. In `Startup.Configure`, the `UseSignalR` method is called to connect the Hub endpoints in the ASP.NET Core request pipeline.

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSignalR();
        services.AddHostedService<Worker>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseSignalR((routes) =>
        {
            routes.MapHub<ClockHub>("/hubs/clock");
        });
    }
}
```

In the preceding example, the `ClockHub` class implements the `Hub<T>` class to create a strongly typed Hub. The `ClockHub` has been configured in the `Startup` class to respond to requests at the endpoint `/hubs/clock`.

For more information on strongly typed Hubs, see [Use hubs in SignalR for ASP.NET Core](hubs?view=aspnetcore-10.0#use-strongly-typed-hubs).

Note

This functionality isn't limited to the [Hub\<T>](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub-1) class. Any class that inherits from [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub), such as [DynamicHub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.dynamichub), works.

```
public class ClockHub : Hub<IClock>
{
    public async Task SendTimeToClients(DateTime dateTime)
    {
        await Clients.All.ShowTime(dateTime);
    }
}
```

The interface used by the strongly typed `ClockHub` is the `IClock` interface.

```
public interface IClock
{
    Task ShowTime(DateTime currentTime);
}
```

## Call a SignalR Hub from a background service

During startup, the `Worker` class, a `BackgroundService`, is enabled using `AddHostedService`.

```
services.AddHostedService<Worker>();
```

Since SignalR is also enabled up during the `Startup` phase, in which each Hub is attached to an individual endpoint in ASP.NET Core's HTTP request pipeline, each Hub is represented by an `IHubContext<T>` on the server. Using ASP.NET Core's DI features, other classes instantiated by the hosting layer, like `BackgroundService` classes, MVC Controller classes, or Razor page models, can get references to server-side Hubs by accepting instances of `IHubContext<ClockHub, IClock>` during construction.

```
public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IHubContext<ClockHub, IClock> _clockHub;

    public Worker(ILogger<Worker> logger, IHubContext<ClockHub, IClock> clockHub)
    {
        _logger = logger;
        _clockHub = clockHub;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {Time}", DateTime.Now);
            await _clockHub.Clients.All.ShowTime(DateTime.Now);
            await Task.Delay(1000);
        }
    }
}
```

As the `ExecuteAsync` method is called iteratively in the background service, the server's current date and time are sent to the connected clients using the `ClockHub`.

## React to SignalR events with background services

Like a Single Page App using the JavaScript client for SignalR, or a .NET desktop app using the [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0), a `BackgroundService` or `IHostedService` implementation can also be used to connect to SignalR Hubs and respond to events.

The `ClockHubClient` class implements both the `IClock` interface and the `IHostedService` interface. This way it can be enabled during `Startup` to run continuously and respond to Hub events from the server.

```
public partial class ClockHubClient : IClock, IHostedService
{
}
```

During initialization, the `ClockHubClient` creates an instance of a `HubConnection` and enables the `IClock.ShowTime` method as the handler for the Hub's `ShowTime` event.

```
private readonly ILogger<ClockHubClient> _logger;
private HubConnection _connection;

public ClockHubClient(ILogger<ClockHubClient> logger)
{
    _logger = logger;
    
    _connection = new HubConnectionBuilder()
        .WithUrl(Strings.HubUrl)
        .Build();

    _connection.On<DateTime>(Strings.Events.TimeSent, 
        dateTime => _ = ShowTime(dateTime));
}

public Task ShowTime(DateTime currentTime)
{
    _logger.LogInformation("{CurrentTime}", currentTime.ToShortTimeString());

    return Task.CompletedTask;
}
```

In the `IHostedService.StartAsync` implementation, the `HubConnection` is started asynchronously.

```
public async Task StartAsync(CancellationToken cancellationToken)
{
    // Loop is here to wait until the server is running
    while (true)
    {
        try
        {
            await _connection.StartAsync(cancellationToken);

            break;
        }
        catch
        {
            await Task.Delay(1000);
        }
    }
}
```

During the `IHostedService.StopAsync` method, the `HubConnection` is disposed of asynchronously.

```
public Task StopAsync(CancellationToken cancellationToken)
{
    return _connection.DisposeAsync();
}
```

## Related content

* [Get started](../tutorials/signalr?view=aspnetcore-10.0)
* [Hubs](hubs?view=aspnetcore-10.0)
* [Publish to Azure](publish-to-azure-web-app?view=aspnetcore-10.0)
* [Strongly typed Hubs](hubs?view=aspnetcore-10.0#use-strongly-typed-hubs)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-storage-format
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/implementation/key-storage-format.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Key storage format in ASP.NET Core

[]()

Objects are stored at rest in XML representation. The default directory for key storage is:

* Windows: \*%LOCALAPPDATA%\ASP.NET\DataProtection-Keys\*
* macOS / Linux: *$HOME/.aspnet/DataProtection-Keys*

## The \<key> element

Keys exist as top-level objects in the key repository. By convention keys have the filename *`key-{guid}.xml`*, where {guid} is the id of the key. Each such file contains a single key. The format of the file is as follows.

```
<?xml version="1.0" encoding="utf-8"?>
<key id="aaaaaaaa-0b0b-1c1c-2d2d-333333333333" version="1">
  <creationDate>2015-03-19T23:32:02.3949887Z</creationDate>
  <activationDate>2015-03-19T23:32:02.3839429Z</activationDate>
  <expirationDate>2015-06-17T23:32:02.3839429Z</expirationDate>
  <descriptor deserializerType="{deserializerType}">
    <descriptor>
      <encryption algorithm="AES_256_CBC" />
      <validation algorithm="HMACSHA256" />
      <enc:encryptedSecret decryptorType="{decryptorType}" xmlns:enc="...">
        <encryptedKey>
          <!-- This key is encrypted with Windows DPAPI. -->
          <value>AQAAANCM...8/zeP8lcwAg==</value>
        </encryptedKey>
      </enc:encryptedSecret>
    </descriptor>
  </descriptor>
</key>
```

The \<key> element contains the following attributes and child elements:

* The key id. This value is treated as authoritative; the filename is simply a nicety for human readability.

* The version of the \<key> element, currently fixed at 1.

* The key's creation, activation, and expiration dates.

* A \<descriptor> element, which contains information on the authenticated encryption implementation contained within this key.

In the above example, the key's id is {aaaaaaaa-0b0b-1c1c-2d2d-333333333333}, it was created and activated on March 19, 2015, and it has a lifetime of 90 days. (Occasionally the activation date might be slightly before the creation date as in this example. This is due to a nit in how the APIs work and is harmless in practice.)

## The \<descriptor> element

The outer \<descriptor> element contains an attribute deserializerType, which is the assembly-qualified name of a type which implements IAuthenticatedEncryptorDescriptorDeserializer. This type is responsible for reading the inner \<descriptor> element and for parsing the information contained within.

The particular format of the \<descriptor> element depends on the authenticated encryptor implementation encapsulated by the key, and each deserializer type expects a slightly different format for this. In general, though, this element will contain algorithmic information (names, types, OIDs, or similar) and secret key material. In the above example, the descriptor specifies that this key wraps AES-256-CBC encryption + HMACSHA256 validation.

## The \<encryptedSecret> element

An **\<encryptedSecret>** element which contains the encrypted form of the secret key material may be present if [encryption of secrets at rest is enabled](key-encryption-at-rest?view=aspnetcore-10.0). The attribute `decryptorType` is the assembly-qualified name of a type which implements [IXmlDecryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.xmlencryption.ixmldecryptor). This type is responsible for reading the inner **\<encryptedKey>** element and decrypting it to recover the original plaintext.

As with `<descriptor>`, the particular format of the `<encryptedSecret>` element depends on the at-rest encryption mechanism in use. In the above example, the master key is encrypted using Windows DPAPI per the comment.

## The \<revocation> element

Revocations exist as top-level objects in the key repository. By convention revocations have the filename *`revocation-{timestamp}.xml`* (for revoking all keys before a specific date) or *`revocation-{guid}.xml`* (for revoking a specific key). Each file contains a single \<revocation> element.

For revocations of individual keys, the file contents will be as below.

```
<?xml version="1.0" encoding="utf-8"?>
<revocation version="1">
  <revocationDate>2015-03-20T22:45:30.2616742Z</revocationDate>
  <key id="bbbbbbbb-1c1c-2d2d-3e3e-444444444444" />
  <reason>human-readable reason</reason>
</revocation>
```

In this case, only the specified key is revoked. If the key id is "\*", however, as in the below example, all keys whose creation date is prior to the specified revocation date are revoked.

```
<?xml version="1.0" encoding="utf-8"?>
<revocation version="1">
  <revocationDate>2015-03-20T15:45:45.7366491-07:00</revocationDate>
  <!-- All keys created before the revocation date are revoked. -->
  <key id="*" />
  <reason>human-readable reason</reason>
</revocation>
```

The \<reason> element is never read by the system. It's simply a convenient place to store a human-readable reason for revocation.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/comparison
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/comparison.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Compare gRPC services with HTTP APIs

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

This article explains how [gRPC services](https://grpc.io/docs/guides/) compare to HTTP APIs with JSON (including ASP.NET Core [web APIs](../web-api/?view=aspnetcore-10.0)). The technology used to provide an API for your app is an important choice, and gRPC offers unique benefits compared to HTTP APIs. This article discusses the strengths and weaknesses of gRPC and recommends scenarios for using gRPC over other technologies.

## High-level comparison

The following table offers a high-level comparison of features between gRPC and HTTP APIs with JSON.

| Feature                | gRPC                                               | HTTP APIs with JSON           |
| ---------------------- | -------------------------------------------------- | ----------------------------- |
| Contract               | Required (`.proto`)                                | Optional (OpenAPI)            |
| Protocol               | HTTP/2                                             | HTTP                          |
| Payload                | [Protobuf (small, binary)](#performance)           | JSON (large, human readable)  |
| Prescriptiveness       | [Strict specification](#strict-specification)      | Loose. Any HTTP is valid.     |
| Streaming              | [Client, server, bi-directional](#streaming)       | Client, server                |
| Browser support        | [No (requires grpc-web)](#limited-browser-support) | Yes                           |
| Security               | Transport (TLS)                                    | Transport (TLS)               |
| Client code-generation | [Yes](#code-generation)                            | OpenAPI + third-party tooling |

## gRPC strengths

### Performance

gRPC messages are serialized using [Protobuf](https://developers.google.com/protocol-buffers/docs/overview), an efficient binary message format. Protobuf serializes very quickly on the server and client. Protobuf serialization results in small message payloads, important in limited bandwidth scenarios like mobile apps.

gRPC is designed for HTTP/2, a major revision of HTTP that provides significant performance benefits over HTTP 1.x:

* Binary framing and compression. HTTP/2 protocol is compact and efficient both in sending and receiving.
* Multiplexing of multiple HTTP/2 calls over a single TCP connection. Multiplexing eliminates [head-of-line blocking](https://en.wikipedia.org/wiki/Head-of-line_blocking).

HTTP/2 is not exclusive to gRPC. Many request types, including HTTP APIs with JSON, can use HTTP/2 and benefit from its performance improvements.

### Code generation

All gRPC frameworks provide first-class support for code generation. A core file to gRPC development is the [`.proto` file](https://developers.google.com/protocol-buffers/docs/proto3), which defines the contract of gRPC services and messages. From this file, gRPC frameworks generate a service base class, messages, and a complete client.

By sharing the `.proto` file between the server and client, messages and client code can be generated from end to end. Code generation of the client eliminates duplication of messages on the client and server, and creates a strongly-typed client for you. Not having to write a client saves significant development time in applications with many services.

### Strict specification

A formal specification for HTTP API with JSON doesn't exist. Developers debate the best format of URLs, HTTP verbs, and response codes.

The [gRPC specification](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) is prescriptive about the format a gRPC service must follow. gRPC eliminates debate and saves developer time because gRPC is consistent across platforms and implementations.

### Streaming

HTTP/2 provides a foundation for long-lived, real-time communication streams. gRPC provides first-class support for streaming through HTTP/2.

A gRPC service supports all streaming combinations:

* Unary (no streaming)
* Server to client streaming
* Client to server streaming
* Bi-directional streaming

### Deadline/timeouts and cancellation

gRPC allows clients to specify how long they are willing to wait for an RPC to complete. The [deadline](https://grpc.io/blog/deadlines) is sent to the server, and the server can decide what action to take if it exceeds the deadline. For example, the server might cancel in-progress gRPC/HTTP/database requests on timeout.

Propagating the deadline and cancellation through child gRPC calls helps enforce resource usage limits.

## gRPC recommended scenarios

gRPC is well suited to the following scenarios:

* **Microservices**: gRPC is designed for low latency and high throughput communication. gRPC is great for lightweight microservices where efficiency is critical.
* **Point-to-point real-time communication**: gRPC has excellent support for bi-directional streaming. gRPC services can push messages in real-time without polling.
* **Polyglot environments**: gRPC tooling supports all popular development languages, making gRPC a good choice for multi-language environments.
* **Network constrained environments**: gRPC messages are serialized with Protobuf, a lightweight message format. A gRPC message is always smaller than an equivalent JSON message.
* **Inter-process communication (IPC)**: IPC transports such as Unix domain sockets and named pipes can be used with gRPC to communicate between apps on the same machine. For more information, see [Inter-process communication with gRPC](interprocess?view=aspnetcore-10.0).

## gRPC weaknesses

### Limited browser support

It's impossible to directly call a gRPC service from a browser today. gRPC heavily uses HTTP/2 features and no browser provides the level of control required over web requests to support a gRPC client. For example, browsers do not allow a caller to require that HTTP/2 be used, or provide access to underlying HTTP/2 frames.

gRPC on ASP.NET Core offers two browser-compatible solutions:

* **gRPC-Web** allows browser apps to call gRPC services with the gRPC-Web client and Protobuf. gRPC-Web requires the browser app to generate a gRPC client. gRPC-Web allows browser apps to benefit from the high-performance and low network usage of gRPC.

  .NET has built-in support for gRPC-Web. For more information, see [gRPC-Web in ASP.NET Core gRPC apps](grpcweb?view=aspnetcore-10.0).

* **gRPC JSON transcoding** allows browser apps to call gRPC services as if they were RESTful APIs with JSON. The browser app doesn't need to generate a gRPC client or know anything about gRPC. RESTful APIs can be automatically created from gRPC services by annotating the `.proto` file with HTTP metadata. Transcoding allows an app to support both gRPC and JSON web APIs without duplicating the effort of building separate services for both.

  .NET has built-in support for creating JSON web APIs from gRPC services. For more information, see [gRPC JSON transcoding in ASP.NET Core gRPC apps](json-transcoding?view=aspnetcore-10.0).

Note

gRPC JSON transcoding requires .NET 7 or later.

### Not human readable

HTTP API requests are sent as text and can be read and created by humans.

gRPC messages are encoded with Protobuf by default. While Protobuf is efficient to send and receive, its binary format isn't human readable. Protobuf requires the message's interface description specified in the `.proto` file to properly deserialize. Additional tooling is required to analyze Protobuf payloads on the wire and to compose requests by hand.

Features such as [server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) and the [gRPC command line tool](https://github.com/grpc/grpc/blob/master/doc/command_line_tool.md) exist to assist with binary Protobuf messages. Also, Protobuf messages support [conversion to and from JSON](https://developers.google.com/protocol-buffers/docs/proto3#json). The built-in JSON conversion provides an efficient way to convert Protobuf messages to and from human readable form when debugging.

## Alternative framework scenarios

Other frameworks are recommended over gRPC in the following scenarios:

* **Browser accessible APIs**: gRPC isn't fully supported in the browser. gRPC-Web can offer browser support, but it has limitations and introduces a server proxy.
* **Broadcast real-time communication**: gRPC supports real-time communication via streaming, but the concept of broadcasting a message out to registered connections doesn't exist. For example in a chat room scenario where new chat messages should be sent to all clients in the chat room, each gRPC call is required to individually stream new chat messages to the client. [SignalR](../signalr/introduction?view=aspnetcore-10.0) is a useful framework for this scenario. SignalR has the concept of persistent connections and built-in support for broadcasting messages.

## Related content

* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [Migrate gRPC from C-core to gRPC for .NET](migration?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/signalr-blazor
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/signalr-blazor.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use ASP.NET Core SignalR with Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This tutorial provides a basic working experience for building a real-time app using SignalR with Blazor. This article is useful for developers who are already familiar with SignalR and are seeking to understand how to use SignalR in a Blazor app. For detailed guidance on the SignalR and Blazor frameworks, see the following reference documentation sets and the API documentation:

* [Overview of ASP.NET Core SignalR](../../signalr/introduction?view=aspnetcore-10.0)
* [ASP.NET Core Blazor](../?view=aspnetcore-10.0)
* [.NET API browser](/en-us/dotnet/api/)

Learn how to:

* Create a Blazor app
* Add the SignalR client library
* Add a SignalR hub
* Add SignalR services and an endpoint for the SignalR hub
* Add a Razor component code for chat

At the end of this tutorial, you'll have a working chat app.

## Prerequisites

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio (latest release)](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2022) with the **ASP.NET and web development** workload

Install the latest releases of the following:

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit)
* [.NET SDK](https://dotnet.microsoft.com/download/dotnet)

For more information, see [Debug ASP.NET Core Blazor apps](../debug?view=aspnetcore-10.0#visual-studio-code-prerequisites).

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use an integrated development environment (IDE) other than Visual Studio Code.

[.NET SDK (latest release)](https://dotnet.microsoft.com/download/dotnet)

## Sample app

Downloading the tutorial's sample chat app isn't required for this tutorial. The sample app is the final, working app produced by following the steps of this tutorial. When you open the samples repository, open the version folder that you plan to target and find the sample named `BlazorSignalRApp`.

Downloading the tutorial's sample chat app isn't required for this tutorial. The sample app is the final, working app produced by following the steps of this tutorial. When you open the samples repository, open the version folder that you plan to target and find the sample named `BlazorWebAssemblySignalRApp`.

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

## Create a Blazor Web App

Follow the guidance for your choice of tooling:

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [.NET CLI](#tabpanel_2_net-cli)

Note

Visual Studio 2022 or later and .NET 8 or later SDK are required.

In Visual Studio:

* Select **Create a new project** from the **Start Window** or select **File** > **New** > **Project** from the menu bar.

* In the **Create a new project** dialog, select **Blazor Web App** from the list of project templates. Select the **Next** button.

* In the **Configure your new project** dialog, name the project `BlazorSignalRApp` in the **Project name** field, including matching the capitalization. Using this exact project name is important to ensure that the namespaces match for code that you copy from the tutorial into the app that you're building.

* Confirm that the **Location** for the app is suitable. Leave the **Place solution and project in the same directory** checkbox selected. Select the **Next** button.

* In the **Additional information** dialog, use the following settings:

  * **Framework**: Confirm that the [latest framework](https://dotnet.microsoft.com/download/dotnet) is selected. If Visual Studio's **Framework** dropdown list doesn't include the latest available .NET framework, [update Visual Studio](/en-us/visualstudio/install/update-visual-studio) and restart the tutorial.
  * **Authentication type**: **None**
  * **Configure for HTTPS**: Selected
  * **Interactive render mode**: **WebAssembly**
  * **Interactivity location**: **Per page/component**
  * **Include sample pages**: Selected
  * **Do not use top-level statements**: Not selected
  * **Use the .dev.localhost TLD in the application URL**: Not selected
  * Select **Create**.

This tutorial assumes that you have familiarity with VS Code. If you're new to VS Code, see the [VS Code documentation](https://code.visualstudio.com/docs). The videos listed by the [Introductory Videos page](https://code.visualstudio.com/docs/getstarted/introvideos) are designed to give you an overview of VS Code's features.

In VS Code:

* Go to the **Explorer** view and select the **Create .NET Project** button. Alternatively, you can bring up the **Command Palette** using `Ctrl`+`Shift`+`P`, and then type "`.NET`" and find and select the **.NET: New Project** command.

* Select the **Blazor Web App** project template from the list.

* In the **Project Location** dialog, create or select a folder for the project.

* In the **Command Palette**, name the project `BlazorSignalRApp`, including matching the capitalization. Using this exact project name is important to ensure that the namespaces match for code that you copy from the tutorial into the app that you're building.

* Adjust the project's options by selecting **Show all template options**.

  Change **Interactive render mode** to **WebAssembly**.

  After the preceding changes are made, the **Command Palette** shows the following:

  * **Framework**: Confirm that the [latest framework](https://dotnet.microsoft.com/download/dotnet) is selected. If Visual Studio's **Framework** dropdown list doesn't include the latest available .NET framework, [update Visual Studio](/en-us/visualstudio/install/update-visual-studio) and restart the tutorial.
  * **Authentication type**: **None**
  * **Configure for HTTPS**: Selected
  * **Interactive render mode**: **WebAssembly**
  * **Interactivity location**: **Per page/component**
  * **Include sample pages**: Selected
  * **Do not use top-level statements**: Not selected
  * **Use the .dev.localhost TLD in the application URL**: Not selected

* Select **Create project** from the **Command Palette**.

In a command shell:

* Change to the directory using the `cd` command to where you want to create the project folder (for example, `cd c:/users/Bernie_Kopell/Documents`).

* Use the [`dotnet new` command](/en-us/dotnet/core/tools/dotnet-new) with the [`blazor` project template](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazor) to create a new Blazor Web App project. The [`-o|--output` option](/en-us/dotnet/core/tools/dotnet-new#options) passed to the command creates the project in a new folder named `BlazorSignalRApp` at the current directory location. Pass the `-int|--interactivity` option with `WebAssembly` to adopt client-side rendering (CSR). Pass the `-ai|--all-interactive` option with `False` to adopt per-page/component interactivity location.

  Important

  Name the project `BlazorSignalRApp`, including matching the capitalization, so the namespaces match for code that you copy from the tutorial to the app as you follow the tutorial.

  ```
  dotnet new blazor -o BlazorSignalRApp -int WebAssembly -ai False
  ```

The guidance in this article uses a WebAssembly component for the SignalR client because it doesn't make sense to use SignalR to connect to a hub from an Interactive Server component in the same app, as that can lead to server port exhaustion.

## Add the SignalR client library

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [.NET CLI](#tabpanel_3_net-cli)

In **Solution Explorer**, right-click the `BlazorSignalRApp.Client` project and select **Manage NuGet Packages**.

In the **Manage NuGet Packages** dialog, confirm that the **Package source** is set to `nuget.org`.

With **Browse** selected, type `Microsoft.AspNetCore.SignalR.Client` in the search box.

In the search results, select the latest release of the [`Microsoft.AspNetCore.SignalR.Client` package](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client). Select **Install**.

If the **Preview Changes** dialog appears, select **OK**.

If the **License Acceptance** dialog appears, select **I Accept** if you agree with the license terms.

In the **Terminal** (**Terminal** > **New Terminal** from the menu bar opened to the `BlazorSignalRApp.Client` project using `cd BlazorSignalRApp.Client`), execute the following command:

```
dotnet add package Microsoft.AspNetCore.SignalR.Client
```

To add an earlier version of the package, supply the `--version {VERSION}` option, where the `{VERSION}` placeholder is the version of the package to add.

In a command shell opened to the `BlazorSignalRApp.Client` project, execute the following command:

```
dotnet add package Microsoft.AspNetCore.SignalR.Client
```

To add an earlier version of the package, supply the `--version {VERSION}` option, where the `{VERSION}` placeholder is the version of the package to add.

## Add a SignalR hub

In the server `BlazorSignalRApp` project, create a `Hubs` (plural) folder and add the following `ChatHub` class (`Hubs/ChatHub.cs`):

```
using Microsoft.AspNetCore.SignalR;

namespace BlazorSignalRApp.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

## Add services and an endpoint for the SignalR hub

Open the `Program` file of the server `BlazorSignalRApp` project.

Add the namespaces for [Microsoft.AspNetCore.ResponseCompression](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression) and the `ChatHub` class to the top of the file:

```
using Microsoft.AspNetCore.ResponseCompression;
using BlazorSignalRApp.Hubs;
```

Add SignalR and response compression middleware services:

```
builder.Services.AddSignalR();

builder.Services.AddResponseCompression(opts =>
{
   opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
       [ "application/octet-stream" ]);
});
```

Use response compression middleware at the top of the processing pipeline's configuration. Place the following line of code immediately after the line that builds the app (`var app = builder.Build();`):

```
app.UseResponseCompression();
```

Add an endpoint for the hub:

```
app.MapHub<ChatHub>("/chathub");
```

## Add Razor component code for chat

Add the following `Pages/Chat.razor` file to the `BlazorSignalRApp.Client` project:

```
@page "/chat"
@rendermode InteractiveWebAssembly
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable

<PageTitle>Chat</PageTitle>

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection? hubConnection;
    private List<string> messages = [];
    private string? userInput;
    private string? messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            InvokeAsync(StateHasChanged);
        });

        await hubConnection.StartAsync();
    }

    private async Task Send()
    {
        if (hubConnection is not null)
        {
            await hubConnection.SendAsync("SendMessage", userInput, messageInput);
        }
    }

    public bool IsConnected =>
        hubConnection?.State == HubConnectionState.Connected;

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}
```

Add an entry to the `NavMenu` component to reach the chat page. In `Components/Layout/NavMenu.razor` immediately after the `<div>` block for the `Weather` component, add the following `<div>` block:

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="chat">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Chat
    </NavLink>
</div>
```

Note

Disable response compression middleware in the `Development` environment when using [Hot Reload](../../test/hot-reload?view=aspnetcore-10.0). For more information, see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#disable-response-compression-for-hot-reload).

## Run the app

Follow the guidance for your tooling:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)
* [.NET CLI](#tabpanel_4_net-cli)

With the server `BlazorSignalRApp` project selected in **Solution Explorer**, press `F5` to run the app with debugging or `Ctrl`+`F5` to run the app without debugging.

Press `F5` to run the app with debugging or `Ctrl`+`F5` to run the app without debugging.

In a command shell opened to the root folder of the server `BlazorSignalRApp` project, execute the following command:

```
dotnet watch
```

Copy the URL from the address bar, open another browser instance or tab, and paste the URL in the address bar.

Choose either browser, enter a name and message, and select the button to send the message. The name and message are displayed on both pages instantly:

Quotes: *Star Trek VI: The Undiscovered Country* ©1991 [Paramount](https://www.paramountmovies.com/movies/star-trek-vi-the-undiscovered-country)

## Hosted Blazor WebAssembly experience

### Create the app

Follow the guidance for your choice of tooling to create a hosted Blazor WebAssembly app:

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)
* [.NET CLI](#tabpanel_5_net-cli)

Note

Visual Studio 2022 or later and .NET or later 6 SDK are required.

Create a new project.

Choose the **Blazor WebAssembly App** template. Select **Next**.

Type `BlazorWebAssemblySignalRApp` in the **Project name** field. Confirm the **Location** entry is correct or provide a location for the project. Select **Next**.

In the **Additional information** dialog, select the **ASP.NET Core Hosted** checkbox.

Select **Create**.

Confirm that a hosted Blazor WebAssembly app was created: In **Solution Explorer**, confirm the presence of a **Client** project and a **Server** project. If the two projects aren't present, start over and confirm selection of the **ASP.NET Core Hosted** checkbox before selecting **Create**.

In a command shell, execute the following command:

```
dotnet new blazorwasm -ho -o BlazorWebAssemblySignalRApp
```

The `-ho|--hosted` option creates a hosted Blazor WebAssembly [solution](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln). For information on configuring VS Code assets in the `.vscode` folder, see the **Linux** operating system guidance in [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0).

The `-o|--output` option creates a folder for the solution. If you've created a folder for the solution and the command shell is open in that folder, omit the `-o|--output` option and value to create the solution.

In Visual Studio Code, open the app's project folder.

Confirm that a hosted Blazor WebAssembly app was created: Confirm the presence of a **Client** project and a **Server** project in the app's solution folder. If the two projects aren't present, start over and confirm passing the `-ho` or `--hosted` option to the `dotnet new` command when creating the solution.

To configure Visual Studio Code debugging assets, see:

* [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0\&pivots=linux-macos) (use the guidance for the *Linux / macOS* operating system regardless of platform)
* [Debug ASP.NET Core Blazor apps](../debug?view=aspnetcore-10.0)

In a command shell, execute the following command:

```
dotnet new blazorwasm -ho -o BlazorWebAssemblySignalRApp
```

The `-ho|--hosted` option creates a hosted Blazor WebAssembly [solution](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln).

The `-o|--output` option creates a folder for the solution. If you've created a folder for the solution and the command shell is open in that folder, omit the `-o|--output` option and value to create the solution.

Confirm that a hosted Blazor WebAssembly app was created: Confirm the presence of a **Client** project and a **Server** project in the app's solution folder. If the two projects aren't present, start over and confirm passing the `-ho` or `--hosted` option to the `dotnet new` command when creating the solution.

### Add the SignalR client library

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)
* [.NET CLI](#tabpanel_6_net-cli)

In **Solution Explorer**, right-click the `BlazorWebAssemblySignalRApp.Client` project and select **Manage NuGet Packages**.

In the **Manage NuGet Packages** dialog, confirm that the **Package source** is set to `nuget.org`.

With **Browse** selected, type `Microsoft.AspNetCore.SignalR.Client` in the search box.

In the search results, select the [`Microsoft.AspNetCore.SignalR.Client` package](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client). Set the version to match the shared framework of the app. Select **Install**.

If the **Preview Changes** dialog appears, select **OK**.

If the **License Acceptance** dialog appears, select **I Accept** if you agree with the license terms.

In the **Terminal** (**Terminal** > **New Terminal** from the menu bar opened to the `BlazorWebAssemblySignalRApp.Client` project using `cd BlazorWebAssemblySignalRApp.Client`), execute the following command:

```
dotnet add Client package Microsoft.AspNetCore.SignalR.Client
```

To add an earlier version of the package, supply the `--version {VERSION}` option, where the `{VERSION}` placeholder is the version of the package to add.

In a command shell from the [solution's](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) folder, execute the following command:

```
dotnet add Client package Microsoft.AspNetCore.SignalR.Client
```

To add an earlier version of the package, supply the `--version {VERSION}` option, where the `{VERSION}` placeholder is the version of the package to add.

### Add a SignalR hub

In the `BlazorWebAssemblySignalRApp.Server` project, create a `Hubs` (plural) folder and add the following `ChatHub` class (`Hubs/ChatHub.cs`):

```
using Microsoft.AspNetCore.SignalR;

namespace BlazorWebAssemblySignalRApp.Server.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

```
using Microsoft.AspNetCore.SignalR;

namespace BlazorWebAssemblySignalRApp.Server.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace BlazorWebAssemblySignalRApp.Server.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
```

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace BlazorWebAssemblySignalRApp.Server.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
```

### Add services and an endpoint for the SignalR hub

In the `BlazorWebAssemblySignalRApp.Server` project, open the `Program.cs` file.

Add the namespace for the `ChatHub` class to the top of the file:

```
using BlazorWebAssemblySignalRApp.Server.Hubs;
```

Add SignalR and response compression middleware services:

```
builder.Services.AddSignalR();
builder.Services.AddResponseCompression(opts =>
{
      opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
         new[] { "application/octet-stream" });
});
```

Use response compression middleware at the top of the processing pipeline's configuration immediately after the line that builds the app:

```
app.UseResponseCompression();
```

Between the endpoints for controllers and the client-side fallback, add an endpoint for the hub. Immediately after the line `app.MapControllers();`, add the following line:

```
app.MapHub<ChatHub>("/chathub");
```

In the `BlazorWebAssemblySignalRApp.Server` project, open the `Startup.cs` file.

Add the namespace for the `ChatHub` class to the top of the file:

```
using BlazorWebAssemblySignalRApp.Server.Hubs;
```

Add SignalR and response compression middleware services:

```
services.AddSignalR();
services.AddResponseCompression(opts =>
{
      opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
         new[] { "application/octet-stream" });
});
```

Use response compression middleware at the top of the processing pipeline's configuration:

```
app.UseResponseCompression();
```

Between the endpoints for controllers and the client-side fallback, add an endpoint for the hub immediately after the line `endpoints.MapControllers();`:

```
endpoints.MapHub<ChatHub>("/chathub");
```

### Add Razor component code for chat

In the `BlazorWebAssemblySignalRApp.Client` project, open the `Pages/Index.razor` file.

Replace the markup with the following code:

```
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable

<PageTitle>Index</PageTitle>

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection? hubConnection;
    private List<string> messages = new List<string>();
    private string? userInput;
    private string? messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    private async Task Send()
    {
        if (hubConnection is not null)
            {
                await hubConnection.SendAsync("SendMessage", userInput, messageInput);
            }
    }

    public bool IsConnected =>
        hubConnection?.State == HubConnectionState.Connected;

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}
```

```
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable

<PageTitle>Index</PageTitle>

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection? hubConnection;
    private List<string> messages = new List<string>();
    private string? userInput;
    private string? messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    private async Task Send()
    {
        if (hubConnection is not null)
            {
                await hubConnection.SendAsync("SendMessage", userInput, messageInput);
            }
    }

    public bool IsConnected =>
        hubConnection?.State == HubConnectionState.Connected;

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}
```

```
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@implements IAsyncDisposable

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection hubConnection;
    private List<string> messages = new List<string>();
    private string userInput;
    private string messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    async Task Send() =>
        await hubConnection.SendAsync("SendMessage", userInput, messageInput);

    public bool IsConnected =>
        hubConnection.State == HubConnectionState.Connected;

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}
```

```
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@implements IDisposable

<div class="form-group">
    <label>
        User:
        <input @bind="userInput" />
    </label>
</div>
<div class="form-group">
    <label>
        Message:
        <input @bind="messageInput" size="50" />
    </label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>

<hr>

<ul id="messagesList">
    @foreach (var message in messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private HubConnection hubConnection;
    private List<string> messages = new List<string>();
    private string userInput;
    private string messageInput;

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var encodedMsg = $"{user}: {message}";
            messages.Add(encodedMsg);
            StateHasChanged();
        });

        await hubConnection.StartAsync();
    }

    async Task Send() =>
        await hubConnection.SendAsync("SendMessage", userInput, messageInput);

    public bool IsConnected =>
        hubConnection.State == HubConnectionState.Connected;

    public void Dispose()
    {
        _ = hubConnection?.DisposeAsync();
    }
}
```

Note

Disable response compression middleware in the `Development` environment when using [Hot Reload](../../test/hot-reload?view=aspnetcore-10.0). For more information, see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#disable-response-compression-for-hot-reload).

### Run the app

Follow the guidance for your tooling:

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)
* [.NET CLI](#tabpanel_7_net-cli)

In **Solution Explorer**, select the `BlazorWebAssemblySignalRApp.Server` project. Press `F5` to run the app with debugging or `Ctrl`+`F5` to run the app without debugging.

Important

When executing a hosted Blazor WebAssembly app, run the app from the [solution's](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) **Server** project.

Google Chrome or Microsoft Edge must be the selected browser for a debugging session.

If the app fails to start in the browser:

* In the .NET console, confirm that the solution is running from the "Server" project.
* Refresh the browser using the browser's reload button.

For information on configuring VS Code assets in the `.vscode` folder, see the **Linux** operating system guidance in [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0).

Press `F5` to run the app with debugging or `Ctrl`+`F5` to run the app without debugging.

Important

When executing a hosted Blazor WebAssembly app, run the app from the [solution's](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) **Server** project.

Google Chrome or Microsoft Edge must be the selected browser for a debugging session.

If the app fails to start in the browser:

* In the .NET console, confirm that the solution is running from the "Server" project.
* Refresh the browser using the browser's reload button.

In a command shell from the [solution's](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) folder, execute the following command to change the directory to the `Server` project folder:

```
cd Server
```

Run the server project with the `dotnet watch` (or `dotnet run`) command:

```
dotnet watch
```

Important

When executing a hosted Blazor WebAssembly app, run the app from the solution's **Server** project.

If the app fails to start in the browser:

* In the .NET console, confirm that the solution is running from the "Server" project.
* Refresh the browser using the browser's reload button.

Copy the URL from the address bar, open another browser instance or tab, and paste the URL in the address bar.

Choose either browser, enter a name and message, and select the button to send the message. The name and message are displayed on both pages instantly:

Quotes: *Star Trek VI: The Undiscovered Country* ©1991 [Paramount](https://www.paramountmovies.com/movies/star-trek-vi-the-undiscovered-country)

## Next steps

In this tutorial, you learned how to:

* Create a Blazor app
* Add the SignalR client library
* Add a SignalR hub
* Add SignalR services and an endpoint for the SignalR hub
* Add a Razor component code for chat

For detailed guidance on the SignalR and Blazor frameworks, see the following reference documentation sets:

[Overview of ASP.NET Core SignalR](../../signalr/introduction?view=aspnetcore-10.0) [ASP.NET Core Blazor](../?view=aspnetcore-10.0)

## Related content

* [Bearer token authentication with Identity Server, WebSockets, and Server-Sent Events](../../signalr/authn-and-authz?view=aspnetcore-10.0#bearer-token-authentication)
* [Secure a SignalR hub in Blazor WebAssembly apps](../security/webassembly/?view=aspnetcore-10.0#secure-a-signalr-hub)
* [SignalR cross-origin negotiation for authentication](../fundamentals/signalr?view=aspnetcore-10.0#client-side-signalr-cross-origin-negotiation-for-authentication)
* [SignalR configuration](../host-and-deploy/server/?view=aspnetcore-10.0#signalr-configuration)
* [Debug ASP.NET Core Blazor apps](../debug?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/test/troubleshoot
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Troubleshoot and debug ASP.NET Core projects

By [Rick Anderson](https://twitter.com/RickAndMSFT)

The following links provide troubleshooting guidance:

* [Troubleshoot ASP.NET Core on Azure App Service and IIS](troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0)
* [NDC Conference (London, 2018): Diagnosing issues in ASP.NET Core Applications](https://www.youtube.com/watch?v=RYI0DHoIVaA)
* [ASP.NET Blog: Troubleshooting ASP.NET Core Performance Problems](https://blogs.msdn.microsoft.com/webdev/2018/05/23/asp-net-core-performance-improvements/)

## .NET Core SDK warnings

### Both the 32-bit and 64-bit versions of the .NET Core SDK are installed

In the **New Project** dialog for ASP.NET Core, you may see the following warning:

> Both 32-bit and 64-bit versions of the .NET Core SDK are installed. Only templates from the 64-bit versions installed at 'C:\Program Files\dotnet\sdk\\' are displayed.

This warning appears when both 32-bit (x86) and 64-bit (x64) versions of the [.NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core) are installed. Common reasons both versions may be installed include:

* You originally downloaded the .NET Core SDK installer using a 32-bit machine but then copied it across and installed it on a 64-bit machine.
* The 32-bit .NET Core SDK was installed by another application.
* The wrong version was downloaded and installed.

Uninstall the 32-bit .NET Core SDK to prevent this warning. Uninstall from **Control Panel** > **Programs and Features** > **Uninstall or change a program**. If you understand why the warning occurs and its implications, you can ignore the warning.

### The .NET Core SDK is installed in multiple locations

In the **New Project** dialog for ASP.NET Core, you may see the following warning:

> The .NET Core SDK is installed in multiple locations. Only templates from the SDKs installed at 'C:\Program Files\dotnet\sdk\\' are displayed.

You see this message when you have at least one installation of the .NET Core SDK in a directory outside of *C:\Program Files\dotnet\sdk\\*. Usually this happens when the .NET Core SDK has been deployed on a machine using copy/paste instead of the MSI installer.

Uninstall all 32-bit .NET Core SDKs and runtimes to prevent this warning. Uninstall from **Control Panel** > **Programs and Features** > **Uninstall or change a program**. If you understand why the warning occurs and its implications, you can ignore the warning.

### No .NET Core SDKs were detected

* In the Visual Studio **New Project** dialog for ASP.NET Core, you may see the following warning:

  > No .NET Core SDKs were detected, ensure they are included in the environment variable `PATH`.

* When executing a `dotnet` command, the warning appears as:

  > It was not possible to find any installed dotnet SDKs.

These warnings appear when the environment variable `PATH` doesn't point to any .NET Core SDKs on the machine. To resolve this problem:

* Install the .NET Core SDK. Obtain the latest installer from [.NET Downloads](https://dotnet.microsoft.com/download).
* Verify that the `PATH` environment variable points to the location where the SDK is installed (`C:\Program Files\dotnet\` for 64-bit/x64 or `C:\Program Files (x86)\dotnet\` for 32-bit/x86). The SDK installer normally sets the `PATH`. Always install the same bitness SDKs and runtimes on the same machine.

### Missing SDK after installing the .NET Core Hosting Bundle

Installing the [.NET Core Hosting Bundle](../host-and-deploy/iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle) modifies the `PATH` when it installs the .NET Core runtime to point to the 32-bit (x86) version of .NET Core (`C:\Program Files (x86)\dotnet\`). This can result in missing SDKs when the 32-bit (x86) .NET Core `dotnet` command is used ([No .NET Core SDKs were detected](#no-net-core-sdks-were-detected)). To resolve this problem, move `C:\Program Files\dotnet\` to a position before `C:\Program Files (x86)\dotnet\` on the `PATH`.

## Obtain data from an app

If an app is capable of responding to requests, you can obtain the following data from the app using middleware:

* Request: Method, scheme, host, pathbase, path, query string, headers
* Connection: Remote IP address, remote port, local IP address, local port, client certificate
* Identity: Name, display name
* Configuration settings
* Environment variables

Place the following [middleware](../fundamentals/middleware/?view=aspnetcore-10.0#create-a-middleware-pipeline-with-iapplicationbuilder) code at the beginning of the `Startup.Configure` method's request processing pipeline. The environment is checked before the middleware is run to ensure that the code is only executed in the `Development` environment.

Get the environment from the `Environment` property of `WebApplication`. For example, `if (app.Environment.IsDevelopment())` as in the following sample code.

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.IISIntegration;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

if (app.Environment.IsDevelopment())
{
    app.Run(async (context) =>
    {
        var sb = new StringBuilder();
        var nl = System.Environment.NewLine;
        var rule = string.Concat(nl, new string('-', 40), nl);


        var authSchemeProvider = app.Services.
                       GetRequiredService<IAuthenticationSchemeProvider>();

        sb.Append($"Request{rule}");
        sb.Append($"{DateTimeOffset.Now}{nl}");
        sb.Append($"{context.Request.Method} {context.Request.Path}{nl}");
        sb.Append($"Scheme: {context.Request.Scheme}{nl}");
        sb.Append($"Host: {context.Request.Headers["Host"]}{nl}");
        sb.Append($"PathBase: {context.Request.PathBase.Value}{nl}");
        sb.Append($"Path: {context.Request.Path.Value}{nl}");
        sb.Append($"Query: {context.Request.QueryString.Value}{nl}{nl}");

        sb.Append($"Connection{rule}");
        sb.Append($"RemoteIp: {context.Connection.RemoteIpAddress}{nl}");
        sb.Append($"RemotePort: {context.Connection.RemotePort}{nl}");
        sb.Append($"LocalIp: {context.Connection.LocalIpAddress}{nl}");
        sb.Append($"LocalPort: {context.Connection.LocalPort}{nl}");
        sb.Append($"ClientCert: {context.Connection.ClientCertificate}{nl}{nl}");

        sb.Append($"Identity{rule}");
        sb.Append($"User: {context.User.Identity.Name}{nl}");
        var scheme = await authSchemeProvider
            .GetSchemeAsync(IISDefaults.AuthenticationScheme);
        sb.Append($"DisplayName: {scheme?.DisplayName}{nl}{nl}");

        sb.Append($"Headers{rule}");
        foreach (var header in context.Request.Headers)
        {
            sb.Append($"{header.Key}: {header.Value}{nl}");
        }
        sb.Append(nl);

        sb.Append($"WebSockets{rule}");
        if (context.Features.Get<IHttpUpgradeFeature>() != null)
        {
            sb.Append($"Status: Enabled{nl}{nl}");
        }
        else
        {
            sb.Append($"Status: Disabled{nl}{nl}");
        }

        sb.Append($"Configuration{rule}");
        var config = builder.Configuration;

         foreach (var pair in config.AsEnumerable())
        {
            sb.Append($"{pair.Key}: {pair.Value}{nl}"); 
        }
        sb.Append(nl);
        sb.Append(nl);

        sb.Append($"Environment Variables{rule}");
        var vars = System.Environment.GetEnvironmentVariables();
        foreach (var key in vars.Keys.Cast<string>().OrderBy(key => key,
            StringComparer.OrdinalIgnoreCase))
        {
            var value = vars[key];
            sb.Append($"{key}: {value}{nl}");
        }

        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync(sb.ToString());
    });
}

app.Run();
```

## Debug ASP.NET Core apps

The following links provide information on debugging ASP.NET Core apps.

* [Debugging ASP Core on Linux](https://devblogs.microsoft.com/premier-developer/debugging-asp-core-on-linux-with-visual-studio-2017/)
* [Debugging .NET Core on Unix over SSH](https://devblogs.microsoft.com/devops/debugging-net-core-on-unix-over-ssh/)
* [Quickstart: Debug ASP.NET with the Visual Studio debugger](/en-us/visualstudio/debugger/quickstart-debug-aspnet)
* See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/2960) for more debugging information.

By [Rick Anderson](https://twitter.com/RickAndMSFT)

The following links provide troubleshooting guidance:

* [Troubleshoot ASP.NET Core on Azure App Service and IIS](troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0)
* [NDC Conference (London, 2018): Diagnosing issues in ASP.NET Core Applications](https://www.youtube.com/watch?v=RYI0DHoIVaA)
* [ASP.NET Blog: Troubleshooting ASP.NET Core Performance Problems](https://blogs.msdn.microsoft.com/webdev/2018/05/23/asp-net-core-performance-improvements/)

## .NET Core SDK warnings

### Both the 32-bit and 64-bit versions of the .NET Core SDK are installed

In the **New Project** dialog for ASP.NET Core, you may see the following warning:

> Both 32-bit and 64-bit versions of the .NET Core SDK are installed. Only templates from the 64-bit versions installed at 'C:\Program Files\dotnet\sdk\\' are displayed.

This warning appears when both 32-bit (x86) and 64-bit (x64) versions of the [.NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core) are installed. Common reasons both versions may be installed include:

* You originally downloaded the .NET Core SDK installer using a 32-bit machine but then copied it across and installed it on a 64-bit machine.
* The 32-bit .NET Core SDK was installed by another application.
* The wrong version was downloaded and installed.

Uninstall the 32-bit .NET Core SDK to prevent this warning. Uninstall from **Control Panel** > **Programs and Features** > **Uninstall or change a program**. If you understand why the warning occurs and its implications, you can ignore the warning.

### The .NET Core SDK is installed in multiple locations

In the **New Project** dialog for ASP.NET Core, you may see the following warning:

> The .NET Core SDK is installed in multiple locations. Only templates from the SDKs installed at 'C:\Program Files\dotnet\sdk\\' are displayed.

You see this message when you have at least one installation of the .NET Core SDK in a directory outside of *C:\Program Files\dotnet\sdk\\*. Usually this happens when the .NET Core SDK has been deployed on a machine using copy/paste instead of the MSI installer.

Uninstall all 32-bit .NET Core SDKs and runtimes to prevent this warning. Uninstall from **Control Panel** > **Programs and Features** > **Uninstall or change a program**. If you understand why the warning occurs and its implications, you can ignore the warning.

### No .NET Core SDKs were detected

* In the Visual Studio **New Project** dialog for ASP.NET Core, you may see the following warning:

  > No .NET Core SDKs were detected, ensure they are included in the environment variable `PATH`.

* When executing a `dotnet` command, the warning appears as:

  > It was not possible to find any installed dotnet SDKs.

These warnings appear when the environment variable `PATH` doesn't point to any .NET Core SDKs on the machine. To resolve this problem:

* Install the .NET Core SDK. Obtain the latest installer from [.NET Downloads](https://dotnet.microsoft.com/download).
* Verify that the `PATH` environment variable points to the location where the SDK is installed (`C:\Program Files\dotnet\` for 64-bit/x64 or `C:\Program Files (x86)\dotnet\` for 32-bit/x86). The SDK installer normally sets the `PATH`. Always install the same bitness SDKs and runtimes on the same machine.

### Missing SDK after installing the .NET Core Hosting Bundle

Installing the [.NET Core Hosting Bundle](../host-and-deploy/iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle) modifies the `PATH` when it installs the .NET Core runtime to point to the 32-bit (x86) version of .NET Core (`C:\Program Files (x86)\dotnet\`). This can result in missing SDKs when the 32-bit (x86) .NET Core `dotnet` command is used ([No .NET Core SDKs were detected](#no-net-core-sdks-were-detected)). To resolve this problem, move `C:\Program Files\dotnet\` to a position before `C:\Program Files (x86)\dotnet\` on the `PATH`.

## Obtain data from an app

If an app is capable of responding to requests, you can obtain the following data from the app using middleware:

* Request: Method, scheme, host, pathbase, path, query string, headers
* Connection: Remote IP address, remote port, local IP address, local port, client certificate
* Identity: Name, display name
* Configuration settings
* Environment variables

Place the following [middleware](../fundamentals/middleware/?view=aspnetcore-10.0#create-a-middleware-pipeline-with-iapplicationbuilder) code at the beginning of the `Startup.Configure` method's request processing pipeline. The environment is checked before the middleware is run to ensure that the code is only executed in the `Development` environment.

To obtain the environment, use either of the following approaches:

* Inject the `IHostingEnvironment` into the `Startup.Configure` method and check the environment with the local variable. The following sample code demonstrates this approach.

* Assign the environment to a property in the `Startup` class. Check the environment using the property (for example, `if (Environment.IsDevelopment())`).

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
    IConfiguration config)
{
    if (env.IsDevelopment())
    {
        app.Run(async (context) =>
        {
            var sb = new StringBuilder();
            var nl = System.Environment.NewLine;
            var rule = string.Concat(nl, new string('-', 40), nl);
            var authSchemeProvider = app.ApplicationServices
                .GetRequiredService<IAuthenticationSchemeProvider>();

            sb.Append($"Request{rule}");
            sb.Append($"{DateTimeOffset.Now}{nl}");
            sb.Append($"{context.Request.Method} {context.Request.Path}{nl}");
            sb.Append($"Scheme: {context.Request.Scheme}{nl}");
            sb.Append($"Host: {context.Request.Headers["Host"]}{nl}");
            sb.Append($"PathBase: {context.Request.PathBase.Value}{nl}");
            sb.Append($"Path: {context.Request.Path.Value}{nl}");
            sb.Append($"Query: {context.Request.QueryString.Value}{nl}{nl}");

            sb.Append($"Connection{rule}");
            sb.Append($"RemoteIp: {context.Connection.RemoteIpAddress}{nl}");
            sb.Append($"RemotePort: {context.Connection.RemotePort}{nl}");
            sb.Append($"LocalIp: {context.Connection.LocalIpAddress}{nl}");
            sb.Append($"LocalPort: {context.Connection.LocalPort}{nl}");
            sb.Append($"ClientCert: {context.Connection.ClientCertificate}{nl}{nl}");

            sb.Append($"Identity{rule}");
            sb.Append($"User: {context.User.Identity.Name}{nl}");
            var scheme = await authSchemeProvider
                .GetSchemeAsync(IISDefaults.AuthenticationScheme);
            sb.Append($"DisplayName: {scheme?.DisplayName}{nl}{nl}");

            sb.Append($"Headers{rule}");
            foreach (var header in context.Request.Headers)
            {
                sb.Append($"{header.Key}: {header.Value}{nl}");
            }
            sb.Append(nl);

            sb.Append($"WebSockets{rule}");
            if (context.Features.Get<IHttpUpgradeFeature>() != null)
            {
                sb.Append($"Status: Enabled{nl}{nl}");
            }
            else
            {
                sb.Append($"Status: Disabled{nl}{nl}");
            }

            sb.Append($"Configuration{rule}");
            foreach (var pair in config.AsEnumerable())
            {
                sb.Append($"{pair.Path}: {pair.Value}{nl}");
            }
            sb.Append(nl);

            sb.Append($"Environment Variables{rule}");
            var vars = System.Environment.GetEnvironmentVariables();
            foreach (var key in vars.Keys.Cast<string>().OrderBy(key => key, 
                StringComparer.OrdinalIgnoreCase))
            {
                var value = vars[key];
                sb.Append($"{key}: {value}{nl}");
            }

            context.Response.ContentType = "text/plain";
            await context.Response.WriteAsync(sb.ToString());
        });
    }
}
```

## Debug ASP.NET Core apps

The following links provide information on debugging ASP.NET Core apps.

* [Debugging ASP Core on Linux](https://devblogs.microsoft.com/premier-developer/debugging-asp-core-on-linux-with-visual-studio-2017/)
* [Debugging .NET Core on Unix over SSH](https://devblogs.microsoft.com/devops/debugging-net-core-on-unix-over-ssh/)
* [Quickstart: Debug ASP.NET with the Visual Studio debugger](/en-us/visualstudio/debugger/quickstart-debug-aspnet)
* See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/2960) for more debugging information.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/identity.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Introduction to Identity on ASP.NET Core

ASP.NET Core Identity:

* Is an API that supports user interface (UI) login functionality.
* Manages users, passwords, profile data, roles, claims, tokens, email confirmation, and more.

Users can create an account with the login information stored in Identity or they can use an external login provider. Supported external login providers include [Facebook, Google, Microsoft Account, and Twitter](social/?view=aspnetcore-10.0).

For information on how to require authentication for all app users, see [Require global user authentication](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication).

The [Identity source code](https://github.com/dotnet/AspNetCore/tree/main/src/Identity) is available on GitHub. [Scaffold Identity](scaffold-identity?view=aspnetcore-10.0) and view the generated files to review the template interaction with Identity.

Identity is typically configured using a SQL Server database to store user names, passwords, and profile data. Alternatively, another persistent store can be used, for example, Azure Table Storage.

In this topic, you learn how to use Identity to register, log in, and log out a user. Note: the templates treat username and email as the same for users. For more detailed instructions about creating apps that use Identity, see [Next Steps](#next).

For more information on Identity in Blazor apps, see [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0) and the articles that follow it in the Blazor documentation.

ASP.NET Core Identity isn't related to the [Microsoft identity platform](/en-us/azure/active-directory/develop/). Microsoft identity platform is:

* An evolution of the Azure Active Directory (Azure AD) developer platform.
* An alternative identity solution for authentication and authorization in ASP.NET Core apps.

[Duende Software](https://duendesoftware.com/) might require you to pay a license fee for production use of Duende Identity Server. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../../migration/50-to-60?view=aspnetcore-10.0#project-templates-use-duende-identity-server).

For more information, see the [Duende Identity Server documentation (Duende Software website)](https://docs.duendesoftware.com).

[View or download the sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/identity/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

[]()

## Create a Blazor Web App with authentication

Create an ASP.NET Core Blazor Web App project with Individual Accounts.

Note

For a Razor Pages experience, see the [Create a Razor Pages app with authentication](#create-a-razor-pages-app-with-authentication) section.

For an MVC experience, see the [Create an MVC app with authentication](#create-an-mvc-app-with-authentication) section.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

- Select the **Blazor Web App** template. Select **Next**.

- Make the following selections:

  * **Authentication type**: **Individual Accounts**
  * **Interactive render mode**: **Server**
  * **Interactivity Location**: **Global**

- Select **Create**.

```
dotnet new blazor -au Individual -o BlazorApp1
```

The `-o|--output` option creates a folder for the app and sets the app name/namespace.

The preceding command creates a Blazor Web App using SQLite. To create the app with LocalDB, run the following command:

```
dotnet new blazor -au Individual -uld -o BlazorApp1
```

The generated project includes Identity Razor components. The components are found in the `Components/Account` folder of the server project. For example:

* `Components/Account/Pages/Register.razor`
* `Components/Account/Pages/Login.razor`
* `Components/Account/Pages/Manage/ChangePassword.razor`

Identity Razor components are described individually in the documentation for specific use cases and are subject to change each release. When you generate a Blazor Web App with Individual Accounts, Identity Razor components are included in the generated project. The Identity Razor components can also be inspected in the `Components/Account` folder of the server project in the [Blazor Web App project template (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

For more information, see [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0) and the articles that follow it in the Blazor documentation. Most of the articles in the *Security and Identity* area of the main ASP.NET Core documentation set apply to Blazor apps. However, the Blazor documentation set contains articles and guidance that supersedes or adds information. We recommend studying the general ASP.NET Core documentation set first, followed by accessing the articles in the Blazor *Security and Identity* documentation.

## Create a Razor Pages app with authentication

Create an ASP.NET Core Web Application (Razor Pages) project with Individual Accounts.

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

- Select the **ASP.NET Core Web App (Razor Pages)** template. Select **Next**.
- For **Authentication type**, select **Individual Accounts**.
- Select **Create**.

```
dotnet new webapp -au Individual -o WebApp1
```

The `-o|--output` option creates a folder for the app and sets the app name/namespace.

The preceding command creates a Razor Pages app using SQLite. To create the app with LocalDB, run the following command:

```
dotnet new webapp -au Individual -uld -o WebApp1
```

The generated project provides [ASP.NET Core Identity](identity?view=aspnetcore-10.0) as a [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0). The Identity Razor class library exposes endpoints with the `Identity` area. For example:

* `Areas/Identity/Pages/Account/Register`
* `Areas/Identity/Pages/Account/Login`
* `Areas/Identity/Pages/Account/Manage/ChangePassword`

Pages are described individually in the documentation for specific use cases and are subject to change each release. To view all of the pages in the RCL, see the [ASP.NET Core reference source (`dotnet/aspnetcore` GitHub repository, `Identity/UI/src/Areas/Identity/Pages` folder)](https://github.com/dotnet/aspnetcore/tree/main/src/Identity/UI/src/Areas/Identity/Pages). You can *scaffold* individual pages or all of the pages into the app. For more information, see [Scaffold Identity in ASP.NET Core projects](scaffold-identity?view=aspnetcore-10.0).

## Create an MVC app with authentication

Create an ASP.NET Core MVC project with Individual Accounts.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

- Select the **ASP.NET Core Web App (Model-View-Controller)** template. Select **Next**.
- For **Authentication type**, select **Individual Accounts**.
- Select **Create**.

```
dotnet new mvc -au Individual -o WebApplication1
```

The `-o|--output` option creates a folder for the app and sets the app name/namespace.

The preceding command creates an MVC app using SQLite. To create the web app with LocalDB, run the following command:

```
dotnet new mvc -au Individual -uld -o WebApplication1
```

The generated project provides [ASP.NET Core Identity](identity?view=aspnetcore-10.0) as a [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0). The Identity Razor class library is based on Razor Pages and exposes endpoints with the `Identity` area. For example:

* `Areas/Identity/Pages/Account/Register`
* `Areas/Identity/Pages/Account/Login`
* `Areas/Identity/Pages/Account/Manage/ChangePassword`

Pages are described individually in the documentation for specific use cases and are subject to change each release. To view all of the pages in the RCL, see the [ASP.NET Core reference source (`dotnet/aspnetcore` GitHub repository, `Identity/UI/src/Areas/Identity/Pages` folder)](https://github.com/dotnet/aspnetcore/tree/main/src/Identity/UI/src/Areas/Identity/Pages). You can *scaffold* individual pages or all of the pages into the app. For more information, see [Scaffold Identity in ASP.NET Core projects](scaffold-identity?view=aspnetcore-10.0).

### Apply migrations

Apply the migrations to initialize the database.

* [Visual Studio](#tabpanel_4_visual-studio)
* [.NET CLI](#tabpanel_4_net-cli)

Run the following command in the Package Manager Console (PMC):

`Update-Database`

Migrations are not necessary at this step when using SQLite.

If `dotnet ef` has not been installed, install it as a global tool:

```
  dotnet tool install --global dotnet-ef
```

For more information on the CLI for EF Core, see [EF Core tools reference for the .NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

For LocalDB, run the following command:

```
dotnet ef database update
```

### Test Register and Login

Run the app and register a user. Depending on your screen size, you might need to select the navigation toggle button to see the **Register** and **Login** links.

### View the Identity database

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

- From the **View** menu, select **SQL Server Object Explorer** (SSOX).
- Navigate to **(localdb)MSSQLLocalDB(SQL Server 13)**. Right-click on **dbo.AspNetUsers** > **View Data**:

There are many third party tools you can download to manage and view a SQLite database, for example [DB Browser for SQLite](https://sqlitebrowser.org/).

[]()

### Configure Identity services

Services are added in `Program.cs`. The typical pattern is to call methods in the following order:

1. `Add{Service}`
2. `builder.Services.Configure{Service}`

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebApp1.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.Configure<IdentityOptions>(options =>
{
    // Password settings.
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequireUppercase = true;
    options.Password.RequiredLength = 6;
    options.Password.RequiredUniqueChars = 1;

    // Lockout settings.
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;

    // User settings.
    options.User.AllowedUserNameCharacters =
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = false;
});

builder.Services.ConfigureApplicationCookie(options =>
{
    // Cookie settings
    options.Cookie.HttpOnly = true;
    options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

    options.LoginPath = "/Identity/Account/Login";
    options.AccessDeniedPath = "/Identity/Account/AccessDenied";
    options.SlidingExpiration = true;
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code configures Identity with default option values. Services are made available to the app through [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

Identity is enabled by calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication). `UseAuthentication` adds authentication [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) to the request pipeline.

The template-generated app doesn't use [authorization](../authorization/secure-data?view=aspnetcore-10.0). `app.UseAuthorization` is included to ensure it's added in the correct order should the app add authorization. `UseRouting`, `UseAuthentication`, and `UseAuthorization` must be called in the order shown in the preceding code.

For more information on `IdentityOptions`, see [IdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions) and [Application Startup](../../fundamentals/startup?view=aspnetcore-10.0).

## ASP.NET Core Identity metrics

ASP.NET Core Identity metrics provide monitoring capabilities for user management and authentication processes. These metrics help you detect unusual sign-in patterns that might indicate security threats, track the performance of identity operations, and understand how users interact with authentication features, such as two-factor authentication. This observability is particularly valuable for apps with strict security requirements or those experiencing high authentication traffic.

For complete details on available metrics and how to use them, see [ASP.NET Core metrics](../../metrics/overview?view=aspnetcore-10.0#aspnet-core-identity-metrics).

## Scaffold Register, Login, LogOut, and RegisterConfirmation

* [Visual Studio](#tabpanel_5_visual-studio)
* [.NET CLI](#tabpanel_5_net-cli)

Add the `Register`, `Login`, `LogOut`, and `RegisterConfirmation` files. Follow the [Scaffold identity into a Razor project with authorization](scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-razor-project-with-authorization) instructions to generate the code shown in this section.

If you created the project with name **WebApp1**, and you're not using SQLite, run the following commands. Otherwise, use the correct namespace for the `ApplicationDbContext`:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet aspnet-codegenerator identity -dc WebApp1.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout;Account.RegisterConfirmation"
```

When using SQLite, append `--useSqLite` or `-sqlite`:

```
dotnet aspnet-codegenerator identity -dc WebApp1.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout;Account.RegisterConfirmation" --useSqLite
```

PowerShell uses semicolon as a command separator. When using PowerShell, escape the semicolons in the file list or put the file list in double quotes, as the preceding example shows.

For more information on scaffolding Identity, see [Scaffold identity into a Razor project with authorization](scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-razor-project-with-authorization).

### Examine Register

When a user clicks the **Register** button on the `Register` page, the `RegisterModel.OnPostAsync` action is invoked. The user is created by [CreateAsync(TUser)](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1.createasync#microsoft-aspnetcore-identity-usermanager-1-createasync\(-0\)) on the `_userManager` object:

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
                                          .ToList();
    if (ModelState.IsValid)
    {
        var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
        var result = await _userManager.CreateAsync(user, Input.Password);
        if (result.Succeeded)
        {
            _logger.LogInformation("User created a new account with password.");

            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            var callbackUrl = Url.Page(
                "/Account/ConfirmEmail",
                pageHandler: null,
                values: new { area = "Identity", userId = user.Id, code = code },
                protocol: Request.Scheme);

            await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

            if (_userManager.Options.SignIn.RequireConfirmedAccount)
            {
                return RedirectToPage("RegisterConfirmation", 
                                      new { email = Input.Email });
            }
            else
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}
```

[]()

### Disable default account verification

With the default templates, the user is redirected to the `Account.RegisterConfirmation` where they can select a link to have the account confirmed. The default `Account.RegisterConfirmation` is used ***only*** for testing, automatic account verification should be disabled in a production app.

To require a confirmed account and prevent immediate login at registration, set `DisplayConfirmAccountLink = false` in `/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs`:

```
[AllowAnonymous]
public class RegisterConfirmationModel : PageModel
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly IEmailSender _sender;

    public RegisterConfirmationModel(UserManager<IdentityUser> userManager, IEmailSender sender)
    {
        _userManager = userManager;
        _sender = sender;
    }

    public string Email { get; set; }

    public bool DisplayConfirmAccountLink { get; set; }

    public string EmailConfirmationUrl { get; set; }

    public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
    {
        if (email == null)
        {
            return RedirectToPage("/Index");
        }

        var user = await _userManager.FindByEmailAsync(email);
        if (user == null)
        {
            return NotFound($"Unable to load user with email '{email}'.");
        }

        Email = email;
        // Once you add a real email sender, you should remove this code that lets you confirm the account
        DisplayConfirmAccountLink = false;
        if (DisplayConfirmAccountLink)
        {
            var userId = await _userManager.GetUserIdAsync(user);
            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            EmailConfirmationUrl = Url.Page(
                "/Account/ConfirmEmail",
                pageHandler: null,
                values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                protocol: Request.Scheme);
        }

        return Page();
    }
}
```

### Log in

The Login form is displayed when:

* The **Log in** link is selected.
* A user attempts to access a restricted page that they aren't authorized to access **or** when they haven't been authenticated by the system.

When the form on the Login page is submitted, the `OnPostAsync` action is called. `PasswordSignInAsync` is called on the `_signInManager` object.

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, 
        // set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(Input.Email,
                           Input.Password, Input.RememberMe, lockoutOnFailure: true);
        if (result.Succeeded)
        {
            _logger.LogInformation("User logged in.");
            return LocalRedirect(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToPage("./LoginWith2fa", new
            {
                ReturnUrl = returnUrl,
                RememberMe = Input.RememberMe
            });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning("User account locked out.");
            return RedirectToPage("./Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return Page();
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}
```

For information on how to make authorization decisions, see [Introduction to authorization in ASP.NET Core](../authorization/introduction?view=aspnetcore-10.0).

### Log out

The **Log out** link invokes the `LogoutModel.OnPost` action.

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace WebApp1.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class LogoutModel : PageModel
    {
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LogoutModel> _logger;

        public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger)
        {
            _signInManager = signInManager;
            _logger = logger;
        }

        public void OnGet()
        {
        }

        public async Task<IActionResult> OnPost(string returnUrl = null)
        {
            await _signInManager.SignOutAsync();
            _logger.LogInformation("User logged out.");
            if (returnUrl != null)
            {
                return LocalRedirect(returnUrl);
            }
            else
            {
                return RedirectToPage();
            }
        }
    }
}
```

In the preceding code, the code `return RedirectToPage();` needs to be a redirect so that the browser performs a new request and the identity for the user gets updated.

[SignOutAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.signoutasync) clears the user's claims stored in a cookie.

Post is specified in the `Pages/Shared/_LoginPartial.cshtml`:

```
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a  class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" 
                                              title="Manage">Hello @User.Identity.Name!</a>
    </li>
    <li class="nav-item">
        <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" 
                                  asp-route-returnUrl="@Url.Page("/", new { area = "" })" 
                                  method="post" >
            <button  type="submit" class="nav-link btn btn-link text-dark">Logout</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
    </li>
}
</ul>
```

## Test Identity

The default web project templates allow anonymous access to the home pages. To test Identity, add [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace WebApp1.Pages
{
    [Authorize]
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }
}
```

If you are signed in, sign out. Run the app and select the **Privacy** link. You are redirected to the login page.

### Explore Identity

To explore Identity in more detail:

* [Create full identity UI source](scaffold-identity?view=aspnetcore-10.0#create-full-identity-ui-source)
* Examine the source of each page and step through the debugger.

## Identity Components

All the Identity-dependent NuGet packages are included in the [ASP.NET Core shared framework](../../release-notes/aspnetcore-3.0?view=aspnetcore-10.0#use-the-aspnet-core-shared-framework).

The primary package for Identity is [Microsoft.AspNetCore.Identity](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity/). This package contains the core set of interfaces for ASP.NET Core Identity, and is included by `Microsoft.AspNetCore.Identity.EntityFrameworkCore`.

## Migrating to ASP.NET Core Identity

For more information and guidance on migrating your existing Identity store, see [Migrate Authentication and Identity](../../migration/fx-to-core/examples/identity?view=aspnetcore-10.0).

## Setting password strength

See [Configuration](#pw6) for a sample that sets the minimum password requirements.

## AddDefaultIdentity and AddIdentity

[AddDefaultIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionuiextensions.adddefaultidentity) was introduced in ASP.NET Core 2.1. Calling `AddDefaultIdentity` is similar to calling the following:

* [AddIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentity)
* [AddDefaultUI](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderuiextensions.adddefaultui)
* [AddDefaultTokenProviders](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderextensions.adddefaulttokenproviders)

See [AddDefaultIdentity source](https://github.com/dotnet/AspNetCore/blob/release/3.1/src/Identity/UI/src/IdentityServiceCollectionUIExtensions.cs#L47-L63) for more information.

## Prevent publish of static Identity assets

To prevent publishing static Identity assets (stylesheets and JavaScript files for Identity UI) to the web root, add the following `ResolveStaticWebAssetsInputsDependsOn` property and `RemoveIdentityAssets` target to the app's project file:

```
<PropertyGroup>
  <ResolveStaticWebAssetsInputsDependsOn>RemoveIdentityAssets</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>

<Target Name="RemoveIdentityAssets">
  <ItemGroup>
    <StaticWebAsset Remove="@(StaticWebAsset)" Condition="%(SourceId) == 'Microsoft.AspNetCore.Identity.UI'" />
  </ItemGroup>
</Target>
```

[]()

## Next Steps

* [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0)
* [ASP.NET Core Identity source code](https://github.com/dotnet/aspnetcore/tree/main/src/Identity)
* [How to work with Roles in ASP.NET Core Identity](https://www.yogihosting.com/aspnet-core-identity-roles/)

- For information on configuring Identity using SQLite, see [How to config Identity for SQLite (`dotnet/AspNetCore.Docs` #5131)](https://github.com/dotnet/AspNetCore.Docs/issues/5131).
- [Configure Identity](identity-configuration?view=aspnetcore-10.0)
- [Create an ASP.NET Core app with user data protected by authorization](../authorization/secure-data?view=aspnetcore-10.0)
- [Add, download, and delete user data to Identity in an ASP.NET Core project](add-user-data?view=aspnetcore-10.0)
- [Enable QR code generation for TOTP authentication](identity-enable-qrcodes?view=aspnetcore-10.0)
- [Migrate Authentication and Identity to ASP.NET Core](../../migration/fx-to-core/examples/identity?view=aspnetcore-10.0)
- [Account confirmation and password recovery](accconfirm?view=aspnetcore-10.0)
- [Multi-factor authentication in ASP.NET Core](mfa?view=aspnetcore-10.0)
- [Host ASP.NET Core in a web farm](../../host-and-deploy/web-farm?view=aspnetcore-10.0)

By [Rick Anderson](https://twitter.com/RickAndMSFT)

ASP.NET Core Identity:

* Is an API that supports user interface (UI) login functionality.
* Manages users, passwords, profile data, roles, claims, tokens, email confirmation, and more.

Users can create an account with the login information stored in Identity or they can use an external login provider. Supported external login providers include [Facebook, Google, Microsoft Account, and Twitter](social/?view=aspnetcore-10.0).

For information on how to require authentication for all app users, see [Require global user authentication](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication).

The [Identity source code](https://github.com/dotnet/AspNetCore/tree/main/src/Identity) is available on GitHub. [Scaffold Identity](scaffold-identity?view=aspnetcore-10.0) and view the generated files to review the template interaction with Identity.

Identity is typically configured using a SQL Server database to store user names, passwords, and profile data. Alternatively, another persistent store can be used, for example, Azure Table Storage.

In this topic, you learn how to use Identity to register, log in, and log out a user. Note: the templates treat username and email as the same for users. For more detailed instructions about creating apps that use Identity, see [Next Steps](#next).

ASP.NET Core Identity isn't related to the [Microsoft identity platform](/en-us/azure/active-directory/develop/). Microsoft identity platform is:

* An evolution of the Azure Active Directory (Azure AD) developer platform.
* An alternative identity solution for authentication and authorization in ASP.NET Core apps.

[Duende Software](https://duendesoftware.com/) might require you to pay a license fee for production use of Duende Identity Server. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../../migration/50-to-60?view=aspnetcore-10.0#project-templates-use-duende-identity-server).

For more information, see the [Duende Identity Server documentation (Duende Software website)](https://docs.duendesoftware.com).

[View or download the sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/identity/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

[]()

## Create a Web app with authentication

Create an ASP.NET Core Web Application project with Individual User Accounts.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

- Select the **ASP.NET Core Web App** template. Name the project **WebApp1** to have the same namespace as the project download. Click **OK**.
- In the **Authentication type** input, select **Individual User Accounts**.

```
dotnet new webapp --auth Individual -o WebApp1
```

The preceding command creates a Razor web app using SQLite. To create the web app with LocalDB, run the following command:

```
dotnet new webapp --auth Individual -uld -o WebApp1
```

The generated project provides [ASP.NET Core Identity](identity?view=aspnetcore-10.0) as a [Razor class library](../../razor-pages/ui-class?view=aspnetcore-10.0). The Identity Razor class library exposes endpoints with the `Identity` area. For example:

* /Identity/Account/Login
* /Identity/Account/Logout
* /Identity/Account/Manage

### Apply migrations

Apply the migrations to initialize the database.

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

Run the following command in the Package Manager Console (PMC):

`Update-Database`

Migrations are not necessary at this step when using SQLite.

If `dotnet ef` has not been installed, install it as a global tool:

```
  dotnet tool install --global dotnet-ef
```

For more information on the CLI for EF Core, see [EF Core tools reference for the .NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

For LocalDB, run the following command:

```
dotnet ef database update
```

### Test Register and Login

Run the app and register a user. Depending on your screen size, you might need to select the navigation toggle button to see the **Register** and **Login** links.

### View the Identity database

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

- From the **View** menu, select **SQL Server Object Explorer** (SSOX).
- Navigate to **(localdb)MSSQLLocalDB(SQL Server 13)**. Right-click on **dbo.AspNetUsers** > **View Data**:

There are many third party tools you can download to manage and view a SQLite database, for example [DB Browser for SQLite](https://sqlitebrowser.org/).

[]()

### Configure Identity services

Services are added in `Program.cs`. The typical pattern is to call methods in the following order:

1. `Add{Service}`
2. `builder.Services.Configure{Service}`

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebApp1.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.Configure<IdentityOptions>(options =>
{
    // Password settings.
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequireUppercase = true;
    options.Password.RequiredLength = 6;
    options.Password.RequiredUniqueChars = 1;

    // Lockout settings.
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;

    // User settings.
    options.User.AllowedUserNameCharacters =
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = false;
});

builder.Services.ConfigureApplicationCookie(options =>
{
    // Cookie settings
    options.Cookie.HttpOnly = true;
    options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

    options.LoginPath = "/Identity/Account/Login";
    options.AccessDeniedPath = "/Identity/Account/AccessDenied";
    options.SlidingExpiration = true;
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code configures Identity with default option values. Services are made available to the app through [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

Identity is enabled by calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication). `UseAuthentication` adds authentication [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) to the request pipeline.

The template-generated app doesn't use [authorization](../authorization/secure-data?view=aspnetcore-10.0). `app.UseAuthorization` is included to ensure it's added in the correct order should the app add authorization. `UseRouting`, `UseAuthentication`, and `UseAuthorization` must be called in the order shown in the preceding code.

For more information on `IdentityOptions`, see [IdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions) and [Application Startup](../../fundamentals/startup?view=aspnetcore-10.0).

## Scaffold Register, Login, LogOut, and RegisterConfirmation

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

Add the `Register`, `Login`, `LogOut`, and `RegisterConfirmation` files. Follow the [Scaffold identity into a Razor project with authorization](scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-razor-project-with-authorization) instructions to generate the code shown in this section.

If you created the project with name **WebApp1**, and you're not using SQLite, run the following commands. Otherwise, use the correct namespace for the `ApplicationDbContext`:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet aspnet-codegenerator identity -dc WebApp1.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout;Account.RegisterConfirmation"
```

When using SQLite, append `--useSqLite` or `-sqlite`:

```
dotnet aspnet-codegenerator identity -dc WebApp1.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout;Account.RegisterConfirmation" --useSqLite
```

PowerShell uses semicolon as a command separator. When using PowerShell, escape the semicolons in the file list or put the file list in double quotes, as the preceding example shows.

For more information on scaffolding Identity, see [Scaffold identity into a Razor project with authorization](scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-razor-project-with-authorization).

### Examine Register

When a user clicks the **Register** button on the `Register` page, the `RegisterModel.OnPostAsync` action is invoked. The user is created by [CreateAsync(TUser)](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1.createasync#microsoft-aspnetcore-identity-usermanager-1-createasync\(-0\)) on the `_userManager` object:

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
                                          .ToList();
    if (ModelState.IsValid)
    {
        var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
        var result = await _userManager.CreateAsync(user, Input.Password);
        if (result.Succeeded)
        {
            _logger.LogInformation("User created a new account with password.");

            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            var callbackUrl = Url.Page(
                "/Account/ConfirmEmail",
                pageHandler: null,
                values: new { area = "Identity", userId = user.Id, code = code },
                protocol: Request.Scheme);

            await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

            if (_userManager.Options.SignIn.RequireConfirmedAccount)
            {
                return RedirectToPage("RegisterConfirmation", 
                                      new { email = Input.Email });
            }
            else
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}
```

[]()

### Disable default account verification

With the default templates, the user is redirected to the `Account.RegisterConfirmation` where they can select a link to have the account confirmed. The default `Account.RegisterConfirmation` is used ***only*** for testing, automatic account verification should be disabled in a production app.

To require a confirmed account and prevent immediate login at registration, set `DisplayConfirmAccountLink = false` in `/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs`:

```
[AllowAnonymous]
public class RegisterConfirmationModel : PageModel
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly IEmailSender _sender;

    public RegisterConfirmationModel(UserManager<IdentityUser> userManager, IEmailSender sender)
    {
        _userManager = userManager;
        _sender = sender;
    }

    public string Email { get; set; }

    public bool DisplayConfirmAccountLink { get; set; }

    public string EmailConfirmationUrl { get; set; }

    public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
    {
        if (email == null)
        {
            return RedirectToPage("/Index");
        }

        var user = await _userManager.FindByEmailAsync(email);
        if (user == null)
        {
            return NotFound($"Unable to load user with email '{email}'.");
        }

        Email = email;
        // Once you add a real email sender, you should remove this code that lets you confirm the account
        DisplayConfirmAccountLink = false;
        if (DisplayConfirmAccountLink)
        {
            var userId = await _userManager.GetUserIdAsync(user);
            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            EmailConfirmationUrl = Url.Page(
                "/Account/ConfirmEmail",
                pageHandler: null,
                values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                protocol: Request.Scheme);
        }

        return Page();
    }
}
```

### Log in

The Login form is displayed when:

* The **Log in** link is selected.
* A user attempts to access a restricted page that they aren't authorized to access **or** when they haven't been authenticated by the system.

When the form on the Login page is submitted, the `OnPostAsync` action is called. `PasswordSignInAsync` is called on the `_signInManager` object.

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, 
        // set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(Input.Email,
                           Input.Password, Input.RememberMe, lockoutOnFailure: true);
        if (result.Succeeded)
        {
            _logger.LogInformation("User logged in.");
            return LocalRedirect(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToPage("./LoginWith2fa", new
            {
                ReturnUrl = returnUrl,
                RememberMe = Input.RememberMe
            });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning("User account locked out.");
            return RedirectToPage("./Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return Page();
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}
```

For information on how to make authorization decisions, see [Introduction to authorization in ASP.NET Core](../authorization/introduction?view=aspnetcore-10.0).

### Log out

The **Log out** link invokes the `LogoutModel.OnPost` action.

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace WebApp1.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class LogoutModel : PageModel
    {
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LogoutModel> _logger;

        public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger)
        {
            _signInManager = signInManager;
            _logger = logger;
        }

        public void OnGet()
        {
        }

        public async Task<IActionResult> OnPost(string returnUrl = null)
        {
            await _signInManager.SignOutAsync();
            _logger.LogInformation("User logged out.");
            if (returnUrl != null)
            {
                return LocalRedirect(returnUrl);
            }
            else
            {
                return RedirectToPage();
            }
        }
    }
}
```

In the preceding code, the code `return RedirectToPage();` needs to be a redirect so that the browser performs a new request and the identity for the user gets updated.

[SignOutAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.signoutasync) clears the user's claims stored in a cookie.

Post is specified in the `Pages/Shared/_LoginPartial.cshtml`:

```
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a  class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" 
                                              title="Manage">Hello @User.Identity.Name!</a>
    </li>
    <li class="nav-item">
        <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" 
                                  asp-route-returnUrl="@Url.Page("/", new { area = "" })" 
                                  method="post" >
            <button  type="submit" class="nav-link btn btn-link text-dark">Logout</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
    </li>
}
</ul>
```

## Test Identity

The default web project templates allow anonymous access to the home pages. To test Identity, add [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace WebApp1.Pages
{
    [Authorize]
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }
}
```

If you are signed in, sign out. Run the app and select the **Privacy** link. You are redirected to the login page.

### Explore Identity

To explore Identity in more detail:

* [Create full identity UI source](scaffold-identity?view=aspnetcore-10.0#create-full-identity-ui-source)
* Examine the source of each page and step through the debugger.

## Identity Components

All the Identity-dependent NuGet packages are included in the [ASP.NET Core shared framework](../../release-notes/aspnetcore-3.0?view=aspnetcore-10.0#use-the-aspnet-core-shared-framework).

The primary package for Identity is [Microsoft.AspNetCore.Identity](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity/). This package contains the core set of interfaces for ASP.NET Core Identity, and is included by `Microsoft.AspNetCore.Identity.EntityFrameworkCore`.

## Migrating to ASP.NET Core Identity

For more information and guidance on migrating your existing Identity store, see [Migrate Authentication and Identity](../../migration/fx-to-core/examples/identity?view=aspnetcore-10.0).

## Setting password strength

See [Configuration](#pw6) for a sample that sets the minimum password requirements.

## AddDefaultIdentity and AddIdentity

[AddDefaultIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionuiextensions.adddefaultidentity) was introduced in ASP.NET Core 2.1. Calling `AddDefaultIdentity` is similar to calling the following:

* [AddIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentity)
* [AddDefaultUI](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderuiextensions.adddefaultui)
* [AddDefaultTokenProviders](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderextensions.adddefaulttokenproviders)

See [AddDefaultIdentity source](https://github.com/dotnet/AspNetCore/blob/release/3.1/src/Identity/UI/src/IdentityServiceCollectionUIExtensions.cs#L47-L63) for more information.

## Prevent publish of static Identity assets

To prevent publishing static Identity assets (stylesheets and JavaScript files for Identity UI) to the web root, add the following `ResolveStaticWebAssetsInputsDependsOn` property and `RemoveIdentityAssets` target to the app's project file:

```
<PropertyGroup>
  <ResolveStaticWebAssetsInputsDependsOn>RemoveIdentityAssets</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>

<Target Name="RemoveIdentityAssets">
  <ItemGroup>
    <StaticWebAsset Remove="@(StaticWebAsset)" Condition="%(SourceId) == 'Microsoft.AspNetCore.Identity.UI'" />
  </ItemGroup>
</Target>
```

[]()

## Next Steps

* [ASP.NET Core Identity source code](https://github.com/dotnet/aspnetcore/tree/main/src/Identity)
* [How to work with Roles in ASP.NET Core Identity](https://www.yogihosting.com/aspnet-core-identity-roles/)

- See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/5131) for information on configuring Identity using SQLite.
- [Configure Identity](identity-configuration?view=aspnetcore-10.0)
- [Create an ASP.NET Core app with user data protected by authorization](../authorization/secure-data?view=aspnetcore-10.0)
- [Add, download, and delete user data to Identity in an ASP.NET Core project](add-user-data?view=aspnetcore-10.0)
- [Enable QR code generation for TOTP authentication](identity-enable-qrcodes?view=aspnetcore-10.0)
- [Migrate Authentication and Identity to ASP.NET Core](../../migration/fx-to-core/examples/identity?view=aspnetcore-10.0)
- [Account confirmation and password recovery](accconfirm?view=aspnetcore-10.0)
- [Two-factor authentication with SMS in ASP.NET Core](2fa?view=aspnetcore-10.0)
- [Host ASP.NET Core in a web farm](../../host-and-deploy/web-farm?view=aspnetcore-10.0)

By [Rick Anderson](https://twitter.com/RickAndMSFT)

ASP.NET Core Identity:

* Is an API that supports user interface (UI) login functionality.
* Manages users, passwords, profile data, roles, claims, tokens, email confirmation, and more.

Users can create an account with the login information stored in Identity or they can use an external login provider. Supported external login providers include [Facebook, Google, Microsoft Account, and Twitter](social/?view=aspnetcore-10.0).

For information on how to require authentication for all app users, see [Require global user authentication](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication).

The [Identity source code](https://github.com/dotnet/AspNetCore/tree/main/src/Identity) is available on GitHub. [Scaffold Identity](scaffold-identity?view=aspnetcore-10.0) and view the generated files to review the template interaction with Identity.

Identity is typically configured using a SQL Server database to store user names, passwords, and profile data. Alternatively, another persistent store can be used, for example, Azure Table Storage.

In this topic, you learn how to use Identity to register, log in, and log out a user. Note: the templates treat username and email as the same for users. For more detailed instructions about creating apps that use Identity, see [Next Steps](#next).

[Microsoft identity platform](/en-us/azure/active-directory/develop/) is:

* An evolution of the Azure Active Directory (Azure AD) developer platform.
* An alternative identity solution for authentication and authorization in ASP.NET Core apps.
* Not related to ASP.NET Core Identity.

ASP.NET Core Identity adds user interface (UI) login functionality to ASP.NET Core web apps. To secure web APIs and SPAs, use one of the following:

* [Microsoft Entra ID](/en-us/azure/api-management/api-management-howto-protect-backend-with-aad)
* [Duende IdentityServer](https://docs.duendesoftware.com). Duende IdentityServer is 3rd party product.

Duende IdentityServer is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core. Duende IdentityServer enables the following security features:

* Authentication as a Service (AaaS)
* Single sign-on/off (SSO) over multiple application types
* Access control for APIs
* Federation Gateway

For more information, see [Overview of Duende IdentityServer](https://docs.duendesoftware.com).

For more information on other authentication providers, see [Community OSS authentication options for ASP.NET Core](community?view=aspnetcore-10.0)

[View or download the sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/identity/sample) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

[]()

## Create a Web app with authentication

Create an ASP.NET Core Web Application project with Individual User Accounts.

* [Visual Studio](#tabpanel_4_visual-studio)
* [.NET CLI](#tabpanel_4_net-cli)

- Select **File** > **New** > **Project**.
- Select **ASP.NET Core Web Application**. Name the project **WebApp1** to have the same namespace as the project download. Click **OK**.
- Select an ASP.NET Core **Web Application**, then select **Change Authentication**.
- Select **Individual User Accounts** and click **OK**.

```
dotnet new webapp --auth Individual -o WebApp1
```

The preceding command creates a Razor web app using SQLite. To create the web app with LocalDB, run the following command:

```
dotnet new webapp --auth Individual -uld -o WebApp1
```

The generated project provides [ASP.NET Core Identity](identity?view=aspnetcore-10.0) as a [Razor class library](../../razor-pages/ui-class?view=aspnetcore-10.0). The Identity Razor class library exposes endpoints with the `Identity` area. For example:

* /Identity/Account/Login
* /Identity/Account/Logout
* /Identity/Account/Manage

### Apply migrations

Apply the migrations to initialize the database.

* [Visual Studio](#tabpanel_5_visual-studio)
* [.NET CLI](#tabpanel_5_net-cli)

Run the following command in the Package Manager Console (PMC):

`PM> Update-Database`

Migrations are not necessary at this step when using SQLite.

If `dotnet ef` has not been installed, install it as a global tool:

```
  dotnet tool install --global dotnet-ef
```

For more information on the CLI for EF Core, see [EF Core tools reference for the .NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

For LocalDB, run the following command:

```
dotnet ef database update
```

### Test Register and Login

Run the app and register a user. Depending on your screen size, you might need to select the navigation toggle button to see the **Register** and **Login** links.

### View the Identity database

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

- From the **View** menu, select **SQL Server Object Explorer** (SSOX).
- Navigate to **(localdb)MSSQLLocalDB(SQL Server 13)**. Right-click on **dbo.AspNetUsers** > **View Data**:

There are many third party tools you can download to manage and view a SQLite database, for example [DB Browser for SQLite](https://sqlitebrowser.org/).

[]()

### Configure Identity services

Services are added in `ConfigureServices`. The typical pattern is to call all the `Add{Service}` methods, and then call all the `services.Configure{Service}` methods.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
     // options.UseSqlite(
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();

    services.Configure<IdentityOptions>(options =>
    {
        // Password settings.
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireUppercase = true;
        options.Password.RequiredLength = 6;
        options.Password.RequiredUniqueChars = 1;

        // Lockout settings.
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        options.Lockout.MaxFailedAccessAttempts = 5;
        options.Lockout.AllowedForNewUsers = true;

        // User settings.
        options.User.AllowedUserNameCharacters =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
        options.User.RequireUniqueEmail = false;
    });

    services.ConfigureApplicationCookie(options =>
    {
        // Cookie settings
        options.Cookie.HttpOnly = true;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

        options.LoginPath = "/Identity/Account/Login";
        options.AccessDeniedPath = "/Identity/Account/AccessDenied";
        options.SlidingExpiration = true;
    });
}
```

The preceding highlighted code configures Identity with default option values. Services are made available to the app through [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

Identity is enabled by calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication). `UseAuthentication` adds authentication [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) to the request pipeline.

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        // options.UseSqlite(
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDatabaseDeveloperPageExceptionFilter();
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();

    services.Configure<IdentityOptions>(options =>
    {
        // Password settings.
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireUppercase = true;
        options.Password.RequiredLength = 6;
        options.Password.RequiredUniqueChars = 1;

        // Lockout settings.
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        options.Lockout.MaxFailedAccessAttempts = 5;
        options.Lockout.AllowedForNewUsers = true;

        // User settings.
        options.User.AllowedUserNameCharacters =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
        options.User.RequireUniqueEmail = false;
    });

    services.ConfigureApplicationCookie(options =>
    {
        // Cookie settings
        options.Cookie.HttpOnly = true;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

        options.LoginPath = "/Identity/Account/Login";
        options.AccessDeniedPath = "/Identity/Account/AccessDenied";
        options.SlidingExpiration = true;
    });
}
```

The preceding code configures Identity with default option values. Services are made available to the app through [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

Identity is enabled by calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication). `UseAuthentication` adds authentication [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) to the request pipeline.

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseMigrationsEndPoint();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

The template-generated app doesn't use [authorization](../authorization/secure-data?view=aspnetcore-10.0). `app.UseAuthorization` is included to ensure it's added in the correct order should the app add authorization. `UseRouting`, `UseAuthentication`, `UseAuthorization`, and `UseEndpoints` must be called in the order shown in the preceding code.

For more information on `IdentityOptions` and `Startup`, see [IdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions) and [Application Startup](../../fundamentals/startup?view=aspnetcore-10.0).

## Scaffold Register, Login, LogOut, and RegisterConfirmation

* [Visual Studio](#tabpanel_6_visual-studio)
* [.NET CLI](#tabpanel_6_net-cli)

Add the `Register`, `Login`, `LogOut`, and `RegisterConfirmation` files. Follow the [Scaffold identity into a Razor project with authorization](scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-razor-project-with-authorization) instructions to generate the code shown in this section.

If you created the project with name **WebApp1**, and you're not using SQLite, run the following commands. Otherwise, use the correct namespace for the `ApplicationDbContext`:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet aspnet-codegenerator identity -dc WebApp1.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout;Account.RegisterConfirmation"
```

When using SQLite, append `--useSqLite` or `-sqlite`:

```
dotnet aspnet-codegenerator identity -dc WebApp1.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout;Account.RegisterConfirmation" --useSqLite
```

PowerShell uses semicolon as a command separator. When using PowerShell, escape the semicolons in the file list or put the file list in double quotes, as the preceding example shows.

For more information on scaffolding Identity, see [Scaffold identity into a Razor project with authorization](scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-razor-project-with-authorization).

### Examine Register

When a user clicks the **Register** button on the `Register` page, the `RegisterModel.OnPostAsync` action is invoked. The user is created by [CreateAsync(TUser)](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1.createasync#microsoft-aspnetcore-identity-usermanager-1-createasync\(-0\)) on the `_userManager` object:

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
                                          .ToList();
    if (ModelState.IsValid)
    {
        var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
        var result = await _userManager.CreateAsync(user, Input.Password);
        if (result.Succeeded)
        {
            _logger.LogInformation("User created a new account with password.");

            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            var callbackUrl = Url.Page(
                "/Account/ConfirmEmail",
                pageHandler: null,
                values: new { area = "Identity", userId = user.Id, code = code },
                protocol: Request.Scheme);

            await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

            if (_userManager.Options.SignIn.RequireConfirmedAccount)
            {
                return RedirectToPage("RegisterConfirmation", 
                                      new { email = Input.Email });
            }
            else
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}
```

[]()

### Disable default account verification

With the default templates, the user is redirected to the `Account.RegisterConfirmation` where they can select a link to have the account confirmed. The default `Account.RegisterConfirmation` is used ***only*** for testing, automatic account verification should be disabled in a production app.

To require a confirmed account and prevent immediate login at registration, set `DisplayConfirmAccountLink = false` in `/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs`:

```
[AllowAnonymous]
public class RegisterConfirmationModel : PageModel
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly IEmailSender _sender;

    public RegisterConfirmationModel(UserManager<IdentityUser> userManager, IEmailSender sender)
    {
        _userManager = userManager;
        _sender = sender;
    }

    public string Email { get; set; }

    public bool DisplayConfirmAccountLink { get; set; }

    public string EmailConfirmationUrl { get; set; }

    public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
    {
        if (email == null)
        {
            return RedirectToPage("/Index");
        }

        var user = await _userManager.FindByEmailAsync(email);
        if (user == null)
        {
            return NotFound($"Unable to load user with email '{email}'.");
        }

        Email = email;
        // Once you add a real email sender, you should remove this code that lets you confirm the account
        DisplayConfirmAccountLink = false;
        if (DisplayConfirmAccountLink)
        {
            var userId = await _userManager.GetUserIdAsync(user);
            var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            EmailConfirmationUrl = Url.Page(
                "/Account/ConfirmEmail",
                pageHandler: null,
                values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                protocol: Request.Scheme);
        }

        return Page();
    }
}
```

### Log in

The Login form is displayed when:

* The **Log in** link is selected.
* A user attempts to access a restricted page that they aren't authorized to access **or** when they haven't been authenticated by the system.

When the form on the Login page is submitted, the `OnPostAsync` action is called. `PasswordSignInAsync` is called on the `_signInManager` object.

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, 
        // set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(Input.Email,
                           Input.Password, Input.RememberMe, lockoutOnFailure: true);
        if (result.Succeeded)
        {
            _logger.LogInformation("User logged in.");
            return LocalRedirect(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToPage("./LoginWith2fa", new
            {
                ReturnUrl = returnUrl,
                RememberMe = Input.RememberMe
            });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning("User account locked out.");
            return RedirectToPage("./Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return Page();
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}
```

For information on how to make authorization decisions, see [Introduction to authorization in ASP.NET Core](../authorization/introduction?view=aspnetcore-10.0).

### Log out

The **Log out** link invokes the `LogoutModel.OnPost` action.

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace WebApp1.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class LogoutModel : PageModel
    {
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LogoutModel> _logger;

        public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger)
        {
            _signInManager = signInManager;
            _logger = logger;
        }

        public void OnGet()
        {
        }

        public async Task<IActionResult> OnPost(string returnUrl = null)
        {
            await _signInManager.SignOutAsync();
            _logger.LogInformation("User logged out.");
            if (returnUrl != null)
            {
                return LocalRedirect(returnUrl);
            }
            else
            {
                return RedirectToPage();
            }
        }
    }
}
```

In the preceding code, the code `return RedirectToPage();` needs to be a redirect so that the browser performs a new request and the identity for the user gets updated.

[SignOutAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.signoutasync) clears the user's claims stored in a cookie.

Post is specified in the `Pages/Shared/_LoginPartial.cshtml`:

```
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a  class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" 
                                              title="Manage">Hello @User.Identity.Name!</a>
    </li>
    <li class="nav-item">
        <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" 
                                  asp-route-returnUrl="@Url.Page("/", new { area = "" })" 
                                  method="post" >
            <button  type="submit" class="nav-link btn btn-link text-dark">Logout</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
    </li>
}
</ul>
```

## Test Identity

The default web project templates allow anonymous access to the home pages. To test Identity, add [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace WebApp1.Pages
{
    [Authorize]
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }
}
```

If you are signed in, sign out. Run the app and select the **Privacy** link. You are redirected to the login page.

### Explore Identity

To explore Identity in more detail:

* [Create full identity UI source](scaffold-identity?view=aspnetcore-10.0#create-full-identity-ui-source)
* Examine the source of each page and step through the debugger.

## Identity Components

All the Identity-dependent NuGet packages are included in the [ASP.NET Core shared framework](../../release-notes/aspnetcore-3.0?view=aspnetcore-10.0#use-the-aspnet-core-shared-framework).

The primary package for Identity is [Microsoft.AspNetCore.Identity](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity/). This package contains the core set of interfaces for ASP.NET Core Identity, and is included by `Microsoft.AspNetCore.Identity.EntityFrameworkCore`.

## Migrating to ASP.NET Core Identity

For more information and guidance on migrating your existing Identity store, see [Migrate Authentication and Identity](../../migration/fx-to-core/examples/identity?view=aspnetcore-10.0).

## Setting password strength

See [Configuration](#pw) for a sample that sets the minimum password requirements.

## Prevent publish of static Identity assets

To prevent publishing static Identity assets (stylesheets and JavaScript files for Identity UI) to the web root, add the following `ResolveStaticWebAssetsInputsDependsOn` property and `RemoveIdentityAssets` target to the app's project file:

```
<PropertyGroup>
  <ResolveStaticWebAssetsInputsDependsOn>RemoveIdentityAssets</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>

<Target Name="RemoveIdentityAssets">
  <ItemGroup>
    <StaticWebAsset Remove="@(StaticWebAsset)" Condition="%(SourceId) == 'Microsoft.AspNetCore.Identity.UI'" />
  </ItemGroup>
</Target>
```

[]()

## Next Steps

* [ASP.NET Core Identity source code](https://github.com/dotnet/aspnetcore/tree/main/src/Identity)
* [AddDefaultIdentity source](https://github.com/dotnet/AspNetCore/blob/release/3.1/src/Identity/UI/src/IdentityServiceCollectionUIExtensions.cs#L47-L63)
* See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/5131) for information on configuring Identity using SQLite.
* [Configure Identity](identity-configuration?view=aspnetcore-10.0)
* [Create an ASP.NET Core app with user data protected by authorization](../authorization/secure-data?view=aspnetcore-10.0)
* [Add, download, and delete user data to Identity in an ASP.NET Core project](add-user-data?view=aspnetcore-10.0)
* [Enable QR code generation for TOTP authentication](identity-enable-qrcodes?view=aspnetcore-10.0)
* [Migrate Authentication and Identity to ASP.NET Core](../../migration/fx-to-core/examples/identity?view=aspnetcore-10.0)
* [Account confirmation and password recovery](accconfirm?view=aspnetcore-10.0)
* [Two-factor authentication with SMS in ASP.NET Core](2fa?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../host-and-deploy/web-farm?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/import-export-interop
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/javascript-interoperability/import-export-interop.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# JavaScript `[JSImport]`/`[JSExport]` interop with ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to interact with JavaScript (JS) in client-side components using JavaScript (JS) `[JSImport]`/`[JSExport]` interop API. For additional information and examples, see [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop in .NET WebAssembly](../../client-side/dotnet-interop/?view=aspnetcore-10.0).

For additional guidance, see the [Configuring and hosting .NET WebAssembly applications](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md) guidance in the .NET Runtime (`dotnet/runtime`) GitHub repository.

Blazor provides its own JS interop mechanism based on the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) interface. Blazor's JS interop is uniformly supported across Blazor render modes and for Blazor Hybrid apps. [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) also enables library authors to build JS interop libraries for sharing across the Blazor ecosystem and remains the recommended approach for JS interop in Blazor. See the following articles:

* [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0)
* [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](call-dotnet-from-javascript?view=aspnetcore-10.0)

This article describes an alternative JS interop approach specific to client-side components executed on WebAssembly. These approaches are appropriate when you only expect to run on client-side WebAssembly. Library authors can use these approaches to optimize JS interop by checking during code execution if the app is running on WebAssembly in a browser ([OperatingSystem.IsBrowser](/en-us/dotnet/api/system.operatingsystem.isbrowser)). The approaches described in this article should be used to replace the obsolete unmarshalled JS interop API when migrating to .NET 7 or later.

Note

This article focuses on JS interop in client-side components. For guidance on calling .NET in JavaScript apps, see [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop with a WebAssembly Browser App project](../../client-side/dotnet-interop/wasm-browser-app?view=aspnetcore-10.0).

## Obsolete JavaScript interop API

Unmarshalled JS interop using [IJSUnmarshalledRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) API is obsolete in ASP.NET Core in .NET 7 or later. Follow the guidance in this article to replace the obsolete API.

## Prerequisites

[Visual Studio](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2022) with the **ASP.NET and web development** workload.

No further tooling is required if you plan on implementing `[JSImport]`/`[JSExport]` interop in a Blazor WebAssembly app generated from the Blazor WebAssembly project template.

If you plan to use the WebAssembly Browser or WebAssembly Console app project templates, install the [`Microsoft.NET.Runtime.WebAssembly.Templates`](https://www.nuget.org/packages/Microsoft.NET.Runtime.WebAssembly.Templates) NuGet package with the following command:

```
dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates
```

For more information, see [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop with a WebAssembly Browser App project](../../client-side/dotnet-interop/wasm-browser-app?view=aspnetcore-10.0#experimental-workload-and-project-templates).

## Namespace

The JS interop API ([JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync)) described in this article is controlled by attributes in the [System.Runtime.InteropServices.JavaScript](/en-us/dotnet/api/system.runtime.interopservices.javascript) namespace.

## Enable unsafe blocks

Enable the [AllowUnsafeBlocks](/en-us/dotnet/api/microsoft.build.tasks.csc.allowunsafeblocks#microsoft-build-tasks-csc-allowunsafeblocks) property in app's project file, which permits the code generator in the Roslyn compiler to use pointers for JS interop:

```
<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
```

Warning

The JS interop API requires enabling [AllowUnsafeBlocks](/en-us/dotnet/api/microsoft.build.tasks.csc.allowunsafeblocks#microsoft-build-tasks-csc-allowunsafeblocks). Be careful when implementing your own unsafe code in .NET apps, which can introduce security and stability risks. For more information, see [Unsafe code, pointer types, and function pointers](/en-us/dotnet/csharp/language-reference/unsafe-code).

## Razor class library (RCL) collocated JS is unsupported

Generally, the JS location support for [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime)-based JS interop ([JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0)) is also present for the `[JSImport]`/`[JSExport]` interop described by this article. The only unsupported JS location feature is for *collocated JS in a Razor class library (RCL)*.

Instead of using collocated JS in an RCL, place the JS file in the RCL's `wwwroot` folder and reference it using the usual path for RCL static assets:

`_content/{PACKAGE ID}/{PATH}/{FILE NAME}.js`

* The `{PACKAGE ID}` placeholder is the RCL's package identifier (or library name for a class library).
* The `{PATH}` placeholder is the path to the file.
* The `{FILE NAME}` placeholder is the file name.

Although collocated JS in an RCL isn't supported by `[JSImport]`/`[JSExport]` interop, you can keep your JS files organized by taking either or both of the following approaches:

* Name the JS file the same as the component where the JS is used. For a component in the RCL named `CallJavaScriptFromLib` (`CallJavaScriptFromLib.razor`), name the file `CallJavaScriptFromLib.js` in the `wwwroot` folder.
* Place component-specific JS files in a `Components` folder inside the RCL's `wwwroot` folder and use "`Components`" in the path to the file: `_content/{PACKAGE ID}/Components/CallJavaScriptFromLib.js`.

## Call JavaScript from .NET

This section explains how to call JS functions from .NET.

In the following `CallJavaScript1` component:

* The `CallJavaScript1` module is imported asynchronously from the [collocated JS file](location-of-javascript?view=aspnetcore-10.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) with [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync).
* The imported `getMessage` JS function is called by `GetWelcomeMessage`.
* The returned welcome message string is displayed in the UI via the `message` field.

`CallJavaScript1.razor`:

```
@page "/call-javascript-1"
@rendermode InteractiveWebAssembly
@using System.Runtime.InteropServices.JavaScript

<h1>
    JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop 
    (Call JS Example 1)
</h1>

@(message is not null ? message : string.Empty)

@code {
    private string? message;

    protected override async Task OnInitializedAsync()
    {
        await JSHost.ImportAsync("CallJavaScript1", 
            "../Components/Pages/CallJavaScript1.razor.js");

        message = GetWelcomeMessage();
    }
}
```

```
@page "/call-javascript-1"
@using System.Runtime.InteropServices.JavaScript

<h1>
    JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop 
    (Call JS Example 1)
</h1>

@(message is not null ? message : string.Empty)

@code {
    private string? message;

    protected override async Task OnInitializedAsync()
    {
        await JSHost.ImportAsync("CallJavaScript1", 
            "../Pages/CallJavaScript1.razor.js");

        message = GetWelcomeMessage();
    }
}
```

Note

Include a conditional check in code with [OperatingSystem.IsBrowser](/en-us/dotnet/api/system.operatingsystem.isbrowser) to ensure that the JS interop is only called by a component rendered on the client. This is important for libraries/NuGet packages that target server-side components, which can't execute the code provided by this JS interop API.

To import a JS function to call it from C#, use the [`[JSImport]` attribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsimportattribute) on a C# method signature that matches the JS function's signature. The first parameter to the `[JSImport]` attribute is the name of the JS function to import, and the second parameter is the name of the [JS module](./?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules).

In the following example, `getMessage` is a JS function that returns a `string` for a module named `CallJavaScript1`. The C# method signature matches: No parameters are passed to the JS function, and the JS function returns a `string`. The JS function is called by `GetWelcomeMessage` in C# code.

`CallJavaScript1.razor.cs`:

```
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

namespace BlazorSample.Components.Pages;

[SupportedOSPlatform("browser")]
public partial class CallJavaScript1
{
    [JSImport("getMessage", "CallJavaScript1")]
    internal static partial string GetWelcomeMessage();
}
```

The app's namespace for the preceding `CallJavaScript1` partial class is `BlazorSample`. The component's namespace is `BlazorSample.Components.Pages`. If using the preceding component in a local test app, update the namespace to match the app. For example, the namespace is `ContosoApp.Components.Pages` if the app's namespace is `ContosoApp`. For more information, see [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#partial-class-support).

```
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

namespace BlazorSample.Pages;

[SupportedOSPlatform("browser")]
public partial class CallJavaScript1
{
    [JSImport("getMessage", "CallJavaScript1")]
    internal static partial string GetWelcomeMessage();
}
```

The app's namespace for the preceding `CallJavaScript1` partial class is `BlazorSample`. The component's namespace is `BlazorSample.Pages`. If using the preceding component in a local test app, update the namespace to match the app. For example, the namespace is `ContosoApp.Pages` if the app's namespace is `ContosoApp`. For more information, see [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#partial-class-support).

In the imported method signature, you can use .NET types for parameters and return values, which are marshalled automatically by the runtime. Use [JSMarshalAsAttribute\<T>](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsmarshalasattribute-1) to control how the imported method parameters are marshalled. For example, you might choose to marshal a `long` as [System.Runtime.InteropServices.JavaScript.JSType.Number](/en-us/dotnet/api/system.runtime.interopservices.javascript.jstype.number) or [System.Runtime.InteropServices.JavaScript.JSType.BigInt](/en-us/dotnet/api/system.runtime.interopservices.javascript.jstype.bigint). You can pass [Action](/en-us/dotnet/api/system.action)/[Func\<TResult>](/en-us/dotnet/api/system.func-1) callbacks as parameters, which are marshalled as callable JS functions. You can pass both JS and managed object references, and they are marshaled as proxy objects, keeping the object alive across the boundary until the proxy is garbage collected. You can also import and export asynchronous methods with a [Task](/en-us/dotnet/api/system.threading.tasks.task) result, which are marshaled as [JS promises](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise). Most of the marshalled types work in both directions, as parameters and as return values, on both imported and exported methods, which are covered in the [Call .NET from JavaScript](#call-net-from-javascript) section later in this article.

For additional type mapping information and examples, see [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop in .NET WebAssembly](../../client-side/dotnet-interop/?view=aspnetcore-10.0#type-mappings).

The module name in the `[JSImport]` attribute and the call to load the module in the component with [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync) must match and be unique in the app. When authoring a library for deployment in a NuGet package, we recommend using the NuGet package namespace as a prefix in module names. In the following example, the module name reflects the `Contoso.InteropServices.JavaScript` package and a folder of user message interop classes (`UserMessages`):

```
[JSImport("getMessage", 
    "Contoso.InteropServices.JavaScript.UserMessages.CallJavaScript1")]
```

Functions accessible on the global namespace can be imported by using the [`globalThis`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/globalThis) prefix in the function name and by using the `[JSImport]` attribute without providing a module name. In the following example, [`console.log`](https://developer.mozilla.org/docs/Web/API/console/log) is prefixed with `globalThis`. The imported function is called by the C# `Log` method, which accepts a C# string message (`message`) and marshalls the C# string to a JS [`String`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) for `console.log`:

```
[JSImport("globalThis.console.log")]
internal static partial void Log([JSMarshalAs<JSType.String>] string message);
```

Export scripts from a standard [JavaScript module](./?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules) either [collocated with a component](location-of-javascript?view=aspnetcore-10.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) or placed with other JavaScript static assets in a JS file (for example, `wwwroot/js/{FILE NAME}.js`, where JS static assets are maintained in a folder named `js` in the app's `wwwroot` folder and the `{FILE NAME}` placeholder is the file name).

In the following example, a JS function named `getMessage` is exported from a collocated JS file that returns a welcome message, "Hello from Blazor!" in Portuguese:

`CallJavaScript1.razor.js`:

```
export function getMessage() {
  return 'Olá do Blazor!';
}
```

## Call .NET from JavaScript

This section explains how to call .NET methods from JS.

The following `CallDotNet1` component calls JS that directly interacts with the DOM to render the welcome message string:

* The `CallDotNet` [JS module](./?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules) is imported asynchronously from the collocated JS file for this component.
* The imported `setMessage` JS function is called by `SetWelcomeMessage`.
* The returned welcome message is displayed by `setMessage` in the UI via the `message` field.

Important

In this section's example, JS interop is used to mutate a DOM element *purely for demonstration purposes* after the component is rendered in [`OnAfterRender`](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync). Typically, you should only mutate the DOM with JS when the object doesn't interact with Blazor. The approach shown in this section is similar to cases where a third-party JS library is used in a Razor component, where the component interacts with the JS library via JS interop, the third-party JS library interacts with part of the DOM, and Blazor isn't involved directly with the DOM updates to that part of the DOM. For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#interaction-with-the-document-object-model-dom).

`CallDotNet1.razor`:

```
@page "/call-dotnet-1"
@rendermode InteractiveWebAssembly
@using System.Runtime.InteropServices.JavaScript

<h1>
    JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop 
    (Call .NET Example 1)
</h1>

<p>
    <span id="result">.NET method not executed yet</span>
</p>

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JSHost.ImportAsync("CallDotNet1", 
                "../Components/Pages/CallDotNet1.razor.js");

            SetWelcomeMessage();
        }
    }
}
```

```
@page "/call-dotnet-1"
@using System.Runtime.InteropServices.JavaScript

<h1>
    JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop 
    (Call .NET Example 1)
</h1>

<p>
    <span id="result">.NET method not executed yet</span>
</p>

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JSHost.ImportAsync("CallDotNet1", 
                "../Pages/CallDotNet1.razor.js");

            SetWelcomeMessage();
        }
    }
}
```

To export a .NET method so that it can be called from JS, use the [`[JSExport]` attribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsexportattribute).

In the following example:

* `SetWelcomeMessage` calls a JS function named `setMessage`. The JS function calls into .NET to receive the welcome message from `GetMessageFromDotnet` and displays the message in the UI.
* `GetMessageFromDotnet` is a .NET method with the `[JSExport]` attribute that returns a welcome message, "Hello from Blazor!" in Portuguese.

`CallDotNet1.razor.cs`:

```
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

namespace BlazorSample.Components.Pages;

[SupportedOSPlatform("browser")]
public partial class CallDotNet1
{
    [JSImport("setMessage", "CallDotNet1")]
    internal static partial void SetWelcomeMessage();

    [JSExport]
    internal static string GetMessageFromDotnet() => "Olá do Blazor!";
}
```

The app's namespace for the preceding `CallDotNet1` partial class is `BlazorSample`. The component's namespace is `BlazorSample.Components.Pages`. If using the preceding component in a local test app, update the app's namespace to match the app. For example, the component namespace is `ContosoApp.Components.Pages` if the app's namespace is `ContosoApp`. For more information, see [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#partial-class-support).

In the following example, a JS function named `setMessage` is imported from a collocated JS file.

The `setMessage` method:

* Calls `globalThis.getDotnetRuntime(0)` to expose the WebAssembly .NET runtime instance for calling exported .NET methods.
* Obtains the app assembly's JS exports. The name of the app's assembly in the following example is `BlazorSample`.
* Calls the `BlazorSample.Components.Pages.CallDotNet1.GetMessageFromDotnet` method from the exports (`exports`). The returned value, which is the welcome message, is assigned to the `CallDotNet1` component's `<span>` text. The app's namespace is `BlazorSample`, and the `CallDotNet1` component's namespace is `BlazorSample.Components.Pages`.

`CallDotNet1.razor.js`:

```
export async function setMessage() {
  const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
  var exports = await getAssemblyExports("BlazorSample.dll");

  document.getElementById("result").innerText = 
    exports.BlazorSample.Components.Pages.CallDotNet1.GetMessageFromDotnet();
}
```

```
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

namespace BlazorSample.Pages;

[SupportedOSPlatform("browser")]
public partial class CallDotNet1
{
    [JSImport("setMessage", "CallDotNet1")]
    internal static partial void SetWelcomeMessage();

    [JSExport]
    internal static string GetMessageFromDotnet() => "Olá do Blazor!";
}
```

The app's namespace for the preceding `CallDotNet1` partial class is `BlazorSample`. The component's namespace is `BlazorSample.Pages`. If using the preceding component in a local test app, update the app's namespace to match the app. For example, the component namespace is `ContosoApp.Pages` if the app's namespace is `ContosoApp`. For more information, see [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#partial-class-support).

In the following example, a JS function named `setMessage` is imported from a collocated JS file.

The `setMessage` method:

* Calls `globalThis.getDotnetRuntime(0)` to expose the WebAssembly .NET runtime instance for calling exported .NET methods.
* Obtains the app assembly's JS exports. The name of the app's assembly in the following example is `BlazorSample`.
* Calls the `BlazorSample.Pages.CallDotNet1.GetMessageFromDotnet` method from the exports (`exports`). The returned value, which is the welcome message, is assigned to the `CallDotNet1` component's `<span>` text. The app's namespace is `BlazorSample`, and the `CallDotNet1` component's namespace is `BlazorSample.Pages`.

`CallDotNet1.razor.js`:

```
export async function setMessage() {
  const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
  var exports = await getAssemblyExports("BlazorSample.dll");

  document.getElementById("result").innerText = 
    exports.BlazorSample.Pages.CallDotNet1.GetMessageFromDotnet();
}
```

Note

Calling `getAssemblyExports` to obtain the exports can occur in a [JavaScript initializer](./?view=aspnetcore-10.0#javascript-initializers) for availability across the app.

## Multiple module import calls

After a JS module is loaded, the module's JS functions are available to the app's components and classes as long as the app is running in the browser window or tab without the user manually reloading the app. [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync) can be called multiple times on the same module without a significant performance penalty when:

* The user visits a component that calls [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync) to import a module, navigates away from the component, and then returns to the component where [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync) is called again for the same module import.
* The same module is used by different components and loaded by [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync) in each of the components.

## Use of a single JavaScript module across components

*Before following the guidance in this section, read the [Call JavaScript from .NET](#call-javascript-from-net) and [Call .NET from JavaScript](#call-net-from-javascript) sections of this article, which provide general guidance on `[JSImport]`/`[JSExport]` interop.*

The example in this section shows how to use JS interop from a shared JS module in a client-side app. The guidance in this section isn't applicable to Razor class libraries (RCLs).

The following components, classes, C# methods, and JS functions are used:

* `Interop` class (`Interop.cs`): Sets up import and export JS interop with the `[JSImport]` and `[JSExport]` attributes for a module named `Interop`.

  * `GetWelcomeMessage`: .NET method that calls the imported `getMessage` JS function.
  * `SetWelcomeMessage`: .NET method that calls the imported `setMessage` JS function.
  * `GetMessageFromDotnet`: An exported C# method that returns a welcome message string when called from JS.

* `wwwroot/js/interop.js` file: Contains the JS functions.

  * `getMessage`: Returns a welcome message when called by C# code in a component.
  * `setMessage`: Calls the `GetMessageFromDotnet` C# method and assigns the returned welcome message to a DOM `<span>` element.

* `Program.cs` calls [JSHost.ImportAsync](/en-us/dotnet/api/system.runtime.interopservices.javascript.jshost.importasync) to load the module from `wwwroot/js/interop.js`.

* `CallJavaScript2` component (`CallJavaScript2.razor`): Calls `GetWelcomeMessage` and displays the returned welcome message in the component's UI.

* `CallDotNet2` component (`CallDotNet2.razor`): Calls `SetWelcomeMessage`.

`Interop.cs`:

```
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

namespace BlazorSample.JavaScriptInterop;

[SupportedOSPlatform("browser")]
public partial class Interop
{
    [JSImport("getMessage", "Interop")]
    internal static partial string GetWelcomeMessage();

    [JSImport("setMessage", "Interop")]
    internal static partial void SetWelcomeMessage();

    [JSExport]
    internal static string GetMessageFromDotnet() => "Olá do Blazor!";
}
```

In the preceding example, the app's namespace is `BlazorSample`, and the full namespace for C# interop classes is `BlazorSample.JavaScriptInterop`.

`wwwroot/js/interop.js`:

```
export function getMessage() {
  return 'Olá do Blazor!';
}

export async function setMessage() {
  const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
  var exports = await getAssemblyExports("BlazorSample.dll");

  document.getElementById("result").innerText =
    exports.BlazorSample.JavaScriptInterop.Interop.GetMessageFromDotnet();
}
```

Make the [System.Runtime.InteropServices.JavaScript](/en-us/dotnet/api/system.runtime.interopservices.javascript) namespace available at the top of the `Program.cs` file:

```
using System.Runtime.InteropServices.JavaScript;
```

Load the module in `Program.cs` before [WebAssemblyHost.RunAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhost.runasync) is called:

```
if (OperatingSystem.IsBrowser())
{
    await JSHost.ImportAsync("Interop", "../js/interop.js");
}
```

`CallJavaScript2.razor`:

```
@page "/call-javascript-2"
@rendermode InteractiveWebAssembly
@using BlazorSample.JavaScriptInterop

<h1>
    JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop 
    (Call JS Example 2)
</h1>

@(message is not null ? message : string.Empty)

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = Interop.GetWelcomeMessage();
    }
}
```

```
@page "/call-javascript-2"
@using BlazorSample.JavaScriptInterop

<h1>
    JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop 
    (Call JS Example 2)
</h1>

@(message is not null ? message : string.Empty)

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = Interop.GetWelcomeMessage();
    }
}
```

`CallDotNet2.razor`:

```
@page "/call-dotnet-2"
@rendermode InteractiveWebAssembly
@using BlazorSample.JavaScriptInterop

<h1>
    JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop  
    (Call .NET Example 2)
</h1>

<p>
    <span id="result">.NET method not executed</span>
</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Interop.SetWelcomeMessage();
        }
    }
}
```

```
@page "/call-dotnet-2"
@using BlazorSample.JavaScriptInterop

<h1>
    JS <code>[JSImport]</code>/<code>[JSExport]</code> Interop  
    (Call .NET Example 2)
</h1>

<p>
    <span id="result">.NET method not executed</span>
</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Interop.SetWelcomeMessage();
        }
    }
}
```

Important

In this section's example, JS interop is used to mutate a DOM element *purely for demonstration purposes* after the component is rendered in [`OnAfterRender`](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync). Typically, you should only mutate the DOM with JS when the object doesn't interact with Blazor. The approach shown in this section is similar to cases where a third-party JS library is used in a Razor component, where the component interacts with the JS library via JS interop, the third-party JS library interacts with part of the DOM, and Blazor isn't involved directly with the DOM updates to that part of the DOM. For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#interaction-with-the-document-object-model-dom).

## Related content

* [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop in .NET WebAssembly](../../client-side/dotnet-interop/?view=aspnetcore-10.0)

* [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop with a WebAssembly Browser App project](../../client-side/dotnet-interop/wasm-browser-app?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0007
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0007.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0007: Route parameter and argument optionality is mismatched

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0007      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A route parameter is declared as required in the Delegate definition but is marked as optional in the endpoint route.

## Rule description

When an endpoint is declared, optionality of parameters can be declared in both the route template and in the route handler arguments. When a parameter is declared as optional in the handler, it must also be declared as optional in the route template. For example, GET `/todos` fails to resolve a match for the following code:

```
app.MapGet("/todos/{id}", (int? id) => {});
```

The preceding code fails to match GET `/todos` because the `id` parameter was not provided, even though it is treated as optional in the route handler.

## How to fix violations

To fix a violation of this rule, make sure that the optionality in the route template and the delegate match. For example, for the following code sample:

```
app.MapGet("/todos/{id}", (int? id) => {});
```

If the parameter is intended to be required, make the type non-nullable by removing the `?` from `int?`:

```
app.MapGet("/todos/{id}", (int id) => {});
```

If the parameter is intended to be optional, then the [nullable value type](/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) operator `?` should be applied:

```
app.MapGet("/todos/{id?}", (int? id) => {});
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule. Mismatched parameter optionality can result in unexpected behavior with routing at runtime.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/bl0004
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/bl0004.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# BL0004: Component parameter should be public

|                                     | Value    |
| ----------------------------------- | -------- |
| **Rule ID**                         | BL0004   |
| **Category**                        | Usage    |
| **Fix is breaking or non-breaking** | Breaking |

## Cause

A property on a type deriving from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) annotated with [`[Parameter]`](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute) is not public.

## Rule description

Component parameters are required to be public and must have a public setter.

```
@code
{
    [Parameter] int Parameter1 { get; set; }
}
```

## How to fix violations

* Make the property and its setter public.

```
@code
{
    [Parameter] public int Parameter1 { get; set; }
}
```

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-05-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/gdpr
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/gdpr.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# EU General Data Protection Regulation (GDPR) support in ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to implement support for [EU General Data Protection Regulation (GDPR)](https://ec.europa.eu/info/law/law-topic/data-protection/reform/what-does-general-data-protection-regulation-gdpr-govern_en) requirements.

In the `Program` file:

* Add [CookiePolicyOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions) configuration to require user consent for non-essential cookies and set the same-site policy to none. For more information, see [Work with SameSite cookies in ASP.NET Core](../../security/samesite?view=aspnetcore-10.0).
* Add the default implementation for the [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) service by calling [AddHttpContextAccessor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpservicecollectionextensions.addhttpcontextaccessor).

```
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;

    options.MinimumSameSitePolicy = SameSiteMode.None;
});

builder.Services.AddHttpContextAccessor();
```

In the `Program` file before the call to [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents), add cookie policy middleware by calling [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy):

In the `Program` file before the call to [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub), add cookie policy middleware by calling [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy):

```
app.UseCookiePolicy();
```

Add the following `CookieConsent` component to handle cookie policy consent.

The component uses a [collocated JavaScript file](../javascript-interoperability/location-of-javascript?view=aspnetcore-10.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component), named `CookieConsent.razor.js`, to load a module. Confirm or adjust the path to the collocated file in the `OnAfterRenderAsync` method. The following component assumes that the component and its companion JavaScript file are in the `Components` folder of the app.

`CookieConsent.razor`:

```
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Http
@implements IAsyncDisposable
@inject IHttpContextAccessor Http
@inject IJSRuntime JS

@if (showBanner)
{
    <div id="cookieConsent" class="alert alert-info alert-dismissible fade show" 
        role="alert">
        Use this space to summarize your privacy and cookie use policy.
        <a href="/privacy">Privacy Policy</a>
        <button type="button" @onclick="AcceptPolicy" class="accept-policy close" 
            data-bs-dismiss="alert" aria-label="Close" 
            data-cookie-string="@cookieString">
            Accept
        </button>
    </div>
}
@code {
    private IJSObjectReference? module;
    private ITrackingConsentFeature? consentFeature;
    private bool showBanner;
    private string? cookieString;

    protected override void OnInitialized()
    {
        consentFeature = Http.HttpContext?.Features.Get<ITrackingConsentFeature>();
        showBanner = !consentFeature?.CanTrack ?? false;
        cookieString = consentFeature?.CreateConsentCookie();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./Components/CookieConsent.razor.js");
        }
    }

    private async Task AcceptPolicy()
    {
        if (module is not null)
        {
            await module.InvokeVoidAsync("acceptPolicy", cookieString);
            showBanner = false;
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

Add the following [collocated JavaScript file](../javascript-interoperability/location-of-javascript?view=aspnetcore-10.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) to maintain the `acceptPolicy` function in a JavaScript module.

`CookieConsent.razor.js`:

```
export function acceptPolicy(cookieString) {
  document.cookie = cookieString;
}
```

Within `<main>` Razor markup of the `MainLayout` component (`MainLayout.razor`), add the `CookieConsent` component:

```
<CookieConsent />
```

## Customize the cookie consent value

Specify the cookie consent value by assigning a custom string to [CookiePolicyOptions.ConsentCookieValue](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.consentcookievalue). The following example changes the default value of "`yes`" to "`true`":

```
options.ConsentCookieValue = "true";
```

In Blazor WebAssembly apps, [local storage](https://developer.mozilla.org/docs/Web/API/Window/localStorage) is a convenient approach for maintaining a user's acceptance of a site's cookie policy. The following approach demonstrates the approach.

If the app doesn't already have a `Shared` folder for shared components, add a `Shared` folder to the app.

Add the namespace for shared components to the imports file (`_Imports.razor`). In the following example, the app's namespace is `BlazorSample`, and the shared folder's namespace is `BlazorSample.Shared`:

```
@using BlazorSample.Shared
```

Add the following `CookieConsent` component to handle cookie policy consent.

`Shared/CookieConsent.razor`:

```
@implements IAsyncDisposable
@inject IJSRuntime JS

@if (showBanner)
{
    <div id="cookieConsent" class="alert alert-info alert-dismissible fade show"
        role="alert">
        Use this space to summarize your privacy and cookie use policy.
        <a href="/privacy">Privacy Policy</a>
        <button type="button" @onclick="AcceptPolicy" class="accept-policy close"
            data-bs-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">Accept</span>
        </button>
    </div>
}
@code {
    private IJSObjectReference? module;
    private bool showBanner = false;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./Shared/CookieConsent.razor.js");
            showBanner = !await module.InvokeAsync<bool>("getCookiePolicyAccepted");
            StateHasChanged();
        }
    }

    private async Task AcceptPolicy()
    {
        if (module is not null)
        {
            await module.InvokeVoidAsync("setCookiePolicyAccepted");
            showBanner = false;
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

Add the following [collocated JavaScript file](../javascript-interoperability/location-of-javascript?view=aspnetcore-10.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) to maintain the `setCookiePolicyAccepted` and `getCookiePolicyAccepted` functions in a JavaScript module.

`Shared/CookieConsent.razor.js`:

```
export function getCookiePolicyAccepted() {
  const cookiePolicy = localStorage.getItem('CookiePolicyAccepted');
  return cookiePolicy === 'yes' ? true : false;
}

export function setCookiePolicyAccepted() {
  localStorage.setItem('CookiePolicyAccepted', 'yes');
}
```

In the preceding example, you can change the name of the local storage item and value from "`CookiePolicyAccepted`" and "`yes`" to any preferred values. If you change one or both values, update both functions.

Within `<main>` Razor markup of the `MainLayout` component (`Layout/MainLayout.razor`), add the `CookieConsent` component:

```
<CookieConsent />
```

## Related content

* [Microsoft Trust Center: Safeguard individual privacy with cloud services from Microsoft: GDPR](https://www.microsoft.com/trust-center/privacy/gdpr-overview)
* [European Commission: Data protection explained](https://ec.europa.eu/info/law/law-topic/data-protection/reform/what-does-general-data-protection-regulation-gdpr-govern_en)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app/part-3
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/movie-database-app/part-3.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor movie database app (Part 3 - Learn about Razor components)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article is the third part of the Blazor movie database app tutorial that teaches you the basics of building an ASP.NET Core Blazor Web App with features to manage a movie database.

This part of the tutorial series examines the Razor components in the project that were scaffolded into the app. Improvements are made for the display of movie data.

## Razor components

Blazor apps are based on *Razor components*, often referred to as just *components*. A *component* is an element of UI, such as a page, dialog, or data entry form. Components are .NET C# classes built into [.NET assemblies](/en-us/dotnet/standard/assembly/).

*Razor* refers to how components are usually written in the form of a [Razor](../../../mvc/views/razor?view=aspnetcore-10.0) markup page (`.razor` file extension) for client-side UI logic and composition. Razor is a syntax for combining HTML markup with C# code designed for developer productivity.

Although developers and online resources use the term "Blazor components," the documentation uses the formal name "Razor components" (or just "components").

The anatomy of a Razor component has the following general pattern:

* At the top of the component definition (`.razor` file), various Razor directives specify how the component markup is compiled or functions.
* Next, Razor markup specifies how HTML is rendered, which includes ordinary HTML elements.
* Finally, an `@code` block contains C# code to define members for the component class, including component parameters and event handlers.

Consider the following `Welcome` component (`Welcome.razor`):

```
@page "/welcome"

<PageTitle>Welcome!</PageTitle>

<h1>Welcome to Blazor!</h1>

<p>@welcomeMessage</p>

@code {
    private string welcomeMessage = "We ❤️ Blazor!";
}
```

The first line represents an important Razor construct in Razor components, a *Razor directive*. A Razor directive is a reserved keyword prefixed with `@` that appears in Razor markup that changes the way component markup is compiled or functions. The `@page` Razor directive specifies the route template for the component. This component is reached in a browser at the relative URL `/welcome`. By convention, most of a component's directives are placed at the top of the component definition file.

The [PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component is a component built into the framework that specifies a page title.

"`Welcome to Blazor!`" is the first rendered body markup of the component per the content of the H1 heading element (`<h1>`).

Next, a welcome message is displayed using Razor syntax by prefixing the at symbol (`@`) to a C# variable (`welcomeMessage`).

The `@code` block contains the C# code of the component. `welcomeMessage` is a private string initialized with a value.

In the following sections of this article:

* Three components for webpage navigation and layout are described, the `NavMenu`, [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink), and `MainLayout` components.
* The components created by scaffolding for CRUD operations on movie database entities are discussed.

## `NavMenu` component for navigation

The `NavMenu` component (`Components/Layout/NavMenu.razor`) implements sidebar navigation using [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) components, which render navigation links to other Razor components.

A [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component behaves like an `<a>` element, except it toggles an `active` CSS class based on whether its `href` matches the current URL. The `active` class helps a user understand which page is the active page among the navigation links displayed. [NavLinkMatch.All](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlinkmatch#microsoft-aspnetcore-components-routing-navlinkmatch-all) assigned to the [Match](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink.match) parameter configures the component to display an active CSS class when it matches the entire current URL.

The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component is built into the Blazor framework for any Blazor app to use, while the `NavMenu` component is only part of Blazor project templates.

`Components/Layout/NavMenu.razor`:

```
<script type="module" src="@Assets["Components/Layout/NavMenu.razor.js"]"></script>

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorWebAppMovies</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div id="nav-scrollable" class="nav-scrollable">
    <nav class="nav flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>
    </nav>
</div>
```

```
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorWebAppMovies</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="nav flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>
    </nav>
</div>
```

```
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorWebAppMovies</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>
    </nav>
</div>
```

Notice in the `NavMenu` component's first `<div>` element the brand link text (`<a>` element content). Change the brand from `BlazorWebAppMovies` to `Sci-fi Movies`:

```
- <a class="navbar-brand" href="">BlazorWebAppMovies</a>
+ <a class="navbar-brand" href="">Sci-fi Movies</a>
```

To allow users to reach the movies `Index` page, add a navigation menu entry to the `NavMenu` component. Immediately after the markup (`<div>`) for the `Weather` component's `NavLink`, add the following markup:

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="movies">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Movies
    </NavLink>
</div>
```

The final `NavMenu` component after making the preceding changes:

```
<script type="module" src="@Assets["Components/Layout/NavMenu.razor.js"]"></script>

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">Sci-fi Movies</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div id="nav-scrollable" class="nav-scrollable">
    <nav class="nav flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="movies">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Movies
            </NavLink>
        </div>
    </nav>
</div>
```

```
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">Sci-fi Movies</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="nav flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="movies">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Movies
            </NavLink>
        </div>
    </nav>
</div>
```

```
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">Sci-fi Movies</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="movies">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Movies
            </NavLink>
        </div>
    </nav>
</div>
```

Run the app to see the updated brand at the top of the sidebar navigation and a link to reach the movies page (**Movies**):

Stop the app by closing the browser's window.

Stop the app by closing the browser's window and pressing `Shift`+`F5` on the keyboard in VS Code.

Stop the app by closing the browser's window and pressing `Ctrl`+`C` in the command shell.

## `MainLayout` component for layout

The `MainLayout` component is the app's default layout. The `MainLayout` component inherits [LayoutComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase), which is a base class for components that represent a layout. The app's components that use the layout are rendered where the [Body](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase.body) (`@Body`) appears in the markup.

`Components/Layout/MainLayout.razor`:

```
@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>
```

The `MainLayout` component adopts the following additional specifications:

* The `NavMenu` component is rendered in the sidebar. Notice that you only need to place an HTML tag with the component name to render a component at that location in Razor markup. This allows you to nest components within each other and within any HTML layout that you implement.

* The `<main>` element's content includes:

  * An **About** link that sends the user to the ASP.NET Core documentation landing page.
  * An `<article>` element with the [Body](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase.body) (`@Body`) parameter, where components that use the layout are rendered.
  * The error UI (`<div id="blazor-error-ui" ...>`), where a notice about an unhandled error is displayed.

The default layout (`MainLayout` component) is specified in the `Routes` component (`Components/Routes.razor`):

```
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
```

Individual components are free to set their own non-default layout, and a layout can be applied to whole folder of components via an imports file (`_Imports.razor`) in the same folder. These features are covered in detail in the Blazor documentation.

## Create, Read, Update, Delete (CRUD) components

The following sections explain the composition of the movie CRUD components and how they work.

### `Index` component

Open the `Index` component definition file (`Components/Pages/Movies/Index.razor`) and examine the Razor directives at the top of the file.

The `@page` directive's route template indicates the URL for the page is `/movies`.

`@using` directives appear to access the following API:

* [Microsoft.EntityFrameworkCore](/en-us/dotnet/api/microsoft.entityframeworkcore)
* [Microsoft.AspNetCore.Components.QuickGrid](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid)
* `BlazorWebAppMovies.Models`
* `BlazorWebAppMovies.Data`

The database context factory (`IDbContextFactory<T>`, where the type (`T`) is a `BlazorWebAppMoviesContext`) is injected into the component with the `@inject` directive. The factory approach requires that a database context be disposed, so the component implements the [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable) interface with the `@implements` directive.

The page title is set via the Blazor framework's [PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component, and an H1 section heading is the first rendered element:

```
<PageTitle>Index</PageTitle>

<h1>Index</h1>
```

A link is rendered to navigate to the `Create` page at `/movies/create`:

```
<p>
    <a href="movies/create">Create New</a>
</p>
```

The [`QuickGrid` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid) displays movie entities. The item provider is a `DbSet<Movie>` obtained from the created database context ([CreateDbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1.createdbcontext)) of the injected database context factory (`DbFactory`). For each movie entity, the component displays the movie's title, release date, genre, and price. A column also holds links to edit, see details, and delete each movie entity.

```
<QuickGrid Class="table" Items="context.Movie">
    <PropertyColumn Property="movie => movie.Title" />
    <PropertyColumn Property="movie => movie.ReleaseDate" />
    <PropertyColumn Property="movie => movie.Genre" />
    <PropertyColumn Property="movie => movie.Price" />

    <TemplateColumn Context="movie">
        <a href="@($"movies/edit?id={movie.Id}")">Edit</a> |
        <a href="@($"movies/details?id={movie.Id}")">Details</a> |
        <a href="@($"movies/delete?id={movie.Id}")">Delete</a>
    </TemplateColumn>
</QuickGrid>

@code {
    private BlazorWebAppMoviesContext context = default!;

    protected override void OnInitialized()
    {
        context = DbFactory.CreateDbContext();
    }

    public async ValueTask DisposeAsync() => await context.DisposeAsync();
}
```

In the code block (`@code`):

* The `context` field holds the database context, typed as a `BlazorWebAppMoviesContext`.
* The `OnInitialized` lifecycle method assigns a created database context ([CreateDbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1.createdbcontext)) from the injected factory (`DbFactory`) to the `context` variable.
* The asynchronous `DisposeAsync` method disposes of the database context when the component is disposed.

Notice how the context (`Context`) parameter of the [TemplateColumn\<TGridItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.templatecolumn-1) specifies a parameter name (`movie`) for the context instance of the column. Specifying a name for the context instance makes the markup more readable (the default name for the context is simply `context`). `Movie` class properties are read from the context instance. For example, the movie identifier (`Id`) is available in `movie.Id`.

The at symbol (`@`) with parentheses (`@(...)`), which is called an *explicit Razor expression*, allows the `href` of each link to include the movie entity's `Id` property in the link query string as an *interpolated string* (`$...{...}...`). For a movie identifier (`Id`) of 7, the string value provided to the `href` to edit that movie is `movies/edit?id=7`. When the link is followed, the `id` field is read from the query string by the `Edit` component to load the movie.

For the movie example from the last part of the tutorial series, *The Matrix*©, the [`QuickGrid` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid) renders the following HTML markup (some elements and attributes aren't present to simplify display). See how the explicit Razor expressions and interpolated strings produced the `href` values for the links to other pages. The movie's identifier in the database happens to be `3` for this example, so the `id` is `3` in the query strings for the `Edit`, `Details`, and `Delete` pages. You may see a different value when you run the app.

```
<table>
    <thead>
        <tr>
            <th>Title</th>
            <th>ReleaseDate</th>
            <th>Genre</th>
            <th>Price</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>The Matrix</td>
            <td>3/29/1999</td>
            <td>Sci-fi (Cyberpunk)</td>
            <td>5.00</td>
            <td>
                <a href="movies/edit?id=3">Edit</a> |
                <a href="movies/details?id=3">Details</a> |
                <a href="movies/delete?id=3">Delete</a>
            </td>
        </tr>
    </tbody>
</table>
```

The column names are taken from the `Movie` model properties, so the release date doesn't have a space between the words. Add a [Title](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.title#microsoft-aspnetcore-components-quickgrid-columnbase-1-title) to the [PropertyColumn\<TGridItem,TProp>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2) with a value that includes a space between the words:

```
- <PropertyColumn Property="movie => movie.ReleaseDate" />
+ <PropertyColumn Property="movie => movie.ReleaseDate" Title="Release Date" />
```

Run the app to see that the column displays two words for the release date.

Stop the app by closing the browser's window.

Stop the app by closing the browser's window and pressing `Shift`+`F5` on the keyboard in VS Code.

Stop the app by closing the browser's window and pressing `Ctrl`+`C` in the command shell.

### `Details` component

Open the `Details` component definition file (`Components/Pages/Movies/Details.razor`).

The `@page` directive at the top of the file indicates the relative URL for the page is `/movies/details`. As before, the database context is injected, and namespaces are provided to access API (`BlazorWebAppMovies.Models` and `Microsoft.EntityFrameworkCore`). The `Details` component also injects the app's [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager), which is used for a variety of navigation-related operations in components.

```
@page "/movies/details"
@using Microsoft.EntityFrameworkCore
@using BlazorWebAppMovies.Models
@inject IDbContextFactory<BlazorWebAppMovies.Data.BlazorWebAppMoviesContext> DbFactory
@inject NavigationManager NavigationManager
```

Details for a movie entity are only shown if the movie, located by its identifier (`Id`) from the query string, has been loaded for display. The presence of the movie in `movie` is checked with an `@if` Razor statement:

```
@if (movie is null)
{
    <p><em>Loading...</em></p>
}
```

When the movie is loaded, it's displayed as a [description list (MDN documentation)](https://developer.mozilla.org/docs/Web/HTML/Element/dl) along with two links:

* The first link provides the user an opportunity to edit the entity.
* The second link allows the user to return to the movies `Index` page.

CSS classes aren't shown in the following example to simplify the Razor markup for display:

```
<dl>
    <dt>Title</dt>
    <dd>@movie.Title</dd>
    <dt>ReleaseDate</dt>
    <dd>@movie.ReleaseDate</dd>
    <dt>Genre</dt>
    <dd>@movie.Genre</dd>
    <dt>Price</dt>
    <dd>@movie.Price</dd>
</dl>
<div>
    <a href="@($"/movies/edit?id={movie.Id}")">Edit</a> |
    <a href="@($"/movies")">Back to List</a>
</div>
</div>
```

Add a space to the content of the description term element (`<dt>`) for the movie's release date to separate the words:

```
- <dt class="col-sm-2">ReleaseDate</dt>
+ <dt class="col-sm-2">Release Date</dt>
```

Examine the C# of the component's `@code` block:

```
private Movie? movie;

[SupplyParameterFromQuery]
private int Id { get; set; }

protected override async Task OnInitializedAsync()
{
    using var context = DbFactory.CreateDbContext();
    movie = await context.Movie.FirstOrDefaultAsync(m => m.Id == Id);

    if (movie is null)
    {
        NavigationManager.NotFound();
    }
}
```

```
private Movie? movie;

[SupplyParameterFromQuery]
private int Id { get; set; }

protected override async Task OnInitializedAsync()
{
    using var context = DbFactory.CreateDbContext();
    movie = await context.Movie.FirstOrDefaultAsync(m => m.Id == Id);

    if (movie is null)
    {
        NavigationManager.NavigateTo("notfound");
    }
}
```

The `movie` variable is a private field of type `Movie`, which is a null-reference type (`?`), meaning that `movie` might be set to `null`.

The `Id` is a *component parameter* supplied from the component's query string due to the presence of the [`[SupplyParameterFromQuery]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromqueryattribute). If the identifier is missing, `Id` defaults to zero (`0`).

`OnInitializedAsync` is the first component lifecycle method that we've seen. This method is executed when the component loads. [FirstOrDefaultAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.firstordefaultasync) is called on the database set (`DbSet<Movie>`) to retrieve the movie entity with an `Id` equal to the `Id` parameter that was set by the query string. If `movie` is `null`, [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) is used to navigate to a `notfound` endpoint.

When a movie isn't found, calling [NavigationManager.NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) renders the `NotFound` component, which produces a Not Found page in the browser with a 404 (Not Found) status code.

There isn't an actual `notfound` endpoint (Razor component) in the app. When adopting server-side rendering (SSR) in .NET 8 or .NET 9, the app doesn't have a mechanism to return a 404 (Not Found) status code. A 404 is generated by navigating to a non-existent endpoint. This scaffolded code is for your further implementation of a suitable result when not finding an entity in .NET 8/9. For example, you could have the component direct the user to a page where they can file an inquiry with your support team, or you could remove the injected [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) and [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) code and replace it with Razor markup and code that displays a message to the user that the entity wasn't found. In .NET 10 or later, Blazor has a built-in Not Found feature. For more information, see the [.NET 10 version of this section](?view=aspnetcore-10.0\&pivots=vs\&preserve-view=true#details-component).

### `Create` component

Open the `Create` component definition file (`Components/Pages/Movies/Create.razor`).

The component uses a built-in component called an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform), which renders a form for user input and includes validation features.

CSS classes aren't present in the following example to simplify the display:

```
<EditForm method="post" Model="Movie" OnValidSubmit="AddMovie" FormName="create" Enhance>
    <DataAnnotationsValidator />
    <ValidationSummary role="alert" />
    <div>
        <label for="title">Title:</label> 
        <InputText id="title" @bind-Value="Movie.Title" /> 
        <ValidationMessage For="() => Movie.Title" /> 
    </div>
    <div>
        <label for="releasedate">ReleaseDate:</label> 
        <InputDate id="releasedate" @bind-Value="Movie.ReleaseDate" /> 
        <ValidationMessage For="() => Movie.ReleaseDate" /> 
    </div>
    <div>
        <label for="genre">Genre:</label> 
        <InputText id="genre" @bind-Value="Movie.Genre" /> 
        <ValidationMessage For="() => Movie.Genre" /> 
    </div>
    <div>
        <label for="price">Price:</label> 
        <InputNumber id="price" @bind-Value="Movie.Price" /> 
        <ValidationMessage For="() => Movie.Price" /> 
    </div>
    <button type="submit">Create</button>
</EditForm>
```

Add a space to the content of the label element (`<label>`) for the movie's release date to separate the words:

```
- <label for="releasedate" class="form-label">ReleaseDate:</label>
+ <label for="releasedate" class="form-label">Release Date:</label>
```

The [Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model) parameter is assigned the model, in this case `Movie`. [OnValidSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onvalidsubmit) specifies a method to invoke (`AddMovie`) when the form is submitted and the data is valid. By convention, every form should assign a [FormName](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.formname) to prevent form collisions when multiple forms are present on a page. The [Enhance](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.enhance) flag activates a Blazor feature for server-side rendering (SSR) that submits the form without performing a full-page reload.

For validation:

* The [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) adds data annotations validation support, which is covered later in this tutorial series.
* The [ValidationSummary](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationsummary) component displays a list of validation messages.
* The [ValidationMessage\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationmessage-1) components hold validation messages for the form's fields.

Blazor includes several form element components to assist you with creating forms, including [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) and various input components, such as [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext), [InputDate\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputdate-1), and [InputNumber\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputnumber-1). Each input component is bound to a model property with `@bind-Value` Razor syntax, where `Value` is a property in each input component.

In the component's `@code` block, C# code includes a `Movie` component parameter tied to the form via the [`[SupplyParameterFromForm]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromformattribute).

The `AddMovie` method:

* Is called when the form is submitted.
* Adds the movie data bound to the form's model (`Movie`) if form validation passes.
* [SaveChangesAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechangesasync) is called on the database context to save the movie.
* [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) is used to return the user to the movies `Index` page.

```
[SupplyParameterFromForm]
private Movie Movie { get; set; } = default!;

protected override void OnInitialized() => Movie ??= new();

private async Task AddMovie()
{
    using var context = DbFactory.CreateDbContext();
    context.Movie.Add(Movie);
    await context.SaveChangesAsync();
    NavigationManager.NavigateTo("/movies");
}
```

```
[SupplyParameterFromForm]
private Movie Movie { get; set; } = new();

private async Task AddMovie()
{
    using var context = DbFactory.CreateDbContext();
    context.Movie.Add(Movie);
    await context.SaveChangesAsync();
    NavigationManager.NavigateTo("/movies");
}
```

Warning

Although it isn't a concern for the app in this tutorial, binding form data to entity data models can be susceptible to overposting attacks. Additional information on this subject appears later in this article.

### `Delete` component

Open the `Delete` component definition file (`Components/Pages/Movies/Delete.razor`).

Add a space to the content of the description term element (`<dt>`) for the movie's release date to separate the words:

```
- <dt class="col-sm-2">ReleaseDate</dt>
+ <dt class="col-sm-2">Release Date</dt>
```

Examine the Razor markup for the submit button of the [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) (CSS class removed for simplicity):

```
<button type="submit" disabled="@(movie is null)">Delete</button>
```

The **Delete** button sets its [`disabled` HTML attribute](https://developer.mozilla.org/docs/Web/HTML/Attributes/disabled) based on the presence of the movie (not `null`) using an explicit Razor expression (`@(...)`).

In the C# code of the `@code` block, the `DeleteMovie` method removes the movie, saves the changes to the database, and navigates the user to the movies `Index` page. The exclamation point on the movie field (`movie!`) is the [null-forgiving operator (C# Language Reference)](/en-us/dotnet/csharp/language-reference/operators/null-forgiving), which suppresses nullable warnings for `movie`.

```
private async Task DeleteMovie()
{
    using var context = DbFactory.CreateDbContext();
    context.Movie.Remove(movie!);
    await context.SaveChangesAsync();
    NavigationManager.NavigateTo("/movies");
}
```

When a movie isn't found, calling [NavigationManager.NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) renders the `NotFound` component, which produces a Not Found page in the browser with a 404 (Not Found) status code.

### `Edit` component

Open the `Edit` component definition file (`Components/Pages/Movies/Edit.razor`).

Add a space to the content of the label element (`<label>`) for the movie's release date to separate the words:

```
- <label for="releasedate" class="form-label">ReleaseDate:</label>
+ <label for="releasedate" class="form-label">Release Date:</label>
```

The component uses an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) similar to the `Create` component.

The movie entity's identifier `Id` is stored in a hidden field of the form:

```
<input type="hidden" name="Movie.Id" value="@Movie.Id" />
```

Examine the C# code of the `@code` block:

```
private async Task UpdateMovie()
{
    using var context = DbFactory.CreateDbContext();
    context.Attach(Movie!).State = EntityState.Modified;

    try
    {
        await context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie!.Id))
        {
            NavigationManager.NotFound();
        }
        else
        {
            throw;
        }
    }

    NavigationManager.NavigateTo("/movies");
}

private bool MovieExists(int id)
{
    using var context = DbFactory.CreateDbContext();
    return context.Movie.Any(e => e.Id == id);
}
```

```
private async Task UpdateMovie()
{
    using var context = DbFactory.CreateDbContext();
    context.Attach(Movie!).State = EntityState.Modified;

    try
    {
        await context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie!.Id))
        {
            NavigationManager.NavigateTo("notfound");
        }
        else
        {
            throw;
        }
    }

    NavigationManager.NavigateTo("/movies");
}

private bool MovieExists(int id)
{
    using var context = DbFactory.CreateDbContext();
    return context.Movie.Any(e => e.Id == id);
}
```

The movie entity's [EntityState](/en-us/dotnet/api/microsoft.entityframeworkcore.entitystate) is set to [Modified](/en-us/dotnet/api/microsoft.entityframeworkcore.entitystate#microsoft-entityframeworkcore-entitystate-modified), which signifies that the entity is tracked by the context, exists in the database, and that some or all of its property values are modified.

When a movie isn't found, calling [NavigationManager.NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) renders the `NotFound` component, which produces a Not Found page in the browser with a 404 (Not Found) status code.

If there's a concurrency exception and the movie entity no longer exists at the time that changes are saved, the component redirects to the Not Found page, which results in returning a 404 (Not Found) status code. If the movie exists and a concurrency exception is thrown, for example when another user has already modified the entity, the exception is rethrown by the component with the [`throw` statement (C# Language Reference)](/en-us/dotnet/csharp/language-reference/statements/exception-handling-statements#the-throw-statement). Additional guidance on handling concurrency with EF Core in Blazor apps is provided by the Blazor documentation.

Note

Later, you're instructed to make a minor modification to the preceding code in the `Edit` component's `UpdateMovie` method, where a `return` statement is added after the call to [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound). Guidance on this change and information on why the change is required is provided in the *Concurrency exception handling* section of [Build a Blazor movie database app (Part 4 - Work with a database)](part-4?view=aspnetcore-10.0), which is the next article in this tutorial.

If there's a concurrency exception and the movie entity no longer exists at the time that changes are saved, the component redirects to the non-existent endpoint (`notfound`), which results in returning a 404 (Not Found) status code. You could change this code to notify the user that the movie no longer exists in the database or create a dedicated *Not Found* component and navigate the user to that endpoint. If the movie exists and a concurrency exception is thrown, for example when another user has already modified the entity, the exception is rethrown by the component with the [`throw` statement (C# Language Reference)](/en-us/dotnet/csharp/language-reference/statements/exception-handling-statements#the-throw-statement). Additional guidance on handling concurrency with EF Core in Blazor apps is provided by the Blazor documentation.

Warning

Although it isn't a concern for the app in this tutorial, binding form data to entity data models can be susceptible to overposting attacks. Additional information on this subject appears in the next section.

## Mitigate overposting attacks

Statically-rendered server-side forms, such as those in the `Create` and `Edit` components, can be vulnerable to an *overposting* attack, also known as a *mass assignment* attack. An overposting attack occurs when a malicious user issues an HTML form POST to the server that processes data for properties that aren't part of the rendered form and that the developer doesn't wish to allow users to modify. The term "overposting" literally means that the malicious user has *over*-POSTed with the form.

In the example `Create` and `Edit` components of this tutorial, the `Movie` model doesn't include restricted properties for create and update operations, so overposting isn't a concern. However, it's important to keep overposting in mind when working with static SSR-based Blazor forms that you create and modify in the future.

To mitigate overposting, we recommend using a separate view model/data transfer object (DTO) for the form and database with create (insert) and update operations. When the form is submitted, only properties of the view model/DTO are used by the component and C# code to modify the database. Any extra data included by a malicious user is discarded, so the malicious user is prevented from conducting an overposting attack.

## Troubleshoot with the completed sample

If you run into a problem while following the tutorial that you can't resolve from the text, compare your code to the completed project in the Blazor samples repository:

[Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)

Select the latest version folder. The sample folder for this tutorial's project is named `BlazorWebAppMovies`.

## Related content

* [`NavLink` component](../../fundamentals/navigation?view=aspnetcore-10.0#navlink-component)
* [ASP.NET Core Blazor layouts](../../components/layouts?view=aspnetcore-10.0)
* [Razor directives](../../../mvc/views/razor?view=aspnetcore-10.0#directives) (Razor syntax article) / [Razor directives](../../components/?view=aspnetcore-10.0#razor-syntax) (Blazor documentation)
* [ASP.NET Core Blazor \`QuickGrid\` component](../../components/quickgrid?view=aspnetcore-10.0)
* [ASP.NET Core Blazor forms overview](../../forms/?view=aspnetcore-10.0)
* [Concurrency with EF Core in Blazor apps](../../blazor-ef-core?view=aspnetcore-10.0)
* [ASP.NET Core Blazor globalization and localization](../../globalization-localization?view=aspnetcore-10.0): Explains how to render globalized and localized content to users in different cultures and languages, including how to accept comma number separators.

## Next steps

[Previous: Add and scaffold a model](part-2?view=aspnetcore-10.0) [Next: Work with a database](part-4?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling-api
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/error-handling-api.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Handle errors in ASP.NET Core APIs

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

* [Minimal APIs](#tabpanel_1_minimal-apis)
* [Controllers](#tabpanel_1_controllers)

This article describes how to handle errors in ASP.NET Core APIs. Documentation for Minimal APIs is selected. To see documentation for controller-based APIs, select the **Controllers** tab. For Blazor error handling guidance, see [Handle errors in ASP.NET Core Blazor apps](../blazor/fundamentals/handle-errors?view=aspnetcore-10.0).

This article describes how to handle errors in ASP.NET Core APIs. Documentation for Controller-based APIs is selected. To see documentation for **Minimal APIs**, select the **Minimal APIs** tab. For Blazor error handling guidance, see [Handle errors in ASP.NET Core Blazor apps](../blazor/fundamentals/handle-errors?view=aspnetcore-10.0).

## Developer Exception Page

```
Status: 500 Internal Server Error
Time: 9.39 msSize: 480 bytes
FormattedRawHeadersRequest
Body
text/plain; charset=utf-8, 480 bytes
System.InvalidOperationException: Sample Exception
   at WebApplicationMinimal.Program.<>c.<Main>b__0_0() in C:\Source\WebApplicationMinimal\Program.cs:line 12
   at lambda_method1(Closure, Object, HttpContext)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

HEADERS
=======
Accept: text/plain
Host: localhost:7267
traceparent: 00-0eab195ea19d07b90a46cd7d6bf2f
```

* [Minimal APIs](#tabpanel_2_minimal-apis)
* [Controllers](#tabpanel_2_controllers)

To see the Developer Exception Page in a Minimal API:

* Run the sample app in the [`Development` environment](environments?view=aspnetcore-10.0).
* Go to the `/exception` endpoint.

This section refers to the following sample app to demonstrate ways to handle exceptions in a Minimal API. It throws an exception when the endpoint `/exception` is requested:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/exception", () => 
{
    throw new InvalidOperationException("Sample Exception");
});

app.MapGet("/", () => "Test by calling /exception");

app.Run();
```

To see the Developer Exception Page in a controller-based API:

* Add the following controller action to a controller-based API. The action throws an exception when the endpoint is requested.

  ```
  [HttpGet("Throw")]
  public IActionResult Throw() =>
      throw new Exception("Sample exception.");
  ```

* Run the app in the [development environment](environments?view=aspnetcore-10.0).

* Go to the endpoint defined by the controller action.

## Exception handler

In non-development environments, use the [exception handler middleware](error-handling?view=aspnetcore-10.0#exception-handler-page) to produce an error payload.

* [Minimal APIs](#tabpanel_3_minimal-apis)
* [Controllers](#tabpanel_3_controllers)

To configure the `exception handler middleware`, call [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler). For example, the following code changes the app to respond with an [RFC 7807](https://tools.ietf.org/html/rfc7807)-compliant payload to the client. For more information, see the [Problem Details](#problem-details) section later in this article.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseExceptionHandler(exceptionHandlerApp 
    => exceptionHandlerApp.Run(async context 
        => await Results.Problem()
                     .ExecuteAsync(context)));

app.MapGet("/exception", () => 
{
    throw new InvalidOperationException("Sample Exception");
});

app.MapGet("/", () => "Test by calling /exception");

app.Run();
```

1. In `Program.cs`, call [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) to add the exception handling middleware:

   ```
   var app = builder.Build();

   app.UseHttpsRedirection();

   if (!app.Environment.IsDevelopment())
   {
       app.UseExceptionHandler("/error");
   }

   app.UseAuthorization();

   app.MapControllers();

   app.Run();
   ```

2. Configure a controller action to respond to the `/error` route:

   ```
   [Route("/error")]
   public IActionResult HandleError() =>
       Problem();
   ```

The preceding `HandleError` action sends an [RFC 7807](https://tools.ietf.org/html/rfc7807)-compliant payload to the client.

Warning

Don't mark the error handler action method with HTTP method attributes, such as `HttpGet`. Explicit verbs prevent some requests from reaching the action method.

For web APIs that use [Swagger / OpenAPI](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0), mark the error handler action with the [\[ApiExplorerSettings\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorersettingsattribute) attribute and set its [IgnoreApi](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorersettingsattribute.ignoreapi) property to `true`. This attribute configuration excludes the error handler action from the app's OpenAPI specification:

```
[ApiExplorerSettings(IgnoreApi = true)]
```

Allow anonymous access to the method if unauthenticated users should see the error.

## Client and Server error responses

* [Minimal APIs](#tabpanel_4_minimal-apis)
* [Controllers](#tabpanel_4_controllers)

Consider the following Minimal API app.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users/{id:int}", (int id) 
    => id <= 0 ? Results.BadRequest() : Results.Ok(new User(id)));

app.MapGet("/", () => "Test by calling /users/{id:int}");

app.Run();

public record User(int Id);
```

The `/users` endpoint produces `200 OK` with a `json` representation of `User` when `id` is greater than `0`, otherwise a `400 BAD REQUEST` status code without a response body. For more information about creating a response, see [Create responses in Minimal API apps](/en-us/aspnet/core/fundamentals/minimal-apis/responses).

The [`Status Code Pages middleware`](#client-and-server-error-responses) can be configured to produce a common body content, **when empty**, for all HTTP client (`400`-`499`) or server (`500` -`599`) responses. The middleware is configured by calling the [UseStatusCodePages](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepages) extension method.

For example, the following example changes the app to respond with an [RFC 7807](https://tools.ietf.org/html/rfc7807)-compliant payload to the client for all client and server responses, including routing errors (for example, `404 NOT FOUND`). For more information, see the [Problem Details](#problem-details) section.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseStatusCodePages(async statusCodeContext 
    => await Results.Problem(statusCode: statusCodeContext.HttpContext.Response.StatusCode)
                 .ExecuteAsync(statusCodeContext.HttpContext));

app.MapGet("/users/{id:int}", (int id) 
    => id <= 0 ? Results.BadRequest() : Results.Ok(new User(id)) );

app.MapGet("/", () => "Test by calling /users/{id:int}");

app.Run();

public record User(int Id);
```

For controller-based APIs, the error response can be configured in one of the following ways:

1. Use the [problem details service](#problem-details-service)
2. [Implement ProblemDetailsFactory](#implement-problemdetailsfactory)
3. [Use ApiBehaviorOptions.ClientErrorMapping](#use-apibehavioroptionsclienterrormapping)

An *error result* is defined as a result with an HTTP status code of 400 or higher. For web API controllers, MVC transforms an error result to produce a [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails).

The automatic creation of a `ProblemDetails` for error status codes is enabled by default.

- [Minimal APIs](#tabpanel_5_minimal-apis)
- [Controllers](#tabpanel_5_controllers)

Minimal API apps can be configured to generate problem details response for all HTTP client and server error responses that ***don't have body content yet*** by using the `AddProblemDetails` extension method.

The following code configures the app to generate problem details:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

app.MapGet("/users/{id:int}", (int id) 
    => id <= 0 ? Results.BadRequest() : Results.Ok(new User(id)));

app.MapGet("/", () => "Test by calling /users/{id:int}");

app.Run();

public record User(int Id);
```

For more information on using `AddProblemDetails`, see [Problem Details](#problem-details)

### IProblemDetailsService fallback

In the following code, `httpContext.Response.WriteAsync("Fallback: An error occurred.")` returns an error if the [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) implementation isn't able to generate a [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails):

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async httpContext =>
    {
        var pds = httpContext.RequestServices.GetService<IProblemDetailsService>();
        if (pds == null
            || !await pds.TryWriteAsync(new() { HttpContext = httpContext }))
        {
            // Fallback behavior
            await httpContext.Response.WriteAsync("Fallback: An error occurred.");
        }
    });
});

app.MapGet("/exception", () =>
{
    throw new InvalidOperationException("Sample Exception");
});

app.MapGet("/", () => "Test by calling /exception");

app.Run();
```

The preceding code:

* Writes an error message with the fallback code if the `problemDetailsService` is unable to write a `ProblemDetails`. For example, an endpoint where the [Accept request header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Accept) specifies a media type that the `DefaultProblemDetailsWriter` does not support.
* Uses the [exception handler middleware](#exception-handler).

Note

The `DefaultProblemDetailsWriter` supports the following media types in the `Accept` request header:

* `application/json`
* `application/problem+json`
* Wildcard types such as `*/*` and `application/*`

Non-JSON media types, such as `application/xml` or `text/html`, are **not** supported and trigger the fallback behavior.

The following sample is similar to the preceding except that it calls the [`Status Code Pages middleware`](#client-and-server-error-responses).

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseStatusCodePages(statusCodeHandlerApp =>
{
    statusCodeHandlerApp.Run(async httpContext =>
    {
        var pds = httpContext.RequestServices.GetService<IProblemDetailsService>();
        if (pds == null
            || !await pds.TryWriteAsync(new() { HttpContext = httpContext }))
        {
            // Fallback behavior
            await httpContext.Response.WriteAsync("Fallback: An error occurred.");
        }
    });
});

app.MapGet("/users/{id:int}", (int id) =>
{
    return id <= 0 ? Results.BadRequest() : Results.Ok(new User(id));
});

app.MapGet("/", () => "Test by calling /users/{id:int}");

app.Run();

public record User(int Id);
```

### Problem details service

ASP.NET Core supports creating [Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457) using the [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice).

The following code configures the app to generate a problem details response for all HTTP client and server error responses that ***don't have body content yet***:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();
```

Consider the API controller from the preceding section, which returns [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.badrequest) when the input is invalid:

```
[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
    // /api/values2/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values2 /squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }
}
```

A problem details response is generated with the preceding code when any of the following conditions apply:

* An invalid input is supplied.
* The URI has no matching endpoint.
* An unhandled exception occurs.

#### Customize problem details with `CustomizeProblemDetails`

The following code uses [ProblemDetailsOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions) to set [CustomizeProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.http.problemdetailsoptions.customizeproblemdetails#microsoft-aspnetcore-http-problemdetailsoptions-customizeproblemdetails):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails(options =>
        options.CustomizeProblemDetails = (context) =>
        {

            var mathErrorFeature = context.HttpContext.Features
                                                       .Get<MathErrorFeature>();
            if (mathErrorFeature is not null)
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError =>
                    ("Divison by zero is not defined.",
                                          "https://wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                                          "https://wikipedia.org/wiki/Square_root")
                };

                context.ProblemDetails.Type = details.Type;
                context.ProblemDetails.Title = "Bad Input";
                context.ProblemDetails.Detail = details.Detail;
            }
        }
    );

var app = builder.Build();

app.UseHttpsRedirection();

app.UseStatusCodePages();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

### Implement `ProblemDetailsFactory`

MVC uses [Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.problemdetailsfactory) to produce all instances of [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails) and [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails). This factory is used for:

* Client error responses
* Validation failure error responses
* [ControllerBase.Problem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.problem) and [ControllerBase.ValidationProblem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.validationproblem)

To customize the problem details response, register a custom implementation of [ProblemDetailsFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.problemdetailsfactory) in `Program.cs`:

```
builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();
```

### Use `ApiBehaviorOptions.ClientErrorMapping`

Use the [ClientErrorMapping](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.clienterrormapping) property to configure the contents of the `ProblemDetails` response. For example, the following code in `Program.cs` updates the [Link](/en-us/dotnet/api/microsoft.aspnetcore.mvc.clienterrordata.link) property for 404 responses:

```
builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });
```

## Additional error handling features

* [Minimal APIs](#tabpanel_6_minimal-apis)
* [Controllers](#tabpanel_6_controllers)

### Migration from controllers to Minimal APIs

If you're migrating from controller-based APIs to Minimal APIs:

1. **Replace action filters** with endpoint filters or middleware
2. **Replace model validation** with manual validation or custom binding
3. **Replace exception filters** with exception handling middleware
4. **Configure problem details** using `AddProblemDetails()` for consistent error responses

### When to use controller-based error handling

Consider controller-based APIs if you need:

* Complex model validation scenarios
* Centralized exception handling across multiple controllers
* Fine-grained control over error response formatting
* Integration with MVC features like filters and conventions

For detailed information about controller-based error handling, including validation errors, problem details customization, and exception filters, see the [Controllers](?view=aspnetcore-10.0\&tabs=controllers) tab sections.

### Validation failure error response

For web API controllers, MVC responds with a [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails) response type when model validation fails. MVC uses the results of [InvalidModelStateResponseFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.invalidmodelstateresponsefactory#microsoft-aspnetcore-mvc-apibehavioroptions-invalidmodelstateresponsefactory) to construct the error response for a validation failure. The following example replaces the default factory with an implementation that also supports formatting responses as XML, in `Program.cs`:

```
builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
            new BadRequestObjectResult(context.ModelState)
            {
                ContentTypes =
                {
                    // using static System.Net.Mime.MediaTypeNames;
                    Application.Json,
                    Application.Xml
                }
            };
    })
    .AddXmlSerializerFormatters();
```

### Use exceptions to modify the response

The contents of the response can be modified from outside of the controller using a custom exception and an action filter:

1. Create a well-known exception type named `HttpResponseException`:

   ```
   public class HttpResponseException : Exception
   {
       public HttpResponseException(int statusCode, object? value = null) =>
           (StatusCode, Value) = (statusCode, value);

       public int StatusCode { get; }

       public object? Value { get; }
   }
   ```

2. Create an action filter named `HttpResponseExceptionFilter`:

   ```
   public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
   {
       public int Order => int.MaxValue - 10;

       public void OnActionExecuting(ActionExecutingContext context) { }

       public void OnActionExecuted(ActionExecutedContext context)
       {
           if (context.Exception is HttpResponseException httpResponseException)
           {
               context.Result = new ObjectResult(httpResponseException.Value)
               {
                   StatusCode = httpResponseException.StatusCode
               };

               context.ExceptionHandled = true;
           }
       }
   }
   ```

   The preceding filter specifies an `Order` of the maximum integer value minus 10. This `Order` allows other filters to run at the end of the pipeline.

3. In `Program.cs`, add the action filter to the filters collection:

   ```
   builder.Services.AddControllers(options =>
   {
       options.Filters.Add<HttpResponseExceptionFilter>();
   });
   ```

### Key differences for controllers

* **Automatic model validation**: Controllers automatically validate model state and return `400 Bad Request` responses for validation failures
* **Exception filters**: Use action filters and exception filters for centralized error handling
* **Built-in problem details**: Configure `ApiBehaviorOptions` for standardized error responses
* **Custom error responses**: Override `InvalidModelStateResponseFactory` for custom validation error formatting

## Related content

* [How to Use ModelState Validation in ASP.NET Core Web API](https://code-maze.com/aspnetcore-modelstate-validation-web-api/)
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/middleware/problem-details-service)
* [Hellang.Middleware.ProblemDetails](https://www.nuget.org/packages/Hellang.Middleware.ProblemDetails/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/http-context.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Access HttpContext in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) encapsulates all information about an individual HTTP request and response. An `HttpContext` instance is initialized when an HTTP request is received. The `HttpContext` instance is accessible by middleware and app frameworks such as Web API controllers, Razor Pages, [SignalR](../signalr/introduction?view=aspnetcore-10.0), gRPC, and more.

For information about using `HttpContext` with an HTTP request and response, see [Use HttpContext in ASP.NET Core](use-http-context?view=aspnetcore-10.0).

## Access HttpContext from Razor Pages

The Razor Pages [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) exposes the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.httpcontext#microsoft-aspnetcore-mvc-razorpages-pagemodel-httpcontext) property:

```
public class IndexModel : PageModel
{
    public void OnGet()
    {
        var message = HttpContext.Request.PathBase;

        // ...
    }
}
```

You can use the same property in the corresponding Razor Page View:

```
@page
@model IndexModel

@{
    var message = HttpContext.Request.PathBase;
    
    // ...
}
```

## Access HttpContext from a Razor view in MVC

Razor views in the MVC pattern expose the `HttpContext` via the [Context](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.razorpage.context) property on the view. The following example retrieves the current username in an intranet app by using Windows Authentication:

```
@{
    var username = Context.User.Identity.Name;
    
    // ...
}
```

## Access HttpContext from a controller

Controllers expose the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.httpcontext) property:

```
public class HomeController : Controller
{
    public IActionResult About()
    {
        var pathBase = HttpContext.Request.PathBase;

        // ...

        return View();
    }
}
```

## Access HttpContext from Minimal APIs

To use `HttpContext` from [Minimal APIs](minimal-apis?view=aspnetcore-10.0):

```
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
```

## Access HttpContext from middleware

To use `HttpContext` from custom middleware components, pass the `HttpContext` parameter into the `Invoke` or `InvokeAsync` method:

```
public class MyCustomMiddleware
{
    // ...

    public async Task InvokeAsync(HttpContext context)
    {
        // ...
    }
}
```

## Access HttpContext from SignalR

To use `HttpContext` from SignalR, call the [GetHttpContext](/en-us/dotnet/api/microsoft.aspnetcore.signalr.gethttpcontextextensions.gethttpcontext) on the [Context](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub.context):

```
public class MyHub : Hub
{
    public async Task SendMessage()
    {
        var httpContext = Context.GetHttpContext();

        // ...
    }
}
```

## Access HttpContext from gRPC methods

To use `HttpContext` from [gRPC](../grpc/aspnetcore?view=aspnetcore-10.0) methods, see [Resolve HttpContext in gRPC methods](../grpc/aspnetcore?view=aspnetcore-10.0#resolve-httpcontext-in-grpc-methods).

## Access HttpContext from custom components

For other framework and custom components that require access to `HttpContext`, the recommended approach is to register a dependency by using the built-in [Dependency Injection (DI)](dependency-injection?view=aspnetcore-10.0) container. The DI container supplies the `IHttpContextAccessor` to any classes that declare it as a dependency in their constructors:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<IUserRepository, UserRepository>();
```

```
public void ConfigureServices(IServiceCollection services)
{
     services.AddControllersWithViews();
     services.AddHttpContextAccessor();
     services.AddTransient<IUserRepository, UserRepository>();
}
```

In the following example:

* The `UserRepository` instance declares its dependency on `IHttpContextAccessor`.
* The dependency is supplied when DI resolves the dependency chain and creates an instance of `UserRepository`.

```
public class UserRepository : IUserRepository
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserRepository(IHttpContextAccessor httpContextAccessor) =>
        _httpContextAccessor = httpContextAccessor;

    public void LogCurrentUser()
    {
        var username = _httpContextAccessor.HttpContext.User.Identity.Name;

        // ...
    }
}
```

```
public class UserRepository : IUserRepository
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserRepository(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void LogCurrentUser()
    {
        var username = _httpContextAccessor.HttpContext.User.Identity.Name;
        service.LogAccessRequest(username);
    }
}
```

## Access HttpContext from a background thread

`HttpContext` isn't thread-safe. Reading or writing properties of the `HttpContext` outside of processing a request can result in a [NullReferenceException](/en-us/dotnet/api/system.nullreferenceexception) error.

Note

If your app generates sporadic `NullReferenceException` errors, review parts of the code that start background processing or code that continues processing after a request completes. Look for mistakes, such as defining a controller method as `async void`.

To safely do background work with `HttpContext` data:

* Copy the required data during request processing.
* Pass the copied data to a background task.
* **Don't** reference `HttpContext` data in parallel tasks. Extract the data needed from the context before starting the parallel tasks.

To avoid unsafe code, never pass `HttpContext` into a method that does background work. Pass the required data instead.

In the following example, the `SendEmail` method calls the `SendEmailCoreAsync` method to start sending an email. The value of the `X-Correlation-Id` header is passed to `SendEmailCoreAsync` instead of the `HttpContext`. Code execution doesn't wait for `SendEmailCoreAsync` to complete:

```
public class EmailController : Controller
{
    public IActionResult SendEmail(string email)
    {
        var correlationId = HttpContext.Request.Headers["X-Correlation-Id"].ToString();

        _ = SendEmailCoreAsync(correlationId);

        return View();
    }

    private async Task SendEmailCoreAsync(string correlationId)
    {
        // ...
    }
}
```

In the following example, the `SendEmailCore` method is called to start sending an email. The `correlationId` parameter is passed to `SendEmailCore`, not the `HttpContext`. Code execution doesn't wait for `SendEmailCore` to complete:

```
public class EmailController : Controller
{
    public IActionResult SendEmail(string email)
    {
        var correlationId = HttpContext.Request.Headers["x-correlation-id"].ToString();

        _ = SendEmailCore(correlationId);

        return View();
    }

    private async Task SendEmailCore(string correlationId)
    {
        // ...
    }
}
```

## Access IHttpContextAccessor or HttpContext in Razor components (Blazor)

If you want to access the `IHttpContextAccessor` or `HttpContext` in Razor components (Blazor apps), see [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../blazor/components/httpcontext?view=aspnetcore-10.0).

## Related content

* [Use HttpContext in ASP.NET Core](use-http-context?view=aspnetcore-10.0)
* [Razor Pages architecture and concepts in ASP.NET Core](../razor-pages/?view=aspnetcore-10.0)
* [gRPC services with ASP.NET Core](../grpc/aspnetcore?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0026
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0026.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0026: `[Authorize]` is overridden by `[AllowAnonymous]` from "farther away"

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0026      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

It seems intuitive that an `[Authorize]` attribute placed "closer" to an MVC action than an `[AllowAnonymous]` attribute would override the `[AllowAnonymous]` attribute and force authorization. However, this is not necessarily the case. What does matter is the relative order of the attributes.

Note

The `[AllowAnonymous]` attribute doesn't disable authentication entirely. When credentials are sent to an endpoint with `[AllowAnonymous]`, the endpoint still authenticates those credentials and establishes the user's identity. The `[AllowAnonymous]` attribute only means that authentication is **not required**—the endpoint will run as anonymous only when no credentials are provided. This behavior can be useful for endpoints that need to work for both authenticated and anonymous users.

The following code shows examples where a closer `[Authorize]` attribute gets overridden by an `[AllowAnonymous]` attribute that is farther away.

```
[AllowAnonymous]
public class MyController
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on the class
    public IActionResult Private() => null;
}
```

```
[AllowAnonymous]
public class MyControllerAnon : ControllerBase
{
}

[Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
public class MyControllerInherited : MyControllerAnon
{
}

public class MyControllerInherited2 : MyControllerAnon
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
    public IActionResult Private() => null;
}
```

```
[AllowAnonymous]
[Authorize] // Overridden by the preceding [AllowAnonymous]
public class MyControllerMultiple : ControllerBase
{
}
```

## Rule description

This warning indicates that an `[Authorize]` attribute is overridden by an `[AllowAnonymous]` attribute from "farther away." When `[AllowAnonymous]` takes precedence, the endpoint doesn't require authentication but still accepts and processes credentials if they're provided. This means:

* If a request includes authentication credentials, the endpoint authenticates the user and makes their identity available.
* If a request doesn't include credentials, the endpoint allows anonymous access.

This behavior might unintentionally expose endpoints that were meant to require authentication.

## How to fix violations

The correct action to take if you see this warning depends on the intention behind the attributes. The farther away `[AllowAnonymous]` attribute should be removed if it's unintentionally exposing the endpoint to anonymous users. If the `[AllowAnonymous]` attribute was intended to override a closer `[Authorize]` attribute, you can repeat the `[AllowAnonymous]` attribute after the `[Authorize]` attribute to clarify the intent.

```
[AllowAnonymous]
public class MyController
{
    // This produces no warning because the second, "closer" [AllowAnonymous]
    // clarifies that [Authorize] is intentionally overridden.
    // Specifying AuthenticationSchemes can be useful for endpoints that
    // allow but don't require authenticated users. When credentials are sent,
    // they will be authenticated; when no credentials are sent, the endpoint
    // allows anonymous access.
    [Authorize(AuthenticationSchemes = "Cookies")]
    [AllowAnonymous]
    public IActionResult Privacy() => null;
}
```

## When to suppress warnings

The severity level of this diagnostic is Information. You can suppress warnings if your intention is to override the `[Authorize]` attribute. However, we recommend that you make the intent clear by repeating the `[AllowAnonymous]` attribute after the `[Authorize]` attribute.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-11-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/in-process-hosting
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/in-process-hosting.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# In-process hosting with IIS and ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

In-process hosting runs an ASP.NET Core app in the same process as its IIS worker process. In-process hosting provides improved performance over out-of-process hosting because requests aren't proxied over the loopback adapter, a network interface that returns outgoing network traffic back to the same machine.

The following diagram illustrates the relationship between IIS, the ASP.NET Core Module, and an app hosted in-process:

## Enable in-process hosting

Since ASP.NET Core 3.0, in-process hosting has been enabled by default for all app deployed to IIS.

To explicitly configure an app for in-process hosting, set the value of the `<AspNetCoreHostingModel>` property to `InProcess` in the project file (`.csproj`):

```
<PropertyGroup>
  <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
```

## General architecture

The general flow of a request is as follows:

`CreateDefaultBuilder` adds an [IServer](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.iserver) instance by calling the [UseIIS](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiis) method to boot the [CoreCLR](/en-us/dotnet/standard/glossary#coreclr) and host the app inside of the IIS worker process (`w3wp.exe` or `iisexpress.exe`). Performance tests indicate that hosting a .NET app in-process delivers significantly higher request throughput compared to hosting the app out-of-process and proxying requests to [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).

Apps published as a single file executable can't be loaded by the in-process hosting model.

## Application configuration

To configure IIS options, include a service configuration for [IISServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisserveroptions) in `Program.cs`. The following example disables [AutomaticAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisserveroptions.automaticauthentication):

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Server.IIS;
using Microsoft.EntityFrameworkCore;
using RPauth.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.Configure<IISServerOptions>(options =>
{
    options.AutomaticAuthentication = false;
});

builder.Services.AddTransient<IClaimsTransformation, MyClaimsTransformation>();
builder.Services.AddAuthentication(IISServerDefaults.AuthenticationScheme);

builder.Services.AddRazorPages();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

| Option                      | Default    | Setting                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| --------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AutomaticAuthentication`   | `true`     | If `true`, IIS Server sets the `HttpContext.User` authenticated by [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0). If `false`, the server only provides an identity for `HttpContext.User` and responds to challenges when explicitly requested by the `AuthenticationScheme`. Windows Authentication must be enabled in IIS for `AutomaticAuthentication` to function. For more information, see [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).                                |
| `AuthenticationDisplayName` | `null`     | Sets the display name shown to users on login pages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `AllowSynchronousIO`        | `false`    | Whether synchronous I/O is allowed for the `HttpContext.Request` and the `HttpContext.Response`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `MaxRequestBodySize`        | `30000000` | Gets or sets the max request body size for the `HttpRequest`. Note that IIS itself has the limit `maxAllowedContentLength` which will be processed before the `MaxRequestBodySize` set in the `IISServerOptions`. Changing the `MaxRequestBodySize` won't affect the `maxAllowedContentLength`. To increase `maxAllowedContentLength`, add an entry in the `web.config` to set `maxAllowedContentLength` to a higher value. For more details, see [Configuration](/en-us/iis/configuration/system.webServer/security/requestFiltering/requestLimits/#configuration). |

## Differences between in-process and out-of-process hosting

The following characteristics apply when hosting in-process:

* IIS HTTP Server (`IISHttpServer`) is used instead of [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) server. For in-process, [`CreateDefaultBuilder`](../../fundamentals/host/generic-host?view=aspnetcore-10.0#default-builder-settings) calls [UseIIS](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiis) to:

  * Register the `IISHttpServer`.
  * Configure the port and base path the server should listen on when running behind the ASP.NET Core Module.
  * Configure the host to capture startup errors.

* The [`requestTimeout` attribute](web-config?view=aspnetcore-10.0#attributes-of-the-aspnetcore-element) doesn't apply to in-process hosting.

* Sharing an app pool among apps isn't supported. Use one app pool per app.

* The architecture (bitness) of the app and installed runtime (x64 or x86) must match the architecture of the app pool. For example, apps published for 32-bit (x86) must have 32-bit enabled for their IIS Application Pools. For more information, see the [Create the IIS site](../../tutorials/publish-to-iis?view=aspnetcore-10.0#create-the-iis-site) section.

* Client disconnects are detected. The [`HttpContext.RequestAborted`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted) cancellation token is cancelled when the client disconnects.

* When hosting in-process, [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationservice.authenticateasync) isn't called internally to initialize a user. Therefore, an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation used to transform claims after every authentication isn't activated by default. When transforming claims with an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation, call [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) to add authentication services:

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Server.IIS;
using Microsoft.EntityFrameworkCore;
using RPauth.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.Configure<IISServerOptions>(options =>
{
    options.AutomaticAuthentication = false;
});

builder.Services.AddTransient<IClaimsTransformation, MyClaimsTransformation>();
builder.Services.AddAuthentication(IISServerDefaults.AuthenticationScheme);

builder.Services.AddRazorPages();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

* [Web Package (single-file) deployments](/en-us/aspnet/web-forms/overview/deployment/web-deployment-in-the-enterprise/deploying-web-packages) aren't supported.

[]()

## Get timing information

See [Get detailed timing information with IHttpSysRequestTimingFeature](../../fundamentals/servers/httpsys?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#ihsrtf8).

In-process hosting runs an ASP.NET Core app in the same process as its IIS worker process. In-process hosting provides improved performance over out-of-process hosting because requests aren't proxied over the loopback adapter, a network interface that returns outgoing network traffic back to the same machine.

The following diagram illustrates the relationship between IIS, the ASP.NET Core Module, and an app hosted in-process:

## Enable in-process hosting

Since ASP.NET Core 3.0, in-process hosting has been enabled by default for all app deployed to IIS.

To explicitly configure an app for in-process hosting, set the value of the `<AspNetCoreHostingModel>` property to `InProcess` in the project file (`.csproj`):

```
<PropertyGroup>
  <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
```

## General architecture

The general flow of a request is as follows:

`CreateDefaultBuilder` adds an [IServer](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.iserver) instance by calling the [UseIIS](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiis) method to boot the [CoreCLR](/en-us/dotnet/standard/glossary#coreclr) and host the app inside of the IIS worker process (`w3wp.exe` or `iisexpress.exe`). Performance tests indicate that hosting a .NET app in-process delivers significantly higher request throughput compared to hosting the app out-of-process and proxying requests to [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).

Apps published as a single file executable can't be loaded by the in-process hosting model.

## Application configuration

To configure IIS options, include a service configuration for [IISServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisserveroptions) in [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartup.configureservices). The following example disables AutomaticAuthentication:

```
services.Configure<IISServerOptions>(options => 
{
    options.AutomaticAuthentication = false;
});
```

| Option                      | Default    | Setting                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| --------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AutomaticAuthentication`   | `true`     | If `true`, IIS Server sets the `HttpContext.User` authenticated by [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0). If `false`, the server only provides an identity for `HttpContext.User` and responds to challenges when explicitly requested by the `AuthenticationScheme`. Windows Authentication must be enabled in IIS for `AutomaticAuthentication` to function. For more information, see [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).                                |
| `AuthenticationDisplayName` | `null`     | Sets the display name shown to users on login pages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `AllowSynchronousIO`        | `false`    | Whether synchronous I/O is allowed for the `HttpContext.Request` and the `HttpContext.Response`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `MaxRequestBodySize`        | `30000000` | Gets or sets the max request body size for the `HttpRequest`. Note that IIS itself has the limit `maxAllowedContentLength` which will be processed before the `MaxRequestBodySize` set in the `IISServerOptions`. Changing the `MaxRequestBodySize` won't affect the `maxAllowedContentLength`. To increase `maxAllowedContentLength`, add an entry in the `web.config` to set `maxAllowedContentLength` to a higher value. For more details, see [Configuration](/en-us/iis/configuration/system.webServer/security/requestFiltering/requestLimits/#configuration). |

## Differences between in-process and out-of-process hosting

The following characteristics apply when hosting in-process:

* IIS HTTP Server (`IISHttpServer`) is used instead of [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) server. For in-process, [`CreateDefaultBuilder`](../../fundamentals/host/generic-host?view=aspnetcore-10.0#default-builder-settings) calls [UseIIS](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiis) to:

  * Register the `IISHttpServer`.
  * Configure the port and base path the server should listen on when running behind the ASP.NET Core Module.
  * Configure the host to capture startup errors.

* The [`requestTimeout` attribute](web-config?view=aspnetcore-10.0#attributes-of-the-aspnetcore-element) doesn't apply to in-process hosting.

* Sharing an app pool among apps isn't supported. Use one app pool per app.

* The architecture (bitness) of the app and installed runtime (x64 or x86) must match the architecture of the app pool. For example, apps published for 32-bit (x86) must have 32-bit enabled for their IIS Application Pools. For more information, see the [Create the IIS site](../../tutorials/publish-to-iis?view=aspnetcore-10.0#create-the-iis-site) section.

* Client disconnects are detected. The [`HttpContext.RequestAborted`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted) cancellation token is cancelled when the client disconnects.

* When hosting in-process, [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationservice.authenticateasync) isn't called internally to initialize a user. Therefore, an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation used to transform claims after every authentication isn't activated by default. When transforming claims with an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation, call [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) to add authentication services:

  ```
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
      services.AddAuthentication(IISServerDefaults.AuthenticationScheme);
  }

  public void Configure(IApplicationBuilder app)
  {
      app.UseAuthentication();
  }
  ```

* [Web Package (single-file) deployments](/en-us/aspnet/web-forms/overview/deployment/web-deployment-in-the-enterprise/deploying-web-packages) aren't supported.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/native-aot.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core support for Native AOT

By [Mitch Denny](https://github.com/mitchdenny)

Publishing and deploying Native ahead-of-time (AOT) applications in ASP.NET Core offers several benefits:

* **Minimized disk footprint**. When you publish an app by using Native AOT, the process produces a single executable file. The executable contains only the code from the external dependencies required to support the app. The reduced executable size can lead to:

  * Smaller container images, for example in containerized deployment scenarios.
  * Reduced deployment time from smaller images.

* **Reduced startup time**. Native AOT apps can require less startup time, which enables:

  * The app to service requests quicker.
  * Improved deployment, where the container orchestrators manage the transition from one app version to another.

* **Reduced memory demand**. Native AOT apps can require less memory, depending on the work done by the app. Reduced memory consumption can lead to greater deployment density and improved scalability.

The following chart shows the results of a benchmarking test on the various template apps. The benchmark compares performance for an AOT published app (orange bar), a trimmed runtime app (green bar), and an untrimmed runtime app (yellow bar). The test revealed that the Native AOT app demonstrates lower app size, memory usage, and startup time.

This article describes support for Native AOT apps in ASP.NET Core, including an overview of publishing and deployment.

For ASP.NET Core Blazor WebAssembly Native AOT guidance, which adds to or supersedes the guidance in this article, see [ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation](../blazor/webassembly-build-tools-and-aot?view=aspnetcore-10.0).

## Review ASP.NET Core and Native AOT compatibility

Not all features in ASP.NET Core are currently compatible with Native AOT.

The following table summarizes ASP.NET Core feature compatibility with Native AOT.

| Feature              | Supported | Partial support | Not supported |
| -------------------- | --------- | --------------- | ------------- |
| Blazor Server        |           |                 | ❌             |
| CORS                 | ✔️        |                 |               |
| gRPC                 | ✔️        |                 |               |
| HealthChecks         | ✔️        |                 |               |
| HttpLogging          | ✔️        |                 |               |
| JWT Authentication   | ✔️        |                 |               |
| Localization         | ✔️        |                 |               |
| Minimal APIs         |           | ✔️              |               |
| MVC                  |           |                 | ❌             |
| OData                |           |                 | ❌             |
| Other Authentication |           |                 | ❌             |
| OutputCaching        | ✔️        |                 |               |
| RateLimiting         | ✔️        |                 |               |
| RequestDecompression | ✔️        |                 |               |
| ResponseCaching      | ✔️        |                 |               |
| ResponseCompression  | ✔️        |                 |               |
| Rewrite              | ✔️        |                 |               |
| Session              |           |                 | ❌             |
| SignalR              |           | ✔️              |               |
| Spa                  |           |                 | ❌             |
| StaticFiles          | ✔️        |                 |               |
| WebSockets           | ✔️        |                 |               |

For more information on limitations, see:

* [Limitations of Native AOT deployment](/en-us/dotnet/core/deploying/native-aot#limitations-of-native-aot-deployment)
* [AOT warnings](/en-us/dotnet/core/deploying/native-aot/fixing-warnings)
* [Known trimming incompatibilities](/en-us/dotnet/core/deploying/trimming/incompatibilities)
* [Fix trimming warnings](/en-us/dotnet/core/deploying/trimming/fixing-warnings)

### Verify app on the Native AOT deployment model

It's important to test an app thoroughly when you move to a Native AOT deployment model. Test the AOT deployed app and confirm the functionality is unchanged from the untrimmed and just-in-time (JIT) compiled app.

When you build the app, review and correct any AOT warnings. An app that issues [AOT warnings](/en-us/dotnet/core/deploying/trimming/fixing-warnings) during publishing might not work correctly. If no AOT warnings are issued at publish time, you can expect the published AOT app to work the same as the untrimmed and JIT-compiled app.

## Publish a Native AOT app (PublishAot)

Enable Native AOT for your application by using the `PublishAot` MSBuild property. The following example shows how to enable Native AOT in a project file:

```
<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>
```

The `PublishAot` property enables Native AOT compilation during the publish process, and enables dynamic code usage analysis during build and editing. A project that uses Native AOT publishing implements JIT compilation when it runs locally.

An AOT app has the following differences from a JIT-compiled app:

* Features that aren't compatible with Native AOT are disabled and throw exceptions at run time.
* A source analyzer is enabled to highlight code that isn't compatible with Native AOT. At publish time, the entire app, including NuGet packages, are analyzed for compatibility again.

Native AOT analysis includes all of the application code and the libraries the app depends on. Review Native AOT warnings and take corrective steps. It's a good idea to publish apps frequently to discover issues early in the development lifecycle.

In .NET 8 and later, the following ASP.NET Core app types support Native AOT:

* **Minimal APIs** - For more information, see [Review the Web API (Native AOT) template](#review-the-web-api-native-aot-template) later in this article.
* **gRPC** - For more information, see [gRPC and Native AOT](../grpc/native-aot?view=aspnetcore-10.0).
* **Worker services** - For more information, see [Background tasks with hosted services in ASP.NET Core > Native AOT](host/hosted-services?view=aspnetcore-10.0#native-aot).

## Review the Web API (Native AOT) template

The ASP.NET Core **Web API (Native AOT)** template (short name `webapiaot`) creates a project with AOT enabled. The template differs from a standard **Web API** project template in the following ways:

* Uses Minimal APIs only, as MVC isn't yet compatible with Native AOT.
* Uses the [CreateSlimBuilder()](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createslimbuilder#microsoft-aspnetcore-builder-webapplication-createslimbuilder) API to ensure only the essential features are enabled by default, which minimizes the app's deployed size.
* Is configured to listen on HTTP only. HTTPS traffic is commonly handled by an ingress service in cloud-native deployments.
* Doesn't include a launch profile for running under IIS or IIS Express.
* Creates an [.http file](../test/http-files?view=aspnetcore-10.0) configured with sample HTTP requests that can be sent to the app's endpoints.
* Includes a sample `Todo` API instead of the weather forecast sample.
* Adds the `PublishAot` property to the project file, as [described earlier](#publish-a-native-aot-app-publishaot).
* Enables the [JSON serializer source generators](/en-us/dotnet/standard/serialization/system-text-json/source-generation). The source generator is used to generate serialization code at build time, which is required for Native AOT compilation.

### Code updates for JSON serialization (Program.cs)

The code in the *Program.cs* file is modified to provide support for JSON serialization source generation.

The following snippet shows the changes to the code:

```
using MyFirstAotWebApi;
+using System.Text.Json.Serialization;

-var builder = WebApplication.CreateBuilder();
+var builder = WebApplication.CreateSlimBuilder(args);

+builder.Services.ConfigureHttpJsonOptions(options =>
+{
+  options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
+});

var app = builder.Build();

var sampleTodos = TodoGenerator.GenerateTodos().ToArray();

var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
    sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
        ? Results.Ok(todo)
        : Results.NotFound());

app.Run();

+[JsonSerializable(typeof(Todo[]))]
+internal partial class AppJsonSerializerContext : JsonSerializerContext
+{
+
+}
```

If you don't modify the code, `System.Text.Json` uses reflection to serialize and deserialize JSON. Reflection isn't supported in Native AOT.

For more information, see:

* [Combine source generators](/en-us/dotnet/standard/serialization/system-text-json/source-generation#combine-source-generators)
* [TypeInfoResolverChain](/en-us/dotnet/api/system.text.json.jsonserializeroptions.typeinforesolverchain#system-text-json-jsonserializeroptions-typeinforesolverchain)

### Code changes for launch profile (launchSettings.json)

The **Web API (Native AOT)** template creates a *launchSettings.json* file. In contrast to a standard launch file, the generated file doesn't include the `iisSettings` section or the `IIS Express` profile.

The following snippet shows the excluded sections (colored red):

```
{
  "$schema": "http://json.schemastore.org/launchsettings.json",
-  "iisSettings": {
-     "windowsAuthentication": false,
-     "anonymousAuthentication": true,
-     "iisExpress": {
-       "applicationUrl": "http://localhost:11152",
-       "sslPort": 0
-     }
-   },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "todos",
      "applicationUrl": "http://localhost:5102",
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development"
        }
      },
-     "IIS Express": {
-       "commandName": "IISExpress",
-       "launchBrowser": true,
-       "launchUrl": "todos",
-      "environmentVariables": {
-       "ASPNETCORE_ENVIRONMENT": "Development"
-      }
-    }
  }
}
```

### CreateSlimBuilder() called for minimal app defaults

The **Web API (Native AOT)** template uses the [CreateSlimBuilder()](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createslimbuilder#microsoft-aspnetcore-builder-webapplication-createslimbuilder) method instead of the [CreateBuilder()](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder#microsoft-aspnetcore-builder-webapplication-createbuilder) method.

```
using System.Text.Json.Serialization;
using MyFirstAotWebApi;

var builder = WebApplication.CreateSlimBuilder(args);
builder.Logging.AddConsole();

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

var sampleTodos = TodoGenerator.GenerateTodos().ToArray();

var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
    sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
        ? Results.Ok(todo)
        : Results.NotFound());

app.Run();

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
```

The `CreateSlimBuilder` method initializes the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) with the minimum ASP.NET Core features necessary to run an app.

As described earlier, the `CreateSlimBuilder` method doesn't include support for HTTPS or HTTP/3. These protocols typically aren't required for apps that run behind a TLS termination proxy. For example, see [TLS termination and end to end TLS with Application Gateway](/en-us/azure/application-gateway/ssl-overview). You can enable HTTPS by calling the [builder.WebHost.UseKestrelHttpsConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.usekestrelhttpsconfiguration) method, or enable HTTP/3 by calling the [builder.WebHost.UseQuic](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderquicextensions.usequic).

## Compare CreateSlimBuilder() and CreateBuilder()

The `CreateSlimBuilder` method provides access to a portion of the application features available with the `CreateBuilder` method. As described earlier, the **Web API (Native AOT)** template calls `CreateSlimBuilder` to initialize [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder), so the builder uses the minimum ASP.NET Core features necessary to run the app.

Both methods provide the necessary features for an efficient development experience:

* Configuration for the *appsettings.json* and *appsettings.{EnvironmentName}.json* files
* User secrets configuration
* Console logging
* Logging configuration

Including minimal features has benefits for trimming as well as AOT. For more information, see [Trim self-contained deployments and executables](/en-us/dotnet/core/deploying/trimming/trim-self-contained).

If you prefer to use a builder that omits all features, see the [WebApplication.CreateEmptyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createemptybuilder) method.

### Unavailable features in CreateSlimBuilder

The `CreateSlimBuilder` method **doesn't** provide the following features, which are available in `CreateBuilder`:

* [Hosting startup assemblies](host/platform-specific-configuration?view=aspnetcore-10.0)

* [UseStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestartup) method

* Logging providers:

  * [Windows EventLog](/en-us/aspnet/core/fundamentals/logging#windows-eventlog)
  * [Debug](/en-us/aspnet/core/fundamentals/logging#debug)
  * [EventSource](/en-us/aspnet/core/fundamentals/logging#eventsource)

* Web hosting features:

  * [UseStaticWebAssets](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestaticwebassets) method
  * [IIS integration](../host-and-deploy/iis/?view=aspnetcore-10.0)

* Kestrel configuration:

  * [HTTPS endpoints](servers/kestrel/endpoints?view=aspnetcore-10.0#configure-https)
  * [QUIC (HTTP/3)](servers/kestrel/http3?view=aspnetcore-10.0#http3-benefits)

* [Regex and alpha constraints used in routing (GitHub /dotnet/aspnetcore/issues #46142)](https://github.com/dotnet/aspnetcore/issues/46142)

For more detailed information, see [Comparing WebApplication.CreateBuilder to CreateSlimBuilder](https://andrewlock.net/exploring-the-dotnet-8-preview-comparing-createbuilder-to-the-new-createslimbuilder-method/)

## Use source generators and avoid reflection

During the publishing process for Native AOT, any unused code is trimmed. As a result, an app can't use unbounded reflection at runtime. You can use [source generators](/en-us/dotnet/csharp/roslyn-sdk/#source-generators) that produce code that avoids the need for reflection. In some cases, source generators output code optimized for AOT even when a generator isn't required.

* To view the generated source code, add the [EmitCompilerGeneratedFiles](/en-us/dotnet/core/extensions/configuration-generator) property to the application project (*.csproj*) file:

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

     <PropertyGroup>
       <!-- Other properties omitted for brevity -->
       <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
     </PropertyGroup>

  </Project>
  ```

* To see the generated code, run the `dotnet build` command. The command compiles the source files and generates the intermediate files needed to run the app in a development environment. The output includes an *obj/Debug/<.NET version>/generated/* directory that contains all the generated files for the project.

* To prepare the app for deployment, run the `dotnet publish` command. The command compiles the source files and generates all files required to deploy the app. It passes the generated assemblies to a native IL compiler, which produces the native executable. The native executable contains the native machine code.

## Work with Minimal APIs and JSON payloads

The Minimal API framework is optimized for receiving and returning JSON payloads by using the [System.Text.Json](/en-us/dotnet/api/system.text.json).

* The namespace imposes compatibility requirements for JSON and Native AOT.
* It requires the use of the [System.Text.Json source generator](/en-us/dotnet/standard/serialization/system-text-json/source-generation).

All types transmitted as part of the HTTP body or returned from request delegates in Minimal APIs apps must be configured on a [JsonSerializerContext](/en-us/dotnet/api/system.text.json.serialization.jsonserializercontext) instance. The instance must be registered with ASP.NET Core dependency injection:

```
using System.Text.Json.Serialization;
using MyFirstAotWebApi;

var builder = WebApplication.CreateSlimBuilder(args);
builder.Logging.AddConsole();

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

var sampleTodos = TodoGenerator.GenerateTodos().ToArray();

var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
    sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
        ? Results.Ok(todo)
        : Results.NotFound());

app.Run();

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
```

* The JSON serializer context is registered with the [DI container](dependency-injection?view=aspnetcore-10.0). For more information, see [Combine source generators](/en-us/dotnet/standard/serialization/system-text-json/source-generation#combine-source-generators) and the [TypeInfoResolverChain](/en-us/dotnet/api/system.text.json.jsonserializeroptions.typeinforesolverchain#system-text-json-jsonserializeroptions-typeinforesolverchain).

* The custom `JsonSerializerContext` is annotated with the [JsonSerializable](/en-us/dotnet/api/system.text.json.serialization.jsonserializableattribute) attribute, which enables source generated JSON serializer code for the `ToDo` type.

A parameter on the delegate that isn't bound to the body **doesn't** need to be serializable. For example, a query string parameter can be a rich object type that implements `IParsable<T>`.

```
public class Todo
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public DateOnly? DueBy { get; set; }
    public bool IsComplete { get; set; }
}

static class TodoGenerator
{
    private static readonly (string[] Prefixes, string[] Suffixes)[] _parts = new[]
        {
            (new[] { "Walk the", "Feed the" }, new[] { "dog", "cat", "goat" }),
            (new[] { "Do the", "Put away the" }, new[] { "groceries", "dishes", "laundry" }),
            (new[] { "Clean the" }, new[] { "bathroom", "pool", "blinds", "car" })
        };
    // Remaining code omitted for brevity.
```

## Review known issues

To report or review issues with Native AOT support in ASP.NET Core, see [GitHub /dotnet/core/issues #8288)](https://github.com/dotnet/core/issues/8288).

## Related content

* [Publish an ASP.NET Core app with Native AOT](aot/native-aot-tutorial?view=aspnetcore-10.0)
* [Native AOT deployment](/en-us/dotnet/core/deploying/native-aot/)
* [Optimize AOT deployments](/en-us/dotnet/core/deploying/native-aot/optimizing)

.NET 8 introduces support for [.NET native ahead-of-time (AOT)](/en-us/dotnet/core/deploying/native-aot/).

## Why use Native AOT with ASP.NET Core

Publishing and deploying a Native AOT app provides the following benefits:

* **Minimized disk footprint**: When publishing using Native AOT, a single executable is produced containing just the code from external dependencies that is needed to support the program. Reduced executable size can lead to:

  * Smaller container images, for example in containerized deployment scenarios.
  * Reduced deployment time from smaller images.

* **Reduced startup time**: Native AOT applications can show reduced start-up times, which means

  * The app is ready to service requests quicker.
  * Improved deployment where container orchestrators need to manage transition from one version of the app to another.

* **Reduced memory demand**: Native AOT apps can have reduced memory demands, depending on the work done by the app. Reduced memory consumption can lead to greater deployment density and improved scalability.

The template app was run in our benchmarking lab to compare performance of an AOT published app, a trimmed runtime app, and an untrimmed runtime app. The following chart shows the results of the benchmarking:

The preceding chart shows that Native AOT has lower app size, memory usage, and startup time.

## ASP.NET Core and Native AOT compatibility

Not all features in ASP.NET Core are currently compatible with Native AOT. The following table summarizes ASP.NET Core feature compatibility with Native AOT.

| Feature              | Fully Supported   | Partially Supported   | Not Supported  |
| -------------------- | ----------------- | --------------------- | -------------- |
| gRPC                 | ✔️Fully supported |                       |                |
| Minimal APIs         |                   | ✔️Partially supported |                |
| MVC                  |                   |                       | ❌Not supported |
| Blazor Server        |                   |                       | ❌Not supported |
| SignalR              |                   |                       | ❌Not supported |
| JWT Authentication   | ✔️Fully supported |                       |                |
| Other Authentication |                   |                       | ❌Not supported |
| CORS                 | ✔️Fully supported |                       |                |
| HealthChecks         | ✔️Fully supported |                       |                |
| HttpLogging          | ✔️Fully supported |                       |                |
| Localization         | ✔️Fully supported |                       |                |
| OutputCaching        | ✔️Fully supported |                       |                |
| RateLimiting         | ✔️Fully supported |                       |                |
| RequestDecompression | ✔️Fully supported |                       |                |
| ResponseCaching      | ✔️Fully supported |                       |                |
| ResponseCompression  | ✔️Fully supported |                       |                |
| Rewrite              | ✔️Fully supported |                       |                |
| Session              |                   |                       | ❌Not supported |
| Spa                  |                   |                       | ❌Not supported |
| StaticFiles          | ✔️Fully supported |                       |                |
| WebSockets           | ✔️Fully supported |                       |                |

For more information on limitations, see:

* [Limitations of Native AOT deployment](/en-us/dotnet/core/deploying/native-aot#limitations-of-native-aot-deployment)
* [Introduction to AOT warnings](/en-us/dotnet/core/deploying/native-aot/fixing-warnings)
* [Known trimming incompatibilities](/en-us/dotnet/core/deploying/trimming/incompatibilities)
* [Introduction to trim warnings](/en-us/dotnet/core/deploying/trimming/fixing-warnings)
* [GitHub issue dotnet/core #8288](https://github.com/dotnet/core/issues/8288)

It's important to test an app thoroughly when moving to a Native AOT deployment model. The AOT deployed app should be tested to verify functionality hasn't changed from the untrimmed and JIT-compiled app. When building the app, review and correct AOT warnings. An app that issues [AOT warnings](/en-us/dotnet/core/deploying/trimming/fixing-warnings) during publishing may not work correctly. If no AOT warnings are issued at publish time, the published AOT app should work the same as the untrimmed and JIT-compiled app.

## Native AOT publishing

Native AOT is enabled with the `PublishAot` MSBuild property. The following example shows how to enable Native AOT in a project file:

```
<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>
```

This setting enables Native AOT compilation during publish and enables dynamic code usage analysis during build and editing. A project that uses Native AOT publishing uses JIT compilation when running locally. An AOT app has the following differences from a JIT-compiled app:

* Features that aren't compatible with Native AOT are disabled and throw exceptions at run time.
* A source analyzer is enabled to highlight code that isn't compatible with Native AOT. At publish time, the entire app, including NuGet packages, are analyzed for compatibility again.

Native AOT analysis includes all of the app's code and the libraries the app depends on. Review Native AOT warnings and take corrective steps. It's a good idea to publish apps frequently to discover issues early in the development lifecycle.

In .NET 8, Native AOT is supported by the following ASP.NET Core app types:

* Minimal APIs - For more information, see the [The Web API (Native AOT) template](#the-web-api-native-aot-template) section later in this article.
* gRPC - For more information, see [gRPC and Native AOT](../grpc/native-aot?view=aspnetcore-10.0).
* Worker services - For more information, see [AOT in Worker Service templates](host/hosted-services?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#native-aot).

## The Web API (Native AOT) template

The **ASP.NET Core Web API (Native AOT)** template (short name `webapiaot`) creates a project with AOT enabled. The template differs from the **Web API** project template in the following ways:

* Uses Minimal APIs only, as MVC isn't yet compatible with Native AOT.
* Uses the [CreateSlimBuilder()](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createslimbuilder#microsoft-aspnetcore-builder-webapplication-createslimbuilder) API to ensure only the essential features are enabled by default, minimizing the app's deployed size.
* Is configured to listen on HTTP only, as HTTPS traffic is commonly handled by an ingress service in cloud-native deployments.
* Doesn't include a launch profile for running under IIS or IIS Express.
* Creates an [`.http` file](../test/http-files?view=aspnetcore-10.0) configured with sample HTTP requests that can be sent to the app's endpoints.
* Includes a sample `Todo` API instead of the weather forecast sample.
* Adds `PublishAot` to the project file, as shown [earlier in this article](#native-aot-publishing).
* Enables the [JSON serializer source generators](/en-us/dotnet/standard/serialization/system-text-json/source-generation). The source generator is used to generate serialization code at build time, which is required for Native AOT compilation.

### Changes to support source generation

The following example shows the code added to the `Program.cs` file to support JSON serialization source generation:

```
using MyFirstAotWebApi;
+using System.Text.Json.Serialization;

-var builder = WebApplication.CreateBuilder();
+var builder = WebApplication.CreateSlimBuilder(args);

+builder.Services.ConfigureHttpJsonOptions(options =>
+{
+  options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
+});

var app = builder.Build();

var sampleTodos = TodoGenerator.GenerateTodos().ToArray();

var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
    sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
        ? Results.Ok(todo)
        : Results.NotFound());

app.Run();

+[JsonSerializable(typeof(Todo[]))]
+internal partial class AppJsonSerializerContext : JsonSerializerContext
+{
+
+}
```

Without this added code, `System.Text.Json` uses reflection to serialize and deserialize JSON. Reflection isn't supported in Native AOT.

For more information, see:

* [Combine source generators](/en-us/dotnet/standard/serialization/system-text-json/source-generation?pivots=dotnet-8-0#combine-source-generators)
* [TypeInfoResolverChain](/en-us/dotnet/api/system.text.json.jsonserializeroptions.typeinforesolverchain#system-text-json-jsonserializeroptions-typeinforesolverchain)

### Changes to `launchSettings.json`

The `launchSettings.json` file created by the **Web API (Native AOT)** template has the `iisSettings` section and `IIS Express` profile removed:

```
{
  "$schema": "http://json.schemastore.org/launchsettings.json",
-  "iisSettings": {
-     "windowsAuthentication": false,
-     "anonymousAuthentication": true,
-     "iisExpress": {
-       "applicationUrl": "http://localhost:11152",
-       "sslPort": 0
-     }
-   },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "todos",
      "applicationUrl": "http://localhost:5102",
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development"
        }
      },
-     "IIS Express": {
-       "commandName": "IISExpress",
-       "launchBrowser": true,
-       "launchUrl": "todos",
-      "environmentVariables": {
-       "ASPNETCORE_ENVIRONMENT": "Development"
-      }
-    }
  }
}
```

[]()

### The `CreateSlimBuilder` method

The template uses the [CreateSlimBuilder()](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createslimbuilder#microsoft-aspnetcore-builder-webapplication-createslimbuilder) method instead of the [CreateBuilder()](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder#microsoft-aspnetcore-builder-webapplication-createbuilder) method.

```
using System.Text.Json.Serialization;
using MyFirstAotWebApi;

var builder = WebApplication.CreateSlimBuilder(args);
builder.Logging.AddConsole();

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

var sampleTodos = TodoGenerator.GenerateTodos().ToArray();

var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
    sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
        ? Results.Ok(todo)
        : Results.NotFound());

app.Run();

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
```

The `CreateSlimBuilder` method initializes the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) with the minimum ASP.NET Core features necessary to run an app.

As noted earlier, the `CreateSlimBuilder` method doesn't include support for HTTPS or HTTP/3. These protocols typically aren't required for apps that run behind a TLS termination proxy. For example, see [TLS termination and end to end TLS with Application Gateway](/en-us/azure/application-gateway/ssl-overview). HTTPS can be enabled by calling [builder.WebHost.UseKestrelHttpsConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.usekestrelhttpsconfiguration) HTTP/3 can be enabled by calling [builder.WebHost.UseQuic](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderquicextensions.usequic).

### `CreateSlimBuilder` vs `CreateBuilder`

The `CreateSlimBuilder` method doesn't support the following features that are supported by the `CreateBuilder` method:

* [Hosting startup assemblies](host/platform-specific-configuration?view=aspnetcore-10.0)

* [UseStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestartup)

* The following logging providers:

  * [Windows EventLog](/en-us/aspnet/core/fundamentals/logging#windows-eventlog)
  * [Debug](/en-us/aspnet/core/fundamentals/logging#debug)
  * [Event Source](/en-us/aspnet/core/fundamentals/logging#event-source)

* Web hosting features:

  * [UseStaticWebAssets](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestaticwebassets)
  * [IIS Integration](../host-and-deploy/iis/?view=aspnetcore-10.0)

* Kestrel configuration

  * [HTTPS endpoints in Kestrel](servers/kestrel/endpoints?view=aspnetcore-10.0#https)
  * [Quic (HTTP/3)](servers/kestrel/http3?view=aspnetcore-10.0#http3-benefits)

* [Regex and alpha constraints used in routing](https://github.com/dotnet/aspnetcore/issues/46142)

The `CreateSlimBuilder` method includes the following features needed for an efficient development experience:

* JSON file configuration for `appsettings.json` and `appsettings.{EnvironmentName}.json`.
* User secrets configuration.
* Console logging.
* Logging configuration.

For a builder that omits the preceding features, see [The `CreateEmptyBuilder` method](../release-notes/aspnetcore-8.0?view=aspnetcore-10.0#new-createemptybuilder-method).

Including minimal features has benefits for trimming as well as AOT. For more information, see [Trim self-contained deployments and executables](/en-us/dotnet/core/deploying/trimming/trim-self-contained).

For more detailed information, see [Comparing `WebApplication.CreateBuilder` to `CreateSlimBuilder`](https://andrewlock.net/exploring-the-dotnet-8-preview-comparing-createbuilder-to-the-new-createslimbuilder-method/)

## Source generators

Because unused code is trimmed during publishing for Native AOT, the app can't use unbounded reflection at runtime. [Source generators](/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) are used to produce code that avoids the need for reflection. In some cases, source generators produce code optimized for AOT even when a generator isn't required.

To view the source code that is generated, add the [`EmitCompilerGeneratedFiles`](/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) property to an app's `.csproj` file, as shown in the following example:

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

  <PropertyGroup>
    <!-- Other properties omitted for brevity -->
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  </PropertyGroup>

</Project>
```

Run the `dotnet build` command to see the generated code. The output includes an `obj/Debug/net8.0/generated/` directory that contains all the generated files for the project.

The `dotnet publish` command also compiles the source files and generates files that are compiled. In addition, `dotnet publish` passes the generated assemblies to a native IL compiler. The IL compiler produces the native executable. The native executable contains the native machine code.

## Minimal APIs and JSON payloads

The Minimal API framework is optimized for receiving and returning JSON payloads using [System.Text.Json](/en-us/dotnet/api/system.text.json). `System.Text.Json`:

* Imposes compatibility requirements for JSON and Native AOT.
* Requires the use of the [`System.Text.Json` source generator](/en-us/dotnet/standard/serialization/system-text-json/source-generation).

All types that are transmitted as part of the HTTP body or returned from request delegates in Minimal APIs apps must be configured on a [JsonSerializerContext](/en-us/dotnet/api/system.text.json.serialization.jsonserializercontext) that is registered via ASP.NET Core's dependency injection:

```
using System.Text.Json.Serialization;
using MyFirstAotWebApi;

var builder = WebApplication.CreateSlimBuilder(args);
builder.Logging.AddConsole();

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

var sampleTodos = TodoGenerator.GenerateTodos().ToArray();

var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
    sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
        ? Results.Ok(todo)
        : Results.NotFound());

app.Run();

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
```

In the preceding highlighted code:

* The JSON serializer context is registered with the [DI container](dependency-injection?view=aspnetcore-10.0). For more information, see:

  * [Combine source generators](/en-us/dotnet/standard/serialization/system-text-json/source-generation?pivots=dotnet-8-0#combine-source-generators)
  * [TypeInfoResolverChain](/en-us/dotnet/api/system.text.json.jsonserializeroptions.typeinforesolverchain#system-text-json-jsonserializeroptions-typeinforesolverchain)

* The custom `JsonSerializerContext` is annotated with the [`[JsonSerializable]`](/en-us/dotnet/api/system.text.json.serialization.jsonserializableattribute) attribute to enable source generated JSON serializer code for the `ToDo` type.

A parameter on the delegate that isn't bound to the body and does ***not*** need to be serializable. For example, a query string parameter that is a rich object type and implements `IParsable<T>`.

```
public class Todo
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public DateOnly? DueBy { get; set; }
    public bool IsComplete { get; set; }
}

static class TodoGenerator
{
    private static readonly (string[] Prefixes, string[] Suffixes)[] _parts = new[]
        {
            (new[] { "Walk the", "Feed the" }, new[] { "dog", "cat", "goat" }),
            (new[] { "Do the", "Put away the" }, new[] { "groceries", "dishes", "laundry" }),
            (new[] { "Clean the" }, new[] { "bathroom", "pool", "blinds", "car" })
        };
    // Remaining code omitted for brevity.
```

## Known issues

See [this GitHub issue](https://github.com/dotnet/core/issues/8288) to report or review issues with Native AOT support in ASP.NET Core.

## See also

* [Tutorial: Publish an ASP.NET Core app using Native AOT](aot/native-aot-tutorial?view=aspnetcore-10.0)
* [Native AOT deployment](/en-us/dotnet/core/deploying/native-aot/)
* [Optimize AOT deployments](/en-us/dotnet/core/deploying/native-aot/optimizing)
* [Configuration-binding source generator](/en-us/dotnet/core/whats-new/dotnet-8#configuration-binding-source-generator)
* [Using the configuration binder source generator](https://andrewlock.net/exploring-the-dotnet-8-preview-using-the-new-configuration-binder-source-generator/)
* [The Minimal API AOT compilation template](https://andrewlock.net/exploring-the-dotnet-8-preview-the-minimal-api-aot-template/)
* [Comparing `WebApplication.CreateBuilder` to `CreateSlimBuilder`](https://andrewlock.net/exploring-the-dotnet-8-preview-comparing-createbuilder-to-the-new-createslimbuilder-method/)
* [Exploring the new Minimal API source generator](https://andrewlock.net/exploring-the-dotnet-8-preview-exploring-the-new-minimal-api-source-generator/)
* [Replacing method calls with Interceptors](https://andrewlock.net/exploring-the-dotnet-8-preview-changing-method-calls-with-interceptors/)
* [Behind `[LogProperties]` and the new telemetry logging source generator](https://andrewlock.net/behind-logproperties-and-the-new-telemetry-logging-source-generator/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/httpcontext
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/httpcontext.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# `IHttpContextAccessor`/`HttpContext` in ASP.NET Core SignalR

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor)/[HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) generally should be avoided with SignalR because a valid [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) isn't always available. In most cases, the context doesn't exist (`null`).

Even when an [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) instance is available, the context is dependent on the transport:

* WebSockets receives a single context as the result of the initial handshake.
* Long polling receives a new context per client "poll" request.
* A SignalR service receives a mocked/faked/shim context.

When working within a SignalR hub, you can access the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) directly using the [GetHttpContextExtensions.GetHttpContext](/en-us/dotnet/api/microsoft.aspnetcore.signalr.gethttpcontextextensions.gethttpcontext) method. This method returns the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) for the current connection or `null` if the connection isn't associated with an HTTP request. This is particularly useful for retrieving HTTP connection information, such as headers and query strings, directly within the hub. We recommend calling this method over [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) for accessing [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) in the hub. For more information, see [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0#use-context-object-properties-and-methods).

For guidance on [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor)/[HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) in ASP.NET Core Blazor apps, see [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../blazor/components/httpcontext?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-nswag
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/getting-started-with-NSwag.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Get started with NSwag and ASP.NET Core

Note

In .NET 9 and later, ASP.NET Core includes built-in OpenAPI support. NSwag isn't included by default, but you can add it manually as a community package to ASP.NET Core projects targeting .NET 9 or later.

• To understand the built-in OpenAPI features, see [Overview of OpenAPI support in ASP.NET Core API apps](../fundamentals/openapi/overview?view=aspnetcore-8.0\&preserve-view=true\&view=aspnetcore-9.0).\
• To add and use the Swagger UI for interactive exploration or local ad-hoc testing, see [Use the generated OpenAPI documents](../fundamentals/openapi/using-openapi-documents?view=aspnetcore-8.0#use-swagger-ui-for-local-ad-hoc-testing).\
• For a step-by-step walkthrough of building a Minimal API that uses the built-in OpenAPI support in the latest version of ASP.NET Core, see [Tutorial: Create a Minimal API with ASP.NET Core](min-web-api?view=aspnetcore-8.0). It shows how to test endpoints with Endpoints Explorer and `.http` files in Visual Studio and with the Scalar UI in Visual Studio Code.

The following instructions apply when using NSwag with .NET versions earlier than 9.

By [Rico Suter](https://rsuter.com) and [Dave Brock](https://twitter.com/daveabrock)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/web-api-help-pages-using-swagger/samples/7.x/NSwagSample) ([how to download](../fundamentals/?view=aspnetcore-8.0#how-to-download-a-sample))

NSwag offers the following capabilities:

* The ability to utilize the Swagger UI and Swagger generator.
* Flexible code generation capabilities.

With NSwag, you don't need an existing API—you can use third-party APIs that incorporate Swagger and generate a client implementation. NSwag allows you to expedite the development cycle and easily adapt to API changes.

## Package installation

Install NSwag to:

* Generate the Swagger specification for the implemented web API.
* Serve the Swagger UI to browse and test the web API.
* Serve the Redoc to add API documentation for the Web API.

To use the [NSwag](https://github.com/RicoSuter/NSwag) ASP.NET Core middleware, install the [NSwag.AspNetCore](https://www.nuget.org/packages/NSwag.AspNetCore/) NuGet package. This package contains the middleware to generate and serve the Swagger specification, Swagger UI (v2 and v3), and [ReDoc UI](https://github.com/Rebilly/ReDoc). NSwag 14 supports only v3 of the Swagger UI spec.

Use one of the following approaches to install the NSwag NuGet package:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [.NET CLI](#tabpanel_1_net-cli)

- From the **Package Manager Console** window:

  * Go to **View** > **Other Windows** > **Package Manager Console**

  * Navigate to the directory in which the `NSwagSample.csproj` file exists

  * Execute the following command:

    ```
    Install-Package NSwag.AspNetCore
    ```

- From the **Manage NuGet Packages** dialog:

  * Right-click the project in **Solution Explorer** > **Manage NuGet Packages**
  * Set the **Package source** to "nuget.org"
  * Enter "NSwag.AspNetCore" in the search box
  * Select the "NSwag.AspNetCore" package from the **Browse** tab and select **Install**

* Right-click the *Packages* folder in **Solution Pad** > **Add Packages...**
* Set the **Add Packages** window's **Source** drop-down to "nuget.org"
* Enter "NSwag.AspNetCore" in the search box
* Select the "NSwag.AspNetCore" package from the results pane and select **Add Package**

Run the following command from the **Integrated Terminal**:

```
dotnet add NSwagSample.csproj package NSwag.AspNetCore
```

Run the following command:

```
dotnet add NSwagSample.csproj package NSwag.AspNetCore
```

## Add and configure Swagger middleware

Add and configure Swagger in your ASP.NET Core app by performing the following steps:

* Add the OpenApi generator to the services collection in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddOpenApiDocument();
```

* Enable the middleware for serving the generated OpenApi specification, the Swagger UI, and the Redoc UI, also in `Program.cs`:

```
if (app.Environment.IsDevelopment())
{
    // Add OpenAPI 3.0 document serving middleware
    // Available at: http://localhost:<port>/swagger/v1/swagger.json
    app.UseOpenApi();

    // Add web UIs to interact with the document
    // Available at: http://localhost:<port>/swagger
    app.UseSwaggerUi(); // UseSwaggerUI Protected by if (env.IsDevelopment())
}
```

* Launch the app. Navigate to:

  * `http://localhost:<port>/swagger` to view the Swagger UI.
  * `http://localhost:<port>/swagger/v1/swagger.json` to view the Swagger specification.

## Code generation

You can take advantage of NSwag's code generation capabilities by choosing one of the following options:

* [NSwagStudio](https://github.com/RicoSuter/NSwag/wiki/NSwagStudio): A Windows desktop app for generating API client code in C# or TypeScript.
* The [NSwag.CodeGeneration.CSharp](https://www.nuget.org/packages/NSwag.CodeGeneration.CSharp/) or [NSwag.CodeGeneration.TypeScript](https://www.nuget.org/packages/NSwag.CodeGeneration.TypeScript/) NuGet packages for code generation inside your project.
* NSwag from the [command line](https://github.com/RicoSuter/NSwag/wiki/CommandLine).
* The [NSwag.MSBuild](https://github.com/RicoSuter/NSwag/wiki/NSwag.MSBuild) NuGet package.
* The [Unchase OpenAPI (Swagger) Connected Service](https://marketplace.visualstudio.com/items?itemName=Unchase.unchaseopenapiconnectedservice): A Visual Studio Connected Service for generating API client code in C# or TypeScript. Also generates C# controllers for OpenAPI services with NSwag.

### Generate code with NSwagStudio

* Install NSwagStudio by following the instructions at the [NSwagStudio GitHub repository](https://github.com/RicoSuter/NSwag/wiki/NSwagStudio). On the NSwag release page, you can download an xcopy version, which can be started without installation and admin privileges.
* Launch NSwagStudio and enter the `swagger.json` file URL in the **Swagger Specification URL** text box. For example, `http://localhost:5232/swagger/v1/swagger.json`.
* Select the **Create local Copy** button to generate a JSON representation of your Swagger specification.

- In the **Outputs** area, select the **CSharp Client** checkbox. Depending on your project, you can also choose **TypeScript Client** or **CSharp Web API Controller**. If you select **CSharp Web API Controller**, a service specification rebuilds the service, serving as a reverse generation.
- Select **Generate Outputs** to produce a complete C# client implementation of the *TodoApi.NSwag* project. To see the generated client code, select the **CSharp Client** tab:

```
namespace MyNamespace
{
    using System = global::System;

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.1.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial class TodoClient
    {
    #pragma warning disable 8618 // Set by constructor via BaseUrl property
        private string _baseUrl;
    #pragma warning restore 8618 // Set by constructor via BaseUrl property
        private System.Net.Http.HttpClient _httpClient;
        private static System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings, true);

        public TodoClient(System.Net.Http.HttpClient httpClient)
        {
            BaseUrl = "http://localhost:5232";
            _httpClient = httpClient;
        }

        private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings()
        {
            var settings = new Newtonsoft.Json.JsonSerializerSettings();
            UpdateJsonSerializerSettings(settings);
            return settings;
        }

        public string BaseUrl
        {
            get { return _baseUrl; }
            set
            {
                _baseUrl = value;
                if (!string.IsNullOrEmpty(_baseUrl) && !_baseUrl.EndsWith("/"))
                    _baseUrl += '/';
            }
        }
        // code omitted for brevity
```

Tip

The C# client code is generated based on selections in the **Settings** tab. Modify the settings to perform tasks such as default namespace renaming and synchronous method generation.

* Copy the generated C# code into a file in the client project that will consume the API.
* Start consuming the web API:

```
var todoClient = new TodoClient(new HttpClient());

// Gets all to-dos from the API
var allTodos = await todoClient.GetAsync();

// Create a new TodoItem, and save it via the API.
await todoClient.CreateAsync(new TodoItem());

// Get a single to-do by ID
var foundTodo = await todoClient.GetByIdAsync(1);
```

## Customize API documentation

OpenApi provides options for documenting the object model to ease the consumption of the web API.

### API info and description

In `Program.cs`, update `AddOpenApiDocument` to configure the document info of the Web API and include more information such as the author, license, and description. Import the `NSwag` namespace first to use the `OpenApi` classes.

```
using NSwag;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApiDocument(options => {
     options.PostProcess = document =>
     {
         document.Info = new OpenApiInfo
         {
             Version = "v1",
             Title = "ToDo API",
             Description = "An ASP.NET Core Web API for managing ToDo items",
             TermsOfService = "https://example.com/terms",
             Contact = new OpenApiContact
             {
                 Name = "Example Contact",
                 Url = "https://example.com/contact"
             },
             License = new OpenApiLicense
             {
                 Name = "Example License",
                 Url = "https://example.com/license"
             }
         };
     };
});
```

The Swagger UI displays the version's information:

### XML comments

To enable XML comments, perform the following steps:

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [.NET CLI](#tabpanel_2_net-cli)

- Right-click the project in **Solution Explorer** and select *`Edit <project_name>.csproj`*.
- Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```

* From the *Solution Pad*, press **control** and select the project name. Navigate to **Tools** > **Edit File**.
* Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```

Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```

Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
```

Enabling XML comments provides debug information for undocumented public types and members. Undocumented types and members are indicated by the warning message. For example, the following message indicates a violation of warning code 1591:

```
warning CS1591: Missing XML comment for publicly visible type or member 'TodoContext'
```

To suppress warnings project-wide, define a semicolon-delimited list of warning codes to ignore in the project file. Appending the warning codes to `$(NoWarn);` applies the [C# default values](https://github.com/dotnet/sdk/blob/2eb6c546931b5bcb92cd3128b93932a980553ea1/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.CSharp.props#L16) too.

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

To suppress warnings only for specific members, enclose the code in [#pragma warning](/en-us/dotnet/csharp/language-reference/preprocessor-directives#pragma-warning) preprocessor directives. This approach is useful for code that shouldn't be exposed via the API docs. In the following example, warning code CS1591 is ignored for the entire `TodoContext` class. Enforcement of the warning code is restored at the close of the class definition. Specify multiple warning codes with a comma-delimited list.

```
namespace NSwagSample.Models;

#pragma warning disable CS1591
public class TodoContext : DbContext
{
    public TodoContext(DbContextOptions<TodoContext> options) : base(options) { }

    public DbSet<TodoItem> TodoItems => Set<TodoItem>();
}
#pragma warning restore CS1591
```

### Data annotations

Mark the model with attributes, found in the [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace, to help drive the Swagger UI components.

Add the `[Required]` attribute to the `Name` property of the `TodoItem` class:

```
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace NSwagSample.Models;

public class TodoItem
{
    public long Id { get; set; }

    [Required]
    public string Name { get; set; } = null!;

    [DefaultValue(false)]
    public bool IsComplete { get; set; }
}
```

The presence of this attribute changes the UI behavior and alters the underlying JSON schema:

```
"TodoItem": {
  "type": "object",
  "additionalProperties": false,
  "required": [
    "name"
  ],
  "properties": {
    "id": {
      "type": "integer",
      "format": "int64"
    },
    "name": {
      "type": "string",
      "minLength": 1
    },
    "isComplete": {
      "type": "boolean",
      "default": false
    }
  }
}
```

As the usage of data annotations in the web API increases, the UI and API help pages become more descriptive and useful.

### Describe response types

Developers consuming a web API are most concerned with what's returned—specifically response types and error codes (if not standard). The response types and error codes are denoted in the XML comments and data annotations.

The `Create` action returns an HTTP 201 status code on success. An HTTP 400 status code is returned when the posted request body is `null`. Without proper documentation in the Swagger UI, the consumer lacks knowledge of these expected outcomes. Fix that problem by adding the highlighted lines in the following example:

```
/// <summary>
/// Creates a TodoItem.
/// </summary>
/// <param name="item"></param>
/// <returns>A newly created TodoItem</returns>
/// <remarks>
/// Sample request:
///
///     POST /Todo
///     {
///        "id": 1,
///        "name": "Item #1",
///        "isComplete": true
///     }
///
/// </remarks>
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(TodoItem item)
{
    _context.TodoItems.Add(item);
    await _context.SaveChangesAsync();

    return CreatedAtAction(nameof(Get), new { id = item.Id }, item);
}
```

The Swagger UI now clearly documents the expected HTTP response codes (and the [XML comments](#xml-comments) are also displayed):

Conventions can be used as an alternative to explicitly decorating individual actions with `[ProducesResponseType]`. For more information, see [Use web API conventions](../web-api/advanced/conventions?view=aspnetcore-8.0).

### Redoc

Redoc is an alternative to the Swagger UI. It's similar because it also provides a documentation page for the Web API using the OpenAPI specification. The difference is that Redoc UI is more focused on the documentation, and doesn't provide an interactive UI to test the API.

To enable Redoc, add its middleware to `Program.cs`:

```
if (app.Environment.IsDevelopment())
{
    // Add OpenAPI 3.0 document serving middleware
    // Available at: http://localhost:<port>/swagger/v1/swagger.json
    app.UseOpenApi();

    // Add web UIs to interact with the document
    // Available at: http://localhost:<port>/swagger
    app.UseSwaggerUi(); // UseSwaggerUI is called only in Development.
    
    // Add ReDoc UI to interact with the document
    // Available at: http://localhost:<port>/redoc
    app.UseReDoc(options =>
    {
        options.Path = "/redoc";
    });
}
```

Run the application and navigate to `http://localhost:<port>/redoc` to view the Redoc UI:

By [Rico Suter](https://rsuter.com) and [Dave Brock](https://twitter.com/daveabrock)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/web-api-help-pages-using-swagger/samples/2.1/TodoApi.NSwag) ([how to download](../fundamentals/?view=aspnetcore-8.0#how-to-download-a-sample))

NSwag offers the following capabilities:

* The ability to utilize the Swagger UI and Swagger generator.
* Flexible code generation capabilities.

With NSwag, you don't need an existing API—you can use third-party APIs that incorporate Swagger and generate a client implementation. NSwag allows you to expedite the development cycle and easily adapt to API changes.

## Register the NSwag middleware

Register the NSwag middleware to:

* Generate the Swagger specification for the implemented web API.
* Serve the Swagger UI to browse and test the web API.

To use the [NSwag](https://github.com/RicoSuter/NSwag) ASP.NET Core middleware, install the [NSwag.AspNetCore](https://www.nuget.org/packages/NSwag.AspNetCore/) NuGet package. This package contains the middleware to generate and serve the Swagger specification, Swagger UI (v2 and v3), and [ReDoc UI](https://github.com/Rebilly/ReDoc).

Use one of the following approaches to install the NSwag NuGet package:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [.NET CLI](#tabpanel_3_net-cli)

- From the **Package Manager Console** window:

  * Go to **View** > **Other Windows** > **Package Manager Console**.

  * Navigate to the directory in which the `TodoApi.csproj` file exists.

  * Execute the following command:

    ```
    Install-Package NSwag.AspNetCore
    ```

- From the **Manage NuGet Packages** dialog:

  * Right-click the project in **Solution Explorer** > **Manage NuGet Packages**
  * Set the **Package source** to "nuget.org"
  * Enter "NSwag.AspNetCore" in the search box
  * Select the "NSwag.AspNetCore" package from the **Browse** tab and click **Install**

* Right-click the *Packages* folder in **Solution Pad** > **Add Packages...**.
* Set the **Add Packages** window's **Source** drop-down to "nuget.org".
* Enter "NSwag.AspNetCore" in the search box.
* Select the "NSwag.AspNetCore" package from the results pane and click **Add Package**.

Run the following command from the **Integrated Terminal**:

```
dotnet add TodoApi.csproj package NSwag.AspNetCore
```

Run the following command:

```
dotnet add TodoApi.csproj package NSwag.AspNetCore
```

## Add and configure Swagger middleware

Add and configure Swagger in your ASP.NET Core app by performing the following steps:

* In the `Startup.ConfigureServices` method, register the required Swagger services:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(opt =>
        opt.UseInMemoryDatabase("TodoList"));
    services.AddMvc();

    // Register the Swagger services
    services.AddSwaggerDocument();
}
```

* In the `Startup.Configure` method, enable the middleware for serving the generated Swagger specification and the Swagger UI:

```
public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();

    // Register the Swagger generator and the Swagger UI middlewares
    app.UseOpenApi();
    app.UseOpenApi();
    if (env.IsDevelopment())
    {
        app.UseSwaggerUi3();
    }
    app.UseMvc();
}
```

* Launch the app. Navigate to:

  * `http://localhost:<port>/swagger` to view the Swagger UI.
  * `http://localhost:<port>/swagger/v1/swagger.json` to view the Swagger specification.

## Code generation

You can take advantage of NSwag's code generation capabilities by choosing one of the following options:

* [NSwagStudio](https://github.com/RicoSuter/NSwag/wiki/NSwagStudio): A Windows desktop app for generating API client code in C# or TypeScript.
* The [NSwag.CodeGeneration.CSharp](https://www.nuget.org/packages/NSwag.CodeGeneration.CSharp/) or [NSwag.CodeGeneration.TypeScript](https://www.nuget.org/packages/NSwag.CodeGeneration.TypeScript/) NuGet packages for code generation inside your project.
* NSwag from the [command line](https://github.com/RicoSuter/NSwag/wiki/CommandLine).
* The [NSwag.MSBuild](https://github.com/RicoSuter/NSwag/wiki/NSwag.MSBuild) NuGet package.
* The [Unchase OpenAPI (Swagger) Connected Service](https://marketplace.visualstudio.com/items?itemName=Unchase.unchaseopenapiconnectedservice): A Visual Studio Connected Service for generating API client code in C# or TypeScript. Also generates C# controllers for OpenAPI services with NSwag.

### Generate code with NSwagStudio

* Install NSwagStudio by following the instructions at the [NSwagStudio GitHub repository](https://github.com/RicoSuter/NSwag/wiki/NSwagStudio). On the NSwag release page you can download an xcopy version which can be started without installation and admin privileges.

* Launch NSwagStudio and enter the `swagger.json` file URL in the **Swagger Specification URL** text box. For example, `http://localhost:44354/swagger/v1/swagger.json`.

* Click the **Create local Copy** button to generate a JSON representation of your Swagger specification.

* In the **Outputs** area, click the **CSharp Client** checkbox. Depending on your project, you can also choose **TypeScript Client** or **CSharp Web API Controller**. If you select **CSharp Web API Controller**, a service specification rebuilds the service, serving as a reverse generation.

* Click **Generate Outputs** to produce a complete C# client implementation of the *TodoApi.NSwag* project. To see the generated client code, click the **CSharp Client** tab:

```
//----------------------
// <auto-generated>
//     Generated using the NSwag toolchain v12.0.9.0 (NJsonSchema v9.13.10.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------

namespace MyNamespace
{
    #pragma warning disable

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "12.0.9.0 (NJsonSchema v9.13.10.0 (Newtonsoft.Json v11.0.0.0))")]
    public partial class TodoClient
    {
        private string _baseUrl = "https://localhost:44354";
        private System.Net.Http.HttpClient _httpClient;
        private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;

        public TodoClient(System.Net.Http.HttpClient httpClient)
        {
            _httpClient = httpClient;
            _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
            {
                var settings = new Newtonsoft.Json.JsonSerializerSettings();
                UpdateJsonSerializerSettings(settings);
                return settings;
            });
        }

        public string BaseUrl
        {
            get { return _baseUrl; }
            set { _baseUrl = value; }
        }

        // code omitted for brevity
```

Tip

The C# client code is generated based on selections in the **Settings** tab. Modify the settings to perform tasks such as default namespace renaming and synchronous method generation.

* Copy the generated C# code into a file in the client project that will consume the API.
* Start consuming the web API:

```
 var todoClient = new TodoClient();

// Gets all to-dos from the API
 var allTodos = await todoClient.GetAllAsync();

 // Create a new TodoItem, and save it via the API.
var createdTodo = await todoClient.CreateAsync(new TodoItem());

// Get a single to-do by ID
var foundTodo = await todoClient.GetByIdAsync(1);
```

## Customize API documentation

Swagger provides options for documenting the object model to ease consumption of the web API.

### API info and description

In the `Startup.ConfigureServices` method, a configuration action passed to the `AddSwaggerDocument` method adds information such as the author, license, and description:

```
services.AddSwaggerDocument(config =>
{
    config.PostProcess = document =>
    {
        document.Info.Version = "v1";
        document.Info.Title = "ToDo API";
        document.Info.Description = "A simple ASP.NET Core web API";
        document.Info.TermsOfService = "None";
        document.Info.Contact = new NSwag.OpenApiContact
        {
            Name = "Shayne Boyer",
            Email = string.Empty,
            Url = "https://twitter.com/spboyer"
        };
        document.Info.License = new NSwag.OpenApiLicense
        {
            Name = "Use under LICX",
            Url = "https://example.com/license"
        };
    };
});
```

The Swagger UI displays the version's information:

### XML comments

To enable XML comments, perform the following steps:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)
* [.NET CLI](#tabpanel_4_net-cli)

- Right-click the project in **Solution Explorer** and select *`Edit <project_name>.csproj`*.
- Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

* From the *Solution Pad*, press **control** and click the project name. Navigate to **Tools** > **Edit File**.
* Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

Manually add the highlighted lines to the `.csproj` file:

```
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

### Data annotations

Because NSwag uses [Reflection](/en-us/dotnet/csharp/programming-guide/concepts/reflection), and the recommended return type for web API actions is [ActionResult\<T>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1), it can only infer the return type defined by `T`. You can't automatically infer other possible return types.

Consider the following example:

```
[HttpPost]
public ActionResult<TodoItem> Create(TodoItem item)
{
    _context.TodoItems.Add(item);
    _context.SaveChanges();

    return CreatedAtRoute("GetTodo", new { id = item.Id }, item);
}
```

The preceding action returns `ActionResult<T>`. Inside the action, it's returning [CreatedAtRoute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdatroute). Since the controller has the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute, a [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest) response is possible, too. For more information, see [Automatic HTTP 400 responses](../web-api/?view=aspnetcore-8.0#automatic-http-400-responses). Use data annotations to tell clients which HTTP status codes this action is known to return. Mark the action with the following attributes:

```
[ProducesResponseType(StatusCodes.Status201Created)]     // Created
[ProducesResponseType(StatusCodes.Status400BadRequest)]  // BadRequest
```

In ASP.NET Core 2.2 or later, you can use conventions instead of explicitly decorating individual actions with `[ProducesResponseType]`. For more information, see [Use web API conventions](../web-api/advanced/conventions?view=aspnetcore-8.0).

The Swagger generator can now accurately describe this action, and generated clients know what they receive when calling the endpoint. As a recommendation, mark all actions with these attributes.

For guidelines on what HTTP responses your API actions should return, see [RFC 9110: HTTP Semantics (Section 9.3. Method Definitions)](https://www.rfc-editor.org/rfc/rfc9110#name-method-definitions).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/choose-web-ui
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/choose-web-ui.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Choose an ASP.NET Core web UI

ASP.NET Core is a complete UI framework. Choose which functionalities to combine that fit the app's web UI needs.

For new project development, we recommend ASP.NET Core Blazor.

## ASP.NET Core Blazor

Blazor is a full-stack web UI framework and is recommended for most web UI scenarios.

Benefits of using Blazor:

* Reusable component model.
* Efficient diff-based component rendering.
* Flexibly render components from the server or client via WebAssembly.
* Build rich interactive web UI components in C#.
* Render components statically from the server.
* Progressively enhance server rendered components for smoother navigation and form handling and to enable streaming rendering.
* Share code for common logic on the client and server.
* Interop with JavaScript.
* Integrate components with existing MVC, Razor Pages, or JavaScript based apps.

For a complete overview of Blazor, its architecture and benefits, see [ASP.NET Core Blazor](../blazor/?view=aspnetcore-10.0) and [ASP.NET Core Blazor hosting models](../blazor/hosting-models?view=aspnetcore-10.0). To get started with your first Blazor app, see [Build your first Blazor app](https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/intro).

## ASP.NET Core Razor Pages

Razor Pages is a page-based model for building server rendered web UI. Razor pages UI are dynamically rendered on the server to generate the page's HTML and CSS in response to a browser request. The page arrives at the client ready to display. Support for Razor Pages is built on ASP.NET Core MVC.

Razor Pages benefits:

* Quickly build and update UI. Code for the page is kept with the page, while keeping UI and business logic concerns separate.

* Testable and scales to large apps.

* Keep your ASP.NET Core pages organized in a simpler way than ASP.NET MVC:

  * View specific logic and view models can be kept together in their own namespace and directory.
  * Groups of related pages can be kept in their own namespace and directory.

To get started with your first ASP.NET Core Razor Pages app, see [Tutorial: Get started with Razor Pages in ASP.NET Core](razor-pages/razor-pages-start?view=aspnetcore-10.0). For a complete overview of ASP.NET Core Razor Pages, its architecture and benefits, see: [Razor Pages architecture and concepts in ASP.NET Core](../razor-pages/?view=aspnetcore-10.0).

## ASP.NET Core MVC

ASP.NET Core MVC renders UI on the server and uses a Model-View-Controller (MVC) architectural pattern. The MVC pattern separates an app into three main groups of components: models, views, and controllers. User requests are routed to a controller. The controller is responsible for working with the model to perform user actions or retrieve results of queries. The controller chooses the view to display to the user and provides it with any model data it requires.

ASP.NET Core MVC benefits:

* Based on a scalable and mature model for building large web apps.
* Clear [separation of concerns](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#separation-of-concerns) for maximum flexibility.
* The Model-View-Controller separation of responsibilities ensures that the business model can evolve without being tightly coupled to low-level implementation details.

To get started with ASP.NET Core MVC, see [Get started with ASP.NET Core MVC](first-mvc-app/start-mvc?view=aspnetcore-10.0). For an overview of ASP.NET Core MVC's architecture and benefits, see [Overview of ASP.NET Core MVC](../mvc/overview?view=aspnetcore-10.0).

## ASP.NET Core Single Page Applications (SPA) with frontend JavaScript technologies

Build client-side logic for ASP.NET Core apps using popular JavaScript technologies, such as [Angular](https://angular.dev/), [React](https://react.dev/), and [Vue](https://vuejs.org/). ASP.NET Core provides project templates for Angular, React, and Vue, and it can be used with other JavaScript frameworks as well.

Benefits of ASP.NET Core SPA with JavaScript technologies, in addition to the client rendering benefits previously listed:

* The JavaScript runtime environment is already provided with the browser.
* Large community and mature ecosystem.
* Build client-side logic for ASP.NET Core apps using popular JavaScript technologies, such as Angular, React, and Vue.

Downsides:

* More coding languages, frameworks, and tools required.
* Difficult to share code so some logic may be duplicated.

To get started, see:

* [Create an ASP.NET Core app with Angular](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-angular)
* [Create an ASP.NET Core app with React](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-react)
* [Create an ASP.NET Core app with Vue](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-vue)
* [JavaScript and TypeScript in Visual Studio](/en-us/visualstudio/javascript/javascript-in-visual-studio)

## Combine multiple web UI solutions: ASP.NET Core MVC or Razor Pages plus Blazor

MVC, Razor Pages, and Blazor are part of the ASP.NET Core framework and are designed to be used together. Razor components can be integrated into Razor Pages and MVC apps. When a view or page is rendered, components can be prerendered at the same time.

Benefits for MVC or Razor Pages plus Blazor, in addition to MVC or Razor Pages benefits:

* Prerendering executes Razor components on the server and renders them into a view or page, which improves the perceived load time of the app.
* Add interactivity to existing views or pages with the [Component Tag Helper](../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

To get started with ASP.NET Core MVC or Razor Pages plus Blazor, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../blazor/components/integration?view=aspnetcore-10.0).

## Next steps

For more information, see:

* [ASP.NET Core Blazor](../blazor/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor hosting models](../blazor/hosting-models?view=aspnetcore-10.0)
* [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../blazor/components/integration?view=aspnetcore-10.0)
* [Compare gRPC services with HTTP APIs](../grpc/comparison?view=aspnetcore-10.0)

ASP.NET Core is a complete UI framework. Choose which functionalities to combine that fit the app's web UI needs.

## ASP.NET Core Blazor

Blazor is a full-stack web UI framework and is recommended for most web UI scenarios.

Benefits of using Blazor:

* Reusable component model.
* Efficient diff-based component rendering.
* Flexibly render components from the server or client via WebAssembly.
* Build rich interactive web UI components in C#.
* Render components statically from the server.
* Progressively enhance server rendered components for smoother navigation and form handling and to enable streaming rendering.
* Share code for common logic on the client and server.
* Interop with JavaScript.
* Integrate components with existing MVC, Razor Pages, or JavaScript based apps.

For a complete overview of Blazor, its architecture and benefits, see [ASP.NET Core Blazor](../blazor/?view=aspnetcore-10.0) and [ASP.NET Core Blazor hosting models](../blazor/hosting-models?view=aspnetcore-10.0). To get started with your first Blazor app, see [Build your first Blazor app](https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/intro).

## ASP.NET Core Razor Pages

Razor Pages is a page-based model for building server rendered web UI. Razor pages UI are dynamically rendered on the server to generate the page's HTML and CSS in response to a browser request. The page arrives at the client ready to display. Support for Razor Pages is built on ASP.NET Core MVC.

Razor Pages benefits:

* Quickly build and update UI. Code for the page is kept with the page, while keeping UI and business logic concerns separate.

* Testable and scales to large apps.

* Keep your ASP.NET Core pages organized in a simpler way than ASP.NET MVC:

  * View specific logic and view models can be kept together in their own namespace and directory.
  * Groups of related pages can be kept in their own namespace and directory.

To get started with your first ASP.NET Core Razor Pages app, see [Tutorial: Get started with Razor Pages in ASP.NET Core](razor-pages/razor-pages-start?view=aspnetcore-10.0). For a complete overview of ASP.NET Core Razor Pages, its architecture and benefits, see: [Razor Pages architecture and concepts in ASP.NET Core](../razor-pages/?view=aspnetcore-10.0).

## ASP.NET Core MVC

ASP.NET Core MVC renders UI on the server and uses a Model-View-Controller (MVC) architectural pattern. The MVC pattern separates an app into three main groups of components: models, views, and controllers. User requests are routed to a controller. The controller is responsible for working with the model to perform user actions or retrieve results of queries. The controller chooses the view to display to the user and provides it with any model data it requires.

ASP.NET Core MVC benefits:

* Based on a scalable and mature model for building large web apps.
* Clear [separation of concerns](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#separation-of-concerns) for maximum flexibility.
* The Model-View-Controller separation of responsibilities ensures that the business model can evolve without being tightly coupled to low-level implementation details.

To get started with ASP.NET Core MVC, see [Get started with ASP.NET Core MVC](first-mvc-app/start-mvc?view=aspnetcore-10.0). For an overview of ASP.NET Core MVC's architecture and benefits, see [Overview of ASP.NET Core MVC](../mvc/overview?view=aspnetcore-10.0).

## ASP.NET Core Single Page Applications (SPA) with frontend JavaScript technologies

Build client-side logic for ASP.NET Core apps using popular JavaScript technologies, such as [Angular](https://angular.dev/), [React](https://react.dev/), and [Vue](https://vuejs.org/). ASP.NET Core provides project templates for Angular, React, and Vue, and it can be used with other JavaScript frameworks as well.

Benefits of ASP.NET Core SPA with JavaScript technologies, in addition to the client rendering benefits previously listed:

* The JavaScript runtime environment is already provided with the browser.
* Large community and mature ecosystem.
* Build client-side logic for ASP.NET Core apps using popular JavaScript technologies, such as Angular, React, and Vue.

Downsides:

* More coding languages and tools are required.
* Difficult to share code so some logic may be duplicated.

To get started, see:

* [Create an ASP.NET Core app with Angular](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-angular)
* [Create an ASP.NET Core app with React](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-react)
* [Create an ASP.NET Core app with Vue](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-vue)
* [JavaScript and TypeScript in Visual Studio](/en-us/visualstudio/javascript/javascript-in-visual-studio)

## Choose a hybrid solution: ASP.NET Core MVC or Razor Pages plus Blazor

MVC, Razor Pages, and Blazor are part of the ASP.NET Core framework and are designed to be used together. Razor components can be integrated into Razor Pages and MVC apps. When a view or page is rendered, components can be prerendered at the same time.

Benefits for MVC or Razor Pages plus Blazor, in addition to MVC or Razor Pages benefits:

* Prerendering executes Razor components on the server and renders them into a view or page, which improves the perceived load time of the app.
* Add interactivity to existing views or pages with the [Component Tag Helper](../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

To get started with ASP.NET Core MVC or Razor Pages plus Blazor, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../blazor/components/integration?view=aspnetcore-10.0).

## Next steps

For more information, see:

* [ASP.NET Core Blazor](../blazor/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor hosting models](../blazor/hosting-models?view=aspnetcore-10.0)
* [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../blazor/components/integration?view=aspnetcore-10.0)
* [Compare gRPC services with HTTP APIs](../grpc/comparison?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-10-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/adding-model
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/adding-model.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 4, add a model to an ASP.NET Core MVC app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

In this tutorial, classes are added for managing movies in a database. These classes are the "**M**odel" part of the **M**VC app.

These model classes are used with [Entity Framework Core](/en-us/ef/core) (EF Core) to work with a database. EF Core is an object-relational mapping (ORM) framework that simplifies the data access code that you have to write.

The model classes created are known as ***POCO*** classes, from **P**lain **O**ld **C**LR **O**bjects. POCO classes don't have any dependency on EF Core. They only define the properties of the data to be stored in the database.

In this tutorial, model classes are created first, and EF Core creates the database.

## Add a data model class

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Right-click the *Models* folder > **Add** > **Class**. Name the file `Movie.cs`.

Add a file named `Movie.cs` to the *Models* folder.

Update the `Models/Movie.cs` file with the following code:

```
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}
```

The `Movie` class contains an `Id` field, which is required by the database for the primary key.

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute on `ReleaseDate` specifies the type of the data (`Date`). With this attribute:

* The user isn't required to enter time information in the date field.
* Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

## Add NuGet packages

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

Visual Studio automatically installs the required packages.

Build the project as a check for compiler errors.

Open a command window in the project directory. The project directory is the directory that contains the `Program.cs` and `.csproj` files.

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
```

## Scaffold movie pages

Use the scaffolding tool to produce `Create`, `Read`, `Update`, and `Delete` (CRUD) pages for the movie model.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

In **Solution Explorer**, right-click the *Controllers* folder and select **Add > New Scaffolded Item**.

In the **Add New Scaffolded Item** dialog:

* In the left pane, select **Installed** > **Common** > **MVC**.
* Select **MVC Controller with views, using Entity Framework**.
* Select **Add**.

Complete the **Add MVC Controller with views, using Entity Framework** dialog:

* In the **Model class** drop down, select **Movie (MvcMovie.Models)**.

* In the **Data context class** row, select the **+** (plus) sign.

  * In the **Add Data Context** dialog, the class name *MvcMovie.Data.MvcMovieContext* is generated.
  * Select **Add**.

* In the **Database provider** drop down, select **SQL Server**.

* **Views** and **Controller name**: Keep the default.

* Select **Add**.

If you get an error message, select **Add** a second time to try it again.

[]()

Scaffolding adds the following packages:

* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.EntityFrameworkCore.Tools`
* `Microsoft.VisualStudio.Web.CodeGeneration.Design`

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

Scaffolding updates the following:

* Inserts required package references in the `MvcMovie.csproj` project file.
* Registers the database context in the `Program.cs` file.
* Adds a database connection string to the `appsettings.json` file.

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

Build the app to verify that there are no errors.

Open a command window in the project directory. The project directory is the directory that contains the `Program.cs` and `.csproj` files.

On macOS and Linux, export the scaffold tool path:

```
export PATH=$HOME/.dotnet/tools:$PATH
```

Run the following command:

```
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovie.Data.MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries --databaseProvider sqlite
```

The following table details the ASP.NET Core code generator parameters.

| Parameter                  | Description                                                  |
| -------------------------- | ------------------------------------------------------------ |
| -m                         | The name of the model.                                       |
| -dc                        | The data context.                                            |
| --relativeFolderPath       | The relative output folder path to create the files.         |
| --useDefaultLayout\|-udl   | The default layout should be used for the views.             |
| --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.   |
| --databaseProvider sqlite  | Specify `DbContext` should use SQLite instead of SQL Server. |

Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

```
dotnet aspnet-codegenerator controller -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

[]()

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

Scaffolding updates the following:

* Registers the database context in the `Program.cs` file
* Adds a database connection string to the `appsettings.json` file.

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

Build the app to verify that there are no errors.

[]()

### Use SQLite for development, SQL Server for production

The following highlighted code in `Program.cs` shows how to use SQLite in development and SQL Server in production.

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
}
else
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMvcMovieContext")));
}
```

[]()

## Initial migration

Use the EF Core [Migrations](../../data/ef-mvc/migrations?view=aspnetcore-10.0) feature to create the database. *Migrations* is a set of tools that create and update a database to match the data model.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** .

In the Package Manager Console (PMC), enter the following command:

```
Add-Migration InitialCreate
```

* `Add-Migration InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. Because this is the first migration, the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class.

The following warning is displayed, which is addressed in a later step:

> No store type was specified for the decimal property 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating' using 'HasColumnType', specify precision and scale using 'HasPrecision', or configure a value converter using 'HasConversion'.

In the PMC, enter the following command:

```
Update-Database
```

* `Update-Database`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

For more information on the PMC tools for EF Core, see [EF Core tools reference - PMC in Visual Studio](/en-us/ef/core/miscellaneous/cli/powershell).

Run the following .NET CLI command:

```
dotnet ef migrations add InitialCreate
```

* `ef migrations add InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. This is the first migration, so the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class, in the `Data/MvcMovieContext.cs` file.

The following warning is displayed, which is addressed in a later step:

> No store type was specified for the decimal property 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating' using 'HasColumnType', specify precision and scale using 'HasPrecision', or configure a value converter using 'HasConversion'.

Run the following .NET CLI command:

```
dotnet ef database update
```

* `ef database update`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

For more information on maintaining multiple providers such as Microsoft SQL Server and SQLite, see [Migrations with Multiple Providers](/en-us/ef/core/managing-schemas/migrations/providers).

[]()

## Test the app

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the `Update-Database` command in the [migrations step](#migration):

```
SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.
```

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the `dotnet ef database update` command in the [migrations step](#migration):

```
SqliteException: SQLite Error 1: 'no such table: Movie'.
```

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326510420).

[]()

### Examine the generated database context class and registration

With EF Core, data access is performed using a model. A model is made up of entity classes and a context object that represents a session with the database. The context object allows querying and saving data. The database context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) and specifies the entities to include in the data model.

Scaffolding creates the `Data/MvcMovieContext.cs` database context class:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<MvcMovie.Models.Movie> Movie { get; set; } = default!;
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property that represents the movies in the database.

### Dependency injection

ASP.NET Core is built with [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the database context, are registered with DI in `Program.cs`. These services are provided to components that require them via constructor parameters.

In the `Controllers/MoviesController.cs` file, the constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the `MvcMovieContext` database context into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Scaffolding generated the following highlighted code in `Program.cs`:

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext") ?? throw new InvalidOperationException("Connection string 'MvcMovieContext' not found.")));
```

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the "MvcMovieContext" database connection string.

[]()

### Examine the generated database connection string

Scaffolding added a connection string to the `appsettings.json` file:

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-4ebefa10-de29-4dea-b2ad-8a8dc6bcf374;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovie.db"
  }
}
```

For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the `ConnectionString` key from the `appsettings.json` file.

### The `InitialCreate` class

Examine the `Migrations/{timestamp}_InitialCreate.cs` migration file:

```
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
    /// <inheritdoc />
    public partial class InitialCreate : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Movie",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Movie", x => x.Id);
                });
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Movie");
        }
    }
}
```

In the preceding code:

* `InitialCreate.Up` creates the Movie table and configures `Id` as the primary key.
* `InitialCreate.Down` reverts the schema changes made by the `Up` migration.

## Dependency injection in the controller

Open the `Controllers/MoviesController.cs` file and examine the constructor:

```
public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }
```

The constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the database context (`MvcMovieContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Test the **Create** page. Enter and submit data.

Test the **Edit**, **Details**, and **Delete** pages.

[]()

## Strongly typed models and the `@model` directive

Earlier in this tutorial, you saw how a controller can pass data or objects to a view using the `ViewData` dictionary. The `ViewData` dictionary is a dynamic object that provides a convenient late-bound way to pass information to a view.

MVC provides the ability to pass strongly typed model objects to a view. This strongly typed approach enables compile time code checking. The scaffolding mechanism passed a strongly typed model in the `MoviesController` class and views.

Examine the generated `Details` method in the `Controllers/MoviesController.cs` file:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The `id` parameter is generally passed as route data. For example, `https://localhost:{PORT}/movies/details/1` sets:

* The controller to the `movies` controller, the first URL segment.
* The action to `details`, the second URL segment.
* The `id` to 1, the last URL segment.

The `id` can be passed in with a query string, as in the following example:

`https://localhost:{PORT}/movies/details?id=1`

The `id` parameter is defined as a [nullable type](/en-us/dotnet/csharp/programming-guide/nullable-types/index) (`int?`) in cases when the `id` value isn't provided.

A [lambda expression](/en-us/dotnet/articles/csharp/programming-guide/statements-expressions-operators/lambda-expressions) is passed in to the [FirstOrDefaultAsync](/en-us/dotnet/api/system.data.entity.queryableextensions.firstordefaultasync) method to select movie entities that match the route data or query string value.

```
var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);
```

If a movie is found, an instance of the `Movie` model is passed to the `Details` view:

```
return View(movie);
```

Examine the contents of the `Views/Movies/Details.cshtml` file:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>
```

The `@model` statement at the top of the view file specifies the type of object that the view expects. When the movie controller was created, the following `@model` statement was included:

```
@model MvcMovie.Models.Movie
```

This `@model` directive allows access to the movie that the controller passed to the view. The `Model` object is strongly typed. For example, in the `Details.cshtml` view, the code passes each movie field to the `DisplayNameFor` and `DisplayFor` HTML Helpers with the strongly typed `Model` object. The `Create` and `Edit` methods and views also pass a `Movie` model object.

Examine the `Index.cshtml` view and the `Index` method in the Movies controller. Notice how the code creates a `List` object when it calls the `View` method. The code passes this `Movies` list from the `Index` action method to the view:

```
// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}
```

The code returns [problem details](../../fundamentals/error-handling-api?view=aspnetcore-10.0#problem-details-service) if the `Movie` property of the data context is null.

When the movies controller was created, scaffolding included the following `@model` statement at the top of the `Index.cshtml` file:

```
@model IEnumerable<MvcMovie.Models.Movie>
```

The `@model` directive allows access to the list of movies that the controller passed to the view by using a `Model` object that's strongly typed. For example, in the `Index.cshtml` view, the code loops through the movies with a `foreach` statement over the strongly typed `Model` object:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Because the `Model` object is strongly typed as an `IEnumerable<Movie>` object, each item in the loop is typed as `Movie`. Among other benefits, the compiler validates the types used in the code.

## Related content

* [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s)
* [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)

[Previous: Adding a View](adding-view?view=aspnetcore-10.0) [Next: Working with SQL](working-with-sql?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. These classes are the "**M**odel" part of the **M**VC app.

These model classes are used with [Entity Framework Core](/en-us/ef/core) (EF Core) to work with a database. EF Core is an object-relational mapping (ORM) framework that simplifies the data access code that you have to write.

The model classes created are known as ***POCO*** classes, from **P**lain **O**ld **C**LR **O**bjects. POCO classes don't have any dependency on EF Core. They only define the properties of the data to be stored in the database.

In this tutorial, model classes are created first, and EF Core creates the database.

## Add a data model class

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Right-click the *Models* folder > **Add** > **Class**. Name the file `Movie.cs`.

Add a file named `Movie.cs` to the *Models* folder.

Update the `Models/Movie.cs` file with the following code:

```
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}
```

The `Movie` class contains an `Id` field, which is required by the database for the primary key.

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute on `ReleaseDate` specifies the type of the data (`Date`). With this attribute:

* The user isn't required to enter time information in the date field.
* Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

## Add NuGet packages

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

Visual Studio automatically installs the required packages.

Build the project as a check for compiler errors.

Open a command window in the project directory. The project directory is the directory that contains the `Program.cs` and `.csproj` files.

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
```

## Scaffold movie pages

Use the scaffolding tool to produce `Create`, `Read`, `Update`, and `Delete` (CRUD) pages for the movie model.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

In **Solution Explorer**, right-click the *Controllers* folder and select **Add > New Scaffolded Item**.

In the **Add New Scaffolded Item** dialog:

* In the left pane, select **Installed** > **Common** > **MVC**.
* Select **MVC Controller with views, using Entity Framework**.
* Select **Add**.

Complete the **Add MVC Controller with views, using Entity Framework** dialog:

* In the **Model class** drop down, select **Movie (MvcMovie.Models)**.

* In the **Data context class** row, select the **+** (plus) sign.

  * In the **Add Data Context** dialog, the class name *MvcMovie.Data.MvcMovieContext* is generated.
  * Select **Add**.

* In the **Database provider** drop down, select **SQL Server**.

* **Views** and **Controller name**: Keep the default.

* Select **Add**.

If you get an error message, select **Add** a second time to try it again.

[]()

Scaffolding adds the following packages:

* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.EntityFrameworkCore.Tools`
* `Microsoft.VisualStudio.Web.CodeGeneration.Design`

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

Scaffolding updates the following:

* Inserts required package references in the `MvcMovie.csproj` project file.
* Registers the database context in the `Program.cs` file.
* Adds a database connection string to the `appsettings.json` file.

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

Build the app to verify that there are no errors.

Open a command window in the project directory. The project directory is the directory that contains the `Program.cs` and `.csproj` files.

On macOS and Linux, export the scaffold tool path:

```
export PATH=$HOME/.dotnet/tools:$PATH
```

Run the following command:

```
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovie.Data.MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries --databaseProvider sqlite
```

The following table details the ASP.NET Core code generator parameters.

| Parameter                  | Description                                                  |
| -------------------------- | ------------------------------------------------------------ |
| -m                         | The name of the model.                                       |
| -dc                        | The data context.                                            |
| --relativeFolderPath       | The relative output folder path to create the files.         |
| --useDefaultLayout\|-udl   | The default layout should be used for the views.             |
| --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.   |
| --databaseProvider sqlite  | Specify `DbContext` should use SQLite instead of SQL Server. |

Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

```
dotnet aspnet-codegenerator controller -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

[]()

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

Scaffolding updates the following:

* Registers the database context in the `Program.cs` file
* Adds a database connection string to the `appsettings.json` file.

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

Build the app to verify that there are no errors.

[]()

### Use SQLite for development, SQL Server for production

The following highlighted code in `Program.cs` shows how to use SQLite in development and SQL Server in production.

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
}
else
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMvcMovieContext")));
}
```

[]()

## Initial migration

Use the EF Core [Migrations](../../data/ef-mvc/migrations?view=aspnetcore-10.0) feature to create the database. *Migrations* is a set of tools that create and update a database to match the data model.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** .

In the Package Manager Console (PMC), enter the following command:

```
Add-Migration InitialCreate
```

* `Add-Migration InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. Because this is the first migration, the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class.

The following warning is displayed, which is addressed in a later step:

> No store type was specified for the decimal property 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating' using 'HasColumnType', specify precision and scale using 'HasPrecision', or configure a value converter using 'HasConversion'.

In the PMC, enter the following command:

```
Update-Database
```

* `Update-Database`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

For more information on the PMC tools for EF Core, see [EF Core tools reference - PMC in Visual Studio](/en-us/ef/core/miscellaneous/cli/powershell).

Run the following .NET CLI command:

```
dotnet ef migrations add InitialCreate
```

* `ef migrations add InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. This is the first migration, so the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class, in the `Data/MvcMovieContext.cs` file.

The following warning is displayed, which is addressed in a later step:

> No store type was specified for the decimal property 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating' using 'HasColumnType', specify precision and scale using 'HasPrecision', or configure a value converter using 'HasConversion'.

Run the following .NET CLI command:

```
dotnet ef database update
```

* `ef database update`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

For more information on maintaining multiple providers such as Microsoft SQL Server and SQLite, see [Migrations with Multiple Providers](/en-us/ef/core/managing-schemas/migrations/providers).

[]()

## Test the app

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the `Update-Database` command in the [migrations step](#migration):

```
SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.
```

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the `dotnet ef database update` command in the [migrations step](#migration):

```
SqliteException: SQLite Error 1: 'no such table: Movie'.
```

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

[]()

### Examine the generated database context class and registration

With EF Core, data access is performed using a model. A model is made up of entity classes and a context object that represents a session with the database. The context object allows querying and saving data. The database context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) and specifies the entities to include in the data model.

Scaffolding creates the `Data/MvcMovieContext.cs` database context class:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<MvcMovie.Models.Movie> Movie { get; set; } = default!;
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property that represents the movies in the database.

### Dependency injection

ASP.NET Core is built with [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the database context, are registered with DI in `Program.cs`. These services are provided to components that require them via constructor parameters.

In the `Controllers/MoviesController.cs` file, the constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the `MvcMovieContext` database context into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Scaffolding generated the following highlighted code in `Program.cs`:

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext") ?? throw new InvalidOperationException("Connection string 'MvcMovieContext' not found.")));
```

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the "MvcMovieContext" database connection string.

[]()

### Examine the generated database connection string

Scaffolding added a connection string to the `appsettings.json` file:

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-4ebefa10-de29-4dea-b2ad-8a8dc6bcf374;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovie.db"
  }
}
```

For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the `ConnectionString` key from the `appsettings.json` file.

### The `InitialCreate` class

Examine the `Migrations/{timestamp}_InitialCreate.cs` migration file:

```
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
    /// <inheritdoc />
    public partial class InitialCreate : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Movie",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Movie", x => x.Id);
                });
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Movie");
        }
    }
}
```

In the preceding code:

* `InitialCreate.Up` creates the Movie table and configures `Id` as the primary key.
* `InitialCreate.Down` reverts the schema changes made by the `Up` migration.

## Dependency injection in the controller

Open the `Controllers/MoviesController.cs` file and examine the constructor:

```
public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }
```

The constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the database context (`MvcMovieContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Test the **Create** page. Enter and submit data.

Test the **Edit**, **Details**, and **Delete** pages.

[]()

## Strongly typed models and the `@model` directive

Earlier in this tutorial, you saw how a controller can pass data or objects to a view using the `ViewData` dictionary. The `ViewData` dictionary is a dynamic object that provides a convenient late-bound way to pass information to a view.

MVC provides the ability to pass strongly typed model objects to a view. This strongly typed approach enables compile time code checking. The scaffolding mechanism passed a strongly typed model in the `MoviesController` class and views.

Examine the generated `Details` method in the `Controllers/MoviesController.cs` file:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The `id` parameter is generally passed as route data. For example, `https://localhost:{PORT}/movies/details/1` sets:

* The controller to the `movies` controller, the first URL segment.
* The action to `details`, the second URL segment.
* The `id` to 1, the last URL segment.

The `id` can be passed in with a query string, as in the following example:

`https://localhost:{PORT}/movies/details?id=1`

The `id` parameter is defined as a [nullable type](/en-us/dotnet/csharp/programming-guide/nullable-types/index) (`int?`) in cases when the `id` value isn't provided.

A [lambda expression](/en-us/dotnet/articles/csharp/programming-guide/statements-expressions-operators/lambda-expressions) is passed in to the [FirstOrDefaultAsync](/en-us/dotnet/api/system.data.entity.queryableextensions.firstordefaultasync) method to select movie entities that match the route data or query string value.

```
var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);
```

If a movie is found, an instance of the `Movie` model is passed to the `Details` view:

```
return View(movie);
```

Examine the contents of the `Views/Movies/Details.cshtml` file:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>
```

The `@model` statement at the top of the view file specifies the type of object that the view expects. When the movie controller was created, the following `@model` statement was included:

```
@model MvcMovie.Models.Movie
```

This `@model` directive allows access to the movie that the controller passed to the view. The `Model` object is strongly typed. For example, in the `Details.cshtml` view, the code passes each movie field to the `DisplayNameFor` and `DisplayFor` HTML Helpers with the strongly typed `Model` object. The `Create` and `Edit` methods and views also pass a `Movie` model object.

Examine the `Index.cshtml` view and the `Index` method in the Movies controller. Notice how the code creates a `List` object when it calls the `View` method. The code passes this `Movies` list from the `Index` action method to the view:

```
// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}
```

The code returns [problem details](../../fundamentals/error-handling-api?view=aspnetcore-10.0#problem-details-service) if the `Movie` property of the data context is null.

When the movies controller was created, scaffolding included the following `@model` statement at the top of the `Index.cshtml` file:

```
@model IEnumerable<MvcMovie.Models.Movie>
```

The `@model` directive allows access to the list of movies that the controller passed to the view by using a `Model` object that's strongly typed. For example, in the `Index.cshtml` view, the code loops through the movies with a `foreach` statement over the strongly typed `Model` object:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Because the `Model` object is strongly typed as an `IEnumerable<Movie>` object, each item in the loop is typed as `Movie`. Among other benefits, the compiler validates the types used in the code.

## Related content

* [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s)
* [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)

[Previous: Adding a View](adding-view?view=aspnetcore-10.0) [Next: Working with SQL](working-with-sql?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. These classes are the "**M**odel" part of the **M**VC app.

These model classes are used with [Entity Framework Core](/en-us/ef/core) (EF Core) to work with a database. EF Core is an object-relational mapping (ORM) framework that simplifies the data access code that you have to write.

The model classes created are known as ***POCO*** classes, from **P**lain **O**ld **C**LR **O**bjects. POCO classes don't have any dependency on EF Core. They only define the properties of the data to be stored in the database.

In this tutorial, model classes are created first, and EF Core creates the database.

## Add a data model class

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Right-click the *Models* folder > **Add** > **Class**. Name the file `Movie.cs`.

Add a file named `Movie.cs` to the *Models* folder.

Update the `Models/Movie.cs` file with the following code:

```
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}
```

The `Movie` class contains an `Id` field, which is required by the database for the primary key.

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute on `ReleaseDate` specifies the type of the data (`Date`). With this attribute:

* The user isn't required to enter time information in the date field.
* Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

## Add NuGet packages

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

Visual Studio automatically installs the required packages.

Build the project as a check for compiler errors.

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
```

## Scaffold movie pages

Use the scaffolding tool to produce `Create`, `Read`, `Update`, and `Delete` (CRUD) pages for the movie model.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

In **Solution Explorer**, right-click the *Controllers* folder and select **Add > New Scaffolded Item**.

In the **Add New Scaffolded Item** dialog:

* In the left pane, select **Installed** > **Common** > **MVC**.
* Select **MVC Controller with views, using Entity Framework**.
* Select **Add**.

Complete the **Add MVC Controller with views, using Entity Framework** dialog:

* In the **Model class** drop down, select **Movie (MvcMovie.Models)**.

* In the **Data context class** row, select the **+** (plus) sign.

  * In the **Add Data Context** dialog, the class name *MvcMovie.Data.MvcMovieContext* is generated.
  * Select **Add**.

* In the **Database provider** drop down, select **SQL Server**.

* **Views** and **Controller name**: Keep the default.

* Select **Add**.

If you get an error message, select **Add** a second time to try it again.

[]()

Scaffolding adds the following packages:

* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.EntityFrameworkCore.Tools`
* `Microsoft.VisualStudio.Web.CodeGeneration.Design`

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

Scaffolding updates the following:

* Inserts required package references in the `MvcMovie.csproj` project file.
* Registers the database context in the `Program.cs` file.
* Adds a database connection string to the `appsettings.json` file.

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

Build the app to verify that there are no errors.

Open a command window in the project directory. The project directory is the directory that contains the `Program.cs` and `.csproj` files.

[]()

### Use SQLite for development, SQL Server for production

The following highlighted code in `Program.cs` shows how to use SQLite in development and SQL Server in production.

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
}
else
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMvcMovieContext")));
}
```

On macOS and Linux, export the scaffold tool path:

```
export PATH=$HOME/.dotnet/tools:$PATH
```

Run the following command:

```
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovie.Data.MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries --databaseProvider sqlite
```

The following table details the ASP.NET Core code generator parameters.

| Parameter                  | Description                                                  |
| -------------------------- | ------------------------------------------------------------ |
| -m                         | The name of the model.                                       |
| -dc                        | The data context.                                            |
| --relativeFolderPath       | The relative output folder path to create the files.         |
| --useDefaultLayout\|-udl   | The default layout should be used for the views.             |
| --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.   |
| --databaseProvider sqlite  | Specify `DbContext` should use SQLite instead of SQL Server. |

Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

```
dotnet aspnet-codegenerator controller -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

[]()

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

Scaffolding updates the following:

* Registers the database context in the `Program.cs` file.
* Adds a database connection string to the `appsettings.json` file.

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

Build the app to verify that there are no errors.

[]()

## Initial migration

Use the EF Core [Migrations](../../data/ef-mvc/migrations?view=aspnetcore-10.0) feature to create the database. *Migrations* is a set of tools that create and update a database to match the data model.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** .

In the Package Manager Console (PMC), enter the following commands:

```
Add-Migration InitialCreate
Update-Database
```

* `Add-Migration InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. Because this is the first migration, the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class.

* `Update-Database`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

The `Update-Database` command generates the following warning:

> No store type was specified for the decimal property 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating' using 'HasColumnType', specify precision and scale using 'HasPrecision', or configure a value converter using 'HasConversion'.

Ignore the preceding warning, it's fixed in a later tutorial.

For more information on the PMC tools for EF Core, see [EF Core tools reference - PMC in Visual Studio](/en-us/ef/core/miscellaneous/cli/powershell).

Run the following .NET CLI commands:

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

* `ef migrations add InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. This is the first migration, so the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class, in the `Data/MvcMovieContext.cs` file.
* `ef database update`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

For more information on maintaining multiple providers such as Microsoft SQL Server and SQLite, see [Migrations with Multiple Providers](/en-us/ef/core/managing-schemas/migrations/providers).

[]()

## Test the app

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the `Update-Database` command in the [migrations step](#migration):

```
SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.
```

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the `dotnet ef database update` command in the [migrations step](#migration):

```
SqliteException: SQLite Error 1: 'no such table: Movie'.
```

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

[]()

### Examine the generated database context class and registration

With EF Core, data access is performed using a model. A model is made up of entity classes and a context object that represents a session with the database. The context object allows querying and saving data. The database context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) and specifies the entities to include in the data model.

Scaffolding creates the `Data/MvcMovieContext.cs` database context class:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<MvcMovie.Models.Movie> Movie { get; set; }
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property that represents the movies in the database.

### Dependency injection

ASP.NET Core is built with [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the database context, are registered with DI in `Program.cs`. These services are provided to components that require them via constructor parameters.

In the `Controllers/MoviesController.cs` file, the constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the `MvcMovieContext` database context into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Scaffolding generated the following highlighted code in `Program.cs`:

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the "MvcMovieContext" database connection string.

[]()

### Examine the generated database connection string

Scaffolding added a connection string to the `appsettings.json` file:

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovieContext-ea7a4069-f366-4742-bd1c-3f753a804ce1.db"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovie.db"
  }
}
```

For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the `ConnectionString` key from the `appsettings.json` file.

### The `InitialCreate` class

Examine the `Migrations/{timestamp}_InitialCreate.cs` migration file:

```
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Movie",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Movie", x => x.Id);
                });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Movie");
        }
    }
}
```

In the preceding code:

* `InitialCreate.Up` creates the Movie table and configures `Id` as the primary key.
* `InitialCreate.Down` reverts the schema changes made by the `Up` migration.

## Dependency injection in the controller

Open the `Controllers/MoviesController.cs` file and examine the constructor:

```
public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }
```

The constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the database context (`MvcMovieContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Test the **Create** page. Enter and submit data.

Test the **Edit**, **Details**, and **Delete** pages.

[]()

## Strongly typed models and the `@model` directive

Earlier in this tutorial, you saw how a controller can pass data or objects to a view using the `ViewData` dictionary. The `ViewData` dictionary is a dynamic object that provides a convenient late-bound way to pass information to a view.

MVC provides the ability to pass strongly typed model objects to a view. This strongly typed approach enables compile time code checking. The scaffolding mechanism passed a strongly typed model in the `MoviesController` class and views.

Examine the generated `Details` method in the `Controllers/MoviesController.cs` file:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The `id` parameter is generally passed as route data. For example, `https://localhost:5001/movies/details/1` sets:

* The controller to the `movies` controller, the first URL segment.
* The action to `details`, the second URL segment.
* The `id` to 1, the last URL segment.

The `id` can be passed in with a query string, as in the following example:

`https://localhost:5001/movies/details?id=1`

The `id` parameter is defined as a [nullable type](/en-us/dotnet/csharp/programming-guide/nullable-types/index) (`int?`) in cases when the `id` value isn't provided.

A [lambda expression](/en-us/dotnet/articles/csharp/programming-guide/statements-expressions-operators/lambda-expressions) is passed in to the [FirstOrDefaultAsync](/en-us/dotnet/api/system.data.entity.queryableextensions.firstordefaultasync) method to select movie entities that match the route data or query string value.

```
var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);
```

If a movie is found, an instance of the `Movie` model is passed to the `Details` view:

```
return View(movie);
```

Examine the contents of the `Views/Movies/Details.cshtml` file:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>
```

The `@model` statement at the top of the view file specifies the type of object that the view expects. When the movie controller was created, the following `@model` statement was included:

```
@model MvcMovie.Models.Movie
```

This `@model` directive allows access to the movie that the controller passed to the view. The `Model` object is strongly typed. For example, in the `Details.cshtml` view, the code passes each movie field to the `DisplayNameFor` and `DisplayFor` HTML Helpers with the strongly typed `Model` object. The `Create` and `Edit` methods and views also pass a `Movie` model object.

Examine the `Index.cshtml` view and the `Index` method in the Movies controller. Notice how the code creates a `List` object when it calls the `View` method. The code passes this `Movies` list from the `Index` action method to the view:

```
// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}
```

The code returns [problem details](../../fundamentals/error-handling-api?view=aspnetcore-10.0#problem-details-service) if the `Movie` property of the data context is null.

When the movies controller was created, scaffolding included the following `@model` statement at the top of the `Index.cshtml` file:

```
@model IEnumerable<MvcMovie.Models.Movie>
```

The `@model` directive allows access to the list of movies that the controller passed to the view by using a `Model` object that's strongly typed. For example, in the `Index.cshtml` view, the code loops through the movies with a `foreach` statement over the strongly typed `Model` object:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Because the `Model` object is strongly typed as an `IEnumerable<Movie>` object, each item in the loop is typed as `Movie`. Among other benefits, the compiler validates the types used in the code.

## Related content

* [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s)
* [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)

[Previous: Adding a View](adding-view?view=aspnetcore-10.0) [Next: Working with SQL](working-with-sql?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. These classes are the "**M**odel" part of the **M**VC app.

These model classes are used with [Entity Framework Core](/en-us/ef/core) (EF Core) to work with a database. EF Core is an object-relational mapping (ORM) framework that simplifies the data access code that you have to write.

The model classes created are known as ***POCO*** classes, from **P**lain **O**ld **C**LR **O**bjects. POCO classes don't have any dependency on EF Core. They only define the properties of the data to be stored in the database.

In this tutorial, model classes are created first, and EF Core creates the database.

## Add a data model class

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Right-click the *Models* folder > **Add** > **Class**. Name the file `Movie.cs`.

Add a file named `Movie.cs` to the *Models* folder.

Control-click the **Models** folder and select **Add** > **New Class** > **Empty Class**. Name the file `Movie.cs`.

Update the `Models/Movie.cs` file with the following code:

```
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}
```

The `Movie` class contains an `Id` field, which is required by the database for the primary key.

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute on `ReleaseDate` specifies the type of the data (`Date`). With this attribute:

* The user isn't required to enter time information in the date field.
* Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

## Add NuGet packages

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

Visual Studio automatically installs the required packages.

Build the project as a check for compiler errors.

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
```

From the **Project** menu, select **Manage NuGet Packages**.

In the **Search** field in the upper right:

* Enter `Microsoft.EntityFrameworkCore.SQLite`.
* Press the `Return` key to search.
* Select the matching NuGet package and select the **Add Package** button.

The **Select Projects** dialog displays, with the `MvcMovie` project selected. Select the **Ok** button.

A **License Acceptance** dialog displays. Review the licenses, then select the **Accept** button.

Repeat the above steps to install the following NuGet packages:

* `Microsoft.VisualStudio.Web.CodeGeneration.Design`
* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.EntityFrameworkCore.Design`

The preceding packages include:

* The EF Core SQLite provider, which installs the EF Core package as a dependency.
* Packages needed for scaffolding: `Microsoft.VisualStudio.Web.CodeGeneration.Design` and `Microsoft.EntityFrameworkCore.SqlServer`.
* Design time tools for EF Core.

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

The preceding commands add:

* The [command-line interface (CLI) tools for EF Core](/en-us/ef/core/miscellaneous/cli/dotnet)
* The [aspnet-codegenerator scaffolding tool](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

Build the project as a check for compiler errors.

## Scaffold movie pages

Use the scaffolding tool to produce `Create`, `Read`, `Update`, and `Delete` (CRUD) pages for the movie model.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

In **Solution Explorer**, right-click the *Controllers* folder and select **Add > New Scaffolded Item**.

In the **Add New Scaffolded Item** dialog:

* In the left pane, select **Installed** > **Common** > **MVC**.
* Select **MVC Controller with views, using Entity Framework**.
* Select **Add**.

Complete the **Add MVC Controller with views, using Entity Framework** dialog:

* In the **Model class** drop down, select **Movie (MvcMovie.Models)**.

* In the **Data context class** row, select the **+** (plus) sign.

  * In the **Add Data Context** dialog, the class name *MvcMovie.Data.MvcMovieContext* is generated.
  * Select **Add**.

* In the **Database provider** drop down, select **SQL Server**.

* **Views** and **Controller name**: Keep the default.

* Select **Add**.

If you get an error message, select **Add** a second time to try it again.

[]()

Scaffolding adds the following packages:

* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.EntityFrameworkCore.Tools`
* `Microsoft.VisualStudio.Web.CodeGeneration.Design`

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

Scaffolding updates the following:

* Inserts required package references in the `MvcMovie.csproj` project file.
* Registers the database context in the `Program.cs` file.
* Adds a database connection string to the `appsettings.json` file.

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

Build the app to verify that there are no errors.

Open a command window in the project directory. The project directory is the directory that contains the `Program.cs` and `.csproj` files.

On macOS and Linux, export the scaffold tool path:

```
export PATH=$HOME/.dotnet/tools:$PATH
```

Run the following command:

```
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovie.Data.MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries --databaseProvider sqlite
```

The following table details the ASP.NET Core code generator parameters.

| Parameter                  | Description                                                  |
| -------------------------- | ------------------------------------------------------------ |
| -m                         | The name of the model.                                       |
| -dc                        | The data context.                                            |
| --relativeFolderPath       | The relative output folder path to create the files.         |
| --useDefaultLayout\|-udl   | The default layout should be used for the views.             |
| --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.   |
| --databaseProvider sqlite  | Specify `DbContext` should use SQLite instead of SQL Server. |

Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

```
dotnet aspnet-codegenerator controller -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

[]()

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

Scaffolding updates the following:

* Registers the database context in the `Program.cs` file
* Adds a database connection string to the `appsettings.json` file.

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

Build the app to verify that there are no errors.

[]()

### Use SQLite for development, SQL Server for production

The following highlighted code in `Program.cs` shows how to use SQLite in development and SQL Server in production.

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
}
else
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMvcMovieContext")));
}
```

Open a command window in the project directory. The project directory is the directory that contains the `.csproj` file.

Export the scaffold tool path:

```
export PATH=$HOME/.dotnet/tools:$PATH
```

Run the following command:

```
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovie.Data.MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries -sqlite
```

The following table details the ASP.NET Core code generator parameters.

| Parameter                  | Description                                                             |
| -------------------------- | ----------------------------------------------------------------------- |
| -m                         | The name of the model.                                                  |
| -dc                        | The data context.                                                       |
| --relativeFolderPath       | The relative output folder path to create the files.                    |
| --useDefaultLayout\|-udl   | The default layout should be used for the views.                        |
| --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.              |
| -sqlite                    | Flag to specify if `DbContext` should use SQLite instead of SQL Server. |

Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

```
dotnet aspnet-codegenerator controller -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

Scaffolding updates the following:

* Registers the database context in the `Program.cs` file
* Adds a database connection string to the `appsettings.json` file.

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

Build the app to verify that there are no errors.

[]()

### Use SQLite for development, SQL Server for production

The following highlighted code in `Program.cs` shows how to use SQLite in development and SQL Server in production.

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
}
else
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMvcMovieContext")));
}
```

[]()

[]()

## Initial migration

Use the EF Core [Migrations](../../data/ef-mvc/migrations?view=aspnetcore-10.0) feature to create the database. *Migrations* is a set of tools that create and update a database to match the data model.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** .

In the Package Manager Console (PMC), enter the following commands:

```
Add-Migration InitialCreate
Update-Database
```

* `Add-Migration InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. Because this is the first migration, the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class.

* `Update-Database`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

The `Update-Database` command generates the following warning:

> No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

Ignore the preceding warning, it's fixed in a later tutorial.

For more information on the PMC tools for EF Core, see [EF Core tools reference - PMC in Visual Studio](/en-us/ef/core/miscellaneous/cli/powershell).

Run the following .NET CLI commands:

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

* `ef migrations add InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. This is the first migration, so the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class, in the `Data/MvcMovieContext.cs` file.
* `ef database update`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

For more information on maintaining multiple providers such as Microsoft SQL Server and SQLite, see [Migrations with Multiple Providers](/en-us/ef/core/managing-schemas/migrations/providers).

[]()

## Test the app

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_5_visual-studio-code+visual-studio-mac)

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the `Update-Database` command in the [migrations step](#migration):

```
SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.
```

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the `dotnet ef database update` command in the [migrations step](#migration):

```
SqliteException: SQLite Error 1: 'no such table: Movie'.
```

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

[]()

### Examine the generated database context class and registration

With EF Core, data access is performed using a model. A model is made up of entity classes and a context object that represents a session with the database. The context object allows querying and saving data. The database context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) and specifies the entities to include in the data model.

Scaffolding creates the `Data/MvcMovieContext.cs` database context class:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<MvcMovie.Models.Movie> Movie { get; set; }
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property that represents the movies in the database.

### Dependency injection

ASP.NET Core is built with [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the database context, are registered with DI in `Program.cs`. These services are provided to components that require them via constructor parameters.

In the `Controllers/MoviesController.cs` file, the constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the `MvcMovieContext` database context into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Scaffolding generated the following highlighted code in `Program.cs`:

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_6_visual-studio-code+visual-studio-mac)

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the "MvcMovieContext" database connection string.

[]()

### Examine the generated database connection string

Scaffolding added a connection string to the `appsettings.json` file:

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_7_visual-studio-code+visual-studio-mac)

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovieContext-ea7a4069-f366-4742-bd1c-3f753a804ce1.db"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovie.db"
  }
}
```

For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the `ConnectionString` key from the `appsettings.json` file.

### The `InitialCreate` class

Examine the `Migrations/{timestamp}_InitialCreate.cs` migration file:

```
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Movie",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Movie", x => x.Id);
                });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Movie");
        }
    }
}
```

In the preceding code:

* `InitialCreate.Up` creates the Movie table and configures `Id` as the primary key.
* `InitialCreate.Down` reverts the schema changes made by the `Up` migration.

## Dependency injection in the controller

Open the `Controllers/MoviesController.cs` file and examine the constructor:

```
public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }
```

The constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the database context (`MvcMovieContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Test the **Create** page. Enter and submit data.

Test the **Edit**, **Details**, and **Delete** pages.

[]()

## Strongly typed models and the `@model` directive

Earlier in this tutorial, you saw how a controller can pass data or objects to a view using the `ViewData` dictionary. The `ViewData` dictionary is a dynamic object that provides a convenient late-bound way to pass information to a view.

MVC provides the ability to pass strongly typed model objects to a view. This strongly typed approach enables compile time code checking. The scaffolding mechanism passed a strongly typed model in the `MoviesController` class and views.

Examine the generated `Details` method in the `Controllers/MoviesController.cs` file:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The `id` parameter is generally passed as route data. For example, `https://localhost:5001/movies/details/1` sets:

* The controller to the `movies` controller, the first URL segment.
* The action to `details`, the second URL segment.
* The `id` to 1, the last URL segment.

The `id` can be passed in with a query string, as in the following example:

`https://localhost:5001/movies/details?id=1`

The `id` parameter is defined as a [nullable type](/en-us/dotnet/csharp/programming-guide/nullable-types/index) (`int?`) in cases when the `id` value isn't provided.

A [lambda expression](/en-us/dotnet/articles/csharp/programming-guide/statements-expressions-operators/lambda-expressions) is passed in to the [FirstOrDefaultAsync](/en-us/dotnet/api/system.data.entity.queryableextensions.firstordefaultasync) method to select movie entities that match the route data or query string value.

```
var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);
```

If a movie is found, an instance of the `Movie` model is passed to the `Details` view:

```
return View(movie);
```

Examine the contents of the `Views/Movies/Details.cshtml` file:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>
```

The `@model` statement at the top of the view file specifies the type of object that the view expects. When the movie controller was created, the following `@model` statement was included:

```
@model MvcMovie.Models.Movie
```

This `@model` directive allows access to the movie that the controller passed to the view. The `Model` object is strongly typed. For example, in the `Details.cshtml` view, the code passes each movie field to the `DisplayNameFor` and `DisplayFor` HTML Helpers with the strongly typed `Model` object. The `Create` and `Edit` methods and views also pass a `Movie` model object.

Examine the `Index.cshtml` view and the `Index` method in the Movies controller. Notice how the code creates a `List` object when it calls the `View` method. The code passes this `Movies` list from the `Index` action method to the view:

```
// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}
```

The code returns [problem details](../../fundamentals/error-handling-api?view=aspnetcore-10.0#problem-details-service) if the `Movie` property of the data context is null.

When the movies controller was created, scaffolding included the following `@model` statement at the top of the `Index.cshtml` file:

```
@model IEnumerable<MvcMovie.Models.Movie>
```

The `@model` directive allows access to the list of movies that the controller passed to the view by using a `Model` object that's strongly typed. For example, in the `Index.cshtml` view, the code loops through the movies with a `foreach` statement over the strongly typed `Model` object:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Because the `Model` object is strongly typed as an `IEnumerable<Movie>` object, each item in the loop is typed as `Movie`. Among other benefits, the compiler validates the types used in the code.

## Related content

* [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s)
* [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)

[Previous: Adding a View](adding-view?view=aspnetcore-10.0) [Next: Working with SQL](working-with-sql?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. These classes are the "**M**odel" part of the **M**VC app.

These model classes are used with [Entity Framework Core](/en-us/ef/core) (EF Core) to work with a database. EF Core is an object-relational mapping (ORM) framework that simplifies the data access code that you have to write.

The model classes created are known as ***POCO*** classes, from **P**lain **O**ld **C**LR **O**bjects. POCO classes don't have any dependency on EF Core. They only define the properties of the data to be stored in the database.

In this tutorial, model classes are created first, and EF Core creates the database.

## Add a data model class

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Right-click the *Models* folder > **Add** > **Class**. Name the file `Movie.cs`.

Add a file named `Movie.cs` to the *Models* folder.

For Visual Studio for Mac, see the .NET 7 version of this tutorial.

Update the `Models/Movie.cs` file with the following code:

```
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string? Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string? Genre { get; set; }
        public decimal Price { get; set; }
    }
}
```

The `Movie` class contains an `Id` field, which is required by the database for the primary key.

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute on `ReleaseDate` specifies the type of the data (`Date`). With this attribute:

* The user isn't required to enter time information in the date field.
* Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

## Add NuGet packages

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** (PMC).

In the PMC, run the following command:

```
Install-Package Microsoft.EntityFrameworkCore.Design
Install-Package Microsoft.EntityFrameworkCore.SqlServer
```

The preceding commands add:

* The EF Core SQL Server provider. The provider package installs the EF Core package as a dependency.
* The utilities used by the packages installed automatically in the scaffolding step, later in the tutorial.

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```

For Visual Studio for Mac, see the .NET 7 version of this tutorial.

Build the project as a check for compiler errors.

## Scaffold movie pages

Use the scaffolding tool to produce `Create`, `Read`, `Update`, and `Delete` (CRUD) pages for the movie model.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

In **Solution Explorer**, right-click the *Controllers* folder and select **Add > New Scaffolded Item**.

In the **Add Scaffold** dialog, select **MVC Controller with views, using Entity Framework > Add**.

Complete the **Add MVC Controller with views, using Entity Framework** dialog:

* In the **Model class** drop down, select **Movie (MvcMovie.Models)**.

* In the **Data context class** row, select the **+** (plus) sign.

  * In the **Add Data Context** dialog, the class name *MvcMovie.Data.MvcMovieContext* is generated.
  * Select **Add**.

* **Views** and **Controller name**: Keep the default.

* Select **Add**.

If you get an error message, select **Add** a second time to try it again.

[]()

Scaffolding updates the following:

* Inserts required package references in the `MvcMovie.csproj` project file.
* Registers the database context in the `Program.cs` file.
* Adds a database connection string to the `appsettings.json` file.

Open a command window in the project directory. The project directory is the directory that contains the `Program.cs` and `.csproj` files.

On macOS and Linux, export the scaffold tool path:

```
export PATH=$HOME/.dotnet/tools:$PATH
```

Run the following command:

```
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries -sqlite
```

The following table details the ASP.NET Core code generator parameters.

| Parameter                  | Description                                                             |
| -------------------------- | ----------------------------------------------------------------------- |
| -m                         | The name of the model.                                                  |
| -dc                        | The data context.                                                       |
| --relativeFolderPath       | The relative output folder path to create the files.                    |
| --useDefaultLayout\|-udl   | The default layout should be used for the views.                        |
| --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.              |
| -sqlite                    | Flag to specify if `DbContext` should use SQLite instead of SQL Server. |

Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

```
dotnet aspnet-codegenerator controller -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

[]()

### Use SQLite for development, SQL Server for production

The following highlighted code in `Program.cs` shows how to use SQLite in development and SQL Server in production.

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
}
else
{
    builder.Services.AddDbContext<MvcMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMvcMovieContext")));
}
```

[]()

Scaffolding updates the following:

* Registers the database context in the `Program.cs` file
* Adds a database connection string to the `appsettings.json` file.

For Visual Studio for Mac, see the .NET 7 version of this tutorial.

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

The automatic creation of these files and file updates is known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

[]()

## Build the app

Build the app. The compiler generates several warnings about how `null` values are handled. See [this GitHub issue](https://github.com/dotnet/Scaffolding/issues/1594) and [Nullable reference types](/en-us/dotnet/csharp/nullable-references) for more information.

To eliminate the warnings from nullable reference types, remove the following line from the `MvcMovie.csproj` file:

```
<Nullable>enable</Nullable>
```

We hope to fix this issue in the next release.

## Initial migration

Use the EF Core [Migrations](../../data/ef-mvc/migrations?view=aspnetcore-10.0) feature to create the database. *Migrations* is a set of tools that create and update a database to match the data model.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** .

In the Package Manager Console (PMC), enter the following commands:

```
Add-Migration InitialCreate
Update-Database
```

* `Add-Migration InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. Because this is the first migration, the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class.

* `Update-Database`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

The `Update-Database` command generates the following warning:

> No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

Ignore the preceding warning, it's fixed in a later tutorial.

For more information on the PMC tools for EF Core, see [EF Core tools reference - PMC in Visual Studio](/en-us/ef/core/miscellaneous/cli/powershell).

If `dotnet ef` has not been installed, install it as a global tool:

```
  dotnet tool install --global dotnet-ef
```

For more information on the CLI for EF Core, see [EF Core tools reference for the .NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Run the following .NET CLI commands:

```
dotnet ef migrations add InitialCreate
dotnet ef database update
```

* `ef migrations add InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. This is the first migration, so the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class, in the `Data/MvcMovieContext.cs` file.
* `ef database update`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

[]()

## Test the app

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the [migrations step](#migration):

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_5_visual-studio-code+visual-studio-mac)

```
SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.
```

```
SqliteException: SQLite Error 1: 'no such table: Movie'.
```

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

[]()

### Examine the generated database context class and registration

With EF Core, data access is performed using a model. A model is made up of entity classes and a context object that represents a session with the database. The context object allows querying and saving data. The database context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) and specifies the entities to include in the data model.

Scaffolding creates the `Data/MvcMovieContext.cs` database context class:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<MvcMovie.Models.Movie> Movie { get; set; }
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property that represents the movies in the database.

### Dependency injection

ASP.NET Core is built with [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the database context, are registered with DI in `Program.cs`. These services are provided to components that require them via constructor parameters.

In the `Controllers/MoviesController.cs` file, the constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the `MvcMovieContext` database context into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Scaffolding generated the following highlighted code in `Program.cs`:

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_6_visual-studio-mac)

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

For Visual Studio for Mac, see the .NET 7 version of this tutorial.

The [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the "MvcMovieContext" database connection string.

[]()

### Examine the generated database connection string

Scaffolding added a connection string to the `appsettings.json` file:

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_7_visual-studio-code+visual-studio-mac)

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-7dc5;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovie.db"
  }
}
```

For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the `ConnectionString` key from the `appsettings.json` file.

### The `InitialCreate` class

Examine the `Migrations/{timestamp}_InitialCreate.cs` migration file:

```
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Movie",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Movie", x => x.Id);
                });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Movie");
        }
    }
}
```

In the preceding code:

* `InitialCreate.Up` creates the Movie table and configures `Id` as the primary key.
* `InitialCreate.Down` reverts the schema changes made by the `Up` migration.

## Dependency injection in the controller

Open the `Controllers/MoviesController.cs` file and examine the constructor:

```
public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }
```

The constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the database context (`MvcMovieContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Test the **Create** page. Enter and submit data.

Test the **Edit**, **Details**, and **Delete** pages.

[]()

## Strongly typed models and the `@model` directive

Earlier in this tutorial, you saw how a controller can pass data or objects to a view using the `ViewData` dictionary. The `ViewData` dictionary is a dynamic object that provides a convenient late-bound way to pass information to a view.

MVC provides the ability to pass strongly typed model objects to a view. This strongly typed approach enables compile time code checking. The scaffolding mechanism passed a strongly typed model in the `MoviesController` class and views.

Examine the generated `Details` method in the `Controllers/MoviesController.cs` file:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The `id` parameter is generally passed as route data. For example, `https://localhost:5001/movies/details/1` sets:

* The controller to the `movies` controller, the first URL segment.
* The action to `details`, the second URL segment.
* The `id` to 1, the last URL segment.

The `id` can be passed in with a query string, as in the following example:

`https://localhost:5001/movies/details?id=1`

The `id` parameter is defined as a [nullable type](/en-us/dotnet/csharp/programming-guide/nullable-types/index) (`int?`) in cases when the `id` value isn't provided.

A [lambda expression](/en-us/dotnet/articles/csharp/programming-guide/statements-expressions-operators/lambda-expressions) is passed in to the [FirstOrDefaultAsync](/en-us/dotnet/api/system.data.entity.queryableextensions.firstordefaultasync) method to select movie entities that match the route data or query string value.

```
var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);
```

If a movie is found, an instance of the `Movie` model is passed to the `Details` view:

```
return View(movie);
```

Examine the contents of the `Views/Movies/Details.cshtml` file:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>
```

The `@model` statement at the top of the view file specifies the type of object that the view expects. When the movie controller was created, the following `@model` statement was included:

```
@model MvcMovie.Models.Movie
```

This `@model` directive allows access to the movie that the controller passed to the view. The `Model` object is strongly typed. For example, in the `Details.cshtml` view, the code passes each movie field to the `DisplayNameFor` and `DisplayFor` HTML Helpers with the strongly typed `Model` object. The `Create` and `Edit` methods and views also pass a `Movie` model object.

Examine the `Index.cshtml` view and the `Index` method in the Movies controller. Notice how the code creates a `List` object when it calls the `View` method. The code passes this `Movies` list from the `Index` action method to the view:

```
// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}
```

When the movies controller was created, scaffolding included the following `@model` statement at the top of the `Index.cshtml` file:

```
@model IEnumerable<MvcMovie.Models.Movie>
```

The `@model` directive allows access to the list of movies that the controller passed to the view by using a `Model` object that's strongly typed. For example, in the `Index.cshtml` view, the code loops through the movies with a `foreach` statement over the strongly typed `Model` object:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Because the `Model` object is strongly typed as an `IEnumerable<Movie>` object, each item in the loop is typed as `Movie`. Among other benefits, the compiler validates the types used in the code.

## Related content

* [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s)
* [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)

[Previous Adding a View](adding-view?view=aspnetcore-10.0) [Next Working with SQL](working-with-sql?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. These classes are the "**M**odel" part of the **M**VC app.

These model classes are used with [Entity Framework Core](/en-us/ef/core) (EF Core) to work with a database. EF Core is an object-relational mapping (ORM) framework that simplifies the data access code that you have to write.

The model classes created are known as ***POCO*** classes, from **P**lain **O**ld **C**LR **O**bjects. POCO classes don't have any dependency on EF Core. They only define the properties of the data to be stored in the database.

In this tutorial, model classes are created first, and EF Core creates the database.

## Add a data model class

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Right-click the *Models* folder > **Add** > **Class**. Name the file `Movie.cs`.

Add a file named `Movie.cs` to the *Models* folder.

Control-click the *Models* folder > **Add** > **New Class** > **Empty Class**. Name the file `Movie.cs`.

Update the `Models/Movie.cs` file with the following code:

```
using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}
```

The `Movie` class contains an `Id` field, which is required by the database for the primary key.

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute on `ReleaseDate` specifies the type of the data (`Date`). With this attribute:

* The user isn't required to enter time information in the date field.
* Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

## Add NuGet packages

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** (PMC).

In the PMC, run the following command:

```
Install-Package Microsoft.EntityFrameworkCore.Design
```

The preceding commands add:

* The EF Core SQL Server provider. The provider package installs the EF Core package as a dependency.
* The utilities used by the packages installed automatically in the scaffolding step, later in the tutorial.

Run the following .NET CLI commands:

```
dotnet tool install --global dotnet-ef
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```

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

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.0-*" />
  </ItemGroup>

</Project>
```

From the **Project** menu, select **Manage NuGet Packages**.

In the **Search** field in the upper right:

* Enter `Microsoft.EntityFrameworkCore.SQLite`.
* Press the `Return` key to search.
* Select the matching NuGet package and select the **Add Package** button.

The **Select Projects** dialog displays, with the `MvcMovie` project selected. Select the **Ok** button.

A **License Acceptance** dialog displays. Review the licenses, then select the **Accept** button.

Repeat the above steps to install the following NuGet packages:

* `Microsoft.VisualStudio.Web.CodeGeneration.Design`
* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.EntityFrameworkCore.Design`

Run the following .NET CLI command:

```
dotnet tool install --global dotnet-aspnet-codegenerator
```

The preceding command adds the [aspnet-codegenerator scaffolding tool](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Build the project as a check for compiler errors.

## Scaffold movie pages

Use the scaffolding tool to produce `Create`, `Read`, `Update`, and `Delete` (CRUD) pages for the movie model.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

In **Solution Explorer**, right-click the *Controllers* folder and select **Add > New Scaffolded Item**.

In the **Add Scaffold** dialog, select **MVC Controller with views, using Entity Framework > Add**.

Complete the **Add MVC Controller with views, using Entity Framework** dialog:

* In the **Model class** drop down, select **Movie (MvcMovie.Models)**.

* In the **Data context class** row, select the **+** (plus) sign.

  * In the **Add Data Context** dialog, the class name *MvcMovie.Data.MvcMovieContext* is generated.
  * Select **Add**.

* **Views** and **Controller name**: Keep the default.

* Select **Add**.

[]()

Scaffolding updates the following:

* Inserts required package references in the `MvcMovie.csproj` project file.
* Registers the database context in `Startup.ConfigureServices` of the `Startup.cs` file.
* Adds a database connection string to the `appsettings.json` file.

Open a command window in the project directory. The project directory is the directory that contains the `Program.cs`, `Startup.cs`, and `.csproj` files.

On macOS and Linux, export the scaffold tool path:

```
export PATH=$HOME/.dotnet/tools:$PATH
```

Run the following command:

```
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries -sqlite
```

The following table details the ASP.NET Core code generator parameters.

| Parameter                  | Description                                                             |
| -------------------------- | ----------------------------------------------------------------------- |
| -m                         | The name of the model.                                                  |
| -dc                        | The data context.                                                       |
| --relativeFolderPath       | The relative output folder path to create the files.                    |
| --useDefaultLayout\|-udl   | The default layout should be used for the views.                        |
| --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.              |
| -sqlite                    | Flag to specify if `DbContext` should use SQLite instead of SQL Server. |

Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

```
dotnet aspnet-codegenerator controller -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

[]()

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to inject [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into `Startup`. `IWebHostEnvironment` is injected so `ConfigureServices` can use SQLite in development and SQL Server in production.

```
public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Environment = env;
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Environment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();

        services.AddDbContext<MvcMovieContext>(options =>
        {
            var connectionString = Configuration.GetConnectionString("MvcMovieContext");

            if (Environment.IsDevelopment())
            {
                options.UseSqlite(connectionString);
            }
            else
            {
                options.UseSqlServer(connectionString);
            }
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

[]()

Scaffolding updates the following:

* Registers the database context in `Startup.ConfigureServices` of the `Startup.cs` file.
* Adds a database connection string to the `appsettings.json` file.

Open a command window in the project directory. The project directory is the directory that contains the `Program.cs`, `Startup.cs`, and `.csproj` files.

Export the scaffold tool path:

```
export PATH=$HOME/.dotnet/tools:$PATH
```

Run the following command:

```
dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries -sqlite
```

The following table details the ASP.NET Core code generator parameters.

| Parameter                  | Description                                                             |
| -------------------------- | ----------------------------------------------------------------------- |
| -m                         | The name of the model.                                                  |
| -dc                        | The data context.                                                       |
| --relativeFolderPath       | The relative output folder path to create the files.                    |
| --useDefaultLayout\|-udl   | The default layout should be used for the views.                        |
| --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.              |
| -sqlite                    | Flag to specify if `DbContext` should use SQLite instead of SQL Server. |

Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

```
dotnet aspnet-codegenerator controller -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

[]()

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to inject [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into `Startup`. `IWebHostEnvironment` is injected so `ConfigureServices` can use SQLite in development and SQL Server in production.

```
public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Environment = env;
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Environment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();

        services.AddDbContext<MvcMovieContext>(options =>
        {
            var connectionString = Configuration.GetConnectionString("MvcMovieContext");

            if (Environment.IsDevelopment())
            {
                options.UseSqlite(connectionString);
            }
            else
            {
                options.UseSqlServer(connectionString);
            }
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

[]()

Scaffolding updates the following:

* Registers the database context in `Startup.ConfigureServices` of the `Startup.cs` file.
* Adds a database connection string to the `appsettings.json` file.

Scaffolding creates the following:

* A movies controller: `Controllers/MoviesController.cs`
* Razor view files for **Create**, **Delete**, **Details**, **Edit**, and **Index** pages: `Views/Movies/*.cshtml`
* A database context class: `Data/MvcMovieContext.cs`

The automatic creation of these files and file updates are known as *scaffolding*.

The scaffolded pages can't be used yet because the database doesn't exist. Running the app and selecting the **Movie App** link results in a *Cannot open database* or *no such table: Movie* error message.

[]()

## Initial migration

Use the EF Core [Migrations](../../data/ef-mvc/migrations?view=aspnetcore-10.0) feature to create the database. Migrations are a set of tools that create and update a database to match the data model.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** .

In the Package Manager Console (PMC), enter the following commands:

```
Add-Migration InitialCreate
Update-Database
```

* `Add-Migration InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. Because this is the first migration, the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class.

* `Update-Database`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

The `Update-Database` command generates the following warning:

> No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

Ignore the preceding warning, it's fixed in a later tutorial.

For more information on the PMC tools for EF Core, see [EF Core tools reference - PMC in Visual Studio](/en-us/ef/core/miscellaneous/cli/powershell).

If `dotnet ef` has not been installed, install it as a global tool:

```
  dotnet tool install --global dotnet-ef
```

For more information on the CLI for EF Core, see [EF Core tools reference for the .NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Run the following .NET CLI commands:

```
dotnet ef migrations add InitialCreate
dotnet ef database update
```

* `ef migrations add InitialCreate`: Generates an `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. This is the first migration, so the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class, in the `Data/MvcMovieContext.cs` file.
* `ef database update`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

[]()

## Test the app

Run the app and select the **Movie App** link.

If you get an exception similar to the following, you may have missed the [migrations step](#migration):

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_5_visual-studio-code+visual-studio-mac)

```
SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.
```

```
SqliteException: SQLite Error 1: 'no such table: Movie'.
```

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

[]()

### Examine the generated database context class and registration

With EF Core, data access is performed using a model. A model is made up of entity classes and a context object that represents a session with the database. The context object allows querying and saving data. The database context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) and specifies the entities to include in the data model.

Scaffolding creates the `Data/MvcMovieContext.cs` database context class:

```
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<Movie> Movie { get; set; }
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property that represents the movies in the database.

ASP.NET Core is built with [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the database context, must be registered with DI in `Startup`. Components that require these services are provided via constructor parameters.

In the `Controllers/MoviesController.cs` file, the constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the `MvcMovieContext` database context into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Scaffolding generated the following highlighted code in `Startup.ConfigureServices`:

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_6_visual-studio-mac)

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
```

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    
    services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(Configuration.GetConnectionString("MvcMovieContext")));
}
```

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    
    services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(Configuration.GetConnectionString("MvcMovieContext")));
}
```

The [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the "MvcMovieContext" database connection string.

[]()

### Examine the generated database connection string

Scaffolding added a connection string to the `appsettings.json` file:

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_7_visual-studio-code+visual-studio-mac)

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovie.db"
  }
}
```

For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the `ConnectionString` key from the `appsettings.json` file.

### The `InitialCreate` class

Examine the `Migrations/{timestamp}_InitialCreate.cs` migration file:

```
public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Movie",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Movie", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Movie");
    }
}
```

In the preceding code:

* `InitialCreate.Up` creates the Movie table and configures `Id` as the primary key.
* `InitialCreate.Down` reverts the schema changes made by the `Up` migration.

## Dependency injection in the controller

Open the `Controllers/MoviesController.cs` file and examine the constructor:

```
public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }
```

The constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the database context (`MvcMovieContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

Test the **Create** page. Enter and submit data.

Test the **Edit**, **Details**, and **Delete** pages.

[]()

## Strongly typed models and the `@model` directive

Earlier in this tutorial, you saw how a controller can pass data or objects to a view using the `ViewData` dictionary. The `ViewData` dictionary is a dynamic object that provides a convenient late-bound way to pass information to a view.

MVC provides the ability to pass strongly typed model objects to a view. This strongly typed approach enables compile time code checking. The scaffolding mechanism passed a strongly typed model in the `MoviesController` class and views.

Examine the generated `Details` method in the `Controllers/MoviesController.cs` file:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The `id` parameter is generally passed as route data. For example, `https://localhost:5001/movies/details/1` sets:

* The controller to the `movies` controller, the first URL segment.
* The action to `details`, the second URL segment.
* The `id` to 1, the last URL segment.

The `id` can be passed in with a query string, as in the following example:

`https://localhost:5001/movies/details?id=1`

The `id` parameter is defined as a [nullable type](/en-us/dotnet/csharp/programming-guide/nullable-types/index) (`int?`) in cases when the `id` value isn't provided.

A [lambda expression](/en-us/dotnet/articles/csharp/programming-guide/statements-expressions-operators/lambda-expressions) is passed in to the [FirstOrDefaultAsync](/en-us/dotnet/api/system.data.entity.queryableextensions.firstordefaultasync) method to select movie entities that match the route data or query string value.

```
var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);
```

If a movie is found, an instance of the `Movie` model is passed to the `Details` view:

```
return View(movie);
```

Examine the contents of the `Views/Movies/Details.cshtml` file:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>
```

The `@model` statement at the top of the view file specifies the type of object that the view expects. When the movie controller was created, the following `@model` statement was included:

```
@model MvcMovie.Models.Movie
```

This `@model` directive allows access to the movie that the controller passed to the view. The `Model` object is strongly typed. For example, in the `Details.cshtml` view, the code passes each movie field to the `DisplayNameFor` and `DisplayFor` HTML Helpers with the strongly typed `Model` object. The `Create` and `Edit` methods and views also pass a `Movie` model object.

Examine the `Index.cshtml` view and the `Index` method in the Movies controller. Notice how the code creates a `List` object when it calls the `View` method. The code passes this `Movies` list from the `Index` action method to the view:

```
// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}
```

When the movies controller was created, scaffolding included the following `@model` statement at the top of the `Index.cshtml` file:

```
@model IEnumerable<MvcMovie.Models.Movie>
```

The `@model` directive allows access to the list of movies that the controller passed to the view by using a `Model` object that's strongly typed. For example, in the `Index.cshtml` view, the code loops through the movies with a `foreach` statement over the strongly typed `Model` object:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Because the `Model` object is strongly typed as an `IEnumerable<Movie>` object, each item in the loop is typed as `Movie`. Among other benefits, the compiler validates the types used in the code.

## SQL Logging of Entity Framework Core

Logging configuration is commonly provided by the `Logging` section of `appsettings.{Environment}.json` files. To log SQL statements, add `"Microsoft.EntityFrameworkCore.Database.Command": "Information"` to the `appsettings.Development.json` file:

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDB-2;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
     ,"Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

With the preceding JSON, SQL statements are displayed on the command line and in the Visual Studio output window.

For more information, see the following resources:

* [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0#configuration)
* [ASP.NET template disables EF Core SQL logging by default (`dotnet/aspnetcore` #32977)](https://github.com/dotnet/aspnetcore/issues/32977)

## Related content

* [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s)
* [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)

[Previous Adding a View](adding-view?view=aspnetcore-10.0) [Next Working with SQL](working-with-sql?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. These classes are the "**M**odel" part of the **M**VC app.

These model classes are used with [Entity Framework Core](/en-us/ef/core) (EF Core) to work with a database. EF Core is an object-relational mapping (ORM) framework that simplifies the data access code that you have to write.

The model classes created are known as ***POCO*** classes, from **P**lain **O**ld **C**LR **O**bjects. POCO classes don't have any dependency on EF Core. They only define the properties of the data to be stored in the database.

In this tutorial, model classes are created first, and EF Core creates the database.

## Add a data model class

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Right-click the *Models* folder > **Add** > **Class**. Name the file `Movie.cs`.

Add a file named `Movie.cs` to the *Models* folder.

Right-click the *Models* folder > **Add** > **New Class** > **Empty Class**. Name the file `Movie.cs`.

Update the `Movie.cs` file with the following code:

```
using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}
```

The `Movie` class contains an `Id` field, which is required by the database for the primary key.

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute on `ReleaseDate` specifies the type of the data (`Date`). With this attribute:

* The user is not required to enter time information in the date field.
* Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

## Add NuGet packages

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** (PMC).

In the PMC, run the following command:

```
Install-Package Microsoft.EntityFrameworkCore.SqlServer
```

The preceding command adds the EF Core SQL Server provider. The provider package installs the EF Core package as a dependency. Additional packages are installed automatically in the scaffolding step later in the tutorial.

Run the following .NET CLI commands:

```
dotnet tool install --global dotnet-ef --version 3.1.9
dotnet tool install --global dotnet-aspnet-codegenerator --version 3.1.4
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 3.1.9
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore --version 3.1.9
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design --version 3.1.4
dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 3.1.9
dotnet add package Microsoft.Extensions.Logging.Debug --version 3.1.9
```

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
  </ItemGroup>

</Project>
```

From the **Project** menu, select **Manage NuGet Packages**.

In the **Search** field in the upper right, enter `Microsoft.EntityFrameworkCore.SQLite` and press the **Return** key to search. Select the matching NuGet package and press the **Add Package** button.

The **Select Projects** dialog will be displayed, with the `MvcMovie` project selected. Press the **Ok** button.

A **License Acceptance** dialog will be displayed. Review the licenses as desired, then click the **Accept** button.

Repeat the above steps to install the following NuGet packages:

* `Microsoft.VisualStudio.Web.CodeGeneration.Design`
* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.EntityFrameworkCore.Design`

[]()

## Create a database context class

A database context class is needed to coordinate EF Core functionality (Create, Read, Update, Delete) for the `Movie` model. The database context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) and specifies the entities to include in the data model.

Create a *Data* folder.

Add a `Data/MvcMovieContext.cs` file with the following code:

```
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<Movie> Movie { get; set; }
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

[]()

## Register the database context

ASP.NET Core is built with [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services (such as the EF Core DB context) must be registered with DI during application startup. Components that require these services (such as Razor Pages) are provided via constructor parameters. The constructor code that gets a DB context instance is shown later in the tutorial. In this section, you register the database context with the DI container.

Add the following `using` statements at the top of `Startup.cs`:

```
using MvcMovie.Data;
using Microsoft.EntityFrameworkCore;
```

Add the following highlighted code in `Startup.ConfigureServices`:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
```

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    
    services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(Configuration.GetConnectionString("MvcMovieContext")));
}
```

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

[]()

## Examine the database connection string

Add a connection string to the `appsettings.json` file:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovie.db"
  }
}
```

Build the project as a check for compiler errors.

## Scaffold movie pages

Use the scaffolding tool to produce Create, Read, Update, and Delete (CRUD) pages for the movie model.

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_5_visual-studio-mac)

In **Solution Explorer**, right-click the *Controllers* folder **> Add > New Scaffolded Item**.

In the **Add Scaffold** dialog, select **MVC Controller with views, using Entity Framework > Add**.

Complete the **Add Controller** dialog:

* **Model class:** *Movie (MvcMovie.Models)*
* **Data context class:** *MvcMovieContext (MvcMovie.Data)*

- **Views:** Keep the default of each option checked
- **Controller name:** Keep the default *MoviesController*
- Select **Add**

Visual Studio creates:

* A movies controller (`Controllers/MoviesController.cs`)
* Razor view files for Create, Delete, Details, Edit, and Index pages (\*Views/Movies/\`.cshtml\`)

The automatic creation of these files is known as *scaffolding*.

* Open a command window in the project directory (The directory that contains the `Program.cs`, `Startup.cs`, and `.csproj` files).

* On macOS and Linux, export the scaffold tool path:

  ```
  export PATH=$HOME/.dotnet/tools:$PATH
  ```

* Run the following command:

  ```
  dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries -sqlite
  ```

  The following table details the ASP.NET Core code generator parameters.

  | Parameter                  | Description                                                             |
  | -------------------------- | ----------------------------------------------------------------------- |
  | -m                         | The name of the model.                                                  |
  | -dc                        | The data context.                                                       |
  | --relativeFolderPath       | The relative output folder path to create the files.                    |
  | --useDefaultLayout\|-udl   | The default layout should be used for the views.                        |
  | --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.              |
  | -sqlite                    | Flag to specify if `DbContext` should use SQLite instead of SQL Server. |

  Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

  ```
  dotnet aspnet-codegenerator controller -h
  ```

  For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

- Open a command window in the project directory (The directory that contains the `Program.cs`, `Startup.cs`, and `.csproj` files).

- Run the following command:

  ```
  dotnet aspnet-codegenerator controller -name MoviesController -m Movie -dc MvcMovieContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries -sqlite
  ```

  The following table details the ASP.NET Core code generator parameters.

  | Parameter                  | Description                                                             |
  | -------------------------- | ----------------------------------------------------------------------- |
  | -m                         | The name of the model.                                                  |
  | -dc                        | The data context.                                                       |
  | --relativeFolderPath       | The relative output folder path to create the files.                    |
  | --useDefaultLayout\|-udl   | The default layout should be used for the views.                        |
  | --referenceScriptLibraries | Adds `_ValidationScriptsPartial` to Edit and Create pages.              |
  | -sqlite                    | Flag to specify if `DbContext` should use SQLite instead of SQL Server. |

  Use the `h` switch to get help on the `aspnet-codegenerator controller` command:

  ```
  dotnet aspnet-codegenerator controller -h
  ```

  For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)

You can't use the scaffolded pages yet because the database doesn't exist. If you run the app and click on the **Movie App** link, you get a *Cannot open database* or *no such table: Movie* error message.

[]()

## Initial migration

Use the EF Core [Migrations](../../data/ef-mvc/migrations?view=aspnetcore-10.0) feature to create the database. Migrations is a set of tools that let you create and update a database to match your data model.

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_6_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console** (PMC).

In the PMC, enter the following commands:

```
Add-Migration InitialCreate
Update-Database
```

* `Add-Migration InitialCreate`: Generates a `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. Because this is the first migration, the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class.

* `Update-Database`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

  The database update command generates the following warning:

  > No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

  You can ignore that warning, it will be fixed in a later tutorial.

For more information on the PMC tools for EF Core, see [EF Core tools reference - PMC in Visual Studio](/en-us/ef/core/miscellaneous/cli/powershell).

If `dotnet ef` has not been installed, install it as a global tool:

```
  dotnet tool install --global dotnet-ef
```

For more information on the CLI for EF Core, see [EF Core tools reference for the .NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Run the following .NET CLI commands:

```
dotnet ef migrations add InitialCreate
dotnet ef database update
```

* `ef migrations add InitialCreate`: Generates an `Migrations/{timestamp}_InitialCreate.cs` migration file. The `InitialCreate` argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. Because this is the first migration, the generated class contains code to create the database schema. The database schema is based on the model specified in the `MvcMovieContext` class (in the `Data/MvcMovieContext.cs` file).

* `ef database update`: Updates the database to the latest migration, which the previous command created. This command runs the `Up` method in the `Migrations/{time-stamp}_InitialCreate.cs` file, which creates the database.

### The InitialCreate class

Examine the `Migrations/{timestamp}_InitialCreate.cs` migration file:

```
public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Movie",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:ValueGenerationStrategy", 
                                 SqlServerValueGenerationStrategy.IdentityColumn),
                Title = table.Column<string>(nullable: true),
                ReleaseDate = table.Column<DateTime>(nullable: false),
                Genre = table.Column<string>(nullable: true),
                Price = table.Column<decimal>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Movie", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Movie");
    }
}
```

The `Up` method creates the Movie table and configures `Id` as the primary key. The `Down` method reverts the schema changes made by the `Up` migration.

[]()

## Test the app

* Run the app and click the **Movie App** link.

  If you get an exception similar to one of the following:

- [Visual Studio](#tabpanel_7_visual-studio)
- [Visual Studio Code / Visual Studio for Mac](#tabpanel_7_visual-studio-code+visual-studio-mac)

```
SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.
```

```
SqliteException: SQLite Error 1: 'no such table: Movie'.
```

You probably missed the [migrations step](#migration).

* Test the **Create** page. Enter and submit data.

  Note

  You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

* Test the **Edit**, **Details**, and **Delete** pages.

## Dependency injection in the controller

* [Visual Studio](#tabpanel_8_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_8_visual-studio-code+visual-studio-mac)

Open the `Controllers/MoviesController.cs` file and examine the constructor:

```
public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }
```

The constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the database context (`MvcMovieContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

```
public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }
```

The constructor uses [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to inject the database context (`MvcMovieContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to inject [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into Startup. `IWebHostEnvironment` is injected so `ConfigureServices` can use SQLite in development and SQL Server in production.

```
public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Environment = env;
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Environment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();

        services.AddDbContext<MvcMovieContext>(options =>
        {
            var connectionString = Configuration.GetConnectionString("MvcMovieContext");

            if (Environment.IsDevelopment())
            {
                options.UseSqlite(connectionString);
            }
            else
            {
                options.UseSqlServer(connectionString);
            }
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

[]()[]()

## Strongly typed models and the @model keyword

Earlier in this tutorial, you saw how a controller can pass data or objects to a view using the `ViewData` dictionary. The `ViewData` dictionary is a dynamic object that provides a convenient late-bound way to pass information to a view.

MVC also provides the ability to pass strongly typed model objects to a view. This strongly typed approach enables compile time code checking. The scaffolding mechanism used this approach (that is, passing a strongly typed model) with the `MoviesController` class and views.

Examine the generated `Details` method in the `Controllers/MoviesController.cs` file:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The `id` parameter is generally passed as route data. For example `https://localhost:5001/movies/details/1` sets:

* The controller to the `movies` controller (the first URL segment).
* The action to `details` (the second URL segment).
* The id to 1 (the last URL segment).

You can also pass in the `id` with a query string as follows:

`https://localhost:5001/movies/details?id=1`

The `id` parameter is defined as a [nullable type](/en-us/dotnet/csharp/programming-guide/nullable-types/index) (`int?`) in case an ID value isn't provided.

A [lambda expression](/en-us/dotnet/articles/csharp/programming-guide/statements-expressions-operators/lambda-expressions) is passed in to `FirstOrDefaultAsync` to select movie entities that match the route data or query string value.

```
var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);
```

If a movie is found, an instance of the `Movie` model is passed to the `Details` view:

```
return View(movie);
```

Examine the contents of the `Views/Movies/Details.cshtml` file:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>
```

The `@model` statement at the top of the view file specifies the type of object that the view expects. When the movie controller was created, the following `@model` statement was included:

```
@model MvcMovie.Models.Movie
```

This `@model` directive allows access to the movie that the controller passed to the view. The `Model` object is strongly typed. For example, in the `Details.cshtml` view, the code passes each movie field to the `DisplayNameFor` and `DisplayFor` HTML Helpers with the strongly typed `Model` object. The `Create` and `Edit` methods and views also pass a `Movie` model object.

Examine the `Index.cshtml` view and the `Index` method in the Movies controller. Notice how the code creates a `List` object when it calls the `View` method. The code passes this `Movies` list from the `Index` action method to the view:

```
// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}
```

When the movies controller was created, scaffolding included the following `@model` statement at the top of the `Index.cshtml` file:

```
@model IEnumerable<MvcMovie.Models.Movie>
```

The `@model` directive allows you to access the list of movies that the controller passed to the view by using a `Model` object that's strongly typed. For example, in the `Index.cshtml` view, the code loops through the movies with a `foreach` statement over the strongly typed `Model` object:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Because the `Model` object is strongly typed (as an `IEnumerable<Movie>` object), each item in the loop is typed as `Movie`. Among other benefits, this means that you get compile time checking of the code.

## Related content

* [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s)
* [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)

[Previous Adding a View](adding-view?view=aspnetcore-10.0) [Next Working with SQL](working-with-sql?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/2fa
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/2fa.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Two-factor authentication with SMS in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Swiss-Devs](https://github.com/Swiss-Devs)

Warning

Two factor authentication (2FA) authenticator apps, using a Time-based One-time Password Algorithm (TOTP), are the industry recommended approach for 2FA. 2FA using TOTP is preferred to SMS 2FA. For more information, see [Enable QR Code generation for TOTP authenticator apps in ASP.NET Core](identity-enable-qrcodes?view=aspnetcore-1.1) for ASP.NET Core 2.0 or later.

This tutorial shows how to set up two-factor authentication (2FA) using SMS. Instructions are given for [twilio](https://www.twilio.com/) and ASPSMS (`https://www.aspsms.com/asp.net/identity/core/testcredits/`), but you can use any other SMS provider. We recommend you complete [Account Confirmation and Password Recovery](accconfirm?view=aspnetcore-1.1) before starting this tutorial.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/2fa/samples/Web2FA). [How to download](../../fundamentals/?view=aspnetcore-1.1#how-to-download-a-sample).

## Create a new ASP.NET Core project

Create a new ASP.NET Core web app named `Web2FA` with individual accounts. Follow the instructions in [Enforce HTTPS in ASP.NET Core](../enforcing-ssl?view=aspnetcore-1.1) to set up and require HTTPS.

### Create an SMS account

Create an SMS account, for example, from [twilio](https://www.twilio.com/) or ASPSMS (`https://www.aspsms.com/asp.net/identity/core/testcredits/`). Record the authentication credentials (for twilio: accountSid and authToken, for ASPSMS: Userkey and Password).

#### Figuring out SMS Provider credentials

**Twilio:**

From the Dashboard tab of your Twilio account, copy the **Account SID** and **Auth token**.

**ASPSMS:**

From your account settings, navigate to **Userkey** and copy it together with your **Password**.

We will later store these values in with the secret-manager tool within the keys `SMSAccountIdentification` and `SMSAccountPassword`.

#### Specifying SenderID / Originator

**Twilio:** From the Numbers tab, copy your Twilio **phone number**.

**ASPSMS:** Within the Unlock Originators Menu, unlock one or more Originators or choose an alphanumeric Originator (Not supported by all networks).

We will later store this value with the secret-manager tool within the key `SMSAccountFrom`.

### Provide credentials for the SMS service

We'll use the [Options pattern](../../fundamentals/configuration/options?view=aspnetcore-1.1) to access the user account and key settings.

* Create a class to fetch the secure SMS key. For this sample, the `SMSoptions` class is created in the `Services/SMSoptions.cs` file.

```
namespace Web2FA.Services
{
    public class SMSoptions
    {
        public string SMSAccountIdentification { get; set; }
        public string SMSAccountPassword { get; set; }
        public string SMSAccountFrom { get; set; }
    }
}
```

Set the `SMSAccountIdentification`, `SMSAccountPassword` and `SMSAccountFrom` with the [secret-manager tool](../app-secrets?view=aspnetcore-1.1). For example:

```
C:/Web2FA/src/WebApp1>dotnet user-secrets set SMSAccountIdentification 12345
info: Successfully saved SMSAccountIdentification = 12345 to the secret store.
```

* Add the NuGet package for the SMS provider. From the Package Manager Console (PMC) run:

**Twilio:**

`Install-Package Twilio`

**ASPSMS:**

`Install-Package ASPSMS`

* Add code in the `Services/MessageServices.cs` file to enable SMS. Use either the Twilio or the ASPSMS section:

**Twilio:**

```
using Microsoft.Extensions.Options;
using System.Threading.Tasks;
using Twilio;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;

namespace Web2FA.Services
{
    // This class is used by the application to send Email and SMS
    // when you turn on two-factor authentication in ASP.NET Identity.
    // For more details see this link https://go.microsoft.com/fwlink/?LinkID=532713
    public class AuthMessageSender : IEmailSender, ISmsSender
    {
        public AuthMessageSender(IOptions<SMSoptions> optionsAccessor)
        {
            Options = optionsAccessor.Value;
        }

        public SMSoptions Options { get; }  // set only via Secret Manager

        public Task SendEmailAsync(string email, string subject, string message)
        {
            // Plug in your email service here to send an email.
            return Task.FromResult(0);
        }

        public Task SendSmsAsync(string number, string message)
        {
            // Plug in your SMS service here to send a text message.
            // Your Account SID from twilio.com/console
            var accountSid = Options.SMSAccountIdentification;
            // Your Auth Token from twilio.com/console
            var authToken = Options.SMSAccountPassword;

            TwilioClient.Init(accountSid, authToken);

            return MessageResource.CreateAsync(
              to: new PhoneNumber(number),
              from: new PhoneNumber(Options.SMSAccountFrom),
              body: message);
        }
    }
}
```

**ASPSMS:**

```
using Microsoft.Extensions.Options;
using System.Threading.Tasks;

namespace Web2FA.Services
{
    // This class is used by the application to send Email and SMS
    // when you turn on two-factor authentication in ASP.NET Identity.
    // For more details see this link https://go.microsoft.com/fwlink/?LinkID=532713
    public class AuthMessageSender : IEmailSender, ISmsSender
    {
        public AuthMessageSender(IOptions<SMSoptions> optionsAccessor)
        {
            Options = optionsAccessor.Value;
        }

        public SMSoptions Options { get; }  // set only via Secret Manager

        public Task SendEmailAsync(string email, string subject, string message)
        {
            // Plug in your email service here to send an email.
            return Task.FromResult(0);
        }

        public Task SendSmsAsync(string number, string message)
        {
            ASPSMS.SMS SMSSender = new ASPSMS.SMS();

            SMSSender.Userkey = Options.SMSAccountIdentification;
            SMSSender.Password = Options.SMSAccountPassword;
            SMSSender.Originator = Options.SMSAccountFrom;

            SMSSender.AddRecipient(number);
            SMSSender.MessageData = message;

            SMSSender.SendTextSMS();

            return Task.FromResult(0);
        }
    }
}
```

### Configure startup to use `SMSoptions`

Add `SMSoptions` to the service container in the `ConfigureServices` method in the `Startup.cs`:

```
    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddTransient<ISmsSender, AuthMessageSender>();
    services.Configure<SMSoptions>(Configuration);
}
```

### Enable two-factor authentication

Open the `Views/Manage/Index.cshtml` Razor view file and remove the comment characters (so no markup is commented out).

## Log in with two-factor authentication

* Run the app and register a new user

- Tap on your user name, which activates the `Index` action method in Manage controller. Then tap the phone number **Add** link.

* Add a phone number that will receive the verification code, and tap **Send verification code**.

- You will get a text message with the verification code. Enter it and tap **Submit**

If you don't get a text message, see twilio log page.

* The Manage view shows your phone number was added successfully.

- Tap **Enable** to enable two-factor authentication.

### Test two-factor authentication

* Log off.

* Log in.

* The user account has enabled two-factor authentication, so you have to provide the second factor of authentication. In this tutorial you have enabled phone verification. The built in templates also allow you to set up email as the second factor. You can set up additional second factors for authentication such as QR codes. Tap **Submit**.

- Enter the code you get in the SMS message.

- Clicking on the **Remember this browser** checkbox will exempt you from needing to use 2FA to log on when using the same device and browser. Enabling 2FA and clicking on **Remember this browser** will provide you with strong 2FA protection from malicious users trying to access your account, as long as they don't have access to your device. You can do this on any private device you regularly use. By setting **Remember this browser**, you get the added security of 2FA from devices you don't regularly use, and you get the convenience on not having to go through 2FA on your own devices.

## Account lockout for protecting against brute force attacks

Account lockout is recommended with 2FA. Once a user signs in through a local account or social account, each failed attempt at 2FA is stored. If the maximum failed access attempts is reached, the user is locked out (default: 5 minute lockout after 5 failed access attempts). A successful authentication resets the failed access attempts count and resets the clock. The maximum failed access attempts and lockout time can be set with [MaxFailedAccessAttempts](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions.maxfailedaccessattempts) and [DefaultLockoutTimeSpan](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions.defaultlockouttimespan). The following configures account lockout for 10 minutes after 10 failed access attempts:

```
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();

    services.Configure<IdentityOptions>(options =>
    {
        options.Lockout.MaxFailedAccessAttempts = 10;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
    });

    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddTransient<ISmsSender, AuthMessageSender>();
    services.Configure<SMSoptions>(Configuration);
}
```

Confirm that [PasswordSignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.passwordsigninasync) sets `lockoutOnFailure` to `true`:

```
var result = await _signInManager.PasswordSignInAsync(
                 Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-03-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/read-related-data
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/read-related-data.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Read related data - ASP.NET MVC with EF Core

In the previous tutorial, you completed the School data model. In this tutorial, you'll read and display related data -- that is, data that the Entity Framework loads into navigation properties.

The following illustrations show the pages that you'll work with.

In this tutorial, you:

* Learn how to load related data
* Create a Courses page
* Create an Instructors page
* Learn about explicit loading

## Prerequisites

* [Create a complex data model](complex-data-model?view=aspnetcore-10.0)

## Learn how to load related data

There are several ways that Object-Relational Mapping (ORM) software such as Entity Framework can load related data into the navigation properties of an entity:

* [Eager loading:](/en-us/ef/core/querying/related-data) When the entity is read, related data is retrieved along with it. This typically results in a single join query that retrieves all of the data that's needed. You specify eager loading in Entity Framework Core by using the `Include` and `ThenInclude` methods.

  You can retrieve some of the data in separate queries, and EF "fixes up" the navigation properties. That is, EF automatically adds the separately retrieved entities where they belong in navigation properties of previously retrieved entities. For the query that retrieves related data, you can use the `Load` method instead of a method that returns a list or object, such as `ToList` or `Single`.

* [Explicit loading:](/en-us/ef/core/querying/related-data) When the entity is first read, related data isn't retrieved. You write code that retrieves the related data if it's needed. As in the case of eager loading with separate queries, explicit loading results in multiple queries sent to the database. The difference is that with explicit loading, the code specifies the navigation properties to be loaded. In Entity Framework Core 1.1 you can use the `Load` method to do explicit loading. For example:

* [Lazy loading:](/en-us/ef/core/querying/related-data) When the entity is first read, related data isn't retrieved. However, the first time you attempt to access a navigation property, the data required for that navigation property is automatically retrieved. A query is sent to the database each time you try to get data from a navigation property for the first time. Entity Framework Core 1.0 doesn't support lazy loading.

### Performance considerations

If you know you need related data for every entity retrieved, eager loading often offers the best performance, because a single query sent to the database is typically more efficient than separate queries for each entity retrieved. For example, suppose that each department has ten related courses. Eager loading of all related data would result in just a single (join) query and a single round trip to the database. A separate query for courses for each department would result in eleven round trips to the database. The extra round trips to the database are especially detrimental to performance when latency is high.

On the other hand, in some scenarios separate queries is more efficient. Eager loading of all related data in one query might cause a very complex join to be generated, which SQL Server can't process efficiently. Or if you need to access an entity's navigation properties only for a subset of a set of the entities you're processing, separate queries might perform better because eager loading of everything up front would retrieve more data than you need. If performance is critical, it's best to test performance both ways in order to make the best choice.

## Create a Courses page

The `Course` entity includes a navigation property that contains the `Department` entity of the department that the course is assigned to. To display the name of the assigned department in a list of courses, you need to get the `Name` property from the `Department` entity that's in the `Course.Department` navigation property.

Create a controller named `CoursesController` for the `Course` entity type, using the same options for the **MVC Controller with views, using Entity Framework** scaffolder that you did earlier for the `StudentsController`, as shown in the following illustration:

Open `CoursesController.cs` and examine the `Index` method. The automatic scaffolding has specified eager loading for the `Department` navigation property by using the `Include` method.

Replace the `Index` method with the following code that uses a more appropriate name for the `IQueryable` that returns Course entities (`courses` instead of `schoolContext`):

```
public async Task<IActionResult> Index()
{
    var courses = _context.Courses
        .Include(c => c.Department)
        .AsNoTracking();
    return View(await courses.ToListAsync());
}
```

Open `Views/Courses/Index.cshtml` and replace the template code with the following code. The changes are highlighted:

```
@model IEnumerable<ContosoUniversity.Models.Course>

@{
    ViewData["Title"] = "Courses";
}

<h2>Courses</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.CourseID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Credits)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Department)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.CourseID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Credits)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Department.Name)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.CourseID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.CourseID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.CourseID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

You've made the following changes to the scaffolded code:

* Changed the heading from **Index** to **Courses**.

* Added a **Number** column that shows the `CourseID` property value. By default, primary keys aren't scaffolded because normally they're meaningless to end users. However, in this case the primary key is meaningful and you want to show it.

* Changed the **Department** column to display the department name. The code displays the `Name` property of the `Department` entity that's loaded into the `Department` navigation property:

  ```
  @Html.DisplayFor(modelItem => item.Department.Name)
  ```

Run the app and select the **Courses** tab to see the list with department names.

## Create an Instructors page

In this section, you'll create a controller and view for the `Instructor` entity in order to display the Instructors page:

This page reads and displays related data in the following ways:

* The list of instructors displays related data from the `OfficeAssignment` entity. The `Instructor` and `OfficeAssignment` entities are in a one-to-zero-or-one relationship. You'll use eager loading for the `OfficeAssignment` entities. As explained earlier, eager loading is typically more efficient when you need the related data for all retrieved rows of the primary table. In this case, you want to display office assignments for all displayed instructors.

* When the user selects an instructor, related `Course` entities are displayed. The `Instructor` and `Course` entities are in a many-to-many relationship. You'll use eager loading for the `Course` entities and their related `Department` entities. In this case, separate queries might be more efficient because you need courses only for the selected instructor. However, this example shows how to use eager loading for navigation properties within entities that are themselves in navigation properties.

* When the user selects a course, related data from the `Enrollments` entity set is displayed. The `Course` and `Enrollment` entities are in a one-to-many relationship. You'll use separate queries for `Enrollment` entities and their related `Student` entities.

### Create a view model for the Instructor Index view

The Instructors page shows data from three different tables. Therefore, you'll create a view model that includes three properties, each holding the data for one of the tables.

In the *SchoolViewModels* folder, create `InstructorIndexData.cs` and replace the existing code with the following code:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}
```

### Create the Instructor controller and views

Create an Instructors controller with EF read/write actions as shown in the following illustration:

Open `InstructorsController.cs` and add a using statement for the ViewModels namespace:

```
using ContosoUniversity.Models.SchoolViewModels;
```

Replace the Index method with the following code to do eager loading of related data and put it in the view model.

```
public async Task<IActionResult> Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Enrollments)
                    .ThenInclude(i => i.Student)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
          .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();
    
    if (id != null)
    {
        ViewData["InstructorID"] = id.Value;
        Instructor instructor = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single();
        viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }

    if (courseID != null)
    {
        ViewData["CourseID"] = courseID.Value;
        viewModel.Enrollments = viewModel.Courses.Where(
            x => x.CourseID == courseID).Single().Enrollments;
    }

    return View(viewModel);
}
```

The method accepts optional route data (`id`) and a query string parameter (`courseID`) that provide the ID values of the selected instructor and selected course. The parameters are provided by the **Select** hyperlinks on the page.

The code begins by creating an instance of the view model and putting in it the list of instructors. The code specifies eager loading for the `Instructor.OfficeAssignment` and the `Instructor.CourseAssignments` navigation properties. Within the `CourseAssignments` property, the `Course` property is loaded, and within that, the `Enrollments` and `Department` properties are loaded, and within each `Enrollment` entity the `Student` property is loaded.

```
viewModel.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();
```

Since the view always requires the `OfficeAssignment` entity, it's more efficient to fetch that in the same query. Course entities are required when an instructor is selected in the web page, so a single query is better than multiple queries only if the page is displayed more often with a course selected than without.

The code repeats `CourseAssignments` and `Course` because you need two properties from `Course`. The first string of `ThenInclude` calls gets `CourseAssignment.Course`, `Course.Enrollments`, and `Enrollment.Student`.

You can read more about including multiple levels of related data [here.](/en-us/ef/core/querying/related-data/eager#including-multiple-levels)

```
viewModel.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();
```

At that point in the code, another `ThenInclude` would be for navigation properties of `Student`, which you don't need. But calling `Include` starts over with `Instructor` properties, so you have to go through the chain again, this time specifying `Course.Department` instead of `Course.Enrollments`.

```
viewModel.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();
```

The following code executes when an instructor was selected. The selected instructor is retrieved from the list of instructors in the view model. The view model's `Courses` property is then loaded with the `Course` entities from that instructor's `CourseAssignments` navigation property.

```
if (id != null)
{
    ViewData["InstructorID"] = id.Value;
    Instructor instructor = viewModel.Instructors.Where(
        i => i.ID == id.Value).Single();
    viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
```

The `Where` method returns a collection, but in this case the criteria passed to that method result in only a single Instructor entity being returned. The `Single` method converts the collection into a single `Instructor` entity, which gives you access to that entity's `CourseAssignments` property. The `CourseAssignments` property contains `CourseAssignment` entities, from which you want only the related `Course` entities.

You use the `Single` method on a collection when you know the collection will have only one item. The `Single` method throws an exception if the collection passed to it's empty or if there's more than one item. An alternative is `SingleOrDefault`, which returns a default value (null in this case) if the collection is empty. However, in this case that would still result in an exception (from trying to find a `Courses` property on a null reference), and the exception message would less clearly indicate the cause of the problem. When you call the `Single` method, you can also pass in the Where condition instead of calling the `Where` method separately:

```
.Single(i => i.ID == id.Value)
```

Instead of:

```
.Where(i => i.ID == id.Value).Single()
```

Next, if a course was selected, the selected course is retrieved from the list of courses in the view model. Then the view model's `Enrollments` property is loaded with the Enrollment entities from that course's `Enrollments` navigation property.

```
if (courseID != null)
{
    ViewData["CourseID"] = courseID.Value;
    viewModel.Enrollments = viewModel.Courses.Where(
        x => x.CourseID == courseID).Single().Enrollments;
}
```

### Tracking vs no-tracking

No-tracking queries are useful when the results are used in a read-only scenario. They're generally quicker to execute because there's no need to set up the change tracking information. If the entities retrieved from the database don't need to be updated, then a no-tracking query is likely to perform better than a tracking query.

In some cases a tracking query is more efficient than a no-tracking query. For more information, see [Tracking vs. No-Tracking Queries](/en-us/ef/core/querying/tracking).

### Modify the Instructor Index view

In `Views/Instructors/Index.cshtml`, replace the template code with the following code. The changes are highlighted.

```
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Instructors)
        {
            string selectedRow = "";
            if (item.ID == (int?)ViewData["InstructorID"])
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @foreach (var course in item.CourseAssignments)
                    {
                        @course.Course.CourseID @course.Course.Title <br />
                    }
                </td>
                <td>
                    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
           }
    </tbody>
</table>
```

```
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Instructors)
        {
            string selectedRow = "";
            if (item.ID == (int?)ViewData["InstructorID"])
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @foreach (var course in item.CourseAssignments)
                    {
                        @course.Course.CourseID @course.Course.Title <br />
                    }
                </td>
                <td>
                    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
           }
    </tbody>
</table>
```

You've made the following changes to the existing code:

* Changed the model class to `InstructorIndexData`.

* Changed the page title from **Index** to **Instructors**.

* Added an **Office** column that displays `item.OfficeAssignment.Location` only if `item.OfficeAssignment` isn't null. (Because this is a one-to-zero-or-one relationship, there might not be a related OfficeAssignment entity.)

  ```
  @if (item.OfficeAssignment != null)
  {
      @item.OfficeAssignment.Location
  }
  ```

* Added a **Courses** column that displays courses taught by each instructor. For more information, see the [Explicit line transition](../../mvc/views/razor?view=aspnetcore-10.0#explicit-line-transition) section of the Razor syntax article.

* Added code that conditionally adds a Bootstrap CSS class to the `tr` element of the selected instructor. This class sets a background color for the selected row.

* Added a new hyperlink labeled **Select** immediately before the other links in each row, which causes the selected instructor's ID to be sent to the `Index` method.

  ```
  <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
  ```

Run the app and select the **Instructors** tab. The page displays the Location property of related OfficeAssignment entities and an empty table cell when there's no related OfficeAssignment entity.

In the `Views/Instructors/Index.cshtml` file, after the closing table element (at the end of the file), add the following code. This code displays a list of courses related to an instructor when an instructor is selected.

```

@if (Model.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == (int?)ViewData["CourseID"])
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}
```

This code reads the `Courses` property of the view model to display a list of courses. It also provides a **Select** hyperlink that sends the ID of the selected course to the `Index` action method.

Refresh the page and select an instructor. Now you see a grid that displays courses assigned to the selected instructor, and for each course you see the name of the assigned department.

After the code block you just added, add the following code. This displays a list of the students who are enrolled in a course when that course is selected.

```
@if (Model.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}
```

This code reads the `Enrollments` property of the view model in order to display a list of students enrolled in the course.

Refresh the page again and select an instructor. Then select a course to see the list of enrolled students and their grades.

## About explicit loading

When you retrieved the list of instructors in `InstructorsController.cs`, you specified eager loading for the `CourseAssignments` navigation property.

Suppose you expected users to only rarely want to see enrollments in a selected instructor and course. In that case, you might want to load the enrollment data only if it's requested. To see an example of how to do explicit loading, replace the `Index` method with the following code, which removes eager loading for `Enrollments` and loads that property explicitly. The code changes are highlighted.

```
public async Task<IActionResult> Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
          .OrderBy(i => i.LastName)
          .ToListAsync();

    if (id != null)
    {
        ViewData["InstructorID"] = id.Value;
        Instructor instructor = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single();
        viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }

    if (courseID != null)
    {
        ViewData["CourseID"] = courseID.Value;
        var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
        await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
            await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
        }
        viewModel.Enrollments = selectedCourse.Enrollments;
    }

    return View(viewModel);
}
```

The new code drops the `ThenInclude` method calls for enrollment data from the code that retrieves instructor entities. It also drops `AsNoTracking`. If an instructor and course are selected, the highlighted code retrieves `Enrollment` entities for the selected course, and `Student` entities for each `Enrollment`.

Run the app, go to the Instructors Index page now and you'll see no difference in what's displayed on the page, although you've changed how the data is retrieved.

## Get the code

[Download or view the completed application.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final)

## Next steps

In this tutorial, you:

* Learned how to load related data
* Created a Courses page
* Created an Instructors page
* Learned about explicit loading

Advance to the next tutorial to learn how to update related data.

[Update related data](update-related-data?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-04-10

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/yarp-overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/yarp-overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Overview of YARP

## Introduction to YARP

YARP (Yet Another Reverse Proxy) is a highly customizable reverse proxy library for .NET. It's designed to provide a robust, flexible, scalable, secure, and easy to use proxy framework. YARP helps developers create powerful and efficient reverse proxy solutions tailored to their specific needs.

## What a reverse proxy does

A reverse proxy is a server that sits between client devices and backend servers. It forwards client requests to the appropriate backend server and then returns the server's response to the client. A reverse proxy provides several benefits:

* Routing: Directs requests to different backend servers based on predefined rules, such as URL patterns or request headers. For example, `/images`, `/api`, and `/db` requests can be routed image, api, and database servers.

* Load Balancing: Distributes incoming traffic across multiple backend servers to prevent overloading a specific server. Distribution increases performance and reliability.

* Scalability: By distributing traffic across multiple servers, a reverse proxy helps scale apps to handle more users and higher loads. Backend servers scaled (added or removed) without impacting the client.

* [SSL/TLS Termination](/en-us/azure/application-gateway/ssl-overview): Offloads the TLS encryption and decryption processes from backend servers, reducing their workload.

* Connection abstraction, decoupling and control over URL-space: Inbound requests from external clients and outbound responses from the backend are independent. This independence allows for different:

  * Versions of HTTP, ie, HTTP/1.1, HTTP/2, HTTP/3. The proxy can upgrade or downgrade HTTP versions.
  * Connection lifetimes, which enables having long-lived connections on the backend while maintaining short client connections.
  * Control Over URL-Space: Incoming URLs can be transformed before forwarding to the backend. This abstracts the external URLs from how they are mapped to internal services. Internal service endpoints can change without affecting external URLs.

* Security: Internal service endpoints can be hidden from external exposure, protecting against some types of cyber attacks such as [DDoS attacks](https://www.microsoft.com/security/business/security-101/what-is-a-ddos-attack?msockid=3e35ed3aa4666d8003aaf830a5006c74).

* Caching: Frequently requested resources can be cached to reduce the load on backend servers and improve response times.

* Versioning: Different versions of an API can be supported using different URL mappings.

* Simplified maintenance: Reverse proxies can handle [SSL/TLS Termination](/en-us/azure/application-gateway/ssl-overview) and other tasks, simplifying the configuration and maintenance of backend servers. For example, SSL certificates and security policies can be managed at the reverse proxy level instead of on each individual server.

## How a reverse proxy handles HTTP

A reverse proxy handles HTTP requests and responses in the following manner:

* Receiving requests: The reverse proxy listens on specified ports and endpoints for incoming HTTP requests from clients.
* Terminating Connections: The inbound HTTP connections are terminated at the proxy and new connections are used for outbound requests to destinations.
* Routing requests: Based on predefined routing rules and configurations, the reverse proxy determines which backend server, or cluster of servers, should handle the request.
* Forwarding requests: The reverse proxy forwards the client request to the appropriate backend server, transforming the path and headers as necessary.
* Connection pooling: The outbound connections are pooled to reduce connection overhead and make most use of HTTP 1.1 reuse and parallel requests with HTTP/2 and HTTP/3.
* Processing responses: The backend server processes the request and sends a response back to the reverse proxy.
* Returning responses: The reverse proxy receives the response from the backend server and forwards it back to the client, performing any necessary response transforms.

This process ensures that the client interacts with the reverse proxy rather than directly with the backend servers, providing the benefits of load balancing, security, versioning, and more.

## Why choose YARP over other proxies

YARP offers several unique benefits that make it an attractive choice for developers:

* Customization: YARP is highly customizable, allowing developers to tailor the proxy to their specific needs with minimal effort.
* Integration with .NET: Built on ASP.NET Core, YARP seamlessly integrates with the .NET ecosystem, making it an ideal choice for .NET developers.
* Extensibility: YARP provides a rich set of extensibility points, enabling developers to add custom logic and features as needed, using familiar C# code.
* Scalability: The direct forwarding extensibility option enables YARP to scale to support domain name and backend scaling that are not viable with most reverse proxies.
* Active development: [YARP is actively maintained](https://github.com/dotnet/yarp) and developed by Microsoft, ensuring it stays up-to-date with the latest technologies and best practices.
* Comprehensive maintained documentation: YARP comes with extensive documentation and examples, making it easy to get started and implement advanced features.
* [Open source](https://github.com/dotnet/yarp). YARP and the YARP documentation are open source. [Contributions](https://github.com/dotnet/yarp/blob/main/README.md), reviews, and feedback are welcome.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/min-web-api.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Create a Minimal API with ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Wade Pickett](https://github.com/wadepickett) and [Tom Dykstra](https://github.com/tdykstra)

Minimal APIs are designed to create HTTP APIs with minimal dependencies. They're ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core.

This tutorial teaches the basics of building a Minimal API with ASP.NET Core. Another approach to creating APIs in ASP.NET Core is to use controllers. For help with choosing between Minimal APIs and controller-based APIs, see [APIs overview](../fundamentals/apis?view=aspnetcore-10.0). For a tutorial on creating an API project based on [controllers](../web-api/?view=aspnetcore-10.0) that contains more features, see [Create a web API](first-web-api?view=aspnetcore-10.0).

## Overview

This tutorial creates the following API:

| API                          | Description                | Request body       | Response body        |
| ---------------------------- | -------------------------- | ------------------ | -------------------- |
| `GET /todoitems`             | Get all to-do items        | None               | Array of to-do items |
| `GET /todoitems/complete`    | Get completed to-do items  | None               | Array of to-do items |
| `GET /todoitems/{id}`        | Get an item by ID          | None               | To-do item           |
| `POST /todoitems`            | Add a new item             | To-do item         | To-do item           |
| `PUT /todoitems/{id}`        | Update an existing item    | To-do item         | None                 |
| `PATCH /todoitems/{id}`      | Partially update an item   | Partial to-do item | None                 |
| `DELETE /todoitems/{id}`     | Delete an item             | None               | None                 |

## Create an API project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio 2026 and select **Create a new project**.

- In the **Create a new project** dialog:

  * Select the **ASP.NET Core Web API** project type, and select **Next**.
  * Name the project *TodoApi*, and select **Next**.

- In the **Additional information** dialog:

  * Confirm the **Framework** is **.NET 10.0 (Long Term Support)**.
  * Confirm the checkbox for **Enable OpenAPI support** is checked.
  * Confirm the checkbox for **Use controllers** is **not** checked. Uncheck this setting to create a Minimal API project as required for this tutorial rather than a controller-based one.
  * Select **Create**.

* Start Visual Studio Code, select **View**, and then select **Terminal** to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).
* Change directory (`cd`) to the folder where you want to create the project.
* Run the following commands:

```
  dotnet new webapi -o TodoApi
  code -r TodoApi
```

* When a dialog box asks if you want to trust the authors, select **Yes**.

* When a dialog box asks if you want to add required assets to the project, select **Yes**.

  The preceding commands create a new web API project and open it in Visual Studio Code.

### Examine the code

The `Program.cs` file generated by the template contains the following code:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

```
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

```
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

The preceding code:

* Creates a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) and a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) with preconfigured defaults.
* Registers the in-box OpenAPI document generator by using `builder.Services.AddOpenApi()`.
* Maps the generated OpenAPI document at `/openapi/v1.json` by using `app.MapOpenApi()`, in the Development environment only.
* Defines a sample `GET /weatherforecast` endpoint that returns five randomly generated `WeatherForecast` records.

In this tutorial, you replace the `WeatherForecast` with a new Todo sample, with endpoints to create, read, update, and delete items, backed by a model and a database.

## Add NuGet packages

Add NuGet packages to support the database used in this tutorial.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

- From the **Tools** menu, select **NuGet Package Manager > Manage NuGet Packages for Solution**.
- Select the **Browse** tab.
- Enter **Microsoft.EntityFrameworkCore.InMemory** in the search box, and then select `Microsoft.EntityFrameworkCore.InMemory`.
- Select the **Project** checkbox in the right pane and then select **Install**.

* Run the following command:

```
  dotnet add package Microsoft.EntityFrameworkCore.InMemory
```

[]()

## The model and database context classes

* In the project folder, create a file named `Todo.cs` with the following code:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}
```

The preceding code creates the model for this app. A *model* is a class that represents data that the app manages.

The `Secret` property is included to demonstrate a common real-world need: data the app stores and uses internally, such as the ID of the user who owns the item, that you don't want clients to see or set. In the [Prevent over-posting](#prevent-over-posting) step later in this tutorial, you use a Data Transfer Object (DTO) to keep fields like `Secret` out of the API's input and responses.

* Create a file named `TodoDb.cs` with the following code:

```
using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}
```

The preceding code defines the *database context*, which is the main class that coordinates [Entity Framework](/en-us/ef/core/) functionality for a data model. This class derives from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.

The app uses an in-memory database named `TodoList`. The database is registered later in `Program.cs` with `builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));`, where the string `"TodoList"` is the database name. Because the data is stored in memory, it's reset every time the app restarts.

## Replace the `WeatherForecast` sample with the Todo API

The `webapi` template adds a sample `GET /weatherforecast` endpoint to `Program.cs` and a `WeatherForecast` record at the bottom of the file. The sample is just a placeholder to show the template works - replace both with the Todo endpoints described in the following section.

* Replace all of the code in `Program.cs` with the following. The `WeatherForecast` sample endpoint and record are removed, and the Todo endpoints take their place:

- [Visual Studio](#tabpanel_5_visual-studio)
- [Visual Studio Code](#tabpanel_5_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

The pasted code also includes `using Scalar.AspNetCore;` and `app.MapScalarApiReference();`, which add a browser UI for testing the API. You configure the Scalar package for these lines later in this tutorial, in [Add a browser UI to view the OpenAPI document](#add-a-browser-ui-to-view-the-openapi-document). The project won't build until then.

The OpenAPI lines that the template adds (`builder.Services.AddOpenApi()` and `app.MapOpenApi()`) stay in place. They now describe the Todo endpoints instead of the sample weather endpoint.

The following highlighted code registers the app's services in the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container: the database context (`AddDbContext`) and the OpenAPI document generator (`AddOpenApi`):

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();
```

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();
```

The DI container provides access to the database context and other services.

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

This tutorial uses [Endpoints Explorer and .http files](../test/http-files?view=aspnetcore-10.0#use-endpoints-explorer) to test the API.

[]()

### Add a browser UI to view the OpenAPI document

The template already generates the OpenAPI document at `/openapi/v1.json`. To explore and test the API from a browser, add a UI that consumes that document. This tutorial uses Scalar.

Add the [Scalar.AspNetCore](https://www.nuget.org/packages/Scalar.AspNetCore/) package, which provides the middleware that serves the UI:

* Run the following command:

```
  dotnet add package Scalar.AspNetCore
```

In this tutorial, Scalar is used only for its API reference middleware, which reads the OpenAPI document generated by the AddOpenApi and MapOpenApi calls.

Confirm that `Program.cs` includes the highlighted `MapScalarApiReference` line inside the `if (app.Environment.IsDevelopment())` block, right after the `app.MapOpenApi();` line, and that `using Scalar.AspNetCore;` is at the top of the file. You added these lines when you pasted the Todo code earlier.

```
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}
```

The preceding code enables the Scalar UI in Development only. It reads `/openapi/v1.json`, the document produced by `MapOpenApi()`, and serves the API reference at `/scalar/v1`. Be sure to add `using Scalar.AspNetCore;` to the top of the file.

Note

Scalar is one of several options for an OpenAPI UI. Alternatives such as `NSwag.AspNetCore` or `Swashbuckle.AspNetCore.SwaggerUi` consume the same OpenAPI document and can be substituted for Scalar without changing the rest of the tutorial.

[]()

## Test posting data

The following code in `Program.cs` creates an HTTP POST endpoint `/todoitems` that adds data to the in-memory database:

* [Visual Studio](#tabpanel_8_visual-studio)
* [Visual Studio Code](#tabpanel_8_visual-studio-code)

```
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});
```

```
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});
```

* [Visual Studio](#tabpanel_9_visual-studio)
* [Visual Studio Code](#tabpanel_9_visual-studio-code)

- Press `Ctrl`+`F5` to run the app without debugging. Visual Studio launches the [Kestrel web server](../fundamentals/servers/kestrel?view=aspnetcore-10.0) and trusts the development certificate if needed.

Visual Studio displays the following dialog:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Use the POST endpoint to add data to the app.

* Select **View** > **Other Windows** > **Endpoints Explorer**.

* Right-click the **POST** endpoint and select **Generate request**.

  A new file is created in the project folder named `TodoApi.http`, with contents similar to the following example:

```
  @TodoApi_HostAddress = https://localhost:7031

  POST {{TodoApi_HostAddress}}/todoitems

  ###
```

* The first line creates a variable that is used for all of the endpoints.

* The next line defines a POST request.

* The triple hashtag (`###`) line is a request delimiter: what comes after it is for a different request.

* The POST request needs headers and a body. To define those parts of the request, add the following lines immediately after the POST request line:

```
  Content-Type: application/json

  {
    "name":"walk dog",
    "isComplete":true
  }
```

The preceding code adds a Content-Type header and a JSON request body. The TodoApi.http file should now look like the following example, but with your port number:

```
  @TodoApi_HostAddress = https://localhost:7057

  POST {{TodoApi_HostAddress}}/todoitems
  Content-Type: application/json

  {
    "name":"walk dog",
    "isComplete":true
  }

  ###
```

* Select the **Send request** link that is above the `POST` request line.

  The POST request is sent to the app and the response is displayed in the **Response** pane.

- Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

- Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

* In Visual Studio Code, press `Ctrl`+`F5` (Windows) or `control`+`F5` (macOS) to run the app without debugging.

* You're prompted to select a debugger. Select **C#**.

* You're prompted to select a launch configuration. Select **C#:TodoApi \[Default Configuration] TodoApi**

The `TodoApi` app starts and listens on a randomly assigned local port. In the **Terminal** panel, find the `Now listening on:` line to get the URL and port, for example:

```
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5090
```

In this example, the `TodoApi` app is listening at `http://localhost:5090`, so the port is `5090`. Your port number is likely different.

* To open the app in your default browser, hold `Ctrl` (Windows) or `Cmd` (macOS) and select the URL on the `Now listening on:` line in the **Terminal** panel. The browser opens at the app's root, `http://localhost:{port}`.

* In the browser address bar, append `/scalar/v1` to the URL to display the API reference page generated by Scalar, for example `http://localhost:5090/scalar/v1`.

* On the Scalar page, select the **POST /todoitems** operation, and then select **Test Request**.

* In the request body, enter JSON for a to-do item without specifying the optional `id`:

```
  {
    "name":"walk dog",
    "isComplete":true
  }
```

* Select **Send**.

The Scalar UI displays the response, including the status code and response body. The response body shows:

* The `id` is set to `1`.
* A 201 `HTTP` status code is returned, which indicates that the request was successfully processed and resulted in the creation of a new resource.

## Examine the GET endpoints

Your `Program.cs` includes several GET endpoints, defined with `MapGet`:

| API                       | Description                   | Request body | Response body        |
| ------------------------- | ----------------------------- | ------------ | -------------------- |
| `GET /todoitems`          | Get all to-do items           | None         | Array of to-do items |
| `GET /todoitems/complete` | Get all completed to-do items | None         | Array of to-do items |
| `GET /todoitems/{id}`     | Get an item by ID             | None         | To-do item           |

```
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());
```

## Test the GET endpoints

* [Visual Studio](#tabpanel_10_visual-studio)
* [Visual Studio Code](#tabpanel_10_visual-studio-code)

Test the app by calling the `GET` endpoints from a browser or by using **Endpoints Explorer**. The following steps are for **Endpoints Explorer**.

* In **Endpoints Explorer**, right-click the first **GET** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

```
  GET {{TodoApi_HostAddress}}/todoitems

  ###
```

* Select the **Send request** link that is above the new `GET` request line.

  The GET request is sent to the app and the response is displayed in the **Response** pane.

* The response body is similar to the following JSON:

```
  [
    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true,
      "secret": null
    }
  ]
```

* In **Endpoints Explorer**, right-click the `/todoitems/{id}` **GET** endpoint and select **Generate request**. The following content is added to the `TodoApi.http` file:

```
  GET {{TodoApi_HostAddress}}/todoitems/{id}

  ###
```

* Replace `{id}` with `1`.

* Select the **Send request** link that is above the new GET request line.

  The GET request is sent to the app and the response is displayed in the **Response** pane.

* The response body is similar to the following JSON:

```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true,
    "secret": null
  }
```

Test the app by calling the endpoints from a browser or the Scalar UI.

* In the Scalar UI, select **GET /todoitems**, and then select **Test Request** > **Send**.

* Alternatively, call **GET /todoitems** from a browser by entering the URI `http://localhost:{port}/todoitems`. For example, `http://localhost:7032/todoitems`.

The call to `GET /todoitems` produces a response similar to the following:

```
[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true,
    "secret": null
  }
]
```

* Call **GET /todoitems/{id}** in Scalar to return data from a specific id:

  * Select **GET /todoitems/{id}**, and then select **Test Request**.
  * Set the **id** field to `1` and select **Send**.

* Alternatively, call **GET /todoitems/{id}** from a browser by entering the URI `http://localhost:{port}/todoitems/1`. For example, `http://localhost:7032/todoitems/1`.

* The response is similar to the following:

```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true,
    "secret": null
  }
```

This app uses an in-memory database. If you restart the app, the data is lost: `GET /todoitems` returns an empty array (`[]`), and `GET /todoitems/{id}` returns a `404 Not Found`. To repopulate the app, [POST](#post) data to the app and try the GET request again.

## Return values

ASP.NET Core automatically serializes the object to [JSON](https://www.json.org) and writes the JSON into the body of the response message. The response code for this return type is [200 OK](https://developer.mozilla.org/docs/Web/HTTP/Status/200), assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.

The return types can represent a wide range of HTTP status codes. For example, `GET /todoitems/{id}` can return two different status values:

* If no item matches the requested ID, the method returns a [404 status](https://developer.mozilla.org/docs/Web/HTTP/Status/404) [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) error code.
* Otherwise, the method returns 200 with a JSON response body. Returning `item` results in an HTTP 200 response.

## Examine the PUT endpoint

The sample app implements a single PUT endpoint using `MapPut`:

* [Visual Studio](#tabpanel_11_visual-studio)
* [Visual Studio Code](#tabpanel_11_visual-studio-code)

```
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

```
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

This method is similar to the `MapPost` method, except it uses HTTP PUT. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use [HTTP PATCH](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute).

## Test the PUT endpoint

This sample uses an in-memory database that you must initialize each time you start the app. You need an item in the database before you make a PUT call. Call POST to ensure there's an item in the database before making a PUT call.

Update the Todo item that has `Id = 1` and set its name to `"feed fish"`.

* [Visual Studio](#tabpanel_12_visual-studio)
* [Visual Studio Code](#tabpanel_12_visual-studio-code)

- In **Endpoints Explorer**, right-click the **PUT** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

```
  PUT {{TodoApi_HostAddress}}/todoitems/{id}

  ###
```

* In the PUT request line, replace `{id}` with `1`.

* Add the following lines immediately after the PUT request line:

```
  Content-Type: application/json

  {
    "id": 1,
    "name": "feed fish",
    "isComplete": false
  }
```

The preceding code adds a Content-Type header and a JSON request body.

* Select the **Send request** link that is above the new PUT request line.

Use the Scalar UI to send a PUT request:

* Select **PUT /todoitems/{id}**, and then select **Test Request**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

```
  {
    "id": 1,
    "name": "feed fish",
    "isComplete": false
  }
```

* Select **Send**.

The PUT request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

## Create and examine the PATCH endpoint

A PATCH endpoint lets clients send only the fields they want to update, such as renaming a Todo item without resending its completion status. This approach differs from a PUT request, which replaces the entire item, so the client must send every field even if only one is changed.

The next steps add a new file and modify the `Program.cs` file. Stop the `TodoApi` app before making these changes. Leave the Scalar page in the browser running.

* [Visual Studio](#tabpanel_13_visual-studio)
* [Visual Studio Code](#tabpanel_13_visual-studio-code)

- To stop the app, select the **Stop** button (the red square) in the Visual Studio toolbar.

* To stop the app, press `Shift`+`F5`.

This sample uses an in-memory database that you must initialize each time the app starts. The database must contain an item before you make a PATCH call. Call POST to ensure the database has an item before making a PATCH call.

The PATCH endpoint uses a `TodoPatchDto` class with nullable properties to properly handle partial updates. By using nullable properties, the endpoint can distinguish between a field that wasn't provided (null) and a field explicitly set to a value (including false for boolean fields). Without nullable properties, a non-nullable bool defaults to false, which could potentially overwrite an existing true value when that field isn't included in the request.

* Create a file named `TodoPatchDto.cs` with the following code:

```
public class TodoPatchDto
{
    public string? Name { get; set; }
    public bool? IsComplete { get; set; }
}
```

The `TodoPatchDto` class uses nullable properties (`string?` and `bool?`) to distinguish between a field that wasn't provided in the request versus a field explicitly set to a value.

* In `Program.cs`, add the following PATCH endpoint immediately after the `MapPut` endpoint:

- [Visual Studio](#tabpanel_14_visual-studio)
- [Visual Studio Code](#tabpanel_14_visual-studio-code)

```
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

```
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

This method is similar to the `MapPut` method, but it uses HTTP PATCH and only updates the fields provided in the request. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204).

Note

PATCH operations allow partial updates to resources. For more advanced partial updates using JSON Patch documents, see [JsonPatch in ASP.NET Core web API](../web-api/jsonpatch?view=aspnetcore-10.0).

## Test the PATCH endpoint

* [Visual Studio](#tabpanel_15_visual-studio)
* [Visual Studio Code](#tabpanel_15_visual-studio-code)

- Press `Ctrl`+`F5` to rebuild and run the app with the new PATCH endpoint.

* Run the app again so it rebuilds with the new PATCH endpoint. In Visual Studio Code, press `Ctrl`+`F5` (Windows) or `control`+`F5` (macOS) to run the app without debugging.

This sample uses an in-memory database that you must initialize each time the app starts. The database must contain an item before you make a PATCH call. Call POST to ensure the database has an item before making a PATCH call.

Update only the `name` property of the Todo item that has `Id = 1` and set its name to `"run errands"`.

* [Visual Studio](#tabpanel_16_visual-studio)
* [Visual Studio Code](#tabpanel_16_visual-studio-code)

- In **Endpoints Explorer**, select the refresh button. Then, right-click the **PATCH** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

```
  PATCH {{TodoApi_HostAddress}}/todoitems/{id}

  ###
```

* In the PATCH request line, replace `{id}` with `1`.

* Add the following lines immediately after the PATCH request line:

```
  Content-Type: application/json

  {
    "name": "run errands"
  }
```

The preceding code adds a Content-Type header and a JSON request body with only the field to update.

* Select the **Send request** link that is above the new PATCH request line.

Use the Scalar UI to send a PATCH request:

* Refresh the scalar page in the browser so that the new `PATCH` endpoint appears.

* Select **PATCH /todoitems/{id}**, and then select **Test Request**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

```
  {
    "name": "run errands"
  }
```

* Select **Send**.

The PATCH request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

## Examine the DELETE endpoint

Your `Program.cs` file includes a single DELETE endpoint, defined with `MapDelete`:

* [Visual Studio](#tabpanel_17_visual-studio)
* [Visual Studio Code](#tabpanel_17_visual-studio-code)

```
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});
```

```
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});
```

## Test the DELETE endpoint

* [Visual Studio](#tabpanel_18_visual-studio)
* [Visual Studio Code](#tabpanel_18_visual-studio-code)

- In **Endpoints Explorer**, right-click the **DELETE** endpoint and select **Generate request**.

  A DELETE request is added to `TodoApi.http`.

- Replace `{id}` in the DELETE request line with `1`. The DELETE request should look like the following example:

```
  DELETE {{TodoApi_HostAddress}}/todoitems/1

  ###
```

* Select the **Send request** link for the DELETE request.

Use the Scalar UI to send a DELETE request:

* Select **DELETE /todoitems/{id}**, then **Test Request**.
* Set the **id** field to `1` and select **Send**.

The DELETE request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

## Use the MapGroup API

* The next steps modify the `Program.cs` file, so stop the app before making these changes. Leave the scalar page in the browser running.

- [Visual Studio](#tabpanel_19_visual-studio)
- [Visual Studio Code](#tabpanel_19_visual-studio-code)

* To stop the app, select the **Stop** button (the red square) in the Visual Studio toolbar.

- To stop the app, press `Shift`+`F5`.

The `Program.cs` file you wrote repeats the `todoitems` URL prefix each time it sets up an endpoint. APIs often have groups of endpoints with a common URL prefix, and the [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) method helps organize such groups. It reduces repetitive code and allows you to customize entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata).

* Replace the contents of `Program.cs` with the following code:

- [Visual Studio](#tabpanel_20_visual-studio)
- [Visual Studio Code](#tabpanel_20_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapPatch("/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapPatch("/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

The preceding code has the following changes:

* Adds `var todoItems = app.MapGroup("/todoitems");` to set up the group using the URL prefix `/todoitems`.

* Changes all the `app.Map<HttpVerb>` methods to `todoItems.Map<HttpVerb>`.

* Removes the URL prefix `/todoitems` from the `Map<HttpVerb>` method calls.

* Runs the app and tests the endpoints to verify that they work the same.

## Use the TypedResults API

The next steps modify the `Program.cs` file, so stop the app before making these changes. Leave the scalar page in the browser running.

Returning [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) rather than [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) has several advantages, including testability and automatically returning the response type metadata for OpenAPI to describe the endpoint. For more information, see [`TypedResults` versus `Results`](../fundamentals/minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

* The `Map<HttpVerb>` methods can call route handler methods instead of using lambdas. To see an example, update *Program.cs* with the following code:

- [Visual Studio](#tabpanel_21_visual-studio)
- [Visual Studio Code](#tabpanel_21_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

```
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

The `Map<HttpVerb>` code now calls methods instead of lambdas:

```
var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
```

The following methods return objects that implement [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) and are defined by [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults):

```
static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

Unit tests can call these methods and test that they return the correct type. For example, if the method is `GetAllTodos`:

```
static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
```

Unit test code can verify that an object of type [Ok\<Todo\[\]>](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.ok-1.value#microsoft-aspnetcore-http-httpresults-ok-1-value) is returned from the handler method. For example:

```
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}
```

[]()

## Prevent over-posting

Currently, your API exposes the entire `Todo` object, including the `Secret` property you added in [The model and database context classes](#model-db-classes). In production applications, use a subset of the model to restrict the data that clients can input and receive. Security is a major reason for this restriction. This subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. This article uses **DTO**.

Use a DTO to:

* Prevent over-posting.
* Hide properties that clients aren't supposed to view.
* Omit some properties to reduce payload size.
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.

This app needs to hide the `Secret` field, but an administrative app could choose to expose it.

* The next steps add a file, so stop the app before making these changes. Leave the scalar page in the browser running.

* Create a file named `TodoItemDTO.cs` with the following code:

```
public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
```

* Replace the contents of the `Program.cs` file with the following code to use this DTO model:

- [Visual Studio](#tabpanel_22_visual-studio)
- [Visual Studio Code](#tabpanel_22_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

```
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

* Run the app and verify you can post and get all fields except the `Secret` field.

[]()

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/min-web-api/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

* [Configure JSON serialization options](../fundamentals/minimal-apis/responses?view=aspnetcore-10.0#configure-json-serialization-options).
* Handle errors and exceptions: The [developer exception page](../fundamentals/error-handling-api?view=aspnetcore-10.0#developer-exception-page) is enabled by default in the `Development` environment for Minimal API apps. For information about how to handle errors and exceptions, see [Handle errors in ASP.NET Core APIs](../fundamentals/error-handling-api?view=aspnetcore-10.0).
* For an example of testing a Minimal API app, see [this GitHub sample](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/MinApiTestsSample).
* [OpenAPI support in Minimal APIs](../fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0).
* [Quickstart: Publish to Azure](/en-us/azure/app-service/quickstart-dotnetcore).
* [Organizing ASP.NET Core Minimal APIs](https://www.tessferrandez.com/blog/2023/10/31/organizing-minimal-apis.html).

### Learn more

See [Minimal APIs quick reference](../fundamentals/minimal-apis?view=aspnetcore-10.0)

Minimal APIs are architected to create HTTP APIs with minimal dependencies. They are ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core.

This tutorial teaches the basics of building a Minimal API with ASP.NET Core. Another approach to creating APIs in ASP.NET Core is to use controllers. For help in choosing between Minimal APIs and controller-based APIs, see [APIs overview](../fundamentals/apis?view=aspnetcore-10.0). For a tutorial on creating an API project based on [controllers](../web-api/?view=aspnetcore-10.0) that contains more features, see [Create a web API](first-web-api?view=aspnetcore-10.0).

## Overview

This tutorial creates the following API:

| API                          | Description                | Request body       | Response body        |
| ---------------------------- | -------------------------- | ------------------ | -------------------- |
| `GET /todoitems`             | Get all to-do items        | None               | Array of to-do items |
| `GET /todoitems/complete`    | Get completed to-do items  | None               | Array of to-do items |
| `GET /todoitems/{id}`        | Get an item by ID          | None               | To-do item           |
| `POST /todoitems`            | Add a new item             | To-do item         | To-do item           |
| `PUT /todoitems/{id}`        | Update an existing item    | To-do item         | None                 |
| `PATCH /todoitems/{id}`      | Partially update an item   | Partial to-do item | None                 |
| `DELETE /todoitems/{id}`     | Delete an item             | None               | None                 |

## Prerequisites

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

- [Visual Studio 2022](https://visualstudio.microsoft.com/vs/#download) with the **ASP.NET and web development** workload.

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [.NET 7 SDK](https://dotnet.microsoft.com/download/dotnet/7.0)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use something other than Visual Studio Code.

## Create an API project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio 2022 and select **Create a new project**.

- In the **Create a new project** dialog:

  * Enter `Empty` in the **Search for templates** search box.
  * Select the **ASP.NET Core Empty** template and select **Next**.

- Name the project *TodoApi* and select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 7.0**
  * Uncheck **Do not use top-level statements**
  * Select **Create**

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to the folder that will contain the project folder.

* Run the following commands:

  ```
  dotnet new web -o TodoApi
  cd TodoApi
  code -r ../TodoApi
  ```

* When a dialog box asks if you want to trust the authors, select **Yes**.

* When a dialog box asks if you want to add required assets to the project, select **Yes**.

  The preceding commands create a new ASP.NET Core project and open it in Visual Studio Code.

### Examine the code

The `Program.cs` file contains the following code:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code:

* Creates a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) and a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) with preconfigured defaults.
* Creates an HTTP GET endpoint `/` that returns `Hello World!`:

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio launches the [Kestrel web server](../fundamentals/servers/kestrel?view=aspnetcore-10.0) and opens a browser window.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Press Ctrl+F5 to run the app. A browser window is opened.

`Hello World!` is displayed in the browser. The `Program.cs` file contains a minimal but complete app.

## Add NuGet packages

NuGet packages must be added to support the database and diagnostics used in this tutorial.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

- From the **Tools** menu, select **NuGet Package Manager > Manage NuGet Packages for Solution**.
- Select the **Browse** tab.
- Enter **Microsoft.EntityFrameworkCore.InMemory** in the search box, and then select `Microsoft.EntityFrameworkCore.InMemory`.
- Select the **Project** checkbox in the right pane.
- In the **Version** drop down select the latest version 7 available, for example `7.0.17`, and then select **Install**.
- Follow the preceding instructions to add the `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` package with the latest version 7 available.

* Run the following commands:

  ```
  dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 7.0.17
  dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore --version 7.0.17
  ```

## The model and database context classes

In the project folder, create a file named `Todo.cs` with the following code:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

The preceding code creates the model for this app. A *model* is a class that represents data that the app manages.

Create a file named `TodoDb.cs` with the following code:

```
using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}
```

The preceding code defines the *database context*, which is the main class that coordinates [Entity Framework](/en-us/ef/core/) functionality for a data model. This class derives from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.

## Add the API code

Replace the contents of the `Program.cs` file with the following code:

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

The following highlighted code adds the database context to the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container and enables displaying database-related exceptions:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
```

The DI container provides access to the database context and other services.

## Create API testing UI with Swagger

There are many available web API testing tools to choose from, and you can follow this tutorial's introductory API test steps with your own preferred tool.

  ```
  dotnet add package NSwag.AspNetCore
  ```

The previous command adds the [NSwag.AspNetCore](https://www.nuget.org/packages/NSwag.AspNetCore/) package, which contains tools to generate Swagger documents and UI.

### Configure Swagger middleware

* Add the following highlighted code before `app` is defined in line `var app = builder.Build();`

  ```
  using Microsoft.EntityFrameworkCore;

  var builder = WebApplication.CreateBuilder(args);
  builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
  builder.Services.AddDatabaseDeveloperPageExceptionFilter();

  builder.Services.AddEndpointsApiExplorer();
  builder.Services.AddOpenApiDocument(config =>
  {
      config.DocumentName = "TodoAPI";
      config.Title = "TodoAPI v1";
      config.Version = "v1";
  });
  var app = builder.Build();
  ```

In the previous code:

* `builder.Services.AddEndpointsApiExplorer();`: Enables the API Explorer, which is a service that provides metadata about the HTTP API. The API Explorer is used by Swagger to generate the Swagger document.

* `builder.Services.AddOpenApiDocument(config => {...});`: Adds the Swagger OpenAPI document generator to the application services and configures it to provide more information about the API, such as its title and version. For information on providing more robust API details, see [Get started with NSwag and ASP.NET Core](getting-started-with-nswag?view=aspnetcore-10.0#customize-api-documentation)

* Add the following highlighted code to the next line after `app` is defined in line `var app = builder.Build();`

  ```
  var app = builder.Build();
  if (app.Environment.IsDevelopment())
  {
      app.UseOpenApi();
      app.UseSwaggerUi(config =>
      {
          config.DocumentTitle = "TodoAPI";
          config.Path = "/swagger";
          config.DocumentPath = "/swagger/{documentName}/swagger.json";
          config.DocExpansion = "list";
      });
  }
  ```

  The previous code enables the Swagger middleware for serving the generated JSON document and the Swagger UI. Swagger is only enabled in a development environment. Enabling Swagger in a production environment could expose potentially sensitive details about the API's structure and implementation.

[]()

## Test posting data

The following code in `Program.cs` creates an HTTP POST endpoint `/todoitems` that adds data to the in-memory database:

```
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});
```

Run the app. The browser displays a 404 error because there's no longer a `/` endpoint.

The POST endpoint will be used to add data to the app.

* With the app still running, in the browser, navigate to `https://localhost:<port>/swagger` to display the API testing page generated by Swagger.

* On the Swagger API testing page, select **Post /todoitems** > **Try it out**.

* Note that the **Request body** field contains a generated example format reflecting the parameters for the API.

* In the request body enter JSON for a to-do item, without specifying the optional `id`:

  ```
  {
    "name":"walk dog",
    "isComplete":true
  }
  ```

* Select **Execute**.

Swagger provides a **Responses** pane below the **Execute** button.

Note a few of the useful details:

* cURL: Swagger provides an example cURL command in Unix/Linux syntax, which can be run at the command line with any bash shell that uses Unix/Linux syntax, including Git Bash from [Git for Windows](https://git-scm.com/downloads).
* Request URL: A simplified representation of the HTTP request made by Swagger UI's JavaScript code for the API call. Actual requests can include details such as headers and query parameters and a request body.
* Server response: Includes the response body and headers. The response body shows the `id` was set to `1`.
* Response Code: A 201 `HTTP` status code was returned, indicating that the request was successfully processed and resulted in the creation of a new resource.

***

## Examine the GET endpoints

The sample app implements several GET endpoints by calling `MapGet`:

| API                       | Description                   | Request body | Response body        |
| ------------------------- | ----------------------------- | ------------ | -------------------- |
| `GET /todoitems`          | Get all to-do items           | None         | Array of to-do items |
| `GET /todoitems/complete` | Get all completed to-do items | None         | Array of to-do items |
| `GET /todoitems/{id}`     | Get an item by ID             | None         | To-do item           |

```
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());
```

## Test the GET endpoints

Test the app by calling the endpoints from a browser or Swagger.

* In Swagger select **GET /todoitems** > **Try it out** > **Execute**.

* Alternatively, call **GET /todoitems** from a browser by entering the URI `http://localhost:<port>/todoitems`. For example, `http://localhost:5001/todoitems`

The call to `GET /todoitems` produces a response similar to the following:

```
[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
```

* Call **GET /todoitems/{id}** in Swagger to return data from a specific id:

  * Select **GET /todoitems** > **Try it out**.
  * Set the **id** field to `1` and select **Execute**.

* Alternatively, call **GET /todoitems** from a browser by entering the URI `https://localhost:<port>/todoitems/1`. For example, `https://localhost:5001/todoitems/1`

* The response is similar to the following:

  ```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
  ```

This app uses an in-memory database. If the app is restarted, the GET request doesn't return any data. If no data is returned, [POST](#post) data to the app and try the GET request again.

## Return values

ASP.NET Core automatically serializes the object to [JSON](https://www.json.org) and writes the JSON into the body of the response message. The response code for this return type is [200 OK](https://developer.mozilla.org/docs/Web/HTTP/Status/200), assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.

The return types can represent a wide range of HTTP status codes. For example, `GET /todoitems/{id}` can return two different status values:

* If no item matches the requested ID, the method returns a [404 status](https://developer.mozilla.org/docs/Web/HTTP/Status/404) [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) error code.
* Otherwise, the method returns 200 with a JSON response body. Returning `item` results in an HTTP 200 response.

## Examine the PUT endpoint

The sample app implements a single PUT endpoint using `MapPut`:

```
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

This method is similar to the `MapPost` method, except it uses HTTP PUT. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use [HTTP PATCH](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute).

## Test the PUT endpoint

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.

Update the to-do item that has `Id = 1` and set its name to `"feed fish"`.

Use Swagger to send a PUT request:

* Select **Put /todoitems/{id}** > **Try it out**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

  ```
  {
    "name": "feed fish",
    "isComplete": false
  }
  ```

* Select **Execute**.

## Examine the PATCH endpoint

Create a file named `TodoPatchDto.cs` with the following code:

```
public class TodoPatchDto
{
    public string? Name { get; set; }
    public bool? IsComplete { get; set; }
}
```

The `TodoPatchDto` class uses nullable properties (`string?` and `bool?`) to distinguish between a field that wasn't provided in the request versus a field explicitly set to a value.

The sample app implements a single PATCH endpoint using `MapPatch`:

```
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

This method is similar to the `MapPut` method, except it uses HTTP PATCH and only updates the fields provided in the request. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PATCH request enables partial updates, allowing clients to send only the fields that need to be changed.

The PATCH endpoint uses a `TodoPatchDto` class with nullable properties to properly handle partial updates. Using nullable properties allows the endpoint to distinguish between a field that wasn't provided (null) versus a field explicitly set to a value (including false for boolean fields). Without nullable properties, a non-nullable bool would default to false, potentially overwriting an existing true value when that field wasn't included in the request.

Note

PATCH operations allow partial updates to resources. For more advanced partial updates using JSON Patch documents, see [JsonPatch in ASP.NET Core web API](../web-api/jsonpatch?view=aspnetcore-10.0).

## Test the PATCH endpoint

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PATCH call. Call GET to ensure there's an item in the database before making a PATCH call.

Update only the `name` property of the to-do item that has `Id = 1` and set its name to `"run errands"`.

Use Swagger to send a PATCH request:

* Select **Patch /todoitems/{id}** > **Try it out**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

  ```
  {
    "name": "run errands"
  }
  ```

* Select **Execute**.

## Examine and test the DELETE endpoint

The sample app implements a single DELETE endpoint using `MapDelete`:

```
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});
```

Use Swagger to send a DELETE request:

* Select **DELETE /todoitems/{id}** > **Try it out**.

* Set the **ID** field to `1` and select **Execute**.

  The DELETE request is sent to the app and the response is displayed in the **Responses** pane. The response body is empty, and the **Server response** status code is 204.

## Use the MapGroup API

The sample app code repeats the `todoitems` URL prefix each time it sets up an endpoint. APIs often have groups of endpoints with a common URL prefix, and the [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) method is available to help organize such groups. It reduces repetitive code and allows for customizing entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata).

Replace the contents of `Program.cs` with the following code:

```
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

The preceding code has the following changes:

* Adds `var todoItems = app.MapGroup("/todoitems");` to set up the group using the URL prefix `/todoitems`.
* Changes all the `app.Map<HttpVerb>` methods to `todoItems.Map<HttpVerb>`.
* Removes the URL prefix `/todoitems` from the `Map<HttpVerb>` method calls.

Test the endpoints to verify that they work the same.

## Use the TypedResults API

Returning [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) rather than [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) has several advantages, including testability and automatically returning the response type metadata for OpenAPI to describe the endpoint. For more information, see [`TypedResults` versus `Results`](../fundamentals/minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

The `Map<HttpVerb>` methods can call route handler methods instead of using lambdas. To see an example, update *Program.cs* with the following code:

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

```
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

The `Map<HttpVerb>` code now calls methods instead of lambdas:

```
var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
```

These methods return objects that implement [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) and are defined by [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults):

```
static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

Unit tests can call these methods and test that they return the correct type. For example, if the method is `GetAllTodos`:

```
static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
```

Unit test code can verify that an object of type [Ok\<Todo\[\]>](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.ok-1.value#microsoft-aspnetcore-http-httpresults-ok-1-value) is returned from the handler method. For example:

```
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}
```

[]()

## Prevent over-posting

Currently the sample app exposes the entire `Todo` object. Production apps In production applications, a subset of the model is often used to restrict the data that can be input and returned. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. **DTO** is used in this article.

A DTO can be used to:

* Prevent over-posting.
* Hide properties that clients aren't supposed to view.
* Omit some properties to reduce payload size.
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.

To demonstrate the DTO approach, update the `Todo` class to include a secret field:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}
```

The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

Verify you can post and get the secret field.

Create a file named `TodoItemDTO.cs` with the following code:

```
public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
```

Replace the contents of the `Program.cs` file with the following code to use this DTO model:

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}


class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}
```

```
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).Select(t => new TodoItemDTO(t)).ToListAsync());


app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

Verify you can post and get all fields except the secret field.

[]()

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/min-web-api/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

* [Configure JSON serialization options](../fundamentals/minimal-apis/responses?view=aspnetcore-10.0#configure-json-serialization-options).
* Handle errors and exceptions: The [developer exception page](../fundamentals/error-handling-api?view=aspnetcore-10.0#developer-exception-page) is enabled by default in the `Development` environment for Minimal API apps. For information about how to handle errors and exceptions, see [Handle errors in ASP.NET Core APIs](../fundamentals/error-handling-api?view=aspnetcore-10.0).
* For an example of testing a Minimal API app, see [this GitHub sample](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/MinApiTestsSample).
* [OpenAPI support in Minimal APIs](../fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0).
* [Quickstart: Publish to Azure](/en-us/azure/app-service/quickstart-dotnetcore).
* [Organizing ASP.NET Core Minimal APIs](https://www.tessferrandez.com/blog/2023/10/31/organizing-minimal-apis.html).

### Learn more

See [Minimal APIs quick reference](../fundamentals/minimal-apis?view=aspnetcore-10.0)

Minimal APIs are architected to create HTTP APIs with minimal dependencies. They are ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core.

This tutorial teaches the basics of building a Minimal API with ASP.NET Core. Another approach to creating APIs in ASP.NET Core is to use controllers. For help in choosing between Minimal APIs and controller-based APIs, see [APIs overview](../fundamentals/apis?view=aspnetcore-10.0). For a tutorial on creating an API project based on [controllers](../web-api/?view=aspnetcore-10.0) that contains more features, see [Create a web API](first-web-api?view=aspnetcore-10.0).

## Overview

This tutorial creates the following API:

| API                          | Description               | Request body | Response body        |
| ---------------------------- | ------------------------- | ------------ | -------------------- |
| `GET /todoitems`             | Get all to-do items       | None         | Array of to-do items |
| `GET /todoitems/complete`    | Get completed to-do items | None         | Array of to-do items |
| `GET /todoitems/{id}`        | Get an item by ID         | None         | To-do item           |
| `POST /todoitems`            | Add a new item            | To-do item   | To-do item           |
| `PUT /todoitems/{id}`        | Update an existing item   | To-do item   | None                 |
| `DELETE /todoitems/{id}`     | Delete an item            | None         | None                 |

## Prerequisites

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

- [Visual Studio 2022](https://visualstudio.microsoft.com/vs/#download) with the **ASP.NET and web development** workload.
- [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use something other than Visual Studio Code.

## Create an API project

* [Visual Studio](#tabpanel_8_visual-studio)
* [Visual Studio Code](#tabpanel_8_visual-studio-code)

- Start Visual Studio 2022 and select **Create a new project**.

- In the **Create a new project** dialog:

  * Enter `Empty` in the **Search for templates** search box.
  * Select the **ASP.NET Core Empty** template and select **Next**.

- Name the project *TodoApi* and select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 6.0**
  * Uncheck **Do not use top-level statements**
  * Select **Create**

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to the folder that will contain the project folder.

* Run the following commands:

  ```
  dotnet new web -o TodoApi
  cd TodoApi
  code -r ../TodoApi
  ```

* When a dialog box asks if you want to trust the authors, select **Yes**.

* When a dialog box asks if you want to add required assets to the project, select **Yes**.

  The preceding commands create a new ASP.NET Core project and open it in Visual Studio Code.

### Examine the code

The `Program.cs` file contains the following code:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code:

* Creates a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) and a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) with preconfigured defaults.
* Creates an HTTP GET endpoint `/` that returns `Hello World!`:

### Run the app

* [Visual Studio](#tabpanel_9_visual-studio)
* [Visual Studio Code](#tabpanel_9_visual-studio-code)

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio launches the [Kestrel web server](../fundamentals/servers/kestrel?view=aspnetcore-10.0) and opens a browser window.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Press Ctrl+F5 to run the app. A browser window is opened.

`Hello World!` is displayed in the browser. The `Program.cs` file contains a minimal but complete app.

## Add NuGet packages

NuGet packages must be added to support the database and diagnostics used in this tutorial.

* [Visual Studio](#tabpanel_10_visual-studio)
* [Visual Studio Code](#tabpanel_10_visual-studio-code)

- From the **Tools** menu, select **NuGet Package Manager > Manage NuGet Packages for Solution**.
- Select the **Browse** tab.
- Enter **Microsoft.EntityFrameworkCore.InMemory** in the search box, and then select `Microsoft.EntityFrameworkCore.InMemory`.
- Select the **Project** checkbox in the right pane.
- In the **Version** drop down select the latest version 7 available, for example `6.0.28`, and then select **Install**.
- Follow the preceding instructions to add the `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` package with the latest version 7 available.

* Run the following commands:

  ```
  dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 6.0.28
  dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore --version 6.0.28
  ```

## The model and database context classes

In the project folder, create a file named `Todo.cs` with the following code:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

The preceding code creates the model for this app. A *model* is a class that represents data that the app manages.

Create a file named `TodoDb.cs` with the following code:

```
using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}
```

The preceding code defines the *database context*, which is the main class that coordinates [Entity Framework](/en-us/ef/core/) functionality for a data model. This class derives from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.

## Add the API code

Replace the contents of the `Program.cs` file with the following code:

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

The following highlighted code adds the database context to the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container and enables displaying database-related exceptions:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
```

The DI container provides access to the database context and other services.

## Create API testing UI with Swagger

There are many available web API testing tools to choose from, and you can follow this tutorial's introductory API test steps with your own preferred tool.

  ```
  dotnet add package NSwag.AspNetCore
  ```

The previous command adds the [NSwag.AspNetCore](https://www.nuget.org/packages/NSwag.AspNetCore/) package, which contains tools to generate Swagger documents and UI.

### Configure Swagger middleware

* In Program.cs add the following `using` statements at the top:

  ```
  using NSwag.AspNetCore;
  ```

* Add the following highlighted code before `app` is defined in line `var app = builder.Build();`

  ```
  using NSwag.AspNetCore;
  using Microsoft.EntityFrameworkCore;

  var builder = WebApplication.CreateBuilder(args);
  builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
  builder.Services.AddDatabaseDeveloperPageExceptionFilter();

  builder.Services.AddEndpointsApiExplorer();
  builder.Services.AddOpenApiDocument(config =>
  {
      config.DocumentName = "TodoAPI";
      config.Title = "TodoAPI v1";
      config.Version = "v1";
  });

  var app = builder.Build();
  ```

In the previous code:

* `builder.Services.AddEndpointsApiExplorer();`: Enables the API Explorer, which is a service that provides metadata about the HTTP API. The API Explorer is used by Swagger to generate the Swagger document.

* `builder.Services.AddOpenApiDocument(config => {...});`: Adds the Swagger OpenAPI document generator to the application services and configures it to provide more information about the API, such as its title and version. For information on providing more robust API details, see [Get started with NSwag and ASP.NET Core](getting-started-with-nswag?view=aspnetcore-10.0#customize-api-documentation)

* Add the following highlighted code to the next line after `app` is defined in line `var app = builder.Build();`

  ```

  var app = builder.Build();

  if (app.Environment.IsDevelopment())
  {
      app.UseOpenApi();
      app.UseSwaggerUi(config =>
      {
          config.DocumentTitle = "TodoAPI";
          config.Path = "/swagger";
          config.DocumentPath = "/swagger/{documentName}/swagger.json";
          config.DocExpansion = "list";
      });
  }
  ```

  The previous code enables the Swagger middleware for serving the generated JSON document and the Swagger UI. Swagger is only enabled in a development environment. Enabling Swagger in a production environment could expose potentially sensitive details about the API's structure and implementation.

[]()

## Test posting data

The following code in `Program.cs` creates an HTTP POST endpoint `/todoitems` that adds data to the in-memory database:

```
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});
```

Run the app. The browser displays a 404 error because there's no longer a `/` endpoint.

The POST endpoint will be used to add data to the app.

* With the app still running, in the browser, navigate to `https://localhost:<port>/swagger` to display the API testing page generated by Swagger.

* On the Swagger API testing page, select **Post /todoitems** > **Try it out**.

* Note that the **Request body** field contains a generated example format reflecting the parameters for the API.

* In the request body enter JSON for a to-do item, without specifying the optional `id`:

  ```
  {
    "name":"walk dog",
    "isComplete":true
  }
  ```

* Select **Execute**.

Swagger provides a **Responses** pane below the **Execute** button.

Note a few of the useful details:

* cURL: Swagger provides an example cURL command in Unix/Linux syntax, which can be run at the command line with any bash shell that uses Unix/Linux syntax, including Git Bash from [Git for Windows](https://git-scm.com/downloads).
* Request URL: A simplified representation of the HTTP request made by Swagger UI's JavaScript code for the API call. Actual requests can include details such as headers and query parameters and a request body.
* Server response: Includes the response body and headers. The response body shows the `id` was set to `1`.
* Response Code: A 201 `HTTP` status code was returned, indicating that the request was successfully processed and resulted in the creation of a new resource.

***

## Examine the GET endpoints

The sample app implements several GET endpoints by calling `MapGet`:

| API                       | Description                   | Request body | Response body        |
| ------------------------- | ----------------------------- | ------------ | -------------------- |
| `GET /todoitems`          | Get all to-do items           | None         | Array of to-do items |
| `GET /todoitems/complete` | Get all completed to-do items | None         | Array of to-do items |
| `GET /todoitems/{id}`     | Get an item by ID             | None         | To-do item           |

```
app.MapGet("/", () => "Hello World!");

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());
```

## Test the GET endpoints

Test the app by calling the endpoints from a browser or Swagger.

* In Swagger select **GET /todoitems** > **Try it out** > **Execute**.

* Alternatively, call **GET /todoitems** from a browser by entering the URI `http://localhost:<port>/todoitems`. For example, `http://localhost:5001/todoitems`

The call to `GET /todoitems` produces a response similar to the following:

```
[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
```

* Call **GET /todoitems/{id}** in Swagger to return data from a specific id:

  * Select **GET /todoitems** > **Try it out**.
  * Set the **id** field to `1` and select **Execute**.

* Alternatively, call **GET /todoitems** from a browser by entering the URI `https://localhost:<port>/todoitems/1`. For example, For example, `https://localhost:5001/todoitems/1`

* The response is similar to the following:

  ```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
  ```

This app uses an in-memory database. If the app is restarted, the GET request doesn't return any data. If no data is returned, [POST](#post) data to the app and try the GET request again.

## Return values

ASP.NET Core automatically serializes the object to [JSON](https://www.json.org) and writes the JSON into the body of the response message. The response code for this return type is [200 OK](https://developer.mozilla.org/docs/Web/HTTP/Status/200), assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.

The return types can represent a wide range of HTTP status codes. For example, `GET /todoitems/{id}` can return two different status values:

* If no item matches the requested ID, the method returns a [404 status](https://developer.mozilla.org/docs/Web/HTTP/Status/404) [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) error code.
* Otherwise, the method returns 200 with a JSON response body. Returning `item` results in an HTTP 200 response.

## Examine the PUT endpoint

The sample app implements a single PUT endpoint using `MapPut`:

```
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

This method is similar to the `MapPost` method, except it uses HTTP PUT. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use [HTTP PATCH](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute).

## Test the PUT endpoint

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.

Update the to-do item that has `Id = 1` and set its name to `"feed fish"`.

Use Swagger to send a PUT request:

* Select **Put /todoitems/{id}** > **Try it out**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

  ```
  {
    "name": "feed fish",
    "isComplete": false
  }
  ```

* Select **Execute**.

## Examine and test the DELETE endpoint

The sample app implements a single DELETE endpoint using `MapDelete`:

```
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});
```

Use Swagger to send a DELETE request:

* Select **DELETE /todoitems/{id}** > **Try it out**.

* Set the **ID** field to `1` and select **Execute**.

  The DELETE request is sent to the app and the response is displayed in the **Responses** pane. The response body is empty, and the **Server response** status code is 204.

[]()

## Prevent over-posting

Currently the sample app exposes the entire `Todo` object. Production apps In production applications, a subset of the model is often used to restrict the data that can be input and returned. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. **DTO** is used in this article.

A DTO can be used to:

* Prevent over-posting.
* Hide properties that clients aren't supposed to view.
* Omit some properties to reduce payload size.
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.

To demonstrate the DTO approach, update the `Todo` class to include a secret field:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}
```

The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

Verify you can post and get the secret field.

Create a file named `TodoItemDTO.cs` with the following code:

```
public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
```

Replace the contents of the `Program.cs` file with the following code to use this DTO model:

* [Visual Studio](#tabpanel_11_visual-studio)
* [Visual Studio Code](#tabpanel_11_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}


class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}
```

```
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).Select(t => new TodoItemDTO(t)).ToListAsync());


app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

Verify you can post and get all fields except the secret field.

## Test Minimal API

For an example of testing a Minimal API app, see [this GitHub sample](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/MinApiTestsSample).

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore).

## Related content

* [Minimal APIs quick reference](../fundamentals/minimal-apis?view=aspnetcore-10.0)

Minimal APIs are architected to create HTTP APIs with minimal dependencies. They're ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core.

This tutorial teaches the basics of building a Minimal API with ASP.NET Core. Another approach to creating APIs in ASP.NET Core is to use controllers. For help with choosing between Minimal APIs and controller-based APIs, see [APIs overview](../fundamentals/apis?view=aspnetcore-10.0). For a tutorial on creating an API project based on [controllers](../web-api/?view=aspnetcore-10.0) that contains more features, see [Create a web API](first-web-api?view=aspnetcore-10.0).

## Overview

This tutorial creates the following API:

| API                          | Description                | Request body       | Response body        |
| ---------------------------- | -------------------------- | ------------------ | -------------------- |
| `GET /todoitems`             | Get all to-do items        | None               | Array of to-do items |
| `GET /todoitems/complete`    | Get completed to-do items  | None               | Array of to-do items |
| `GET /todoitems/{id}`        | Get an item by ID          | None               | To-do item           |
| `POST /todoitems`            | Add a new item             | To-do item         | To-do item           |
| `PUT /todoitems/{id}`        | Update an existing item    | To-do item         | None                 |
| `PATCH /todoitems/{id}`      | Partially update an item   | Partial to-do item | None                 |
| `DELETE /todoitems/{id}`     | Delete an item             | None               | None                 |

## Create an API project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio 2022 and select **Create a new project**.

- In the **Create a new project** dialog:

  * Enter `Empty` in the **Search for templates** search box.
  * Select the **ASP.NET Core Empty** template and select **Next**.

- Name the project *TodoApi* and select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 8.0 (Long Term Support)**
  * Uncheck **Do not use top-level statements**
  * Select **Create**

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to the folder that will contain the project folder.

* Run the following commands:

  ```
  dotnet new web -o TodoApi
  cd TodoApi
  code -r ../TodoApi
  ```

* When a dialog box asks if you want to trust the authors, select **Yes**.

* When a dialog box asks if you want to add required assets to the project, select **Yes**.

  The preceding commands create a new ASP.NET Core project and open it in Visual Studio Code.

### Examine the code

The `Program.cs` file contains the following code:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code:

* Creates a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) and a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) with preconfigured defaults.
* Creates an HTTP GET endpoint `/` that returns `Hello World!`:

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio launches the [Kestrel web server](../fundamentals/servers/kestrel?view=aspnetcore-10.0) and opens a browser window.

`Hello World!` is displayed in the browser. The `Program.cs` file contains a minimal but complete app.

Close the browser window.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

In Visual Studio Code, press `Ctrl`+`F5` (Windows) or `control`+`F5` (macOS) to run the app without debugging.

The default browser launches with the following URL: `https://localhost:<port>` where `<port>` is the randomly generated port number.

Close the browser window.

In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

## Add NuGet packages

NuGet packages must be added to support the database and diagnostics used in this tutorial.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

- From the **Tools** menu, select **NuGet Package Manager > Manage NuGet Packages for Solution**.
- Select the **Browse** tab.
- Enter **Microsoft.EntityFrameworkCore.InMemory** in the search box, and then select `Microsoft.EntityFrameworkCore.InMemory`.
- Select the **Project** checkbox in the right pane and then select **Install**.
- Follow the preceding instructions to add the `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` package.

* Run the following commands:

  ```
  dotnet add package Microsoft.EntityFrameworkCore.InMemory
  dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
  ```

## The model and database context classes

* In the project folder, create a file named `Todo.cs` with the following code:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

The preceding code creates the model for this app. A *model* is a class that represents data that the app manages.

* Create a file named `TodoDb.cs` with the following code:

```
using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}
```

The preceding code defines the *database context*, which is the main class that coordinates [Entity Framework](/en-us/ef/core/) functionality for a data model. This class derives from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.

## Add the API code

* Replace the contents of the `Program.cs` file with the following code:

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

The following highlighted code adds the database context to the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container and enables displaying database-related exceptions:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
```

The DI container provides access to the database context and other services.

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

This tutorial uses [Endpoints Explorer and .http files](../test/http-files?view=aspnetcore-10.0#use-endpoints-explorer) to test the API.

## Create API testing UI with Swagger

There are many available web API testing tools to choose from, and you can follow this tutorial's introductory API test steps with your own preferred tool.

  ```
  dotnet add package NSwag.AspNetCore
  ```

The previous command adds the [NSwag.AspNetCore](https://www.nuget.org/packages/NSwag.AspNetCore/) package, which contains tools to generate Swagger documents and UI.

### Configure Swagger middleware

* Add the following highlighted code before `app` is defined in line `var app = builder.Build();`

  ```
  using NSwag.AspNetCore;
  using Microsoft.EntityFrameworkCore;

  var builder = WebApplication.CreateBuilder(args);
  builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
  builder.Services.AddDatabaseDeveloperPageExceptionFilter();

  builder.Services.AddEndpointsApiExplorer();
  builder.Services.AddOpenApiDocument(config =>
  {
      config.DocumentName = "TodoAPI";
      config.Title = "TodoAPI v1";
      config.Version = "v1";
  });
  var app = builder.Build();
  ```

In the previous code:

* `builder.Services.AddEndpointsApiExplorer();`: Enables the API Explorer, which is a service that provides metadata about the HTTP API. The API Explorer is used by Swagger to generate the Swagger document.

* `builder.Services.AddOpenApiDocument(config => {...});`: Adds the Swagger OpenAPI document generator to the application services and configures it to provide more information about the API, such as its title and version. For information on providing more robust API details, see [Get started with NSwag and ASP.NET Core](getting-started-with-nswag?view=aspnetcore-10.0#customize-api-documentation)

* Add the following highlighted code to the next line after `app` is defined in line `var app = builder.Build();`

  ```
  var app = builder.Build();
  if (app.Environment.IsDevelopment())
  {
      app.UseOpenApi();
      app.UseSwaggerUi(config =>
      {
          config.DocumentTitle = "TodoAPI";
          config.Path = "/swagger";
          config.DocumentPath = "/swagger/{documentName}/swagger.json";
          config.DocExpansion = "list";
      });
  }
  ```

  The previous code enables the Swagger middleware for serving the generated JSON document and the Swagger UI. Swagger is only enabled in a development environment. Enabling Swagger in a production environment could expose potentially sensitive details about the API's structure and implementation.

[]()

## Test posting data

The following code in `Program.cs` creates an HTTP POST endpoint `/todoitems` that adds data to the in-memory database:

```
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});
```

Run the app. The browser displays a 404 error because there's no longer a `/` endpoint.

The POST endpoint will be used to add data to the app.

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

- Select **View** > **Other Windows** > **Endpoints Explorer**.

- Right-click the **POST** endpoint and select **Generate request**.

  A new file is created in the project folder named `TodoApi.http`, with contents similar to the following example:

  ```
  @TodoApi_HostAddress = https://localhost:7031

  Post {{TodoApi_HostAddress}}/todoitems

  ###
  ```

  * The first line creates a variable that is used for all of the endpoints.
  * The next line defines a POST request.
  * The triple hashtag (`###`) line is a request delimiter: what comes after it is for a different request.

- The POST request needs headers and a body. To define those parts of the request, add the following lines immediately after the POST request line:

  ```
  Content-Type: application/json

  {
    "name":"walk dog",
    "isComplete":true
  }
  ```

  The preceding code adds a Content-Type header and a JSON request body. The TodoApi.http file should now look like the following example, but with your port number:

  ```
  @TodoApi_HostAddress = https://localhost:7057

  Post {{TodoApi_HostAddress}}/todoitems
  Content-Type: application/json

  {
    "name":"walk dog",
    "isComplete":true
  }

  ###
  ```

- Run the app.

- Select the **Send request** link that is above the `POST` request line.

  The POST request is sent to the app and the response is displayed in the **Response** pane.

* With the app still running, in the browser, navigate to `https://localhost:<port>/swagger` to display the API testing page generated by Swagger.

* On the Swagger API testing page, select **Post /todoitems** > **Try it out**.

* Note that the **Request body** field contains a generated example format reflecting the parameters for the API.

* In the request body enter JSON for a to-do item, without specifying the optional `id`:

  ```
  {
    "name":"walk dog",
    "isComplete":true
  }
  ```

* Select **Execute**.

Swagger provides a **Responses** pane below the **Execute** button.

Note a few of the useful details:

* cURL: Swagger provides an example cURL command in Unix/Linux syntax, which can be run at the command line with any bash shell that uses Unix/Linux syntax, including Git Bash from [Git for Windows](https://git-scm.com/downloads).
* Request URL: A simplified representation of the HTTP request made by Swagger UI's JavaScript code for the API call. Actual requests can include details such as headers and query parameters and a request body.
* Server response: Includes the response body and headers. The response body shows the `id` was set to `1`.
* Response Code: A 201 `HTTP` status code was returned, indicating that the request was successfully processed and resulted in the creation of a new resource.

## Examine the GET endpoints

The sample app implements several GET endpoints by calling `MapGet`:

| API                       | Description                   | Request body | Response body        |
| ------------------------- | ----------------------------- | ------------ | -------------------- |
| `GET /todoitems`          | Get all to-do items           | None         | Array of to-do items |
| `GET /todoitems/complete` | Get all completed to-do items | None         | Array of to-do items |
| `GET /todoitems/{id}`     | Get an item by ID             | None         | To-do item           |

```
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());
```

## Test the GET endpoints

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

Test the app by calling the `GET` endpoints from a browser or by using **Endpoints Explorer**. The following steps are for **Endpoints Explorer**.

* In **Endpoints Explorer**, right-click the first **GET** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

  ```
  Get {{TodoApi_HostAddress}}/todoitems

  ###
  ```

* Select the **Send request** link that is above the new `GET` request line.

  The GET request is sent to the app and the response is displayed in the **Response** pane.

* The response body is similar to the following JSON:

  ```
  [
    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
  ]
  ```

* In **Endpoints Explorer**, right-click the `/todoitems/{id}` **GET** endpoint and select **Generate request**. The following content is added to the `TodoApi.http` file:

  ```
  GET {{TodoApi_HostAddress}}/todoitems/{id}

  ###
  ```

* Replace `{id}` with `1`.

* Select the **Send request** link that is above the new GET request line.

  The GET request is sent to the app and the response is displayed in the **Response** pane.

* The response body is similar to the following JSON:

  ```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
  ```

Test the app by calling the endpoints from a browser or Swagger.

* In Swagger select **GET /todoitems** > **Try it out** > **Execute**.

* Alternatively, call **GET /todoitems** from a browser by entering the URI `http://localhost:<port>/todoitems`. For example, `http://localhost:5001/todoitems`

The call to `GET /todoitems` produces a response similar to the following:

```
[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
```

* Call **GET /todoitems/{id}** in Swagger to return data from a specific id:

  * Select **GET /todoitems** > **Try it out**.
  * Set the **id** field to `1` and select **Execute**.

* Alternatively, call **GET /todoitems** from a browser by entering the URI `https://localhost:<port>/todoitems/1`. For example, `https://localhost:5001/todoitems/1`

* The response is similar to the following:

  ```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
  ```

This app uses an in-memory database. If the app is restarted, the GET request doesn't return any data. If no data is returned, [POST](#post) data to the app and try the GET request again.

## Return values

ASP.NET Core automatically serializes the object to [JSON](https://www.json.org) and writes the JSON into the body of the response message. The response code for this return type is [200 OK](https://developer.mozilla.org/docs/Web/HTTP/Status/200), assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.

The return types can represent a wide range of HTTP status codes. For example, `GET /todoitems/{id}` can return two different status values:

* If no item matches the requested ID, the method returns a [404 status](https://developer.mozilla.org/docs/Web/HTTP/Status/404) [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) error code.
* Otherwise, the method returns 200 with a JSON response body. Returning `item` results in an HTTP 200 response.

## Examine the PUT endpoint

The sample app implements a single PUT endpoint using `MapPut`:

```
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

This method is similar to the `MapPost` method, except it uses HTTP PUT. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use [HTTP PATCH](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute).

## Test the PUT endpoint

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.

Update the to-do item that has `Id = 1` and set its name to `"feed fish"`.

* [Visual Studio](#tabpanel_8_visual-studio)
* [Visual Studio Code](#tabpanel_8_visual-studio-code)

- In **Endpoints Explorer**, right-click the **PUT** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

  ```
  Put {{TodoApi_HostAddress}}/todoitems/{id}

  ###
  ```

- In the PUT request line, replace `{id}` with `1`.

- Add the following lines immediately after the PUT request line:

  ```
  Content-Type: application/json

  {
    "name": "feed fish",
    "isComplete": false
  }
  ```

  The preceding code adds a Content-Type header and a JSON request body.

- Select the **Send request** link that is above the new PUT request line.

  The PUT request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

Use Swagger to send a PUT request:

* Select **Put /todoitems/{id}** > **Try it out**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

  ```
  {
    "name": "feed fish",
    "isComplete": false
  }
  ```

* Select **Execute**.

## Examine the PATCH endpoint

Create a file named `TodoPatchDto.cs` with the following code:

```
public class TodoPatchDto
{
    public string? Name { get; set; }
    public bool? IsComplete { get; set; }
}
```

The `TodoPatchDto` class uses nullable properties (`string?` and `bool?`) to distinguish between a field that wasn't provided in the request versus a field explicitly set to a value.

The sample app implements a single PATCH endpoint using `MapPatch`:

```
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

This method is similar to the `MapPut` method, except it uses HTTP PATCH and only updates the fields provided in the request. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PATCH request enables partial updates, allowing clients to send only the fields that need to be changed.

The PATCH endpoint uses a `TodoPatchDto` class with nullable properties to properly handle partial updates. Using nullable properties allows the endpoint to distinguish between a field that wasn't provided (null) versus a field explicitly set to a value (including false for boolean fields). Without nullable properties, a non-nullable bool would default to false, potentially overwriting an existing true value when that field wasn't included in the request.

Note

PATCH operations allow partial updates to resources. For more advanced partial updates using JSON Patch documents, see [JsonPatch in ASP.NET Core web API](../web-api/jsonpatch?view=aspnetcore-10.0).

## Test the PATCH endpoint

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PATCH call. Call GET to ensure there's an item in the database before making a PATCH call.

Update only the `name` property of the to-do item that has `Id = 1` and set its name to `"run errands"`.

* [Visual Studio](#tabpanel_9_visual-studio)
* [Visual Studio Code](#tabpanel_9_visual-studio-code)

- In **Endpoints Explorer**, right-click the **PATCH** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

  ```
  PATCH {{TodoApi_HostAddress}}/todoitems/{id}

  ###
  ```

- In the PATCH request line, replace `{id}` with `1`.

- Add the following lines immediately after the PATCH request line:

  ```
  Content-Type: application/json

  {
    "name": "run errands"
  }
  ```

  The preceding code adds a Content-Type header and a JSON request body with only the field to update.

- Select the **Send request** link that is above the new PATCH request line.

  The PATCH request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

Use Swagger to send a PATCH request:

* Select **Patch /todoitems/{id}** > **Try it out**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

  ```
  {
    "name": "run errands"
  }
  ```

* Select **Execute**.

## Examine and test the DELETE endpoint

The sample app implements a single DELETE endpoint using `MapDelete`:

```
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});
```

* [Visual Studio](#tabpanel_10_visual-studio)
* [Visual Studio Code](#tabpanel_10_visual-studio-code)

- In **Endpoints Explorer**, right-click the **DELETE** endpoint and select **Generate request**.

  A DELETE request is added to `TodoApi.http`.

- Replace `{id}` in the DELETE request line with `1`. The DELETE request should look like the following example:

  ```
  DELETE {{TodoApi_HostAddress}}/todoitems/1

  ###
  ```

- Select the **Send request** link for the DELETE request.

  The DELETE request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

Use Swagger to send a DELETE request:

* Select **DELETE /todoitems/{id}** > **Try it out**.

* Set the **ID** field to `1` and select **Execute**.

  The DELETE request is sent to the app and the response is displayed in the **Responses** pane. The response body is empty, and the **Server response** status code is 204.

## Use the MapGroup API

The sample app code repeats the `todoitems` URL prefix each time it sets up an endpoint. APIs often have groups of endpoints with a common URL prefix, and the [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) method is available to help organize such groups. It reduces repetitive code and allows for customizing entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata).

Replace the contents of `Program.cs` with the following code:

* [Visual Studio](#tabpanel_11_visual-studio)
* [Visual Studio Code](#tabpanel_11_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

```
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

The preceding code has the following changes:

* Adds `var todoItems = app.MapGroup("/todoitems");` to set up the group using the URL prefix `/todoitems`.
* Changes all the `app.Map<HttpVerb>` methods to `todoItems.Map<HttpVerb>`.
* Removes the URL prefix `/todoitems` from the `Map<HttpVerb>` method calls.

Test the endpoints to verify that they work the same.

## Use the TypedResults API

Returning [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) rather than [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) has several advantages, including testability and automatically returning the response type metadata for OpenAPI to describe the endpoint. For more information, see [`TypedResults` versus `Results`](../fundamentals/minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

The `Map<HttpVerb>` methods can call route handler methods instead of using lambdas. To see an example, update *Program.cs* with the following code:

* [Visual Studio](#tabpanel_12_visual-studio)
* [Visual Studio Code](#tabpanel_12_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

```
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

The `Map<HttpVerb>` code now calls methods instead of lambdas:

```
var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
```

These methods return objects that implement [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) and are defined by [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults):

```
static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

Unit tests can call these methods and test that they return the correct type. For example, if the method is `GetAllTodos`:

```
static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
```

Unit test code can verify that an object of type [Ok\<Todo\[\]>](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.ok-1.value#microsoft-aspnetcore-http-httpresults-ok-1-value) is returned from the handler method. For example:

```
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}
```

[]()

## Prevent over-posting

Currently the sample app exposes the entire `Todo` object. In production applications, a subset of the model is often used to restrict the data that can be input and returned. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. **DTO** is used in this article.

A DTO can be used to:

* Prevent over-posting.
* Hide properties that clients aren't supposed to view.
* Omit some properties to reduce payload size.
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.

To demonstrate the DTO approach, update the `Todo` class to include a secret field:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}
```

The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

Verify you can post and get the secret field.

Create a file named `TodoItemDTO.cs` with the following code:

```
public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
```

Replace the contents of the `Program.cs` file with the following code to use this DTO model:

* [Visual Studio](#tabpanel_13_visual-studio)
* [Visual Studio Code](#tabpanel_13_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

```
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

Verify you can post and get all fields except the secret field.

[]()

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/min-web-api/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

* [Configure JSON serialization options](../fundamentals/minimal-apis/responses?view=aspnetcore-10.0#configure-json-serialization-options).
* Handle errors and exceptions: The [developer exception page](../fundamentals/error-handling-api?view=aspnetcore-10.0#developer-exception-page) is enabled by default in the `Development` environment for Minimal API apps. For information about how to handle errors and exceptions, see [Handle errors in ASP.NET Core APIs](../fundamentals/error-handling-api?view=aspnetcore-10.0).
* For an example of testing a Minimal API app, see [this GitHub sample](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/MinApiTestsSample).
* [OpenAPI support in Minimal APIs](../fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0).
* [Quickstart: Publish to Azure](/en-us/azure/app-service/quickstart-dotnetcore).
* [Organizing ASP.NET Core Minimal APIs](https://www.tessferrandez.com/blog/2023/10/31/organizing-minimal-apis.html).

### Learn more

See [Minimal APIs quick reference](../fundamentals/minimal-apis?view=aspnetcore-10.0)

Minimal APIs are architected to create HTTP APIs with minimal dependencies. They're ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core.

This tutorial teaches the basics of building a Minimal API with ASP.NET Core. Another approach to creating APIs in ASP.NET Core is to use controllers. For help with choosing between Minimal APIs and controller-based APIs, see [APIs overview](../fundamentals/apis?view=aspnetcore-10.0). For a tutorial on creating an API project based on [controllers](../web-api/?view=aspnetcore-10.0) that contains more features, see [Create a web API](first-web-api?view=aspnetcore-10.0).

## Overview

This tutorial creates the following API:

| API                          | Description                | Request body       | Response body        |
| ---------------------------- | -------------------------- | ------------------ | -------------------- |
| `GET /todoitems`             | Get all to-do items        | None               | Array of to-do items |
| `GET /todoitems/complete`    | Get completed to-do items  | None               | Array of to-do items |
| `GET /todoitems/{id}`        | Get an item by ID          | None               | To-do item           |
| `POST /todoitems`            | Add a new item             | To-do item         | To-do item           |
| `PUT /todoitems/{id}`        | Update an existing item    | To-do item         | None                 |
| `PATCH /todoitems/{id}`      | Partially update an item   | Partial to-do item | None                 |
| `DELETE /todoitems/{id}`     | Delete an item             | None               | None                 |

## Create an API project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio 2022 and select **Create a new project**.

- In the **Create a new project** dialog:

  * Enter `Empty` in the **Search for templates** search box.
  * Select the **ASP.NET Core Empty** template and select **Next**.

- Name the project *TodoApi* and select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 9.0**
  * Uncheck **Do not use top-level statements**
  * Select **Create**

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to the folder that will contain the project folder.

* Run the following commands:

  ```
  dotnet new web -o TodoApi
  cd TodoApi
  code -r ../TodoApi
  ```

* When a dialog box asks if you want to trust the authors, select **Yes**.

* When a dialog box asks if you want to add required assets to the project, select **Yes**.

  The preceding commands create a new ASP.NET Core project and open it in Visual Studio Code.

### Examine the code

The `Program.cs` file contains the following code:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code:

* Creates a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) and a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) with preconfigured defaults.
* Creates an HTTP GET endpoint `/` that returns `Hello World!`.

### Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio launches the [Kestrel web server](../fundamentals/servers/kestrel?view=aspnetcore-10.0) and opens a browser window.

`Hello World!` is displayed in the browser. The `Program.cs` file contains a minimal but complete app.

Close the browser window.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

In Visual Studio Code, press `Ctrl`+`F5` (Windows) or `control`+`F5` (macOS) to run the app without debugging.

The default browser launches with the following URL: `https://localhost:{PORT}` where `{PORT}` is the randomly generated port number.

Close the browser window.

In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

## Add NuGet packages

NuGet packages must be added to support the database and diagnostics used in this tutorial.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

- From the **Tools** menu, select **NuGet Package Manager > Manage NuGet Packages for Solution**.
- Select the **Browse** tab.
- Select **Include prerelease**.
- Enter **Microsoft.EntityFrameworkCore.InMemory** in the search box, and then select `Microsoft.EntityFrameworkCore.InMemory`.
- Select the **Project** checkbox in the right pane and then select **Install**.
- Follow the preceding instructions to add the `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` package.

* Run the following commands:

  ```
  dotnet add package Microsoft.EntityFrameworkCore.InMemory
  dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
  ```

## The model and database context classes

* In the project folder, create a file named `Todo.cs` with the following code:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

The preceding code creates the model for this app. A *model* is a class that represents data that the app manages.

* Create a file named `TodoDb.cs` with the following code:

```
using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}
```

The preceding code defines the *database context*, which is the main class that coordinates [Entity Framework](/en-us/ef/core/) functionality for a data model. This class derives from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.

## Add the API code

* Replace the contents of the `Program.cs` file with the following code:

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

The following highlighted code adds the database context to the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container and enables displaying database-related exceptions:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
```

The DI container provides access to the database context and other services.

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

This tutorial uses [Endpoints Explorer and .http files](../test/http-files?view=aspnetcore-10.0#use-endpoints-explorer) to test the API.

## Create API testing UI with Swagger

There are many available web API testing tools to choose from, and you can follow this tutorial's introductory API test steps with your own preferred tool.

  ```
  dotnet add package NSwag.AspNetCore
  ```

The previous command adds the [NSwag.AspNetCore](https://www.nuget.org/packages/NSwag.AspNetCore/) package, which contains tools to generate Swagger documents and UI.

### Configure Swagger middleware

* In Program.cs add the following highlighted code before `app` is defined in line `var app = builder.Build();`

  ```
  using Microsoft.EntityFrameworkCore;

  var builder = WebApplication.CreateBuilder(args);
  builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
  builder.Services.AddDatabaseDeveloperPageExceptionFilter();

  builder.Services.AddEndpointsApiExplorer();
  builder.Services.AddOpenApiDocument(config =>
  {
      config.DocumentName = "TodoAPI";
      config.Title = "TodoAPI v1";
      config.Version = "v1";
  });
  var app = builder.Build();
  ```

In the previous code:

* `builder.Services.AddEndpointsApiExplorer();`: Enables the API Explorer, which is a service that provides metadata about the HTTP API. The API Explorer is used by Swagger to generate the Swagger document.

* `builder.Services.AddOpenApiDocument(config => {...});`: Adds the Swagger OpenAPI document generator to the application services and configures it to provide more information about the API, such as its title and version. For information on providing more robust API details, see [Get started with NSwag and ASP.NET Core](getting-started-with-nswag?view=aspnetcore-10.0#customize-api-documentation)

* Add the following highlighted code to the next line after `app` is defined in line `var app = builder.Build();`

  ```
  var app = builder.Build();
  if (app.Environment.IsDevelopment())
  {
      app.UseOpenApi();
      app.UseSwaggerUi(config =>
      {
          config.DocumentTitle = "TodoAPI";
          config.Path = "/swagger";
          config.DocumentPath = "/swagger/{documentName}/swagger.json";
          config.DocExpansion = "list";
      });
  }
  ```

  The previous code enables the Swagger middleware for serving the generated JSON document and the Swagger UI. Swagger is only enabled in a development environment. Enabling Swagger in a production environment could expose potentially sensitive details about the API's structure and implementation.

[]()

## Test posting data

The following code in `Program.cs` creates an HTTP POST endpoint `/todoitems` that adds data to the in-memory database:

```
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});
```

Run the app. The browser displays a 404 error because there's no longer a `/` endpoint.

The POST endpoint will be used to add data to the app.

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

- Select **View** > **Other Windows** > **Endpoints Explorer**.

- Right-click the **POST** endpoint and select **Generate request**.

  A new file is created in the project folder named `TodoApi.http`, with contents similar to the following example:

  ```
  @TodoApi_HostAddress = https://localhost:7031

  POST {{TodoApi_HostAddress}}/todoitems

  ###
  ```

  * The first line creates a variable that is used for all of the endpoints.
  * The next line defines a POST request.
  * The triple hashtag (`###`) line is a request delimiter: what comes after it is for a different request.

- The POST request needs headers and a body. To define those parts of the request, add the following lines immediately after the POST request line:

  ```
  Content-Type: application/json

  {
    "name":"walk dog",
    "isComplete":true
  }
  ```

  The preceding code adds a Content-Type header and a JSON request body. The TodoApi.http file should now look like the following example, but with your port number:

  ```
  @TodoApi_HostAddress = https://localhost:7057

  POST {{TodoApi_HostAddress}}/todoitems
  Content-Type: application/json

  {
    "name":"walk dog",
    "isComplete":true
  }

  ###
  ```

- Run the app.

- Select the **Send request** link that is above the `POST` request line.

  The POST request is sent to the app and the response is displayed in the **Response** pane.

* With the app still running, in the browser, navigate to `https://localhost:{PORT}/swagger` to display the API testing page generated by Swagger.

* On the Swagger API testing page, select **Post /todoitems** > **Try it out**.

* Note that the **Request body** field contains a generated example format reflecting the parameters for the API.

* In the request body enter JSON for a to-do item, without specifying the optional `id`:

  ```
  {
    "name":"walk dog",
    "isComplete":true
  }
  ```

* Select **Execute**.

Swagger provides a **Responses** pane below the **Execute** button.

Note a few of the useful details:

* cURL: Swagger provides an example cURL command in Unix/Linux syntax, which can be run at the command line with any bash shell that uses Unix/Linux syntax, including Git Bash from [Git for Windows](https://git-scm.com/downloads).
* Request URL: A simplified representation of the HTTP request made by Swagger UI's JavaScript code for the API call. Actual requests can include details such as headers and query parameters and a request body.
* Server response: Includes the response body and headers. The response body shows the `id` was set to `1`.
* Response Code: A 201 `HTTP` status code was returned, indicating that the request was successfully processed and resulted in the creation of a new resource.

## Examine the GET endpoints

The sample app implements several GET endpoints by calling `MapGet`:

| API                       | Description                   | Request body | Response body        |
| ------------------------- | ----------------------------- | ------------ | -------------------- |
| `GET /todoitems`          | Get all to-do items           | None         | Array of to-do items |
| `GET /todoitems/complete` | Get all completed to-do items | None         | Array of to-do items |
| `GET /todoitems/{id}`     | Get an item by ID             | None         | To-do item           |

```
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());
```

## Test the GET endpoints

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

Test the app by calling the `GET` endpoints from a browser or by using **Endpoints Explorer**. The following steps are for **Endpoints Explorer**.

* In **Endpoints Explorer**, right-click the first **GET** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

  ```
  GET {{TodoApi_HostAddress}}/todoitems

  ###
  ```

* Select the **Send request** link that is above the new `GET` request line.

  The GET request is sent to the app and the response is displayed in the **Response** pane.

* The response body is similar to the following JSON:

  ```
  [
    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
  ]
  ```

* In **Endpoints Explorer**, right-click the `/todoitems/{id}` **GET** endpoint and select **Generate request**. The following content is added to the `TodoApi.http` file:

  ```
  GET {{TodoApi_HostAddress}}/todoitems/{id}

  ###
  ```

* Replace `{id}` with `1`.

* Select the **Send request** link that is above the new GET request line.

  The GET request is sent to the app and the response is displayed in the **Response** pane.

* The response body is similar to the following JSON:

  ```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
  ```

Test the app by calling the endpoints from a browser or Swagger.

* In Swagger select **GET /todoitems** > **Try it out** > **Execute**.

* Alternatively, call **GET /todoitems** from a browser by entering the URI `http://localhost:{PORT}/todoitems`. For example, `http://localhost:5001/todoitems`

The call to `GET /todoitems` produces a response similar to the following:

```
[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
```

* Call **GET /todoitems/{id}** in Swagger to return data from a specific id:

  * Select **GET /todoitems** > **Try it out**.
  * Set the **id** field to `1` and select **Execute**.

* Alternatively, call **GET /todoitems** from a browser by entering the URI `https://localhost:{PORT}/todoitems/1`. For example, `https://localhost:5001/todoitems/1`

* The response is similar to the following:

  ```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
  ```

This app uses an in-memory database. If the app is restarted, the GET request doesn't return any data. If no data is returned, [POST](#post) data to the app and try the GET request again.

## Return values

ASP.NET Core automatically serializes the object to [JSON](https://www.json.org) and writes the JSON into the body of the response message. The response code for this return type is [200 OK](https://developer.mozilla.org/docs/Web/HTTP/Status/200), assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.

The return types can represent a wide range of HTTP status codes. For example, `GET /todoitems/{id}` can return two different status values:

* If no item matches the requested ID, the method returns a [404 status](https://developer.mozilla.org/docs/Web/HTTP/Status/404) [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) error code.
* Otherwise, the method returns 200 with a JSON response body. Returning `item` results in an HTTP 200 response.

## Examine the PUT endpoint

The sample app implements a single PUT endpoint using `MapPut`:

```
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

This method is similar to the `MapPost` method, except it uses HTTP PUT. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use [HTTP PATCH](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute).

## Test the PUT endpoint

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.

Update the to-do item that has `Id = 1` and set its name to `"feed fish"`.

* [Visual Studio](#tabpanel_8_visual-studio)
* [Visual Studio Code](#tabpanel_8_visual-studio-code)

- In **Endpoints Explorer**, right-click the **PUT** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

  ```
  PUT {{TodoApi_HostAddress}}/todoitems/{id}

  ###
  ```

- In the PUT request line, replace `{id}` with `1`.

- Add the following lines immediately after the PUT request line:

  ```
  Content-Type: application/json

  {
    "id": 1,
    "name": "feed fish",
    "isComplete": false
  }
  ```

  The preceding code adds a Content-Type header and a JSON request body.

- Select the **Send request** link that is above the new PUT request line.

  The PUT request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

Use Swagger to send a PUT request:

* Select **Put /todoitems/{id}** > **Try it out**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

  ```
  {
    "id": 1,
    "name": "feed fish",
    "isComplete": false
  }
  ```

* Select **Execute**.

## Examine the PATCH endpoint

Create a file named `TodoPatchDto.cs` with the following code:

```
public class TodoPatchDto
{
    public string? Name { get; set; }
    public bool? IsComplete { get; set; }
}
```

The `TodoPatchDto` class uses nullable properties (`string?` and `bool?`) to distinguish between a field that wasn't provided in the request versus a field explicitly set to a value.

The sample app implements a single PATCH endpoint using `MapPatch`:

```
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
    if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;

    await db.SaveChangesAsync();

    return Results.NoContent();
});
```

This method is similar to the `MapPut` method, except it uses HTTP PATCH and only updates the fields provided in the request. A successful response returns [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a PATCH request enables partial updates, allowing clients to send only the fields that need to be changed.

The PATCH endpoint uses a `TodoPatchDto` class with nullable properties to properly handle partial updates. Using nullable properties allows the endpoint to distinguish between a field that wasn't provided (null) versus a field explicitly set to a value (including false for boolean fields). Without nullable properties, a non-nullable bool would default to false, potentially overwriting an existing true value when that field wasn't included in the request.

Note

PATCH operations allow partial updates to resources. For more advanced partial updates using JSON Patch documents, see [JsonPatch in ASP.NET Core web API](../web-api/jsonpatch?view=aspnetcore-10.0).

## Test the PATCH endpoint

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PATCH call. Call GET to ensure there's an item in the database before making a PATCH call.

Update only the `name` property of the to-do item that has `Id = 1` and set its name to `"run errands"`.

* [Visual Studio](#tabpanel_9_visual-studio)
* [Visual Studio Code](#tabpanel_9_visual-studio-code)

- In **Endpoints Explorer**, right-click the **PATCH** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

  ```
  PATCH {{TodoApi_HostAddress}}/todoitems/{id}

  ###
  ```

- In the PATCH request line, replace `{id}` with `1`.

- Add the following lines immediately after the PATCH request line:

  ```
  Content-Type: application/json

  {
    "name": "run errands"
  }
  ```

  The preceding code adds a Content-Type header and a JSON request body with only the field to update.

- Select the **Send request** link that is above the new PATCH request line.

  The PATCH request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

Use Swagger to send a PATCH request:

* Select **Patch /todoitems/{id}** > **Try it out**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

  ```
  {
    "name": "run errands"
  }
  ```

* Select **Execute**.

## Examine and test the DELETE endpoint

The sample app implements a single DELETE endpoint using `MapDelete`:

```
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});
```

* [Visual Studio](#tabpanel_10_visual-studio)
* [Visual Studio Code](#tabpanel_10_visual-studio-code)

- In **Endpoints Explorer**, right-click the **DELETE** endpoint and select **Generate request**.

  A DELETE request is added to `TodoApi.http`.

- Replace `{id}` in the DELETE request line with `1`. The DELETE request should look like the following example:

  ```
  DELETE {{TodoApi_HostAddress}}/todoitems/1

  ###
  ```

- Select the **Send request** link for the DELETE request.

  The DELETE request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

Use Swagger to send a DELETE request:

* Select **DELETE /todoitems/{id}** > **Try it out**.

* Set the **ID** field to `1` and select **Execute**.

  The DELETE request is sent to the app and the response is displayed in the **Responses** pane. The response body is empty, and the **Server response** status code is 204.

## Use the MapGroup API

The sample app code repeats the `todoitems` URL prefix each time it sets up an endpoint. APIs often have groups of endpoints with a common URL prefix, and the [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) method is available to help organize such groups. It reduces repetitive code and allows for customizing entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata).

Replace the contents of `Program.cs` with the following code:

* [Visual Studio](#tabpanel_11_visual-studio)
* [Visual Studio Code](#tabpanel_11_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", async (TodoDb db) =>
    await db.Todos.ToListAsync());

todoItems.MapGet("/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();
```

The preceding code has the following changes:

* Adds `var todoItems = app.MapGroup("/todoitems");` to set up the group using the URL prefix `/todoitems`.
* Changes all the `app.Map<HttpVerb>` methods to `todoItems.Map<HttpVerb>`.
* Removes the URL prefix `/todoitems` from the `Map<HttpVerb>` method calls.

Test the endpoints to verify that they work the same.

## Use the TypedResults API

Returning [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) rather than [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) has several advantages, including testability and automatically returning the response type metadata for OpenAPI to describe the endpoint. For more information, see [`TypedResults` versus `Results`](../fundamentals/minimal-apis/responses?view=aspnetcore-10.0#typedresults-versus-results).

The `Map<HttpVerb>` methods can call route handler methods instead of using lambdas. To see an example, update *Program.cs* with the following code:

* [Visual Studio](#tabpanel_12_visual-studio)
* [Visual Studio Code](#tabpanel_12_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

The `Map<HttpVerb>` code now calls methods instead of lambdas:

```
var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
```

These methods return objects that implement [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) and are defined by [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults):

```
static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

Unit tests can call these methods and test that they return the correct type. For example, if the method is `GetAllTodos`:

```
static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
```

Unit test code can verify that an object of type [Ok\<Todo\[\]>](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.ok-1.value#microsoft-aspnetcore-http-httpresults-ok-1-value) is returned from the handler method. For example:

```
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}
```

[]()

## Prevent over-posting

Currently the sample app exposes the entire `Todo` object. In production applications, a subset of the model is often used to restrict the data that can be input and returned. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. **DTO** is used in this article.

A DTO can be used to:

* Prevent over-posting.
* Hide properties that clients aren't supposed to view.
* Omit some properties to reduce payload size.
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.

To demonstrate the DTO approach, update the `Todo` class to include a secret field:

```
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}
```

The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

Verify you can post and get the secret field.

Create a file named `TodoItemDTO.cs` with the following code:

```
public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
```

Replace the contents of the `Program.cs` file with the following code to use this DTO model:

* [Visual Studio](#tabpanel_13_visual-studio)
* [Visual Studio Code](#tabpanel_13_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
    config.DocumentName = "TodoAPI";
    config.Title = "TodoAPI v1";
    config.Version = "v1";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseOpenApi();
    app.UseSwaggerUi(config =>
    {
        config.DocumentTitle = "TodoAPI";
        config.Path = "/swagger";
        config.DocumentPath = "/swagger/{documentName}/swagger.json";
        config.DocExpansion = "list";
    });
}

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}
```

Verify you can post and get all fields except the secret field.

[]()

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/min-web-api/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

* [Configure JSON serialization options](../fundamentals/minimal-apis/responses?view=aspnetcore-10.0#configure-json-serialization-options).
* Handle errors and exceptions: The [developer exception page](../fundamentals/error-handling-api?view=aspnetcore-10.0#developer-exception-page) is enabled by default in the `Development` environment for Minimal API apps. For information about how to handle errors and exceptions, see [Handle errors in ASP.NET Core APIs](../fundamentals/error-handling-api?view=aspnetcore-10.0).
* For an example of testing a Minimal API app, see [this GitHub sample](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/MinApiTestsSample).
* [OpenAPI support in Minimal APIs](../fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0).
* [Quickstart: Publish to Azure](/en-us/azure/app-service/quickstart-dotnetcore).
* [Organizing ASP.NET Core Minimal APIs](https://www.tessferrandez.com/blog/2023/10/31/organizing-minimal-apis.html).

### Learn more

See [Minimal APIs quick reference](../fundamentals/minimal-apis?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/secure-data
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/secure-data.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Create an ASP.NET Core web app with user data protected by authorization

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Joe Audette](https://twitter.com/joeaudette)

This tutorial shows how to create an ASP.NET Core web app with user data protected by authorization. It displays a list of contacts created by the authenticated (registered) users. The app supports three security groups:

* **Registered users** can view all *Approved* data and can edit/delete their own data.
* **Managers** can approve or reject contact data. Only contacts marked as *Approved* are visible to users.
* **Administrators** can approve/reject and edit/delete any data.

Note

The images in this article don't exactly match the latest templates.

In the following image, the user `rick@contoso.com` is signed in to the web app. This user can view only *Approved* contacts along with the **Edit**/**Delete**/**Create New** links for the contacts. In this view, only the last record (created by this user) displays the **Edit** and **Delete** links. Other users don't see the last record until a manager or administrator approves the record.

In the next image, the `manager@contoso.com` user is signed in and has access to the management features:

A manager can select a contact to see details about the user, as shown in the following image:

The **Approve** and **Reject** options display for managers and administrators only.

In the following image, the `admin@contoso.com` user is signed in and has access to the administration features:

An administrator has all privileges. They can read, edit, or delete any contact and change the status of contacts.

The app was created by [scaffolding](../../tutorials/first-mvc-app/adding-model?view=aspnetcore-10.0#scaffold-movie-pages) the following `Contact` model:

```
public class Contact
{
    public int ContactId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}
```

The sample contains the following authorization handlers:

* `ContactIsOwnerAuthorizationHandler`: Ensures that a user can only edit their data.
* `ContactManagerAuthorizationHandler`: Allows managers to approve or reject contacts.
* `ContactAdministratorsAuthorizationHandler`: Allows administrators to approve or reject contacts and to edit/delete contacts.

## Prerequisites

This tutorial is advanced. You should be familiar with:

* [ASP.NET Core](../../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0)
* [Authentication and ASP.NET Core Identity](../authentication/identity?view=aspnetcore-10.0)
* [Account confirmation and password recovery](../authentication/accconfirm?view=aspnetcore-10.0)
* [Authorization](introduction?view=aspnetcore-10.0)
* [Entity Framework Core](../../data/ef-mvc/intro?view=aspnetcore-10.0)

## The starter and completed app

[Download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample) the [completed](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authorization/secure-data/samples) app. [Test](#test-the-completed-app) the completed app so you become familiar with its security features.

Tip

You can use the [git sparse-checkout](https://git-scm.com/docs/git-sparse-checkout) command to download only the sample subfolder.

For example:

```
git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.git --sparse
cd AspNetCore.Docs
git sparse-checkout init --cone
git sparse-checkout set aspnetcore/security/authorization/secure-data/samples
```

### The starter app

[Download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample) the [starter](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authorization/secure-data/samples/) app.

Run the app, tap the **ContactManager** link, and verify you can create, edit, and delete a contact. To create the starter app, see [Create the starter app](#create-the-starter-app).

## Secure user data

The following sections demonstrate all the major steps to create the secure user data app. You might find it helpful to refer to the completed project.

### Tie the contact data to the user

Use the ASP.NET [Identity](../authentication/identity?view=aspnetcore-10.0) user ID to ensure users can edit their data, but not other users data. Add the `OwnerID` and `ContactStatus` fields to the `Contact` model:

```
public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string? OwnerID { get; set; }

    public string? Name { get; set; }
    public string? Address { get; set; }
    public string? City { get; set; }
    public string? State { get; set; }
    public string? Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string? Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}
```

`OwnerID` is the user's ID from the `AspNetUser` table in the [Identity](../authentication/identity?view=aspnetcore-10.0) database. The `Status` field determines if a contact is viewable by general users.

Create a new migration and update the database:

```
dotnet ef migrations add userID_Status
dotnet ef database update
```

### Add Role services to Identity

Enable the app to use Role services by appending the [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) method:

```
var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
```

### Require authenticated users

Set the fallback authorization policy to require users to be authenticated:

```
var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});
```

The preceding highlighted code sets the fallback authorization policy to require an authenticated user. The fallback policy is selected when the authorization middleware can't build a policy from authorization metadata. An endpoint with `[Authorize]` uses the default policy, and an endpoint with `[Authorize(Policy = "{POLICY NAME}")]` uses the named policy. Apply `[AllowAnonymous]` to an endpoint that is intentionally public.

The [RequireAuthenticatedUser](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireauthenticateduser) method adds the [DenyAnonymousAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.denyanonymousauthorizationrequirement) class to the current instance, which enforces that the current user is authenticated.

The fallback authorization policy applies to requests processed by the authorization middleware when no authorization policy is produced from endpoint metadata. It can also apply when a request doesn't match an endpoint. Middleware order determines whether the authorization middleware processes requests for resources such as [static files](../../fundamentals/static-files?view=aspnetcore-10.0#static-file-authorization).

Setting the fallback authorization policy to require users to be authenticated protects newly added Razor Pages and controllers. Having authorization required by default is more secure than relying on new controllers and Razor Pages to include the `[Authorize]` attribute.

For a comparison of named, default, and fallback policies, see [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0#default-and-fallback-policies).

As an alternate approach, MVC controllers and Razor Pages can add an authorization filter to require all users be authenticated:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddControllers(config =>
{
    var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});

var app = builder.Build();
```

The preceding code uses an authorization filter. Setting a fallback policy is the preferred way to require all users to be authenticated.

Add the [AllowAnonymous](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) attribute to the `Index` and `Privacy` pages so anonymous users can get information about the site before they register:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages;

[AllowAnonymous]
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {

    }
}
```

### Configure the test account

The `SeedData` class creates two accounts: administrator and manager. Use the [Secret Manager tool](../app-secrets?view=aspnetcore-10.0) to set a password for these accounts. Set the password from the project directory (the directory containing the *Program.cs* file):

```
dotnet user-secrets set SeedUserPW <PW>
```

If a weak password is specified, an exception is thrown when the `SeedData.Initialize` method is called.

Update the app to use the test password:

```
var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
                      ContactIsOwnerAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}
```

### Create the test accounts and update the contacts

Create the test accounts by updating the `Initialize` method in the `SeedData` class:

```
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser
        {
            UserName = UserName,
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, testUserPw);
    }

    if (user == null)
    {
        throw new Exception("The password is probably not strong enough!");
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    IdentityResult IR;
    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    //if (userManager == null)
    //{
    //    throw new Exception("userManager is null");
    //}

    var user = await userManager.FindByIdAsync(uid);

    if (user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }

    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}
```

Add the administrator user ID and the `ContactStatus` field to the contacts. Mark one of the contacts as *Submitted* and one as *Rejected*. Add the user ID and status to all the contacts. Only one contact is shown:

```
public static void SeedDB(ApplicationDbContext context, string adminID)
{
    if (context.Contact.Any())
    {
        return;   // DB has been seeded
    }

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },
```

## Create owner, manager, and administrator authorization handlers

Create a `ContactIsOwnerAuthorizationHandler` class in the *Authorization* folder. The `ContactIsOwnerAuthorizationHandler` verifies that the user acting on a resource owns the resource.

```
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<IdentityUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.CompletedTask;
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}
```

The `ContactIsOwnerAuthorizationHandler` calls the [context.Succeed](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.succeed) method if the current authenticated user is the contact owner.

The authorization handler generally:

* Calls the `context.Succeed` method when the requirements are met.
* When requirements aren't met, it returns `Task.CompletedTask`. When `Task.CompletedTask` is returned without a prior call to `context.Succeed` or `context.Fail`, the result isn't a success or failure. Instead, it allows other authorization handlers to run.

If you need to explicitly fail, call the [context.Fail](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.fail) method.

The app allows contact owners to edit/delete/create their own data. `ContactIsOwnerAuthorizationHandler` doesn't need to check the operation passed in the requirement parameter.

### Create a manager authorization handler

Create a `ContactManagerAuthorizationHandler` class in the *Authorization* folder. The `ContactManagerAuthorizationHandler` verifies the user acting on the resource is a manager. Only managers can approve or reject content changes (new or changed).

```
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}
```

### Create an administrator authorization handler

Create a `ContactAdministratorsAuthorizationHandler` class in the *Authorization* folder. The `ContactAdministratorsAuthorizationHandler` verifies the user acting on the resource is an administrator. Administrator can do all operations.

```
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}
```

## Register the authorization handlers

Services that use Entity Framework Core must be registered for [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) with the [AddScoped](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addscoped) method. The `ContactIsOwnerAuthorizationHandler` uses ASP.NET Core [Identity](../authentication/identity?view=aspnetcore-10.0), which is built on Entity Framework Core. Register the handlers with the service collection so they're available to the `ContactsController` through [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Add the following code to the end of `ConfigureServices`:

```
var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
                      ContactIsOwnerAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}
```

`ContactAdministratorsAuthorizationHandler` and `ContactManagerAuthorizationHandler` are added as singletons. They're singletons because they don't use Entity Framework and all the information needed is in the `Context` parameter of the `HandleRequirementAsync` method.

## Support authorization

In this section, you update the Razor Pages and add an operations requirements class.

### Review the contact operations requirements class

Review the `ContactOperations` class. This class contains the requirements the app supports:

```
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}
```

### Create a base class for the Contacts Razor Pages

Create a base class that contains the services used in the contacts Razor Pages. The base class puts the initialization code in one location:

```
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}
```

The preceding code:

* Adds the `IAuthorizationService` service to access to the authorization handlers.
* Adds the Identity `UserManager` service.
* Add the `ApplicationDbContext`.

### Update the CreateModel

Update the create page model:

* Define the constructor to use the `DI_BasePageModel` base class.

* Configure the `OnPostAsync` method to:

  * Add the user ID to the `Contact` model.
  * Call the authorization handler to verify the user has permission to create contacts.

```
using ContactManager.Authorization;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace ContactManager.Pages.Contacts
{
    public class CreateModel : DI_BasePageModel
    {
        public CreateModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager)
            : base(context, authorizationService, userManager)
        {
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Contact Contact { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            Contact.OwnerID = UserManager.GetUserId(User);

            var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                        User, Contact,
                                                        ContactOperations.Create);
            if (!isAuthorized.Succeeded)
            {
                return Forbid();
            }

            Context.Contact.Add(Contact);
            await Context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}
```

### Update the IndexModel

Update the `OnGetAsync` method so only *Approved* contacts are shown to standard registered users:

```
public class IndexModel : DI_BasePageModel
{
    public IndexModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}
```

### Update the EditModel

Add an authorization handler to verify the user owns the contact. Because resource authorization is being validated, the `[Authorize]` attribute isn't enough. The app doesn't have access to the resource when attributes are evaluated. Resource-based authorization must be imperative. Checks must be performed after the app has access to the resource, either by loading it in the page model or by loading it within the handler itself. You frequently access the resource by passing in the resource key.

```
public class EditModel : DI_BasePageModel
{
    public EditModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? contact = await Context.Contact.FirstOrDefaultAsync(
                                                         m => m.ContactId == id);
        if (contact == null)
        {
            return NotFound();
        }

        Contact = contact;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                  User, Contact,
                                                  ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch Contact from DB to get OwnerID.
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Contact.OwnerID = contact.OwnerID;

        Context.Attach(Contact).State = EntityState.Modified;

        if (Contact.Status == ContactStatus.Approved)
        {
            // If the contact is updated after approval, 
            // and the user cannot approve,
            // set the status back to submitted so the update can be
            // checked and approved.
            var canApprove = await AuthorizationService.AuthorizeAsync(User,
                                    Contact,
                                    ContactOperations.Approve);

            if (!canApprove.Succeeded)
            {
                Contact.Status = ContactStatus.Submitted;
            }
        }

        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

### Update the DeleteModel

Update the delete page model to use the authorization handler and verify the user has delete permission on the contact.

```
public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Context.Contact.Remove(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

## Inject the authorization service into the views

Currently, the UI shows edit and delete links for contacts the user can't modify.

Inject the authorization service in the *Pages/\_ViewImports.cshtml* file so it's available to all views:

```
@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService
```

The preceding markup adds several `using` statements.

Update the **Edit** and **Delete** links in *Pages/Contacts/Index.cshtml* file so they're only rendered for users with the appropriate permissions:

```
@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
             <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Contact) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Address)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.City)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.State)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Zip)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Email)
            </td>
                           <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>
```

Warning

Hiding links from users that don't have permission to change data doesn't secure the app. Hiding links makes the app more user-friendly by displaying only valid links. Users can hack the generated URLs to invoke edit and delete operations on data they don't own. The Razor Page or controller must enforce access checks to secure the data.

### Update Details

Update the details view so managers can approve or reject contacts:

```
        @*Preceding markup omitted for brevity.*@
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
    <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Approve)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }
}

@if (Model.Contact.Status != ContactStatus.Rejected)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Reject)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-danger">Reject</button>
        </form>
    }
}

<div>
    @if ((await AuthorizationService.AuthorizeAsync(
         User, Model.Contact,
         ContactOperations.Update)).Succeeded)
    {
        <a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
        <text> | </text>
    }
    <a asp-page="./Index">Back to List</a>
</div>
```

### Update the details page model

```
public class DetailsModel : DI_BasePageModel
{
    public DetailsModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
    {
        var contact = await Context.Contact.FirstOrDefaultAsync(
                                                  m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var contactOperation = (status == ContactStatus.Approved)
                                                   ? ContactOperations.Approve
                                                   : ContactOperations.Reject;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
                                    contactOperation);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

## Add or remove user roles

Updating a user's role assignments lets you control the privileges available to the user. Remove a user from a role and reduce their ability to change data, such as editing or deleting a contact. Add a user to a role and increase their privileges for making global changes. You can also use role assignments to limit user participation, such as muting a user in a chat conversation.

For more information, see [GitHub dotnet/aspnetcore issue #8502](https://github.com/dotnet/AspNetCore.Docs/issues/8502) - *Mute or remove privileges from a user. Admin changes*.

[]()

## Differences between Challenge and Forbid

This app sets the default policy to [require authenticated users](#require-authenticated-users). The following code allows anonymous users. Anonymous users are allowed to show the differences between Challenge versus Forbid.

```
[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
    public Details2Model(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        if (!User.Identity!.IsAuthenticated)
        {
            return Challenge();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }
}
```

In the preceding code:

* When the user is **not** authenticated, a `ChallengeResult` is returned. When a `ChallengeResult` is returned, the user is redirected to the sign-in page.
* When the user is authenticated, but not authorized, a `ForbidResult` is returned. When a `ForbidResult` is returned, the user is redirected to the access denied page.

## Test the completed app

Warning

This article uses the [Secret Manager tool](../app-secrets?view=aspnetcore-10.0) to store the password for the seeded user accounts. The Secret Manager tool is used to store sensitive data during local development. For information on authentication procedures that can be used when an app is deployed to a test or production environment, see [Secure authentication flows](../?view=aspnetcore-10.0#secure-authentication-flows).

If you haven't already set a password for seeded user accounts, use the [Secret Manager tool](../app-secrets?view=aspnetcore-10.0#secret-manager) to set a password:

* Choose a [strong password](https://support.microsoft.com/account-billing/how-to-help-keep-your-microsoft-account-secure-628538c2-7006-33bb-5ef4-c917657362b9):

  * At least 12 characters long, but 14 or more is better.
  * A combination of uppercase letters, lowercase letters, numbers, and symbols.
  * Not a word that can be found in a dictionary or the name of a person, character, product, or organization.
  * Significantly different from your previous passwords.
  * Easy for you to remember but difficult for others to guess. Consider using a memorable phrase like `6MonkeysRLooking^`.

* Execute the following command from the project's folder, where `<PW>` is the password:

  ```
  dotnet user-secrets set SeedUserPW <PW>
  ```

If the app has contacts:

* Delete all of the records in the `Contact` table.
* Restart the app to seed the database.

An easy way to test the completed app is to launch three different browsers (or incognito/InPrivate sessions). In one browser, register a new user (for example, `test@contoso.com`). Sign in to each browser with a different user. Verify the following operations:

* Registered users can view all of the *Approved* contact data.
* Registered users can edit/delete their own data.
* Managers can approve/reject contact data. The `Details` view shows **Approve** and **Reject** buttons.
* Administrators can approve/reject and edit/delete all data.

| User                  | Approve/Reject contacts | Options                         |
| --------------------- | ----------------------- | ------------------------------- |
| `test@contoso.com`    | No                      | Edit and delete their data.     |
| `manager@contoso.com` | Yes                     | Edit and delete their data.     |
| `admin@contoso.com`   | Yes                     | Edit and delete ***all*** data. |

Create a contact in the administrator's browser. Copy the URL for delete and edit from the administrator contact. Paste these links into the test user's browser and verify the test user can't perform these operations.

## Create the starter app

* Create a Razor Pages app:

  * Create the app with *Individual Accounts*.
  * Name the app **ContactManager**, so the namespace matches the namespace used in the sample.
  * Use the `-uld` flag to specify LocalDB instead of SQLite.

  ```
  dotnet new webapp -o ContactManager -au Individual -uld
  ```

* Add the *Models/Contact.cs* file:

  ```
  using System.ComponentModel.DataAnnotations;

  namespace ContactManager.Models
  {
      public class Contact
      {
          public int ContactId { get; set; }
          public string? Name { get; set; }
          public string? Address { get; set; }
          public string? City { get; set; }
          public string? State { get; set; }
          public string? Zip { get; set; }
          [DataType(DataType.EmailAddress)]
          public string? Email { get; set; }
      }
  }
  ```

* Scaffold the `Contact` model.

* Create initial migration and update the database:

  ```
  dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
  dotnet tool install -g dotnet-aspnet-codegenerator
  dotnet-aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
  dotnet ef database drop -f
  dotnet ef migrations add initial
  dotnet ef database update
  ```

  Note

  By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

* Update the **ContactManager** anchor in the *Pages/Shared/\_Layout.cshtml* file:

  ```
  <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
  ```

* Test the app by creating, editing, and deleting a contact

### Seed the database

Add the [SeedData](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authorization/secure-data/samples/starter6/Data/SeedData.cs) class to the *Data* folder:

```
using ContactManager.Models;
using Microsoft.EntityFrameworkCore;

// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries

namespace ContactManager.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw="")
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {
                SeedDB(context, testUserPw);
            }
        }

        public static void SeedDB(ApplicationDbContext context, string adminID)
        {
            if (context.Contact.Any())
            {
                return;   // DB has been seeded
            }

            context.Contact.AddRange(
                new Contact
                {
                    Name = "Debra Garcia",
                    Address = "1234 Main St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "debra@example.com"
                },
                new Contact
                {
                    Name = "Thorsten Weinrich",
                    Address = "5678 1st Ave W",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "thorsten@example.com"
                },
                new Contact
                {
                    Name = "Yuhong Li",
                    Address = "9012 State st",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "yuhong@example.com"
                },
                new Contact
                {
                    Name = "Jon Orton",
                    Address = "3456 Maple St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "jon@example.com"
                },
                new Contact
                {
                    Name = "Diliana Alexieva-Bosseva",
                    Address = "7890 2nd Ave E",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "diliana@example.com"
                }
             );
            context.SaveChanges();
        }

    }
}
```

Call the `SeedData.Initialize` method from the *Program.cs* file:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    await SeedData.Initialize(services);
}

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

Test the app and confirm the database is seeded. If there are any rows in the contact database, the seed method doesn't run.

This tutorial shows how to create an ASP.NET Core web app with user data protected by authorization. It displays a list of contacts that authenticated (registered) users have created. There are three security groups:

* **Registered users** can view all the approved data and can edit/delete their own data.
* **Managers** can approve or reject contact data. Only approved contacts are visible to users.
* **Administrators** can approve/reject and edit/delete any data.

The images in this document don't exactly match the latest templates.

In the following image, user Rick (`rick@example.com`) is signed in. Rick can only view approved contacts and **Edit**/**Delete**/**Create New** links for his contacts. Only the last record, created by Rick, displays **Edit** and **Delete** links. Other users won't see the last record until a manager or administrator changes the status to "Approved".

In the following image, `manager@contoso.com` is signed in and in the manager's role:

The following image shows the managers details view of a contact:

The **Approve** and **Reject** buttons are only displayed for managers and administrators.

In the following image, `admin@contoso.com` is signed in and in the administrator's role:

The administrator has all privileges. She can read/edit/delete any contact and change the status of contacts.

The app was created by [scaffolding](../../tutorials/first-mvc-app/adding-model?view=aspnetcore-10.0#scaffold-movie-pages) the following `Contact` model:

```
public class Contact
{
    public int ContactId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}
```

The sample contains the following authorization handlers:

* `ContactIsOwnerAuthorizationHandler`: Ensures that a user can only edit their data.

* `ContactManagerAuthorizationHandler`: Allows managers to approve or reject contacts.

* `ContactAdministratorsAuthorizationHandler`: Allows administrators to:

  * Approve or reject contacts
  * Edit and delete contacts

## Prerequisites

This tutorial is advanced. You should be familiar with:

* [ASP.NET Core](../../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0)
* [Authentication](../authentication/identity?view=aspnetcore-10.0)
* [Account Confirmation and Password Recovery](../authentication/accconfirm?view=aspnetcore-10.0)
* [Authorization](introduction?view=aspnetcore-10.0)
* [Entity Framework Core](../../data/ef-mvc/intro?view=aspnetcore-10.0)

## The starter and completed app

[Download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample) the [completed](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authorization/secure-data/samples) app. [Test](#test-the-completed-app) the completed app so you become familiar with its security features.

### The starter app

[Download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample) the [starter](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authorization/secure-data/samples/) app.

Run the app, tap the **ContactManager** link, and verify you can create, edit, and delete a contact. To create the starter app, see [Create the starter app](#create-the-starter-app).

## Secure user data

The following sections have all the major steps to create the secure user data app. You may find it helpful to refer to the completed project.

### Tie the contact data to the user

Use the ASP.NET [Identity](../authentication/identity?view=aspnetcore-10.0) user ID to ensure users can edit their data, but not other users data. Add `OwnerID` and `ContactStatus` to the `Contact` model:

```
public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string OwnerID { get; set; }

    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}
```

`OwnerID` is the user's ID from the `AspNetUser` table in the [Identity](../authentication/identity?view=aspnetcore-10.0) database. The `Status` field determines if a contact is viewable by general users.

Create a new migration and update the database:

```
dotnet ef migrations add userID_Status
dotnet ef database update
```

### Add Role services to Identity

Append [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) to add Role services:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();
```

[]()

### Require authenticated users

Set the fallback authorization policy to require users to be authenticated:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });
```

The preceding highlighted code sets the fallback authorization policy to require an authenticated user. The fallback policy is selected when the authorization middleware can't build a policy from authorization metadata. An endpoint with `[Authorize]` uses the default policy, and an endpoint with `[Authorize(Policy = "{POLICY NAME}")]` uses the named policy. Apply `[AllowAnonymous]` to an endpoint that is intentionally public.

[RequireAuthenticatedUser](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireauthenticateduser) adds [DenyAnonymousAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.denyanonymousauthorizationrequirement) to the current instance, which enforces that the current user is authenticated.

The fallback authorization policy applies to requests processed by the authorization middleware when no authorization policy is produced from endpoint metadata. It can also apply when a request doesn't match an endpoint. Middleware order determines whether the authorization middleware processes requests for resources such as [static files](../../fundamentals/static-files?view=aspnetcore-10.0#static-file-authorization).

Setting the fallback authorization policy to require users to be authenticated protects newly added Razor Pages and controllers. Having authorization required by default is more secure than relying on new controllers and Razor Pages to include the `[Authorize]` attribute.

For a comparison of named, default, and fallback policies, see [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0#default-and-fallback-policies).

An alternative way for MVC controllers and Razor Pages to require all users be authenticated is adding an authorization filter:

```
public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddControllers(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });
```

The preceding code uses an authorization filter. Setting a fallback policy is the preferred way to require all users to be authenticated.

Add [AllowAnonymous](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) to the `Index` and `Privacy` pages so anonymous users can get information about the site before they register:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace ContactManager.Pages
{
    [AllowAnonymous]
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {

        }
    }
}
```

### Configure the test account

The `SeedData` class creates two accounts: administrator and manager. Use the [Secret Manager tool](../app-secrets?view=aspnetcore-10.0) to set a password for these accounts. Set the password from the project directory (the directory containing `Program.cs`):

```
dotnet user-secrets set SeedUserPW <PW>
```

If a strong password is not specified, an exception is thrown when `SeedData.Initialize` is called.

Update `Main` to use the test password:

```
public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                var context = services.GetRequiredService<ApplicationDbContext>();
                context.Database.Migrate();

                // requires using Microsoft.Extensions.Configuration;
                var config = host.Services.GetRequiredService<IConfiguration>();
                // Set password with the Secret Manager tool.
                // dotnet user-secrets set SeedUserPW <pw>

                var testUserPw = config["SeedUserPW"];

                SeedData.Initialize(services, testUserPw).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

### Create the test accounts and update the contacts

Update the `Initialize` method in the `SeedData` class to create the test accounts:

```
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser
        {
            UserName = UserName,
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, testUserPw);
    }

    if (user == null)
    {
        throw new Exception("The password is probably not strong enough!");
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    IdentityResult IR;
    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    //if (userManager == null)
    //{
    //    throw new Exception("userManager is null");
    //}

    var user = await userManager.FindByIdAsync(uid);

    if (user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }

    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}
```

Add the administrator user ID and `ContactStatus` to the contacts. Make one of the contacts "Submitted" and one "Rejected". Add the user ID and status to all the contacts. Only one contact is shown:

```
public static void SeedDB(ApplicationDbContext context, string adminID)
{
    if (context.Contact.Any())
    {
        return;   // DB has been seeded
    }

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },
```

## Create owner, manager, and administrator authorization handlers

Create a `ContactIsOwnerAuthorizationHandler` class in the *Authorization* folder. The `ContactIsOwnerAuthorizationHandler` verifies that the user acting on a resource owns the resource.

```
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<IdentityUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.CompletedTask;
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}
```

The `ContactIsOwnerAuthorizationHandler` calls [context.Succeed](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.succeed) if the current authenticated user is the contact owner. Authorization handlers generally:

* Call `context.Succeed` when the requirements are met.
* Return `Task.CompletedTask` when requirements aren't met. Returning `Task.CompletedTask` without a prior call to `context.Success` or `context.Fail`, is not a success or failure, it allows other authorization handlers to run.

If you need to explicitly fail, call [context.Fail](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.fail).

The app allows contact owners to edit/delete/create their own data. `ContactIsOwnerAuthorizationHandler` doesn't need to check the operation passed in the requirement parameter.

### Create a manager authorization handler

Create a `ContactManagerAuthorizationHandler` class in the *Authorization* folder. The `ContactManagerAuthorizationHandler` verifies the user acting on the resource is a manager. Only managers can approve or reject content changes (new or changed).

```
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}
```

### Create an administrator authorization handler

Create a `ContactAdministratorsAuthorizationHandler` class in the *Authorization* folder. The `ContactAdministratorsAuthorizationHandler` verifies the user acting on the resource is an administrator. Administrator can do all operations.

```
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}
```

## Register the authorization handlers

Services using Entity Framework Core must be registered for [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) using [AddScoped](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addscoped). The `ContactIsOwnerAuthorizationHandler` uses ASP.NET Core [Identity](../authentication/identity?view=aspnetcore-10.0), which is built on Entity Framework Core. Register the handlers with the service collection so they're available to the `ContactsController` through [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Add the following code to the end of `ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

    // Authorization handlers.
    services.AddScoped<IAuthorizationHandler,
                          ContactIsOwnerAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactManagerAuthorizationHandler>();
}
```

`ContactAdministratorsAuthorizationHandler` and `ContactManagerAuthorizationHandler` are added as singletons. They're singletons because they don't use EF and all the information needed is in the `Context` parameter of the `HandleRequirementAsync` method.

## Support authorization

In this section, you update the Razor Pages and add an operations requirements class.

### Review the contact operations requirements class

Review the `ContactOperations` class. This class contains the requirements the app supports:

```
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}
```

### Create a base class for the Contacts Razor Pages

Create a base class that contains the services used in the contacts Razor Pages. The base class puts the initialization code in one location:

```
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}
```

The preceding code:

* Adds the `IAuthorizationService` service to access to the authorization handlers.
* Adds the Identity `UserManager` service.
* Add the `ApplicationDbContext`.

### Update the CreateModel

Update the create page model constructor to use the `DI_BasePageModel` base class:

```
public class CreateModel : DI_BasePageModel
{
    public CreateModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }
```

Update the `CreateModel.OnPostAsync` method to:

* Add the user ID to the `Contact` model.
* Call the authorization handler to verify the user has permission to create contacts.

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    Contact.OwnerID = UserManager.GetUserId(User);

    // requires using ContactManager.Authorization;
    var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                User, Contact,
                                                ContactOperations.Create);
    if (!isAuthorized.Succeeded)
    {
        return Forbid();
    }

    Context.Contact.Add(Contact);
    await Context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

### Update the IndexModel

Update the `OnGetAsync` method so only approved contacts are shown to general users:

```
public class IndexModel : DI_BasePageModel
{
    public IndexModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}
```

### Update the EditModel

Add an authorization handler to verify the user owns the contact. Because resource authorization is being validated, the `[Authorize]` attribute is not enough. The app doesn't have access to the resource when attributes are evaluated. Resource-based authorization must be imperative. Checks must be performed once the app has access to the resource, either by loading it in the page model or by loading it within the handler itself. You frequently access the resource by passing in the resource key.

```
public class EditModel : DI_BasePageModel
{
    public EditModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                  User, Contact,
                                                  ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch Contact from DB to get OwnerID.
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Contact.OwnerID = contact.OwnerID;

        Context.Attach(Contact).State = EntityState.Modified;

        if (Contact.Status == ContactStatus.Approved)
        {
            // If the contact is updated after approval, 
            // and the user cannot approve,
            // set the status back to submitted so the update can be
            // checked and approved.
            var canApprove = await AuthorizationService.AuthorizeAsync(User,
                                    Contact,
                                    ContactOperations.Approve);

            if (!canApprove.Succeeded)
            {
                Contact.Status = ContactStatus.Submitted;
            }
        }

        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

### Update the DeleteModel

Update the delete page model to use the authorization handler to verify the user has delete permission on the contact.

```
public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Context.Contact.Remove(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

## Inject the authorization service into the views

Currently, the UI shows edit and delete links for contacts the user can't modify.

Inject the authorization service in the `Pages/_ViewImports.cshtml` file so it's available to all views:

```
@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService
```

The preceding markup adds several `using` statements.

Update the **Edit** and **Delete** links in `Pages/Contacts/Index.cshtml` so they're only rendered for users with the appropriate permissions:

```
@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Contact)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Address)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.City)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.State)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Zip)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Email)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>
```

Warning

Hiding links from users that don't have permission to change data doesn't secure the app. Hiding links makes the app more user-friendly by displaying only valid links. Users can hack the generated URLs to invoke edit and delete operations on data they don't own. The Razor Page or controller must enforce access checks to secure the data.

### Update Details

Update the details view so managers can approve or reject contacts:

```
        @*Precedng markup omitted for brevity.*@
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Approve)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }
}

@if (Model.Contact.Status != ContactStatus.Rejected)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Reject)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-danger">Reject</button>
        </form>
    }
}

<div>
    @if ((await AuthorizationService.AuthorizeAsync(
         User, Model.Contact,
         ContactOperations.Update)).Succeeded)
    {
        <a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
        <text> | </text>
    }
    <a asp-page="./Index">Back to List</a>
</div>
```

Update the details page model:

```
public class DetailsModel : DI_BasePageModel
{
    public DetailsModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
    {
        var contact = await Context.Contact.FirstOrDefaultAsync(
                                                  m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var contactOperation = (status == ContactStatus.Approved)
                                                   ? ContactOperations.Approve
                                                   : ContactOperations.Reject;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
                                    contactOperation);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

## Add or remove a user to a role

See [this issue](https://github.com/dotnet/AspNetCore.Docs/issues/8502) for information on:

* Removing privileges from a user. For example, muting a user in a chat app.
* Adding privileges to a user.

[]()

## Differences between Challenge and Forbid

This app sets the default policy to [require authenticated users](#require-authenticated-users). The following code allows anonymous users. Anonymous users are allowed to show the differences between Challenge vs Forbid.

```
[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
    public Details2Model(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        if (!User.Identity.IsAuthenticated)
        {
            return Challenge();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized
            && currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved)
        {
            return Forbid();
        }

        return Page();
    }
}
```

In the preceding code:

* When the user is **not** authenticated, a `ChallengeResult` is returned. When a `ChallengeResult` is returned, the user is redirected to the sign-in page.
* When the user is authenticated, but not authorized, a `ForbidResult` is returned. When a `ForbidResult` is returned, the user is redirected to the access denied page.

## Test the completed app

If you haven't already set a password for seeded user accounts, use the [Secret Manager tool](../app-secrets?view=aspnetcore-10.0#secret-manager) to set a password:

* Choose a strong password: Use eight or more characters and at least one upper-case character, number, and symbol. For example, `Passw0rd!` meets the strong password requirements.

* Execute the following command from the project's folder, where `<PW>` is the password:

  ```
  dotnet user-secrets set SeedUserPW <PW>
  ```

If the app has contacts:

* Delete all of the records in the `Contact` table.
* Restart the app to seed the database.

An easy way to test the completed app is to launch three different browsers (or incognito/InPrivate sessions). In one browser, register a new user (for example, `test@example.com`). Sign in to each browser with a different user. Verify the following operations:

* Registered users can view all of the approved contact data.
* Registered users can edit/delete their own data.
* Managers can approve/reject contact data. The `Details` view shows **Approve** and **Reject** buttons.
* Administrators can approve/reject and edit/delete all data.

| User                 | Seeded by the app | Options                                  |
| -------------------- | ----------------- | ---------------------------------------- |
| test\@example.com    | No                | Edit/delete the own data.                |
| manager\@contoso.com | Yes               | Approve/reject and edit/delete own data. |
| admin\@contoso.com   | Yes               | Approve/reject and edit/delete all data. |

Create a contact in the administrator's browser. Copy the URL for delete and edit from the administrator contact. Paste these links into the test user's browser to verify the test user can't perform these operations.

## Create the starter app

* Create a Razor Pages app named "ContactManager"

  * Create the app with **Individual Accounts**.
  * Name it "ContactManager" so the namespace matches the namespace used in the sample.
  * `-uld` specifies LocalDB instead of SQLite

  ```
  dotnet new webapp -o ContactManager -au Individual -uld
  ```

* Add `Models/Contact.cs`:

  ```
  public class Contact
  {
      public int ContactId { get; set; }
      public string Name { get; set; }
      public string Address { get; set; }
      public string City { get; set; }
      public string State { get; set; }
      public string Zip { get; set; }
      [DataType(DataType.EmailAddress)]
      public string Email { get; set; }
  }
  ```

* Scaffold the `Contact` model.

* Create initial migration and update the database:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
dotnet ef database drop -f
dotnet ef migrations add initial
dotnet ef database update
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

If you experience a bug with the `dotnet aspnet-codegenerator razorpage` command, see [this GitHub issue](https://github.com/aspnet/Scaffolding/issues/984).

* Update the **ContactManager** anchor in the `Pages/Shared/_Layout.cshtml` file:

```
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
```

* Test the app by creating, editing, and deleting a contact

### Seed the database

Add the [SeedData](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authorization/secure-data/samples/starter3/Data/SeedData.cs) class to the *Data* folder:

```
using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;

// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries

namespace ContactManager.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {              
                SeedDB(context, "0");
            }
        }        

        public static void SeedDB(ApplicationDbContext context, string adminID)
        {
            if (context.Contact.Any())
            {
                return;   // DB has been seeded
            }

            context.Contact.AddRange(
                new Contact
                {
                    Name = "Debra Garcia",
                    Address = "1234 Main St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "debra@example.com"
                },
                new Contact
                {
                    Name = "Thorsten Weinrich",
                    Address = "5678 1st Ave W",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "thorsten@example.com"
                },
                new Contact
                {
                    Name = "Yuhong Li",
                    Address = "9012 State st",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "yuhong@example.com"
                },
                new Contact
                {
                    Name = "Jon Orton",
                    Address = "3456 Maple St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "jon@example.com"
                },
                new Contact
                {
                    Name = "Diliana Alexieva-Bosseva",
                    Address = "7890 2nd Ave E",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "diliana@example.com"
                }
             );
            context.SaveChanges();
        }

    }
}
```

Call `SeedData.Initialize` from `Main`:

```
using ContactManager.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContactManager
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    var context = services.GetRequiredService<ApplicationDbContext>();
                    context.Database.Migrate();
                    SeedData.Initialize(services, "not used");
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

Test that the app seeded the database. If there are any rows in the contact DB, the seed method doesn't run.

## Related content

* [Tutorial: Build an ASP.NET Core and Azure SQL Database app in Azure App Service](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app)
* [ASP.NET Core Authorization Lab](https://github.com/blowdart/AspNetAuthorizationWorkshop) (extended details on security features)
* [Introduction to authorization in ASP.NET Core](introduction?view=aspnetcore-10.0)
* [Custom policy-based authorization](policies?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/interprocess
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/interprocess.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Inter-process communication with gRPC

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Processes running on the same machine can be designed to communicate with each other. Operating systems provide technologies for enabling fast and efficient [inter-process communication (IPC)](https://wikipedia.org/wiki/Inter-process_communication). Popular examples of IPC technologies are Unix domain sockets and Named pipes.

.NET provides support for inter-process communication using gRPC.

Built-in support for Named pipes in ASP.NET Core requires .NET 8 or later.

## Get started

IPC calls are sent from a client to a server. To communicate between apps on a machine with gRPC, at least one app must host an ASP.NET Core gRPC server.

An ASP.NET Core gRPC server is usually created from the gRPC template. The project file created by the template uses `Microsoft.NET.SDK.Web` as the SDK:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>
```

The `Microsoft.NET.SDK.Web` SDK value automatically adds a reference to the ASP.NET Core framework. The reference allows the app to use ASP.NET Core types required to host a server.

It's also possible to add a server to existing non-ASP.NET Core projects, such as Windows Services, WPF apps, or WinForms apps. See [Host gRPC in non-ASP.NET Core projects](aspnetcore?view=aspnetcore-10.0#host-grpc-in-non-aspnet-core-projects) for more information.

## Inter-process communication (IPC) transports

gRPC calls between a client and server on different machines are usually sent over TCP sockets. TCP is a good choice for communicating across a network or the Internet. However, IPC transports offer advantages when communicating between processes on the same machine:

* Less overhead and faster transfer speeds.
* Integration with OS security features.
* Doesn't use TCP ports, which are a limited resource.

.NET supports multiple IPC transports:

* [Unix domain sockets (UDS)](https://wikipedia.org/wiki/Unix_domain_socket) is a widely supported IPC technology. UDS is the best choice for building cross-platform apps, and it's usable on Linux, macOS, and [Windows 10/Windows Server 2019 or later](https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/).
* [Named pipes](https://wikipedia.org/wiki/Named_pipe) are supported by all versions of Windows. Named pipes integrate well with [Windows security](/en-us/windows/win32/ipc/named-pipe-security-and-access-rights), which can control client access to the pipe.
* Additional IPC transports by implementing [IConnectionListenerFactory](/en-us/dotnet/api/microsoft.aspnetcore.connections.iconnectionlistenerfactory) and registering the implementation at app startup.

Depending on the OS, cross-platform apps may choose different IPC transports. An app can check the OS on startup and choose the desired transport for that platform:

```
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    if (OperatingSystem.IsWindows())
    {
        serverOptions.ListenNamedPipe("MyPipeName");
    }
    else
    {
        var socketPath = Path.Combine(Path.GetTempPath(), "socket.tmp");
        serverOptions.ListenUnixSocket(socketPath);
    }

    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
    });
});
```

## Security considerations

IPC apps send and receive RPC calls. External communication is a potential attack vector for IPC apps and must be properly secured.

### Secure IPC server app against unexpected callers

The IPC server app hosts RPC services for other apps to call. Incoming callers should be authenticated to prevent untrusted clients from making RPC calls to the server.

Transport security is one option for securing a server. IPC transports, such as Unix domain sockets and named pipes, support limiting access based on operating system permissions:

* Named pipes supports securing a pipe with the [Windows access control model](/en-us/windows/win32/ipc/named-pipe-security-and-access-rights). Access rights can be configured in .NET when a server is started using the [PipeSecurity](/en-us/dotnet/api/system.io.pipes.pipesecurity) class.
* Unix domain sockets support securing a socket with file permissions.

Another option for securing an IPC server is to use authentication and authorization built into ASP.NET Core. For example, the server could be configured to require [certificate authentication](../security/authentication/certauth?view=aspnetcore-10.0). RPC calls made by client apps without the required certificate fail with an unauthorized response.

### Validate the server in the IPC client app

It's important for the client app to validate the identity of the server it is calling. Validation is necessary to protect against a malicious actor from stopping the trusted server, running their own, and accepting incoming data from clients.

Named pipes provides support for getting the account that a server is running under. A client can validate the server was launched by the expected account:

```
internal static bool CheckPipeConnectionOwnership(
    NamedPipeClientStream pipeStream, SecurityIdentifier expectedOwner)
{
    var remotePipeSecurity = pipeStream.GetAccessControl();
    var remoteOwner = remotePipeSecurity.GetOwner(typeof(SecurityIdentifier));
    return expectedOwner.Equals(remoteOwner);
}
```

Another option for validating the server is to [secure its endpoints with HTTPS](/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints#configure-https) inside ASP.NET Core. The client can configure `SocketsHttpHandler` to validate the server is using the expected certificate when the connection is established.

```
var socketsHttpHandler = new SocketsHttpHandler()
{
    SslOptions = new SslOptions()
    {
        RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
        {
            if (sslPolicyErrors != SslPolicyErrors.None)
            {
                return false;
            }

            // Validate server cert thumbprint matches the expected thumbprint.
        }
    }
};
```

### Protect against named pipe privilege escalation

Named pipes supports a feature called [impersonation](/en-us/windows/win32/ipc/impersonating-a-named-pipe-client). Using impersonation, the named pipes server can execute code with the privileges of the client user. This is a powerful feature but can allow a low-privilege server to impersonate a high-privilege caller and then run malicious code.

Client's can protect against this attack by not allowing impersonation when connecting to a server. Unless required by a server, a [TokenImpersonationLevel](/en-us/dotnet/api/system.security.principal.tokenimpersonationlevel) value of `None` or `Anonymous` should be used when creating a client connection:

```
using var pipeClient = new NamedPipeClientStream(
    serverName: ".", pipeName: "testpipe", PipeDirection.In, PipeOptions.None, TokenImpersonationLevel.None);
await pipeClient.ConnectAsync();
```

`TokenImpersonationLevel.None` is the default value in `NamedPipeClientStream` constructors that don't have an `impersonationLevel` parameter.

## Configure client and server

The client and server must be configured to use an inter-process communication (IPC) transport. For more information about configuring Kestrel and [SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler) to use IPC:

* [Inter-process communication with gRPC and Unix domain sockets](interprocess-uds?view=aspnetcore-10.0)
* [Inter-process communication with gRPC and Named pipes](interprocess-namedpipes?view=aspnetcore-10.0)

Note

Built-in support for Named pipes in ASP.NET Core requires .NET 8 or later.

Warning

An IPC transport, such as Unix domain sockets or named pipes, requires the client to configure `SocketsHttpHandler.ConnectCallback` to create a connection to the custom endpoint. A channel that uses a `ConnectCallback` doesn't support client-side load balancing or connectivity state tracking, and the following features aren't available:

* Client-side load balancing configured with `ServiceConfig.LoadBalancingConfigs`.
* Connectivity state tracking with `GrpcChannel.State` and `GrpcChannel.WaitForStateChangedAsync`.

Attempting to use these features on a channel configured for IPC throws an exception:

```
System.InvalidOperationException: Channel is configured with an HTTP transport doesn't support client-side load balancing or connectivity state tracking. The underlying HTTP transport must be a SocketsHttpHandler with no SocketsHttpHandler.ConnectCallback configured. The HTTP transport must be configured on the channel using GrpcChannelOptions.HttpHandler.
```

Processes running on the same machine can be designed to communicate with each other. Operating systems provide technologies for enabling fast and efficient [inter-process communication (IPC)](https://wikipedia.org/wiki/Inter-process_communication). Popular examples of IPC technologies are Unix domain sockets and Named pipes.

.NET provides support for inter-process communication using gRPC.

Note

Built-in support for Named pipes in ASP.NET Core requires .NET 8 or later.\
For more information, see [the .NET 8 or later version of this topic](interprocess?view=aspnetcore-8.0)

## Get started

gRPC calls are sent from a client to a server. To communicate between apps on a machine with gRPC, at least one app must host an ASP.NET Core gRPC server.

ASP.NET Core and gRPC can be hosted in any app using .NET Core 3.1 or later by adding the `Microsoft.AspNetCore.App` framework to the project.

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

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>
```

The preceding project file:

* Adds a framework reference to `Microsoft.AspNetCore.App`. The framework reference allows non-ASP.NET Core apps, such as Windows Services, WPF apps, or WinForms apps to use ASP.NET Core and host an ASP.NET Core server.
* Adds a NuGet package reference to [`Grpc.AspNetCore`](https://www.nuget.org/packages/Grpc.AspNetCore).
* Adds a `.proto` file.

## Configure Unix domain sockets

gRPC calls between a client and server on different machines are usually sent over TCP sockets. TCP was designed for communicating across a network. [Unix domain sockets (UDS)](https://wikipedia.org/wiki/Unix_domain_socket) are a widely supported IPC technology that's more efficient than TCP when the client and server are on the same machine. .NET provides built-in support for UDS in client and server apps.

Requirements:

* .NET 5 or later
* Linux, macOS, or [Windows 10/Windows Server 2019 or later](https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/)

### Server configuration

Unix domain sockets (UDS) are supported by [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0), which is configured in `Program.cs`:

```
public static readonly string SocketPath = Path.Combine(Path.GetTempPath(), "socket.tmp");

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(options =>
            {
                if (File.Exists(SocketPath))
                {
                    File.Delete(SocketPath);
                }
                options.ListenUnixSocket(SocketPath, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http2;
                });
            });
        });
```

The preceding example:

* Configures Kestrel's endpoints in `ConfigureKestrel`.
* Calls [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) to listen to a UDS with the specified path.
* Creates a UDS endpoint that isn't configured to use HTTPS. For information about enabling HTTPS, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsusehttps).

### Client configuration

`GrpcChannel` supports making gRPC calls over custom transports. When a channel is created, it can be configured with a `SocketsHttpHandler` that has a custom `ConnectCallback`. The callback allows the client to make connections over custom transports and then send HTTP requests over that transport.

Unix domain sockets connection factory example:

```
public class UnixDomainSocketConnectionFactory
{
    private readonly EndPoint _endPoint;

    public UnixDomainSocketConnectionFactory(EndPoint endPoint)
    {
        _endPoint = endPoint;
    }

    public async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext _,
        CancellationToken cancellationToken = default)
    {
        var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);

        try
        {
            await socket.ConnectAsync(_endPoint, cancellationToken).ConfigureAwait(false);
            return new NetworkStream(socket, true);
        }
        catch
        {
            socket.Dispose();
            throw;
        }
    }
}
```

Using the custom connection factory to create a channel:

```
public static readonly string SocketPath = Path.Combine(Path.GetTempPath(), "socket.tmp");

public static GrpcChannel CreateChannel()
{
    var udsEndPoint = new UnixDomainSocketEndPoint(SocketPath);
    var connectionFactory = new UnixDomainSocketConnectionFactory(udsEndPoint);
    var socketsHttpHandler = new SocketsHttpHandler
    {
        ConnectCallback = connectionFactory.ConnectAsync
    };

    return GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
    {
        HttpHandler = socketsHttpHandler
    });
}
```

Channels created using the preceding code send gRPC calls over Unix domain sockets. Support for other IPC technologies can be implemented using the extensibility in Kestrel and `SocketsHttpHandler`.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/java-client
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/java-client.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core SignalR Java client

By [Mikael Mengistu](https://twitter.com/MikaelM_12)

The Java client enables connecting to an ASP.NET Core SignalR server from Java code, including Android apps. Like the [JavaScript client](javascript-client?view=aspnetcore-10.0) and the [.NET client](dotnet-client?view=aspnetcore-10.0), the Java client enables you to receive and send messages to a hub in real time. The Java client is available in ASP.NET Core 2.2 or later.

The sample Java console app referenced in this article uses the SignalR Java client.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/java-client/sample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Install the SignalR Java client package

The *signalr-7.0.0* JAR file allows clients to connect to SignalR hubs. To find the latest JAR file version number, see the [Maven search results](https://central.sonatype.com/search?q=g:com.microsoft.signalr%20%20a:signalr\&smo=true).

If using Gradle, add the following line to the `dependencies` section of your *build.gradle* file:

```
implementation 'com.microsoft.signalr:signalr:7.0.0'
```

If using Maven, add the following lines inside the `<dependencies>` element of your `pom.xml` file:

```
<dependency>
    <groupId>com.microsoft.signalr</groupId>
    <artifactId>signalr</artifactId>
    <version>1.0.0</version>
</dependency>
```

## Connect to a hub

To establish a `HubConnection`, the `HubConnectionBuilder` should be used. The hub URL and log level can be configured while building a connection. Configure any required options by calling any of the `HubConnectionBuilder` methods before `build`. Start the connection with `start`.

```
HubConnection hubConnection = HubConnectionBuilder.create(input)
        .build();
```

## Call hub methods from client

A call to `send` invokes a hub method. Pass the hub method name and any arguments defined in the hub method to `send`.

```
hubConnection.send("Send", input);
```

Note

Calling hub methods from a client is only supported when using the Azure SignalR Service in [*Default* mode](/en-us/azure/azure-signalr/concept-service-mode#default-mode).

## Call client methods from hub

Use `hubConnection.on` to define methods on the client that the hub can call. Define the methods after building but before starting the connection.

```
hubConnection.on("Send", (message) -> {
    System.out.println("New Message: " + message);
}, String.class);
```

## Add logging

The SignalR Java client uses the [SLF4J](https://www.slf4j.org/) library for logging. It's a high-level logging API that allows users of the library to choose their own specific logging implementation by bringing in a specific logging dependency. The following code snippet shows how to use `java.util.logging` with the SignalR Java client.

```
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
```

If you don't configure logging in your dependencies, SLF4J loads a default no-operation logger with the following warning message:

```
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

This can safely be ignored.

## Android development notes

With regard to Android SDK compatibility for the SignalR client features, consider the following items when specifying your target Android SDK version:

* The SignalR Java client runs on Android API Level 16 or later.
* Connecting through the Azure SignalR Service requires Android API Level 20 or later because the [Azure SignalR Service](/en-us/azure/azure-signalr/signalr-overview) requires TLS 1.2 and doesn't support SHA-1-based cipher suites. Android [added support for SHA-256 (and above) cipher suites](https://developer.android.com/reference/javax/net/ssl/SSLSocket) in API Level 20.
* When using [MessagePack](messagepackhubprotocol?view=aspnetcore-10.0) for the Java client, see [MessagePack considerations: Android device support](messagepackhubprotocol?view=aspnetcore-10.0#android-device-support).

## Configure bearer token authentication

In the SignalR Java client, you can configure a bearer token to use for authentication by providing an "access token factory" to the [HttpHubConnectionBuilder](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder?view=aspnet-signalr-java\&preserve-view=true). Use [withAccessTokenFactory](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder.withaccesstokenprovider?view=aspnet-signalr-java\&preserve-view=true) to provide an [RxJava](https://github.com/ReactiveX/RxJava) [Single\<String>](https://reactivex.io/documentation/single.html). With a call to [Single.defer](https://reactivex.io/RxJava/javadoc/io/reactivex/Single.html#defer-java.util.concurrent.Callable-), you can write logic to produce access tokens for your client.

```
HubConnection hubConnection = HubConnectionBuilder.create("YOUR HUB URL HERE")
    .withAccessTokenProvider(Single.defer(() -> {
        // Your logic here.
        return Single.just("An Access Token");
    })).build();
```

### Passing Class information in Java

When calling the `on`, `invoke`, or `stream` methods of `HubConnection` in the Java client, users should pass a `Type` object rather than a `Class<?>` object to describe any generic `Object` passed to the method. A `Type` can be acquired using the provided `TypeReference` class. For example, using a custom generic class named `Foo<T>`, the following code gets the `Type`:

```
Type fooType = new TypeReference<Foo<String>>() { }).getType();
```

For non-generics, such as primitives or other non-parameterized types like `String`, you can simply use the built-in `.class`.

When calling one of these methods with one or more object types, use the generics syntax when invoking the method. For example, when registering an `on` handler for a method named `func`, which takes as arguments a String and a `Foo<String>` object, use the following code to set an action to print the arguments:

```
hubConnection.<String, Foo<String>>on("func", (param1, param2) ->{
    System.out.println(param1);
    System.out.println(param2);
}, String.class, fooType);
```

This convention is necessary because we can not retrieve complete information about complex types with the `Object.getClass` method due to type erasure in Java. For example, calling `getClass` on an `ArrayList<String>` would not return `Class<ArrayList<String>>`, but rather `Class<ArrayList>`, which does not give the deserializer enough information to correctly deserialize an incoming message. The same is true for custom objects.

## Known limitations

* Transport fallback and the Server Sent Events transport aren't supported.

- Transport fallback and the Server Sent Events transport aren't supported.
- Only the JSON protocol is supported.

* Only the JSON protocol is supported.
* Only the WebSockets transport is supported.
* Streaming isn't supported yet.

## Related content

* [Java API reference](/en-us/java/api/com.microsoft.signalr?view=aspnet-signalr-java\&preserve-view=true)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [Publish an ASP.NET Core SignalR app to Azure App Service](publish-to-azure-web-app?view=aspnetcore-10.0)
* [Azure SignalR Service serverless documentation](/en-us/azure/azure-signalr/signalr-concept-serverless-development-config)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0024
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0024.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0024: Route handler has multiple parameters with the `[FromBody]` attribute

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0024      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A route handler has multiple parameters with the [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) attribute or a parameter with an [`[AsParameters]`](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute) attribute where the parameter type contains multiple members with `[FromBody]` attributes.

## Rule description

Route handler has multiple parameters with the `[FromBody]` attribute.

## How to fix violations

To fix a violation of this rule, remove `[FromBody]` attributes from all but one parameter.

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/responses
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/responses.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# How to create responses in Minimal API apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to create responses for Minimal API endpoints in ASP.NET Core. Minimal APIs provide several ways to return data and HTTP status codes.

Minimal endpoints support the following types of return values:

1. `string` - This includes `Task<string>` and `ValueTask<string>`.
2. `T` (Any other type) - This includes `Task<T>` and `ValueTask<T>`.
3. `IResult` based - This includes `Task<IResult>` and `ValueTask<IResult>`.

Important

Starting with ASP.NET Core 10, known API endpoints no longer redirect to login pages when using cookie authentication. Instead, they return 401/403 status codes. For details, see [API endpoint authentication behavior in ASP.NET Core](../../security/authentication/api-endpoint-auth?view=aspnetcore-10.0).

## `string` return values

| Behavior                                                  | Content-Type |
| --------------------------------------------------------- | ------------ |
| The framework writes the string directly to the response. | `text/plain` |

Consider the following route handler, which returns a `Hello world` text.

```
app.MapGet("/hello", () => "Hello World");
```

The `200` status code is returned with `text/plain` Content-Type header and the following content.

```
Hello World
```

## `T` (Any other type) return values

| Behavior                                    | Content-Type       |
| ------------------------------------------- | ------------------ |
| The framework JSON-serializes the response. | `application/json` |

Consider the following route handler, which returns an anonymous type containing a `Message` string property.

```
app.MapGet("/hello", () => new { Message = "Hello World" });
```

The `200` status code is returned with `application/json` Content-Type header and the following content.

```
{"message":"Hello World"}
```

## `IResult` return values

| Behavior                                                                                                      | Content-Type                             |
| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| The framework calls [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync). | Decided by the `IResult` implementation. |

The `IResult` interface defines a contract that represents the result of an HTTP endpoint. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class and the static [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) are used to create various `IResult` objects that represent different types of responses.

### `TypedResults` versus `Results`

The [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes provide similar sets of results helpers. The `TypedResults` class is the *typed* equivalent of the `Results` class. However, the `Results` helpers' return type is [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult), while each `TypedResults` helper's return type is one of the `IResult` implementation types. The difference means that for `Results` helpers a conversion is needed when the concrete type is needed, for example, for unit testing. The implementation types are defined in the [Microsoft.AspNetCore.Http.HttpResults](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults) namespace.

Returning `TypedResults` rather than `Results` has the following advantages:

* `TypedResults` helpers return strongly typed objects, which can improve code readability, unit testing, and reduce the chance of runtime errors.
* The implementation type [automatically provides the response type metadata for OpenAPI](/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi#describe-response-types) to describe the endpoint.

Consider the following endpoint, for which a `200 OK` status code with the expected JSON response is produced.

```
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();
```

In order to document this endpoint correctly the extensions method `Produces` is called. However, it's not necessary to call `Produces` if `TypedResults` is used instead of `Results`, as shown in the following code. `TypedResults` automatically provides the metadata for the endpoint.

```
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
```

For more information about describing a response type, see [OpenAPI support in Minimal APIs](/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi#describe-response-types-1).

For examples on testing result types, see the [Test documentation](/en-us/aspnet/core/fundamentals/minimal-apis/test-min-api#unit-test-iresult-implementation-types).

Because all methods on `Results` return `IResult` in their signature, the compiler automatically infers that as the request delegate return type when returning different results from a single endpoint. `TypedResults` requires the use of `Results<T1, TN>` from such delegates.

The following method compiles because both [`Results.Ok`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.ok) and [`Results.NotFound`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.notfound) are declared as returning `IResult`, even though the actual concrete types of the objects returned are different:

```
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());
```

The following method does not compile, because `TypedResults.Ok` and `TypedResults.NotFound` are declared as returning different types and the compiler won't attempt to infer the best matching type:

```
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());
```

To use `TypedResults`, the return type must be fully declared; when the method is asynchronous, the declaration requires wrapping the return type in a `Task<>`. Using `TypedResults` is more verbose, but that's the trade-off for having the type information be statically available and thus capable of self-describing to OpenAPI:

```
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
   await db.Todos.FindAsync(id)
    is Todo todo
       ? TypedResults.Ok(todo)
       : TypedResults.NotFound());
```

### Results\<TResult1, TResultN>

Use [`Results<TResult1, TResultN>`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.results-2) as the endpoint handler return type instead of `IResult` when:

* Multiple `IResult` implementation types are returned from the endpoint handler.
* The static `TypedResult` class is used to create the `IResult` objects.

This alternative is better than returning `IResult` because the generic union types automatically retain the endpoint metadata. And since the `Results<TResult1, TResultN>` union types implement implicit cast operators, the compiler can automatically convert the types specified in the generic arguments to an instance of the union type.

This has the added benefit of providing compile-time checking that a route handler actually only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to `Results<>` results in a compilation error.

Consider the following endpoint, for which a `400 BadRequest` status code is returned when the `orderId` is greater than `999`. Otherwise, it produces a `200 OK` with the expected content.

```
app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();
```

In order to document this endpoint correctly the extension method `Produces` is called. However, since the `TypedResults` helper automatically includes the metadata for the endpoint, you can return the `Results<T1, Tn>` union type instead, as shown in the following code.

```
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
```

[]()

### Built-in results

Common result helpers exist in the [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes. Returning `TypedResults` is preferred to returning `Results`. For more information, see [`TypedResults` versus `Results`](responses?view=aspnetcore-10.0#typedresults-versus-results).

The following sections demonstrate the usage of the common result helpers.

#### JSON

```
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
```

[WriteAsJsonAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponsejsonextensions.writeasjsonasync) is an alternative way to return JSON:

```
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));
```

#### Custom Status Code

```
app.MapGet("/405", () => Results.StatusCode(405));
```

#### Internal Server Error

```
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
```

The preceding example returns a 500 status code.

#### Problem and ValidationProblem

```
app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});
```

#### Customize validation error responses using IProblemDetailsService

Customize error responses from Minimal API validation logic with an [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) implementation. Register this service in your application's service collection to enable more consistent and user-specific error responses. Support for Minimal API validation was introduced in ASP.NET Core in .NET 10.

To implement custom validation error responses:

* Implement [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) or use the default implementation
* Register the service in the DI container
* The validation system automatically uses the registered service to format validation error responses

The following example shows how to register and configure the [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) to customize validation error responses:

```
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        if (context.ProblemDetails.Status == 400)
        {
            context.ProblemDetails.Title = "Validation error occurred";
            context.ProblemDetails.Extensions["support"] = "Contact support@example.com";
            context.ProblemDetails.Extensions["traceId"] = Guid.NewGuid().ToString();
        }
    };
});
```

When a validation error occurs, the [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) will be used to generate the error response, including any customizations added in the `CustomizeProblemDetails` callback.

For a complete app example, see the [Minimal API sample app](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/samples/10.0/MinApiIproblemDetailsService/Program.cs) demonstrating how to customize validation error responses using the [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) in ASP.NET Core Minimal APIs.

#### Text

```
app.MapGet("/text", () => Results.Text("This is some text"));
```

[]()

#### Stream

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
```

[`Results.Stream`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.stream?view=aspnetcore-7.0\&preserve-view=true) overloads allow access to the underlying HTTP response stream without buffering. The following example uses [ImageSharp](https://sixlabors.com/products/imagesharp) to return a reduced size of the specified image:

```
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
```

The following example streams an image from [Azure Blob storage](/en-us/azure/storage/blobs/storage-blobs-introduction):

```
app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
```

The following example streams a video from an Azure Blob:

```
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});
```

#### Server-Sent Events (SSE)

The [TypedResults.ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,051e6796e1492f84) API supports returning a [ServerSentEvents](/en-us/dotnet/api/system.net.serversentevents) result.

[Server-Sent Events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) is a server push technology that allows a server to send a stream of event messages to a client over a single HTTP connection. In .NET, the event messages are represented as [`SseItem<T>`](/en-us/dotnet/api/system.net.serversentevents.sseitem-1) objects, which may contain an event type, an ID, and a data payload of type `T`.

The [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) class has a static method called [ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,ceb980606eb9e295) that can be used to return a `ServerSentEvents` result. The first parameter to this method is an `IAsyncEnumerable<SseItem<T>>` that represents the stream of event messages to be sent to the client.

The following example illustrates how to use the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as JSON objects to the client:

```
app.MapGet("sse-item", (CancellationToken cancellationToken) =>
{
    async IAsyncEnumerable<SseItem<int>> GetHeartRate(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var heartRate = Random.Shared.Next(60, 100);
            yield return new SseItem<int>(heartRate, eventType: "heartRate")
            {
                ReconnectionInterval = TimeSpan.FromMinutes(1)
            };
            await Task.Delay(2000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken));
});
```

For more information, see the [Minimal API sample app](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/samples/10.0/MinimalServerSentEvents/Program.cs) using the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as string, `ServerSentEvents`, and JSON objects to the client.

#### Redirect

```
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
```

#### File

```
app.MapGet("/download", () => Results.File("myfile.text"));
```

[]()

### HttpResult interfaces

Here's an example of a filter that uses one of these interfaces:

```
app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});
```

For more information, see [Filters in Minimal API apps](min-api-filters?view=aspnetcore-10.0) and [IResult implementation types](test-min-api?view=aspnetcore-10.0#iresult-implementation-types).

## File result return values

When an API endpoint returns content other than JSON, or supports HTTP protocol features like conditional or range requests, ASP.NET Core provides result types that handle the necessary HTTP protocol details for you. These result types are referred to as "file results", but the functionality is useful for scenarios beyond serving files on disk. There are file result types for both Minimal APIs and controller-based APIs, and they share a common underlying implementation and behavior.

To access this functionality, the API endpoint creates and returns a file result object - an instance of one of these file result types. The file result object encapsulates the content to be sent, the content type, and any additional parameters like a download file name. The result object implements a method -- `ExecuteAsync(HttpContext)` for Minimal APIs or `ExecuteResultAsync(ActionContext)` for controllers -- that the framework calls to write the response. This method sets the `Content-Type` header and, when a file name is provided, the `Content-Disposition` header. It also handles conditional request headers and range request headers when the appropriate parameters are set on the result object.

### File result types

In Minimal APIs, the most common and recommended way to create a file result is [`TypedResults.File`](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.file), which accepts a `byte[]` or `Stream` and returns a [`FileContentHttpResult`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.filecontenthttpresult) or [`FileStreamHttpResult`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.filestreamhttpresult).

Alternatives include [`TypedResults.Bytes`](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.bytes) for an explicit byte-array helper, [`TypedResults.PhysicalFile`](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.physicalfile) for serving files by absolute path, or [`Results.File`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.file) / [`Results.Bytes`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.bytes) when you only need the `IResult` interface.

ASP.NET Core also provides [static files middleware](../static-files?view=aspnetcore-10.0) that serves files relative to the web root (`wwwroot`) without requiring an explicit endpoint.

```
app.MapGet("/report", () =>
{
    // TypedResults.File with a byte[] returns a FileContentHttpResult
    byte[] pdf = GenerateReport();
    return TypedResults.File(pdf, "application/pdf", "report.pdf");
});

app.MapGet("/download", () =>
{
    // TypedResults.File with a Stream returns a FileStreamHttpResult
    Stream stream = new MemoryStream("Hello, World!"u8.ToArray());
    return TypedResults.File(stream, "application/octet-stream");
});
```

In controller-based APIs, the [`ControllerBase.File()`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.file) helper method accepts either a `byte[]` or `Stream` and returns the appropriate concrete result type ([`FileContentResult`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filecontentresult), [`FileStreamResult`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filestreamresult)).

Alternatives include returning a [`VirtualFileResult`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.virtualfileresult) for files relative to the web root, or a [`PhysicalFileResult`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.physicalfileresult) for files by absolute path, but these are less common as the static files middleware usually handles those scenarios.

```
[HttpGet("report")]
public FileContentResult Report()
{
    // File() with a byte[] returns a FileContentResult
    byte[] pdf = GenerateReport();
    return File(pdf, "application/pdf", "report.pdf");
}

[HttpGet("download")]
public FileStreamResult Download()
{
    // File() with a Stream returns a FileStreamResult
    Stream stream = new MemoryStream("Hello, World!"u8.ToArray());
    return File(stream, "application/octet-stream");
}
```

### OpenAPI support for file results

File result types don't automatically contribute response metadata to the generated OpenAPI document. To get a proper response description in the OpenAPI document, you must specify this metadata explicitly.

In Minimal APIs, use the [`Produces<TResponse>()`](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.produces) extension method to provide the OpenAPI metadata for the response. This metadata determines the status code, content type, and schema for the response in the OpenAPI document. For a file result, it's important to specify the content type, such as `application/pdf`, and to use an appropriate `TResponse` to get the desired schema. You can also specify the status code in the `Produces` extension method if it differs from the default of `200 OK`. Common status codes are defined in the [`StatusCodes`](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) class, and common content types are defined in the [`MediaTypeNames`](/en-us/dotnet/api/system.net.mime.mediatypenames) class. For example:

```
app.MapGet("/image", () =>
{
    // A 1x1 red pixel BMP (bitmap header + single pixel)
    byte[] data = [0x42, 0x4D, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                   0x1A, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00,
                   0x01, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x00];
    return TypedResults.File(data, MediaTypeNames.Image.Bmp, "pixel.bmp");
})
// Use Stream to produce the correct schema in OpenAPI (format: binary)
.Produces<Stream>(contentType: MediaTypeNames.Image.Bmp);
```

Conceptually, `TResponse` represents the type of the response body, but the appropriate schema for a file result depends more on the body content than the CLR type. Therefore, it may be necessary to specify a `TResponse` that doesn't match the CLR type of the content in the file result in order to get the desired OpenAPI schema.

In particular, there are three common schemas for file results:

* **binary content**, such as PDFs, images, or videos, where the schema should be `type: string, format: binary`. The recommended `TResponse` for this case is `Stream`. The framework has special logic to map this type to the `binary` format in the OpenAPI schema.
* **text content**, such as CSV or plain text. Here the schema should be simply `type: string` with no `format`. Use `string` as the `TResponse` for this case.
* **base64-encoded content**, where the schema should be `type: string, format: byte`. It's uncommon to base64-encode file content in an API response, but for legacy reasons this is the schema produced when the `TResponse` is `byte[]`.

In a controller-based app, use the [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) or the [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attribute:

```
[HttpGet("image")]
// Use Stream to produce the correct schema in OpenAPI (format: binary)
[ProducesResponseType<Stream>(StatusCodes.Status200OK, MediaTypeNames.Image.Bmp)]
public FileContentResult Image()
{
    // A 1x1 red pixel BMP (bitmap header + single pixel)
    byte[] data = [0x42, 0x4D, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                   0x1A, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00,
                   0x01, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0xFF, 0x00];
    return File(data, "image/bmp", "pixel.bmp");
}
```

As with Minimal APIs, you should choose the `Type` parameter of the `[Produces]` and `[ProducesResponseType]` attributes that corresponds to the desired OpenAPI schema for your file result responses:

* **binary content**: Use `FileContentResult` or `FileStreamResult` to get the `binary` format in the OpenAPI schema.
* **text content**: Use `string` to get a simple `type: string` schema.
* **base64-encoded content**: Use `byte[]` to get the `byte` format in the OpenAPI schema, though this is uncommon for file results.

### File result support for conditional requests

File results support [conditional requests](https://www.rfc-editor.org/rfc/rfc9110#section-13) for cache validation. Set the `lastModified` and/or `entityTag` parameters when creating the result object, and the framework automatically inspects incoming `If-None-Match` and `If-Modified-Since` headers. If the resource hasn't changed, the framework returns `304 Not Modified` with no body — no additional code is needed.

| Parameter      | Purpose                                                                                                                                                               |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lastModified` | Sets the `Last-Modified` response header. If the client sends `If-Modified-Since` and the file hasn't changed, the framework returns `304 Not Modified` with no body. |
| `entityTag`    | Sets the `ETag` response header. If the client sends `If-None-Match` with a matching ETag, the framework returns `304 Not Modified` with no body.                     |

Note

Precondition checks (`If-Match`, `If-Unmodified-Since`) typically require custom logic in the endpoint to verify preconditions *before* performing their function.

The following example demonstrates how to use file results to enable cache validation for a configuration endpoint.

```
app.MapGet("/config", (
    [FromHeader(Name = "If-None-Match")] string? ifNoneMatch,
    [FromHeader(Name = "If-Modified-Since")] string? ifModifiedSince) =>
{
    byte[] data = File.ReadAllBytes("Data/config.json");
    var lastModified = File.GetLastWriteTimeUtc("Data/config.json");
    var etag = new EntityTagHeaderValue($"\"{Convert.ToHexString(SHA256.HashData(data))}\"");

    return TypedResults.File(
        data,
        contentType: MediaTypeNames.Application.Json,
        lastModified: lastModified,
        entityTag: etag);
})
.Produces<object>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status304NotModified);
```

This example also illustrates how to document the `304 Not Modified` response in the OpenAPI document using the `Produces` extension method, and including the `if-none-match` and `if-modified-since` headers as parameters so they're included in the OpenAPI schema for the endpoint.

A controller-based API can achieve the same behavior using the `File` helper method and the `[ProducesResponseType]` attribute:

```
[HttpGet("config")]
[ProducesResponseType<object>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status304NotModified)]
public FileContentResult Config(
    [FromHeader(Name = "If-None-Match")] string? ifNoneMatch,
    [FromHeader(Name = "If-Modified-Since")] string? ifModifiedSince)
{
    byte[] data = System.IO.File.ReadAllBytes("Data/config.json");
    var lastModified = System.IO.File.GetLastWriteTimeUtc("Data/config.json");
    var etag = new EntityTagHeaderValue($"\"{Convert.ToHexString(SHA256.HashData(data))}\"");

    return File(
        data,
        MediaTypeNames.Application.Json,
        lastModified: lastModified,
        entityTag: etag);
}
```

### File result support for range requests

**Range requests** allow clients to request only a portion of a file rather than the entire content. A client sends a `Range` header specifying byte offsets, and the server responds with `206 Partial Content` containing just those bytes. This enables resumable downloads, parallel chunked downloads, and efficient seeking in media players.

Range requests can also be conditional: the client sends an `If-Range` header containing an ETag or date alongside the `Range` header, and the server returns the partial content only if the resource hasn't changed. If it has changed, the server ignores the range and returns the full resource instead. `If-Range` is only evaluated when `entityTag` or `lastModified` is also set on the file result.

Set `enableRangeProcessing` to `true` to enable range processing. The following examples enable range processing for a video streaming endpoint, and document the additional response types in the OpenAPI metadata.

**Minimal API:**

```
app.MapGet("/video/{id}", (string id,
    [FromHeader(Name = "Range")] string? range) =>
{
    var bytes = GetVideo(id);

    return TypedResults.File(
        bytes,
        contentType: "video/mp4",
        fileDownloadName: "video.mp4",
        enableRangeProcessing: true);
})
.Produces<Stream>(StatusCodes.Status200OK, "video/mp4")
.Produces<Stream>(StatusCodes.Status206PartialContent, "video/mp4")
.Produces(StatusCodes.Status416RangeNotSatisfiable);
```

**Controller:**

```
[HttpGet("video/{id}")]
[ProducesResponseType<Stream>(StatusCodes.Status200OK, "video/mp4")]
[ProducesResponseType<Stream>(StatusCodes.Status206PartialContent, "video/mp4")]
[ProducesResponseType(StatusCodes.Status416RangeNotSatisfiable)]
public FileContentResult Video(string id,
    [FromHeader(Name = "Range")] string? range)
{
    var bytes = GetVideo(id);

    return File(
        bytes,
        contentType: "video/mp4",
        fileDownloadName: "video.mp4",
        enableRangeProcessing: true);
}
```

With this configuration, the framework automatically handles the following HTTP interactions:

* **Full request**: Returns `200 OK` with the complete file.
* **Range request** (`Range: bytes=0-1023`): Returns `206 Partial Content` with the `Content-Range` header and only the requested bytes.
* **Invalid range**: Returns `416 Range Not Satisfiable`.

## Modifying Headers

Use the `HttpResponse` object to modify response headers:

```
app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});
```

## Customizing responses

Applications can control responses by implementing a custom [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) type. The following code is an example of an HTML result type:

```
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}
```

We recommend adding an extension method to [Microsoft.AspNetCore.Http.IResultExtensions](/en-us/dotnet/api/microsoft.aspnetcore.http.iresultextensions) to make these custom results more discoverable.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();
```

Also, a custom `IResult` type can provide its own annotation by implementing the [IEndpointMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iendpointmetadataprovider) interface. For example, the following code adds an annotation to the preceding `HtmlResult` type that describes the response produced by the endpoint.

```
class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}
```

The `ProducesHtmlMetadata` is an implementation of [IProducesResponseTypeMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iproducesresponsetypemetadata) that defines the produced response content type `text/html` and the status code `200 OK`.

```
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
```

An alternative approach is using the [Microsoft.AspNetCore.Mvc.ProducesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) to describe the produced response. The following code changes the `PopulateMetadata` method to use `ProducesAttribute`.

```
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
```

## Configure JSON serialization options

By default, Minimal API apps use [`Web defaults`](/en-us/dotnet/standard/serialization/system-text-json-configure-options#web-defaults-for-jsonserializeroptions) options during JSON serialization and deserialization.

### Configure JSON serialization options globally

Options can be configured globally for an app by invoking [ConfigureHttpJsonOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpjsonserviceextensions.configurehttpjsonoptions). The following example includes public fields and formats JSON output.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }
```

Since fields are included, the preceding code reads `NameField` and includes it in the output JSON.

### Configure JSON serialization options for an endpoint

To configure serialization options for an endpoint, invoke [Results.Json](/en-us/dotnet/api/microsoft.aspnetcore.http.results.json) and pass to it a [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object, as shown in the following example:

```
using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }
```

As an alternative, use an overload of [WriteAsJsonAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponsejsonextensions.writeasjsonasync) that accepts a [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object. The following example uses this overload to format the output JSON:

```
using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }
```

## Related content

* [Authentication and authorization in Minimal APIs](security?view=aspnetcore-10.0)

Minimal endpoints support the following types of return values:

1. `string` - This includes `Task<string>` and `ValueTask<string>`.
2. `T` (Any other type) - This includes `Task<T>` and `ValueTask<T>`.
3. `IResult` based - This includes `Task<IResult>` and `ValueTask<IResult>`.

## `string` return values

| Behavior                                                  | Content-Type |
| --------------------------------------------------------- | ------------ |
| The framework writes the string directly to the response. | `text/plain` |

Consider the following route handler, which returns a `Hello world` text.

```
app.MapGet("/hello", () => "Hello World");
```

The `200` status code is returned with `text/plain` Content-Type header and the following content.

```
Hello World
```

## `T` (Any other type) return values

| Behavior                                    | Content-Type       |
| ------------------------------------------- | ------------------ |
| The framework JSON-serializes the response. | `application/json` |

Consider the following route handler, which returns an anonymous type containing a `Message` string property.

```
app.MapGet("/hello", () => new { Message = "Hello World" });
```

The `200` status code is returned with `application/json` Content-Type header and the following content.

```
{"message":"Hello World"}
```

## `IResult` return values

| Behavior                                                                                                      | Content-Type                             |
| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| The framework calls [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync). | Decided by the `IResult` implementation. |

The `IResult` interface defines a contract that represents the result of an HTTP endpoint. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class and the static [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) are used to create various `IResult` objects that represent different types of responses.

### `TypedResults` versus `Results`

The [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes provide similar sets of results helpers. The `TypedResults` class is the *typed* equivalent of the `Results` class. However, the `Results` helpers' return type is [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult), while each `TypedResults` helper's return type is one of the `IResult` implementation types. The difference means that for `Results` helpers a conversion is needed when the concrete type is needed, for example, for unit testing. The implementation types are defined in the [Microsoft.AspNetCore.Http.HttpResults](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults) namespace.

Returning `TypedResults` rather than `Results` has the following advantages:

* `TypedResults` helpers return strongly typed objects, which can improve code readability, unit testing, and reduce the chance of runtime errors.
* The implementation type [automatically provides the response type metadata for OpenAPI](/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi#describe-response-types) to describe the endpoint.

Consider the following endpoint, for which a `200 OK` status code with the expected JSON response is produced.

```
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();
```

In order to document this endpoint correctly the extensions method `Produces` is called. However, it's not necessary to call `Produces` if `TypedResults` is used instead of `Results`, as shown in the following code. `TypedResults` automatically provides the metadata for the endpoint.

```
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
```

For more information about describing a response type, see [OpenAPI support in Minimal APIs](/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi#describe-response-types-1).

As mentioned previously, when using `TypedResults`, a conversion is not needed. Consider the following Minimal API which returns a `TypedResults` class

```
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}
```

The following test checks for the full concrete type:

```
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}
```

Because all methods on `Results` return `IResult` in their signature, the compiler automatically infers that as the request delegate return type when returning different results from a single endpoint. `TypedResults` requires the use of `Results<T1, TN>` from such delegates.

The following method compiles because both [`Results.Ok`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.ok) and [`Results.NotFound`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.notfound) are declared as returning `IResult`, even though the actual concrete types of the objects returned are different:

```
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());
```

The following method does not compile, because `TypedResults.Ok` and `TypedResults.NotFound` are declared as returning different types and the compiler won't attempt to infer the best matching type:

```
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());
```

To use `TypedResults`, the return type must be fully declared, which when asynchronous requires the `Task<>` wrapper. Using `TypedResults` is more verbose, but that's the trade-off for having the type information be statically available and thus capable of self-describing to OpenAPI:

```
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
   await db.Todos.FindAsync(id)
    is Todo todo
       ? TypedResults.Ok(todo)
       : TypedResults.NotFound());
```

### Results\<TResult1, TResultN>

Use [`Results<TResult1, TResultN>`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.results-2) as the endpoint handler return type instead of `IResult` when:

* Multiple `IResult` implementation types are returned from the endpoint handler.
* The static `TypedResult` class is used to create the `IResult` objects.

This alternative is better than returning `IResult` because the generic union types automatically retain the endpoint metadata. And since the `Results<TResult1, TResultN>` union types implement implicit cast operators, the compiler can automatically convert the types specified in the generic arguments to an instance of the union type.

This has the added benefit of providing compile-time checking that a route handler actually only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to `Results<>` results in a compilation error.

Consider the following endpoint, for which a `400 BadRequest` status code is returned when the `orderId` is greater than `999`. Otherwise, it produces a `200 OK` with the expected content.

```
app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();
```

In order to document this endpoint correctly the extension method `Produces` is called. However, since the `TypedResults` helper automatically includes the metadata for the endpoint, you can return the `Results<T1, Tn>` union type instead, as shown in the following code.

```
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
```

[]()

### Built-in results

Common result helpers exist in the [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes. Returning `TypedResults` is preferred to returning `Results`. For more information, see [`TypedResults` versus `Results`](responses?view=aspnetcore-10.0#typedresults-versus-results).

The following sections demonstrate the usage of the common result helpers.

#### JSON

```
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
```

[WriteAsJsonAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponsejsonextensions.writeasjsonasync) is an alternative way to return JSON:

```
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));
```

#### Custom Status Code

```
app.MapGet("/405", () => Results.StatusCode(405));
```

#### Internal Server Error

```
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
```

The preceding example returns a 500 status code.

#### Problem and ValidationProblem

```
app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});
```

#### Text

```
app.MapGet("/text", () => Results.Text("This is some text"));
```

[]()

#### Stream

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
```

[`Results.Stream`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.stream?view=aspnetcore-7.0\&preserve-view=true) overloads allow access to the underlying HTTP response stream without buffering. The following example uses [ImageSharp](https://sixlabors.com/products/imagesharp) to return a reduced size of the specified image:

```
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
```

The following example streams an image from [Azure Blob storage](/en-us/azure/storage/blobs/storage-blobs-introduction):

```
app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
```

The following example streams a video from an Azure Blob:

```
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});
```

#### Redirect

```
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
```

#### File

```
app.MapGet("/download", () => Results.File("myfile.text"));
```

[]()

### HttpResult interfaces

Here's an example of a filter that uses one of these interfaces:

```
app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});
```

For more information, see [Filters in Minimal API apps](min-api-filters?view=aspnetcore-10.0) and [IResult implementation types](test-min-api?view=aspnetcore-10.0#iresult-implementation-types).

## Modifying Headers

Use the `HttpResponse` object to modify response headers:

```
app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});
```

## Customizing responses

Applications can control responses by implementing a custom [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) type. The following code is an example of an HTML result type:

```
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}
```

We recommend adding an extension method to [Microsoft.AspNetCore.Http.IResultExtensions](/en-us/dotnet/api/microsoft.aspnetcore.http.iresultextensions) to make these custom results more discoverable.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();
```

Also, a custom `IResult` type can provide its own annotation by implementing the [IEndpointMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iendpointmetadataprovider) interface. For example, the following code adds an annotation to the preceding `HtmlResult` type that describes the response produced by the endpoint.

```
class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}
```

The `ProducesHtmlMetadata` is an implementation of [IProducesResponseTypeMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iproducesresponsetypemetadata) that defines the produced response content type `text/html` and the status code `200 OK`.

```
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
```

An alternative approach is using the [Microsoft.AspNetCore.Mvc.ProducesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) to describe the produced response. The following code changes the `PopulateMetadata` method to use `ProducesAttribute`.

```
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
```

## Configure JSON serialization options

By default, Minimal API apps use [`Web defaults`](/en-us/dotnet/standard/serialization/system-text-json-configure-options#web-defaults-for-jsonserializeroptions) options during JSON serialization and deserialization.

### Configure JSON serialization options globally

Options can be configured globally for an app by invoking [ConfigureHttpJsonOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpjsonserviceextensions.configurehttpjsonoptions). The following example includes public fields and formats JSON output.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }
```

Since fields are included, the preceding code reads `NameField` and includes it in the output JSON.

### Configure JSON serialization options for an endpoint

To configure serialization options for an endpoint, invoke [Results.Json](/en-us/dotnet/api/microsoft.aspnetcore.http.results.json) and pass to it a [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object, as shown in the following example:

```
using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }
```

As an alternative, use an overload of [WriteAsJsonAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponsejsonextensions.writeasjsonasync) that accepts a [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object. The following example uses this overload to format the output JSON:

```
using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }
```

## Related content

* [Authentication and authorization in Minimal APIs](security?view=aspnetcore-10.0)

Minimal endpoints support the following types of return values:

1. `string` - This includes `Task<string>` and `ValueTask<string>`.
2. `T` (Any other type) - This includes `Task<T>` and `ValueTask<T>`.
3. `IResult` based - This includes `Task<IResult>` and `ValueTask<IResult>`.

## `string` return values

| Behavior                                                  | Content-Type |
| --------------------------------------------------------- | ------------ |
| The framework writes the string directly to the response. | `text/plain` |

Consider the following route handler, which returns a `Hello world` text.

```
app.MapGet("/hello", () => "Hello World");
```

The `200` status code is returned with `text/plain` Content-Type header and the following content.

```
Hello World
```

## `T` (Any other type) return values

| Behavior                                    | Content-Type       |
| ------------------------------------------- | ------------------ |
| The framework JSON-serializes the response. | `application/json` |

Consider the following route handler, which returns an anonymous type containing a `Message` string property.

```
app.MapGet("/hello", () => new { Message = "Hello World" });
```

The `200` status code is returned with `application/json` Content-Type header and the following content.

```
{"message":"Hello World"}
```

## `IResult` return values

| Behavior                                                                                                      | Content-Type                             |
| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| The framework calls [IResult.ExecuteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult.executeasync). | Decided by the `IResult` implementation. |

The `IResult` interface defines a contract that represents the result of an HTTP endpoint. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class and the static [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) are used to create various `IResult` objects that represent different types of responses.

### `TypedResults` versus `Results`

The [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes provide similar sets of results helpers. The `TypedResults` class is the *typed* equivalent of the `Results` class. However, the `Results` helpers' return type is [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult), while each `TypedResults` helper's return type is one of the `IResult` implementation types. The difference means that for `Results` helpers a conversion is needed when the concrete type is needed, for example, for unit testing. The implementation types are defined in the [Microsoft.AspNetCore.Http.HttpResults](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults) namespace.

Returning `TypedResults` rather than `Results` has the following advantages:

* `TypedResults` helpers return strongly typed objects, which can improve code readability, unit testing, and reduce the chance of runtime errors.
* The implementation type [automatically provides the response type metadata for OpenAPI](/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi#describe-response-types) to describe the endpoint.

Consider the following endpoint, for which a `200 OK` status code with the expected JSON response is produced.

```
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();
```

In order to document this endpoint correctly the extensions method `Produces` is called. However, it's not necessary to call `Produces` if `TypedResults` is used instead of `Results`, as shown in the following code. `TypedResults` automatically provides the metadata for the endpoint.

```
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
```

For more information about describing a response type, see [OpenAPI support in Minimal APIs](/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi#describe-response-types-1).

As mentioned previously, when using `TypedResults`, a conversion is not needed. Consider the following Minimal API which returns a `TypedResults` class

```
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}
```

The following test checks for the full concrete type:

```
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}
```

Because all methods on `Results` return `IResult` in their signature, the compiler automatically infers that as the request delegate return type when returning different results from a single endpoint. `TypedResults` requires the use of `Results<T1, TN>` from such delegates.

The following method compiles because both [`Results.Ok`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.ok) and [`Results.NotFound`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.notfound) are declared as returning `IResult`, even though the actual concrete types of the objects returned are different:

```
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());
```

The following method does not compile, because `TypedResults.Ok` and `TypedResults.NotFound` are declared as returning different types and the compiler won't attempt to infer the best matching type:

```
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());
```

To use `TypedResults`, the return type must be fully declared, which when asynchronous requires the `Task<>` wrapper. Using `TypedResults` is more verbose, but that's the trade-off for having the type information be statically available and thus capable of self-describing to OpenAPI:

```
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
   await db.Todos.FindAsync(id)
    is Todo todo
       ? TypedResults.Ok(todo)
       : TypedResults.NotFound());
```

### Results\<TResult1, TResultN>

Use [`Results<TResult1, TResultN>`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.results-2) as the endpoint handler return type instead of `IResult` when:

* Multiple `IResult` implementation types are returned from the endpoint handler.
* The static `TypedResult` class is used to create the `IResult` objects.

This alternative is better than returning `IResult` because the generic union types automatically retain the endpoint metadata. And since the `Results<TResult1, TResultN>` union types implement implicit cast operators, the compiler can automatically convert the types specified in the generic arguments to an instance of the union type.

This has the added benefit of providing compile-time checking that a route handler actually only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to `Results<>` results in a compilation error.

Consider the following endpoint, for which a `400 BadRequest` status code is returned when the `orderId` is greater than `999`. Otherwise, it produces a `200 OK` with the expected content.

```
app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();
```

In order to document this endpoint correctly the extension method `Produces` is called. However, since the `TypedResults` helper automatically includes the metadata for the endpoint, you can return the `Results<T1, Tn>` union type instead, as shown in the following code.

```
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId) 
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
```

[]()

### Built-in results

Common result helpers exist in the [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) and [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) static classes. Returning `TypedResults` is preferred to returning `Results`. For more information, see [`TypedResults` versus `Results`](responses?view=aspnetcore-10.0#typedresults-versus-results).

The following sections demonstrate the usage of the common result helpers.

#### JSON

```
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
```

[WriteAsJsonAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponsejsonextensions.writeasjsonasync) is an alternative way to return JSON:

```
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));
```

#### Custom Status Code

```
app.MapGet("/405", () => Results.StatusCode(405));
```

#### Text

```
app.MapGet("/text", () => Results.Text("This is some text"));
```

[]()

#### Stream

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
```

[`Results.Stream`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.stream?view=aspnetcore-7.0\&preserve-view=true) overloads allow access to the underlying HTTP response stream without buffering. The following example uses [ImageSharp](https://sixlabors.com/products/imagesharp) to return a reduced size of the specified image:

```
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
```

The following example streams an image from [Azure Blob storage](/en-us/azure/storage/blobs/storage-blobs-introduction):

```
app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
```

The following example streams a video from an Azure Blob:

```
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});
```

#### Redirect

```
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
```

#### File

```
app.MapGet("/download", () => Results.File("myfile.text"));
```

[]()

### HttpResult interfaces

Here's an example of a filter that uses one of these interfaces:

```
app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});
```

For more information, see [Filters in Minimal API apps](min-api-filters?view=aspnetcore-10.0) and [IResult implementation types](test-min-api?view=aspnetcore-10.0#iresult-implementation-types).

## Customizing responses

Applications can control responses by implementing a custom [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) type. The following code is an example of an HTML result type:

```
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}
```

We recommend adding an extension method to [Microsoft.AspNetCore.Http.IResultExtensions](/en-us/dotnet/api/microsoft.aspnetcore.http.iresultextensions) to make these custom results more discoverable.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();
```

Also, a custom `IResult` type can provide its own annotation by implementing the [IEndpointMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iendpointmetadataprovider) interface. For example, the following code adds an annotation to the preceding `HtmlResult` type that describes the response produced by the endpoint.

```
class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}
```

The `ProducesHtmlMetadata` is an implementation of [IProducesResponseTypeMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iproducesresponsetypemetadata) that defines the produced response content type `text/html` and the status code `200 OK`.

```
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
```

An alternative approach is using the [Microsoft.AspNetCore.Mvc.ProducesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) to describe the produced response. The following code changes the `PopulateMetadata` method to use `ProducesAttribute`.

```
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
```

## Configure JSON serialization options

By default, Minimal API apps use [`Web defaults`](/en-us/dotnet/standard/serialization/system-text-json-configure-options#web-defaults-for-jsonserializeroptions) options during JSON serialization and deserialization.

### Configure JSON serialization options globally

Options can be configured globally for an app by invoking [ConfigureHttpJsonOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpjsonserviceextensions.configurehttpjsonoptions). The following example includes public fields and formats JSON output.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }
```

Since fields are included, the preceding code reads `NameField` and includes it in the output JSON.

### Configure JSON serialization options for an endpoint

To configure serialization options for an endpoint, invoke [Results.Json](/en-us/dotnet/api/microsoft.aspnetcore.http.results.json) and pass to it a [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object, as shown in the following example:

```
using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }
```

As an alternative, use an overload of [WriteAsJsonAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponsejsonextensions.writeasjsonasync) that accepts a [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object. The following example uses this overload to format the output JSON:

```
using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }
```

## Related content

* [Authentication and authorization in Minimal APIs](security?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor authentication and authorization

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes ASP.NET Core's support for the configuration and management of security in Blazor apps.

Blazor uses the existing ASP.NET Core authentication mechanisms to establish the user's identity. The exact mechanism depends on how the Blazor app is hosted, server-side or client-side.

Security scenarios differ between authorization code running server-side and client-side in Blazor apps. For authorization code that runs on the server, authorization checks are able to enforce access rules for areas of the app and components. Because client-side code execution can be tampered with, authorization code executing on the client can't be trusted to absolutely enforce access rules or control the display of client-side content.

If authorization rule enforcement must be guaranteed, don't implement authorization checks in client-side code. Build a Blazor Web App that only relies on server-side rendering (SSR) for authorization checks and rule enforcement.

If authorization rule enforcement and the security of data and code must be guaranteed, don't develop a client-side app. Build a Blazor Server app.

[Razor Pages authorization conventions](../../razor-pages/security/authorization/conventions?view=aspnetcore-10.0) don't apply to routable Razor components. If a non-routable Razor component is [embedded in a page of a Razor Pages app](../components/integration?view=aspnetcore-10.0), the page's authorization conventions indirectly affect the Razor component along with the rest of the page's content.

ASP.NET Core Identity is designed to work in the context of HTTP request and response communication, which generally isn't the Blazor app client-server communication model. ASP.NET Core apps that use ASP.NET Core Identity for user management should use Razor Pages instead of Razor components for Identity-related UI, such as user registration, login, logout, and other user management tasks. Building Razor components that directly handle Identity tasks is possible for several scenarios but isn't recommended or supported by Microsoft.

ASP.NET Core abstractions, such as [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) and [UserManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1), aren't supported in Razor components. For more information on using ASP.NET Core Identity with Blazor, see [Scaffold ASP.NET Core Identity into a server-side Blazor app](../../security/authentication/scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-server-side-blazor-app).

Note

The code examples in this article adopt [nullable reference types (NRTs) and .NET compiler null-state static analysis](../../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis), which are supported in ASP.NET Core in .NET 6 or later. When targeting .NET 5 or earlier, remove the null type designation (`?`) from examples in this article.

## Securely maintain sensitive data and credentials

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private .NET/C# code, or private keys/tokens in client-side code, which is ***always insecure***. Client-side Blazor code should access secure services and databases through a secure web API that you control.

In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see the following resources:

* [Secure authentication flows (ASP.NET Core documentation)](../../security/?view=aspnetcore-10.0#secure-authentication-flows)
* [Managed identities for Microsoft Azure services (this article)](#managed-identities-for-microsoft-azure-services)

For client-side and server-side local development and testing, use the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) to secure sensitive credentials.

## Managed identities for Microsoft Azure services

For Microsoft Azure services, we recommend using *managed identities*. Managed identities securely authenticate to Azure services without storing credentials in app code. For more information, see the following resources:

* [What are managed identities for Azure resources? (Microsoft Entra documentation)](/en-us/entra/identity/managed-identities-azure-resources/overview)

* Azure services documentation

  * [Managed identities in Microsoft Entra for Azure SQL](/en-us/azure/azure-sql/database/authentication-azure-ad-user-assigned-managed-identity)
  * [How to use managed identities for App Service and Azure Functions](/en-us/azure/app-service/overview-managed-identity)

## Antiforgery support

The Blazor project template:

* Enables automatic Cross-Site Request Forgery (CSRF) protection middleware in apps built with `WebApplication.CreateBuilder`. The middleware inspects the `Sec-Fetch-Site` and `Origin` headers on unsafe HTTP methods and records a validation verdict on the request. Blazor server-side rendering (SSR) form posts enforce that verdict and return `400 Bad Request` for cross-origin form posts that aren't trusted.
* Implicitly adds token-based antiforgery *services* to the app when [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) is called in the `Program` file.

Antiforgery *middleware* isn't automatically included in the request processing pipeline without explicitly calling [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery).

To explicitly add antiforgery middleware, call [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) after the call to [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting). If there are calls to [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints), the call to [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) must go between them. A call to [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) must be placed after calls to [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization).

Adding token-based antiforgery middleware doesn't replace the automatic header-based CSRF protection middleware. When an app calls [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery), both defense mechanisms run for a form post:

* The header-based CSRF protection middleware runs first and records its verdict.
* Antiforgery middleware performs token-based validation.

The token-based result from antiforgery middleware is authoritative and overrides the earlier header-based CSRF middleware verdict.

To disable the automatic header-based CSRF protection middleware, set the `DisableCsrfProtection` configuration key to true. For example, use the app settings file (`appsettings.json`) to disable the middleware:

```
{
  "DisableCsrfProtection": true
}
```

The `DisableCsrfProtection` configuration setting can be supplied by any configuration source, including via an environment variable (`DisableCsrfProtection=true`).

Warning

Disabling the automatic CSRF protection middleware removes the default header-based (`Sec-Fetch-Site`/`Origin`) protection for the entire app. Only disable it if you provide an alternative CSRF defense, such as explicitly adopting token-based antiforgery middleware by calling [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery).

For more information, see [Automatic CSRF protection](../../security/anti-request-forgery?view=aspnetcore-10.0#automatic-csrf-protection-in-aspnet-core).

Important

The following guidance on the [AntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerytoken) component and the [AntiforgeryStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerystateprovider) service only apply to an app that explicitly adopts token-based antiforgery middleware by calling [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) in its request processing pipeline.

* Adds antiforgery services automatically when [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) is called in the `Program` file.
* Adds antiforgery middleware by calling [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) in its request processing pipeline in the `Program` file and requires endpoint [antiforgery protection](../../security/anti-request-forgery?view=aspnetcore-10.0) to mitigate the threats of Cross-Site Request Forgery (CSRF/XSRF). [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) is called after [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection). A call to [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) must be placed after calls, if present, to [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization).

The [AntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerytoken) component renders an antiforgery token as a hidden field, and this component is automatically added to form ([EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform)) instances. For more information, see [ASP.NET Core Blazor forms overview](../forms/?view=aspnetcore-10.0#antiforgery-support).

The [AntiforgeryStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerystateprovider) service provides access to an antiforgery token associated with the current session. Inject the service and call its [GetAntiforgeryToken()](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerystateprovider.getantiforgerytoken#microsoft-aspnetcore-components-forms-antiforgerystateprovider-getantiforgerytoken) method to obtain the current [AntiforgeryRequestToken](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgeryrequesttoken). For more information, see [Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0#antiforgery-support).

Blazor stores request tokens in component state, which guarantees that antiforgery tokens are available to interactive components, even when they don't have access to the request.

Note

[Antiforgery mitigation](../../security/anti-request-forgery?view=aspnetcore-10.0) is only required when submitting form data to the server encoded as `application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain` since these are the [only valid form enctypes](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-enctype).

For more information, see the following resources:

* [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0): This article is the primary ASP.NET Core article on the subject, which applies to server-side Blazor Server, the server project of Blazor Web Apps, and Blazor integration with MVC/Razor Pages.
* [ASP.NET Core Blazor forms overview](../forms/?view=aspnetcore-10.0#antiforgery-support): The *Antiforgery support* section of the article pertains to Blazor forms antiforgery support.

## Server-side Blazor authentication

Server-side Blazor apps are configured for security in the same manner as ASP.NET Core apps. For more information, see the articles under [ASP.NET Core security topics](../../security/?view=aspnetcore-10.0).

The authentication context is only established when the app starts, which is when the app first [connects to the WebSocket over a SignalR connection](../../signalr/authn-and-authz?view=aspnetcore-10.0) with the client. Authentication can be based on a cookie or some other bearer token, but authentication is managed via the SignalR hub and entirely within the [circuit](../hosting-models?view=aspnetcore-10.0#blazor-server). The authentication context is maintained for the lifetime of the connection and is re-evaluated on reconnection.

If the app must capture users for custom services or react to updates to the user, see [ASP.NET Core Blazor additional server-side security scenarios](additional-scenarios?view=aspnetcore-10.0#circuit-handler-to-capture-users-for-custom-services).

Blazor differs from traditional server-rendered web apps that make new HTTP requests with cookies on every page navigation. Authentication is checked during navigation events. However, cookies aren't involved. Cookies are only sent when making an HTTP request to a server, which isn't what happens when the user navigates in a Blazor app. During navigation, the user's authentication state is checked within the Blazor circuit, which you can update at any time on the server using the [`RevalidatingAuthenticationStateProvider` abstraction](#additional-security-abstractions).

Important

Implementing a custom `NavigationManager` to achieve authentication validation during navigation isn't recommended. If the app must execute custom authentication state logic during navigation, use a [custom `AuthenticationStateProvider`](authentication-state?view=aspnetcore-10.0#implement-a-custom-authenticationstateprovider).

Note

The code examples in this article adopt [nullable reference types (NRTs) and .NET compiler null-state static analysis](../../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis), which are supported in ASP.NET Core in .NET 6 or later. When targeting .NET 5 or earlier, remove the null type designation (`?`) from the examples in this article.

The built-in or custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) service obtains authentication state data from ASP.NET Core's [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user). This is how authentication state integrates with existing ASP.NET Core authentication mechanisms.

### Shared state

Server-side Blazor apps live in server memory, and multiple app sessions are hosted within the same process. For each app session, Blazor starts a circuit with its own dependency injection container scope, thus scoped services are unique per Blazor session.

Warning

We don't recommend apps on the same server share state using singleton services unless extreme care is taken, as this can introduce security vulnerabilities, such as leaking user state across circuits.

You can use stateful singleton services in Blazor apps if they're specifically designed for it. For example, use of a singleton memory cache is acceptable because a memory cache requires a key to access a given entry. Assuming users don't have control over the cache keys that are used with the cache, state stored in the cache doesn't leak across circuits.

For general guidance on state management, see [ASP.NET Core Blazor state management overview](../state-management/?view=aspnetcore-10.0).

### Server-side security of sensitive data and credentials

In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see the following resources:

* [Secure authentication flows (ASP.NET Core documentation)](../../security/?view=aspnetcore-10.0#secure-authentication-flows)
* [Managed identities for Microsoft Azure services (Blazor documentation)](#managed-identities-for-microsoft-azure-services)

For client-side and server-side local development and testing, use the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) to secure sensitive credentials.

### Project template

Create a new server-side Blazor app by following the guidance in [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0).

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [.NET CLI](#tabpanel_1_net-cli)

After choosing the server-side app template and configuring the project, select the app's authentication under **Authentication type**:

* **None** (default): No authentication.
* **Individual Accounts**: User accounts are stored within the app using ASP.NET Core [Identity](../../security/authentication/identity?view=aspnetcore-10.0).

- **None** (default): No authentication.
- **Individual Accounts**: User accounts are stored within the app using ASP.NET Core [Identity](../../security/authentication/identity?view=aspnetcore-10.0).
- **Microsoft identity platform**: For more information, see the links in the [Related content](#related-content) section.
- **Windows**: Use Windows Authentication.

When issuing the .NET CLI command to create and configure the server-side Blazor app, indicate the authentication mechanism with the `-au|--auth` option:

```
-au {AUTHENTICATION}
```

Note

For the full command, see [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0).

Permissible authentication values for the `{AUTHENTICATION}` placeholder are shown in the following table.

| Authentication mechanism | Description                                        |
| ------------------------ | -------------------------------------------------- |
| `None` (default)         | No authentication                                  |
| `Individual`             | Users stored in the app with ASP.NET Core Identity |

| Authentication mechanism | Description                                                                                     |
| ------------------------ | ----------------------------------------------------------------------------------------------- |
| `None` (default)         | No authentication                                                                               |
| `Individual`             | Users stored in the app with ASP.NET Core Identity                                              |
| `IndividualB2C`          | Users stored in [Azure AD B2C](../../security/authentication/azure-ad-b2c?view=aspnetcore-10.0) |
| `SingleOrg`              | Organizational authentication for a single tenant                                               |
| `MultiOrg`               | Organizational authentication for multiple tenants                                              |
| `Windows`                | Windows Authentication                                                                          |

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

For more information, see the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) command in the .NET Guide.

When issuing the .NET CLI command to create and configure the server-side Blazor app, indicate the authentication mechanism with the `-au|--auth` option:

```
-au {AUTHENTICATION}
```

Note

For the full command, see [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0).

Permissible authentication values for the `{AUTHENTICATION}` placeholder are shown in the following table.

| Authentication mechanism | Description                                        |
| ------------------------ | -------------------------------------------------- |
| `None` (default)         | No authentication                                  |
| `Individual`             | Users stored in the app with ASP.NET Core Identity |

| Authentication mechanism | Description                                                                                     |
| ------------------------ | ----------------------------------------------------------------------------------------------- |
| `None` (default)         | No authentication                                                                               |
| `Individual`             | Users stored in the app with ASP.NET Core Identity                                              |
| `IndividualB2C`          | Users stored in [Azure AD B2C](../../security/authentication/azure-ad-b2c?view=aspnetcore-10.0) |
| `SingleOrg`              | Organizational authentication for a single tenant                                               |
| `MultiOrg`               | Organizational authentication for multiple tenants                                              |
| `Windows`                | Windows Authentication                                                                          |

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

For more information:

* See the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) command in the .NET Guide.

* Execute the help command for the template in a command shell:

  ```
  dotnet new {PROJECT TEMPLATE} --help
  ```

  In the preceding command, the `{PROJECT TEMPLATE}` placeholder is the project template.

### Blazor Identity UI (Individual Accounts)

Blazor supports generating a full Blazor-based Identity UI when you choose the authentication option for *Individual Accounts*.

The Blazor Web App template scaffolds Identity code for a SQL Server database. The command line version uses SQLite and includes a SQLite database for Identity.

The template:

* Supports interactive server-side rendering (interactive SSR) and client-side rendering (CSR) scenarios with authenticated users.
* Adds Identity Razor components and related logic for routine authentication tasks, such as signing users in and out. The Identity components also support advanced Identity features, such as [account confirmation and password recovery](../../security/authentication/accconfirm?view=aspnetcore-10.0) and [multi-factor authentication](../../security/authentication/mfa?view=aspnetcore-10.0) using a third-party app. Note that the Identity components themselves don't support interactivity.
* Adds the Identity-related packages and dependencies.
* References the Identity packages in the imports file (`_Imports.razor`).
* Creates a custom user Identity class (`ApplicationUser`).
* Creates and registers an EF Core database context (`ApplicationDbContext`).
* Configures routing for the built-in Identity endpoints.
* Includes Identity validation and business logic.

To inspect the Blazor framework's Identity components, access them in the `Pages` and `Shared` folders of the `Components/Account` folder in the server project of the [Blazor Web App project template (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp).

When you choose the Interactive WebAssembly or Interactive Auto render modes, the server handles all authentication and authorization requests, and the Identity components render statically on the server in the Blazor Web App's main project.

The framework provides a custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) in both the server and client (`.Client`) projects to flow the user's authentication state to the browser. The server project calls [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization), while the client project calls [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization). Authenticating on the server rather than the client allows the app to access authentication state during prerendering and before the .NET WebAssembly runtime is initialized. The custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) implementations use the [Persistent Component State service](../state-management/prerendered-state-persistence?view=aspnetcore-10.0) ([PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate)) to serialize the authentication state into HTML comments and then read it back from WebAssembly to create a new [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) instance. For more information, see the [Manage authentication state in Blazor Web Apps](#manage-authentication-state-in-blazor-web-apps) section.

Only for Interactive Server solutions, `IdentityRevalidatingAuthenticationStateProvider` (`Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs`) in the server project of the [Blazor Web App project template (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp) is a server-side [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) that revalidates the security stamp for the connected user every 30 minutes an interactive circuit is connected.

When you choose the Interactive WebAssembly or Interactive Auto render modes, the server handles all authentication and authorization requests, and the Identity components render statically on the server in the Blazor Web App's main project. The project template includes a [`PersistentAuthenticationStateProvider` class (reference source)](https://github.com/dotnet/aspnetcore/blob/release/8.0/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs) in the `.Client` project to synchronize the user's authentication state between the server and the browser. The class is a custom implementation of [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider). The provider uses the [Persistent Component State service](../state-management/prerendered-state-persistence?view=aspnetcore-10.0) ([PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate)) to prerender the authentication state and persist it to the page.

In the main project of a Blazor Web App, the authentication state provider is named either `IdentityRevalidatingAuthenticationStateProvider` in the `Components/Account` folder of the server project in the [Blazor Web App project template (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp) (Server interactivity solutions only) or the `PersistingRevalidatingAuthenticationStateProvider` (WebAssembly or Auto interactivity solutions) in the same folder.

Blazor Identity depends on [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) instances not [created by a factory](../blazor-ef-core?view=aspnetcore-10.0#new-dbcontext-instances), which is intentional because [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) is sufficient for the project template's Identity components to render statically without supporting interactivity.

For a description on how global interactive render modes are applied to non-Identity components while at the same time enforcing static SSR for the Identity components, see [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0#area-folder-of-static-ssr-components).

For more information on persisting prerendered state, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Manage authentication state in Blazor Web Apps

*This section applies to Blazor Web Apps that adopt:*

* Individual Accounts
* *Client-side rendering (CSR, WebAssembly-based interactivity).*

A client-side authentication state provider is only used within Blazor and isn't integrated with the ASP.NET Core authentication system. During prerendering, Blazor respects the metadata defined on the page and uses the ASP.NET Core authentication system to determine if the user is authenticated. When a user navigates from one page to another, a client-side authentication provider is used. When the user refreshes the page (full-page reload), the client-side authentication state provider isn't involved in the authentication decision on the server. Since the user's state isn't persisted by the server, any authentication state maintained client-side is lost.

To address this, the best approach is to perform authentication within the ASP.NET Core authentication system. The client-side authentication state provider only takes care of reflecting the user's authentication state. Examples for how to accomplish this with authentication state providers are demonstrated by the Blazor Web App project template and described below.

In the server project's `Program` file, call [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization), which serializes the [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) returned by the server-side [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) using the [Persistent Component State service](../state-management/prerendered-state-persistence?view=aspnetcore-10.0) ([PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate)):

```
builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents()
    .AddAuthenticationStateSerialization();
```

The API only serializes the server-side name and role claims for access in the browser. To include all claims, set [SerializeAllClaims](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.server.authenticationstateserializationoptions.serializeallclaims) to `true` in the server-side call to [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization):

```
builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents()
    .AddAuthenticationStateSerialization(
        options => options.SerializeAllClaims = true);
```

In the client (`.Client`) project's `Program` file, call [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization), which adds an [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) where the [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) is deserialized from the server using `AuthenticationStateData` and the [Persistent Component State service](../state-management/prerendered-state-persistence?view=aspnetcore-10.0) ([PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate)). There should be a corresponding call to [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization) in the server project.

```
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization();
```

* [`PersistingRevalidatingAuthenticationStateProvider` (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/release/8.0/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs): For Blazor Web Apps that adopt interactive server-side rendering (interactive SSR) and client-side rendering (CSR). This is a server-side [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) that revalidates the security stamp for the connected user every 30 minutes an interactive circuit is connected. It also uses the [Persistent Component State service](../state-management/prerendered-state-persistence?view=aspnetcore-10.0) to flow the authentication state to the client, which is then fixed for the lifetime of CSR.

* [`PersistingServerAuthenticationStateProvider` (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/release/8.0/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Account/PersistingServerAuthenticationStateProvider.cs): For Blazor Web Apps that only adopt CSR. This is a server-side [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) that uses the [Persistent Component State service](../state-management/prerendered-state-persistence?view=aspnetcore-10.0) to flow the authentication state to the client, which is then fixed for the lifetime of CSR.

* [`PersistentAuthenticationStateProvider` (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/release/8.0/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp.Client/PersistentAuthenticationStateProvider.cs): For Blazor Web Apps that adopt CSR. This is a client-side [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) that determines the user's authentication state by looking for data persisted in the page when it was rendered on the server. This authentication state is fixed for the lifetime of CSR. If the user needs to log in or out, a full-page reload is required. This only provides a user name and email for display purposes. It doesn't include tokens that authenticate to the server when making subsequent requests, which is handled separately using a cookie that's included on `HttpClient` requests to the server.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Scaffold Identity

For more information on scaffolding Identity into a server-side Blazor app, see [Scaffold Identity in ASP.NET Core projects](../../security/authentication/scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-server-side-blazor-app).

Scaffold Identity into a server-side Blazor app:

* [Without existing authorization](../../security/authentication/scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-server-side-blazor-app-without-existing-authorization).
* [With authorization](../../security/authentication/scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-server-side-blazor-app-with-authorization).

### Additional claims and tokens from external providers

To store additional claims from external providers, see [Persist additional claims and tokens from external providers in ASP.NET Core](../../security/authentication/social/additional-claims?view=aspnetcore-10.0).

### Azure App Service on Linux with Identity Server

Specify the issuer explicitly when deploying to Azure App Service on Linux with Identity Server. For more information, see [Use Identity to secure a Web API backend for SPAs](../../security/authentication/identity-api-authorization?view=aspnetcore-10.0#azure-app-service-on-linux).

### Inject `AuthenticationStateProvider` for services scoped to a component

Don't attempt to resolve [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) within a custom scope because it results in the creation of a new instance of the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) that isn't correctly initialized.

To access the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) within a service scoped to a component, inject the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) into the component and pass it to the service as a parameter. This approach ensures that the correct, initialized instance of the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) is used for each user app instance.

`ExampleService.cs`:

```
public class ExampleService
{
    public async Task<string> ExampleMethod(AuthenticationStateProvider authStateProvider)
    {
        var authState = await authStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            return $"{user.Identity.Name} is authenticated.";
        }
        else
        {
            return "The user is NOT authenticated.";
        }
    }
}
```

Register the service as scoped. In a server-side Blazor app, scoped services have a lifetime equal to the duration of the client connection [circuit](../hosting-models?view=aspnetcore-10.0#blazor-server).

In the `Program` file:

```
builder.Services.AddScoped<ExampleService>();
```

In `Startup.ConfigureServices` of `Startup.cs`:

```
services.AddScoped<ExampleService>();
```

In the following `InjectAuthStateProvider` component:

* The component inherits [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase).
* The [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) is injected and passed to `ExampleService.ExampleMethod`.
* `ExampleService` is resolved with [OwningComponentBase.ScopedServices](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase.scopedservices#microsoft-aspnetcore-components-owningcomponentbase-scopedservices) and [GetRequiredService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.getrequiredservice), which returns the correct, initialized instance of `ExampleService` that exists for the lifetime of the user's circuit.

`InjectAuthStateProvider.razor`:

```
@page "/inject-auth-state-provider"
@inherits OwningComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>Inject <code>AuthenticationStateProvider</code> Example</h1>

<p>@message</p>

@code {
    private string? message;
    private ExampleService? ExampleService { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ExampleService = ScopedServices.GetRequiredService<ExampleService>();

        message = await ExampleService.ExampleMethod(AuthenticationStateProvider);
    }
}
```

```
@page "/inject-auth-state-provider"
@inject AuthenticationStateProvider AuthenticationStateProvider
@inherits OwningComponentBase

<h1>Inject <code>AuthenticationStateProvider</code> Example</h1>

<p>@message</p>

@code {
    private string? message;
    private ExampleService? ExampleService { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ExampleService = ScopedServices.GetRequiredService<ExampleService>();

        message = await ExampleService.ExampleMethod(AuthenticationStateProvider);
    }
}
```

For more information, see the guidance on [OwningComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.owningcomponentbase) in [ASP.NET Core Blazor dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0#owningcomponentbase).

### Unauthorized content display while prerendering with a custom `AuthenticationStateProvider`

To avoid showing unauthorized content, for example content in an [`AuthorizeView` component](#authorizeview-component), while prerendering with a [custom `AuthenticationStateProvider`](authentication-state?view=aspnetcore-10.0#implement-a-custom-authenticationstateprovider), adopt ***one*** of the following approaches:

* Implement [IHostEnvironmentAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.ihostenvironmentauthenticationstateprovider) for the custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) to support prerendering: For an example implementation of [IHostEnvironmentAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.ihostenvironmentauthenticationstateprovider), see the Blazor framework's [ServerAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.server.serverauthenticationstateprovider) implementation in [`ServerAuthenticationStateProvider.cs` (reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Endpoints/src/DependencyInjection/ServerAuthenticationStateProvider.cs).

  Note

  Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

- Disable prerendering: Indicate the render mode with the `prerender` parameter set to `false` at the highest-level component in the app's component hierarchy that isn't a root component.

  Note

  Making a root component interactive, such as the `App` component, isn't supported. Therefore, prerendering can't be disabled directly by the `App` component.

  For apps based on the Blazor Web App project template, prerendering is typically disabled where the `Routes` component is used in the `App` component (`Components/App.razor`) :

  ```
  <Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
  ```

  Also, disable prerendering for the `HeadOutlet` component:

  ```
  <HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
  ```

  You can also selectively control the render mode applied to the `Routes` component instance. For example, see [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0#static-ssr-pages-in-an-interactive-app).

* Disable prerendering: Open the `_Host.cshtml` file and change the `render-mode` attribute of the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) to [Server](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-server):

  ```
  <component type="typeof(App)" render-mode="Server" />
  ```

- Authenticate the user on the server before the app starts: To adopt this approach, the app must respond to a user's initial request with the Identity-based sign-in page or view and prevent any requests to Blazor endpoints until they're authenticated. For more information, see [Require global user authentication](../../security/authorization/policies?view=aspnetcore-10.0#require-global-user-authentication). After authentication, unauthorized content in prerendered Razor components is only shown when the user is truly unauthorized to view the content.

### User state management

In spite of the word "state" in the name, [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) isn't for storing *general user state*. [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) only indicates the user's authentication state to the app, whether they are signed into the app and who they are signed in as.

Authentication uses the same ASP.NET Core Identity authentication as Razor Pages and MVC apps. The user state stored for ASP.NET Core Identity flows to Blazor without adding additional code to the app. Follow the guidance in the ASP.NET Core Identity articles and tutorials for the Identity features to take effect in the Blazor parts of the app.

For guidance on general state management outside of ASP.NET Core Identity, see [ASP.NET Core Blazor state management overview](../state-management/?view=aspnetcore-10.0).

### Additional security abstractions

Two additional abstractions participate in managing authentication state:

* [ServerAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.server.serverauthenticationstateprovider) ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Endpoints/src/DependencyInjection/ServerAuthenticationStateProvider.cs)): An [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) used by the Blazor framework to obtain authentication state from the server.

* [RevalidatingServerAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider) ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs)): A base class for [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) services used by the Blazor framework to receive an authentication state from the host environment and revalidate it at regular intervals, 30 minutes by default.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Authentication state management at sign out

The default revalidation interval is 30 minutes for either ASP.NET Core Identity-based authentication or cookie-based authentication without Identity. Within the 30-minute window, it remains possible under certain sign-out conditions for a user to retain access to areas of the app that you might wish to prevent.

To control the revalidation period and enforce a complete sign out process for users, begin by implementing a [RevalidatingServerAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider) with a shorter [RevalidationInterval](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider.revalidationinterval).

For an example implementation showing the default 30-minute interval, see the [`IdentityRevalidatingAuthenticationStateProvider` class (reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs) in the Blazor Web App project template.

In the following example, the interval is set to five minutes:

```
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(5);
```

Implementing [RevalidatingServerAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider) with a short [RevalidationInterval](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider.revalidationinterval) only revalidates the authentication state held by the current Blazor circuit. Returning `false` from [RevalidatingServerAuthenticationStateProvider.ValidateAuthenticationStateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider.validateauthenticationstateasync) flips the circuit's state to unauthenticated, so instances of [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview)/[AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) re-evaluate and redirect the user to sign in. However, returning `false` from [ValidateAuthenticationStateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider.validateauthenticationstateasync) doesn't affect the underlying authentication cookie. The next full navigation that occurs before the cookie expires or is invalidated recreates the principal from the cookie. When that happens, the cookie indicates a signed-in user to the [RevalidatingServerAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider), so the user appears signed in until the next [RevalidationInterval](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider.revalidationinterval) tick fires.

Note

Each browser tab requires a separate circuit, so additional tabs opened by a user don't observe an authentication state change until their circuits revalidate. However, the approach in this section sets the revalidation interval for all of the tabs opened by a user.

To control the revalidation interval in apps that adopt ASP.NET Core Identity with cookie authentication, see the following [Sign out for ASP.NET Core Identity](#sign-out-for-aspnet-core-identity) subsection for details. For apps that adopt cookie-based authentication without Identity, see the following [Sign out for cookie-based authentication](#sign-out-for-cookie-based-authentication) subsection.

#### Sign out for ASP.NET Core Identity

To force a complete sign-out within less than the default 30-minute revalidation interval in apps that adopt ASP.NET Core Identity, use the guidance in this section.

For Blazor apps that target .NET 8 or later, reduce the default 30-minute [RevalidationInterval](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider.revalidationinterval) in the `IdentityRevalidatingAuthenticationStateProvider` class (`Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs`). If the app targets .NET earlier than .NET 8, reduce the interval in `RevalidatingIdentityAuthenticationStateProvider`.

Whether or not the authentication cookie remains valid is checked by the *security stamp validator* ([SecurityStampValidator](/en-us/dotnet/api/microsoft.aspnetcore.identity.securitystampvalidator)), which hooks into the [CookieAuthenticationEvents.OnValidatePrincipal](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.onvalidateprincipal#microsoft-aspnetcore-authentication-cookies-cookieauthenticationevents-onvalidateprincipal) event of an authentication cookie and queries the user datastore to determine if the user is still signed in. The security stamp validator's interval is governed by [SecurityStampValidatorOptions.ValidationInterval](/en-us/dotnet/api/microsoft.aspnetcore.identity.securitystampvalidatoroptions.validationinterval), which defaults to 30 minutes because validating users on every request triggers a database query on every request for every user.

The following example shortens the default 30-minute interval to four minutes:

```
builder.Services.Configure<SecurityStampValidatorOptions>(
    o => o.ValidationInterval = TimeSpan.FromMinutes(4));
```

The call interval is a tradeoff between hitting the user datastore too frequently and not often enough. Checking with a short interval can result in high demand on the user datastore and reduced app performance but with the benefit of more timely sign-outs. Checking with a long interval results in stale claims, which can make it appear that a user is still signed in if a full navigation recreates the principal from the authentication cookie before it naturally expires.

To catch the next interval tick of the [RevalidatingServerAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider), set the [SecurityStampValidatorOptions.ValidationInterval](/en-us/dotnet/api/microsoft.aspnetcore.identity.securitystampvalidatoroptions.validationinterval) to a period just inside the value set for [RevalidatingServerAuthenticationStateProvider.RevalidationInterval](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider.revalidationinterval).

Custom C# code that's required to force a sign out on a user can call [UserManager\<TUser>.UpdateSecurityStampAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1.updatesecuritystampasync), which immediately invalidates existing cookies the next time they're checked.

For more information, see [Configure ASP.NET Core Identity](../../security/authentication/identity-configuration?view=aspnetcore-10.0#isecuritystampvalidator-and-signout-everywhere).

#### Sign out for cookie-based authentication

To proactively, completely sign a user off within less than the default 30-minute revalidation interval in apps that adopt cookie-based authentication without ASP.NET Core Identity, use the guidance in this section.

There are two approaches that you can take. The first approach is to wait for a revalidation check to occur and ensure cookie invalidation when the check is made. To adopt this approach, pair an implementation of [RevalidatingServerAuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider) with a shorter [RevalidationInterval](/en-us/dotnet/api/microsoft.aspnetcore.components.server.revalidatingserverauthenticationstateprovider.revalidationinterval) (default: 30 minutes) and a sign-out trigger. Implement the sign-out trigger using ***either*** of the following approaches.

* Sign out on GET in the app's login page. This approach is only valid for static SSR because [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is `null` during interactive rendering:

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

  protected override async Task OnInitializedAsync()
  {
      ...

      if (HttpMethods.IsGet(HttpContext.Request.Method))
      {
          await HttpContext.SignOutAsync(
            CookieAuthenticationDefaults.AuthenticationScheme);
      }
  }
  ```

* Call `NavigationManager.NavigateTo("/Account/Logout", forceLoad: true)` where you sign out users. Create an endpoint for `/Account/Logout` with a call to [MapGet](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapget) in the app's `Program` file, which in turn calls [SignOutAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationservice.signoutasync):

  ```
  app.MapGet("/Account/Logout", async (HttpContext context) =>
  {
      await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

      return TypedResults.LocalRedirect("/");
  })
  .RequireAuthorization();
  ```

The alternative second approach is aimed at first-request freshness without waiting for the next revalidation check. To adopt this approach, perform a user datastore authentication check in [CookieAuthenticationEvents.OnValidatePrincipal](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.onvalidateprincipal#microsoft-aspnetcore-authentication-cookies-cookieauthenticationevents-onvalidateprincipal) and call [CookieValidatePrincipalContext.RejectPrincipal](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookievalidateprincipalcontext.rejectprincipal) with [SignOutAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationservice.signoutasync).

For more information, see the following sections of the *Use cookie authentication without ASP.NET Core Identity* article:

* [Sign out](../../security/authentication/cookie?view=aspnetcore-10.0#sign-out)
* [React to back-end changes](../../security/authentication/cookie?view=aspnetcore-10.0#react-to-back-end-changes)

### Temporary redirection URL validity duration

*This section applies to Blazor Web Apps.*

Use the [RazorComponentsServiceOptions.TemporaryRedirectionUrlValidityDuration](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions.temporaryredirectionurlvalidityduration) option to get or set the lifetime of ASP.NET Core Data Protection validity for temporary redirection URLs emitted by Blazor server-side rendering. These are only used transiently, so the lifetime only needs to be long enough for a client to receive the URL and begin navigation to it. However, it should also be long enough to allow for clock skew across servers. The default value is five minutes.

In the following example the value is extended to seven minutes:

```
builder.Services.AddRazorComponents(options => 
    options.TemporaryRedirectionUrlValidityDuration = 
        TimeSpan.FromMinutes(7));
```

## Client-side Blazor authentication

In client-side Blazor apps, client-side authentication checks can be bypassed because all client-side code can be modified by users. The same is true for all client-side app technologies, including JavaScript SPA frameworks and native apps for any operating system.

Add the following:

* A package reference for the [`Microsoft.AspNetCore.Components.Authorization`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization) NuGet package.

  Note

  For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

* The [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespace to the app's imports file (`_Imports.razor`).

To handle authentication, use the built-in or custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) service.

For more information on client-side authentication, see [Secure ASP.NET Core Blazor WebAssembly](webassembly/?view=aspnetcore-10.0).

## Secure data in Blazor Web Apps with Interactive Auto rendering

When a Blazor Web App adopts server-side rendering (SSR) and client-side rendering (CSR) for components or an entire app that specifies the [Interactive Auto render mode](../components/render-modes?view=aspnetcore-10.0#automatic-auto-rendering), authorization to access components and data is applied in *two places*. The component restricts access to itself (and any data that it obtains) when rendered on the server by virtue of an authorization attribute in the component's definition file (`@attribute [Authorize]`). When the component is rendered on the client, access to data is restricted via the server web API endpoints that are called from the client. Care must be taken when securing data access in both locations to prevent improper data access.

Consider the following scenario where secure weather data is displayed by a component. Demonstrations of some of the following approaches can be evaluated and tested using the `BlazorWebAppEntra`/`BlazorWebAppEntraBff` samples (.NET 9 or later) or the `BlazorWebAppOidc`/`BlazorWebAppOidcBff` samples (.NET 8 or later) in the [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps)).

The client project maintains a `WeatherForecast` class to hold weather data:

```
public sealed class WeatherForecast(DateOnly date, int temperatureC, string summary)
{
    public DateOnly Date { get; set; } = date;
    public int TemperatureC { get; set; } = temperatureC;
    public string? Summary { get; set; } = summary;
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

The client project's `IWeatherForecaster` interface defines a `GetWeatherForecastAsync` method for obtaining weather data:

```
public interface IWeatherForecaster
{
    Task<IEnumerable<WeatherForecast>> GetWeatherForecastAsync();
}
```

The client project's `ClientWeatherForecaster` service implements `IWeatherForecaster`. The `GetWeatherForecastAsync` method calls a web API in the server project at the `/weather-forecast` endpoint for weather data:

```
internal sealed class ClientWeatherForecaster(HttpClient httpClient) 
    : IWeatherForecaster
{
    public async Task<IEnumerable<WeatherForecast>> GetWeatherForecastAsync() =>
        await httpClient.GetFromJsonAsync<WeatherForecast[]>("/weather-forecast") ??
            throw new IOException("No weather forecast!");
}
```

The client project maintains a `Weather` component that:

* Enforces authorization with an [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute).
* Uses the [Persistent Component State service](../state-management/prerendered-state-persistence?view=aspnetcore-10.0) ([PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate)) to persist weather forecast data when the component transitions from static to interactive SSR on the server. For more information, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0).

```
@page "/weather"
@using Microsoft.AspNetCore.Authorization
@using BlazorWebAppEntra.Client.Weather
@attribute [Authorize]
@inject IWeatherForecaster WeatherForecaster

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<p>This component demonstrates showing data.</p>

@if (Forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th aria-label="Temperature in Celsius">Temp. (C)</th>
                <th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in Forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    [PersistentState]
    public IEnumerable<WeatherForecast>? Forecasts { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Forecasts ??= await WeatherForecaster.GetWeatherForecastAsync();
    }
}
```

```
@page "/weather"
@using Microsoft.AspNetCore.Authorization
@using BlazorWebAppEntra.Client.Weather
@attribute [Authorize]
@implements IDisposable
@inject PersistentComponentState ApplicationState
@inject IWeatherForecaster WeatherForecaster

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<p>This component demonstrates showing data.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th aria-label="Temperature in Celsius">Temp. (C)</th>
                <th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private IEnumerable<WeatherForecast>? forecasts;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<IEnumerable<WeatherForecast>>(
            nameof(forecasts), out var restoredData))
        {
            forecasts = await WeatherForecaster.GetWeatherForecastAsync();
        }
        else
        {
            forecasts = restoredData!;
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson(nameof(forecasts), forecasts);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose() => persistingSubscription.Dispose();
}
```

The server project implements `IWeatherForecaster` as `ServerWeatherForecaster`, which generates and returns weather data via its `GetWeatherForecastAsync` method:

```
internal sealed class ServerWeatherForecaster() : IWeatherForecaster
{
    public readonly string[] summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", 
        "Sweltering", "Scorching"
    ];

    public async Task<IEnumerable<WeatherForecast>> GetWeatherForecastAsync()
    {
        // Simulate asynchronous loading to demonstrate streaming rendering
        await Task.Delay(500);

        return Enumerable.Range(1, 5).Select(index =>
            new WeatherForecast
            (
                DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                Random.Shared.Next(-20, 55),
                summaries[Random.Shared.Next(summaries.Length)]
            ))
        .ToArray();
    }
}
```

If the app must call an external web API to obtain the weather data, you can inject an HTTP client (`HttpClient`) to request the data:

```
internal sealed class ServerWeatherForecaster(HttpClient httpClient, 
    IHttpContextAccessor httpContextAccessor) : IWeatherForecaster
{
    public async Task<IEnumerable<WeatherForecast>> GetWeatherForecastAsync()
    {
        var httpContext = httpContextAccessor.HttpContext ??
            throw new InvalidOperationException("No HttpContext!");
        var accessToken = await httpContext.GetTokenAsync("access_token") ??
            throw new InvalidOperationException("No access_token was saved");
        using var request = 
            new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
        request.Headers.Authorization = new("Bearer", accessToken);
        using var response = await httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
            throw new IOException("No weather forecast!");
    }
}
```

In yet another approach, you can inject an HTTP client factory (`IHttpClientFactory`) into the `ServerWeatherForecaster` and call an external web API using a named HTTP Client with a token handler. For more information, see [Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0#use-a-token-handler-for-web-API-calls).

If the app uses [Microsoft identity platform](/en-us/entra/identity-platform/) with [Microsoft Identity Web packages](/en-us/entra/msal/dotnet/microsoft-identity-web/) for [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra) (see [Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0#microsoft-identity-platform-for-web-api-calls)), the following `ServerWeatherForecaster` demonstrates making an external web API call. The access token is automatically attached to the request.

```
internal sealed class ServerWeatherForecaster(IDownstreamApi downstreamApi) : IWeatherForecaster
{
    public async Task<IEnumerable<WeatherForecast>> GetWeatherForecastAsync()
    {
        using var response = await downstreamApi.CallApiForUserAsync("DownstreamApi",
            options =>
            {
                options.RelativePath = "/weather-forecast";
            });

        return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
            throw new IOException("No weather forecast!");
    }
}
```

Regardless of the approach taken by the `ServerWeatherForecaster` to obtain the data, the server project maintains a secure web API endpoint for client weather data calls. This endpoint results in a `ServerWeatherForecaster.GetWeatherForecastAsync` call on the server:

```
app.MapGet("/weather-forecast", (
    [FromServices] IWeatherForecaster WeatherForecaster) =>
{
    return WeatherForecaster.GetWeatherForecastAsync();
}).RequireAuthorization();
```

Using the preceding approach, there are two systems in place to supply secure weather data to the user:

* When the `Weather` component is rendered *on the server*, the `ServerWeatherForecaster` service's `GetWeatherForecastAsync` method is used directly to obtain the weather data. The security of the data is enforced by the component's [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute). In summary, the security of the weather data is enforced by the component.
* When the `Weather` component is rendered *on the client*, the `ClientWeatherForecaster` service is used to make a web API call to the secure `/weather-forecast` endpoint that applies the [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) extension method. If the user has the authority to access weather data, the endpoint uses the `ServerWeatherForecaster` service to call `GetWeatherForecastAsync`. The data is returned to the client. In summary, the security of the weather data is enforced by the server app's web API endpoint.

The preceding approach works well when the security requirements of the web API match the security requirements of the component. For example, the same authorization policy can be applied to both the web API endpoint and the component.

Complex scenarios require additional planning and implementation. For example, a server web API that has multiple callers with different access permissions either requires a more sophisticated authorization policy, one or more additional policies, or additional endpoints with different access requirements.

As you build security into apps that adopt Interactive Auto rendering, be mindful that the security implemented for the server's web API endpoints doesn't secure the server's service implementation that's used when a component is rendered on the server and accesses data through the service. Carefully weigh the difference between accessing data on the server during SSR versus accessing the data on a client web API request during CSR. Strategically apply security to avoid improper access to data.

Examples in the [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples/) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps)) that demonstrate the approach described in this section:

* `BlazorWebAppOidc`
* `BlazorWebAppOidcBff`
* `BlazorWebAppEntra`
* `BlazorWebAppEntraBff`

## `AuthenticationStateProvider` service

[AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) is the underlying service used by the [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component and cascading authentication services to obtain the authentication state for a user.

[AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) is the underlying service used by the [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component and [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) component to obtain the authentication state for a user.

You don't typically use [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) directly. Use the [`AuthorizeView` component](#authorizeview-component) or [`Task<AuthenticationState>`](#expose-the-authentication-state-as-a-cascading-parameter) approaches described later in this article. The main drawback to using [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) directly is that the component isn't notified automatically if the underlying authentication state data changes.

To implement a custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider), see [ASP.NET Core Blazor authentication state](authentication-state?view=aspnetcore-10.0), which includes guidance on implementing user authentication state change notifications.

## Obtain a user's claims principal data

The [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) service can provide the current user's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) data, as shown in the following example.

`ClaimsPrincipalData.razor`:

```
@page "/claims-principal-data"
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>ClaimsPrincipal Data</h1>

<button @onclick="GetClaimsPrincipalData">Get ClaimsPrincipal Data</button>

<p>@authMessage</p>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}

<p>@surname</p>

@code {
    private string? authMessage;
    private string? surname;
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    private async Task GetClaimsPrincipalData()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            authMessage = $"{user.Identity.Name} is authenticated.";
            claims = user.Claims;
            surname = user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value;
        }
        else
        {
            authMessage = "The user is NOT authenticated.";
        }
    }
}
```

In the preceding example:

* [ClaimsPrincipal.Claims](/en-us/dotnet/api/system.security.claims.claimsprincipal.claims) returns the user's claims (`claims`) for display in the UI.
* The line that obtains the user's surname (`surname`) calls [ClaimsPrincipal.FindAll](/en-us/dotnet/api/system.security.claims.claimsprincipal.findall) with a predicate to filter the user's claims.

```
@page "/claims-principal-data"
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>ClaimsPrincipal Data</h1>

<button @onclick="GetClaimsPrincipalData">Get ClaimsPrincipal Data</button>

<p>@authMessage</p>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}

<p>@surname</p>

@code {
    private string? authMessage;
    private string? surname;
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    private async Task GetClaimsPrincipalData()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            authMessage = $"{user.Identity.Name} is authenticated.";
            claims = user.Claims;
            surname = user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value;
        }
        else
        {
            authMessage = "The user is NOT authenticated.";
        }
    }
}
```

If `user.Identity.IsAuthenticated` is `true` and because the user is a [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal), claims can be enumerated and membership in roles evaluated.

For more information on dependency injection (DI) and services, see [ASP.NET Core Blazor dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) and [Dependency injection in ASP.NET Core](../../fundamentals/dependency-injection?view=aspnetcore-10.0). For information on how to implement a custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider), see [ASP.NET Core Blazor authentication state](authentication-state?view=aspnetcore-10.0#implement-a-custom-authenticationstateprovider).

## Expose the authentication state as a cascading parameter

If authentication state data is required for procedural logic, such as when performing an action triggered by the user, obtain the authentication state data by defining a [cascading parameter](../components/cascading-values-and-parameters?view=aspnetcore-10.0) of type `Task<`[AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate)`>`, as the following example demonstrates.

`CascadeAuthState.razor`:

```
@page "/cascade-auth-state"

<h1>Cascade Auth State</h1>

<p>@authMessage</p>

@code {
    private string authMessage = "The user is NOT authenticated.";

    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user?.Identity is not null && user.Identity.IsAuthenticated)
            {
                authMessage = $"{user.Identity.Name} is authenticated.";
            }
        }
    }
}
```

```
@page "/cascade-auth-state"

<h1>Cascade Auth State</h1>

<p>@authMessage</p>

@code {
    private string authMessage = "The user is NOT authenticated.";

    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user?.Identity is not null && user.Identity.IsAuthenticated)
            {
                authMessage = $"{user.Identity.Name} is authenticated.";
            }
        }
    }
}
```

If `user.Identity.IsAuthenticated` is `true`, claims can be enumerated and membership in roles evaluated.

Set up the `Task<`[AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate)`>` [cascading parameter](../components/cascading-values-and-parameters?view=aspnetcore-10.0) using the [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) and cascading authentication state services.

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app includes the [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) and the call to [AddCascadingAuthenticationState](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cascadingauthenticationstateservicecollectionextensions.addcascadingauthenticationstate) shown in the following example. A client-side Blazor app includes the required service registrations as well. Additional information is presented in the [Customize unauthorized content with the `Router` component](#customize-unauthorized-content-with-the-router-component) section.

```
<Router ...>
    <Found ...>
        <AuthorizeRouteView RouteData="routeData" 
            DefaultLayout="typeof(Layout.MainLayout)" />
        ...
    </Found>
</Router>
```

In the `Program` file, register cascading authentication state services:

```
builder.Services.AddCascadingAuthenticationState();
```

Set up the `Task<`[AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate)`>` [cascading parameter](../components/cascading-values-and-parameters?view=aspnetcore-10.0) using the [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) and [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) components.

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app includes the [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) and [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) components shown in the following example. A client-side Blazor app includes the required service registrations as well. Additional information is presented in the [Customize unauthorized content with the `Router` component](#customize-unauthorized-content-with-the-router-component) section.

```
<CascadingAuthenticationState>
    <Router ...>
        <Found ...>
            <AuthorizeRouteView RouteData="routeData" 
                DefaultLayout="typeof(MainLayout)" />
            ...
        </Found>
    </Router>
</CascadingAuthenticationState>
```

Note

With the release of .NET 5.0.1 and for any additional 5.x releases, the `Router` component includes the `PreferExactMatches` parameter set to `@true`. For more information, see [Migrate from ASP.NET Core 3.1 to .NET 5](../../migration/31-to-50?view=aspnetcore-10.0#changes-to-blazor-app-routing-logic-in-501-and-further-5x-releases-up-to-60).

In a client-side Blazor app, add authorization services to the `Program` file:

```
builder.Services.AddAuthorizationCore();
```

In a client-side Blazor app, add options and authorization services to the `Program` file:

```
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
```

In a server-side Blazor app, services for options and authorization are already present, so no further steps are required.

## Authorization

After a user is authenticated, *authorization* rules are applied to control what the user can do.

Access is typically granted or denied based on whether:

* A user is authenticated (signed in).
* A user is in a *role*.
* A user has a *claim*.
* A *policy* is satisfied.

Each of these concepts is the same as in an ASP.NET Core MVC or Razor Pages app. For more information on ASP.NET Core security, see the articles under [ASP.NET Core Security and Identity](../../security/?view=aspnetcore-10.0).

## `AuthorizeView` component

The [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component selectively displays UI content depending on whether the user is authorized. This approach is useful when you only need to *display* data for the user and don't need to use the user's identity in procedural logic.

The component exposes a `context` variable of type [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) (`@context` in Razor syntax), which you can use to access information about the signed-in user:

```
<AuthorizeView>
    <p>Hello, @context.User.Identity?.Name!</p>
</AuthorizeView>
```

You can also supply different content for display if the user isn't authorized with a combination of the [Authorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.authorized) and [NotAuthorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.notauthorized) parameters:

```
<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
        <p><button @onclick="HandleClick">Authorized Only Button</button></p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    private void HandleClick() { ... }
}
```

Although the [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component controls the visibility of elements based on the user's authorization status, it doesn't enforce security on the event handler itself. In the preceding example, the `HandleClick` method is only associated with a button visible to authorized users, but nothing prevents invoking this method from other places. To ensure method-level security, implement additional authorization logic within the handler itself or in the relevant API.

Razor components of Blazor Web Apps never display `<NotAuthorized>` content when authorization fails server-side during static server-side rendering (static SSR). The server-side ASP.NET Core pipeline processes authorization on the server. Use server-side techniques, such as configuring [LoginPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions.loginpath) to handle unauthorized requests. For more information, see [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0#static-server-side-rendering-static-ssr).

Warning

Client-side markup and methods associated with an [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) are only protected from view and execution in the ***rendered UI*** in client-side Blazor apps. In order to protect authorized content and secure methods in client-side Blazor, the content is usually supplied by a secure, authorized web API call to a server API and never stored in the app. For more information, see [Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0) and [ASP.NET Core Blazor WebAssembly additional security scenarios](webassembly/additional-scenarios?view=aspnetcore-10.0).

The content of [Authorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.authorized) and [NotAuthorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.notauthorized) can include arbitrary items, such as other interactive components.

Authorization conditions, such as roles or policies that control UI options or access, are covered in the [Authorization](#authorization) section.

If authorization conditions aren't specified, [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) uses a default policy:

* Authenticated (signed-in) users are authorized.
* Unauthenticated (signed-out) users are unauthorized.

The [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component can be used in the `NavMenu` component (`Shared/NavMenu.razor`) to display a [`NavLink` component](../fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), but note that this approach only removes the list item from the rendered output. It doesn't prevent the user from navigating to the component. Implement authorization separately in the destination component.

### Role-based and policy-based authorization

The [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component supports *role-based* or *policy-based* authorization.

For role-based authorization, use the [AuthorizeView.Roles](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.roles#microsoft-aspnetcore-components-authorization-authorizeview-roles) parameter. In the following example, the user must have a role claim for either the `Admin` or `Superuser` roles:

```
<AuthorizeView Roles="Admin, Superuser">
    <p>You have an 'Admin' or 'Superuser' role claim.</p>
</AuthorizeView>
```

To require both `Admin` and `Superuser` role claims, nest [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) components:

```
<AuthorizeView Roles="Admin">
    <p>User: @context.User</p>
    <p>You have the 'Admin' role claim.</p>
    <AuthorizeView Roles="Superuser" Context="innerContext">
        <p>User: @innerContext.User</p>
        <p>You have both 'Admin' and 'Superuser' role claims.</p>
    </AuthorizeView>
</AuthorizeView>
```

The preceding code establishes a `Context` for the inner [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component to prevent an [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) context collision. The [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) context is accessed in the outer [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) with the standard approach for accessing the context (`@context.User`). The context is accessed in the inner [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) with the named `innerContext` context (`@innerContext.User`).

For more information, including configuration guidance, see [Role-based authorization in ASP.NET Core](../../security/authorization/roles?view=aspnetcore-10.0).

For policy-based authorization, use the [AuthorizeView.Policy](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.policy#microsoft-aspnetcore-components-authorization-authorizeview-policy) parameter with a single policy name:

```
<AuthorizeView Policy="Over21">
    <p>You satisfy the 'Over21' policy.</p>
</AuthorizeView>
```

To handle the case where the user should satisfy one of several policies, create a policy that confirms that the user satisfies other policies.

To handle the case where the user must satisfy several policies simultaneously, take *either* of the following approaches:

* Create a policy for [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) that confirms that the user satisfies several other policies.

* Nest the policies in multiple [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) components:

  ```
  <AuthorizeView Policy="Over21">
      <AuthorizeView Policy="LivesInCalifornia">
          <p>You satisfy the 'Over21' and 'LivesInCalifornia' policies.</p>
      </AuthorizeView>
  </AuthorizeView>
  ```

Claim-based authorization is a special case of policy-based authorization. For example, you can define a policy that requires users to have a certain claim. For more information, see [Policy-based authorization in ASP.NET Core](../../security/authorization/policies?view=aspnetcore-10.0).

If both [Roles](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.roles#microsoft-aspnetcore-components-authorization-authorizeview-roles) and [Policy](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.policy#microsoft-aspnetcore-components-authorization-authorizeview-policy) are set, authorization succeeds only when both conditions are satisfied. That is, the user must belong to at least one of the specified roles *and* meet the requirements defined by the policy.

If neither [Roles](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.roles#microsoft-aspnetcore-components-authorization-authorizeview-roles) nor [Policy](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.policy#microsoft-aspnetcore-components-authorization-authorizeview-policy) is specified, [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) uses the default policy:

* Authenticated (signed-in) users are authorized.
* Unauthenticated (signed-out) users are unauthorized.

Role matching is typically case-sensitive because role names are stored and compared using .NET string comparisons. For example, `Admin` (uppercase `A`) isn't treated as the same role as `admin` (lowercase `a`). For more information, see [Claim-based authorization in ASP.NET Core](../../security/authorization/claims?view=aspnetcore-10.0#claim-case-sensitivity). By contrast, ASP.NET Core policy name lookup is typically case-insensitive, so `RequireAdministratorRole` and `requireadministratorrole` refer to the same policy.

### Content displayed during asynchronous authentication

Blazor allows for authentication state to be determined *asynchronously*. The primary scenario for this approach is in client-side Blazor apps that make a request to an external endpoint for authentication.

While authentication is in progress, [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) displays no content. To display content while authentication occurs, assign content to the [Authorizing](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.authorizing) parameter:

```
<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <Authorizing>
        <p>You can only see this content while authentication is in progress.</p>
    </Authorizing>
</AuthorizeView>
```

This approach isn't normally applicable to server-side Blazor apps. Server-side Blazor apps know the authentication state as soon as the state is established. [Authorizing](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.authorizing#microsoft-aspnetcore-components-authorization-authorizeviewcore-authorizing) content can be provided in an app's [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component, but the content is never displayed.

## `[Authorize]` attribute

The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) is available in Razor components:

```
@page "/"
@attribute [Authorize]

You can only see this if you're signed in.
```

Important

Only use [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) on `@page` components reached via the Blazor router. Authorization is only performed as an aspect of routing and *not* for child components rendered within a page. To authorize the display of specific parts within a page, use [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) instead.

The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) also supports role-based or policy-based authorization. For role-based authorization, use the [AuthorizeAttribute.Roles](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles#microsoft-aspnetcore-authorization-authorizeattribute-roles) parameter:

```
@page "/"
@attribute [Authorize(Roles = "Admin, Superuser")]

<p>You can only see this if you're in the 'Admin' or 'Superuser' role.</p>
```

For policy-based authorization, use the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) parameter:

```
@page "/"
@attribute [Authorize(Policy = "Over21")]

<p>You can only see this if you satisfy the 'Over21' policy.</p>
```

If neither [Roles](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles#microsoft-aspnetcore-authorization-authorizeattribute-roles) nor [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) is specified, [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) uses the default policy:

* Authenticated (signed-in) users are authorized.
* Unauthenticated (signed-out) users are unauthorized.

When the user isn't authorized and if the app doesn't [customize unauthorized content with the `Router` component](#customize-unauthorized-content-with-the-router-component), the framework automatically displays the following fallback message:

```
Not authorized.
```

## Resource authorization

To authorize users for resources, pass the request's route data to the [Resource](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview.resource#microsoft-aspnetcore-components-authorization-authorizerouteview-resource) parameter of [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview).

In the [Router.Found](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.found#microsoft-aspnetcore-components-routing-router-found) content for a requested route:

```
<AuthorizeRouteView Resource="routeData" RouteData="routeData" 
    DefaultLayout="typeof(MainLayout)" />
```

For more information on how authorization state data is passed and used in procedural logic, see the [Expose the authentication state as a cascading parameter](#expose-the-authentication-state-as-a-cascading-parameter) section.

When the [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) receives the route data for the resource, authorization policies have access to [RouteData.PageType](/en-us/dotnet/api/microsoft.aspnetcore.components.routedata.pagetype#microsoft-aspnetcore-components-routedata-pagetype) and [RouteData.RouteValues](/en-us/dotnet/api/microsoft.aspnetcore.components.routedata.routevalues#microsoft-aspnetcore-components-routedata-routevalues) that permit custom logic to make authorization decisions.

In the following example, an `EditUser` policy is created in [AuthorizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions) for the app's authorization service configuration ([AddAuthorizationCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authorizationservicecollectionextensions.addauthorizationcore)) with the following logic:

* Determine if a route value exists with a key of `id`. If the key exists, the route value is stored in `value`.
* In a variable named `id`, store `value` as a string or set an empty string value (`string.Empty`).
* If `id` isn't an empty string, assert that the policy is satisfied (return `true`) if the string's value starts with `EMP`. Otherwise, assert that the policy fails (return `false`).

In the `Program` file:

* Add namespaces for [Microsoft.AspNetCore.Components](/en-us/dotnet/api/microsoft.aspnetcore.components) and [System.Linq](/en-us/dotnet/api/system.linq):

  ```
  using Microsoft.AspNetCore.Components;
  using System.Linq;
  ```

* Add the policy:

  ```
  options.AddPolicy("EditUser", policy =>
      policy.RequireAssertion(context =>
      {
          if (context.Resource is RouteData rd)
          {
              var routeValue = rd.RouteValues.TryGetValue("id", out var value);
              var id = Convert.ToString(value, 
                  System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty;

              if (!string.IsNullOrEmpty(id))
              {
                  return id.StartsWith("EMP", StringComparison.InvariantCulture);
              }
          }

          return false;
      })
  );
  ```

The preceding example is an oversimplified authorization policy, merely used to demonstrate the concept with a working example. For more information on creating and configuring authorization policies, see [Policy-based authorization in ASP.NET Core](../../security/authorization/policies?view=aspnetcore-10.0).

In the following `EditUser` component, the resource at `/users/{id}/edit` has a route parameter for the user's identifier (`{id}`). The component uses the preceding `EditUser` authorization policy to determine if the route value for `id` starts with `EMP`. If `id` starts with `EMP`, the policy succeeds and access to the component is authorized. If `id` starts with a value other than `EMP` or if `id` is an empty string, the policy fails, and the component doesn't load.

`EditUser.razor`:

```
@page "/users/{id}/edit"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "EditUser")]

<h1>Edit User</h1>

<p>The "EditUser" policy is satisfied! <code>Id</code> starts with 'EMP'.</p>

@code {
    [Parameter]
    public string? Id { get; set; }
}
```

```
@page "/users/{id}/edit"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "EditUser")]

<h1>Edit User</h1>

<p>The "EditUser" policy is satisfied! <code>Id</code> starts with 'EMP'.</p>

@code {
    [Parameter]
    public string? Id { get; set; }
}
```

## Customize unauthorized content with the `Router` component

The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component, in conjunction with the [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) component, allows the app to specify custom content if:

* The user fails an [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) condition applied to the component. The markup of the [`<NotAuthorized>`](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview.notauthorized#microsoft-aspnetcore-components-authorization-authorizerouteview-notauthorized) element is displayed. The [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute is covered in the [`[Authorize]` attribute](#authorize-attribute) section.
* Asynchronous authorization is in progress, which usually means that the process of authenticating the user is in progress. The markup of the [`<Authorizing>`](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview.authorizing#microsoft-aspnetcore-components-authorization-authorizerouteview-authorizing) element is displayed.

Important

Blazor router features that display `<NotAuthorized>` and `<NotFound>` content aren't operational during static server-side rendering (static SSR) because request processing is entirely handled by ASP.NET Core middleware pipeline request processing and Razor components aren't rendered at all for unauthorized or bad requests. Use server-side techniques to handle unauthorized and bad requests during static SSR. For more information, see [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0#static-server-side-rendering-static-ssr).

```
<Router ...>
    <Found ...>
        <AuthorizeRouteView ...>
            <NotAuthorized>
                ...
            </NotAuthorized>
            <Authorizing>
                ...
            </Authorizing>
        </AuthorizeRouteView>
    </Found>
</Router>
```

The content of [Authorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.authorized) and [NotAuthorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.notauthorized) can include arbitrary items, such as other interactive components.

Note

The preceding requires cascading authentication state services registration in the app's `Program` file:

```
builder.Services.AddCascadingAuthenticationState();
```

```
<CascadingAuthenticationState>
    <Router ...>
        <Found ...>
            <AuthorizeRouteView ...>
                <NotAuthorized>
                    ...
                </NotAuthorized>
                <Authorizing>
                    ...
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
    </Router>
</CascadingAuthenticationState>
```

The content of [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfound), [Authorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.authorized), and [NotAuthorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.notauthorized) can include arbitrary items, such as other interactive components.

If [NotAuthorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeviewcore.notauthorized) content isn't specified, the [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) uses the following fallback message:

```
Not authorized.
```

An app created from the Blazor WebAssembly project template with authentication enabled includes a `RedirectToLogin` component, which is positioned in the `<NotAuthorized>` content of the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. When a user isn't authenticated (`context.User.Identity?.IsAuthenticated != true`), the `RedirectToLogin` component redirects the browser to the `authentication/login` endpoint for authentication. The user is returned to the requested URL after authenticating with the identity provider.

## Procedural logic

If the app is required to check authorization rules as part of procedural logic, use a cascaded parameter of type `Task<`[AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate)`>` to obtain the user's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal). `Task<`[AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate)`>` can be combined with other services, such as `IAuthorizationService`, to evaluate policies.

In the following example:

* The `user.Identity.IsAuthenticated` executes code for authenticated (signed-in) users.
* The `user.IsInRole("admin")` executes code for users in the 'Admin' role.
* The `(await AuthorizationService.AuthorizeAsync(user, "content-editor")).Succeeded` executes code for users satisfying the 'content-editor' policy.

A server-side Blazor app includes the appropriate namespaces when created from the project template. In a client-side Blazor app, confirm the presence of the [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization) and [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespaces either in the component or in the app's imports file (`_Imports.razor`):

```
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
```

`ProceduralLogic.razor`:

```
@page "/procedural-logic"
@inject IAuthorizationService AuthorizationService

<h1>Procedural Logic Example</h1>

<button @onclick="@DoSomething">Do something important</button>

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    private async Task DoSomething()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user is not null)
            {
                if (user.Identity is not null && user.Identity.IsAuthenticated)
                {
                    // ...
                }

                if (user.IsInRole("Admin"))
                {
                    // ...
                }

                if ((await AuthorizationService.AuthorizeAsync(user, "content-editor"))
                    .Succeeded)
                {
                    // ...
                }
            }
        }
    }
}
```

```
@page "/procedural-logic"
@inject IAuthorizationService AuthorizationService

<h1>Procedural Logic Example</h1>

<button @onclick="@DoSomething">Do something important</button>

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    private async Task DoSomething()
    {
        if (authenticationState is not null)
        {
            var authState = await authenticationState;
            var user = authState?.User;

            if (user is not null)
            {
                if (user.Identity is not null && user.Identity.IsAuthenticated)
                {
                    // ...
                }

                if (user.IsInRole("Admin"))
                {
                    // ...
                }

                if ((await AuthorizationService.AuthorizeAsync(user, "content-editor"))
                    .Succeeded)
                {
                    // ...
                }
            }
        }
    }
}
```

## Troubleshoot errors

Common errors:

* **Authorization requires a cascading parameter of type `Task<AuthenticationState>`. Consider using `CascadingAuthenticationState` to supply this.**

* **`null` value is received for `authenticationStateTask`**

It's likely that the project wasn't created using a server-side Blazor template with authentication enabled.

In .NET 7 or earlier, wrap a `<CascadingAuthenticationState>` around some part of the UI tree, for example around the Blazor router:

```
<CascadingAuthenticationState>
    <Router ...>
        ...
    </Router>
</CascadingAuthenticationState>
```

In .NET 8 or later, don't use the [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) component:

```
- <CascadingAuthenticationState>
      <Router ...>
          ...
      </Router>
- </CascadingAuthenticationState>
```

Instead, add cascading authentication state services to the service collection in the `Program` file:

```
builder.Services.AddCascadingAuthenticationState();
```

The [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) component (.NET 7 or earlier) or services provided by [AddCascadingAuthenticationState](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cascadingauthenticationstateservicecollectionextensions.addcascadingauthenticationstate) (.NET 8 or later) supplies the `Task<`[AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate)`>` cascading parameter, which in turn it receives from the underlying [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) dependency injection service.

## Personally Identifiable Information (PII)

Microsoft uses the [GDPR definition for 'personal data' (GDPR 4.1)](https://gdpr-text.com/read/article-4/) when documentation discusses Personally Identifiable Information (PII).

PII refers any information relating to an identified or identifiable natural person. An identifiable natural person is one who can be identified, directly or indirectly, with any of the following:

* Name

* Identification number

* Location coordinates

* Online identifier

* Other specific factors

  * Physical
  * Physiological
  * Genetic
  * Mental (psychological)
  * Economic
  * Cultural
  * Social identity

## Related content

* Server-side and Blazor Web App resources

  * [Authorization patterns](additional-scenarios?view=aspnetcore-10.0#server-side-blazor-app-authorization-patterns)

  * [Quickstart: Add sign-in with Microsoft to an ASP.NET Core web app](/en-us/entra/identity-platform/quickstart-v2-aspnet-core-webapp)

  * [Quickstart: Protect an ASP.NET Core web API with Microsoft identity platform](/en-us/entra/identity-platform/quickstart-v2-aspnet-core-web-api)

  * [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0): Includes guidance on:

    * Using forwarded headers middleware to preserve HTTPS scheme information across proxy servers and internal networks.
    * Additional scenarios and use cases, including manual scheme configuration, request path changes for correct request routing, and forwarding the request scheme for Linux and non-IIS reverse proxies.

* Microsoft identity platform documentation

  * [Overview](/en-us/entra/identity-platform/)
  * [OAuth 2.0 and OpenID Connect protocols on the Microsoft identity platform](/en-us/entra/identity-platform/v2-protocols)
  * [Microsoft identity platform and OAuth 2.0 authorization code flow](/en-us/entra/identity-platform/v2-oauth2-auth-code-flow)
  * [Microsoft identity platform ID tokens](/en-us/entra/identity-platform/id-tokens)
  * [Microsoft identity platform access tokens](/en-us/entra/identity-platform/access-tokens)

* [ASP.NET Core security topics](../../security/?view=aspnetcore-10.0)

* [Configure Windows Authentication in ASP.NET Core](../../security/authentication/windowsauth?view=aspnetcore-10.0)

* [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../components/httpcontext?view=aspnetcore-10.0)

* [Build a custom version of the Authentication.MSAL JavaScript library](webassembly/additional-scenarios?view=aspnetcore-10.0#build-a-custom-version-of-the-authenticationmsal-javascript-library)

* [Awesome Blazor: Authentication](https://github.com/AdrienTorris/awesome-blazor#authentication) community sample links

* [ASP.NET Core Blazor Hybrid authentication and authorization](../hybrid/security/?view=aspnetcore-10.0)

* [Opaque (reference) access token support](additional-scenarios?view=aspnetcore-10.0#opaque-reference-access-token-support)

* [Blazor WebAssembly authorization patterns](webassembly/?view=aspnetcore-10.0#blazor-webassembly-authorization-patterns)

- Server-side Blazor resources

  * [Authorization patterns](additional-scenarios?view=aspnetcore-10.0#server-side-blazor-app-authorization-patterns)

  * [Quickstart: Add sign-in with Microsoft to an ASP.NET Core web app](/en-us/entra/identity-platform/quickstart-v2-aspnet-core-webapp)

  * [Quickstart: Protect an ASP.NET Core web API with Microsoft identity platform](/en-us/entra/identity-platform/quickstart-v2-aspnet-core-web-api)

  * [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0): Includes guidance on:

    * Using forwarded headers middleware to preserve HTTPS scheme information across proxy servers and internal networks.
    * Additional scenarios and use cases, including manual scheme configuration, request path changes for correct request routing, and forwarding the request scheme for Linux and non-IIS reverse proxies.

- Microsoft identity platform documentation

  * [Overview](/en-us/entra/identity-platform/)
  * [OAuth 2.0 and OpenID Connect protocols on the Microsoft identity platform](/en-us/entra/identity-platform/v2-protocols)
  * [Microsoft identity platform and OAuth 2.0 authorization code flow](/en-us/entra/identity-platform/v2-oauth2-auth-code-flow)
  * [Microsoft identity platform ID tokens](/en-us/entra/identity-platform/id-tokens)
  * [Microsoft identity platform access tokens](/en-us/entra/identity-platform/access-tokens)

- [ASP.NET Core security topics](../../security/?view=aspnetcore-10.0)

- [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../components/httpcontext?view=aspnetcore-10.0)

- [Configure Windows Authentication in ASP.NET Core](../../security/authentication/windowsauth?view=aspnetcore-10.0)

- [Build a custom version of the Authentication.MSAL JavaScript library](webassembly/additional-scenarios?view=aspnetcore-10.0#build-a-custom-version-of-the-authenticationmsal-javascript-library)

- [Awesome Blazor: Authentication](https://github.com/AdrienTorris/awesome-blazor#authentication) community sample links

- [Opaque (reference) access token support](additional-scenarios?view=aspnetcore-10.0#opaque-reference-access-token-support)

- [Blazor WebAssembly authorization patterns](webassembly/?view=aspnetcore-10.0#blazor-webassembly-authorization-patterns)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/debug
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/debug.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Debug ASP.NET Core apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes how to debug Blazor apps, including debugging Blazor WebAssembly apps with browser developer tools or an integrated development environment (IDE).

Blazor Web Apps can be debugged in Visual Studio or Visual Studio Code.

Blazor WebAssembly apps can be debugged:

* In Visual Studio or Visual Studio Code.
* Using browser developer tools in Chromium-based browsers, including Microsoft Edge, Google Chrome, and Firefox.

Available scenarios for Blazor WebAssembly debugging include:

* Set and remove breakpoints.
* Run the app with debugging support in IDEs.
* Single-step through the code.
* Resume code execution with a keyboard shortcut in IDEs.
* In the *Locals* window, observe the values of local variables.
* See the call stack, including call chains between JavaScript and .NET.
* Use a [symbol server](../test/debug-aspnetcore-source?view=aspnetcore-10.0) for debugging, configured by Visual Studio preferences.

Unsupported scenarios include:

* Debug in non-local scenarios (for example, [Windows Subsystem for Linux (WSL)](/en-us/windows/wsl/) or [Visual Studio Codespaces](https://visualstudio.microsoft.com/services/github-codespaces/)).
* Debug in Firefox from Visual Studio or Visual Studio Code.

Blazor Server apps can be debugged in Visual Studio or Visual Studio Code.

Blazor WebAssembly apps can be debugged:

* In Visual Studio or Visual Studio Code.
* Using browser developer tools in Chromium-based browsers, including Microsoft Edge and Google Chrome.

Unsupported scenarios for Blazor WebAssembly apps include:

* Set and remove breakpoints.
* Run the app with debugging support in IDEs.
* Single-step through the code.
* Resume code execution with a keyboard shortcut in IDEs.
* In the *Locals* window, observe the values of local variables.
* See the call stack, including call chains between JavaScript and .NET.
* Debug in non-local scenarios (for example, [Windows Subsystem for Linux (WSL)](/en-us/windows/wsl/) or [Visual Studio Codespaces](https://visualstudio.microsoft.com/services/github-codespaces/)).
* Use a [symbol server](../test/debug-aspnetcore-source?view=aspnetcore-10.0) for debugging.

Blazor Server apps can be debugged in Visual Studio or Visual Studio Code.

Blazor WebAssembly apps can be debugged:

* In Visual Studio or Visual Studio Code.
* Using browser developer tools in Chromium-based browsers, including Microsoft Edge and Google Chrome.

Unsupported scenarios for Blazor WebAssembly apps include:

* Set and remove breakpoints.
* Run the app with debugging support in IDEs.
* Single-step through the code.
* Resume code execution with a keyboard shortcut in IDEs.
* In the *Locals* window, observe the values of local variables.
* See the call stack, including call chains between JavaScript and .NET.
* Hit breakpoints during app startup before the debug proxy is running. This includes breakpoints in the `Program` file and breakpoints in the [`OnInitialized{Async}` lifecycle methods](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) of components that are loaded by the first page requested from the app.
* Debug in non-local scenarios (for example, [Windows Subsystem for Linux (WSL)](/en-us/windows/wsl/) or [Visual Studio Codespaces](https://visualstudio.microsoft.com/services/github-codespaces/)).
* Use a [symbol server](../test/debug-aspnetcore-source?view=aspnetcore-10.0) for debugging.

## Blazor WebAssembly Gateway

[`Microsoft.AspNetCore.Components.Gateway`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Gateway) is a lightweight ASP.NET Core host for serving standalone Blazor WebAssembly apps during development and production.

The Gateway is a full ASP.NET Core host, not merely a static-file dev tool, so standalone Blazor WebAssembly apps feature:

* Built-in SPA fallback routing: Requests that don't match a static asset fall back to `index.html`, so client-side routes such as `/orders/42` work on browser refresh and direct navigation without a custom MSBuild target.
* Multiple Blazor WebAssembly clients per host: A single Gateway instance can serve more than one Blazor WebAssembly client under different path prefixes, configured through its `ClientApps` section. This is the integration point .NET Aspire uses to host Blazor WebAssembly clients alongside backend services in a single AppHost run.
* Built-in YARP reverse-proxy infrastructure: YARP is bundled with the Gateway, providing the foundation for forwarding backend traffic alongside the WebAssembly client and enabling the Aspire multi-client scenarios.

To adopt the Gateway in an existing standalone Blazor WebAssembly app that targets .NET 11 or later, reference the `Microsoft.AspNetCore.Components.Gateway` package in the app's project file.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Custom routing code and middleware aren't required by the app. Fallback endpoints come from the static web assets manifest the SDK emits when the `StaticWebAssetSpaFallbackEnabled` property is set in the app's project file, which is present by default in standalone Blazor WebAssembly apps created from the project template:

```
<StaticWebAssetSpaFallbackEnabled>true</StaticWebAssetSpaFallbackEnabled>
```

## Prerequisites

This section explains the prerequisites for debugging.

### Browser prerequisites

The latest version of the following browsers:

* Google Chrome
* Microsoft Edge
* Firefox (browser developer tools only)

Debugging requires the latest version of the following browsers:

* Google Chrome (default)
* Microsoft Edge

Ensure that firewalls or proxies don't block communication with the debug proxy (`NodeJS` process). For more information, see the [Firewall configuration](#firewall-configuration) section.

Note

Apple Safari on macOS isn't currently supported.

### IDE prerequisites

The latest version of Visual Studio or Visual Studio Code is required.

### Visual Studio Code prerequisites

Visual Studio Code requires the [C# Dev Kit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) ([Getting Started with C# in VS Code](https://code.visualstudio.com/docs/csharp/get-started)). In the Visual Studio Code Extensions Marketplace, filter the extension list with "`c# dev kit`" to locate the extension:

Installing the C# Dev Kit automatically installs the following additional extensions:

* [.NET Install Tool](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.vscode-dotnet-runtime)
* [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [IntelliCode for C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.vscodeintellicode-csharp)

If you encounter warnings or errors, you can [open an issue (`microsoft/vscode-dotnettools` GitHub repository)](https://github.com/microsoft/vscode-dotnettools/issues) describing the problem.

### App configuration prerequisites

*The guidance in this subsection applies to client-side debugging.*

Open the `Properties/launchSettings.json` file of the startup project. Confirm the presence of the following `inspectUri` property in each launch profile of the file's `profiles` node. If the following property isn't present, add it to each profile:

```
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
```

The `inspectUri` property:

* Enables the IDE to detect that the app is a Blazor app.
* Instructs the script debugging infrastructure to connect to the browser through Blazor's debugging proxy.

The placeholder values for the WebSocket protocol (`wsProtocol`), host (`url.hostname`), port (`url.port`), and inspector URI on the launched browser (`browserInspectUri`) are provided by the framework.

Note

When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

## Packages

Blazor Web Apps: [`Microsoft.AspNetCore.Components.WebAssembly.Server`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Server): References an internal package ([`Microsoft.NETCore.BrowserDebugHost.Transport`](https://github.com/dotnet/runtime/blob/main/src/mono/nuget/Microsoft.NETCore.BrowserDebugHost.Transport/Microsoft.NETCore.BrowserDebugHost.Transport.pkgproj)) for assemblies that share the browser debug host.

Blazor Server: [`Microsoft.AspNetCore.Components.WebAssembly.Server`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Server): References an internal package ([`Microsoft.NETCore.BrowserDebugHost.Transport`](https://github.com/dotnet/runtime/blob/main/src/mono/nuget/Microsoft.NETCore.BrowserDebugHost.Transport/Microsoft.NETCore.BrowserDebugHost.Transport.pkgproj)) for assemblies that share the browser debug host.

Standalone Blazor WebAssembly: [`Microsoft.AspNetCore.Components.Gateway`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Gateway): A lightweight ASP.NET Core host for serving standalone Blazor WebAssembly apps during development and production.

Standalone Blazor WebAssembly: [`Microsoft.AspNetCore.Components.WebAssembly.DevServer`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.DevServer): Development server for use when building Blazor WebAssembly apps. Calls [UseWebAssemblyDebugging](/en-us/dotnet/api/microsoft.aspnetcore.builder.webassemblynetdebugproxyappbuilderextensions.usewebassemblydebugging) internally to add middleware for debugging Blazor WebAssembly apps inside Chromium developer tools.

Hosted Blazor WebAssembly:

* **Client** project: [`Microsoft.AspNetCore.Components.WebAssembly.DevServer`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.DevServer): Development server for use when building Blazor apps. Calls [UseWebAssemblyDebugging](/en-us/dotnet/api/microsoft.aspnetcore.builder.webassemblynetdebugproxyappbuilderextensions.usewebassemblydebugging) internally to add middleware for debugging Blazor WebAssembly apps inside Chromium developer tools.
* **Server** project: [`Microsoft.AspNetCore.Components.WebAssembly.Server`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Server): References an internal package ([`Microsoft.NETCore.BrowserDebugHost.Transport`](https://github.com/dotnet/runtime/blob/main/src/mono/nuget/Microsoft.NETCore.BrowserDebugHost.Transport/Microsoft.NETCore.BrowserDebugHost.Transport.pkgproj)) for assemblies that share the browser debug host.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

## Debug a Blazor Web App in an IDE

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

The example in this section assumes that you've created a Blazor Web App with an interactive render mode of Auto (Server and WebAssembly) and per-component interactivity location.

1. Open the app.
2. Set a breakpoint on the `currentCount++;` line in the `Counter` component (`Pages/Counter.razor`) of the client project (`.Client`).
3. With the server project selected in **Solution Explorer**, press `F5` to run the app in the debugger.
4. In the browser, navigate to `Counter` page at `/counter`. Wait a few seconds for the debug proxy to load and run. Select the **Click me** button to hit the breakpoint.
5. In Visual Studio, inspect the value of the `currentCount` field in the **Locals** window.
6. Press `F5` to continue execution.

Breakpoints can also be hit in the server project in statically-rendered and interactively-rendered server-side components.

1. Stop the debugger.
2. In the server app, open the statically-rendered `Weather` component (`Components/Pages/Weather.razor`) and set a breakpoint anywhere in the `OnInitializedAsync` method.
3. Press `F5` to run the app in the debugger.
4. In the browser, navigate to the `Weather` page at `/weather`. Wait a few seconds for the debug proxy to load and run. Application execution stops at the breakpoint.
5. Press `F5` to continue execution.

Breakpoints are **not** hit during app startup before the debug proxy is running. This includes breakpoints in the `Program` file and breakpoints in the [`OnInitialized{Async}` lifecycle methods](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) of components that are loaded by the first page requested from the app.

The example in this section assumes that you've created a Blazor Web App with an interactive Auto (Server and WebAssembly) render mode and per-component interactivity location.

1. Open the app in Visual Studio Code by opening the solution folder, which is the folder that contains the server and `.Client` project folders.
2. Set a breakpoint on the `currentCount++;` line in the `Counter` component (`Pages/Counter.razor`) of the client project (`.Client`).
3. Open the **Run and Debug** pane and select the **Run and Debug** button. Alternatively, press `F5` (**Start Debugging**). Select the `C#` debugger in the command palette at the top of the UI. Select the default profile **for the server project** (for example, `C#:BlazorSample [Default Configuration]`).
4. In the browser, navigate to `Counter` page at `/counter`. Wait a few seconds for the debug proxy to load and run. Select the **Click me** button to hit the breakpoint.
5. Select the **Continue** button in the UI or press `F5` (**Continue**) to continue execution.

Breakpoints can also be hit in the server project.

1. Stop debugging by selecting the **Stop** button or press `Shift`+`F5` on the keyboard.
2. In the server app, open the statically-rendered `Weather` component (`Components/Pages/Weather.razor`) and set a breakpoint anywhere in the `OnInitializedAsync` method.
3. Select the **Start Debugging** button in the UI or press `F5` (**Start Debugging**) to run the app in the debugger.
4. In the browser, navigate to the `Weather` page at `/weather`. Wait a few seconds for the debug proxy to load and run. Application execution stops at the breakpoint.
5. Select the **Continue** button in the UI or press `F5` (**Continue**) to continue execution.

Breakpoints are **not** hit during app startup before the debug proxy is running. This includes breakpoints in the `Program` file and breakpoints in the [`OnInitialized{Async}` lifecycle methods](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) of components that are loaded by the first page requested from the app.

## Debug a Blazor Server app in an IDE

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

1. Open the app.
2. Set a breakpoint on the `currentCount++;` line in the `Counter` component (`Pages/Counter.razor`).
3. Press `F5` to run the app in the debugger.
4. In the browser, navigate to `Counter` page at `/counter`. Wait a few seconds for the debug proxy to load and run. Select the **Click me** button to hit the breakpoint.
5. In Visual Studio, inspect the value of the `currentCount` field in the **Locals** window.
6. Press `F5` to continue execution.

Breakpoints are **not** hit during app startup before the debug proxy is running. This includes breakpoints in the `Program` file and breakpoints in the [`OnInitialized{Async}` lifecycle methods](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) of components that are loaded by the first page requested from the app.

1. Open the app's folder in Visual Studio Code.
2. Set a breakpoint on the `currentCount++;` line in the `Counter` component (`Pages/Counter.razor`).
3. Open the **Run and Debug** pane and select the **Run and Debug** button. Alternatively, press `F5` (**Start Debugging**). Select the `C#` debugger in the command palette at the top of the UI. Select the default profile (for example, `C#:BlazorSample [Default Configuration]`).
4. Press `F5` to run the app in the debugger.
5. In the browser, navigate to `Counter` page at `/counter`. Wait a few seconds for the debug proxy to load and run. Select the **Click me** button to hit the breakpoint.
6. Press `F5` to continue execution.

Breakpoints are **not** hit during app startup before the debug proxy is running. This includes breakpoints in the `Program` file and breakpoints in the [`OnInitialized{Async}` lifecycle methods](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) of components that are loaded by the first page requested from the app.

## Debug a Blazor WebAssembly app in an IDE

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

1. Open the app.
2. Set a breakpoint on the `currentCount++;` line in the `Counter` component (`Pages/Counter.razor`).
3. Press `F5` to run the app in the debugger.
4. In the browser, navigate to `Counter` page at `/counter`. Wait a few seconds for the debug proxy to load and run. Select the **Click me** button to hit the breakpoint.
5. In Visual Studio, inspect the value of the `currentCount` field in the **Locals** window.
6. Press `F5` to continue execution.

Breakpoints are **not** hit during app startup before the debug proxy is running. This includes breakpoints in the `Program` file and breakpoints in the [`OnInitialized{Async}` lifecycle methods](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) of components that are loaded by the first page requested from the app.

1. Open the app's folder in Visual Studio Code.
2. Set a breakpoint on the `currentCount++;` line in the `Counter` component (`Pages/Counter.razor`).
3. Open the **Run and Debug** pane and select the **Run and Debug** button. Alternatively, press `F5` (**Start Debugging**). Select the `C#` debugger in the command palette at the top of the UI. Select the default profile (for example, `C#:BlazorSample [Default Configuration]`).
4. Press `F5` to run the app in the debugger.
5. The standalone app is launched, and a debugging browser is opened.
6. In the browser, navigate to `Counter` page at `/counter`. Wait a few seconds for the debug proxy to load and run. Select the **Click me** button to hit the breakpoint.
7. Press `F5` to continue execution.

Breakpoints are **not** hit during app startup before the debug proxy is running. This includes breakpoints in the `Program` file and breakpoints in the [`OnInitialized{Async}` lifecycle methods](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) of components that are loaded by the first page requested from the app.

## Debug a hosted Blazor WebAssembly app in an IDE

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

1. With the **Server** project selected in **Solution Explorer**, press `F5` to run the app in the debugger.

   When debugging with a Chromium-based browser, such as Google Chrome or Microsoft Edge, a new browser window might open with a separate profile for the debugging session instead of opening a tab in an existing browser window with the user's profile. If debugging with the user's profile is a requirement, adopt **one** of the following approaches:

   * Close all open browser instances before pressing `F5` to start debugging.
   * Configure Visual Studio to launch the browser with the user's profile. For more information on this approach, see [Blazor WASM Debugging in VS launches Edge with a separate user data directory (dotnet/aspnetcore #20915)](https://github.com/dotnet/aspnetcore/issues/20915#issuecomment-614933322).

2. In the **Client** project, set a breakpoint on the `currentCount++;` line in the `Counter` component (`Pages/Counter.razor`).

3. In the browser, navigate to `Counter` page at `/counter`. Wait a few seconds for the debug proxy to load and run. Select the **Click me** button to hit the breakpoint.

4. In Visual Studio, inspect the value of the `currentCount` field in the **Locals** window.

5. Press `F5` to continue execution.

You can also debug server code in the **Server** project:

1. Set a breakpoint in the `Pages/FetchData.razor` page in [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync).
2. Set a breakpoint in the `WeatherForecastController` in the `Get` action method.
3. Browse to the `Fetch Data` page to hit the first breakpoint in the `FetchData` component just before it issues an HTTP request to the server.
4. Press `F5` to continue execution and then hit the breakpoint on the server in the `WeatherForecastController`.
5. Press `F5` again to let execution continue and see the weather forecast table rendered in the browser.

Breakpoints are **not** hit during app startup before the debug proxy is running. This includes breakpoints in the `Program` file and breakpoints in the [`OnInitialized{Async}` lifecycle methods](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) of components that are loaded by the first page requested from the app.

Note

Only browser debugging is supported.

You can't automatically rebuild the backend **Server** project of a hosted Blazor WebAssembly [solution](tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) during debugging, for example by running the app with [`dotnet watch`](../tutorials/dotnet-watch?view=aspnetcore-10.0).

Publishing an app disables debugger support. To debug a published, hosted Blazor WebAssembly app, configure debugger support (`DebuggerSupport`) and copy output symbols to the `publish` directory (`CopyOutputSymbolsToPublishDirectory`) in the **Client** project's project file:

```
<DebuggerSupport>true</DebuggerSupport>
<CopyOutputSymbolsToPublishDirectory>true</CopyOutputSymbolsToPublishDirectory>
```

Warning

Published, hosted Blazor WebAssembly apps should only enable debugging and copying output symbols when deploying published assets ***locally***. Don't deploy a published app into a production environment with the `DebuggerSupport` and `CopyOutputSymbolsToPublishDirectory` properties set to `true`.

## Attach to an existing Visual Studio Code debugging session

To attach to a running Blazor app, open the `.vscode/launch.json` file and replace the `{URL}` placeholder with the URL where the app is running:

```
{
  "name": "Attach and Debug",
  "type": "blazorwasm",
  "request": "attach",
  "url": "{URL}"
}
```

## Visual Studio Code launch options

The launch configuration options in the following table are supported for the `blazorwasm` debug type (`.vscode/launch.json`).

| Option    | Description                                                                                                                                            |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `browser` | The browser to launch for the debugging session. Set to `edge` or `chrome`. Defaults to `edge`.                                                        |
| `cwd`     | The working directory to launch the app under.                                                                                                         |
| `request` | Use `launch` to launch and attach a debugging session to a Blazor WebAssembly app or `attach` to attach a debugging session to an already-running app. |
| `timeout` | The number of milliseconds to wait for the debugging session to attach. Defaults to 30,000 milliseconds (30 seconds).                                  |
| `trace`   | Used to generate logs from the JS debugger. Set to `true` to generate logs.                                                                            |
| `url`     | The URL to open in the browser when debugging.                                                                                                         |
| `webRoot` | Specifies the absolute path of the web server. Should be set if an app is served from a sub-route.                                                     |

The additional options in the following table only apply to **hosted Blazor WebAssembly apps**.

| Option    | Description                                                                                                 |
| --------- | ----------------------------------------------------------------------------------------------------------- |
| `env`     | The environment variables to provide to the launched process. Only applicable if `hosted` is set to `true`. |
| `hosted`  | Must be set to `true` if launching and debugging a hosted Blazor WebAssembly app.                           |
| `program` | A reference to the executable to run the server of the hosted app. Must be set if `hosted` is `true`.       |

## Debug Blazor WebAssembly with Google Chrome or Microsoft Edge

*The guidance in this section applies debugging Blazor WebAssembly apps in:*

* ***Google Chrome*** *running on Windows or macOS.*
* ***Microsoft Edge*** *running on Windows.*

1. Run the app in a command shell with `dotnet watch` (or `dotnet run`).

2. Launch a browser and navigate to the app's URL.

3. Start remote debugging by pressing:

   * `Shift`+`Alt`+`d` on Windows.
   * `Shift`+`⌘`+`d` on macOS.

   The browser must be running with remote debugging enabled, which isn't the default. If remote debugging is disabled, an **Unable to find debuggable browser tab** error page is rendered with instructions for launching the browser with the debugging port open. Follow the instructions for your browser.

   After following the instructions to enable remote debugging, the app opens in a new browser window. Start remote debugging by pressing the HotKey combination in the new browser window:

   * `Shift`+`Alt`+`d` on Windows.
   * `Shift`+`⌘`+`d` on macOS.

   A new window developer tools browser tab opens showing a ghosted image of the app.

   Note

   If you followed the instructions to open a new browser tab with remote debugging enabled, you can close the original browser window, leaving the second window open with the first tab running the app and the second tab running the debugger.

4. After a moment, the **Sources** tab shows a list of the app's .NET assemblies and pages.

5. Open the `file://` node. In component code (`.razor` files) and C# code files (`.cs`), breakpoints that you set are hit when code executes in the app's browser tab (the initial tab opened after starting remote debugging). After a breakpoint is hit, single-step (`F10`) through the code or resume (`F8`) code execution normally in the debugging tab.

For Chromium-based browser debugging, Blazor provides a debugging proxy that implements the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) and augments the protocol with .NET-specific information. When debugging keyboard shortcut is pressed, Blazor points the Chrome DevTools at the proxy. The proxy connects to the browser window you're seeking to debug (hence the need to enable remote debugging).

## Debug a Blazor WebAssembly app with Firefox

*The guidance in this section applies debugging Blazor WebAssembly apps in Firefox running on Windows.*

Debugging a Blazor WebAssembly app with Firefox requires configuring the browser for remote debugging and connecting to the browser using the browser developer tools through the .NET WebAssembly debugging proxy.

Note

Debugging in Firefox from Visual Studio isn't supported at this time.

To debug a Blazor WebAssembly app in Firefox during development:

1. Configure Firefox:

   * Open `about:config` in a new browser tab. Read and dismiss the warning that appears.
   * Enable `devtools.debugger.remote-enabled` by setting its value to `True`.
   * Enable `devtools.chrome.enabled` by setting its value to `True`.
   * Disable `devtools.debugger.prompt-connection` by setting its value to `False`.

2. Close all Firefox instances.

3. Run the app in a command shell with `dotnet watch` (or `dotnet run`).

4. Relaunch the Firefox browser and navigate to the app.

5. Open `about:debugging` in a new browser tab. **Leave this tab open**.

6. Go back to the tab where the app is running. Start remote debugging by pressing `Shift`+`Alt`+`d`.

7. In the `Debugger` tab, open the app source file you wish to debug under the `file://` node and set a breakpoint. For example, set a breakpoint on the `currentCount++;` line in the `IncrementCount` method of the `Counter` component (`Pages/Counter.razor`).

8. Navigate to the `Counter` component page (`/counter`) in the app's browser tab and select the counter button to hit the breakpoint.

9. Press `F5` to continue execution in the debugging tab.

## Break on unhandled exceptions

The debugger doesn't break on unhandled exceptions because Blazor catches exceptions that are unhandled by developer code.

To break on unhandled exceptions:

* Open the debugger's exception settings (**Debug** > **Windows** > **Exception Settings**) in Visual Studio.

* Set the following **JavaScript Exceptions** settings:

  * **All Exceptions**
  * **Uncaught Exceptions**

## Browser source maps

Browser source maps allow the browser to map compiled files back to their original source files and are commonly used for client-side debugging. However, Blazor doesn't currently map C# directly to JavaScript/Wasm. Instead, Blazor does IL interpretation within the browser, so source maps aren't relevant.

## Firewall configuration

If a firewall blocks communication with the debug proxy, create a firewall exception rule that permits communication between the browser and the `NodeJS` process.

Warning

Modification of a firewall configuration must be made with care to avoid creating security vulnerabilities. Carefully apply security guidance, follow best security practices, and respect warnings issued by the firewall's manufacturer.

Permitting open communication with the `NodeJS` process:

* Opens up the Node server to any connection, depending on the firewall's capabilities and configuration.
* Might be risky depending on your network.
* **Is only recommended on developer machines.**

If possible, only allow open communication with the `NodeJS` process **on trusted or private networks**.

For [Windows Firewall](/en-us/windows/security/threat-protection/windows-firewall/windows-firewall-with-advanced-security) configuration guidance, see [Create an Inbound Program or Service Rule](/en-us/windows/security/threat-protection/windows-firewall/create-an-inbound-program-or-service-rule). For more information, see [Windows Defender Firewall with Advanced Security](/en-us/windows/security/threat-protection/windows-firewall/windows-firewall-with-advanced-security) and related articles in the Windows Firewall documentation set.

## Troubleshoot

If you're running into errors, the following tips may help:

* Remove breakpoints:

  * Google Chrome: In the **Debugger** tab, open the developer tools in your browser. In the console, execute `localStorage.clear()` to remove any breakpoints.
  * Microsoft Edge: In the **Application** tab, open **Local storage**. Right-click the site and select **Clear**.

* Confirm that you've installed and trusted the ASP.NET Core HTTPS development certificate. For more information, see [Enforce HTTPS in ASP.NET Core](../security/enforcing-ssl?view=aspnetcore-10.0#troubleshoot-certificate-problems-such-as-certificate-not-trusted).

* Visual Studio requires the **Enable JavaScript debugging for ASP.NET (Chrome and Edge)** option in **Tools** > **Options** > **Debugging** > **General**. This is the default setting for Visual Studio. If debugging isn't working, confirm that the option is selected.

* If your environment uses an HTTP proxy, make sure that `localhost` is included in the proxy bypass settings. This can be done by setting the `NO_PROXY` environment variable in either:

  * The `launchSettings.json` file for the project.
  * At the user or system environment variables level for it to apply to all apps. When using an environment variable, restart Visual Studio for the change to take effect.

* Ensure that firewalls or proxies don't block communication with the debug proxy (`NodeJS` process). For more information, see the [Firewall configuration](#firewall-configuration) section.

### Breakpoints in `OnInitialized{Async}` not hit

The Blazor framework's debugging proxy doesn't launch instantly on app startup, so breakpoints in the [`OnInitialized{Async}` lifecycle methods](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) might not be hit. We recommend adding a delay at the start of the method body to give the debug proxy some time to launch before the breakpoint is hit. You can include the delay based on an [`if` compiler directive](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-if) to ensure that the delay isn't present for a release build of the app.

[OnInitialized](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitialized):

```
protected override void OnInitialized()
{
#if DEBUG
    Thread.Sleep(10000);
#endif

    ...
}
```

[OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync):

```
protected override async Task OnInitializedAsync()
{
#if DEBUG
    await Task.Delay(10000);
#endif

    ...
}
```

### Visual Studio (Windows) timeout

If Visual Studio throws an exception that the debug adapter failed to launch mentioning that the timeout was reached, you can adjust the timeout with a Registry setting:

```
VsRegEdit.exe set "<VSInstallFolder>" HKCU JSDebugger\Options\Debugging "BlazorTimeoutInMilliseconds" dword {TIMEOUT}
```

The `{TIMEOUT}` placeholder in the preceding command is in milliseconds. For example, one minute is assigned as `60000`.

## Control Hot Reload

The `WasmEnableHotReload` MSBuild property enables [Hot Reload](../test/hot-reload?view=aspnetcore-10.0) and is set to `true` by default when building in the `Debug` configuration. Hot Reload isn't enabled (set to `false`) when building in any other configuration.

To use a custom configuration name when debugging, for example, `DebugWebAssembly`, set the property to `true` to enable Hot Reload:

```
<PropertyGroup>
  <WasmEnableHotReload>true</WasmEnableHotReload>
</PropertyGroup>
```

To disable Hot Reload for the `Debug` configuration, set the value to `false`:

```
<PropertyGroup>
  <WasmEnableHotReload>false</WasmEnableHotReload>
</PropertyGroup>
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/swift-client
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/swift-client.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core SignalR Swift client

SignalR Swift is a client library for connecting to SignalR servers from Swift applications. This document provides an overview of how to install the client, establish a connection, handle server-to-client calls, invoke server methods, work with streaming responses, and configure automatic reconnection and other options.

## Install the SignalR client package

The SignalR Swift client library is delivered as a Swift package. You can add it to your project using the [Swift Package Manager](https://swift.org/package-manager/).

### Requirements

* Swift **>= 5.10**
* macOS **>= 11.0**
* iOS >= 14

### Install with Swift Package Manager

Add the SignalR Swift package as a dependency in your `Package.swift` file:

```
// swift-tools-version: 5.10
import PackageDescription

let package = Package(
    name: "signalr-client-app",
    dependencies: [
        .package(url: "https://github.com/dotnet/signalr-client-swift", branch: "main")
    ],
    targets: [
        .executableTarget(name: "YourTargetName", dependencies: [.product(name: "SignalRClient", package: "signalr-client-swift")])
    ]
)
```

After adding the dependency, import the library in your Swift code:

```
import SignalRClient
```

## Connect to a hub

To establish a connection, create a `HubConnectionBuilder` and configure it with the URL of your SignalR server using the `withUrl()` method. Once the connection is built, call `start()` to connect to the server:

```
import SignalRClient

let connection = HubConnectionBuilder()
    .withUrl(url: "https://your-signalr-server")
    .build()

try await connection.start()
```

## Call client methods from the hub

To receive messages from the server, register a handler using the `on` method. The `on` method takes the name of the hub method and a closure that will be executed when the server calls that method.

In the following the method name is `ReceiveMessage`. The argument names are `user` and `message`:

```
await connection.on("ReceiveMessage") { (user: String, message: String) in
    print("\(user) says: \(message)")
}
```

The preceding code in `connection.on` runs when server-side code calls it using the [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.clientproxyextensions.sendasync) method:

```
using Microsoft.AspNetCore.SignalR;
namespace SignalRChat.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

SignalR determines which client method to call by matching the method name and arguments defined in `SendAsync` and `connection.on`.

A **best practice** is to call the `try await connection.start()` method on the `HubConnection` after `on`. Doing so ensures the handlers are registered before any messages are received.

## Call hub methods from the client

Swift clients can invoke hub methods on the server using either the `invoke` or `send` methods of the HubConnection. The `invoke` method waits for the server's response and throws an error if the call fails, whereas the `send` method does not wait for a response.

In the following code, the method name on the hub is `SendMessage`. The second and third arguments passed to `invoke` map to the hub method's `user` and `message` arguments:

```
// Using invoke, which waits for a response
try await connection.invoke(method: "SendMessage", arguments: "myUser", "Hello")

// Using send, which does not wait for a response
try await connection.send(method: "SendMessage", arguments: "myUser", "Hello")
```

The `invoke` method returns with the return value (if any) when the method on the server returns. If the method on the server throws an error, the function throws an error.

## Logging

Swift client library includes a lightweight logging system designed for Swift applications. It provides a structured way to log messages at different levels of severity, using a customizable log handler. On Apple platforms, it leverages `os.Logger` for efficient system logging, while on other platforms, it falls back to standard console output.

### Log level

Use `HubConnectionBuilder().withLogLevel(LogLevel:)` to set the log level. Messages are logged with the specified log level and higher:

* `LogLevel.debug`: Detailed information useful for debugging.
* `LogLevel.information`: General application messages.
* `LogLevel.warning`: Warnings about potential issues.
* `LogLevel.error`: Errors that need immediate attention.

## Client results

In addition to invoking server methods, the server can call methods on the client and await a response. To support this, define a client handler that returns a result from its closure:

```
await connection.on("ClientResult") { (message: String) in
    return "client response"
}
```

For example, the server can invoke the `ClientResult` method on the client and wait for the returned value:

```
public class ChatHub : Hub
{
    public async Task TriggerClientResult()
    {
        using var cancellationTokenSource =
            new CancellationTokenSource(TimeSpan.FromSeconds(30));

        var message = await Clients.Client(connectionId).InvokeAsync<string>(
            "ClientResult", cancellationTokenSource.Token);
    }
}
```

## Working with streaming responses

To receive a stream of data from the server, use the `stream` method. The method returns a stream that you can iterate over asynchronously:

```
let streamResult: any StreamResult<String> = try await connection.stream(method: "StreamMethod")
for try await item in streamResult.stream {
    print("Received item: \(item)")
}
```

## Handle lost connection

### Automatic reconnect

The SignalR Swift client supports automatic reconnect. To enable it, call withAutomaticReconnect() while building the connection. Automatic reconnect is disabled by default.

```
let connection = HubConnectionBuilder()
    .withUrl(url: "https://your-signalr-server")
    .withAutomaticReconnect()
    .build()
```

Without parameters, withAutomaticReconnect() configures the client to wait 0, 2, 10, and 30 seconds respectively before each reconnect attempt. After four failed attempts, the client stops trying to reconnect.

Before starting any reconnect attempts, the `HubConnection` transitions to the `Reconnecting` state and fires its `onReconnecting` callbacks.

After the reconnection succeeds, the `HubConnection` transitions to the `connected` state and fires its `onReconnected` callbacks.

A general way to use `onReconnecting` and `onReconnected` is to mark the connection state changes:

```
connection.onReconnecting { error in
    // connection is disconnected because of error
}

connection.onReconnected {
    // connection is connected back
}
```

### Configure strategy in automatic reconnect

To customize the reconnect behavior, you can pass an array of numbers representing the delay in seconds before each reconnect attempt. For more granular control, pass an object that conforms to the RetryPolicy protocol.

#### Using an array of delay values

```
let connection = HubConnectionBuilder()
    .withUrl(url: "https://your-signalr-server")
    .withAutomaticReconnect([0, 0, 1]) // Wait 0, 0, and 1 second before each reconnect attempt; stop after 3 attempts.
    .build()
```

#### Using a custom retry policy

Implement the RetryPolicy protocol to control the reconnect timing:

```
// Define a custom retry policy
struct CustomRetryPolicy: RetryPolicy {
    func nextRetryInterval(retryContext: RetryContext) -> TimeInterval? {
        // For example, retry every 1 second indefinitely.
        return 1
    }
}

let connection = HubConnectionBuilder()
    .withUrl(url: "https://your-signalr-server")
    .withAutomaticReconnect(CustomRetryPolicy())
    .build()
```

## Configure timeout and keep-alive options

You can customize the client's timeout and keep-alive settings via the HubConnectionBuilder:

| Options               | Default Value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| withKeepAliveInterval | 15 (seconds)  | Determines the interval at which the client sends ping messages and is set directly on HubConnectionBuilder. This setting allows the server to detect hard disconnects, such as when a client unplugs their computer from the network. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the ClientTimeoutInterval set on the server, the server considers the client disconnected. |
| withServerTimeout     | 30 (seconds)  | Determines the interval at which the client waits for a response from the server before it considers the server disconnected. This setting is set directly on HubConnectionBuilder.                                                                                                                                                                                                                                                                         |

## Configure transport

The SignalR Swift client supports three transports: LongPolling, ServerSentEvents, and WebSockets. By default, the client will use WebSockets if the server supports it, and fall back to ServerSentEvents and LongPolling if it doesn't. You can configure the client to use a specific transport by calling `withUrl(url:transport:)` while building the connection.

```
let connection = HubConnectionBuilder()
    .withUrl(url: "https://your-signalr-server", transport: .webSockets) // use websocket only
    .build()
```

```
let connection = HubConnectionBuilder()
    .withUrl(url: "https://your-signalr-server", transport: [.webSockets, .serverSentEvents]) // use websockets and server sent events
    .build()
```

## Related content

* [WebPack and TypeScript tutorial](../tutorials/signalr-typescript-webpack?view=aspnetcore-10.0)
* [Hubs](hubs?view=aspnetcore-10.0)
* [.NET client](dotnet-client?view=aspnetcore-10.0)
* [JavaScript client](javascript-client?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/bl0008
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/bl0008.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# BL0008: Component parameters should be auto properties

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | BL0008       |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A property has the [`[SupplyParameterFromForm]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromformattribute) and is initialized with a non-default property initializer.

## Rule description

The property initializer can be overwritten with `null` during form submission, causing the `EditForm` to fail with the following exception:

> InvalidOperationException: EditForm requires either a Model parameter, or an EditContext parameter, please provide one of these.

The analyzer warns developers when this pattern is detected, while safely ignoring default value initializers (`null`, `null!`, `default`, `default!`) that don't cause the same issue.

Consider the following `Input` form model with a property initializer:

```
[SupplyParameterFromForm]
public InputModel Input { get; set; } = new();
```

The analyzer reports the following warning, where the `{COMPONENT}` placeholder is the component type name:

> Property '{COMPONENT}.Input' has \[SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.

✔️ Safe patterns that are ignored by analyzer:

```
[SupplyParameterFromForm]
public InputModel Input { get; set; } = default!;

[SupplyParameterFromForm]
public InputModel Input { get; set; } = null!;
```

## How to fix violations

To ensure the initialized value isn't overwritten, move the initialization to one of the [`OnInitialized{Async}` lifecycle methods](../blazor/components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync).

Consider the following Razor component that generates a BL0008 code analysis warning because the `Input` model for the form is decorated with `[SupplyParameterFromForm]` and has a property initializer:

```
<EditForm Model="Input" OnSubmit="Submit" FormName="Starship1">
    ...
</EditForm>

@code {
    [SupplyParameterFromForm]
    private InputModel Input { get; set; } = new();

    private void Submit()
    {
        ...
    }

    public class InputModel
    {
        public string? Id { get; set; }
    }
}
```

To fix the violation, the initialization code for the `Input` property is moved to the [`OnInitialized` lifecycle method](../blazor/components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync), leaving the property as an [automatically implemented property (*auto property*)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties). The following change also makes `Input` a [nullable reference type (NRT)](../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis):

```
[SupplyParameterFromForm]
private InputModel? Input { get; set; }

protected override void OnInitialized() => Input ??= new();
```

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-11-18

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/reuse-razor-components
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/reuse-razor-components.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Reuse Razor components in ASP.NET Core Blazor Hybrid

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to author and organize Razor components for the web and Web Views in Blazor Hybrid apps.

Razor components work across hosting models (Blazor WebAssembly, Blazor Server, and in the Web View of Blazor Hybrid) and across platforms (Android, iOS, and Windows). Hosting models and platforms have unique capabilities that components can leverage, but components executing across hosting models and platforms must leverage unique capabilities separately, which the following examples demonstrate:

* Blazor WebAssembly supports synchronous JavaScript (JS) interop, which isn't supported by the strictly asynchronous JS interop communication channel in Blazor Server and Web Views of Blazor Hybrid apps.
* Components in a Blazor Server app can access services that are only available on the server, such as an Entity Framework database context.
* Components in a `BlazorWebView` can directly access native desktop and mobile device features, such as geolocation services. Blazor Server and Blazor WebAssembly apps must rely upon web API interfaces of apps on external servers to provide similar features.

## Design principles

In order to author Razor components that can seamlessly work across hosting models and platforms, adhere to the following design principles:

* Place shared UI code in Razor class libraries (RCLs), which are containers designed to maintain reusable pieces of UI for use across different hosting models and platforms.
* Implementations of unique features shouldn't exist in RCLs. Instead, the RCL should define abstractions (interfaces and base classes) that hosting models and platforms implement.
* Only opt-in to unique features by hosting model or platform. For example, Blazor WebAssembly supports the use of [IJSInProcessRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime) and [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) in a component as an optimization, but only use them with conditional casts and fallback implementations that rely on the universal [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) and [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) abstractions that all hosting models and platforms support. For more information on [IJSInProcessRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime), see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#synchronous-js-interop-in-client-side-components). For more information on [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference), see [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0#synchronous-js-interop-in-client-side-components).
* As a general rule, use CSS for HTML styling in components. The most common case is for consistency in the look and feel of an app. In places where UI styles must differ across hosting models or platforms, use CSS to style the differences.
* If some part of the UI requires additional or different content for a target hosting model or platform, the content can be encapsulated inside a component and rendered inside the RCL using [`DynamicComponent`](../components/dynamiccomponent?view=aspnetcore-10.0). Additional UI can also be provided to components via [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) instances. For more information on [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), see [Child content render fragments](../components/?view=aspnetcore-10.0#child-content-render-fragments) and [Render fragments for reusable rendering logic](../components/?view=aspnetcore-10.0#render-fragments-for-reusable-rendering-logic).

## Project code organization

As much as possible, place code and static content in Razor class libraries (RCLs). Each hosting model or platform references the RCL and registers individual implementations in the app's service collection that a Razor component might require.

Each target assembly should contain only the code that is specific to that hosting model or platform along with the code that helps bootstrap the app.

## Use abstractions for unique features

The following example demonstrates how to use an abstraction for a geolocation service by hosting model and platform.

* In a Razor class library (RCL) used by the app to obtain geolocation data for the user's location on a map, the `MapComponent` Razor component injects an `ILocationService` service abstraction.
* `App.Web` for Blazor WebAssembly and Blazor Server projects implement `ILocationService` as `WebLocationService`, which uses web API calls to obtain geolocation data.
* `App.Desktop` for .NET MAUI, WPF, and Windows Forms, implement `ILocationService` as `DesktopLocationService`. `DesktopLocationService` uses platform-specific device features to obtain geolocation data.

## .NET MAUI Blazor platform-specific code

A common pattern in .NET MAUI is to create separate implementations for different platforms, such as defining partial classes with platform-specific implementations. For example, see the following diagram, where partial classes for `CameraService` are implemented in each of `CameraService.Windows.cs`, `CameraService.iOS.cs`, `CameraService.Android.cs`, and `CameraService.cs`:

Where you want to pack platform-specific features into a class library that can be consumed by other apps, we recommend that you follow a similar approach to the one described in the preceding example and create an abstraction for the Razor component:

* Place the component in a Razor class library (RCL).
* From a .NET MAUI class library, reference the RCL and create the platform-specific implementations.
* Within the consuming app, reference the .NET MAUI class library.

The following example demonstrates the concepts for images in an app that organizes photographs:

* A .NET MAUI Blazor Hybrid app uses `InputPhoto` from an RCL that it references.
* The .NET MAUI app also references a .NET MAUI class library.
* `InputPhoto` in the RCL injects an `ICameraService` interface, which is defined in the RCL.
* `CameraService` partial class implementations for `ICameraService` are in the .NET MAUI class library (`CameraService.Windows.cs`, `CameraService.iOS.cs`, `CameraService.Android.cs`), which references the RCL.

For an example, see [Build a .NET MAUI Blazor Hybrid app with a Blazor Web App](tutorials/maui-blazor-web-app?view=aspnetcore-10.0#using-interfaces-to-support-different-device-implementations).

## Related content

* [Combine Blazor Hybrid with .NET MAUI and Razor class libraries (RCLs)](class-libraries-best-practices?view=aspnetcore-10.0)

* eShop Reference Application (AdventureWorks): The .NET MAUI Blazor Hybrid app is in the `src/HybridApp` folder.

  * For Azure hosting: [`Azure-Samples/eShopOnAzure` GitHub repository](https://github.com/Azure-Samples/eShopOnAzure)
  * For general non-Azure hosting: [`dotnet/eShop` GitHub repository](https://github.com/dotnet/eShop).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/event-handling.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor event handling

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains Blazor's event handling features, including event argument types, event callbacks, and managing default browser events.

The [`Microsoft.AspNetCore.Components.Web` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Web) ([Microsoft.AspNetCore.Components.Web](/en-us/dotnet/api/microsoft.aspnetcore.components.web) namespace) contains types for supplying information about browser events to the Blazor framework. Blazor apps target the package implicitly, while [Razor class libraries](class-libraries?view=aspnetcore-10.0) include an explicit package reference in the library's project file (`.csproj`).

## Delegate event handlers

Specify delegate event handlers in Razor component markup with [`@on{DOM EVENT}="{DELEGATE}"`](../../mvc/views/razor?view=aspnetcore-10.0#onevent) Razor syntax:

* The `{DOM EVENT}` placeholder is a [DOM event](https://developer.mozilla.org/docs/Web/Events) (for example, `click`).
* The `{DELEGATE}` placeholder is the C# delegate event handler.

For supported events, see [EventHandlers](/en-us/dotnet/api/microsoft.aspnetcore.components.web.eventhandlers).

For event handling:

* Delegate event handlers in Blazor Web Apps are only called in components that adopt an interactive render mode. The examples throughout this article assume that the app adopts an interactive render mode globally in the app's root component, typically the `App` component. For more information, see [ASP.NET Core Blazor render modes](render-modes?view=aspnetcore-10.0#apply-a-render-mode-to-the-entire-app).
* Asynchronous delegate event handlers that return a [Task](/en-us/dotnet/api/system.threading.tasks.task) (`async Task`) are supported by Blazor and adopted by Blazor Web App and Blazor WebAssembly documentation examples.
* Delegate event handlers automatically trigger a UI render, so there's no need to manually call [`StateHasChanged`](lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged).
* Exceptions are logged.

- Asynchronous delegate event handlers that return a [Task](/en-us/dotnet/api/system.threading.tasks.task) (`async Task`) are supported by Blazor and adopted by Blazor Server and Blazor WebAssembly documentation examples.
- Delegate event handlers automatically trigger a UI render, so there's no need to manually call [`StateHasChanged`](lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged).
- Exceptions are logged.

Important

The Blazor framework doesn't track `void`-returning asynchronous methods (`async`). As a result, the entire process fails when an exception isn't caught if `void` is returned. Always return a [Task](/en-us/dotnet/api/system.threading.tasks.task)/[ValueTask](/en-us/dotnet/api/system.threading.tasks.valuetask) from asynchronous methods.

The following code:

* Calls the `UpdateHeading` method when the button is selected in the UI.
* Calls the `CheckChanged` method when the checkbox is changed in the UI.

`EventHandler1.razor`:

```
@page "/event-handler-1"

<PageTitle>Event Handler 1</PageTitle>

<h1>Event Handler Example 1</h1>

<h2>@headingValue</h2>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

<p>
    <label>
        <input type="checkbox" @onchange="CheckChanged" />
        @checkedMessage
    </label>
</p>

@code {
    private string headingValue = "Initial heading";
    private string checkedMessage = "Not changed yet";

    private void UpdateHeading() => headingValue = $"New heading ({DateTime.Now})";

    private void CheckChanged() => checkedMessage = $"Last change {DateTime.Now}";
}
```

`EventHandler1.razor`:

```
@page "/event-handler-1"

<PageTitle>Event Handler 1</PageTitle>

<h1>Event Handler Example 1</h1>

<h2>@headingValue</h2>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

<p>
    <label>
        <input type="checkbox" @onchange="CheckChanged" />
        @checkedMessage
    </label>
</p>

@code {
    private string headingValue = "Initial heading";
    private string checkedMessage = "Not changed yet";

    private void UpdateHeading() => headingValue = $"New heading ({DateTime.Now})";

    private void CheckChanged() => checkedMessage = $"Last change {DateTime.Now}";
}
```

`EventHandlerExample1.razor`:

```
@page "/event-handler-1"

<h1>@headingValue</h1>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

<p>
    <label>
        <input type="checkbox" @onchange="CheckChanged" />
        @checkedMessage
    </label>
</p>

@code {
    private string headingValue = "Initial heading";
    private string checkedMessage = "Not changed yet";

    private void UpdateHeading()
    {
        headingValue = $"New heading ({DateTime.Now})";
    }

    private void CheckChanged()
    {
        checkedMessage = $"Last changed at {DateTime.Now}";
    }
}
```

`EventHandlerExample1.razor`:

```
@page "/event-handler-1"

<h1>@headingValue</h1>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

<p>
    <label>
        <input type="checkbox" @onchange="CheckChanged" />
        @checkedMessage
    </label>
</p>

@code {
    private string headingValue = "Initial heading";
    private string checkedMessage = "Not changed yet";

    private void UpdateHeading()
    {
        headingValue = $"New heading ({DateTime.Now})";
    }

    private void CheckChanged()
    {
        checkedMessage = $"Last changed at {DateTime.Now}";
    }
}
```

`EventHandlerExample1.razor`:

```
@page "/event-handler-1"

<h1>@headingValue</h1>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

<p>
    <label>
        <input type="checkbox" @onchange="CheckChanged" />
        @checkedMessage
    </label>
</p>

@code {
    private string headingValue = "Initial heading";
    private string checkedMessage = "Not changed yet";

    private void UpdateHeading()
    {
        headingValue = $"New heading ({DateTime.Now})";
    }

    private void CheckChanged()
    {
        checkedMessage = $"Last changed at {DateTime.Now}";
    }
}
```

`EventHandlerExample1.razor`:

```
@page "/event-handler-1"

<h1>@headingValue</h1>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

<p>
    <label>
        <input type="checkbox" @onchange="CheckChanged" />
        @checkedMessage
    </label>
</p>

@code {
    private string headingValue = "Initial heading";
    private string checkedMessage = "Not changed yet";

    private void UpdateHeading()
    {
        headingValue = $"New heading ({DateTime.Now})";
    }

    private void CheckChanged()
    {
        checkedMessage = $"Last changed at {DateTime.Now}";
    }
}
```

In the following example, `UpdateHeading`:

* Is called asynchronously when the button is selected.
* Waits two seconds before updating the heading.

`EventHandler2.razor`:

```
@page "/event-handler-2"

<PageTitle>Event Handler 2</PageTitle>

<h1>Event Handler Example 2</h1>

<h2>@headingValue</h2>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

@code {
    private string headingValue = "Initial heading";

    private async Task UpdateHeading()
    {
        await Task.Delay(2000);

        headingValue = $"New heading ({DateTime.Now})";
    }
}
```

`EventHandler2.razor`:

```
@page "/event-handler-2"

<PageTitle>Event Handler 2</PageTitle>

<h1>Event Handler Example 2</h1>

<h2>@headingValue</h2>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

@code {
    private string headingValue = "Initial heading";

    private async Task UpdateHeading()
    {
        await Task.Delay(2000);

        headingValue = $"New heading ({DateTime.Now})";
    }
}
```

`EventHandlerExample2.razor`:

```
@page "/event-handler-2"

<h1>@headingValue</h1>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

@code {
    private string headingValue = "Initial heading";

    private async Task UpdateHeading()
    {
        await Task.Delay(2000);

        headingValue = $"New heading ({DateTime.Now})";
    }
}
```

`EventHandlerExample2.razor`:

```
@page "/event-handler-2"

<h1>@headingValue</h1>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

@code {
    private string headingValue = "Initial heading";

    private async Task UpdateHeading()
    {
        await Task.Delay(2000);

        headingValue = $"New heading ({DateTime.Now})";
    }
}
```

`EventHandlerExample2.razor`:

```
@page "/event-handler-2"

<h1>@headingValue</h1>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

@code {
    private string headingValue = "Initial heading";

    private async Task UpdateHeading()
    {
        await Task.Delay(2000);

        headingValue = $"New heading ({DateTime.Now})";
    }
}
```

`EventHandlerExample2.razor`:

```
@page "/event-handler-2"

<h1>@headingValue</h1>

<p>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

@code {
    private string headingValue = "Initial heading";

    private async Task UpdateHeading()
    {
        await Task.Delay(2000);

        headingValue = $"New heading ({DateTime.Now})";
    }
}
```

## Built-in event arguments

For events that support an event argument type, specifying an event parameter in the event method definition is only necessary if the event type is used in the method. In the following example, [MouseEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.mouseeventargs) is used in the `ReportPointerLocation` method to set message text that reports the mouse coordinates when the user selects a button in the UI.

`EventHandler3.razor`:

```
@page "/event-handler-3"

<PageTitle>Event Handler 3</PageTitle>

<h1>Event Handler Example 3</h1>

@for (var i = 0; i < 4; i++)
{
    <p>
        <button @onclick="ReportPointerLocation">
            Where's my mouse pointer for this button?
        </button>
    </p>
}

<p>@mousePointerMessage</p>

@code {
    private string? mousePointerMessage;

    private void ReportPointerLocation(MouseEventArgs e) => 
        mousePointerMessage = $"Mouse coordinates: {e.ScreenX}:{e.ScreenY}";
}
```

`EventHandler3.razor`:

```
@page "/event-handler-3"

<PageTitle>Event Handler 3</PageTitle>

<h1>Event Handler Example 3</h1>

@for (var i = 0; i < 4; i++)
{
    <p>
        <button @onclick="ReportPointerLocation">
            Where's my mouse pointer for this button?
        </button>
    </p>
}

<p>@mousePointerMessage</p>

@code {
    private string? mousePointerMessage;

    private void ReportPointerLocation(MouseEventArgs e) => 
        mousePointerMessage = $"Mouse coordinates: {e.ScreenX}:{e.ScreenY}";
}
```

`EventHandlerExample3.razor`:

```
@page "/event-handler-example-3"

@for (var i = 0; i < 4; i++)
{
    <p>
        <button @onclick="ReportPointerLocation">
            Where's my mouse pointer for this button?
        </button>
    </p>
}

<p>@mousePointerMessage</p>

@code {
    private string? mousePointerMessage;

    private void ReportPointerLocation(MouseEventArgs e)
    {
        mousePointerMessage = $"Mouse coordinates: {e.ScreenX}:{e.ScreenY}";
    }
}
```

`EventHandlerExample3.razor`:

```
@page "/event-handler-example-3"

@for (var i = 0; i < 4; i++)
{
    <p>
        <button @onclick="ReportPointerLocation">
            Where's my mouse pointer for this button?
        </button>
    </p>
}

<p>@mousePointerMessage</p>

@code {
    private string? mousePointerMessage;

    private void ReportPointerLocation(MouseEventArgs e)
    {
        mousePointerMessage = $"Mouse coordinates: {e.ScreenX}:{e.ScreenY}";
    }
}
```

`EventHandlerExample3.razor`:

```
@page "/event-handler-example-3"

@for (var i = 0; i < 4; i++)
{
    <p>
        <button @onclick="ReportPointerLocation">
            Where's my mouse pointer for this button?
        </button>
    </p>
}

<p>@mousePointerMessage</p>

@code {
    private string mousePointerMessage;

    private void ReportPointerLocation(MouseEventArgs e)
    {
        mousePointerMessage = $"Mouse coordinates: {e.ScreenX}:{e.ScreenY}";
    }
}
```

`EventHandlerExample3.razor`:

```
@page "/event-handler-example-3"

@for (var i = 0; i < 4; i++)
{
    <p>
        <button @onclick="ReportPointerLocation">
            Where's my mouse pointer for this button?
        </button>
    </p>
}

<p>@mousePointerMessage</p>

@code {
    private string mousePointerMessage;

    private void ReportPointerLocation(MouseEventArgs e)
    {
        mousePointerMessage = $"Mouse coordinates: {e.ScreenX}:{e.ScreenY}";
    }
}
```

Supported [EventArgs](/en-us/dotnet/api/system.eventargs) are shown in the following table.

| Event         | Class                                                                                          | DOM notes                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Clipboard     | [ClipboardEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.clipboardeventargs) |                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Drag          | [DragEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.drageventargs)           | [DataTransfer](/en-us/dotnet/api/microsoft.aspnetcore.components.web.datatransfer) and [DataTransferItem](/en-us/dotnet/api/microsoft.aspnetcore.components.web.datatransferitem) hold dragged item data. Implement drag and drop in Blazor apps using [JS interop](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0) with [HTML Drag and Drop API](https://developer.mozilla.org/docs/Web/API/HTML_Drag_and_Drop_API). |
| Error         | [ErrorEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.erroreventargs)         |                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Event         | [EventArgs](/en-us/dotnet/api/system.eventargs)                                                | [EventHandlers](/en-us/dotnet/api/microsoft.aspnetcore.components.web.eventhandlers) holds attributes to configure the mappings between event names and event argument types.                                                                                                                                                                                                                                                                          |
| Focus         | [FocusEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.focuseventargs)         | Doesn't include support for `relatedTarget`.                                                                                                                                                                                                                                                                                                                                                                                                           |
| Input         | [ChangeEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.changeeventargs)           |                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Keyboard      | [KeyboardEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.keyboardeventargs)   |                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Mouse         | [MouseEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.mouseeventargs)         |                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Mouse pointer | [PointerEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pointereventargs)     |                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Mouse wheel   | [WheelEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.wheeleventargs)         |                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Progress      | [ProgressEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.progresseventargs)   |                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Touch         | [TouchEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.toucheventargs)         | [TouchPoint](/en-us/dotnet/api/microsoft.aspnetcore.components.web.touchpoint) represents a single contact point on a touch-sensitive device.                                                                                                                                                                                                                                                                                                          |

For more information, see the following resources:

* [`EventArgs` classes in the ASP.NET Core reference source (dotnet/aspnetcore `main` branch)](https://github.com/dotnet/aspnetcore/tree/main/src/Components/Web/src/Web)

  Note

  Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

* [EventHandlers](/en-us/dotnet/api/microsoft.aspnetcore.components.web.eventhandlers) holds attributes to configure the mappings between event names and event argument types.

## Custom event arguments

Blazor supports custom event arguments, which enable you to pass arbitrary data to .NET event handlers with custom events.

### General configuration

Custom events with custom event arguments are generally enabled with the following steps.

In JavaScript, define a function for building the custom event argument object from the source event:

```
function eventArgsCreator(event) { 
  return {
    customProperty1: 'any value for property 1',
    customProperty2: event.srcElement.id
  };
}
```

The `event` parameter is a [DOM Event](https://developer.mozilla.org/docs/Web/API/Event).

Register the custom event with the preceding handler in a [JavaScript initializer](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers). Provide the appropriate browser event name to `browserEventName`, which for the example shown in this section is `click` for a button selection in the UI.

`wwwroot/{PACKAGE ID/ASSEMBLY NAME}.lib.module.js` (the `{PACKAGE ID/ASSEMBLY NAME}` placeholder is the package ID or assembly name of the app):

For a Blazor Web App:

```
export function afterWebStarted(blazor) {
  blazor.registerCustomEventType('customevent', {
    browserEventName: 'click',
    createEventArgs: eventArgsCreator
  });
}
```

For a Blazor Server or Blazor WebAssembly app:

```
export function afterStarted(blazor) {
  blazor.registerCustomEventType('customevent', {
    browserEventName: 'click',
    createEventArgs: eventArgsCreator
  });
}
```

The call to `registerCustomEventType` is performed in a script only once per event.

For the call to `registerCustomEventType`, use the `blazor` parameter (lowercase `b`) provided by the Blazor start event. Although the registration is valid when using the `Blazor` object (uppercase `B`), the preferred approach is to use the parameter.

The custom event name, `customevent` in the preceding example, must not match a reserved Blazor event name. The reserved names can be found in the [Blazor framework reference source (see the calls to the `registerBuiltInEventType` function)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web.JS/src/Rendering/Events/EventTypes.ts).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Define a class for the event arguments:

```
namespace BlazorSample.CustomEvents;

public class CustomEventArgs : EventArgs
{
    public string? CustomProperty1 {get; set;}
    public string? CustomProperty2 {get; set;}
}
```

Wire up the custom event with the event arguments by adding an [`[EventHandler]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.eventhandlerattribute) annotation for the custom event:

* In order for the compiler to find the `[EventHandler]` class, it must be placed into a C# class file (`.cs`), making it a normal top-level class.
* Mark the class `public`.
* The class doesn't require members.
* The class *must* be called "`EventHandlers`" in order to be found by the Razor compiler.
* Place the class under a namespace specific to your app.
* Import the namespace into the Razor component (`.razor`) where the event is used.

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample.CustomEvents;

[EventHandler("oncustomevent", typeof(CustomEventArgs),
    enableStopPropagation: true, enablePreventDefault: true)]
public static class EventHandlers
{
}
```

Register the event handler on one or more HTML elements. Access the data that was passed in from JavaScript in the delegate handler method:

```
@using BlazorSample.CustomEvents

<button id="buttonId" @oncustomevent="HandleCustomEvent">Handle</button>

@if (!string.IsNullOrEmpty(propVal1) && !string.IsNullOrEmpty(propVal2))
{
    <ul>
        <li>propVal1: @propVal1</li>
        <li>propVal2: @propVal2</li>
    </ul>
}

@code
{
    private string? propVal1;
    private string? propVal2;

    private void HandleCustomEvent(CustomEventArgs eventArgs)
    {
        propVal1 = eventArgs.CustomProperty1;
        propVal2 = eventArgs.CustomProperty2;
    }
}
```

If the `@oncustomevent` attribute isn't recognized by [IntelliSense](/en-us/visualstudio/ide/using-intellisense), make sure that the component or the imports file (`_Imports.razor`) contains an `@using` statement for the namespace containing the `EventHandler` class.

Whenever the custom event is fired on the DOM, the event handler is called with the data passed from the JavaScript.

If you're attempting to fire a custom event, [`bubbles`](https://developer.mozilla.org/docs/Web/API/Event/bubbles) must be enabled by setting its value to `true`. Otherwise, the event doesn't reach the Blazor handler for processing into the C# custom [`[EventHandler]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.eventhandlerattribute) class. For more information, see [MDN Web Docs: Event bubbling](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events#event_bubbling).

### Custom clipboard paste event example

The following example receives a custom clipboard paste event that includes the time of the paste and the user's pasted text.

Declare a custom name (`oncustompaste`) for the event and a .NET class (`CustomPasteEventArgs`) to hold the event arguments for this event:

`CustomEvents.cs`:

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample.CustomEvents;

[EventHandler("oncustompaste", typeof(CustomPasteEventArgs), 
    enableStopPropagation: true, enablePreventDefault: true)]
public static class EventHandlers
{
}

public class CustomPasteEventArgs : EventArgs
{
    public DateTime EventTimestamp { get; set; }
    public string? PastedData { get; set; }
}
```

Add JavaScript code to supply data for the [EventArgs](/en-us/dotnet/api/system.eventargs) subclass with the preceding handler in a [JavaScript initializer](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers). The following example only handles pasting text, but you could use arbitrary JavaScript APIs to deal with users pasting other types of data, such as images.

`wwwroot/{PACKAGE ID/ASSEMBLY NAME}.lib.module.js`:

For a Blazor Web App:

```
export function afterWebStarted(blazor) {
  blazor.registerCustomEventType('custompaste', {
    browserEventName: 'paste',
    createEventArgs: event => {
      return {
        eventTimestamp: new Date(),
        pastedData: event.clipboardData.getData('text')
      };
    }
  });
}
```

For a Blazor Server or Blazor WebAssembly app:

```
export function afterStarted(blazor) {
  blazor.registerCustomEventType('custompaste', {
    browserEventName: 'paste',
    createEventArgs: event => {
      return {
        eventTimestamp: new Date(),
        pastedData: event.clipboardData.getData('text')
      };
    }
  });
}
```

In the preceding example, the `{PACKAGE ID/ASSEMBLY NAME}` placeholder of the file name represents the package ID or assembly name of the app.

Note

For the call to `registerCustomEventType`, use the `blazor` parameter (lowercase `b`) provided by the Blazor start event. Although the registration is valid when using the `Blazor` object (uppercase `B`), the preferred approach is to use the parameter.

The preceding code tells the browser that when a native [`paste`](https://developer.mozilla.org/docs/Web/API/Element/paste_event) event occurs:

* Raise a `custompaste` event.

* Supply the event arguments data using the custom logic stated:

  * For the `eventTimestamp`, create a new date.
  * For the `pastedData`, get the clipboard data as text. For more information, see [MDN Web Docs: ClipboardEvent.clipboardData](https://developer.mozilla.org/docs/Web/API/ClipboardEvent/clipboardData).

Event name conventions differ between .NET and JavaScript:

* In .NET, event names are prefixed with "`on`".
* In JavaScript, event names don't have a prefix.

In a Razor component, attach the custom handler to an element.

`CustomPasteArguments.razor`:

```
@page "/custom-paste-arguments"
@using BlazorSample.CustomEvents

<label>
    Try pasting into the following text box:
    <input @oncustompaste="HandleCustomPaste" />
</label>

<p>
    @message
</p>

@code {
    private string? message;

    private void HandleCustomPaste(CustomPasteEventArgs eventArgs)
    {
        message = $"At {eventArgs.EventTimestamp.ToShortTimeString()}, " +
            $"you pasted: {eventArgs.PastedData}";
    }
}
```

## Lambda expressions

[Lambda expressions](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions) are supported as the delegate event handler.

`EventHandler4.razor`:

```
@page "/event-handler-4"

<PageTitle>Event Handler 4</PageTitle>

<h1>Event Handler Example 4</h1>

<h2>@heading</h2>

<p>
    <button @onclick="@(e => heading = "New heading!!!")">
        Update heading
    </button>
</p>

@code {
    private string heading = "Initial heading";
}
```

`EventHandler4.razor`:

```
@page "/event-handler-4"

<PageTitle>Event Handler 4</PageTitle>

<h1>Event Handler Example 4</h1>

<h2>@heading</h2>

<p>
    <button @onclick="@(e => heading = "New heading!!!")">
        Update heading
    </button>
</p>

@code {
    private string heading = "Initial heading";
}
```

`EventHandlerExample4.razor`:

```
@page "/event-handler-example-4"

<h1>@heading</h1>

<p>
    <button @onclick="@(e => heading = "New heading!!!")">
        Update heading
    </button>
</p>

@code {
    private string heading = "Initial heading";
}
```

`EventHandlerExample4.razor`:

```
@page "/event-handler-example-4"

<h1>@heading</h1>

<p>
    <button @onclick="@(e => heading = "New heading!!!")">
        Update heading
    </button>
</p>

@code {
    private string heading = "Initial heading";
}
```

`EventHandlerExample4.razor`:

```
@page "/event-handler-example-4"

<h1>@heading</h1>

<p>
    <button @onclick="@(e => heading = "New heading!!!")">
        Update heading
    </button>
</p>

@code {
    private string heading = "Initial heading";
}
```

`EventHandlerExample4.razor`:

```
@page "/event-handler-example-4"

<h1>@heading</h1>

<p>
    <button @onclick="@(e => heading = "New heading!!!")">
        Update heading
    </button>
</p>

@code {
    private string heading = "Initial heading";
}
```

It's often convenient to close over additional values using C# method parameters, such as when iterating over a set of elements. The following example creates three buttons, each of which calls `UpdateHeading` and passes the following data:

* An event argument ([MouseEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.mouseeventargs)) in `e`.
* The button number in `buttonNumber`.

`EventHandler5.razor`:

```
@page "/event-handler-5"

<PageTitle>Event Handler 5</PageTitle>

<h1>Event Handler Example 5</h1>

<h2>@heading</h2>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber) => 
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
}
```

`EventHandler5.razor`:

```
@page "/event-handler-5"

<PageTitle>Event Handler 5</PageTitle>

<h1>Event Handler Example 5</h1>

<h2>@heading</h2>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber) => 
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
}
```

`EventHandlerExample5.razor`:

```
@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}
```

`EventHandlerExample5.razor`:

```
@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}
```

`EventHandlerExample5.razor`:

```
@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}
```

`EventHandlerExample5.razor`:

```
@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}
```

Creating a large number of event delegates in a loop may cause poor rendering performance. For more information, see [ASP.NET Core Blazor rendering performance best practices](../performance/rendering?view=aspnetcore-10.0#avoid-recreating-delegates-for-many-repeated-elements-or-components).

Avoid using a loop variable directly in a lambda expression, such as `i` in the preceding `for` loop example. Otherwise, the same variable is used by all lambda expressions, which results in use of the same value in all lambdas. Capture the variable's value in a local variable. In the preceding example:

* The loop variable `i` is assigned to `buttonNumber`.
* `buttonNumber` is used in the lambda expression.

Alternatively, use a `foreach` loop with [Enumerable.Range](/en-us/dotnet/api/system.linq.enumerable.range), which doesn't suffer from the preceding problem:

```
@foreach (var buttonNumber in Enumerable.Range(1, 3))
{
    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@buttonNumber
        </button>
    </p>
}
```

## EventCallback

A common scenario with nested components is executing a method in a parent component when a child component event occurs. An `onclick` event occurring in the child component is a common use case. To expose events across components, use an [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback). A parent component can assign a callback method to a child component's [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback).

The following `Child` component demonstrates how a button's `onclick` handler is set up to receive an [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback) delegate from the sample's `ParentComponent`. The [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback) is typed with [MouseEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.mouseeventargs), which is appropriate for an `onclick` event from a peripheral device.

`Child.razor`:

```
<p>
    <button @onclick="OnClickCallback">
        Trigger a Parent component method
    </button>
</p>

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

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<p>
    <button @onclick="OnClickCallback">
        Trigger a Parent component method
    </button>
</p>

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

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<p>
    <button @onclick="OnClickCallback">
        Trigger a Parent component method
    </button>
</p>

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

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<p>
    <button @onclick="OnClickCallback">
        Trigger a Parent component method
    </button>
</p>

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

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<p>
    <button @onclick="OnClickCallback">
        Trigger a Parent component method
    </button>
</p>

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

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<p>
    <button @onclick="OnClickCallback">
        Trigger a Parent component method
    </button>
</p>

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

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

The parent component sets the child's [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1) (`OnClickCallback`) to its `ShowMessage` method.

`ParentChild.razor`:

```
@page "/parent-child"

<PageTitle>Parent Child</PageTitle>

<h1>Parent Child Example</h1>

<Child Title="Panel Title from Parent" OnClickCallback="ShowMessage">
    Content of the child component is supplied by the parent component.
</Child>

<p>@message</p>

@code {
    private string? message;

    private void ShowMessage(MouseEventArgs e) => 
        message = $"Blaze a new trail with Blazor! ({e.ScreenX}:{e.ScreenY})";
}
```

`ParentChild.razor`:

```
@page "/parent-child"

<PageTitle>Parent Child</PageTitle>

<h1>Parent Child Example</h1>

<Child Title="Panel Title from Parent" OnClickCallback="ShowMessage">
    Content of the child component is supplied by the parent component.
</Child>

<p>@message</p>

@code {
    private string? message;

    private void ShowMessage(MouseEventArgs e) => 
        message = $"Blaze a new trail with Blazor! ({e.ScreenX}:{e.ScreenY})";
}
```

`Parent.razor`:

```
@page "/parent"

<h1>Parent-child example</h1>

<Child Title="Panel Title from Parent" OnClickCallback="ShowMessage">
    Content of the child component is supplied by the parent component.
</Child>

<p>@message</p>

@code {
    private string? message;

    private void ShowMessage(MouseEventArgs e)
    {
        message = $"Blaze a new trail with Blazor! ({e.ScreenX}:{e.ScreenY})";
    }
}
```

`Parent.razor`:

```
@page "/parent"

<h1>Parent-child example</h1>

<Child Title="Panel Title from Parent" OnClickCallback="ShowMessage">
    Content of the child component is supplied by the parent component.
</Child>

<p>@message</p>

@code {
    private string? message;

    private void ShowMessage(MouseEventArgs e)
    {
        message = $"Blaze a new trail with Blazor! ({e.ScreenX}:{e.ScreenY})";
    }
}
```

`Parent.razor`:

```
@page "/parent"

<h1>Parent-child example</h1>

<Child Title="Panel Title from Parent" OnClickCallback="ShowMessage">
    Content of the child component is supplied by the parent component.
</Child>

<p>@message</p>

@code {
    private string message;

    private void ShowMessage(MouseEventArgs e)
    {
        message = $"Blaze a new trail with Blazor! ({e.ScreenX}:{e.ScreenY})";
    }
}
```

`Parent.razor`:

```
@page "/parent"

<h1>Parent-child example</h1>

<Child Title="Panel Title from Parent" OnClickCallback="ShowMessage">
    Content of the child component is supplied by the parent component.
</Child>

<p>@message</p>

@code {
    private string message;

    private void ShowMessage(MouseEventArgs e)
    {
        message = $"Blaze a new trail with Blazor! ({e.ScreenX}:{e.ScreenY})";
    }
}
```

When the button is selected in the `ChildComponent`:

* The `Parent` component's `ShowMessage` method is called. `message` is updated and displayed in the `Parent` component.
* A call to [`StateHasChanged`](lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged) isn't required in the callback's method (`ShowMessage`). [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is called automatically to rerender the `Parent` component, just as child events trigger component rerendering in event handlers that execute within the child. For more information, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0#statehaschanged).

Use [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback) and [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1) for event handling and binding component parameters.

Prefer the strongly typed [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1) over [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback). [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1) provides enhanced error feedback when an inappropriate type is used, guiding users of the component towards correct implementation. Similar to other UI event handlers, specifying the event parameter is optional. Use [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback) when there's no value passed to the callback.

[EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback) and [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1) permit asynchronous delegates. [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback) is weakly typed and allows passing any type argument in `InvokeAsync(Object)`. [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1) is strongly typed and requires passing a `T` argument in `InvokeAsync(T)` that's assignable to `TValue`.

Invoke an [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback) or [EventCallback\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback-1) with [InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback.invokeasync) and await the [Task](/en-us/dotnet/api/system.threading.tasks.task):

```
await OnClickCallback.InvokeAsync({ARGUMENT});
```

In the preceding example, the `{ARGUMENT}` placeholder is an optional argument.

The following parent-child example demonstrates the technique.

`Child2.razor`:

```
<h3>Child2 Component</h3>

<button @onclick="TriggerEvent">Click Me</button>

@code {
    [Parameter]
    public EventCallback<string> OnClickCallback { get; set; }

    private async Task TriggerEvent()
    {
        await OnClickCallback.InvokeAsync("Blaze It!");
    }
}
```

`ParentChild2.razor`:

```
@page "/parent-child-2"

<PageTitle>Parent Child 2</PageTitle>

<h1>Parent Child 2 Example</h1>

<div>
    <Child2 OnClickCallback="(value) => { message1 = value; }" />
    @message1
</div>

<div>
    <Child2 OnClickCallback=
        "async (value) => { await Task.Delay(2000); message2 = value; }" /> 
    @message2
</div>

@code {
    private string message1 = string.Empty;
    private string message2 = string.Empty;
}
```

The second occurrence of the `Child2` component demonstrates an asynchronous callback, and the new `message2` value is assigned and rendered with a delay of two seconds.

## Prevent default actions

Use the [`@on{DOM EVENT}:preventDefault`](../../mvc/views/razor?view=aspnetcore-10.0#oneventpreventdefault) directive attribute to prevent the default action for an event, where the `{DOM EVENT}` placeholder is a [DOM event](https://developer.mozilla.org/docs/Web/Events).

When a key is selected on an input device and the element focus is on a text box, a browser normally displays the key's character in the text box. In the following example, the default behavior is prevented by specifying the `@onkeydown:preventDefault` directive attribute. When the focus is on the `<input>` element, the counter increments with the key sequence `Shift`+`+`. The `+` character isn't assigned to the `<input>` element's value. For more information on `keydown`, see [`MDN Web Docs: Document: keydown` event](https://developer.mozilla.org/docs/Web/API/Document/keydown_event).

`EventHandler6.razor`:

```
@page "/event-handler-6"

<PageTitle>Event Handler 6</PageTitle>

<h1>Event Handler Example 6</h1>

<p>For this example, give the <code><input></code> focus.</p>

<p>
    <label>
        Count of '+' key presses: 
        <input value="@count" @onkeydown="KeyHandler" @onkeydown:preventDefault />
    </label>
</p>

@code {
    private int count = 0;

    private void KeyHandler(KeyboardEventArgs e)
    {
        if (e.Key == "+")
        {
            count++;
        }
    }
}
```

`EventHandler6.razor`:

```
@page "/event-handler-6"

<PageTitle>Event Handler 6</PageTitle>

<h1>Event Handler Example 6</h1>

<p>For this example, give the <code><input></code> focus.</p>

<p>
    <label>
        Count of '+' key presses: 
        <input value="@count" @onkeydown="KeyHandler" @onkeydown:preventDefault />
    </label>
</p>

@code {
    private int count = 0;

    private void KeyHandler(KeyboardEventArgs e)
    {
        if (e.Key == "+")
        {
            count++;
        }
    }
}
```

`EventHandlerExample6.razor`:

```
@page "/event-handler-example-6"

<p>
    <input value="@count" @onkeydown="KeyHandler" @onkeydown:preventDefault />
</p>

@code {
    private int count = 0;

    private void KeyHandler(KeyboardEventArgs e)
    {
        if (e.Key == "+")
        {
            count++;
        }
    }
}
```

`EventHandlerExample6.razor`:

```
@page "/event-handler-example-6"

<p>
    <input value="@count" @onkeydown="KeyHandler" @onkeydown:preventDefault />
</p>

@code {
    private int count = 0;

    private void KeyHandler(KeyboardEventArgs e)
    {
        if (e.Key == "+")
        {
            count++;
        }
    }
}
```

`EventHandlerExample6.razor`:

```
@page "/event-handler-example-6"

<p>
    <input value="@count" @onkeydown="KeyHandler" @onkeydown:preventDefault />
</p>

@code {
    private int count = 0;

    private void KeyHandler(KeyboardEventArgs e)
    {
        if (e.Key == "+")
        {
            count++;
        }
    }
}
```

`EventHandlerExample6.razor`:

```
@page "/event-handler-example-6"

<p>
    <input value="@count" @onkeydown="KeyHandler" @onkeydown:preventDefault />
</p>

@code {
    private int count = 0;

    private void KeyHandler(KeyboardEventArgs e)
    {
        if (e.Key == "+")
        {
            count++;
        }
    }
}
```

Specifying the `@on{DOM EVENT}:preventDefault` attribute without a value is equivalent to `@on{DOM EVENT}:preventDefault="true"`.

An expression is also a permitted value of the attribute. In the following example, `shouldPreventDefault` is a `bool` field set to either `true` or `false`:

```
<input @onkeydown:preventDefault="shouldPreventDefault" />

...

@code {
    private bool shouldPreventDefault = true;
}
```

## Stop event propagation

Use the [`@on{DOM EVENT}:stopPropagation`](../../mvc/views/razor?view=aspnetcore-10.0#oneventstoppropagation) directive attribute to stop event propagation within the Blazor scope. `{DOM EVENT}` is a placeholder for a [DOM event](https://developer.mozilla.org/docs/Web/Events).

The `stopPropagation` directive attribute's effect is limited to the Blazor scope and doesn't extend to the HTML DOM. Events must propagate to the HTML DOM root before Blazor can act upon them. For a mechanism to prevent HTML DOM event propagation, consider the following approach:

* Obtain the event's path by calling [`Event.composedPath()`](https://developer.mozilla.org/docs/Web/API/Event/composedPath).
* Filter events based on the composed [event targets (`EventTarget`)](https://developer.mozilla.org/docs/Web/API/EventTarget).

In the following example, selecting the checkbox prevents click events from the second child `<div>` from propagating to the parent `<div>`. Since propagated click events normally fire the `OnSelectParentDiv` method, selecting the second child `<div>` results in the parent `<div>` message appearing unless the checkbox is selected.

`EventHandler7.razor`:

```
@page "/event-handler-7"

<PageTitle>Event Handler 7</PageTitle>

<h1>Event Handler Example 7</h1>

<div>
    <b>stopPropagation</b>: @stopPropagation
</div>

<div>
    <button @onclick="StopPropagation">
        Stop Propagation (stopPropagation = true)
    </button>
    <button @onclick="EnablePropagation">
        Enable Propagation (stopPropagation = false)
    </button>
</div>

<div class="m-1 p-1 border border-primary" @onclick="OnSelectParentDiv">
    <h3>Parent div</h3>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv">
        Child div that never stops propagation to the parent div when 
        selected.
    </div>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv" 
            @onclick:stopPropagation="stopPropagation">
        Child div that stops propagation when selected if 
        <b>stopPropagation</b> is <b>true</b>.
    </div>
</div>

<p>
    @message
</p>

@code {
    private bool stopPropagation = false;
    private string? message;

    private void StopPropagation() => stopPropagation = true;

    private void EnablePropagation() => stopPropagation = false;

    private void OnSelectParentDiv() => 
        message = $"The parent div was selected. {DateTime.Now}";

    private void OnSelectChildDiv() => 
        message = $"The child div was selected. {DateTime.Now}";
}
```

`EventHandler7.razor`:

```
@page "/event-handler-7"

<PageTitle>Event Handler 7</PageTitle>

<h1>Event Handler Example 7</h1>

<div>
    <b>stopPropagation</b>: @stopPropagation
</div>

<div>
    <button @onclick="StopPropagation">
        Stop Propagation (stopPropagation = true)
    </button>
    <button @onclick="EnablePropagation">
        Enable Propagation (stopPropagation = false)
    </button>
</div>

<div class="m-1 p-1 border border-primary" @onclick="OnSelectParentDiv">
    <h3>Parent div</h3>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv">
        Child div that never stops propagation to the parent div when 
        selected.
    </div>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv" 
            @onclick:stopPropagation="stopPropagation">
        Child div that stops propagation when selected if 
        <b>stopPropagation</b> is <b>true</b>.
    </div>
</div>

<p>
    @message
</p>

@code {
    private bool stopPropagation = false;
    private string? message;

    private void StopPropagation() => stopPropagation = true;

    private void EnablePropagation() => stopPropagation = false;

    private void OnSelectParentDiv() => 
        message = $"The parent div was selected. {DateTime.Now}";

    private void OnSelectChildDiv() => 
        message = $"The child div was selected. {DateTime.Now}";
}
```

`EventHandlerExample7.razor`:

```
@page "/event-handler-example-7"

<div>
    <b>stopPropagation</b>: @stopPropagation
</div>

<div>
    <button @onclick="StopPropagation">
        Stop Propagation (stopPropagation = true)
    </button>
    <button @onclick="EnablePropagation">
        Enable Propagation (stopPropagation = false)
    </button>
</div>

<div class="m-1 p-1 border border-primary" @onclick="OnSelectParentDiv">
    <h3>Parent div</h3>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv">
        Child div that never stops propagation to the parent div when 
        selected.
    </div>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv" 
            @onclick:stopPropagation="stopPropagation">
        Child div that stops propagation when selected if 
        <b>stopPropagation</b> is <b>true</b>.
    </div>
</div>

<p>
    @message
</p>

@code {
    private bool stopPropagation = false;
    private string? message;

    private void StopPropagation() => stopPropagation = true;

    private void EnablePropagation() => stopPropagation = false;

    private void OnSelectParentDiv() =>
        message = $"The parent div was selected. {DateTime.Now}";

    private void OnSelectChildDiv() =>
        message = $"The child div was selected. {DateTime.Now}";
}
```

`EventHandlerExample7.razor`:

```
@page "/event-handler-example-7"

<div>
    <b>stopPropagation</b>: @stopPropagation
</div>

<div>
    <button @onclick="StopPropagation">
        Stop Propagation (stopPropagation = true)
    </button>
    <button @onclick="EnablePropagation">
        Enable Propagation (stopPropagation = false)
    </button>
</div>

<div class="m-1 p-1 border border-primary" @onclick="OnSelectParentDiv">
    <h3>Parent div</h3>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv">
        Child div that never stops propagation to the parent div when 
        selected.
    </div>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv" 
            @onclick:stopPropagation="stopPropagation">
        Child div that stops propagation when selected if 
        <b>stopPropagation</b> is <b>true</b>.
    </div>
</div>

<p>
    @message
</p>

@code {
    private bool stopPropagation = false;
    private string? message;

    private void StopPropagation() => stopPropagation = true;

    private void EnablePropagation() => stopPropagation = false;

    private void OnSelectParentDiv() =>
        message = $"The parent div was selected. {DateTime.Now}";

    private void OnSelectChildDiv() =>
        message = $"The child div was selected. {DateTime.Now}";
}
```

`EventHandlerExample7.razor`:

```
@page "/event-handler-example-7"

<div>
    <b>stopPropagation</b>: @stopPropagation
</div>

<div>
    <button @onclick="StopPropagation">
        Stop Propagation (stopPropagation = true)
    </button>
    <button @onclick="EnablePropagation">
        Enable Propagation (stopPropagation = false)
    </button>
</div>

<div class="m-1 p-1 border border-primary" @onclick="OnSelectParentDiv">
    <h3>Parent div</h3>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv">
        Child div that never stops propagation to the parent div when 
        selected.
    </div>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv" 
            @onclick:stopPropagation="stopPropagation">
        Child div that stops propagation when selected if 
        <b>stopPropagation</b> is <b>true</b>.
    </div>
</div>

<p>
    @message
</p>

@code {
    private bool stopPropagation = false;
    private string message;

    private void StopPropagation() => stopPropagation = true;

    private void EnablePropagation() => stopPropagation = false;

    private void OnSelectParentDiv() =>
        message = $"The parent div was selected. {DateTime.Now}";

    private void OnSelectChildDiv() =>
        message = $"The child div was selected. {DateTime.Now}";
}
```

`EventHandlerExample7.razor`:

```
@page "/event-handler-example-7"

<div>
    <b>stopPropagation</b>: @stopPropagation
</div>

<div>
    <button @onclick="StopPropagation">
        Stop Propagation (stopPropagation = true)
    </button>
    <button @onclick="EnablePropagation">
        Enable Propagation (stopPropagation = false)
    </button>
</div>

<div class="m-1 p-1 border border-primary" @onclick="OnSelectParentDiv">
    <h3>Parent div</h3>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv">
        Child div that never stops propagation to the parent div when 
        selected.
    </div>

    <div class="m-1 p-1 border" @onclick="OnSelectChildDiv" 
            @onclick:stopPropagation="stopPropagation">
        Child div that stops propagation when selected if 
        <b>stopPropagation</b> is <b>true</b>.
    </div>
</div>

<p>
    @message
</p>

@code {
    private bool stopPropagation = false;
    private string message;

    private void StopPropagation() => stopPropagation = true;

    private void EnablePropagation() => stopPropagation = false;

    private void OnSelectParentDiv() =>
        message = $"The parent div was selected. {DateTime.Now}";

    private void OnSelectChildDiv() =>
        message = $"The child div was selected. {DateTime.Now}";
}
```

## Focus an element

Call [FocusAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreferenceextensions.focusasync) on an [element reference](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#capture-references-to-elements) to focus an element in code. In the following example, select the button to focus the `<input>` element.

`EventHandler8.razor`:

```
@page "/event-handler-8"

<PageTitle>Event Handler 8</PageTitle>

<h1>Event Handler Example 8</h1>

<p>Select the button to give the <code><input></code> focus.</p>

<p>
    <label>
        Input: 
        <input @ref="exampleInput" />
    </label>
    
</p>

<button @onclick="ChangeFocus">
    Focus the Input Element
</button>

@code {
    private ElementReference exampleInput;

    private async Task ChangeFocus()
    {
        await exampleInput.FocusAsync();
    }
}
```

`EventHandler8.razor`:

```
@page "/event-handler-8"

<PageTitle>Event Handler 8</PageTitle>

<h1>Event Handler Example 8</h1>

<p>Select the button to give the <code><input></code> focus.</p>

<p>
    <label>
        Input: 
        <input @ref="exampleInput" />
    </label>
    
</p>

<button @onclick="ChangeFocus">
    Focus the Input Element
</button>

@code {
    private ElementReference exampleInput;

    private async Task ChangeFocus()
    {
        await exampleInput.FocusAsync();
    }
}
```

`EventHandlerExample8.razor`:

```
@page "/event-handler-example-8"

<p>
    <input @ref="exampleInput" />
</p>

<button @onclick="ChangeFocus">
    Focus the Input Element
</button>

@code {
    private ElementReference exampleInput;

    private async Task ChangeFocus()
    {
        await exampleInput.FocusAsync();
    }
}
```

`EventHandlerExample8.razor`:

```
@page "/event-handler-example-8"

<p>
    <input @ref="exampleInput" />
</p>

<button @onclick="ChangeFocus">
    Focus the Input Element
</button>

@code {
    private ElementReference exampleInput;

    private async Task ChangeFocus()
    {
        await exampleInput.FocusAsync();
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/razor-pages-conventions
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/razor-pages-conventions.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Razor Pages route and app conventions in ASP.NET Core

Learn how to use page [route and app model provider conventions](../mvc/controllers/application-model?view=aspnetcore-10.0#conventions) to control page routing, discovery, and processing in Razor Pages apps.

To specify a page route, add route segments, or add parameters to a route, use the page's `@page` directive. For more information, see [Custom routes](./?view=aspnetcore-10.0#custom-routes).

There are reserved words that can't be used as route segments or parameter names. For more information, see [Routing: Reserved routing names](../mvc/controllers/routing?view=aspnetcore-10.0#reserved-routing-names).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/razor-pages-conventions/samples/) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

| Scenario                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | The sample demonstrates                                                                                                                                                                               |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Model conventions](#model-conventions) Conventions.Add- [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention)
- [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention)
- [IPageHandlerModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipagehandlermodelconvention)                                                                                                                                                 | Add a route template and header to an app's pages.                                                                                                                                                    |
| [Page route action conventions](#page-route-action-conventions)- [AddFolderRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderroutemodelconvention)
- [AddPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addpageroutemodelconvention)
- [AddPageRoute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.addpageroute)                                                                                    | Add a route template to pages in a folder and to a single page.                                                                                                                                       |
| [Page model action conventions](#page-model-action-conventions)- [AddFolderApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderapplicationmodelconvention)
- [AddPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addpageapplicationmodelconvention)
- [ConfigureFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.configurefilter) (filter class, lambda expression, or filter factory) | Add a header to pages in a folder, add a header to a single page, and configure a [filter factory](../mvc/controllers/filters?view=aspnetcore-10.0#ifilterfactory) to add a header to an app's pages. |

Razor Pages conventions are configured using an [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages) overload that configures [RazorPagesOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions). The following convention examples are explained later in this topic:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
    {
        options.Conventions.Add( ... );
        options.Conventions.AddFolderRouteModelConvention(
            "/OtherPages", model => { ... });
        options.Conventions.AddPageRouteModelConvention(
            "/About", model => { ... });
        options.Conventions.AddPageRoute(
            "/Contact", "TheContactPage/{text?}");
        options.Conventions.AddFolderApplicationModelConvention(
            "/OtherPages", model => { ... });
        options.Conventions.AddPageApplicationModelConvention(
            "/About", model => { ... });
        options.Conventions.ConfigureFilter(model => { ... });
        options.Conventions.ConfigureFilter( ... );
    });
}
```

## Route order

Routes specify an [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) for processing (route matching).

| Route order | Behavior                                                                                                                             |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| -1          | The route is processed before other routes are processed.                                                                            |
| 0           | Order isn't specified (default value). Not assigning `Order` (`Order = null`) defaults the route `Order` to 0 (zero) for processing. |
| 1, 2, … n   | Specifies the route processing order.                                                                                                |

Route processing is established by convention:

* Routes are processed in sequential order (-1, 0, 1, 2, … n).
* When routes have the same `Order`, the most specific route is matched first followed by less specific routes.
* When routes with the same `Order` and the same number of parameters match a request URL, routes are processed in the order that they're added to the [PageConventionCollection](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection).

If possible, avoid depending on an established route processing order. Generally, routing selects the correct route with URL matching. If you must set route `Order` properties to route requests correctly, the app's routing scheme is probably confusing to clients and fragile to maintain. Seek to simplify the app's routing scheme. The sample app requires an explicit route processing order to demonstrate several routing scenarios using a single app. However, you should attempt to avoid the practice of setting route `Order` in production apps.

Razor Pages routing and MVC controller routing share an implementation. Information on route order in the MVC topics is available at [Routing to controller actions: Ordering attribute routes](../mvc/controllers/routing?view=aspnetcore-10.0#routing-ordering-ref-label).

## Model conventions

Add a delegate for [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) to add [model conventions](../mvc/controllers/application-model?view=aspnetcore-10.0#conventions) that apply to Razor Pages.

### Add a route model convention to all pages

Use [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions) to create and add an [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention) to the collection of [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) instances that are applied during page route model construction.

The sample app contains the `GlobalTemplatePageRouteModelConvention` class to add a `{globalTemplate?}` route template to all of the pages in the app:

```
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace SampleApp.Conventions;

public class GlobalTemplatePageRouteModelConvention : IPageRouteModelConvention
{
    public void Apply(PageRouteModel model)
    {
        var selectorCount = model.Selectors.Count;
        for (var i = 0; i < selectorCount; i++)
        {
            var selector = model.Selectors[i];
            model.Selectors.Add(new SelectorModel
            {
                AttributeRouteModel = new AttributeRouteModel
                {
                    Order = 1,
                    Template = AttributeRouteModel.CombineTemplates(
                        selector.AttributeRouteModel!.Template, 
                        "{globalTemplate?}"),
                }
            });
        }
    }
}
```

In the preceding code:

* The [PageRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutemodel) is passed to the [Apply](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention.apply) method.
* The [PageRouteModel.Selectors](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutemodel.selectors#microsoft-aspnetcore-mvc-applicationmodels-pageroutemodel-selectors) gets the count of selectors.
* A new [SelectorModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.selectormodel) is added which contains a [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel)

Razor Pages options, such as adding [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions), are added when Razor Pages is added to the service collection. For an example, see the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/razor-pages-conventions/samples/).

```
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.EntityFrameworkCore;
using SampleApp.Conventions;
using SampleApp.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
                                   options.UseInMemoryDatabase("InMemoryDb"));

builder.Services.AddRazorPages(options =>
   {
       options.Conventions.Add(new GlobalTemplatePageRouteModelConvention());

       options.Conventions.AddFolderRouteModelConvention("/OtherPages", model =>
       {
           var selectorCount = model.Selectors.Count;
           for (var i = 0; i < selectorCount; i++)
           {
               var selector = model.Selectors[i];
               model.Selectors.Add(new SelectorModel
               {
                   AttributeRouteModel = new AttributeRouteModel
                   {
                       Order = 2,
                       Template = AttributeRouteModel.CombineTemplates(
                           selector.AttributeRouteModel!.Template,
                           "{otherPagesTemplate?}"),
                   }
               });
           }
       });

       options.Conventions.AddPageRouteModelConvention("/About", model =>
       {
           var selectorCount = model.Selectors.Count;
           for (var i = 0; i < selectorCount; i++)
           {
               var selector = model.Selectors[i];
               model.Selectors.Add(new SelectorModel
               {
                   AttributeRouteModel = new AttributeRouteModel
                   {
                       Order = 2,
                       Template = AttributeRouteModel.CombineTemplates(
                           selector.AttributeRouteModel!.Template,
                           "{aboutTemplate?}"),
                   }
               });
           }
       });

   });

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
```

Consider the `GlobalTemplatePageRouteModelConvention` class:

```
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace SampleApp.Conventions;

public class GlobalTemplatePageRouteModelConvention : IPageRouteModelConvention
{
    public void Apply(PageRouteModel model)
    {
        var selectorCount = model.Selectors.Count;
        for (var i = 0; i < selectorCount; i++)
        {
            var selector = model.Selectors[i];
            model.Selectors.Add(new SelectorModel
            {
                AttributeRouteModel = new AttributeRouteModel
                {
                    Order = 1,
                    Template = AttributeRouteModel.CombineTemplates(
                        selector.AttributeRouteModel!.Template, 
                        "{globalTemplate?}"),
                }
            });
        }
    }
}
```

The [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) property for the [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel) is set to `1`. This ensures the following route matching behavior in the sample app:

* A route template for `TheContactPage/{text?}` is added later in this topic. The `Contact Page` route has a default order of `null` (`Order = 0`), so it matches before the `{globalTemplate?}` route template which has `Order = 1`.

* The `{aboutTemplate?}` route template is show in the preceding code. The `{aboutTemplate?}` template is given an `Order` of `2`. When the About page is requested at `/About/RouteDataValue`, "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["aboutTemplate"]` (`Order = 2`) due to setting the `Order` property.

* The `{otherPagesTemplate?}` route template is shown in the preceding code. The `{otherPagesTemplate?}` template is given an `Order` of `2`. When any page in the *Pages/OtherPages* folder is requested with a route parameter:

* For example, `/OtherPages/Page1/xyz`

* The route data value `"xyz"` is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`).

* `RouteData.Values["otherPagesTemplate"]` with (`Order = 2`) is not loaded due to the `Order` property `2` having a higher value.

**When possible, don't set the `Order`**. When `Order` is not set, it defaults to `Order = 0`. Rely on routing to select the correct route rather than the `Order` property.

Request the sample's `About` page at `localhost:{port}/About/GlobalRouteValue` and inspect the result:

The sample app uses the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package to display routing information in the logging output. Using `localhost:{port}/About/GlobalRouteValue`, the logger displays the request, the `Order`, and the template used:

```
info: SampleApp.Pages.AboutModel[0]
       /About/GlobalRouteValue   Order = 1 Template = About/{globalTemplate?}
```

### Add an app model convention to all pages

Use [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions) to create and add an [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention) to the collection of [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) instances that are applied during page app model construction.

To demonstrate this and other conventions later in the topic, the sample app includes an `AddHeaderAttribute` class. The class constructor accepts a `name` string and a `values` string array. These values are used in its `OnResultExecuting` method to set a response header. The full class is shown in the [Page model action conventions](#page-model-action-conventions) section later in the topic.

The sample app uses the `AddHeaderAttribute` class to add a header, `GlobalHeader`, to all of the pages in the app:

```
public class GlobalHeaderPageApplicationModelConvention 
    : IPageApplicationModelConvention
{
    public void Apply(PageApplicationModel model)
    {
        model.Filters.Add(new AddHeaderAttribute(
            "GlobalHeader", new string[] { "Global Header Value" }));
    }
}
```

`Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseInMemoryDatabase("InMemoryDb"));

builder.Services.AddRazorPages(options =>
   {
       options.Conventions.Add(new GlobalTemplatePageRouteModelConvention());

       options.Conventions.Add(new GlobalHeaderPageApplicationModelConvention());
```

Request the sample's About page at `localhost:{port}/About` and inspect the headers to view the result:

### Add a handler model convention to all pages

Use [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions) to create and add an [IPageHandlerModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipagehandlermodelconvention) to the collection of [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) instances that are applied during page handler model construction.

```
public class GlobalPageHandlerModelConvention
    : IPageHandlerModelConvention
{
    public void Apply(PageHandlerModel model)
    {
        // Access the PageHandlerModel
    }
}
```

## Page route action conventions

The default route model provider that derives from [IPageRouteModelProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelprovider) invokes conventions which are designed to provide extensibility points for configuring page routes.

### Folder route model convention

Use [AddFolderRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderroutemodelconvention) to create and add an [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention) that invokes an action on the [PageRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutemodel) for all of the pages under the specified folder.

The sample app uses [AddFolderRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderroutemodelconvention) to add an `{otherPagesTemplate?}` route template to the pages in the *OtherPages* folder:

```
options.Conventions.AddFolderRouteModelConvention("/OtherPages", model =>
{
    var selectorCount = model.Selectors.Count;
    for (var i = 0; i < selectorCount; i++)
    {
        var selector = model.Selectors[i];
        model.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel
            {
                Order = 2,
                Template = AttributeRouteModel.CombineTemplates(
                    selector.AttributeRouteModel!.Template,
                    "{otherPagesTemplate?}"),
            }
        });
    }
});
```

The [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) property for the [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel) is set to `2`. This ensures that the template for `{globalTemplate?}` (set earlier in the topic to `1`) is given priority for the first route data value position when a single route value is provided. If a page in the *Pages/OtherPages* folder is requested with a route parameter value (for example, `/OtherPages/Page1/RouteDataValue`), "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["otherPagesTemplate"]` (`Order = 2`) due to setting the `Order` property.

Wherever possible, don't set the `Order`, which results in `Order = 0`. Rely on routing to select the correct route.

Request the sample's Page1 page at `localhost:5000/OtherPages/Page1/GlobalRouteValue/OtherPagesRouteValue` and inspect the result:

### Page route model convention

Use [AddPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addpageroutemodelconvention) to create and add an [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention) that invokes an action on the [PageRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutemodel) for the page with the specified name.

The sample app uses `AddPageRouteModelConvention` to add an `{aboutTemplate?}` route template to the About page:

```
options.Conventions.AddPageRouteModelConvention("/About", model =>
{
    var selectorCount = model.Selectors.Count;
    for (var i = 0; i < selectorCount; i++)
    {
        var selector = model.Selectors[i];
        model.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel
            {
                Order = 2,
                Template = AttributeRouteModel.CombineTemplates(
                    selector.AttributeRouteModel!.Template,
                    "{aboutTemplate?}"),
            }
        });
    }
});
```

The [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) property for the [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel) is set to `2`. This ensures that the template for `{globalTemplate?}` (set earlier in the topic to `1`) is given priority for the first route data value position when a single route value is provided. If the About page is requested with a route parameter value at `/About/RouteDataValue`, "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["aboutTemplate"]` (`Order = 2`) due to setting the `Order` property.

Wherever possible, don't set the `Order`, which results in `Order = 0`. Rely on routing to select the correct route.

Request the sample's About page at `localhost:{port}/About/GlobalRouteValue/AboutRouteValue` and inspect the result:

The logger output displays:

```
info: SampleApp.Pages.AboutModel[0]
       /About/GlobalRouteValue/AboutRouteValue   Order = 2 Template = About/{globalTemplate?}/{aboutTemplate?}
```

## Use a parameter transformer to customize page routes

See [Parameter transformers](../fundamentals/routing?view=aspnetcore-10.0#parameter-transformers).

## Configure a page route

Use [AddPageRoute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.addpageroute) to configure a route to a page at the specified page path. Generated links to the page use the specified route. [AddPageRoute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.addpageroute) uses [AddPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addpageroutemodelconvention) to establish the route.

The sample app creates a route to `/TheContactPage` for the `Contact` Razor Page:

```
options.Conventions.AddPageRoute("/Contact", "TheContactPage/{text?}");
```

The `Contact` page can also be reached at `/`Contact1\` via its default route.

The sample app's custom route to the `Contact` page allows for an optional `text` route segment (`{text?}`). The page also includes this optional segment in its `@page` directive in case the visitor accesses the page at its `/Contact` route:

```
@page "{text?}"
@model ContactModel
@{
    ViewData["Title"] = "Contact";
}

<h1>@ViewData["Title"]</h1>
<h2>@Model.Message</h2>

<address>
    One Microsoft Way<br>
    Redmond, WA 98052-6399<br>
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>

<address>
    <strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br>
    <strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>

<p>@Model.RouteDataTextTemplateValue</p>
```

Note that the URL generated for the **Contact** link in the rendered page reflects the updated route:

Visit the `Contact` page at either its ordinary route, `/Contact`, or the custom route, `/TheContactPage`. If you supply an additional `text` route segment, the page shows the HTML-encoded segment that you provide:

## Page model action conventions

The default page model provider that implements [IPageApplicationModelProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelprovider) invokes conventions which are designed to provide extensibility points for configuring page models. These conventions are useful when building and modifying page discovery and processing scenarios.

For the examples in this section, the sample app uses an `AddHeaderAttribute` class, which is a [ResultFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultfilterattribute), that applies a response header:

```
public class AddHeaderAttribute : ResultFilterAttribute
{
    private readonly string _name;
    private readonly string[] _values;

    public AddHeaderAttribute(string name, string[] values)
    {
        _name = name;
        _values = values;
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers.Add(_name, _values);
        base.OnResultExecuting(context);
    }
}
```

Using conventions, the sample demonstrates how to apply the attribute to all of the pages in a folder and to a single page.

**Folder app model convention**

Use [AddFolderApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderapplicationmodelconvention) to create and add an [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention) that invokes an action on [PageApplicationModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageapplicationmodel) instances for all pages under the specified folder.

The sample demonstrates the use of `AddFolderApplicationModelConvention` by adding a header, `OtherPagesHeader`, to the pages inside the *OtherPages* folder of the app:

```
options.Conventions.AddFolderApplicationModelConvention("/OtherPages", model =>
{
    model.Filters.Add(new AddHeaderAttribute(
        "OtherPagesHeader", new string[] { "OtherPages Header Value" }));
});
```

Request the sample's Page1 page at `localhost:5000/OtherPages/Page1` and inspect the headers to view the result:

**Page app model convention**

Use [AddPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addpageapplicationmodelconvention) to create and add an [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention) that invokes an action on the [PageApplicationModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageapplicationmodel) for the page with the specified name.

The sample demonstrates the use of `AddPageApplicationModelConvention` by adding a header, `AboutHeader`, to the About page:

```
options.Conventions.AddPageApplicationModelConvention("/About", model =>
{
    model.Filters.Add(new AddHeaderAttribute(
        "AboutHeader", new string[] { "About Header Value" }));
});
```

Request the sample's About page at `localhost:5000/About` and inspect the headers to view the result:

**Configure a filter**

[ConfigureFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.configurefilter) configures the specified filter to apply. You can implement a filter class, but the sample app shows how to implement a filter in a lambda expression, which is implemented behind-the-scenes as a factory that returns a filter:

```
options.Conventions.ConfigureFilter(model =>
{
    if (model.RelativePath.Contains("OtherPages/Page2"))
    {
        return new AddHeaderAttribute(
            "OtherPagesPage2Header",
            new string[] { "OtherPages/Page2 Header Value" });
    }
    return new EmptyFilter();
});
```

The page app model is used to check the relative path for segments that lead to the Page2 page in the *OtherPages* folder. If the condition passes, a header is added. If not, the `EmptyFilter` is applied.

`EmptyFilter` is an [Action filter](../mvc/controllers/filters?view=aspnetcore-10.0#action-filters). Since Action filters are ignored by Razor Pages, the `EmptyFilter` has no effect as intended if the path doesn't contain `OtherPages/Page2`.

Request the sample's Page2 page at `localhost:5000/OtherPages/Page2` and inspect the headers to view the result:

**Configure a filter factory**

[ConfigureFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.configurefilter) configures the specified factory to apply [filters](../mvc/controllers/filters?view=aspnetcore-10.0) to all Razor Pages.

The sample app provides an example of using a [filter factory](../mvc/controllers/filters?view=aspnetcore-10.0#ifilterfactory) by adding a header, `FilterFactoryHeader`, with two values to the app's pages:

```
options.Conventions.ConfigureFilter(new AddHeaderWithFactory());
```

`AddHeaderWithFactory.cs`:

```
public class AddHeaderWithFactory : IFilterFactory
{
    // Implement IFilterFactory
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        return new AddHeaderFilter();
    }

    private class AddHeaderFilter : IResultFilter
    {
        public void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(
                "FilterFactoryHeader", 
                new string[] 
                { 
                    "Filter Factory Header Value 1",
                    "Filter Factory Header Value 2"
                });
        }

        public void OnResultExecuted(ResultExecutedContext context)
        {
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}
```

Request the sample's About page at `localhost:5000/About` and inspect the headers to view the result:

## MVC Filters and the Page filter (IPageFilter)

MVC [Action filters](../mvc/controllers/filters?view=aspnetcore-10.0#action-filters) are ignored by Razor Pages, since Razor Pages use handler methods. Other types of MVC filters are available for you to use: [Authorization](../mvc/controllers/filters?view=aspnetcore-10.0#authorization-filters), [Exception](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters), [Resource](../mvc/controllers/filters?view=aspnetcore-10.0#resource-filters), and [Result](../mvc/controllers/filters?view=aspnetcore-10.0#result-filters). For more information, see the [Filters](../mvc/controllers/filters?view=aspnetcore-10.0) topic.

The Page filter ([IPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter)) is a filter that applies to Razor Pages. For more information, see [Filter methods for Razor Pages](filter?view=aspnetcore-10.0).

## Additional resources

* [Razor Pages Routing](https://www.learnrazorpages.com/razor-pages/routing)
* [Razor Pages authorization conventions in ASP.NET Core](security/authorization/conventions?view=aspnetcore-10.0)
* [Areas in ASP.NET Core](../mvc/controllers/areas?view=aspnetcore-10.0#areas-with-razor-pages)

Learn how to use page [route and app model provider conventions](../mvc/controllers/application-model?view=aspnetcore-10.0#conventions) to control page routing, discovery, and processing in Razor Pages apps.

When you need to configure custom page routes for individual pages, configure routing to pages with the [AddPageRoute convention](#configure-a-page-route) described later in this topic.

To specify a page route, add route segments, or add parameters to a route, use the page's `@page` directive. For more information, see [Custom routes](./?view=aspnetcore-10.0#custom-routes).

There are reserved words that can't be used as route segments or parameter names. For more information, see [Routing: Reserved routing names](../mvc/controllers/routing?view=aspnetcore-10.0#reserved-routing-names).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/razor-pages-conventions/samples/) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

| Scenario                                                                                                                                                                                                        | The sample demonstrates ...                                                                                                                                                                           |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Model conventions](#model-conventions) Conventions.Add- IPageRouteModelConvention
- IPageApplicationModelConvention
- IPageHandlerModelConvention                                                              | Add a route template and header to an app's pages.                                                                                                                                                    |
| [Page route action conventions](#page-route-action-conventions)- AddFolderRouteModelConvention
- AddPageRouteModelConvention
- AddPageRoute                                                                     | Add a route template to pages in a folder and to a single page.                                                                                                                                       |
| [Page model action conventions](#page-model-action-conventions)- AddFolderApplicationModelConvention
- AddPageApplicationModelConvention
- ConfigureFilter (filter class, lambda expression, or filter factory) | Add a header to pages in a folder, add a header to a single page, and configure a [filter factory](../mvc/controllers/filters?view=aspnetcore-10.0#ifilterfactory) to add a header to an app's pages. |

Razor Pages conventions are configured using an [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages) overload that configures [RazorPagesOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions) in `Startup.ConfigureServices`. The following convention examples are explained later in this topic:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages(options =>
    {
        options.Conventions.Add( ... );
        options.Conventions.AddFolderRouteModelConvention(
            "/OtherPages", model => { ... });
        options.Conventions.AddPageRouteModelConvention(
            "/About", model => { ... });
        options.Conventions.AddPageRoute(
            "/Contact", "TheContactPage/{text?}");
        options.Conventions.AddFolderApplicationModelConvention(
            "/OtherPages", model => { ... });
        options.Conventions.AddPageApplicationModelConvention(
            "/About", model => { ... });
        options.Conventions.ConfigureFilter(model => { ... });
        options.Conventions.ConfigureFilter( ... );
    });
}
```

## Route order

Routes specify an [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) for processing (route matching).

| Order     | Behavior                                                                                                                             |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| -1        | The route is processed before other routes are processed.                                                                            |
| 0         | Order isn't specified (default value). Not assigning `Order` (`Order = null`) defaults the route `Order` to 0 (zero) for processing. |
| 1, 2, … n | Specifies the route processing order.                                                                                                |

Route processing is established by convention:

* Routes are processed in sequential order (-1, 0, 1, 2, … n).
* When routes have the same `Order`, the most specific route is matched first followed by less specific routes.
* When routes with the same `Order` and the same number of parameters match a request URL, routes are processed in the order that they're added to the [PageConventionCollection](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection).

If possible, avoid depending on an established route processing order. Generally, routing selects the correct route with URL matching. If you must set route `Order` properties to route requests correctly, the app's routing scheme is probably confusing to clients and fragile to maintain. Seek to simplify the app's routing scheme. The sample app requires an explicit route processing order to demonstrate several routing scenarios using a single app. However, you should attempt to avoid the practice of setting route `Order` in production apps.

Razor Pages routing and MVC controller routing share an implementation. Information on route order in the MVC topics is available at [Routing to controller actions: Ordering attribute routes](../mvc/controllers/routing?view=aspnetcore-10.0#routing-ordering-ref-label).

## Model conventions

Add a delegate for [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) to add [model conventions](../mvc/controllers/application-model?view=aspnetcore-10.0#conventions) that apply to Razor Pages.

### Add a route model convention to all pages

Use [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions) to create and add an [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention) to the collection of [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) instances that are applied during page route model construction.

The sample app adds a `{globalTemplate?}` route template to all of the pages in the app:

```
public class GlobalTemplatePageRouteModelConvention 
    : IPageRouteModelConvention
{
    public void Apply(PageRouteModel model)
    {
        var selectorCount = model.Selectors.Count;
        for (var i = 0; i < selectorCount; i++)
        {
            var selector = model.Selectors[i];
            model.Selectors.Add(new SelectorModel
            {
                AttributeRouteModel = new AttributeRouteModel
                {
                    Order = 1,
                    Template = AttributeRouteModel.CombineTemplates(
                        selector.AttributeRouteModel.Template, 
                        "{globalTemplate?}"),
                }
            });
        }
    }
}
```

The [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) property for the [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel) is set to `1`. This ensures the following route matching behavior in the sample app:

* A route template for `TheContactPage/{text?}` is added later in the topic. The Contact Page route has a default order of `null` (`Order = 0`), so it matches before the `{globalTemplate?}` route template.
* An `{aboutTemplate?}` route template is added later in the topic. The `{aboutTemplate?}` template is given an `Order` of `2`. When the About page is requested at `/About/RouteDataValue`, "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["aboutTemplate"]` (`Order = 2`) due to setting the `Order` property.
* An `{otherPagesTemplate?}` route template is added later in the topic. The `{otherPagesTemplate?}` template is given an `Order` of `2`. When any page in the *Pages/OtherPages* folder is requested with a route parameter (for example, `/OtherPages/Page1/RouteDataValue`), "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["otherPagesTemplate"]` (`Order = 2`) due to setting the `Order` property.

Wherever possible, don't set the `Order`, which results in `Order = 0`. Rely on routing to select the correct route.

Razor Pages options, such as adding [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions), are added when Razor Pages is added to the service collection in `Startup.ConfigureServices`. For an example, see the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/razor-pages-conventions/samples/).

```
options.Conventions.Add(new GlobalTemplatePageRouteModelConvention());
```

Request the sample's About page at `localhost:5000/About/GlobalRouteValue` and inspect the result:

### Add an app model convention to all pages

Use [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions) to create and add an [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention) to the collection of [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) instances that are applied during page app model construction.

To demonstrate this and other conventions later in the topic, the sample app includes an `AddHeaderAttribute` class. The class constructor accepts a `name` string and a `values` string array. These values are used in its `OnResultExecuting` method to set a response header. The full class is shown in the [Page model action conventions](#page-model-action-conventions) section later in the topic.

The sample app uses the `AddHeaderAttribute` class to add a header, `GlobalHeader`, to all of the pages in the app:

```
public class GlobalHeaderPageApplicationModelConvention 
    : IPageApplicationModelConvention
{
    public void Apply(PageApplicationModel model)
    {
        model.Filters.Add(new AddHeaderAttribute(
            "GlobalHeader", new string[] { "Global Header Value" }));
    }
}
```

`Startup.cs`:

```
options.Conventions.Add(new GlobalHeaderPageApplicationModelConvention());
```

Request the sample's About page at `localhost:5000/About` and inspect the headers to view the result:

### Add a handler model convention to all pages

Use [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions) to create and add an [IPageHandlerModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipagehandlermodelconvention) to the collection of [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) instances that are applied during page handler model construction.

```
public class GlobalPageHandlerModelConvention
    : IPageHandlerModelConvention
{
    public void Apply(PageHandlerModel model)
    {
        // Access the PageHandlerModel
    }
}
```

`Startup.cs`:

```
options.Conventions.Add(new GlobalPageHandlerModelConvention());
```

## Page route action conventions

The default route model provider that derives from [IPageRouteModelProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelprovider) invokes conventions which are designed to provide extensibility points for configuring page routes.

### Folder route model convention

Use [AddFolderRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderroutemodelconvention) to create and add an [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention) that invokes an action on the [PageRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutemodel) for all of the pages under the specified folder.

The sample app uses [AddFolderRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderroutemodelconvention) to add an `{otherPagesTemplate?}` route template to the pages in the *OtherPages* folder:

```
options.Conventions.AddFolderRouteModelConvention("/OtherPages", model =>
{
    var selectorCount = model.Selectors.Count;
    for (var i = 0; i < selectorCount; i++)
    {
        var selector = model.Selectors[i];
        model.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel
            {
                Order = 2,
                Template = AttributeRouteModel.CombineTemplates(
                    selector.AttributeRouteModel.Template, 
                    "{otherPagesTemplate?}"),
            }
        });
    }
});
```

The [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) property for the [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel) is set to `2`. This ensures that the template for `{globalTemplate?}` (set earlier in the topic to `1`) is given priority for the first route data value position when a single route value is provided. If a page in the *Pages/OtherPages* folder is requested with a route parameter value (for example, `/OtherPages/Page1/RouteDataValue`), "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["otherPagesTemplate"]` (`Order = 2`) due to setting the `Order` property.

Wherever possible, don't set the `Order`, which results in `Order = 0`. Rely on routing to select the correct route.

Request the sample's Page1 page at `localhost:5000/OtherPages/Page1/GlobalRouteValue/OtherPagesRouteValue` and inspect the result:

### Page route model convention

Use [AddPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addpageroutemodelconvention) to create and add an [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention) that invokes an action on the [PageRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutemodel) for the page with the specified name.

The sample app uses `AddPageRouteModelConvention` to add an `{aboutTemplate?}` route template to the About page:

```
options.Conventions.AddPageRouteModelConvention("/About", model =>
{
    var selectorCount = model.Selectors.Count;
    for (var i = 0; i < selectorCount; i++)
    {
        var selector = model.Selectors[i];
        model.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel
            {
                Order = 2,
                Template = AttributeRouteModel.CombineTemplates(
                    selector.AttributeRouteModel.Template, 
                    "{aboutTemplate?}"),
            }
        });
    }
});
```

The [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) property for the [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel) is set to `2`. This ensures that the template for `{globalTemplate?}` (set earlier in the topic to `1`) is given priority for the first route data value position when a single route value is provided. If the About page is requested with a route parameter value at `/About/RouteDataValue`, "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["aboutTemplate"]` (`Order = 2`) due to setting the `Order` property.

Wherever possible, don't set the `Order`, which results in `Order = 0`. Rely on routing to select the correct route.

Request the sample's About page at `localhost:5000/About/GlobalRouteValue/AboutRouteValue` and inspect the result:

## Use a parameter transformer to customize page routes

Page routes generated by ASP.NET Core can be customized using a parameter transformer. A parameter transformer implements `IOutboundParameterTransformer` and transforms the value of parameters. For example, a custom `SlugifyParameterTransformer` parameter transformer changes the `SubscriptionManagement` route value to `subscription-management`.

The `PageRouteTransformerConvention` page route model convention applies a parameter transformer to the folder and file name segments of automatically generated page routes in an app. For example, the Razor Pages file at `/Pages/SubscriptionManagement/ViewAll.cshtml` would have its route rewritten from `/SubscriptionManagement/ViewAll` to `/subscription-management/view-all`.

`PageRouteTransformerConvention` only transforms the automatically generated segments of a page route that come from the Razor Pages folder and file name. It doesn't transform route segments added with the `@page` directive. The convention also doesn't transform routes added by [AddPageRoute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.addpageroute).

The `PageRouteTransformerConvention` is registered as an option in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages(options =>
    {
        options.Conventions.Add(
            new PageRouteTransformerConvention(
                new SlugifyParameterTransformer()));
    });
}
```

```
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) { return null; }

        return Regex.Replace(value.ToString(),
                             "([a-z])([A-Z])",
                             "$1-$2",
                             RegexOptions.CultureInvariant,
                             TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}
```

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

## Configure a page route

Use [AddPageRoute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.addpageroute) to configure a route to a page at the specified page path. Generated links to the page use your specified route. `AddPageRoute` uses `AddPageRouteModelConvention` to establish the route.

The sample app creates a route to `/TheContactPage` for `Contact.cshtml`:

```
options.Conventions.AddPageRoute("/Contact", "TheContactPage/{text?}");
```

The Contact page can also be reached at `/Contact` via its default route.

The sample app's custom route to the Contact page allows for an optional `text` route segment (`{text?}`). The page also includes this optional segment in its `@page` directive in case the visitor accesses the page at its `/Contact` route:

```
@page "{text?}"
@model ContactModel
@{
    ViewData["Title"] = "Contact";
}

<h1>@ViewData["Title"]</h1>
<h2>@Model.Message</h2>

<address>
    One Microsoft Way<br>
    Redmond, WA 98052-6399<br>
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>

<address>
    <strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br>
    <strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>

<p>@Model.RouteDataTextTemplateValue</p>
```

Note that the URL generated for the **Contact** link in the rendered page reflects the updated route:

Visit the Contact page at either its ordinary route, `/Contact`, or the custom route, `/TheContactPage`. If you supply an additional `text` route segment, the page shows the HTML-encoded segment that you provide:

## Page model action conventions

The default page model provider that implements [IPageApplicationModelProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelprovider) invokes conventions which are designed to provide extensibility points for configuring page models. These conventions are useful when building and modifying page discovery and processing scenarios.

For the examples in this section, the sample app uses an `AddHeaderAttribute` class, which is a [ResultFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultfilterattribute), that applies a response header:

```
public class AddHeaderAttribute : ResultFilterAttribute
{
    private readonly string _name;
    private readonly string[] _values;

    public AddHeaderAttribute(string name, string[] values)
    {
        _name = name;
        _values = values;
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers.Add(_name, _values);
        base.OnResultExecuting(context);
    }
}
```

Using conventions, the sample demonstrates how to apply the attribute to all of the pages in a folder and to a single page.

**Folder app model convention**

Use [AddFolderApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderapplicationmodelconvention) to create and add an [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention) that invokes an action on [PageApplicationModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageapplicationmodel) instances for all pages under the specified folder.

The sample demonstrates the use of `AddFolderApplicationModelConvention` by adding a header, `OtherPagesHeader`, to the pages inside the *OtherPages* folder of the app:

```
options.Conventions.AddFolderApplicationModelConvention("/OtherPages", model =>
{
    model.Filters.Add(new AddHeaderAttribute(
        "OtherPagesHeader", new string[] { "OtherPages Header Value" }));
});
```

Request the sample's Page1 page at `localhost:5000/OtherPages/Page1` and inspect the headers to view the result:

**Page app model convention**

Use [AddPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addpageapplicationmodelconvention) to create and add an [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention) that invokes an action on the [PageApplicationModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageapplicationmodel) for the page with the specified name.

The sample demonstrates the use of `AddPageApplicationModelConvention` by adding a header, `AboutHeader`, to the About page:

```
options.Conventions.AddPageApplicationModelConvention("/About", model =>
{
    model.Filters.Add(new AddHeaderAttribute(
        "AboutHeader", new string[] { "About Header Value" }));
});
```

Request the sample's About page at `localhost:5000/About` and inspect the headers to view the result:

**Configure a filter**

[ConfigureFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.configurefilter) configures the specified filter to apply. You can implement a filter class, but the sample app shows how to implement a filter in a lambda expression, which is implemented behind-the-scenes as a factory that returns a filter:

```
options.Conventions.ConfigureFilter(model =>
{
    if (model.RelativePath.Contains("OtherPages/Page2"))
    {
        return new AddHeaderAttribute(
            "OtherPagesPage2Header", 
            new string[] { "OtherPages/Page2 Header Value" });
    }
    return new EmptyFilter();
});
```

The page app model is used to check the relative path for segments that lead to the Page2 page in the *OtherPages* folder. If the condition passes, a header is added. If not, the `EmptyFilter` is applied.

`EmptyFilter` is an [Action filter](../mvc/controllers/filters?view=aspnetcore-10.0#action-filters). Since Action filters are ignored by Razor Pages, the `EmptyFilter` has no effect as intended if the path doesn't contain `OtherPages/Page2`.

Request the sample's Page2 page at `localhost:5000/OtherPages/Page2` and inspect the headers to view the result:

**Configure a filter factory**

[ConfigureFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.configurefilter) configures the specified factory to apply [filters](../mvc/controllers/filters?view=aspnetcore-10.0) to all Razor Pages.

The sample app provides an example of using a [filter factory](../mvc/controllers/filters?view=aspnetcore-10.0#ifilterfactory) by adding a header, `FilterFactoryHeader`, with two values to the app's pages:

```
options.Conventions.ConfigureFilter(new AddHeaderWithFactory());
```

`AddHeaderWithFactory.cs`:

```
public class AddHeaderWithFactory : IFilterFactory
{
    // Implement IFilterFactory
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        return new AddHeaderFilter();
    }

    private class AddHeaderFilter : IResultFilter
    {
        public void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(
                "FilterFactoryHeader", 
                new string[] 
                { 
                    "Filter Factory Header Value 1",
                    "Filter Factory Header Value 2"
                });
        }

        public void OnResultExecuted(ResultExecutedContext context)
        {
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}
```

Request the sample's About page at `localhost:5000/About` and inspect the headers to view the result:

## MVC Filters and the Page filter (IPageFilter)

MVC [Action filters](../mvc/controllers/filters?view=aspnetcore-10.0#action-filters) are ignored by Razor Pages, since Razor Pages use handler methods. Other types of MVC filters are available for you to use: [Authorization](../mvc/controllers/filters?view=aspnetcore-10.0#authorization-filters), [Exception](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters), [Resource](../mvc/controllers/filters?view=aspnetcore-10.0#resource-filters), and [Result](../mvc/controllers/filters?view=aspnetcore-10.0#result-filters). For more information, see the [Filters](../mvc/controllers/filters?view=aspnetcore-10.0) topic.

The Page filter ([IPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter)) is a filter that applies to Razor Pages. For more information, see [Filter methods for Razor Pages](filter?view=aspnetcore-10.0).

## Additional resources

* [Razor Pages authorization conventions in ASP.NET Core](security/authorization/conventions?view=aspnetcore-10.0)
* [Areas in ASP.NET Core](../mvc/controllers/areas?view=aspnetcore-10.0#areas-with-razor-pages)

Learn how to use page [route and app model provider conventions](../mvc/controllers/application-model?view=aspnetcore-10.0#conventions) to control page routing, discovery, and processing in Razor Pages apps.

When you need to configure custom page routes for individual pages, configure routing to pages with the [AddPageRoute convention](#configure-a-page-route) described later in this topic.

To specify a page route, add route segments, or add parameters to a route, use the page's `@page` directive. For more information, see [Custom routes](./?view=aspnetcore-10.0#custom-routes).

There are reserved words that can't be used as route segments or parameter names. For more information, see [Routing: Reserved routing names](../fundamentals/routing?view=aspnetcore-10.0#reserved-routing-names).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/razor-pages-conventions/samples/) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

| Scenario                                                                                                                                                                                                        | The sample demonstrates ...                                                                                                                                                                           |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Model conventions](#model-conventions) Conventions.Add- IPageRouteModelConvention
- IPageApplicationModelConvention
- IPageHandlerModelConvention                                                              | Add a route template and header to an app's pages.                                                                                                                                                    |
| [Page route action conventions](#page-route-action-conventions)- AddFolderRouteModelConvention
- AddPageRouteModelConvention
- AddPageRoute                                                                     | Add a route template to pages in a folder and to a single page.                                                                                                                                       |
| [Page model action conventions](#page-model-action-conventions)- AddFolderApplicationModelConvention
- AddPageApplicationModelConvention
- ConfigureFilter (filter class, lambda expression, or filter factory) | Add a header to pages in a folder, add a header to a single page, and configure a [filter factory](../mvc/controllers/filters?view=aspnetcore-10.0#ifilterfactory) to add a header to an app's pages. |

Razor Pages conventions are added and configured using the [AddRazorPagesOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcrazorpagesmvcbuilderextensions.addrazorpagesoptions) extension method to [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc) on the service collection in the `Startup` class. The following convention examples are explained later in this topic:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddRazorPagesOptions(options =>
        {
            options.Conventions.Add( ... );
            options.Conventions.AddFolderRouteModelConvention(
                "/OtherPages", model => { ... });
            options.Conventions.AddPageRouteModelConvention(
                "/About", model => { ... });
            options.Conventions.AddPageRoute(
                "/Contact", "TheContactPage/{text?}");
            options.Conventions.AddFolderApplicationModelConvention(
                "/OtherPages", model => { ... });
            options.Conventions.AddPageApplicationModelConvention(
                "/About", model => { ... });
            options.Conventions.ConfigureFilter(model => { ... });
            options.Conventions.ConfigureFilter( ... );
        });
}
```

## Route order

Routes specify an [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) for processing (route matching).

| Order     | Behavior                                                                                                                             |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| -1        | The route is processed before other routes are processed.                                                                            |
| 0         | Order isn't specified (default value). Not assigning `Order` (`Order = null`) defaults the route `Order` to 0 (zero) for processing. |
| 1, 2, … n | Specifies the route processing order.                                                                                                |

Route processing is established by convention:

* Routes are processed in sequential order (-1, 0, 1, 2, … n).
* When routes have the same `Order`, the most specific route is matched first followed by less specific routes.
* When routes with the same `Order` and the same number of parameters match a request URL, routes are processed in the order that they're added to the [PageConventionCollection](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection).

If possible, avoid depending on an established route processing order. Generally, routing selects the correct route with URL matching. If you must set route `Order` properties to route requests correctly, the app's routing scheme is probably confusing to clients and fragile to maintain. Seek to simplify the app's routing scheme. The sample app requires an explicit route processing order to demonstrate several routing scenarios using a single app. However, you should attempt to avoid the practice of setting route `Order` in production apps.

Razor Pages routing and MVC controller routing share an implementation. Information on route order in the MVC topics is available at [Routing to controller actions: Ordering attribute routes](../mvc/controllers/routing?view=aspnetcore-10.0#routing-ordering-ref-label).

## Model conventions

Add a delegate for [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) to add [model conventions](../mvc/controllers/application-model?view=aspnetcore-10.0#conventions) that apply to Razor Pages.

### Add a route model convention to all pages

Use [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions) to create and add an [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention) to the collection of [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) instances that are applied during page route model construction.

The sample app adds a `{globalTemplate?}` route template to all of the pages in the app:

```
public class GlobalTemplatePageRouteModelConvention 
    : IPageRouteModelConvention
{
    public void Apply(PageRouteModel model)
    {
        var selectorCount = model.Selectors.Count;
        for (var i = 0; i < selectorCount; i++)
        {
            var selector = model.Selectors[i];
            model.Selectors.Add(new SelectorModel
            {
                AttributeRouteModel = new AttributeRouteModel
                {
                    Order = 1,
                    Template = AttributeRouteModel.CombineTemplates(
                        selector.AttributeRouteModel.Template, 
                        "{globalTemplate?}"),
                }
            });
        }
    }
}
```

The [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) property for the [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel) is set to `1`. This ensures the following route matching behavior in the sample app:

* A route template for `TheContactPage/{text?}` is added later in the topic. The Contact Page route has a default order of `null` (`Order = 0`), so it matches before the `{globalTemplate?}` route template.
* An `{aboutTemplate?}` route template is added later in the topic. The `{aboutTemplate?}` template is given an `Order` of `2`. When the About page is requested at `/About/RouteDataValue`, "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["aboutTemplate"]` (`Order = 2`) due to setting the `Order` property.
* An `{otherPagesTemplate?}` route template is added later in the topic. The `{otherPagesTemplate?}` template is given an `Order` of `2`. When any page in the *Pages/OtherPages* folder is requested with a route parameter (for example, `/OtherPages/Page1/RouteDataValue`), "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["otherPagesTemplate"]` (`Order = 2`) due to setting the `Order` property.

Wherever possible, don't set the `Order`, which results in `Order = 0`. Rely on routing to select the correct route.

Razor Pages options, such as adding [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions), are added when MVC is added to the service collection in `Startup.ConfigureServices`. For an example, see the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/razor-pages-conventions/samples/).

```
options.Conventions.Add(new GlobalTemplatePageRouteModelConvention());
```

Request the sample's About page at `localhost:5000/About/GlobalRouteValue` and inspect the result:

### Add an app model convention to all pages

Use [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions) to create and add an [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention) to the collection of [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) instances that are applied during page app model construction.

To demonstrate this and other conventions later in the topic, the sample app includes an `AddHeaderAttribute` class. The class constructor accepts a `name` string and a `values` string array. These values are used in its `OnResultExecuting` method to set a response header. The full class is shown in the [Page model action conventions](#page-model-action-conventions) section later in the topic.

The sample app uses the `AddHeaderAttribute` class to add a header, `GlobalHeader`, to all of the pages in the app:

```
public class GlobalHeaderPageApplicationModelConvention 
    : IPageApplicationModelConvention
{
    public void Apply(PageApplicationModel model)
    {
        model.Filters.Add(new AddHeaderAttribute(
            "GlobalHeader", new string[] { "Global Header Value" }));
    }
}
```

`Startup.cs`:

```
options.Conventions.Add(new GlobalHeaderPageApplicationModelConvention());
```

Request the sample's About page at `localhost:5000/About` and inspect the headers to view the result:

### Add a handler model convention to all pages

Use [Conventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions.conventions#microsoft-aspnetcore-mvc-razorpages-razorpagesoptions-conventions) to create and add an [IPageHandlerModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipagehandlermodelconvention) to the collection of [IPageConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageconvention) instances that are applied during page handler model construction.

```
public class GlobalPageHandlerModelConvention
    : IPageHandlerModelConvention
{
    public void Apply(PageHandlerModel model)
    {
        // Access the PageHandlerModel
    }
}
```

`Startup.cs`:

```
options.Conventions.Add(new GlobalPageHandlerModelConvention());
```

## Page route action conventions

The default route model provider that derives from [IPageRouteModelProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelprovider) invokes conventions which are designed to provide extensibility points for configuring page routes.

### Folder route model convention

Use [AddFolderRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderroutemodelconvention) to create and add an [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention) that invokes an action on the [PageRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutemodel) for all of the pages under the specified folder.

The sample app uses [AddFolderRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderroutemodelconvention) to add an `{otherPagesTemplate?}` route template to the pages in the *OtherPages* folder:

```
options.Conventions.AddFolderRouteModelConvention("/OtherPages", model =>
{
    var selectorCount = model.Selectors.Count;
    for (var i = 0; i < selectorCount; i++)
    {
        var selector = model.Selectors[i];
        model.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel
            {
                Order = 2,
                Template = AttributeRouteModel.CombineTemplates(
                    selector.AttributeRouteModel.Template, 
                    "{otherPagesTemplate?}"),
            }
        });
    }
});
```

The [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) property for the [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel) is set to `2`. This ensures that the template for `{globalTemplate?}` (set earlier in the topic to `1`) is given priority for the first route data value position when a single route value is provided. If a page in the *Pages/OtherPages* folder is requested with a route parameter value (for example, `/OtherPages/Page1/RouteDataValue`), "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["otherPagesTemplate"]` (`Order = 2`) due to setting the `Order` property.

Wherever possible, don't set the `Order`, which results in `Order = 0`. Rely on routing to select the correct route.

Request the sample's Page1 page at `localhost:5000/OtherPages/Page1/GlobalRouteValue/OtherPagesRouteValue` and inspect the result:

### Page route model convention

Use [AddPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addpageroutemodelconvention) to create and add an [IPageRouteModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageroutemodelconvention) that invokes an action on the [PageRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutemodel) for the page with the specified name.

The sample app uses `AddPageRouteModelConvention` to add an `{aboutTemplate?}` route template to the About page:

```
options.Conventions.AddPageRouteModelConvention("/About", model =>
{
    var selectorCount = model.Selectors.Count;
    for (var i = 0; i < selectorCount; i++)
    {
        var selector = model.Selectors[i];
        model.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel
            {
                Order = 2,
                Template = AttributeRouteModel.CombineTemplates(
                    selector.AttributeRouteModel.Template, 
                    "{aboutTemplate?}"),
            }
        });
    }
});
```

The [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel.order) property for the [AttributeRouteModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.attributeroutemodel) is set to `2`. This ensures that the template for `{globalTemplate?}` (set earlier in the topic to `1`) is given priority for the first route data value position when a single route value is provided. If the About page is requested with a route parameter value at `/About/RouteDataValue`, "RouteDataValue" is loaded into `RouteData.Values["globalTemplate"]` (`Order = 1`) and not `RouteData.Values["aboutTemplate"]` (`Order = 2`) due to setting the `Order` property.

Wherever possible, don't set the `Order`, which results in `Order = 0`. Rely on routing to select the correct route.

Request the sample's About page at `localhost:5000/About/GlobalRouteValue/AboutRouteValue` and inspect the result:

## Configure a page route

Use [AddPageRoute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.addpageroute) to configure a route to a page at the specified page path. Generated links to the page use your specified route. `AddPageRoute` uses `AddPageRouteModelConvention` to establish the route.

The sample app creates a route to `/TheContactPage` for `Contact.cshtml`:

```
options.Conventions.AddPageRoute("/Contact", "TheContactPage/{text?}");
```

The Contact page can also be reached at `/Contact` via its default route.

The sample app's custom route to the Contact page allows for an optional `text` route segment (`{text?}`). The page also includes this optional segment in its `@page` directive in case the visitor accesses the page at its `/Contact` route:

```
@page "{text?}"
@model ContactModel
@{
    ViewData["Title"] = "Contact";
}

<h1>@ViewData["Title"]</h1>
<h2>@Model.Message</h2>

<address>
    One Microsoft Way<br>
    Redmond, WA 98052-6399<br>
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>

<address>
    <strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br>
    <strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>

<p>@Model.RouteDataTextTemplateValue</p>
```

Note that the URL generated for the **Contact** link in the rendered page reflects the updated route:

Visit the Contact page at either its ordinary route, `/Contact`, or the custom route, `/TheContactPage`. If you supply an additional `text` route segment, the page shows the HTML-encoded segment that you provide:

## Page model action conventions

The default page model provider that implements [IPageApplicationModelProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelprovider) invokes conventions which are designed to provide extensibility points for configuring page models. These conventions are useful when building and modifying page discovery and processing scenarios.

For the examples in this section, the sample app uses an `AddHeaderAttribute` class, which is a [ResultFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultfilterattribute), that applies a response header:

```
public class AddHeaderAttribute : ResultFilterAttribute
{
    private readonly string _name;
    private readonly string[] _values;

    public AddHeaderAttribute(string name, string[] values)
    {
        _name = name;
        _values = values;
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers.Add(_name, _values);
        base.OnResultExecuting(context);
    }
}
```

Using conventions, the sample demonstrates how to apply the attribute to all of the pages in a folder and to a single page.

**Folder app model convention**

Use [AddFolderApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderapplicationmodelconvention) to create and add an [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention) that invokes an action on [PageApplicationModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageapplicationmodel) instances for all pages under the specified folder.

The sample demonstrates the use of `AddFolderApplicationModelConvention` by adding a header, `OtherPagesHeader`, to the pages inside the *OtherPages* folder of the app:

```
options.Conventions.AddFolderApplicationModelConvention("/OtherPages", model =>
{
    model.Filters.Add(new AddHeaderAttribute(
        "OtherPagesHeader", new string[] { "OtherPages Header Value" }));
});
```

Request the sample's Page1 page at `localhost:5000/OtherPages/Page1` and inspect the headers to view the result:

**Page app model convention**

Use [AddPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addpageapplicationmodelconvention) to create and add an [IPageApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.ipageapplicationmodelconvention) that invokes an action on the [PageApplicationModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageapplicationmodel) for the page with the specified name.

The sample demonstrates the use of `AddPageApplicationModelConvention` by adding a header, `AboutHeader`, to the About page:

```
options.Conventions.AddPageApplicationModelConvention("/About", model =>
{
    model.Filters.Add(new AddHeaderAttribute(
        "AboutHeader", new string[] { "About Header Value" }));
});
```

Request the sample's About page at `localhost:5000/About` and inspect the headers to view the result:

**Configure a filter**

[ConfigureFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.configurefilter) configures the specified filter to apply. You can implement a filter class, but the sample app shows how to implement a filter in a lambda expression, which is implemented behind-the-scenes as a factory that returns a filter:

```
options.Conventions.ConfigureFilter(model =>
{
    if (model.RelativePath.Contains("OtherPages/Page2"))
    {
        return new AddHeaderAttribute(
            "OtherPagesPage2Header", 
            new string[] { "OtherPages/Page2 Header Value" });
    }
    return new EmptyFilter();
});
```

The page app model is used to check the relative path for segments that lead to the Page2 page in the *OtherPages* folder. If the condition passes, a header is added. If not, the `EmptyFilter` is applied.

`EmptyFilter` is an [Action filter](../mvc/controllers/filters?view=aspnetcore-10.0#action-filters). Since Action filters are ignored by Razor Pages, the `EmptyFilter` has no effect as intended if the path doesn't contain `OtherPages/Page2`.

Request the sample's Page2 page at `localhost:5000/OtherPages/Page2` and inspect the headers to view the result:

**Configure a filter factory**

[ConfigureFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.pageconventioncollectionextensions.configurefilter) configures the specified factory to apply [filters](../mvc/controllers/filters?view=aspnetcore-10.0) to all Razor Pages.

The sample app provides an example of using a [filter factory](../mvc/controllers/filters?view=aspnetcore-10.0#ifilterfactory) by adding a header, `FilterFactoryHeader`, with two values to the app's pages:

```
options.Conventions.ConfigureFilter(new AddHeaderWithFactory());
```

`AddHeaderWithFactory.cs`:

```
public class AddHeaderWithFactory : IFilterFactory
{
    // Implement IFilterFactory
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        return new AddHeaderFilter();
    }

    private class AddHeaderFilter : IResultFilter
    {
        public void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(
                "FilterFactoryHeader", 
                new string[] 
                { 
                    "Filter Factory Header Value 1",
                    "Filter Factory Header Value 2"
                });
        }

        public void OnResultExecuted(ResultExecutedContext context)
        {
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}
```

Request the sample's About page at `localhost:5000/About` and inspect the headers to view the result:

## MVC Filters and the Page filter (IPageFilter)

MVC [Action filters](../mvc/controllers/filters?view=aspnetcore-10.0#action-filters) are ignored by Razor Pages, since Razor Pages use handler methods. Other types of MVC filters are available for you to use: [Authorization](../mvc/controllers/filters?view=aspnetcore-10.0#authorization-filters), [Exception](../mvc/controllers/filters?view=aspnetcore-10.0#exception-filters), [Resource](../mvc/controllers/filters?view=aspnetcore-10.0#resource-filters), and [Result](../mvc/controllers/filters?view=aspnetcore-10.0#result-filters). For more information, see the [Filters](../mvc/controllers/filters?view=aspnetcore-10.0) topic.

The Page filter ([IPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter)) is a filter that applies to Razor Pages. For more information, see [Filter methods for Razor Pages](filter?view=aspnetcore-10.0).

## Additional resources

* [Razor Pages authorization conventions in ASP.NET Core](security/authorization/conventions?view=aspnetcore-10.0)
* [Areas in ASP.NET Core](../mvc/controllers/areas?view=aspnetcore-10.0#areas-with-razor-pages)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/mfa
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/mfa.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Multi-factor authentication in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Damien Bowden](https://github.com/damienbod)

Multi-factor authentication (MFA) is a process in which a user is requested during sign in for more forms of identification. The prompt might request the user to enter a code from a cellphone, use an FIDO2 key, or provide a fingerprint scan. When you require a second form of authentication, security is enhanced. The extra factor isn't easily obtained or duplicated by a cyberattacker.

This article provides an overview of MFA in ASP.NET Core and the recommended authentication flows with examples that show how to:

* Configure MFA for administration pages by using ASP.NET Core Identity.
* Send an MFA sign-in requirement to the OpenID Connect server.
* Force the ASP.NET Core OpenID Connect client to require MFA.

[View or download the sample code](https://github.com/damienbod/AspNetCoreHybridFlowWithApi) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## MFA versus 2FA

MFA requires at least two or more types of proof for an identity. Proof types can be something you know, something you possess, or biometric validation for the user to authenticate.

Two-factor authentication (2FA) is like a subset of MFA. 2FA requires exactly two types of proof, whereas MFA can require two or *more* factors to prove the identity.

2FA is supported by default when using ASP.NET Core Identity. To enable or disable 2FA for a specific user, set the [IdentityUser\<TKey>.TwoFactorEnabled](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser-1.twofactorenabled) property. The ASP.NET Core Identity Default UI includes pages for configuring 2FA.

### MFA TOTP (Time-based One-time Password Algorithm)

MFA with TOTP is supported by default when using ASP.NET Core Identity. This approach can be used together with any compliant authenticator app, including:

* Microsoft Authenticator
* Google Authenticator

For implementation details, see [Enable QR Code generation for TOTP authenticator apps in ASP.NET Core](identity-enable-qrcodes?view=aspnetcore-10.0).

To disable support for MFA TOTP, configure authentication by using the [AddIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentity) method instead of the [AddDefaultIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionuiextensions.adddefaultidentity) method. The `AddDefaultIdentity` method calls the [AddDefaultTokenProviders](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderextensions.adddefaulttokenproviders) method internally, which registers multiple token providers including one for MFA TOTP. To register only specific token providers, call the [AddTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addtokenprovider) method for each required provider. For more information on available token providers, see the ['AddDefaultTokenProviders' reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Core/src/IdentityBuilderExtensions.cs) in the `dotnet/aspnetcore` GitHub repository.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### MFA passkeys/FIDO2 or passwordless

The **passkeys/FIDO2** approach is currently:

* The most secure way to achieve MFA.
* MFA that protects against phishing attacks (also certificate authentication and Windows for business).

ASP.NET Core supports passkeys by using ASP.NET Core Identity. Passkeys/FIDO2 can be used for MFA or passwordless flows. For more information, see [Enable Web Authentication API (WebAuthn) passkeys](passkeys/?view=aspnetcore-10.0).

Microsoft Entra ID provides support for passkeys/FIDO2 and passwordless flows. For more information, see [Authentication methods in Microsoft Entra ID - passkeys (FIDO2)](/en-us/entra/identity/authentication/concept-authentication-passkeys-fido2).

Other forms of passwordless MFA either don't or might not protect against phishing.

### MFA SMS

MFA with SMS increases security massively compared with password authentication (single factor). However, using SMS as a second factor is no longer recommended. Too many known attack vectors exist for this type of implementation.

For more information, see the [NIST Digital Identity Guidelines (Special Publication 800-63B)](https://pages.nist.gov/800-63-3/sp800-63b.html).

## Configure MFA for administration pages with ASP.NET Core Identity

You can force MFA on users to enable access to sensitive pages within an ASP.NET Core Identity app. This approach can be useful for apps where different levels of access exist for different identities. For example, users might be able to view the profile data by using a password sign-in process, but an administrator would be required to use MFA to access the administrative pages.

### Extend the sign-in process with an MFA claim

The example code configuration uses ASP.NET Core with Identity and Razor Pages. The `AddIdentity` method is used instead of `AddDefaultIdentity`, so an `IUserClaimsPrincipalFactory` implementation can be used to add claims to the identity after successful sign in.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../?view=aspnetcore-10.0#secure-authentication-flows).

```
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(
        Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
		options.SignIn.RequireConfirmedAccount = false)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

builder.Services.AddSingleton<IEmailSender, EmailSender>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
    AdditionalUserClaimsPrincipalFactory>();

builder.Services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled", x => x.RequireClaim("amr", "mfa")));

builder.Services.AddRazorPages();
```

The `AdditionalUserClaimsPrincipalFactory` class adds the `amr` claim (Authentication Methods References claim) to the user claims only after a successful sign in. The claim value is read from the database. The claim is added here because the user should only access the higher protected view if the identity is signed in with MFA. If the database view is read from the database directly instead of using the claim, it's possible to access the view without MFA directly after activating the MFA.

```
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : 
        UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("amr", "mfa"));
            }
            else
            {
                claims.Add(new Claim("amr", "pwd"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}
```

Because the Identity service setup changed in the `Startup` class, the layouts of the Identity need to be updated:

* Scaffold the Identity pages into the app.

* Define the layout in the *Identity/Account/Manage/\_Layout.cshtml* file.

  ```
  @{
      Layout = "/Pages/Shared/_Layout.cshtml";
  }
  ```

* Also, assign the layout for all the manage pages from the Identity pages:

  ```
  @{
      Layout = "_Layout.cshtml";
  }
  ```

### Validate the MFA requirement in the administration page

The administration Razor Page validates that the user is signed in via MFA. In the `OnGet` method, the identity is used to access the user claims. The `amr` claim is checked for the value `mfa`. If the identity is missing this claim or is `false`, the page redirects to the Enable MFA page. This action is possible because the user is already signed in, but without MFA.

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = 
                User.Claims.FirstOrDefault(t => t.Type == "amr");

            if (claimTwoFactorEnabled != null && 
                "mfa".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the administrative stuff
            }
            else
            {
                return Redirect(
                    "/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}
```

### UI logic to toggle user sign-in information

An authorization policy was added at startup. The policy requires the `amr` claim with the value `mfa`:

```
services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled",
        x => x.RequireClaim("amr", "mfa")));
```

This policy can then be used in the `_Layout` view to show or hide the **Admin** menu with the warning:

```
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService
```

If the identity is signed in via MFA, the **Admin** menu displays without the tooltip warning.

When the user is signed in without MFA, the **Admin (Not Enabled)** menu displays along with the tooltip that informs the user (explaining the warning).

```
@if (SignInManager.IsSignedIn(User))
{
    @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
               id="tooltip-demo"  
               data-toggle="tooltip" 
               data-placement="bottom" 
               title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
                Admin (Not Enabled)
            </a>
        </li>
    }
}
```

If the user signs in without MFA, the **Admin** option shows the **(Not Enabled)** warning:

When the user selects the **Admin** option, they're redirected to a view where they can enable MFA:

## Send MFA sign-in requirement to OpenID Connect server

The `acr_values` parameter can be used to pass the `mfa` required value from the client to the server in an authentication request.

Note

The `acr_values` parameter needs to be handled on the OpenID Connect server for this approach to work.

### OpenID Connect ASP.NET Core client

The ASP.NET Core Razor Pages OpenID Connect client app uses the `AddOpenIdConnect` method to sign into the OpenID Connect server. The `acr_values` parameter is set with the `mfa` value and sent with the authentication request. The `OpenIdConnectEvents` is used to add this value.

For recommended `acr_values` parameter values, see the [IETF Authentication Method Reference Values](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-amr-values-08) specification.

```
build.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "<OpenID Connect server URL>";
	options.RequireHttpsMetadata = true;
	options.ClientId = "<OpenID Connect client ID>";
	options.ClientSecret = "<>";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
	options.AdditionalAuthorizationParameters.Add("acr_values", "mfa");
});
```

### Example: OpenID Connect Duende IdentityServer server

The following example demonstrates OpenID Connect Duende IdentityServer server with ASP.NET Core Identity. On the OpenID Connect server, which is implemented by using ASP.NET Core Identity with Razor Pages, a new page named `ErrorEnable2FA.cshtml` is created.

The page view:

* Displays if the Identity comes from an app that requires MFA, but the user hasn't activated MFA in Identity.
* Informs the user and adds a link to activate this.

```
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:

<br />

<a href="~/Identity/Account/Manage/TwoFactorAuthentication">Enable MFA</a>
```

In the `Login` method, the `IIdentityServerInteractionService` interface implementation `_interaction` is used to access the OpenID Connect request parameters. The `acr_values` parameter is accessed by using the `AcrValues` property. Because the client sent this with `mfa` set, this can then be checked.

If MFA is required, and the user in ASP.NET Core Identity is enabled with MFA, the sign-in process continues. When the user has no MFA enabled, the user is redirected to the custom view `ErrorEnable2FA.cshtml`. Then ASP.NET Core Identity signs the user in.

The Fido2Store is used to check if the user activated MFA by using a custom FIDO2 Token Provider.

```
public async Task<IActionResult> OnPost()
{
	// check if we are in the context of an authorization request
	var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);

	var requires2Fa = context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

	var user = await _userManager.FindByNameAsync(Input.Username);
	if (user != null && !user.TwoFactorEnabled && requires2Fa)
	{
		return RedirectToPage("/Home/ErrorEnable2FA/Index");
	}

	// code omitted for brevity

	if (ModelState.IsValid)
	{
		var result = await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberLogin, lockoutOnFailure: true);
		if (result.Succeeded)
		{
			// code omitted for brevity
		}
		if (result.RequiresTwoFactor)
		{
			var fido2ItemExistsForUser = await _fido2Store.GetCredentialsByUserNameAsync(user.UserName);
			if (fido2ItemExistsForUser.Count > 0)
			{
				return RedirectToPage("/Account/LoginFido2Mfa", new { area = "Identity", Input.ReturnUrl, Input.RememberLogin });
			}

			return RedirectToPage("/Account/LoginWith2fa", new { area = "Identity", Input.ReturnUrl, RememberMe = Input.RememberLogin });
		}

		await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", clientId: context?.Client.ClientId));
		ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
	}

	// something went wrong, show form with error
	await BuildModelAsync(Input.ReturnUrl);
	return Page();
}
```

If the user is already signed in, the client app:

* Still validates the `amr` claim.

* Can set up the MFA with a link to the ASP.NET Core Identity view, such as **Enable MFA**:

## Force ASP.NET Core OpenID Connect client to require MFA

This example shows how an ASP.NET Core Razor Page app, which uses OpenID Connect to sign in, can require users are authenticated by using MFA.

To validate the MFA requirement, an `IAuthorizationRequirement` requirement is created. The requirement is added to the pages by using a policy that requires MFA.

```
using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}
```

An `AuthorizationHandler` is implemented to use the `amr` claim and check for the value `mfa`. The `amr` is returned in the `id_token` of a successful authentication and can have many different values, as defined in the [IETF Authentication Method Reference Values](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-amr-values-08) specification.

The returned value depends on how the identity authenticated and on the OpenID Connect server implementation.

The `AuthorizationHandler` uses the `RequireMfa` requirement and validates the `amr` claim. The OpenID Connect server can be implemented by using Duende Identity Server with ASP.NET Core Identity. When a user signs in by using TOTP, the `amr` claim is returned with an MFA value. If you use a different OpenID Connect server implementation or a different MFA type, the `amr` claim can have a different value. The code must be extended to accept the other possible values.

```
public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
{
	protected override Task HandleRequirementAsync(
		AuthorizationHandlerContext context, 
		RequireMfa requirement)
	{
		if (context == null)
			throw new ArgumentNullException(nameof(context));
		if (requirement == null)
			throw new ArgumentNullException(nameof(requirement));

		var amrClaim =
			context.User.Claims.FirstOrDefault(t => t.Type == "amr");

		if (amrClaim != null && amrClaim.Value == Amr.Mfa)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}
}
```

In the program file, the `AddOpenIdConnect` method is used as the default challenge scheme. The authorization handler, which is used to check the `amr` claim, is added to the Inversion of Control container. A policy is then created which adds the `RequireMfa` requirement.

```
builder.Services.ConfigureApplicationCookie(options =>
        options.Cookie.SecurePolicy =
            CookieSecurePolicy.Always);

builder.Services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();

builder.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "https://localhost:44352";
	options.RequireHttpsMetadata = true;
	options.ClientId = "AspNetCoreRequireMfaOidc";
	options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
});

builder.Services.AddAuthorization(options =>
{
	options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
	{
		policyIsAdminRequirement.Requirements.Add(new RequireMfa());
	});
});

builder.Services.AddRazorPages();
```

The policy is then used in the Razor page as required. The policy can also be added globally for the entire app.

```
[Authorize(Policy= "RequireMfa")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}
```

If the user authenticates without MFA, the `amr` claim probably has a `pwd` (password) value. As a result, the request isn't authorized to access the page. Using the default values, the user is redirected to the **Account** > **AccessDenied** page. This behavior can be changed or you can implement your own custom logic. In this example, a link is added so the valid user can set up MFA for their account.

```
@page
@model AspNetCoreRequireMfaOidc.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>AccessDenied</h1>

You require MFA to login here

<a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>
```

After this change, only users that authenticate with MFA can access the page or website. If different MFA types are used, or if 2FA is permitted, the `amr` claim has different values and needs to be processed correctly. Different OpenID Connect servers also return different values for this claim. These values might not follow the [IETF Authentication Method Reference Values](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-amr-values-08) specification.

When the user signs in without MFA (for example, by using only a password):

* The `amr` claim has the `pwd` password value:

* Access is denied:

Alternatively, when the user signs in by using OTP with Identity, the `amr` claim has the `mfa` value:

### OIDC and OAuth parameter customization

The OAuth and OIDC authentication handlers [AdditionalAuthorizationParameters](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.oauthoptions.additionalauthorizationparameters#microsoft-aspnetcore-authentication-oauth-oauthoptions-additionalauthorizationparameters) option allows customization of authorization message parameters that are commonly included as part of the redirect query string:

```
builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.AdditionalAuthorizationParameters.Add("prompt", "login");
    options.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com");
});
```

## Related content

* [Enable QR Code generation for TOTP authenticator apps in ASP.NET Core](identity-enable-qrcodes?view=aspnetcore-10.0)
* [Authentication methods in Microsoft Entra ID - passkeys (FIDO2)](/en-us/entra/identity/authentication/concept-authentication-passkeys-fido2)
* [FIDO2 .NET library for FIDO2 / WebAuthn attestation and assertion with .NET](https://github.com/passwordless-lib/fido2-net-lib) (GitHub)
* [WebAuthn and Passkeys Awesome](https://github.com/yackermann/awesome-webauthn) (GitHub)

By [Damien Bowden](https://github.com/damienbod)

[View or download sample code (damienbod/AspNetCoreHybridFlowWithApi GitHub repository)](https://github.com/damienbod/AspNetCoreHybridFlowWithApi)

Multi-factor authentication (MFA) is a process in which a user is requested during a sign-in event for additional forms of identification. This prompt could be to enter a code from a cellphone, use a FIDO2 key, or to provide a fingerprint scan. When you require a second form of authentication, security is enhanced. The additional factor isn't easily obtained or duplicated by a cyberattacker.

This article covers the following areas:

* What is MFA and what MFA flows are recommended
* Configure MFA for administration pages using ASP.NET Core Identity
* Send MFA sign-in requirement to OpenID Connect server
* Force ASP.NET Core OpenID Connect client to require MFA

## MFA, 2FA

MFA requires at least two or more types of proof for an identity like something you know, something you possess, or biometric validation for the user to authenticate.

Two-factor authentication (2FA) is like a subset of MFA, but the difference being that MFA can require two or more factors to prove the identity.

2FA is supported by default when using ASP.NET Core Identity. To enable or disable 2FA for a specific user, set the [IdentityUser\<TKey>.TwoFactorEnabled](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser-1.twofactorenabled) property. The ASP.NET Core Identity Default UI includes pages for configuring 2FA.

### MFA TOTP (Time-based One-time Password Algorithm)

MFA using TOTP is supported by default when using ASP.NET Core Identity. This approach can be used together with any compliant authenticator app, including:

* Microsoft Authenticator
* Google Authenticator

For implementation details, see [Enable QR Code generation for TOTP authenticator apps in ASP.NET Core](identity-enable-qrcodes?view=aspnetcore-10.0).

To disable support for MFA TOTP, configure authentication using [AddIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentity) instead of [AddDefaultIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionuiextensions.adddefaultidentity). `AddDefaultIdentity` calls [AddDefaultTokenProviders](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderextensions.adddefaulttokenproviders) internally, which registers multiple token providers including one for MFA TOTP. To register only specific token providers, call [AddTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addtokenprovider) for each required provider. For more information about available token providers, see the extension methods at [IdentityBuilderExtensions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderextensions).

### MFA passkeys/FIDO2 or passwordless

passkeys/FIDO2 is currently:

* The most secure way of achieving MFA.
* MFA that protects against phishing attacks. (As well as certificate authentication and Windows for business)

At present, ASP.NET Core doesn't support passkeys/FIDO2 directly. Passkeys/FIDO2 can be used for MFA or passwordless flows.

Microsoft Entra ID provides support for passkeys/FIDO2 and passwordless flows. For more information, see [Passwordless authentication options](/en-us/azure/active-directory/authentication/concept-authentication-passwordless).

Other forms of passwordless MFA do not or may not protect against phishing.

### MFA SMS

MFA with SMS increases security massively compared with password authentication (single factor). However, using SMS as a second factor is no longer recommended. Too many known attack vectors exist for this type of implementation.

[NIST guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html)

## Configure MFA for administration pages using ASP.NET Core Identity

MFA could be forced on users to access sensitive pages within an ASP.NET Core Identity app. This could be useful for apps where different levels of access exist for the different identities. For example, users might be able to view the profile data using a password login, but an administrator would be required to use MFA to access the administrative pages.

### Extend the login with an MFA claim

The demo code is setup using ASP.NET Core with Identity and Razor Pages. The `AddIdentity` method is used instead of `AddDefaultIdentity` one, so an `IUserClaimsPrincipalFactory` implementation can be used to add claims to the identity after a successful login.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../?view=aspnetcore-10.0#secure-authentication-flows).

```
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(
        Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
		options.SignIn.RequireConfirmedAccount = false)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

builder.Services.AddSingleton<IEmailSender, EmailSender>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
    AdditionalUserClaimsPrincipalFactory>();

builder.Services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled", x => x.RequireClaim("amr", "mfa")));

builder.Services.AddRazorPages();
```

The `AdditionalUserClaimsPrincipalFactory` class adds the `amr` claim to the user claims only after a successful login. The claim's value is read from the database. The claim is added here because the user should only access the higher protected view if the identity has logged in with MFA. If the database view is read from the database directly instead of using the claim, it's possible to access the view without MFA directly after activating the MFA.

```
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : 
        UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("amr", "mfa"));
            }
            else
            {
                claims.Add(new Claim("amr", "pwd"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}
```

Because the Identity service setup changed in the `Startup` class, the layouts of the Identity need to be updated. Scaffold the Identity pages into the app. Define the layout in the `Identity/Account/Manage/_Layout.cshtml` file.

```
@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}
```

Also assign the layout for all the manage pages from the Identity pages:

```
@{
    Layout = "_Layout.cshtml";
}
```

### Validate the MFA requirement in the administration page

The administration Razor Page validates that the user has logged in using MFA. In the `OnGet` method, the identity is used to access the user claims. The `amr` claim is checked for the value `mfa`. If the identity is missing this claim or is `false`, the page redirects to the Enable MFA page. This is possible because the user has logged in already, but without MFA.

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = 
                User.Claims.FirstOrDefault(t => t.Type == "amr");

            if (claimTwoFactorEnabled != null && 
                "mfa".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the administrative stuff
            }
            else
            {
                return Redirect(
                    "/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}
```

### UI logic to toggle user login information

An authorization policy was added at startup. The policy requires the `amr` claim with the value `mfa`.

```
services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled",
        x => x.RequireClaim("amr", "mfa")));
```

This policy can then be used in the `_Layout` view to show or hide the **Admin** menu with the warning:

```
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService
```

If the identity has logged in using MFA, the **Admin** menu is displayed without the tooltip warning. When the user has logged in without MFA, the **Admin (Not Enabled)** menu is displayed along with the tooltip that informs the user (explaining the warning).

```
@if (SignInManager.IsSignedIn(User))
{
    @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
               id="tooltip-demo"  
               data-toggle="tooltip" 
               data-placement="bottom" 
               title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
                Admin (Not Enabled)
            </a>
        </li>
    }
}
```

If the user logs in without MFA, the warning is displayed:

The user is redirected to the MFA enable view when clicking the **Admin** link:

## Send MFA sign-in requirement to OpenID Connect server

The `acr_values` parameter can be used to pass the `mfa` required value from the client to the server in an authentication request.

Note

The `acr_values` parameter needs to be handled on the OpenID Connect server for this to work.

### OpenID Connect ASP.NET Core client

The ASP.NET Core Razor Pages OpenID Connect client app uses the `AddOpenIdConnect` method to login to the OpenID Connect server. The `acr_values` parameter is set with the `mfa` value and sent with the authentication request. The `OpenIdConnectEvents` is used to add this.

For recommended `acr_values` parameter values, see [Authentication Method Reference Values](https://tools.ietf.org/html/draft-ietf-oauth-amr-values-08).

```
build.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "<OpenID Connect server URL>";
	options.RequireHttpsMetadata = true;
	options.ClientId = "<OpenID Connect client ID>";
	options.ClientSecret = "<>";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
	options.Events = new OpenIdConnectEvents
	{
		OnRedirectToIdentityProvider = context =>
		{
			context.ProtocolMessage.SetParameter("acr_values", "mfa");
			return Task.FromResult(0);
		}
	};
});
```

### Example OpenID Connect Duende IdentityServer server with ASP.NET Core Identity

On the OpenID Connect server, which is implemented using ASP.NET Core Identity with Razor Pages, a new page named `ErrorEnable2FA.cshtml` is created. The view:

* Displays if the Identity comes from an app that requires MFA but the user hasn't activated this in Identity.
* Informs the user and adds a link to activate this.

```
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:

<br />

<a href="~/Identity/Account/Manage/TwoFactorAuthentication">Enable MFA</a>
```

In the `Login` method, the `IIdentityServerInteractionService` interface implementation `_interaction` is used to access the OpenID Connect request parameters. The `acr_values` parameter is accessed using the `AcrValues` property. As the client sent this with `mfa` set, this can then be checked.

If MFA is required, and the user in ASP.NET Core Identity has MFA enabled, then the login continues. When the user has no MFA enabled, the user is redirected to the custom view `ErrorEnable2FA.cshtml`. Then ASP.NET Core Identity signs the user in.

The Fido2Store is used to check if the user has activated MFA using a custom FIDO2 Token Provider.

```
public async Task<IActionResult> OnPost()
{
	// check if we are in the context of an authorization request
	var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);

	var requires2Fa = context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

	var user = await _userManager.FindByNameAsync(Input.Username);
	if (user != null && !user.TwoFactorEnabled && requires2Fa)
	{
		return RedirectToPage("/Home/ErrorEnable2FA/Index");
	}

	// code omitted for brevity

	if (ModelState.IsValid)
	{
		var result = await _signInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberLogin, lockoutOnFailure: true);
		if (result.Succeeded)
		{
			// code omitted for brevity
		}
		if (result.RequiresTwoFactor)
		{
			var fido2ItemExistsForUser = await _fido2Store.GetCredentialsByUserNameAsync(user.UserName);
			if (fido2ItemExistsForUser.Count > 0)
			{
				return RedirectToPage("/Account/LoginFido2Mfa", new { area = "Identity", Input.ReturnUrl, Input.RememberLogin });
			}

			return RedirectToPage("/Account/LoginWith2fa", new { area = "Identity", Input.ReturnUrl, RememberMe = Input.RememberLogin });
		}

		await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", clientId: context?.Client.ClientId));
		ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
	}

	// something went wrong, show form with error
	await BuildModelAsync(Input.ReturnUrl);
	return Page();
}
```

If the user is already logged in, the client app:

* Still validates the `amr` claim.
* Can set up the MFA with a link to the ASP.NET Core Identity view.

## Force ASP.NET Core OpenID Connect client to require MFA

This example shows how an ASP.NET Core Razor Page app, which uses OpenID Connect to sign in, can require that users have authenticated using MFA.

To validate the MFA requirement, an `IAuthorizationRequirement` requirement is created. This will be added to the pages using a policy that requires MFA.

```
using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc;

public class RequireMfa : IAuthorizationRequirement{}
```

An `AuthorizationHandler` is implemented that will use the `amr` claim and check for the value `mfa`. The `amr` is returned in the `id_token` of a successful authentication and can have many different values as defined in the [Authentication Method Reference Values](https://tools.ietf.org/html/draft-ietf-oauth-amr-values-08) specification.

The returned value depends on how the identity authenticated and on the OpenID Connect server implementation.

The `AuthorizationHandler` uses the `RequireMfa` requirement and validates the `amr` claim. The OpenID Connect server can be implemented using Duende Identity Server with ASP.NET Core Identity. When a user logs in using TOTP, the `amr` claim is returned with an MFA value. If using a different OpenID Connect server implementation or a different MFA type, the `amr` claim will, or can, have a different value. The code must be extended to accept this as well.

```
public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
{
	protected override Task HandleRequirementAsync(
		AuthorizationHandlerContext context, 
		RequireMfa requirement)
	{
		if (context == null)
			throw new ArgumentNullException(nameof(context));
		if (requirement == null)
			throw new ArgumentNullException(nameof(requirement));

		var amrClaim =
			context.User.Claims.FirstOrDefault(t => t.Type == "amr");

		if (amrClaim != null && amrClaim.Value == Amr.Mfa)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}
}
```

In the program file, the `AddOpenIdConnect` method is used as the default challenge scheme. The authorization handler, which is used to check the `amr` claim, is added to the Inversion of Control container. A policy is then created which adds the `RequireMfa` requirement.

```
builder.Services.ConfigureApplicationCookie(options =>
        options.Cookie.SecurePolicy =
            CookieSecurePolicy.Always);

builder.Services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();

builder.Services.AddAuthentication(options =>
{
	options.DefaultScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme =
		OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme =
		CookieAuthenticationDefaults.AuthenticationScheme;
	options.Authority = "https://localhost:44352";
	options.RequireHttpsMetadata = true;
	options.ClientId = "AspNetCoreRequireMfaOidc";
	options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
	options.ResponseType = "code";
	options.UsePkce = true;	
	options.Scope.Add("profile");
	options.Scope.Add("offline_access");
	options.SaveTokens = true;
});

builder.Services.AddAuthorization(options =>
{
	options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
	{
		policyIsAdminRequirement.Requirements.Add(new RequireMfa());
	});
});

builder.Services.AddRazorPages();
```

This policy is then used in the Razor page as required. The policy could be added globally for the entire app as well.

```
[Authorize(Policy= "RequireMfa")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}
```

If the user authenticates without MFA, the `amr` claim will probably have a `pwd` value. The request won't be authorized to access the page. Using the default values, the user will be redirected to the *Account/AccessDenied* page. This behavior can be changed or you can implement your own custom logic here. In this example, a link is added so that the valid user can set up MFA for their account.

```
@page
@model AspNetCoreRequireMfaOidc.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>AccessDenied</h1>

You require MFA to login here

<a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>
```

Now only users that authenticate with MFA can access the page or website. If different MFA types are used or if 2FA is okay, the `amr` claim will have different values and needs to be processed correctly. Different OpenID Connect servers also return different values for this claim and might not follow the [Authentication Method Reference Values](https://tools.ietf.org/html/draft-ietf-oauth-amr-values-08) specification.

When logging in without MFA (for example, using just a password):

* The `amr` has the `pwd` value:

* Access is denied:

Alternatively, logging in using OTP with Identity:

## Additional resources

* [Enable QR Code generation for TOTP authenticator apps in ASP.NET Core](identity-enable-qrcodes?view=aspnetcore-10.0)
* [Passwordless authentication options for Azure Active Directory](/en-us/azure/active-directory/authentication/concept-authentication-passwordless)
* [FIDO2 .NET library for FIDO2 / WebAuthn Attestation and Assertion using .NET](https://github.com/abergs/fido2-net-lib)
* [WebAuthn Awesome](https://github.com/herrjemand/awesome-webauthn)

By [Damien Bowden](https://github.com/damienbod)

[View or download sample code (damienbod/AspNetCoreHybridFlowWithApi GitHub repository)](https://github.com/damienbod/AspNetCoreHybridFlowWithApi)

Multi-factor authentication (MFA) is a process in which a user is requested during a sign-in event for additional forms of identification. This prompt could be to enter a code from a cellphone, use a FIDO2 key, or to provide a fingerprint scan. When you require a second form of authentication, security is enhanced. The additional factor isn't easily obtained or duplicated by a cyberattacker.

This article covers the following areas:

* What is MFA and what MFA flows are recommended
* Configure MFA for administration pages using ASP.NET Core Identity
* Send MFA sign-in requirement to OpenID Connect server
* Force ASP.NET Core OpenID Connect client to require MFA

## MFA, 2FA

MFA requires at least two or more types of proof for an identity like something you know, something you possess, or biometric validation for the user to authenticate.

Two-factor authentication (2FA) is like a subset of MFA, but the difference being that MFA can require two or more factors to prove the identity.

### MFA TOTP (Time-based One-time Password Algorithm)

MFA using TOTP is a supported implementation using ASP.NET Core Identity. This can be used together with any compliant authenticator app, including:

* Microsoft Authenticator App
* Google Authenticator App

See the following link for implementation details:

[Enable QR Code generation for TOTP authenticator apps in ASP.NET Core](identity-enable-qrcodes?view=aspnetcore-10.0)

### MFA passkeys/FIDO2 or passwordless

passkeys/FIDO2 is currently:

* The most secure way of achieving MFA.
* MFA that protects against phishing attacks. (As well as certificate authentication and Windows for business)

At present, ASP.NET Core doesn't support passkeys/FIDO2 directly. Passkeys/FIDO2 can be used for MFA or passwordless flows.

Microsoft Entra ID provides support for passkeys/FIDO2 and passwordless flows. For more information, see [Passwordless authentication options](/en-us/azure/active-directory/authentication/concept-authentication-passwordless).

Other forms of passwordless MFA do not or may not protect against phishing.

### MFA SMS

MFA with SMS increases security massively compared with password authentication (single factor). However, using SMS as a second factor is no longer recommended. Too many known attack vectors exist for this type of implementation.

[NIST guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html)

## Configure MFA for administration pages using ASP.NET Core Identity

MFA could be forced on users to access sensitive pages within an ASP.NET Core Identity app. This could be useful for apps where different levels of access exist for the different identities. For example, users might be able to view the profile data using a password login, but an administrator would be required to use MFA to access the administrative pages.

### Extend the login with an MFA claim

The demo code is setup using ASP.NET Core with Identity and Razor Pages. The `AddIdentity` method is used instead of `AddDefaultIdentity` one, so an `IUserClaimsPrincipalFactory` implementation can be used to add claims to the identity after a successful login.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../?view=aspnetcore-10.0#secure-authentication-flows).

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlite(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<IdentityUser, IdentityRole>(
            options => options.SignIn.RequireConfirmedAccount = false)
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddSingleton<IEmailSender, EmailSender>();
    services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, 
        AdditionalUserClaimsPrincipalFactory>();

    services.AddAuthorization(options =>
        options.AddPolicy("TwoFactorEnabled",
            x => x.RequireClaim("amr", "mfa")));

    services.AddRazorPages();
}
```

The `AdditionalUserClaimsPrincipalFactory` class adds the `amr` claim to the user claims only after a successful login. The claim's value is read from the database. The claim is added here because the user should only access the higher protected view if the identity has logged in with MFA. If the database view is read from the database directly instead of using the claim, it's possible to access the view without MFA directly after activating the MFA.

```
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace IdentityStandaloneMfa
{
    public class AdditionalUserClaimsPrincipalFactory : 
        UserClaimsPrincipalFactory<IdentityUser, IdentityRole>
    {
        public AdditionalUserClaimsPrincipalFactory( 
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager, 
            IOptions<IdentityOptions> optionsAccessor) 
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
        {
            var principal = await base.CreateAsync(user);
            var identity = (ClaimsIdentity)principal.Identity;

            var claims = new List<Claim>();

            if (user.TwoFactorEnabled)
            {
                claims.Add(new Claim("amr", "mfa"));
            }
            else
            {
                claims.Add(new Claim("amr", "pwd"));
            }

            identity.AddClaims(claims);
            return principal;
        }
    }
}
```

Because the Identity service setup changed in the `Startup` class, the layouts of the Identity need to be updated. Scaffold the Identity pages into the app. Define the layout in the `Identity/Account/Manage/_Layout.cshtml` file.

```
@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}
```

Also assign the layout for all the manage pages from the Identity pages:

```
@{
    Layout = "_Layout.cshtml";
}
```

### Validate the MFA requirement in the administration page

The administration Razor Page validates that the user has logged in using MFA. In the `OnGet` method, the identity is used to access the user claims. The `amr` claim is checked for the value `mfa`. If the identity is missing this claim or is `false`, the page redirects to the Enable MFA page. This is possible because the user has logged in already, but without MFA.

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityStandaloneMfa
{
    public class AdminModel : PageModel
    {
        public IActionResult OnGet()
        {
            var claimTwoFactorEnabled = 
                User.Claims.FirstOrDefault(t => t.Type == "amr");

            if (claimTwoFactorEnabled != null && 
                "mfa".Equals(claimTwoFactorEnabled.Value))
            {
                // You logged in with MFA, do the administrative stuff
            }
            else
            {
                return Redirect(
                    "/Identity/Account/Manage/TwoFactorAuthentication");
            }

            return Page();
        }
    }
}
```

### UI logic to toggle user login information

An authorization policy was added in the program file. The policy requires the `amr` claim with the value `mfa`.

```
builder.Services.AddAuthorization(options =>
    options.AddPolicy("TwoFactorEnabled",
        x => x.RequireClaim("amr", "mfa")));
```

This policy can then be used in the `_Layout` view to show or hide the **Admin** menu with the warning:

```
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager
@inject IAuthorizationService AuthorizationService
```

If the identity has logged in using MFA, the **Admin** menu is displayed without the tooltip warning. When the user has logged in without MFA, the **Admin (Not Enabled)** menu is displayed along with the tooltip that informs the user (explaining the warning).

```
@if (SignInManager.IsSignedIn(User))
{
    @if ((AuthorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Admin" 
               id="tooltip-demo"  
               data-toggle="tooltip" 
               data-placement="bottom" 
               title="MFA is NOT enabled. This is required for the Admin Page. If you have activated MFA, then logout, login again.">
                Admin (Not Enabled)
            </a>
        </li>
    }
}
```

If the user logs in without MFA, the warning is displayed:

The user is redirected to the MFA enable view when clicking the **Admin** link:

## Send MFA sign-in requirement to OpenID Connect server

The `acr_values` parameter can be used to pass the `mfa` required value from the client to the server in an authentication request.

Note

The `acr_values` parameter needs to be handled on the OpenID Connect server for this to work.

### OpenID Connect ASP.NET Core client

The ASP.NET Core Razor Pages OpenID Connect client app uses the `AddOpenIdConnect` method to login to the OpenID Connect server. The `acr_values` parameter is set with the `mfa` value and sent with the authentication request. The `OpenIdConnectEvents` is used to add this.

For recommended `acr_values` parameter values, see [Authentication Method Reference Values](https://tools.ietf.org/html/draft-ietf-oauth-amr-values-08).

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme =
            OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.SignInScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "<OpenID Connect server URL>";
        options.RequireHttpsMetadata = true;
        options.ClientId = "<OpenID Connect client ID>";
        options.ClientSecret = "<>";
        options.ResponseType = "code";
        options.UsePkce = true;	
        options.Scope.Add("profile");
        options.Scope.Add("offline_access");
        options.SaveTokens = true;
        options.Events = new OpenIdConnectEvents
        {
            OnRedirectToIdentityProvider = context =>
            {
                context.ProtocolMessage.SetParameter("acr_values", "mfa");
                return Task.FromResult(0);
            }
        };
    });
```

### Example OpenID Connect IdentityServer 4 server with ASP.NET Core Identity

On the OpenID Connect server, which is implemented using ASP.NET Core Identity with MVC views, a new view named `ErrorEnable2FA.cshtml` is created. The view:

* Displays if the Identity comes from an app that requires MFA but the user hasn't activated this in Identity.
* Informs the user and adds a link to activate this.

```
@{
    ViewData["Title"] = "ErrorEnable2FA";
}

<h1>The client application requires you to have MFA enabled. Enable this, try login again.</h1>

<br />

You can enable MFA to login here:

<br />

<a asp-controller="Manage" asp-action="TwoFactorAuthentication">Enable MFA</a>
```

In the `Login` method, the `IIdentityServerInteractionService` interface implementation `_interaction` is used to access the OpenID Connect request parameters. The `acr_values` parameter is accessed using the `AcrValues` property. As the client sent this with `mfa` set, this can then be checked.

If MFA is required, and the user in ASP.NET Core Identity has MFA enabled, then the login continues. When the user has no MFA enabled, the user is redirected to the custom view `ErrorEnable2FA.cshtml`. Then ASP.NET Core Identity signs the user in.

```
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
    var returnUrl = model.ReturnUrl;
    var context = 
        await _interaction.GetAuthorizationContextAsync(returnUrl);
    var requires2Fa = 
        context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

    var user = await _userManager.FindByNameAsync(model.Email);
    if (user != null && !user.TwoFactorEnabled && requires2Fa)
    {
        return RedirectToAction(nameof(ErrorEnable2FA));
    }

    // code omitted for brevity
```

The `ExternalLoginCallback` method works like the local Identity login. The `AcrValues` property is checked for the `mfa` value. If the `mfa` value is present, MFA is forced before the login completes (for example, redirected to the `ErrorEnable2FA` view).

```
//
// GET: /Account/ExternalLoginCallback
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(
    string returnUrl = null,
    string remoteError = null)
{
    var context =
        await _interaction.GetAuthorizationContextAsync(returnUrl);
    var requires2Fa =
        context?.AcrValues.Count(t => t.Contains("mfa")) >= 1;

    if (remoteError != null)
    {
        ModelState.AddModelError(
            string.Empty,
            _sharedLocalizer["EXTERNAL_PROVIDER_ERROR", 
            remoteError]);
        return View(nameof(Login));
    }
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        return RedirectToAction(nameof(Login));
    }

    var email = info.Principal.FindFirstValue(ClaimTypes.Email);

    if (!string.IsNullOrEmpty(email))
    {
        var user = await _userManager.FindByNameAsync(email);
        if (user != null && !user.TwoFactorEnabled && requires2Fa)
        {
            return RedirectToAction(nameof(ErrorEnable2FA));
        }
    }

    // Sign in the user with this external login provider if the user already has a login.
    var result = await _signInManager
        .ExternalLoginSignInAsync(
            info.LoginProvider, 
            info.ProviderKey, 
            isPersistent: 
            false);

    // code omitted for brevity
```

If the user is already logged in, the client app:

* Still validates the `amr` claim.
* Can set up the MFA with a link to the ASP.NET Core Identity view.

## Force ASP.NET Core OpenID Connect client to require MFA

This example shows how an ASP.NET Core Razor Page app, which uses OpenID Connect to sign in, can require that users have authenticated using MFA.

To validate the MFA requirement, an `IAuthorizationRequirement` requirement is created. This will be added to the pages using a policy that requires MFA.

```
using Microsoft.AspNetCore.Authorization;

namespace AspNetCoreRequireMfaOidc
{
    public class RequireMfa : IAuthorizationRequirement{}
}
```

An `AuthorizationHandler` is implemented that will use the `amr` claim and check for the value `mfa`. The `amr` is returned in the `id_token` of a successful authentication and can have many different values as defined in the [Authentication Method Reference Values](https://tools.ietf.org/html/draft-ietf-oauth-amr-values-08) specification.

The returned value depends on how the identity authenticated and on the OpenID Connect server implementation.

The `AuthorizationHandler` uses the `RequireMfa` requirement and validates the `amr` claim. The OpenID Connect server can be implemented using IdentityServer4 with ASP.NET Core Identity. When a user logs in using TOTP, the `amr` claim is returned with an MFA value. If using a different OpenID Connect server implementation or a different MFA type, the `amr` claim will, or can, have a different value. The code must be extended to accept this as well.

```
public class RequireMfaHandler : AuthorizationHandler<RequireMfa>
{
	protected override Task HandleRequirementAsync(
		AuthorizationHandlerContext context, 
		RequireMfa requirement)
	{
		if (context == null)
			throw new ArgumentNullException(nameof(context));
		if (requirement == null)
			throw new ArgumentNullException(nameof(requirement));

		var amrClaim =
			context.User.Claims.FirstOrDefault(t => t.Type == "amr");

		if (amrClaim != null && amrClaim.Value == Amr.Mfa)
		{
			context.Succeed(requirement);
		}

		return Task.CompletedTask;
	}
}
```

In the `Startup.ConfigureServices` method, the `AddOpenIdConnect` method is used as the default challenge scheme. The authorization handler, which is used to check the `amr` claim, is added to the Inversion of Control container. A policy is then created which adds the `RequireMfa` requirement.

```
public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureApplicationCookie(options =>
        options.Cookie.SecurePolicy =
            CookieSecurePolicy.Always);

    services.AddSingleton<IAuthorizationHandler, RequireMfaHandler>();

    services.AddAuthentication(options =>
    {
        options.DefaultScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme =
            OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.SignInScheme =
            CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "https://localhost:44352";
        options.RequireHttpsMetadata = true;
        options.ClientId = "AspNetCoreRequireMfaOidc";
        options.ClientSecret = "AspNetCoreRequireMfaOidcSecret";
        options.ResponseType = "code";
        options.UsePkce = true;	
        options.Scope.Add("profile");
        options.Scope.Add("offline_access");
        options.SaveTokens = true;
    });

    services.AddAuthorization(options =>
    {
        options.AddPolicy("RequireMfa", policyIsAdminRequirement =>
        {
            policyIsAdminRequirement.Requirements.Add(new RequireMfa());
        });
    });

    services.AddRazorPages();
}
```

This policy is then used in the Razor page as required. The policy could be added globally for the entire app as well.

```
[Authorize(Policy= "RequireMfa")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}
```

If the user authenticates without MFA, the `amr` claim will probably have a `pwd` value. The request won't be authorized to access the page. Using the default values, the user will be redirected to the *Account/AccessDenied* page. This behavior can be changed or you can implement your own custom logic here. In this example, a link is added so that the valid user can set up MFA for their account.

```
@page
@model AspNetCoreRequireMfaOidc.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
    Layout = "~/Pages/Shared/_Layout.cshtml";
}

<h1>AccessDenied</h1>

You require MFA to login here

<a href="https://localhost:44352/Manage/TwoFactorAuthentication">Enable MFA</a>
```

Now only users that authenticate with MFA can access the page or website. If different MFA types are used or if 2FA is okay, the `amr` claim will have different values and needs to be processed correctly. Different OpenID Connect servers also return different values for this claim and might not follow the [Authentication Method Reference Values](https://tools.ietf.org/html/draft-ietf-oauth-amr-values-08) specification.

When logging in without MFA (for example, using just a password):

* The `amr` has the `pwd` value:

* Access is denied:

Alternatively, logging in using OTP with Identity:

## Additional resources

* [Enable QR Code generation for TOTP authenticator apps in ASP.NET Core](identity-enable-qrcodes?view=aspnetcore-10.0)
* [Passwordless authentication options for Azure Active Directory](/en-us/azure/active-directory/authentication/concept-authentication-passwordless)
* [FIDO2 .NET library for FIDO2 / WebAuthn Attestation and Assertion using .NET](https://github.com/abergs/fido2-net-lib)
* [WebAuthn Awesome](https://github.com/herrjemand/awesome-webauthn)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization/select-language-culture
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/localization/select-language-culture.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Implement a strategy to select the language/culture for each request in a localized ASP.NET Core app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[Hisham Bin Ateya](https://twitter.com/hishambinateya), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax), [Nadeem Afana](https://afana.me/), and [Rick Anderson](https://twitter.com/RickAndMSFT)

One task for localizing an app is to implement a strategy for selecting the appropriate culture for each response the app returns.

## Configure Localization middleware

The current culture on a request is set in the localization [Middleware](../middleware/?view=aspnetcore-10.0). The localization middleware is enabled in `Program.cs`. The localization middleware must be configured before any middleware that might check the request culture (for example, `app.UseMvcWithDefaultRoute()`).

```
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[] { "en-US", "fr" };
    options.SetDefaultCulture(supportedCultures[0])
        .AddSupportedCultures(supportedCultures)
        .AddSupportedUICultures(supportedCultures);
});
```

[UseRequestLocalization](/en-us/dotnet/api/microsoft.aspnetcore.builder.applicationbuilderextensions.userequestlocalization) initializes a [RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions) object. On every request the list of [RequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.requestcultureprovider) in the [RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions) is enumerated and the first provider that can successfully determine the request culture is used. The default providers come from the `RequestLocalizationOptions` class:

1. [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider)
2. [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider)
3. [AcceptLanguageHeaderRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.acceptlanguageheaderrequestcultureprovider)

The default list goes from most specific to least specific. Later in the article you'll see how you can change the order and even add a custom culture provider. If none of the providers can determine the request culture, the [DefaultRequestCulture](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.defaultrequestculture#microsoft-aspnetcore-builder-requestlocalizationoptions-defaultrequestculture) is used.

## QueryStringRequestCultureProvider

Some apps will use a query string to set the [CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo). For apps that use the cookie or Accept-Language header approach, adding a query string to the URL is useful for debugging and testing code. By default, the [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider) is registered as the first localization provider in the `RequestCultureProvider` list. You pass the query string parameters `culture` and `ui-culture`. The following example sets the specific culture (language and region) to Spanish/Mexico:

```
http://localhost:5000/?culture=es-MX&ui-culture=es-MX
```

If only `culture` or `ui-culture` is passed in, the query string provider sets both values using the one passed in. For example, setting just the culture will set both the `Culture` and the `UICulture`:

```
http://localhost:5000/?culture=es-MX
```

## CookieRequestCultureProvider

Production apps will often provide a mechanism to set the culture with the ASP.NET Core culture cookie. Use the [MakeCookieValue](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider.makecookievalue) method to create a cookie.

The [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider) [DefaultCookieName](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider.defaultcookiename#microsoft-aspnetcore-localization-cookierequestcultureprovider-defaultcookiename) returns the default cookie name used to track the user's preferred culture information. The default cookie name is `.AspNetCore.Culture`.

The cookie format is `c=%LANGCODE%|uic=%LANGCODE%`, where `c` is `Culture` and `uic` is `UICulture`, for example:

```
c=en-UK|uic=en-US
```

If only one of the cultures is provided and the other is empty, the provided culture is used for both culture and UI culture.

## The Accept-Language HTTP header

The [Accept-Language header](https://www.w3.org/International/questions/qa-accept-lang-locales) is settable in most browsers and was originally intended to specify the user's language. This setting indicates what the browser has been set to send or has inherited from the underlying operating system. The Accept-Language HTTP header from a browser request isn't an infallible way to detect the user's preferred language (see [Setting language preferences in a browser](https://www.w3.org/International/questions/qa-lang-priorities.en.php)). A production app should include a way for a user to customize their choice of culture.

## Set the Accept-Language HTTP header in Edge

1. Search **Settings** for **Preferred languages**.

2. The preferred languages are listed in the **Preferred languages** box.

3. Select **Add languages** to add to the list.

4. Select **More actions …** next to a language to change the order of preference.

## The Content-Language HTTP header

The [Content-Language](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Language) entity header:

* Is used to describe the languages intended for the audience.
* Allows a user to differentiate according to the users' own preferred language.

Entity headers are used in both HTTP requests and responses.

The `Content-Language` header can be added by setting the property [ApplyCurrentCultureToResponseHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.applycurrentculturetoresponseheaders#microsoft-aspnetcore-builder-requestlocalizationoptions-applycurrentculturetoresponseheaders).

Adding the [`Content-Language`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Language) header:

* Allows the [RequestLocalizationMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.localization.requestlocalizationmiddleware) to set the `Content-Language` header with the `CurrentUICulture`.
* Eliminates the need to set the response header `Content-Language` explicitly.

```
app.UseRequestLocalization(new RequestLocalizationOptions
{
    ApplyCurrentCultureToResponseHeaders = true
});
```

## Apply the RouteDataRequest CultureProvider

The [RouteDataRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.routing.routedatarequestcultureprovider) sets the culture based on the value of the `culture` route value. See [Url culture provider using middleware as filters](https://andrewlock.net/url-culture-provider-using-middleware-as-mvc-filter-in-asp-net-core-1-1-0/) for information on:

* Using the middleware as filters feature of ASP.NET Core.
* How to use `RouteDataRequestCultureProvider` to set the culture of an app from the url.

See [Applying the RouteDataRequest CultureProvider globally with middleware as filters](https://andrewlock.net/applying-the-routedatarequest-cultureprovider-globally-with-middleware-as-filters/) for information on how to apply the `RouteDataRequestCultureProvider` globally.

## Use a custom provider

Suppose you want to let your customers store their language and culture in your databases. You could write a provider to look up these values for the user. The following code shows how to add a custom provider:

```
private const string enUSCulture = "en-US";

services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[]
    {
        new CultureInfo(enUSCulture),
        new CultureInfo("fr")
    };

    options.DefaultRequestCulture = new RequestCulture(culture: enUSCulture, uiCulture: enUSCulture);
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;

    options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context =>
    {
        // My custom request culture logic
        return await Task.FromResult(new ProviderCultureResult("en"));
    }));
});
```

Use `RequestLocalizationOptions` to add or remove localization providers.

## Change request culture providers order

[RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions) has three default request culture providers: [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider), [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider), and [AcceptLanguageHeaderRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.acceptlanguageheaderrequestcultureprovider). Use [`RequestLocalizationOptions.RequestCultureProviders`](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.requestcultureproviders#microsoft-aspnetcore-builder-requestlocalizationoptions-requestcultureproviders) property to change the order of these providers as shown in the following below:

```
    app.UseRequestLocalization(options =>
    {
        var questStringCultureProvider = options.RequestCultureProviders[0];    
        options.RequestCultureProviders.RemoveAt(0);
        options.RequestCultureProviders.Insert(1, questStringCultureProvider);
    });
```

In the preceding example, the order of `QueryStringRequestCultureProvider` and `CookieRequestCultureProvider` is switched, so the `RequestLocalizationMiddleware` looks for the cultures from the cookies first, then the query string.

As previously mentioned, add a custom provider via [AddInitialRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptionsextensions.addinitialrequestcultureprovider) which sets the order to `0`, so this provider takes the precedence over the others.

## User override culture

The [RequestLocalizationOptions.CultureInfoUseUserOverride](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.cultureinfouseuseroverride#microsoft-aspnetcore-builder-requestlocalizationoptions-cultureinfouseuseroverride) property allows the app to decide whether or not to use non-default Windows settings for the [CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo) [DateTimeFormat](/en-us/dotnet/api/system.globalization.cultureinfo.datetimeformat#system-globalization-cultureinfo-datetimeformat) and [NumberFormat](/en-us/dotnet/api/system.globalization.cultureinfo.numberformat#system-globalization-cultureinfo-numberformat) properties. This has ***no*** impact on Linux. This directly corresponds to [UseUserOverride](/en-us/dotnet/api/system.globalization.cultureinfo.useuseroverride#system-globalization-cultureinfo-useuseroverride).

```
    app.UseRequestLocalization(options =>
    {
        options.CultureInfoUseUserOverride = false;
    });
```

## Set the culture programmatically

This sample **Localization.StarterWeb** project on [GitHub](https://github.com/aspnet/entropy) contains UI to set the `Culture`. The `Views/Shared/_SelectLanguagePartial.cshtml` file allows you to select the culture from the list of supported cultures:

```
@using Microsoft.AspNetCore.Builder
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options

@inject IViewLocalizer Localizer
@inject IOptions<RequestLocalizationOptions> LocOptions

@{
    var requestCulture = Context.Features.Get<IRequestCultureFeature>();
    var cultureItems = LocOptions.Value.SupportedUICultures
        .Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
        .ToList();
    var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}";
}

<div title="@Localizer["Request culture provider:"] @requestCulture?.Provider?.GetType().Name">
    <form id="selectLanguage" asp-controller="Home" 
          asp-action="SetLanguage" asp-route-returnUrl="@returnUrl" 
          method="post" class="form-horizontal" role="form">
        <label asp-for="@requestCulture.RequestCulture.UICulture.Name">@Localizer["Language:"]</label> <select name="culture"
          onchange="this.form.submit();"
          asp-for="@requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems">
        </select>
    </form>
</div>
```

The `Views/Shared/_SelectLanguagePartial.cshtml` file is added to the `footer` section of the layout file so it will be available to all views:

```
<div class="container body-content" style="margin-top:60px">
    @RenderBody()
    <hr>
    <footer>
        <div class="row">
            <div class="col-md-6">
                <p>&copy; @System.DateTime.Now.Year - Localization</p>
            </div>
            <div class="col-md-6 text-right">
                @await Html.PartialAsync("_SelectLanguagePartial")
            </div>
        </div>
    </footer>
</div>
```

The `SetLanguage` method sets the culture cookie.

```
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );

    return LocalRedirect(returnUrl);
}
```

You can't plug in the `_SelectLanguagePartial.cshtml` to sample code for this project. The **Localization.StarterWeb** project on [GitHub](https://github.com/aspnet/entropy) has code to flow the `RequestLocalizationOptions` to a Razor partial through the [Dependency Injection](../dependency-injection?view=aspnetcore-10.0) container.

## Model binding route data and query strings

See [Globalization behavior of model binding route data and query strings](../../mvc/models/model-binding?view=aspnetcore-10.0#glob).

## Next steps

Localizing an app also involves the following tasks:

* [Make the app's content localizable](make-content-localizable?view=aspnetcore-10.0).
* [Provide localized resources for the languages and cultures the app supports](provide-resources?view=aspnetcore-10.0)

## Related content

* [Url culture provider using middleware as filters in ASP.NET Core](https://andrewlock.net/url-culture-provider-using-middleware-as-mvc-filter-in-asp-net-core-1-1-0/)
* [Applying the RouteDataRequest CultureProvider globally with middleware as filters](https://andrewlock.net/applying-the-routedatarequest-cultureprovider-globally-with-middleware-as-filters/)
* [Globalization and localization in ASP.NET Core](../localization?view=aspnetcore-10.0)
* [Make an ASP.NET Core app's content localizable](make-content-localizable?view=aspnetcore-10.0)
* [Provide localized resources for languages and cultures in an ASP.NET Core app](provide-resources?view=aspnetcore-10.0)
* [Troubleshoot ASP.NET Core localization](../troubleshoot-aspnet-core-localization?view=aspnetcore-10.0)
* [Globalizing and localizing .NET applications](/en-us/dotnet/standard/globalization-localization/index)
* [Localization.StarterWeb project](https://github.com/aspnet/Entropy/tree/master/samples/Localization.StarterWeb) used in the article.
* [Resources in .resx Files](/en-us/dotnet/framework/resources/working-with-resx-files-programmatically)
* [Localization & Generics](http://hishambinateya.com/localization-and-generics)

[Hisham Bin Ateya](https://twitter.com/hishambinateya), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax), [Nadeem Afana](https://afana.me/), and [Rick Anderson](https://twitter.com/RickAndMSFT)

One task for localizing an app is to implement a strategy for selecting the appropriate culture for each response the app returns.

## Configure Localization middleware

The current culture on a request is set in the localization [Middleware](../middleware/?view=aspnetcore-10.0). The localization middleware is enabled in `Program.cs`. The localization middleware must be configured before any middleware that might check the request culture (for example, `app.UseMvcWithDefaultRoute()`).

```
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[] { "en-US", "fr" };
    options.SetDefaultCulture(supportedCultures[0])
        .AddSupportedCultures(supportedCultures)
        .AddSupportedUICultures(supportedCultures);
});
```

[UseRequestLocalization](/en-us/dotnet/api/microsoft.aspnetcore.builder.applicationbuilderextensions.userequestlocalization) initializes a [RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions) object. On every request the list of [RequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.requestcultureprovider) in the [RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions) is enumerated and the first provider that can successfully determine the request culture is used. The default providers come from the `RequestLocalizationOptions` class:

1. [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider)
2. [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider)
3. [AcceptLanguageHeaderRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.acceptlanguageheaderrequestcultureprovider)

The default list goes from most specific to least specific. Later in the article you'll see how you can change the order and even add a custom culture provider. If none of the providers can determine the request culture, the [DefaultRequestCulture](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.defaultrequestculture#microsoft-aspnetcore-builder-requestlocalizationoptions-defaultrequestculture) is used.

## QueryStringRequestCultureProvider

Some apps will use a query string to set the [CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo). For apps that use the cookie or Accept-Language header approach, adding a query string to the URL is useful for debugging and testing code. By default, the [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider) is registered as the first localization provider in the `RequestCultureProvider` list. You pass the query string parameters `culture` and `ui-culture`. The following example sets the specific culture (language and region) to Spanish/Mexico:

```
http://localhost:5000/?culture=es-MX&ui-culture=es-MX
```

If only `culture` or `ui-culture` is passed in, the query string provider sets both values using the one passed in. For example, setting just the culture will set both the `Culture` and the `UICulture`:

```
http://localhost:5000/?culture=es-MX
```

## CookieRequestCultureProvider

Production apps will often provide a mechanism to set the culture with the ASP.NET Core culture cookie. Use the [MakeCookieValue](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider.makecookievalue) method to create a cookie.

The xref:Microsoft.AspNetCore.Localization.CookieRequestCultureProvider> [DefaultCookieName](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider.defaultcookiename#microsoft-aspnetcore-localization-cookierequestcultureprovider-defaultcookiename) returns the default cookie name used to track the user's preferred culture information. The default cookie name is `.AspNetCore.Culture`.

The cookie format is `c=%LANGCODE%|uic=%LANGCODE%`, where `c` is `Culture` and `uic` is `UICulture`, for example:

```
c=en-UK|uic=en-US
```

If only one of culture info or UI culture is provided, the provided culture is used for both culture info and UI culture.

## The Accept-Language HTTP header

The [Accept-Language header](https://www.w3.org/International/questions/qa-accept-lang-locales) is settable in most browsers and was originally intended to specify the user's language. This setting indicates what the browser has been set to send or has inherited from the underlying operating system. The Accept-Language HTTP header from a browser request isn't an infallible way to detect the user's preferred language (see [Setting language preferences in a browser](https://www.w3.org/International/questions/qa-lang-priorities.en.php)). A production app should include a way for a user to customize their choice of culture.

## Set the Accept-Language HTTP header in Edge

1. Search **Settings** for **Preferred languages**.

2. The preferred languages are listed in the **Preferred languages** box.

3. Select **Add languages** to add to the list.

4. Select **More actions …** next to a language to change the order of preference.

## The Content-Language HTTP header

The [Content-Language](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Language) entity header:

* Is used to describe the languages intended for the audience.
* Allows a user to differentiate according to the users' own preferred language.

Entity headers are used in both HTTP requests and responses.

The `Content-Language` header can be added by setting the property [ApplyCurrentCultureToResponseHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.applycurrentculturetoresponseheaders#microsoft-aspnetcore-builder-requestlocalizationoptions-applycurrentculturetoresponseheaders).

Adding the [`Content-Language`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Language) header:

* Allows the [RequestLocalizationMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.localization.requestlocalizationmiddleware) to set the `Content-Language` header with the `CurrentUICulture`.
* Eliminates the need to set the response header `Content-Language` explicitly.

```
app.UseRequestLocalization(new RequestLocalizationOptions
{
    ApplyCurrentCultureToResponseHeaders = true
});
```

## Use a custom provider

Suppose you want to let your customers store their language and culture in your databases. You could write a provider to look up these values for the user. The following code shows how to add a custom provider:

```
private const string enUSCulture = "en-US";

services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[]
    {
        new CultureInfo(enUSCulture),
        new CultureInfo("fr")
    };

    options.DefaultRequestCulture = new RequestCulture(culture: enUSCulture, uiCulture: enUSCulture);
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;

    options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context =>
    {
        // My custom request culture logic
        return await Task.FromResult(new ProviderCultureResult("en"));
    }));
});
```

Use `RequestLocalizationOptions` to add or remove localization providers.

## Change request culture providers order

[RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions) has three default request culture providers: [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider), [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider), and [AcceptLanguageHeaderRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.acceptlanguageheaderrequestcultureprovider). Use [`RequestLocalizationOptions.RequestCultureProviders`](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.requestcultureproviders#microsoft-aspnetcore-builder-requestlocalizationoptions-requestcultureproviders) property to change the order of these providers as shown in the following below:

```
    app.UseRequestLocalization(options =>
    {
        var questStringCultureProvider = options.RequestCultureProviders[0];    
        options.RequestCultureProviders.RemoveAt(0);
        options.RequestCultureProviders.Insert(1, questStringCultureProvider);
    });
```

In the preceding example, the order of `QueryStringRequestCultureProvider` and `CookieRequestCultureProvider` is switched, so the `RequestLocalizationMiddleware` looks for the cultures from the cookies first, then the query string.

As previously mentioned, add a custom provider via [AddInitialRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptionsextensions.addinitialrequestcultureprovider) which sets the order to `0`, so this provider takes the precedence over the others.

## Set the culture programmatically

This sample **Localization.StarterWeb** project on [GitHub](https://github.com/aspnet/entropy) contains UI to set the `Culture`. The `Views/Shared/_SelectLanguagePartial.cshtml` file allows you to select the culture from the list of supported cultures:

```
@using Microsoft.AspNetCore.Builder
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options

@inject IViewLocalizer Localizer
@inject IOptions<RequestLocalizationOptions> LocOptions

@{
    var requestCulture = Context.Features.Get<IRequestCultureFeature>();
    var cultureItems = LocOptions.Value.SupportedUICultures
        .Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
        .ToList();
    var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}";
}

<div title="@Localizer["Request culture provider:"] @requestCulture?.Provider?.GetType().Name">
    <form id="selectLanguage" asp-controller="Home" 
          asp-action="SetLanguage" asp-route-returnUrl="@returnUrl" 
          method="post" class="form-horizontal" role="form">
        <label asp-for="@requestCulture.RequestCulture.UICulture.Name">@Localizer["Language:"]</label> <select name="culture"
          onchange="this.form.submit();"
          asp-for="@requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems">
        </select>
    </form>
</div>
```

The `Views/Shared/_SelectLanguagePartial.cshtml` file is added to the `footer` section of the layout file so it will be available to all views:

```
<div class="container body-content" style="margin-top:60px">
    @RenderBody()
    <hr>
    <footer>
        <div class="row">
            <div class="col-md-6">
                <p>&copy; @System.DateTime.Now.Year - Localization</p>
            </div>
            <div class="col-md-6 text-right">
                @await Html.PartialAsync("_SelectLanguagePartial")
            </div>
        </div>
    </footer>
</div>
```

The `SetLanguage` method sets the culture cookie.

```
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );

    return LocalRedirect(returnUrl);
}
```

You can't plug in the `_SelectLanguagePartial.cshtml` to sample code for this project. The **Localization.StarterWeb** project on [GitHub](https://github.com/aspnet/entropy) has code to flow the `RequestLocalizationOptions` to a Razor partial through the [Dependency Injection](../dependency-injection?view=aspnetcore-10.0) container.

## Model binding route data and query strings

See [Globalization behavior of model binding route data and query strings](../../mvc/models/model-binding?view=aspnetcore-10.0#glob).

## Next steps

Localizing an app also involves the following tasks:

* [Make the app's content localizable](make-content-localizable?view=aspnetcore-10.0).
* [Provide localized resources for the languages and cultures the app supports](provide-resources?view=aspnetcore-10.0)

## Related content

* [Globalization and localization in ASP.NET Core](../localization?view=aspnetcore-10.0)
* [Make an ASP.NET Core app's content localizable](make-content-localizable?view=aspnetcore-10.0)
* [Provide localized resources for languages and cultures in an ASP.NET Core app](provide-resources?view=aspnetcore-10.0)
* [Troubleshoot ASP.NET Core localization](../troubleshoot-aspnet-core-localization?view=aspnetcore-10.0)
* [Globalizing and localizing .NET applications](/en-us/dotnet/standard/globalization-localization/index)
* [Localization.StarterWeb project](https://github.com/aspnet/Entropy/tree/master/samples/Localization.StarterWeb) used in the article.
* [Resources in .resx Files](/en-us/dotnet/framework/resources/working-with-resx-files-programmatically)
* [Localization & Generics](http://hishambinateya.com/localization-and-generics)

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax), [Nadeem Afana](https://afana.me/), and [Hisham Bin Ateya](https://twitter.com/hishambinateya)

One task for localizing an app is to implement a strategy for selecting the appropriate culture for each response the app returns.

## Configure Localization middleware

The current culture on a request is set in the localization [Middleware](../middleware/?view=aspnetcore-10.0). The localization middleware is enabled in the `Startup.Configure` method. The localization middleware must be configured before any middleware that might check the request culture (for example, `app.UseMvcWithDefaultRoute()`).

```
var supportedCultures = new[] { "en-US", "fr" };
var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);
```

`UseRequestLocalization` initializes a `RequestLocalizationOptions` object. On every request the list of `RequestCultureProvider` in the `RequestLocalizationOptions` is enumerated and the first provider that can successfully determine the request culture is used. The default providers come from the `RequestLocalizationOptions` class:

1. `QueryStringRequestCultureProvider`
2. `CookieRequestCultureProvider`
3. `AcceptLanguageHeaderRequestCultureProvider`

The default list goes from most specific to least specific. Later in the article you'll see how you can change the order and even add a custom culture provider. If none of the providers can determine the request culture, the `DefaultRequestCulture` is used.

## QueryStringRequestCultureProvider

Some apps will use a query string to set the [CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo). For apps that use the cookie or Accept-Language header approach, adding a query string to the URL is useful for debugging and testing code. By default, the `QueryStringRequestCultureProvider` is registered as the first localization provider in the `RequestCultureProvider` list. You pass the query string parameters `culture` and `ui-culture`. The following example sets the specific culture (language and region) to Spanish/Mexico:

```
http://localhost:5000/?culture=es-MX&ui-culture=es-MX
```

If you only pass in one of the two (`culture` or `ui-culture`), the query string provider will set both values using the one you passed in. For example, setting just the culture will set both the `Culture` and the `UICulture`:

```
http://localhost:5000/?culture=es-MX
```

## CookieRequestCultureProvider

Production apps will often provide a mechanism to set the culture with the ASP.NET Core culture cookie. Use the `MakeCookieValue` method to create a cookie.

The `CookieRequestCultureProvider` `DefaultCookieName` returns the default cookie name used to track the user's preferred culture information. The default cookie name is `.AspNetCore.Culture`.

The cookie format is `c=%LANGCODE%|uic=%LANGCODE%`, where `c` is `Culture` and `uic` is `UICulture`, for example:

```
c=en-UK|uic=en-US
```

If you only specify one of culture info and UI culture, the specified culture will be used for both culture info and UI culture.

## The Accept-Language HTTP header

The [Accept-Language header](https://www.w3.org/International/questions/qa-accept-lang-locales) is settable in most browsers and was originally intended to specify the user's language. This setting indicates what the browser has been set to send or has inherited from the underlying operating system. The Accept-Language HTTP header from a browser request isn't an infallible way to detect the user's preferred language (see [Setting language preferences in a browser](https://www.w3.org/International/questions/qa-lang-priorities.en.php)). A production app should include a way for a user to customize their choice of culture.

## Set the Accept-Language HTTP header in Edge

1. Search **Settings** for **Preferred languages**.

2. The preferred languages are listed in the **Preferred languages** box.

3. Select **Add languages** to add to the list.

4. Select **More actions …** next to a language to change the order of preference.

## The Content-Language HTTP header

The [Content-Language](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Language) entity header:

* Is used to describe the languages intended for the audience.
* Allows a user to differentiate according to the users' own preferred language.

Entity headers are used in both HTTP requests and responses.

The `Content-Language` header can be added by setting the property `ApplyCurrentCultureToResponseHeaders`.

Adding the `Content-Language` header:

* Allows the RequestLocalizationMiddleware to set the `Content-Language` header with the `CurrentUICulture`.
* Eliminates the need to set the response header `Content-Language` explicitly.

```
app.UseRequestLocalization(new RequestLocalizationOptions
{
    ApplyCurrentCultureToResponseHeaders = true
});
```

## Use a custom provider

Suppose you want to let your customers store their language and culture in your databases. You could write a provider to look up these values for the user. The following code shows how to add a custom provider:

```
private const string enUSCulture = "en-US";

services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[]
    {
        new CultureInfo(enUSCulture),
        new CultureInfo("fr")
    };

    options.DefaultRequestCulture = new RequestCulture(culture: enUSCulture, uiCulture: enUSCulture);
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;

    options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context =>
    {
        // My custom request culture logic
        return await Task.FromResult(new ProviderCultureResult("en"));
    }));
});
```

Use `RequestLocalizationOptions` to add or remove localization providers.

## Change request culture providers order

[RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions) has three default request culture providers: [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider), [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider), and [AcceptLanguageHeaderRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.acceptlanguageheaderrequestcultureprovider). Use [`RequestLocalizationOptions.RequestCultureProviders`](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.requestcultureproviders#microsoft-aspnetcore-builder-requestlocalizationoptions-requestcultureproviders) property to change the order of these providers as shown in the following below:

```
    app.UseRequestLocalization(options =>
    {
        var questStringCultureProvider = options.RequestCultureProviders[0];    
        options.RequestCultureProviders.RemoveAt(0);
        options.RequestCultureProviders.Insert(1, questStringCultureProvider);
    });
```

In the preceding example, the order of `QueryStringRequestCultureProvider` and `CookieRequestCultureProvider` is switched, so the `RequestLocalizationMiddleware` looks for the cultures from the cookies first, then the query string.

As previously mentioned, add a custom provider via [AddInitialRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptionsextensions.addinitialrequestcultureprovider) which sets the order to `0`, so this provider takes the precedence over the others.

## Set the culture programmatically

This sample **Localization.StarterWeb** project on [GitHub](https://github.com/aspnet/entropy) contains UI to set the `Culture`. The `Views/Shared/_SelectLanguagePartial.cshtml` file allows you to select the culture from the list of supported cultures:

```
@using Microsoft.AspNetCore.Builder
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options

@inject IViewLocalizer Localizer
@inject IOptions<RequestLocalizationOptions> LocOptions

@{
    var requestCulture = Context.Features.Get<IRequestCultureFeature>();
    var cultureItems = LocOptions.Value.SupportedUICultures
        .Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
        .ToList();
    var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}";
}

<div title="@Localizer["Request culture provider:"] @requestCulture?.Provider?.GetType().Name">
    <form id="selectLanguage" asp-controller="Home" 
          asp-action="SetLanguage" asp-route-returnUrl="@returnUrl" 
          method="post" class="form-horizontal" role="form">
        <label asp-for="@requestCulture.RequestCulture.UICulture.Name">@Localizer["Language:"]</label> <select name="culture"
          onchange="this.form.submit();"
          asp-for="@requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems">
        </select>
    </form>
</div>
```

The `Views/Shared/_SelectLanguagePartial.cshtml` file is added to the `footer` section of the layout file so it will be available to all views:

```
<div class="container body-content" style="margin-top:60px">
    @RenderBody()
    <hr>
    <footer>
        <div class="row">
            <div class="col-md-6">
                <p>&copy; @System.DateTime.Now.Year - Localization</p>
            </div>
            <div class="col-md-6 text-right">
                @await Html.PartialAsync("_SelectLanguagePartial")
            </div>
        </div>
    </footer>
</div>
```

The `SetLanguage` method sets the culture cookie.

```
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );

    return LocalRedirect(returnUrl);
}
```

You can't plug in the `_SelectLanguagePartial.cshtml` to sample code for this project. The **Localization.StarterWeb** project on [GitHub](https://github.com/aspnet/entropy) has code to flow the `RequestLocalizationOptions` to a Razor partial through the [Dependency Injection](../dependency-injection?view=aspnetcore-10.0) container.

## Model binding route data and query strings

See [Globalization behavior of model binding route data and query strings](../../mvc/models/model-binding?view=aspnetcore-10.0#glob).

## Next steps

Localizing an app also involves the following tasks:

* [Make the app's content localizable](make-content-localizable?view=aspnetcore-10.0).
* [Provide localized resources for the languages and cultures the app supports](provide-resources?view=aspnetcore-10.0)

## Related content

* [Globalization and localization in ASP.NET Core](../localization?view=aspnetcore-10.0)
* [Make an ASP.NET Core app's content localizable](make-content-localizable?view=aspnetcore-10.0)
* [Provide localized resources for languages and cultures in an ASP.NET Core app](provide-resources?view=aspnetcore-10.0)
* [Troubleshoot ASP.NET Core localization](../troubleshoot-aspnet-core-localization?view=aspnetcore-10.0)
* [Globalizing and localizing .NET applications](/en-us/dotnet/standard/globalization-localization/index)
* [Localization.StarterWeb project](https://github.com/aspnet/Entropy/tree/master/samples/Localization.StarterWeb) used in the article.
* [Resources in .resx Files](/en-us/dotnet/framework/resources/working-with-resx-files-programmatically)
* [Localization & Generics](http://hishambinateya.com/localization-and-generics)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/customizingauthorizationmiddlewareresponse.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Customize the behavior of authorization middleware

Apps can register an [IAuthorizationMiddlewareResultHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationmiddlewareresulthandler) to customize how [AuthorizationMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationmiddleware) handles authorization results. Apps can use `IAuthorizationMiddlewareResultHandler` to:

* Return customized responses.
* Enhance the default challenge or forbid responses.

The following code shows an example implementation of `IAuthorizationMiddlewareResultHandler` that returns a custom response for specific authorization failures:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;

public class SampleAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
    private readonly AuthorizationMiddlewareResultHandler defaultHandler = new();

    public async Task HandleAsync(
        RequestDelegate next,
        HttpContext context,
        AuthorizationPolicy policy,
        PolicyAuthorizationResult authorizeResult)
    {
        // If the authorization was forbidden and the resource had a specific requirement,
        // provide a custom 404 response.
        if (authorizeResult.Forbidden
            && authorizeResult.AuthorizationFailure!.FailedRequirements
                .OfType<Show404Requirement>().Any())
        {
            // Return a 404 to make it appear as if the resource doesn't exist.
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            return;
        }

        // Fall back to the default implementation.
        await defaultHandler.HandleAsync(next, context, policy, authorizeResult);
    }
}

public class Show404Requirement : IAuthorizationRequirement { }
```

Register this implementation of `IAuthorizationMiddlewareResultHandler` in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<
    IAuthorizationMiddlewareResultHandler, SampleAuthorizationMiddlewareResultHandler>();

var app = builder.Build();
```

Apps can register an [IAuthorizationMiddlewareResultHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationmiddlewareresulthandler) to customize how [AuthorizationMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationmiddleware) handles authorization results. Apps can use the `IAuthorizationMiddlewareResultHandler` to:

* Return customized responses.
* Enhance the default challenge or forbid responses.

The following code shows an example implementation of `IAuthorizationMiddlewareResultHandler` that returns a custom response for specific authorization failures:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

public class MyAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
   private readonly AuthorizationMiddlewareResultHandler 
        DefaultHandler = new AuthorizationMiddlewareResultHandler();
    
    public async Task HandleAsync(
        RequestDelegate requestDelegate,
        HttpContext httpContext,
        AuthorizationPolicy authorizationPolicy,
        PolicyAuthorizationResult policyAuthorizationResult)
    {
        // if the authorization was forbidden and the resource had specific requirements,
        // provide a custom response.
        if (Show404ForForbiddenResult(policyAuthorizationResult))
        {
            // Return a 404 to make it appear as if the resource does not exist.
            httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
            return;
        }

        // Fallback to the default implementation.
        await DefaultHandler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, 
                               policyAuthorizationResult);
    }

    bool Show404ForForbiddenResult(PolicyAuthorizationResult policyAuthorizationResult)
    {
        return policyAuthorizationResult.Forbidden &&
            policyAuthorizationResult.AuthorizationFailure.FailedRequirements.OfType<
                                                           Show404Requirement>().Any();
    }
}

public class Show404Requirement : IAuthorizationRequirement { }
```

Register `MyAuthorizationMiddlewareResultHandler` in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSingleton<IAuthorizationMiddlewareResultHandler,
                          MyAuthorizationMiddlewareResultHandler>();
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app/part-8
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/movie-database-app/part-8.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor movie database app (Part 8 - Add interactivity)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article is the eighth part of the Blazor movie database app tutorial that teaches you the basics of building an ASP.NET Core Blazor Web App with features to manage a movie database.

Up to this point in the tutorial, the entire app has been *enabled* for interactivity, but the app has only *adopted* interactivity in its `Counter` sample component. This part of the tutorial series explains how to adopt interactivity in the movie `Index` component.

Important

Confirm that the app isn't running for the next steps.

## Adopt interactivity

*Interactivity* means that a component has the capacity to process UI events via C# code, such as a button click. The events are either processed on the server by the ASP.NET Core runtime or in the browser on the client by the WebAssembly-based Blazor runtime. This tutorial adopts interactive server-side rendering (interactive SSR), also known generally as Interactive Server (`InteractiveServer`) rendering. Client-side rendering (CSR), which is inherently interactive, is covered in the Blazor reference documentation.

Interactive SSR enables a rich user experience like one would expect from a client app but without the need to create API endpoints to access server resources. UI interactions are handled by the server over a real-time SignalR connection with the browser. Page content for interactive pages is prerendered, where content on the server is initially generated and sent to the client without enabling event handlers for rendered controls. With prerendering, the server outputs the HTML UI of the page as soon as possible in response to the initial request, which makes the app feel more responsive to users.

Review the API in the `Program` file (`Program.cs`) that enables interactive SSR. Razor component services are added to the app to enable Razor components to render statically from the server ([AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents)) and execute code with interactive SSR ([AddInteractiveServerComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serverrazorcomponentsbuilderextensions.addinteractiveservercomponents)):

```
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
```

[MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) maps components defined in the root `App` component to the given .NET assembly and renders routable components, and [AddInteractiveServerRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.serverrazorcomponentsendpointconventionbuilderextensions.addinteractiveserverrendermode) configures the app's SignalR hub to support interactive SSR:

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

Up to this point in the tutorial series, the calls to [AddInteractiveServerComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serverrazorcomponentsbuilderextensions.addinteractiveservercomponents) and [AddInteractiveServerRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.serverrazorcomponentsendpointconventionbuilderextensions.addinteractiveserverrendermode) weren't required for the movie pages because the app only adopted static SSR features for the movie components. This article explains how to adopt interactive features in the movie `Index` component.

When Blazor sets the type of rendering for a component, the rendering is referred to as the component's *render mode*. The following table shows the available render modes for rendering Razor components in a Blazor Web App.

| Name                    | Description                                                                                                            | Render location     | Interactive |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------- |
| Static Server           | Static server-side rendering (static SSR)                                                                              | Server              | ❌No         |
| Interactive Server      | Interactive server-side rendering (interactive SSR) using Blazor Server.                                               | Server              | ✔️Yes       |
| Interactive WebAssembly | Client-side rendering (CSR) using Blazor WebAssembly.                                                                  | Client              | ✔️Yes       |
| Interactive Auto        | Interactive SSR using Blazor Server initially and then CSR on subsequent visits after the Blazor bundle is downloaded. | Server, then client | ✔️Yes       |

To apply a render mode to a component, the developer either uses the `@rendermode` directive or directive attribute on the component instance or on the component definition:

* The following example shows how to set the render mode on a component instance with the `@rendermode` directive attribute. The following example uses a hypothetical dialog (`Dialog`) component in a parent chat (`Chat`) component.

  Within the `Components/Pages/Chat.razor` file:

  ```
  <Dialog @rendermode="InteractiveServer" />
  ```

* The following example shows how to set the render mode on a component definition with the `@rendermode` directive. The following example shows setting the render mode in a hypothetical sales forecast (`SalesForecast`) component definition file (`.razor`).

  At the top of `Components/Pages/SalesForecast.razor` file:

  ```
  @page "/sales-forecast"
  @rendermode InteractiveServer
  ```

Using the preceding approaches, you can apply a render mode on a per-page/component basis. However, an entire app can adopt a single render mode via a root component that then by inheritance sets the render mode of every other component loaded. This is termed *global interactivity*, as opposed to *per-page/component interactivity*. Global interactivity is useful if most of the app requires interactive features. Global interactivity is usually applied via the `App` component, which is the root component of an app created from the Blazor Web App project template.

Note

More information on render modes and global interactivity is provided by Blazor's reference documentation. For the purposes of this tutorial, the app only adopts interactive SSR on a per-page/component basis. After the tutorial, you're free to use this app to study the other component render modes and the global interactivity location.

Open the movie `Index` component file (`Components/Pages/MoviePages/Index.razor`), and add the following `@rendermode` directive immediately after the `@page` directive to make the component interactive:

```
@rendermode InteractiveServer
```

To see how making a component interactive enhances the user experience, let's provide three enhancements to the app in the following sections:

* Add pagination to the movie `QuickGrid` component.

* Make the `QuickGrid` component *sortable*.

* Replace the HTML form for filtering movies by title with C# code that:

  * Runs on the server.
  * Renders content interactively over the underlying SignalR connection.

## Add pagination to the QuickGrid

The `QuickGrid` component can page data from the database.

Open the `Index` component (`Components/Pages/Movies/Index.razor`). Add a [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) instance to the `@code` block. Because the tutorial only uses five movie records for demonstration, set the [ItemsPerPage](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate.itemsperpage) to `2` items in order to demonstrate pagination. Normally, the number of items to display would be set to a higher value or set dynamically via a dropdown list.

```
private PaginationState pagination = new PaginationState { ItemsPerPage = 2 };
```

Set the `QuickGrid` component's [Pagination](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.pagination#microsoft-aspnetcore-components-quickgrid-quickgrid-1-pagination) property to `pagination`:

```
- <QuickGrid Class="table" Items="FilteredMovies">
+ <QuickGrid Class="table" Items="FilteredMovies" Pagination="pagination">
```

To provide a UI for pagination below the `QuickGrid` component, add a [`Paginator` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginator) below the `QuickGrid` component. Set the [Paginator.State](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginator.state) to `pagination`:

```
<Paginator State="pagination" />
```

Run the app and navigate to the movies `Index` page. You can page through the movie items at two movies per page:

The component is *interactive*. The page doesn't reload for paging to occur. The paging is performed live over the SignalR connection between the browser and the server, where the paging operation is performed on the server with the rendered result sent back to the client for the browser to display.

Change [ItemsPerPage](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate.itemsperpage) to a more reasonable value, such as five items per page:

```
- private PaginationState pagination = new PaginationState { ItemsPerPage = 2 };
+ private PaginationState pagination = new PaginationState { ItemsPerPage = 5 };
```

## Sortable QuickGrid

Open the `Index` component (`Components/Pages/Movies/Index.razor`).

Add `Sortable="true"` ([Sortable](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.sortable)) to the title [PropertyColumn\<TGridItem,TProp>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2):

```
- <PropertyColumn Property="movie => movie.Title" />
+ <PropertyColumn Property="movie => movie.Title" Sortable="true" />
```

You can sort the QuickGrid by movie title by selecting the **Title** column. The page doesn't reload for sorting to occur. The sorting is performed live over the SignalR connection, where the sorting operation is performed on the server with the rendered result sent back to the client:

## Use C# code and interactivity to search by title

In an earlier part of the tutorial series, the `Index` component was modified to allow the user to filter movies by title. This was accomplished by:

* Adding an HTML form that issues a GET request to the server with the user's title search string as a query string field-value pair (for example, `?titleFilter=road+warrior` if the user searches for "`road warrior`").
* Adding code to the component that obtains the title search string from the query string and uses it to filter the database's records.

The preceding approach is effective for a component that adopts static SSR, where the only interaction between the client and server is via HTTP requests. There was no live SignalR connection between the client and the server, and there was no way for the app on the server to process C# code *interactively* based on the user's actions in the component's UI and return content.

Now that the component is interactive, it can provide an improved user experience with Blazor features for binding and event handling.

Add a delegate event handler that the user can trigger to filter the database's movie records. The method uses the value of the `TitleFilter` property to perform the operation. If the user clears `TitleFilter` and searches, the method loads the entire movie list for display.

Delete the following lines from the `@code` block:

```
- [SupplyParameterFromQuery]
- private string? TitleFilter { get; set; }
    
- private IQueryable<Movie> FilteredMovies =>
-     context.Movie.Where(m => m.Title!.Contains(TitleFilter ?? string.Empty));
```

Replace the deleted code with the following code:

```
private string titleFilter = string.Empty;

private IQueryable<Movie> FilteredMovies => 
    context.Movie.Where(m => m.Title!.Contains(titleFilter));
```

Next, the component should bind the `titleFilter` field to an `<input>` element, so user input is stored in the `titleFilter` variable. Binding is achieved in Blazor with the `@bind` directive attribute.

Remove the HTML form from the component:

```
- <form action="/movies" data-enhance>
-     <input type="search" name="titleFilter" />
-     <input type="submit" value="Search" />
- </form>
```

In its place, add the following Razor markup:

```
<input type="search" @bind="titleFilter" @bind:event="oninput" />
```

`@bind:event="oninput"` performs binding for the HTML's `oninput` event, which fires when the `<input>` element's value is changed as a direct result of a user typing in the search box. The QuickGrid is bound to `FilteredMovies`. As `titleFilter` changes with the value of the search box, rerendering the QuickGrid bound to the `FilteredMovies` method filters movie entities based on the updated value of `titleFilter`.

Run the app, type "`road warrior`" into the search field and notice how the QuickGrid is filtered for each character entered until *The Road Warrior* movie is left when the search field reaches "`road `" ("road" followed by a space).

Run the app, type "`Road Warrior`" into the search field and notice how the QuickGrid is filtered for each character entered until *The Road Warrior* movie is left when the search field reaches "`Road `" ("Road" followed by a space).

Run the app, type "`Road Warrior`" into the search field and notice how the QuickGrid is filtered for each character entered until *The Road Warrior* movie is left when the search field reaches "`Road `" ("Road" followed by a space).

Filtering database records is performed on the server, and the server interactively sends back the HTML to display over the same SignalR connection. The page doesn't reload. The user feels like their interactions with the page are running code on the client. Actually, the code is running the server.

Instead of an HTML form, submitting a GET request in this scenario could've also used JavaScript to submit the request to the server, either using the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API)\` or [XMLHttpRequest API](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest). In most cases, JavaScript can be replaced by using Blazor and C# in an interactive component.

## Style the `QuickGrid` component

You can apply styles to the rendered `QuickGrid` component with a stylesheet isolated to the `Index` component using *CSS isolation*.

CSS isolation is applied by adding a stylesheet file using the file name format `{COMPONENT NAME}.razor.css`, where the `{COMPONENT NAME}` placeholder is the component name.

To apply styles to a child component, such as the `QuickGrid` component of the `Index` component, use the `::deep` pseudo-element.

In the `MoviePages` folder, add the following stylesheet for the `Index` component. Use `::deep` pseudo-elements to make the row height `3em` and vertically center the table cell content.

`Components/Pages/MoviePages/Index.razor.css`:

```
::deep tr {
    height: 3em;
}

    ::deep tr > td {
        vertical-align: middle;
    }
```

The `::deep` pseudo-element only works with descendant elements, so the `QuickGrid` component must be wrapped with a `<div>` or some other block-level element in order to apply the styles to it.

In `Components/Pages/MoviePages/Index.razor`, place `<div>` tags around the `QuickGrid` component:

```
+ <div>
    <QuickGrid ...>
        ...
    </QuickGrid>
+ </div>
```

Blazor rewrites CSS selectors to match the markup rendered by the component. The rewritten CSS styles are bundled and produced as a static asset, so you don't need to take further action to apply the styles to the rendered `QuickGrid` component.

## Clean up

When you're finished with the tutorial and delete the sample app from your local system, you can also delete the `BlazorWebAppMovies` database in Visual Studio's SQL Server Object Explorer (SSOX):

1. Access SSOX by selecting **View** > **SQL Server Object Explorer** from the menu bar.
2. Display the database list by selecting the triangles next to `SQL Server` > `(localdb)\MSSQLLocalDB` > `Databases`.
3. Right-click the `BlazorWebAppMovies` database in the list and select **Delete**.
4. Select the checkbox to close existing connections before selecting **OK**.

When deleting the database in SSOX, the database's physical files are removed from your Windows user folder.

When you're finished with the tutorial and delete the sample app from your local system, you can also manually delete the `BlazorWebAppMovies` database. The database's location varies depending on the platform and operating system, but you can search for it by the file name indicated in the database connection string of app settings file (`appsettings.json`).

When you're finished with the tutorial and delete the sample app from your local system, you can also manually delete the `BlazorWebAppMovies` database. The database's location varies depending on the platform and operating system, but you can search for it by the file name indicated in the database connection string of the app settings file (`appsettings.json`).

## Congratulations!

Congratulations on completing the tutorial series! We hope you enjoyed this tutorial on Blazor. Blazor offers many more features than we were able to cover in this series, and we invite you to explore the Blazor documentation, examples, and sample apps to learn more. *Happy coding with Blazor!*

## Next steps

If you're new to Blazor, we recommend reading the following Blazor articles that cover important general information for Blazor development:

* [ASP.NET Core Blazor](../../?view=aspnetcore-10.0)
* [ASP.NET Core Blazor supported platforms](../../supported-platforms?view=aspnetcore-10.0)
* [Tooling for ASP.NET Core Blazor](../../tooling?view=aspnetcore-10.0)
* [ASP.NET Core Blazor hosting models](../../hosting-models?view=aspnetcore-10.0)
* [ASP.NET Core Blazor project structure](../../project-structure?view=aspnetcore-10.0)
* [ASP.NET Core Blazor fundamentals](../../fundamentals/?view=aspnetcore-10.0) and the other *Fundamentals* node articles.
* [ASP.NET Core Razor components](../../components/?view=aspnetcore-10.0) and the other *Components* node articles.
* [ASP.NET Core Blazor forms overview](../../forms/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor JavaScript interoperability (JS interop)](../../javascript-interoperability/?view=aspnetcore-10.0) and the other *JavaScript interop* articles.
* [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0)
* [Host and deploy ASP.NET Core Blazor](../../host-and-deploy/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor with Entity Framework Core (EF Core)](../../blazor-ef-core?view=aspnetcore-10.0) covers concurrency with EF Core in Blazor apps.

For guidance on adding a thumbnail file upload feature to this tutorial's sample app, see [ASP.NET Core Blazor file uploads](../../file-uploads?view=aspnetcore-10.0#save-small-files-directly-to-a-database-with-ef-core).

In the documentation website's sidebar navigation, articles are organized by subject matter and laid out in roughly in a general-to-specific or basic-to-complex order. The best approach when starting to learn about Blazor is to read down the table of contents from top to bottom.

## Troubleshoot with the completed sample

If you run into a problem while following the tutorial that you can't resolve from the text, compare your code to the completed project in the Blazor samples repository:

[Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)

Select the latest version folder. The sample folder for this tutorial's project is named `BlazorWebAppMovies`.

[Previous: Add a new field](part-7?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/security/authorization/claims
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/security/authorization/claims.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Claim-based authorization in ASP.NET Core Razor Pages

When an identity is created for an app user upon signing into an app, the identity provider may assign one or more [claims](/en-us/dotnet/api/system.security.claims.claim#remarks) to the user's identity. A claim is a name value pair that represents what the subject (a user, an app or service, or a device/computer) is, not what the subject can do. A claim can be evaluated by the app to determine access rights to data and other secured resources during the process of authorization and can also be used to make or express authentication decisions about a subject. An identity can contain multiple claims with multiple values and can contain multiple claims of the same type. This article explains how to add claims checks for authorization in an ASP.NET Core app.

This article uses Razor Pages examples and focuses on Razor Pages authorization scenarios. For Blazor and MVC guidance, see the following resources:

* [Claim-based authorization in ASP.NET Core](../../../security/authorization/claims?view=aspnetcore-10.0)
* [Claim-based authorization in ASP.NET Core MVC](../../../mvc/security/authorization/claims?view=aspnetcore-10.0)

Examples throughout this article apply claim-based authorization via one or more [`[Authorize]` attributes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) on `PageModel` classes. Alternatively, claim-based authorization can be applied using *conventions*. For more information, see [Razor Pages route and app conventions in ASP.NET Core](../../razor-pages-conventions?view=aspnetcore-10.0#page-model-action-conventions).

## Sample app

The sample app for this article is the [`WebAll` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/claims) ([how to download](../../../overview?view=aspnetcore-10.0#how-to-download-a-sample)). For more information, see the sample's README file (`README.md`).

## Add claim checks

Claim-based authorization checks:

* Are declarative.
* Are applied to Razor Pages, MVC controllers, or actions within a controller.
* Can't be applied at the Razor Page handler level. They must be applied to the page model class.

Claims in code specify claims which the current user must possess, and optionally the value the claim must hold to access the requested resource. Claims requirements are policy based. The developer must build and register a policy expressing the claims requirements.

The simplest type of claim policy looks for the presence of a claim and doesn't check the value.

Build and register the policy and call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) (place the call after the line that calls [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)). Registering the policy takes place as part of the Authorization service configuration, typically in the `Program` file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

Build and register the policy and call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) (place the call after the line that calls [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)). Registering the policy takes place as part of the Authorization service configuration, typically in the `Program` file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorization(options =>
{
   options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

Build and register the policy in `Startup.ConfigureServices` (`Startup.cs`) in the Authorization service's configuration:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("EmployeeOnly", 
        policy => policy.RequireClaim("EmployeeNumber"));
});
```

Call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) in `Startup.Configure` (`Startup.cs`) immediately after [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) is called:

```
app.UseAuthorization();
```

Apply the policy using the `Policy` property on the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute to specify the policy name. In the following example, the `EmployeeOnly` policy checks for the presence of an `EmployeeNumber` claim on the current identity:

```
[Authorize(Policy = "EmployeeOnly")]
public class IndexModel : PageModel
{
    public void OnGet()
    {

    }
}
```

Filter attributes, including the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute), can only be applied to the entire `PageModel` class and can't be applied to specific page handler methods. If you need to implement different authorization rules for different page handlers, adopt either of the following approaches.

* Use separate Razor Pages for operations requiring different authorization levels, using [partial views](../../../mvc/views/partial?view=aspnetcore-10.0) for shared content.

* Inject [IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice) and manually check the authorization policy by calling [IAuthorizationService.AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) within handler methods. If authorization fails, the handler returns a `Forbid` result ([ForbidResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.forbidresult)).

  The following example demonstrates the approach:

  * The page's `OnGet` handler requires a Sid claim via the `RequireSidClaim` policy.
  * The page's `OnPostAsync` handler requires an email claim via the `RequireEmailClaim` policy.

  Note

  Constructor injection of [IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice) in the following example is supported with [primary constructors](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) in C# 12 (.NET 8) or later.

  ```
  public class AuthPageHandlersExampleModel(
      IAuthorizationService authorizationService) : PageModel
  {
      public async Task<IActionResult> OnGet()
      {
          var authResult = 
              await authorizationService.AuthorizeAsync(User, "RequireSidClaim");

          if (!authResult.Succeeded)
          {
              return Forbid();
          }

          // Authorized logic

          return Page();
      }

      public async Task<IActionResult> OnPostAsync()
      {
          var authResult = 
              await authorizationService.AuthorizeAsync(User, "RequireEmailClaim");

          if (!authResult.Succeeded)
          {
              return Forbid();
          }

          // Authorized logic

          return Page();
      }
  }
  ```

  Alternatively, page handler methods can check claims directly by calling [HasClaim](/en-us/dotnet/api/system.security.claims.claimsidentity.hasclaim):

  ```
  public IActionResult OnGet()
  {
      if (!User.HasClaim(c => c.Type == ClaimTypes.Sid))
      {
          return Forbid();
      }

      // Authorized logic

      return Page();
  }

  public IActionResult OnPostAsync()
  {
      if (!User.HasClaim(c => c.Type == ClaimTypes.Email))
      {
          return Forbid();
      }

      // Authorized logic

      return Page();
  }
  ```

  Note

  [ClaimTypes](/en-us/dotnet/api/system.security.claims.claimtypes) is in the [System.Security.Claims](/en-us/dotnet/api/system.security.claims) namespace.

You can specify a list of allowed values when creating a policy. The following policy only passes for employees whose employee number is 1, 2, 3, 4, or 5:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("Founders", policy =>
        policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Founders", policy =>
                      policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("Founders", policy =>
        policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});
```

### Add a generic claim check

If the claim value isn't a single value or you need more flexible claim evaluation logic, such as pattern matching, checking the claim issuer, or parsing complex claim values, use [RequireAssertion](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireassertion) with [HasClaim](/en-us/dotnet/api/system.security.claims.claimsprincipal.hasclaim). For example, the following policy requires that the user's `email` claim ends with a specific domain:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ContosoOnly", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                c.Type == "email" &&
                c.Value.EndsWith("@contoso.com", StringComparison.OrdinalIgnoreCase))));
```

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ContosoOnly", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                c.Type == "email" &&
                c.Value.EndsWith("@contoso.com", StringComparison.OrdinalIgnoreCase))));
});
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ContosoOnly", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                c.Type == "email" &&
                c.Value.EndsWith("@contoso.com", StringComparison.OrdinalIgnoreCase))));
});
```

For more information, see [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0#use-a-func-to-fulfill-a-policy).

## Evaluate multiple policies

If multiple policies are applied at the controller and action levels, ***all*** policies must pass before access is granted. In the following sample, both page handler methods must fulfill *both* the `EmployeeOnly` policy and the `HumanResources` policy:

```
[Authorize(Policy = "EmployeeOnly")]
[Authorize(Policy = "HumanResources")]
public class SalaryModel : PageModel
{
    public ContentResult OnGetPayStub()
    {
        return Content("OnGetPayStub");
    }

    public ContentResult OnGetSalary()
    {
        return Content("OnGetSalary");
    }
}
```

If you want more complicated policies, such as taking a date of birth claim, calculating an age from it then checking the age is 21 or older then you need to write [custom policy handlers](../../../security/authorization/policies?view=aspnetcore-10.0).

## Claim case sensitivity

Claim *values* are compared using [`StringComparison.Ordinal`](/en-us/dotnet/api/system.stringcomparison). This means values such as `Admin` (uppercase `A`) and `admin` (lowercase `a`) are always treated as different claim values, regardless of which authentication handler created the identity.

Separately, claim *type* comparison (used to locate claims by type, such as `EmployeeNumber`, `department`, or `http://schemas.microsoft.com/ws/2008/06/identity/claims/role`) may be case-sensitive or case-insensitive depending on the [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) implementation. With `Microsoft.IdentityModel` in ASP.NET Core 8.0 or later (used by [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer), [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect), [AddWsFederation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.wsfederationextensions.addwsfederation), and [AddMicrosoftIdentityWebApp](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapp)/[AddMicrosoftIdentityWebApi](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapi)), [CaseSensitiveClaimsIdentity](/en-us/dotnet/api/microsoft.identitymodel.tokens.casesensitiveclaimsidentity) is produced during token validation, which uses case-sensitive claim type matching.

The default [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) provided by the .NET runtime (used in most cases, including all cookie-based flows) still uses case-insensitive claim type matching.

In practice, this distinction rarely matters when the same claim types are issued and checked consistently. Role authorization follows the same rules because roles are represented as claims. Always use consistent casing for claim values and claim types to avoid subtle issues.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/customize-openapi
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/openapi/customize-openapi.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Customize OpenAPI documents

[]()

## OpenAPI document transformers

Transformers provide an API for modifying the OpenAPI document with user-defined customizations. Transformers are useful for scenarios like:

* Adding parameters to all operations in a document.
* Modifying descriptions for parameters or operations.
* Adding top-level information to the OpenAPI document.

Transformers fall into three categories:

* Document transformers have access to the entire OpenAPI document. These can be used to make global modifications to the document.
* Operation transformers apply to each individual operation. Each individual operation is a combination of path and HTTP method. These can be used to modify parameters or responses on endpoints.
* Schema transformers apply to each schema in the document. These can be used to modify the schema of request or response bodies, or any nested schemas.

Transformers can be registered onto the document by calling the [AddDocumentTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.adddocumenttransformer) method on the [OpenApiOptions](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions) object. The following snippet shows different ways to register transformers onto the document:

* Register a document transformer using a delegate.
* Register a document transformer using an instance of [IOpenApiDocumentTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapidocumenttransformer).
* Register a document transformer using a DI-activated [IOpenApiDocumentTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapidocumenttransformer).
* Register an operation transformer using a delegate.
* Register an operation transformer using an instance of [IOpenApiOperationTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapioperationtransformer).
* Register an operation transformer using a DI-activated [IOpenApiOperationTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapioperationtransformer).
* Register a schema transformer using a delegate.
* Register a schema transformer using an instance of [IOpenApiSchemaTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapischematransformer).
* Register a schema transformer using a DI-activated [IOpenApiSchemaTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapischematransformer).

```
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken)
                             => Task.CompletedTask);
    options.AddDocumentTransformer(new MyDocumentTransformer());
    options.AddDocumentTransformer<MyDocumentTransformer>();
    options.AddOperationTransformer((operation, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddOperationTransformer(new MyOperationTransformer());
    options.AddOperationTransformer<MyOperationTransformer>();
    options.AddSchemaTransformer((schema, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddSchemaTransformer(new MySchemaTransformer());
    options.AddSchemaTransformer<MySchemaTransformer>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

### Execution order for transformers

Transformers execute in the following order:

* Schema transformers execute when a schema is registered to the document. They execute in the order they're added. All schemas are added to the document before any operation processing occurs, so schema transformers execute before operation transformers.
* Operation transformers execute when an operation is added to the document. They execute in the order they're added. All operations are added to the document before any document transformers execute.
* Document transformers execute when the document is generated. This is the final pass over the document, and all operations and schemas are added by this point.
* When an app is configured to generate multiple OpenAPI documents, transformers execute for each document independently.

For example, in the following snippet:

* `SchemaTransformer2` is executed and has access to the modifications made by `SchemaTransformer1`.
* Both `OperationTransformer1` and `OperationTransformer2` have access to the modifications made by both schema transformers for the types involved in the operation they're called to process.
* `OperationTransformer2` is executed after `OperationTransformer1`, so it has access to the modifications made by `OperationTransformer1`.
* Both `DocumentTransformer1` and `DocumentTransformer2` are executed after all operations and schemas have been added to the document, so they have access to all modifications made by the operation and schema transformers.
* `DocumentTransformer2` is executed after `DocumentTransformer1`, so it has access to the modifications made by `DocumentTransformer1`.

```
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<DocumentTransformer1>();
    options.AddSchemaTransformer<SchemaTransformer1>();
    options.AddDocumentTransformer<DocumentTransformer2>();
    options.AddOperationTransformer<OperationTransformer1>();
    options.AddSchemaTransformer<SchemaTransformer2>();
    options.AddOperationTransformer<OperationTransformer2>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

## Use document transformers

Document transformers have access to a context object that includes:

* The name of the document being modified.
* The [ApiDescriptionGroups](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescriptiongroupcollectionprovider.apidescriptiongroups#microsoft-aspnetcore-mvc-apiexplorer-apidescriptiongroupcollectionprovider-apidescriptiongroups) associated with that document.
* The [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) used in document generation.

Document transformers can also mutate the OpenAPI document that is generated. The following example demonstrates a document transformer that adds some information about the API to the OpenAPI document.

```
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info = new()
        {
            Title = "Checkout API",
            Version = "v1",
            Description = "API for processing checkouts from cart."
        };
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

Service-activated document transformers can utilize instances from DI to modify the app. The following sample demonstrates a document transformer that uses the [IAuthenticationSchemeProvider](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationschemeprovider) service from the authentication layer. It checks if any JWT bearer-related schemes are registered in the app and adds them to the OpenAPI document's top level:

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            var securitySchemes = new Dictionary<string, IOpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = securitySchemes;
        }
    }
}
```

Document transformers are unique to the document instance they're associated with. In the following example, a transformer:

* Registers authentication-related requirements to the `internal` document.
* Leaves the `public` document unmodified.

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi("internal", options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
builder.Services.AddOpenApi("public");

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/world", () => "Hello world!")
    .WithGroupName("internal");
app.MapGet("/", () => "Hello universe!")
    .WithGroupName("public");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            // Add the security scheme at the document level
            var securitySchemes = new Dictionary<string, IOpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = securitySchemes;

            // Apply it as a requirement for all operations
            foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations))
            {
                operation.Value.Security ??= [];
                operation.Value.Security.Add(new OpenApiSecurityRequirement
                {
                    [new OpenApiSecuritySchemeReference("Bearer", document)] = []
                });
            }
        }
    }
}
```

## Use operation transformers

Operations are unique combinations of HTTP paths and methods in an OpenAPI document. Operation transformers are helpful when a modification:

* Should be made to each endpoint in an app, or
* Conditionally applied to certain routes.

Operation transformers have access to a context object which contains:

* The name of the document the operation belongs to.
* The [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription) associated with the operation.
* The [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) used in document generation.

For example, the following operation transformer adds `500` as a response status code supported by all operations in the document.

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken) =>
    {
        operation.Responses ??= new OpenApiResponses();
        operation.Responses.Add("500", new OpenApiResponse { Description = "Internal server error" });
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

Operation transformers can also be added to specific endpoint with the [AddOpenApiOperationTransformer](/en-us/dotnet/api/microsoft.aspnetcore.builder.openapiendpointconventionbuilderextensions.addopenapioperationtransformer) API, instead of all endpoints in a document. This can be useful to change specific OpenAPI data for a specific endpoint, like adding a security scheme, response description or other OpenAPI operation properties. The following example demonstrates adding an operation transformer to a deprecated endpoint specifically, which marks the endpoint as deprecated in the OpenAPI document.

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/old", () => "This endpoint is old and should not be used anymore")
.AddOpenApiOperationTransformer((operation, context, cancellationToken) =>
{
    operation.Deprecated = true;
    return Task.CompletedTask;
});

app.MapGet("/new", () => "This endpoint replaces /old");

app.Run();
```

### Conditionally applying security requirements

In some scenarios, developers may want to apply security requirements to all endpoints except those explicitly marked with the `AllowAnonymous` attribute.

Use an operation transformer, which has access to endpoint metadata through the associated [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription).

The following example demonstrates how to skip adding a security requirement for endpoints that have the `AllowAnonymousAttribute` applied:

```
internal sealed class AuthOperationTransformer : IOpenApiOperationTransformer
{
    public Task TransformAsync(
        OpenApiOperation operation,
        OpenApiOperationTransformerContext context,
        CancellationToken cancellationToken)
    {
        var hasAllowAnonymous = context.Description.ActionDescriptor.EndpointMetadata
            .OfType<AllowAnonymousAttribute>()
            .Any();

        if (hasAllowAnonymous)
        {
            return Task.CompletedTask;
        }

        operation.Security ??= new List<OpenApiSecurityRequirement>();

        operation.Security.Add(new OpenApiSecurityRequirement
        {
            [new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Id = "Bearer",
                    Type = ReferenceType.SecurityScheme
                }
            }] = Array.Empty<string>()
        });

        return Task.CompletedTask;
    }
}
```

Use this approach instead of document transformers when conditional logic based on endpoint metadata is required. This transformer adds security *requirements* per operation and assumes the security *scheme* is already registered at the document level. For an example of registering the Bearer security scheme, see the `BearerSecuritySchemeTransformer` in the [Use document transformers](#use-document-transformers) section.

## Use schema transformers

Schemas are the data models that are used in request and response bodies in an OpenAPI document. Schema transformers are useful when a modification:

* Should be made to each schema in the document, or
* Conditionally applied to certain schemas.

Schema transformers have access to a context object which contains:

* The name of the document the schema belongs to.
* The JSON type information associated with the target schema.
* The [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) used in document generation.

For example, the following schema transformer sets the `format` of decimal types to `decimal` instead of `double`:

```
using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options => {
    // Schema transformer to set the format of decimal to 'decimal'
    options.AddSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.JsonTypeInfo.Type == typeof(decimal))
        {
            schema.Format = "decimal";
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => new Body { Amount = 1.1m });

app.Run();

public class Body {
    public decimal Amount { get; set; }
}
```

## Support for generating OpenApiSchemas in transformers

Developers can generate a schema for a C# type using the same logic as ASP.NET Core OpenAPI document generation and add it to the OpenAPI document. The schema can then be referenced from elsewhere in the OpenAPI document. This capability is available starting with .NET 10.

The context passed to document, operation, and schema transformers includes a new `GetOrCreateSchemaAsync` method that can be used to generate a schema for a type. This method also has an optional `ApiParameterDescription` parameter to specify additional metadata for the generated schema.

To support adding the schema to the OpenAPI document, a `Document` property has been added to the Operation and Schema transformer contexts. This allows any transformer to add a schema to the OpenAPI document using the document's `AddComponent` method.

### Example

To use this feature in a document, operation, or schema transformer, create the schema using the `GetOrCreateSchemaAsync` method provided in the context and add it to the OpenAPI document using the document's `AddComponent` method.

```
builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer(async (operation, context, cancellationToken) =>
    {
        // Generate schema for error responses
        var errorSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), null, cancellationToken);
        context.Document?.AddComponent("Error", errorSchema);

        operation.Responses ??= new OpenApiResponses();
        // Add a "4XX" response to the operation with the newly created schema
        operation.Responses["4XX"] = new OpenApiResponse
        {
            Description = "Bad Request",
            Content = new Dictionary<string, OpenApiMediaType>
            {
                ["application/problem+json"] = new OpenApiMediaType
                {
                    Schema = new OpenApiSchemaReference("Error", context.Document)
                }
            }
        };
    });
});
```

## Customize schema reuse

After all transformers have been applied, the framework makes a pass over the document to transfer certain schemas to the `components.schemas` section, replacing them with `$ref` references to the transferred schema. This reduces the size of the document and makes it easier to read.

The details of this processing are complicated and might change in future versions of .NET, but in general:

* Schemas for class/record/struct types are replaced with a `$ref` to a schema in `components.schemas` if they appear more than once in the document.
* Schemas for primitive types and standard collections are left inline.
* Schemas for enum types are always replaced with a `$ref` to a schema in components.schemas.

Typically the name of the schema in `components.schemas` is the name of the class/record/struct type, but in some circumstances a different name must be used.

ASP.NET Core lets you customize which schemas are replaced with a `$ref` to a schema in `components.schemas` using the [CreateSchemaReferenceId](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.createschemareferenceid#microsoft-aspnetcore-openapi-openapioptions-createschemareferenceid) property of [OpenApiOptions](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions). This property is a delegate that takes a [JsonTypeInfo](/en-us/dotnet/api/system.text.json.serialization.metadata.jsontypeinfo) object and returns the name of the schema in `components.schemas` that should be used for that type. The framework provides a default implementation of this delegate, [CreateDefaultSchemaReferenceId](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.createdefaultschemareferenceid) that uses the name of the type, but you can replace it with your own implementation.

As a simple example of this customization, you might choose to always inline enum schemas. This is done by setting [CreateSchemaReferenceId](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.createschemareferenceid#microsoft-aspnetcore-openapi-openapioptions-createschemareferenceid) to a delegate that returns null for enum types, and otherwise returns the value from the default implementation. The following code demonstrates this:

```
builder.Services.AddOpenApi(options =>
{
    // Always inline enum schemas
    options.CreateSchemaReferenceId = (type) =>
        type.Type.IsEnum ? null : OpenApiOptions.CreateDefaultSchemaReferenceId(type);
});
```

Note

Starting with .NET 10, relative JSON schema references (`$ref`) within the root schema document are resolved correctly during OpenAPI document generation.

## Related content

* [Microsoft.OpenApi upgraded to 3.x (ASP.NET Core 11.0 breaking change announcement)](/en-us/aspnet/core/breaking-changes/11/microsoft-openapi-3x)
* [OpenAPI 3.x upgrade migration guidance](../../migration/100-to-110?view=aspnetcore-10.0#microsoftopenapi-upgraded-to-3x)
* [Use the generated OpenAPI documents](using-openapi-documents?view=aspnetcore-10.0)
* [OpenAPI specification](https://spec.openapis.org/oas/v3.0.3)

[]()

## OpenAPI document transformers

Transformers provide an API for modifying the OpenAPI document with user-defined customizations. Transformers are useful for scenarios like:

* Adding parameters to all operations in a document.
* Modifying descriptions for parameters or operations.
* Adding top-level information to the OpenAPI document.

Transformers fall into three categories:

* Document transformers have access to the entire OpenAPI document. These can be used to make global modifications to the document.
* Operation transformers apply to each individual operation. Each individual operation is a combination of path and HTTP method. These can be used to modify parameters or responses on endpoints.
* Schema transformers apply to each schema in the document. These can be used to modify the schema of request or response bodies, or any nested schemas.

Transformers can be registered onto the document by calling the [AddDocumentTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.adddocumenttransformer) method on the [OpenApiOptions](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions) object. The following snippet shows different ways to register transformers onto the document:

* Register a document transformer using a delegate.
* Register a document transformer using an instance of [IOpenApiDocumentTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapidocumenttransformer).
* Register a document transformer using a DI-activated [IOpenApiDocumentTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapidocumenttransformer).
* Register an operation transformer using a delegate.
* Register an operation transformer using an instance of [IOpenApiOperationTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapioperationtransformer).
* Register an operation transformer using a DI-activated [IOpenApiOperationTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapioperationtransformer).
* Register a schema transformer using a delegate.
* Register a schema transformer using an instance of [IOpenApiSchemaTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapischematransformer).
* Register a schema transformer using a DI-activated [IOpenApiSchemaTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapischematransformer).

```
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken)
                             => Task.CompletedTask);
    options.AddDocumentTransformer(new MyDocumentTransformer());
    options.AddDocumentTransformer<MyDocumentTransformer>();
    options.AddOperationTransformer((operation, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddOperationTransformer(new MyOperationTransformer());
    options.AddOperationTransformer<MyOperationTransformer>();
    options.AddSchemaTransformer((schema, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddSchemaTransformer(new MySchemaTransformer());
    options.AddSchemaTransformer<MySchemaTransformer>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

### Execution order for transformers

Transformers are executed as follows:

* Schema transformers are executed when a schema is registered to the document. Schema transformers are executed in the order in which they were added. All schemas are added to the document before any operation processing occurs, so all schema transformers are executed before any operation transformers.
* Operation transformers are executed when an operation is added to the document. Operation transformers are executed in the order in which they were added. All operations are added to the document before any document transformers are executed.
* Document transformers are executed when the document is generated. This is the final pass over the document, and all operations and schemas have been add by this point.
* When an app is configured to generate multiple OpenAPI documents, transformers are executed for each document independently.

For example, in the following snippet:

* `SchemaTransformer2` is executed and has access to the modifications made by `SchemaTransformer1`.
* Both `OperationTransformer1` and `OperationTransformer2` have access to the modifications made by both schema transformers for the types involved in the operation they are called to process.
* `OperationTransformer2` is executed after `OperationTransformer1`, so it has access to the modifications made by `OperationTransformer1`.
* Both `DocumentTransformer1` and `DocumentTransformer2` are executed after all operations and schemas have been added to the document, so they have access to all modifications made by the operation and schema transformers.
* `DocumentTransformer2` is executed after `DocumentTransformer1`, so it has access to the modifications made by `DocumentTransformer1`.

```
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<DocumentTransformer1>();
    options.AddSchemaTransformer<SchemaTransformer1>();
    options.AddDocumentTransformer<DocumentTransformer2>();
    options.AddOperationTransformer<OperationTransformer1>();
    options.AddSchemaTransformer<SchemaTransformer2>();
    options.AddOperationTransformer<OperationTransformer2>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

## Use document transformers

Document transformers have access to a context object that includes:

* The name of the document being modified.
* The [ApiDescriptionGroups](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescriptiongroupcollectionprovider.apidescriptiongroups#microsoft-aspnetcore-mvc-apiexplorer-apidescriptiongroupcollectionprovider-apidescriptiongroups) associated with that document.
* The [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) used in document generation.

Document transformers can also mutate the OpenAPI document that is generated. The following example demonstrates a document transformer that adds some information about the API to the OpenAPI document.

```
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info = new()
        {
            Title = "Checkout API",
            Version = "v1",
            Description = "API for processing checkouts from cart."
        };
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

Service-activated document transformers can utilize instances from DI to modify the app. The following sample demonstrates a document transformer that uses the [IAuthenticationSchemeProvider](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationschemeprovider) service from the authentication layer. It checks if any JWT bearer-related schemes are registered in the app and adds them to the OpenAPI document's top level:

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;
        }
    }
}
```

Document transformers are unique to the document instance they're associated with. In the following example, a transformer:

* Registers authentication-related requirements to the `internal` document.
* Leaves the `public` document unmodified.

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi("internal", options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
builder.Services.AddOpenApi("public");

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/world", () => "Hello world!")
    .WithGroupName("internal");
app.MapGet("/", () => "Hello universe!")
    .WithGroupName("public");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            // Add the security scheme at the document level
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;

            // Apply it as a requirement for all operations
            foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations))
            {
                operation.Value.Security.Add(new OpenApiSecurityRequirement
                {
                    [new OpenApiSecurityScheme { Reference = new OpenApiReference { Id = "Bearer", Type = ReferenceType.SecurityScheme } }] = Array.Empty<string>()
                });
            }
        }
    }
}
```

## Use operation transformers

Operations are unique combinations of HTTP paths and methods in an OpenAPI document. Operation transformers are helpful when a modification:

* Should be made to each endpoint in an app, or
* Conditionally applied to certain routes.

Operation transformers have access to a context object which contains:

* The name of the document the operation belongs to.
* The [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription) associated with the operation.
* The [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) used in document generation.

For example, the following operation transformer adds `500` as a response status code supported by all operations in the document.

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken) =>
    {
        operation.Responses.Add("500", new OpenApiResponse { Description = "Internal server error" });
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

## Use schema transformers

Schemas are the data models that are used in request and response bodies in an OpenAPI document. Schema transformers are useful when a modification:

* Should be made to each schema in the document, or
* Conditionally applied to certain schemas.

Schema transformers have access to a context object which contains:

* The name of the document the schema belongs to.
* The JSON type information associated with the target schema.
* The [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) used in document generation.

For example, the following schema transformer sets the `format` of decimal types to `decimal` instead of `double`:

```
using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options => {
    // Schema transformer to set the format of decimal to 'decimal'
    options.AddSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.JsonTypeInfo.Type == typeof(decimal))
        {
            schema.Format = "decimal";
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => new Body { Amount = 1.1m });

app.Run();

public class Body {
    public decimal Amount { get; set; }
}
```

## Customize schema reuse

After all transformers have been applied, the framework makes a pass over the document to transfer certain schemas to the `components.schemas` section, replacing them with `$ref` references to the transferred schema. This reduces the size of the document and makes it easier to read.

The details of this processing are complicated and might change in future versions of .NET, but in general:

* Schemas for class/record/struct types are replaced with a `$ref` to a schema in `components.schemas` if they appear more than once in the document.
* Schemas for primitive types and standard collections are left inline.
* Schemas for enum types are always replaced with a `$ref` to a schema in components.schemas.

Typically the name of the schema in `components.schemas` is the name of the class/record/struct type, but in some circumstances a different name must be used.

ASP.NET Core lets you customize which schemas are replaced with a `$ref` to a schema in `components.schemas` using the [CreateSchemaReferenceId](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.createschemareferenceid#microsoft-aspnetcore-openapi-openapioptions-createschemareferenceid) property of [OpenApiOptions](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions). This property is a delegate that takes a [JsonTypeInfo](/en-us/dotnet/api/system.text.json.serialization.metadata.jsontypeinfo) object and returns the name of the schema in `components.schemas` that should be used for that type. The framework provides a default implementation of this delegate, [CreateDefaultSchemaReferenceId](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.createdefaultschemareferenceid) that uses the name of the type, but you can replace it with your own implementation.

As a simple example of this customization, you might choose to always inline enum schemas. This is done by setting [CreateSchemaReferenceId](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.createschemareferenceid#microsoft-aspnetcore-openapi-openapioptions-createschemareferenceid) to a delegate that returns null for enum types, and otherwise returns the value from the default implementation. The following code shows how to do this:

```
builder.Services.AddOpenApi(options =>
{
    // Always inline enum schemas
    options.CreateSchemaReferenceId = (type) =>
        type.Type.IsEnum ? null : OpenApiOptions.CreateDefaultSchemaReferenceId(type);
});
```

## Related content

* [Use the generated OpenAPI documents](using-openapi-documents?view=aspnetcore-10.0)
* [OpenAPI specification](https://spec.openapis.org/oas/v3.0.3)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-19

----
url: https://learn.microsoft.com/en-us/aspnet/core/get-started
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/get-started.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Get started with ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This tutorial shows how to create, run, and modify an ASP.NET Core Blazor Web App using the .NET CLI. *Blazor* is a .NET frontend web framework that supports both server-side rendering and client interactivity in a single programming model.

You'll learn how to:

* Create a Blazor Web App.
* Run the app.
* Change the app.
* Shut the app down.

## Prerequisites

Obtain and install the latest .NET SDK at [Download .NET](https://dotnet.microsoft.com/download/dotnet).

## Create a Blazor Web App

Open a command shell to a suitable location for the sample app and use the following command to create a Blazor Web App. The `-o|--output` option creates a folder for the project and names the project `BlazorSample`:

```
dotnet new blazor -o BlazorSample
```

## Run the app

Change the directory to the `BlazorSample` folder with the following command:

```
cd BlazorSample
```

The `dotnet watch` command runs the app and opens your default browser to the app's landing page:

```
dotnet watch
```

Using the app's sidebar navigation, visit the Counter page, where you can select the **Click me** button to increment the counter.

## Change the app

Leave the browser open with the Counter page loaded. By using the `dotnet watch` command to run the app, you can make changes to the app's markup and code without having to rebuild the app to reflect the changes in the browser.

The `Counter` Razor component that renders the Counter web page is located at `Components/Pages/Counter.razor` in the project. *Razor* is a syntax for combining HTML markup with C# code designed for developer productivity.

Open the `Counter.razor` file in a text editor and note a few interesting lines that render content and make the component's counter feature work.

`Components/Pages/Counter.razor`:

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

The file starts with a line that indicates the component's relative path (`/counter`):

```
@page "/counter"
```

The title of the page is set by `<PageTitle>` tags:

```
<PageTitle>Counter</PageTitle>
```

An H1 heading is displayed:

```
<h1>Counter</h1>
```

A paragraph element (`<p>`) displays the current count, which is stored in a variable named `currentCount`:

```
<p role="status">Current count: @currentCount</p>
```

A button (`<button>`) allows the user to increment the counter, which occurs when a button click executes a C# method named `IncrementCount`:

```
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
```

The `@code` block contains C# code that the component executes:

* The counter variable `currentCount` is established with an initial value of zero.
* The `IncrementCount` method is defined. The code within the method increments the `currentCount` variable by one each time the method is invoked.

```
private int currentCount = 0;

private void IncrementCount()
{
    currentCount++;
}
```

Let's change the increment of the counter in the `IncrementCount` method.

Change the line so that `currentCount` is incremented by a value of ten each time `IncrementCount` is called:

```
- currentCount++;
+ currentCount += 10;
```

Save the file.

As soon as you save the file, the running app is updated automatically because you used the `dotnet watch` command. Go back to the app in the browser and select the **Click me** button in the Counter page. Witness how the counter now increments from its existing value of one to a value of eleven. Each time the button is selected the value increments by ten.

## Shut the app down

Follow these steps:

* Close the browser window.
* To shut down the app, press `Ctrl`+`C` in the command shell.

*Congratulations!* You've successfully completed this tutorial.

## Next steps

In this tutorial, you learned how to:

* Create a Blazor Web App.
* Run the app.
* Change the app.
* Shut the app down.

This tutorial shows how to create and run an ASP.NET Core web app using the .NET CLI.

For Blazor tutorials, see [ASP.NET Core Blazor tutorials](blazor/tutorials/?view=aspnetcore-10.0).

You'll learn how to:

* Create a Razor Pages app.
* Run the app.
* Change the app.
* Shut the app down.

## Prerequisites

Obtain and install the latest .NET SDK at [Download .NET](https://dotnet.microsoft.com/download/dotnet).

## Create Razor Pages app

Open a command shell to a suitable location for the sample app and use the following command to create a Razor Pages app. The `-o|--output` option creates a folder for the project and names the project `RazorPagesSample`:

```
dotnet new webapp -o RazorPagesSample
```

## Run the app

Change the directory to the `RazorPagesSample` folder with the following command:

```
cd RazorPagesSample
```

The `dotnet watch` command runs the app and opens your default browser to the app's landing page:

```
dotnet watch
```

## Change the app

Open the `Pages/Index.cshtml` file in a text editor.

After the line with the "Welcome" greeting, add the following line to display the local system date and time:

```
<p>The time on the server is @DateTime.Now</p>
```

Save the changes.

As soon as you save the file, the running app is updated automatically because you used the `dotnet watch` command.

Refresh the page in the browser to see the result:

## Shut the app down

To shut down the app:

* Close the browser window.
* Press `Ctrl`+`C` in the command shell.

*Congratulations!* You've successfully completed this tutorial.

## Next steps

In this tutorial, you learned how to:

* Create a Razor Pages app.
* Run the app.
* Change the app.
* Shut the app down.

To learn more about the fundamentals of ASP.NET Core, see the following:

[ASP.NET Core fundamentals overview](fundamentals/?view=aspnetcore-10.0)

## Additional tutorials

| App type                        | Scenario                                          | Tutorials                                                                                                                                                                                              |
| ------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Web app                         | New server and client web development with Blazor | [Build a Blazor todo list app](blazor/tutorials/build-a-blazor-app?view=aspnetcore-10.0) and [Build a Blazor movie database app (Overview)](blazor/tutorials/movie-database-app/?view=aspnetcore-10.0) |
| Web API                         | Server-based data processing with Minimal APIs    | [Tutorial: Create a Minimal API with ASP.NET Core](tutorials/min-web-api?view=aspnetcore-10.0)                                                                                                         |
| Remote Procedure Call (RPC) app | Contract-first services using Protocol Buffers    | [Create a .NET gRPC client and server in ASP.NET Core](tutorials/grpc/grpc-start?view=aspnetcore-10.0)                                                                                                 |
| Real-time app                   | Server/client bidirectional communication         | [Get started with ASP.NET Core SignalR](tutorials/signalr?view=aspnetcore-10.0)                                                                                                                        |

| App type                        | Scenario                                          | Tutorials                                                                                                                                                                                              |
| ------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Web app                         | New server and client web development with Blazor | [Build a Blazor todo list app](blazor/tutorials/build-a-blazor-app?view=aspnetcore-10.0) and [Build a Blazor movie database app (Overview)](blazor/tutorials/movie-database-app/?view=aspnetcore-10.0) |
| Web API                         | Server-based data processing                      | [Tutorial: Create a controller-based web API with ASP.NET Core](tutorials/first-web-api?view=aspnetcore-10.0)                                                                                          |
| Remote Procedure Call (RPC) app | Contract-first services using Protocol Buffers    | [Create a .NET gRPC client and server in ASP.NET Core](tutorials/grpc/grpc-start?view=aspnetcore-10.0)                                                                                                 |
| Real-time app                   | Server/client bidirectional communication         | [Get started with ASP.NET Core SignalR](tutorials/signalr?view=aspnetcore-10.0)                                                                                                                        |

## Related content

* [Introduction to .NET](/en-us/dotnet/core/introduction)
* [Visual Studio](https://visualstudio.microsoft.com/)
* [Visual Studio Code](https://code.visualstudio.com/)
* [.NET Developer Community](https://dotnet.microsoft.com/platform/community)
* [.NET Live TV](https://live.dot.net)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/11/mvc-compatibility-options-removed
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/breaking-changes/11/mvc-compatibility-options-removed.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# MVC compatibility options removed

The MVC compatibility version APIs, such as `CompatibilityVersion` and `SetCompatibilityVersion`, are removed in ASP.NET Core 11. These APIs were no-ops since ASP.NET Core 3.0 and have been marked obsolete since ASP.NET Core 6.

## Version introduced

.NET 11 Preview 7

## Previous behavior

Previously, the compatibility version APIs existed and could be called, but they produced an obsoletion warning at build time and had no effect at runtime:

```
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Latest);
```

## New behavior

Starting in ASP.NET Core 11, these APIs no longer exist. Code that calls them fails to compile, and existing binaries that reference them fail at runtime with a [MissingMethodException](/en-us/dotnet/api/system.missingmethodexception) or [TypeLoadException](/en-us/dotnet/api/system.typeloadexception).

## Type of breaking change

This change can affect [source compatibility](/en-us/dotnet/core/compatibility/categories#source-compatibility) and [binary compatibility](/en-us/dotnet/core/compatibility/categories#binary-compatibility).

## Reason for change

The APIs have been marked obsolete since ASP.NET Core 6 and did nothing since ASP.NET Core 3.0, so they served no value. For more information, see [dotnet/aspnetcore#67077](https://github.com/dotnet/aspnetcore/pull/67077).

## Recommended action

Remove any usages of these APIs. For example, change the following code:

```
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Latest);
```

To:

```
services.AddMvc();
```

## Affected APIs

* `Microsoft.Extensions.DependencyInjection.MvcCoreMvcBuilderExtensions.SetCompatibilityVersion`
* `Microsoft.Extensions.DependencyInjection.MvcCoreMvcCoreBuilderExtensions.SetCompatibilityVersion`
* `Microsoft.AspNetCore.Mvc.Infrastructure.ConfigureCompatibilityOptions<TOptions>`
* `Microsoft.AspNetCore.Mvc.MvcCompatibilityOptions`
* `Microsoft.AspNetCore.Mvc.CompatibilityVersion`

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/class-libraries-and-static-server-side-rendering
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/class-libraries-and-static-server-side-rendering.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Razor class libraries (RCLs) with static server-side rendering (static SSR)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article provides guidance for component library authors considering support for static server-side rendering (static SSR).

Blazor encourages the development of an ecosystem of open-source and commercial component libraries, formally called *Razor class libraries (RCLs)*. Developers might also create reusable components for sharing components privately across apps within their own companies. Ideally, components are developed for compatibility with as many hosting models and rendering modes as possible. Static SSR introduces additional restrictions that can be more challenging to support than interactive rendering modes.

## Understand the capabilities and restrictions of static SSR

Static SSR is a mode in which components run when the server handles an incoming HTTP request. Blazor renders the component as HTML, which is included in the response. Once the response is sent, the server-side component and renderer state is discarded, so all that remains is HTML in the browser.

The benefit of this mode is cheaper, more scalable hosting, because no ongoing server resources are required to hold component state, no ongoing connection must be maintained between browser and server, and no WebAssembly payload is required in the browser.

All existing components can still be used with static SSR. However, the cost of this mode is that event handlers, such as `@onclick`†, can't be run for the following reasons:

* There's no .NET code in the browser to run them.
* The server has immediately discarded any component and renderer state that would be needed to execute event handlers or to rerender the same component instances.

†There's a special exception for the `@onsubmit` event handler for forms, which is always functional, regardless of render mode.

This is equivalent to how components behave during [prerendering](../fundamentals/?view=aspnetcore-10.0#client-and-server-rendering-concepts), before Blazor's SignalR circuit or the .NET WebAssembly runtime is started.

For components whose only role is to produce read-only DOM content, these behaviors for static SSR are completely sufficient. However, library authors must consider what approach to take when including interactive components in their libraries.

## Options for component authors

There are three main approaches:

* **Don't use interactive behaviors** (Basic)

  For components whose only role is to produce read-only DOM content, the developer isn't required to take any special action. These components naturally work with any render mode.

  Examples:

  * A "user card" component that loads data corresponding to a person and renders it in a stylized UI with a photo, job title, and other details.
  * A "video" component that acts as a wrapper around the HTML `<video>` element, making it more convenient to use in a Razor component.

* **Require interactive rendering** (Basic)

  You can choose to require that your component is only used with interactive rendering. This limits the applicability of your component, but means that you may freely rely on arbitrary event handlers. Even then, you should still avoid declaring a specific `@rendermode` and permit the app author who consumes your library to select one.

  Examples:

  * A video editing component in which users may splice and re-order segments of video. Even if there was a way to represent these edit operations with plain HTML buttons and form posts, the user experience would be unacceptable without true interactivity.
  * A collaborative document editor that must show the activities of other users in real time.

* **Use interactive behaviors, but design for static SSR and progressive enhancement** (Advanced)

  Many interactive behaviors can be implemented using only HTML capabilities. With a good understanding of HTML and CSS, you can often produce a useful baseline of functionality that works with static SSR. You can still declare event handlers that implement more advanced, optional behaviors, which only work in interactive render modes.

  Examples:

  * A grid component. Under static SSR, the component may only support displaying data and navigating between pages (implemented with `<a>` links). When used with interactive rendering, the component may add live sorting and filtering.
  * A tabset component. As long as navigation between tabs is achieved using `<a>` links and state is held only in URL query parameters, the component can work without `@onclick`.
  * An advanced file upload component. Under static SSR, the component may behave as a native `<input type=file>`. When used with interactive rendering, the component could also display upload progress.
  * A stock ticker. Under static SSR, the component may display the stock quote at the time the HTML was rendered. When used with interactive rendering, the component may then update the stock price in real time.

For any of these strategies, there's also the option of implementing interactive features with JavaScript.

To choose among these approaches, reusable Razor component authors must make a cost/benefit tradeoff. Your component is more useful and has a broader potential user base if it supports all render modes, including static SSR. However, it takes more work to design and implement a component that supports and takes best advantage of each render mode.

## When to use the `@rendermode` directive

In most cases, reusable component authors should **not** specify a render mode, even when interactivity is required. This is because the component author doesn't know whether the app enables support for [InteractiveServer](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactiveserver#microsoft-aspnetcore-components-web-rendermode-interactiveserver), [InteractiveWebAssembly](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactivewebassembly#microsoft-aspnetcore-components-web-rendermode-interactivewebassembly), or both with [InteractiveAuto](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactiveauto#microsoft-aspnetcore-components-web-rendermode-interactiveauto). By not specifying a `@rendermode`, the component author leaves the choice to the app developer.

Even if the component author thinks that interactivity is required, there may still be cases where an app author considers it sufficient to use static SSR alone. For example, a map component with drag and zoom interactivity may seem to require interactivity. However, some scenarios may only call for rendering a static map image and avoiding drag/zoom features.

The only reason why a reusable component author should use the `@rendermode` directive on their component is if the implementation is fundamentally coupled to one specific render mode and would certainly cause an error if used in a different mode. Consider a component with a core purpose of interacting directly with the host OS using Windows or Linux-specific APIs. It might be impossible to use such a component on WebAssembly. If so, it's reasonable to declare `@rendermode InteractiveServer` for the component.

## Streaming rendering

Reusable Razor components are free to declare `@attribute [StreamRendering]` for [streaming rendering](rendering?view=aspnetcore-10.0#streaming-rendering) ([`[StreamRendering]` attribute API](/en-us/dotnet/api/microsoft.aspnetcore.components.streamrenderingattribute)). This results in incremental UI updates during static SSR. Since the same data-loading patterns produce incremental UI updates during interactive rendering, regardless of the presence of the `[StreamRendering]` attribute, the component can behave correctly in all cases. Even in cases where streaming static SSR is suppressed on the server, the component still renders its correct final state.

## Using links across render modes

Reusable Razor components may use links and enhanced navigation. HTML `<a>` tags should produce equivalent behaviors with or without an interactive [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component and whether or not enhanced navigation is enabled/disabled at an ancestor level in the DOM.

## Using forms across render modes

Reusable Razor components may include forms (either `<form>` or `<EditForm>`), as these can be implemented to work equivalently across both static and interactive render modes.

Consider the following example:

```
<EditForm Enhance FormName="NewProduct" Model="Model" OnValidSubmit="Save">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p><label>Name: <InputText @bind-Value="Model!.Name" /></label></p>

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

@code {
    [SupplyParameterFromForm]
    private Product? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private async Task Save()
    {
        ...
    }
}
```

The [Enhance](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.enhance), [FormName](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.formname), and [SupplyParameterFromFormAttribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromformattribute) APIs are only used during static SSR and are ignored during interactive rendering. The form works correctly during both interactive and static SSR.

## Avoid APIs that are specific to static SSR

[HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is only available during static SSR, so don't rely on [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) when creating components that work across render modes. The [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) API doesn't make sense to use during interactive rendering either because there's no active HTTP request in flight at those times. It's meaningless to think about setting a HTTP status code or writing to the HTTP response.

Reusable components are free to receive an [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) when available, as follows:

```
[CascadingParameter]
private HttpContext? Context { get; set; }
```

The value is `null` during interactive rendering and is only set during static SSR.

In many cases, there are better alternatives than using [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext). If you need to know the current URL or to perform a redirection, the APIs on [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) work with all render modes. If you need to know the user's authentication state, use Blazor's [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) service ([`AuthenticationStateProvider` documentation](../security/authentication-state?view=aspnetcore-10.0)) over using [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-7.0
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-7.0.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# What's new in ASP.NET Core in .NET 7

This article highlights the most significant changes in ASP.NET Core in .NET 7 with links to relevant documentation.

## Rate limiting middleware in ASP.NET Core

The `Microsoft.AspNetCore.RateLimiting` middleware provides rate limiting middleware. Apps configure rate limiting policies and then attach the policies to endpoints. For more information, see [Rate limiting middleware in ASP.NET Core](../performance/rate-limit?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0).

## Authentication uses single scheme as DefaultScheme

As part of the work to simplify authentication, when there's only a single authentication scheme registered, it's automatically used as the [DefaultScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultscheme#microsoft-aspnetcore-authentication-authenticationoptions-defaultscheme) and doesn't need to be specified. For more information, see [DefaultScheme](../security/authentication/?view=aspnetcore-10.0#defaultscheme).

## MVC and Razor pages

### Support for nullable models in MVC views and Razor Pages

Nullable page or view models are supported to improve the experience when using null state checking with ASP.NET Core apps:

```
@model Product?
```

### Bind with `IParsable<T>.TryParse` in MVC and API Controllers

The [`IParsable<TSelf>.TryParse`](/en-us/dotnet/api/system.iparsable-1.tryparse#system-iparsable-1-tryparse\(system-string-system-iformatprovider-0@\)) API supports binding controller action parameter values. For more information, see [Bind with `IParsable<T>.TryParse`](../mvc/models/model-binding?view=aspnetcore-10.0#itp7).

### Customize the cookie consent value

In ASP.NET Core versions earlier than 7, the cookie consent validation uses the cookie value `yes` to indicate consent. Now you can specify the value that represents consent. For example, you could use `true` instead of `yes`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
    options.ConsentCookieValue = "true";
});

var app = builder.Build();
```

For more information, see [Customize the cookie consent value](../security/gdpr?view=aspnetcore-10.0#customize-the-cookie-consent-value).

## API controllers

### Parameter binding with DI in API controllers

Parameter binding for API controller actions binds parameters through [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) when the type is configured as a service. This means it's no longer required to explicitly apply the [`[FromServices]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) attribute to a parameter. In the following code, both actions return the time:

```
[Route("[controller]")]
[ApiController]
public class MyController : ControllerBase
{
    public ActionResult GetWithAttribute([FromServices] IDateTime dateTime) 
                                                        => Ok(dateTime.Now);

    [Route("noAttribute")]
    public ActionResult Get(IDateTime dateTime) => Ok(dateTime.Now);
}
```

In rare cases, automatic DI can break apps that have a type in DI that is also accepted in an API controllers action method. It's not common to have a type in DI and as an argument in an API controller action. To disable automatic binding of parameters, set [DisableImplicitFromServicesParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.disableimplicitfromservicesparameters)

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.DisableImplicitFromServicesParameters = true;
});

var app = builder.Build();

app.MapControllers();

app.Run();
```

In .NET 7, types in DI are checked at app startup with [IServiceProviderIsService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iserviceproviderisservice) to determine if an argument in an API controller action comes from DI or from the other sources.

The new mechanism to infer binding source of API Controller action parameters uses the following rules:

1. A previously specified [`BindingInfo.BindingSource`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindinginfo.bindingsource#microsoft-aspnetcore-mvc-modelbinding-bindinginfo-bindingsource) is never overwritten.
2. A complex type parameter, registered in the DI container, is assigned [`BindingSource.Services`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindingsource.services#microsoft-aspnetcore-mvc-modelbinding-bindingsource-services).
3. A complex type parameter, not registered in the DI container, is assigned [`BindingSource.Body`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindingsource.body#microsoft-aspnetcore-mvc-modelbinding-bindingsource-body).
4. A parameter with a name that appears as a route value in ***any*** route template is assigned [`BindingSource.Path`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindingsource.path#microsoft-aspnetcore-mvc-modelbinding-bindingsource-path).
5. All other parameters are [`BindingSource.Query`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindingsource.query#microsoft-aspnetcore-mvc-modelbinding-bindingsource-query).

### JSON property names in validation errors

By default, when a validation error occurs, model validation produces a [ModelStateDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary) with the property name as the error key. Some apps, such as single page apps, benefit from using JSON property names for validation errors generated from Web APIs. The following code configures validation to use the [`SystemTextJsonValidationMetadataProvider`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.systemtextjsonvalidationmetadataprovider) to use JSON property names:

```
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new SystemTextJsonValidationMetadataProvider());
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The following code configures validation to use the [`NewtonsoftJsonValidationMetadataProvider`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.newtonsoftjson.newtonsoftjsonvalidationmetadataprovider) to use JSON property name when using [Json.NET](https://www.newtonsoft.com/json):

```
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new NewtonsoftJsonValidationMetadataProvider());
}).AddNewtonsoftJson();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

For more information, see [Use JSON property names in validation errors](../mvc/models/validation?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0#use-json-property-names-in-validation-errors)

## Minimal APIs

### Filters in Minimal API apps

Minimal API filters allow developers to implement business logic that supports:

* Running code before and after the route handler.
* Inspecting and modifying parameters provided during a route handler invocation.
* Intercepting the response behavior of a route handler.

Filters can be helpful in the following scenarios:

* Validating the request parameters and body that are sent to an endpoint.
* Logging information about the request and response.
* Validating that a request is targeting a supported API version.

For more information, see [Filters in Minimal API apps](../fundamentals/minimal-apis/min-api-filters?view=aspnetcore-10.0)

### Bind arrays and string values from headers and query strings

In ASP.NET 7, binding query strings to an array of primitive types, string arrays, and [StringValues](/en-us/dotnet/api/microsoft.extensions.primitives.stringvalues) is supported:

```
// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
```

Binding query strings or header values to an array of complex types is supported when the type has `TryParse` implemented. For more information, see [Bind arrays and string values from headers and query strings](../fundamentals/minimal-apis?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0#bindar).

For more information, see [Add endpoint summary or description](../fundamentals/minimal-apis?view=aspnetcore-10.0#add-endpoint-summary-or-description).

### Bind the request body as a `Stream` or `PipeReader`

The request body can bind as a [`Stream`](/en-us/dotnet/api/system.io.stream) or [`PipeReader`](/en-us/dotnet/api/system.io.pipelines.pipereader) to efficiently support scenarios where the user has to process data and:

* Store the data to blob storage or enqueue the data to a queue provider.
* Process the stored data with a worker process or cloud function.

For example, the data might be enqueued to [Azure Queue storage](/en-us/azure/storage/queues/storage-queues-introduction) or stored in [Azure Blob storage](/en-us/azure/storage/blobs/storage-blobs-introduction).

For more information, see [Bind the request body as a `Stream` or `PipeReader`](../fundamentals/minimal-apis?view=aspnetcore-10.0#rbs)

### New Results.Stream overloads

We introduced new [`Results.Stream`](/en-us/dotnet/api/microsoft.aspnetcore.http.results.stream?view=aspnetcore-7.0\&preserve-view=true) overloads to accommodate scenarios that need access to the underlying HTTP response stream without buffering. These overloads also improve cases where an API streams data to the HTTP response stream, like from Azure Blob Storage. The following example uses [ImageSharp](https://sixlabors.com/products/imagesharp) to return a reduced size of the specified image:

```
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
```

For more information, see [Stream examples](../fundamentals/minimal-apis?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0#stream7)

### Typed results for Minimal APIs

In .NET 6, the [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) interface was introduced to represent values returned from Minimal APIs that don't utilize the implicit support for JSON serializing the returned object to the HTTP response. The static [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) class is used to create varying `IResult` objects that represent different types of responses. For example, setting the response status code or redirecting to another URL. The `IResult` implementing framework types returned from these methods were internal however, making it difficult to verify the specific `IResult` type being returned from methods in a unit test.

In .NET 7 the types implementing `IResult` are public, allowing for type assertions when testing. For example:

```
[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}
```

### Improved unit testability for minimal route handlers

[IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) implementation types are now publicly available in the [Microsoft.AspNetCore.Http.HttpResults](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults) namespace. The `IResult` implementation types can be used to unit test minimal route handlers when using named methods instead of lambdas.

The following code uses the [`Ok<TValue>`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.ok-1) class:

```
[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title",
        Description = "Test description",
        IsDone = false
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var okResult = (Ok<Todo>)result.Result;

    Assert.NotNull(okResult.Value);
    Assert.Equal(1, okResult.Value.Id);
}
```

For more information, see [`IResult` implementation types](../fundamentals/minimal-apis/test-min-api?view=aspnetcore-10.0#iit7).

### New HttpResult interfaces

For more information, see [IHttpResult interfaces](../fundamentals/minimal-apis/responses?view=aspnetcore-10.0#httpresultinterfaces7).

### OpenAPI improvements for Minimal APIs

[]()

#### `Microsoft.AspNetCore.OpenApi` NuGet package

The [`Microsoft.AspNetCore.OpenApi`](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi/) package allows interactions with OpenAPI specifications for endpoints. The package acts as a link between the OpenAPI models that are defined in the `Microsoft.AspNetCore.OpenApi` package and the endpoints that are defined in Minimal APIs. The package provides an API that examines an endpoint's parameters, responses, and metadata to construct an OpenAPI annotation type that is used to describe an endpoint.

```
app.MapPost("/todoitems/{id}", async (int id, Todo todo, TodoDb db) =>
{
    todo.Id = id;
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi();
```

#### Call `WithOpenApi` with parameters

The [`WithOpenApi`](https://github.com/dotnet/aspnetcore/blob/8a4b4deb09c04134f22f8d39aae21d212282004f/src/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs#L49) method accepts a function that can be used to modify the OpenAPI annotation. For example, in the following code, a description is added to the first parameter of the endpoint:

```
app.MapPost("/todo2/{id}", async (int id, Todo todo, TodoDb db) =>
{
    todo.Id = id;
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi(generatedOperation =>
{
    var parameter = generatedOperation.Parameters[0];
    parameter.Description = "The ID associated with the created Todo";
    return generatedOperation;
});
```

#### Provide endpoint descriptions and summaries

Minimal APIs now support annotating operations with descriptions and summaries for OpenAPI spec generation. You can call extension methods [WithDescription](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.withdescription) and [WithSummary](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.withsummary) or use attributes [\[EndpointDescription\]](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointdescriptionattribute) and [\[EndpointSummary\]](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointsummaryattribute)).

For more information, see [OpenAPI in Minimal API apps](../fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-7.0)

### File uploads using IFormFile and IFormFileCollection

Minimal APIs now support file upload with `IFormFile` and `IFormFileCollection`. The following code uses [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection) to upload file:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();
```

Authenticated file upload requests are supported using an [Authorization header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Authorization), a [client certificate](/en-us/aspnet/core/security/authentication/certauth), or a cookie header.

There is no built-in support for [antiforgery](/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-7.0\&preserve-view=true#anti7). However, it can be implemented using the [`IAntiforgery` service](/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-7.0\&preserve-view=true#antimin7).

### `[AsParameters]` attribute enables parameter binding for argument lists

The [`[AsParameters]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute) enables parameter binding for argument lists. For more information, see [Parameter binding for argument lists with `[AsParameters]`](../fundamentals/minimal-apis?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0#asparam7).

## Minimal APIs and API controllers

### New problem details service

The problem details service implements the [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) interface, which supports creating [Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc7807.html).

For more information, see [Problem details service](../fundamentals/error-handling-api?view=aspnetcore-10.0#pds7).

### Route groups

The [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) extension method helps organize groups of endpoints with a common prefix and reduces repetitive code. Use this method to customize entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata) that add [endpoint metadata](../fundamentals/routing?view=aspnetcore-10.0#endpoint-metadata).

For example, the following code creates two similar groups of endpoints:

```
app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
```

```
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}
```

In this scenario, you can use a relative address for the `Location` header in the `201 Created` result:

```
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}
```

The first group of endpoints matches only requests prefixed with `/public/todos` and are accessible without any authentication. The second group of endpoints matches only requests prefixed with `/private/todos` and require authentication.

`QueryPrivateTodos` is a local function that modifies the `TodoDb` parameters of the route handler, to enable them to access and store private todo data. `QueryPrivateTodos` serves as an [endpoint filter factory](../fundamentals/minimal-apis/min-api-filters?view=aspnetcore-10.0).

Route groups also support nested groups and complex prefix patterns with route parameters and constraints. In the following example, the route handler mapped to the `user` group can capture the `{org}` and `{group}` route parameters defined in the outer group prefixes.

The prefix can also be empty. This approach can be useful for adding endpoint metadata or filters to a group of endpoints without changing the route pattern.

```
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
```

Adding filters or metadata to a group results in the same behavior as adding them individually to each endpoint (before adding extra filters or metadata that might exist in an inner group or specific endpoint).

```
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});
```

In the preceding example, the outer filter logs the incoming request before the inner filter even though the outer filter is added second. Because the filters are applied to different groups, the order that they're added relative to each other doesn't matter. The order in which filters are added matters when applied to the same group or specific endpoint.

A request to `/outer/inner/` logs the following data:

```
/outer group filter
/inner group filter
MapGet filter
```

## gRPC

### JSON transcoding

gRPC JSON transcoding is an extension for ASP.NET Core that creates RESTful JSON APIs for gRPC services. gRPC JSON transcoding allows:

* Apps to call gRPC services with familiar HTTP concepts.
* ASP.NET Core gRPC apps to support both gRPC and RESTful JSON APIs without replicating functionality.
* Experimental support for generating OpenAPI from transcoded RESTful APIs by integrating with [Swashbuckle](../tutorials/getting-started-with-swashbuckle?view=aspnetcore-10.0).

For more information, see [gRPC JSON transcoding in ASP.NET Core gRPC apps](../grpc/json-transcoding?view=aspnetcore-7.0).

### gRPC health checks in ASP.NET Core

The [gRPC health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) is a standard for reporting the health of gRPC server apps. An app exposes health checks as a gRPC service. They are typically used with an external monitoring service to check the status of an app.

gRPC ASP.NET Core has added built-in support for gRPC health checks with the [`Grpc.AspNetCore.HealthChecks`](https://www.nuget.org/packages/Grpc.AspNetCore.HealthChecks) package. Results from [.NET health checks](../host-and-deploy/health-checks?view=aspnetcore-10.0) are reported to callers.

For more information, see [gRPC health checks in ASP.NET Core](../grpc/health-checks?view=aspnetcore-10.0).

### Improved call credentials support

Call credentials are the recommended way to configure a gRPC client to send an auth token to the server. gRPC clients support two new features to make call credentials easier to use:

* Support for call credentials with plaintext connections. Previously, a gRPC call only sent call credentials if the connection was secured with TLS. A new setting on `GrpcChannelOptions`, called `UnsafeUseInsecureChannelCallCredentials`, allows this behavior to be customized. There are security implications to not securing a connection with TLS.
* A new method called `AddCallCredentials` is available with the [gRPC client factory](../grpc/clientfactory?view=aspnetcore-10.0). `AddCallCredentials` is a quick way to configure call credentials for a gRPC client and integrates well with dependency injection (DI).

The following code configures the gRPC client factory to send `Authorization` metadata:

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
       o.Address = new Uri("https://localhost:5001");
    })
    .AddCallCredentials((context, metadata) =>
    {
       if (!string.IsNullOrEmpty(_token))
       {
          metadata.Add("Authorization", $"Bearer {_token}");
       }
       return Task.CompletedTask;
    });
```

For more information, see [Configure a bearer token with the gRPC client factory](../grpc/authn-and-authz?view=aspnetcore-10.0#bearer-token-with-grpc-client-factory).

## SignalR

### Client results

The server now supports requesting a result from a client. This requires the server to use `ISingleClientProxy.InvokeAsync` and the client to return a result from its `.On` handler. Strongly-typed hubs can also return values from interface methods.

For more information, see [Client results](../signalr/hubs?view=aspnetcore-7.0#client-results)

### Dependency injection for SignalR hub methods

SignalR hub methods now support injecting services through dependency injection (DI).

Hub constructors can accept services from DI as parameters, which can be stored in properties on the class for use in a hub method. For more information, see [Inject services into a hub](../signalr/hubs?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0#inject-services-into-a-hub)

## Blazor

### Handle location changing events and navigation state

In .NET 7, Blazor supports location changing events and maintaining navigation state. This allows you to warn users about unsaved work or to perform related actions when the user performs a page navigation.

For more information, see the following sections of the *Routing and navigation* article:

* [Navigation options](../blazor/fundamentals/routing?view=aspnetcore-7.0#navigation-options)
* [Handle/prevent location changes](../blazor/fundamentals/routing?view=aspnetcore-7.0#handleprevent-location-changes)

### Empty Blazor project templates

Blazor has two new project templates for starting from a blank slate. The new **Blazor Server App Empty** and **Blazor WebAssembly App Empty** project templates are just like their non-empty counterparts but without example code. These empty templates only include a basic home page, and we've removed Bootstrap so that you can start with a different CSS framework.

For more information, see the following articles:

* [Tooling for ASP.NET Core Blazor](../blazor/tooling?view=aspnetcore-7.0)
* [ASP.NET Core Blazor project structure](../blazor/project-structure?view=aspnetcore-7.0)

### Blazor custom elements

The [`Microsoft.AspNetCore.Components.CustomElements`](https://www.nuget.org/packages/microsoft.aspnetcore.components.customelements) package enables building [standards based custom DOM elements](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) using Blazor.

For more information, see [ASP.NET Core Razor components](../blazor/components/?view=aspnetcore-7.0#blazor-custom-elements).

### Bind modifiers (`@bind:after`, `@bind:get`, `@bind:set`)

Important

The `@bind:after`/`@bind:get`/`@bind:set` features are receiving further updates at this time. To take advantage of the latest updates, confirm that you've installed the [latest SDK](https://dotnet.microsoft.com/download/dotnet/7.0).

Using an event callback parameter (`[Parameter] public EventCallback<string> ValueChanged { get; set; }`) isn't supported. Instead, pass an [Action](/en-us/dotnet/api/system.action)-returning or [Task](/en-us/dotnet/api/system.threading.tasks.task)-returning method to `@bind:set`/`@bind:after`.

For more information, see the following resources:

* [Blazor `@bind:after` not working on .NET 7 RTM release (dotnet/aspnetcore #44957)](https://github.com/dotnet/aspnetcore/issues/44957)
* [`BindGetSetAfter701` sample app (javiercn/BindGetSetAfter701 GitHub repository)](https://github.com/javiercn/BindGetSetAfter701)

In .NET 7, you can run asynchronous logic after a binding event has completed using the new `@bind:after` modifier. In the following example, the `PerformSearch` asynchronous method runs automatically after any changes to the search text are detected:

```
<input @bind="searchText" @bind:after="PerformSearch" />

@code {
    private string searchText;

    private async Task PerformSearch()
    {
        ...
    }
}
```

In .NET 7, it's also easier to set up binding for component parameters. Components can support two-way data binding by defining a pair of parameters:

* `@bind:get`: Specifies the value to bind.
* `@bind:set`: Specifies a callback for when the value changes.

The `@bind:get` and `@bind:set` modifiers are always used together.

Examples:

```
@* Elements *@

<input type="text" @bind="text" @bind:after="() => { }" />

<input type="text" @bind:get="text" @bind:set="(value) => { }" />

<input type="text" @bind="text" @bind:after="AfterAsync" />

<input type="text" @bind:get="text" @bind:set="SetAsync" />

<input type="text" @bind="text" @bind:after="() => { }" />

<input type="text" @bind:get="text" @bind:set="(value) => { }" />

<input type="text" @bind="text" @bind:after="AfterAsync" />

<input type="text" @bind:get="text" @bind:set="SetAsync" />

@* Components *@

<InputText @bind-Value="text" @bind-Value:after="() => { }" />

<InputText @bind-Value:get="text" @bind-Value:set="(value) => { }" />

<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />

<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />

<InputText @bind-Value="text" @bind-Value:after="() => { }" />

<InputText @bind-Value:get="text" @bind-Value:set="(value) => { }" />

<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />

<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />

@code {
    private string text = "";

    private void After(){}
    private void Set() {}
    private Task AfterAsync() { return Task.CompletedTask; }
    private Task SetAsync(string value) { return Task.CompletedTask; }
}
```

For more information on the `InputText` component, see [ASP.NET Core Blazor input components](../blazor/forms/input-components?view=aspnetcore-10.0).

### Hot Reload improvements

In .NET 7, Hot Reload support includes the following:

* Components reset their parameters to their default values when a value is removed.

* Blazor WebAssembly:

  * Add new types.
  * Add nested classes.
  * Add static and instance methods to existing types.
  * Add static fields and methods to existing types.
  * Add static lambdas to existing methods.
  * Add lambdas that capture `this` to existing methods that already captured `this` previously.

### Dynamic authentication requests with MSAL in Blazor WebAssembly

New in .NET 7, Blazor WebAssembly supports creating dynamic authentication requests at runtime with custom parameters to handle advanced authentication scenarios.

For more information, see the following articles:

* [Secure ASP.NET Core Blazor WebAssembly](../blazor/security/webassembly/?view=aspnetcore-7.0#customize-authentication)
* [ASP.NET Core Blazor WebAssembly additional security scenarios](../blazor/security/webassembly/additional-scenarios?view=aspnetcore-7.0#custom-authentication-request-scenarios)

### Blazor WebAssembly debugging improvements

Blazor WebAssembly debugging has the following improvements:

* Support for the **Just My Code** setting to show or hide type members that aren't from user code.
* Support for inspecting multidimensional arrays.
* **Call Stack** now shows the correct name for asynchronous methods.
* Improved expression evaluation.
* Correct handling of the `new` keyword on derived members.
* Support for debugger-related attributes in `System.Diagnostics`.

### `System.Security.Cryptography` support on WebAssembly

.NET 6 supported the SHA family of hashing algorithms when running on WebAssembly. .NET 7 enables more cryptographic algorithms by taking advantage of [SubtleCrypto](https://developer.mozilla.org/docs/Web/API/SubtleCrypto), when possible, and falling back to a .NET implementation when SubtleCrypto can't be used. The following algorithms are supported on WebAssembly in .NET 7:

* SHA1
* SHA256
* SHA384
* SHA512
* HMACSHA1
* HMACSHA256
* HMACSHA384
* HMACSHA512
* AES-CBC
* PBKDF2
* HKDF

For more information, see [Developers targeting browser-wasm can use Web Crypto APIs (dotnet/runtime #40074)](https://github.com/dotnet/runtime/issues/40074).

### Inject services into custom validation attributes

You can now inject services into custom validation attributes. Blazor sets up the `ValidationContext` so that it can be used as a service provider.

For more information, see [ASP.NET Core Blazor forms validation](../blazor/forms/validation?view=aspnetcore-7.0#inject-services-into-a-custom-validation-attribute).

### `Input*` components outside of an `EditContext`/`EditForm`

The built-in input components are now supported outside of a form in Razor component markup.

For more information, see [ASP.NET Core Blazor input components](../blazor/forms/input-components?view=aspnetcore-7.0).

### Project template changes

When .NET 6 was released last year, the HTML markup of the `_Host` page (`Pages/_Host.chstml`) was split between the `_Host` page and a new `_Layout` page (`Pages/_Layout.chstml`) in the .NET 6 Blazor Server project template.

In .NET 7, the HTML markup has been recombined with the `_Host` page in project templates.

Several additional changes were made to the Blazor project templates. It isn't feasible to list every change to the templates in the documentation. To migrate an app to .NET 7 in order to adopt all of the changes, see [Migrate from ASP.NET Core in .NET 6 to .NET 7](../migration/60-to-70?view=aspnetcore-10.0#blazor).

### Experimental `QuickGrid` component

The new `QuickGrid` component provides a convenient data grid component for most common requirements and as a reference architecture and performance baseline for anyone building Blazor data grid components.

For more information, see [ASP.NET Core Blazor \`QuickGrid\` component](../blazor/components/quickgrid?view=aspnetcore-10.0).

Live demo: [QuickGrid for Blazor sample app](https://aspnet.github.io/quickgridsamples/)

### Virtualization enhancements

Virtualization enhancements in .NET 7:

* The `Virtualize` component supports using the document itself as the scroll root, as an alternative to having some other element with `overflow-y: scroll` applied.
* If the `Virtualize` component is placed inside an element that requires a specific child tag name, `SpacerElement` allows you to obtain or set the virtualization spacer tag name.

For more information, see the following sections of the *Virtualization* article:

* [Root-level virtualization](../blazor/components/virtualization?view=aspnetcore-7.0#root-level-virtualization)
* [Control the spacer element tag name](../blazor/components/virtualization?view=aspnetcore-7.0#control-the-spacer-element-tag-name)

### `MouseEventArgs` updates

`MovementX` and `MovementY` have been added to `MouseEventArgs`.

For more information, see [ASP.NET Core Blazor event handling](../blazor/components/event-handling?view=aspnetcore-7.0#built-in-event-arguments).

### New Blazor loading page

The Blazor WebAssembly project template has a new loading UI that shows the progress of loading the app.

For more information, see [ASP.NET Core Blazor startup](../blazor/fundamentals/startup?view=aspnetcore-7.0#loading-progress-indicators).

### Improved diagnostics for authentication in Blazor WebAssembly

To help diagnose authentication issues in Blazor WebAssembly apps, detailed logging is available.

For more information, see [ASP.NET Core Blazor logging](../blazor/fundamentals/logging?view=aspnetcore-7.0#blazor-webassembly-authentication-logging).

### JavaScript interop on WebAssembly

JavaScript `[JSImport]`/`[JSExport]` interop API is a new low-level mechanism for using .NET in Blazor WebAssembly and JavaScript-based apps. With this new JavaScript interop capability, you can invoke .NET code from JavaScript using the .NET WebAssembly runtime and call into JavaScript functionality from .NET without any dependency on the Blazor UI component model.

For more information:

* [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](../blazor/javascript-interoperability/import-export-interop?view=aspnetcore-10.0): Pertains only to Blazor WebAssembly apps.
* [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop in .NET WebAssembly](../client-side/dotnet-interop/?view=aspnetcore-10.0): Pertains only to JavaScript apps that don't depend on the Blazor UI component model.

### Conditional registration of the authentication state provider

Prior to the release of .NET 7, `AuthenticationStateProvider` was registered in the service container with `AddScoped`. This made it difficult to debug apps, as it forced a specific order of service registrations when providing a custom implementation. Due to internal framework changes over time, it's no longer necessary to register `AuthenticationStateProvider` with `AddScoped`.

In developer code, make the following change to the authentication state provider service registration:

```
- builder.Services.AddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
+ builder.Services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();
```

In the preceding example, `ExternalAuthStateProvider` is the developer's service implementation.

### Improvements to the .NET WebAssembly build tools

New features in the `wasm-tools` workload for .NET 7 that help improve performance and handle exceptions:

* [WebAssembly Single Instruction, Multiple Data (SIMD)](https://wikipedia.org/wiki/Single_instruction,_multiple_data) support (only with AOT, not supported by Apple Safari)
* WebAssembly exception handling support

For more information, see [ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation](../blazor/webassembly-build-tools-and-aot?view=aspnetcore-7.0).

## Blazor Hybrid

### External URLs

An option has been added that permits opening external webpages in the browser.

For more information, see [ASP.NET Core Blazor Hybrid routing and navigation](../blazor/hybrid/routing?view=aspnetcore-7.0#external-navigation).

### Security

New guidance is available for Blazor Hybrid security scenarios. For more information, see the following articles:

* [ASP.NET Core Blazor Hybrid authentication and authorization](../blazor/hybrid/security/?view=aspnetcore-7.0)
* [ASP.NET Core Blazor Hybrid security considerations](../blazor/hybrid/security/security-considerations?view=aspnetcore-7.0)

## Performance

### Output caching middleware

Output caching is a new middleware that stores responses from a web app and serves them from a cache rather than computing them every time. Output caching differs from [response caching](../performance/caching/overview?view=aspnetcore-10.0#response-caching) in the following ways:

* The caching behavior is configurable on the server.
* Cache entries can be programmatically invalidated.
* Resource locking mitigates the risk of [cache stampede](https://en.wikipedia.org/wiki/Cache_stampede) and [thundering herd](https://en.wikipedia.org/wiki/Thundering_herd_problem).
* Cache revalidation means the server can return a `304 Not Modified` HTTP status code instead of a cached response body.
* The cache storage medium is extensible.

For more information, see [Overview of caching](../performance/caching/overview?view=aspnetcore-10.0) and [Output caching middleware](../performance/caching/output?view=aspnetcore-10.0).

### HTTP/3 improvements

This release:

* Makes HTTP/3 fully supported by ASP.NET Core, it's no longer experimental.
* Improves Kestrel's support for HTTP/3. The two main areas of improvement are feature parity with HTTP/1.1 and HTTP/2, and performance.
* Provides full support for [UseHttps(ListenOptions, X509Certificate2)](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionshttpsextensions.usehttps#microsoft-aspnetcore-hosting-listenoptionshttpsextensions-usehttps\(microsoft-aspnetcore-server-kestrel-core-listenoptions-system-security-cryptography-x509certificates-x509certificate2\)) with HTTP/3. Kestrel offers advanced options for configuring connection certificates, such as hooking into [Server Name Indication (SNI)](https://wikipedia.org/wiki/Server_Name_Indication).
* Adds support for HTTP/3 on [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) and [IIS](../host-and-deploy/iis/modules?view=aspnetcore-10.0).

The following example shows how to use an SNI callback to resolve TLS options:

```
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(8080, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
        listenOptions.UseHttps(new TlsHandshakeCallbackOptions
        {
            OnConnection = context =>
            {
                var options = new SslServerAuthenticationOptions
                {
                    ServerCertificate = 
                         MyResolveCertForHost(context.ClientHelloInfo.ServerName)
                };
                return new ValueTask<SslServerAuthenticationOptions>(options);
            },
        });
    });
});
```

Significant work was done in .NET 7 to reduce HTTP/3 allocations. You can see some of those improvements in the following GitHub PR's:

* [HTTP/3: Avoid per-request cancellation token allocations](https://github.com/dotnet/aspnetcore/pull/42685)
* [HTTP/3: Avoid ConnectionAbortedException allocations](https://github.com/dotnet/aspnetcore/pull/42708)
* [HTTP/3: ValueTask pooling](https://github.com/dotnet/aspnetcore/pull/42760)

### HTTP/2 Performance improvements

.NET 7 introduces a significant re-architecture of how Kestrel processes HTTP/2 requests. ASP.NET Core apps with busy HTTP/2 connections will experience reduced CPU usage and higher throughput.

Previously, the HTTP/2 multiplexing implementation relied on a [lock](/en-us/dotnet/csharp/language-reference/statements/lock) controlling which request can write to the underlying TCP connection. A [thread-safe queue](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) replaces the write lock. Now, rather than fighting over which thread gets to use the write lock, requests now queue up and a dedicated consumer processes them. Previously wasted CPU resources are available to the rest of the app.

One place where these improvements can be noticed is in gRPC, a popular RPC framework that uses HTTP/2. Kestrel + gRPC benchmarks show a dramatic improvement:

Changes were made in the HTTP/2 frame writing code that improves performance when there are multiple streams trying to write data on a single HTTP/2 connection. We now dispatch TLS work to the thread pool and more quickly release a write lock that other streams can acquire to write their data. The reduction in wait times can yield significant performance improvements in cases where there is contention for this write lock. A gRPC benchmark with 70 streams on a single connection (with TLS) showed a \~15% improvement in requests per second (RPS) with this change.

### Http/2 WebSockets support

.NET 7 introduces WebSockets over HTTP/2 support for Kestrel, the SignalR JavaScript client, and SignalR with Blazor WebAssembly.

Using WebSockets over HTTP/2 takes advantage of new features such as:

* Header compression.
* Multiplexing, which reduces the time and resources needed when making multiple requests to the server.

These supported features are available in Kestrel on all HTTP/2 enabled platforms. The version negotiation is automatic in browsers and Kestrel, so no new APIs are needed.

For more information, see [Http/2 WebSockets support](../fundamentals/websockets?view=aspnetcore-7.0#http2-websockets-support).

### Kestrel performance improvements on high core machines

Kestrel uses [ConcurrentQueue\<T>](/en-us/dotnet/api/system.collections.concurrent.concurrentqueue-1) for many purposes. One purpose is scheduling I/O operations in Kestrel's default Socket transport. Partitioning the `ConcurrentQueue` based on the associated socket reduces contention and increases throughput on machines with many CPU cores.

Profiling on high core machines on .NET 6 showed significant contention in one of Kestrel's other `ConcurrentQueue` instances, the `PinnedMemoryPool` that Kestrel uses to cache byte buffers.

In .NET 7, Kestrel's memory pool is partitioned the same way as its I/O queue, which leads to much lower contention and higher throughput on high core machines. On the 80 core ARM64 VMs, we're seeing over 500% improvement in responses per second (RPS) in the TechEmpower plaintext benchmark. On 48 Core AMD VMs, the improvement is nearly 100% in our HTTPS JSON benchmark.

### `ServerReady` event to measure startup time

Apps using [EventSource](/en-us/dotnet/api/system.diagnostics.tracing.eventsource) can measure the startup time to understand and optimize startup performance. The new [`ServerReady`](https://source.dot.net/#Microsoft.AspNetCore.Hosting/Internal/HostingEventSource.cs,76) event in [Microsoft.AspNetCore.Hosting](/en-us/dotnet/api/microsoft.aspnetcore.hosting) represents the point where the server is ready to respond to requests.

## Server

### New ServerReady event for measuring startup time

The [`ServerReady`](https://github.com/dotnet/aspnetcore/blob/main/src/Hosting/Hosting/src/Internal/HostingEventSource.cs#L76-L79) event has been added to measure [startup time](https://github.com/dotnet/aspnetcore/blob/main/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs#L162) of ASP.NET Core apps.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## IIS

### Shadow copying in IIS

Shadow copying app assemblies to the [ASP.NET Core Module (ANCM)](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) for IIS can provide a better end user experience than stopping the app by deploying an [app offline file](../host-and-deploy/app-offline?view=aspnetcore-10.0).

For more information, see [Shadow copying in IIS](../host-and-deploy/iis/advanced?view=aspnetcore-7.0#shadow-copy).

## Miscellaneous

### Kestrel full certificate chain improvements

[HttpsConnectionAdapterOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions?view=aspnetcore-7.0\&preserve-view=true) has a new [ServerCertificateChain](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.servercertificatechain?view=aspnetcore-7.0\&preserve-view=true#microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions-servercertificatechain) property of type [X509Certificate2Collection](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2collection), which makes it easier to validate certificate chains by allowing a full chain including intermediate certificates to be specified. See [dotnet/aspnetcore#21513](https://github.com/dotnet/aspnetcore/issues/21513) for more details.

### dotnet watch

#### Improved console output for dotnet watch

The console output from dotnet watch has been improved to better align with the logging of ASP.NET Core and to stand out with 😮emojis😍.

Here's an example of what the new output looks like:

For more information, see [this GitHub pull request](https://github.com/dotnet/sdk/pull/23318).

### Configure dotnet watch to always restart for rude edits

Rude edits are edits that can't be hot reloaded. To configure dotnet watch to always restart without a prompt for rude edits, set the `DOTNET_WATCH_RESTART_ON_RUDE_EDIT` environment variable to `true`.

### Developer exception page dark mode

Dark mode support has been added to the developer exception page, thanks to a contribution by [Patrick Westerhoff](https://twitter.com/poke). To test dark mode in a browser, from the developer tools page, set the mode to dark. For example, in Firefox:

In Chrome:

### Project template option to use Program.Main method instead of top-level statements

The .NET 7 templates include an option to not use [top-level statements](/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) and generate a `namespace` and a `Main` method declared on a `Program` class.

Using the .NET CLI, use the `--use-program-main` option:

```
dotnet new web --use-program-main
```

With Visual Studio, select the new **Do not use top-level statements** checkbox during project creation:

### Updated Angular and React templates

The Angular project template has been updated to Angular 14. The React project template has been updated to React 18.2.

### Manage JSON Web Tokens in development with dotnet user-jwts

The new `dotnet user-jwts` command line tool can create and manage app specific local [JSON Web Tokens](https://jwt.io/introduction) (JWTs). For more information, see [Manage JSON Web Tokens in development with dotnet user-jwts](../security/authentication/jwt-authn?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0).

### Support for additional request headers in W3CLogger

You can now specify additional request headers to log when using the W3C logger by calling `AdditionalRequestHeaders()` on [W3CLoggerOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions):

```
services.AddW3CLogging(logging =>
{
    logging.AdditionalRequestHeaders.Add("x-forwarded-for");
    logging.AdditionalRequestHeaders.Add("x-client-ssl-protocol");
});
```

For more information, see [W3CLogger options](../fundamentals/w3c-logger/?view=aspnetcore-10.0#w3clogger-options-1).

### Request decompression

The new [Request decompression middleware](../fundamentals/middleware/request-decompression?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0):

* Enables API endpoints to accept requests with compressed content.
* Uses the [`Content-Encoding`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Encoding) HTTP header to automatically identify and decompress requests which contain compressed content.
* Eliminates the need to write code to handle compressed requests.

For more information, see [Request decompression middleware](../fundamentals/middleware/request-decompression?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0).

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/troubleshoot
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/troubleshoot.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Troubleshoot connection errors

This section provides help with errors that can occur when trying to establish a connection to an ASP.NET Core SignalR hub.

### Response code 404

```
WebSocket connection to 'wss://xxx/HubName' failed: Error during WebSocket handshake: Unexpected response code: 404
```

**General causes (all configurations):**

* Verify the client is connecting to the correct endpoint. For example, the server is hosted at `http://127.0.0.1:5000/hub/myHub` and the client is trying to connect to `http://127.0.0.1:5000/myHub`.

**When using the default negotiation flow (`skipNegotiation = false`):**

* When using multiple servers without sticky sessions, the connection can start on one server and then switch to another server. The other server is not aware of the previous connection.

* If the connection uses the ID and takes too long to send a request to the server after the negotiate, the server:

  * Deletes the ID.
  * Returns a 404.

**When using WebSockets with `skipNegotiation = true`:**

* Since negotiation is skipped and no connection ID is obtained from `/negotiate`, the sticky session and connection ID timeout scenarios don't apply. A 404 in this case typically means the WebSocket endpoint URL is incorrect or the hub isn't mapped on the server.

### Response code 400 or 503

For the following error:

```
WebSocket connection to 'wss://xxx/HubName' failed: Error during WebSocket handshake: Unexpected response code: 400

Error: Failed to start the connection: Error: There was an error with the transport.
```

This error is usually caused by a client using only the WebSockets transport but the WebSocket protocol isn't enabled on the server.

### Response code 307

When using WebSockets and `skipNegotiation = true`

```
WebSocket connection to 'ws://xxx/HubName' failed: Error during WebSocket handshake: Unexpected response code: 307
```

This error can also happen during the negotiate request.

Common cause:

* App is configured to enforce HTTPS by calling `UseHttpsRedirection` in `Startup`, or enforces HTTPS via URL rewrite rule.

Possible solution:

* Change the URL on the client side from "http" to "https". `.withUrl("https://xxx/HubName")`

### Response code 405

Http status code 405 - Method Not Allowed

* The app doesn't have [CORS](security?view=aspnetcore-10.0#cross-origin-resource-sharing) enabled

### Response code 0

Http status code 0 - Usually a [CORS](security?view=aspnetcore-10.0#cross-origin-resource-sharing) issue, no status code is given

```
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/default/negotiate?negotiateVersion=1. (Reason: CORS header 'Access-Control-Allow-Origin' missing).
```

* Add the expected origins to `.WithOrigins(...)`

```
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/default/negotiate?negotiateVersion=1. (Reason: expected 'true' in CORS header 'Access-Control-Allow-Credentials').
```

* Add `.AllowCredentials()` to your CORS policy. Cannot use `.AllowAnyOrigin()` or `.WithOrigins("*")` with this option

### Response code 413

Http status code 413 - Payload Too Large

This is often caused by having an access token that is over 4k.

* If using the Azure SignalR Service, reduce the token size by customizing the claims being sent through the Service with:

```
.AddAzureSignalR(options =>
{
    options.ClaimsProvider = context => context.User.Claims;
});
```

### Transient network failures

Transient network failures may close the SignalR connection. The server may interpret the closed connection as a graceful client disconnect. To get more info on why a client disconnected in those cases [gather logs from the client and server](diagnostics?view=aspnetcore-10.0).

## Related content

* [SignalR Hub Protocol](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/HubProtocol.md)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host and deploy ASP.NET Core Blazor WebAssembly

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to host and deploy Blazor WebAssembly apps.

With the [Blazor WebAssembly hosting model](../../hosting-models?view=aspnetcore-10.0#blazor-webassembly):

* The Blazor app, its dependencies, and the .NET runtime are downloaded to the browser in parallel.
* The app is executed directly on the browser UI thread.

This article pertains to the deployment scenario where the Blazor app is placed on a static hosting web server or service, .NET isn't used to serve the Blazor app. This strategy is covered in the [Standalone deployment](#standalone-deployment) section and other articles in this node for IIS, Azure services, Apache, Nginx, and GitHub Pages.

The following deployment strategies are supported:

* The Blazor app is served by an ASP.NET Core app. This strategy is covered in the [Hosted deployment with ASP.NET Core](#hosted-deployment-with-aspnet-core) section.
* The Blazor app is placed on a static hosting web server or service, where .NET isn't used to serve the Blazor app. This strategy is covered in the [Standalone deployment](#standalone-deployment) section, which includes information on hosting a Blazor WebAssembly app as an IIS sub-app.
* An ASP.NET Core app hosts multiple Blazor WebAssembly apps. For more information, see [Multiple hosted ASP.NET Core Blazor WebAssembly apps](multiple-hosted-webassembly?view=aspnetcore-10.0).

## Serve a Blazor WebAssembly app locally for testing

Many open-source and commercial HTTP servers can serve a published Blazor WebAssembly app locally. For a simple command-line HTTP server, consider using the [`natemcmaster/dotnet-serve` .NET tool](https://github.com/natemcmaster/dotnet-serve).

Warning

The [`natemcmaster/dotnet-serve` .NET tool](https://github.com/natemcmaster/dotnet-serve) isn't owned or maintained by Microsoft and isn't covered by any Microsoft support agreement or license. Use caution when using a third-party tool, especially for testing security scenarios. Confirm that the tool follows official specifications and adopts security best practices. Keep the tool's version current to obtain the latest bug fixes.

## Subdomain and IIS sub-application hosting

Subdomain hosting doesn't require special configuration of the app. You ***don't*** need to configure the app base path (the `<base>` tag in `wwwroot/index.html`) to host the app at a subdomain.

IIS sub-application hosting ***does*** require you to set the app base path. For more information and cross-links to further guidance on IIS sub-application hosting, see [Host and deploy ASP.NET Core Blazor](../?view=aspnetcore-10.0#iis).

## Decrease maximum heap size for some mobile device browsers

When building a Blazor app that runs on the client (`.Client` project of a Blazor Web App or standalone Blazor WebAssembly app) and targets mobile device browsers, especially Safari on iOS, decreasing the maximum memory for the app with the MSBuild property `EmccMaximumHeapSize` may be required. The default value is 2,147,483,648 bytes, which may be too large and result in the app crashing if the app attempts to allocate more memory with the browser failing to grant it. The following example sets the value to 268,435,456 bytes in the `Program` file:

When building a Blazor WebAssembly app that targets mobile device browsers, especially Safari on iOS, decreasing the maximum memory for the app with the MSBuild property `EmccMaximumHeapSize` may be required. The default value is 2,147,483,648 bytes, which may be too large and result in the app crashing if the app attempts to allocate more memory with the browser failing to grant it. The following example sets the value to 268,435,456 bytes in the `Program` file:

```
<EmccMaximumHeapSize>268435456</EmccMaximumHeapSize>
```

For more information on [Mono](https://github.com/mono/mono)/WebAssembly MSBuild properties and targets, see [`WasmApp.Common.targets` (`dotnet/runtime` GitHub repository)](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/build/WasmApp.Common.targets).

## Webcil packaging format for .NET assemblies

[Webcil](https://github.com/dotnet/runtime/blob/main/docs/design/mono/webcil.md) is a web-friendly packaging format for .NET assemblies designed to enable using Blazor WebAssembly in restrictive network environments. Webcil files use a standard WebAssembly wrapper, where the assemblies are deployed as WebAssembly files that use the standard `.wasm` file extension.

Webcil is the default packaging format when you publish a Blazor WebAssembly app. To disable the use of Webcil, set the following MSBuild property in the app's project file:

```
<PropertyGroup>
  <WasmEnableWebcil>false</WasmEnableWebcil>
</PropertyGroup>
```

## Customize how boot resources are loaded

Customize how boot resources are loaded using the `loadBootResource` API. For more information, see [ASP.NET Core Blazor startup](../../fundamentals/startup?view=aspnetcore-10.0#load-client-side-boot-resources).

## Compression

When a Blazor WebAssembly app is published, the output is statically compressed during publish to reduce the app's size and remove the overhead for runtime compression. The following compression algorithms are used:

* [Brotli](https://tools.ietf.org/html/rfc7932) (highest level)
* [Gzip](https://tools.ietf.org/html/rfc1952)

Blazor relies on the host to serve the appropriate compressed files. When hosting a Blazor WebAssembly standalone app, additional work might be required to ensure that statically-compressed files are served:

Blazor relies on the host to serve the appropriate compressed files. When using an **ASP.NET Core Hosted** Blazor WebAssembly project, the host project is capable of performing content negotiation and serving the statically-compressed files. When hosting a Blazor WebAssembly standalone app, additional work might be required to ensure that statically-compressed files are served:

* For IIS `web.config` compression configuration, see the [IIS: Brotli and Gzip compression](iis?view=aspnetcore-10.0#brotli-and-gzip-compression) section.
* When hosting on static hosting solutions that don't support statically-compressed file content negotiation, consider configuring the app to fetch and decode Brotli compressed files:

Obtain the JavaScript Brotli decoder from the [`google/brotli` GitHub repository](https://github.com/google/brotli). The minified decoder file is named `decode.min.js` and found in the repository's [`js` folder](https://github.com/google/brotli/tree/master/js).

Note

If the minified version of the `decode.js` script (`decode.min.js`) fails, try using the unminified version (`decode.js`) instead.

Update the app to use the decoder.

In the `wwwroot/index.html` file, set `autostart` to `false` on Blazor's `<script>` tag:

```
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
```

After Blazor's `<script>` tag and before the closing `</body>` tag, add the following JavaScript code `<script>` block. The following function calls `fetch` with [`cache: 'no-cache'`](https://developer.mozilla.org/docs/Web/API/Request/cache#value) to keep the browser's cache updated.

Blazor Web App:

```
<script type="module">
  import { BrotliDecode } from './decode.min.js';
  Blazor.start({
    webAssembly: {
      loadBootResource: function (type, name, defaultUri, integrity) {
        if (type !== 'dotnetjs' && location.hostname !== 'localhost' && type !== 'configuration' && type !== 'manifest') {
          return (async function () {
            const response = await fetch(defaultUri + '.br', { cache: 'no-cache' });
            if (!response.ok) {
              throw new Error(response.statusText);
            }
            const originalResponseBuffer = await response.arrayBuffer();
            const originalResponseArray = new Int8Array(originalResponseBuffer);
            const decompressedResponseArray = BrotliDecode(originalResponseArray);
            const contentType = type === 
              'dotnetwasm' ? 'application/wasm' : 'application/octet-stream';
            return new Response(decompressedResponseArray, 
              { headers: { 'content-type': contentType } });
          })();
        }
      }
    }
  });
</script>
```

Standalone Blazor WebAssembly:

```
<script type="module">
  import { BrotliDecode } from './decode.min.js';
  Blazor.start({
    loadBootResource: function (type, name, defaultUri, integrity) {
      if (type !== 'dotnetjs' && location.hostname !== 'localhost' && type !== 'configuration') {
        return (async function () {
          const response = await fetch(defaultUri + '.br', { cache: 'no-cache' });
          if (!response.ok) {
            throw new Error(response.statusText);
          }
          const originalResponseBuffer = await response.arrayBuffer();
          const originalResponseArray = new Int8Array(originalResponseBuffer);
          const decompressedResponseArray = BrotliDecode(originalResponseArray);
          const contentType = type === 
            'dotnetwasm' ? 'application/wasm' : 'application/octet-stream';
          return new Response(decompressedResponseArray, 
            { headers: { 'content-type': contentType } });
        })();
      }
    }
  });
</script>
```

For more information on loading boot resources, see [ASP.NET Core Blazor startup](../../fundamentals/startup?view=aspnetcore-10.0#load-boot-resources).

To disable compression, add the `CompressionEnabled` MSBuild property to the app's project file and set the value to `false`:

```
<PropertyGroup>
  <CompressionEnabled>false</CompressionEnabled>
</PropertyGroup>
```

The `CompressionEnabled` property can be passed to the [`dotnet publish`](/en-us/dotnet/core/tools/dotnet-publish) command with the following syntax in a command shell:

```
dotnet publish -p:CompressionEnabled=false
```

To disable compression, add the `BlazorEnableCompression` MSBuild property to the app's project file and set the value to `false`:

```
<PropertyGroup>
  <BlazorEnableCompression>false</BlazorEnableCompression>
</PropertyGroup>
```

The `BlazorEnableCompression` property can be passed to the [`dotnet publish`](/en-us/dotnet/core/tools/dotnet-publish) command with the following syntax in a command shell:

```
dotnet publish -p:BlazorEnableCompression=false
```

## Rewrite URLs for correct routing

Routing requests for page components in a Blazor WebAssembly app isn't as straightforward as routing requests in a Blazor Server app. Consider a Blazor WebAssembly app with two components:

* `Main.razor`: Loads at the root of the app and contains a link to the `About` component (`href="About"`).
* `About.razor`: `About` component.

When the app's default document is requested using the browser's address bar (for example, `https://www.contoso.com/`):

1. The browser makes a request.
2. The default page is returned, which is usually `index.html`.
3. `index.html` bootstraps the app.
4. [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component loads, and the Razor `Main` component is rendered.

In the Main page, selecting the link to the `About` component works on the client because the Blazor router stops the browser from making a request on the Internet to `www.contoso.com` for `About` and serves the rendered `About` component itself. All of the requests for internal endpoints *within the Blazor WebAssembly app* work the same way: Requests don't trigger browser-based requests to server-hosted resources on the Internet. The router handles the requests internally.

If a request is made using the browser's address bar for `www.contoso.com/About`, the request fails. No such resource exists on the app's Internet host, so a *404 - Not Found* response is returned.

Because browsers make requests to Internet-based hosts for client-side pages, web servers and hosting services must rewrite all requests for resources not physically on the server to the `index.html` page. When `index.html` is returned, the app's Blazor router takes over and responds with the correct resource.

When deploying to an IIS server, you can use the URL Rewrite Module with the app's published `web.config` file. For more information, see [Host and deploy ASP.NET Core Blazor WebAssembly with IIS](iis?view=aspnetcore-10.0).

## Hosted deployment with ASP.NET Core

A *hosted deployment* serves the Blazor WebAssembly app to browsers from an [ASP.NET Core app](../../../overview?view=aspnetcore-10.0) that runs on a web server.

The client Blazor WebAssembly app is published into the `/bin/Release/{TARGET FRAMEWORK}/publish/wwwroot` folder of the server app, along with any other static web assets of the server app. The two apps are deployed together. A web server that is capable of hosting an ASP.NET Core app is required. For a hosted deployment, Visual Studio includes the **Blazor WebAssembly App** project template (`blazorwasm` template when using the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) command) with the **`Hosted`** option selected (`-ho|--hosted` when using the `dotnet new` command).

For more information, see the following articles:

* ASP.NET Core app hosting and deployment: [Host and deploy ASP.NET Core](../../../host-and-deploy/?view=aspnetcore-10.0)
* Deployment to Azure App Service: [Publish an ASP.NET Core app to Azure with Visual Studio](../../../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0)
* Blazor project templates: [ASP.NET Core Blazor project structure](../../project-structure?view=aspnetcore-10.0)

## Hosted deployment of a framework-dependent executable for a specific platform

To deploy a hosted Blazor WebAssembly app as a [framework-dependent executable for a specific platform](/en-us/dotnet/core/deploying/#publish-framework-dependent) (not self-contained) use the following guidance based on the tooling in use.

### Visual Studio

A [self-contained](/en-us/dotnet/core/deploying/#publish-self-contained) deployment is configured for a generated publish profile (`.pubxml`). Confirm that the **Server** project's publish profile contains the `<SelfContained>` MSBuild property set to `false`.

In the `.pubxml` publish profile file in the **Server** project's `Properties` folder:

```
<SelfContained>false</SelfContained>
```

Set the [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog) using the **Target Runtime** setting in the **Settings** area of the **Publish** UI, which generates the `<RuntimeIdentifier>` MSBuild property in the publish profile:

```
<RuntimeIdentifier>{RID}</RuntimeIdentifier>
```

In the preceding configuration, the `{RID}` placeholder is the [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog).

Publish the **Server** project in the **Release** configuration.

Note

It's possible to publish an app with publish profile settings using the .NET CLI by passing `/p:PublishProfile={PROFILE}` to the [`dotnet publish` command](/en-us/dotnet/core/tools/dotnet-publish), where the `{PROFILE}` placeholder is the profile. For more information, see the *Publish profiles* and *Folder publish example* sections in the [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](../../../host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-10.0#publish-profiles) article. If you pass the RID in the [`dotnet publish` command](/en-us/dotnet/core/tools/dotnet-publish) and not in the publish profile, use the MSBuild property (`/p:RuntimeIdentifier`) with the command, not with the `-r|--runtime` option.

### .NET CLI

Configure a [self-contained](/en-us/dotnet/core/deploying/#publish-self-contained) deployment by placing the `<SelfContained>` MSBuild property in a `<PropertyGroup>` in the **Server** project's project file set to `false`:

```
<SelfContained>false</SelfContained>
```

Important

The `SelfContained` property must be placed in the **Server** project's project file. The property can't be set correctly with the [`dotnet publish` command](/en-us/dotnet/core/tools/dotnet-publish) using the `--no-self-contained` option or the MSBuild property `/p:SelfContained=false`.

Set the [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog) using ***either*** of the following approaches:

* Option 1: Set the RID in a `<PropertyGroup>` in the **Server** project's project file:

  ```
  <RuntimeIdentifier>{RID}</RuntimeIdentifier>
  ```

  In the preceding configuration, the `{RID}` placeholder is the [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog).

  Publish the app in the Release configuration from the **Server** project:

  ```
  dotnet publish -c Release
  ```

* Option 2: Pass the RID in the [`dotnet publish` command](/en-us/dotnet/core/tools/dotnet-publish) as the MSBuild property (`/p:RuntimeIdentifier`), not with the `-r|--runtime` option:

  ```
  dotnet publish -c Release /p:RuntimeIdentifier={RID}
  ```

  In the preceding command, the `{RID}` placeholder is the [Runtime Identifier (RID)](/en-us/dotnet/core/rid-catalog).

For more information, see the following articles:

* [.NET application publishing overview](/en-us/dotnet/core/deploying/)
* [Host and deploy ASP.NET Core](../../../host-and-deploy/?view=aspnetcore-10.0)

## Standalone deployment

A *standalone deployment* serves the Blazor WebAssembly app as a set of static files that are requested directly by clients. Any static file server is able to serve the Blazor app.

Standalone deployment assets are published into either the `/bin/Release/{TARGET FRAMEWORK}/publish/wwwroot` or `bin/Release/{TARGET FRAMEWORK}/browser-wasm/publish` folder, where the `{TARGET FRAMEWORK}` placeholder is the target framework.

## Publish without an HTML document

When an app isn't using an HTML document produced as part of the .NET build/publish process, there's no place to write the site's [import map](https://developer.mozilla.org/docs/Web/HTML/Element/script/type/importmap). For example, this scenario occurs when publishing a standalone Blazor app for consumption by a JavaScript/SPA app through a [Blazor custom element](../../components/js-spa-frameworks?view=aspnetcore-10.0#blazor-custom-elements).

To avoid problems loading app resources on the client, disable [fingerprinting](../../fundamentals/static-files?view=aspnetcore-10.0#fingerprint-client-side-static-assets-in-standalone-blazor-webassembly-apps) for `blazor.webassembly.js` and `dotnet.js` by removing `<OverrideHtmlAssetPlaceholders>` MSBuild property (or set it to `false`) in the app's project file (`.csproj`).

## Azure App Service

Blazor WebAssembly apps can be deployed to Azure App Services on Windows, which hosts the app on IIS.

Deploying a standalone Blazor WebAssembly app to Azure App Service for Linux isn't currently supported. We recommend hosting a standalone Blazor WebAssembly app using [Azure Static Web Apps](azure-static-web-apps?view=aspnetcore-10.0), which supports this scenario.

## Standalone with Docker

A standalone Blazor WebAssembly app is published as a set of static files for hosting by a static file server.

To host the app in Docker:

* Choose a Docker container with web server support, such as Nginx or Apache.
* Copy the `publish` folder assets to a location folder defined in the web server for serving static files.
* Apply additional configuration as needed to serve the Blazor WebAssembly app.

For configuration guidance, see the following resources:

* [Nginx](nginx?view=aspnetcore-10.0) or [Apache](apache?view=aspnetcore-10.0) articles
* [Docker Documentation](https://docs.docker.com/)

## Host configuration values

[Blazor WebAssembly apps](../../hosting-models?view=aspnetcore-10.0#blazor-webassembly) can accept the following host configuration values as command-line arguments at runtime in the `Development` environment.

### Content root

The `--contentroot` argument sets the absolute path to the directory that contains the app's content files ([content root](../../../fundamentals/?view=aspnetcore-10.0#content-root)). In the following examples, `/content-root-path` is the app's content root path.

* Pass the argument when running the app locally at a command prompt. From the app's directory, execute:

  ```
  dotnet watch --contentroot=/content-root-path
  ```

* Add an entry to the app's `launchSettings.json` file in the **IIS Express** profile. This setting is used when the app is run with the Visual Studio Debugger and from a command prompt with `dotnet watch` (or `dotnet run`).

  ```
  "commandLineArgs": "--contentroot=/content-root-path"
  ```

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

* In Visual Studio, specify the argument in **Properties** > **Debug** > **Application arguments**. Setting the argument in the Visual Studio property page adds the argument to the `launchSettings.json` file.

  ```
  --contentroot=/content-root-path
  ```

### Path base

The `--pathbase` argument sets the app base path for an app run locally with a non-root relative URL path (the `<base>` tag `href` is set to a path other than `/` for staging and production). In the following examples, `/relative-URL-path` is the app's path base. For more information, see [ASP.NET Core Blazor app base path](../app-base-path?view=aspnetcore-10.0).

Important

Unlike the path provided to `href` of the `<base>` tag, don't include a trailing slash (`/`) when passing the `--pathbase` argument value. If the app base path is provided in the `<base>` tag as `<base href="/CoolApp/">` (includes a trailing slash), pass the command-line argument value as `--pathbase=/CoolApp` (no trailing slash).

* Pass the argument when running the app locally at a command prompt. From the app's directory, execute:

  ```
  dotnet watch --pathbase=/relative-URL-path
  ```

* Add an entry to the app's `launchSettings.json` file in the **IIS Express** profile. This setting is used when running the app with the Visual Studio Debugger and from a command prompt with `dotnet watch` (or `dotnet run`).

  ```
  "commandLineArgs": "--pathbase=/relative-URL-path"
  ```

* In Visual Studio, specify the argument in **Properties** > **Debug** > **Application arguments**. Setting the argument in the Visual Studio property page adds the argument to the `launchSettings.json` file.

  ```
  --pathbase=/relative-URL-path
  ```

For more information, see [ASP.NET Core Blazor app base path](../app-base-path?view=aspnetcore-10.0).

### URLs

The `--urls` argument sets the IP addresses or host addresses with ports and protocols to listen on for requests.

* Pass the argument when running the app locally at a command prompt. From the app's directory, execute:

  ```
  dotnet watch --urls=http://127.0.0.1:0
  ```

* Add an entry to the app's `launchSettings.json` file in the **IIS Express** profile. This setting is used when running the app with the Visual Studio Debugger and from a command prompt with `dotnet watch` (or `dotnet run`).

  ```
  "commandLineArgs": "--urls=http://127.0.0.1:0"
  ```

* In Visual Studio, specify the argument in **Properties** > **Debug** > **Application arguments**. Setting the argument in the Visual Studio property page adds the argument to the `launchSettings.json` file.

  ```
  --urls=http://127.0.0.1:0
  ```

## Configure the Trimmer

Blazor performs Intermediate Language (IL) trimming on each Release build to remove unnecessary IL from the output assemblies. For more information, see [Configure the Trimmer for ASP.NET Core Blazor](../configure-trimmer?view=aspnetcore-10.0).

## Configure the Linker

Blazor performs Intermediate Language (IL) linking on each Release build to remove unnecessary IL from the output assemblies. For more information, see [Configure the Linker for ASP.NET Core Blazor](../configure-linker?view=aspnetcore-10.0).

## Change the file name extension of DLL files

*This section applies to .NET 5 through .NET 7. In .NET 8 or later, .NET assemblies are deployed as WebAssembly files (`.wasm`) using the Webcil file format.*

If a firewall, anti-virus program, or network security appliance is blocking the transmission of the app's dynamic-link library (DLL) files (`.dll`), you can follow the guidance in this section to change the file name extensions of the app's published DLL files.

Changing the file name extensions of the app's DLL files might not resolve the problem because many security systems scan the content of the app's files, not merely check file extensions.

For a more robust approach in environments that block the download and execution of DLL files, take ***either*** of the following approaches:

* Use .NET 8 or later, which packages .NET assemblies as WebAssembly files (`.wasm`) using the [Webcil](https://github.com/dotnet/runtime/blob/main/docs/design/mono/webcil.md) file format. For more information, see the *Webcil packaging format for .NET assemblies* section in a .NET 8 or later version of this article.
* In .NET 6 or later, use a [custom deployment layout](deployment-layout?view=aspnetcore-10.0).

Third-party approaches exist for dealing with this problem. For more information, see the resources at [Awesome Blazor](https://github.com/AdrienTorris/awesome-blazor).

After publishing the app, use a shell script or DevOps build pipeline to rename `.dll` files to use a different file extension in the directory of the app's published output.

In the following examples:

* PowerShell (PS) is used to update the file extensions.
* `.dll` files are renamed to use the `.bin` file extension from the command line.
* Files listed in the published Blazor boot manifest with a `.dll` file extension are updated to the `.bin` file extension.
* If service worker assets are also in use, a PowerShell command updates the `.dll` files listed in the `service-worker-assets.js` file to the `.bin` file extension.

To use a different file extension than `.bin`, replace `.bin` in the following commands with the desired file extension.

* [Windows](#tabpanel_1_windows)
* [Linux / macOS](#tabpanel_1_linux-macos)

In the following commands, the `{PATH}` placeholder is the path to the published `_framework` folder in the [`publish` folder](../?view=aspnetcore-10.0#default-publish-locations).

Rename file extensions in the folder:

```
dir {PATH} | rename-item -NewName { $_.name -replace ".dll\b",".bin" }
```

Rename file extensions in the `blazor.boot.json` file:

```
((Get-Content {PATH}\blazor.boot.json -Raw) -replace '.dll"','.bin"') | Set-Content {PATH}\blazor.boot.json
```

If service worker assets are also in use because the app is a [Progressive Web App (PWA)](../../progressive-web-app/?view=aspnetcore-10.0):

```
((Get-Content {PATH}\service-worker-assets.js -Raw) -replace '.dll"','.bin"') | Set-Content {PATH}\service-worker-assets.js
```

In the preceding command, the `{PATH}` placeholder is the path to the published `service-worker-assets.js` file.

In the following commands, the `{PATH}` placeholder is the path to the published `_framework` folder in the [`publish` folder](../?view=aspnetcore-10.0#default-publish-locations).

Rename file extensions in the folder:

```
for f in {PATH}/*; do mv "$f" "`echo $f | sed -e 's/\.dll/.bin/g'`"; done
```

Rename file extensions in the `blazor.boot.json` file:

```
sed -i 's/\.dll"/.bin"/g' {PATH}/blazor.boot.json
```

If service worker assets are also in use because the app is a [Progressive Web App (PWA)](../../progressive-web-app/?view=aspnetcore-10.0):

```
sed -i 's/\.dll"/.bin"/g' {PATH}/service-worker-assets.js
```

In the preceding command, the `{PATH}` placeholder is the path to the published `service-worker-assets.js` file.

To address the compressed `blazor.boot.json` file, adopt either of the following approaches:

* Recompress the updated `blazor.boot.json` file, producing new `blazor.boot.json.gz` and `blazor.boot.json.br` files. (*Recommended*)
* Remove the compressed `blazor.boot.json.gz` and `blazor.boot.json.br` files. (*Compression is disabled with this approach.*)

For a [Progressive Web App (PWA)](../../progressive-web-app/?view=aspnetcore-10.0)'s compressed `service-worker-assets.js` file, adopt either of the following approaches:

* Recompress the updated `service-worker-assets.js` file, producing new `service-worker-assets.js.br` and `service-worker-assets.js.gz` files. (*Recommended*)
* Remove the compressed `service-worker-assets.js.gz` and `service-worker-assets.js.br` files. (*Compression is disabled with this approach.*)

To automate the extension change on Windows in .NET 6/7, the following approach uses a PowerShell script placed at the root of the project. The following script, which disables compression, is the basis for further modification if you wish to recompress the `blazor.boot.json` file and `service-worker-assets.js` file if the app is a [Progressive Web App (PWA)](../../progressive-web-app/?view=aspnetcore-10.0). The path to the [`publish` folder](../?view=aspnetcore-10.0#default-publish-locations) is passed to the script when it's executed.

`ChangeDLLExtensions.ps1:`:

```
param([string]$filepath)
dir $filepath\wwwroot\_framework | rename-item -NewName { $_.name -replace ".dll\b",".bin" }
((Get-Content $filepath\wwwroot\_framework\blazor.boot.json -Raw) -replace '.dll"','.bin"') | Set-Content $filepath\wwwroot\_framework\blazor.boot.json
Remove-Item $filepath\wwwroot\_framework\blazor.boot.json.gz
Remove-Item $filepath\wwwroot\_framework\blazor.boot.json.br
```

If service worker assets are also in use because the app is a [Progressive Web App (PWA)](../../progressive-web-app/?view=aspnetcore-10.0), add the following commands:

```
((Get-Content $filepath\wwwroot\service-worker-assets.js -Raw) -replace '.dll"','.bin"') | Set-Content $filepath\wwwroot\service-worker-assets.js
Remove-Item $filepath\wwwroot\service-worker-assets.js.gz
Remove-Item $filepath\wwwroot\service-worker-assets.js.br
```

In the project file, the script is executed after publishing the app for the `Release` configuration:

```
<Target Name="ChangeDLLFileExtensions" AfterTargets="AfterPublish" Condition="'$(Configuration)'=='Release'">
  <Exec Command="powershell.exe -command &quot;&amp; {.\ChangeDLLExtensions.ps1 '$(SolutionDir)'}&quot;" />
</Target>
```

After publishing the app, manually recompress `blazor.boot.json`, and `service-worker-assets.js` if used, to re-enable compression.

Note

When renaming and lazy loading the same assemblies, see the guidance in [Lazy load assemblies in ASP.NET Core Blazor WebAssembly](../../webassembly-lazy-load-assemblies?view=aspnetcore-10.0#onnavigateasync-events-and-renamed-assembly-files).

Usually, the app's server requires static asset configuration to serve the files with the updated extension. For an app hosted by IIS, add a MIME map entry (`<mimeMap>`) for the new file extension in the static content section (`<staticContent>`) in a custom `web.config` file. The following example assumes that the file extension is changed from `.dll` to `.bin`:

```
<staticContent>
  ...
  <mimeMap fileExtension=".bin" mimeType="application/octet-stream" />
  ...
</staticContent>
```

Include an update for compressed files if [compression](#compression) is in use:

```
<mimeMap fileExtension=".bin.br" mimeType="application/octet-stream" />
<mimeMap fileExtension=".bin.gz" mimeType="application/octet-stream" />
```

Remove the entry for the `.dll` file extension:

```
- <mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
```

Remove entries for compressed `.dll` files if [compression](#compression) is in use:

```
- <mimeMap fileExtension=".dll.br" mimeType="application/octet-stream" />
- <mimeMap fileExtension=".dll.gz" mimeType="application/octet-stream" />
```

For more information on custom `web.config` files, see the [Use of a custom `web.config`](iis?view=aspnetcore-10.0#use-of-a-custom-webconfig) section.

## Prior deployment corruption

Typically on deployment:

* Only the files that have changed are replaced, which usually results in a faster deployment.
* Existing files that aren't part of the new deployment are left in place for use by the new deployment.

In rare cases, lingering files from a prior deployment can corrupt a new deployment. Completely deleting the existing deployment (or locally-published app prior to deployment) may resolve the issue with a corrupted deployment. Often, deleting the existing deployment ***once*** is sufficient to resolve the problem, including for a DevOps build and deploy pipeline.

If you determine that clearing a prior deployment is always required when a DevOps build and deploy pipeline is in use, you can temporarily add a step to the build pipeline to delete the prior deployment for each new deployment until you troubleshoot the exact cause of the corruption.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-microsoft-entra-id
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/hosted-with-microsoft-entra-id.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure a hosted ASP.NET Core Blazor WebAssembly app with Microsoft Entra ID

Important

The Hosted Blazor WebAssembly project template was removed from the framework with the release of .NET 8 (November, 2023). The guidance in this article is only supported for .NET 7 or earlier. Hosted Blazor WebAssembly apps that are upgraded each release continue to receive product support. Alternatively, refactor the app into either a standalone Blazor WebAssembly app or a Blazor Web App.

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

This article explains how to create a [hosted Blazor WebAssembly solution](../../hosting-models?view=aspnetcore-7.0#blazor-webassembly) that uses [Microsoft Entra ID (ME-ID)](https://azure.microsoft.com/services/active-directory/) for authentication. This article focuses on a single tenant app with a single tenant Azure app registration.

This article doesn't cover a *multi-tenant ME-ID registration*. For more information, see [Making your application multi-tenant](/en-us/entra/identity-platform/howto-convert-app-to-be-multi-tenant).

This article focuses on the use of a **Microsoft Entra** tenant, as described in [Quickstart: Set up a tenant](/en-us/entra/identity-platform/quickstart-create-new-tenant). If the app is registered in an **Azure Active Directory B2C** tenant, as described in [Tutorial: Create an Azure Active Directory B2C tenant](/en-us/azure/active-directory-b2c/tutorial-create-tenant) but follows the guidance in this article, the App ID URI is managed differently by ME-ID. For more information, see the [Use of an Azure Active Directory B2C tenant](#use-of-an-azure-active-directory-b2c-tenant) section of this article.

* Modify the **Server** `appsettings.json` configuration
* Modify the default access token scope scheme
* Run the app

### Create a tenant in Azure

Follow the guidance in [Quickstart: Set up a tenant](/en-us/entra/identity-platform/quickstart-create-new-tenant) to create a tenant in ME-ID.

### Register a server API app in Azure

Register an ME-ID app for the *Server API app*:

1. Navigate to [**Microsoft Entra ID** in the Azure portal](https://entra.microsoft.com/#home). Select **Applications** > **App registrations** in the sidebar. Select the **New registration** button.
2. Provide a **Name** for the app (for example, **Blazor Server ME-ID**).
3. Choose a **Supported account types**. You may select **Accounts in this organizational directory only** (single tenant) for this experience.
4. The *Server API app* doesn't require a **Redirect URI** in this scenario, so leave the **Select a platform** dropdown list unselected and don't enter a redirect URI.
5. This article assumes the app is registered in a **Microsoft Entra** tenant. If the app is registered in an **Azure Active Directory B2C** tenant, the **Permissions** > **Grant admin consent to openid and offline\_access permissions** checkbox is present and selected. Deselect the checkbox to disable the setting. When using an **Active Azure Directory** tenant, the checkbox isn't present.
6. Select **Register**.

Record the following information:

* *Server API app* Application (client) ID (for example, `00001111-aaaa-2222-bbbb-3333cccc4444`)
* Directory (tenant) ID (for example, `aaaabbbb-0000-cccc-1111-dddd2222eeee`)
* ME-ID Primary/Publisher/Tenant domain (for example, `contoso.onmicrosoft.com`): The domain is available as the **Publisher domain** in the **Branding** blade of the Azure portal for the registered app.

In **API permissions**, remove the **Microsoft Graph** > **User.Read** permission, as the server API app doesn't require additional API access for merely signing in users and calling server API endpoints.

In **Expose an API**:

1. Confirm or add the **App ID URI** in the format `api://{SERVER API APP CLIENT ID}`.
2. Select **Add a scope**.
3. Select **Save and continue**.
4. Provide a **Scope name** (for example, `API.Access`).
5. Provide an **Admin consent display name** (for example, `Access API`).
6. Provide an **Admin consent description** (for example, `Allows the app to access server app API endpoints.`).
7. Confirm that the **State** is set to **Enabled**.
8. Select **Add scope**.

Record the following information:

* App ID URI GUID (for example, record `00001111-aaaa-2222-bbbb-3333cccc4444` from the App ID URI of `api://00001111-aaaa-2222-bbbb-3333cccc4444`)
* Scope name (for example, `API.Access`)

Important

If a custom value is used for the App ID URI, configuration changes are required to both the **Server** and **Client** apps after the apps are created from the Blazor WebAssembly project template. For more information, see the [Use of a custom App ID URI](#use-of-a-custom-app-id-uri) section.

### Register a client app in Azure

Register an ME-ID app for the *Client app*:

1. Navigate to **Microsoft Entra ID** in the Azure portal. Select **App registrations** in the sidebar. Select the **New registration** button.
2. Provide a **Name** for the app (for example, **Blazor Client ME-ID**).
3. Choose a **Supported account types**. You may select **Accounts in this organizational directory only** (single tenant) for this experience.
4. Set the **Redirect URI** dropdown list to **Single-page application (SPA)** and provide the following redirect URI: `https://localhost/authentication/login-callback`. If you know the production redirect URI for the Azure default host (for example, `azurewebsites.net`) or the custom domain host (for example, `contoso.com`), you can also add the production redirect URI at the same time that you're providing the `localhost` redirect URI. Be sure to include the port number for non-`:443` ports in any production redirect URIs that you add.
5. This article assumes the app is registered in a **Microsoft Entra** tenant. If the app is registered in an **Azure Active Directory B2C** tenant, the **Permissions** > **Grant admin consent to openid and offline\_access permissions** checkbox is present and selected. Deselect the checkbox to disable the setting. When using an **Active Azure Directory** tenant, the checkbox isn't present.
6. Select **Register**.

Note

Supplying the port number for a `localhost` ME-ID redirect URI isn't required. For more information, see [Redirect URI (reply URL) restrictions and limitations: Localhost exceptions (Entra documentation)](/en-us/entra/identity-platform/reply-url#localhost-exceptions).

Record the **Client** app Application (client) ID (for example, `11112222-bbbb-3333-cccc-4444dddd5555`).

In **Authentication** > **Platform configurations** > **Single-page application**:

1. Confirm the redirect URI of `https://localhost/authentication/login-callback` is present.
2. In the **Implicit grant** section, ensure that the checkboxes for **Access tokens** and **ID tokens** aren't selected. **Implicit grant isn't recommended for Blazor apps using MSAL v2.0 or later.** For more information, see [Secure ASP.NET Core Blazor WebAssembly](./?view=aspnetcore-7.0#use-the-authorization-code-flow-with-pkce).
3. The remaining defaults for the app are acceptable for this experience.
4. Select the **Save** button if you made changes.

In **API permissions**:

1. Confirm that the app has **Microsoft Graph** > **User.Read** permission.
2. Select **Add a permission** followed by **My APIs**.
3. Select the *Server API app* from the **Name** column (for example, **Blazor Server ME-ID**). You must be an owner of the app registration (and the API app registration if it's a separate app) in order to see the API in the **My APIs** area of the Azure portal. For more information, see [Assign application owner (Microsoft Entra documentation)](/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis#assign-application-owner).
4. Open the **API** list.
5. Enable access to the API (for example, `API.Access`).
6. Select **Add permissions**.
7. Select the **Grant admin consent for {TENANT NAME}** button. Select **Yes** to confirm.

Important

If you don't have the authority to grant admin consent to the tenant in the last step of **API permissions** configuration because consent to use the app is delegated to users, then you must take the following additional steps:

* The app must use a [trusted publisher domain](/en-us/entra/identity-platform/howto-configure-publisher-domain).
* In the **`Server`** app's configuration in the Azure portal, select **Expose an API**. Under **Authorized client applications**, select the button to **Add a client application**. Add the **`Client`** app's Application (client) ID (for example, `11112222-bbbb-3333-cccc-4444dddd5555`).

### Create the Blazor app

In an empty folder, replace the placeholders in the following command with the information recorded earlier and execute the command in a command shell:

```
dotnet new blazorwasm -au SingleOrg --api-client-id "{SERVER API APP CLIENT ID}" --app-id-uri "{SERVER API APP ID URI GUID}" --client-id "{CLIENT APP CLIENT ID}" --default-scope "{DEFAULT SCOPE}" --domain "{TENANT DOMAIN}" -ho -o {PROJECT NAME} --tenant-id "{TENANT ID}"
```

Warning

**Avoid using dashes (`-`) in the app name `{PROJECT NAME}` that break the formation of the OIDC app identifier.** Logic in the Blazor WebAssembly project template uses the project name for an OIDC app identifier in the solution's configuration. Pascal case (`BlazorSample`) or underscores (`Blazor_Sample`) are acceptable alternatives. For more information, see [Dashes in a hosted Blazor WebAssembly project name break OIDC security (dotnet/aspnetcore #35337)](https://github.com/dotnet/aspnetcore/issues/35337).

| Placeholder                    | Azure portal name                                | Example                                                                                      |
| ------------------------------ | ------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `{PROJECT NAME}`               | —                                                | `BlazorSample`                                                                               |
| `{CLIENT APP CLIENT ID}`       | Application (client) ID for the **Client** app   | `11112222-bbbb-3333-cccc-4444dddd5555`                                                       |
| `{DEFAULT SCOPE}`              | Scope name                                       | `API.Access`                                                                                 |
| `{SERVER API APP CLIENT ID}`   | Application (client) ID for the *Server API app* | `00001111-aaaa-2222-bbbb-3333cccc4444`                                                       |
| `{SERVER API APP ID URI GUID}` | Application ID URI GUID                          | `00001111-aaaa-2222-bbbb-3333cccc4444` (GUID ONLY, matches the `{SERVER API APP CLIENT ID}`) |
| `{TENANT DOMAIN}`              | Primary/Publisher/Tenant domain                  | `contoso.onmicrosoft.com`                                                                    |
| `{TENANT ID}`                  | Directory (tenant) ID                            | `aaaabbbb-0000-cccc-1111-dddd2222eeee`                                                       |

The output location specified with the `-o|--output` option creates a project folder if it doesn't exist and becomes part of the project's name. **Avoid using dashes (`-`) in the app name that break the formation of the OIDC app identifier (see the earlier WARNING).**

Important

If a custom value is used for the App ID URI, configuration changes are required to both the **Server** and **Client** apps after the apps are created from the Blazor WebAssembly project template. For more information, see the [Use of a custom App ID URI](#use-of-a-custom-app-id-uri) section.

## Configure `User.Identity.Name`

*The guidance in this section covers optionally populating `User.Identity.Name` with the value from the `name` claim.*

The **Server** app API populates `User.Identity.Name` with the value from the `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name` claim type (for example, `bbbb0000-cccc-1111-dddd-2222eeee3333@contoso.onmicrosoft.com`).

To configure the app to receive the value from the `name` claim type:

* Add a namespace for [Microsoft.AspNetCore.Authentication.JwtBearer](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer) to the `Program` file:

  ```
  using Microsoft.AspNetCore.Authentication.JwtBearer;
  ```

* Configure the [TokenValidationParameters.NameClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.nameclaimtype#microsoft-identitymodel-tokens-tokenvalidationparameters-nameclaimtype) of the [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions).

  ```
  builder.Services.Configure<JwtBearerOptions>(
      JwtBearerDefaults.AuthenticationScheme, options =>
      {
          options.TokenValidationParameters.NameClaimType = "name";
      });
  ```

  ```
  @using Microsoft.Extensions.Options
  @inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> RemoteOptions
  ```

  Modify the component's redirect in the `OnInitialized` method:

  ```
  - Navigation.NavigateToLogin("authentication/login");
  + Navigation.NavigateToLogin(RemoteOptions.Get(Options.DefaultName)
  +     .AuthenticationPaths.LogInPath);
  ```

  Note

  If other paths differ from the project template's paths or [framework's default paths](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationDefaults.cs), manage them in the same fashion.

## Parts of the solution

This section describes the parts of a solution generated from the Blazor WebAssembly project template and describes how the solution's **Client** and **Server** projects are configured for reference. There's no specific guidance to follow in this section for a basic working application if you created the app using the guidance in the [Walkthrough](#walkthrough) section. The guidance in this section is helpful for updating an app to authenticate and authorize users. However, an alternative approach to updating an app is to create a new app from the guidance in the [Walkthrough](#walkthrough) section and moving the app's components, classes, and resources to the new app.

### `appsettings.json` configuration

*This section pertains to the solution's **Server** app.*

The `appsettings.json` file contains the options to configure the JWT bearer handler used to validate access tokens. Add the following `AzureAd` configuration section:

```
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "{TENANT DOMAIN}",
    "TenantId": "{TENANT ID}",
    "ClientId": "{SERVER API APP CLIENT ID}",
    "CallbackPath": "/signin-oidc",
    "Scopes": "{SCOPES}"
  }
}
```

Example:

```
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "contoso.onmicrosoft.com",
    "TenantId": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
    "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
    "CallbackPath": "/signin-oidc",
    "Scopes": "API.Access"
  }
}
```

Important

If the **Server** app is registered to use a custom App ID URI in ME-ID (not in the default format `api://{SERVER API APP CLIENT ID}`), see the [Use of a custom App ID URI](#use-of-a-custom-app-id-uri) section. Changes are required in both the **Server** and **Client** apps.

### Authentication package

*This section pertains to the solution's **Server** app.*

The support for authenticating and authorizing calls to ASP.NET Core web APIs with the Microsoft identity platform is provided by the [`Microsoft.Identity.Web` package](https://www.nuget.org/packages/Microsoft.Identity.Web).

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

The **Server** app of a hosted Blazor solution created from the Blazor WebAssembly template includes the [`Microsoft.Identity.Web.UI` package](https://www.nuget.org/packages/Microsoft.Identity.Web.UI). The package adds UI for user authentication in web apps and isn't used by the Blazor framework. If the **Server** app won't be used to authenticate users directly, it's safe to remove the package reference from the **Server** app's project file.

### Authentication service support

*This section pertains to the solution's **Server** app.*

The `AddAuthentication` method sets up authentication services within the app and configures the JWT bearer handler as the default authentication method. The [AddMicrosoftIdentityWebApi](/en-us/dotnet/api/microsoft.identity.web.microsoftidentitywebapiauthenticationbuilderextensions.addmicrosoftidentitywebapi) method configures services to protect the web API with Microsoft identity platform v2.0. This method expects an `AzureAd` section in the app's configuration with the necessary settings to initialize authentication options.

```
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
```

Note

When a single authentication scheme is registered, the authentication scheme is automatically used as the app's default scheme, and it isn't necessary to state the scheme to [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) or via [AuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions). For more information, see [Overview of ASP.NET Core Authentication](../../../security/authentication/?view=aspnetcore-7.0#defaultscheme) and the [ASP.NET Core announcement (aspnet/Announcements #490)](https://github.com/aspnet/Announcements/issues/490).

[UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) ensure that:

* The app attempts to parse and validate tokens on incoming requests.
* Any request attempting to access a protected resource without proper credentials fails.

```
app.UseAuthentication();
app.UseAuthorization();
```

### WeatherForecast controller

*This section pertains to the solution's **Server** app.*

The `WeatherForecast` controller (`Controllers/WeatherForecastController.cs`) exposes a protected API with the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) applied to the controller. It's **important** to understand that:

* The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) in this API controller is the only thing that protects this API from unauthorized access.
* The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) used in the Blazor WebAssembly app only serves as a hint to the app that the user should be authorized for the app to work correctly.

```
[Authorize]
[ApiController]
[Route("[controller]")]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        ...
    }
}
```

### `wwwroot/appsettings.json` configuration

*This section pertains to the solution's **Client** app.*

Configuration is supplied by the `wwwroot/appsettings.json` file:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT APP CLIENT ID}",
    "ValidateAuthority": true
  }
}
```

Example:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/e86c78e2-...-918e0565a45e",
    "ClientId": "11112222-bbbb-3333-cccc-4444dddd5555",
    "ValidateAuthority": true
  }
}
```

### Authentication package

*This section pertains to the solution's **Client** app.*

When an app is created to use Work or School Accounts (`SingleOrg`), the app automatically receives a package reference for the [Microsoft Authentication Library](/en-us/entra/identity-platform/msal-overview) ([`Microsoft.Authentication.WebAssembly.Msal`](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal)). The package provides a set of primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the [`Microsoft.Authentication.WebAssembly.Msal` package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal) to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

The [`Microsoft.Authentication.WebAssembly.Msal` package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal) transitively adds the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) to the app.

### Authentication service support

*This section pertains to the solution's **Client** app.*

Support for [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances is added that include access tokens when making requests to the **Server** app.

In the `Program` file:

```
builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI", client => 
        client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("{PROJECT NAME}.ServerAPI"));
```

The `{PROJECT NAME}` placeholder is the project name at solution creation. For example, providing a project name of `BlazorSample` produces a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) of `BlazorSample.ServerAPI`.

Support for authenticating users is registered in the service container with the [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) extension method provided by the [`Microsoft.Authentication.WebAssembly.Msal` package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal). This method sets up the services required for the app to interact with the Identity Provider (IP).

In the `Program` file:

```
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
```

The [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) method accepts a callback to configure the parameters required to authenticate an app. The values required for configuring the app can be obtained from the Azure portal ME-ID configuration when you register the app.

### Access token scopes

*This section pertains to the solution's **Client** app.*

The default access token scopes represent the list of access token scopes that are:

* Included in the sign in request.
* Used to provision an access token immediately after authentication.

Additional scopes can be added as needed in the `Program` file:

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
```

Specify additional scopes with `AdditionalScopesToConsent`:

```
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE URI}");
```

Note

[AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent) isn't able to provision delegated user permissions for Microsoft Graph via the Microsoft Entra ID consent UI when a user first uses an app registered in Microsoft Azure. For more information, see [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-7.0\&pivots=graph-sdk-5#defaultaccesstokenscopes-versus-additionalscopestoconsent).

Example default access token scope:

```
options.ProviderOptions.DefaultAccessTokenScopes.Add(
    "api://00001111-aaaa-2222-bbbb-3333cccc4444/API.Access");
```

For more information, see the following sections of the *Additional scenarios* article:

* [Request additional access tokens](additional-scenarios?view=aspnetcore-7.0#request-additional-access-tokens)
* [Attach tokens to outgoing requests](additional-scenarios?view=aspnetcore-7.0#attach-tokens-to-outgoing-requests)

### Login mode

*This section pertains to the solution's **Client** app.*

The framework defaults to pop-up login mode and falls back to redirect login mode if a pop-up can't be opened. Configure MSAL to use redirect login mode by setting the `LoginMode` property of [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions) to `redirect`:

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.LoginMode = "redirect";
});
```

The default setting is `popup`, and the string value isn't case-sensitive.

### Imports file

*This section pertains to the solution's **Client** app.*

The [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespace is made available throughout the app via the imports file (`_Imports.razor`):

```
...
@using Microsoft.AspNetCore.Components.Authorization
...
```

### Index page

*This section pertains to the solution's **Client** app.*

The Index page (`wwwroot/index.html`) page includes a script that defines the `AuthenticationService` in JavaScript. `AuthenticationService` handles the low-level details of the OIDC protocol. The app internally calls methods defined in the script to perform the authentication operations.

```
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
```

```
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string? Action { get; set; }
}
```

```
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using {APP NAMESPACE}.Shared
@attribute [Authorize]
@inject HttpClient Http

...

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}
```

## Use of an Azure Active Directory B2C tenant

If the app is registered in an **Azure Active Directory B2C** tenant, as described in [Tutorial: Create an Azure Active Directory B2C tenant](/en-us/azure/active-directory-b2c/tutorial-create-tenant) but follows the guidance in this article, the App ID URI is managed differently by ME-ID.

You can check the tenant type of an existing tenant by selecting the **Manage tenants** link at the top of the ME-ID organization **Overview**. Examine the **Tenant type** column value for the organization. This section pertains to apps that follow the guidance in this article but that are registered in an **Azure Active Directory B2C** tenant.

Instead of the App ID URI matching the format `api://{SERVER API APP CLIENT ID OR CUSTOM VALUE}`, the App ID URI has the format `https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE}`. This difference affects **Client** and **Server** app configurations:

* For the server API app, set the `Audience` in the app settings file (`appsettings.json`) to match the app's audience (App ID URI) provided by the Azure portal with no trailing slash:

  ```
  "Audience": "https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE}"
  ```

  Example:

  ```
  "Audience": "https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444"
  ```

* In the `Program` file of the **`Client`** app, set the audience of the scope (App ID URI) to match the server API app's audience:

  ```
  options.ProviderOptions.DefaultAccessTokenScopes
      .Add("https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE}/{DEFAULT SCOPE}");
  ```

  In the preceding scope, the App ID URI/audience is the `https://{TENANT}.onmicrosoft.com/{SERVER API APP CLIENT ID OR CUSTOM VALUE}` portion of the value, which doesn't include a trailing slash (`/`) and doesn't include the scope name (`{DEFAULT SCOPE}`).

  Example:

  ```
  options.ProviderOptions.DefaultAccessTokenScopes
      .Add("https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444/API.Access");
  ```

  In the preceding scope, the App ID URI/audience is the `https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444` portion of the value, which doesn't include a trailing slash (`/`) and doesn't include the scope name (`API.Access`).

## Use of a custom App ID URI

If the App ID URI is a custom value, you must manually update the default access token scope URI in the **Client** app and add the audience to the **Server** app's ME-ID configuration.

Important

The following configuration isn't required when using the default App ID URI of `api://{SERVER API APP CLIENT ID}`.

Example App ID URI of `urn://custom-app-id-uri` and a scope name of `API.Access`:

* In the `Program` file of the **Client** app:

  ```
  options.ProviderOptions.DefaultAccessTokenScopes.Add(
      "urn://custom-app-id-uri/API.Access");
  ```

* In `appsettings.json` of the **Server** app, add an `Audience` entry with **only** the App ID URI and no trailing slash:

  ```
  "Audience": "urn://custom-app-id-uri"
  ```

    * [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)
    * [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)
    * [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

  > info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

```
@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthenticationState { get; set; }

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}
```

### Inspect the content of a JSON Web Token (JWT)

To decode a JSON Web Token (JWT), use Microsoft's [jwt.ms](https://jwt.ms/) tool. Values in the UI never leave your browser.

Example encoded JWT (shortened for display):

> eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V\_bU55Ug\_PW4pLPr20tTS8Ct7\_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2\_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

```
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
  "sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]
```

## Related content

* [Configure an app's publisher domain](/en-us/entra/identity-platform/howto-configure-publisher-domain)
* [Microsoft Entra ID app manifest: identifierUris attribute](/en-us/entra/identity-platform/reference-app-manifest#identifieruris-attribute)
* [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-7.0)
* [Build a custom version of the Authentication.MSAL JavaScript library](additional-scenarios?view=aspnetcore-7.0#build-a-custom-version-of-the-authenticationmsal-javascript-library)
* [Unauthenticated or unauthorized web API requests in an app with a secure default client](additional-scenarios?view=aspnetcore-7.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client)
* [ASP.NET Core Blazor WebAssembly with Microsoft Entra ID groups and roles (.NET 5 to .NET 7)](microsoft-entra-id-groups-and-roles-net-5-to-7?view=aspnetcore-7.0)
* [Microsoft identity platform and Microsoft Entra ID with ASP.NET Core](../../../security/authentication/azure-active-directory/?view=aspnetcore-7.0)
* [Microsoft identity platform documentation](/en-us/entra/identity-platform/)
* [Quickstart: Register an application with the Microsoft identity platform](/en-us/entra/identity-platform/quickstart-register-app)
* [Security best practices for application properties in Microsoft Entra ID](/en-us/entra/identity-platform/security-best-practices-for-app-registration)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-11-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/response-compression
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/response-compression.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Response compression in ASP.NET Core

Network bandwidth is a limited resource. Reducing the size of the response usually increases the responsiveness of an app, often dramatically. One way to reduce payload size is to compress the responses from an application. This article describes how to implement response compression for your apps by using response compression middleware in ASP.NET Core.

## Explore compression with HTTPS

Compressed responses over secure connections can be controlled with the [EnableForHttps](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.responsecompressionoptions.enableforhttps#microsoft-aspnetcore-responsecompression-responsecompressionoptions-enableforhttps) option, which is disabled by default because of the security risk. Using compression with dynamically generated pages can expose the app to [CRIME](https://wikipedia.org/wiki/CRIME) and [BREACH](https://wikipedia.org/wiki/BREACH) attacks. CRIME and BREACH attacks can be mitigated in ASP.NET Core with antiforgery tokens. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-10.0). For information on mitigating BREACH attacks, see [Mitigations](http://www.breachattack.com/#mitigations) at <http://www.breachattack.com/>.

Even when the app disables the `EnableForHttps` property (`false`), Internet Information Services (IIS), IIS Express, and [Azure App Service](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0) can apply [Gzip](https://www.gnu.org/software/gzip/) at the IIS web server. When you review response headers, take note of the [Server](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Server) header value. An unexpected `content-encoding` response header value might be the result of the web server and not the ASP.NET Core app configuration.

## Determine when to use response compression middleware

Use server-based response compression technologies in IIS, Apache, or Nginx. The performance of the response compression middleware probably can't match that of the server modules. The [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) server and [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server don't currently offer built-in compression support.

Use response compression middleware when the app is:

* Unable to use the following server-based compression technologies:

  * [IIS dynamic caching and compression](https://www.iis.net/overview/reliability/dynamiccachingandcompression)
  * [Apache mod\_deflate module](https://httpd.apache.org/docs/current/mod/mod_deflate.html)
  * [Nginx compression and decompression](https://docs.nginx.com/nginx/admin-guide/web-server/compression/)

* Hosting directly on:

  * [HTTP.sys server](../fundamentals/servers/httpsys?view=aspnetcore-10.0)
  * [Kestrel server](../fundamentals/servers/kestrel?view=aspnetcore-10.0)

## Explore response compression

Usually, any response not natively compressed can benefit from response compression. Responses not natively compressed typically include CSS, JavaScript, HTML, XML, and JSON. Don't compress natively compressed assets, such as PNG files. When attempting to further compress a natively compressed response, any small extra reduction in size and transmission time is likely overshadowed by the time it takes to process the compression. Don't compress files smaller than about 150 - 1,000 bytes, depending on the file's content and the efficiency of compression. The overhead of compressing small files might produce a compressed file larger than the uncompressed file.

When a client can process compressed content, the client must inform the server of its capabilities by sending the [Accept-Encoding](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Accept-Encoding) header with the request. When a server sends compressed content, it must include information in the [Content-Encoding](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Encoding) header regarding how the compressed response is encoded.

The following table shows the content encoding designations for the `Accept-Encoding` header and indicates whether the response compression middleware supports the designation.

| Designation    | Middleware    | Format                                                               | Details                                                                        |
| -------------- | ------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `br`           | Yes (default) | Brotli Compressed Data Format                                        | [RFC 7932](https://datatracker.ietf.org/doc/html/rfc7932)                      |
| `deflate`      | No            | DEFLATE Compressed Data Format                                       | [RFC 1951](https://datatracker.ietf.org/doc/html/rfc1951)                      |
| `exi`          | No            | Efficient XML Interchange (EXI)                                      | [W3C Recommendation](https://www.w3.org/TR/exi/)                               |
| `gzip`         | Yes           | Gzip file format                                                     | [RFC 1952](https://www.ietf.org/rfc/rfc1952.txt)                               |
| `identity`     | Yes           | "No encoding" - the response must not be encoded                     | [Troubleshoot response compression](#troubleshoot-response-compression)        |
| `pack200-gzip` | No            | Network Transfer Format for Java archives                            | [JSR 200](https://jcp.org/aboutJava/communityprocess/review/jsr200/index.html) |
| `*` (asterisk) | Yes           | "Wildcard" - any available content encoding not explicitly requested | [Troubleshoot response compression](#troubleshoot-response-compression)        |

| Designation    | Middleware    | Format                                                               | Details                                                                        |
| -------------- | ------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `br`           | Yes (default) | Brotli Compressed Data Format                                        | [RFC 7932](https://datatracker.ietf.org/doc/html/rfc7932)                      |
| `deflate`      | No            | DEFLATE Compressed Data Format                                       | [RFC 1951](https://datatracker.ietf.org/doc/html/rfc1951)                      |
| `exi`          | No            | Efficient XML Interchange (EXI)                                      | [W3C Recommendation](https://www.w3.org/TR/exi/)                               |
| `gzip`         | Yes           | Gzip file format                                                     | [RFC 1952](https://www.ietf.org/rfc/rfc1952.txt)                               |
| `identity`     | Yes           | "No encoding" - the response must not be encoded                     | [Troubleshoot response compression](#troubleshoot-response-compression)        |
| `pack200-gzip` | No            | Network Transfer Format for Java archives                            | [JSR 200](https://jcp.org/aboutJava/communityprocess/review/jsr200/index.html) |
| `zstd`         | Yes (default) | Zstandard Compressed Data Format                                     | [RFC 8878](https://datatracker.ietf.org/doc/html/rfc8878)                      |
| `*` (asterisk) | Yes           | "Wildcard" - any available content encoding not explicitly requested | [Troubleshoot response compression](#troubleshoot-response-compression)        |

For more information, see the [IANA Official Content Coding List](https://www.iana.org/assignments/http-parameters/http-parameters.xml#http-content-coding-registry) for HTTP parameters.

The response compression middleware allows adding other compression providers for custom `Accept-Encoding` header values. For more information, see [Custom Providers](#custom-providers) later in this article.

The response compression middleware is capable of reacting to quality value (qvalue, `q`) weighting when sent by the client to prioritize compression schemes. For more information, see [RFC 9110: HTTP Semantics (Section 12.5.3 Accept-Encoding)](https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding).

Compression algorithms are subject to a tradeoff between compression speed and the effectiveness of the compression. *Effectiveness* in this context refers to the size of the output after compression. The smallest size is achieved by the optimal compression.

The headers involved in requesting, sending, caching, and receiving compressed content are described in the following table.

| Header             | Role                                                                                                                                                                                                                                                                                                                                                                                                                         | Details                                                                                                   |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `Accept-Encoding`  | Sent from the client to the server to indicate the content encoding schemes acceptable to the client.                                                                                                                                                                                                                                                                                                                        | [Accept-Encoding header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Accept-Encoding)   |
| `Content-Encoding` | Sent from the server to the client to indicate the encoding of the content in the payload.                                                                                                                                                                                                                                                                                                                                   | [Content-Encoding header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Encoding) |
| `Content-Length`   | When compression occurs, the `Content-Length` header is removed because the body content changes when the response is compressed.                                                                                                                                                                                                                                                                                            | [Content-Length header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Length)     |
| `Content-MD5`      | When compression occurs, the `Content-MD5` header is removed because the body content is changed and the hash is no longer valid.                                                                                                                                                                                                                                                                                            | [RFC 1864: The Content-MD5 Header Field](https://datatracker.ietf.org/doc/html/rfc1864)                   |
| `Content-Type`     | Specifies the [MIME type](https://developer.mozilla.org/docs/Glossary/MIME_type) of the content. Every response should specify its `Content-Type` value. The response compression middleware checks this value to determine if the response should be compressed. The response compression middleware specifies a set of [default MIME types](#review-the-mime-types) that it can encode, and they can be replaced or added. | [Content-Type header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Type)         |
| `Vary`             | When sent by the server with a value of `Accept-Encoding` to clients and proxies, the `Vary` header indicates to the client or proxy that it should cache (vary) responses based on the value of the `Accept-Encoding` header of the request. The result of returning content with the `Vary: Accept-Encoding` header is that both compressed and uncompressed responses are cached separately.                              | [Vary header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Vary)                         |

Explore the features of the response compression middleware with the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/response-compression/samples). The sample illustrates:

* Compression of app responses by using Gzip and custom compression providers.
* How to add a [MIME type](https://developer.mozilla.org/docs/Web/HTTP/Guides/MIME_types) to the default list of MIME types for compression.
* How to add a custom response compression provider.

## Configure the response compression middleware

The following code shows how to enable the response compression middleware for default [MIME types](https://developer.mozilla.org/docs/Web/HTTP/Guides/MIME_types) and compression providers ([Brotli and Gzip](#brotli-and-gzip-compression-providers)):

The following code shows how to enable the response compression middleware for default [MIME types](https://developer.mozilla.org/docs/Web/HTTP/Guides/MIME_types) and compression providers ([Brotli, Gzip, and Zstandard](#review-providers)):

### Notes about response compression middleware

When you work with response compression middleware, keep in mind the following points:

* Setting the `EnableForHttps` property to `true` is a security risk. For more information, see the [Compression with HTTPS](#explore-compression-with-https) section earlier in this article.
* The [app.UseResponseCompression](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecompressionbuilderextensions.useresponsecompression) method must be called ***before*** any middleware that compresses responses. For more information, see [ASP.NET Core middleware > Middleware order](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).
* Use a tool such as [Firefox Browser - Developer edition](https://www.firefox.com/channel/desktop/developer/) to set the `Accept-Encoding` request header and examine the response headers, size, and body.

Submit a request to the sample app without the `Accept-Encoding` header and observe that the response is uncompressed. The `Content-Encoding` header isn't in the Response Headers collection.

For example, in Firefox Developer:

1. Select the network tab.
2. Right-click the request in the [Network request list](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/request_list/index.html) and select **Edit and resend**.
3. Change the `Accept-Encoding:` value from `gzip, deflate, br` to `none`.
4. Select **Send**.

Submit a request to the sample app with a browser by using the developer tools and observe that the response is compressed. The `Content-Encoding` and `Vary` headers are present on the response.

## Review providers

This section provides details about compression providers, including Brotli, Gzip, and custom providers.

This section provides details about compression providers, including Brotli, Gzip, Zstandard, and custom providers.

### Brotli and Gzip compression providers

Use the [BrotliCompressionProvider](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.brotlicompressionprovider) class to compress responses with the [RFC 7932: Brotli Compressed Data Format](https://datatracker.ietf.org/doc/html/rfc7932).

If [no compression providers are explicitly added](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCompression/src/ResponseCompressionProvider.cs#L44) to the [CompressionProviderCollection](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.compressionprovidercollection) class:

* By default, the Brotli and Gzip compression providers are added to the array of compression providers.
* When the client supports the Brotli compressed data format, compression defaults to Brotli compression.
* If the client doesn't support Brotli, compression defaults to Gzip when the client supports Gzip compression.

- By default, the Brotli, Gzip, and Zstandard compression providers are added to the array of compression providers.
- When the client supports the Zstandard compressed data format, compression defaults to Zstandard compression.
- If the client doesn't support Zstandard but supports Brotli, compression defaults to Brotli compression.
- If the client doesn't support Zstandard or Brotli, compression defaults to Gzip when the client supports Gzip compression.

When a compression provider is added, other providers aren't added. For example, if the Gzip compression provider is the only provider explicitly added, no other compression providers are added.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

The following code:

* Enables response compression for HTTPS requests.
* Adds the Brotli and Gzip response compression providers.

```
using System.IO.Compression;
using Microsoft.AspNetCore.ResponseCompression;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
});

builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.Fastest;
});

builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.SmallestSize;
});

var app = builder.Build();

app.UseResponseCompression();

app.MapGet("/", () => "Hello World!");

app.Run();
```

Set the compression level with the [BrotliCompressionProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.brotlicompressionprovideroptions) class and [GzipCompressionProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.gzipcompressionprovideroptions) class. The Brotli and Gzip compression providers default to the fastest compression level as determined by the [CompressionLevel.Fastest](/en-us/dotnet/api/system.io.compression.compressionlevel) enum. However, this approach might not produce the most efficient compression. If the most efficient compression is desired, configure the response compression middleware for optimal compression.

For values that indicate whether a compression operation emphasizes speed or compression size, see the [CompressionLevel Enum](/en-us/dotnet/api/system.io.compression.compressionlevel).

```
using System.IO.Compression;
using Microsoft.AspNetCore.ResponseCompression;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
});

builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.Fastest;
});

builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.SmallestSize;
});

var app = builder.Build();

app.UseResponseCompression();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Zstandard compression provider

Use the [ZstandardCompressionProvider](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.zstandardcompressionprovider) class to compress responses with the [RFC 8878: Zstandard Compression for HTTP](https://datatracker.ietf.org/doc/html/rfc8878).

Set the compression quality with the [ZstandardCompressionProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.zstandardcompressionprovideroptions) class. The Zstandard quality level ranges from 1 to 22, where higher values produce better compression but slower speeds. The following example sets the Zstandard compression quality:

```
builder.Services.Configure<ZstandardCompressionProviderOptions>(options =>
{
    options.CompressionOptions = new ZstandardCompressionOptions
    {
        Quality = 6 // 1 to 22, higher = better compression, slower
    };
});
```

### Custom providers

Create custom compression implementations with the [ICompressionProvider](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.icompressionprovider) interface. The [EncodingName](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.icompressionprovider.encodingname) property represents the content encoding that this `ICompressionProvider` produces. The response compression middleware uses this information to choose the provider based on the list specified in the `Accept-Encoding` header of the request.

Requests to the sample app with the `Accept-Encoding: mycustomcompression` header return a response with a `Content-Encoding: mycustomcompression` header. The client must be able to decompress the custom encoding in order for a custom compression implementation to work.

```
using Microsoft.AspNetCore.ResponseCompression;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
    options.Providers.Add<CustomCompressionProvider>();
});

var app = builder.Build();

app.UseResponseCompression();

app.MapGet("/", () => "Hello World!");

app.Run();
```

```
using Microsoft.AspNetCore.ResponseCompression;

public class CustomCompressionProvider : ICompressionProvider
{
    public string EncodingName => "mycustomcompression";
    public bool SupportsFlush => true;

    public Stream CreateStream(Stream outputStream)
    {
        // Replace with a custom compression stream wrapper.
        return outputStream;
    }
}
```

In the preceding code, the sample doesn't compress the response body. However, the sample shows where to implement a custom compression algorithm.

## Review the MIME types

The response compression middleware specifies a default set of MIME types for compression. Review the [source code for a complete list of supported MIME types](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCompression/src/ResponseCompressionDefaults.cs).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Replace or append MIME types with the [ResponseCompressionOptions.MimeTypes](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.responsecompressionoptions.mimetypes#microsoft-aspnetcore-responsecompression-responsecompressionoptions-mimetypes) property. Wildcard MIME types such as `text/*` aren't supported. The sample app adds a MIME type for `image/svg+xml` and compresses and serves the ASP.NET Core banner image *banner.svg*.

```
using Microsoft.AspNetCore.ResponseCompression;
using ResponseCompressionSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
    options.Providers.Add<CustomCompressionProvider>();
    options.MimeTypes =
    ResponseCompressionDefaults.MimeTypes.Concat(
        new[] { "image/svg+xml" });
});

var app = builder.Build();

app.UseResponseCompression();
```

## Add the Vary header

When responses are compressed based on the [Accept-Encoding request header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Accept-Encoding), there can be uncompressed and multiple compressed versions of the response. To instruct client and proxy caches that multiple versions exist and should be stored, the `Vary` header is added with an `Accept-Encoding` value. The response middleware [adds the 'Vary' header](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCompression/src/ResponseCompressionBody.cs#L198-L241) in the *ResponseCompressionBody.cs* file automatically when the response is compressed.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

The response compression middleware adds `Vary: Accept-Encoding` even to responses whose body it doesn't compress—for example, when the content type isn't configured for compression or the client didn't request a supported encoding. The broader behavior gives shared caches and CDNs a correct cache key so a stored compressed response isn't served to a client that didn't request that encoding. If the response already lists `Accept-Encoding` in its `Vary` header, the middleware doesn't add a duplicate. For details and mitigation, see [Response compression always emits Vary: Accept-Encoding](../breaking-changes/11/response-compression-always-vary?view=aspnetcore-10.0).

The `Vary: Accept-Encoding` header is only added when the response is compressed. This behavior changes for apps targeting .NET 11 or later. For more information, see [Response compression always emits Vary: Accept-Encoding](../breaking-changes/11/response-compression-always-vary?view=aspnetcore-10.0).

## Issues with Nginx reverse proxy

When Nginx proxies the request, the `Accept-Encoding` header is removed. Removal of the `Accept-Encoding` header prevents the response compression middleware from compressing the response. For more information, see [Nginx: Compression and decompression](https://docs.nginx.com/nginx/admin-guide/web-server/compression/). This issue is tracked in [GitHub dotnet/aspnetcore issue #5989](https://github.com/dotnet/aspnetcore/issues/5989) - *Figure out pass-through compression for Nginx*.

## Disable IIS dynamic compression

To disable IIS Dynamic Compression Module configured at the server level, see [Disabling IIS modules](../host-and-deploy/iis/modules?view=aspnetcore-10.0#disabling-iis-modules).

## Troubleshoot response compression

Use a tool like [Firefox Browser - Developer edition](https://www.firefox.com/channel/desktop/developer/) that lets you set the `Accept-Encoding` request header and study the response headers, size, and body. By default, response compression middleware compresses responses that meet the following conditions:

* The `Accept-Encoding` header is present with a value of `br`, `gzip`, `*` (asterisk), or custom encoding that matches a custom compression provider. The value must not be `identity` (no encoding) or have a quality value (qvalue, `q`) setting of 0 (zero).

- The `Accept-Encoding` header is present with a value of `br`, `gzip`, `zstd`, `*` (asterisk), or custom encoding that matches a custom compression provider. The value must not be `identity` (no encoding) or have a quality value (qvalue, `q`) setting of 0 (zero).

* The MIME type (`Content-Type`) must be set and must match a MIME type configured on the [ResponseCompressionOptions](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.responsecompressionoptions) class.

* The request must not include the [Content-Range header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Range).

* The request must use the insecure hypertext protocol (http), unless the secure hypertext protocol (https) is configured in the response compression middleware options.

  Important

  Review the risks associated with enabling secure content compression, as described in [Compression with HTTPS](#explore-compression-with-https) earlier in this article.

## Review the Azure deployed sample

The sample app deployed to Azure has the following *Program.cs* file:

```
using Microsoft.AspNetCore.ResponseCompression;
using ResponseCompressionSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
    options.Providers.Add<CustomCompressionProvider>();
    options.MimeTypes =
    ResponseCompressionDefaults.MimeTypes.Concat(
        new[] { "image/svg+xml" });
});

var app = builder.Build();

app.UseResponseCompression();

app.Map("/trickle", async (HttpResponse httpResponse) =>
{
    httpResponse.ContentType = "text/plain;charset=utf-8";

    for (int i = 0; i < 20; i++)
    {
        await httpResponse.WriteAsync("a");
        await httpResponse.Body.FlushAsync();
        await Task.Delay(TimeSpan.FromMilliseconds(50));
    }
});

app.Map("/testfile1kb.txt", () => Results.File(
    app.Environment.ContentRootFileProvider.GetFileInfo("testfile1kb.txt").PhysicalPath,
    "text/plain;charset=utf-8"));

app.Map("/banner.svg", () => Results.File(
    app.Environment.ContentRootFileProvider.GetFileInfo("banner.svg").PhysicalPath,
    "image/svg+xml;charset=utf-8"));

app.MapFallback(() => LoremIpsum.Text);

app.Run();
```

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/response-compression/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Response compression middleware source](https://github.com/dotnet/aspnetcore/tree/main/src/Middleware/ResponseCompression/src)
* [RFC 9110: HTTP Semantics (Section 8.4.1 Content Codings)](https://www.rfc-editor.org/rfc/rfc9110#name-content-codings)
* [RFC 9110: HTTP Semantics (Section 8.4.1.3 Gzip Coding)](https://www.rfc-editor.org/rfc/rfc9110#gzip.coding)
* [RFC 1952: GZIP file format specification version 4.3](https://www.ietf.org/rfc/rfc1952.txt)
* [RFC 8878: Zstandard Compression for HTTP](https://www.rfc-editor.org/rfc/rfc8878)

Network bandwidth is a limited resource. Reducing the size of the response usually increases the responsiveness of an app, often dramatically. One way to reduce payload sizes is to compress an app's responses.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/response-compression/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## When to use response compression middleware

Use server-based response compression technologies in IIS, Apache, or Nginx. The performance of the middleware probably won't match that of the server modules. [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) server and [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server don't currently offer built-in compression support.

Use response compression middleware when you're:

* Unable to use the following server-based compression technologies:

  * [IIS Dynamic Compression module](https://www.iis.net/overview/reliability/dynamiccachingandcompression)
  * [Apache mod\_deflate module](https://httpd.apache.org/docs/current/mod/mod_deflate.html)
  * [Nginx Compression and Decompression](https://www.nginx.com/resources/admin-guide/compression-and-decompression/)

* Hosting directly on:

  * [HTTP.sys server](../fundamentals/servers/httpsys?view=aspnetcore-10.0) (formerly called WebListener)
  * [Kestrel server](../fundamentals/servers/kestrel?view=aspnetcore-10.0)

## Response compression

Usually, any response not natively compressed can benefit from response compression. Responses not natively compressed typically include: CSS, JavaScript, HTML, XML, and JSON. You shouldn't compress natively compressed assets, such as PNG files. If you attempt to further compress a natively compressed response, any small additional reduction in size and transmission time will likely be overshadowed by the time it took to process the compression. Don't compress files smaller than about 150-1000 bytes (depending on the file's content and the efficiency of compression). The overhead of compressing small files may produce a compressed file larger than the uncompressed file.

When a client can process compressed content, the client must inform the server of its capabilities by sending the `Accept-Encoding` header with the request. When a server sends compressed content, it must include information in the `Content-Encoding` header on how the compressed response is encoded. Content encoding designations supported by the middleware are shown in the following table.

| `Accept-Encoding` header values | Middleware supported | Description                                                                                                      |
| ------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `br`                            | Yes (default)        | [Brotli compressed data format](https://tools.ietf.org/html/rfc7932)                                             |
| `deflate`                       | No                   | [DEFLATE compressed data format](https://tools.ietf.org/html/rfc1951)                                            |
| `exi`                           | No                   | [W3C Efficient XML Interchange](https://www.w3.org/TR/exi/)                                                      |
| `gzip`                          | Yes                  | [Gzip file format](https://tools.ietf.org/html/rfc1952)                                                          |
| `identity`                      | Yes                  | "No encoding" identifier: The response must not be encoded.                                                      |
| `pack200-gzip`                  | No                   | [Network Transfer Format for Java Archives](https://jcp.org/aboutJava/communityprocess/review/jsr200/index.html) |
| `*`                             | Yes                  | Any available content encoding not explicitly requested                                                          |

For more information, see the [IANA Official Content Coding List](https://www.iana.org/assignments/http-parameters/http-parameters.xml#http-content-coding-registry).

The middleware allows you to add additional compression providers for custom `Accept-Encoding` header values. For more information, see [Custom Providers](#custom-providers) below.

The middleware is capable of reacting to quality value (qvalue, `q`) weighting when sent by the client to prioritize compression schemes. For more information, see [RFC 9110: Accept-Encoding](https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding).

Compression algorithms are subject to a tradeoff between compression speed and the effectiveness of the compression. *Effectiveness* in this context refers to the size of the output after compression. The smallest size is achieved by the most *optimal* compression.

The headers involved in requesting, sending, caching, and receiving compressed content are described in the table below.

| Header             | Role                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Accept-Encoding`  | Sent from the client to the server to indicate the content encoding schemes acceptable to the client.                                                                                                                                                                                                                                                                                           |
| `Content-Encoding` | Sent from the server to the client to indicate the encoding of the content in the payload.                                                                                                                                                                                                                                                                                                      |
| `Content-Length`   | When compression occurs, the `Content-Length` header is removed, since the body content changes when the response is compressed.                                                                                                                                                                                                                                                                |
| `Content-MD5`      | When compression occurs, the `Content-MD5` header is removed, since the body content has changed and the hash is no longer valid.                                                                                                                                                                                                                                                               |
| `Content-Type`     | Specifies the MIME type of the content. Every response should specify its `Content-Type`. The middleware checks this value to determine if the response should be compressed. The middleware specifies a set of [default MIME types](#review-the-mime-types) that it can encode, but you can replace or add MIME types.                                                                         |
| `Vary`             | When sent by the server with a value of `Accept-Encoding` to clients and proxies, the `Vary` header indicates to the client or proxy that it should cache (vary) responses based on the value of the `Accept-Encoding` header of the request. The result of returning content with the `Vary: Accept-Encoding` header is that both compressed and uncompressed responses are cached separately. |

Explore the features of the response compression middleware with the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/response-compression/samples). The sample illustrates:

* The compression of app responses using Gzip and custom compression providers.
* How to add a MIME type to the default list of MIME types for compression.

## Configuration

The following code shows how to enable the response compression middleware for default MIME types and compression providers ([Brotli](#brotli-compression-provider) and [Gzip](#gzip-compression-provider)):

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddResponseCompression();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseResponseCompression();
    }
}
```

Notes:

* `app.UseResponseCompression` must be called before any middleware that compresses responses. For more information, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).
* Use a tool such as [Fiddler](https://www.telerik.com/fiddler), [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/) to set the `Accept-Encoding` request header and study the response headers, size, and body.

Submit a request to the sample app without the `Accept-Encoding` header and observe that the response is uncompressed. The `Content-Encoding` and `Vary` headers aren't present on the response.

Submit a request to the sample app with the `Accept-Encoding: br` header (Brotli compression) and observe that the response is compressed. The `Content-Encoding` and `Vary` headers are present on the response.

## Providers

### Brotli Compression Provider

Use the [BrotliCompressionProvider](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.brotlicompressionprovider) to compress responses with the [Brotli compressed data format](https://tools.ietf.org/html/rfc7932).

If no compression providers are explicitly added to the [CompressionProviderCollection](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.compressionprovidercollection):

* The Brotli Compression Provider is added by default to the array of compression providers along with the [Gzip compression provider](#gzip-compression-provider).
* Compression defaults to Brotli compression when the Brotli compressed data format is supported by the client. If Brotli isn't supported by the client, compression defaults to Gzip when the client supports Gzip compression.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression();
}
```

The Brotli Compression Provider must be added when any compression providers are explicitly added:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression(options =>
    {
        options.Providers.Add<BrotliCompressionProvider>();
        options.Providers.Add<GzipCompressionProvider>();
        options.Providers.Add<CustomCompressionProvider>();
        options.MimeTypes = 
            ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "image/svg+xml" });
    });
}
```

Set the compression level with [BrotliCompressionProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.brotlicompressionprovideroptions). The Brotli Compression Provider defaults to the fastest compression level ([CompressionLevel.Fastest](/en-us/dotnet/api/system.io.compression.compressionlevel)), which might not produce the most efficient compression. If the most efficient compression is desired, configure the middleware for optimal compression.

| Compression Level                                                                          | Description                                                                                                  |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| [CompressionLevel.Fastest](/en-us/dotnet/api/system.io.compression.compressionlevel)       | Compression should complete as quickly as possible, even if the resulting output isn't optimally compressed. |
| [CompressionLevel.NoCompression](/en-us/dotnet/api/system.io.compression.compressionlevel) | No compression should be performed.                                                                          |
| [CompressionLevel.Optimal](/en-us/dotnet/api/system.io.compression.compressionlevel)       | Responses should be optimally compressed, even if the compression takes more time to complete.               |

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression();

    services.Configure<BrotliCompressionProviderOptions>(options => 
    {
        options.Level = CompressionLevel.Fastest;
    });
}
```

### Gzip Compression Provider

Use the [GzipCompressionProvider](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.gzipcompressionprovider) to compress responses with the [Gzip file format](https://tools.ietf.org/html/rfc1952).

If no compression providers are explicitly added to the [CompressionProviderCollection](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.compressionprovidercollection):

* The Gzip Compression Provider is added by default to the array of compression providers along with the [Brotli Compression Provider](#brotli-compression-provider).
* Compression defaults to Brotli compression when the Brotli compressed data format is supported by the client. If Brotli isn't supported by the client, compression defaults to Gzip when the client supports Gzip compression.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression();
}
```

The Gzip Compression Provider must be added when any compression providers are explicitly added:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression(options =>
    {
        options.Providers.Add<BrotliCompressionProvider>();
        options.Providers.Add<GzipCompressionProvider>();
        options.Providers.Add<CustomCompressionProvider>();
        options.MimeTypes = 
            ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "image/svg+xml" });
    });
}
```

Set the compression level with [GzipCompressionProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.gzipcompressionprovideroptions). The Gzip Compression Provider defaults to the fastest compression level ([CompressionLevel.Fastest](/en-us/dotnet/api/system.io.compression.compressionlevel)), which might not produce the most efficient compression. If the most efficient compression is desired, configure the middleware for optimal compression.

| Compression Level                                                                          | Description                                                                                                  |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| [CompressionLevel.Fastest](/en-us/dotnet/api/system.io.compression.compressionlevel)       | Compression should complete as quickly as possible, even if the resulting output isn't optimally compressed. |
| [CompressionLevel.NoCompression](/en-us/dotnet/api/system.io.compression.compressionlevel) | No compression should be performed.                                                                          |
| [CompressionLevel.Optimal](/en-us/dotnet/api/system.io.compression.compressionlevel)       | Responses should be optimally compressed, even if the compression takes more time to complete.               |

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression();

    services.Configure<GzipCompressionProviderOptions>(options => 
    {
        options.Level = CompressionLevel.Fastest;
    });
}
```

### Custom providers

Create custom compression implementations with [ICompressionProvider](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.icompressionprovider). The [EncodingName](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.icompressionprovider.encodingname) represents the content encoding that this `ICompressionProvider` produces. The middleware uses this information to choose the provider based on the list specified in the `Accept-Encoding` header of the request.

Using the sample app, the client submits a request with the `Accept-Encoding: mycustomcompression` header. The middleware uses the custom compression implementation and returns the response with a `Content-Encoding: mycustomcompression` header. The client must be able to decompress the custom encoding in order for a custom compression implementation to work.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression(options =>
    {
        options.Providers.Add<BrotliCompressionProvider>();
        options.Providers.Add<GzipCompressionProvider>();
        options.Providers.Add<CustomCompressionProvider>();
        options.MimeTypes = 
            ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "image/svg+xml" });
    });
}
```

```
public class CustomCompressionProvider : ICompressionProvider
{
    public string EncodingName => "mycustomcompression";
    public bool SupportsFlush => true;

    public Stream CreateStream(Stream outputStream)
    {
        // Create a custom compression stream wrapper here
        return outputStream;
    }
}
```

Submit a request to the sample app with the `Accept-Encoding: mycustomcompression` header and observe the response headers. The `Vary` and `Content-Encoding` headers are present on the response. The response body (not shown) isn't compressed by the sample. There isn't a compression implementation in the `CustomCompressionProvider` class of the sample. However, the sample shows where you would implement such a compression algorithm.

## MIME types

The middleware specifies a default set of MIME types for compression:

* `application/javascript`
* `application/json`
* `application/xml`
* `text/css`
* `text/html`
* `text/json`
* `text/plain`
* `text/xml`

Replace or append MIME types with the response compression middleware options. Note that wildcard MIME types, such as `text/*` aren't supported. The sample app adds a MIME type for `image/svg+xml` and compresses and serves the ASP.NET Core banner image (*banner.svg*).

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression(options =>
    {
        options.Providers.Add<BrotliCompressionProvider>();
        options.Providers.Add<GzipCompressionProvider>();
        options.Providers.Add<CustomCompressionProvider>();
        options.MimeTypes = 
            ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "image/svg+xml" });
    });
}
```

[]()

## Compression with secure protocol

Compressed responses over secure connections can be controlled with the `EnableForHttps` option, which is disabled by default. Using compression with dynamically generated pages can lead to security problems such as the [CRIME](https://wikipedia.org/wiki/CRIME_\(security_exploit\)) and [BREACH](https://wikipedia.org/wiki/BREACH_\(security_exploit\)) attacks.

## Add the Vary header

When compressing responses based on the `Accept-Encoding` header, there are potentially multiple compressed versions of the response and an uncompressed version. In order to instruct client and proxy caches that multiple versions exist and should be stored, the `Vary` header is added with an `Accept-Encoding` value. In ASP.NET Core 2.0 or later, the middleware adds the `Vary` header automatically when the response is compressed.

## Middleware issue when behind an Nginx reverse proxy

When a request is proxied by Nginx, the `Accept-Encoding` header is removed. Removal of the `Accept-Encoding` header prevents the middleware from compressing the response. For more information, see [NGINX: Compression and Decompression](https://www.nginx.com/resources/admin-guide/compression-and-decompression/). This issue is tracked by [Figure out pass-through compression for Nginx (dotnet/aspnetcore#5989)](https://github.com/dotnet/aspnetcore/issues/5989).

## Working with IIS dynamic compression

If you have an active IIS Dynamic Compression Module configured at the server level that you would like to disable for an app, disable the module with an addition to the *web.config* file. For more information, see [Disabling IIS modules](../host-and-deploy/iis/modules?view=aspnetcore-10.0#disabling-iis-modules).

## Troubleshooting

Use a tool like [Fiddler](https://www.telerik.com/fiddler) or [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/), which allow you to set the `Accept-Encoding` request header and study the response headers, size, and body. By default, response compression middleware compresses responses that meet the following conditions:

* The `Accept-Encoding` header is present with a value of `br`, `gzip`, `*`, or custom encoding that matches a custom compression provider that you've established. The value must not be `identity` or have a quality value (qvalue, `q`) setting of 0 (zero).
* The MIME type (`Content-Type`) must be set and must match a MIME type configured on the [ResponseCompressionOptions](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.responsecompressionoptions).
* The request must not include the `Content-Range` header.
* The request must use insecure protocol (http), unless secure protocol (https) is configured in the response compression middleware options. *Note the danger [described above](#risk) when enabling secure content compression.*

## Related content

* [App startup in ASP.NET Core](../fundamentals/startup?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0)
* [Mozilla Developer Network: Accept-Encoding](https://developer.mozilla.org/docs/Web/HTTP/Headers/Accept-Encoding)
* [RFC 9110 Section 8.4.1: Content Codings](https://www.rfc-editor.org/rfc/rfc9110#name-content-codings)
* [RFC 9110 Section 8.4.1.3: Gzip Coding](https://www.rfc-editor.org/rfc/rfc9110#gzip.coding)
* [GZIP file format specification version 4.3](https://www.ietf.org/rfc/rfc1952.txt)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resource-based
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/resource-based.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Resource-based authorization in ASP.NET Core

This article describes how to authorize users for access to app resources.

In an app, a *resource* is typically represented by a C# class that includes data stored in a collection, such as a [`byte[]` array](/en-us/dotnet/api/system.byte). The class usually contains additional metadata pertaining to the resource, such as a unique resource identifier, dates, authors, source information, and a friendly name for display in a UI. The collection that holds resource data is usually loaded from physical file content, a cloud storage object, an in-memory object, or data from a database.

Resource-based authorization requires special attention in ASP.NET Core apps. Attribute evaluation occurs before data binding and before execution of any method that loads a resource. Declarative authorization with an `[Authorize]` attribute doesn't suffice for resource-based authorization. Instead, the app must invoke a custom authorization method—an approach known as *imperative authorization*.

This article uses Razor component examples and focuses on [Blazor](../../blazor/?view=aspnetcore-10.0) authorization scenarios for ASP.NET Core 3.1 or later. For Razor Pages and MVC guidance, which apply to all releases of ASP.NET Core, see the following resources:

* [Resource-based authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/resource-based?view=aspnetcore-10.0)
* [Resource-based authorization in ASP.NET Core MVC](../../mvc/security/authorization/resource-based?view=aspnetcore-10.0)

Examples in this article use *primary constructors*, available in C# 12 (.NET 8) or later. For more information, see [Declare primary constructors for classes and structs (C# documentation tutorial)](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) and [Primary constructors (C# Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors).

## Sample app

The Blazor Web App sample for this article is the [`BlazorWebAppAuthorization` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/BlazorWebAppAuthorization) ([how to download](../../overview?view=aspnetcore-10.0#how-to-download-a-sample)). The sample app uses seeded accounts with preconfigured document objects to demonstrate the examples in this article. For more information, see the sample's README file (`README.md`).

Caution

This sample app uses an in-memory database to store user information, which isn't suitable for production scenarios. The sample app is intended for demonstration purposes only and shouldn't be used as a starting point for production apps.

## Use imperative authorization

Authorization is implemented as an [IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice), which is registered in the service collection at app startup *by the ASP.NET Core framework*. The service is made available to Razor components and other classes via [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0):

```
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
```

[IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice) has two [AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) method overloads. One of the overloads accepts a resource and policy name:

```
Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource, 
    string policyName);
```

The other overload accepts a resource and collection of requirements ([IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement)) to evaluate:

```
Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource,
    IEnumerable<IAuthorizationRequirement> requirements);
```

In the following example, which is fully explained in the [Create a resource-based handler](#create-a-resource-based-handler) section, the secured resource is loaded into a custom `Document` object. An [AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) overload is invoked to determine whether the current user is allowed access to the document based on the "`SameAuthorPolicy`" authorization policy. If [`authorizationResult.Succeeded`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationresult.succeeded) is `true`, the user is authorized for the document because they authored the document (`Document.Author` matches the user's [Name](/en-us/dotnet/api/system.security.principal.iidentity.name)):

```
protected override async Task OnParametersSetAsync()
{
    var user = (await AuthStateProvider.GetAuthenticationStateAsync()).User;

    if (user.Identity is not null && user.Identity.IsAuthenticated)
    {
        var document = DocumentRepository.Find(DocumentId);

        ...

        var authorizationResult = await AuthorizationService
            .AuthorizeAsync(user, document, "SameAuthorPolicy");

        ...
    }
}
```

## Create a resource-based handler

Creating a resource-based authorization handler is similar to [creating a plain requirements handler](policies?view=aspnetcore-10.0#authorization-handlers). Create a custom requirement class and implement a requirement handler class. For more information on creating a requirement class, see [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0#requirements-and-policy-registration).

The following demonstration `Document` class is used:

```
namespace BlazorWebAppAuthorization.Models;

public class Document
{
    public string? Author { get; set; }

    public byte[]? Content { get; set; }

    public Guid ID { get; set; }

    public string? Title { get; set; }
}
```

The handler class specifies the requirement and resource type. The following example demonstrates a handler utilizing a `SameAuthorRequirement` requirement and a `Document` resource.

`Policies/Handlers/DocumentAuthorizationHandler.cs`:

```
using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Models;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class DocumentAuthorizationHandler :
    AuthorizationHandler<SameAuthorRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        SameAuthorRequirement requirement, 
        Document resource)
    {
        if (context.User.Identity?.Name == resource.Author)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

public class SameAuthorRequirement : IAuthorizationRequirement { }
```

Register the requirement and handler in `Program.cs`:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("SameAuthorPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));

builder.Services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
```

Register the requirement and handler in `Startup.ConfigureServices`:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("SameAuthorPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));
});

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
```

For more information on creating authorization policies, see [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0).

The following `AccessDocument` component calls an [AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) overload to determine whether the current user is allowed to view a document based on the "`SameAuthorPolicy`" authorization policy. If [`authorizationResult.Succeeded`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationresult.succeeded) is `true`, the user is authorized for the document because they authored the document (`Document.Author` matches the user's [Name](/en-us/dotnet/api/system.security.principal.iidentity.name)).

`Pages/AccessDocument.razor`:

```
@page "/access-document/{documentId}"
@using Microsoft.AspNetCore.Authorization
@using BlazorWebAppAuthorization.Data
@inject AuthenticationStateProvider AuthStateProvider
@inject IAuthorizationService AuthorizationService
@inject IDocumentRepository DocumentRepository

<h1>Access Document</h1>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
        <p>@message</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized to access this page.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    private string? message;

    [Parameter]
    public string? DocumentId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        var user = (await AuthStateProvider.GetAuthenticationStateAsync()).User;

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            var document = DocumentRepository.Find(DocumentId);

            if (document == null)
            {
                message = "Document not found.";
                return;
            }

            var authorizationResult = await AuthorizationService
                .AuthorizeAsync(user, document, "SameAuthorPolicy");

            message = authorizationResult.Succeeded
                ? $"You are authorized for document {DocumentId}."
                : $"You are NOT authorized for document {DocumentId}.";
        }
    }
}
```

In the [sample app](#sample-app), each user of the app is authorized access to the seeded document that they authored.

## Operational requirements

To make decisions based on the outcomes of CRUD (Create, Read, Update, Delete) operations, use the [OperationAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.operationauthorizationrequirement) helper class. The helper class enables you to write a single handler instead of an individual class for each operation type. The following `Operations` class establishes all four CRUD operation types:

```
using Microsoft.AspNetCore.Authorization.Infrastructure;

public static class Operations
{
    public static readonly OperationAuthorizationRequirement Create =
        new() { Name = nameof(Create) };
    public static readonly OperationAuthorizationRequirement Delete =
        new() { Name = nameof(Delete) };
    public static readonly OperationAuthorizationRequirement Read =
        new() { Name = nameof(Read) };
    public static readonly OperationAuthorizationRequirement Update =
        new() { Name = nameof(Update) };
}
```

The following `DocumentAuthorizationCrudHandler` authorization handler validates the operation using the resource, the user's identity (role) in some cases, and the requirement's `Name` property:

* All users can read documents.
* Only users in the `Admin` role can create and update documents.
* Only users in the `SuperUser` role can delete documents.

`Policies/Handlers/DocumentAuthorizationCrudHandler.cs`:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using BlazorWebAppAuthorization.Models;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class DocumentAuthorizationCrudHandler :
    AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OperationAuthorizationRequirement requirement, 
        Document resource)
    {
        if (requirement.Name == Operations.Create.Name &&
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Delete.Name &&
            context.User.IsInRole("SuperUser"))
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Read.Name)
        {
            context.Succeed(requirement);
        }

        if (requirement.Name == Operations.Update.Name &&
            context.User.IsInRole("Admin"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
```

Where services are registered in the app:

```
builder.Services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationCrudHandler>();
```

Call the overload of [AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) with the operation to return the authorization result.

For authorization to *create* a document:

```
var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Create);
```

For authorization to *read* a document:

```
var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Read);
```

For authorization to *delete* a document:

```
var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Delete);
```

For authorization to *update* a document:

```
var authorizationResult = await AuthorizationService
    .AuthorizeAsync(user, document, Operations.Update);
```

In the [sample app](#sample-app)'s `AccessDocumentCrud` page:

* Leela (`leela@contoso.com`), as an `Admin` and `SuperUser`, can perform full CRUD operations on resources.
* Harry (`harry@contoso.com`), as only an `Admin`, can create, read, and update resources.
* Sarah (`sarah@contoso.com`), as only a `SuperUser`, can delete and read resources.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/web-api/http-repl/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/web-api/http-repl/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Test web APIs with the HttpRepl

The HTTP Read-Eval-Print Loop (REPL) is:

* A lightweight, cross-platform command-line tool that's supported everywhere .NET is supported.
* Used for making HTTP requests to test ASP.NET Core web APIs (and non-ASP.NET Core web APIs) and view their results.
* Capable of testing web APIs hosted in any environment, including localhost and Azure App Service.

The following [HTTP verbs](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#74-supported-methods) are supported:

* [DELETE](#test-http-delete-requests)
* [GET](#test-http-get-requests)
* [HEAD](#test-http-head-requests)
* [OPTIONS](#test-http-options-requests)
* [PATCH](#test-http-patch-requests)
* [POST](#test-http-post-requests)
* [PUT](#test-http-put-requests)

To follow along, [view or download the sample ASP.NET Core web API](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/http-repl/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Prerequisites

* [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)

## Installation

To install the HttpRepl, run the following command:

```
dotnet tool install -g Microsoft.dotnet-httprepl
```

A [.NET Global Tool](/en-us/dotnet/core/tools/global-tools#install-a-global-tool) is installed from the [Microsoft.dotnet-httprepl](https://www.nuget.org/packages/Microsoft.dotnet-httprepl) NuGet package.

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

On macOS, update the path:

```
export PATH="$HOME/.dotnet/tools:$PATH"
```

## Usage

After successful installation of the tool, run the following command to start the HttpRepl:

```
httprepl
```

To view the available HttpRepl commands, run one of the following commands:

```
httprepl -h
```

```
httprepl --help
```

The following output is displayed:

```
Usage:
  httprepl [<BASE_ADDRESS>] [options]

Arguments:
  <BASE_ADDRESS> - The initial base address for the REPL.

Options:
  -h|--help - Show help information.

Once the REPL starts, these commands are valid:

Setup Commands:
Use these commands to configure the tool for your API server

connect        Configures the directory structure and base address of the api server
set header     Sets or clears a header for all requests. e.g. `set header content-type application/json`

HTTP Commands:
Use these commands to execute requests against your application.

GET            get - Issues a GET request
POST           post - Issues a POST request
PUT            put - Issues a PUT request
DELETE         delete - Issues a DELETE request
PATCH          patch - Issues a PATCH request
HEAD           head - Issues a HEAD request
OPTIONS        options - Issues a OPTIONS request

Navigation Commands:
The REPL allows you to navigate your URL space and focus on specific APIs that you are working on.

ls             Show all endpoints for the current path
cd             Append the given directory to the currently selected path, or move up a path when using `cd ..`

Shell Commands:
Use these commands to interact with the REPL shell.

clear          Removes all text from the shell
echo [on/off]  Turns request echoing on or off, show the request that was made when using request commands
exit           Exit the shell

REPL Customization Commands:
Use these commands to customize the REPL behavior.

pref [get/set] Allows viewing or changing preferences, e.g. 'pref set editor.command.default 'C:\\Program Files\\Microsoft VS Code\\Code.exe'`
run            Runs the script at the given path. A script is a set of commands that can be typed with one command per line
ui             Displays the Swagger UI page, if available, in the default browser

Use `help <COMMAND>` for more detail on an individual command. e.g. `help get`.
For detailed tool info, see https://aka.ms/http-repl-doc.
```

The HttpRepl offers command completion. Pressing the `Tab` key iterates through the list of commands that complete the characters or API endpoint that you typed. The following sections outline the available CLI commands.

## Connect to the web API

Connect to a web API by running the following command:

```
httprepl <ROOT URI>
```

`<ROOT URI>` is the base URI for the web API. For example:

```
httprepl https://localhost:5001
```

Alternatively, run the following command at any time while the HttpRepl is running:

```
connect <ROOT URI>
```

For example:

```
(Disconnected)> connect https://localhost:5001
```

### Manually point to the OpenAPI description for the web API

The connect command above will attempt to find the OpenAPI description automatically. If for some reason it's unable to do so, you can specify the URI of the OpenAPI description for the web API by using the `--openapi` option:

```
connect <ROOT URI> --openapi <OPENAPI DESCRIPTION ADDRESS>
```

For example:

```
(Disconnected)> connect https://localhost:5001 --openapi /swagger/v1/swagger.json
```

### Enable verbose output for details on OpenAPI description searching, parsing, and validation

Specifying the `--verbose` option with the `connect` command will produce more details when the tool searches for the OpenAPI description, parses, and validates it.

```
connect <ROOT URI> --verbose
```

For example:

```
(Disconnected)> connect https://localhost:5001 --verbose
Checking https://localhost:5001/swagger.json... 404 NotFound
Checking https://localhost:5001/swagger/v1/swagger.json... 404 NotFound
Checking https://localhost:5001/openapi.json... Found
Parsing... Successful (with warnings)
The field 'info' in 'document' object is REQUIRED [#/info]
The field 'paths' in 'document' object is REQUIRED [#/paths]
```

## Navigate the web API

### View available endpoints

To list the different endpoints (controllers) at the current path of the web API address, run the `ls` or `dir` command:

```
https://localhost:5001/> ls
```

The following output format is displayed:

```
.        []
Fruits   [get|post]
People   [get|post]

https://localhost:5001/>
```

The preceding output indicates that there are two controllers available: `Fruits` and `People`. Both controllers support parameterless HTTP GET and POST operations.

Navigating into a specific controller reveals more detail. For example, the following command's output shows the `Fruits` controller also supports HTTP GET, PUT, and DELETE operations. Each of these operations expects an `id` parameter in the route:

```
https://localhost:5001/fruits> ls
.      [get|post]
..     []
{id}   [get|put|delete]

https://localhost:5001/fruits>
```

Alternatively, run the `ui` command to open the web API's Swagger UI page in a browser. For example:

```
https://localhost:5001/> ui
```

### Navigate to an endpoint

To navigate to a different endpoint on the web API, run the `cd` command:

```
https://localhost:5001/> cd people
```

The path following the `cd` command is case insensitive. The following output format is displayed:

```
/people    [get|post]

https://localhost:5001/people>
```

## Customize the HttpRepl

The HttpRepl's default [colors](#set-color-preferences) can be customized. Additionally, a [default text editor](#set-the-default-text-editor) can be defined. The HttpRepl preferences are persisted across the current session and are honored in future sessions. Once modified, the preferences are stored in the following file:

* [Linux](#tabpanel_1_linux)
* [macOS](#tabpanel_1_macos)
* [Windows](#tabpanel_1_windows)

*%HOME%/.httpreplprefs*

*%HOME%/.httpreplprefs*

*%USERPROFILE%\\.httpreplprefs*

The *.httpreplprefs* file is loaded on startup and not monitored for changes at runtime. Manual modifications to the file take effect only after restarting the tool.

### View the settings

To view the available settings, run the `pref get` command. For example:

```
https://localhost:5001/> pref get
```

The preceding command displays the available key-value pairs:

```
colors.json=Green
colors.json.arrayBrace=BoldCyan
colors.json.comma=BoldYellow
colors.json.name=BoldMagenta
colors.json.nameSeparator=BoldWhite
colors.json.objectBrace=Cyan
colors.protocol=BoldGreen
colors.status=BoldYellow
```

### Set color preferences

Response colorization is currently supported for JSON only. To customize the default HttpRepl tool coloring, locate the key corresponding to the color to be changed. For instructions on how to find the keys, see the [View the settings](#view-the-settings) section. For example, change the `colors.json` key value from `Green` to `White` as follows:

```
https://localhost:5001/people> pref set colors.json White
```

Only the [allowed colors](https://github.com/dotnet/HttpRepl/blob/01d5c3c3373e98fe566ff5ef8a17c571de880293/src/Microsoft.Repl/ConsoleHandling/AllowedColors.cs) may be used. Subsequent HTTP requests display output with the new coloring.

When specific color keys aren't set, more generic keys are considered. To demonstrate this fallback behavior, consider the following example:

* If `colors.json.name` doesn't have a value, `colors.json.string` is used.
* If `colors.json.string` doesn't have a value, `colors.json.literal` is used.
* If `colors.json.literal` doesn't have a value, `colors.json` is used.
* If `colors.json` doesn't have a value, the command shell's default text color (`AllowedColors.None`) is used.

### Set indentation size

Response indentation size customization is currently supported for JSON only. The default size is two spaces. For example:

```
[
  {
    "id": 1,
    "name": "Apple"
  },
  {
    "id": 2,
    "name": "Orange"
  },
  {
    "id": 3,
    "name": "Strawberry"
  }
]
```

To change the default size, set the `formatting.json.indentSize` key. For example, to always use four spaces:

```
pref set formatting.json.indentSize 4
```

Subsequent responses honor the setting of four spaces:

```
[
    {
        "id": 1,
        "name": "Apple"
    },
    {
        "id": 2,
        "name": "Orange"
    },
    {
        "id": 3,
        "name": "Strawberry"
    }
]
```

### Set the default text editor

By default, the HttpRepl has no text editor configured for use. To test web API methods requiring an HTTP request body, a default text editor must be set. The HttpRepl tool launches the configured text editor for the sole purpose of composing the request body. Run the following command to set your preferred text editor as the default:

```
pref set editor.command.default "<EXECUTABLE>"
```

In the preceding command, `<EXECUTABLE>` is the full path to the text editor's executable file. For example, run the following command to set Visual Studio Code as the default text editor:

* [Linux](#tabpanel_2_linux)
* [macOS](#tabpanel_2_macos)
* [Windows](#tabpanel_2_windows)

```
pref set editor.command.default "/usr/bin/code"
```

```
pref set editor.command.default "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"
```

```
pref set editor.command.default "C:\Program Files\Microsoft VS Code\Code.exe"
```

To launch the default text editor with specific CLI arguments, set the `editor.command.default.arguments` key. For example, assume Visual Studio Code is the default text editor and that you always want the HttpRepl to open Visual Studio Code in a new session with extensions disabled. Run the following command:

```
pref set editor.command.default.arguments "--disable-extensions --new-window"
```

Tip

If your default editor is Visual Studio Code, you'll usually want to pass the `-w` or `--wait` argument to force Visual Studio Code to wait for you to close the file before returning.

### Set the OpenAPI Description search paths

By default, the HttpRepl has a set of relative paths that it uses to find the OpenAPI description when executing the `connect` command without the `--openapi` option. These relative paths are combined with the root and base paths specified in the `connect` command. The default relative paths are:

* `swagger.json`
* `swagger/v1/swagger.json`
* `/swagger.json`
* `/swagger/v1/swagger.json`
* `openapi.json`
* `/openapi.json`

To use a different set of search paths in your environment, set the `swagger.searchPaths` preference. The value must be a pipe-delimited list of relative paths. For example:

```
pref set swagger.searchPaths "swagger/v2/swagger.json|swagger/v3/swagger.json"
```

Instead of replacing the default list altogether, the list can also be modified by adding or removing paths.

To add one or more search paths to the default list, set the `swagger.addToSearchPaths` preference. The value must be a pipe-delimited list of relative paths. For example:

```
pref set swagger.addToSearchPaths "openapi/v2/openapi.json|openapi/v3/openapi.json"
```

To remove one or more search paths from the default list, set the `swagger.addToSearchPaths` preference. The value must be a pipe-delimited list of relative paths. For example:

```
pref set swagger.removeFromSearchPaths "swagger.json|/swagger.json"
```

## Test HTTP GET requests

### Synopsis

```
get <PARAMETER> [-F|--no-formatting] [-h|--header] [--response:body] [--response:headers] [-s|--streaming]
```

### Arguments

`PARAMETER`

The route parameter, if any, expected by the associated controller action method.

### Options

The following options are available for the `get` command:

* `-F|--no-formatting`

  A flag whose presence suppresses HTTP response formatting.

* `-h|--header`

  Sets an HTTP request header. The following two value formats are supported:

  * `{header}={value}`
  * `{header}:{value}`

* `--response:body`

  Specifies a file to which the HTTP response body should be written. For example, `--response:body "C:\response.json"`. The file is created if it doesn't exist.

* `--response:headers`

  Specifies a file to which the HTTP response headers should be written. For example, `--response:headers "C:\response.txt"`. The file is created if it doesn't exist.

* `-s|--streaming`

  A flag whose presence enables streaming of the HTTP response.

### Example

To issue an HTTP GET request:

1. Run the `get` command on an endpoint that supports it:

   ```
   https://localhost:5001/people> get
   ```

   The preceding command displays the following output format:

   ```
   HTTP/1.1 200 OK
   Content-Type: application/json; charset=utf-8
   Date: Fri, 21 Jun 2019 03:38:45 GMT
   Server: Kestrel
   Transfer-Encoding: chunked

   [
     {
       "id": 1,
       "name": "Scott Hunter"
     },
     {
       "id": 2,
       "name": "Scott Hanselman"
     },
     {
       "id": 3,
       "name": "Scott Guthrie"
     }
   ]


   https://localhost:5001/people>
   ```

2. Retrieve a specific record by passing a parameter to the `get` command:

   ```
   https://localhost:5001/people> get 2
   ```

   The preceding command displays the following output format:

   ```
   HTTP/1.1 200 OK
   Content-Type: application/json; charset=utf-8
   Date: Fri, 21 Jun 2019 06:17:57 GMT
   Server: Kestrel
   Transfer-Encoding: chunked

   [
     {
       "id": 2,
       "name": "Scott Hanselman"
     }
   ]


   https://localhost:5001/people>
   ```

## Test HTTP POST requests

### Synopsis

```
post <PARAMETER> [-c|--content] [-f|--file] [-h|--header] [--no-body] [-F|--no-formatting] [--response] [--response:body] [--response:headers] [-s|--streaming]
```

### Arguments

`PARAMETER`

The route parameter, if any, expected by the associated controller action method.

### Options

* `-F|--no-formatting`

  A flag whose presence suppresses HTTP response formatting.

* `-h|--header`

  Sets an HTTP request header. The following two value formats are supported:

  * `{header}={value}`
  * `{header}:{value}`

* `--response:body`

  Specifies a file to which the HTTP response body should be written. For example, `--response:body "C:\response.json"`. The file is created if it doesn't exist.

* `--response:headers`

  Specifies a file to which the HTTP response headers should be written. For example, `--response:headers "C:\response.txt"`. The file is created if it doesn't exist.

* `-s|--streaming`

  A flag whose presence enables streaming of the HTTP response.

- `-c|--content`

  Provides an inline HTTP request body. For example, `-c "{\"id\":2,\"name\":\"Cherry\"}"`.

- `-f|--file`

  Provides a path to a file containing the HTTP request body. For example, `-f "C:\request.json"`.

- `--no-body`

  Indicates that no HTTP request body is needed.

### Example

To issue an HTTP POST request:

1. Run the `post` command on an endpoint that supports it:

   ```
   https://localhost:5001/people> post -h Content-Type=application/json
   ```

   In the preceding command, the `Content-Type` HTTP request header is set to indicate a request body media type of JSON. The default text editor opens a *.tmp* file with a JSON template representing the HTTP request body. For example:

   ```
   {
     "id": 0,
     "name": ""
   }
   ```

   Tip

   To set the default text editor, see the [Set the default text editor](#set-the-default-text-editor) section.

2. Modify the JSON template to satisfy model validation requirements:

   ```
   {
     "id": 0,
     "name": "Scott Addie"
   }
   ```

3. Save the *.tmp* file, and close the text editor. The following output appears in the command shell:

   ```
   HTTP/1.1 201 Created
   Content-Type: application/json; charset=utf-8
   Date: Thu, 27 Jun 2019 21:24:18 GMT
   Location: https://localhost:5001/people/4
   Server: Kestrel
   Transfer-Encoding: chunked

   {
     "id": 4,
     "name": "Scott Addie"
   }


   https://localhost:5001/people>
   ```

## Test HTTP PUT requests

### Synopsis

```
put <PARAMETER> [-c|--content] [-f|--file] [-h|--header] [--no-body] [-F|--no-formatting] [--response] [--response:body] [--response:headers] [-s|--streaming]
```

### Arguments

`PARAMETER`

The route parameter, if any, expected by the associated controller action method.

### Options

* `-F|--no-formatting`

  A flag whose presence suppresses HTTP response formatting.

* `-h|--header`

  Sets an HTTP request header. The following two value formats are supported:

  * `{header}={value}`
  * `{header}:{value}`

* `--response:body`

  Specifies a file to which the HTTP response body should be written. For example, `--response:body "C:\response.json"`. The file is created if it doesn't exist.

* `--response:headers`

  Specifies a file to which the HTTP response headers should be written. For example, `--response:headers "C:\response.txt"`. The file is created if it doesn't exist.

* `-s|--streaming`

  A flag whose presence enables streaming of the HTTP response.

- `-c|--content`

  Provides an inline HTTP request body. For example, `-c "{\"id\":2,\"name\":\"Cherry\"}"`.

- `-f|--file`

  Provides a path to a file containing the HTTP request body. For example, `-f "C:\request.json"`.

- `--no-body`

  Indicates that no HTTP request body is needed.

### Example

To issue an HTTP PUT request:

1. *Optional*: Run the `get` command to view the data before modifying it:

   ```
   https://localhost:5001/fruits> get
   HTTP/1.1 200 OK
   Content-Type: application/json; charset=utf-8
   Date: Sat, 22 Jun 2019 00:07:32 GMT
   Server: Kestrel
   Transfer-Encoding: chunked

   [
     {
       "id": 1,
       "data": "Apple"
     },
     {
       "id": 2,
       "data": "Orange"
     },
     {
       "id": 3,
       "data": "Strawberry"
     }
   ]
   ```

2. Run the `put` command on an endpoint that supports it:

   ```
   https://localhost:5001/fruits> put 2 -h Content-Type=application/json
   ```

   In the preceding command, the `Content-Type` HTTP request header is set to indicate a request body media type of JSON. The default text editor opens a *.tmp* file with a JSON template representing the HTTP request body. For example:

   ```
   {
     "id": 0,
     "name": ""
   }
   ```

   Tip

   To set the default text editor, see the [Set the default text editor](#set-the-default-text-editor) section.

3. Modify the JSON template to satisfy model validation requirements:

   ```
   {
     "id": 2,
     "name": "Cherry"
   }
   ```

4. Save the *.tmp* file, and close the text editor. The following output appears in the command shell:

   ```
   [main 2019-06-28T17:27:01.805Z] update#setState idle
   HTTP/1.1 204 No Content
   Date: Fri, 28 Jun 2019 17:28:21 GMT
   Server: Kestrel
   ```

5. *Optional*: Issue a `get` command to see the modifications. For example, if you typed "Cherry" in the text editor, a `get` returns the following output:

   ```
   https://localhost:5001/fruits> get
   HTTP/1.1 200 OK
   Content-Type: application/json; charset=utf-8
   Date: Sat, 22 Jun 2019 00:08:20 GMT
   Server: Kestrel
   Transfer-Encoding: chunked

   [
     {
       "id": 1,
       "data": "Apple"
     },
     {
       "id": 2,
       "data": "Cherry"
     },
     {
       "id": 3,
       "data": "Strawberry"
     }
   ]


   https://localhost:5001/fruits>
   ```

## Test HTTP DELETE requests

### Synopsis

```
delete <PARAMETER> [-F|--no-formatting] [-h|--header] [--response] [--response:body] [--response:headers] [-s|--streaming]
```

### Arguments

`PARAMETER`

The route parameter, if any, expected by the associated controller action method.

### Options

* `-F|--no-formatting`

  A flag whose presence suppresses HTTP response formatting.

* `-h|--header`

  Sets an HTTP request header. The following two value formats are supported:

  * `{header}={value}`
  * `{header}:{value}`

* `--response:body`

  Specifies a file to which the HTTP response body should be written. For example, `--response:body "C:\response.json"`. The file is created if it doesn't exist.

* `--response:headers`

  Specifies a file to which the HTTP response headers should be written. For example, `--response:headers "C:\response.txt"`. The file is created if it doesn't exist.

* `-s|--streaming`

  A flag whose presence enables streaming of the HTTP response.

### Example

To issue an HTTP DELETE request:

1. *Optional*: Run the `get` command to view the data before modifying it:

   ```
   https://localhost:5001/fruits> get
   HTTP/1.1 200 OK
   Content-Type: application/json; charset=utf-8
   Date: Sat, 22 Jun 2019 00:07:32 GMT
   Server: Kestrel
   Transfer-Encoding: chunked

   [
     {
       "id": 1,
       "data": "Apple"
     },
     {
       "id": 2,
       "data": "Orange"
     },
     {
       "id": 3,
       "data": "Strawberry"
     }
   ]
   ```

2. Run the `delete` command on an endpoint that supports it:

   ```
   https://localhost:5001/fruits> delete 2
   ```

   The preceding command displays the following output format:

   ```
   HTTP/1.1 204 No Content
   Date: Fri, 28 Jun 2019 17:36:42 GMT
   Server: Kestrel
   ```

3. *Optional*: Issue a `get` command to see the modifications. In this example, a `get` returns the following output:

   ```
   https://localhost:5001/fruits> get
   HTTP/1.1 200 OK
   Content-Type: application/json; charset=utf-8
   Date: Sat, 22 Jun 2019 00:16:30 GMT
   Server: Kestrel
   Transfer-Encoding: chunked

   [
     {
       "id": 1,
       "data": "Apple"
     },
     {
       "id": 3,
       "data": "Strawberry"
     }
   ]


   https://localhost:5001/fruits>
   ```

## Test HTTP PATCH requests

### Synopsis

```
patch <PARAMETER> [-c|--content] [-f|--file] [-h|--header] [--no-body] [-F|--no-formatting] [--response] [--response:body] [--response:headers] [-s|--streaming]
```

### Arguments

`PARAMETER`

The route parameter, if any, expected by the associated controller action method.

### Options

* `-F|--no-formatting`

  A flag whose presence suppresses HTTP response formatting.

* `-h|--header`

  Sets an HTTP request header. The following two value formats are supported:

  * `{header}={value}`
  * `{header}:{value}`

* `--response:body`

  Specifies a file to which the HTTP response body should be written. For example, `--response:body "C:\response.json"`. The file is created if it doesn't exist.

* `--response:headers`

  Specifies a file to which the HTTP response headers should be written. For example, `--response:headers "C:\response.txt"`. The file is created if it doesn't exist.

* `-s|--streaming`

  A flag whose presence enables streaming of the HTTP response.

- `-c|--content`

  Provides an inline HTTP request body. For example, `-c "{\"id\":2,\"name\":\"Cherry\"}"`.

- `-f|--file`

  Provides a path to a file containing the HTTP request body. For example, `-f "C:\request.json"`.

- `--no-body`

  Indicates that no HTTP request body is needed.

## Test HTTP HEAD requests

### Synopsis

```
head <PARAMETER> [-F|--no-formatting] [-h|--header] [--response] [--response:body] [--response:headers] [-s|--streaming]
```

### Arguments

`PARAMETER`

The route parameter, if any, expected by the associated controller action method.

### Options

* `-F|--no-formatting`

  A flag whose presence suppresses HTTP response formatting.

* `-h|--header`

  Sets an HTTP request header. The following two value formats are supported:

  * `{header}={value}`
  * `{header}:{value}`

* `--response:body`

  Specifies a file to which the HTTP response body should be written. For example, `--response:body "C:\response.json"`. The file is created if it doesn't exist.

* `--response:headers`

  Specifies a file to which the HTTP response headers should be written. For example, `--response:headers "C:\response.txt"`. The file is created if it doesn't exist.

* `-s|--streaming`

  A flag whose presence enables streaming of the HTTP response.

## Test HTTP OPTIONS requests

### Synopsis

```
options <PARAMETER> [-F|--no-formatting] [-h|--header] [--response] [--response:body] [--response:headers] [-s|--streaming]
```

### Arguments

`PARAMETER`

The route parameter, if any, expected by the associated controller action method.

### Options

* `-F|--no-formatting`

  A flag whose presence suppresses HTTP response formatting.

* `-h|--header`

  Sets an HTTP request header. The following two value formats are supported:

  * `{header}={value}`
  * `{header}:{value}`

* `--response:body`

  Specifies a file to which the HTTP response body should be written. For example, `--response:body "C:\response.json"`. The file is created if it doesn't exist.

* `--response:headers`

  Specifies a file to which the HTTP response headers should be written. For example, `--response:headers "C:\response.txt"`. The file is created if it doesn't exist.

* `-s|--streaming`

  A flag whose presence enables streaming of the HTTP response.

## Set HTTP request headers

To set an HTTP request header, use one of the following approaches:

* Set inline with the HTTP request. For example:

  ```
  https://localhost:5001/people> post -h Content-Type=application/json
  ```

  With the preceding approach, each distinct HTTP request header requires its own `-h` option.

* Set before sending the HTTP request. For example:

  ```
  https://localhost:5001/people> set header Content-Type application/json
  ```

  When setting the header before sending a request, the header remains set for the duration of the command shell session. To clear the header, provide an empty value. For example:

  ```
  https://localhost:5001/people> set header Content-Type
  ```

## Test secured endpoints

The HttpRepl supports the testing of secured endpoints in the following ways:

* Via the default credentials of the logged in user.
* Through the use of HTTP request headers.

### Default credentials

Consider a web API you're testing that's hosted in IIS and secured with Windows authentication. You want the credentials of the user running the tool to flow across to the HTTP endpoints being tested. To pass the default credentials of the logged in user:

1. Set the `httpClient.useDefaultCredentials` preference to `true`:

   ```
   pref set httpClient.useDefaultCredentials true
   ```

2. Exit and restart the tool before sending another request to the web API.

### Default proxy credentials

Consider a scenario in which the web API you're testing is behind a proxy secured with Windows authentication. You want the credentials of the user running the tool to flow to the proxy. To pass the default credentials of the logged in user:

1. Set the `httpClient.proxy.useDefaultCredentials` preference to `true`:

   ```
   pref set httpClient.proxy.useDefaultCredentials true
   ```

2. Exit and restart the tool before sending another request to the web API.

### HTTP request headers

Examples of supported authentication and authorization schemes include:

* basic authentication
* JWT bearer tokens
* digest authentication

For example, you can send a bearer token to an endpoint with the following command:

```
set header Authorization "bearer <TOKEN VALUE>"
```

To access an Azure-hosted endpoint or to use the [Azure REST API](/en-us/rest/api/azure/), you need a bearer token. Use the following steps to obtain a bearer token for your Azure subscription via the [Azure CLI](/en-us/cli/azure/). The HttpRepl sets the bearer token in an HTTP request header. A list of Azure App Service Web Apps is retrieved.

1. Sign in to Azure:

   ```
   az login
   ```

2. Get your subscription ID with the following command:

   ```
   az account show --query id
   ```

3. Copy your subscription ID and run the following command:

   ```
   az account set --subscription "<SUBSCRIPTION ID>"
   ```

4. Get your bearer token with the following command:

   ```
   az account get-access-token --query accessToken
   ```

5. Connect to the Azure REST API via the HttpRepl:

   ```
   httprepl https://management.azure.com
   ```

6. Set the `Authorization` HTTP request header:

   ```
   https://management.azure.com/> set header Authorization "bearer <ACCESS TOKEN>"
   ```

7. Navigate to the subscription:

   ```
   https://management.azure.com/> cd subscriptions/<SUBSCRIPTION ID>
   ```

8. Get a list of your subscription's Azure App Service Web Apps:

   ```
   https://management.azure.com/subscriptions/{SUBSCRIPTION ID}> get providers/Microsoft.Web/sites?api-version=2016-08-01
   ```

   The following response is displayed:

   ```
   HTTP/1.1 200 OK
   Cache-Control: no-cache
   Content-Length: 35948
   Content-Type: application/json; charset=utf-8
   Date: Thu, 19 Sep 2019 23:04:03 GMT
   Expires: -1
   Pragma: no-cache
   Strict-Transport-Security: max-age=31536000; includeSubDomains
   X-Content-Type-Options: nosniff
   x-ms-correlation-request-id: <em>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</em>
   x-ms-original-request-ids: <em>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</em>
   x-ms-ratelimit-remaining-subscription-reads: 11999
   x-ms-request-id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
   x-ms-routing-request-id: WESTUS:xxxxxxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx
   {
     "value": [
       <AZURE RESOURCES LIST>
     ]
   }
   ```

## Toggle HTTP request display

By default, display of the HTTP request being sent is suppressed. It's possible to change the corresponding setting for the duration of the command shell session.

### Enable request display

View the HTTP request being sent by running the `echo on` command. For example:

```
https://localhost:5001/people> echo on
Request echoing is on
```

Subsequent HTTP requests in the current session display the request headers. For example:

```
https://localhost:5001/people> post

[main 2019-06-28T18:50:11.930Z] update#setState idle
Request to https://localhost:5001...

POST /people HTTP/1.1
Content-Length: 41
Content-Type: application/json
User-Agent: HTTP-REPL

{
  "id": 0,
  "name": "Scott Addie"
}

Response from https://localhost:5001...

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Fri, 28 Jun 2019 18:50:21 GMT
Location: https://localhost:5001/people/4
Server: Kestrel
Transfer-Encoding: chunked

{
  "id": 4,
  "name": "Scott Addie"
}


https://localhost:5001/people>
```

### Disable request display

Suppress display of the HTTP request being sent by running the `echo off` command. For example:

```
https://localhost:5001/people> echo off
Request echoing is off
```

## Run a script

If you frequently execute the same set of HttpRepl commands, consider storing them in a text file. Commands in the file take the same form as commands executed manually on the command line. The commands can be executed in a batched fashion using the `run` command. For example:

1. Create a text file containing a set of newline-delimited commands. To illustrate, consider a *people-script.txt* file containing the following commands:

   ```
   set base https://localhost:5001
   ls
   cd People
   ls
   get 1
   ```

2. Execute the `run` command, passing in the text file's path. For example:

   ```
   https://localhost:5001/> run C:\http-repl-scripts\people-script.txt
   ```

   The following output appears:

   ```
   https://localhost:5001/> set base https://localhost:5001
   Using OpenAPI description at https://localhost:5001/swagger/v1/swagger.json

   https://localhost:5001/> ls
   .        []
   Fruits   [get|post]
   People   [get|post]

   https://localhost:5001/> cd People
   /People    [get|post]

   https://localhost:5001/People> ls
   .      [get|post]
   ..     []
   {id}   [get|put|delete]

   https://localhost:5001/People> get 1
   HTTP/1.1 200 OK
   Content-Type: application/json; charset=utf-8
   Date: Fri, 12 Jul 2019 19:20:10 GMT
   Server: Kestrel
   Transfer-Encoding: chunked

   {
     "id": 1,
     "name": "Scott Hunter"
   }


   https://localhost:5001/People>
   ```

## Clear the output

To remove all output written to the command shell by the HttpRepl tool, run the `clear` or `cls` command. To illustrate, imagine the command shell contains the following output:

```
httprepl https://localhost:5001
(Disconnected)> set base "https://localhost:5001"
Using OpenAPI description at https://localhost:5001/swagger/v1/swagger.json

https://localhost:5001/> ls
.        []
Fruits   [get|post]
People   [get|post]

https://localhost:5001/>
```

Run the following command to clear the output:

```
https://localhost:5001/> clear
```

After running the preceding command, the command shell contains only the following output:

```
https://localhost:5001/>
```

## Related content

* [REST API requests](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#74-supported-methods)
* [HttpRepl GitHub repository](https://github.com/dotnet/HttpRepl)
* [Configure Visual Studio to launch HttpRepl](https://devblogs.microsoft.com/aspnet/httprepl-a-command-line-tool-for-interacting-with-restful-http-services/#configure-visual-studio-for-windows-to-launch-httprepl-on-f5)
* [Configure Visual Studio Code to launch HttpRepl](https://devblogs.microsoft.com/aspnet/httprepl-a-command-line-tool-for-interacting-with-restful-http-services/#configure-visual-studio-code-to-launch-httprepl-on-debug)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/new-field
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/new-field.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 7, add a new field to a Razor Page in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT)

In this section, [Entity Framework Core (EF Core)](/en-us/ef/core/get-started/aspnetcore/new-db) is used to define the database schema based on the app's model class:

* Add a new field to the model.
* Migrate the new field schema change to the database.

The EF Core approach allows for a more agile development process. You work on the app's data model directly while EF Core creates and synchronizes the database schema. You don't need to switch contexts to and from a database management tool. For an overview of Entity Framework Core and its benefits, see [Entity Framework Core](/en-us/ef/core).

Using EF Core to automatically create and track a database:

* Adds an [`__EFMigrationsHistory`](/en-us/ef/core/managing-schemas/migrations/history-table) table to the database to track whether the schema of the database is in sync with the model classes it was generated from.
* Throws an exception if the model classes aren't in sync with the database.

Automatic verification that the schema and model are in sync makes it easier to find inconsistent database code issues.

## Add a Rating property to the Movie model

1. Open the `Models/Movie.cs` file and add a `Rating` property:

   ```
   public class Movie
   {
       public int Id { get; set; }
       public string Title { get; set; } = string.Empty;

       [Display(Name = "Release Date")]
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string Genre { get; set; } = string.Empty;

       [Column(TypeName = "decimal(18, 2)")]
       public decimal Price { get; set; }
       public string Rating { get; set; } = string.Empty;
   }
   ```

2. Edit `Pages/MoviePages/Index.cshtml`, and add a `Rating` field:

   ```
   @page
   @model RazorPagesMovie.Pages.MoviePages.IndexModel

   @{
       ViewData["Title"] = "Index";
   }

   <h1>Index</h1>

   <p>
       <a asp-page="Create">Create New</a>
   </p>

   <form>
       <p>
           <select asp-for="MovieGenre" asp-items="Model.Genres">
               <option value="">All</option>
           </select>
           <label>Title: <input type="text" asp-for="SearchString" /></label>
           <input type="submit" value="Filter" />
       </p>
   </form>

   <table class="table">

       <thead>
           <tr>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Title)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Genre)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Price)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Rating)
               </th>
               <th></th>
           </tr>
       </thead>
       <tbody>
           @foreach (var item in Model.Movie)
           {
               <tr>
                   <td>
                       @Html.DisplayFor(modelItem => item.Title)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.ReleaseDate)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Genre)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Price)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Rating)
                   </td>
                   <td>
                       <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                       <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                       <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
                   </td>
               </tr>
           }
       </tbody>
   </table>
   ```

3. Update the following pages with a `Rating` field:

   * *[Pages/MoviePages/Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie10/Pages/MoviePages/Create.cshtml)*.
   * *[Pages/MoviePages/Delete.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie10/Pages/MoviePages/Delete.cshtml)*.
   * *[Pages/MoviePages/Details.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie10/Pages/MoviePages/Details.cshtml)*.
   * *[Pages/MoviePages/Edit.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie10/Pages/MoviePages/Edit.cshtml)*.

The app doesn't work until you update the database to include the new field. Running the app without an update to the database throws a `SqlException`:

`SqlException: Invalid column name 'Rating'.`

The `SqlException` exception occurs because the updated Movie model class doesn't match the schema of the Movie table in the database. The database table doesn't have a `Rating` column.

You can resolve the error by using one of the following approaches:

1. Have Entity Framework automatically drop and re-create the database using the new model class schema. This approach is convenient early in the development cycle because it allows developers to quickly evolve the model and database schema together. The downside is that existing data in the database is lost. Don't use this approach on a production database! Dropping the database on schema changes and using an initializer to automatically seed the database with test data is often a productive way to develop an app.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is to keep the data. Make this change either manually or by creating a database change script.
3. Use EF Core Migrations to update the database schema.

For this tutorial, use EF Core Migrations.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown in the following code, but make this change for each `new Movie` block.

```
context.Movie.AddRange(
    new Movie
    {
        Title = "When Harry Met Sally",
        ReleaseDate = DateTime.Parse("1989-2-12"),
        Genre = "Romantic Comedy",
        Price = 7.99M,
        Rating = "R"
    },
```

See the [completed SeedData.cs file](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie10/Models/SeedData.cs).

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Press `Ctrl`+`Shift`+`B`.

From the *View* menu, select *Terminal* and enter the following command:

```
dotnet build
```

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

## Add a migration for the rating field

1. From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

2. In the Package Manager Console (PMC), enter the following command:

   ```
   Add-Migration Rating
   ```

The `Add-Migration` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

1. In the PMC, enter the following command:

   ```
   Update-Database
   ```

The `Update-Database` command tells the framework to apply the schema changes to the database and to preserve existing data.

To delete all the records in the database, the initializer seeds the database and includes the `Rating` field. You can delete records by using the delete links in the browser or from [Sql Server Object Explorer](sql?view=aspnetcore-10.0#ssox) (SSOX).

Another option is to delete the database and use migrations to re-create the database. To delete the database in SSOX:

1. Select the database in SSOX.

2. Right-click on the database, and select **Delete**.

3. Check **Close existing connections**.

4. Select **OK**.

5. In the [PMC](new-field?view=aspnetcore-10.0#pmc), update the database:

   ```
   Update-Database
   ```

## Add a migration for rating

Use the following commands to add a migration for the rating field:

```
dotnet ef migrations add rating
dotnet ef database update
```

The `dotnet ef migrations add rating` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name `rating` is arbitrary and is used to name the migration file. Use a meaningful name for the migration file.

The `dotnet ef database update` command applies the schema changes to the database and preserves existing data.

If you delete all the records in the database, the initializer seeds the database and includes the `Rating` field.

### Optional: Drop and re-create the database for other providers

Skip this section if you successfully migrated the database.

In this tutorial, use Entity Framework Core *migrations* features when possible. Migrations update the database schema to match changes in the data model. However, migrations can only make changes that the EF Core provider supports, and some providers have limited capabilities. For example, adding a column might be supported, but removing or changing a column might not be. If you create a migration to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, you can drop and re-create the database.

To work around these limitations, manually write migrations code to perform a table rebuild when something in the table changes. A table rebuild involves:

* Creating a new table.
* Copying data from the old table to the new table.
* Dropping the old table.
* Renaming the new table.

For more information, see the following resources:

* [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations)
* [Customize migration code](/en-us/ef/core/managing-schemas/migrations/#customize-migration-code)
* [Data seeding](/en-us/ef/core/modeling/data-seeding)
* [SQLite ALTER TABLE statement](https://sqlite.org/lang_altertable.html)

1. Delete the migration folder.

2. Use the following commands to recreate the database.

   ```
   dotnet ef database drop
   dotnet ef migrations add InitialCreate
   dotnet ef database update
   ```

Run the app and verify you can create, edit, and display movies with a `Rating` field. If the database isn't seeded, set a break point in the `SeedData.Initialize` method.

## Next steps

[Previous: Add Search](search?view=aspnetcore-10.0) [Next: Add Validation](validation?view=aspnetcore-10.0)

In this section [Entity Framework Core (EF Core)](/en-us/ef/core/get-started/aspnetcore/new-db) is used to define the database schema based on the app's model class:

* Add a new field to the model.
* Migrate the new field schema change to the database.

The EF Core approach allows for a more agile development process. The developer works on the app's data model directly while the database schema is created and then synchronized, all without the developer having to switch contexts to and from a database management tool. For an overview of Entity Framework Core and its benefits, see [Entity Framework Core](/en-us/ef/core).

Using EF Code to automatically create and track a database:

* Adds an [`__EFMigrationsHistory`](/en-us/ef/core/managing-schemas/migrations/history-table) table to the database to track whether the schema of the database is in sync with the model classes it was generated from.
* Throws an exception if the model classes aren't in sync with the database.

Automatic verification that the schema and model are in sync makes it easier to find inconsistent database code issues.

## Add a `Rating` property to the `Movie` model

1. Open the `Models/Movie.cs` file and add a `Rating` property:

   ```
   public class Movie
   {
       public int Id { get; set; }
       public string Title { get; set; } = string.Empty;

       [Display(Name = "Release Date")]
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string Genre { get; set; } = string.Empty;

       [Column(TypeName = "decimal(18, 2)")]
       public decimal Price { get; set; }
       public string Rating { get; set; } = string.Empty;
   }
   ```

2. Edit `Pages/Movies/Index.cshtml`, and add a `Rating` field:[]()

   ```
   @page
   @model RazorPagesMovie.Pages.Movies.IndexModel

   @{
       ViewData["Title"] = "Index";
   }

   <h1>Index</h1>

   <p>
       <a asp-page="Create">Create New</a>
   </p>

   <form>
       <p>
           <select asp-for="MovieGenre" asp-items="Model.Genres">
               <option value="">All</option>
           </select>
           <label>Title: <input type="text" asp-for="SearchString" /></label>
           <input type="submit" value="Filter" />
       </p>
   </form>

   <table class="table">

       <thead>
           <tr>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Title)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Genre)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Price)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Rating)
               </th>
               <th></th>
           </tr>
       </thead>
       <tbody>
           @foreach (var item in Model.Movie)
           {
               <tr>
                   <td>
                       @Html.DisplayFor(modelItem => item.Title)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.ReleaseDate)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Genre)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Price)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Rating)
                   </td>
                   <td>
                       <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                       <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                       <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
                   </td>
               </tr>
           }
       </tbody>
   </table>
   ```

3. Update the following pages with a `Rating` field:

   * *[Pages/Movies/Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie90/Pages/Movies/Create.cshtml)*.
   * *[Pages/Movies/Delete.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie90/Pages/Movies/Delete.cshtml)*.
   * *[Pages/Movies/Details.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie90/Pages/Movies/Details.cshtml)*.
   * *[Pages/Movies/Edit.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie90/Pages/Movies/Edit.cshtml)*.

The app won't work until the database is updated to include the new field. Running the app without an update to the database throws a `SqlException`:

`SqlException: Invalid column name 'Rating'.`

The `SqlException` exception is caused by the updated Movie model class being different than the schema of the Movie table of the database. There's no `Rating` column in the database table.

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database using the new model class schema. This approach is convenient early in the development cycle, it allows developers to quickly evolve the model and database schema together. The downside is that existing data in the database is lost. Don't use this approach on a production database! Dropping the database on schema changes and using an initializer to automatically seed the database with test data is often a productive way to develop an app.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is to keep the data. Make this change either manually or by creating a database change script.
3. Use EF Core Migrations to update the database schema.

For this tutorial, use EF Core Migrations.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but make this change for each `new Movie` block.

```
context.Movie.AddRange(
    new Movie
    {
        Title = "When Harry Met Sally",
        ReleaseDate = DateTime.Parse("1989-2-12"),
        Genre = "Romantic Comedy",
        Price = 7.99M,
        Rating = "R"
    },
```

See the [completed SeedData.cs file](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie90/Models/SeedData.cs).

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Press `Ctrl`+`Shift`+`B`

From the *View* menu, select *Terminal* and enter the following command:

```
dotnet build
```

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

[]()

### Add a migration for the rating field

1. From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

2. In the Package Manager Console (PMC), enter the following command:

   ```
   Add-Migration Rating
   ```

The `Add-Migration` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

2. In the PMC, enter the following command:

   ```
   Update-Database
   ```

The `Update-Database` command tells the framework to apply the schema changes to the database and to preserve existing data.

[]()

Delete all the records in the database, the initializer will seed the database and include the `Rating` field. Deleting can be done with the delete links in the browser or from [Sql Server Object Explorer](sql?view=aspnetcore-10.0#ssox) (SSOX).

Another option is to delete the database and use migrations to re-create the database. To delete the database in SSOX:

1. Select the database in SSOX.

2. Right-click on the database, and select **Delete**.

3. Check **Close existing connections**.

4. Select **OK**.

5. In the [PMC](new-field?view=aspnetcore-10.0#pmc), update the database:

   ```
   Update-Database
   ```

### Add a migration for rating

Use the following commands to add a migration for the rating field:

```
dotnet ef migrations add rating
dotnet ef database update
```

The `dotnet ef migrations add rating` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name `rating` is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `dotnet ef database update` command tells the framework to apply the schema changes to the database and to preserve existing data.

Delete all the records in the database, the initializer will seed the database and include the `Rating` field.

### Optional: Drop and re-create the database for other providers

Skip this section if you successfully migrated the database.

In this tutorial, Entity Framework Core *migrations* features are used when possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and some provider's capabilities are limited. For example, adding a column may be supported, but removing or changing a column is not. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, you can drop and re-create the database.

The workaround for the limitations is to manually write migrations code to perform a table rebuild when something in the table changes. A table rebuild involves:

* Creating a new table.
* Copying data from the old table to the new table.
* Dropping the old table.
* Renaming the new table.

For more information, see the following resources:

* [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations)
* [Customize migration code](/en-us/ef/core/managing-schemas/migrations/#customize-migration-code)
* [Data seeding](/en-us/ef/core/modeling/data-seeding)
* [SQLite ALTER TABLE statement](https://sqlite.org/lang_altertable.html)

1. Delete the migration folder.

2. Use the following commands to recreate the database.

   ```
   dotnet ef database drop
   dotnet ef migrations add InitialCreate
   dotnet ef database update
   ```

Run the app and verify you can create, edit, and display movies with a `Rating` field. If the database isn't seeded, set a break point in the `SeedData.Initialize` method.

## Next steps

[Previous: Add Search](search?view=aspnetcore-10.0) [Next: Add Validation](validation?view=aspnetcore-10.0)

In this section [Entity Framework](/en-us/ef/core/get-started/aspnetcore/new-db) Code First Migrations is used to:

* Add a new field to the model.
* Migrate the new field schema change to the database.

When using EF Code First to automatically create and track a database, Code First:

* Adds an [`__EFMigrationsHistory`](/en-us/ef/core/managing-schemas/migrations/history-table) table to the database to track whether the schema of the database is in sync with the model classes it was generated from.
* Throws an exception if the model classes aren't in sync with the database.

Automatic verification that the schema and model are in sync makes it easier to find inconsistent database code issues.

## Add a `Rating` property to the `Movie` model

1. Open the `Models/Movie.cs` file and add a `Rating` property:

   ```
   public class Movie
   {
       public int Id { get; set; }
       public string Title { get; set; } = string.Empty;

       [Display(Name = "Release Date")]
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string Genre { get; set; } = string.Empty;

       [Column(TypeName = "decimal(18, 2)")]
       public decimal Price { get; set; }
       public string Rating { get; set; } = string.Empty;
   }
   ```

2. Edit `Pages/Movies/Index.cshtml`, and add a `Rating` field:[]()

   ```
   @page
   @model RazorPagesMovie.Pages.Movies.IndexModel

   @{
       ViewData["Title"] = "Index";
   }

   <h1>Index</h1>

   <p>
       <a asp-page="Create">Create New</a>
   </p>

   <form>
       <p>
           <select asp-for="MovieGenre" asp-items="Model.Genres">
               <option value="">All</option>
           </select>
           <label>Title: <input type="text" asp-for="SearchString" /></label>
           <input type="submit" value="Filter" />
       </p>
   </form>

   <table class="table">

       <thead>
           <tr>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Title)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Genre)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Price)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Rating)
               </th>
               <th></th>
           </tr>
       </thead>
       <tbody>
           @foreach (var item in Model.Movie)
           {
               <tr>
                   <td>
                       @Html.DisplayFor(modelItem => item.Title)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.ReleaseDate)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Genre)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Price)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Rating)
                   </td>
                   <td>
                       <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                       <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                       <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
                   </td>
               </tr>
           }
       </tbody>
   </table>
   ```

3. Update the following pages with a `Rating` field:

   * *[Pages/Movies/Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie80/Pages/Movies/Create.cshtml)*.
   * *[Pages/Movies/Delete.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie80/Pages/Movies/Delete.cshtml)*.
   * *[Pages/Movies/Details.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie80/Pages/Movies/Details.cshtml)*.
   * *[Pages/Movies/Edit.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie80/Pages/Movies/Edit.cshtml)*.

The app won't work until the database is updated to include the new field. Running the app without an update to the database throws a `SqlException`:

`SqlException: Invalid column name 'Rating'.`

The `SqlException` exception is caused by the updated Movie model class being different than the schema of the Movie table of the database. There's no `Rating` column in the database table.

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database using the new model class schema. This approach is convenient early in the development cycle, it allows developers to quickly evolve the model and database schema together. The downside is that existing data in the database is lost. Don't use this approach on a production database! Dropping the database on schema changes and using an initializer to automatically seed the database with test data is often a productive way to develop an app.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is to keep the data. Make this change either manually or by creating a database change script.
3. Use Code First Migrations to update the database schema.

For this tutorial, use Code First Migrations.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but make this change for each `new Movie` block.

```
context.Movie.AddRange(
    new Movie
    {
        Title = "When Harry Met Sally",
        ReleaseDate = DateTime.Parse("1989-2-12"),
        Genre = "Romantic Comedy",
        Price = 7.99M,
        Rating = "R"
    },
```

See the [completed SeedData.cs file](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie80/Models/SeedDataRating.cs).

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Press `Ctrl`+`Shift`+`B`

From the *View* menu, select *Terminal* and enter the following command:

```
dotnet build
```

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

[]()

### Add a migration for the rating field

1. From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

2. In the PMC, enter the following commands:

   ```
   Add-Migration Rating
   Update-Database
   ```

The `Add-Migration` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `Update-Database` command tells the framework to apply the schema changes to the database and to preserve existing data.

[]()

Delete all the records in the database, the initializer will seed the database and include the `Rating` field. Deleting can be done with the delete links in the browser or from [Sql Server Object Explorer](sql?view=aspnetcore-10.0#ssox) (SSOX).

Another option is to delete the database and use migrations to re-create the database. To delete the database in SSOX:

1. Select the database in SSOX.

2. Right-click on the database, and select **Delete**.

3. Check **Close existing connections**.

4. Select **OK**.

5. In the [PMC](new-field?view=aspnetcore-10.0#pmc), update the database:

   ```
   Update-Database
   ```

### Add a migration for rating

Use the following commands to add a migration for the rating field:

```
dotnet ef migrations add rating
dotnet ef database update
```

The `dotnet ef migrations add rating` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name `rating` is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `dotnet ef database update` command tells the framework to apply the schema changes to the database and to preserve existing data.

Delete all the records in the database, the initializer will seed the database and include the `Rating` field.

### Optional: Drop and re-create the database for other providers

Skip this section if you successfully migrated the database.

In this tutorial, Entity Framework Core *migrations* features are used when possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and some provider's capabilities are limited. For example, adding a column may be supported, but removing or changing a column is not. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, you can drop and re-create the database.

The workaround for the limitations is to manually write migrations code to perform a table rebuild when something in the table changes. A table rebuild involves:

* Creating a new table.
* Copying data from the old table to the new table.
* Dropping the old table.
* Renaming the new table.

For more information, see the following resources:

* [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations)
* [Customize migration code](/en-us/ef/core/managing-schemas/migrations/#customize-migration-code)
* [Data seeding](/en-us/ef/core/modeling/data-seeding)
* [SQLite ALTER TABLE statement](https://sqlite.org/lang_altertable.html)

1. Delete the migration folder.

2. Use the following commands to recreate the database.

   ```
   dotnet ef database drop
   dotnet ef migrations add InitialCreate
   dotnet ef database update
   ```

Run the app and verify you can create, edit, and display movies with a `Rating` field. If the database isn't seeded, set a break point in the `SeedData.Initialize` method.

## Next steps

[Previous: Add Search](search?view=aspnetcore-10.0) [Next: Add Validation](validation?view=aspnetcore-10.0)

In this section [Entity Framework](/en-us/ef/core/get-started/aspnetcore/new-db) Code First Migrations is used to:

* Add a new field to the model.
* Migrate the new field schema change to the database.

When using EF Code First to automatically create and track a database, Code First:

* Adds an [`__EFMigrationsHistory`](/en-us/ef/core/managing-schemas/migrations/history-table) table to the database to track whether the schema of the database is in sync with the model classes it was generated from.
* Throws an exception if the model classes aren't in sync with the database.

Automatic verification that the schema and model are in sync makes it easier to find inconsistent database code issues.

## Add a `Rating` property to the `Movie` model

1. Open the `Models/Movie.cs` file and add a `Rating` property:

   ```
   public class Movie
   {
       public int Id { get; set; }
       public string Title { get; set; } = string.Empty;

       [Display(Name = "Release Date")]
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string Genre { get; set; } = string.Empty;

       [Column(TypeName = "decimal(18, 2)")]
       public decimal Price { get; set; }
       public string Rating { get; set; } = string.Empty;
   }
   ```

2. Edit `Pages/Movies/Index.cshtml`, and add a `Rating` field:[]()

   ```
   @page
   @model RazorPagesMovie.Pages.Movies.IndexModel

   @{
       ViewData["Title"] = "Index";
   }

   <h1>Index</h1>

   <p>
       <a asp-page="Create">Create New</a>
   </p>

   <form>
       <p>
           <select asp-for="MovieGenre" asp-items="Model.Genres">
               <option value="">All</option>
           </select>
           <label>Title: <input type="text" asp-for="SearchString" /></label>
           <input type="submit" value="Filter" />
       </p>
   </form>

   <table class="table">

       <thead>
           <tr>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Title)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Genre)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Price)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Rating)
               </th>
               <th></th>
           </tr>
       </thead>
       <tbody>
           @foreach (var item in Model.Movie)
           {
               <tr>
                   <td>
                       @Html.DisplayFor(modelItem => item.Title)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.ReleaseDate)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Genre)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Price)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Rating)
                   </td>
                   <td>
                       <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                       <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                       <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
                   </td>
               </tr>
           }
       </tbody>
   </table>
   ```

3. Update the following pages with a `Rating` field:

   * *[Pages/Movies/Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie70/Pages/Movies/Create.cshtml)*.
   * *[Pages/Movies/Delete.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie70/Pages/Movies/Delete.cshtml)*.
   * *[Pages/Movies/Details.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie70/Pages/Movies/Details.cshtml)*.
   * *[Pages/Movies/Edit.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie70/Pages/Movies/Edit.cshtml)*.

The app won't work until the database is updated to include the new field. Running the app without an update to the database throws a `SqlException`:

`SqlException: Invalid column name 'Rating'.`

The `SqlException` exception is caused by the updated Movie model class being different than the schema of the Movie table of the database. There's no `Rating` column in the database table.

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database using the new model class schema. This approach is convenient early in the development cycle, it allows developers to quickly evolve the model and database schema together. The downside is that existing data in the database is lost. Don't use this approach on a production database! Dropping the database on schema changes and using an initializer to automatically seed the database with test data is often a productive way to develop an app.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is to keep the data. Make this change either manually or by creating a database change script.
3. Use Code First Migrations to update the database schema.

For this tutorial, use Code First Migrations.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but make this change for each `new Movie` block.

```
context.Movie.AddRange(
    new Movie
    {
        Title = "When Harry Met Sally",
        ReleaseDate = DateTime.Parse("1989-2-12"),
        Genre = "Romantic Comedy",
        Price = 7.99M,
        Rating = "R"
    },
```

See the [completed SeedData.cs file](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie70/Models/SeedDataRating.cs).

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Press `Ctrl`+`Shift`+`B`

From the *View* menu, select *Terminal* and enter the following command:

```
dotnet build
```

Press `⌘`+`B`

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

[]()

### Add a migration for the rating field

1. From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

2. In the PMC, enter the following commands:

   ```
   Add-Migration Rating
   Update-Database
   ```

The `Add-Migration` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `Update-Database` command tells the framework to apply the schema changes to the database and to preserve existing data.

[]()

Delete all the records in the database, the initializer will seed the database and include the `Rating` field. Deleting can be done with the delete links in the browser or from [Sql Server Object Explorer](sql?view=aspnetcore-10.0#ssox) (SSOX).

Another option is to delete the database and use migrations to re-create the database. To delete the database in SSOX:

1. Select the database in SSOX.

2. Right-click on the database, and select **Delete**.

3. Check **Close existing connections**.

4. Select **OK**.

5. In the [PMC](new-field?view=aspnetcore-10.0#pmc), update the database:

   ```
   Update-Database
   ```

### Add a migration for rating

Use the following commands to add a migration for the rating field:

```
dotnet ef migrations add rating
dotnet ef database update
```

The `dotnet ef migrations add rating` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name `rating` is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `dotnet ef database update` command tells the framework to apply the schema changes to the database and to preserve existing data.

Delete all the records in the database, the initializer will seed the database and include the `Rating` field.

### Optional: Drop and re-create the database for other providers

Skip this section if you successfully migrated the database.

In this tutorial, Entity Framework Core *migrations* features are used when possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and some provider's capabilities are limited. For example, adding a column may be supported, but removing or changing a column is not. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, you can drop and re-create the database.

The workaround for the limitations is to manually write migrations code to perform a table rebuild when something in the table changes. A table rebuild involves:

* Creating a new table.
* Copying data from the old table to the new table.
* Dropping the old table.
* Renaming the new table.

For more information, see the following resources:

* [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations)
* [Customize migration code](/en-us/ef/core/managing-schemas/migrations/#customize-migration-code)
* [Data seeding](/en-us/ef/core/modeling/data-seeding)
* [SQLite ALTER TABLE statement](https://sqlite.org/lang_altertable.html)

1. Delete the migration folder.

2. Use the following commands to recreate the database.

   ```
   dotnet ef database drop
   dotnet ef migrations add InitialCreate
   dotnet ef database update
   ```

Run the app and verify you can create, edit, and display movies with a `Rating` field. If the database isn't seeded, set a break point in the `SeedData.Initialize` method.

## Next steps

[Previous: Add Search](search?view=aspnetcore-10.0) [Next: Add Validation](validation?view=aspnetcore-10.0)

In this section [Entity Framework](/en-us/ef/core/get-started/aspnetcore/new-db) Code First Migrations is used to:

* Add a new field to the model.
* Migrate the new field schema change to the database.

When using EF Code First to automatically create and track a database, Code First:

* Adds an [`__EFMigrationsHistory`](/en-us/ef/core/managing-schemas/migrations/history-table) table to the database to track whether the schema of the database is in sync with the model classes it was generated from.
* Throws an exception if the model classes aren't in sync with the database.

Automatic verification that the schema and model are in sync makes it easier to find inconsistent database code issues.

## Add a `Rating` property to the `Movie` model

1. Open the `Models/Movie.cs` file and add a `Rating` property:

   ```
   public class Movie
   {
       public int ID { get; set; }
       public string Title { get; set; } = string.Empty;

       [Display(Name = "Release Date")]
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string Genre { get; set; } = string.Empty;

       [Column(TypeName = "decimal(18, 2)")]
       public decimal Price { get; set; }
       public string Rating { get; set; } = string.Empty;
   }
   ```

2. Edit `Pages/Movies/Index.cshtml`, and add a `Rating` field:[]()

   ```
   @page
   @model RazorPagesMovie.Pages.Movies.IndexModel

   @{
       ViewData["Title"] = "Index";
   }

   <h1>Index</h1>

   <p>
       <a asp-page="Create">Create New</a>
   </p>

   <form>
       <p>
           <select asp-for="MovieGenre" asp-items="Model.Genres">
               <option value="">All</option>
           </select>
           <label>Title: <input type="text" asp-for="SearchString" /></label>
           <input type="submit" value="Filter" />
       </p>
   </form>

   <table class="table">

       <thead>
           <tr>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Title)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Genre)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Price)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Rating)
               </th>
               <th></th>
           </tr>
       </thead>
       <tbody>
           @foreach (var item in Model.Movie)
           {
               <tr>
                   <td>
                       @Html.DisplayFor(modelItem => item.Title)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.ReleaseDate)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Genre)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Price)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Rating)
                   </td>
                   <td>
                       <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                       <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                       <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                   </td>
               </tr>
           }
       </tbody>
   </table>
   ```

3. Update the following pages with a `Rating` field:

   * *[Pages/Movies/Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie60/Pages/Movies/Create.cshtml)*.
   * *[Pages/Movies/Delete.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie60/Pages/Movies/Delete.cshtml)*.
   * *[Pages/Movies/Details.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie60/Pages/Movies/Details.cshtml)*.
   * *[Pages/Movies/Edit.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie60/Pages/Movies/Edit.cshtml)*.

The app won't work until the database is updated to include the new field. Running the app without an update to the database throws a `SqlException`:

`SqlException: Invalid column name 'Rating'.`

The `SqlException` exception is caused by the updated Movie model class being different than the schema of the Movie table of the database. There's no `Rating` column in the database table.

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database using the new model class schema. This approach is convenient early in the development cycle, it allows developers to quickly evolve the model and database schema together. The downside is that existing data in the database is lost. Don't use this approach on a production database! Dropping the database on schema changes and using an initializer to automatically seed the database with test data is often a productive way to develop an app.
2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is to keep the data. Make this change either manually or by creating a database change script.
3. Use Code First Migrations to update the database schema.

For this tutorial, use Code First Migrations.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but make this change for each `new Movie` block.

```
context.Movie.AddRange(
    new Movie
    {
        Title = "When Harry Met Sally",
        ReleaseDate = DateTime.Parse("1989-2-12"),
        Genre = "Romantic Comedy",
        Price = 7.99M,
        Rating = "R"
    },
```

See the [completed SeedData.cs file](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie60/Models/SeedDataRating.cs).

Build the solution.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

[]()

### Add a migration for the rating field

1. From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

2. In the PMC, enter the following commands:

   ```
   Add-Migration Rating
   Update-Database
   ```

The `Add-Migration` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `Update-Database` command tells the framework to apply the schema changes to the database and to preserve existing data.

[]()

Delete all the records in the database, the initializer will seed the database and include the `Rating` field. Deleting can be done with the delete links in the browser or from [Sql Server Object Explorer](sql?view=aspnetcore-10.0#ssox) (SSOX).

Another option is to delete the database and use migrations to re-create the database. To delete the database in SSOX:

1. Select the database in SSOX.

2. Right-click on the database, and select **Delete**.

3. Check **Close existing connections**.

4. Select **OK**.

5. In the [PMC](new-field?view=aspnetcore-10.0#pmc), update the database:

   ```
   Update-Database
   ```

### Add a migration for rating

Use the following commands to add a migration for the rating field:

```
dotnet ef migrations add rating
dotnet ef database update
```

The `dotnet ef migrations add rating` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name `rating` is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `dotnet ef database update` command tells the framework to apply the schema changes to the database and to preserve existing data.

Delete all the records in the database, the initializer will seed the database and include the `Rating` field.

### Optional: Drop and re-create the database for other providers

Skip this section if you successfully migrated the database.

In this tutorial, Entity Framework Core *migrations* features are used when possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and some provider's capabilities are limited. For example, adding a column may be supported, but removing or changing a column is not. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, you can drop and re-create the database.

The workaround for the limitations is to manually write migrations code to perform a table rebuild when something in the table changes. A table rebuild involves:

* Creating a new table.
* Copying data from the old table to the new table.
* Dropping the old table.
* Renaming the new table.

For more information, see the following resources:

* [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations)
* [Customize migration code](/en-us/ef/core/managing-schemas/migrations/#customize-migration-code)
* [Data seeding](/en-us/ef/core/modeling/data-seeding)
* [SQLite ALTER TABLE statement](https://sqlite.org/lang_altertable.html)

1. Delete the migration folder.

2. Use the following commands to recreate the database.

   ```
   dotnet ef database drop
   dotnet ef migrations add InitialCreate
   dotnet ef database update
   ```

Run the app and verify you can create, edit, and display movies with a `Rating` field. If the database isn't seeded, set a break point in the `SeedData.Initialize` method.

## Next steps

[Previous: Add Search](search?view=aspnetcore-10.0) [Next: Add Validation](validation?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

In this section [Entity Framework](/en-us/ef/core/get-started/aspnetcore/new-db) Code First Migrations is used to:

* Add a new field to the model.
* Migrate the new field schema change to the database.

When using EF Code First to automatically create a database, Code First:

* Adds an [`__EFMigrationsHistory`](/en-us/ef/core/managing-schemas/migrations/history-table) table to the database to track whether the schema of the database is in sync with the model classes it was generated from.
* If the model classes aren't in sync with the database, EF throws an exception.

Automatic verification that the schema and model are in sync makes it easier to find inconsistent database code issues.

## Add a `Rating` property to the `Movie` model

1. Open the `Models/Movie.cs` file and add a `Rating` property:

   ```
   public class Movie
   {
       public int ID { get; set; }
       public string Title { get; set; }

       [Display(Name = "Release Date")]
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string Genre { get; set; }

       [Column(TypeName = "decimal(18, 2)")]
       public decimal Price { get; set; }
       public string Rating { get; set; }
   }
   ```

2. Build the app.

3. Edit `Pages/Movies/Index.cshtml`, and add a `Rating` field:

   []()

   ```
   @page
   @model RazorPagesMovie.Pages.Movies.IndexModel

   @{
       ViewData["Title"] = "Index";
   }

   <h1>Index</h1>

   <p>
       <a asp-page="Create">Create New</a>
   </p>

   <form>
       <p>
           <select asp-for="MovieGenre" asp-items="Model.Genres">
               <option value="">All</option>
           </select>
           <label>Title: <input type="text" asp-for="SearchString" /></label>
           <input type="submit" value="Filter" />
       </p>
   </form>

   <table class="table">

       <thead>
           <tr>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Title)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Genre)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Price)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Rating)
               </th>
               <th></th>
           </tr>
       </thead>
       <tbody>
           @foreach (var item in Model.Movie)
           {
               <tr>
                   <td>
                       @Html.DisplayFor(modelItem => item.Title)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.ReleaseDate)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Genre)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Price)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Rating)
                   </td>
                   <td>
                       <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                       <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                       <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                   </td>
               </tr>
           }
       </tbody>
   </table>
   ```

4. Update the following pages with a `Rating` field:

   * *[Pages/Movies/Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50/Pages/Movies/Create.cshtml)*.
   * *[Pages/Movies/Delete.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50/Pages/Movies/Delete.cshtml)*.
   * *[Pages/Movies/Details.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50/Pages/Movies/Details.cshtml)*.
   * *[Pages/Movies/Edit.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50/Pages/Movies/Edit.cshtml)*.

The app won't work until the database is updated to include the new field. Running the app without an update to the database throws a `SqlException`:

`SqlException: Invalid column name 'Rating'.`

The `SqlException` exception is caused by the updated Movie model class being different than the schema of the Movie table of the database. There's no `Rating` column in the database table.

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database using the new model class schema. This approach is convenient early in the development cycle, it allows you to quickly evolve the model and database schema together. The downside is that you lose existing data in the database. Don't use this approach on a production database! Dropping the database on schema changes and using an initializer to automatically seed the database with test data is often a productive way to develop an app.

2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is to keep the data. Make this change either manually or by creating a database change script.

3. Use Code First Migrations to update the database schema.

For this tutorial, use Code First Migrations.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but make this change for each `new Movie` block.

```
context.Movie.AddRange(
    new Movie
    {
        Title = "When Harry Met Sally",
        ReleaseDate = DateTime.Parse("1989-2-12"),
        Genre = "Romantic Comedy",
        Price = 7.99M,
        Rating = "R"
    },
```

See the [completed SeedData.cs file](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50/Models/SeedDataRating.cs).

Build the solution.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

[]()

### Add a migration for the rating field

1. From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

2. In the PMC, enter the following commands:

   ```
   Add-Migration Rating
   Update-Database
   ```

The `Add-Migration` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `Update-Database` command tells the framework to apply the schema changes to the database and to preserve existing data.

[]()

Delete all the records in the database, the initializer will seed the database and include the `Rating` field. Deleting can be done with the delete links in the browser or from [Sql Server Object Explorer](sql?view=aspnetcore-10.0#ssox) (SSOX).

Another option is to delete the database and use migrations to re-create the database. To delete the database in SSOX:

1. Select the database in SSOX.

2. Right-click on the database, and select **Delete**.

3. Check **Close existing connections**.

4. Select **OK**.

5. In the [PMC](new-field?view=aspnetcore-10.0#pmc), update the database:

   ```
   Update-Database
   ```

### Add a migration for rating

Use the following commands to add a migration for the rating field:

```
dotnet ef migrations add rating
dotnet ef database update
```

The `dotnet-ef migrations add rating` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name `rating` is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `dotnet ef database update` command tells the framework to apply the schema changes to the database and to preserve existing data.

Delete all the records in the database, the initializer will seed the database and include the `Rating` field.

### Optional: Drop and re-create the database for other providers

Skip this section if you successfully migrated the database.

In this tutorial, Entity Framework Core *migrations* features are used when possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and some provider's capabilities are limited. For example, adding a column may be supported, but removing or changing a column is not. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, you can drop and re-create the database.

The workaround for the limitations is to manually write migrations code to perform a table rebuild when something in the table changes. A table rebuild involves:

* Creating a new table.
* Copying data from the old table to the new table.
* Dropping the old table.
* Renaming the new table.

For more information, see the following resources:

* [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations)
* [Customize migration code](/en-us/ef/core/managing-schemas/migrations/#customize-migration-code)
* [Data seeding](/en-us/ef/core/modeling/data-seeding)
* [SQLite ALTER TABLE statement](https://sqlite.org/lang_altertable.html)

1. Delete the migration folder.

2. Use the following commands to recreate the database.

   ```
   dotnet ef database drop
   dotnet ef migrations add InitialCreate
   dotnet ef database update
   ```

Run the app and verify you can create/edit/display movies with a `Rating` field. If the database isn't seeded, set a break point in the `SeedData.Initialize` method.

## Next steps

[Previous: Add Search](search?view=aspnetcore-10.0) [Next: Add Validation](validation?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie30) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

In this section [Entity Framework](/en-us/ef/core/get-started/aspnetcore/new-db) Code First Migrations is used to:

* Add a new field to the model.
* Migrate the new field schema change to the database.

When using EF Code First to automatically create a database, Code First:

* Adds an [`__EFMigrationsHistory`](/en-us/ef/core/managing-schemas/migrations/history-table) table to the database to track whether the schema of the database is in sync with the model classes it was generated from.
* If the model classes aren't in sync with the database, EF throws an exception.

Automatic verification that the schema and model are in sync makes it easier to find inconsistent database code issues.

## Add a `Rating` property to the `Movie` model

1. Open the `Models/Movie.cs` file and add a `Rating` property:

   ```
   public class Movie
   {
       public int ID { get; set; }
       public string Title { get; set; }

       [Display(Name = "Release Date")]
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string Genre { get; set; }

       [Column(TypeName = "decimal(18, 2)")]
       public decimal Price { get; set; }
       public string Rating { get; set; }
   }
   ```

2. Build the app.

3. Edit `Pages/Movies/Index.cshtml`, and add a `Rating` field:

   []()

   ```
   @page
   @model RazorPagesMovie.Pages.Movies.IndexModel

   @{
       ViewData["Title"] = "Index";
   }

   <h1>Index</h1>

   <p>
       <a asp-page="Create">Create New</a>
   </p>

   <form>
       <p>
           <select asp-for="MovieGenre" asp-items="Model.Genres">
               <option value="">All</option>
           </select>
           <label>Title: <input type="text" asp-for="SearchString" /></label>
           <input type="submit" value="Filter" />
       </p>
   </form>

   <table class="table">

       <thead>
           <tr>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Title)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Genre)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Price)
               </th>
               <th>
                   @Html.DisplayNameFor(model => model.Movie[0].Rating)
               </th>
               <th></th>
           </tr>
       </thead>
       <tbody>
           @foreach (var item in Model.Movie)
           {
               <tr>
                   <td>
                       @Html.DisplayFor(modelItem => item.Title)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.ReleaseDate)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Genre)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Price)
                   </td>
                   <td>
                       @Html.DisplayFor(modelItem => item.Rating)
                   </td>
                   <td>
                       <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                       <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                       <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                   </td>
               </tr>
           }
       </tbody>
   </table>
   ```

4. Update the following pages with a `Rating` field:

   * *[Pages/Movies/Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie30/Pages/Movies/Create.cshtml)*.
   * *[Pages/Movies/Delete.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie30/Pages/Movies/Delete.cshtml)*.
   * *[Pages/Movies/Details.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie30/Pages/Movies/Details.cshtml)*.
   * *[Pages/Movies/Edit.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie30/Pages/Movies/Edit.cshtml)*.

The app won't work until the database is updated to include the new field. Running the app without an update to the database throws a `SqlException`:

`SqlException: Invalid column name 'Rating'.`

The `SqlException` exception is caused by the updated Movie model class being different than the schema of the Movie table of the database. There's no `Rating` column in the database table.

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database using the new model class schema. This approach is convenient early in the development cycle, it allows you to quickly evolve the model and database schema together. The downside is that you lose existing data in the database. Don't use this approach on a production database! Dropping the database on schema changes and using an initializer to automatically seed the database with test data is often a productive way to develop an app.

2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is to keep the data. Make this change either manually or by creating a database change script.

3. Use Code First Migrations to update the database schema.

For this tutorial, use Code First Migrations.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but make this change for each `new Movie` block.

```
context.Movie.AddRange(
    new Movie
    {
        Title = "When Harry Met Sally",
        ReleaseDate = DateTime.Parse("1989-2-12"),
        Genre = "Romantic Comedy",
        Price = 7.99M,
        Rating = "R"
    },
```

See the [completed SeedData.cs file](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50/Models/SeedDataRating.cs).

Build the solution.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

[]()

### Add a migration for the rating field

1. From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

2. In the PMC, enter the following commands:

   ```
   Add-Migration Rating
   Update-Database
   ```

The `Add-Migration` command tells the framework to:

* Compare the `Movie` model with the `Movie` database schema.
* Create code to migrate the database schema to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

The `Update-Database` command tells the framework to apply the schema changes to the database and to preserve existing data.

[]()

If you delete all the records in the database, the initializer will seed the database and include the `Rating` field. You can do this with the delete links in the browser or from [Sql Server Object Explorer](sql?view=aspnetcore-10.0#ssox) (SSOX).

Another option is to delete the database and use migrations to re-create the database. To delete the database in SSOX:

* Select the database in SSOX.

* Right-click on the database, and select **Delete**.

* Check **Close existing connections**.

* Select **OK**.

* In the [PMC](new-field?view=aspnetcore-10.0#pmc), update the database:

  ```
  Update-Database
  ```

### Drop and re-create the database

Note

For this tutorial you, use the Entity Framework Core *migrations* feature where possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and the SQLite provider's capabilities are limited. For example, adding a column is supported, but removing or changing a column is not supported. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, this tutorial doesn't use migrations for SQLite schema changes. Instead, when the schema changes, you drop and re-create the database.

1. Delete the migration folder.

2. Use the following commands to recreate the database.

   ```
   dotnet ef database drop
   dotnet ef migrations add InitialCreate
   dotnet ef database update
   ```

Run the app and verify you can create/edit/display movies with a `Rating` field. If the database isn't seeded, set a break point in the `SeedData.Initialize` method.

## Next steps

[Previous: Add Search](search?view=aspnetcore-10.0) [Next: Add Validation](validation?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/security/authorization/resource-based
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/security/authorization/resource-based.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Resource-based authorization in ASP.NET Core MVC

This article describes how to authorize users for access to app resources.

In an app, a *resource* is typically represented by a C# class that includes data stored in a collection, such as a [`byte[]` array](/en-us/dotnet/api/system.byte). The class usually contains additional metadata pertaining to the resource, such as a unique resource identifier, dates, authors, source information, and a friendly name for display in a UI. The collection that holds resource data is usually loaded from physical file content, a cloud storage object, an in-memory object, or data from a database.

Resource-based authorization requires special attention in ASP.NET Core apps. Attribute evaluation occurs before data binding and before execution of an action that loads a resource. Declarative authorization with an `[Authorize]` attribute doesn't suffice for resource-based authorization. Instead, the app must invoke a custom authorization method—an approach known as *imperative authorization*.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/resource-based) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

[Create an ASP.NET Core app with user data protected by authorization](../../../security/authorization/secure-data?view=aspnetcore-10.0) contains a sample app that uses resource-based authorization.

Examples in this article use *primary constructors*, available in C# 12 (.NET 8) or later. For more information, see [Declare primary constructors for classes and structs (C# documentation tutorial)](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) and [Primary constructors (C# Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors). Sample apps that accompany the article that target versions of .NET earlier than .NET 8 use constructor injection.

## Use imperative authorization

Authorization is implemented as an [IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice), which is registered in the service collection at app startup *by the ASP.NET Core framework*. The service is made available to classes and actions via [dependency injection](../../../fundamentals/dependency-injection?view=aspnetcore-10.0). The following controller also injects a document repository, which the developer creates and registers in the service container to manage document operations:

```
public class DocumentController(IAuthorizationService authorizationService,
    IDocumentRepository documentRepository) : Controller
{
    private readonly IAuthorizationService _authorizationService;
    private readonly IDocumentRepository _documentRepository;

    public DocumentController(IAuthorizationService authorizationService,
        IDocumentRepository documentRepository)
    {
        _authorizationService = authorizationService;
        _documentRepository = documentRepository;
    }

    ...
}
```

[IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice) has two [AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) method overloads. One of the overloads accepts a resource and policy name:

```
Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource, 
    string policyName);
```

The other overload accepts a resource and collection of requirements ([IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement)) to evaluate:

```
Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource,
    IEnumerable<IAuthorizationRequirement> requirements);
```

In the following example, the secured resource is loaded into a custom `Document` object. An [AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) overload is invoked to determine whether the current user is allowed to edit the document via a custom "`EditPolicy`" authorization policy. If [`authorizationResult.Succeeded`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationresult.succeeded) is `true`, the user is authorized for the document because they authored the document (`Document.Author` matches the user's [Name](/en-us/dotnet/api/system.security.principal.iidentity.name)).

Note

The following example assumes successful authentication with the `User` property set.

```
[HttpGet]
public async Task<IActionResult> Edit(Guid documentId)
{
    Document document = _documentRepository.Find(documentId);

    ...

    var authorizationResult = await _authorizationService
        .AuthorizeAsync(User, document, "EditPolicy");

    ...
}
```

## Create a resource-based handler

Creating a resource-based authorization handler is similar to [creating a plain requirements handler](../../../security/authorization/policies?view=aspnetcore-10.0#authorization-handlers). Create a custom requirement class and implement a requirement handler class. For more information on creating a requirement class, see [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0#requirements-and-policy-registration).

The handler class specifies the requirement and resource type. The following example demonstrates a handler utilizing a `SameAuthorRequirement` requirement and a `Document` resource:

```
public class DocumentAuthorizationHandler : 
    AuthorizationHandler<SameAuthorRequirement, Document>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   SameAuthorRequirement requirement,
                                                   Document resource)
    {
        if (context.User.Identity?.Name == resource.Author)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

public class SameAuthorRequirement : IAuthorizationRequirement { }
```

In the preceding example, imagine that `SameAuthorRequirement` is a special case of a more generic `SpecificAuthorRequirement` class. The `SpecificAuthorRequirement` class (not shown) contains a `Name` property representing the name of the author. The `Name` property could be set to the current user.

Register the requirement and handler in `Program.cs`:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("EditPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));

builder.Services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
```

Register the requirement and handler in `Startup.ConfigureServices`:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("EditPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));
});

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
```

For more information on creating authorization policies, see [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0).

### Operational requirements

To make decisions based on the outcomes of CRUD (Create, Read, Update, Delete) operations, use the [OperationAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.operationauthorizationrequirement) helper class. This class enables you to write a single handler instead of an individual class for each operation type. To use it, provide some operation names:

```
public static class Operations
{
    public static OperationAuthorizationRequirement Create =
        new OperationAuthorizationRequirement { Name = nameof(Create) };
    public static OperationAuthorizationRequirement Read =
        new OperationAuthorizationRequirement { Name = nameof(Read) };
    public static OperationAuthorizationRequirement Update =
        new OperationAuthorizationRequirement { Name = nameof(Update) };
    public static OperationAuthorizationRequirement Delete =
        new OperationAuthorizationRequirement { Name = nameof(Delete) };
}
```

The handler is implemented as follows, using an [OperationAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.operationauthorizationrequirement) requirement and a `Document` resource:

```
public class DocumentAuthorizationCrudHandler :
    AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   OperationAuthorizationRequirement requirement,
                                                   Document resource)
    {
        if (context.User.Identity?.Name == resource.Author &&
            requirement.Name == Operations.Read.Name)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
```

The preceding handler validates the operation using the resource, the user's identity, and the requirement's `Name` property.

## Challenge and forbid with an operational resource handler

This section shows how the challenge and forbid action results are processed and how challenge and forbid differ.

When authorization fails but the user is authenticated, the app can return a [ForbidResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.forbidresult), which informs authentication middleware that authorization failed. Return a [ChallengeResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.challengeresult) for unauthenticated users. For interactive browser clients, it may be appropriate to redirect the user to a login page.

Note

The following example assumes successful authentication with the `User` property set.

```
if ((await _authorizationService
    .AuthorizeAsync(User, document, Operations.Read)).Succeeded)
{
    return View(document);
}
else if (User.Identity?.IsAuthenticated ?? false)
{
    return new ForbidResult();
}
else
{
    return new ChallengeResult();
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/introduction.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Overview of ASP.NET Core SignalR

ASP.NET Core SignalR is an open-source library that simplifies adding real-time web functionality to apps. Real-time web functionality enables server-side code to push content to clients instantly. There are many scenarios where an ASP.NET Core application can benefit from SignalR:

| Scenario                                                         | Examples                                                   |
| ---------------------------------------------------------------- | ---------------------------------------------------------- |
| Applications that require high frequency updates from the server | Gaming, Social networks, Voting sites, Auctions, Maps, GPS |
| Dashboards and apps for monitoring                               | Company dashboards, Instant sales updates, Travel alerts   |
| Apps that support collaboration                                  | Whiteboard apps, Team meeting software                     |
| Apps that require notifications                                  | Social networks, Email, Chat, Games, Travel alerts         |

This article provides an introduction to working with SignalR in your ASP.NET Core apps.

## Features and source code

SignalR provides an API for creating server-to-client [remote procedure calls (RPC)](https://wikipedia.org/wiki/Remote_procedure_call). The RPCs invoke functions on clients from server-side .NET code. There are several [supported platforms](supported-platforms?view=aspnetcore-10.0), each with their respective client SDK. The programming language invoked by the RPC call varies based on the platform.

SignalR for ASP.NET Core provides developers with many features:

* Handle connection management automatically
* Send messages to all connected clients simultaneously (for example, a chat room)
* Send messages to specific clients or groups of clients
* Scale to handle increasing traffic with options like the [Azure SignalR Service](scale?view=aspnetcore-10.0) and [Redis backplane](redis-backplane?view=aspnetcore-10.0)
* Support trimming and native ahead-of-time (AOT) compilation for supported scenarios
* Support polymorphic type handling in hub methods
* Support distributed tracing with `ActivitySource` for SignalR hub server and .NET client
* Work with the [SignalR Hub Protocol](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/HubProtocol.md)

The source is hosted in the [ASP.NET Core SignalR repository on GitHub](https://github.com/dotnet/AspNetCore/tree/main/src/SignalR).

## Transports

SignalR supports the following techniques for handling real-time communication (in order of graceful fallback):

* [WebSockets](../fundamentals/websockets?view=aspnetcore-10.0)
* Server-sent events
* Long polling

SignalR automatically chooses the best transport method within the capabilities of the server and client. WebSockets is the preferred transport because it generally provides the best performance.

## Hubs

SignalR uses *hubs* to communicate between clients and servers.

A hub is a high-level pipeline that a client and server use to call methods on each other. SignalR automatically handles the dispatching across machine boundaries, so clients can call methods on the server and vice versa. You can pass strongly typed parameters to methods and enable model binding.

SignalR supports two built-in hub protocols:

* A text protocol based on JSON (default)
* A binary protocol based on MessagePack. MessagePack generally creates smaller messages compared to JSON. For more information, see [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0).

Hubs call client-side code by sending messages that contain the name and parameters of the client-side method. The configured protocol deserializes objects sent as method parameters. The client tries to match the name to a method in the client-side code. When the client finds a match, it calls the method and passes the deserialized parameter data.

## Related content

* [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](supported-platforms?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [Logging and diagnostics in ASP.NET Core SignalR](diagnostics?view=aspnetcore-10.0)
* [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core Blazor SignalR guidance](../blazor/fundamentals/signalr?view=aspnetcore-10.0)

## What is SignalR?

ASP.NET Core SignalR is an open-source library that simplifies adding real-time web functionality to apps. Real-time web functionality enables server-side code to push content to clients instantly.

Good candidates for SignalR:

* Apps that require high frequency updates from the server. Examples are gaming, social networks, voting, auction, maps, and GPS apps.
* Dashboards and monitoring apps. Examples include company dashboards, instant sales updates, or travel alerts.
* Collaborative apps. Whiteboard apps and team meeting software are examples of collaborative apps.
* Apps that require notifications. Social networks, email, chat, games, travel alerts, and many other apps use notifications.

SignalR provides an API for creating server-to-client [remote procedure calls (RPC)](https://wikipedia.org/wiki/Remote_procedure_call). The RPCs invoke functions on clients from server-side .NET code. There are several [supported platforms](supported-platforms?view=aspnetcore-10.0), each with their respective client SDK. Because of this, the programming language being invoked by the RPC call varies.

Here are some features of SignalR for ASP.NET Core:

* Handles connection management automatically.
* Sends messages to all connected clients simultaneously. For example, a chat room.
* Sends messages to specific clients or groups of clients.
* Scales to handle increasing traffic with options such as the [Azure SignalR Service](scale?view=aspnetcore-10.0) and [Redis backplane](redis-backplane?view=aspnetcore-10.0).
* Supports trimming and native ahead-of-time (AOT) compilation for supported scenarios.
* Supports polymorphic type handling in hub methods.
* Supports distributed tracing with `ActivitySource` for SignalR hub server and .NET client.
* [SignalR Hub Protocol](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/HubProtocol.md)

The source is hosted in a [SignalR repository on GitHub](https://github.com/dotnet/AspNetCore/tree/main/src/SignalR).

## Transports

SignalR supports the following techniques for handling real-time communication (in order of graceful fallback):

* [WebSockets](../fundamentals/websockets?view=aspnetcore-10.0)
* Server-Sent Events
* Long Polling

SignalR automatically chooses the best transport method that is within the capabilities of the server and client. WebSockets is the preferred transport because it generally provides the best performance.

## Hubs

SignalR uses *hubs* to communicate between clients and servers.

A hub is a high-level pipeline that allows a client and server to call methods on each other. SignalR handles the dispatching across machine boundaries automatically, allowing clients to call methods on the server and vice versa. You can pass strongly-typed parameters to methods, which enables model binding. SignalR supports two built-in hub protocols: a text protocol based on JSON (default) and a binary protocol based on MessagePack. MessagePack generally creates smaller messages compared to JSON. For more information, see [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0).

Hubs call client-side code by sending messages that contain the name and parameters of the client-side method. Objects sent as method parameters are deserialized using the configured protocol. The client tries to match the name to a method in the client-side code. When the client finds a match, it calls the method and passes to it the deserialized parameter data.

## Related content

* [Get started with SignalR for ASP.NET Core](../tutorials/signalr?view=aspnetcore-10.0)
* [Supported Platforms](supported-platforms?view=aspnetcore-10.0)
* [Hubs](hubs?view=aspnetcore-10.0)
* [Logging and diagnostics in ASP.NET Core SignalR](diagnostics?view=aspnetcore-10.0)
* [Hosting and scaling ASP.NET Core SignalR](scale?view=aspnetcore-10.0)
* [JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core Blazor SignalR guidance](../blazor/fundamentals/signalr?view=aspnetcore-10.0)

## What is SignalR?

ASP.NET Core SignalR is an open-source library that simplifies adding real-time web functionality to apps. Real-time web functionality enables server-side code to push content to clients instantly.

Good candidates for SignalR:

* Apps that require high frequency updates from the server. Examples are gaming, social networks, voting, auction, maps, and GPS apps.
* Dashboards and monitoring apps. Examples include company dashboards, instant sales updates, or travel alerts.
* Collaborative apps. Whiteboard apps and team meeting software are examples of collaborative apps.
* Apps that require notifications. Social networks, email, chat, games, travel alerts, and many other apps use notifications.

SignalR provides an API for creating server-to-client [remote procedure calls (RPC)](https://wikipedia.org/wiki/Remote_procedure_call). The RPCs invoke functions on clients from server-side .NET code. There are several [supported platforms](supported-platforms?view=aspnetcore-10.0), each with their respective client SDK. Because of this, the programming language being invoked by the RPC call varies.

Here are some features of SignalR for ASP.NET Core:

* Handles connection management automatically.
* Sends messages to all connected clients simultaneously. For example, a chat room.
* Sends messages to specific clients or groups of clients.
* Scales to handle increasing traffic.
* [SignalR Hub Protocol](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/HubProtocol.md)

The source is hosted in a [SignalR repository on GitHub](https://github.com/dotnet/AspNetCore/tree/main/src/SignalR).

## Transports

SignalR supports the following techniques for handling real-time communication (in order of graceful fallback):

* [WebSockets](../fundamentals/websockets?view=aspnetcore-10.0)
* Server-Sent Events
* Long Polling

SignalR automatically chooses the best transport method that is within the capabilities of the server and client.

## Hubs

SignalR uses *hubs* to communicate between clients and servers.

A hub is a high-level pipeline that allows a client and server to call methods on each other. SignalR handles the dispatching across machine boundaries automatically, allowing clients to call methods on the server and vice versa. You can pass strongly-typed parameters to methods, which enables model binding. SignalR provides two built-in hub protocols: a text protocol based on JSON and a binary protocol based on [MessagePack](https://msgpack.org/). MessagePack generally creates smaller messages compared to JSON. Older browsers must support [XHR level 2](https://caniuse.com/#feat=xhr2) to provide MessagePack protocol support.

Hubs call client-side code by sending messages that contain the name and parameters of the client-side method. Objects sent as method parameters are deserialized using the configured protocol. The client tries to match the name to a method in the client-side code. When the client finds a match, it calls the method and passes to it the deserialized parameter data.

[]()

## Browsers that don't support ECMAScript 6 (ES6)

SignalR targets ES6. For browsers that don't support ES6, transpile the library to ES5. For more information, see [Getting Started with ES6 – Transpiling ES6 to ES5 with Traceur and Babel](https://blog.codewithdan.com/getting-started-with-es6-transpiling-es6-to-es5-with-traceur-and-babel/).

## Related content

* [Introduction to ASP.NET Core SignalR](/en-us/training/modules/aspnet-core-signalr)
* [Get started with SignalR for ASP.NET Core](../tutorials/signalr?view=aspnetcore-10.0)
* [Supported Platforms](supported-platforms?view=aspnetcore-10.0)
* [Hubs](hubs?view=aspnetcore-10.0)
* [JavaScript client](javascript-client?view=aspnetcore-10.0)
* [Browsers that don't support ECMAScript 6 (ES6)](supported-platforms?view=aspnetcore-10.0#es6)
* [ASP.NET Core Blazor SignalR guidance](../blazor/fundamentals/signalr?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/caching/memory.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Cache in-memory in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT), [John Luo](https://github.com/JunTaoLuo), and [Steve Smith](https://ardalis.com/)

Caching can significantly improve the performance and scalability of an app by reducing the work required to generate content. Caching works best with data that changes infrequently **and** is expensive to generate. Caching makes a copy of data that can be returned much faster than from the source. Apps should be written and tested to **never** depend on cached data.

ASP.NET Core supports several different caches. The simplest cache is based on the [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) interface. `IMemoryCache` represents a cache stored in the memory of the web server. Apps running on a server farm (multiple servers) should ensure sessions are sticky when using the in-memory cache. Sticky sessions ensure that requests from a client all go to the same server. For example, an Azure web app uses [Microsoft Application Request Routing (ARR)](/en-us/iis/extensions/planning-for-arr/application-request-routing-version-2-overview) to route all requests to the same server.

Nonsticky sessions in a web farm require a [distributed cache](distributed?view=aspnetcore-10.0) to avoid cache consistency problems. For some apps, a distributed cache can support higher scale-out than an in-memory cache. Using a distributed cache offloads the cache memory to an external process.

The in-memory cache can store any object. The distributed cache interface is limited to `byte[]`. The in-memory and distributed cache store cache items as key-value pairs.

## Use System.Runtime.Caching/MemoryCache

[System.Runtime.Caching](/en-us/dotnet/api/system.runtime.caching)/[MemoryCache](/en-us/dotnet/api/system.runtime.caching.memorycache) ([NuGet package](https://www.nuget.org/packages/System.Runtime.Caching/)) can be used with:

* .NET Standard 2.0 or later
* Any [.NET implementation](/en-us/dotnet/standard/net-standard#net-implementation-support) that targets .NET Standard 2.0 or later (such as ASP.NET Core 3.1 or later)
* .NET Framework 4.5 or later

[Microsoft.Extensions.Caching.Memory](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory/)/`IMemoryCache` (described in this article) is recommended over `System.Runtime.Caching`/`MemoryCache` because it offers better integration with ASP.NET Core. For example, `IMemoryCache` works natively with ASP.NET Core [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

Use `System.Runtime.Caching`/`MemoryCache` as a compatibility bridge when porting code from ASP.NET 4.x to ASP.NET Core.

## Review guidelines for in-memory caching

The following guidelines apply to in-memory caching:

* Code should always have a fallback option to fetch data and **not** depend on the availability of a cached value.

* The cache uses memory, which is a scarce resource. Limit cache growth:

  * **Don't** insert external input into the cache. As an example, using arbitrary user-provided input as a cache key isn't recommended because the input might consume an unpredictable amount of memory.

  * Use expirations to limit cache growth.

  * [Use SetSize, Size, and SizeLimit to limit cache size](#use-setsize-size-and-sizelimit-to-limit-cache-size). The ASP.NET Core runtime **doesn't** limit cache size based on memory pressure. The developer is responsible for limiting the cache size.

## Create an instance of IMemoryCache

In-memory caching is a *service* that an app references by using [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

Warning

If the same cache is used by multiple frameworks or libraries, it's a *shared* cache. If you use a shared memory cache from [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) and also use `SetSize`, `Size`, and `SizeLimit` to limit the cache size, the app can fail.

When a size limit is set on a cache, all entries must specify a size when they're added. This approach can lead to issues because developers might not have full control over what uses the shared cache.

To limit the cache size with the `SetSize` method, the `Size` property, or the `SizeLimit` property, create a cache singleton for caching. For more information and an example, see [Use SetSize, Size, and SizeLimit to limit cache size](#use-setsize-size-and-sizelimit-to-limit-cache-size).

Request the `IMemoryCache` instance in the constructor:

```
public class IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

    public IndexModel(IMemoryCache memoryCache) =>
        _memoryCache = memoryCache;

    // ...
```

The following code uses the [TryGetValue](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache.trygetvalue) method to check if a time is in the cache. If a time isn't cached, a new entry is created and added to the cache with the [Set](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.set) method:

```
public void OnGet()
{
    CurrentDateTime = DateTime.Now;

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = CurrentDateTime;

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }

    CacheCurrentDateTime = cacheValue;
}
```

In the preceding code, the cache entry is configured with a sliding expiration of 3 seconds. If the cache entry isn't accessed for more than 3 seconds, the entry is evicted from the cache. Each time the cache entry is accessed, it remains in the cache for a further 3 seconds. The `CacheKeys` class is part of the download sample.

The current time and the cached time are displayed:

```
<ul>
    <li>Current Time: @Model.CurrentDateTime</li>
    <li>Cached Time: @Model.CacheCurrentDateTime</li>
</ul>
```

The following code uses the `Set` extension method to cache data for a relative time without [MemoryCacheEntryOptions](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryoptions):

```
_memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));
```

In the preceding code, the cache entry is configured with a relative expiration of one day. The cache entry is evicted from the cache after one day, even if the entry is accessed during the timeout period.

The following code uses the [GetOrCreate](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.getorcreate) and [GetOrCreateAsync](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.getorcreateasync) methods to cache data.

```
public void OnGetCacheGetOrCreate()
{
    var cachedValue = _memoryCache.GetOrCreate(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return DateTime.Now;
        });

    // ...
}

public async Task OnGetCacheGetOrCreateAsync()
{
    var cachedValue = await _memoryCache.GetOrCreateAsync(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    // ...
}
```

The following code calls the [Get](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.get) method to fetch the cached time:

```
var cacheEntry = _memoryCache.Get<DateTime?>(CacheKeys.Entry);
```

The following code gets or creates a cached item with absolute expiration:

```
var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.Entry,
    cacheEntry =>
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });
```

A cached item set with only a sliding expiration is at risk of never expiring. If the cached item is repeatedly accessed within the sliding expiration interval, the item never expires. Combining a sliding expiration with an absolute expiration guarantees the item expires. The absolute expiration sets an upper bound on how long the item can be cached. It still allows the item to expire earlier, if the item isn't requested within the sliding expiration interval. If either the sliding expiration interval *or* the absolute expiration time passes, the item is evicted from the cache.

The following code gets or creates a cached item with both sliding *and* absolute expiration:

```
var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.CallbackEntry,
    cacheEntry =>
    {
        cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });
```

The preceding code guarantees the data isn't cached longer than the absolute time.

[GetOrCreate](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.getorcreate), [GetOrCreateAsync](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.getorcreateasync), and [Get](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.get) are extension methods in the [CacheExtensions](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions) class. These methods extend the capability of [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache).

## Create MemoryCacheEntryOptions for an entry

The following example demonstrates how to create [MemoryCacheEntryOptions](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryoptions) for an entry. The code completes the following tasks:

* Sets the cache priority to [CacheItemPriority.NeverRemove](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheitempriority#microsoft-extensions-caching-memory-cacheitempriority-neverremove).

* Sets a [PostEvictionDelegate](/en-us/dotnet/api/microsoft.extensions.caching.memory.postevictiondelegate) to call after the entry is evicted from the cache. The callback runs on a different thread from the code that removes the item from the cache.

```
public void OnGetCacheRegisterPostEvictionCallback()
{
    var memoryCacheEntryOptions = new MemoryCacheEntryOptions()
        .SetPriority(CacheItemPriority.NeverRemove)
        .RegisterPostEvictionCallback(PostEvictionCallback, _memoryCache);

    _memoryCache.Set(CacheKeys.CallbackEntry, DateTime.Now, memoryCacheEntryOptions);
}

private static void PostEvictionCallback(
    object cacheKey, object cacheValue, EvictionReason evictionReason, object state)
{
    var memoryCache = (IMemoryCache)state;

    memoryCache.Set(
        CacheKeys.CallbackMessage,
        $"Entry {cacheKey} was evicted: {evictionReason}.");
}
```

## Limit cache size with SetSize, Size, and SizeLimit

A `MemoryCache` instance can optionally specify and enforce a size limit. The cache size limit doesn't have a defined unit of measure because the cache has no mechanism to measure the size of entries. If the cache size limit is set, all entries must specify size. The ASP.NET Core runtime doesn't limit cache size based on memory pressure. It's up to the developer to limit cache size. The size specified is in units the developer chooses.

For example:

* If the web app primarily caches strings, each cache entry size might be the string length.
* The app can specify the size of all entries as 1, and the size limit is the count of entries.

If the [SizeLimit](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions.sizelimit#microsoft-extensions-caching-memory-memorycacheoptions-sizelimit) property isn't set, the cache grows without bound. The ASP.NET Core runtime doesn't trim the cache when system memory is low. Apps must be architected to:

* Limit cache growth.
* Call the [Compact](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache.compact) or [Remove](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache.remove) method when available memory is limited.

The following code creates a unitless fixed-size [MemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache) instance that's accessible by [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0):

```
public class MyMemoryCache
{
    public MemoryCache Cache { get; } = new MemoryCache(
        new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
}
```

The `SizeLimit` property doesn't have units. Cached entries must specify size in whatever units they consider most appropriate when the cache size limit is set. All users of a cache instance should use the same unit system. An entry isn't cached if the sum of the cached entry sizes exceeds the value specified by `SizeLimit`. If no cache size limit is set, the cache size set on the entry is ignored.

The following code registers the `MyMemoryCache` instance with the [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container:

```
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSingleton<MyMemoryCache>();
```

`MyMemoryCache` is created as an independent memory cache for components that are aware of this size-limited cache and know how to set cache entry size appropriately.

The size of the cache entry can be set by using the [SetSize](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryextensions.setsize) extension method or the [Size](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryoptions.size#microsoft-extensions-caching-memory-memorycacheentryoptions-size) property:

```
if (!_myMemoryCache.Cache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        .SetSize(1);

    // cacheEntryOptions.Size = 1;

    _myMemoryCache.Cache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
}
```

In the preceding code, the two highlighted lines achieve the same result of setting the size of the cache entry. The `SetSize` method is provided for convenience when chaining calls onto `new MemoryCacheOptions()`.

### Remove cache items with MemoryCache.Compact

The `MemoryCache.Compact` method attempts to remove the specified percentage of the cache in the following order:

* All expired items
* Items by priority, where lowest priority items are removed first
* Least recently used objects
* Items with the earliest absolute expiration
* Items with the earliest sliding expiration

Pinned items with priority [NeverRemove](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheitempriority#microsoft-extensions-caching-memory-cacheitempriority-neverremove) are [never removed](https://github.com/dotnet/runtime/blob/release/6.0/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs#L415-L430). The following code removes a cache item and calls the `Compact` method to remove 25% of cached entries:

```
_myMemoryCache.Cache.Remove(CacheKeys.Entry);
_myMemoryCache.Cache.Compact(.25);
```

For more information, see the [Compact source on GitHub](https://github.com/dotnet/runtime/blob/release/6.0/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs#L382-L393).

## Evict a cache entry with expired dependencies

The following sample shows how to expire a cache entry if a dependent entry expires. A [CancellationChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.cancellationchangetoken) is added to the cached item. When the `Cancel` method is called on the `CancellationTokenSource` object, both cache entries are evicted:

```
public void OnGetCacheCreateDependent()
{
    var cancellationTokenSource = new CancellationTokenSource();

    _memoryCache.Set(
        CacheKeys.DependentCancellationTokenSource,
        cancellationTokenSource);

    using var parentCacheEntry = _memoryCache.CreateEntry(CacheKeys.Parent);

    parentCacheEntry.Value = DateTime.Now;

    _memoryCache.Set(
        CacheKeys.Child,
        DateTime.Now,
        new CancellationChangeToken(cancellationTokenSource.Token));
}

public void OnGetCacheRemoveDependent()
{
    var cancellationTokenSource = _memoryCache.Get<CancellationTokenSource>(
        CacheKeys.DependentCancellationTokenSource);

    cancellationTokenSource.Cancel();
}
```

When you use a [CancellationTokenSource](/en-us/dotnet/api/system.threading.cancellationtokensource) object, you can evict multiple cache entries as a group. With the `using` pattern in the preceding code, cache entries created inside the `using` scope inherit triggers and expiration settings.

## Review notes about in-memory caching

The following notes apply to in-memory caching:

* Expiration doesn't happen in the background.

  There's no timer that actively scans the cache for expired items. Any activity on the cache (via `Get`, `TryGetValue`, `Set`, or `Remove`) can trigger a background scan for expired items. A timer set on the `CancellationTokenSource` object (by using the [CancelAfter](/en-us/dotnet/api/system.threading.cancellationtokensource.cancelafter) method) also removes the entry and triggers a scan for expired items.

  The following example uses the [CancellationTokenSource(TimeSpan)](/en-us/dotnet/api/system.threading.cancellationtokensource.-ctor#system-threading-cancellationtokensource-ctor\(system-timespan\)) overloaded constructor for the registered token. When this token fires, it removes the entry immediately and fires the eviction callbacks:

  ```
  if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
  {
      cacheValue = DateTime.Now;

      var cancellationTokenSource = new CancellationTokenSource(
          TimeSpan.FromSeconds(10));

      var cacheEntryOptions = new MemoryCacheEntryOptions()
          .AddExpirationToken(
              new CancellationChangeToken(cancellationTokenSource.Token))
          .RegisterPostEvictionCallback((key, value, reason, state) =>
          {
              ((CancellationTokenSource)state).Dispose();
          }, cancellationTokenSource);

      _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
  }
  ```

* When you use a callback to repopulate a cache item:

  * Multiple requests might discover the cached key value is empty because the callback isn't finished.
  * This approach can result in several threads repopulating the cached item.

* When one cache entry (the parent) creates another entry (the child), the child copies the parent entry's expiration tokens and time-based expiration settings. The child doesn't expire by manual removal or updating of the parent entry.

* Use the [PostEvictionCallbacks](/en-us/dotnet/api/microsoft.extensions.caching.memory.icacheentry.postevictioncallbacks#microsoft-extensions-caching-memory-icacheentry-postevictioncallbacks) property to specify which callbacks should fire after a cache entry is evicted from the cache.

* For most apps, `IMemoryCache` is enabled. For example, calling `AddMvc`, `AddControllersWithViews`, `AddRazorPages`, `AddMvcCore().AddRazorViewEngine`, and many other `Add{Service}` methods in the *Program.cs* file enables `IMemoryCache`.

  For apps that don't call one of the mentioned `Add{Service}` methods, you might need to call the [AddMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.addmemorycache) method in the *Program.cs* file.

## Use a background cache update

Use a [background service](../../fundamentals/host/hosted-services?view=aspnetcore-10.0) such as the [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) interface to update the cache. The background service can recompute the entries and assign them to the cache only after they're ready.

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/memory/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Detect changes with change tokens in ASP.NET Core](../../fundamentals/change-tokens?view=aspnetcore-10.0)
* [Response caching in ASP.NET Core](response?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/memory/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Caching basics

Caching can significantly improve the performance and scalability of an app by reducing the work required to generate content. Caching works best with data that changes infrequently **and** is expensive to generate. Caching makes a copy of data that can be returned much faster than from the source. Apps should be written and tested to **never** depend on cached data.

ASP.NET Core supports several different caches. The simplest cache is based on the [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache). `IMemoryCache` represents a cache stored in the memory of the web server. Apps running on a server farm (multiple servers) should ensure sessions are sticky when using the in-memory cache. Sticky sessions ensure that subsequent requests from a client all go to the same server. For example, Azure Web apps use [Application Request Routing](/en-us/iis/extensions/planning-for-arr/application-request-routing-version-2-overview) (ARR) to route all subsequent requests to the same server.

Non-sticky sessions in a web farm require a [distributed cache](distributed?view=aspnetcore-10.0) to avoid cache consistency problems. For some apps, a distributed cache can support higher scale-out than an in-memory cache. Using a distributed cache offloads the cache memory to an external process.

The in-memory cache can store any object. The distributed cache interface is limited to `byte[]`. The in-memory and distributed cache store cache items as key-value pairs.

## System.Runtime.Caching/MemoryCache

[System.Runtime.Caching](/en-us/dotnet/api/system.runtime.caching)/[MemoryCache](/en-us/dotnet/api/system.runtime.caching.memorycache) ([NuGet package](https://www.nuget.org/packages/System.Runtime.Caching/)) can be used with:

* .NET Standard 2.0 or later.
* Any [.NET implementation](/en-us/dotnet/standard/net-standard#net-implementation-support) that targets .NET Standard 2.0 or later. For example, ASP.NET Core 3.1 or later.
* .NET Framework 4.5 or later.

[Microsoft.Extensions.Caching.Memory](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory/)/`IMemoryCache` (described in this article) is recommended over `System.Runtime.Caching`/`MemoryCache` because it's better integrated into ASP.NET Core. For example, `IMemoryCache` works natively with ASP.NET Core [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

Use `System.Runtime.Caching`/`MemoryCache` as a compatibility bridge when porting code from ASP.NET 4.x to ASP.NET Core.

## Cache guidelines

* Code should always have a fallback option to fetch data and **not** depend on a cached value being available.

* The cache uses a scarce resource, memory. Limit cache growth:

  * Do **not** use external input as cache keys.
  * Use expirations to limit cache growth.
  * [Use SetSize, Size, and SizeLimit to limit cache size](#use-setsize-size-and-sizelimit-to-limit-cache-size). The ASP.NET Core runtime does **not** limit cache size based on memory pressure. It's up to the developer to limit cache size.

## Use IMemoryCache

Warning

Using a *shared* memory cache from [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) and calling `SetSize`, `Size`, or `SizeLimit` to limit cache size can cause the app to fail. When a size limit is set on a cache, all entries must specify a size when being added. This can lead to issues since developers may not have full control on what uses the shared cache. When using `SetSize`, `Size`, or `SizeLimit` to limit cache, create a cache singleton for caching. For more information and an example, see [Use SetSize, Size, and SizeLimit to limit cache size](#use-setsize-size-and-sizelimit-to-limit-cache-size). A shared cache is one shared by other frameworks or libraries.

In-memory caching is a *service* that's referenced from an app using [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Request the `IMemoryCache` instance in the constructor:

```
public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }
```

The following code uses [TryGetValue](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache.trygetvalue) to check if a time is in the cache. If a time isn't cached, a new entry is created and added to the cache with [Set](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.set). The `CacheKeys` class is part of the download sample.

```
public static class CacheKeys
{
    public static string Entry => "_Entry";
    public static string CallbackEntry => "_Callback";
    public static string CallbackMessage => "_CallbackMessage";
    public static string Parent => "_Parent";
    public static string Child => "_Child";
    public static string DependentMessage => "_DependentMessage";
    public static string DependentCTS => "_DependentCTS";
    public static string Ticks => "_Ticks";
    public static string CancelMsg => "_CancelMsg";
    public static string CancelTokenSource => "_CancelTokenSource";
}
```

```
public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}
```

The current time and the cached time are displayed:

```
@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsynchronous">CacheGetOrCreateAsynchronous</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbs">CacheGetOrCreateAbs</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbsSliding">CacheGetOrCreateAbsSliding</a></li>

    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>
```

The following code uses the `Set` extension method to cache data for a relative time without creating the `MemoryCacheEntryOptions` object:

```
public IActionResult SetCacheRelativeExpiration()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Save data in cache and set the relative expiration time to one day
        _cache.Set(CacheKeys.Entry, cacheEntry, TimeSpan.FromDays(1));
    }

    return View("Cache", cacheEntry);
}
```

The cached `DateTime` value remains in the cache while there are requests within the timeout period.

The following code uses [GetOrCreate](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.getorcreate) and [GetOrCreateAsync](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.getorcreateasync) to cache data.

```
public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsynchronous()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    return View("Cache", cacheEntry);
}
```

The following code calls [Get](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.get) to fetch the cached time:

```
public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}
```

The following code gets or creates a cached item with absolute expiration:

```
public IActionResult CacheGetOrCreateAbs()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}
```

A cached item set with only a sliding expiration is at risk of never expiring. If the cached item is repeatedly accessed within the sliding expiration interval, the item never expires. Combine a sliding expiration with an absolute expiration to guarantee the item expires. The absolute expiration sets an upper bound on how long the item can be cached while still allowing the item to expire earlier if it isn't requested within the sliding expiration interval. If either the sliding expiration interval *or* the absolute expiration time pass, the item is evicted from the cache.

The following code gets or creates a cached item with both sliding *and* absolute expiration:

```
public IActionResult CacheGetOrCreateAbsSliding()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SetSlidingExpiration(TimeSpan.FromSeconds(3));
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}
```

The preceding code guarantees the data will not be cached longer than the absolute time.

[GetOrCreate](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.getorcreate), [GetOrCreateAsync](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.getorcreateasync), and [Get](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions.get) are extension methods in the [CacheExtensions](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheextensions) class. These methods extend the capability of [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache).

## `MemoryCacheEntryOptions`

The following sample:

* Sets a sliding expiration time. Requests that access this cached item will reset the sliding expiration clock.
* Sets the cache priority to [CacheItemPriority.NeverRemove](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheitempriority#microsoft-extensions-caching-memory-cacheitempriority-neverremove).
* Sets a [PostEvictionDelegate](/en-us/dotnet/api/microsoft.extensions.caching.memory.postevictiondelegate) that will be called after the entry is evicted from the cache. The callback is run on a different thread from the code that removes the item from the cache.

```
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}
```

## Use SetSize, Size, and SizeLimit to limit cache size

A `MemoryCache` instance may optionally specify and enforce a size limit. The cache size limit does not have a defined unit of measure because the cache has no mechanism to measure the size of entries. If the cache size limit is set, all entries must specify size. The ASP.NET Core runtime does not limit cache size based on memory pressure. It's up to the developer to limit cache size. The size specified is in units the developer chooses.

For example:

* If the web app was primarily caching strings, each cache entry size could be the string length.
* The app could specify the size of all entries as 1, and the size limit is the count of entries.

If [SizeLimit](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions.sizelimit#microsoft-extensions-caching-memory-memorycacheoptions-sizelimit) isn't set, the cache grows without bound. The ASP.NET Core runtime doesn't trim the cache when system memory is low. Apps must be architected to:

* Limit cache growth.
* Call [Compact](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache.compact) or [Remove](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache.remove) when available memory is limited:

The following code creates a unitless fixed size [MemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache) accessible by [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0):

```
// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache 
{
    public MemoryCache Cache { get; private set; }
    public MyMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
    }
}
```

`SizeLimit` does not have units. Cached entries must specify size in whatever units they deem most appropriate if the cache size limit has been set. All users of a cache instance should use the same unit system. An entry will not be cached if the sum of the cached entry sizes exceeds the value specified by `SizeLimit`. If no cache size limit is set, the cache size set on the entry will be ignored.

The following code registers `MyMemoryCache` with the [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSingleton<MyMemoryCache>();
}
```

`MyMemoryCache` is created as an independent memory cache for components that are aware of this size limited cache and know how to set cache entry size appropriately.

The following code uses `MyMemoryCache`:

```
public class SetSize : PageModel
{
    private MemoryCache _cache;
    public static readonly string MyKey = "_MyKey";

    public SetSize(MyMemoryCache memoryCache)
    {
        _cache = memoryCache.Cache;
    }

    [TempData]
    public string DateTime_Now { get; set; }

    public IActionResult OnGet()
    {
        if (!_cache.TryGetValue(MyKey, out string cacheEntry))
        {
            // Key not in cache, so get data.
            cacheEntry = DateTime.Now.TimeOfDay.ToString();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
                // Set cache entry size by extension method.
                .SetSize(1)
                // Keep in cache for this time, reset time if accessed.
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // Set cache entry size via property.
            // cacheEntryOptions.Size = 1;

            // Save data in cache.
            _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
        }

        DateTime_Now = cacheEntry;

        return RedirectToPage("./Index");
    }
}
```

The size of the cache entry can be set by [Size](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryoptions.size#microsoft-extensions-caching-memory-memorycacheentryoptions-size) or the [SetSize](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryextensions.setsize) extension methods:

```
public IActionResult OnGet()
{
    if (!_cache.TryGetValue(MyKey, out string cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now.TimeOfDay.ToString();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Set cache entry size by extension method.
            .SetSize(1)
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Set cache entry size via property.
        // cacheEntryOptions.Size = 1;

        // Save data in cache.
        _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
    }

    DateTime_Now = cacheEntry;

    return RedirectToPage("./Index");
}
```

### MemoryCache.Compact

`MemoryCache.Compact` attempts to remove the specified percentage of the cache in the following order:

* All expired items.
* Items by priority. Lowest priority items are removed first.
* Least recently used objects.
* Items with the earliest absolute expiration.
* Items with the earliest sliding expiration.

Pinned items with priority [NeverRemove](/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheitempriority#microsoft-extensions-caching-memory-cacheitempriority-neverremove) are never removed. The following code removes a cache item and calls `Compact`:

```
_cache.Remove(MyKey);

// Remove 33% of cached items.
_cache.Compact(.33);   
cache_size = _cache.Count;
```

For more information, see the [Compact source on GitHub](https://github.com/dotnet/extensions/blob/v3.0.0-preview8.19405.4/src/Caching/Memory/src/MemoryCache.cs#L382-L393).

## Cache dependencies

The following sample shows how to expire a cache entry if a dependent entry expires. A [CancellationChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.cancellationchangetoken) is added to the cached item. When `Cancel` is called on the `CancellationTokenSource`, both cache entries are evicted.

```
public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}
```

Using a [CancellationTokenSource](/en-us/dotnet/api/system.threading.cancellationtokensource) allows multiple cache entries to be evicted as a group. With the `using` pattern in the code above, cache entries created inside the `using` block will inherit triggers and expiration settings.

## Additional notes

* Expiration doesn't happen in the background. There is no timer that actively scans the cache for expired items. Any activity on the cache (`Get`, `Set`, `Remove`) can trigger a background scan for expired items. A timer on the `CancellationTokenSource` ([CancelAfter](/en-us/dotnet/api/system.threading.cancellationtokensource.cancelafter)) also removes the entry and triggers a scan for expired items. The following example uses [CancellationTokenSource(TimeSpan)](/en-us/dotnet/api/system.threading.cancellationtokensource.-ctor#system-threading-cancellationtokensource-ctor\(system-timespan\)) for the registered token. When this token fires it removes the entry immediately and fires the eviction callbacks:

  ```
  public IActionResult CacheAutoExpiringTryGetValueSet()
  {
      DateTime cacheEntry;

      if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
      {
          cacheEntry = DateTime.Now;

          var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

          var cacheEntryOptions = new MemoryCacheEntryOptions()
              .AddExpirationToken(new CancellationChangeToken(cts.Token));

          _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
      }

      return View("Cache", cacheEntry);
  }
  ```

* When using a callback to repopulate a cache item:

  * Multiple requests can find the cached key value empty because the callback hasn't completed.
  * This can result in several threads repopulating the cached item.

* When one cache entry is used to create another, the child copies the parent entry's expiration tokens and time-based expiration settings. The child isn't expired by manual removal or updating of the parent entry.

* Use [PostEvictionCallbacks](/en-us/dotnet/api/microsoft.extensions.caching.memory.icacheentry.postevictioncallbacks#microsoft-extensions-caching-memory-icacheentry-postevictioncallbacks) to set the callbacks that will be fired after the cache entry is evicted from the cache. In the example code, [CancellationTokenSource.Dispose()](/en-us/dotnet/api/system.threading.cancellationtokensource.dispose#system-threading-cancellationtokensource-dispose) is called to release the unmanaged resources used by the `CancellationTokenSource`. However, the `CancellationTokenSource` is not disposed immediately because it is still being used by the cache entry. The `CancellationToken` is passed to `MemoryCacheEntryOptions` to create a cache entry that expires after a certain time. So `Dispose` should not be called until the cache entry is removed or expired. The example code calls the [RegisterPostEvictionCallback](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryextensions.registerpostevictioncallback) method to register a callback that will be invoked when the cache entry is evicted, and it disposes the `CancellationTokenSource` in that callback.

* For most apps, `IMemoryCache` is enabled. For example, calling `AddMvc`, `AddControllersWithViews`, `AddRazorPages`, `AddMvcCore().AddRazorViewEngine`, and many other `Add{Service}` methods in `ConfigureServices`, enables `IMemoryCache`. For apps that are not calling one of the preceding `Add{Service}` methods, it may be necessary to call [AddMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.addmemorycache) in `ConfigureServices`.

## Background cache update

Use a [background service](../../fundamentals/host/hosted-services?view=aspnetcore-10.0) such as [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) to update the cache. The background service can recompute the entries and then assign them to the cache only when they're ready.

## Related content

* [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0)
* [Detect changes with change tokens in ASP.NET Core](../../fundamentals/change-tokens?view=aspnetcore-10.0)
* [Response caching in ASP.NET Core](response?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/redis-backplane
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/redis-backplane.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Set up a Redis backplane for ASP.NET Core SignalR scale-out

By [Ashley Stanton-Nurse](https://github.com/analogrelay), [Brady Gaster](https://twitter.com/bradygaster), and [Tom Dykstra](https://github.com/tdykstra).

This article explains SignalR-specific aspects of setting up a [Redis](https://redis.io/) server to use for scaling out an ASP.NET Core SignalR app.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../security/?view=aspnetcore-10.0#secure-authentication-flows).

## Set up a Redis backplane

* Deploy a Redis server.

  Important

  For production use, a Redis backplane is recommended only when it runs in the same data center as the SignalR app. Otherwise, network latency degrades performance. If your SignalR app is running in the Azure cloud, we recommend Azure SignalR Service instead of a Redis backplane.

  For more information, see the following resources:

  * [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
  * [Redis documentation](https://redis.io/)
  * [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

* In the SignalR app, install the following NuGet package:

  * `Microsoft.AspNetCore.SignalR.StackExchangeRedis`

* Call [AddStackExchangeRedis](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangeredisdependencyinjectionextensions.addstackexchangeredis) by adding the following line before the line that calls `builder.Build()`) in the `Program.cs` file.

  ```
  builder.Services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");
  ```

* Configure options as needed:

  Most options can be set in the connection string or in the [`ConfigurationOptions`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options) object. Options specified in `ConfigurationOptions` override the ones set in the connection string.

  The following example shows how to set options in the `ConfigurationOptions` object. This example adds a channel prefix so that multiple apps can share the same Redis instance, as explained in the following step.

  ```
  builder.Services.AddSignalR()
    .AddStackExchangeRedis(connectionString, options => {
        options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
    });
  ```

  In the preceding code, `options.Configuration` is initialized with whatever was specified in the connection string.

  For information about Redis options, see the [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/Configuration.html).

* If you're using one Redis server for multiple SignalR apps, use a different channel prefix for each SignalR app.

  Setting a channel prefix isolates one SignalR app from others that use different channel prefixes. If you don't assign different prefixes, a message sent from one app to all of its own clients will go to all clients of all apps that use the Redis server as a backplane.

* Configure your server farm load balancing software for sticky sessions. Here are some examples of documentation on how to do that:

  * [IIS](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing)
  * [HAProxy](https://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know/)
  * [Nginx](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#sticky)

## Redis server errors

When a Redis server goes down, SignalR throws exceptions that indicate messages won't be delivered. Some typical exception messages:

* *Failed writing message*
* *Failed to invoke hub method 'MethodName'*
* *Connection to Redis failed*

SignalR doesn't buffer messages to send them when the server comes back up. Any messages sent while the Redis server is down are lost.

SignalR automatically reconnects when the Redis server is available again.

### Custom behavior for connection failures

Here's an example that shows how to handle Redis connection failure events.

```
services.AddSignalR()
        .AddMessagePackProtocol()
        .AddStackExchangeRedis(o =>
        {
            o.ConnectionFactory = async writer =>
            {
                var config = new ConfigurationOptions
                {
                    AbortOnConnectFail = false
                };
                config.EndPoints.Add(IPAddress.Loopback, 0);
                config.SetDefaultPorts();
                var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
                connection.ConnectionFailed += (_, e) =>
                {
                    Console.WriteLine("Connection to Redis failed.");
                };

                if (!connection.IsConnected)
                {
                    Console.WriteLine("Did not connect to Redis.");
                }

                return connection;
            };
        });
```

## Redis Cluster

[Redis Cluster](https://redis.io/topics/cluster-spec) utilizes multiple simultaneously active Redis servers to achieve high availability. When Redis Cluster is used as the backplane for SignalR, messages are delivered to all of the nodes of the cluster without code modifications to the app.

There's a tradeoff between the number of nodes in the cluster and the throughput of the backplane. Increasing the number of nodes increases the availability of the cluster but decreases the throughput because messages must be transmitted to all of the nodes in the cluster.

In the SignalR app, include all of the possible Redis nodes using either of the following approaches:

* List the nodes in the connection string delimited with commas.
* If using custom behavior for connection failures, add the nodes to [`ConfigurationOptions.Endpoints`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options).

## Next steps

For more information, see the following resources:

* [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
* [Redis documentation](https://redis.io/documentation)
* [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/)
* [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

This article explains SignalR-specific aspects of setting up a [Redis](https://redis.io/) server to use for scaling out an ASP.NET Core SignalR app.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../security/?view=aspnetcore-10.0#secure-authentication-flows).

## Set up a Redis backplane

* Deploy a Redis server.

  Important

  For production use, a Redis backplane is recommended only when it runs in the same data center as the SignalR app. Otherwise, network latency degrades performance. If your SignalR app is running in the Azure cloud, we recommend Azure SignalR Service instead of a Redis backplane.

  For more information, see the following resources:

  * [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
  * [Redis documentation](https://redis.io/)
  * [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

* In the SignalR app, install the following NuGet package:

  * `Microsoft.AspNetCore.SignalR.StackExchangeRedis`

* Call [AddStackExchangeRedis](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangeredisdependencyinjectionextensions.addstackexchangeredis) by adding the following line before the line that calls `builder.Build()`) in the `Program.cs` file.

  ```
  builder.Services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");
  ```

* Configure options as needed:

  Most options can be set in the connection string or in the [`ConfigurationOptions`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options) object. Options specified in `ConfigurationOptions` override the ones set in the connection string.

  The following example shows how to set options in the `ConfigurationOptions` object. This example adds a channel prefix so that multiple apps can share the same Redis instance, as explained in the following step.

  ```
  services.AddSignalR()
    .AddStackExchangeRedis(connectionString, options => {
        options.Configuration.ChannelPrefix = "MyApp";
    });
  ```

  In the preceding code, `options.Configuration` is initialized with whatever was specified in the connection string.

  For information about Redis options, see the [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/Configuration.html).

* If you're using one Redis server for multiple SignalR apps, use a different channel prefix for each SignalR app.

  Setting a channel prefix isolates one SignalR app from others that use different channel prefixes. If you don't assign different prefixes, a message sent from one app to all of its own clients will go to all clients of all apps that use the Redis server as a backplane.

* Configure your server farm load balancing software for sticky sessions. Here are some examples of documentation on how to do that:

  * [IIS](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing)
  * [HAProxy](https://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know/)
  * [Nginx](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#sticky)

## Redis server errors

When a Redis server goes down, SignalR throws exceptions that indicate messages won't be delivered. Some typical exception messages:

* *Failed writing message*
* *Failed to invoke hub method 'MethodName'*
* *Connection to Redis failed*

SignalR doesn't buffer messages to send them when the server comes back up. Any messages sent while the Redis server is down are lost.

SignalR automatically reconnects when the Redis server is available again.

### Custom behavior for connection failures

Here's an example that shows how to handle Redis connection failure events.

```
services.AddSignalR()
        .AddMessagePackProtocol()
        .AddStackExchangeRedis(o =>
        {
            o.ConnectionFactory = async writer =>
            {
                var config = new ConfigurationOptions
                {
                    AbortOnConnectFail = false
                };
                config.EndPoints.Add(IPAddress.Loopback, 0);
                config.SetDefaultPorts();
                var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
                connection.ConnectionFailed += (_, e) =>
                {
                    Console.WriteLine("Connection to Redis failed.");
                };

                if (!connection.IsConnected)
                {
                    Console.WriteLine("Did not connect to Redis.");
                }

                return connection;
            };
        });
```

## Redis Cluster

[Redis Cluster](https://redis.io/topics/cluster-spec) utilizes multiple simultaneously active Redis servers to achieve high availability. When Redis Cluster is used as the backplane for SignalR, messages are delivered to all of the nodes of the cluster without code modifications to the app.

There's a tradeoff between the number of nodes in the cluster and the throughput of the backplane. Increasing the number of nodes increases the availability of the cluster but decreases the throughput because messages must be transmitted to all of the nodes in the cluster.

In the SignalR app, include all of the possible Redis nodes using either of the following approaches:

* List the nodes in the connection string delimited with commas.
* If using custom behavior for connection failures, add the nodes to [`ConfigurationOptions.Endpoints`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options).

## Next steps

For more information, see the following resources:

* [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
* [Redis documentation](https://redis.io/documentation)
* [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/)
* [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

This article explains SignalR-specific aspects of setting up a [Redis](https://redis.io/) server to use for scaling out an ASP.NET Core SignalR app.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../security/?view=aspnetcore-10.0#secure-authentication-flows).

## Set up a Redis backplane

* Deploy a Redis server.

  Important

  For production use, a Redis backplane is recommended only when it runs in the same data center as the SignalR app. Otherwise, network latency degrades performance. If your SignalR app is running in the Azure cloud, we recommend Azure SignalR Service instead of a Redis backplane.

  For more information, see the following resources:

  * [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
  * [Redis documentation](https://redis.io/)
  * [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

* In the SignalR app, install the following NuGet package:

  * `Microsoft.AspNetCore.SignalR.StackExchangeRedis`

* Call [AddStackExchangeRedis](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangeredisdependencyinjectionextensions.addstackexchangeredis) by adding the following line before the line that calls `builder.Build()`) in the `Program.cs` file.

  ```
  builder.Services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");
  ```

* Configure options as needed:

  Most options can be set in the connection string or in the [`ConfigurationOptions`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options) object. Options specified in `ConfigurationOptions` override the ones set in the connection string.

  The following example shows how to set options in the `ConfigurationOptions` object. This example adds a channel prefix so that multiple apps can share the same Redis instance, as explained in the following step.

  ```
  builder.Services.AddSignalR()
    .AddStackExchangeRedis(connectionString, options => {
        options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
    });
  ```

  In the preceding code, `options.Configuration` is initialized with whatever was specified in the connection string.

  For information about Redis options, see the [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/Configuration.html).

* If you're using one Redis server for multiple SignalR apps, use a different channel prefix for each SignalR app.

  Setting a channel prefix isolates one SignalR app from others that use different channel prefixes. If you don't assign different prefixes, a message sent from one app to all of its own clients will go to all clients of all apps that use the Redis server as a backplane.

* Configure your server farm load balancing software for sticky sessions. Here are some examples of documentation on how to do that:

  * [IIS](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing)
  * [HAProxy](https://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know/)
  * [Nginx](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#sticky)

## Redis server errors

When a Redis server goes down, SignalR throws exceptions that indicate messages won't be delivered. Some typical exception messages:

* *Failed writing message*
* *Failed to invoke hub method 'MethodName'*
* *Connection to Redis failed*

SignalR doesn't buffer messages to send them when the server comes back up. Any messages sent while the Redis server is down are lost.

SignalR automatically reconnects when the Redis server is available again.

### Custom behavior for connection failures

Here's an example that shows how to handle Redis connection failure events.

```
services.AddSignalR()
        .AddMessagePackProtocol()
        .AddStackExchangeRedis(o =>
        {
            o.ConnectionFactory = async writer =>
            {
                var config = new ConfigurationOptions
                {
                    AbortOnConnectFail = false
                };
                config.EndPoints.Add(IPAddress.Loopback, 0);
                config.SetDefaultPorts();
                var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
                connection.ConnectionFailed += (_, e) =>
                {
                    Console.WriteLine("Connection to Redis failed.");
                };

                if (!connection.IsConnected)
                {
                    Console.WriteLine("Did not connect to Redis.");
                }

                return connection;
            };
        });
```

## Redis Cluster

[Redis Cluster](https://redis.io/topics/cluster-spec) utilizes multiple simultaneously active Redis servers to achieve high availability. When Redis Cluster is used as the backplane for SignalR, messages are delivered to all of the nodes of the cluster without code modifications to the app.

There's a tradeoff between the number of nodes in the cluster and the throughput of the backplane. Increasing the number of nodes increases the availability of the cluster but decreases the throughput because messages must be transmitted to all of the nodes in the cluster.

In the SignalR app, include all of the possible Redis nodes using either of the following approaches:

* List the nodes in the connection string delimited with commas.
* If using custom behavior for connection failures, add the nodes to [`ConfigurationOptions.Endpoints`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options).

## Next steps

For more information, see the following resources:

* [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
* [Redis documentation](https://redis.io/documentation)
* [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/)
* [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

This article explains SignalR-specific aspects of setting up a [Redis](https://redis.io/) server to use for scaling out an ASP.NET Core SignalR app.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../security/?view=aspnetcore-10.0#secure-authentication-flows).

## Set up a Redis backplane

* Deploy a Redis server.

  Important

  For production use, a Redis backplane is recommended only when it runs in the same data center as the SignalR app. Otherwise, network latency degrades performance. If your SignalR app is running in the Azure cloud, we recommend Azure SignalR Service instead of a Redis backplane.

  For more information, see the following resources:

  * [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
  * [Redis documentation](https://redis.io/)
  * [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

* In the SignalR app, install the following NuGet package:

  * `Microsoft.AspNetCore.SignalR.StackExchangeRedis`

* In the `Startup.ConfigureServices` method, call [AddStackExchangeRedis](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangeredisdependencyinjectionextensions.addstackexchangeredis):

  ```
  services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");
  ```

* Configure options as needed:

  Most options can be set in the connection string or in the [`ConfigurationOptions`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options) object. Options specified in `ConfigurationOptions` override the ones set in the connection string.

  The following example shows how to set options in the `ConfigurationOptions` object. This example adds a channel prefix so that multiple apps can share the same Redis instance, as explained in the following step.

  ```
  services.AddSignalR()
    .AddStackExchangeRedis(connectionString, options => {
        options.Configuration.ChannelPrefix = "MyApp";
    });
  ```

  In the preceding code, `options.Configuration` is initialized with whatever was specified in the connection string.

  For information about Redis options, see the [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/Configuration.html).

* If you're using one Redis server for multiple SignalR apps, use a different channel prefix for each SignalR app.

  Setting a channel prefix isolates one SignalR app from others that use different channel prefixes. If you don't assign different prefixes, a message sent from one app to all of its own clients will go to all clients of all apps that use the Redis server as a backplane.

* Configure your server farm load balancing software for sticky sessions. Here are some examples of documentation on how to do that:

  * [IIS](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing)
  * [HAProxy](https://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know/)
  * [Nginx](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#sticky)

## Redis server errors

When a Redis server goes down, SignalR throws exceptions that indicate messages won't be delivered. Some typical exception messages:

* *Failed writing message*
* *Failed to invoke hub method 'MethodName'*
* *Connection to Redis failed*

SignalR doesn't buffer messages to send them when the server comes back up. Any messages sent while the Redis server is down are lost.

SignalR automatically reconnects when the Redis server is available again.

### Custom behavior for connection failures

Here's an example that shows how to handle Redis connection failure events.

```
services.AddSignalR()
        .AddMessagePackProtocol()
        .AddStackExchangeRedis(o =>
        {
            o.ConnectionFactory = async writer =>
            {
                var config = new ConfigurationOptions
                {
                    AbortOnConnectFail = false
                };
                config.EndPoints.Add(IPAddress.Loopback, 0);
                config.SetDefaultPorts();
                var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
                connection.ConnectionFailed += (_, e) =>
                {
                    Console.WriteLine("Connection to Redis failed.");
                };

                if (!connection.IsConnected)
                {
                    Console.WriteLine("Did not connect to Redis.");
                }

                return connection;
            };
        });
```

## Redis Cluster

[Redis Cluster](https://redis.io/topics/cluster-spec) utilizes multiple simultaneously active Redis servers to achieve high availability. When Redis Cluster is used as the backplane for SignalR, messages are delivered to all of the nodes of the cluster without code modifications to the app.

There's a tradeoff between the number of nodes in the cluster and the throughput of the backplane. Increasing the number of nodes increases the availability of the cluster but decreases the throughput because messages must be transmitted to all of the nodes in the cluster.

In the SignalR app, include all of the possible Redis nodes using either of the following approaches:

* List the nodes in the connection string delimited with commas.
* If using custom behavior for connection failures, add the nodes to [`ConfigurationOptions.Endpoints`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options).

## Next steps

For more information, see the following resources:

* [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
* [Redis documentation](https://redis.io/documentation)
* [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/)
* [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

This article explains SignalR-specific aspects of setting up a [Redis](https://redis.io/) server to use for scaling out an ASP.NET Core SignalR app.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../security/?view=aspnetcore-10.0#secure-authentication-flows).

## Set up a Redis backplane

* Deploy a Redis server.

  Important

  For production use, a Redis backplane is recommended only when it runs in the same data center as the SignalR app. Otherwise, network latency degrades performance. If your SignalR app is running in the Azure cloud, we recommend Azure SignalR Service instead of a Redis backplane.

  For more information, see the following resources:

  * [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
  * [Redis documentation](https://redis.io/)
  * [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

* In the SignalR app, install the following NuGet package:

  * `Microsoft.AspNetCore.SignalR.StackExchangeRedis`

* In the `Startup.ConfigureServices` method, call [AddStackExchangeRedis](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangeredisdependencyinjectionextensions.addstackexchangeredis):

  ```
  services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");
  ```

* Configure options as needed:

  Most options can be set in the connection string or in the [`ConfigurationOptions`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options) object. Options specified in `ConfigurationOptions` override the ones set in the connection string.

  The following example shows how to set options in the `ConfigurationOptions` object. This example adds a channel prefix so that multiple apps can share the same Redis instance, as explained in the following step.

  ```
  services.AddSignalR()
    .AddStackExchangeRedis(connectionString, options => {
        options.Configuration.ChannelPrefix = "MyApp";
    });
  ```

  In the preceding code, `options.Configuration` is initialized with whatever was specified in the connection string.

  For information about Redis options, see the [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/Configuration.html).

* If you're using one Redis server for multiple SignalR apps, use a different channel prefix for each SignalR app.

  Setting a channel prefix isolates one SignalR app from others that use different channel prefixes. If you don't assign different prefixes, a message sent from one app to all of its own clients will go to all clients of all apps that use the Redis server as a backplane.

* Configure your server farm load balancing software for sticky sessions. Here are some examples of documentation on how to do that:

  * [IIS](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing)
  * [HAProxy](https://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know/)
  * [Nginx](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#sticky)

## Redis server errors

When a Redis server goes down, SignalR throws exceptions that indicate messages won't be delivered. Some typical exception messages:

* *Failed writing message*
* *Failed to invoke hub method 'MethodName'*
* *Connection to Redis failed*

SignalR doesn't buffer messages to send them when the server comes back up. Any messages sent while the Redis server is down are lost.

SignalR automatically reconnects when the Redis server is available again.

### Custom behavior for connection failures

Here's an example that shows how to handle Redis connection failure events.

```
services.AddSignalR()
        .AddMessagePackProtocol()
        .AddStackExchangeRedis(o =>
        {
            o.ConnectionFactory = async writer =>
            {
                var config = new ConfigurationOptions
                {
                    AbortOnConnectFail = false
                };
                config.EndPoints.Add(IPAddress.Loopback, 0);
                config.SetDefaultPorts();
                var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
                connection.ConnectionFailed += (_, e) =>
                {
                    Console.WriteLine("Connection to Redis failed.");
                };

                if (!connection.IsConnected)
                {
                    Console.WriteLine("Did not connect to Redis.");
                }

                return connection;
            };
        });
```

## Redis Cluster

[Redis Cluster](https://redis.io/topics/cluster-spec) utilizes multiple simultaneously active Redis servers to achieve high availability. When Redis Cluster is used as the backplane for SignalR, messages are delivered to all of the nodes of the cluster without code modifications to the app.

There's a tradeoff between the number of nodes in the cluster and the throughput of the backplane. Increasing the number of nodes increases the availability of the cluster but decreases the throughput because messages must be transmitted to all of the nodes in the cluster.

In the SignalR app, include all of the possible Redis nodes using either of the following approaches:

* List the nodes in the connection string delimited with commas.
* If using custom behavior for connection failures, add the nodes to [`ConfigurationOptions.Endpoints`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options).

## Next steps

For more information, see the following resources:

* [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
* [Redis documentation](https://redis.io/documentation)
* [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/)
* [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

This article explains SignalR-specific aspects of setting up a [Redis](https://redis.io/) server to use for scaling out an ASP.NET Core SignalR app.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../security/?view=aspnetcore-10.0#secure-authentication-flows).

## Set up a Redis backplane

* Deploy a Redis server.

  Important

  For production use, a Redis backplane is recommended only when it runs in the same data center as the SignalR app. Otherwise, network latency degrades performance. If your SignalR app is running in the Azure cloud, we recommend Azure SignalR Service instead of a Redis backplane.

  For more information, see the following resources:

  * [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
  * [Redis documentation](https://redis.io/)
  * [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

* In the SignalR app, install one of the following NuGet packages:

  * `Microsoft.AspNetCore.SignalR.StackExchangeRedis` - Depends on StackExchange.Redis 2.X.X. This is the recommended package for ASP.NET Core 2.2 or later.
  * `Microsoft.AspNetCore.SignalR.Redis` - Depends on StackExchange.Redis 1.X.X. This package isn't included in ASP.NET Core 3.0 or later.

* In the `Startup.ConfigureServices` method, call [AddStackExchangeRedis](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangeredisdependencyinjectionextensions.addstackexchangeredis):

  ```
  services.AddSignalR().AddStackExchangeRedis("<your_Redis_connection_string>");
  ```

When using `Microsoft.AspNetCore.SignalR.Redis`, call [AddRedis](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.redisdependencyinjectionextensions.addredis).

* Configure options as needed:

  Most options can be set in the connection string or in the [ConfigurationOptions](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options) object. Options specified in `ConfigurationOptions` override the ones set in the connection string.

  The following example shows how to set options in the `ConfigurationOptions` object. This example adds a channel prefix so that multiple apps can share the same Redis instance, as explained in the following step.

  ```
  services.AddSignalR()
    .AddStackExchangeRedis(connectionString, options => {
        options.Configuration.ChannelPrefix = "MyApp";
    });
  ```

When using `Microsoft.AspNetCore.SignalR.Redis`, call [AddRedis](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.redisdependencyinjectionextensions.addredis).

In the preceding code, `options.Configuration` is initialized with whatever was specified in the connection string.

For information about Redis options, see the [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/Configuration.html).

* If you're using one Redis server for multiple SignalR apps, use a different channel prefix for each SignalR app.

  Setting a channel prefix isolates one SignalR app from others that use different channel prefixes. If you don't assign different prefixes, a message sent from one app to all of its own clients will go to all clients of all apps that use the Redis server as a backplane.

* Configure your server farm load balancing software for sticky sessions. Here are some examples of documentation on how to do that:

  * [IIS](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing)
  * [HAProxy](https://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know/)
  * [Nginx](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#sticky)

## Redis server errors

When a Redis server goes down, SignalR throws exceptions that indicate messages won't be delivered. Some typical exception messages:

* *Failed writing message*
* *Failed to invoke hub method 'MethodName'*
* *Connection to Redis failed*

SignalR doesn't buffer messages to send them when the server comes back up. Any messages sent while the Redis server is down are lost.

SignalR automatically reconnects when the Redis server is available again.

### Custom behavior for connection failures

Here's an example that shows how to handle Redis connection failure events.

```
services.AddSignalR()
        .AddMessagePackProtocol()
        .AddStackExchangeRedis(o =>
        {
            o.ConnectionFactory = async writer =>
            {
                var config = new ConfigurationOptions
                {
                    AbortOnConnectFail = false
                };
                config.EndPoints.Add(IPAddress.Loopback, 0);
                config.SetDefaultPorts();
                var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
                connection.ConnectionFailed += (_, e) =>
                {
                    Console.WriteLine("Connection to Redis failed.");
                };

                if (!connection.IsConnected)
                {
                    Console.WriteLine("Did not connect to Redis.");
                }

                return connection;
            };
        });
```

## Redis Cluster

[Redis Cluster](https://redis.io/topics/cluster-spec) utilizes multiple simultaneously active Redis servers to achieve high availability. When Redis Cluster is used as the backplane for SignalR, messages are delivered to all of the nodes of the cluster without code modifications to the app.

There's a tradeoff between the number of nodes in the cluster and the throughput of the backplane. Increasing the number of nodes increases the availability of the cluster but decreases the throughput because messages must be transmitted to all of the nodes in the cluster.

In the SignalR app, include all of the possible Redis nodes using either of the following approaches:

* List the nodes in the connection string delimited with commas.
* If using custom behavior for connection failures, add the nodes to [`ConfigurationOptions.Endpoints`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options).

## Next steps

For more information, see the following resources:

* [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
* [Redis documentation](https://redis.io/documentation)
* [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/)
* [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

This article explains SignalR-specific aspects of setting up a [Redis](https://redis.io/) server to use for scaling out an ASP.NET Core SignalR app.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../security/?view=aspnetcore-10.0#secure-authentication-flows).

## Set up a Redis backplane

* Deploy a Redis server.

  Important

  For production use, a Redis backplane is recommended only when it runs in the same data center as the SignalR app. Otherwise, network latency degrades performance. If your SignalR app is running in the Azure cloud, we recommend Azure SignalR Service instead of a Redis backplane.

  For more information, see the following resources:

  * [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
  * [Redis documentation](https://redis.io/)
  * [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

* In the SignalR app, install the `Microsoft.AspNetCore.SignalR.Redis` NuGet package.

* In the `Startup.ConfigureServices` method, call `AddRedis` after `AddSignalR`:

  ```
  services.AddSignalR().AddRedis("<your_Redis_connection_string>");
  ```

* Configure options as needed:

  Most options can be set in the connection string or in the [ConfigurationOptions](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options) object. Options specified in `ConfigurationOptions` override the ones set in the connection string.

  The following example shows how to set options in the `ConfigurationOptions` object. This example adds a channel prefix so that multiple apps can share the same Redis instance, as explained in the following step.

  ```
  services.AddSignalR()
    .AddRedis(connectionString, options => {
        options.Configuration.ChannelPrefix = "MyApp";
    });
  ```

  In the preceding code, `options.Configuration` is initialized with whatever was specified in the connection string.

* If you're using one Redis server for multiple SignalR apps, use a different channel prefix for each SignalR app.

  Setting a channel prefix isolates one SignalR app from others that use different channel prefixes. If you don't assign different prefixes, a message sent from one app to all of its own clients will go to all clients of all apps that use the Redis server as a backplane.

* Configure your server farm load balancing software for sticky sessions. Here are some examples of documentation on how to do that:

  * [IIS](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing)
  * [HAProxy](https://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know/)
  * [Nginx](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#sticky)

## Redis server errors

When a Redis server goes down, SignalR throws exceptions that indicate messages won't be delivered. Some typical exception messages:

* *Failed writing message*
* *Failed to invoke hub method 'MethodName'*
* *Connection to Redis failed*

SignalR doesn't buffer messages to send them when the server comes back up. Any messages sent while the Redis server is down are lost.

SignalR automatically reconnects when the Redis server is available again.

### Custom behavior for connection failures

Here's an example that shows how to handle Redis connection failure events.

```
services.AddSignalR()
        .AddRedis(o =>
        {
            o.ConnectionFactory = async writer =>
            {
                var config = new ConfigurationOptions
                {
                    AbortOnConnectFail = false
                };
                config.EndPoints.Add(IPAddress.Loopback, 0);
                config.SetDefaultPorts();
                var connection = await ConnectionMultiplexer.ConnectAsync(config, writer);
                connection.ConnectionFailed += (_, e) =>
                {
                    Console.WriteLine("Connection to Redis failed.");
                };

                if (!connection.IsConnected)
                {
                    Console.WriteLine("Did not connect to Redis.");
                }

                return connection;
            };
        });
```

## Redis Cluster

[Redis Cluster](https://redis.io/topics/cluster-spec) utilizes multiple simultaneously active Redis servers to achieve high availability. When Redis Cluster is used as the backplane for SignalR, messages are delivered to all of the nodes of the cluster without code modifications to the app.

There's a tradeoff between the number of nodes in the cluster and the throughput of the backplane. Increasing the number of nodes increases the availability of the cluster but decreases the throughput because messages must be transmitted to all of the nodes in the cluster.

In the SignalR app, include all of the possible Redis nodes using either of the following approaches:

* List the nodes in the connection string delimited with commas.
* If using custom behavior for connection failures, add the nodes to [`ConfigurationOptions.Endpoints`](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options).

## Next steps

For more information, see the following resources:

* [ASP.NET Core SignalR production hosting and scaling](scale?view=aspnetcore-10.0)
* [Redis documentation](https://redis.io/documentation)
* [StackExchange Redis documentation](https://stackexchange.github.io/StackExchange.Redis/)
* [Azure Redis Cache documentation](/en-us/azure/azure-cache-for-redis/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/distributed-cache-tag-helper
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/distributed-cache-tag-helper.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Distributed Cache Tag Helper in ASP.NET Core

By [Peter Kellner](https://peterkellner.net)

The Distributed Cache Tag Helper provides the ability to dramatically improve the performance of your ASP.NET Core app by caching its content to a [distributed cache source](../../../../performance/caching/distributed?view=aspnetcore-10.0).

For an overview of Tag Helpers, see [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0).

The Distributed Cache Tag Helper inherits from the same base class as the Cache Tag Helper. All of the [Cache Tag Helper](cache-tag-helper?view=aspnetcore-10.0) attributes are available to the Distributed Tag Helper.

The Distributed Cache Tag Helper uses [constructor injection](../../../../fundamentals/dependency-injection?view=aspnetcore-10.0#constructor-injection-behavior). The [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface is passed into the Distributed Cache Tag Helper's constructor. If no concrete implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) is created in the `Program` file or `Startup.ConfigureServices` (`Startup.cs`), the Distributed Cache Tag Helper uses the same in-memory provider for storing cached data as the [Cache Tag Helper](cache-tag-helper?view=aspnetcore-10.0).

## Distributed Cache Tag Helper Attributes

### Attributes shared with the Cache Tag Helper

* `enabled`
* `expires-on`
* `expires-after`
* `expires-sliding`
* `vary-by`
* `vary-by-cookie`
* `vary-by-culture`
* `vary-by-header`
* `vary-by-query`
* `vary-by-route`
* `vary-by-user`

The `expires-sliding` attribute value defaults to 30 seconds if `expires-after` and `expires-on` aren't set. Otherwise, the value defaults to 20 minutes with the SQL Server caching service.

The Distributed Cache Tag Helper inherits from the same class as Cache Tag Helper. For descriptions of these attributes, see the [Cache Tag Helper](cache-tag-helper?view=aspnetcore-10.0).

### name

| Attribute Type | Example                               |
| -------------- | ------------------------------------- |
| String         | `my-distributed-cache-unique-key-101` |

`name` is required. The `name` attribute is used as a key for each stored cache instance. Unlike the Cache Tag Helper that assigns a cache key to each instance based on the Razor page name and location in the Razor page, the Distributed Cache Tag Helper only bases its key on the attribute `name`.

Example:

```
<distributed-cache name="my-distributed-cache-unique-key-101">
    Time Inside Cache Tag Helper: @DateTime.Now
</distributed-cache>
```

## Distributed Cache Tag Helper `IDistributedCache` implementations

There are two implementations of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) built in to ASP.NET Core. One is based on SQL Server, and the other is based on Redis. Third-party implementations are also available, such as [NCache](http://www.alachisoft.com/ncache/aspnet-core-idistributedcache-ncache.html). Both implementations involve setting an instance of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) in either the `Program` file or `Startup` class.

There are no tag attributes specifically associated with using any implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache).

For more information, see [Distributed caching in ASP.NET Core](../../../../performance/caching/distributed?view=aspnetcore-10.0).

## Related content

* [Cache Tag Helper in ASP.NET Core MVC](cache-tag-helper?view=aspnetcore-10.0)
* [Dependency injection in ASP.NET Core](../../../../fundamentals/dependency-injection?view=aspnetcore-10.0)
* [Cache in-memory in ASP.NET Core](../../../../performance/caching/memory?view=aspnetcore-10.0)
* [Introduction to Identity on ASP.NET Core](../../../../security/authentication/identity?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/claims.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Map, customize, and transform claims in ASP.NET Core

By [Damien Bowden](https://github.com/damienbod)

Claims can be created from any user or identity data that can be issued by using a trusted identity provider or ASP.NET Core identity. A claim is a name-value pair that represents what the subject is, not what the subject can do.

This article describes how to configure and map claims by using an [OpenID Connect](https://openid.net/developers/how-connect-works/) client and covers the following tasks:

* Set name and role claims
* Reset the claims namespaces
* Customize and extend the claims with the [TransformAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation.transformasync) method

## Map claims with OpenID Connect authentication

The profile claims can be returned in the `id_token`, which is returned after a successful authentication. The ASP.NET Core client app only requires the profile scope. When you use the `id_token` for claims, no extra claims mapping is required.

```
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code requires the [Microsoft.AspNetCore.Authentication.OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect) NuGet package.

Another way to get the user claims is to use the OpenID Connect User Info API. The ASP.NET Core client app uses the `GetClaimsFromUserInfoEndpoint` property to do the configuration. In this scenario, you must explicitly specify the required claims by using the `MapUniqueJsonKey` method. Otherwise, only the `name`, `given_name`, and `email` standard claims are available in the client app. The claims included in the `id_token` are mapped per default.

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
       options.GetClaimsFromUserInfoEndpoint = true;
       options.ClaimActions.MapUniqueJsonKey("preferred_username",
                                             "preferred_username");
       options.ClaimActions.MapUniqueJsonKey("gender", "gender");
   });

var app = builder.Build();

// Code removed for brevity.
```

Note

The default Open ID Connect handler uses Pushed Authorization Requests (PAR) if the identity provider's discovery document advertises support for PAR. The common location for the identity provider's discovery document is the *.well-known/openid-configuration* folder. If you can't use PAR in the client configuration on the identity provider, PAR can be disabled by using the `PushedAuthorizationBehavior` option.

```
builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect("oidc", oidcOptions =>
    {
        // Other provider-specific configuration goes here.

        // The default value is PushedAuthorizationBehavior.UseIfAvailable.

        // 'OpenIdConnectOptions' does not contain a definition for 'PushedAuthorizationBehavior'
        // and no accessible extension method 'PushedAuthorizationBehavior' accepting a first argument
        // of type 'OpenIdConnectOptions' could be found
        oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
    });
```

To ensure authentication succeeds only if PAR is used, use the [PushedAuthorizationBehavior.Require](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.pushedauthorizationbehavior) enum instead. This change also introduces a new [OnPushAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onpushauthorization#microsoft-aspnetcore-authentication-openidconnect-openidconnectevents-onpushauthorization) event to [OpenIdConnectEvents](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents), which can be used to customize the pushed authorization request or handle it manually. For more information, see the [API proposal in GitHub dotnet/aspnetcore issue #51686](https://github.com/dotnet/aspnetcore/issues/51686) - *Support for Pushed Authorization Requests in OidcHandler*.

## Name claim and role claim mapping

The **name** claim and **role** claim are mapped to default properties in the ASP.NET Core HTTP context. Sometimes, you need to use different claims for the default properties, or the name claim and role claim don't match the default values. The claims can be mapped by using the `TokenValidationParameters` property and set to any claim as required. The values from the claims can be used directly in the HttpContext `User.Identity.Name` property and the roles.

If the `User.Identity.Name` property has no value, or the roles are missing, check the values in the returned claims and set the `NameClaimType` and the `RoleClaimType` values. The returned claims from the client authentication can be viewed in the HTTP context.

```
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
  .AddCookie()
  .AddOpenIdConnect(options =>
  {
       // Other options...
       options.TokenValidationParameters = new TokenValidationParameters
       {
          NameClaimType = "email"
          //, RoleClaimType = "role"
       };
  });
```

## Claims namespaces, default namespaces

ASP.NET Core adds default namespaces to some known claims. The default claims might not be required in the app. As an option, you can disable the added namespaces and use the exact claims that the OpenID Connect server created.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });

var app = builder.Build();

// Code removed for brevity.
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });

var app = builder.Build();

// Code removed for brevity.
```

If you need to disable the namespaces per scheme and not globally, you can use the `MapInboundClaims = false` option.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.MapInboundClaims = false;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });

var app = builder.Build();

// Code removed for brevity.
```

## Extend or add custom claims using 'IClaimsTransformation'

The [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) interface can be used to add extra claims to the [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) class. The interface requires a single method, [TransformAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation.transformasync). This method might be called multiple times. Only add a new claim if it doesn't already exist in the `ClaimsPrincipal`. A `ClaimsIdentity` object is created to add the new claims and it can be added to the `ClaimsPrincipal`.

```
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;

public class MyClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        ClaimsIdentity claimsIdentity = new ClaimsIdentity();
        var claimType = "myNewClaim";
        if (!principal.HasClaim(claim => claim.Type == claimType))
        {
            claimsIdentity.AddClaim(new Claim(claimType, "myClaimValue"));
        }

        principal.AddIdentity(claimsIdentity);
        return Task.FromResult(principal);
    }
}
```

The [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) interface and the `MyClaimsTransformation` class can be registered as a service:

```
builder.Services.AddTransient<IClaimsTransformation, MyClaimsTransformation>();
```

## Map claims from external identity providers

If you want to map claims from external identity providers for your app, see [Persist other claims and tokens from external providers in ASP.NET Core](social/additional-claims?view=aspnetcore-10.0).

Claims can be created from any user or identity data which can be issued using a trusted identity provider or ASP.NET Core identity. A claim is a name value pair that represents what the subject is, not what the subject can do. This article covers the following areas:

* How to configure and map claims using an [OpenID Connect](https://openid.net/developers/how-connect-works/) client
* Set the name and role claim
* Reset the claims namespaces
* Customize, extend the claims using [TransformAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation.transformasync)

## Mapping claims using OpenID Connect authentication

The profile claims can be returned in the `id_token`, which is returned after a successful authentication. The ASP.NET Core client app only requires the profile scope. When using the `id_token` for claims, no extra claims mapping is required.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
   });
```

Another way to get the user claims is to use the OpenID Connect User Info API. The ASP.NET Core client application uses the `GetClaimsFromUserInfoEndpoint` property to configure this. One important difference from the first settings, is that you must specify the claims you require using the `MapUniqueJsonKey` method, otherwise only the `name`, `given_name` and `email` standard claims will be available in the client application. The claims included in the `id_token` are mapped per default. This is the major difference to the first option. You must explicitly define some of the claims you require.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       options.SignInScheme = "Cookies";
       options.Authority = "-your-identity-provider-";
       options.RequireHttpsMetadata = true;
       options.ClientId = "-your-clientid-";
       options.ClientSecret = "-your-client-secret-from-user-secrets-or-keyvault";
       options.ResponseType = "code";
       options.UsePkce = true;
       options.Scope.Add("profile");
       options.SaveTokens = true;
       options.GetClaimsFromUserInfoEndpoint = true;
       options.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username");
       options.ClaimActions.MapUniqueJsonKey("gender", "gender");
   }); 
```

## Name claim and role claim mapping

The **Name** claim and the **Role** claim are mapped to default properties in the ASP.NET Core HTTP context. Sometimes it is required to use different claims for the default properties, or the name claim and the role claim do not match the default values. The claims can be mapped using the **TokenValidationParameters** property and set to any claim as required. The values from the claims can be used directly in the HttpContext **User.Identity.Name** property and the roles.

If the `User.Identity.Name` has no value or the roles are missing, please check the values in the returned claims and set the `NameClaimType` and the `RoleClaimType` values. The returned claims from the client authentication can be viewed in the HTTP context.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
       // other options...
       options.TokenValidationParameters = new TokenValidationParameters
       {
         NameClaimType = "email", 
         // RoleClaimType = "role"
       };
   });
```

## Claims namespaces, default namespaces

ASP.NET Core adds default namespaces to some known claims, which might not be required in the app. Optionally, disable these added namespaces and use the exact claims that the OpenID Connect server created.

```
public void Configure(IApplicationBuilder app)
{
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
```

## Extend or add custom claims using `IClaimsTransformation`

The `IClaimsTransformation` interface can be used to add extra claims to the `ClaimsPrincipal` class. The interface requires a single method `TransformAsync`. This method might get called multiple times. Only add a new claim if it does not already exist in the `ClaimsPrincipal`. A `ClaimsIdentity` is created to add the new claims and this can be added to the `ClaimsPrincipal`.

```
public class MyClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
       ClaimsIdentity claimsIdentity = new ClaimsIdentity();
       var claimType = "myNewClaim";
       if (!principal.HasClaim(claim => claim.Type == claimType))
       {		   
          claimsIdentity.AddClaim(new Claim(claimType, "myClaimValue"));
       }

       principal.AddIdentity(claimsIdentity);
       return Task.FromResult(principal);
    }
}
```

The `IClaimsTransformation` interface and the `MyClaimsTransformation` class can be added in the ConfigureServices method as a service.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IClaimsTransformation, MyClaimsTransformation>();
```

## Extend or add custom claims in ASP.NET Core Identity

Refer to the following document:

[Add claims to Identity using IUserClaimsPrincipalFactory](add-user-data?view=aspnetcore-10.0#add-claims-to-identity-using-iuserclaimsprincipalfactoryapplicationuser)

## Map claims from external identity providers

Refer to the following document:

[Persist additional claims and tokens from external providers in ASP.NET Core](social/additional-claims?view=aspnetcore-10.0)

## Related content

* [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation)
* [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/middleware/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core middleware

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Middleware is software that's assembled into an app pipeline to handle requests and responses. Each middleware:

* Chooses whether to pass the request to the next middleware in the pipeline.
* Can perform work before and after the next middleware in the pipeline.

Request delegates are used to build the request pipeline. The request delegates handle each HTTP request.

Configure request delegates by using the [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run), [Map](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapextensions.map), and [Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) extension methods. You can specify an individual request delegate inline as an anonymous method (called inline middleware) or define it in a reusable class. These inline anonymous methods or reusable classes are called *middleware* or *middleware components*. Each middleware in the request pipeline is responsible for invoking the next middleware in the pipeline or short-circuiting the pipeline. When a middleware short-circuits, it's called a *terminal middleware* because it prevents further middleware from processing the request.

For more information about the difference between request pipelines in ASP.NET Core and ASP.NET 4.x with additional middleware samples, see [Migrate HTTP modules to ASP.NET Core middleware](../../migration/fx-to-core/areas/http-modules?view=aspnetcore-10.0).

## Role of middleware by app type

Server-side Blazor, Razor Pages, and MVC process browser requests on the server with middleware. The guidance in this article applies to these types of apps.

Standalone Blazor WebAssembly apps run entirely on the client and don't process requests with a middleware pipeline. The guidance in this article doesn't apply to standalone Blazor WebAssembly apps.

## Middleware code analysis

For more information about ASP.NET Core's compiler platform analyzers that inspect app code for quality, see [Diagnostic Code Analysis in ASP.NET Core Apps](../../diagnostics/code-analysis?view=aspnetcore-10.0).

## Create a middleware pipeline with `WebApplication`

The ASP.NET Core request pipeline consists of a sequence of request delegates, called one after the other. The following diagram demonstrates the concept. The thread of execution follows the black arrows.

Each delegate can perform operations before and after the next delegate. Exception-handling delegates should be called early in the pipeline, so they can catch exceptions that occur in later stages of the pipeline.

Note

To experiment locally with the code examples in this section, create an ASP.NET Core app using the **ASP.NET Core Empty** project template. If using the .NET CLI, the template short name is `web` (`dotnet new web`).

The simplest ASP.NET Core app calls [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run) to set up a single terminal middleware as an anonymous function request delegate to handle requests without a request pipeline.

In the following example:

* The call to [RunExtensions.Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run) is invoked on every request and writes "Hello world!" to the response.
* The call to [WebApplication.Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.run) at the end of the code block runs the app and blocks the calling thread until host shutdown.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello world!");
});

app.Run();
```

Response when accessing the app in a browser at its launch URL:

> Hello world!

Chain multiple request delegates together with [Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use). The `next` parameter represents the next delegate in the pipeline. You can typically perform actions both before and after the `next` delegate.

The following example demonstrates:

* Two [Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) calls, each writing to the console:

  * Where work can be performed that can write to the response (`context.Response`, [HttpResponse](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse)).
  * Where work can be performed that doesn't write to the response after the `next` parameter is invoked.

* A terminal request delegate with a call to [RunExtensions.Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run) that writes "Hello world!" to the response.

* A final [Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) call, which never executes because it follows the [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run) terminal request delegate.

* A call to [WebApplication.Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.run) at the end of the code block to run the app and block the calling thread until host shutdown.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    Console.WriteLine("Work that can write to the response. (1)");
    await next.Invoke(context);
    Console.WriteLine("Work that doesn't write to the response. (1)");
});

app.Use(async (context, next) =>
{
    Console.WriteLine("Work that can write to the response. (2)");
    await next.Invoke(context);
    Console.WriteLine("Work that doesn't write to the response. (2)");
});

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello world!");
});

app.Use(async (context, next) =>
{
    Console.WriteLine("This statement isn't reached. (3)");
    await next.Invoke(context);
    Console.WriteLine("This statement isn't reached. (3)");
});

app.Run();
```

In the app's console window when the app is run:

> Work that can write to the response. (1)\
> Work that can write to the response. (2)\
> Work that doesn't write to the response. (2)\
> Work that doesn't write to the response. (1)

*Short-circuiting* the request pipeline is often desirable because it avoids unnecessary work. For example, [static file middleware](../static-files?view=aspnetcore-10.0) can act as a *terminal middleware* by processing a request for a static file and short-circuiting the rest of the pipeline. Middleware added to the pipeline before the terminal middleware still processes code after their `next.Invoke` statements. If you don't plan to call `next.Invoke` because your goal is to terminate the pipeline, use a [`Run` delegate](#run-delegate) instead of calling the [Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) extension method.

Don't call `next.Invoke` during or after the response is sent to the client. After an [HttpResponse](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse) is started, changes result in an exception. For example, [setting headers or a response status code throw an exception](../best-practices?view=aspnetcore-10.0#do-not-modify-the-status-code-or-headers-after-the-response-body-has-started) after the response starts. Writing to the response body after calling `next` may:

* Cause a protocol violation, such as writing more bytes to the response than the stated response's content length (`Content-Length` header value).
* Corrupt the body format, such as writing an HTML footer to a CSS file.

To determine if the response has started, check the value of [HasStarted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.hasstarted).

For more information, see [Short-circuit middleware after routing](../routing?view=aspnetcore-10.0#short-circuit-middleware-after-routing).

## `Run` delegate

A [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run) delegate doesn't receive a `next` parameter. The first `Run` delegate always terminates the pipeline. `Run` is also a convention, and some middleware might expose `Run` methods that execute at the end of the pipeline.

Any [Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) or [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run) delegates after the first [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run) delegate aren't called.

## Branch the middleware pipeline

[Map](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapextensions.map) extensions are used as a convention to branch the request processing pipeline. [Map](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapextensions.map) branches the request pipeline based on matches of the given request path. If the request path starts with the given path, the branch executes.

In the following example, `HandleMap1` is called for requests to `/map1`, and `HandleMap2` is called for requests to `/map2`:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1", HandleMap1);
app.Map("/map2", HandleMap2);

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from the non-Map delegate!");
});

app.Run();

private static void HandleMap1(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Map 1");
    });
}

private static void HandleMap2(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Map 2");
    });
}
```

The following table shows the requests and responses using the preceding code.

| Request | Response                         |
| ------- | -------------------------------- |
| `/`     | Hello from the non-Map delegate. |
| `/map1` | Map 1                            |
| `/map2` | Map 2                            |
| `/map3` | Hello from the non-Map delegate. |

When [Map](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapextensions.map) is used, the matched path segments are removed from [HttpRequest.Path](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.path) and appended to [HttpRequest.PathBase](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase) for each request.

[Map](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapextensions.map) can match multiple segments at once:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1/segment1", HandleMultipleSegments);

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from the non-Map delegate.");
});

app.Run();

private static void HandleMultipleSegments(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Processing '/map1/segment1'");
    });
}
```

The following table shows the requests and responses using the preceding code.

| Request          | Response                         |
| ---------------- | -------------------------------- |
| `/`              | Hello from the non-Map delegate. |
| `/map1/segment1` | Processing '/map1/segment1'      |

[Map](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapextensions.map) supports nesting:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/level1", level1App => {
    level1App.Map("/level2a", level2AApp => {
        level2AApp.Run(async context =>
        {
            await context.Response.WriteAsync("Processing '/level1/level2a'");
        });
    });
    level1App.Map("/level2b", level2BApp => {
        level2BApp.Run(async context =>
        {
            await context.Response.WriteAsync("Processing '/level1/level2b'");
        });
    });
});

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from the non-Map delegate!");
});

app.Run();
```

The following table shows the requests and responses using the preceding code.

| Request           | Response                         |
| ----------------- | -------------------------------- |
| `/`               | Hello from the non-Map delegate. |
| `/level1/level2a` | Processing '/level1/level2a'     |
| `/level1/level2b` | Processing '/level1/level2b'     |

[MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen) branches the request pipeline based on the result of the given predicate. Any predicate of type `Func<HttpContext, bool>` can be used to map requests to a new branch of the pipeline. In the following example, a predicate is used to detect the presence of a query string variable named "`branch`":

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from the non-Map delegate.");
});

app.Run();

private static void HandleBranch(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        var branchVer = context.Request.Query["branch"];
        await context.Response.WriteAsync($"Branch used = '{branchVer}'");
    });
}
```

The following table shows the requests and responses using the preceding code.

| Request         | Response                         |
| --------------- | -------------------------------- |
| `/`             | Hello from the non-Map delegate. |
| `/?branch=main` | Branch used = 'main'             |

[UseWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.usewhenextensions.usewhen) can branch the request pipeline based on the result of the given predicate. Unlike [MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen), the branch is rejoined to the main pipeline if it doesn't contain a terminal middleware:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
    appBuilder => HandleBranchAndRejoin(appBuilder));

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from the non-Map delegate.");
});

app.Run();

void HandleBranchAndRejoin(IApplicationBuilder app)
{
    var logger = app.ApplicationServices.GetRequiredService<ILogger<Program>>(); 

    app.Use(async (context, next) =>
    {
        var branchVer = context.Request.Query["branch"];
        logger.LogInformation("Branch used = {branchVer}", branchVer.ToString());

        Console.WriteLine("Work that can write to the response.");
        await next.Invoke(context);
        Console.WriteLine("Work that doesn't write to the response.");
    });
}
```

In the preceding example, a response of "Hello from the non-Map delegate." is written for all requests. If the request includes a query string variable named "`branch`," its value is logged before the main pipeline is rejoined.

## Create a middleware pipeline with `IApplicationBuilder`

The ASP.NET Core request pipeline consists of a sequence of request delegates, called one after the other. The following diagram demonstrates the concept. The thread of execution follows the black arrows.

Each delegate can perform operations before and after the next delegate. Exception-handling delegates should be called early in the pipeline, so they can catch exceptions that occur in later stages of the pipeline.

The simplest possible ASP.NET Core app sets up a single request delegate that handles all requests. This case doesn't include an actual request pipeline. Instead, a single anonymous function is called in response to every HTTP request.

```
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello, World!");
        });
    }
}
```

Chain multiple request delegates together with [Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use). The `next` parameter represents the next delegate in the pipeline. You can short-circuit the pipeline by *not* calling the *next* parameter. You can typically perform actions both before and after the next delegate, as the following example demonstrates:

```
app.Use(async (context, next) =>
{
    // Do work that doesn't write to the Response.
    await next.Invoke();
    // Do logging or other work that doesn't write to the Response.
});
```

When a delegate doesn't pass a request to the next delegate, it's called *short-circuiting the request pipeline*. Short-circuiting is often desirable because it avoids unnecessary work. For example, [static file middleware](../static-files?view=aspnetcore-10.0) can act as a *terminal middleware* by processing a request for a static file and short-circuiting the rest of the pipeline. Middleware added to the pipeline before the middleware that terminates further processing still processes code after their `next.Invoke` statements. However, see the following warning about attempting to write to a response that has already been sent.

Warning

Don't call `next.Invoke` after the response has been sent to the client. Changes to [HttpResponse](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse) after the response has started throw an exception. For example, [setting headers and a status code throw an exception](../best-practices?view=aspnetcore-10.0#do-not-modify-the-status-code-or-headers-after-the-response-body-has-started). Writing to the response body after calling `next`:

* May cause a protocol violation. For example, writing more than the stated `Content-Length`.
* May corrupt the body format. For example, writing an HTML footer to a CSS file.

[HasStarted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.hasstarted) is a useful hint to indicate if headers have been sent or the body has been written to.

[Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.runextensions.run) delegates don't receive a `next` parameter. The first `Run` delegate is always terminal and terminates the pipeline. `Run` is a convention. Some middleware components may expose `Run[Middleware]` methods that run at the end of the pipeline:

```
app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from 2nd delegate.");
});
```

In the preceding example, the `Run` delegate writes `"Hello from 2nd delegate."` to the response and then terminates the pipeline. If another `Use` or `Run` delegate is added after the `Run` delegate, it's not called.

## Branch the middleware pipeline

[Map](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapextensions.map) extensions are used as a convention for branching the pipeline. `Map` branches the request pipeline based on matches of the given request path. If the request path starts with the given path, the branch executes.

```
public class Startup
{
    private static void HandleMapTest1(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 1");
        });
    }

    private static void HandleMapTest2(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 2");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);

        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate.");
        });
    }
}
```

The following table shows the requests and responses from `http://localhost:1234` using the previous code.

| Request             | Response                     |
| ------------------- | ---------------------------- |
| localhost:1234      | Hello from non-Map delegate. |
| localhost:1234/map1 | Map Test 1                   |
| localhost:1234/map2 | Map Test 2                   |
| localhost:1234/map3 | Hello from non-Map delegate. |

When `Map` is used, the matched path segments are removed from `HttpRequest.Path` and appended to `HttpRequest.PathBase` for each request.

`Map` supports nesting, for example:

```
app.Map("/level1", level1App => {
    level1App.Map("/level2a", level2AApp => {
        // "/level1/level2a" processing
    });
    level1App.Map("/level2b", level2BApp => {
        // "/level1/level2b" processing
    });
});
```

`Map` can also match multiple segments at once:

```
public class Startup
{
    private static void HandleMultiSeg(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map multiple segments.");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1/seg1", HandleMultiSeg);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate.");
        });
    }
}
```

[MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen) branches the request pipeline based on the result of the given predicate. You can use any predicate of type `Func<HttpContext, bool>` to map requests to a new branch of the pipeline. In the following example, a predicate detects the presence of a query string variable `branch`:

```
public class Startup
{
    private static void HandleBranch(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var branchVer = context.Request.Query["branch"];
            await context.Response.WriteAsync($"Branch used = {branchVer}");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
                               HandleBranch);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate.");
        });
    }
}
```

The following table shows the requests and responses from `http://localhost:1234` using the previous code.

| Request                     | Response                     |
| --------------------------- | ---------------------------- |
| localhost:1234              | Hello from non-Map delegate. |
| localhost:1234/?branch=main | Branch used = main           |

[UseWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.usewhenextensions.usewhen) also branches the request pipeline based on the result of the given predicate. Unlike with `MapWhen`, this branch is rejoined to the main pipeline if it doesn't short-circuit or contain a terminal middleware:

```
public class Startup
{
    private void HandleBranchAndRejoin(IApplicationBuilder app, ILogger<Startup> logger)
    {
        app.Use(async (context, next) =>
        {
            var branchVer = context.Request.Query["branch"];
            logger.LogInformation("Branch used = {branchVer}", branchVer.ToString());

            // Do work that doesn't write to the Response.
            await next();
            // Do other work that doesn't write to the Response.
        });
    }

    public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
    {
        app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
                               appBuilder => HandleBranchAndRejoin(appBuilder, logger));

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from main pipeline.");
        });
    }
}
```

In the preceding example, a response of "Hello from main pipeline." is written for all requests. If the request includes a query string variable `branch`, its value is logged before the main pipeline is rejoined.

## Middleware added automatically by `WebApplication`

[`WebApplication`](../minimal-apis/webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should be run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case as it is automatically added as described previously:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.
* The app needs to call `UseRouting` and `UseEndpoints` so that the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

[WebApplication](../minimal-apis/webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// User middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// End user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case because it's automatically added as described earlier:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// Other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.

* The app needs to call `UseRouting` and `UseEndpoints` so the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

For information on antiforgery middleware in Minimal APIs, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0#afwma).

## Middleware order

The order that middleware appears in the app's `Program` file defines the order in which middleware are invoked on a request with the reverse order for the response.

You have full control over the order of middleware and the ability to add custom middleware for request processing scenarios, keeping in mind that the order of middleware can be critical for security, performance, and functionality.

The following examples demonstrate middleware order for common app scenarios. Each middleware extension method is exposed on [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) through the [Microsoft.AspNetCore.Builder](/en-us/dotnet/api/microsoft.aspnetcore.builder) namespace:

1. Exception and error handling

   * When the app runs in the `Development` environment:

     * Developer exception page middleware ([UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage)) reports app runtime errors.
     * Database error page middleware ([UseDatabaseErrorPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.databaseerrorpageextensions.usedatabaseerrorpage)) reports database runtime errors.

   * When the app runs in the `Production` environment:

     * Exception handler middleware ([UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)) catches exceptions thrown in the following middlewares.
     * HTTP Strict Transport Security (HSTS) protocol middleware ([UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)) adds the `Strict-Transport-Security` header.

2. HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) redirects HTTP requests to HTTPS.

3. Static file middleware (if required, [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)) returns static files and short-circuits further request processing.

4. Cookie policy middleware ([UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy)) conforms the app to the EU General Data Protection Regulation (GDPR).

5. Routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) to route requests.

6. Authentication middleware ([UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)) attempts to authenticate the user before they're allowed access to secure resources.

7. Authorization middleware ([UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)) authorizes a user to access secure resources.

8. Antiforgery middleware ([UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery)) adds antiforgery middleware to the pipeline [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) must be placed after calls to [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization).

9. Session middleware (Razor Pages and MVC only, [UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)) establishes and maintains session state. If the app uses session state, call session middleware after cookie policy middleware and before Razor Pages/MVC middleware.

10. Endpoint routing middleware

* [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) to add Razor component endpoints to the request pipeline.
* [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages) to add Razor Pages endpoints to the request pipeline.
* [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) to add controller endpoints to the request pipeline.

Typical Blazor Web App middleware pipeline:

```
app.UseWebAssemblyDebugging(); // Development environment with client-side rendering
app.UseMigrationsEndPoint(); // Development environment with ASP.NET Core Identity

app.UseExceptionHandler("/Error", createScopeForErrors: true); // Non-Development environment
app.UseHsts(); // Non-Development environment with HTTPS protocol

app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);

app.UseHttpsRedirection(); // With HTTPS protocol

app.UseAntiforgery();

app.MapStaticAssets();

app.MapRazorComponents<App>(); // With additional extension methods for render modes

app.MapAdditionalIdentityEndpoints(); // With ASP.NET Core Identity

app.Run();
```

Typical Razor Pages/MVC middleware pipeline:

```
app.UseMigrationsEndPoint(); // Development environment with ASP.NET Core Identity

app.UseExceptionHandler("/Error"); // Non-Development environment
app.UseHsts(); // Non-Development environment with HTTPS protocol

app.UseHttpsRedirection(); // With HTTPS protocol

// app.UseCookiePolicy();
app.UseRouting(); // If not called, runs at the beginning of the pipeline by default
// app.UseRateLimiter();
// app.UseRequestLocalization();
// app.UseCors();

// app.UseAuthentication(); // Called internally for ASP.NET Core Identity
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();

app.MapStaticAssets();

app.MapControllerRoute(...); // For MVC controllers

app.MapRazorPages(); // For Razor Pages pages

app.MapControllers(); // With authentication in a Razor Pages app

app.Run();
```

In the preceding code:

* CORS middleware ([UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors)), authentication middleware ([UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)), and authorization middleware ([UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)) must appear in the order shown.
* CORS middleware ([UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors)) must appear before response caching middleware ([UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching)) to add CORS headers on every request, including cached responses. For more information, see [It is not clear that UseCORS must come before UseResponseCaching (`dotnet/aspnetcore` #23218](https://github.com/dotnet/aspnetcore/issues/23218).
* Request localization middleware ([UseRequestLocalization](/en-us/dotnet/api/microsoft.aspnetcore.builder.applicationbuilderextensions.userequestlocalization)) must appear before any middleware that might check the request culture, for example, static file middleware ([UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)).
* Rate-limiting middleware ([UseRateLimiter](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterapplicationbuilderextensions.useratelimiter)) must be called after routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) when rate limiting endpoint-specific APIs are used. For example, if the [`[EnableRateLimiting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.enableratelimitingattribute) is used, rate-limiting middleware must be called after routing middleware. When calling only global limiters, rate-limiting middleware can be called before routing middleware.

In some scenarios, middleware has different ordering. For example, caching and compression ordering depends on the app's specification. In the following order, CPU usage might be reduced by caching the compressed response, but the app might end up caching multiple representations of a resource using different compression algorithms, such as Gzip or Brotli:

```
app.UseResponseCaching();
app.UseResponseCompression();
```

Static assets are typically served early in the pipeline so that the app can short-circuit request processing to improve performance.

Authentication doesn't short-circuit unauthenticated requests. Although authentication middleware authenticates requests, authorization occurs after the framework selects a Razor component in a Blazor Web App, a page in a Razor Pages app, or a controller and action in an MVC app.

The following diagram shows the complete request processing pipeline for ASP.NET Core MVC and Razor Pages apps. You can see how, in a typical app, existing middlewares are ordered and where custom middlewares are added. You have full control over how to reorder existing middlewares or inject new custom middlewares as necessary for your scenarios.

The **Endpoint** middleware in the preceding diagram executes the filter pipeline for the corresponding app type—MVC or Razor Pages.

The preceding diagram shows the **Routing** middleware following **Static Files**. This order is how the project templates work by explicitly calling [app.UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting). If you don't call `app.UseRouting`, the **Routing** middleware runs at the beginning of the pipeline by default. For more information, see [Routing](../routing?view=aspnetcore-10.0).

The order you add middleware components in the `Program.cs` file defines the order in which the middleware components are invoked on requests and the reverse order for the response. The order is **critical** for security, performance, and functionality.

The following highlighted code in `Program.cs` adds security-related middleware components in the typical recommended order:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMiddleware.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
    ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();

app.UseRouting();
// app.UseRateLimiter();
// app.UseRequestLocalization();
// app.UseCors();

app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();
```

In the preceding code:

* Commented out middleware isn't added when you create a new web app with [individual users accounts](../../security/authentication/identity?view=aspnetcore-10.0).

* Not every middleware appears in this exact order, but many do. For example:

  * `UseCors`, `UseAuthentication`, and `UseAuthorization` must appear in the order shown.
  * `UseCors` currently must appear before `UseResponseCaching`. This requirement is explained in [GitHub issue dotnet/aspnetcore #23218](https://github.com/dotnet/aspnetcore/issues/23218).
  * `UseRequestLocalization` must appear before any middleware that might check the request culture, for example, `app.UseStaticFiles()`.
  * [UseRateLimiter](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterapplicationbuilderextensions.useratelimiter) must be called after `UseRouting` when rate limiting endpoint specific APIs are used. For example, if the [`[EnableRateLimiting]`](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.enableratelimitingattribute) attribute is used, `UseRateLimiter` must be called after `UseRouting`. When calling only global limiters, `UseRateLimiter` can be called before `UseRouting`.

In some scenarios, middleware has different ordering. For example, caching and compression ordering is scenario specific, and there are multiple valid orderings. For example:

```
app.UseResponseCaching();
app.UseResponseCompression();
```

With the preceding code, you could reduce CPU usage by caching the compressed response, but you might end up caching multiple representations of a resource using different compression algorithms such as Gzip or Brotli.

The following ordering combines static files to allow caching compressed static files:

```
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();
```

The following `Program.cs` code adds middleware components for common app scenarios:

1. Exception and error handling

   * When the app runs in the `Development` environment:

     * Developer exception page middleware ([UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage)) reports app runtime errors.
     * Database error page middleware ([UseDatabaseErrorPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.databaseerrorpageextensions.usedatabaseerrorpage)) reports database runtime errors.

   * When the app runs in the `Production` environment:

     * Exception handler middleware ([UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)) catches exceptions thrown in the following middlewares.
     * HTTP Strict Transport Security (HSTS) protocol middleware ([UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)) adds the `Strict-Transport-Security` header.

2. HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) redirects HTTP requests to HTTPS.

3. Static file middleware ([UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)) returns static files and short-circuits further request processing.

4. Cookie policy middleware ([UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy)) conforms the app to the EU General Data Protection Regulation (GDPR) regulations.

5. Routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) to route requests.

6. Authentication middleware ([UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)) attempts to authenticate the user before they're allowed access to secure resources.

7. Authorization middleware ([UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)) authorizes a user to access secure resources.

8. Session middleware ([UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)) establishes and maintains session state. If the app uses session state, call session middleware after cookie policy middleware and before MVC middleware.

9. Endpoint routing middleware ([UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) with [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)) to add Razor Pages endpoints to the request pipeline.

```
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseDatabaseErrorPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();
```

In the preceding example code, each middleware extension method is exposed on [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) through the [Microsoft.AspNetCore.Builder](/en-us/dotnet/api/microsoft.aspnetcore.builder) namespace.

[UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) is the first middleware component added to the pipeline. Therefore, the exception handler middleware catches any exceptions that occur in later calls.

Static file middleware is called early in the pipeline so that it can handle requests and short-circuit without going through the remaining components. The static file middleware provides **no** authorization checks. Any files served by static file middleware, including those under *wwwroot*, are publicly available. For an approach to secure static files, see [Serve static files in ASP.NET Core apps](../static-files?view=aspnetcore-10.0).

If the request isn't handled by the static file middleware, it's passed on to the authentication middleware ([UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)), which performs authentication. Authentication doesn't short-circuit unauthenticated requests. Although authentication middleware authenticates requests, authorization (and rejection) occurs only after MVC selects a specific Razor Page or MVC controller and action.

The following example demonstrates a middleware order where requests for static files are handled by static file middleware before response compression middleware. Static files aren't compressed with this middleware order. The Razor Pages responses can be compressed.

```
// Static files aren't compressed by static file middleware.
app.UseStaticFiles();

app.UseRouting();

app.UseResponseCompression();

app.MapRazorPages();
```

The following diagram shows the complete request processing pipeline for ASP.NET Core MVC and Razor Pages apps. You can see how, in a typical app, existing middlewares are ordered and where custom middlewares are added. You have full control over how to reorder existing middlewares or inject new custom middlewares as necessary for your scenarios.

The **Endpoint** middleware in the preceding diagram executes the filter pipeline for the corresponding app type—MVC or Razor Pages.

The preceding diagram shows the **Routing** middleware following **Static Files**. This order is how the project templates work by explicitly calling [app.UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting). If you don't call `app.UseRouting`, the **Routing** middleware runs at the beginning of the pipeline by default. For more information, see [Routing](../routing?view=aspnetcore-10.0).

The order you add middleware components in the `Program.cs` file defines the order in which the middleware components are invoked on requests and the reverse order for the response. The order is **critical** for security, performance, and functionality.

The following highlighted code in `Program.cs` adds security-related middleware components in the typical recommended order:

```
using IndividualAccountsExample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();

app.UseRouting();
// app.UseRequestLocalization();
// app.UseCors();

app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();

app.MapRazorPages();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

In the preceding code:

* Commented out middleware isn't added when you create a new web app with [individual users accounts](../../security/authentication/identity?view=aspnetcore-10.0).

* Not every middleware appears in this exact order, but many do. For example:

  * `UseCors`, `UseAuthentication`, and `UseAuthorization` must appear in the order shown.
  * `UseCors` currently must appear before `UseResponseCaching`. This requirement is explained in [GitHub issue dotnet/aspnetcore #23218](https://github.com/dotnet/aspnetcore/issues/23218).
  * `UseRequestLocalization` must appear before any middleware that might check the request culture (for example, `app.UseMvcWithDefaultRoute()`).

In some scenarios, middleware has different ordering. For example, caching and compression ordering is scenario specific, and there are multiple valid orderings. For example:

```
app.UseResponseCaching();
app.UseResponseCompression();
```

With the preceding code, you could reduce CPU usage by caching the compressed response, but you might end up caching multiple representations of a resource using different compression algorithms such as Gzip or Brotli.

The following ordering combines static files to allow caching compressed static files:

```
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();
```

The following `Program.cs` code adds middleware components for common app scenarios:

1. Exception and error handling

   * When the app runs in the `Development` environment:

     * Developer exception page middleware ([UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage)) reports app runtime errors.
     * Database error page middleware ([UseDatabaseErrorPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.databaseerrorpageextensions.usedatabaseerrorpage)) reports database runtime errors.

   * When the app runs in the `Production` environment:

     * Exception handler middleware ([UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)) catches exceptions thrown in the following middlewares.
     * HTTP Strict Transport Security (HSTS) protocol middleware ([UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)) adds the `Strict-Transport-Security` header.

2. HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) redirects HTTP requests to HTTPS.

3. Static file middleware ([UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)) returns static files and short-circuits further request processing.

4. Cookie policy middleware ([UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy)) conforms the app to the EU General Data Protection Regulation (GDPR) regulations.

5. Routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) to route requests.

6. Authentication middleware ([UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)) attempts to authenticate the user before they're allowed access to secure resources.

7. Authorization middleware ([UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)) authorizes a user to access secure resources.

8. Session middleware ([UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)) establishes and maintains session state. If the app uses session state, call session middleware after cookie policy middleware and before MVC middleware.

9. Endpoint routing middleware ([UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) with [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)) to add Razor Pages endpoints to the request pipeline.

```
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseDatabaseErrorPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();
```

In the preceding example code, each middleware extension method is exposed on [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) through the [Microsoft.AspNetCore.Builder](/en-us/dotnet/api/microsoft.aspnetcore.builder) namespace.

[UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) is the first middleware component added to the pipeline. Therefore, the exception handler middleware catches any exceptions that occur in later calls.

Static file middleware is called early in the pipeline so that it can handle requests and short-circuit without going through the remaining components. The static file middleware provides **no** authorization checks. Any files served by static file middleware, including those under *wwwroot*, are publicly available. For an approach to secure static files, see [Serve static files in ASP.NET Core apps](../static-files?view=aspnetcore-10.0).

If the request isn't handled by the static file middleware, it's passed on to the authentication middleware ([UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)), which performs authentication. Authentication doesn't short-circuit unauthenticated requests. Although authentication middleware authenticates requests, authorization (and rejection) occurs only after MVC selects a specific Razor Page or MVC controller and action.

The following example demonstrates a middleware order where requests for static files are handled by static file middleware before response compression middleware. Static files aren't compressed with this middleware order. The Razor Pages responses can be compressed.

```
// Static files aren't compressed by static file middleware.
app.UseStaticFiles();

app.UseRouting();

app.UseResponseCompression();

app.MapRazorPages();
```

The following diagram shows the complete request processing pipeline for ASP.NET Core MVC and Razor Pages apps. You can see how, in a typical app, existing middlewares are ordered and where custom middlewares are added. You have full control over how to reorder existing middlewares or inject new custom middlewares as necessary for your scenarios.

The **Endpoint** middleware in the preceding diagram executes the filter pipeline for the corresponding app type—MVC or Razor Pages.

The order that middleware components are added in the `Startup.Configure` method defines the order in which the middleware components are invoked on requests and the reverse order for the response. The order is **critical** for security, performance, and functionality.

The following `Startup.Configure` method adds security-related middleware components in the typical recommended order:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    // app.UseCookiePolicy();

    app.UseRouting();
    // app.UseRequestLocalization();
    // app.UseCors();

    app.UseAuthentication();
    app.UseAuthorization();
    // app.UseSession();
    // app.UseResponseCompression();
    // app.UseResponseCaching();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

In the preceding code:

* Commented out middleware isn't added when you create a new web app with [individual users accounts](../../security/authentication/identity?view=aspnetcore-10.0).

* Not every middleware appears in this exact order, but many do. For example:

  * `UseCors`, `UseAuthentication`, and `UseAuthorization` must appear in the order shown.
  * `UseCors` currently must appear before `UseResponseCaching` due to [this bug](https://github.com/dotnet/aspnetcore/issues/23218).
  * `UseRequestLocalization` must appear before any middleware that might check the request culture (for example, `app.UseMvcWithDefaultRoute()`).

In some scenarios, middleware has different ordering. For example, caching and compression ordering is scenario specific, and there's multiple valid orderings. For example:

```
app.UseResponseCaching();
app.UseResponseCompression();
```

With the preceding code, CPU could be saved by caching the compressed response, but you might end up caching multiple representations of a resource using different compression algorithms such as Gzip or Brotli.

The following ordering combines static files to allow caching compressed static files:

```
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();
```

The following `Startup.Configure` method adds middleware components for common app scenarios:

1. Exception and error handling

   * When the app runs in the `Development` environment:

     * Developer exception page middleware ([UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage)) reports app runtime errors.
     * Database error page middleware reports database runtime errors.

   * When the app runs in the `Production` environment:

     * Exception handler middleware ([UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)) catches exceptions thrown in the following middlewares.
     * HTTP Strict Transport Security (HSTS) protocol middleware ([UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)) adds the `Strict-Transport-Security` header.

2. HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) redirects HTTP requests to HTTPS.

3. Static file middleware ([UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)) returns static files and short-circuits further request processing.

4. Cookie policy middleware ([UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy)) conforms the app to the EU General Data Protection Regulation (GDPR) regulations.

5. Routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) to route requests.

6. Authentication middleware ([UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)) attempts to authenticate the user before they're allowed access to secure resources.

7. Authorization middleware ([UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)) authorizes a user to access secure resources.

8. Session middleware ([UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)) establishes and maintains session state. If the app uses session state, call session middleware after cookie policy middleware and before MVC middleware.

9. Endpoint routing middleware ([UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) with [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)) to add Razor Pages endpoints to the request pipeline.

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseSession();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

In the preceding example code, each middleware extension method is exposed on [IApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder) through the [Microsoft.AspNetCore.Builder](/en-us/dotnet/api/microsoft.aspnetcore.builder) namespace.

[UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) is the first middleware component added to the pipeline. Therefore, the exception handler middleware catches any exceptions that occur in later calls.

Static file middleware is called early in the pipeline so that it can handle requests and short-circuit without going through the remaining components. The static file middleware provides **no** authorization checks. Any files served by static file middleware, including those under *wwwroot*, are publicly available. For an approach to secure static files, see [Serve static files in ASP.NET Core apps](../static-files?view=aspnetcore-10.0).

If the request isn't handled by the static file middleware, it's passed on to the authentication middleware ([UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)), which performs authentication. Authentication doesn't short-circuit unauthenticated requests. Although authentication middleware authenticates requests, authorization (and rejection) occurs only after MVC selects a specific Razor Page or MVC controller and action.

The following example demonstrates a middleware order where requests for static files are handled by static file middleware before response compression middleware. Static files aren't compressed with this middleware order. The Razor Pages responses can be compressed.

```
public void Configure(IApplicationBuilder app)
{
    // Static files aren't compressed by static file middleware.
    app.UseStaticFiles();

    app.UseRouting();

    app.UseResponseCompression();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

For Single Page Applications (SPAs), the SPA middleware [UseSpaStaticFiles](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.spastaticfilesextensions.usespastaticfiles) usually comes last in the middleware pipeline. The SPA middleware comes last:

* To allow all other middleware to respond to matching requests first.
* To allow SPAs with client-side routing to run for all routes that the server app doesn't recognize.

For more details on SPAs, see the guides for the [React](../../client-side/spa/react?view=aspnetcore-10.0) and [Angular](../../client-side/spa/angular?view=aspnetcore-10.0) project templates.

For information about Single Page Applications, see the guides for the [React](../../client-side/spa/react?view=aspnetcore-10.0) and [Angular](../../client-side/spa/angular?view=aspnetcore-10.0) project templates.

## `UseCors` and `UseStaticFiles` order

For more information on ordering [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) and [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../../security/cors?view=aspnetcore-10.0#usecors-and-usestaticfiles-order).

## Forwarded headers middleware order

Run forwarded headers middleware before other middleware to ensure that the middleware relying on forwarded headers information can consume the header values for processing. To run forwarded headers middleware after diagnostics and error handling middleware, see [forwarded headers middleware order](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0#forwarded-headers-middleware-order).

## Built-in middleware

The latest release of ASP.NET Core includes the following middleware. The *UI stack* column shows the typical UI stack where the middleware is used: \[All, Blazor Web App (BWA), Razor Pages and MVC (RP/MVC)]. The *Order* column provides notes on middleware placement in the request processing pipeline and under what conditions the middleware might stop request processing. When a middleware short-circuits the request processing pipeline and prevents further downstream middleware from processing a request, it's called a *terminal middleware*. For more information about short-circuiting, see the [Create a middleware pipeline with `WebApplication`](#create-a-middleware-pipeline-with-webapplication) section.

| Middleware                                                                                                                              | Description                                                                                                                                         | UI stack | Order                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Antiforgery](../../security/anti-request-forgery?view=aspnetcore-10.0)                                                                 | Provides anti-request-forgery support.                                                                                                              | All      | After authentication and authorization, before endpoints.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| [Authentication](../../security/authentication/identity?view=aspnetcore-10.0)                                                           | Provides authentication support.                                                                                                                    | All      | Before `HttpContext.User` is required. Terminal for OAuth callbacks.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| [Authorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)                      | Provides authorization support.                                                                                                                     | All      | Immediately after the authentication middleware.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| [Cookie Policy](../../security/gdpr?view=aspnetcore-10.0)                                                                               | Tracks consent from users for storing personal information and enforces minimum standards for cookie fields, such as `secure` and `SameSite`.       | All      | Before middleware that issues cookies. Examples: Authentication, Session, MVC (TempData).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| [CORS](../../security/cors?view=aspnetcore-10.0)                                                                                        | Configures Cross-Origin Resource Sharing.                                                                                                           | All      | Before middleware that use CORS. [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) must go before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching). For more information, see [It is not clear that UseCORS must come before UseResponseCaching (`dotnet/aspnetcore` #23218](https://github.com/dotnet/aspnetcore/issues/23218).                                                                                                                                                                                                   |
| [Developer Exception Page](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.developerexceptionpagemiddleware)                         | Generates a page with error information that's intended for use only in the `Development` environment.                                              | All      | Before middleware that generate errors. The project templates automatically register this middleware as the first middleware in the pipeline when the environment is `Development`.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| [Diagnostics](../error-handling?view=aspnetcore-10.0)                                                                                   | Several separate middlewares that provide a developer exception page, exception handling, status code pages, and the default web page for new apps. | All      | Before middleware that generate errors. Terminal for exceptions or serving the default web page for new apps.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| [Forwarded Headers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)                                                     | Forwards proxied headers onto the current request.                                                                                                  | All      | Before middleware that consume the updated fields. Examples: scheme, host, client IP, method.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| [Health Check](../../host-and-deploy/health-checks?view=aspnetcore-10.0)                                                                | Checks the health of an ASP.NET Core app and its dependencies, such as checking database availability.                                              | All      | Terminal if a request matches a health check endpoint.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| [Header Propagation](../http-requests?view=aspnetcore-10.0#header-propagation-middleware)                                               | Propagates HTTP headers from the incoming request to the outgoing HTTP Client requests.                                                             |          |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| All                                                                                                                                     |                                                                                                                                                     |          |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| [HTTP Logging](../http-logging/?view=aspnetcore-10.0)                                                                                   | Logs HTTP Requests and Responses.                                                                                                                   | All      | At the beginning of the middleware pipeline.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| [HTTP Method Override](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpmethodoverrideextensions)                                     | Allows an incoming POST request to override the method.                                                                                             | All      | Before middleware that consume the updated method.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| [HTTPS Redirection](../../security/enforcing-ssl?view=aspnetcore-10.0#require-https)                                                    | Redirects all HTTP requests to HTTPS.                                                                                                               | All      | Before middleware that consume the URL.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| [HTTP Strict Transport Security (HSTS)](../../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) | Security enhancement middleware that adds a special response header.                                                                                | All      | Before responses are sent and after middleware that modify requests. Examples: Forwarded Headers, URL Rewriting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| [MVC](../../mvc/overview?view=aspnetcore-10.0)                                                                                          | Processes requests with MVC and Razor Pages.                                                                                                        | RP/MVC   | Terminal if a request matches a route.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| [OWIN](../owin?view=aspnetcore-10.0)                                                                                                    | Interop with OWIN-based apps, servers, and middleware.                                                                                              | RP/MVC   | Terminal if the OWIN middleware fully processes the request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| [Output Caching](../../performance/caching/output?view=aspnetcore-10.0)                                                                 | Provides support for caching responses based on configuration.                                                                                      | RP/MVC   | Before middleware that require caching. [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting), [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors), [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication), and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) must come before [UseOutputCache](/en-us/dotnet/api/microsoft.aspnetcore.builder.outputcacheapplicationbuilderextensions.useoutputcache). |
| [Response Caching](../../performance/caching/middleware?view=aspnetcore-10.0)                                                           | Provides support for caching responses. This middleware requires client participation to work. Use output caching for complete server control.      | RP/MVC   | Before middleware that require caching. [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) must come before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching). Response caching isn't typically beneficial for UI apps, such as Razor Pages, because browsers generally set request headers that prevent caching. [Output caching](../../performance/caching/output?view=aspnetcore-10.0) benefits UI apps.                                                                                                                          |
| [Request Decompression](request-decompression?view=aspnetcore-10.0)                                                                     | Provides support for decompressing requests.                                                                                                        | All      | Before middleware that read the request body.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| [Response Compression](../../performance/response-compression?view=aspnetcore-10.0)                                                     | Provides support for compressing responses.                                                                                                         | All      | Before middleware that require compression.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| [Request Localization](../localization?view=aspnetcore-10.0)                                                                            | Provides localization support.                                                                                                                      | All      | Before localization sensitive middleware. Must appear after routing middleware when using [RouteDataRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.routing.routedatarequestcultureprovider).                                                                                                                                                                                                                                                                                                                                                                                                                    |
| [Request Timeouts](../../performance/timeouts?view=aspnetcore-10.0)                                                                     | Provides support for configuring request timeouts, global and per endpoint.                                                                         | All      | [UseRequestTimeouts](/en-us/dotnet/api/microsoft.aspnetcore.builder.requesttimeoutsiapplicationbuilderextensions.userequesttimeouts) must come after [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler), [UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage), and [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting).                                                                                                  |
| [Endpoint Routing](../routing?view=aspnetcore-10.0).                                                                                    | Defines and constrains request routes.                                                                                                              | All      | Terminal for matching routes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| [SPA](/en-us/dotnet/api/microsoft.aspnetcore.builder.spaapplicationbuilderextensions.usespa)                                            | Handles all requests from this point in the middleware chain by returning the default page for the Single Page Application (SPA).                   | All      | Appears late in the pipeline, so other middleware for serving static files, such as MVC actions, take precedence.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| [Session](../app-state?view=aspnetcore-10.0)                                                                                            | Provides support for managing user sessions.                                                                                                        | RP/MVC   | Before middleware that require Session.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| [Static File](../static-files?view=aspnetcore-10.0)                                                                                     | Provides support for serving static files and directory browsing.                                                                                   | All      | Terminal if a request matches a file.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| [URL Rewrite](../url-rewriting?view=aspnetcore-10.0)                                                                                    | Provides support for rewriting URLs and redirecting requests.                                                                                       | All      | Before middleware that consume the URL.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| [W3C Logging](../w3c-logger/?view=aspnetcore-10.0)                                                                                      | Generates server access logs in the [W3C Extended Log File Format](https://www.w3.org/TR/WD-logfile.html).                                          | All      | At the beginning of the middleware pipeline.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| [Blazor WebAssembly Debugging](../../blazor/debug?view=aspnetcore-10.0)                                                                 | Debugs Blazor Web Apps that use client-side rendering (CSR) inside Chromium developer tools.                                                        | BWA      | At the beginning of the middleware pipeline.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| [WebSockets](../websockets?view=aspnetcore-10.0)                                                                                        | Enables the WebSockets protocol.                                                                                                                    | All      | Before middleware that are required to accept WebSocket requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |

## Related content

* [Lifetime and registration options (includes middleware sample)](../dependency-injection?view=aspnetcore-10.0#lifetime-and-registration-options)
* [Write custom ASP.NET Core middleware](write?view=aspnetcore-10.0)
* [Test ASP.NET Core middleware](../../test/middleware?view=aspnetcore-10.0)
* [Configure gRPC-Web in ASP.NET Core](../../grpc/browser?view=aspnetcore-10.0#configure-grpc-web-in-aspnet-core)
* [Migrate HTTP modules to ASP.NET Core middleware](../../migration/fx-to-core/areas/http-modules?view=aspnetcore-10.0)
* [App startup in ASP.NET Core](../startup?view=aspnetcore-10.0)
* [Request Features in ASP.NET Core](../request-features?view=aspnetcore-10.0)
* [Factory-based middleware activation in ASP.NET Core](extensibility?view=aspnetcore-10.0)
* [Middleware activation with a third-party container in ASP.NET Core](extensibility-third-party-container?view=aspnetcore-10.0)
* [APIs overview](../apis?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-19

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/mvc1000
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/mvc1000.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# MVC1000: Use of IHtmlHelper.Partial should be avoided

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | MVC1000      |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

The [Partial](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.partial) or [RenderPartial](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.renderpartial) method was called or referenced.

### Rule description

Rendering a partial using `IHtmlHelper.Partial` or `IHtmlHelper.RenderPartial` extension methods results in blocking calls. This may result in performance degradation and application dead locks issues due to thread pool starvation.

## How to fix violations

* Use the [PartialTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.partialtaghelper)
* Use the [PartialAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.partialasync) or [RenderPartialAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.htmlhelperpartialextensions.renderpartialasync)

## When to suppress warnings

It's safe to suppress this rule if updating the application to use the suggested fixes is non-trivial. Before the validation is disabled, be sure to consider the risks of thread pool starvation to the application.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/security
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/security.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Security considerations in gRPC for ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

This article provides information on securing gRPC with .NET.

## Transport security

gRPC messages are sent and received using HTTP/2. We recommend:

* [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246) be used to secure messages in production gRPC apps.
* gRPC services should only listen and respond over secured ports.

TLS is configured in Kestrel. For more information on configuring Kestrel endpoints, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

TLS is configured in Kestrel. For more information on configuring Kestrel endpoints, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration).

A [TLS termination proxy](https://wikipedia.org/wiki/TLS_termination_proxy) can be combined with TLS. The benefits of using TLS termination should be considered against the security risks of sending unsecured HTTP requests between apps in the private network.

## Exceptions

Exception messages are generally considered sensitive data that shouldn't be revealed to a client. By default, gRPC doesn't send the details of an exception thrown by a gRPC service to the client. Instead, the client receives a generic message indicating an error occurred. Exception message delivery to the client can be overridden (for example, in development or test) with [EnableDetailedErrors](configuration?view=aspnetcore-10.0#configure-services-options). Exception messages shouldn't be exposed to the client in production apps.

## Message size limits

Incoming messages to gRPC clients and services are loaded into memory. Message size limits are a mechanism to help prevent gRPC from consuming excessive resources.

gRPC uses per-message size limits to manage incoming and outgoing messages. By default, gRPC limits incoming messages to 4 MB. There is no limit on outgoing messages.

On the server, gRPC message limits can be configured for all services in an app with `AddGrpc`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.MaxReceiveMessageSize = 1 * 1024 * 1024; // 1 MB
        options.MaxSendMessageSize = 1 * 1024 * 1024; // 1 MB
    });
}
```

Limits can also be configured for an individual service using `AddServiceOptions<TService>`. For more information on configuring message size limits, see [gRPC configuration](configuration?view=aspnetcore-10.0).

## Client certificate validation

[Client certificates](https://tools.ietf.org/html/rfc5246#section-7.4.4) are initially validated when the connection is established. By default, Kestrel doesn't perform additional validation of a connection's client certificate.

We recommend that gRPC services secured by client certificates use the [Microsoft.AspNetCore.Authentication.Certificate](../security/authentication/certauth?view=aspnetcore-10.0) package. ASP.NET Core certification authentication will perform additional validation on a client certificate, including:

* Certificate has a valid extended key use (EKU)
* Is within its validity period
* Check certificate revocation

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0013
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0013.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0013: Suggest switching from using Configure methods to WebApplicationBuilder.Configuration

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0013      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

[Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configure) isn't the recommended strategy for reading and writing to configuration in a Minimal API app. `Configure` was designed to be used with [Web Host](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) or [.NET Generic Host](/en-us/dotnet/core/extensions/generic-host). In a Minimal API app, [WebApplicationBuilder.Configuration](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.configuration#microsoft-aspnetcore-builder-webapplicationbuilder-configuration) should be used to modify configuration directly.

## Rule description

`Configure` isn't the recommended strategy for configuring logging in a Minimal API app.

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.ConfigureAppConfiguration(builder =>
{
    builder.AddJsonFile("customAppSettings.json");
})

var app = builder.Build();

app.Run();
```

## How to fix violations

To fix a violation of this rule, use [WebApplicationBuilder.Configuration](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.configuration#microsoft-aspnetcore-builder-webapplicationbuilder-configuration) to modify application configuration directly without the need for an additional [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration) call.

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("customAppSettings.json");

var app = builder.Build();

app.Run();
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/native-aot
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/native-aot.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC and Native AOT

By [James Newton-King](https://twitter.com/jamesnk)

gRPC supports [.NET native ahead-of-time (AOT)](/en-us/dotnet/core/deploying/native-aot/) in .NET 8. Native AOT enables publishing gRPC client and server apps as small, fast native executables.

Warning

In .NET 8, not all ASP.NET Core features are compatible with Native AOT. For more information, see [ASP.NET Core and Native AOT compatibility](../fundamentals/native-aot?view=aspnetcore-10.0#aspnet-core-and-native-aot-compatibility).

## Get started

AOT compilation happens when the app is published. Native AOT is enabled with the `PublishAot` option.

1. Add `<PublishAot>true</PublishAot>` to the gRPC client or server app's project file. This will enable Native AOT compilation during publish and enable dynamic code usage analysis during build and editing.

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

     <PropertyGroup>
       <TargetFramework>net8.0</TargetFramework>
       <PublishAot>true</PublishAot>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Grpc.AspNetCore" Version="2.51.0" />
       <PackageReference Include="Google.Protobuf" Version="3.22.0" />
     </ItemGroup>

   </Project>
   ```

   Native AOT can also be enabled by specifying the `--aot` option with the ASP.NET Core gRPC template:

   ```
   dotnet new grpc --aot
   ```

2. Publish the app for a specific [runtime identifier (RID)](/en-us/dotnet/core/rid-catalog) using `dotnet publish -r <RID>`.

The app is available in the publish directory and contains all the code needed to run in it.

Native AOT analysis includes all of the app's code and the libraries the app depends on. Review Native AOT warnings and take corrective steps. It's a good idea to test publishing apps frequently to discover issues early in the development lifecycle.

## Optimize publish size

A Native AOT executable contains just the code from external dependencies required to support the app. Unused code is automatically trimmed away.

The publish size of an ASP.NET Core gRPC service can be optimized by creating the host builder with `WebApplication.CreateSlimBuilder()`. This builder provides a minimal list of features required to run an ASP.NET Core app.

```
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddGrpc();

var app = builder.Build();
app.MapGrpcService<GreeterService>();
app.Run();
```

## Benefits of using Native AOT

Apps published with Native AOT have:

* Minimized disk footprint
* Reduced startup time
* Reduce memory demand

For more information and examples of the benefits that Native AOT provides, see [Benefits of using Native AOT with ASP.NET Core](../fundamentals/native-aot?view=aspnetcore-10.0#benefits-of-using-native-aot-with-aspnet-core).

## Related content

* [ASP.NET Core support for Native AOT](../fundamentals/native-aot?view=aspnetcore-10.0)
* [Native AOT deployment](/en-us/dotnet/core/deploying/native-aot/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-04-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/other-logins
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/social/other-logins.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# External OAuth authentication providers

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Pranav Rastogi](https://github.com/rustd), and [Valeriy Novytskyy](https://github.com/01binary)

The following list includes common external OAuth authentication providers that work with ASP.NET Core apps. Third-party NuGet packages, such as the ones maintained by [OpenIddict](https://documentation.openiddict.com/integrations/web-providers) or [aspnet-contrib](https://www.nuget.org/packages?q=owners%3Aaspnet-contrib+title%3AOAuth), can be used to complement the authentication providers implemented by the ASP.NET Core team.

* [LinkedIn](https://www.linkedin.com/developer/apps)

* [Instagram](https://www.instagram.com/developer/register/)

- Reddit ([Instructions](https://github.com/reddit/reddit/wiki/OAuth2-Quick-Start-Example))

- [Github](https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fsettings%2Fapplications%2Fnew) ([Instructions](https://developer.github.com/v3/oauth/))

- [Yahoo](https://login.yahoo.com/config/login?src=devnet&.done=http%3A%2F%2Fdeveloper.yahoo.com%2Fapps%2Fcreate%2F) ([Instructions](https://developer.yahoo.com/oauth2/guide/))

- [Tumblr](https://www.tumblr.com/oauth/apps) ([Instructions](https://www.tumblr.com/docs/api/v2#auth))

- [Pinterest](https://www.pinterest.com/login/?next=http%3A%2F%2Fdevsite%2Fapps%2F) ([Instructions](https://developers.pinterest.com/docs/api/overview/?))

- [Flickr](https://www.flickr.com/services/apps/create) ([Instructions](https://www.flickr.com/services/api/auth.oauth.html))

- [Dribbble](https://dribbble.com/signup) ([Instructions](https://developer.dribbble.com))

- [Vimeo](https://vimeo.com/join) ([Instructions](https://developer.vimeo.com/api/authentication))

- [SoundCloud](https://soundcloud.com/you/apps/new) ([Instructions](https://developers.soundcloud.com/blog/we-love-oauth-2))

- [VK](https://vk.com/apps?act=manage) ([Instructions](https://vk.com/pages?oid=-17680044\&p=Authorizing_Sites))

To use multiple authentication providers, see [Using external login providers with Identity in ASP.NET Core](./?view=aspnetcore-10.0#multiple-authentication-providers).

## Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (`https`), host, and client IP address. Apps don't automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (`https`) results in the app generating incorrect insecure redirect URLs.

Use forwarded headers middleware to make the original request information available to the app for request processing.

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/libman/libman-cli
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/libman/libman-cli.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use the LibMan CLI with ASP.NET Core

Library Manager (LibMan) is a lightweight, client-side library acquisition tool. LibMan downloads popular libraries and frameworks from the file system or from a [content delivery network (CDN)](https://wikipedia.org/wiki/Content_delivery_network). The supported CDNs include [CDNJS](https://cdnjs.com/), [jsDelivr](https://www.jsdelivr.com/), and [unpkg](https://unpkg.com/#/). The selected library files are fetched and placed in the appropriate location within the ASP.NET Core project.

## Prerequisites

* [latest .NET SDK](https://dotnet.microsoft.com/download/dotnet)

## Installation

The following command installs LibMan:

```
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

A [.NET Global Tool](/en-us/dotnet/core/tools/global-tools#install-a-global-tool) is installed from the [`Microsoft.Web.LibraryManager.Cli`](https://www.nuget.org/packages/Microsoft.Web.LibraryManager.Cli/) NuGet package.

## Usage

```
libman
```

To view the installed LibMan version:

```
libman --version
```

To view the available CLI commands:

```
libman --help
```

The preceding command displays output similar to the following:

```
 1.0.163+g45474d37ed

Usage: libman [options] [command]

Options:
  --help|-h  Show help information
  --version  Show version information

Commands:
  cache      List or clean libman cache contents
  clean      Deletes all library files defined in libman.json from the project
  init       Create a new libman.json
  install    Add a library definition to the libman.json file, and download the 
             library to the specified location
  restore    Downloads all files from provider and saves them to specified 
             destination
  uninstall  Deletes all files for the specified library from their specified 
             destination, then removes the specified library definition from 
             libman.json
  update     Updates the specified library

Use "libman [command] --help" for more information about a command.
```

The following sections outline the available CLI commands.

## Initialize LibMan in the project

The `libman init` command creates a `libman.json` file if one doesn't exist. The file is created with the default item template content.

### Synopsis

```
libman init [-d|--default-destination] [-p|--default-provider] [--verbosity]
libman init [-h|--help]
```

### Options

The following options are available for the `libman init` command:

* `-d|--default-destination <PATH>`

  A path relative to the current folder. Library files are installed in this location if no `destination` property is defined for a library in `libman.json`. The `<PATH>` value is written to the `defaultDestination` property of `libman.json`.

* `-p|--default-provider <PROVIDER>`

  The provider to use if no provider is defined for a given library. The `<PROVIDER>` value is written to the `defaultProvider` property of `libman.json`. Replace `<PROVIDER>` with one of the following values:

  * `cdnjs`
  * `filesystem`
  * `jsdelivr`
  * `unpkg`

- `-h|--help`

  Show help information.

- `--verbosity <LEVEL>`

  Set the verbosity of the output. Replace `<LEVEL>` with one of the following values:

  * `quiet`
  * `normal`
  * `detailed`

### Examples

To create a `libman.json` file in an ASP.NET Core project:

* Navigate to the project root.

* Run the following command:

  ```
  libman init
  ```

* Type the name of the default provider, or press `Enter` to use the default CDNJS provider. Valid values include:

  * `cdnjs`
  * `filesystem`
  * `jsdelivr`
  * `unpkg`

A `libman.json` file is added to the project root with the following content:

```
{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": []
}
```

## Add library files

The `libman install` command downloads and installs library files into the project. A `libman.json` file is added if one doesn't exist. The `libman.json` file is modified to store configuration details for the library files.

### Synopsis

```
libman install <LIBRARY> [-d|--destination] [--files] [-p|--provider] [--verbosity]
libman install [-h|--help]
```

### Arguments

`LIBRARY`

The name of the library to install. This name may include version number notation (for example, `@1.2.0`).

### Options

The following options are available for the `libman install` command:

* `-d|--destination <PATH>`

  The location to install the library. If not specified, the default location is used. If no `defaultDestination` property is specified in `libman.json`, this option is required.

  ***Note:*** There are limitations to the destination path. For example, when the package source has a full project structure and not just the distribution folder, you can't specify moving a folder. For more information, see [Issue #407](https://github.com/aspnet/LibraryManager/issues/407) and [Issue #702](https://github.com/aspnet/LibraryManager/issues/702)

* `--files <FILE>`

  Specify the name of the file to install from the library. If not specified, all files from the library are installed. Provide one `--files` option per file to be installed. Relative paths are supported too. For example: `--files dist/browser/signalr.js`.

* `-p|--provider <PROVIDER>`

  The name of the provider to use for the library acquisition. Replace `<PROVIDER>` with one of the following values:

  * `cdnjs`
  * `filesystem`
  * `jsdelivr`
  * `unpkg`

  If not specified, the `defaultProvider` property in `libman.json` is used. If no `defaultProvider` property is specified in `libman.json`, this option is required.

- `-h|--help`

  Show help information.

- `--verbosity <LEVEL>`

  Set the verbosity of the output. Replace `<LEVEL>` with one of the following values:

  * `quiet`
  * `normal`
  * `detailed`

### Examples

Consider the following `libman.json` file:

```
{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": []
}
```

To install the jQuery version 3.2.1 `jquery.min.js` file to the *wwwroot/scripts/jquery* folder using the CDNJS provider:

```
libman install jquery@3.2.1 --provider cdnjs --destination wwwroot/scripts/jquery --files jquery.min.js
```

The `libman.json` file resembles the following:

```
{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "jquery@3.2.1",
      "destination": "wwwroot/scripts/jquery",
      "files": [
        "jquery.min.js"
      ]
    }
  ]
}
```

To install the `calendar.js` and `calendar.css` files from *C:\temp\contosoCalendar\\* using the file system provider:

```
libman install C:\temp\contosoCalendar\ --provider filesystem --files calendar.js --files calendar.css
```

The following prompt appears for two reasons:

* The `libman.json` file doesn't contain a `defaultDestination` property.
* The `libman install` command doesn't contain the `-d|--destination` option.

After accepting the default destination, the `libman.json` file resembles the following:

```
{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "jquery@3.2.1",
      "destination": "wwwroot/scripts/jquery",
      "files": [
        "jquery.min.js"
      ]
    },
    {
      "library": "C:\\temp\\contosoCalendar\\",
      "provider": "filesystem",
      "destination": "wwwroot/lib/contosoCalendar",
      "files": [
        "calendar.js",
        "calendar.css"
      ]
    }
  ]
}
```

## Restore library files

The `libman restore` command installs library files defined in `libman.json`. The following rules apply:

* If no `libman.json` file exists in the project root, an error is returned.
* If a library specifies a provider, the `defaultProvider` property in `libman.json` is ignored.
* If a library specifies a destination, the `defaultDestination` property in `libman.json` is ignored.

### Synopsis

```
libman restore [--verbosity]
libman restore [-h|--help]
```

### Options

The following options are available for the `libman restore` command:

* `-h|--help`

  Show help information.

* `--verbosity <LEVEL>`

  Set the verbosity of the output. Replace `<LEVEL>` with one of the following values:

  * `quiet`
  * `normal`
  * `detailed`

### Examples

To restore the library files defined in `libman.json`:

```
libman restore
```

## Delete library files

The `libman clean` command deletes library files previously restored via LibMan. Folders that become empty after this operation are deleted. The library files' associated configurations in the `libraries` property of `libman.json` aren't removed.

### Synopsis

```
libman clean [--verbosity]
libman clean [-h|--help]
```

### Options

The following options are available for the `libman clean` command:

* `-h|--help`

  Show help information.

* `--verbosity <LEVEL>`

  Set the verbosity of the output. Replace `<LEVEL>` with one of the following values:

  * `quiet`
  * `normal`
  * `detailed`

### Examples

To delete library files installed via LibMan:

```
libman clean
```

## Uninstall library files

The `libman uninstall` command:

* Deletes all files associated with the specified library from the destination in `libman.json`.
* Removes the associated library configuration from `libman.json`.

An error occurs when:

* No `libman.json` file exists in the project root.
* The specified library doesn't exist.

If more than one library with the same name is installed, you're prompted to choose one.

### Synopsis

```
libman uninstall <LIBRARY> [--verbosity]
libman uninstall [-h|--help]
```

### Arguments

`LIBRARY`

The name of the library to uninstall. This name may include version number notation (for example, `@1.2.0`).

### Options

The following options are available for the `libman uninstall` command:

* `-h|--help`

  Show help information.

* `--verbosity <LEVEL>`

  Set the verbosity of the output. Replace `<LEVEL>` with one of the following values:

  * `quiet`
  * `normal`
  * `detailed`

### Examples

Consider the following `libman.json` file:

```
{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "jquery@3.7.1",
      "files": [
        "jquery.min.js",
        "jquery.js",
        "jquery.min.map"
      ],
      "destination": "wwwroot/lib/jquery/"
    },
    {
      "provider": "unpkg",
      "library": "bootstrap@5.3.6",
      "destination": "wwwroot/lib/bootstrap/"
    }
  ]
}
```

* To uninstall jQuery, either of the following commands succeed:

  ```
  libman uninstall jquery
  ```

  ```
  libman uninstall jquery@3.3.1
  ```

* To uninstall the Lodash files installed via the `filesystem` provider:

  ```
  libman uninstall C:\temp\lodash\
  ```

## Update library version

The `libman update` command updates a library installed via LibMan to the specified version.

An error occurs when:

* No `libman.json` file exists in the project root.
* The specified library doesn't exist.

If more than one library with the same name is installed, you're prompted to choose one.

### Synopsis

```
libman update <LIBRARY> [-pre] [--to] [--verbosity]
libman update [-h|--help]
```

### Arguments

`LIBRARY`

The name of the library to update.

### Options

The following options are available for the `libman update` command:

* `-pre`

  Obtain the latest prerelease version of the library.

* `--to <VERSION>`

  Obtain a specific version of the library.

- `-h|--help`

  Show help information.

- `--verbosity <LEVEL>`

  Set the verbosity of the output. Replace `<LEVEL>` with one of the following values:

  * `quiet`
  * `normal`
  * `detailed`

### Examples

* To update jQuery to the latest version:

  ```
  libman update jquery
  ```

* To update jQuery to version 3.3.1:

  ```
  libman update jquery --to 3.3.1
  ```

* To update jQuery to the latest prerelease version:

  ```
  libman update jquery -pre
  ```

## Manage library cache

The `libman cache` command manages the LibMan library cache. The `filesystem` provider doesn't use the library cache.

### Synopsis

```
libman cache clean [<PROVIDER>] [--verbosity]
libman cache list [--files] [--libraries] [--verbosity]
libman cache [-h|--help]
```

### Arguments

`PROVIDER`

Only used with the `clean` command. Specifies the provider cache to clean. Valid values include:

* `cdnjs`
* `filesystem`
* `jsdelivr`
* `unpkg`

### Options

The following options are available for the `libman cache` command:

* `--files`

  List the names of files that are cached.

* `--libraries`

  List the names of libraries that are cached.

- `-h|--help`

  Show help information.

- `--verbosity <LEVEL>`

  Set the verbosity of the output. Replace `<LEVEL>` with one of the following values:

  * `quiet`
  * `normal`
  * `detailed`

### Examples

* To view the names of cached libraries per provider, use one of the following commands:

  ```
  libman cache list
  ```

  ```
  libman cache list --libraries
  ```

  Output similar to the following is displayed:

  ```
  Cache contents:
  ---------------
  unpkg:
      knockout
      react
      vue
  cdnjs:
      font-awesome
      jquery
      knockout
      lodash.js
      react
  ```

* To view the names of cached library files per provider:

  ```
  libman cache list --files
  ```

  Output similar to the following is displayed:

  ```
  Cache contents:
  ---------------
  unpkg:
      knockout:
          <list omitted for brevity>
      react:
          <list omitted for brevity>
      vue:
          <list omitted for brevity>
  cdnjs:
      font-awesome
          metadata.json
      jquery
          metadata.json
          3.2.1\core.js
          3.2.1\jquery.js
          3.2.1\jquery.min.js
          3.2.1\jquery.min.map
          3.2.1\jquery.slim.js
          3.2.1\jquery.slim.min.js
          3.2.1\jquery.slim.min.map
          3.3.1\core.js
          3.3.1\jquery.js
          3.3.1\jquery.min.js
          3.3.1\jquery.min.map
          3.3.1\jquery.slim.js
          3.3.1\jquery.slim.min.js
          3.3.1\jquery.slim.min.map
      knockout
          metadata.json
          3.4.2\knockout-debug.js
          3.4.2\knockout-min.js
      lodash.js
          metadata.json
          4.17.10\lodash.js
          4.17.10\lodash.min.js
      react
          metadata.json
  ```

  Notice the preceding output shows that jQuery versions 3.2.1 and 3.3.1 are cached under the CDNJS provider.

* To empty the library cache for the CDNJS provider:

  ```
  libman cache clean cdnjs
  ```

  After emptying the CDNJS provider cache, the `libman cache list` command displays the following:

  ```
  Cache contents:
  ---------------
  unpkg:
      knockout
      react
      vue
  cdnjs:
      (empty)
  ```

* To empty the cache for all supported providers:

  ```
  libman cache clean
  ```

  After emptying all provider caches, the `libman cache list` command displays the following:

  ```
  Cache contents:
  ---------------
  unpkg:
      (empty)
  cdnjs:
      (empty)
  ```

## Related content

* [Install a Global Tool](/en-us/dotnet/core/tools/global-tools#install-a-global-tool)
* [Use LibMan with ASP.NET Core in Visual Studio](libman-vs?view=aspnetcore-10.0)
* [LibMan GitHub repository](https://github.com/aspnet/LibraryManager)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate an ASP.NET Core app

Use the guidance in this node to migrate an ASP.NET Core app.

For AI-assisted upgrades and modernization—including moving from ASP.NET Framework to ASP.NET Core, upgrading between .NET versions, and modernizing to the cloud—see [GitHub Copilot app modernization](/en-us/dotnet/core/porting/github-copilot-app-modernization/overview) and [Overview of upgrading .NET apps](/en-us/dotnet/core/porting/).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/render-modes.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor render modes

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains control of Razor component rendering in Blazor Web Apps, either at compile time or runtime.

This guidance doesn't apply to standalone Blazor WebAssembly apps. Blazor WebAssembly apps only render on the client via a client-side WebAssembly-based runtime and have no concept of a *render mode*. If a render mode is applied to a component in a Blazor WebAssembly app, the render mode designation has no influence on rendering the component.

## Render modes

Every component in a Blazor Web App adopts a *render mode* to determine the hosting model that it uses, where it's rendered, and whether or not it's interactive. *Interactivity* makes it possible for users to interact with rendered components. This includes app responses to [Document Object Model (DOM)](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction) events and state changes tied to C# members via Blazor's event handlers and binding.

The following table shows the available render modes for rendering Razor components in a Blazor Web App. To apply a render mode to a component use the `@rendermode` directive on the component instance or on the component definition. Later in this article, examples are shown for each render mode scenario.

| Name                    | Description                                                                                                            | Render location     | Interactive |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------- |
| Static Server           | Static server-side rendering (static SSR)                                                                              | Server              | ❌No         |
| Interactive Server      | Interactive server-side rendering (interactive SSR) using Blazor Server.                                               | Server              | ✔️Yes       |
| Interactive WebAssembly | Client-side rendering (CSR) using Blazor WebAssembly†.                                                                 | Client              | ✔️Yes       |
| Interactive Auto        | Interactive SSR using Blazor Server initially and then CSR on subsequent visits after the Blazor bundle is downloaded. | Server, then client | ✔️Yes       |

†Client-side rendering (CSR) is assumed to be interactive. "*Interactive* client-side rendering" and "*interactive* CSR" aren't used by the industry or in the Blazor documentation.

Prerendering is enabled by default for interactive components. Guidance on controlling prerendering is provided later in this article. For general industry terminology on client and server rendering concepts, see [ASP.NET Core Blazor fundamentals](../fundamentals/?view=aspnetcore-10.0#client-and-server-rendering-concepts).

The following examples demonstrate setting the component's render mode with a few basic Razor component features.

To test the render mode behaviors locally, you can place the following components in an app created from the *Blazor Web App* project template. When you create the app, select options from dropdown menus (Visual Studio) or apply the CLI options (.NET CLI) to enable both server-side and client-side interactivity. For guidance on how to create a Blazor Web App, see [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0).

## Enable support for interactive render modes

A Blazor Web App must be configured to support interactive render modes. The following extensions are automatically applied to apps created from the *Blazor Web App* project template during app creation. Individual components are still required to declare their render mode per the [*Render modes*](#render-modes) section after the component services and endpoints are configured in the app's `Program` file.

Services for Razor components are added by calling [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents).

Component builder extensions:

* [AddInteractiveServerComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serverrazorcomponentsbuilderextensions.addinteractiveservercomponents) adds services to support rendering Interactive Server components.
* [AddInteractiveWebAssemblyComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addinteractivewebassemblycomponents) adds services to support rendering Interactive WebAssembly components.

[MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) discovers available components and specifies the root component for the app (the first component loaded), which by default is the `App` component (`App.razor`).

Endpoint convention builder extensions:

* [AddInteractiveServerRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.serverrazorcomponentsendpointconventionbuilderextensions.addinteractiveserverrendermode) configures interactive server-side rendering (interactive SSR) for the app.
* [AddInteractiveWebAssemblyRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.webassemblyrazorcomponentsendpointconventionbuilderextensions.addinteractivewebassemblyrendermode) configures the Interactive WebAssembly render mode for the app.

Note

For orientation on the placement of the API in the following examples, inspect the `Program` file of an app generated from the Blazor Web App project template. For guidance on how to create a Blazor Web App, see [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0).

Example 1: The following `Program` file API adds services and configuration for enabling interactive SSR:

```
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
```

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

Example 2: The following `Program` file API adds services and configuration for enabling the Interactive WebAssembly render mode:

```
builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents();
```

```
app.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode();
```

Example 3: The following `Program` file API adds services and configuration for enabling the Interactive Server, Interactive WebAssembly, and Interactive Auto render modes:

```
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();
```

```
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode();
```

Blazor uses the Blazor WebAssembly hosting model to download and execute components that use the Interactive WebAssembly render mode. A separate client project is required to set up Blazor WebAssembly hosting for these components. The client project contains the startup code for the Blazor WebAssembly host and sets up the .NET runtime for running in a browser. The Blazor Web App template adds this client project for you when you select the option to enable WebAssembly interactivity. Any components using the Interactive WebAssembly render mode should be built from the client project, so they get included in the downloaded app bundle.

## Apply a render mode to a component instance

To apply a render mode to a component instance use the [`@rendermode` Razor directive attribute](../../mvc/views/razor?view=aspnetcore-10.0#rendermode) where the component is used.

In the following example, interactive server-side rendering (interactive SSR) is applied to the `Dialog` component instance:

```
<Dialog @rendermode="InteractiveServer" />
```

Note

Blazor templates include a static `using` directive for [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) in the app's imports file (`Components/_Imports.razor`) for shorter `@rendermode` syntax:

```
@using static Microsoft.AspNetCore.Components.Web.RenderMode
```

Without the preceding directive, components must specify the static [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) class in `@rendermode` syntax:

```
<Dialog @rendermode="RenderMode.InteractiveServer" />
```

You can also reference custom render mode instances instantiated directly with custom configuration. For more information, see the [Custom shorthand render modes](#custom-shorthand-render-modes) section later in this article.

## Apply a render mode to a component definition

To specify the render mode for a component as part of its definition, use the [`@rendermode` Razor directive](../../mvc/views/razor?view=aspnetcore-10.0#rendermode) and the corresponding render mode attribute.

```
@page "..."
@rendermode InteractiveServer
```

Applying a render mode to a component definition is commonly used when applying a render mode to a specific page. Routable pages use the same render mode as the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component that rendered the page.

Technically, `@rendermode` is both a Razor *directive* and a Razor *directive attribute*. The semantics are similar, but there are differences. The `@rendermode` directive is on the component definition, so the referenced render mode instance must be static. The `@rendermode` directive attribute can take any render mode instance.

Note

Component authors should avoid coupling a component's implementation to a specific render mode. Instead, component authors should typically design components to support any render mode or hosting model. A component's implementation should avoid assumptions on where it's running (server or client) and should degrade gracefully when rendered statically. Specifying the render mode in the component definition may be needed if the component isn't instantiated directly (such as with a routable page component) or to specify a render mode for all component instances.

## Apply a render mode to the entire app

To set the render mode for the entire app, indicate the render mode at the highest-level interactive component in the app's component hierarchy that isn't a root component.

Note

Making a root component interactive, such as the `App` component, isn't supported. Therefore, the render mode for the entire app can't be set directly by the `App` component.

For apps based on the Blazor Web App project template, a render mode assigned to the entire app is typically specified where the `Routes` component is used in the `App` component (`Components/App.razor`):

```
<Routes @rendermode="InteractiveServer" />
```

The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component propagates its render mode to the pages it routes.

You also typically must set the same interactive render mode on the [`HeadOutlet` component](control-head-content?view=aspnetcore-10.0#headoutlet-component), which is also found in the `App` component of a Blazor Web App generated from the project template:

```
<HeadOutlet @rendermode="InteractiveServer" />
```

For apps that adopt an interactive client-side (WebAssembly or Auto) rendering mode and enable the render mode for the entire app via the `Routes` component:

* Place or move the layout and navigation files of the server app's `Components/Layout` folder into the `.Client` project's `Layout` folder. Create a `Layout` folder in the `.Client` project if it doesn't exist.
* Place or move the components of the server app's `Components/Pages` folder into the `.Client` project's `Pages` folder. Create a `Pages` folder in the `.Client` project if it doesn't exist.
* Place or move the `Routes` component of the server app's `Components` folder into the `.Client` project's root folder.

To enable global interactivity when creating a Blazor Web App:

* Visual Studio: Set the **Interactivity location** dropdown list to **Global**.
* .NET CLI: Use the `-ai|--all-interactive` option.

For more information, see [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0).

## Apply a render mode programmatically

Properties and fields can assign a render mode.

The second approach described in this section, setting the render mode by component instance, is especially useful when your app specification calls for one or more components to adopt static SSR in an otherwise interactive app. This scenario is covered in the [Static SSR pages in an interactive app](#static-ssr-pages-in-an-interactive-app) section later in this article.

### Set the render mode by component definition

A component definition can define a render mode via a private field:

```
@rendermode pageRenderMode

...

@code {
    private static IComponentRenderMode pageRenderMode = InteractiveServer;
}
```

### Set the render mode by component instance

The following example applies interactive server-side rendering (interactive SSR) to any request.

```
<Routes @rendermode="PageRenderMode" />

...

@code {
    private IComponentRenderMode? PageRenderMode => InteractiveServer;
}
```

Additional information on render mode propagation is provided in the [Render mode propagation](#render-mode-propagation) section later in this article. The [Static SSR pages in an interactive app](#static-ssr-pages-in-an-interactive-app) section shows how to use the preceding approach to adopt static SSR in an otherwise interactive app.

## Blazor documentation examples for Blazor Web Apps

When using a Blazor Web App, most of the Blazor documentation example components ***require*** interactivity to function and demonstrate the concepts covered by the articles. When you test an example component provided by an article, make sure that either the app adopts global interactivity or the component adopts an interactive render mode.

## Prerendering

Interactive render modes (Interactive Server, Interactive WebAssembly, Interactive Auto) support prerendering by default, which initially renders the page content statically from the server to improve the initial load experience. For more information, see [Prerender ASP.NET Core Razor components](prerender?view=aspnetcore-10.0).

## Detect rendering location, interactivity, and assigned render mode at runtime

The [ComponentBase.RendererInfo](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.rendererinfo#microsoft-aspnetcore-components-componentbase-rendererinfo) and [ComponentBase.AssignedRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.assignedrendermode#microsoft-aspnetcore-components-componentbase-assignedrendermode) properties permit the app to detect details about the location, interactivity, and assigned render mode of a component:

* [RendererInfo.Name](/en-us/dotnet/api/microsoft.aspnetcore.components.rendererinfo.name#microsoft-aspnetcore-components-rendererinfo-name) returns the location where the component is executing:

  * `Static`: On the server (SSR) and incapable of interactivity.
  * `Server`: On the server (SSR) and capable of interactivity after prerendering.
  * `WebAssembly`: On the client (CSR) and capable of interactivity after prerendering.
  * `WebView`: On the native device and capable of interactivity after prerendering.

* [RendererInfo.IsInteractive](/en-us/dotnet/api/microsoft.aspnetcore.components.rendererinfo.isinteractive#microsoft-aspnetcore-components-rendererinfo-isinteractive) indicates if the component supports interactivity at the time of rendering. The value is `true` when rendering interactively or `false` when prerendering or for static SSR ([RendererInfo.Name](/en-us/dotnet/api/microsoft.aspnetcore.components.rendererinfo.name#microsoft-aspnetcore-components-rendererinfo-name) of `Static`).

* [AssignedRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.assignedrendermode#microsoft-aspnetcore-components-componentbase-assignedrendermode) exposes the component's assigned render mode:

  * `InteractiveServerRenderMode` for Interactive Server.
  * `InteractiveAutoRenderMode` for Interactive Auto.
  * `InteractiveWebAssemblyRenderMode` for Interactive WebAssembly.
  * `null` if a render mode isn't assigned.

Components use these properties to render content depending on their location or interactivity status. The following examples demonstrate typical use cases.

Display content until a component is interactive:

```
@if (!RendererInfo.IsInteractive)
{
    <p>Connecting to the assistant...</p>
}
else
{
    ...
}
```

Disable a button until a component is interactive:

```
<button @onclick="Send" disabled="@(!RendererInfo.IsInteractive)">
    Send
</button>
```

Disable a form during prerendering and enable the form when the component is interactive:

```
<EditForm Model="Movie" ...>
    <fieldset disabled="@disabled">

        ...

        <button type="submit" >Save</button>
    </fieldset>
</EditForm>

@code {
    private bool disabled = true;

    [SupplyParameterFromForm]
    private Movie? Movie { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Movie ??= await ...;

        if (RendererInfo.IsInteractive)
        {
            disabled = false;
        }
    }
}
```

Render markup to support performing a regular HTML action if the component is statically rendered:

```
@if (AssignedRenderMode is null)
{
    // The render mode is Static Server
    <form action="/movies">
        <input type="text" name="titleFilter" />
        <input type="submit" value="Search" />
    </form>
}
else
{
    // The render mode is Interactive Server, WebAssembly, or Auto
    <input @bind="titleFilter" />
    <button @onclick="FilterMovies">Search</button>
}
```

In the preceding example:

* When the value of [AssignedRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.assignedrendermode#microsoft-aspnetcore-components-componentbase-assignedrendermode) is `null`, the component adopts static SSR. Blazor event handling isn't functional in a browser with static SSR, so the component submits a form (GET request) with a `titleFilter` query string set to the user's `<input>` value. The `Movie` component (`/movie`) can read the query string and process the value of `titleFilter` to render the component with the filtered results.
* Otherwise, the render mode is any of `InteractiveServer`, `InteractiveWebAssembly`, or `InteractiveAuto`. The component is capable of using an event handler delegate (`FilterMovies`) and the value bound to the `<input>` element (`titleFilter`) to filter movies interactively over the background SignalR connection.

## Static server-side rendering (static SSR)

Components use static server-side rendering (static SSR). The component renders to the response stream and interactivity isn't enabled.

In the following example, there's no designation for the component's render mode, so the component inherits its render mode from its parent. Since no ancestor component specifies a render mode, the following component is *statically rendered* on the server. The button isn't interactive and doesn't call the `UpdateMessage` method when selected. The value of `message` doesn't change, and the component isn't rerendered in response to UI events.

`RenderMode1.razor`:

```
@page "/render-mode-1"

<button @onclick="UpdateMessage">Click me</button> @message

@code {
    private string message = "Not updated yet.";

    private void UpdateMessage()
    {
        message = "Somebody updated me!";
    }
}
```

If using the preceding component locally in a Blazor Web App, place the component in the server project's `Components/Pages` folder. The server project is the solution's project with a name that doesn't end in `.Client`. When the app is running, navigate to `/render-mode-1` in the browser's address bar.

During static SSR, Razor component page requests are processed by server-side ASP.NET Core middleware pipeline request processing for authorization. Dedicated Blazor features for authorization aren't operational because Razor components aren't rendered during server-side request processing. Blazor router features in the `Routes` component that aren't available during static SSR include displaying [Not Authorized content (`<NotAuthorized>...</NotAuthorized>`)](../security/?view=aspnetcore-10.0#authorizeview-component) ([NotAuthorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview.notauthorized#microsoft-aspnetcore-components-authorization-authorizerouteview-notauthorized)). Blazor Web Apps typically process unauthorized requests on the server by [customizing the behavior of authorization middleware](../../security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-10.0).

During static SSR, Razor component page requests are processed by server-side ASP.NET Core middleware pipeline request processing for routing and authorization. Dedicated Blazor features for routing and authorization aren't operational because Razor components aren't rendered during server-side request processing. Blazor router features in the `Routes` component that aren't available during static SSR include displaying:

* [Not Authorized content (`<NotAuthorized>...</NotAuthorized>`)](../security/?view=aspnetcore-10.0#authorizeview-component) ([NotAuthorized](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview.notauthorized#microsoft-aspnetcore-components-authorization-authorizerouteview-notauthorized)): Blazor Web Apps typically process unauthorized requests on the server by [customizing the behavior of authorization middleware](../../security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-10.0).

* [Not Found content (`<NotFound>...</NotFound>`)](../fundamentals/routing?view=aspnetcore-10.0#provide-custom-content-when-content-isnt-found) ([NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfound#microsoft-aspnetcore-components-routing-router-notfound)): Blazor Web Apps typically process bad URL requests on the server by either displaying the browser's built-in 404 UI or returning a custom 404 page (or other response) via ASP.NET Core middleware (for example, [`UseStatusCodePagesWithRedirects`](../../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepageswithredirects) / [API documentation](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithredirects)).

If the app exhibits root-level interactivity, server-side ASP.NET Core request processing isn't involved after the initial static SSR, which means that the preceding Blazor features work as expected.

[Enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling) with static SSR requires special attention when loading JavaScript. For more information, see [ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](../javascript-interoperability/static-server-rendering?view=aspnetcore-10.0).

## Interactive server-side rendering (interactive SSR)

Interactive server-side rendering (interactive SSR) renders the component interactively from the server using Blazor Server. User interactions are handled over a real-time connection with the browser. The circuit connection is established when the Server component is rendered.

In the following example, the render mode is set interactive SSR by adding `@rendermode InteractiveServer` to the component definition. The button calls the `UpdateMessage` method when selected. The value of `message` changes, and the component is rerendered to update the message in the UI.

`RenderMode2.razor`:

```
@page "/render-mode-2"
@rendermode InteractiveServer

<button @onclick="UpdateMessage">Click me</button> @message

@code {
    private string message = "Not updated yet.";

    private void UpdateMessage()
    {
        message = "Somebody updated me!";
    }
}
```

If using the preceding component in a Blazor Web App, place the component in the server project's `Components/Pages` folder. The server project is the solution's project with a name that doesn't end in `.Client`. When the app is running, navigate to `/render-mode-2` in the browser's address bar.

Interactive Server components handle web UI events using a real-time connection with the browser called a circuit. A circuit and its associated state are created when a root Interactive Server component is rendered. The circuit is closed when there are no remaining Interactive Server components on the page, which frees up server resources.

## Client-side rendering (CSR)

Client-side rendering (CSR) renders the component interactively on the client using Blazor WebAssembly. The .NET runtime and app bundle are downloaded and cached when the WebAssembly component is initially rendered. Components using CSR must be built from a separate client project that sets up the Blazor WebAssembly host.

In the following example, the render mode is set to CSR with `@rendermode InteractiveWebAssembly`. The button calls the `UpdateMessage` method when selected. The value of `message` changes, and the component is rerendered to update the message in the UI.

`RenderMode3.razor`:

```
@page "/render-mode-3"
@rendermode InteractiveWebAssembly

<button @onclick="UpdateMessage">Click me</button> @message

@code {
    private string message = "Not updated yet.";

    private void UpdateMessage()
    {
        message = "Somebody updated me!";
    }
}
```

If using the preceding component locally in a Blazor Web App, place the component in the client project's `Pages` folder. The client project is the solution's project with a name that ends in `.Client`. When the app is running, navigate to `/render-mode-3` in the browser's address bar.

## Automatic (Auto) rendering

Automatic (Auto) rendering determines how to render the component at runtime. The component is initially rendered with interactive server-side rendering (interactive SSR) using the Blazor Server hosting model. The .NET runtime and app bundle are downloaded to the client in the background and cached so that they can be used on future visits.

The Auto render mode never dynamically changes the render mode of a component already on the page. The Auto render mode makes an initial decision about which type of interactivity to use for a component, then the component keeps that type of interactivity for as long as it's on the page. One factor in this initial decision is considering whether components already exist on the page with WebAssembly/Server interactivity. Auto mode prefers to select a render mode that matches the render mode of existing interactive components. The reason that the Auto mode prefers to use an existing interactivity mode is to avoid introducing a new interactive runtime that doesn't share state with the existing runtime.

Components using the Auto render mode must be built from a separate client project that sets up the Blazor WebAssembly host.

In the following example, the component is interactive throughout the process. The button calls the `UpdateMessage` method when selected. The value of `message` changes, and the component is rerendered to update the message in the UI. Initially, the component is rendered interactively from the server, but on subsequent visits it's rendered from the client after the .NET runtime and app bundle are downloaded and cached.

`RenderMode4.razor`:

```
@page "/render-mode-4"
@rendermode InteractiveAuto

<button @onclick="UpdateMessage">Click me</button> @message

@code {
    private string message = "Not updated yet.";

    private void UpdateMessage()
    {
        message = "Somebody updated me!";
    }
}
```

If using the preceding component locally in a Blazor Web App, place the component in the client project's `Pages` folder. The client project is the solution's project with a name that ends in `.Client`. When the app is running, navigate to `/render-mode-4` in the browser's address bar.

## Render mode propagation

Render modes propagate down the component hierarchy.

Rules for applying render modes:

* The default render mode is Static.
* The Interactive Server ([InteractiveServer](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactiveserver#microsoft-aspnetcore-components-web-rendermode-interactiveserver)), Interactive WebAssembly ([InteractiveWebAssembly](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactivewebassembly#microsoft-aspnetcore-components-web-rendermode-interactivewebassembly)), and Interactive Auto ([InteractiveAuto](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactiveauto#microsoft-aspnetcore-components-web-rendermode-interactiveauto)) render modes can be used from a component, including using different render modes for sibling components.
* You can't switch to a different interactive render mode in a child component. For example, a Server component can't be a child of a WebAssembly component.
* Parameters passed to an interactive child component from a Static parent must be JSON serializable. This means that you can't pass render fragments or child content from a Static parent component to an interactive child component.

The following examples use a non-routable, non-page `SharedMessage` component. The render mode agnostic `SharedMessage` component doesn't apply a render mode with an [`@attribute` directive](../../mvc/views/razor?view=aspnetcore-10.0#attribute). If you're testing these scenarios with a Blazor Web App, place the following component in the app's `Components` folder.

`SharedMessage.razor`:

```
<p>@Greeting</p>

<button @onclick="UpdateMessage">Click me</button> @message

<p>@ChildContent</p>

@code {
    private string message = "Not updated yet.";

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    [Parameter]
    public string Greeting { get; set; } = "Hello!";

    private void UpdateMessage()
    {
        message = "Somebody updated me!";
    }
}
```

### Render mode inheritance

If the `SharedMessage` component is placed in a statically-rendered parent component, the `SharedMessage` component is also rendered statically and isn't interactive. The button doesn't call `UpdateMessage`, and the message isn't updated.

`RenderMode5.razor`:

```
@page "/render-mode-5"

<SharedMessage />
```

If the `SharedMessage` component is placed in a component that defines the render mode, it inherits the applied render mode.

In the following example, the `SharedMessage` component is interactive over a SignalR connection to the client. The button calls `UpdateMessage`, and the message is updated.

`RenderMode6.razor`:

```
@page "/render-mode-6"
@rendermode InteractiveServer

<SharedMessage />
```

### Child components with different render modes

In the following example, both `SharedMessage` components are prerendered and appear when the page is displayed in the browser.

* The first `SharedMessage` component with interactive server-side rendering (interactive SSR) is interactive after Blazor's SignalR circuit is established.
* The second `SharedMessage` component with client-side rendering (CSR) is interactive *after* the Blazor app bundle is downloaded and the .NET runtime is active on the client.

`RenderMode7.razor`:

```
@page "/render-mode-7"

<SharedMessage @rendermode="InteractiveServer" />
<SharedMessage @rendermode="InteractiveWebAssembly" />
```

### Child component with a serializable parameter

The following example demonstrates an interactive child component that takes a parameter. Parameters must be serializable.

`RenderMode8.razor`:

```
@page "/render-mode-8"

<SharedMessage @rendermode="InteractiveServer" Greeting="Welcome!" />
```

Non-serializable component parameters, such as child content or a render fragment, aren't supported. In the following example, passing child content to the `SharedMessage` component results in a runtime error.

`RenderMode9.razor`:

```
@page "/render-mode-9"

<SharedMessage @rendermode="InteractiveServer">
    Child content
</SharedMessage>
```

❌ **Error**:

> System.InvalidOperationException: Cannot pass the parameter 'ChildContent' to component 'SharedMessage' with rendermode 'InteractiveServerRenderMode'. This is because the parameter is of the delegate type 'Microsoft.AspNetCore.Components.RenderFragment', which is arbitrary code and cannot be serialized.

The same thing happens if you attempt to adopt interactive rendering in a layout that inherits from [LayoutComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase), such as the app's `MainLayout` component, in an app that adopts per-page/component rendering. For more information, see [ASP.NET Core Blazor layouts](layouts?view=aspnetcore-10.0#statically-rendered-layout-components).

To circumvent the preceding limitation, wrap the child component in another component that doesn't have the parameter. This is the approach taken in the Blazor Web App project template with the `Routes` component (`Components/Routes.razor`) to wrap the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component.

`WrapperComponent.razor`:

```
<SharedMessage>
    Child content
</SharedMessage>
```

`RenderMode10.razor`:

```
@page "/render-mode-10"

<WrapperComponent @rendermode="InteractiveServer" />
```

In the preceding example:

* The child content is passed to the `SharedMessage` component without generating a runtime error.
* The `SharedMessage` component renders interactively on the server.

### Child component with a different render mode than its parent

Don't try to apply a different interactive render mode to a child component than its parent's render mode.

The following component results in a runtime error when the component is rendered:

`RenderMode11.razor`:

```
@page "/render-mode-11"
@rendermode InteractiveServer

<SharedMessage @rendermode="InteractiveWebAssembly" />
```

❌ **Error**:

> Cannot create a component of type 'BlazorSample.Components.SharedMessage' because its render mode 'Microsoft.AspNetCore.Components.Web.InteractiveWebAssemblyRenderMode' is not supported by Interactive Server rendering.

## Static SSR pages in an interactive app

There are cases where the app's specification calls for components to adopt static server-side rendering (static SSR) and only run on the server, while the rest of the app uses an interactive render mode.

This approach is only useful when the app has specific pages that can't work with interactive Server or WebAssembly rendering. For example, adopt this approach for pages that depend on reading/writing HTTP cookies and can only work in a request/response cycle instead of interactive rendering. For pages that work with interactive rendering, you shouldn't force them to use static SSR, as it's less efficient and less responsive for the end user.

Mark any Razor component page with the [`[ExcludeFromInteractiveRouting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.excludefrominteractiveroutingattribute) assigned with the `@attribute` Razor directive:

```
@attribute [ExcludeFromInteractiveRouting]
```

Applying the attribute causes navigation to the page to exit from [interactive routing](../fundamentals/routing?view=aspnetcore-10.0#static-versus-interactive-routing). Inbound navigation is forced to perform a full-page reload instead resolving the page via interactive routing. The full-page reload forces the top-level root component, typically the `App` component (`App.razor`), to rerender from the server, allowing the app to switch to a different top-level render mode.

The [RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.razorcomponentsendpointhttpcontextextensions.acceptsinteractiverouting) extension method allows the component to detect whether the [`[ExcludeFromInteractiveRouting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.excludefrominteractiveroutingattribute) is applied to the current page.

In the `App` component, use the pattern in the following example:

* Pages that aren't annotated with the [`[ExcludeFromInteractiveRouting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.excludefrominteractiveroutingattribute) default to the `InteractiveServer` render mode with global interactivity. You can replace `InteractiveServer` with `InteractiveWebAssembly` or `InteractiveAuto` to specify a different default global render mode.
* Pages annotated with the [`[ExcludeFromInteractiveRouting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.excludefrominteractiveroutingattribute) adopt static SSR (`PageRenderMode` is assigned `null`).

```
<!DOCTYPE html>
<html>
<head>
    ...
    <HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
    <Routes @rendermode="@PageRenderMode" />
    ...
</body>
</html>

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? PageRenderMode
        => HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}
```

An alternative to using the [RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.razorcomponentsendpointhttpcontextextensions.acceptsinteractiverouting) extension method is to read endpoint metadata manually using `HttpContext.GetEndpoint()?.Metadata`.

There are two approaches that can be taken for fine control of render modes, each of which is described in the following subsections:

* [Area (folder) of static SSR components](#area-folder-of-static-ssr-components): You have an area (folder) of the app with components that must adopt static SSR and share the same route path prefix. The app controls the render mode globally by setting the render mode on the `Routes` component in the `App` component based on the path to the folder.

* [Static SSR components spread out across the app](#static-ssr-components-spread-out-across-the-app): You have components spread around the app in various locations that must adopt static SSR and only run on the server. The static SSR-only components aren't in a single folder and don't share a common route path prefix. The app controls the render mode on a per-component basis by setting the render mode with the `@rendermode` directive in component instances. Reflection is used in the `App` component to set the render mode on the `Routes` component.

In both cases, the component that must adopt static SSR must also force a full-page reload.

The following examples use the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) cascading parameter to determine if the page is statically-rendered. A `null` [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) indicates that the component is rendering interactively, which is useful as a signal in app code to trigger a full-page reload.

### Area (folder) of static SSR components

*For an example of the approach in this section, see the [`BlazorWebAppAreaOfStaticSsrComponents` sample app](https://github.com/dotnet/blazor-samples/tree/main/9.0/BlazorWebAppAreaOfStaticSsrComponents). The technique described in this section is most appropriate for .NET 8 Blazor Web Apps, but the sample is implemented in .NET 9 using Blazor features that simplify demonstrating how the approach works.*

The approach described in this subsection is used by the Blazor Web App project template with global interactivity.

An area (folder) of the app contains the components that must adopt static SSR and only run on the server. The components in the folder share the same route path prefix. For example, the Identity Razor components of the Blazor Web App project template are in the `Components/Account/Pages` folder and share the root path prefix `/account`.

The app also contains an imports file (`_Imports.razor`) automatically applied to static SSR components in the `Components` folder, which applies a custom layout.

`Components/Account/_Imports.razor`:

```
@using BlazorSample.Components.Account.Shared
@layout AccountLayout
```

The `Shared` folder maintains the `AccountLayout` layout component. The component makes use of [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) to determine if the component has adopted static SSR. For example, Identity components must render on the server with static SSR because they set Identity cookies. If the value of [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is `null`, the component is rendering interactively, and a full-page reload is performed by calling [NavigationManager.Refresh](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.refresh) with `forceLoad` set to `true`. This forces a full rerender of the page using static SSR.

`Components/Account/Shared/AccountLayout.razor`:

```
@inherits LayoutComponentBase
@layout BlazorSample.Components.Layout.MainLayout
@inject NavigationManager Navigation

@if (HttpContext is null)
{
    <p>Loading...</p>
}
else
{
    @Body
}

@code {
    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    protected override void OnParametersSet()
    {
        if (HttpContext is null)
        {
            Navigation.Refresh(forceReload: true);
        }
    }
}
```

Note

In the Blazor Web App project template for authentication scenarios, there's a second layout file (`ManageLayout.razor` in the `Components/Account/Shared` folder) for Identity components in the `Components/Account/Pages/Manage` folder. The `Manage` folder has its own imports file to apply to the `ManageLayout` to components in the folder. In your own apps, using nested imports files is a useful approach for applying custom layouts to groups of pages.

In the `App` component, any request for a component in the `Account` folder applies a `null` render mode, which enforces static SSR. Other component requests receive a global application of the interactive SSR render mode (`InteractiveServer`).

Important

Applying a `null` render mode doesn't always enforce static SSR. It just happens to behave that way using the approach shown in this section.

A `null` render mode is effectively the same as not specifying a render mode, which results in the component inheriting its parent's render mode. In this case, the `App` component is rendered using static SSR, so a `null` render mode results in the `Routes` component inheriting static SSR from the `App` component. If a null render mode is specified for a child component whose parent uses an interactive render mode, the child inherits the same interactive render mode.

`Components/App.razor`:

```
<Routes @rendermode="RenderModeForPage" />

...

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? RenderModeForPage => 
        HttpContext.Request.Path.StartsWithSegments("/account")
            ? null
            : {INTERACTIVE RENDER MODE};
}
```

In the preceding code, change the `{INTERACTIVE RENDER MODE}` placeholder to the appropriate value, depending on if the rest of the application should adopt global [InteractiveServer](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactiveserver#microsoft-aspnetcore-components-web-rendermode-interactiveserver), [InteractiveWebAssembly](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactivewebassembly#microsoft-aspnetcore-components-web-rendermode-interactivewebassembly), or [InteractiveAuto](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactiveauto#microsoft-aspnetcore-components-web-rendermode-interactiveauto) rendering.

The components that must adopt static SSR in the `Account` folder aren't required to set the layout, which is applied via the imports file (`_Imports.razor`). The components don't set a render mode because they should render with static SSR. Nothing further must be done for the components in the `Account` folder to enforce static SSR.

### Static SSR components spread out across the app

*For an example of the approach in this section, see the [`BlazorWebAppSpreadOutStaticSsrComponents` sample app](https://github.com/dotnet/blazor-samples/tree/main/9.0/BlazorWebAppSpreadOutStaticSsrComponents). The technique described in this section is most appropriate for .NET 8 Blazor Web Apps, but the sample is implemented in .NET 9 using Blazor features that simplify demonstrating how the approach works.*

In the [preceding subsection](#area-folder-of-static-ssr-components), the app controls the render mode of the components by setting the render mode globally in the `App` component. Alternatively, the `App` component can also adopt ***per-component*** render modes for setting the render mode, which permits components spread around the app to enforce adoption of static SSR. This subsection describes the approach.

The app has a custom layout that can be applied to components around the app. Usually, a shared component for the app is placed in the `Components/Layout` folder. The component makes use of [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) to determine if the component has adopted static SSR. If the value of [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is `null`, the component is rendering interactively, and a full-page reload is performed by calling [NavigationManager.Refresh](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.refresh) with `forceLoad` set to `true`. This triggers a request to the server for the component.

`Components/Layout/StaticSsrLayout.razor`:

```
@inherits LayoutComponentBase
@layout MainLayout
@inject NavigationManager Navigation

@if (HttpContext is null)
{
    <p>Loading...</p>
}
else
{
    @Body
}

@code {
    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    protected override void OnParametersSet()
    {
        if (HttpContext is null)
        {
            Navigation.Refresh(forceReload: true);
        }
    }
}
```

In the `App` component, reflection is used to set the render mode. Whatever render mode is assigned to the individual component definition file is applied to the `Routes` component.

`Components/App.razor`:

```
<Routes @rendermode="RenderModeForPage" />

...

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? RenderModeForPage =>
        HttpContext.GetEndpoint()?.Metadata.GetMetadata<RenderModeAttribute>()?
            .Mode;
}
```

Each component that must adopt static SSR sets the custom layout and doesn't specify a render mode. Not specifying a render mode results in a `null` value of [RenderModeAttribute.Mode](/en-us/dotnet/api/microsoft.aspnetcore.components.rendermodeattribute.mode#microsoft-aspnetcore-components-rendermodeattribute-mode) in the `App` component, which results in no render mode assigned to the `Routes` component instance and enforcement of static SSR.

Important

Applying a `null` render mode doesn't always enforce static SSR. It just happens to behave that way using the approach shown in this section.

A `null` render mode is effectively the same as not specifying a render mode, which results in the component inheriting its parent's render mode. In this case, the `App` component is rendered using static SSR, so a `null` render mode results in the `Routes` component inheriting static SSR from the `App` component. If a null render mode is specified for a child component whose parent uses an interactive render mode, the child inherits the same interactive render mode.

Nothing further must be done for the components to enforce static SSR than applying the custom layout ***without setting an interactive render mode***:

```
@layout BlazorSample.Components.Layout.StaticSsrLayout
```

Interactive components around the app ***avoid*** applying the custom static SSR layout and ***only set an appropriate interactive render mode***, which upon reflection in the `App` component is applied to the `Routes` component:

```
@rendermode {INTERACTIVE RENDER MODE}
```

In the preceding code, change the `{INTERACTIVE RENDER MODE}` placeholder to the appropriate value, depending on if the component should adopt [InteractiveServer](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactiveserver#microsoft-aspnetcore-components-web-rendermode-interactiveserver), [InteractiveWebAssembly](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactivewebassembly#microsoft-aspnetcore-components-web-rendermode-interactivewebassembly), or [InteractiveAuto](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode.interactiveauto#microsoft-aspnetcore-components-web-rendermode-interactiveauto) rendering.

## Custom shorthand render modes

The `@rendermode` directive takes a single parameter that's a static instance of type [IComponentRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.icomponentrendermode). The `@rendermode` directive attribute can take any render mode instance, static or not. The Blazor framework provides the [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) static class with some predefined render modes for convenience, but you can create your own.

Normally, a component uses the following `@rendermode` directive to [disable prerendering](prerender?view=aspnetcore-10.0#disable-prerendering):

```
@rendermode @(new InteractiveServerRenderMode(prerender: false))
```

However, consider the following example that creates a shorthand interactive server-side render mode without prerendering via the app's imports file (`Components/_Imports.razor`):

```
public static IComponentRenderMode InteractiveServerWithoutPrerendering { get; } = 
    new InteractiveServerRenderMode(prerender: false);
```

Use the shorthand render mode in components throughout the `Components` folder:

```
@rendermode InteractiveServerWithoutPrerendering
```

Alternatively, a single component instance can define a custom render mode via a private field:

```
@rendermode interactiveServerWithoutPrerendering

...

@code {
    private static IComponentRenderMode interactiveServerWithoutPrerendering = 
        new InteractiveServerRenderMode(prerender: false);
}
```

At the moment, the shorthand render mode approach is probably only useful for reducing the verbosity of specifying the `prerender` flag. The shorthand approach might be more useful in the future if additional flags become available for interactive rendering and you would like to create shorthand render modes with different combinations of flags.

## Related content

* WebSocket compression

  * [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#websocket-compression-for-interactive-server-components)
  * [Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering](../security/interactive-server-side-rendering?view=aspnetcore-10.0#interactive-server-components-with-websocket-compression-enabled)

* [ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](../javascript-interoperability/static-server-rendering?view=aspnetcore-10.0)

* [Cascading values/parameters and render mode boundaries](cascading-values-and-parameters?view=aspnetcore-10.0#cascading-valuesparameters-and-render-mode-boundaries): Also see the [Root-level cascading values](cascading-values-and-parameters?view=aspnetcore-10.0#root-level-cascading-values) and [Root-level cascading values with notifications](cascading-values-and-parameters?view=aspnetcore-10.0#root-level-cascading-values-with-notifications) sections earlier in the article.

* [ASP.NET Core Razor class libraries (RCLs) with static server-side rendering (static SSR)](class-libraries-and-static-server-side-rendering?view=aspnetcore-10.0)

* [Secure data in Blazor Web Apps with Interactive Auto rendering](../security/?view=aspnetcore-10.0#secure-data-in-blazor-web-apps-with-interactive-auto-rendering)

- [ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](../javascript-interoperability/static-server-rendering?view=aspnetcore-10.0)
- [Cascading values/parameters and render mode boundaries](cascading-values-and-parameters?view=aspnetcore-10.0#cascading-valuesparameters-and-render-mode-boundaries): Also see the [Root-level cascading values](cascading-values-and-parameters?view=aspnetcore-10.0#root-level-cascading-values) and [Root-level cascading values with notifications](cascading-values-and-parameters?view=aspnetcore-10.0#root-level-cascading-values-with-notifications) sections earlier in the article.
- [ASP.NET Core Razor class libraries (RCLs) with static server-side rendering (static SSR)](class-libraries-and-static-server-side-rendering?view=aspnetcore-10.0)
- [Secure data in Blazor Web Apps with Interactive Auto rendering](../security/?view=aspnetcore-10.0#secure-data-in-blazor-web-apps-with-interactive-auto-rendering)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/binding
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/forms/binding.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor forms binding

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to use binding in Blazor forms.

## `EditForm`/`EditContext` model

An [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) creates an [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext) based on the assigned object as a [cascading value](../components/cascading-values-and-parameters?view=aspnetcore-10.0) for other components in the form. The [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext) tracks metadata about the edit process, including which form fields have been modified and the current validation messages. Assigning to either an [EditForm.Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model#microsoft-aspnetcore-components-forms-editform-model) or an [EditForm.EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.editcontext#microsoft-aspnetcore-components-forms-editform-editcontext) can bind a form to data.

## Model binding

Assignment to [EditForm.Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model#microsoft-aspnetcore-components-forms-editform-model):

```
<EditForm ... Model="Model" ...>
    ...
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();
}
```

```
<EditForm ... Model="Model" ...>
    ...
</EditForm>

@code {
    public Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();
}
```

Note

Most of this article's form model examples bind forms to C# *properties*, but C# field binding is also supported.

When a different object instance is assigned to the [Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model#microsoft-aspnetcore-components-forms-editform-model) parameter of `EditForm`, the form creates a new `EditContext`. The new context starts with no modified fields or validation messages.

## Context binding

Assignment to [EditForm.EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.editcontext#microsoft-aspnetcore-components-forms-editform-editcontext):

```
<EditForm ... EditContext="editContext" ...>
    ...
</EditForm>

@code {
    private EditContext? editContext;

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

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
    }
}
```

```
<EditForm ... EditContext="editContext" ...>
    ...
</EditForm>

@code {
    private EditContext? editContext;

    public Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
    }
}
```

Assign **either** an [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.editcontext#microsoft-aspnetcore-components-forms-editform-editcontext) **or** a [Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model#microsoft-aspnetcore-components-forms-editform-model) to an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform). If both are assigned, a runtime error is thrown.

## Supported types

Binding supports:

* Primitive types
* Collections
* Complex types
* Recursive types
* Types with constructors
* Enums

You can also use the [`[DataMember]`](/en-us/dotnet/api/system.runtime.serialization.datamemberattribute) and [`[IgnoreDataMember]`](/en-us/dotnet/api/system.runtime.serialization.ignoredatamemberattribute) attributes to customize model binding. Use these attributes to rename properties, ignore properties, and mark properties as required.

When binding a type with constructor parameters, if a constructor parameter matches a property by name, the constructor parameter takes precedence. The mapper uses the property's explicit `DataMember.Name`, if present, as the form field name, but otherwise ignores the property's mapping attributes. Constructor parameters are always required.

## Additional binding options

Additional model binding options are available from [RazorComponentsServiceOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions) when calling [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents):

* [MaxFormMappingCollectionSize](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions.maxformmappingcollectionsize): Maximum number of elements allowed in a form collection.
* [MaxFormMappingRecursionDepth](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions.maxformmappingrecursiondepth): Maximum depth allowed when recursively mapping form data.
* [MaxFormMappingErrorCount](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions.maxformmappingerrorcount): Maximum number of errors allowed when mapping form data.
* [MaxFormMappingKeySize](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions.maxformmappingkeysize): Maximum size of the buffer used to read form data keys.

The following demonstrates the default values assigned by the framework:

```
builder.Services.AddRazorComponents(options =>
{
    options.FormMappingUseCurrentCulture = true;
    options.MaxFormMappingCollectionSize = 1024;
    options.MaxFormMappingErrorCount = 200;
    options.MaxFormMappingKeySize = 1024 * 2;
    options.MaxFormMappingRecursionDepth = 64;
}).AddInteractiveServerComponents();
```

## Form names

Use the [FormName](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.formname) parameter to assign a form name. Form names must be unique to bind model data. The following form is named `RomulanAle`:

```
<EditForm ... FormName="RomulanAle" ...>
    ...
</EditForm>
```

Supplying a form name:

* Is required for all forms that are submitted by statically-rendered server-side components.
* Isn't required for forms that are submitted by interactively-rendered components, which includes forms in Blazor WebAssembly apps and components with an interactive render mode. However, we recommend supplying a unique form name for every form to prevent runtime form posting errors if interactivity is ever dropped for a form.

The form name is only checked when the form is posted to an endpoint as a traditional HTTP POST request from a statically-rendered server-side component. The framework doesn't throw an exception at the point of rendering a form, but only at the point that an HTTP POST arrives and doesn't specify a form name.

There's an unnamed (empty string) form scope above the app's root component, which suffices when there are no form name collisions in the app. If form name collisions are possible, such as when including a form from a library and you have no control of the form name used by the library's developer, provide a form name scope with the [FormMappingScope](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.formmappingscope) component in the Blazor Web App's main project.

In the following example, the `HelloFormFromLibrary` component has a form named `Hello` and is in a library.

`HelloFormFromLibrary.razor`:

```
<EditForm FormName="Hello" Model="this" OnSubmit="Submit">
    <InputText @bind-Value="Name" />
    <button type="submit">Submit</button>
</EditForm>

@if (submitted)
{
    <p>Hello @Name from the library's form!</p>
}

@code {
    bool submitted = false;

    [SupplyParameterFromForm]
    private string? Name { get; set; }

    private void Submit() => submitted = true;
}
```

The following `NamedFormsWithScope` component uses the library's `HelloFormFromLibrary` component and also has a form named `Hello`. The [FormMappingScope](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.formmappingscope) component's scope name is `ParentContext` for any forms supplied by the `HelloFormFromLibrary` component. Although both of the forms in this example have the form name (`Hello`), the form names don't collide and events are routed to the correct form for form POST events.

`NamedFormsWithScope.razor`:

```
@page "/named-forms-with-scope"

<div>Hello form from a library</div>

<FormMappingScope Name="ParentContext">
    <HelloFormFromLibrary />
</FormMappingScope>

<div>Hello form using the same form name</div>

<EditForm FormName="Hello" Model="this" OnSubmit="Submit">
    <InputText @bind-Value="Name" />
    <button type="submit">Submit</button>
</EditForm>

@if (submitted)
{
    <p>Hello @Name from the app form!</p>
}

@code {
    bool submitted = false;

    [SupplyParameterFromForm]
    private string? Name { get; set; }

    private void Submit() => submitted = true;
}
```

## Supply a parameter from the form (`[SupplyParameterFromForm]`)

The `[SupplyParameterFromForm]` attribute indicates that the value of the associated property should be supplied from the form data for the form. Data in the request that matches the name of the property is bound to the property. Inputs based on `InputBase<TValue>` generate form value names that match the names Blazor uses for model binding. Unlike component parameter properties (`[Parameter]`), properties annotated with `[SupplyParameterFromForm]` aren't required to be marked `public`.

Blazor form mapping with [`[SupplyParameterFromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromformattribute) doesn't use MVC model binding. Attributes in the [Microsoft.AspNetCore.Mvc.ModelBinding](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding) namespace, such as [`[BindNever]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindneverattribute) and [`[BindRequired]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindrequiredattribute), aren't supported. Don't use these attributes to prevent overposting. Instead, use a dedicated form model, view model, or data transfer object (DTO) that includes only the properties users are allowed to modify. For more information, see [Mitigate overposting attacks](./?view=aspnetcore-10.0#mitigate-overposting-attacks).

You can specify the following form binding parameters to the [`[SupplyParameterFromForm]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromformattribute):

* [Name](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromformattribute.name): Gets or sets the name for the parameter. The name is used to determine the prefix to use to match the form data and decide whether or not the value needs to be bound.
* [FormName](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromformattribute.formname): Gets or sets the name for the handler. The name is used to match the parameter to the form by form name to decide whether or not the value needs to be bound.

The following example independently binds two forms to their models by form name.

`Starship6.razor`:

```
@page "/starship-6"
@inject ILogger<Starship6> Logger

<EditForm Model="Model1" OnSubmit="Submit1" FormName="Holodeck1">
    <div>
        <label>
            Holodeck 1 Identifier: 
            <InputText @bind-Value="Model1!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<EditForm Model="Model2" OnSubmit="Submit2" FormName="Holodeck2">
    <div>
        <label>
            Holodeck 2 Identifier: 
            <InputText @bind-Value="Model2!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm(FormName = "Holodeck1")]
    private Holodeck? Model1 { get; set; }

    [SupplyParameterFromForm(FormName = "Holodeck2")]
    private Holodeck? Model2 { get; set; }

    protected override void OnInitialized()
    {
        Model1 ??= new();
        Model2 ??= new();
    }

    private void Submit1() => Logger.LogInformation("Submit1: Id={Id}", Model1?.Id);

    private void Submit2() => Logger.LogInformation("Submit2: Id={Id}", Model2?.Id);

    public class Holodeck
    {
        public string? Id { get; set; }
    }
}
```

```
@page "/starship-6"
@inject ILogger<Starship6> Logger

<EditForm Model="Model1" OnSubmit="Submit1" FormName="Holodeck1">
    <div>
        <label>
            Holodeck 1 Identifier: 
            <InputText @bind-Value="Model1!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<EditForm Model="Model2" OnSubmit="Submit2" FormName="Holodeck2">
    <div>
        <label>
            Holodeck 2 Identifier: 
            <InputText @bind-Value="Model2!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm(FormName = "Holodeck1")]
    private Holodeck? Model1 { get; set; }

    [SupplyParameterFromForm(FormName = "Holodeck2")]
    private Holodeck? Model2 { get; set; }

    protected override void OnInitialized()
    {
        Model1 ??= new();
        Model2 ??= new();
    }

    private void Submit1() => Logger.LogInformation("Submit1: Id={Id}", Model1?.Id);

    private void Submit2() => Logger.LogInformation("Submit2: Id={Id}", Model2?.Id);

    public class Holodeck
    {
        public string? Id { get; set; }
    }
}
```

## Nest and bind forms

The following guidance demonstrates how to nest and bind child forms.

The following ship details class (`ShipDetails`) holds a description and length for a subform.

`ShipDetails.cs`:

```
namespace BlazorSample;

public class ShipDetails
{
    public string? Description { get; set; }
    public int? Length { get; set; }
}
```

```
namespace BlazorSample;

public class ShipDetails
{
    public string? Description { get; set; }
    public int? Length { get; set; }
}
```

The following `Ship` class names an identifier (`Id`) and includes the ship details.

`Ship.cs`:

```
namespace BlazorSample
{
    public class Ship
    {
        public string? Id { get; set; }
        public ShipDetails Details { get; set; } = new();
    }
}
```

```
namespace BlazorSample
{
    public class Ship
    {
        public string? Id { get; set; }
        public ShipDetails Details { get; set; } = new();
    }
}
```

The following subform is used for editing values of the `ShipDetails` type. This is implemented by inheriting [Editor\<T>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editor-1) at the top of the component. [Editor\<T>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editor-1) ensures that the child component generates the correct form field names based on the model (`T`), where `T` in the following example is `ShipDetails`.

`StarshipSubform.razor`:

```
@inherits Editor<ShipDetails>

<div>
    <label>
        Description: 
        <InputText @bind-Value="Value!.Description" />
    </label>
</div>
<div>
    <label>
        Length: 
        <InputNumber @bind-Value="Value!.Length" />
    </label>
</div>
```

```
@inherits Editor<ShipDetails>

<div>
    <label>
        Description: 
        <InputText @bind-Value="Value!.Description" />
    </label>
</div>
<div>
    <label>
        Length: 
        <InputNumber @bind-Value="Value!.Length" />
    </label>
</div>
```

The main form is bound to the `Ship` class. The `StarshipSubform` component is used to edit ship details, bound as `Model!.Details`.

`Starship7.razor`:

```
@page "/starship-7"
@inject ILogger<Starship7> Logger

<EditForm Model="Model" OnSubmit="Submit" FormName="Starship7">
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <StarshipSubform @bind-Value="Model!.Details" />
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Ship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => 
        Logger.LogInformation("Id = {Id} Desc = {Description} Length = {Length}",
            Model?.Id, Model?.Details?.Description, Model?.Details?.Length);
}
```

```
@page "/starship-7"
@inject ILogger<Starship7> Logger

<EditForm Model="Model" OnSubmit="Submit" FormName="Starship7">
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <StarshipSubform @bind-Value="Model!.Details" />
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Ship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => 
        Logger.LogInformation("Id = {Id} Desc = {Description} Length = {Length}",
            Model?.Id, Model?.Details?.Description, Model?.Details?.Length);
}
```

## Initialize form data with static SSR

When a component adopts static SSR, the [`OnInitialized{Async}` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) and the [`OnParametersSet{Async}` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#after-parameters-are-set-onparameterssetasync) fire when the component is initially rendered and on every form POST to the server. To initialize form model values, confirm if the model already has data before assigning new model values in `OnParametersSet{Async}`, as the following example demonstrates.

`StarshipInit.razor`:

```
@page "/starship-init"
@inject ILogger<StarshipInit> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="StarshipInit">
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    protected override void OnParametersSet()
    {
        if (Model!.Id == default)
        {
            LoadData();
        }
    }

    private void LoadData()
    {
        Model!.Id = "Set by LoadData";
    }

    private void Submit()
    {
        Logger.LogInformation("Id = {Id}", Model?.Id);
    }

    public class Starship
    {
        public string? Id { get; set; }
    }
}
```

## Advanced form mapping error scenarios

The framework instantiates and populates the [FormMappingContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.formmappingcontext) for a form, which is the context associated with a given form's mapping operation. Each mapping scope (defined by a [FormMappingScope](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.formmappingscope) component) instantiates [FormMappingContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.formmappingcontext). Each time a `[SupplyParameterFromForm]` asks the context for a value, the framework populates the [FormMappingContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.formmappingcontext) with the attempted value and any mapping errors.

Developers aren't expected to interact with [FormMappingContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.formmappingcontext) directly, as it's mainly a source of data for [InputBase\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1), [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext), and other internal implementations to show mapping errors as validation errors. In advanced custom scenarios, developers can access [FormMappingContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.formmappingcontext) directly as a `[CascadingParameter]` to write custom code that consumes the attempted values and mapping errors.

## `InputText` based on the input event

Use the [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext) component to create a custom component that uses the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)) instead of the `onchange` event ([`change`](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)). Use of the `input` event triggers field validation on each keystroke.

The following `CustomInputText` component inherits the framework's `InputText` component and sets event binding to the `oninput` event ([`input`](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event)).

`CustomInputText.razor`:

```
@inherits InputText

<input @attributes="AdditionalAttributes" 
       class="@CssClass" 
       @bind="CurrentValueAsString" 
       @bind:event="oninput" />
```

The `CustomInputText` component can be used anywhere [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext) is used. The following component uses the shared `CustomInputText` component.

`Starship11.razor`:

```
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <CustomInputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
```

```
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship11">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <CustomInputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
```

```
@page "/starship-11"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship11> Logger

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <CustomInputText @bind-Value="Model!.Id" />
    <button type="submit">Submit</button>
</EditForm>

<div>
    CurrentValue: @Model?.Id
</div>

@code {
    public Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
```

## Custom input components

For custom input processing scenarios, the following subsections demonstrate custom input components:

* [Input component based on `InputBase<T>`](#input-component-based-on-inputbaset): The component inherits from [InputBase\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1), which provides a base implementation for binding, callbacks, and validation. Components that inherit from [InputBase\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1) must be used in a Blazor form ([EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform)).

* [Input component with full developer control](#input-component-with-full-developer-control): The component takes full control of input processing. The component's code must manage binding, callbacks, and validation. The component can be used inside or outside of a Blazor form.

We recommend that you derive your custom input components from [InputBase\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1) unless specific requirements prevent you from doing so. The [InputBase\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1) class is actively maintained by the ASP.NET Core team, ensuring it stays up-to-date with the latest Blazor features and framework changes.

### Input component based on `InputBase<T>`

The following example component:

* Inherits from [InputBase\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1). Components that inherit from [InputBase\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1) must be used in a Blazor form ([EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform)).
* Takes boolean input from a checkbox.
* Sets the background color of its container `<div>` based on the checkbox's state, which occurs when the `AfterChange` method executes after binding (`@bind:after`).
* Is required to override the base class's `TryParseValueFromString` method but doesn't process string input data because a checkbox doesn't provide string data. Example implementations of `TryParseValueFromString` for other types of input components that process string input are available in the [ASP.NET Core reference source](https://github.com/search?q=repo%3Adotnet%2Faspnetcore%20TryParseValueFromString\&type=code).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

`EngineeringApprovalInputDerived.razor`:

```
@using System.Diagnostics.CodeAnalysis
@inherits InputBase<bool>

<div class="@divCssClass">
    <label>
        Engineering Approval:
        <input @bind="CurrentValue" @bind:after="AfterChange" class="@CssClass" 
            type="checkbox" />
    </label>
</div>

@code {
    private string? divCssClass;

    private void AfterChange()
    {
        divCssClass = CurrentValue ? "bg-success text-white" : null;
    }

    protected override bool TryParseValueFromString(
        string? value, out bool result, 
        [NotNullWhen(false)] out string? validationErrorMessage)
            => throw new NotSupportedException(
                "This component does not parse string inputs. " +
                $"Bind to the '{nameof(CurrentValue)}' property, " +
                $"not '{nameof(CurrentValueAsString)}'.");
}
```

To use the preceding component in the [starship example form (`Starship3.razor`/`Starship.cs`)](input-components?view=aspnetcore-10.0#example-form), replace the `<div>` block for the engineering approval field with an `EngineeringApprovalInputDerived` component instance bound to the model's `IsValidatedDesign` property:

```
- <div>
-     <label>
-         Engineering Approval: 
-         <InputCheckbox @bind-Value="Model!.IsValidatedDesign" />
-     </label>
- </div>
+ <EngineeringApprovalInputDerived @bind-Value="Model!.IsValidatedDesign" />
```

If the component that inherits from [InputBase\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1) is ever statically rendered, assign the [InputBase\<TValue>.NameAttributeValue](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1.nameattributevalue#microsoft-aspnetcore-components-forms-inputbase-1-nameattributevalue) property to the `name` attribute of `<input>` elements:

```
<input @bind="CurrentValue" @bind:after="AfterChange" class="@CssClass"
    type="checkbox" name="@NameAttributeValue" />
```

The preceding assignment isn't necessary if the component is guaranteed to always render interactively.

### Input component with full developer control

The following example component:

* Doesn't inherit from [InputBase\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1). The component takes full control of input processing, including binding, callbacks, and validation. The component can be used inside or outside of a Blazor form ([EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform)).
* Takes boolean input from a checkbox.
* Changes the background color if the checkbox is checked.

Code in the component includes:

* The `Value` property is used with two-way binding to get or set the value of the input. `ValueChanged` is the callback that updates the bound value.

* When used in a Blazor form:

  * The [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext) is a [cascading value](../components/cascading-values-and-parameters?view=aspnetcore-10.0).
  * `fieldCssClass` styles the field based on the result of [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext) validation.
  * `ValueExpression` is an expression (`Expression<Func<T>>`) assigned by the framework that identifies the bound value.
  * [FieldIdentifier](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.fieldidentifier) uniquely identifies a single field that can be edited, usually corresponding to a model property. The field identifier is created with the expression that identifies the bound value (`ValueExpression`).

* In the `OnChange` event handler:

  * The value of the checkbox input is obtained from [InputFileChangeEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfilechangeeventargs).
  * The background color and text color of the container `<div>` element are set.
  * [EventCallback.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback.invokeasync) invokes the delegate associated with the binding and dispatches an event notification to consumers that the value has changed.
  * If the component is used in an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) (the `EditContext` property isn't `null`), [EditContext.NotifyFieldChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.notifyfieldchanged) is called to trigger validation.

`EngineeringApprovalInputStandalone.razor`:

```
@using System.Globalization
@using System.Linq.Expressions

<div class="@divCssClass">
    <label>
        Engineering Approval:
        <input class="@fieldCssClass" @onchange="OnChange" type="checkbox" 
            value="@Value" />
    </label>
</div>

@code {
    private string? divCssClass;
    private FieldIdentifier fieldIdentifier;
    private string? fieldCssClass => EditContext?.FieldCssClass(fieldIdentifier);

    [CascadingParameter]
    private EditContext? EditContext { get; set; }

    [Parameter]
    public bool? Value { get; set; }

    [Parameter]
    public EventCallback<bool> ValueChanged { get; set; }

    [Parameter]
    public Expression<Func<bool>>? ValueExpression { get; set; }

    protected override void OnInitialized()
    {
        fieldIdentifier = FieldIdentifier.Create(ValueExpression!);
    }

    private async Task OnChange(ChangeEventArgs args)
    {
        BindConverter.TryConvertToBool(args.Value, CultureInfo.CurrentCulture, 
            out var value);

        divCssClass = value ? "bg-success text-white" : null;

        await ValueChanged.InvokeAsync(value);
        EditContext?.NotifyFieldChanged(fieldIdentifier);
    }
}
```

To use the preceding component in the [starship example form (`Starship3.razor`/`Starship.cs`)](input-components?view=aspnetcore-10.0#example-form), replace the `<div>` block for the engineering approval field with a `EngineeringApprovalInputStandalone` component instance bound to the model's `IsValidatedDesign` property:

```
- <div>
-     <label>
-         Engineering Approval: 
-         <InputCheckbox @bind-Value="Model!.IsValidatedDesign" />
-     </label>
- </div>
+ <EngineeringApprovalInputStandalone @bind-Value="Model!.IsValidatedDesign" />
```

The `EngineeringApprovalInputStandalone` component is also functional outside of an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform):

```
<EngineeringApprovalInputStandalone @bind-Value="ValidDesign" />

<div>
    <b>ValidDesign:</b> @ValidDesign
</div>

@code {
    private bool ValidDesign { get; set; }
}
```

## Radio buttons

The example in this section is based on the `Starfleet Starship Database` form (`Starship3` component) of the [Example form](input-components?view=aspnetcore-10.0#example-form) section of this article.

Add the following [`enum` types](/en-us/dotnet/csharp/language-reference/language-specification/enums) to the app. Create a new file to hold them or add them to the `Starship.cs` file.

```
public class ComponentEnums
{
    public enum Manufacturer { SpaceX, NASA, ULA, VirginGalactic, Unknown }
    public enum Color { ImperialRed, SpacecruiserGreen, StarshipBlue, VoyagerOrange }
    public enum Engine { Ion, Plasma, Fusion, Warp }
}
```

Make the `ComponentEnums` class accessible to the:

* `Starship` model in `Starship.cs` (for example, `using static ComponentEnums;`).
* `Starfleet Starship Database` form (`Starship3.razor`) (for example, `@using static ComponentEnums`).

Use [InputRadio\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradio-1) components with the [InputRadioGroup\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradiogroup-1) component to create a radio button group. In the following example, properties are added to the `Starship` model described in the [Example form](input-components?view=aspnetcore-10.0#example-form) section of the *Input components* article:

```
[Required]
[Range(typeof(Manufacturer), nameof(Manufacturer.SpaceX), 
    nameof(Manufacturer.VirginGalactic), ErrorMessage = "Pick a manufacturer.")]
public Manufacturer Manufacturer { get; set; } = Manufacturer.Unknown;

[Required, EnumDataType(typeof(Color))]
public Color? Color { get; set; } = null;

[Required, EnumDataType(typeof(Engine))]
public Engine? Engine { get; set; } = null;
```

Update the `Starfleet Starship Database` form (`Starship3` component) of the [Example form](input-components?view=aspnetcore-10.0#example-form) section of the *Input components* article. Add the components to produce:

* A radio button group for the ship manufacturer.
* A nested radio button group for engine and ship color.

Note

Nested radio button groups aren't often used in forms because they can result in a disorganized layout of form controls that may confuse users. However, there are cases when they make sense in UI design, such as in the following example that pairs recommendations for two user inputs, ship engine and ship color. One engine and one color are required by the form's validation. The form's layout uses nested [InputRadioGroup\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradiogroup-1)s to pair engine and color recommendations. However, the user can combine any engine with any color to submit the form.

Note

Be sure to make the `ComponentEnums` class available to the component for the following example:

```
@using static ComponentEnums
```

```
<fieldset>
    <legend>Manufacturer</legend>
    <InputRadioGroup @bind-Value="Model!.Manufacturer">
        @foreach (var manufacturer in Enum.GetValues<Manufacturer>())
        {
            <div>
                <label>
                    <InputRadio Value="manufacturer" />
                    @manufacturer
                </label>
            </div>
        }
    </InputRadioGroup>
</fieldset>

<fieldset>
    <legend>Engine and Color</legend>
    <p>
        Engine and color pairs are recommended, but any
        combination of engine and color is allowed.
    </p>
    <InputRadioGroup Name="engine" @bind-Value="Model!.Engine">
        <InputRadioGroup Name="color" @bind-Value="Model!.Color">
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Ion" />
                        Ion
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.ImperialRed" />
                        Imperial Red
                    </label>
                </div>
            </div>
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Plasma" />
                        Plasma
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.SpacecruiserGreen" />
                        Spacecruiser Green
                    </label>
                </div>
            </div>
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Fusion" />
                        Fusion
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.StarshipBlue" />
                        Starship Blue
                    </label>
                </div>
            </div>
            <div style="margin-bottom:5px">
                <div>
                    <label>
                        <InputRadio Name="engine" Value="Engine.Warp" />
                        Warp
                    </label>
                </div>
                <div>
                    <label>
                        <InputRadio Name="color" Value="Color.VoyagerOrange" />
                        Voyager Orange
                    </label>
                </div>
            </div>
        </InputRadioGroup>
    </InputRadioGroup>
</fieldset>
```

Note

If `Name` is omitted, [InputRadio\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradio-1) components are grouped by their most recent ancestor.

If you implemented the preceding Razor markup in the `Starship3` component of the [Example form](input-components?view=aspnetcore-10.0#example-form) section of the *Input components* article, update the logging for the `Submit` method:

```
Logger.LogInformation("Id = {Id} Description = {Description} " +
    "Classification = {Classification} MaximumAccommodation = " +
    "{MaximumAccommodation} IsValidatedDesign = " +
    "{IsValidatedDesign} ProductionDate = {ProductionDate} " +
    "Manufacturer = {Manufacturer}, Engine = {Engine}, " +
    "Color = {Color}",
    Model?.Id, Model?.Description, Model?.Classification,
    Model?.MaximumAccommodation, Model?.IsValidatedDesign,
    Model?.ProductionDate, Model?.Manufacturer, Model?.Engine, 
    Model?.Color);
```

When working with radio buttons in a form, data binding is handled differently than other elements because radio buttons are evaluated as a group. The value of each radio button is fixed, but the value of the radio button group is the value of the selected radio button. The following example shows how to:

* Handle data binding for a radio button group.
* Support validation using a custom [InputRadio\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradio-1) component.

`InputRadio.razor`:

```
@using System.Globalization
@inherits InputBase<TValue>
@typeparam TValue

<input @attributes="AdditionalAttributes" type="radio" value="@SelectedValue" 
       checked="@(SelectedValue.Equals(Value))" @onchange="OnChange" />

@code {
    [Parameter]
    public TValue SelectedValue { get; set; }

    private void OnChange(ChangeEventArgs args)
    {
        CurrentValueAsString = args.Value.ToString();
    }

    protected override bool TryParseValueFromString(string value, 
        out TValue result, out string errorMessage)
    {
        var success = BindConverter.TryConvertTo<TValue>(
            value, CultureInfo.CurrentCulture, out var parsedValue);
        if (success)
        {
            result = parsedValue;
            errorMessage = null;

            return true;
        }
        else
        {
            result = default;
            errorMessage = "The field isn't valid.";

            return false;
        }
    }
}
```

For more information on generic type parameters (`@typeparam`), see the following articles:

* [Razor syntax reference for ASP.NET Core](../../mvc/views/razor?view=aspnetcore-10.0#typeparam)
* [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#generic-type-parameter-support)
* [ASP.NET Core Blazor templated components](../components/templated-components?view=aspnetcore-10.0)

Use the following example model.

`StarshipModel.cs`:

```
using System.ComponentModel.DataAnnotations;

namespace BlazorServer80
{
    public class Model
    {
        [Range(1, 5)]
        public int Rating { get; set; }
    }
}
```

The following `RadioButtonExample` component uses the preceding `InputRadio` component to obtain and validate a rating from the user:

`RadioButtonExample.razor`:

```
@page "/radio-button-example"
@using System.ComponentModel.DataAnnotations
@using Microsoft.Extensions.Logging
@inject ILogger<RadioButtonExample> Logger

<h1>Radio Button Example</h1>

<EditForm Model="Model" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    @for (int i = 1; i <= 5; i++)
    {
        <div>
            <label>
                <InputRadio name="rate" SelectedValue="i" 
                    @bind-Value="Model.Rating" />
                @i
            </label>
        </div>
    }

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

<div>@Model.Rating</div>

@code {
    public StarshipModel Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-25

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/bl0005
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/bl0005.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# BL0005: Component parameter should not be set outside of its component

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | BL0005       |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A property on a type deriving from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) annotated with [`[Parameter]`](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute) is being assigned to from outside the component.

## Rule description

Component parameters should be assigned to as part of the component initialization or as part of `SetParametersAsync`. Assigning a value to a parameter from an external source results in the value being overwritten the next time the component renders.

## How to fix violations

Consider using a distinct property to receive values from other components. Additional code can then be written to decide which of the two values to use in the component.

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-05-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr-typescript-webpack
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/signalr-typescript-webpack.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Get started with ASP.NET Core SignalR using TypeScript and Webpack

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This tutorial demonstrates using [Webpack](https://webpack.js.org/) in an ASP.NET Core SignalR web app to bundle and build a client written in [TypeScript](https://www.typescriptlang.org/). Webpack enables developers to bundle and build the client-side resources of a web app.

In this tutorial, you learn how to:

* Create an ASP.NET Core SignalR app
* Configure the SignalR server
* Configure a build pipeline using Webpack
* Configure the SignalR TypeScript client
* Enable communication between the client and the server

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/signalr-typescript-webpack/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

* [Node.js](https://nodejs.org/) with [npm](https://www.npmjs.com/)

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)

* [The latest version of Visual Studio](https://visualstudio.microsoft.com/downloads/) with the **ASP.NET and web development** workload.

  [](min-web-api/static/asp-net-web-dev-2026.png?view=aspnetcore-10.0#lightbox)

- [Visual Studio Code](https://code.visualstudio.com/download)
- [C# Dev Kit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit)
- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)

You can follow the Visual Studio Code instructions on macOS, Linux, or Windows. Changes may be required if you use an integrated development environment (IDE) other than Visual Studio Code.

## Create the ASP.NET Core web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

By default, Visual Studio uses the version of npm found in its installation directory. To configure Visual Studio to look for npm in the `PATH` environment variable:

Launch the latest version of Visual Studio. At the start window, select **Continue without code**.

1. Navigate to **Tools** > **Options** > **Projects and Solutions** > **Web Package Management** > **External Web Tools**.

2. Select the `$(PATH)` entry from the list. Select the up arrow to move the entry to the second position in the list, and select **OK**:

To create a new ASP.NET Core web app:

1. Use the **File** > **New** > **Project/Solution...** menu option.
2. In the **Create a new project** dialog, select **ASP.NET Core Empty** template. Then select **Next**.
3. In the **Configure your new project** dialog, enter `SignalRWebpack` for **Project name**. Select **Next**.
4. In the **Additional information** dialog, select **.NET 10.0 (Long Term Support)** from the **Framework** drop-down. Select **Create**.

Add the [Microsoft.TypeScript.MSBuild](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild/) NuGet package to the project:

1. In **Solution Explorer**, right-click the project node and select **Manage NuGet Packages...**.
2. In the **Browse** tab, search for `Microsoft.TypeScript.MSBuild` and then select **Install** on the right to install the package.
3. In the **Preview Changes** dialog, select **Apply**.
4. In the **License Acceptance** dialog, select **I Accept**.

Visual Studio adds the NuGet package under the **Dependencies** node in **Solution Explorer**, enabling TypeScript compilation in the project.

Run the following commands in the **Terminal**:

```
dotnet new web -o SignalRWebpack
code -r SignalRWebpack
```

* The `dotnet new` command creates an empty ASP.NET Core web app in a `SignalRWebpack` directory.
* The `code` command opens the `SignalRWebpack` directory in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Run the following .NET CLI command in the **Terminal**:

```
dotnet add package Microsoft.TypeScript.MSBuild
```

The preceding command adds the [Microsoft.TypeScript.MSBuild](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild/) package, enabling TypeScript compilation in the project.

## Configure the server

In this section, you configure the ASP.NET Core web app to send and receive SignalR messages.

1. In `Program.cs`, call [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr):

   ```
   var builder = WebApplication.CreateBuilder(args);

   builder.Services.AddSignalR();
   ```

2. Again, in `Program.cs`, call [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) and [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

   ```
   var app = builder.Build();

   app.UseDefaultFiles();
   app.UseStaticFiles();
   ```

   The preceding code allows the server to locate and serve the `index.html` file. The file is served whether the user enters its full URL or the root URL of the web app.

3. Create a new directory named `Hubs` in the project root, `SignalRWebpack/`, for the SignalR hub class.

4. Create a new file, `Hubs/ChatHub.cs`, with the following code:

   ```
   using Microsoft.AspNetCore.SignalR;

   namespace SignalRWebpack.Hubs;

   public class ChatHub : Hub
   {
       public async Task NewMessage(long username, string message) =>
           await Clients.All.SendAsync("messageReceived", username, message);
   }
   ```

   The preceding code broadcasts received messages to all connected users once the server receives them. It's unnecessary to have a generic `on` method to receive all the messages. A method named after the message name is enough.

   In this example:

   * The TypeScript client sends a message identified as `newMessage`.
   * The C# `NewMessage` method expects the data sent by the client.
   * A call is made to [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.clientproxyextensions.sendasync) on [Clients.All](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.all#microsoft-aspnetcore-signalr-ihubclients-1-all).
   * The received messages are sent to all clients connected to the hub.

5. Add the following `using` statement at the top of `Program.cs` to resolve the `ChatHub` reference:

   ```
   using SignalRWebpack.Hubs;
   ```

6. In `Program.cs`, map the `/hub` route to the `ChatHub` hub. Replace the code that displays `Hello World!` with the following code:

   ```
   app.MapHub<ChatHub>("/hub");
   ```

## Configure the client

In this section, you create a [Node.js](https://nodejs.org/) project to convert TypeScript to JavaScript and bundle client-side resources, including HTML and CSS, using Webpack.

1. Run the following command in the project root to create a `package.json` file:

   ```
   npm init -y
   ```

2. Add the highlighted property to the `package.json` file and save the file changes:

   ```
   {
     "name": "signalrwebpack",
     "version": "1.0.0",
     "private": true,
     "description": "",
     "main": "index.js",
     "scripts": {
       "build": "webpack --mode=development --watch",
       "release": "webpack --mode=production",
       "publish": "npm run release && dotnet publish -c Release"
     },
     "keywords": [],
     "author": "",
     "license": "ISC",
     "type": "commonjs",
     "devDependencies": {
       "clean-webpack-plugin": "4.0.0",
       "css-loader": "7.1.3",
       "html-webpack-plugin": "5.6.6",
       "mini-css-extract-plugin": "2.10.0",
       "ts-loader": "9.5.4",
       "typescript": "5.9.3",
       "webpack": "5.104.1",
       "webpack-cli": "6.0.1"
     },
     "dependencies": {
       "@microsoft/signalr": "^10.0.0",
       "@types/node": "^25.0.10"
     }
   }
   ```

   Setting the `private` property to `true` prevents package installation warnings in the next step.

3. Install the required npm packages. Run the following command from the project root:

   ```
   npm i -D -E clean-webpack-plugin css-loader html-webpack-plugin mini-css-extract-plugin ts-loader typescript webpack webpack-cli
   ```

   The `-E` option disables npm's default behavior of writing [semantic versioning](https://semver.org/) range operators to `package.json`. For example, `"webpack": "5.76.1"` is used instead of `"webpack": "^5.76.1"`. This option prevents unintended upgrades to newer package versions.

   For more information, see the [npm-install](https://docs.npmjs.com/cli/install) documentation.

4. Replace the `scripts` property of `package.json` file with the following code:

   ```
   "scripts": {
     "build": "webpack --mode=development --watch",
     "release": "webpack --mode=production",
     "publish": "npm run release && dotnet publish -c Release"
   },
   ```

   The following scripts are defined:

   * `build`: Bundles the client-side resources in development mode and watches for file changes. The file watcher causes the bundle to regenerate each time a project file changes. The `mode` option disables production optimizations, such as tree shaking and minification. use `build` in development only.
   * `release`: Bundles the client-side resources in production mode.
   * `publish`: Runs the `release` script to bundle the client-side resources in production mode. It calls the .NET CLI's [publish](/en-us/dotnet/core/tools/dotnet-publish) command to publish the app.

5. Create a file named `webpack.config.js` in the project root, with the following code:

   ```
   const path = require("path");
   const HtmlWebpackPlugin = require("html-webpack-plugin");
   const { CleanWebpackPlugin } = require("clean-webpack-plugin");
   const MiniCssExtractPlugin = require("mini-css-extract-plugin");

   module.exports = {
       entry: "./src/index.ts",
       output: {
           path: path.resolve(__dirname, "wwwroot"),
           filename: "[name].[chunkhash].js",
           publicPath: "/",
       },
       resolve: {
           extensions: [".js", ".ts"],
       },
       module: {
           rules: [
               {
                   test: /\.ts$/,
                   use: "ts-loader",
               },
               {
                   test: /\.css$/,
                   use: [MiniCssExtractPlugin.loader, "css-loader"],
               },
           ],
       },
       plugins: [
           new CleanWebpackPlugin(),
           new HtmlWebpackPlugin({
               template: "./src/index.html",
           }),
           new MiniCssExtractPlugin({
               filename: "css/[name].[chunkhash].css",
           }),
       ],
   };
   ```

   The preceding file configures the Webpack compilation process:

   * The `output` property overrides the default value of `dist`. The bundle is instead emitted in the `wwwroot` directory.
   * The `resolve.extensions` array includes `.js` to import the SignalR client JavaScript.

6. Create a new directory named `src` in the project root, `SignalRWebpack/`, for the client code.

7. Copy the `src` directory and its contents from the [sample project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/signalr-typescript-webpack/samples/) into the project root. The `src` directory contains the following files:

   * `index.html`, which defines the homepage's boilerplate markup:

     ```
     <!DOCTYPE html>
     <html lang="en">
       <head>
         <meta charset="utf-8" />
         <title>ASP.NET Core SignalR with TypeScript and Webpack</title>
       </head>
       <body>
         <div id="divMessages" class="messages"></div>
         <div class="input-zone">
           <label id="lblMessage" for="tbMessage">Message:</label>
           <input id="tbMessage" class="input-zone-input" type="text" />
           <button id="btnSend">Send</button>
         </div>
       </body>
     </html>
     ```

   * `css/main.css`, which provides CSS styles for the homepage:

     ```
     *,
     *::before,
     *::after {
       box-sizing: border-box;
     }

     html,
     body {
       margin: 0;
       padding: 0;
     }

     .input-zone {
       align-items: center;
       display: flex;
       flex-direction: row;
       margin: 10px;
     }

     .input-zone-input {
       flex: 1;
       margin-right: 10px;
     }

     .message-author {
       font-weight: bold;
     }

     .messages {
       border: 1px solid #000;
       margin: 10px;
       max-height: 300px;
       min-height: 300px;
       overflow-y: auto;
       padding: 5px;
     }
     ```

   * `tsconfig.json`, which configures the TypeScript compiler to produce [ECMAScript](https://wikipedia.org/wiki/ECMAScript) 5-compatible JavaScript:

     ```
     {
       "compilerOptions": {
         "target": "es5"
       }
     }
     ```

   * `index.ts`:

     ```
     import * as signalR from "@microsoft/signalr";
     import "./css/main.css";

     const divMessages: HTMLDivElement = document.querySelector("#divMessages");
     const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
     const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
     const username = new Date().getTime();

     const connection = new signalR.HubConnectionBuilder()
         .withUrl("/hub")
         .build();

     connection.on("messageReceived", (username: string, message: string) => {
       const m = document.createElement("div");

       const author = document.createElement("div");
       author.className = "message-author";
       author.textContent = username;

       const content = document.createElement("div");
       content.textContent = message;

       m.append(author, content);

       divMessages.appendChild(m);
       divMessages.scrollTop = divMessages.scrollHeight;
     });

     connection.start().catch((err) => document.write(err));

     tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
       if (e.key === "Enter") {
         send();
       }
     });

     btnSend.addEventListener("click", send);

     function send() {
       connection.send("newMessage", username, tbMessage.value)
         .then(() => (tbMessage.value = ""));
     }
     ```

     The preceding code retrieves references to DOM elements and attaches two event handlers:

     * `keyup`: Fires when the user types in the `tbMessage` textbox and calls the `send` function when the user presses the **Enter** key.
     * `click`: Fires when the user selects the **Send** button and calls `send` function is called.

     The `HubConnectionBuilder` class creates a new builder for configuring the server connection. The `withUrl` function configures the hub URL.

     SignalR enables the exchange of messages between a client and a server. Each message has a specific name. For example, messages with the name `messageReceived` can run the logic responsible for displaying the new message in the messages zone. Listening to a specific message can be done via the `on` function. Any number of message names can be listened to. It's also possible to pass parameters to the message, such as the author's name and the content of the message received. Once the client receives a message, a new `div` element is created with the author's name and message content appended as child elements using `textContent`. It's added to the main `div` element displaying the messages.

     Sending a message through the WebSockets connection requires calling the `send` method. The method's first parameter is the message name. The message data inhabits the other parameters. In this example, a message identified as `newMessage` is sent to the server. The message consists of the username and the user input from a text box. If the send works, the text box value is cleared.

8. Run the following command at the project root:

   ```
   npm i @microsoft/signalr @types/node
   ```

   The preceding command installs:

   * The [SignalR TypeScript client](https://www.npmjs.com/package/@microsoft/signalr), which allows the client to send messages to the server.
   * The TypeScript type definitions for Node.js, which enables compile-time checking of Node.js types.

## Test the app

Confirm that the app works with the following steps:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

1. Run Webpack in `release` mode. Using the **Package Manager Console** window, run the following command in the project root.

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2. Select **Debug** > **Start without debugging** to launch the app in a browser without attaching the debugger. The `wwwroot/index.html` file is served at `https://localhost:<port>`.

   If there are compile errors, try closing and reopening the solution.

3. Open another browser instance (any browser) and paste the URL in the address bar.

4. Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

1) Run Webpack in `release` mode by executing the following command in the project root:

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2) Build and run the app by executing the following command in the project root:

   ```
   dotnet run
   ```

   The web server starts the app and makes it available on localhost.

3) Open a browser to `https://localhost:<port>`. The `wwwroot/index.html` file is served. Copy the URL from the address bar.

4) Open another browser instance (any browser). Paste the URL in the address bar.

5) Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

## Next steps

* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#use-strongly-typed-hubs)
* [Authentication and authorization in ASP.NET Core SignalR](../signalr/authn-and-authz?view=aspnetcore-10.0)
* [MessagePack Hub Protocol in SignalR for ASP.NET Core](../signalr/messagepackhubprotocol?view=aspnetcore-10.0)

## Related content

* [ASP.NET Core SignalR JavaScript client](../signalr/javascript-client?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](../signalr/hubs?view=aspnetcore-10.0)

This tutorial demonstrates using [Webpack](https://webpack.js.org/) in an ASP.NET Core SignalR web app to bundle and build a client written in [TypeScript](https://www.typescriptlang.org/). Webpack enables developers to bundle and build the client-side resources of a web app.

In this tutorial, you learn how to:

* Create an ASP.NET Core SignalR app
* Configure the SignalR server
* Configure a build pipeline using Webpack
* Configure the SignalR TypeScript client
* Enable communication between the client and the server

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/tutorials/signalr-typescript-webpack/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

* [Node.js](https://nodejs.org/) with [npm](https://www.npmjs.com/)

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)

* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) with the **ASP.NET and web development** workload.

- [Visual Studio Code](https://code.visualstudio.com/download)
- [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use something other than Visual Studio Code.

## Create the ASP.NET Core web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

By default, Visual Studio uses the version of npm found in its installation directory. To configure Visual Studio to look for npm in the `PATH` environment variable:

Launch Visual Studio. At the start window, select **Continue without code**.

1. Navigate to **Tools** > **Options** > **Projects and Solutions** > **Web Package Management** > **External Web Tools**.

2. Select the `$(PATH)` entry from the list. Select the up arrow to move the entry to the second position in the list, and select **OK**:

   .

To create a new ASP.NET Core web app:

1. Use the **File** > **New** > **Project** menu option and choose the **ASP.NET Core Empty** template. Select **Next**.
2. Name the project `SignalRWebpack`, and select **Create**.
3. Select **.NET 8.0 (Long Term Support)** from the **Framework** drop-down. Select **Create**.

Add the [Microsoft.TypeScript.MSBuild](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild/) NuGet package to the project:

1. In **Solution Explorer**, right-click the project node and select **Manage NuGet Packages**. In the **Browse** tab, search for `Microsoft.TypeScript.MSBuild` and then select **Install** on the right to install the package.

Visual Studio adds the NuGet package under the **Dependencies** node in **Solution Explorer**, enabling TypeScript compilation in the project.

Run the following commands in the **Integrated Terminal**:

```
dotnet new web -o SignalRWebpack
code -r SignalRWebpack
```

* The `dotnet new` command creates an empty ASP.NET Core web app in a `SignalRWebpack` directory.
* The `code` command opens the `SignalRWebpack` directory in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Run the following .NET CLI command in the **Integrated Terminal**:

```
dotnet add package Microsoft.TypeScript.MSBuild
```

The preceding command adds the [Microsoft.TypeScript.MSBuild](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild/) package, enabling TypeScript compilation in the project.

## Configure the server

In this section, you configure the ASP.NET Core web app to send and receive SignalR messages.

1. In `Program.cs`, call [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr):

   ```
   var builder = WebApplication.CreateBuilder(args);

   builder.Services.AddSignalR();
   ```

2. Again, in `Program.cs`, call [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) and [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

   ```
   var app = builder.Build();

   app.UseDefaultFiles();
   app.UseStaticFiles();
   ```

   The preceding code allows the server to locate and serve the `index.html` file. The file is served whether the user enters its full URL or the root URL of the web app.

3. Create a new directory named `Hubs` in the project root, `SignalRWebpack/`, for the SignalR hub class.

4. Create a new file, `Hubs/ChatHub.cs`, with the following code:

   ```
   using Microsoft.AspNetCore.SignalR;

   namespace SignalRWebpack.Hubs;

   public class ChatHub : Hub
   {
       public async Task NewMessage(long username, string message) =>
           await Clients.All.SendAsync("messageReceived", username, message);
   }
   ```

   The preceding code broadcasts received messages to all connected users once the server receives them. It's unnecessary to have a generic `on` method to receive all the messages. A method named after the message name is enough.

   In this example:

   * The TypeScript client sends a message identified as `newMessage`.
   * The C# `NewMessage` method expects the data sent by the client.
   * A call is made to [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.clientproxyextensions.sendasync) on [Clients.All](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.all#microsoft-aspnetcore-signalr-ihubclients-1-all).
   * The received messages are sent to all clients connected to the hub.

5. Add the following `using` statement at the top of `Program.cs` to resolve the `ChatHub` reference:

   ```
   using SignalRWebpack.Hubs;
   ```

6. In `Program.cs`, map the `/hub` route to the `ChatHub` hub. Replace the code that displays `Hello World!` with the following code:

   ```
   app.MapHub<ChatHub>("/hub");
   ```

## Configure the client

In this section, you create a [Node.js](https://nodejs.org/) project to convert TypeScript to JavaScript and bundle client-side resources, including HTML and CSS, using Webpack.

1. Run the following command in the project root to create a `package.json` file:

   ```
   npm init -y
   ```

2. Add the highlighted property to the `package.json` file and save the file changes:

   ```
   {
     "name": "SignalRWebpack",
     "version": "1.0.0",
     "private": true,
     "description": "",
     "main": "index.js",
     "scripts": {
       "test": "echo \"Error: no test specified\" && exit 1"
     },
     "keywords": [],
     "author": "",
     "license": "ISC"
   }
   ```

   Setting the `private` property to `true` prevents package installation warnings in the next step.

3. Install the required npm packages. Run the following command from the project root:

   ```
   npm i -D -E clean-webpack-plugin css-loader html-webpack-plugin mini-css-extract-plugin ts-loader typescript webpack webpack-cli
   ```

   The `-E` option disables npm's default behavior of writing [semantic versioning](https://semver.org/) range operators to `package.json`. For example, `"webpack": "5.76.1"` is used instead of `"webpack": "^5.76.1"`. This option prevents unintended upgrades to newer package versions.

   For more information, see the [npm-install](https://docs.npmjs.com/cli/install) documentation.

4. Replace the `scripts` property of `package.json` file with the following code:

   ```
   "scripts": {
     "build": "webpack --mode=development --watch",
     "release": "webpack --mode=production",
     "publish": "npm run release && dotnet publish -c Release"
   },
   ```

   The following scripts are defined:

   * `build`: Bundles the client-side resources in development mode and watches for file changes. The file watcher causes the bundle to regenerate each time a project file changes. The `mode` option disables production optimizations, such as tree shaking and minification. use `build` in development only.
   * `release`: Bundles the client-side resources in production mode.
   * `publish`: Runs the `release` script to bundle the client-side resources in production mode. It calls the .NET CLI's [publish](/en-us/dotnet/core/tools/dotnet-publish) command to publish the app.

5. Create a file named `webpack.config.js` in the project root, with the following code:

   ```
   const path = require("path");
   const HtmlWebpackPlugin = require("html-webpack-plugin");
   const { CleanWebpackPlugin } = require("clean-webpack-plugin");
   const MiniCssExtractPlugin = require("mini-css-extract-plugin");

   module.exports = {
       entry: "./src/index.ts",
       output: {
           path: path.resolve(__dirname, "wwwroot"),
           filename: "[name].[chunkhash].js",
           publicPath: "/",
       },
       resolve: {
           extensions: [".js", ".ts"],
       },
       module: {
           rules: [
               {
                   test: /\.ts$/,
                   use: "ts-loader",
               },
               {
                   test: /\.css$/,
                   use: [MiniCssExtractPlugin.loader, "css-loader"],
               },
           ],
       },
       plugins: [
           new CleanWebpackPlugin(),
           new HtmlWebpackPlugin({
               template: "./src/index.html",
           }),
           new MiniCssExtractPlugin({
               filename: "css/[name].[chunkhash].css",
           }),
       ],
   };
   ```

   The preceding file configures the Webpack compilation process:

   * The `output` property overrides the default value of `dist`. The bundle is instead emitted in the `wwwroot` directory.
   * The `resolve.extensions` array includes `.js` to import the SignalR client JavaScript.

6. Create a new directory named `src` in the project root, `SignalRWebpack/`, for the client code.

7. Copy the `src` directory and its contents from the [sample project](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/tutorials/signalr-typescript-webpack/samples/8.x/SignalRWebpack/) into the project root. The `src` directory contains the following files:

   * `index.html`, which defines the homepage's boilerplate markup:

     ```
     <!DOCTYPE html>
     <html lang="en">
       <head>
         <meta charset="utf-8" />
         <title>ASP.NET Core SignalR with TypeScript and Webpack</title>
       </head>
       <body>
         <div id="divMessages" class="messages"></div>
         <div class="input-zone">
           <label id="lblMessage" for="tbMessage">Message:</label>
           <input id="tbMessage" class="input-zone-input" type="text" />
           <button id="btnSend">Send</button>
         </div>
       </body>
     </html>
     ```

   * `css/main.css`, which provides CSS styles for the homepage:

     ```
     *,
     *::before,
     *::after {
       box-sizing: border-box;
     }

     html,
     body {
       margin: 0;
       padding: 0;
     }

     .input-zone {
       align-items: center;
       display: flex;
       flex-direction: row;
       margin: 10px;
     }

     .input-zone-input {
       flex: 1;
       margin-right: 10px;
     }

     .message-author {
       font-weight: bold;
     }

     .messages {
       border: 1px solid #000;
       margin: 10px;
       max-height: 300px;
       min-height: 300px;
       overflow-y: auto;
       padding: 5px;
     }
     ```

   * `tsconfig.json`, which configures the TypeScript compiler to produce [ECMAScript](https://wikipedia.org/wiki/ECMAScript) 5-compatible JavaScript:

     ```
     {
       "compilerOptions": {
         "target": "es5"
       }
     }
     ```

   * `index.ts`:

     ```
     import * as signalR from "@microsoft/signalr";
     import "./css/main.css";

     const divMessages: HTMLDivElement = document.querySelector("#divMessages");
     const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
     const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
     const username = new Date().getTime();

     const connection = new signalR.HubConnectionBuilder()
         .withUrl("/hub")
         .build();

     connection.on("messageReceived", (username: string, message: string) => {
       const m = document.createElement("div");

       m.innerHTML = `<div class="message-author">${username}</div><div>${message}</div>`;

       divMessages.appendChild(m);
       divMessages.scrollTop = divMessages.scrollHeight;
     });

     connection.start().catch((err) => document.write(err));

     tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
       if (e.key === "Enter") {
         send();
       }
     });

     btnSend.addEventListener("click", send);

     function send() {
       connection.send("newMessage", username, tbMessage.value)
         .then(() => (tbMessage.value = ""));
     }
     ```

     The preceding code retrieves references to DOM elements and attaches two event handlers:

     * `keyup`: Fires when the user types in the `tbMessage` textbox and calls the `send` function when the user presses the **Enter** key.
     * `click`: Fires when the user selects the **Send** button and calls `send` function is called.

     The `HubConnectionBuilder` class creates a new builder for configuring the server connection. The `withUrl` function configures the hub URL.

     SignalR enables the exchange of messages between a client and a server. Each message has a specific name. For example, messages with the name `messageReceived` can run the logic responsible for displaying the new message in the messages zone. Listening to a specific message can be done via the `on` function. Any number of message names can be listened to. It's also possible to pass parameters to the message, such as the author's name and the content of the message received. Once the client receives a message, a new `div` element is created with the author's name and message content appended as child elements using `textContent`. It's added to the main `div` element displaying the messages.

     Sending a message through the WebSockets connection requires calling the `send` method. The method's first parameter is the message name. The message data inhabits the other parameters. In this example, a message identified as `newMessage` is sent to the server. The message consists of the username and the user input from a text box. If the send works, the text box value is cleared.

8. Run the following command at the project root:

   ```
   npm i @microsoft/signalr @types/node
   ```

   The preceding command installs:

   * The [SignalR TypeScript client](https://www.npmjs.com/package/@microsoft/signalr), which allows the client to send messages to the server.
   * The TypeScript type definitions for Node.js, which enables compile-time checking of Node.js types.

## Test the app

Confirm that the app works with the following steps:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

1. Run Webpack in `release` mode. Using the **Package Manager Console** window, run the following command in the project root.

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2. Select **Debug** > **Start without debugging** to launch the app in a browser without attaching the debugger. The `wwwroot/index.html` file is served at `https://localhost:<port>`.

   If there are compile errors, try closing and reopening the solution.

3. Open another browser instance (any browser) and paste the URL in the address bar.

4. Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

1) Run Webpack in `release` mode by executing the following command in the project root:

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2) Build and run the app by executing the following command in the project root:

   ```
   dotnet run
   ```

   The web server starts the app and makes it available on localhost.

3) Open a browser to `https://localhost:<port>`. The `wwwroot/index.html` file is served. Copy the URL from the address bar.

4) Open another browser instance (any browser). Paste the URL in the address bar.

5) Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

## Next steps

* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#use-strongly-typed-hubs)
* [Authentication and authorization in ASP.NET Core SignalR](../signalr/authn-and-authz?view=aspnetcore-10.0)
* [MessagePack Hub Protocol in SignalR for ASP.NET Core](../signalr/messagepackhubprotocol?view=aspnetcore-10.0)

## Related content

* [ASP.NET Core SignalR JavaScript client](../signalr/javascript-client?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](../signalr/hubs?view=aspnetcore-10.0)

This tutorial demonstrates using [Webpack](https://webpack.js.org/) in an ASP.NET Core SignalR web app to bundle and build a client written in [TypeScript](https://www.typescriptlang.org/). Webpack enables developers to bundle and build the client-side resources of a web app.

In this tutorial, you learn how to:

* Create an ASP.NET Core SignalR app
* Configure the SignalR server
* Configure a build pipeline using Webpack
* Configure the SignalR TypeScript client
* Enable communication between the client and the server

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/tutorials/signalr-typescript-webpack/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

* [Node.js](https://nodejs.org/) with [npm](https://www.npmjs.com/)

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)

* [Visual Studio 2022](https://visualstudio.microsoft.com/vs/#download) with the **ASP.NET and web development** workload.

- [Visual Studio Code](https://code.visualstudio.com/download)
- [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
- [.NET 7 SDK](https://dotnet.microsoft.com/download/dotnet/7.0)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use something other than Visual Studio Code.

## Create the ASP.NET Core web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

By default, Visual Studio uses the version of npm found in its installation directory. To configure Visual Studio to look for npm in the `PATH` environment variable:

Launch Visual Studio. At the start window, select **Continue without code**.

1. Navigate to **Tools** > **Options** > **Projects and Solutions** > **Web Package Management** > **External Web Tools**.

2. Select the `$(PATH)` entry from the list. Select the up arrow to move the entry to the second position in the list, and select **OK**:

   .

To create a new ASP.NET Core web app:

1. Use the **File** > **New** > **Project** menu option and choose the **ASP.NET Core Empty** template. Select **Next**.
2. Name the project `SignalRWebpack`, and select **Create**.
3. Select **.NET 7.0 (Standard Term Support)** from the **Framework** drop-down. Select **Create**.

Add the [Microsoft.TypeScript.MSBuild](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild/) NuGet package to the project:

1. In **Solution Explorer**, right-click the project node and select **Manage NuGet Packages**. In the **Browse** tab, search for `Microsoft.TypeScript.MSBuild` and then select **Install** on the right to install the package.

Visual Studio adds the NuGet package under the **Dependencies** node in **Solution Explorer**, enabling TypeScript compilation in the project.

1. Run the following commands in the **Integrated Terminal**:

   ```
   dotnet new web -o SignalRWebpack
   code -r SignalRWebpack
   ```

* The `dotnet new` command creates an empty ASP.NET Core web app in a `SignalRWebpack` directory.
* The `code` command opens the `SignalRWebpack` directory in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

1. Run the following .NET CLI command in the **Integrated Terminal**:

   ```
   dotnet add package Microsoft.TypeScript.MSBuild
   ```

   The preceding command adds the [Microsoft.TypeScript.MSBuild](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild/) package, enabling TypeScript compilation in the project.

## Configure the server

In this section, you configure the ASP.NET Core web app to send and receive SignalR messages.

1. In `Program.cs`, call [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr):

   ```
   var builder = WebApplication.CreateBuilder(args);

   builder.Services.AddSignalR();
   ```

2. Again, in `Program.cs`, call [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) and [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

   ```
   var app = builder.Build();

   app.UseDefaultFiles();
   app.UseStaticFiles();
   ```

   The preceding code allows the server to locate and serve the `index.html` file. The file is served whether the user enters its full URL or the root URL of the web app.

3. Create a new directory named `Hubs` in the project root, `SignalRWebpack/`, for the SignalR hub class.

4. Create a new file, `Hubs/ChatHub.cs`, with the following code:

   ```
   using Microsoft.AspNetCore.SignalR;

   namespace SignalRWebpack.Hubs;

   public class ChatHub : Hub
   {
       public async Task NewMessage(long username, string message) =>
           await Clients.All.SendAsync("messageReceived", username, message);
   }
   ```

   The preceding code broadcasts received messages to all connected users once the server receives them. It's unnecessary to have a generic `on` method to receive all the messages. A method named after the message name is enough.

   In this example:

   * The TypeScript client sends a message identified as `newMessage`.
   * The C# `NewMessage` method expects the data sent by the client.
   * A call is made to [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.clientproxyextensions.sendasync) on [Clients.All](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.all#microsoft-aspnetcore-signalr-ihubclients-1-all).
   * The received messages are sent to all clients connected to the hub.

5. Add the following `using` statement at the top of `Program.cs` to resolve the `ChatHub` reference:

   ```
   using SignalRWebpack.Hubs;
   ```

6. In `Program.cs`, map the `/hub` route to the `ChatHub` hub. Replace the code that displays `Hello World!` with the following code:

   ```
   app.MapHub<ChatHub>("/hub");
   ```

## Configure the client

In this section, you create a [Node.js](https://nodejs.org/) project to convert TypeScript to JavaScript and bundle client-side resources, including HTML and CSS, using Webpack.

1. Run the following command in the project root to create a `package.json` file:

   ```
   npm init -y
   ```

2. Add the highlighted property to the `package.json` file and save the file changes:

   ```
   {
     "name": "SignalRWebpack",
     "version": "1.0.0",
     "private": true,
     "description": "",
     "main": "index.js",
     "scripts": {
       "test": "echo \"Error: no test specified\" && exit 1"
     },
     "keywords": [],
     "author": "",
     "license": "ISC"
   }
   ```

   Setting the `private` property to `true` prevents package installation warnings in the next step.

3. Install the required npm packages. Run the following command from the project root:

   ```
   npm i -D -E clean-webpack-plugin css-loader html-webpack-plugin mini-css-extract-plugin ts-loader typescript webpack webpack-cli
   ```

   The `-E` option disables npm's default behavior of writing [semantic versioning](https://semver.org/) range operators to `package.json`. For example, `"webpack": "5.76.1"` is used instead of `"webpack": "^5.76.1"`. This option prevents unintended upgrades to newer package versions.

   For more information, see the [npm-install](https://docs.npmjs.com/cli/install) documentation.

4. Replace the `scripts` property of `package.json` file with the following code:

   ```
   "scripts": {
     "build": "webpack --mode=development --watch",
     "release": "webpack --mode=production",
     "publish": "npm run release && dotnet publish -c Release"
   },
   ```

   The following scripts are defined:

   * `build`: Bundles the client-side resources in development mode and watches for file changes. The file watcher causes the bundle to regenerate each time a project file changes. The `mode` option disables production optimizations, such as tree shaking and minification. use `build` in development only.
   * `release`: Bundles the client-side resources in production mode.
   * `publish`: Runs the `release` script to bundle the client-side resources in production mode. It calls the .NET CLI's [publish](/en-us/dotnet/core/tools/dotnet-publish) command to publish the app.

5. Create a file named `webpack.config.js` in the project root, with the following code:

   ```
   const path = require("path");
   const HtmlWebpackPlugin = require("html-webpack-plugin");
   const { CleanWebpackPlugin } = require("clean-webpack-plugin");
   const MiniCssExtractPlugin = require("mini-css-extract-plugin");

   module.exports = {
       entry: "./src/index.ts",
       output: {
           path: path.resolve(__dirname, "wwwroot"),
           filename: "[name].[chunkhash].js",
           publicPath: "/",
       },
       resolve: {
           extensions: [".js", ".ts"],
       },
       module: {
           rules: [
               {
                   test: /\.ts$/,
                   use: "ts-loader",
               },
               {
                   test: /\.css$/,
                   use: [MiniCssExtractPlugin.loader, "css-loader"],
               },
           ],
       },
       plugins: [
           new CleanWebpackPlugin(),
           new HtmlWebpackPlugin({
               template: "./src/index.html",
           }),
           new MiniCssExtractPlugin({
               filename: "css/[name].[chunkhash].css",
           }),
       ],
   };
   ```

   The preceding file configures the Webpack compilation process:

   * The `output` property overrides the default value of `dist`. The bundle is instead emitted in the `wwwroot` directory.
   * The `resolve.extensions` array includes `.js` to import the SignalR client JavaScript.

6. Copy the `src` directory and its contents from the [sample project](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/tutorials/signalr-typescript-webpack/samples/7.x/SignalRWebpack/) into the project root. The `src` directory contains the following files:

   * `index.html`, which defines the homepage's boilerplate markup:

     ```
     <!DOCTYPE html>
     <html lang="en">
       <head>
         <meta charset="utf-8" />
         <title>ASP.NET Core SignalR with TypeScript and Webpack</title>
       </head>
       <body>
         <div id="divMessages" class="messages"></div>
         <div class="input-zone">
           <label id="lblMessage" for="tbMessage">Message:</label>
           <input id="tbMessage" class="input-zone-input" type="text" />
           <button id="btnSend">Send</button>
         </div>
       </body>
     </html>
     ```

   * `css/main.css`, which provides CSS styles for the homepage:

     ```
     *,
     *::before,
     *::after {
       box-sizing: border-box;
     }

     html,
     body {
       margin: 0;
       padding: 0;
     }

     .input-zone {
       align-items: center;
       display: flex;
       flex-direction: row;
       margin: 10px;
     }

     .input-zone-input {
       flex: 1;
       margin-right: 10px;
     }

     .message-author {
       font-weight: bold;
     }

     .messages {
       border: 1px solid #000;
       margin: 10px;
       max-height: 300px;
       min-height: 300px;
       overflow-y: auto;
       padding: 5px;
     }
     ```

   * `tsconfig.json`, which configures the TypeScript compiler to produce [ECMAScript](https://wikipedia.org/wiki/ECMAScript) 5-compatible JavaScript:

     ```
     {
       "compilerOptions": {
         "target": "es5"
       }
     }
     ```

   * `index.ts`:

     ```
     import * as signalR from "@microsoft/signalr";
     import "./css/main.css";

     const divMessages: HTMLDivElement = document.querySelector("#divMessages");
     const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
     const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
     const username = new Date().getTime();

     const connection = new signalR.HubConnectionBuilder()
         .withUrl("/hub")
         .build();

     connection.on("messageReceived", (username: string, message: string) => {
       const m = document.createElement("div");

       m.innerHTML = `<div class="message-author">${username}</div><div>${message}</div>`;

       divMessages.appendChild(m);
       divMessages.scrollTop = divMessages.scrollHeight;
     });

     connection.start().catch((err) => document.write(err));

     tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
       if (e.key === "Enter") {
         send();
       }
     });

     btnSend.addEventListener("click", send);

     function send() {
       connection.send("newMessage", username, tbMessage.value)
         .then(() => (tbMessage.value = ""));
     }
     ```

     The preceding code retrieves references to DOM elements and attaches two event handlers:

     * `keyup`: Fires when the user types in the `tbMessage` textbox and calls the `send` function when the user presses the **Enter** key.
     * `click`: Fires when the user selects the **Send** button and calls `send` function is called.

     The `HubConnectionBuilder` class creates a new builder for configuring the server connection. The `withUrl` function configures the hub URL.

     SignalR enables the exchange of messages between a client and a server. Each message has a specific name. For example, messages with the name `messageReceived` can run the logic responsible for displaying the new message in the messages zone. Listening to a specific message can be done via the `on` function. Any number of message names can be listened to. It's also possible to pass parameters to the message, such as the author's name and the content of the message received. Once the client receives a message, a new `div` element is created with the author's name and message content appended as child elements using `textContent`. It's added to the main `div` element displaying the messages.

     Sending a message through the WebSockets connection requires calling the `send` method. The method's first parameter is the message name. The message data inhabits the other parameters. In this example, a message identified as `newMessage` is sent to the server. The message consists of the username and the user input from a text box. If the send works, the text box value is cleared.

7. Run the following command at the project root:

   ```
   npm i @microsoft/signalr @types/node
   ```

   The preceding command installs:

   * The [SignalR TypeScript client](https://www.npmjs.com/package/@microsoft/signalr), which allows the client to send messages to the server.
   * The TypeScript type definitions for Node.js, which enables compile-time checking of Node.js types.

## Test the app

Confirm that the app works with the following steps:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

1. Run Webpack in `release` mode. Using the **Package Manager Console** window, run the following command in the project root.

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2. Select **Debug** > **Start without debugging** to launch the app in a browser without attaching the debugger. The `wwwroot/index.html` file is served at `https://localhost:<port>`.

   If there are compile errors, try closing and reopening the solution.

3. Open another browser instance (any browser) and paste the URL in the address bar.

4. Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

1) Run Webpack in `release` mode by executing the following command in the project root:

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2) Build and run the app by executing the following command in the project root:

   ```
   dotnet run
   ```

   The web server starts the app and makes it available on localhost.

3) Open a browser to `https://localhost:<port>`. The `wwwroot/index.html` file is served. Copy the URL from the address bar.

4) Open another browser instance (any browser). Paste the URL in the address bar.

5) Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

## Next steps

* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#strongly-typed-hubs)
* [Authentication and authorization in ASP.NET Core SignalR](../signalr/authn-and-authz?view=aspnetcore-10.0)
* [MessagePack Hub Protocol in SignalR for ASP.NET Core](../signalr/messagepackhubprotocol?view=aspnetcore-10.0)

## Related content

* [ASP.NET Core SignalR JavaScript client](../signalr/javascript-client?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](../signalr/hubs?view=aspnetcore-10.0)

This tutorial demonstrates using [Webpack](https://webpack.js.org/) in an ASP.NET Core SignalR web app to bundle and build a client written in [TypeScript](https://www.typescriptlang.org/). Webpack enables developers to bundle and build the client-side resources of a web app.

In this tutorial, you learn how to:

* Create an ASP.NET Core SignalR app
* Configure the SignalR server
* Configure a build pipeline using Webpack
* Configure the SignalR TypeScript client
* Enable communication between the client and the server

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/tutorials/signalr-typescript-webpack/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

* [Node.js](https://nodejs.org/) with [npm](https://www.npmjs.com/)

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)

* [Visual Studio 2022](https://visualstudio.microsoft.com/vs/#download) with the **ASP.NET and web development** workload.
* [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

- [Visual Studio Code](https://code.visualstudio.com/download)
- [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
- [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use something other than Visual Studio Code.

## Create the ASP.NET Core web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

By default, Visual Studio uses the version of npm found in its installation directory. To configure Visual Studio to look for npm in the `PATH` environment variable:

1. Launch Visual Studio. At the start window, select **Continue without code**.

2. Navigate to **Tools** > **Options** > **Projects and Solutions** > **Web Package Management** > **External Web Tools**.

3. Select the `$(PATH)` entry from the list. Select the up arrow to move the entry to the second position in the list, and select **OK**:

   .

To create a new ASP.NET Core web app:

1. Use the **File** > **New** > **Project** menu option and choose the **ASP.NET Core Empty** template. Select **Next**.
2. Name the project `SignalRWebpack`, and select **Create**.
3. Select **.NET 6.0 (Long Term Support)** from the **Framework** drop-down. Select **Create**.

Add the [Microsoft.TypeScript.MSBuild](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild/) NuGet package to the project:

1. In **Solution Explorer**, right-click the project node and select **Manage NuGet Packages**. In the **Browse** tab, search for `Microsoft.TypeScript.MSBuild` and then select **Install** on the right to install the package.

Visual Studio adds the NuGet package under the **Dependencies** node in **Solution Explorer**, enabling TypeScript compilation in the project.

1. Run the following commands in the **Integrated Terminal**:

   ```
   dotnet new web -o SignalRWebpack
   code -r SignalRWebpack
   ```

   * The `dotnet new` command creates an empty ASP.NET Core web app in a `SignalRWebpack` directory.
   * The `code` command opens the `SignalRWebpack` directory in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

1. Run the following .NET CLI command in the **Integrated Terminal**:

   ```
   dotnet add package Microsoft.TypeScript.MSBuild
   ```

   The preceding command adds the [Microsoft.TypeScript.MSBuild](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild/) package, enabling TypeScript compilation in the project.

## Configure the server

In this section, you configure the ASP.NET Core web app to send and receive SignalR messages.

1. In `Program.cs`, call [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr):

   ```
   var builder = WebApplication.CreateBuilder(args);

   builder.Services.AddSignalR();
   ```

2. Again, in `Program.cs`, call [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) and [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

   ```
   var app = builder.Build();

   app.UseDefaultFiles();
   app.UseStaticFiles();
   ```

   The preceding code allows the server to locate and serve the `index.html` file. The file is served whether the user enters its full URL or the root URL of the web app.

3. Create a new directory named `Hubs` in the project root, `SignalRWebpack/`, for the SignalR hub class.

4. Create a new file, `Hubs/ChatHub.cs`, with the following code:

   ```
   using Microsoft.AspNetCore.SignalR;

   namespace SignalRWebpack.Hubs;

   public class ChatHub : Hub
   {
       public async Task NewMessage(long username, string message) =>
           await Clients.All.SendAsync("messageReceived", username, message);
   }
   ```

   The preceding code broadcasts received messages to all connected users once the server receives them. It's unnecessary to have a generic `on` method to receive all the messages. A method named after the message name is enough.

   In this example, the TypeScript client sends a message identified as `newMessage`. The C# `NewMessage` method expects the data sent by the client. A call is made to [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.clientproxyextensions.sendasync) on [Clients.All](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.all#microsoft-aspnetcore-signalr-ihubclients-1-all). The received messages are sent to all clients connected to the hub.

5. Add the following `using` statement at the top of `Program.cs` to resolve the `ChatHub` reference:

   ```
   using SignalRWebpack.Hubs;
   ```

6. In `Program.cs`, map the `/hub` route to the `ChatHub` hub. Replace the code that displays `Hello World!` with the following code:

   ```
   app.MapHub<ChatHub>("/hub");
   ```

## Configure the client

In this section, you create a [Node.js](https://nodejs.org/) project to convert TypeScript to JavaScript and bundle client-side resources, including HTML and CSS, using Webpack.

1. Run the following command in the project root to create a `package.json` file:

   ```
   npm init -y
   ```

2. Add the highlighted property to the `package.json` file and save the file changes:

   ```
   {
     "name": "SignalRWebpack",
     "version": "1.0.0",
     "private": true,
     "description": "",
     "main": "index.js",
     "scripts": {
       "test": "echo \"Error: no test specified\" && exit 1"
     },
     "keywords": [],
     "author": "",
     "license": "ISC"
   }
   ```

   Setting the `private` property to `true` prevents package installation warnings in the next step.

3. Install the required npm packages. Run the following command from the project root:

   ```
   npm i -D -E clean-webpack-plugin css-loader html-webpack-plugin mini-css-extract-plugin ts-loader typescript webpack webpack-cli
   ```

   The `-E` option disables npm's default behavior of writing [semantic versioning](https://semver.org/) range operators to `package.json`. For example, `"webpack": "5.70.0"` is used instead of `"webpack": "^5.70.0"`. This option prevents unintended upgrades to newer package versions.

   For more information, see the [npm-install](https://docs.npmjs.com/cli/install) documentation.

4. Replace the `scripts` property of `package.json` file with the following code:

   ```
   "scripts": {
     "build": "webpack --mode=development --watch",
     "release": "webpack --mode=production",
     "publish": "npm run release && dotnet publish -c Release"
   },
   ```

   The following scripts are defined:

   * `build`: Bundles the client-side resources in development mode and watches for file changes. The file watcher causes the bundle to regenerate each time a project file changes. The `mode` option disables production optimizations, such as tree shaking and minification. use `build` in development only.
   * `release`: Bundles the client-side resources in production mode.
   * `publish`: Runs the `release` script to bundle the client-side resources in production mode. It calls the .NET CLI's [publish](/en-us/dotnet/core/tools/dotnet-publish) command to publish the app.

5. Create a file named `webpack.config.js` in the project root, with the following code:

   ```
   const path = require("path");
   const HtmlWebpackPlugin = require("html-webpack-plugin");
   const { CleanWebpackPlugin } = require("clean-webpack-plugin");
   const MiniCssExtractPlugin = require("mini-css-extract-plugin");

   module.exports = {
     entry: "./src/index.ts",
     output: {
       path: path.resolve(__dirname, "wwwroot"),
       filename: "[name].[chunkhash].js",
       publicPath: "/",
     },
     resolve: {
       extensions: [".js", ".ts"],
     },
     module: {
       rules: [
         {
           test: /\.ts$/,
           use: "ts-loader",
         },
         {
           test: /\.css$/,
           use: [MiniCssExtractPlugin.loader, "css-loader"],
         },
       ],
     },
     plugins: [
       new CleanWebpackPlugin(),
       new HtmlWebpackPlugin({
         template: "./src/index.html",
       }),
       new MiniCssExtractPlugin({
         filename: "css/[name].[chunkhash].css",
       }),
     ],
   };
   ```

   The preceding file configures the Webpack compilation process:

   * The `output` property overrides the default value of `dist`. The bundle is instead emitted in the `wwwroot` directory.
   * The `resolve.extensions` array includes `.js` to import the SignalR client JavaScript.

6. Copy the `src` directory and its contents from the [sample project](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/tutorials/signalr-typescript-webpack/samples/6.x/SignalRWebpack/) into the project root. The `src` directory contains the following files:

   * `index.html`, which defines the homepage's boilerplate markup:

     ```
     <!DOCTYPE html>
     <html lang="en">
       <head>
         <meta charset="utf-8" />
         <title>ASP.NET Core SignalR with TypeScript and Webpack</title>
       </head>
       <body>
         <div id="divMessages" class="messages"></div>
         <div class="input-zone">
           <label id="lblMessage" for="tbMessage">Message:</label>
           <input id="tbMessage" class="input-zone-input" type="text" />
           <button id="btnSend">Send</button>
         </div>
       </body>
     </html>
     ```

   * `css/main.css`, which provides CSS styles for the homepage:

     ```
     *,
     *::before,
     *::after {
       box-sizing: border-box;
     }

     html,
     body {
       margin: 0;
       padding: 0;
     }

     .input-zone {
       align-items: center;
       display: flex;
       flex-direction: row;
       margin: 10px;
     }

     .input-zone-input {
       flex: 1;
       margin-right: 10px;
     }

     .message-author {
       font-weight: bold;
     }

     .messages {
       border: 1px solid #000;
       margin: 10px;
       max-height: 300px;
       min-height: 300px;
       overflow-y: auto;
       padding: 5px;
     }
     ```

   * `tsconfig.json`, which configures the TypeScript compiler to produce [ECMAScript](https://wikipedia.org/wiki/ECMAScript) 5-compatible JavaScript:

     ```
     {
       "compilerOptions": {
         "target": "es5"
       }
     }
     ```

   * `index.ts`:

     ```
     import * as signalR from "@microsoft/signalr";
     import "./css/main.css";

     const divMessages: HTMLDivElement = document.querySelector("#divMessages");
     const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
     const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
     const username = new Date().getTime();

     const connection = new signalR.HubConnectionBuilder()
         .withUrl("/hub")
         .build();

     connection.on("messageReceived", (username: string, message: string) => {
       const m = document.createElement("div");

       m.innerHTML = `<div class="message-author">${username}</div><div>${message}</div>`;

       divMessages.appendChild(m);
       divMessages.scrollTop = divMessages.scrollHeight;
     });

     connection.start().catch((err) => document.write(err));

     tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
       if (e.key === "Enter") {
         send();
       }
     });

     btnSend.addEventListener("click", send);

     function send() {
       connection.send("newMessage", username, tbMessage.value)
         .then(() => (tbMessage.value = ""));
     }
     ```

   The preceding code retrieves references to DOM elements and attaches two event handlers:

   * `keyup`: Fires when the user types in the `tbMessage` textbox and calls the `send` function when the user presses the **Enter** key.
   * `click`: Fires when the user selects the **Send** button and calls `send` function is called.

   The `HubConnectionBuilder` class creates a new builder for configuring the server connection. The `withUrl` function configures the hub URL.

   SignalR enables the exchange of messages between a client and a server. Each message has a specific name. For example, messages with the name `messageReceived` can run the logic responsible for displaying the new message in the messages zone. Listening to a specific message can be done via the `on` function. Any number of message names can be listened to. It's also possible to pass parameters to the message, such as the author's name and the content of the message received. Once the client receives a message, a new `div` element is created with the author's name and message content appended as child elements using `textContent`. It's added to the main `div` element displaying the messages.

   Sending a message through the WebSockets connection requires calling the `send` method. The method's first parameter is the message name. The message data inhabits the other parameters. In this example, a message identified as `newMessage` is sent to the server. The message consists of the username and the user input from a text box. If the send works, the text box value is cleared.

7. Run the following command at the project root:

   ```
   npm i @microsoft/signalr @types/node
   ```

   The preceding command installs:

   * The [SignalR TypeScript client](https://www.npmjs.com/package/@microsoft/signalr), which allows the client to send messages to the server.
   * The TypeScript type definitions for Node.js, which enables compile-time checking of Node.js types.

## Test the app

Confirm that the app works with the following steps:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

1. Run Webpack in `release` mode. Using the **Package Manager Console** window, run the following command in the project root. If you aren't in the project root, enter `cd SignalRWebpack` before entering the command.

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2. Select **Debug** > **Start without debugging** to launch the app in a browser without attaching the debugger. The `wwwroot/index.html` file is served at `https://localhost:<port>`.

   If you get compile errors, try closing and reopening the solution.

3. Open another browser instance (any browser) and paste the URL in the address bar.

4. Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

1) Run Webpack in `release` mode by executing the following command in the project root:

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2) Build and run the app by executing the following command in the project root:

   ```
   dotnet run
   ```

   The web server starts the app and makes it available on localhost.

3) Open a browser to `https://localhost:<port>`. The `wwwroot/index.html` file is served. Copy the URL from the address bar.

4) Open another browser instance (any browser). Paste the URL in the address bar.

5) Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

## Next steps

* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#strongly-typed-hubs)
* [Authentication and authorization in ASP.NET Core SignalR](../signalr/authn-and-authz?view=aspnetcore-10.0)
* [MessagePack Hub Protocol in SignalR for ASP.NET Core](../signalr/messagepackhubprotocol?view=aspnetcore-10.0)

## Related content

* [ASP.NET Core SignalR JavaScript client](../signalr/javascript-client?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](../signalr/hubs?view=aspnetcore-10.0)

This tutorial demonstrates using [Webpack](https://webpack.js.org/) in an ASP.NET Core SignalR web app to bundle and build a client written in [TypeScript](https://www.typescriptlang.org/). Webpack enables developers to bundle and build the client-side resources of a web app.

In this tutorial, you learn how to:

* Scaffold a starter ASP.NET Core SignalR app
* Configure the SignalR TypeScript client
* Configure a build pipeline using Webpack
* Configure the SignalR server
* Enable communication between client and server

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/signalr-typescript-webpack/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

- [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the **ASP.NET and web development** workload
- [.NET Core SDK 3.0 or later](https://dotnet.microsoft.com/download/dotnet-core)
- [Node.js](https://nodejs.org/) with [npm](https://www.npmjs.com/)

* [Visual Studio Code](https://code.visualstudio.com/download)
* [.NET Core SDK 3.0 or later](https://dotnet.microsoft.com/download/dotnet-core)
* [C# for Visual Studio Code version 1.17.1 or later](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [Node.js](https://nodejs.org/) with [npm](https://www.npmjs.com/)

## Create the ASP.NET Core web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

Configure Visual Studio to look for npm in the *PATH* environment variable. By default, Visual Studio uses the version of npm found in its installation directory. Follow these instructions in Visual Studio:

1. Launch Visual Studio. At the start window, select **Continue without code**.

2. Navigate to **Tools** > **Options** > **Projects and Solutions** > **Web Package Management** > **External Web Tools**.

3. Select the *$(PATH)* entry from the list. Select the up arrow to move the entry to the second position in the list, and select **OK**.

   .

Visual Studio configuration is complete.

1. Use the **File** > **New** > **Project** menu option and choose the **ASP.NET Core Web Application** template. Select **Next**.
2. Name the project \*SignalRWebPac\`\`, and select **Create**.
3. Select *.NET Core* from the target framework drop-down, and select *ASP.NET Core 3.1* from the framework selector drop-down. Select the **Empty** template, and select **Create**.

Add the `Microsoft.TypeScript.MSBuild` package to the project:

1. In **Solution Explorer** (right pane), right-click the project node and select **Manage NuGet Packages**. In the **Browse** tab, search for `Microsoft.TypeScript.MSBuild`, and then click **Install** on the right to install the package.

Visual Studio adds the NuGet package under the **Dependencies** node in **Solution Explorer**, enabling TypeScript compilation in the project.

Run the following command in the **Integrated Terminal**:

```
dotnet new web -o SignalRWebPack
code -r SignalRWebPack
```

* The `dotnet new` command creates an empty ASP.NET Core web app in a `SignalRWebPack` directory.
* The `code` command opens the `SignalRWebPack` folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Run the following .NET CLI command in the **Integrated Terminal**:

```
dotnet add package Microsoft.TypeScript.MSBuild
```

The preceding command adds the [Microsoft.TypeScript.MSBuild](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild/) package, enabling TypeScript compilation in the project.

## Configure Webpack and TypeScript

The following steps configure the conversion of TypeScript to JavaScript and the bundling of client-side resources.

1. Run the following command in the project root to create a `package.json` file:

   ```
   npm init -y
   ```

2. Add the highlighted property to the `package.json` file and save the file changes:

   ```
   {
     "name": "SignalRWebPack",
     "version": "1.0.0",
     "private": true,
     "description": "",
     "main": "index.js",
     "scripts": {
       "test": "echo \"Error: no test specified\" && exit 1"
     },
     "keywords": [],
     "author": "",
     "license": "ISC"
   }
   ```

   Setting the `private` property to `true` prevents package installation warnings in the next step.

3. Install the required npm packages. Run the following command from the project root:

   ```
   npm i -D -E clean-webpack-plugin@3.0.0 css-loader@3.4.2 html-webpack-plugin@3.2.0 mini-css-extract-plugin@0.9.0 ts-loader@6.2.1 typescript@3.7.5 webpack@4.41.5 webpack-cli@3.3.10
   ```

   Some command details to note:

   * A version number follows the `@` sign for each package name. npm installs those specific package versions.
   * The `-E` option disables npm's default behavior of writing [semantic versioning](https://semver.org/) range operators to \*package`json`. For example, `"webpack": "4.41.5"` is used instead of `"webpack": "^4.41.5"`. This option prevents unintended upgrades to newer package versions.

   See the [npm-install](https://docs.npmjs.com/cli/install) docs for more detail.

4. Replace the `scripts` property of the `package.json` file with the following code:

   ```
   "scripts": {
     "build": "webpack --mode=development --watch",
     "release": "webpack --mode=production",
     "publish": "npm run release && dotnet publish -c Release"
   },
   ```

   Some explanation of the scripts:

   * `build`: Bundles the client-side resources in development mode and watches for file changes. The file watcher causes the bundle to regenerate each time a project file changes. The `mode` option disables production optimizations, such as tree shaking and minification. Only use `build` in development.
   * `release`: Bundles the client-side resources in production mode.
   * `publish`: Runs the `release` script to bundle the client-side resources in production mode. It calls the .NET CLI's [publish](/en-us/dotnet/core/tools/dotnet-publish) command to publish the app.

5. Create a file named `webpack.config.js`, in the project root, with the following code:

   ```
   const path = require("path");
   const HtmlWebpackPlugin = require("html-webpack-plugin");
   const { CleanWebpackPlugin } = require("clean-webpack-plugin");
   const MiniCssExtractPlugin = require("mini-css-extract-plugin");
   module.exports = {
       entry: "./src/index.ts",
       output: {
           path: path.resolve(__dirname, "wwwroot"),
           filename: "[name].[chunkhash].js",
           publicPath: "/"
       },
       resolve: {
           extensions: [".js", ".ts"]
       },
       module: {
           rules: [
               {
                   test: /\.ts$/,
                   use: "ts-loader"
               },
               {
                   test: /\.css$/,
                   use: [MiniCssExtractPlugin.loader, "css-loader"]
               }
           ]
       },
       plugins: [
           new CleanWebpackPlugin(),
           new HtmlWebpackPlugin({
               template: "./src/index.html"
           }),
           new MiniCssExtractPlugin({
               filename: "css/[name].[chunkhash].css"
           })
       ]
   };
   ```

   The preceding file configures the Webpack compilation. Some configuration details to note:

   * The `output` property overrides the default value of `dist`. The bundle is instead emitted in the `wwwroot` directory.
   * The `resolve.extensions` array includes `.js` to import the SignalR client JavaScript.

6. Create a new *src* directory in the project root to store the project's client-side assets.

7. Create `src/index.html` with the following markup.

   ```
   <!DOCTYPE html>
   <html>
   <head>
       <meta charset="utf-8" />
       <title>ASP.NET Core SignalR</title>
   </head>
   <body>
       <div id="divMessages" class="messages">
       </div>
       <div class="input-zone">
           <label id="lblMessage" for="tbMessage">Message:</label>
           <input id="tbMessage" class="input-zone-input" type="text" />
           <button id="btnSend">Send</button>
       </div>
   </body>
   </html>
   ```

   The preceding HTML defines the homepage's boilerplate markup.

8. Create a new *src/css* directory. Its purpose is to store the project's `.css` files.

9. Create `src/css/main.css` with the following CSS:

   ```
   *, *::before, *::after {
       box-sizing: border-box;
   }

   html, body {
       margin: 0;
       padding: 0;
   }

   .input-zone {
       align-items: center;
       display: flex;
       flex-direction: row;
       margin: 10px;
   }

   .input-zone-input {
       flex: 1;
       margin-right: 10px;
   }

   .message-author {
       font-weight: bold;
   }

   .messages {
       border: 1px solid #000;
       margin: 10px;
       max-height: 300px;
       min-height: 300px;
       overflow-y: auto;
       padding: 5px;
   }
   ```

   The preceding `main.css` file styles the app.

10. Create `src/tsconfig.json` with the following JSON:

    ```
    {
      "compilerOptions": {
        "target": "es5"
      }
    }
    ```

    The preceding code configures the TypeScript compiler to produce [ECMAScript](https://wikipedia.org/wiki/ECMAScript) 5-compatible JavaScript.

11. Create `src/index.ts` with the following code:

    ```
    import "./css/main.css";

    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();

    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.key === "Enter") {
            send();
        }
    });

    btnSend.addEventListener("click", send);

    function send() {
    }
    ```

    The preceding TypeScript retrieves references to DOM elements and attaches two event handlers:

    * `keyup`: This event fires when the user types in the `tbMessage`textbox. The `send` function is called when the user presses the **Enter** key.
    * `click`: This event fires when the user selects the **Send** button. The `send` function is called.

## Configure the app

1. In `Startup.Configure`, add calls to [UseDefaultFiles(IApplicationBuilder)](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles#microsoft-aspnetcore-builder-defaultfilesextensions-usedefaultfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)) and [UseStaticFiles(IApplicationBuilder)](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles#microsoft-aspnetcore-builder-staticfileextensions-usestaticfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)).

   ```
   public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
   {
       if (env.IsDevelopment())
       {
           app.UseDeveloperExceptionPage();
       }
       
       app.UseRouting();
       app.UseDefaultFiles();
       app.UseStaticFiles();
       
       app.UseEndpoints(endpoints =>
       {
           endpoints.MapHub<ChatHub>("/hub");
       });
           
   }
   ```

   The preceding code allows the server to locate and serve the `index.html` file. The file is served whether the user enters its full URL or the root URL of the web app.

2. At the end of `Startup.Configure`, map a */hub* route to the `ChatHub` hub. Replace the code that displays *Hello World!* with the following line:

   ```
   app.UseEndpoints(endpoints =>
   {
       endpoints.MapHub<ChatHub>("/hub");
   });
   ```

3. In `Startup.ConfigureServices`, call [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr).

   ```
   services.AddSignalR();
   ```

4. Create a new directory named *Hubs* in the project root *SignalRWebPack/* to store the SignalR hub.

5. Create hub `Hubs/ChatHub.cs` with the following code:

   ```
   using Microsoft.AspNetCore.SignalR;
   using System.Threading.Tasks;

   namespace SignalRWebPack.Hubs
   {
       public class ChatHub : Hub
       {
       }
   }
   ```

6. Add the following `using` statement at the top of the `Startup.cs` file to resolve the `ChatHub` reference:

   ```
   using SignalRWebPack.Hubs;
   ```

## Enable client and server communication

The app currently displays a basic form to send messages, but isn't yet functional. The server is listening to a specific route but does nothing with sent messages.

1. Run the following command at the project root:

   ```
   npm i @microsoft/signalr @types/node
   ```

   The preceding command installs:

   * The [SignalR TypeScript client](https://www.npmjs.com/package/@microsoft/signalr), which allows the client to send messages to the server.
   * The TypeScript type definitions for Node.js, which enables compile-time checking of Node.js types.

2. Add the highlighted code to the `src/index.ts` file:

   ```
   import "./css/main.css";
   import * as signalR from "@microsoft/signalr";

   const divMessages: HTMLDivElement = document.querySelector("#divMessages");
   const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
   const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
   const username = new Date().getTime();

   const connection = new signalR.HubConnectionBuilder()
       .withUrl("/hub")
       .build();

   connection.on("messageReceived", (username: string, message: string) => {
       let m = document.createElement("div");

       const author = document.createElement("div");
       author.className = "message-author";
       author.textContent = username;

       const content = document.createElement("div");
       content.textContent = message;

       m.append(author, content);

       divMessages.appendChild(m);
       divMessages.scrollTop = divMessages.scrollHeight;
   });

   connection.start().catch(err => document.write(err));

   tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
       if (e.key === "Enter") {
           send();
       }
   });

   btnSend.addEventListener("click", send);

   function send() {
   }
   ```

   The preceding code supports receiving messages from the server. The `HubConnectionBuilder` class creates a new builder for configuring the server connection. The `withUrl` function configures the hub URL.

   SignalR enables the exchange of messages between a client and a server. Each message has a specific name. For example, messages with the name `messageReceived` can run the logic responsible for displaying the new message in the messages zone. Listening to a specific message can be done via the `on` function. Any number of message names can be listened to. It's also possible to pass parameters to the message, such as the author's name and the content of the message received. Once the client receives a message, a new `div` element is created with the author's name and message content appended as child elements using `textContent`. It's added to the main `div` element displaying the messages.

3. Now that the client can receive a message, configure it to send messages. Add the highlighted code to the `src/index.ts` file:

   ```
   import "./css/main.css";
   import * as signalR from "@microsoft/signalr";

   const divMessages: HTMLDivElement = document.querySelector("#divMessages");
   const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
   const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
   const username = new Date().getTime();

   const connection = new signalR.HubConnectionBuilder()
       .withUrl("/hub")
       .build();

   connection.on("messageReceived", (username: string, message: string) => {
       let messages = document.createElement("div");

       const author = document.createElement("div");
       author.className = "message-author";
       author.textContent = username;

       const content = document.createElement("div");
       content.textContent = message;

       messages.append(author, content);

       divMessages.appendChild(messages);
       divMessages.scrollTop = divMessages.scrollHeight;
   });

   connection.start().catch(err => document.write(err));

   tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
       if (e.key === "Enter") {
           send();
       }
   });

   btnSend.addEventListener("click", send);

   function send() {
       connection.send("newMessage", username, tbMessage.value)
           .then(() => tbMessage.value = "");
   }
   ```

   Sending a message through the WebSockets connection requires calling the `send` method. The method's first parameter is the message name. The message data inhabits the other parameters. In this example, a message identified as `newMessage` is sent to the server. The message consists of the username and the user input from a text box. If the send works, the text box value is cleared.

4. Add the `NewMessage` method to the `ChatHub` class:

   ```
   using Microsoft.AspNetCore.SignalR;
   using System.Threading.Tasks;

   namespace SignalRWebPack.Hubs
   {
       public class ChatHub : Hub
       {
           public async Task NewMessage(long username, string message)
           {
               await Clients.All.SendAsync("messageReceived", username, message);
           }
       }
   }
   ```

   The preceding code broadcasts received messages to all connected users once the server receives them. It's unnecessary to have a generic `on` method to receive all the messages. A method named after the message name suffices.

   In this example, the TypeScript client sends a message identified as `newMessage`. The C# `NewMessage` method expects the data sent by the client. A call is made to [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.clientproxyextensions.sendasync) on [Clients.All](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.all#microsoft-aspnetcore-signalr-ihubclients-1-all). The received messages are sent to all clients connected to the hub.

## Test the app

Confirm that the app works with the following steps.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

1. Run Webpack in *release* mode. Using the **Package Manager Console** window, run the following command in the project root. If you aren't in the project root, enter `cd SignalRWebPack` before entering the command.

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2. Select **Debug** > **Start without debugging** to launch the app in a browser without attaching the debugger. The `wwwroot/index.html` file is served at `http://localhost:<port_number>`.

   If you get compile errors, try closing and reopening the solution.

3. Open another browser instance (any browser). Paste the URL in the address bar.

4. Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

1) Run Webpack in *release* mode by executing the following command in the project root:

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2) Build and run the app by executing the following command in the project root:

   ```
   dotnet run
   ```

   The web server starts the app and makes it available on localhost.

3) Open a browser to `http://localhost:<port_number>`. The `wwwroot/index.html` file is served. Copy the URL from the address bar.

4) Open another browser instance (any browser). Paste the URL in the address bar.

5) Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

## Additional resources

* [ASP.NET Core SignalR JavaScript client](../signalr/javascript-client?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](../signalr/hubs?view=aspnetcore-10.0)

This tutorial demonstrates using [Webpack](https://webpack.js.org/) in an ASP.NET Core SignalR web app to bundle and build a client written in [TypeScript](https://www.typescriptlang.org/). Webpack enables developers to bundle and build the client-side resources of a web app.

In this tutorial, you learn how to:

* Scaffold a starter ASP.NET Core SignalR app
* Configure the SignalR TypeScript client
* Configure a build pipeline using Webpack
* Configure the SignalR server
* Enable communication between client and server

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/signalr-typescript-webpack/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

- [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the **ASP.NET and web development** workload
- [.NET Core SDK 2.2 or later](https://dotnet.microsoft.com/download/dotnet-core)
- [Node.js](https://nodejs.org/) with [npm](https://www.npmjs.com/)

* [Visual Studio Code](https://code.visualstudio.com/download)
* [.NET Core SDK 2.2 or later](https://dotnet.microsoft.com/download/dotnet-core)
* [C# for Visual Studio Code version 1.17.1 or later](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [Node.js](https://nodejs.org/) with [npm](https://www.npmjs.com/)

## Create the ASP.NET Core web app

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

Configure Visual Studio to look for npm in the *PATH* environment variable. By default, Visual Studio uses the version of npm found in its installation directory. Follow these instructions in Visual Studio:

1. Navigate to **Tools** > **Options** > **Projects and Solutions** > **Web Package Management** > **External Web Tools**.

2. Select the *$(PATH)* entry from the list. Select the up arrow to move the entry to the second position in the list.

Visual Studio configuration is completed. It's time to create the project.

1. Use the **File** > **New** > **Project** menu option and choose the **ASP.NET Core Web Application** template.
2. Name the project \*SignalRWebPack\`, and select **Create**.
3. Select *.NET Core* from the target framework drop-down, and select *ASP.NET Core 2.2* from the framework selector drop-down. Select the **Empty** template, and select **Create**.

Run the following command in the **Integrated Terminal**:

```
dotnet new web -o SignalRWebPack
```

An empty ASP.NET Core web app, targeting .NET Core, is created in a `SignalRWebPack` directory.

## Configure Webpack and TypeScript

The following steps configure the conversion of TypeScript to JavaScript and the bundling of client-side resources.

1. Run the following command in the project root to create a `package.json` file:

   ```
   npm init -y
   ```

2. Add the highlighted property to the `package.json` file:

   ```
   {
     "name": "SignalRWebPack",
     "version": "1.0.0",
     "private": true,
     "description": "",
     "main": "index.js",
     "scripts": {
       "test": "echo \"Error: no test specified\" && exit 1"
     },
     "keywords": [],
     "author": "",
     "license": "ISC"
   }
   ```

   Setting the `private` property to `true` prevents package installation warnings in the next step.

3. Install the required npm packages. Run the following command from the project root:

   ```
   npm install -D -E clean-webpack-plugin@1.0.1 css-loader@2.1.0 html-webpack-plugin@4.0.0-beta.5 mini-css-extract-plugin@0.5.0 ts-loader@5.3.3 typescript@3.3.3 webpack@4.29.3 webpack-cli@3.2.3
   ```

   Some command details to note:

   * A version number follows the `@` sign for each package name. npm installs those specific package versions.
   * The `-E` option disables npm's default behavior of writing [semantic versioning](https://semver.org/) range operators to \*package`json`. For example, `"webpack": "4.29.3"` is used instead of `"webpack": "^4.29.3"`. This option prevents unintended upgrades to newer package versions.

   See the [npm-install](https://docs.npmjs.com/cli/install) docs for more detail.

4. Replace the `scripts` property of the `package.json` file with the following code:

   ```
   "scripts": {
     "build": "webpack --mode=development --watch",
     "release": "webpack --mode=production",
     "publish": "npm run release && dotnet publish -c Release"
   },
   ```

   Some explanation of the scripts:

   * `build`: Bundles the client-side resources in development mode and watches for file changes. The file watcher causes the bundle to regenerate each time a project file changes. The `mode` option disables production optimizations, such as tree shaking and minification. Only use `build` in development.
   * `release`: Bundles the client-side resources in production mode.
   * `publish`: Runs the `release` script to bundle the client-side resources in production mode. It calls the .NET CLI's [publish](/en-us/dotnet/core/tools/dotnet-publish) command to publish the app.

5. Create a file named`*webpack.config.js` in the project root, with the following code:

   ```
   const path = require("path");
   const HtmlWebpackPlugin = require("html-webpack-plugin");
   const CleanWebpackPlugin = require("clean-webpack-plugin");
   const MiniCssExtractPlugin = require("mini-css-extract-plugin");

   module.exports = {
       entry: "./src/index.ts",
       output: {
           path: path.resolve(__dirname, "wwwroot"),
           filename: "[name].[chunkhash].js",
           publicPath: "/"
       },
       resolve: {
           extensions: [".js", ".ts"]
       },
       module: {
           rules: [
               {
                   test: /\.ts$/,
                   use: "ts-loader"
               },
               {
                   test: /\.css$/,
                   use: [MiniCssExtractPlugin.loader, "css-loader"]
               }
           ]
       },
       plugins: [
           new CleanWebpackPlugin(["wwwroot/*"]),
           new HtmlWebpackPlugin({
               template: "./src/index.html"
           }),
           new MiniCssExtractPlugin({
               filename: "css/[name].[chunkhash].css"
           })
       ]
   };
   ```

   The preceding file configures the Webpack compilation. Some configuration details to note:

   * The `output` property overrides the default value of `dist`. The bundle is instead emitted in the `wwwroot` directory.
   * The `resolve.extensions` array includes `.js` to import the SignalR client JavaScript.

6. Create a new *src* directory in the project root to store the project's client-side assets.

7. Create `src/index.html` with the following markup.

   ```
   <!DOCTYPE html>
   <html>
   <head>
       <meta charset="utf-8" />
       <title>ASP.NET Core SignalR</title>
   </head>
   <body>
       <div id="divMessages" class="messages">
       </div>
       <div class="input-zone">
           <label id="lblMessage" for="tbMessage">Message:</label>
           <input id="tbMessage" class="input-zone-input" type="text" />
           <button id="btnSend">Send</button>
       </div>
   </body>
   </html>
   ```

   The preceding HTML defines the homepage's boilerplate markup.

8. Create a new *src/css* directory. Its purpose is to store the project's `.css` files.

9. Create `src/css/main.css` with the following markup:

   ```
   *, *::before, *::after {
       box-sizing: border-box;
   }

   html, body {
       margin: 0;
       padding: 0;
   }

   .input-zone {
       align-items: center;
       display: flex;
       flex-direction: row;
       margin: 10px;
   }

   .input-zone-input {
       flex: 1;
       margin-right: 10px;
   }

   .message-author {
       font-weight: bold;
   }

   .messages {
       border: 1px solid #000;
       margin: 10px;
       max-height: 300px;
       min-height: 300px;
       overflow-y: auto;
       padding: 5px;
   }
   ```

   The preceding `main.css` file styles the app.

10. Create `src/tsconfig.json` with the following JSON:

    ```
    {
      "compilerOptions": {
        "target": "es5"
      }
    }
    ```

    The preceding code configures the TypeScript compiler to produce [ECMAScript](https://wikipedia.org/wiki/ECMAScript) 5-compatible JavaScript.

11. Create `src/index.ts` with the following code:

    ```
    import "./css/main.css";

    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();

    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.keyCode === 13) {
            send();
        }
    });

    btnSend.addEventListener("click", send);

    function send() {
    }
    ```

    The preceding TypeScript retrieves references to DOM elements and attaches two event handlers:

    * `keyup`: This event fires when the user types in the `tbMessage` textbox. The `send` function is called when the user presses the **Enter** key.
    * `click`: This event fires when the user selects the **Send** button. The `send` function is called.

## Configure the ASP.NET Core app

1. The code provided in the `Startup.Configure` method displays *Hello World!*. Replace the `app.Run` method call with calls to [UseDefaultFiles(IApplicationBuilder)](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles#microsoft-aspnetcore-builder-defaultfilesextensions-usedefaultfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)) and [UseStaticFiles(IApplicationBuilder)](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles#microsoft-aspnetcore-builder-staticfileextensions-usestaticfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)).

   ```
   app.UseDefaultFiles();
   app.UseStaticFiles();
   ```

   The preceding code allows the server to locate and serve the `index.html` file, whether the user enters its full URL or the root URL of the web app.

2. Call [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) in `Startup.ConfigureServices`. It adds the SignalR services to the project.

   ```
   services.AddSignalR();
   ```

3. Map a */hub* route to the `ChatHub` hub. Add the following lines at the end of `Startup.Configure`:

   ```
   app.UseSignalR(options =>
   {
       options.MapHub<ChatHub>("/hub");
   });
   ```

4. Create a new directory, called *Hubs*, in the project root. Its purpose is to store the SignalR hub, which is created in the next step.

5. Create hub `Hubs/ChatHub.cs` with the following code:

   ```
   using Microsoft.AspNetCore.SignalR;
   using System.Threading.Tasks;

   namespace SignalRWebPack.Hubs
   {
       public class ChatHub : Hub
       {
       }
   }
   ```

6. Add the following code at the top of the `Startup.cs` file to resolve the `ChatHub` reference:

   ```
   using SignalRWebPack.Hubs;
   ```

## Enable client and server communication

The app currently displays a simple form to send messages. Nothing happens when you try to do so. The server is listening to a specific route but does nothing with sent messages.

1. Run the following command at the project root:

   ```
   npm install @aspnet/signalr
   ```

   The preceding command installs the [SignalR TypeScript client](https://www.npmjs.com/package/@microsoft/signalr), which allows the client to send messages to the server.

2. Add the highlighted code to the `src/index.ts` file:

   ```
   import "./css/main.css";
   import * as signalR from "@aspnet/signalr";

   const divMessages: HTMLDivElement = document.querySelector("#divMessages");
   const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
   const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
   const username = new Date().getTime();

   const connection = new signalR.HubConnectionBuilder()
       .withUrl("/hub")
       .build();

   connection.on("messageReceived", (username: string, message: string) => {
       let m = document.createElement("div");

       const author = document.createElement("div");
       author.className = "message-author";
       author.textContent = username;

       const content = document.createElement("div");
       content.textContent = message;

       m.append(author, content);

       divMessages.appendChild(m);
       divMessages.scrollTop = divMessages.scrollHeight;
   });

   connection.start().catch(err => document.write(err));

   tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
       if (e.keyCode === 13) {
           send();
       }
   });

   btnSend.addEventListener("click", send);

   function send() {
   }
   ```

   The preceding code supports receiving messages from the server. The `HubConnectionBuilder` class creates a new builder for configuring the server connection. The `withUrl` function configures the hub URL.

   SignalR enables the exchange of messages between a client and a server. Each message has a specific name. For example, messages with the name `messageReceived` can run the logic responsible for displaying the new message in the messages zone. Listening to a specific message can be done via the `on` function. You can listen to any number of message names. It's also possible to pass parameters to the message, such as the author's name and the content of the message received. Once the client receives a message, a new `div` element is created with the author's name and message content appended as child elements using `textContent`. The new message is added to the main `div` element displaying the messages.

3. Now that the client can receive a message, configure it to send messages. Add the highlighted code to the `src/index.ts` file:

   ```
   import "./css/main.css";
   import * as signalR from "@aspnet/signalr";

   const divMessages: HTMLDivElement = document.querySelector("#divMessages");
   const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
   const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
   const username = new Date().getTime();

   const connection = new signalR.HubConnectionBuilder()
       .withUrl("/hub")
       .build();

   connection.on("messageReceived", (username: string, message: string) => {
       let messageContainer = document.createElement("div");

       const author = document.createElement("div");
       author.className = "message-author";
       author.textContent = username;

       const content = document.createElement("div");
       content.textContent = message;

       messageContainer.append(author, content);

       divMessages.appendChild(messageContainer);
       divMessages.scrollTop = divMessages.scrollHeight;
   });

   connection.start().catch(err => document.write(err));

   tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
       if (e.keyCode === 13) {
           send();
       }
   });

   btnSend.addEventListener("click", send);

   function send() {
       connection.send("newMessage", username, tbMessage.value)
                 .then(() => tbMessage.value = "");
   }
   ```

   Sending a message through the WebSockets connection requires calling the `send` method. The method's first parameter is the message name. The message data inhabits the other parameters. In this example, a message identified as `newMessage` is sent to the server. The message consists of the username and the user input from a text box. If the send works, the text box value is cleared.

4. Add the `NewMessage` method to the `ChatHub` class:

   ```
   using Microsoft.AspNetCore.SignalR;
   using System.Threading.Tasks;

   namespace SignalRWebPack.Hubs
   {
       public class ChatHub : Hub
       {
           public async Task NewMessage(long username, string message)
           {
               await Clients.All.SendAsync("messageReceived", username, message);
           }
       }
   }
   ```

   The preceding code broadcasts received messages to all connected users once the server receives them. It's unnecessary to have a generic `on` method to receive all the messages. A method named after the message name suffices.

   In this example, the TypeScript client sends a message identified as `newMessage`. The C# `NewMessage` method expects the data sent by the client. A call is made to [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.clientproxyextensions.sendasync) on [Clients.All](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.all#microsoft-aspnetcore-signalr-ihubclients-1-all). The received messages are sent to all clients connected to the hub.

## Test the app

Confirm that the app works with the following steps.

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

1. Run Webpack in *release* mode. Using the **Package Manager Console** window, run the following command in the project root. If you aren't in the project root, enter `cd SignalRWebPack` before entering the command.

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2. Select **Debug** > **Start without debugging** to launch the app in a browser without attaching the debugger. The `wwwroot/index.html` file is served at `http://localhost:<port_number>`.

3. Open another browser instance (any browser). Paste the URL in the address bar.

4. Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

1) Run Webpack in *release* mode by executing the following command in the project root:

   ```
   npm run release
   ```

   This command generates the client-side assets to be served when running the app. The assets are placed in the `wwwroot` folder.

   Webpack completed the following tasks:

   * Purged the contents of the `wwwroot` directory.

   * Converted the TypeScript to JavaScript in a process known as *transpilation*.

   * Mangled the generated JavaScript to reduce file size in a process known as *minification*.

   * Copied the processed JavaScript, CSS, and HTML files from `src` to the `wwwroot` directory.

   * Injected the following elements into the `wwwroot/index.html` file:

     * A `<link>` tag, referencing the `wwwroot/main.<hash>.css` file. This tag is placed immediately before the closing `</head>` tag.
     * A `<script>` tag, referencing the minified `wwwroot/main.<hash>.js` file. This tag is placed immediately after the closing `</title>` tag.

2) Build and run the app by executing the following command in the project root:

   ```
   dotnet run
   ```

   The web server starts the app and makes it available on localhost.

3) Open a browser to `http://localhost:<port_number>`. The `wwwroot/index.html` file is served. Copy the URL from the address bar.

4) Open another browser instance (any browser). Paste the URL in the address bar.

5) Choose either browser, type something in the **Message** text box, and select the **Send** button. The unique user name and message are displayed on both pages instantly.

## Additional resources

* [ASP.NET Core SignalR JavaScript client](../signalr/javascript-client?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](../signalr/hubs?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/advanced
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/advanced.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Advanced configuration of the ASP.NET Core Module and IIS

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article covers advanced configuration options and scenarios for the ASP.NET Core Module and IIS.

## Modify the stack size

*Only applies when using the in-process hosting model.*

Configure the managed stack size using the `stackSize` setting in hexadecimal bytes in the `web.config` file. The default size is 0x100000 bytes (1 MB). The following example changes the stack size to 2 MB (2,097,152 bytes) in hexadecimal 0x200000:

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="false"
    stdoutLogFile="\\?\%home%\LogFiles\stdout"
    hostingModel="inprocess">
  <handlerSettings>
    <handlerSetting name="stackSize" value="200000" />
  </handlerSettings>
</aspNetCore>
```

## Disallow rotation on config

The `disallowRotationOnConfigChange` setting is intended for blue/green scenarios where a change to global config shouldn't cause all sites to recycle. When this flag is true, only changes relevant to the site itself will cause it to recycle. For example, a site recycles if its *web.config* changes or something changes that's relevant to the site's path from IIS's perspective. But a general change to *applicationHost.config* wouldn't cause an app to recycle. The following example sets this setting to true:

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="false"
    stdoutLogFile="\\?\%home%\LogFiles\stdout"
    hostingModel="inprocess">
  <handlerSettings>
    <handlerSetting name="disallowRotationOnConfigChange" value="true" />
  </handlerSettings>
</aspNetCore>
```

This setting corresponds to the [ApplicationPoolRecycling.DisallowRotationOnConfigChange](/en-us/dotnet/api/microsoft.web.administration.applicationpoolrecycling.disallowrotationonconfigchange#microsoft-web-administration-applicationpoolrecycling-disallowrotationonconfigchange) API.

## Reduce 503 likelihood during app recycle

By default, there's a one-second delay between when IIS is notified of a recycle or shutdown and when ANCM tells the managed server to initiate shutdown. The delay is configurable via the `ANCM_shutdownDelay` environment variable or by setting the `shutdownDelay` handler setting. Both values are in milliseconds. The delay is primarily to reduce the likelihood of a race where:

* IIS hasn't started queuing requests to go to the new app.
* ANCM starts rejecting new requests that come into the old app.

This setting doesn't mean incoming requests will be delayed by this amount. The setting indicates that the old app instance will start shutting down after the timeout occurs. Slower machines or machines with heavier CPU usage might need to adjust this value to reduce 503 likelihood.

The following example sets the delay to 5 seconds:

```
<aspNetCore processPath="dotnet"
    arguments=".\MyApp.dll"
    stdoutLogEnabled="false"
    stdoutLogFile="\\?\%home%\LogFiles\stdout"
    hostingModel="inprocess">
  <handlerSettings>
    <handlerSetting name="shutdownDelay" value="5000" />
  </handlerSettings>
</aspNetCore>
```

```
dotnet-hosting-{VERSION}.exe OPT_NO_SHARED_CONFIG_CHECK=1
```

When the path to the shared configuration isn't on the same machine as the IIS installation, follow these steps:

1. Disable the IIS Shared Configuration.
2. Run the installer.
3. Export the updated `applicationHost.config` file to the file share.
4. Re-enable the IIS Shared Configuration.

## Data protection

The [ASP.NET Core Data Protection stack](../../security/data-protection/introduction?view=aspnetcore-10.0) is used by several ASP.NET Core [middlewares](../../fundamentals/middleware/?view=aspnetcore-10.0), including middleware used in authentication. Even if Data Protection APIs aren't called by user code, data protection should be configured with a deployment script or in user code to create a persistent cryptographic [key store](../../security/data-protection/implementation/key-management?view=aspnetcore-10.0). If data protection isn't configured, the keys are held in memory and discarded when the app restarts.

If the Data Protection key ring is stored in memory when the app restarts:

* All cookie-based authentication tokens are invalidated.
* Users are required to sign in again on their next request.
* Any data protected with the key ring can no longer be decrypted. This might include [CSRF tokens](../../security/anti-request-forgery?view=aspnetcore-10.0#aspnet-core-antiforgery-configuration) and [ASP.NET Core MVC TempData cookies](../../fundamentals/app-state?view=aspnetcore-10.0#tempdata).

To configure data protection under IIS to persist the key ring, use **one** of the following approaches:

* **Create Data Protection Registry keys**

  ASP.NET Core Data Protection keys used by ASP.NET Core apps are stored in the registry external to the apps. To persist the keys for a given app, create Registry keys for the app pool.

  For standalone, non-webfarm IIS installations, the [Data Protection Provision-AutoGenKeys.ps1 PowerShell script](https://github.com/dotnet/AspNetCore/blob/main/src/DataProtection/Provision-AutoGenKeys.ps1) can be used for each app pool used with an ASP.NET Core app. This script creates a Registry key in the HKLM registry that's accessible only to the worker process account of the app's app pool. Keys are encrypted at rest using DPAPI with a machine-wide key.

  In web farm scenarios, an app can be configured to use a UNC path to store its Data Protection key ring. By default, the keys aren't encrypted. Ensure that the file permissions for the network share are limited to the Windows account that the app runs under. An X509 certificate can be used to protect keys at rest. Consider a mechanism to allow users to upload certificates. Place certificates into the user's trusted certificate store and ensure they're available on all machines where the user's app runs. For more information, see [Configure ASP.NET Core Data Protection](../../security/data-protection/configuration/overview?view=aspnetcore-10.0).

* **Configure the IIS Application Pool to load the user profile**

  This setting is in the **Process Model** section under the **Advanced Settings** for the app pool. Set **Load User Profile** to `True`. When set to `True`, keys are stored in the user profile directory and protected using DPAPI with a key specific to the user account. Keys are persisted to the `%LOCALAPPDATA%/ASP.NET/DataProtection-Keys` folder.

  The app pool's [`setProfileEnvironment` attribute](/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel#configuration) must also be enabled. The default value of `setProfileEnvironment` is `true`. In some scenarios (for example, Windows OS), `setProfileEnvironment` is set to `false`. If keys aren't stored in the user profile directory as expected:

  1. Navigate to the `%windir%/system32/inetsrv/config` folder.
  2. Open the `applicationHost.config` file.
  3. Locate the `<system.applicationHost><applicationPools><applicationPoolDefaults><processModel>` element.
  4. Confirm that the `setProfileEnvironment` attribute isn't present, which defaults the value to `true`, or explicitly set the attribute's value to `true`.

* **Use the file system as a key ring store**

  Adjust the app code to [use the file system as a key ring store](../../security/data-protection/configuration/overview?view=aspnetcore-10.0). Use an X509 certificate to protect the key ring and ensure the certificate is a trusted certificate. If the certificate is self-signed, place the certificate in the Trusted Root store.

  When using IIS in a web farm:

  * Use a file share that all machines can access.
  * Deploy an X509 certificate to each machine. Configure [Data Protection in code](../../security/data-protection/configuration/overview?view=aspnetcore-10.0).

* **Set a machine-wide policy for Data Protection**

  The Data Protection system has limited support for setting a default [machine-wide policy](../../security/data-protection/configuration/machine-wide-policy?view=aspnetcore-10.0) for all apps that consume the Data Protection APIs. For more information, see [ASP.NET Core Data Protection Overview](../../security/data-protection/introduction?view=aspnetcore-10.0).

## IIS configuration

### Windows Server operating systems

### Windows desktop operating systems

   To enable Windows Authentication, expand the following nodes: **World Wide Web Services** > **Security**. Select the **Windows Authentication** feature. For more information, see [Windows Authentication `<windowsAuthentication>`](/en-us/iis/configuration/system.webServer/security/authentication/windowsAuthentication/) and [Configure Windows authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).

   **WebSockets (Optional)**\
   WebSockets is supported with ASP.NET Core 1.1 or later. To enable WebSockets, expand the following nodes: **World Wide Web Services** > **Application Development Features**. Select the **WebSocket Protocol** feature. For more information, see [WebSockets](../../fundamentals/websockets?view=aspnetcore-10.0).

6. If the IIS installation requires a restart, restart the system.

## Virtual Directories

[IIS Virtual Directories](/en-us/iis/get-started/planning-your-iis-architecture/understanding-sites-applications-and-virtual-directories-on-iis#virtual-directories) aren't supported with ASP.NET Core apps. An app can be hosted as a [sub-application](#sub-applications).

## Sub-applications

An ASP.NET Core app can be hosted as an [IIS sub-application (sub-app)](/en-us/iis/get-started/planning-your-iis-architecture/understanding-sites-applications-and-virtual-directories-on-iis#applications). The sub-app's path becomes part of the root app's URL.

Static asset links within the sub-app should use tilde-slash (`~/`) notation in MVC and Razor Pages. Tilde-slash notation triggers a [Tag Helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) to prepend the sub-app's pathbase to the rendered relative link. For a sub-app at `/subapp_path`, an image linked with `src="~/image.png"` is rendered as `src="/subapp_path/image.png"`. The root app's static file middleware doesn't process the static file request. The sub-app's static file middleware processes the request.

Note

Razor components (`.razor`) shouldn't use tilde-slash notation. For more information, see [ASP.NET Core Blazor app base path](../../blazor/host-and-deploy/app-base-path?view=aspnetcore-10.0).

## Application Pools

The hosting model determines app pool isolation:

* **In-process hosting**: Apps are required to run in separate app pools.
* **Out-of-process hosting**: We recommend isolating the apps from each other by running each app in its own app pool.

The IIS **Add Website** dialog defaults to a single app pool per app. When a **Site name** is provided, the text is automatically transferred to the **Application pool** textbox. A new app pool is created using the site name when the site is added.

## Application Pool Identity

An app pool identity account allows an app to run under a unique account without having to create and manage domains or local accounts. On IIS 8.0 or later, the IIS Admin Worker Process (WAS) creates a virtual account with the name of the new app pool and runs the app pool's worker processes under this account by default. In the IIS Management Console under **Advanced Settings** for the app pool, ensure that the **Identity** is set to use `ApplicationPoolIdentity`:

The IIS management process creates a secure identifier with the name of the app pool in the Windows Security System. Resources can be secured using this identity. However, this identity isn't a real user account and doesn't show up in the Windows User Management Console.

If the IIS worker process requires elevated access to the app, modify the Access Control List (ACL) for the directory containing the app:

1. Open Windows Explorer and navigate to the directory.

2. Right-click on the directory and select **Properties**.

3. Under the **Security** tab, select the **Edit** button, and then the **Add** button.

4. Select the **Locations** button and make sure the system is selected.

5. Enter `IIS AppPool\{APP POOL NAME}` format, where the placeholder `{APP POOL NAME}` is the app pool name, in **Enter the object names to select** area. Select the **Check Names** button. For the *DefaultAppPool* check the names using `IIS AppPool\DefaultAppPool`. When the **Check Names** button is selected, a value of `DefaultAppPool` is indicated in the object names area. It isn't possible to enter the app pool name directly into the object names area. Use the `IIS AppPool\{APP POOL NAME}` format, where the placeholder `{APP POOL NAME}` is the app pool name, when checking for the object name.

6. Select **OK**.

7. Read & execute permissions should be granted by default. Provide additional permissions as needed.

Access can also be granted at a command prompt using the **ICACLS** tool. Using the *DefaultAppPool* as an example, the following command is used to grant read and execute permissions to the `MyWebApp` folder, subfolders, and files:

```
ICACLS C:\sites\MyWebApp /grant "IIS AppPool\DefaultAppPool:(OI)(CI)RX"
```

For more information, see the [icacls](/en-us/windows-server/administration/windows-commands/icacls) article.

## HTTP/2 support

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is supported with ASP.NET Core in the following IIS deployment scenarios:

* In-process

  * Windows Server 2016/Windows 10 or later; IIS 10 or later.
  * TLS 1.2 or later connection.

* Out-of-process

  * Windows Server 2016/Windows 10 or later; IIS 10 or later.
  * Public-facing edge server connections use HTTP/2, but the reverse proxy connection to the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) uses HTTP/1.1.
  * TLS 1.2 or later connection.

* [**Application Initialization Module**](#application-initialization-module): App's hosted [in-process](in-process-hosting?view=aspnetcore-10.0) or [out-of-process](out-of-process-hosting?view=aspnetcore-10.0) can be configured to start automatically on a worker process restart or server restart.
* [**Idle Timeout**](#idle-timeout): App's hosted [in-process](in-process-hosting?view=aspnetcore-10.0) can be configured not to time out during periods of inactivity.

  3. The default **Start Mode** is `OnDemand`. Set the **Start Mode** to `AlwaysRunning`. Select **OK**.
  4. Open the **Sites** node in the **Connections** panel.
  5. Right-click the app and select **Manage Website** > **Advanced Settings**.
  6. The default **Preload Enabled** setting is `False`. Set **Preload Enabled** to `True`. Select **OK**.

* Using `web.config`, add the `<applicationInitialization>` element with `doAppInitAfterRestart` set to `true` to the `<system.webServer>` elements in the app's `web.config` file:

  ```
  <?xml version="1.0" encoding="utf-8"?>
  <configuration>
    <location path="." inheritInChildApplications="false">
      <system.webServer>
        <applicationInitialization doAppInitAfterRestart="true" />
      </system.webServer>
    </location>
  </configuration>
  ```

### Idle Timeout

*Only applies to apps hosted in-process.*

To prevent the app from idling, set the app pool's idle timeout using IIS Manager:

1. Select **Application Pools** in the **Connections** panel.
2. Right-click the app's app pool in the list and select **Advanced Settings**.
3. The default **Idle Time-out (minutes)** is `20` minutes. Set the **Idle Time-out (minutes)** to `0` (zero). Select **OK**.
4. Recycle the worker process.

To prevent apps hosted [out-of-process](out-of-process-hosting?view=aspnetcore-10.0) from timing out, use either of the following approaches:

* Ping the app from an external service in order to keep it running.
* If the app only hosts background services, avoid IIS hosting and use a [Windows Service to host the ASP.NET Core app](../windows-service?view=aspnetcore-10.0).

### Application Initialization Module and Idle Timeout additional resources

* [IIS 8.0 Application Initialization](/en-us/iis/get-started/whats-new-in-iis-8/iis-80-application-initialization)
* [Application Initialization `<applicationInitialization>`](/en-us/iis/configuration/system.webserver/applicationinitialization/)
* [Process Model Settings for an Application Pool `<processModel>`](/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel)

**IIS**:

* `%windir%\System32\inetsrv\config\schema\aspnetcore_schema.xml`

* `%windir%\System32\inetsrv\config\schema\aspnetcore_schema_v2.xml`

**IIS Express**:

* `%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema.xml`

* `%ProgramFiles%\IIS Express\config\schema\aspnetcore_schema_v2.xml`

### Configuration

**IIS**:

* `%windir%\System32\inetsrv\config\applicationHost.config`

**IIS Express**:

* Visual Studio: `{APPLICATION ROOT}\.vs\config\applicationHost.config`

* *iisexpress.exe* CLI: `%USERPROFILE%\Documents\IISExpress\config\applicationhost.config`

The files can be found by searching for `aspnetcore` in the `applicationHost.config` file.

## Install Web Deploy when publishing with Visual Studio

When deploying apps to servers with [Web Deploy](/en-us/iis/install/installing-publishing-technologies/installing-and-configuring-web-deploy-on-iis-80-or-later), install the latest version of Web Deploy on the server. To install Web Deploy, see [IIS Downloads: Web Deploy](https://www.iis.net/downloads/microsoft/web-deploy).

5. Right-click the site's app pool, and select **Basic Settings** from the contextual menu.

6. In the **Edit Application Pool** window, set the **.NET CLR version** to **No Managed Code**:

   ASP.NET Core runs in a separate process and manages the runtime. ASP.NET Core doesn't rely on loading the desktop CLR (.NET CLR). The Core Common Language Runtime (CoreCLR) for .NET is booted to host the app in the worker process. Setting the **.NET CLR version** to **No Managed Code** is optional but recommended.

   * For a 32-bit (x86) [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) published with a 32-bit SDK that uses the in-process hosting model, enable the Application Pool for 32-bit. In IIS Manager, navigate to **Application Pools** in the **Connections** sidebar. Select the app's Application Pool. In the **Actions** sidebar, select **Advanced Settings**. Set **Enable 32-Bit Applications** to `True`.

   * For a 64-bit (x64) [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) that uses the in-process hosting model, disable the app pool for 32-bit (x86) processes. In IIS Manager, navigate to **Application Pools** in the **Connections** sidebar. Select the app's Application Pool. In the **Actions** sidebar, select **Advanced Settings**. Set **Enable 32-Bit Applications** to `False`.

7. Confirm the process model identity has the proper permissions.

   If the default identity of the app pool (**Process Model** > **Identity**) is changed from **ApplicationPoolIdentity** to another identity, verify that the new identity has the required permissions to access the app's folder, database, and other required resources. For example, the app pool requires read and write access to folders where the app reads and writes files.

**Windows Authentication configuration (Optional)**\
For more information, see [Configure Windows authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).

## Shadow copy

Shadow copying app assemblies to the [ASP.NET Core Module (ANCM)](../aspnet-core-module?view=aspnetcore-10.0) for IIS can provide a better end user experience than stopping the app by deploying an [app offline file](../app-offline?view=aspnetcore-10.0).

When an ASP.NET Core app is running on Windows, the binaries are locked so that they can't be modified or replaced. Shadow copying enables the app assemblies to be updated while the app is running by making a copy of the assemblies.

Shadow copy isn't intended to enable zero-downtime deployment, so it's expected that IIS will still recycle the app, and some requests might get a [503 Service Unavailable](https://developer.mozilla.org/docs/Web/HTTP/Reference/Status/503) response. We recommend using a pattern like [blue-green deployments](https://www.martinfowler.com/bliki/BlueGreenDeployment.html) or [Azure deployment slots](/en-us/azure/app-service/deploy-best-practices#use-deployment-slots) for zero-downtime deployments. Shadow copy helps minimize downtime on deployments, but it can't completely eliminate it.

Customizing the ANCM handler settings in `web.config` enables shadow copying:

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <remove name="aspNetCore"/>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".logsstdout">
      <handlerSettings>
        <handlerSetting name="enableShadowCopy" value="true" />
        <!-- Ensure that the IIS ApplicationPool identity has permission to this directory -->
        <handlerSetting name="shadowCopyDirectory" value="../ShadowCopyDirectory/" />
        <handlerSetting name="cleanShadowCopyDirectory" value="true" />
      </handlerSettings>
    </aspNetCore>
  </system.webServer>
</configuration>
```

The following handler settings configure shadow copying:

* `enableShadowCopy`: When set to `true`, enables shadow copying of app assemblies.
* `shadowCopyDirectory`: Specifies the directory where assemblies are shadow copied. Ensure that the IIS `ApplicationPool` identity has read, write, and delete permissions (such as **Modify** permissions) to this directory.
* `cleanShadowCopyDirectory`: Optional. When set to `true`, cleans the shadow copy directory before copying assemblies on startup. The default value is `false`.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-06

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/security/authorization/simple
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/security/authorization/simple.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Simple authorization in ASP.NET Core MVC

Authorization in ASP.NET Core is controlled with the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and its various parameters. In its most basic form, applying the `[Authorize]` attribute to a Razor component, controller, action, or Razor Page, limits access to that component to authenticated users.

This article covers scenarios that pertain to MVC apps. For the primary coverage on this subject, see [Simple authorization in ASP.NET Core](../../../security/authorization/simple?view=aspnetcore-10.0).

## `[Authorize]` attribute

The following example limits access to authenticated users by specifying the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute):

```
[Authorize]
public class AccountController : Controller
{
    public ActionResult Login() { ... }
    public ActionResult Logout() { ... }
}
```

The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) also supports role-based or policy-based authorization. For role-based authorization, use the [Roles](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles#microsoft-aspnetcore-authorization-authorizeattribute-roles) parameter. In the following example, the user can only access the page if they're in the `Admin` or `Superuser` role:

```
[Authorize(Roles = "Admin, Superuser")]
public class OrderController : Controller
{
    ...
}
```

For policy-based authorization, use the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) parameter. In the following example, the user can only access the page if they satisfy the requirements of the `Over21` [authorization policy](../../../security/authorization/policies?view=aspnetcore-10.0):

```
[Authorize(Policy = "Over21")]
public class LicenseApplicationController : Controller
{
    ...
}
```

If neither [Roles](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles#microsoft-aspnetcore-authorization-authorizeattribute-roles) nor [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) is specified, [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) uses the default policy:

* Authenticated (signed-in) users are authorized.
* Unauthenticated (signed-out) users are unauthorized.

To apply authorization to an action rather than the controller, apply the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the action. In the following example, only authenticated users can trigger a logout (call the `Logout` method):

```
public class AccountController : Controller
{
   public ActionResult Login() { ... }

   [Authorize]
   public ActionResult Logout() { ... }
}
```

Use the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) to allow access by non-authenticated users to individual actions:

```
[AllowAnonymous]
```

Warning

For MVC controllers, the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) bypasses authorization statements. If you combine `[AllowAnonymous]` and one or more `[Authorize]` attributes, the `[Authorize]` attributes are ignored. If you apply `[AllowAnonymous]` at the controller level:

* Any authorization requirements from `[Authorize]` attributes on the same controller or action methods on the controller are ignored.
* Authentication middleware isn't short-circuited but doesn't need to succeed.

For information on how to require authentication for all app users, see [Require global user authentication](../../../security/authorization/policies?view=aspnetcore-10.0#require-global-user-authentication).

## Related content

* [Simple authorization in ASP.NET Core](../../../security/authorization/simple?view=aspnetcore-10.0)
* [Simple authorization in ASP.NET Core Razor Pages](../../../razor-pages/security/authorization/simple?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/protected-browser-storage
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/state-management/protected-browser-storage.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor protected browser storage

For transient data that the user is actively creating, a commonly used storage location is the browser's [`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage) and [`sessionStorage`](https://developer.mozilla.org/docs/Web/API/Window/sessionStorage) collections:

* `localStorage` is scoped to the browser's instance. If the user reloads the page or closes and reopens the browser, the state persists. If the user opens multiple browser tabs, the state is shared across the tabs. Data persists in `localStorage` until explicitly cleared. The `localStorage` data for a document loaded in a "private browsing" or "incognito" session is cleared when the last "private" tab is closed.
* `sessionStorage` is scoped to the browser tab. If the user reloads the tab, the state persists. If the user closes the tab or the browser, the state is lost. If the user opens multiple browser tabs, each tab has its own independent version of the data.

Generally, `sessionStorage` is safer to use. `sessionStorage` avoids the risk that a user opens multiple tabs and encounters the following:

* Bugs in state storage across tabs.
* Confusing behavior when a tab overwrites the state of other tabs.

`localStorage` is the better choice if the app must persist state across closing and reopening the browser.

Caveats for using browser storage:

* Similar to the use of a server-side database, loading and saving data are asynchronous.
* The requested page doesn't exist in the browser during prerendering, so local storage isn't available during prerendering.
* Storage of a few kilobytes of data is reasonable to persist for server-side Blazor apps. Beyond a few kilobytes, you must consider the performance implications because the data is loaded and saved across the network.
* Users may view or tamper with the data. [ASP.NET Core Data Protection](../../security/data-protection/introduction?view=aspnetcore-10.0) can mitigate the risk. For example, [ASP.NET Core Protected Browser Storage](#aspnet-core-protected-browser-storage) uses ASP.NET Core Data Protection.

Third-party NuGet packages provide APIs for working with `localStorage` and `sessionStorage`. It's worth considering choosing a package that transparently uses [ASP.NET Core Data Protection](../../security/data-protection/introduction?view=aspnetcore-10.0). Data Protection encrypts stored data and reduces the potential risk of tampering with stored data. If JSON-serialized data is stored in plain text, users can see the data using browser developer tools and also modify the stored data. Securing trivial data isn't a problem. For example, reading or modifying the stored color of a UI element isn't a significant security risk to the user or the organization. Avoid allowing users to inspect or tamper with *sensitive data*.

## ASP.NET Core Protected Browser Storage

ASP.NET Core Protected Browser Storage leverages [ASP.NET Core Data Protection](../../security/data-protection/introduction?view=aspnetcore-10.0) for [`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage) and [`sessionStorage`](https://developer.mozilla.org/docs/Web/API/Window/sessionStorage).

Note

Protected Browser Storage relies on ASP.NET Core Data Protection and is only supported for server-side Blazor apps.

Warning

`Microsoft.AspNetCore.ProtectedBrowserStorage` is an unsupported, experimental package that isn't intended for production use.

The package is only available for use in ASP.NET Core 3.1 apps.

## Configuration

1. Add a package reference to [`Microsoft.AspNetCore.ProtectedBrowserStorage`](https://www.nuget.org/packages/Microsoft.AspNetCore.ProtectedBrowserStorage).

   Note

   For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

2. In the `_Host.cshtml` file, add the following script inside the closing `</body>` tag:

   ```
   <script src="_content/Microsoft.AspNetCore.ProtectedBrowserStorage/protectedBrowserStorage.js"></script>
   ```

3. In `Startup.ConfigureServices`, call `AddProtectedBrowserStorage` to add `localStorage` and `sessionStorage` services to the service collection:

   ```
   services.AddProtectedBrowserStorage();
   ```

## Save and load data within a component

In any component that requires loading or saving data to browser storage, use the [`@inject`](../../mvc/views/razor?view=aspnetcore-10.0#inject) directive to inject an instance of either of the following:

* `ProtectedLocalStorage`
* `ProtectedSessionStorage`

The choice depends on which browser storage location you wish to use. In the following example, `sessionStorage` is used:

```
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
```

```
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
```

The `@using` directive can be placed in the app's imports file (`_Imports.razor`) instead of in the component. Use of the imports file makes the namespace available to larger segments of the app or the whole app.

To persist the `currentCount` value in the `Counter` component of an app based on the [Blazor project template](../project-structure?view=aspnetcore-10.0), modify the `IncrementCount` method to use `ProtectedSessionStore.SetAsync`:

```
private async Task IncrementCount()
{
    currentCount++;
    await ProtectedSessionStore.SetAsync("count", currentCount);
}
```

In larger, more realistic apps, storage of individual fields is an unlikely scenario. Apps are more likely to store entire model objects that include complex state. `ProtectedSessionStore` automatically serializes and deserializes JSON data to store complex state objects.

In the preceding code example, the `currentCount` data is stored as `sessionStorage['count']` in the user's browser. The data isn't stored in plain text but rather is protected using ASP.NET Core Data Protection. The encrypted data can be inspected if `sessionStorage['count']` is evaluated in the browser's developer console.

To recover the `currentCount` data if the user returns to the `Counter` component later, including if the user is on a new circuit, use `ProtectedSessionStore.GetAsync`:

```
protected override async Task OnInitializedAsync()
{
    var result = await ProtectedSessionStore.GetAsync<int>("count");
    currentCount = result.Success ? result.Value : 0;
}
```

```
protected override async Task OnInitializedAsync()
{
    currentCount = await ProtectedSessionStore.GetAsync<int>("count");
}
```

If the component's parameters include navigation state, call `ProtectedSessionStore.GetAsync` and assign a non-`null` result in [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync), not [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync). [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) is only called once when the component is first instantiated. [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) isn't called again later if the user navigates to a different URL while remaining on the same page. For more information, see [ASP.NET Core Razor component lifecycle](../components/lifecycle?view=aspnetcore-10.0).

Warning

The examples in this section only work if the server doesn't have prerendering enabled. With prerendering enabled, an error is generated explaining that JavaScript interop calls cannot be issued because the component is being prerendered.

Either disable prerendering or add additional code to work with prerendering. To learn more about writing code that works with prerendering, see the [Handle prerendering](#handle-prerendering) section.

## Handle the loading state

Since browser storage is accessed asynchronously over a network connection, there's always a period of time before the data is loaded and available to a component. For the best results, render a message while loading is in progress instead of displaying blank or default data.

One approach is to track whether the data is `null`, which means that the data is still loading. In the default `Counter` component, the count is held in an `int`. [Make `currentCount` nullable](/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) by adding a question mark (`?`) to the type (`int`):

```
private int? currentCount;
```

Instead of unconditionally displaying the count and **`Increment`** button, display these elements only if the data is loaded by checking [HasValue](/en-us/dotnet/api/system.nullable-1.hasvalue):

```
@if (currentCount.HasValue)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}
```

## Handle prerendering

During prerendering:

* An interactive connection to the user's browser doesn't exist.
* The browser doesn't yet have a page in which it can run JavaScript code.

`localStorage` or `sessionStorage` aren't available during prerendering. If the component attempts to interact with storage, an error is generated explaining that JavaScript interop calls cannot be issued because the component is being prerendered.

One way to resolve the error is to disable prerendering. This is usually the best choice if the app makes heavy use of browser-based storage. Prerendering adds complexity and doesn't benefit the app because the app can't prerender any useful content until `localStorage` or `sessionStorage` are available.

To disable prerendering, indicate the render mode with the `prerender` parameter set to `false` at the highest-level component in the app's component hierarchy that isn't a root component.

Note

Making a root component interactive, such as the `App` component, isn't supported. Therefore, prerendering can't be disabled directly by the `App` component.

For apps based on the Blazor Web App project template, prerendering is typically disabled where the `Routes` component is used in the `App` component (`Components/App.razor`):

```
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
```

Also, disable prerendering for the `HeadOutlet` component:

```
<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
```

For more information, see [Prerender ASP.NET Core Razor components](../components/prerender?view=aspnetcore-10.0#disable-prerendering).

To disable prerendering, open the `_Host.cshtml` file and change the `render-mode` attribute of the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) to [Server](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-server):

```
<component type="typeof(App)" render-mode="Server" />
```

When prerendering is disabled, [prerendering of `<head>` content](../components/control-head-content?view=aspnetcore-10.0) is disabled.

Prerendering might be useful for other pages that don't use `localStorage` or `sessionStorage`. To retain prerendering, defer the loading operation until the browser is connected to the circuit. The following is an example for storing a counter value:

```
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (RendererInfo.IsInteractive)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount;

    protected override async Task OnInitializedAsync()
    {
        if (RendererInfo.IsInteractive)
        {
            await LoadStateAsync();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedLocalStore.GetAsync<int>("count");
        currentCount = result.Success ? result.Value : 0;
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}
```

For more information on [RendererInfo.IsInteractive](/en-us/dotnet/api/microsoft.aspnetcore.components.rendererinfo.isinteractive#microsoft-aspnetcore-components-rendererinfo-isinteractive), see [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0#detect-rendering-location-interactivity-and-assigned-render-mode-at-runtime).

```
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount;
    private bool isConnected;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedLocalStore.GetAsync<int>("count");
        currentCount = result.Success ? result.Value : 0;
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}
```

```
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount = 0;
    private bool isConnected = false;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        currentCount = await ProtectedLocalStore.GetAsync<int>("count");
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}
```

## Factor out state preservation to a common provider

If many components rely on browser-based storage, implementing state provider code many times creates code duplication. One option for avoiding code duplication is to create a *state provider parent component* that encapsulates the state provider logic. Child components can work with persisted data without regard to the state persistence mechanism.

In the following example of a `CounterStateProvider` component, counter data is persisted to `sessionStorage`, and it handles the loading phase by not rendering its child content until state loading is complete.

The `CounterStateProvider` component deals with prerendering by not loading state until after component rendering in the [`OnAfterRenderAsync` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync), which doesn't execute during prerendering.

The approach in this section isn't capable of triggering rerendering of multiple subscribed components on the same page. If one subscribed component changes the state, it rerenders and can display the updated state, but a different component on the same page displaying that state displays stale data until its own next rerender. Therefore, the approach described in this section is best suited to using state in a single component on the page.

`CounterStateProvider.razor`:

```
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isLoaded = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedSessionStore.GetAsync<int>("count");
        CurrentCount = result.Success ? result.Value : 0;
        isLoaded = true;
    }

    public async Task IncrementCount()
    {
        CurrentCount++;
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}
```

```
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isLoaded = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        CurrentCount = await ProtectedSessionStore.GetAsync<int>("count");
        isLoaded = true;
    }

    public async Task IncrementCount()
    {
        CurrentCount++;
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}
```

Note

For more information on [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), see [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#child-content-render-fragments).

To make the state accessible to all components in an app, wrap the `CounterStateProvider` component around the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) (`<Router>...</Router>`) in the `Routes` component with global interactive server-side rendering (interactive SSR).

In the `App` component (`Components/App.razor`):

```
<Routes @rendermode="InteractiveServer" />
```

In the `Routes` component (`Components/Routes.razor`):

To use the `CounterStateProvider` component, wrap an instance of the component around any other component that requires access to the counter state. To make the state accessible to all components in an app, wrap the `CounterStateProvider` component around the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) in the `App` component (`App.razor`):

```
<CounterStateProvider>
    <Router ...>
        ...
    </Router>
</CounterStateProvider>
```

Note

With the release of .NET 5.0.1 and for any additional 5.x releases, the `Router` component includes the `PreferExactMatches` parameter set to `@true`. For more information, see [Migrate from ASP.NET Core 3.1 to .NET 5](../../migration/31-to-50?view=aspnetcore-10.0#changes-to-blazor-app-routing-logic-in-501-and-further-5x-releases-up-to-60).

Wrapped components receive and can modify the persisted counter state. The following `Counter` component implements the pattern:

```
@page "/counter"

<p>Current count: <strong>@CounterStateProvider?.CurrentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>

@code {
    [CascadingParameter]
    private CounterStateProvider? CounterStateProvider { get; set; }

    private async Task IncrementCount()
    {
        if (CounterStateProvider is not null)
        {
            await CounterStateProvider.IncrementCount();
        }
    }
}
```

The preceding component isn't required to interact with `ProtectedBrowserStorage`, nor does it deal with a "loading" phase.

In general, the *state provider parent component* pattern is recommended:

* To consume state across many components.
* If there's just one top-level state object to persist.

To persist many different state objects and consume different subsets of objects in different places, it's better to avoid persisting state globally.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/complex-data-model
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-rp/complex-data-model.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 5, Razor Pages with EF Core in ASP.NET Core - Data Model

By [Tom Dykstra](https://github.com/tdykstra), [Jeremy Likness](https://twitter.com/jeremylikness), and [Jon P Smith](https://twitter.com/thereformedprog)

The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. For information about the tutorial series, see [the first tutorial](intro?view=aspnetcore-10.0).

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) and compare that code to what you created by following the tutorial.

The previous tutorials worked with a basic data model that was composed of three entities. In this tutorial:

* More entities and relationships are added.
* The data model is customized by specifying formatting, validation, and database mapping rules.

The completed data model is shown in the following illustration:

The following database diagram was made with [Dataedo](https://dataedo.com/):

To create a database diagram with Dataedo:

* [Deploy the app to Azure](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app)
* Download and install [Dataedo](https://dataedo.com/) on your computer.
* Follow the instructions [Generate documentation for Azure SQL Database in 5 minutes](https://dataedo.com/tutorials/generate-documentation-for-azure-sql-database)

In the preceding Dataedo diagram, the `CourseInstructor` is a join table created by Entity Framework. For more information, see [Many-to-many](/en-us/ef/core/modeling/relationships#many-to-many)

## The Student entity

Replace the code in `Models/Student.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The preceding code adds a `FullName` property and adds the following attributes to existing properties:

* [`[DataType]`](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute)
* [`[DisplayFormat]`](/en-us/dotnet/api/system.componentmodel.dataannotations.displayformatattribute)
* [`[StringLength]`](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute)
* [`[Column]`](/en-us/dotnet/api/system.componentmodel.dataannotations.schema.columnattribute)
* [`[Required]`](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute)
* [`[Display]`](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute)

### The FullName calculated property

`FullName` is a calculated property that returns a value that's created by concatenating two other properties. `FullName` can't be set, so it has only a get accessor. No `FullName` column is created in the database.

### The DataType attribute

```
[DataType(DataType.Date)]
```

For student enrollment dates, all of the pages currently display the time of day along with the date, although only the date is relevant. By using data annotation attributes, you can make one code change that will fix the display format in every page that shows the data.

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies a data type that's more specific than the database intrinsic type. In this case only the date should be displayed, not the date and time. The [DataType Enumeration](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress, etc. The `DataType` attribute can also enable the app to automatically provide type-specific features. For example:

* The `mailto:` link is automatically created for `DataType.EmailAddress`.
* The date selector is provided for `DataType.Date` in most browsers.

The `DataType` attribute emits HTML 5 `data-` (pronounced data dash) attributes. The `DataType` attributes don't provide validation.

### The DisplayFormat attribute

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
```

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the date field is displayed according to the default formats based on the server's [CultureInfo](../../fundamentals/localization?view=aspnetcore-10.0#provide-localized-resources-for-the-languages-and-cultures-you-support).

The `DisplayFormat` attribute is used to explicitly specify the date format. The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied to the edit UI. Some fields shouldn't use `ApplyFormatInEditMode`. For example, the currency symbol should generally not be displayed in an edit text box.

The `DisplayFormat` attribute can be used by itself. It's generally a good idea to use the `DataType` attribute with the `DisplayFormat` attribute. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen. The `DataType` attribute provides the following benefits that are not available in `DisplayFormat`:

* The browser can enable HTML5 features. For example, show a calendar control, the locale-appropriate currency symbol, email links, and client-side input validation.
* By default, the browser renders data using the correct format based on the locale.

For more information, see the [\<input> Tag Helper documentation](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper).

### The StringLength attribute

```
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
```

Data validation rules and validation error messages can be specified with attributes. The [StringLength](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute) attribute specifies the minimum and maximum length of characters that are allowed in a data field. The code shown limits names to no more than 50 characters. An example that sets the minimum string length is shown [later](#the-required-attribute).

The `StringLength` attribute also provides client-side and server-side validation. The minimum value has no impact on the database schema.

The `StringLength` attribute doesn't prevent a user from entering white space for a name. The [RegularExpression](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute) attribute can be used to apply restrictions to the input. For example, the following code requires the first character to be upper case and the remaining characters to be alphabetical:

```
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
```

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

In **SQL Server Object Explorer** (SSOX), open the Student table designer by double-clicking the **Student** table.

The preceding image shows the schema for the `Student` table. The name fields have type `nvarchar(MAX)`. When a migration is created and applied later in this tutorial, the name fields become `nvarchar(50)` as a result of the string length attributes.

Using a SQLite tool, examine the column definitions for the `Student` table. The name fields have type `Text`. Notice that the first name field is called `FirstMidName`. In the next section, `FirstMidName` is changed to `FirstName`.

### The Column attribute

```
[Column("FirstName")]
public string FirstMidName { get; set; }
```

Attributes can control how classes and properties are mapped to the database. In the `Student` model, the `Column` attribute is used to map the name of the `FirstMidName` property to "FirstName" in the database.

When the database is created, property names on the model are used for column names (except when the `Column` attribute is used). The `Student` model uses `FirstMidName` for the first-name field because the field might also contain a middle name.

With the `[Column]` attribute, `Student.FirstMidName` in the data model maps to the `FirstName` column of the `Student` table. The addition of the `Column` attribute changes the model backing the `SchoolContext`. The model backing the `SchoolContext` no longer matches the database. That discrepancy will be resolved by adding a migration later in this tutorial.

### The Required attribute

```
[Required]
```

The `Required` attribute makes the name properties required fields. The `Required` attribute isn't needed for non-nullable types such as value types (for example, `DateTime`, `int`, and `double`). Types that can't be null are automatically treated as required fields.

The `Required` attribute must be used with `MinimumLength` for the `MinimumLength` to be enforced.

```
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
```

`MinimumLength` and `Required` allow whitespace to satisfy the validation. Use the `RegularExpression` attribute for full control over the string.

### The Display attribute

```
[Display(Name = "Last Name")]
```

The `Display` attribute specifies that the caption for the text boxes should be "First Name", "Last Name", "Full Name", and "Enrollment Date." The default captions had no space dividing the words, for example "Lastname."

### Create a migration

Run the app and go to the Students page. An exception is thrown. The `[Column]` attribute causes EF to expect to find a column named `FirstName`, but the column name in the database is still `FirstMidName`.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

The error message is similar to the following example:

```
SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:

SchoolContext
```

* In the PMC, enter the following commands to create a new migration and update the database:

  ```
  Add-Migration ColumnFirstName
  Update-Database
  ```

  The first of these commands generates the following warning message:

  ```
  An operation was scaffolded that may result in the loss of data.
  Please review the migration for accuracy.
  ```

  The warning is generated because the name fields are now limited to 50 characters. If a name in the database had more than 50 characters, the 51 to last character would be lost.

* Open the Student table in SSOX:

  Before the migration was applied, the name columns were of type [nvarchar(MAX)](/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql). The name columns are now `nvarchar(50)`. The column name has changed from `FirstMidName` to `FirstName`.

The error message is similar to the following example:

```
SqliteException: SQLite Error 1: 'no such column: s.FirstName'.
```

* Open a command window in the project folder. Enter the following commands to create a new migration and update the database:

  ```
  dotnet ef migrations add ColumnFirstName
  dotnet ef database update
  ```

- Examine the Student table with a SQLite tool. The column that was `FirstMidName` is now `FirstName`.

* Run the app and go to the Students page.
* Notice that times are not input or displayed along with dates.
* Select **Create New**, and try to enter a name longer than 50 characters.

Note

In the following sections, building the app at some stages generates compiler errors. The instructions specify when to build the app.

## The Instructor Entity

Create `Models/Instructor.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<Course> Courses { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}
```

Multiple attributes can be on one line. The `HireDate` attributes could be written as follows:

```
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
```

### Navigation properties

The `Courses` and `OfficeAssignment` properties are navigation properties.

An instructor can teach any number of courses, so `Courses` is defined as a collection.

```
public ICollection<Course> Courses { get; set; }
```

An instructor can have at most one office, so the `OfficeAssignment` property holds a single `OfficeAssignment` entity. `OfficeAssignment` is null if no office is assigned.

```
public OfficeAssignment OfficeAssignment { get; set; }
```

## The OfficeAssignment entity

Create `Models/OfficeAssignment.cs` with the following code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}
```

### The Key attribute

The [`[Key]`](/en-us/dotnet/api/system.componentmodel.dataannotations.keyattribute) attribute is used to identify a property as the primary key (PK) when the property name is something other than `classnameID` or `ID`.

There's a one-to-zero-or-one relationship between the `Instructor` and `OfficeAssignment` entities. An office assignment only exists in relation to the instructor it's assigned to. The `OfficeAssignment` PK is also its foreign key (FK) to the `Instructor` entity. A one-to-zero-or-one relationship occurs when a PK in one table is both a PK and a FK in another table.

EF Core can't automatically recognize `InstructorID` as the PK of `OfficeAssignment` because `InstructorID` doesn't follow the ID or classnameID naming convention. Therefore, the `Key` attribute is used to identify `InstructorID` as the PK:

```
[Key]
public int InstructorID { get; set; }
```

By default, EF Core treats the key as non-database-generated because the column is for an identifying relationship. For more information, see [EF Keys](/en-us/ef/core/modeling/keys).

### The Instructor navigation property

The `Instructor.OfficeAssignment` navigation property can be null because there might not be an `OfficeAssignment` row for a given instructor. An instructor might not have an office assignment.

The `OfficeAssignment.Instructor` navigation property will always have an instructor entity because the foreign key `InstructorID` type is `int`, a non-nullable value type. An office assignment can't exist without an instructor.

When an `Instructor` entity has a related `OfficeAssignment` entity, each entity has a reference to the other one in its navigation property.

## The Course Entity

Update `Models/Course.cs` with the following code:

```
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<Instructor> Instructors { get; set; }
    }
}
```

The `Course` entity has a foreign key (FK) property `DepartmentID`. `DepartmentID` points to the related `Department` entity. The `Course` entity has a `Department` navigation property.

EF Core doesn't require a foreign key property for a data model when the model has a navigation property for a related entity. EF Core automatically creates FKs in the database wherever they're needed. EF Core creates [shadow properties](/en-us/ef/core/modeling/shadow-properties) for automatically created FKs. However, explicitly including the FK in the data model can make updates simpler and more efficient. For example, consider a model where the FK property `DepartmentID` is ***not*** included. When a course entity is fetched to edit:

* The `Department` property is `null` if it's not explicitly loaded.
* To update the course entity, the `Department` entity must first be fetched.

When the FK property `DepartmentID` is included in the data model, there's no need to fetch the `Department` entity before an update.

### The DatabaseGenerated attribute

The `[DatabaseGenerated(DatabaseGeneratedOption.None)]` attribute specifies that the PK is provided by the application rather than generated by the database.

```
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
```

By default, EF Core assumes that PK values are generated by the database. Database-generated is generally the best approach. For `Course` entities, the user specifies the PK. For example, a course number such as a 1000 series for the math department, a 2000 series for the English department.

The `DatabaseGenerated` attribute can also be used to generate default values. For example, the database can automatically generate a date field to record the date a row was created or updated. For more information, see [Generated Properties](/en-us/ef/core/modeling/generated-properties).

### Foreign key and navigation properties

The foreign key (FK) properties and navigation properties in the `Course` entity reflect the following relationships:

A course is assigned to one department, so there's a `DepartmentID` FK and a `Department` navigation property.

```
public int DepartmentID { get; set; }
public Department Department { get; set; }
```

A course can have any number of students enrolled in it, so the `Enrollments` navigation property is a collection:

```
public ICollection<Enrollment> Enrollments { get; set; }
```

A course may be taught by multiple instructors, so the `Instructors` navigation property is a collection:

```
public ICollection<Instructor> Instructors { get; set; }
```

## The Department entity

Create `Models/Department.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
                       ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}
```

### The Column attribute

Previously the `Column` attribute was used to change column name mapping. In the code for the `Department` entity, the `Column` attribute is used to change SQL data type mapping. The `Budget` column is defined using the SQL Server money type in the database:

```
[Column(TypeName="money")]
public decimal Budget { get; set; }
```

Column mapping is generally not required. EF Core chooses the appropriate SQL Server data type based on the CLR type for the property. The CLR `decimal` type maps to a SQL Server `decimal` type. `Budget` is for currency, and the money data type is more appropriate for currency.

### Foreign key and navigation properties

The FK and navigation properties reflect the following relationships:

* A department may or may not have an administrator.
* An administrator is always an instructor. Therefore the `InstructorID` property is included as the FK to the `Instructor` entity.

The navigation property is named `Administrator` but holds an `Instructor` entity:

```
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
```

The `?` in the preceding code specifies the property is nullable.

A department may have many courses, so there's a Courses navigation property:

```
public ICollection<Course> Courses { get; set; }
```

By convention, EF Core enables cascade delete for non-nullable FKs and for many-to-many relationships. This default behavior can result in circular cascade delete rules. Circular cascade delete rules cause an exception when a migration is added.

For example, if the `Department.InstructorID` property was defined as non-nullable, EF Core would configure a cascade delete rule. In that case, the department would be deleted when the instructor assigned as its administrator is deleted. In this scenario, a restrict rule would make more sense. The following [fluent API](#fluent-api-alternative-to-attributes) would set a restrict rule and disable cascade delete.

```
modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)
```

### The Enrollment foreign key and navigation properties

An enrollment record is for one course taken by one student.

Update `Models/Enrollment.cs` with the following code:

```
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}
```

The FK properties and navigation properties reflect the following relationships:

An enrollment record is for one course, so there's a `CourseID` FK property and a `Course` navigation property:

```
public int CourseID { get; set; }
public Course Course { get; set; }
```

An enrollment record is for one student, so there's a `StudentID` FK property and a `Student` navigation property:

```
public int StudentID { get; set; }
public Student Student { get; set; }
```

## Many-to-Many Relationships

There's a many-to-many relationship between the `Student` and `Course` entities. The `Enrollment` entity functions as a many-to-many join table ***with payload*** in the database. ***With payload*** means that the `Enrollment` table contains additional data besides FKs for the joined tables. In the `Enrollment` entity, the additional data besides FKs are the PK and `Grade`.

The following illustration shows what these relationships look like in an entity diagram. (This diagram was generated using [EF Power Tools](https://marketplace.visualstudio.com/items?itemName=ErikEJ.EntityFramework6PowerToolsCommunityEdition) for EF 6.x. Creating the diagram isn't part of the tutorial.)

Each relationship line has a 1 at one end and an asterisk (\*) at the other, indicating a one-to-many relationship.

If the `Enrollment` table didn't include grade information, it would only need to contain the two FKs, `CourseID` and `StudentID`. A many-to-many join table without payload is sometimes called a pure join table (PJT).

The `Instructor` and `Course` entities have a many-to-many relationship using a PJT.

## Update the database context

Update `Data/SchoolContext.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable(nameof(Course))
                .HasMany(c => c.Instructors)
                .WithMany(i => i.Courses);
            modelBuilder.Entity<Student>().ToTable(nameof(Student));
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
        }
    }
}
```

The preceding code adds the new entities and configures the many-to-many relationship between the `Instructor` and `Course` entities.

## Fluent API alternative to attributes

The `OnModelCreating` method in the preceding code uses the *fluent API* to configure EF Core behavior. The API is called "fluent" because it's often used by stringing a series of method calls together into a single statement. The [following code](/en-us/ef/core/modeling/#use-fluent-api-to-configure-a-model) is an example of the fluent API:

```
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}
```

In this tutorial, the fluent API is used only for database mapping that can't be done with attributes. However, the fluent API can specify most of the formatting, validation, and mapping rules that can be done with attributes.

Some attributes such as `MinimumLength` can't be applied with the fluent API. `MinimumLength` doesn't change the schema, it only applies a minimum length validation rule.

Some developers prefer to use the fluent API exclusively so that they can keep their entity classes *clean*. Attributes and the fluent API can be mixed. There are some configurations that can only be done with the fluent API, for example, specifying a composite PK. There are some configurations that can only be done with attributes (`MinimumLength`). The recommended practice for using fluent API or attributes:

* Choose one of these two approaches.
* Use the chosen approach consistently as much as possible.

Some of the attributes used in this tutorial are used for:

* Validation only (for example, `MinimumLength`).
* EF Core configuration only (for example, `HasKey`).
* Validation and EF Core configuration (for example, `[StringLength(50)]`).

For more information about attributes vs. fluent API, see [Methods of configuration](/en-us/ef/core/modeling/).

## Seed the database

Update the code in `Data/DbInitializer.cs`:

```
using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var alexander = new Student
            {
                FirstMidName = "Carson",
                LastName = "Alexander",
                EnrollmentDate = DateTime.Parse("2016-09-01")
            };

            var alonso = new Student
            {
                FirstMidName = "Meredith",
                LastName = "Alonso",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var anand = new Student
            {
                FirstMidName = "Arturo",
                LastName = "Anand",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var barzdukas = new Student
            {
                FirstMidName = "Gytis",
                LastName = "Barzdukas",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var li = new Student
            {
                FirstMidName = "Yan",
                LastName = "Li",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var justice = new Student
            {
                FirstMidName = "Peggy",
                LastName = "Justice",
                EnrollmentDate = DateTime.Parse("2017-09-01")
            };

            var norman = new Student
            {
                FirstMidName = "Laura",
                LastName = "Norman",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var olivetto = new Student
            {
                FirstMidName = "Nino",
                LastName = "Olivetto",
                EnrollmentDate = DateTime.Parse("2011-09-01")
            };

            var students = new Student[]
            {
                alexander,
                alonso,
                anand,
                barzdukas,
                li,
                justice,
                norman,
                olivetto
            };

            context.AddRange(students);

            var abercrombie = new Instructor
            {
                FirstMidName = "Kim",
                LastName = "Abercrombie",
                HireDate = DateTime.Parse("1995-03-11")
            };

            var fakhouri = new Instructor
            {
                FirstMidName = "Fadi",
                LastName = "Fakhouri",
                HireDate = DateTime.Parse("2002-07-06")
            };

            var harui = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Harui",
                HireDate = DateTime.Parse("1998-07-01")
            };

            var kapoor = new Instructor
            {
                FirstMidName = "Candace",
                LastName = "Kapoor",
                HireDate = DateTime.Parse("2001-01-15")
            };

            var zheng = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Zheng",
                HireDate = DateTime.Parse("2004-02-12")
            };

            var instructors = new Instructor[]
            {
                abercrombie,
                fakhouri,
                harui,
                kapoor,
                zheng
            };

            context.AddRange(instructors);

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    Instructor = fakhouri,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    Instructor = harui,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    Instructor = kapoor,
                    Location = "Thompson 304" }
            };

            context.AddRange(officeAssignments);

            var english = new Department
            {
                Name = "English",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = abercrombie
            };

            var mathematics = new Department
            {
                Name = "Mathematics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = fakhouri
            };

            var engineering = new Department
            {
                Name = "Engineering",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = harui
            };

            var economics = new Department
            {
                Name = "Economics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = kapoor
            };

            var departments = new Department[]
            {
                english,
                mathematics,
                engineering,
                economics
            };

            context.AddRange(departments);

            var chemistry = new Course
            {
                CourseID = 1050,
                Title = "Chemistry",
                Credits = 3,
                Department = engineering,
                Instructors = new List<Instructor> { kapoor, harui }
            };

            var microeconomics = new Course
            {
                CourseID = 4022,
                Title = "Microeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var macroeconmics = new Course
            {
                CourseID = 4041,
                Title = "Macroeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var calculus = new Course
            {
                CourseID = 1045,
                Title = "Calculus",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { fakhouri }
            };

            var trigonometry = new Course
            {
                CourseID = 3141,
                Title = "Trigonometry",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { harui }
            };

            var composition = new Course
            {
                CourseID = 2021,
                Title = "Composition",
                Credits = 3,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var literature = new Course
            {
                CourseID = 2042,
                Title = "Literature",
                Credits = 4,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var courses = new Course[]
            {
                chemistry,
                microeconomics,
                macroeconmics,
                calculus,
                trigonometry,
                composition,
                literature
            };

            context.AddRange(courses);

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    Student = alexander,
                    Course = chemistry,
                    Grade = Grade.A
                },
                new Enrollment {
                    Student = alexander,
                    Course = microeconomics,
                    Grade = Grade.C
                },
                new Enrollment {
                    Student = alexander,
                    Course = macroeconmics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = calculus,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = trigonometry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = anand,
                    Course = chemistry
                },
                new Enrollment {
                    Student = anand,
                    Course = microeconomics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = barzdukas,
                    Course = chemistry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = li,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = justice,
                    Course = literature,
                    Grade = Grade.B
                }
            };

            context.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}
```

The preceding code provides seed data for the new entities. Most of this code creates new entity objects and loads sample data. The sample data is used for testing.

[]()

## Apply the migration or drop and re-create

With the existing database, there are two approaches to changing the database:

* [Drop and re-create the database](#drop). Choose this section when using SQLite.
* [Apply the migration to the existing database](#applyexisting). The instructions in this section work for SQL Server only, ***not for SQLite***.

Either choice works for SQL Server. While the apply-migration method is more complex and time-consuming, it's the preferred approach for real-world, production environments.

[]()

## Drop and re-create the database

To force EF Core to create a new database, drop and update the database:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

- Delete the *Migrations* folder.
- In the **Package Manager Console** (PMC), run the following commands:

```
Drop-Database
Add-Migration InitialCreate
Update-Database
```

* Open a command window and navigate to the project folder. The project folder contains the `ContosoUniversity.csproj` file.
* Delete the *Migrations* folder.
* Run the following commands:

```
dotnet ef database drop --force
dotnet ef migrations add InitialCreate
dotnet ef database update
```

Run the app. Running the app runs the `DbInitializer.Initialize` method. The `DbInitializer.Initialize` populates the new database.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

Open the database in SSOX:

* If SSOX was opened previously, click the **Refresh** button.
* Expand the **Tables** node. The created tables are displayed.

Use a SQLite tool to examine the database:

* New tables and columns.
* Seeded data in tables.

## Next steps

The next two tutorials show how to read and update related data.

[Previous tutorial](migrations?view=aspnetcore-10.0) [Next tutorial](read-related-data?view=aspnetcore-10.0)

The previous tutorials worked with a basic data model that was composed of three entities. In this tutorial:

* More entities and relationships are added.
* The data model is customized by specifying formatting, validation, and database mapping rules.

The completed data model is shown in the following illustration:

## The Student entity

Replace the code in `Models/Student.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The preceding code adds a `FullName` property and adds the following attributes to existing properties:

* `[DataType]`
* `[DisplayFormat]`
* `[StringLength]`
* `[Column]`
* `[Required]`
* `[Display]`

### The FullName calculated property

`FullName` is a calculated property that returns a value that's created by concatenating two other properties. `FullName` can't be set, so it has only a get accessor. No `FullName` column is created in the database.

### The DataType attribute

```
[DataType(DataType.Date)]
```

For student enrollment dates, all of the pages currently display the time of day along with the date, although only the date is relevant. By using data annotation attributes, you can make one code change that will fix the display format in every page that shows the data.

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies a data type that's more specific than the database intrinsic type. In this case only the date should be displayed, not the date and time. The [DataType Enumeration](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress, etc. The `DataType` attribute can also enable the app to automatically provide type-specific features. For example:

* The `mailto:` link is automatically created for `DataType.EmailAddress`.
* The date selector is provided for `DataType.Date` in most browsers.

The `DataType` attribute emits HTML 5 `data-` (pronounced data dash) attributes. The `DataType` attributes don't provide validation.

### The DisplayFormat attribute

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
```

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the date field is displayed according to the default formats based on the server's [CultureInfo](../../fundamentals/localization?view=aspnetcore-10.0#provide-localized-resources-for-the-languages-and-cultures-you-support).

The `DisplayFormat` attribute is used to explicitly specify the date format. The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied to the edit UI. Some fields shouldn't use `ApplyFormatInEditMode`. For example, the currency symbol should generally not be displayed in an edit text box.

The `DisplayFormat` attribute can be used by itself. It's generally a good idea to use the `DataType` attribute with the `DisplayFormat` attribute. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen. The `DataType` attribute provides the following benefits that are not available in `DisplayFormat`:

* The browser can enable HTML5 features. For example, show a calendar control, the locale-appropriate currency symbol, email links, and client-side input validation.
* By default, the browser renders data using the correct format based on the locale.

For more information, see the [\<input> Tag Helper documentation](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper).

### The StringLength attribute

```
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
```

Data validation rules and validation error messages can be specified with attributes. The [StringLength](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute) attribute specifies the minimum and maximum length of characters that are allowed in a data field. The code shown limits names to no more than 50 characters. An example that sets the minimum string length is shown [later](#the-required-attribute).

The `StringLength` attribute also provides client-side and server-side validation. The minimum value has no impact on the database schema.

The `StringLength` attribute doesn't prevent a user from entering white space for a name. The [RegularExpression](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute) attribute can be used to apply restrictions to the input. For example, the following code requires the first character to be upper case and the remaining characters to be alphabetical:

```
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
```

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

In **SQL Server Object Explorer** (SSOX), open the Student table designer by double-clicking the **Student** table.

The preceding image shows the schema for the `Student` table. The name fields have type `nvarchar(MAX)`. When a migration is created and applied later in this tutorial, the name fields become `nvarchar(50)` as a result of the string length attributes.

In your SQLite tool, examine the column definitions for the `Student` table. The name fields have type `Text`. Notice that the first name field is called `FirstMidName`. In the next section, you change the name of that column to `FirstName`.

### The Column attribute

```
[Column("FirstName")]
public string FirstMidName { get; set; }
```

Attributes can control how classes and properties are mapped to the database. In the `Student` model, the `Column` attribute is used to map the name of the `FirstMidName` property to "FirstName" in the database.

When the database is created, property names on the model are used for column names (except when the `Column` attribute is used). The `Student` model uses `FirstMidName` for the first-name field because the field might also contain a middle name.

With the `[Column]` attribute, `Student.FirstMidName` in the data model maps to the `FirstName` column of the `Student` table. The addition of the `Column` attribute changes the model backing the `SchoolContext`. The model backing the `SchoolContext` no longer matches the database. That discrepancy will be resolved by adding a migration later in this tutorial.

### The Required attribute

```
[Required]
```

The `Required` attribute makes the name properties required fields. The `Required` attribute isn't needed for non-nullable types such as value types (for example, `DateTime`, `int`, and `double`). Types that can't be null are automatically treated as required fields.

The `Required` attribute must be used with `MinimumLength` for the `MinimumLength` to be enforced.

```
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
```

`MinimumLength` and `Required` allow whitespace to satisfy the validation. Use the `RegularExpression` attribute for full control over the string.

### The Display attribute

```
[Display(Name = "Last Name")]
```

The `Display` attribute specifies that the caption for the text boxes should be "First Name", "Last Name", "Full Name", and "Enrollment Date." The default captions had no space dividing the words, for example "Lastname."

### Create a migration

Run the app and go to the Students page. An exception is thrown. The `[Column]` attribute causes EF to expect to find a column named `FirstName`, but the column name in the database is still `FirstMidName`.

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

The error message is similar to the following example:

```
SqlException: Invalid column name 'FirstName'.
```

* In the PMC, enter the following commands to create a new migration and update the database:

  ```
  Add-Migration ColumnFirstName
  Update-Database
  ```

  The first of these commands generates the following warning message:

  ```
  An operation was scaffolded that may result in the loss of data.
  Please review the migration for accuracy.
  ```

  The warning is generated because the name fields are now limited to 50 characters. If a name in the database had more than 50 characters, the 51 to last character would be lost.

* Open the Student table in SSOX:

  Before the migration was applied, the name columns were of type [nvarchar(MAX)](/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql). The name columns are now `nvarchar(50)`. The column name has changed from `FirstMidName` to `FirstName`.

The error message is similar to the following example:

```
SqliteException: SQLite Error 1: 'no such column: s.FirstName'.
```

* Open a command window in the project folder. Enter the following commands to create a new migration and update the database:

  ```
  dotnet ef migrations add ColumnFirstName
  dotnet ef database update
  ```

  The database update command displays an error like the following example:

  ```
  SQLite does not support this migration operation ('AlterColumnOperation'). For more information, see http://go.microsoft.com/fwlink/?LinkId=723262.
  ```

For this tutorial, the way to get past this error is to delete and re-create the initial migration. For more information, see the SQLite warning note at the top of the [migrations tutorial](migrations?view=aspnetcore-10.0).

* Delete the *Migrations* folder.

* Run the following commands to drop the database, create a new initial migration, and apply the migration:

  ```
  dotnet ef database drop --force
  dotnet ef migrations add InitialCreate
  dotnet ef database update
  ```

* Examine the Student table in your SQLite tool. The column that was FirstMidName is now FirstName.

- Run the app and go to the Students page.
- Notice that times are not input or displayed along with dates.
- Select **Create New**, and try to enter a name longer than 50 characters.

Note

In the following sections, building the app at some stages generates compiler errors. The instructions specify when to build the app.

## The Instructor Entity

Create `Models/Instructor.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}
```

Multiple attributes can be on one line. The `HireDate` attributes could be written as follows:

```
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
```

### Navigation properties

The `CourseAssignments` and `OfficeAssignment` properties are navigation properties.

An instructor can teach any number of courses, so `CourseAssignments` is defined as a collection.

```
public ICollection<CourseAssignment> CourseAssignments { get; set; }
```

An instructor can have at most one office, so the `OfficeAssignment` property holds a single `OfficeAssignment` entity. `OfficeAssignment` is null if no office is assigned.

```
public OfficeAssignment OfficeAssignment { get; set; }
```

## The OfficeAssignment entity

Create `Models/OfficeAssignment.cs` with the following code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}
```

### The Key attribute

The `[Key]` attribute is used to identify a property as the primary key (PK) when the property name is something other than classnameID or ID.

There's a one-to-zero-or-one relationship between the `Instructor` and `OfficeAssignment` entities. An office assignment only exists in relation to the instructor it's assigned to. The `OfficeAssignment` PK is also its foreign key (FK) to the `Instructor` entity.

EF Core can't automatically recognize `InstructorID` as the PK of `OfficeAssignment` because `InstructorID` doesn't follow the ID or classnameID naming convention. Therefore, the `Key` attribute is used to identify `InstructorID` as the PK:

```
[Key]
public int InstructorID { get; set; }
```

By default, EF Core treats the key as non-database-generated because the column is for an identifying relationship.

### The Instructor navigation property

The `Instructor.OfficeAssignment` navigation property can be null because there might not be an `OfficeAssignment` row for a given instructor. An instructor might not have an office assignment.

The `OfficeAssignment.Instructor` navigation property will always have an instructor entity because the foreign key `InstructorID` type is `int`, a non-nullable value type. An office assignment can't exist without an instructor.

When an `Instructor` entity has a related `OfficeAssignment` entity, each entity has a reference to the other one in its navigation property.

## The Course Entity

Update `Models/Course.cs` with the following code:

```
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}
```

The `Course` entity has a foreign key (FK) property `DepartmentID`. `DepartmentID` points to the related `Department` entity. The `Course` entity has a `Department` navigation property.

EF Core doesn't require a foreign key property for a data model when the model has a navigation property for a related entity. EF Core automatically creates FKs in the database wherever they're needed. EF Core creates [shadow properties](/en-us/ef/core/modeling/shadow-properties) for automatically created FKs. However, explicitly including the FK in the data model can make updates simpler and more efficient. For example, consider a model where the FK property `DepartmentID` is *not* included. When a course entity is fetched to edit:

* The `Department` property is null if it's not explicitly loaded.
* To update the course entity, the `Department` entity must first be fetched.

When the FK property `DepartmentID` is included in the data model, there's no need to fetch the `Department` entity before an update.

### The DatabaseGenerated attribute

The `[DatabaseGenerated(DatabaseGeneratedOption.None)]` attribute specifies that the PK is provided by the application rather than generated by the database.

```
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
```

By default, EF Core assumes that PK values are generated by the database. Database-generated is generally the best approach. For `Course` entities, the user specifies the PK. For example, a course number such as a 1000 series for the math department, a 2000 series for the English department.

The `DatabaseGenerated` attribute can also be used to generate default values. For example, the database can automatically generate a date field to record the date a row was created or updated. For more information, see [Generated Properties](/en-us/ef/core/modeling/generated-properties).

### Foreign key and navigation properties

The foreign key (FK) properties and navigation properties in the `Course` entity reflect the following relationships:

A course is assigned to one department, so there's a `DepartmentID` FK and a `Department` navigation property.

```
public int DepartmentID { get; set; }
public Department Department { get; set; }
```

A course can have any number of students enrolled in it, so the `Enrollments` navigation property is a collection:

```
public ICollection<Enrollment> Enrollments { get; set; }
```

A course may be taught by multiple instructors, so the `CourseAssignments` navigation property is a collection:

```
public ICollection<CourseAssignment> CourseAssignments { get; set; }
```

`CourseAssignment` is explained [later](#many-to-many-relationships).

## The Department entity

Create `Models/Department.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}
```

### The Column attribute

Previously the `Column` attribute was used to change column name mapping. In the code for the `Department` entity, the `Column` attribute is used to change SQL data type mapping. The `Budget` column is defined using the SQL Server money type in the database:

```
[Column(TypeName="money")]
public decimal Budget { get; set; }
```

Column mapping is generally not required. EF Core chooses the appropriate SQL Server data type based on the CLR type for the property. The CLR `decimal` type maps to a SQL Server `decimal` type. `Budget` is for currency, and the money data type is more appropriate for currency.

### Foreign key and navigation properties

The FK and navigation properties reflect the following relationships:

* A department may or may not have an administrator.
* An administrator is always an instructor. Therefore the `InstructorID` property is included as the FK to the `Instructor` entity.

The navigation property is named `Administrator` but holds an `Instructor` entity:

```
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
```

The question mark (?) in the preceding code specifies the property is nullable.

A department may have many courses, so there's a Courses navigation property:

```
public ICollection<Course> Courses { get; set; }
```

By convention, EF Core enables cascade delete for non-nullable FKs and for many-to-many relationships. This default behavior can result in circular cascade delete rules. Circular cascade delete rules cause an exception when a migration is added.

For example, if the `Department.InstructorID` property was defined as non-nullable, EF Core would configure a cascade delete rule. In that case, the department would be deleted when the instructor assigned as its administrator is deleted. In this scenario, a restrict rule would make more sense. The following [fluent API](#fluent-api-alternative-to-attributes) would set a restrict rule and disable cascade delete.

```
modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)
```

## The Enrollment entity

An enrollment record is for one course taken by one student.

Update `Models/Enrollment.cs` with the following code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}
```

### Foreign key and navigation properties

The FK properties and navigation properties reflect the following relationships:

An enrollment record is for one course, so there's a `CourseID` FK property and a `Course` navigation property:

```
public int CourseID { get; set; }
public Course Course { get; set; }
```

An enrollment record is for one student, so there's a `StudentID` FK property and a `Student` navigation property:

```
public int StudentID { get; set; }
public Student Student { get; set; }
```

## Many-to-Many Relationships

There's a many-to-many relationship between the `Student` and `Course` entities. The `Enrollment` entity functions as a many-to-many join table *with payload* in the database. "With payload" means that the `Enrollment` table contains additional data besides FKs for the joined tables (in this case, the PK and `Grade`).

The following illustration shows what these relationships look like in an entity diagram. (This diagram was generated using [EF Power Tools](https://marketplace.visualstudio.com/items?itemName=ErikEJ.EntityFramework6PowerToolsCommunityEdition) for EF 6.x. Creating the diagram isn't part of the tutorial.)

Each relationship line has a 1 at one end and an asterisk (\*) at the other, indicating a one-to-many relationship.

If the `Enrollment` table didn't include grade information, it would only need to contain the two FKs (`CourseID` and `StudentID`). A many-to-many join table without payload is sometimes called a pure join table (PJT).

The `Instructor` and `Course` entities have a many-to-many relationship using a pure join table.

Note: EF 6.x supports implicit join tables for many-to-many relationships, but EF Core doesn't. For more information, see [Many-to-many relationships in EF Core 2.0](https://blog.oneunicorn.com/2017/09/25/many-to-many-relationships-in-ef-core-2-0-part-1-the-basics/).

## The CourseAssignment entity

Create `Models/CourseAssignment.cs` with the following code:

```
namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}
```

The Instructor-to-Courses many-to-many relationship requires a join table, and the entity for that join table is CourseAssignment.

It's common to name a join entity `EntityName1EntityName2`. For example, the Instructor-to-Courses join table using this pattern would be `CourseInstructor`. However, we recommend using a name that describes the relationship.

Data models start out simple and grow. Join tables without payload (PJTs) frequently evolve to include payload. By starting with a descriptive entity name, the name doesn't need to change when the join table changes. Ideally, the join entity would have its own natural (possibly single word) name in the business domain. For example, Books and Customers could be linked with a join entity called Ratings. For the Instructor-to-Courses many-to-many relationship, `CourseAssignment` is preferred over `CourseInstructor`.

### Composite key

The two FKs in `CourseAssignment` (`InstructorID` and `CourseID`) together uniquely identify each row of the `CourseAssignment` table. `CourseAssignment` doesn't require a dedicated PK. The `InstructorID` and `CourseID` properties function as a composite PK. The only way to specify composite PKs to EF Core is with the *fluent API*. The next section shows how to configure the composite PK.

The composite key ensures that:

* Multiple rows are allowed for one course.
* Multiple rows are allowed for one instructor.
* Multiple rows aren't allowed for the same instructor and course.

The `Enrollment` join entity defines its own PK, so duplicates of this sort are possible. To prevent such duplicates:

* Add a unique index on the FK fields, or
* Configure `Enrollment` with a primary composite key similar to `CourseAssignment`. For more information, see [Indexes](/en-us/ef/core/modeling/indexes).

## Update the database context

Update `Data/SchoolContext.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}
```

The preceding code adds the new entities and configures the `CourseAssignment` entity's composite PK.

## Fluent API alternative to attributes

The `OnModelCreating` method in the preceding code uses the *fluent API* to configure EF Core behavior. The API is called "fluent" because it's often used by stringing a series of method calls together into a single statement. The [following code](/en-us/ef/core/modeling/#use-fluent-api-to-configure-a-model) is an example of the fluent API:

```
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}
```

In this tutorial, the fluent API is used only for database mapping that can't be done with attributes. However, the fluent API can specify most of the formatting, validation, and mapping rules that can be done with attributes.

Some attributes such as `MinimumLength` can't be applied with the fluent API. `MinimumLength` doesn't change the schema, it only applies a minimum length validation rule.

Some developers prefer to use the fluent API exclusively so that they can keep their entity classes "clean." Attributes and the fluent API can be mixed. There are some configurations that can only be done with the fluent API (specifying a composite PK). There are some configurations that can only be done with attributes (`MinimumLength`). The recommended practice for using fluent API or attributes:

* Choose one of these two approaches.
* Use the chosen approach consistently as much as possible.

Some of the attributes used in this tutorial are used for:

* Validation only (for example, `MinimumLength`).
* EF Core configuration only (for example, `HasKey`).
* Validation and EF Core configuration (for example, `[StringLength(50)]`).

For more information about attributes vs. fluent API, see [Methods of configuration](/en-us/ef/core/modeling/).

## Entity diagram

The following illustration shows the diagram that EF Power Tools create for the completed School model.

The preceding diagram shows:

* Several one-to-many relationship lines (1 to \*).
* The one-to-zero-or-one relationship line (1 to 0..1) between the `Instructor` and `OfficeAssignment` entities.
* The zero-or-one-to-many relationship line (0..1 to \*) between the `Instructor` and `Department` entities.

## Seed the database

Update the code in `Data/DbInitializer.cs`:

```
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2016-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2017-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2011-09-01") }
            };

            context.Students.AddRange(students);
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            context.Instructors.AddRange(instructors);
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            context.Departments.AddRange(departments);
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            context.Courses.AddRange(courses);
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            context.OfficeAssignments.AddRange(officeAssignments);
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            context.CourseAssignments.AddRange(courseInstructors);
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}
```

The preceding code provides seed data for the new entities. Most of this code creates new entity objects and loads sample data. The sample data is used for testing. See `Enrollments` and `CourseAssignments` for examples of how many-to-many join tables can be seeded.

## Add a migration

Build the project.

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

In PMC, run the following command.

```
Add-Migration ComplexDataModel
```

The preceding command displays a warning about possible data loss.

```
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
To undo this action, use 'ef migrations remove'
```

If the `database update` command is run, the following error is produced:

```
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
```

In the next section, you see what to do about this error.

If you add a migration and run the `database update` command, the following error would be produced:

```
SQLite does not support this migration operation ('DropForeignKeyOperation').
For more information, see http://go.microsoft.com/fwlink/?LinkId=723262.
```

In the next section, you see how to avoid this error.

## Apply the migration or drop and re-create

Now that you have an existing database, you need to think about how to apply changes to it. This tutorial shows two alternatives:

* [Drop and re-create the database](#drop). Choose this section if you're using SQLite.
* [Apply the migration to the existing database](#applyexisting). The instructions in this section work for SQL Server only, **not for SQLite**.

Either choice works for SQL Server. While the apply-migration method is more complex and time-consuming, it's the preferred approach for real-world, production environments.

[]()

## Drop and re-create the database

[Skip this section](#apply-the-migration) if you're using SQL Server and want to do the apply-migration approach in the following section.

To force EF Core to create a new database, drop and update the database:

* [Visual Studio](#tabpanel_8_visual-studio)
* [Visual Studio Code](#tabpanel_8_visual-studio-code)

- In the **Package Manager Console** (PMC), run the following command:

  ```
  Drop-Database
  ```

- Delete the *Migrations* folder, then run the following command:

  ```
  Add-Migration InitialCreate
  Update-Database
  ```

* Open a command window and navigate to the project folder. The project folder contains the `ContosoUniversity.csproj` file.

* Run the following command:

  ```
  dotnet ef database drop --force
  ```

* Delete the *Migrations* folder, then run the following command:

  ```
  dotnet ef migrations add InitialCreate
  dotnet ef database update
  ```

Run the app. Running the app runs the `DbInitializer.Initialize` method. The `DbInitializer.Initialize` populates the new database.

* [Visual Studio](#tabpanel_9_visual-studio)
* [Visual Studio Code](#tabpanel_9_visual-studio-code)

Open the database in SSOX:

* If SSOX was opened previously, click the **Refresh** button.

* Expand the **Tables** node. The created tables are displayed.

* Examine the **CourseAssignment** table:

  * Right-click the **CourseAssignment** table and select **View Data**.
  * Verify the **CourseAssignment** table contains data.

Use your SQLite tool to examine the database:

* New tables and columns.
* Seeded data in tables, for example the **CourseAssignment** table.

[]()

## Apply the migration

This section is optional. These steps work only for SQL Server LocalDB and only if you skipped the preceding [Drop and re-create the database](#drop) section.

When migrations are run with existing data, there may be FK constraints that are not satisfied with the existing data. With production data, steps must be taken to migrate the existing data. This section provides an example of fixing FK constraint violations. Don't make these code changes without a backup. Don't make these code changes if you completed the preceding [Drop and re-create the database](#drop) section.

The `{timestamp}_ComplexDataModel.cs` file contains the following code:

```
migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);
```

The preceding code adds a non-nullable `DepartmentID` FK to the `Course` table. The database from the previous tutorial contains rows in `Course`, so that table cannot be updated by migrations.

To make the `ComplexDataModel` migration work with existing data:

* Change the code to give the new column (`DepartmentID`) a default value.
* Create a fake department named "Temp" to act as the default department.

#### Fix the foreign key constraints

In the `ComplexDataModel` migration class, update the `Up` method:

* Open the `{timestamp}_ComplexDataModel.cs` file.
* Comment out the line of code that adds the `DepartmentID` column to the `Course` table.

```
migrationBuilder.AlterColumn<string>(
    name: "Title",
    table: "Course",
    maxLength: 50,
    nullable: true,
    oldClrType: typeof(string),
    oldNullable: true);
            
//migrationBuilder.AddColumn<int>(
//    name: "DepartmentID",
//    table: "Course",
//    nullable: false,
//    defaultValue: 0);
```

Add the following highlighted code. The new code goes after the `.CreateTable( name: "Department"` block:

```
migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);
```

With the preceding changes, existing `Course` rows will be related to the "Temp" department after the `ComplexDataModel.Up` method runs.

The way of handling the situation shown here is simplified for this tutorial. A production app would:

* Include code or scripts to add `Department` rows and related `Course` rows to the new `Department` rows.
* Not use the "Temp" department or the default value for `Course.DepartmentID`.

- [Visual Studio](#tabpanel_10_visual-studio)
- [Visual Studio Code](#tabpanel_10_visual-studio-code)

* In the **Package Manager Console** (PMC), run the following command:

  ```
  Update-Database
  ```

Because the `DbInitializer.Initialize` method is designed to work only with an empty database, use SSOX to delete all the rows in the Student and Course tables. (Cascade delete will take care of the Enrollment table.)

* If you're using SQL Server LocalDB with Visual Studio Code, run the following command:

  ```
  dotnet ef database update
  ```

Run the app. Running the app runs the `DbInitializer.Initialize` method. The `DbInitializer.Initialize` populates the new database.

## Next steps

The next two tutorials show how to read and update related data.

[Previous tutorial](migrations?view=aspnetcore-10.0) [Next tutorial](read-related-data?view=aspnetcore-10.0)

The previous tutorials worked with a basic data model that was composed of three entities. In this tutorial:

* More entities and relationships are added.
* The data model is customized by specifying formatting, validation, and database mapping rules.

The entity classes for the completed data model are shown in the following illustration:

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples).

## Customize the data model with attributes

In this section, the data model is customized using attributes.

### The DataType attribute

The student pages currently displays the time of the enrollment date. Typically, date fields show only the date and not the time.

Update `Models/Student.cs` with the following highlighted code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies a data type that's more specific than the database intrinsic type. In this case only the date should be displayed, not the date and time. The [DataType Enumeration](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress, etc. The `DataType` attribute can also enable the app to automatically provide type-specific features. For example:

* The `mailto:` link is automatically created for `DataType.EmailAddress`.
* The date selector is provided for `DataType.Date` in most browsers.

The `DataType` attribute emits HTML 5 `data-` (pronounced data dash) attributes that HTML 5 browsers consume. The `DataType` attributes don't provide validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the date field is displayed according to the default formats based on the server's [CultureInfo](../../fundamentals/localization?view=aspnetcore-10.0#provide-localized-resources-for-the-languages-and-cultures-you-support).

The `DisplayFormat` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
```

The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied to the edit UI. Some fields shouldn't use `ApplyFormatInEditMode`. For example, the currency symbol should generally not be displayed in an edit text box.

The `DisplayFormat` attribute can be used by itself. It's generally a good idea to use the `DataType` attribute with the `DisplayFormat` attribute. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen. The `DataType` attribute provides the following benefits that are not available in `DisplayFormat`:

* The browser can enable HTML5 features. For example, show a calendar control, the locale-appropriate currency symbol, email links, client-side input validation, etc.
* By default, the browser renders data using the correct format based on the locale.

For more information, see the [\<input> Tag Helper documentation](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper).

Run the app. Navigate to the Students Index page. Times are no longer displayed. Every view that uses the `Student` model displays the date without time.

### The StringLength attribute

Data validation rules and validation error messages can be specified with attributes. The [StringLength](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute) attribute specifies the minimum and maximum length of characters that are allowed in a data field. The `StringLength` attribute also provides client-side and server-side validation. The minimum value has no impact on the database schema.

Update the `Student` model with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The preceding code limits names to no more than 50 characters. The `StringLength` attribute doesn't prevent a user from entering white space for a name. The [RegularExpression](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute) attribute is used to apply restrictions to the input. For example, the following code requires the first character to be upper case and the remaining characters to be alphabetical:

```
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
```

Run the app:

* Navigate to the Students page.
* Select **Create New**, and enter a name longer than 50 characters.
* Select **Create**, client-side validation shows an error message.

In **SQL Server Object Explorer** (SSOX), open the Student table designer by double-clicking the **Student** table.

The preceding image shows the schema for the `Student` table. The name fields have type `nvarchar(MAX)` because migrations has not been run on the DB. When migrations are run later in this tutorial, the name fields become `nvarchar(50)`.

### The Column attribute

Attributes can control how classes and properties are mapped to the database. In this section, the `Column` attribute is used to map the name of the `FirstMidName` property to "FirstName" in the DB.

When the DB is created, property names on the model are used for column names (except when the `Column` attribute is used).

The `Student` model uses `FirstMidName` for the first-name field because the field might also contain a middle name.

Update the `Student.cs` file with the following highlighted code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

With the preceding change, `Student.FirstMidName` in the app maps to the `FirstName` column of the `Student` table.

The addition of the `Column` attribute changes the model backing the `SchoolContext`. The model backing the `SchoolContext` no longer matches the database. If the app is run before applying migrations, the following exception is generated:

```
SqlException: Invalid column name 'FirstName'.
```

To update the DB:

* Build the project.
* Open a command window in the project folder. Enter the following commands to create a new migration and update the DB:

- [Visual Studio](#tabpanel_11_visual-studio)
- [Visual Studio Code](#tabpanel_11_visual-studio-code)

```
Add-Migration ColumnFirstName
Update-Database
```

```
dotnet ef migrations add ColumnFirstName
dotnet ef database update
```

The `migrations add ColumnFirstName` command generates the following warning message:

```
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
```

The warning is generated because the name fields are now limited to 50 characters. If a name in the DB had more than 50 characters, the 51 to last character would be lost.

* Test the app.

Open the Student table in SSOX:

Before migration was applied, the name columns were of type [nvarchar(MAX)](/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql). The name columns are now `nvarchar(50)`. The column name has changed from `FirstMidName` to `FirstName`.

Note

In the following section, building the app at some stages generates compiler errors. The instructions specify when to build the app.

## Student entity update

Update `Models/Student.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

### The Required attribute

The `Required` attribute makes the name properties required fields. The `Required` attribute isn't needed for non-nullable types such as value types (`DateTime`, `int`, `double`, etc.). Types that can't be null are automatically treated as required fields.

The `Required` attribute could be replaced with a minimum length parameter in the `StringLength` attribute:

```
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
```

### The Display attribute

The `Display` attribute specifies that the caption for the text boxes should be "First Name", "Last Name", "Full Name", and "Enrollment Date." The default captions had no space dividing the words, for example "Lastname."

### The FullName calculated property

`FullName` is a calculated property that returns a value that's created by concatenating two other properties. `FullName` cannot be set, it has only a get accessor. No `FullName` column is created in the database.

## Create the Instructor Entity

Create `Models/Instructor.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}
```

Multiple attributes can be on one line. The `HireDate` attributes could be written as follows:

```
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
```

### The CourseAssignments and OfficeAssignment navigation properties

The `CourseAssignments` and `OfficeAssignment` properties are navigation properties.

An instructor can teach any number of courses, so `CourseAssignments` is defined as a collection.

```
public ICollection<CourseAssignment> CourseAssignments { get; set; }
```

If a navigation property holds multiple entities:

* It must be a list type where the entries can be added, deleted, and updated.

Navigation property types include:

* `ICollection<T>`
* `List<T>`
* `HashSet<T>`

If `ICollection<T>` is specified, EF Core creates a `HashSet<T>` collection by default.

The `CourseAssignment` entity is explained in the section on many-to-many relationships.

Contoso University business rules state that an instructor can have at most one office. The `OfficeAssignment` property holds a single `OfficeAssignment` entity. `OfficeAssignment` is null if no office is assigned.

```
public OfficeAssignment OfficeAssignment { get; set; }
```

## Create the OfficeAssignment entity

Create `Models/OfficeAssignment.cs` with the following code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}
```

### The Key attribute

The `[Key]` attribute is used to identify a property as the primary key (PK) when the property name is something other than classnameID or ID.

There's a one-to-zero-or-one relationship between the `Instructor` and `OfficeAssignment` entities. An office assignment only exists in relation to the instructor it's assigned to. The `OfficeAssignment` PK is also its foreign key (FK) to the `Instructor` entity. EF Core can't automatically recognize `InstructorID` as the PK of `OfficeAssignment` because:

* `InstructorID` doesn't follow the ID or classnameID naming convention.

Therefore, the `Key` attribute is used to identify `InstructorID` as the PK:

```
[Key]
public int InstructorID { get; set; }
```

By default, EF Core treats the key as non-database-generated because the column is for an identifying relationship.

### The Instructor navigation property

The `OfficeAssignment` navigation property for the `Instructor` entity is nullable because:

* Reference types (such as classes are nullable).
* An instructor might not have an office assignment.

The `OfficeAssignment` entity has a non-nullable `Instructor` navigation property because:

* `InstructorID` is non-nullable.
* An office assignment can't exist without an instructor.

When an `Instructor` entity has a related `OfficeAssignment` entity, each entity has a reference to the other one in its navigation property.

The `[Required]` attribute could be applied to the `Instructor` navigation property:

```
[Required]
public Instructor Instructor { get; set; }
```

The preceding code specifies that there must be a related instructor. The preceding code is unnecessary because the `InstructorID` foreign key (which is also the PK) is non-nullable.

## Modify the Course Entity

Update `Models/Course.cs` with the following code:

```
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}
```

The `Course` entity has a foreign key (FK) property `DepartmentID`. `DepartmentID` points to the related `Department` entity. The `Course` entity has a `Department` navigation property.

EF Core doesn't require a FK property for a data model when the model has a navigation property for a related entity.

EF Core automatically creates FKs in the database wherever they're needed. EF Core creates [shadow properties](/en-us/ef/core/modeling/shadow-properties) for automatically created FKs. Having the FK in the data model can make updates simpler and more efficient. For example, consider a model where the FK property `DepartmentID` is *not* included. When a course entity is fetched to edit:

* The `Department` entity is null if it's not explicitly loaded.
* To update the course entity, the `Department` entity must first be fetched.

When the FK property `DepartmentID` is included in the data model, there's no need to fetch the `Department` entity before an update.

### The DatabaseGenerated attribute

The `[DatabaseGenerated(DatabaseGeneratedOption.None)]` attribute specifies that the PK is provided by the application rather than generated by the database.

```
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
```

By default, EF Core assumes that PK values are generated by the DB. DB generated PK values is generally the best approach. For `Course` entities, the user specifies the PK. For example, a course number such as a 1000 series for the math department, a 2000 series for the English department.

The `DatabaseGenerated` attribute can also be used to generate default values. For example, the DB can automatically generate a date field to record the date a row was created or updated. For more information, see [Generated Properties](/en-us/ef/core/modeling/generated-properties).

### Foreign key and navigation properties

The foreign key (FK) properties and navigation properties in the `Course` entity reflect the following relationships:

A course is assigned to one department, so there's a `DepartmentID` FK and a `Department` navigation property.

```
public int DepartmentID { get; set; }
public Department Department { get; set; }
```

A course can have any number of students enrolled in it, so the `Enrollments` navigation property is a collection:

```
public ICollection<Enrollment> Enrollments { get; set; }
```

A course may be taught by multiple instructors, so the `CourseAssignments` navigation property is a collection:

```
public ICollection<CourseAssignment> CourseAssignments { get; set; }
```

`CourseAssignment` is explained [later](#many-to-many-relationships).

## Create the Department entity

Create `Models/Department.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}
```

### The Column attribute

Previously the `Column` attribute was used to change column name mapping. In the code for the `Department` entity, the `Column` attribute is used to change SQL data type mapping. The `Budget` column is defined using the SQL Server money type in the DB:

```
[Column(TypeName="money")]
public decimal Budget { get; set; }
```

Column mapping is generally not required. EF Core generally chooses the appropriate SQL Server data type based on the CLR type for the property. The CLR `decimal` type maps to a SQL Server `decimal` type. `Budget` is for currency, and the money data type is more appropriate for currency.

### Foreign key and navigation properties

The FK and navigation properties reflect the following relationships:

* A department may or may not have an administrator.
* An administrator is always an instructor. Therefore the `InstructorID` property is included as the FK to the `Instructor` entity.

The navigation property is named `Administrator` but holds an `Instructor` entity:

```
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
```

The question mark (?) in the preceding code specifies the property is nullable.

A department may have many courses, so there's a Courses navigation property:

```
public ICollection<Course> Courses { get; set; }
```

Note: By convention, EF Core enables cascade delete for non-nullable FKs and for many-to-many relationships. Cascading delete can result in circular cascade delete rules. Circular cascade delete rules causes an exception when a migration is added.

For example, if the `Department.InstructorID` property was defined as non-nullable:

* EF Core configures a cascade delete rule to delete the department when the instructor is deleted.

* Deleting the department when the instructor is deleted isn't the intended behavior.

* The following [fluent API](#fluent-api-alternative-to-attributes) would set a restrict rule instead of cascade.

  ```
  modelBuilder.Entity<Department>()
      .HasOne(d => d.Administrator)
      .WithMany()
      .OnDelete(DeleteBehavior.Restrict)
  ```

The preceding code disables cascade delete on the department-instructor relationship.

## Update the Enrollment entity

An enrollment record is for one course taken by one student.

Update `Models/Enrollment.cs` with the following code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}
```

### Foreign key and navigation properties

The FK properties and navigation properties reflect the following relationships:

An enrollment record is for one course, so there's a `CourseID` FK property and a `Course` navigation property:

```
public int CourseID { get; set; }
public Course Course { get; set; }
```

An enrollment record is for one student, so there's a `StudentID` FK property and a `Student` navigation property:

```
public int StudentID { get; set; }
public Student Student { get; set; }
```

## Many-to-Many Relationships

There's a many-to-many relationship between the `Student` and `Course` entities. The `Enrollment` entity functions as a many-to-many join table *with payload* in the database. "With payload" means that the `Enrollment` table contains additional data besides FKs for the joined tables (in this case, the PK and `Grade`).

The following illustration shows what these relationships look like in an entity diagram. (This diagram was generated using [EF Power Tools](https://marketplace.visualstudio.com/items?itemName=ErikEJ.EntityFramework6PowerToolsCommunityEdition) for EF 6.x. Creating the diagram isn't part of the tutorial.)

Each relationship line has a 1 at one end and an asterisk (\*) at the other, indicating a one-to-many relationship.

If the `Enrollment` table didn't include grade information, it would only need to contain the two FKs (`CourseID` and `StudentID`). A many-to-many join table without payload is sometimes called a pure join table (PJT).

The `Instructor` and `Course` entities have a many-to-many relationship using a pure join table.

Note: EF 6.x supports implicit join tables for many-to-many relationships, but EF Core doesn't. For more information, see [Many-to-many relationships in EF Core 2.0](https://blog.oneunicorn.com/2017/09/25/many-to-many-relationships-in-ef-core-2-0-part-1-the-basics/).

## The CourseAssignment entity

Create `Models/CourseAssignment.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}
```

### Instructor-to-Courses

The Instructor-to-Courses many-to-many relationship:

* Requires a join table that must be represented by an entity set.
* Is a pure join table (table without payload).

It's common to name a join entity `EntityName1EntityName2`. For example, the Instructor-to-Courses join table using this pattern is `CourseInstructor`. However, we recommend using a name that describes the relationship.

Data models start out simple and grow. No-payload joins (PJTs) frequently evolve to include payload. By starting with a descriptive entity name, the name doesn't need to change when the join table changes. Ideally, the join entity would have its own natural (possibly single word) name in the business domain. For example, Books and Customers could be linked with a join entity called Ratings. For the Instructor-to-Courses many-to-many relationship, `CourseAssignment` is preferred over `CourseInstructor`.

### Composite key

FKs are not nullable. The two FKs in `CourseAssignment` (`InstructorID` and `CourseID`) together uniquely identify each row of the `CourseAssignment` table. `CourseAssignment` doesn't require a dedicated PK. The `InstructorID` and `CourseID` properties function as a composite PK. The only way to specify composite PKs to EF Core is with the *fluent API*. The next section shows how to configure the composite PK.

The composite key ensures:

* Multiple rows are allowed for one course.
* Multiple rows are allowed for one instructor.
* Multiple rows for the same instructor and course isn't allowed.

The `Enrollment` join entity defines its own PK, so duplicates of this sort are possible. To prevent such duplicates:

* Add a unique index on the FK fields, or
* Configure `Enrollment` with a primary composite key similar to `CourseAssignment`. For more information, see [Indexes](/en-us/ef/core/modeling/indexes).

## Update the DB context

Add the following highlighted code to `Data/SchoolContext.cs`:

```
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Models
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollment { get; set; }
        public DbSet<Student> Student { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}
```

The preceding code adds the new entities and configures the `CourseAssignment` entity's composite PK.

## Fluent API alternative to attributes

The `OnModelCreating` method in the preceding code uses the *fluent API* to configure EF Core behavior. The API is called "fluent" because it's often used by stringing a series of method calls together into a single statement. The [following code](/en-us/ef/core/modeling/#use-fluent-api-to-configure-a-model) is an example of the fluent API:

```
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}
```

In this tutorial, the fluent API is used only for DB mapping that can't be done with attributes. However, the fluent API can specify most of the formatting, validation, and mapping rules that can be done with attributes.

Some attributes such as `MinimumLength` can't be applied with the fluent API. `MinimumLength` doesn't change the schema, it only applies a minimum length validation rule.

Some developers prefer to use the fluent API exclusively so that they can keep their entity classes "clean." Attributes and the fluent API can be mixed. There are some configurations that can only be done with the fluent API (specifying a composite PK). There are some configurations that can only be done with attributes (`MinimumLength`). The recommended practice for using fluent API or attributes:

* Choose one of these two approaches.
* Use the chosen approach consistently as much as possible.

Some of the attributes used in the this tutorial are used for:

* Validation only (for example, `MinimumLength`).
* EF Core configuration only (for example, `HasKey`).
* Validation and EF Core configuration (for example, `[StringLength(50)]`).

For more information about attributes vs. fluent API, see [Methods of configuration](/en-us/ef/core/modeling/).

## Entity Diagram Showing Relationships

The following illustration shows the diagram that EF Power Tools create for the completed School model.

The preceding diagram shows:

* Several one-to-many relationship lines (1 to \*).
* The one-to-zero-or-one relationship line (1 to 0..1) between the `Instructor` and `OfficeAssignment` entities.
* The zero-or-one-to-many relationship line (0..1 to \*) between the `Instructor` and `Department` entities.

## Seed the DB with Test Data

Update the code in `Data/DbInitializer.cs`:

```
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Student.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            foreach (Student s in students)
            {
                context.Student.Add(s);
            }
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            foreach (Instructor i in instructors)
            {
                context.Instructors.Add(i);
            }
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            foreach (Department d in departments)
            {
                context.Departments.Add(d);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            foreach (OfficeAssignment o in officeAssignments)
            {
                context.OfficeAssignments.Add(o);
            }
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            foreach (CourseAssignment ci in courseInstructors)
            {
                context.CourseAssignments.Add(ci);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollment.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollment.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}
```

The preceding code provides seed data for the new entities. Most of this code creates new entity objects and loads sample data. The sample data is used for testing. See `Enrollments` and `CourseAssignments` for examples of how many-to-many join tables can be seeded.

## Add a migration

Build the project.

* [Visual Studio](#tabpanel_12_visual-studio)
* [Visual Studio Code](#tabpanel_12_visual-studio-code)

```
Add-Migration ComplexDataModel
```

```
dotnet ef migrations add ComplexDataModel
```

The preceding command displays a warning about possible data loss.

```
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'
```

If the `database update` command is run, the following error is produced:

```
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
```

## Apply the migration

Now that you have an existing database, you need to think about how to apply future changes to it. This tutorial shows two approaches:

* [Drop and re-create the database](#drop)
* [Apply the migration to the existing database](#applyexisting). While this method is more complex and time-consuming, it's the preferred approach for real-world, production environments. **Note**: This is an optional section of the tutorial. You can do the drop and re-create steps and skip this section. If you do want to follow the steps in this section, don't do the drop and re-create steps.

[]()

### Drop and re-create the database

The code in the updated `DbInitializer` adds seed data for the new entities. To force EF Core to create a new DB, drop and update the DB:

* [Visual Studio](#tabpanel_13_visual-studio)
* [Visual Studio Code](#tabpanel_13_visual-studio-code)

In the **Package Manager Console** (PMC), run the following command:

```
Drop-Database
Update-Database
```

Run `Get-Help about_EntityFrameworkCore` from the PMC to get help information.

Open a command window and navigate to the project folder. The project folder contains the `Startup.cs` file.

Enter the following in the command window:

```
dotnet ef database drop
dotnet ef database update
```

Run the app. Running the app runs the `DbInitializer.Initialize` method. The `DbInitializer.Initialize` populates the new DB.

Open the DB in SSOX:

* If SSOX was opened previously, click the **Refresh** button.
* Expand the **Tables** node. The created tables are displayed.

Examine the **CourseAssignment** table:

* Right-click the **CourseAssignment** table and select **View Data**.
* Verify the **CourseAssignment** table contains data.

[]()

### Apply the migration to the existing database

This section is optional. These steps work only if you skipped the preceding [Drop and re-create the database](#drop) section.

When migrations are run with existing data, there may be FK constraints that are not satisfied with the existing data. With production data, steps must be taken to migrate the existing data. This section provides an example of fixing FK constraint violations. Don't make these code changes without a backup. Don't make these code changes if you completed the previous section and updated the database.

The `{timestamp}_ComplexDataModel.cs` file contains the following code:

```
migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);
```

The preceding code adds a non-nullable `DepartmentID` FK to the `Course` table. The DB from the previous tutorial contains rows in `Course`, so that table cannot be updated by migrations.

To make the `ComplexDataModel` migration work with existing data:

* Change the code to give the new column (`DepartmentID`) a default value.
* Create a fake department named "Temp" to act as the default department.

#### Fix the foreign key constraints

Update the `ComplexDataModel` classes `Up` method:

* Open the `{timestamp}_ComplexDataModel.cs` file.
* Comment out the line of code that adds the `DepartmentID` column to the `Course` table.

```
migrationBuilder.AlterColumn<string>(
    name: "Title",
    table: "Course",
    maxLength: 50,
    nullable: true,
    oldClrType: typeof(string),
    oldNullable: true);
            
//migrationBuilder.AddColumn<int>(
//    name: "DepartmentID",
//    table: "Course",
//    nullable: false,
//    defaultValue: 0);
```

Add the following highlighted code. The new code goes after the `.CreateTable( name: "Department"` block:

```
migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);
```

With the preceding changes, existing `Course` rows will be related to the "Temp" department after the `ComplexDataModel` `Up` method runs.

A production app would:

* Include code or scripts to add `Department` rows and related `Course` rows to the new `Department` rows.
* Not use the "Temp" department or the default value for `Course.DepartmentID`.

The next tutorial covers related data.

## Related content

* [YouTube version of this tutorial(Part 1)](https://www.youtube.com/watch?v=0n2f0ObgCoA)
* [YouTube version of this tutorial(Part 2)](https://www.youtube.com/watch?v=Je0Z5K1TNmY)

[Previous](migrations?view=aspnetcore-10.0) [Next](read-related-data?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/diagnostics
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel/diagnostics.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Diagnostics in Kestrel

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Sourabh Shirhatti](https://twitter.com/sshirhatti)

This article provides guidance for gathering diagnostics from Kestrel to help troubleshoot issues. Topics covered include:

* **Logging**: Structured logs written to [.NET logging](../../logging/?view=aspnetcore-10.0). [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) is used by app frameworks to write logs, and by users for their own logging in an app.
* **Metrics**: Representation of data measures over intervals of time, for example, requests per second. Metrics are emitted using `EventCounter` and can be observed using the [dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters) command line tool or with [Application Insights](/en-us/azure/azure-monitor/app/eventcounters).
* **DiagnosticSource**: `DiagnosticSource` is a mechanism for production-time logging with rich data payloads for consumption within the process. Unlike logging, which assumes data will leave the process and expects serializable data, `DiagnosticSource` works well with complex data.

## Logging

Like most components in ASP.NET Core, Kestrel uses `Microsoft.Extensions.Logging` to emit log information. Kestrel employs the use of multiple [categories](../../logging/?view=aspnetcore-10.0#log-category) which allows you to be selective on which logs you listen to.

| Logging Category Name                             | Logging Events                                                                                                                                                                                                                                                              |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Microsoft.AspNetCore.Server.Kestrel`             | `ApplicationError`, `ConnectionHeadResponseBodyWrite`, `ApplicationNeverCompleted`, `RequestBodyStart`, `RequestBodyDone`, `RequestBodyNotEntirelyRead`, `RequestBodyDrainTimedOut`, `ResponseMinimumDataRateNotSatisfied`, `InvalidResponseHeaderRemoved`, `HeartbeatSlow` |
| `Microsoft.AspNetCore.Server.Kestrel.BadRequests` | `ConnectionBadRequest`, `RequestProcessingError`, `RequestBodyMinimumDataRateNotSatisfied`                                                                                                                                                                                  |
| `Microsoft.AspNetCore.Server.Kestrel.Connections` | `ConnectionAccepted`, `ConnectionStart`, `ConnectionStop`, `ConnectionPause`, `ConnectionResume`, `ConnectionKeepAlive`, `ConnectionRejected`, `ConnectionDisconnect`, `NotAllConnectionsClosedGracefully`, `NotAllConnectionsAborted`, `ApplicationAbortedConnection`      |
| `Microsoft.AspNetCore.Server.Kestrel.Http2`       | `Http2ConnectionError`, `Http2ConnectionClosing`, `Http2ConnectionClosed`, `Http2StreamError`, `Http2StreamResetAbort`, `HPackDecodingError`, `HPackEncodingError`, `Http2FrameReceived`, `Http2FrameSending`, `Http2MaxConcurrentStreamsReached`                           |
| `Microsoft.AspNetCore.Server.Kestrel.Http3`       | `Http3ConnectionError`, `Http3ConnectionClosing`, `Http3ConnectionClosed`, `Http3StreamAbort`, `Http3FrameReceived`, `Http3FrameSending`                                                                                                                                    |

### Connection logging

Kestrel also supports the ability to emit `Debug` level logs for byte-level communication and can be enabled on a per-endpoint basis. To enable connection logging, see [configure endpoints for Kestrel](endpoints?view=aspnetcore-10.0)

## Metrics

Metrics is a representation of data measures over intervals of time, for example, requests per second. Metrics data allows observation of the state of an app at a high-level. Kestrel metrics are emitted using `EventCounter`.

Note

The `connections-per-second` and `tls-handshakes-per-second` counters are named incorrectly. The counters:

* Do ***not*** always contain the number of new connections or TLS handshakes per second
* Display the number of new connection or TLS handshakes in the last update interval as requested as the consumer of Events via the `EventCounterIntervalSec` argument in the `filterPayload` to `KestrelEventSource`.

We **recommend** consumers of these counters scale the metric value based on the `DisplayRateTimeScale` of one second.

| Name                        | Display Name                           | Description                                                                                                                                                                                                       |
| --------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `connections-per-second`    | Connection Rate                        | The number of new incoming connections per update interval                                                                                                                                                        |
| `total-connections`         | Total Connections                      | The total number of connections                                                                                                                                                                                   |
| `tls-handshakes-per-second` | TLS Handshake Rate                     | The number of new TLS handshakes per update interval                                                                                                                                                              |
| `total-tls-handshakes`      | Total TLS Handshakes                   | The total number of TLS handshakes                                                                                                                                                                                |
| `current-tls-handshakes`    | Current TLS Handshakes                 | The number of TLS handshakes in process                                                                                                                                                                           |
| `failed-tls-handshakes`     | Failed TLS Handshakes                  | The total number of failed TLS handshakes                                                                                                                                                                         |
| `current-connections`       | Current Connections                    | The total number of connections, including idle connections                                                                                                                                                       |
| `connection-queue-length`   | Connection Queue Length                | The total number connections queued to the thread pool. In a healthy system at steady state, this number should always be close to zero                                                                           |
| `request-queue-length`      | Request Queue Length                   | The total number requests queued to the thread pool. In a healthy system at steady state, this number should always be close to zero. This metric is unlike the IIS/Http.Sys request queue and cannot be compared |
| `current-upgraded-requests` | Current Upgraded Requests (WebSockets) | The number of active WebSocket requests                                                                                                                                                                           |

## DiagnosticSource

Kestrel emits a `DiagnosticSource` event for HTTP requests rejected at server layer such as malformed requests and protocols violations. As such, these requests never make it into the hosting layer of ASP.NET Core.

Kestrel emits these events with the `Microsoft.AspNetCore.Server.Kestrel.BadRequest` event name and an `IFeatureCollection` as the object payload. The underlying exception can be retrieved by accessing the `IBadRequestExceptionFeature` on the feature collection.

Resolving these events is a two-step process. An observer for `DiagnosticListener` must be created:

```
class BadRequestEventListener : IObserver<KeyValuePair<string, object>>, IDisposable
{
    private readonly IDisposable _subscription;
    private readonly Action<IBadRequestExceptionFeature> _callback;

    public BadRequestEventListener(DiagnosticListener diagnosticListener, Action<IBadRequestExceptionFeature> callback)
    {
        _subscription = diagnosticListener.Subscribe(this!, IsEnabled);
        _callback = callback;
    }
    private static readonly Predicate<string> IsEnabled = (provider) => provider switch
    {
        "Microsoft.AspNetCore.Server.Kestrel.BadRequest" => true,
        _ => false
    };
    public void OnNext(KeyValuePair<string, object> pair)
    {
        if (pair.Value is IFeatureCollection featureCollection)
        {
            var badRequestFeature = featureCollection.Get<IBadRequestExceptionFeature>();

            if (badRequestFeature is not null)
            {
                _callback(badRequestFeature);
            }
        }
    }
    public void OnError(Exception error) { }
    public void OnCompleted() { }
    public virtual void Dispose() => _subscription.Dispose();
}
```

Subscribe to the ASP.NET Core `DiagnosticListener` with the observer. In this example, we create a callback that logs the underlying exception.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var diagnosticSource = app.Services.GetRequiredService<DiagnosticListener>();
using var badRequestListener = new BadRequestEventListener(diagnosticSource, (badRequestExceptionFeature) =>
{
    app.Logger.LogError(badRequestExceptionFeature.Error, "Bad request received");
});
app.MapGet("/", () => "Hello world");
app.Run();
```

## Behavior with debugger attached

Certain timeouts and rate limits aren't enforced when a debugger is attached to a Kestrel process. For more information, see [Behavior with debugger attached](options?view=aspnetcore-10.0#behavior-with-debugger-attached).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/1x-to-2x/identity-2x.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate authentication and Identity to ASP.NET Core 2.0

By [Scott Addie](https://github.com/scottaddie) and [Hao Kung](https://github.com/HaoK)

ASP.NET Core 2.0 has a new model for authentication and [Identity](../../security/authentication/identity?view=aspnetcore-10.0) that simplifies configuration by using services. ASP.NET Core 1.x applications that use authentication or Identity can be updated to use the new model as outlined below.

## Update namespaces

In 1.x, classes such `IdentityRole` and `IdentityUser` were found in the `Microsoft.AspNetCore.Identity.EntityFrameworkCore` namespace.

In 2.0, the [Microsoft.AspNetCore.Identity](/en-us/dotnet/api/microsoft.aspnetcore.identity) namespace became the new home for several of such classes. With the default Identity code, affected classes include `ApplicationUser` and `Startup`. Adjust your `using` statements to resolve the affected references.

[]()

## Authentication middleware and services

In 1.x projects, authentication is configured via middleware. A middleware method is invoked for each authentication scheme you want to support.

The following 1.x example configures Facebook authentication with Identity in `Startup.cs`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
{
    app.UseIdentity();
    app.UseFacebookAuthentication(new FacebookOptions {
        AppId = Configuration["auth:facebook:appid"],
        AppSecret = Configuration["auth:facebook:appsecret"]
    });
}
```

In 2.0 projects, authentication is configured via services. Each authentication scheme is registered in the `ConfigureServices` method of `Startup.cs`. The `UseIdentity` method is replaced with `UseAuthentication`.

The following 2.0 example configures Facebook authentication with Identity in `Startup.cs`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

    // If you want to tweak Identity cookies, they're no longer part of IdentityOptions.
    services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");
    services.AddAuthentication()
            .AddFacebook(options =>
            {
                options.AppId = Configuration["auth:facebook:appid"];
                options.AppSecret = Configuration["auth:facebook:appsecret"];
            });
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {
    app.UseAuthentication();
}
```

The `UseAuthentication` method adds a single authentication middleware component, which is responsible for automatic authentication and the handling of remote authentication requests. It replaces all of the individual middleware components with a single, common middleware component.

Below are 2.0 migration instructions for each major authentication scheme.

### Cookie-based authentication

Select one of the two options below, and make the necessary changes in `Startup.cs`:

1. Use cookies with Identity

   * Replace `UseIdentity` with `UseAuthentication` in the `Configure` method:

     ```
     app.UseAuthentication();
     ```

   * Invoke the `AddIdentity` method in the `ConfigureServices` method to add the cookie authentication services.

   * Optionally, invoke the `ConfigureApplicationCookie` or `ConfigureExternalCookie` method in the `ConfigureServices` method to tweak the Identity cookie settings.

     ```
     services.AddIdentity<ApplicationUser, IdentityRole>()
             .AddEntityFrameworkStores<ApplicationDbContext>()
             .AddDefaultTokenProviders();

     services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");
     ```

2. Use cookies without Identity

   * Replace the `UseCookieAuthentication` method call in the `Configure` method with `UseAuthentication`:

     ```
     app.UseAuthentication();
     ```

   * Invoke the `AddAuthentication` and `AddCookie` methods in the `ConfigureServices` method:

     ```
     // If you don't want the cookie to be automatically authenticated and assigned to HttpContext.User,
     // remove the CookieAuthenticationDefaults.AuthenticationScheme parameter passed to AddAuthentication.
     services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
             .AddCookie(options =>
             {
                 options.LoginPath = "/Account/LogIn";
                 options.LogoutPath = "/Account/LogOff";
             });
     ```

### JWT bearer Authentication

Make the following changes in `Startup.cs`:

* Replace the `UseJwtBearerAuthentication` method call in the `Configure` method with `UseAuthentication`:

  ```
  app.UseAuthentication();
  ```

* Invoke the `AddJwtBearer` method in the `ConfigureServices` method:

  ```
  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
          .AddJwtBearer(options =>
          {
              options.Audience = "http://localhost:5001/";
              options.Authority = "http://localhost:5000/";
          });
  ```

  This code snippet doesn't use Identity, so the default scheme should be set by passing `JwtBearerDefaults.AuthenticationScheme` to the `AddAuthentication` method.

### OpenID Connect (OIDC) authentication

Make the following changes in `Startup.cs`:

* Replace the `UseOpenIdConnectAuthentication` method call in the `Configure` method with `UseAuthentication`:

  ```
  app.UseAuthentication();
  ```

* Invoke the `AddOpenIdConnect` method in the `ConfigureServices` method:

  ```
  services.AddAuthentication(options =>
  {
      options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
      options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
  })
  .AddCookie()
  .AddOpenIdConnect(options =>
  {
      options.Authority = Configuration["auth:oidc:authority"];
      options.ClientId = Configuration["auth:oidc:clientid"];
  });
  ```

* Replace the `PostLogoutRedirectUri` property in the `OpenIdConnectOptions` action with `SignedOutRedirectUri`:

  ```
  .AddOpenIdConnect(options =>
  {
      options.SignedOutRedirectUri = "https://contoso.com";
  });
  ```

### Facebook authentication

Make the following changes in `Startup.cs`:

* Replace the `UseFacebookAuthentication` method call in the `Configure` method with `UseAuthentication`:

  ```
  app.UseAuthentication();
  ```

* Invoke the `AddFacebook` method in the `ConfigureServices` method:

  ```
  services.AddAuthentication()
          .AddFacebook(options =>
          {
              options.AppId = Configuration["auth:facebook:appid"];
              options.AppSecret = Configuration["auth:facebook:appsecret"];
          });
  ```

### Google authentication

Make the following changes in `Startup.cs`:

* Replace the `UseGoogleAuthentication` method call in the `Configure` method with `UseAuthentication`:

  ```
  app.UseAuthentication();
  ```

* Invoke the `AddGoogle` method in the `ConfigureServices` method:

  ```
  services.AddAuthentication()
          .AddGoogle(options =>
          {
              options.ClientId = Configuration["auth:google:clientid"];
              options.ClientSecret = Configuration["auth:google:clientsecret"];
          });
  ```

### Microsoft Account authentication

For more information on Microsoft account authentication, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/14455).

Make the following changes in `Startup.cs`:

* Replace the `UseMicrosoftAccountAuthentication` method call in the `Configure` method with `UseAuthentication`:

  ```
  app.UseAuthentication();
  ```

* Invoke the `AddMicrosoftAccount` method in the `ConfigureServices` method:

  ```
  services.AddAuthentication()
          .AddMicrosoftAccount(options =>
          {
              options.ClientId = Configuration["auth:microsoft:clientid"];
              options.ClientSecret = Configuration["auth:microsoft:clientsecret"];
          });
  ```

### Twitter authentication

Make the following changes in `Startup.cs`:

* Replace the `UseTwitterAuthentication` method call in the `Configure` method with `UseAuthentication`:

  ```
  app.UseAuthentication();
  ```

* Invoke the `AddTwitter` method in the `ConfigureServices` method:

  ```
  services.AddAuthentication()
          .AddTwitter(options =>
          {
              options.ConsumerKey = Configuration["auth:twitter:consumerkey"];
              options.ConsumerSecret = Configuration["auth:twitter:consumersecret"];
          });
  ```

### Setting default authentication schemes

In 1.x, the `AutomaticAuthenticate` and `AutomaticChallenge` properties of the [AuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.authenticationoptions?view=aspnetcore-1.1) base class were intended to be set on a single authentication scheme. There was no good way to enforce this.

In 2.0, these two properties have been removed as properties on the individual `AuthenticationOptions` instance. They can be configured in the `AddAuthentication` method call within the `ConfigureServices` method of `Startup.cs`:

```
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
```

In the preceding code snippet, the default scheme is set to `CookieAuthenticationDefaults.AuthenticationScheme` ("Cookies").

Alternatively, use an overloaded version of the `AddAuthentication` method to set more than one property. In the following overloaded method example, the default scheme is set to `CookieAuthenticationDefaults.AuthenticationScheme`. The authentication scheme may alternatively be specified within your individual `[Authorize]` attributes or authorization policies.

```
services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});
```

Define a default scheme in 2.0 if one of the following conditions is true:

* You want the user to be automatically signed in
* You use the `[Authorize]` attribute or authorization policies without specifying schemes

An exception to this rule is the `AddIdentity` method. This method adds cookies for you and sets the default authenticate and challenge schemes to the application cookie `IdentityConstants.ApplicationScheme`. Additionally, it sets the default sign-in scheme to the external cookie `IdentityConstants.ExternalScheme`.

[]()

## Use HttpContext authentication extensions

The `IAuthenticationManager` interface is the main entry point into the 1.x authentication system. It has been replaced with a new set of `HttpContext` extension methods in the `Microsoft.AspNetCore.Authentication` namespace.

For example, 1.x projects reference an `Authentication` property:

```
// Clear the existing external cookie to ensure a clean login process
await HttpContext.Authentication.SignOutAsync(_externalCookieScheme);
```

In 2.0 projects, import the `Microsoft.AspNetCore.Authentication` namespace, and delete the `Authentication` property references:

```
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
```

[]()

## Windows Authentication (HTTP.sys / IISIntegration)

There are two variations of Windows authentication:

* The host only allows authenticated users. This variation isn't affected by the 2.0 changes.

* The host allows both anonymous and authenticated users. This variation is affected by the 2.0 changes. For example, the app should allow anonymous users at the [IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0) or [HTTP.sys](../../fundamentals/servers/httpsys?view=aspnetcore-10.0) layer but authorize users at the controller level. In this scenario, set the default scheme in the `Startup.ConfigureServices` method.

  For [Microsoft.AspNetCore.Server.IISIntegration](https://www.nuget.org/packages/Microsoft.AspNetCore.Server.IISIntegration/), set the default scheme to `IISDefaults.AuthenticationScheme`:

  ```
  using Microsoft.AspNetCore.Server.IISIntegration;

  services.AddAuthentication(IISDefaults.AuthenticationScheme);
  ```

  For [Microsoft.AspNetCore.Server.HttpSys](https://www.nuget.org/packages/Microsoft.AspNetCore.Server.HttpSys/), set the default scheme to `HttpSysDefaults.AuthenticationScheme`:

  ```
  using Microsoft.AspNetCore.Server.HttpSys;

  services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);
  ```

  Failure to set the default scheme prevents the authorize (challenge) request from working with the following exception:

  > `System.InvalidOperationException`: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.

For more information, see [Configure Windows Authentication in ASP.NET Core](../../security/authentication/windowsauth?view=aspnetcore-10.0).

[]()

## IdentityCookieOptions instances

A side effect of the 2.0 changes is the switch to using named options instead of cookie options instances. The ability to customize the Identity cookie scheme names is removed.

For example, 1.x projects use [constructor injection](../../mvc/controllers/dependency-injection?view=aspnetcore-10.0#constructor-injection) to pass an `IdentityCookieOptions` parameter into `AccountController.cs` and `ManageController.cs`. The external cookie authentication scheme is accessed from the provided instance:

```
public AccountController(
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager,
    IOptions<IdentityCookieOptions> identityCookieOptions,
    IEmailSender emailSender,
    ISmsSender smsSender,
    ILoggerFactory loggerFactory)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme;
    _emailSender = emailSender;
    _smsSender = smsSender;
    _logger = loggerFactory.CreateLogger<AccountController>();
}
```

The aforementioned constructor injection becomes unnecessary in 2.0 projects, and the `_externalCookieScheme` field can be deleted:

```
public AccountController(
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager,
    IEmailSender emailSender,
    ISmsSender smsSender,
    ILoggerFactory loggerFactory)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _emailSender = emailSender;
    _smsSender = smsSender;
    _logger = loggerFactory.CreateLogger<AccountController>();
}
```

1.x projects used the `_externalCookieScheme` field as follows:

```
// Clear the existing external cookie to ensure a clean login process
await HttpContext.Authentication.SignOutAsync(_externalCookieScheme);
```

In 2.0 projects, replace the preceding code with the following. The `IdentityConstants.ExternalScheme` constant can be used directly.

```
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
```

Resolve the newly added `SignOutAsync` call by importing the following namespace:

```
using Microsoft.AspNetCore.Authentication;
```

[]()

## Add IdentityUser POCO navigation properties

The Entity Framework (EF) Core navigation properties of the base `IdentityUser` POCO (Plain Old CLR Object) have been removed. If your 1.x project used these properties, manually add them back to the 2.0 project:

```
/// <summary>
/// Navigation property for the roles this user belongs to.
/// </summary>
public virtual ICollection<IdentityUserRole<int>> Roles { get; } = new List<IdentityUserRole<int>>();

/// <summary>
/// Navigation property for the claims this user possesses.
/// </summary>
public virtual ICollection<IdentityUserClaim<int>> Claims { get; } = new List<IdentityUserClaim<int>>();

/// <summary>
/// Navigation property for this users login accounts.
/// </summary>
public virtual ICollection<IdentityUserLogin<int>> Logins { get; } = new List<IdentityUserLogin<int>>();
```

To prevent duplicate foreign keys when running EF Core Migrations, add the following to your `IdentityDbContext` class' `OnModelCreating` method (after the `base.OnModelCreating();` call):

```
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    // Customize the ASP.NET Core Identity model and override the defaults if needed.
    // For example, you can rename the ASP.NET Core Identity table names and more.
    // Add your customizations after calling base.OnModelCreating(builder);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Claims)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Logins)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Roles)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);
}
```

[]()

## Replace GetExternalAuthenticationSchemes

The synchronous method `GetExternalAuthenticationSchemes` was removed in favor of an asynchronous version. 1.x projects have the following code in `Controllers/ManageController.cs`:

```
var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList();
```

This method appears in `Views/Account/Login.cshtml` too:

```
@{
    var loginProviders = SignInManager.GetExternalAuthenticationSchemes().ToList();
    if (loginProviders.Count == 0)
    {
        <div>
            <p>
                There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                for details on setting up this ASP.NET application to support logging in via external services.
            </p>
        </div>
    }
    else
    {
        <form asp-controller="Account" asp-action="ExternalLogin" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal">
            <div>
                <p>
                    @foreach (var provider in loginProviders)
                    {
                        <button type="submit" class="btn btn-default" name="provider" value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayName account">@provider.AuthenticationScheme</button>
                    }
                </p>
            </div>
        </form>
    }
}
```

In 2.0 projects, use the [GetExternalAuthenticationSchemesAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.getexternalauthenticationschemesasync) method. The change in `ManageController.cs` resembles the following code:

```
var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync();
var otherLogins = schemes.Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList();
```

In `Login.cshtml`, the `AuthenticationScheme` property accessed in the `foreach` loop changes to `Name`:

```
@{
    var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList();
    if (loginProviders.Count == 0)
    {
        <div>
            <p>
                There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                for details on setting up this ASP.NET application to support logging in via external services.
            </p>
        </div>
    }
    else
    {
        <form asp-controller="Account" asp-action="ExternalLogin" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal">
            <div>
                <p>
                    @foreach (var provider in loginProviders)
                    {
                        <button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                    }
                </p>
            </div>
        </form>
    }
}
```

[]()

## ManageLoginsViewModel property change

A `ManageLoginsViewModel` object is used in the `ManageLogins` action of `ManageController.cs`. In 1.x projects, the object's `OtherLogins` property return type is `IList<AuthenticationDescription>`. This return type requires an import of `Microsoft.AspNetCore.Http.Authentication`:

```
using System.Collections.Generic;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreDotNetCore1App.Models.ManageViewModels
{
    public class ManageLoginsViewModel
    {
        public IList<UserLoginInfo> CurrentLogins { get; set; }

        public IList<AuthenticationDescription> OtherLogins { get; set; }
    }
}
```

In 2.0 projects, the return type changes to `IList<AuthenticationScheme>`. This new return type requires replacing the `Microsoft.AspNetCore.Http.Authentication` import with a `Microsoft.AspNetCore.Authentication` import.

```
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreDotNetCore2App.Models.ManageViewModels
{
    public class ManageLoginsViewModel
    {
        public IList<UserLoginInfo> CurrentLogins { get; set; }

        public IList<AuthenticationScheme> OtherLogins { get; set; }
    }
}
```

[]()

## Related content

For more information, see the [Discussion for Auth 2.0](https://github.com/aspnet/Security/issues/1338) issue on GitHub.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/advanced
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/advanced.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Learn about advanced scenarios - ASP.NET MVC with EF Core

In the previous tutorial, you implemented table-per-hierarchy inheritance. This tutorial introduces several topics that are useful to be aware of when you go beyond the basics of developing ASP.NET Core web applications that use Entity Framework Core.

In this tutorial, you:

* Perform raw SQL queries
* Call a query to return entities
* Call a query to return other types
* Call an update query
* Examine SQL queries
* Create an abstraction layer
* Learn about Automatic change detection
* Learn about EF Core source code and development plans
* Learn how to use dynamic LINQ to simplify code

## Prerequisites

* [Implement Inheritance](inheritance?view=aspnetcore-10.0)

## Perform raw SQL queries

One of the advantages of using the Entity Framework is that it avoids tying your code too closely to a particular method of storing data. It does this by generating SQL queries and commands for you, which also frees you from having to write them yourself. But there are exceptional scenarios when you need to run specific SQL queries that you have manually created. For these scenarios, the Entity Framework Code First API includes methods that enable you to pass SQL commands directly to the database. You have the following options in EF Core 1.0:

* Use the `DbSet.FromSql` method for queries that return entity types. The returned objects must be of the type expected by the `DbSet` object, and they're automatically tracked by the database context unless you [turn tracking off](crud?view=aspnetcore-10.0#disable-tracking-of-entity-objects-no-tracking-queries).

* Use the `Database.ExecuteSqlCommand` for non-query commands.

If you need to run a query that returns types that aren't entities, you can use ADO.NET with the database connection provided by EF. The returned data isn't tracked by the database context, even if you use this method to retrieve entity types.

As is always true when you execute SQL commands in a web application, you must take precautions to protect your site against SQL injection attacks. One way to do that is to use parameterized queries to make sure that strings submitted by a web page can't be interpreted as SQL commands. In this tutorial you'll use parameterized queries when integrating user input into a query.

## Call a query to return entities

The `DbSet<TEntity>` class provides a method that you can use to execute a query that returns an entity of type `TEntity`. To see how this works you'll change the code in the `Details` method of the Department controller.

In `DepartmentsController.cs`, in the `Details` method, replace the code that retrieves a department with a `FromSql` method call, as shown in the following highlighted code:

```
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
    var department = await _context.Departments
        .FromSql(query, id)
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync();

    if (department == null)
    {
        return NotFound();
    }

    return View(department);
}
```

To verify that the new code works correctly, select the **Departments** tab and then **Details** for one of the departments.

## Call a query to return other types

Earlier you created a student statistics grid for the About page that showed the number of students for each enrollment date. You got the data from the Students entity set (`_context.Students`) and used LINQ to project the results into a list of `EnrollmentDateGroup` view model objects. Suppose you want to write the SQL itself rather than using LINQ. To do that you need to run a SQL query that returns something other than entity objects. In EF Core 1.0, one way to do that is to write ADO.NET code and get the database connection from EF.

In `HomeController.cs`, replace the `About` method with the following code:

```
public async Task<ActionResult> About()
{
    List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
    var conn = _context.Database.GetDbConnection();
    try
    {
        await conn.OpenAsync();
        using (var command = conn.CreateCommand())
        {
            string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
                + "FROM Person "
                + "WHERE Discriminator = 'Student' "
                + "GROUP BY EnrollmentDate";
            command.CommandText = query;
            DbDataReader reader = await command.ExecuteReaderAsync();

            if (reader.HasRows)
            {
                while (await reader.ReadAsync())
                {
                    var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
                    groups.Add(row);
                }
            }
            reader.Dispose();
        }
    }
    finally
    {
        conn.Close();
    }
    return View(groups);
}
```

Add a using statement:

```
using System.Data.Common;
```

Run the app and go to the About page. It displays the same data it did before.

## Call an update query

Suppose Contoso University administrators want to perform global changes in the database, such as changing the number of credits for every course. If the university has a large number of courses, it would be inefficient to retrieve them all as entities and change them individually. In this section you'll implement a web page that enables the user to specify a factor by which to change the number of credits for all courses, and you'll make the change by executing a SQL UPDATE statement. The web page will look like the following illustration:

In `CoursesController.cs`, add UpdateCourseCredits methods for HttpGet and HttpPost:

```
public IActionResult UpdateCourseCredits()
{
    return View();
}
```

```
[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
    if (multiplier != null)
    {
        ViewData["RowsAffected"] = 
            await _context.Database.ExecuteSqlCommandAsync(
                "UPDATE Course SET Credits = Credits * {0}",
                parameters: multiplier);
    }
    return View();
}
```

When the controller processes an HttpGet request, nothing is returned in `ViewData["RowsAffected"]`, and the view displays an empty text box and a submit button, as shown in the preceding illustration.

When the **Update** button is clicked, the HttpPost method is called, and multiplier has the value entered in the text box. The code then executes the SQL that updates courses and returns the number of affected rows to the view in `ViewData`. When the view gets a `RowsAffected` value, it displays the number of rows updated.

In **Solution Explorer**, right-click the *Views/Courses* folder, and then click **Add > New Item**.

In the **Add New Item** dialog, click **ASP.NET Core** under **Installed** in the left pane, click **Razor View**, and name the new view `UpdateCourseCredits.cshtml`.

In `Views/Courses/UpdateCourseCredits.cshtml`, replace the template code with the following code:

```
@{
    ViewBag.Title = "UpdateCourseCredits";
}

<h2>Update Course Credits</h2>

@if (ViewData["RowsAffected"] == null)
{
    <form asp-action="UpdateCourseCredits">
        <div class="form-actions no-color">
            <p>
                Enter a number to multiply every course's credits by: @Html.TextBox("multiplier")
            </p>
            <p>
                <input type="submit" value="Update" class="btn btn-default" />
            </p>
        </div>
    </form>
}
@if (ViewData["RowsAffected"] != null)
{
    <p>
        Number of rows updated: @ViewData["RowsAffected"]
    </p>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>
```

Run the `UpdateCourseCredits` method by selecting the **Courses** tab, then adding "/UpdateCourseCredits" to the end of the URL in the browser's address bar (for example: `http://localhost:5813/Courses/UpdateCourseCredits`). Enter a number in the text box:

Click **Update**. You see the number of rows affected:

Click **Back to List** to see the list of courses with the revised number of credits.

Note that production code would ensure that updates always result in valid data. The simplified code shown here could multiply the number of credits enough to result in numbers greater than 5. (The `Credits` property has a `[Range(0, 5)]` attribute.) The update query would work but the invalid data could cause unexpected results in other parts of the system that assume the number of credits is 5 or less.

For more information about raw SQL queries, see [Raw SQL Queries](/en-us/ef/core/querying/raw-sql).

## Examine SQL queries

Sometimes it's helpful to be able to see the actual SQL queries that are sent to the database. Built-in logging functionality for ASP.NET Core is automatically used by EF Core to write logs that contain the SQL for queries and updates. In this section you'll see some examples of SQL logging.

Open `StudentsController.cs` and in the `Details` method set a breakpoint on the `if (student == null)` statement.

Run the app in debug mode, and go to the Details page for a student.

Go to the **Output** window showing debug output, and you see the query:

```
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (56ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].[LastName], [s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (122ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID], [s.Enrollments].[Grade], [s.Enrollments].[StudentID], [e.Course].[CourseID], [e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] = [e.Course].[CourseID]
INNER JOIN (
    SELECT TOP(1) [s0].[ID]
    FROM [Person] AS [s0]
    WHERE ([s0].[Discriminator] = N'Student') AND ([s0].[ID] = @__id_0)
    ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]
```

You'll notice something here that might surprise you: the SQL selects up to 2 rows (`TOP(2)`) from the Person table. The `SingleOrDefaultAsync` method doesn't resolve to 1 row on the server. Here's why:

* If the query would return multiple rows, the method returns null.
* To determine whether the query would return multiple rows, EF has to check if it returns at least 2.

Note that you don't have to use debug mode and stop at a breakpoint to get logging output in the **Output** window. It's just a convenient way to stop the logging at the point you want to look at the output. If you don't do that, logging continues and you have to scroll back to find the parts you're interested in.

## Create an abstraction layer

Many developers write code to implement the repository and unit of work patterns as a wrapper around code that works with the Entity Framework. These patterns are intended to create an abstraction layer between the data access layer and the business logic layer of an application. Implementing these patterns can help insulate your application from changes in the data store and can facilitate automated unit testing or test-driven development (TDD). However, writing additional code to implement these patterns isn't always the best choice for applications that use EF, for several reasons:

* The EF context class itself insulates your code from data-store-specific code.

* The EF context class can act as a unit-of-work class for database updates that you do using EF.

* EF includes features for implementing TDD without writing repository code.

For information about how to implement the repository and unit of work patterns, see [the Entity Framework 5 version of this tutorial series](/en-us/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application).

Entity Framework Core implements an in-memory database provider that can be used for testing. For more information, see [Test with InMemory](/en-us/ef/core/miscellaneous/testing/in-memory).

## Automatic change detection

The Entity Framework determines how an entity has changed (and therefore which updates need to be sent to the database) by comparing the current values of an entity with the original values. The original values are stored when the entity is queried or attached. Some of the methods that cause automatic change detection are the following:

* DbContext.SaveChanges

* DbContext.Entry

* ChangeTracker.Entries

If you're tracking a large number of entities and you call one of these methods many times in a loop, you might get significant performance improvements by temporarily turning off automatic change detection using the `ChangeTracker.AutoDetectChangesEnabled` property. For example:

```
_context.ChangeTracker.AutoDetectChangesEnabled = false;
```

## EF Core source code and development plans

The Entity Framework Core source is at <https://github.com/dotnet/efcore>. The EF Core repository contains nightly builds, issue tracking, feature specs, design meeting notes, and [the roadmap for future development](https://github.com/dotnet/efcore/wiki/Roadmap). You can file or find bugs, and contribute.

Although the source code is open, Entity Framework Core is fully supported as a Microsoft product. The Microsoft Entity Framework team keeps control over which contributions are accepted and tests all code changes to ensure the quality of each release.

## Reverse engineer from existing database

To reverse engineer a data model including entity classes from an existing database, use the [scaffold-dbcontext](/en-us/ef/core/miscellaneous/cli/powershell#scaffold-dbcontext) command. See the [getting-started tutorial](/en-us/ef/core/get-started/aspnetcore/existing-db).

[]()

## Use dynamic LINQ to simplify code

The [third tutorial in this series](sort-filter-page?view=aspnetcore-10.0) shows how to write LINQ code by hard-coding column names in a `switch` statement. With two columns to choose from, this works fine, but if you have many columns the code could get verbose. To solve that problem, you can use the `EF.Property` method to specify the name of the property as a string. To try out this approach, replace the `Index` method in the `StudentsController` with the following code.

```
 public async Task<IActionResult> Index(
     string sortOrder,
     string currentFilter,
     string searchString,
     int? pageNumber)
 {
     ViewData["CurrentSort"] = sortOrder;
     ViewData["NameSortParm"] = 
         String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
     ViewData["DateSortParm"] = 
         sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

     if (searchString != null)
     {
         pageNumber = 1;
     }
     else
     {
         searchString = currentFilter;
     }

     ViewData["CurrentFilter"] = searchString;

     var students = from s in _context.Students
                    select s;
     
     if (!String.IsNullOrEmpty(searchString))
     {
         students = students.Where(s => s.LastName.Contains(searchString)
                                || s.FirstMidName.Contains(searchString));
     }

     if (string.IsNullOrEmpty(sortOrder))
     {
         sortOrder = "LastName";
     }

     bool descending = false;
     if (sortOrder.EndsWith("_desc"))
     {
         sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
         descending = true;
     }

     if (descending)
     {
         students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
     }
     else
     {
         students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
     }

     int pageSize = 3;
     return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), 
         pageNumber ?? 1, pageSize));
 }
```

## Acknowledgments

Tom Dykstra and Rick Anderson (twitter @RickAndMSFT) wrote this tutorial. Rowan Miller, Diego Vega, and other members of the Entity Framework team assisted with code reviews and helped debug issues that arose while we were writing code for the tutorials. John Parente and Paul Goldman worked on updating the tutorial for ASP.NET Core 2.2.

[]()

## Troubleshoot common errors

### ContosoUniversity.dll used by another process

Error message:

> Cannot open '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The process cannot access the file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll' because it is being used by another process.

Solution:

Stop the site in IIS Express. Go to the Windows System Tray, find IIS Express and right-click its icon, select the Contoso University site, and then click **Stop Site**.

### Migration scaffolded with no code in Up and Down methods

Possible cause:

The EF CLI commands don't automatically close and save code files. If you have unsaved changes when you run the `migrations add` command, EF won't find your changes.

Solution:

Run the `migrations remove` command, save your code changes and rerun the `migrations add` command.

### Errors while running database update

It's possible to get other errors when making schema changes in a database that has existing data. If you get migration errors you can't resolve, you can either change the database name in the connection string or delete the database. With a new database, there's no data to migrate, and the update-database command is much more likely to complete without errors.

The simplest approach is to rename the database in `appsettings.json`. The next time you run `database update`, a new database will be created.

To delete a database in SSOX, right-click the database, click **Delete**, and then in the **Delete Database** dialog box select **Close existing connections** and click **OK**.

To delete a database by using the CLI, run the `database drop` CLI command:

```
dotnet ef database drop
```

### Error locating SQL Server instance

Error Message:

> A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)

Solution:

Check the connection string. If you have manually deleted the database file, change the name of the database in the construction string to start over with a new database.

## Get the code

[Download or view the completed application.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final)

## Related content

For more information about EF Core, see the [Entity Framework Core documentation](/en-us/ef/core). A book is also available: [Entity Framework Core in Action](https://www.manning.com/books/entity-framework-core-in-action).

For information on how to deploy a web app, see [Host and deploy ASP.NET Core](../../host-and-deploy/?view=aspnetcore-10.0).

For information about other topics related to ASP.NET Core MVC, such as authentication and authorization, see [Overview of ASP.NET Core](../../overview?view=aspnetcore-10.0).

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

## Next steps

In this tutorial, you:

* Performed raw SQL queries
* Called a query to return entities
* Called a query to return other types
* Called an update query
* Examined SQL queries
* Created an abstraction layer
* Learned about Automatic change detection
* Learned about EF Core source code and development plans
* Learned how to use dynamic LINQ to simplify code

This completes this series of tutorials on using the Entity Framework Core in an ASP.NET Core MVC application. This series worked with a new database; an alternative is to [reverse engineer a model from an existing database](/en-us/ef/core/managing-schemas/scaffolding).

[Tutorial: EF Core with MVC, existing database](/en-us/ef/core/get-started/aspnetcore/existing-db?toc=/aspnet/core/toc.json\&bc=/aspnet/core/breadcrumb/toc.json)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/javascript-interoperability/call-javascript-from-dotnet.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Call JavaScript functions from .NET methods in ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to invoke JavaScript (JS) functions from .NET.

For information on how to call .NET methods from JS, see [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](call-dotnet-from-javascript?view=aspnetcore-10.0).

## Invoke JS functions

[IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) is registered by the Blazor framework. To call into JS from .NET, inject the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) abstraction and call one of the following methods:

* [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync)
* [JSRuntimeExtensions.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokeasync)
* [JSRuntimeExtensions.InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync)

For the preceding .NET methods that invoke JS functions:

* The function identifier (`String`) is relative to the global scope (`window`). To call `window.someScope.someFunction`, the identifier is `someScope.someFunction`. There's no need to register the function before it's called.
* Pass any number of JSON-serializable arguments in `Object[]` to a JS function.
* The cancellation token (`CancellationToken`) propagates a notification that operations should be canceled.
* `TimeSpan` represents a time limit for a JS operation.
* The `TValue` return type must also be JSON serializable. `TValue` should match the .NET type that best maps to the JSON type returned.
* A [JS `Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) is returned for `InvokeAsync` methods. `InvokeAsync` unwraps the [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) and returns the value awaited by the [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise).
* [C# union types](/en-us/dotnet/csharp/language-reference/builtin-types/union) are supported.

- The function identifier (`String`) is relative to the global scope (`window`). To call `window.someScope.someFunction`, the identifier is `someScope.someFunction`. There's no need to register the function before it's called.
- Pass any number of JSON-serializable arguments in `Object[]` to a JS function.
- The cancellation token (`CancellationToken`) propagates a notification that operations should be canceled.
- `TimeSpan` represents a time limit for a JS operation.
- The `TValue` return type must also be JSON serializable. `TValue` should match the .NET type that best maps to the JSON type returned.
- A [JS `Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) is returned for `InvokeAsync` methods. `InvokeAsync` unwraps the [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) and returns the value awaited by the [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise).

For Blazor apps with prerendering enabled, which is the default for server-side apps, calling into JS isn't possible during prerendering. For more information, see the [Prerendering](#prerendering) section.

The following example is based on [`TextDecoder`](https://developer.mozilla.org/docs/Web/API/TextDecoder), a JS-based decoder. The example demonstrates how to invoke a JS function from a C# method that offloads a requirement from developer code to an existing JS API. The JS function accepts a byte array from a C# method, decodes the array, and returns the text to the component for display.

```
<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    return decodedArray;
  };
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

The following component:

* Invokes the `convertArray` JS function with [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokeasync) when selecting a button (**`Convert Array`**).
* After the JS function is called, the passed array is converted into a string. The string is returned to the component for display (`text`).

`CallJs1.razor`:

```
@page "/call-js-1"
@inject IJSRuntime JS

<PageTitle>Call JS 1</PageTitle>

<h1>Call JS Example 1</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray() => 
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
}
```

`CallJs1.razor`:

```
@page "/call-js-1"
@inject IJSRuntime JS

<PageTitle>Call JS 1</PageTitle>

<h1>Call JS Example 1</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray() => 
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
}
```

`CallJsExample1.razor`:

```
@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}
```

`CallJsExample1.razor`:

```
@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}
```

`CallJsExample1.razor`:

```
@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}
```

`CallJsExample1.razor`:

```
@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new MarkupString(await JS.InvokeAsync<string>("convertArray", 
            quoteArray));
    }
}
```

## JavaScript API restricted to user gestures

*This section applies to server-side components.*

Some browser JavaScript (JS) APIs can only be executed in the context of a user gesture, such as using the [`Fullscreen API`](https://developer.mozilla.org/docs/Web/API/Fullscreen_API). These APIs can't be called through the JS interop mechanism in server-side components because UI event handling is performed asynchronously and generally no longer in the context of the user gesture. The app must handle the UI event completely in JavaScript, so use `onclick` instead of Blazor's `@onclick` directive attribute.

## Invoke JavaScript functions without reading a returned value (`InvokeVoidAsync`)

Use [InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync) when:

* .NET isn't required to read the result of a JavaScript (JS) call.
* JS functions return [void(0)/void 0](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void) or [undefined](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined).

Provide a `displayTickerAlert1` JS function. The function is called with [InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync) and doesn't return a value:

```
<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

### Component (`.razor`) example (`InvokeVoidAsync`)

The following component:

* Invokes the `displayTickerAlert1` JS function with [InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync) with two JSON-serializable parameters, a string in `stockSymbol` and a decimal in `price`, when selecting a button (**`Set Stock`**).
* The JS function receives the two arguments in strictly-typed parameters and displays a modal alert box with a specified message.

`CallJs2.razor`:

```
@page "/call-js-2"
@inject IJSRuntime JS

<PageTitle>Call JS 2</PageTitle>

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}" +
            $"{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}
```

`CallJs2.razor`:

```
@page "/call-js-2"
@inject IJSRuntime JS

<PageTitle>Call JS 2</PageTitle>

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}" +
            $"{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}
```

`CallJsExample2.razor`:

```
@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}
```

`CallJsExample2.razor`:

```
@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}
```

`CallJsExample2.razor`:

```
@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}
```

`CallJsExample2.razor`:

```
@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}
```

### Class (`.cs`) example (`InvokeVoidAsync`)

`JsInteropClasses1.cs`:

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class JsInteropClasses1(IJSRuntime js) : IDisposable
{
    private readonly IJSRuntime js = js;

    public async ValueTask TickerChanged(string symbol, decimal price) => 
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);

    // Calling SuppressFinalize(this) prevents derived types that introduce 
    // a finalizer from needing to re-implement IDisposable.
    public void Dispose() => GC.SuppressFinalize(this);
}
```

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class JsInteropClasses1(IJSRuntime js) : IDisposable
{
    private readonly IJSRuntime js = js;

    public async ValueTask TickerChanged(string symbol, decimal price) => 
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);

    // Calling SuppressFinalize(this) prevents derived types that introduce 
    // a finalizer from needing to re-implement IDisposable.
    public void Dispose() => GC.SuppressFinalize(this);
}
```

```
using Microsoft.JSInterop;

public class JsInteropClasses1 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}
```

```
using Microsoft.JSInterop;

public class JsInteropClasses1 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}
```

```
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}
```

```
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}
```

`TickerChanged` calls the `displayTickerAlert1` method in the following component.

`CallJs3.razor`:

```
@page "/call-js-3"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 3</PageTitle>

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses1? jsClass;

    protected override void OnInitialized() => jsClass = new(JS);

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}" +
                $"{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            await jsClass.TickerChanged(stockSymbol, price);
        }
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJs3.razor`:

```
@page "/call-js-3"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 3</PageTitle>

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses1? jsClass;

    protected override void OnInitialized() => jsClass = new(JS);

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}" +
                $"{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            await jsClass.TickerChanged(stockSymbol, price);
        }
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJsExample3.razor`:

```
@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses1? jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            await jsClass.TickerChanged(stockSymbol, price);
        }
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJsExample3.razor`:

```
@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses1? jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            await jsClass.TickerChanged(stockSymbol, price);
        }
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJsExample3.razor`:

```
@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJsExample3.razor`:

```
@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses1(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}
```

## Invoke JavaScript functions and read a returned value (`InvokeAsync`)

Use [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokeasync) when .NET should read the result of a JavaScript (JS) call.

Provide a `displayTickerAlert2` JS function. The following example returns a string for display by the caller:

```
<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

### Component (`.razor`) example (`InvokeAsync`)

The following component:

* Invokes the `displayTickerAlert2` JS function with [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokeasync) with two JSON-serializable parameters, a string in `stockSymbol` and a decimal in `price`, when selecting a button (**`Set Stock`**).
* The JS function receives the two arguments in strictly-typed parameters and displays a modal alert box with a specified message.

`CallJs4.razor`:

```
@page "/call-js-4"
@inject IJSRuntime JS

<PageTitle>Call JS 4</PageTitle>

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private string? result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}" +
            $"{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of SetStock call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}
```

`CallJs4.razor`:

```
@page "/call-js-4"
@inject IJSRuntime JS

<PageTitle>Call JS 4</PageTitle>

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private string? result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}" +
            $"{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of SetStock call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}
```

`CallJsExample4.razor`:

```
@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private string? result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of SetStock call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}
```

`CallJsExample4.razor`:

```
@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private string? result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of SetStock call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}
```

`CallJsExample4.razor`:

```
@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of SetStock call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}
```

`CallJsExample4.razor`:

```
@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of SetStock call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}
```

### Class (`.cs`) example (`InvokeAsync`)

`JsInteropClasses2.cs`:

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class JsInteropClasses2(IJSRuntime js) : IDisposable
{
    private readonly IJSRuntime js = js;

    public async ValueTask<string> TickerChanged(string symbol, decimal price) => 
        await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);

    // Calling SuppressFinalize(this) prevents derived types that introduce 
    // a finalizer from needing to re-implement IDisposable.
    public void Dispose() => GC.SuppressFinalize(this);
}
```

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class JsInteropClasses2(IJSRuntime js) : IDisposable
{
    private readonly IJSRuntime js = js;

    public async ValueTask<string> TickerChanged(string symbol, decimal price) => 
        await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);

    // Calling SuppressFinalize(this) prevents derived types that introduce 
    // a finalizer from needing to re-implement IDisposable.
    public void Dispose() => GC.SuppressFinalize(this);
}
```

```
using Microsoft.JSInterop;

public class JsInteropClasses2 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}
```

```
using Microsoft.JSInterop;

public class JsInteropClasses2 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}
```

```
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}
```

```
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}
```

`TickerChanged` calls the `displayTickerAlert2` method and displays the returned string in the following component.

`CallJs5.razor`:

```
@page "/call-js-5"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 5</PageTitle>

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses2? jsClass;
    private string? result;

    protected override void OnInitialized() => jsClass = new(JS);

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}" +
                $"{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            var interopResult = await jsClass.TickerChanged(stockSymbol, price);
            result = $"Result of SetStock call for {stockSymbol} at " +
                $"{price.ToString("c")}: {interopResult}";
        }
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJs5.razor`:

```
@page "/call-js-5"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 5</PageTitle>

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses2? jsClass;
    private string? result;

    protected override void OnInitialized() => jsClass = new(JS);

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}" +
                $"{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            var interopResult = await jsClass.TickerChanged(stockSymbol, price);
            result = $"Result of SetStock call for {stockSymbol} at " +
                $"{price.ToString("c")}: {interopResult}";
        }
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJsExample5.razor`:

```
@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses2? jsClass;
    private string? result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            var interopResult = await jsClass.TickerChanged(stockSymbol, price);
            result = $"Result of SetStock call for {stockSymbol} at " +
                $"{price.ToString("c")}: {interopResult}";
        }
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJsExample5.razor`:

```
@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses2? jsClass;
    private string? result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            var interopResult = await jsClass.TickerChanged(stockSymbol, price);
            result = $"Result of SetStock call for {stockSymbol} at " +
                $"{price.ToString("c")}: {interopResult}";
        }
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJsExample5.razor`:

```
@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of SetStock call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}
```

`CallJsExample5.razor`:

```
@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses2(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of SetStock call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}
```

## Dynamic content generation scenarios

For dynamic content generation with [BuildRenderTree](../advanced-scenarios?view=aspnetcore-10.0#manually-build-a-render-tree-rendertreebuilder), use the `[Inject]` attribute:

```
[Inject]
IJSRuntime JS { get; set; }
```

## Prerendering

*This section applies to server-side apps that prerender Razor components. Prerendering is covered in [Prerender ASP.NET Core Razor components](../components/prerender?view=aspnetcore-10.0).*

*This section applies to server-side apps that prerender Razor components. Prerendering is covered in [Prerender ASP.NET Core Razor components](../components/prerender?view=aspnetcore-10.0).*

Note

Internal navigation for [interactive routing](../fundamentals/routing?view=aspnetcore-10.0#static-versus-interactive-routing) in Blazor Web Apps doesn't involve requesting new page content from the server. Therefore, prerendering doesn't occur for internal page requests. If the app adopts interactive routing, perform a full page reload for component examples that demonstrate prerendering behavior. For more information, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0#interactive-routing-and-prerendering).

*This section applies to server-side apps and hosted Blazor WebAssembly apps that prerender Razor components. Prerendering is covered in [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../components/integration?view=aspnetcore-10.0).*

During prerendering, calling into JavaScript (JS) isn't possible. The following example demonstrates how to use JS interop as part of a component's initialization logic in a way that's compatible with prerendering.

The following `scrollElementIntoView` function:

* Scrolls to the passed element with [`scrollIntoView`](https://developer.mozilla.org/docs/Web/API/Element/scrollIntoView).
* Returns the element's `top` property value from the [`getBoundingClientRect`](https://developer.mozilla.org/docs/Web/API/Element/getBoundingClientRect) method.

```
window.scrollElementIntoView = (element) => {
  element.scrollIntoView();
  return element.getBoundingClientRect().top;
}
```

Where [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) calls the JS function in component code, the [ElementReference](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreference) is only used in [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) and not in any earlier lifecycle method because there's no HTML DOM element until after the component is rendered.

[`StateHasChanged`](../components/lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged) ([reference source](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged)) is called to enqueue rerendering of the component with the new state obtained from the JS interop call (for more information, see [ASP.NET Core Razor component rendering](../components/rendering?view=aspnetcore-10.0)). An infinite loop isn't created because [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is only called when `scrollPosition` is `null`.

`PrerenderedInterop.razor`:

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<PageTitle>Prerendered Interop</PageTitle>

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

The preceding example pollutes the client with a global function. For a better approach in production apps, see [JavaScript isolation in JavaScript modules](call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules).

## Synchronous JS interop in client-side components

*This section only applies to client-side components.*

JS interop calls are asynchronous, regardless of whether the called code is synchronous or asynchronous. Calls are asynchronous to ensure that components are compatible across server-side and client-side render modes. On the server, all JS interop calls must be asynchronous because they're sent over a network connection.

If you know for certain that your component only runs on WebAssembly, you can choose to make synchronous JS interop calls. This has slightly less overhead than making asynchronous calls and can result in fewer render cycles because there's no intermediate state while awaiting results.

To make a synchronous call from .NET to JavaScript in a client-side component, cast [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) to [IJSInProcessRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime) to make the JS interop call:

```
@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}
```

When working with [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) in .NET 5 or later client-side components, you can use [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) synchronously instead. [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) implements [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable)/[IDisposable](/en-us/dotnet/api/system.idisposable) and should be disposed for garbage collection to prevent a memory leak, as the following example demonstrates:

```
@inject IJSRuntime JS
@implements IDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var jsInProcess = (IJSInProcessRuntime)JS;
            module = await jsInProcess.Invoke<IJSInProcessObjectReference>("import", 
                "./scripts.js");
            var value = module.Invoke<string>("javascriptFunctionIdentifier");
        }
    }

    ...

    void IDisposable.Dispose()
    {
        if (module is not null)
        {
            await module.Dispose();
        }
    }
}
```

In the preceding example, a [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) isn't trapped during module disposal because there's no Blazor-SignalR circuit in a Blazor WebAssembly app to lose. For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit).

## Create an instance of a JS object using a constructor function

Create an instance of a JS object using a constructor function and get the [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference)/[IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) .NET handle for referencing the instance with the following API:

* [InvokeConstructorAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.invokeconstructorasync) (asynchronous)
* [InvokeConstructor](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference.invokeconstructor) (synchronous)

Examples in this section demonstrate the API calls with the following `TestClass` with a constructor function (`constructor(text)`):

```
window.TestClass = class {
  constructor(text) {
    this.text = text;
  }

  getTextLength() {
    return this.text.length;
  }
}
```

### Asynchronous `InvokeConstructorAsync`

Use [InvokeConstructorAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.invokeconstructorasync) on [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) and [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) to invoke the specified JS constructor function asynchronously. The function is invoked with the `new` operator. In the following example, `TestClass` contains a constructor function, and `classRef` is an [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference).

```
var classRef = await JSRuntime.InvokeConstructorAsync("TestClass", "Blazor!");
var text = await classRef.GetValueAsync<string>("text");
var textLength = await classRef.InvokeAsync<int>("getTextLength");
```

An overload is available that takes a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) argument or [TimeSpan](/en-us/dotnet/api/system.timespan) timeout argument.

### Synchronous `InvokeConstructor`

Use [InvokeConstructor](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference.invokeconstructor) on [IJSInProcessRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime) and [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) to invoke the specified JS constructor function synchronously. The function is invoked with the `new` operator. In the following example, `TestClass` contains a constructor function, and `classRef` is an [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference):

```
var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
var classRef = inProcRuntime.InvokeConstructor("TestClass", "Blazor!");
var text = classRef.GetValue<string>("text");
var textLength = classRef.Invoke<int>("getTextLength");
```

An overload is available that takes a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) argument or [TimeSpan](/en-us/dotnet/api/system.timespan) timeout argument.

## Read or modify the value of a JS object property

Read or modify the value of a JS object property, both data and accessor properties, with the following API:

* [GetValueAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.getvalueasync)/[SetValueAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.setvalueasync) (asynchronous)
* `GetValue`/`SetValue` (synchronous)

Examples in this section demonstrate the API calls with the following JS object (`testObject`):

```
window.testObject = {
  num: 10
}
```

### Asynchronous `GetValueAsync` and `SetValueAsync`

Use [GetValueAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.getvalueasync) to read the value of the specified JS property asynchronously. A [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) is thrown if the property doesn't exist or is a `set`-only property. In the following example, the value of `testObject.num` (10) is stored in `valueFromDataPropertyAsync`:

```
var valueFromDataPropertyAsync = 
    await JSRuntime.GetValueAsync<int>("testObject.num");
```

Use [SetValueAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.setvalueasync) to update the value of the specified JS property asynchronously. If the property isn't defined on the target object, the property is created. A [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) is thrown if the property exists but isn't writable or when a new property can't be added to the object. In the following example, `testObject.num` is set to 20, and `num2` is created with a value of 30 on `testObject`:

```
await JSRuntime.SetValueAsync("testObject.num", 20);
await JSRuntime.SetValueAsync("testObject.num2", 30);
```

### Synchronous `GetValue` and `SetValue`

Use `GetValue` to read the value of the specified JS property synchronously. A [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) is thrown if the property doesn't exist or is a `set`-only property. In the following example, the value of `testObject.num` (10) is stored in `valueFromDataProperty`:

```
var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
var valueFromDataProperty = inProcRuntime.GetValue<int>("testObject.num");
```

Use `SetValue` to update the value of the specified JS property synchronously. The property can't be a `get`-only property. If the property isn't defined on the target object, the property is created. A [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) is thrown if the property exists but isn't writable or when a new property can't be added to the object. In the following example, `testObject.num` is set to 20, and `num2` is created with a value of 30 on `testObject`:

```
var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);
inProcRuntime.SetValue("testObject.num", 20);
inProcRuntime.SetValue("testObject.num2", 30);
```

- [Inject a script after Blazor starts](location-of-javascript?view=aspnetcore-10.0#inject-a-script-after-blazor-starts)

For information on isolating scripts in [JS modules](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules), see the [JavaScript isolation in JavaScript modules](#javascript-isolation-in-javascript-modules) section.

[Dynamic import with the `import()` operator](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/import) is supported with ASP.NET Core and Blazor:

```
if ({CONDITION}) import("/additionalModule.js");
```

In the preceding example, the `{CONDITION}` placeholder represents a conditional check to determine if the module should be loaded.

For browser compatibility, see [Can I use: JavaScript modules: dynamic import](https://caniuse.com/es6-module-dynamic-import).

In server-side scenarios, JS interop calls can't be issued after Blazor's SignalR circuit is disconnected. Without a circuit during component disposal or at any other time that a circuit doesn't exist, the following method calls fail and log a message that the circuit is disconnected as a [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception):

* JS interop method calls

  * [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync)
  * [JSRuntimeExtensions.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokeasync)
  * [JSRuntimeExtensions.InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync)
  * [InvokeConstructorAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.invokeconstructorasync)
  * [InvokeConstructor](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference.invokeconstructor)
  * [GetValueAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.getvalueasync)
  * [SetValueAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.setvalueasync)

* [Dispose](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.dispose)/`DisposeAsync` calls on any [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference).

- JS interop method calls

  * [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync)
  * [JSRuntimeExtensions.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokeasync)
  * [JSRuntimeExtensions.InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync)

- [Dispose](/en-us/dotnet/api/microsoft.jsinterop.jsruntime.dispose)/`DisposeAsync` calls on any [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference).

In order to avoid logging [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) or to log custom information in server-side Blazor, catch the exception in a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement.

For the following component disposal example:

* The server-side component implements [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable).
* `module` is an [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) for a JS module.
* [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) is caught and not logged.
* Optionally, you can log custom information in the `catch` statement at whatever log level you prefer. The following example doesn't log custom information. The code assumes that the developer doesn't care about when or where circuits are disconnected during component disposal.

```
async ValueTask IAsyncDisposable.DisposeAsync()
{
    try
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
    catch (JSDisconnectedException)
    {
    }
}
```

If you must clean up your own JS objects or execute other JS code on the client after a circuit is lost in a server-side Blazor app, use the [`MutationObserver`](https://developer.mozilla.org/docs/Web/API/MutationObserver) pattern in JS on the client. The `MutationObserver` pattern allows you to execute JS code when an element is removed from the DOM.

For more information, see the following articles:

* [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#dom-cleanup-tasks-during-component-disposal): Includes a code example of the `MutationObserver` pattern.
* [Handle errors in ASP.NET Core Blazor apps](../fundamentals/handle-errors?view=aspnetcore-10.0#javascript-interop): The *JavaScript interop* section discusses error handling in JS interop scenarios.
* [ASP.NET Core Razor component disposal](../components/component-disposal?view=aspnetcore-10.0): The article describes how to implement disposal patterns in Razor components.

The following JS module exports a JS function for showing a [browser window prompt](https://developer.mozilla.org/docs/Web/API/Window/prompt). Place the following JS code in an external JS file.

`wwwroot/scripts.js`:

```
export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}
```

Add the preceding JS module to an app or class library as a static web asset in the `wwwroot` folder and then import the module into the .NET code by calling [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) on the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) instance.

[IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) imports the module as an [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference), which represents a reference to a JS object from .NET code. Use the [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) to invoke exported JS functions from the module.

`CallJs6.razor`:

```
@page "/call-js-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 6</PageTitle>

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt() => result = await Prompt("Provide text");

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

`CallJs6.razor`:

```
@page "/call-js-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 6</PageTitle>

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt() => result = await Prompt("Provide text");

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

`CallJsExample6.razor`:

```
@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}
```

`CallJsExample6.razor`:

```
@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}
```

`CallJsExample6.razor`:

```
@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference module;
    private string result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string> Prompt(string message)
    {
        return await module.InvokeAsync<string>("showPrompt", message);
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}
```

In the preceding example:

* By convention, the `import` identifier is a special identifier used specifically for importing a JS module.

* Specify the module's external JS file using its stable static web asset path: `./{SCRIPT PATH AND FILE NAME (.js)}`, where:

  * The path segment for the current directory (`./`) is required in order to create the correct static asset path to the JS file.
  * The `{SCRIPT PATH AND FILE NAME (.js)}` placeholder is the path and file name under `wwwroot`.

* Disposes the [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) for [garbage collection](../components/component-disposal?view=aspnetcore-10.0#asynchronous-iasyncdisposable) in [IAsyncDisposable.DisposeAsync](/en-us/dotnet/api/system.iasyncdisposable.disposeasync).

* Don't place a `<script>` tag for the script after the [Blazor script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) because the module is loaded and cached automatically when the [dynamic `import()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/import) is invoked.

Dynamically importing a module requires a network request, so it can only be achieved asynchronously by calling [InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync).

[IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) represents a reference to a JS object whose functions can be invoked synchronously in client-side components. For more information, see the [Synchronous JS interop in client-side components](#synchronous-js-interop-in-client-side-components) section.

Note

When the external JS file is supplied by a [Razor class library](../components/class-libraries?view=aspnetcore-10.0), specify the module's JS file using its stable static web asset path: `./_content/{PACKAGE ID}/{SCRIPT PATH AND FILE NAME (.js)}`:

* The path segment for the current directory (`./`) is required in order to create the correct static asset path to the JS file.
* The `{PACKAGE ID}` placeholder is the library's [package ID](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties). The package ID defaults to the project's assembly name if `<PackageId>` isn't specified in the project file. In the following example, the library's assembly name is `ComponentLibrary` and the library's project file doesn't specify `<PackageId>`.
* The `{SCRIPT PATH AND FILE NAME (.js)}` placeholder is the path and file name under `wwwroot`. In the following example, the external JS file (`script.js`) is placed in the class library's `wwwroot` folder.
* `module` is a private nullable [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) of the component class (`private IJSObjectReference? module;`).

```
module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");
```

For more information, see [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../components/class-libraries?view=aspnetcore-10.0).

Throughout the Blazor documentation, examples use the `.js` file extension for module files, not the [newer `.mjs` file extension (RFC 9239)](https://www.rfc-editor.org/rfc/rfc9239). Our documentation continues to use the `.js` file extension for the same reasons the Mozilla Foundation's documentation continues to use the `.js` file extension. For more information, see [Aside — .mjs versus .js (MDN documentation)](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules#aside_%E2%80%94_.mjs_versus_.js).

## Capture references to elements

Some JavaScript (JS) interop scenarios require references to HTML elements. For example, a UI library may require an element reference for initialization, or you might need to call command-like APIs on an element, such as `click` or `play`.

Capture references to HTML elements in a component using the following approach:

* Add an `@ref` attribute to the HTML element.
* Define a field of type [ElementReference](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreference) whose name matches the value of the `@ref` attribute.

The following example shows capturing a reference to the `username` `<input>` element:

```
<input @ref="username" ... />

@code {
    private ElementReference username;
}
```

Warning

Only use an element reference to mutate the contents of an empty element that doesn't interact with Blazor. This scenario is useful when a third-party API supplies content to the element. Because Blazor doesn't interact with the element, there's no possibility of a conflict between Blazor's representation of the element and the DOM.

In the following example, it's *dangerous* to mutate the contents of the unordered list (`ul`) using `MyList` via JS interop because Blazor interacts with the DOM to populate this element's list items (`<li>`) from the `Todos` object:

```
<ul @ref="MyList">
    @foreach (var item in Todos)
    {
        <li>@item.Text</li>
    }
</ul>
```

Using the `MyList` element reference to merely read DOM content or trigger an event is supported.

If JS interop *mutates the contents* of element `MyList` and Blazor attempts to apply diffs to the element, the diffs won't match the DOM. Modifying the contents of the list via JS interop with the `MyList` element reference is ***not supported***.

For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#interaction-with-the-document-object-model-dom).

An [ElementReference](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreference) is passed through to JS code via JS interop. The JS code receives an [`HTMLElement`](https://developer.mozilla.org/docs/Web/API/HTMLElement) instance, which it can use with normal DOM APIs. For example, the following code defines a .NET extension method (`TriggerClickEvent`) that enables sending a mouse click to an element.

The JS function `clickElement` creates a [`click`](https://developer.mozilla.org/docs/Web/API/Element/click_event) event on the passed HTML element (`element`):

```
window.interopFunctions = {
  clickElement : function (element) {
    element.click();
  }
}
```

To call a JS function that doesn't return a value, use [JSRuntimeExtensions.InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync). The following code triggers a client-side [`click`](https://developer.mozilla.org/docs/Web/API/Element/click_event) event by calling the preceding JS function with the captured [ElementReference](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreference):

```
@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await JS.InvokeVoidAsync(
            "interopFunctions.clickElement", exampleButton);
    }
}
```

To use an extension method, create a static extension method that receives the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) instance:

```
public static async Task TriggerClickEvent(this ElementReference elementRef, 
    IJSRuntime js)
{
    await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}
```

The `clickElement` method is called directly on the object. The following example assumes that the `TriggerClickEvent` method is available from the `JsInteropClasses` namespace:

```
@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await exampleButton.TriggerClickEvent(JS);
    }
}
```

Important

The `exampleButton` variable is only populated after the component is rendered. If an unpopulated [ElementReference](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreference) is passed to JS code, the JS code receives a value of `null`. To manipulate element references after the component has finished rendering, use the [`OnAfterRenderAsync` or `OnAfterRender` component lifecycle methods](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync).

When working with generic types and returning a value, use [ValueTask\<TResult>](/en-us/dotnet/api/system.threading.tasks.valuetask-1):

```
public static ValueTask<T> GenericMethod<T>(
        this ElementReference elementRef, IJSRuntime js) => 
    js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
```

The `{JAVASCRIPT FUNCTION}` placeholder is the JS function identifier.

`GenericMethod` is called directly on the object with a type. The following example assumes that the `GenericMethod` is available from the `JsInteropClasses` namespace:

```
@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string? returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}
```

```
@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string? returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}
```

```
@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}
```

## Reference elements across components

An [ElementReference](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreference) can't be passed between components because:

* The instance is only guaranteed to exist after the component is rendered, which is during or after a component's [OnAfterRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrender)/[OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) method executes.
* An [ElementReference](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreference) is a [`struct`](/en-us/dotnet/csharp/language-reference/builtin-types/struct), which can't be passed as a [component parameter](../components/?view=aspnetcore-10.0#component-parameters).

For a parent component to make an element reference available to other components, the parent component can:

* Allow child components to register callbacks.
* Invoke the registered callbacks during the [OnAfterRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrender) event with the passed element reference. Indirectly, this approach allows child components to interact with the parent's element reference.

```
<style>
    .red { color: red }
</style>
```

```
<script>
  function setElementClass(element, className) {
    var myElement = element;
    myElement.classList.add(className);
  }
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

`CallJs7.razor` (parent component):

```
@page "/call-js-7"

<PageTitle>Call JS 7</PageTitle>

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />
```

`CallJs7.razor` (parent component):

```
@page "/call-js-7"

<PageTitle>Call JS 7</PageTitle>

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />
```

`CallJsExample7.razor` (parent component):

```
@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />
```

`CallJsExample7.razor` (parent component):

```
@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />
```

`CallJsExample7.razor` (parent component):

```
@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />
```

`CallJsExample7.razor` (parent component):

```
@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />
```

`CallJs7.razor.cs`:

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages;

public partial class CallJs7 : 
    ComponentBase, IObservable<ElementReference>, IDisposable
{
    private bool disposing;
    private readonly List<IObserver<ElementReference>> subscriptions = [];
    private ElementReference title;

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        foreach (var subscription in subscriptions)
        {
            subscription.OnNext(title);
        }
    }

    public void Dispose()
    {
        disposing = true;

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnCompleted();
            }
            catch (Exception)
            {
            }
        }

        subscriptions.Clear();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }

    public IDisposable Subscribe(IObserver<ElementReference> observer)
    {
        if (disposing)
        {
            throw new InvalidOperationException("Parent being disposed");
        }

        subscriptions.Add(observer);

        return new Subscription(observer, this);
    }

    private class Subscription(IObserver<ElementReference> observer,
        CallJs7 self) : IDisposable
    {
        public IObserver<ElementReference> Observer { get; } = observer;
        public CallJs7 Self { get; } = self;

        public void Dispose() => Self.subscriptions.Remove(Observer);
    }
}
```

`CallJs7.razor.cs`:

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages;

public partial class CallJs7 : 
    ComponentBase, IObservable<ElementReference>, IDisposable
{
    private bool disposing;
    private readonly List<IObserver<ElementReference>> subscriptions = [];
    private ElementReference title;

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        foreach (var subscription in subscriptions)
        {
            subscription.OnNext(title);
        }
    }

    public void Dispose()
    {
        disposing = true;

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnCompleted();
            }
            catch (Exception)
            {
            }
        }

        subscriptions.Clear();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }

    public IDisposable Subscribe(IObserver<ElementReference> observer)
    {
        if (disposing)
        {
            throw new InvalidOperationException("Parent being disposed");
        }

        subscriptions.Add(observer);

        return new Subscription(observer, this);
    }

    private class Subscription(IObserver<ElementReference> observer,
        CallJs7 self) : IDisposable
    {
        public IObserver<ElementReference> Observer { get; } = observer;
        public CallJs7 Self { get; } = self;

        public void Dispose() => Self.subscriptions.Remove(Observer);
    }
}
```

`CallJsExample7.razor.cs`:

```
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages;

public partial class CallJsExample7 : 
    ComponentBase, IObservable<ElementReference>, IDisposable
{
    private bool disposing;
    private IList<IObserver<ElementReference>> subscriptions = 
        new List<IObserver<ElementReference>>();
    private ElementReference title;

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        foreach (var subscription in subscriptions)
        {
            subscription.OnNext(title);
        }
    }

    public void Dispose()
    {
        disposing = true;

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnCompleted();
            }
            catch (Exception)
            {
            }
        }

        subscriptions.Clear();
    }

    public IDisposable Subscribe(IObserver<ElementReference> observer)
    {
        if (disposing)
        {
            throw new InvalidOperationException("Parent being disposed");
        }

        subscriptions.Add(observer);

        return new Subscription(observer, this);
    }

    private class Subscription : IDisposable
    {
        public Subscription(IObserver<ElementReference> observer, 
            CallJsExample7 self)
        {
            Observer = observer;
            Self = self;
        }

        public IObserver<ElementReference> Observer { get; }
        public CallJsExample7 Self { get; }

        public void Dispose()
        {
            Self.subscriptions.Remove(Observer);
        }
    }
}
```

`CallJsExample7.razor.cs`:

```
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages;

public partial class CallJsExample7 : 
    ComponentBase, IObservable<ElementReference>, IDisposable
{
    private bool disposing;
    private IList<IObserver<ElementReference>> subscriptions = 
        new List<IObserver<ElementReference>>();
    private ElementReference title;

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        foreach (var subscription in subscriptions)
        {
            subscription.OnNext(title);
        }
    }

    public void Dispose()
    {
        disposing = true;

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnCompleted();
            }
            catch (Exception)
            {
            }
        }

        subscriptions.Clear();
    }

    public IDisposable Subscribe(IObserver<ElementReference> observer)
    {
        if (disposing)
        {
            throw new InvalidOperationException("Parent being disposed");
        }

        subscriptions.Add(observer);

        return new Subscription(observer, this);
    }

    private class Subscription : IDisposable
    {
        public Subscription(IObserver<ElementReference> observer, 
            CallJsExample7 self)
        {
            Observer = observer;
            Self = self;
        }

        public IObserver<ElementReference> Observer { get; }
        public CallJsExample7 Self { get; }

        public void Dispose()
        {
            Self.subscriptions.Remove(Observer);
        }
    }
}
```

`CallJsExample7.razor.cs`:

```
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                subscription.OnNext(title);
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}
```

`CallJsExample7.razor.cs`:

```
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                subscription.OnNext(title);
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}
```

In the preceding example, the namespace of the app is `BlazorSample`. If testing the code locally, update the namespace.

`SurveyPrompt.razor` (child component):

```
<div class="alert alert-secondary mt-4">
    <span class="oi oi-pencil me-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2186158">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    // Demonstrates how a parent component can supply parameters
    [Parameter]
    public string? Title { get; set; }
}
```

```
<div class="alert alert-secondary mt-4">
    <span class="oi oi-pencil me-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2186158">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    // Demonstrates how a parent component can supply parameters
    [Parameter]
    public string? Title { get; set; }
}
```

```
<div class="alert alert-secondary mt-4">
    <span class="oi oi-pencil me-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2186157">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    // Demonstrates how a parent component can supply parameters
    [Parameter]
    public string? Title { get; set; }
}
```

```
<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string? Title { get; set; }
}
```

```
<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

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

```
<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

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

`SurveyPrompt.razor.cs`:

```
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Components;

public partial class SurveyPrompt : 
    ComponentBase, IObserver<ElementReference>, IDisposable
{
    private IDisposable? subscription = null;

    [Parameter]
    public IObservable<ElementReference>? Parent { get; set; }

    [Inject]
    public IJSRuntime? JS {get; set;}

    protected override void OnParametersSet()
    {
        base.OnParametersSet();

        subscription?.Dispose();
        subscription = Parent?.Subscribe(this);
    }

    public void OnCompleted() => subscription = null;

    public void OnError(Exception error) => subscription = null;

    public void OnNext(ElementReference value) =>
        _ = (JS?.InvokeAsync<object>("setElementClass", [ value, "red" ]));

    public void Dispose()
    {
        subscription?.Dispose();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
```

```
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Components;

public partial class SurveyPrompt : 
    ComponentBase, IObserver<ElementReference>, IDisposable
{
    private IDisposable? subscription = null;

    [Parameter]
    public IObservable<ElementReference>? Parent { get; set; }

    [Inject]
    public IJSRuntime? JS {get; set;}

    protected override void OnParametersSet()
    {
        base.OnParametersSet();

        subscription?.Dispose();
        subscription = Parent?.Subscribe(this);
    }

    public void OnCompleted() => subscription = null;

    public void OnError(Exception error) => subscription = null;

    public void OnNext(ElementReference value) =>
        _ = (JS?.InvokeAsync<object>("setElementClass", [ value, "red" ]));

    public void Dispose()
    {
        subscription?.Dispose();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
```

```
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Shared;

public partial class SurveyPrompt : 
    ComponentBase, IObserver<ElementReference>, IDisposable
{
    private IDisposable? subscription = null;

    [Parameter]
    public IObservable<ElementReference>? Parent { get; set; }

    [Inject]
    public IJSRuntime? JS {get; set;}

    protected override void OnParametersSet()
    {
        base.OnParametersSet();

        subscription?.Dispose();
        subscription = 
            Parent is not null ? Parent.Subscribe(this) : null;
    }

    public void OnCompleted()
    {
        subscription = null;
    }

    public void OnError(Exception error)
    {
        subscription = null;
    }

    public void OnNext(ElementReference value)
    {
        JS?.InvokeAsync<object>(
            "setElementClass", new object[] { value, "red" });
    }

    public void Dispose()
    {
        subscription?.Dispose();
    }
}
```

```
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Shared;

public partial class SurveyPrompt : 
    ComponentBase, IObserver<ElementReference>, IDisposable
{
    private IDisposable? subscription = null;

    [Parameter]
    public IObservable<ElementReference>? Parent { get; set; }

    [Inject]
    public IJSRuntime? JS {get; set;}

    protected override void OnParametersSet()
    {
        base.OnParametersSet();

        subscription?.Dispose();
        subscription = 
            Parent is not null ? Parent.Subscribe(this) : null;
    }

    public void OnCompleted()
    {
        subscription = null;
    }

    public void OnError(Exception error)
    {
        subscription = null;
    }

    public void OnNext(ElementReference value)
    {
        JS?.InvokeAsync<object>(
            "setElementClass", new object[] { value, "red" });
    }

    public void Dispose()
    {
        subscription?.Dispose();
    }
}
```

```
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        [Inject]
        public IJSRuntime JS {get; set;}

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS?.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}
```

```
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        [Inject]
        public IJSRuntime JS {get; set;}

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS?.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}
```

In the preceding example, the namespace of the app is `BlazorSample` with shared components in the `Shared` folder. If testing the code locally, update the namespace.

## Harden JavaScript interop calls

*This section only applies to Interactive Server components, but client-side components may also set JS interop timeouts if conditions warrant it.*

In server-side apps with server interactivity, JavaScript (JS) interop may fail due to networking errors and should be treated as unreliable. Blazor apps use a one minute timeout for JS interop calls. If an app can tolerate a more aggressive timeout, set the timeout using one of the following approaches.

Set a global timeout in the `Program.cs` with [CircuitOptions.JSInteropDefaultCallTimeout](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.jsinteropdefaultcalltimeout#microsoft-aspnetcore-components-server-circuitoptions-jsinteropdefaultcalltimeout):

```
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents(options => 
        options.JSInteropDefaultCallTimeout = {TIMEOUT});
```

```
builder.Services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});
```

Set a global timeout in the `Startup.ConfigureServices` method of `Startup.cs` with [CircuitOptions.JSInteropDefaultCallTimeout](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.jsinteropdefaultcalltimeout#microsoft-aspnetcore-components-server-circuitoptions-jsinteropdefaultcalltimeout):

```
services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});
```

The `{TIMEOUT}` placeholder is a [TimeSpan](/en-us/dotnet/api/system.timespan) (for example, `TimeSpan.FromSeconds(80)`).

Set a per-invocation timeout in component code. The specified timeout overrides the global timeout set by [JSInteropDefaultCallTimeout](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.jsinteropdefaultcalltimeout#microsoft-aspnetcore-components-server-circuitoptions-jsinteropdefaultcalltimeout):

```
var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, [ "Arg1" ]);
```

```
var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1" });
```

In the preceding example:

* The `{TIMEOUT}` placeholder is a [TimeSpan](/en-us/dotnet/api/system.timespan) (for example, `TimeSpan.FromSeconds(80)`).
* The `{ID}` placeholder is the identifier for the function to invoke. For example, the value `someScope.someFunction` invokes the function `window.someScope.someFunction`.

Although a common cause of JS interop failures are network failures with server-side components, per-invocation timeouts can be set for JS interop calls for client-side components. Although no Blazor-SignalR circuit exists for a client-side component, JS interop calls might fail for other reasons that apply.

For more information on resource exhaustion, see [Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering](../security/interactive-server-side-rendering?view=aspnetcore-10.0).

## Avoid circular object references

Objects that contain circular references can't be serialized on the client for either:

* .NET method calls.
* JavaScript method calls from C# when the return type has circular references.

## JavaScript libraries that render UI

Sometimes you may wish to use JavaScript (JS) libraries that produce visible user interface elements within the browser DOM. At first glance, this might seem difficult because Blazor's diffing system relies on having control over the tree of DOM elements and runs into errors if some external code mutates the DOM tree and invalidates its mechanism for applying diffs. This isn't a Blazor-specific limitation. The same challenge occurs with any diff-based UI framework.

Fortunately, it's straightforward to embed externally-generated UI within a Razor component UI reliably. The recommended technique is to have the component's code (`.razor` file) produce an empty element. As far as Blazor's diffing system is concerned, the element is always empty, so the renderer does not recurse into the element and instead leaves its contents alone. This makes it safe to populate the element with arbitrary externally-managed content.

The following example demonstrates the concept. Within the `if` statement when `firstRender` is `true`, interact with `unmanagedElement` outside of Blazor using JS interop. For example, call an external JS library to populate the element. Blazor leaves the element's contents alone until this component is removed. When the component is removed, the component's entire DOM subtree is also removed.

```
<h1>Hello! This is a Razor component rendered at @DateTime.Now</h1>

<div @ref="unmanagedElement"></div>

@code {
    private ElementReference unmanagedElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            ...
        }
    }
}
```

Consider the following example that renders an interactive map using [open-source Mapbox APIs](https://www.mapbox.com/).

The following JS module is placed into the app or made available from a Razor class library.

Note

To create the [Mapbox](https://www.mapbox.com/) map, obtain an access token from [Mapbox Sign in](https://account.mapbox.com) and provide it where the `{ACCESS TOKEN}` appears in the following code.

`wwwroot/mapComponent.js`:

```
import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';

mapboxgl.accessToken = '{ACCESS TOKEN}';

export function addMapToElement(element) {
  return new mapboxgl.Map({
    container: element,
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [-74.5, 40],
    zoom: 9
  });
}

export function setMapCenter(map, latitude, longitude) {
  map.setCenter([longitude, latitude]);
}
```

To produce correct styling, add the following stylesheet tag to the host HTML page.

Add the following `<link>` element to the `<head>` element markup ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)):

```
<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
    rel="stylesheet" />
```

`CallJs8.razor`:

```
@page "/call-js-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 8</PageTitle>

<HeadContent>
    <link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
        rel="stylesheet" />
</HeadContent>

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference? mapModule;
    private IJSObjectReference? mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
    {
        if (mapModule is not null && mapInstance is not null)
        {
            await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, 
                latitude, longitude);
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            try
            {
                await mapInstance.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }

        if (mapModule is not null)
        {
            try
            {
                await mapModule.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

`CallJs8.razor`:

```
@page "/call-js-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 8</PageTitle>

<HeadContent>
    <link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
        rel="stylesheet" />
</HeadContent>

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference? mapModule;
    private IJSObjectReference? mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
    {
        if (mapModule is not null && mapInstance is not null)
        {
            await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, 
                latitude, longitude);
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            try
            {
                await mapInstance.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }

        if (mapModule is not null)
        {
            try
            {
                await mapModule.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

`CallJsExample8.razor`:

```
@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference? mapModule;
    private IJSObjectReference? mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
    {
        if (mapModule is not null && mapInstance is not null)
        {
            await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, 
                latitude, longitude);
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}
```

`CallJsExample8.razor`:

```
@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference? mapModule;
    private IJSObjectReference? mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
    {
        if (mapModule is not null && mapInstance is not null)
        {
            await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, 
                latitude, longitude);
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}
```

The preceding example produces an interactive map UI. The user:

* Can drag to scroll or zoom.
* Select buttons to jump to predefined locations.

In the preceding example:

* The `<div>` with `@ref="mapElement"` is left empty as far as Blazor is concerned. The `mapbox-gl.js` script can safely populate the element and modify its contents. Use this technique with any JS library that renders UI. You can embed components from a third-party JS SPA framework inside Razor components, as long as they don't try to reach out and modify other parts of the page. It is **not** safe for external JS code to modify elements that Blazor does not regard as empty.
* When using this approach, bear in mind the rules about how Blazor retains or destroys DOM elements. The component safely handles button click events and updates the existing map instance because DOM elements are retained where possible. If you were rendering a list of map elements from inside a `@foreach` loop, you want to use `@key` to ensure the preservation of component instances. Otherwise, changes in the list data could cause component instances to retain the state of previous instances in an undesirable manner. For more information, see how to [use the `@key` directive attribute to preserve the relationship among elements, components, and model objects](../components/element-component-model-relationships?view=aspnetcore-10.0).
* The example encapsulates JS logic and dependencies within a JavaScript module and loads the module dynamically using the `import` identifier. For more information, see [JavaScript isolation in JavaScript modules](#javascript-isolation-in-javascript-modules).

## Byte array support

Blazor supports optimized byte array JavaScript (JS) interop that avoids encoding/decoding byte arrays into Base64. The following example uses JS interop to pass a byte array to JavaScript.

Provide a `receiveByteArray` JS function. The function is called with [InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync) and doesn't return a value:

```
<script>
  window.receiveByteArray = (bytes) => {
    let utf8decoder = new TextDecoder();
    let str = utf8decoder.decode(bytes);
    return str;
  };
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

`CallJs9.razor`:

```
@page "/call-js-9"
@inject IJSRuntime JS

<h1>Call JS Example 9</h1>

<p>
    <button @onclick="SendByteArray">Send Bytes</button>
</p>

<p>
    @result
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    private string? result;

    private async Task SendByteArray()
    {
        var bytes = new byte[] { 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69,
            0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, 0x2c,
            0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x61, 0x6e, 0x2e, 0x20, 0x4e,
            0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e };

        result = await JS.InvokeAsync<string>("receiveByteArray", bytes);
    }
}
```

`CallJsExample9.razor`:

```
@page "/call-js-example-9"
@inject IJSRuntime JS

<h1>Call JS Example 9</h1>

<p>
    <button @onclick="SendByteArray">Send Bytes</button>
</p>

<p>
    @result
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    private string? result;

    private async Task SendByteArray()
    {
        var bytes = new byte[] { 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69,
            0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, 0x2c,
            0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x61, 0x6e, 0x2e, 0x20, 0x4e,
            0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e };

        result = await JS.InvokeAsync<string>("receiveByteArray", bytes);
    }
}
```

For information on using a byte array when calling .NET from JavaScript, see [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](call-dotnet-from-javascript?view=aspnetcore-10.0#byte-array-support).

## Stream from .NET to JavaScript

Blazor supports streaming data directly from .NET to JavaScript (JS). Streams are created using a [DotNetStreamReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetstreamreference).

[DotNetStreamReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetstreamreference) represents a .NET stream and uses the following parameters:

* `stream`: The stream sent to JS.
* `leaveOpen`: Determines if the stream is left open after transmission. If a value isn't provided, `leaveOpen` defaults to `false`.

In JS, use an array buffer or a readable stream to receive the data:

* Using an [`ArrayBuffer`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer):

  ```
  async function streamToJavaScript(streamRef) {
    const data = await streamRef.arrayBuffer();
  }
  ```

* Using a [`ReadableStream`](https://developer.mozilla.org/docs/Web/API/ReadableStream):

  ```
  async function streamToJavaScript(streamRef) {
    const stream = await streamRef.stream();
  }
  ```

In C# code:

```
var streamRef = new DotNetStreamReference(stream: {STREAM}, leaveOpen: false);
await JS.InvokeVoidAsync("streamToJavaScript", streamRef);
```

In the preceding example:

* The `{STREAM}` placeholder represents the [Stream](/en-us/dotnet/api/system.io.stream) sent to JS.
* `JS` is an injected [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) instance.

Disposing a [DotNetStreamReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetstreamreference) instance is usually unnecessary. When `leaveOpen` is set to its default value of `false`, the underlying [Stream](/en-us/dotnet/api/system.io.stream) is automatically disposed after transmission to JS.

If `leaveOpen` is `true`, then disposing a [DotNetStreamReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetstreamreference) doesn't dispose its underlying [Stream](/en-us/dotnet/api/system.io.stream). The app's code determines when to dispose the underlying [Stream](/en-us/dotnet/api/system.io.stream). When deciding how to dispose the underlying [Stream](/en-us/dotnet/api/system.io.stream), consider the following:

* Disposing a [Stream](/en-us/dotnet/api/system.io.stream) while it's being transmitted to JS is considered an application error and may cause an unhandled exception to occur.
* [Stream](/en-us/dotnet/api/system.io.stream) transmission begins as soon as the [DotNetStreamReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetstreamreference) is passed as an argument to a JS interop call, regardless of whether the stream is actually used in JS logic.

Given these characteristics, we recommend disposing the underlying [Stream](/en-us/dotnet/api/system.io.stream) only after it's fully consumed by JS (the promise returned by `arrayBuffer` or `stream` resolves). It follows that a [DotNetStreamReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetstreamreference) should only be passed to JS if it's unconditionally going to be consumed by JS logic.

[Call .NET methods from JavaScript functions in ASP.NET Core Blazor](call-dotnet-from-javascript?view=aspnetcore-10.0#stream-from-javascript-to-net) covers the reverse operation, streaming from JavaScript to .NET.

[ASP.NET Core Blazor file downloads](../file-downloads?view=aspnetcore-10.0) covers how to download a file in Blazor.

## Catch JavaScript exceptions

To catch JS exceptions, wrap the JS interop in a [`try`-`catch` block](/en-us/dotnet/csharp/fundamentals/exceptions/exception-handling) and catch a [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception).

In the following example, the `nonFunction` JS function doesn't exist. When the function isn't found, the [JSException](/en-us/dotnet/api/microsoft.jsinterop.jsexception) is trapped with a [Message](/en-us/dotnet/api/system.exception.message#system-exception-message) that indicates the following error:

> `Could not find 'nonFunction' ('nonFunction' was undefined).`

`CallJs11.razor`:

```
@page "/call-js-11"
@inject IJSRuntime JS

<PageTitle>Call JS 11</PageTitle>

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string? errorMessage;
    private string? result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}
```

`CallJs11.razor`:

```
@page "/call-js-11"
@inject IJSRuntime JS

<PageTitle>Call JS 11</PageTitle>

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string? errorMessage;
    private string? result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}
```

`CallJsExample11.razor`:

```
@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string? errorMessage;
    private string? result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}
```

`CallJsExample11.razor`:

```
@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string? errorMessage;
    private string? result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}
```

`CallJsExample11.razor`:

```
@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string errorMessage;
    private string result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}
```

`CallJsExample11.razor`:

```
@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string errorMessage;
    private string result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}
```

## Abort a long-running JavaScript function

Use a JS [AbortController](https://developer.mozilla.org/docs/Web/API/AbortController) with a [CancellationTokenSource](/en-us/dotnet/api/system.threading.cancellationtokensource) in the component to abort a long-running JavaScript function from C# code.

The following JS `Helpers` class contains a simulated long-running function, `longRunningFn`, to count continuously until the [`AbortController.signal`](https://developer.mozilla.org/docs/Web/API/AbortController/signal) indicates that [`AbortController.abort`](https://developer.mozilla.org/docs/Web/API/AbortController/abort) has been called. The `sleep` function is for demonstration purposes to simulate slow execution of the long-running function and wouldn't be present in production code. When a component calls `stopFn`, the `longRunningFn` is signalled to abort via the `while` loop conditional check on [`AbortSignal.aborted`](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted).

```
<script>
  class Helpers {
    static #controller = new AbortController();

    static async #sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }

    static async longRunningFn() {
      var i = 0;
      while (!this.#controller.signal.aborted) {
        i++;
        console.log(`longRunningFn: ${i}`);
        await this.#sleep(1000);
      }
    }

    static stopFn() {
      this.#controller.abort();
      console.log('longRunningFn aborted!');
    }
  }

  window.Helpers = Helpers;
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

The following component:

* Invokes the JS function `longRunningFn` when the **`Start Task`** button is selected. A [CancellationTokenSource](/en-us/dotnet/api/system.threading.cancellationtokensource) is used to manage the execution of the long-running function. [CancellationToken.Register](/en-us/dotnet/api/system.threading.cancellationtoken.register) sets a JS interop call delegate to execute the JS function `stopFn` when the [CancellationTokenSource.Token](/en-us/dotnet/api/system.threading.cancellationtokensource.token#system-threading-cancellationtokensource-token) is cancelled.
* When the **`Cancel Task`** button is selected, the [CancellationTokenSource.Token](/en-us/dotnet/api/system.threading.cancellationtokensource.token#system-threading-cancellationtokensource-token) is cancelled with a call to [Cancel](/en-us/dotnet/api/system.threading.cancellationtokensource.cancel).
* The [CancellationTokenSource](/en-us/dotnet/api/system.threading.cancellationtokensource) is disposed in the `Dispose` method.

`CallJs12.razor`:

```
@page "/call-js-12"
@inject IJSRuntime JS

<h1>Cancel long-running JS interop</h1>

<p>
    <button @onclick="StartTask">Start Task</button>
    <button @onclick="CancelTask">Cancel Task</button>
</p>

@code {
    private CancellationTokenSource? cts;

    private async Task StartTask()
    {
        cts = new CancellationTokenSource();
        cts.Token.Register(() => JS.InvokeVoidAsync("Helpers.stopFn"));

        await JS.InvokeVoidAsync("Helpers.longRunningFn");
    }

    private void CancelTask()
    {
        cts?.Cancel();
    }

    public void Dispose()
    {
        cts?.Cancel();
        cts?.Dispose();
    }
}
```

`CallJsExample12.razor`:

```
@page "/call-js-example-12"
@inject IJSRuntime JS

<h1>Cancel long-running JS interop</h1>

<p>
    <button @onclick="StartTask">Start Task</button>
    <button @onclick="CancelTask">Cancel Task</button>
</p>

@code {
    private CancellationTokenSource? cts;

    private async Task StartTask()
    {
        cts = new CancellationTokenSource();
        cts.Token.Register(() => JS.InvokeVoidAsync("Helpers.stopFn"));

        await JS.InvokeVoidAsync("Helpers.longRunningFn");
    }

    private void CancelTask()
    {
        cts?.Cancel();
    }

    public void Dispose()
    {
        cts?.Cancel();
        cts?.Dispose();
    }
}
```

A browser's [developer tools](https://developer.mozilla.org/docs/Glossary/Developer_Tools) console indicates the execution of the long-running JS function after the **`Start Task`** button is selected and when the function is aborted after the **`Cancel Task`** button is selected:

```
longRunningFn: 1
longRunningFn: 2
longRunningFn: 3
longRunningFn aborted!
```

## JavaScript `[JSImport]`/`[JSExport]` interop

*This section applies to client-side components.*

As an alternative to interacting with JavaScript (JS) in client-side components using Blazor's JS interop mechanism based on the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) interface, a JS `[JSImport]`/`[JSExport]` interop API is available to apps targeting .NET 7 or later.

For more information, see [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](import-export-interop?view=aspnetcore-10.0).

## Unmarshalled JavaScript interop

*This section applies to client-side components.*

Unmarshalled interop using the [IJSUnmarshalledRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) interface is obsolete and should be replaced with JavaScript `[JSImport]`/`[JSExport]` interop.

For more information, see [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](import-export-interop?view=aspnetcore-10.0).

## Unmarshalled JavaScript interop

Blazor WebAssembly components may experience poor performance when .NET objects are serialized for JavaScript (JS) interop and either of the following are true:

* A high volume of .NET objects are rapidly serialized. For example, poor performance may result when JS interop calls are made on the basis of moving an input device, such as spinning a mouse wheel.
* Large .NET objects or many .NET objects must be serialized for JS interop. For example, poor performance may result when JS interop calls require serializing dozens of files.

[IJSUnmarshalledObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledobjectreference) represents a reference to an JS object whose functions can be invoked without the overhead of serializing .NET data.

In the following example:

* A [struct](/en-us/dotnet/csharp/language-reference/builtin-types/struct) containing a string and an integer is passed unserialized to JS.
* JS functions process the data and return either a boolean or string to the caller.
* A JS string isn't directly convertible into a .NET `string` object. The `unmarshalledFunctionReturnString` function calls `BINDING.js_string_to_mono_string` to manage the conversion of a JS string.

Note

The following examples aren't typical use cases for this scenario because the [struct](/en-us/dotnet/csharp/language-reference/builtin-types/struct) passed to JS doesn't result in poor component performance. The example uses a small object merely to demonstrate the concepts for passing unserialized .NET data.

```
<script>
  window.returnObjectReference = () => {
    return {
      unmarshalledFunctionReturnBoolean: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return name === "Brigadier Alistair Gordon Lethbridge-Stewart" &&
            year === 1968;
      },
      unmarshalledFunctionReturnString: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return BINDING.js_string_to_mono_string(`Hello, ${name} (${year})!`);
      }
    };
  }
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

Warning

The `js_string_to_mono_string` function name, behavior, and existence is subject to change in a future release of .NET. For example:

* The function is likely to be renamed.
* The function itself might be removed in favor of automatic conversion of strings by the framework.

`CallJsExample10.razor`:

```
@page "/call-js-example-10"
@using System.Runtime.InteropServices
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Call JS Example 10</h1>

@if (callResultForBoolean)
{
    <p>JS interop was successful!</p>
}

@if (!string.IsNullOrEmpty(callResultForString))
{
    <p>@callResultForString</p>
}

<p>
    <button @onclick="CallJSUnmarshalledForBoolean">
        Call Unmarshalled JS & Return Boolean
    </button>
    <button @onclick="CallJSUnmarshalledForString">
        Call Unmarshalled JS & Return String
    </button>
</p>

<p>
    <a href="https://www.doctorwho.tv">Doctor Who</a>
    is a registered trademark of the <a href="https://www.bbc.com/">BBC</a>.
</p>

@code {
    private bool callResultForBoolean;
    private string? callResultForString;

    private void CallJSUnmarshalledForBoolean()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForBoolean = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>(
                "unmarshalledFunctionReturnBoolean", GetStruct());
    }

    private void CallJSUnmarshalledForString()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForString = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>(
                "unmarshalledFunctionReturnString", GetStruct());
    }

    private InteropStruct GetStruct()
    {
        return new InteropStruct
        {
            Name = "Brigadier Alistair Gordon Lethbridge-Stewart",
            Year = 1968,
        };
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InteropStruct
    {
        [FieldOffset(0)]
        public string Name;

        [FieldOffset(8)]
        public int Year;
    }
}
```

```
@page "/call-js-example-10"
@using System.Runtime.InteropServices
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Call JS Example 10</h1>

@if (callResultForBoolean)
{
    <p>JS interop was successful!</p>
}

@if (!string.IsNullOrEmpty(callResultForString))
{
    <p>@callResultForString</p>
}

<p>
    <button @onclick="CallJSUnmarshalledForBoolean">
        Call Unmarshalled JS & Return Boolean
    </button>
    <button @onclick="CallJSUnmarshalledForString">
        Call Unmarshalled JS & Return String
    </button>
</p>

<p>
    <a href="https://www.doctorwho.tv">Doctor Who</a>
    is a registered trademark of the <a href="https://www.bbc.com/">BBC</a>.
</p>

@code {
    private bool callResultForBoolean;
    private string callResultForString;

    private void CallJSUnmarshalledForBoolean()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForBoolean = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>(
                "unmarshalledFunctionReturnBoolean", GetStruct());
    }

    private void CallJSUnmarshalledForString()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForString = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>(
                "unmarshalledFunctionReturnString", GetStruct());
    }

    private InteropStruct GetStruct()
    {
        return new InteropStruct
        {
            Name = "Brigadier Alistair Gordon Lethbridge-Stewart",
            Year = 1968,
        };
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InteropStruct
    {
        [FieldOffset(0)]
        public string Name;

        [FieldOffset(8)]
        public int Year;
    }
}
```

If an [IJSUnmarshalledObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledobjectreference) instance isn't disposed in C# code, it can be disposed in JS. The following `dispose` function disposes the object reference when called from JS:

```
window.exampleJSObjectReferenceNotDisposedInCSharp = () => {
  return {
    dispose: function () {
      DotNet.disposeJSObjectReference(this);
    },

    ...
  };
}
```

Array types can be converted from JS objects into .NET objects using `js_typed_array_to_array`, but the JS array must be a typed array. Arrays from JS can be read in C# code as a .NET object array (`object[]`).

Other data types, such as string arrays, can be converted but require creating a new Mono array object (`mono_obj_array_new`) and setting its value (`mono_obj_array_set`).

Warning

JS functions provided by the Blazor framework, such as `js_typed_array_to_array`, `mono_obj_array_new`, and `mono_obj_array_set`, are subject to name changes, behavioral changes, or removal in future releases of .NET.

## Disposal of JavaScript interop object references

Examples throughout the JavaScript (JS) interop articles demonstrate typical object disposal patterns:

* When calling JS from .NET, as described in this article, dispose any created [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference)/[IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference)/`JSObjectReference` either from .NET or from JS to avoid leaking JS memory.

* When calling .NET from JS, as described in [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](call-dotnet-from-javascript?view=aspnetcore-10.0), dispose of a created [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) either from .NET or from JS to avoid leaking .NET memory.

* [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](call-dotnet-from-javascript?view=aspnetcore-10.0)
* [`InteropComponent.razor` example (`dotnet/AspNetCore` GitHub repository `main` branch)](https://github.com/dotnet/AspNetCore/blob/main/src/Components/test/testassets/BasicTestApp/InteropComponent.razor): The `main` branch represents the product unit's current development for the next release of ASP.NET Core. To select the branch for a different release (for example, `release/{VERSION}`, where the `{VERSION}` placeholder is the release version), use the **Switch branches or tags** dropdown list to select the branch. For a branch that no longer exists, use the **Tags** tab to find the API (for example, `v7.0.0`).
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))
* [Handle errors in ASP.NET Core Blazor apps](../fundamentals/handle-errors?view=aspnetcore-10.0#javascript-interop) (*JavaScript interop* section)
* [Threat mitigation: JavaScript functions invoked from .NET](../security/interactive-server-side-rendering?view=aspnetcore-10.0#javascript-functions-invoked-from-net)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/mvc1001
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/mvc1001.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# MVC1001: Filters cannot be applied to page handler methods

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | MVC1001      |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

An attribute implementing [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata) was applied to a Razor Page handler method.

### Rule description

Razor Page handler methods are selected after MVC filter execution has started, and consequently cannot contribute filters to execute. Applying a filter to a Razor Page handler is unsupported and always incorrect.

```
public class IndexModel : PageModel
{
    [MyFilter]
    public IActionResult OnGet() => Page();
}
```

## How to fix violations

Remove the filter from the handler and apply it to the page model. If a filter has to be applied to a specific handler, consider using multiple Razor Pages.

```
[MyFilter]
public class IndexModel : PageModel
{
    public IActionResult OnGet() => Page();
}
```

## When to suppress warnings

Don't suppress warnings from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/authenticated-encryption-details
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/implementation/authenticated-encryption-details.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Authenticated encryption details in ASP.NET Core

[]()

Calls to IDataProtector.Protect are authenticated encryption operations. The Protect method offers both confidentiality and authenticity, and it's tied to the purpose chain that was used to derive this particular IDataProtector instance from its root IDataProtectionProvider.

IDataProtector.Protect takes a byte\[] plaintext parameter and produces a byte\[] protected payload, whose format is described below. (There's also an extension method overload which takes a string plaintext parameter and returns a string protected payload. If this API is used the protected payload format will still have the below structure, but it will be [base64url-encoded](https://tools.ietf.org/html/rfc4648#section-5).)

## Protected payload format

The protected payload format consists of three primary components:

* A 32-bit magic header that identifies the version of the data protection system.

* A 128-bit key id that identifies the key used to protect this particular payload.

* The remainder of the protected payload is [specific to the encryptor encapsulated by this key](subkeyderivation?view=aspnetcore-10.0#data-protection-implementation-subkey-derivation). In the example below, the key represents an AES-256-CBC + HMACSHA256 encryptor, and the payload is further subdivided as follows:

  * A 128-bit key modifier.
  * A 128-bit initialization vector.
  * 48 bytes of AES-256-CBC output.
  * An HMACSHA256 authentication tag.

A sample protected payload is illustrated below.

```
09 F0 C9 F0 80 9C 81 0C 19 66 19 40 95 36 53 F8
AA FF EE 57 57 2F 40 4C 3F 7F CC 9D CC D9 32 3E
84 17 99 16 EC BA 1F 4A A1 18 45 1F 2D 13 7A 28
79 6B 86 9C F8 B7 84 F9 26 31 FC B1 86 0A F1 56
61 CF 14 58 D3 51 6F CF 36 50 85 82 08 2D 3F 73
5F B0 AD 9E 1A B2 AE 13 57 90 C8 F5 7C 95 4E 6A
8A AA 06 EF 43 CA 19 62 84 7C 11 B2 C8 71 9D AA
52 19 2E 5B 4C 1E 54 F0 55 BE 88 92 12 C1 4B 5E
52 C9 74 A0
```

From the payload format above the first 32 bits, or 4 bytes are the magic header identifying the version (09 F0 C9 F0)

The next 128 bits, or 16 bytes is the key identifier (80 9C 81 0C 19 66 19 40 95 36 53 F8 AA FF EE 57)

The remainder contains the payload and is specific to the format used.

Warning

All payloads protected to a given key will begin with the same 20-byte (magic value, key id) header. Administrators can use this fact for diagnostic purposes to approximate when a payload was generated. For example, the payload above corresponds to key {aaaaaaaa-0b0b-1c1c-2d2d-333333333333}. If after checking the key repository you find that this specific key's activation date was 2015-01-01 and its expiration date was 2015-03-01, then it's reasonable to assume that the payload (if not tampered with) was generated within that window, give or take a small fudge factor on either side.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/dependency-injection
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/dependency-injection.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Dependency injection into views in ASP.NET Core

ASP.NET Core supports [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) into views. This can be useful for view-specific services, such as localization or data required only for populating view elements. Most of the data views display should be passed in from the controller.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/dependency-injection/samples/6.0) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Configuration injection

The values in settings files, such as `appsettings.json` and `appsettings.Development.json`, can be injected into a view. Consider the `appsettings.Development.json` from the [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/dependency-injection/samples/6.0):

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "MyRoot": {
    "MyParent": {
      "MyChildName": "Joe"
    }
  }
}
```

The following markup displays the configuration value in a Razor Pages view:

```
@page
@model PrivacyModel
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{
    ViewData["Title"] = "Privacy RP";
}
<h1>@ViewData["Title"]</h1>

<p>PR Privacy</p>

<h2>
   MyRoot:MyParent:MyChildName: @Configuration["MyRoot:MyParent:MyChildName"]
</h2>
```

The following markup displays the configuration value in a MVC view:

```
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{
    ViewData["Title"] = "Privacy MVC";
}
<h1>@ViewData["Title"]</h1>

<p>MVC Use this page to detail your site's privacy policy.</p>

<h2>
   MyRoot:MyParent:MyChildName: @Configuration["MyRoot:MyParent:MyChildName"]
</h2>
```

For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0)

## Service injection

A service can be injected into a view using the `@inject` directive.

```
@using System.Threading.Tasks
@using ViewInjectSample.Model
@using ViewInjectSample.Model.Services
@model IEnumerable<ToDoItem>
@inject StatisticsService StatsService
<!DOCTYPE html>
<html>
<head>
    <title>To Do Items</title>
</head>
<body>
    <div>
        <h1>To Do Items</h1>
        <ul>
            <li>Total Items: @StatsService.GetCount()</li>
            <li>Completed: @StatsService.GetCompletedCount()</li>
            <li>Avg. Priority: @StatsService.GetAveragePriority()</li>
        </ul>
        <table>
            <tr>
                <th>Name</th>
                <th>Priority</th>
                <th>Is Done?</th>
            </tr>
            @foreach (var item in Model)
            {
                <tr>
                    <td>@item.Name</td>
                    <td>@item.Priority</td>
                    <td>@item.IsDone</td>
                </tr>
            }
        </table>
    </div>
</body>
</html>
```

This view displays a list of `ToDoItem` instances, along with a summary showing overall statistics. The summary is populated from the injected `StatisticsService`. This service is registered for dependency injection in `ConfigureServices` in `Program.cs`:

```
using ViewInjectSample.Helpers;
using ViewInjectSample.Infrastructure;
using ViewInjectSample.Interfaces;
using ViewInjectSample.Model.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

builder.Services.AddTransient<IToDoItemRepository, ToDoItemRepository>();
builder.Services.AddTransient<StatisticsService>();
builder.Services.AddTransient<ProfileOptionsService>();
builder.Services.AddTransient<MyHtmlHelper>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.MapRazorPages();

app.MapDefaultControllerRoute();


app.Run();
```

The `StatisticsService` performs some calculations on the set of `ToDoItem` instances, which it accesses via a repository:

```
using System.Linq;
using ViewInjectSample.Interfaces;

namespace ViewInjectSample.Model.Services
{
    public class StatisticsService
    {
        private readonly IToDoItemRepository _toDoItemRepository;

        public StatisticsService(IToDoItemRepository toDoItemRepository)
        {
            _toDoItemRepository = toDoItemRepository;
        }

        public int GetCount()
        {
            return _toDoItemRepository.List().Count();
        }

        public int GetCompletedCount()
        {
            return _toDoItemRepository.List().Count(x => x.IsDone);
        }

        public double GetAveragePriority()
        {
            if (_toDoItemRepository.List().Count() == 0)
            {
                return 0.0;
            }

            return _toDoItemRepository.List().Average(x => x.Priority);
        }
    }
}
```

The sample repository uses an in-memory collection. An in-memory implementation shouldn't be used for large, remotely accessed data sets.

The sample displays data from the model bound to the view and the service injected into the view:

## Populating Lookup Data

View injection can be useful to populate options in UI elements, such as dropdown lists. Consider a user profile form that includes options for specifying gender, state, and other preferences. Rendering such a form using a standard approach might require the controller or Razor Page to:

* Request data access services for each of the sets of options.
* Populate a model or `ViewBag` with each set of options to be bound.

An alternative approach injects services directly into the view to obtain the options. This minimizes the amount of code required by the controller or razor Page, moving this view element construction logic into the view itself. The controller action or Razor Page to display a profile editing form only needs to pass the form the profile instance:

```
using Microsoft.AspNetCore.Mvc;
using ViewInjectSample.Model;

namespace ViewInjectSample.Controllers;

public class ProfileController : Controller
{
    public IActionResult Index()
    {
        // A real app would up profile based on the user.
        var profile = new Profile()
        {
            Name = "Rick",
            FavColor = "Blue",
            Gender = "Male",
            State = new State("Ohio","OH")
        };
        return View(profile);
    }
}
```

The HTML form used to update the preferences includes dropdown lists for three of the properties:

These lists are populated by a service that has been injected into the view:

```
@using System.Threading.Tasks
@using ViewInjectSample.Model.Services
@model ViewInjectSample.Model.Profile
@inject ProfileOptionsService Options
<!DOCTYPE html>
<html>
<head>
    <title>Update Profile</title>
</head>
<body>
<div>
    <h1>Update Profile</h1>
    Name: @Html.TextBoxFor(m => m.Name)
    <br/>
    Gender: @Html.DropDownList("Gender",
           Options.ListGenders().Select(g => 
                new SelectListItem() { Text = g, Value = g }))
    <br/>

    State: @Html.DropDownListFor(m => m.State!.Code,
           Options.ListStates().Select(s => 
                new SelectListItem() { Text = s.Name, Value = s.Code}))
    <br />

    Fav. Color: @Html.DropDownList("FavColor",
           Options.ListColors().Select(c => 
                new SelectListItem() { Text = c, Value = c }))
    </div>
</body>
</html>
```

The `ProfileOptionsService` is a UI-level service designed to provide just the data needed for this form:

```
namespace ViewInjectSample.Model.Services;

public class ProfileOptionsService
{
    public List<string> ListGenders()
    {
        // Basic sample
        return new List<string>() {"Female", "Male"};
    }

    public List<State> ListStates()
    {
        // Add a few states
        return new List<State>()
        {
            new State("Alabama", "AL"),
            new State("Alaska", "AK"),
            new State("Ohio", "OH")
        };
    }

    public List<string> ListColors()
    {
        return new List<string>() { "Blue","Green","Red","Yellow" };
    }
}
```

Note an unregistered type throws an exception at runtime because the service provider is internally queried via [GetRequiredService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.getrequiredservice).

## Overriding Services

In addition to injecting new services, this technique can be used to override previously injected services on a page. The figure below shows all of the fields available on the page used in the first example:

The default fields include `Html`, `Component`, and `Url`. To replace the default HTML Helpers with a custom version, use `@inject`:

```
@using System.Threading.Tasks
@using ViewInjectSample.Helpers
@inject MyHtmlHelper Html
<!DOCTYPE html>
<html>
<head>
    <title>My Helper</title>
</head>
<body>
    <div>
        Test: @Html.Value
    </div>
</body>
</html>
```

## See Also

* Simon Timms Blog: [Getting Lookup Data Into Your View](https://blog.simontimms.com/2015/06/09/getting-lookup-data-into-you-view/)

ASP.NET Core supports [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) into views. This can be useful for view-specific services, such as localization or data required only for populating view elements. You should try to maintain [separation of concerns](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#separation-of-concerns) between your controllers and views. Most of the data your views display should be passed in from the controller.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/dependency-injection/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Configuration injection

`appsettings.json` values can be injected directly into a view.

Example of an `appsettings.json` file:

```
{
   "root": {
      "parent": {
         "child": "myvalue"
      }
   }
}
```

The syntax for `@inject`: `@inject <type> <name>`

An example using `@inject`:

```
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration
@{
   string myValue = Configuration["root:parent:child"];
   ...
}
```

## Service injection

A service can be injected into a view using the `@inject` directive. You can think of `@inject` as adding a property to the view, and populating the property using DI.

```
@using System.Threading.Tasks
@using ViewInjectSample.Model
@using ViewInjectSample.Model.Services
@model IEnumerable<ToDoItem>
@inject StatisticsService StatsService
<!DOCTYPE html>
<html>
<head>
    <title>To Do Items</title>
</head>
<body>
    <div>
        <h1>To Do Items</h1>
        <ul>
            <li>Total Items: @StatsService.GetCount()</li>
            <li>Completed: @StatsService.GetCompletedCount()</li>
            <li>Avg. Priority: @StatsService.GetAveragePriority()</li>
        </ul>
        <table>
            <tr>
                <th>Name</th>
                <th>Priority</th>
                <th>Is Done?</th>
            </tr>
            @foreach (var item in Model)
            {
                <tr>
                    <td>@item.Name</td>
                    <td>@item.Priority</td>
                    <td>@item.IsDone</td>
                </tr>
            }
        </table>
    </div>
</body>
</html>
```

This view displays a list of `ToDoItem` instances, along with a summary showing overall statistics. The summary is populated from the injected `StatisticsService`. This service is registered for dependency injection in `ConfigureServices` in `Startup.cs`:

```
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
```

The `StatisticsService` performs some calculations on the set of `ToDoItem` instances, which it accesses via a repository:

```
using System.Linq;
using ViewInjectSample.Interfaces;

namespace ViewInjectSample.Model.Services
{
    public class StatisticsService
    {
        private readonly IToDoItemRepository _toDoItemRepository;

        public StatisticsService(IToDoItemRepository toDoItemRepository)
        {
            _toDoItemRepository = toDoItemRepository;
        }

        public int GetCount()
        {
            return _toDoItemRepository.List().Count();
        }

        public int GetCompletedCount()
        {
            return _toDoItemRepository.List().Count(x => x.IsDone);
        }

        public double GetAveragePriority()
        {
            if (_toDoItemRepository.List().Count() == 0)
            {
                return 0.0;
            }

            return _toDoItemRepository.List().Average(x => x.Priority);
        }
    }
}
```

The sample repository uses an in-memory collection. The implementation shown above (which operates on all of the data in memory) isn't recommended for large, remotely accessed data sets.

The sample displays data from the model bound to the view and the service injected into the view:

## Populating Lookup Data

View injection can be useful to populate options in UI elements, such as dropdown lists. Consider a user profile form that includes options for specifying gender, state, and other preferences. Rendering such a form using a standard MVC approach would require the controller to request data access services for each of these sets of options, and then populate a model or `ViewBag` with each set of options to be bound.

An alternative approach injects services directly into the view to obtain the options. This minimizes the amount of code required by the controller, moving this view element construction logic into the view itself. The controller action to display a profile editing form only needs to pass the form the profile instance:

```
using Microsoft.AspNetCore.Mvc;
using ViewInjectSample.Model;

namespace ViewInjectSample.Controllers
{
    public class ProfileController : Controller
    {
        [Route("Profile")]
        public IActionResult Index()
        {
            // TODO: look up profile based on logged-in user
            var profile = new Profile()
            {
                Name = "Steve",
                FavColor = "Blue",
                Gender = "Male",
                State = new State("Ohio","OH")
            };
            return View(profile);
        }
    }
}
```

The HTML form used to update these preferences includes dropdown lists for three of the properties:

These lists are populated by a service that has been injected into the view:

```
@using System.Threading.Tasks
@using ViewInjectSample.Model.Services
@model ViewInjectSample.Model.Profile
@inject ProfileOptionsService Options
<!DOCTYPE html>
<html>
<head>
    <title>Update Profile</title>
</head>
<body>
<div>
    <h1>Update Profile</h1>
    Name: @Html.TextBoxFor(m => m.Name)
    <br/>
    Gender: @Html.DropDownList("Gender",
           Options.ListGenders().Select(g => 
                new SelectListItem() { Text = g, Value = g }))
    <br/>

    State: @Html.DropDownListFor(m => m.State.Code,
           Options.ListStates().Select(s => 
                new SelectListItem() { Text = s.Name, Value = s.Code}))
    <br />

    Fav. Color: @Html.DropDownList("FavColor",
           Options.ListColors().Select(c => 
                new SelectListItem() { Text = c, Value = c }))
    </div>
</body>
</html>
```

The `ProfileOptionsService` is a UI-level service designed to provide just the data needed for this form:

```
using System.Collections.Generic;

namespace ViewInjectSample.Model.Services
{
    public class ProfileOptionsService
    {
        public List<string> ListGenders()
        {
            // keeping this simple
            return new List<string>() {"Female", "Male"};
        }

        public List<State> ListStates()
        {
            // a few states from USA
            return new List<State>()
            {
                new State("Alabama", "AL"),
                new State("Alaska", "AK"),
                new State("Ohio", "OH")
            };
        }

        public List<string> ListColors()
        {
            return new List<string>() { "Blue","Green","Red","Yellow" };
        }
    }
}
```

Important

Don't forget to register types you request through dependency injection in `Startup.ConfigureServices`. An unregistered type throws an exception at runtime because the service provider is internally queried via [GetRequiredService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.getrequiredservice).

## Overriding Services

In addition to injecting new services, this technique can also be used to override previously injected services on a page. The figure below shows all of the fields available on the page used in the first example:

As you can see, the default fields include `Html`, `Component`, and `Url` (as well as the `StatsService` that we injected). If for instance you wanted to replace the default HTML Helpers with your own, you could easily do so using `@inject`:

```
@using System.Threading.Tasks
@using ViewInjectSample.Helpers
@inject MyHtmlHelper Html
<!DOCTYPE html>
<html>
<head>
    <title>My Helper</title>
</head>
<body>
    <div>
        Test: @Html.Value
    </div>
</body>
</html>
```

If you want to extend existing services, you can simply use this technique while inheriting from or wrapping the existing implementation with your own.

## See Also

* Simon Timms Blog: [Getting Lookup Data Into Your View](https://blog.simontimms.com/2015/06/09/getting-lookup-data-into-you-view/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/standalone-with-authentication-library.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure an ASP.NET Core Blazor WebAssembly standalone app with the Authentication library

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to secure an ASP.NET Core Blazor WebAssembly standalone app with the Blazor WebAssembly Authentication library.

The Blazor WebAssembly Authentication library (`Authentication.js`) only supports the Proof Key for Code Exchange (PKCE) authorization code flow via the [Microsoft Authentication Library (MSAL, `msal.js`)](/en-us/entra/identity-platform/msal-overview). To implement other grant flows, access the MSAL guidance to implement MSAL directly, but we don't support or recommend the use of grant flows other than PKCE for Blazor apps.

*For Microsoft Entra (ME-ID) guidance, don't follow the guidance in this topic. See [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0).*

For additional security scenario coverage after reading this article, see [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0).

## Walkthrough

The subsections of the walkthrough explain how to:

* Register an app
* Create the Blazor app
* Run the app

### Register an app

Register an app with an [OpenID Connect (OIDC)](https://openid.net/developers/how-connect-works/) Identity Provider (IP) following the guidance provided by the maintainer of the IP.

Record the following information:

* Authority (for example, `https://accounts.google.com/`).
* Application (client) ID (for example, `2...7-e...q.apps.googleusercontent.com`).
* Additional IP configuration (see the IP's documentation).

Note

The IP must use OIDC. For example, Facebook's IP isn't an OIDC-compliant provider, so the guidance in this topic doesn't work with the Facebook IP. For more information, see [Secure ASP.NET Core Blazor WebAssembly](./?view=aspnetcore-10.0#authentication-library).

### Create the Blazor app

To create a [standalone Blazor WebAssembly app](../../hosting-models?view=aspnetcore-10.0#blazor-webassembly) that uses the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) library, follow the guidance for your choice of tooling. If adding support for authentication, see the [Parts of the app](#parts-of-the-app) section of this article for guidance on setting up and configuring the app.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / .NET CLI](#tabpanel_1_visual-studio-code+net-cli)

To create a new Blazor WebAssembly project with an authentication mechanism:

After choosing the **Blazor WebAssembly App** template, set the **Authentication type** to **Individual Accounts**.

After choosing the **Blazor WebAssembly App** template, set the **Authentication type** to **Individual Accounts**. Confirm that the **ASP.NET Core Hosted** checkbox isn't selected.

The **Individual Accounts** selection uses ASP.NET Core's [Identity](../../../security/authentication/identity?view=aspnetcore-10.0) system. This selection adds authentication support and doesn't result in storing users in a database. The following sections of this article provide further details.

Create a new Blazor WebAssembly project with an authentication mechanism in an empty folder. Specify the `Individual` authentication mechanism with the `-au|--auth` option to use ASP.NET Core's [Identity](../../../security/authentication/identity?view=aspnetcore-10.0) system. This selection adds authentication support and doesn't result in storing users in a database. The following sections of this article provide further details.

```
dotnet new blazorwasm -au Individual -o {PROJECT NAME}
```

| Placeholder      | Example        |
| ---------------- | -------------- |
| `{PROJECT NAME}` | `BlazorSample` |

The output location specified with the `-o|--output` option creates a project folder if it doesn't exist and becomes part of the project's name.

For more information, see the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) command in the .NET Guide.

### Configure the app

Configure the app following the IP's guidance. At a minimum, the app requires the `Local:Authority` and `Local:ClientId` configuration settings in the app's `wwwroot/appsettings.json` file:

```
{
  "Local": {
    "Authority": "{AUTHORITY}",
    "ClientId": "{CLIENT ID}"
  }
}
```

Google OAuth 2.0 OIDC example for an app that runs on the `localhost` address at port 5001:

```
{
  "Local": {
    "Authority": "https://accounts.google.com/",
    "ClientId": "2...7-e...q.apps.googleusercontent.com",
    "PostLogoutRedirectUri": "https://localhost:5001/authentication/logout-callback",
    "RedirectUri": "https://localhost:5001/authentication/login-callback",
    "ResponseType": "code"
  }
}
```

The redirect URI (`https://localhost:5001/authentication/login-callback`) is registered in the [Google APIs console](https://console.developers.google.com/apis/dashboard) in **Credentials** > **`{NAME}`** > **Authorized redirect URIs**, where `{NAME}` is the app's client name in the **OAuth 2.0 Client IDs** app list of the Google APIs console.

Note

Supplying the port number for a `localhost` redirect URI isn't required for some OIDC IPs per the [OAuth 2.0 specification](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3). Some IPs permit the redirect URI for loopback addresses to omit the port. Others allow the use of a wildcard for the port number (for example, `*`). For additional information, see the IP's documentation.

  ```
  @using Microsoft.Extensions.Options
  @inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> RemoteOptions
  ```

  Modify the component's redirect in the `OnInitialized` method:

  ```
  - Navigation.NavigateToLogin("authentication/login");
  + Navigation.NavigateToLogin(RemoteOptions.Get(Options.DefaultName)
  +     .AuthenticationPaths.LogInPath);
  ```

  Note

  If other paths differ from the project template's paths or [framework's default paths](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationDefaults.cs), manage them in the same fashion.

## Parts of the app

This section describes the parts of an app generated from the Blazor WebAssembly project template and how the app is configured. There's no specific guidance to follow in this section for a basic working application if you created the app using the guidance in the [Walkthrough](#walkthrough) section. The guidance in this section is helpful for updating an app to authenticate and authorize users. However, an alternative approach to updating an app is to create a new app from the guidance in the [Walkthrough](#walkthrough) section and moving the app's components, classes, and resources to the new app.

### Authentication package

When an app is created to use Individual Accounts, the app automatically receives a package reference for the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication). The package provides a set of primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

### Authentication service support

Support for authenticating users using OpenID Connect (OIDC) is registered in the service container with the [AddOidcAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addoidcauthentication) extension method provided by the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication).

The [AddOidcAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addoidcauthentication) method accepts a callback to configure the parameters required to authenticate an app using OIDC. The values required for configuring the app can be obtained from the OIDC-compliant IP. Obtain the values when you register the app, which typically occurs in their online portal.

For a new app, provide values for the `{AUTHORITY}` and `{CLIENT ID}` placeholders in the following configuration. Provide other configuration values that are required for use with the app's IP. The example is for Google, which requires `PostLogoutRedirectUri`, `RedirectUri`, and `ResponseType`. If adding authentication to an app, manually add the following code and configuration to the app with values for the placeholders and other configuration values.

In the `Program` file:

```
builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("Local", options.ProviderOptions);
});
```

### `wwwroot/appsettings.json` configuration

Configuration is supplied by the `wwwroot/appsettings.json` file:

```
{
  "Local": {
    "Authority": "{AUTHORITY}",
    "ClientId": "{CLIENT ID}"
  }
}
```

### Access token scopes

The Blazor WebAssembly template automatically configures default scopes for `openid` and `profile`.

The Blazor WebAssembly template doesn't automatically configure the app to request an access token for a secure API. To provision an access token as part of the sign-in flow, add the scope to the default token scopes of the [OidcProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.oidcprovideroptions). If adding authentication to an app, manually add the following code and configure the scope URI.

In the `Program` file:

```
builder.Services.AddOidcAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultScopes.Add("{SCOPE URI}");
});
```

For more information, see the following sections of the *Additional scenarios* article:

* [Request additional access tokens](additional-scenarios?view=aspnetcore-10.0#request-additional-access-tokens)
* [Attach tokens to outgoing requests](additional-scenarios?view=aspnetcore-10.0#attach-tokens-to-outgoing-requests)

### Imports file

The [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespace is made available throughout the app via the imports file (`_Imports.razor`):

```
...
@using Microsoft.AspNetCore.Components.Authorization
...
```

### Index page

The Index page (`wwwroot/index.html`) page includes a script that defines the `AuthenticationService` in JavaScript. `AuthenticationService` handles the low-level details of the OIDC protocol. The app internally calls methods defined in the script to perform the authentication operations.

```
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
```

```
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string? Action { get; set; }
}
```

    * [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)
    * [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)
    * [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

  > info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

```
@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthenticationState { get; set; }

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}
```

### Inspect the content of a JSON Web Token (JWT)

To decode a JSON Web Token (JWT), use Microsoft's [jwt.ms](https://jwt.ms/) tool. Values in the UI never leave your browser.

Example encoded JWT (shortened for display):

> eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V\_bU55Ug\_PW4pLPr20tTS8Ct7\_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2\_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

```
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
  "sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]
```

## Related content

* [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0)

* [Unauthenticated or unauthorized web API requests in an app with a secure default client](additional-scenarios?view=aspnetcore-10.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client)

* [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0): Includes guidance on:

  * Using forwarded headers middleware to preserve HTTPS scheme information across proxy servers and internal networks.
  * Additional scenarios and use cases, including manual scheme configuration, request path changes for correct request routing, and forwarding the request scheme for Linux and non-IIS reverse proxies.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/publish-to-azure-webapp-using-vscode
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/publish-to-azure-webapp-using-vscode.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Publish an ASP.NET Core app to Azure with Visual Studio Code

With this tutorial, you'll learn how to create an ASP.Net Core MVC Application and deploy it within Visual Studio Code. The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs). This tutorial will work on Windows, macOS, or Linux environments. Be sure to use the correct path separating characters (`\` vs `/`) for your environment.

Important

**ASP.NET Core preview releases with Azure App Service**

ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core preview release, see [Deploy ASP.NET Core preview release to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service).

To troubleshoot an App Service deployment issue, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

## Prerequisites

* An Azure subscription. Get a [free Azure account](https://azure.microsoft.com/pricing/purchase-options/azure-account?cid=msft_learn) if you don't have one.

* [.NET SDK](https://dotnet.microsoft.com/download) (latest stable release).

* [Visual Studio Code](https://code.visualstudio.com/Download).

  * [C# Extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp).
  * [Azure App Service Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azureappservice). Use the extension to sign into Azure before proceeding.

## Create an ASP.Net Core MVC project

1. Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

2. Set your working directory (`cd`) to the directory that will contain the project.

3. Run the following commands:

   ```
   dotnet new mvc -o MyMVCapp
   code -r MyMVCapp
   ```

   For the preceding commands:

   * `dotnet new mvc -o MyMVCapp`
     * Creates a new ASP.NET Core MVC project in the *MyMVCapp* folder.

   * `code -r MyMVCapp`

     * Loads the `MyMVCapp.csproj` project file in Visual Studio Code.
     * Visual Studio Code updates the integrated terminal to the project directory.

Note

If a dialog box appears with **Required assets to build and debug are missing from 'MyMVCapp'. Add them?**, select **Yes**.

A new ASP.NET Core MVC project is created in a *MyMVCapp* folder with a structure similar to the following:

```
      appsettings.Development.json
      appsettings.json
<DIR> bin
<DIR> Controllers
<DIR> Models
      MyMVCapp.csproj
<DIR> obj
      Program.cs
<DIR> Properties
<DIR> Views
<DIR> wwwroot
```

A `.vscode` folder will be created under the project structure. It will contain utility files to help you build and debug your ASP.NET Core web app.

## Test the project

Before deploying the app to Azure, make sure it is running properly on your local machine.

1. Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal) (if needed).

2. Set up the a trusted HTTPS development certificate:

   * Trust the HTTPS development certificate by running the following command:

     ```
     dotnet dev-certs https --trust
     ```

     The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

     The preceding command displays the following dialog, provided the certificate was not previously trusted:

   * Select **Yes** if you agree to trust the development certificate.

     For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../security/enforcing-ssl?view=aspnetcore-10.0) article.

   For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

3. Run the following command:

   ```
   dotnet run
   ```

   The preceding command:

   * Starts [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0), ASP.NET Core's built-in web server.
   * Displays a URL to test the web app such as `http://localhost:<port>`, where `<port>` is the random port number set in `Properties\launchSettings.json` at project creation.

   The output shows messages similar to the following, indicating that the app is running and awaiting requests:

   ```
   $ dotnet run
   Building...
   info: Microsoft.Hosting.Lifetime[14]
       Now listening on: https://localhost:7064
   info: Microsoft.Hosting.Lifetime[14]
       Now listening on: http://localhost:5119
   info: Microsoft.Hosting.Lifetime[0]
       Application started. Press Ctrl+C to shut down.
   info: Microsoft.Hosting.Lifetime[0]
       Hosting environment: Development
   info: Microsoft.Hosting.Lifetime[0]
       Content root path: D:\Src\MyMVCapp\
   ```

4. `Ctrl`+*click* the HTTPS URL in the output to test the web app in a browser. In the example above, the URL is `https://localhost:7064`.

5. Press `Ctrl`+`C` in the integrated terminal to shut down the web app after testing it.

## Generate the deployment package locally

1. In the integrated terminal, use the following command to generate a `Release` package in a folder located at `bin/Publish`:

   ```
   dotnet publish -c Release -o ./bin/Publish
   ```

   A new `Publish` subfolder will be created in the `bin` folder. This folder contains the files to be deployed to Azure.

   [](publish-to-azure-webapp-using-vscode/_static/publish-folder.png?view=aspnetcore-10.0#lightbox)

## Publish to Azure App Service

Leveraging the Azure App Service extension for Visual Studio Code, follow the steps below to publish the website directly to the Azure App Service.

### Create a new Azure Web App resource

If you don't have an existing Azure Web App resource to publish to, you must create one.

1. In the Azure extension tab, in the **RESOURCES** pane, expand the subscription you wish to use.

2. Right-click **App Services** and select **Create New Web App...**.

3. Follow the prompts:

   1. Enter a unique name for the web app.
   2. Select the most recent stable .NET runtime (such as `.NET 6 (LTS)`). Do not select the ASP.NET runtime, which is for .NET Framework apps.
   3. Select your pricing tier. Free (F1) is acceptable for this tutorial.

### Publish to Azure

1. Right click the `bin\Publish` folder and select `Deploy to Web App...` and follow the prompts.

   1. Select the subscription where the Azure Web App resource is located.
   2. Select the Azure Web App resource to which you will publish.
   3. Select **Deploy** when prompted with a confirmation dialog.

2. Once the deployment is finished, click `Browse Website` to validate the deployment.

   Once you click `Browse Website`, you'll navigate to it using your default browser:

Tip

You can repeat the above steps to redeploy the app to the same Azure Web App resource as needed. Be sure to run `dotnet publish` again before you deploy to Azure.

## Next steps

* [Create your first Azure DevOps pipeline](/en-us/azure/devops/pipelines/create-first-pipeline)

## Related content

* [Azure App Service](/en-us/azure/app-service/app-service-web-overview)
* [Azure resource groups](/en-us/azure/azure-resource-manager/resource-group-overview#resource-groups)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/migrations
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-rp/migrations.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 4, Razor Pages with EF Core migrations in ASP.NET Core

By [Tom Dykstra](https://github.com/tdykstra), [Jon P Smith](https://twitter.com/thereformedprog), and [Rick Anderson](https://twitter.com/RickAndMSFT)

The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. For information about the tutorial series, see [the first tutorial](intro?view=aspnetcore-10.0).

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) and compare that code to what you created by following the tutorial.

This tutorial introduces the EF Core migrations feature for managing data model changes.

When a new app is developed, the data model changes frequently. Each time the model changes, the model gets out of sync with the database. This tutorial series started by configuring the Entity Framework to create the database if it doesn't exist. Each time the data model changes, the database needs to be dropped. The next time the app runs, the call to `EnsureCreated` re-creates the database to match the new data model. The `DbInitializer` class then runs to seed the new database.

This approach to keeping the DB in sync with the data model works well until the app needs to be deployed to production. When the app is running in production, it's usually storing data that needs to be maintained. The app can't start with a test DB each time a change is made (such as adding a new column). The EF Core Migrations feature solves this problem by enabling EF Core to update the DB schema instead of creating a new database.

Rather than dropping and recreating the database when the data model changes, migrations updates the schema and retains existing data.

Note

**SQLite limitations**

This tutorial uses the Entity Framework Core [migrations](/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli) feature where possible. Migrations updates the database schema to match changes in the data model. However, migrations only does the kinds of changes that the database engine supports, and SQLite's schema change capabilities are limited. For example, adding a column is supported, but removing a column is not supported. If a migration is created to remove a column, the `ef migrations add` command succeeds but the `ef database update` command fails.

The workaround for the SQLite limitations is to manually write migrations code to perform a table rebuild when something in the table changes. The code goes in the `Up` and `Down` methods for a migration and involves:

* Creating a new table.
* Copying data from the old table to the new table.
* Dropping the old table.
* Renaming the new table.

Writing database-specific code of this type is outside the scope of this tutorial. Instead, this tutorial drops and re-creates the database whenever an attempt to apply a migration would fail. For more information, see the following resources:

* [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations)
* [Customize migration code](/en-us/ef/core/managing-schemas/migrations/#customize-migration-code)
* [Data seeding](/en-us/ef/core/modeling/data-seeding)
* [SQLite ALTER TABLE statement](https://sqlite.org/lang_altertable.html)

## Drop the database

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Use **SQL Server Object Explorer** (SSOX) to delete the database, or run the following command in the **Package Manager Console** (PMC):

```
Drop-Database
```

* Run the following command at a command prompt to install the EF CLI:

  ```
  dotnet tool install --global dotnet-ef
  ```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

* In the command prompt, navigate to the project folder. The project folder contains the `ContosoUniversity.csproj` file.

* Delete the `CU.db` file, or run the following command:

  ```
  dotnet ef database drop --force
  ```

## Create an initial migration

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

Run the following commands in the PMC:

```
Add-Migration InitialCreate
Update-Database
```

Make sure the command prompt is in the project folder, and run the following commands:

```
dotnet ef migrations add InitialCreate
dotnet ef database update
```

### Remove EnsureCreated

This tutorial series started by using [EnsureCreated](/en-us/dotnet/api/microsoft.entityframeworkcore.infrastructure.databasefacade.ensurecreated). `EnsureCreated` doesn't create a migrations history table and so can't be used with migrations. It's designed for testing or rapid prototyping where the database is dropped and re-created frequently.

From this point forward, the tutorials will use migrations.

In `Program.cs`, delete the following line:

```
context.Database.EnsureCreated();
```

Run the app and verify that the database is seeded.

## Up and Down methods

The EF Core `migrations add` command generated code to create the database. This migrations code is in the `Migrations\<timestamp>_InitialCreate.cs` file. The `Up` method of the `InitialCreate` class creates the database tables that correspond to the data model entity sets. The `Down` method deletes them, as shown in the following example:

```
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;

namespace ContosoUniversity.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Course",
                columns: table => new
                {
                    CourseID = table.Column<int>(nullable: false),
                    Title = table.Column<string>(nullable: true),
                    Credits = table.Column<int>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Course", x => x.CourseID);
                });

            migrationBuilder.CreateTable(
                name: "Student",
                columns: table => new
                {
                    ID = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    LastName = table.Column<string>(nullable: true),
                    FirstMidName = table.Column<string>(nullable: true),
                    EnrollmentDate = table.Column<DateTime>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Student", x => x.ID);
                });

            migrationBuilder.CreateTable(
                name: "Enrollment",
                columns: table => new
                {
                    EnrollmentID = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    CourseID = table.Column<int>(nullable: false),
                    StudentID = table.Column<int>(nullable: false),
                    Grade = table.Column<int>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Enrollment", x => x.EnrollmentID);
                    table.ForeignKey(
                        name: "FK_Enrollment_Course_CourseID",
                        column: x => x.CourseID,
                        principalTable: "Course",
                        principalColumn: "CourseID",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_Enrollment_Student_StudentID",
                        column: x => x.StudentID,
                        principalTable: "Student",
                        principalColumn: "ID",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_Enrollment_CourseID",
                table: "Enrollment",
                column: "CourseID");

            migrationBuilder.CreateIndex(
                name: "IX_Enrollment_StudentID",
                table: "Enrollment",
                column: "StudentID");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Enrollment");

            migrationBuilder.DropTable(
                name: "Course");

            migrationBuilder.DropTable(
                name: "Student");
        }
    }
}
```

The preceding code is for the initial migration. The code:

* Was generated by the `migrations add InitialCreate` command.
* Is executed by the `database update` command.
* Creates a database for the data model specified by the database context class.

The migration name parameter (`InitialCreate` in the example) is used for the file name. The migration name can be any valid file name. It's best to choose a word or phrase that summarizes what is being done in the migration. For example, a migration that added a department table might be called "AddDepartmentTable."

## The migrations history table

* Use SSOX or SQLite tool to inspect the database.
* Notice the addition of an `__EFMigrationsHistory` table. The `__EFMigrationsHistory` table keeps track of which migrations have been applied to the database.
* View the data in the `__EFMigrationsHistory` table. It shows one row for the first migration.

## The data model snapshot

Migrations creates a *snapshot* of the current data model in `Migrations/SchoolContextModelSnapshot.cs`. When add a migration is added, EF determines what changed by comparing the current data model to the snapshot file.

Because the snapshot file tracks the state of the data model, a migration cannot be deleted by deleting the `<timestamp>_<migrationname>.cs` file. To back out the most recent migration, use the [`migrations remove`](/en-us/ef/core/managing-schemas/migrations/managing#remove-a-migration) command. `migrations remove` deletes the migration and ensures the snapshot is correctly reset. For more information, see [dotnet ef migrations remove](/en-us/ef/core/miscellaneous/cli/dotnet#dotnet-ef-migrations-remove).

See [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations) to remove all migrations.

## Applying migrations in production

We recommend that production apps **not** call [Database.Migrate](/en-us/dotnet/api/microsoft.entityframeworkcore.relationaldatabasefacadeextensions.migrate) at application startup. `Migrate` shouldn't be called from an app that is deployed to a server farm. If the app is scaled out to multiple server instances, it's hard to ensure database schema updates don't happen from multiple servers or conflict with read/write access.

Database migration should be done as part of deployment, and in a controlled way. Production database migration approaches include:

* Using migrations to create SQL scripts and using the SQL scripts in deployment.
* Running `dotnet ef database update` from a controlled environment.

## Troubleshooting

If the app uses SQL Server LocalDB and displays the following exception:

```
SqlException: Cannot open database "ContosoUniversity" requested by the login.
The login failed.
Login failed for user 'user name'.
```

The solution may be to run `dotnet ef database update` at a command prompt.

In this tutorial, the EF Core migrations feature for managing data model changes is used.

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples).

When a new app is developed, the data model changes frequently. Each time the model changes, the model gets out of sync with the database. This tutorial started by configuring the Entity Framework to create the database if it doesn't exist. Each time the data model changes:

* The DB is dropped.
* EF creates a new one that matches the model.
* The app seeds the DB with test data.

This approach to keeping the DB in sync with the data model works well until the app needs to be deployed to production. When the app is running in production, it's usually storing data that needs to be maintained. The app can't start with a test DB each time a change is made (such as adding a new column). The EF Core Migrations feature solves this problem by enabling EF Core to update the DB schema instead of creating a new DB.

Rather than dropping and recreating the DB when the data model changes, migrations updates the schema and retains existing data.

## Drop the database

Use **SQL Server Object Explorer** (SSOX) or the `database drop` command:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

In the **Package Manager Console** (PMC), run the following command:

```
Drop-Database
```

Run `Get-Help about_EntityFrameworkCore` from the PMC to get help information.

Open a command window and navigate to the project folder. The project folder contains the `Startup.cs` file.

Enter the following in the command window:

```
dotnet ef database drop
```

## Create an initial migration and update the DB

Build the project and create the first migration.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

```
Add-Migration InitialCreate
Update-Database
```

```
dotnet ef migrations add InitialCreate
dotnet ef database update
```

### Examine the Up and Down methods

The EF Core `migrations add` command generated code to create the database. This migrations code is in the `Migrations\<timestamp>_InitialCreate.cs` file. The `Up` method of the `InitialCreate` class creates the database tables that correspond to the data model entity sets. The `Down` method deletes them, as shown in the following example:

```
public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Course",
            columns: table => new
            {
                CourseID = table.Column<int>(nullable: false),
                Title = table.Column<string>(nullable: true),
                Credits = table.Column<int>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Course", x => x.CourseID);
            });

        migrationBuilder.CreateTable(
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Enrollment");

        migrationBuilder.DropTable(
            name: "Course");

        migrationBuilder.DropTable(
            name: "Student");
    }
}
```

Migrations calls the `Up` method to implement the data model changes for a migration. When a command is entered to roll back the update, migrations calls the `Down` method.

The preceding code is for the initial migration. That code was created when the `migrations add InitialCreate` command was run. The migration name parameter ("InitialCreate" in the example) is used for the file name. The migration name can be any valid file name. It's best to choose a word or phrase that summarizes what is being done in the migration. For example, a migration that added a department table might be called "AddDepartmentTable."

If the initial migration is created and the DB exists:

* The DB creation code is generated.
* The DB creation code doesn't need to run because the DB already matches the data model. If the DB creation code is run, it doesn't make any changes because the DB already matches the data model.

When the app is deployed to a new environment, the DB creation code must be run to create the DB.

Previously the DB was dropped and doesn't exist, so migrations creates the new DB.

### The data model snapshot

Migrations create a *snapshot* of the current database schema in `Migrations/SchoolContextModelSnapshot.cs`. When you add a migration, EF determines what changed by comparing the data model to the snapshot file.

To delete a migration, use the following command:

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

Remove-Migration

```
dotnet ef migrations remove
```

For more information, see [dotnet ef migrations remove](/en-us/ef/core/miscellaneous/cli/dotnet#dotnet-ef-migrations-remove).

The remove migrations command deletes the migration and ensures the snapshot is correctly reset.

### Remove EnsureCreated and test the app

For early development, `EnsureCreated` was used. In this tutorial, migrations are used. `EnsureCreated` has the following limitations:

* Bypasses migrations and creates the DB and schema.
* Doesn't create a migrations table.
* Can *not* be used with migrations.
* Is designed for testing or rapid prototyping where the DB is dropped and re-created frequently.

Remove `EnsureCreated`:

```
context.Database.EnsureCreated();
```

Run the app and verify the DB is seeded.

### Inspect the database

Use **SQL Server Object Explorer** to inspect the DB. Notice the addition of an `__EFMigrationsHistory` table. The `__EFMigrationsHistory` table keeps track of which migrations have been applied to the DB. View the data in the `__EFMigrationsHistory` table, it shows one row for the first migration. The last log in the preceding CLI output example shows the INSERT statement that creates this row.

Run the app and verify that everything works.

## Applying migrations in production

We recommend production apps should **not** call [Database.Migrate](/en-us/dotnet/api/microsoft.entityframeworkcore.relationaldatabasefacadeextensions.migrate) at application startup. `Migrate` shouldn't be called from an app in server farm. For example, if the app has been cloud deployed with scale-out (multiple instances of the app are running).

Database migration should be done as part of deployment, and in a controlled way. Production database migration approaches include:

* Using migrations to create SQL scripts and using the SQL scripts in deployment.
* Running `dotnet ef database update` from a controlled environment.

EF Core uses the `__MigrationsHistory` table to see if any migrations need to run. If the DB is up-to-date, no migration is run.

## Troubleshooting

Download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu21snapshots/Part-4-Migrations).

The app generates the following exception:

```
SqlException: Cannot open database "ContosoUniversity" requested by the login.
The login failed.
Login failed for user 'user name'.
```

Solution: Run `dotnet ef database update`

### Related content

* [EF Core CLI](/en-us/ef/core/miscellaneous/cli/dotnet).
* [dotnet ef migrations CLI commands](/en-us/ef/core/miscellaneous/cli/dotnet)
* [Package Manager Console (Visual Studio)](/en-us/ef/core/miscellaneous/cli/powershell)

- [YouTube version of this tutorial](https://www.youtube.com/watch?v=OWSUuMLKTJo)
- [.NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).
- [Package Manager Console (Visual Studio)](/en-us/ef/core/miscellaneous/cli/powershell)

## Next steps

The next tutorial builds out the data model, adding entity properties and new entities.

[Previous tutorial](sort-filter-page?view=aspnetcore-10.0) [Next tutorial](complex-data-model?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-05-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/static-server-rendering
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/javascript-interoperability/static-server-rendering.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to load JavaScript (JS) in a Blazor Web App with static server-side rendering (static SSR) and [enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling).

Some apps depend on JS to perform initialization tasks that are specific to each page. When using Blazor's enhanced navigation feature, which allows the user to avoid reloading the entire page, page-specific JS may not be executed again as expected each time an enhanced page navigation occurs.

To avoid this problem, we don't recommended relying on page-specific `<script>` elements placed outside of the layout file applied to the component. Instead, scripts should register an [`afterWebStarted` JS initializer](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers) to perform initialization logic and use an event listener to listen for page updates caused by enhanced navigation.

## Events

In the following event listener examples, the `{CALLBACK}` placeholder is the callback function.

* Enhanced navigation start (`enhancednavigationstart`) triggers a callback before an enhanced navigation occurs:

  ```
  blazor.addEventListener("enhancednavigationstart", {CALLBACK});
  ```

* Enhanced navigation end (`enhancednavigationend`) triggers a callback after an enhanced navigation occurs:

  ```
  blazor.addEventListener("enhancednavigationend", {CALLBACK});
  ```

* Enhanced navigation page load (`enhancedload`) triggers a callback each time the page updates due to an enhanced navigation, including [streaming updates](../components/rendering?view=aspnetcore-10.0#streaming-rendering):

  ```
  blazor.addEventListener("enhancedload", {CALLBACK});
  ```

To avoid this problem, we don't recommended relying on page-specific `<script>` elements placed outside of the layout file applied to the component. Instead, scripts should register an [`afterWebStarted` JS initializer](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers) to perform initialization logic and use an event listener to listen for page updates caused by enhanced navigation:

```
blazor.addEventListener("enhancedload", {CALLBACK});
```

In the preceding example, the `{CALLBACK}` placeholder is the callback function.

## Enhanced page load script example

The following example demonstrates one way to configure JS code to run when a statically-rendered page with enhanced navigation is initially loaded or updated.

The following `PageWithScript` component example is a component in the app that requires scripts to run with static SSR and enhanced navigation. The following component example includes a `PageScript` component from a Razor class library (RCL) that's added to the solution later in this article.

`Components/Pages/PageWithScript.razor`:

```
@page "/page-with-script"
@using BlazorPageScript

<PageTitle>Enhanced Load Script Example</PageTitle>

<PageScript Src="./Components/Pages/PageWithScript.razor.js" />

Welcome to my page.
```

In the Blazor Web App, add the following [collocated JS file](location-of-javascript?view=aspnetcore-10.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component):

* `onLoad` is called when the script is added to the page.
* `onUpdate` is called when the script still exists on the page after an enhanced update.
* `onDispose` is called when the script is removed from the page after an enhanced update.

`Components/Pages/PageWithScript.razor.js`:

```
export function onLoad() {
  console.log('Loaded');
}

export function onUpdate() {
  console.log('Updated');
}

export function onDispose() {
  console.log('Disposed');
}
```

In a [Razor Class Library (RCL)](../components/class-libraries?view=aspnetcore-10.0) (the example RCL is named `BlazorPageScript`), add the following module, which is a [JS initializer](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers).

`wwwroot/BlazorPageScript.lib.module.js`:

```
const pageScriptInfoBySrc = new Map();

function registerPageScriptElement(src) {
  if (!src) {
    throw new Error('Must provide a non-empty value for the "src" attribute.');
  }

  let pageScriptInfo = pageScriptInfoBySrc.get(src);

  if (pageScriptInfo) {
    pageScriptInfo.referenceCount++;
  } else {
    pageScriptInfo = { referenceCount: 1, module: null };
    pageScriptInfoBySrc.set(src, pageScriptInfo);
    initializePageScriptModule(src, pageScriptInfo);
  }
}

function unregisterPageScriptElement(src) {
  if (!src) {
    return;
  }

  const pageScriptInfo = pageScriptInfoBySrc.get(src);
  
  if (!pageScriptInfo) {
    return;
  }

  pageScriptInfo.referenceCount--;
}

async function initializePageScriptModule(src, pageScriptInfo) {
  if (src.startsWith("./")) {
    src = new URL(src.substr(2), document.baseURI).toString();
  }

  const module = await import(src);

  if (pageScriptInfo.referenceCount <= 0) {
    return;
  }

  pageScriptInfo.module = module;
  module.onLoad?.();
  module.onUpdate?.();
}

function onEnhancedLoad() {
  for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) {
    if (referenceCount <= 0) {
      module?.onDispose?.();
      pageScriptInfoBySrc.delete(src);
    }
  }

  for (const { module } of pageScriptInfoBySrc.values()) {
    module?.onUpdate?.();
  }
}

export function afterWebStarted(blazor) {
  customElements.define('page-script', class extends HTMLElement {
    static observedAttributes = ['src'];

    attributeChangedCallback(name, oldValue, newValue) {
      if (name !== 'src') {
        return;
      }

      this.src = newValue;
      unregisterPageScriptElement(oldValue);
      registerPageScriptElement(newValue);
    }

    disconnectedCallback() {
      unregisterPageScriptElement(this.src);
    }
  });

  blazor.addEventListener('enhancedload', onEnhancedLoad);
}
```

Do ***not*** add a `<script>` tag to the app's root component, typically the `App` componennt, for `BlazorPageScript.lib.module.js` because the module in this case is a [JS initializer (`afterWebStarted`)](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers). JS initializers are detected and loaded automatically by the Blazor framework.

In the RCL, add the following `PageScript` component.

`PageScript.razor`:

```
<page-script src="@Src"></page-script>

@code {
    [Parameter]
    [EditorRequired]
    public string Src { get; set; } = default!;
}
```

The `PageScript` component functions normally on the top-level of a page.

If you place the `PageScript` component in an app's layout (for example, `MainLayout.razor`), which results in a shared `PageScript` among pages that use the layout, then the component only runs `onLoad` after a full page reload and `onUpdate` when any enhanced page update occurs, including enhanced navigation.

To reuse the same module among pages, but have the `onLoad` and `onDispose` callbacks invoked on each page change, append a query string to the end of the script so that it's recognized as a different module. An app could adopt the convention of using the component's name as the query string value. In the following example, the query string is "`counter`" because this `PageScript` component reference is placed in a `Counter` component. This is merely a suggestion, and you can use whatever query string scheme that you prefer.

```
<PageScript Src="./Components/Pages/PageWithScript.razor.js?counter" />
```

To monitor changes in specific DOM elements, use the [`MutationObserver`](https://developer.mozilla.org/docs/Web/API/MutationObserver) pattern in JS on the client. For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#dom-cleanup-tasks-during-component-disposal).

## Implementation without using an RCL

The approach described in this article can be implemented directly in a Blazor Web App without using a Razor class library (RCL) by moving the assets of the RCL into the app. However, the use of an RCL is convenient because the RCL can be packaged into a NuGet package for consumption by Blazor apps across an organization.

If you move the assets into a Blazor Web App, be sure to rename the module (`BlazorPageScript.lib.module.js`) to match the app per the file naming rules for [JS initializers](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers). If the file isn't named correctly, Blazor can't detect and load the module, and the `afterWebStarted` event doesn't execute automatically when the app starts.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/publish-to-azure-api-management-using-vs
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/publish-to-azure-api-management-using-vs.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Publish an ASP.NET Core web API to Azure API Management with Visual Studio

By [Matt Soucoup](https://twitter.com/codemillmatt)

In this tutorial you'll learn how to create an ASP.NET Core web API project using Visual Studio, ensure it has OpenAPI support, and then publish the web API to both Azure App Service and Azure API Management.

## Set up

To complete the tutorial you'll need an Azure account.

* Open a [free Azure account](https://azure.microsoft.com/pricing/purchase-options/azure-account?cid=msft_learn) if you don't have one.

## Create an ASP.NET Core web API

Visual Studio allows you to easily create a new ASP.NET Core web API project from a template. Follow these directions to create a new ASP.NET Core web API project:

* From the File menu, select **New** > **Project**.
* Enter **Web API** in the search box.
* Select the **ASP.NET Core Web API** template and select **Next**.
* In the **Configure your new project dialog**, name the project **WeatherAPI** and select **Next**.
* In the **Additional information** dialog:
* Confirm the Framework is **.NET 6.0 (Long-term support)**.
* Confirm the checkbox for **Use controllers (uncheck to use Minimal APIs)** is checked.
* Confirm the checkbox for **Enable OpenAPI support** is checked.
* Select **Create**.

## Explore the code

Swagger definitions allow Azure API Management to read the app's API definitions. By checking the **Enable OpenAPI support** checkbox during app creation, Visual Studio automatically adds the code to create the Swagger definitions. Open up the `Program.cs` file which shows the following code:

```

...

builder.Services.AddSwaggerGen();

...

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(); // Protected by if (env.IsDevelopment())
}

...
```

### Ensure the Swagger definitions are always generated

Azure API Management needs the Swagger definitions to always be present, regardless of the application's environment. To ensure they are always generated, move `app.UseSwagger();` outside of the `if (app.Environment.IsDevelopment())` block.

The updated code:

```

...

app.UseSwagger();

if (app.Environment.IsDevelopment())
{
    app.UseSwaggerUI();
}

...
```

### Change the API routing

Change the URL structure needed to access the `Get` action of the `WeatherForecastController`. Complete the following steps:

1. Open the `WeatherForecastController.cs` file.

2. Replace the `[Route("[controller]")]` class-level attribute with `[Route("/")]`. The updated class definition :

   ```
   [ApiController]
   [Route("/")]
   public class WeatherForecastController : ControllerBase
   ```

## Publish the web API to Azure App Service

Complete the following steps to publish the ASP.NET Core web API to Azure API Management:

1. Publish the API app to Azure App Service.
2. Publish the ASP.NET Core web API app to the Azure API Management service instance.

### Publish the API app to Azure App Service

Complete the following steps to publish the ASP.NET Core web API to Azure API Management:

1. In **Solution Explorer**, right-click the project and select **Publish**.

2. In the **Publish** dialog, select **Azure** and select the **Next** button.

3. Select **Azure App Service (Windows)** and select the **Next** button.

4. Select **Create a new Azure App Service**.

   The **Create App Service** dialog appears. The **App Name**, **Resource Group**, and **App Service Plan** entry fields are populated. You can keep these names or change them.

5. Select the **Create** button.

6. Once the app service is created, select the **Next** button.

7. Select **Create a new API Management Service**.

   The **Create API Management Service** dialog appears. You can leave the **API Name**, **Subscription Name**, and **Resource Group** entry fields as they are. Select the **new** button next to the **API Management Service** entry and enter the required fields from that dialog box.

   Select the **OK** button to create the API Management service.

8. Select the **Create** button to proceed with the API Management service creation. This step may take several minutes to complete.

9. When that completes, select the **Finish** button.

10. The dialog closes and a summary screen appears with information about the publish. Select the **Publish** button.

    The web API publishes to both Azure App Service and Azure API Management. A new browser window will appear and show the API running in Azure App Service. You can close that window.

11. Open up the Azure portal in a web browser and navigate to the API Management instance you created.

12. Select the **APIs** option from the left-hand menu.

13. Select the API you created in the preceding steps. It's now populated and you can explore around.

### Configure the published API name

Notice the name of the API is named *WeatherAPI*; however, we would like to call it *Weather Forecasts*. Complete the following steps to update the name:

1. Add the following to `Program.cs` immediately after `services.AddSwaggerGen();`

   ```
   builder.Services.ConfigureSwaggerGen(setup =>
   {
       setup.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
       {
           Title = "Weather Forecasts",
           Version = "v1"
       });
   });
   ```

2. Republish the ASP.NET Core web API and open the Azure API Management instance in the Azure portal.

3. Refresh the page in your browser. You'll see the name of the API is now correct.

### Verify the web API is working

You can test the deployed ASP.NET Core web API in Azure API Management from the Azure portal with the following steps:

1. Open the **Test** tab.
2. Select **/** or the **Get** operation.
3. Select **Send**.

## Clean up

When you've finished testing the app, go to the [Azure portal](https://portal.azure.com/) and delete the app.

1. Select **Resource groups**, then select the resource group you created.

2. In the **Resource groups** page, select **Delete**.

3. Enter the name of the resource group and select **Delete**. Your app and all other resources created in this tutorial are now deleted from Azure.

## Related content

* [Azure API Management](/en-us/azure/api-management/api-management-key-concepts)
* [Azure App Service](/en-us/azure/app-service/app-service-web-overview)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/update-related-data
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/update-related-data.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Update related data - ASP.NET MVC with EF Core

In the previous tutorial you displayed related data; in this tutorial you'll update related data by updating foreign key fields and navigation properties.

The following illustrations show some of the pages that you'll work with.

In this tutorial, you:

* Customize Courses pages
* Add Instructors Edit page
* Add courses to Edit page
* Update Delete page
* Add office location and courses to Create page

## Prerequisites

* [Read related data](read-related-data?view=aspnetcore-10.0)

## Customize Courses pages

When a new `Course` entity is created, it must have a relationship to an existing department. To facilitate this, the scaffolded code includes controller methods and Create and Edit views that include a drop-down list for selecting the department. The drop-down list sets the `Course.DepartmentID` foreign key property, and that's all the Entity Framework needs in order to load the `Department` navigation property with the appropriate `Department` entity. You'll use the scaffolded code, but change it slightly to add error handling and sort the drop-down list.

In `CoursesController.cs`, delete the four Create and Edit methods and replace them with the following code:

```
public IActionResult Create()
{
    PopulateDepartmentsDropDownList();
    return View();
}
```

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("CourseID,Credits,DepartmentID,Title")] Course course)
{
    if (ModelState.IsValid)
    {
        _context.Add(course);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}
```

```
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}
```

```
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var courseToUpdate = await _context.Courses
        .FirstOrDefaultAsync(c => c.CourseID == id);

    if (await TryUpdateModelAsync<Course>(courseToUpdate,
        "",
        c => c.Credits, c => c.DepartmentID, c => c.Title))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
    }
    PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
    return View(courseToUpdate);
}
```

After the `Edit` HttpPost method, create a new method that loads department info for the drop-down list.

```
private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
{
    var departmentsQuery = from d in _context.Departments
                           orderby d.Name
                           select d;
    ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(), "DepartmentID", "Name", selectedDepartment);
}
```

The `PopulateDepartmentsDropDownList` method gets a list of all departments sorted by name, creates a `SelectList` collection for a drop-down list, and passes the collection to the view in `ViewBag`. The method accepts the optional `selectedDepartment` parameter that allows the calling code to specify the item that will be selected when the drop-down list is rendered. The view will pass the name "DepartmentID" to the `<select>` tag helper, and the helper then knows to look in the `ViewBag` object for a `SelectList` named "DepartmentID".

The HttpGet `Create` method calls the `PopulateDepartmentsDropDownList` method without setting the selected item, because for a new course the department isn't established yet:

```
public IActionResult Create()
{
    PopulateDepartmentsDropDownList();
    return View();
}
```

The HttpGet `Edit` method sets the selected item, based on the ID of the department that's already assigned to the course being edited:

```
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}
```

The HttpPost methods for both `Create` and `Edit` also include code that sets the selected item when they redisplay the page after an error. This ensures that when the page is redisplayed to show the error message, whatever department was selected stays selected.

### Add .AsNoTracking to Details and Delete methods

To optimize performance of the Course Details and Delete pages, add `AsNoTracking` calls in the `Details` and HttpGet `Delete` methods.

```
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }

    return View(course);
}
```

```
public async Task<IActionResult> Delete(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }

    return View(course);
}
```

### Modify the Course views

In `Views/Courses/Create.cshtml`, add a "Select Department" option to the **Department** drop-down list, change the caption from **DepartmentID** to **Department**, and add a validation message.

```
<div class="form-group">
    <label asp-for="Department" class="control-label"></label>
    <select asp-for="DepartmentID" class="form-control" asp-items="ViewBag.DepartmentID">
        <option value="">-- Select Department --</option>
    </select>
    <span asp-validation-for="DepartmentID" class="text-danger" />
</div>
```

In `Views/Courses/Edit.cshtml`, make the same change for the Department field that you just did in `Create.cshtml`.

Also in `Views/Courses/Edit.cshtml`, add a course number field before the **Title** field. Because the course number is the primary key, it's displayed, but it can't be changed.

```
<div class="form-group">
    <label asp-for="CourseID" class="control-label"></label>
    <div>@Html.DisplayFor(model => model.CourseID)</div>
</div>
```

There's already a hidden field (`<input type="hidden">`) for the course number in the Edit view. Adding a `<label>` tag helper doesn't eliminate the need for the hidden field because it doesn't cause the course number to be included in the posted data when the user clicks **Save** on the **Edit** page.

In `Views/Courses/Delete.cshtml`, add a course number field at the top and change department ID to department name.

```
@model ContosoUniversity.Models.Course

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Course</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.CourseID)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.CourseID)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Credits)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Credits)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-action="Index">Back to List</a>
        </div>
    </form>
</div>
```

In `Views/Courses/Details.cshtml`, make the same change that you just did for `Delete.cshtml`.

### Test the Course pages

Run the app, select the **Courses** tab, click **Create New**, and enter data for a new course:

Click **Create**. The Courses Index page is displayed with the new course added to the list. The department name in the Index page list comes from the navigation property, showing that the relationship was established correctly.

Click **Edit** on a course in the Courses Index page.

Change data on the page and click **Save**. The Courses Index page is displayed with the updated course data.

## Add Instructors Edit page

When you edit an instructor record, you want to be able to update the instructor's office assignment. The `Instructor` entity has a one-to-zero-or-one relationship with the `OfficeAssignment` entity, which means your code has to handle the following situations:

* If the user clears the office assignment and it originally had a value, delete the `OfficeAssignment` entity.

* If the user enters an office assignment value and it originally was empty, create a new `OfficeAssignment` entity.

* If the user changes the value of an office assignment, change the value in an existing `OfficeAssignment` entity.

### Update the Instructors controller

In `InstructorsController.cs`, change the code in the HttpGet `Edit` method so that it loads the Instructor entity's `OfficeAssignment` navigation property and calls `AsNoTracking`:

```
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructor = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);
    if (instructor == null)
    {
        return NotFound();
    }
    return View(instructor);
}
```

Replace the HttpPost `Edit` method with the following code to handle office assignment updates:

```
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructorToUpdate = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .FirstOrDefaultAsync(s => s.ID == id);

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    {
        if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
        {
            instructorToUpdate.OfficeAssignment = null;
        }
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
    }
    return View(instructorToUpdate);
}
```

The code does the following:

* Changes the method name to `EditPost` because the signature is now the same as the HttpGet `Edit` method (the `ActionName` attribute specifies that the `/Edit/` URL is still used).

* Gets the current `Instructor` entity from the database using eager loading for the `OfficeAssignment` navigation property. This is the same as what you did in the HttpGet `Edit` method.

* Updates the retrieved `Instructor` entity with values from the model binder. The `TryUpdateModel` overload enables you to declare the properties you want to include. This prevents over-posting, as explained in the [second tutorial](crud?view=aspnetcore-10.0).

  ```
  if (await TryUpdateModelAsync<Instructor>(
      instructorToUpdate,
      "",
      i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
  ```

* If the office location is blank, sets the `Instructor.OfficeAssignment` property to null so that the related row in the `OfficeAssignment` table will be deleted.

  ```
  if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
  {
      instructorToUpdate.OfficeAssignment = null;
  }
  ```

* Saves the changes to the database.

### Update the Instructor Edit view

In `Views/Instructors/Edit.cshtml`, add a new field for editing the office location, at the end before the **Save** button:

```
<div class="form-group">
    <label asp-for="OfficeAssignment.Location" class="control-label"></label>
    <input asp-for="OfficeAssignment.Location" class="form-control" />
    <span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>
```

Run the app, select the **Instructors** tab, and then click **Edit** on an instructor. Change the **Office Location** and click **Save**.

## Add courses to Edit page

Instructors may teach any number of courses. Now you'll enhance the Instructor Edit page by adding the ability to change course assignments using a group of checkboxes, as shown in the following screen shot:

The relationship between the `Course` and `Instructor` entities is many-to-many. To add and remove relationships, you add and remove entities to and from the `CourseAssignments` join entity set.

The UI that enables you to change which courses an instructor is assigned to is a group of checkboxes. A checkbox for every course in the database is displayed, and the ones that the instructor is currently assigned to are selected. The user can select or clear checkboxes to change course assignments. If the number of courses were much greater, you would probably want to use a different method of presenting the data in the view, but you'd use the same method of manipulating a join entity to create or delete relationships.

### Update the Instructors controller

To provide data to the view for the list of checkboxes, you'll use a view model class.

Create `AssignedCourseData.cs` in the *SchoolViewModels* folder and replace the existing code with the following code:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class AssignedCourseData
    {
        public int CourseID { get; set; }
        public string Title { get; set; }
        public bool Assigned { get; set; }
    }
}
```

In `InstructorsController.cs`, replace the HttpGet `Edit` method with the following code. The changes are highlighted.

```
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructor = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);
    if (instructor == null)
    {
        return NotFound();
    }
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)
{
    var allCourses = _context.Courses;
    var instructorCourses = new HashSet<int>(instructor.CourseAssignments.Select(c => c.CourseID));
    var viewModel = new List<AssignedCourseData>();
    foreach (var course in allCourses)
    {
        viewModel.Add(new AssignedCourseData
        {
            CourseID = course.CourseID,
            Title = course.Title,
            Assigned = instructorCourses.Contains(course.CourseID)
        });
    }
    ViewData["Courses"] = viewModel;
}
```

The code adds eager loading for the `Courses` navigation property and calls the new `PopulateAssignedCourseData` method to provide information for the checkbox array using the `AssignedCourseData` view model class.

The code in the `PopulateAssignedCourseData` method reads through all `Course` entities in order to load a list of courses using the view model class. For each course, the code checks whether the course exists in the instructor's `Courses` navigation property. To create efficient lookup when checking whether a course is assigned to the instructor, the courses assigned to the instructor are put into a `HashSet` collection. The `Assigned` property is set to true for courses the instructor is assigned to. The view will use this property to determine which checkboxes must be displayed as selected. Finally, the list is passed to the view in `ViewData`.

Next, add the code that's executed when the user clicks **Save**. Replace the `EditPost` method with the following code, and add a new method that updates the `Courses` navigation property of the Instructor entity.

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, string[] selectedCourses)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructorToUpdate = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
        .FirstOrDefaultAsync(m => m.ID == id);

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    {
        if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
        {
            instructorToUpdate.OfficeAssignment = null;
        }
        UpdateInstructorCourses(selectedCourses, instructorToUpdate);
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
    }
    UpdateInstructorCourses(selectedCourses, instructorToUpdate);
    PopulateAssignedCourseData(instructorToUpdate);
    return View(instructorToUpdate);
}
```

```
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}
```

The method signature is now different from the HttpGet `Edit` method, so the method name changes from `EditPost` back to `Edit`.

Since the view doesn't have a collection of Course entities, the model binder can't automatically update the `CourseAssignments` navigation property. Instead of using the model binder to update the `CourseAssignments` navigation property, you do that in the new `UpdateInstructorCourses` method. Therefore, you need to exclude the `CourseAssignments` property from model binding. This doesn't require any change to the code that calls `TryUpdateModel` because you're using the overload that requires explicit approval and `CourseAssignments` isn't in the include list.

If no checkboxes were selected, the code in `UpdateInstructorCourses` initializes the `CourseAssignments` navigation property with an empty collection and returns:

```
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}
```

The code then loops through all courses in the database and checks each course against the ones currently assigned to the instructor versus the ones that were selected in the view. To facilitate efficient lookups, the latter two collections are stored in `HashSet` objects.

If the checkbox for a course was selected but the course isn't in the `Instructor.CourseAssignments` navigation property, the course is added to the collection in the navigation property.

```
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}
```

If the checkbox for a course wasn't selected, but the course is in the `Instructor.CourseAssignments` navigation property, the course is removed from the navigation property.

```
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}
```

### Update the Instructor views

In `Views/Instructors/Edit.cshtml`, add a **Courses** field with an array of checkboxes by adding the following code immediately after the `div` elements for the **Office** field and before the `div` element for the **Save** button.

[]()

Note

When you paste the code in Visual Studio, line breaks might be changed in a way that breaks the code. If the code looks different after pasting, press Ctrl+Z one time to undo the automatic formatting. This will fix the line breaks so that they look like what you see here. The indentation doesn't have to be perfect, but the `@:</tr><tr>`, `@:<td>`, `@:</td>`, and `@:</tr>` lines must each be on a single line as shown or you'll get a runtime error. With the block of new code selected, press Tab three times to line up the new code with the existing code. This problem is fixed in Visual Studio 2019.

```
<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            <tr>
                @{
                    int cnt = 0;
                    List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses = ViewBag.Courses;

                    foreach (var course in courses)
                    {
                        if (cnt++ % 3 == 0)
                        {
                            @:</tr><tr>
                        }
                        @:<td>
                            <input type="checkbox"
                                   name="selectedCourses"
                                   value="@course.CourseID"
                                   @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                   @course.CourseID @:  @course.Title
                        @:</td>
                    }
                    @:</tr>
                }
        </table>
    </div>
</div>
```

This code creates an HTML table that has three columns. In each column is a checkbox followed by a caption that consists of the course number and title. The checkboxes all have the same name ("selectedCourses"), which informs the model binder that they're to be treated as a group. The value attribute of each checkbox is set to the value of `CourseID`. When the page is posted, the model binder passes an array to the controller that consists of the `CourseID` values for only the checkboxes which are selected.

When the checkboxes are initially rendered, those that are for courses assigned to the instructor have checked attributes, which selects them (displays them checked).

Run the app, select the **Instructors** tab, and click **Edit** on an instructor to see the **Edit** page.

Change some course assignments and click Save. The changes you make are reflected on the Index page.

Note

The approach taken here to edit instructor course data works well when there's a limited number of courses. For collections that are much larger, a different UI and a different updating method would be required.

## Update Delete page

In `InstructorsController.cs`, delete the `DeleteConfirmed` method and insert the following code in its place.

```
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    Instructor instructor = await _context.Instructors
        .Include(i => i.CourseAssignments)
        .SingleAsync(i => i.ID == id);

    var departments = await _context.Departments
        .Where(d => d.InstructorID == id)
        .ToListAsync();
    departments.ForEach(d => d.InstructorID = null);

    _context.Instructors.Remove(instructor);

    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}
```

This code makes the following changes:

* Does eager loading for the `CourseAssignments` navigation property. You have to include this or EF won't know about related `CourseAssignment` entities and won't delete them. To avoid needing to read them here you could configure cascade delete in the database.

* If the instructor to be deleted is assigned as administrator of any departments, removes the instructor assignment from those departments.

## Add office location and courses to Create page

In `InstructorsController.cs`, delete the HttpGet and HttpPost `Create` methods, and then add the following code in their place:

```
public IActionResult Create()
{
    var instructor = new Instructor();
    instructor.CourseAssignments = new List<CourseAssignment>();
    PopulateAssignedCourseData(instructor);
    return View();
}

// POST: Instructors/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("FirstMidName,HireDate,LastName,OfficeAssignment")] Instructor instructor, string[] selectedCourses)
{
    if (selectedCourses != null)
    {
        instructor.CourseAssignments = new List<CourseAssignment>();
        foreach (var course in selectedCourses)
        {
            var courseToAdd = new CourseAssignment { InstructorID = instructor.ID, CourseID = int.Parse(course) };
            instructor.CourseAssignments.Add(courseToAdd);
        }
    }
    if (ModelState.IsValid)
    {
        _context.Add(instructor);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}
```

This code is similar to what you saw for the `Edit` methods except that initially no courses are selected. The HttpGet `Create` method calls the `PopulateAssignedCourseData` method not because there might be courses selected but in order to provide an empty collection for the `foreach` loop in the view (otherwise the view code would throw a null reference exception).

The HttpPost `Create` method adds each selected course to the `CourseAssignments` navigation property before it checks for validation errors and adds the new instructor to the database. Courses are added even if there are model errors so that when there are model errors (for an example, the user keyed an invalid date), and the page is redisplayed with an error message, any course selections that were made are automatically restored.

Notice that in order to be able to add courses to the `CourseAssignments` navigation property you have to initialize the property as an empty collection:

```
instructor.CourseAssignments = new List<CourseAssignment>();
```

As an alternative to doing this in controller code, you could do it in the `Instructor` model by changing the property getter to automatically create the collection if it doesn't exist, as shown in the following example:

```
private ICollection<CourseAssignment> _courseAssignments;
public ICollection<CourseAssignment> CourseAssignments
{
    get
    {
        return _courseAssignments ?? (_courseAssignments = new List<CourseAssignment>());
    }
    set
    {
        _courseAssignments = value;
    }
}
```

If you modify the `CourseAssignments` property in this way, you can remove the explicit property initialization code in the controller.

In `Views/Instructor/Create.cshtml`, add an office location text box and checkboxes for courses before the Submit button. As in the case of the Edit page, [fix the formatting if Visual Studio reformats the code when you paste it](#notepad).

```
<div class="form-group">
    <label asp-for="OfficeAssignment.Location" class="control-label"></label>
    <input asp-for="OfficeAssignment.Location" class="form-control" />
    <span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            <tr>
                @{
                    int cnt = 0;
                    List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses = ViewBag.Courses;

                    foreach (var course in courses)
                    {
                        if (cnt++ % 3 == 0)
                        {
                            @:</tr><tr>
                        }
                        @:<td>
                            <input type="checkbox"
                                   name="selectedCourses"
                                   value="@course.CourseID"
                                   @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                   @course.CourseID @:  @course.Title
                            @:</td>
                    }
                    @:</tr>
                }
        </table>
    </div>
</div>
```

Test by running the app and creating an instructor.

## Handling Transactions

As explained in the [CRUD tutorial](crud?view=aspnetcore-10.0), the Entity Framework implicitly implements transactions. For scenarios where you need more control -- for example, if you want to include operations done outside of Entity Framework in a transaction -- see [Transactions](/en-us/ef/core/saving/transactions).

## Get the code

[Download or view the completed application.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final)

## Next steps

In this tutorial, you:

* Customized Courses pages
* Added Instructors Edit page
* Added courses to Edit page
* Updated Delete page
* Added office location and courses to Create page

Advance to the next tutorial to learn how to handle concurrency conflicts.

[Handle concurrency conflicts](concurrency?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/css-isolation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor CSS isolation

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Dave Brock](https://twitter.com/daveabrock)

This article explains how CSS isolation scopes CSS to Razor components, which can simplify CSS and avoid collisions with other components or libraries.

Isolate CSS styles to individual pages, views, and components to reduce or avoid:

* Dependencies on global styles that can be challenging to maintain.
* Style conflicts in nested content.

## Enable CSS isolation

To define component-specific styles, create a `.razor.css` file matching the name of the `.razor` file for the component in the same folder. The `.razor.css` file is a *scoped CSS file*.

For an `Example` component in an `Example.razor` file, create a file alongside the component named `Example.razor.css`. The `Example.razor.css` file must reside in the same folder as the `Example` component (`Example.razor`). The "`Example`" base name of the file is **not** case-sensitive.

`Example.razor`:

```
@page "/example"

<h1>Scoped CSS Example</h1>
```

`Example.razor.css`:

```
h1 { 
    color: brown;
    font-family: Tahoma, Geneva, Verdana, sans-serif;
}
```

**The styles defined in `Example.razor.css` are only applied to the rendered output of the `Example` component.** CSS isolation is applied to HTML elements in the matching Razor file. Any `h1` CSS declarations defined elsewhere in the app don't conflict with the `Example` component's styles.

Note

In order to guarantee style isolation when bundling occurs, importing CSS in Razor code blocks isn't supported.

## CSS isolation bundling

CSS isolation occurs at build time. Blazor rewrites CSS selectors to match markup rendered by the component. The rewritten CSS styles are bundled and produced as a static asset. The stylesheet is referenced inside the `<head>` tag ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)). The following `<link>` element is added to an app created from the Blazor project templates:

Blazor Web Apps:

```
<link href="@Assets["{PACKAGE ID/ASSEMBLY NAME}.styles.css"]" rel="stylesheet">
```

Standalone Blazor WebAssembly apps:

```
<link href="{PACKAGE ID/ASSEMBLY NAME}.styles.css" rel="stylesheet">
```

```
<link href="{PACKAGE ID/ASSEMBLY NAME}.styles.css" rel="stylesheet">
```

The `{PACKAGE ID/ASSEMBLY NAME}` placeholder is the project's package ID (`<PackageId>` in the project file) for a library or assembly name for an app.

The following example is from a hosted Blazor WebAssembly **Client** app. The app's assembly name is `BlazorSample.Client`, and the `<link>` is added by the Blazor WebAssembly project template when the project is created with the Hosted option (`-ho|--hosted` option using the .NET CLI or **ASP.NET Core Hosted** checkbox using Visual Studio):

```
<link href="BlazorSample.Client.styles.css" rel="stylesheet">
```

Within the bundled file, each component is associated with a scope identifier. For each styled component, an HTML attribute is appended with the format `b-{STRING}`, where the `{STRING}` placeholder is a ten-character string generated by the framework. The identifier is unique for each app. In the rendered `Counter` component, Blazor appends a scope identifier to the `h1` element:

```
<h1 b-3xxtam6d07>
```

The `{PACKAGE ID/ASSEMBLY NAME}.styles.css` file uses the scope identifier to group a style declaration with its component. The following example provides the style for the preceding `<h1>` element:

```
/* /Components/Pages/Counter.razor.rz.scp.css */
h1[b-3xxtam6d07] {
    color: brown;
}
```

At build time, a project bundle is created with the convention `obj/{CONFIGURATION}/{TARGET FRAMEWORK}/scopedcss/projectbundle/{PACKAGE ID/ASSEMBLY NAME}.bundle.scp.css`, where the placeholders are:

* `{CONFIGURATION}`: The app's build configuration (for example, `Debug`, `Release`).
* `{TARGET FRAMEWORK}`: The target framework (for example, `net6.0`).
* `{PACKAGE ID/ASSEMBLY NAME}`: The project's package ID (`<PackageId>` in the project file) for a library or assembly name for an app (for example, `BlazorSample`).

## Child component support

CSS isolation only applies to the component you associate with the format `{COMPONENT NAME}.razor.css`, where the `{COMPONENT NAME}` placeholder is usually the component name. To apply changes to a child component, use the `::deep` [pseudo-element](https://developer.mozilla.org/docs/Web/CSS/Pseudo-elements) to any descendant elements in the parent component's `.razor.css` file. The `::deep` pseudo-element selects elements that are *descendants* of an element's generated scope identifier.

The following example shows a parent component called `Parent` with a child component called `Child`.

`Parent.razor`:

```
@page "/parent"

<div>
    <h1>Parent component</h1>

    <Child />
</div>
```

`Child.razor`:

```
<h1>Child Component</h1>
```

Update the `h1` declaration in `Parent.razor.css` with the `::deep` pseudo-element to signify the `h1` style declaration must apply to the parent component and its children.

`Parent.razor.css`:

```
::deep h1 { 
    color: red;
}
```

The `h1` style now applies to the `Parent` and `Child` components without the need to create a separate scoped CSS file for the child component.

The `::deep` pseudo-element only works with descendant elements. The following markup applies the `h1` styles to components as expected. The parent component's scope identifier is applied to the `div` element, so the browser knows to inherit styles from the parent component.

`Parent.razor`:

```
<div>
    <h1>Parent</h1>

    <Child />
</div>
```

However, excluding the `div` element removes the descendant relationship. In the following example, the style is **not** applied to the child component.

`Parent.razor`:

```
<h1>Parent</h1>

<Child />
```

The `::deep` pseudo-element affects where the scope attribute is applied to the rule. When you define a CSS rule in a scoped CSS file, the scope is applied to the right most element. For example: `div > a` is transformed to `div > a[b-{STRING}]`, where the `{STRING}` placeholder is a ten-character string generated by the framework (for example, `b-3xxtam6d07`). If you instead want the rule to apply to a different selector, the `::deep` pseudo-element allows you do so. For example, `div ::deep > a` is transformed to `div[b-{STRING}] > a` (for example, `div[b-3xxtam6d07] > a`).

The ability to attach the `::deep` pseudo-element to any HTML element allows you to create scoped CSS styles that affect elements rendered by other components when you can determine the structure of the rendered HTML tags. For a component that renders an hyperlink tag (`<a>`) inside another component, ensure the component is wrapped in a `div` (or any other element) and use the rule `::deep > a` to create a style that's only applied to that component when the parent component renders.

Important

Scoped CSS only applies to ***HTML elements*** and not to Razor components or Tag Helpers, including elements with a Tag Helper applied, such as `<input asp-for="..." />`.

## CSS preprocessor support

CSS preprocessors are useful for improving CSS development by utilizing features such as variables, nesting, modules, mixins, and inheritance. While CSS isolation doesn't natively support CSS preprocessors such as Sass or Less, integrating CSS preprocessors is seamless as long as preprocessor compilation occurs before Blazor rewrites the CSS selectors during the build process. Using Visual Studio for example, configure existing preprocessor compilation as a **Before Build** task in the Visual Studio Task Runner Explorer.

Many third-party NuGet packages, such as [`AspNetCore.SassCompiler`](https://www.nuget.org/packages/AspNetCore.SassCompiler#readme-body-tab), can compile SASS/SCSS files at the beginning of the build process before CSS isolation occurs.

## CSS isolation configuration

CSS isolation is designed to work out-of-the-box but provides configuration for some advanced scenarios, such as when there are dependencies on existing tools or workflows.

### Customize scope identifier format

Scope identifiers use the format `b-{STRING}`, where the `{STRING}` placeholder is a ten-character string generated by the framework. To customize the scope identifier format, update the project file to a desired pattern:

```
<ItemGroup>
  <None Update="Components/Pages/Example.razor.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

In the preceding example, the CSS generated for `Example.razor.css` changes its scope identifier from `b-{STRING}` to `custom-scope-identifier`.

Use scope identifiers to achieve inheritance with scoped CSS files. In the following project file example, a `BaseComponent.razor.css` file contains common styles across components. A `DerivedComponent.razor.css` file inherits these styles.

```
<ItemGroup>
  <None Update="Components/Pages/BaseComponent.razor.css" CssScope="custom-scope-identifier" />
  <None Update="Components/Pages/DerivedComponent.razor.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

Use the wildcard (`*`) operator to share scope identifiers across multiple files:

```
<ItemGroup>
  <None Update="Components/Pages/*.razor.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

Scope identifiers use the format `b-{STRING}`, where the `{STRING}` placeholder is a ten-character string generated by the framework. To customize the scope identifier format, update the project file to a desired pattern:

```
<ItemGroup>
  <None Update="Pages/Example.razor.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

In the preceding example, the CSS generated for `Example.razor.css` changes its scope identifier from `b-{STRING}` to `custom-scope-identifier`.

Use scope identifiers to achieve inheritance with scoped CSS files. In the following project file example, a `BaseComponent.razor.css` file contains common styles across components. A `DerivedComponent.razor.css` file inherits these styles.

```
<ItemGroup>
  <None Update="Pages/BaseComponent.razor.css" CssScope="custom-scope-identifier" />
  <None Update="Pages/DerivedComponent.razor.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

Use the wildcard (`*`) operator to share scope identifiers across multiple files:

```
<ItemGroup>
  <None Update="Pages/*.razor.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

### Change base path for static web assets

The `scoped.styles.css` file is generated at the root of the app. In the project file, use the [`<StaticWebAssetBasePath>` property](../fundamentals/static-files?view=aspnetcore-10.0#static-web-asset-base-path) to change the default path. The following example places the `scoped.styles.css` file, and the rest of the app's assets, at the `_content` path:

```
<PropertyGroup>
  <StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>
```

### Disable automatic bundling

To opt out of how Blazor publishes and loads scoped files at runtime, use the `DisableScopedCssBundling` property. When using this property, it means other tools or processes are responsible for taking the isolated CSS files from the `obj` directory and publishing and loading them at runtime:

```
<PropertyGroup>
  <DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>
```

### Disable CSS isolation

Disable CSS isolation for a project by setting the `<ScopedCssEnabled>` property to `false` in the app's project file:

```
<ScopedCssEnabled>false</ScopedCssEnabled>
```

## Razor class library (RCL) support

Isolated styles for components in a NuGet package or [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0) are automatically bundled:

* The app uses CSS imports to reference the RCL's bundled styles. For a class library named `ClassLib` and a Blazor app with a `BlazorSample.styles.css` stylesheet, the RCL's stylesheet is imported at the top of the app's stylesheet:

  ```
  @import '_content/ClassLib/ClassLib.bundle.scp.css';
  ```

* The RCL's bundled styles aren't published as a static web asset of the app that consumes the styles.

For more information on RCLs, see the following articles:

* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](class-libraries?view=aspnetcore-10.0)
* [Reusable Razor UI in class libraries with ASP.NET Core](../../razor-pages/ui-class?view=aspnetcore-10.0)

## Related content

* [Razor Pages CSS isolation](../../razor-pages/?view=aspnetcore-10.0#css-isolation)
* [MVC CSS isolation](../../mvc/views/overview?view=aspnetcore-10.0#css-isolation)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/static-files
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/static-files.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Serve static files in ASP.NET Core apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Learn how to serve, secure, and optimize static files in ASP.NET Core apps by using Map Static Assets endpoint conventions or static file middleware. Static files, also called static assets, aren't dynamically generated and are served directly to clients, including HTML, CSS, images, and JavaScript.

For Blazor static files guidance, which adds to or supersedes the guidance in this article, see [ASP.NET Core Blazor static files](../blazor/fundamentals/static-files?view=aspnetcore-10.0).

To enable static file handling in ASP.NET Core, call [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets).

By default, store static files within the project's [web root](./?view=aspnetcore-10.0#web-root) directory. The default directory is `{CONTENT ROOT}/wwwroot`, where the `{CONTENT ROOT}` placeholder is the app's [content root](./?view=aspnetcore-10.0#content-root). Only files in the `wwwroot` folder are addressable, so you don't need to worry about the rest of your code.

Only files with specific file extensions mapped to supported media types are treated as static web assets.

Static web assets are discovered at build time and optimized using content-based [fingerprinting](https://wikipedia.org/wiki/Fingerprint_\(computing\)) to prevent the reuse of old files. Assets are also [compressed](/en-us/aspnet/core/performance/response-compression) to reduce asset delivery time.

At runtime, the discovered static web assets are exposed as endpoints with HTTP headers applied, such as [caching headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control) and content type headers. An asset is served once until the file changes or the browser clears its cache. The [`ETag`](https://developer.mozilla.org/docs/Web/HTTP/Headers/ETag), [`Last-Modified`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Last-Modified), and [`Content-Type`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Type) headers are set. The browser is prevented from using stale assets after an app is updated.

Delivery of static assets is based on [endpoint routing](routing?view=aspnetcore-10.0), so it works with other endpoint-aware features, such as authorization. It's designed to work with all UI frameworks, including Blazor, Razor Pages, and MVC.

Map Static Assets provides the following benefits:

* Build-time compression for all the assets in the app, including JavaScript (JS) and stylesheets but excluding image and font assets that are already compressed. [Gzip](https://tools.ietf.org/html/rfc1952) (`Content-Encoding: gz`) compression is used during development. Gzip and [Brotli](https://tools.ietf.org/html/rfc7932) (`Content-Encoding: br`) compression are both used during publish.

* [Fingerprinting](https://developer.mozilla.org/docs/Glossary/Fingerprinting) for all assets at build time with a [Base64](https://developer.mozilla.org/docs/Glossary/Base64)-encoded string of the [SHA-256](/en-us/dotnet/api/system.security.cryptography.sha256) hash of each file's content. This prevents reusing an old version of a file, even if the old file is cached. Fingerprinted assets are cached using the [`immutable` directive](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control#directives), which results in the browser never requesting the asset again until it changes. For browsers that don't support the `immutable` directive, a [`max-age` directive](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control#directives) is added.

  * Even if an asset isn't fingerprinted, content based `ETags` are generated for each static asset using the fingerprint hash of the file as the `ETag` value. This ensures that the browser only downloads a file if its content changes (or the file is being downloaded for the first time).

  * Internally, the framework maps physical assets to their fingerprints, which allows the app to:

    * Find automatically generated assets, such as Razor component scoped CSS for Blazor's [CSS isolation feature](../blazor/components/css-isolation?view=aspnetcore-10.0) and JS assets described by [JS import maps](https://developer.mozilla.org/docs/Web/HTML/Element/script/type/importmap).
    * Generate link tags in the `<head>` content of the page to preload assets.

Map Static Assets doesn't provide features for minification or other file transformations. Minification is usually handled by custom code or [third-party tooling](../blazor/fundamentals/?view=aspnetcore-10.0#community-links-to-blazor-resources).

Note

[MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) doesn't serve [default documents](#serve-default-documents) on its own. To serve default documents, call [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) followed by [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles). For more information, see the [Serve default documents](#serve-default-documents) section.

To enable static file handling in ASP.NET Core, call [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles).

By default, store static files within the project's [web root](./?view=aspnetcore-10.0#web-root) directory. The default directory is `{CONTENT ROOT}/wwwroot`, where the `{CONTENT ROOT}` placeholder is the app's [content root](./?view=aspnetcore-10.0#content-root). Only files in the `wwwroot` folder are addressable, so you don't need to worry about the rest of your code.

At runtime, static web assets are returned by static file middleware when requested with asset modification and content type headers applied. The [`ETag`](https://developer.mozilla.org/docs/Web/HTTP/Headers/ETag), [`Last-Modified`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Last-Modified), and [`Content-Type`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Type) headers are set.

Static file middleware enables static file serving and is used by an app when [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) is called in the app's request processing pipeline. Files are served from the path specified in [IWebHostEnvironment.WebRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootpath) or [WebRootFileProvider](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootfileprovider#microsoft-aspnetcore-hosting-iwebhostenvironment-webrootfileprovider), which defaults to the web root folder, typically `wwwroot`.

You can also serve static web assets from [referenced projects and packages](../razor-pages/ui-class?view=aspnetcore-10.0#consume-content-from-a-referenced-rcl).

## Change the web root directory

To change the web root, use the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method. For more information, see [ASP.NET Core fundamentals overview](./?view=aspnetcore-10.0#web-root).

Prevent publishing files in `wwwroot` by using the [`<Content>` project item](/en-us/visualstudio/msbuild/common-msbuild-project-items#content) in the project file. The following example prevents publishing content in `wwwroot/local` and its subdirectories:

```
<ItemGroup>
  <Content Update="wwwroot\local\**\*.*" CopyToPublishDirectory="Never" />
</ItemGroup>
```

The [CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) method sets the content root to the current directory:

```
var builder = WebApplication.CreateBuilder(args);
```

The [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder) method sets the content root to the current directory:

```
Host.CreateDefaultBuilder(args)
```

In the request processing pipeline, after the call to [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection), call [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) to enable serving static files from the app's [web root](./?view=aspnetcore-10.0#web-root):

```
app.MapStaticAssets();
```

In the request processing pipeline, after the call to [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection), call [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) to enable serving static files from the app's [web root](./?view=aspnetcore-10.0#web-root):

```
app.UseStaticFiles();
```

Static files are accessible via a path relative to the [web root](./?view=aspnetcore-10.0#web-root).

To access an image at `wwwroot/images/favicon.png`:

* URL format: `https://{HOST}/images/{FILE NAME}`

  * The `{HOST}` placeholder is the host.
  * The `{FILE NAME}` placeholder is the file name.

* Examples

  * Absolute URL: `https://localhost:5001/images/favicon.png`
  * Root relative URL: `images/favicon.png`

In a Blazor app, `images/favicon.png` loads the icon image (`favicon.png`) from the app's `wwwroot/images` folder:

```
<link rel="icon" type="image/png" href="images/favicon.png" />
```

In Razor Pages and MVC apps, the tilde character `~` points to the web root. In the following example, `~/images/favicon.png` loads the icon image (`favicon.png`) from the app's `wwwroot/images` folder:

```
<link rel="icon" type="image/png" href="~/images/favicon.png" />
```

## Short-circuit the middleware pipeline

To avoid running the entire middleware pipeline after a static asset is matched, which is the behavior of [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), call [ShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.builder.routeshortcircuitendpointconventionbuilderextensions.shortcircuit) on [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets). Calling [ShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.builder.routeshortcircuitendpointconventionbuilderextensions.shortcircuit) immediately executes the endpoint and returns the response, preventing other middleware from executing for static asset requests:

```
app.MapStaticAssets().ShortCircuit();
```

## Control static file caching during development

When running in the `Development` environment, for example during [Visual Studio Hot Reload](/en-us/visualstudio/debugger/hot-reload) development testing, the framework overrides cache headers to prevent browsers from caching static files. This behavior helps ensure that the latest version of files are used when files change, avoiding issues with stale content. In production, the framework sets the correct cache headers, so browsers can cache static assets as expected.

To disable this behavior, set `EnableStaticAssetsDevelopmentCaching` to `true` in the `Development` environment's app setting file (`appsettings.Development.json`).

## Static files in non-`Development` environments

When running an app locally, the `Development` environment is the only environment that enables static web assets. To enable static files for environments other than `Development` during local development and testing (for example, in the `Staging` environment), call [UseStaticWebAssets](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestaticwebassets) on the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

Warning

Call [UseStaticWebAssets](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestaticwebassets) for the ***exact environment*** to prevent activating the feature in production, as it serves files from separate locations on disk *other than from the project*. The example in this section checks for the `Staging` environment with [IsStaging](/en-us/dotnet/api/microsoft.extensions.hosting.hostenvironmentenvextensions.isstaging).

```
if (builder.Environment.IsStaging())
{
    builder.WebHost.UseStaticWebAssets();
}
```

## Serve files outside of the web root directory via `IWebHostEnvironment.WebRootPath`

When you set [IWebHostEnvironment.WebRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootpath) to a folder other than `wwwroot`, the app exhibits the following default behaviors:

* In the `Development` environment, static assets are served from `wwwroot` if assets with the same name are in both `wwwroot` and a different folder assigned to [WebRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootpath).
* In any environment other than `Development`, duplicate static assets are served from the [WebRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootpath) folder.

Consider a web app created from the empty web template:

* Containing an `Index.html` file in `wwwroot` and `wwwroot-custom`.
* The `Program` file is updated to set `WebRootPath = "wwwroot-custom"`.

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    WebRootPath = "wwwroot-custom"
});
```

By default, for requests to `/`:

* In the `Development` environment, `wwwroot/Index.html` is returned.
* In any environment other than `Development`, `wwwroot-custom/Index.html` is returned.

To ensure assets from `wwwroot-custom` are always returned, use ***one*** of the following approaches:

* Delete duplicate-named assets in `wwwroot`.

* Set `ASPNETCORE_ENVIRONMENT` in `Properties/launchSettings.json` to any value other than `Development`.

* Disable static web assets by setting `<StaticWebAssetsEnabled>` to `false` in the app's project file. ***WARNING:*** Disabling static web assets disables [Razor class libraries](../razor-pages/ui-class?view=aspnetcore-10.0).

* Add the following XML to the project file:

  ```
  <ItemGroup>
    <Content Remove="wwwroot\**" />
  </ItemGroup>
  ```

The following code updates [WebRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootpath) to a non-Development value (`Staging`), guaranteeing duplicate content is returned from `wwwroot-custom` rather than `wwwroot`:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    EnvironmentName = Environments.Staging,
    WebRootPath = "wwwroot-custom"
});
```

## Static file middleware

Static file middleware enables static file serving in specific static files scenarios, usually in addition to Map Static Assets endpoint routing conventions ([MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets)).

Include static file middleware in request processing when you call [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) in the app's request processing pipeline, typically after adding Map Static Assets endpoint conventions ([MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets)).

Use Map Static Assets endpoint conventions in apps that target .NET 9 or later. Use static file middleware in apps that target versions of .NET prior to .NET 9.

Static file middleware serves static files, but it doesn't provide the same level of optimization as Map Static Assets endpoint conventions. The build-time compression and fingerprinting features of Map Static Assets endpoint conventions aren't available when you rely only on static file middleware.

The endpoint conventions are optimized for serving assets that the app knows about at runtime. If the app serves assets from other locations, such as disk or embedded resources, use static file middleware.

The following features covered in this article are supported with static file middleware but not with Map Static Assets endpoint conventions:

* [Serve files outside of the web root directory](#serve-files-outside-of-the-web-root-directory-via-usestaticfiles)
* [Set HTTP response headers](#set-http-response-headers)
* [Serving files from disk or embedded resources, or other locations](#serve-files-from-multiple-locations)
* [Directory browsing](#directory-browsing)
* [Serve default documents](#serve-default-documents) (with [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets), requires a call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles))
* [Combine static files, default documents, and directory browsing](#combine-static-files-default-documents-and-directory-browsing)
* [Map file extensions to MIME types](#map-file-extensions-to-mime-types)
* [Serving non-standard content types](#non-standard-content-types)

## Serve files outside of the web root directory via `UseStaticFiles`

Consider the following directory hierarchy with static files residing outside of the app's [web root](./?view=aspnetcore-10.0#web-root) in a folder named `ExtraStaticFiles`:

* `wwwroot`

  * `css`
  * `images`
  * `js`

* `ExtraStaticFiles`
  * `images`
    * `red-rose.jpg`

A request can access `red-rose.jpg` by configuring a new instance of static file middleware:

Namespaces for the following API:

```
using Microsoft.Extensions.FileProviders;
```

In the request processing pipeline, after the existing call to either [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) (.NET 9 or later) or [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) (.NET 8 or earlier):

```
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "ExtraStaticFiles")),
    RequestPath = "/static-files"
});
```

In the preceding code, the `ExtraStaticFiles` directory hierarchy is publicly accessible through the `static-files` URL segment. A request to `https://{HOST}/StaticFiles/images/red-rose.jpg`, where the `{HOST}` placeholder is the host, serves the `red-rose.jpg` file.

The following markup references `ExtraStaticFiles/images/red-rose.jpg`:

```
<img src="static-files/images/red-rose.jpg" alt="A red rose" />
```

For the preceding example, Razor Pages and MVC views support tilde-slash notation (`src="~/StaticFiles/images/red-rose.jpg"`), but Razor components in Blazor apps don't support this notation.

## Serve files from multiple locations

*The guidance in this section applies to Razor Pages and MVC apps. For guidance that applies to Blazor Web Apps, see [ASP.NET Core Blazor static files](../blazor/fundamentals/static-files?view=aspnetcore-10.0#serve-files-from-multiple-locations).*

Consider the following markup that displays a company logo:

```
<img src="~/logo.png" asp-append-version="true" alt="Company logo">
```

The developer intends to use the [Image Tag Helper](../mvc/views/tag-helpers/built-in/image-tag-helper?view=aspnetcore-10.0) to append a version and serve the file from a custom location, a folder named `ExtraStaticFiles`.

The following example calls [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) to serve files from `wwwroot` and [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) to serve files from `ExtraStaticFiles`:

In the request processing pipeline, after the existing call to either [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) (.NET 9 or later) or [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) (.NET 8 or earlier):

```
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "ExtraStaticFiles"))
});
```

The following example calls [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) twice to serve files from both `wwwroot` and `ExtraStaticFiles`.

In the request processing pipeline, after the existing call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

```
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "ExtraStaticFiles"))
});
```

Using the preceding code, the `ExtraStaticFiles/logo.png` file is displayed. However, the [Image Tag Helper](../mvc/views/tag-helpers/built-in/image-tag-helper?view=aspnetcore-10.0) ([AppendVersion](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.imagetaghelper.appendversion#microsoft-aspnetcore-mvc-taghelpers-imagetaghelper-appendversion)) isn't applied because the Tag Helper depends on [WebRootFileProvider](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootfileprovider#microsoft-aspnetcore-hosting-iwebhostenvironment-webrootfileprovider), which hasn't been updated to include the `ExtraStaticFiles` folder.

The following code updates the [WebRootFileProvider](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootfileprovider#microsoft-aspnetcore-hosting-iwebhostenvironment-webrootfileprovider) to include the `ExtraStaticFiles` folder by using a [CompositeFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.compositefileprovider). This enables the Image Tag Helper to apply a version to images in the `ExtraStaticFiles` folder.

Namespace for the following API:

```
using Microsoft.Extensions.FileProviders;
```

In the request processing pipeline before the existing call to [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) (.NET 9 or later) or [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) (.NET 8 or earlier):

```
var webRootProvider = new PhysicalFileProvider(builder.Environment.WebRootPath);
var newPathProvider = new PhysicalFileProvider(
    Path.Combine(builder.Environment.ContentRootPath, "ExtraStaticFiles"));

var compositeProvider = new CompositeFileProvider(webRootProvider, newPathProvider);

app.Environment.WebRootFileProvider = compositeProvider;
```

[UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) and [UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) default to the file provider pointing at `wwwroot`. You can provide additional instances of [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) and [UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) with other file providers to serve files from other locations. For more information, see [UseStaticFiles still needed with UseFileServer for wwwroot (`dotnet/AspNetCore.Docs` #15578)](https://github.com/dotnet/AspNetCore.Docs/issues/15578).

## Set HTTP response headers

Use [StaticFileOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileoptions) to set HTTP response headers. In addition to configuring static file middleware to serve static files, the following code sets the [`Cache-Control` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control) to 604,800 seconds (one week).

Namespaces for the following API:

```
using Microsoft.AspNetCore.Http;
```

In the request processing pipeline, after the existing call to either [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) (.NET 9 or later) or [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) (.NET 8 or earlier):

```
app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        ctx.Context.Response.Headers.Append(
            "Cache-Control", "public, max-age=604800");
    }
});
```

## Large collection of assets

When you deal with large collections of assets, which is around 1,000 or more assets, use a bundler to reduce the final number of assets your app serves or combine [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) with [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles).

[MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) eagerly loads the precomputed metadata captured during the build process for the resources in order to support compression, caching, and fingerprinting. These features come at the cost of greater memory usage by the app. For assets that are frequently accessed, it's usually worth the costs. For assets that aren't frequently accessed, the trade-off might not be worth the costs.

If you don't use bundling, combine [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) with [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles). The following example demonstrates the approach.

In the project file (`.csproj`), the `StaticWebAssetEndpointExclusionPattern` MSBuild property is used to filter endpoints from the final manifest for [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets). Excluded files are served by [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) and don't benefit from compression, caching, and fingerprinting.

To keep the framework's default exclusion pattern, retain `$(StaticWebAssetEndpointExclusionPattern)` when setting the value of `StaticWebAssetEndpointExclusionPattern`. Add more patterns in a semicolon-separated list.

In the following example, the exclusion pattern adds the static files in the `lib/icons` folder, which represents a hypothetical batch of icons:

```
<StaticWebAssetEndpointExclusionPattern>
  $(StaticWebAssetEndpointExclusionPattern);lib/icons/**
</StaticWebAssetEndpointExclusionPattern>
```

After HTTPS redirection middleware (`app.UseHttpsRedirection();`) processing in the `Program` file:

* Call [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) to handle the excluded files (`lib/icons/**`) and any other files not covered by [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets).
* Call [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) after [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) to handle critical application files (CSS, JS, images).

```
app.UseStaticFiles();

app.UseAuthorization();

app.MapStaticAssets();
```

## Static assets manifest

[MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) serves assets from a *static assets manifest* rather than by scanning the [web root](./?view=aspnetcore-10.0#web-root) at runtime. The manifest is generated at build and publish time and records the static web assets discovered for the app, along with metadata such as content fingerprints, `Content-Type` headers, caching headers, and the precomputed compressed representations ([Gzip](https://tools.ietf.org/html/rfc1952) and [Brotli](https://tools.ietf.org/html/rfc7932)). At runtime, `MapStaticAssets` reads the manifest, registers an endpoint for each asset, and serves the optimized responses.

The build process generates the manifest in the build output directory. Its file name is based on the project's assembly name (for example, `{ASSEMBLY NAME}.staticwebassets.endpoints.json`, where the `{ASSEMBLY NAME}` placeholder is the app's MSBuild `AssemblyName` value). To provide a manifest from a different location, see the [Provide a custom static files manifest](#provide-a-custom-static-files-manifest) section.

Because `MapStaticAssets` only serves assets listed in the manifest, it doesn't serve files that aren't part of the manifest. Files aren't part of the manifest when they're:

* Located outside the build-time web root, such as files served from disk, embedded resources, or a custom [WebRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootpath) set at runtime.
* Excluded from the manifest with the `StaticWebAssetEndpointExclusionPattern` MSBuild property (see the [Large collection of assets](#large-collection-of-assets) section).

To serve files that aren't in the manifest, call [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), which serves files directly from the web root at runtime. This is also why serving [default documents](#serve-default-documents) with `MapStaticAssets` requires a call to `UseStaticFiles`.

## Integrate build-generated files into static web assets

Build tools, such as TypeScript compilers and JavaScript bundlers, often produce files during the build. To serve these generated files with the fingerprinting, compression, and caching that [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) provides, the files must be discovered as static web assets during the build. Generated files are usually kept outside of the [web root](./?view=aspnetcore-10.0#web-root) (`wwwroot`) and excluded from source control, so they aren't discovered as static web assets by default. Only files resolved as being under `wwwroot` when static web assets are resolved during the build are added to the [static assets manifest](#static-assets-manifest) and served by `MapStaticAssets`. Files linked into `wwwroot` (for example, with a `<Content>` item and a `Link`) are also included, even when the source file is stored outside of `wwwroot`.

To include build-generated files as static web assets, use either of the following approaches.

### Link generated files into the web root

Add a [`<Content>` item](/en-us/visualstudio/msbuild/common-msbuild-project-items#content) with a `Link` that places each generated file under `wwwroot`. A file that's linked into `wwwroot` is discovered by the static web assets pipeline and served by `MapStaticAssets` with fingerprinting, compression, and caching, even though the source file is stored outside of `wwwroot`.

In the following example, a build step generates `main.js` in a `generated` folder, and the `<Content>` item links the file into `wwwroot`:

```
<ItemGroup>
  <Content Include="generated\main.js" Link="wwwroot\main.js"
    CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
```

The generated file must exist when the build process resolves static web assets.

### Use a JavaScript project for complex build pipelines

For a complex JavaScript or TypeScript client build, use a separate JavaScript project that builds the client assets with the [JavaScript project system](/en-us/visualstudio/javascript/javascript-project-system-msbuild-reference) (the `Microsoft.VisualStudio.JavaScript.Sdk` MSBuild SDK and an `.esproj` project file). Reference the JavaScript project from the ASP.NET Core app so that its output is consumed as static web assets. For an example, see the [`Microsoft.FluentUI.AspNetCore.Components.Assets.esproj` project file (`microsoft/fluentui-blazor` GitHub repository)](https://github.com/microsoft/fluentui-blazor/blob/dev/src/Core.Assets/Microsoft.FluentUI.AspNetCore.Components.Assets.esproj).

## Static file authorization

When an app adopts a [fallback authorization policy](../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies), it requires authorization for requests processed by authorization middleware when no policy is produced from authorization metadata. This requirement includes requests for static assets mapped as endpoints. To allow anonymous access to static assets, apply [AllowAnonymousAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) to the endpoint builder:

```
app.MapStaticAssets().Add(endpointBuilder =>
    endpointBuilder.Metadata.Add(new AllowAnonymousAttribute()));
```

When an app adopts a [fallback authorization policy](../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies), authorization is required for requests processed by authorization middleware when no policy is produced from authorization metadata. The ASP.NET Core templates allow anonymous access to static files by calling [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) before calling [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). Most apps follow this pattern. When the static file middleware is called before the authorization middleware:

* No authorization checks are performed on the static files.
* Static files served by the static file middleware, such as those in the web root (typically, `wwwroot`), are publicly accessible.

To serve static files based on authorization:

* Confirm that the app sets the [fallback authorization policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.fallbackpolicy#microsoft-aspnetcore-authorization-authorizationoptions-fallbackpolicy) to require authenticated users.
* Store the static file outside of the app's web root.
* After calling [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization), call [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), specifying the path to the static files folder outside of the web root.

Namespaces for the following API:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.FileProviders;
```

Service registration:

```
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});
```

In the request processing pipeline after the call to [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization):

```
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
           Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
    RequestPath = "/StaticFiles"
});
```

Namespaces for the following API:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.FileProviders;
```

In `Startup.ConfigureServices`:

```
services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});
```

In `Startup.Configure` after the call to [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization):

```
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
                 Path.Combine(env.ContentRootPath, "MyStaticFiles")),
    RequestPath = "/StaticFiles"
});
```

In the preceding code, the fallback authorization policy requires authenticated users. Endpoints that specify authorization requirements use the policy produced from their authorization metadata instead of the fallback policy. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

[RequireAuthenticatedUser](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireauthenticateduser) adds [DenyAnonymousAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.denyanonymousauthorizationrequirement) to the current instance, which enforces that the current user is authenticated.

Static assets stored in the app's web root are publicly accessible because the default static file middleware ([UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)) is called before [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). Static assets in the `MyStaticFiles` folder require authentication and are requested using the `/StaticFiles` URL segment.

An alternative approach to serve files based on authorization is to:

* Store the files outside of the web root and any directory accessible to static file middleware.
* Serve the files via an action method to which authorization is applied and return a [FileResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fileresult) object.

From a Razor page (`Pages/BannerImage.cshtml.cs`):

```
[Authorize]
public class BannerImageModel : PageModel
{
    private readonly IWebHostEnvironment _env;

    public BannerImageModel(IWebHostEnvironment env) =>
        _env = env;

    public PhysicalFileResult OnGet()
    {
        var filePath = Path.Combine(
                _env.ContentRootPath, "MyStaticFiles", "images", "red-rose.jpg");

        return PhysicalFile(filePath, "image/jpeg");
    }
}
```

From a controller (`Controllers/HomeController.cs`):

```
[Authorize]
public IActionResult BannerImage()
{
    var filePath = Path.Combine(
        _env.ContentRootPath, "MyStaticFiles", "images", "red-rose.jpg");

    return PhysicalFile(filePath, "image/jpeg");
}
```

The preceding approach requires a page or endpoint per file.

The following route endpoint example returns files for authenticated users.

In the [`Program` file](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/fundamentals/static-files/9.0/StaticFileAuth/Program.cs#L129-L148):

```
builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("AuthenticatedUsers", b => b.RequireAuthenticatedUser());
    o.AddPolicy("AdminsOnly", b => b.RequireRole("admin"));
});
app.MapGet("/files/{fileName}",  IResult (string fileName) => 
    {
        // Validate the requested name against the stored-name format
        // (a 32-character hex GUID plus an optional safe extension).
        if (!FileNameValidator.StoredFileName().IsMatch(fileName))
        {
            return TypedResults.BadRequest("Invalid file name.");
        }

        var filePath = GetOrCreateFilePath(fileName);

        if (File.Exists(filePath))
        {
           return TypedResults.PhysicalFile(filePath, fileDownloadName: $"{fileName}");
        }

        return TypedResults.NotFound("No file found with the supplied file name");
    })
    .WithName("GetFileByName")
    .RequireAuthorization("AuthenticatedUsers");
```

The following route endpoint example uploads files for authenticated users in the administrator role (`admin`).

In the `Program` file:

```
app.MapPost("/files", 
       async Task<IResult> (IFormFile file, LinkGenerator linker, HttpContext context) =>
    {
        // Don't rely on file.FileName: it's client-controlled metadata. Generate
        // the stored name on the server, then validate it with the same
        // FileNameValidator.StoredFileName pattern used by the download endpoint.
        // Because the extension is derived from the untrusted file.FileName, this
        // rejects uploads whose extension would produce a name the download
        // endpoint can't serve.
        var fileSaveName = Guid.NewGuid().ToString("N") 
                           + Path.GetExtension(file.FileName);

        if (!FileNameValidator.StoredFileName().IsMatch(fileSaveName))
        {
            return TypedResults.BadRequest("Invalid file name.");
        }

        await SaveFileWithCustomFileName(file, fileSaveName);
        
        context.Response.Headers.Append("Location",
                                     linker.GetPathByName(context, "GetFileByName", 
                                     new { fileName = fileSaveName}));
        return TypedResults.Ok("File Uploaded Successfully!");
    })
    .RequireAuthorization("AdminsOnly");
```

Both endpoints validate stored file names with the following helper:

```
using System.Text.RegularExpressions;
internal static partial class FileNameValidator
{
    [GeneratedRegex(@"^[0-9a-fA-F]{32}(\.[A-Za-z0-9]+)?$")]
    public static partial Regex StoredFileName();
}
```

The upload endpoint uses buffered model binding. For large files, use streaming instead. For more information, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#upload-large-files-with-streaming).

## Directory browsing

Directory browsing allows directory listing within specified directories.

For security reasons, directory browsing is disabled by default. For more information, see [Security considerations for static files](#security-considerations-for-static-files).

Enable directory browsing by using the following APIs:

* [AddDirectoryBrowser](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.directorybrowserserviceextensions.adddirectorybrowser)
* [UseDirectoryBrowser](/en-us/dotnet/api/microsoft.aspnetcore.builder.directorybrowserextensions.usedirectorybrowser)

In the following example:

* An `images` folder at the root of the app holds images for directory browsing.
* The request path to browse the images is `/DirectoryImages`.
* Calling [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) and setting the [FileProvider](/en-us/dotnet/api/microsoft.aspnetcore.staticfiles.infrastructure.sharedoptionsbase.fileprovider) of [StaticFileOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileoptions) enables displaying browser links to the individual files.

Namespaces for the following API:

```
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
```

Service registration:

```
builder.Services.AddDirectoryBrowser();
```

In the request processing pipeline, after the existing call to either [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) (.NET 9 or later) or [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) (.NET 8 or earlier):

```
var fileProvider = new PhysicalFileProvider(
    Path.Combine(builder.Environment.WebRootPath, "images"));
var requestPath = "/DirectoryImages";

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = fileProvider,
    RequestPath = requestPath
});

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = fileProvider,
    RequestPath = requestPath
});
```

Namespaces for the following API:

```
using Microsoft.Extensions.FileProviders;
using System.IO;
```

In `Startup.ConfigureServices`:

```
services.AddDirectoryBrowser();
```

In `Startup.Configure` after the existing call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

```
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(env.WebRootPath, "images")),
    RequestPath = "/DirectoryImages"
});

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(env.WebRootPath, "images")),
    RequestPath = "/DirectoryImages"
});
```

The preceding code allows directory browsing of the `wwwroot/images` folder using the URL `https://{HOST}/DirectoryImages` with links to each file and folder, where the `{HOST}` placeholder is the host.

[AddDirectoryBrowser](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.directorybrowserserviceextensions.adddirectorybrowser) adds services required by the directory-browsing middleware, including [HtmlEncoder](/en-us/dotnet/api/system.text.encodings.web.htmlencoder). These services might be added by other calls, such as [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages), but call [AddDirectoryBrowser](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.directorybrowserserviceextensions.adddirectorybrowser) to ensure the services are added.

## Serve default documents

Setting a default page provides visitors a starting point on a site. To serve a default file from `wwwroot` without requiring the request URL to include the file's name, call the [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) method.

[UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) is a URL rewriter that doesn't serve the file. It rewrites the request URL to the default document (for example, `/` to `/index.html`), and another component serves the file.

Because [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) serves assets discovered at build time through endpoint routing, it doesn't serve default documents on its own. Call [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) to rewrite the request, followed by [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) to serve the rewritten request for the default document:

```
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapStaticAssets();
```

Important

Configuring only [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) and [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) (without [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)) returns a *404 - Not Found* response for a request to `/`. This behavior occurs because minimal hosting adds routing middleware at the start of the request processing pipeline, so endpoint routing matches the request before `UseDefaultFiles` rewrites it to the default document. The problem is especially apparent when you change the [web root](./?view=aspnetcore-10.0#web-root) to a custom path with [WebRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootpath), because files in a custom web root aren't part of the [build-time static assets manifest](#static-assets-manifest) that `MapStaticAssets` serves. Add a call to `UseStaticFiles` after `UseDefaultFiles`, as shown in the preceding example, to serve default documents.

In the request processing pipeline before the existing call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

```
app.UseDefaultFiles();
```

With [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles), requests to a folder in `wwwroot` search for:

* `default.htm`
* `default.html`
* `index.htm`
* `index.html`

The first file found from the list is served as though the request included the file's name. The browser URL continues to reflect the URI requested.

The following code changes the default file name to `default-document.html`:

```
var options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("default-document.html");
app.UseDefaultFiles(options);
```

## Combine static files, default documents, and directory browsing

[UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) combines the functionality of [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), [UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles), and optionally [UseDirectoryBrowser](/en-us/dotnet/api/microsoft.aspnetcore.builder.directorybrowserextensions.usedirectorybrowser).

In the request processing pipeline, after the existing call to either [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) (.NET 9 or later) or [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) (.NET 8 or earlier), call [UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) to enable the serving of static files and the default file:

```
app.UseFileServer();
```

Directory browsing isn't enabled for the preceding example.

The following code enables the serving of static files, the default file, and directory browsing.

Service registration:

```
builder.Services.AddDirectoryBrowser();
```

In the request processing pipeline, after the existing call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

```
app.UseFileServer(enableDirectoryBrowsing: true);
```

In `Startup.ConfigureServices`:

```
services.AddDirectoryBrowser();
```

In `Startup.Configure` after the existing call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

```
app.UseFileServer(enableDirectoryBrowsing: true);
```

For the host address (`/`), [UseFileServer](/en-us/dotnet/api/microsoft.aspnetcore.builder.fileserverextensions.usefileserver) returns the default HTML document before the default Razor Page (`Pages/Index.cshtml`) or default MVC view (`Home/Index.cshtml`).

Consider the following directory hierarchy:

* `wwwroot`

  * `css`
  * `images`
  * `js`

* `ExtraStaticFiles`

  * `images`
    * `logo.png`
  * `default.html`

The following code enables the serving of static files, the default file, and directory browsing of `ExtraStaticFiles`.

Namespaces for the following API:

```
using Microsoft.Extensions.FileProviders;
```

Service registration:

```
builder.Services.AddDirectoryBrowser();
```

In the request processing pipeline, after the existing call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

```
app.UseFileServer(new FileServerOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "ExtraStaticFiles")),
    RequestPath = "/static-files",
    EnableDirectoryBrowsing = true
});
```

Namespaces for the following API:

```
using Microsoft.Extensions.FileProviders;
using System.IO;
```

In `Startup.ConfigureServices`:

```
services.AddDirectoryBrowser();
```

In `Startup.Configure` after the existing call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

```
app.UseFileServer(new FileServerOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(env.ContentRootPath, "ExtraStaticFiles")),
    RequestPath = "/static-files",
    EnableDirectoryBrowsing = true
});
```

[AddDirectoryBrowser](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.directorybrowserserviceextensions.adddirectorybrowser) must be called when the `EnableDirectoryBrowsing` property value is `true`.

Using the preceding file hierarchy and code, URLs resolve as shown in the following table (the `{HOST}` placeholder is the host).

| URI                                           | Response file                      |
| --------------------------------------------- | ---------------------------------- |
| `https://{HOST}/static-files/images/logo.png` | `ExtraStaticFiles/images/logo.png` |
| `https://{HOST}/static-files`                 | `ExtraStaticFiles/default.html`    |

If no default-named file exists in the `ExtraStaticFiles` directory, `https://{HOST}/static-files` returns the directory listing with clickable links, where the `{HOST}` placeholder is the host.

[UseDefaultFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles) and [UseDirectoryBrowser](/en-us/dotnet/api/microsoft.aspnetcore.builder.directorybrowserextensions.usedirectorybrowser) perform a client-side redirect from the target URI without a trailing `/` to the target URI with a trailing `/`. For example, from `https://{HOST}/static-files` (no trailing `/`) to `https://{HOST}/static-files/` (includes a trailing `/`). Relative URLs within the `ExtraStaticFiles` directory are invalid without a trailing slash (`/`) unless the [RedirectToAppendTrailingSlash](/en-us/dotnet/api/microsoft.aspnetcore.staticfiles.infrastructure.sharedoptions.redirecttoappendtrailingslash#microsoft-aspnetcore-staticfiles-infrastructure-sharedoptions-redirecttoappendtrailingslash) option of [DefaultFilesOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesoptions) is used.

## Map file extensions to MIME types

Note

For guidance that applies to Blazor apps, see [ASP.NET Core Blazor static files](../blazor/fundamentals/static-files?view=aspnetcore-10.0#file-mappings-and-static-file-options).

Use [FileExtensionContentTypeProvider.Mappings](/en-us/dotnet/api/microsoft.aspnetcore.staticfiles.fileextensioncontenttypeprovider.mappings) to add or modify file extension to MIME content type mappings.

Note

[FileExtensionContentTypeProvider](/en-us/dotnet/api/microsoft.aspnetcore.staticfiles.fileextensioncontenttypeprovider) is **not thread-safe** for concurrent writes. Its internal mappings dictionary is a standard `Dictionary<string, string>` without synchronization. The provider's mappings are intended to be configured once at startup. If only read operations (lookups) are performed afterward, the provider can safely be registered as a singleton. Do not add, remove, or modify mappings after the provider is in use by concurrent requests.

In the following example, several file extensions are mapped to known MIME types. The `.rtf` extension is replaced, and `.mp4` is removed:

```
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;

...

// Set up custom content types - associating file extension to MIME type
var provider = new FileExtensionContentTypeProvider();
// Add new mappings
provider.Mappings[".myapp"] = "application/x-msdownload";
provider.Mappings[".htm3"] = "text/html";
provider.Mappings[".image"] = "image/png";
// Replace an existing mapping
provider.Mappings[".rtf"] = "application/x-msdownload";
// Remove MP4 videos
provider.Mappings.Remove(".mp4");

app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
});
```

When you have several static file options to configure, you can alternatively set the provider by using [StaticFileOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileoptions):

```
var provider = new FileExtensionContentTypeProvider();

...

builder.Services.Configure<StaticFileOptions>(options =>
{
    options.ContentTypeProvider = provider;
});

app.UseStaticFiles();
```

In `Startup.Configure`:

```
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using System.IO;

...

// Set up custom content types - associating file extension to MIME type
var provider = new FileExtensionContentTypeProvider();
// Add new mappings
provider.Mappings[".myapp"] = "application/x-msdownload";
provider.Mappings[".htm3"] = "text/html";
provider.Mappings[".image"] = "image/png";
// Replace an existing mapping
provider.Mappings[".rtf"] = "application/x-msdownload";
// Remove MP4 videos
provider.Mappings.Remove(".mp4");

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(env.WebRootPath, "images")),
    RequestPath = "/images",
    ContentTypeProvider = provider
});

app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(env.WebRootPath, "images")),
    RequestPath = "/images"
});
```

For more information, see [MIME content types](https://www.iana.org/assignments/media-types/media-types.xhtml).

## Non-standard content types

The static file middleware recognizes almost 400 known file content types. If the user requests a file with an unknown file type, the static file middleware passes the request to the next middleware in the pipeline. If no middleware handles the request, the server returns a *404 Not Found* response. If directory browsing is enabled, the server displays a link to the file in a directory listing.

The following code enables serving unknown content types and renders the unknown file as an image:

```
app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = true,
    DefaultContentType = "image/png"
});
```

With the preceding code, a request for a file with an unknown content type is returned as an image.

Warning

Enabling [ServeUnknownFileTypes](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileoptions.serveunknownfiletypes#microsoft-aspnetcore-builder-staticfileoptions-serveunknownfiletypes) is a security risk. It's disabled by default, and its use is discouraged. [Map file extensions to MIME types](#map-file-extensions-to-mime-types) provides a safer alternative to serving files with nonstandard extensions.

## Provide a custom static files manifest

If [`staticAssetsManifestPath`](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) is `null`, the [IHostEnvironment.ApplicationName](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.applicationname) is used to locate the manifest. Alternatively, specify a full path to the manifest file. If you use a relative path, the framework searches for the file in the [AppContext.BaseDirectory](/en-us/dotnet/api/system.appcontext.basedirectory).

## Security considerations for static files

Warning

[UseDirectoryBrowser](/en-us/dotnet/api/microsoft.aspnetcore.builder.directorybrowserextensions.usedirectorybrowser) and [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) can leak secrets. Disabling directory browsing in production is highly recommended. Carefully review which directories are enabled via [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) or [UseDirectoryBrowser](/en-us/dotnet/api/microsoft.aspnetcore.builder.directorybrowserextensions.usedirectorybrowser). The entire directory and its sub-directories become publicly accessible. Store files suitable for serving to the public in a dedicated directory, such as `<content_root>/wwwroot`. Separate these files from MVC views, Razor Pages, configuration files, etc.

Note

A file with an accessible 8.3 short-name alias can also be served through that alias. Static file middleware determines the content type from the requested path, not the resolved file name. If the alias has a recognized extension, a file whose long-name extension is unrecognized can be served even when `ServeUnknownFileTypes` is disabled. Don't rely on file extension filtering to protect private files. Keep them outside all directories served by static file middleware.

* The URLs for content exposed through [UseDirectoryBrowser](/en-us/dotnet/api/microsoft.aspnetcore.builder.directorybrowserextensions.usedirectorybrowser) and [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) follow the case sensitivity and character restrictions of the underlying file system. For example, Windows is case insensitive, but macOS and Linux aren't.

* ASP.NET Core apps hosted in IIS use the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) to forward all requests to the app, including static file requests. The IIS static file handler isn't used and doesn't handle requests.

* Complete the following steps in IIS Manager to remove the IIS static file handler at the server or website level:

  1. Navigate to the **Modules** feature.
  2. Select **StaticFileModule** in the list.
  3. Click **Remove** in the **Actions** sidebar.

  Warning

  If the IIS static file handler is enabled **and** the ASP.NET Core Module is configured incorrectly, static files are served. This condition occurs, for example, if the `web.config` file isn't deployed.

* Place code files, including `.cs` and `.cshtml`, outside of the app project's [web root](./?view=aspnetcore-10.0#web-root). This configuration creates a logical separation between the app's client-side content and server-based code. This separation prevents server-side code from being leaked.

## MSBuild properties

The following tables show the static files MSBuild properties and metadata descriptions.

| Property                                 | Description                                                                                   |
| ---------------------------------------- | --------------------------------------------------------------------------------------------- |
| `EnableDefaultCompressedItems`           | Enables default compression include and exclude patterns.                                     |
| `CompressionIncludePatterns`             | Semicolon-separated list of file patterns to include for compression.                         |
| `CompressionExcludePatterns`             | Semicolon-separated list of file patterns to exclude from compression. See the example below. |
| `CompressionEnabled`                     | Completely disables static asset compression when set to `false`. See the example below.      |
| `EnableDefaultCompressionFormats`        | Enables default compression formats (Gzip and Brotli).                                        |
| `BuildCompressionFormats`                | Compression formats to use during build.                                                      |
| `PublishCompressionFormats`              | Compression formats to use during publish.                                                    |
| `DisableBuildCompression`                | Disables compression during build.                                                            |
| `CompressDiscoveredAssetsDuringBuild`    | Compresses discovered assets during build.                                                    |
| `BrotliCompressionLevel`                 | Compression level for the Brotli algorithm.                                                   |
| `StaticWebAssetBuildCompressAllAssets`   | Compresses all assets during build, not just assets discovered or computed during a build.    |
| `StaticWebAssetPublishCompressAllAssets` | Compresses all assets during publish, not just assets discovered or computed during a build.  |

The following example excludes JavaScript files from compression:

```
<PropertyGroup>
  <CompressionExcludePatterns>$(CompressionExcludePatterns);**\*.js</CompressionExcludePatterns>
</PropertyGroup>
```

To completely disable static asset compression:

```
<PropertyGroup>
  <CompressionEnabled>false</CompressionEnabled>
</PropertyGroup>
```

| Property                                           | Description                                           |
| -------------------------------------------------- | ----------------------------------------------------- |
| `StaticWebAssetBasePath`                           | Base URL path for all the assets in a library.        |
| `StaticWebAssetsFingerprintContent`                | Enables content fingerprinting for cache busting.     |
| `StaticWebAssetFingerprintingEnabled`              | Enables fingerprinting feature for static web assets. |
| `StaticWebAssetsCacheDefineStaticWebAssetsEnabled` | Enables caching for static web asset definitions.     |
| `StaticWebAssetEndpointExclusionPattern`           | Pattern for excluding endpoints.                      |

| Item group                         | Description                                                                        | Metadata                       |
| ---------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------ |
| `StaticWebAssetContentTypeMapping` | Maps file patterns to content types and cache headers for endpoints.               | `Pattern`, `Cache`, `Priority` |
| `StaticWebAssetFingerprintPattern` | Defines patterns for applying fingerprints to static web assets for cache busting. | `Pattern`, `Expression`        |

Metadata descriptions:

* **`Pattern`**: A glob pattern used to match files. For `StaticWebAssetContentTypeMapping`, it matches files to determine their content type (for example, `*.js` for JavaScript files). For `StaticWebAssetFingerprintPattern`, it identifies multi-extension files that require special fingerprinting treatment (for example, `*.lib.module.js`).

* **`Cache`**: Specifies the `Cache-Control` header value for the matched content type. This value controls browser caching behavior (for example, `max-age=3600, must-revalidate` for media files).

* **`Priority`**: Controls precedence when multiple `StaticWebAssetContentTypeMapping` items match the same file. Higher numeric values take precedence over lower ones. `Priority` is required.

* **`Expression`**: Defines how the fingerprint is inserted into the filename. The default is `#[.{FINGERPRINT}]`, which inserts the fingerprint (`{FINGERPRINT}` placeholder) before the extension.

The following example maps the bitmap file pattern (`.bmp`) to the `image/bmp` content type with the `{CACHE HEADER}` placeholder representing the `Cache-Control` header to use for non-fingerprinted endpoints:

```
<ItemGroup>
  <StaticWebAssetContentTypeMapping Include="image/bmp" Cache="{CACHE HEADER}"
    Pattern="*.bmp" Priority="1" />
</ItemGroup>
```

## Runtime configuration options

The following table describes the runtime configuration options.

| Configuration key                           | Description                                                                                                                                                                                                                                                      |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ReloadStaticAssetsAtRuntime`               | Enables dev-time hot reloading of static assets: serves modified web root (`wwwroot`) files (recomputes `ETag`, recompresses if required) instead of build-time manifest versions. Defaults to enabled only when serving a build manifest unless explicitly set. |
| `DisableStaticAssetNotFoundRuntimeFallback` | When `true`, suppresses the fallback endpoint that serves newly added files not present in the build manifest. When `false` or absent, a file-exists-checked `{**path}` fallback (GET/HEAD) logs a warning and serves the file with a computed `ETag`.           |
| `EnableStaticAssetsDevelopmentCaching`      | When `true`, preserves the original `Cache-Control` headers on asset descriptors. When `false` or absent, rewrites `Cache-Control` headers to `no-cache` to avoid aggressive client caching during development.                                                  |
| `EnableStaticAssetsDevelopmentIntegrity`    | When `true`, keeps integrity properties on asset descriptors. When `false` or absent, removes any integrity property to prevent mismatches when files change during development.                                                                                 |

## Related content

* [ASP.NET Core Blazor static files](../blazor/fundamentals/static-files?view=aspnetcore-10.0)
* [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/microsoft-logins
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/social/microsoft-logins.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Microsoft Account external login setup with ASP.NET Core

By [Valeriy Novytskyy](https://github.com/01binary) and [Rick Anderson](https://twitter.com/RickAndMSFT)

This sample shows how to enable users to sign in with their work, school, or personal Microsoft account using the ASP.NET Core project created on the [previous page](./?view=aspnetcore-10.0).

## Create the app in the Microsoft Entra admin center

* Add the [Microsoft.AspNetCore.Authentication.MicrosoftAccount](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.MicrosoftAccount/) NuGet package to the project.
* Register the application in the Microsoft Entra admin center by following the steps in [Register an application with the Microsoft identity platform](/en-us/entra/identity-platform/quickstart-register-app?tabs=client-secret)

### Create a client secret

Generate a client secret in the Microsoft Entra admin center by following the steps in [Add and manage application credentials in Microsoft Entra ID](/en-us/entra/identity-platform/how-to-add-credentials).

## Store the Microsoft client ID and secret

Store sensitive settings such as the Microsoft **Application (client) ID** and **Client Secret** created in the previous step with [Secret Manager](../../app-secrets?view=aspnetcore-10.0). For this sample, use the following steps:

1. Initialize the project for secret storage per the instructions at [Enable secret storage](../../app-secrets?view=aspnetcore-10.0#enable-secret-storage).

2. Store the sensitive settings in the local secret store with the secret keys `Authentication:Microsoft:ClientId` and `Authentication:Microsoft:ClientSecret`. The `<client-id>` is listed on the Azure App registrations blade under **Application (client) ID**. The `<client-secret>` is on listed under **Certificates & secrets** as the **Value**, not the **Secret ID**.

   ```
   dotnet user-secrets set "Authentication:Microsoft:ClientId" "<client-id>"
   dotnet user-secrets set "Authentication:Microsoft:ClientSecret" "<client-secret>"
   ```

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For example, [Bash](https://linuxhint.com/bash-environment-variables/) doesn't support colon (`:`) as a separator. All platforms support the double underscore (`__`) syntax and automatically replace it with a colon (`:`).

## Configure Microsoft Account Authentication

Add the Authentication service to the `Program`:

```
builder.Services.AddAuthentication().AddMicrosoftAccount(microsoftOptions =>
{
    microsoftOptions.ClientId = configuration["Authentication:Microsoft:ClientId"];
    microsoftOptions.ClientSecret = configuration["Authentication:Microsoft:ClientSecret"];
});
```

The [AddAuthentication(IServiceCollection, String)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-string\)) overload sets the [DefaultScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultscheme) property. The [AddAuthentication(IServiceCollection, Action\<AuthenticationOptions>)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-action\(\(microsoft-aspnetcore-authentication-authenticationoptions\)\)\)) overload allows configuring authentication options, which can be used to set up default authentication schemes for different purposes. Subsequent calls to `AddAuthentication` override previously configured [AuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.authenticationoptions) properties.

[AuthenticationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder) extension methods that register an authentication handler may only be called once per authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display name.

For more information about configuration options supported by Microsoft Account authentication, see the [MicrosoftAccountOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.microsoftaccountoptions) API reference. This can be used to request different information about the user.

## Sign in with Microsoft Account

* Run the app and select **Log in**. An option to sign in with Microsoft appears.
* Select to sign in with Microsoft to navigate to Microsoft for authentication. After signing in with your Microsoft Account, you'll be prompted to let the app access your info:
* Select **Yes** to navigate back to the web site where to set your email.

You're now logged in using your Microsoft credentials.

To use multiple authentication providers, see [Using external login providers with Identity in ASP.NET Core](./?view=aspnetcore-10.0#multiple-authentication-providers).

## Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (`https`), host, and client IP address. Apps don't automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (`https`) results in the app generating incorrect insecure redirect URLs.

Use forwarded headers middleware to make the original request information available to the app for request processing.

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

## Troubleshooting

* If the Microsoft Account provider redirects to a sign in error page, note the error title and description query string parameters directly following the `#` (hashtag) in the Uri.

  Although the error message seems to indicate a problem with Microsoft authentication, the most common cause is your application Uri not matching any of the **Redirect URIs** specified for the **Web** platform.

* If Identity isn't configured by calling `services.AddIdentity` in `ConfigureServices`, attempting to authenticate will result in *ArgumentException: The 'SignInScheme' option must be provided*. The project template used in this sample ensures that this is done.

* If the site database hasn't been created by applying the initial migration, *A database operation failed while processing the request* error occurs. Tap **Apply Migrations** to create the database and refresh to continue past the error.

## Next steps

* This article showed how to authenticate with Microsoft. Follow a similar approach to authenticate with other providers listed on the [previous page](./?view=aspnetcore-10.0).
* Once the web site is published to Azure web app, create a new client secrets in the Microsoft Entra admin center.
* Set the `Authentication:Microsoft:ClientId` and `Authentication:Microsoft:ClientSecret` as application settings in the Microsoft Entra admin center. The configuration system is set up to read keys from environment variables.

This sample shows you how to enable users to sign in with their work, school, or personal Microsoft account using the ASP.NET Core 3.0 project created on the [previous page](./?view=aspnetcore-10.0).

## Create the app in the Microsoft Entra admin center

* Add the [Microsoft.AspNetCore.Authentication.MicrosoftAccount](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.MicrosoftAccount/) NuGet package to the project.
* Register the application in the Microsoft Entra admin center by following the steps in [Register an application with the Microsoft identity platform](/en-us/entra/identity-platform/quickstart-register-app?tabs=client-secret#register-an-application)

### Create client secret

Generate a client secret in the Microsoft Entra admin center by following the steps in [Add and manage application credentials in Microsoft Entra ID](/en-us/entra/identity-platform/how-to-add-credentials).

## Store the Microsoft client ID and secret

Store sensitive settings such as the Microsoft **Application (client) ID** and **Client Secret** you created in the previous step with [Secret Manager](../../app-secrets?view=aspnetcore-10.0). For this sample, use the following steps:

1. Initialize the project for secret storage per the instructions at [Enable secret storage](../../app-secrets?view=aspnetcore-10.0#enable-secret-storage).

2. Store the sensitive settings in the local secret store with the secret keys `Authentication:Microsoft:ClientId` and `Authentication:Microsoft:ClientSecret`:

   ```
   dotnet user-secrets set "Authentication:Microsoft:ClientId" "<client-id>"
   dotnet user-secrets set "Authentication:Microsoft:ClientSecret" "<client-secret>"
   ```

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For example, [Bash](https://linuxhint.com/bash-environment-variables/) doesn't support colon (`:`) as a separator. All platforms support the double underscore (`__`) syntax and automatically replace it with a colon (`:`).

## Configure Microsoft Account Authentication

Add the Microsoft Account service to the `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();

    services.AddAuthentication().AddMicrosoftAccount(microsoftOptions =>
    {
        microsoftOptions.ClientId = Configuration["Authentication:Microsoft:ClientId"];
        microsoftOptions.ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"];
    });
}
```

The [AddAuthentication(IServiceCollection, String)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-string\)) overload sets the [DefaultScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultscheme) property. The [AddAuthentication(IServiceCollection, Action\<AuthenticationOptions>)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-action\(\(microsoft-aspnetcore-authentication-authenticationoptions\)\)\)) overload allows configuring authentication options, which can be used to set up default authentication schemes for different purposes. Subsequent calls to `AddAuthentication` override previously configured [AuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.authenticationoptions) properties.

[AuthenticationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder) extension methods that register an authentication handler may only be called once per authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display name.

For more information about configuration options supported by Microsoft Account authentication, see the [MicrosoftAccountOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.microsoftaccountoptions) API reference. This can be used to request different information about the user.

## Sign in with Microsoft Account

Run the app and select **Log in**. An option to sign in with Microsoft appears. Select **Microsoft** to navigate to Microsoft for authentication. After signing in with your Microsoft Account, you'll be prompted to let the app access your info:

Tap **Yes** and you'll be redirected back to the web site where you can set your email.

You're now logged in using your Microsoft credentials.

## Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (`https`), host, and client IP address. Apps don't automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (`https`) results in the app generating incorrect insecure redirect URLs.

Use forwarded headers middleware to make the original request information available to the app for request processing.

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

## Troubleshooting

* If the Microsoft Account provider redirects you to a sign in error page, note the error title and description query string parameters directly following the `#` (hashtag) in the Uri.

  Although the error message seems to indicate a problem with Microsoft authentication, the most common cause is your application Uri not matching any of the **Redirect URIs** specified for the **Web** platform.

* If Identity isn't configured by calling `services.AddIdentity` in `ConfigureServices`, attempting to authenticate will result in *ArgumentException: The 'SignInScheme' option must be provided*. The project template used in this sample ensures that this is done.

* If the site database hasn't been created by applying the initial migration, you'll get *A database operation failed while processing the request* error. Tap **Apply Migrations** to create the database and refresh to continue past the error.

## Next steps

* This article showed how you can authenticate with Microsoft. You can follow a similar approach to authenticate with other providers listed on the [previous page](./?view=aspnetcore-10.0).
* Once you publish your web site to Azure web app, create a new client secrets in the Microsoft Entra admin center.
* Set the `Authentication:Microsoft:ClientId` and `Authentication:Microsoft:ClientSecret` as application settings in Microsoft Entra admin center. The configuration system is set up to read keys from environment variables.

## Related content

[Multiple authentication providers](./?view=aspnetcore-10.0#multiple-authentication-providers)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/advanced-scenarios
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/advanced-scenarios.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor advanced scenarios (render tree construction)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes the advanced scenario for building Blazor render trees manually with [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder).

Warning

Use of [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) to create components is an *advanced scenario*. A malformed component (for example, an unclosed markup tag) can result in undefined behavior. Undefined behavior includes broken content rendering, loss of app features, and ***compromised security***.

## Manually build a render tree (`RenderTreeBuilder`)

[RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) provides methods for manipulating components and elements, including building components manually in C# code.

Consider the following `PetDetails` component, which can be manually rendered in another component.

`PetDetails.razor`:

```
<h2>Pet Details</h2>

<p>@PetDetailsQuote</p>

@code
{
    [Parameter]
    public string? PetDetailsQuote { get; set; }
}
```

In the following `BuiltContent` component, the loop in the `CreateComponent` method generates three `PetDetails` components.

In [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) methods with a sequence number, sequence numbers are source code line numbers. The Blazor difference algorithm relies on the sequence numbers corresponding to distinct lines of code, not distinct call invocations. When creating a component with [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) methods, hardcode the arguments for sequence numbers. **Using a calculation or counter to generate the sequence number can lead to poor performance.** For more information, see the [Sequence numbers relate to code line numbers and not execution order](#sequence-numbers-relate-to-code-line-numbers-and-not-execution-order) section.

`BuiltContent.razor`:

```
@page "/built-content"

<PageTitle>Built Content</PageTitle>

<h1>Built Content Example</h1>

<div>
    @CustomRender
</div>

<button @onclick="RenderComponent">
    Create three Pet Details components
</button>

@code {
    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (var i = 0; i < 3; i++) 
        {
            builder.OpenComponent(0, typeof(PetDetails));
            builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
            builder.CloseComponent();
        }
    };

    private void RenderComponent() => CustomRender = CreateComponent();
}
```

```
@page "/built-content"

<PageTitle>Built Content</PageTitle>

<h1>Built Content Example</h1>

<div>
    @CustomRender
</div>

<button @onclick="RenderComponent">
    Create three Pet Details components
</button>

@code {
    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (var i = 0; i < 3; i++) 
        {
            builder.OpenComponent(0, typeof(PetDetails));
            builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
            builder.CloseComponent();
        }
    };

    private void RenderComponent() => CustomRender = CreateComponent();
}
```

```
@page "/built-content"

<h1>Build a component</h1>

<div>
    @CustomRender
</div>

<button @onclick="RenderComponent">
    Create three Pet Details components
</button>

@code {
    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (var i = 0; i < 3; i++) 
        {
            builder.OpenComponent(0, typeof(PetDetails));
            builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
            builder.CloseComponent();
        }
    };

    private void RenderComponent()
    {
        CustomRender = CreateComponent();
    }
}
```

Warning

The types in [Microsoft.AspNetCore.Components.RenderTree](/en-us/dotnet/api/microsoft.aspnetcore.components.rendertree) allow processing of the *results* of rendering operations. These are internal details of the Blazor framework implementation. These types should be considered *unstable* and subject to change in future releases.

### Sequence numbers relate to code line numbers and not execution order

Razor component files (`.razor`) are always compiled. Executing compiled code has a potential advantage over interpreting code because the compile step that yields the compiled code can be used to inject information that improves app performance at runtime.

A key example of these improvements involves *sequence numbers*. Sequence numbers indicate to the runtime which outputs came from which distinct and ordered lines of code. The runtime uses this information to generate efficient tree diffs in linear time, which is far faster than is normally possible for a general tree diff algorithm.

Consider the following Razor component file (`.razor`):

```
@if (someFlag)
{
    <text>First</text>
}

Second
```

The preceding Razor markup and text content compiles into C# code similar to the following:

```
if (someFlag)
{
    builder.AddContent(0, "First");
}

builder.AddContent(1, "Second");
```

When the code executes for the first time and `someFlag` is `true`, the builder receives the sequence in the following table.

| Sequence | Type      | Data   |
| -------- | --------- | ------ |
| 0        | Text node | First  |
| 1        | Text node | Second |

Imagine that `someFlag` becomes `false` and the markup is rendered again. This time, the builder receives the sequence in the following table.

| Sequence | Type      | Data   |
| -------- | --------- | ------ |
| 1        | Text node | Second |

When the runtime performs a diff, it sees that the item at sequence `0` was removed, so it generates the following trivial *edit script* with a single step:

* Remove the first text node.

### The problem with generating sequence numbers programmatically

Imagine instead that you wrote the following render tree builder logic:

```
var seq = 0;

if (someFlag)
{
    builder.AddContent(seq++, "First");
}

builder.AddContent(seq++, "Second");
```

The first output is reflected in the following table.

| Sequence | Type      | Data   |
| -------- | --------- | ------ |
| 0        | Text node | First  |
| 1        | Text node | Second |

This outcome is identical to the prior case, so no negative issues exist. `someFlag` is `false` on the second rendering, and the output is seen in the following table.

| Sequence | Type      | Data   |
| -------- | --------- | ------ |
| 0        | Text node | Second |

This time, the diff algorithm sees that *two* changes have occurred. The algorithm generates the following edit script:

* Change the value of the first text node to `Second`.
* Remove the second text node.

Generating the sequence numbers has lost all the useful information about where the `if/else` branches and loops were present in the original code. This results in a diff **twice as long** as before.

This is a trivial example. In more realistic cases with complex and deeply nested structures, and especially with loops, the performance cost is usually higher. Instead of immediately identifying which loop blocks or branches have been inserted or removed, the diff algorithm must recurse deeply into the render trees. This usually results in building longer edit scripts because the diff algorithm is misinformed about how the old and new structures relate to each other.

### Guidance and conclusions

* App performance suffers if sequence numbers are generated dynamically.
* The necessary information doesn't exist to permit the framework to generate sequence numbers automatically at runtime unless the information is captured at compile time.
* Don't write long blocks of manually-implemented [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) logic. Prefer `.razor` files and allow the compiler to deal with the sequence numbers. If you're unable to avoid manual [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) logic, split long blocks of code into smaller pieces wrapped in [OpenRegion](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder.openregion)/[CloseRegion](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder.closeregion) calls. Each region has its own separate space of sequence numbers, so you can restart from zero (or any other arbitrary number) inside each region.
* If sequence numbers are hardcoded, the diff algorithm only requires that sequence numbers increase in value. The initial value and gaps are irrelevant. One legitimate option is to use the code line number as the sequence number, or start from zero and increase by ones or hundreds (or any preferred interval).
* For loops, the sequence numbers should increase in your source code, not in terms of runtime behavior. The fact that, at runtime, the numbers repeat is how the diffing system realises you're in a loop.
* Blazor uses sequence numbers, while other tree-diffing UI frameworks don't use them. Diffing is far faster when sequence numbers are used, and Blazor has the advantage of a compile step that deals with sequence numbers automatically for developers authoring `.razor` files.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/webassembly-runtime-performance
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/performance/webassembly-runtime-performance.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor WebAssembly runtime performance

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article provides guidance on Blazor WebAssembly runtime performance.

## Potentially reduced performance with Microsoft Edge enhanced security

The Microsoft Edge browser's WebAssembly (Wasm) interpreter running in [enhanced security mode](/en-us/DeployEdge/microsoft-edge-security-browse-safer) might not yield the same performance as when Blazor is running without enhanced security. If enhanced security mode is enabled and an app's performance is degraded, we recommend adding the site as an [exception](/en-us/DeployEdge/microsoft-edge-security-browse-safer#enhanced-security-sites) to opt out of enhanced security mode.

## Trim .NET IL after ahead-of-time (AOT) compilation

The `WasmStripILAfterAOT` MSBuild option enables removing the .NET Intermediate Language (IL) for compiled methods after performing AOT compilation to WebAssembly, which reduces the size of the `_framework` folder.

In the app's project file:

```
<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
  <WasmStripILAfterAOT>true</WasmStripILAfterAOT>
</PropertyGroup>
```

This setting trims away the IL code for most compiled methods, including methods from libraries and methods in the app. Not all compiled methods can be trimmed, as some are still required by the .NET interpreter at runtime.

To report a problem with the trimming option, [open an issue on the `dotnet/runtime` GitHub repository](https://github.com/dotnet/runtime/issues).

Disable the trimming property if it prevents your app from running normally:

```
<WasmStripILAfterAOT>false</WasmStripILAfterAOT>
```

## Heap size for some mobile device browsers

When building a Blazor app that runs on the client and targets mobile device browsers, especially Safari on iOS, decreasing the maximum memory for the app with the MSBuild property `EmccMaximumHeapSize` may be required. For more information, see [Host and deploy ASP.NET Core Blazor WebAssembly](../host-and-deploy/webassembly/?view=aspnetcore-10.0#decrease-maximum-heap-size-for-some-mobile-device-browsers).

## Runtime relinking

One of the largest parts of a Blazor WebAssembly app is the WebAssembly-based .NET runtime (`dotnet.wasm`) that the browser must download when the app is first accessed by a user's browser. Relinking the .NET WebAssembly runtime trims unused runtime code and thus improves download speed.

Runtime relinking requires installation of the .NET WebAssembly build tools. For more information, see [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0#net-webassembly-build-tools).

With the .NET WebAssembly build tools installed, runtime relinking is performed automatically when an app is **published** in the `Release` configuration. The size reduction is particularly dramatic when disabling globalization. For more information, see [ASP.NET Core Blazor globalization and localization](../globalization-localization?view=aspnetcore-10.0#invariant-globalization).

Important

Runtime relinking trims class instance JavaScript-invokable .NET methods unless they're protected. For more information, see [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0#avoid-trimming-javascript-invokable-net-methods).

## Single Instruction, Multiple Data (SIMD)

Blazor uses [WebAssembly Single Instruction, Multiple Data (SIMD)](https://wikipedia.org/wiki/Single_instruction,_multiple_data) to improve the throughput of vectorized computations by performing an operation on multiple pieces of data in parallel using a single instruction.

To disable SIMD, for example when targeting old browsers or browsers on mobile devices that don't support SIMD, set the `<WasmEnableSIMD>` property to `false` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <WasmEnableSIMD>false</WasmEnableSIMD>
</PropertyGroup>
```

For more information, see [Configuring and hosting .NET WebAssembly applications: SIMD - Single instruction, multiple data](https://aka.ms/dotnet-wasm-features#simd---single-instruction-multiple-data) and note that the guidance isn't versioned and applies to the latest public release.

Blazor uses [WebAssembly Single Instruction, Multiple Data (SIMD)](https://wikipedia.org/wiki/Single_instruction,_multiple_data) to improve the throughput of vectorized computations by performing an operation on multiple pieces of data in parallel using a single instruction.

To enable SIMD, add the `<WasmEnableSIMD>` property set to `true` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <WasmEnableSIMD>true</WasmEnableSIMD>
</PropertyGroup>
```

For more information, see [Configuring and hosting .NET WebAssembly applications: SIMD - Single instruction, multiple data](https://aka.ms/dotnet-wasm-features#simd---single-instruction-multiple-data) and note that the guidance isn't versioned and applies to the latest public release.

## Related content

* [ASP.NET Core Blazor WebAssembly browser developer tools diagnostics](webassembly-browser-developer-tools-diagnostics?view=aspnetcore-10.0)
* [ASP.NET Core Blazor WebAssembly Event Pipe diagnostics](webassembly-event-pipe-diagnostics?view=aspnetcore-10.0)
* [What diagnostic tools are available in .NET Core?](/en-us/dotnet/core/diagnostics/)
* [.NET diagnostic tools](/en-us/dotnet/core/diagnostics/tools-overview)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/roles.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Role-based authorization in ASP.NET Core

When a user's identity is created after authentication, the user may belong to one or more *roles*, reflecting various authorizations that the user has to access data and perform operations. For example, Tracy may belong to the "Administrator" and "User" roles with access to administrative web pages in the app, while Scott may only belong to the "User" role and not have access to administrative data or operations. How these roles are created and managed depends on the backing store of the authorization process. Roles are exposed to the developer through [ClaimsPrincipal.IsInRole](/en-us/dotnet/api/system.security.claims.claimsprincipal.isinrole). [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) must be called to add Role services when setting up the app's identity system.

While roles are claims, not all claims are roles. Depending on the identity issuer, a role may be a collection of users that may apply claims for group members, as well as an actual claim on an identity. However, claims are meant to be information about an individual user. Using roles to add claims to a user can confuse the boundary between the user and their individual claims. This confusion is why the single-page application (SPA) templates aren't designed around roles. In addition, for organizations migrating from an on-premises legacy system, the proliferation of roles over the years can mean a role claim may be too large to be contained within a token usable by a SPA. To secure SPAs, see [Use Identity to secure a Web API backend for SPAs](../authentication/identity-api-authorization?view=aspnetcore-10.0).

This article uses Razor component examples and focuses on Blazor authorization scenarios. For additional Blazor guidance, see the [Related content](#related-content) section. For Razor Pages and MVC guidance, see the following resources:

* [Role-based authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/roles?view=aspnetcore-10.0)
* [Role-based authorization in ASP.NET Core MVC](../../mvc/security/authorization/roles?view=aspnetcore-10.0)

Identity configuration changed with the release of .NET 6. Examples in this article demonstrate approaches that configure Identity services in the app's `Program` file. For .NET apps prior to the release of .NET 6 (and before Blazor Web Apps were released with .NET 8), services are configured in `Startup.ConfigureServices` of the `Startup.cs` file. The syntax for Identity configuration is shown in the companion [Razor Pages roles-based authorization article](../../razor-pages/security/authorization/roles?view=aspnetcore-10.0) and the [MVC roles-based authorization article](../../mvc/security/authorization/roles?view=aspnetcore-10.0). See the preceding resources and set the article version selector to the version of .NET that your app targets.

## Sample app

The Blazor Web App sample for this article is the [`BlazorWebAppAuthorization` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/BlazorWebAppAuthorization) ([how to download](../../overview?view=aspnetcore-10.0#how-to-download-a-sample)). The sample app uses seeded accounts with preconfigured roles to demonstrate most of the examples in this article. For more information, see the sample's README file (`README.md`).

Caution

This sample app uses an in-memory database to store user information, which isn't suitable for production scenarios. The sample app is intended for demonstration purposes only and shouldn't be used as a starting point for production apps.

## Add Role services to Identity

Register role-based authorization services in the `Program` file by calling [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) with the role type in the app's Identity configuration. The role type in the following example is `IdentityRole`:

```
builder.Services.AddDefaultIdentity<IdentityUser>( ... )
    .AddRoles<IdentityRole>()
    ...
```

The preceding code requires the [`Microsoft.AspNetCore.Identity.UI` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.UI) and a `using` directive for [Microsoft.AspNetCore.Identity](/en-us/dotnet/api/microsoft.aspnetcore.identity).

In cases where the app takes granular control to build Identity manually, call [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) on [AddIdentityCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentitycore):

```
builder.Services.AddIdentityCore<IdentityUser>()
    .AddRoles<IdentityRole>()
    ...
```

Register role-based authorization services in `Startup.ConfigureServices` (`Startup.cs`) by calling [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) with the role type in the app's Identity configuration. The role type in the following example is `IdentityRole`:

```
services.AddDefaultIdentity<IdentityUser>()
    .AddRoles<IdentityRole>()
    ...
```

The preceding code requires the [`Microsoft.AspNetCore.Identity.UI` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.UI) and a `using` directive for [Microsoft.AspNetCore.Identity](/en-us/dotnet/api/microsoft.aspnetcore.identity).

In cases where the app takes granular control to build Identity manually, call [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) on [AddIdentityCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentitycore):

```
services.AddIdentityCore<IdentityUser>()
    .AddRoles<IdentityRole>()
    ...
```

In Blazor Web Apps (.NET 8 or later), calling [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) in the `Program` file isn't required.

In Blazor Server apps, call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) in the `Program` file after the line that calls [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) (if present):

```
app.UseAuthentication(); // Only present if not called internally
app.UseAuthorization();
```

In Blazor Server apps (not Blazor Web Apps), call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) in the `Program` file after the line that calls [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) (if present):

```
app.UseAuthentication(); // Only present if not called internally
app.UseAuthorization();
```

In Blazor Server apps (not Blazor Web Apps), call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) in `Startup.Configure` (`Startup.cs`) after the line that calls [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) (if present):

```
app.UseAuthentication(); // Only present if not called internally
app.UseAuthorization();
```

Blazor WebAssembly apps call [AddAuthorizationCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authorizationservicecollectionextensions.addauthorizationcore) in the `Program` file to add authorization services:

```
builder.Services.AddAuthorizationCore();
```

## Role-based authorization checks

Role-based authorization checks:

* Are declarative and specify roles that the current user must be a member of to access the requested resource.
* Are applied to Razor components (examples in this article), [Razor Pages](../../razor-pages/security/authorization/roles?view=aspnetcore-10.0), or [MVC controllers or actions within a controller](../../mvc/security/authorization/roles?view=aspnetcore-10.0).

The [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component ([`AuthorizeView` component in Blazor documentation](../../blazor/security/?view=aspnetcore-10.0#authorizeview-component)) supports *role-based* authorization. This section covers basic concepts. For complete coverage, see [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0).

For role-based authorization of content in Razor components, use the [AuthorizeView.Roles](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.roles#microsoft-aspnetcore-components-authorization-authorizeview-roles) parameter.

In the following example:

* The user must have a role claim for either the `Admin` or `SuperUser` roles to see the content of the first [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component.
* To require both `Admin` and `SuperUser` role claims, the second example nests [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) components.

`Pages/RoleChecksWithAuthorizeView.razor`:

```
@page "/role-checks-with-authorizeview"

<h3>Role Checks with AuthorizeView</h3>

<AuthorizeView Roles="Admin, SuperUser">
    <p>User: @context.User.Identity?.Name</p>
    <p>You have an 'Admin' or 'SuperUser' role claim.</p>
</AuthorizeView>

<AuthorizeView Roles="Admin">
    <p>User: @context.User.Identity?.Name</p>
    <p>You have the 'Admin' role claim.</p>
    <AuthorizeView Roles="SuperUser" Context="innerContext">
        <p>User: @innerContext.User.Identity?.Name</p>
        <p>You have both 'Admin' and 'SuperUser' role claims.</p>
    </AuthorizeView>
</AuthorizeView>
```

The preceding code establishes a `Context` for the inner [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component to prevent an [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) context collision. The [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) context is accessed in the outer [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) with the standard approach for accessing the context (`@context.User`). The context is accessed in the inner [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) with the named `innerContext` context (`@innerContext.User`).

The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) supports role-based authorization for entire Razor components. Use the [AuthorizeAttribute.Roles](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles#microsoft-aspnetcore-authorization-authorizeattribute-roles) parameter. The following code limits component access to users who are a member of the `Admin` role.

`Pages/RequireAdminRoleWithAuthorizeAttribute.razor`:

```
@page "/require-admin-role-with-authorize-attribute"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "Admin")]

<h1>Require 'Admin' role with [Authorize] attribute</h1>

<p>You can only see this if you're in the 'Admin' role.</p>
```

Multiple roles can be specified as a comma separated list. In the following example, access is limited to users who are members of the `Admin` role *or* the `SuperUser` role.

`Pages/RequireAdminOrSuperUserRoleWithAuthorizeAttribute.razor`:

```
@page "/require-admin-or-superuser-role-with-authorize-attribute"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "Admin, SuperUser")]

<h1>Require 'Admin' or 'SuperUser' role with [Authorize] attribute</h1>

<p>
    You can only see this if you're in the 'Admin' role or the 'SuperUser' role.
</p>
```

When multiple attributes are applied, the user must be a member of *all* of the roles specified. The following example requires *both* `Admin` *and* `SuperUser` roles.

`Pages/RequireAdminAndSuperUserRolesWithAuthorizeAttributes.razor`:

```
@page "/require-admin-and-superuser-roles-with-authorize-attributes"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "Admin")]
@attribute [Authorize(Roles = "SuperUser")]

<h1>Require 'Admin' and 'SuperUser' roles with [Authorize] attributes</h1>

<p>
    You can only see this if you're in both the 'Admin' role 
    and the 'SuperUser' role.
</p>
```

Role matching is typically case-sensitive because role names are stored and compared using .NET string comparisons. For example, `Admin` (uppercase `A`) isn't treated as the same role as `admin` (lowercase `a`). For more information, see [Claim-based authorization in ASP.NET Core](claims?view=aspnetcore-10.0#claim-case-sensitivity).

## Policy-based authorization checks

Role requirements can be expressed using policy syntax, where the app registers a policy at startup as part of the Authorization service configuration.

In the following example:

* The `RequireAdminRole` policy specifies that users must be in the `Admin` role.
* The `RequireSuperUserRole` policy specifies that users must be in the `SuperUser` role.

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("RequireAdminRole",
         policy => policy.RequireRole("Admin"))
    .AddPolicy("RequireSuperUserRole",
         policy => policy.RequireRole("SuperUser"));
```

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole",
        policy => policy.RequireRole("Admin"));
    options.AddPolicy("RequireSuperUserRole",
        policy => policy.RequireRole("SuperUser"));
});
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole",
        policy => policy.RequireRole("Admin"));
    options.AddPolicy("RequireSuperUserRole",
        policy => policy.RequireRole("SuperUser"));
});
```

For policy-based authorization using an [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component, use the [AuthorizeView.Policy](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.policy#microsoft-aspnetcore-components-authorization-authorizeview-policy) parameter with a single policy name.

`Pages/PassRequireAdminRolePolicy.razor`:

```
@page "/pass-requireadminrole-policy-with-authorizeview"

<h1>Pass 'RequireAdminRole' policy with AuthorizeView</h1>

<AuthorizeView Policy="RequireAdminRole">
    <p>You satisfy the 'RequireAdminRole' policy.</p>
</AuthorizeView>
```

To handle the case where the user should satisfy one of several policies, create a policy that confirms that the user satisfies other policies.

To handle the case where the user must satisfy several policies simultaneously, take *either* of the following approaches:

* Create a policy for [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) that confirms that the user satisfies several other policies.

* Nest the policies in multiple [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) components.

  `Pages/PassRequireAdminRoleAndRequireSuperUserRolePoliciesWithAuthorizeViews.razor`:

  ```
  @page "/pass-requireadminrole-and-requiresuperuserrole-policies-with-authorizeviews"

  <h1>
      Pass 'RequireAdminRole' and 'RequireSuperUserRole' policies with AuthorizeViews
  </h1>

  <AuthorizeView Policy="RequireAdminRole">
      <AuthorizeView Policy="RequireSuperUserRole" Context="innerContext">
          <p>
              You satisfy the 'RequireAdminRole' and 
              'RequireSuperUserRole' policies.
          </p>
      </AuthorizeView>
  </AuthorizeView>
  ```

If both [Roles](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.roles#microsoft-aspnetcore-components-authorization-authorizeview-roles) and [Policy](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.policy#microsoft-aspnetcore-components-authorization-authorizeview-policy) are set, authorization succeeds only when both conditions are satisfied. That is, the user must belong to at least one of the specified roles *and* meet the requirements defined by the policy.

If neither [Roles](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.roles#microsoft-aspnetcore-components-authorization-authorizeview-roles) nor [Policy](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.policy#microsoft-aspnetcore-components-authorization-authorizeview-policy) is specified, [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) uses the default policy:

* Authenticated (signed-in) users are authorized.
* Unauthenticated (signed-out) users are unauthorized.

In contrast to role matching, which is typically case-sensitive, ASP.NET Core policy name lookup is typically case-insensitive, so `RequireAdminRole` and `requireadminrole` refer to the same policy.

Policies are applied to an entire Razor component using the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) property on the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute).

`Pages/PassRequireAdminRolePolicyWithAuthorizeAttribute.razor`:

```
@page "/pass-requireadminrole-policy-with-authorize-attribute"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "RequireAdminRole")]

<h1>Pass RequireAdminRole policy with [Authorize] attribute</h1>

<p>You can only see this if the 'RequireAdminRole' policy is satisfied.</p>
```

To specify multiple allowed roles in a requirement, specify the roles as parameters to the [RequireRole](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requirerole) method. In the following example, users are authorized if they belong to the `Admin` *or* `SuperUser` roles:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ElevatedRights", policy =>
        policy.RequireRole("Admin", "SuperUser"));
```

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy =>
        policy.RequireRole("Admin", "SuperUser"));
});
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy =>
        policy.RequireRole("Admin", "SuperUser"));
});
```

If you want the policy to require all of the preceding roles, either chain the roles to the policy builder or specify them to the policy builder individually in a [statement lambda](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#statement-lambdas).

Chained to the policy builder:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ElevatedRights", policy => 
        policy
            .RequireRole("Admin")
            .RequireRole("SuperUser"));
```

Alternatively, use a statement lambda:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ElevatedRights",
        policy =>
        {
            policy.RequireRole("Admin");
            policy.RequireRole("SuperUser");
        });
```

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy => 
        policy
            .RequireRole("Admin")
            .RequireRole("SuperUser"));
});
```

Alternatively, use a statement lambda:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights",
        policy =>
        {
            policy.RequireRole("Admin");
            policy.RequireRole("SuperUser");
        });
});
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights", policy => 
        policy
            .RequireRole("Admin")
            .RequireRole("SuperUser"));
});
```

Alternatively, use a statement lambda:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ElevatedRights",
        policy =>
        {
            policy.RequireRole("Admin");
            policy.RequireRole("SuperUser");
        });
});
```

## Windows Authentication security groups as app roles

After the app is [configured for Windows Authentication](../authentication/windowsauth?view=aspnetcore-10.0) ([Blazor-specific guidance](../../blazor/security/blazor-web-app-with-windows-authentication?view=aspnetcore-10.0)) with the client and server machines part of the same Windows domain, user security groups are automatically included as claims in the user's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal).

After the app is [configured for Windows Authentication](../authentication/windowsauth?view=aspnetcore-10.0) with the client and server machines part of the same Windows domain, user security groups are automatically included as claims in the user's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal).

The `User.Identity` is typically a [WindowsIdentity](/en-us/dotnet/api/system.security.principal.windowsidentity) when using Windows Authentication, and you can retrieve the SID group claims or check if a user is in a role with the following code, where the `{DOMAIN}` placeholder is the domain and the `{SID GROUP NAME}` is the SID group name:

```
if (User.Identity is WindowsIdentity windowsIdentity)
{
    var groups = windowsIdentity.Groups;

    // If needed, obtain a list of the SID groups
    var securityGroups = 
        groups.Select(g => g.Translate(typeof(NTAccount)).ToString()).ToList();

    // If needed, obtain the user's Windows identity name
    var windowsIdentityName = windowsIdentity.Name;

    // Check if the user is in a specific SID group
    if (User.IsInRole(@"{DOMAIN}\{SID GROUP NAME}"))
    {
        // User is in the specified group
    }
    else
    {
        // User isn't in the specified group
    }
}
else
{
    // The user isn't authenticated with Windows Authentication
}
```

For a demonstration of related code that translates SID group claims into human-readable values in a Blazor app, see the `UserClaims` component in [Secure an ASP.NET Core Blazor Web App with Windows Authentication](../../blazor/security/blazor-web-app-with-windows-authentication?view=aspnetcore-10.0). Such an approach to retrieving SID group claims can be combined with [adding claims with an `IClaimsTransformation`](../authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation) to create custom role claims when a user is authenticated.

An approach similar to the preceding example for retrieving SID group claims can be combined with [adding claims with an `IClaimsTransformation`](../authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation) to create custom role claims when a user is authenticated.

## Related content

* [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor WebAssembly with Microsoft Entra ID groups and roles](../../blazor/security/webassembly/microsoft-entra-id-groups-and-roles?view=aspnetcore-10.0)
* [Role-based authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/roles?view=aspnetcore-10.0)
* [Role-based authorization in ASP.NET Core MVC](../../mvc/security/authorization/roles?view=aspnetcore-10.0)
* [Extend or add custom claims, including role claims, using `IClaimsTransformation`](../authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/rendering
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/rendering.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Razor component rendering

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains Razor component rendering in ASP.NET Core Blazor apps, including when to call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) to manually trigger a component to render.

## Rendering conventions for `ComponentBase`

Components *must* render when they're first added to the component hierarchy by a parent component. This is the only time that a component must render. Components *may* render at other times according to their own logic and conventions.

Razor components inherit from the [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) base class, which contains logic to trigger rerendering at the following times:

* After applying an updated set of [parameters](data-binding?view=aspnetcore-10.0#binding-with-component-parameters) from a parent component.
* After applying an updated value for a [cascading parameter](cascading-values-and-parameters?view=aspnetcore-10.0).
* After notification of an event and invoking one of its own [event handlers](event-handling?view=aspnetcore-10.0).
* After a call to its own [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) method (see [ASP.NET Core Razor component lifecycle](lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged)). For guidance on how to prevent overwriting child component parameters when [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is called in a parent component, see [Avoid overwriting parameters in ASP.NET Core Blazor](overwriting-parameters?view=aspnetcore-10.0).

Components inherited from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) skip rerenders due to parameter updates if either of the following are true:

* All of the parameters are from a set of known types† or any [primitive type](/en-us/dotnet/api/system.type.isprimitive) that hasn't changed since the previous set of parameters were set.

  †The Blazor framework uses a set of built-in rules and explicit parameter type checks for change detection. These rules and the types are subject to change at any time. For more information, see the [`ChangeDetection` API in the ASP.NET Core reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ChangeDetection.cs).

  Note

  Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

* The override of the component's [`ShouldRender` method](#suppress-ui-refreshing-shouldrender) returns `false` (the default `ComponentBase` implementation always returns `true`).

## Control the rendering flow

In most cases, [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) conventions result in the correct subset of component rerenders after an event occurs. Developers aren't usually required to provide manual logic to tell the framework which components to rerender and when to rerender them. The overall effect of the framework's conventions is that the component receiving an event rerenders itself, which recursively triggers rerendering of descendant components whose parameter values may have changed.

For more information on the performance implications of the framework's conventions and how to optimize an app's component hierarchy for rendering, see [ASP.NET Core Blazor rendering performance best practices](../performance/rendering?view=aspnetcore-10.0).

## Streaming rendering

Use *streaming rendering* with [static server-side rendering (static SSR)](render-modes?view=aspnetcore-10.0) or prerendering to stream content updates on the response stream and improve the user experience for components that perform long-running asynchronous tasks to fully render.

For example, consider a component that makes a long-running database query or web API call to render data when the page loads. Normally, asynchronous tasks executed as part of rendering a server-side component must complete before the rendered response is sent, which can delay loading the page. Any significant delay in rendering the page harms the user experience. To improve the user experience, streaming rendering initially renders the entire page quickly with placeholder content while asynchronous operations execute. After the operations are complete, the updated content is sent to the client on the same response connection and patched into the DOM.

Streaming rendering requires the server to avoid buffering the output. The response data must flow to the client as the data is generated. For hosts that enforce buffering, streaming rendering degrades gracefully, and the page loads without streaming rendering.

To stream content updates when using static server-side rendering (static SSR) or prerendering, apply the [`[StreamRendering]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.streamrenderingattribute) in .NET 9 or later (use `[StreamRendering(true)]` in .NET 8) to the component. Streaming rendering must be explicitly enabled because streamed updates may cause content on the page to shift. Components without the attribute automatically adopt streaming rendering if the parent component uses the feature. Pass `false` to the attribute in a child component to disable the feature at that point and further down the component subtree. The attribute is functional when applied to components supplied by a [Razor class library](class-libraries?view=aspnetcore-10.0).

If [enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling) is active, streaming rendering renders [Not Found responses](../fundamentals/navigation?view=aspnetcore-10.0#not-found-responses) without reloading the page. When enhanced navigation is blocked, the framework redirects to Not Found content with a page refresh.

Streaming rendering can only render components that have a route, such as a [`NotFoundPage` assignment](../fundamentals/navigation?view=aspnetcore-10.0#not-found-responses) (`NotFoundPage="..."`) or a [status code pages re-execution middleware page assignment](../../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepageswithreexecute) ([UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute)). The Not Found render fragment (`<NotFound>...</NotFound>`) and the `DefaultNotFound` 404 content ("`Not found`" plain text) don't have routes, so they can't be used during streaming rendering.

Streaming `NavigationManager.NotFound` content rendering uses (in order):

* A `NotFoundPage` passed to the `Router` component, if present.
* A status code pages re-execution middleware page, if configured.
* No action if neither of the preceding approaches is adopted.

Non-streaming `NavigationManager.NotFound` content rendering uses (in order):

* A `NotFoundPage` passed to the `Router` component, if present.
* Not Found render fragment content, if present. *Not recommended in .NET 10 or later.*
* `DefaultNotFound` 404 content ("`Not found`" plain text).

[Status code pages re-execution middleware](../../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepageswithreexecute) with [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) takes precedence for browser-based address routing problems, such as an incorrect URL typed into the browser's address bar or selecting a link that has no endpoint in the app.

The following example is based on the `Weather` component in an app created from the [Blazor Web App project template](../project-structure?view=aspnetcore-10.0#blazor-web-app). The call to [Task.Delay](/en-us/dotnet/api/system.threading.tasks.task.delay) simulates retrieving weather data asynchronously. The component initially renders placeholder content ("`Loading...`") without waiting for the asynchronous delay to complete. When the asynchronous delay completes and the weather data content is generated, the content is streamed to the response and patched into the weather forecast table.

`Weather.razor`:

```
@page "/weather"
@attribute [StreamRendering]

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        ...
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    ...

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(500);

        ...

        forecasts = ...
    }
}
```

## Suppress UI refreshing (`ShouldRender`)

[ShouldRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.shouldrender) is called each time a component is rendered. Override [ShouldRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.shouldrender) to manage UI refreshing. If the implementation returns `true`, the UI is refreshed.

Even if [ShouldRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.shouldrender) is overridden, the component is always initially rendered.

`ControlRender.razor`:

```
@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender() => shouldRender;

    private void IncrementCount() => currentCount++;
}
```

```
@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender() => shouldRender;

    private void IncrementCount() => currentCount++;
}
```

```
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

For more information on performance best practices pertaining to [ShouldRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.shouldrender), see [ASP.NET Core Blazor rendering performance best practices](../performance/rendering?view=aspnetcore-10.0#avoid-unnecessary-rendering-of-component-subtrees).

## `StateHasChanged`

Calling [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) enqueues a rerender to occur when the app's main thread is free.

Components are enqueued for rendering, and they aren't enqueued again if there's already a pending rerender. If a component calls [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) five times in a row in a loop, the component only renders once. This behavior is encoded in [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase), which checks first if it has queued a rerender before enqueuing an additional one.

A component can render multiple times during the same cycle, which commonly occurs when a component has children that interact with each other:

* A parent component renders several children.
* Child components render and trigger an update on the parent.
* A parent component rerenders with new state.

This design allows for [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) to be called when necessary without the risk of introducing unnecessary rendering. You can always take control of this behavior in individual components by implementing [IComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.icomponent) directly and manually handling when the component renders.

Consider the following `IncrementCount` method that increments a count, calls [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged), and increments the count again:

```
private void IncrementCount()
{
    currentCount++;
    StateHasChanged();
    currentCount++;
}
```

Stepping through the code in the debugger, you might think that the count updates in the UI for the first `currentCount++` execution immediately after [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is called. However, the UI doesn't show an updated count at that point due to the synchronous processing taking place for this method's execution. There's no opportunity for the renderer to render the component until after the event handler is finished. The UI displays increases for both `currentCount++` executions *in a single render*.

If you await something between the `currentCount++` lines, the awaited call gives the renderer a chance to render. This has led to some developers calling [Delay](/en-us/dotnet/api/system.threading.tasks.task.delay) with a one millisecond delay in their components to allow a render to occur, but we don't recommend arbitrarily slowing down an app to enqueue a render.

The best approach is to await [Task.Yield](/en-us/dotnet/api/system.threading.tasks.task.yield), which forces the component to process code asynchronously and render during the current batch with a second render in a separate batch after the yielded task runs the continuation.

Consider the following revised `IncrementCount` method, which updates the UI twice because the render enqueued by [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is performed when the task is yielded with the call to [Task.Yield](/en-us/dotnet/api/system.threading.tasks.task.yield):

```
private async Task IncrementCount()
{
    currentCount++;
    StateHasChanged();
    await Task.Yield();
    currentCount++;
}
```

Be careful not to call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) unnecessarily, which is a common mistake that imposes unnecessary rendering costs. Code shouldn't need to call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) when:

* Routinely handling events, whether synchronously or asynchronously, since [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) triggers a render for most routine event handlers.
* Implementing typical lifecycle logic, such as [`OnInitialized`](lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) or [`OnParametersSetAsync`](lifecycle?view=aspnetcore-10.0#after-parameters-are-set-onparameterssetasync), whether synchronously or asynchronously, since [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) triggers a render for typical lifecycle events.

However, it might make sense to call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) in the cases described in the following sections of this article:

* [An asynchronous handler involves multiple asynchronous phases](#an-asynchronous-handler-involves-multiple-asynchronous-phases)
* [Receiving a call from something external to the Blazor rendering and event handling system](#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system)
* [To render component outside the subtree that is rerendered by a particular event](#to-render-a-component-outside-the-subtree-thats-rerendered-by-a-particular-event)

### An asynchronous handler involves multiple asynchronous phases

Due to the way that tasks are defined in .NET, a receiver of a [Task](/en-us/dotnet/api/system.threading.tasks.task) can only observe its final completion, not intermediate asynchronous states. Therefore, [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) can only trigger rerendering when the [Task](/en-us/dotnet/api/system.threading.tasks.task) is first returned and when the [Task](/en-us/dotnet/api/system.threading.tasks.task) finally completes. The framework can't know to rerender a component at other intermediate points, such as when an [IAsyncEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1) [returns data in a series of intermediate `Task`s](https://github.com/dotnet/aspnetcore/issues/43098#issuecomment-1206224427). If you want to rerender at intermediate points, call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) at those points.

Consider the following `CounterState1` component, which updates the count four times each time the `IncrementCount` method executes:

* Automatic renders occur after the first and last increments of `currentCount`.
* Manual renders are triggered by calls to [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) when the framework doesn't automatically trigger rerenders at intermediate processing points where `currentCount` is incremented.

`CounterState1.razor`:

```
@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
```

```
@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
```

```
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
```

```
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
```

```
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
```

```
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
```

### Receiving a call from something external to the Blazor rendering and event handling system

[ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) only knows about its own lifecycle methods and Blazor-triggered events. [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) doesn't know about other events that may occur in code. For example, any C# events raised by a custom data store are unknown to Blazor. In order for such events to trigger rerendering to display updated values in the UI, call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged).

Consider the following `CounterState2` component that uses [System.Timers.Timer](/en-us/dotnet/api/system.timers.timer) to update a count at a regular interval and calls [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) to update the UI:

* `OnTimerCallback` runs outside of any Blazor-managed rendering flow or event notification. Therefore, `OnTimerCallback` must call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) because Blazor isn't aware of the changes to `currentCount` in the callback.
* The component implements [IDisposable](/en-us/dotnet/api/system.idisposable), where the [Timer](/en-us/dotnet/api/system.timers.timer) is disposed when the framework calls the `Dispose` method. For more information, see [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

Because the callback is invoked outside of Blazor's synchronization context, the component must wrap the logic of `OnTimerCallback` in [ComponentBase.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync) to move it onto the renderer's synchronization context. This is equivalent to marshalling to the UI thread in other UI frameworks. [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) can only be called from the renderer's synchronization context and throws an exception otherwise:

> System.InvalidOperationException: 'The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.'

`CounterState2.razor`:

```
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

```
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

```
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

```
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

```
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

```
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

### To render a component outside the subtree that's rerendered by a particular event

The UI might involve:

1. Dispatching an event to one component.
2. Changing some state.
3. Rerendering a completely different component that isn't a descendant of the component receiving the event.

One way to deal with this scenario is to provide a *state management* class, often as a dependency injection (DI) service, injected into multiple components. When one component calls a method on the state manager, the state manager raises a C# event that's then received by an independent component.

For approaches to manage state, see the following resources:

* [Bind across more than two components](data-binding?view=aspnetcore-10.0#bind-across-more-than-two-components) using data bindings.
* [Pass data across a component hierarchy](cascading-values-and-parameters?view=aspnetcore-10.0#pass-data-across-a-component-hierarchy) using cascading values and parameters.
* [In-memory state container service](../state-management/?view=aspnetcore-10.0#in-memory-state-container-service) section of the *State management* overview.

For the state manager approach, C# events are outside the Blazor rendering pipeline. Call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) on other components you wish to rerender in response to the state manager's events.

The state manager approach is similar to the earlier case with [System.Timers.Timer](/en-us/dotnet/api/system.timers.timer) in the [previous section](#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system). Since the execution call stack typically remains on the renderer's synchronization context, calling [InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync) isn't normally required. Calling [InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync) is only required if the logic escapes the synchronization context, such as calling [ContinueWith](/en-us/dotnet/api/system.threading.tasks.task.continuewith) on a [Task](/en-us/dotnet/api/system.threading.tasks.task) or awaiting a [Task](/en-us/dotnet/api/system.threading.tasks.task) with [`ConfigureAwait(false)`](/en-us/dotnet/api/system.threading.tasks.task.configureawait). For more information, see the [Receiving a call from something external to the Blazor rendering and event handling system](#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system) section.

## WebAssembly loading progress indicator for Blazor Web Apps

An app can adopt custom code to create a loading progress indicator. For more information, see [ASP.NET Core Blazor startup](../fundamentals/startup?view=aspnetcore-10.0#client-side-loading-indicators).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Overview for gRPC on .NET

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

[gRPC](https://grpc.io) is a language agnostic, high-performance Remote Procedure Call (RPC) framework.

The main benefits of gRPC are:

* Modern, high-performance, lightweight RPC framework.
* Contract-first API development that uses Protocol Buffers by default, allowing for language agnostic implementations.
* Tooling available for many languages to generate strongly typed servers and clients.
* Supports client, server, and bi-directional streaming calls.
* Reduced network usage with Protobuf binary serialization.

These benefits make gRPC ideal for:

* Lightweight microservices where efficiency is critical.
* Polyglot systems where multiple languages are required for development.
* Point-to-point real-time services that need to handle streaming requests or responses.

## C# tooling support for .proto files

gRPC uses a contract-first approach to API development. Services and messages are defined in *.proto* files:

```
syntax = "proto3";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
```

.NET types for services, clients, and messages are automatically generated by including *.proto* files in a project:

* Add a package reference to the [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) package.
* Add *.proto* files to the `<Protobuf>` item group.

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" />
</ItemGroup>
```

For more information on gRPC tooling support, see [gRPC services with C#](basics?view=aspnetcore-10.0).

## gRPC services on ASP.NET Core

gRPC services can be hosted on ASP.NET Core. Services have full integration with ASP.NET Core features such as logging, dependency injection (DI), authentication, and authorization.

### Add gRPC services to an ASP.NET Core app

gRPC requires the [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore) package. For information on configuring gRPC in a .NET app, see [Configure gRPC](aspnetcore?view=aspnetcore-10.0#configure-grpc).

### The gRPC service project template

The **ASP.NET Core gRPC Service** project template provides a starter service:

```
public class GreeterService(ILogger<GreeterService> logger) : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request,
        ServerCallContext context)
    {
        logger.LogInformation("Saying hello to {Name}", request.Name);

        return Task.FromResult(new HelloReply 
        {
            Message = "Hello " + request.Name
        });
    }
}
```

```
public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloReply> SayHello(HelloRequest request,
        ServerCallContext context)
    {
        _logger.LogInformation("Saying hello to {Name}", request.Name);

        return Task.FromResult(new HelloReply 
        {
            Message = "Hello " + request.Name
        });
    }
}
```

`GreeterService` inherits from the `GreeterBase` type, which is generated from the `Greeter` service in the *.proto* file. The service is made accessible to clients in the *Program.cs* file:

```
app.MapGrpcService<GreeterService>();
```

To learn more, see [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0).

## gRPC service calls with a .NET client

gRPC clients are concrete client types that are [generated from .proto files](basics?view=aspnetcore-10.0#generated-c-assets). The concrete gRPC client has methods that translate to the gRPC service in the *.proto* file.

```
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);

var response = await client.SayHelloAsync(
    new HelloRequest { Name = "World" });

Console.WriteLine(response.Message);
```

A gRPC client is created by using a channel, which represents a long-lived connection to a gRPC service. A channel can be created by using `GrpcChannel.ForAddress`.

For more information on creating clients, and calling different service methods, see [Call gRPC services with the .NET client](client?view=aspnetcore-10.0).

## Related content

* [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0)
* [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0)
* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)

[gRPC](https://grpc.io) is a language agnostic, high-performance Remote Procedure Call (RPC) framework.

The main benefits of gRPC are:

* Modern, high-performance, lightweight RPC framework.
* Contract-first API development, using Protocol Buffers by default, allowing for language agnostic implementations.
* Tooling available for many languages to generate strongly typed servers and clients.
* Supports client, server, and bi-directional streaming calls.
* Reduced network usage with Protobuf binary serialization.

These benefits make gRPC ideal for:

* Lightweight microservices where efficiency is critical.
* Polyglot systems where multiple languages are required for development.
* Point-to-point real-time services that need to handle streaming requests or responses.

## C# Tooling support for `.proto` files

gRPC uses a contract-first approach to API development. Services and messages are defined in `.proto` files:

```
syntax = "proto3";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
```

.NET types for services, clients, and messages are automatically generated by including `.proto` files in a project:

* Add a package reference to [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/) package.
* Add `.proto` files to the `<Protobuf>` item group.

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" />
</ItemGroup>
```

For more information on gRPC tooling support, see [gRPC Services with C#](basics?view=aspnetcore-10.0).

## gRPC services on ASP.NET Core

gRPC services can be hosted on ASP.NET Core. Services have full integration with ASP.NET Core features such as logging, dependency injection (DI), authentication, and authorization.

### Add gRPC services to an ASP.NET Core app

gRPC requires the [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore) package. For information on configuring gRPC in a .NET app, see [Configure gRPC](aspnetcore?view=aspnetcore-10.0#configure-grpc).

### The gRPC service project template

The gRPC service project template provides a starter service:

```
public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloReply> SayHello(HelloRequest request,
        ServerCallContext context)
    {
        _logger.LogInformation("Saying hello to {Name}", request.Name);
        return Task.FromResult(new HelloReply 
        {
            Message = "Hello " + request.Name
        });
    }
}
```

`GreeterService` inherits from the `GreeterBase` type, which is generated from the `Greeter` service in the `.proto` file. The service is made accessible to clients in `Startup.cs`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapGrpcService<GreeterService>();
});
```

To learn more about gRPC services on ASP.NET Core, see [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0).

## Call gRPC services with a .NET client

gRPC clients are concrete client types that are [generated from `.proto` files](basics?view=aspnetcore-10.0#generated-c-assets). The concrete gRPC client has methods that translate to the gRPC service in the `.proto` file.

```
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);

var response = await client.SayHelloAsync(
    new HelloRequest { Name = "World" });

Console.WriteLine(response.Message);
```

A gRPC client is created using a channel, which represents a long-lived connection to a gRPC service. A channel can be created using `GrpcChannel.ForAddress`.

For more information on creating clients, and calling different service methods, see [Call gRPC services with the .NET client](client?view=aspnetcore-10.0).

## Related content

* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0)
* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)
* [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0)
* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/intro.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Get started with EF Core in an ASP.NET MVC web app

By [Tom Dykstra](https://github.com/tdykstra) and [Rick Anderson](https://twitter.com/RickAndMSFT)

The Contoso University sample web app demonstrates how to create an ASP.NET Core MVC web app using Entity Framework (EF) Core and Visual Studio.

The sample app is a web site for a fictional Contoso University. It includes functionality such as student admission, course creation, and instructor assignments. This is the first in a series of tutorials that explain how to build the Contoso University sample app.

## Prerequisites

* If you're new to ASP.NET Core MVC, go through the [Get started with ASP.NET Core MVC](../../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0) tutorial series before starting this one.

- [Visual Studio 2022](https://visualstudio.microsoft.com/vs/#download) with the **ASP.NET and web development** workload.
- [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

This tutorial has not been updated for ASP.NET Core in .NET 6 or later. The tutorial's instructions will not work correctly if you create a project that targets ASP.NET Core in .NET 6 or later. For example, the ASP.NET Core in .NET 6 or later web templates use the [minimal hosting model](../../migration/50-to-60?view=aspnetcore-10.0#new-hosting-model), which unifies `Startup.cs` and `Program.cs` into a single `Program.cs` file.

Another difference introduced in .NET 6 is the NRT [(nullable reference types)](/en-us/ef/core/miscellaneous/nullable-reference-types) feature. The project templates enable this feature by default. Problems can happen where EF considers a property to be required in .NET 6 which is nullable in .NET 5. For example, the Create Student page will fail silently unless the `Enrollments` property is made nullable or the `asp-validation-summary` helper tag is changed from `ModelOnly` to `All`.

We recommend that you install and use the .NET 5 SDK for this tutorial. Until this tutorial is updated, see [Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8](../ef-rp/intro?view=aspnetcore-10.0) on how to use Entity Framework with ASP.NET Core in .NET 6 or later.

## Database engines

The Visual Studio instructions use [SQL Server LocalDB](/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb), a version of SQL Server Express that runs only on Windows.

## Solve problems and troubleshoot

If you run into a problem you can't resolve, you can generally find the solution by comparing your code to the [completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples). For a list of common errors and how to solve them, see [the Troubleshooting section of the last tutorial in the series](advanced?view=aspnetcore-10.0#common-errors). If you don't find what you need there, you can post a question to StackOverflow\.com for [ASP.NET Core](https://stackoverflow.com/questions/tagged/asp.net-core) or [EF Core](https://stackoverflow.com/questions/tagged/entity-framework-core).

Tip

This is a series of 10 tutorials, each of which builds on what is done in earlier tutorials. Consider saving a copy of the project after each successful tutorial completion. Then if you run into problems, you can start over from the previous tutorial instead of going back to the beginning of the whole series.

## Contoso University web app

The app built in these tutorials is a basic university web site.

Users can view and update student, course, and instructor information. Here are a few of the screens in the app:

## Create web app

1. Start Visual Studio and select **Create a new project**.

2. In the **Create a new project** dialog, select **ASP.NET Core Web Application** > **Next**.

3. In the **Configure your new project** dialog, enter `ContosoUniversity` for **Project name**. It's important to use this exact name including capitalization, so each `namespace` matches when code is copied.

4. Select **Create**.

5. In the **Create a new ASP.NET Core web application** dialog, select:

   1. **.NET Core** and **ASP.NET Core 5.0** in the dropdowns.
   2. **ASP.NET Core Web App (Model-View-Controller)**.
   3. **Create**

## Set up the site style

A few basic changes set up the site menu, layout, and home page.

Open `Views/Shared/_Layout.cshtml` and make the following changes:

* Change each occurrence of `ContosoUniversity` to `Contoso University`. There are three occurrences.
* Add menu entries for **About**, **Students**, **Courses**, **Instructors**, and **Departments**, and delete the **Privacy** menu entry.

The preceding changes are highlighted in the following code:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Contoso University</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Contoso University</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="About">About</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Students" asp-action="Index">Students</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Courses" asp-action="Index">Courses</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Departments" asp-action="Index">Departments</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2020 - Contoso University - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

In `Views/Home/Index.cshtml`, replace the contents of the file with the following markup:

```
@{
    ViewData["Title"] = "Home Page";
}

<div class="jumbotron">
    <h1>Contoso University</h1>
</div>
<div class="row">
    <div class="col-md-4">
        <h2>Welcome to Contoso University</h2>
        <p>
            Contoso University is a sample application that
            demonstrates how to use Entity Framework Core in an
            ASP.NET Core MVC web application.
        </p>
    </div>
    <div class="col-md-4">
        <h2>Build it from scratch</h2>
        <p>You can build the application by following the steps in a series of tutorials.</p>
        <p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the tutorial &raquo;</a></p>
    </div>
    <div class="col-md-4">
        <h2>Download it</h2>
        <p>You can download the completed project from GitHub.</p>
        <p><a class="btn btn-default" href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final">See project source code &raquo;</a></p>
    </div>
</div>
```

Press CTRL+F5 to run the project or choose **Debug > Start Without Debugging** from the menu. The home page is displayed with tabs for the pages created in this tutorial.

## EF Core NuGet packages

This tutorial uses SQL Server, and the provider package is [Microsoft.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer/).

The EF SQL Server package and its dependencies, `Microsoft.EntityFrameworkCore` and `Microsoft.EntityFrameworkCore.Relational`, provide runtime support for EF.

Add the [Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore) NuGet package. In the Package Manager Console (PMC), enter the following commands to add the NuGet packages:

```
Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
```

The `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` NuGet package provides ASP.NET Core middleware for EF Core error pages. This middleware helps to detect and diagnose errors with EF Core migrations.

For information about other database providers that are available for EF Core, see [Database providers](/en-us/ef/core/providers/).

## Create the data model

The following entity classes are created for this app:

The preceding entities have the following relationships:

* A one-to-many relationship between `Student` and `Enrollment` entities. A student can be enrolled in any number of courses.
* A one-to-many relationship between `Course` and `Enrollment` entities. A course can have any number of students enrolled in it.

In the following sections, a class is created for each of these entities.

### The Student entity

In the *Models* folder, create the `Student` class with the following code:

```
using System;
using System.Collections.Generic;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The `ID` property is the primary key (**PK**) column of the database table that corresponds to this class. By default, EF interprets a property that's named `ID` or `classnameID` as the primary key. For example, the PK could be named `StudentID` rather than `ID`.

The `Enrollments` property is a [navigation property](/en-us/ef/core/modeling/relationships). Navigation properties hold other entities that are related to this entity. The `Enrollments` property of a `Student` entity:

* Contains all of the `Enrollment` entities that are related to that `Student` entity.
* If a specific `Student` row in the database has two related `Enrollment` rows:
  * That `Student` entity's `Enrollments` navigation property contains those two `Enrollment` entities.

`Enrollment` rows contain a student's PK value in the `StudentID` foreign key (**FK**) column.

If a navigation property can hold multiple entities:

* The type must be a list, such as `ICollection<T>`, `List<T>`, or `HashSet<T>`.
* Entities can be added, deleted, and updated.

Many-to-many and one-to-many navigation relationships can contain multiple entities. When `ICollection<T>` is used, EF creates a `HashSet<T>` collection by default.

### The Enrollment entity

In the *Models* folder, create the `Enrollment` class with the following code:

```
namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}
```

The `EnrollmentID` property is the PK. This entity uses the `classnameID` pattern instead of `ID` by itself. The `Student` entity used the `ID` pattern. Some developers prefer to use one pattern throughout the data model. In this tutorial, the variation illustrates that either pattern can be used. A [later tutorial](inheritance?view=aspnetcore-10.0) shows how using `ID` without classname makes it easier to implement inheritance in the data model.

The `Grade` property is an `enum`. The `?` after the `Grade` type declaration indicates that the `Grade` property is [nullable](/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types). A grade that's `null` is different from a zero grade. `null` means a grade isn't known or hasn't been assigned yet.

The `StudentID` property is a foreign key (FK), and the corresponding navigation property is `Student`. An `Enrollment` entity is associated with one `Student` entity, so the property can only hold a single `Student` entity. This differs from the `Student.Enrollments` navigation property, which can hold multiple `Enrollment` entities.

The `CourseID` property is a FK, and the corresponding navigation property is `Course`. An `Enrollment` entity is associated with one `Course` entity.

Entity Framework interprets a property as a FK property if it's named `<`navigation property name`><`primary key property name`>`. For example, `StudentID` for the `Student` navigation property since the `Student` entity's PK is `ID`. FK properties can also be named `<`primary key property name`>`. For example, `CourseID` because the `Course` entity's PK is `CourseID`.

### The Course entity

In the *Models* folder, create the `Course` class with the following code:

```
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int CourseID { get; set; }
        public string Title { get; set; }
        public int Credits { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The `Enrollments` property is a navigation property. A `Course` entity can be related to any number of `Enrollment` entities.

The [DatabaseGenerated](/en-us/dotnet/api/system.componentmodel.dataannotations.databasegeneratedattribute) attribute is explained in a [later tutorial](complex-data-model?view=aspnetcore-10.0). This attribute allows entering the PK for the course rather than having the database generate it.

## Create the database context

The main class that coordinates EF functionality for a given data model is the [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) database context class. This class is created by deriving from the `Microsoft.EntityFrameworkCore.DbContext` class. The `DbContext` derived class specifies which entities are included in the data model. Some EF behaviors can be customized. In this project, the class is named `SchoolContext`.

In the project folder, create a folder named `Data`.

In the *Data* folder create a `SchoolContext` class with the following code:

```
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
    }
}
```

The preceding code creates a `DbSet` property for each entity set. In EF terminology:

* An entity set typically corresponds to a database table.
* An entity corresponds to a row in the table.

The `DbSet<Enrollment>` and `DbSet<Course>` statements could be omitted and it would work the same. EF would include them implicitly because:

* The `Student` entity references the `Enrollment` entity.
* The `Enrollment` entity references the `Course` entity.

When the database is created, EF creates tables that have names the same as the `DbSet` property names. Property names for collections are typically plural. For example, `Students` rather than `Student`. Developers disagree about whether table names should be pluralized or not. For these tutorials, the default behavior is overridden by specifying singular table names in the `DbContext`. To do that, add the following highlighted code after the last DbSet property.

```
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
        }
    }
}
```

## Register the `SchoolContext`

ASP.NET Core includes [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the EF database context, are registered with dependency injection during app startup. Components that require these services, such as MVC controllers, are provided these services via constructor parameters. The controller constructor code that gets a context instance is shown later in this tutorial.

To register `SchoolContext` as a service, open `Startup.cs`, and add the highlighted lines to the `ConfigureServices` method.

```
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ContosoUniversity
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<SchoolContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddControllersWithViews();
        }
```

The name of the connection string is passed in to the context by calling a method on a `DbContextOptionsBuilder` object. For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

Open the `appsettings.json` file and add a connection string as shown in the following markup:

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

### Add the database exception filter

Add [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) to `ConfigureServices` as shown in the following code:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<SchoolContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDatabaseDeveloperPageExceptionFilter();

    services.AddControllersWithViews();
}
```

The `AddDatabaseDeveloperPageExceptionFilter` provides helpful error information in the [development environment](../../fundamentals/environments?view=aspnetcore-10.0).

### SQL Server Express LocalDB

The connection string specifies [SQL Server LocalDB](/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb). LocalDB is a lightweight version of the SQL Server Express Database Engine and is intended for app development, not production use. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default, LocalDB creates *.mdf* DB files in the `C:/Users/<user>` directory.

## Initialize DB with test data

EF creates an empty database. In this section, a method is added that's called after the database is created in order to populate it with test data.

The `EnsureCreated` method is used to automatically create the database. In a [later tutorial](migrations?view=aspnetcore-10.0), you see how to handle model changes by using Code First Migrations to change the database schema instead of dropping and re-creating the database.

In the *Data* folder, create a new class named `DbInitializer` with the following code:

```
using ContosoUniversity.Models;
using System;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
            new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")},
            new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")},
            new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")}
            };
            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
            new Course{CourseID=1050,Title="Chemistry",Credits=3},
            new Course{CourseID=4022,Title="Microeconomics",Credits=3},
            new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
            new Course{CourseID=1045,Title="Calculus",Credits=4},
            new Course{CourseID=3141,Title="Trigonometry",Credits=4},
            new Course{CourseID=2021,Title="Composition",Credits=3},
            new Course{CourseID=2042,Title="Literature",Credits=4}
            };
            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
            new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
            new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
            new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
            new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
            new Enrollment{StudentID=3,CourseID=1050},
            new Enrollment{StudentID=4,CourseID=1050},
            new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
            new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
            new Enrollment{StudentID=6,CourseID=1045},
            new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
            };
            foreach (Enrollment e in enrollments)
            {
                context.Enrollments.Add(e);
            }
            context.SaveChanges();
        }
    }
}
```

The preceding code checks if the database exists:

* If the database is not found;
  * It is created and loaded with test data. It loads test data into arrays rather than `List<T>` collections to optimize performance.
* If the database is found, it takes no action.

Update `Program.cs` with the following code:

```
using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            CreateDbIfNotExists(host);

            host.Run();
        }

        private static void CreateDbIfNotExists(IHost host)
        {
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    var context = services.GetRequiredService<SchoolContext>();
                    DbInitializer.Initialize(context);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

`Program.cs` does the following on app startup:

* Get a database context instance from the dependency injection container.
* Call the `DbInitializer.Initialize` method.
* Dispose the context when the `Initialize` method completes as shown in the following code:

```
public static void Main(string[] args)
{
     var host = CreateWebHostBuilder(args).Build();

    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        try
        {
            var context = services.GetRequiredService<SchoolContext>();
            DbInitializer.Initialize(context);
        }
        catch (Exception ex)
        {
            var logger = services.GetRequiredService<ILogger<Program>>();
            logger.LogError(ex, "An error occurred while seeding the database.");
        }
    }

    host.Run();
}
```

The first time the app is run, the database is created and loaded with test data. Whenever the data model changes:

* Delete the database.
* Update the seed method, and start afresh with a new database.

In later tutorials, the database is modified when the data model changes, without deleting and re-creating it. No data is lost when the data model changes.

## Create controller and views

Use the scaffolding engine in Visual Studio to add an MVC controller and views that will use EF to query and save data.

The automatic creation of [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) action methods and views is known as scaffolding.

* In **Solution Explorer**, right-click the `Controllers` folder and select **Add > New Scaffolded Item**.

* In the **Add Scaffold** dialog box:

  * Select **MVC controller with views, using Entity Framework**.
  * Click **Add**. The **Add MVC Controller with views, using Entity Framework** dialog box appears:
  * In **Model class**, select **Student**.
  * In **Data context class**, select **SchoolContext**.
  * Accept the default **StudentsController** as the name.
  * Click **Add**.

The Visual Studio scaffolding engine creates a `StudentsController.cs` file and a set of views (`*.cshtml` files) that work with the controller.

Notice the controller takes a `SchoolContext` as a constructor parameter.

```
namespace ContosoUniversity.Controllers
{
    public class StudentsController : Controller
    {
        private readonly SchoolContext _context;

        public StudentsController(SchoolContext context)
        {
            _context = context;
        }
```

ASP.NET Core dependency injection takes care of passing an instance of `SchoolContext` into the controller. You configured that in the `Startup` class.

The controller contains an `Index` action method, which displays all students in the database. The method gets a list of students from the Students entity set by reading the `Students` property of the database context instance:

```
public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}
```

The asynchronous programming elements in this code are explained later in the tutorial.

The `Views/Students/Index.cshtml` view displays this list in a table:

```
@model IEnumerable<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.LastName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.EnrollmentDate)
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.LastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FirstMidName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Press CTRL+F5 to run the project or choose **Debug > Start Without Debugging** from the menu.

Click the Students tab to see the test data that the `DbInitializer.Initialize` method inserted. Depending on how narrow your browser window is, you'll see the `Students` tab link at the top of the page or you'll have to click the navigation icon in the upper right corner to see the link.

## View the database

When the app is started, the `DbInitializer.Initialize` method calls `EnsureCreated`. EF saw that there was no database:

* So it created a database.
* The `Initialize` method code populated the database with data.

Use **SQL Server Object Explorer** (SSOX) to view the database in Visual Studio:

* Select **SQL Server Object Explorer** from the **View** menu in Visual Studio.
* In SSOX, select **(localdb)\MSSQLLocalDB > Databases**.
* Select `ContosoUniversity1`, the entry for the database name that's in the connection string in the `appsettings.json` file.
* Expand the **Tables** node to see the tables in the database.

Right-click the **Student** table and click **View Data** to see the data in the table.

The `*.mdf` and `*.ldf` database files are in the *C:\Users\\\<username>* folder.

Because `EnsureCreated` is called in the initializer method that runs on app start, you could:

* Make a change to the `Student` class.
* Delete the database.
* Stop, then start the app. The database is automatically re-created to match the change.

For example, if an `EmailAddress` property is added to the `Student` class, a new `EmailAddress` column is created in the re-created table. The view won't display the new `EmailAddress` property.

## Conventions

The amount of code written in order for the EF to create a complete database is minimal because of the use of the conventions EF uses:

* The names of `DbSet` properties are used as table names. For entities not referenced by a `DbSet` property, entity class names are used as table names.
* Entity property names are used for column names.
* Entity properties that are named `ID` or `classnameID` are recognized as PK properties.
* A property is interpreted as a FK property if it's named `<`navigation property name`><`PK property name`>`. For example, `StudentID` for the `Student` navigation property since the `Student` entity's PK is `ID`. FK properties can also be named `<`primary key property name`>`. For example, `EnrollmentID` since the `Enrollment` entity's PK is `EnrollmentID`.

Conventional behavior can be overridden. For example, table names can be explicitly specified, as shown earlier in this tutorial. Column names and any property can be set as a PK or FK.

## Asynchronous code

Asynchronous programming is the default mode for ASP.NET Core and EF Core.

A web server has a limited number of threads available, and in high load situations all of the available threads might be in use. When that happens, the server can't process new requests until the threads are freed up. With synchronous code, many threads may be tied up while they aren't actually doing any work because they're waiting for I/O to complete. With asynchronous code, when a process is waiting for I/O to complete, its thread is freed up for the server to use for processing other requests. As a result, asynchronous code enables server resources to be used more efficiently, and the server is enabled to handle more traffic without delays.

Asynchronous code does introduce a small amount of overhead at run time, but for low traffic situations the performance hit is negligible, while for high traffic situations, the potential performance improvement is substantial.

In the following code, `async`, `Task<T>`, `await`, and `ToListAsync` make the code execute asynchronously.

```
public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}
```

* The `async` keyword tells the compiler to generate callbacks for parts of the method body and to automatically create the `Task<IActionResult>` object that's returned.
* The return type `Task<IActionResult>` represents ongoing work with a result of type `IActionResult`.
* The `await` keyword causes the compiler to split the method into two parts. The first part ends with the operation that's started asynchronously. The second part is put into a callback method that's called when the operation completes.
* `ToListAsync` is the asynchronous version of the `ToList` extension method.

Some things to be aware of when writing asynchronous code that uses EF:

* Only statements that cause queries or commands to be sent to the database are executed asynchronously. That includes, for example, `ToListAsync`, `SingleOrDefaultAsync`, and `SaveChangesAsync`. It doesn't include, for example, statements that just change an `IQueryable`, such as `var students = context.Students.Where(s => s.LastName == "Davolio")`.
* An EF context isn't thread safe: don't try to do multiple operations in parallel. When you call any async EF method, always use the `await` keyword.
* To take advantage of the performance benefits of async code, make sure that any library packages used also use async if they call any EF methods that cause queries to be sent to the database.

For more information about asynchronous programming in .NET, see [Async Overview](/en-us/dotnet/articles/standard/async).

## Limit entities fetched

See [Performance considerations](../ef-rp/intro?view=aspnetcore-10.0#performance-considerations) for information on limiting the number of entities returned from a query.

## SQL Logging of Entity Framework Core

Logging configuration is commonly provided by the `Logging` section of `appsettings.{Environment}.json` files. To log SQL statements, add `"Microsoft.EntityFrameworkCore.Database.Command": "Information"` to the `appsettings.Development.json` file:

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDB-2;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
     ,"Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

With the preceding JSON, SQL statements are displayed on the command line and in the Visual Studio output window.

For more information, see the following resources:

* [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0#configuration)
* [ASP.NET template disables EF Core SQL logging by default (`dotnet/aspnetcore` #32977)](https://github.com/dotnet/aspnetcore/issues/32977)

Advance to the next tutorial to learn how to perform basic CRUD (create, read, update, delete) operations.

[Implement basic CRUD functionality](crud?view=aspnetcore-10.0)

The Contoso University sample web application demonstrates how to create ASP.NET Core 2.2 MVC web applications using Entity Framework (EF) Core 2.2 and Visual Studio 2019.

This tutorial has not been updated for ASP.NET Core 3.1. It has been updated for [ASP.NET Core in .NET 5](intro?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-5.0).

The sample application is a web site for a fictional Contoso University. It includes functionality such as student admission, course creation, and instructor assignments. This is the first in a series of tutorials that explain how to build the Contoso University sample application from scratch.

## Prerequisites

* [.NET Core SDK 2.2](https://dotnet.microsoft.com/download)

* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the following workloads:

  * **ASP.NET and web development** workload
  * **.NET Core cross-platform development** workload

## Troubleshooting

If you run into a problem you can't resolve, you can generally find the solution by comparing your code to the [completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples). For a list of common errors and how to solve them, see [the Troubleshooting section of the last tutorial in the series](advanced?view=aspnetcore-10.0#common-errors). If you don't find what you need there, you can post a question to StackOverflow\.com for [ASP.NET Core](https://stackoverflow.com/questions/tagged/asp.net-core) or [EF Core](https://stackoverflow.com/questions/tagged/entity-framework-core).

Tip

This is a series of 10 tutorials, each of which builds on what is done in earlier tutorials. Consider saving a copy of the project after each successful tutorial completion. Then if you run into problems, you can start over from the previous tutorial instead of going back to the beginning of the whole series.

## Contoso University web app

The application you'll be building in these tutorials is a simple university web site.

Users can view and update student, course, and instructor information. Here are a few of the screens you'll create.

## Create web app

* Open Visual Studio.

* From the **File** menu, select **New > Project**.

* From the left pane, select **Installed > Visual C# > Web**.

* Select the **ASP.NET Core Web Application** project template.

* Enter **ContosoUniversity** as the name and click **OK**.

* Wait for the **New ASP.NET Core Web Application** dialog to appear.

* Select **.NET Core**, **ASP.NET Core 2.2** and the **Web Application (Model-View-Controller)** template.

* Make sure **Authentication** is set to **No Authentication**.

* Select **OK**

## Set up the site style

A few simple changes will set up the site menu, layout, and home page.

Open `Views/Shared/_Layout.cshtml` and make the following changes:

* Change each occurrence of "ContosoUniversity" to "Contoso University". There are three occurrences.

* Add menu entries for **About**, **Students**, **Courses**, **Instructors**, and **Departments**, and delete the **Privacy** menu entry.

The changes are highlighted.

```
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Contoso University</title>

    <environment include="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
              crossorigin="anonymous"
              integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
    </environment>
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Contoso University</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="About">About</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Students" asp-action="Index">Students</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Courses" asp-action="Index">Courses</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Departments" asp-action="Index">Departments</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <partial name="_CookieConsentPartial" />
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2019 - Contoso University - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>

    <environment include="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    </environment>
    <environment exclude="Development">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.js"
                asp-fallback-test="window.jQuery"
                crossorigin="anonymous"
                integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
        </script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/js/bootstrap.bundle.js"
                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"
                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
                crossorigin="anonymous"
                integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
        </script>
    </environment>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>
```

In `Views/Home/Index.cshtml`, replace the contents of the file with the following code to replace the text about ASP.NET and MVC with text about this application:

```
@{
    ViewData["Title"] = "Home Page";
}

<div class="jumbotron">
    <h1>Contoso University</h1>
</div>
<div class="row">
    <div class="col-md-4">
        <h2>Welcome to Contoso University</h2>
        <p>
            Contoso University is a sample application that
            demonstrates how to use Entity Framework Core in an
            ASP.NET Core MVC web application.
        </p>
    </div>
    <div class="col-md-4">
        <h2>Build it from scratch</h2>
        <p>You can build the application by following the steps in a series of tutorials.</p>
        <p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the tutorial &raquo;</a></p>
    </div>
    <div class="col-md-4">
        <h2>Download it</h2>
        <p>You can download the completed project from GitHub.</p>
        <p><a class="btn btn-default" href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final">See project source code &raquo;</a></p>
    </div>
</div>
```

Press CTRL+F5 to run the project or choose **Debug > Start Without Debugging** from the menu. You see the home page with tabs for the pages you'll create in these tutorials.

## About EF Core NuGet packages

To add EF Core support to a project, install the database provider that you want to target. This tutorial uses SQL Server, and the provider package is [Microsoft.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer/). This package is included in the [Microsoft.AspNetCore.App metapackage](../../fundamentals/metapackage-app?view=aspnetcore-10.0), so you don't need to reference the package.

The EF SQL Server package and its dependencies (`Microsoft.EntityFrameworkCore` and `Microsoft.EntityFrameworkCore.Relational`) provide runtime support for EF. You'll add a tooling package later, in the [Migrations](migrations?view=aspnetcore-10.0) tutorial.

For information about other database providers that are available for Entity Framework Core, see [Database providers](/en-us/ef/core/providers/).

## Create the data model

Next you'll create entity classes for the Contoso University application. You'll start with the following three entities.

There's a one-to-many relationship between `Student` and `Enrollment` entities, and there's a one-to-many relationship between `Course` and `Enrollment` entities. In other words, a student can be enrolled in any number of courses, and a course can have any number of students enrolled in it.

In the following sections you'll create a class for each one of these entities.

### The Student entity

In the *Models* folder, create a class file named `Student.cs` and replace the template code with the following code.

```
using System;
using System.Collections.Generic;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The `ID` property will become the primary key column of the database table that corresponds to this class. By default, the Entity Framework interprets a property that's named `ID` or `classnameID` as the primary key.

The `Enrollments` property is a [navigation property](/en-us/ef/core/modeling/relationships). Navigation properties hold other entities that are related to this entity. In this case, the `Enrollments` property of a `Student entity` will hold all of the `Enrollment` entities that are related to that `Student` entity. In other words, if a `Student` row in the database has two related `Enrollment` rows (rows that contain that student's primary key value in their StudentID foreign key column), that `Student` entity's `Enrollments` navigation property will contain those two `Enrollment` entities.

If a navigation property can hold multiple entities (as in many-to-many or one-to-many relationships), its type must be a list in which entries can be added, deleted, and updated, such as `ICollection<T>`. You can specify `ICollection<T>` or a type such as `List<T>` or `HashSet<T>`. If you specify `ICollection<T>`, EF creates a `HashSet<T>` collection by default.

### The Enrollment entity

In the *Models* folder, create `Enrollment.cs` and replace the existing code with the following code:

```
namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}
```

The `EnrollmentID` property will be the primary key; this entity uses the `classnameID` pattern instead of `ID` by itself as you saw in the `Student` entity. Ordinarily you would choose one pattern and use it throughout your data model. Here, the variation illustrates that you can use either pattern. In a [later tutorial](inheritance?view=aspnetcore-10.0), you'll see how using ID without classname makes it easier to implement inheritance in the data model.

The `Grade` property is an `enum`. The question mark after the `Grade` type declaration indicates that the `Grade` property is nullable. A grade that's null is different from a zero grade -- null means a grade isn't known or hasn't been assigned yet.

The `StudentID` property is a foreign key, and the corresponding navigation property is `Student`. An `Enrollment` entity is associated with one `Student` entity, so the property can only hold a single `Student` entity (unlike the `Student.Enrollments` navigation property you saw earlier, which can hold multiple `Enrollment` entities).

The `CourseID` property is a foreign key, and the corresponding navigation property is `Course`. An `Enrollment` entity is associated with one `Course` entity.

Entity Framework interprets a property as a foreign key property if it's named `<navigation property name><primary key property name>` (for example, `StudentID` for the `Student` navigation property since the `Student` entity's primary key is `ID`). Foreign key properties can also be named simply `<primary key property name>` (for example, `CourseID` since the `Course` entity's primary key is `CourseID`).

### The Course entity

In the *Models* folder, create `Course.cs` and replace the existing code with the following code:

```
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int CourseID { get; set; }
        public string Title { get; set; }
        public int Credits { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The `Enrollments` property is a navigation property. A `Course` entity can be related to any number of `Enrollment` entities.

We'll say more about the `DatabaseGenerated` attribute in a [later tutorial](complex-data-model?view=aspnetcore-10.0) in this series. Basically, this attribute lets you enter the primary key for the course rather than having the database generate it.

## Create the database context

The main class that coordinates Entity Framework functionality for a given data model is the database context class. You create this class by deriving from the `Microsoft.EntityFrameworkCore.DbContext` class. In your code you specify which entities are included in the data model. You can also customize certain Entity Framework behavior. In this project, the class is named `SchoolContext`.

In the project folder, create a folder named *Data*.

In the *Data* folder create a new class file named `SchoolContext.cs`, and replace the template code with the following code:

```
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
    }
}
```

This code creates a `DbSet` property for each entity set. In Entity Framework terminology, an entity set typically corresponds to a database table, and an entity corresponds to a row in the table.

You could've omitted the `DbSet<Enrollment>` and `DbSet<Course>` statements and it would work the same. The Entity Framework would include them implicitly because the `Student` entity references the `Enrollment` entity and the `Enrollment` entity references the `Course` entity.

When the database is created, EF creates tables that have names the same as the `DbSet` property names. Property names for collections are typically plural (Students rather than Student), but developers disagree about whether table names should be pluralized or not. For these tutorials you'll override the default behavior by specifying singular table names in the DbContext. To do that, add the following highlighted code after the last DbSet property.

```
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
        }
    }
}
```

Build the project as a check for compiler errors.

## Register the SchoolContext

ASP.NET Core implements [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) by default. Services (such as the EF database context) are registered with dependency injection during application startup. Components that require these services (such as MVC controllers) are provided these services via constructor parameters. You'll see the controller constructor code that gets a context instance later in this tutorial.

To register `SchoolContext` as a service, open `Startup.cs`, and add the highlighted lines to the `ConfigureServices` method.

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<SchoolContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddMvc();
}
```

The name of the connection string is passed in to the context by calling a method on a `DbContextOptionsBuilder` object. For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

Add `using` statements for `ContosoUniversity.Data` and `Microsoft.EntityFrameworkCore` namespaces, and then build the project.

```
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http;
```

Open the `appsettings.json` file and add a connection string as shown in the following example.

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  }
}
```

### SQL Server Express LocalDB

The connection string specifies a SQL Server LocalDB database. LocalDB is a lightweight version of the SQL Server Express Database Engine and is intended for application development, not production use. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default, LocalDB creates *.mdf* database files in the `C:/Users/<user>` directory.

## Initialize DB with test data

The Entity Framework will create an empty database for you. In this section, you write a method that's called after the database is created in order to populate it with test data.

Here you'll use the `EnsureCreated` method to automatically create the database. In a [later tutorial](migrations?view=aspnetcore-10.0) you'll see how to handle model changes by using Code First Migrations to change the database schema instead of dropping and re-creating the database.

In the *Data* folder, create a new class file named `DbInitializer.cs` and replace the template code with the following code, which causes a database to be created when needed and loads test data into the new database.

```
using ContosoUniversity.Models;
using System;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
            new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")},
            new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")},
            new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")}
            };
            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
            new Course{CourseID=1050,Title="Chemistry",Credits=3},
            new Course{CourseID=4022,Title="Microeconomics",Credits=3},
            new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
            new Course{CourseID=1045,Title="Calculus",Credits=4},
            new Course{CourseID=3141,Title="Trigonometry",Credits=4},
            new Course{CourseID=2021,Title="Composition",Credits=3},
            new Course{CourseID=2042,Title="Literature",Credits=4}
            };
            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
            new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
            new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
            new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
            new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
            new Enrollment{StudentID=3,CourseID=1050},
            new Enrollment{StudentID=4,CourseID=1050},
            new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
            new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
            new Enrollment{StudentID=6,CourseID=1045},
            new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
            };
            foreach (Enrollment e in enrollments)
            {
                context.Enrollments.Add(e);
            }
            context.SaveChanges();
        }
    }
}
```

The code checks if there are any students in the database, and if not, it assumes the database is new and needs to be seeded with test data. It loads test data into arrays rather than `List<T>` collections to optimize performance.

In `Program.cs`, modify the `Main` method to do the following on application startup:

* Get a database context instance from the dependency injection container.
* Call the seed method, passing to it the context.
* Dispose the context when the seed method is done.

```
using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            CreateDbIfNotExists(host);

            host.Run();
        }

        private static void CreateDbIfNotExists(IHost host)
        {
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    var context = services.GetRequiredService<SchoolContext>();
                    DbInitializer.Initialize(context);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

The first time you run the application, the database will be created and seeded with test data. Whenever you change the data model:

* Delete the database.
* Update the seed method, and start afresh with a new database the same way.

In later tutorials, you'll see how to modify the database when the data model changes, without deleting and re-creating it.

## Create controller and views

In this section, the scaffolding engine in Visual Studio is used to add an MVC controller and views that will use EF to query and save data.

The automatic creation of CRUD action methods and views is known as scaffolding. Scaffolding differs from code generation in that the scaffolded code is a starting point that you can modify to suit your own requirements, whereas you typically don't modify generated code. When you need to customize generated code, you use partial classes or you regenerate the code when things change.

* Right-click the **Controllers** folder in **Solution Explorer** and select **Add > New Scaffolded Item**.

* In the **Add Scaffold** dialog box:

  * Select **MVC controller with views, using Entity Framework**.
  * Click **Add**. The **Add MVC Controller with views, using Entity Framework** dialog box appears:
  * In **Model class** select **Student**.
  * In **Data context class** select **SchoolContext**.
  * Accept the default **StudentsController** as the name.
  * Click **Add**.

The Visual Studio scaffolding engine creates a `StudentsController.cs` file and a set of views (`.cshtml` files) that work with the controller.

Notice the controller takes a `SchoolContext` as a constructor parameter.

```
namespace ContosoUniversity.Controllers
{
    public class StudentsController : Controller
    {
        private readonly SchoolContext _context;

        public StudentsController(SchoolContext context)
        {
            _context = context;
        }
```

ASP.NET Core dependency injection takes care of passing an instance of `SchoolContext` into the controller. That was configured in the `Startup.cs` file.

The controller contains an `Index` action method, which displays all students in the database. The method gets a list of students from the Students entity set by reading the `Students` property of the database context instance:

```
public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}
```

You learn about the asynchronous programming elements in this code later in the tutorial.

The `Views/Students/Index.cshtml` view displays this list in a table:

```
@model IEnumerable<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.LastName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.EnrollmentDate)
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.LastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FirstMidName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Press CTRL+F5 to run the project or choose **Debug > Start Without Debugging** from the menu.

Click the Students tab to see the test data that the `DbInitializer.Initialize` method inserted. Depending on how narrow your browser window is, you'll see the `Students` tab link at the top of the page or you'll have to click the navigation icon in the upper right corner to see the link.

## View the database

When you started the application, the `DbInitializer.Initialize` method calls `EnsureCreated`. EF saw that there was no database and so it created one, then the remainder of the `Initialize` method code populated the database with data. You can use **SQL Server Object Explorer** (SSOX) to view the database in Visual Studio.

Close the browser.

If the SSOX window isn't already open, select it from the **View** menu in Visual Studio.

In SSOX, click **(localdb)\MSSQLLocalDB > Databases**, and then click the entry for the database name that's in the connection string in the `appsettings.json` file.

Expand the **Tables** node to see the tables in the database.

Right-click the **Student** table and click **View Data** to see the columns that were created and the rows that were inserted into the table.

The *.mdf* and *.ldf* database files are in the *C:\Users\\\<username>* folder.

Because you're calling `EnsureCreated` in the initializer method that runs on app start, you could now make a change to the `Student` class, delete the database, run the application again, and the database would automatically be re-created to match your change. For example, if you add an `EmailAddress` property to the `Student` class, you'll see a new `EmailAddress` column in the re-created table.

## Conventions

The amount of code you had to write in order for the Entity Framework to be able to create a complete database for you is minimal because of the use of conventions, or assumptions that the Entity Framework makes.

* The names of `DbSet` properties are used as table names. For entities not referenced by a `DbSet` property, entity class names are used as table names.
* Entity property names are used for column names.
* Entity properties that are named ID or classnameID are recognized as primary key properties.
* A property is interpreted as a foreign key property if it's named *\<navigation property name>\<primary key property name>* (for example, `StudentID` for the `Student` navigation property since the `Student` entity's primary key is `ID`). Foreign key properties can also be named simply *\<primary key property name>* (for example, `EnrollmentID` since the `Enrollment` entity's primary key is `EnrollmentID`).

Conventional behavior can be overridden. For example, you can explicitly specify table names, as you saw earlier in this tutorial. And you can set column names and set any property as primary key or foreign key, as you'll see in a [later tutorial](complex-data-model?view=aspnetcore-10.0) in this series.

## Asynchronous code

Asynchronous programming is the default mode for ASP.NET Core and EF Core.

A web server has a limited number of threads available, and in high load situations all of the available threads might be in use. When that happens, the server can't process new requests until the threads are freed up. With synchronous code, many threads may be tied up while they aren't actually doing any work because they're waiting for I/O to complete. With asynchronous code, when a process is waiting for I/O to complete, its thread is freed up for the server to use for processing other requests. As a result, asynchronous code enables server resources to be used more efficiently, and the server is enabled to handle more traffic without delays.

Asynchronous code does introduce a small amount of overhead at run time, but for low traffic situations the performance hit is negligible, while for high traffic situations, the potential performance improvement is substantial.

In the following code, the `async` keyword, `Task<T>` return value, `await` keyword, and `ToListAsync` method make the code execute asynchronously.

```
public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}
```

* The `async` keyword tells the compiler to generate callbacks for parts of the method body and to automatically create the `Task<IActionResult>` object that's returned.
* The return type `Task<IActionResult>` represents ongoing work with a result of type `IActionResult`.
* The `await` keyword causes the compiler to split the method into two parts. The first part ends with the operation that's started asynchronously. The second part is put into a callback method that's called when the operation completes.
* `ToListAsync` is the asynchronous version of the `ToList` extension method.

Some things to be aware of when you are writing asynchronous code that uses the Entity Framework:

* Only statements that cause queries or commands to be sent to the database are executed asynchronously. That includes, for example, `ToListAsync`, `SingleOrDefaultAsync`, and `SaveChangesAsync`. It doesn't include, for example, statements that just change an `IQueryable`, such as `var students = context.Students.Where(s => s.LastName == "Davolio")`.
* An EF context isn't thread safe: don't try to do multiple operations in parallel. When you call any async EF method, always use the `await` keyword.
* If you want to take advantage of the performance benefits of async code, make sure that any library packages that you're using (such as for paging), also use async if they call any Entity Framework methods that cause queries to be sent to the database.

For more information about asynchronous programming in .NET, see [Async Overview](/en-us/dotnet/articles/standard/async).

## Next steps

Advance to the next tutorial to learn how to perform basic CRUD (create, read, update, delete) operations.

[Implement basic CRUD functionality](crud?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-25

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/navigation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/navigation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor navigation

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to trigger and handle page navigation in Blazor. While users can navigate between different pages using normal HTML links, Blazor enhances navigation within the application to avoid full page reloads and deliver a smoother experience. Use the [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component to create navigation links that automatically apply styling when the link matches the current page. For programmatic navigation and URI management in C# code, use the [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) service.

This article explains how to trigger and handle page navigation in Blazor. Use the [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component to create navigation links that automatically apply styling when the link matches the current page. For programmatic navigation and URI management in C# code, use the [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) service.

Important

Code examples throughout this article show methods called on `Navigation`, which is an injected [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) in classes and components.

## `NavLink` component

Use a [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component in place of HTML hyperlink elements (`<a>`) when creating navigation links. A [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component behaves like an `<a>` element, except it toggles an `active` CSS class based on whether its `href` matches the current URL. The `active` class helps a user understand which page is the active page among the navigation links displayed. Optionally, assign a CSS class name to [NavLink.ActiveClass](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink.activeclass#microsoft-aspnetcore-components-routing-navlink-activeclass) to apply a custom CSS class to the rendered link when the current route matches the `href`.

In the `NavMenu` component (`NavMenu.razor`) of a Blazor app created from a Blazor project template:

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
        <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
    </NavLink>
</div>
<div class="nav-item px-3">
    <NavLink class="nav-link" href="counter">
        <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
    </NavLink>
</div>
```

In the preceding example, the Home [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) `href=""` matches the home URL and only receives the `active` CSS class at the app's default base path (`/`). The second [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) receives the `active` class when the user visits the `Counter` component at `/counter`.

There are two [NavLinkMatch](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlinkmatch) options that you can assign to the [Match](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink.match) attribute of the `<NavLink>` element:

* [NavLinkMatch.All](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlinkmatch#microsoft-aspnetcore-components-routing-navlinkmatch-all): The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) is active when it matches the current URL, ignoring the query string and fragment. To include matching on the query string/fragment, use the `Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragment` [`AppContext` switch](/en-us/dotnet/fundamentals/runtime-libraries/system-appcontext) set to `true`.
* [NavLinkMatch.Prefix](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlinkmatch#microsoft-aspnetcore-components-routing-navlinkmatch-prefix) (*default*): The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) is active when it matches any prefix of the current URL.

To adopt custom matching logic, subclass [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) and override its [ShouldMatch](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink.shouldmatch) method. Return `true` from the method when you want to apply the `active` CSS class:

```
public class CustomNavLink : NavLink
{
    protected override bool ShouldMatch(string currentUriAbsolute)
    {
        // Custom matching logic
    }
}
```

There are two [NavLinkMatch](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlinkmatch) options that you can assign to the [Match](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink.match) attribute of the `<NavLink>` element:

* [NavLinkMatch.All](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlinkmatch#microsoft-aspnetcore-components-routing-navlinkmatch-all): The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) is active when it matches the entire current URL, including the query string and fragment.
* [NavLinkMatch.Prefix](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlinkmatch#microsoft-aspnetcore-components-routing-navlinkmatch-prefix) (*default*): The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) is active when it matches any prefix of the current URL.

Set `NavLink.RelativeToCurrentUri` to `true` to resolve the `href` relative to the current page path instead of the app's base URI:

```
<NavLink href="details" RelativeToCurrentUri="true">View Details</NavLink>
```

If the current URI is `/docs/getting-started/installation`, the preceding link navigates to `/docs/getting-started/details`.

Additional [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component attributes are passed through to the rendered anchor tag. In the following example, the [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component includes the `target` attribute:

```
<NavLink href="example-page" target="_blank">Example page</NavLink>
```

The following HTML markup is rendered:

```
<a href="example-page" target="_blank">Example page</a>
```

Warning

Due to the way that Blazor renders child content, rendering `NavLink` components inside a `for` loop requires a local index variable if the incrementing loop variable is used in the `NavLink` (child) component's content:

```
@for (int c = 1; c < 4; c++)
{
    var ct = c;
    <li ...>
        <NavLink ...>
            <span ...></span> Product #@ct
        </NavLink>
    </li>
}
```

Using an index variable in this scenario is a requirement for **any** child component that uses a loop variable in its [child content](../components/?view=aspnetcore-10.0#child-content-render-fragments), not just the `NavLink` component.

Alternatively, use a `foreach` loop with [Enumerable.Range](/en-us/dotnet/api/system.linq.enumerable.range):

```
@foreach (var c in Enumerable.Range(1, 3))
{
    <li ...>
        <NavLink ...>
            <span ...></span> Product #@c
        </NavLink>
    </li>
}
```

## URI and navigation state helpers

Use [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) to manage URIs and navigation in C# code. [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) provides the event and methods shown in the following table.

| Member                                                                                                                                                                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri)                                     | Gets the current absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri)                         | Gets the base URI (with a trailing slash) that can be prepended to relative URI paths to produce an absolute URI. Typically, [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri) corresponds to the `href` attribute on the document's `<base>` element ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)).                                                                                                                                                                                                                                    |
| [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto)                                                                             | Navigates to the specified URI. If `forceLoad` is `false`:- And enhanced navigation is available at the current URL, Blazor's enhanced navigation is activated.
- Otherwise, Blazor performs a full-page reload for the requested URL.If `forceLoad` is `true`:- Client-side routing is bypassed.
- The browser is forced to load the new page from the server, whether or not the URI is normally handled by the client-side interactive router.For more information, see the [Enhanced navigation and form handling](#enhanced-navigation-and-form-handling) section.If `replace` is `true`, the current URI in the browser history is replaced instead of pushing a new URI onto the history stack. |
| [LocationChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.locationchanged#microsoft-aspnetcore-components-navigationmanager-locationchanged) | An event that fires when the navigation location has changed. For more information, see the [Location changes](#location-changes) section.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound)                                                                                 | Called to handle scenarios where a requested resource isn't found. For more information, see the [Not Found responses](#not-found-responses) section.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| [ToAbsoluteUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.toabsoluteuri)                                                                       | Converts a relative URI into an absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| [ToBaseRelativePath](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.tobaserelativepath)                                                             | Based on the app's base URI, converts an absolute URI into a URI relative to the base URI prefix. For an example, see the [Produce a URI relative to the base URI prefix](#produce-a-uri-relative-to-the-base-uri-prefix) section.                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| [`RegisterLocationChangingHandler`](#handleprevent-location-changes)                                                                                                     | Registers a handler to process incoming navigation events. Calling [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) always invokes the handler.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| [GetUriWithQueryParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameter)                                       | Returns a URI constructed by updating [NavigationManager.Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri) with a single parameter added, updated, or removed. For more information, see the [Query strings](#query-strings) section.                                                                                                                                                                                                                                                                                                                                                                                |

| Member                                                                                                                                                                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri)                                     | Gets the current absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri)                         | Gets the base URI (with a trailing slash) that can be prepended to relative URI paths to produce an absolute URI. Typically, [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri) corresponds to the `href` attribute on the document's `<base>` element ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)).                                                                                                                                                                                                                                    |
| [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto)                                                                             | Navigates to the specified URI. If `forceLoad` is `false`:- And enhanced navigation is available at the current URL, Blazor's enhanced navigation is activated.
- Otherwise, Blazor performs a full-page reload for the requested URL.If `forceLoad` is `true`:- Client-side routing is bypassed.
- The browser is forced to load the new page from the server, whether or not the URI is normally handled by the client-side interactive router.For more information, see the [Enhanced navigation and form handling](#enhanced-navigation-and-form-handling) section.If `replace` is `true`, the current URI in the browser history is replaced instead of pushing a new URI onto the history stack. |
| [LocationChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.locationchanged#microsoft-aspnetcore-components-navigationmanager-locationchanged) | An event that fires when the navigation location has changed. For more information, see the [Location changes](#location-changes) section.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| [ToAbsoluteUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.toabsoluteuri)                                                                       | Converts a relative URI into an absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| [ToBaseRelativePath](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.tobaserelativepath)                                                             | Based on the app's base URI, converts an absolute URI into a URI relative to the base URI prefix. For an example, see the [Produce a URI relative to the base URI prefix](#produce-a-uri-relative-to-the-base-uri-prefix) section.                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| [`RegisterLocationChangingHandler`](#handleprevent-location-changes)                                                                                                     | Registers a handler to process incoming navigation events. Calling [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) always invokes the handler.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| [GetUriWithQueryParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameter)                                       | Returns a URI constructed by updating [NavigationManager.Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri) with a single parameter added, updated, or removed. For more information, see the [Query strings](#query-strings) section.                                                                                                                                                                                                                                                                                                                                                                                |

| Member                                                                                                                                                                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri)                                     | Gets the current absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri)                         | Gets the base URI (with a trailing slash) that can be prepended to relative URI paths to produce an absolute URI. Typically, [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri) corresponds to the `href` attribute on the document's `<base>` element ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)). |
| [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto)                                                                             | Navigates to the specified URI. If `forceLoad` is `true`:- Client-side routing is bypassed.
- The browser is forced to load the new page from the server, whether or not the URI is normally handled by the client-side router.If `replace` is `true`, the current URI in the browser history is replaced instead of pushing a new URI onto the history stack.                                                                                                      |
| [LocationChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.locationchanged#microsoft-aspnetcore-components-navigationmanager-locationchanged) | An event that fires when the navigation location has changed. For more information, see the [Location changes](#location-changes) section.                                                                                                                                                                                                                                                                                                                          |
| [ToAbsoluteUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.toabsoluteuri)                                                                       | Converts a relative URI into an absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| [ToBaseRelativePath](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.tobaserelativepath)                                                             | Based on the app's base URI, converts an absolute URI into a URI relative to the base URI prefix. For an example, see the [Produce a URI relative to the base URI prefix](#produce-a-uri-relative-to-the-base-uri-prefix) section.                                                                                                                                                                                                                                  |
| [`RegisterLocationChangingHandler`](#handleprevent-location-changes)                                                                                                     | Registers a handler to process incoming navigation events. Calling [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) always invokes the handler.                                                                                                                                                                                                                                                                         |
| [GetUriWithQueryParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameter)                                       | Returns a URI constructed by updating [NavigationManager.Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri) with a single parameter added, updated, or removed. For more information, see the [Query strings](#query-strings) section.                                                                                                                                             |

| Member                                                                                                                                                                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri)                                     | Gets the current absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri)                         | Gets the base URI (with a trailing slash) that can be prepended to relative URI paths to produce an absolute URI. Typically, [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri) corresponds to the `href` attribute on the document's `<base>` element ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)). |
| [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto)                                                                             | Navigates to the specified URI. If `forceLoad` is `true`:- Client-side routing is bypassed.
- The browser is forced to load the new page from the server, whether or not the URI is normally handled by the client-side router.If `replace` is `true`, the current URI in the browser history is replaced instead of pushing a new URI onto the history stack.                                                                                                      |
| [LocationChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.locationchanged#microsoft-aspnetcore-components-navigationmanager-locationchanged) | An event that fires when the navigation location has changed. For more information, see the [Location changes](#location-changes) section.                                                                                                                                                                                                                                                                                                                          |
| [ToAbsoluteUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.toabsoluteuri)                                                                       | Converts a relative URI into an absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| [ToBaseRelativePath](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.tobaserelativepath)                                                             | Based on the app's base URI, converts an absolute URI into a URI relative to the base URI prefix. For an example, see the [Produce a URI relative to the base URI prefix](#produce-a-uri-relative-to-the-base-uri-prefix) section.                                                                                                                                                                                                                                  |
| [GetUriWithQueryParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameter)                                       | Returns a URI constructed by updating [NavigationManager.Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri) with a single parameter added, updated, or removed. For more information, see the [Query strings](#query-strings) section.                                                                                                                                             |

| Member                                                                                                                                                                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri)                                     | Gets the current absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri)                         | Gets the base URI (with a trailing slash) that can be prepended to relative URI paths to produce an absolute URI. Typically, [BaseUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.baseuri#microsoft-aspnetcore-components-navigationmanager-baseuri) corresponds to the `href` attribute on the document's `<base>` element ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)). |
| [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto)                                                                             | Navigates to the specified URI. If `forceLoad` is `true`:- Client-side routing is bypassed.
- The browser is forced to load the new page from the server, whether or not the URI is normally handled by the client-side router.                                                                                                                                                                                                                                     |
| [LocationChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.locationchanged#microsoft-aspnetcore-components-navigationmanager-locationchanged) | An event that fires when the navigation location has changed.                                                                                                                                                                                                                                                                                                                                                                                                       |
| [ToAbsoluteUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.toabsoluteuri)                                                                       | Converts a relative URI into an absolute URI.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| [ToBaseRelativePath](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.tobaserelativepath)                                                             | Based on the app's base URI, converts an absolute URI into a URI relative to the base URI prefix. For an example, see the [Produce a URI relative to the base URI prefix](#produce-a-uri-relative-to-the-base-uri-prefix) section.                                                                                                                                                                                                                                  |

## Location changes

For the [LocationChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.locationchanged#microsoft-aspnetcore-components-navigationmanager-locationchanged) event, [LocationChangedEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangedeventargs) provides the following information about navigation events:

* [Location](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangedeventargs.location#microsoft-aspnetcore-components-routing-locationchangedeventargs-location): The URL of the new location.
* [IsNavigationIntercepted](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangedeventargs.isnavigationintercepted#microsoft-aspnetcore-components-routing-locationchangedeventargs-isnavigationintercepted): If `true`, Blazor intercepted the navigation from the browser. If `false`, [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) caused the navigation to occur.

The following component:

* Navigates to the app's `Counter` component (`Counter.razor`) when the button is selected using [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto).

* Handles the location changed event by subscribing to [NavigationManager.LocationChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.locationchanged#microsoft-aspnetcore-components-navigationmanager-locationchanged).

  * The `HandleLocationChanged` method is unhooked when `Dispose` is called by the framework. Unhooking the method permits garbage collection of the component.

  * The logger implementation logs the following information when the button is selected:

    > BlazorSample.Pages.Navigate: Information: URL of new location: https\://localhost:{PORT}/counter

`Navigate.razor`:

```
@page "/navigate"
@implements IDisposable
@inject ILogger<Navigate> Logger
@inject NavigationManager Navigation

<h1>Navigate Example</h1>

<button class="btn btn-primary" @onclick="NavigateToCounterComponent">
    Navigate to the Counter component
</button>

@code {
    private void NavigateToCounterComponent() => Navigation.NavigateTo("counter");

    protected override void OnInitialized() => 
        Navigation.LocationChanged += HandleLocationChanged;

    private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) => 
        Logger.LogInformation("URL of new location: {Location}", e.Location);

    public void Dispose() => Navigation.LocationChanged -= HandleLocationChanged;
}
```

For more information on component disposal, see [ASP.NET Core Razor component disposal](../components/component-disposal?view=aspnetcore-10.0).

## Navigation Manager redirect behavior during static server-side rendering (static SSR)

For a redirect during static server-side rendering (static SSR), [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) relies on throwing a [NavigationException](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationexception) that gets captured by the framework, which converts the error into a redirect. Code that exists after the call to [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) isn't called. When using Visual Studio, the debugger breaks on the exception, requiring you to deselect the checkbox for **Break when this exception type is user-handled** in the Visual Studio UI to avoid the debugger stopping for future redirects.

You can use the `<BlazorDisableThrowNavigationException>` MSBuild property set to `true` in the app's project file to opt-in to no longer throwing a [NavigationException](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationexception). Also, code after the call to [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) executes when it wouldn't have run before. This behavior is enabled by default in the .NET 10 or later Blazor Web App project template:

```
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
```

Note

In .NET 10 or later, you can opt-in to not throwing a [NavigationException](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationexception) by setting the `<BlazorDisableThrowNavigationException>` MSBuild property to `true` in the app's project file. To take advantage of the new MSBuild property and behavior, upgrade the app to .NET 10 or later.

## Not Found responses

[NavigationManager.NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) handles scenarios where a requested resource isn't found:

* **Static server-side rendering (static SSR)**: Calling [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) sets the HTTP status code to 404. If Not Found content is configured through `Router.NotFoundPage` or `NotFoundEventArgs.Path`, the router renders Not Found content.

* **Global interactive rendering**: Signals the Blazor router ([`Router` component](routing?view=aspnetcore-10.0#route-templates)) to render Not Found content.

* **Per-page/component interactive rendering**: Behaves the same as static server-side rendering (static SSR) during prerendering on the server, while interactive Not Found behavior no-ops.

* **Streaming rendering**: If [enhanced navigation](routing?view=aspnetcore-10.0#enhanced-navigation-and-form-handling) is active, [streaming rendering](../components/rendering?view=aspnetcore-10.0#streaming-rendering) renders Not Found content without reloading the page. When enhanced navigation is blocked, the framework redirects to Not Found content with a page refresh.

Note

The following discussion mentions that a Not Found Razor component can be assigned to the `Router` component's [NotFoundPage](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfoundpage) parameter. The parameter works in concert with [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) and is described in more detail later in this section.

Streaming rendering can only render components that have a route, such as a [Router.NotFoundPage](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfoundpage) assignment (`NotFoundPage="..."`) or a [status code pages re-execution middleware page assignment](../../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepageswithreexecute) ([UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute)). `DefaultNotFound` 404 content ("`Not found`" plain text) doesn't have a route, so it can't be used during streaming rendering.

Note

The Not Found render fragment (`<NotFound>...</NotFound>`) isn't supported in .NET 10 or later.

[NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) content rendering uses the following, regardless if the response has started or not (in order):

* If [NotFoundEventArgs.Path](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.notfoundeventargs.path) is set, render the contents of the assigned page.
* If [Router.NotFoundPage](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfoundpage) is set, render the assigned page.
* A status code pages re-execution middleware page, if configured.
* No action if none of the preceding approaches are adopted.

[Status code pages re-execution middleware](../../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepageswithreexecute) with [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) takes precedence for browser-based address routing problems, such as an incorrect URL typed into the browser's address bar or selecting a link that has no endpoint in the app.

When a component is rendered statically (static SSR) and [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called, the 404 status code is set on the response:

```
@page "/render-not-found-ssr"
@inject NavigationManager Navigation

@code {
    protected override void OnInitialized()
    {
        Navigation.NotFound();
    }
}
```

To provide Not Found content for global interactive rendering or for per-page/component interactive rendering *during prerendering*, use a Not Found page (Razor component).

Note

The Blazor project template includes a `NotFound.razor` page and configures it as the router's `NotFoundPage`. This page renders when [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called in a supported context, making it possible to handle missing routes with a consistent user experience.

`Pages/NotFound.razor`:

```
@page "/not-found"
@layout MainLayout

<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
```

The `NotFound` component is assigned to [Router.NotFoundPage](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfoundpage), which supports routing that can be used across status code pages re-execution middleware, including non-Blazor middleware.

In the following example, the preceding `NotFound` component is present in the app's `Pages` folder and passed to the [NotFoundPage](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfoundpage) parameter:

```
<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
```

When a component is rendered with a global interactive render mode, calling [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) signals the Blazor router to render the `NotFound` component:

```
@page "/render-not-found-interactive"
@inject NavigationManager Navigation

@if (RendererInfo.IsInteractive)
{
    <button @onclick="TriggerNotFound">Trigger Not Found</button>
}

@code {
    private void TriggerNotFound()
    {
        Navigation.NotFound();
    }
}
```

When a component is rendered with a per-page/component interactive render mode, calling [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) signals the Blazor router to render the `NotFound` component ***only during prerendering*** ([RendererInfo.IsInteractive](/en-us/dotnet/api/microsoft.aspnetcore.components.rendererinfo.isinteractive#microsoft-aspnetcore-components-rendererinfo-isinteractive)):

```
protected override void OnInitialized()
{
    // Where your initialization code must lead to a Not Found response:
    if (!RendererInfo.IsInteractive)
    {
        Navigation.NotFound();
    }
}
```

The preceding example would assign a per-page/component render mode at the top of the Razor component file (for example, `@rendermode InteractiveServer`).

The Not Found response and 404 status code aren't applied outside of prerendering, so if prerendering is disabled or the [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) call is made after prerendering, the component should indicate to the user in the component's UI that a resource isn't found.

[NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) doesn't terminate the method. If [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) should end the control flow, add a `return` statement after calling [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound). In the following movie database update method, a concurrency exception results in checking for the presence of a movie in the database to determine if a Not Found response is appropriate. The `return` statement after calling [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) ensures that the handler selects only the Not Found outcome, independently of a given database provider synchronously or asynchronously executing [SaveChangesAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechangesasync):

```
private async Task UpdateMovie()
{
    using var context = DbFactory.CreateDbContext();
    context.Attach(Movie!).State = EntityState.Modified;

    try
    {
        await context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie!.Id))
        {
            Navigation.NotFound();

            return;
        }
        else
        {
            throw;
        }
    }

    Navigation.NavigateTo("/movies");
}
```

Services not shown in the preceding example:

* `NavigationManager` is an injected [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) (`@inject NavigationManager Navigation`).
* `DbFactory` is an injected [IDbContextFactory\<TContext>](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1) (`@inject IDbContextFactory<{CONTEXT}> DbFactory`, where the `{CONTEXT}` placeholder is a [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext)).

Use the [NavigationManager.OnNotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.onnotfound) event for notifications when [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is invoked. The event is only fired when [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called, not for any 404 response. For example, setting `HttpContextAccessor.HttpContext.Response.StatusCode` to `404` doesn't trigger [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound)/[OnNotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.onnotfound).

Apps that implement a custom router can use [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound). There are two ways to inform the renderer what page should be rendered when [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called.

The recommended approach that works regardless of the response state is to call [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute). When [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called, the middleware renders the path passed to the method:

```
app.UseStatusCodePagesWithReExecute(
    "/not-found", createScopeForStatusCodePages: true);
```

If you don't want to use [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute), the app can still support [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) for responses that have already started. Subscribe to `OnNotFoundEvent` in the router and assign the Not Found page path to `NotFoundEventArgs.Path` to inform the renderer what content to render when [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called.

`CustomRouter.razor`:

```
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Http
@implements IDisposable
@inject NavigationManager NavigationManager

@code {
    protected override void OnInitialized() =>
        NavigationManager.OnNotFound += OnNotFoundEvent;

    [CascadingParameter]
    public HttpContext? HttpContext { get; set; }

    private void OnNotFoundEvent(object sender, NotFoundEventArgs e)
    {
        // Only execute the logic if HTTP response has started
        // because setting NotFoundEventArgs.Path blocks re-execution
        if (HttpContext?.Response.HasStarted == false)
        {
            return;
        }

        e.Path = GetNotFoundRoutePath();
    }

    // Return the path of the Not Found page that you want to display
    private string GetNotFoundRoutePath()
    {
        ...
    }

    public void Dispose() => NavigationManager.OnNotFound -= OnNotFoundEvent;
}
```

If you use both approaches in your app, the Not Found path specified in the `OnNotFoundEvent` handler takes precedence over the path configured in the re-execution middleware.

In the following example for components that adopt [interactive server-side rendering (interactive SSR)](./?view=aspnetcore-10.0#client-and-server-rendering-concepts), custom content is rendered depending on where [OnNotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.onnotfound) is called. If the event is triggered by the following `Movie` component when a movie isn't found on component initialization, a custom message states that the requested movie isn't found. If the event is triggered by the `User` component in the following example, a different message states that the user isn't found.

The following `NotFoundContext` service manages the context and the message for when content isn't found by components.

`NotFoundContext.cs`:

```
public class NotFoundContext
{
    public string? Heading { get; private set; }
    public string? Message { get; private set; }

    public void UpdateContext(string heading, string message)
    {
        Heading = heading;
        Message = message;
    }
}
```

The service is registered in the server-side `Program` file:

```
builder.Services.AddScoped<NotFoundContext>();
```

The `NotFound` page injects the `NotFoundContext` and displays the heading and message.

`Pages/NotFound.razor`:

```
@page "/not-found"
@layout MainLayout
@inject NotFoundContext NotFoundContext

<h3>@NotFoundContext.Heading</h3>
<div>
    <p>@NotFoundContext.Message</p>
</div>
```

The `Routes` component (`Routes.razor`) sets the `NotFound` component as the Not Found page via the [NotFoundPage](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.notfoundpage) parameter:

```
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
    ...
</Router>
```

In the following example components:

* The `NotFoundContext` service is injected, along with the [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager).
* In [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync), `HandleNotFound` is an event handler assigned to the [OnNotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.onnotfound) event. `HandleNotFound` calls `NotFoundContext.UpdateContext` to set a heading and message for Not Found content in the `NotFound` component.
* The components would normally use an ID from a route parameter to obtain a movie or user from a data store, such as a database. In the following examples, no entity is returned (`null`) to simulate what happens when an entity isn't found.
* When no entity is returned to [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync), [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called, which in turn triggers the [OnNotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.onnotfound) event and the `HandleNotFound` event handler. Not Found content is displayed by the router.
* The `HandleNotFound` method is unhooked on component disposal in [IDisposable.Dispose](/en-us/dotnet/api/system.idisposable.dispose).

`Movie` component (`Movie.razor`):

```
@page "/movie/{Id:int}"
@implements IDisposable
@inject NavigationManager NavigationManager
@inject NotFoundContext NotFoundContext

<div>
    No matter what ID is used, no matching movie is returned
    from the call to GetMovie().
</div>

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

    protected override async Task OnInitializedAsync()
    {
        NavigationManager.OnNotFound += HandleNotFound;

        var movie = await GetMovie(Id);

        if (movie == null)
        {
            NavigationManager.NotFound();
        }
    }

    private void HandleNotFound(object? sender, NotFoundEventArgs e)
    {
        NotFoundContext.UpdateContext("Movie Not Found",
            "Sorry! The requested movie wasn't found.");
    }

    private async Task<MovieItem[]?> GetMovie(int id)
    {
        // Simulate no movie with matching id found
        return await Task.FromResult<MovieItem[]?>(null);
    }

    void IDisposable.Dispose()
    {
        NavigationManager.OnNotFound -= HandleNotFound;
    }

    public class MovieItem
    {
        public int Id { get; set; }
        public string? Title { get; set; }
    }
}
```

`User` component (`User.razor`):

```
@page "/user/{Id:int}"
@implements IDisposable
@inject NavigationManager NavigationManager
@inject NotFoundContext NotFoundContext

<div>
    No matter what ID is used, no matching user is returned
    from the call to GetUser().
</div>

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

    protected override async Task OnInitializedAsync()
    {
        NavigationManager.OnNotFound += HandleNotFound;

        var user = await GetUser(Id);

        if (user == null)
        {
            NavigationManager.NotFound();
        }
    }

    private void HandleNotFound(object? sender, NotFoundEventArgs e)
    {
        NotFoundContext.UpdateContext("User Not Found",
            "Sorry! The requested user wasn't found.");
    }

    private async Task<UserItem[]?> GetUser(int id)
    {
        // Simulate no user with matching id found
        return await Task.FromResult<UserItem[]?>(null);
    }

    void IDisposable.Dispose()
    {
        NavigationManager.OnNotFound -= HandleNotFound;
    }

    public class UserItem
    {
        public int Id { get; set; }
        public string? Name { get; set; }
    }
}
```

To reach the preceding components in a local demonstration with a test app, create entries in the `NavMenu` component (`NavMenu.razor`) to reach the `Movie` and `User` components. The entity IDs, passed as route parameters, in the following example are mock values that have no effect because they aren't actually used by the components, which simulate not finding a movie or user.

In `NavMenu.razor`:

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="movie/1">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Movie
    </NavLink>
</div>

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user/2">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User
    </NavLink>
</div>
```

## Enhanced navigation and form handling

*This section applies to Blazor Web Apps.*

Blazor Web Apps are capable of two types of routing for page navigation and form handling requests:

* Normal navigation (cross-document navigation): a full-page reload is triggered for the request URL.
* Enhanced navigation (same-document navigation): Blazor intercepts the request and performs a `fetch` request instead. Blazor then patches the response content into the page's DOM. Blazor's enhanced navigation and form handling avoid the need for a full-page reload and preserves more of the page state, so pages load faster, usually without losing the user's scroll position on the page.

Enhanced navigation is available when:

* The Blazor Web App script (`blazor.web.js`) is used, not the Blazor Server script (`blazor.server.js`) or Blazor WebAssembly script (`blazor.webassembly.js`).
* The feature isn't [explicitly disabled](startup?view=aspnetcore-10.0#disable-enhanced-navigation-and-form-handling).
* The destination URL is within the internal base URI space (the app's base path) and the link to the page doesn't have the `data-enhance-nav` attribute set to `false`.

If server-side routing and enhanced navigation are enabled, [location changing handlers](#location-changes) are only invoked for programmatic navigation initiated from an interactive runtime. In future releases, additional types of navigation, such as following a link, may also invoke location changing handlers.

When an enhanced navigation occurs, [`LocationChanged` event handlers](#location-changes) registered with Interactive Server and WebAssembly runtimes are typically invoked. There are cases when location changing handlers might not intercept an enhanced navigation. For example, the user might switch to another page before an interactive runtime becomes available. Therefore, it's important that app logic not rely on invoking a location changing handler, as there's no guarantee of the handler executing.

When calling [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto):

* If `forceLoad` is `false`, which is the default:

  * And enhanced navigation is available at the current URL, Blazor's enhanced navigation is activated.
  * Otherwise, Blazor performs a full-page reload for the requested URL.

* If `forceLoad` is `true`: Blazor performs a full-page reload for the requested URL, whether enhanced navigation is available or not.

You can refresh the current page by calling `NavigationManager.Refresh(bool forceLoad = false)`, which always performs an enhanced navigation, if available. If enhanced navigation isn't available, Blazor performs a full-page reload.

```
Navigation.Refresh();
```

Pass `true` to the `forceLoad` parameter to ensure a full-page reload is always performed, even if enhanced navigation is available:

```
Navigation.Refresh(true);
```

Enhanced navigation is enabled by default, but it can be controlled hierarchically and on a per-link basis using the `data-enhance-nav` HTML attribute.

The following examples disable enhanced navigation:

```
<a href="redirect" data-enhance-nav="false">
    GET without enhanced navigation
</a>
```

```
<ul data-enhance-nav="false">
    <li>
        <a href="redirect">GET without enhanced navigation</a>
    </li>
    <li>
        <a href="redirect-2">GET without enhanced navigation</a>
    </li>
</ul>
```

If the destination is a non-Blazor endpoint, enhanced navigation doesn't apply, and the client-side JavaScript retries as a full page load. This ensures no confusion to the framework about external pages that shouldn't be patched into an existing page.

To enable enhanced form handling, add the [Enhance](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.enhance) parameter to [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) forms or the `data-enhance` attribute to HTML forms (`<form>`):

```
<EditForm ... Enhance ...>
    ...
</EditForm>
```

```
<form ... data-enhance ...>
    ...
</form>
```

Enhanced form handling isn't hierarchical and doesn't flow to child forms:

❌Unsupported: You can't set enhanced navigation on a form's ancestor element to enable enhanced navigation for the form.

```
<div ... data-enhance ...>
    <form ...>
        <!-- NOT enhanced -->
    </form>
</div>
```

Enhanced form posts only work with Blazor endpoints. Posting an enhanced form to non-Blazor endpoint results in an error.

To disable enhanced navigation:

* For an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform), remove the [Enhance](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.enhance) parameter from the form element (or set it to `false`: `Enhance="false"`).
* For an HTML `<form>`, remove the `data-enhance` attribute from form element (or set it to `false`: `data-enhance="false"`).

Blazor's enhanced navigation and form handing may undo dynamic changes to the DOM if the updated content isn't part of the server rendering. To preserve the content of an element, use the `data-permanent` attribute.

In the following example, the content of the `<div>` element is updated dynamically by a script when the page loads:

```
<div data-permanent>
    ...
</div>
```

Once Blazor has started on the client, you can use the `enhancedload` event to listen for enhanced page updates. This allows for re-applying changes to the DOM that may have been undone by an enhanced page update.

```
Blazor.addEventListener('enhancedload', () => console.log('Enhanced update!'));
```

To disable enhanced navigation and form handling globally, see [ASP.NET Core Blazor startup](startup?view=aspnetcore-10.0#disable-enhanced-navigation-and-form-handling).

Enhanced navigation with [static server-side rendering (static SSR)](../components/render-modes?view=aspnetcore-10.0#static-server-side-rendering-static-ssr) requires special attention when loading JavaScript. For more information, see [ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](../javascript-interoperability/static-server-rendering?view=aspnetcore-10.0).

## Produce a URI relative to the base URI prefix

Based on the app's base URI, [ToBaseRelativePath](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.tobaserelativepath) converts an absolute URI into a URI relative to the base URI prefix.

Consider the following example:

```
try
{
    baseRelativePath = Navigation.ToBaseRelativePath(inputURI);
}
catch (ArgumentException ex)
{
    ...
}
```

If the base URI of the app is `https://localhost:8000`, the following results are obtained:

* Passing `https://localhost:8000/segment` in `inputURI` results in a `baseRelativePath` of `segment`.
* Passing `https://localhost:8000/segment1/segment2` in `inputURI` results in a `baseRelativePath` of `segment1/segment2`.

If the base URI of the app doesn't match the base URI of `inputURI`, an [ArgumentException](/en-us/dotnet/api/system.argumentexception) is thrown.

Passing `https://localhost:8001/segment` in `inputURI` results in the following exception:

> System.ArgumentException: 'The URI 'https\://localhost:8001/segment' is not contained by the base URI 'https\://localhost:8000/'.'

## Navigation history state

The [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) uses the browser's [History API](https://developer.mozilla.org/docs/Web/API/History_API) to maintain navigation history state associated with each location change made by the app. Maintaining history state is particularly useful in external redirect scenarios, such as when [authenticating users with external identity providers](../security/webassembly/?view=aspnetcore-10.0#customize-authorization). For more information, see the [Navigation options](#navigation-options) section.

## Navigation options

Pass [NavigationOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationoptions) to [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) to control the following behaviors:

* [ForceLoad](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationoptions.forceload#microsoft-aspnetcore-components-navigationoptions-forceload): Bypass client-side routing and force the browser to load the new page from the server, whether or not the URI is handled by the client-side router. The default value is `false`.
* [ReplaceHistoryEntry](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationoptions.replacehistoryentry#microsoft-aspnetcore-components-navigationoptions-replacehistoryentry): Replace the current entry in the history stack. If `false`, append the new entry to the history stack. The default value is `false`.
* [HistoryEntryState](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationoptions.historyentrystate#microsoft-aspnetcore-components-navigationoptions-historyentrystate): Gets or sets the state to append to the history entry.
* `RelativeToCurrentUri`: When `true`, the URI is resolved relative to the current page path instead of the app's base URI. The default value is `false`.

In the following example:

* The state appended to the history entry is "`Navigation state`."
* If the current URI is `/docs/getting-started/installation`, navigation results in a request for `/docs/getting-started/configuration`.

```
Navigation.NavigateTo("/configuration", new NavigationOptions
{
    HistoryEntryState = "Navigation state",
    RelativeToCurrentUri = true
});
```

For more information on obtaining the state associated with the target history entry while handling location changes, see the [Handle/prevent location changes](#handleprevent-location-changes) section.

* [ForceLoad](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationoptions.forceload#microsoft-aspnetcore-components-navigationoptions-forceload): Bypass client-side routing and force the browser to load the new page from the server, whether or not the URI is handled by the client-side router. The default value is `false`.
* [ReplaceHistoryEntry](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationoptions.replacehistoryentry#microsoft-aspnetcore-components-navigationoptions-replacehistoryentry): Replace the current entry in the history stack. If `false`, append the new entry to the history stack. The default value is `false`.
* [HistoryEntryState](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationoptions.historyentrystate#microsoft-aspnetcore-components-navigationoptions-historyentrystate): Gets or sets the state to append to the history entry.

In the following example, the state appended to the history entry is "`Navigation state`."

```
Navigation.NavigateTo("/path", new NavigationOptions
{
    HistoryEntryState = "Navigation state"
});
```

For more information on obtaining the state associated with the target history entry while handling location changes, see the [Handle/prevent location changes](#handleprevent-location-changes) section.

## Query strings

Use the [`[SupplyParameterFromQuery]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromqueryattribute) to specify that a component parameter comes from the query string.

Use the [`[SupplyParameterFromQuery]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromqueryattribute) with the [`[Parameter]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute) to specify that a component parameter of a *routable* component comes from the query string.

Note

Component parameters can only receive query parameter values in routable components with an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive.

Only routable components directly receive query parameters in order to avoid subverting top-down information flow and to make parameter processing order clear, both by the framework and by the app. This design avoids subtle bugs in app code that was written assuming a specific parameter processing order. You're free to define custom cascading parameters or directly assign to regular component parameters in order to pass query parameter values to non-routable components.

Component parameters supplied from the query string support the following types:

* `bool`, `DateTime`, `decimal`, `double`, `float`, `Guid`, `int`, `long`, `string`.
* Nullable variants of the preceding types.
* Arrays of the preceding types, whether they're nullable or not nullable.

The correct culture-invariant formatting is applied for the given type ([CultureInfo.InvariantCulture](/en-us/dotnet/api/system.globalization.cultureinfo.invariantculture#system-globalization-cultureinfo-invariantculture)).

Note

[C# union types](/en-us/dotnet/csharp/language-reference/builtin-types/union) aren't supported by `[SupplyParameterFromQuery]` or `[SupplyParameterFromForm]`, which bind string or form values without JSON parsing.

Specify the `[SupplyParameterFromQuery]` attribute's [Name](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromqueryattribute.name#microsoft-aspnetcore-components-supplyparameterfromqueryattribute-name) property to use a query parameter name different from the component parameter name. In the following example, the C# name of the component parameter is `{COMPONENT PARAMETER NAME}`. A different query parameter name is specified for the `{QUERY PARAMETER NAME}` placeholder:

Unlike component parameter properties (`[Parameter]`), `[SupplyParameterFromQuery]` properties can be marked `private` in addition to `public`.

```
[SupplyParameterFromQuery(Name = "{QUERY PARAMETER NAME}")]
private string? {COMPONENT PARAMETER NAME} { get; set; }
```

Just like component parameter properties (`[Parameter]`), `[SupplyParameterFromQuery]` properties are always `public` properties in .NET 6/7. In .NET 8 or later, `[SupplyParameterFromQuery]` properties can be marked `public` or `private`.

```
[Parameter]
[SupplyParameterFromQuery(Name = "{QUERY PARAMETER NAME}")]
public string? {COMPONENT PARAMETER NAME} { get; set; }
```

In the following example with a URL of `/search?filter=scifi%20stars&page=3&star=LeVar%20Burton&star=Gary%20Oldman`:

* The `Filter` property resolves to `scifi stars`.
* The `Page` property resolves to `3`.
* The `Stars` array is filled from query parameters named `star` (`Name = "star"`) and resolves to `LeVar Burton` and `Gary Oldman`.

Note

The query string parameters in the following routable page component also work in a *non-routable* component without an `@page` directive (for example, `Search.razor` for a shared `Search` component used in other components).

`Search.razor`:

```
@page "/search"

<h1>Search Example</h1>

<p>Filter: @Filter</p>

<p>Page: @Page</p>

@if (Stars is not null)
{
    <p>Stars:</p>

    <ul>
        @foreach (var name in Stars)
        {
            <li>@name</li>
        }
    </ul>
}

@code {
    [SupplyParameterFromQuery]
    private string? Filter { get; set; }

    [SupplyParameterFromQuery]
    private int? Page { get; set; }

    [SupplyParameterFromQuery(Name = "star")]
    private string[]? Stars { get; set; }
}
```

`Search.razor`:

```
@page "/search"

<h1>Search Example</h1>

<p>Filter: @Filter</p>

<p>Page: @Page</p>

@if (Stars is not null)
{
    <p>Stars:</p>

    <ul>
        @foreach (var name in Stars)
        {
            <li>@name</li>
        }
    </ul>
}

@code {
    [Parameter]
    [SupplyParameterFromQuery]
    public string? Filter { get; set; }

    [Parameter]
    [SupplyParameterFromQuery]
    public int? Page { get; set; }

    [Parameter]
    [SupplyParameterFromQuery(Name = "star")]
    public string[]? Stars { get; set; }
}
```

Use [GetUriWithQueryParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameter) to add, change, or remove one or more query parameters on the current URL.

Note

For the following examples throughout this section that call [GetUriWithQueryParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameter) or [GetUriWithQueryParameters](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameters), `Navigation` is an injected [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager):

```
@inject NavigationManager Navigation
```

```
Navigation.NavigateTo(Navigation.GetUriWithQueryParameter("{NAME}", {VALUE}));
```

For the preceding example:

* The `{NAME}` placeholder specifies the query parameter name. The `{VALUE}` placeholder specifies the value as a supported type. Supported types are listed later in this section.

* A string is returned equal to the current URL with a single parameter:

  * Added if the query parameter name doesn't exist in the current URL.
  * Updated to the value provided if the query parameter exists in the current URL.
  * Removed if the type of the provided value is nullable and the value is `null`.

* The correct culture-invariant formatting is applied for the given type ([CultureInfo.InvariantCulture](/en-us/dotnet/api/system.globalization.cultureinfo.invariantculture#system-globalization-cultureinfo-invariantculture)).

* The query parameter name and value are URL-encoded.

* All of the values with the matching query parameter name are replaced if there are multiple instances of the type.

Call [GetUriWithQueryParameters](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameters) to create a URI constructed from [Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri) with multiple parameters added, updated, or removed. For each value, the framework uses `value?.GetType()` to determine the runtime type for each query parameter and selects the correct culture-invariant formatting. The framework throws an error for unsupported types.

```
Navigation.NavigateTo(Navigation.GetUriWithQueryParameters({PARAMETERS}));
```

The `{PARAMETERS}` placeholder is a `Dictionary<string, object?>`.

Pass a URI string to [GetUriWithQueryParameters](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameters) to generate a new URI from a provided URI with multiple parameters added, updated, or removed. For each value, the framework uses `value?.GetType()` to determine the runtime type for each query parameter and selects the correct culture-invariant formatting. The framework throws an error for unsupported types. Supported types are listed later in this section.

```
Navigation.NavigateTo(Navigation.GetUriWithQueryParameters("{URI}", {PARAMETERS}));
```

* The `{URI}` placeholder is the URI with or without a query string.
* The `{PARAMETERS}` placeholder is a `Dictionary<string, object?>`.

Supported types are identical to supported types for route constraints:

* `bool`
* `DateOnly`
* `DateTime`
* `decimal`
* `double`
* `float`
* `Guid`
* `int`
* `long`
* `string`
* `TimeOnly`

Supported types include:

* Nullable variants of the preceding types.
* Arrays of the preceding types, whether they're nullable or not nullable.

Warning

With compression, which is enabled by default, avoid creating secure (authenticated/authorized) interactive server-side components that render data from untrusted sources. Untrusted sources include route parameters, query strings, data from JS interop, and any other source of data that a third-party user can control (databases, external services). For more information, see [ASP.NET Core Blazor SignalR guidance](signalr?view=aspnetcore-10.0#websocket-compression-for-interactive-server-components) and [Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering](../security/interactive-server-side-rendering?view=aspnetcore-9.0#interactive-server-components-with-websocket-compression-enabled).

### Replace a query parameter value when the parameter exists

```
Navigation.NavigateTo(
    Navigation.GetUriWithQueryParameter("full name", "Morena Baccarin"));
```

| Current URL                                                                  | Generated URL                                                                       |
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `scheme://host/?full%20name=David%20Krumholtz&age=42`                        | `scheme://host/?full%20name=Morena%20Baccarin&age=42`                               |
| `scheme://host/?fUlL%20nAmE=David%20Krumholtz&AgE=42`                        | `scheme://host/?full%20name=Morena%20Baccarin&AgE=42`                               |
| `scheme://host/?full%20name=Jewel%20Staite&age=42&full%20name=Summer%20Glau` | `scheme://host/?full%20name=Morena%20Baccarin&age=42&full%20name=Morena%20Baccarin` |
| `scheme://host/?full%20name=&age=42`                                         | `scheme://host/?full%20name=Morena%20Baccarin&age=42`                               |
| `scheme://host/?full%20name=`                                                | `scheme://host/?full%20name=Morena%20Baccarin`                                      |

### Append a query parameter and value when the parameter doesn't exist

```
Navigation.NavigateTo(
    Navigation.GetUriWithQueryParameter("name", "Morena Baccarin"));
```

| Current URL             | Generated URL                                  |
| ----------------------- | ---------------------------------------------- |
| `scheme://host/?age=42` | `scheme://host/?age=42&name=Morena%20Baccarin` |
| `scheme://host/`        | `scheme://host/?name=Morena%20Baccarin`        |
| `scheme://host/?`       | `scheme://host/?name=Morena%20Baccarin`        |

### Remove a query parameter when the parameter value is `null`

```
Navigation.NavigateTo(
    Navigation.GetUriWithQueryParameter("full name", (string)null));
```

| Current URL                                                                 | Generated URL           |
| --------------------------------------------------------------------------- | ----------------------- |
| `scheme://host/?full%20name=David%20Krumholtz&age=42`                       | `scheme://host/?age=42` |
| `scheme://host/?full%20name=Sally%20Smith&age=42&full%20name=Summer%20Glau` | `scheme://host/?age=42` |
| `scheme://host/?full%20name=Sally%20Smith&age=42&FuLl%20NaMe=Summer%20Glau` | `scheme://host/?age=42` |
| `scheme://host/?full%20name=&age=42`                                        | `scheme://host/?age=42` |
| `scheme://host/?full%20name=`                                               | `scheme://host/`        |

### Add, update, and remove query parameters

In the following example:

* `name` is removed, if present.
* `age` is added with a value of `25` (`int`), if not present. If present, `age` is updated to a value of `25`.
* `eye color` is added or updated to a value of `green`.

```
Navigation.NavigateTo(Navigation.GetUriWithQueryParameters(
    new Dictionary<string, object?>
    {
        ["name"] = null,
        ["age"] = (int?)25,
        ["eye color"] = "green"
    }));
```

| Current URL                                                | Generated URL                                         |
| ---------------------------------------------------------- | ----------------------------------------------------- |
| `scheme://host/?name=David%20Krumholtz&age=42`             | `scheme://host/?age=25&eye%20color=green`             |
| `scheme://host/?NaMe=David%20Krumholtz&AgE=42`             | `scheme://host/?age=25&eye%20color=green`             |
| `scheme://host/?name=David%20Krumholtz&age=42&keepme=true` | `scheme://host/?age=25&keepme=true&eye%20color=green` |
| `scheme://host/?age=42&eye%20color=87`                     | `scheme://host/?age=25&eye%20color=green`             |
| `scheme://host/?`                                          | `scheme://host/?age=25&eye%20color=green`             |
| `scheme://host/`                                           | `scheme://host/?age=25&eye%20color=green`             |

### Support for enumerable values

In the following example:

* `full name` is added or updated to `Morena Baccarin`, a single value.
* `ping` parameters are added or replaced with `35`, `16`, `87` and `240`.

```
Navigation.NavigateTo(Navigation.GetUriWithQueryParameters(
    new Dictionary<string, object?>
    {
        ["full name"] = "Morena Baccarin",
        ["ping"] = new int?[] { 35, 16, null, 87, 240 }
    }));
```

| Current URL                                                    | Generated URL                                                                   |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `scheme://host/?full%20name=David%20Krumholtz&ping=8&ping=300` | `scheme://host/?full%20name=Morena%20Baccarin&ping=35&ping=16&ping=87&ping=240` |
| `scheme://host/?ping=8&full%20name=David%20Krumholtz&ping=300` | `scheme://host/?ping=35&full%20name=Morena%20Baccarin&ping=16&ping=87&ping=240` |
| `scheme://host/?ping=8&ping=300&ping=50&ping=68&ping=42`       | `scheme://host/?ping=35&ping=16&ping=87&ping=240&full%20name=Morena%20Baccarin` |

### Navigate with an added or modified query string

To navigate with an added or modified query string, pass a generated URL to [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto).

The following example calls:

* [GetUriWithQueryParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanagerextensions.geturiwithqueryparameter) to add or replace the `name` query parameter using a value of `Morena Baccarin`.
* Calls [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) to trigger navigation to the new URL.

```
Navigation.NavigateTo(
    Navigation.GetUriWithQueryParameter("name", "Morena Baccarin"));
```

The query string of a request is obtained from the [NavigationManager.Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri) property:

```
@inject NavigationManager Navigation

...

var query = new Uri(Navigation.Uri).Query;
```

To parse a query string's parameters, one approach is to use [`URLSearchParams`](https://developer.mozilla.org/docs/Web/API/URLSearchParams) with [JavaScript (JS) interop](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0):

```
export createQueryString = (string queryString) => new URLSearchParams(queryString);
```

For more information on JavaScript isolation with JavaScript modules, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules).

## Navigate to an element at the current URI

Use the `GetUriWithFragment` extension method to construct a URI with a hash fragment. This helper method provides an efficient, zero-allocation way to append hash fragments to the current URI. The following example demonstrates two use cases:

* Inline call that jumps to Section 1 (`id="section-1"`) of the rendered page.
* Method call that receives a section identifier (`sectionId`) and navigates to the section of the page.

```
@inject NavigationManager Navigation

<a href="@Navigation.GetUriWithFragment("section-1")">
    Jump to Section 1
</a>

@code {
    private void NavigateToSection(string sectionId)
    {
        var uri = Navigation.GetUriWithFragment(sectionId);
        Navigation.NavigateTo(uri);
    }
}
```

The method uses `string.Create` for optimal performance and works correctly with non-root base URIs (for example, when using `<base href="/app/">`).

## Navigate to an element with a hashed (`#`) reference

Navigate to an element using the following approaches with a hashed (`#`) reference to the element. Routes to elements within the component and routes to elements in external components use root-relative paths. A leading forward slash (`/`) is optional.

Examples for each of the following approaches demonstrate navigation to an element with an `id` of `targetElement` in the `Counter` component:

* Anchor element (`<a>`) with an `href`:

  ```
  <a href="/counter#targetElement">
  ```

* [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component with an `href`:

  ```
  <NavLink href="/counter#targetElement">
  ```

* [NavigationManager.NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) passing the relative URL:

  ```
  Navigation.NavigateTo("/counter#targetElement");
  ```

The following example demonstrates navigating to an H2 heading in an external component.

Note

For element navigation at the same URI, see [Navigate to an element at the current URI](#navigate-to-an-element-at-the-current-uri).

In the `Home` (`Home.razor`) and `Counter` (`Counter.razor`) components, place the following markup at the bottoms of the existing component markup to serve as navigation targets. The `<div>` creates artificial vertical space to demonstrate browser scrolling behavior:

```
<div class="border border-info rounded bg-info" style="height:500px"></div>

<h2 id="targetElement">Target H2 heading</h2>
<p>Content!</p>
```

Add the following `FragmentRouting` component to the app.

`FragmentRouting.razor`:

```
@page "/fragment-routing"
@inject NavigationManager Navigation

<PageTitle>Fragment routing</PageTitle>

<h1>Fragment routing to elements</h1>

<ul>
    <li>
        <a href="/#targetElement">
            Anchor to the <code>Home</code> component
        </a>
    </li>
    <li>
        <a href="/counter#targetElement">
            Anchor to the <code>Counter</code> component
        </a>
    </li>
    <li>
        <button @onclick="NavigateToElement">
            Navigate with <code>NavigationManager</code> to the 
            <code>Counter</code> component
        </button>
    </li>
</ul>

@code {
    private void NavigateToElement()
    {
        Navigation.NavigateTo("/counter#targetElement");
    }
}
```

The following example demonstrates navigating to H2 headings within a component and to external components.

In the `Home` (`Home.razor`) and `Counter` (`Counter.razor`) components, place the following markup at the bottoms of the existing component markup to serve as navigation targets. The `<div>` creates artificial vertical space to demonstrate browser scrolling behavior:

```
<div class="border border-info rounded bg-info" style="height:500px"></div>

<h2 id="targetElement">Target H2 heading</h2>
<p>Content!</p>
```

Add the following `FragmentRouting` component to the app.

`FragmentRouting.razor`:

```
@page "/fragment-routing"
@inject NavigationManager Navigation

<PageTitle>Fragment routing</PageTitle>

<h1>Fragment routing to elements</h1>

<ul>
    <li>
        <a href="/fragment-routing#targetElement">
            Anchor in this component
        </a>
    </li>
    <li>
        <a href="/#targetElement">
            Anchor to the <code>Home</code> component
        </a>
    </li>
    <li>
        <a href="/counter#targetElement">
            Anchor to the <code>Counter</code> component
        </a>
    </li>
    <li>
        <NavLink href="/fragment-routing#targetElement">
            Use a `NavLink` component in this component
        </NavLink>
    </li>
    <li>
        <button @onclick="NavigateToElement">
            Navigate with <code>NavigationManager</code> to the 
            <code>Counter</code> component
        </button>
    </li>
</ul>

<div class="border border-info rounded bg-info" style="height:500px"></div>

<h2 id="targetElement">Target H2 heading</h2>
<p>Content!</p>

@code {
    private void NavigateToElement()
    {
        Navigation.NavigateTo("/counter#targetElement");
    }
}
```

## Handle/prevent location changes

[RegisterLocationChangingHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.registerlocationchanginghandler) registers a handler to process incoming navigation events. The handler's context provided by [LocationChangingContext](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangingcontext) includes the following properties:

* [TargetLocation](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangingcontext.targetlocation#microsoft-aspnetcore-components-routing-locationchangingcontext-targetlocation): Gets the target location.
* [HistoryEntryState](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangingcontext.historyentrystate#microsoft-aspnetcore-components-routing-locationchangingcontext-historyentrystate): Gets the state associated with the target history entry.
* [IsNavigationIntercepted](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangingcontext.isnavigationintercepted#microsoft-aspnetcore-components-routing-locationchangingcontext-isnavigationintercepted): Gets whether the navigation was intercepted from a link.
* [CancellationToken](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangingcontext.cancellationtoken#microsoft-aspnetcore-components-routing-locationchangingcontext-cancellationtoken): Gets a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) to determine if the navigation was canceled, for example, to determine if the user triggered a different navigation.
* [PreventNavigation](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangingcontext.preventnavigation): Called to prevent the navigation from continuing.

A component can register multiple location changing handlers in the [`OnAfterRender{Async}` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync). Navigation invokes all of the location changing handlers registered across the entire app (across multiple components), and any internal navigation executes them all in parallel. In addition to [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) handlers are invoked:

* When selecting internal links, which are links that point to URLs under the app's base path.
* When navigating using the forward and back buttons in a browser.

Handlers are only executed for internal navigation within the app. If the user selects a link that navigates to a different site or changes the address bar to a different site manually, location changing handlers aren't executed.

Implement [IDisposable](/en-us/dotnet/api/system.idisposable) and dispose registered handlers to unregister them. For more information, see [ASP.NET Core Razor component disposal](../components/component-disposal?view=aspnetcore-10.0).

Important

Don't attempt to execute DOM cleanup tasks via JavaScript (JS) interop when handling location changes. Use the [`MutationObserver` pattern](https://developer.mozilla.org/docs/Web/API/MutationObserver) in JS on the client. For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](../javascript-interoperability/?view=aspnetcore-10.0#dom-cleanup-tasks-during-component-disposal).

In the following example, a location changing handler is registered for navigation events.

`NavHandler.razor`:

```
@page "/nav-handler"
@implements IDisposable
@inject NavigationManager Navigation

<p>
    <button @onclick="@(() => Navigation.NavigateTo("/"))">
        Home (Allowed)
    </button>
    <button @onclick="@(() => Navigation.NavigateTo("/counter"))">
        Counter (Prevented)
    </button>
</p>

@code {
    private IDisposable? registration;

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            registration = 
                Navigation.RegisterLocationChangingHandler(OnLocationChanging);
        }
    }

    private ValueTask OnLocationChanging(LocationChangingContext context)
    {
        if (context.TargetLocation == "/counter")
        {
            context.PreventNavigation();
        }

        return ValueTask.CompletedTask;
    }

    public void Dispose() => registration?.Dispose();
}
```

Since internal navigation can be canceled asynchronously, multiple overlapping calls to registered handlers may occur. For example, multiple handler calls may occur when the user rapidly selects the back button on a page or selects multiple links before a navigation is executed. The following is a summary of the asynchronous navigation logic:

* If any location changing handlers are registered, all navigation is initially reverted, then replayed if the navigation isn't canceled.

* If overlapping navigation requests are made, the latest request always cancels earlier requests, which means the following:

  * The app may treat multiple back and forward button selections as a single selection.
  * If the user selects multiple links before the navigation completes, the last link selected determines the navigation.

For more information on passing [NavigationOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationoptions) to [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) to control entries and state of the navigation history stack, see the [Navigation options](#navigation-options) section.

For additional example code, see the [`NavigationManagerComponent` in the `BasicTestApp` (`dotnet/aspnetcore` reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/RouterTest/NavigationManagerComponent.razor).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

The [`NavigationLock` component](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationlock) intercepts navigation events as long as it is rendered, effectively "locking" any given navigation until a decision is made to either proceed or cancel. Use `NavigationLock` when navigation interception can be scoped to the lifetime of a component.

[NavigationLock](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationlock) parameters:

* [ConfirmExternalNavigation](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationlock.confirmexternalnavigation#microsoft-aspnetcore-components-routing-navigationlock-confirmexternalnavigation) sets a browser dialog to prompt the user to either confirm or cancel external navigation. The default value is `false`. Displaying the confirmation dialog requires initial user interaction with the page before triggering external navigation with the URL in the browser's address bar. For more information on the interaction requirement, see [Window: `beforeunload` event](https://developer.mozilla.org/docs/Web/API/Window/beforeunload_event).
* [OnBeforeInternalNavigation](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationlock.onbeforeinternalnavigation#microsoft-aspnetcore-components-routing-navigationlock-onbeforeinternalnavigation) sets a callback for internal navigation events.

In the following `NavLock` component:

* An attempt to follow the link to Microsoft's website must be confirmed by the user before the navigation to `https://www.microsoft.com` succeeds.
* [PreventNavigation](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.locationchangingcontext.preventnavigation) is called to prevent navigation from occurring if the user declines to confirm the navigation via a [JavaScript (JS) interop call](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0) that spawns the [JS `confirm` dialog](https://developer.mozilla.org/docs/Web/API/Window/confirm).

`NavLock.razor`:

```
@page "/nav-lock"
@inject IJSRuntime JSRuntime
@inject NavigationManager Navigation

<NavigationLock ConfirmExternalNavigation="true" 
    OnBeforeInternalNavigation="OnBeforeInternalNavigation" />

<p>
    <button @onclick="Navigate">Navigate</button>
</p>

<p>
    <a href="https://www.microsoft.com">Microsoft homepage</a>
</p>

@code {
    private void Navigate()
    {
        Navigation.NavigateTo("/");
    }

    private async Task OnBeforeInternalNavigation(LocationChangingContext context)
    {
        var isConfirmed = await JSRuntime.InvokeAsync<bool>("confirm", 
            "Are you sure you want to navigate to the root page?");

        if (!isConfirmed)
        {
            context.PreventNavigation();
        }
    }
}
```

For additional example code, see the [`ConfigurableNavigationLock` component in the `BasicTestApp` (`dotnet/aspnetcore` reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/RouterTest/ConfigurableNavigationLock.razor).

## Dynamically-generated `NavLink` components via reflection

[NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component entries can be dynamically created from the app's components via reflection. The following example demonstrates the general approach for further customization.

For the following demonstration, a consistent, standard naming convention is used for the app's components:

* Routable component file names use Pascal case†, for example `Pages/ProductDetail.razor`.
* Routable component file paths match their URLs in kebab case‡ with hyphens appearing between words in a component's route template. For example, a `ProductDetail` component with a route template of `/product-detail` (`@page "/product-detail"`) is requested in a browser at the relative URL `/product-detail`.

†Pascal case (upper camel case) is a naming convention without spaces and punctuation and with the first letter of each word capitalized, including the first word.\
‡Kebab case is a naming convention without spaces and punctuation that uses lowercase letters and dashes between words.

In the Razor markup of the `NavMenu` component (`NavMenu.razor`) under the default `Home` page, [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) components are added from a collection:

```
<nav class="flex-column">
    <div class="nav-item px-3">
        <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
            <span class="bi bi-house-door-fill-nav-menu" 
                aria-hidden="true"></span> Home
        </NavLink>
    </div>

+   @foreach (var name in GetRoutableComponents())
+   {
+       <div class="nav-item px-3">
+           <NavLink class="nav-link" 
+                   href="@Regex.Replace(name, @"(\B[A-Z]|\d+)", "-$1").ToLower()">
+               @Regex.Replace(name, @"(\B[A-Z]|\d+)", " $1")
+           </NavLink>
+       </div>
+   }
</nav>
```

The `GetRoutableComponents` method in the `@code` block:

```
public IEnumerable<string> GetRoutableComponents() => 
    Assembly.GetExecutingAssembly()
        .ExportedTypes
        .Where(t => t.IsSubclassOf(typeof(ComponentBase)))
        .Where(c => c.GetCustomAttributes(inherit: true)
                     .OfType<RouteAttribute>()
                     .Any())
        .Where(c => c.Name != "Home" && c.Name != "Error")
        .OrderBy(o => o.Name)
        .Select(c => c.Name);
```

The preceding example doesn't include the following pages in the rendered list of components:

* `Home` page: The page is listed separately from the automatically generated links because it should appear at the top of the list and set the `Match` parameter.
* `Error` page: The error page is only navigated to by the framework and shouldn't be listed.

For an demonstration of the preceding code in a sample app, obtain the [**Blazor Web App** or **Blazor WebAssembly** sample app](./?view=aspnetcore-10.0#sample-apps).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/websockets.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# WebSockets support in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to get started with WebSockets in ASP.NET Core. [WebSocket](https://wikipedia.org/wiki/WebSocket) ([RFC 6455](https://tools.ietf.org/html/rfc6455)) is a protocol that enables two-way persistent communication channels over TCP connections. It's used in apps that benefit from fast, real-time communication, such as chat, dashboard, and game apps.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample), [how to run](#sample-app)).

## Http/2 WebSockets support

Using WebSockets over HTTP/2 takes advantage of new features such as:

* Header compression.
* Multiplexing, which reduces the time and resources needed when making multiple requests to the server.

These supported features are available in Kestrel on all HTTP/2 enabled platforms. The version negotiation is automatic in browsers and Kestrel, so no new APIs are needed.

.NET 7 introduced WebSockets over HTTP/2 support for Kestrel, the SignalR JavaScript client, and SignalR with Blazor WebAssembly.

Note

HTTP/2 WebSockets use CONNECT requests rather than GET, so your own routes and controllers may need updating. For more information, see [Add HTTP/2 WebSockets support for existing controllers](#add-http2-websockets-support-for-existing-controllers) in this article.

Chrome, Edge, and Firefox (version 128 and later) have HTTP/2 WebSockets enabled by default. You can verify or change this setting in Firefox by opening `about:config` and locating the `network.http.http2.websockets` preference.

WebSockets were originally designed for HTTP/1.1 but have since been adapted to work over HTTP/2. ([RFC 8441](https://www.rfc-editor.org/rfc/rfc8441))

## SignalR

[ASP.NET Core SignalR](../signalr/introduction?view=aspnetcore-10.0) is a library that simplifies adding real-time web functionality to apps. It uses WebSockets whenever possible.

For most applications, we recommend SignalR rather than raw WebSockets. SignalR:

* Provides transport fallback for environments where WebSockets isn't available.
* Provides a basic remote procedure call app model.
* Has no significant performance disadvantage compared to using raw WebSockets in most scenarios.

WebSockets over HTTP/2 is supported for:

* ASP.NET Core SignalR JavaScript client
* ASP.NET Core SignalR with Blazor WebAssembly

For some apps, [gRPC on .NET](../grpc/?view=aspnetcore-10.0) provides an alternative to WebSockets.

## Prerequisites

* Any OS that supports ASP.NET Core:

  * Windows 7 / Windows Server 2008 or later
  * Linux
  * macOS

* If the app runs on Windows with IIS:

  * Windows 8 / Windows Server 2012 or later
  * IIS 8 / IIS 8 Express
  * WebSockets must be enabled. See the [IIS/IIS Express support](#iisiis-express-support) section.

* If the app runs on [HTTP.sys](servers/httpsys?view=aspnetcore-10.0):
  * Windows 8 / Windows Server 2012 or later

* For supported browsers, see [Can I use](https://caniuse.com/?search=websockets).

## Configure the middleware

Add the WebSockets middleware in `Program.cs`:

```
app.UseWebSockets();
```

The following settings can be configured:

* [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.keepaliveinterval) - How frequently to send "ping" frames to the client to ensure proxies keep the connection open. The default is two minutes.
* [KeepAliveTimeout](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.keepalivetimeout) - How long to wait for a "pong" frame from the client after sending a "ping" frame. If the timeout is exceeded, the WebSocket connection is aborted and `WebSocket.ReceiveAsync` throws an exception. The default is `Timeout.InfiniteTimeSpan` (disabled). For more information, see [Handle client disconnects](#handle-client-disconnects) in this article.
* [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) - A list of allowed Origin header values for WebSocket requests. By default, all origins are allowed. For more information, see [WebSocket origin restriction](#websocket-origin-restriction) in this article.

```
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);
```

## Accept WebSocket requests

Somewhere later in the request life cycle (later in `Program.cs` or in an action method, for example) check if it's a WebSocket request and accept the WebSocket request.

The following example is from later in `Program.cs`:

```
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});
```

A WebSocket request could come in on any URL, but this sample code only accepts requests for `/ws`.

A similar approach can be taken in a controller method:

```
public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
```

When using a WebSocket, you **must** keep the middleware pipeline running for the duration of the connection. If you attempt to send or receive a WebSocket message after the middleware pipeline ends, you may get an exception like the following:

```
System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.
```

If you're using a background service to write data to a WebSocket, make sure you keep the middleware pipeline running. Do this by using a [TaskCompletionSource\<TResult>](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1). Pass the `TaskCompletionSource` to your background service and have it call [TrySetResult](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.trysetresult) when you finish with the WebSocket. Then `await` the [Task](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.task#system-threading-tasks-taskcompletionsource-1-task) property during the request, as shown in the following example:

```
app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});
```

The WebSocket closed exception can also happen when returning too soon from an action method. When accepting a socket in an action method, wait for the code that uses the socket to complete before returning from the action method.

Never use `Task.Wait`, `Task.Result`, or similar blocking calls to wait for the socket to complete, as that can cause serious threading issues. Always use `await`.

## Add HTTP/2 WebSockets support for existing controllers

.NET 7 introduced WebSockets over HTTP/2 support for Kestrel, the SignalR JavaScript client, and SignalR with Blazor WebAssembly. HTTP/2 WebSockets use CONNECT requests rather than GET. If you previously used `[HttpGet("/path")]` on your controller action method for Websocket requests, update it to use `[Route("/path")]` instead.

```
public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
```

### Compression

Warning

Enabling compression over encrypted connections can make an app subject to CRIME/BREACH attacks. If sending sensitive information, avoid enabling compression or use `WebSocketMessageFlags.DisableCompression` when calling `WebSocket.SendAsync`. This applies to both sides of the WebSocket. Note that the WebSockets API in the browser doesn't have configuration for disabling compression per send.

If compression of messages over WebSockets is desired, then the accept code must specify that it allows compression as follows:

```
using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}
```

`WebSocketAcceptContext.ServerMaxWindowBits` and `WebSocketAcceptContext.DisableServerContextTakeover` are advanced options that control how the compression works.

Compression is negotiated between the client and server when first establishing a connection. You can read more about the negotiation in the [Compression Extensions for WebSocket RFC](https://datatracker.ietf.org/doc/html/rfc7692#section-7).

Note

If the compression negotiation isn't accepted by either the server or client, the connection is still established. However, the connection doesn't use compression when sending and receiving messages.

## Send and receive messages

The `AcceptWebSocketAsync` method upgrades the TCP connection to a WebSocket connection and provides a [WebSocket](/en-us/dotnet/api/system.net.websockets.websocket) object. Use the `WebSocket` object to send and receive messages.

The code shown earlier that accepts the WebSocket request passes the `WebSocket` object to an `Echo` method. The code receives a message and immediately sends back the same message. Messages are sent and received in a loop until the client closes the connection:

```
private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}
```

When accepting the WebSocket connection before beginning the loop, the middleware pipeline ends. Upon closing the socket, the pipeline unwinds. That is, the request stops moving forward in the pipeline when the WebSocket is accepted. When the loop is finished and the socket is closed, the request proceeds back up the pipeline.

## Handle client disconnects

The server isn't automatically informed when the client disconnects due to loss of connectivity. The server receives a disconnect message only if the client sends it, which can't be done if the internet connection is lost. If you want to take some action when that happens, set a timeout after nothing is received from the client within a certain time window.

If the client isn't always sending messages and you don't want to time out just because the connection goes idle, have the client use a timer to send a ping message every X seconds. On the server, if a message hasn't arrived within 2\*X seconds after the previous one, terminate the connection and report that the client disconnected. Wait for twice the expected time interval to leave extra time for network delays that might hold up the ping message.

### Keep-alive timeout

The WebSockets middleware can be configured with a keep-alive timeout. The keep-alive timeout aborts the WebSocket connection and throws an exception from `WebSocket.ReceiveAsync` if both of the following conditions are met:

* The server sends a ping frame using the WebSocket protocol.
* The client doesn't reply with a pong frame within the specified timeout.

The server automatically sends the ping frame and configures its frequency with [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.keepaliveinterval).

The keep-alive timeout setting is useful for detecting connections that might be slow or ungracefully disconnected. The default is `Timeout.InfiniteTimeSpan`, which disables the timeout.

The keep-alive timeout can be configured globally for the WebSocket middleware:

```
app.UseWebSockets(new WebSocketOptions { KeepAliveTimeout = TimeSpan.FromSeconds(15) });
```

Or configured per accepted WebSocket:

```
using var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { KeepAliveTimeout = TimeSpan.FromSeconds(15) });
```

## WebSocket origin restriction

The protections provided by CORS don't apply to WebSockets. Browsers do **not**:

* Perform CORS pre-flight requests.
* Respect the restrictions specified in `Access-Control` headers when making WebSocket requests.

However, browsers do send the `Origin` header when issuing WebSocket requests. Applications should be configured to validate these headers to ensure that only WebSockets coming from the expected origins are allowed.

If you're hosting your server on "https\://server.com" and hosting your client on "https\://client.com", add "https\://client.com" to the [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) list for WebSockets to verify.

```
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);
```

Note

The `Origin` header is controlled by the client and, like the `Referer` header, can be faked. Do **not** use these headers as an authentication mechanism.

### IIS/IIS Express support

Windows Server 2012 or later and Windows 8 or later with IIS/IIS Express 8 or later has support for the WebSocket protocol, but not for WebSockets over HTTP/2.

Note

WebSockets are always enabled when using IIS Express.

### Enabling WebSockets on IIS

To enable support for the WebSocket protocol on Windows Server 2012 or later:

Note

These steps are not required when using IIS Express

1. Use the **Add Roles and Features** wizard from the **Manage** menu or the link in **Server Manager**.
2. Select **Role-based or Feature-based Installation**. Select **Next**.
3. Select the appropriate server (the local server is selected by default). Select **Next**.
4. Expand **Web Server (IIS)** in the **Roles** tree, expand **Web Server**, and then expand **Application Development**.
5. Select **WebSocket Protocol**. Select **Next**.
6. If additional features aren't needed, select **Next**.
7. Select **Install**.
8. When the installation completes, select **Close** to exit the wizard.

To enable support for the WebSocket protocol on Windows 8 or later:

Note

These steps are not required when using IIS Express

1. Navigate to **Control Panel** > **Programs** > **Programs and Features** > **Turn Windows features on or off** (left side of the screen).
2. Open the following nodes: **Internet Information Services** > **World Wide Web Services** > **Application Development Features**.
3. Select the **WebSocket Protocol** feature. Select **OK**.

### Disable WebSocket when using socket.io on Node.js

If using the WebSocket support in [socket.io](https://socket.io/) on [Node.js](https://nodejs.org/), disable the default IIS WebSocket module using the `webSocket` element in *web.config* or *applicationHost.config*. If this step isn't performed, the IIS WebSocket module attempts to handle the WebSocket communication rather than Node.js and the app.

```
<system.webServer>
  <webSocket enabled="false" />
</system.webServer>
```

## Sample app

The [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) that accompanies this article is an echo app. It has a webpage that makes WebSocket connections, and the server resends any messages it receives back to the client. The sample app supports WebSockets over HTTP/2 when using a targeted framework of .NET 7 or later.

Run the app:

* To run app in Visual Studio: Open the sample project in Visual Studio, and press Ctrl+F5 to run without the debugger.
* To run the app in a command shell: Run the command [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) and navigate in a browser to `http://localhost:<port>`.

The webpage shows the connection status:

Select **Connect** to send a WebSocket request to the URL shown. Enter a test message and select **Send**. When done, select **Close Socket**. The **Communication Log** section reports each open, send, and close action as it happens.

This article explains how to get started with WebSockets in ASP.NET Core. [WebSocket](https://wikipedia.org/wiki/WebSocket) ([RFC 6455](https://tools.ietf.org/html/rfc6455)) is a protocol that enables two-way persistent communication channels over TCP connections. It's used in apps that benefit from fast, real-time communication, such as chat, dashboard, and game apps.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample), [how to run](#sample-app)).

## Http/2 WebSockets support

Using WebSockets over HTTP/2 takes advantage of new features such as:

* Header compression.
* Multiplexing, which reduces the time and resources needed when making multiple requests to the server.

These supported features are available in Kestrel on all HTTP/2 enabled platforms. The version negotiation is automatic in browsers and Kestrel, so no new APIs are needed.

.NET 7 introduced WebSockets over HTTP/2 support for Kestrel, the SignalR JavaScript client, and SignalR with Blazor WebAssembly.

Note

HTTP/2 WebSockets use CONNECT requests rather than GET, so your own routes and controllers may need updating. For more information, see [Add HTTP/2 WebSockets support for existing controllers](#add-http2-websockets-support-for-existing-controllers) in this article.

Chrome, Edge, and Firefox (version 128 and later) have HTTP/2 WebSockets enabled by default. You can verify or change this setting in Firefox by opening `about:config` and locating the `network.http.http2.websockets` preference.

WebSockets were originally designed for HTTP/1.1 but have since been adapted to work over HTTP/2. ([RFC 8441](https://www.rfc-editor.org/rfc/rfc8441))

## SignalR

[ASP.NET Core SignalR](../signalr/introduction?view=aspnetcore-10.0) is a library that simplifies adding real-time web functionality to apps. It uses WebSockets whenever possible.

For most applications, we recommend SignalR rather than raw WebSockets. SignalR:

* Provides transport fallback for environments where WebSockets isn't available.
* Provides a basic remote procedure call app model.
* Has no significant performance disadvantage compared to using raw WebSockets in most scenarios.

WebSockets over HTTP/2 is supported for:

* ASP.NET Core SignalR JavaScript client
* ASP.NET Core SignalR with Blazor WebAssembly

For some apps, [gRPC on .NET](../grpc/?view=aspnetcore-10.0) provides an alternative to WebSockets.

## Prerequisites

* Any OS that supports ASP.NET Core:

  * Windows 7 / Windows Server 2008 or later
  * Linux
  * macOS

* If the app runs on Windows with IIS:

  * Windows 8 / Windows Server 2012 or later
  * IIS 8 / IIS 8 Express
  * WebSockets must be enabled. See the [IIS/IIS Express support](#iisiis-express-support) section.

* If the app runs on [HTTP.sys](servers/httpsys?view=aspnetcore-10.0):
  * Windows 8 / Windows Server 2012 or later

* For supported browsers, see [Can I use](https://caniuse.com/?search=websockets).

## Configure the middleware

Add the WebSockets middleware in `Program.cs`:

```
app.UseWebSockets();
```

The following settings can be configured:

* [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.keepaliveinterval) - How frequently to send "ping" frames to the client to ensure proxies keep the connection open. The default is two minutes.
* [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) - A list of allowed Origin header values for WebSocket requests. By default, all origins are allowed. For more information, see [WebSocket origin restriction](#websocket-origin-restriction) in this article.

```
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);
```

## Accept WebSocket requests

Somewhere later in the request life cycle (later in `Program.cs` or in an action method, for example) check if it's a WebSocket request and accept the WebSocket request.

The following example is from later in `Program.cs`:

```
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});
```

A WebSocket request could come in on any URL, but this sample code only accepts requests for `/ws`.

A similar approach can be taken in a controller method:

```
public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
```

When using a WebSocket, you **must** keep the middleware pipeline running for the duration of the connection. If you attempt to send or receive a WebSocket message after the middleware pipeline ends, you may get an exception like the following:

```
System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.
```

If you're using a background service to write data to a WebSocket, make sure you keep the middleware pipeline running. Do this by using a [TaskCompletionSource\<TResult>](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1). Pass the `TaskCompletionSource` to your background service and have it call [TrySetResult](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.trysetresult) when you finish with the WebSocket. Then `await` the [Task](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.task#system-threading-tasks-taskcompletionsource-1-task) property during the request, as shown in the following example:

```
app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});
```

The WebSocket closed exception can also happen when returning too soon from an action method. When accepting a socket in an action method, wait for the code that uses the socket to complete before returning from the action method.

Never use `Task.Wait`, `Task.Result`, or similar blocking calls to wait for the socket to complete, as that can cause serious threading issues. Always use `await`.

## Add HTTP/2 WebSockets support for existing controllers

.NET 7 introduced WebSockets over HTTP/2 support for Kestrel, the SignalR JavaScript client, and SignalR with Blazor WebAssembly. HTTP/2 WebSockets use CONNECT requests rather than GET. If you previously used `[HttpGet("/path")]` on your controller action method for Websocket requests, update it to use `[Route("/path")]` instead.

```
public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
```

### Compression

Warning

Enabling compression over encrypted connections can make an app subject to CRIME/BREACH attacks. If sending sensitive information, avoid enabling compression or use `WebSocketMessageFlags.DisableCompression` when calling `WebSocket.SendAsync`. This applies to both sides of the WebSocket. Note that the WebSockets API in the browser doesn't have configuration for disabling compression per send.

If compression of messages over WebSockets is desired, then the accept code must specify that it allows compression as follows:

```
using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}
```

`WebSocketAcceptContext.ServerMaxWindowBits` and `WebSocketAcceptContext.DisableServerContextTakeover` are advanced options that control how the compression works.

Compression is negotiated between the client and server when first establishing a connection. You can read more about the negotiation in the [Compression Extensions for WebSocket RFC](https://datatracker.ietf.org/doc/html/rfc7692#section-7).

Note

If the compression negotiation isn't accepted by either the server or client, the connection is still established. However, the connection doesn't use compression when sending and receiving messages.

## Send and receive messages

The `AcceptWebSocketAsync` method upgrades the TCP connection to a WebSocket connection and provides a [WebSocket](/en-us/dotnet/api/system.net.websockets.websocket) object. Use the `WebSocket` object to send and receive messages.

The code shown earlier that accepts the WebSocket request passes the `WebSocket` object to an `Echo` method. The code receives a message and immediately sends back the same message. Messages are sent and received in a loop until the client closes the connection:

```
private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}
```

When accepting the WebSocket connection before beginning the loop, the middleware pipeline ends. Upon closing the socket, the pipeline unwinds. That is, the request stops moving forward in the pipeline when the WebSocket is accepted. When the loop is finished and the socket is closed, the request proceeds back up the pipeline.

## Handle client disconnects

The server isn't automatically informed when the client disconnects due to loss of connectivity. The server receives a disconnect message only if the client sends it, which can't be done if the internet connection is lost. If you want to take some action when that happens, set a timeout after nothing is received from the client within a certain time window.

If the client isn't always sending messages and you don't want to time out just because the connection goes idle, have the client use a timer to send a ping message every X seconds. On the server, if a message hasn't arrived within 2\*X seconds after the previous one, terminate the connection and report that the client disconnected. Wait for twice the expected time interval to leave extra time for network delays that might hold up the ping message.

## WebSocket origin restriction

The protections provided by CORS don't apply to WebSockets. Browsers do **not**:

* Perform CORS pre-flight requests.
* Respect the restrictions specified in `Access-Control` headers when making WebSocket requests.

However, browsers do send the `Origin` header when issuing WebSocket requests. Applications should be configured to validate these headers to ensure that only WebSockets coming from the expected origins are allowed.

If you're hosting your server on "https\://server.com" and hosting your client on "https\://client.com", add "https\://client.com" to the [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) list for WebSockets to verify.

```
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);
```

Note

The `Origin` header is controlled by the client and, like the `Referer` header, can be faked. Do **not** use these headers as an authentication mechanism.

### IIS/IIS Express support

Windows Server 2012 or later and Windows 8 or later with IIS/IIS Express 8 or later has support for the WebSocket protocol, but not for WebSockets over HTTP/2.

Note

WebSockets are always enabled when using IIS Express.

### Enabling WebSockets on IIS

To enable support for the WebSocket protocol on Windows Server 2012 or later:

Note

These steps are not required when using IIS Express

1. Use the **Add Roles and Features** wizard from the **Manage** menu or the link in **Server Manager**.
2. Select **Role-based or Feature-based Installation**. Select **Next**.
3. Select the appropriate server (the local server is selected by default). Select **Next**.
4. Expand **Web Server (IIS)** in the **Roles** tree, expand **Web Server**, and then expand **Application Development**.
5. Select **WebSocket Protocol**. Select **Next**.
6. If additional features aren't needed, select **Next**.
7. Select **Install**.
8. When the installation completes, select **Close** to exit the wizard.

To enable support for the WebSocket protocol on Windows 8 or later:

Note

These steps are not required when using IIS Express

1. Navigate to **Control Panel** > **Programs** > **Programs and Features** > **Turn Windows features on or off** (left side of the screen).
2. Open the following nodes: **Internet Information Services** > **World Wide Web Services** > **Application Development Features**.
3. Select the **WebSocket Protocol** feature. Select **OK**.

### Disable WebSocket when using socket.io on Node.js

If using the WebSocket support in [socket.io](https://socket.io/) on [Node.js](https://nodejs.org/), disable the default IIS WebSocket module using the `webSocket` element in *web.config* or *applicationHost.config*. If this step isn't performed, the IIS WebSocket module attempts to handle the WebSocket communication rather than Node.js and the app.

```
<system.webServer>
  <webSocket enabled="false" />
</system.webServer>
```

## Sample app

The [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) that accompanies this article is an echo app. It has a webpage that makes WebSocket connections, and the server resends any messages it receives back to the client. The sample app supports WebSockets over HTTP/2 when using a targeted framework of .NET 7 or later.

Run the app:

* To run app in Visual Studio: Open the sample project in Visual Studio, and press Ctrl+F5 to run without the debugger.
* To run the app in a command shell: Run the command [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) and navigate in a browser to `http://localhost:<port>`.

The webpage shows the connection status:

Select **Connect** to send a WebSocket request to the URL shown. Enter a test message and select **Send**. When done, select **Close Socket**. The **Communication Log** section reports each open, send, and close action as it happens.

This article explains how to get started with WebSockets in ASP.NET Core. [WebSocket](https://wikipedia.org/wiki/WebSocket) ([RFC 6455](https://tools.ietf.org/html/rfc6455)) is a protocol that enables two-way persistent communication channels over TCP connections. It's used in apps that benefit from fast, real-time communication, such as chat, dashboard, and game apps.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample), [how to run](#sample-app)).

## Http/2 WebSockets support

Using WebSockets over HTTP/2 takes advantage of new features such as:

* Header compression.
* Multiplexing, which reduces the time and resources needed when making multiple requests to the server.

These supported features are available in Kestrel on all HTTP/2 enabled platforms. The version negotiation is automatic in browsers and Kestrel, so no new APIs are needed.

.NET 7 introduced WebSockets over HTTP/2 support for Kestrel, the SignalR JavaScript client, and SignalR with Blazor WebAssembly.

Note

HTTP/2 WebSockets use CONNECT requests rather than GET, so your own routes and controllers may need updating. For more information, see [Add HTTP/2 WebSockets support for existing controllers](#add-http2-websockets-support-for-existing-controllers) in this article.

Chrome, Edge, and Firefox (version 128 and later) have HTTP/2 WebSockets enabled by default. You can verify or change this setting in Firefox by opening `about:config` and locating the `network.http.http2.websockets` preference.

WebSockets were originally designed for HTTP/1.1 but have since been adapted to work over HTTP/2. ([RFC 8441](https://www.rfc-editor.org/rfc/rfc8441))

## SignalR

[ASP.NET Core SignalR](../signalr/introduction?view=aspnetcore-10.0) is a library that simplifies adding real-time web functionality to apps. It uses WebSockets whenever possible.

For most applications, we recommend SignalR rather than raw WebSockets. SignalR:

* Provides transport fallback for environments where WebSockets isn't available.
* Provides a basic remote procedure call app model.
* Has no significant performance disadvantage compared to using raw WebSockets in most scenarios.

WebSockets over HTTP/2 is supported for:

* ASP.NET Core SignalR JavaScript client
* ASP.NET Core SignalR with Blazor WebAssembly

For some apps, [gRPC on .NET](../grpc/?view=aspnetcore-10.0) provides an alternative to WebSockets.

## Prerequisites

* Any OS that supports ASP.NET Core:

  * Windows 7 / Windows Server 2008 or later
  * Linux
  * macOS

* If the app runs on Windows with IIS:

  * Windows 8 / Windows Server 2012 or later
  * IIS 8 / IIS 8 Express
  * WebSockets must be enabled. See the [IIS/IIS Express support](#iisiis-express-support) section.

* If the app runs on [HTTP.sys](servers/httpsys?view=aspnetcore-10.0):
  * Windows 8 / Windows Server 2012 or later

* For supported browsers, see [Can I use](https://caniuse.com/?search=websockets).

## Configure the middleware

Add the WebSockets middleware in `Program.cs`:

```
app.UseWebSockets();
```

The following settings can be configured:

* [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.keepaliveinterval) - How frequently to send "ping" frames to the client to ensure proxies keep the connection open. The default is two minutes.
* [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) - A list of allowed Origin header values for WebSocket requests. By default, all origins are allowed. For more information, see [WebSocket origin restriction](#websocket-origin-restriction) in this article.

```
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);
```

## Accept WebSocket requests

Somewhere later in the request life cycle (later in `Program.cs` or in an action method, for example) check if it's a WebSocket request and accept the WebSocket request.

The following example is from later in `Program.cs`:

```
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});
```

A WebSocket request could come in on any URL, but this sample code only accepts requests for `/ws`.

A similar approach can be taken in a controller method:

```
public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
```

When using a WebSocket, you **must** keep the middleware pipeline running for the duration of the connection. If you attempt to send or receive a WebSocket message after the middleware pipeline ends, you may get an exception like the following:

```
System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.
```

If you're using a background service to write data to a WebSocket, make sure you keep the middleware pipeline running. Do this by using a [TaskCompletionSource\<TResult>](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1). Pass the `TaskCompletionSource` to your background service and have it call [TrySetResult](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.trysetresult) when you finish with the WebSocket. Then `await` the [Task](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.task#system-threading-tasks-taskcompletionsource-1-task) property during the request, as shown in the following example:

```
app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});
```

The WebSocket closed exception can also happen when returning too soon from an action method. When accepting a socket in an action method, wait for the code that uses the socket to complete before returning from the action method.

Never use `Task.Wait`, `Task.Result`, or similar blocking calls to wait for the socket to complete, as that can cause serious threading issues. Always use `await`.

## Add HTTP/2 WebSockets support for existing controllers

.NET 7 introduced WebSockets over HTTP/2 support for Kestrel, the SignalR JavaScript client, and SignalR with Blazor WebAssembly. HTTP/2 WebSockets use CONNECT requests rather than GET. If you previously used `[HttpGet("/path")]` on your controller action method for Websocket requests, update it to use `[Route("/path")]` instead.

```
public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
```

### Compression

Warning

Enabling compression over encrypted connections can make an app subject to CRIME/BREACH attacks. If sending sensitive information, avoid enabling compression or use `WebSocketMessageFlags.DisableCompression` when calling `WebSocket.SendAsync`. This applies to both sides of the WebSocket. Note that the WebSockets API in the browser doesn't have configuration for disabling compression per send.

If compression of messages over WebSockets is desired, then the accept code must specify that it allows compression as follows:

```
using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}
```

`WebSocketAcceptContext.ServerMaxWindowBits` and `WebSocketAcceptContext.DisableServerContextTakeover` are advanced options that control how the compression works.

Compression is negotiated between the client and server when first establishing a connection. You can read more about the negotiation in the [Compression Extensions for WebSocket RFC](https://datatracker.ietf.org/doc/html/rfc7692#section-7).

Note

If the compression negotiation isn't accepted by either the server or client, the connection is still established. However, the connection doesn't use compression when sending and receiving messages.

## Send and receive messages

The `AcceptWebSocketAsync` method upgrades the TCP connection to a WebSocket connection and provides a [WebSocket](/en-us/dotnet/api/system.net.websockets.websocket) object. Use the `WebSocket` object to send and receive messages.

The code shown earlier that accepts the WebSocket request passes the `WebSocket` object to an `Echo` method. The code receives a message and immediately sends back the same message. Messages are sent and received in a loop until the client closes the connection:

```
private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}
```

When accepting the WebSocket connection before beginning the loop, the middleware pipeline ends. Upon closing the socket, the pipeline unwinds. That is, the request stops moving forward in the pipeline when the WebSocket is accepted. When the loop is finished and the socket is closed, the request proceeds back up the pipeline.

## Handle client disconnects

The server isn't automatically informed when the client disconnects due to loss of connectivity. The server receives a disconnect message only if the client sends it, which can't be done if the internet connection is lost. If you want to take some action when that happens, set a timeout after nothing is received from the client within a certain time window.

If the client isn't always sending messages and you don't want to time out just because the connection goes idle, have the client use a timer to send a ping message every X seconds. On the server, if a message hasn't arrived within 2\*X seconds after the previous one, terminate the connection and report that the client disconnected. Wait for twice the expected time interval to leave extra time for network delays that might hold up the ping message.

## WebSocket origin restriction

The protections provided by CORS don't apply to WebSockets. Browsers do **not**:

* Perform CORS pre-flight requests.
* Respect the restrictions specified in `Access-Control` headers when making WebSocket requests.

However, browsers do send the `Origin` header when issuing WebSocket requests. Applications should be configured to validate these headers to ensure that only WebSockets coming from the expected origins are allowed.

If you're hosting your server on "https\://server.com" and hosting your client on "https\://client.com", add "https\://client.com" to the [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) list for WebSockets to verify.

```
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);
```

Note

The `Origin` header is controlled by the client and, like the `Referer` header, can be faked. Do **not** use these headers as an authentication mechanism.

### IIS/IIS Express support

Windows Server 2012 or later and Windows 8 or later with IIS/IIS Express 8 or later has support for the WebSocket protocol, but not for WebSockets over HTTP/2.

Note

WebSockets are always enabled when using IIS Express.

### Enabling WebSockets on IIS

To enable support for the WebSocket protocol on Windows Server 2012 or later:

Note

These steps are not required when using IIS Express

1. Use the **Add Roles and Features** wizard from the **Manage** menu or the link in **Server Manager**.
2. Select **Role-based or Feature-based Installation**. Select **Next**.
3. Select the appropriate server (the local server is selected by default). Select **Next**.
4. Expand **Web Server (IIS)** in the **Roles** tree, expand **Web Server**, and then expand **Application Development**.
5. Select **WebSocket Protocol**. Select **Next**.
6. If additional features aren't needed, select **Next**.
7. Select **Install**.
8. When the installation completes, select **Close** to exit the wizard.

To enable support for the WebSocket protocol on Windows 8 or later:

Note

These steps are not required when using IIS Express

1. Navigate to **Control Panel** > **Programs** > **Programs and Features** > **Turn Windows features on or off** (left side of the screen).
2. Open the following nodes: **Internet Information Services** > **World Wide Web Services** > **Application Development Features**.
3. Select the **WebSocket Protocol** feature. Select **OK**.

### Disable WebSocket when using socket.io on Node.js

If using the WebSocket support in [socket.io](https://socket.io/) on [Node.js](https://nodejs.org/), disable the default IIS WebSocket module using the `webSocket` element in *web.config* or *applicationHost.config*. If this step isn't performed, the IIS WebSocket module attempts to handle the WebSocket communication rather than Node.js and the app.

```
<system.webServer>
  <webSocket enabled="false" />
</system.webServer>
```

## Sample app

The [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) that accompanies this article is an echo app. It has a webpage that makes WebSocket connections, and the server resends any messages it receives back to the client. The sample app supports WebSockets over HTTP/2 when using a targeted framework of .NET 7 or later.

Run the app:

* To run app in Visual Studio: Open the sample project in Visual Studio, and press Ctrl+F5 to run without the debugger.
* To run the app in a command shell: Run the command [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) and navigate in a browser to `http://localhost:<port>`.

The webpage shows the connection status:

Select **Connect** to send a WebSocket request to the URL shown. Enter a test message and select **Send**. When done, select **Close Socket**. The **Communication Log** section reports each open, send, and close action as it happens.

This article explains how to get started with WebSockets in ASP.NET Core. [WebSocket](https://wikipedia.org/wiki/WebSocket) ([RFC 6455](https://tools.ietf.org/html/rfc6455)) is a protocol that enables two-way persistent communication channels over TCP connections. It's used in apps that benefit from fast, real-time communication, such as chat, dashboard, and game apps.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample), [how to run](#sample-app)).

## SignalR

[ASP.NET Core SignalR](../signalr/introduction?view=aspnetcore-10.0) is a library that simplifies adding real-time web functionality to apps. It uses WebSockets whenever possible.

For most applications, we recommend SignalR over raw WebSockets. SignalR provides transport fallback for environments where WebSockets isn't available. It also provides a basic remote procedure call app model. And in most scenarios, SignalR has no significant performance disadvantage compared to using raw WebSockets.

For some apps, [gRPC on .NET](../grpc/?view=aspnetcore-10.0) provides an alternative to WebSockets.

## Prerequisites

* Any OS that supports ASP.NET Core:

  * Windows 7 / Windows Server 2008 or later
  * Linux
  * macOS

* If the app runs on Windows with IIS:

  * Windows 8 / Windows Server 2012 or later
  * IIS 8 / IIS 8 Express
  * WebSockets must be enabled. See the [IIS/IIS Express support](#iisiis-express-support) section.

* If the app runs on [HTTP.sys](servers/httpsys?view=aspnetcore-10.0):
  * Windows 8 / Windows Server 2012 or later

* For supported browsers, see [Can I use](https://caniuse.com/?search=websockets).

## Configure the middleware

Add the WebSockets middleware in `Program.cs`:

```
app.UseWebSockets();
```

The following settings can be configured:

* [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.keepaliveinterval) - How frequently to send "ping" frames to the client to ensure proxies keep the connection open. The default is two minutes.
* [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) - A list of allowed Origin header values for WebSocket requests. By default, all origins are allowed. For more information, see [WebSocket origin restriction](#websocket-origin-restriction) in this article.

```
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);
```

## Accept WebSocket requests

Somewhere later in the request life cycle (later in `Program.cs` or in an action method, for example) check if it's a WebSocket request and accept the WebSocket request.

The following example is from later in `Program.cs`:

```
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});
```

A WebSocket request could come in on any URL, but this sample code only accepts requests for `/ws`.

A similar approach can be taken in a controller method:

```
public class WebSocketController : ControllerBase
{
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
```

When using a WebSocket, you **must** keep the middleware pipeline running for the duration of the connection. If you attempt to send or receive a WebSocket message after the middleware pipeline ends, you may get an exception like the following:

```
System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.
```

If you're using a background service to write data to a WebSocket, make sure you keep the middleware pipeline running. Do this by using a [TaskCompletionSource\<TResult>](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1). Pass the `TaskCompletionSource` to your background service and have it call [TrySetResult](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.trysetresult) when you finish with the WebSocket. Then `await` the [Task](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.task#system-threading-tasks-taskcompletionsource-1-task) property during the request, as shown in the following example:

```
app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});
```

The WebSocket closed exception can also happen when returning too soon from an action method. When accepting a socket in an action method, wait for the code that uses the socket to complete before returning from the action method.

Never use `Task.Wait`, `Task.Result`, or similar blocking calls to wait for the socket to complete, as that can cause serious threading issues. Always use `await`.

### Compression

Warning

Enabling compression over encrypted connections can make an app subject to CRIME/BREACH attacks. If sending sensitive information, avoid enabling compression or use `WebSocketMessageFlags.DisableCompression` when calling `WebSocket.SendAsync`. This applies to both sides of the WebSocket. Note that the WebSockets API in the browser doesn't have configuration for disabling compression per send.

If compression of messages over WebSockets is desired, then the accept code must specify that it allows compression as follows:

```
using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}
```

`WebSocketAcceptContext.ServerMaxWindowBits` and `WebSocketAcceptContext.DisableServerContextTakeover` are advanced options that control how the compression works.

Compression is negotiated between the client and server when first establishing a connection. You can read more about the negotiation in the [Compression Extensions for WebSocket RFC](https://datatracker.ietf.org/doc/html/rfc7692#section-7).

Note

If the compression negotiation isn't accepted by either the server or client, the connection is still established. However, the connection doesn't use compression when sending and receiving messages.

## Send and receive messages

The `AcceptWebSocketAsync` method upgrades the TCP connection to a WebSocket connection and provides a [WebSocket](/en-us/dotnet/api/system.net.websockets.websocket) object. Use the `WebSocket` object to send and receive messages.

The code shown earlier that accepts the WebSocket request passes the `WebSocket` object to an `Echo` method. The code receives a message and immediately sends back the same message. Messages are sent and received in a loop until the client closes the connection:

```
private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}
```

When accepting the WebSocket connection before beginning the loop, the middleware pipeline ends. Upon closing the socket, the pipeline unwinds. That is, the request stops moving forward in the pipeline when the WebSocket is accepted. When the loop is finished and the socket is closed, the request proceeds back up the pipeline.

## Handle client disconnects

The server isn't automatically informed when the client disconnects due to loss of connectivity. The server receives a disconnect message only if the client sends it, which can't be done if the internet connection is lost. If you want to take some action when that happens, set a timeout after nothing is received from the client within a certain time window.

If the client isn't always sending messages and you don't want to time out just because the connection goes idle, have the client use a timer to send a ping message every X seconds. On the server, if a message hasn't arrived within 2\*X seconds after the previous one, terminate the connection and report that the client disconnected. Wait for twice the expected time interval to leave extra time for network delays that might hold up the ping message.

## WebSocket origin restriction

The protections provided by CORS don't apply to WebSockets. Browsers do **not**:

* Perform CORS pre-flight requests.
* Respect the restrictions specified in `Access-Control` headers when making WebSocket requests.

However, browsers do send the `Origin` header when issuing WebSocket requests. Applications should be configured to validate these headers to ensure that only WebSockets coming from the expected origins are allowed.

If you're hosting your server on "https\://server.com" and hosting your client on "https\://client.com", add "https\://client.com" to the [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) list for WebSockets to verify.

```
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);
```

Note

The `Origin` header is controlled by the client and, like the `Referer` header, can be faked. Do **not** use these headers as an authentication mechanism.

## IIS/IIS Express support

Windows Server 2012 or later and Windows 8 or later with IIS/IIS Express 8 or later has support for the WebSocket protocol.

Note

WebSockets are always enabled when using IIS Express.

### Enabling WebSockets on IIS

To enable support for the WebSocket protocol on Windows Server 2012 or later:

Note

These steps are not required when using IIS Express

1. Use the **Add Roles and Features** wizard from the **Manage** menu or the link in **Server Manager**.
2. Select **Role-based or Feature-based Installation**. Select **Next**.
3. Select the appropriate server (the local server is selected by default). Select **Next**.
4. Expand **Web Server (IIS)** in the **Roles** tree, expand **Web Server**, and then expand **Application Development**.
5. Select **WebSocket Protocol**. Select **Next**.
6. If additional features aren't needed, select **Next**.
7. Select **Install**.
8. When the installation completes, select **Close** to exit the wizard.

To enable support for the WebSocket protocol on Windows 8 or later:

Note

These steps are not required when using IIS Express

1. Navigate to **Control Panel** > **Programs** > **Programs and Features** > **Turn Windows features on or off** (left side of the screen).
2. Open the following nodes: **Internet Information Services** > **World Wide Web Services** > **Application Development Features**.
3. Select the **WebSocket Protocol** feature. Select **OK**.

### Disable WebSocket when using socket.io on Node.js

If using the WebSocket support in [socket.io](https://socket.io/) on [Node.js](https://nodejs.org/), disable the default IIS WebSocket module using the `webSocket` element in *web.config* or *applicationHost.config*. If this step isn't performed, the IIS WebSocket module attempts to handle the WebSocket communication rather than Node.js and the app.

```
<system.webServer>
  <webSocket enabled="false" />
</system.webServer>
```

## Sample app

The [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) that accompanies this article is an echo app. It has a webpage that makes WebSocket connections, and the server resends any messages it receives back to the client. The sample app isn't configured to run from Visual Studio with IIS Express, so run the app in a command shell with [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) and navigate in a browser to `http://localhost:<port>`. The webpage shows the connection status:

Select **Connect** to send a WebSocket request to the URL shown. Enter a test message and select **Send**. When done, select **Close Socket**. The **Communication Log** section reports each open, send, and close action as it happens.

This article explains how to get started with WebSockets in ASP.NET Core. [WebSocket](https://wikipedia.org/wiki/WebSocket) ([RFC 6455](https://tools.ietf.org/html/rfc6455)) is a protocol that enables two-way persistent communication channels over TCP connections. It's used in apps that benefit from fast, real-time communication, such as chat, dashboard, and game apps.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample)). [How to run](#sample-app).

## SignalR

[ASP.NET Core SignalR](../signalr/introduction?view=aspnetcore-10.0) is a library that simplifies adding real-time web functionality to apps. It uses WebSockets whenever possible.

For most applications, we recommend SignalR over raw WebSockets. SignalR provides transport fallback for environments where WebSockets isn't available. It also provides a basic remote procedure call app model. And in most scenarios, SignalR has no significant performance disadvantage compared to using raw WebSockets.

For some apps, [gRPC on .NET](../grpc/?view=aspnetcore-10.0) provides an alternative to WebSockets.

## Prerequisites

* Any OS that supports ASP.NET Core:

  * Windows 7 / Windows Server 2008 or later
  * Linux
  * macOS

* If the app runs on Windows with IIS:

  * Windows 8 / Windows Server 2012 or later
  * IIS 8 / IIS 8 Express
  * WebSockets must be enabled. See the [IIS/IIS Express support](#iisiis-express-support) section.

* If the app runs on [HTTP.sys](servers/httpsys?view=aspnetcore-10.0):
  * Windows 8 / Windows Server 2012 or later

* For supported browsers, see [Can I use](https://caniuse.com/?search=websockets).

## Configure the middleware

Add the WebSockets middleware in the `Configure` method of the `Startup` class:

```
app.UseWebSockets();
```

Note

If you would like to accept WebSocket requests in a controller, the call to `app.UseWebSockets` must occur before `app.UseEndpoints`.

The following settings can be configured:

* [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.keepaliveinterval) - How frequently to send "ping" frames to the client to ensure proxies keep the connection open. The default is two minutes.
* [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) - A list of allowed Origin header values for WebSocket requests. By default, all origins are allowed. See "WebSocket origin restriction" below for details.

```
var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};

app.UseWebSockets(webSocketOptions);
```

## Accept WebSocket requests

Somewhere later in the request life cycle (later in the `Configure` method or in an action method, for example) check if it's a WebSocket request and accept the WebSocket request.

The following example is from later in the `Configure` method:

```
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
            {
                await Echo(context, webSocket);
            }
        }
        else
        {
            context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
        }
    }
    else
    {
        await next();
    }

});
```

A WebSocket request could come in on any URL, but this sample code only accepts requests for `/ws`.

A similar approach can be taken in a controller method:

```
public class WebSocketController : ControllerBase
{
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
```

When using a WebSocket, you **must** keep the middleware pipeline running for the duration of the connection. If you attempt to send or receive a WebSocket message after the middleware pipeline ends, you may get an exception like the following:

```
System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.
```

If you're using a background service to write data to a WebSocket, make sure you keep the middleware pipeline running. Do this by using a [TaskCompletionSource\<TResult>](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1). Pass the `TaskCompletionSource` to your background service and have it call [TrySetResult](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.trysetresult) when you finish with the WebSocket. Then `await` the [Task](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.task#system-threading-tasks-taskcompletionsource-1-task) property during the request, as shown in the following example:

```
app.Use(async (context, next) =>
{
    using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
    {
        var socketFinishedTcs = new TaskCompletionSource<object>();

        BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

        await socketFinishedTcs.Task;
    }
});
```

The WebSocket closed exception can also happen when returning too soon from an action method. When accepting a socket in an action method, wait for the code that uses the socket to complete before returning from the action method.

Never use `Task.Wait`, `Task.Result`, or similar blocking calls to wait for the socket to complete, as that can cause serious threading issues. Always use `await`.

## Send and receive messages

The `AcceptWebSocketAsync` method upgrades the TCP connection to a WebSocket connection and provides a [WebSocket](/en-us/dotnet/api/system.net.websockets.websocket) object. Use the `WebSocket` object to send and receive messages.

The code shown earlier that accepts the WebSocket request passes the `WebSocket` object to an `Echo` method. The code receives a message and immediately sends back the same message. Messages are sent and received in a loop until the client closes the connection:

```
private async Task Echo(HttpContext context, WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    while (!result.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

        result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    }
    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
```

When accepting the WebSocket connection before beginning the loop, the middleware pipeline ends. Upon closing the socket, the pipeline unwinds. That is, the request stops moving forward in the pipeline when the WebSocket is accepted. When the loop is finished and the socket is closed, the request proceeds back up the pipeline.

## Handle client disconnects

The server isn't automatically informed when the client disconnects due to loss of connectivity. The server receives a disconnect message only if the client sends it, which can't be done if the internet connection is lost. If you want to take some action when that happens, set a timeout after nothing is received from the client within a certain time window.

If the client isn't always sending messages and you don't want to time out just because the connection goes idle, have the client use a timer to send a ping message every X seconds. On the server, if a message hasn't arrived within 2\*X seconds after the previous one, terminate the connection and report that the client disconnected. Wait for twice the expected time interval to leave extra time for network delays that might hold up the ping message.

Note

The internal [`ManagedWebSocket`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.WebSockets/src/System/Net/WebSockets/ManagedWebSocket.cs) handles the Ping/Pong frames implicitly to keep the connection alive if the `KeepAliveInterval` option is greater than zero, which defaults to 30 seconds (`TimeSpan.FromSeconds(30)`).

## WebSocket origin restriction

The protections provided by CORS don't apply to WebSockets. Browsers do **not**:

* Perform CORS pre-flight requests.
* Respect the restrictions specified in `Access-Control` headers when making WebSocket requests.

However, browsers do send the `Origin` header when issuing WebSocket requests. Applications should be configured to validate these headers to ensure that only WebSockets coming from the expected origins are allowed.

If you're hosting your server on "https\://server.com" and hosting your client on "https\://client.com", add "https\://client.com" to the [AllowedOrigins](/en-us/dotnet/api/microsoft.aspnetcore.builder.websocketoptions.allowedorigins) list for WebSockets to verify.

```
var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};
webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);
```

Note

The `Origin` header is controlled by the client and, like the `Referer` header, can be faked. Do **not** use these headers as an authentication mechanism.

## IIS/IIS Express support

Windows Server 2012 or later and Windows 8 or later with IIS/IIS Express 8 or later has support for the WebSocket protocol.

Note

WebSockets are always enabled when using IIS Express.

### Enabling WebSockets on IIS

To enable support for the WebSocket protocol on Windows Server 2012 or later:

Note

These steps are not required when using IIS Express

1. Use the **Add Roles and Features** wizard from the **Manage** menu or the link in **Server Manager**.
2. Select **Role-based or Feature-based Installation**. Select **Next**.
3. Select the appropriate server (the local server is selected by default). Select **Next**.
4. Expand **Web Server (IIS)** in the **Roles** tree, expand **Web Server**, and then expand **Application Development**.
5. Select **WebSocket Protocol**. Select **Next**.
6. If additional features aren't needed, select **Next**.
7. Select **Install**.
8. When the installation completes, select **Close** to exit the wizard.

To enable support for the WebSocket protocol on Windows 8 or later:

Note

These steps are not required when using IIS Express

1. Navigate to **Control Panel** > **Programs** > **Programs and Features** > **Turn Windows features on or off** (left side of the screen).
2. Open the following nodes: **Internet Information Services** > **World Wide Web Services** > **Application Development Features**.
3. Select the **WebSocket Protocol** feature. Select **OK**.

### Disable WebSocket when using socket.io on Node.js

If using the WebSocket support in [socket.io](https://socket.io/) on [Node.js](https://nodejs.org/), disable the default IIS WebSocket module using the `webSocket` element in *web.config* or *applicationHost.config*. If this step isn't performed, the IIS WebSocket module attempts to handle the WebSocket communication rather than Node.js and the app.

```
<system.webServer>
  <webSocket enabled="false" />
</system.webServer>
```

## Sample app

The [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/websockets/samples) that accompanies this article is an echo app. It has a webpage that makes WebSocket connections, and the server resends any messages it receives back to the client. The sample app isn't configured to run from Visual Studio with IIS Express, so run the app in a command shell with [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) and navigate in a browser to `http://localhost:5000`. The webpage shows the connection status:

Select **Connect** to send a WebSocket request to the URL shown. Enter a test message and select **Send**. When done, select **Close Socket**. The **Communication Log** section reports each open, send, and close action as it happens.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/request-decompression
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/middleware/request-decompression.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Request decompression in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [David Acker](https://github.com/david-acker)

Request decompression middleware:

* Enables API endpoints to accept requests with compressed content.
* Uses the [`Content-Encoding`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Encoding) HTTP header to automatically identify and decompress requests which contain compressed content.
* Eliminates the need to write code to handle compressed requests.

When the `Content-Encoding` header value on a request matches one of the available decompression providers, the middleware:

* Uses the matching provider to wrap the [HttpRequest.Body](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.body#microsoft-aspnetcore-http-httprequest-body) in an appropriate decompression stream.
* Removes the `Content-Encoding` header, indicating that the request body is no longer compressed.

Requests that don't include a `Content-Encoding` header are ignored by the request decompression middleware.

Decompression:

* Occurs when the body of the request is read. That is, decompression occurs at the endpoint on model binding. The request body isn't decompressed eagerly.

- When attempting to read the decompressed request body with invalid compressed data for the specified `Content-Encoding`, an exception is thrown. Brotli can throw [System.InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception): Decoder ran into invalid data. Deflate and GZip can throw [System.IO.InvalidDataException](/en-us/dotnet/api/system.io.invaliddataexception): The archive entry was compressed using an unsupported compression method.

* When attempting to read the decompressed request body with invalid compressed data for the specified `Content-Encoding`, an exception is thrown. Brotli can throw [System.InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception): Decoder ran into invalid data. Deflate, GZip, and Zstandard can throw [System.IO.InvalidDataException](/en-us/dotnet/api/system.io.invaliddataexception): The archive entry was compressed using an unsupported compression method.

If the middleware encounters a request with compressed content but is unable to decompress it, the request is passed to the next delegate in the pipeline. For example, a request with an unsupported `Content-Encoding` header value or multiple `Content-Encoding` header values is passed to the next delegate in the pipeline.

## Configuration

The following code uses [AddRequestDecompression(IServiceCollection)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.requestdecompressionserviceextensions.addrequestdecompression#microsoft-extensions-dependencyinjection-requestdecompressionserviceextensions-addrequestdecompression\(microsoft-extensions-dependencyinjection-iservicecollection\)) and [UseRequestDecompression](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestdecompressionbuilderextensions.userequestdecompression) to enable request decompression for the [default](#default) `Content-Encoding` types:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRequestDecompression();

var app = builder.Build();

app.UseRequestDecompression();

app.MapPost("/", (HttpRequest request) => Results.Stream(request.Body));

app.Run();
```

[]()

## Default decompression providers

The `Content-Encoding` header values that the request decompression middleware supports by default are listed in the following table:

| [`Content-Encoding`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Encoding) header values | Description                                                           |
| -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `br`                                                                                                     | [Brotli compressed data format](https://tools.ietf.org/html/rfc7932)  |
| `deflate`                                                                                                | [DEFLATE compressed data format](https://tools.ietf.org/html/rfc1951) |
| `gzip`                                                                                                   | [Gzip file format](https://tools.ietf.org/html/rfc1952)               |

| [`Content-Encoding`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Encoding) header values | Description                                                             |
| -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `br`                                                                                                     | [Brotli compressed data format](https://tools.ietf.org/html/rfc7932)    |
| `deflate`                                                                                                | [DEFLATE compressed data format](https://tools.ietf.org/html/rfc1951)   |
| `gzip`                                                                                                   | [Gzip file format](https://tools.ietf.org/html/rfc1952)                 |
| `zstd`                                                                                                   | [Zstandard compressed data format](https://tools.ietf.org/html/rfc8878) |

## Custom decompression providers

Support for custom encodings can be added by creating custom decompression provider classes that implement [IDecompressionProvider](/en-us/dotnet/api/microsoft.aspnetcore.requestdecompression.idecompressionprovider):

```
public class CustomDecompressionProvider : IDecompressionProvider
{
    public Stream GetDecompressionStream(Stream stream)
    {
        // Perform custom decompression logic here
        return stream;
    }
}
```

Custom decompression providers are registered with [RequestDecompressionOptions](/en-us/dotnet/api/microsoft.aspnetcore.requestdecompression.requestdecompressionoptions) along with their corresponding `Content-Encoding` header values:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRequestDecompression(options =>
{
    options.DecompressionProviders.Add("custom", new CustomDecompressionProvider());
});

var app = builder.Build();

app.UseRequestDecompression();

app.MapPost("/", (HttpRequest request) => Results.Stream(request.Body));

app.Run();
```

## Request size limits

In order to protect against [zip bombs or decompression bombs](https://en.wikipedia.org/wiki/Zip_bomb):

* The maximum size of the decompressed request body is limited to the request body size limit enforced by the endpoint or server.
* If the number of bytes read from the decompressed request body stream exceeds the limit, an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) is thrown to prevent additional bytes from being read from the stream.

In order of precedence, the maximum request size for an endpoint is set by:

1. [IRequestSizeLimitMetadata.MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.irequestsizelimitmetadata.maxrequestbodysize#microsoft-aspnetcore-http-metadata-irequestsizelimitmetadata-maxrequestbodysize), such as [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) or [DisableRequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.disablerequestsizelimitattribute) for MVC endpoints.
2. The global server size limit [IHttpMaxRequestBodySizeFeature.MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature.maxrequestbodysize#microsoft-aspnetcore-http-features-ihttpmaxrequestbodysizefeature-maxrequestbodysize). `MaxRequestBodySize` can be overridden per request with [IHttpMaxRequestBodySizeFeature.MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature.maxrequestbodysize#microsoft-aspnetcore-http-features-ihttpmaxrequestbodysizefeature-maxrequestbodysize), but defaults to the limit configured for the web server implementation.

| Web server implementation                              | `MaxRequestBodySize` configuration                                                                                                                                                                                          |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [HTTP.sys](../servers/httpsys?view=aspnetcore-10.0)    | [HttpSysOptions.MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.maxrequestbodysize#microsoft-aspnetcore-server-httpsys-httpsysoptions-maxrequestbodysize)                          |
| [IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0) | [IISServerOptions.MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisserveroptions.maxrequestbodysize#microsoft-aspnetcore-builder-iisserveroptions-maxrequestbodysize)                                  |
| [Kestrel](../servers/kestrel?view=aspnetcore-10.0)     | [KestrelServerLimits.MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxrequestbodysize#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxrequestbodysize) |

Warning

Disabling the request body size limit poses a security risk in regards to uncontrolled resource consumption, particularly if the request body is being buffered. Ensure that safeguards are in place to mitigate the risk of [denial-of-service](https://www.cisa.gov/uscert/ncas/tips/ST04-015) (DoS) attacks.

## Related content

* [ASP.NET Core middleware](./?view=aspnetcore-10.0)
* [Mozilla Developer Network: Content-Encoding](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Encoding)
* [Brotli Compressed Data Format](https://www.rfc-editor.org/rfc/rfc7932)
* [DEFLATE Compressed Data Format Specification version 1.3](https://www.rfc-editor.org/rfc/rfc1951)
* [GZIP file format specification version 4.3](https://www.rfc-editor.org/rfc/rfc1952)
* [RFC 8878: Zstandard Compression for HTTP](https://www.rfc-editor.org/rfc/rfc8878)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/metrics/overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/metrics/overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core metrics

Metrics are numerical measurements reported over time. Use them to monitor the health of an app and generate alerts. For example, a web service might track how many:

* Requests it receives per second.
* Milliseconds it takes to respond.
* Responses it sends with an error.

Report these metrics to a monitoring system at regular intervals. Set up dashboards to view metrics and create alerts to notify people of problems. If the web service is intended to respond to requests within 400 ms and starts responding in 600 ms, the monitoring system can notify the operations staff that the app response is slower than normal.

The comprehensive list of all instruments together with their attributes is described in [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0).

## Use metrics

Using metrics involves the following:

* **Instrumentation:** Code in .NET libraries takes measurements and associates these measurements with a metric name. .NET and ASP.NET Core include many built-in metrics.
* **Collection and storage:** A .NET app configures named metrics to be transmitted from the app for external storage and analysis. Some tools might perform configuration outside the app by using configuration files or a UI tool.
* **Visualization:** A tool that can display the metrics in a human-readable format. For example, the [Aspire dashboard](https://aspire.dev/dashboard/overview/), [Grafana](https://grafana.com/), and [Prometheus](https://prometheus.io/).
* **Alerting:** A tool that provides notifications when a metric exceeds a threshold. For example, if the average response time for a web service exceeds 400 ms, an alert can be sent to the operations staff.
* **Analysis:** A tool that can analyze the metrics over time. This tool is often a web-based dashboard that can be customized to show the most important metrics for a specific app.

Instrumented code can record numeric measurements, but to create useful metrics for monitoring, you need to aggregate, transmit, and store the measurements. The process of aggregating, transmitting, and storing data is called collection. This tutorial shows several examples of collecting and displaying metrics:

* Populating metrics in [Grafana](https://grafana.com/) with [OpenTelemetry](https://opentelemetry.io/) and [Prometheus](https://prometheus.io/).
* Viewing metrics in real time with [`dotnet-counters`](/en-us/dotnet/core/diagnostics/dotnet-counters).

For another visualization option, see the [Aspire dashboard overview](https://aspire.dev/dashboard/overview/).

You can also associate measurements with key-value pairs called tags that allow you to categorize data for analysis. For more information, see [Multi-dimensional metrics](/en-us/dotnet/core/diagnostics/metrics-instrumentation#multi-dimensional-metrics).

## Create the starter app

Create a new ASP.NET Core app with the following command:

```
dotnet new web -o WebMetric
cd WebMetric
dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore --prerelease
dotnet add package OpenTelemetry.Extensions.Hosting
```

Replace the contents of `Program.cs` with the following code:

```
using OpenTelemetry.Metrics;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
    .WithMetrics(builder =>
    {
        builder.AddPrometheusExporter();

        builder.AddMeter("Microsoft.AspNetCore.Hosting",
                         "Microsoft.AspNetCore.Server.Kestrel");
        builder.AddView("http.server.request.duration",
            new ExplicitBucketHistogramConfiguration
            {
                Boundaries = new double[] { 0, 0.005, 0.01, 0.025, 0.05,
                       0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 }
            });
    });
var app = builder.Build();

app.MapPrometheusScrapingEndpoint();

app.MapGet("/", () => "Hello OpenTelemetry! ticks:"
                     + DateTime.Now.Ticks.ToString()[^3..]);

app.Run();
```

## View metrics with dotnet-counters

[dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters) is a command-line tool that can view live metrics for .NET apps on demand. It doesn't require setup, making it useful for ad-hoc investigations or verifying that metric instrumentation is working. It works with both [System.Diagnostics.Metrics](/en-us/dotnet/api/system.diagnostics.metrics) based APIs and [EventCounters](/en-us/dotnet/core/diagnostics/event-counters).

If the [dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters) tool isn't installed, run the following command:

```
dotnet tool update -g dotnet-counters
```

While the test app is running, launch [dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters). The following command shows an example of `dotnet-counters` monitoring all metrics from the [`Microsoft.AspNetCore.Hosting` meter](/en-us/dotnet/core/diagnostics/built-in-metrics-aspnetcore).

```
dotnet-counters monitor -n WebMetric --counters Microsoft.AspNetCore.Hosting
```

Output similar to the following is displayed:

```
Press p to pause, r to resume, q to quit.
    Status: Running

[Microsoft.AspNetCore.Hosting]
    http-server-current-requests
        host=localhost,method=GET,port=5045,scheme=http                    0
    http-server-request-duration (s)
        host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro           0.001
        host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro           0.001
        host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro           0.001
        host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro           0
        host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro           0
        host=localhost,method=GET,port=5045,protocol=HTTP/1.1,ro           0
```

For more information, see [dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters).

## Enrich the ASP.NET Core request metric

ASP.NET Core has many built-in metrics. The `http.server.request.duration` metric:

* Records the duration of HTTP requests on the server.
* Captures request information in tags, such as the matched route and response status code.

The `http.server.request.duration` metric supports tag enrichment by using [IHttpMetricsTagsFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmetricstagsfeature). Enrichment is when a library or app adds its own tags to a metric. This feature is useful if an app wants to add a custom categorization to dashboards or alerts built with metrics.

```
using Microsoft.AspNetCore.Http.Features;

var builder = WebApplication.CreateBuilder();
var app = builder.Build();

app.Use(async (context, next) =>
{
    var tagsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
    if (tagsFeature != null)
    {
        var source = context.Request.Query["utm_medium"].ToString() switch
        {
            "" => "none",
            "social" => "social",
            "email" => "email",
            "organic" => "organic",
            _ => "other"
        };
        tagsFeature.Tags.Add(new KeyValuePair<string, object?>("mkt_medium", source));
    }

    await next.Invoke();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding example:

* Adds middleware to enrich the ASP.NET Core request metric.

* Gets the [IHttpMetricsTagsFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmetricstagsfeature) from the `HttpContext`. The feature is present on the context only if someone is listening to the metric. Verify `IHttpMetricsTagsFeature` isn't `null` before using it.

* Adds a custom tag containing the request's marketing source to the [`http.server.request.duration`](/en-us/dotnet/core/diagnostics/built-in-metrics-aspnetcore) metric.

  * The tag has the name `mkt_medium` and a value based on the [utm\_medium](https://wikipedia.org/wiki/UTM_parameters) query string value. The `utm_medium` value is resolved to a known range of values.
  * The tag allows requests to be categorized by marketing medium type, which could be useful when analyzing web app traffic.

Note

Follow the [multi-dimensional metrics](/en-us/dotnet/core/diagnostics/metrics-instrumentation#multi-dimensional-metrics) best practices when enriching with custom tags. Tags that are too numerous or have an unbound range create many tag combinations, resulting in high dimensions. Collection tools have limits on supported dimensions for a counter and might filter results to prevent excessive memory use.

## Opt out of HTTP metrics on certain endpoints and requests

Opting out of recording metrics is beneficial for endpoints frequently called by automated systems, such as health checks. Recording metrics for these requests is generally unnecessary. Unwanted telemetry uses resources to collect and store, and can distort results displayed in a telemetry dashboard.

You can exclude HTTP requests to an endpoint from metrics by adding metadata, with either the [DisableHttpMetrics](/en-us/dotnet/api/microsoft.aspnetcore.http.disablehttpmetricsattribute) attribute or the [DisableHttpMetrics](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpmetricsendpointconventionbuilderextensions.disablehttpmetrics#microsoft-aspnetcore-builder-httpmetricsendpointconventionbuilderextensions-disablehttpmetrics-1\(-0\)) method:

* Add the [DisableHttpMetrics](/en-us/dotnet/api/microsoft.aspnetcore.http.disablehttpmetricsattribute) attribute to the Web API controller, SignalR hub, or gRPC service.
* Call [DisableHttpMetrics](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpmetricsendpointconventionbuilderextensions.disablehttpmetrics#microsoft-aspnetcore-builder-httpmetricsendpointconventionbuilderextensions-disablehttpmetrics-1\(-0\)) when mapping endpoints in app startup:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();

var app = builder.Build();
app.MapHealthChecks("/healthz").DisableHttpMetrics();
app.Run();
```

Alternatively, the [IHttpMetricsTagsFeature.MetricsDisabled](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmetricstagsfeature.metricsdisabled#microsoft-aspnetcore-http-features-ihttpmetricstagsfeature-metricsdisabled) property was added for:

* Advanced scenarios where a request doesn't map to an endpoint.
* Dynamically disabling metrics collection for specific HTTP requests.

```
// Middleware that conditionally opts-out HTTP requests.
app.Use(async (context, next) =>
{
    var metricsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
    if (metricsFeature != null &&
        context.Request.Headers.ContainsKey("x-disable-metrics"))
    {
        metricsFeature.MetricsDisabled = true;
    }

    await next(context);
});
```

## Create custom metrics

You create metrics by using APIs in the [System.Diagnostics.Metrics](/en-us/dotnet/api/system.diagnostics.metrics) namespace. For information, see [Create custom metrics](/en-us/dotnet/core/diagnostics/metrics-instrumentation#create-a-custom-metric).

### Creating metrics in ASP.NET Core apps with `IMeterFactory`

Create [Meter](/en-us/dotnet/api/system.diagnostics.metrics.meter) instances in ASP.NET Core apps with [IMeterFactory](/en-us/dotnet/api/system.diagnostics.metrics.imeterfactory).

ASP.NET Core registers [IMeterFactory](/en-us/dotnet/api/system.diagnostics.metrics.imeterfactory) in dependency injection (DI) by default. The meter factory integrates metrics with DI, making isolating and collecting metrics easy. `IMeterFactory` is especially useful for testing. It allows for multiple tests to run side-by-side and only collects metrics values that are recorded in a test.

To use `IMeterFactory` in an app, create a type that uses `IMeterFactory` to create the app's custom metrics:

```
public class ContosoMetrics
{
    private readonly Counter<int> _productSoldCounter;

    public ContosoMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("Contoso.Web");
        _productSoldCounter = meter.CreateCounter<int>("contoso.product.sold");
    }

    public void ProductSold(string productName, int quantity)
    {
        _productSoldCounter.Add(quantity,
            new KeyValuePair<string, object?>("contoso.product.name", productName));
    }
}
```

Register the metrics type with DI in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ContosoMetrics>();
```

Inject the metrics type and record values where needed. Because the metrics type is registered in DI it can be used with MVC controllers, Minimal APIs, or any other type that is created by DI:

```
app.MapPost("/complete-sale", (SaleModel model, ContosoMetrics metrics) =>
{
    // ... business logic such as saving the sale to a database ...

    metrics.ProductSold(model.ProductName, model.QuantitySold);
});
```

To monitor the "Contoso.Web" meter, use the following [dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters) command.

```
dotnet-counters monitor -n WebMetric --counters Contoso.Web
```

Output similar to the following is displayed:

```
Press p to pause, r to resume, q to quit.
    Status: Running

[Contoso.Web]
    contoso.product.sold (Count / 1 sec)
        contoso.product.name=Eggs            12    
        contoso.product.name=Milk            0    
```

## View metrics in Grafana with OpenTelemetry and Prometheus

### Overview

[OpenTelemetry](https://opentelemetry.io/):

* Is a vendor-neutral open-source project supported by the [Cloud Native Computing Foundation](https://www.cncf.io/).
* Standardizes generating and collecting telemetry for cloud-native software.
* Works with .NET using the .NET metric APIs.
* Is endorsed by [Azure Monitor](/en-us/azure/azure-monitor/app/opentelemetry-overview) and many APM vendors.

Starting in ASP.NET Core 11, the framework's built-in HTTP server metrics and traces comply with the required parts of the [OpenTelemetry HTTP server semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/). The HTTP server request activity emits these attributes by default, matching the built-in metrics. As a result, the [`OpenTelemetry.Instrumentation.AspNetCore`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNetCore) NuGet package is optional for collecting HTTP server metrics and traces. The sample in this article uses only the built-in meters (`Microsoft.AspNetCore.Hosting` and `Microsoft.AspNetCore.Server.Kestrel`) and doesn't reference the instrumentation package. For the list of built-in instruments and their attributes, see [ASP.NET Core built-in HTTP metrics](http?view=aspnetcore-10.0).

Although the package is optional, it isn't a drop-in equivalent of the built-in instrumentation. The built-in instrumentation covers only the *required* parts of the semantic conventions. Consider the following differences before you remove the package:

* Some *recommended* HTTP server attributes aren't emitted by the built-in instrumentation, such as certain client and network attributes (for example, `client.address`). Full support for the *conditionally required* `url.query` attribute, including redaction, is also still in progress. If you rely on these attributes, keep the package. For more information, see [dotnet/aspnetcore#65873](https://github.com/dotnet/aspnetcore/issues/65873).
* The package is also a convenience for enabling telemetry beyond the HTTP server, including Blazor and SignalR. For tracing, it registers extra activity sources for SignalR (`Microsoft.AspNetCore.SignalR.Server` on .NET 9 and later) and Blazor (`Microsoft.AspNetCore.Components` and `Microsoft.AspNetCore.Components.Server.Circuits` on .NET 10 and later). For metrics, it enables the related built-in meters (such as `Microsoft.AspNetCore.Components`). Without the package, register the sources with `AddSource` and the meters with `AddMeter` yourself to collect the same telemetry.

When you add telemetry to an app that only needs HTTP server metrics and traces, you can rely on the built-in instrumentation and omit the package. When you upgrade an existing app from .NET 10 to .NET 11 that already references the package, keep it if you depend on the attributes, sources, or meters described in the preceding list. Removing the package silently drops that telemetry.

Important

When you enable OpenTelemetry *tracing* (in addition to metrics) without the `OpenTelemetry.Instrumentation.AspNetCore` package, register the framework's HTTP server [ActivitySource](/en-us/dotnet/api/system.diagnostics.activitysource) so that the request activity is recorded. ASP.NET Core's HTTP server activity source is named `Microsoft.AspNetCore`, and the framework creates a request activity named `Microsoft.AspNetCore.Hosting.HttpRequestIn` for each request to propagate trace context. If the `Microsoft.AspNetCore` source isn't registered with the OpenTelemetry SDK, the request activity isn't recorded, and the default `ParentBased` sampler silently drops any custom child spans started during the request.

In your existing tracing pipeline, register the source explicitly:

```
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource("Microsoft.AspNetCore")
        .AddSource("MyApp"));
```

In the preceding example:

* `AddSource("Microsoft.AspNetCore")` registers ASP.NET Core's HTTP server activity source so the request activity is recorded.
* `AddSource("MyApp")` registers the app's own [ActivitySource](/en-us/dotnet/api/system.diagnostics.activitysource). Replace `MyApp` with the name your app uses.
* An exporter isn't shown. This example assumes an exporter is already configured in your tracing pipeline.

Alternatively, call `AddAspNetCoreInstrumentation()` from the [`OpenTelemetry.Instrumentation.AspNetCore`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNetCore) package, which registers the source for you.

To prevent ASP.NET Core from adding OpenTelemetry HTTP semantic-convention attributes to the built-in request activity, set the `Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData` [AppContext](/en-us/dotnet/api/system.appcontext) switch to `true` early in app startup:

```
AppContext.SetSwitch(
    "Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", true);
```

The switch only suppresses the OpenTelemetry attributes that ASP.NET Core adds to the activity. It doesn't disable activity creation, tracing propagation, or metrics collection.

This tutorial shows one of the integrations available for OpenTelemetry metrics using the OSS [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/) projects. The metrics data flow:

1. The ASP.NET Core metric APIs record measurements from the example app.

2. The OpenTelemetry .NET library running in the app aggregates the measurements.

3. The Prometheus exporter library makes the aggregated data available via an HTTP metrics endpoint. 'Exporter' is what OpenTelemetry calls the libraries that transmit telemetry to vendor-specific backends.

4. A Prometheus server:

   * Polls the metrics endpoint.
   * Reads the data.
   * Stores the data in a database for long-term persistence. Prometheus refers to reading and storing data as *scraping* an endpoint.
   * Can run on a different machine.

5. The Grafana server:

   * Queries the data stored in Prometheus and displays it on a web-based monitoring dashboard.
   * Can run on a different machine.

### View metrics from sample app

Go to the sample app. The browser shows `Hello OpenTelemetry! ticks:<3digits>` where `3digits` are the last three digits of the current [DateTime.Ticks](/en-us/dotnet/api/system.datetime.ticks).

Append `/metrics` to the URL to view the metrics endpoint. The browser displays the metrics being collected:

### Set up and configure Prometheus

Follow the [Prometheus first steps](https://prometheus.io/docs/introduction/first_steps/) to set up a Prometheus server and confirm it's working.

Modify the *prometheus.yml* configuration file so that Prometheus scrapes the metrics endpoint that the example app exposes. Add the following highlighted text in the `scrape_configs` section:

```
# my global config
global:
  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: "prometheus"

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
      - targets: ["localhost:9090"]

  - job_name: 'MyASPNETApp'
    scrape_interval: 5s # Poll every 5 seconds for a more responsive demo.
    static_configs:
      - targets: ["localhost:5045"]  ## Enter the HTTP port number of the demo app.
```

In the preceding highlighted YAML, replace `5045` with the port number that the example app uses.

#### Start Prometheus

1. Reload the configuration or restart the Prometheus server.
2. Confirm that OpenTelemetryTest is in the UP state in the **Status** > **Targets** page of the Prometheus web portal.

Select the **Open metric explorer** icon to see available metrics:

Enter a counter category such as `http_` in the **Expression** input box to see the available metrics:

Alternatively, enter a counter category such as `kestrel` in the **Expression** input box to see the available metrics:

### Show metrics on a Grafana dashboard

* Follow the [installation instructions](https://prometheus.io/docs/visualization/grafana/#creating-a-prometheus-graph) to install Grafana and connect it to a Prometheus data source.

* Follow [Creating a Prometheus graph](https://prometheus.io/docs/visualization/grafana/#creating-a-prometheus-graph). Alternatively, pre-built dashboards for .NET metrics are available to download at [.NET team dashboards @ grafana.com](https://aka.ms/dotnet/grafana-dashboards). Downloaded dashboard JSON can be [imported into Grafana](https://grafana.com/docs/grafana/latest/dashboards/manage-dashboards/#import-a-dashboard).

## Test metrics in ASP.NET Core apps

You can test metrics in ASP.NET Core apps. One way to do this is to collect and assert metrics values in [ASP.NET Core integration tests](../test/integration-tests?view=aspnetcore-10.0) by using [MetricCollector\<T>](/en-us/dotnet/api/microsoft.extensions.diagnostics.metrics.testing.metriccollector-1).

```
public class BasicTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    public BasicTests(WebApplicationFactory<Program> factory) => _factory = factory;

    [Fact]
    public async Task Get_RequestCounterIncreased()
    {
        // Arrange
        var client = _factory.CreateClient();
        var meterFactory = _factory.Services.GetRequiredService<IMeterFactory>();
        var collector = new MetricCollector<double>(meterFactory,
            "Microsoft.AspNetCore.Hosting", "http.server.request.duration");

        // Act
        var response = await client.GetAsync("/");

        // Assert
        Assert.Contains("Hello OpenTelemetry!", await response.Content.ReadAsStringAsync());

        await collector.WaitForMeasurementsAsync(minCount: 1).WaitAsync(TimeSpan.FromSeconds(5));
        Assert.Collection(collector.GetMeasurementSnapshot(),
            measurement =>
            {
                Assert.Equal("http", measurement.Tags["url.scheme"]);
                Assert.Equal("GET", measurement.Tags["http.request.method"]);
                Assert.Equal("/", measurement.Tags["http.route"]);
            });
    }
}
```

The preceding test:

* Bootstraps a web app in memory with [WebApplicationFactory\<TEntryPoint>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1). `Program` in the factory's generic argument specifies the web app.

* Collects metrics values with [MetricCollector\<T>](/en-us/dotnet/api/microsoft.extensions.diagnostics.metrics.testing.metriccollector-1)

  * Requires a package reference to `Microsoft.Extensions.Diagnostics.Testing`.
  * The `MetricCollector<T>` is created using the web app's [IMeterFactory](/en-us/dotnet/api/system.diagnostics.metrics.imeterfactory). This allows the collector to only report metrics values recorded by test.
  * Includes the meter name, `Microsoft.AspNetCore.Hosting`, and counter name, `http.server.request.duration` to collect.

* Makes an HTTP request to the web app.

* Asserts the test by using results from the metrics collector.

## ASP.NET Core Identity metrics

ASP.NET Core Identity observability helps you monitor user management activities and authentication processes.

The metrics are in the `Microsoft.AspNetCore.Identity` meter and are described in the following sections.

### User management metrics

* `aspnetcore.identity.user.create.duration` measures the duration of user creation operations.
* `aspnetcore.identity.user.update.duration` measures the duration of user update operations.
* `aspnetcore.identity.user.delete.duration` measures the duration of user deletion operations.
* `aspnetcore.identity.user.check_password_attempts` counts password verification attempts.
* `aspnetcore.identity.user.generated_tokens` counts tokens generated for users, such as password reset tokens.
* `aspnetcore.identity.user.verify_token_attempts` counts token verification attempts.

### Authentication metrics

* `aspnetcore.identity.sign_in.authenticate.duration` measures the duration of authentication operations.
* `aspnetcore.identity.sign_in.check_password_attempts` counts password check attempts during sign-in.
* `aspnetcore.identity.sign_in.sign_ins` counts successful sign-ins.
* `aspnetcore.identity.sign_in.sign_outs` counts sign-outs.
* `aspnetcore.identity.sign_in.two_factor_clients_remembered` counts remembered two-factor clients.
* `aspnetcore.identity.sign_in.two_factor_clients_forgotten` counts forgotten two-factor clients.

Use these metrics to:

* Monitor user registration and management.
* Track authentication patterns and potential security issues.
* Measure performance of Identity operations.
* Observe two-factor authentication usage.

### Viewing Identity metrics

Use `dotnet-counters` to view these metrics and monitor them in real-time. Or, export them to Prometheus and visualize them in Grafana by using the techniques described earlier in this article.

For example, to monitor all Identity metrics with `dotnet-counters`:

```
dotnet-counters monitor -n YourAppName --counters Microsoft.AspNetCore.Identity
```

## ASP.NET Core meters and counters

For a list of ASP.NET Core meters and counters, see [ASP.NET Core metrics](/en-us/dotnet/core/diagnostics/built-in-metrics-aspnetcore). In ASP.NET Core 11 and later, the built-in HTTP server meters (for example, `Microsoft.AspNetCore.Hosting` and `Microsoft.AspNetCore.Server.Kestrel`) emit data that conforms to the required parts of the [OpenTelemetry HTTP server semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/). You can consume these meters with the OpenTelemetry SDK without the `OpenTelemetry.Instrumentation.AspNetCore` package.

## Additional resources

* [Aspire dashboard overview](https://aspire.dev/dashboard/overview/)
* [Explore Aspire telemetry](https://aspire.dev/fundamentals/telemetry/)
* [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-09

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0014
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0014.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0014: Suggest using top level route registrations

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0014      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

Routes can be registered directly at the top-level of a Minimal API application.

## Rule description

Routes can be registered directly at the top-level of a Minimal API application and don't need to be nested within a `UseEndpoints` call.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", () => "Hello World!");
});

app.Run();
```

## How to fix violations

To fix a violation of this rule, register the endpoints directly on the `WebApplication`.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

## When to suppress warnings

Warnings from this rule can be suppressed if the target `UseEndpoints` invocation is invoked without any mappings as a strategy to organize middleware ordering.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

#pragma warning disable ASP0014
app.UseEndpoints(e => {});
#pragma warning restore ASP0014

app.Run();
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0034
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0034.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0034: Endpoint parameter type is inaccessible from generated code

|              | Value   |
| ------------ | ------- |
| **Rule ID**  | ASP0034 |
| **Category** | Usage   |
| **Severity** | Warning |

## Cause

A Minimal API endpoint has a parameter whose type isn't accessible to the validation source generator. The type, or one of its containing types, is private or file-local.

## Rule description

When [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation) enables validation, the validation source generator discovers model types used by Minimal API endpoints. The generated code can't access private or file-local types, so validation is silently skipped for an inaccessible endpoint parameter type.

The following code produces this diagnostic:

```
app.MapPost("/orders", (Order order) => Results.Ok(order));

file class Order
{
    [Required]
    public string? CustomerName { get; set; }
}
```

## How to fix violations

Make the parameter type and each of its containing types public or internal and don't declare them as file-local:

```
app.MapPost("/orders", (Order order) => Results.Ok(order));

internal class Order
{
    [Required]
    public string? CustomerName { get; set; }
}
```

If the endpoint shouldn't validate the parameter, disable validation for the endpoint or apply [SkipValidationAttribute](/en-us/dotnet/api/microsoft.extensions.validation.skipvalidationattribute) to the parameter.

For more information, see [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-12.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/31-to-50
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/31-to-50.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core 3.1 to .NET 5

This article explains how to update an existing ASP.NET Core 3.1 project to ASP.NET Core in .NET 5. For instructions on how to migrate from ASP.NET Core 3.1 to ASP.NET Core in .NET 6, see [Migrate from ASP.NET Core 3.1 to .NET 6](31-to-60?view=aspnetcore-10.0).

## Update .NET SDK version in global.json

If you rely upon a [global.json](/en-us/dotnet/core/tools/global-json) file to target a specific .NET SDK version, update the `version` property to the .NET 5 SDK version that's installed. For example:

```
{
  "sdk": {
-    "version": "3.1.200"
+    "version": "5.0.100"
  }
}
```

## Update the target framework

If updating a Blazor WebAssembly project, skip to the [Update Blazor WebAssembly projects](#update-blazor-webassembly-projects) section. For any other ASP.NET Core project type, update the project file's [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks) to `net5.0`:

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

  <PropertyGroup>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>
```

## Delete `bin` and `obj` folders

You may need to delete the `bin` and `obj` folders. Run `dotnet nuget locals --clear all` to clear the NuGet package cache.

## Changes to Blazor app routing logic in 5.0.1 and further .NET 5.x releases up to .NET 6

The computation of route precedence changed in the 5.0.1 patch release. This might affect you if you've defined catch-all routes or routes with optional parameters.

### Old behavior

With the prior behavior 5.0.0 or earlier, routes with lower precedence, such as `{*slug}`, are matched before routes with higher precedence, such as `/customer/{id}`.

### New behavior

The new behavior in 5.0.1 or later more closely matches the routing behavior defined in ASP.NET Core apps, where the framework computes and establishes the route precedence for each segment first and only uses the length of the route to break ties as a secondary criteria.

### Reason for change

The original behavior is considered a bug in the implementation because our goal is for the Blazor routing system to behave in the same way as the ASP.NET Core routing system for the subset of features supported by Blazor routing.

### Recommended action

Add the `PreferExactMatches` attribute to the `Router` component in the `App.razor` file to opt into the correct behavior:

```
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
```

When `PreferExactMatches` is set to `@true`, route matching prefers exact matches over wildcards.

Important

All apps should explicitly set `PreferExactMatches` to `@true`.

The ability to set `PreferExactMatches` to `@false` or leave it unset *is only provided for backward compatibility*.

When .NET 6 is released, the router will always prefer exact matches, and the `PreferExactMatches` option won't be available.

## Update Blazor WebAssembly and Blazor Server projects

*The guidance in this section applies to both Blazor hosting models. Sections following this section provide additional guidance specific to hosting models and app types. Apply the guidance from all relevant sections to your app.*

1. In `wwwroot/index.html` of a Blazor WebAssembly app or the `Pages/_Host.cshtml` of a Blazor Server app, add a `<link>` element to the `<head>` element for styles. In the following `<link>` element `href` attribute values, the placeholder `{ASSEMBLY NAME}` is the app's assembly name.

   ```
   +<link href="{ASSEMBLY NAME}.styles.css" rel="stylesheet" />
   ```

   Standalone Blazor WebAssembly or Blazor Server example:

   ```
   +<link href="BlazorSample.styles.css" rel="stylesheet" />
   ```

   *`Client`* project of a hosted Blazor WebAssembly solution example:

   ```
   +<link href="BlazorSample.Client.styles.css" rel="stylesheet" />
   ```

2. Include a new namespace in the app's imports file (`_Imports.razor`) for [component virtualization](../blazor/components/virtualization?view=aspnetcore-10.0), [Microsoft.AspNetCore.Components.Web.Virtualization](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization). The following imports files show the default namespaces in apps generated from the Blazor project templates. The placeholder `{ASSEMBLY NAME}` is the app's assembly name.

   Blazor WebAssembly (`_Imports.razor`):

   ```
   @using System.Net.Http
   @using System.Net.Http.Json
   @using Microsoft.AspNetCore.Components.Forms
   @using Microsoft.AspNetCore.Components.Routing
   @using Microsoft.AspNetCore.Components.Web
   @using Microsoft.AspNetCore.Components.Web.Virtualization
   @using Microsoft.AspNetCore.Components.WebAssembly.Http
   @using Microsoft.JSInterop
   @using {ASSEMBLY NAME}
   @using {ASSEMBLY NAME}.Shared
   ```

   Blazor Server (`_Imports.razor`):

   ```
   @using System.Net.Http
   @using Microsoft.AspNetCore.Authorization
   @using Microsoft.AspNetCore.Components.Authorization
   @using Microsoft.AspNetCore.Components.Forms
   @using Microsoft.AspNetCore.Components.Routing
   @using Microsoft.AspNetCore.Components.Web
   @using Microsoft.AspNetCore.Components.Web.Virtualization
   @using Microsoft.JSInterop
   @using {ASSEMBLY NAME}
   @using {ASSEMBLY NAME}.Shared
   ```

3. In the `MainLayout` component (`Shared/MainLayout.razor`), surround the component's HTML markup with a `<div>` element that has a `class` attribute set to `page`:

   ```
   <div class="page">

       ...

   </div>
   ```

4. Add the following files to the `Shared` folder:

   `MainLayout.razor.css`:

   ```
   .page {
       position: relative;
       display: flex;
       flex-direction: column;
   }

   .main {
       flex: 1;
   }

   .sidebar {
       background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
   }

   .top-row {
       background-color: #f7f7f7;
       border-bottom: 1px solid #d6d5d5;
       justify-content: flex-end;
       height: 3.5rem;
       display: flex;
       align-items: center;
   }

       .top-row ::deep a, .top-row .btn-link {
           white-space: nowrap;
           margin-left: 1.5rem;
       }

       .top-row a:first-child {
           overflow: hidden;
           text-overflow: ellipsis;
       }

   @media (max-width: 767.98px) {
       .top-row:not(.auth) {
           display: none;
       }

       .top-row.auth {
           justify-content: space-between;
       }

       .top-row a, .top-row .btn-link {
           margin-left: 0;
       }
   }

   @media (min-width: 768px) {
       .page {
           flex-direction: row;
       }

       .sidebar {
           width: 250px;
           height: 100vh;
           position: sticky;
           top: 0;
       }

       .top-row {
           position: sticky;
           top: 0;
           z-index: 1;
       }

       .main > div {
           padding-left: 2rem !important;
           padding-right: 1.5rem !important;
       }
   }
   ```

   `NavMenu.razor.css`:

   ```
   .navbar-toggler {
       background-color: rgba(255, 255, 255, 0.1);
   }

   .top-row {
       height: 3.5rem;
       background-color: rgba(0,0,0,0.4);
   }

   .navbar-brand {
       font-size: 1.1rem;
   }

   .oi {
       width: 2rem;
       font-size: 1.1rem;
       vertical-align: text-top;
       top: -2px;
   }

   .nav-item {
       font-size: 0.9rem;
       padding-bottom: 0.5rem;
   }

       .nav-item:first-of-type {
           padding-top: 1rem;
       }

       .nav-item:last-of-type {
           padding-bottom: 1rem;
       }

       .nav-item ::deep a {
           color: #d7d7d7;
           border-radius: 4px;
           height: 3rem;
           display: flex;
           align-items: center;
           line-height: 3rem;
       }

   .nav-item ::deep a.active {
       background-color: rgba(255,255,255,0.25);
       color: white;
   }

   .nav-item ::deep a:hover {
       background-color: rgba(255,255,255,0.1);
       color: white;
   }

   @media (min-width: 768px) {
       .navbar-toggler {
           display: none;
       }

       .collapse {
           /* Never collapse the sidebar for wide screens */
           display: block;
       }
   }
   ```

5. The latest base `wwwroot/css/app.css` file of a Blazor WebAssembly app or `wwwroot/css/site.css` file of a Blazor Server app includes the following styles. Remove extra styles leaving the following styles and any that you've added to the app.

   The following stylesheet only includes base styles and does **not** include custom styles added by the developer:

   ```
   html, body {
       font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
   }

   a, .btn-link {
       color: #0366d6;
   }

   .btn-primary {
       color: #fff;
       background-color: #1b6ec2;
       border-color: #1861ac;
   }

   .content {
       padding-top: 1.1rem;
   }

   .valid.modified:not([type=checkbox]) {
       outline: 1px solid #26b050;
   }

   .invalid {
       outline: 1px solid red;
   }

   .validation-message {
       color: red;
   }

   #blazor-error-ui {
       background: lightyellow;
       bottom: 0;
       box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
       display: none;
       left: 0;
       padding: 0.6rem 1.25rem 0.7rem 1.25rem;
       position: fixed;
       width: 100%;
       z-index: 1000;
   }

   #blazor-error-ui .dismiss {
       cursor: pointer;
       position: absolute;
       right: 0.75rem;
       top: 0.5rem;
   }
   ```

   Note

   The preceding example doesn't show the `@import` directive for Open Iconic icons (`open-iconic-bootstrap.css`), provided by the Blazor project template. [Open Iconic](https://github.com/iconic/open-iconic) was abandoned by its maintainers.

## Update Blazor WebAssembly projects

Follow the guidance in the preceding [Update Blazor WebAssembly and Blazor Server projects](#update-blazor-webassembly-and-blazor-server-projects) section.

For a Blazor WebAssembly project, including the *`Client`* project of a hosted Blazor solution, apply the following changes to the project file:

1. Update the SDK from `Microsoft.NET.Sdk.Web` to `Microsoft.NET.Sdk.BlazorWebAssembly`:

   ```
   - <Project Sdk="Microsoft.NET.Sdk.Web">
   + <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
   ```

   Note

   This update only applies to standalone Blazor WebAssembly projects and the *`Client`* projects of hosted Blazor solutions.

2. Update the following properties:

   ```
   <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

     <PropertyGroup>
   -     <TargetFramework>netstandard2.1</TargetFramework>
   -     <RazorLangVersion>3.0</RazorLangVersion>
   +     <TargetFramework>net5.0</TargetFramework>
     </PropertyGroup>
   ```

3. Remove the package reference to [Microsoft.AspNetCore.Components.WebAssembly.Build](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Build):

   ```
   <ItemGroup>
   -    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.1" PrivateAssets="all" />
   ```

4. Update other packages to their latest versions. The latest versions can be found at [NuGet.org](https://www.nuget.org).

5. In `wwwroot/index.html`, change the element that loads the `App` component to a `<div>` element with an `id` set to `app`:

   ```
   -<app>Loading...</app>
   +<div id="app">Loading...</div>
   ```

6. In `Program.Main` (`Program.cs`), change the reference to the `<app>` element to a CSS selector by adding a hash `#` to it:

   ```
   -builder.RootComponents.Add<App>("app");
   +builder.RootComponents.Add<App>("#app");
   ```

7. In `Program.Main` (`Program.cs`), change a default transient `HttpClient` registration to scoped, if present:

   ```
   -builder.Services.AddTransient(sp => new HttpClient 
   -    { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
   +builder.Services.AddScoped(sp => new HttpClient 
   +    { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
   ```

8. In `Program.Main` (`Program.cs`) of the **`Client`** app of hosted Blazor solutions:

   * Optionally, substitute `builder.HostEnvironment.BaseAddress` for string client base addresses.
   * Change any named transient client factory registrations to scoped.

   ```
   -builder.Services.AddHttpClient("{APP NAMESPACE}.ServerAPI", 
   -    client => client.BaseAddress = new Uri("https://localhost:5001"))
   -    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
   -builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>()
   -    .CreateClient("{APP NAMESPACE}.ServerAPI"));
   +builder.Services.AddHttpClient("{APP NAMESPACE}.ServerAPI", 
   +    client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
   +    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
   +builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
   +    .CreateClient("{APP NAMESPACE}.ServerAPI"));
   ```

   In the preceding code, the `{APP NAMESPACE}` placeholder is the app's namespace.

### Standalone Blazor WebAssembly app with Microsoft Accounts

Follow the guidance in the preceding [Update Blazor WebAssembly and Blazor Server projects](#update-blazor-webassembly-and-blazor-server-projects) and [Update Blazor WebAssembly projects](#update-blazor-webassembly-projects) sections.

For a standalone Blazor WebAssembly app registered in the Azure portal to use Microsoft Entra ID (ME-ID) for Microsoft Accounts:

* The app requires the `openid` and `offline_access` scopes:

  ```
  options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
  options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
  ```

* In the Azure portal app registration **Authentication** blade:

  1. Remove the **Web** platform configuration.
  2. Add a **Single-page application** platform configuration with the app's redirect URI.
  3. Disable **Implicit grant** for **Access tokens** and **ID tokens**.

For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Accounts](../blazor/security/webassembly/standalone-with-microsoft-accounts?view=aspnetcore-10.0).

### Standalone Blazor WebAssembly app with Microsoft Entra ID (ME-ID)

Follow the guidance in the preceding [Update Blazor WebAssembly and Blazor Server projects](#update-blazor-webassembly-and-blazor-server-projects) and [Update Blazor WebAssembly projects](#update-blazor-webassembly-projects) sections.

For a standalone Blazor WebAssembly app registered in the Azure portal to use Microsoft Entra ID (ME-ID):

* The app requires the `https://graph.microsoft.com/User.Read` scope:

  ```
  options.ProviderOptions.DefaultAccessTokenScopes
      .Add("https://graph.microsoft.com/User.Read");
  ```

* In the Azure portal app registration **Authentication** blade:

  1. Remove the **Web** platform configuration.
  2. Add a **Single-page application** platform configuration with the app's redirect URI.
  3. Disable **Implicit grant** for **Access tokens** and **ID tokens**.

For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](../blazor/security/webassembly/standalone-with-microsoft-entra-id?view=aspnetcore-10.0).

### Standalone Blazor WebAssembly app with Azure Active Directory (AAD) B2C

Follow the guidance in the preceding [Update Blazor WebAssembly and Blazor Server projects](#update-blazor-webassembly-and-blazor-server-projects) and [Update Blazor WebAssembly projects](#update-blazor-webassembly-projects) sections.

For a standalone Blazor WebAssembly app registered in the Azure portal to use Azure Active Directory (AAD) B2C:

* The app requires the `openid` and `offline_access` scopes:

  ```
  options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
  options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
  ```

* In the Azure portal app registration **Authentication** blade:

  1. Remove the **Web** platform configuration.
  2. Add a **Single-page application** platform configuration with the app's redirect URI.
  3. Disable **Implicit grant** for **Access tokens** and **ID tokens**.

For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Azure Active Directory B2C](../blazor/security/webassembly/standalone-with-azure-active-directory-b2c?view=aspnetcore-10.0).

### Hosted Blazor WebAssembly app with Microsoft Entra ID (ME-ID) or AAD B2C

Follow the guidance in the preceding [Update Blazor WebAssembly and Blazor Server projects](#update-blazor-webassembly-and-blazor-server-projects) and [Update Blazor WebAssembly projects](#update-blazor-webassembly-projects) sections.

The *`Client`* app registration of a hosted Blazor solution that uses AAD or AAD B2C for user authentication should use a **Single-page application** Azure Apps platform configuration.

In the Azure portal *`Client`* app registration **Authentication** blade:

1. Remove the **Web** platform configuration.
2. Add a **Single-page application** platform configuration with the app's redirect URI.
3. Disable **Implicit grant** for **Access tokens** and **ID tokens**.

For more information, see:

* [Secure a hosted ASP.NET Core Blazor WebAssembly app with Microsoft Entra ID](../blazor/security/webassembly/hosted-with-microsoft-entra-id?view=aspnetcore-10.0)
* [Secure a hosted ASP.NET Core Blazor WebAssembly app with Azure Active Directory B2C](../blazor/security/webassembly/hosted-with-azure-active-directory-b2c?view=aspnetcore-10.0)

### Update the Server project of a hosted Blazor solution

Follow the guidance in the preceding sections:

* [Update Blazor WebAssembly and Blazor Server projects](#update-blazor-webassembly-and-blazor-server-projects)

* [Update Blazor WebAssembly projects](#update-blazor-webassembly-projects)

* The section that applies to the app's provider with Azure Active Directory:

  * [Standalone Blazor WebAssembly app with Microsoft Accounts](#standalone-blazor-webassembly-app-with-microsoft-accounts)
  * [Standalone Blazor WebAssembly app with Microsoft Entra ID (ME-ID)](#standalone-blazor-webassembly-app-with-microsoft-entra-id-me-id)
  * [Standalone Blazor WebAssembly app with Azure Active Directory (AAD) B2C](#standalone-blazor-webassembly-app-with-azure-active-directory-aad-b2c)

Update the *`Server`* project of a hosted Blazor solution as an ASP.NET Core app following the general guidance in this article.

Additionally, *`Server`* projects that authenticate users to client Blazor WebAssembly apps with Microsoft Entra ID (ME-ID) or B2C should adopt new Microsoft Identity v2.0 packages:

For AAD:

```
-<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureAD.UI" Version="..." />
+<PackageReference Include="Microsoft.Identity.Web" Version="{VERSION}" />
+<PackageReference Include="Microsoft.Identity.Web.UI" Version="{VERSION}" />
```

For AAD B2C:

```
-<PackageReference Include="Microsoft.AspNetCore.Authentication.AzureADB2C.UI" Version="..." />
+<PackageReference Include="Microsoft.Identity.Web" Version="{VERSION}" />
+<PackageReference Include="Microsoft.Identity.Web.UI" Version="{VERSION}" />
```

For the preceding package references, determine the package versions for the `{VERSION}` placeholders at NuGet.org:

* [`Microsoft.Identity.Web`](https://www.nuget.org/packages/Microsoft.Identity.Web)
* [`Microsoft.Identity.Web.UI`](https://www.nuget.org/packages/Microsoft.Identity.Web.UI)

Note

The SDK of the *`Server`* project in a hosted Blazor WebAssembly solution remains `Microsoft.NET.Sdk.Web`:

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

For more information, see:

* [Secure a hosted ASP.NET Core Blazor WebAssembly app with Microsoft Entra ID](../blazor/security/webassembly/hosted-with-microsoft-entra-id?view=aspnetcore-10.0)
* [Secure a hosted ASP.NET Core Blazor WebAssembly app with Azure Active Directory B2C](../blazor/security/webassembly/hosted-with-azure-active-directory-b2c?view=aspnetcore-10.0)

### Clean and rebuild the solution

After migrating the app or solution to .NET 5, clean and rebuild the app or solution. If package incompatibilities exist between new package references and cached packages:

1. Clear NuGet package caches by executing the following [`dotnet nuget locals`](/en-us/dotnet/core/tools/dotnet-nuget-locals) command in a command shell:

   ```
   dotnet nuget locals --clear all
   ```

2. Clean and rebuild the app or solution.

### Troubleshoot

Follow the *Troubleshoot* guidance at the end of the Blazor WebAssembly security topic that applies to your app:

Standalone Blazor WebAssembly apps:

* [General guidance for OIDC providers and the WebAssembly Authentication Library](../blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-10.0)
* [Microsoft Accounts](../blazor/security/webassembly/standalone-with-microsoft-accounts?view=aspnetcore-10.0)
* [Microsoft Entra ID (ME-ID)](../blazor/security/webassembly/standalone-with-microsoft-entra-id?view=aspnetcore-10.0)
* [Azure Active Directory (AAD) B2C](../blazor/security/webassembly/standalone-with-azure-active-directory-b2c?view=aspnetcore-10.0)

Hosted Blazor WebAssembly apps:

* [Microsoft Entra ID (ME-ID)](../blazor/security/webassembly/hosted-with-microsoft-entra-id?view=aspnetcore-10.0)
* [Azure Active Directory (AAD) B2C](../blazor/security/webassembly/hosted-with-azure-active-directory-b2c?view=aspnetcore-10.0)
* [Identity Server](../blazor/security/webassembly/hosted-with-identity-server?view=aspnetcore-10.0)

### Unauthorized client for Microsoft Entra ID (ME-ID)

After upgrading a Blazor WebAssembly app that uses AAD for authentication, you may receive the following error on the login callback to the app after the user signs in with AAD:

> info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

Login callback error from AAD:

* Error: `unauthorized_client`
* Description: `AADB2C90058: The provided application is not configured to allow public clients.`

To resolve the error:

1. In the Azure portal, access the [app's manifest](/en-us/azure/active-directory/develop/reference-app-manifest).
2. Set the [`allowPublicClient`](/en-us/azure/active-directory/develop/reference-app-manifest#allowpublicclient-attribute) attribute to `null` or `true`.

## Update a Blazor Progressive Web Application (PWA)

Add the following item to the PWA app's project file:

```
<ItemGroup>
  <ServiceWorker Include="wwwroot\service-worker.js" 
    PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
```

## Remove preview CSS isolation stylesheet link

If the project's `wwwroot/index.html` (Blazor WebAssembly) or `Pages/_Host.cshtml` (Blazor Server) contains a stylesheet `<link>` element for `scoped.styles.css` from an earlier 5.0 preview release, remove the `<link>` tag:

```
-<link href="_framework/scoped.styles.css/" rel="stylesheet" />
```

## Update Razor class libraries (RCLs)

Migrate Razor class libraries (RCLs) to take advantage of new APIs or features that are introduced as part of ASP.NET Core in .NET 5.

To update a RCL that targets components:

1. Update the following properties in the project file:

   ```
   <Project Sdk="Microsoft.NET.Sdk.Razor">

     <PropertyGroup>
   -     <TargetFramework>netstandard2.0</TargetFramework>
   -     <RazorLangVersion>3.0</RazorLangVersion>
   +     <TargetFramework>net5.0</TargetFramework>
     </PropertyGroup>
   ```

2. Update other packages to their latest versions. The latest versions can be found at [NuGet.org](https://www.nuget.org).

To update an RCL targeting MVC, update the following properties in the project file:

```
<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <TargetFramework>net5.0</TargetFramework>
    <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
  </PropertyGroup>
```

## Update package references

In the project file, update each [Microsoft.AspNetCore.\*](https://www.nuget.org/packages?q=Microsoft.AspNetCore.*), [Microsoft.EntityFrameworkCore.\*](https://www.nuget.org/packages?q=Microsoft.EntityFrameworkCore.*), [Microsoft.Extensions.\*](https://www.nuget.org/packages?q=Microsoft.Extensions.*), and [System.Net.Http.Json](https://www.nuget.org/packages/System.Net.Http.Json) package reference's `Version` attribute to 5.0.0 or later. For example:

```
<ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="3.1.6" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6">
-    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
-    <PackageReference Include="System.Net.Http.Json" Version="3.2.1" />
+    <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="5.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
+    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
+    <PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
</ItemGroup>
```

## Update Docker images

For apps using Docker, update your *Dockerfile* `FROM` statements and scripts. Use a base image that includes the .NET 5 runtime. Consider the following `docker pull` command difference between ASP.NET Core 3.1 and .NET 5:

```
- docker pull mcr.microsoft.com/dotnet/core/aspnet:3.1
+ docker pull mcr.microsoft.com/dotnet/aspnet:5.0
```

As part of the move to ".NET" as the product name, the Docker images moved from the `mcr.microsoft.com/dotnet/core` repositories to `mcr.microsoft.com/dotnet`. For more information, see [dotnet/dotnet-docker#1939](https://github.com/dotnet/dotnet-docker/issues/1939).

## Model binding changes in ASP.NET Core MVC and Razor Pages

### DateTime values are model bound as UTC times

In ASP.NET Core 3.1 or earlier, `DateTime` values were model-bound as local time, where the timezone was determined by the server. `DateTime` values bound from input formatting (JSON) and `DateTimeOffset` values were bound as UTC timezones.

In .NET 5 or later, model binding consistently binds `DateTime` values with the UTC timezone.

To retain the previous behavior, remove the `DateTimeModelBinderProvider` in `Startup.ConfigureServices`:

```
services.AddControllersWithViews(options => 
    options.ModelBinderProviders.RemoveType<DateTimeModelBinderProvider>());
```

### ComplexObjectModelBinderProvider \ ComplexObjectModelBinder replace ComplexTypeModelBinderProvider \ ComplexTypeModelBinder

To add support for model binding [C# 9 record types](/en-us/dotnet/csharp/whats-new/csharp-9#record-types), the [ComplexTypeModelBinderProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.binders.complextypemodelbinderprovider) is:

* Annotated as obsolete.
* No longer registered by default.

Apps that rely on the presence of the `ComplexTypeModelBinderProvider` in the `ModelBinderProviders` collection need to reference the new binder provider:

```
- var complexModelBinderProvider = options.ModelBinderProviders.OfType<ComplexTypeModelBinderProvider>();
+ var complexModelBinderProvider = options.ModelBinderProviders.OfType<ComplexObjectModelBinderProvider>();
```

## UseDatabaseErrorPage obsolete

The ASP.NET Core 3.1 templates that include an option for individual user accounts generate a call to [UseDatabaseErrorPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.databaseerrorpageextensions.usedatabaseerrorpage). `UseDatabaseErrorPage` is now obsolete and should be replaced with a combination of `AddDatabaseDeveloperPageExceptionFilter` and `UseMigrationsEndPoint`, as shown in the following code:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
+   services.AddDatabaseDeveloperPageExceptionFilter();
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
+       app.UseMigrationsEndPoint();
-       app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }
```

For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/24987).

## ASP.NET Core Module (ANCM)

If the [ASP.NET Core Module (ANCM)](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) wasn't a selected component when Visual Studio was installed or if a prior version of the ANCM was installed on the system, download the latest [.NET Core Hosting Bundle Installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer) and run the installer. For more information, see [Hosting Bundle](../host-and-deploy/iis/hosting-bundle?view=aspnetcore-10.0).

## Package reference changes affecting some NuGet packages

With the migration of some `Microsoft.Extensions.*` NuGet packages from the [dotnet/extensions](https://github.com/dotnet/extensions) repository to [dotnet/runtime](https://github.com/dotnet/runtime), as described in [Migrating dotnet/extensions content to dotnet/runtime and dotnet/aspnetcore (aspnet/Announcements #411)](https://github.com/aspnet/Announcements/issues/411), packaging changes are being applied to some of the migrated packages. These changes often result in namespace changes for .NET API.

To research APIs further for app namespace changes when migrating to .NET 5, use the [.NET API browser](/en-us/dotnet/api/).

## Migrate Microsoft.Identity.Web

The following wiki pages explain how to migrate Microsoft.Identity.Web from ASP.NET Core 3.1 to .NET 5:

* [Microsoft.Identity.Web in web apps](https://github.com/AzureAD/microsoft-identity-web/wiki/web-apps)
* [Microsoft.Identity.Web in web APIs](https://github.com/AzureAD/microsoft-identity-web/wiki/web-apis)

The following tutorials also explain the migration:

* [An ASP.NET Core Web app signing-in users with the Microsoft identity platform in your organization](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/1-WebApp-OIDC/1-1-MyOrg#option-2-create-the-sample-from-the-command-line). See **Option 2: Create the sample from the command line**.
* [Sign-in a user with the Microsoft identity platform in a WPF Desktop application and call an ASP.NET Core Web API](https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/1.%20Desktop%20app%20calls%20Web%20API#how-was-the-code-created). See **How was the code created**.

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/validation-client-side
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/forms/validation-client-side.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor client-side form validation in static SSR

This article explains how Blazor adds live client-side validation to forms that use [static server-side rendering (static SSR)](../components/render-modes?view=aspnetcore-12.0#static-server-side-rendering-static-ssr). The browser validates individual fields as the user edits them and validates the full form before it's submitted. If the client-side check passes, the form is submitted and validated again on the server.

Forms that use an interactive render mode don't use the static SSR client-side validation feature described in this article. Both their per-field validation and full-form submit validation run in .NET through the form's [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext).

## How client-side validation works

When a static SSR form contains a [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component, Blazor renders the form's validation rules into the page and enforces them using JavaScript before the form is submitted. The user sees validation errors without a round trip to the server.

Client-side validation activates automatically when the following conditions are met:

* The form's hosting component uses static SSR (no `@rendermode` directive applied to the component).
* The form contains a [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component.
* The form's model uses [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) validation attributes.

For the built-in set of validation attributes, no JavaScript configuration, additional package, or service registration is required. The section [Custom client-side validation rules](#custom-client-side-validation-rules) describes how to add support for custom validation attributes.

Important

Client-side validation is a user experience improvement, not an authoritative validation pass. It can be bypassed by disabling or modifying the browser's JavaScript execution. Server-side validation runs after the form is posted and remains authoritative. Never rely on client-side validation to protect data integrity.

## Supported validation attributes

The following [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) attributes are enforced client-side, matching the server-side data annotations behavior:

* [RequiredAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute)
* [StringLengthAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute)
* [MinLengthAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.minlengthattribute)
* [MaxLengthAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.maxlengthattribute)
* [RangeAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute) (only when the operand type is numeric)
* [RegularExpressionAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute)
* [EmailAddressAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.emailaddressattribute)
* [UrlAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.urlattribute)
* [PhoneAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.phoneattribute)
* [CreditCardAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.creditcardattribute)
* [CompareAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.compareattribute)
* [FileExtensionsAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.fileextensionsattribute)

Validation attributes that don't appear in this list, including custom [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute)-derived attributes, aren't enforced client-side by default. They continue to run server-side after the form is submitted. To supply a client-side rule for a custom attribute, see the [Custom client-side validation rules](#custom-client-side-validation-rules) section.

## Validation timing

A field is validated when its value is committed. For text inputs (`<input>` elements), this occurs when the field loses focus. Checkboxes and dropdown lists are validated immediately after selection.

After a field has shown a validation error or after the form has been submitted at least once, the field is validated again on every keystroke so that corrections are reflected immediately.

Submitting the form validates every tracked field. If any field is invalid, the submission is blocked and focus moves to the first invalid field.

## Validation messages, localization, and accessibility

Client-side validation uses [ValidationMessage\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationmessage-1) to display messages for individual fields and [ValidationSummary](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationsummary) to display messages for the whole form, which is the same way that interactive validation reports messages to users.

When validation localization is configured, error messages are localized on the server as the page is rendered, so client-side validation displays the same localized strings as the server-side experience. Localization requires [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation). For more information, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-12.0#localize-validation-messages).

ARIA attributes on input elements and validation message containers are managed by Blazor automatically, so assistive technologies announce validation errors without additional configuration.

## Validation state CSS classes

The client-side validation engine applies the same CSS classes as Blazor's interactive validation, which are shown in the following table.

| Element            | Classes                                                             |
| ------------------ | ------------------------------------------------------------------- |
| Input              | `valid` or `invalid`, plus `modified` once the user edits the field |
| Validation message | `validation-message`                                                |
| Validation summary | `validation-summary-errors` or `validation-summary-valid`           |

Client-side validation also calls the browser's [Constraint Validation API](https://developer.mozilla.org/docs/Web/API/Constraint_validation), so the standard CSS pseudo-classes `:valid` and `:invalid` reflect each input's current validation state.

## Opt out of client-side validation

The feature can be disabled at multiple levels. Server-side validation is unaffected by any of the options in this section.

### Opt out for a single form

Set the [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component's `DisableClientValidation` parameter to `true`:

```
<DataAnnotationsValidator DisableClientValidation="true" />
```

### Opt out for the entire app

Set `DisableClientValidation` on [RazorComponentsServiceOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.endpoints.razorcomponentsserviceoptions) when Razor components services are registered in the `Program` file:

```
builder.Services.AddRazorComponents(options =>
{
    options.DisableClientValidation = true;
});
```

The global option takes precedence. When it's set to `true`, forms don't emit client-side validation rules.

### Opt out for a single submit button

Use the standard HTML [`formnovalidate` attribute](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/formNoValidate) on the button. The form is posted without a client-side check, and server-side validation still runs after the post:

```
<button type="submit" formnovalidate>Save draft</button>
```

This can be used to implement a "save draft" or "back" button that don't require a completely valid form for the submit to succeed.

## Custom client-side validation rules

Custom validation attributes continue to run on the server but don't have a client-side implementation by default. Enforcing a custom rule in the browser involves two steps: emitting the rule from the .NET attribute and registering a matching JavaScript validator. The server-side implementation remains authoritative.

### Emit the rule from .NET

Implement `IClientValidationRuleProvider` on the validation attribute and return one or more `ClientValidationRule` instances. The rule's `Name` identifies the client-side validator, and `Parameters` supplies values the validator needs.

The following `StartsWithAttribute` validates server-side in `IsValid` and contributes a `startswith` client-side rule with a `prefix` parameter:

```
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample.Validation;

// A custom validation attribute that also contributes a client-side rule for static SSR
// forms. The rule name ('startswith') must match the validator registered on the client
// with Blazor.formValidation.addValidator.
public sealed class StartsWithAttribute : ValidationAttribute, IClientValidationRuleProvider
{
    private readonly string prefix;

    public StartsWithAttribute(string prefix)
    {
        this.prefix = prefix;
        ErrorMessage = $"The value must start with '{prefix}'.";
    }

    protected override ValidationResult? IsValid(object? value,
        ValidationContext validationContext)
    {
        // Leave empty values to RequiredAttribute, matching the client-side validator.
        if (value is not string text || text.Length == 0)
        {
            return ValidationResult.Success;
        }

        if (!text.StartsWith(prefix, StringComparison.Ordinal))
        {
            return new ValidationResult(ErrorMessage, [validationContext.MemberName!]);
        }

        return ValidationResult.Success;
    }

    public IEnumerable<ClientValidationRule> GetClientValidationRules()
    {
        yield return new ClientValidationRule(
            "startswith",
            new Dictionary<string, string> { ["prefix"] = prefix });
    }
}
```

Apply the attribute to the model in the usual way:

```
using System.ComponentModel.DataAnnotations;

namespace BlazorSample.Validation;

// Model for the static SSR client-side validation sample. The StartsWith attribute
// contributes a client-side rule enforced in the browser before the form is submitted.
public class ShipModel
{
    [Required]
    [StartsWith("NCC-")]
    public string? Registry { get; set; }
}
```

### Register the JavaScript validator

In JavaScript, call `Blazor.formValidation.addValidator(name, validator)` to associate a rule name with a validator function. The `name` must exactly match `ClientValidationRule.Name`, including casing. Registrations are app-wide, and registering the same name again replaces the previous validator.

Warning

If no JavaScript validator is registered for an emitted rule name, the rule is skipped in the browser. Server-side validation still runs when the form is posted.

Register custom validators once from app startup code. Choose the registration location based on how Blazor starts, as described in the following table.

| Blazor startup              | Registration location                                 |
| --------------------------- | ----------------------------------------------------- |
| Automatic startup (default) | A script immediately after `blazor.web.js`            |
| Manual `Blazor.start()`     | The continuation returned by `Blazor.start()`         |
| Either startup style        | A JavaScript initializer's `afterWebStarted` callback |

A [JavaScript initializer](../fundamentals/startup?view=aspnetcore-12.0#javascript-initializers) works with either startup style. In a file named `{ASSEMBLY NAME}.lib.module.js` in the app's `wwwroot` folder:

```
// A JavaScript initializer works with automatic or manual Blazor startup. The validation service
// is available when afterWebStarted runs, even if the current page contains no validated form.
export function afterWebStarted(blazor) {
  blazor.formValidation.addValidator('startswith', (context) => {
    const value = context.value;

    // An empty value is valid. Use [Required] to require a value.
    if (!value) {
      return { success: true };
    }

    return { success: value.startsWith(context.params.prefix) };
  });
}
```

With automatic startup, an app-specific validator script can instead be loaded immediately after `blazor.web.js`:

```
<script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="@Assets["js/custom-validation.js"]"></script>
```

The second script can call `Blazor.formValidation.addValidator` directly.

With manual startup, define the registration in an app script:

```
function registerCustomValidators(blazor) {
  blazor.formValidation.addValidator('startswith', (context) => {
    const value = context.value;

    if (!value) {
      return { success: true };
    }

    return { success: value.startsWith(context.params.prefix) };
  });
}

// Automatic startup initializes formValidation before this script runs.
// Manual startup calls registerCustomValidators after Blazor.start completes.
if (Blazor.formValidation) {
  registerCustomValidators(Blazor);
}
```

Load the scripts with automatic startup disabled, and register the validators after `Blazor.start()` completes:

```
<script src="@Assets["_framework/blazor.web.js"]" autostart="false"></script>
<script src="@Assets["js/custom-validation.js"]"></script>
<script>
    Blazor.start().then(() => {
        registerCustomValidators(Blazor);
    });
</script>
```

Important

Register validators from app startup code, not from a page or form component. A component script can run before Blazor starts, and scripts added by enhanced navigation aren't executed. Static SSR components also can't use `IJSRuntime` because they don't have an interactive .NET runtime.

### Write JavaScript validator functions

The validator receives a context object with the following members, as shown in the following table.

| Member    | Description                                                                         |
| --------- | ----------------------------------------------------------------------------------- |
| `value`   | The field's current value as a string, or `null`/`undefined` when there's no value. |
| `element` | The validated `input`, `select`, or `textarea` element.                             |
| `params`  | The rule's `Parameters` as a string dictionary.                                     |

The validator is expected to return `{ success: true }` when the value is valid. Return `{ success: false }` to use the rule's server-supplied message, or you can return `{ success: false, message: '...' }` to override the message for that call.

Empty values should normally be treated as valid by rules other than `required`, allowing an optional field to remain empty while still validating values that are supplied.

Implement the same rule semantics in .NET and JavaScript, including case sensitivity, normalization, and empty-value handling. If the implementations differ, the browser and the authoritative server-side validation can produce different results.

## Validate form on demand

The `Blazor.formValidation` API also exposes JavaScript methods for validating on demand, as the following table shows.

| Method                   | Description                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| `validateField(element)` | Validates a single field element and updates its error display. Returns `true` when valid. |
| `validateForm(form)`     | Validates every tracked field in a form. Returns `true` when all fields are valid.         |

## Limitations

* Client-side rules are emitted only for fields included in server-side validation as well. Without [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation), only top-level model properties are validated. Validating nested objects and collections requires the app to call [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation) and the model to be discovered. For more information, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-12.0#nested-objects-and-collections). Note that this limitation is an intentional feature to help prevent bugs where the authoritative server validation would be missing due to misconfiguration.
* Inputs added to an existing form by a later [streaming rendering](../components/rendering?view=aspnetcore-12.0#streaming-rendering) update don't receive client-side validation. A form delivered in a single streamed batch is covered normally.
* Only the attributes listed in [Supported validation attributes](#supported-validation-attributes) have built-in client-side implementations. For example, a [RangeAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute) with a non-numeric operand type is only enforced on the server. Other attributes require a [custom client-side validation rule](#custom-client-side-validation-rules).
* Custom JavaScript validators are synchronous. Rules that require a network call or other asynchronous work must run on the server or use asynchronous validation with an interactive render mode. For more information, see [ASP.NET Core Blazor advanced form validation](validation-advanced?view=aspnetcore-12.0).

## Related content

* [ASP.NET Core Blazor forms validation](validation?view=aspnetcore-12.0)
* [ASP.NET Core Blazor advanced form validation](validation-advanced?view=aspnetcore-12.0)
* [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-12.0)
* [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-12.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/sql
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/sql.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 4 of tutorial series on Razor Pages

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Joe Audette](https://twitter.com/joeaudette)

The `RazorPagesMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. Register the database context with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in `Program.cs`:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, the configuration gets the connection string from the `appsettings.json` file.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

The generated connection string is similar to the following JSON:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-f2e0482c-952d-4b1c-afe9-a1a3dfe52e55;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Data Source=RazorPagesMovie.Data.db"
  }
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

## SQL Server Express LocalDB

LocalDB is a lightweight version of the SQL Server Express database engine that's designed for program development. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default, LocalDB creates `*.mdf` database files in the `C:\Users\<user>\` directory.

1. From the **View** menu, open **SQL Server Object Explorer** (SSOX).

2. Right-click on the `Movie` table and select **View Designer**:

   Note the key icon next to `ID`. By default, EF creates a property named `ID` for the primary key.

3. Right-click on the `Movie` table and select **View Data**:

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL Database engine. SQLite is the most used database engine in the world.

You can download many third-party tools to manage and view a SQLite database. The following image is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

Note

For this tutorial, use the Entity Framework Core *migrations* feature where possible. Migrations updates the database schema to match changes in the data model. However, migrations can only make changes that the EF Core provider supports, and the SQLite provider's capabilities are limited. For example, adding a column is supported, but removing or changing a column isn't supported. If you create a migration to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, this tutorial doesn't use migrations for SQLite schema changes. Instead, when the schema changes, the database is dropped and re-created.

## Seed the database

Create a new class named `SeedData` in the *Models* folder with the following code:

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;

namespace RazorPagesMovie.Models;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using (var context = new RazorPagesMovieContext(
            serviceProvider.GetRequiredService<
                DbContextOptions<RazorPagesMovieContext>>()))
        {
            if (context == null || context.Movie == null)
            {
                throw new ArgumentNullException("Null RazorPagesMovieContext");
            }

            // Look for any movies.
            if (context.Movie.Any())
            {
                return;   // DB has been seeded
            }

            context.Movie.AddRange(
                new Movie
                {
                    Title = "When Harry Met Sally",
                    ReleaseDate = DateTime.Parse("1989-2-12"),
                    Genre = "Romantic Comedy",
                    Price = 7.99M
                },

                new Movie
                {
                    Title = "Ghostbusters ",
                    ReleaseDate = DateTime.Parse("1984-3-13"),
                    Genre = "Comedy",
                    Price = 8.99M
                },

                new Movie
                {
                    Title = "Ghostbusters 2",
                    ReleaseDate = DateTime.Parse("1986-2-23"),
                    Genre = "Comedy",
                    Price = 9.99M
                },

                new Movie
                {
                    Title = "Rio Bravo",
                    ReleaseDate = DateTime.Parse("1959-4-15"),
                    Genre = "Western",
                    Price = 3.99M
                }
            );
            context.SaveChanges();
        }
    }
}
```

If the database contains any movies, the seed initializer returns and doesn't add any movies.

```
if (context.Movie.Any())
{
    return;
}
```

[]()

### Add the seed initializer

Update `Program.cs` with the following highlighted code:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages()
   .WithStaticAssets();

app.Run();
```

In the preceding code, you modify `Program.cs` to do the following steps:

* Create a service scope with `app.Services.CreateScope()`.
* Obtain the service provider ([IServiceProvider](/en-us/dotnet/api/system.iserviceprovider)) from the scope.
* Call the `SeedData.Initialize` method, passing the service provider.
* Dispose the service scope when the seed method completes. The [`using` statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) ensures the scope is disposed.

The following exception occurs when you don't run `Update-Database`:

> `SqlException: Cannot open database "RazorPagesMovieContext-" requested by the login. The login failed.` `Login failed for user 'user name'.`

### Test the app

Delete all the records in the database so the seed method runs. Stop and start the app to seed the database. If the database isn't seeded, put a breakpoint on `if (context.Movie.Any())` and step through the code.

The app shows the seeded data:

## Next steps

[Previous: Scaffolded Razor Pages](page?view=aspnetcore-10.0) [Next: Update the pages](da1?view=aspnetcore-10.0)

The `RazorPagesMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in `Program.cs`:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, configuration gets the connection string from the `appsettings.json` file.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

The generated connection string is similar to the following JSON:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-f2e0482c-952d-4b1c-afe9-a1a3dfe52e55;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Data Source=RazorPagesMovie.Data.db"
  }
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

## SQL Server Express LocalDB

LocalDB is a lightweight version of the SQL Server Express database engine that's targeted for program development. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default, LocalDB database creates `*.mdf` files in the `C:\Users\<user>\` directory.

[]()

1. From the **View** menu, open **SQL Server Object Explorer** (SSOX).

2. Right-click on the `Movie` table and select **View Designer**:

   Note the key icon next to `ID`. By default, EF creates a property named `ID` for the primary key.

3. Right-click on the `Movie` table and select **View Data**:

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third-party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

Note

For this tutorial, the Entity Framework Core *migrations* feature is used where possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and the SQLite provider's capabilities are limited. For example, adding a column is supported, but removing or changing a column is not supported. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, this tutorial doesn't use migrations for SQLite schema changes. Instead, when the schema changes, the database is dropped and re-created.

## Seed the database

Create a new class named `SeedData` in the *Models* folder with the following code:

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;

namespace RazorPagesMovie.Models;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using (var context = new RazorPagesMovieContext(
            serviceProvider.GetRequiredService<
                DbContextOptions<RazorPagesMovieContext>>()))
        {
            if (context == null || context.Movie == null)
            {
                throw new ArgumentNullException("Null RazorPagesMovieContext");
            }

            // Look for any movies.
            if (context.Movie.Any())
            {
                return;   // DB has been seeded
            }

            context.Movie.AddRange(
                new Movie
                {
                    Title = "When Harry Met Sally",
                    ReleaseDate = DateTime.Parse("1989-2-12"),
                    Genre = "Romantic Comedy",
                    Price = 7.99M
                },

                new Movie
                {
                    Title = "Ghostbusters ",
                    ReleaseDate = DateTime.Parse("1984-3-13"),
                    Genre = "Comedy",
                    Price = 8.99M
                },

                new Movie
                {
                    Title = "Ghostbusters 2",
                    ReleaseDate = DateTime.Parse("1986-2-23"),
                    Genre = "Comedy",
                    Price = 9.99M
                },

                new Movie
                {
                    Title = "Rio Bravo",
                    ReleaseDate = DateTime.Parse("1959-4-15"),
                    Genre = "Western",
                    Price = 3.99M
                }
            );
            context.SaveChanges();
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;
}
```

[]()

### Add the seed initializer

Update the `Program.cs` with the following highlighted code:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages();

app.Run();
```

In the previous code, `Program.cs` has been modified to do the following:

* Create a service scope with `app.Services.CreateScope()`.
* Obtain the service provider ([IServiceProvider](/en-us/dotnet/api/system.iserviceprovider)) from the scope.
* Call the `SeedData.Initialize` method, passing to it the service provider.
* Dispose the service scope when the seed method completes. The [`using` statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) ensures the scope is disposed.

The following exception occurs when `Update-Database` has not been run:

> `SqlException: Cannot open database "RazorPagesMovieContext-" requested by the login. The login failed.` `Login failed for user 'user name'.`

### Test the app

Delete all the records in the database so the seed method will run. Stop and start the app to seed the database. If the database isn't seeded, put a breakpoint on `if (context.Movie.Any())` and step through the code.

The app shows the seeded data:

## Next steps

[Previous: Scaffolded Razor Pages](page?view=aspnetcore-10.0) [Next: Update the pages](da1?view=aspnetcore-10.0)

The `RazorPagesMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in `Program.cs`:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, configuration gets the connection string from the `appsettings.json` file.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

The generated connection string is similar to the following JSON:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-bc;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Data Source=RazorPagesMovie.Data.db"
  }
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

## SQL Server Express LocalDB

LocalDB is a lightweight version of the SQL Server Express database engine that's targeted for program development. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default, LocalDB database creates `*.mdf` files in the `C:\Users\<user>\` directory.

[]()

1. From the **View** menu, open **SQL Server Object Explorer** (SSOX).

2. Right-click on the `Movie` table and select **View Designer**:

   Note the key icon next to `ID`. By default, EF creates a property named `ID` for the primary key.

3. Right-click on the `Movie` table and select **View Data**:

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third-party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

Note

For this tutorial, the Entity Framework Core *migrations* feature is used where possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and the SQLite provider's capabilities are limited. For example, adding a column is supported, but removing or changing a column is not supported. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, this tutorial doesn't use migrations for SQLite schema changes. Instead, when the schema changes, the database is dropped and re-created.

## Seed the database

Create a new class named `SeedData` in the *Models* folder with the following code:

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;

namespace RazorPagesMovie.Models;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using (var context = new RazorPagesMovieContext(
            serviceProvider.GetRequiredService<
                DbContextOptions<RazorPagesMovieContext>>()))
        {
            if (context == null || context.Movie == null)
            {
                throw new ArgumentNullException("Null RazorPagesMovieContext");
            }

            // Look for any movies.
            if (context.Movie.Any())
            {
                return;   // DB has been seeded
            }

            context.Movie.AddRange(
                new Movie
                {
                    Title = "When Harry Met Sally",
                    ReleaseDate = DateTime.Parse("1989-2-12"),
                    Genre = "Romantic Comedy",
                    Price = 7.99M
                },

                new Movie
                {
                    Title = "Ghostbusters ",
                    ReleaseDate = DateTime.Parse("1984-3-13"),
                    Genre = "Comedy",
                    Price = 8.99M
                },

                new Movie
                {
                    Title = "Ghostbusters 2",
                    ReleaseDate = DateTime.Parse("1986-2-23"),
                    Genre = "Comedy",
                    Price = 9.99M
                },

                new Movie
                {
                    Title = "Rio Bravo",
                    ReleaseDate = DateTime.Parse("1959-4-15"),
                    Genre = "Western",
                    Price = 3.99M
                }
            );
            context.SaveChanges();
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;
}
```

[]()

### Add the seed initializer

Update the `Program.cs` with the following highlighted code:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

In the previous code, `Program.cs` has been modified to do the following:

* Create a service scope with `app.Services.CreateScope()`.
* Obtain the service provider ([IServiceProvider](/en-us/dotnet/api/system.iserviceprovider)) from the scope.
* Call the `SeedData.Initialize` method, passing to it the service provider.
* Dispose the service scope when the seed method completes. The [`using` statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) ensures the scope is disposed.

The following exception occurs when `Update-Database` has not been run:

> `SqlException: Cannot open database "RazorPagesMovieContext-" requested by the login. The login failed.` `Login failed for user 'user name'.`

### Test the app

Delete all the records in the database so the seed method will run. Stop and start the app to seed the database. If the database isn't seeded, put a breakpoint on `if (context.Movie.Any())` and step through the code.

The app shows the seeded data:

## Next steps

[Previous: Scaffolded Razor Pages](page?view=aspnetcore-10.0) [Next: Update the pages](da1?view=aspnetcore-10.0)

The `RazorPagesMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in `Program.cs`:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, configuration gets the connection string from the `appsettings.json` file.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

The generated connection string is similar to the following JSON:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-bc;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Data Source=RazorPagesMovie.Data.db"
  }
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

## SQL Server Express LocalDB

LocalDB is a lightweight version of the SQL Server Express database engine that's targeted for program development. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default, LocalDB database creates `*.mdf` files in the `C:\Users\<user>\` directory.

[]()

1. From the **View** menu, open **SQL Server Object Explorer** (SSOX).

2. Right-click on the `Movie` table and select **View Designer**:

   Note the key icon next to `ID`. By default, EF creates a property named `ID` for the primary key.

3. Right-click on the `Movie` table and select **View Data**:

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third-party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

Note

For this tutorial, the Entity Framework Core *migrations* feature is used where possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and the SQLite provider's capabilities are limited. For example, adding a column is supported, but removing or changing a column is not supported. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, this tutorial doesn't use migrations for SQLite schema changes. Instead, when the schema changes, the database is dropped and re-created.

## Seed the database

Create a new class named `SeedData` in the *Models* folder with the following code:

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;

namespace RazorPagesMovie.Models;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using (var context = new RazorPagesMovieContext(
            serviceProvider.GetRequiredService<
                DbContextOptions<RazorPagesMovieContext>>()))
        {
            if (context == null || context.Movie == null)
            {
                throw new ArgumentNullException("Null RazorPagesMovieContext");
            }

            // Look for any movies.
            if (context.Movie.Any())
            {
                return;   // DB has been seeded
            }

            context.Movie.AddRange(
                new Movie
                {
                    Title = "When Harry Met Sally",
                    ReleaseDate = DateTime.Parse("1989-2-12"),
                    Genre = "Romantic Comedy",
                    Price = 7.99M
                },

                new Movie
                {
                    Title = "Ghostbusters ",
                    ReleaseDate = DateTime.Parse("1984-3-13"),
                    Genre = "Comedy",
                    Price = 8.99M
                },

                new Movie
                {
                    Title = "Ghostbusters 2",
                    ReleaseDate = DateTime.Parse("1986-2-23"),
                    Genre = "Comedy",
                    Price = 9.99M
                },

                new Movie
                {
                    Title = "Rio Bravo",
                    ReleaseDate = DateTime.Parse("1959-4-15"),
                    Genre = "Western",
                    Price = 3.99M
                }
            );
            context.SaveChanges();
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;
}
```

[]()

### Add the seed initializer

Update the `Program.cs` with the following highlighted code:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

In the previous code, `Program.cs` has been modified to do the following:

* Get a database context instance from the dependency injection (DI) container.
* Call the `seedData.Initialize` method, passing to it the database context instance.
* Dispose the context when the seed method completes. The [using statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) ensures the context is disposed.

The following exception occurs when `Update-Database` has not been run:

> `SqlException: Cannot open database "RazorPagesMovieContext-" requested by the login. The login failed.` `Login failed for user 'user name'.`

### Test the app

Delete all the records in the database so the seed method will run. Stop and start the app to seed the database. If the database isn't seeded, put a breakpoint on `if (context.Movie.Any())` and step through the code.

The app shows the seeded data:

## Next steps

[Previous: Scaffolded Razor Pages](page?view=aspnetcore-10.0) [Next: Update the pages](da1?view=aspnetcore-10.0)

The `RazorPagesMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in `Program.cs`:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();
```

```
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, configuration gets the connection string from the `appsettings.json` file.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

The generated connection string is similar to the following JSON:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-bc;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Data Source=RazorPagesMovie.db"
  }
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

## SQL Server Express LocalDB

LocalDB is a lightweight version of the SQL Server Express database engine that's targeted for program development. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default, LocalDB database creates `*.mdf` files in the `C:\Users\<user>\` directory.

[]()

1. From the **View** menu, open **SQL Server Object Explorer** (SSOX).

2. Right-click on the `Movie` table and select **View Designer**:

   Note the key icon next to `ID`. By default, EF creates a property named `ID` for the primary key.

3. Right-click on the `Movie` table and select **View Data**:

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third-party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

Note

For this tutorial, the Entity Framework Core *migrations* feature is used where possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and the SQLite provider's capabilities are limited. For example, adding a column is supported, but removing or changing a column is not supported. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, this tutorial doesn't use migrations for SQLite schema changes. Instead, when the schema changes, the database is dropped and re-created.

## Seed the database

Create a new class named `SeedData` in the *Models* folder with the following code:

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;

namespace RazorPagesMovie.Models
{
    public static class SeedData
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new RazorPagesMovieContext(
                serviceProvider.GetRequiredService<
                    DbContextOptions<RazorPagesMovieContext>>()))
            {
                if (context == null || context.Movie == null)
                {
                    throw new ArgumentNullException("Null RazorPagesMovieContext");
                }

                // Look for any movies.
                if (context.Movie.Any())
                {
                    return;   // DB has been seeded
                }

                context.Movie.AddRange(
                    new Movie
                    {
                        Title = "When Harry Met Sally",
                        ReleaseDate = DateTime.Parse("1989-2-12"),
                        Genre = "Romantic Comedy",
                        Price = 7.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters ",
                        ReleaseDate = DateTime.Parse("1984-3-13"),
                        Genre = "Comedy",
                        Price = 8.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters 2",
                        ReleaseDate = DateTime.Parse("1986-2-23"),
                        Genre = "Comedy",
                        Price = 9.99M
                    },

                    new Movie
                    {
                        Title = "Rio Bravo",
                        ReleaseDate = DateTime.Parse("1959-4-15"),
                        Genre = "Western",
                        Price = 3.99M
                    }
                );
                context.SaveChanges();
            }
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;
}
```

[]()

### Add the seed initializer

Update the `Program.cs` with the following highlighted code:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

In the previous code, `Program.cs` has been modified to do the following:

* Get a database context instance from the dependency injection (DI) container.
* Call the `seedData.Initialize` method, passing to it the database context instance.
* Dispose the context when the seed method completes. The [using statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) ensures the context is disposed.

The following exception occurs when `Update-Database` has not been run:

> `SqlException: Cannot open database "RazorPagesMovieContext-" requested by the login. The login failed.` `Login failed for user 'user name'.`

### Test the app

Delete all the records in the database so the seed method will run. Stop and start the app to seed the database. If the database isn't seeded, put a breakpoint on `if (context.Movie.Any())` and step through the code.

The app shows the seeded data:

## Next steps

[Previous: Scaffolded Razor Pages](page?view=aspnetcore-10.0) [Next: Update the pages](da1?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

The `RazorPagesMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in the `ConfigureServices` method in `Startup.cs`:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<RazorPagesMovieContext>(options =>
      options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
```

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, configuration gets the connection string from the `appsettings.json` file.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

The generated connection string is similar to the following JSON:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-bc;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Data Source=RazorPagesMovie.db"
  }
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

## SQL Server Express LocalDB

LocalDB is a lightweight version of the SQL Server Express database engine that's targeted for program development. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default, LocalDB database creates `*.mdf` files in the `C:\Users\<user>\` directory.

[]()

1. From the **View** menu, open **SQL Server Object Explorer** (SSOX).

2. Right-click on the `Movie` table and select **View Designer**:

   Note the key icon next to `ID`. By default, EF creates a property named `ID` for the primary key.

3. Right-click on the `Movie` table and select **View Data**:

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third-party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

Note

For this tutorial, the Entity Framework Core *migrations* feature is used where possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and the SQLite provider's capabilities are limited. For example, adding a column is supported, but removing or changing a column is not supported. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, this tutorial doesn't use migrations for SQLite schema changes. Instead, when the schema changes, the database is dropped and re-created.

## Seed the database

Create a new class named `SeedData` in the *Models* folder with the following code:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
using System;
using System.Linq;

namespace RazorPagesMovie.Models
{
    public static class SeedData
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new RazorPagesMovieContext(
                serviceProvider.GetRequiredService<
                    DbContextOptions<RazorPagesMovieContext>>()))
            {
                // Look for any movies.
                if (context.Movie.Any())
                {
                    return;   // DB has been seeded
                }

                context.Movie.AddRange(
                    new Movie
                    {
                        Title = "When Harry Met Sally",
                        ReleaseDate = DateTime.Parse("1989-2-12"),
                        Genre = "Romantic Comedy",
                        Price = 7.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters ",
                        ReleaseDate = DateTime.Parse("1984-3-13"),
                        Genre = "Comedy",
                        Price = 8.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters 2",
                        ReleaseDate = DateTime.Parse("1986-2-23"),
                        Genre = "Comedy",
                        Price = 9.99M
                    },

                    new Movie
                    {
                        Title = "Rio Bravo",
                        ReleaseDate = DateTime.Parse("1959-4-15"),
                        Genre = "Western",
                        Price = 3.99M
                    }
                );
                context.SaveChanges();
            }
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;
}
```

[]()

### Add the seed initializer

Replace the contents of the `Program.cs` with the following code:

```
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RazorPagesMovie.Models;
using System;

namespace RazorPagesMovie
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    SeedData.Initialize(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

In the previous code, the `Main` method has been modified to do the following:

* Get a database context instance from the dependency injection container.
* Call the `seedData.Initialize` method, passing to it the database context instance.
* Dispose the context when the seed method completes. The [using statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) ensures the context is disposed.

The following exception occurs when `Update-Database` has not been run:

> `SqlException: Cannot open database "RazorPagesMovieContext-" requested by the login. The login failed.` `Login failed for user 'user name'.`

### Test the app

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

1. Delete all the records in the database. Use the delete links in the browser or from [SSOX](new-field?view=aspnetcore-10.0#ssox)

2. Force the app to initialize by calling the methods in the `Startup` class, so the seed method runs. To force initialization, IIS Express must be stopped and restarted. Stop and restart IIS with any of the following approaches:

   1. Right-click the IIS Express system tray icon in the notification area and select **Exit** or **Stop Site**:

   2. If the app is running in non-debug mode, press `F5` to run in debug mode.

   3. If the app in debug mode, stop the debugger and press `F5`.

Delete all the records in the database, so the seed method will run. Stop and start the app to seed the database.

The app shows the seeded data:

## Next steps

[Previous: Scaffolded Razor Pages](page?view=aspnetcore-10.0) [Next: Update the pages](da1?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie30) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

The `RazorPagesMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in the `ConfigureServices` method in `Startup.cs`:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<RazorPagesMovieContext>(options =>
      options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
```

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, configuration gets the connection string from the `appsettings.json` file.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

The generated connection string will be similar to the following:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-bc;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Data Source=MvcMovie.db"
  }
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

## SQL Server Express LocalDB

LocalDB is a lightweight version of the SQL Server Express database engine that's targeted for program development. LocalDB starts on demand and runs in user mode, so there's no complex configuration. By default, LocalDB database creates `*.mdf` files in the `C:\Users\<user>\` directory.

[]()

* From the **View** menu, open **SQL Server Object Explorer** (SSOX).

* Right-click on the `Movie` table and select **View Designer**:

Note the key icon next to `ID`. By default, EF creates a property named `ID` for the primary key.

* Right-click on the `Movie` table and select **View Data**:

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third-party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

Note

For this tutorial, the Entity Framework Core *migrations* feature is used where possible. Migrations updates the database schema to match changes in the data model. However, migrations can only do the kinds of changes that the EF Core provider supports, and the SQLite provider's capabilities are limited. For example, adding a column is supported, but removing or changing a column is not supported. If a migration is created to remove or change a column, the `ef migrations add` command succeeds but the `ef database update` command fails. Due to these limitations, this tutorial doesn't use migrations for SQLite schema changes. Instead, when the schema changes, the database is dropped and re-created.

## Seed the database

Create a new class named `SeedData` in the *Models* folder with the following code:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
using System;
using System.Linq;

namespace RazorPagesMovie.Models
{
    public static class SeedData
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new RazorPagesMovieContext(
                serviceProvider.GetRequiredService<
                    DbContextOptions<RazorPagesMovieContext>>()))
            {
                // Look for any movies.
                if (context.Movie.Any())
                {
                    return;   // DB has been seeded
                }

                context.Movie.AddRange(
                    new Movie
                    {
                        Title = "When Harry Met Sally",
                        ReleaseDate = DateTime.Parse("1989-2-12"),
                        Genre = "Romantic Comedy",
                        Price = 7.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters ",
                        ReleaseDate = DateTime.Parse("1984-3-13"),
                        Genre = "Comedy",
                        Price = 8.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters 2",
                        ReleaseDate = DateTime.Parse("1986-2-23"),
                        Genre = "Comedy",
                        Price = 9.99M
                    },

                    new Movie
                    {
                        Title = "Rio Bravo",
                        ReleaseDate = DateTime.Parse("1959-4-15"),
                        Genre = "Western",
                        Price = 3.99M
                    }
                );
                context.SaveChanges();
            }
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;
}
```

[]()

### Add the seed initializer

Replace the contents of the `Program.cs` with the following code:

```
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RazorPagesMovie.Models;
using System;

namespace RazorPagesMovie
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    SeedData.Initialize(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }
            
            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

In the previous code, the `Main` method has been modified to do the following:

* Get a database context instance from the dependency injection container.
* Call the `seedData.Initialize` method, passing to it the database context instance.
* Dispose the context when the seed method completes. The [using statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) ensures the context is disposed.

The following exception occurs when `Update-Database` has not been run:

> `SqlException: Cannot open database "RazorPagesMovieContext-" requested by the login. The login failed.` `Login failed for user 'user name'.`

### Test the app

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

- Delete all the records in the database. Use the delete links in the browser or from [SSOX](new-field?view=aspnetcore-10.0#ssox).

- Force the app to initialize by calling the methods in the `Startup` class, so the seed method runs. To force initialization, IIS Express must be stopped and restarted. Stop and restart IIS with any of the following approaches:

  * Right-click the IIS Express system tray icon in the notification area and tap **Exit** or **Stop Site**:

    * If the app is running in non-debug mode, press `F5` to run in debug mode.
    * If the app in debug mode, stop the debugger and press `F5`.

Delete all the records in the database, so the seed method will run. Stop and start the app to seed the database.

The app shows the seeded data:

## Next steps

[Previous: Scaffolded Razor Pages](page?view=aspnetcore-10.0) [Next: Update the pages](da1?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Upload files in ASP.NET Core

By [Rutger Storm](https://github.com/rutix)

ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Security considerations

Use caution when providing users with the ability to upload files to a server. Cyberattackers might attempt to:

* Execute [denial of service](/en-us/windows-hardware/drivers/ifs/denial-of-service) attacks.
* Upload viruses or malware.
* Compromise networks and servers in other ways.

Security steps that reduce the likelihood of a successful attack are:

* Upload files to a dedicated file upload area, preferably to a non-system drive. A dedicated location makes it easier to impose security restrictions on uploaded files. Disable execute permissions on the file upload location.†
* Do **not** persist uploaded files in the same directory tree as the app.†
* Use a safe file name determined by the app. Don't use a file name provided by the user or the untrusted file name of the uploaded file.† HTML encode the untrusted file name when displaying it. For example, logging the file name or displaying in UI (Razor automatically HTML encodes output).
* Allow only approved file extensions for the app's design specification.†
* Verify that client-side checks are performed on the server.† Client-side checks are easy to circumvent.
* Check the size of an uploaded file. Set a maximum size limit to prevent large uploads.†
* When an uploaded file shouldn't overwrite files with the same name. Check the file name against the database or physical storage before uploading the file.
* **Run a virus/malware scanner on uploaded content before the file is stored.**

†The sample app demonstrates an approach that meets the criteria.

Warning

Uploading malicious code to a system is frequently the first step to executing code that can:

* Completely gain control of a system.
* Overload a system with the result that the system crashes.
* Compromise user or system data.
* Apply graffiti to a public UI.

For information on reducing vulnerabilities when accepting files from users, see the following resources:

* [Unrestricted File Upload](https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload)
* [Azure Security: Ensure appropriate controls are in place when accepting files from users](/en-us/azure/security/azure-security-threat-modeling-tool-input-validation#controls-users)

For more information on implementing security measures, including examples from the sample app, see the [Validation](#validation) section.

## Storage scenarios

Common storage options for files include:

* Database

  * For [small](#small5) file uploads, a database is often faster than physical storage (file system or network share) options.
  * A database is often more convenient than physical storage options because retrieval of a database record for user data can concurrently supply the file content (for example, an avatar image).
  * A database is potentially less expensive than using a cloud data storage service.

* Physical storage (file system or network share)

  * For large file uploads:

    * Database limits might restrict the size of the upload.
    * Physical storage is often less economical than storage in a database.

  * Physical storage is potentially less expensive than using a cloud data storage service.

  * The app's process must have read and write permissions to the storage location. **Never grant execute permission.**

* Cloud data storage service, for example, [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs/).

  * Services usually offer improved scalability and resiliency over on-premises solutions that are usually subject to single points of failure.
  * Services are potentially lower cost in large storage infrastructure scenarios.

  For more information, see [Quickstart: Use .NET to create a blob in object storage](/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet).

[]()

## Small and large files

The definition of small and large files depend on the computing resources available. Apps should benchmark the storage approach used to ensure it can handle the expected sizes. Benchmark memory, CPU, disk, and database performance.

While specific boundaries can't be provided on what is small versus large for your deployment, here are some of ASP.NET Core's related defaults for [`FormOptions`](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/Features/FormOptions.cs) ([API documentation](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions)):

* By default, [`HttpRequest.Form`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.form#microsoft-aspnetcore-http-httprequest-form) doesn't buffer the entire request body ([BufferBody](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.bufferbody#microsoft-aspnetcore-http-features-formoptions-bufferbody)), but it does buffer any multipart form files included.
* [MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) is the maximum size for buffered form files (default: 128 MB).
* [MemoryBufferThreshold](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.memorybufferthreshold#microsoft-aspnetcore-http-features-formoptions-memorybufferthreshold) indicates the buffering threshold in memory before transitioning to a buffer file on disk (default: 64 KB). `MemoryBufferThreshold` acts as a boundary between small and large files, which is raised or lowered depending on the apps resources and scenarios.

For more information on [FormOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions), see the [`FormOptions` class](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/Features/FormOptions.cs) in the ASP.NET Core reference source.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## File upload scenarios

Two general approaches for uploading files are buffering and streaming.

### Buffering

The entire file is read into an [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile). `IFormFile` is a C# representation of the file used to process or save the file.

The disk and memory used by file uploads depend on the number and size of concurrent file uploads. If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. If the size or frequency of file uploads is exhausting app resources, use streaming.

Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.

Temporary files for larger requests are written to the location named in the `ASPNETCORE_TEMP` environment variable. If `ASPNETCORE_TEMP` isn't defined, the files are written to the current user's temporary folder.

Buffering small files is covered in the following sections of this article:

* [Physical storage](#upload-small-files-with-buffered-model-binding-to-physical-storage)
* [Database](#upload-small-files-with-buffered-model-binding-to-a-database)

### Streaming

The file is received from a multipart request and directly processed or saved by the app. Streaming doesn't improve performance significantly. Streaming reduces the demands for memory or disk space when uploading files.

Streaming large files is covered in the [Upload large files with streaming](#upload-large-files-with-streaming) section.

### Upload small files with buffered model binding to physical storage

To upload small files, use a multipart form or construct a POST request using JavaScript.

The following example demonstrates the use of a Razor Pages form to upload a single file (`Pages/BufferedSingleFileUploadPhysical.cshtml` in the sample app):

```
<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file" />
            <span asp-validation-for="FileUpload.FormFile"></span>
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
```

The following example is analogous to the prior example except that:

* JavaScript's ([Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API)) is used to submit the form's data.
* There's no validation.

```
<form action="BufferedSingleFileUploadPhysical/?handler=Upload" 
      enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;" 
      method="post">
    <dl>
        <dt>
            <label for="FileUpload_FormFile">File</label>
        </dt>
        <dd>
            <input id="FileUpload_FormFile" type="file" 
                name="FileUpload.FormFile" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output name="result"></output>
    </div>
</form>

<script>
  async function AJAXSubmit (oFormElement) {
    var resultElement = oFormElement.elements.namedItem("result");
    const formData = new FormData(oFormElement);

    try {
    const response = await fetch(oFormElement.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      window.location.href = '/';
    }

    resultElement.value = 'Result: ' + response.status + ' ' + 
      response.statusText;
    } catch (error) {
      console.error('Error:', error);
    }
  }
</script>
```

To perform the form POST in JavaScript for clients that [don't support the Fetch API](https://caniuse.com/fetch), use one of the following approaches:

* Use a Fetch Polyfill (for example, [window.fetch polyfill (github/fetch)](https://github.com/JakeChampion/fetch)).

* Use `XMLHttpRequest`. For example:

  ```
  <script>
    "use strict";

    function AJAXSubmit (oFormElement) {
      var oReq = new XMLHttpRequest();
      oReq.onload = function(e) { 
      oFormElement.elements.namedItem("result").value = 
        'Result: ' + this.status + ' ' + this.statusText;
      };
      oReq.open("post", oFormElement.action);
      oReq.send(new FormData(oFormElement));
    }
  </script>
  ```

In order to support file uploads, HTML forms must specify an encoding type (`enctype`) of `multipart/form-data`.

For a `files` input element to support uploading multiple files provide the `multiple` attribute on the `<input>` element:

```
<input asp-for="FileUpload.FormFiles" type="file" multiple />
```

The individual files uploaded to the server can be accessed through [Model Binding](model-binding?view=aspnetcore-10.0) using [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile). The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.

[]()

Warning

Do **not** use the `FileName` property of [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) other than for display and logging. When displaying or logging, HTML encode the file name. A cyberattacker can provide a malicious filename, including full paths or relative paths. Applications should:

* Remove the path from the user-supplied filename.
* Save the HTML-encoded, path-removed filename for UI or logging.
* Generate a new random filename for storage.

The following code removes the path from the file name:

```
string untrustedFileName = Path.GetFileName(pathName);
```

The examples provided thus far don't take into account security considerations. Additional information is provided by the following sections and the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/):

* [Security considerations](#security-considerations)
* [Validation](#validation)

When uploading files using model binding and [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile), the action method can accept:

* A single [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile).

* Any of the following collections that represent several files:

  * [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection)
  * [IEnumerable](/en-us/dotnet/api/system.collections.ienumerable)<[IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile)>
  * [List](/en-us/dotnet/api/system.collections.generic.list-1)<[IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile)>

Note

Binding matches form files by name. For example, the HTML `name` value in `<input type="file" name="formFile">` must match the C# parameter/property bound (`FormFile`). For more information, see the [Match name attribute value to parameter name of POST method](#match-name-attribute-value-to-parameter-name-of-post-method) section.

The following example:

* Loops through one or more uploaded files.
* Uses [Path.GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename) to return a full path for a file, including the file name.
* Saves the files to the local file system using a file name generated by the app.
* Returns the total number and size of files uploaded.

```
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            var filePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(filePath))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}
```

Use `Path.GetRandomFileName` to generate a file name without a path. In the following example, the path is obtained from configuration:

```
foreach (var formFile in files)
{
    if (formFile.Length > 0)
    {
        var filePath = Path.Combine(_config["StoredFilesPath"], 
            Path.GetRandomFileName());

        using (var stream = System.IO.File.Create(filePath))
        {
            await formFile.CopyToAsync(stream);
        }
    }
}
```

The path passed to the [FileStream](/en-us/dotnet/api/system.io.filestream) *must* include the file name. If the file name isn't provided, an [UnauthorizedAccessException](/en-us/dotnet/api/system.unauthorizedaccessexception) is thrown at runtime.

Files uploaded using the [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) technique are buffered in memory or on disk on the server before processing. Inside the action method, the [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) contents are accessible as a [Stream](/en-us/dotnet/api/system.io.stream). In addition to the local file system, files can be saved to a network share or to a file storage service, such as [Azure Blob storage](/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet).

For another example that loops over multiple files for upload and uses safe file names, see `Pages/BufferedMultipleFileUploadPhysical.cshtml.cs` in the sample app.

Warning

In .NET 7 or earlier versions, [Path.GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename) throws an [IOException](/en-us/dotnet/api/system.io.ioexception) when more than 65,535 files are created without deleting previous temporary files. The limit of 65,535 files is a per-server limit. For more information on this limit on Windows OS, see the remarks in the following articles:

* [GetTempFileNameA function](/en-us/windows/win32/api/fileapi/nf-fileapi-gettempfilenamea#remarks)
* [GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename)

### Upload small files with buffered model binding to a database

To store binary file data in a database using [Entity Framework](/en-us/ef/core), define a [Byte](/en-us/dotnet/api/system.byte) array property on the entity:

```
public class AppFile
{
    public int Id { get; set; }
    public byte[] Content { get; set; }
}
```

Specify a page model property for the class that includes an [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile):

```
public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

    [BindProperty]
    public BufferedSingleFileUploadDb FileUpload { get; set; }

    ...
}

public class BufferedSingleFileUploadDb
{
    [Required]
    [Display(Name="File")]
    public IFormFile FormFile { get; set; }
}
```

Note

[IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) can be used directly as an action method parameter or as a bound model property. The prior example uses a bound model property.

The `FileUpload` is used in the Razor Pages form:

```
<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>
```

When the form is POSTed to the server, copy the [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) to a stream and save it as a byte array in the database. In the following example, `_dbContext` stores the app's database context:

```
public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 2 MB
        if (memoryStream.Length < 2097152)
        {
            var file = new AppFile()
            {
                Content = memoryStream.ToArray()
            };

            _dbContext.File.Add(file);

            await _dbContext.SaveChangesAsync();
        }
        else
        {
            ModelState.AddModelError("File", "The file is too large.");
        }
    }

    return Page();
}
```

The preceding example is similar to a scenario demonstrated in the sample app:

* `Pages/BufferedSingleFileUploadDb.cshtml`
* `Pages/BufferedSingleFileUploadDb.cshtml.cs`

Warning

Use caution when storing binary data in relational databases, as it can adversely affect performance.

Don't rely on or trust the `FileName` property of [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) without validation. The `FileName` property should only be used for display purposes and only after HTML encoding.

The examples provided don't take into account security considerations. Additional information is provided by the following sections and the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/):

* [Security considerations](#security-considerations)
* [Validation](#validation)

### Upload large files with streaming

For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds.

The [sample application for 9.x](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token.

[MultipartReader](/en-us/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a [FileStream](/en-us/dotnet/api/system.io.filestream)):

```
// Read the boundary from the Content-Type header
var boundary = HeaderUtilities.RemoveQuotes(
    MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value;

// Use MultipartReader to stream data to a destination
var reader = new MultipartReader(boundary, Request.Body);
MultipartSection? section;

while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null)
{
    var contentDisposition = section.GetContentDispositionHeader();

    if (contentDisposition != null && contentDisposition.IsFileDisposition())
    {
        await section.Body.CopyToAsync(outputStream, cancellationToken);
    }
}
```

[IFormFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iformfeature) is a wrapper around [MultipartReader](/en-us/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) that doesn't require you to write manual request body parsing code. You can use its [ReadFormAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iformfeature.readformasync) method to populate the request's form data, then access uploaded files from the built-in collection:

```
// Get the IFormFeature and read the form
var formFeature = Request.HttpContext.Features.GetRequiredFeature<IFormFeature>();
await formFeature.ReadFormAsync(cancellationToken);

// Access the uploaded file (example: first file)
var filePath = Request.Form.Files.First().FileName;

return Results.Ok($"Saved file at {filePath}");
```

For advanced scenarios, manually parse the raw request body using [HttpRequest.BodyReader](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader), which exposes an [`IPipeReader`](/en-us/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample app includes endpoint handlers that use `IPipeReader` in both Minimal APIs and controllers.

The [3.1 example](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadDb.cshtml) demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, another customer filter disables form model binding. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding.

The initial page response loads the form and saves an antiforgery token in a cookie (via the `GenerateAntiforgeryTokenCookieAttribute` attribute). The attribute uses ASP.NET Core's built-in [antiforgery support](../../security/anti-request-forgery?view=aspnetcore-10.0) to set a cookie with a request token:

```
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}
```

The `DisableFormValueModelBindingAttribute` is used to disable model binding:

```
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}
```

In the sample app, `GenerateAntiforgeryTokenCookieAttribute` and `DisableFormValueModelBindingAttribute` are applied as filters to the page application models of `/StreamedSingleFileUploadDb` and `/StreamedSingleFileUploadPhysical` in `Startup.ConfigureServices` using [Razor Pages conventions](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0):

```
services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
            model =>
            {
                model.Filters.Add(
                    new GenerateAntiforgeryTokenCookieAttribute());
                model.Filters.Add(
                    new DisableFormValueModelBindingAttribute());
            });
    options.Conventions
        .AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
            model =>
            {
                model.Filters.Add(
                    new GenerateAntiforgeryTokenCookieAttribute());
                model.Filters.Add(
                    new DisableFormValueModelBindingAttribute());
            });
});
```

Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). The action method works directly with the `Request` property. A `MultipartReader` is used to read each section. Key/value data is stored in a `KeyValueAccumulator`. After the multipart sections are read, the contents of the `KeyValueAccumulator` are used to bind the form data to a model type.

The complete `StreamingController.UploadDatabase` method for streaming to a database with EF Core:

```
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    // Accumulate the form data key-value pairs in the request (formAccumulator).
    var formAccumulator = new KeyValueAccumulator();
    var trustedFileNameForDisplay = string.Empty;
    var untrustedFileNameForStorage = string.Empty;
    var streamedFileContent = Array.Empty<byte>();

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                untrustedFileNameForStorage = contentDisposition.FileName.Value;
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);

                streamedFileContent = 
                    await FileHelpers.ProcessStreamedFile(section, contentDisposition, 
                        ModelState, _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
            }
            else if (MultipartRequestHelper
                .HasFormDataContentDisposition(contentDisposition))
            {
                // Don't limit the key name length because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities
                    .RemoveQuotes(contentDisposition.Name).Value;
                var encoding = GetEncoding(section);

                if (encoding == null)
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by 
                    // MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();

                    if (string.Equals(value, "undefined", 
                        StringComparison.OrdinalIgnoreCase))
                    {
                        value = string.Empty;
                    }

                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > 
                        _defaultFormOptions.ValueCountLimit)
                    {
                        // Form key count limit of 
                        // _defaultFormOptions.ValueCountLimit 
                        // is exceeded.
                        ModelState.AddModelError("File", 
                            $"The request couldn't be processed (Error 3).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to the model
    var formData = new FormData();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);
    var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
        valueProvider: formValueProvider);

    if (!bindingSuccessful)
    {
        ModelState.AddModelError("File", 
            "The request couldn't be processed (Error 5).");
        // Log error

        return BadRequest(ModelState);
    }

    // **WARNING!**
    // In the following example, the file is saved without
    // scanning the file's contents. In most production
    // scenarios, an anti-virus/anti-malware scanner API
    // is used on the file before making the file available
    // for download or for use by other systems. 
    // For more information, see the topic that accompanies 
    // this sample app.

    var file = new AppFile()
    {
        Content = streamedFileContent,
        UntrustedName = untrustedFileNameForStorage,
        Note = formData.Note,
        Size = streamedFileContent.Length, 
        UploadDT = DateTime.UtcNow
    };

    _context.File.Add(file);
    await _context.SaveChangesAsync();

    return Created(nameof(StreamingController), null);
}
```

`MultipartRequestHelper` (`Utilities/MultipartRequestHelper.cs`):

```
using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}
```

The complete `StreamingController.UploadPhysical` method for streaming to a physical location:

```
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems. 
                // For more information, see the topic that accompanies 
                // this sample.

                var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                    section, contentDisposition, ModelState, 
                    _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    return Created(nameof(StreamingController), null);
}
```

In the sample app, `FileHelpers.ProcessStreamedFile` handles validation checks.

## Validation

The sample app's `FileHelpers` class demonstrates several checks for buffered [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and streamed file uploads. For processing [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) buffered file uploads in the sample app, see the `ProcessFormFile` method in the `Utilities/FileHelpers.cs` file. For processing streamed files, see the `ProcessStreamedFile` method in the same file.

Warning

The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.

Although the article sample provides a working example of validation techniques, don't implement the `FileHelpers` class in a production app unless you:

* Fully understand the implementation.
* Modify the implementation as appropriate for the app's environment and specifications.

**Never indiscriminately implement security code in an app without addressing these requirements.**

### Content validation

**Use a third party virus/malware scanning API on uploaded content.**

Scanning files is demanding on server resources in high volume scenarios. If request processing performance is diminished due to file scanning, consider offloading the scanning work to a [background service](../../fundamentals/host/hosted-services?view=aspnetcore-10.0), possibly a service running on a server different from the app's server. Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. When a file passes, the file is moved to the normal file storage location. These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. By using such an approach, the app and app server remain focused on responding to requests.

### File extension validation

The uploaded file's extension should be checked against a list of permitted extensions. For example:

```
private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}
```

### File signature validation

The first few bytes at the start of a file determine a file's signature. These bytes can be used to indicate if the extension matches the content of the file. The sample app checks file signatures for a few common file types. In the following example, the file signature for a JPEG image is checked against the file:

```
private static readonly Dictionary<string, List<byte[]>> _fileSignature = 
    new Dictionary<string, List<byte[]>>
{
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
        }
    },
};

using (var reader = new BinaryReader(uploadedFileData))
{
    var signatures = _fileSignature[ext];
    var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

    return signatures.Any(signature => 
        headerBytes.Take(signature.Length).SequenceEqual(signature));
}
```

To obtain additional file signatures, use a [file signatures database (Google search result)](https://www.google.com/search?q=file+signatures+databases) and official file specifications. Consulting official file specifications might ensure that the selected signatures are valid.

### File name security

Never use a client-supplied file name for saving a file to physical storage. Create a safe file name for the file using [Path.GetRandomFileName](/en-us/dotnet/api/system.io.path.getrandomfilename) or [Path.GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename) to create a full path (including the file name) for temporary storage.

Razor automatically HTML encodes property values for display. The following code is safe to use:

```
@foreach (var file in Model.DatabaseFiles) {
    <tr>
        <td>
            @file.UntrustedName
        </td>
    </tr>
}
```

Outside of Razor, always [HtmlEncode](/en-us/dotnet/api/system.net.webutility.htmlencode) file name content from a user's request.

Many implementations must include a check that the file exists; otherwise, a file of the same name overwrites the file. Supply additional logic to meet your app's specifications.

### Size validation

Limit the size of uploaded files.

In the sample app, the size of the file is limited to 2 MB (indicated in bytes). The limit is supplied via [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) from the `appsettings.json` file:

```
{
  "FileSizeLimit": 2097152
}
```

The `FileSizeLimit` is injected into `PageModel` classes:

```
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

    public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
    {
        _fileSizeLimit = config.GetValue<long>("FileSizeLimit");
    }

    ...
}
```

When a file size exceeds the limit, the file is rejected:

```
if (formFile.Length > _fileSizeLimit)
{
    // The file is too large ... discontinue processing the file
}
```

### Match name attribute value to parameter name of POST method

In non-Razor forms that POST form data or use JavaScript's `FormData` directly, the name specified in the form's element or `FormData` must match the name of the parameter in the controller's action.

In the following example:

* When using an `<input>` element, the `name` attribute is set to the value `battlePlans`:

  ```
  <input type="file" name="battlePlans" multiple>
  ```

* When using `FormData` in JavaScript, the name is set to the value `battlePlans`:

  ```
  var formData = new FormData();

  for (var file in files) {
    formData.append("battlePlans", file, file.name);
  }
  ```

Use a matching name for the parameter of the C# method (`battlePlans`):

* For a Razor Pages page handler method named `Upload`:

  ```
  public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
  ```

* For an MVC POST controller action method:

  ```
  public async Task<IActionResult> Post(List<IFormFile> battlePlans)
  ```

## Server and app configuration

### Multipart body length limit

[MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) sets the limit for the length of each multipart body. Form sections that exceed this limit throw an [InvalidDataException](/en-us/dotnet/api/system.io.invaliddataexception) when parsed. The default is 134,217,728 (128 MB). Customize the limit using the [MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) setting in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<FormOptions>(options =>
{
    // Set the limit to 256 MB
    options.MultipartBodyLengthLimit = 268435456;
});
```

[RequestFormLimitsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestformlimitsattribute) is used to set the [MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) for a single page or action.

In a Razor Pages app, apply the filter with a [convention](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0) in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model =>
            {
                // Set the limit to 256 MB
                model.Filters.Add(
                    new RequestFormLimitsAttribute()
                    {
                        MultipartBodyLengthLimit = 268435456
                    });
            });
});
```

In a Razor Pages app or an MVC app, apply the filter to the page model or action method:

```
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}
```

### Kestrel maximum request body size

For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit using the [MaxRequestBodySize](../../fundamentals/servers/kestrel/options?view=aspnetcore-10.0#maximum-request-body-size) Kestrel server option:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    // Handle requests up to 50 MB
    options.Limits.MaxRequestBodySize = 52428800;
});
```

[RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) is used to set the [MaxRequestBodySize](../../fundamentals/servers/kestrel/options?view=aspnetcore-10.0#maximum-request-body-size) for a single page or action.

In a Razor Pages app, apply the filter with a [convention](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0) in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model =>
            {
                // Handle requests up to 50 MB
                model.Filters.Add(
                    new RequestSizeLimitAttribute(52428800));
            });
});
```

In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:

```
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}
```

The `RequestSizeLimitAttribute` can also be applied using the [`@attribute`](../views/razor?view=aspnetcore-10.0#attribute) Razor directive:

```
@attribute [RequestSizeLimitAttribute(52428800)]
```

### Other Kestrel limits

Other Kestrel limits might apply for apps hosted by Kestrel:

* [Maximum client connections](../../fundamentals/servers/kestrel/options?view=aspnetcore-10.0#maximum-client-connections)
* [Request and response data rates](../../fundamentals/servers/kestrel/options?view=aspnetcore-10.0#minimum-request-body-data-rate)

### IIS

When hosted on IIS in-process, request size limits can exist at both the IIS request filtering module layer and the ASP.NET Core server layer:

* **IIS request filtering (`maxAllowedContentLength`)**: The default IIS request limit is 30,000,000 bytes (\~28.6 MB). Customize this limit in the `web.config` file. IIS inspects incoming requests and rejects any request that exceeds this limit with HTTP 404.13 before the request reaches the ASP.NET Core app:

  ```
  <system.webServer>
    <security>
      <requestFiltering>
        <requestLimits maxAllowedContentLength="52428800" />
      </requestFiltering>
    </security>
  </system.webServer>
  ```

  For more information, see [Request Limits `<requestLimits>`](/en-us/iis/configuration/system.webServer/security/requestFiltering/requestLimits/).

* **ASP.NET Core IIS server options (`IISServerOptions.MaxRequestBodySize`)**: ASP.NET Core also enforces a default request body size limit of 30,000,000 bytes (\~28.6 MB) when hosted in-process. Customize this limit in `Program.cs`:

  ```
  builder.Services.Configure<IISServerOptions>(options =>
  {
      options.MaxRequestBodySize = 52428800; // 50 MB
  });
  ```

Note

When hosted in-process on IIS, both limits apply. If `maxAllowedContentLength` is smaller than `IISServerOptions.MaxRequestBodySize`, IIS rejects requests exceeding `maxAllowedContentLength` before ASP.NET Core processes them. To support larger uploads on IIS, increase both `maxAllowedContentLength` in `web.config` and `IISServerOptions.MaxRequestBodySize` (or apply [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute)).

### Per-request body size limits

To dynamically adjust or remove the request body size limit for a specific request, use the [IHttpMaxRequestBodySizeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature) feature in middleware or an endpoint:

```
app.MapPost("/upload", async (HttpContext context) =>
{
    var maxRequestBodySizeFeature = context.Features.Get<IHttpMaxRequestBodySizeFeature>();

    if (maxRequestBodySizeFeature is { IsReadOnly: false })
    {
        // Set a 100 MB limit for this request, or set to null for unlimited
        maxRequestBodySizeFeature.MaxRequestBodySize = 104857600;
    }

    // Process file upload stream
});
```

* Setting `MaxRequestBodySize` to `null` disables the ASP.NET Core server-side limit for that request.
* The limit must be configured **before** the app begins reading the request body. If `IsReadOnly` is `true`, the request body has already been read and attempting to set `MaxRequestBodySize` throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception).
* Changing `IHttpMaxRequestBodySizeFeature.MaxRequestBodySize` or applying [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) only overrides ASP.NET Core server limits (Kestrel or `IISServerOptions`). It does **not** bypass the IIS module-level `maxAllowedContentLength` limit configured in `web.config`.

### Request body size limits summary

The following table summarizes the request body size limit mechanisms in ASP.NET Core:

| Setting / Mechanism                                                                                                   | Scope / Layer               | How to configure                                         | Default                      | Notes                                                                                             |
| --------------------------------------------------------------------------------------------------------------------- | --------------------------- | -------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------- |
| `maxAllowedContentLength`                                                                                             | IIS Module (IIS level)      | `web.config` (`<requestLimits>`)                         | 30,000,000 bytes (\~28.6 MB) | Checked by IIS before request reaches ASP.NET Core. Rejects with HTTP 404.13.                     |
| `IISServerOptions.MaxRequestBodySize`                                                                                 | ASP.NET Core IIS server     | `builder.Services.Configure<IISServerOptions>`           | 30,000,000 bytes (\~28.6 MB) | Enforced by ASP.NET Core IIS in-process server handler.                                           |
| `KestrelServerOptions.Limits.MaxRequestBodySize`                                                                      | ASP.NET Core Kestrel server | `builder.WebHost.ConfigureKestrel`                       | 30,000,000 bytes (\~28.6 MB) | Enforced by Kestrel server.                                                                       |
| [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute)                     | Action or Razor Page        | `[RequestSizeLimit(bytes)]` or `@attribute`              | N/A                          | Overrides ASP.NET Core server limit per action/page. Cannot bypass IIS `maxAllowedContentLength`. |
| [IHttpMaxRequestBodySizeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature) | Single HTTP Request         | `context.Features.Get<IHttpMaxRequestBodySizeFeature>()` | N/A                          | Dynamically changes or disables (`null`) ASP.NET Core server limit before request body is read.   |

## Troubleshoot

Following are some common problems encountered when working with uploading files and their possible solutions.

### Not Found error when deployed to an IIS server

The following error indicates that the uploaded file exceeds the server's configured content length:

```
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
```

For more information, see the [IIS](#iis) section.

### Connection failure

A connection error and a reset server connection probably indicate that the uploaded file exceeds Kestrel's maximum request body size. For more information, see the [Kestrel maximum request body size](#kestrel-maximum-request-body-size) section. Kestrel client connection limits might also require adjustment.

### Null Reference Exception with IFormFile

If the controller is accepting uploaded files using [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) but the value is `null`, confirm that the HTML form is specifying an `enctype` value of `multipart/form-data`. If this attribute isn't set on the `<form>` element, the file upload doesn't occur and any bound [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) arguments are `null`. Also confirm that the [upload naming in form data matches the app's naming](#match-name-attribute-value-to-parameter-name-of-post-method).

### Stream was too long

The examples in this article rely upon [MemoryStream](/en-us/dotnet/api/system.io.memorystream) to hold the uploaded file's content. The size limit of a `MemoryStream` is `int.MaxValue`. If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single `MemoryStream` for holding an uploaded file's content.

ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Security considerations

Use caution when providing users with the ability to upload files to a server. Cyberattackers may attempt to:

* Execute [denial of service](/en-us/windows-hardware/drivers/ifs/denial-of-service) attacks.
* Upload viruses or malware.
* Compromise networks and servers in other ways.

Security steps that reduce the likelihood of a successful attack are:

* Upload files to a dedicated file upload area, preferably to a non-system drive. A dedicated location makes it easier to impose security restrictions on uploaded files. Disable execute permissions on the file upload location.†
* Do **not** persist uploaded files in the same directory tree as the app.†
* Use a safe file name determined by the app. Don't use a file name provided by the user or the untrusted file name of the uploaded file.† HTML encode the untrusted file name when displaying it. For example, logging the file name or displaying in UI (Razor automatically HTML encodes output).
* Allow only approved file extensions for the app's design specification.†
* Verify that client-side checks are performed on the server.† Client-side checks are easy to circumvent.
* Check the size of an uploaded file. Set a maximum size limit to prevent large uploads.†
* When files shouldn't be overwritten by an uploaded file with the same name, check the file name against the database or physical storage before uploading the file.
* **Run a virus/malware scanner on uploaded content before the file is stored.**

†The sample app demonstrates an approach that meets the criteria.

Warning

Uploading malicious code to a system is frequently the first step to executing code that can:

* Completely gain control of a system.
* Overload a system with the result that the system crashes.
* Compromise user or system data.
* Apply graffiti to a public UI.

For information on reducing vulnerabilities when accepting files from users, see the following resources:

* [Unrestricted File Upload](https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload)
* [Azure Security: Ensure appropriate controls are in place when accepting files from users](/en-us/azure/security/azure-security-threat-modeling-tool-input-validation#controls-users)

For more information on implementing security measures, including examples from the sample app, see the [Validation](#validation) section.

## Storage scenarios

Common storage options for files include:

* Database

  * For small file uploads, a database is often faster than physical storage (file system or network share) options.
  * A database is often more convenient than physical storage options because retrieval of a database record for user data can concurrently supply the file content (for example, an avatar image).
  * A database is potentially less expensive than using a data storage service.

* Physical storage (file system or network share)

  * For large file uploads:

    * Database limits may restrict the size of the upload.
    * Physical storage is often less economical than storage in a database.

  * Physical storage is potentially less expensive than using a data storage service.

  * The app's process must have read and write permissions to the storage location. **Never grant execute permission.**

* Data storage service (for example, [Azure Blob Storage](https://azure.microsoft.com/services/storage/blobs/))

  * Services usually offer improved scalability and resiliency over on-premises solutions that are usually subject to single points of failure.
  * Services are potentially lower cost in large storage infrastructure scenarios.

  For more information, see [Quickstart: Use .NET to create a blob in object storage](/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet).

## File upload scenarios

Two general approaches for uploading files are buffering and streaming.

**Buffering**

The entire file is read into an [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile), which is a C# representation of the file used to process or save the file.

The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. If the size or frequency of file uploads is exhausting app resources, use streaming.

Note

Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.

Buffering small files is covered in the following sections of this topic:

* [Physical storage](#upload-small-files-with-buffered-model-binding-to-physical-storage)
* [Database](#upload-small-files-with-buffered-model-binding-to-a-database)

**Streaming**

The file is received from a multipart request and directly processed or saved by the app. Streaming doesn't improve performance significantly. Streaming reduces the demands for memory or disk space when uploading files.

Streaming large files is covered in the [Upload large files with streaming](#upload-large-files-with-streaming) section.

### Upload small files with buffered model binding to physical storage

To upload small files, use a multipart form or construct a POST request using JavaScript.

The following example demonstrates the use of a Razor Pages form to upload a single file (`Pages/BufferedSingleFileUploadPhysical.cshtml` in the sample app):

```
<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
            <span asp-validation-for="FileUpload.FormFile"></span>
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
```

The following example is analogous to the prior example except that:

* JavaScript's ([Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API)) is used to submit the form's data.
* There's no validation.

```
<form action="BufferedSingleFileUploadPhysical/?handler=Upload" 
      enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;" 
      method="post">
    <dl>
        <dt>
            <label for="FileUpload_FormFile">File</label>
        </dt>
        <dd>
            <input id="FileUpload_FormFile" type="file" 
                name="FileUpload.FormFile" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output name="result"></output>
    </div>
</form>

<script>
  async function AJAXSubmit (oFormElement) {
    var resultElement = oFormElement.elements.namedItem("result");
    const formData = new FormData(oFormElement);

    try {
    const response = await fetch(oFormElement.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      window.location.href = '/';
    }

    resultElement.value = 'Result: ' + response.status + ' ' + 
      response.statusText;
    } catch (error) {
      console.error('Error:', error);
    }
  }
</script>
```

To perform the form POST in JavaScript for clients that [don't support the Fetch API](https://caniuse.com/#feat=fetch), use one of the following approaches:

* Use a Fetch Polyfill (for example, [window.fetch polyfill (github/fetch)](https://github.com/github/fetch)).

* Use `XMLHttpRequest`. For example:

  ```
  <script>
    "use strict";

    function AJAXSubmit (oFormElement) {
      var oReq = new XMLHttpRequest();
      oReq.onload = function(e) { 
      oFormElement.elements.namedItem("result").value = 
        'Result: ' + this.status + ' ' + this.statusText;
      };
      oReq.open("post", oFormElement.action);
      oReq.send(new FormData(oFormElement));
    }
  </script>
  ```

In order to support file uploads, HTML forms must specify an encoding type (`enctype`) of `multipart/form-data`.

For a `files` input element to support uploading multiple files provide the `multiple` attribute on the `<input>` element:

```
<input asp-for="FileUpload.FormFiles" type="file" multiple>
```

The individual files uploaded to the server can be accessed through [Model Binding](model-binding?view=aspnetcore-10.0) using [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile). The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.

[]()

Warning

Do **not** use the `FileName` property of [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) other than for display and logging. When displaying or logging, HTML encode the file name. A cyberattacker can provide a malicious filename, including full paths or relative paths. Applications should:

* Remove the path from the user-supplied filename.
* Save the HTML-encoded, path-removed filename for UI or logging.
* Generate a new random filename for storage.

The following code removes the path from the file name:

```
string untrustedFileName = Path.GetFileName(pathName);
```

The examples provided thus far don't take into account security considerations. Additional information is provided by the following sections and the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/):

* [Security considerations](#security-considerations)
* [Validation](#validation)

When uploading files using model binding and [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile), the action method can accept:

* A single [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile).

* Any of the following collections that represent several files:

  * [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection)
  * [IEnumerable](/en-us/dotnet/api/system.collections.ienumerable)<[IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile)>
  * [List](/en-us/dotnet/api/system.collections.generic.list-1)<[IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile)>

Note

Binding matches form files by name. For example, the HTML `name` value in `<input type="file" name="formFile">` must match the C# parameter/property bound (`FormFile`). For more information, see the [Match name attribute value to parameter name of POST method](#match-name-attribute-value-to-parameter-name-of-post-method) section.

The following example:

* Loops through one or more uploaded files.
* Uses [Path.GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename) to return a full path for a file, including the file name.
* Saves the files to the local file system using a file name generated by the app.
* Returns the total number and size of files uploaded.

```
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            var filePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(filePath))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}
```

Use `Path.GetRandomFileName` to generate a file name without a path. In the following example, the path is obtained from configuration:

```
foreach (var formFile in files)
{
    if (formFile.Length > 0)
    {
        var filePath = Path.Combine(_config["StoredFilesPath"], 
            Path.GetRandomFileName());

        using (var stream = System.IO.File.Create(filePath))
        {
            await formFile.CopyToAsync(stream);
        }
    }
}
```

The path passed to the [FileStream](/en-us/dotnet/api/system.io.filestream) *must* include the file name. If the file name isn't provided, an [UnauthorizedAccessException](/en-us/dotnet/api/system.unauthorizedaccessexception) is thrown at runtime.

Files uploaded using the [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) technique are buffered in memory or on disk on the server before processing. Inside the action method, the [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) contents are accessible as a [Stream](/en-us/dotnet/api/system.io.stream). In addition to the local file system, files can be saved to a network share or to a file storage service, such as [Azure Blob storage](/en-us/azure/visual-studio/vs-storage-aspnet5-getting-started-blobs).

For another example that loops over multiple files for upload and uses safe file names, see `Pages/BufferedMultipleFileUploadPhysical.cshtml.cs` in the sample app.

Warning

[Path.GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename) throws an [IOException](/en-us/dotnet/api/system.io.ioexception) if more than 65,535 files are created without deleting previous temporary files. The limit of 65,535 files is a per-server limit. For more information on this limit on Windows OS, see the remarks in the following topics:

* [GetTempFileNameA function](/en-us/windows/desktop/api/fileapi/nf-fileapi-gettempfilenamea#remarks)
* [GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename)

### Upload small files with buffered model binding to a database

To store binary file data in a database using [Entity Framework](/en-us/ef/core/index), define a [Byte](/en-us/dotnet/api/system.byte) array property on the entity:

```
public class AppFile
{
    public int Id { get; set; }
    public byte[] Content { get; set; }
}
```

Specify a page model property for the class that includes an [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile):

```
public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

    [BindProperty]
    public BufferedSingleFileUploadDb FileUpload { get; set; }

    ...
}

public class BufferedSingleFileUploadDb
{
    [Required]
    [Display(Name="File")]
    public IFormFile FormFile { get; set; }
}
```

Note

[IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) can be used directly as an action method parameter or as a bound model property. The prior example uses a bound model property.

The `FileUpload` is used in the Razor Pages form:

```
<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>
```

When the form is POSTed to the server, copy the [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) to a stream and save it as a byte array in the database. In the following example, `_dbContext` stores the app's database context:

```
public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 2 MB
        if (memoryStream.Length < 2097152)
        {
            var file = new AppFile()
            {
                Content = memoryStream.ToArray()
            };

            _dbContext.File.Add(file);

            await _dbContext.SaveChangesAsync();
        }
        else
        {
            ModelState.AddModelError("File", "The file is too large.");
        }
    }

    return Page();
}
```

The preceding example is similar to a scenario demonstrated in the sample app:

* `Pages/BufferedSingleFileUploadDb.cshtml`
* `Pages/BufferedSingleFileUploadDb.cshtml.cs`

Warning

Use caution when storing binary data in relational databases, as it can adversely impact performance.

Don't rely on or trust the `FileName` property of [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) without validation. The `FileName` property should only be used for display purposes and only after HTML encoding.

The examples provided don't take into account security considerations. Additional information is provided by the following sections and the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/):

* [Security considerations](#security-considerations)
* [Validation](#validation)

### Upload large files with streaming

The following example demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding.

The initial page response loads the form and saves an antiforgery token in a cookie (via the `GenerateAntiforgeryTokenCookieAttribute` attribute). The attribute uses ASP.NET Core's built-in [antiforgery support](../../security/anti-request-forgery?view=aspnetcore-10.0) to set a cookie with a request token:

```
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}
```

The `DisableFormValueModelBindingAttribute` is used to disable model binding:

```
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}
```

In the sample app, `GenerateAntiforgeryTokenCookieAttribute` and `DisableFormValueModelBindingAttribute` are applied as filters to the page application models of `/StreamedSingleFileUploadDb` and `/StreamedSingleFileUploadPhysical` in `Startup.ConfigureServices` using [Razor Pages conventions](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0):

```
services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
            model =>
            {
                model.Filters.Add(
                    new GenerateAntiforgeryTokenCookieAttribute());
                model.Filters.Add(
                    new DisableFormValueModelBindingAttribute());
            });
    options.Conventions
        .AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
            model =>
            {
                model.Filters.Add(
                    new GenerateAntiforgeryTokenCookieAttribute());
                model.Filters.Add(
                    new DisableFormValueModelBindingAttribute());
            });
});
```

Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). The action method works directly with the `Request` property. A `MultipartReader` is used to read each section. Key/value data is stored in a `KeyValueAccumulator`. After the multipart sections are read, the contents of the `KeyValueAccumulator` are used to bind the form data to a model type.

The complete `StreamingController.UploadDatabase` method for streaming to a database with EF Core:

```
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    // Accumulate the form data key-value pairs in the request (formAccumulator).
    var formAccumulator = new KeyValueAccumulator();
    var trustedFileNameForDisplay = string.Empty;
    var untrustedFileNameForStorage = string.Empty;
    var streamedFileContent = Array.Empty<byte>();

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                untrustedFileNameForStorage = contentDisposition.FileName.Value;
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);

                streamedFileContent = 
                    await FileHelpers.ProcessStreamedFile(section, contentDisposition, 
                        ModelState, _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
            }
            else if (MultipartRequestHelper
                .HasFormDataContentDisposition(contentDisposition))
            {
                // Don't limit the key name length because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities
                    .RemoveQuotes(contentDisposition.Name).Value;
                var encoding = GetEncoding(section);

                if (encoding == null)
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by 
                    // MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();

                    if (string.Equals(value, "undefined", 
                        StringComparison.OrdinalIgnoreCase))
                    {
                        value = string.Empty;
                    }

                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > 
                        _defaultFormOptions.ValueCountLimit)
                    {
                        // Form key count limit of 
                        // _defaultFormOptions.ValueCountLimit 
                        // is exceeded.
                        ModelState.AddModelError("File", 
                            $"The request couldn't be processed (Error 3).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to the model
    var formData = new FormData();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);
    var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
        valueProvider: formValueProvider);

    if (!bindingSuccessful)
    {
        ModelState.AddModelError("File", 
            "The request couldn't be processed (Error 5).");
        // Log error

        return BadRequest(ModelState);
    }

    // **WARNING!**
    // In the following example, the file is saved without
    // scanning the file's contents. In most production
    // scenarios, an anti-virus/anti-malware scanner API
    // is used on the file before making the file available
    // for download or for use by other systems. 
    // For more information, see the topic that accompanies 
    // this sample app.

    var file = new AppFile()
    {
        Content = streamedFileContent,
        UntrustedName = untrustedFileNameForStorage,
        Note = formData.Note,
        Size = streamedFileContent.Length, 
        UploadDT = DateTime.UtcNow
    };

    _context.File.Add(file);
    await _context.SaveChangesAsync();

    return Created(nameof(StreamingController), null);
}
```

`MultipartRequestHelper` (`Utilities/MultipartRequestHelper.cs`):

```
using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}
```

The complete `StreamingController.UploadPhysical` method for streaming to a physical location:

```
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems. 
                // For more information, see the topic that accompanies 
                // this sample.

                var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                    section, contentDisposition, ModelState, 
                    _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    return Created(nameof(StreamingController), null);
}
```

In the sample app, validation checks are handled by `FileHelpers.ProcessStreamedFile`.

## Validation

The sample app's `FileHelpers` class demonstrates a several checks for buffered [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and streamed file uploads. For processing [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) buffered file uploads in the sample app, see the `ProcessFormFile` method in the `Utilities/FileHelpers.cs` file. For processing streamed files, see the `ProcessStreamedFile` method in the same file.

Warning

The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.

Although the topic sample provides a working example of validation techniques, don't implement the `FileHelpers` class in a production app unless you:

* Fully understand the implementation.
* Modify the implementation as appropriate for the app's environment and specifications.

**Never indiscriminately implement security code in an app without addressing these requirements.**

### Content validation

**Use a third party virus/malware scanning API on uploaded content.**

Scanning files is demanding on server resources in high volume scenarios. If request processing performance is diminished due to file scanning, consider offloading the scanning work to a [background service](../../fundamentals/host/hosted-services?view=aspnetcore-10.0), possibly a service running on a server different from the app's server. Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. When a file passes, the file is moved to the normal file storage location. These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. By using such an approach, the app and app server remain focused on responding to requests.

### File extension validation

The uploaded file's extension should be checked against a list of permitted extensions. For example:

```
private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}
```

### File signature validation

A file's signature is determined by the first few bytes at the start of a file. These bytes can be used to indicate if the extension matches the content of the file. The sample app checks file signatures for a few common file types. In the following example, the file signature for a JPEG image is checked against the file:

```
private static readonly Dictionary<string, List<byte[]>> _fileSignature = 
    new Dictionary<string, List<byte[]>>
{
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
        }
    },
};

using (var reader = new BinaryReader(uploadedFileData))
{
    var signatures = _fileSignature[ext];
    var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

    return signatures.Any(signature => 
        headerBytes.Take(signature.Length).SequenceEqual(signature));
}
```

To obtain additional file signatures, use a [file signatures database (Google search result)](https://www.google.com/search?q=file+signatures+databases) and official file specifications. Consulting official file specifications may ensure that the selected signatures are valid.

### File name security

Never use a client-supplied file name for saving a file to physical storage. Create a safe file name for the file using [Path.GetRandomFileName](/en-us/dotnet/api/system.io.path.getrandomfilename) or [Path.GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename) to create a full path (including the file name) for temporary storage.

Razor automatically HTML encodes property values for display. The following code is safe to use:

```
@foreach (var file in Model.DatabaseFiles) {
    <tr>
        <td>
            @file.UntrustedName
        </td>
    </tr>
}
```

Outside of Razor, always [HtmlEncode](/en-us/dotnet/api/system.net.webutility.htmlencode) file name content from a user's request.

Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. Supply additional logic to meet your app's specifications.

### Size validation

Limit the size of uploaded files.

In the sample app, the size of the file is limited to 2 MB (indicated in bytes). The limit is supplied via [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) from the `appsettings.json` file:

```
{
  "FileSizeLimit": 2097152
}
```

The `FileSizeLimit` is injected into `PageModel` classes:

```
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

    public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
    {
        _fileSizeLimit = config.GetValue<long>("FileSizeLimit");
    }

    ...
}
```

When a file size exceeds the limit, the file is rejected:

```
if (formFile.Length > _fileSizeLimit)
{
    // The file is too large ... discontinue processing the file
}
```

### Match name attribute value to parameter name of POST method

In non-Razor forms that POST form data or use JavaScript's `FormData` directly, the name specified in the form's element or `FormData` must match the name of the parameter in the controller's action.

In the following example:

* When using an `<input>` element, the `name` attribute is set to the value `battlePlans`:

  ```
  <input type="file" name="battlePlans" multiple>
  ```

* When using `FormData` in JavaScript, the name is set to the value `battlePlans`:

  ```
  var formData = new FormData();

  for (var file in files) {
    formData.append("battlePlans", file, file.name);
  }
  ```

Use a matching name for the parameter of the C# method (`battlePlans`):

* For a Razor Pages page handler method named `Upload`:

  ```
  public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
  ```

* For an MVC POST controller action method:

  ```
  public async Task<IActionResult> Post(List<IFormFile> battlePlans)
  ```

## Server and app configuration

### Multipart body length limit

[MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) sets the limit for the length of each multipart body. Form sections that exceed this limit throw an [InvalidDataException](/en-us/dotnet/api/system.io.invaliddataexception) when parsed. The default is 134,217,728 (128 MB). Customize the limit using the [MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) setting in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<FormOptions>(options =>
    {
        // Set the limit to 256 MB
        options.MultipartBodyLengthLimit = 268435456;
    });
}
```

[RequestFormLimitsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestformlimitsattribute) is used to set the [MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) for a single page or action.

In a Razor Pages app, apply the filter with a [convention](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0) in `Startup.ConfigureServices`:

```
services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model.Filters.Add(
                new RequestFormLimitsAttribute()
                {
                    // Set the limit to 256 MB
                    MultipartBodyLengthLimit = 268435456
                });
});
```

In a Razor Pages app or an MVC app, apply the filter to the page model or action method:

```
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}
```

### Kestrel maximum request body size

For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit using the [MaxRequestBodySize](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#maximum-request-body-size) Kestrel server option:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel((context, options) =>
            {
                // Handle requests up to 50 MB
                options.Limits.MaxRequestBodySize = 52428800;
            })
            .UseStartup<Startup>();
        });
```

[RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) is used to set the [MaxRequestBodySize](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#maximum-request-body-size) for a single page or action.

In a Razor Pages app, apply the filter with a [convention](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0) in `Startup.ConfigureServices`:

```
services.AddRazorPages(options =>
{
    options.Conventions
        .AddPageApplicationModelConvention("/FileUploadPage",
            model =>
            {
                // Handle requests up to 50 MB
                model.Filters.Add(
                    new RequestSizeLimitAttribute(52428800));
            });
});
```

In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:

```
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}
```

The `RequestSizeLimitAttribute` can also be applied using the [`@attribute`](../views/razor?view=aspnetcore-10.0#attribute) Razor directive:

```
@attribute [RequestSizeLimitAttribute(52428800)]
```

### Other Kestrel limits

Other Kestrel limits may apply for apps hosted by Kestrel:

* [Maximum client connections](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#maximum-client-connections)
* [Request and response data rates](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#minimum-request-body-data-rate)

### IIS

The default request limit (`maxAllowedContentLength`) is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit in the `web.config` file. In the following example, the limit is set to 50 MB (52,428,800 bytes):

```
<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="52428800" />
    </requestFiltering>
  </security>
</system.webServer>
```

The `maxAllowedContentLength` setting only applies to IIS. For more information, see [Request Limits `<requestLimits>`](/en-us/iis/configuration/system.webServer/security/requestFiltering/requestLimits/).

Increase the maximum request body size for the HTTP request by setting [IISServerOptions.MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisserveroptions.maxrequestbodysize) in `Startup.ConfigureServices`. In the following example, the limit is set to 50 MB (52,428,800 bytes):

```
services.Configure<IISServerOptions>(options =>
{
    options.MaxRequestBodySize = 52428800;
});
```

For more information, see [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0#iis-options).

## Troubleshoot

Below are some common problems encountered when working with uploading files and their possible solutions.

### Not Found error when deployed to an IIS server

The following error indicates that the uploaded file exceeds the server's configured content length:

```
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
```

For more information, see the [IIS](#iis) section.

### Connection failure

A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. For more information, see the [Kestrel maximum request body size](#kestrel-maximum-request-body-size) section. Kestrel client connection limits may also require adjustment.

### Null Reference Exception with IFormFile

If the controller is accepting uploaded files using [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) but the value is `null`, confirm that the HTML form is specifying an `enctype` value of `multipart/form-data`. If this attribute isn't set on the `<form>` element, the file upload doesn't occur and any bound [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) arguments are `null`. Also confirm that the [upload naming in form data matches the app's naming](#match-name-attribute-value-to-parameter-name-of-post-method).

### Stream was too long

The examples in this topic rely upon [MemoryStream](/en-us/dotnet/api/system.io.memorystream) to hold the uploaded file's content. The size limit of a `MemoryStream` is `int.MaxValue`. If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single `MemoryStream` for holding an uploaded file's content.

ASP.NET Core supports uploading one or more files using buffered model binding for smaller files and unbuffered streaming for larger files.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Security considerations

Use caution when providing users with the ability to upload files to a server. Cyberattackers may attempt to:

* Execute [denial of service](/en-us/windows-hardware/drivers/ifs/denial-of-service) attacks.
* Upload viruses or malware.
* Compromise networks and servers in other ways.

Security steps that reduce the likelihood of a successful attack are:

* Upload files to a dedicated file upload area, preferably to a non-system drive. A dedicated location makes it easier to impose security restrictions on uploaded files. Disable execute permissions on the file upload location.†
* Do **not** persist uploaded files in the same directory tree as the app.†
* Use a safe file name determined by the app. Don't use a file name provided by the user or the untrusted file name of the uploaded file.† HTML encode the untrusted file name when displaying it. For example, logging the file name or displaying in UI (Razor automatically HTML encodes output).
* Allow only approved file extensions for the app's design specification.†
* Verify that client-side checks are performed on the server.† Client-side checks are easy to circumvent.
* Check the size of an uploaded file. Set a maximum size limit to prevent large uploads.†
* When files shouldn't be overwritten by an uploaded file with the same name, check the file name against the database or physical storage before uploading the file.
* **Run a virus/malware scanner on uploaded content before the file is stored.**

†The sample app demonstrates an approach that meets the criteria.

Warning

Uploading malicious code to a system is frequently the first step to executing code that can:

* Completely gain control of a system.
* Overload a system with the result that the system crashes.
* Compromise user or system data.
* Apply graffiti to a public UI.

For information on reducing vulnerabilities when accepting files from users, see the following resources:

* [Unrestricted File Upload](https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload)
* [Azure Security: Ensure appropriate controls are in place when accepting files from users](/en-us/azure/security/azure-security-threat-modeling-tool-input-validation#controls-users)

For more information on implementing security measures, including examples from the sample app, see the [Validation](#validation) section.

## Storage scenarios

Common storage options for files include:

* Database

  * For small file uploads, a database is often faster than physical storage (file system or network share) options.
  * A database is often more convenient than physical storage options because retrieval of a database record for user data can concurrently supply the file content (for example, an avatar image).
  * A database is potentially less expensive than using a data storage service.

* Physical storage (file system or network share)

  * For large file uploads:

    * Database limits may restrict the size of the upload.
    * Physical storage is often less economical than storage in a database.

  * Physical storage is potentially less expensive than using a data storage service.

  * The app's process must have read and write permissions to the storage location. **Never grant execute permission.**

* Data storage service (for example, [Azure Blob Storage](https://azure.microsoft.com/services/storage/blobs/))

  * Services usually offer improved scalability and resiliency over on-premises solutions that are usually subject to single points of failure.
  * Services are potentially lower cost in large storage infrastructure scenarios.

  For more information, see [Quickstart: Use .NET to create a blob in object storage](/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet). The topic demonstrates [`UploadFromFileAsync`](/en-us/dotnet/api/microsoft.azure.storage.file.cloudfile.uploadfromfileasync), but [`UploadFromStreamAsync`](/en-us/dotnet/api/microsoft.azure.storage.file.cloudfile.uploadfromstreamasync) can be used to save a [FileStream](/en-us/dotnet/api/system.io.filestream) to blob storage when working with a [Stream](/en-us/dotnet/api/system.io.stream).

## File upload scenarios

Two general approaches for uploading files are buffering and streaming.

**Buffering**

The entire file is read into an [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile), which is a C# representation of the file used to process or save the file.

The resources (disk, memory) used by file uploads depend on the number and size of concurrent file uploads. If an app attempts to buffer too many uploads, the site crashes when it runs out of memory or disk space. If the size or frequency of file uploads is exhausting app resources, use streaming.

Note

Any single buffered file exceeding 64 KB is moved from memory to a temp file on disk.

Buffering small files is covered in the following sections of this topic:

* [Physical storage](#upload-small-files-with-buffered-model-binding-to-physical-storage)
* [Database](#upload-small-files-with-buffered-model-binding-to-a-database)

**Streaming**

The file is received from a multipart request and directly processed or saved by the app. Streaming doesn't improve performance significantly. Streaming reduces the demands for memory or disk space when uploading files.

Streaming large files is covered in the [Upload large files with streaming](#upload-large-files-with-streaming) section.

### Upload small files with buffered model binding to physical storage

To upload small files, use a multipart form or construct a POST request using JavaScript.

The following example demonstrates the use of a Razor Pages form to upload a single file (`Pages/BufferedSingleFileUploadPhysical.cshtml` in the sample app):

```
<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
            <span asp-validation-for="FileUpload.FormFile"></span>
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
```

The following example is analogous to the prior example except that:

* JavaScript's ([Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API)) is used to submit the form's data.
* There's no validation.

```
<form action="BufferedSingleFileUploadPhysical/?handler=Upload" 
      enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;" 
      method="post">
    <dl>
        <dt>
            <label for="FileUpload_FormFile">File</label>
        </dt>
        <dd>
            <input id="FileUpload_FormFile" type="file" 
                name="FileUpload.FormFile" />
        </dd>
    </dl>

    <input class="btn" type="submit" value="Upload" />

    <div style="margin-top:15px">
        <output name="result"></output>
    </div>
</form>

<script>
  async function AJAXSubmit (oFormElement) {
    var resultElement = oFormElement.elements.namedItem("result");
    const formData = new FormData(oFormElement);

    try {
    const response = await fetch(oFormElement.action, {
      method: 'POST',
      body: formData
    });

    if (response.ok) {
      window.location.href = '/';
    }

    resultElement.value = 'Result: ' + response.status + ' ' + 
      response.statusText;
    } catch (error) {
      console.error('Error:', error);
    }
  }
</script>
```

To perform the form POST in JavaScript for clients that [don't support the Fetch API](https://caniuse.com/#feat=fetch), use one of the following approaches:

* Use a Fetch Polyfill (for example, [window.fetch polyfill (github/fetch)](https://github.com/github/fetch)).

* Use `XMLHttpRequest`. For example:

  ```
  <script>
    "use strict";

    function AJAXSubmit (oFormElement) {
      var oReq = new XMLHttpRequest();
      oReq.onload = function(e) { 
      oFormElement.elements.namedItem("result").value = 
        'Result: ' + this.status + ' ' + this.statusText;
      };
      oReq.open("post", oFormElement.action);
      oReq.send(new FormData(oFormElement));
    }
  </script>
  ```

In order to support file uploads, HTML forms must specify an encoding type (`enctype`) of `multipart/form-data`.

For a `files` input element to support uploading multiple files provide the `multiple` attribute on the `<input>` element:

```
<input asp-for="FileUpload.FormFiles" type="file" multiple>
```

The individual files uploaded to the server can be accessed through [Model Binding](model-binding?view=aspnetcore-10.0) using [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile). The sample app demonstrates multiple buffered file uploads for database and physical storage scenarios.

[]()

Warning

Do **not** use the `FileName` property of [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) other than for display and logging. When displaying or logging, HTML encode the file name. A cyberattacker can provide a malicious filename, including full paths or relative paths. Applications should:

* Remove the path from the user-supplied filename.
* Save the HTML-encoded, path-removed filename for UI or logging.
* Generate a new random filename for storage.

The following code removes the path from the file name:

```
string untrustedFileName = Path.GetFileName(pathName);
```

The examples provided thus far don't take into account security considerations. Additional information is provided by the following sections and the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/):

* [Security considerations](#security-considerations)
* [Validation](#validation)

When uploading files using model binding and [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile), the action method can accept:

* A single [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile).

* Any of the following collections that represent several files:

  * [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection)
  * [IEnumerable](/en-us/dotnet/api/system.collections.ienumerable)<[IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile)>
  * [List](/en-us/dotnet/api/system.collections.generic.list-1)<[IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile)>

Note

Binding matches form files by name. For example, the HTML `name` value in `<input type="file" name="formFile">` must match the C# parameter/property bound (`FormFile`). For more information, see the [Match name attribute value to parameter name of POST method](#match-name-attribute-value-to-parameter-name-of-post-method) section.

The following example:

* Loops through one or more uploaded files.
* Uses [Path.GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename) to return a full path for a file, including the file name.
* Saves the files to the local file system using a file name generated by the app.
* Returns the total number and size of files uploaded.

```
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            var filePath = Path.GetTempFileName();

            using (var stream = System.IO.File.Create(filePath))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // Process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size });
}
```

Use `Path.GetRandomFileName` to generate a file name without a path. In the following example, the path is obtained from configuration:

```
foreach (var formFile in files)
{
    if (formFile.Length > 0)
    {
        var filePath = Path.Combine(_config["StoredFilesPath"], 
            Path.GetRandomFileName());

        using (var stream = System.IO.File.Create(filePath))
        {
            await formFile.CopyToAsync(stream);
        }
    }
}
```

The path passed to the [FileStream](/en-us/dotnet/api/system.io.filestream) *must* include the file name. If the file name isn't provided, an [UnauthorizedAccessException](/en-us/dotnet/api/system.unauthorizedaccessexception) is thrown at runtime.

Files uploaded using the [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) technique are buffered in memory or on disk on the server before processing. Inside the action method, the [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) contents are accessible as a [Stream](/en-us/dotnet/api/system.io.stream). In addition to the local file system, files can be saved to a network share or to a file storage service, such as [Azure Blob storage](/en-us/azure/visual-studio/vs-storage-aspnet5-getting-started-blobs).

For another example that loops over multiple files for upload and uses safe file names, see `Pages/BufferedMultipleFileUploadPhysical.cshtml.cs` in the sample app.

Warning

[Path.GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename) throws an [IOException](/en-us/dotnet/api/system.io.ioexception) if more than 65,535 files are created without deleting previous temporary files. The limit of 65,535 files is a per-server limit. For more information on this limit on Windows OS, see the remarks in the following topics:

* [GetTempFileNameA function](/en-us/windows/desktop/api/fileapi/nf-fileapi-gettempfilenamea#remarks)
* [GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename)

### Upload small files with buffered model binding to a database

To store binary file data in a database using [Entity Framework](/en-us/ef/core/index), define a [Byte](/en-us/dotnet/api/system.byte) array property on the entity:

```
public class AppFile
{
    public int Id { get; set; }
    public byte[] Content { get; set; }
}
```

Specify a page model property for the class that includes an [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile):

```
public class BufferedSingleFileUploadDbModel : PageModel
{
    ...

    [BindProperty]
    public BufferedSingleFileUploadDb FileUpload { get; set; }

    ...
}

public class BufferedSingleFileUploadDb
{
    [Required]
    [Display(Name="File")]
    public IFormFile FormFile { get; set; }
}
```

Note

[IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) can be used directly as an action method parameter or as a bound model property. The prior example uses a bound model property.

The `FileUpload` is used in the Razor Pages form:

```
<form enctype="multipart/form-data" method="post">
    <dl>
        <dt>
            <label asp-for="FileUpload.FormFile"></label>
        </dt>
        <dd>
            <input asp-for="FileUpload.FormFile" type="file">
        </dd>
    </dl>
    <input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>
```

When the form is POSTed to the server, copy the [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) to a stream and save it as a byte array in the database. In the following example, `_dbContext` stores the app's database context:

```
public async Task<IActionResult> OnPostUploadAsync()
{
    using (var memoryStream = new MemoryStream())
    {
        await FileUpload.FormFile.CopyToAsync(memoryStream);

        // Upload the file if less than 2 MB
        if (memoryStream.Length < 2097152)
        {
            var file = new AppFile()
            {
                Content = memoryStream.ToArray()
            };

            _dbContext.File.Add(file);

            await _dbContext.SaveChangesAsync();
        }
        else
        {
            ModelState.AddModelError("File", "The file is too large.");
        }
    }

    return Page();
}
```

The preceding example is similar to a scenario demonstrated in the sample app:

* `Pages/BufferedSingleFileUploadDb.cshtml`
* `Pages/BufferedSingleFileUploadDb.cshtml.cs`

Warning

Use caution when storing binary data in relational databases, as it can adversely impact performance.

Don't rely on or trust the `FileName` property of [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) without validation. The `FileName` property should only be used for display purposes and only after HTML encoding.

The examples provided don't take into account security considerations. Additional information is provided by the following sections and the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/):

* [Security considerations](#security-considerations)
* [Validation](#validation)

### Upload large files with streaming

The following example demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding.

The initial page response loads the form and saves an antiforgery token in a cookie (via the `GenerateAntiforgeryTokenCookieAttribute` attribute). The attribute uses ASP.NET Core's built-in [antiforgery support](../../security/anti-request-forgery?view=aspnetcore-10.0) to set a cookie with a request token:

```
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // Send the request token as a JavaScript-readable cookie
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        context.HttpContext.Response.Cookies.Append(
            "RequestVerificationToken",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
    }
}
```

The `DisableFormValueModelBindingAttribute` is used to disable model binding:

```
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}
```

In the sample app, `GenerateAntiforgeryTokenCookieAttribute` and `DisableFormValueModelBindingAttribute` are applied as filters to the page application models of `/StreamedSingleFileUploadDb` and `/StreamedSingleFileUploadPhysical` in `Startup.ConfigureServices` using [Razor Pages conventions](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0):

```
services.AddMvc()
    .AddRazorPagesOptions(options =>
        {
            options.Conventions
                .AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
                    model =>
                    {
                        model.Filters.Add(
                            new GenerateAntiforgeryTokenCookieAttribute());
                        model.Filters.Add(
                            new DisableFormValueModelBindingAttribute());
                    });
            options.Conventions
                .AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
                    model =>
                    {
                        model.Filters.Add(
                            new GenerateAntiforgeryTokenCookieAttribute());
                        model.Filters.Add(
                            new DisableFormValueModelBindingAttribute());
                    });
        })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
```

Since model binding doesn't read the form, parameters that are bound from the form don't bind (query, route, and header continue to work). The action method works directly with the `Request` property. A `MultipartReader` is used to read each section. Key/value data is stored in a `KeyValueAccumulator`. After the multipart sections are read, the contents of the `KeyValueAccumulator` are used to bind the form data to a model type.

The complete `StreamingController.UploadDatabase` method for streaming to a database with EF Core:

```
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    // Accumulate the form data key-value pairs in the request (formAccumulator).
    var formAccumulator = new KeyValueAccumulator();
    var trustedFileNameForDisplay = string.Empty;
    var untrustedFileNameForStorage = string.Empty;
    var streamedFileContent = Array.Empty<byte>();

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                untrustedFileNameForStorage = contentDisposition.FileName.Value;
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);

                streamedFileContent = 
                    await FileHelpers.ProcessStreamedFile(section, contentDisposition, 
                        ModelState, _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
            }
            else if (MultipartRequestHelper
                .HasFormDataContentDisposition(contentDisposition))
            {
                // Don't limit the key name length because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities
                    .RemoveQuotes(contentDisposition.Name).Value;
                var encoding = GetEncoding(section);

                if (encoding == null)
                {
                    ModelState.AddModelError("File", 
                        $"The request couldn't be processed (Error 2).");
                    // Log error

                    return BadRequest(ModelState);
                }

                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by 
                    // MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();

                    if (string.Equals(value, "undefined", 
                        StringComparison.OrdinalIgnoreCase))
                    {
                        value = string.Empty;
                    }

                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > 
                        _defaultFormOptions.ValueCountLimit)
                    {
                        // Form key count limit of 
                        // _defaultFormOptions.ValueCountLimit 
                        // is exceeded.
                        ModelState.AddModelError("File", 
                            $"The request couldn't be processed (Error 3).");
                        // Log error

                        return BadRequest(ModelState);
                    }
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to the model
    var formData = new FormData();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);
    var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
        valueProvider: formValueProvider);

    if (!bindingSuccessful)
    {
        ModelState.AddModelError("File", 
            "The request couldn't be processed (Error 5).");
        // Log error

        return BadRequest(ModelState);
    }

    // **WARNING!**
    // In the following example, the file is saved without
    // scanning the file's contents. In most production
    // scenarios, an anti-virus/anti-malware scanner API
    // is used on the file before making the file available
    // for download or for use by other systems. 
    // For more information, see the topic that accompanies 
    // this sample app.

    var file = new AppFile()
    {
        Content = streamedFileContent,
        UntrustedName = untrustedFileNameForStorage,
        Note = formData.Note,
        Size = streamedFileContent.Length, 
        UploadDT = DateTime.UtcNow
    };

    _context.File.Add(file);
    await _context.SaveChangesAsync();

    return Created(nameof(StreamingController), null);
}
```

`MultipartRequestHelper` (`Utilities/MultipartRequestHelper.cs`):

```
using System;
using System.IO;
using Microsoft.Net.Http.Headers;

namespace SampleApp.Utilities
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && string.IsNullOrEmpty(contentDisposition.FileName.Value)
                && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
        }

        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                && contentDisposition.DispositionType.Equals("form-data")
                && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                    || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
        }
    }
}
```

The complete `StreamingController.UploadPhysical` method for streaming to a physical location:

```
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        ModelState.AddModelError("File", 
            $"The request couldn't be processed (Error 1).");
        // Log error

        return BadRequest(ModelState);
    }

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        var hasContentDispositionHeader = 
            ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

        if (hasContentDispositionHeader)
        {
            // This check assumes that there's a file
            // present without form data. If form data
            // is present, this method immediately fails
            // and returns the model error.
            if (!MultipartRequestHelper
                .HasFileContentDisposition(contentDisposition))
            {
                ModelState.AddModelError("File", 
                    $"The request couldn't be processed (Error 2).");
                // Log error

                return BadRequest(ModelState);
            }
            else
            {
                // Don't trust the file name sent by the client. To display
                // the file name, HTML-encode the value.
                var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                        contentDisposition.FileName.Value);
                var trustedFileNameForFileStorage = Path.GetRandomFileName();

                // **WARNING!**
                // In the following example, the file is saved without
                // scanning the file's contents. In most production
                // scenarios, an anti-virus/anti-malware scanner API
                // is used on the file before making the file available
                // for download or for use by other systems. 
                // For more information, see the topic that accompanies 
                // this sample.

                var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                    section, contentDisposition, ModelState, 
                    _permittedExtensions, _fileSizeLimit);

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                using (var targetStream = System.IO.File.Create(
                    Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                {
                    await targetStream.WriteAsync(streamedFileContent);

                    _logger.LogInformation(
                        "Uploaded file '{TrustedFileNameForDisplay}' saved to " +
                        "'{TargetFilePath}' as {TrustedFileNameForFileStorage}", 
                        trustedFileNameForDisplay, _targetFilePath, 
                        trustedFileNameForFileStorage);
                }
            }
        }

        // Drain any remaining section body that hasn't been consumed and
        // read the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    return Created(nameof(StreamingController), null);
}
```

In the sample app, validation checks are handled by `FileHelpers.ProcessStreamedFile`.

## Validation

The sample app's `FileHelpers` class demonstrates a several checks for buffered [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and streamed file uploads. For processing [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) buffered file uploads in the sample app, see the `ProcessFormFile` method in the `Utilities/FileHelpers.cs` file. For processing streamed files, see the `ProcessStreamedFile` method in the same file.

Warning

The validation processing methods demonstrated in the sample app don't scan the content of uploaded files. In most production scenarios, a virus/malware scanner API is used on the file before making the file available to users or other systems.

Although the topic sample provides a working example of validation techniques, don't implement the `FileHelpers` class in a production app unless you:

* Fully understand the implementation.
* Modify the implementation as appropriate for the app's environment and specifications.

**Never indiscriminately implement security code in an app without addressing these requirements.**

### Content validation

**Use a third party virus/malware scanning API on uploaded content.**

Scanning files is demanding on server resources in high volume scenarios. If request processing performance is diminished due to file scanning, consider offloading the scanning work to a [background service](../../fundamentals/host/hosted-services?view=aspnetcore-10.0), possibly a service running on a server different from the app's server. Typically, uploaded files are held in a quarantined area until the background virus scanner checks them. When a file passes, the file is moved to the normal file storage location. These steps are usually performed in conjunction with a database record that indicates the scanning status of a file. By using such an approach, the app and app server remain focused on responding to requests.

### File extension validation

The uploaded file's extension should be checked against a list of permitted extensions. For example:

```
private string[] permittedExtensions = { ".txt", ".pdf" };

var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();

if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
    // The extension is invalid ... discontinue processing the file
}
```

### File signature validation

A file's signature is determined by the first few bytes at the start of a file. These bytes can be used to indicate if the extension matches the content of the file. The sample app checks file signatures for a few common file types. In the following example, the file signature for a JPEG image is checked against the file:

```
private static readonly Dictionary<string, List<byte[]>> _fileSignature = 
    new Dictionary<string, List<byte[]>>
{
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
        }
    },
};

using (var reader = new BinaryReader(uploadedFileData))
{
    var signatures = _fileSignature[ext];
    var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));

    return signatures.Any(signature => 
        headerBytes.Take(signature.Length).SequenceEqual(signature));
}
```

To obtain additional file signatures, use a [file signatures database (Google search result)](https://www.google.com/search?q=file+signatures+databases) and official file specifications. Consulting official file specifications may ensure that the selected signatures are valid.

### File name security

Never use a client-supplied file name for saving a file to physical storage. Create a safe file name for the file using [Path.GetRandomFileName](/en-us/dotnet/api/system.io.path.getrandomfilename) or [Path.GetTempFileName](/en-us/dotnet/api/system.io.path.gettempfilename) to create a full path (including the file name) for temporary storage.

Razor automatically HTML encodes property values for display. The following code is safe to use:

```
@foreach (var file in Model.DatabaseFiles) {
    <tr>
        <td>
            @file.UntrustedName
        </td>
    </tr>
}
```

Outside of Razor, always [HtmlEncode](/en-us/dotnet/api/system.net.webutility.htmlencode) file name content from a user's request.

Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. Supply additional logic to meet your app's specifications.

### Size validation

Limit the size of uploaded files.

In the sample app, the size of the file is limited to 2 MB (indicated in bytes). The limit is supplied via [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) from the `appsettings.json` file:

```
{
  "FileSizeLimit": 2097152
}
```

The `FileSizeLimit` is injected into `PageModel` classes:

```
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    private readonly long _fileSizeLimit;

    public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
    {
        _fileSizeLimit = config.GetValue<long>("FileSizeLimit");
    }

    ...
}
```

When a file size exceeds the limit, the file is rejected:

```
if (formFile.Length > _fileSizeLimit)
{
    // The file is too large ... discontinue processing the file
}
```

### Match name attribute value to parameter name of POST method

In non-Razor forms that POST form data or use JavaScript's `FormData` directly, the name specified in the form's element or `FormData` must match the name of the parameter in the controller's action.

In the following example:

* When using an `<input>` element, the `name` attribute is set to the value `battlePlans`:

  ```
  <input type="file" name="battlePlans" multiple>
  ```

* When using `FormData` in JavaScript, the name is set to the value `battlePlans`:

  ```
  var formData = new FormData();

  for (var file in files) {
    formData.append("battlePlans", file, file.name);
  }
  ```

Use a matching name for the parameter of the C# method (`battlePlans`):

* For a Razor Pages page handler method named `Upload`:

  ```
  public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
  ```

* For an MVC POST controller action method:

  ```
  public async Task<IActionResult> Post(List<IFormFile> battlePlans)
  ```

## Server and app configuration

### Multipart body length limit

[MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) sets the limit for the length of each multipart body. Form sections that exceed this limit throw an [InvalidDataException](/en-us/dotnet/api/system.io.invaliddataexception) when parsed. The default is 134,217,728 (128 MB). Customize the limit using the [MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) setting in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<FormOptions>(options =>
    {
        // Set the limit to 256 MB
        options.MultipartBodyLengthLimit = 268435456;
    });
}
```

[RequestFormLimitsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestformlimitsattribute) is used to set the [MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) for a single page or action.

In a Razor Pages app, apply the filter with a [convention](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0) in `Startup.ConfigureServices`:

```
services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions
            .AddPageApplicationModelConvention("/FileUploadPage",
                model.Filters.Add(
                    new RequestFormLimitsAttribute()
                    {
                        // Set the limit to 256 MB
                        MultipartBodyLengthLimit = 268435456
                    });
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
```

In a Razor Pages app or an MVC app, apply the filter to the page model or action method:

```
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}
```

### Kestrel maximum request body size

For apps hosted by Kestrel, the default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit using the [MaxRequestBodySize](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#maximum-request-body-size) Kestrel server option:

```
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureKestrel((context, options) =>
        {
            // Handle requests up to 50 MB
            options.Limits.MaxRequestBodySize = 52428800;
        });
```

[RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) is used to set the [MaxRequestBodySize](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#maximum-request-body-size) for a single page or action.

In a Razor Pages app, apply the filter with a [convention](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0) in `Startup.ConfigureServices`:

```
services.AddMvc()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions
            .AddPageApplicationModelConvention("/FileUploadPage",
                model =>
                {
                    // Handle requests up to 50 MB
                    model.Filters.Add(
                        new RequestSizeLimitAttribute(52428800));
                });
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
```

In a Razor pages app or an MVC app, apply the filter to the page handler class or action method:

```
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
    ...
}
```

### Other Kestrel limits

Other Kestrel limits may apply for apps hosted by Kestrel:

* [Maximum client connections](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#maximum-client-connections)
* [Request and response data rates](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#minimum-request-body-data-rate)

### IIS

The default request limit (`maxAllowedContentLength`) is 30,000,000 bytes, which is approximately 28.6 MB. Customize the limit in the `web.config` file. In the following example, the limit is set to 50 MB (52,428,800 bytes):

```
<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="52428800" />
    </requestFiltering>
  </security>
</system.webServer>
```

The `maxAllowedContentLength` setting only applies to IIS. For more information, see [Request Limits `<requestLimits>`](/en-us/iis/configuration/system.webServer/security/requestFiltering/requestLimits/).

Increase the maximum request body size for the HTTP request by setting [IISServerOptions.MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisserveroptions.maxrequestbodysize) in `Startup.ConfigureServices`. In the following example, the limit is set to 50 MB (52,428,800 bytes):

```
services.Configure<IISServerOptions>(options =>
{
    options.MaxRequestBodySize = 52428800;
});
```

For more information, see [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0#iis-options).

## Troubleshoot

Below are some common problems encountered when working with uploading files and their possible solutions.

### Not Found error when deployed to an IIS server

The following error indicates that the uploaded file exceeds the server's configured content length:

```
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
```

For more information, see the [IIS](#iis) section.

### Connection failure

A connection error and a reset server connection probably indicates that the uploaded file exceeds Kestrel's maximum request body size. For more information, see the [Kestrel maximum request body size](#kestrel-maximum-request-body-size) section. Kestrel client connection limits may also require adjustment.

### Null Reference Exception with IFormFile

If the controller is accepting uploaded files using [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) but the value is `null`, confirm that the HTML form is specifying an `enctype` value of `multipart/form-data`. If this attribute isn't set on the `<form>` element, the file upload doesn't occur and any bound [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) arguments are `null`. Also confirm that the [upload naming in form data matches the app's naming](#match-name-attribute-value-to-parameter-name-of-post-method).

### Stream was too long

The examples in this topic rely upon [MemoryStream](/en-us/dotnet/api/system.io.memorystream) to hold the uploaded file's content. The size limit of a `MemoryStream` is `int.MaxValue`. If the app's file upload scenario requires holding file content larger than 50 MB, use an alternative approach that doesn't rely upon a single `MemoryStream` for holding an uploaded file's content.

## Related content

* [HTTP connection request draining](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#http11-request-draining)

- [HTTP connection request draining](../../fundamentals/servers/kestrel/request-draining?view=aspnetcore-10.0)

* [Unrestricted File Upload](https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload)
* [Azure Security: Security Frame: Input Validation | Mitigations](/en-us/azure/security/azure-security-threat-modeling-tool-input-validation)
* [Azure Cloud Design Patterns: Valet Key pattern](/en-us/azure/architecture/patterns/valet-key)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/getting-started
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/getting-started.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Get started with YARP

YARP is designed as a .NET library that provides core proxy functionality. You can customize the library by adding or replacing modules. YARP is currently provided as a NuGet package and code samples, but a project template and prebuilt executable (`.exe`) are planned for the future.

YARP is implemented on top of .NET infrastructure. You can develop apps on Windows, Linux, or macOS with the .NET SDK and your favorite editor: [Microsoft Visual Studio](https://visualstudio.microsoft.com/vs/) or [Visual Studio Code](https://code.visualstudio.com/). For production hosting, use Windows or Linux server distributions.

Note

macOS is supported for ASP.NET Core development, testing, continuous integration (CI), and local services without external access, but not for production server workloads. There is no server edition of macOS, and [Apple discontinued macOS Server](https://support.apple.com/101601). For production hosting, use Windows or Linux server distributions.

YARP 2.3.0 supports .NET 8 or later. You can download the .NET SDK from <https://dotnet.microsoft.com/download/dotnet/>.

This article describes how to create a basic ASP.NET Core app that uses the YARP library.

## Create a new project

Start by creating an empty ASP.NET Core app from the command line:

```
dotnet new web -n MyProxy
```

Or, build a new ASP.NET Core web app in Visual Studio 2022 by selecting **Empty** for the project template.

Note

For the complete version of the project implemented in this article, download the [Basic YARP Sample](https://github.com/dotnet/yarp/tree/release/latest/samples/BasicYarpSample) on GitHub.

## Add the YARP package reference

Add a package reference for [Yarp.ReverseProxy](https://www.nuget.org/packages/Yarp.ReverseProxy) version 2.3.0 or later.

```
dotnet add package Yarp.ReverseProxy
```

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

## Add the YARP middleware

Update the *Program.cs* file to use the YARP middleware:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
```

## Customize the YARP configuration

The configuration for YARP is defined in the *appsettings.json* file. For more information, see [YARP configuration files](config-files?view=aspnetcore-10.0).

You can also specify the configuration programmatically. For more information, see [YARP extensibility: Configuration providers](config-providers?view=aspnetcore-10.0).

To learn more about the available configuration options, see the [RouteConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.routeconfig) and [ClusterConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.clusterconfig) reference articles.

```
 {
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ReverseProxy": {
    "Routes": {
      "route1" : {
        "ClusterId": "cluster1",
        "Match": {
          "Path": "{**catch-all}"
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "destination1": {
            "Address": "https://example.com/"
          }
        }
      }
    }
  }
}
```

## Run your YARP project application

To run your new YARP project:

* **.NET CLI**: Run the `dotnet run` command within the sample's directory, or use the `dotnet run --project <path to .csproj file>` command.

* **Visual Studio**: Start the app by selecting **Run** on the main menubar.

## Related content

* [Basic YARP sample on GitHub](https://github.com/dotnet/yarp/tree/release/latest/samples/BasicYarpSample)
* [YARP Configuration Files](config-files?view=aspnetcore-10.0)
* [YARP extensibility: Configuration providers](config-providers?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/localization.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Globalization and localization in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax), [Nadeem Afana](https://afana.me/), and [Hisham Bin Ateya](https://twitter.com/hishambinateya)

A multilingual website allows a website to reach a wider audience. ASP.NET Core provides services and middleware for localizing into different languages and cultures.

For Blazor localization guidance, which adds to or supersedes the guidance in this article, see [ASP.NET Core Blazor globalization and localization](../blazor/globalization-localization?view=aspnetcore-10.0).

## Terms

* Globalization (G11N): The process of making an app support different languages and regions. The abbreviation comes from the first and last letters and the number of letters between them.
* Localization (L10N): The process of customizing a globalized app for specific languages and regions.
* Internationalization (I18N): Both globalization and localization.
* Culture: A language and, optionally, a region.
* Neutral culture: A culture that has a specified language, but not a region (for example "en", "es").
* Specific culture: A culture that has a specified language and region (for example, "en-US", "en-GB", "es-CL").
* Parent culture: The neutral culture that contains a specific culture (for example, "en" is the parent culture of "en-US" and "en-GB").
* Locale: A locale is the same as a culture.

## Language and country/region codes

The [RFC 4646](https://www.ietf.org/rfc/rfc4646.txt) format for the culture name is `<language code>-<country/region code>`, where `<language code>` identifies the language and `<country/region code>` identifies the subculture. For example, `es-CL` for Spanish (Chile), `en-US` for English (United States), and `en-AU` for English (Australia). [RFC 4646](https://www.ietf.org/rfc/rfc4646.txt) is a combination of an ISO 639 two-letter lowercase culture code associated with a language and an ISO 3166 two-letter uppercase subculture code associated with a country or region. For more information, see [System.Globalization.CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo).

## Tasks to localize an app

Globalizing and localizing an app involves the following tasks:

* [Make an ASP.NET Core app's content localizable](localization/make-content-localizable?view=aspnetcore-10.0).
* [Provide localized resources for the cultures the app supports](localization/provide-resources?view=aspnetcore-10.0)
* [Implement a strategy to select the culture for each request](localization/select-language-culture?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/localization/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax), [Nadeem Afana](https://afana.me/), and [Hisham Bin Ateya](https://twitter.com/hishambinateya)

A multilingual website allows a website to reach a wider audience. ASP.NET Core provides services and middleware for localizing into different languages and cultures.

## Terms

* Globalization (G11N): The process of making an app support different languages and regions. The abbreviation comes from the first and last letters and the number of letters between them.
* Localization (L10N): The process of customizing a globalized app for specific languages and regions.
* Internationalization (I18N): Both globalization and localization.
* Culture: A language and, optionally, a region.
* Neutral culture: A culture that has a specified language, but not a region (for example "en", "es").
* Specific culture: A culture that has a specified language and region (for example, "en-US", "en-GB", "es-CL").
* Parent culture: The neutral culture that contains a specific culture (for example, "en" is the parent culture of "en-US" and "en-GB").
* Locale: A locale is the same as a culture.

## Language and country/region codes

The [RFC 4646](https://www.ietf.org/rfc/rfc4646.txt) format for the culture name is `<language code>-<country/region code>`, where `<language code>` identifies the language and `<country/region code>` identifies the subculture. For example, `es-CL` for Spanish (Chile), `en-US` for English (United States), and `en-AU` for English (Australia). [RFC 4646](https://www.ietf.org/rfc/rfc4646.txt) is a combination of an ISO 639 two-letter lowercase culture code associated with a language and an ISO 3166 two-letter uppercase subculture code associated with a country or region. For more information, see [System.Globalization.CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo).

## Tasks to localize an app

Globalizing and localizing an app involves the following tasks:

* [Make an ASP.NET Core app's content localizable](localization/make-content-localizable?view=aspnetcore-10.0).
* [Provide localized resources for the cultures the app supports](localization/provide-resources?view=aspnetcore-10.0)
* [Implement a strategy to select the culture for each request](localization/select-language-culture?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/localization/samples/3.x/) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Damien Bowden](https://twitter.com/damien_bod), [Bart Calixto](https://twitter.com/bartmax), [Nadeem Afana](https://afana.me/), and [Hisham Bin Ateya](https://twitter.com/hishambinateya)

A multilingual website allows the site to reach a wider audience. ASP.NET Core provides services and middleware for localizing into different languages and cultures.

Internationalization involves [System.Globalization](/en-us/dotnet/api/system.globalization) and [Localization](/en-us/dotnet/standard/globalization-localization/localization). Globalization is the process of designing apps that support different cultures. Globalization adds support for input, display, and output of a defined set of language scripts that relate to specific geographic areas.

Localization is the process of adapting a globalized app, which you have already processed for localizability, to a particular culture/locale. For more information see **Globalization and localization terms** near the end of this document.

App localization involves the following:

1. Make the app's content localizable
2. Provide localized resources for the languages and cultures you support
3. Implement a strategy to select the language/culture for each request

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/localization/samples/3.x/Localization) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## Make the app content localizable

[IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer) and [IStringLocalizer\<T>](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer-1) were architected to improve productivity when developing localized apps. `IStringLocalizer` uses the [ResourceManager](/en-us/dotnet/api/system.resources.resourcemanager) and [ResourceReader](/en-us/dotnet/api/system.resources.resourcereader) to provide culture-specific resources at run time. The interface has an indexer and an `IEnumerable` for returning localized strings. `IStringLocalizer` doesn't require storing the default language strings in a resource file. You can develop an app targeted for localization and not need to create resource files early in development. The code below shows how to wrap the string "About Title" for localization.

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Localization.Controllers
{
    [Route("api/[controller]")]
    public class AboutController : Controller
    {
        private readonly IStringLocalizer<AboutController> _localizer;

        public AboutController(IStringLocalizer<AboutController> localizer)
        {
            _localizer = localizer;
        }

        [HttpGet]
        public string Get()
        {
            return _localizer["About Title"];
        }
    }
}
```

In the preceding code, the `IStringLocalizer<T>` implementation comes from [Dependency Injection](dependency-injection?view=aspnetcore-10.0). If the localized value of "About Title" isn't found, then the indexer key is returned, that is, the string "About Title". You can leave the default language literal strings in the app and wrap them in the localizer, so that you can focus on developing the app. You develop your app with your default language and prepare it for the localization step without first creating a default resource file. Alternatively, you can use the traditional approach and provide a key to retrieve the default language string. For many developers the new workflow of not having a default language *.resx* file and simply wrapping the string literals can reduce the overhead of localizing an app. Other developers will prefer the traditional work flow as it can make it easier to work with longer string literals and make it easier to update localized strings.

Use the `IHtmlLocalizer<T>` implementation for resources that contain HTML. `IHtmlLocalizer` HTML encodes arguments that are formatted in the resource string, but doesn't HTML encode the resource string itself. In the sample highlighted below, only the value of `name` parameter is HTML encoded.

```
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;

namespace Localization.Controllers
{
    public class BookController : Controller
    {
        private readonly IHtmlLocalizer<BookController> _localizer;

        public BookController(IHtmlLocalizer<BookController> localizer)
        {
            _localizer = localizer;
        }

        public IActionResult Hello(string name)
        {
            ViewData["Message"] = _localizer["<b>Hello</b><i> {0}</i>", name];

            return View();
        }
```

Note

Generally, only localize text, not HTML.

At the lowest level, you can get `IStringLocalizerFactory` out of [Dependency Injection](dependency-injection?view=aspnetcore-10.0):

```
{
    public class TestController : Controller
    {
        private readonly IStringLocalizer _localizer;
        private readonly IStringLocalizer _localizer2;

        public TestController(IStringLocalizerFactory factory)
        {
            var type = typeof(SharedResource);
            var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
            _localizer = factory.Create(type);
            _localizer2 = factory.Create("SharedResource", assemblyName.Name);
        }       

        public IActionResult About()
        {
            ViewData["Message"] = _localizer["Your application description page."] 
                + " loc 2: " + _localizer2["Your application description page."];
```

The code above demonstrates each of the two factory create methods.

You can partition your localized strings by controller, area, or have just one container. In the sample app, a dummy class named `SharedResource` is used for shared resources.

```
// Dummy class to group shared resources

namespace Localization
{
    public class SharedResource
    {
    }
}
```

Some developers use the `Startup` class to contain global or shared strings. In the sample below, the `InfoController` and the `SharedResource` localizers are used:

```
public class InfoController : Controller
{
    private readonly IStringLocalizer<InfoController> _localizer;
    private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

    public InfoController(IStringLocalizer<InfoController> localizer,
                   IStringLocalizer<SharedResource> sharedLocalizer)
    {
        _localizer = localizer;
        _sharedLocalizer = sharedLocalizer;
    }

    public string TestLoc()
    {
        string msg = "Shared resx: " + _sharedLocalizer["Hello!"] +
                     " Info resx " + _localizer["Hello!"];
        return msg;
    }
```

## View localization

The `IViewLocalizer` service provides localized strings for a [view](../mvc/views/overview?view=aspnetcore-10.0). The `ViewLocalizer` class implements this interface and finds the resource location from the view file path. The following code shows how to use the default implementation of `IViewLocalizer`:

```
@using Microsoft.AspNetCore.Mvc.Localization

@inject IViewLocalizer Localizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>@Localizer["Use this area to provide additional information."]</p>
```

The default implementation of `IViewLocalizer` finds the resource file based on the view's file name. There's no option to use a global shared resource file. `ViewLocalizer` implements the localizer using `IHtmlLocalizer`, so Razor doesn't HTML encode the localized string. You can parameterize resource strings and `IViewLocalizer` will HTML encode the parameters, but not the resource string. Consider the following Razor markup:

```
@Localizer["<i>Hello</i> <b>{0}!</b>", UserManager.GetUserName(User)]
```

A French resource file could contain the following:

| Key                        | Value                         |
| -------------------------- | ----------------------------- |
| `<i>Hello</i> <b>{0}!</b>` | `<i>Bonjour</i> <b>{0} !</b>` |

The rendered view would contain the HTML markup from the resource file.

Note

Generally, only localize text, not HTML.

To use a shared resource file in a view, inject `IHtmlLocalizer<T>`:

```
@using Microsoft.AspNetCore.Mvc.Localization
@using Localization.Services

@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedLocalizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>

<h1>@SharedLocalizer["Hello!"]</h1>
```

## DataAnnotations localization

DataAnnotations error messages are localized with `IStringLocalizer<T>`. Using the option `ResourcesPath = "Resources"`, the error messages in `RegisterViewModel` can be stored in either of the following paths:

* *Resources/ViewModels.Account.RegisterViewModel.fr.resx*
* *Resources/ViewModels/Account/RegisterViewModel.fr.resx*

```
public class RegisterViewModel
{
    [Required(ErrorMessage = "The Email field is required.")]
    [EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required(ErrorMessage = "The Password field is required.")]
    [StringLength(8, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}
```

Non-validation attributes are localized.

[]()

### Using one resource string for multiple classes

The following code shows how to use one resource string for validation attributes with multiple classes:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddDataAnnotationsLocalization(options => {
            options.DataAnnotationLocalizerProvider = (type, factory) =>
                factory.Create(typeof(SharedResource));
        });
}
```

In the preceding code, `SharedResource` is the class corresponding to the resx where your validation messages are stored. With this approach, DataAnnotations will only use `SharedResource`, rather than the resource for each class.

## Provide localized resources for the languages and cultures you support

### SupportedCultures and SupportedUICultures

ASP.NET Core allows you to specify two culture values, [SupportedCultures](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.supportedcultures#microsoft-aspnetcore-builder-requestlocalizationoptions-supportedcultures) and [SupportedUICultures](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.supporteduicultures#microsoft-aspnetcore-builder-requestlocalizationoptions-supporteduicultures). The [CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo) object for `SupportedCultures` determines the results of culture-dependent functions, such as date, time, number, and currency formatting. `SupportedCultures` also determines the sorting order of text, casing conventions, and string comparisons. For more information on how the server obtains the culture, see [CultureInfo.CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture) and [CultureInfo.CurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentuiculture#system-globalization-cultureinfo-currentuiculture). The `SupportedUICultures` determines which translated strings (from `.resx` files) are looked up by the [ResourceManager](/en-us/dotnet/api/system.resources.resourcemanager). The `ResourceManager` looks up culture-specific strings that are determined by `CurrentUICulture`. Every thread in .NET has `CurrentCulture` and `CurrentUICulture` objects. The framework inspects these values when rendering culture-dependent functions. If the current thread's culture is set to `en-US` (English, United States), `DateTime.Now.ToLongDateString()` displays `Thursday, February 18, 2016`; but if `CurrentCulture` is set to `es-ES` (Spanish, Spain), the output is `jueves, 18 de febrero de 2016`.

## Resource files

A resource file is a useful mechanism for separating localizable strings from code. Translated strings for the non-default language are isolated in *.resx* resource files. For example, you might want to create Spanish resource file named *Welcome.es.resx* containing translated strings. "es" is the language code for Spanish. To create this resource file in Visual Studio:

1. In **Solution Explorer**, right click on the folder which will contain the resource file > **Add** > **New Item**.

2. In the **Search installed templates** box, enter "resource" and name the file.

3. Enter the key value (native string) in the **Name** column and the translated string in the **Value** column.

   Visual Studio shows the *Welcome.es.resx* file.

## Resource file naming

Resources are named for the full type name of their class minus the assembly name. For example, a French resource in a project whose main assembly is `LocalizationWebsite.Web.dll` for the class `LocalizationWebsite.Web.Startup` would be named *Startup.fr.resx*. A resource for the class `LocalizationWebsite.Web.Controllers.HomeController` would be named *Controllers.HomeController.fr.resx*. If your targeted class's namespace isn't the same as the assembly name you will need the full type name. For example, in the sample project a resource for the type `ExtraNamespace.Tools` would be named *ExtraNamespace.Tools.fr.resx*.

In the sample project, the `ConfigureServices` method sets the `ResourcesPath` to "Resources", so the project relative path for the home controller's French resource file is *Resources/Controllers.HomeController.fr.resx*. Alternatively, you can use folders to organize resource files. For the home controller, the path would be *Resources/Controllers/HomeController.fr.resx*. If you don't use the `ResourcesPath` option, the *.resx* file would go in the project base directory. The resource file for `HomeController` would be named *Controllers.HomeController.fr.resx*. The choice of using the dot or path naming convention depends on how you want to organize your resource files.

| Resource name                                | Dot or path naming |
| -------------------------------------------- | ------------------ |
| Resources/Controllers.HomeController.fr.resx | Dot                |
| Resources/Controllers/HomeController.fr.resx | Path               |

Resource files using `@inject IViewLocalizer` in Razor views follow a similar pattern. The resource file for a view can be named using either dot naming or path naming. Razor view resource files mimic the path of their associated view file. Assuming we set the `ResourcesPath` to "Resources", the French resource file associated with the `Views/Home/About.cshtml` view could be either of the following:

* Resources/Views/Home/About.fr.resx

* Resources/Views.Home.About.fr.resx

If you don't use the `ResourcesPath` option, the *.resx* file for a view would be located in the same folder as the view.

### RootNamespaceAttribute

```
using System.Reflection;
using Microsoft.Extensions.Localization;

[assembly: ResourceLocation("Resource Folder Name")]
[assembly: RootNamespace("App Root Namespace")]
```

The preceding code enables the successful resolution of resx files.

## Culture fallback behavior

When searching for a resource, localization engages in "culture fallback". Starting from the requested culture, if not found, it reverts to the parent culture of that culture. As an aside, the [CultureInfo.Parent](/en-us/dotnet/api/system.globalization.cultureinfo.parent) property represents the parent culture. This usually (but not always) means removing the national signifier from the ISO. For example, the dialect of Spanish spoken in Mexico is "es-MX". It has the parent "es"—Spanish non-specific to any country.

Imagine your site receives a request for a "Welcome" resource using culture "fr-CA". The localization system looks for the following resources, in order, and selects the first match:

* *Welcome.fr-CA.resx*
* *Welcome.fr.resx*
* *Welcome.resx* (if the `NeutralResourcesLanguage` is "fr-CA")

As an example, if you remove the ".fr" culture designator and you have the culture set to French, the default resource file is read and strings are localized. The Resource manager designates a default or fallback resource for when nothing meets your requested culture. If you want to just return the key when missing a resource for the requested culture you must not have a default resource file.

### Generate resource files with Visual Studio

If you create a resource file in Visual Studio without a culture in the file name (for example, *Welcome.resx*), Visual Studio will create a C# class with a property for each string. That's usually not what you want with ASP.NET Core. You typically don't have a default *.resx* resource file (a *.resx* file without the culture name). We suggest you create the *.resx* file with a culture name (for example *Welcome.fr.resx*). When you create a *.resx* file with a culture name, Visual Studio won't generate the class file.

### Add other cultures

Each language and culture combination (other than the default language) requires a unique resource file. You create resource files for different cultures and locales by creating new resource files in which the ISO language codes are part of the file name (for example, **en-us**, **fr-ca**, and **en-gb**). These ISO codes are placed between the file name and the *.resx* file extension, as in *Welcome.es-MX.resx* (Spanish/Mexico).

## Implement a strategy to select the language/culture for each request

### Configure localization

Localization is configured in the `Startup.ConfigureServices` method:

```
services.AddLocalization(options => options.ResourcesPath = "Resources");

services.AddMvc()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization();
```

* `AddLocalization` adds the localization services to the services container. The code above also sets the resources path to "Resources".

* `AddViewLocalization` adds support for localized view files. In this sample view localization is based on the view file suffix. For example "fr" in the `Index.fr.cshtml` file.

* `AddDataAnnotationsLocalization` adds support for localized `DataAnnotations` validation messages through `IStringLocalizer` abstractions.

### Localization middleware

The current culture on a request is set in the localization [Middleware](middleware/?view=aspnetcore-10.0). The localization middleware is enabled in the `Startup.Configure` method. The localization middleware must be configured before any middleware that might check the request culture (for example, `app.UseMvcWithDefaultRoute()`). Localization middleware must appear after routing middleware if using [RouteDataRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.routing.routedatarequestcultureprovider). For more information on middleware order, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0#middleware-order).

```
var supportedCultures = new[] { "en-US", "fr" };
var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);
```

`UseRequestLocalization` initializes a `RequestLocalizationOptions` object. On every request the list of `RequestCultureProvider` in the `RequestLocalizationOptions` is enumerated and the first provider that can successfully determine the request culture is used. The default providers come from the `RequestLocalizationOptions` class:

1. `QueryStringRequestCultureProvider`
2. `CookieRequestCultureProvider`
3. `AcceptLanguageHeaderRequestCultureProvider`

The default list goes from most specific to least specific. Later in the article we'll see how you can change the order and even add a custom culture provider. If none of the providers can determine the request culture, the `DefaultRequestCulture` is used.

### QueryStringRequestCultureProvider

Some apps will use a query string to set the [CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo). For apps that use the cookie or Accept-Language header approach, adding a query string to the URL is useful for debugging and testing code. By default, the `QueryStringRequestCultureProvider` is registered as the first localization provider in the `RequestCultureProvider` list. You pass the query string parameters `culture` and `ui-culture`. The following example sets the specific culture (language and region) to Spanish/Mexico:

`http://localhost:5000/?culture=es-MX&ui-culture=es-MX`

If you only pass in one of the two (`culture` or `ui-culture`), the query string provider will set both values using the one you passed in. For example, setting just the culture will set both the `Culture` and the `UICulture`:

```
http://localhost:5000/?culture=es-MX
```

### CookieRequestCultureProvider

Production apps will often provide a mechanism to set the culture with the ASP.NET Core culture cookie. Use the `MakeCookieValue` method to create a cookie.

The `CookieRequestCultureProvider` `DefaultCookieName` returns the default cookie name used to track the user's preferred culture information. The default cookie name is `.AspNetCore.Culture`.

The cookie format is `c=%LANGCODE%|uic=%LANGCODE%`, where `c` is `Culture` and `uic` is `UICulture`, for example:

```
c=en-UK|uic=en-US
```

If you only specify one of culture info and UI culture, the specified culture will be used for both culture info and UI culture.

### The Accept-Language HTTP header

The [Accept-Language header](https://www.w3.org/International/questions/qa-accept-lang-locales) is settable in most browsers and was originally intended to specify the user's language. This setting indicates what the browser has been set to send or has inherited from the underlying operating system. The Accept-Language HTTP header from a browser request isn't an infallible way to detect the user's preferred language (see [Setting language preferences in a browser](https://www.w3.org/International/questions/qa-lang-priorities.en.php)). A production app should include a way for a user to customize their choice of culture.

### Set the Accept-Language HTTP header in IE

1. From the gear icon, tap **Internet Options**.

2. Tap **Languages**.

3. Tap **Set Language Preferences**.

4. Tap **Add a language**.

5. Add the language.

6. Tap the language, then tap **Move Up**.

### Use a custom provider

Suppose you want to let your customers store their language and culture in your databases. You could write a provider to look up these values for the user. The following code shows how to add a custom provider:

```
private const string enUSCulture = "en-US";

services.Configure<RequestLocalizationOptions>(options =>
{
    var supportedCultures = new[]
    {
        new CultureInfo(enUSCulture),
        new CultureInfo("fr")
    };

    options.DefaultRequestCulture = new RequestCulture(culture: enUSCulture, uiCulture: enUSCulture);
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;

    options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context =>
    {
        // My custom request culture logic
        return await Task.FromResult(new ProviderCultureResult("en"));
    }));
});
```

Use `RequestLocalizationOptions` to add or remove localization providers.

### Change request culture providers order

[RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions) has three default request culture providers: [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider), [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider), and [AcceptLanguageHeaderRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.acceptlanguageheaderrequestcultureprovider). Use \[`RequestLocalizationOptions.RequestCultureProviders`]]\(xref:Microsoft.AspNetCore.Builder.RequestLocalizationOptions.RequestCultureProviders) property to change the order of these providers as shown in the following below:

```
    app.UseRequestLocalization(options =>
    {
        var questStringCultureProvider = options.RequestCultureProviders[0];
        options.RequestCultureProviders.RemoveAt(0);
        options.RequestCultureProviders.Insert(1, questStringCultureProvider);
    });
```

In the preceding example, the order of `QueryStringRequestCultureProvider` and `CookieRequestCultureProvider` is switched, so the `RequestLocalizationMiddleware` looks for the cultures from the cookies first, then query string.

As previously mentioned, add a custom provider via [AddInitialRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptionsextensions.addinitialrequestcultureprovider) which sets the order to `0`, so this provider takes the precedence over the others.

### Set the culture programmatically

This sample **Localization.StarterWeb** project on [GitHub](https://github.com/aspnet/entropy) contains UI to set the `Culture`. The `Views/Shared/_SelectLanguagePartial.cshtml` file allows you to select the culture from the list of supported cultures:

```
@using Microsoft.AspNetCore.Builder
@using Microsoft.AspNetCore.Http.Features
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options

@inject IViewLocalizer Localizer
@inject IOptions<RequestLocalizationOptions> LocOptions

@{
    var requestCulture = Context.Features.Get<IRequestCultureFeature>();
    var cultureItems = LocOptions.Value.SupportedUICultures
        .Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
        .ToList();
    var returnUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}";
}

<div title="@Localizer["Request culture provider:"] @requestCulture?.Provider?.GetType().Name">
    <form id="selectLanguage" asp-controller="Home" 
          asp-action="SetLanguage" asp-route-returnUrl="@returnUrl" 
          method="post" class="form-horizontal" role="form">
        <label asp-for="@requestCulture.RequestCulture.UICulture.Name">@Localizer["Language:"]</label> <select name="culture"
          onchange="this.form.submit();"
          asp-for="@requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems">
        </select>
    </form>
</div>
```

The `Views/Shared/_SelectLanguagePartial.cshtml` file is added to the `footer` section of the layout file so it will be available to all views:

```
<div class="container body-content" style="margin-top:60px">
    @RenderBody()
    <hr>
    <footer>
        <div class="row">
            <div class="col-md-6">
                <p>&copy; @System.DateTime.Now.Year - Localization</p>
            </div>
            <div class="col-md-6 text-right">
                @await Html.PartialAsync("_SelectLanguagePartial")
            </div>
        </div>
    </footer>
</div>
```

The `SetLanguage` method sets the culture cookie.

```
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );

    return LocalRedirect(returnUrl);
}
```

You can't plug in the `_SelectLanguagePartial.cshtml` to sample code for this project. The **Localization.StarterWeb** project on [GitHub](https://github.com/aspnet/entropy) has code to flow the `RequestLocalizationOptions` to a Razor partial through the [Dependency Injection](dependency-injection?view=aspnetcore-10.0) container.

## Model binding route data and query strings

See [Globalization behavior of model binding route data and query strings](../mvc/models/model-binding?view=aspnetcore-10.0#glob).

## Globalization and localization terms

The process of localizing your app also requires a basic understanding of relevant character sets commonly used in modern software development and an understanding of the issues associated with them. Although all computers store text as numbers (codes), different systems store the same text using different numbers. The localization process refers to translating the app user interface (UI) for a specific culture/locale.

[Localizability](/en-us/dotnet/standard/globalization-localization/localizability-review) is an intermediate process for verifying that a globalized app is ready for localization.

The [RFC 4646](https://www.ietf.org/rfc/rfc4646.txt) format for the culture name is `<languagecode2>-<country/regioncode2>`, where `<languagecode2>` is the language code and `<country/regioncode2>` is the subculture code. For example, `es-CL` for Spanish (Chile), `en-US` for English (United States), and `en-AU` for English (Australia). [RFC 4646](https://www.ietf.org/rfc/rfc4646.txt) is a combination of an ISO 639 two-letter lowercase culture code associated with a language and an ISO 3166 two-letter uppercase subculture code associated with a country or region. For more information, see [System.Globalization.CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo).

Internationalization is often abbreviated to "I18N". The abbreviation takes the first and last letters and the number of letters between them, so 18 stands for the number of letters between the first "I" and the last "N". The same applies to Globalization (G11N), and Localization (L10N).

Terms:

* Globalization (G11N): The process of making an app support different languages and regions.
* Localization (L10N): The process of customizing an app for a given language and region.
* Internationalization (I18N): Describes both globalization and localization.
* Culture: It's a language and, optionally, a region.
* Neutral culture: A culture that has a specified language, but not a region. (for example "en", "es")
* Specific culture: A culture that has a specified language and region. (for example "en-US", "en-GB", "es-CL")
* Parent culture: The neutral culture that contains a specific culture. (for example, "en" is the parent culture of "en-US" and "en-GB")
* Locale: A locale is the same as a culture.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Note

Prior to ASP.NET Core 3.0 web apps write one log of type `LogLevel.Warning` per request if the requested culture is unsupported. Logging one `LogLevel.Warning` per request can make large log files with redundant information. This behavior has been changed in ASP.NET Core 3.0. The `RequestLocalizationMiddleware` writes a log of type `LogLevel.Debug`, which reduces the size of production logs.

## Related content

* [Url culture provider using middleware as filters in ASP.NET Core](https://andrewlock.net/url-culture-provider-using-middleware-as-mvc-filter-in-asp-net-core-1-1-0/)
* [Applying the RouteDataRequest CultureProvider globally with middleware as filters](https://andrewlock.net/applying-the-routedatarequest-cultureprovider-globally-with-middleware-as-filters/)
* [`IStringLocalizer`](localization/make-content-localizable?view=aspnetcore-10.0) : Uses the [ResourceManager](/en-us/dotnet/api/system.resources.resourcemanager) and [ResourceReader](/en-us/dotnet/api/system.resources.resourcereader) to provide culture-specific resources at run time. The interface has an indexer and an `IEnumerable` for returning localized strings.
* [`IHtmlLocalizer`](localization/make-content-localizable?view=aspnetcore-10.0#ihtmllocalizer): For resources that contain HTML.
* [View and DataAnnotations](localization/make-content-localizable?view=aspnetcore-10.0#view-localization)
* [Troubleshoot ASP.NET Core localization](troubleshoot-aspnet-core-localization?view=aspnetcore-10.0)
* [Globalizing and localizing .NET applications](/en-us/dotnet/standard/globalization-localization/index)
* [Resources in .resx Files](/en-us/dotnet/framework/resources/working-with-resx-files-programmatically)
* [Localization & Generics](http://hishambinateya.com/localization-and-generics)
* [Tim Heuer's ResX Viewer and Editor (Visual Studio Code)](https://marketplace.visualstudio.com/items?itemName=TimHeuer.resx-editor)

- [Make an ASP.NET Core app's content localizable](localization/make-content-localizable?view=aspnetcore-10.0)
- [Provide localized resources for languages and cultures in an ASP.NET Core app](localization/provide-resources?view=aspnetcore-10.0)
- [Strategies for selecting language and culture in a localized ASP.NET Core app](localization/select-language-culture?view=aspnetcore-10.0)
- [Troubleshoot ASP.NET Core localization](troubleshoot-aspnet-core-localization?view=aspnetcore-10.0)
- [Globalizing and localizing .NET applications](/en-us/dotnet/standard/globalization-localization/index)
- [Localization.StarterWeb project](https://github.com/aspnet/Entropy/tree/master/samples/Localization.StarterWeb) used in the article.
- [Resources in .resx Files](/en-us/dotnet/framework/resources/working-with-resx-files-programmatically)
- [Localization & Generics](http://hishambinateya.com/localization-and-generics)
- [Tim Heuer's ResX Viewer and Editor (Visual Studio Code)](https://marketplace.visualstudio.com/items?itemName=TimHeuer.resx-editor)

* [Troubleshoot ASP.NET Core localization](troubleshoot-aspnet-core-localization?view=aspnetcore-10.0)
* [Localization.StarterWeb project](https://github.com/aspnet/Entropy/tree/master/samples/Localization.StarterWeb) used in the article.
* [Globalizing and localizing .NET applications](/en-us/dotnet/standard/globalization-localization/index)
* [Resources in .resx Files](/en-us/dotnet/framework/resources/working-with-resx-files-programmatically)
* [Localization & Generics](http://hishambinateya.com/localization-and-generics)
* [Tim Heuer's ResX Viewer and Editor (Visual Studio Code)](https://marketplace.visualstudio.com/items?itemName=TimHeuer.resx-editor)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/dependency-injection
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/dependency-injection.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Dependency injection into controllers in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

ASP.NET Core MVC controllers request dependencies explicitly via constructors. ASP.NET Core has built-in support for [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). DI makes apps easier to test and maintain.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/dependency-injection/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Constructor injection

Services are added as a constructor parameter, and the runtime resolves the service from the service container. Services are typically defined using interfaces. For example, consider an app that requires the current time. The following interface exposes the `IDateTime` service:

```
public interface IDateTime
{
    DateTime Now { get; }
}
```

The following code implements the `IDateTime` interface:

```
public class SystemDateTime : IDateTime
{
    public DateTime Now
    {
        get { return DateTime.Now; }
    }
}
```

Add the service to the service container:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDateTime, SystemDateTime>();

    services.AddControllersWithViews();
}
```

For more information on [AddSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addsingleton), see [DI service lifetimes](../../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes).

The following code displays a greeting to the user based on the time of day:

```
public class HomeController : Controller
{
    private readonly IDateTime _dateTime;

    public HomeController(IDateTime dateTime)
    {
        _dateTime = dateTime;
    }

    public IActionResult Index()
    {
        var serverTime = _dateTime.Now;
        if (serverTime.Hour < 12)
        {
            ViewData["Message"] = "It's morning here - Good Morning!";
        }
        else if (serverTime.Hour < 17)
        {
            ViewData["Message"] = "It's afternoon here - Good Afternoon!";
        }
        else
        {
            ViewData["Message"] = "It's evening here - Good Evening!";
        }
        return View();
    }
```

Run the app and a message is displayed based on the time.

## Action injection with `FromServices`

The [FromServicesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) enables injecting a service directly into an action method without using constructor injection:

```
public IActionResult About([FromServices] IDateTime dateTime)
{
    return Content( $"Current server time: {dateTime.Now}");
}
```

## Action injection with `FromKeyedServices`

The following code shows how to access keyed services from the DI container by using the [`[FromKeyedServices]`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute) attribute:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
    [HttpGet("big")]
    public ActionResult<object> GetBigCache([FromKeyedServices("big")] ICache cache)
    {
        return cache.Get("data-mvc");
    }

    [HttpGet("small")]
    public ActionResult<object> GetSmallCache([FromKeyedServices("small")] ICache cache)
    {
        return cache.Get("data-mvc");
    }
}
```

## Access settings from a controller

Accessing app or configuration settings from within a controller is a common pattern. The *options pattern* described in [Options pattern in ASP.NET Core](../../fundamentals/configuration/options?view=aspnetcore-10.0) is the preferred approach to manage settings. Generally, don't directly inject [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) into a controller.

Create a class that represents the options. For example:

```
public class SampleWebSettings
{
    public string Title { get; set; }
    public int Updates { get; set; }
}
```

Add the configuration class to the services collection:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDateTime, SystemDateTime>();
    services.Configure<SampleWebSettings>(Configuration);

    services.AddControllersWithViews();
}
```

Configure the app to read the settings from a JSON-formatted file:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                config.AddJsonFile("samplewebsettings.json",
                    optional: false,
                    reloadOnChange: true);
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

The following code requests the `IOptions<SampleWebSettings>` settings from the service container and uses them in the `Index` method:

```
public class SettingsController : Controller
{
    private readonly SampleWebSettings _settings;

    public SettingsController(IOptions<SampleWebSettings> settingsOptions)
    {
        _settings = settingsOptions.Value;
    }

    public IActionResult Index()
    {
        ViewData["Title"] = _settings.Title;
        ViewData["Updates"] = _settings.Updates;
        return View();
    }
}
```

## Controllers as services

By default, ASP.NET Core doesn't register controllers as services in the DI container. The runtime uses the [DefaultControllerActivator](https://source.dot.net/#Microsoft.AspNetCore.Mvc.Core/Controllers/DefaultControllerActivator.cs) to create controller instances and resolves services from the DI container for constructor parameters, but the controller itself isn't resolved from the container.

Calling `AddControllersAsServices` registers all controllers as services in the DI container:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews().AddControllersAsServices();
```

Registering controllers as services enables:

* Intercepting controller creation with a custom `IControllerActivator`.
* Using any DI lifetime management for controllers.
* Injecting services into controllers using any registered constructor, since the DI container selects the constructor.

Note

Configure the `ApplicationPartManager` **before** calling `AddControllersAsServices`. See [Share controllers, views, Razor Pages and more with Application Parts in ASP.NET Core](../advanced/app-parts?view=aspnetcore-10.0#prevent-loading-resources) for details.

## Related content

* See [Test controller logic in ASP.NET Core](testing?view=aspnetcore-10.0) to learn how to make code easier to test by explicitly requesting dependencies in controllers.
* [Keyed service dependency injection container support](https://andrewlock.net/exploring-the-dotnet-8-preview-keyed-services-dependency-injection-support/)
* [Replace the default dependency injection container with a third party implementation](../../fundamentals/dependency-injection?view=aspnetcore-10.0#default-service-container-replacement).

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Steve Smith](https://github.com/ardalis)

ASP.NET Core MVC controllers request dependencies explicitly via constructors. ASP.NET Core has built-in support for [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). DI makes apps easier to test and maintain.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/dependency-injection/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Constructor injection

Services are added as a constructor parameter, and the runtime resolves the service from the service container. Services are typically defined using interfaces. For example, consider an app that requires the current time. The following interface exposes the `IDateTime` service:

```
public interface IDateTime
{
    DateTime Now { get; }
}
```

The following code implements the `IDateTime` interface:

```
public class SystemDateTime : IDateTime
{
    public DateTime Now
    {
        get { return DateTime.Now; }
    }
}
```

Add the service to the service container:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDateTime, SystemDateTime>();

    services.AddControllersWithViews();
}
```

For more information on [AddSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addsingleton), see [DI service lifetimes](../../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes).

The following code displays a greeting to the user based on the time of day:

```
public class HomeController : Controller
{
    private readonly IDateTime _dateTime;

    public HomeController(IDateTime dateTime)
    {
        _dateTime = dateTime;
    }

    public IActionResult Index()
    {
        var serverTime = _dateTime.Now;
        if (serverTime.Hour < 12)
        {
            ViewData["Message"] = "It's morning here - Good Morning!";
        }
        else if (serverTime.Hour < 17)
        {
            ViewData["Message"] = "It's afternoon here - Good Afternoon!";
        }
        else
        {
            ViewData["Message"] = "It's evening here - Good Evening!";
        }
        return View();
    }
```

Run the app and a message is displayed based on the time.

## Action injection with `FromServices`

The [FromServicesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) enables injecting a service directly into an action method without using constructor injection:

```
public IActionResult About([FromServices] IDateTime dateTime)
{
    return Content( $"Current server time: {dateTime.Now}");
}
```

## Access settings from a controller

Accessing app or configuration settings from within a controller is a common pattern. The *options pattern* described in [Options pattern in ASP.NET Core](../../fundamentals/configuration/options?view=aspnetcore-10.0) is the preferred approach to manage settings. Generally, don't directly inject [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) into a controller.

Create a class that represents the options. For example:

```
public class SampleWebSettings
{
    public string Title { get; set; }
    public int Updates { get; set; }
}
```

Add the configuration class to the services collection:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDateTime, SystemDateTime>();
    services.Configure<SampleWebSettings>(Configuration);

    services.AddControllersWithViews();
}
```

Configure the app to read the settings from a JSON-formatted file:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                config.AddJsonFile("samplewebsettings.json",
                    optional: false,
                    reloadOnChange: true);
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

The following code requests the `IOptions<SampleWebSettings>` settings from the service container and uses them in the `Index` method:

```
public class SettingsController : Controller
{
    private readonly SampleWebSettings _settings;

    public SettingsController(IOptions<SampleWebSettings> settingsOptions)
    {
        _settings = settingsOptions.Value;
    }

    public IActionResult Index()
    {
        ViewData["Title"] = _settings.Title;
        ViewData["Updates"] = _settings.Updates;
        return View();
    }
}
```

## Controllers as services

By default, ASP.NET Core doesn't register controllers as services in the DI container. The runtime uses the [DefaultControllerActivator](https://source.dot.net/#Microsoft.AspNetCore.Mvc.Core/Controllers/DefaultControllerActivator.cs) to create controller instances and resolves services from the DI container for constructor parameters, but the controller itself isn't resolved from the container.

Calling `AddControllersAsServices` registers all controllers as services in the DI container:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews().AddControllersAsServices();
}
```

Registering controllers as services enables:

* Intercepting controller creation with a custom `IControllerActivator`.
* Using any DI lifetime management for controllers.
* Injecting services into controllers using any registered constructor, since the DI container selects the constructor.

Note

Configure the `ApplicationPartManager` **before** calling `AddControllersAsServices`. See [Share controllers, views, Razor Pages and more with Application Parts in ASP.NET Core](../advanced/app-parts?view=aspnetcore-10.0#prevent-loading-resources) for details.

## Additional resources

* See [Test controller logic in ASP.NET Core](testing?view=aspnetcore-10.0) to learn how to make code easier to test by explicitly requesting dependencies in controllers.

* [Replace the default dependency injection container with a third party implementation](../../fundamentals/dependency-injection?view=aspnetcore-10.0#default-service-container-replacement).

ASP.NET Core MVC controllers request dependencies explicitly via constructors. ASP.NET Core has built-in support for [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0). DI makes apps easier to test and maintain.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/dependency-injection/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Constructor injection

Services are added as a constructor parameter, and the runtime resolves the service from the service container. Services are typically defined using interfaces. For example, consider an app that requires the current time. The following interface exposes the `IDateTime` service:

```
public interface IDateTime
{
    DateTime Now { get; }
}
```

The following code implements the `IDateTime` interface:

```
public class SystemDateTime : IDateTime
{
    public DateTime Now
    {
        get { return DateTime.Now; }
    }
}
```

Add the service to the service container:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDateTime, SystemDateTime>();

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
```

For more information on [AddSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addsingleton), see [DI service lifetimes](../../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes).

The following code displays a greeting to the user based on the time of day:

```
public class HomeController : Controller
{
    private readonly IDateTime _dateTime;

    public HomeController(IDateTime dateTime)
    {
        _dateTime = dateTime;
    }

    public IActionResult Index()
    {
        var serverTime = _dateTime.Now;
        if (serverTime.Hour < 12)
        {
            ViewData["Message"] = "It's morning here - Good Morning!";
        }
        else if (serverTime.Hour < 17)
        {
            ViewData["Message"] = "It's afternoon here - Good Afternoon!";
        }
        else
        {
            ViewData["Message"] = "It's evening here - Good Evening!";
        }
        return View();
    }
```

Run the app and a message is displayed based on the time.

## Action injection with `FromServices`

The [FromServicesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) enables injecting a service directly into an action method without using constructor injection:

```
public IActionResult About([FromServices] IDateTime dateTime)
{
    ViewData["Message"] = $"Current server time: {dateTime.Now}";

    return View();
}
```

## Access settings from a controller

Accessing app or configuration settings from within a controller is a common pattern. The *options pattern* described in [Options pattern in ASP.NET Core](../../fundamentals/configuration/options?view=aspnetcore-10.0) is the preferred approach to manage settings. Generally, don't directly inject [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) into a controller.

Create a class that represents the options. For example:

```
public class SampleWebSettings
{
    public string Title { get; set; }
    public int Updates { get; set; }
}
```

Add the configuration class to the services collection:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDateTime, SystemDateTime>();
    services.Configure<SampleWebSettings>(Configuration);
    
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
```

Configure the app to read the settings from a JSON-formatted file:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.AddJsonFile("samplewebsettings.json", 
                                optional: false,        // File is not optional.
                                reloadOnChange: false);
        })
        .UseStartup<Startup>();
}
```

The following code requests the `IOptions<SampleWebSettings>` settings from the service container and uses them in the `Index` method:

```
public class SettingsController : Controller
{
    private readonly SampleWebSettings _settings;

    public SettingsController(IOptions<SampleWebSettings> settingsOptions)
    {
        _settings = settingsOptions.Value;
    }

    public IActionResult Index()
    {
        ViewData["Title"] = _settings.Title;
        ViewData["Updates"] = _settings.Updates;
        return View();
    }
}
```

## Controllers as services

By default, ASP.NET Core doesn't register controllers as services in the DI container. The runtime uses the [DefaultControllerActivator](https://source.dot.net/#Microsoft.AspNetCore.Mvc.Core/Controllers/DefaultControllerActivator.cs) to create controller instances and resolves services from the DI container for constructor parameters, but the controller itself isn't resolved from the container.

Calling `AddControllersAsServices` registers all controllers as services in the DI container:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddControllersAsServices();
}
```

Registering controllers as services enables:

* Intercepting controller creation with a custom `IControllerActivator`.
* Using any DI lifetime management for controllers.
* Injecting services into controllers using any registered constructor, since the DI container selects the constructor.

Note

Configure the `ApplicationPartManager` **before** calling `AddControllersAsServices`. See [Share controllers, views, Razor Pages and more with Application Parts in ASP.NET Core](../advanced/app-parts?view=aspnetcore-10.0#prevent-loading-resources) for details.

## Additional resources

* See [Test controller logic in ASP.NET Core](testing?view=aspnetcore-10.0) to learn how to make code easier to test by explicitly requesting dependencies in controllers.

* [Replace the default dependency injection container with a third party implementation](../../fundamentals/dependency-injection?view=aspnetcore-10.0#default-service-container-replacement).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-03-04

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/dotnet-grpc
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/dotnet-grpc.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Manage Protobuf references with dotnet-grpc

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

`dotnet-grpc` is a .NET Global Tool for managing [Protobuf (`.proto`)](basics?view=aspnetcore-10.0#proto-file) references within a .NET gRPC project. The tool can be used to add, refresh, remove, and list Protobuf references.

## Installation

To install the `dotnet-grpc` [.NET Global Tool](/en-us/dotnet/core/tools/global-tools), run the following command:

```
dotnet tool install -g dotnet-grpc
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

## Add references

`dotnet-grpc` can be used to add Protobuf references as `<Protobuf />` items to the `.csproj` file:

```
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
```

The Protobuf references are used to generate the C# client and/or server assets. The `dotnet-grpc` tool can:

* Create a Protobuf reference from local files on disk.
* Create a Protobuf reference from a remote file specified by a URL.
* Ensure the correct gRPC package dependencies are added to the project.

For example, the `Grpc.AspNetCore` package is added to a web app. `Grpc.AspNetCore` contains gRPC server and client libraries and tooling support. Alternatively, the `Grpc.Net.Client`, `Grpc.Tools` and `Google.Protobuf` packages, which contain only the gRPC client libraries and tooling support, are added to a Console app.

### Add file

The `add-file` command is used to add local files on disk as Protobuf references. The file paths provided:

* Can be relative to the current directory or absolute paths.
* May contain wild cards for pattern-based file [globbing](https://wikipedia.org/wiki/Glob_\(programming\)).

If any files are outside the project directory, a `Link` element is added to display the file under the folder `Protos` in Visual Studio.

### Usage

```
dotnet-grpc add-file [options] <files>...
```

#### Arguments

| Argument | Description                                                                         |
| -------- | ----------------------------------------------------------------------------------- |
| files    | The protobuf file references. These can be a path to glob for local protobuf files. |

#### Options

| Short option | Long option              | Description                                                                                                                                                                                                                  |
| ------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -p           | --project                | The path to the project file to operate on. If a file is not specified, the command searches the current directory for one.                                                                                                  |
| -s           | --services               | The type of gRPC services that should be generated. If `Default` is specified, `Both` is used for Web projects and `Client` is used for non-Web projects. Accepted values are `Both`, `Client`, `Default`, `None`, `Server`. |
| -i           | --additional-import-dirs | Additional directories to be used when resolving imports for the protobuf files. This is a semicolon separated list of paths.                                                                                                |
|              | --access                 | The access modifier to use for the generated C# classes. The default value is `Public`. Accepted values are `Internal` and `Public`.                                                                                         |

### Add URL

The `add-url` command is used to add a remote file specified by an source URL as Protobuf reference. A file path must be provided to specify where to download the remote file. The file path can be relative to the current directory or an absolute path. If the file path is outside the project directory, a `Link` element is added to display the file under the virtual folder `Protos` in Visual Studio.

### Usage

```
dotnet-grpc add-url [options] <url>
```

#### Arguments

| Argument | Description                        |
| -------- | ---------------------------------- |
| url      | The URL to a remote protobuf file. |

#### Options

| Short option | Long option              | Description                                                                                                                                                                                                                  |
| ------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -o           | --output                 | Specifies the download path for the remote protobuf file. This is a required option.                                                                                                                                         |
| -p           | --project                | The path to the project file to operate on. If a file is not specified, the command searches the current directory for one.                                                                                                  |
| -s           | --services               | The type of gRPC services that should be generated. If `Default` is specified, `Both` is used for Web projects and `Client` is used for non-Web projects. Accepted values are `Both`, `Client`, `Default`, `None`, `Server`. |
| -i           | --additional-import-dirs | Additional directories to be used when resolving imports for the protobuf files. This is a semicolon separated list of paths.                                                                                                |
|              | --access                 | The access modifier to use for the generated C# classes. Default value is `Public`. Accepted values are `Internal` and `Public`.                                                                                             |

## Remove

The `remove` command is used to remove Protobuf references from the `.csproj` file. The command accepts path arguments and source URLs as arguments. The tool:

* Only removes the Protobuf reference.
* Does not delete the `.proto` file, even if it was originally downloaded from a remote URL.

### Usage

```
dotnet-grpc remove [options] <references>...
```

### Arguments

| Argument   | Description                                                  |
| ---------- | ------------------------------------------------------------ |
| references | The URLs or file paths of the protobuf references to remove. |

### Options

| Short option | Long option | Description                                                                                                                 |
| ------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------- |
| -p           | --project   | The path to the project file to operate on. If a file is not specified, the command searches the current directory for one. |

## Refresh

The `refresh` command is used to update a remote reference with the latest content from the source URL. Both the download file path and the source URL can be used to specify the reference to be updated. Note:

* The hashes of the file contents are compared to determine whether the local file should be updated.
* No timestamp information is compared.

The tool always replaces the local file with the remote file if an update is needed.

### Usage

```
dotnet-grpc refresh [options] [<references>...]
```

### Arguments

| Argument   | Description                                                                                                                              |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| references | The URLs or file paths to remote protobuf references that should be updated. Leave this argument empty to refresh all remote references. |

### Options

| Short option | Long option | Description                                                                                                                 |
| ------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------- |
| -p           | --project   | The path to the project file to operate on. If a file is not specified, the command searches the current directory for one. |
|              | --dry-run   | Outputs a list of files that would be updated without downloading any new content.                                          |

## List

The `list` command is used to display all the Protobuf references in the project file. If all values of a column are default values, the column may be omitted.

### Usage

```
dotnet-grpc list [options]
```

### Options

| Short option | Long option | Description                                                                                                                 |
| ------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------- |
| -p           | --project   | The path to the project file to operate on. If a file is not specified, the command searches the current directory for one. |

## Related content

* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0028
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0028.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0028: Consider using `IPAddress.IPv6Any` instead of `IPAddress.Any`

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0028      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

On the server machine that supports `IPv6`, [IPv6Any](/en-us/dotnet/api/system.net.ipaddress.ipv6any) is preferred to [Any](/en-us/dotnet/api/system.net.ipaddress.any) because `Any` can be slower than `IPv6Any`. In some cases, `Any` may not work at all. `Any` can be slower due to the [underlying System types implementation](https://github.com/dotnet/runtime/issues/82404).

`127.0.0.1` is the IPv4 loopback address. `::1` is the IPv6 loopback address. `Any` is the wildcard address for IPv4. `IPv6Any` is the wildcard address for IPv6.

Current behavior with IPv6 when using HTTP/1.x or HTTP/2.0:

* `localhost` resolves to `[::1]`.
* `[::1]` isn't accepted by the server, which forces a retry using `127.0.0.1`, creating a repeated cycle.

Using `Any` with the preceding conditions causes the `ASP0028` diagnostic message. Here's an example of the code that can result in these conditions:

```
.UseKestrel().ConfigureKestrel(options =>
{ 
    options.Listen(IPAddress.Any, ...);
})
```

## Rule description

The recommended way to configure Kestrel to listen for incoming connections on all available `IPv6` network interfaces is with `IPv6Any`.

## How to fix violations

For the problematic code, replace `Any` with `IPv6Any`.

Use the [ListenAnyIP(Int32)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenanyip#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-listenanyip\(system-int32\)) method without specifying any arguments:

```
.UseKestrel().ConfigureKestrel(options =>
{ 
-   options.Listen(IPAddress.Any, ...);
+   options.ListenAnyIP(...);
})
```

Or use the [IPv6Any](/en-us/dotnet/api/system.net.ipaddress.ipv6any#system-net-ipaddress-ipv6any) field:

```
.UseKestrel().ConfigureKestrel(options =>
{ 
-   options.Listen(IPAddress.Any, ...);
+   options.Listen(IPAddress.IPv6Any, ...);
})
```

## When to suppress warnings

The `ASP0028` diagnostic has [Information](/en-us/dotnet/api/microsoft.extensions.logging.loglevel) level severity. Suppress this warning if your intention is to disable `IPv6` usage completely on the server, although doing so risks the performance problems mentioned in this article.

`IPv6` can be disabled either system-wide, or for .NET only via the [AppCtx switch or environment variable](https://devblogs.microsoft.com/dotnet/dotnet-6-networking-improvements/#an-option-to-globally-disable-ipv6)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/start
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/start.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Get started with incremental ASP.NET to ASP.NET Core migration

Important

**Before you begin**: This article assumes you have read the [ASP.NET Core migration overview](./?view=aspnetcore-10.0). If you haven't read it yet, please start there to understand the concepts, approach, and benefits of incremental migration.

For a large migration, we recommend setting up an ASP.NET Core app that proxies to the original .NET Framework app. The new proxy enabled app is shown in the following image:

This article provides the practical steps to proceed with an incremental migration after you understand the approach.

## Prerequisites

Before starting your incremental migration, ensure you have:

1. **Read the overview**: [Incremental ASP.NET to ASP.NET Core migration](./?view=aspnetcore-10.0)
2. **A working ASP.NET Framework application** that you want to migrate
3. **Visual Studio 2022** with the latest updates
4. **.NET 8 or later SDK** installed
5. **Understanding of your application's dependencies** and third-party libraries

## Migration Steps Overview

The incremental migration process follows these key steps:

1. [Set up ASP.NET Core Project](#set-up-aspnet-core-project)
2. [Remediate Technical Debt](#remediate-technical-debt)
3. [Identify and address cross-cutting concerns](#identify-and-address-cross-cutting-concerns)
4. [Upgrade supporting libraries](#upgrade-supporting-libraries)

## Set up ASP.NET Core Project

The first step is to create the new ASP.NET Core application that will serve as your proxy.

**What you'll do:**

* Create a new ASP.NET Core project alongside your existing ASP.NET Framework app
* Configure it to proxy requests to your original application using YARP (Yet Another Reverse Proxy)
* Set up the basic infrastructure for incremental migration

**Detailed instructions:**

* See [Remote app setup](inc/remote-app-setup?view=aspnetcore-10.0) to understand how to set up an application for incremental migration.
* See [Learn to upgrade from ASP.NET MVC, Web API, and Web Forms to ASP.NET Core](tooling?view=aspnetcore-10.0) for help in setting up projects required for incremental migration using Visual Studio tooling.

## Remediate Technical Debt

**When to do this step:** Before upgrading any supporting libraries, address technical debt that could complicate the migration process.

Before you begin upgrading your supporting libraries, it's important to clean up technical debt that could interfere with the migration process. This step should be completed first to ensure a smoother upgrade experience.

### Update Package Dependencies

Review and update your NuGet packages to their latest compatible versions:

1. **Audit existing packages**: Use Visual Studio's NuGet Package Manager as the `dotnet` CLI does not work for ASP.NET Framework applications
2. **Update packages incrementally**: Update packages one at a time to avoid compatibility issues
3. **Test after each update**: Ensure your application still functions correctly after each package update
4. **Address breaking changes**: Some package updates may introduce breaking changes that need to be addressed

### Modernize Build Tools

Update your build tools and project configuration:

1. **Update tools**: Ensure you're using a recent version of MSBuild/Visual Studio
2. **Migrate to PackageReference for dependencies**: Consider migrating from `packages.config` to `PackageReference` format if you haven't already in the Web application project
3. **Clean up unused references**: Remove any unused assembly references or NuGet packages
4. **Migrate to SDK-style project files**: Convert your existing project files to the modern SDK-style format. This is essential for compatibility with modern .NET projects and provides better tooling support
5. **Update build scripts**: Review and update any custom build scripts or CI/CD configurations

### Address Code Quality Issues

Fix known code quality issues that could complicate migration:

1. **Fix compiler warnings**: Address any compiler warnings, especially those related to deprecated APIs
2. **Remove dead code**: Clean up unused classes, methods, and other code elements
3. **Update deprecated API usage**: Replace usage of deprecated APIs with their modern equivalents where possible

This preparation work will make the library upgrade process much smoother and reduce the likelihood of encountering complex issues during migration.

## Identify and address cross-cutting concerns

**When to do this step:** While remediating technical debt but before upgrading supporting libraries, identify and configure cross-cutting concerns that affect your entire application.

Cross-cutting concerns are aspects of your application that span multiple layers or components, such as authentication, session management, logging, and caching. These need to be addressed early in the migration process because they affect how your ASP.NET Framework and ASP.NET Core applications communicate and share state during the incremental migration.

The following sections cover the most common cross-cutting concerns. Configure only the ones that apply to your application:

### Session Support Configuration

**Configure this if:** Your ASP.NET Framework application uses session state.

See the general [session migration documentation](areas/session?view=aspnetcore-10.0#remote-app-session-state) for guidance here.

Session is a commonly used feature of ASP.NET that shares the name with a feature in ASP.NET Core, but the APIs are much different. When upgrading libraries that use session state, you'll need to configure session support. See the documentation on [remote session support](areas/session?view=aspnetcore-10.0) for detailed guidance on how to enable session state sharing between your applications.

### Authentication Configuration

**Configure this if:** Your ASP.NET Framework application uses authentication and you want to share authentication state between the old and new applications.

See the general [authentication migration documentation](areas/authentication?view=aspnetcore-10.0) for guidance here.

It is possible to share authentication between the original ASP.NET app and the new ASP.NET Core app by using the System.Web adapters remote authentication feature. This feature allows the ASP.NET Core app to defer authentication to the original ASP.NET app. See the documentation on [remote authentication](areas/authentication?view=aspnetcore-10.0#remote-authentication) for more details.

### Other Cross-Cutting Concerns to Consider

Depending on your application, you may also need to address:

* **Logging**: Ensure consistent logging across both applications. Consider using a shared logging provider or ensuring logs are aggregated properly.
* **Caching**: If your application uses caching (in-memory, distributed, or output caching), plan how to maintain cache consistency between applications.
* **Error Handling**: Establish consistent error handling and reporting across both the ASP.NET Framework and ASP.NET Core applications.
* **Configuration Management**: Plan how configuration settings will be shared or managed between the two applications.
* **Health Monitoring**: Set up monitoring and health checks for both applications during the migration process.
* **Dependency Injection**: If using a DI container in your ASP.NET Framework app, plan the migration to ASP.NET Core's built-in DI container.

## Upgrade supporting libraries

**When to do this step:** Only when you need to migrate specific routes that depend on class libraries containing business logic you'll need to share between the old and new applications.

Note

**Incremental approach**: With the incremental migration process, you don't need to upgrade all your supporting libraries at once. You only need to upgrade the libraries that are required for the specific routes you're currently migrating. This allows you to tackle the migration in smaller, more manageable pieces.

### Library Upgrade Process

Important

Supporting libraries must be upgraded in a **postorder depth-first search ordering**. This means:

1. **Start with leaf dependencies**: Begin with libraries that have no dependencies on other libraries in your solution
2. **Work upward through the dependency tree**: Only upgrade a library after all of its dependencies have been successfully upgraded
3. **End with the main application**: The main ASP.NET Framework application should be the last item to be modified

This ordering is essential because:

* It ensures that when you upgrade a library, all of its dependencies are already compatible
* It prevents circular dependency issues during the upgrade process
* It allows you to test each library independently before moving to its dependents

**NOTE**: You only need to follow this ordering for the subset of libraries required by the routes you're currently migrating, not your entire solution.

**Upgrade process for each library:**

If you have supporting libraries in your solution that you will need to use for the routes you're migrating, they should be upgraded to .NET Standard 2.0, if possible. [GitHub Copilot app modernization](/en-us/dotnet/core/porting/github-copilot-app-modernization/overview) can help you with this. If libraries are unable to target .NET Standard, you can target .NET 8 or later either along with the .NET Framework target in the original project or in a new project alongside the original.

The [System.Web adapters](inc/systemweb-adapters?view=aspnetcore-10.0) can be used in these libraries to enable support for [HttpContext](/en-us/dotnet/api/system.web.httpcontext) usage in class libraries. In order to enable [HttpContext](/en-us/dotnet/api/system.web.httpcontext) usage in a library:

1. Remove reference to `System.Web` in the project file
2. Add the `Microsoft.AspNetCore.SystemWebAdapters` package
3. Enable multi-targeting and add a .NET 8 target or later, or convert the project to .NET Standard 2.0.

This step may require a number of projects to change depending on your solution structure and which routes you're migrating. [GitHub Copilot app modernization](/en-us/dotnet/core/porting/github-copilot-app-modernization/overview) can help you identify which ones need to change and automate a number of steps in the process.

## Next Steps

Once you've completed the setup and library upgrade steps above:

1. **Start small**: Begin by migrating simple, stateless endpoints first
2. **Test thoroughly**: Ensure each migrated component works correctly in both environments
3. **Monitor performance**: Watch for any performance impacts from the proxy setup
4. **Iterate**: Continue migrating components incrementally until the migration is complete

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-23

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/file-uploads
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/file-uploads.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor file uploads

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to upload files in Blazor with the [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component.

## File uploads

Warning

Always follow security best practices when permitting users to upload files. For more information, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations).

Use the [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component to read browser file data into .NET code. The [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component renders an HTML `<input>` element of type `file` for single file uploads. Add the `multiple` attribute to permit the user to upload multiple files at once.

File selection isn't cumulative when using an [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component or its underlying [HTML `<input type="file">`](https://developer.mozilla.org/docs/Web/HTML/Element/input/file), so you can't add files to an existing file selection. The component always replaces the user's initial file selection, so file references from prior selections aren't available.

The following [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component executes the `LoadFiles` method when the [OnChange](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile.onchange#microsoft-aspnetcore-components-forms-inputfile-onchange) ([`change`](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)) event occurs. An [InputFileChangeEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfilechangeeventargs) provides access to the selected file list and details about each file:

```
<InputFile OnChange="LoadFiles" multiple />

@code {
    private void LoadFiles(InputFileChangeEventArgs e)
    {
        ...
    }
}
```

Rendered HTML:

```
<input multiple="" type="file" _bl_2="">
```

Note

In the preceding example, the `<input>` element's `_bl_2` attribute is used for Blazor's internal processing.

To read data from a user-selected file with a [Stream](/en-us/dotnet/api/system.io.stream) that represents the file's bytes, call [IBrowserFile.OpenReadStream](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.openreadstream) on the file and read from the returned stream. For more information, see the [File streams](#file-streams) section.

[OpenReadStream](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.openreadstream) enforces a maximum size in bytes of its [Stream](/en-us/dotnet/api/system.io.stream). Reading one file or multiple files larger than 500 KB results in an exception. This limit prevents developers from accidentally reading large files into memory. The `maxAllowedSize` parameter of [OpenReadStream](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.openreadstream) can be used to specify a larger size if required.

Outside of processing a small file, avoid reading the incoming file stream directly into memory all at once. For example, don't copy all of the file's bytes into a [MemoryStream](/en-us/dotnet/api/system.io.memorystream) or read the entire stream into a byte array all at once. These approaches can result in degraded app performance and potential [Denial of Service (DoS)](security/interactive-server-side-rendering?view=aspnetcore-10.0#denial-of-service-dos-attacks) risk, especially for server-side components. Instead, consider adopting either of the following approaches:

* Copy the stream directly to a file on disk without reading it into memory. Note that Blazor apps executing code on the server aren't able to access the client's file system directly.
* Upload files from the client directly to an external service. For more information, see the [Upload files to an external service](#upload-files-to-an-external-service) section.

In the following examples, `browserFile` implements [IBrowserFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile) to represent an uploaded file. Working implementations for [IBrowserFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile) are shown in the file upload components later in this article.

When calling [OpenReadStream](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.openreadstream), we recommend passing a maximum allowed file size in the `maxAllowedSize` parameter at the limit of the file sizes that you expect to receive. The default value is 500 KB. This article's examples use a maximum allowed file size variable or constant named `maxFileSize` but usually don't show setting a specific value.

✔️Supported: The following approach is **recommended** because the file's [Stream](/en-us/dotnet/api/system.io.stream) is provided directly to the consumer, a [FileStream](/en-us/dotnet/api/system.io.filestream) that creates the file at the provided path:

```
await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(fs);
```

✔️Supported: The following approach is **recommended** for [Microsoft Azure Blob Storage](/en-us/azure/storage/blobs/storage-blobs-overview) because the file's [Stream](/en-us/dotnet/api/system.io.stream) is provided directly to [UploadBlobAsync](/en-us/dotnet/api/azure.storage.blobs.blobcontainerclient.uploadblobasync):

```
await blobContainerClient.UploadBlobAsync(
    trustedFileName, browserFile.OpenReadStream(maxFileSize));
```

✔️Only recommended for small files: The following approach is only **recommended for small files** because the file's [Stream](/en-us/dotnet/api/system.io.stream) content is read into a [MemoryStream](/en-us/dotnet/api/system.io.memorystream) in memory (`memoryStream`), which incurs a performance penalty and [DoS](security/interactive-server-side-rendering?view=aspnetcore-10.0#denial-of-service-dos-attacks) risk. For an example that demonstrates this technique to save a thumbnail image with an [IBrowserFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile) to a database using [Entity Framework Core (EF Core)](/en-us/ef/core/), see the [Save small files directly to a database with EF Core](#save-small-files-directly-to-a-database-with-ef-core) section later in this article.

```
using var memoryStream = new MemoryStream();
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
var smallFileByteArray = memoryStream.ToArray();
```

❌Not recommended: The following approach is **NOT recommended** because the file's [Stream](/en-us/dotnet/api/system.io.stream) content is read into a [String](/en-us/dotnet/api/system.string) in memory (`reader`):

```
var reader = 
    await new StreamReader(browserFile.OpenReadStream(maxFileSize)).ReadToEndAsync();
```

❌Not recommended: The following approach is **NOT recommended** for [Microsoft Azure Blob Storage](/en-us/azure/storage/blobs/storage-blobs-overview) because the file's [Stream](/en-us/dotnet/api/system.io.stream) content is copied into a [MemoryStream](/en-us/dotnet/api/system.io.memorystream) in memory (`memoryStream`) before calling [UploadBlobAsync](/en-us/dotnet/api/azure.storage.blobs.blobcontainerclient.uploadblobasync):

```
var memoryStream = new MemoryStream();
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
    trustedFileName, memoryStream));
```

A component that receives an image file can call the [BrowserFileExtensions.RequestImageFileAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.browserfileextensions.requestimagefileasync) convenience method on the file to resize the image data within the browser's JavaScript runtime before the image is streamed into the app. Use cases for calling [RequestImageFileAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.browserfileextensions.requestimagefileasync) are most appropriate for Blazor WebAssembly apps.

## File size read and upload limits

For Chromium-based browsers (for example, Google Chrome and Microsoft Edge) using the HTTP/2 protocol, HTTPS, and [CORS](../security/cors?view=aspnetcore-10.0), client-side Blazor supports using the [Streams API](https://developer.mozilla.org/docs/Web/API/Streams_API) to permit uploading large files with [request streaming](call-web-api?view=aspnetcore-10.0#client-side-request-streaming).

Without a Chromium browser, HTTP/2 protocol, or HTTPS, client-side Blazor reads the file's bytes into a single JavaScript array buffer when marshaling the data from JavaScript to C#, which is limited to 2 GB or to the device's available memory. Large file uploads may fail for client-side uploads using the [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component.

Client-side Blazor reads the file's bytes into a single JavaScript array buffer when marshaling the data from JavaScript to C#, which is limited to 2 GB or to the device's available memory. Large file uploads may fail for client-side uploads using the [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component. We recommend adopting [request streaming](call-web-api?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-9.0#client-side-request-streaming) with .NET 9 or later.

## Security considerations

### Avoid `IBrowserFile.Size` for file size limits

Avoid using [IBrowserFile.Size](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.size#microsoft-aspnetcore-components-forms-ibrowserfile-size) to impose a limit on the file size. Instead of using the unsafe client-supplied file size, explicitly specify the maximum file size. The following example uses the maximum file size assigned to `maxFileSize`:

```
- var fileContent = new StreamContent(file.OpenReadStream(file.Size));
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
```

### File name security

Never use a client-supplied file name for saving a file to physical storage. Create a safe file name for the file using [Path.GetRandomFileName()](/en-us/dotnet/api/system.io.path.getrandomfilename#system-io-path-getrandomfilename) or [Path.GetTempFileName()](/en-us/dotnet/api/system.io.path.gettempfilename#system-io-path-gettempfilename) to create a full path (including the file name) for temporary storage.

Razor automatically HTML encodes property values for display. The following code is safe to use:

```
@foreach (var file in Model.DatabaseFiles) {
    <tr>
        <td>
            @file.UntrustedName
        </td>
    </tr>
}
```

Outside of Razor, always use [HtmlEncode](/en-us/dotnet/api/system.net.webutility.htmlencode) to safely encode file names from a user's request.

Many implementations must include a check that the file exists; otherwise, the file is overwritten by a file of the same name. Supply additional logic to meet your app's specifications.

## Examples

The following examples demonstrate multiple file upload in a component. [InputFileChangeEventArgs.GetMultipleFiles](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfilechangeeventargs.getmultiplefiles) allows reading multiple files. Specify the maximum number of files to prevent a malicious user from uploading a larger number of files than the app expects. [InputFileChangeEventArgs.File](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfilechangeeventargs.file#microsoft-aspnetcore-components-forms-inputfilechangeeventargs-file) allows reading the first and only file if the file upload doesn't support multiple files.

[InputFileChangeEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfilechangeeventargs) is in the [Microsoft.AspNetCore.Components.Forms](/en-us/dotnet/api/microsoft.aspnetcore.components.forms) namespace, which is typically one of the namespaces in the app's imports file (`_Imports.razor`). When the namespace is present in the imports file, it provides API member access to the app's components.

Namespaces in the imports file aren't applied to C# files (`.cs`). C# files require an explicit [`using`](/en-us/dotnet/csharp/language-reference/language-specification/namespaces#using-directives) directive at the top of the class file:

```
using Microsoft.AspNetCore.Components.Forms;
```

For testing file upload components, you can create test files of any size with [PowerShell](/en-us/powershell/):

```
$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)
```

In the preceding command:

* The `{SIZE}` placeholder is the size of the file in bytes (for example, `2097152` for a 2 MB file).
* The `{PATH}` placeholder is the path and file with file extension (for example, `D:/test_files/testfile2MB.txt`).

### Server-side file upload example

**To use the following code, create a `Development/unsafe_uploads` folder at the root of the app running in the `Development` environment.**

Because the example uses the app's [environment](fundamentals/environments?view=aspnetcore-10.0) as part of the path where files are saved, additional folders are required if other environments are used in testing and production. For example, create a `Staging/unsafe_uploads` folder for the `Staging` environment. Create a `Production/unsafe_uploads` folder for the `Production` environment.

Warning

The example saves files without scanning their contents, and the guidance in this article doesn't take into account additional security best practices for uploaded files. On staging and production systems, disable execute permission on the upload folder and scan files with an anti-virus/anti-malware scanner API immediately after upload. For more information, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations).

`FileUpload1.razor`:

```
@page "/file-upload-1"
@using System 
@using System.IO
@using Microsoft.AspNetCore.Hosting
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment

<PageTitle>File Upload 1</PageTitle>

<h1>File Upload Example 1</h1>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Uploading...</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = [];
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;

    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                var trustedFileName = Path.GetRandomFileName();
                var path = Path.Combine(Environment.ContentRootPath,
                    Environment.EnvironmentName, "unsafe_uploads",
                    trustedFileName);

                await using FileStream fs = new(path, FileMode.Create);
                await file.OpenReadStream(maxFileSize).CopyToAsync(fs);

                loadedFiles.Add(file);

                Logger.LogInformation(
                    "Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
                    file.Name, trustedFileName);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {Filename} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

```
@page "/file-upload-1"
@using System 
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment

<h3>Upload Files</h3>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Uploading...</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;

    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                loadedFiles.Add(file);

                var trustedFileNameForFileStorage = Path.GetRandomFileName();
                var path = Path.Combine(Environment.ContentRootPath,
                        Environment.EnvironmentName, "unsafe_uploads",
                        trustedFileNameForFileStorage);

                await using FileStream fs = new(path, FileMode.Create);
                await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {Filename} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

```
@page "/file-upload-1"
@using System 
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment

<h3>Upload Files</h3>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Uploading...</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;

    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                loadedFiles.Add(file);

                var trustedFileNameForFileStorage = Path.GetRandomFileName();
                var path = Path.Combine(Environment.ContentRootPath,
                        Environment.EnvironmentName, "unsafe_uploads",
                        trustedFileNameForFileStorage);

                await using FileStream fs = new(path, FileMode.Create);
                await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {Filename} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

```
@page "/file-upload-1"
@using System 
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment

<h3>Upload Files</h3>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Uploading...</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;

    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                loadedFiles.Add(file);

                var trustedFileNameForFileStorage = Path.GetRandomFileName();
                var path = Path.Combine(Environment.ContentRootPath,
                        Environment.EnvironmentName, "unsafe_uploads",
                        trustedFileNameForFileStorage);

                await using FileStream fs = new(path, FileMode.Create);
                await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {Filename} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

### Client-side file upload example

The following example processes file bytes and doesn't send files to a destination outside of the app. For an example of a Razor component that sends a file to a server or service, see the following sections:

* [Upload files to a server with client-side rendering (CSR)](#upload-files-to-a-server-with-client-side-rendering-csr)
* [Upload files to an external service](#upload-files-to-an-external-service)

The component assumes that the Interactive WebAssembly render mode (`InteractiveWebAssembly`) is inherited from a parent component or applied globally to the app.

```
@page "/file-upload-1"
@inject ILogger<FileUpload1> Logger

<PageTitle>File Upload 1</PageTitle>

<h1>File Upload Example 1</h1>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Uploading...</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = [];
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;

    private void LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                loadedFiles.Add(file);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {FileName} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

```
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger

<h3>Upload Files</h3>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Uploading...</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;

    private void LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                loadedFiles.Add(file);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {FileName} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

```
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger

<h3>Upload Files</h3>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Uploading...</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;

    private void LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                loadedFiles.Add(file);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {Filename} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

```
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger

<h3>Upload Files</h3>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Uploading...</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;

    private void LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                loadedFiles.Add(file);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {Filename} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

[IBrowserFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile) returns metadata [exposed by the browser](https://developer.mozilla.org/docs/Web/API/File#Instance_properties) as properties. Use this metadata for preliminary validation.

* [Name](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.name#microsoft-aspnetcore-components-forms-ibrowserfile-name)
* [Size](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.size#microsoft-aspnetcore-components-forms-ibrowserfile-size)
* [LastModified](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.lastmodified#microsoft-aspnetcore-components-forms-ibrowserfile-lastmodified)
* [ContentType](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.contenttype#microsoft-aspnetcore-components-forms-ibrowserfile-contenttype)

***Never trust the values of the preceding properties, especially the [Name](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.name#microsoft-aspnetcore-components-forms-ibrowserfile-name) property for display in the UI.*** Treat all user-supplied data as a significant security risk to the app, server, and network. For more information, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations).

## Upload files to a server with server-side rendering

*This section applies to Interactive Server components in Blazor Web Apps or Blazor Server apps.*

The following example demonstrates uploading files from a server-side app to a backend web API controller in a separate app, possibly on a separate server.

In the server-side app's `Program` file, add [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) and related services that allow the app to create [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances:

```
builder.Services.AddHttpClient();
```

For more information, see [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0).

For the examples in this section:

* The web API runs at the URL: `https://localhost:5001`
* The server-side app runs at the URL: `https://localhost:5003`

For testing, the preceding URLs are configured in the projects' `Properties/launchSettings.json` files.

The following `UploadResult` class maintains the result of an uploaded file. When a file fails to upload on the server, an error code is returned in `ErrorCode` for display to the user. A safe file name is generated on the server for each file and returned to the client in `StoredFileName` for display. Files are keyed between the client and server using the unsafe/untrusted file name in `FileName`.

`UploadResult.cs`:

```
public class UploadResult
{
    public bool Uploaded { get; set; }
    public string? FileName { get; set; }
    public string? StoredFileName { get; set; }
    public int ErrorCode { get; set; }
}
```

A security best practice for production apps is to avoid sending error messages to clients that might reveal sensitive information about an app, server, or network. Providing detailed error messages can aid a malicious user in devising attacks on an app, server, or network. The example code in this section only sends back an error code number (`int`) for display by the component client-side if a server-side error occurs. If a user requires assistance with a file upload, they provide the error code to support personnel for support ticket resolution without ever knowing the exact cause of the error.

The following `LazyBrowserFileStream` class defines a custom stream type that lazily calls [OpenReadStream](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.openreadstream) just before the first bytes of the stream are requested. The stream isn't transmitted from the browser to the server until reading the stream begins in .NET.

`LazyBrowserFileStream.cs`:

```
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;

namespace BlazorSample;

internal sealed class LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize) 
    : Stream
{
    private readonly IBrowserFile file = file;
    private readonly int maxAllowedSize = maxAllowedSize;
    private Stream? underlyingStream;
    private bool isDisposed;

    public override bool CanRead => true;

    public override bool CanSeek => false;

    public override bool CanWrite => false;

    public override long Length => file.Size;

    public override long Position
    {
        get => underlyingStream?.Position ?? 0;
        set => throw new NotSupportedException();
    }

    public override void Flush() => underlyingStream?.Flush();

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, 
        CancellationToken cancellationToken)
    {
        EnsureStreamIsOpen();

        return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
    }

    public override ValueTask<int> ReadAsync(Memory<byte> buffer, 
        CancellationToken cancellationToken = default)
    {
        EnsureStreamIsOpen();
        return underlyingStream.ReadAsync(buffer, cancellationToken);
    }

    [MemberNotNull(nameof(underlyingStream))]
    private void EnsureStreamIsOpen() => 
        underlyingStream ??= file.OpenReadStream(maxAllowedSize);

    protected override void Dispose(bool disposing)
    {
        if (isDisposed)
        {
            return;
        }

        underlyingStream?.Dispose();
        isDisposed = true;

        base.Dispose(disposing);
    }

    public override int Read(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();

    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();

    public override void SetLength(long value)
        => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();
}
```

```
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;

namespace BlazorSample;

internal sealed class LazyBrowserFileStream : Stream
{
    private readonly IBrowserFile file;
    private readonly int maxAllowedSize;
    private Stream? underlyingStream;
    private bool isDisposed;

    public override bool CanRead => true;

    public override bool CanSeek => false;

    public override bool CanWrite => false;

    public override long Length => file.Size;

    public override long Position
    {
        get => underlyingStream?.Position ?? 0;
        set => throw new NotSupportedException();
    }

    public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
    {
        this.file = file;
        this.maxAllowedSize = maxAllowedSize;
    }

    public override void Flush()
    {
        underlyingStream?.Flush();
    }

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, 
        CancellationToken cancellationToken)
    {
        EnsureStreamIsOpen();

        return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
    }

    public override ValueTask<int> ReadAsync(Memory<byte> buffer, 
        CancellationToken cancellationToken = default)
    {
        EnsureStreamIsOpen();
        return underlyingStream.ReadAsync(buffer, cancellationToken);
    }

    [MemberNotNull(nameof(underlyingStream))]
    private void EnsureStreamIsOpen()
    {
        underlyingStream ??= file.OpenReadStream(maxAllowedSize);
    }

    protected override void Dispose(bool disposing)
    {
        if (isDisposed)
        {
            return;
        }

        underlyingStream?.Dispose();
        isDisposed = true;

        base.Dispose(disposing);
    }

    public override int Read(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();

    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();

    public override void SetLength(long value)
        => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();
}
```

```
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;

namespace BlazorSample;

internal sealed class LazyBrowserFileStream : Stream
{
    private readonly IBrowserFile file;
    private readonly int maxAllowedSize;
    private Stream? underlyingStream;
    private bool isDisposed;

    public override bool CanRead => true;

    public override bool CanSeek => false;

    public override bool CanWrite => false;

    public override long Length => file.Size;

    public override long Position
    {
        get => underlyingStream?.Position ?? 0;
        set => throw new NotSupportedException();
    }

    public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
    {
        this.file = file;
        this.maxAllowedSize = maxAllowedSize;
    }

    public override void Flush()
    {
        underlyingStream?.Flush();
    }

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, 
        CancellationToken cancellationToken)
    {
        EnsureStreamIsOpen();

        return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
    }

    public override ValueTask<int> ReadAsync(Memory<byte> buffer, 
        CancellationToken cancellationToken = default)
    {
        EnsureStreamIsOpen();
        return underlyingStream.ReadAsync(buffer, cancellationToken);
    }

    [MemberNotNull(nameof(underlyingStream))]
    private void EnsureStreamIsOpen()
    {
        underlyingStream ??= file.OpenReadStream(maxAllowedSize);
    }

    protected override void Dispose(bool disposing)
    {
        if (isDisposed)
        {
            return;
        }

        underlyingStream?.Dispose();
        isDisposed = true;

        base.Dispose(disposing);
    }

    public override int Read(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();

    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();

    public override void SetLength(long value)
        => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();
}
```

```
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorSample;

internal sealed class LazyBrowserFileStream : Stream
{
    private readonly IBrowserFile file;
    private readonly int maxAllowedSize;
    private Stream underlyingStream;
    private bool isDisposed;

    public override bool CanRead => true;

    public override bool CanSeek => false;

    public override bool CanWrite => false;

    public override long Length => file.Size;

    public override long Position
    {
        get => underlyingStream?.Position ?? 0;
        set => throw new NotSupportedException();
    }

    public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
    {
        this.file = file;
        this.maxAllowedSize = maxAllowedSize;
    }

    public override void Flush()
    {
        underlyingStream?.Flush();
    }

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, 
        CancellationToken cancellationToken)
    {
        EnsureStreamIsOpen();

        return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
    }

    public override ValueTask<int> ReadAsync(Memory<byte> buffer, 
        CancellationToken cancellationToken = default)
    {
        EnsureStreamIsOpen();
        return underlyingStream.ReadAsync(buffer, cancellationToken);
    }

    [MemberNotNull(nameof(underlyingStream))]
    private void EnsureStreamIsOpen()
    {
        underlyingStream ??= file.OpenReadStream(maxAllowedSize);
    }

    protected override void Dispose(bool disposing)
    {
        if (isDisposed)
        {
            return;
        }

        underlyingStream?.Dispose();
        isDisposed = true;

        base.Dispose(disposing);
    }

    public override int Read(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();

    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();

    public override void SetLength(long value)
        => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();
}
```

The following `FileUpload2` component:

* Permits users to upload files from the client.
* Displays the untrusted/unsafe file name provided by the client in the UI. The untrusted/unsafe file name is automatically HTML-encoded by Razor for safe display in the UI.

Warning

**Don't trust file names supplied by clients** for:

* Saving the file to a file system or service.
* Display in UIs that don't encode file names automatically or via developer code.

For more information on security considerations when uploading files to a server, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations).

`FileUpload2.razor`:

```
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<PageTitle>File Upload 2</PageTitle>

<h1>File Upload Example 2</h1>

<p>
    This example requires a backend server API to function. For more information, 
    see the <em>Upload files to a server</em> section 
    of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Any())
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = [];
    private List<UploadResult> uploadResults = [];
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        int maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var stream = new LazyBrowserFileStream(file, maxFileSize);
                    var fileContent = new StreamContent(stream);

                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var client = ClientFactory.CreateClient();

            using var response = 
                await client.PostAsync("https://localhost:5001/Filesave", 
                content);

            if (response.IsSuccessStatusCode)
            {
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                };

                using var responseStream =
                    await response.Content.ReadAsStreamAsync();

                var newUploadResults = await JsonSerializer
                    .DeserializeAsync<IList<UploadResult>>(responseStream, options);

                if (newUploadResults is not null)
                {
                    uploadResults = uploadResults.Concat(newUploadResults).ToList();
                }
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>File Upload Example 2</h1>

<p>
    This example requires a backend server API to function. For more information, 
    see the <em>Upload files to a server</em> section 
    of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        int maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var stream = new LazyBrowserFileStream(file, maxFileSize);
                    var fileContent = new StreamContent(stream);
                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var client = ClientFactory.CreateClient();

            using var response = 
                await client.PostAsync("https://localhost:5001/Filesave", 
                content);

            if (response.IsSuccessStatusCode)
            {
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                };

                using var responseStream =
                    await response.Content.ReadAsStreamAsync();

                var newUploadResults = await JsonSerializer
                    .DeserializeAsync<IList<UploadResult>>(responseStream, options);

                if (newUploadResults is not null)
                {
                    uploadResults = uploadResults.Concat(newUploadResults).ToList();
                }
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>File Upload Example 2</h1>

<p>
    This example requires a backend server API to function. For more information, 
    see the <em>Upload files to a server</em> section 
    of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        int maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var stream = new LazyBrowserFileStream(file, maxFileSize);
                    var fileContent = new StreamContent(stream);
                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var client = ClientFactory.CreateClient();

            using var response = 
                await client.PostAsync("https://localhost:5001/Filesave", 
                content);

            if (response.IsSuccessStatusCode)
            {
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                };

                using var responseStream =
                    await response.Content.ReadAsStreamAsync();

                var newUploadResults = await JsonSerializer
                    .DeserializeAsync<IList<UploadResult>>(responseStream, options);

                if (newUploadResults is not null)
                {
                    uploadResults = uploadResults.Concat(newUploadResults).ToList();
                }
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>File Upload Example 2</h1>

<p>
    This example requires a backend server API to function. For more information, 
    see the <em>Upload files to a server</em> section 
    of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        int maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var stream = new LazyBrowserFileStream(file, maxFileSize);
                    var fileContent = new StreamContent(stream);
                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            var client = ClientFactory.CreateClient();

            using var response = 
                await client.PostAsync("https://localhost:5001/Filesave", 
                content);

            if (response.IsSuccessStatusCode)
            {
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                };

                using var responseStream =
                    await response.Content.ReadAsStreamAsync();

                var newUploadResults = await JsonSerializer
                    .DeserializeAsync<IList<UploadResult>>(responseStream, options);

                if (newUploadResults is not null)
                {
                    uploadResults = uploadResults.Concat(newUploadResults).ToList();
                }
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string Name { get; set; }
    }
}
```

If the component limits file uploads to a single file at a time or if the component only adopts client-side rendering (CSR, `InteractiveWebAssembly`), the component can avoid the use of the `LazyBrowserFileStream` and use a [Stream](/en-us/dotnet/api/system.io.stream). The following demonstrates the changes for the `FileUpload2` component:

```
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
```

Remove the `LazyBrowserFileStream` class (`LazyBrowserFileStream.cs`), as it isn't used.

If the component limits file uploads to a single file at a time, the component can avoid the use of the `LazyBrowserFileStream` and use a [Stream](/en-us/dotnet/api/system.io.stream). The following demonstrates the changes for the `FileUpload2` component:

```
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
```

Remove the `LazyBrowserFileStream` class (`LazyBrowserFileStream.cs`), as it isn't used.

The following controller in the web API project saves uploaded files from the client.

Important

The controller in this section is intended for use in a separate web API project from the Blazor app. The web API should [mitigate Cross-Site Request Forgery (XSRF/CSRF) attacks](../security/anti-request-forgery?view=aspnetcore-10.0) if file upload users are authenticated.

Note

Binding form values with the [`[FromForm]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) isn't natively supported for [Minimal APIs](../fundamentals/minimal-apis?view=aspnetcore-6.0#explicit-parameter-binding) in ASP.NET Core in .NET 6. Therefore, the following `Filesave` controller example can't be converted to use Minimal APIs. Support for binding from form values with Minimal APIs is available in ASP.NET Core in .NET 7 or later.

**To use the following code, create a `Development/unsafe_uploads` folder at the root of the web API project for the app running in the `Development` environment.**

Because the example uses the app's [environment](fundamentals/environments?view=aspnetcore-10.0) as part of the path where files are saved, additional folders are required if other environments are used in testing and production. For example, create a `Staging/unsafe_uploads` folder for the `Staging` environment. Create a `Production/unsafe_uploads` folder for the `Production` environment.

Warning

The example saves files without scanning their contents, and the guidance in this article doesn't take into account additional security best practices for uploaded files. On staging and production systems, disable execute permission on the upload folder and scan files with an anti-virus/anti-malware scanner API immediately after upload. For more information, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations).

`Controllers/FilesaveController.cs`:

```
using System.Net;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("[controller]")]
public class FilesaveController(
    IHostEnvironment env, ILogger<FilesaveController> logger) 
    : ControllerBase
{
    [HttpPost]
    public async Task<ActionResult<IList<UploadResult>>> PostFile(
        [FromForm] IEnumerable<IFormFile> files)
    {
        var maxAllowedFiles = 3;
        long maxFileSize = 1024 * 15;
        var filesProcessed = 0;
        var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
        List<UploadResult> uploadResults = [];

        foreach (var file in files)
        {
            var uploadResult = new UploadResult();
            string trustedFileNameForFileStorage;
            var untrustedFileName = file.FileName;
            uploadResult.FileName = untrustedFileName;
            var trustedFileNameForDisplay =
                WebUtility.HtmlEncode(untrustedFileName);

            if (filesProcessed < maxAllowedFiles)
            {
                if (file.Length == 0)
                {
                    logger.LogInformation("{FileName} length is 0 (Err: 1)",
                        trustedFileNameForDisplay);
                    uploadResult.ErrorCode = 1;
                }
                else if (file.Length > maxFileSize)
                {
                    logger.LogInformation("{FileName} of {Length} bytes is " +
                        "larger than the limit of {Limit} bytes (Err: 2)",
                        trustedFileNameForDisplay, file.Length, maxFileSize);
                    uploadResult.ErrorCode = 2;
                }
                else
                {
                    try
                    {
                        trustedFileNameForFileStorage = Path.GetRandomFileName();
                        var path = Path.Combine(env.ContentRootPath,
                            env.EnvironmentName, "unsafe_uploads",
                            trustedFileNameForFileStorage);

                        await using FileStream fs = new(path, FileMode.Create);
                        await file.CopyToAsync(fs);

                        logger.LogInformation("{FileName} saved at {Path}",
                            trustedFileNameForDisplay, path);
                        uploadResult.Uploaded = true;
                        uploadResult.StoredFileName = trustedFileNameForFileStorage;
                    }
                    catch (IOException ex)
                    {
                        logger.LogError("{FileName} error on upload (Err: 3): {Message}",
                            trustedFileNameForDisplay, ex.Message);
                        uploadResult.ErrorCode = 3;
                    }
                }

                filesProcessed++;
            }
            else
            {
                logger.LogInformation("{FileName} not uploaded because the " +
                    "request exceeded the allowed {Count} of files (Err: 4)",
                    trustedFileNameForDisplay, maxAllowedFiles);
                uploadResult.ErrorCode = 4;
            }

            uploadResults.Add(uploadResult);
        }

        return new CreatedResult(resourcePath, uploadResults);
    }
}
```

In the preceding code, [GetRandomFileName](/en-us/dotnet/api/system.io.path.getrandomfilename) is called to generate a secure file name. Never trust the file name provided by the browser, as a cyberattacker may choose an existing file name that overwrites an existing file or send a path that attempts to write outside of the app.

The server app must register controller services and map controller endpoints. For more information, see [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0).

## Upload files to a server with client-side rendering (CSR)

*This section applies to client-side rendered (CSR) components in Blazor Web Apps or Blazor WebAssembly apps.*

The following example demonstrates uploading files to a backend web API controller in a separate app, possibly on a separate server, from a component in a Blazor Web App that adopts CSR or a component in a Blazor WebAssembly app.

The example adopts [request streaming](call-web-api?view=aspnetcore-10.0#client-side-request-streaming) for a Chromium-based browser (for example, Google Chrome or Microsoft Edge) with HTTP/2 protocol and HTTPS. If request streaming can't be used, Blazor gracefully degrades to [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) without request streaming. For more information, see the [File size read and upload limits](#file-size-read-and-upload-limits) section.

The following `UploadResult` class maintains the result of an uploaded file. When a file fails to upload on the server, an error code is returned in `ErrorCode` for display to the user. A safe file name is generated on the server for each file and returned to the client in `StoredFileName` for display. Files are keyed between the client and server using the unsafe/untrusted file name in `FileName`.

`UploadResult.cs`:

```
public class UploadResult
{
    public bool Uploaded { get; set; }
    public string? FileName { get; set; }
    public string? StoredFileName { get; set; }
    public int ErrorCode { get; set; }
}
```

Note

The preceding `UploadResult` class can be shared between client- and server-based projects. When client and server projects share the class, add an import to each project's imports file (`_Imports.razor`) for the shared project. For example:

```
@using BlazorSample.Shared
```

The following `FileUpload2` component:

* Permits users to upload files from the client.
* Displays the untrusted/unsafe file name provided by the client in the UI. The untrusted/unsafe file name is automatically HTML-encoded by Razor for safe display in the UI.

A security best practice for production apps is to avoid sending error messages to clients that might reveal sensitive information about an app, server, or network. Providing detailed error messages can aid a malicious user in devising attacks on an app, server, or network. The example code in this section only sends back an error code number (`int`) for display by the component client-side if a server-side error occurs. If a user requires assistance with a file upload, they provide the error code to support personnel for support ticket resolution without ever knowing the exact cause of the error.

Warning

**Don't trust file names supplied by clients** for:

* Saving the file to a file system or service.
* Display in UIs that don't encode file names automatically or via developer code.

For more information on security considerations when uploading files to a server, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations).

In the Blazor Web App server project, add [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) and related services in the project's `Program` file:

```
builder.Services.AddHttpClient();
```

The [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) services must be added to the server project because the client-side component is prerendered on the server. If you [disable prerendering for the following component](components/prerender?view=aspnetcore-10.0#disable-prerendering), you aren't required to provide the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) services in the server project and don't need to add the preceding line to the server project.

For more information on adding [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) services to an ASP.NET Core app, see [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0).

The client project (`.Client`) of a Blazor Web App must also register an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) for HTTP POST requests to a backend web API controller. Confirm or add the following to the client project's `Program` file:

```
builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
```

The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` ([IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress)), which gets the base address for the app and is typically derived from the `<base>` tag's `href` value in the host page. If you're calling an external web API, set the URI to the web API's base address.

A standalone Blazor WebAssembly app that uploads files to a separate server web API either uses a [named `HttpClient`](call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory) or sets the default [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service registration to point to the web API's endpoint. In the following example where the web API is hosted locally at port 5001, the base address is `https://localhost:5001`:

```
builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri("https://localhost:5001") });
```

In a Blazor Web App, add the [Microsoft.AspNetCore.Components.WebAssembly.Http](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http) namespace to the component's directives:

```
@using Microsoft.AspNetCore.Components.WebAssembly.Http
```

`FileUpload2.razor`:

```
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using System.Net
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger

<PageTitle>File Upload 2</PageTitle>

<h1>File Upload Example 2</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                       out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));

                    fileContent.Headers.ContentType =
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}",
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name,
                            ErrorCode = 6,
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            using var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave");
            request.SetBrowserRequestStreamingEnabled(true);
            request.Content = content;

            using var response = await Http.SendAsync(request);

            var newUploadResults = await response.Content
                .ReadFromJsonAsync<IList<UploadResult>>();

            if (newUploadResults is not null)
            {
                uploadResults = uploadResults.Concat(newUploadResults).ToList();
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger

<PageTitle>File Upload 2</PageTitle>

<h1>File Upload Example 2</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = [];
    private List<UploadResult> uploadResults = [];
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));

                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            using var response = await Http.PostAsync("/Filesave", content);

            var newUploadResults = await response.Content
                .ReadFromJsonAsync<IList<UploadResult>>();

            if (newUploadResults is not null)
            {
                uploadResults = uploadResults.Concat(newUploadResults).ToList();
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));

                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            using var response = await Http.PostAsync("/Filesave", content);

            var newUploadResults = await response.Content
                .ReadFromJsonAsync<IList<UploadResult>>();

            if (newUploadResults is not null)
            {
                uploadResults = uploadResults.Concat(newUploadResults).ToList();
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));

                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            using var response = await Http.PostAsync("/Filesave", content);

            var newUploadResults = await response.Content
                .ReadFromJsonAsync<IList<UploadResult>>();

            if (newUploadResults is not null)
            {
                uploadResults = uploadResults.Concat(newUploadResults).ToList();
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();

        if (!result.Uploaded)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 15;
        var upload = false;

        using var content = new MultipartFormDataContent();

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            if (uploadResults.SingleOrDefault(
                f => f.FileName == file.Name) is null)
            {
                try
                {
                    files.Add(new() { Name = file.Name });

                    var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));

                    fileContent.Headers.ContentType = 
                        new MediaTypeHeaderValue(file.ContentType);

                    content.Add(
                        content: fileContent,
                        name: "\"files\"",
                        fileName: file.Name);

                    upload = true;
                }
                catch (Exception ex)
                {
                    Logger.LogInformation(
                        "{FileName} not uploaded (Err: 6): {Message}", 
                        file.Name, ex.Message);

                    uploadResults.Add(
                        new()
                        {
                            FileName = file.Name, 
                            ErrorCode = 6, 
                            Uploaded = false
                        });
                }
            }
        }

        if (upload)
        {
            using var response = await Http.PostAsync("/Filesave", content);

            var newUploadResults = await response.Content
                .ReadFromJsonAsync<IList<UploadResult>>();

            uploadResults = uploadResults.Concat(newUploadResults).ToList();
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName);

        if (result is null)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result = new();
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string Name { get; set; }
    }
}
```

The following controller in the server-side project saves uploaded files from the client.

Note

Binding form values with the [`[FromForm]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) isn't natively supported for [Minimal APIs](../fundamentals/minimal-apis?view=aspnetcore-6.0#explicit-parameter-binding) in ASP.NET Core in .NET 6. Therefore, the following `Filesave` controller example can't be converted to use Minimal APIs. Support for binding from form values with Minimal APIs is available in ASP.NET Core in .NET 7 or later.

**To use the following code, create a `Development/unsafe_uploads` folder at the root of the server-side project for the app running in the `Development` environment.**

Because the example uses the app's [environment](fundamentals/environments?view=aspnetcore-10.0) as part of the path where files are saved, additional folders are required if other environments are used in testing and production. For example, create a `Staging/unsafe_uploads` folder for the `Staging` environment. Create a `Production/unsafe_uploads` folder for the `Production` environment.

Warning

The example saves files without scanning their contents, and the guidance in this article doesn't take into account additional security best practices for uploaded files. On staging and production systems, disable execute permission on the upload folder and scan files with an anti-virus/anti-malware scanner API immediately after upload. For more information, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations).

In the following example for a hosted Blazor WebAssembly app or where a shared project is used to supply the `UploadResult` class, add the shared project's namespace:

```
using BlazorSample.Shared;
```

We recommend using a namespace for the following controller (for example: `namespace BlazorSample.Controllers`).

`Controllers/FilesaveController.cs`:

```
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("[controller]")]
public class FilesaveController(
    IHostEnvironment env, ILogger<FilesaveController> logger) 
    : ControllerBase
{
    [HttpPost]
    public async Task<ActionResult<IList<UploadResult>>> PostFile(
        [FromForm] IEnumerable<IFormFile> files)
    {
        var maxAllowedFiles = 3;
        long maxFileSize = 1024 * 15;
        var filesProcessed = 0;
        var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
        List<UploadResult> uploadResults = [];

        foreach (var file in files)
        {
            var uploadResult = new UploadResult();
            string trustedFileNameForFileStorage;
            var untrustedFileName = file.FileName;
            uploadResult.FileName = untrustedFileName;
            var trustedFileNameForDisplay =
                WebUtility.HtmlEncode(untrustedFileName);

            if (filesProcessed < maxAllowedFiles)
            {
                if (file.Length == 0)
                {
                    logger.LogInformation("{FileName} length is 0 (Err: 1)",
                        trustedFileNameForDisplay);
                    uploadResult.ErrorCode = 1;
                }
                else if (file.Length > maxFileSize)
                {
                    logger.LogInformation("{FileName} of {Length} bytes is " +
                        "larger than the limit of {Limit} bytes (Err: 2)",
                        trustedFileNameForDisplay, file.Length, maxFileSize);
                    uploadResult.ErrorCode = 2;
                }
                else
                {
                    try
                    {
                        trustedFileNameForFileStorage = Path.GetRandomFileName();
                        var path = Path.Combine(env.ContentRootPath,
                            env.EnvironmentName, "unsafe_uploads",
                            trustedFileNameForFileStorage);

                        await using FileStream fs = new(path, FileMode.Create);
                        await file.CopyToAsync(fs);

                        logger.LogInformation("{FileName} saved at {Path}",
                            trustedFileNameForDisplay, path);
                        uploadResult.Uploaded = true;
                        uploadResult.StoredFileName = trustedFileNameForFileStorage;
                    }
                    catch (IOException ex)
                    {
                        logger.LogError("{FileName} error on upload (Err: 3): {Message}",
                            trustedFileNameForDisplay, ex.Message);
                        uploadResult.ErrorCode = 3;
                    }
                }

                filesProcessed++;
            }
            else
            {
                logger.LogInformation("{FileName} not uploaded because the " +
                    "request exceeded the allowed {Count} of files (Err: 4)",
                    trustedFileNameForDisplay, maxAllowedFiles);
                uploadResult.ErrorCode = 4;
            }

            uploadResults.Add(uploadResult);
        }

        return new CreatedResult(resourcePath, uploadResults);
    }
}
```

In the preceding code, [GetRandomFileName](/en-us/dotnet/api/system.io.path.getrandomfilename) is called to generate a secure file name. Never trust the file name provided by the browser, as a cyberattacker may choose an existing file name that overwrites an existing file or send a path that attempts to write outside of the app.

The server app must register controller services and map controller endpoints. For more information, see [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0). We recommend adding controller services with [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews) in order to automatically [mitigate Cross-Site Request Forgery (XSRF/CSRF) attacks](../security/anti-request-forgery?view=aspnetcore-10.0) for authenticated users. If you merely use [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers), antiforgery isn't enabled automatically. For more information, see [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0).

Cross-Origin Requests (CORS) configuration on the server is required for [request streaming](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests) when the server is hosted at a different origin, and a preflight request is always made by the client. In the service configuration of the server's `Program` file (the server project of a Blazor Web App or the backend server web API of a Blazor WebAssembly app), the following default CORS policy is suitable for testing with the examples in this article. The client makes the local request from port 5003. Change the port number to match the client app port that you're using:

Configure Cross-Origin Requests (CORS) on the server. In the service configuration of the server's `Program` file (the server project of a Blazor Web App or the backend server web API of a Blazor WebAssembly app), the following default CORS policy is suitable for testing with the examples in this article. The client makes the local request from port 5003. Change the port number to match the client app port that you're using:

Configure Cross-Origin Requests (CORS) on the server. In the service configuration of the backend server web API's `Program` file, the following default CORS policy is suitable for testing with the examples in this article. The client makes the local request from port 5003. Change the port number to match the client app port that you're using:

```
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        policy =>
        {
            policy.WithOrigins("https://localhost:5003")
                  .AllowAnyMethod()
                  .AllowAnyHeader();
        });
});
```

After calling [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) in the `Program` file, call [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) to add CORS middleware:

```
app.UseCors();
```

For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0).

Configure the server's maximum request body size and multipart body length limits if the limits constrain the upload size.

For the Kestrel server, set [MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxrequestbodysize#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxrequestbodysize) (default: 30,000,000 bytes) and [FormOptions.MultipartBodyLengthLimit](/en-us/dotnet/api/microsoft.aspnetcore.http.features.formoptions.multipartbodylengthlimit#microsoft-aspnetcore-http-features-formoptions-multipartbodylengthlimit) (default: 134,217,728 bytes). Set the `maxFileSize` variable in the component and the controller to the same value.

In the following `Program` file Kestrel configuration (the server project of a Blazor Web App or the backend server web API of a Blazor WebAssembly app), the `{LIMIT}` placeholder is the limit in bytes:

```
using Microsoft.AspNetCore.Http.Features;

...

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxRequestBodySize = {LIMIT};
});

builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = {LIMIT};
});
```

## Cancel a file upload

A file upload component can detect when a user has cancelled an upload by using a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) when calling into the [IBrowserFile.OpenReadStream](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.openreadstream) or [StreamReader.ReadAsync](/en-us/dotnet/api/system.io.streamreader.readasync).

Create a [CancellationTokenSource](/en-us/dotnet/api/system.threading.cancellationtokensource) for the `InputFile` component. At the start of the `OnInputFileChange` method, check if a previous upload is in progress.

If a file upload is in progress:

* Call [Cancel](/en-us/dotnet/api/system.threading.cancellationtokensource.cancel) on the previous upload.
* Create a new [CancellationTokenSource](/en-us/dotnet/api/system.threading.cancellationtokensource) for the next upload and pass the [CancellationTokenSource.Token](/en-us/dotnet/api/system.threading.cancellationtokensource.token#system-threading-cancellationtokensource-token) to [OpenReadStream](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.openreadstream) or [ReadAsync](/en-us/dotnet/api/system.io.streamreader.readasync).

## Upload files server-side with progress

The following example demonstrates how to upload files in a server-side app with upload progress displayed to the user.

To use the following example in a test app:

* **Create a folder to save uploaded files for the `Development` environment: `Development/unsafe_uploads`.**
* Configure the maximum file size (`maxFileSize`, 15 KB in the following example) and maximum number of allowed files (`maxAllowedFiles`, 3 in the following example).
* Set the buffer to a different value (10 KB in the following example), if desired, for increased granularity in progress reporting. We don't recommended using a buffer larger than 30 KB due to performance and security concerns.

Warning

The example saves files without scanning their contents, and the guidance in this article doesn't take into account additional security best practices for uploaded files. On staging and production systems, disable execute permission on the upload folder and scan files with an anti-virus/anti-malware scanner API immediately after upload. For more information, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations).

`FileUpload3.razor`:

```
@page "/file-upload-3"
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment

<PageTitle>File Upload 3</PageTitle>

<h1>File Upload Example 3</h1>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = [];
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;
    private decimal progressPercent;

    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();
        progressPercent = 0;

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                var trustedFileName = Path.GetRandomFileName();
                var path = Path.Combine(Environment.ContentRootPath,
                    Environment.EnvironmentName, "unsafe_uploads", trustedFileName);

                await using FileStream writeStream = new(path, FileMode.Create);
                using var readStream = file.OpenReadStream(maxFileSize);
                var bytesRead = 0;
                var totalRead = 0;
                var buffer = new byte[1024 * 10];

                while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
                {
                    totalRead += bytesRead;
                    await writeStream.WriteAsync(buffer, 0, bytesRead);
                    progressPercent = Decimal.Divide(totalRead, file.Size);
                    StateHasChanged();
                }

                loadedFiles.Add(file);

                Logger.LogInformation(
                    "Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
                    file.Name, trustedFileName);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {FileName} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

```
@page "/file-upload-3"
@using System 
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment

<h3>Upload Files</h3>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;
    private decimal progressPercent;

    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();
        progressPercent = 0;

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                var trustedFileName = Path.GetRandomFileName();
                var path = Path.Combine(Environment.ContentRootPath,
                    Environment.EnvironmentName, "unsafe_uploads", trustedFileName);

                await using FileStream writeStream = new(path, FileMode.Create);
                using var readStream = file.OpenReadStream(maxFileSize);
                var bytesRead = 0;
                var totalRead = 0;
                var buffer = new byte[1024 * 10];

                while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
                {
                    totalRead += bytesRead;

                    await writeStream.WriteAsync(buffer, 0, bytesRead);

                    progressPercent = Decimal.Divide(totalRead, file.Size);

                    StateHasChanged();
                }

                loadedFiles.Add(file);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {FileName} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

```
@page "/file-upload-3"
@using System 
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment

<h3>Upload Files</h3>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;
    private decimal progressPercent;

    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();
        progressPercent = 0;

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                var trustedFileName = Path.GetRandomFileName();
                var path = Path.Combine(Environment.ContentRootPath,
                    Environment.EnvironmentName, "unsafe_uploads", trustedFileName);

                await using FileStream writeStream = new(path, FileMode.Create);
                using var readStream = file.OpenReadStream(maxFileSize);
                var bytesRead = 0;
                var totalRead = 0;
                var buffer = new byte[1024 * 10];

                while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
                {
                    totalRead += bytesRead;

                    await writeStream.WriteAsync(buffer, 0, bytesRead);

                    progressPercent = Decimal.Divide(totalRead, file.Size);

                    StateHasChanged();
                }

                loadedFiles.Add(file);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {Filename} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

```
@page "/file-upload-3"
@using System 
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment

<h3>Upload Files</h3>

<p>
    <label>
        Max file size:
        <input type="number" @bind="maxFileSize" />
    </label>
</p>

<p>
    <label>
        Max allowed files:
        <input type="number" @bind="maxAllowedFiles" />
    </label>
</p>

<p>
    <label>
        Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
        <InputFile OnChange="LoadFiles" multiple />
    </label>
</p>

@if (isLoading)
{
    <p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
    <ul>
        @foreach (var file in loadedFiles)
        {
            <li>
                <ul>
                    <li>Name: @file.Name</li>
                    <li>Last modified: @file.LastModified.ToString()</li>
                    <li>Size (bytes): @file.Size</li>
                    <li>Content type: @file.ContentType</li>
                </ul>
            </li>
        }
    </ul>
}

@code {
    private List<IBrowserFile> loadedFiles = new();
    private long maxFileSize = 1024 * 15;
    private int maxAllowedFiles = 3;
    private bool isLoading;
    private decimal progressPercent;

    private async Task LoadFiles(InputFileChangeEventArgs e)
    {
        isLoading = true;
        loadedFiles.Clear();
        progressPercent = 0;

        foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
        {
            try
            {
                var trustedFileName = Path.GetRandomFileName();
                var path = Path.Combine(Environment.ContentRootPath,
                    Environment.EnvironmentName, "unsafe_uploads", trustedFileName);

                await using FileStream writeStream = new(path, FileMode.Create);
                using var readStream = file.OpenReadStream(maxFileSize);
                var bytesRead = 0;
                var totalRead = 0;
                var buffer = new byte[1024 * 10];

                while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
                {
                    totalRead += bytesRead;

                    await writeStream.WriteAsync(buffer, 0, bytesRead);

                    progressPercent = Decimal.Divide(totalRead, file.Size);

                    StateHasChanged();
                }

                loadedFiles.Add(file);
            }
            catch (Exception ex)
            {
                Logger.LogError("File: {Filename} Error: {Error}", 
                    file.Name, ex.Message);
            }
        }

        isLoading = false;
    }
}
```

For more information, see the following API resources:

* [FileStream](/en-us/dotnet/api/system.io.filestream): Provides a [Stream](/en-us/dotnet/api/system.io.stream) for a file, supporting both synchronous and asynchronous read and write operations.
* [FileStream.ReadAsync](/en-us/dotnet/api/system.io.filestream.readasync): The preceding `FileUpload3` component reads the stream asynchronously with [ReadAsync](/en-us/dotnet/api/system.io.filestream.readasync). Reading a stream synchronously with [Read](/en-us/dotnet/api/system.io.filestream.read) isn't supported in Razor components.

## File streams

With server interactivity, file data is streamed over the SignalR connection into .NET code on the server as the file is read.

[RemoteBrowserFileStreamOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.remotebrowserfilestreamoptions) allows configuring file upload characteristics.

For a WebAssembly-rendered component, file data is streamed directly into the .NET code within the browser.

## Upload image preview

For an image preview of uploading images, start by adding an `InputFile` component with a component reference and an `OnChange` handler:

```
<InputFile @ref="inputFile" OnChange="ShowPreview" />
```

Add an image element with an [element reference](javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#capture-references-to-elements), which serves as the placeholder for the image preview:

```
<img @ref="previewImageElem" />
```

Add the associated references:

```
@code {
    private InputFile? inputFile;
    private ElementReference previewImageElem;
}
```

In JavaScript, add a function called with an HTML [`input`](https://developer.mozilla.org/docs/Web/HTML/Element/input/) and [`img`](https://developer.mozilla.org/docs/Web/HTML/Element/img) element that performs the following:

* Extracts the selected file.
* Creates an object URL with [`createObjectURL`](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL).
* Sets an event listener to revoke the object URL with [`revokeObjectURL`](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL) after the image is loaded, so memory isn't leaked.
* Sets the `img` element's source to display the image.

```
window.previewImage = (inputElem, imgElem) => {
  const url = URL.createObjectURL(inputElem.files[0]);
  imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
  imgElem.src = url;
}
```

Finally, use an injected [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) to add the `OnChange` handler that calls the JavaScript function:

```
@inject IJSRuntime JS

...

@code {
    ...

    private async Task ShowPreview() => await JS.InvokeVoidAsync(
        "previewImage", inputFile!.Element, previewImageElem);
}
```

The preceding example is for uploading a single image. The approach can be expanded to support `multiple` images.

The following `FileUpload4` component shows the complete example.

`FileUpload4.razor`:

```
@page "/file-upload-4"
@inject IJSRuntime JS

<h1>File Upload Example</h1>

<InputFile @ref="inputFile" OnChange="ShowPreview" />

<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />

@code {
    private InputFile? inputFile;
    private ElementReference previewImageElem;

    private async Task ShowPreview() => await JS.InvokeVoidAsync(
        "previewImage", inputFile!.Element, previewImageElem);
}
```

```
@page "/file-upload-4"
@inject IJSRuntime JS

<h1>File Upload Example</h1>

<InputFile @ref="inputFile" OnChange="ShowPreview" />

<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />

@code {
    private InputFile? inputFile;
    private ElementReference previewImageElem;

    private async Task ShowPreview() => await JS.InvokeVoidAsync(
        "previewImage", inputFile!.Element, previewImageElem);
}
```

## Save small files directly to a database with EF Core

Many ASP.NET Core apps use [Entity Framework Core (EF Core)](/en-us/ef/core/) to manage database operations. Saving thumbnails and avatars directly to the database is a common requirement. This section demonstrates a general approach that can be further enhanced for production apps.

The following pattern:

* Is based on the [Blazor movie database tutorial app](tutorials/movie-database-app/?view=aspnetcore-10.0).
* Can be enhanced with additional code for file size and content type [validation feedback](forms/validation?view=aspnetcore-10.0).
* Incurs a performance penalty and [DoS](security/interactive-server-side-rendering?view=aspnetcore-10.0#denial-of-service-dos-attacks) risk. Carefully weigh the risk when reading any file into memory and consider alternative approaches, especially for larger files. Alternative approaches include saving files directly to disk or a third-party service for antivirus/antimalware checks, further processing, and serving to clients.

For the following example to work in a Blazor Web App (.NET 8 or later), the component must adopt an [interactive render mode](fundamentals/?view=aspnetcore-10.0#static-and-interactive-rendering-concepts) (for example, `@rendermode InteractiveServer`) to call `HandleSelectedThumbnail` on an `InputFile` component file change (`OnChange` parameter/event). Blazor Server app components are always interactive and don't require a render mode.

In the following example, a small thumbnail (<= 100 KB) in an [IBrowserFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile) is saved to a database with EF Core. If a file isn't selected by the user for the `InputFile` component, a default thumbnail is saved to the database.

The default thumbnail (`default-thumbnail.jpg`) is at the project root with a **Copy to Output Directory** setting of **Copy if newer**:

The `Movie` model (`Movie.cs`) has a property (`Thumbnail`) to hold the thumbnail image data:

```
[Column(TypeName = "varbinary(MAX)")]
public byte[]? Thumbnail { get; set; }
```

Image data is stored as bytes in the database as [`varbinary(MAX)`](/en-us/sql/t-sql/data-types/binary-and-varbinary-transact-sql). The app base-64 encodes the bytes for display because base-64 encoded data is roughly a third larger than the raw bytes of the image, thus base-64 image data requires additional database storage and reduces the performance of database read/write operations.

Components that display the thumbnail pass image data to the `img` tag's `src` attribute as JPEG, base-64 encoded data:

```
<img src="data:image/jpeg;base64,@Convert.ToBase64String(movie.Thumbnail)" 
    alt="User thumbnail" />
```

In the following `Create` component, an image upload is processed. You can enhance the example further with custom validation for file type and size using the approaches in [ASP.NET Core Blazor forms validation](forms/validation?view=aspnetcore-10.0). To see the full `Create` component without the thumbnail upload code in the following example, see the `BlazorWebAppMovies` sample app in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples).

`Components/Pages/MoviePages/Create.razor`:

```
@page "/movies/create"
@rendermode InteractiveServer
@using Microsoft.EntityFrameworkCore
@using BlazorWebAppMovies.Models
@inject IDbContextFactory<BlazorWebAppMovies.Data.BlazorWebAppMoviesContext> DbFactory
@inject NavigationManager NavigationManager

...

<div class="row">
    <div class="col-md-4">
        <EditForm method="post" Model="Movie" OnValidSubmit="AddMovie" 
            FormName="create" Enhance>
            <DataAnnotationsValidator />
            <ValidationSummary class="text-danger" role="alert"/>

            ...

            <div class="mb-3">
                <label for="thumbnail" class="form-label">Thumbnail:</label>
                <InputFile id="thumbnail" OnChange="HandleSelectedThumbnail" 
                    class="form-control" />
            </div>
            <button type="submit" class="btn btn-primary">Create</button>
        </EditForm>
    </div>
</div>

...

@code {
    private const long maxFileSize = 102400;
    private IBrowserFile? browserFile;

    [SupplyParameterFromForm]
    private Movie Movie { get; set; } = new();

    private void HandleSelectedThumbnail(InputFileChangeEventArgs e)
    {
        browserFile = e.File;
    }

    private async Task AddMovie()
    {
        using var context = DbFactory.CreateDbContext();

        if (browserFile?.Size > 0 && browserFile?.Size <= maxFileSize)
        {
            using var memoryStream = new MemoryStream();
            await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);

            Movie.Thumbnail = memoryStream.ToArray();
        }
        else
        {
            Movie.Thumbnail = File.ReadAllBytes(
                $"{AppDomain.CurrentDomain.BaseDirectory}default_thumbnail.jpg");
        }

        context.Movie.Add(Movie);
        await context.SaveChangesAsync();
        NavigationManager.NavigateTo("/movies");
    }
}
```

The same approach would be adopted in the `Edit` component with an interactive render mode if users were allowed to edit a movie's thumbnail image.

## Upload files to an external service

Instead of an app handling file upload bytes and the app's server receiving uploaded files, clients can directly upload files to an external service. The app can safely process the files from the external service on demand. This approach hardens the app and its server against malicious attacks and potential performance problems.

Consider an approach that uses [Azure Files](https://azure.microsoft.com/services/storage/files/), [Azure Blob Storage](https://azure.microsoft.com/services/storage/blobs/), or a third-party service with the following potential benefits:

* Upload files from the client directly to an external service with a JavaScript client library or REST API. For example, Azure offers the following client libraries and APIs:

  * [Azure Storage File Share client library](/en-us/javascript/api/overview/azure/storage-file-share-readme)
  * [Azure Files REST API](/en-us/rest/api/storageservices/file-service-rest-api)
  * [Azure Storage Blob client library for JavaScript](/en-us/javascript/api/overview/azure/storage-blob-readme)
  * [Blob service REST API](/en-us/rest/api/storageservices/blob-service-rest-api)

* Authorize user uploads with a user-delegated shared access signature (SAS) token generated by the app (server-side) for each client file upload. For example, Azure offers the following SAS features:

  * [Azure Storage File Share client library for JavaScript: with SAS Token](/en-us/javascript/api/overview/azure/storage-file-share-readme#with-sas-token)
  * [Azure Storage Blob client library for JavaScript: with SAS Token](/en-us/javascript/api/overview/azure/storage-blob-readme#with-sas-token)

* Provide automatic redundancy and file share backup.

* Limit uploads with quotas. Note that Azure Blob Storage's quotas are set at the account level, not the container level. However, Azure Files quotas are at the file share level and might provide better control over upload limits. For more information, see the Azure documents linked earlier in this list.

* Secure files with server-side encryption (SSE).

For more information on Azure Blob Storage and Azure Files, see the [Azure Storage documentation](/en-us/azure/storage/).

## Server-side SignalR message size limit

File uploads may fail even before they start, when Blazor retrieves data about the files that exceeds the maximum SignalR message size.

SignalR defines a message size limit that applies to every message Blazor receives, and the [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component streams files to the server in messages that respect the configured limit. However, the first message, which indicates the set of files to upload, is sent as a unique single message. The size of the first message may exceed the SignalR message size limit. The issue isn't related to the size of the files, it's related to the number of files.

The logged error is similar to the following:

> Error: Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'. e.log @ blazor.server.js:1

When uploading files, reaching the message size limit on the first message is rare. If the limit is reached, the app can configure [HubOptions.MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize) with a larger value.

For more information on SignalR configuration and how to set [MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize), see [ASP.NET Core Blazor SignalR guidance](fundamentals/signalr?view=aspnetcore-10.0#server-side-circuit-handler-options).

## Maximum parallel invocations per client hub setting

Blazor relies on [MaximumParallelInvocationsPerClient](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumparallelinvocationsperclient) set to 1, which is the default value.

Increasing the value leads to a high probability that `CopyTo` operations throw `System.InvalidOperationException: 'Reading is not allowed after reader was completed.'`. For more information, see [MaximumParallelInvocationsPerClient > 1 breaks file upload in Blazor Server mode (`dotnet/aspnetcore` #53951)](https://github.com/dotnet/aspnetcore/issues/53951).

## Troubleshoot

The line that calls [IBrowserFile.OpenReadStream](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.openreadstream) throws a [System.TimeoutException](/en-us/dotnet/api/system.timeoutexception):

> System.TimeoutException: Did not receive any data in the allotted time.

Possible causes:

* Using the [Autofac Inversion of Control (IoC) container](https://autofac.org/) instead of the built-in ASP.NET Core dependency injection container in .NET 8 or earlier. To resolve the issue, set [DisableImplicitFromServicesParameters](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.disableimplicitfromservicesparameters) to `true` in the [server-side circuit handler hub options](fundamentals/signalr?view=aspnetcore-10.0#server-side-circuit-handler-options). For more information, see [FileUpload: Did not receive any data in the allotted time (`dotnet/aspnetcore` #38842)](https://github.com/dotnet/aspnetcore/issues/38842#issuecomment-1342540950).

* Not reading the stream to completion. This isn't a framework issue. Trap the exception and investigate it further in your local environment/network.

- Using server-side rendering and calling [OpenReadStream](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.ibrowserfile.openreadstream) on multiple files before reading them to completion. To resolve the issue, use the `LazyBrowserFileStream` class and approach described in the [Upload files to a server with server-side rendering](#upload-files-to-a-server-with-server-side-rendering) section of this article.

## Related content

* [ASP.NET Core Blazor file downloads](file-downloads?view=aspnetcore-10.0)
* [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations)
* [ASP.NET Core Blazor forms overview](forms/?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](fundamentals/?view=aspnetcore-10.0#sample-apps))

- [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations)
- [ASP.NET Core Blazor forms overview](forms/?view=aspnetcore-10.0)
- [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](fundamentals/?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/grpcweb
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/grpcweb.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC-Web in ASP.NET Core gRPC apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

Learn how to configure an existing ASP.NET Core gRPC service to be callable from browser apps, using the [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) protocol. gRPC-Web allows browser JavaScript and Blazor apps to call gRPC services. It's not possible to call an HTTP/2 gRPC service from a browser-based app. gRPC services hosted in ASP.NET Core can be configured to support gRPC-Web alongside HTTP/2 gRPC.

For instructions on adding a gRPC service to an existing ASP.NET Core app, see [Add gRPC services to an ASP.NET Core app](aspnetcore?view=aspnetcore-10.0#add-grpc-services-to-an-aspnet-core-app).

For instructions on creating a gRPC project, see [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0).

## ASP.NET Core gRPC-Web versus Envoy

There are two choices for how to add gRPC-Web to an ASP.NET Core app:

* Support gRPC-Web alongside gRPC HTTP/2 in ASP.NET Core. This option uses middleware provided by the [`Grpc.AspNetCore.Web`](https://www.nuget.org/packages/Grpc.AspNetCore.Web) package.
* Use the [Envoy proxy's](https://www.envoyproxy.io/) gRPC-Web support to translate gRPC-Web to gRPC HTTP/2. The translated call is then forwarded onto the ASP.NET Core app.

There are pros and cons to each approach. If an app's environment is already using Envoy as a proxy, it might make sense to also use Envoy to provide gRPC-Web support. For a basic solution for gRPC-Web that only requires ASP.NET Core, `Grpc.AspNetCore.Web` is a good choice.

## Configure gRPC-Web in ASP.NET Core

gRPC services hosted in ASP.NET Core can be configured to support gRPC-Web alongside HTTP/2 gRPC. gRPC-Web doesn't require any changes to services. The only modification is in setting the middleware in `Program.cs`.

To enable gRPC-Web with an ASP.NET Core gRPC service:

* Add a reference to the [`Grpc.AspNetCore.Web`](https://www.nuget.org/packages/Grpc.AspNetCore.Web) package.
* Configure the app to use gRPC-Web by adding `UseGrpcWeb` and `EnableGrpcWeb` to `Program.cs`:

```
using GrpcGreeter.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

var app = builder.Build();

app.UseGrpcWeb();

app.MapGrpcService<GreeterService>().EnableGrpcWeb();
app.MapGet("/", () => "This gRPC service is gRPC-Web enabled and is " +
    "callable from browser apps using the gRPC-Web protocol");

app.Run();
```

The preceding code:

* Adds the gRPC-Web middleware, `UseGrpcWeb`, after routing and before endpoints.
* Specifies that the `endpoints.MapGrpcService<GreeterService>()` method supports gRPC-Web with `EnableGrpcWeb`.

Alternatively, the gRPC-Web middleware can be configured so that all services support gRPC-Web by default and `EnableGrpcWeb` isn't required. Specify `new GrpcWebOptions { DefaultEnabled = true }` when the middleware is added.

```
using GrpcGreeter.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

var app = builder.Build();

app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });

app.MapGrpcService<GreeterService>();
app.MapGet("/", () => "All gRPC service are supported by default in " +
    "this example, and are callable from browser apps using the " +
    "gRPC-Web protocol");

app.Run();
```

Note

There is a known issue that causes gRPC-Web to fail when [hosted by HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) in .NET Core 3.x.

A workaround to get gRPC-Web working on HTTP.sys is available in [Grpc-web experimental and UseHttpSys()? (grpc/grpc-dotnet #853)](https://github.com/grpc/grpc-dotnet/issues/853#issuecomment-610078202).

### gRPC-Web and CORS

Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction applies to making gRPC-Web calls with browser apps. For example, a browser app served by `https://www.contoso.com` is blocked from calling gRPC-Web services hosted on `https://services.contoso.com`. Cross-Origin Resource Sharing (CORS) can be used to relax this restriction.

To allow a browser app to make cross-origin gRPC-Web calls, set up [CORS in ASP.NET Core](../security/cors?view=aspnetcore-10.0). Use the built-in CORS support, and expose gRPC-specific headers with [WithExposedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withexposedheaders).

```
using GrpcGreeter.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

builder.Services.AddCors(o => o.AddPolicy("AllowAll", builder =>
{
    builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader()
            .WithExposedHeaders("Grpc-Status", "Grpc-Message", 
                "Grpc-Encoding", "Grpc-Accept-Encoding", 
                "Grpc-Status-Details-Bin");
}));

var app = builder.Build();

app.UseGrpcWeb();
app.UseCors();

app.MapGrpcService<GreeterService>().EnableGrpcWeb()
                                    .RequireCors("AllowAll");

app.MapGet("/", () => "This gRPC service is gRPC-Web enabled, CORS " +
    "enabled, and is callable from browser apps using the gRPC-Web " +
    "protocol");

app.Run();
```

The preceding code:

* Calls `AddCors` to add CORS services and configure a CORS policy that exposes gRPC-specific headers.
* Calls `UseCors` to add the CORS middleware after routing configuration and before endpoints configuration.
* Specifies that the `endpoints.MapGrpcService<GreeterService>()` method supports CORS with `RequireCors`.

### gRPC-Web and streaming

Traditional gRPC over HTTP/2 supports client, server and bidirectional streaming. gRPC-Web offers limited support for streaming:

* gRPC-Web browser clients don't support calling client streaming and bidirectional streaming methods.
* gRPC-Web .NET clients don't support calling client streaming and bidirectional streaming methods over HTTP/1.1.
* ASP.NET Core gRPC services hosted on Azure App Service and IIS don't support bidirectional streaming.

When using gRPC-Web, we only recommend the use of unary methods and server streaming methods.

### HTTP protocol

The ASP.NET Core gRPC service template, included in the .NET SDK, creates an app that's only configured for HTTP/2. This is a good default when an app only supports traditional gRPC over HTTP/2. gRPC-Web, however, works with both HTTP/1.1 and HTTP/2. Some platforms, such as UWP or Unity, can't use HTTP/2. To support all client apps, configure the server to enable HTTP/1.1 and HTTP/2.

Update the default protocol in `appsettings.json`:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1AndHttp2"
    }
  }
}
```

Alternatively, [configure Kestrel endpoints in startup code](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Enabling HTTP/1.1 and HTTP/2 on the same port requires TLS for protocol negotiation. For more information, see [ASP.NET Core gRPC protocol negotiation](aspnetcore?view=aspnetcore-10.0#protocol-negotiation).

## Call gRPC-Web from the browser

Browser apps can use gRPC-Web to call gRPC services. There are some requirements and limitations when calling gRPC services with gRPC-Web from the browser:

* The server must contain configuration to support gRPC-Web.
* Client streaming and bidirectional streaming calls aren't supported. Server streaming is supported.
* Calling gRPC services on a different domain requires [CORS](../security/cors?view=aspnetcore-10.0) configuration on the server.

### JavaScript gRPC-Web client

A JavaScript gRPC-Web client exists. For instructions on how to use gRPC-Web from JavaScript, see [write JavaScript client code with gRPC-Web](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld#write-client-code).

### Configure gRPC-Web with the .NET gRPC client

The .NET gRPC client can be configured to make gRPC-Web calls. This is useful for [Blazor WebAssembly](../blazor/?view=aspnetcore-10.0#blazor-webassembly) apps, which are hosted in the browser and have the same HTTP limitations of JavaScript code. Calling gRPC-Web with a .NET client is the same as [HTTP/2 gRPC](client?view=aspnetcore-10.0). The only modification is how the channel is created.

To use gRPC-Web:

* Add a reference to the [`Grpc.Net.Client.Web`](https://www.nuget.org/packages/Grpc.Net.Client.Web) package.
* Ensure the reference to [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client) package is version 2.29.0 or later.
* Configure the channel to use the `GrpcWebHandler`:

```
var channel = GrpcChannel.ForAddress("https://localhost:53305", new GrpcChannelOptions
{
    HttpHandler = new GrpcWebHandler(new HttpClientHandler())
});

var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
```

The preceding code:

* Configures a channel to use gRPC-Web.
* Creates a client and makes a call using the channel.

`GrpcWebHandler` has the following configuration options:

* `InnerHandler`: The underlying [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) that makes the gRPC HTTP request, for example, `HttpClientHandler`.

* `GrpcWebMode`: An enumeration type that specifies whether the gRPC HTTP request `Content-Type` is `application/grpc-web` or `application/grpc-web-text`.

  * `GrpcWebMode.GrpcWeb` configures sending content without encoding. Default value.
  * `GrpcWebMode.GrpcWebText` configures base64-encoded content. Required for server streaming calls in browsers.

`GrpcChannelOptions.HttpVersion` and `GrpcChannelOptions.HttpVersionPolicy` can be used to configure the HTTP protocol version.

Important

Generated gRPC clients have synchronous and asynchronous methods for calling unary methods. For example, `SayHello` is synchronous, and `SayHelloAsync` is asynchronous. Asynchronous methods are always required in Blazor WebAssembly. Calling a synchronous method in a Blazor WebAssembly app causes the app to become unresponsive.

### Use gRPC client factory with gRPC-Web

Create a .NET client compatible with gRPC-Web using the [gRPC client factory](clientfactory?view=aspnetcore-10.0):

* Add package references to the project file for the following packages:

  * [`Grpc.Net.Client.Web`](https://www.nuget.org/packages/Grpc.Net.Client.Web)
  * [`Grpc.Net.ClientFactory`](https://www.nuget.org/packages/Grpc.Net.ClientFactory)

* Register a gRPC client with dependency injection (DI) using the generic `AddGrpcClient` extension method. In a Blazor WebAssembly app, services are registered with DI in `Program.cs`.

* Configure `GrpcWebHandler` using the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method.

```
builder.Services
    .AddGrpcClient<Greet.GreeterClient>(options =>
    {
        options.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(
        () => new GrpcWebHandler(new HttpClientHandler()));
```

For more information, see [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0).

## Related content

* [gRPC for Web Clients GitHub project](https://github.com/grpc/grpc-web)
* [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0)
* [gRPC JSON transcoding in ASP.NET Core gRPC apps](json-transcoding?view=aspnetcore-10.0)

Learn how to configure an existing ASP.NET Core gRPC service to be callable from browser apps, using the [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) protocol. gRPC-Web allows browser JavaScript and Blazor apps to call gRPC services. It's not possible to call an HTTP/2 gRPC service from a browser-based app. gRPC services hosted in ASP.NET Core can be configured to support gRPC-Web alongside HTTP/2 gRPC.

For instructions on adding a gRPC service to an existing ASP.NET Core app, see [Add gRPC services to an ASP.NET Core app](aspnetcore?view=aspnetcore-10.0#add-grpc-services-to-an-aspnet-core-app).

For instructions on creating a gRPC project, see [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0).

## ASP.NET Core gRPC-Web versus Envoy

There are two choices for how to add gRPC-Web to an ASP.NET Core app:

* Support gRPC-Web alongside gRPC HTTP/2 in ASP.NET Core. This option uses middleware provided by the [`Grpc.AspNetCore.Web`](https://www.nuget.org/packages/Grpc.AspNetCore.Web) package.
* Use the [Envoy proxy's](https://www.envoyproxy.io/) gRPC-Web support to translate gRPC-Web to gRPC HTTP/2. The translated call is then forwarded onto the ASP.NET Core app.

There are pros and cons to each approach. If an app's environment is already using Envoy as a proxy, it might make sense to also use Envoy to provide gRPC-Web support. For a basic solution for gRPC-Web that only requires ASP.NET Core, `Grpc.AspNetCore.Web` is a good choice.

## Configure gRPC-Web in ASP.NET Core

gRPC services hosted in ASP.NET Core can be configured to support gRPC-Web alongside HTTP/2 gRPC. gRPC-Web doesn't require any changes to services. The only modification is in setting the middelware in `Program.cs`.

To enable gRPC-Web with an ASP.NET Core gRPC service:

* Add a reference to the [`Grpc.AspNetCore.Web`](https://www.nuget.org/packages/Grpc.AspNetCore.Web) package.
* Configure the app to use gRPC-Web by adding `UseGrpcWeb` and `EnableGrpcWeb` to `Program.cs`:

```
using GrpcGreeter.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

var app = builder.Build();

app.UseGrpcWeb();

app.MapGrpcService<GreeterService>().EnableGrpcWeb();
app.MapGet("/", () => "This gRPC service is gRPC-Web enabled and is callable from browser apps using the gRPC-Web protocol");

app.Run();
```

The preceding code:

* Adds the gRPC-Web middleware, `UseGrpcWeb`, after routing and before endpoints.
* Specifies that the `endpoints.MapGrpcService<GreeterService>()` method supports gRPC-Web with `EnableGrpcWeb`.

Alternatively, the gRPC-Web middleware can be configured so that all services support gRPC-Web by default and `EnableGrpcWeb` isn't required. Specify `new GrpcWebOptions { DefaultEnabled = true }` when the middleware is added.

```
using GrpcGreeter.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

var app = builder.Build();

app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });

app.MapGrpcService<GreeterService>();
app.MapGet("/", () => "All gRPC service are supported by default in " +
    "this example, and are callable from browser apps using the " +
    "gRPC-Web protocol");

app.Run();
```

Note

There is a known issue that causes gRPC-Web to fail when [hosted by HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) in .NET Core 3.x.

A workaround to get gRPC-Web working on HTTP.sys is available in [Grpc-web experimental and UseHttpSys()? (grpc/grpc-dotnet #853)](https://github.com/grpc/grpc-dotnet/issues/853#issuecomment-610078202).

### gRPC-Web and CORS

Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction applies to making gRPC-Web calls with browser apps. For example, a browser app served by `https://www.contoso.com` is blocked from calling gRPC-Web services hosted on `https://services.contoso.com`. Cross-Origin Resource Sharing (CORS) can be used to relax this restriction.

To allow a browser app to make cross-origin gRPC-Web calls, set up [CORS in ASP.NET Core](../security/cors?view=aspnetcore-10.0). Use the built-in CORS support, and expose gRPC-specific headers with [WithExposedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withexposedheaders).

```
using GrpcGreeter.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

builder.Services.AddCors(o => o.AddPolicy("AllowAll", builder =>
{
    builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader()
            .WithExposedHeaders("Grpc-Status", "Grpc-Message", 
                "Grpc-Encoding", "Grpc-Accept-Encoding", 
                "Grpc-Status-Details-Bin");
}));

var app = builder.Build();

app.UseGrpcWeb();
app.UseCors();

app.MapGrpcService<GreeterService>().EnableGrpcWeb()
                                    .RequireCors("AllowAll");

app.MapGet("/", () => "This gRPC service is gRPC-Web enabled, CORS " +
    "enabled, and is callable from browser apps using the gRPC-Web " +
    "protocol");

app.Run();
```

The preceding code:

* Calls `AddCors` to add CORS services and configure a CORS policy that exposes gRPC-specific headers.
* Calls `UseCors` to add the CORS middleware after routing configuration and before endpoints configuration.
* Specifies that the `endpoints.MapGrpcService<GreeterService>()` method supports CORS with `RequireCors`.

### gRPC-Web and streaming

Traditional gRPC over HTTP/2 supports client, server and bidirectional streaming. gRPC-Web offers limited support for streaming:

* gRPC-Web browser clients don't support calling client streaming and bidirectional streaming methods.
* gRPC-Web .NET clients don't support calling client streaming and bidirectional streaming methods over HTTP/1.1.
* ASP.NET Core gRPC services hosted on Azure App Service and IIS don't support bidirectional streaming.

When using gRPC-Web, we only recommend the use of unary methods and server streaming methods.

### HTTP protocol

The ASP.NET Core gRPC service template, included in the .NET SDK, creates an app that's only configured for HTTP/2. This is a good default when an app only supports traditional gRPC over HTTP/2. gRPC-Web, however, works with both HTTP/1.1 and HTTP/2. Some platforms, such as UWP or Unity, can't use HTTP/2. To support all client apps, configure the server to enable HTTP/1.1 and HTTP/2.

Update the default protocol in `appsettings.json`:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1AndHttp2"
    }
  }
}
```

Alternatively, [configure Kestrel endpoints in startup code](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Enabling HTTP/1.1 and HTTP/2 on the same port requires TLS for protocol negotiation. For more information, see [ASP.NET Core gRPC protocol negotiation](aspnetcore?view=aspnetcore-10.0#protocol-negotiation).

## Call gRPC-Web from the browser

Browser apps can use gRPC-Web to call gRPC services. There are some requirements and limitations when calling gRPC services with gRPC-Web from the browser:

* The server must contain configuration to support gRPC-Web.
* Client streaming and bidirectional streaming calls aren't supported. Server streaming is supported.
* Calling gRPC services on a different domain requires [CORS](../security/cors?view=aspnetcore-10.0) configuration on the server.

### JavaScript gRPC-Web client

A JavaScript gRPC-Web client exists. For instructions on how to use gRPC-Web from JavaScript, see [write JavaScript client code with gRPC-Web](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld#write-client-code).

### Configure gRPC-Web with the .NET gRPC client

The .NET gRPC client can be configured to make gRPC-Web calls. This is useful for [Blazor WebAssembly](../blazor/?view=aspnetcore-10.0#blazor-webassembly) apps, which are hosted in the browser and have the same HTTP limitations of JavaScript code. Calling gRPC-Web with a .NET client is the same as [HTTP/2 gRPC](client?view=aspnetcore-10.0). The only modification is how the channel is created.

To use gRPC-Web:

* Add a reference to the [`Grpc.Net.Client.Web`](https://www.nuget.org/packages/Grpc.Net.Client.Web) package.
* Ensure the reference to [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client) package is version 2.29.0 or later.
* Configure the channel to use the `GrpcWebHandler`:

```
var channel = GrpcChannel.ForAddress("https://localhost:53305", new GrpcChannelOptions
{
    HttpHandler = new GrpcWebHandler(new HttpClientHandler())
});

var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
```

The preceding code:

* Configures a channel to use gRPC-Web.
* Creates a client and makes a call using the channel.

`GrpcWebHandler` has the following configuration options:

* `InnerHandler`: The underlying [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) that makes the gRPC HTTP request, for example, `HttpClientHandler`.

* `GrpcWebMode`: An enumeration type that specifies whether the gRPC HTTP request `Content-Type` is `application/grpc-web` or `application/grpc-web-text`.

  * `GrpcWebMode.GrpcWeb` configures sending content without encoding. Default value.
  * `GrpcWebMode.GrpcWebText` configures base64-encoded content. Required for server streaming calls in browsers.

* `HttpVersion`: HTTP protocol `Version` used to set [HttpRequestMessage.Version](/en-us/dotnet/api/system.net.http.httprequestmessage.version#system-net-http-httprequestmessage-version) on the underlying gRPC HTTP request. gRPC-Web doesn't require a specific version and doesn't override the default unless specified.

Important

Generated gRPC clients have synchronous and asynchronous methods for calling unary methods. For example, `SayHello` is synchronous, and `SayHelloAsync` is asynchronous. Asynchronous methods are always required in Blazor WebAssembly. Calling a synchronous method in a Blazor WebAssembly app causes the app to become unresponsive.

### Use gRPC client factory with gRPC-Web

Create a .NET client compatible with gRPC-Web using the [gRPC client factory](clientfactory?view=aspnetcore-10.0):

* Add package references to the project file for the following packages:

  * [`Grpc.Net.Client.Web`](https://www.nuget.org/packages/Grpc.Net.Client.Web)
  * [`Grpc.Net.ClientFactory`](https://www.nuget.org/packages/Grpc.Net.ClientFactory)

* Register a gRPC client with dependency injection (DI) using the generic `AddGrpcClient` extension method. In a Blazor WebAssembly app, services are registered with DI in `Program.cs`.

* Configure `GrpcWebHandler` using the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method.

```
builder.Services
    .AddGrpcClient<Greet.GreeterClient>(options =>
    {
        options.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(
        () => new GrpcWebHandler(new HttpClientHandler()));
```

For more information, see [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0).

## Related content

* [gRPC for Web Clients GitHub project](https://github.com/grpc/grpc-web)
* [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0)
* [gRPC JSON transcoding in ASP.NET Core gRPC apps](json-transcoding?view=aspnetcore-10.0)

Learn how to configure an existing ASP.NET Core gRPC service to be callable from browser apps, using the [gRPC-Web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) protocol. gRPC-Web allows browser JavaScript and Blazor apps to call gRPC services. It's not possible to call an HTTP/2 gRPC service from a browser-based app. gRPC services hosted in ASP.NET Core can be configured to support gRPC-Web alongside HTTP/2 gRPC.

For instructions on adding a gRPC service to an existing ASP.NET Core app, see [Add gRPC services to an ASP.NET Core app](aspnetcore?view=aspnetcore-10.0#add-grpc-services-to-an-aspnet-core-app).

For instructions on creating a gRPC project, see [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0).

## ASP.NET Core gRPC-Web versus Envoy

There are two choices for how to add gRPC-Web to an ASP.NET Core app:

* Support gRPC-Web alongside gRPC HTTP/2 in ASP.NET Core. This option uses middleware provided by the [`Grpc.AspNetCore.Web`](https://www.nuget.org/packages/Grpc.AspNetCore.Web) package.
* Use the [Envoy proxy's](https://www.envoyproxy.io/) gRPC-Web support to translate gRPC-Web to gRPC HTTP/2. The translated call is then forwarded onto the ASP.NET Core app.

There are pros and cons to each approach. If an app's environment is already using Envoy as a proxy, it might make sense to also use Envoy to provide gRPC-Web support. For a basic solution for gRPC-Web that only requires ASP.NET Core, `Grpc.AspNetCore.Web` is a good choice.

## Configure gRPC-Web in ASP.NET Core

gRPC services hosted in ASP.NET Core can be configured to support gRPC-Web alongside HTTP/2 gRPC. gRPC-Web doesn't require any changes to services. The only modification is startup configuration.

To enable gRPC-Web with an ASP.NET Core gRPC service:

* Add a reference to the [`Grpc.AspNetCore.Web`](https://www.nuget.org/packages/Grpc.AspNetCore.Web) package.
* Configure the app to use gRPC-Web by adding `UseGrpcWeb` and `EnableGrpcWeb` to `Startup.cs`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();
}

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseGrpcWeb(); // Must be added between UseRouting and UseEndpoints

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb();
    });
}
```

The preceding code:

* Adds the gRPC-Web middleware, `UseGrpcWeb`, after routing and before endpoints.
* Specifies that the `endpoints.MapGrpcService<GreeterService>()` method supports gRPC-Web with `EnableGrpcWeb`.

Alternatively, the gRPC-Web middleware can be configured so that all services support gRPC-Web by default and `EnableGrpcWeb` isn't required. Specify `new GrpcWebOptions { DefaultEnabled = true }` when the middleware is added.

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<GreeterService>();
        });
    }
}
```

Note

There is a known issue that causes gRPC-Web to fail when [hosted by HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) in .NET Core 3.x.

A workaround to get gRPC-Web working on HTTP.sys is available in [Grpc-web experimental and UseHttpSys()? (grpc/grpc-dotnet #853)](https://github.com/grpc/grpc-dotnet/issues/853#issuecomment-610078202).

### gRPC-Web and CORS

Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction applies to making gRPC-Web calls with browser apps. For example, a browser app served by `https://www.contoso.com` is blocked from calling gRPC-Web services hosted on `https://services.contoso.com`. Cross-Origin Resource Sharing (CORS) can be used to relax this restriction.

To allow a browser app to make cross-origin gRPC-Web calls, set up [CORS in ASP.NET Core](../security/cors?view=aspnetcore-10.0). Use the built-in CORS support, and expose gRPC-specific headers with [WithExposedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withexposedheaders).

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();

    services.AddCors(o => o.AddPolicy("AllowAll", builder =>
    {
        builder.AllowAnyOrigin()
               .AllowAnyMethod()
               .AllowAnyHeader()
               .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
    }));
}

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseGrpcWeb();
    app.UseCors();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb()
                                                  .RequireCors("AllowAll");
    });
}
```

The preceding code:

* Calls `AddCors` to add CORS services and configure a CORS policy that exposes gRPC-specific headers.
* Calls `UseCors` to add the CORS middleware after routing configuration and before endpoints configuration.
* Specifies that the `endpoints.MapGrpcService<GreeterService>()` method supports CORS with `RequireCors`.

### gRPC-Web and streaming

Traditional gRPC over HTTP/2 supports client, server and bidirectional streaming. gRPC-Web offers limited support for streaming:

* gRPC-Web browser clients don't support calling client streaming and bidirectional streaming methods.
* gRPC-Web .NET clients don't support calling client streaming and bidirectional streaming methods over HTTP/1.1.
* ASP.NET Core gRPC services hosted on Azure App Service and IIS don't support bidirectional streaming.

When using gRPC-Web, we only recommend the use of unary methods and server streaming methods.

### HTTP protocol

The ASP.NET Core gRPC service template, included in the .NET SDK, creates an app that's only configured for HTTP/2. This is a good default when an app only supports traditional gRPC over HTTP/2. gRPC-Web, however, works with both HTTP/1.1 and HTTP/2. Some platforms, such as UWP or Unity, can't use HTTP/2. To support all client apps, configure the server to enable HTTP/1.1 and HTTP/2.

Update the default protocol in `appsettings.json`:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1AndHttp2"
    }
  }
}
```

Alternatively, [configure Kestrel endpoints in startup code](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Enabling HTTP/1.1 and HTTP/2 on the same port requires TLS for protocol negotiation. For more information, see [ASP.NET Core gRPC protocol negotiation](aspnetcore?view=aspnetcore-10.0#protocol-negotiation).

## Call gRPC-Web from the browser

Browser apps can use gRPC-Web to call gRPC services. There are some requirements and limitations when calling gRPC services with gRPC-Web from the browser:

* The server must contain configuration to support gRPC-Web.
* Client streaming and bidirectional streaming calls aren't supported. Server streaming is supported.
* Calling gRPC services on a different domain requires [CORS](../security/cors?view=aspnetcore-10.0) configuration on the server.

### JavaScript gRPC-Web client

A JavaScript gRPC-Web client exists. For instructions on how to use gRPC-Web from JavaScript, see [write JavaScript client code with gRPC-Web](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld#write-client-code).

### Configure gRPC-Web with the .NET gRPC client

The .NET gRPC client can be configured to make gRPC-Web calls. This is useful for [Blazor WebAssembly](../blazor/?view=aspnetcore-10.0#blazor-webassembly) apps, which are hosted in the browser and have the same HTTP limitations of JavaScript code. Calling gRPC-Web with a .NET client is the same as [HTTP/2 gRPC](client?view=aspnetcore-10.0). The only modification is how the channel is created.

To use gRPC-Web:

* Add a reference to the [`Grpc.Net.Client.Web`](https://www.nuget.org/packages/Grpc.Net.Client.Web) package.
* Ensure the reference to [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client) package is version 2.29.0 or later.
* Configure the channel to use the `GrpcWebHandler`:

```
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
    {
        HttpHandler = new GrpcWebHandler(new HttpClientHandler())
    });

var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = ".NET" });
```

The preceding code:

* Configures a channel to use gRPC-Web.
* Creates a client and makes a call using the channel.

`GrpcWebHandler` has the following configuration options:

* `InnerHandler`: The underlying [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) that makes the gRPC HTTP request, for example, `HttpClientHandler`.

* `GrpcWebMode`: An enumeration type that specifies whether the gRPC HTTP request `Content-Type` is `application/grpc-web` or `application/grpc-web-text`.

  * `GrpcWebMode.GrpcWeb` configures sending content without encoding. Default value.
  * `GrpcWebMode.GrpcWebText` configures base64-encoded content. Required for server streaming calls in browsers.

* `HttpVersion`: HTTP protocol `Version` used to set [HttpRequestMessage.Version](/en-us/dotnet/api/system.net.http.httprequestmessage.version#system-net-http-httprequestmessage-version) on the underlying gRPC HTTP request. gRPC-Web doesn't require a specific version and doesn't override the default unless specified.

Important

Generated gRPC clients have synchronous and asynchronous methods for calling unary methods. For example, `SayHello` is synchronous, and `SayHelloAsync` is asynchronous. Asynchronous methods are always required in Blazor WebAssembly. Calling a synchronous method in a Blazor WebAssembly app causes the app to become unresponsive.

### Use gRPC client factory with gRPC-Web

Create a .NET client compatible with gRPC-Web using the [gRPC client factory](clientfactory?view=aspnetcore-10.0):

* Add package references to the project file for the following packages:

  * [`Grpc.Net.Client.Web`](https://www.nuget.org/packages/Grpc.Net.Client.Web)
  * [`Grpc.Net.ClientFactory`](https://www.nuget.org/packages/Grpc.Net.ClientFactory)

* Register a gRPC client with dependency injection (DI) using the generic `AddGrpcClient` extension method. In a Blazor WebAssembly app, services are registered with DI in `Program.cs`.

* Configure `GrpcWebHandler` using the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method.

```
builder.Services
    .AddGrpcClient<Greet.GreeterClient>(options =>
    {
        options.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(
        () => new GrpcWebHandler(new HttpClientHandler()));
```

For more information, see [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0).

## Related content

* [gRPC for Web Clients GitHub project](https://github.com/grpc/grpc-web)
* [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0)
* [gRPC JSON transcoding in ASP.NET Core gRPC apps](json-transcoding?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-logging/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/http-logging/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# HTTP logging in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

HTTP logging is a middleware that logs information about incoming HTTP requests and HTTP responses. HTTP logging provides logs of:

* HTTP request information
* Common properties
* Headers
* Body
* HTTP response information

HTTP logging can:

* Log all requests and responses or only requests and responses that meet certain criteria.
* Select which parts of the request and response are logged.
* Allow you to redact sensitive information from the logs.

HTTP logging ***can reduce the performance of an app***, especially when logging the request and response bodies. Consider the performance impact when selecting fields to log. Test the performance impact of the selected logging properties.

Warning

HTTP logging can potentially log personally identifiable information (PII). Consider the risk and avoid logging sensitive information. For more information about redaction, check [redacting sensitive data](#redacting-sensitive-data)

## Enable HTTP logging

HTTP logging is enabled by calling [AddHttpLogging](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addhttplogging) and [UseHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usehttplogging), as shown in the following example:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The empty lambda in the preceding example of calling `AddHttpLogging` adds the middleware with the default configuration. By default, HTTP logging logs common properties such as path, status-code, and headers for requests and responses.

Add the following line to the `appsettings.Development.json` file at the `"LogLevel": {` level so the HTTP logs are displayed:

```
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
```

With the default configuration, a request and response is logged as a pair of messages similar to the following example:

```
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
      Request:
      Protocol: HTTP/2
      Method: GET
      Scheme: https
      PathBase:
      Path: /
      Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
      Host: localhost:52941
      User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.61
      Accept-Encoding: gzip, deflate, br
      Accept-Language: en-US,en;q=0.9
      Upgrade-Insecure-Requests: [Redacted]
      sec-ch-ua: [Redacted]
      sec-ch-ua-mobile: [Redacted]
      sec-ch-ua-platform: [Redacted]
      sec-fetch-site: [Redacted]
      sec-fetch-mode: [Redacted]
      sec-fetch-user: [Redacted]
      sec-fetch-dest: [Redacted]
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: text/plain; charset=utf-8
      Date: Tue, 24 Oct 2023 02:03:53 GMT
      Server: Kestrel
```

## HTTP logging options

To configure global options for the HTTP logging middleware, call [AddHttpLogging](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addhttplogging) in `Program.cs`, using the lambda to configure [HttpLoggingOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions).

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging();

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

Note

In the preceding sample and following samples, `UseHttpLogging` is called after `UseStaticFiles`, so HTTP logging isn't enabled for static files. To enable static file HTTP logging, call `UseHttpLogging` before `UseStaticFiles`.

### `LoggingFields`

[`HttpLoggingOptions.LoggingFields`](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.loggingfields#microsoft-aspnetcore-httplogging-httploggingoptions-loggingfields) is an enum flag that configures specific parts of the request and response to log. `LoggingFields` defaults to [RequestPropertiesAndHeaders](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingfields#microsoft-aspnetcore-httplogging-httploggingfields-requestpropertiesandheaders) | [ResponsePropertiesAndHeaders](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingfields#microsoft-aspnetcore-httplogging-httploggingfields-responsepropertiesandheaders).

### `RequestHeaders` and `ResponseHeaders`

[RequestHeaders](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.requestheaders#microsoft-aspnetcore-httplogging-httploggingoptions-requestheaders) and [ResponseHeaders](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.responseheaders#microsoft-aspnetcore-httplogging-httploggingoptions-responseheaders) are sets of HTTP headers that are logged. Header values are only logged for header names that are in these collections. The following code adds `sec-ch-ua` to the [RequestHeaders](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.requestheaders#microsoft-aspnetcore-httplogging-httploggingoptions-requestheaders), so the value of the `sec-ch-ua` header is logged. And it adds `MyResponseHeader` to the [ResponseHeaders](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.responseheaders#microsoft-aspnetcore-httplogging-httploggingoptions-responseheaders), so the value of the `MyResponseHeader` header is logged. If these lines are removed, the values of these headers are `[Redacted]`.

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging();

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

### `MediaTypeOptions`

[MediaTypeOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.mediatypeoptions#microsoft-aspnetcore-httplogging-httploggingoptions-mediatypeoptions) provides configuration for selecting which encoding to use for a specific media type.

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging();

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

This approach can also be used to enable logging for data that isn't logged by default (for example, form data, which might have a media type such as `application/x-www-form-urlencoded` or `multipart/form-data`).

#### `MediaTypeOptions` methods

* [AddText](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.mediatypeoptions.addtext)
* [AddBinary](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.mediatypeoptions.addbinary)
* [Clear](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.mediatypeoptions.clear)

### `RequestBodyLogLimit` and `ResponseBodyLogLimit`

* [RequestBodyLogLimit](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.requestbodyloglimit#microsoft-aspnetcore-httplogging-httploggingoptions-requestbodyloglimit)
* [ResponseBodyLogLimit](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.responsebodyloglimit#microsoft-aspnetcore-httplogging-httploggingoptions-responsebodyloglimit)

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging();

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

### `CombineLogs`

Setting [CombineLogs](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.combinelogs#microsoft-aspnetcore-httplogging-httploggingoptions-combinelogs) to `true` configures the middleware to consolidate all of its enabled logs for a request and response into one log at the end. This includes the request, request body, response, response body, and duration.

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging();

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

## Endpoint-specific configuration

For endpoint-specific configuration in Minimal API apps, a [WithHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingendpointconventionbuilderextensions.withhttplogging) extension method is available. The following example shows how to configure HTTP logging for one endpoint:

```
app.MapGet("/response", () => "Hello World! (logging response)")
    .WithHttpLogging(HttpLoggingFields.ResponsePropertiesAndHeaders);
```

For endpoint-specific configuration in apps that use controllers, the [`[HttpLogging]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingattribute) is available. The attribute can also be used in Minimal API apps, as shown in the following example:

```
app.MapGet("/duration", [HttpLogging(loggingFields: HttpLoggingFields.Duration)]
    () => "Hello World! (logging duration)");
```

## `IHttpLoggingInterceptor`

[IHttpLoggingInterceptor](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.ihttplogginginterceptor) is the interface for a service that can be implemented to handle per-request and per-response callbacks for customizing what details get logged. Any endpoint-specific log settings are applied first and can then be overridden in these callbacks. An implementation can:

* Inspect a request or response.
* Enable or disable any [HttpLoggingFields](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingfields).
* Adjust how much of the request or response body is logged.
* Add custom fields to the logs.

Register an [IHttpLoggingInterceptor](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.ihttplogginginterceptor) implementation by calling [AddHttpLoggingInterceptor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addhttplogginginterceptor) in `Program.cs`. If multiple [IHttpLoggingInterceptor](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.ihttplogginginterceptor) instances are registered, they're run in the order registered.

The following example shows how to register an [IHttpLoggingInterceptor](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.ihttplogginginterceptor) implementation:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.Duration;
});
builder.Services.AddHttpLoggingInterceptor<SampleHttpLoggingInterceptor>();
```

The following example is an [IHttpLoggingInterceptor](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.ihttplogginginterceptor) implementation that:

* Inspects the request method and disables logging for POST requests.

* For non-POST requests:

  * Redacts request path, request headers, and response headers.
  * Adds custom fields and field values to the request and response logs.

```
using Microsoft.AspNetCore.HttpLogging;

namespace HttpLoggingSample;

internal sealed class SampleHttpLoggingInterceptor : IHttpLoggingInterceptor
{
    public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext)
    {
        if (logContext.HttpContext.Request.Method == "POST")
        {
            // Don't log anything if the request is a POST.
            logContext.LoggingFields = HttpLoggingFields.None;
        }

        // Don't enrich if we're not going to log any part of the request.
        if (!logContext.IsAnyEnabled(HttpLoggingFields.Request))
        {
            return default;
        }

        if (logContext.TryDisable(HttpLoggingFields.RequestPath))
        {
            RedactPath(logContext);
        }

        if (logContext.TryDisable(HttpLoggingFields.RequestHeaders))
        {
            RedactRequestHeaders(logContext);
        }

        EnrichRequest(logContext);

        return default;
    }

    public ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext)
    {
        // Don't enrich if we're not going to log any part of the response
        if (!logContext.IsAnyEnabled(HttpLoggingFields.Response))
        {
            return default;
        }

        if (logContext.TryDisable(HttpLoggingFields.ResponseHeaders))
        {
            RedactResponseHeaders(logContext);
        }

        EnrichResponse(logContext);

        return default;
    }

    private void RedactPath(HttpLoggingInterceptorContext logContext)
    {
        logContext.AddParameter(nameof(logContext.HttpContext.Request.Path), "RedactedPath");
    }

    private void RedactRequestHeaders(HttpLoggingInterceptorContext logContext)
    {
        foreach (var header in logContext.HttpContext.Request.Headers)
        {
            logContext.AddParameter(header.Key, "RedactedHeader");
        }
    }

    private void EnrichRequest(HttpLoggingInterceptorContext logContext)
    {
        logContext.AddParameter("RequestEnrichment", "Stuff");
    }

    private void RedactResponseHeaders(HttpLoggingInterceptorContext logContext)
    {
        foreach (var header in logContext.HttpContext.Response.Headers)
        {
            logContext.AddParameter(header.Key, "RedactedHeader");
        }
    }

    private void EnrichResponse(HttpLoggingInterceptorContext logContext)
    {
        logContext.AddParameter("ResponseEnrichment", "Stuff");
    }
}
```

With this interceptor, a POST request doesn't generate any logs even if HTTP logging is configured to log [`HttpLoggingFields.All`](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingfields). A GET request generates logs similar to the following example:

```
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
      Request:
      Path: RedactedPath
      Accept: RedactedHeader
      Host: RedactedHeader
      User-Agent: RedactedHeader
      Accept-Encoding: RedactedHeader
      Accept-Language: RedactedHeader
      Upgrade-Insecure-Requests: RedactedHeader
      sec-ch-ua: RedactedHeader
      sec-ch-ua-mobile: RedactedHeader
      sec-ch-ua-platform: RedactedHeader
      sec-fetch-site: RedactedHeader
      sec-fetch-mode: RedactedHeader
      sec-fetch-user: RedactedHeader
      sec-fetch-dest: RedactedHeader
      RequestEnrichment: Stuff
      Protocol: HTTP/2
      Method: GET
      Scheme: https
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      Content-Type: RedactedHeader
      MyResponseHeader: RedactedHeader
      ResponseEnrichment: Stuff
      StatusCode: 200
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[4]
      ResponseBody: Hello World!
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[8]
      Duration: 2.2778ms
```

## Logging configuration order of precedence

The following list shows the order of precedence for logging configuration:

1. Global configuration from [HttpLoggingOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions), set by calling [AddHttpLogging](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addhttplogging).
2. Endpoint-specific configuration from the [`[HttpLogging]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingattribute) or the [WithHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingendpointconventionbuilderextensions.withhttplogging) extension method overrides global configuration.
3. [`IHttpLoggingInterceptor`](#ihttplogginginterceptor) is called with the results and can further modify the configuration per request.

HTTP Logging is a middleware that logs information about incoming HTTP requests and HTTP responses. HTTP logging provides logs of:

* HTTP request information
* Common properties
* Headers
* Body
* HTTP response information

HTTP Logging is valuable in several scenarios to:

* Record information about incoming requests and responses.
* Filter which parts of the request and response are logged.
* Filtering which headers to log.

HTTP Logging ***can reduce the performance of an app***, especially when logging the request and response bodies. Consider the performance impact when selecting fields to log. Test the performance impact of the selected logging properties.

Warning

HTTP Logging can potentially log personally identifiable information (PII). Consider the risk and avoid logging sensitive information.

## Enabling HTTP logging

HTTP Logging is enabled with [UseHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usehttplogging), which adds HTTP logging middleware.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Hello World!");

app.Run();
```

By default, HTTP Logging logs common properties such as path, status-code, and headers for requests and responses. Add the following line to the `appsettings.Development.json` file at the `"LogLevel": {` level so the HTTP logs are displayed:

```
 "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
```

The output is logged as a single message at `LogLevel.Information`.

## HTTP Logging options

To configure the HTTP logging middleware, call [AddHttpLogging](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addhttplogging) in `Program.cs`.

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

Note

In the preceding sample and following samples, `UseHttpLogging` is called after `UseStaticFiles`, so HTTP logging is not enabled for static file. To enable static file HTTP logging, call `UseHttpLogging` before `UseStaticFiles`.

### `LoggingFields`

[`HttpLoggingOptions.LoggingFields`](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.loggingfields#microsoft-aspnetcore-httplogging-httploggingoptions-loggingfields) is an enum flag that configures specific parts of the request and response to log. `HttpLoggingOptions.LoggingFields` defaults to [RequestPropertiesAndHeaders](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingfields#microsoft-aspnetcore-httplogging-httploggingfields-requestpropertiesandheaders) | [ResponsePropertiesAndHeaders](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingfields#microsoft-aspnetcore-httplogging-httploggingfields-responsepropertiesandheaders).

### `RequestHeaders`

[Headers](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.headers#microsoft-aspnetcore-http-httprequest-headers) are a set of HTTP Request Headers that are allowed to be logged. Header values are only logged for header names that are in this collection. The following code logs the request header `"sec-ch-ua"`. If `logging.RequestHeaders.Add("sec-ch-ua");` is removed, the value of the request header `"sec-ch-ua"` is redacted. The following highlighted code calls [`HttpLoggingOptions.RequestHeaders`](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.requestheaders#microsoft-aspnetcore-httplogging-httploggingoptions-requestheaders) and [`HttpLoggingOptions.ResponseHeaders`](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.responseheaders#microsoft-aspnetcore-httplogging-httploggingoptions-responseheaders) :

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

### `MediaTypeOptions`

[MediaTypeOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.mediatypeoptions#microsoft-aspnetcore-httplogging-httploggingoptions-mediatypeoptions) provides configuration for selecting which encoding to use for a specific media type.

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

This approach can also be used to enable logging for data that is not logged by default. For example, form data, which might have a media type such as `application/x-www-form-urlencoded` or `multipart/form-data`.

#### `MediaTypeOptions` methods

* [AddText](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.mediatypeoptions.addtext)
* [AddBinary](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.mediatypeoptions.addbinary)
* [Clear](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.mediatypeoptions.clear)

### `RequestBodyLogLimit` and `ResponseBodyLogLimit`

* [RequestBodyLogLimit](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.requestbodyloglimit#microsoft-aspnetcore-httplogging-httploggingoptions-requestbodyloglimit)
* [ResponseBodyLogLimit](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.responsebodyloglimit#microsoft-aspnetcore-httplogging-httploggingoptions-responsebodyloglimit)

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

## Redacting sensitive data

Http logging with redaction can be enabled by calling [AddHttpLoggingRedaction](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicecollectionextensions.addhttploggingredaction):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.Duration;
});

builder.Services.AddRedaction();
builder.Services.AddHttpLoggingRedaction(op => { });
```

For more information about .NET's data redaction library, see [Data redaction in .NET](/en-us/dotnet/core/extensions/data-redaction).

## Logging redaction options

To configure options for logging with redaction, call [AddHttpLoggingRedaction](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicecollectionextensions.addhttploggingredaction) in `Program.cs` using the lambda to configure [LoggingRedactionOptions](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.loggingredactionoptions):

```
using Microsoft.Extensions.Compliance.Classification;

namespace HttpLoggingSample
{
    public static class MyTaxonomyClassifications
    {
        public static string Name => "MyTaxonomy";

        public static DataClassification Private => new(Name, nameof(Private));
        public static DataClassification Public => new(Name, nameof(Public));
        public static DataClassification Personal => new(Name, nameof(Personal));
    }
}
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });
builder.Services.AddRedaction();

builder.Services.AddHttpLoggingRedaction(op =>
{
    op.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None;
    op.RequestPathLoggingMode = IncomingPathLoggingMode.Formatted;
    op.RequestHeadersDataClasses.Add(HeaderNames.Accept, MyTaxonomyClassifications.Public);
    op.ResponseHeadersDataClasses.Add(HeaderNames.ContentType, MyTaxonomyClassifications.Private);
    op.RouteParameterDataClasses = new Dictionary<string, DataClassification>
    {
        { "one", MyTaxonomyClassifications.Personal },
    };
    // Add the paths that should be filtered, with a leading '/'.
    op.ExcludePathStartsWith.Add("/home");
    op.IncludeUnmatchedRoutes = true;
});

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Logged!");
app.MapGet("/home", () => "Not logged!");

app.Run();
```

With the previous redaction configuration, the output is similar to the following:

```
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[9]
      Request and Response:
      server.address: localhost:61361
      Path: /
      http.request.header.accept:
      Protocol: HTTP/2
      Method: GET
      Scheme: https
      http.response.header.content-type:
      StatusCode: 200
      Duration: 8.4684
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/2 GET https://localhost:61361/ - 200 - text/plain;+charset=utf-8 105.5334ms
```

Note

Request path `/home` isn't logged because it's included in the [`ExcludePathStartsWith` property](#excludepathstartswith). `http.request.header.accept` and `http.response.header.content-type` were redacted by [Microsoft.Extensions.Compliance.Redaction.ErasingRedactor](/en-us/dotnet/api/microsoft.extensions.compliance.redaction.erasingredactor).

### `RequestPathLoggingMode`

[RequestPathLoggingMode](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.loggingredactionoptions.requestpathloggingmode) determines how the request path is logged, whether `Formatted` or `Structured`, set by [IncomingPathLoggingMode](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.incomingpathloggingmode):

* `Formatted`: Logs the request path without parameters.
* `Structured`: Logs the request path with parameters included.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });
builder.Services.AddRedaction();

builder.Services.AddHttpLoggingRedaction(op =>
{
    op.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None;
    op.RequestPathLoggingMode = IncomingPathLoggingMode.Formatted;
    op.RequestHeadersDataClasses.Add(HeaderNames.Accept, MyTaxonomyClassifications.Public);
    op.ResponseHeadersDataClasses.Add(HeaderNames.ContentType, MyTaxonomyClassifications.Private);
    op.RouteParameterDataClasses = new Dictionary<string, DataClassification>
    {
        { "one", MyTaxonomyClassifications.Personal },
    };
    // Add the paths that should be filtered, with a leading '/'.
    op.ExcludePathStartsWith.Add("/home");
    op.IncludeUnmatchedRoutes = true;
});

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Logged!");
app.MapGet("/home", () => "Not logged!");

app.Run();
```

### `RequestPathParameterRedactionMode`

[RequestPathParameterRedactionMode](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.loggingredactionoptions.requestpathparameterredactionmode) specifies how route parameters in the request path should be redacted, whether `Strict`, `Loose`, or `None`, set by [HttpRouteParameterRedactionMode](/en-us/dotnet/api/microsoft.extensions.http.diagnostics.httprouteparameterredactionmode):

* `Strict`: Request route parameters are considered sensitive, require explicit annotation with a data classification, and are redacted by default.
* `Loose`: All parameters are considered as non-sensitive and included as-is by default.
* `None`: Route parameters aren't redacted regardless of the presence of data classification annotations.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });
builder.Services.AddRedaction();

builder.Services.AddHttpLoggingRedaction(op =>
{
    op.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None;
    op.RequestPathLoggingMode = IncomingPathLoggingMode.Formatted;
    op.RequestHeadersDataClasses.Add(HeaderNames.Accept, MyTaxonomyClassifications.Public);
    op.ResponseHeadersDataClasses.Add(HeaderNames.ContentType, MyTaxonomyClassifications.Private);
    op.RouteParameterDataClasses = new Dictionary<string, DataClassification>
    {
        { "one", MyTaxonomyClassifications.Personal },
    };
    // Add the paths that should be filtered, with a leading '/'.
    op.ExcludePathStartsWith.Add("/home");
    op.IncludeUnmatchedRoutes = true;
});

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Logged!");
app.MapGet("/home", () => "Not logged!");

app.Run();
```

### `RequestHeadersDataClasses`

[RequestHeadersDataClasses](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.loggingredactionoptions.requestheadersdataclasses) maps request headers to their data classification, which determines how they are redacted:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });
builder.Services.AddRedaction();

builder.Services.AddHttpLoggingRedaction(op =>
{
    op.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None;
    op.RequestPathLoggingMode = IncomingPathLoggingMode.Formatted;
    op.RequestHeadersDataClasses.Add(HeaderNames.Accept, MyTaxonomyClassifications.Public);
    op.ResponseHeadersDataClasses.Add(HeaderNames.ContentType, MyTaxonomyClassifications.Private);
    op.RouteParameterDataClasses = new Dictionary<string, DataClassification>
    {
        { "one", MyTaxonomyClassifications.Personal },
    };
    // Add the paths that should be filtered, with a leading '/'.
    op.ExcludePathStartsWith.Add("/home");
    op.IncludeUnmatchedRoutes = true;
});

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Logged!");
app.MapGet("/home", () => "Not logged!");

app.Run();
```

### `ResponseHeadersDataClasses`

[ResponseHeadersDataClasses](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.loggingredactionoptions.responseheadersdataclasses), similar to [RequestHeadersDataClasses](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.loggingredactionoptions.requestheadersdataclasses)\`, but for response headers:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });
builder.Services.AddRedaction();

builder.Services.AddHttpLoggingRedaction(op =>
{
    op.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None;
    op.RequestPathLoggingMode = IncomingPathLoggingMode.Formatted;
    op.RequestHeadersDataClasses.Add(HeaderNames.Accept, MyTaxonomyClassifications.Public);
    op.ResponseHeadersDataClasses.Add(HeaderNames.ContentType, MyTaxonomyClassifications.Private);
    op.RouteParameterDataClasses = new Dictionary<string, DataClassification>
    {
        { "one", MyTaxonomyClassifications.Personal },
    };
    // Add the paths that should be filtered, with a leading '/'.
    op.ExcludePathStartsWith.Add("/home");
    op.IncludeUnmatchedRoutes = true;
});

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Logged!");
app.MapGet("/home", () => "Not logged!");

app.Run();
```

### `RouteParameterDataClasses`

[RouteParameterDataClasses](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.loggingredactionoptions.routeparameterdataclasses) maps route parameters to their data classification:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });
builder.Services.AddRedaction();

builder.Services.AddHttpLoggingRedaction(op =>
{
    op.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None;
    op.RequestPathLoggingMode = IncomingPathLoggingMode.Formatted;
    op.RequestHeadersDataClasses.Add(HeaderNames.Accept, MyTaxonomyClassifications.Public);
    op.ResponseHeadersDataClasses.Add(HeaderNames.ContentType, MyTaxonomyClassifications.Private);
    op.RouteParameterDataClasses = new Dictionary<string, DataClassification>
    {
        { "one", MyTaxonomyClassifications.Personal },
    };
    // Add the paths that should be filtered, with a leading '/'.
    op.ExcludePathStartsWith.Add("/home");
    op.IncludeUnmatchedRoutes = true;
});

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Logged!");
app.MapGet("/home", () => "Not logged!");

app.Run();
```

### `ExcludePathStartsWith`

[ExcludePathStartsWith](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.loggingredactionoptions.excludepathstartswith) specifies paths that should be excluded from logging entirely:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });
builder.Services.AddRedaction();

builder.Services.AddHttpLoggingRedaction(op =>
{
    op.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None;
    op.RequestPathLoggingMode = IncomingPathLoggingMode.Formatted;
    op.RequestHeadersDataClasses.Add(HeaderNames.Accept, MyTaxonomyClassifications.Public);
    op.ResponseHeadersDataClasses.Add(HeaderNames.ContentType, MyTaxonomyClassifications.Private);
    op.RouteParameterDataClasses = new Dictionary<string, DataClassification>
    {
        { "one", MyTaxonomyClassifications.Personal },
    };
    // Add the paths that should be filtered, with a leading '/'.
    op.ExcludePathStartsWith.Add("/home");
    op.IncludeUnmatchedRoutes = true;
});

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Logged!");
app.MapGet("/home", () => "Not logged!");

app.Run();
```

### `IncludeUnmatchedRoutes`

[IncludeUnmatchedRoutes](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.logging.loggingredactionoptions.includeunmatchedroutes) allows reporting unmatched routes. If set to `true`, logs whole path of routes not identified by [Routing](../routing?view=aspnetcore-10.0) instead of logging `Unknown` value for path attribute:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });
builder.Services.AddRedaction();

builder.Services.AddHttpLoggingRedaction(op =>
{
    op.RequestPathParameterRedactionMode = HttpRouteParameterRedactionMode.None;
    op.RequestPathLoggingMode = IncomingPathLoggingMode.Formatted;
    op.RequestHeadersDataClasses.Add(HeaderNames.Accept, MyTaxonomyClassifications.Public);
    op.ResponseHeadersDataClasses.Add(HeaderNames.ContentType, MyTaxonomyClassifications.Private);
    op.RouteParameterDataClasses = new Dictionary<string, DataClassification>
    {
        { "one", MyTaxonomyClassifications.Personal },
    };
    // Add the paths that should be filtered, with a leading '/'.
    op.ExcludePathStartsWith.Add("/home");
    op.IncludeUnmatchedRoutes = true;
});

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Logged!");
app.MapGet("/home", () => "Not logged!");

app.Run();
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/membership
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/membership.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Membership authentication to ASP.NET Core 2.0 Identity

By [Isaac Levin](https://isaaclevin.com)

This article demonstrates migrating the database schema for ASP.NET apps using Membership authentication to ASP.NET Core 2.0 Identity.

Note

This document provides the steps needed to migrate the database schema for ASP.NET Membership-based apps to the database schema used for ASP.NET Core Identity. For more information about migrating from ASP.NET Membership-based authentication to ASP.NET Identity, see [Migrate an existing app from SQL Membership to ASP.NET Identity](/en-us/aspnet/identity/overview/migrations/migrating-an-existing-website-from-sql-membership-to-aspnet-identity). For more information about ASP.NET Core Identity, see [Introduction to Identity on ASP.NET Core](../../../security/authentication/identity?view=aspnetcore-10.0).

## Review of Membership schema

Prior to ASP.NET 2.0, developers were tasked with creating the entire authentication and authorization process for their apps. With ASP.NET 2.0, Membership was introduced, providing a boilerplate solution to handling security within ASP.NET apps. Developers were now able to bootstrap a schema into a SQL Server database with the ASP.NET SQL Server Registration Tool (`Aspnet_regsql.exe`) (no longer supported). After running this command, the following tables were created in the database.

To migrate existing apps to ASP.NET Core 2.0 Identity, the data in these tables needs to be migrated to the tables used by the new Identity schema.

## ASP.NET Core Identity 2.0 schema

ASP.NET Core 2.0 follows the [Identity](/en-us/aspnet/identity/index) principle introduced in ASP.NET 4.5. Though the principle is shared, the implementation between the frameworks is different, even between versions of ASP.NET Core (see [Migrate authentication and Identity to ASP.NET Core 2.0](../../1x-to-2x/?view=aspnetcore-10.0)).

The fastest way to view the schema for ASP.NET Core 2.0 Identity is to create a new ASP.NET Core 2.0 app. Follow these steps in Visual Studio 2017:

1. Select **File** > **New** > **Project**.

2. Create a new **ASP.NET Core Web Application** project named *CoreIdentitySample*.

3. Select **ASP.NET Core 2.0** in the dropdown and then select **Web Application**. This template produces a [Razor Pages](../../../razor-pages/?view=aspnetcore-10.0) app. Before clicking **OK**, click **Change Authentication**.

4. Choose **Individual User Accounts** for the Identity templates. Finally, click **OK**, then **OK**. Visual Studio creates a project using the ASP.NET Core Identity template.

5. Select **Tools** > **NuGet Package Manager** > **Package Manager Console** to open the **Package Manager Console** (PMC) window.

6. Navigate to the project root in PMC, and run the [Entity Framework (EF) Core](/en-us/ef/core) `Update-Database` command.

   ASP.NET Core 2.0 Identity uses EF Core to interact with the database storing the authentication data. In order for the newly created app to work, there needs to be a database to store this data. After creating a new app, the fastest way to inspect the schema in a database environment is to create the database using [EF Core Migrations](/en-us/ef/core/managing-schemas/migrations/). This process creates a database, either locally or elsewhere, which mimics that schema. Review the preceding documentation for more information.

   EF Core commands use the connection string for the database specified in `appsettings.json`. The following connection string targets a database on *localhost* named *asp-net-core-identity*. In this setting, EF Core is configured to use the `DefaultConnection` connection string.

   ```
   {
     "ConnectionStrings": {
       "DefaultConnection": "Server=localhost;Database=aspnet-core-identity;Trusted_Connection=True;MultipleActiveResultSets=true"
     }
   }
   ```

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../../../security/?view=aspnetcore-10.0#secure-authentication-flows).

1. Select **View** > **SQL Server Object Explorer**. Expand the node corresponding to the database name specified in the `ConnectionStrings:DefaultConnection` property of `appsettings.json`.

   The `Update-Database` command created the database specified with the schema and any data needed for app initialization. The following image depicts the table structure that's created with the preceding steps.

## Migrate the schema

There are subtle differences in the table structures and fields for both Membership and ASP.NET Core Identity. The pattern has changed substantially for authentication/authorization with ASP.NET and ASP.NET Core apps. The key objects that are still used with Identity are *Users* and *Roles*. Here are mapping tables for *Users*, *Roles*, and *UserRoles*.

### Users

| Identity (`dbo.AspNetUsers`) column | Type     | Membership (`dbo.aspnet_Users` / `dbo.aspnet_Membership`) column | Type     |
| ----------------------------------- | -------- | ---------------------------------------------------------------- | -------- |
| `Id`                                | `string` | `aspnet_Users.UserId`                                            | `string` |
| `UserName`                          | `string` | `aspnet_Users.UserName`                                          | `string` |
| `Email`                             | `string` | `aspnet_Membership.Email`                                        | `string` |
| `NormalizedUserName`                | `string` | `aspnet_Users.LoweredUserName`                                   | `string` |
| `NormalizedEmail`                   | `string` | `aspnet_Membership.LoweredEmail`                                 | `string` |
| `PhoneNumber`                       | `string` | `aspnet_Users.MobileAlias`                                       | `string` |
| `LockoutEnabled`                    | `bit`    | `aspnet_Membership.IsLockedOut`                                  | `bit`    |

`IsLockedOut` doesn't map to `LockoutEnabled`. `IsLockedOut` is set if a user had too many failed logins and is locked out for a set time. `LockoutEnabled` enables locking out a user with too many failed sign in attempts. When the user has too many failed sign in attempts, `LockoutEnd` is set to a date in the future, and the user can't sign in until that date. If `LockoutEnabled` is false then a user is never locked out for too many failed sign in attempts. Per [OWASP](https://owasp.org/www-community/Slow_Down_Online_Guessing_Attacks_with_Device_Cookies), *Temporary account lockout after several failed attempts is too simple of a target for DoS attacks against legitimate users*.

For more information on lock out, see [OWASP Testing for Weak Lock Out Mechanism](https://owasp.org/www-project-web-security-testing-guide/v41/4-Web_Application_Security_Testing/04-Authentication_Testing/03-Testing_for_Weak_Lock_Out_Mechanism).

Apps migrating to Identity that wish to enable failed login lockout should set `LockoutEnabled` to true as part of the migration.

Note

Not all the field mappings resemble one-to-one relationships from Membership to ASP.NET Core Identity. The preceding table takes the default Membership User schema and maps it to the ASP.NET Core Identity schema. Any other custom fields that were used for Membership need to be mapped manually. In this mapping, there's no map for passwords, as both password criteria and password salts don't migrate between the two. **It's recommended to leave the password as null and to ask users to reset their passwords.** In ASP.NET Core Identity, `LockoutEnd` should be set to some date in the future if the user is locked out. This is shown in the migration script.

### Roles

| Identity (`dbo.AspNetRoles`) column | Type     | Membership (`dbo.aspnet_Roles`) column | Type     |
| ----------------------------------- | -------- | -------------------------------------- | -------- |
| `Id`                                | `string` | `RoleId`                               | `string` |
| `Name`                              | `string` | `RoleName`                             | `string` |
| `NormalizedName`                    | `string` | `LoweredRoleName`                      | `string` |

### User Roles

| Identity (`dbo.AspNetUserRoles`) column | Type     | Membership (`dbo.aspnet_UsersInRoles`) column | Type     |
| --------------------------------------- | -------- | --------------------------------------------- | -------- |
| `RoleId`                                | `string` | `RoleId`                                      | `string` |
| `UserId`                                | `string` | `UserId`                                      | `string` |

Reference the preceding mapping tables when creating a migration script for *Users* and *Roles*. The following example assumes you have two databases on a database server. One database contains the existing ASP.NET Membership schema and data. The other *CoreIdentitySample* database was created using steps described earlier. Comments are included inline for more details.

```
-- THIS SCRIPT NEEDS TO RUN FROM THE CONTEXT OF THE MEMBERSHIP DB
BEGIN TRANSACTION MigrateUsersAndRoles
USE aspnetdb

-- INSERT USERS
INSERT INTO CoreIdentitySample.dbo.AspNetUsers
            (Id,
             UserName,
             NormalizedUserName,
             PasswordHash,
             SecurityStamp,
             EmailConfirmed,
             PhoneNumber,
             PhoneNumberConfirmed,
             TwoFactorEnabled,
             LockoutEnd,
             LockoutEnabled,
             AccessFailedCount,
             Email,
             NormalizedEmail)
SELECT aspnet_Users.UserId,
       aspnet_Users.UserName,
       -- The NormalizedUserName value is upper case in ASP.NET Core Identity
       UPPER(aspnet_Users.UserName),
       -- Creates an empty password since passwords don't map between the 2 schemas
       '',
       /*
        The SecurityStamp token is used to verify the state of an account and
        is subject to change at any time. It should be initialized as a new ID.
       */
       NewID(),
       /*
        EmailConfirmed is set when a new user is created and confirmed via email.
        Users must have this set during migration to reset passwords.
       */
       1,
       aspnet_Users.MobileAlias,
       CASE
         WHEN aspnet_Users.MobileAlias IS NULL THEN 0
         ELSE 1
       END,
       -- 2FA likely wasn't setup in Membership for users, so setting as false.
       0,
       CASE
         -- Setting lockout date to time in the future (1,000 years)
         WHEN aspnet_Membership.IsLockedOut = 1 THEN Dateadd(year, 1000,
                                                     Sysutcdatetime())
         ELSE NULL
       END,
       aspnet_Membership.IsLockedOut,
       /*
        AccessFailedAccount is used to track failed logins. This is stored in
        Membership in multiple columns. Setting to 0 arbitrarily.
       */
       0,
       aspnet_Membership.Email,
       -- The NormalizedEmail value is upper case in ASP.NET Core Identity
       UPPER(aspnet_Membership.Email)
FROM   aspnet_Users
       LEFT OUTER JOIN aspnet_Membership
                    ON aspnet_Membership.ApplicationId =
                       aspnet_Users.ApplicationId
                       AND aspnet_Users.UserId = aspnet_Membership.UserId
       LEFT OUTER JOIN CoreIdentitySample.dbo.AspNetUsers
                    ON aspnet_Membership.UserId = AspNetUsers.Id
WHERE  AspNetUsers.Id IS NULL

-- INSERT ROLES
INSERT INTO CoreIdentitySample.dbo.AspNetRoles(Id, Name)
SELECT RoleId, RoleName
FROM aspnet_Roles;

-- INSERT USER ROLES
INSERT INTO CoreIdentitySample.dbo.AspNetUserRoles(UserId, RoleId)
SELECT UserId, RoleId
FROM aspnet_UsersInRoles;

IF @@ERROR <> 0
  BEGIN
    ROLLBACK TRANSACTION MigrateUsersAndRoles
    RETURN
  END

COMMIT TRANSACTION MigrateUsersAndRoles
```

After completion of the preceding script, the ASP.NET Core Identity app created earlier is populated with Membership users. Users need to change their passwords before logging in.

Note

If the Membership system had users with user names that didn't match their email address, changes are required to the app created earlier to accommodate this. The default template expects `UserName` and `Email` to be the same. For situations in which they're different, the login process needs to be modified to use `UserName` instead of `Email`.

In the `PageModel` of the Login Page, located at , remove the `[EmailAddress]` attribute from the *Email* property. Rename it to *UserName*. This requires a change wherever `EmailAddress` is mentioned, in the *View* and *PageModel*. The result looks like the following:

## Next steps

In this tutorial, you learned how to port users from SQL membership to ASP.NET Core 2.0 Identity. For more information regarding ASP.NET Core Identity, see [Introduction to Identity](../../../security/authentication/identity?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/dotnet-watch
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/dotnet-watch.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Develop ASP.NET Core apps using a file watcher

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Victor Hurdugaci](https://twitter.com/victorhurdugaci)

`dotnet watch` is a tool that runs a [.NET CLI](/en-us/dotnet/core/tools) command when source files change. For example, a file change can trigger compilation, test execution, or deployment.

This tutorial uses an existing web API with two endpoints: one that returns a sum and one that returns a product. The product method has a bug, which is fixed in this tutorial.

Download the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/dotnet-watch/samples). It consists of two projects: *WebApp* (an ASP.NET Core web API) and *WebAppTests* (unit tests for the web API).

In a command shell, navigate to the *WebApp* folder. Run the following command:

```
dotnet run
```

Note

You can use `dotnet run --project <PROJECT>` to specify a project to run. For example, running `dotnet run --project WebApp` from the root of the sample app will also run the *WebApp* project.

The console output shows messages similar to the following (indicating that the app is running and awaiting requests):

```
$ dotnet run
Hosting environment: Development
Content root path: C:/Docs/aspnetcore/tutorials/dotnet-watch/samples/WebApp
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
```

In a web browser, navigate to `http://localhost:<port number>/api/math/sum?a=4&b=5`. You should see the result of `9`.

Navigate to the product API (`http://localhost:<port number>/api/math/product?a=4&b=5`). It returns `9`, not `20` as you'd expect. That problem is fixed later in the tutorial.

## Add `dotnet watch` to a project

The `dotnet watch` file watcher tool is included with version 2.1.300 of the .NET SDK. The following steps are required when using an earlier version of the .NET SDK.

1. Add a `Microsoft.DotNet.Watcher.Tools` package reference to the `.csproj` file:

   ```
   <ItemGroup>
       <DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" />
   </ItemGroup>
   ```

2. Install the `Microsoft.DotNet.Watcher.Tools` package by running the following command:

   ```
   dotnet restore
   ```

## Run .NET CLI commands using `dotnet watch`

Any [.NET CLI command](/en-us/dotnet/core/tools#cli-commands) can be run with `dotnet watch`. For example:

| Command                               | Command with watch                          |
| ------------------------------------- | ------------------------------------------- |
| dotnet run                            | dotnet watch run                            |
| dotnet run -f netcoreapp3.1           | dotnet watch run -f netcoreapp3.1           |
| dotnet run -f netcoreapp3.1 -- --arg1 | dotnet watch run -f netcoreapp3.1 -- --arg1 |
| dotnet test                           | dotnet watch test                           |

Run `dotnet watch run` in the *WebApp* folder. The console output indicates `watch` has started.

Running `dotnet watch run` on a web app launches a browser that navigates to the app's URL once ready. `dotnet watch` does this by reading the app's console output and waiting for the ready message displayed by [WebHost](/en-us/dotnet/api/microsoft.aspnetcore.webhost).

`dotnet watch` refreshes the browser when it detects changes to watched files. To do this, the watch command injects a middleware to the app that modifies HTML responses created by the app. The middleware adds a JavaScript script block to the page that allows `dotnet watch` to instruct the browser to refresh. Currently, changes to all watched files, including static content such as `.html` and `.css` files cause the app to be rebuilt.

`dotnet watch`:

* Only watches files that impact builds by default.
* Any additionally watched files (via configuration) still results in a build taking place.

For more information on configuration, see [dotnet-watch configuration](#dotnet-watch-configuration) in this document.

Note

You can use `dotnet watch --project <PROJECT>` to specify a project to watch. For example, running `dotnet watch --project WebApp run` from the root of the sample app will also run and watch the *WebApp* project.

## Make changes with `dotnet watch`

Make sure `dotnet watch` is running.

Fix the bug in the `Product` method of `MathController.cs` so it returns the product and not the sum:

```
public static int Product(int a, int b)
{
    return a * b;
}
```

Save the file. The console output indicates that `dotnet watch` detected a file change and restarted the app.

Verify `http://localhost:<port number>/api/math/product?a=4&b=5` returns the correct result.

## Run tests using `dotnet watch`

1. Change the `Product` method of `MathController.cs` back to returning the sum. Save the file.

2. In a command shell, navigate to the *WebAppTests* folder.

3. Run [dotnet restore](/en-us/dotnet/core/tools/dotnet-restore).

4. Run `dotnet watch test`. Its output indicates that a test failed and that the watcher is awaiting file changes:

   ```
   Total tests: 2. Passed: 1. Failed: 1. Skipped: 0.
   Test Run Failed.
   ```

5. Fix the `Product` method code so it returns the product. Save the file.

`dotnet watch` detects the file change and reruns the tests. The console output indicates the tests passed.

## Customize files list to watch

By default, `dotnet-watch` tracks all files matching the following glob patterns:

* `**/*.cs`
* `*.csproj`
* `**/*.resx`
* Content files: `wwwroot/**`, `**/*.config`, `**/*.json`

More items can be added to the watch list by editing the `.csproj` file. Items can be specified individually or by using glob patterns.

```
<ItemGroup>
    <!-- extends watching group to include *.js files -->
    <Watch Include="**\*.js" Exclude="node_modules\**\*;**\*.js.map;obj\**\*;bin\**\*" />
</ItemGroup>
```

## Opt-out of files to be watched

`dotnet-watch` can be configured to ignore its default settings. To ignore specific files, add the `Watch="false"` attribute to an item's definition in the `.csproj` file:

```
<ItemGroup>
    <!-- exclude Generated.cs from dotnet-watch -->
    <Compile Include="Generated.cs" Watch="false" />

    <!-- exclude Strings.resx from dotnet-watch -->
    <EmbeddedResource Include="Strings.resx" Watch="false" />

    <!-- exclude changes in this referenced project -->
    <ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj" Watch="false" />
</ItemGroup>
```

```
<ItemGroup>
     <!-- Exclude all Content items from being watched. -->
    <Content Update="@(Content)" Watch="false" />
</ItemGroup>
```

## Custom watch projects

`dotnet-watch` isn't restricted to C# projects. Custom watch projects can be created to handle different scenarios. Consider the following project layout:

* **test/**

  * `UnitTests/UnitTests.csproj`
  * `IntegrationTests/IntegrationTests.csproj`

If the goal is to watch both projects, create a custom project file configured to watch both projects:

```
<Project>
    <ItemGroup>
        <TestProjects Include="**\*.csproj" />
        <Watch Include="**\*.cs" />
    </ItemGroup>

    <Target Name="Test">
        <MSBuild Targets="VSTest" Projects="@(TestProjects)" />
    </Target>

    <Import Project="$(MSBuildExtensionsPath)\Microsoft.Common.targets" />
</Project>
```

To start file watching on both projects, change to the *test* folder. Execute the following command:

```
dotnet watch msbuild /t:Test
```

VSTest executes when any file changes in either test project.

## dotnet-watch configuration

Some configuration options can be passed to `dotnet watch` through environment variables. The available variables are:

| Setting                                        | Description                                                                                                                                                                                                                      |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DOTNET_USE_POLLING_FILE_WATCHER`              | If set to "1" or "true", `dotnet watch` uses a polling file watcher instead of CoreFx's `FileSystemWatcher`. Used when watching files on network shares or Docker mounted volumes.                                               |
| `DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM` | By default, `dotnet watch` optimizes the build by avoiding certain operations such as running restore or re-evaluating the set of watched files on every file change. If set to "1" or "true", these optimizations are disabled. |
| `DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER`         | `dotnet watch run` attempts to launch browsers for web apps with `launchBrowser` configured in `launchSettings.json`. If set to "1" or "true", this behavior is suppressed.                                                      |
| `DOTNET_WATCH_SUPPRESS_BROWSER_REFRESH`        | `dotnet watch run` attempts to refresh browsers when it detects file changes. If set to "1" or "true", this behavior is suppressed. This behavior is also suppressed if `DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER` is set.           |

## Browser refresh

`dotnet watch` injects a script into the app that allows it to refresh the browser when the content changes. In some scenarios, such as when the app enables response compression, `dotnet watch` might ***not*** be able to inject the script. For such cases in development, manually inject the script into the app. For example, to configure the web app to manually inject the script, update the layout file to include `_framework/aspnet-browser-refresh.js`:

```
@* _Layout.cshtml *@
<environment names="Development">
    <script src="/_framework/aspnetcore-browser-refresh.js"></script>
</environment>
```

## Non-ASCII characters

Visual Studio 17.2 or later includes the .NET SDK 6.0.300 or later. With the .NET SDK and 6.0.300 later, `dotnet-watch` emits non-ASCII characters to the console during a hot reload session. On certain console hosts, such as the Windows conhost, these characters may appear garbled. To avoid garbled characters, consider one of the following approaches:

* Configure the `DOTNET_WATCH_SUPPRESS_EMOJIS=1` environment variable to suppress emitting these values.
* Switch to a different terminal, such as <https://github.com/microsoft/terminal>, that supports rendering non-ASCII characters.

## File watcher limits in Linux

When running `dotnet watch` on Linux or in Docker containers, you may encounter an error indicating that the configured user limit on the number of inotify instances has been reached:

`The configured user limit (128) on the number of inotify instances has been reached, or the per-process limit on the number of open file descriptors has been reached.`

If the inotify instance limit is exhausted, increase `fs.inotify.max_user_instances`. If the process has reached its open-file-descriptor limit, increase that limit instead. The separate `fs.inotify.max_user_watches` setting applies when the inotify watch limit is exhausted.

To temporarily increase these limits, run the following commands:

```
sudo sysctl fs.inotify.max_user_instances=524288
sudo sysctl fs.inotify.max_user_watches=524288
```

To make this change permanent, add the following lines to the `/etc/sysctl.conf` file:

```
fs.inotify.max_user_instances=524288
fs.inotify.max_user_watches=524288
```

Note

For Docker containers, these limits are inherited from the host operating system. You must configure the `sysctl` settings on the host machine, rather than inside the container.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/class-libraries
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/class-libraries.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Consume ASP.NET Core Razor components from a Razor class library (RCL)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Components can be shared in a [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0) across projects. Include components and static assets in an app from:

* Another project in the [solution](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln).
* A referenced .NET library.
* A NuGet package.

Just as components are regular .NET types, components provided by an RCL are normal .NET assemblies.

## Create an RCL

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / .NET CLI](#tabpanel_1_visual-studio-code+net-cli)

1. Create a new project.

2. In the **Create a new project** dialog, select **Razor Class Library** from the list of ASP.NET Core project templates. Select **Next**.

3. In the **Configure your new project** dialog, provide a project name in the **Project name** field. Examples in this topic use the project name `ComponentLibrary`. Select **Next**.

4. In the **Additional information** dialog, don't select **Support pages and views**. Select **Create**.

5. Add the RCL to a solution:

   1. Open the solution.
   2. Right-click the solution in **Solution Explorer**. Select **Add** > **Existing Project**.
   3. Navigate to the RCL's project file.
   4. Select the RCL's project file (`.csproj`).

6. Add a reference to the RCL from the app:

   1. Right-click the app project. Select **Add** > **Project Reference**.
   2. Select the RCL project. Select **OK**.

1) Use the **Razor Class Library** project template (`razorclasslib`) with the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) command in a command shell. In the following example, an RCL is created and named `ComponentLibrary` using the `-o|--output` option. The folder that holds `ComponentLibrary` is created automatically when the command is executed:

   ```
   dotnet new razorclasslib -o ComponentLibrary
   ```

2) To add the library to an existing project, use the [`dotnet add reference`](/en-us/dotnet/core/tools/dotnet-add-reference) command in a command shell. In the following command, the `{PATH TO LIBRARY}` placeholder is the path to the library's project folder:

   ```
   dotnet add reference {PATH TO LIBRARY}
   ```

## How an RCL is different from an ASP.NET Core class library

The following characteristics distinguish an RCL from an ASP.NET Core class library:

* An RCL uses the `Microsoft.NET.Sdk.Razor` SDK, while an ordinary class library project uses the `Microsoft.NET.Sdk` SDK. The `Microsoft.NET.Sdk.Razor` SDK builds the library with custom MSBuild tooling specifically designed to compile, build, and package Razor files (`.cshtml`) and Razor component files (`.razor`).

- An RCL's project file (`.csproj`) includes the `SupportedPlatform` property for the `browser` platform. For more information, see the [Client-side browser compatibility analyzer](#client-side-browser-compatibility-analyzer) section.

* An RCL references the [`Microsoft.AspNetCore.Components.Web` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Web) and usually relies on an imports file (`_Imports.razor`) with an `@using` statement for the [Microsoft.AspNetCore.Components.Web](/en-us/dotnet/api/microsoft.aspnetcore.components.web) namespace. The package provides web-specific infrastructure, HTML element abstractions, and event handling bindings required to run Blazor apps inside a web browser.

## Consume a Razor component from an RCL

To consume components from an RCL in another project, use either of the following approaches:

* Use the full component type name, which includes the RCL's namespace.

* Individual components can be added by name without the RCL's namespace if Razor's [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive declares the RCL's namespace. Use the following approaches:

  * Add the `@using` directive to individual components.
  * Include the `@using` directive in the top-level imports file (`_Imports.razor`) to make the library's components available to an entire project. Add the directive to an imports file at any level to apply the namespace to a single component or set of components within a folder. When an imports file is used, individual components don't require an `@using` directive for the RCL's namespace.

In the following examples, `ComponentLibrary` is an RCL containing the `Component1` component. The `Component1` component is an example component automatically added to an RCL created from the RCL project template that isn't created to support pages and views.

`Component1.razor` in the `ComponentLibrary` RCL:

```
<div class="my-component">
    This component is defined in the <strong>ComponentLibrary</strong> package.
</div>
```

In the app that consumes the RCL, reference the `Component1` component using its namespace, as the following example shows.

`ConsumeComponent1.razor`:

```
@page "/consume-component-1"

<h1>Consume component (full namespace example)</h1>

<ComponentLibrary.Component1 />
```

Alternatively, add a [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive and use the component without its namespace. The following `@using` directive can also appear in any imports file (`_Imports.razor`) in or above the current folder.

`ConsumeComponent2.razor`:

```
@page "/consume-component-2"
@using ComponentLibrary

<h1>Consume component (<code>@@using</code> example)</h1>

<Component1 />
```

For library components that use [CSS isolation](css-isolation?view=aspnetcore-10.0), the component styles are automatically made available to the consuming app. There's no need to manually link or import the library's individual component stylesheets or its bundled CSS file in the app that consumes the library. The app uses CSS imports to reference the RCL's bundled styles. The bundled styles aren't published as a static web asset of the app that consumes the library. For a class library named `ClassLib` and a Blazor app with a `BlazorSample.styles.css` stylesheet, the RCL's stylesheet is imported at the top of the app's stylesheet automatically at build time:

```
@import '_content/ClassLib/ClassLib.bundle.scp.css';
```

For the preceding examples, `Component1`'s stylesheet (`Component1.razor.css`) is bundled automatically.

`Component1.razor.css` in the `ComponentLibrary` RCL:

```
.my-component {
    border: 2px dashed red;
    padding: 1em;
    margin: 1em 0;
    background-image: url('background.png');
}
```

The background image is also included from the RCL project template and resides in the `wwwroot` folder of the RCL.

`wwwroot/background.png` in the `ComponentLibrary` RCL:

To provide additional library component styles from stylesheets in the library's `wwwroot` folder, add stylesheet `<link>` tags to the RCL's consumer, as the next example demonstrates.

Important

Generally, library components use [CSS isolation](css-isolation?view=aspnetcore-10.0) to bundle and provide component styles. Component styles that rely upon CSS isolation are automatically made available to the app that uses the RCL. There's no need to manually link or import the library's individual component stylesheets or its bundled CSS file in the app that consumes the library. The following example is for providing global stylesheets *outside of CSS isolation*, which usually isn't a requirement for typical apps that consume RCLs.

The following background image is used in the next example. If you implement the example shown in this section, right-click the image to save it locally.

`wwwroot/extra-background.png` in the `ComponentLibrary` RCL:

Add a new stylesheet to the RCL with an `extra-style` class.

`wwwroot/additionalStyles.css` in the `ComponentLibrary` RCL:

```
.extra-style {
    border: 2px dashed blue;
    padding: 1em;
    margin: 1em 0;
    background-image: url('extra-background.png');
}
```

Add a component to the RCL that uses the `extra-style` class.

`ExtraStyles.razor` in the `ComponentLibrary` RCL:

```
<div class="extra-style">
    <p>
        This component is defined in the <strong>ComponentLibrary</strong> package.
    </p>
</div>
```

Add a page to the app that uses the `ExtraStyles` component from the RCL.

`ConsumeComponent3.razor`:

```
@page "/consume-component-3"
@using ComponentLibrary

<h1>Consume component (<code>additionalStyles.css</code> example)</h1>

<ExtraStyles />
```

Link to the library's stylesheet in the app's `<head>` markup ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)):

Blazor Web Apps:

```
<link href="@Assets["_content/ComponentLibrary/additionalStyles.css"]" rel="stylesheet">
```

Standalone Blazor WebAssembly apps:

```
<link href="_content/ComponentLibrary/additionalStyles.css" rel="stylesheet">
```

```
<link href="_content/ComponentLibrary/additionalStyles.css" rel="stylesheet">
```

For library components that use [CSS isolation](css-isolation?view=aspnetcore-10.0), the component styles are automatically made available to the consuming app. There's no need to manually link or import the library's individual component stylesheets or its bundled CSS file in the app that consumes the library. The app uses CSS imports to reference the RCL's bundled styles. The bundled styles aren't published as a static web asset of the app that consumes the library. For a class library named `ClassLib` and a Blazor app with a `BlazorSample.styles.css` stylesheet, the RCL's stylesheet is imported at the top of the app's stylesheet automatically at build time:

```
@import '_content/ClassLib/ClassLib.bundle.scp.css';
```

For the preceding examples, `Component1`'s stylesheet (`Component1.razor.css`) is bundled automatically.

`Component1.razor.css` in the `ComponentLibrary` RCL:

```
.my-component {
    border: 2px dashed red;
    padding: 1em;
    margin: 1em 0;
    background-image: url('background.png');
}
```

The background image is also included from the RCL project template and resides in the `wwwroot` folder of the RCL.

`wwwroot/background.png` in the `ComponentLibrary` RCL:

The following background image and stylesheet are used by the RCL's `Component1` example component. There's no need to add these static assets to a new RCL created from the RCL project template, as they're added automatically by the project template.

`wwwroot/background.png` in the `ComponentLibrary` RCL:

`wwwroot/styles.css` in the `ComponentLibrary` RCL:

```
.my-component {
    border: 2px dashed red;
    padding: 1em;
    margin: 1em 0;
    background-image: url('background.png');
}
```

To provide `Component1`'s `my-component` CSS class, link to the library's stylesheet in the app's `<head>` markup ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)):

```
<link href="_content/ComponentLibrary/styles.css" rel="stylesheet" />
```

## Make routable components available from the RCL

To make routable components in the RCL available for direct requests, the RCL's assembly must be disclosed to the app's router.

Open the app's `App` component (`App.razor`). Assign an [Assembly](/en-us/dotnet/api/system.reflection.assembly) collection to the [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies) parameter of the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component to include the RCL's assembly. In the following example, the `ComponentLibrary.Component1` component is used to discover the RCL's assembly.

```
AdditionalAssemblies="new[] { typeof(ComponentLibrary.Component1).Assembly }"
```

For more information, see [ASP.NET Core Blazor routing](../fundamentals/routing?view=aspnetcore-10.0#route-to-components-from-multiple-assemblies).

## Create an RCL with static assets in the `wwwroot` folder

An RCL's static assets are available to any app that consumes the library.

Place static assets in the `wwwroot` folder of the RCL and reference the static assets with the following path in the app: `_content/{PACKAGE ID}/{PATH AND FILE NAME}`. The `{PACKAGE ID}` placeholder is the library's [package ID](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties). The package ID defaults to the project's assembly name if `<PackageId>` isn't specified in the project file. The `{PATH AND FILE NAME}` placeholder is path and file name under `wwwroot`. This path format is also used in the app for static assets supplied by NuGet packages added to the RCL.

The following example demonstrates the use of RCL static assets with an RCL named `ComponentLibrary` and a Blazor app that consumes the RCL. The app has a project reference for the `ComponentLibrary` RCL.

The following Jeep® image is used in this section's example. If you implement the example shown in this section, right-click the image to save it locally.

`wwwroot/jeep-yj.png` in the `ComponentLibrary` RCL:

Add the following `JeepYJ` component to the RCL.

`JeepYJ.razor` in the `ComponentLibrary` RCL:

```
<h3>ComponentLibrary.JeepYJ</h3>

<p>
    <img alt="Jeep YJ&reg;" src="_content/ComponentLibrary/jeep-yj.png" />
</p>
```

Add the following `Jeep` component to the app that consumes the `ComponentLibrary` RCL. The `Jeep` component uses:

* The Jeep YJ® image from the `ComponentLibrary` RCL's `wwwroot` folder.
* The `JeepYJ` component from the RCL.

`Jeep.razor`:

```
@page "/jeep"
@using ComponentLibrary

<div style="float:left;margin-right:10px">
    <h3>Direct use</h3>

    <p>
        <img alt="Jeep YJ&reg;" src="_content/ComponentLibrary/jeep-yj.png" />
    </p>
</div>

<JeepYJ />

<p>
    <em>Jeep</em> and <em>Jeep YJ</em> are registered trademarks of 
    <a href="https://www.stellantis.com">FCA US LLC (Stellantis NV)</a>.
</p>
```

Rendered `Jeep` component:

For more information, see [Reusable Razor UI in class libraries with ASP.NET Core](../../razor-pages/ui-class?view=aspnetcore-10.0#create-an-rcl-with-static-assets).

## Create an RCL with JavaScript files collocated with components

This section and the following examples are primarily focused on explaining JS file collocation. The first example demonstrates a collocated JS file with an ordinary JS function. The second example demonstrates the use of a module to load a function, which is the recommended approach for most production apps. Calling JS from .NET is fully covered in [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0), where there are further explanations of the Blazor JS API with additional examples. Component disposal, which is present in the second example, is covered in [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

The following `JsCollocation1` component loads a script via a [`HeadContent` component](control-head-content?view=aspnetcore-10.0) and calls a JS function with [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync). The `{PATH}` placeholder is the path to the component.

Important

If you use the following code for a demonstration in a test app, change the `{PATH}` placeholder to the path of the component (example: `Components/Pages` in .NET 8 or later or `Pages` in .NET 7 or earlier). In a Blazor Web App (.NET 8 or later), the component requires an interactive render mode applied either globally to the app or to the component definition.

Add the following script after the Blazor script ([location of the Blazor start script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script)):

```
<script src="{PATH}/JsCollocation1.razor.js"></script>
```

`JsCollocation1` component (`{PATH}/JsCollocation1.razor`):

```
@page "/js-collocation-1"
@inject IJSRuntime JS

<PageTitle>JS Collocation 1</PageTitle>

<h1>JS Collocation Example 1</h1>

<button @onclick="ShowPrompt">Call showPrompt1</button>

@if (!string.IsNullOrEmpty(result))
{
    <p>
        Hello @result!
    </p>
}

@code {
    private string? result;

    public async Task ShowPrompt()
    {
        result = await JS.InvokeAsync<string>(
            "showPrompt1", "What's your name?");
        StateHasChanged();
    }
}
```

The collocated JS file is placed next to the `JsCollocation1` component file with the file name `JsCollocation1.razor.js`. In the `JsCollocation1` component, the script is referenced at the path of the collocated file. In the following example, the `showPrompt1` function accepts the user's name from a [`Window prompt()`](https://developer.mozilla.org/docs/Web/API/Window/prompt) and returns it to the `JsCollocation1` component for display.

`{PATH}/JsCollocation1.razor.js`:

```
function showPrompt1(message) {
  return prompt(message, 'Type your name here');
}
```

The preceding approach isn't recommended for general use in production apps because the approach pollutes the client with global functions. A better approach for production apps is to use JS modules. The same general principles apply to loading a JS module from a collocated JS file, as the next example demonstrates.

The following `JsCollocation2` component's `OnAfterRenderAsync` method loads a JS module into `module`, which is an [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) of the component class. `module` is used to call the `showPrompt2` function. The `{PATH}` placeholder is the path to the component.

Important

If you use the following code for a demonstration in a test app, change the `{PATH}` placeholder to the path of the component. In a Blazor Web App (.NET 8 or later), the component requires an interactive render mode applied either globally to the app or to the component definition.

`JsCollocation2` component (`{PATH}/JsCollocation2.razor`):

```
@page "/js-collocation-2"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>JS Collocation 2</PageTitle>

<h1>JS Collocation Example 2</h1>

<button @onclick="ShowPrompt">Call showPrompt2</button>

@if (!string.IsNullOrEmpty(result))
{
    <p>
        Hello @result!
    </p>
}

@code {
    private IJSObjectReference? module;
    private string? result;

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            /*
                Change the {PATH} placeholder in the next line to the path of
                the collocated JS file in the app. Examples:

                ./Components/Pages/JsCollocation2.razor.js (.NET 8 or later)
                ./Pages/JsCollocation2.razor.js (.NET 7 or earlier)
            */
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./{PATH}/JsCollocation2.razor.js");
        }
    }

    public async Task ShowPrompt()
    {
        if (module is not null)
        {
            result = await module.InvokeAsync<string>(
                "showPrompt2", "What's your name?");
            StateHasChanged();
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

In the preceding example, [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) is trapped during module disposal in case Blazor's SignalR circuit is lost. If the preceding code is used in a Blazor WebAssembly app, there's no SignalR connection to lose, so you can remove the `try`-`catch` block and leave the line that disposes the module (`await module.DisposeAsync();`). For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](../javascript-interoperability/?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit).

`{PATH}/JsCollocation2.razor.js`:

```
export function showPrompt2(message) {
  return prompt(message, 'Type your name here');
}
```

Important

Don't place a `<script>` tag for `JsCollocation2.razor.js` after the [Blazor script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) because the module is loaded and cached automatically when the [dynamic `import()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/import) is invoked.

Use of scripts and modules for collocated JS in a Razor class library (RCL) is only supported for Blazor's JS interop mechanism based on the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) interface. If you're implementing [JavaScript `[JSImport]`/`[JSExport]` interop](../javascript-interoperability/import-export-interop?view=aspnetcore-10.0), see [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](../javascript-interoperability/import-export-interop?view=aspnetcore-10.0#razor-class-library-rcl-collocated-js-is-unsupported).

```
module = await JS.InvokeAsync<IJSObjectReference>("import", 
    "./_content/AppJS/Components/Pages/JsCollocation3.razor.js");
```

## Supply components and static assets to multiple hosted Blazor apps

For more information, see [Multiple hosted ASP.NET Core Blazor WebAssembly apps](../host-and-deploy/webassembly/multiple-hosted-webassembly?view=aspnetcore-10.0).

## Client-side browser compatibility analyzer

Client-side apps target the full .NET API surface area, but not all .NET APIs are supported on WebAssembly due to browser sandbox constraints. Unsupported APIs throw [PlatformNotSupportedException](/en-us/dotnet/api/system.platformnotsupportedexception) when running on WebAssembly. A platform compatibility analyzer warns the developer when the app uses APIs that aren't supported by the app's target platforms. For client-side apps, this means checking that APIs are supported in browsers. Annotating .NET framework APIs for the compatibility analyzer is an on-going process, so not all .NET framework API is currently annotated.

Blazor Web Apps that enable Interactive WebAssembly components, Blazor WebAssembly apps, and RCL projects *automatically* enable browser compatibility checks by adding `browser` as a supported platform with the `SupportedPlatform` MSBuild item. Library developers can manually add the `SupportedPlatform` item to a library's project file to enable the feature:

```
<ItemGroup>
  <SupportedPlatform Include="browser" />
</ItemGroup>
```

When authoring a library, indicate that a particular API isn't supported in browsers by specifying `browser` to [UnsupportedOSPlatformAttribute](/en-us/dotnet/api/system.runtime.versioning.unsupportedosplatformattribute):

```
using System.Runtime.Versioning;

...

[UnsupportedOSPlatform("browser")]
private static string GetLoggingDirectory()
{
    ...
}
```

For more information, see [Annotating APIs as unsupported on specific platforms (`dotnet/designs` GitHub repository](https://github.com/dotnet/designs/blob/main/accepted/2020/platform-exclusion/platform-exclusion.md#build-configuration-for-platforms).

## JavaScript isolation in JavaScript modules

Blazor enables JavaScript isolation in standard [JavaScript modules](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules). JavaScript isolation provides the following benefits:

* Imported JavaScript no longer pollutes the global namespace.
* Consumers of the library and components aren't required to manually import the related JavaScript.

For more information, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules).

## Avoid trimming JavaScript-invokable .NET methods

[Runtime relinking](../webassembly-build-tools-and-aot?view=aspnetcore-10.0#runtime-relinking) trims class instance JavaScript-invokable .NET methods unless they're explicitly preserved. For more information, see [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0#avoid-trimming-javascript-invokable-net-methods).

## Build, pack, and ship to NuGet

Because Razor class libraries that contain Razor components are standard .NET libraries, packing and shipping them to NuGet is no different from packing and shipping any library to NuGet. Packing is performed using the [`dotnet pack`](/en-us/dotnet/core/tools/dotnet-pack) command in a command shell:

```
dotnet pack
```

Upload the package to NuGet using the [`dotnet nuget push`](/en-us/dotnet/core/tools/dotnet-nuget-push) command in a command shell.

## Trademarks

*Jeep* and *Jeep YJ* are registered trademarks of [FCA US LLC (Stellantis NV)](https://www.stellantis.com).

## Related content

* [Reusable Razor UI in class libraries with ASP.NET Core](../../razor-pages/ui-class?view=aspnetcore-10.0)
* [Use ASP.NET Core APIs in a class library](../../fundamentals/target-aspnetcore?view=aspnetcore-10.0)
* [Add an XML Intermediate Language (IL) Trimmer configuration file to a library](../host-and-deploy/configure-trimmer?view=aspnetcore-10.0)
* [CSS isolation support with Razor class libraries](css-isolation?view=aspnetcore-10.0#razor-class-library-rcl-support)

- [Reusable Razor UI in class libraries with ASP.NET Core](../../razor-pages/ui-class?view=aspnetcore-10.0)
- [Use ASP.NET Core APIs in a class library](../../fundamentals/target-aspnetcore?view=aspnetcore-10.0)
- [Add an XML Intermediate Language (IL) Linker configuration file to a library](../host-and-deploy/configure-linker?view=aspnetcore-10.0#add-an-xml-linker-configuration-file-to-a-library)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/security
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/security.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Security considerations in ASP.NET Core SignalR

By [Ashley Stanton-Nurse](https://github.com/analogrelay)

This article provides information on securing SignalR.

## Cross-Origin Resource Sharing

[Cross-Origin Resource Sharing (CORS)](https://www.w3.org/TR/cors/) can be used to allow cross-origin SignalR connections in the browser. If JavaScript code is hosted on a different domain from the SignalR app, [CORS middleware](../security/cors?view=aspnetcore-10.0) must be enabled to allow the JavaScript to connect to the SignalR app. Allow cross-origin requests only from domains you trust or control. For example:

* Your site is hosted on `http://www.example.com`
* Your SignalR app is hosted on `http://signalr.example.com`

CORS should be configured in the SignalR app to only allow the origin `www.example.com`.

For more information on configuring CORS, see [Enable Cross-Origin Requests (CORS)](../security/cors?view=aspnetcore-10.0). SignalR **requires** the following CORS policies:

* Allow the specific expected origins. Allowing any origin is possible but is **not** secure or recommended.
* HTTP methods `GET`, `POST`, and `DELETE` must be allowed. The `DELETE` method is required for the Long Polling transport to clean up connections on the server.
* Credentials must be allowed in order for cookie-based sticky sessions to work correctly. They must be enabled even when authentication isn't used.

However, in 5.0 we have provided an option in the TypeScript client to not use credentials. The option to not use credentials should only be used when you know 100% that credentials like Cookies are not needed in your app (cookies are used by azure app service when using multiple servers for sticky sessions).

For example, the following highlighted CORS policy allows a SignalR browser client hosted on `https://example.com` to access the SignalR app hosted on `https://signalr.example.com`:

```
using SignalRChat.Hubs;

var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy =>
                      {
                          policy.WithOrigins("http://example.com");
                          policy.WithMethods("GET", "POST", "DELETE");
                          policy.AllowCredentials();
                      });
});

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");
```

In the previous example, the CORS policy is customized to allow specific origins, methods, and credentials. For more information on customizing CORS policies and middleware in ASP.NET Core, see [CORS middleware: CORS with named policy and middleware](../security/cors?view=aspnetcore-10.0#cors-with-named-policy-and-middleware).

### Apply a CORS policy to SignalR hub endpoints

Instead of applying a CORS policy globally with the `UseCors` middleware, you can apply CORS specifically to SignalR hub endpoints. This approach allows different CORS policies for different parts of the app.

There are two ways to apply a CORS policy to SignalR hubs: chaining `RequireCors` on the endpoint mapping, or adding the `[EnableCors]` attribute to the Hub class. Both approaches require a named CORS policy to be registered in the service configuration. The following example defines a policy named `"SignalRPolicy"`:

```
builder.Services.AddCors(options =>
{
    options.AddPolicy("SignalRPolicy", policy =>
    {
        policy.WithOrigins("https://example.com")
            .AllowAnyHeader()
            .WithMethods("GET", "POST", "DELETE")
            .AllowCredentials();
    });
});
```

**Apply the CORS policy on the hub endpoint mapping** by chaining [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) on the `MapHub` call:

```
app.MapHub<ChatHub>("/chatHub")
    .RequireCors("SignalRPolicy");
```

**Apply the CORS policy on the Hub class** by adding the [`[EnableCors]`](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute:

```
[EnableCors("SignalRPolicy")]
public class ChatHub : Hub
{
    // ...
}
```

For more information on enabling CORS with endpoint routing, see [Enable CORS with endpoint routing](../security/cors?view=aspnetcore-10.0#enable-cors-with-endpoint-routing).

## WebSocket Origin Restriction

The protections provided by CORS don't apply to WebSockets. For origin restriction on WebSockets, read [WebSockets origin restriction](../fundamentals/websockets?view=aspnetcore-10.0#websocket-origin-restriction).

## ConnectionId

Exposing `ConnectionId` can lead to malicious impersonation if the SignalR server or client version is ASP.NET Core 2.2 or earlier. If the SignalR server and client version are ASP.NET Core 3.0 or later, the `ConnectionToken` rather than the `ConnectionId` must be kept secret. The `ConnectionToken` is purposely not exposed in any API. It can be difficult to ensure that older SignalR clients aren't connecting to the server, so even if your SignalR server version is ASP.NET Core 3.0 or later, the `ConnectionId` shouldn't be exposed.

## Access token logging

When using WebSockets or Server-Sent Events, the browser client sends the access token in the query string. Receiving the access token via query string is generally as secure as using the standard `Authorization` header. Always use HTTPS to ensure a secure end-to-end connection between the client and the server. Many web servers log the URL for each request, including the query string. Logging the URLs may log the access token. ASP.NET Core logs the URL for each request by default, which includes the query string. For example:

```
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:5000/chathub?access_token=1234
```

If you have concerns about logging this data with your server logs, you can disable this logging entirely by configuring the `Microsoft.AspNetCore.Hosting` logger to the `Warning` level or above (these messages are written at `Info` level). For more information, see [Apply log filter rules in code](../fundamentals/logging/?view=aspnetcore-10.0#apply-log-filter-rules-in-code) for more information. If you still want to log certain request information, you can [write middleware](../fundamentals/middleware/write?view=aspnetcore-10.0) to log the data that you require and filter out the `access_token` query string value (if present).

## Exceptions

Exception messages are generally considered sensitive data that shouldn't be revealed to a client. By default, SignalR doesn't send the details of an exception thrown by a hub method to the client. Instead, the client receives a generic message indicating an error occurred. Exception message delivery to the client can be overridden (for example in development or test) with [EnableDetailedErrors](configuration?view=aspnetcore-10.0#configure-server-options). Exception messages should not be exposed to the client in production apps.

## Buffer management

SignalR uses per-connection buffers to manage incoming and outgoing messages. By default, SignalR limits these buffers to 32 KB. The largest message a client or server can send is 32 KB. The maximum memory consumed by a connection for messages is 32 KB. If your messages are always smaller than 32 KB, you can reduce the limit, which:

* Prevents a client from being able to send a larger message.
* The server will never need to allocate large buffers to accept messages.

If your messages are larger than 32 KB, you can increase the limit. Increasing this limit means:

* The client can cause the server to allocate large memory buffers.
* Server allocation of large buffers may reduce the number of concurrent connections.

There are limits for incoming and outgoing messages, both can be configured on the [HttpConnectionDispatcherOptions](configuration?view=aspnetcore-10.0#configure-server-options) object configured in `MapHub`:

* `ApplicationMaxBufferSize` represents the maximum number of bytes from the client that the server buffers. If the client attempts to send a message larger than this limit, the connection may be closed.
* `TransportMaxBufferSize` represents the maximum number of bytes the server can send. If the server attempts to send a message (including return values from hub methods) larger than this limit, an exception will be thrown.

Setting the limit to `0` disables the limit. Removing the limit allows a client to send a message of any size. Malicious clients sending large messages can cause excess memory to be allocated. Excess memory usage can significantly reduce the number of concurrent connections.

This article provides information on securing SignalR.

## Cross-Origin Resource Sharing

[Cross-Origin Resource Sharing (CORS)](https://www.w3.org/TR/cors/) can be used to allow cross-origin SignalR connections in the browser. If JavaScript code is hosted on a different domain from the SignalR app, [CORS middleware](../security/cors?view=aspnetcore-10.0) must be enabled to allow the JavaScript to connect to the SignalR app. Allow cross-origin requests only from domains you trust or control. For example:

* Your site is hosted on `http://www.example.com`
* Your SignalR app is hosted on `http://signalr.example.com`

CORS should be configured in the SignalR app to only allow the origin `www.example.com`.

For more information on configuring CORS, see [Enable Cross-Origin Requests (CORS)](../security/cors?view=aspnetcore-10.0). SignalR **requires** the following CORS policies:

* Allow the specific expected origins. Allowing any origin is possible but is **not** secure or recommended.
* HTTP methods `GET`, `POST`, and `DELETE` must be allowed. The `DELETE` method is required for the Long Polling transport to clean up connections on the server.
* Credentials must be allowed in order for cookie-based sticky sessions to work correctly. They must be enabled even when authentication isn't used.

However, in 5.0 we have provided an option in the TypeScript client to not use credentials. The option to not use credentials should only be used when you know 100% that credentials like Cookies are not needed in your app (cookies are used by azure app service when using multiple servers for sticky sessions).

For example, the following highlighted CORS policy allows a SignalR browser client hosted on `https://example.com` to access the SignalR app hosted on `https://signalr.example.com`:

```
using SignalRChat.Hubs;

var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy =>
                      {
                          policy.WithOrigins("http://example.com");
                          policy.WithMethods("GET", "POST", "DELETE");
                          policy.AllowCredentials();
                      });
});

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");
```

In the previous example, the CORS policy is customized to allow specific origins, methods, and credentials. For more information on customizing CORS policies and middleware in ASP.NET Core, see [CORS middleware: CORS with named policy and middleware](../security/cors?view=aspnetcore-10.0#cors-with-named-policy-and-middleware).

## WebSocket Origin Restriction

The protections provided by CORS don't apply to WebSockets. For origin restriction on WebSockets, read [WebSockets origin restriction](../fundamentals/websockets?view=aspnetcore-10.0#websocket-origin-restriction).

## ConnectionId

Exposing `ConnectionId` can lead to malicious impersonation if the SignalR server or client version is ASP.NET Core 2.2 or earlier. If the SignalR server and client version are ASP.NET Core 3.0 or later, the `ConnectionToken` rather than the `ConnectionId` must be kept secret. The `ConnectionToken` is purposely not exposed in any API. It can be difficult to ensure that older SignalR clients aren't connecting to the server, so even if your SignalR server version is ASP.NET Core 3.0 or later, the `ConnectionId` shouldn't be exposed.

## Access token logging

When using WebSockets or Server-Sent Events, the browser client sends the access token in the query string. Receiving the access token via query string is generally as secure as using the standard `Authorization` header. Always use HTTPS to ensure a secure end-to-end connection between the client and the server. Many web servers log the URL for each request, including the query string. Logging the URLs may log the access token. ASP.NET Core logs the URL for each request by default, which includes the query string. For example:

```
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:5000/chathub?access_token=1234
```

If you have concerns about logging this data with your server logs, you can disable this logging entirely by configuring the `Microsoft.AspNetCore.Hosting` logger to the `Warning` level or above (these messages are written at `Info` level). For more information, see [Apply log filter rules in code](../fundamentals/logging/?view=aspnetcore-10.0#apply-log-filter-rules-in-code) for more information. If you still want to log certain request information, you can [write middleware](../fundamentals/middleware/write?view=aspnetcore-10.0) to log the data that you require and filter out the `access_token` query string value (if present).

## Exceptions

Exception messages are generally considered sensitive data that shouldn't be revealed to a client. By default, SignalR doesn't send the details of an exception thrown by a hub method to the client. Instead, the client receives a generic message indicating an error occurred. Exception message delivery to the client can be overridden (for example in development or test) with [EnableDetailedErrors](configuration?view=aspnetcore-10.0#configure-server-options). Exception messages should not be exposed to the client in production apps.

## Buffer management

SignalR uses per-connection buffers to manage incoming and outgoing messages. By default, SignalR limits these buffers to 32 KB. The largest message a client or server can send is 32 KB. The maximum memory consumed by a connection for messages is 32 KB. If your messages are always smaller than 32 KB, you can reduce the limit, which:

* Prevents a client from being able to send a larger message.
* The server will never need to allocate large buffers to accept messages.

If your messages are larger than 32 KB, you can increase the limit. Increasing this limit means:

* The client can cause the server to allocate large memory buffers.
* Server allocation of large buffers may reduce the number of concurrent connections.

There are limits for incoming and outgoing messages, both can be configured on the [HttpConnectionDispatcherOptions](configuration?view=aspnetcore-10.0#configure-server-options) object configured in `MapHub`:

* `ApplicationMaxBufferSize` represents the maximum number of bytes from the client that the server buffers. If the client attempts to send a message larger than this limit, the connection may be closed.
* `TransportMaxBufferSize` represents the maximum number of bytes the server can send. If the server attempts to send a message (including return values from hub methods) larger than this limit, an exception will be thrown.

Setting the limit to `0` disables the limit. Removing the limit allows a client to send a message of any size. Malicious clients sending large messages can cause excess memory to be allocated. Excess memory usage can significantly reduce the number of concurrent connections.

This article provides information on securing SignalR.

## Cross-Origin Resource Sharing

[Cross-Origin Resource Sharing (CORS)](https://www.w3.org/TR/cors/) can be used to allow cross-origin SignalR connections in the browser. If JavaScript code is hosted on a different domain from the SignalR app, [CORS middleware](../security/cors?view=aspnetcore-10.0) must be enabled to allow the JavaScript to connect to the SignalR app. Allow cross-origin requests only from domains you trust or control. For example:

* Your site is hosted on `http://www.example.com`
* Your SignalR app is hosted on `http://signalr.example.com`

CORS should be configured in the SignalR app to only allow the origin `www.example.com`.

For more information on configuring CORS, see [Enable Cross-Origin Requests (CORS)](../security/cors?view=aspnetcore-10.0). SignalR **requires** the following CORS policies:

* Allow the specific expected origins. Allowing any origin is possible but is **not** secure or recommended.
* HTTP methods `GET`, `POST`, and `DELETE` must be allowed. The `DELETE` method is required for the Long Polling transport to clean up connections on the server.
* Credentials must be allowed in order for cookie-based sticky sessions to work correctly. They must be enabled even when authentication isn't used.

However, in 5.0 we have provided an option in the TypeScript client to not use credentials. The option to not use credentials should only be used when you know 100% that credentials like Cookies are not needed in your app (cookies are used by azure app service when using multiple servers for sticky sessions).

For example, the following highlighted CORS policy allows a SignalR browser client hosted on `https://example.com` to access the SignalR app hosted on `https://signalr.example.com`:

```
using SignalRChat.Hubs;

var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy =>
                      {
                          policy.WithOrigins("http://example.com");
                          policy.WithMethods("GET", "POST", "DELETE");
                          policy.AllowCredentials();
                      });
});

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");
```

In the previous example, the CORS policy is customized to allow specific origins, methods, and credentials. For more information on customizing CORS policies and middleware in ASP.NET Core, see [CORS middleware: CORS with named policy and middleware](../security/cors?view=aspnetcore-10.0#cors-with-named-policy-and-middleware).

## WebSocket Origin Restriction

The protections provided by CORS don't apply to WebSockets. For origin restriction on WebSockets, read [WebSockets origin restriction](../fundamentals/websockets?view=aspnetcore-10.0#websocket-origin-restriction).

## ConnectionId

Exposing `ConnectionId` can lead to malicious impersonation if the SignalR server or client version is ASP.NET Core 2.2 or earlier. If the SignalR server and client version are ASP.NET Core 3.0 or later, the `ConnectionToken` rather than the `ConnectionId` must be kept secret. The `ConnectionToken` is purposely not exposed in any API. It can be difficult to ensure that older SignalR clients aren't connecting to the server, so even if your SignalR server version is ASP.NET Core 3.0 or later, the `ConnectionId` shouldn't be exposed.

## Access token logging

When using WebSockets or Server-Sent Events, the browser client sends the access token in the query string. Receiving the access token via query string is generally as secure as using the standard `Authorization` header. Always use HTTPS to ensure a secure end-to-end connection between the client and the server. Many web servers log the URL for each request, including the query string. Logging the URLs may log the access token. ASP.NET Core logs the URL for each request by default, which includes the query string. For example:

```
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:5000/chathub?access_token=1234
```

If you have concerns about logging this data with your server logs, you can disable this logging entirely by configuring the `Microsoft.AspNetCore.Hosting` logger to the `Warning` level or above (these messages are written at `Info` level). For more information, see [Apply log filter rules in code](../fundamentals/logging/?view=aspnetcore-10.0#apply-log-filter-rules-in-code) for more information. If you still want to log certain request information, you can [write middleware](../fundamentals/middleware/write?view=aspnetcore-10.0) to log the data that you require and filter out the `access_token` query string value (if present).

## Exceptions

Exception messages are generally considered sensitive data that shouldn't be revealed to a client. By default, SignalR doesn't send the details of an exception thrown by a hub method to the client. Instead, the client receives a generic message indicating an error occurred. Exception message delivery to the client can be overridden (for example in development or test) with [EnableDetailedErrors](configuration?view=aspnetcore-10.0#configure-server-options). Exception messages should not be exposed to the client in production apps.

## Buffer management

SignalR uses per-connection buffers to manage incoming and outgoing messages. By default, SignalR limits these buffers to 32 KB. The largest message a client or server can send is 32 KB. The maximum memory consumed by a connection for messages is 32 KB. If your messages are always smaller than 32 KB, you can reduce the limit, which:

* Prevents a client from being able to send a larger message.
* The server will never need to allocate large buffers to accept messages.

If your messages are larger than 32 KB, you can increase the limit. Increasing this limit means:

* The client can cause the server to allocate large memory buffers.
* Server allocation of large buffers may reduce the number of concurrent connections.

There are limits for incoming and outgoing messages, both can be configured on the [HttpConnectionDispatcherOptions](configuration?view=aspnetcore-10.0#configure-server-options) object configured in `MapHub`:

* `ApplicationMaxBufferSize` represents the maximum number of bytes from the client that the server buffers. If the client attempts to send a message larger than this limit, the connection may be closed.
* `TransportMaxBufferSize` represents the maximum number of bytes the server can send. If the server attempts to send a message (including return values from hub methods) larger than this limit, an exception will be thrown.

Setting the limit to `0` disables the limit. Removing the limit allows a client to send a message of any size. Malicious clients sending large messages can cause excess memory to be allocated. Excess memory usage can significantly reduce the number of concurrent connections.

This article provides information on securing SignalR.

## Cross-Origin Resource Sharing

[Cross-Origin Resource Sharing (CORS)](https://www.w3.org/TR/cors/) can be used to allow cross-origin SignalR connections in the browser. If JavaScript code is hosted on a different domain from the SignalR app, [CORS middleware](../security/cors?view=aspnetcore-10.0) must be enabled to allow the JavaScript to connect to the SignalR app. Allow cross-origin requests only from domains you trust or control. For example:

* Your site is hosted on `http://www.example.com`
* Your SignalR app is hosted on `http://signalr.example.com`

CORS should be configured in the SignalR app to only allow the origin `www.example.com`.

For more information on configuring CORS, see [Enable Cross-Origin Requests (CORS)](../security/cors?view=aspnetcore-10.0). SignalR **requires** the following CORS policies:

* Allow the specific expected origins. Allowing any origin is possible but is **not** secure or recommended.
* HTTP methods `GET`, `POST`, and `DELETE` must be allowed. The `DELETE` method is required for the Long Polling transport to clean up connections on the server.
* Credentials must be allowed in order for cookie-based sticky sessions to work correctly. They must be enabled even when authentication isn't used.

However, in 5.0 we have provided an option in the TypeScript client to not use credentials. The option to not use credentials should only be used when you know 100% that credentials like Cookies are not needed in your app (cookies are used by azure app service when using multiple servers for sticky sessions).

For example, the following CORS policy allows a SignalR browser client hosted on `https://example.com` to access the SignalR app hosted on `https://signalr.example.com`:

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // ... other middleware ...

    // Make sure the CORS middleware is ahead of SignalR.
    app.UseCors(builder =>
    {
        builder.WithOrigins("https://example.com")
            .AllowAnyHeader()
            .WithMethods("GET", "POST", "DELETE")
            .AllowCredentials();
    });

    // ... other middleware ...
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chathub");
    });

    // ... other middleware ...
}
```

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // ... other middleware ...

    // Make sure the CORS middleware is ahead of SignalR.
    app.UseCors(builder =>
    {
        builder.WithOrigins("https://example.com")
            .AllowAnyHeader()
            .WithMethods("GET", "POST", "DELETE")
            .AllowCredentials();
    });

    // ... other middleware ...

    app.UseSignalR(routes =>
    {
        routes.MapHub<ChatHub>("/chathub");
    });

    // ... other middleware ...
}
```

## WebSocket Origin Restriction

The protections provided by CORS don't apply to WebSockets. For origin restriction on WebSockets, read [WebSockets origin restriction](../fundamentals/websockets?view=aspnetcore-10.0#websocket-origin-restriction).

The protections provided by CORS don't apply to WebSockets. Browsers do **not**:

* Perform CORS pre-flight requests.
* Respect the restrictions specified in `Access-Control` headers when making WebSocket requests.

However, browsers do send the `Origin` header when issuing WebSocket requests. Applications should be configured to validate these headers to ensure that only WebSockets coming from the expected origins are allowed.

In ASP.NET Core 2.1 or later, header validation can be achieved using a custom middleware placed **before `UseSignalR`, and authentication middleware** in `Configure`:

```

// In Startup, add a static field listing the allowed Origin values:
private static readonly HashSet<string> _allowedOrigins = new HashSet<string>()
{
    // Add allowed origins here. For example:
    "https://www.mysite.com",
    "https://mysite.com",
};

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // ... other middleware ...

    // Validate Origin header on WebSocket requests to prevent unexpected cross-site 
    // WebSocket requests.
    app.Use((context, next) =>
    {
        // Check for a WebSocket request.
        if (string.Equals(context.Request.Headers["Upgrade"], "websocket"))
        {
            var origin = context.Request.Headers["Origin"];

            // If there is an origin header, and the origin header doesn't match 
            // an allowed value:
            if (!string.IsNullOrEmpty(origin) && !_allowedOrigins.Contains(origin))
            {
                // The origin is not allowed, reject the request
                context.Response.StatusCode = (int) HttpStatusCode.Forbidden;
                return Task.CompletedTask;
            }
        }

        // The request is a valid Origin or not a WebSocket request, so continue.
        return next();
    });

    // ... other middleware ...

    app.UseSignalR(routes =>
    {
        routes.MapHub<ChatHub>("/chathub");
    });

    // ... other middleware ...
}
```

Note

The `Origin` header is controlled by the client and, like the `Referer` header, can be faked. These headers should **not** be used as an authentication mechanism.

## ConnectionId

Exposing `ConnectionId` can lead to malicious impersonation if the SignalR server or client version is ASP.NET Core 2.2 or earlier. If the SignalR server and client version are ASP.NET Core 3.0 or later, the `ConnectionToken` rather than the `ConnectionId` must be kept secret. The `ConnectionToken` is purposely not exposed in any API. It can be difficult to ensure that older SignalR clients aren't connecting to the server, so even if your SignalR server version is ASP.NET Core 3.0 or later, the `ConnectionId` shouldn't be exposed.

## Access token logging

When using WebSockets or Server-Sent Events, the browser client sends the access token in the query string. Receiving the access token via query string is generally as secure as using the standard `Authorization` header. Always use HTTPS to ensure a secure end-to-end connection between the client and the server. Many web servers log the URL for each request, including the query string. Logging the URLs may log the access token. ASP.NET Core logs the URL for each request by default, which includes the query string. For example:

```
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 GET http://localhost:5000/chathub?access_token=1234
```

If you have concerns about logging this data with your server logs, you can disable this logging entirely by configuring the `Microsoft.AspNetCore.Hosting` logger to the `Warning` level or above (these messages are written at `Info` level). For more information, see [Apply log filter rules in code](../fundamentals/logging/?view=aspnetcore-10.0#apply-log-filter-rules-in-code) for more information. If you still want to log certain request information, you can [write middleware](../fundamentals/middleware/write?view=aspnetcore-10.0) to log the data that you require and filter out the `access_token` query string value (if present).

## Exceptions

Exception messages are generally considered sensitive data that shouldn't be revealed to a client. By default, SignalR doesn't send the details of an exception thrown by a hub method to the client. Instead, the client receives a generic message indicating an error occurred. Exception message delivery to the client can be overridden (for example in development or test) with [EnableDetailedErrors](configuration?view=aspnetcore-10.0#configure-server-options). Exception messages should not be exposed to the client in production apps.

## Buffer management

SignalR uses per-connection buffers to manage incoming and outgoing messages. By default, SignalR limits these buffers to 32 KB. The largest message a client or server can send is 32 KB. The maximum memory consumed by a connection for messages is 32 KB. If your messages are always smaller than 32 KB, you can reduce the limit, which:

* Prevents a client from being able to send a larger message.
* The server will never need to allocate large buffers to accept messages.

If your messages are larger than 32 KB, you can increase the limit. Increasing this limit means:

* The client can cause the server to allocate large memory buffers.
* Server allocation of large buffers may reduce the number of concurrent connections.

There are limits for incoming and outgoing messages, both can be configured on the [HttpConnectionDispatcherOptions](configuration?view=aspnetcore-10.0#configure-server-options) object configured in `MapHub`:

* `ApplicationMaxBufferSize` represents the maximum number of bytes from the client that the server buffers. If the client attempts to send a message larger than this limit, the connection may be closed.
* `TransportMaxBufferSize` represents the maximum number of bytes the server can send. If the server attempts to send a message (including return values from hub methods) larger than this limit, an exception will be thrown.

Setting the limit to `0` disables the limit. Removing the limit allows a client to send a message of any size. Malicious clients sending large messages can cause excess memory to be allocated. Excess memory usage can significantly reduce the number of concurrent connections.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/mvc1005
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/mvc1005.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# MVC1005: Cannot use UseMvc with Endpoint Routing

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | MVC1005      |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

UseMvc was invoked as part of startup.

### Rule description

Using MVC via [UseMvc](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvc) or [UseMvcWithDefaultRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvcwithdefaultroute) requires an explicit opt-in inside `Startup.ConfigureServices`. This is required because MVC must know whether it can rely on the authorization and CORS middleware during initialization.

## How to fix violations

If the app requires legacy [IRouter](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouter) support, disable [EnableEndpointRouting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.enableendpointrouting#microsoft-aspnetcore-mvc-mvcoptions-enableendpointrouting)using any of the following approaches in `Startup.ConfigureServices`:

```
services.AddMvc(options => options.EnableEndpointRouting = false);
```

If legacy `IRouter` support is not required, replace the call to `UseMvc` with `UseEndpoints`. For more details, see the [migration guide](../migration/22-to-30?view=aspnetcore-10.0#migrate-startupconfigure).

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/http-client-config
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/http-client-config.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP HTTP Client Configuration

## Introduction

Each [Cluster](/en-us/dotnet/api/yarp.reverseproxy.configuration.clusterconfig) has a dedicated [HttpMessageInvoker](/en-us/dotnet/api/system.net.http.httpmessageinvoker) instance used to forward requests to its [Destination](/en-us/dotnet/api/yarp.reverseproxy.configuration.destinationconfig)s. The configuration is defined per cluster. On YARP startup, all clusters get new `HttpMessageInvoker` instances, however if later the cluster configuration gets changed the [IForwarderHttpClientFactory](/en-us/dotnet/api/yarp.reverseproxy.forwarder.iforwarderhttpclientfactory) will re-run and decide if it should create a new `HttpMessageInvoker` or keep using the existing one. The default `IForwarderHttpClientFactory` implementation creates a new `HttpMessageInvoker` when there are changes to the [HttpClientConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.httpclientconfig).

Properties of outgoing requests for a given cluster can be configured as well. They are defined in [ForwarderRequestConfig](/en-us/dotnet/api/yarp.reverseproxy.forwarder.forwarderrequestconfig).

The configuration is represented differently if you're using the [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) model or the code-first model.

## IConfiguration

These types are focused on defining serializable configuration. The code based configuration model is described below in the "Code Configuration" section.

### HttpClient

HTTP client configuration is based on [HttpClientConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.httpclientconfig) and represented by the following configuration schema. If you need a more granular approach, use a [custom implementation](http-client-config?view=aspnetcore-10.0#custom-iforwarderhttpclientfactory) of `IForwarderHttpClientFactory`.

```
"HttpClient": {
  "SslProtocols": [ "<protocol-names>" ],
  "MaxConnectionsPerServer": "<int>",
  "DangerousAcceptAnyServerCertificate": "<bool>",
  "RequestHeaderEncoding": "<encoding-name>",
  "ResponseHeaderEncoding": "<encoding-name>",
  "EnableMultipleHttp2Connections": "<bool>",
  "WebProxy": {
    "Address": "<url>",
    "BypassOnLocal": "<bool>",
    "UseDefaultCredentials": "<bool>"
  }
}
```

Configuration settings:

* SslProtocols - [SSL protocols](/en-us/dotnet/api/system.security.authentication.sslprotocols) enabled on the given HTTP client. Protocol names are specified as array of strings. Default value is [None](/en-us/dotnet/api/system.security.authentication.sslprotocols#System_Security_Authentication_SslProtocols_None).

```
"SslProtocols": [
  "Tls11",
  "Tls12"
]
```

* MaxConnectionsPerServer - maximal number of HTTP 1.1 connections open concurrently to the same server. Default value is [int32.MaxValue](/en-us/dotnet/api/system.int32.maxvalue).

```
"MaxConnectionsPerServer": "10"
```

* DangerousAcceptAnyServerCertificate - indicates whether the server's SSL certificate validity is checked by the client. Setting it to `true` completely disables validation. Default value is `false`.

```
"DangerousAcceptAnyServerCertificate": "true"
```

* RequestHeaderEncoding - enables other than ASCII encoding for outgoing request headers. Setting this value will leverage [`SocketsHttpHandler.RequestHeaderEncodingSelector`](/en-us/dotnet/api/system.net.http.socketshttphandler.requestheaderencodingselector) and use the selected encoding for all headers. The value is then parsed by [`Encoding.GetEncoding`](/en-us/dotnet/api/system.text.encoding.getencoding#System_Text_Encoding_GetEncoding_System_String_), use values like: "utf-8", "iso-8859-1", etc.

```
"RequestHeaderEncoding": "utf-8"
```

* ResponseHeaderEncoding - enables other than ASCII encoding for incoming response headers (from requests that the proxy would forward out). Setting this value will leverage [`SocketsHttpHandler.ResponseHeaderEncodingSelector`](/en-us/dotnet/api/system.net.http.socketshttphandler.responseheaderencodingselector) and use the selected encoding for all headers. The value is then parsed by [`Encoding.GetEncoding`](/en-us/dotnet/api/system.text.encoding.getencoding#System_Text_Encoding_GetEncoding_System_String_), use values like: "utf-8", "iso-8859-1", etc.

```
"ResponseHeaderEncoding": "utf-8"
```

Note that if you're using an encoding other than ASCII, you also need to set your server to accept requests and/or send responses with such headers. For example, when using Kestrel as the server, use [`KestrelServerOptions.RequestHeaderEncodingSelector`](/en-us/dotnet/api/Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.RequestHeaderEncodingSelector) / [`.ResponseHeaderEncodingSelector`](/en-us/dotnet/api/Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions.ResponseHeaderEncodingSelector) to configure Kestrel to allow `Latin1` ("`iso-8859-1`") headers:

```
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(kestrel =>
{
    kestrel.RequestHeaderEncodingSelector = _ => Encoding.Latin1;
    // and/or
    kestrel.ResponseHeaderEncodingSelector = _ => Encoding.Latin1;
});
```

* EnableMultipleHttp2Connections - enables opening additional HTTP/2 connections to the same server when the maximum number of concurrent streams is reached on all existing connections. The default is `true`. See [SocketsHttpHandler.EnableMultipleHttp2Connections](/en-us/dotnet/api/system.net.http.socketshttphandler.enablemultiplehttp2connections)

```
"EnableMultipleHttp2Connections": false
```

* WebProxy - Enables sending requests through an outbound HTTP proxy to reach the destinations. See [`SocketsHttpHandler.Proxy`](/en-us/dotnet/api/system.net.http.socketshttphandler.proxy) for details.

  * Address - The url address of the outbound proxy.
  * BypassOnLocal - A bool indicating if requests to local addresses should bypass the outbound proxy.
  * UseDefaultCredentials - A bool indicating if the current application credentials should be use to authenticate to the outbound proxy. ASP.NET Core does not impersonate authenticated users for outbound requests.

```
"WebProxy": {
  "Address": "http://myproxy:8080",
  "BypassOnLocal": "true",
  "UseDefaultCredentials": "false"
}
```

Note

The default `ForwarderHttpClientFactory` sets `UseProxy` to `false`. If you want to use the default system proxy instead of specifying a custom proxy address, you can set `UseProxy` back to `true` using the `ConfigureHttpClient` method. This is useful when debugging with tools like Fiddler. See the [Configuring the http client](#configuring-the-http-client) section for an example.

### HttpRequest

HTTP request configuration is based on [ForwarderRequestConfig](/en-us/dotnet/api/yarp.reverseproxy.forwarder.forwarderrequestconfig) and represented by the following configuration schema.

```
"HttpRequest": {
  "ActivityTimeout": "<timespan>",
  "Version": "<string>",
  "VersionPolicy": ["RequestVersionOrLower", "RequestVersionOrHigher", "RequestVersionExact"],
  "AllowResponseBuffering": "<bool>"
}
```

Configuration settings:

* ActivityTimeout - how long a request is allowed to remain idle between any operation completing, after which it will be canceled. The default is 100 seconds. The timeout will reset when response headers are received or after successfully reading or writing any request, response, or streaming data like gRPC or WebSockets. TCP keep-alives and HTTP/2 protocol pings will not reset the timeout, but WebSocket pings will.
* Version - outgoing request [version](/en-us/dotnet/api/system.net.http.httprequestmessage.version). The supported values at the moment are `1.0`, `1.1`, `2` and `3`. Default value is 2.
* VersionPolicy - defines how the final version is selected for the outgoing requests. See [HttpRequestMessage.VersionPolicy](/en-us/dotnet/api/system.net.http.httprequestmessage.versionpolicy). The default value is `RequestVersionOrLower`.
* AllowResponseBuffering - allows to use write buffering when sending a response back to the client, if the server hosting YARP (e.g. IIS) supports it. **NOTE**: enabling it can break SSE (server side event) scenarios.

## Configuration example

The below example shows 2 samples of HTTP client and request configurations for `cluster1` and `cluster2`.

```
{
  "Clusters": {
    "cluster1": {
      "LoadBalancingPolicy": "Random",
      "HttpClient": {
        "SslProtocols": [
          "Tls11",
          "Tls12"
        ],
        "MaxConnectionsPerServer": "10",
        "DangerousAcceptAnyServerCertificate": "true"
      },
      "HttpRequest": {
        "ActivityTimeout": "00:00:30"
      },
      "Destinations": {
        "cluster1/destination1": {
          "Address": "https://localhost:10000/"
        },
        "cluster1/destination2": {
          "Address": "http://localhost:10010/"
        }
      }
    },
    "cluster2": {
      "HttpClient": {
        "SslProtocols": [
          "Tls12"
        ]
      },
      "HttpRequest": {
        "Version": "1.1",
        "VersionPolicy": "RequestVersionExact"
      },
      "Destinations": {
        "cluster2/destination1": {
          "Address": "https://localhost:10001/"
        }
      }
    }
  }
}
```

## Code Configuration

HTTP client configuration uses the type [HttpClientConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.httpclientconfig).

The following is an example of `HttpClientConfig` using [code based](config-providers?view=aspnetcore-10.0) configuration. An instance of `HttpClientConfig` is assigned to the [ClusterConfig.HttpClient](/en-us/dotnet/api/yarp.reverseproxy.configuration.clusterconfig) property before passing the cluster array to `LoadFromMemory` method.

```
var routes = new[]
{
    new RouteConfig()
    {
        RouteId = "route1",
        ClusterId = "cluster1",
        Match =
        {
            Path = "{**catch-all}"
        }
    }
};
var clusters = new[]
{
    new ClusterConfig()
    {
        ClusterId = "cluster1",
        Destinations =
        {
            { "destination1", new DestinationConfig() { Address = "https://localhost:10000" } }
        },
        HttpClient = new HttpClientConfig { MaxConnectionsPerServer = 10, SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12 }
    }
};

services.AddReverseProxy()
    .LoadFromMemory(routes, clusters);
```

## Configuring the http client

`ConfigureHttpClient` provides a callback to customize the `SocketsHttpHandler` settings used for proxying requests. This will be called each time a cluster is added or changed. Cluster settings are applied to the handler before the callback. Custom data can be provided in the cluster metadata. This example shows adding a client certificate that will authenticate the proxy to the destination servers.

```
var clientCert = new X509Certificate2("path");
services.AddReverseProxy()
    .ConfigureHttpClient((context, handler) =>
    {
        handler.SslOptions.ClientCertificates.Add(clientCert);
    });
```

### Using the default system proxy

The default `ForwarderHttpClientFactory` sets `UseProxy` to `false` to avoid the overhead of proxy detection and configuration. If you need to use the default system proxy (for example, when debugging with Fiddler or other proxy tools), you can set `UseProxy` back to `true`:

```
services.AddReverseProxy()
    .ConfigureHttpClient((context, handler) =>
    {
        handler.UseProxy = true;
    });
```

This configuration allows the `HttpClient` to use the system's default proxy settings without having to manually specify a proxy address in the `WebProxy` configuration.

## Custom IForwarderHttpClientFactory

If direct control of HTTP client construction is necessary, the default [IForwarderHttpClientFactory](/en-us/dotnet/api/yarp.reverseproxy.forwarder.iforwarderhttpclientfactory) can be replaced with a custom one. For some customizations you can derive from the default [ForwarderHttpClientFactory](/en-us/dotnet/api/yarp.reverseproxy.forwarder.forwarderhttpclientfactory) and override the methods that configure the client.

It's recommended that any custom factory set the following `SocketsHttpHandler` properties to the same values as the default factory does in order to preserve a correct reverse proxy behavior and avoid unnecessary overhead.

```
new SocketsHttpHandler
{
    UseProxy = false,
    AllowAutoRedirect = false,
    AutomaticDecompression = DecompressionMethods.None,
    UseCookies = false,
    EnableMultipleHttp2Connections = true,
    ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),
    ConnectTimeout = TimeSpan.FromSeconds(15),
};
```

Always return an HttpMessageInvoker instance rather than an HttpClient instance which derives from HttpMessageInvoker. HttpClient buffers responses by default which breaks streaming scenarios and increases memory usage and latency.

Custom data can be provided in the cluster metadata.

The below is an example of a custom `IForwarderHttpClientFactory` implementation.

```
public class CustomForwarderHttpClientFactory : IForwarderHttpClientFactory
{
    public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context)
    {
        var handler = new SocketsHttpHandler
        {
            UseProxy = false,
            AllowAutoRedirect = false,
            AutomaticDecompression = DecompressionMethods.None,
            UseCookies = false,
            EnableMultipleHttp2Connections = true,
            ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),
            ConnectTimeout = TimeSpan.FromSeconds(15),
        };

        return new HttpMessageInvoker(handler, disposeHandler: true);
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-23

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Razor components

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to create and use Razor components in Blazor apps, including guidance on Razor syntax, component naming, namespaces, and component parameters.

## Razor components

Blazor apps are built using *Razor components*, informally known as *Blazor components* or only *components*. A component is a self-contained portion of user interface (UI) with processing logic to enable dynamic behavior. Components can be nested, reused, shared among projects, and used in MVC and Razor Pages apps.

Components render into an in-memory representation of the browser's [Document Object Model (DOM)](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction) called a *render tree*, which is used to update the UI in a flexible and efficient way.

Although "Razor components" shares some naming with other ASP.NET Core content-rendering technologies, Razor components must be distinguished from the following different features in ASP.NET Core:

* [Razor views](../../tutorials/first-mvc-app/adding-view?view=aspnetcore-10.0), which are [Razor-based](../../mvc/views/razor?view=aspnetcore-10.0) markup pages for MVC apps.
* [View components](../../mvc/views/view-components?view=aspnetcore-10.0), which are for rendering chunks of content rather than whole responses in Razor Pages and MVC apps.

Important

When using a Blazor Web App, most of the Blazor documentation example components ***require*** interactivity to function and demonstrate the concepts covered by the articles. *Interactivity* makes it possible for users to interact with rendered components. This includes app responses to [Document Object Model (DOM)](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction) events and state changes tied to C# members via Blazor's event handlers and binding. When you test an example component provided by an article in a Blazor Web App, make sure that either the app adopts global interactivity or the component adopts an interactive render mode. More information on this subject is provided by [ASP.NET Core Blazor render modes](render-modes?view=aspnetcore-10.0), which is the next article in the table of contents after this article.

## Component classes

Components are implemented using a combination of C# and HTML markup in [Razor](../../mvc/views/razor?view=aspnetcore-10.0) component files with the `.razor` file extension.

[ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) is the base class for components described by Razor component files. [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) implements the lowest abstraction of components, the [IComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.icomponent) interface. [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) defines component properties and methods for basic functionality, for example, to process a set of built-in component lifecycle events.

[`ComponentBase` in `dotnet/aspnetcore` reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ComponentBase.cs): The reference source contains additional remarks on the built-in lifecycle events. However, keep in mind that the internal implementations of component features are subject to change at any time without notice.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Developers typically create Razor components from Razor component files (`.razor`) or base their components on [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase), but components can also be built by implementing [IComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.icomponent). Developer-built components that implement [IComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.icomponent) can take low-level control over rendering at the cost of having to manually trigger rendering with events and lifecycle methods that the developer must create and maintain.

Additional conventions adopted by Blazor documentation example code and sample apps is found in [ASP.NET Core Blazor fundamentals](../fundamentals/?view=aspnetcore-10.0#razor-components).

### Razor syntax

Components use [Razor syntax](../../mvc/views/razor?view=aspnetcore-10.0). Two Razor features are extensively used by components, *directives* and *directive attributes*. These are reserved keywords prefixed with `@` that appear in Razor markup:

* [Directives](../../mvc/views/razor?view=aspnetcore-10.0#directives): Change the way component markup is compiled or functions. For example, the [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive specifies a routable component with a route template that can be reached directly by a user's request in the browser at a specific URL.

  By convention, a component's directives at the top of a component definition (`.razor` file) are placed in a consistent order. For repeated directives, directives are placed alphabetically by namespace or type, except `@using` directives, which have special second-level ordering.

  The following order is adopted by Blazor sample apps and documentation. Components provided by a Blazor project template may differ from the following order and use a different format. For example, Blazor framework Identity components include blank lines between blocks of `@using` directives and blocks of `@inject` directives. You're free to use a custom ordering scheme and format in your own apps.

  Documentation and sample app Razor directive order:

  * `@page`

  * `@rendermode` (.NET 8 or later)

  * `@using`

    * `System` namespaces (alphabetical order)
    * `Microsoft` namespaces (alphabetical order)
    * Third-party API namespaces (alphabetical order)
    * App namespaces (alphabetical order)

  * Other directives (alphabetical order)

  Note

  A *render mode* is only applied in Blazor Web Apps and includes modes that establish user interactivity with the rendered component. For more information, see [ASP.NET Core Blazor render modes](render-modes?view=aspnetcore-10.0).

  No blank lines appear among the directives. One blank line appears between the directives and the first line of Razor markup.

  Example:

  ```
  @page "/doctor-who-episodes/{season:int}"
  @rendermode InteractiveWebAssembly
  @using System.Globalization
  @using System.Text.Json
  @using Microsoft.AspNetCore.Localization
  @using Mandrill
  @using BlazorSample.Components.Layout
  @attribute [Authorize]
  @implements IAsyncDisposable
  @inject IJSRuntime JS
  @inject ILogger<DoctorWhoEpisodes> Logger

  <PageTitle>Doctor Who Episode List</PageTitle>

  ...
  ```

* [Directive attributes](../../mvc/views/razor?view=aspnetcore-10.0#directive-attributes): Change the way a component element is compiled or functions.

  Example:

  ```
  <input @bind="episodeId" />
  ```

  You can prefix directive attribute values with the at symbol (`@`) for non-explicit Razor expressions (`@bind="@episodeId"`), but we don't recommend it, and the docs don't adopt the approach in examples.

Directives and directive attributes used in components are explained further in this article and other articles of the Blazor documentation set. For general information on Razor syntax, see [Razor syntax reference for ASP.NET Core](../../mvc/views/razor?view=aspnetcore-10.0).

### Component name, class name, and namespace

A component's name must start with an uppercase character:

✔️Supported: `ProductDetail.razor`

❌Unsupported: `productDetail.razor`

Common Blazor naming conventions used throughout the Blazor documentation include:

* File paths and file names use Pascal case† and appear before showing code examples. If a path is present, it indicates the typical folder location. For example, `Components/Pages/ProductDetail.razor` indicates that the `ProductDetail` component has a file name of `ProductDetail.razor` and resides in the `Pages` folder of the `Components` folder of the app.
* Component file paths for routable components match their URLs in kebab case‡ with hyphens appearing between words in a component's route template. For example, a `ProductDetail` component with a route template of `/product-detail` (`@page "/product-detail"`) is requested in a browser at the relative URL `/product-detail`.

†Pascal case (upper camel case) is a naming convention without spaces and punctuation and with the first letter of each word capitalized, including the first word.\
‡Kebab case is a naming convention without spaces and punctuation that uses lowercase letters and dashes between words.

Components are ordinary [C# classes](/en-us/dotnet/csharp/programming-guide/classes-and-structs/classes) and can be placed anywhere within a project. Components that produce webpages usually reside in the `Components/Pages` folder. Non-page components are frequently placed in the `Components` folder or a custom folder added to the project.

Typically, a component's namespace is derived from the app's root namespace and the component's location (folder) within the app. If the app's root namespace is `BlazorSample` and the `Counter` component resides in the `Components/Pages` folder:

* The `Counter` component's namespace is `BlazorSample.Components.Pages`.
* The fully qualified type name of the component is `BlazorSample.Components.Pages.Counter`.

For custom folders that hold components, add an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive to the parent component or to the app's imports file (`_Imports.razor`). The following example makes components in the `AdminComponents` folder available:

```
@using BlazorSample.AdminComponents
```

Note

[`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directives in the imports file (`_Imports.razor`) are only applied to Razor files (`.razor`), not C# files (`.cs`).

Aliased [`using`](/en-us/dotnet/csharp/language-reference/keywords/using-directive) statements are supported. In the following example, the public `WeatherForecast` class of the `GridRendering` component is made available as `WeatherForecast` in a component elsewhere in the app:

```
@using WeatherForecast = Components.Pages.GridRendering.WeatherForecast
```

Components can also be referenced using their fully qualified names, which doesn't require an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive. The following example directly references the `ProductDetail` component in the `AdminComponents/Pages` folder of the app:

```
<BlazorSample.AdminComponents.Pages.ProductDetail />
```

The namespace of a component authored with Razor is based on the following (in priority order):

* The [`@namespace`](../../mvc/views/razor?view=aspnetcore-10.0#namespace) directive in the Razor file's markup (for example, `@namespace BlazorSample.CustomNamespace`).

* The project's `RootNamespace` in the project file (for example, `<RootNamespace>BlazorSample</RootNamespace>`).

* The project namespace and the path from the project root to the component. For example, the framework resolves `{PROJECT NAMESPACE}/Components/Pages/Home.razor` with a project namespace of `BlazorSample` to the namespace `BlazorSample.Components.Pages` for the `Home` component. `{PROJECT NAMESPACE}` is the project namespace. Components follow C# name binding rules. For the `Home` component in this example, the components in scope are all of the components:

  * In the same folder, `Components/Pages`.
  * The components in the project's root that don't explicitly specify a different namespace.

The following are **not** supported:

* The [`global::`](/en-us/dotnet/csharp/language-reference/operators/namespace-alias-qualifier) qualification.
* Partially-qualified names. For example, you can't add `@using BlazorSample.Components` to a component and then reference the `NavMenu` component in the app's `Components/Layout` folder (`Components/Layout/NavMenu.razor`) with `<Layout.NavMenu></Layout.NavMenu>`.

A component's name must start with an uppercase character:

✔️Supported: `ProductDetail.razor`

❌Unsupported: `productDetail.razor`

Common Blazor naming conventions used throughout the Blazor documentation include:

* File paths and file names use Pascal case† and appear before showing code examples. If a path is present, it indicates the typical folder location. For example, `Pages/ProductDetail.razor` indicates that the `ProductDetail` component has a file name of `ProductDetail.razor` and resides in the `Pages` folder of the app.
* Component file paths for routable components match their URLs in kebab case‡ with hyphens appearing between words in a component's route template. For example, a `ProductDetail` component with a route template of `/product-detail` (`@page "/product-detail"`) is requested in a browser at the relative URL `/product-detail`.

†Pascal case (upper camel case) is a naming convention without spaces and punctuation and with the first letter of each word capitalized, including the first word.\
‡Kebab case is a naming convention without spaces and punctuation that uses lowercase letters and dashes between words.

Components are ordinary [C# classes](/en-us/dotnet/csharp/programming-guide/classes-and-structs/classes) and can be placed anywhere within a project. Components that produce webpages usually reside in the `Pages` folder. Non-page components are frequently placed in the `Shared` folder or a custom folder added to the project.

Typically, a component's namespace is derived from the app's root namespace and the component's location (folder) within the app. If the app's root namespace is `BlazorSample` and the `Counter` component resides in the `Pages` folder:

* The `Counter` component's namespace is `BlazorSample.Pages`.
* The fully qualified type name of the component is `BlazorSample.Pages.Counter`.

For custom folders that hold components, add an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive to the parent component or to the app's imports file (`_Imports.razor`). The following example makes components in the `AdminComponents` folder available:

```
@using BlazorSample.AdminComponents
```

Note

[`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directives in the imports file (`_Imports.razor`) are only applied to Razor files (`.razor`), not C# files (`.cs`).

Aliased [`using`](/en-us/dotnet/csharp/language-reference/keywords/using-directive) statements are supported. In the following example, the public `WeatherForecast` class of the `GridRendering` component is made available as `WeatherForecast` in a component elsewhere in the app:

```
@using WeatherForecast = Pages.GridRendering.WeatherForecast
```

Components can also be referenced using their fully qualified names, which doesn't require an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive. The following example directly references the `ProductDetail` component in the `Components` folder of the app:

```
<BlazorSample.Components.ProductDetail />
```

The namespace of a component authored with Razor is based on the following (in priority order):

* The [`@namespace`](../../mvc/views/razor?view=aspnetcore-10.0#namespace) directive in the Razor file's markup (for example, `@namespace BlazorSample.CustomNamespace`).

* The project's `RootNamespace` in the project file (for example, `<RootNamespace>BlazorSample</RootNamespace>`).

* The project namespace and the path from the project root to the component. For example, the framework resolves `{PROJECT NAMESPACE}/Pages/Index.razor` with a project namespace of `BlazorSample` to the namespace `BlazorSample.Pages` for the `Index` component. `{PROJECT NAMESPACE}` is the project namespace. Components follow C# name binding rules. For the `Index` component in this example, the components in scope are all of the components:

  * In the same folder, `Pages`.
  * The components in the project's root that don't explicitly specify a different namespace.

The following are **not** supported:

* The [`global::`](/en-us/dotnet/csharp/language-reference/operators/namespace-alias-qualifier) qualification.
* Partially-qualified names. For example, you can't add `@using BlazorSample` to a component and then reference the `NavMenu` component in the app's `Shared` folder (`Shared/NavMenu.razor`) with `<Shared.NavMenu></Shared.NavMenu>`.

### Partial class support

Components are generated as [C# partial classes](/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods) and are authored using either of the following approaches:

* A single file contains C# code defined in one or more [`@code`](../../mvc/views/razor?view=aspnetcore-10.0#code) blocks, HTML markup, and Razor markup. Blazor project templates define their components using this single-file approach.
* HTML and Razor markup are placed in a Razor file (`.razor`). C# code is placed in a code-behind file defined as a partial class (`.cs`).

Note

A component stylesheet that defines component-specific styles is a separate file (`.css`). Blazor CSS isolation is described later in [ASP.NET Core Blazor CSS isolation](css-isolation?view=aspnetcore-10.0).

The following example shows the default `Counter` component with an [`@code`](../../mvc/views/razor?view=aspnetcore-10.0#code) block in an app generated from a Blazor project template. Markup and C# code are in the same file. This is the most common approach taken in component authoring.

`Counter.razor`:

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount() => currentCount++;
}
```

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount() => currentCount++;
}
```

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

The following `Counter` component splits presentation HTML and Razor markup from the C# code using a code-behind file with a partial class. Splitting the markup from the C# code is favored by some organizations and developers to organize their component code to suit how they prefer to work. For example, the organization's UI expert can work on the presentation layer independently of another developer working on the component's C# logic. The approach is also useful when working with automatically-generated code or source generators. For more information, see [Partial Classes and Methods (C# Programming Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods).

`CounterPartialClass.razor`:

```
@page "/counter-partial-class"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
```

```
@page "/counter-partial-class"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
```

```
@page "/counter-partial-class"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
```

```
@page "/counter-partial-class"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
```

```
@page "/counter-partial-class"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
```

```
@page "/counter-partial-class"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
```

`CounterPartialClass.razor.cs`:

```
namespace BlazorSample.Components.Pages;

public partial class CounterPartialClass
{
    private int currentCount = 0;

    private void IncrementCount() => currentCount++;
}
```

```
namespace BlazorSample.Components.Pages;

public partial class CounterPartialClass
{
    private int currentCount = 0;

    private void IncrementCount() => currentCount++;
}
```

```
namespace BlazorSample.Pages;

public partial class CounterPartialClass
{
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
namespace BlazorSample.Pages
{
    public partial class CounterPartialClass
    {
        private int currentCount = 0;

        private void IncrementCount()
        {
            currentCount++;
        }
    }
}
```

[`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directives in the imports file (`_Imports.razor`) are only applied to Razor files (`.razor`), not C# files (`.cs`). Add namespaces to a partial class file as needed.

Typical namespaces used by components:

```
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Sections
using Microsoft.AspNetCore.Components.Web;
using static Microsoft.AspNetCore.Components.Web.RenderMode;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.JSInterop;
```

Typical namespaces also include the namespace of the app and the namespace corresponding to the app's `Components` folder:

```
using BlazorSample;
using BlazorSample.Components;
```

Additional folders can also be included, such as the `Layout` folder:

```
using BlazorSample.Components.Layout;
```

```
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.JSInterop;
```

Typical namespaces also include the namespace of the app and the namespace corresponding to the app's `Shared` folder:

```
using BlazorSample;
using BlazorSample.Shared;
```

```
using System.Net.Http;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
```

Typical namespaces also include the namespace of the app and the namespace corresponding to the app's `Shared` folder:

```
using BlazorSample;
using BlazorSample.Shared;
```

### Specify a base class

The [`@inherits`](../../mvc/views/razor?view=aspnetcore-10.0#inherits) directive is used to specify a base class for a component. Unlike using [partial classes](#partial-class-support), which only split markup from C# logic, using a base class allows you to inherit C# code for use across a group of components that share the base class's properties and methods. Using base classes reduce code redundancy in apps and are useful when supplying base code from class libraries to multiple apps. For more information, see [Inheritance in C# and .NET](/en-us/dotnet/csharp/fundamentals/tutorials/inheritance).

In the following example, the `BlazorRocksBase1` base class derives from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase).

`BlazorRocks1.razor`:

```
@page "/blazor-rocks-1"
@inherits BlazorRocksBase1

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 1</h1>

<p>
    @BlazorRocksText
</p>
```

```
@page "/blazor-rocks-1"
@inherits BlazorRocksBase1

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 1</h1>

<p>
    @BlazorRocksText
</p>
```

```
@page "/blazor-rocks-1"
@inherits BlazorRocksBase1

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 1</h1>

<p>
    @BlazorRocksText
</p>
```

```
@page "/blazor-rocks-1"
@inherits BlazorRocksBase1

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 1</h1>

<p>
    @BlazorRocksText
</p>
```

```
@page "/blazor-rocks-1"
@inherits BlazorRocksBase1

<h1>Blazor Rocks! Example 1</h1>

<p>
    @BlazorRocksText
</p>
```

```
@page "/blazor-rocks-1"
@inherits BlazorRocksBase1

<h1>Blazor Rocks! Example 1</h1>

<p>
    @BlazorRocksText
</p>
```

`BlazorRocksBase1.cs`:

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase1 : ComponentBase
{
    public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase1 : ComponentBase
{
    public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase1 : ComponentBase
{
    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase1 : ComponentBase
{
    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase1 : ComponentBase
{
    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase1 : ComponentBase
{
    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";
}
```

### Markup

A component's UI is defined using [Razor syntax](../../mvc/views/razor?view=aspnetcore-10.0), which consists of Razor markup, C#, and HTML. When an app is compiled, the HTML markup and C# rendering logic are converted into a component class. The name of the generated class matches the name of the file.

Members of the component class are defined in one or more [`@code`](../../mvc/views/razor?view=aspnetcore-10.0#code) blocks. In [`@code`](../../mvc/views/razor?view=aspnetcore-10.0#code) blocks, component state is specified and processed with C#:

* Property and field initializers.
* Parameter values from arguments passed by parent components and route parameters.
* Methods for user event handling, lifecycle events, and custom component logic.

Component members are used in rendering logic using C# expressions that start with the `@` symbol. For example, a C# field is rendered by prefixing `@` to the field name. The following `Markup` component evaluates and renders:

* `headingFontStyle` for the CSS property value `font-style` of the heading element.
* `headingText` for the content of the heading element.

`Markup.razor`:

```
@page "/markup"

<PageTitle>Markup</PageTitle>

<h1>Markup Example</h1>

<h2 style="font-style:@headingFontStyle">@headingText</h2>

@code {
    private string headingFontStyle = "italic";
    private string headingText = "Put on your new Blazor!";
}
```

```
@page "/markup"

<PageTitle>Markup</PageTitle>

<h1>Markup Example</h1>

<h2 style="font-style:@headingFontStyle">@headingText</h2>

@code {
    private string headingFontStyle = "italic";
    private string headingText = "Put on your new Blazor!";
}
```

```
@page "/markup"

<h1 style="font-style:@headingFontStyle">@headingText</h1>

@code {
    private string headingFontStyle = "italic";
    private string headingText = "Put on your new Blazor!";
}
```

```
@page "/markup"

<h1 style="font-style:@headingFontStyle">@headingText</h1>

@code {
    private string headingFontStyle = "italic";
    private string headingText = "Put on your new Blazor!";
}
```

```
@page "/markup"

<h1 style="font-style:@headingFontStyle">@headingText</h1>

@code {
    private string headingFontStyle = "italic";
    private string headingText = "Put on your new Blazor!";
}
```

```
@page "/markup"

<h1 style="font-style:@headingFontStyle">@headingText</h1>

@code {
    private string headingFontStyle = "italic";
    private string headingText = "Put on your new Blazor!";
}
```

Note

Examples throughout the Blazor documentation specify the [`private` access modifier](/en-us/dotnet/csharp/language-reference/keywords/private) for private members. Private members are scoped to a component's class. However, C# assumes the `private` access modifier when no access modifier is present, so explicitly marking members "`private`" in your own code is optional. For more information on access modifiers, see [Access Modifiers (C# Programming Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers).

The Blazor framework processes a component internally as a [*render tree*](https://developer.mozilla.org/docs/Web/Performance/How_browsers_work#render), which is the combination of a component's DOM and [Cascading Style Sheet Object Model (CSSOM)](https://developer.mozilla.org/docs/Web/API/CSS_Object_Model). After the component is initially rendered, the component's render tree is regenerated in response to events. Blazor compares the new render tree against the previous render tree and applies any modifications to the browser's DOM for display. For more information, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0).

Razor syntax for C# control structures, directives, and directive attributes are lowercase (examples: [`@if`](../../mvc/views/razor?view=aspnetcore-10.0#conditionals-if-else-if-else-and-switch), [`@code`](../../mvc/views/razor?view=aspnetcore-10.0#code), [`@bind`](../../mvc/views/razor?view=aspnetcore-10.0#bind)). Property names are uppercase (example: `@Body` for [LayoutComponentBase.Body](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase.body#microsoft-aspnetcore-components-layoutcomponentbase-body)).

### Asynchronous methods (`async`) don't support returning `void`

The Blazor framework doesn't track `void`-returning asynchronous methods (`async`). As a result, the entire process fails when an exception isn't caught if `void` is returned. Always return a [Task](/en-us/dotnet/api/system.threading.tasks.task)/[ValueTask](/en-us/dotnet/api/system.threading.tasks.valuetask) from asynchronous methods.

### Nested components

Components can include other components by declaring them using HTML syntax. The markup for using a component looks like an HTML tag where the name of the tag is the component type.

Consider the following `Heading` component, which can be used by other components to display a heading.

`Heading.razor`:

```
<h1 style="font-style:@headingFontStyle">Heading Example</h1>

@code {
    private string headingFontStyle = "italic";
}
```

```
<h1 style="font-style:@headingFontStyle">Heading Example</h1>

@code {
    private string headingFontStyle = "italic";
}
```

```
<h1 style="font-style:@headingFontStyle">Heading Example</h1>

@code {
    private string headingFontStyle = "italic";
}
```

```
<h1 style="font-style:@headingFontStyle">Heading Example</h1>

@code {
    private string headingFontStyle = "italic";
}
```

```
<h1 style="font-style:@headingFontStyle">Heading Example</h1>

@code {
    private string headingFontStyle = "italic";
}
```

```
<h1 style="font-style:@headingFontStyle">Heading Example</h1>

@code {
    private string headingFontStyle = "italic";
}
```

The following markup in the `HeadingExample` component renders the preceding `Heading` component at the location where the `<Heading />` tag appears.

`HeadingExample.razor`:

```
@page "/heading-example"

<PageTitle>Heading</PageTitle>

<h1>Heading Example</h1>

<Heading />
```

```
@page "/heading-example"

<PageTitle>Heading</PageTitle>

<h1>Heading Example</h1>

<Heading />
```

```
@page "/heading-example"

<Heading />
```

```
@page "/heading-example"

<Heading />
```

```
@page "/heading-example"

<Heading />
```

```
@page "/heading-example"

<Heading />
```

If a component contains an HTML element with an uppercase first letter that doesn't match a component name within the same namespace, a warning is emitted indicating that the element has an unexpected name. Adding an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive for the component's namespace makes the component available, which resolves the warning. For more information, see the [Component name, class name, and namespace](#component-name-class-name-and-namespace) section.

The `Heading` component example shown in this section doesn't have an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive, so the `Heading` component isn't directly accessible to a user via a direct request in the browser. However, any component with an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive can be nested in another component. If the `Heading` component was directly accessible by including `@page "/heading"` at the top of its Razor file, then the component would be rendered for browser requests at both `/heading` and `/heading-example`.

## Component parameters

*Component parameters* pass data to components and are defined using public [C# properties](/en-us/dotnet/csharp/programming-guide/classes-and-structs/properties) on the component class with the [`[Parameter]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute).

In the following `ParameterChild` component, component parameters include:

* Built-in reference types.

  * [System.String](/en-us/dotnet/api/system.string) to pass a title in `Title`.
  * [System.Int32](/en-us/dotnet/api/system.int32) to pass a count in `Count`.

* A user-defined reference type (`PanelBody`) to pass a Bootstrap card body in `Body`.

  `PanelBody.cs`:

  ```
  namespace BlazorSample;

  public class PanelBody
  {
      public string? Text { get; set; }
      public string? Style { get; set; }
  }
  ```

  ```
  namespace BlazorSample;

  public class PanelBody
  {
      public string? Text { get; set; }
      public string? Style { get; set; }
  }
  ```

  ```
  public class PanelBody
  {
      public string? Text { get; set; }
      public string? Style { get; set; }
  }
  ```

  ```
  public class PanelBody
  {
      public string? Text { get; set; }
      public string? Style { get; set; }
  }
  ```

  ```
  public class PanelBody
  {
      public string Text { get; set; }
      public string Style { get; set; }
  }
  ```

  ```
  public class PanelBody
  {
      public string Text { get; set; }
      public string Style { get; set; }
  }
  ```

`ParameterChild.razor`:

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" style="font-style:@Body.Style">
        <p>@Body.Text</p>
        @if (Count is not null)
        {
            <p>The count is @Count.</p>
        }
    </div>
</div>

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

    [Parameter]
    public PanelBody Body { get; set; } =
        new()
        {
            Text = "Card content set by child.",
            Style = "normal"
        };

    [Parameter]
    public int? Count { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" style="font-style:@Body.Style">
        <p>@Body.Text</p>
        @if (Count is not null)
        {
            <p>The count is @Count.</p>
        }
    </div>
</div>

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

    [Parameter]
    public PanelBody Body { get; set; } =
        new()
        {
            Text = "Card content set by child.",
            Style = "normal"
        };
    
    [Parameter]
    public int? Count { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" style="font-style:@Body.Style">
        <p>@Body.Text</p>
        @if (Count is not null)
        {
            <p>The count is @Count.</p>
        }
    </div>
</div>

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

    [Parameter]
    public PanelBody Body { get; set; } =
        new()
        {
            Text = "Set by child.",
            Style = "normal"
        };

    [Parameter]
    public int? Count { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" style="font-style:@Body.Style">
        <p>@Body.Text</p>
        @if (Count is not null)
        {
            <p>The count is @Count.</p>
        }
    </div>
</div>

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

    [Parameter]
    public PanelBody Body { get; set; } =
        new()
        {
            Text = "Set by child.",
            Style = "normal"
        };

    [Parameter]
    public int? Count { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" style="font-style:@Body.Style">
        <p>@Body.Text</p>
        @if (Count is not null)
        {
            <p>The count is @Count.</p>
        }
    </div>
</div>

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

    [Parameter]
    public PanelBody Body { get; set; } =
        new()
        {
            Text = "Set by child.",
            Style = "normal"
        };

    [Parameter]
    public int? Count { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" style="font-style:@Body.Style">
        <p>@Body.Text</p>
        @if (Count is not null)
        {
            <p>The count is @Count.</p>
        }
    </div>
</div>

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

    [Parameter]
    public PanelBody Body { get; set; } =
        new PanelBody()
        {
            Text = "Set by child.",
            Style = "normal"
        };

    [Parameter]
    public int? Count { get; set; }
}
```

Warning

Providing initial values for component parameters is supported, but don't create a component that writes to its own parameters after the component is rendered for the first time. For more information, see [Avoid overwriting parameters in ASP.NET Core Blazor](overwriting-parameters?view=aspnetcore-10.0).

The component parameters of the `ParameterChild` component can be set by arguments in the HTML tag that renders an instance of the `ParameterChild` component. The following parent component renders two `ParameterChild` components:

* The first `ParameterChild` component is rendered without supplying parameter arguments.
* The second `ParameterChild` component receives values for `Title` and `Body` from the parent component, which uses an [explicit C# expression](../../mvc/views/razor?view=aspnetcore-10.0#explicit-razor-expressions) to set the values of the `PanelBody`'s properties.

`Parameter1.razor`:

```
@page "/parameter-1"

<PageTitle>Parameter 1</PageTitle>

<h1>Parameter Example 1</h1>

<h1>Child component (without attribute values)</h1>

<ParameterChild />

<h1>Child component (with attribute values)</h1>

<ParameterChild Title="Set by Parent" 
    Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
```

`Parameter1.razor`:

```
@page "/parameter-1"

<PageTitle>Parameter 1</PageTitle>

<h1>Parameter Example 1</h1>

<h1>Child component (without attribute values)</h1>

<ParameterChild />

<h1>Child component (with attribute values)</h1>

<ParameterChild Title="Set by Parent" 
    Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
```

`ParameterParent.razor`:

```
@page "/parameter-parent"

<h1>Child component (without attribute values)</h1>

<ParameterChild />

<h1>Child component (with attribute values)</h1>

<ParameterChild Title="Set by Parent"
                Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
```

`ParameterParent.razor`:

```
@page "/parameter-parent"

<h1>Child component (without attribute values)</h1>

<ParameterChild />

<h1>Child component (with attribute values)</h1>

<ParameterChild Title="Set by Parent"
                Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
```

`ParameterParent.razor`:

```
@page "/parameter-parent"

<h1>Child component (without attribute values)</h1>

<ParameterChild />

<h1>Child component (with attribute values)</h1>

<ParameterChild Title="Set by Parent"
                Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
```

`ParameterParent.razor`:

```
@page "/parameter-parent"

<h1>Child component (without attribute values)</h1>

<ParameterChild />

<h1>Child component (with attribute values)</h1>

<ParameterChild Title="Set by Parent"
                Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
```

The following rendered HTML markup from the parent component shows `ParameterChild` component default values when the parent component doesn't supply component parameter values. When the parent component provides component parameter values, they replace the `ParameterChild` component's default values.

Note

For clarity, most of the rendered CSS style classes and some elements aren't shown in the following rendered HTML markup. The main concept demonstrated by the following example is that the parent component assigned values to the child component using its component parameters.

```
<h1>Child component (without attribute values)</h1>

<div>Set By Child</div>
<div style="font-style:normal">
    <p>Card content set by child.</p>
</div>

<h1>Child component (with attribute values)</h1>

<div>Set by Parent</div>
<div style="font-style:italic">
    <p>Set by parent.</p>
</div>
```

Assign a C# field, property, or result of a method to a component parameter as an HTML attribute value. The value of the attribute can typically be any C# expression that matches the type of the parameter. The value of the attribute can optionally lead with a [Razor reserved `@` symbol](../../mvc/views/razor?view=aspnetcore-10.0#razor-syntax), but it isn't required.

If the component parameter is of type string, then the attribute value is instead treated as a C# string literal. If you want to specify a C# expression instead, then use the `@` prefix.

The following parent component displays four instances of the preceding `ParameterChild` component and sets their `Title` parameter values to:

* The value of the `title` field.
* The result of the `GetTitle` C# method.
* The current local date in long format with [ToLongDateString](/en-us/dotnet/api/system.datetime.tolongdatestring), which uses an [implicit C# expression](../../mvc/views/razor?view=aspnetcore-10.0#implicit-razor-expressions).
* The `panelData` object's `Title` property.

The fifth `ParameterChild` component instance also sets the `Count` parameter. Note how a `string`-typed parameter requires an `@` prefix to ensure that an expression isn't treated as a string literal. However, `Count` is a nullable integer ([System.Int32](/en-us/dotnet/api/system.int32)), so `Count` can receive the value of `count` without an `@` prefix. You can establish an alternative code convention that requires developers in your organization to always prefix with `@`. Either way, we merely recommend that you adopt a consistent approach for how component parameters are passed in Razor markup.

Quotes around parameter attribute values are optional in most cases per the HTML5 specification. For example, `Value=this` is supported, instead of `Value="this"`. However, we recommend using quotes because it's easier to remember and widely adopted across web-based technologies.

Throughout the documentation, code examples:

* Always use quotes. Example: `Value="this"`.
* Don't use the `@` prefix with nonliterals unless required. Example: `Count="count"`, where `count` is a number-typed variable. `Count="@count"` is a valid stylistic approach, but the documentation and examples don't adopt the convention.
* Always avoid `@` for literals, outside of Razor expressions. Example: `IsFixed="true"`. This includes keywords (for example, `this`) and `null`, but you can choose to use them if you wish. For example, `IsFixed="@true"` is uncommon but supported.

`Parameter2.razor`:

```
@page "/parameter-2"

<PageTitle>Parameter 2</PageTitle>

<h1>Parameter Example 2</h1>

<ParameterChild Title="@title" />

<ParameterChild Title="@GetTitle()" />

<ParameterChild Title="@DateTime.Now.ToLongDateString()" />

<ParameterChild Title="@panelData.Title" />

<ParameterChild Title="String literal title" Count="count" />

@code {
    private string title = "From Parent field";
    private PanelData panelData = new();
    private int count = 12345;

    private string GetTitle() => "From Parent method";

    private class PanelData
    {
        public string Title { get; set; } = "From Parent object";
    }
}
```

`Parameter2.razor`:

```
@page "/parameter-2"

<PageTitle>Parameter 2</PageTitle>

<h1>Parameter Example 2</h1>

<ParameterChild Title="@title" />

<ParameterChild Title="@GetTitle()" />

<ParameterChild Title="@DateTime.Now.ToLongDateString()" />

<ParameterChild Title="@panelData.Title" />

<ParameterChild Title="String literal title" Count="count" />

@code {
    private string title = "From Parent field";
    private PanelData panelData = new();
    private int count = 12345;

    private string GetTitle() => "From Parent method";

    private class PanelData
    {
        public string Title { get; set; } = "From Parent object";
    }
}
```

`ParameterParent2.razor`:

```
@page "/parameter-parent-2"

<ParameterChild Title="@title" />

<ParameterChild Title="@GetTitle()" />

<ParameterChild Title="@DateTime.Now.ToLongDateString()" />

<ParameterChild Title="@panelData.Title" />

<ParameterChild Title="String literal title" Count="count" />

@code {
    private string title = "From Parent field";
    private PanelData panelData = new();
    private int count = 12345;

    private string GetTitle()
    {
        return "From Parent method";
    }

    private class PanelData
    {
        public string Title { get; set; } = "From Parent object";
    }
}
```

`ParameterParent2.razor`:

```
@page "/parameter-parent-2"

<ParameterChild Title="@title" />

<ParameterChild Title="@GetTitle()" />

<ParameterChild Title="@DateTime.Now.ToLongDateString()" />

<ParameterChild Title="@panelData.Title" />

<ParameterChild Title="String literal title" Count="count" />

@code {
    private string title = "From Parent field";
    private PanelData panelData = new();
    private int count = 12345;

    private string GetTitle()
    {
        return "From Parent method";
    }

    private class PanelData
    {
        public string Title { get; set; } = "From Parent object";
    }
}
```

`ParameterParent2.razor`:

```
@page "/parameter-parent-2"

<ParameterChild Title="@title" />

<ParameterChild Title="@GetTitle()" />

<ParameterChild Title="@DateTime.Now.ToLongDateString()" />

<ParameterChild Title="@panelData.Title" />

<ParameterChild Title="String literal title" Count="count" />

@code {
    private string title = "From Parent field";
    private PanelData panelData = new PanelData();
    private int count = 12345;

    private string GetTitle()
    {
        return "From Parent method";
    }

    private class PanelData
    {
        public string Title { get; set; } = "From Parent object";
    }
}
```

Note

When assigning a C# member to a component parameter, don't prefix the parameter's HTML attribute with `@`.

Correct (`Title` is a string parameter, `Count` is a number-typed parameter):

```
<ParameterChild Title="@title" Count="count" />
```

```
<ParameterChild Title="@title" Count="@count" />
```

Incorrect:

```
<ParameterChild @Title="@title" @Count="count" />
```

```
<ParameterChild @Title="@title" @Count="@count" />
```

Unlike in Razor pages (`.cshtml`), Blazor can't perform asynchronous work in a Razor expression while rendering a component. This is because Blazor is designed for rendering interactive UIs. In an interactive UI, the screen must always display something, so it doesn't make sense to block the rendering flow. Instead, asynchronous work is performed during one of the [asynchronous lifecycle events](lifecycle?view=aspnetcore-10.0). After each asynchronous lifecycle event, the component may render again. The following Razor syntax is **not** supported:

```
<ParameterChild Title="await ..." />
<ParameterChild Title="@await ..." />
```

The code in the preceding example generates a *compiler error* when the app is built:

> The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.

To obtain a value for the `Title` parameter in the preceding example asynchronously, the component can use the [`OnInitializedAsync` lifecycle event](lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync), as the following example demonstrates:

```
<ParameterChild Title="@title" />

@code {
    private string? title;
    
    protected override async Task OnInitializedAsync()
    {
        title = await ...;
    }
}
```

For more information, see [ASP.NET Core Razor component lifecycle](lifecycle?view=aspnetcore-10.0).

Use of an explicit Razor expression to concatenate text with an expression result for assignment to a parameter is **not** supported. The following example seeks to concatenate the text "`Set by `" with an object's property value. Although this syntax is supported in a Razor page (`.cshtml`), it isn't valid for assignment to the child's `Title` parameter in a component. The following Razor syntax is **not** supported:

```
<ParameterChild Title="Set by @(panelData.Title)" />
```

The code in the preceding example generates a *compiler error* when the app is built:

> Component attributes do not support complex content (mixed C# and markup).

To support the assignment of a composed value, use a method, field, or property. The following example performs the concatenation of "`Set by `" and an object's property value in the C# method `GetTitle`:

`Parameter3.razor`:

```
@page "/parameter-3"

<PageTitle>Parameter 3</PageTitle>

<h1>Parameter Example 3</h1>

<ParameterChild Title="@GetTitle()" />

@code {
    private PanelData panelData = new();

    private string GetTitle() => $"Set by {panelData.Title}";

    private class PanelData
    {
        public string Title { get; set; } = "Parent";
    }
}
```

`Parameter3.razor`:

```
@page "/parameter-3"

<PageTitle>Parameter 3</PageTitle>

<h1>Parameter Example 3</h1>

<ParameterChild Title="@GetTitle()" />

@code {
    private PanelData panelData = new();

    private string GetTitle() => $"Set by {panelData.Title}";

    private class PanelData
    {
        public string Title { get; set; } = "Parent";
    }
}
```

`ParameterParent3.razor`:

```
@page "/parameter-parent-3"

<ParameterChild Title="@GetTitle()" />

@code {
    private PanelData panelData = new();

    private string GetTitle() => $"Set by {panelData.Title}";

    private class PanelData
    {
        public string Title { get; set; } = "Parent";
    }
}
```

`ParameterParent3.razor`:

```
@page "/parameter-parent-3"

<ParameterChild Title="@GetTitle()" />

@code {
    private PanelData panelData = new();

    private string GetTitle() => $"Set by {panelData.Title}";

    private class PanelData
    {
        public string Title { get; set; } = "Parent";
    }
}
```

`ParameterParent3.razor`:

```
@page "/parameter-parent-3"

<ParameterChild Title="@GetTitle()" />

@code {
    private PanelData panelData = new();

    private string GetTitle() => $"Set by {panelData.Title}";

    private class PanelData
    {
        public string Title { get; set; } = "Parent";
    }
}
```

`ParameterParent3.razor`:

```
@page "/parameter-parent-3"

<ParameterChild Title="@GetTitle()" />

@code {
    private PanelData panelData = new PanelData();

    private string GetTitle() => $"Set by {panelData.Title}";

    private class PanelData
    {
        public string Title { get; set; } = "Parent";
    }
}
```

For more information, see [Razor syntax reference for ASP.NET Core](../../mvc/views/razor?view=aspnetcore-10.0).

Warning

Providing initial values for component parameters is supported, but don't create a component that writes to its own parameters after the component is rendered for the first time. For more information, see [Avoid overwriting parameters in ASP.NET Core Blazor](overwriting-parameters?view=aspnetcore-10.0).

Component parameters should be declared as [automatically-implemented properties (*auto properties*)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties), meaning that they shouldn't contain custom logic in their `get` or `set` accessors. For example, the following `StartData` property is an auto property:

```
[Parameter]
public DateTime StartData { get; set; }
```

Don't place custom logic in the `get` or `set` accessor because component parameters are purely intended for use as a channel for a parent component to flow information to a child component. If a `set` accessor of a child component property contains logic that causes rerendering of the parent component, an infinite rendering loop results. Other side effects include unexpected extra renderings and parameter value overwrites.

To transform a received parameter value:

* Leave the parameter property as an auto-property to represent the supplied raw data.
* Create a different property or method to supply the transformed data based on the parameter property.

Override [`OnParametersSetAsync`](lifecycle?view=aspnetcore-10.0#after-parameters-are-set-onparameterssetasync) to transform a received parameter each time new data is received.

Writing an initial value to a component parameter is supported because initial value assignments don't interfere with the Blazor's automatic component rendering. The following assignment of the current local [DateTime](/en-us/dotnet/api/system.datetime) with [DateTime.Now](/en-us/dotnet/api/system.datetime.now#system-datetime-now) to `StartData` is valid syntax in a component:

```
[Parameter]
public DateTime StartData { get; set; } = DateTime.Now;
```

After the initial assignment of [DateTime.Now](/en-us/dotnet/api/system.datetime.now#system-datetime-now), do **not** assign a value to `StartData` in developer code. For more information, see [Avoid overwriting parameters in ASP.NET Core Blazor](overwriting-parameters?view=aspnetcore-10.0).

Apply the [`[EditorRequired]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.editorrequiredattribute) to specify a required component parameter. If a parameter value isn't provided, editors or build tools may display warnings to the user. This attribute is only valid on properties also marked with the [`[Parameter]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute). The [EditorRequiredAttribute](/en-us/dotnet/api/microsoft.aspnetcore.components.editorrequiredattribute) is enforced at design-time and when the app is built. The attribute isn't enforced at runtime, and it doesn't guarantee a non-`null` parameter value.

```
[Parameter]
[EditorRequired]
public string? Title { get; set; }
```

Single-line attribute lists are also supported:

```
[Parameter, EditorRequired]
public string? Title { get; set; }
```

Don't use the [`required` modifier](/en-us/dotnet/csharp/language-reference/keywords/required) or [`init` accessor](/en-us/dotnet/csharp/language-reference/keywords/init) on component parameter properties. Components are usually instantiated and assigned parameter values using [reflection](/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/), which bypasses the guarantees that `init` and `required` are designed to make. Instead, use the [`[EditorRequired]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.editorrequiredattribute) to specify a required component parameter.

Don't use the [`init` accessor](/en-us/dotnet/csharp/language-reference/keywords/init) on component parameter properties because setting component parameter values with [ParameterView.SetParameterProperties](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview.setparameterproperties) uses [reflection](/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/), which bypasses the init-only setter restriction. Use the [`[EditorRequired]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.editorrequiredattribute) to specify a required component parameter.

Don't use the [`init` accessor](/en-us/dotnet/csharp/language-reference/keywords/init) on component parameter properties because setting component parameter values with [ParameterView.SetParameterProperties](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview.setparameterproperties) uses [reflection](/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/), which bypasses the init-only setter restriction.

[`Tuples`](/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples) ([API documentation](/en-us/dotnet/api/system.tuple)) are supported for component parameters and [`RenderFragment`](#child-content-render-fragments) types. The following component parameter example passes three values in a `Tuple`:

`RenderTupleChild.razor`:

```
<div class="card w-50" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Tuple Card</div>
    <div class="card-body">
        <ul>
            <li>Integer: @Data?.Item1</li>
            <li>String: @Data?.Item2</li>
            <li>Boolean: @Data?.Item3</li>
        </ul>
    </div>
</div>

@code {
    [Parameter]
    public (int, string, bool)? Data { get; set; }
}
```

`RenderTupleParent.razor`:

```
@page "/render-tuple-parent"

<PageTitle>Render Tuple Parent</PageTitle>

<h1>Render Tuple Parent Example</h1>

<RenderTupleChild Data="data" />

@code {
    private (int, string, bool) data = new(999, "I aim to misbehave.", true);
}
```

[Named tuples](/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples#tuple-field-names) are supported, as seen in the following example:

`NamedTupleChild.razor`:

```
<div class="card w-50" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Tuple Card</div>
    <div class="card-body">
        <ul>
            <li>Integer: @Data?.TheInteger</li>
            <li>String: @Data?.TheString</li>
            <li>Boolean: @Data?.TheBoolean</li>
        </ul>
    </div>
</div>

@code {
    [Parameter]
    public (int TheInteger, string TheString, bool TheBoolean)? Data { get; set; }
}
```

`NamedTuples.razor`:

```
@page "/named-tuples"

<PageTitle>Named Tuples</PageTitle>

<h1>Named Tuples Example</h1>

<NamedTupleChild Data="data" />

@code {
    private (int TheInteger, string TheString, bool TheBoolean) data = 
        new(999, "I aim to misbehave.", true);
}
```

Quote ©2005 [Universal Pictures](https://www.uphe.com): [Serenity](https://www.uphe.com/movies/serenity-2005) ([Nathan Fillion](https://www.imdb.com/name/nm0277213/))

[`Tuples`](/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples) ([API documentation](/en-us/dotnet/api/system.tuple)) are supported for component parameters and [`RenderFragment`](#child-content-render-fragments) types. The following component parameter example passes three values in a `Tuple`:

`RenderTupleChild.razor`:

```
<div class="card w-50" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Tuple Card</div>
    <div class="card-body">
        <ul>
            <li>Integer: @Data?.Item1</li>
            <li>String: @Data?.Item2</li>
            <li>Boolean: @Data?.Item3</li>
        </ul>
    </div>
</div>

@code {
    [Parameter]
    public (int, string, bool)? Data { get; set; }
}
```

`RenderTupleParent.razor`:

```
@page "/render-tuple-parent"

<PageTitle>Render Tuple Parent</PageTitle>

<h1>Render Tuple Parent Example</h1>

<RenderTupleChild Data="data" />

@code {
    private (int, string, bool) data = new(999, "I aim to misbehave.", true);
}
```

[Named tuples](/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples#tuple-field-names) are supported, as seen in the following example:

`NamedTupleChild.razor`:

```
<div class="card w-50" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Tuple Card</div>
    <div class="card-body">
        <ul>
            <li>Integer: @Data?.TheInteger</li>
            <li>String: @Data?.TheString</li>
            <li>Boolean: @Data?.TheBoolean</li>
        </ul>
    </div>
</div>

@code {
    [Parameter]
    public (int TheInteger, string TheString, bool TheBoolean)? Data { get; set; }
}
```

`NamedTuples.razor`:

```
@page "/named-tuples"

<PageTitle>Named Tuples</PageTitle>

<h1>Named Tuples Example</h1>

<NamedTupleChild Data="data" />

@code {
    private (int TheInteger, string TheString, bool TheBoolean) data = 
        new(999, "I aim to misbehave.", true);
}
```

Quote ©2005 [Universal Pictures](https://www.uphe.com): [Serenity](https://www.uphe.com/movies/serenity-2005) ([Nathan Fillion](https://www.imdb.com/name/nm0277213/))

[`Tuples`](/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples) ([API documentation](/en-us/dotnet/api/system.tuple)) are supported for component parameters and [`RenderFragment`](#child-content-render-fragments) types. The following component parameter example passes three values in a `Tuple`:

`RenderTupleChild.razor`:

```
<div class="card w-50" style="margin-bottom:15px">
    <div class="card-header font-weight-bold"><code>Tuple</code> Card</div>
    <div class="card-body">
        <ul>
            <li>Integer: @Data?.Item1</li>
            <li>String: @Data?.Item2</li>
            <li>Boolean: @Data?.Item3</li>
        </ul>
    </div>
</div>

@code {
    [Parameter]
    public (int, string, bool)? Data { get; set; }
}
```

`RenderTupleParent.razor`:

```
@page "/render-tuple-parent"

<h1>Render Tuple Parent</h1>

<RenderTupleChild Data="data" />

@code {
    private (int, string, bool) data = new(999, "I aim to misbehave.", true);
}
```

[Named tuples](/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples#tuple-field-names) are supported, as seen in the following example:

`RenderNamedTupleChild.razor`:

```
<div class="card w-50" style="margin-bottom:15px">
    <div class="card-header font-weight-bold"><code>Tuple</code> Card</div>
    <div class="card-body">
        <ul>
            <li>Integer: @Data?.TheInteger</li>
            <li>String: @Data?.TheString</li>
            <li>Boolean: @Data?.TheBoolean</li>
        </ul>
    </div>
</div>

@code {
    [Parameter]
    public (int TheInteger, string TheString, bool TheBoolean)? Data { get; set; }
}
```

`RenderNamedTupleParent.razor`:

```
@page "/render-named-tuple-parent"

<h1>Render Named Tuple Parent</h1>

<RenderNamedTupleChild Data="data" />

@code {
    private (int TheInteger, string TheString, bool TheBoolean) data = 
        new(999, "I aim to misbehave.", true);
}
```

Quote ©2005 [Universal Pictures](https://www.uphe.com): [Serenity](https://www.uphe.com/movies/serenity-2005) ([Nathan Fillion](https://www.imdb.com/name/nm0277213/))

Support for [C# union types](/en-us/dotnet/csharp/language-reference/builtin-types/union) allows a single component parameter to represent a value that's exactly one of a fixed set of types with compiler-enforced exhaustive pattern matching. A component parameter is set by direct assignment when a component is rendered from Razor markup or through [RenderTreeBuilder.AddComponentParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder.addcomponentparameter).

Note

During the preview of .NET 11, the following example requires the app's project file to specify the "`preview`" C# language version:

```
<LangVersion>preview</LangVersion>
```

The following C# union type, `SlotContent`, accepts text, a [MarkupString](/en-us/dotnet/api/microsoft.aspnetcore.components.markupstring), or a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment):

```
using Microsoft.AspNetCore.Components;

public union SlotContent(string, MarkupString, RenderFragment);
```

The following `Slot` component exposes a component parameter typed to the `SlotContent` union and renders the union's content using two implementations of the [C# `switch` expression](/en-us/dotnet/csharp/language-reference/operators/switch-expression), one through an assignment to a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) (`ContentSwitch`) and one via an inline `switch` expression. In the following example, the [`[EditorRequired]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.editorrequiredattribute) specifies that `Content` is a required component parameter at design-time or build-time.

`Slot.razor`:

```
<h2><code>ContentSwitch</code></h2>

<div>
    @ContentSwitch()
</div>

<h2>Inline <code>switch</code> expression</h2>

<div>
    @switch (Content)
    {
        case string text:
            @text
            break;
        case MarkupString html:
            @html
            break;
        case RenderFragment fragment:
            @fragment
            break;
        default:
            <em>Empty slot!</em>
            break;
    }
</div>

@code {
    [Parameter, EditorRequired] 
    public SlotContent Content { get; set; }

    private RenderFragment ContentSwitch() => Content switch
    {
        string text => @<span>@text</span>,
        MarkupString html => @<span>@html</span>,
        RenderFragment fragment => fragment,
        _ => @<em>Empty slot!</em>
    };
}
```

The following `SlotExample` component uses the preceding `SlotContent` type and `Slot` component.

`SlotExample.razor`:

```
@page "/slot-example"

<h1>Plain text</h1>

<Slot Content="@("Simple plain text slot")" />

<h1><code>MarkupString</code></h1>

<Slot Content='@((MarkupString)"<strong>Bold HTML slot</strong>")' />

<h1><code>RenderFragment</code></h1>

<Slot Content="@content" />

@code {
    private SlotContent content;

    protected override void OnInitialized()
    {
        content = new SlotContent((RenderFragment)(b =>
        {
            b.OpenElement(0, "button");
            b.AddAttribute(1, "onclick", EventCallback.Factory.Create(this, Increment));
            b.AddContent(2, "Count: ");
            b.AddContent(3, currentCount);
            b.CloseElement();
        }));
    }

    private int currentCount = 0;

    private void Increment() => currentCount++;
}
```

The string-literal shortcut applies only to parameters declared as `string`. A parameter declared as a C# union type, even one whose cases include `string`, isn't a `string`-typed parameter, so the attribute value must be a C# expression. Use the `@` prefix, as demonstrated by `Content="@("Simple plain text slot")"` in the preceding example.

Populating a union-typed parameter via a [child content render fragment](#child-content-render-fragments) (`<Slot>...</Slot>`) isn't supported at this time but might be introduced in a future preview release.

For more information, see [Explore union types in C# 15](https://devblogs.microsoft.com/dotnet/csharp-15-union-types/).

## Route parameters

Components can specify route parameters in the route template of the [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive. The [Blazor router](../fundamentals/routing?view=aspnetcore-10.0) uses route parameters to populate corresponding component parameters.

`RouteParameter1.razor`:

```
@page "/route-parameter-1/{text}"

<PageTitle>Route Parameter 1</PageTitle>

<h1>Route Parameter Example 1</h1>

<p>Blazor is @Text!</p>

@code {
    [Parameter]
    public string? Text { get; set; }
}
```

```
@page "/route-parameter-1/{text}"

<PageTitle>Route Parameter 1</PageTitle>

<h1>Route Parameter Example 1</h1>

<p>Blazor is @Text!</p>

@code {
    [Parameter]
    public string? Text { get; set; }
}
```

```
@page "/route-parameter-1/{text}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string? Text { get; set; }
}
```

```
@page "/route-parameter-1/{text}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string? Text { get; set; }
}
```

```
@page "/route-parameter-1/{text}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string Text { get; set; }
}
```

```
@page "/route-parameter-1/{text}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string Text { get; set; }
}
```

For more information, see the *Route parameters* section of [ASP.NET Core Blazor routing](../fundamentals/routing?view=aspnetcore-10.0#route-parameters). Optional route parameters are also supported and covered in the same section. For information on catch-all route parameters (`{*pageRoute}`), which capture paths across multiple folder boundaries, see the *Catch-all route parameters* section of [ASP.NET Core Blazor routing](../fundamentals/routing?view=aspnetcore-10.0#catch-all-route-parameters).

For more information, see the *Route parameters* section of [ASP.NET Core Blazor routing](../fundamentals/routing?view=aspnetcore-10.0#route-parameters). Optional route parameters aren't supported, so two [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directives are required (see the *Route parameters* section for more information). For information on catch-all route parameters (`{*pageRoute}`), which capture paths across multiple folder boundaries, see the *Catch-all route parameters* section of [ASP.NET Core Blazor routing](../fundamentals/routing?view=aspnetcore-10.0#catch-all-route-parameters).

Warning

With compression, which is enabled by default, avoid creating secure (authenticated/authorized) interactive server-side components that render data from untrusted sources. Untrusted sources include route parameters, query strings, data from JS interop, and any other source of data that a third-party user can control (databases, external services). For more information, see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#websocket-compression-for-interactive-server-components) and [Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering](../security/interactive-server-side-rendering?view=aspnetcore-9.0#interactive-server-components-with-websocket-compression-enabled).

## Child content render fragments

Components can set the content of another component. The assigning component provides the content between the child component's opening and closing tags.

In the following example, the `RenderFragmentChild` component has a `ChildContent` component parameter that represents a segment of the UI to render as a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment). The position of `ChildContent` in the component's Razor markup is where the content is rendered in the final HTML output.

`RenderFragmentChild.razor`:

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Child content</div>
    <div class="card-body">@ChildContent</div>
</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Child content</div>
    <div class="card-body">@ChildContent</div>
</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Child content</div>
    <div class="card-body">@ChildContent</div>
</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Child content</div>
    <div class="card-body">@ChildContent</div>
</div>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Child content</div>
    <div class="card-body">@ChildContent</div>
</div>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }
}
```

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Child content</div>
    <div class="card-body">@ChildContent</div>
</div>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }
}
```

Important

The property receiving the [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) content must be named `ChildContent` by convention.

[Event callbacks](event-handling?view=aspnetcore-10.0#eventcallback) aren't supported for [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment).

The following component provides content for rendering the `RenderFragmentChild` by placing the content inside the child component's opening and closing tags.

`RenderFragments.razor`:

```
@page "/render-fragments"

<PageTitle>Render Fragments</PageTitle>

<h1>Render Fragments Example</h1>

<RenderFragmentChild>
    Content of the child component is supplied
    by the parent component.
</RenderFragmentChild>
```

`RenderFragments.razor`:

```
@page "/render-fragments"

<PageTitle>Render Fragments</PageTitle>

<h1>Render Fragments Example</h1>

<RenderFragmentChild>
    Content of the child component is supplied
    by the parent component.
</RenderFragmentChild>
```

`RenderFragmentParent.razor`:

```
@page "/render-fragment-parent"

<h1>Render child content</h1>

<RenderFragmentChild>
    Content of the child component is supplied
    by the parent component.
</RenderFragmentChild>
```

`RenderFragmentParent.razor`:

```
@page "/render-fragment-parent"

<h1>Render child content</h1>

<RenderFragmentChild>
    Content of the child component is supplied
    by the parent component.
</RenderFragmentChild>
```

`RenderFragmentParent.razor`:

```
@page "/render-fragment-parent"

<h1>Render child content</h1>

<RenderFragmentChild>
    Content of the child component is supplied
    by the parent component.
</RenderFragmentChild>
```

`RenderFragmentParent.razor`:

```
@page "/render-fragment-parent"

<h1>Render child content</h1>

<RenderFragmentChild>
    Content of the child component is supplied
    by the parent component.
</RenderFragmentChild>
```

Render fragments are used to render child content throughout Blazor apps and are described with examples in the following articles and article sections:

* [Blazor layouts](layouts?view=aspnetcore-10.0)
* [Pass data across a component hierarchy](cascading-values-and-parameters?view=aspnetcore-10.0#pass-data-across-a-component-hierarchy)
* [Templated components](templated-components?view=aspnetcore-10.0)
* [Global exception handling](../fundamentals/handle-errors?view=aspnetcore-10.0#global-exception-handling)

Note

Blazor framework's [built-in Razor components](built-in-components?view=aspnetcore-10.0) use the same `ChildContent` component parameter convention to set their content. You can see the components that set child content by searching for the component parameter property name `ChildContent` in the [API documentation (filters API with the search term "ChildContent")](/en-us/dotnet/api/?term=ChildContent).

## Render fragments for reusable rendering logic

You can factor out child components purely as a way of reusing rendering logic. In any component's `@code` block, define a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) and render the fragment from any location as many times as needed:

```
@RenderWelcomeInfo

<p>Render the welcome info a second time:</p>

@RenderWelcomeInfo

@code {
    private RenderFragment RenderWelcomeInfo =  @<p>Welcome to your new app!</p>;
}
```

For more information, see [Reuse rendering logic](../performance/rendering?view=aspnetcore-10.0#define-reusable-renderfragments-in-code).

## Loop variables with component parameters and child content

Rendering components inside a [`for`](/en-us/dotnet/csharp/language-reference/keywords/for) loop requires a local index variable if the incrementing loop variable is used by the component's parameters or [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) child content.

Consider the following `RenderFragmentChild2` component that has both a component parameter (`Id`) and a render fragment to display child content (`ChildContent`).

`RenderFragmentChild2.razor`:

```
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Child content (@Id)</div>
    <div class="card-body">@ChildContent</div>
</div>

@code {
    [Parameter]
    public string? Id { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

When rendering the `RenderFragmentChild2` component in a parent component, use a local index variable (`ct` in the following example) instead of the loop variable (`c`) when assigning the component parameter value and providing the child component's content:

```
@for (int c = 1; c < 4; c++)
{
    var ct = c;

    <RenderFragmentChild2 Id="@($"Child{ct}")">
        Count: @ct
    </RenderFragmentChild2>
}
```

Alternatively, use a [`foreach`](/en-us/dotnet/csharp/language-reference/keywords/foreach-in) loop with [Enumerable.Range](/en-us/dotnet/api/system.linq.enumerable.range) instead of a [`for`](/en-us/dotnet/csharp/language-reference/keywords/for) loop:

```
@foreach (var c in Enumerable.Range(1, 3))
{
    <RenderFragmentChild2 Id="@($"Child{c}")">
        Count: @c
    </RenderFragmentChild2>
}
```

## Capture references to components

Component references provide a way to reference a component instance for issuing commands. To capture a component reference:

* Add an [`@ref`](../../mvc/views/razor?view=aspnetcore-10.0#ref) attribute to the child component.
* Define a field with the same type as the child component.

When the component is rendered, the field is populated with the component instance. You can then invoke .NET methods on the instance.

Consider the following `ReferenceChild` component that logs a message when its `ChildMethod` is called.

`ReferenceChild.razor`:

```
@inject ILogger<ReferenceChild> Logger

@if (value > 0)
{
    <p>
        <code>value</code>: @value
    </p>
}

@code {
    private int value;

    public void ChildMethod(int value)
    {
        Logger.LogInformation("Received {Value} in ChildMethod", value);

        this.value = value;
        StateHasChanged();
    }
}
```

```
@inject ILogger<ReferenceChild> Logger

@if (value > 0)
{
    <p>
        <code>value</code>: @value
    </p>
}

@code {
    private int value;

    public void ChildMethod(int value)
    {
        Logger.LogInformation("Received {Value} in ChildMethod", value);

        this.value = value;
        StateHasChanged();
    }
}
```

```
@using Microsoft.Extensions.Logging
@inject ILogger<ReferenceChild> Logger

@code {
    public void ChildMethod(int value)
    {
        Logger.LogInformation("Received {Value} in ChildMethod", value);
    }
}
```

```
@using Microsoft.Extensions.Logging
@inject ILogger<ReferenceChild> Logger

@code {
    public void ChildMethod(int value)
    {
        Logger.LogInformation("Received {Value} in ChildMethod", value);
    }
}
```

```
@using Microsoft.Extensions.Logging
@inject ILogger<ReferenceChild> Logger

@code {
    public void ChildMethod(int value)
    {
        Logger.LogInformation("Received {Value} in ChildMethod", value);
    }
}
```

```
@using Microsoft.Extensions.Logging
@inject ILogger<ReferenceChild> Logger

@code {
    public void ChildMethod(int value)
    {
        Logger.LogInformation("Received {Value} in ChildMethod", value);
    }
}
```

A component reference is only populated after the component is rendered and its output includes `ReferenceChild`'s element. Until the component is rendered, there's nothing to reference. Don't attempt to call a referenced component method to an event handler directly (for example, `@onclick="childComponent!.ChildMethod(5)"`) because the reference variable may not be assigned at the time the click event is assigned.

To manipulate component references after the component has finished rendering, use the [`OnAfterRender` or `OnAfterRenderAsync` methods](lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync).

The following example uses the preceding `ReferenceChild` component.

`ReferenceParent.razor`:

```
@page "/reference-parent"

<div>
    <button @onclick="@(() => childComponent1!.ChildMethod(5))">
        Call <code>ReferenceChild.ChildMethod</code> (first instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent1" />
</div>

<div>
    <button @onclick="CallChildMethod">
        Call <code>ReferenceChild.ChildMethod</code> (second instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent2" />
</div>

@code {
    private ReferenceChild? childComponent1;
    private ReferenceChild? childComponent2;

    private void CallChildMethod() => childComponent2!.ChildMethod(5);
}
```

```
@page "/reference-parent"

<div>
    <button @onclick="@(() => childComponent1!.ChildMethod(5))">
        Call <code>ReferenceChild.ChildMethod</code> (first instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent1" />
</div>

<div>
    <button @onclick="CallChildMethod">
        Call <code>ReferenceChild.ChildMethod</code> (second instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent2" />
</div>

@code {
    private ReferenceChild? childComponent1;
    private ReferenceChild? childComponent2;

    private void CallChildMethod() => childComponent2!.ChildMethod(5);
}
```

```
@page "/reference-parent"

<div>
    <button @onclick="@(() => childComponent1!.ChildMethod(5))">
        Call <code>ReferenceChild.ChildMethod</code> (first instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent1" />
</div>

<div>
    <button @onclick="CallChildMethod">
        Call <code>ReferenceChild.ChildMethod</code> (second instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent2" />
</div>

@code {
    private ReferenceChild? childComponent1;
    private ReferenceChild? childComponent2;

    private void CallChildMethod()
    {
        childComponent2!.ChildMethod(5);
    }
}
```

```
@page "/reference-parent"

<div>
    <button @onclick="@(() => childComponent1!.ChildMethod(5))">
        Call <code>ReferenceChild.ChildMethod</code> (first instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent1" />
</div>

<div>
    <button @onclick="CallChildMethod">
        Call <code>ReferenceChild.ChildMethod</code> (second instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent2" />
</div>

@code {
    private ReferenceChild? childComponent1;
    private ReferenceChild? childComponent2;

    private void CallChildMethod()
    {
        childComponent2!.ChildMethod(5);
    }
}
```

```
@page "/reference-parent"

<div>
    <button @onclick="@(() => childComponent1!.ChildMethod(5))">
        Call <code>ReferenceChild.ChildMethod</code> (first instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent1" />
</div>

<div>
    <button @onclick="CallChildMethod">
        Call <code>ReferenceChild.ChildMethod</code> (second instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent2" />
</div>

@code {
    private ReferenceChild childComponent1;
    private ReferenceChild childComponent2;

    private void CallChildMethod()
    {
        childComponent2!.ChildMethod(5);
    }
}
```

```
@page "/reference-parent"

<div>
    <button @onclick="@(() => childComponent1!.ChildMethod(5))">
        Call <code>ReferenceChild.ChildMethod</code> (first instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent1" />
</div>

<div>
    <button @onclick="CallChildMethod">
        Call <code>ReferenceChild.ChildMethod</code> (second instance) 
        with an argument of 5
    </button>

    <ReferenceChild @ref="childComponent2" />
</div>

@code {
    private ReferenceChild childComponent1;
    private ReferenceChild childComponent2;

    private void CallChildMethod()
    {
        childComponent2!.ChildMethod(5);
    }
}
```

While capturing component references use a similar syntax to [capturing element references](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#capture-references-to-elements), capturing component references isn't a JavaScript interop feature. Component references aren't passed to JavaScript code. Component references are only used in .NET code.

Important

Do **not** use component references to mutate the state of child components. Instead, use normal declarative component parameters to pass data to child components. Use of component parameters result in child components that rerender at the correct times automatically. For more information, see the [component parameters](#component-parameters) section and the [ASP.NET Core Blazor data binding](data-binding?view=aspnetcore-10.0) article.

## Apply an attribute

Attributes can be applied to components with the [`@attribute`](../../mvc/views/razor?view=aspnetcore-10.0#attribute) directive. The following example applies the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the component's class:

```
@page "/"
@attribute [Authorize]
```

## Conditional HTML element attributes and DOM properties

Blazor adopts the following general behaviors:

* For HTML attributes, Blazor sets or removes the attribute conditionally based on the .NET value. If the .NET value is `false` or `null`, the attribute isn't set or is removed if it was previously set.
* For DOM properties, such as `checked` or `value`, Blazor sets the DOM property based on the .NET value. If the .NET value is `false` or `null`, the DOM property is reset to a default value.

Which Razor syntax attributes correspond to HTML attributes and which ones correspond to DOM properties remains undocumented because this is a framework implementation detail that might change without notice.

Warning

Some HTML attributes, such as [`aria-pressed`](https://developer.mozilla.org/docs/Web/Accessibility/ARIA/Roles/button_role#Toggle_buttons), must have a string value of either "true" or "false". Since they require a string value and not a boolean, you must use a .NET `string` and not a `bool` for their value. This is a requirement set by browser DOM APIs.

## Raw HTML

Strings are normally rendered using DOM text nodes, which means that any markup they may contain is ignored and treated as literal text. To render raw HTML, wrap the HTML content in a [MarkupString](/en-us/dotnet/api/microsoft.aspnetcore.components.markupstring) value. The value is parsed as HTML or SVG and inserted into the DOM.

Warning

Rendering raw HTML constructed from any untrusted source is a **security risk** and should **always** be avoided.

The following example shows using the [MarkupString](/en-us/dotnet/api/microsoft.aspnetcore.components.markupstring) type to add a block of static HTML content to the rendered output of a component.

`MarkupStrings.razor`:

```
@page "/markup-strings"

<PageTitle>Markup Strings</PageTitle>

<h1>Markup Strings Example</h1>

@((MarkupString)myMarkup)

@code {
    private string myMarkup =
        "<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";
}
```

`MarkupStrings.razor`:

```
@page "/markup-strings"

<PageTitle>Markup Strings</PageTitle>

<h1>Markup Strings Example</h1>

@((MarkupString)myMarkup)

@code {
    private string myMarkup =
        "<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";
}
```

`MarkupStringExample.razor`:

```
@page "/markup-string-example"

@((MarkupString)myMarkup)

@code {
    private string myMarkup =
        "<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";
}
```

`MarkupStringExample.razor`:

```
@page "/markup-string-example"

@((MarkupString)myMarkup)

@code {
    private string myMarkup =
        "<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";
}
```

`MarkupStringExample.razor`:

```
@page "/markup-string-example"

@((MarkupString)myMarkup)

@code {
    private string myMarkup =
        "<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";
}
```

`MarkupStringExample.razor`:

```
@page "/markup-string-example"

@((MarkupString)myMarkup)

@code {
    private string myMarkup =
        "<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";
}
```

## Razor templates

Render fragments can be defined using Razor template syntax to define a UI snippet. Razor templates use the following format:

```
@<{HTML tag}>...</{HTML tag}>
```

The following example illustrates how to specify [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) and [RenderFragment\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment-1) values and render templates directly in a component. Render fragments can also be passed as arguments to [templated components](templated-components?view=aspnetcore-10.0).

`RazorTemplate.razor`:

```
@page "/razor-template"

<PageTitle>Razor Template</PageTitle>

<h1>Razor Template Example</h1>

@timeTemplate

@petTemplate(new Pet { Name = "Nutty Rex" })

@code {
    private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
    private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;

    private class Pet
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/razor-template"

<PageTitle>Razor Template</PageTitle>

<h1>Razor Template Example</h1>

@timeTemplate

@petTemplate(new Pet { Name = "Nutty Rex" })

@code {
    private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
    private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;

    private class Pet
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/razor-template"

@timeTemplate

@petTemplate(new Pet { Name = "Nutty Rex" })

@code {
    private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
    private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;

    private class Pet
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/razor-template"

@timeTemplate

@petTemplate(new Pet { Name = "Nutty Rex" })

@code {
    private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
    private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;

    private class Pet
    {
        public string? Name { get; set; }
    }
}
```

```
@page "/razor-template"

@timeTemplate

@petTemplate(new Pet { Name = "Nutty Rex" })

@code {
    private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
    private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;

    private class Pet
    {
        public string Name { get; set; }
    }
}
```

```
@page "/razor-template"

@timeTemplate

@petTemplate(new Pet { Name = "Nutty Rex" })

@code {
    private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
    private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;

    private class Pet
    {
        public string Name { get; set; }
    }
}
```

Rendered output of the preceding code:

```
<p>The time is 4/19/2021 8:54:46 AM.</p>
<p>Pet: Nutty Rex</p>
```

When the Razor delegate must return more than one HTML element, wrap the result in a `<text>` tag for an [explicit delimited transition](../../mvc/views/razor?view=aspnetcore-10.0#explicit-delimited-transition):

```
@RenderTwoElements()

<h2>ReturnIf</h2>
@ReturnIf()

<h2>ReturnForeach</h2>
@ReturnForeach()

@code {
    private bool showTrueStatement = true;

    private RenderFragment RenderTwoElements() =>
        @<text>
            <h2>Render Two Elements</h2>
            @ChildFragment
        </text>;

    private RenderFragment ChildFragment => @<p>This is a paragraph.</p>;

    private RenderFragment ReturnIf() =>
        @<text>
            @if (showTrueStatement)
            {
                <p>This is true!</p>
            }
            else
            {
                <p>This is false!</p>
            }
        </text>;

    private RenderFragment ReturnForeach() =>
        @<text>
            @foreach (var item in new[] { 1, 2, 3 })
            {
                <p>Item: @item</p>
            }
        </text>;
}
```

## Static assets

Blazor follows the convention of ASP.NET Core apps for static assets. Static assets are located in the project's [`web root` (`wwwroot`) folder](../../fundamentals/?view=aspnetcore-10.0#web-root) or folders under the `wwwroot` folder.

Use a base-relative path (`/`) to refer to the web root for a static asset. In the following example, `logo.png` is physically located in the `{PROJECT ROOT}/wwwroot/images` folder. `{PROJECT ROOT}` is the app's project root.

```
<img alt="Company logo" src="/images/logo.png" />
```

Components do **not** support tilde-slash notation (`~/`).

For information on setting an app's base path, see [ASP.NET Core Blazor app base path](../host-and-deploy/app-base-path?view=aspnetcore-10.0).

## Tag Helpers aren't supported in components

[`Tag Helpers`](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) aren't supported in components. To provide Tag Helper-like functionality in Blazor, create a component with the same functionality as the Tag Helper and use the component instead.

## Scalable Vector Graphics (SVG) images

Since Blazor renders HTML, browser-supported images, including [Scalable Vector Graphics (SVG) images (`.svg`)](https://developer.mozilla.org/docs/Web/SVG), are supported via the `<img>` tag:

```
<img alt="Example image" src="image.svg" />
```

Similarly, SVG images are supported in the CSS rules of a stylesheet file (`.css`):

```
.element-class {
    background-image: url("image.svg");
}
```

Blazor supports the [`<foreignObject>`](https://developer.mozilla.org/docs/Web/SVG/Element/foreignObject) element to display arbitrary HTML within an SVG. The markup can represent arbitrary HTML, a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), or a Razor component.

The following example demonstrates:

* Display of a `string` (`@message`).
* Two-way binding with an `<input>` element and a `value` field.
* A `Robot` component.

```
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
    <rect x="0" y="0" rx="10" ry="10" width="200" height="200" stroke="black" 
        fill="none" />
    <foreignObject x="20" y="20" width="160" height="160">
        <p>@message</p>
    </foreignObject>
</svg>

<svg xmlns="http://www.w3.org/2000/svg">
    <foreignObject width="200" height="200">
        <label>
            Two-way binding:
            <input @bind="value" @bind:event="oninput" />
        </label>
    </foreignObject>
</svg>

<svg xmlns="http://www.w3.org/2000/svg">
    <foreignObject>
        <Robot />
    </foreignObject>
</svg>

@code {
    private string message = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
        "elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";

    private string? value;
}
```

## Whitespace rendering behavior

Unless the [`@preservewhitespace`](../../mvc/views/razor?view=aspnetcore-10.0#preservewhitespace) directive is used with a value of `true`, extra whitespace is removed if:

* Leading or trailing within an element.
* Leading or trailing within a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment)/[RenderFragment\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment-1) parameter (for example, child content passed to another component).
* It precedes or follows a C# code block, such as `@if` or `@foreach`.

Whitespace removal might affect the rendered output when using a CSS rule, such as `white-space: pre`. To disable this performance optimization and preserve the whitespace, take one of the following actions:

* Add the `@preservewhitespace true` directive at the top of the Razor file (`.razor`) to apply the preference to a specific component.
* Add the `@preservewhitespace true` directive inside an imports file (`_Imports.razor`) to apply the preference to a subdirectory or to the entire project.

In most cases, no action is required, as apps typically continue to behave normally (but faster). If stripping whitespace causes a rendering problem for a particular component, use `@preservewhitespace true` in that component to disable this optimization.

Whitespace is retained in a component's source markup. Whitespace-only text renders in the browser's DOM even when there's no visual effect.

Consider the following component markup:

```
<ul>
    @foreach (var item in Items)
    {
        <li>
            @item.Text
        </li>
    }
</ul>
```

The preceding example renders the following unnecessary whitespace:

* Outside of the `@foreach` code block.
* Around the `<li>` element.
* Around the `@item.Text` output.

A list of 100 items results in over 400 areas of whitespace. None of the extra whitespace visually affects the rendered output.

When rendering static HTML for components, whitespace inside a tag isn't preserved. For example, view the rendered output of the following `<img>` tag in a component Razor file (`.razor`):

```
<img     alt="Example image"   src="img.png"     />
```

Whitespace isn't preserved from the preceding markup:

```
<img alt="Example image" src="img.png" />
```

## Root component

A *root Razor component* (*root component*) is the first component loaded of any component hierarchy created by the app.

In an app created from the Blazor Web App project template, the `App` component (`App.razor`) is specified as the default root component by the type parameter declared for the call to [`MapRazorComponents<TRootComponent>`](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) in the server-side `Program` file. The following example shows the use of the `App` component as the root component, which is the default for an app created from the Blazor project template:

```
app.MapRazorComponents<App>();
```

Note

Making a root component interactive, such as the `App` component, isn't supported.

In an app created from the Blazor Server project template, the `App` component (`App.razor`) is specified as the default root component in `Pages/_Host.cshtml` using the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0):

```
<component type="typeof(App)" render-mode="ServerPrerendered" />
```

In an app created from the Blazor WebAssembly project template, the `App` component (`App.razor`) is specified as the default root component in the `Program` file:

```
builder.RootComponents.Add<App>("#app");
```

In the preceding code, the CSS selector, `#app`, indicates that the `App` component is specified for the `<div>` in `wwwroot/index.html` with an `id` of `app`:

```
<div id="app">...</app>
```

MVC and Razor Pages apps can also use the [Component Tag Helper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper) to register statically-rendered Blazor WebAssembly root components:

```
<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />
```

Statically-rendered components can only be added to the app. They can't be removed or updated afterwards.

For more information, see the following resources:

* [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0)
* [Integrate ASP.NET Core Razor components with MVC or Razor Pages](integration?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/validation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/validation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 8 of tutorial series on Razor Pages

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT)

In this section, you add validation logic to the `Movie` model. The app enforces the validation rules any time a user creates or edits a movie.

## Validation

A key tenet of software development is called [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("**D**on't **R**epeat **Y**ourself"). Razor Pages encourages development where you specify functionality once, and it's reflected throughout the app. DRY can help:

* Reduce the amount of code in an app.
* Make the code less error prone, and easier to test and maintain.

The validation support that Razor Pages and Entity Framework provide is a good example of the DRY principle:

* You declaratively specify validation rules in one place, in the model class.
* The app enforces rules everywhere.

## Validation in .NET 10

In .NET 10, the unified validation APIs are in the `Microsoft.Extensions.Validation` NuGet package. By using this package, you can use the validation APIs outside of ASP.NET Core HTTP scenarios.

To use the `Microsoft.Extensions.Validation` APIs:

* Add the following package reference:

  ```
  <PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.1" />
  ```

  The functionality is the same but now requires an explicit package reference.

* Register validation services by using dependency injection:

  ```
  builder.Services.AddValidation();
  ```

## Add validation rules to the movie model

The [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace provides:

* A set of built-in validation attributes that you apply declaratively to a class or property.
* Formatting attributes like `[DataType]` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in `[Required]`, `[StringLength]`, `[RegularExpression]`, and `[Range]` validation attributes.

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    [Required]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
    [Required]
    [StringLength(30)]
    public string Genre { get; set; } = string.Empty;

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string Rating { get; set; } = string.Empty;
}
```

The validation attributes specify behavior to enforce on the model properties they're applied to:

* The `[Required]` and `[MinimumLength]` attributes indicate that a property must have a value. Nothing prevents a user from entering white space to satisfy this validation.

* The `[RegularExpression]` attribute limits what characters can be input. In the preceding code, `Genre`:

  * Must only use letters.
  * The first letter must be uppercase. White spaces are allowed, while numbers and special characters aren't allowed.

* The `RegularExpression` `Rating`:

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a `Genre`.

* The `[Range]` attribute constrains a value to within a specified range.

* The `[StringLength]` attribute can set a maximum length of a string property, and optionally its minimum length.

* Value types, such as `decimal`, `int`, `float`, `DateTime`, are inherently required and don't need the `[Required]` attribute.

The preceding validation rules are for demonstration. They aren't optimal for a production system. For example, the preceding rules prevent entering a movie with only two characters and don't allow special characters in `Genre`.

By having ASP.NET Core automatically enforce validation rules, you can:

* Make the app more robust.
* Reduce chances of saving invalid data to the database.

### Validation error UI in Razor Pages

Run the app and go to **Pages/MoviePages**.

Select the **Create New** link. Complete the form with some invalid values. When jQuery client-side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form automatically renders a validation error message in each field containing an invalid value. The errors are enforced both client-side, by using JavaScript and jQuery, and server-side, when a user has JavaScript disabled.

A significant benefit is that **no** code changes are necessary in the Create or Edit pages. Once you apply data annotations to the model, the validation UI is enabled. The Razor Pages you created in this tutorial automatically pick up the validation rules, by using validation attributes on the properties of the `Movie` model class. To test validation by using the Edit page, the same validation is applied.

The form data isn't posted to the server until there are no client-side validation errors. Verify form data isn't posted by one or more of the following approaches:

* Put a break point in the `OnPostAsync` method. Submit the form by selecting **Create** or **Save**. The break point is never hit.
* Use the [Fiddler tool](https://www.telerik.com/fiddler).
* Use the browser developer tools to monitor network traffic.

### Server-side validation

When JavaScript is disabled in the browser, submitting the form with errors posts the form to the server.

To test server-side validation:

1. Disable JavaScript in the browser. Use the browser's developer tools to disable JavaScript. If you can't disable JavaScript in the browser, try another browser.

2. Set a break point in the `OnPostAsync` method of the Create or Edit page.

3. Submit a form with invalid data.

4. Verify the model state is invalid.

   ```
    if (!ModelState.IsValid)
    {
       return Page();
    }
   ```

Alternatively, [disable client-side validation on the server](../../mvc/models/validation?view=aspnetcore-10.0#disable-client-side-validation).

The following code shows a portion of the `Create.cshtml` page scaffolded earlier in the tutorial. The Create and Edit pages use this code to:

* Display the initial form.
* Redisplay the form in the event of an error.

```
<form method="post">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="Movie.Title" class="control-label"></label>
        <input asp-for="Movie.Title" class="form-control" />
        <span asp-validation-for="Movie.Title" class="text-danger"></span>
    </div>
```

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. For more information, see [Validation](../../mvc/models/validation?view=aspnetcore-10.0).

The Create and Edit pages don't contain validation rules. The validation rules and the error strings are specified only in the `Movie` class. These validation rules automatically apply to Razor Pages that edit the `Movie` model.

When you need to change validation logic, change it only in the model. By defining validation logic in one place, you ensure consistent validation throughout the app. Validation in one place helps keep the code clean and makes it easier to maintain and update.

## Use DataType attributes

Examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. The `[DataType]` attribute is applied to the `ReleaseDate` and `Price` properties.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
[Required]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
```

The `[DataType]` attributes provide:

* Hints for the view engine to format the data.
* Attributes such as `<a>` for URLs and `<a href="mailto:EmailAddress.com">` for email.

Use the `[RegularExpression]` attribute to validate the format of the data. Use the `[DataType]` attribute to specify a data type that's more specific than the database intrinsic type. `[DataType]` attributes aren't validation attributes. In the sample app, only the date is displayed, without time.

The `DataType` enumeration provides many data types, such as `Date`, `Time`, `PhoneNumber`, `Currency`, `EmailAddress`, and more.

The `[DataType]` attributes:

* Can enable the app to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`.
* Can provide a date selector `DataType.Date` in browsers that support HTML5.
* Emit HTML 5 `data-`, pronounced "data dash", attributes that HTML 5 browsers consume.
* Do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

The `[DisplayFormat]` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting is applied when the value is displayed for editing. That behavior might not be wanted for some fields. For example, in currency values, the currency symbol is usually not wanted in the edit UI.

The `[DisplayFormat]` attribute can be used by itself, but it's generally a good idea to use the `[DataType]` attribute. The `[DataType]` attribute conveys the semantics of the data as opposed to how to render it on a screen. The `[DataType]` attribute provides the following benefits that aren't available with `[DisplayFormat]`:

* The browser can enable HTML5 features, for example to show a calendar control, the locale-appropriate currency symbol, email links, and more.
* By default, the browser renders data using the correct format based on its locale.
* The `[DataType]` attribute can enable the ASP.NET Core framework to choose the right field template to render the data. The `DisplayFormat`, if used by itself, uses the string template.

**Note:** jQuery validation doesn't work with the `[Range]` attribute and `DateTime`. For example, the following code always displays a client-side validation error, even when the date is in the specified range:

```
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
```

It's a best practice to avoid compiling hard dates in models, so using the `[Range]` attribute and `DateTime` is discouraged. Use [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) for date ranges and other values that are subject to frequent change rather than specifying it in code.

The following code shows combining attributes on one line:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date"), DataType(DataType.Date), Required]
    public DateTime ReleaseDate { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
    public string Genre { get; set; } = string.Empty;

    [Range(1, 100), DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string Rating { get; set; } = string.Empty;
}
```

[Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) shows advanced EF Core operations with Razor Pages.

### Apply migrations

The DataAnnotations you apply to the class change the schema. For example, the DataAnnotations you apply to the `Title` field:

```
[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; } = string.Empty;
```

* Limit the characters to 60.
* Doesn't allow a `null` value.

The `Movie` table currently has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (MAX)  NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (MAX)  NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (MAX)  NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

The preceding schema changes don't cause EF to throw an exception. However, create a migration so the schema is consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**. In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

`Update-Database` runs the `Up` method of the `New_DataAnnotations` class.

Use the following commands to add a migration for the new DataAnnotations:

```
dotnet ef migrations add New_DataAnnotations
dotnet ef database update
```

`dotnet ef database update` runs the `Up` method of the `New_DataAnnotations` class.

Examine the `Up` method:

```
public partial class New_DataAnnotations : Migration
{
    /// <inheritdoc />
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AlterColumn<string>(
            name: "Title",
            table: "Movie",
            type: "nvarchar(60)",
            maxLength: 60,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Rating",
            table: "Movie",
            type: "nvarchar(5)",
            maxLength: 5,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Genre",
            table: "Movie",
            type: "nvarchar(30)",
            maxLength: 30,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");
    }
```

The updated `Movie` table has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (60)   NOT NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (30)   NOT NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (5)    NOT NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

Note

The preceding `Up` method and schema show SQL Server output. If you're using SQLite instead of SQL Server, the generated migration and schema differ. SQLite has a minimal type system that maps `string` properties to `TEXT` and doesn't apply the `[StringLength]` maximum length to the schema. Columns such as `Title`, `Genre`, and `Rating` are created as `TEXT` with no length. The maximum length is still enforced by ASP.NET Core model validation, not by the database schema. For more information, see [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations).

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core app in Azure with SQL Database](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

Thanks for completing this introduction to Razor Pages. [Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) is an excellent follow up to this tutorial.

## Related content

* [Tag Helpers in forms in ASP.NET Core](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization in ASP.NET Core](../../fundamentals/localization?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

## Next steps

[Previous: Add a new field](new-field?view=aspnetcore-10.0)

In this section, validation logic is added to the `Movie` model. The validation rules are enforced any time a user creates or edits a movie.

## Validation

A key tenet of software development is called [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("**D**on't **R**epeat **Y**ourself"). Razor Pages encourages development where functionality is specified once, and it's reflected throughout the app. DRY can help:

* Reduce the amount of code in an app.
* Make the code less error prone, and easier to test and maintain.

The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle:

* Validation rules are declaratively specified in one place, in the model class.
* Rules are enforced everywhere in the app.

## Add validation rules to the movie model

The [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace provides:

* A set of built-in validation attributes that are applied declaratively to a class or property.
* Formatting attributes like `[DataType]` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in `[Required]`, `[StringLength]`, `[RegularExpression]`, and `[Range]` validation attributes.

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
    [Required]
    [StringLength(30)]
    public string Genre { get; set; } = string.Empty;

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string Rating { get; set; } = string.Empty;
}
```

The validation attributes specify behavior to enforce on the model properties they're applied to:

* The `[Required]` and `[MinimumLength]` attributes indicate that a property must have a value. Nothing prevents a user from entering white space to satisfy this validation.

* The `[RegularExpression]` attribute is used to limit what characters can be input. In the preceding code, `Genre`:

  * Must only use letters.
  * The first letter must be uppercase. White spaces are allowed, while numbers and special characters aren't allowed.

* The `RegularExpression` `Rating`:

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a `Genre`.

* The `[Range]` attribute constrains a value to within a specified range.

* The `[StringLength]` attribute can set a maximum length of a string property, and optionally its minimum length.

* Value types, such as `decimal`, `int`, `float`, `DateTime`, are inherently required and don't need the `[Required]` attribute.

The preceding validation rules are used for demonstration, they are not optimal for a production system. For example, the preceding prevents entering a movie with only two chars and doesn't allow special characters in `Genre`.

Having validation rules automatically enforced by ASP.NET Core helps:

* Make the app more robust.
* Reduce chances of saving invalid data to the database.

### Validation Error UI in Razor Pages

Run the app and navigate to Pages/Movies.

Select the **Create New** link. Complete the form with some invalid values. When jQuery client-side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered a validation error message in each field containing an invalid value. The errors are enforced both client-side, using JavaScript and jQuery, and server-side, when a user has JavaScript disabled.

A significant benefit is that **no** code changes were necessary in the Create or Edit pages. Once data annotations were applied to the model, the validation UI was enabled. The Razor Pages created in this tutorial automatically picked up the validation rules, using validation attributes on the properties of the `Movie` model class. Test validation using the Edit page, the same validation is applied.

The form data isn't posted to the server until there are no client-side validation errors. Verify form data isn't posted by one or more of the following approaches:

* Put a break point in the `OnPostAsync` method. Submit the form by selecting **Create** or **Save**. The break point is never hit.
* Use the [Fiddler tool](https://www.telerik.com/fiddler).
* Use the browser developer tools to monitor network traffic.

### Server-side validation

When JavaScript is disabled in the browser, submitting the form with errors will post to the server.

Optional, test server-side validation:

1. Disable JavaScript in the browser. JavaScript can be disabled using browser's developer tools. If JavaScript cannot be disabled in the browser, try another browser.

2. Set a break point in the `OnPostAsync` method of the Create or Edit page.

3. Submit a form with invalid data.

4. Verify the model state is invalid:

   ```
    if (!ModelState.IsValid)
    {
       return Page();
    }
   ```

Alternatively, [Disable client-side validation on the server](../../mvc/models/validation?view=aspnetcore-10.0#disable-client-side-validation).

The following code shows a portion of the `Create.cshtml` page scaffolded earlier in the tutorial. It's used by the Create and Edit pages to:

* Display the initial form.
* Redisplay the form in the event of an error.

```
<form method="post">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="Movie.Title" class="control-label"></label>
        <input asp-for="Movie.Title" class="form-control" />
        <span asp-validation-for="Movie.Title" class="text-danger"></span>
    </div>
```

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

The Create and Edit pages have no validation rules in them. The validation rules and the error strings are specified only in the `Movie` class. These validation rules are automatically applied to Razor Pages that edit the `Movie` model.

When validation logic needs to change, it's done only in the model. Validation is applied consistently throughout the app, validation logic is defined in one place. Validation in one place helps keep the code clean, and makes it easier to maintain and update.

## Use DataType Attributes

Examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. The `[DataType]` attribute is applied to the `ReleaseDate` and `Price` properties.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
```

The `[DataType]` attributes provide:

* Hints for the view engine to format the data.
* Supplies attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email.

Use the `[RegularExpression]` attribute to validate the format of the data. The `[DataType]` attribute is used to specify a data type that's more specific than the database intrinsic type. `[DataType]` attributes aren't validation attributes. In the sample app, only the date is displayed, without time.

The `DataType` enumeration provides many data types, such as `Date`, `Time`, `PhoneNumber`, `Currency`, `EmailAddress`, and more.

The `[DataType]` attributes:

* Can enable the app to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`.
* Can provide a date selector `DataType.Date` in browsers that support HTML5.
* Emit HTML 5 `data-`, pronounced "data dash", attributes that HTML 5 browsers consume.
* Do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

The `[DisplayFormat]` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting will be applied when the value is displayed for editing. That behavior may not be wanted for some fields. For example, in currency values, the currency symbol is usually not wanted in the edit UI.

The `[DisplayFormat]` attribute can be used by itself, but it's generally a good idea to use the `[DataType]` attribute. The `[DataType]` attribute conveys the semantics of the data as opposed to how to render it on a screen. The `[DataType]` attribute provides the following benefits that aren't available with `[DisplayFormat]`:

* The browser can enable HTML5 features, for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.
* By default, the browser renders data using the correct format based on its locale.
* The `[DataType]` attribute can enable the ASP.NET Core framework to choose the right field template to render the data. The `DisplayFormat`, if used by itself, uses the string template.

**Note:** jQuery validation doesn't work with the `[Range]` attribute and `DateTime`. For example, the following code will always display a client-side validation error, even when the date is in the specified range:

```
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
```

It's a best practice to avoid compiling hard dates in models, so using the `[Range]` attribute and `DateTime` is discouraged. Use [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) for date ranges and other values that are subject to frequent change rather than specifying it in code.

The following code shows combining attributes on one line:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date"), DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
    public string Genre { get; set; } = string.Empty;

    [Range(1, 100), DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string Rating { get; set; } = string.Empty;
}
```

[Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) shows advanced EF Core operations with Razor Pages.

### Apply migrations

The DataAnnotations applied to the class changes the schema. For example, the DataAnnotations applied to the `Title` field:

```
[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; } = string.Empty;
```

* Limits the characters to 60.
* Doesn't allow a `null` value.

The `Movie` table currently has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (MAX)  NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (MAX)  NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (MAX)  NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

The preceding schema changes don't cause EF to throw an exception. However, create a migration so the schema is consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**. In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

`Update-Database` runs the `Up` method of the `New_DataAnnotations` class.

Use the following commands to add a migration for the new DataAnnotations:

```
dotnet ef migrations add New_DataAnnotations
dotnet ef database update
```

`dotnet ef database update` runs the `Up` method of the `New_DataAnnotations` class.

Examine the `Up` method:

```
public partial class New_DataAnnotations : Migration
{
    /// <inheritdoc />
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AlterColumn<string>(
            name: "Title",
            table: "Movie",
            type: "nvarchar(60)",
            maxLength: 60,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Rating",
            table: "Movie",
            type: "nvarchar(5)",
            maxLength: 5,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Genre",
            table: "Movie",
            type: "nvarchar(30)",
            maxLength: 30,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");
    }
```

The updated `Movie` table has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (60)   NOT NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (30)   NOT NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (5)    NOT NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

Note

The preceding `Up` method and schema show SQL Server output. If you're using SQLite instead of SQL Server, the generated migration and schema differ. SQLite has a minimal type system that maps `string` properties to `TEXT` and doesn't apply the `[StringLength]` maximum length to the schema. Columns such as `Title`, `Genre`, and `Rating` are created as `TEXT` with no length. The maximum length is still enforced by ASP.NET Core model validation, not by the database schema. For more information, see [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations).

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core app in Azure with SQL Database](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

Thanks for completing this introduction to Razor Pages. [Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) is an excellent follow up to this tutorial.

## Related content

* [Tag Helpers in forms in ASP.NET Core](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization in ASP.NET Core](../../fundamentals/localization?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

## Next steps

[Previous: Add a new field](new-field?view=aspnetcore-10.0)

In this section, validation logic is added to the `Movie` model. The validation rules are enforced any time a user creates or edits a movie.

## Validation

A key tenet of software development is called [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("**D**on't **R**epeat **Y**ourself"). Razor Pages encourages development where functionality is specified once, and it's reflected throughout the app. DRY can help:

* Reduce the amount of code in an app.
* Make the code less error prone, and easier to test and maintain.

The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle:

* Validation rules are declaratively specified in one place, in the model class.
* Rules are enforced everywhere in the app.

## Add validation rules to the movie model

The [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace provides:

* A set of built-in validation attributes that are applied declaratively to a class or property.
* Formatting attributes like `[DataType]` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in `[Required]`, `[StringLength]`, `[RegularExpression]`, and `[Range]` validation attributes.

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string Title { get; set; } = string.Empty;

    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
    [Required]
    [StringLength(30)]
    public string Genre { get; set; } = string.Empty;

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string Rating { get; set; } = string.Empty;
}
```

The validation attributes specify behavior to enforce on the model properties they're applied to:

* The `[Required]` and `[MinimumLength]` attributes indicate that a property must have a value. Nothing prevents a user from entering white space to satisfy this validation.

* The `[RegularExpression]` attribute is used to limit what characters can be input. In the preceding code, `Genre`:

  * Must only use letters.
  * The first letter must be uppercase. White spaces are allowed, while numbers and special characters aren't allowed.

* The `RegularExpression` `Rating`:

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a `Genre`.

* The `[Range]` attribute constrains a value to within a specified range.

* The `[StringLength]` attribute can set a maximum length of a string property, and optionally its minimum length.

* Value types, such as `decimal`, `int`, `float`, `DateTime`, are inherently required and don't need the `[Required]` attribute.

The preceding validation rules are used for demonstration, they are not optimal for a production system. For example, the preceding prevents entering a movie with only two chars and doesn't allow special characters in `Genre`.

Having validation rules automatically enforced by ASP.NET Core helps:

* Make the app more robust.
* Reduce chances of saving invalid data to the database.

### Validation Error UI in Razor Pages

Run the app and navigate to Pages/Movies.

Select the **Create New** link. Complete the form with some invalid values. When jQuery client-side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered a validation error message in each field containing an invalid value. The errors are enforced both client-side, using JavaScript and jQuery, and server-side, when a user has JavaScript disabled.

A significant benefit is that **no** code changes were necessary in the Create or Edit pages. Once data annotations were applied to the model, the validation UI was enabled. The Razor Pages created in this tutorial automatically picked up the validation rules, using validation attributes on the properties of the `Movie` model class. Test validation using the Edit page, the same validation is applied.

The form data isn't posted to the server until there are no client-side validation errors. Verify form data isn't posted by one or more of the following approaches:

* Put a break point in the `OnPostAsync` method. Submit the form by selecting **Create** or **Save**. The break point is never hit.
* Use the [Fiddler tool](https://www.telerik.com/fiddler).
* Use the browser developer tools to monitor network traffic.

### Server-side validation

When JavaScript is disabled in the browser, submitting the form with errors will post to the server.

Optional, test server-side validation:

1. Disable JavaScript in the browser. JavaScript can be disabled using browser's developer tools. If JavaScript cannot be disabled in the browser, try another browser.

2. Set a break point in the `OnPostAsync` method of the Create or Edit page.

3. Submit a form with invalid data.

4. Verify the model state is invalid:

   ```
    if (!ModelState.IsValid)
    {
       return Page();
    }
   ```

Alternatively, [Disable client-side validation on the server](../../mvc/models/validation?view=aspnetcore-10.0#disable-client-side-validation).

The following code shows a portion of the `Create.cshtml` page scaffolded earlier in the tutorial. It's used by the Create and Edit pages to:

* Display the initial form.
* Redisplay the form in the event of an error.

```
<form method="post">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="Movie.Title" class="control-label"></label>
        <input asp-for="Movie.Title" class="form-control" />
        <span asp-validation-for="Movie.Title" class="text-danger"></span>
    </div>
```

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

The Create and Edit pages have no validation rules in them. The validation rules and the error strings are specified only in the `Movie` class. These validation rules are automatically applied to Razor Pages that edit the `Movie` model.

When validation logic needs to change, it's done only in the model. Validation is applied consistently throughout the app, validation logic is defined in one place. Validation in one place helps keep the code clean, and makes it easier to maintain and update.

## Use DataType Attributes

Examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. The `[DataType]` attribute is applied to the `ReleaseDate` and `Price` properties.

```
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
```

The `[DataType]` attributes provide:

* Hints for the view engine to format the data.
* Supplies attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email.

Use the `[RegularExpression]` attribute to validate the format of the data. The `[DataType]` attribute is used to specify a data type that's more specific than the database intrinsic type. `[DataType]` attributes aren't validation attributes. In the sample app, only the date is displayed, without time.

The `DataType` enumeration provides many data types, such as `Date`, `Time`, `PhoneNumber`, `Currency`, `EmailAddress`, and more.

The `[DataType]` attributes:

* Can enable the app to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`.
* Can provide a date selector `DataType.Date` in browsers that support HTML5.
* Emit HTML 5 `data-`, pronounced "data dash", attributes that HTML 5 browsers consume.
* Do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

The `[DisplayFormat]` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting will be applied when the value is displayed for editing. That behavior may not be wanted for some fields. For example, in currency values, the currency symbol is usually not wanted in the edit UI.

The `[DisplayFormat]` attribute can be used by itself, but it's generally a good idea to use the `[DataType]` attribute. The `[DataType]` attribute conveys the semantics of the data as opposed to how to render it on a screen. The `[DataType]` attribute provides the following benefits that aren't available with `[DisplayFormat]`:

* The browser can enable HTML5 features, for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.
* By default, the browser renders data using the correct format based on its locale.
* The `[DataType]` attribute can enable the ASP.NET Core framework to choose the right field template to render the data. The `DisplayFormat`, if used by itself, uses the string template.

**Note:** jQuery validation doesn't work with the `[Range]` attribute and `DateTime`. For example, the following code will always display a client-side validation error, even when the date is in the specified range:

```
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
```

It's a best practice to avoid compiling hard dates in models, so using the `[Range]` attribute and `DateTime` is discouraged. Use [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) for date ranges and other values that are subject to frequent change rather than specifying it in code.

The following code shows combining attributes on one line:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date"), DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
    public string Genre { get; set; } = string.Empty;

    [Range(1, 100), DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string Rating { get; set; } = string.Empty;
}
```

[Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) shows advanced EF Core operations with Razor Pages.

### Apply migrations

The DataAnnotations applied to the class changes the schema. For example, the DataAnnotations applied to the `Title` field:

```
[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; } = string.Empty;
```

* Limits the characters to 60.
* Doesn't allow a `null` value.

The `Movie` table currently has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (MAX)  NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (MAX)  NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (MAX)  NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

The preceding schema changes don't cause EF to throw an exception. However, create a migration so the schema is consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**. In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

`Update-Database` runs the `Up` method of the `New_DataAnnotations` class.

Use the following commands to add a migration for the new DataAnnotations:

```
dotnet ef migrations add New_DataAnnotations
dotnet ef database update
```

`dotnet ef database update` runs the `Up` method of the `New_DataAnnotations` class.

Examine the `Up` method:

```
public partial class New_DataAnnotations : Migration
{
    /// <inheritdoc />
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AlterColumn<string>(
            name: "Title",
            table: "Movie",
            type: "nvarchar(60)",
            maxLength: 60,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Rating",
            table: "Movie",
            type: "nvarchar(5)",
            maxLength: 5,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Genre",
            table: "Movie",
            type: "nvarchar(30)",
            maxLength: 30,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");
    }
```

The updated `Movie` table has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (60)   NOT NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (30)   NOT NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (5)    NOT NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

Note

The preceding `Up` method and schema show SQL Server output. If you're using SQLite instead of SQL Server, the generated migration and schema differ. SQLite has a minimal type system that maps `string` properties to `TEXT` and doesn't apply the `[StringLength]` maximum length to the schema. Columns such as `Title`, `Genre`, and `Rating` are created as `TEXT` with no length. The maximum length is still enforced by ASP.NET Core model validation, not by the database schema. For more information, see [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations).

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core app in Azure with SQL Database](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

Thanks for completing this introduction to Razor Pages. [Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) is an excellent follow up to this tutorial.

## Related content

* [Tag Helpers in forms in ASP.NET Core](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization in ASP.NET Core](../../fundamentals/localization?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

## Next steps

[Previous: Add a new field](new-field?view=aspnetcore-10.0)

In this section, validation logic is added to the `Movie` model. The validation rules are enforced any time a user creates or edits a movie.

## Validation

A key tenet of software development is called [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("**D**on't **R**epeat **Y**ourself"). Razor Pages encourages development where functionality is specified once, and it's reflected throughout the app. DRY can help:

* Reduce the amount of code in an app.
* Make the code less error prone, and easier to test and maintain.

The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle:

* Validation rules are declaratively specified in one place, in the model class.
* Rules are enforced everywhere in the app.

## Add validation rules to the movie model

The [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace provides:

* A set of built-in validation attributes that are applied declaratively to a class or property.
* Formatting attributes like `[DataType]` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in `[Required]`, `[StringLength]`, `[RegularExpression]`, and `[Range]` validation attributes.

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string Title { get; set; } = string.Empty;

    // [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
    [Required]
    [StringLength(30)]
    public string Genre { get; set; } = string.Empty;

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string Rating { get; set; } = string.Empty;
}
```

The validation attributes specify behavior to enforce on the model properties they're applied to:

* The `[Required]` and `[MinimumLength]` attributes indicate that a property must have a value. Nothing prevents a user from entering white space to satisfy this validation.

* The `[RegularExpression]` attribute is used to limit what characters can be input. In the preceding code, `Genre`:

  * Must only use letters.
  * The first letter is required to be uppercase. White spaces are allowed while numbers, and special characters are not allowed.

* The `RegularExpression` `Rating`:

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a `Genre`.

* The `[Range]` attribute constrains a value to within a specified range.

* The `[StringLength]` attribute can set a maximum length of a string property, and optionally its minimum length.

* Value types, such as `decimal`, `int`, `float`, `DateTime`, are inherently required and don't need the `[Required]` attribute.

The preceding validation rules are used for demonstration, they are not optimal for a production system. For example, the preceding prevents entering a movie with only two chars and doesn't allow special characters in `Genre`.

Having validation rules automatically enforced by ASP.NET Core helps:

* Make the app more robust.
* Reduce chances of saving invalid data to the database.

### Validation Error UI in Razor Pages

Run the app and navigate to Pages/Movies.

Select the **Create New** link. Complete the form with some invalid values. When jQuery client-side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered a validation error message in each field containing an invalid value. The errors are enforced both client-side, using JavaScript and jQuery, and server-side, when a user has JavaScript disabled.

A significant benefit is that **no** code changes were necessary in the Create or Edit pages. Once data annotations were applied to the model, the validation UI was enabled. The Razor Pages created in this tutorial automatically picked up the validation rules, using validation attributes on the properties of the `Movie` model class. Test validation using the Edit page, the same validation is applied.

The form data isn't posted to the server until there are no client-side validation errors. Verify form data isn't posted by one or more of the following approaches:

* Put a break point in the `OnPostAsync` method. Submit the form by selecting **Create** or **Save**. The break point is never hit.
* Use the [Fiddler tool](https://www.telerik.com/fiddler).
* Use the browser developer tools to monitor network traffic.

### Server-side validation

When JavaScript is disabled in the browser, submitting the form with errors will post to the server.

Optional, test server-side validation:

1. Disable JavaScript in the browser. JavaScript can be disabled using browser's developer tools. If JavaScript cannot be disabled in the browser, try another browser.

2. Set a break point in the `OnPostAsync` method of the Create or Edit page.

3. Submit a form with invalid data.

4. Verify the model state is invalid:

   ```
    if (!ModelState.IsValid)
    {
       return Page();
    }
   ```

Alternatively, [Disable client-side validation on the server](../../mvc/models/validation?view=aspnetcore-10.0#disable-client-side-validation).

The following code shows a portion of the `Create.cshtml` page scaffolded earlier in the tutorial. It's used by the Create and Edit pages to:

* Display the initial form.
* Redisplay the form in the event of an error.

```
<form method="post">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="Movie.Title" class="control-label"></label>
        <input asp-for="Movie.Title" class="form-control" />
        <span asp-validation-for="Movie.Title" class="text-danger"></span>
    </div>
```

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

The Create and Edit pages have no validation rules in them. The validation rules and the error strings are specified only in the `Movie` class. These validation rules are automatically applied to Razor Pages that edit the `Movie` model.

When validation logic needs to change, it's done only in the model. Validation is applied consistently throughout the app, validation logic is defined in one place. Validation in one place helps keep the code clean, and makes it easier to maintain and update.

## Use DataType Attributes

Examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. The `[DataType]` attribute is applied to the `ReleaseDate` and `Price` properties.

```
// [Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
```

The `[DataType]` attributes provide:

* Hints for the view engine to format the data.
* Supplies attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email.

Use the `[RegularExpression]` attribute to validate the format of the data. The `[DataType]` attribute is used to specify a data type that's more specific than the database intrinsic type. `[DataType]` attributes aren't validation attributes. In the sample app, only the date is displayed, without time.

The `DataType` enumeration provides many data types, such as `Date`, `Time`, `PhoneNumber`, `Currency`, `EmailAddress`, and more.

The `[DataType]` attributes:

* Can enable the app to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`.
* Can provide a date selector `DataType.Date` in browsers that support HTML5.
* Emit HTML 5 `data-`, pronounced "data dash", attributes that HTML 5 browsers consume.
* Do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

The `[DisplayFormat]` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting will be applied when the value is displayed for editing. That behavior may not be wanted for some fields. For example, in currency values, the currency symbol is usually not wanted in the edit UI.

The `[DisplayFormat]` attribute can be used by itself, but it's generally a good idea to use the `[DataType]` attribute. The `[DataType]` attribute conveys the semantics of the data as opposed to how to render it on a screen. The `[DataType]` attribute provides the following benefits that aren't available with `[DisplayFormat]`:

* The browser can enable HTML5 features, for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.
* By default, the browser renders data using the correct format based on its locale.
* The `[DataType]` attribute can enable the ASP.NET Core framework to choose the right field template to render the data. The `DisplayFormat`, if used by itself, uses the string template.

**Note:** jQuery validation doesn't work with the `[Range]` attribute and `DateTime`. For example, the following code will always display a client-side validation error, even when the date is in the specified range:

```
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
```

It's a best practice to avoid compiling hard dates in models, so using the `[Range]` attribute and `DateTime` is discouraged. Use [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) for date ranges and other values that are subject to frequent change rather than specifying it in code.

The following code shows combining attributes on one line:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date"), DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
    public string Genre { get; set; } = string.Empty;

    [Range(1, 100), DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string Rating { get; set; } = string.Empty;
}
```

[Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) shows advanced EF Core operations with Razor Pages.

### Apply migrations

The DataAnnotations applied to the class changes the schema. For example, the DataAnnotations applied to the `Title` field:

```
[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; } = string.Empty;
```

* Limits the characters to 60.
* Doesn't allow a `null` value.

The `Movie` table currently has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (MAX)  NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (MAX)  NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (MAX)  NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

The preceding schema changes don't cause EF to throw an exception. However, create a migration so the schema is consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**. In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

`Update-Database` runs the `Up` method of the `New_DataAnnotations` class.

Use the following commands to add a migration for the new DataAnnotations:

```
dotnet ef migrations add New_DataAnnotations
dotnet ef database update
```

`dotnet ef database update` runs the `Up` method of the `New_DataAnnotations` class.

Examine the `Up` method:

```
public partial class NewDataAnnotations : Migration
{
    /// <inheritdoc />
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AlterColumn<string>(
            name: "Title",
            table: "Movie",
            type: "nvarchar(60)",
            maxLength: 60,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Rating",
            table: "Movie",
            type: "nvarchar(5)",
            maxLength: 5,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Genre",
            table: "Movie",
            type: "nvarchar(30)",
            maxLength: 30,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");
    }
```

The updated `Movie` table has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (60)   NOT NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (30)   NOT NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (5)    NOT NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

Note

The preceding `Up` method and schema show SQL Server output. If you're using SQLite instead of SQL Server, the generated migration and schema differ. SQLite has a minimal type system that maps `string` properties to `TEXT` and doesn't apply the `[StringLength]` maximum length to the schema. Columns such as `Title`, `Genre`, and `Rating` are created as `TEXT` with no length. The maximum length is still enforced by ASP.NET Core model validation, not by the database schema. For more information, see [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations).

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core app in Azure with SQL Database](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

Thanks for completing this introduction to Razor Pages. [Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) is an excellent follow up to this tutorial.

## Related content

* [Tag Helpers in forms in ASP.NET Core](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization in ASP.NET Core](../../fundamentals/localization?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

## Next steps

[Previous: Add a new field](new-field?view=aspnetcore-10.0)

In this section, validation logic is added to the `Movie` model. The validation rules are enforced any time a user creates or edits a movie.

## Validation

A key tenet of software development is called [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("**D**on't **R**epeat **Y**ourself"). Razor Pages encourages development where functionality is specified once, and it's reflected throughout the app. DRY can help:

* Reduce the amount of code in an app.
* Make the code less error prone, and easier to test and maintain.

The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle:

* Validation rules are declaratively specified in one place, in the model class.
* Rules are enforced everywhere in the app.

## Add validation rules to the movie model

The [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace provides:

* A set of built-in validation attributes that are applied declaratively to a class or property.
* Formatting attributes like `[DataType]` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in `[Required]`, `[StringLength]`, `[RegularExpression]`, and `[Range]` validation attributes.

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; } = string.Empty;

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; } = string.Empty;

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; } = string.Empty;
    }
}
```

The validation attributes specify behavior to enforce on the model properties they're applied to:

* The `[Required]` and `[MinimumLength]` attributes indicate that a property must have a value. Nothing prevents a user from entering white space to satisfy this validation.

* The `[RegularExpression]` attribute is used to limit what characters can be input. In the preceding code, `Genre`:

  * Must only use letters.
  * The first letter is required to be uppercase. White spaces are allowed while numbers, and special characters are not allowed.

* The `RegularExpression` `Rating`:

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a `Genre`.

* The `[Range]` attribute constrains a value to within a specified range.

* The `[StringLength]` attribute can set a maximum length of a string property, and optionally its minimum length.

* Value types, such as `decimal`, `int`, `float`, `DateTime`, are inherently required and don't need the `[Required]` attribute.

The preceding validation rules are used for demonstration, they are not optimal for a production system. For example, the preceding prevents entering a movie with only two chars and doesn't allow special characters in `Genre`.

Having validation rules automatically enforced by ASP.NET Core helps:

* Make the app more robust.
* Reduce chances of saving invalid data to the database.

### Validation Error UI in Razor Pages

Run the app and navigate to Pages/Movies.

Select the **Create New** link. Complete the form with some invalid values. When jQuery client-side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered a validation error message in each field containing an invalid value. The errors are enforced both client-side, using JavaScript and jQuery, and server-side, when a user has JavaScript disabled.

A significant benefit is that **no** code changes were necessary in the Create or Edit pages. Once data annotations were applied to the model, the validation UI was enabled. The Razor Pages created in this tutorial automatically picked up the validation rules, using validation attributes on the properties of the `Movie` model class. Test validation using the Edit page, the same validation is applied.

The form data isn't posted to the server until there are no client-side validation errors. Verify form data isn't posted by one or more of the following approaches:

* Put a break point in the `OnPostAsync` method. Submit the form by selecting **Create** or **Save**. The break point is never hit.
* Use the [Fiddler tool](https://www.telerik.com/fiddler).
* Use the browser developer tools to monitor network traffic.

### Server-side validation

When JavaScript is disabled in the browser, submitting the form with errors will post to the server.

Optional, test server-side validation:

1. Disable JavaScript in the browser. JavaScript can be disabled using browser's developer tools. If you can't disable JavaScript in the browser, try another browser.

2. Set a break point in the `OnPostAsync` method of the Create or Edit page.

3. Submit a form with invalid data.

4. Verify the model state is invalid:

   ```
    if (!ModelState.IsValid)
    {
       return Page();
    }
   ```

Alternatively, [Disable client-side validation on the server](../../mvc/models/validation?view=aspnetcore-10.0#disable-client-side-validation).

The following code shows a portion of the `Create.cshtml` page scaffolded earlier in the tutorial. It's used by the Create and Edit pages to:

* Display the initial form.
* Redisplay the form in the event of an error.

```
<form method="post">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="Movie.Title" class="control-label"></label>
        <input asp-for="Movie.Title" class="form-control" />
        <span asp-validation-for="Movie.Title" class="text-danger"></span>
    </div>
```

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

The Create and Edit pages have no validation rules in them. The validation rules and the error strings are specified only in the `Movie` class. These validation rules are automatically applied to Razor Pages that edit the `Movie` model.

When validation logic needs to change, it's done only in the model. Validation is applied consistently throughout the application, validation logic is defined in one place. Validation in one place helps keep the code clean, and makes it easier to maintain and update.

## Use DataType Attributes

Examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. The `[DataType]` attribute is applied to the `ReleaseDate` and `Price` properties.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
```

The `[DataType]` attributes provide:

* Hints for the view engine to format the data.
* Supplies attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email.

Use the `[RegularExpression]` attribute to validate the format of the data. The `[DataType]` attribute is used to specify a data type that's more specific than the database intrinsic type. `[DataType]` attributes aren't validation attributes. In the sample application, only the date is displayed, without time.

The `DataType` enumeration provides many data types, such as `Date`, `Time`, `PhoneNumber`, `Currency`, `EmailAddress`, and more.

The `[DataType]` attributes:

* Can enable the application to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`.
* Can provide a date selector `DataType.Date` in browsers that support HTML5.
* Emit HTML 5 `data-`, pronounced "data dash", attributes that HTML 5 browsers consume.
* Do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

The `[DisplayFormat]` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting will be applied when the value is displayed for editing. That behavior may not be wanted for some fields. For example, in currency values, the currency symbol is usually not wanted in the edit UI.

The `[DisplayFormat]` attribute can be used by itself, but it's generally a good idea to use the `[DataType]` attribute. The `[DataType]` attribute conveys the semantics of the data as opposed to how to render it on a screen. The `[DataType]` attribute provides the following benefits that aren't available with `[DisplayFormat]`:

* The browser can enable HTML5 features, for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.
* By default, the browser renders data using the correct format based on its locale.
* The `[DataType]` attribute can enable the ASP.NET Core framework to choose the right field template to render the data. The `DisplayFormat`, if used by itself, uses the string template.

**Note:** jQuery validation doesn't work with the `[Range]` attribute and `DateTime`. For example, the following code will always display a client-side validation error, even when the date is in the specified range:

```
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
```

It's a best practice to avoid compiling hard dates in models, so using the `[Range]` attribute and `DateTime` is discouraged. Use [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) for date ranges and other values that are subject to frequent change rather than specifying it in code.

The following code shows combining attributes on one line:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        public string Title { get; set; } = string.Empty;

        [Display(Name = "Release Date"), DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
        public string Genre { get; set; } = string.Empty;

        [Range(1, 100), DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
        public string Rating { get; set; } = string.Empty;
    }
}
```

[Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) shows advanced EF Core operations with Razor Pages.

### Apply migrations

The DataAnnotations applied to the class changes the schema. For example, the DataAnnotations applied to the `Title` field:

```
[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; } = string.Empty;
```

* Limits the characters to 60.
* Doesn't allow a `null` value.

The `Movie` table currently has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (MAX)  NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (MAX)  NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (MAX)  NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

The preceding schema changes don't cause EF to throw an exception. However, create a migration so the schema is consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**. In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

`Update-Database` runs the `Up` method of the `New_DataAnnotations` class.

Use the following commands to add a migration for the new DataAnnotations:

```
dotnet ef migrations add New_DataAnnotations
dotnet ef database update
```

`dotnet ef database update` runs the `Up` method of the `New_DataAnnotations` class.

Examine the `Up` method:

```
public partial class New_DataAnnotations : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AlterColumn<string>(
            name: "Title",
            table: "Movie",
            type: "nvarchar(60)",
            maxLength: 60,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Rating",
            table: "Movie",
            type: "nvarchar(5)",
            maxLength: 5,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");

        migrationBuilder.AlterColumn<string>(
            name: "Genre",
            table: "Movie",
            type: "nvarchar(30)",
            maxLength: 30,
            nullable: false,
            oldClrType: typeof(string),
            oldType: "nvarchar(max)");
    }
```

The updated `Movie` table has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (60)   NOT NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (30)   NOT NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (5)    NOT NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

Note

The preceding `Up` method and schema show SQL Server output. If you're using SQLite instead of SQL Server, the generated migration and schema differ. SQLite has a minimal type system that maps `string` properties to `TEXT` and doesn't apply the `[StringLength]` maximum length to the schema. Columns such as `Title`, `Genre`, and `Rating` are created as `TEXT` with no length. The maximum length is still enforced by ASP.NET Core model validation, not by the database schema. For more information, see [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations).

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core app in Azure with SQL Database](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

Thanks for completing this introduction to Razor Pages. [Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) is an excellent follow up to this tutorial.

## Related content

* [Tag Helpers in forms in ASP.NET Core](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization in ASP.NET Core](../../fundamentals/localization?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

## Next steps

[Previous: Add a new field](new-field?view=aspnetcore-10.0)

In this section, validation logic is added to the `Movie` model. The validation rules are enforced any time a user creates or edits a movie.

## Validation

A key tenet of software development is called [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("**D**on't **R**epeat **Y**ourself"). Razor Pages encourages development where functionality is specified once, and it's reflected throughout the app. DRY can help:

* Reduce the amount of code in an app.
* Make the code less error prone, and easier to test and maintain.

The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle:

* Validation rules are declaratively specified in one place, in the model class.
* Rules are enforced everywhere in the app.

## Add validation rules to the movie model

The `DataAnnotations` namespace provides:

* A set of built-in validation attributes that are applied declaratively to a class or property.
* Formatting attributes like `[DataType]` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in `[Required]`, `[StringLength]`, `[RegularExpression]`, and `[Range]` validation attributes.

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}
```

The validation attributes specify behavior to enforce on the model properties they're applied to:

* The `[Required]` and `[MinimumLength]` attributes indicate that a property must have a value. Nothing prevents a user from entering white space to satisfy this validation.

* The `[RegularExpression]` attribute is used to limit what characters can be input. In the preceding code, `Genre`:

  * Must only use letters.
  * The first letter is required to be uppercase. White spaces are allowed while numbers, and special characters are not allowed.

* The `RegularExpression` `Rating`:

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a `Genre`.

* The `[Range]` attribute constrains a value to within a specified range.

* The `[StringLength]` attribute can set a maximum length of a string property, and optionally its minimum length.

* Value types, such as `decimal`, `int`, `float`, `DateTime`, are inherently required and don't need the `[Required]` attribute.

The preceding validation rules are used for demonstration, they are not optimal for a production system. For example, the preceding prevents entering a movie with only two chars and doesn't allow special characters in `Genre`.

Having validation rules automatically enforced by ASP.NET Core helps:

* Make the app more robust.
* Reduce chances of saving invalid data to the database.

### Validation Error UI in Razor Pages

Run the app and navigate to Pages/Movies.

Select the **Create New** link. Complete the form with some invalid values. When jQuery client-side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered a validation error message in each field containing an invalid value. The errors are enforced both client-side, using JavaScript and jQuery, and server-side, when a user has JavaScript disabled.

A significant benefit is that **no** code changes were necessary in the Create or Edit pages. Once data annotations were applied to the model, the validation UI was enabled. The Razor Pages created in this tutorial automatically picked up the validation rules, using validation attributes on the properties of the `Movie` model class. Test validation using the Edit page, the same validation is applied.

The form data isn't posted to the server until there are no client-side validation errors. Verify form data isn't posted by one or more of the following approaches:

* Put a break point in the `OnPostAsync` method. Submit the form by selecting **Create** or **Save**. The break point is never hit.
* Use the [Fiddler tool](https://www.telerik.com/fiddler).
* Use the browser developer tools to monitor network traffic.

### Server-side validation

When JavaScript is disabled in the browser, submitting the form with errors will post to the server.

Optional, test server-side validation:

1. Disable JavaScript in the browser. JavaScript can be disabled using browser's developer tools. If JavaScript cannot be disabled in the browser, try another browser.

2. Set a break point in the `OnPostAsync` method of the Create or Edit page.

3. Submit a form with invalid data.

4. Verify the model state is invalid:

   ```
    if (!ModelState.IsValid)
    {
       return Page();
    }
   ```

Alternatively, [Disable client-side validation on the server](../../mvc/models/validation?view=aspnetcore-10.0#disable-client-side-validation).

The following code shows a portion of the `Create.cshtml` page scaffolded earlier in the tutorial. It's used by the Create and Edit pages to:

* Display the initial form.
* Redisplay the form in the event of an error.

```
<form method="post">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="Movie.Title" class="control-label"></label>
        <input asp-for="Movie.Title" class="form-control" />
        <span asp-validation-for="Movie.Title" class="text-danger"></span>
    </div>
```

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

The Create and Edit pages have no validation rules in them. The validation rules and the error strings are specified only in the `Movie` class. These validation rules are automatically applied to Razor Pages that edit the `Movie` model.

When validation logic needs to change, it's done only in the model. Validation is applied consistently throughout the application, validation logic is defined in one place. Validation in one place helps keep the code clean, and makes it easier to maintain and update.

## Use DataType Attributes

Examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. The `[DataType]` attribute is applied to the `ReleaseDate` and `Price` properties.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
```

The `[DataType]` attributes provide:

* Hints for the view engine to format the data.
* Supplies attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email.

Use the `[RegularExpression]` attribute to validate the format of the data. The `[DataType]` attribute is used to specify a data type that's more specific than the database intrinsic type. `[DataType]` attributes aren't validation attributes. In the sample application, only the date is displayed, without time.

The `DataType` enumeration provides many data types, such as `Date`, `Time`, `PhoneNumber`, `Currency`, `EmailAddress`, and more.

The `[DataType]` attributes:

* Can enable the application to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`.
* Can provide a date selector `DataType.Date` in browsers that support HTML5.
* Emit HTML 5 `data-`, pronounced "data dash", attributes that HTML 5 browsers consume.
* Do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

The `[DisplayFormat]` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting will be applied when the value is displayed for editing. That behavior may not be wanted for some fields. For example, in currency values, the currency symbol is usually not wanted in the edit UI.

The `[DisplayFormat]` attribute can be used by itself, but it's generally a good idea to use the `[DataType]` attribute. The `[DataType]` attribute conveys the semantics of the data as opposed to how to render it on a screen. The `[DataType]` attribute provides the following benefits that aren't available with `[DisplayFormat]`:

* The browser can enable HTML5 features, for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.
* By default, the browser renders data using the correct format based on its locale.
* The `[DataType]` attribute can enable the ASP.NET Core framework to choose the right field template to render the data. The `DisplayFormat`, if used by itself, uses the string template.

**Note:** jQuery validation doesn't work with the `[Range]` attribute and `DateTime`. For example, the following code will always display a client-side validation error, even when the date is in the specified range:

```
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
```

It's a best practice to avoid compiling hard dates in models, so using the `[Range]` attribute and `DateTime` is discouraged. Use [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) for date ranges and other values that are subject to frequent change rather than specifying it in code.

The following code shows combining attributes on one line:

```
public class Movie
{
    public int ID { get; set; }

    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; }

    [Display(Name = "Release Date"), DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
    public string Genre { get; set; }

    [Range(1, 100), DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string Rating { get; set; }
}
```

[Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) shows advanced EF Core operations with Razor Pages.

### Apply migrations

The DataAnnotations applied to the class changes the schema. For example, the DataAnnotations applied to the `Title` field:

```
[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; }
```

* Limits the characters to 60.
* Doesn't allow a `null` value.

The `Movie` table currently has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (MAX)  NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (MAX)  NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (MAX)  NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

The preceding schema changes don't cause EF to throw an exception. However, create a migration so the schema is consistent with the model.

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**. In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

`Update-Database` runs the `Up` methods of the `New_DataAnnotations` class. Examine the `Up` method:

```
public partial class New_DataAnnotations : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AlterColumn<string>(
            name: "Title",
            table: "Movie",
            maxLength: 60,
            nullable: false,
            oldClrType: typeof(string),
            oldNullable: true);

        migrationBuilder.AlterColumn<string>(
            name: "Rating",
            table: "Movie",
            maxLength: 5,
            nullable: false,
            oldClrType: typeof(string),
            oldNullable: true);

        migrationBuilder.AlterColumn<string>(
            name: "Genre",
            table: "Movie",
            maxLength: 30,
            nullable: false,
            oldClrType: typeof(string),
            oldNullable: true);
    }
```

The updated `Movie` table has the following schema:

```
CREATE TABLE [dbo].[Movie] (
    [ID]          INT             IDENTITY (1, 1) NOT NULL,
    [Title]       NVARCHAR (60)   NOT NULL,
    [ReleaseDate] DATETIME2 (7)   NOT NULL,
    [Genre]       NVARCHAR (30)   NOT NULL,
    [Price]       DECIMAL (18, 2) NOT NULL,
    [Rating]      NVARCHAR (5)    NOT NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([ID] ASC)
);
```

Note

The preceding `Up` method and schema show SQL Server output. If you're using SQLite instead of SQL Server, the generated migration and schema differ. SQLite has a minimal type system that maps `string` properties to `TEXT` and doesn't apply the `[StringLength]` maximum length to the schema. Columns such as `Title`, `Genre`, and `Rating` are created as `TEXT` with no length. The maximum length is still enforced by ASP.NET Core model validation, not by the database schema. For more information, see [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations).

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core app in Azure with SQL Database](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

Thanks for completing this introduction to Razor Pages. [Get started with Razor Pages and EF Core](../../data/ef-rp/intro?view=aspnetcore-10.0) is an excellent follow up to this tutorial.

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

## Related content

* [Tag Helpers in forms in ASP.NET Core](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization in ASP.NET Core](../../fundamentals/localization?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

## Next steps

[Previous: Add a new field](new-field?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-06

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/advanced/custom-model-binding.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Custom Model Binding in ASP.NET Core

By [Kirk Larkin](https://twitter.com/serpent5)

Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. Mapping between incoming request data and application models is handled by model binders. Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/advanced/custom-model-binding/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Default model binder limitations

The default model binders support most of the common .NET Core data types and should meet most developers' needs. They expect to bind text-based input from the request directly to model types. You might need to transform the input prior to binding it. For example, when you have a key that can be used to look up model data. You can use a custom model binder to fetch data based on the key.

## Model binding simple and complex types

Model binding uses specific definitions for the types it operates on. A *simple type* is converted from a single string using [TypeConverter](/en-us/dotnet/api/system.componentmodel.typeconverter) or a `TryParse` method. A *complex type* is converted from multiple input values. The framework determines the difference based on the existence of a `TypeConverter` or `TryParse`. We recommend creating a type converter or using `TryParse` for a `string` to `SomeType` conversion that doesn't require external resources or multiple inputs.

See [Simple types](../models/model-binding?view=aspnetcore-10.0#simp7) for a list of types that the model binder can convert from strings.

Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented. Consider the [ByteArrayModelBinder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.binders.bytearraymodelbinder) which can be used to convert base64-encoded strings into byte arrays. The byte arrays are often stored as files or database BLOB fields.

### Working with the ByteArrayModelBinder

Base64-encoded strings can be used to represent binary data. For example, an image can be encoded as a string. The sample includes an image as a base64-encoded string in [Base64String.txt](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/advanced/custom-model-binding/samples/3.x/CustomModelBindingSample/Base64String.txt).

ASP.NET Core MVC can take a base64-encoded string and use a `ByteArrayModelBinder` to convert it into a byte array. The [ByteArrayModelBinderProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.binders.bytearraymodelbinderprovider) maps `byte[]` arguments to `ByteArrayModelBinder`:

```
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
        return new ByteArrayModelBinder(loggerFactory);
    }

    return null;
}
```

When creating your own custom model binder, you can implement your own `IModelBinderProvider` type, or use the [ModelBinderAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinderattribute).

The following example shows how to use `ByteArrayModelBinder` to convert a base64-encoded string to a `byte[]` and save the result to a file:

```
[HttpPost]
public void Post([FromForm] byte[] file, string filename)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, file);
}
```

You can POST a base64-encoded string to the previous API method using a tool like [curl](https://curl.haxx.se/).

As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. The following example shows how to use `ByteArrayModelBinder` with a view model:

```
[HttpPost("Profile")]
public void SaveProfile([FromForm] ProfileViewModel model)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel
{
    public byte[] File { get; set; }
    public string FileName { get; set; }
}
```

## Custom model binder sample

In this section, we'll implement a custom model binder that:

* Converts incoming request data into strongly typed key arguments.
* Uses Entity Framework Core to fetch the associated entity.
* Passes the associated entity as an argument to the action method.

The following sample uses the `ModelBinder` attribute on the `Author` model:

```
using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

namespace CustomModelBindingSample.Data
{
    [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
    public class Author
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string GitHub { get; set; }
        public string Twitter { get; set; }
        public string BlogUrl { get; set; }
    }
}
```

In the preceding code, the `ModelBinder` attribute specifies the type of `IModelBinder` that should be used to bind `Author` action parameters.

The following `AuthorEntityBinder` class binds an `Author` parameter by using the `author` route value to fetch the entity from a data source with Entity Framework Core:

```
public class AuthorEntityBinder : IModelBinder
{
    private readonly AuthorContext _context;

    public AuthorEntityBinder(AuthorContext context)
    {
        _context = context;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        if (!int.TryParse(value, out var id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                modelName, "Author Id must be an integer.");

            return Task.CompletedTask;
        }

        // Model will be null if not found, including for
        // out of range id values (0, -3, etc.)
        var model = _context.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}
```

Note

The preceding `AuthorEntityBinder` class is intended to illustrate a custom model binder. The class isn't intended to illustrate best practices for a lookup scenario. For lookup, bind the `author` parameter directly and query the database in an action method. This approach separates model binding failures from `NotFound` cases.

The following code shows how to use the `AuthorEntityBinder` in an action method:

```
[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}
```

The `ModelBinder` attribute can be used to apply the `AuthorEntityBinder` to parameters that don't use default conventions:

```
[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}
```

In this example, since the route parameter name (`id`) doesn't match the action method parameter name (`author`), the `Name` property on the `ModelBinder` attribute is used to specify which route value to bind. Both the controller and action method are simplified compared to looking up the entity in the action method. The logic to fetch the author using Entity Framework Core is moved to the model binder. This can be a considerable simplification when you have several methods that bind to the `Author` model.

You can apply the `ModelBinder` attribute to individual model properties (such as on a viewmodel) or to action method parameters to specify a certain model binder or model name for just that type or action.

### Implementing a ModelBinderProvider

Instead of applying an attribute, you can implement `IModelBinderProvider`. This is how the built-in framework binders are implemented. When you specify the type your binder operates on, you specify the type of argument it produces, **not** the input your binder accepts. The following binder provider works with the `AuthorEntityBinder`. When it's added to MVC's collection of providers, you don't need to use the `ModelBinder` attribute on `Author` or `Author`-typed parameters.

```
using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
    public class AuthorEntityBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(Author))
            {
                return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
            }

            return null;
        }
    }
}
```

Note

The preceding code returns a `BinderTypeModelBinder`. `BinderTypeModelBinder` acts as a factory for model binders and provides dependency injection (DI). The `AuthorEntityBinder` requires DI to access EF Core. Use `BinderTypeModelBinder` if your model binder requires services from DI.

To use a custom model binder provider, add it in `ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AuthorContext>(options => options.UseInMemoryDatabase("Authors"));

    services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}
```

When evaluating model binders, the collection of providers is examined in order. The first provider that returns a binder that matches the input model is used. Adding your provider to the end of the collection may thus result in a built-in model binder being called before your custom binder has a chance. In this example, the custom provider is added to the beginning of the collection to ensure it's always used for `Author` action arguments.

### Polymorphic model binding

Binding to different models of derived types is known as polymorphic model binding. Polymorphic custom model binding is required when the request value must be bound to the specific derived model type. Polymorphic model binding:

* Isn't typical for a REST API that's designed to interoperate with all languages.
* Makes it difficult to reason about the bound models.

However, if an app requires polymorphic model binding, an implementation might look like the following code:

```
public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}
```

## Recommendations and best practices

Custom model binders:

* Shouldn't attempt to set status codes or return results (for example, 404 Not Found). If model binding fails, an [action filter](../controllers/filters?view=aspnetcore-10.0) or logic within the action method itself should handle the failure.
* Are most useful for eliminating repetitive code and cross-cutting concerns from action methods.
* Typically shouldn't be used to convert a string into a custom type, a [TypeConverter](/en-us/dotnet/api/system.componentmodel.typeconverter) is usually a better option.

By [Steve Smith](https://ardalis.com/)

Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. Mapping between incoming request data and application models is handled by model binders. Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/advanced/custom-model-binding/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Default model binder limitations

The default model binders support most of the common .NET Core data types and should meet most developers' needs. They expect to bind text-based input from the request directly to model types. You might need to transform the input prior to binding it. For example, when you have a key that can be used to look up model data. You can use a custom model binder to fetch data based on the key.

## Model binding review

Model binding uses specific definitions for the types it operates on. A *simple type* is converted from a single string in the input. A *complex type* is converted from multiple input values. The framework determines the difference based on the existence of a `TypeConverter`. We recommend you create a type converter if you have a simple `string` -> `SomeType` mapping that doesn't require external resources.

Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented. Consider the [ByteArrayModelBinder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.binders.bytearraymodelbinder) which can be used to convert base64-encoded strings into byte arrays. The byte arrays are often stored as files or database BLOB fields.

### Working with the ByteArrayModelBinder

Base64-encoded strings can be used to represent binary data. For example, an image can be encoded as a string. The sample includes an image as a base64-encoded string in [Base64String.txt](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/advanced/custom-model-binding/samples/2.x/CustomModelBindingSample/Base64String.txt).

ASP.NET Core MVC can take a base64-encoded string and use a `ByteArrayModelBinder` to convert it into a byte array. The [ByteArrayModelBinderProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.binders.bytearraymodelbinderprovider) maps `byte[]` arguments to `ByteArrayModelBinder`:

```
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        return new ByteArrayModelBinder();
    }

    return null;
}
```

When creating your own custom model binder, you can implement your own `IModelBinderProvider` type, or use the [ModelBinderAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinderattribute).

The following example shows how to use `ByteArrayModelBinder` to convert a base64-encoded string to a `byte[]` and save the result to a file:

```
[HttpPost]
public void Post([FromForm] byte[] file, string filename)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, file);
}
```

You can POST a base64-encoded string to the previous api method using a tool like [curl](https://curl.haxx.se/).

As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. The following example shows how to use `ByteArrayModelBinder` with a view model:

```
[HttpPost("Profile")]
public void SaveProfile([FromForm] ProfileViewModel model)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel
{
    public byte[] File { get; set; }
    public string FileName { get; set; }
}
```

## Custom model binder sample

In this section we'll implement a custom model binder that:

* Converts incoming request data into strongly typed key arguments.
* Uses Entity Framework Core to fetch the associated entity.
* Passes the associated entity as an argument to the action method.

The following sample uses the `ModelBinder` attribute on the `Author` model:

```
using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

namespace CustomModelBindingSample.Data
{
    [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
    public class Author
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string GitHub { get; set; }
        public string Twitter { get; set; }
        public string BlogUrl { get; set; }
    }
}
```

In the preceding code, the `ModelBinder` attribute specifies the type of `IModelBinder` that should be used to bind `Author` action parameters.

The following `AuthorEntityBinder` class binds an `Author` parameter by fetching the entity from a data source using Entity Framework Core and an `author` route value:

```
public class AuthorEntityBinder : IModelBinder
{
    private readonly AppDbContext _db;

    public AuthorEntityBinder(AppDbContext db)
    {
        _db = db;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        if (!int.TryParse(value, out var id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                modelName, "Author Id must be an integer.");

            return Task.CompletedTask;
        }

        // Model will be null if not found, including for 
        // out of range id values (0, -3, etc.)
        var model = _db.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}
```

Note

The preceding `AuthorEntityBinder` class is intended to illustrate a custom model binder. The class isn't intended to illustrate best practices for a lookup scenario. For lookup, bind the `author` parameter directly and query the database in an action method. This approach separates model binding failures from `NotFound` cases.

The following code shows how to use the `AuthorEntityBinder` in an action method:

```
[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }
    
    return Ok(author);
}
```

The `ModelBinder` attribute can be used to apply the `AuthorEntityBinder` to parameters that don't use default conventions:

```
[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}
```

In this example, since the route parameter name (`id`) doesn't match the action method parameter name (`author`), the `Name` property on the `ModelBinder` attribute is used to specify which route value to bind. Both the controller and action method are simplified compared to looking up the entity in the action method. The logic to fetch the author using Entity Framework Core is moved to the model binder. This can be a considerable simplification when you have several methods that bind to the `Author` model.

You can apply the `ModelBinder` attribute to individual model properties (such as on a viewmodel) or to action method parameters to specify a certain model binder or model name for just that type or action.

### Implementing a ModelBinderProvider

Instead of applying an attribute, you can implement `IModelBinderProvider`. This is how the built-in framework binders are implemented. When you specify the type your binder operates on, you specify the type of argument it produces, **not** the input your binder accepts. The following binder provider works with the `AuthorEntityBinder`. When it's added to MVC's collection of providers, you don't need to use the `ModelBinder` attribute on `Author` or `Author`-typed parameters.

```
using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
    public class AuthorEntityBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(Author))
            {
                return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
            }

            return null;
        }
    }
}
```

Note

The preceding code returns a `BinderTypeModelBinder`. `BinderTypeModelBinder` acts as a factory for model binders and provides dependency injection (DI). The `AuthorEntityBinder` requires DI to access EF Core. Use `BinderTypeModelBinder` if your model binder requires services from DI.

To use a custom model binder provider, add it in `ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("App"));

    services.AddMvc(options =>
        {
            // add custom binder to beginning of collection
            options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
        })
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
```

When evaluating model binders, the collection of providers is examined in order. The first provider that returns a binder is used. Adding your provider to the end of the collection may result in a built-in model binder being called before your custom binder has a chance. In this example, the custom provider is added to the beginning of the collection to ensure it's used for `Author` action arguments.

### Polymorphic model binding

Binding to different models of derived types is known as polymorphic model binding. Polymorphic custom model binding is required when the request value must be bound to the specific derived model type. Polymorphic model binding:

* Isn't typical for a REST API that's designed to interoperate with all languages.
* Makes it difficult to reason about the bound models.

However, if an app requires polymorphic model binding, an implementation might look like the following code:

```
public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}
```

## Recommendations and best practices

Custom model binders:

* Shouldn't attempt to set status codes or return results (for example, 404 Not Found). If model binding fails, an [action filter](../controllers/filters?view=aspnetcore-10.0) or logic within the action method itself should handle the failure.
* Are most useful for eliminating repetitive code and cross-cutting concerns from action methods.
* Typically shouldn't be used to convert a string into a custom type, a [TypeConverter](/en-us/dotnet/api/system.componentmodel.typeconverter) is usually a better option.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/web-api-javascript
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/web-api-javascript.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Call an ASP.NET Core web API with JavaScript

By [Rick Anderson](https://twitter.com/RickAndMSFT)

This tutorial shows how to call an ASP.NET Core web API with JavaScript, using the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API).

## Prerequisites

* Complete [Tutorial: Create a web API](first-web-api?view=aspnetcore-10.0)
* Familiarity with CSS, HTML, and JavaScript

## Call the web API with JavaScript

In this section, you'll add an HTML page containing forms for creating and managing to-do items. Event handlers are attached to elements on the page. The event handlers result in HTTP requests to the web API's action methods. The Fetch API's `fetch` function initiates each HTTP request.

The `fetch` function returns a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) object, which contains an HTTP response represented as a `Response` object. A common pattern is to extract the JSON response body by invoking the `json` function on the `Response` object. JavaScript updates the page with the details from the web API's response.

The simplest `fetch` call accepts a single parameter representing the route. A second parameter, known as the `init` object, is optional. `init` is used to configure the HTTP request.

1. Configure the app to [serve static files](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles#microsoft-aspnetcore-builder-staticfileextensions-usestaticfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)) and [enable default file mapping](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles#microsoft-aspnetcore-builder-defaultfilesextensions-usedefaultfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)). The following highlighted code is needed in `Program.cs`:

```
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));

var app = builder.Build();

if (builder.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseDefaultFiles();
app.UseStaticFiles();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

1. Create a *wwwroot* folder in the project root.

2. Create a *css* folder inside of the *wwwroot* folder.

3. Create a *js* folder inside of the *wwwroot* folder.

4. Add an HTML file named `index.html` to the *wwwroot* folder. Replace the contents of `index.html` with the following markup:

   ```
   <!DOCTYPE html>
   <html>
   <head>
       <meta charset="UTF-8">
       <title>To-do CRUD</title>
       <link rel="stylesheet" href="css/site.css" />
   </head>
   <body>
       <h1>To-do CRUD</h1>
       <h3>Add</h3>
       <form action="javascript:void(0);" method="POST" onsubmit="addItem()">
           <input type="text" id="add-name" placeholder="New to-do">
           <input type="submit" value="Add">
       </form>

       <div id="editForm">
           <h3>Edit</h3>
           <form action="javascript:void(0);" onsubmit="updateItem()">
               <input type="hidden" id="edit-id">
               <input type="checkbox" id="edit-isComplete">
               <input type="text" id="edit-name">
               <input type="submit" value="Save">
               <a onclick="closeInput()" aria-label="Close">&#10006;</a>
           </form>
       </div>

       <p id="counter"></p>

       <table>
           <tr>
               <th>Is Complete?</th>
               <th>Name</th>
               <th></th>
               <th></th>
           </tr>
           <tbody id="todos"></tbody>
       </table>

       <script src="js/site.js" asp-append-version="true"></script>
       <script type="text/javascript">
           getItems();
       </script>
   </body>
   </html>
   ```

5. Add a CSS file named `site.css` to the *wwwroot/css* folder. Replace the contents of `site.css` with the following styles:

   ```
   input[type='submit'], button, [aria-label] {
       cursor: pointer;
   }

   #editForm {
       display: none;
   }

   table {
       font-family: Arial, sans-serif;
       border: 1px solid;
       border-collapse: collapse;
   }

   th {
       background-color: #f8f8f8;
       padding: 5px;
   }

   td {
       border: 1px solid;
       padding: 5px;
   }
   ```

6. Add a JavaScript file named `site.js` to the *wwwroot/js* folder. Replace the contents of `site.js` with the following code:

   ```
   const uri = 'api/todoitems';
   let todos = [];

   function getItems() {
     fetch(uri)
       .then(response => response.json())
       .then(data => _displayItems(data))
       .catch(error => console.error('Unable to get items.', error));
   }

   function addItem() {
     const addNameTextbox = document.getElementById('add-name');

     const item = {
       isComplete: false,
       name: addNameTextbox.value.trim()
     };

     fetch(uri, {
       method: 'POST',
       headers: {
         'Accept': 'application/json',
         'Content-Type': 'application/json'
       },
       body: JSON.stringify(item)
     })
       .then(response => response.json())
       .then(() => {
         getItems();
         addNameTextbox.value = '';
       })
       .catch(error => console.error('Unable to add item.', error));
   }

   function deleteItem(id) {
     fetch(`${uri}/${id}`, {
       method: 'DELETE'
     })
     .then(() => getItems())
     .catch(error => console.error('Unable to delete item.', error));
   }

   function displayEditForm(id) {
     const item = todos.find(item => item.id === id);
     
     document.getElementById('edit-name').value = item.name;
     document.getElementById('edit-id').value = item.id;
     document.getElementById('edit-isComplete').checked = item.isComplete;
     document.getElementById('editForm').style.display = 'block';
   }

   function updateItem() {
     const itemId = document.getElementById('edit-id').value;
     const item = {
       id: parseInt(itemId, 10),
       isComplete: document.getElementById('edit-isComplete').checked,
       name: document.getElementById('edit-name').value.trim()
     };

     fetch(`${uri}/${itemId}`, {
       method: 'PUT',
       headers: {
         'Accept': 'application/json',
         'Content-Type': 'application/json'
       },
       body: JSON.stringify(item)
     })
     .then(() => getItems())
     .catch(error => console.error('Unable to update item.', error));

     closeInput();

     return false;
   }

   function closeInput() {
     document.getElementById('editForm').style.display = 'none';
   }

   function _displayCount(itemCount) {
     const name = (itemCount === 1) ? 'to-do' : 'to-dos';

     document.getElementById('counter').innerText = `${itemCount} ${name}`;
   }

   function _displayItems(data) {
     const tBody = document.getElementById('todos');
     tBody.innerHTML = '';

     _displayCount(data.length);

     const button = document.createElement('button');

     data.forEach(item => {
       let isCompleteCheckbox = document.createElement('input');
       isCompleteCheckbox.type = 'checkbox';
       isCompleteCheckbox.disabled = true;
       isCompleteCheckbox.checked = item.isComplete;

       let editButton = button.cloneNode(false);
       editButton.innerText = 'Edit';
       editButton.setAttribute('onclick', `displayEditForm(${item.id})`);

       let deleteButton = button.cloneNode(false);
       deleteButton.innerText = 'Delete';
       deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);

       let tr = tBody.insertRow();
       
       let td1 = tr.insertCell(0);
       td1.appendChild(isCompleteCheckbox);

       let td2 = tr.insertCell(1);
       let textNode = document.createTextNode(item.name);
       td2.appendChild(textNode);

       let td3 = tr.insertCell(2);
       td3.appendChild(editButton);

       let td4 = tr.insertCell(3);
       td4.appendChild(deleteButton);
     });

     todos = data;
   }
   ```

A change to the ASP.NET Core project's launch settings might be required to test the HTML page locally:

1. Open *Properties\launchSettings.json*.
2. Remove the `launchUrl` property to force the app to open at `index.html`—the project's default file.

This sample calls all of the CRUD methods of the web API. Following are explanations of the web API requests.

### Get a list of to-do items

In the following code, an HTTP GET request is sent to the *api/todoitems* route:

```
fetch(uri)
  .then(response => response.json())
  .then(data => _displayItems(data))
  .catch(error => console.error('Unable to get items.', error));
```

When the web API returns a successful status code, the `_displayItems` function is invoked. Each to-do item in the array parameter accepted by `_displayItems` is added to a table with **Edit** and **Delete** buttons. If the web API request fails, an error is logged to the browser's console.

### Add a to-do item

In the following code:

* An `item` variable is declared to construct an object literal representation of the to-do item.

* A Fetch request is configured with the following options:

  * `method`—specifies the POST HTTP action verb.
  * `body`—specifies the JSON representation of the request body. The JSON is produced by passing the object literal stored in `item` to the [JSON.stringify](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) function.
  * `headers`—specifies the `Accept` and `Content-Type` HTTP request headers. Both headers are set to `application/json` to specify the media type being received and sent, respectively.

* An HTTP POST request is sent to the *api/todoitems* route.

```
function addItem() {
  const addNameTextbox = document.getElementById('add-name');

  const item = {
    isComplete: false,
    name: addNameTextbox.value.trim()
  };

  fetch(uri, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(item)
  })
    .then(response => response.json())
    .then(() => {
      getItems();
      addNameTextbox.value = '';
    })
    .catch(error => console.error('Unable to add item.', error));
}
```

When the web API returns a successful status code, the `getItems` function is invoked to update the HTML table. If the web API request fails, an error is logged to the browser's console.

### Update a to-do item

Updating a to-do item is similar to adding one; however, there are two significant differences:

* The route is suffixed with the unique identifier of the item to update. For example, *api/todoitems/1*.
* The HTTP action verb is PUT, as indicated by the `method` option.

```
fetch(`${uri}/${itemId}`, {
  method: 'PUT',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(item)
})
.then(() => getItems())
.catch(error => console.error('Unable to update item.', error));
```

### Delete a to-do item

To delete a to-do item, set the request's `method` option to `DELETE` and specify the item's unique identifier in the URL.

```
fetch(`${uri}/${id}`, {
  method: 'DELETE'
})
.then(() => getItems())
.catch(error => console.error('Unable to delete item.', error));
```

Advance to the next tutorial to learn how to generate web API help pages:

[Get started with Swashbuckle and ASP.NET Core](getting-started-with-swashbuckle?view=aspnetcore-10.0)

This tutorial shows how to call an ASP.NET Core web API with JavaScript, using the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API).

## Prerequisites

* Complete [Tutorial: Create a web API](first-web-api?view=aspnetcore-10.0)
* Familiarity with CSS, HTML, and JavaScript

## Call the web API with JavaScript

In this section, you'll add an HTML page containing forms for creating and managing to-do items. Event handlers are attached to elements on the page. The event handlers result in HTTP requests to the web API's action methods. The Fetch API's `fetch` function initiates each HTTP request.

The `fetch` function returns a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) object, which contains an HTTP response represented as a `Response` object. A common pattern is to extract the JSON response body by invoking the `json` function on the `Response` object. JavaScript updates the page with the details from the web API's response.

The simplest `fetch` call accepts a single parameter representing the route. A second parameter, known as the `init` object, is optional. `init` is used to configure the HTTP request.

1. Configure the app to [serve static files](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles#microsoft-aspnetcore-builder-staticfileextensions-usestaticfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)) and [enable default file mapping](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles#microsoft-aspnetcore-builder-defaultfilesextensions-usedefaultfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)). The following highlighted code is needed in the `Configure` method of `Startup.cs`:

   ```
   public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
   {
       if (env.IsDevelopment())
       {
           app.UseDeveloperExceptionPage();
       }

       app.UseDefaultFiles();
       app.UseStaticFiles();

       app.UseHttpsRedirection();

       app.UseRouting();

       app.UseAuthorization();

       app.UseEndpoints(endpoints =>
       {
           endpoints.MapControllers();
       });
   }
   ```

2. Create a *wwwroot* folder in the project root.

3. Create a *css* folder inside of the *wwwroot* folder.

4. Create a *js* folder inside of the *wwwroot* folder.

5. Add an HTML file named `index.html` to the *wwwroot* folder. Replace the contents of `index.html` with the following markup:

   ```
   <!DOCTYPE html>
   <html>
   <head>
       <meta charset="UTF-8">
       <title>To-do CRUD</title>
       <link rel="stylesheet" href="css/site.css" />
   </head>
   <body>
       <h1>To-do CRUD</h1>
       <h3>Add</h3>
       <form action="javascript:void(0);" method="POST" onsubmit="addItem()">
           <input type="text" id="add-name" placeholder="New to-do">
           <input type="submit" value="Add">
       </form>

       <div id="editForm">
           <h3>Edit</h3>
           <form action="javascript:void(0);" onsubmit="updateItem()">
               <input type="hidden" id="edit-id">
               <input type="checkbox" id="edit-isComplete">
               <input type="text" id="edit-name">
               <input type="submit" value="Save">
               <a onclick="closeInput()" aria-label="Close">&#10006;</a>
           </form>
       </div>

       <p id="counter"></p>

       <table>
           <tr>
               <th>Is Complete?</th>
               <th>Name</th>
               <th></th>
               <th></th>
           </tr>
           <tbody id="todos"></tbody>
       </table>

       <script src="js/site.js" asp-append-version="true"></script>
       <script type="text/javascript">
           getItems();
       </script>
   </body>
   </html>
   ```

6. Add a CSS file named `site.css` to the *wwwroot/css* folder. Replace the contents of `site.css` with the following styles:

   ```
   input[type='submit'], button, [aria-label] {
       cursor: pointer;
   }

   #editForm {
       display: none;
   }

   table {
       font-family: Arial, sans-serif;
       border: 1px solid;
       border-collapse: collapse;
   }

   th {
       background-color: #f8f8f8;
       padding: 5px;
   }

   td {
       border: 1px solid;
       padding: 5px;
   }
   ```

7. Add a JavaScript file named `site.js` to the *wwwroot/js* folder. Replace the contents of `site.js` with the following code:

   ```
   const uri = 'api/todoitems';
   let todos = [];

   function getItems() {
     fetch(uri)
       .then(response => response.json())
       .then(data => _displayItems(data))
       .catch(error => console.error('Unable to get items.', error));
   }

   function addItem() {
     const addNameTextbox = document.getElementById('add-name');

     const item = {
       isComplete: false,
       name: addNameTextbox.value.trim()
     };

     fetch(uri, {
       method: 'POST',
       headers: {
         'Accept': 'application/json',
         'Content-Type': 'application/json'
       },
       body: JSON.stringify(item)
     })
       .then(response => response.json())
       .then(() => {
         getItems();
         addNameTextbox.value = '';
       })
       .catch(error => console.error('Unable to add item.', error));
   }

   function deleteItem(id) {
     fetch(`${uri}/${id}`, {
       method: 'DELETE'
     })
     .then(() => getItems())
     .catch(error => console.error('Unable to delete item.', error));
   }

   function displayEditForm(id) {
     const item = todos.find(item => item.id === id);
     
     document.getElementById('edit-name').value = item.name;
     document.getElementById('edit-id').value = item.id;
     document.getElementById('edit-isComplete').checked = item.isComplete;
     document.getElementById('editForm').style.display = 'block';
   }

   function updateItem() {
     const itemId = document.getElementById('edit-id').value;
     const item = {
       id: parseInt(itemId, 10),
       isComplete: document.getElementById('edit-isComplete').checked,
       name: document.getElementById('edit-name').value.trim()
     };

     fetch(`${uri}/${itemId}`, {
       method: 'PUT',
       headers: {
         'Accept': 'application/json',
         'Content-Type': 'application/json'
       },
       body: JSON.stringify(item)
     })
     .then(() => getItems())
     .catch(error => console.error('Unable to update item.', error));

     closeInput();

     return false;
   }

   function closeInput() {
     document.getElementById('editForm').style.display = 'none';
   }

   function _displayCount(itemCount) {
     const name = (itemCount === 1) ? 'to-do' : 'to-dos';

     document.getElementById('counter').innerText = `${itemCount} ${name}`;
   }

   function _displayItems(data) {
     const tBody = document.getElementById('todos');
     tBody.innerHTML = '';

     _displayCount(data.length);

     const button = document.createElement('button');

     data.forEach(item => {
       let isCompleteCheckbox = document.createElement('input');
       isCompleteCheckbox.type = 'checkbox';
       isCompleteCheckbox.disabled = true;
       isCompleteCheckbox.checked = item.isComplete;

       let editButton = button.cloneNode(false);
       editButton.innerText = 'Edit';
       editButton.setAttribute('onclick', `displayEditForm(${item.id})`);

       let deleteButton = button.cloneNode(false);
       deleteButton.innerText = 'Delete';
       deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);

       let tr = tBody.insertRow();
       
       let td1 = tr.insertCell(0);
       td1.appendChild(isCompleteCheckbox);

       let td2 = tr.insertCell(1);
       let textNode = document.createTextNode(item.name);
       td2.appendChild(textNode);

       let td3 = tr.insertCell(2);
       td3.appendChild(editButton);

       let td4 = tr.insertCell(3);
       td4.appendChild(deleteButton);
     });

     todos = data;
   }
   ```

A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally:

1. Open *Properties\launchSettings.json*.
2. Remove the `launchUrl` property to force the app to open at `index.html`—the project's default file.

This sample calls all of the CRUD methods of the web API. Following are explanations of the web API requests.

### Get a list of to-do items

In the following code, an HTTP GET request is sent to the *api/todoitems* route:

```
fetch(uri)
  .then(response => response.json())
  .then(data => _displayItems(data))
  .catch(error => console.error('Unable to get items.', error));
```

When the web API returns a successful status code, the `_displayItems` function is invoked. Each to-do item in the array parameter accepted by `_displayItems` is added to a table with **Edit** and **Delete** buttons. If the web API request fails, an error is logged to the browser's console.

### Add a to-do item

In the following code:

* An `item` variable is declared to construct an object literal representation of the to-do item.

* A Fetch request is configured with the following options:

  * `method`—specifies the POST HTTP action verb.
  * `body`—specifies the JSON representation of the request body. The JSON is produced by passing the object literal stored in `item` to the [JSON.stringify](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) function.
  * `headers`—specifies the `Accept` and `Content-Type` HTTP request headers. Both headers are set to `application/json` to specify the media type being received and sent, respectively.

* An HTTP POST request is sent to the *api/todoitems* route.

```
function addItem() {
  const addNameTextbox = document.getElementById('add-name');

  const item = {
    isComplete: false,
    name: addNameTextbox.value.trim()
  };

  fetch(uri, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(item)
  })
    .then(response => response.json())
    .then(() => {
      getItems();
      addNameTextbox.value = '';
    })
    .catch(error => console.error('Unable to add item.', error));
}
```

When the web API returns a successful status code, the `getItems` function is invoked to update the HTML table. If the web API request fails, an error is logged to the browser's console.

### Update a to-do item

Updating a to-do item is similar to adding one; however, there are two significant differences:

* The route is suffixed with the unique identifier of the item to update. For example, *api/todoitems/1*.
* The HTTP action verb is PUT, as indicated by the `method` option.

```
fetch(`${uri}/${itemId}`, {
  method: 'PUT',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(item)
})
.then(() => getItems())
.catch(error => console.error('Unable to update item.', error));
```

### Delete a to-do item

To delete a to-do item, set the request's `method` option to `DELETE` and specify the item's unique identifier in the URL.

```
fetch(`${uri}/${id}`, {
  method: 'DELETE'
})
.then(() => getItems())
.catch(error => console.error('Unable to delete item.', error));
```

Advance to the next tutorial to learn how to generate web API help pages:

[Get started with Swashbuckle and ASP.NET Core](getting-started-with-swashbuckle?view=aspnetcore-10.0)

This tutorial shows how to call an ASP.NET Core web API with JavaScript, using the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API).

## Prerequisites

* Complete [Tutorial: Create a web API](first-web-api?view=aspnetcore-10.0)
* Familiarity with CSS, HTML, and JavaScript

## Call the web API with JavaScript

In this section, you'll add an HTML page containing forms for creating and managing to-do items. Event handlers are attached to elements on the page. The event handlers result in HTTP requests to the web API's action methods. The Fetch API's `fetch` function initiates each HTTP request.

The `fetch` function returns a [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) object, which contains an HTTP response represented as a `Response` object. A common pattern is to extract the JSON response body by invoking the `json` function on the `Response` object. JavaScript updates the page with the details from the web API's response.

The simplest `fetch` call accepts a single parameter representing the route. A second parameter, known as the `init` object, is optional. `init` is used to configure the HTTP request.

1. Configure the app to [serve static files](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles#microsoft-aspnetcore-builder-staticfileextensions-usestaticfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)) and [enable default file mapping](/en-us/dotnet/api/microsoft.aspnetcore.builder.defaultfilesextensions.usedefaultfiles#microsoft-aspnetcore-builder-defaultfilesextensions-usedefaultfiles\(microsoft-aspnetcore-builder-iapplicationbuilder\)). The following highlighted code is needed in `Program.cs`:

```
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));

var app = builder.Build();

if (builder.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseDefaultFiles();
app.UseStaticFiles();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

1. Create a *wwwroot* folder in the project root.

2. Create a *css* folder inside of the *wwwroot* folder.

3. Create a *js* folder inside of the *wwwroot* folder.

4. Add an HTML file named `index.html` to the *wwwroot* folder. Replace the contents of `index.html` with the following markup:

   ```
   <!DOCTYPE html>
   <html>
   <head>
       <meta charset="UTF-8">
       <title>To-do CRUD</title>
       <link rel="stylesheet" href="css/site.css" />
   </head>
   <body>
       <h1>To-do CRUD</h1>
       <h3>Add</h3>
       <form action="javascript:void(0);" method="POST" onsubmit="addItem()">
           <input type="text" id="add-name" placeholder="New to-do">
           <input type="submit" value="Add">
       </form>

       <div id="editForm">
           <h3>Edit</h3>
           <form action="javascript:void(0);" onsubmit="updateItem()">
               <input type="hidden" id="edit-id">
               <input type="checkbox" id="edit-isComplete">
               <input type="text" id="edit-name">
               <input type="submit" value="Save">
               <a onclick="closeInput()" aria-label="Close">&#10006;</a>
           </form>
       </div>

       <p id="counter"></p>

       <table>
           <tr>
               <th>Is Complete?</th>
               <th>Name</th>
               <th></th>
               <th></th>
           </tr>
           <tbody id="todos"></tbody>
       </table>

       <script src="js/site.js" asp-append-version="true"></script>
       <script type="text/javascript">
           getItems();
       </script>
   </body>
   </html>
   ```

5. Add a CSS file named `site.css` to the *wwwroot/css* folder. Replace the contents of `site.css` with the following styles:

   ```
   input[type='submit'], button, [aria-label] {
       cursor: pointer;
   }

   #editForm {
       display: none;
   }

   table {
       font-family: Arial, sans-serif;
       border: 1px solid;
       border-collapse: collapse;
   }

   th {
       background-color: #f8f8f8;
       padding: 5px;
   }

   td {
       border: 1px solid;
       padding: 5px;
   }
   ```

6. Add a JavaScript file named `site.js` to the *wwwroot/js* folder. Replace the contents of `site.js` with the following code:

   ```
   const uri = 'api/todoitems';
   let todos = [];

   function getItems() {
     fetch(uri)
       .then(response => response.json())
       .then(data => _displayItems(data))
       .catch(error => console.error('Unable to get items.', error));
   }

   function addItem() {
     const addNameTextbox = document.getElementById('add-name');

     const item = {
       isComplete: false,
       name: addNameTextbox.value.trim()
     };

     fetch(uri, {
       method: 'POST',
       headers: {
         'Accept': 'application/json',
         'Content-Type': 'application/json'
       },
       body: JSON.stringify(item)
     })
       .then(response => response.json())
       .then(() => {
         getItems();
         addNameTextbox.value = '';
       })
       .catch(error => console.error('Unable to add item.', error));
   }

   function deleteItem(id) {
     fetch(`${uri}/${id}`, {
       method: 'DELETE'
     })
     .then(() => getItems())
     .catch(error => console.error('Unable to delete item.', error));
   }

   function displayEditForm(id) {
     const item = todos.find(item => item.id === id);
     
     document.getElementById('edit-name').value = item.name;
     document.getElementById('edit-id').value = item.id;
     document.getElementById('edit-isComplete').checked = item.isComplete;
     document.getElementById('editForm').style.display = 'block';
   }

   function updateItem() {
     const itemId = document.getElementById('edit-id').value;
     const item = {
       id: parseInt(itemId, 10),
       isComplete: document.getElementById('edit-isComplete').checked,
       name: document.getElementById('edit-name').value.trim()
     };

     fetch(`${uri}/${itemId}`, {
       method: 'PUT',
       headers: {
         'Accept': 'application/json',
         'Content-Type': 'application/json'
       },
       body: JSON.stringify(item)
     })
     .then(() => getItems())
     .catch(error => console.error('Unable to update item.', error));

     closeInput();

     return false;
   }

   function closeInput() {
     document.getElementById('editForm').style.display = 'none';
   }

   function _displayCount(itemCount) {
     const name = (itemCount === 1) ? 'to-do' : 'to-dos';

     document.getElementById('counter').innerText = `${itemCount} ${name}`;
   }

   function _displayItems(data) {
     const tBody = document.getElementById('todos');
     tBody.innerHTML = '';

     _displayCount(data.length);

     const button = document.createElement('button');

     data.forEach(item => {
       let isCompleteCheckbox = document.createElement('input');
       isCompleteCheckbox.type = 'checkbox';
       isCompleteCheckbox.disabled = true;
       isCompleteCheckbox.checked = item.isComplete;

       let editButton = button.cloneNode(false);
       editButton.innerText = 'Edit';
       editButton.setAttribute('onclick', `displayEditForm(${item.id})`);

       let deleteButton = button.cloneNode(false);
       deleteButton.innerText = 'Delete';
       deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);

       let tr = tBody.insertRow();
       
       let td1 = tr.insertCell(0);
       td1.appendChild(isCompleteCheckbox);

       let td2 = tr.insertCell(1);
       let textNode = document.createTextNode(item.name);
       td2.appendChild(textNode);

       let td3 = tr.insertCell(2);
       td3.appendChild(editButton);

       let td4 = tr.insertCell(3);
       td4.appendChild(deleteButton);
     });

     todos = data;
   }
   ```

A change to the ASP.NET Core project's launch settings may be required to test the HTML page locally:

1. Open *Properties\launchSettings.json*.
2. Remove the `launchUrl` property to force the app to open at `index.html`—the project's default file.

This sample calls all of the CRUD methods of the web API. Following are explanations of the web API requests.

### Get a list of to-do items

In the following code, an HTTP GET request is sent to the *api/todoitems* route:

```
fetch(uri)
  .then(response => response.json())
  .then(data => _displayItems(data))
  .catch(error => console.error('Unable to get items.', error));
```

When the web API returns a successful status code, the `_displayItems` function is invoked. Each to-do item in the array parameter accepted by `_displayItems` is added to a table with **Edit** and **Delete** buttons. If the web API request fails, an error is logged to the browser's console.

### Add a to-do item

In the following code:

* An `item` variable is declared to construct an object literal representation of the to-do item.

* A Fetch request is configured with the following options:

  * `method`—specifies the POST HTTP action verb.
  * `body`—specifies the JSON representation of the request body. The JSON is produced by passing the object literal stored in `item` to the [JSON.stringify](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) function.
  * `headers`—specifies the `Accept` and `Content-Type` HTTP request headers. Both headers are set to `application/json` to specify the media type being received and sent, respectively.

* An HTTP POST request is sent to the *api/todoitems* route.

```
function addItem() {
  const addNameTextbox = document.getElementById('add-name');

  const item = {
    isComplete: false,
    name: addNameTextbox.value.trim()
  };

  fetch(uri, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(item)
  })
    .then(response => response.json())
    .then(() => {
      getItems();
      addNameTextbox.value = '';
    })
    .catch(error => console.error('Unable to add item.', error));
}
```

When the web API returns a successful status code, the `getItems` function is invoked to update the HTML table. If the web API request fails, an error is logged to the browser's console.

### Update a to-do item

Updating a to-do item is similar to adding one; however, there are two significant differences:

* The route is suffixed with the unique identifier of the item to update. For example, *api/todoitems/1*.
* The HTTP action verb is PUT, as indicated by the `method` option.

```
fetch(`${uri}/${itemId}`, {
  method: 'PUT',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(item)
})
.then(() => getItems())
.catch(error => console.error('Unable to update item.', error));
```

### Delete a to-do item

To delete a to-do item, set the request's `method` option to `DELETE` and specify the item's unique identifier in the URL.

```
fetch(`${uri}/${id}`, {
  method: 'DELETE'
})
.then(() => getItems())
.catch(error => console.error('Unable to delete item.', error));
```

Advance to the next tutorial to learn how to generate web API help pages:

[Get started with Swashbuckle and ASP.NET Core](getting-started-with-swashbuckle?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/hubcontext.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Send messages from outside a hub

The SignalR hub is the core abstraction for sending messages to clients connected to the SignalR server. It's also possible to send messages from other places in your app using the `IHubContext` service. This article explains how to access a SignalR `IHubContext` to send notifications to clients from outside a hub.

Note

The `IHubContext` is for sending notifications to clients, it is not used to call methods on the `Hub`.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/hubcontext/samples/) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

## Get an instance of `IHubContext`

In ASP.NET Core SignalR, you can access an instance of `IHubContext` via dependency injection. You can inject an instance of `IHubContext` into a controller, middleware, or other DI service. Use the instance to send messages to clients.

### Inject an instance of `IHubContext` in a controller

You can inject an instance of `IHubContext` into a controller by adding it to your constructor:

```
public class HomeController : Controller
{
    private readonly IHubContext<NotificationHub> _hubContext;

    public HomeController(IHubContext<NotificationHub> hubContext)
    {
        _hubContext = hubContext;
    }
}
```

With access to an instance of `IHubContext`, call client methods as if you were in the hub itself:

```
public async Task<IActionResult> Index()
{
    await _hubContext.Clients.All.SendAsync("Notify", $"Home page loaded at: {DateTime.Now}");
    return View();
}
```

### Get an instance of `IHubContext` in middleware

Access the `IHubContext` within the middleware pipeline like so:

```
app.Use(async (context, next) =>
{
    var hubContext = context.RequestServices
                            .GetRequiredService<IHubContext<ChatHub>>();
    //...
    
    if (next != null)
    {
        await next.Invoke();
    }
});
```

Note

When client methods are called from outside of the `Hub` class, there's no caller associated with the invocation. Therefore, there's no access to the `ConnectionId`, `Caller`, and `Others` properties.

Apps that need to map a user to the connection ID and persist that mapping can do one of the following:

* Persist mapping of single or multiple connections as groups. See [Groups in SignalR](groups?view=aspnetcore-10.0#groups-in-signalr) for more information.

* Retain connection and user information through a singleton service. See [Inject services into a hub](hubs?view=aspnetcore-10.0#inject-services-into-a-hub) for more information. The singleton service can use any storage method, such as:

  * In-memory storage in a dictionary.
  * Permanent external storage. For example, a database or Azure Table storage using the [Azure.Data.Tables NuGet package](https://www.nuget.org/packages/Azure.Data.Tables/).

* Pass the connection ID between clients.

### Get an instance of `IHubContext` from IHost

Accessing an `IHubContext` from the web host is useful for integrating with areas outside of ASP.NET Core, for example, using third-party dependency injection frameworks:

```
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();
            var hubContext = host.Services.GetService(typeof(IHubContext<ChatHub>));
            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => {
                    webBuilder.UseStartup<Startup>();
                });
    }
```

### Inject a strongly-typed HubContext

To inject a strongly-typed HubContext, ensure your Hub inherits from `Hub<T>`. Inject it using the `IHubContext<THub, T>` interface rather than `IHubContext<THub>`.

```
public class ChatController : Controller
{
    public IHubContext<ChatHub, IChatClient> _strongChatHubContext { get; }

    public ChatController(IHubContext<ChatHub, IChatClient> chatHubContext)
    {
        _strongChatHubContext = chatHubContext;
    }

    public async Task SendMessage(string user, string message)
    {
        await _strongChatHubContext.Clients.All.ReceiveMessage(user, message);
    }
}
```

See [Use strongly typed hubs](hubs?view=aspnetcore-10.0#use-strongly-typed-hubs) for more information.

### Use `IHubContext` in generic code

An injected `IHubContext<THub>` instance can be cast to `IHubContext` without a generic `Hub` type specified.

```
class MyHub : Hub
{ }

class MyOtherHub : Hub
{ }

app.Use(async (context, next) =>
{
    var myHubContext = context.RequestServices
                            .GetRequiredService<IHubContext<MyHub>>();
    var myOtherHubContext = context.RequestServices
                            .GetRequiredService<IHubContext<MyOtherHub>>();
    await CommonHubContextMethod((IHubContext)myHubContext);
    await CommonHubContextMethod((IHubContext)myOtherHubContext);

    await next.Invoke();
}

async Task CommonHubContextMethod(IHubContext context)
{
    await context.Clients.All.SendAsync("clientMethod", new Args());
}
```

This is useful when:

* Writing libraries that don't have a reference to the specific `Hub` type the app is using.
* Writing code that is generic and can apply to multiple different `Hub` implementations

## Related content

* [SignalR assemblies in shared framework](../migration/22-to-30?view=aspnetcore-10.0#signalr-assemblies-in-shared-framework)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/use-http-context
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/use-http-context.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use HttpContext in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) encapsulates all information about an individual HTTP request and response. An `HttpContext` instance is initialized when an HTTP request is received. The `HttpContext` instance is accessible by middleware and app frameworks such as Blazor Web Apps, Web API controllers, Razor Pages, SignalR, gRPC, and more.

## `HttpRequest`

[HttpContext.Request](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.request#microsoft-aspnetcore-http-httpcontext-request) provides access to [HttpRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest). `HttpRequest` has information about the incoming HTTP request, and it's initialized when an HTTP request is received by the server. `HttpRequest` isn't read-only, and middleware can change request values in the middleware pipeline.

Commonly used members on `HttpRequest` include:

| Property                                                                                                                                                                                       | Description                                                                                                                                                                                                                                                                                 | Example                                     |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| [HttpRequest.Path](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.path#microsoft-aspnetcore-http-httprequest-path)                                                                    | The request path.                                                                                                                                                                                                                                                                           | `/en/article/getstarted`                    |
| [HttpRequest.Method](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.method#microsoft-aspnetcore-http-httprequest-method)                                                              | The request method.                                                                                                                                                                                                                                                                         | `GET`                                       |
| [HttpRequest.Headers](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.headers#microsoft-aspnetcore-http-httprequest-headers)                                                           | A collection of request headers.                                                                                                                                                                                                                                                            | `user-agent=Edge` `x-custom-header=MyValue` |
| [HttpRequest.RouteValues](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.routevalues#microsoft-aspnetcore-http-httprequest-routevalues)                                               | A collection of route values. The collection is set when the request is matched to a route.                                                                                                                                                                                                 | `language=en` `article=getstarted`          |
| [HttpRequest.Query](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.query#microsoft-aspnetcore-http-httprequest-query)                                                                 | A collection of query values parsed from [QueryString](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.querystring#microsoft-aspnetcore-http-httprequest-querystring).                                                                                                              | `filter=hello` `page=1`                     |
| [HttpRequest.ReadFormAsync()](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.readformasync#microsoft-aspnetcore-http-httprequest-readformasync\(system-threading-cancellationtoken\)) | A method that reads the request body as a form and returns a form values collection. For information about why `ReadFormAsync` should be used to access form data, see [Prefer ReadFormAsync over Request.Form](best-practices?view=aspnetcore-10.0#prefer-readformasync-over-requestform). | `email=user@contoso.com`                    |
| [HttpRequest.Body](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.body#microsoft-aspnetcore-http-httprequest-body)                                                                    | A [Stream](/en-us/dotnet/api/system.io.stream) for reading the request body.                                                                                                                                                                                                                | UTF-8 JSON payload                          |

### Get request headers

[HttpRequest.Headers](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.headers#microsoft-aspnetcore-http-httprequest-headers) provides access to the request headers sent with the HTTP request. There are two ways to access headers using this collection:

* Provide the header name to the indexer on the header collection. The header name isn't case-sensitive. The indexer can access any header value.
* The header collection also has properties for getting and setting commonly used HTTP headers. The properties provide a fast, IntelliSense driven way to access headers.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", (HttpRequest request) =>
{
    var userAgent = request.Headers.UserAgent;
    var customHeader = request.Headers["x-custom-header"];

    return Results.Ok(new { userAgent = userAgent, customHeader = customHeader });
});

app.Run();
```

For information on efficiently handling headers that appear more than once, see [A brief look at StringValues](https://andrewlock.net/a-brief-look-at-stringvalues/).

### Read request body

An HTTP request can include a request body. The request body is data associated with the request, such as the content of an HTML form, UTF-8 JSON payload, or a file.

[HttpRequest.Body](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.body#microsoft-aspnetcore-http-httprequest-body) allows the request body to be read with [Stream](/en-us/dotnet/api/system.io.stream):

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpContext context) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await context.Request.Body.CopyToAsync(writeStream);
});

app.Run();
```

`HttpRequest.Body` can be read directly or used with other APIs that accept stream.

Note

[Minimal APIs](minimal-apis?view=aspnetcore-10.0) supports binding [HttpRequest.Body](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.body#microsoft-aspnetcore-http-httprequest-body) directly to a [Stream](/en-us/dotnet/api/system.io.stream) parameter.

#### Enable request body buffering

The request body can only be read once, from beginning to end. Forward-only reading of the request body avoids the overhead of buffering the entire request body and reduces memory usage. However, in some scenarios, there's a need to read the request body multiple times. For example, middleware might need to read the request body and then rewind it so it's available for the endpoint.

The [EnableBuffering](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequestrewindextensions.enablebuffering) extension method enables buffering of the HTTP request body and is the recommended way to enable multiple reads. Because a request can be any size, `EnableBuffering` supports options for buffering large request bodies to disk, or rejecting them entirely.

The middleware in the following example:

* Enables multiple reads with `EnableBuffering`. It must be called before reading the request body.
* Reads the request body.
* Rewinds the request body to the start so other middleware or the endpoint can read it.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    context.Request.EnableBuffering();
    await ReadRequestBody(context.Request.Body);
    context.Request.Body.Position = 0;
    
    await next.Invoke();
});

app.Run();
```

#### BodyReader

An alternative way to read the request body is to use the [HttpRequest.BodyReader](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader#microsoft-aspnetcore-http-httprequest-bodyreader) property. The `BodyReader` property exposes the request body as a [PipeReader](/en-us/dotnet/api/system.io.pipelines.pipereader). This API is from [I/O pipelines](/en-us/dotnet/standard/io/pipelines), an advanced, high-performance way to read the request body.

The reader directly accesses the request body and manages memory on the caller's behalf. Unlike `HttpRequest.Body`, the reader doesn't copy request data into a buffer. However, a reader is more complicated to use than a stream and should be used with caution.

For information on how to read content from `BodyReader`, see [I/O pipelines PipeReader](/en-us/dotnet/standard/io/pipelines#pipereader).

## `HttpResponse`

[HttpContext.Response](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.response#microsoft-aspnetcore-http-httpcontext-response) provides access to [HttpResponse](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse). `HttpResponse` is used to set information on the HTTP response sent back to the client.

Commonly used members on `HttpResponse` include:

| Property                                                                                                                                            | Description                                                                          | Example                                    |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------ |
| [HttpResponse.StatusCode](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.statuscode#microsoft-aspnetcore-http-httpresponse-statuscode)    | The response code. Must be set before writing to the response body.                  | `200`                                      |
| [HttpResponse.ContentType](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.contenttype#microsoft-aspnetcore-http-httpresponse-contenttype) | The response `content-type` header. Must be set before writing to the response body. | `application/json`                         |
| [HttpResponse.Headers](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.headers#microsoft-aspnetcore-http-httpresponse-headers)             | A collection of response headers. Must be set before writing to the response body.   | `server=Kestrel` `x-custom-header=MyValue` |
| [HttpResponse.Body](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.body#microsoft-aspnetcore-http-httpresponse-body)                      | A [Stream](/en-us/dotnet/api/system.io.stream) for writing the response body.        | Generated web page                         |

### Set response headers

[HttpResponse.Headers](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.headers#microsoft-aspnetcore-http-httpresponse-headers) provides access to the response headers sent with the HTTP response. There are two ways to access headers using this collection:

* Provide the header name to the indexer on the header collection. The header name isn't case-sensitive. The indexer can access any header value.
* Use the header collection properties for getting and setting commonly used HTTP headers. The properties provide a fast, IntelliSense driven way to access headers.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", (HttpResponse response) =>
{
    response.Headers.CacheControl = "no-cache";
    response.Headers["x-custom-header"] = "Custom value";

    return Results.File(File.OpenRead("helloworld.txt"));
});

app.Run();
```

An app can't modify headers after the response has started. Once the response starts, the headers are sent to the client. A response is started by flushing the response body or calling [HttpResponse.StartAsync(CancellationToken)](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.startasync#microsoft-aspnetcore-http-httpresponse-startasync\(system-threading-cancellationtoken\)). The [HttpResponse.HasStarted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.hasstarted#microsoft-aspnetcore-http-httpresponse-hasstarted) property indicates whether the response has started. An error is thrown when attempting to modify headers after the response has started:

> System.InvalidOperationException: Headers are read-only, response has already started.

Note

Unless response buffering is enabled, all write operations (for example, [WriteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponsewritingextensions.writeasync)) flush the response body internally and mark the response as started. Response buffering is disabled by default.

### Write response body

An HTTP response can include a response body. The response body is data associated with the response, such as generated web page content, UTF-8 JSON payload, or a file.

[HttpResponse.Body](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.body#microsoft-aspnetcore-http-httpresponse-body) allows the response body to be written with [Stream](/en-us/dotnet/api/system.io.stream):

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/downloadfile", async (IConfiguration config, HttpContext context) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], "helloworld.txt");

    await using var fileStream = File.OpenRead(filePath);
    await fileStream.CopyToAsync(context.Response.Body);
});

app.Run();
```

`HttpResponse.Body` can be written directly or used with other APIs that write to a stream.

#### BodyWriter

An alternative way to write the response body is to use the [HttpResponse.BodyWriter](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.bodywriter#microsoft-aspnetcore-http-httpresponse-bodywriter) property. The `BodyWriter` property exposes the response body as a [PipeWriter](/en-us/dotnet/api/system.io.pipelines.pipewriter). This API is from [I/O pipelines](/en-us/dotnet/standard/io/pipelines), and it's an advanced, high-performance way to write the response.

The writer provides direct access to the response body and manages memory on the caller's behalf. Unlike `HttpResponse.Body`, the write doesn't copy request data into a buffer. However, a writer is more complicated to use than a stream and writer code should be thoroughly tested.

For information on how to write content to `BodyWriter`, see [I/O pipelines PipeWriter](/en-us/dotnet/standard/io/pipelines#pipewriter).

### Set response trailers

HTTP/2 and HTTP/3 support response trailers. Trailers are headers sent with the response after the response body is complete. Because trailers are sent after the response body, trailers can be added to the response at any time.

The following code sets trailers using [AppendTrailer](/en-us/dotnet/api/microsoft.aspnetcore.http.responsetrailerextensions.appendtrailer):

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", (HttpResponse response) =>
{
    // Write body
    response.WriteAsync("Hello world");

    if (response.SupportsTrailers())
    {
        response.AppendTrailer("trailername", "TrailerValue");
    }
});

app.Run();
```

## `RequestAborted`

The [HttpContext.RequestAborted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted) cancellation token can be used to notify that the HTTP request has been aborted by the client or server. The cancellation token should be passed to long-running tasks so they can be canceled if the request is aborted. For example, aborting a database query or HTTP request to get data to return in the response.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var httpClient = new HttpClient();
app.MapPost("/books/{bookId}", async (int bookId, HttpContext context) =>
{
    var stream = await httpClient.GetStreamAsync(
        $"http://contoso/books/{bookId}.json", context.RequestAborted);

    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
```

The `RequestAborted` cancellation token doesn't need to be used for request body read operations because reads always throw immediately when the request is aborted. The `RequestAborted` token is also usually unnecessary when writing response bodies, because writes immediately no-op when the request is aborted.

In some cases, passing the `RequestAborted` token to write operations can be a convenient way to force a write loop to exit early with an [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception). However, it's typically better to pass the `RequestAborted` token into any asynchronous operations responsible for retrieving the response body content instead.

Note

[Minimal APIs](minimal-apis?view=aspnetcore-10.0) supports binding [HttpContext.RequestAborted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted) directly to a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) parameter.

## `Abort()`

The [HttpContext.Abort()](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.abort#microsoft-aspnetcore-http-httpcontext-abort) method can be used to abort an HTTP request from the server. Aborting the HTTP request immediately triggers the [HttpContext.RequestAborted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted) cancellation token and sends a notification to the client that the server has aborted the request.

The middleware in the following example:

* Adds a custom check for malicious requests.
* Aborts the HTTP request if the request is malicious.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    if (RequestAppearsMalicious(context.Request))
    {
        // Malicious requests don't even deserve an error response (e.g. 400).
        context.Abort();
        return;
    }

    await next.Invoke();
});

app.Run();
```

## `User`

The [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user) property is used to get or set the user, represented by [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal), for the request. The [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) is typically set by [ASP.NET Core authentication](../security/authentication/?view=aspnetcore-10.0).

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/user/current", [Authorize] async (HttpContext context) =>
{
    var user = await GetUserAsync(context.User.Identity.Name);
    return Results.Ok(user);
});

app.Run();
```

Note

[Minimal APIs](minimal-apis?view=aspnetcore-10.0) supports binding [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user) directly to a [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) parameter.

## `Features`

The [HttpContext.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features#microsoft-aspnetcore-http-httpcontext-features) property provides access to the collection of feature interfaces for the current request. Since the feature collection is mutable even within the context of a request, middleware can be used to modify the collection and add support for additional features. Some advanced features are only available by accessing the associated interface through the feature collection.

The following example:

* Gets [IHttpMinRequestBodyDataRateFeature](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminrequestbodydataratefeature) from the features collection.
* Sets [MinDataRate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminresponsedataratefeature.mindatarate#microsoft-aspnetcore-server-kestrel-core-features-ihttpminresponsedataratefeature-mindatarate) to null. This removes the minimum data rate that the request body must be sent by the client for this HTTP request.

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/long-running-stream", async (HttpContext context) =>
{
    var feature = context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
    if (feature != null)
    {
        feature.MinDataRate = null;
    }

    // await and read long-running stream from request body.
    await Task.Yield();
});

app.Run();
```

For more information about using request features and `HttpContext`, see [Request Features in ASP.NET Core](request-features?view=aspnetcore-10.0).

## HttpContext isn't thread safe

This article primarily discusses using `HttpContext` in request and response flow from Blazor Web App components, Razor Pages, controllers, middleware, and so forth. Consider the following when using `HttpContext` outside the request and response flow:

* The `HttpContext` is **NOT** thread safe. Accessing it from multiple threads can result in unpredictable results, such as exceptions and data corruption.

* The [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) interface should be used with caution. As always, the `HttpContext` must ***not*** be captured outside of the request flow. `IHttpContextAccessor`:

  * Relies on [AsyncLocal\<T>](/en-us/dotnet/api/system.threading.asynclocal-1), which can have a negative performance impact on asynchronous calls.
  * Creates a dependency on "ambient state" which can make testing more difficult.

* [IHttpContextAccessor.HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor.httpcontext) might be `null` if accessed outside of the request flow.

* To access information from `HttpContext` outside the request flow, copy the information inside the request flow. Be careful to copy the actual data and not just references. For example, rather than copying a reference to an `IHeaderDictionary`, copy the relevant header values or copy the entire dictionary key by key before leaving the request flow.

* Don't capture `IHttpContextAccessor.HttpContext` in a constructor.

The following sample logs GitHub branches when requested from the `/branch` endpoint:

```
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // The GitHub API requires two headers. The Use-Agent header is added
    // dynamically through UserAgentHeaderHandler
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();

builder.Services.AddTransient<UserAgentHeaderHandler>();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
                         HttpContext context, Logger<Program> logger) =>
{
    var httpClient = httpClientFactory.CreateClient("GitHub");
    var httpResponseMessage = await httpClient.GetAsync(
        "repos/dotnet/AspNetCore.Docs/branches");

    if (!httpResponseMessage.IsSuccessStatusCode) 
        return Results.BadRequest();

    await using var contentStream =
        await httpResponseMessage.Content.ReadAsStreamAsync();

    var response = await JsonSerializer.DeserializeAsync
        <IEnumerable<GitHubBranch>>(contentStream);

    app.Logger.LogInformation($"/branches request: " +
                              $"{JsonSerializer.Serialize(response)}");

    return Results.Ok(response);
});

app.Run();
```

The GitHub API requires two headers. The `User-Agent` header is added dynamically by the `UserAgentHeaderHandler`:

```
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // The GitHub API requires two headers. The Use-Agent header is added
    // dynamically through UserAgentHeaderHandler
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();

builder.Services.AddTransient<UserAgentHeaderHandler>();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
                         HttpContext context, Logger<Program> logger) =>
{
    var httpClient = httpClientFactory.CreateClient("GitHub");
    var httpResponseMessage = await httpClient.GetAsync(
        "repos/dotnet/AspNetCore.Docs/branches");

    if (!httpResponseMessage.IsSuccessStatusCode) 
        return Results.BadRequest();

    await using var contentStream =
        await httpResponseMessage.Content.ReadAsStreamAsync();

    var response = await JsonSerializer.DeserializeAsync
        <IEnumerable<GitHubBranch>>(contentStream);

    app.Logger.LogInformation($"/branches request: " +
                              $"{JsonSerializer.Serialize(response)}");

    return Results.Ok(response);
});

app.Run();
```

The `UserAgentHeaderHandler`:

```
using Microsoft.Net.Http.Headers;

namespace HttpContextInBackgroundThread;

public class UserAgentHeaderHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ILogger _logger;

    public UserAgentHeaderHandler(IHttpContextAccessor httpContextAccessor,
                                  ILogger<UserAgentHeaderHandler> logger)
    {
        _httpContextAccessor = httpContextAccessor;
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> 
                                    SendAsync(HttpRequestMessage request, 
                                    CancellationToken cancellationToken)
    {
        var contextRequest = _httpContextAccessor.HttpContext?.Request;
        string? userAgentString = contextRequest?.Headers["user-agent"].ToString();
        
        if (string.IsNullOrEmpty(userAgentString))
        {
            userAgentString = "Unknown";
        }

        request.Headers.Add(HeaderNames.UserAgent, userAgentString);
        _logger.LogInformation($"User-Agent: {userAgentString}");

        return await base.SendAsync(request, cancellationToken);
    }
}
```

In the preceding code, when the `HttpContext` is `null`, the `userAgent` string is set to `"Unknown"`. If possible, `HttpContext` should be explicitly passed to the service. Explicitly passing in `HttpContext` data:

* Makes the service API more useable outside the request flow.
* Is better for performance.
* Makes the code easier to understand and reason about than relying on ambient state.

When the service must access `HttpContext`, it should account for the possibility of `HttpContext` being `null` when not called from a request thread.

The application also includes `PeriodicBranchesLoggerService`, which logs the open GitHub branches of the specified repository every 30 seconds:

```
using System.Text.Json;

namespace HttpContextInBackgroundThread;

public class PeriodicBranchesLoggerService : BackgroundService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger _logger;
    private readonly PeriodicTimer _timer;

    public PeriodicBranchesLoggerService(IHttpClientFactory httpClientFactory,
                                         ILogger<PeriodicBranchesLoggerService> logger)
    {
        _httpClientFactory = httpClientFactory;
        _logger = logger;
        _timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (await _timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                // Cancel sending the request to sync branches if it takes too long
                // rather than miss sending the next request scheduled 30 seconds from now.
                // Having a single loop prevents this service from sending an unbounded
                // number of requests simultaneously.
                using var syncTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
                syncTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
                
                var httpClient = _httpClientFactory.CreateClient("GitHub");
                var httpResponseMessage = await httpClient.GetAsync("repos/dotnet/AspNetCore.Docs/branches",
                                                                    stoppingToken);

                if (httpResponseMessage.IsSuccessStatusCode)
                {
                    await using var contentStream =
                        await httpResponseMessage.Content.ReadAsStreamAsync(stoppingToken);

                    // Sync the response with preferred datastore.
                    var response = await JsonSerializer.DeserializeAsync<
                        IEnumerable<GitHubBranch>>(contentStream, cancellationToken: stoppingToken);

                    _logger.LogInformation(
                        $"Branch sync successful! Response: {JsonSerializer.Serialize(response)}");
                }
                else
                {
                    _logger.LogError(1, $"Branch sync failed! HTTP status code: {httpResponseMessage.StatusCode}");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(1, ex, "Branch sync failed!");
            }
        }
    }

    public override Task StopAsync(CancellationToken stoppingToken)
    {
        // This will cause any active call to WaitForNextTickAsync() to return false immediately.
        _timer.Dispose();
        // This will cancel the stoppingToken and await ExecuteAsync(stoppingToken).
        return base.StopAsync(stoppingToken);
    }
}
```

`PeriodicBranchesLoggerService` is a [hosted service](host/hosted-services?view=aspnetcore-10.0), which runs outside the request and response flow. Logging from the `PeriodicBranchesLoggerService` has a null `HttpContext`. The `PeriodicBranchesLoggerService` was written to not depend on the `HttpContext`.

```
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>
{
```

## Related content

For more information about accessing `HttpContext`, see [Access HttpContext in ASP.NET Core](http-context?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/scale
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/scale.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core SignalR hosting and scaling

By [Ashley Stanton-Nurse](https://github.com/analogrelay), [Brady Gaster](https://twitter.com/bradygaster), and [Tom Dykstra](https://github.com/tdykstra)

This article explains hosting and scaling considerations for high-traffic apps that use ASP.NET Core SignalR.

## Sticky sessions

SignalR requires the same server process handle all HTTP requests for a specific connection. When SignalR runs on a server farm (multiple servers), "sticky sessions" must be used. "Sticky sessions" are also called *session affinity*. Azure App Service uses [Microsoft Application Request Routing (ARR)](/en-us/iis/extensions/planning-for-arr/application-request-routing-version-2-overview) to route requests. Enabling the "Session affinity" (ARR Affinity) setting in your App Service app enables sticky sessions.

There are three scenarios where sticky sessions aren't required for an app:

* Hosting on a single server in a single process
* Using the Azure SignalR Service (sticky sessions are enabled for the service, not the app)
* All clients are configured to use WebSockets **only** and the client configuration enables [SkipNegotiation](configuration?view=aspnetcore-10.0#configure-other-options)

In all other scenarios (including when the Redis backplane is used), the server environment must be configured for sticky sessions.

For guidance on configuring Azure App Service for SignalR, see [Publish an ASP.NET Core SignalR app to Azure App Service](publish-to-azure-web-app?view=aspnetcore-10.0). For guidance on configuring sticky sessions for Blazor apps that use the [Azure SignalR Service](#azure-signalr-service), see [Host and deploy ASP.NET Core server-side Blazor apps](../blazor/host-and-deploy/server/?view=aspnetcore-10.0#signalr-configuration).

## TCP connection resources

The number of concurrent TCP connections that a web server can support is limited. Standard HTTP clients use *ephemeral* connections. These connections can be closed when the client goes idle and reopened later. On the other hand, a SignalR connection is *persistent*. SignalR connections stay open even when the client goes idle. In a high-traffic app that serves many clients, these persistent connections can cause servers to hit their maximum number of connections.

Persistent connections also consume extra memory, to track each connection.

The heavy use of connection-related resources by SignalR can affect other web apps that are hosted on the same server. When SignalR opens and holds the last available TCP connections, other web apps on the same server also have no more connections available to them.

If a server runs out of connections, you see random socket errors and connection reset errors. For example:

```
An attempt was made to access a socket in a way forbidden by its access permissions...
```

To keep SignalR resource usage from causing errors in other web apps, run SignalR on different servers than your other web apps.

To keep SignalR resource usage from causing errors in a SignalR app, scale out to limit the number of connections a server has to handle.

## Scale out

An app that uses SignalR needs to keep track of all its connections, which creates problems for a server farm. Add a server, and it gets new connections that the other servers don't know about. For example, SignalR on each server in the following diagram is unaware of the connections on the other servers. When SignalR on one of the servers wants to send a message to all clients, the message only goes to the clients connected to that server.

The options for solving this problem are the [Azure SignalR Service](#azure-signalr-service) and [Redis backplane](#redis-backplane).

## Azure SignalR Service

The Azure SignalR Service functions as a proxy for real-time traffic and doubles as a backplane when the app is scaled out across multiple servers. Each time a client initiates a connection to the server, the client is redirected to connect to the service. The following diagram illustrates this process:

The result is that the service manages all of the client connections, while each server needs only a small constant number of connections to the service, as shown in the following diagram:

This approach to scale out has several advantages over the Redis backplane alternative:

* Sticky sessions, also known as [client affinity](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing#step-3---configure-client-affinity), isn't required because clients are immediately redirected to the Azure SignalR Service when they connect.
* A SignalR app can scale out based on the number of messages sent, while the Azure SignalR Service scales to handle any number of connections. For example, there could be thousands of clients, but if only a few messages per second are sent, the SignalR app doesn't need to scale out to multiple servers just to handle the connections themselves.
* A SignalR app doesn't use many more connection resources than a web app without SignalR.

For these reasons, the recommendation is to use the Azure SignalR Service for all ASP.NET Core SignalR apps hosted on Azure, including App Service, virtual machines, and containers.

For more information, see the [Azure SignalR Service documentation](/en-us/azure/azure-signalr/signalr-overview).

## Redis backplane

[Redis](https://redis.io/) is an in-memory key-value store that supports a messaging system with a publish/subscribe model. The SignalR Redis backplane uses the publish/subscribe feature to forward messages to other servers. When a client makes a connection, the connection information is passed to the backplane. When a server wants to send a message to all clients, it sends it to the backplane. The backplane knows all connected clients and which servers they're on. It sends the message to all clients via their respective servers. This process is illustrated in the following diagram:

The Redis backplane is the recommended scale-out approach for apps hosted on your own infrastructure. If significant connection latency exists between your data center and an Azure data center, Azure SignalR Service might not be a practical option for on-premises apps with low latency or high throughput requirements.

The Azure SignalR Service advantages described earlier are disadvantages for the Redis backplane:

* Sticky sessions, also known as [client affinity](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing#step-3---configure-client-affinity), is required, except when **both** of the following are true:

  * All clients are configured to **only** use WebSockets.
  * The [SkipNegotiation setting](configuration?view=aspnetcore-10.0#configure-other-options) is enabled in the client configuration. After a connection is initiated on a server, the connection has to stay on that server.

* A SignalR app must scale out based on number of clients, even when sending few messages.

* A SignalR app uses many more connection resources than a web app without SignalR.

## IIS limitations on Windows client operating system

Windows 10 and Windows 8.x are client operating systems. Internet Information Services (IIS) on client operating systems has a limit of 10 concurrent connections. The SignalR connections have the following characteristics:

* They're transient and frequently re-established.
* They **aren't** disposed immediately when no longer used.

These characteristics make it likely to hit the limit of 10 connections on a client operating system. When you use a client operating system for development, consider the following recommendations:

* Avoid IIS
* Use Kestrel or IIS Express as deployment targets

## Linux with Nginx

The following code contains the minimum required settings to enable WebSockets, ServerSentEvents, and LongPolling for SignalR:

```
http {
  map $http_connection $connection_upgrade {
    "~*Upgrade" $http_connection;
    default keep-alive;
  }

  server {
    listen 80;
    server_name example.com *.example.com;

    # Configure the SignalR Endpoint
    location /hubroute {
      # App server url
      proxy_pass http://localhost:5000;

      # Configuration for WebSockets
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;
      proxy_cache off;
      # WebSockets were implemented after http/1.0
      proxy_http_version 1.1;

      # Configuration for ServerSentEvents
      proxy_buffering off;

      # Configuration for LongPolling or if your KeepAliveInterval is longer than 60 seconds
      proxy_read_timeout 100s;

      proxy_set_header Host $host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
    }
  }
}
```

When multiple back-end servers are used, sticky sessions must be added to prevent SignalR connections from switching servers when connecting. There are multiple ways to add sticky sessions in Nginx. The following examples show two approaches based on what you have available.

The following code supplements the previous example configuration. In the snippets, `backend` is the name of the group of servers.

* With [Nginx Open Source](https://nginx.org/en/), use `ip_hash` to route connections to a server based on the client IP address:

  ```
  http {
     upstream backend {
       # App server 1
       server localhost:5000;
       # App server 2
       server localhost:5002;

       ip_hash;
     }
  }
  ```

* With [Nginx Plus](https://www.f5.com/products/nginx/nginx-plus), use `sticky` to add a cookie to requests and pin the user requests to a server:

  ```
  http {
     upstream backend {
       # App server 1
       server localhost:5000;
       # App server 2
       server localhost:5002;

       sticky cookie srv_id expires=max domain=.example.com path=/ httponly;
     }
  }
  ```

* For both configurations, change `proxy_pass http://localhost:5000` in the `server` section to `proxy_pass http://backend`.

You can find more information in [Host ASP.NET Core on Linux with Nginx](../host-and-deploy/linux-nginx?view=aspnetcore-10.0).

* To use WebSockets over Nginx, see [WebSocket proxying with Nginx](https://nginx.org/docs/http/websocket.html).
* To use load balancing and sticky sessions, see [HTTP load balancing with Nginx](https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/).

## Other SignalR backplane providers

The following non-Microsoft providers also offer SignalR backplane:

* [NCache](https://www.alachisoft.com/ncache/asp-net-core-signalr.html)
* [Orleans](https://github.com/OrleansContrib/SignalR.Orleans)
* [Rebus](https://github.com/rebus-org/Rebus.SignalR)
* [SQL Server](https://github.com/IntelliTect/IntelliTect.AspNetCore.SignalR.SqlServer)

## Related content

* [Azure SignalR Service documentation](/en-us/azure/azure-signalr/signalr-overview)
* [Set up a Redis backplane](redis-backplane?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/metapackage-app
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/metapackage-app.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Microsoft.AspNetCore.App for ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The ASP.NET Core shared framework (`Microsoft.AspNetCore.App`) contains assemblies that are developed and supported by Microsoft. `Microsoft.AspNetCore.App` is installed when the [.NET Core 3.0 or later SDK](https://dotnet.microsoft.com/download/dotnet-core/3.0) is installed. The *shared framework* is the set of assemblies (`.dll` files) that are installed on the machine and includes a runtime component and a targeting pack.

* Projects that target the `Microsoft.NET.Sdk.Web` SDK implicitly reference the `Microsoft.AspNetCore.App` framework.

No additional references are required for these projects:

```
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>
    ...
</Project>
```

The ASP.NET Core shared framework:

* Doesn't include third-party dependencies.
* Includes all supported packages by the ASP.NET Core team.

This feature requires ASP.NET Core 2.x targeting .NET Core 2.x.

The [Microsoft.AspNetCore.App](https://www.nuget.org/packages/Microsoft.AspNetCore.App) [metapackage](/en-us/dotnet/core/packages#metapackages) for ASP.NET Core:

* Does not include third-party dependencies except for [Json.NET](https://www.nuget.org/packages/Newtonsoft.Json/), [Remotion.Linq](https://www.nuget.org/packages/Remotion.Linq/), and [IX-Async](https://www.nuget.org/packages/System.Interactive.Async/). These 3rd-party dependencies are deemed necessary to ensure the major frameworks features function.
* Includes all supported packages by the ASP.NET Core team except those that contain third-party dependencies (other than those previously mentioned).
* Includes all supported packages by the Entity Framework Core team except those that contain third-party dependencies (other than those previously mentioned).

All the features of ASP.NET Core 2.x and Entity Framework Core 2.x are included in the `Microsoft.AspNetCore.App` package. The default project templates targeting ASP.NET Core 2.x use this package. We recommend applications targeting ASP.NET Core 2.x and Entity Framework Core 2.x use the `Microsoft.AspNetCore.App` package.

The version number of the `Microsoft.AspNetCore.App` metapackage represents the minimum ASP.NET Core version and Entity Framework Core version.

Using the `Microsoft.AspNetCore.App` metapackage provides version restrictions that protect your app:

* If a package is included that has a transitive (not direct) dependency on a package in `Microsoft.AspNetCore.App`, and those version numbers differ, NuGet will generate an error.
* Other packages added to your app cannot change the version of packages included in `Microsoft.AspNetCore.App`.
* Version consistency ensures a reliable experience. `Microsoft.AspNetCore.App` was designed to prevent untested version combinations of related bits being used together in the same app.

Applications that use the `Microsoft.AspNetCore.App` metapackage automatically take advantage of the ASP.NET Core shared framework. When you use the `Microsoft.AspNetCore.App` metapackage, **no** assets from the referenced ASP.NET Core NuGet packages are deployed with the application—the ASP.NET Core shared framework contains these assets. The assets in the shared framework are precompiled to improve application startup time.

The following project file references the `Microsoft.AspNetCore.App` metapackage for ASP.NET Core and represents a typical ASP.NET Core 2.2 template:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

The preceding markup represents a typical ASP.NET Core 2.x template. It doesn't specify a version number for the `Microsoft.AspNetCore.App` package reference. When the version is not specified, an [implicit](https://github.com/dotnet/core/blob/main/release-notes/1.0/sdk/1.0-rc3-implicit-package-refs.md) version is specified by the SDK, that is, `Microsoft.NET.Sdk.Web`. We recommend relying on the implicit version specified by the SDK and not explicitly setting the version number on the package reference. If you have questions on this approach, leave a GitHub comment at the [Discussion for the Microsoft.AspNetCore.App implicit version](https://github.com/dotnet/AspNetCore.Docs/issues/6430).

The implicit version is set to `major.minor.0` for portable apps. The shared framework roll-forward mechanism will run the app on the latest compatible version among the installed shared frameworks. To guarantee the same version is used in development, test, and production, ensure the same version of the shared framework is installed in all environments. For self contained apps, the implicit version number is set to the `major.minor.patch` of the shared framework bundled in the installed SDK.

Specifying a version number on the `Microsoft.AspNetCore.App` reference does **not** guarantee that version of the shared framework will be chosen. For example, suppose version "2.2.1" is specified, but "2.2.3" is installed. In that case, the app will use "2.2.3". Although not recommended, you can disable roll forward (patch and/or minor). For more information regarding dotnet host roll-forward and how to configure its behavior, see [dotnet host roll forward](https://github.com/dotnet/core-setup/blob/master/Documentation/design-docs/roll-forward-on-no-candidate-fx.md).

`<Project Sdk` must be set to `Microsoft.NET.Sdk.Web` to use the implicit version `Microsoft.AspNetCore.App`. When `<Project Sdk="Microsoft.NET.Sdk">` (without the trailing `.Web`) is used:

* The following warning is generated:

  *Warning NU1604: Project dependency Microsoft.AspNetCore.App does not contain an inclusive lower bound. Include a lower bound in the dependency version to ensure consistent restore results.*

* This is a known issue with the .NET Core 2.1 SDK.

[]()

## Update ASP.NET Core

The `Microsoft.AspNetCore.App` [metapackage](/en-us/dotnet/core/packages#metapackages) isn't a traditional package that's updated from NuGet. Similar to `Microsoft.NETCore.App`, `Microsoft.AspNetCore.App` represents a shared runtime, which has special versioning semantics handled outside of NuGet. For more information, see [Packages, metapackages and frameworks](/en-us/dotnet/core/packages).

To update ASP.NET Core:

* On development machines and build servers: Download and install the [.NET Core SDK](https://dotnet.microsoft.com/download).
* On deployment servers: Download and install the [.NET Core runtime](https://dotnet.microsoft.com/download).

Applications will roll forward to the latest installed version on application restart. It's not necessary to update the `Microsoft.AspNetCore.App` version number in the project file. For more information, see [Framework-dependent apps roll forward](/en-us/dotnet/core/versions/selection#framework-dependent-apps-roll-forward).

If your application previously used `Microsoft.AspNetCore.All`, see [Migrating from Microsoft.AspNetCore.All to Microsoft.AspNetCore.App](metapackage?view=aspnetcore-10.0#migrate).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-05-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/host/web-host.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Web Host

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

ASP.NET Core apps configure and launch a *host*. The host is responsible for app startup and lifetime management. At a minimum, the host configures a server and a request processing pipeline. The host can also set up logging, dependency injection, and configuration.

This article covers the Web Host, which remains available only for backward compatibility. The ASP.NET Core templates create a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) and [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication), which is recommended for web apps. For more information on `WebApplicationBuilder` and `WebApplication`, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../../migration/50-to-60?view=aspnetcore-10.0#new-hosting-model).

## Set up a host

Create a host by using an instance of [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder). This task is typically performed in the app's entry point, the `Main` method in the *Program.cs* file. A typical app calls the [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder) method to start setting up a host:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}
```

The code that calls `CreateDefaultBuilder` is in a method named `CreateWebHostBuilder`, which separates it from the code in `Main` that calls the `Run` method on the builder object. The separation is required if you use [Entity Framework Core tools](/en-us/ef/core/cli/). The tools expect to find a `CreateWebHostBuilder` method that they can call at design time to configure the host without running the app. An alternative is to create an instance of `IDesignTimeDBContextfactory`. For more information, see [Design-time DbContext Creation](/en-us/ef/core/cli/dbcontext-creation).

The `CreateDefaultBuilder` method performs the following tasks:

* Configures [Kestrel](../servers/kestrel?view=aspnetcore-10.0) server as the web server by using the app's hosting configuration providers. For the Kestrel server's default options, see [Configure options for the ASP.NET Core Kestrel web server](../servers/kestrel/options?view=aspnetcore-10.0).

* Sets the [content root](../?view=aspnetcore-10.0#content-root) to the path returned by [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory).

* Loads the [host configuration](#set-web-host-configuration-values) from the following sources:

  * Environment variables prefixed with `ASPNETCORE_` (for example, `ASPNETCORE_ENVIRONMENT`)

* Configures [logging](../logging/?view=aspnetcore-10.0) for console and debug output. Logging includes [log filtering](../logging/?view=aspnetcore-10.0#apply-log-filter-rules-in-code) rules specified in a Logging configuration section of an *appsettings.json* or *appsettings.{Environment}.json* file.

* When the application runs behind IIS with the [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0), `CreateDefaultBuilder` enables [IIS Integration](../../host-and-deploy/iis/?view=aspnetcore-10.0), which configures the application base address and port. IIS Integration also configures the app to [capture startup errors](#capture-startup-errors). For the IIS default options, see [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0#iis-options).

* Sets the [ValidateScopes](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validatescopes) property to `true`, if the application environment is `Development`. For more information, see [Configure scope validation](#configure-scope-validation).

The [content root](../?view=aspnetcore-10.0#content-root) determines where the host searches for content files, such as MVC view files. When the app launches from the project root folder, that folder is used as the content root. This behavior is the default for [Visual Studio](https://visualstudio.microsoft.com) and the [dotnet new templates](/en-us/dotnet/core/tools/dotnet-new).

For more information on app configuration, see [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

Note

As an alternative to using the static `CreateDefaultBuilder` method, you can create a host from [WebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder) with ASP.NET Core 2.x.

When you set up a host, you can use the [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.configure) and [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder.configureservices) methods. If a `Startup` class is specified, it must define a `Configure` method. For more information, see [App startup in ASP.NET Core](../startup?view=aspnetcore-10.0). Multiple calls to `ConfigureServices` append each other. Multiple calls to `Configure` or `UseStartup` on the `WebHostBuilder` instance replace any previous settings.

### Methods for overriding the configuration

You can override and augment the configuration defined by `CreateDefaultBuilder`. Use the [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.configureappconfiguration) and [ConfigureLogging](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.configurelogging) methods, and other methods and extension methods of [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder).

Here are some examples:

* [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.configureappconfiguration) is used to specify other `IConfiguration` properties for the app. The following call to the `ConfigureAppConfiguration` method adds a delegate to include app configuration in the *appsettings.xml* file. `ConfigureAppConfiguration` can be called multiple times. This configuration doesn't apply to the host (for example, server URLs or environment). For more information, see the [Host configuration values](#set-web-host-configuration-values) section.

  ```
  WebHost.CreateDefaultBuilder(args)
      .ConfigureAppConfiguration((hostingContext, config) =>
      {
          config.AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true);
      })
      ...
  ```

* The following `ConfigureLogging` call adds a delegate to configure the minimum logging level ([SetMinimumLevel](/en-us/dotnet/api/microsoft.extensions.logging.loggingbuilderextensions.setminimumlevel)) to [LogLevel](/en-us/dotnet/api/microsoft.extensions.logging.loglevel#microsoft-extensions-logging-loglevel-warning). This setting overrides the settings in the *appsettings.Development.json* file (`LogLevel.Debug`) and *appsettings.Production.json* file (`LogLevel.Error`) configured by the `CreateDefaultBuilder` method. `ConfigureLogging` can be called multiple times.

  ```
  WebHost.CreateDefaultBuilder(args)
      .ConfigureLogging(logging => 
      {
          logging.SetMinimumLevel(LogLevel.Warning);
      })
      ...
  ```

* The following call to `ConfigureKestrel` overrides the default [Limits.MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxrequestbodysize) of 30,000,000 bytes established when Kestrel is configured with the `CreateDefaultBuilder` method:

  ```
  WebHost.CreateDefaultBuilder(args)
      .ConfigureKestrel((context, options) =>
      {
          options.Limits.MaxRequestBodySize = 20000000;
      });
  ```

## Set Web Host configuration values

The [WebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder) instance relies on the following approaches to set the host configuration values:

* The host builder configuration, which includes environment variables that use the format `ASPNETCORE_{configurationKey}`. For example, `ASPNETCORE_ENVIRONMENT`.

* Extensions, such as [UseContentRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usecontentroot) and [UseConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useconfiguration). For more information, see [Override the Web Host configuration](#override-the-web-host-configuration).

* The [UseSetting](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder.usesetting) method and the associated key. When you set a value with `UseSetting`, the value is set as a string, regardless of the type.

The host uses whichever option sets a value last. For more information, see [Override the Web Host configuration](#override-the-web-host-configuration).

### Application name

Defines the name of the assembly that contains the entry point for the application.

**Key**: `applicationName`\
**Type**: *string*\
**Default**: The name of the assembly that has the app entry point.\
**Set using**: `UseSetting`\
**Environment variable**: `ASPNETCORE_APPLICATIONNAME`

The `IWebHostEnvironment.ApplicationName` property is automatically set when the [UseStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestartup) or [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartup.configure) method is called during host construction. To set the value explicitly, use the [ApplicationKey](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostdefaults.applicationkey#microsoft-aspnetcore-hosting-webhostdefaults-applicationkey) field.

```
WebHost.CreateDefaultBuilder(args)
    .UseSetting(WebHostDefaults.ApplicationKey, "CustomApplicationName")
```

### Capture startup errors

Controls the capture of startup errors.

**Key**: `captureStartupErrors`\
**Type**: *bool* (`true` or `1`)\
**Default**: `false`. If the app runs with Kestrel behind IIS, the default is `true`.\
**Set using**: `CaptureStartupErrors`\
**Environment variable**: `ASPNETCORE_CAPTURESTARTUPERRORS`

When set to `false`, errors during startup result in the host exiting. When set to `true`, the host captures exceptions during startup and attempts to start the server.

```
WebHost.CreateDefaultBuilder(args)
    .CaptureStartupErrors(true)
```

### Content root

Determines where ASP.NET Core begins searching for content files.

**Key**: `contentRoot`\
**Type**: *string*\
**Default**: The folder that contains the app assembly.\
**Set using**: `UseContentRoot`\
**Environment variable**: `ASPNETCORE_CONTENTROOT`

The content root is also used as the base path for the [web root](../?view=aspnetcore-10.0#web-root). If the content root path doesn't exist, the host fails to start.

```
WebHost.CreateDefaultBuilder(args)
    .UseContentRoot("c:\\<content-root>")
```

For more information, see:

* [Fundamentals: Content root](../?view=aspnetcore-10.0#content-root)
* [Web root](#web-root)

### Detailed errors

Determines whether to capture detailed errors.

**Key**: `detailedErrors`\
**Type**: *bool* (`true` or `1`)\
**Default**: `false`\
**Set using**: `UseSetting`\
**Environment variable**: `ASPNETCORE_DETAILEDERRORS`

When enabled (or when the [Environment](#environment) value is set to `Development`), the app captures detailed exceptions.

```
WebHost.CreateDefaultBuilder(args)
    .UseSetting(WebHostDefaults.DetailedErrorsKey, "true")
```

### Environment

Sets the application environment.

**Key**: `environment`\
**Type**: *string*\
**Default**: `Production`\
**Set using**: `UseEnvironment`\
**Environment variable**: `ASPNETCORE_ENVIRONMENT`

The environment can be set to any value. Framework-defined values include `Development`, `Staging`, and `Production`. Values aren't case sensitive.

By default, the *Environment* is read from the `ASPNETCORE_ENVIRONMENT` environment variable. When you use [Visual Studio](https://visualstudio.microsoft.com), environment variables can be set in the *launchSettings.json* file. For more information, see [ASP.NET Core runtime environments](../environments?view=aspnetcore-10.0).

```
WebHost.CreateDefaultBuilder(args)
    .UseEnvironment(EnvironmentName.Development)
```

### Hosting startup assemblies

Provides a semicolon-delimited string of hosting startup assemblies to load on startup.

**Key**: `hostingStartupAssemblies`\
**Type**: *string*\
**Default**: An empty string.\
**Set using**: `UseSetting`\
**Environment variable**: `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES`

Although the configuration value defaults to an empty string, the hosting startup assemblies always include the app's assembly. When hosting startup assemblies are provided, they're added to the app's assembly for loading when the app builds its common services during startup.

```
WebHost.CreateDefaultBuilder(args)
    .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "assembly1;assembly2")
```

### HTTPS port

Sets the HTTPS port for redirection if you get a non-HTTPS connection.

**Key**: `https_port`\
**Type**: *string*\
**Default**: No default.\
**Set using**: `UseSetting`\
**Environment variable**: `ASPNETCORE_HTTPS_PORT`

This setting is used in [enforcing HTTPS](../../security/enforcing-ssl?view=aspnetcore-10.0). This setting doesn't cause the server to listen on the specified port. That is, it's possible to accidentally redirect requests to an unused port.

```
WebHost.CreateDefaultBuilder(args)
    .UseSetting("https_port", "8080")
```

### Hosting startup exclude assemblies

Provides a semicolon-delimited string of hosting startup assemblies to exclude on startup.

**Key**: `hostingStartupExcludeAssemblies`\
**Type**: *string*\
**Default**: An empty string.\
**Set using**: `UseSetting`\
**Environment variable**: `ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES`

```
WebHost.CreateDefaultBuilder(args)
    .UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, "assembly1;assembly2")
```

### Prefer hosting URLs

Indicates whether the host should listen on the URLs configured with the `WebHostBuilder` instead of the URLs configured with the `IServer` implementation.

**Key**: `preferHostingUrls`\
**Type**: *bool* (`true` or `1`)\
**Default**: `false`\
**Set using**: `PreferHostingUrls`\
**Environment variable**: `ASPNETCORE_PREFERHOSTINGURLS`

```
WebHost.CreateDefaultBuilder(args)
    .PreferHostingUrls(true)
```

### Prevent hosting startup

Prevents the automatic loading of hosting startup assemblies, including hosting startup assemblies configured by the app's assembly. For more information, see [Use hosting startup assemblies in ASP.NET Core](platform-specific-configuration?view=aspnetcore-10.0).

**Key**: `preventHostingStartup`\
**Type**: *bool* (`true` or `1`)\
**Default**: `false`\
**Set using**: `UseSetting`\
**Environment variable**: `ASPNETCORE_PREVENTHOSTINGSTARTUP`

```
WebHost.CreateDefaultBuilder(args)
    .UseSetting(WebHostDefaults.PreventHostingStartupKey, "true")
```

### Server URLs

Indicates the IP addresses or host addresses with ports and protocols that the server should listen on for requests.

**Key**: `urls`\
**Type**: *string*\
**Default**: `http://localhost:5000`\
**Set using**: `UseUrls`\
**Environment variable**: `ASPNETCORE_URLS`

Set to a semicolon-separated `;` list of URL prefixes to which the server should respond. For example, `http://localhost:123`. Use a wildcard asterisk `*` to indicate that the server should listen for requests on any IP address or hostname by using the specified port and protocol (for example, `http://*:5000`). The protocol (`http://` or `https://`) must be included with each URL. Supported formats vary among servers.

```
WebHost.CreateDefaultBuilder(args)
    .UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002")
```

Kestrel has its own endpoint configuration API. For more information, see [Configure endpoints for Kestrel web server](../servers/kestrel/endpoints?view=aspnetcore-10.0).

### Shutdown timeout

Specifies the amount of time to wait for the Web Host to shut down.

**Key**: `shutdownTimeoutSeconds`\
**Type**: *int*\
**Default**: 5 seconds\
**Set using**: `UseShutdownTimeout`\
**Environment variable**: `ASPNETCORE_SHUTDOWNTIMEOUTSECONDS`

Although the key accepts an *int* with `UseSetting` (for example, `.UseSetting(WebHostDefaults.ShutdownTimeoutKey, "10")`), the [UseShutdownTimeout](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useshutdowntimeout) extension method takes a [TimeSpan](/en-us/dotnet/api/system.timespan) parameter.

During the timeout period, the Web Host:

* Triggers [ApplicationStopping](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iapplicationlifetime.applicationstopping).
* Attempts to stop hosted services, logging any errors for services that fail to stop.

If the timeout period expires before all hosted services stop, any remaining active services stop when the app shuts down. The services stop even if they're still processing. If services require more time to stop, increase the timeout.

```
WebHost.CreateDefaultBuilder(args)
    .UseShutdownTimeout(TimeSpan.FromSeconds(10))
```

### Startup assembly

Specifies the assembly to search for the `Startup` class.

**Key**: `startupAssembly`\
**Type**: *string*\
**Default**: The application assembly.\
**Set using**: `UseStartup`\
**Environment variable**: `ASPNETCORE_STARTUPASSEMBLY`

You can reference the assembly by name (`string`) or type (`TStartup`). If multiple `UseStartup` methods are called, the last call takes precedence.

```
WebHost.CreateDefaultBuilder(args)
    .UseStartup("StartupAssemblyName")
```

```
WebHost.CreateDefaultBuilder(args)
    .UseStartup<TStartup>()
```

### Web root

Sets the relative path to the app's static assets.

**Key**: `webroot`\
**Type**: *string*\
**Default**: `wwwroot`\
**Set using**: `UseWebRoot`\
**Environment variable**: `ASPNETCORE_WEBROOT`

The path to `{content root}/wwwroot` must exist. If the path doesn't exist, a no-op file provider is used.

```
WebHost.CreateDefaultBuilder(args)
    .UseWebRoot("public")
```

For more information, see:

* [Fundamentals: Web root](../?view=aspnetcore-10.0#web-root)
* [Content root](#content-root)

## Override the Web Host configuration

Use application [configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0) to configure the Web Host.

In the following example, host configuration is optionally specified in a *hostsettings.json* file. Command-line arguments can override any configuration loaded from the *hostsettings.json* file. The built configuration (in config) is used to configure the host with the [UseConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useconfiguration) method. `IWebHostBuilder` configuration is added to the application configuration, but the converse isn't true. The `ConfigureAppConfiguration` method doesn't affect the `IWebHostBuilder` configuration.

Overriding the configuration provided by `UseUrls` with the *hostsettings.json* config followed by the command-line argument config:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args)
    {
        var config = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("hostsettings.json", optional: true)
            .AddCommandLine(args)
            .Build();

        return WebHost.CreateDefaultBuilder(args)
            .UseUrls("http://*:5000")
            .UseConfiguration(config)
            .Configure(app =>
            {
                app.Run(context => 
                    context.Response.WriteAsync("Hello, World!"));
            });
    }
}
```

The *hostsettings.json* file contents:

```
{
    urls: "http://*:5005"
}
```

Note

The [UseConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useconfiguration) method only copies keys from the provided `IConfiguration` to the host builder configuration. Therefore, setting the `reloadOnChange: true` property for JSON, INI, and XML settings files have no effect.

To specify the host run on a particular URL, the desired value can be passed in from a command prompt when executing [dotnet run](/en-us/dotnet/core/tools/dotnet-run). The command-line argument overrides the `urls` value from the *hostsettings.json* file, and the server listens on port 8080:

```
dotnet run --urls "http://*:8080"
```

## Manage the Web Host

To manage the Web Host, you use the following methods.

### Run

The `Run` method starts the web app and blocks the calling thread until the host is shut down:

```
host.Run();
```

### Start

Run the host in a nonblocking manner by calling its `Start` method:

```
using (host)
{
    host.Start();
    Console.ReadLine();
}
```

If a list of URLs is passed to the `Start` method, it listens on the URLs specified:

```
var urls = new List<string>()
{
    "http://*:5000",
    "http://localhost:5001"
};

var host = new WebHostBuilder()
    .UseKestrel()
    .UseStartup<Startup>()
    .Start(urls.ToArray());

using (host)
{
    Console.ReadLine();
}
```

The app can initialize and start a new host with the preconfigured defaults of the `CreateDefaultBuilder` method by using a static convenience method. These methods start the server without console output and use the [WaitForShutdown](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostextensions.waitforshutdown) method, which waits for a break (`Ctrl`+`C`/SIGINT (Windows)/SIGINT (Windows), `Ctrl`+`C` (macOS), or SIGTERM).

### Start(RequestDelegate app)

Run the host with a `RequestDelegate`:

```
using (var host = WebHost.Start(app => app.Response.WriteAsync("Hello, World!")))
{
    Console.WriteLine("Use Ctrl-C to shutdown the host...");
    host.WaitForShutdown();
}
```

Make a request in the browser to the `http://localhost:5000` URL to receive the response "Hello World!" The `WaitForShutdown` method blocks until a break (Ctrl-C/SIGINT or SIGTERM) is issued. The app displays the `Console.WriteLine` message and waits for a keypress to exit.

### Start(string url, RequestDelegate app)

Run the host with a URL and a `RequestDelegate`:

```
using (var host = WebHost.Start("http://localhost:8080", app => app.Response.WriteAsync("Hello, World!")))
{
    Console.WriteLine("Use Ctrl-C to shutdown the host...");
    host.WaitForShutdown();
}
```

Produces the same result as **Start(RequestDelegate app)**, except the app responds on the `http://localhost:8080` URL.

### Start(Action\<IRouteBuilder> routeBuilder)

Run the host by starting with an instance of `IRouteBuilder` ([Microsoft.AspNetCore.Routing](https://www.nuget.org/packages/Microsoft.AspNetCore.Routing/)), which uses routing middleware:

```
using (var host = WebHost.Start(router => router
    .MapGet("hello/{name}", (req, res, data) => 
        res.WriteAsync($"Hello, {data.Values["name"]}!"))
    .MapGet("buenosdias/{name}", (req, res, data) => 
        res.WriteAsync($"Buenos dias, {data.Values["name"]}!"))
    .MapGet("throw/{message?}", (req, res, data) => 
        throw new Exception((string)data.Values["message"] ?? "Uh oh!"))
    .MapGet("{greeting}/{name}", (req, res, data) => 
        res.WriteAsync($"{data.Values["greeting"]}, {data.Values["name"]}!"))
    .MapGet("", (req, res, data) => res.WriteAsync("Hello, World!"))))
{
    Console.WriteLine("Use Ctrl-C to shutdown the host...");
    host.WaitForShutdown();
}
```

Use the following browser requests with the example:

| Request                                    | Response                                 |
| ------------------------------------------ | ---------------------------------------- |
| `http://localhost:5000/hello/Martin`       | Hello, Martin!                           |
| `http://localhost:5000/buenosdias/Catrina` | Buenos dias, Catrina!                    |
| `http://localhost:5000/throw/ooops!`       | Throws an exception with string "oops!"  |
| `http://localhost:5000/throw`              | Throws an exception with string "Uh oh!" |
| `http://localhost:5000/Sante/Kevin`        | Sante, Kevin!                            |
| `http://localhost:5000`                    | Hello World!                             |

The `WaitForShutdown` method blocks until a break (Ctrl-C/SIGINT or SIGTERM) is issued. The app displays the `Console.WriteLine` message and waits for a keypress to exit.

### Start(string url, Action\<IRouteBuilder> routeBuilder)

Run the host by starting with a URL and an instance of `IRouteBuilder`:

```
using (var host = WebHost.Start("http://localhost:8080", router => router
    .MapGet("hello/{name}", (req, res, data) => 
        res.WriteAsync($"Hello, {data.Values["name"]}!"))
    .MapGet("buenosdias/{name}", (req, res, data) => 
        res.WriteAsync($"Buenos dias, {data.Values["name"]}!"))
    .MapGet("throw/{message?}", (req, res, data) => 
        throw new Exception((string)data.Values["message"] ?? "Uh oh!"))
    .MapGet("{greeting}/{name}", (req, res, data) => 
        res.WriteAsync($"{data.Values["greeting"]}, {data.Values["name"]}!"))
    .MapGet("", (req, res, data) => res.WriteAsync("Hello, World!"))))
{
    Console.WriteLine("Use Ctrl-C to shut down the host...");
    host.WaitForShutdown();
}
```

Produces the same result as [Start(Action\<IRouteBuilder> routeBuilder)](#startactioniroutebuilder-routebuilder), except the app responds on the `http://localhost:8080` URL.

### StartWith(Action\<IApplicationBuilder> app)

Run the host by starting with a delegate that configures an `IApplicationBuilder`:

```
using (var host = WebHost.StartWith(app => 
    app.Use(next => 
    {
        return async context => 
        {
            await context.Response.WriteAsync("Hello World!");
        };
    })))
{
    Console.WriteLine("Use Ctrl-C to shut down the host...");
    host.WaitForShutdown();
}
```

Make a request in the browser to the `http://localhost:5000` URL to receive the response "Hello World!" The `WaitForShutdown` method blocks until a break (Ctrl-C/SIGINT or SIGTERM) is issued. The app displays the `Console.WriteLine` message and waits for a keypress to exit.

### StartWith(string url, Action\<IApplicationBuilder> app)

Run the host by starting with a URL and a delegate that configures an `IApplicationBuilder`:

```
using (var host = WebHost.StartWith("http://localhost:8080", app => 
    app.Use(next => 
    {
        return async context => 
        {
            await context.Response.WriteAsync("Hello World!");
        };
    })))
{
    Console.WriteLine("Use Ctrl-C to shut down the host...");
    host.WaitForShutdown();
}
```

Produces the same result as [StartWith(Action\<IApplicationBuilder> app)](#startwithactioniapplicationbuilder-app), except the app responds on the `http://localhost:8080` URL.

## Use the IWebHostEnvironment interface

The `IWebHostEnvironment` interface provides information about the app's web hosting environment. Use [constructor injection](../dependency-injection?view=aspnetcore-10.0) to obtain the `IWebHostEnvironment` instance, and then access its properties and extension methods:

```
public class CustomFileReader
{
    private readonly IWebHostEnvironment _env;

    public CustomFileReader(IWebHostEnvironment env)
    {
        _env = env;
    }

    public string ReadFile(string filePath)
    {
        var fileProvider = _env.WebRootFileProvider;
        // Process the file here
    }
}
```

A [convention-based approach](../environments?view=aspnetcore-10.0#environment-based-startup-class-and-methods) can be used to configure the app at startup based on the environment. Alternatively, inject the `IWebHostEnvironment` instance into the `Startup` constructor for use in the `ConfigureServices` method:

```
public class Startup
{
    public Startup(IWebHostEnvironment env)
    {
        HostingEnvironment = env;
    }

    public IWebHostEnvironment HostingEnvironment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        if (HostingEnvironment.IsDevelopment())
        {
            // Development configuration
        }
        else
        {
            // Staging/Production configuration
        }

        var contentRootPath = HostingEnvironment.ContentRootPath;
    }
}
```

Note

In addition to the `IsDevelopment` extension method, `IWebHostEnvironment` offers the `IsStaging`, `IsProduction`, and `IsEnvironment(string environmentName)` methods. For more information, see [ASP.NET Core runtime environments](../environments?view=aspnetcore-10.0).

The `IWebHostEnvironment` service can also be injected directly into the `Configure` method for setting up the processing pipeline:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        // In Development, use the Developer Exception Page
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // In Staging/Production, route exceptions to /error
        app.UseExceptionHandler("/error");
    }

    var contentRootPath = env.ContentRootPath;
}
```

`IWebHostEnvironment` can be injected into the `Invoke` method when creating custom [middleware](../middleware/write?view=aspnetcore-10.0):

```
public async Task Invoke(HttpContext context, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        // Configure middleware for Development
    }
    else
    {
        // Configure middleware for Staging/Production
    }

    var contentRootPath = env.ContentRootPath;
}
```

## Use the IHostApplicationLifetime interface

`IHostApplicationLifetime` allows for post-startup and shutdown activities. Three properties on the interface are cancellation tokens used to register `Action` methods that define startup and shutdown events.

| Cancellation token    | Trigger                                                                                                                       |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `ApplicationStarted`  | The host is fully started.                                                                                                    |
| `ApplicationStopped`  | The host is completing a graceful shutdown. All requests are expected to process. Shutdown blocks until this event completes. |
| `ApplicationStopping` | The host is performing a graceful shutdown. Requests might still be in process. Shutdown blocks until this event completes.   |

```
public class Startup
{
    public void Configure(IApplicationBuilder app, IHostApplicationLifetime appLifetime)
    {
        appLifetime.ApplicationStarted.Register(OnStarted);
        appLifetime.ApplicationStopping.Register(OnStopping);
        appLifetime.ApplicationStopped.Register(OnStopped);

        Console.CancelKeyPress += (sender, eventArgs) =>
        {
            appLifetime.StopApplication();
            // Don't terminate the process immediately, wait for the Main thread to exit gracefully.
            eventArgs.Cancel = true;
        };
    }

    private void OnStarted()
    {
        // Perform post-startup activities here
    }

    private void OnStopping()
    {
        // Perform on-stopping activities here
    }

    private void OnStopped()
    {
        // Perform post-stopped activities here
    }
}
```

The `StopApplication` method requests termination of the app. The following class uses `StopApplication` to gracefully shut down an app when the class's `Shutdown` method is called:

```
public class MyClass
{
    private readonly IHostApplicationLifetime _appLifetime;

    public MyClass(IHostApplicationLifetime appLifetime)
    {
        _appLifetime = appLifetime;
    }

    public void Shutdown()
    {
        _appLifetime.StopApplication();
    }
}
```

## Configure scope validation

The [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder) method sets the [ValidateScopes](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validatescopes) property to `true` if the app's environment is `Development`.

When `ValidateScopes` is set to `true`, the default service provider performs checks to verify:

* Scoped services aren't directly or indirectly resolved from the root service provider.
* Scoped services aren't directly or indirectly injected into singletons.

The root service provider is created when the [BuildServiceProvider](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectioncontainerbuilderextensions.buildserviceprovider) method is called. The root service provider's lifetime corresponds to the app/server's lifetime when the provider starts with the app and is disposed when the app shuts down.

The container that created the scoped services also disposes the services. If a scoped service is created in the root container, the service's lifetime is effectively promoted to singleton. The root container disposes the service only when the app/server is shut down. Validating service scopes catches these situations when `BuildServiceProvider` is called.

To always validate scopes, including in the `Production` environment, configure the [ServiceProviderOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions) object with the [UseDefaultServiceProvider](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usedefaultserviceprovider) method on the host builder:

```
WebHost.CreateDefaultBuilder(args)
    .UseDefaultServiceProvider((context, options) => {
        options.ValidateScopes = true;
    })
```

## Related content

* [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0)
* [Host ASP.NET Core on Linux with Nginx](../../host-and-deploy/linux-nginx?view=aspnetcore-10.0)
* [Host ASP.NET Core in a Windows Service](../../host-and-deploy/windows-service?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/security/authorization/resource-based
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/security/authorization/resource-based.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Resource-based authorization in ASP.NET Core Razor Pages

This article describes how to authorize users for access to app resources.

In an app, a *resource* is typically represented by a C# class that includes data stored in a collection, such as a [`byte[]` array](/en-us/dotnet/api/system.byte). The class usually contains additional metadata pertaining to the resource, such as a unique resource identifier, dates, authors, source information, and a friendly name for display in a UI. The collection that holds resource data is usually loaded from physical file content, a cloud storage object, an in-memory object, or data from a database.

Resource-based authorization requires special attention in ASP.NET Core apps. Attribute evaluation occurs before data binding and before execution of a page handler that loads a resource. Declarative authorization with an `[Authorize]` attribute doesn't suffice for resource-based authorization. Instead, the app must invoke a custom authorization method—an approach known as *imperative authorization*.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/resource-based) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

[Create an ASP.NET Core app with user data protected by authorization](../../../security/authorization/secure-data?view=aspnetcore-10.0) contains a sample app that uses resource-based authorization.

Examples in this article use *primary constructors*, available in C# 12 (.NET 8) or later. For more information, see [Declare primary constructors for classes and structs (C# documentation tutorial)](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) and [Primary constructors (C# Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors). Sample apps that accompany the article that target versions of .NET earlier than .NET 8 use constructor injection.

## Use imperative authorization

Authorization is implemented as an [IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice), which is registered in the service collection at app startup *by the ASP.NET Core framework*. The service is made available to classes and page handlers via [dependency injection](../../../fundamentals/dependency-injection?view=aspnetcore-10.0). The following page handler also injects a document repository, which the developer creates and registers in the service container to manage document operations:

```
public class DocumentModel(IAuthorizationService authorizationService,
    IDocumentRepository documentRepository) : PageModel
{
    private readonly IAuthorizationService _authorizationService;
    private readonly IDocumentRepository _documentRepository;

    public DocumentModel(IAuthorizationService authorizationService,
        IDocumentRepository documentRepository)
    {
        _authorizationService = authorizationService;
        _documentRepository = documentRepository;
    }

    ...
}
```

[IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice) has two [AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) method overloads. One of the overloads accepts a resource and policy name:

```
Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource, 
    string policyName);
```

The other overload accepts a resource and collection of requirements ([IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement)) to evaluate:

```
Task<AuthorizationResult> AuthorizeAsync(
    ClaimsPrincipal user, 
    object resource,
    IEnumerable<IAuthorizationRequirement> requirements);
```

In the following example, the secured resource is loaded into a custom `Document` object. An [AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) overload is invoked to determine whether the current user is allowed to edit the document via a custom "`EditPolicy`" authorization policy. If [`authorizationResult.Succeeded`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationresult.succeeded) is `true`, the user is authorized for the document because they authored the document (`Document.Author` matches the user's [Name](/en-us/dotnet/api/system.security.principal.iidentity.name)).

Note

The following example assumes successful authentication with the `User` property set.

```
public async Task<IActionResult> OnGetAsync(Guid documentId)
{
    Document = _documentRepository.Find(documentId);

    ...

    var authorizationResult = await _authorizationService
            .AuthorizeAsync(User, Document, "EditPolicy");

    ...
}
```

## Create a resource-based handler

Creating a resource-based authorization handler is similar to [creating a plain requirements handler](../../../security/authorization/policies?view=aspnetcore-10.0#authorization-handlers). Create a custom requirement class and implement a requirement handler class. For more information on creating a requirement class, see [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0#requirements-and-policy-registration).

The handler class specifies the requirement and resource type. The following example demonstrates a handler utilizing a `SameAuthorRequirement` requirement and a `Document` resource:

```
public class DocumentAuthorizationHandler : 
    AuthorizationHandler<SameAuthorRequirement, Document>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   SameAuthorRequirement requirement,
                                                   Document resource)
    {
        if (context.User.Identity?.Name == resource.Author)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

public class SameAuthorRequirement : IAuthorizationRequirement { }
```

In the preceding example, imagine that `SameAuthorRequirement` is a special case of a more generic `SpecificAuthorRequirement` class. The `SpecificAuthorRequirement` class (not shown) contains a `Name` property representing the name of the author. The `Name` property could be set to the current user.

Register the requirement and handler in `Program.cs`:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("EditPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));

builder.Services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
```

Register the requirement and handler in `Startup.ConfigureServices`:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("EditPolicy", policy =>
        policy.Requirements.Add(new SameAuthorRequirement()));
});

services.AddSingleton<IAuthorizationHandler, DocumentAuthorizationHandler>();
```

For more information on creating authorization policies, see [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0).

### Operational requirements

To make decisions based on the outcomes of CRUD (Create, Read, Update, Delete) operations, use the [OperationAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.operationauthorizationrequirement) helper class. This class enables you to write a single handler instead of an individual class for each operation type. To use it, provide some operation names:

```
public static class Operations
{
    public static OperationAuthorizationRequirement Create =
        new OperationAuthorizationRequirement { Name = nameof(Create) };
    public static OperationAuthorizationRequirement Read =
        new OperationAuthorizationRequirement { Name = nameof(Read) };
    public static OperationAuthorizationRequirement Update =
        new OperationAuthorizationRequirement { Name = nameof(Update) };
    public static OperationAuthorizationRequirement Delete =
        new OperationAuthorizationRequirement { Name = nameof(Delete) };
}
```

The handler is implemented as follows, using an [OperationAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.operationauthorizationrequirement) requirement and a `Document` resource:

```
public class DocumentAuthorizationCrudHandler :
    AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   OperationAuthorizationRequirement requirement,
                                                   Document resource)
    {
        if (context.User.Identity?.Name == resource.Author &&
            requirement.Name == Operations.Read.Name)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
```

The preceding handler validates the operation using the resource, the user's identity, and the requirement's `Name` property.

## Challenge and forbid with an operational resource handler

This section shows how the challenge and forbid results are processed and how challenge and forbid differ.

When authorization fails but the user is authenticated, the app can return a [ForbidResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.forbidresult), which informs authentication middleware that authorization failed. Return a [ChallengeResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.challengeresult) for unauthenticated users. For interactive browser clients, it may be appropriate to redirect the user to a login page.

Note

The following example assumes successful authentication with the `User` property set.

```
var authorizationResult = await _authorizationService
    .AuthorizeAsync(User, Document, Operations.Read);

if (authorizationResult.Succeeded)
{
    return Page();
}
else if (User.Identity?.IsAuthenticated ?? false)
{
    return new ForbidResult();
}
else
{
    return new ChallengeResult();
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/groups
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/groups.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Manage users and groups in SignalR

By [Brennan Conroy](https://github.com/BrennanConroy)

SignalR allows messages to be sent to all connections associated with a specific user and to named groups of connections.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/groups/samples/) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

## Users in SignalR

A single user in SignalR can have multiple connections to an app. For example, a user could be connected on their desktop as well as their phone. Each device has a separate SignalR connection, but they're all associated with the same user. If a message is sent to the user, all of the connections associated with that user receive the message. The user identifier for a connection can be accessed by the `Context.UserIdentifier` property in the hub.

By default, SignalR uses the `ClaimTypes.NameIdentifier` from the `ClaimsPrincipal` associated with the connection as the user identifier. To customize this behavior, see [Use claims to customize identity handling](authn-and-authz?view=aspnetcore-10.0#use-claims-to-customize-identity-handling).

Send a message to a specific user by passing the user identifier to the `User` function in a hub method, as shown in the following example:

Note

The user identifier is case-sensitive.

```
public Task SendPrivateMessage(string user, string message)
{
    return Clients.User(user).SendAsync("ReceiveMessage", message);
}
```

## Groups in SignalR

A group is a collection of connections associated with a name. Messages can be sent to all connections in a group. Groups are the recommended way to send to a connection or multiple connections because the groups are managed by the application. A connection can be a member of multiple groups. Groups are ideal for something like a chat application, where each room can be represented as a group.

### Add or remove connections from a group

Connections are added to or removed from groups via the `AddToGroupAsync` and `RemoveFromGroupAsync` methods:

```
public async Task AddToGroup(string groupName)
{
    await Groups.AddToGroupAsync(Context.ConnectionId, groupName);

    await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has joined the group {groupName}.");
}

public async Task RemoveFromGroup(string groupName)
{
    await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);

    await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has left the group {groupName}.");
}
```

It's safe to add a user to a group multiple times, no exception is thrown in the case that the user already exists in the group.

Group membership isn't preserved when a connection reconnects. The connection needs to rejoin the group when it's re-established. It's not possible to count the members of a group, since this information isn't available if the application is scaled to multiple servers.

Groups are kept in memory, so they won't persist through a server restart. Consider the Azure SignalR service for scenarios requiring group membership to be persisted. For more information, see [Azure SignalR](/en-us/azure/azure-signalr/signalr-overview)

To protect access to resources while using groups, use [authentication and authorization](authn-and-authz?view=aspnetcore-10.0) functionality in ASP.NET Core. If a user is added to a group only when the credentials are valid for that group, messages sent to that group will only go to authorized users. However, groups are not a security feature. Authentication claims have features that groups don't, such as expiry and revocation. If a user's permission to access the group is revoked, the app must remove the user from the group explicitly.

Note

Group names are case-sensitive.

## Related content


***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/tutorials/windows-forms
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/tutorials/windows-forms.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Windows Forms Blazor app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This tutorial shows you how to build and run a Windows Forms Blazor app. You learn how to:

* Create a Windows Forms Blazor app project
* Run the app on Windows

## Prerequisites

* [Supported platforms (Windows Forms documentation)](/en-us/dotnet/desktop/winforms/overview/)
* [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) with the **.NET desktop development** workload

## Visual Studio workload

If the **.NET desktop development** workload isn't installed, use the Visual Studio installer to install the workload. For more information, see [Modify Visual Studio workloads, components, and language packs](/en-us/visualstudio/install/modify-visual-studio).

## Create a Windows Forms Blazor project

Launch Visual Studio. In the **Start Window**, select **Create a new project**:

In the **Create a new project** dialog, filter the **Project type** dropdown to **Desktop**. Select the C# project template for **Windows Forms App** and select the **Next** button:

In the **Configure your new project** dialog:

* Set the **Project name** to **WinFormsBlazor**.
* Choose a suitable location for the project.
* Select the **Next** button.

In the **Additional information** dialog, select the framework version with the **Framework** dropdown list. Select the **Create** button:

Use [NuGet Package Manager](/en-us/nuget/consume-packages/install-use-packages-visual-studio) to install the [`Microsoft.AspNetCore.Components.WebView.WindowsForms`](https://nuget.org/packages/Microsoft.AspNetCore.Components.WebView.WindowsForms) NuGet package:

In **Solution Explorer**, right-click the project's name, **WinFormsBlazor**, and select **Edit Project File** to open the project file (`WinFormsBlazor.csproj`).

At the top of the project file, change the SDK to `Microsoft.NET.Sdk.Razor`:

```
<Project Sdk="Microsoft.NET.Sdk.Razor">
```

Save the changes to the project file (`WinFormsBlazor.csproj`).

Add an imports file to the root of the project with an [`@using`](../../../mvc/views/razor?view=aspnetcore-10.0#using) directive for [Microsoft.AspNetCore.Components.Web](/en-us/dotnet/api/microsoft.aspnetcore.components.web).

`_Imports.razor`:

```
@using Microsoft.AspNetCore.Components.Web
```

Save the imports file.

Add a `wwwroot` folder to the project.

Add an `index.html` file to the `wwwroot` folder with the following markup.

`wwwroot/index.html`:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WinFormsBlazor</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="WinFormsBlazor.styles.css" rel="stylesheet" />
</head>

<body>

    <div id="app">Loading...</div>

    <div id="blazor-error-ui" data-nosnippet>
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.webview.js"></script>

</body>

</html>
```

Inside the `wwwroot` folder, create a `css` folder to hold stylesheets.

Add an `app.css` stylesheet to the `wwwroot/css` folder with the following content.

`wwwroot/css/app.css`:

```
html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1:focus {
    outline: none;
}

a, .btn-link {
    color: #0071c1;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }
```

Inside the `wwwroot/css` folder, create a `bootstrap` folder. Inside the `bootstrap` folder, place a copy of `bootstrap.min.css`. You can obtain the latest version of `bootstrap.min.css` from the [Bootstrap website](https://getbootstrap.com/). Because all of the content at the site is versioned in the URL, a direct link can't be provided here. Therefore, follow navigation bar links to **Docs** > **Download** to obtain `bootstrap.min.css`.

Add the following `Counter` component to the root of the project, which is the default `Counter` component found in Blazor project templates.

`Counter.razor`:

```
<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

Save the `Counter` component (`Counter.razor`).

In **Solution Explorer**, double-click on the `Form1.cs` file to open the designer:

Open the **Toolbox** by either selecting the **Toolbox** button along the left edge of the Visual Studio window or selecting the **View** > **Toolbox** menu command.

Locate the **`BlazorWebView`** control under **`Microsoft.AspNetCore.Components.WebView.WindowsForms`**. Drag the [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.windowsforms.blazorwebview) from the **Toolbox** into the `Form1` designer. Be careful not to accidentally drag a **`WebView2`** control into the form.

Visual Studio shows the [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.windowsforms.blazorwebview) control in the form designer as `WebView2` and automatically names the control `blazorWebView1`:

In `Form1`, select the [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.windowsforms.blazorwebview) (`WebView2`) with a single click.

In the [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.windowsforms.blazorwebview)'s **Properties**, confirm that the control is named `blazorWebView1`. If the name isn't `blazorWebView1`, the wrong control was dragged from the **Toolbox**. Delete the `WebView2` control in `Form1` and drag the **`BlazorWebView` control** into the form.

In the control's properties, change the [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.windowsforms.blazorwebview)'s **Dock** value to **Fill**:

In the `Form1` designer, right-click `Form1` and select **View Code**.

Add namespaces for [Microsoft.AspNetCore.Components.WebView.WindowsForms](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.windowsforms) and [Microsoft.Extensions.DependencyInjection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection) to the top of the `Form1.cs` file:

```
using Microsoft.AspNetCore.Components.WebView.WindowsForms;
using Microsoft.Extensions.DependencyInjection;
```

Inside the `Form1` constructor, after the `InitializeComponent` method call, add the following code:

```
var services = new ServiceCollection();
services.AddWindowsFormsBlazorWebView();
blazorWebView1.HostPage = "wwwroot\\index.html";
blazorWebView1.Services = services.BuildServiceProvider();
blazorWebView1.RootComponents.Add<Counter>("#app");
```

Note

The `InitializeComponent` method is automatically generated at app build time and added to the compilation object for the calling class.

The final, complete C# code of `Form1.cs` with a [file-scoped namespace](/en-us/dotnet/csharp/language-reference/keywords/namespace):

```
using Microsoft.AspNetCore.Components.WebView.WindowsForms;
using Microsoft.Extensions.DependencyInjection;

namespace WinFormsBlazor;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        var services = new ServiceCollection();
        services.AddWindowsFormsBlazorWebView();
        blazorWebView1.HostPage = "wwwroot\\index.html";
        blazorWebView1.Services = services.BuildServiceProvider();
        blazorWebView1.RootComponents.Add<Counter>("#app");
    }
}
```

## Run the app

Select the start button in the Visual Studio toolbar:

The app running on Windows:

## Next steps

In this tutorial, you learned how to:

* Create a Windows Forms Blazor app project

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/examples/identity
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/examples/identity.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate Authentication and Identity to ASP.NET Core

By [Steve Smith](https://ardalis.com/)

In the previous article, we [migrated configuration from an ASP.NET MVC project to ASP.NET Core MVC](configuration?view=aspnetcore-10.0). In this article, we migrate the registration, login, and user management features.

## Configure Identity and Membership

In ASP.NET MVC, authentication and identity features are configured using ASP.NET Identity in `Startup.Auth.cs` and `IdentityConfig.cs`, located in the *App\_Start* folder. In ASP.NET Core MVC, these features are configured in `Startup.cs`.

Install the following NuGet packages:

* `Microsoft.AspNetCore.Identity.EntityFrameworkCore`
* `Microsoft.AspNetCore.Authentication.Cookies`
* `Microsoft.EntityFrameworkCore.SqlServer`

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../../../security/?view=aspnetcore-10.0#secure-authentication-flows).

In `Startup.cs`, update the `Startup.ConfigureServices` method to use Entity Framework and Identity services:

```
public void ConfigureServices(IServiceCollection services)
{
    // Add EF services to the services container.
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

     services.AddMvc();
}
```

At this point, there are two types referenced in the above code that we haven't yet migrated from the ASP.NET MVC project: `ApplicationDbContext` and `ApplicationUser`. Create a new *Models* folder in the ASP.NET Core project, and add two classes to it corresponding to these types. You will find the ASP.NET MVC versions of these classes in `/Models/IdentityModels.cs`, but we will use one file per class in the migrated project since that's more clear.

`ApplicationUser.cs`:

```
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace NewMvcProject.Models
{
    public class ApplicationUser : IdentityUser
    {
    }
}
```

`ApplicationDbContext.cs`:

```
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Data.Entity;

namespace NewMvcProject.Models
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            // Customize the ASP.NET Core Identity model and override the defaults if needed.
            // For example, you can rename the ASP.NET Core Identity table names and more.
            // Add your customizations after calling base.OnModelCreating(builder);
        }
    }
}
```

The ASP.NET Core MVC Starter Web project doesn't include much customization of users, or the `ApplicationDbContext`. When migrating a real app, you also need to migrate all of the custom properties and methods of your app's user and `DbContext` classes, as well as any other Model classes your app utilizes. For example, if your `DbContext` has a `DbSet<Album>`, you need to migrate the `Album` class.

With these files in place, the `Startup.cs` file can be made to compile by updating its `using` statements:

```
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
```

Our app is now ready to support authentication and Identity services. It just needs to have these features exposed to users.

## Migrate registration and login logic

With Identity services configured for the app and data access configured using Entity Framework and SQL Server, we're ready to add support for registration and login to the app.

Uncomment the `@Html.Partial` line in `_Layout.cshtml`:

```
      <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
    </ul>
    @*@Html.Partial("_LoginPartial")*@
  </div>
</div>
```

Now, add a new Razor view called *\_LoginPartial* to the *Views/Shared* folder:

Update `_LoginPartial.cshtml` with the following code (replace all of its contents):

```
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager

@if (SignInManager.IsSignedIn(User))
{
    <form asp-area="" asp-controller="Account" asp-action="Logout" method="post" id="logoutForm" class="navbar-right">
        <ul class="nav navbar-nav navbar-right">
            <li>
                <a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello @UserManager.GetUserName(User)!</a>
            </li>
            <li>
                <button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button>
            </li>
        </ul>
    </form>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li><a asp-area="" asp-controller="Account" asp-action="Register">Register</a></li>
        <li><a asp-area="" asp-controller="Account" asp-action="Login">Log in</a></li>
    </ul>
}
```

At this point, you should be able to refresh the site in your browser.

## Summary

ASP.NET Core introduces changes to the ASP.NET Identity features. In this article, you have seen how to migrate the authentication and user management features of ASP.NET Identity to ASP.NET Core.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/sections
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/sections.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor sections

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to control the content in a Razor component from a child Razor component.

## Blazor sections

To control the content in a Razor component from a child Razor component, Blazor supports *sections* using the following built-in components:

* [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet): Renders content provided by [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) components with matching [SectionName](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionname) or [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid) arguments. Two or more [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet) components can't have the same [SectionName](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionname) or [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid).

* [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent): Provides content as a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) to [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet) components with a matching [SectionName](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionname) or [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid). If several [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) components have the same [SectionName](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionname) or [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid), the matching [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet) component renders the content of the last rendered [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent).

Sections can be used in both [layouts](layouts?view=aspnetcore-10.0) and across nested parent-child components.

Although the argument passed to [SectionName](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionname) can use any type of casing, the documentation adopts kebab casing (for example, `top-bar`), which is a common casing choice for HTML element IDs. [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid) receives a static `object` field, and we always recommend Pascal casing for C# field names (for example, `TopbarSection`).

In the following example, the app's main layout component implements an increment counter button for the app's `Counter` component.

If the namespace for sections isn't in the imports file (`_Imports.razor`), add it:

```
@using Microsoft.AspNetCore.Components.Sections
```

In the `MainLayout` component (`MainLayout.razor`), place a [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet) component and pass a string to the [SectionName](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionname) parameter to indicate the section's name. The following example uses the section name `top-bar`:

```
<SectionOutlet SectionName="top-bar" />
```

In the `Counter` component (`Counter.razor`), create a [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) component and pass the matching string (`top-bar`) to its [SectionName](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionname) parameter:

```
<SectionContent SectionName="top-bar">
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</SectionContent>
```

When the `Counter` component is accessed at `/counter`, the `MainLayout` component renders the increment count button from the `Counter` component where the [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet) component is placed. When any other component is accessed, the increment count button isn't rendered.

Instead of using a named section, you can pass a static `object` with the [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid) parameter to identify the section. The following example also implements an increment counter button for the app's `Counter` component in the app's main layout.

If you don't want other [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) components to accidentally match the name of a [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet), pass an object [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid) parameter to identify the section. This can be useful when designing a [Razor class library (RCL)](class-libraries?view=aspnetcore-10.0). When a [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet) in the RCL uses an object reference with [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid) and the consumer places a [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) component with a matching [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid) object, an accidental match by name isn't possible when consumers of the RCL implement other [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) components.

The following example also implements an increment counter button for the app's `Counter` component in the app's main layout, using an object reference instead of a section name.

Add a `TopbarSection` static `object` to the `MainLayout` component in an `@code` block:

```
@code {
    internal static object TopbarSection = new();
}
```

In the `MainLayout` component's Razor markup, place a [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet) component and pass `TopbarSection` to the [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid) parameter to indicate the section:

```
<SectionOutlet SectionId="TopbarSection" />
```

Add a [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) component to the app's `Counter` component that renders an increment count button. Use the `MainLayout` component's `TopbarSection` section static `object` as the [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid) (`MainLayout.TopbarSection`).

In `Counter.razor`:

```
<SectionContent SectionId="MainLayout.TopbarSection">
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</SectionContent>
```

When the `Counter` component is accessed, the `MainLayout` component renders the increment count button where the [SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet) component is placed.

Note

[SectionOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet) and [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) components can only set either [SectionId](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionid) or [SectionName](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectionoutlet.sectionname), not both.

## `RenderFragment` caching rules and section rendering behavior

When a [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent)'s [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) content changes, which is a different instance than the component where it's rendered, Blazor completely destroys and recreates the section instead of attempting to update the section's content. Unlike normal rendering, the section's content could come from different instances, and it doesn't make sense to attempt processing content from two separate components, which might lead to unexpected results. For a detailed explanation on this behavior, see [Inconsistent component initialization with Blazor SectionOutlet/SectionContent and CascadingValue (`dotnet/aspnetcore` #58316)](https://github.com/dotnet/aspnetcore/issues/58316).

## Section interaction with other Blazor features

A section interacts with other Blazor features in the following ways:

* [Cascading values](cascading-values-and-parameters?view=aspnetcore-10.0) flow into section content from where the content is defined by the [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) component.
* Unhandled exceptions are handled by [error boundaries](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries) defined around a [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) component.
* A Razor component configured for [streaming rendering](rendering?view=aspnetcore-10.0#streaming-rendering) also configures section content provided by a [SectionContent](/en-us/dotnet/api/microsoft.aspnetcore.components.sections.sectioncontent) component to use streaming rendering.
* A section that contains interactive components is statically rendered (non-functional) in a layout component in a Blazor Web App that adopts per-page/component rendering. For more information, see [ASP.NET Core Blazor layouts](layouts?view=aspnetcore-10.0#statically-rendered-layout-components).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0037
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0037.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0037: `[ValidatableType]` cannot be used in generated code

|              | Value   |
| ------------ | ------- |
| **Rule ID**  | ASP0037 |
| **Category** | Usage   |
| **Severity** | Warning |

## Cause

The [ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute) attribute is applied to a type declared in generated code, such as code generated from a Razor (`.razor`) file.

## Rule description

The validation source generator can't inspect another source generator's output. Applying `[ValidatableType]` to a type in generated code has no effect, so the type isn't included in generated validation metadata.

For example, declaring the following model in a Razor component's `@code` block produces this diagnostic:

```
@code {
    [ValidatableType]
    public class Order
    {
        [Required]
        public string? CustomerName { get; set; }
    }
}
```

## How to fix violations

Move the type to a regular C# (`.cs`) file and apply `[ValidatableType]` there:

```
[ValidatableType]
public class Order
{
    [Required]
    public string? CustomerName { get; set; }
}
```

For more information, see [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-12.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/routing.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Routing to controller actions in ASP.NET Core

By [Ryan Nowak](https://github.com/rynowak), [Kirk Larkin](https://twitter.com/serpent5), and [Rick Anderson](https://twitter.com/RickAndMSFT)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

ASP.NET Core controllers use the Routing [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) to match the URLs of incoming requests and map them to [actions](#action). Route templates:

* Are defined at startup in `Program.cs` or in attributes.
* Describe how URL paths are matched to [actions](#action).
* Are used to generate URLs for links. The generated links are typically returned in responses.

Actions are either [conventionally-routed](#cr6) or [attribute-routed](#ar6). Placing a route on the controller or [action](#action) makes it attribute-routed. See [Mixed routing](#routing-mixed-ref-label) for more information.

This document:

* Explains the interactions between MVC and routing:

  * How typical MVC apps make use of routing features.

  * Covers both:

    * [Conventional routing](#cr6) typically used with controllers and views.
    * *Attribute routing* used with REST APIs. If you're primarily interested in routing for REST APIs, jump to the [Attribute routing for REST APIs](#ar6) section.

  * See [Routing](../../fundamentals/routing?view=aspnetcore-10.0) for advanced routing details.

* Refers to the default routing system as endpoint routing. You can use controllers with the previous version of routing for compatibility purposes. See the [2.2-3.0 migration guide](../../migration/22-to-30?view=aspnetcore-10.0) for instructions.

[]()

## Set up conventional route

The ASP.NET Core MVC template generates [conventional routing](#crd6) code similar to the following example:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

Use [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) to create a single route. The single route is the `default` route. Most apps with controllers and views use a route template similar to the `default` route. REST APIs should use [attribute routing](#ar6).

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

The route template `"{controller=Home}/{action=Index}/{id?}"`:

* Matches a URL path like `/Products/Details/5`

* Extracts the route values `{ controller = Products, action = Details, id = 5 }` by tokenizing the path. The extraction of route values results in a match if the app has a controller named `ProductsController` and a `Details` action:

  ```
  public class ProductsController : Controller
  {
      public IActionResult Details(int id)
      {
          return ControllerContext.MyDisplayRouteInfo(id);
      }
  }
  ```

  [MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.

* `/Products/Details/5` model binds the value of `id = 5` to set the `id` parameter to `5`. For more information, see [Model Binding](../models/model-binding?view=aspnetcore-10.0).

* `{controller=Home}` defines `Home` as the default `controller`.

* `{action=Index}` defines `Index` as the default `action`.

* The `?` character in `{id?}` defines `id` as optional.

  * Default and optional route parameters don't need to be present in the URL path for a match. For a detailed description of route template syntax, see [Route Template Reference](../../fundamentals/routing?view=aspnetcore-10.0#route-template-reference).

* Matches the URL path `/`.

* Produces the route values `{ controller = Home, action = Index }`.

The values for `controller` and `action` make use of the default values. `id` doesn't produce a value since there's no corresponding segment in the URL path. `/` only matches if there exists a `HomeController` and `Index` action:

```
public class HomeController : Controller
{
    public IActionResult Index() { ... }
}
```

Using the preceding controller definition and route template, the `HomeController.Index` action runs for the following URL paths:

* `/Home/Index/17`
* `/Home/Index`
* `/Home`
* `/`

The URL path `/` uses the route template default `Home` controller and `Index` action. The URL path `/Home` uses the route template default `Index` action.

The convenience method [MapDefaultControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapdefaultcontrollerroute):

```
app.MapDefaultControllerRoute();
```

Replaces:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

Important

Routing is configured by using the [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) middleware. To use controllers:

* Call [MapControllers](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollers) to map [attribute routed](#ar6) controllers.
* Call [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) or [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute), to map both [conventionally routed](#cr6) controllers and [attribute routed](#ar6) controllers.

Apps typically don't need to call `UseRouting` or `UseEndpoints`. [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) configures a middleware pipeline that wraps middleware added in `Program.cs` by using `UseRouting` and `UseEndpoints`. For more information, see [Routing in ASP.NET Core](../../fundamentals/routing?view=aspnetcore-10.0).

[]()[]()

## Conventional routing

Use conventional routing with controllers and views. The `default` route is:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

The preceding code is an example of a *conventional route*. It's called *conventional routing* because it establishes a *convention* for URL paths:

* The first path segment, `{controller=Home}`, maps to the controller name.
* The second segment, `{action=Index}`, maps to the [action](#action) name.
* The third segment, `{id?}` is used for an optional `id`. The `?` in `{id?}` makes it optional. `id` maps to a model entity.

Using this `default` route, the URL path:

* `/Products/List` maps to the `ProductsController.List` action.
* `/Blog/Article/17` maps to `BlogController.Article` and typically model binds the `id` parameter to 17.

This mapping:

* Is based on the controller and [action](#action) names **only**.
* Isn't based on namespaces, source file locations, or method parameters.

By using conventional routing with the default route, you don't need to create a new URL pattern for each action. For an app with [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) style actions, having consistency for the URLs across controllers:

* Helps simplify the code.
* Makes the UI more predictable.

Warning

The route template defines the `id` as optional. Actions can execute without the optional ID provided as part of the URL. Generally, when `id` is omitted from the URL:

* Model binding sets `id` to `0`.
* No entity is found in the database matching `id == 0`.

[Attribute routing](#ar6) provides fine-grained control to make the ID required for some actions and not for others. By convention, the documentation includes optional parameters like `id` when they're likely to appear in correct usage.

Most apps should choose a basic and descriptive routing scheme so that URLs are readable and meaningful. The default conventional route `{controller=Home}/{action=Index}/{id?}`:

* Supports a basic and descriptive routing scheme.
* Is a useful starting point for UI-based apps.
* Is the only route template needed for many web UI apps. For larger web UI apps, another route using [Areas](#areas) is frequently all that's needed.

[MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) and [MapAreaRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcarearoutebuilderextensions.maparearoute) :

* Automatically assign an **order** value to their endpoints based on the order they are invoked.

Endpoint routing in ASP.NET Core:

* Doesn't have a concept of routes.
* Doesn't provide ordering guarantees for the execution of extensibility. All endpoints are processed at once.

Enable [Logging](../../fundamentals/logging/?view=aspnetcore-10.0) to see how the built-in routing implementations, such as [Route](/en-us/dotnet/api/microsoft.aspnetcore.routing.route), match requests.

[Attribute routing](#ar6) is explained later in this document.

[]()

### Multiple conventional routes

You can configure multiple [conventional routes](#cr6) by adding more calls to [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) and [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute). When you add these calls, you can define multiple conventions or add conventional routes that are dedicated to a specific [action](#action), such as:

```
app.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");
```

[]()

The `blog` route in the preceding code is a **dedicated conventional route**. It's a dedicated conventional route because:

* It uses [conventional routing](#cr6).
* It's dedicated to a specific [action](#action).

Because the route template `"blog/{*article}"` doesn't include `controller` and `action` as parameters:

* They can only have the default values `{ controller = "Blog", action = "Article" }`.
* This route always maps to the action `BlogController.Article`.

`/Blog`, `/Blog/Article`, and `/Blog/{any-string}` are the only URL paths that match the blog route.

In the preceding example:

* The `blog` route has a higher priority for matches than the `default` route because you add it first.
* It's an example of [Slug](https://developer.mozilla.org/docs/Glossary/Slug) style routing where it's typical to have an article name as part of the URL.

Warning

In ASP.NET Core, routing doesn't:

* Define a concept called a *route*. `UseRouting` adds route matching to the middleware pipeline. The `UseRouting` middleware looks at the set of endpoints defined in the app, and selects the best endpoint match based on the request.
* Provide guarantees about the execution order of extensibility like [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) or [IActionConstraint](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionconstraints.iactionconstraint).

See [Routing](../../fundamentals/routing?view=aspnetcore-10.0) for reference material on routing.

[]()

### Conventional routing order

Conventional routing only matches a combination of action and controller that the app defines. This approach simplifies cases where conventional routes overlap. When you add routes by using [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute), [MapDefaultControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapdefaultcontrollerroute), and [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute), the endpoints automatically get an order value based on the order you invoke these methods. Matches from a route that appears earlier in the list have a higher priority. Conventional routing depends on order. In general, place routes with areas earlier because they're more specific than routes without an area. [Dedicated conventional routes](#dcr) with catch-all route parameters, such as `{*article}`, can make a route too [greedy](../../fundamentals/routing?view=aspnetcore-10.0#greedy). A greedy route matches URLs that you intended to be matched by other routes. Put the greedy routes later in the route table to prevent greedy matches.

```
public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
```

[]()

### Resolving ambiguous actions

When two endpoints match through routing, routing must do one of the following steps:

* Choose the best candidate.
* Throw an exception.

For example:

```
public class Products33Controller : Controller
{
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpPost]
    public IActionResult Edit(int id, Product product)
    {
        return ControllerContext.MyDisplayRouteInfo(id, product.name);
    }
}
```

The preceding controller defines two actions that match:

* The URL path `/Products33/Edit/17`
* Route data `{ controller = Products33, action = Edit, id = 17 }`.

This is a typical pattern for MVC controllers:

* `Edit(int)` displays a form to edit a product.
* `Edit(int, Product)` processes the posted form.

To resolve the correct route:

* `Edit(int, Product)` is selected when the request is an HTTP `POST`.
* `Edit(int)` is selected when the [HTTP verb](#verb) is anything else. `Edit(int)` is generally called via `GET`.

The [HttpPostAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute), `[HttpPost]`, is provided to routing so that it can choose based on the HTTP method of the request. The `HttpPostAttribute` makes `Edit(int, Product)` a better match than `Edit(int)`.

It's important to understand the role of attributes like `HttpPostAttribute`. Similar attributes are defined for other [HTTP verbs](#verb). In [conventional routing](#cr6), actions often use the same action name when they're part of a show form, submit form workflow. For example, see [Examine the two Edit action methods](../../tutorials/first-mvc-app/controller-methods-views?view=aspnetcore-10.0#get-post).

If routing can't choose the best candidate, it throws an [AmbiguousMatchException](/en-us/dotnet/api/system.reflection.ambiguousmatchexception) and lists the multiple matched endpoints.

[]()

### Conventional route names

The strings `"blog"` and `"default"` in the following examples are conventional route names:

```
app.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");
```

The route names give the route a logical name. The named route can be used for URL generation. Using a named route simplifies URL creation when the ordering of routes could make URL generation complicated. Route names must be unique application wide.

Route names:

* Have no impact on URL matching or handling of requests.
* Are used only for URL generation.

The route name concept is represented in routing as [IEndpointNameMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointnamemetadata). The terms **route name** and **endpoint name**:

* Are interchangeable.
* Which one is used in documentation and code depends on the API being described.

[]()[]()

## Attribute routing for REST APIs

REST APIs should use attribute routing to model the app's functionality as a set of resources where operations are represented by [HTTP verbs](#verb).

Attribute routing uses a set of attributes to map actions directly to route templates. The following code is typical for a REST API and is used in the next sample:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

In the preceding code, you call [MapControllers](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollers) to map attribute routed controllers.

In the following example:

* `HomeController` matches a set of URLs similar to what the default conventional route `{controller=Home}/{action=Index}/{id?}` matches.

```
public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

The `HomeController.Index` action runs for any of the URL paths `/`, `/Home`, `/Home/Index`, or `/Home/Index/3`.

This example highlights a key programming difference between attribute routing and [conventional routing](#cr6). Attribute routing requires more input to specify a route. The conventional default route handles routes more succinctly. However, attribute routing allows and requires precise control of which route templates apply to each [action](#action).

With attribute routing, the controller and action names play no part in which action is matched, unless [token replacement](#routing-token-replacement-templates-ref-label) is used. The following example matches the same URLs as the previous example:

```
public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

The following code uses token replacement for `action` and `controller`:

```
public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("[controller]/[action]")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("[controller]/[action]")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The following code applies `[Route("[controller]/[action]")]` to the controller:

```
[Route("[controller]/[action]")]
public class HomeController : Controller
{
    [Route("~/")]
    [Route("/Home")]
    [Route("~/Home/Index")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

In the preceding code, the `Index` method templates must prepend `/` or `~/` to the route templates. Route templates applied to an action that begin with `/` or `~/` don't get combined with route templates applied to the controller.

See [Route template precedence](../../fundamentals/routing?view=aspnetcore-10.0#rtp) for information on route template selection.

## Reserved routing names

The following keywords are reserved route parameter names when you use Controllers or Razor Pages:

* `action`
* `area`
* `controller`
* `handler`
* `page`

Using `page` as a route parameter with attribute routing is a common error. This choice results in inconsistent and confusing behavior with URL generation.

```
public class MyDemo2Controller : Controller
{
    [Route("/articles/{page}")]
    public IActionResult ListArticles(int page)
    {
        return ControllerContext.MyDisplayRouteInfo(page);
    }
}
```

URL generation uses these special parameter names to determine if a URL generation operation refers to a Razor Page or to a Controller.

The following keywords are reserved in the context of a Razor view or a Razor Page:

* `page`
* `using`
* `namespace`
* `inject`
* `section`
* `inherits`
* `model`
* `addTagHelper`
* `removeTagHelper`

Don't use these keywords for link generations, model bound parameters, or top level properties.

[]()

## HTTP verb templates

ASP.NET Core includes the following HTTP verb templates:

* [\[HttpGet\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute)
* [\[HttpPost\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute)
* [\[HttpPut\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpputattribute)
* [\[HttpDelete\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpdeleteattribute)
* [\[HttpHead\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpheadattribute)
* [\[HttpPatch\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute)

[]()

### Route templates

ASP.NET Core includes the following route templates:

* All the [HTTP verb templates](#verb6) are route templates.
* [\[Route\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute)

[]()

### Attribute routing with HTTP verb attributes

Consider the following controller:

```
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
    [HttpGet]   // GET /api/test2
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
       return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

In the preceding code:

* Each action contains the `[HttpGet]` attribute, which constrains matching to HTTP GET requests only.

* The `GetProduct` action includes the `"{id}"` template, so the `id` appends to the `"api/[controller]"` template on the controller. The method's template is `"api/[controller]/{id}"`. Therefore this action only matches GET requests for the form `/api/test2/xyz`, `/api/test2/123`, `/api/test2/{any string}`, and so on.
  ```
  [HttpGet("{id}")]   // GET /api/test2/xyz
  public IActionResult GetProduct(string id)
  {
     return ControllerContext.MyDisplayRouteInfo(id);
  }
  ```

* The `GetIntProduct` action contains the `"int/{id:int}"` template. The `:int` portion of the template constrains the `id` route values to strings that can be converted to an integer. A GET request to `/api/test2/int/abc`:

  * Doesn't match this action.
  * Returns a [404 Not Found](https://developer.mozilla.org/docs/Web/HTTP/Status/404) error.
    ```
    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
    ```

* The `GetInt2Product` action contains `{id}` in the template, but doesn't constrain `id` to values that can be converted to an integer. A GET request to `/api/test2/int2/abc`:

  * Matches this route.
  * Model binding fails to convert `abc` to an integer. The `id` parameter of the method is integer.
  * Returns a [400 Bad Request](https://developer.mozilla.org/docs/Web/HTTP/Status/400) because model binding failed to convert `abc` to an integer.
    ```
    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
    ```

Attribute routing can use [HttpMethodAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.httpmethodattribute) attributes such as [HttpPostAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute), [HttpPutAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpputattribute), and [HttpDeleteAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpdeleteattribute). All of the [HTTP verb](#verb6) attributes accept a route template. The following example shows two actions that match the same route template:

```
[ApiController]
public class MyProductsController : ControllerBase
{
    [HttpGet("/products3")]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpPost("/products3")]
    public IActionResult CreateProduct(MyProduct myProduct)
    {
        return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
    }
}
```

Using the URL path `/products3`:

* The `MyProductsController.ListProducts` action runs when the [HTTP verb](#verb6) is `GET`.
* The `MyProductsController.CreateProduct` action runs when the [HTTP verb](#verb6) is `POST`.

When building a REST API, it's rare that you need to use `[Route(...)]` on an action method because the action accepts all HTTP methods. Use the more specific [HTTP verb attribute](#verb6) to be precise about what your API supports. Clients of REST APIs are expected to know what paths and HTTP verbs map to specific logical operations.

REST APIs should use attribute routing to model the app's functionality as a set of resources where operations are represented by HTTP verbs. This design means that many operations, such as GET and POST on the same logical resource, use the same URL. Attribute routing provides the level of control needed to carefully design an API's public endpoint layout.

Since an attribute route applies to a specific action, it's easy to make parameters required as part of the route template definition. In the following example, `id` is required as part of the URL path:

```
[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

The `Products2ApiController.GetProduct(int)` action:

* Is run with URL path like `/products2/3`
* Isn't run with the URL path `/products2`.

The [\[Consumes\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute allows an action to limit the supported request content types. For more information, see [Define supported request content types with the Consumes attribute](../../web-api/?view=aspnetcore-10.0#consumes).

See [Routing](../../fundamentals/routing?view=aspnetcore-10.0) for a full description of route templates and related options.

For more information on `[ApiController]`, see [ApiController attribute](../../web-api/?view=aspnetcore-10.0#apicontroller-attribute).

## Route name

The following code defines a route name of `Products_List`:

```
[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

Use route names to generate a URL based on a specific route. Route names:

* Have no impact on the URL matching behavior of routing.
* Are only used for URL generation.

Route names must be unique application-wide.

Contrast the preceding code with the conventional default route, which defines the `id` parameter as optional (`{id?}`). The ability to precisely specify APIs has advantages, such as allowing `/products` and `/products/5` to be dispatched to different actions.

[]()

## Combining attribute routes

To make attribute routing less repetitive, combine route attributes on the controller with route attributes on the individual actions. The route templates you define on the controller are prepended to route templates on the actions. When you place a route attribute on the controller, **all** actions in the controller use attribute routing.

```
[ApiController]
[Route("products")]
public class ProductsApiController : ControllerBase
{
    [HttpGet]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

In the preceding example:

* The URL path `/products` can match `ProductsApi.ListProducts`
* The URL path `/products/5` can match `ProductsApi.GetProduct(int)`.

Both of these actions only match HTTP `GET` because they're marked with the `[HttpGet]` attribute.

Route templates that you apply to an action and that begin with `/` or `~/` don't get combined with route templates that you apply to the controller. The following example matches a set of URL paths similar to the default route.

```
[Route("Home")]
public class HomeController : Controller
{
    [Route("")]
    [Route("Index")]
    [Route("/")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("About")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The following table explains the `[Route]` attributes in the preceding code.

| Attribute          | Combines with `[Route("Home")]` | Defines route template |
| ------------------ | ------------------------------- | ---------------------- |
| `[Route("")]`      | Yes                             | `"Home"`               |
| `[Route("Index")]` | Yes                             | `"Home/Index"`         |
| `[Route("/")]`     | **No**                          | `""`                   |
| `[Route("About")]` | Yes                             | `"Home/About"`         |

[]()[]()

### Attribute route order

Routing builds a tree and matches all endpoints simultaneously:

* The route entries behave as if they're placed in an ideal ordering.
* The most specific routes have a chance to execute before the more general routes.

For example, an attribute route like `blog/search/{topic}` is more specific than an attribute route like `blog/{*article}`. The `blog/search/{topic}` route has higher priority, by default, because it's more specific. By using [conventional routing](#cr6), the developer is responsible for placing routes in the desired order.

Attribute routes can configure an order using the [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute.order#microsoft-aspnetcore-mvc-routeattribute-order) property. All of the framework provided [route attributes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute) include `Order` . Routes are processed according to an ascending sort of the `Order` property. The default order is `0`. Setting a route using `Order = -1` runs before routes that don't set an order. Setting a route using `Order = 1` runs after default route ordering.

**Avoid** depending on `Order`. If an app's URL-space requires explicit order values to route correctly, then it's likely confusing to clients as well. In general, attribute routing selects the correct route with URL matching. If the default order used for URL generation isn't working, using a route name as an override is usually simpler than applying the `Order` property.

Consider the following two controllers which both define the route matching `/home`:

```
public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

```
public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

Requesting `/home` by using the preceding code throws an exception similar to the following:

```
AmbiguousMatchException: The request matched multiple endpoints. Matches:

 WebMvcRouting.Controllers.HomeController.Index
 WebMvcRouting.Controllers.MyDemoController.MyIndex
```

Adding `Order` to one of the route attributes resolves the ambiguity:

```
[Route("")]
[Route("Home", Order = 2)]
[Route("Home/MyIndex")]
public IActionResult MyIndex()
{
    return ControllerContext.MyDisplayRouteInfo();
}
```

With the preceding code, `/home` runs the `HomeController.Index` endpoint. To get to the `MyDemoController.MyIndex`, request `/home/MyIndex`. **Note**:

* The preceding code is an example of poor routing design. It illustrates the `Order` property.
* The `Order` property only resolves the ambiguity. That template cannot be matched. It's better to remove the `[Route("Home")]` template.

For information on route order with Razor Pages, see [Razor Pages route and app conventions: Route order](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0#route-order).

In some cases, an HTTP 500 error is returned with ambiguous routes. Use [logging](../../fundamentals/logging/?view=aspnetcore-10.0) to see which endpoints caused the `AmbiguousMatchException`.

[]()

## Token replacement in route templates \[controller], \[action], \[area]

For convenience, attribute routes support *token replacement* by enclosing a token in square brackets (`[`, `]`). The tokens `[action]`, `[area]`, and `[controller]` are replaced with the values of the action name, area name, and controller name from the action where you define the route:

```
[Route("[controller]/[action]")]
public class Products0Controller : Controller
{
    [HttpGet]
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }


    [HttpGet("{id}")]
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

In the preceding code:

```
[HttpGet]
public IActionResult List()
{
    return ControllerContext.MyDisplayRouteInfo();
}
```

* Matches `/Products0/List`

```
[HttpGet("{id}")]
public IActionResult Edit(int id)
{
    return ControllerContext.MyDisplayRouteInfo(id);
}
```

* Matches `/Products0/Edit/{id}`

Token replacement happens as the last step of building the attribute routes. The preceding example behaves the same as the following code:

```
public class Products20Controller : Controller
{
    [HttpGet("[controller]/[action]")]  // Matches '/Products20/List'
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("[controller]/[action]/{id}")]   // Matches '/Products20/Edit/{id}'
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

If you are reading this in a language other than English, let us know in this [GitHub discussion issue](https://github.com/dotnet/AspNetCore.Docs/issues/16455) if you'd like to see the code comments in your native language.

You can also combine attribute routes with inheritance. This combination is powerful when you use token replacement. Token replacement also applies to route names defined by attribute routes. `[Route("[controller]/[action]", Name="[controller]_[action]")]`generates a unique route name for each action:

```
[ApiController]
[Route("api/[controller]/[action]", Name = "[controller]_[action]")]
public abstract class MyBase2Controller : ControllerBase
{
}

public class Products11Controller : MyBase2Controller
{
    [HttpGet]                      // /api/products11/list
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]             //    /api/products11/edit/3
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

To match the literal token replacement delimiter `[` or `]`, escape it by repeating the character (`[[` or `]]`).

[]()

### Use a parameter transformer to customize token replacement

You can customize token replacement by using a parameter transformer. A parameter transformer implements [IOutboundParameterTransformer](/en-us/dotnet/api/microsoft.aspnetcore.routing.ioutboundparametertransformer) and transforms the value of parameters. For example, a custom `SlugifyParameterTransformer` parameter transformer changes the `SubscriptionManagement` route value to `subscription-management`:

```
using System.Text.RegularExpressions;

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value == null) { return null; }

        return Regex.Replace(value.ToString()!,
                             "([a-z])([A-Z])",
                             "$1-$2",
                             RegexOptions.CultureInvariant,
                             TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}
```

The [RouteTokenTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.routetokentransformerconvention) is an application model convention that:

* Apply a parameter transformer to all attribute routes in an application.
* Customizes the attribute route token values as they're replaced.

```
public class SubscriptionManagementController : Controller
{
    [HttpGet("[controller]/[action]")]
    public IActionResult ListAll()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The preceding `ListAll` method matches `/subscription-management/list-all`.

The `RouteTokenTransformerConvention` is registered as an option:

```
using Microsoft.AspNetCore.Mvc.ApplicationModels;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
    options.Conventions.Add(new RouteTokenTransformerConvention(
                                 new SlugifyParameterTransformer()));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

For the definition of slug, see [MDN web docs on Slug](https://developer.mozilla.org/docs/Glossary/Slug).

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

[]()

### Multiple attribute routes

Attribute routing supports defining multiple routes that reach the same action. The most common usage of this is to mimic the behavior of the default conventional route as shown in the following example:

```
[Route("[controller]")]
public class Products13Controller : Controller
{
    [Route("")]     // Matches 'Products13'
    [Route("Index")] // Matches 'Products13/Index'
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
```

Putting multiple route attributes on the controller means that each one combines with each of the route attributes on the action methods:

```
[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
    [HttpPost("Buy")]       // Matches 'Products6/Buy' and 'Store/Buy'
    [HttpPost("Checkout")]  // Matches 'Products6/Checkout' and 'Store/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

All the [HTTP verb](#verb6) route constraints implement `IActionConstraint`.

When multiple route attributes that implement [IActionConstraint](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionconstraints.iactionconstraint) are placed on an action:

* Each action constraint combines with the route template applied to the controller.

```
[Route("api/[controller]")]
public class Products7Controller : ControllerBase
{
    [HttpPut("Buy")]        // Matches PUT 'api/Products7/Buy'
    [HttpPost("Checkout")]  // Matches POST 'api/Products7/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

Using multiple routes on actions might seem useful and powerful, it's better to keep your app's URL space basic and well defined. Use multiple routes on actions **only** where needed, for example, to support existing clients.

[]()

### Specifying attribute route optional parameters, default values, and constraints

Attribute routes support the same inline syntax as conventional routes to specify optional parameters, default values, and constraints.

```
public class Products14Controller : Controller
{
    [HttpPost("product14/{id:int}")]
    public IActionResult ShowProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

In the preceding code, `[HttpPost("product14/{id:int}")]` applies a route constraint. The `Products14Controller.ShowProduct` action is matched only by URL paths like `/product14/3`. The route template portion `{id:int}` constrains that segment to only integers.

For a detailed description of route template syntax, see [Route Template Reference](../../fundamentals/routing?view=aspnetcore-10.0#route-template-reference).

[]()

### Custom route attributes using IRouteTemplateProvider

All of the [route attributes](#rt6) implement [IRouteTemplateProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.iroutetemplateprovider). The ASP.NET Core runtime:

* Looks for attributes on controller classes and action methods when the app starts.
* Uses the attributes that implement `IRouteTemplateProvider` to build the initial set of routes.

Implement `IRouteTemplateProvider` to define custom route attributes. Each `IRouteTemplateProvider` allows you to define a single route with a custom route template, order, and name:

```
public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
{
    public string Template => "api/[controller]";
    public int? Order => 2;
    public string Name { get; set; } = string.Empty;
}

[MyApiController]
[ApiController]
public class MyTestApiController : ControllerBase
{
    // GET /api/MyTestApi
    [HttpGet]
    public IActionResult Get()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The preceding `Get` method returns `Order = 2, Template = api/MyTestApi`.

[]()

### Use application model to customize attribute routes

The application model:

* Is an object model created at startup in `Program.cs`.
* Contains all of the metadata used by ASP.NET Core to route and execute the actions in an app.

The application model includes all of the data gathered from route attributes. The data from route attributes is provided by the `IRouteTemplateProvider` implementation. Conventions:

* Can be written to modify the application model to customize how routing behaves.
* Are read at app startup.

This section shows a basic example of customizing routing by using the application model. The following code makes routes roughly line up with the folder structure of the project.

```
public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
    private readonly string _baseNamespace;

    public NamespaceRoutingConvention(string baseNamespace)
    {
        _baseNamespace = baseNamespace;
    }

    public void Apply(ControllerModel controller)
    {
        var hasRouteAttributes = controller.Selectors.Any(selector =>
                                                selector.AttributeRouteModel != null);
        if (hasRouteAttributes)
        {
            return;
        }

        var namespc = controller.ControllerType.Namespace;
        if (namespc == null)
            return;
        var template = new StringBuilder();
        template.Append(namespc, _baseNamespace.Length + 1,
                        namespc.Length - _baseNamespace.Length - 1);
        template.Replace('.', '/');
        template.Append("/[controller]/[action]/{id?}");

        foreach (var selector in controller.Selectors)
        {
            selector.AttributeRouteModel = new AttributeRouteModel()
            {
                Template = template.ToString()
            };
        }
    }
}
```

The following code prevents the `namespace` convention from being applied to controllers that are attribute routed:

```
public void Apply(ControllerModel controller)
{
    var hasRouteAttributes = controller.Selectors.Any(selector =>
                                            selector.AttributeRouteModel != null);
    if (hasRouteAttributes)
    {
        return;
    }
```

For example, the following controller doesn't use `NamespaceRoutingConvention`:

```
[Route("[controller]/[action]/{id?}")]
public class ManagersController : Controller
{
    // /managers/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        return Content($"Index- template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}
```

The `NamespaceRoutingConvention.Apply` method:

* Does nothing if the controller is attribute routed.
* Sets the controllers template based on the `namespace`, with the base `namespace` removed.

The `NamespaceRoutingConvention` can be applied in `Program.cs`:

```
using My.Application.Controllers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
    options.Conventions.Add(
     new NamespaceRoutingConvention(typeof(HomeController).Namespace!));
});

var app = builder.Build();
```

For example, consider the following controller:

```
using Microsoft.AspNetCore.Mvc;

namespace My.Application.Admin.Controllers
{
    public class UsersController : Controller
    {
        // GET /admin/controllers/users/index
        public IActionResult Index()
        {
            var fullname = typeof(UsersController).FullName;
            var template = 
                ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
            var path = Request.Path.Value;

            return Content($"Path: {path} fullname: {fullname}  template:{template}");
        }

        public IActionResult List(int? id)
        {
            var path = Request.Path.Value;
            return Content($"Path: {path} ID:{id}");
        }
    }
}
```

In the preceding code:

* The base `namespace` is `My.Application`.
* The full name of the preceding controller is `My.Application.Admin.Controllers.UsersController`.
* The `NamespaceRoutingConvention` sets the controllers template to `Admin/Controllers/Users/[action]/{id?`.

The `NamespaceRoutingConvention` can also be applied as an attribute on a controller:

```
[NamespaceRoutingConvention("My.Application")]
public class TestController : Controller
{
    // /admin/controllers/test/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        var actionname = ControllerContext.ActionDescriptor.ActionName;
        return Content($"Action- {actionname} template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}
```

[]()

## Mixed routing: Attribute routing vs conventional routing

ASP.NET Core apps can mix the use of conventional routing and attribute routing. Typically, you use conventional routes for controllers that serve HTML pages to browsers, and attribute routing for controllers that serve REST APIs.

Actions are either conventionally routed or attribute routed. When you place a route on the controller or the action, it becomes attribute routed. You can't reach actions that define attribute routes through the conventional routes, and vice versa. ***Any*** route attribute on the controller makes ***all*** actions in the controller attribute routed.

Attribute routing and conventional routing use the same routing engine.

## Routing with special characters

Routing with special characters can lead to unexpected results. For example, consider a controller with the following action method:

```
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}
```

When `string id` contains the following encoded values, unexpected results might occur:

| ASCII | Encoded |
| ----- | ------- |
| `/`   | `%2F`   |
| ``    | `+`     |

Route parameters are not always URL decoded. This problem may be addressed in the future. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/11544);

[]()[]()

## URL generation and ambient values

Apps can use routing URL generation features to generate URL links to actions. Generating URLs eliminates [hard-coding](https://wikipedia.org/wiki/Hard_coding) URLs, making code more robust and maintainable. This section focuses on the URL generation features provided by MVC and only cover basics of how URL generation works. See [Routing](../../fundamentals/routing?view=aspnetcore-10.0) for a detailed description of URL generation.

The [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) interface is the underlying element of infrastructure between MVC and routing for URL generation. An instance of `IUrlHelper` is available through the `Url` property in controllers, views, and view components.

In the following example, the `IUrlHelper` interface is used through the `Controller.Url` property to generate a URL to another action.

```
public class UrlGenerationController : Controller
{
    public IActionResult Source()
    {
        // Generates /UrlGeneration/Destination
        var url = Url.Action("Destination");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    public IActionResult Destination()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

If the app uses the default conventional route, the value of the `url` variable is the URL path string `/UrlGeneration/Destination`. Routing creates this URL path by combining:

* The route values from the current request, which are called **ambient values**.
* The values passed to `Url.Action` and substituting those values into the route template:

```
ambient values: { controller = "UrlGeneration", action = "Source" }
values passed to Url.Action: { controller = "UrlGeneration", action = "Destination" }
route template: {controller}/{action}/{id?}

result: /UrlGeneration/Destination
```

Each route parameter in the route template has its value substituted by matching names with the values and ambient values. A route parameter that doesn't have a value can:

* Use a default value if it has one.
* Be skipped if it's optional. For example, the `id` from the route template `{controller}/{action}/{id?}`.

URL generation fails if any required route parameter doesn't have a corresponding value. If URL generation fails for a route, the next route is tried until all routes have been tried or a match is found.

The preceding example of `Url.Action` assumes [conventional routing](#cr6). URL generation works similarly with [attribute routing](#ar6), though the concepts are different. With conventional routing:

* The route values are used to expand a template.
* The route values for `controller` and `action` usually appear in that template. This works because the URLs matched by routing adhere to a convention.

The following example uses attribute routing:

```
public class UrlGenerationAttrController : Controller
{
    [HttpGet("custom")]
    public IActionResult Source()
    {
        var url = Url.Action("Destination");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    [HttpGet("custom/url/to/destination")]
    public IActionResult Destination()
    {
       return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The `Source` action in the preceding code generates `custom/url/to/destination`.

[LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) was added in ASP.NET Core 3.0 as an alternative to `IUrlHelper`. `LinkGenerator` offers similar but more flexible functionality. Each method on `IUrlHelper` has a corresponding family of methods on `LinkGenerator` as well.

### Generating URLs by action name

[Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.action), [LinkGenerator.GetPathByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.getpathbyaction), and all related overloads all are designed to generate the target endpoint by specifying a controller name and action name.

When you use `Url.Action`, the runtime provides the current route values for `controller` and `action`:

* The values of `controller` and `action` are part of both [ambient values](#ambient) and values. The method `Url.Action` always uses the current values of `action` and `controller` and generates a URL path that routes to the current action.

Routing attempts to use the values in ambient values to fill in information that wasn't provided when generating a URL. Consider a route like `{a}/{b}/{c}/{d}` with ambient values `{ a = Alice, b = Bob, c = Carol, d = David }`:

* Routing has enough information to generate a URL without any additional values.
* Routing has enough information because all route parameters have a value.

If the value `{ d = Donovan }` is added:

* The value `{ d = David }` is ignored.
* The generated URL path is `Alice/Bob/Carol/Donovan`.

**Warning**: URL paths are hierarchical. In the preceding example, if the value `{ c = Cheryl }` is added:

* Both of the values `{ c = Carol, d = David }` are ignored.
* There's no longer a value for `d` and URL generation fails.
* You must specify the desired values of `c` and `d` to generate a URL.

You might expect to hit this problem with the default route `{controller}/{action}/{id?}`. This problem is rare in practice because `Url.Action` always explicitly specifies a `controller` and `action` value.

Several overloads of [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.action) take a route values object to provide values for route parameters other than `controller` and `action`. The route values object is frequently used with `id`. For example, `Url.Action("Buy", "Products", new { id = 17 })`. The route values object:

* By convention is usually an object of anonymous type.
* Can be an `IDictionary<>` or a [POCO](https://wikipedia.org/wiki/Plain_old_CLR_object)).

Any additional route values that don't match route parameters go in the query string.

```
public IActionResult Index()
{
    var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
    return Content(url!);
}
```

The preceding code generates `/Products/Buy/17?color=red`.

The following code generates an absolute URL:

```
public IActionResult Index2()
{
    var url = Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme);
    // Returns https://localhost:5001/Products/Buy/17
    return Content(url!);
}
```

To create an absolute URL, use one of the following options:

* An overload that accepts a `protocol`. For example, the preceding code.
* [LinkGenerator.GetUriByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.geturibyaction), which generates absolute URIs by default.

[]()

### Generate URLs by route

The preceding code demonstrated generating a URL by passing in the controller and action name. `IUrlHelper` also provides the [Url.RouteUrl](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.routeurl) family of methods. These methods are similar to [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.action), but they don't copy the current values of `action` and `controller` to the route values. The most common usage of `Url.RouteUrl`:

* Specifies a route name to generate the URL.
* Generally doesn't specify a controller or action name.

```
public class UrlGeneration2Controller : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.RouteUrl("Destination_Route");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    [HttpGet("custom/url/to/destination2", Name = "Destination_Route")]
    public IActionResult Destination()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
```

The following Razor file generates an HTML link to the `Destination_Route`:

```
<h1>Test Links</h1>

<ul>
    <li><a href="@Url.RouteUrl("Destination_Route")">Test Destination_Route</a></li>
</ul>
```

[]()

### Generate URLs in HTML and Razor

[IHtmlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper) provides the [HtmlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.htmlhelper) methods [Html.BeginForm](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper.beginform) and [Html.ActionLink](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper.actionlink) to generate `<form>` and `<a>` elements respectively. These methods use the [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.action) method to generate a URL and they accept similar arguments. The `Url.RouteUrl` companions for `HtmlHelper` are `Html.BeginRouteForm` and `Html.RouteLink` which have similar functionality.

TagHelpers generate URLs through the `form` TagHelper and the `<a>` TagHelper. Both of these use `IUrlHelper` for their implementation. For more information, see [Tag Helpers in forms](../views/working-with-forms?view=aspnetcore-10.0).

Inside views, the `IUrlHelper` is available through the `Url` property for any ad-hoc URL generation not covered by the preceding methods.

[]()

### URL generation in Action Results

The preceding examples show how to use `IUrlHelper` in a controller. The most common usage in a controller is to generate a URL as part of an action result.

The [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase) and [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) base classes provide convenience methods for action results that reference another action. One typical usage is to redirect after accepting user input:

```
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int id, Customer customer)
{
    if (ModelState.IsValid)
    {
        // Update DB with new details.
        ViewData["Message"] = $"Successful edit of customer {id}";
        return RedirectToAction("Index");
    }
    return View(customer);
}
```

The action results factory methods such as [RedirectToAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.redirecttoaction) and [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) follow a similar pattern to the methods on `IUrlHelper`.

[]()

### Special case for dedicated conventional routes

[Conventional routing](#cr6) can use a special kind of route definition called a [dedicated conventional route](#dcr). In the following example, the route named `blog` is a dedicated conventional route:

```
app.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");
```

Using the preceding route definitions, `Url.Action("Index", "Home")` generates the URL path `/` by using the `default` route, but why? You might guess the route values `{ controller = Home, action = Index }` are enough to generate a URL by using `blog`, and the result would be `/blog?action=Index&controller=Home`.

[Dedicated conventional routes](#dcr) rely on a special behavior of default values that don't have a corresponding route parameter that prevents the route from being too [greedy](../../fundamentals/routing?view=aspnetcore-10.0#greedy) with URL generation. In this case the default values are `{ controller = Blog, action = Article }`, and neither `controller` nor `action` appears as a route parameter. When routing performs URL generation, the values provided must match the default values. URL generation using `blog` fails because the values `{ controller = Home, action = Index }` don't match `{ controller = Blog, action = Article }`. Routing then falls back to try `default`, which succeeds.

[]()

## Areas

[Areas](areas?view=aspnetcore-10.0) are an MVC feature used to organize related functionality into a group as a separate:

* Routing namespace for controller actions.
* Folder structure for views.

Using areas allows an app to have multiple controllers with the same name, as long as they have different areas. Using areas creates a hierarchy for the purpose of routing by adding another route parameter, `area` to `controller` and `action`. This section discusses how routing interacts with areas. See [Areas](areas?view=aspnetcore-10.0) for details about how areas are used with views.

The following example configures MVC to use the default conventional route and an `area` route for an `area` named `Blog`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{    
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapAreaControllerRoute("blog_route", "Blog",
        "Manage/{controller}/{action}/{id?}");
app.MapControllerRoute("default_route", "{controller}/{action}/{id?}");

app.Run();
```

In the preceding code, [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute) is called to create the `"blog_route"`. The second parameter, `"Blog"`, is the area name.

When matching a URL path like `/Manage/Users/AddUser`, the `"blog_route"` route generates the route values `{ area = Blog, controller = Users, action = AddUser }`. The `area` route value comes from a default value for `area`. The route created by `MapAreaControllerRoute` is equivalent to the following code:

```
app.MapControllerRoute("blog_route", "Manage/{controller}/{action}/{id?}",
        defaults: new { area = "Blog" }, constraints: new { area = "Blog" });
app.MapControllerRoute("default_route", "{controller}/{action}/{id?}");
```

`MapAreaControllerRoute` creates a route using both a default value and constraint for `area` by using the provided area name, in this case `Blog`. The default value ensures that the route always produces `{ area = Blog, ... }`, and the constraint requires the value `{ area = Blog, ... }` for URL generation.

Conventional routing is order-dependent. In general, place routes with areas earlier as they're more specific than routes without an area.

Using the preceding example, the route values `{ area = Blog, controller = Users, action = AddUser }` match the following action:

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        // GET /manage/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
```

The [\[Area\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.areaattribute) attribute is what denotes a controller as part of an area. This controller is in the `Blog` area. Controllers without an `[Area]` attribute aren't members of any area, and don't match when the `area` route value is provided by routing. In the following example, only the first controller listed can match the route values `{ area = Blog, controller = Users, action = AddUser }`.

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        // GET /manage/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
```

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace2
{
    // Matches { area = Zebra, controller = Users, action = AddUser }
    [Area("Zebra")]
    public class UsersController : Controller
    {
        // GET /zebra/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
```

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace3
{
    // Matches { area = string.Empty, controller = Users, action = AddUser }
    // Matches { area = null, controller = Users, action = AddUser }
    // Matches { controller = Users, action = AddUser }
    public class UsersController : Controller
    {
        // GET /users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }
    }
}
```

For completeness, the namespace of each controller is shown here. If the preceding controllers used the same namespace, a compiler error would be generated. Class namespaces have no effect on MVC's routing.

The first two controllers are members of areas, and only match when their respective area name is provided by the `area` route value. The third controller isn't a member of any area, and can only match when no value for `area` is provided by routing.

[]()

In terms of matching *no value*, the absence of the `area` value is the same as if the value for `area` were null or the empty string.

When executing an action inside an area, the route value for `area` is available as an [ambient value](#ambient) for routing to use for URL generation. This means that by default areas act *sticky* for URL generation as demonstrated by the following sample.

```
app.MapAreaControllerRoute(name: "duck_route",
                                     areaName: "Duck",
                                     pattern: "Manage/{controller}/{action}/{id?}");
app.MapControllerRoute(name: "default",
                             pattern: "Manage/{controller=Home}/{action=Index}/{id?}");
```

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace4
{
    [Area("Duck")]
    public class UsersController : Controller
    {
        // GET /Manage/users/GenerateURLInArea
        public IActionResult GenerateURLInArea()
        {
            // Uses the 'ambient' value of area.
            var url = Url.Action("Index", "Home");
            // Returns /Manage/Home/Index
            return Content(url);
        }

        // GET /Manage/users/GenerateURLOutsideOfArea
        public IActionResult GenerateURLOutsideOfArea()
        {
            // Uses the empty value for area.
            var url = Url.Action("Index", "Home", new { area = "" });
            // Returns /Manage
            return Content(url);
        }
    }
}
```

The following code generates a URL to `/Zebra/Users/AddUser`:

```
public class HomeController : Controller
{
    public IActionResult About()
    {
        var url = Url.Action("AddUser", "Users", new { Area = "Zebra" });
        return Content($"URL: {url}");
    }
```

[]()

## Action definition

Public methods on a controller, except those with the [NonAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.nonactionattribute) attribute, are actions.

## Sample code

* [MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/routing/samples/3.x) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Debug diagnostics

For detailed routing diagnostic output, set `Logging:LogLevel:Microsoft` to `Debug`. In the `Development` environment, set the log level in `appsettings.Development.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
```

ASP.NET Core controllers use the Routing [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) to match the URLs of incoming requests and map them to [actions](#action). Route templates:

* Are defined in startup code or attributes.
* Describe how URL paths are matched to [actions](#action).
* Are used to generate URLs for links. The generated links are typically returned in responses.

Actions are either [conventionally-routed](#cr) or [attribute-routed](#ar). Placing a route on the controller or [action](#action) makes it attribute-routed. See [Mixed routing](#routing-mixed-ref-label) for more information.

This document:

* Explains the interactions between MVC and routing:

  * How typical MVC apps make use of routing features.

  * Covers both:

    * [Conventional routing](#cr) typically used with controllers and views.
    * *Attribute routing* used with REST APIs. If you're primarily interested in routing for REST APIs, jump to the [Attribute routing for REST APIs](#ar) section.

  * See [Routing](../../fundamentals/routing?view=aspnetcore-10.0) for advanced routing details.

* Refers to the default routing system added in ASP.NET Core 3.0, called endpoint routing. It's possible to use controllers with the previous version of routing for compatibility purposes. See the [2.2-3.0 migration guide](../../migration/22-to-30?view=aspnetcore-10.0) for instructions. Refer to the [2.2 version of this document](routing?view=aspnetcore-2.2) for reference material on the legacy routing system.

[]()

## Set up conventional route

`Startup.Configure` typically has code similar to the following example when using [conventional routing](#crd):

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

Inside the call to [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints), [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) is used to create a single route. The single route is named `default` route. Most apps with controllers and views use a route template similar to the `default` route. REST APIs should use [attribute routing](#ar).

The route template `"{controller=Home}/{action=Index}/{id?}"`:

* Matches a URL path like `/Products/Details/5`

* Extracts the route values `{ controller = Products, action = Details, id = 5 }` by tokenizing the path. The extraction of route values results in a match if the app has a controller named `ProductsController` and a `Details` action:

  ```
  public class ProductsController : Controller
  {
      public IActionResult Details(int id)
      {
          return ControllerContext.MyDisplayRouteInfo(id);
      }
  }
  ```

  [MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.

* `/Products/Details/5` model binds the value of `id = 5` to set the `id` parameter to `5`. For more information, see [Model Binding](../models/model-binding?view=aspnetcore-10.0).

* `{controller=Home}` defines `Home` as the default `controller`.

* `{action=Index}` defines `Index` as the default `action`.

* The `?` character in `{id?}` defines `id` as optional.

* Default and optional route parameters don't need to be present in the URL path for a match. For a detailed description of route template syntax, see [Route Template Reference](../../fundamentals/routing?view=aspnetcore-10.0#route-template-reference).

* Matches the URL path `/`.

* Produces the route values `{ controller = Home, action = Index }`.

The values for `controller` and `action` make use of the default values. `id` doesn't produce a value since there's no corresponding segment in the URL path. `/` only matches if there exists a `HomeController` and `Index` action:

```
public class HomeController : Controller
{
  public IActionResult Index() { ... }
}
```

Using the preceding controller definition and route template, the `HomeController.Index` action runs for the following URL paths:

* `/Home/Index/17`
* `/Home/Index`
* `/Home`
* `/`

The URL path `/` uses the route template default `Home` controller and `Index` action. The URL path `/Home` uses the route template default `Index` action.

The convenience method [MapDefaultControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapdefaultcontrollerroute):

```
endpoints.MapDefaultControllerRoute();
```

Replaces:

```
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
```

Important

Routing is configured by using the [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting), `MapControllerRoute`, and `MapAreaControllerRoute` middleware. To use controllers:

* Call [MapControllers](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollers) inside `UseEndpoints` to map [attribute routed](#ar) controllers.
* Call [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) or [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute) to map both [conventionally routed](#cr) controllers and [attribute routed](#ar) controllers.

[]()[]()

## Conventional routing

Use conventional routing with controllers and views. The `default` route is:

```
endpoints.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

The preceding code is an example of a *conventional route*. It's called *conventional routing* because it establishes a *convention* for URL paths:

* The first path segment, `{controller=Home}`, maps to the controller name.
* The second segment, `{action=Index}`, maps to the [action](#action) name.
* The third segment, `{id?}` is used for an optional `id`. The `?` in `{id?}` makes it optional. `id` maps to a model entity.

Using this `default` route, the URL path:

* `/Products/List` maps to the `ProductsController.List` action.
* `/Blog/Article/17` maps to `BlogController.Article` and typically model binds the `id` parameter to 17.

This mapping:

* Is based on the controller and [action](#action) names **only**.
* Isn't based on namespaces, source file locations, or method parameters.

By using conventional routing with the default route, you don't need to create a new URL pattern for each action. For an app with [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) style actions, having consistency for the URLs across controllers:

* Helps simplify the code.
* Makes the UI more predictable.

Warning

The route template defines the `id` as optional. Actions can execute without the optional ID provided as part of the URL. Generally, when `id` is omitted from the URL:

* Model binding sets `id` to `0`.
* No entity is found in the database matching `id == 0`.

[Attribute routing](#ar) provides fine-grained control to make the ID required for some actions and not for others. By convention, the documentation includes optional parameters like `id` when they're likely to appear in correct usage.

Most apps should choose a basic and descriptive routing scheme so that URLs are readable and meaningful. The default conventional route `{controller=Home}/{action=Index}/{id?}`:

* Supports a basic and descriptive routing scheme.
* Is a useful starting point for UI-based apps.
* Is the only route template needed for many web UI apps. For larger web UI apps, another route using [Areas](#areas) is frequently all that's needed.

[MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) and [MapAreaRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcarearoutebuilderextensions.maparearoute) :

* Automatically assign an **order** value to their endpoints based on the order they are invoked.

Endpoint routing in ASP.NET Core 3.0 or later:

* Doesn't have a concept of routes.
* Doesn't provide ordering guarantees for the execution of extensibility. All endpoints are processed at once.

Enable [Logging](../../fundamentals/logging/?view=aspnetcore-10.0) to see how the built-in routing implementations, such as [Route](/en-us/dotnet/api/microsoft.aspnetcore.routing.route), match requests.

[Attribute routing](#ar) is explained later in this document.

[]()

### Multiple conventional routes

Multiple [conventional routes](#cr) can be added inside `UseEndpoints` by adding more calls to [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) and [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute). Doing so allows defining multiple conventions, or to adding conventional routes that are dedicated to a specific [action](#action), such as:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
    endpoints.MapControllerRoute(name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

[]()

The `blog` route in the preceding code is a **dedicated conventional route**. It's a dedicated conventional route because:

* It uses [conventional routing](#cr).
* It's dedicated to a specific [action](#action).

Because the route template `"blog/{*article}"` doesn't include `controller` and `action` as parameters:

* They can only have the default values `{ controller = "Blog", action = "Article" }`.
* This route always maps to the action `BlogController.Article`.

`/Blog`, `/Blog/Article`, and `/Blog/{any-string}` are the only URL paths that match the blog route.

In the preceding example:

* The `blog` route has a higher priority for matches than the `default` route because you add it first.
* It's an example of [Slug](https://developer.mozilla.org/docs/Glossary/Slug) style routing where it's typical to have an article name as part of the URL.

Warning

In ASP.NET Core 3.0 or later, routing doesn't:

* Define a concept called a *route*. `UseRouting` adds route matching to the middleware pipeline. The `UseRouting` middleware looks at the set of endpoints defined in the app, and selects the best endpoint match based on the request.
* Provide guarantees about the execution order of extensibility like [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) or [IActionConstraint](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionconstraints.iactionconstraint).

See [Routing](../../fundamentals/routing?view=aspnetcore-10.0) for reference material on routing.

[]()

### Conventional routing order

Conventional routing only matches a combination of action and controller that the app defines. This approach simplifies cases where conventional routes overlap. When you add routes by using [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute), [MapDefaultControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapdefaultcontrollerroute), and [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute), the endpoints automatically get an order value based on the order you invoke these methods. Matches from a route that appears earlier in the list have a higher priority. Conventional routing depends on order. In general, place routes with areas earlier because they're more specific than routes without an area. [Dedicated conventional routes](#dcr) with catch-all route parameters, such as `{*article}`, can make a route too [greedy](../../fundamentals/routing?view=aspnetcore-10.0#greedy). A greedy route matches URLs that you intended to be matched by other routes. Put the greedy routes later in the route table to prevent greedy matches.

```
public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
```

[]()

### Resolving ambiguous actions

When two endpoints match through routing, routing must do one of the following steps:

* Choose the best candidate.
* Throw an exception.

For example:

```
    public class Products33Controller : Controller
    {
        public IActionResult Edit(int id)
        {
            return ControllerContext.MyDisplayRouteInfo(id);
        }

        [HttpPost]
        public IActionResult Edit(int id, Product product)
        {
            return ControllerContext.MyDisplayRouteInfo(id, product.name);
        }
    }
}
```

The preceding controller defines two actions that match:

* The URL path `/Products33/Edit/17`
* Route data `{ controller = Products33, action = Edit, id = 17 }`.

This is a typical pattern for MVC controllers:

* `Edit(int)` displays a form to edit a product.
* `Edit(int, Product)` processes the posted form.

To resolve the correct route:

* `Edit(int, Product)` is selected when the request is an HTTP `POST`.
* `Edit(int)` is selected when the [HTTP verb](#verb) is anything else. `Edit(int)` is generally called via `GET`.

The [HttpPostAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute), `[HttpPost]`, is provided to routing so that it can choose based on the HTTP method of the request. The `HttpPostAttribute` makes `Edit(int, Product)` a better match than `Edit(int)`.

It's important to understand the role of attributes like `HttpPostAttribute`. Similar attributes are defined for other [HTTP verbs](#verb). In [conventional routing](#cr), it's common for actions to use the same action name when they're part of a show form, submit form workflow. For example, see [Examine the two Edit action methods](../../tutorials/first-mvc-app/controller-methods-views?view=aspnetcore-10.0#get-post).

If routing can't choose the best candidate, it throws an [AmbiguousMatchException](/en-us/dotnet/api/system.reflection.ambiguousmatchexception) and lists the multiple matched endpoints.

[]()

### Conventional route names

The strings `"blog"` and `"default"` in the following examples are conventional route names:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
    endpoints.MapControllerRoute(name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

The route names give the route a logical name. The named route can be used for URL generation. Using a named route simplifies URL creation when the ordering of routes could make URL generation complicated. Route names must be unique application wide.

Route names:

* Have no impact on URL matching or handling of requests.
* Are used only for URL generation.

The route name concept is represented in routing as [IEndpointNameMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointnamemetadata). The terms **route name** and **endpoint name**:

* Are interchangeable.
* Which one is used in documentation and code depends on the API being described.

[]()[]()

## Attribute routing for REST APIs

REST APIs should use attribute routing to model the app's functionality as a set of resources where operations are represented by [HTTP verbs](#verb).

Attribute routing uses a set of attributes to map actions directly to route templates. The following `StartUp.Configure` code is typical for a REST API and is used in the next sample:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

In the preceding code, [MapControllers](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollers) is called inside `UseEndpoints` to map attribute routed controllers.

In the following example:

* `HomeController` matches a set of URLs similar to what the default conventional route `{controller=Home}/{action=Index}/{id?}` matches.

```
public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

The `HomeController.Index` action runs for any of the URL paths `/`, `/Home`, `/Home/Index`, or `/Home/Index/3`.

This example highlights a key programming difference between attribute routing and [conventional routing](#cr). Attribute routing requires more input to specify a route. The conventional default route handles routes more succinctly. However, attribute routing allows and requires precise control of which route templates apply to each [action](#action).

With attribute routing, the controller and action names play no part in which action is matched, unless [token replacement](#routing-token-replacement-templates-ref-label) is used. The following example matches the same URLs as the previous example:

```
public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

The following code uses token replacement for `action` and `controller`:

```
public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("[controller]/[action]")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("[controller]/[action]")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The following code applies `[Route("[controller]/[action]")]` to the controller:

```
[Route("[controller]/[action]")]
public class HomeController : Controller
{
    [Route("~/")]
    [Route("/Home")]
    [Route("~/Home/Index")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

In the preceding code, the `Index` method templates must prepend `/` or `~/` to the route templates. Route templates applied to an action that begin with `/` or `~/` don't get combined with route templates applied to the controller.

See [Route template precedence](../../fundamentals/routing?view=aspnetcore-10.0#rtp) for information on route template selection.

## Reserved routing names

The following keywords are reserved route parameter names when you use Controllers or Razor Pages:

* `action`
* `area`
* `controller`
* `handler`
* `page`

Using `page` as a route parameter with attribute routing is a common error. This choice results in inconsistent and confusing behavior with URL generation.

```
public class MyDemo2Controller : Controller
{
    [Route("/articles/{page}")]
    public IActionResult ListArticles(int page)
    {
        return ControllerContext.MyDisplayRouteInfo(page);
    }
}
```

URL generation uses these special parameter names to determine if a URL generation operation refers to a Razor Page or to a Controller.

The following keywords are reserved in the context of a Razor view or a Razor Page:

* `page`
* `using`
* `namespace`
* `inject`
* `section`
* `inherits`
* `model`
* `addTagHelper`
* `removeTagHelper`

Don't use these keywords for link generations, model bound parameters, or top level properties.

[]()

## HTTP verb templates

ASP.NET Core includes the following HTTP verb templates:

* [\[HttpGet\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute)
* [\[HttpPost\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute)
* [\[HttpPut\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpputattribute)
* [\[HttpDelete\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpdeleteattribute)
* [\[HttpHead\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpheadattribute)
* [\[HttpPatch\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute)

[]()

### Route templates

ASP.NET Core includes the following route templates:

* All the [HTTP verb templates](#verb) are route templates.
* [\[Route\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute)

[]()

### Attribute routing with HTTP verb attributes

Consider the following controller:

```
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
    [HttpGet]   // GET /api/test2
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
       return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

In the preceding code:

* Each action contains the `[HttpGet]` attribute, which constrains matching to HTTP GET requests only.

* The `GetProduct` action includes the `"{id}"` template, so the `id` appends to the `"api/[controller]"` template on the controller. The method's template is `"api/[controller]/{id}"`. Therefore this action only matches GET requests for the form `/api/test2/xyz`, `/api/test2/123`, `/api/test2/{any string}`, and so on.
  ```
  [HttpGet("{id}")]   // GET /api/test2/xyz
  public IActionResult GetProduct(string id)
  {
     return ControllerContext.MyDisplayRouteInfo(id);
  }
  ```

* The `GetIntProduct` action contains the `"int/{id:int}"` template. The `:int` portion of the template constrains the `id` route values to strings that can be converted to an integer. A GET request to `/api/test2/int/abc`:

  * Doesn't match this action.
  * Returns a [404 Not Found](https://developer.mozilla.org/docs/Web/HTTP/Status/404) error.
    ```
    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
    ```

* The `GetInt2Product` action contains `{id}` in the template, but doesn't constrain `id` to values that can be converted to an integer. A GET request to `/api/test2/int2/abc`:

  * Matches this route.
  * Model binding fails to convert `abc` to an integer. The `id` parameter of the method is integer.
  * Returns a [400 Bad Request](https://developer.mozilla.org/docs/Web/HTTP/Status/400) because model binding failed to convert `abc` to an integer.
    ```
    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
    ```

Attribute routing can use [HttpMethodAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.httpmethodattribute) attributes such as [HttpPostAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute), [HttpPutAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpputattribute), and [HttpDeleteAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpdeleteattribute). All of the [HTTP verb](#verb) attributes accept a route template. The following example shows two actions that match the same route template:

```
[ApiController]
public class MyProductsController : ControllerBase
{
    [HttpGet("/products3")]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpPost("/products3")]
    public IActionResult CreateProduct(MyProduct myProduct)
    {
        return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
    }
}
```

Using the URL path `/products3`:

* The `MyProductsController.ListProducts` action runs when the [HTTP verb](#verb) is `GET`.
* The `MyProductsController.CreateProduct` action runs when the [HTTP verb](#verb) is `POST`.

When building a REST API, it's rare that you'll need to use `[Route(...)]` on an action method because the action accepts all HTTP methods. It's better to use the more specific [HTTP verb attribute](#verb) to be precise about what your API supports. Clients of REST APIs are expected to know what paths and HTTP verbs map to specific logical operations.

REST APIs should use attribute routing to model the app's functionality as a set of resources where operations are represented by HTTP verbs. This design means that many operations, such as GET and POST on the same logical resource, use the same URL. Attribute routing provides the level of control needed to carefully design an API's public endpoint layout.

Since an attribute route applies to a specific action, it's easy to make parameters required as part of the route template definition. In the following example, `id` is required as part of the URL path:

```
[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

The `Products2ApiController.GetProduct(int)` action:

* Is run with URL path like `/products2/3`
* Isn't run with the URL path `/products2`.

The [\[Consumes\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute allows an action to limit the supported request content types. For more information, see [Define supported request content types with the Consumes attribute](../../web-api/?view=aspnetcore-10.0#consumes).

See [Routing](../../fundamentals/routing?view=aspnetcore-10.0) for a full description of route templates and related options.

For more information on `[ApiController]`, see [ApiController attribute](../../web-api/?view=aspnetcore-10.0#apicontroller-attribute).

## Route name

The following code defines a route name of `Products_List`:

```
[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

Use route names to generate a URL based on a specific route. Route names:

* Have no impact on the URL matching behavior of routing.
* Are only used for URL generation.

Route names must be unique application-wide.

Contrast the preceding code with the conventional default route, which defines the `id` parameter as optional (`{id?}`). The ability to precisely specify APIs has advantages, such as allowing `/products` and `/products/5` to be dispatched to different actions.

[]()

## Combining attribute routes

To make attribute routing less repetitive, combine route attributes on the controller with route attributes on the individual actions. The route templates you define on the controller are prepended to route templates on the actions. When you place a route attribute on the controller, **all** actions in the controller use attribute routing.

```
[ApiController]
[Route("products")]
public class ProductsApiController : ControllerBase
{
    [HttpGet]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

In the preceding example:

* The URL path `/products` can match `ProductsApi.ListProducts`
* The URL path `/products/5` can match `ProductsApi.GetProduct(int)`.

Both of these actions only match HTTP `GET` because they're marked with the `[HttpGet]` attribute.

Route templates that you apply to an action and that begin with `/` or `~/` don't get combined with route templates that you apply to the controller. The following example matches a set of URL paths similar to the default route.

```
[Route("Home")]
public class HomeController : Controller
{
    [Route("")]
    [Route("Index")]
    [Route("/")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("About")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The following table explains the `[Route]` attributes in the preceding code.

| Attribute          | Combines with `[Route("Home")]` | Defines route template |
| ------------------ | ------------------------------- | ---------------------- |
| `[Route("")]`      | Yes                             | `"Home"`               |
| `[Route("Index")]` | Yes                             | `"Home/Index"`         |
| `[Route("/")]`     | **No**                          | `""`                   |
| `[Route("About")]` | Yes                             | `"Home/About"`         |

[]()[]()

### Attribute route order

Routing builds a tree and matches all endpoints simultaneously:

* The route entries behave as if they're placed in an ideal ordering.
* The most specific routes have a chance to execute before the more general routes.

For example, an attribute route like `blog/search/{topic}` is more specific than an attribute route like `blog/{*article}`. The `blog/search/{topic}` route has higher priority, by default, because it's more specific. Using [conventional routing](#cr), the developer is responsible for placing routes in the desired order.

Attribute routes can configure an order using the [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute.order#microsoft-aspnetcore-mvc-routeattribute-order) property. All of the framework provided [route attributes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute) include `Order` . Routes are processed according to an ascending sort of the `Order` property. The default order is `0`. Setting a route using `Order = -1` runs before routes that don't set an order. Setting a route using `Order = 1` runs after default route ordering.

**Avoid** depending on `Order`. If an app's URL-space requires explicit order values to route correctly, then it's likely confusing to clients as well. In general, attribute routing selects the correct route with URL matching. If the default order used for URL generation isn't working, using a route name as an override is usually simpler than applying the `Order` property.

Consider the following two controllers which both define the route matching `/home`:

```
public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

```
public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

Requesting `/home` by using the preceding code throws an exception similar to the following:

```
AmbiguousMatchException: The request matched multiple endpoints. Matches:

 WebMvcRouting.Controllers.HomeController.Index
 WebMvcRouting.Controllers.MyDemoController.MyIndex
```

Adding `Order` to one of the route attributes resolves the ambiguity:

```
[Route("")]
[Route("Home", Order = 2)]
[Route("Home/MyIndex")]
public IActionResult MyIndex()
{
    return ControllerContext.MyDisplayRouteInfo();
}
```

With the preceding code, `/home` runs the `HomeController.Index` endpoint. To get to the `MyDemoController.MyIndex`, request `/home/MyIndex`. **Note**:

* The preceding code is an example of poor routing design. It illustrates the `Order` property.
* The `Order` property only resolves the ambiguity. That template cannot be matched. It's better to remove the `[Route("Home")]` template.

For information on route order with Razor Pages, see [Razor Pages route and app conventions: Route order](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0#route-order).

In some cases, an HTTP 500 error is returned with ambiguous routes. Use [logging](../../fundamentals/logging/?view=aspnetcore-10.0) to see which endpoints caused the `AmbiguousMatchException`.

[]()

## Token replacement in route templates \[controller], \[action], \[area]

For convenience, attribute routes support *token replacement* by enclosing a token in square brackets (`[`, `]`). The tokens `[action]`, `[area]`, and `[controller]` are replaced with the values of the action name, area name, and controller name from the action where you define the route:

```
[Route("[controller]/[action]")]
public class Products0Controller : Controller
{
    [HttpGet]
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }


    [HttpGet("{id}")]
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

In the preceding code:

```
[HttpGet]
public IActionResult List()
{
    return ControllerContext.MyDisplayRouteInfo();
}
```

* Matches `/Products0/List`

```
[HttpGet("{id}")]
public IActionResult Edit(int id)
{
    return ControllerContext.MyDisplayRouteInfo(id);
}
```

* Matches `/Products0/Edit/{id}`

Token replacement happens as the last step of building the attribute routes. The preceding example behaves the same as the following code:

```
public class Products20Controller : Controller
{
    [HttpGet("[controller]/[action]")]  // Matches '/Products20/List'
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("[controller]/[action]/{id}")]   // Matches '/Products20/Edit/{id}'
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

If you are reading this in a language other than English, let us know in this [GitHub discussion issue](https://github.com/dotnet/AspNetCore.Docs/issues/16455) if you'd like to see the code comments in your native language.

You can also combine attribute routes with inheritance. This combination is powerful when you use token replacement. Token replacement also applies to route names defined by attribute routes. `[Route("[controller]/[action]", Name="[controller]_[action]")]`generates a unique route name for each action:

```
[ApiController]
[Route("api/[controller]/[action]", Name = "[controller]_[action]")]
public abstract class MyBase2Controller : ControllerBase
{
}

public class Products11Controller : MyBase2Controller
{
    [HttpGet]                      // /api/products11/list
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]             //    /api/products11/edit/3
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

To match the literal token replacement delimiter `[` or `]`, escape it by repeating the character (`[[` or `]]`).

[]()

### Use a parameter transformer to customize token replacement

You can customize token replacement by using a parameter transformer. A parameter transformer implements [IOutboundParameterTransformer](/en-us/dotnet/api/microsoft.aspnetcore.routing.ioutboundparametertransformer) and transforms the value of parameters. For example, a custom `SlugifyParameterTransformer` parameter transformer changes the `SubscriptionManagement` route value to `subscription-management`:

```
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) { return null; }

        return Regex.Replace(value.ToString(),
                             "([a-z])([A-Z])",
                             "$1-$2",
                             RegexOptions.CultureInvariant,
                             TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}
```

The [RouteTokenTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.routetokentransformerconvention) is an application model convention that:

* Apply a parameter transformer to all attribute routes in an application.
* Customizes the attribute route token values as they're replaced.

```
public class SubscriptionManagementController : Controller
{
    [HttpGet("[controller]/[action]")]
    public IActionResult ListAll()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The preceding `ListAll` method matches `/subscription-management/list-all`.

The `RouteTokenTransformerConvention` is registered as an option in `ConfigureServices`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        options.Conventions.Add(new RouteTokenTransformerConvention(
                                     new SlugifyParameterTransformer()));
    });
}
```

For the definition of slug, see [MDN web docs on Slug](https://developer.mozilla.org/docs/Glossary/Slug).

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

[]()

### Multiple attribute routes

Attribute routing supports defining multiple routes that reach the same action. The most common usage of this is to mimic the behavior of the default conventional route as shown in the following example:

```
[Route("[controller]")]
public class Products13Controller : Controller
{
    [Route("")]     // Matches 'Products13'
    [Route("Index")] // Matches 'Products13/Index'
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
```

Putting multiple route attributes on the controller means that each one combines with each of the route attributes on the action methods:

```
[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
    [HttpPost("Buy")]       // Matches 'Products6/Buy' and 'Store/Buy'
    [HttpPost("Checkout")]  // Matches 'Products6/Checkout' and 'Store/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

All the [HTTP verb](#verb) route constraints implement `IActionConstraint`.

When multiple route attributes that implement [IActionConstraint](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionconstraints.iactionconstraint) are placed on an action:

* Each action constraint combines with the route template applied to the controller.

```
[Route("api/[controller]")]
public class Products7Controller : ControllerBase
{
    [HttpPut("Buy")]        // Matches PUT 'api/Products7/Buy'
    [HttpPost("Checkout")]  // Matches POST 'api/Products7/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

Using multiple routes on actions might seem useful and powerful, it's better to keep your app's URL space basic and well defined. Use multiple routes on actions **only** where needed, for example, to support existing clients.

[]()

### Specifying attribute route optional parameters, default values, and constraints

Attribute routes support the same inline syntax as conventional routes to specify optional parameters, default values, and constraints.

```
public class Products14Controller : Controller
{
    [HttpPost("product14/{id:int}")]
    public IActionResult ShowProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

In the preceding code, `[HttpPost("product14/{id:int}")]` applies a route constraint. The `Products14Controller.ShowProduct` action is matched only by URL paths like `/product14/3`. The route template portion `{id:int}` constrains that segment to only integers.

For a detailed description of route template syntax, see [Route Template Reference](../../fundamentals/routing?view=aspnetcore-10.0#route-template-reference).

[]()

### Custom route attributes using IRouteTemplateProvider

All of the [route attributes](#rt) implement [IRouteTemplateProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.iroutetemplateprovider). The ASP.NET Core runtime:

* Looks for attributes on controller classes and action methods when the app starts.
* Uses the attributes that implement `IRouteTemplateProvider` to build the initial set of routes.

Implement `IRouteTemplateProvider` to define custom route attributes. Each `IRouteTemplateProvider` allows you to define a single route with a custom route template, order, and name:

```
public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
{
    public string Template => "api/[controller]";
    public int? Order => 2;
    public string Name { get; set; }
}

[MyApiController]
[ApiController]
public class MyTestApiController : ControllerBase
{
    // GET /api/MyTestApi
    [HttpGet]
    public IActionResult Get()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The preceding `Get` method returns `Order = 2, Template = api/MyTestApi`.

[]()

### Use application model to customize attribute routes

The application model:

* Is an object model created at startup.
* Contains all of the metadata used by ASP.NET Core to route and execute the actions in an app.

The application model includes all of the data gathered from route attributes. The data from route attributes is provided by the `IRouteTemplateProvider` implementation. Conventions:

* Can be written to modify the application model to customize how routing behaves.
* Are read at app startup.

This section shows a basic example of customizing routing by using the application model. The following code makes routes roughly line up with the folder structure of the project.

```
public class NamespaceRoutingConvention : Attribute, IControllerModelConvention
{
    private readonly string _baseNamespace;

    public NamespaceRoutingConvention(string baseNamespace)
    {
        _baseNamespace = baseNamespace;
    }

    public void Apply(ControllerModel controller)
    {
        var hasRouteAttributes = controller.Selectors.Any(selector =>
                                                selector.AttributeRouteModel != null);
        if (hasRouteAttributes)
        {
            return;
        }

        var namespc = controller.ControllerType.Namespace;
        if (namespc == null)
            return;
        var template = new StringBuilder();
        template.Append(namespc, _baseNamespace.Length + 1,
                        namespc.Length - _baseNamespace.Length - 1);
        template.Replace('.', '/');
        template.Append("/[controller]/[action]/{id?}");

        foreach (var selector in controller.Selectors)
        {
            selector.AttributeRouteModel = new AttributeRouteModel()
            {
                Template = template.ToString()
            };
        }
    }
}
```

The following code prevents the `namespace` convention from being applied to controllers that are attribute routed:

```
public void Apply(ControllerModel controller)
{
    var hasRouteAttributes = controller.Selectors.Any(selector =>
                                            selector.AttributeRouteModel != null);
    if (hasRouteAttributes)
    {
        return;
    }
```

For example, the following controller doesn't use `NamespaceRoutingConvention`:

```
[Route("[controller]/[action]/{id?}")]
public class ManagersController : Controller
{
    // /managers/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        return Content($"Index- template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}
```

The `NamespaceRoutingConvention.Apply` method:

* Does nothing if the controller is attribute routed.
* Sets the controllers template based on the `namespace`, with the base `namespace` removed.

The `NamespaceRoutingConvention` can be applied in `Startup.ConfigureServices`:

```
namespace My.Application
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews(options =>
            {
                options.Conventions.Add(
                    new NamespaceRoutingConvention(typeof(Startup).Namespace));
            });
        }
        // Remaining code ommitted for brevity.
```

For example, consider the following controller:

```
using Microsoft.AspNetCore.Mvc;

namespace My.Application.Admin.Controllers
{
    public class UsersController : Controller
    {
        // GET /admin/controllers/users/index
        public IActionResult Index()
        {
            var fullname = typeof(UsersController).FullName;
            var template = 
                ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
            var path = Request.Path.Value;

            return Content($"Path: {path} fullname: {fullname}  template:{template}");
        }

        public IActionResult List(int? id)
        {
            var path = Request.Path.Value;
            return Content($"Path: {path} ID:{id}");
        }
    }
}
```

In the preceding code:

* The base `namespace` is `My.Application`.
* The full name of the preceding controller is `My.Application.Admin.Controllers.UsersController`.
* The `NamespaceRoutingConvention` sets the controllers template to `Admin/Controllers/Users/[action]/{id?`.

The `NamespaceRoutingConvention` can also be applied as an attribute on a controller:

```
[NamespaceRoutingConvention("My.Application")]
public class TestController : Controller
{
    // /admin/controllers/test/index
    public IActionResult Index()
    {
        var template = ControllerContext.ActionDescriptor.AttributeRouteInfo?.Template;
        var actionname = ControllerContext.ActionDescriptor.ActionName;
        return Content($"Action- {actionname} template:{template}");
    }

    public IActionResult List(int? id)
    {
        var path = Request.Path.Value;
        return Content($"List- Path:{path}");
    }
}
```

[]()

## Mixed routing: Attribute routing vs conventional routing

ASP.NET Core apps can mix the use of conventional routing and attribute routing. Typically, you use conventional routes for controllers that serve HTML pages to browsers, and attribute routing for controllers that serve REST APIs.

Actions are either conventionally routed or attribute routed. Placing a route on the controller or the action makes it attribute routed. Actions that define attribute routes cannot be reached through the conventional routes and vice-versa. **Any** route attribute on the controller makes **all** actions in the controller attribute routed.

Attribute routing and conventional routing use the same routing engine.

[]()[]()

## URL generation and ambient values

Apps can use routing URL generation features to generate URL links to actions. Generating URLs eliminates hardcoding URLs, making code more robust and maintainable. This section focuses on the URL generation features provided by MVC and only cover basics of how URL generation works. See [Routing](../../fundamentals/routing?view=aspnetcore-10.0) for a detailed description of URL generation.

The [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) interface is the underlying element of infrastructure between MVC and routing for URL generation. An instance of `IUrlHelper` is available through the `Url` property in controllers, views, and view components.

In the following example, the `IUrlHelper` interface is used through the `Controller.Url` property to generate a URL to another action.

```
public class UrlGenerationController : Controller
{
    public IActionResult Source()
    {
        // Generates /UrlGeneration/Destination
        var url = Url.Action("Destination");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    public IActionResult Destination()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

If the app uses the default conventional route, the value of the `url` variable is the URL path string `/UrlGeneration/Destination`. Routing creates this URL path by combining:

* The route values from the current request, which are called **ambient values**.
* The values passed to `Url.Action` and substituting those values into the route template:

```
ambient values: { controller = "UrlGeneration", action = "Source" }
values passed to Url.Action: { controller = "UrlGeneration", action = "Destination" }
route template: {controller}/{action}/{id?}

result: /UrlGeneration/Destination
```

Each route parameter in the route template has its value substituted by matching names with the values and ambient values. A route parameter that doesn't have a value can:

* Use a default value if it has one.
* Be skipped if it's optional. For example, the `id` from the route template `{controller}/{action}/{id?}`.

URL generation fails if any required route parameter doesn't have a corresponding value. If URL generation fails for a route, the next route is tried until all routes have been tried or a match is found.

The preceding example of `Url.Action` assumes [conventional routing](#cr). URL generation works similarly with [attribute routing](#ar), though the concepts are different. With conventional routing:

* The route values are used to expand a template.
* The route values for `controller` and `action` usually appear in that template. This works because the URLs matched by routing adhere to a convention.

The following example uses attribute routing:

```
public class UrlGenerationAttrController : Controller
{
    [HttpGet("custom")]
    public IActionResult Source()
    {
        var url = Url.Action("Destination");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    [HttpGet("custom/url/to/destination")]
    public IActionResult Destination()
    {
       return ControllerContext.MyDisplayRouteInfo();
    }
}
```

The `Source` action in the preceding code generates `custom/url/to/destination`.

[LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) was added in ASP.NET Core 3.0 as an alternative to `IUrlHelper`. `LinkGenerator` offers similar but more flexible functionality. Each method on `IUrlHelper` has a corresponding family of methods on `LinkGenerator` as well.

### Generating URLs by action name

[Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.action), [LinkGenerator.GetPathByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.getpathbyaction), and all related overloads all are designed to generate the target endpoint by specifying a controller name and action name.

When you use `Url.Action`, the runtime provides the current route values for `controller` and `action`:

* The values of `controller` and `action` are part of both [ambient values](#ambient) and values. The method `Url.Action` always uses the current values of `action` and `controller` and generates a URL path that routes to the current action.

Routing attempts to use the values in ambient values to fill in information that wasn't provided when generating a URL. Consider a route like `{a}/{b}/{c}/{d}` with ambient values `{ a = Alice, b = Bob, c = Carol, d = David }`:

* Routing has enough information to generate a URL without any additional values.
* Routing has enough information because all route parameters have a value.

If the value `{ d = Donovan }` is added:

* The value `{ d = David }` is ignored.
* The generated URL path is `Alice/Bob/Carol/Donovan`.

**Warning**: URL paths are hierarchical. In the preceding example, if the value `{ c = Cheryl }` is added:

* Both of the values `{ c = Carol, d = David }` are ignored.
* There's no longer a value for `d` and URL generation fails.
* You must specify the desired values of `c` and `d` to generate a URL.

You might expect to hit this problem with the default route `{controller}/{action}/{id?}`. This problem is rare in practice because `Url.Action` always explicitly specifies a `controller` and `action` value.

Several overloads of [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.action) take a route values object to provide values for route parameters other than `controller` and `action`. The route values object is frequently used with `id`. For example, `Url.Action("Buy", "Products", new { id = 17 })`. The route values object:

* By convention is usually an object of anonymous type.
* Can be an `IDictionary<>` or a [POCO](https://wikipedia.org/wiki/Plain_old_CLR_object)).

Any additional route values that don't match route parameters go in the query string.

```
public IActionResult Index()
{
    var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
    return Content(url);
}
```

The preceding code generates `/Products/Buy/17?color=red`.

The following code generates an absolute URL:

```
public IActionResult Index2()
{
    var url = Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme);
    // Returns https://localhost:5001/Products/Buy/17
    return Content(url);
}
```

To create an absolute URL, use one of the following options:

* An overload that accepts a `protocol`. For example, the preceding code.
* [LinkGenerator.GetUriByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.geturibyaction), which generates absolute URIs by default.

[]()

### Generate URLs by route

The preceding code demonstrated generating a URL by passing in the controller and action name. `IUrlHelper` also provides the [Url.RouteUrl](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.routeurl) family of methods. These methods are similar to [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.action), but they don't copy the current values of `action` and `controller` to the route values. The most common usage of `Url.RouteUrl`:

* Specifies a route name to generate the URL.
* Generally doesn't specify a controller or action name.

```
public class UrlGeneration2Controller : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.RouteUrl("Destination_Route");
        return ControllerContext.MyDisplayRouteInfo("", $" URL = {url}");
    }

    [HttpGet("custom/url/to/destination2", Name = "Destination_Route")]
    public IActionResult Destination()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
```

The following Razor file generates an HTML link to the `Destination_Route`:

```
<h1>Test Links</h1>

<ul>
    <li><a href="@Url.RouteUrl("Destination_Route")">Test Destination_Route</a></li>
</ul>
```

[]()

### Generate URLs in HTML and Razor

[IHtmlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper) provides the [HtmlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.htmlhelper) methods [Html.BeginForm](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper.beginform) and [Html.ActionLink](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper.actionlink) to generate `<form>` and `<a>` elements respectively. These methods use the [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.action) method to generate a URL and they accept similar arguments. The `Url.RouteUrl` companions for `HtmlHelper` are `Html.BeginRouteForm` and `Html.RouteLink` which have similar functionality.

TagHelpers generate URLs through the `form` TagHelper and the `<a>` TagHelper. Both of these use `IUrlHelper` for their implementation. For more information, see [Tag Helpers in forms](../views/working-with-forms?view=aspnetcore-10.0).

Inside views, the `IUrlHelper` is available through the `Url` property for any ad-hoc URL generation not covered by the preceding methods.

[]()

### URL generation in Action Results

The preceding examples show how to use `IUrlHelper` in a controller. The most common usage in a controller is to generate a URL as part of an action result.

The [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase) and [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) base classes provide convenience methods for action results that reference another action. One typical usage is to redirect after accepting user input:

```
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(int id, Customer customer)
{
    if (ModelState.IsValid)
    {
        // Update DB with new details.
        ViewData["Message"] = $"Successful edit of customer {id}";
        return RedirectToAction("Index");
    }
    return View(customer);
}
```

The action results factory methods such as [RedirectToAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.redirecttoaction) and [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) follow a similar pattern to the methods on `IUrlHelper`.

[]()

### Special case for dedicated conventional routes

[Conventional routing](#cr) can use a special kind of route definition called a [dedicated conventional route](#dcr). In the following example, the route named `blog` is a dedicated conventional route:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
    endpoints.MapControllerRoute(name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

Using the preceding route definitions, `Url.Action("Index", "Home")` generates the URL path `/` by using the `default` route, but why? You might guess the route values `{ controller = Home, action = Index }` are enough to generate a URL by using `blog`, and the result would be `/blog?action=Index&controller=Home`.

[Dedicated conventional routes](#dcr) rely on a special behavior of default values that don't have a corresponding route parameter that prevents the route from being too [greedy](../../fundamentals/routing?view=aspnetcore-10.0#greedy) with URL generation. In this case the default values are `{ controller = Blog, action = Article }`, and neither `controller` nor `action` appears as a route parameter. When routing performs URL generation, the values provided must match the default values. URL generation using `blog` fails because the values `{ controller = Home, action = Index }` don't match `{ controller = Blog, action = Article }`. Routing then falls back to try `default`, which succeeds.

[]()

## Areas

[Areas](areas?view=aspnetcore-10.0) are an MVC feature used to organize related functionality into a group as a separate:

* Routing namespace for controller actions.
* Folder structure for views.

Using areas allows an app to have multiple controllers with the same name, as long as they have different areas. Using areas creates a hierarchy for the purpose of routing by adding another route parameter, `area` to `controller` and `action`. This section discusses how routing interacts with areas. See [Areas](areas?view=aspnetcore-10.0) for details about how areas are used with views.

The following example configures MVC to use the default conventional route and an `area` route for an `area` named `Blog`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapAreaControllerRoute("blog_route", "Blog",
        "Manage/{controller}/{action}/{id?}");
    endpoints.MapControllerRoute("default_route", "{controller}/{action}/{id?}");
});
```

In the preceding code, [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute) is called to create the `"blog_route"`. The second parameter, `"Blog"`, is the area name.

When matching a URL path like `/Manage/Users/AddUser`, the `"blog_route"` route generates the route values `{ area = Blog, controller = Users, action = AddUser }`. The `area` route value comes from a default value for `area`. The route created by `MapAreaControllerRoute` is equivalent to the following code:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute("blog_route", "Manage/{controller}/{action}/{id?}",
        defaults: new { area = "Blog" }, constraints: new { area = "Blog" });
    endpoints.MapControllerRoute("default_route", "{controller}/{action}/{id?}");
});
```

`MapAreaControllerRoute` creates a route using both a default value and constraint for `area` by using the provided area name, in this case `Blog`. The default value ensures that the route always produces `{ area = Blog, ... }`, and the constraint requires the value `{ area = Blog, ... }` for URL generation.

Conventional routing is order-dependent. In general, place routes with areas earlier as they're more specific than routes without an area.

Using the preceding example, the route values `{ area = Blog, controller = Users, action = AddUser }` match the following action:

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        // GET /manage/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
```

The [\[Area\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.areaattribute) attribute is what denotes a controller as part of an area. This controller is in the `Blog` area. Controllers without an `[Area]` attribute aren't members of any area, and don't match when the `area` route value is provided by routing. In the following example, only the first controller listed can match the route values `{ area = Blog, controller = Users, action = AddUser }`.

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        // GET /manage/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
```

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace2
{
    // Matches { area = Zebra, controller = Users, action = AddUser }
    [Area("Zebra")]
    public class UsersController : Controller
    {
        // GET /zebra/users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }        
    }
}
```

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace3
{
    // Matches { area = string.Empty, controller = Users, action = AddUser }
    // Matches { area = null, controller = Users, action = AddUser }
    // Matches { controller = Users, action = AddUser }
    public class UsersController : Controller
    {
        // GET /users/adduser
        public IActionResult AddUser()
        {
            var area = ControllerContext.ActionDescriptor.RouteValues["area"];
            var actionName = ControllerContext.ActionDescriptor.ActionName;
            var controllerName = ControllerContext.ActionDescriptor.ControllerName;

            return Content($"area name:{area}" +
                $" controller:{controllerName}  action name: {actionName}");
        }
    }
}
```

For completeness, the namespace of each controller is shown here. If the preceding controllers used the same namespace, a compiler error would be generated. Class namespaces have no effect on MVC's routing.

The first two controllers are members of areas, and only match when their respective area name is provided by the `area` route value. The third controller isn't a member of any area, and can only match when no value for `area` is provided by routing.

[]()

In terms of matching *no value*, the absence of the `area` value is the same as if the value for `area` were null or the empty string.

When executing an action inside an area, the route value for `area` is available as an [ambient value](#ambient) for routing to use for URL generation. This means that by default areas act *sticky* for URL generation as demonstrated by the following sample.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapAreaControllerRoute(name: "duck_route", 
                                     areaName: "Duck",
                                     pattern: "Manage/{controller}/{action}/{id?}");
    endpoints.MapControllerRoute(name: "default",
                                 pattern: "Manage/{controller=Home}/{action=Index}/{id?}");
});
```

```
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace4
{
    [Area("Duck")]
    public class UsersController : Controller
    {
        // GET /Manage/users/GenerateURLInArea
        public IActionResult GenerateURLInArea()
        {
            // Uses the 'ambient' value of area.
            var url = Url.Action("Index", "Home");
            // Returns /Manage/Home/Index
            return Content(url);
        }

        // GET /Manage/users/GenerateURLOutsideOfArea
        public IActionResult GenerateURLOutsideOfArea()
        {
            // Uses the empty value for area.
            var url = Url.Action("Index", "Home", new { area = "" });
            // Returns /Manage
            return Content(url);
        }
    }
}
```

The following code generates a URL to `/Zebra/Users/AddUser`:

```
public class HomeController : Controller
{
    public IActionResult About()
    {
        var url = Url.Action("AddUser", "Users", new { Area = "Zebra" });
        return Content($"URL: {url}");
    }
```

[]()

## Action definition

Public methods on a controller, except those with the [NonAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.nonactionattribute) attribute, are actions.

## Sample code

* [MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/routing/samples/3.x) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Debug diagnostics

For detailed routing diagnostic output, set `Logging:LogLevel:Microsoft` to `Debug`. In the `Development` environment, set the log level in `appsettings.Development.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/how-to-choose-identity-solution
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/how-to-choose-identity-solution.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Choose an identity management solution

Most web apps support authentication to ensure that users are who they claim to be. A *user* might be a person or another app. Management of access ensures users are only able to see and modify the information they're authorized to see and modify. For example, an end user shouldn't have access to the administrative section of a website. Identity management solutions are built to handle the requirements of authentication and authorization-related tasks. To learn more about identity management, see [What is identity and access management?](/en-us/azure/active-directory/fundamentals/introduction-identity-access-management). Many [identity management solutions for .NET web apps](identity-management-solutions?view=aspnetcore-10.0) are available, each with different capabilities and requirements to use or install. This article provides guidance on how to choose the right solution.

## Basic identity management with ASP.NET Core Identity

ASP.NET Core includes the built-in [ASP.NET Core Identity](authentication/identity?view=aspnetcore-10.0) authentication provider. The provider includes the APIs, UI, and backend database configuration to support managing user identities, storing user credentials, and granting or revoking permissions. Additional features include:

* [External logins](authentication/social/?view=aspnetcore-10.0)
* [Multi-factor authentication (MFA)](authentication/mfa?view=aspnetcore-10.0)
* [Password management](authentication/accconfirm?view=aspnetcore-10.0)
* Account lockout and reactivation
* Authenticator apps

For most scenarios, this may be the only provider needed. In other scenarios, a server or service that manages authentication and identity may be beneficial.

For more information, see [Introduction to Identity on ASP.NET Core](authentication/identity?view=aspnetcore-10.0).

## Determine if an OIDC server is needed

Web apps require a way to *remember* past actions because the web, by default, is stateless. Otherwise, users would be forced to enter their credentials every time they navigated to a new page. The common solution for remembering state is *cookies*, a browser-based mechanism for storing data. The web server sends the initial cookie, then the browser stores it and sends it back with each request. This is done automatically without the need for the developer to write any code. Cookies are easy to use and built into the browser but are designed for use within a single website or web domain. The default solution that is built into ASP.NET Core uses cookie-based authentication.

Tokens are containers with metadata that are explicitly passed through the headers or body of HTTP requests. The main advantage of tokens over cookies is that they are not tied to a specific app or domain. Instead, tokens are usually *signed* with asymmetric cryptography. For example, OIDC servers issue tokens with information about identity using the [JSON Web Token (JWT)](https://en.wikipedia.org/wiki/JSON_Web_Token) format which includes signing. Asymmetric cryptography uses a combination of a private key known only to the signer, and a public key which everyone can know. Tokens may also be encrypted.

The signed token can't be tampered with due to the private key. The public key:

* Makes it possible to validate the token to ensure it hasn't been changed.
* Guarantees that it was generated by the entity holding the private key.

The main disadvantage to using tokens is that they require a service (typically an OIDC server) to both issue and provide validation for tokens. The service must be installed, configured, and maintained.

A common reason an OIDC server is required is for applications that expose web-based APIs that are consumed by other apps. For exposed web-based APIs, client UIs such as Single Page Applications (SPA), mobile clients, and desktop clients are considered to be part of the same app. SPA examples include Angular, React, and Blazor WebAssembly. If apps other than your web app or any client UIs must make a secure API call to your app, you'll likely want to use tokens. If you only have client UIs, ASP.NET Core Identity provides the option to acquire a token during authentication. The authentication token issued by ASP.NET Core Identity:

* Can be used by mobile and desktop clients. Cookies are preferred over tokens for both security and simplicity.
* Isn't suitable for managing access from third-party apps.

Another reason an OIDC server is required is for sharing sign-ins with other apps. Commonly referred to as *single sign on*, this feature enables users to:

* Sign in once with a web app's form.
* Use the resulting credentials to authenticate with other apps without having to sign-in again or choose a different password.

An OIDC server is typically preferred to provide a secure and scalable solution for single sign on.

For apps that don't share logins with other apps, the simplest way to quickly secure an app is to use the built-in ASP.NET Core Identity provider. Otherwise, an OIDC server provided by a third-party identity management solution is needed. OIDC servers are available as:

* Products you install on your server, called *self-host*.
* Containers run in a host like Docker.
* Web-based services you integrate with to manage identity.

Some solutions are free and open source, while others are commercially licensed. See [identity management solutions](identity-management-solutions?view=aspnetcore-10.0) for a list of available options. It's possible that your organization already uses an identity provider. In that case, it may make sense to use the existing provider instead of going with a different solution. All of the major solutions provide documentation for configuring ASP.NET Core to use their product or service.

## Disconnected scenarios

Many solutions, such as [Microsoft Entra ID](/en-us/entra/fundamentals/what-is-entra), are cloud-based and require an Internet connection to work. If your environment doesn't allow Internet connectivity, you won't be able to use the service.

ASP.NET Core Identity works perfectly well in disconnected scenarios, such as:

* The app can't access the Internet.
* The app must still function on the local network even if the Internet is disconnected.

If you require a full OIDC server for a disconnected scenario, choose one of the following options:

* A solution that allows you to install and run the service on your own machines.
* Run the authentication service locally as a container.

## Decide where user data such as sign-ins are stored

Another important factor to consider is where the user sign-in data is stored. Many developers choose external, cloud-based services like Microsoft Entra ID to manage identity. A cloud-based service provider:

* Takes on the responsibilities of securely storing data.
* keeps the software up to date with the latest security patches and releases.
* Complies with privacy regulations.

Others prefer to store data on their own servers due to regulations, compliance, policy, or other reasons.

If the data is stored on your servers, you'll most likely need to choose an installable or container-based solution.

## Identity vs OIDC server

Use the following diagram to help you decide whether to use the ASP.NET Core Identity system or an OIDC server for authentication and authorization:

The following table lists some of the things to consider when choosing your identity management solution.

| **Feature**                  | **Self-host (infrastructure or container)**                                                                                                                                                                                                                                | **Cloud**                                                                                                                                                                                              |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **App integration**          | Local solutions that are implemented as libraries or frameworks can often be integrated directly in your own app. Container-based solutions require a hand-off to occur between your web app and the container-based service.                                              | Cloud-based solutions typically integrate at specific points in your sign-in flow and provide configuration to update the UI to match your theme, but the level of customization available is limited. |
| **Configuration**            | Self host solutions require configuring the software for the environment in addition to setting up how you want to manage identities. Container-based solutions typically provide a web-based UI for configuration.                                                        | Cloud-based solutions typically provide a web-based UI for configuration.                                                                                                                              |
| **Customization**            | Self-host solutions are usually highly customizable, including code-based changes. Although containerized solutions provide extensibility options, they are often more limited.                                                                                            | Cloud-based services allow customization, but it's typically limited to configuration-based changes.                                                                                                   |
| **Maintenance**              | Installed products require a dedicated resource to ensure all security patches are applied in a timely fashion and to manage upgrades. The upgrade and patch process for containers is usually lower-friction and involves simply installing the provided container image. | The service provider maintains their cloud-based solution, including applying needed patches and handling upgrades.                                                                                    |
| **User credentials storage** | You are responsible for data governance and handling breaches.                                                                                                                                                                                                             | Managing the risks associated with handling user credentials, and complying with regulations. is delegated to the service provider.                                                                    |

For more information about available options, see [Identity management solutions for ASP.NET Core](identity-management-solutions?view=aspnetcore-10.0).

## Next steps

* Learn about [JSON Web Tokens](https://auth0.com/docs/secure/tokens/json-web-tokens)
* Browse [sample apps with authentication/authorization and identity for ASP.NET Core](/en-us/aspnet/core/security/authentication/samples).
* Follow a tutorial to [secure a .NET web app using built-in ASP.NET Core Identity](/en-us/training/modules/secure-aspnet-core-identity).
* Learn more about how to [protect web APIs](/en-us/azure/active-directory/develop/index-web-api).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-dotnet-from-javascript
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/javascript-interoperability/call-dotnet-from-javascript.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Call .NET methods from JavaScript functions in ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to invoke .NET methods from JavaScript (JS).

For information on how to call JS functions from .NET, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0).

Note

[C# union types](/en-us/dotnet/csharp/language-reference/builtin-types/union) are supported.

## Invoke a static .NET method

To invoke a static .NET method from JavaScript (JS), use the JS functions:

* `DotNet.invokeMethodAsync` (*recommended*): Asynchronous for both server-side and client-side components.
* `DotNet.invokeMethod`: Synchronous for client-side components only.

Pass in the name of the assembly containing the method, the identifier of the static .NET method, and any arguments.

In the following example:

* The `{PACKAGE ID/ASSEMBLY NAME}` placeholder is the project's package ID (`<PackageId>` in the project file) for a library or assembly name for an app.
* The `{.NET METHOD ID}` placeholder is the .NET method identifier.
* The `{ARGUMENTS}` placeholder are optional, comma-separated arguments to pass to the method, each of which must be JSON-serializable.

```
DotNet.invokeMethodAsync('{PACKAGE ID/ASSEMBLY NAME}', '{.NET METHOD ID}', {ARGUMENTS});
```

`DotNet.invokeMethodAsync` returns a [JS `Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) representing the result of the operation. `DotNet.invokeMethod` (client-side components) returns the result of the operation.

Important

For server-side components, we recommend the asynchronous function (`invokeMethodAsync`) over the synchronous version (`invokeMethod`).

The .NET method must be public, static, and have the [`[JSInvokable]` attribute](/en-us/dotnet/api/microsoft.jsinterop.jsinvokableattribute).

In the following example:

* The `{<T>}` placeholder indicates the return type, which is only required for methods that return a value.
* The `{.NET METHOD ID}` placeholder is the method identifier.

```
@code {
    [JSInvokable]
    public static Task{<T>} {.NET METHOD ID}()
    {
        ...
    }
}
```

Note

Calling open generic methods isn't supported with static .NET methods but is supported with instance methods. For more information, see the [Call .NET generic class methods](#call-net-generic-class-methods) section.

In the following component, the `ReturnArrayAsync` C# method returns an `int` array. The [`[JSInvokable]` attribute](/en-us/dotnet/api/microsoft.jsinterop.jsinvokableattribute) is applied to the method, which makes the method invokable by JS.

`CallDotnet1.razor`:

```
@page "/call-dotnet-1"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET 1</PageTitle>

<h1>Call .NET Example 1</h1>

<p>
    <button id="btn">Trigger .NET static method</button>
</p>

<p>
    See the result in the developer tools console.
</p>

@code {
    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./Components/Pages/CallDotnet1.razor.js");

            await module.InvokeVoidAsync("addHandlers");
        }
    }

    [JSInvokable]
    public static Task<int[]> ReturnArrayAsync() =>
        Task.FromResult(new int[] { 11, 12, 13 });

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

`CallDotnet1.razor.js`:

```
export function returnArrayAsync() {
  DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
    .then(data => {
      console.log(data);
    });
}

export function addHandlers() {
  const btn = document.getElementById("btn");
  btn.addEventListener("click", returnArrayAsync);
}
```

The `addHandlers` JS function adds a [`click`](https://developer.mozilla.org/docs/Web/API/Element/click_event) event to the button. The `returnArrayAsync` JS function is assigned as the handler.

The `returnArrayAsync` JS function calls the `ReturnArrayAsync` .NET method of the component, which logs the result to the browser's web developer tools console. `BlazorSample` is the app's assembly name.

`CallDotnet1.razor`:

```
@page "/call-dotnet-1"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET 1</PageTitle>

<h1>Call .NET Example 1</h1>

<p>
    <button id="btn">Trigger .NET static method</button>
</p>

<p>
    See the result in the developer tools console.
</p>

@code {
    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./Components/Pages/CallDotnet1.razor.js");

            await module.InvokeVoidAsync("addHandlers");
        }
    }

    [JSInvokable]
    public static Task<int[]> ReturnArrayAsync() =>
        Task.FromResult(new int[] { 11, 12, 13 });

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

`CallDotnet1.razor.js`:

```
export function returnArrayAsync() {
  DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
    .then(data => {
      console.log(data);
    });
}

export function addHandlers() {
  const btn = document.getElementById("btn");
  btn.addEventListener("click", returnArrayAsync);
}
```

The `addHandlers` JS function adds a [`click`](https://developer.mozilla.org/docs/Web/API/Element/click_event) event to the button. The `returnArrayAsync` JS function is assigned as the handler.

The `returnArrayAsync` JS function calls the `ReturnArrayAsync` .NET method of the component, which logs the result to the browser's web developer tools console. `BlazorSample` is the app's assembly name.

`CallDotNetExample1.razor`:

```
@page "/call-dotnet-example-1"

<h1>Call .NET Example 1</h1>

<p>
    <button onclick="returnArrayAsync()">
        Trigger .NET static method
    </button>
</p>

@code {
    [JSInvokable]
    public static Task<int[]> ReturnArrayAsync()
    {
        return Task.FromResult(new int[] { 1, 2, 3 });
    }
}
```

`CallDotNetExample1.razor`:

```
@page "/call-dotnet-example-1"

<h1>Call .NET Example 1</h1>

<p>
    <button onclick="returnArrayAsync()">
        Trigger .NET static method
    </button>
</p>

@code {
    [JSInvokable]
    public static Task<int[]> ReturnArrayAsync()
    {
        return Task.FromResult(new int[] { 1, 2, 3 });
    }
}
```

`CallDotNetExample1.razor`:

```
@page "/call-dotnet-example-1"

<h1>Call .NET Example 1</h1>

<p>
    <button onclick="returnArrayAsync()">
        Trigger .NET static method
    </button>
</p>

@code {
    [JSInvokable]
    public static Task<int[]> ReturnArrayAsync()
    {
        return Task.FromResult(new int[] { 1, 2, 3 });
    }
}
```

`CallDotNetExample1.razor`:

```
@page "/call-dotnet-example-1"

<h1>Call .NET Example 1</h1>

<p>
    <button onclick="returnArrayAsync()">
        Trigger .NET static method
    </button>
</p>

@code {
    [JSInvokable]
    public static Task<int[]> ReturnArrayAsync()
    {
        return Task.FromResult(new int[] { 1, 2, 3 });
    }
}
```

The `<button>` element's `onclick` HTML attribute is JavaScript's `onclick` event handler assignment for processing [`click`](https://developer.mozilla.org/docs/Web/API/Element/click_event) events, not Blazor's `@onclick` directive attribute. The `returnArrayAsync` JS function is assigned as the handler.

The following `returnArrayAsync` JS function, calls the `ReturnArrayAsync` .NET method of the component, which logs the result to the browser's web developer tools console. `BlazorSample` is the app's assembly name.

```
<script>
  window.returnArrayAsync = () => {
    DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
      .then(data => {
        console.log(data);
      });
    };
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

When the **`Trigger .NET static method`** button is selected, the browser's developer tools console output displays the array data. The format of the output differs slightly among browsers. The following output shows the format used by Microsoft Edge:

```
Array(3) [ 11, 12, 13 ]
```

Pass data to a .NET method when calling the `invokeMethodAsync` function by passing the data as arguments.

To demonstrate passing data to .NET, pass a starting position to the `ReturnArrayAsync` method where the method is invoked in JS:

```
export function returnArrayAsync() {
  DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync', 14)
    .then(data => {
      console.log(data);
    });
}
```

```
<script>
  window.returnArrayAsync = () => {
    DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync', 14)
      .then(data => {
        console.log(data);
      });
    };
</script>
```

The component's invokable `ReturnArrayAsync` method receives the starting position and constructs the array from it. The array is returned for logging to the console:

```
[JSInvokable]
public static Task<int[]> ReturnArrayAsync(int startPosition) => 
    Task.FromResult(Enumerable.Range(startPosition, 3).ToArray());
```

After the app is recompiled and the browser is refreshed, the following output appears in the browser's console when the button is selected:

```
Array(3) [ 14, 15, 16 ]
```

The .NET method identifier for the JS call is the .NET method name, but you can specify a different identifier using the [`[JSInvokable]` attribute](/en-us/dotnet/api/microsoft.jsinterop.jsinvokableattribute) constructor. In the following example, `DifferentMethodName` is the assigned method identifier for the `ReturnArrayAsync` method:

```
[JSInvokable("DifferentMethodName")]
```

In the call to `DotNet.invokeMethodAsync` (server-side or client-side components) or `DotNet.invokeMethod` (client-side components only), call `DifferentMethodName` to execute the `ReturnArrayAsync` .NET method:

* `DotNet.invokeMethodAsync('BlazorSample', 'DifferentMethodName');`
* `DotNet.invokeMethod('BlazorSample', 'DifferentMethodName');` (client-side components only)

Note

The `ReturnArrayAsync` method example in this section returns the result of a [Task](/en-us/dotnet/api/system.threading.tasks.task) without the use of explicit C# [`async`](/en-us/dotnet/csharp/language-reference/keywords/async) and [`await`](/en-us/dotnet/csharp/language-reference/operators/await) keywords. Coding methods with [`async`](/en-us/dotnet/csharp/language-reference/keywords/async) and [`await`](/en-us/dotnet/csharp/language-reference/operators/await) is typical of methods that use the [`await`](/en-us/dotnet/csharp/language-reference/operators/await) keyword to return the value of asynchronous operations.

`ReturnArrayAsync` method composed with [`async`](/en-us/dotnet/csharp/language-reference/keywords/async) and [`await`](/en-us/dotnet/csharp/language-reference/operators/await) keywords:

```
[JSInvokable]
public static async Task<int[]> ReturnArrayAsync() => 
    await Task.FromResult(new int[] { 11, 12, 13 });
```

For more information, see [Asynchronous programming with async and await](/en-us/dotnet/csharp/programming-guide/concepts/async/) in the C# guide.

## Create JavaScript object and data references to pass to .NET

Call `DotNet.createJSObjectReference(jsObject)` to construct a JS object reference so that it can be passed to .NET, where `jsObject` is the [JS `Object`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object) used to create the JS object reference. The following example passes a reference to the non-serializable `window` object to .NET, which receives it in the `ReceiveWindowObject` C# method as an [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference):

```
DotNet.invokeMethodAsync('{PACKAGE ID/ASSEMBLY NAME}', 'ReceiveWindowObject', 
  DotNet.createJSObjectReference(window));
```

```
[JSInvokable]
public static void ReceiveWindowObject(IJSObjectReference objRef)
{
    ...
}
```

In the preceding example, the `{PACKAGE ID/ASSEMBLY NAME}` placeholder is the project's package ID (`<PackageId>` in the project file) for a library or assembly name for an app.

Note

The preceding example doesn't require disposal of the `JSObjectReference`, as a reference to the `window` object isn't held in JS.

Maintaining a reference to a `JSObjectReference` requires disposing of it to avoid leaking JS memory on the client. The following example refactors the preceding code to capture a reference to the `JSObjectReference`, followed by a call to `DotNet.disposeJSObjectReference()` to dispose of the reference:

```
var jsObjectReference = DotNet.createJSObjectReference(window);

DotNet.invokeMethodAsync('{PACKAGE ID/ASSEMBLY NAME}', 'ReceiveWindowObject', jsObjectReference);

DotNet.disposeJSObjectReference(jsObjectReference);
```

In the preceding example, the `{PACKAGE ID/ASSEMBLY NAME}` placeholder is the project's package ID (`<PackageId>` in the project file) for a library or assembly name for an app.

Call `DotNet.createJSStreamReference(streamReference)` to construct a JS stream reference so that it can be passed to .NET, where `streamReference` is an [`ArrayBuffer`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob), or any [typed array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypedArray), such as [`Uint8Array`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) or [`Float32Array`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Float32Array), used to create the JS stream reference.

## Invoke an instance .NET method

To invoke an instance .NET method from JavaScript (JS):

* Pass the .NET instance by reference to JS by wrapping the instance in a [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) and calling [Create](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference.create) on it.

* Invoke a .NET instance method from JS using `invokeMethodAsync` (*recommended*) or `invokeMethod` (client-side components only) from the passed [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference). Pass the identifier of the instance .NET method and any arguments. The .NET instance can also be passed as an argument when invoking other .NET methods from JS.

  In the following example:

  * `dotNetHelper` is a [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference).
  * The `{.NET METHOD ID}` placeholder is the .NET method identifier.
  * The `{ARGUMENTS}` placeholder are optional, comma-separated arguments to pass to the method, each of which must be JSON-serializable.

  ```
  dotNetHelper.invokeMethodAsync('{.NET METHOD ID}', {ARGUMENTS});
  ```

  Note

  `invokeMethodAsync` and `invokeMethod` don't accept an assembly name parameter when invoking an instance method.

  `invokeMethodAsync` returns a [JS `Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) representing the result of the operation. `invokeMethod` (client-side components only) returns the result of the operation.

  Important

  For server-side components, we recommend the asynchronous function (`invokeMethodAsync`) over the synchronous version (`invokeMethod`).

* Dispose of the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference).

The following sections of this article demonstrate various approaches for invoking an instance .NET method:

* [Pass a `DotNetObjectReference` to an individual JavaScript function](#pass-a-dotnetobjectreference-to-an-individual-javascript-function)
* [Pass a `DotNetObjectReference` to a class with multiple JavaScript functions](#pass-a-dotnetobjectreference-to-a-class-with-multiple-javascript-functions)
* [Call .NET generic class methods](#call-net-generic-class-methods)
* [Class instance examples](#class-instance-examples)
* [Component instance .NET method helper class](#component-instance-net-method-helper-class)

- [Pass a `DotNetObjectReference` to an individual JavaScript function](#pass-a-dotnetobjectreference-to-an-individual-javascript-function)
- [Pass a `DotNetObjectReference` to a class with multiple JavaScript functions](#pass-a-dotnetobjectreference-to-a-class-with-multiple-javascript-functions)
- [Class instance examples](#class-instance-examples)
- [Component instance .NET method helper class](#component-instance-net-method-helper-class)

## Avoid trimming JavaScript-invokable .NET methods

*This section applies to client-side apps with [ahead-of-time (AOT) compilation](../webassembly-build-tools-and-aot?view=aspnetcore-10.0#ahead-of-time-aot-compilation) and [runtime relinking](../webassembly-build-tools-and-aot?view=aspnetcore-10.0#runtime-relinking) enabled.*

Several of the examples in the following sections are based on a class instance approach, where the JavaScript-invokable .NET method marked with the [`[JSInvokable]` attribute](/en-us/dotnet/api/microsoft.jsinterop.jsinvokableattribute) is a member of a class that isn't a Razor component. When such .NET methods are located in a Razor component, they're protected from [runtime relinking/trimming](../webassembly-build-tools-and-aot?view=aspnetcore-10.0#runtime-relinking). In order to protect the .NET methods from trimming outside of Razor components, implement the methods with the [`DynamicDependency` attribute](/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute) on the class's constructor, as the following example demonstrates:

```
using System.Diagnostics.CodeAnalysis;
using Microsoft.JSInterop;

public class ExampleClass {

    [DynamicDependency(nameof(ExampleJSInvokableMethod))]
    public ExampleClass()
    {
    }

    [JSInvokable]
    public string ExampleJSInvokableMethod()
    {
        ...
    }
}
```

For more information, see [Prepare .NET libraries for trimming: DynamicDependency](/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming#dynamicdependency).

## Pass a `DotNetObjectReference` to an individual JavaScript function

The example in this section demonstrates how to pass a [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) to an individual JavaScript (JS) function.

The following `sayHello1` JS function receives a [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) and calls `invokeMethodAsync` to call the `GetHelloMessage` .NET method of a component:

```
<script>
  window.sayHello1 = (dotNetHelper) => {
    return dotNetHelper.invokeMethodAsync('GetHelloMessage');
  };
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

In the preceding example, the variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.

For the following component:

* The component has a JS-invokable .NET method named `GetHelloMessage`.

* When the **`Trigger .NET instance method`** button is selected, the JS function `sayHello1` is called with the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference).

* `sayHello1`:

  * Calls `GetHelloMessage` and receives the message result.
  * Returns the message result to the calling `TriggerDotNetInstanceMethod` method.

* The returned message from `sayHello1` in `result` is displayed to the user.

* To avoid a memory leak and allow garbage collection, the .NET object reference created by [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) is disposed in the `Dispose` method.

`CallDotnet2.razor`:

```
@page "/call-dotnet-2"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET 2</PageTitle>

<h1>Call .NET Example 2</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private DotNetObjectReference<CallDotnet2>? objRef;

    protected override void OnInitialized() =>
        objRef = DotNetObjectReference.Create(this);

    public async Task TriggerDotNetInstanceMethod() =>
        result = await JS.InvokeAsync<string>("sayHello1", objRef);

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {name}!";

    public void Dispose() => objRef?.Dispose();
}
```

`CallDotnet2.razor`:

```
@page "/call-dotnet-2"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET 2</PageTitle>

<h1>Call .NET Example 2</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private DotNetObjectReference<CallDotnet2>? objRef;

    protected override void OnInitialized() =>
        objRef = DotNetObjectReference.Create(this);

    public async Task TriggerDotNetInstanceMethod() =>
        result = await JS.InvokeAsync<string>("sayHello1", objRef);

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {name}!";

    public void Dispose() => objRef?.Dispose();
}
```

`CallDotNetExample2.razor`:

```
@page "/call-dotnet-example-2"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 2</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private DotNetObjectReference<CallDotNetExample2>? objRef;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    public async Task TriggerDotNetInstanceMethod()
    {
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {name}!";

    public void Dispose()
    {
        objRef?.Dispose();
    }
}
```

`CallDotNetExample2.razor`:

```
@page "/call-dotnet-example-2"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 2</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private DotNetObjectReference<CallDotNetExample2>? objRef;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    public async Task TriggerDotNetInstanceMethod()
    {
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {name}!";

    public void Dispose()
    {
        objRef?.Dispose();
    }
}
```

`CallDotNetExample2.razor`:

```
@page "/call-dotnet-example-2"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 2</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;
    private DotNetObjectReference<CallDotNetExample2> objRef;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    public async Task TriggerDotNetInstanceMethod()
    {
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {name}!";

    public void Dispose()
    {
        objRef?.Dispose();
    }
}
```

`CallDotNetExample2.razor`:

```
@page "/call-dotnet-example-2"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 2</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;
    private DotNetObjectReference<CallDotNetExample2> objRef;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    public async Task TriggerDotNetInstanceMethod()
    {
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {name}!";

    public void Dispose()
    {
        objRef?.Dispose();
    }
}
```

In the preceding example, the variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.

Use the following guidance to pass arguments to an instance method:

Add parameters to the .NET method invocation. In the following example, a name is passed to the method. Add additional parameters to the list as needed.

```
<script>
  window.sayHello2 = (dotNetHelper, name) => {
    return dotNetHelper.invokeMethodAsync('GetHelloMessage', name);
  };
</script>
```

In the preceding example, the variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.

Provide the parameter list to the .NET method.

`CallDotnet3.razor`:

```
@page "/call-dotnet-3"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET 3</PageTitle>

<h1>Call .NET Example 3</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private DotNetObjectReference<CallDotnet3>? objRef;

    protected override void OnInitialized() => 
        objRef = DotNetObjectReference.Create(this);

    public async Task TriggerDotNetInstanceMethod() =>
        result = await JS.InvokeAsync<string>("sayHello2", objRef, name);

    [JSInvokable]
    public string GetHelloMessage(string passedName) => $"Hello, {passedName}!";

    public void Dispose() => objRef?.Dispose();
}
```

`CallDotnet3.razor`:

```
@page "/call-dotnet-3"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET 3</PageTitle>

<h1>Call .NET Example 3</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private DotNetObjectReference<CallDotnet3>? objRef;

    protected override void OnInitialized() => 
        objRef = DotNetObjectReference.Create(this);

    public async Task TriggerDotNetInstanceMethod() =>
        result = await JS.InvokeAsync<string>("sayHello2", objRef, name);

    [JSInvokable]
    public string GetHelloMessage(string passedName) => $"Hello, {passedName}!";

    public void Dispose() => objRef?.Dispose();
}
```

`CallDotNetExample3.razor`:

```
@page "/call-dotnet-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 3</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private DotNetObjectReference<CallDotNetExample3>? objRef;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    public async Task TriggerDotNetInstanceMethod()
    {
        result = await JS.InvokeAsync<string>("sayHello2", objRef, name);
    }

    [JSInvokable]
    public string GetHelloMessage(string passedName) => $"Hello, {passedName}!";

    public void Dispose()
    {
        objRef?.Dispose();
    }
}
```

`CallDotNetExample3.razor`:

```
@page "/call-dotnet-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 3</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private DotNetObjectReference<CallDotNetExample3>? objRef;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    public async Task TriggerDotNetInstanceMethod()
    {
        result = await JS.InvokeAsync<string>("sayHello2", objRef, name);
    }

    [JSInvokable]
    public string GetHelloMessage(string passedName) => $"Hello, {passedName}!";

    public void Dispose()
    {
        objRef?.Dispose();
    }
}
```

`CallDotNetExample3.razor`:

```
@page "/call-dotnet-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 3</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;
    private DotNetObjectReference<CallDotNetExample3> objRef;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    public async Task TriggerDotNetInstanceMethod()
    {
        result = await JS.InvokeAsync<string>("sayHello2", objRef, name);
    }

    [JSInvokable]
    public string GetHelloMessage(string passedName) => $"Hello, {passedName}!";

    public void Dispose()
    {
        objRef?.Dispose();
    }
}
```

`CallDotNetExample3.razor`:

```
@page "/call-dotnet-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call .NET Example 3</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;
    private DotNetObjectReference<CallDotNetExample3> objRef;

    protected override void OnInitialized()
    {
        objRef = DotNetObjectReference.Create(this);
    }

    public async Task TriggerDotNetInstanceMethod()
    {
        result = await JS.InvokeAsync<string>("sayHello2", objRef, name);
    }

    [JSInvokable]
    public string GetHelloMessage(string passedName) => $"Hello, {passedName}!";

    public void Dispose()
    {
        objRef?.Dispose();
    }
}
```

In the preceding example, the variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.

## Pass a `DotNetObjectReference` to a class with multiple JavaScript functions

The example in this section demonstrates how to pass a [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) to a JavaScript (JS) class with multiple functions.

Create and pass a [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) from the [`OnAfterRenderAsync` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync) to a JS class for multiple functions to use. Make sure that the .NET code disposes of the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference), as the following example shows.

In the following component, the `Trigger JS function` buttons call JS functions by setting the JS `onclick` property, not Blazor's `@onclick` directive attribute.

`CallDotNetExampleOneHelper.razor`:

```
@page "/call-dotnet-example-one-helper"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET Example</PageTitle>

<h1>Pass <code>DotNetObjectReference</code> to a JavaScript class</h1>

<p>
    <label>
        Message: <input @bind="name" />
    </label>
</p>

<p>
    <button id="sayHelloBtn">
        Trigger JS function <code>sayHello</code>
    </button>
</p>

<p>
    <button id="welcomeVisitorBtn">
        Trigger JS function <code>welcomeVisitor</code>
    </button>
</p>

@code {
    private IJSObjectReference? module;
    private string? name;
    private DotNetObjectReference<CallDotNetExampleOneHelper>? dotNetHelper;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./Components/Pages/CallDotNetExampleOneHelper.razor.js");

            dotNetHelper = DotNetObjectReference.Create(this);
            await module.InvokeVoidAsync("GreetingHelpers.setDotNetHelper", 
                dotNetHelper);

            await module.InvokeVoidAsync("addHandlers");
        }
    }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {name}!";

    [JSInvokable]
    public string GetWelcomeMessage() => $"Welcome, {name}!";

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }

        dotNetHelper?.Dispose();
    }
}
```

In the preceding example:

* `JS` is an injected [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) instance. [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) is registered by the Blazor framework.
* The variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.
* The component must explicitly dispose of the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) to permit garbage collection and prevent a memory leak.
* [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) is trapped during module disposal in case Blazor's SignalR circuit is lost. If the preceding code is used in a Blazor WebAssembly app, there's no SignalR connection to lose, so you can remove the `try`-`catch` block and leave the line that disposes the module (`await module.DisposeAsync();`). For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit).

`CallDotNetExampleOneHelper.razor.js`:

```
export class GreetingHelpers {
  static dotNetHelper;

  static setDotNetHelper(value) {
    GreetingHelpers.dotNetHelper = value;
  }

  static async sayHello() {
    const msg =
      await GreetingHelpers.dotNetHelper.invokeMethodAsync('GetHelloMessage');
    alert(`Message from .NET: "${msg}"`);
  }

  static async welcomeVisitor() {
    const msg =
      await GreetingHelpers.dotNetHelper.invokeMethodAsync('GetWelcomeMessage');
    alert(`Message from .NET: "${msg}"`);
  }
}

export function addHandlers() {
  const sayHelloBtn = document.getElementById("sayHelloBtn");
  sayHelloBtn.addEventListener("click", GreetingHelpers.sayHello);

  const welcomeVisitorBtn = document.getElementById("welcomeVisitorBtn");
  welcomeVisitorBtn.addEventListener("click", GreetingHelpers.welcomeVisitor);
}
```

In the preceding example, the variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.

```
@page "/call-dotnet-example-one-helper"
@implements IDisposable
@inject IJSRuntime JS

<h1>Pass <code>DotNetObjectReference</code> to a JavaScript class</h1>

<p>
    <label>
        Message: <input @bind="name" />
    </label>
</p>

<p>
    <button onclick="GreetingHelpers.sayHello()">
        Trigger JS function <code>sayHello</code>
    </button>
</p>

<p>
    <button onclick="GreetingHelpers.welcomeVisitor()">
        Trigger JS function <code>welcomeVisitor</code>
    </button>
</p>

@code {
    private string? name;
    private DotNetObjectReference<CallDotNetExampleOneHelper>? dotNetHelper;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            dotNetHelper = DotNetObjectReference.Create(this);
            await JS.InvokeVoidAsync("GreetingHelpers.setDotNetHelper", 
                dotNetHelper);
        }
    }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {name}!";

    [JSInvokable]
    public string GetWelcomeMessage() => $"Welcome, {name}!";

    public void Dispose()
    {
        dotNetHelper?.Dispose();
    }
}
```

In the preceding example:

* `JS` is an injected [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) instance. [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) is registered by the Blazor framework.
* The variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.
* The component must explicitly dispose of the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) to permit garbage collection and prevent a memory leak.

```
<script>
  class GreetingHelpers {
    static dotNetHelper;

    static setDotNetHelper(value) {
      GreetingHelpers.dotNetHelper = value;
    }

    static async sayHello() {
      const msg = 
        await GreetingHelpers.dotNetHelper.invokeMethodAsync('GetHelloMessage');
      alert(`Message from .NET: "${msg}"`);
    }

    static async welcomeVisitor() {
      const msg = 
        await GreetingHelpers.dotNetHelper.invokeMethodAsync('GetWelcomeMessage');
      alert(`Message from .NET: "${msg}"`);
    }
  }

  window.GreetingHelpers = GreetingHelpers;
</script>
```

In the preceding example:

* The `GreetingHelpers` class is added to the `window` object to globally define the class, which permits Blazor to locate the class for JS interop.
* The variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

## Call .NET generic class methods

JavaScript (JS) functions can call [.NET generic class](/en-us/dotnet/csharp/programming-guide/generics/generic-classes) methods, where a JS function calls a .NET method of a generic class.

In the following generic type class (`GenericType<TValue>`):

* The class has a single type parameter (`TValue`) with a single generic `Value` property.

* The class has two non-generic methods marked with the [`[JSInvokable]` attribute](/en-us/dotnet/api/microsoft.jsinterop.jsinvokableattribute), each with a generic type parameter named `newValue`:

  * `Update` synchronously updates the value of `Value` from `newValue`.
  * `UpdateAsync` asynchronously updates the value of `Value` from `newValue` after creating an awaitable task with [Task.Yield](/en-us/dotnet/api/system.threading.tasks.task.yield) that asynchronously yields back to the current context when awaited.

* Each of the class methods write the type of `TValue` and the value of `Value` to the console. Writing to the console is only for demonstration purposes. Production apps usually avoid writing to the console in favor of app *logging*. For more information, see [ASP.NET Core Blazor logging](../fundamentals/logging?view=aspnetcore-10.0) and [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0).

Note

*Open generic types and methods* don't specify types for type placeholders. Conversely, *closed generics* supply types for all type placeholders. The examples in this section demonstrate closed generics, but invoking JS interop *instance methods* with open generics ***is supported***. Use of open generics isn't supported for [static .NET method invocations](#invoke-a-static-net-method), which were described earlier in this article.

For more information, see the following articles:

* [Generic classes and methods (C# documentation)](/en-us/dotnet/csharp/fundamentals/types/generics)
* [Generic Classes (C# Programming Guide)](/en-us/dotnet/csharp/programming-guide/generics/generic-classes)
* [Generics in .NET (.NET documentation)](/en-us/dotnet/standard/generics/)

`GenericType.cs`:

```
using Microsoft.JSInterop;

public class GenericType<TValue>
{
    public TValue? Value { get; set; }

    [JSInvokable]
    public void Update(TValue newValue)
    {
        Value = newValue;

        Console.WriteLine($"Update: GenericType<{typeof(TValue)}>: {Value}");
    }

    [JSInvokable]
    public async Task UpdateAsync(TValue newValue)
    {
        await Task.Yield();
        Value = newValue;

        Console.WriteLine($"UpdateAsync: GenericType<{typeof(TValue)}>: {Value}");
    }
}
```

In the following `invokeMethodsAsync` function:

* The generic type class's `Update` and `UpdateAsync` methods are called with arguments representing strings and numbers.
* Client-side components support calling .NET methods synchronously with `invokeMethod`. `syncInterop` receives a boolean value indicating if the JS interop is occurring on the client. When `syncInterop` is `true`, `invokeMethod` is safely called. If the value of `syncInterop` is `false`, only the asynchronous function `invokeMethodAsync` is called because the JS interop is executing in a server-side component.
* For demonstration purposes, the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) function call (`invokeMethod` or `invokeMethodAsync`), the .NET method called (`Update` or `UpdateAsync`), and the argument are written to the console. The arguments use a random number to permit matching the JS function call to the .NET method invocation (also written to the console on the .NET side). Production code usually doesn't write to the console, either on the client or the server. Production apps usually rely upon app *logging*. For more information, see [ASP.NET Core Blazor logging](../fundamentals/logging?view=aspnetcore-10.0) and [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0).

```
<script>
  const randomInt = () => Math.floor(Math.random() * 99999);

  window.invokeMethodsAsync = async (syncInterop, dotNetHelper1, dotNetHelper2) => {
    var n = randomInt();
    console.log(`JS: invokeMethodAsync:Update('string ${n}')`);
    await dotNetHelper1.invokeMethodAsync('Update', `string ${n}`);

    n = randomInt();
    console.log(`JS: invokeMethodAsync:UpdateAsync('string ${n}')`);
    await dotNetHelper1.invokeMethodAsync('UpdateAsync', `string ${n}`);

    if (syncInterop) {
      n = randomInt();
      console.log(`JS: invokeMethod:Update('string ${n}')`);
      dotNetHelper1.invokeMethod('Update', `string ${n}`);
    }

    n = randomInt();
    console.log(`JS: invokeMethodAsync:Update(${n})`);
    await dotNetHelper2.invokeMethodAsync('Update', n);

    n = randomInt();
    console.log(`JS: invokeMethodAsync:UpdateAsync(${n})`);
    await dotNetHelper2.invokeMethodAsync('UpdateAsync', n);

    if (syncInterop) {
      n = randomInt();
      console.log(`JS: invokeMethod:Update(${n})`);
      dotNetHelper2.invokeMethod('Update', n);
    }
  };
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

In the following `GenericsExample` component:

* The JS function `invokeMethodsAsync` is called when the **`Invoke Interop`** button is selected.
* A pair of [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) types are created and passed to the JS function for instances of the `GenericType` as a `string` and an `int`.

`GenericsExample.razor`:

```
@page "/generics-example"
@implements IDisposable
@inject IJSRuntime JS

<p>
    <button @onclick="InvokeInterop">Invoke Interop</button>
</p>

<ul>
    <li>genericType1: @genericType1?.Value</li>
    <li>genericType2: @genericType2?.Value</li>
</ul>

@code {
    private GenericType<string> genericType1 = new() { Value = "string 0" };
    private GenericType<int> genericType2 = new() { Value = 0 };
    private DotNetObjectReference<GenericType<string>>? objRef1;
    private DotNetObjectReference<GenericType<int>>? objRef2;

    protected override void OnInitialized()
    {
        objRef1 = DotNetObjectReference.Create(genericType1);
        objRef2 = DotNetObjectReference.Create(genericType2);
    }

    public async Task InvokeInterop()
    {
        var syncInterop = OperatingSystem.IsBrowser();

        await JS.InvokeVoidAsync(
            "invokeMethodsAsync", syncInterop, objRef1, objRef2);
    }

    public void Dispose()
    {
        objRef1?.Dispose();
        objRef2?.Dispose();
    }
}
```

```
@page "/generics-example"
@implements IDisposable
@inject IJSRuntime JS

<p>
    <button @onclick="InvokeInterop">Invoke Interop</button>
</p>

<ul>
    <li>genericType1: @genericType1?.Value</li>
    <li>genericType2: @genericType2?.Value</li>
</ul>

@code {
    private GenericType<string> genericType1 = new() { Value = "string 0" };
    private GenericType<int> genericType2 = new() { Value = 0 };
    private DotNetObjectReference<GenericType<string>>? objRef1;
    private DotNetObjectReference<GenericType<int>>? objRef2;

    protected override void OnInitialized()
    {
        objRef1 = DotNetObjectReference.Create(genericType1);
        objRef2 = DotNetObjectReference.Create(genericType2);
    }

    public async Task InvokeInterop()
    {
        var syncInterop = OperatingSystem.IsBrowser();

        await JS.InvokeVoidAsync(
            "invokeMethodsAsync", syncInterop, objRef1, objRef2);
    }

    public void Dispose()
    {
        objRef1?.Dispose();
        objRef2?.Dispose();
    }
}
```

In the preceding example, `JS` is an injected [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) instance. [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) is registered by the Blazor framework.

The following demonstrates typical output of the preceding example when the **`Invoke Interop`** button is selected in a client-side component:

> JS: invokeMethodAsync:Update('string 37802')\
> .NET: Update: GenericType\<System.String>: string 37802\
> JS: invokeMethodAsync:UpdateAsync('string 53051')\
> JS: invokeMethod:Update('string 26784')\
> .NET: Update: GenericType\<System.String>: string 26784\
> JS: invokeMethodAsync:Update(14107)\
> .NET: Update: GenericType\<System.Int32>: 14107\
> JS: invokeMethodAsync:UpdateAsync(48995)\
> JS: invokeMethod:Update(12872)\
> .NET: Update: GenericType\<System.Int32>: 12872\
> .NET: UpdateAsync: GenericType\<System.String>: string 53051\
> .NET: UpdateAsync: GenericType\<System.Int32>: 48995

If the preceding example is implemented in a server-side component, the synchronous calls with `invokeMethod` are avoided. For server-side components, we recommend the asynchronous function (`invokeMethodAsync`) over the synchronous version (`invokeMethod`).

Typical output of a server-side component:

> JS: invokeMethodAsync:Update('string 34809')\
> .NET: Update: GenericType\<System.String>: string 34809\
> JS: invokeMethodAsync:UpdateAsync('string 93059')\
> JS: invokeMethodAsync:Update(41997)\
> .NET: Update: GenericType\<System.Int32>: 41997\
> JS: invokeMethodAsync:UpdateAsync(24652)\
> .NET: UpdateAsync: GenericType\<System.String>: string 93059\
> .NET: UpdateAsync: GenericType\<System.Int32>: 24652

The preceding output examples demonstrate that asynchronous methods execute and complete in an *arbitrary order* depending on several factors, including thread scheduling and the speed of method execution. It isn't possible to reliably predict the order of completion for asynchronous method calls.

## Class instance examples

The following `sayHello1` JS function:

* Calls the `GetHelloMessage` .NET method on the passed [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference).
* Returns the message from `GetHelloMessage` to the `sayHello1` caller.

```
<script>
  window.sayHello1 = (dotNetHelper) => {
    return dotNetHelper.invokeMethodAsync('GetHelloMessage');
  };
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

In the preceding example, the variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.

The following `HelloHelper` class has a JS-invokable .NET method named `GetHelloMessage`. When `HelloHelper` is created, the name in the `Name` property is used to return a message from `GetHelloMessage`.

`HelloHelper.cs`:

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class HelloHelper(string? name)
{
    public string? Name { get; set; } = name ?? "No Name";

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {Name}!";
}
```

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class HelloHelper(string? name)
{
    public string? Name { get; set; } = name ?? "No Name";

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {Name}!";
}
```

```
using Microsoft.JSInterop;

public class HelloHelper
{
    public HelloHelper(string? name)
    {
        Name = name ?? "No Name";
    }

    public string? Name { get; set; }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {Name}!";
}
```

```
using Microsoft.JSInterop;

public class HelloHelper
{
    public HelloHelper(string? name)
    {
        Name = name ?? "No Name";
    }

    public string? Name { get; set; }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {Name}!";
}
```

```
using Microsoft.JSInterop;

public class HelloHelper
{
    public HelloHelper(string name)
    {
        Name = name;
    }

    public string Name { get; set; }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {Name}!";
}
```

```
using Microsoft.JSInterop;

public class HelloHelper
{
    public HelloHelper(string name)
    {
        Name = name;
    }

    public string Name { get; set; }

    [JSInvokable]
    public string GetHelloMessage() => $"Hello, {Name}!";
}
```

The `CallHelloHelperGetHelloMessage` method in the following `JsInteropClasses3` class invokes the JS function `sayHello1` with a new instance of `HelloHelper`.

`JsInteropClasses3.cs`:

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class JsInteropClasses3(IJSRuntime js)
{
    private readonly IJSRuntime js = js;

    public async ValueTask<string> CallHelloHelperGetHelloMessage(string? name)
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        return await js.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class JsInteropClasses3(IJSRuntime js)
{
    private readonly IJSRuntime js = js;

    public async ValueTask<string> CallHelloHelperGetHelloMessage(string? name)
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        return await js.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

```
using Microsoft.JSInterop;

public class JsInteropClasses3
{
    private readonly IJSRuntime js;

    public JsInteropClasses3(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> CallHelloHelperGetHelloMessage(string? name)
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        return await js.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

```
using Microsoft.JSInterop;

public class JsInteropClasses3
{
    private readonly IJSRuntime js;

    public JsInteropClasses3(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> CallHelloHelperGetHelloMessage(string? name)
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        return await js.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

```
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses3
{
    private readonly IJSRuntime js;

    public JsInteropClasses3(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> CallHelloHelperGetHelloMessage(string name)
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        return await js.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

```
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses3
{
    private readonly IJSRuntime js;

    public JsInteropClasses3(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> CallHelloHelperGetHelloMessage(string name)
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        return await js.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

To avoid a memory leak and allow garbage collection, the .NET object reference created by [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) is disposed when the object reference goes out of scope with [`using var` syntax](/en-us/dotnet/csharp/language-reference/keywords/using-statement).

When the **`Trigger .NET instance method`** button is selected in the following component, `JsInteropClasses3.CallHelloHelperGetHelloMessage` is called with the value of `name`.

`CallDotnet4.razor`:

```
@page "/call-dotnet-4"
@inject IJSRuntime JS

<PageTitle>Call .NET 4</PageTitle>

<h1>Call .NET Example 4</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private JsInteropClasses3? jsInteropClasses;

    protected override void OnInitialized() => 
        jsInteropClasses = new JsInteropClasses3(JS);

    private async Task TriggerDotNetInstanceMethod()
    {
        if (jsInteropClasses is not null)
        {
            result = await jsInteropClasses.CallHelloHelperGetHelloMessage(name);
        }
    }
}
```

`CallDotnet4.razor`:

```
@page "/call-dotnet-4"
@inject IJSRuntime JS

<PageTitle>Call .NET 4</PageTitle>

<h1>Call .NET Example 4</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private JsInteropClasses3? jsInteropClasses;

    protected override void OnInitialized() => 
        jsInteropClasses = new JsInteropClasses3(JS);

    private async Task TriggerDotNetInstanceMethod()
    {
        if (jsInteropClasses is not null)
        {
            result = await jsInteropClasses.CallHelloHelperGetHelloMessage(name);
        }
    }
}
```

`CallDotNetExample4.razor`:

```
@page "/call-dotnet-example-4"
@inject IJSRuntime JS

<h1>Call .NET Example 4</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private JsInteropClasses3? jsInteropClasses;

    protected override void OnInitialized()
    {
        jsInteropClasses = new JsInteropClasses3(JS);
    }

    private async Task TriggerDotNetInstanceMethod()
    {
        if (jsInteropClasses is not null)
        {
            result = await jsInteropClasses.CallHelloHelperGetHelloMessage(name);
        }
    }
}
```

`CallDotNetExample4.razor`:

```
@page "/call-dotnet-example-4"
@inject IJSRuntime JS

<h1>Call .NET Example 4</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;
    private JsInteropClasses3? jsInteropClasses;

    protected override void OnInitialized()
    {
        jsInteropClasses = new JsInteropClasses3(JS);
    }

    private async Task TriggerDotNetInstanceMethod()
    {
        if (jsInteropClasses is not null)
        {
            result = await jsInteropClasses.CallHelloHelperGetHelloMessage(name);
        }
    }
}
```

`CallDotNetExample4.razor`:

```
@page "/call-dotnet-example-4"
@inject IJSRuntime JS

<h1>Call .NET Example 4</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;
    private JsInteropClasses3 jsInteropClasses;

    protected override void OnInitialized()
    {
        jsInteropClasses = new JsInteropClasses3(JS);
    }

    private async Task TriggerDotNetInstanceMethod()
    {
        result = await jsInteropClasses.CallHelloHelperGetHelloMessage(name);
    }
}
```

`CallDotNetExample4.razor`:

```
@page "/call-dotnet-example-4"
@inject IJSRuntime JS

<h1>Call .NET Example 4</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;
    private JsInteropClasses3 jsInteropClasses;

    protected override void OnInitialized()
    {
        jsInteropClasses = new JsInteropClasses3(JS);
    }

    private async Task TriggerDotNetInstanceMethod()
    {
        result = await jsInteropClasses.CallHelloHelperGetHelloMessage(name);
    }
}
```

The following image shows the rendered component with the name `Amy Pond` in the `Name` field. After the button is selected, `Hello, Amy Pond!` is displayed in the UI:

The preceding pattern shown in the `JsInteropClasses3` class can also be implemented entirely in a component.

`CallDotnet5.razor`:

```
@page "/call-dotnet-5"
@inject IJSRuntime JS

<PageTitle>Call .NET 5</PageTitle>

<h1>Call .NET Example 5</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;

    public async Task TriggerDotNetInstanceMethod()
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

`CallDotnet5.razor`:

```
@page "/call-dotnet-5"
@inject IJSRuntime JS

<PageTitle>Call .NET 5</PageTitle>

<h1>Call .NET Example 5</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;

    public async Task TriggerDotNetInstanceMethod()
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

`CallDotNetExample5.razor`:

```
@page "/call-dotnet-example-5"
@inject IJSRuntime JS

<h1>Call .NET Example 5</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;

    public async Task TriggerDotNetInstanceMethod()
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

`CallDotNetExample5.razor`:

```
@page "/call-dotnet-example-5"
@inject IJSRuntime JS

<h1>Call .NET Example 5</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string? name;
    private string? result;

    public async Task TriggerDotNetInstanceMethod()
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

`CallDotNetExample5.razor`:

```
@page "/call-dotnet-example-5"
@inject IJSRuntime JS

<h1>Call .NET Example 5</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;

    public async Task TriggerDotNetInstanceMethod()
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

`CallDotNetExample5.razor`:

```
@page "/call-dotnet-example-5"
@inject IJSRuntime JS

<h1>Call .NET Example 5</h1>

<p>
    <label>
        Name: <input @bind="name" />
    </label>
</p>

<p>
    <button @onclick="TriggerDotNetInstanceMethod">
        Trigger .NET instance method
    </button>
</p>

<p>
    @result
</p>

@code {
    private string name;
    private string result;

    public async Task TriggerDotNetInstanceMethod()
    {
        using var objRef = DotNetObjectReference.Create(new HelloHelper(name));
        result = await JS.InvokeAsync<string>("sayHello1", objRef);
    }
}
```

To avoid a memory leak and allow garbage collection, the .NET object reference created by [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) is disposed when the object reference goes out of scope with [`using var` syntax](/en-us/dotnet/csharp/language-reference/keywords/using-statement).

The output displayed by the component is `Hello, Amy Pond!` when the name `Amy Pond` is provided in the `name` field.

In the preceding component, the .NET object reference is disposed. If a class or component doesn't dispose the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference), dispose it from the client by calling `dispose` on the passed [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference):

```
window.{JS FUNCTION NAME} = (dotNetHelper) => {
  dotNetHelper.invokeMethodAsync('{.NET METHOD ID}');
  dotNetHelper.dispose();
}
```

In the preceding example:

* The `{JS FUNCTION NAME}` placeholder is the JS function's name.
* The variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.
* The `{.NET METHOD ID}` placeholder is the .NET method identifier.

## Component instance .NET method helper class

A helper class can invoke a .NET instance method as an [Action](/en-us/dotnet/api/system.action). Helper classes are useful in scenarios where using static .NET methods aren't applicable:

* When several components of the same type are rendered on the same page.
* In server-side apps with multiple users concurrently using the same component.

In the following example:

* The component contains several `ListItem1` components.
* Each `ListItem1` component is composed of a message and a button.
* When a `ListItem1` component button is selected, that `ListItem1`'s `UpdateMessage` method changes the list item text and hides the button.

The following `MessageUpdateInvokeHelper` class maintains a JS-invokable .NET method, `UpdateMessageCaller`, to invoke the [Action](/en-us/dotnet/api/system.action) specified when the class is instantiated.

`MessageUpdateInvokeHelper.cs`:

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class MessageUpdateInvokeHelper(Action action)
{
    private readonly Action action = action;

    [JSInvokable]
    public void UpdateMessageCaller() => action.Invoke();
}
```

```
using Microsoft.JSInterop;

namespace BlazorSample;

public class MessageUpdateInvokeHelper(Action action)
{
    private readonly Action action = action;

    [JSInvokable]
    public void UpdateMessageCaller() => action.Invoke();
}
```

```
using Microsoft.JSInterop;

public class MessageUpdateInvokeHelper
{
    private Action action;

    public MessageUpdateInvokeHelper(Action action)
    {
        this.action = action;
    }

    [JSInvokable]
    public void UpdateMessageCaller()
    {
        action.Invoke();
    }
}
```

```
using Microsoft.JSInterop;

public class MessageUpdateInvokeHelper
{
    private Action action;

    public MessageUpdateInvokeHelper(Action action)
    {
        this.action = action;
    }

    [JSInvokable]
    public void UpdateMessageCaller()
    {
        action.Invoke();
    }
}
```

```
using System;
using Microsoft.JSInterop;

public class MessageUpdateInvokeHelper
{
    private Action action;

    public MessageUpdateInvokeHelper(Action action)
    {
        this.action = action;
    }

    [JSInvokable]
    public void UpdateMessageCaller()
    {
        action.Invoke();
    }
}
```

```
using System;
using Microsoft.JSInterop;

public class MessageUpdateInvokeHelper
{
    private Action action;

    public MessageUpdateInvokeHelper(Action action)
    {
        this.action = action;
    }

    [JSInvokable]
    public void UpdateMessageCaller()
    {
        action.Invoke();
    }
}
```

The following `updateMessageCaller` JS function invokes the `UpdateMessageCaller` .NET method.

```
<script>
  window.updateMessageCaller = (dotNetHelper) => {
    dotNetHelper.invokeMethodAsync('UpdateMessageCaller');
    dotNetHelper.dispose();
  }
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

In the preceding example, the variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.

The following `ListItem1` component is a shared component that can be used any number of times in a parent component and creates list items (`<li>...</li>`) for an HTML list (`<ul>...</ul>` or `<ol>...</ol>`). Each `ListItem1` component instance establishes an instance of `MessageUpdateInvokeHelper` with an [Action](/en-us/dotnet/api/system.action) set to its `UpdateMessage` method.

When a `ListItem1` component's **`InteropCall`** button is selected, `updateMessageCaller` is invoked with a created [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) for the `MessageUpdateInvokeHelper` instance. This permits the framework to call `UpdateMessageCaller` on that `ListItem1`'s `MessageUpdateInvokeHelper` instance. The passed [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) is disposed in JS (`dotNetHelper.dispose()`).

`ListItem1.razor`:

```
@inject IJSRuntime JS

<li>
    @message
    <button @onclick="InteropCall" style="display:@display">InteropCall</button>
</li>

@code {
    private string message = "Select one of these list item buttons.";
    private string display = "inline-block";
    private MessageUpdateInvokeHelper? messageUpdateInvokeHelper;

    protected override void OnInitialized()
    {
        messageUpdateInvokeHelper = new MessageUpdateInvokeHelper(UpdateMessage);
    }

    protected async Task InteropCall()
    {
        if (messageUpdateInvokeHelper is not null)
        {
            await JS.InvokeVoidAsync("updateMessageCaller",
                DotNetObjectReference.Create(messageUpdateInvokeHelper));
        }
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }
}
```

```
@inject IJSRuntime JS

<li>
    @message
    <button @onclick="InteropCall" style="display:@display">InteropCall</button>
</li>

@code {
    private string message = "Select one of these list item buttons.";
    private string display = "inline-block";
    private MessageUpdateInvokeHelper? messageUpdateInvokeHelper;

    protected override void OnInitialized()
    {
        messageUpdateInvokeHelper = new MessageUpdateInvokeHelper(UpdateMessage);
    }

    protected async Task InteropCall()
    {
        if (messageUpdateInvokeHelper is not null)
        {
            await JS.InvokeVoidAsync("updateMessageCaller",
                DotNetObjectReference.Create(messageUpdateInvokeHelper));
        }
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }
}
```

```
@inject IJSRuntime JS

<li>
    @message
    <button @onclick="InteropCall" style="display:@display">InteropCall</button>
</li>

@code {
    private string message = "Select one of these list item buttons.";
    private string display = "inline-block";
    private MessageUpdateInvokeHelper? messageUpdateInvokeHelper;

    protected override void OnInitialized()
    {
        messageUpdateInvokeHelper = new MessageUpdateInvokeHelper(UpdateMessage);
    }

    protected async Task InteropCall()
    {
        if (messageUpdateInvokeHelper is not null)
        {
            await JS.InvokeVoidAsync("updateMessageCaller",
                DotNetObjectReference.Create(messageUpdateInvokeHelper));
        }
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }
}
```

```
@inject IJSRuntime JS

<li>
    @message
    <button @onclick="InteropCall" style="display:@display">InteropCall</button>
</li>

@code {
    private string message = "Select one of these list item buttons.";
    private string display = "inline-block";
    private MessageUpdateInvokeHelper? messageUpdateInvokeHelper;

    protected override void OnInitialized()
    {
        messageUpdateInvokeHelper = new MessageUpdateInvokeHelper(UpdateMessage);
    }

    protected async Task InteropCall()
    {
        if (messageUpdateInvokeHelper is not null)
        {
            await JS.InvokeVoidAsync("updateMessageCaller",
                DotNetObjectReference.Create(messageUpdateInvokeHelper));
        }
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }
}
```

```
@inject IJSRuntime JS

<li>
    @message
    <button @onclick="InteropCall" style="display:@display">InteropCall</button>
</li>

@code {
    private string message = "Select one of these list item buttons.";
    private string display = "inline-block";
    private MessageUpdateInvokeHelper messageUpdateInvokeHelper;

    protected override void OnInitialized()
    {
        messageUpdateInvokeHelper = new MessageUpdateInvokeHelper(UpdateMessage);
    }

    protected async Task InteropCall()
    {
        await JS.InvokeVoidAsync("updateMessageCaller",
            DotNetObjectReference.Create(messageUpdateInvokeHelper));
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }
}
```

```
@inject IJSRuntime JS

<li>
    @message
    <button @onclick="InteropCall" style="display:@display">InteropCall</button>
</li>

@code {
    private string message = "Select one of these list item buttons.";
    private string display = "inline-block";
    private MessageUpdateInvokeHelper messageUpdateInvokeHelper;

    protected override void OnInitialized()
    {
        messageUpdateInvokeHelper = new MessageUpdateInvokeHelper(UpdateMessage);
    }

    protected async Task InteropCall()
    {
        await JS.InvokeVoidAsync("updateMessageCaller",
            DotNetObjectReference.Create(messageUpdateInvokeHelper));
    }

    private void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }
}
```

[`StateHasChanged`](../components/lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged) is called to update the UI when `message` is set in `UpdateMessage`. If `StateHasChanged` isn't called, Blazor has no way of knowing that the UI should be updated when the [Action](/en-us/dotnet/api/system.action) is invoked.

The following parent component includes four list items, each an instance of the `ListItem1` component.

`CallDotnet6.razor`:

```
@page "/call-dotnet-6"

<PageTitle>Call .NET 6</PageTitle>

<h1>Call .NET Example 6</h1>

<ul>
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
</ul>
```

`CallDotnet6.razor`:

```
@page "/call-dotnet-6"

<PageTitle>Call .NET 6</PageTitle>

<h1>Call .NET Example 6</h1>

<ul>
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
</ul>
```

`CallDotNetExample6.razor`:

```
@page "/call-dotnet-example-6"

<h1>Call .NET Example 6</h1>

<ul>
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
</ul>
```

`CallDotNetExample6.razor`:

```
@page "/call-dotnet-example-6"

<h1>Call .NET Example 6</h1>

<ul>
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
</ul>
```

`CallDotNetExample6.razor`:

```
@page "/call-dotnet-example-6"

<h1>Call .NET Example 6</h1>

<ul>
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
</ul>
```

`CallDotNetExample6.razor`:

```
@page "/call-dotnet-example-6"

<h1>Call .NET Example 6</h1>

<ul>
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
    <ListItem1 />
</ul>
```

The following image shows the rendered parent component after the second **`InteropCall`** button is selected:

* The second `ListItem1` component has displayed the `UpdateMessage Called!` message.
* The **`InteropCall`** button for the second `ListItem1` component isn't visible because the button's CSS `display` property is set to `none`.

## Component instance .NET method called from `DotNetObjectReference` assigned to an element property

The assignment of a [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) to a property of an HTML element permits calling .NET methods on a component instance:

* An [element reference](call-javascript-from-dotnet?view=aspnetcore-10.0#capture-references-to-elements) is captured ([ElementReference](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreference)).
* In the component's [`OnAfterRender{Async}` method](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync), a JavaScript (JS) function is invoked with the element reference and the component instance as a [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference). The JS function attaches the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) to the element in a property.
* When an element event is invoked in JS (for example, `onclick`), the element's attached [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) is used to call a .NET method.

Similar to the approach described in the [Component instance .NET method helper class](#component-instance-net-method-helper-class) section, this approach is useful in scenarios where using static .NET methods aren't applicable:

* When several components of the same type are rendered on the same page.
* In server-side apps with multiple users concurrently using the same component.
* The .NET method is invoked from a JS event (for example, `onclick`), not from a Blazor event (for example, `@onclick`).

In the following example:

* The component contains several `ListItem2` components, which is a shared component.
* Each `ListItem2` component is composed of a list item message `<span>` and a second `<span>` with a `display` CSS property set to `inline-block` for display.
* When a `ListItem2` component list item is selected, that `ListItem2`'s `UpdateMessage` method changes the list item text in the first `<span>` and hides the second `<span>` by setting its `display` property to `none`.

The following `assignDotNetHelper` JS function assigns the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) to an element in a property named `dotNetHelper`. The following `interopCall` JS function uses the [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) for the passed element to invoke a .NET method named `UpdateMessage`.

`ListItem2.razor.js`:

```
export function assignDotNetHelper(element, dotNetHelper) {
  element.dotNetHelper = dotNetHelper;
}

export async function interopCall(element) {
  await element.dotNetHelper.invokeMethodAsync('UpdateMessage');
}
```

`ListItem2.razor.js`:

```
export function assignDotNetHelper(element, dotNetHelper) {
  element.dotNetHelper = dotNetHelper;
}

export async function interopCall(element) {
  await element.dotNetHelper.invokeMethodAsync('UpdateMessage');
}
```

```
<script>
  window.assignDotNetHelper = (element, dotNetHelper) => {
    element.dotNetHelper = dotNetHelper;
  }

  window.interopCall = async (element) => {
    await element.dotNetHelper.invokeMethodAsync('UpdateMessage');
  }
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

In the preceding example, the variable name `dotNetHelper` is arbitrary and can be changed to any preferred name.

The following `ListItem2` component is a shared component that can be used any number of times in a parent component and creates list items (`<li>...</li>`) for an HTML list (`<ul>...</ul>` or `<ol>...</ol>`).

Each `ListItem2` component instance invokes the `assignDotNetHelper` JS function in [`OnAfterRenderAsync`](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync) with an element reference (the first `<span>` element of the list item) and the component instance as a [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference).

When a `ListItem2` component's message `<span>` is selected, `interopCall` is invoked passing the `<span>` element as a parameter (`this`), which invokes the `UpdateMessage` .NET method. In `UpdateMessage`, [`StateHasChanged`](../components/lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged) is called to update the UI when `message` is set and the `display` property of the second `<span>` is updated. If `StateHasChanged` isn't called, Blazor has no way of knowing that the UI should be updated when the method is invoked.

The [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) is disposed when the component is disposed.

`ListItem2.razor`:

```
@inject IJSRuntime JS
@implements IAsyncDisposable

<li>
    <span style="font-weight:bold;color:@color" @ref="elementRef"
        @onclick="CallJSToInvokeDotnet">
        @message
    </span>
    <span style="display:@display">
        Not Updated Yet!
    </span>
</li>

@code {
    private IJSObjectReference? module;
    private DotNetObjectReference<ListItem2>? objRef;
    private ElementReference elementRef;
    private string display = "inline-block";
    private string message = "Select one of these list items.";
    private string color = "initial";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./Components/ListItem2.razor.js");

            objRef = DotNetObjectReference.Create(this);
            await module.InvokeVoidAsync("assignDotNetHelper", elementRef, objRef);
        }
    }

    public async Task CallJSToInvokeDotnet()
    {
        if (module is not null)
        {
            await module.InvokeVoidAsync("interopCall", elementRef);
        }
    }

    [JSInvokable]
    public void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        color = "MediumSeaGreen";
        StateHasChanged();
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }

        objRef?.Dispose();
    }
}
```

```
@inject IJSRuntime JS
@implements IAsyncDisposable

<li>
    <span style="font-weight:bold;color:@color" @ref="elementRef"
        @onclick="CallJSToInvokeDotnet">
        @message
    </span>
    <span style="display:@display">
        Not Updated Yet!
    </span>
</li>

@code {
    private IJSObjectReference? module;
    private DotNetObjectReference<ListItem2>? objRef;
    private ElementReference elementRef;
    private string display = "inline-block";
    private string message = "Select one of these list items.";
    private string color = "initial";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./Components/ListItem2.razor.js");

            objRef = DotNetObjectReference.Create(this);
            await module.InvokeVoidAsync("assignDotNetHelper", elementRef, objRef);
        }
    }

    public async Task CallJSToInvokeDotnet()
    {
        if (module is not null)
        {
            await module.InvokeVoidAsync("interopCall", elementRef);
        }
    }

    [JSInvokable]
    public void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        color = "MediumSeaGreen";
        StateHasChanged();
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }

        objRef?.Dispose();
    }
}
```

```
@inject IJSRuntime JS

<li>
    <span @ref="elementRef" onclick="interopCall(this)">@message</span>
    <span style="display:@display">Not Updated Yet!</span>
</li>

@code {
    private DotNetObjectReference<ListItem2>? objRef;
    private ElementReference elementRef;
    private string display = "inline-block";
    private string message = "Select one of these list items.";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            objRef = DotNetObjectReference.Create(this);
            await JS.InvokeVoidAsync("assignDotNetHelper", elementRef, objRef);
        }
    }

    [JSInvokable]
    public void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }

    public void Dispose() => objRef?.Dispose();
}
```

```
@inject IJSRuntime JS

<li>
    <span @ref="elementRef" onclick="interopCall(this)">@message</span>
    <span style="display:@display">Not Updated Yet!</span>
</li>

@code {
    private DotNetObjectReference<ListItem2>? objRef;
    private ElementReference elementRef;
    private string display = "inline-block";
    private string message = "Select one of these list items.";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            objRef = DotNetObjectReference.Create(this);
            await JS.InvokeVoidAsync("assignDotNetHelper", elementRef, objRef);
        }
    }

    [JSInvokable]
    public void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }

    public void Dispose() => objRef?.Dispose();
}
```

```
@inject IJSRuntime JS

<li>
    <span @ref="elementRef" onclick="interopCall(this)">@message</span>
    <span style="display:@display">Not Updated Yet!</span>
</li>

@code {
    private DotNetObjectReference<ListItem2> objRef;
    private ElementReference elementRef;
    private string display = "inline-block";
    private string message = "Select one of these list items.";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            objRef = DotNetObjectReference.Create(this);
            await JS.InvokeVoidAsync("assignDotNetHelper", elementRef, objRef);
        }
    }

    [JSInvokable]
    public void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }

    public void Dispose() => objRef?.Dispose();
}
```

```
@inject IJSRuntime JS

<li>
    <span @ref="elementRef" onclick="interopCall(this)">@message</span>
    <span style="display:@display">Not Updated Yet!</span>
</li>

@code {
    private DotNetObjectReference<ListItem2> objRef;
    private ElementReference elementRef;
    private string display = "inline-block";
    private string message = "Select one of these list items.";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            objRef = DotNetObjectReference.Create(this);
            await JS.InvokeVoidAsync("assignDotNetHelper", elementRef, objRef);
        }
    }

    [JSInvokable]
    public void UpdateMessage()
    {
        message = "UpdateMessage Called!";
        display = "none";
        StateHasChanged();
    }

    public void Dispose() => objRef?.Dispose();
}
```

The following parent component includes four list items, each an instance of the `ListItem2` component.

`CallDotnet7.razor`:

```
@page "/call-dotnet-7"

<PageTitle>Call .NET 7</PageTitle>

<h1>Call .NET Example 7</h1>

<ul>
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
</ul>
```

`CallDotnet7.razor`:

```
@page "/call-dotnet-7"

<PageTitle>Call .NET 7</PageTitle>

<h1>Call .NET Example 7</h1>

<ul>
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
</ul>
```

`CallDotNetExample7.razor`:

```
@page "/call-dotnet-example-7"

<h1>Call .NET Example 7</h1>

<ul>
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
</ul>
```

`CallDotNetExample7.razor`:

```
@page "/call-dotnet-example-7"

<h1>Call .NET Example 7</h1>

<ul>
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
</ul>
```

`CallDotNetExample7.razor`:

```
@page "/call-dotnet-example-7"

<h1>Call .NET Example 7</h1>

<ul>
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
</ul>
```

`CallDotNetExample7.razor`:

```
@page "/call-dotnet-example-7"

<h1>Call .NET Example 7</h1>

<ul>
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
    <ListItem2 />
</ul>
```

## Synchronous JS interop in client-side components

- [Inject a script before or after Blazor starts](location-of-javascript?view=aspnetcore-10.0#inject-a-script-before-or-after-blazor-starts)

Using JS modules to load JS is described in this article in the [JavaScript isolation in JavaScript modules](#javascript-isolation-in-javascript-modules) section.

For more information, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules).

[Dynamic import with the `import()` operator](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/import) is supported with ASP.NET Core and Blazor:

```
if ({CONDITION}) import("/additionalModule.js");
```

In the preceding example, the `{CONDITION}` placeholder represents a conditional check to determine if the module should be loaded.

For browser compatibility, see [Can I use: JavaScript modules: dynamic import](https://caniuse.com/es6-module-dynamic-import).

## Avoid circular object references

Objects that contain circular references can't be serialized on the client for either:

* .NET method calls.
* JavaScript method calls from C# when the return type has circular references.

## Byte array support

Blazor supports optimized byte array JavaScript (JS) interop that avoids encoding/decoding byte arrays into Base64. The following example uses JS interop to pass a byte array to .NET.

Provide a `sendByteArray` JS function. The function is called statically, which includes the assembly name parameter in the `invokeMethodAsync` call, by a button in the component and doesn't return a value:

`CallDotnet8.razor.js`:

```
export function sendByteArray() {
  const data = new Uint8Array([0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69,
    0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, 0x2c,
    0x20, 0x43, 0x61, 0x70, 0x74, 0x61, 0x69, 0x6e, 0x2e, 0x20, 0x4e,
    0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e]);
  DotNet.invokeMethodAsync('BlazorSample', 'ReceiveByteArray', data)
    .then(str => {
      alert(str);
    });
}

export function addHandlers() {
  const btn = document.getElementById("btn");
  btn.addEventListener("click", sendByteArray);
}
```

```
<script>
  window.sendByteArray = () => {
    const data = new Uint8Array([0x45,0x76,0x65,0x72,0x79,0x74,0x68,0x69,
      0x6e,0x67,0x27,0x73,0x20,0x73,0x68,0x69,0x6e,0x79,0x2c,
      0x20,0x43,0x61,0x70,0x74,0x61,0x69,0x6e,0x2e,0x20,0x4e,
      0x6f,0x74,0x20,0x74,0x6f,0x20,0x66,0x72,0x65,0x74,0x2e]);
    DotNet.invokeMethodAsync('BlazorSample', 'ReceiveByteArray', data)
      .then(str => {
        alert(str);
      });
  };
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0).

`CallDotnet8.razor`:

```
@page "/call-dotnet-8"
@using System.Text
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET 8</PageTitle>

<h1>Call .NET Example 8</h1>

<p>
    <button id="btn">Send Bytes</button>
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./Components/Pages/CallDotnet8.razor.js");

            await module.InvokeVoidAsync("addHandlers");
        }
    }

    [JSInvokable]
    public static Task<string> ReceiveByteArray(byte[] receivedBytes) => 
        Task.FromResult(Encoding.UTF8.GetString(receivedBytes, 0, 
            receivedBytes.Length));

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

`CallDotnet8.razor`:

```
@page "/call-dotnet-8"
@using System.Text
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call .NET 8</PageTitle>

<h1>Call .NET Example 8</h1>

<p>
    <button id="btn">Send Bytes</button>
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./Components/Pages/CallDotnet8.razor.js");

            await module.InvokeVoidAsync("addHandlers");
        }
    }

    [JSInvokable]
    public static Task<string> ReceiveByteArray(byte[] receivedBytes) => 
        Task.FromResult(Encoding.UTF8.GetString(receivedBytes, 0, 
            receivedBytes.Length));

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

`CallDotNetExample8.razor`:

```
@page "/call-dotnet-example-8"
@using System.Text

<PageTitle>Call .NET 8</PageTitle>

<h1>Call .NET Example 8</h1>

<p>
    <button onclick="sendByteArray()">Send Bytes</button>
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    [JSInvokable]
    public static Task<string> ReceiveByteArray(byte[] receivedBytes)
    {
        return Task.FromResult(
            Encoding.UTF8.GetString(receivedBytes, 0, receivedBytes.Length));
    }
}
```

`CallDotNetExample8.razor`:

```
@page "/call-dotnet-example-8"
@using System.Text

<h1>Call .NET Example 8</h1>

<p>
    <button onclick="sendByteArray()">Send Bytes</button>
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    [JSInvokable]
    public static Task<string> ReceiveByteArray(byte[] receivedBytes)
    {
        return Task.FromResult(
            Encoding.UTF8.GetString(receivedBytes, 0, receivedBytes.Length));
    }
}
```

For information on using a byte array when calling JavaScript from .NET, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0#byte-array-support).

## Stream from JavaScript to .NET

Blazor supports streaming data directly from JavaScript to .NET. Streams are requested using the `Microsoft.JSInterop.IJSStreamReference` interface.

`Microsoft.JSInterop.IJSStreamReference.OpenReadStreamAsync` returns a [Stream](/en-us/dotnet/api/system.io.stream) and uses the following parameters:

* `maxAllowedSize`: Maximum number of bytes permitted for the read operation from JavaScript, which defaults to 512,000 bytes if not specified.
* `cancellationToken`: A [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) for cancelling the read.

In JavaScript:

```
function streamToDotNet() {
  return new Uint8Array(10000000);
}
```

In C# code:

```
var dataReference = 
    await JS.InvokeAsync<IJSStreamReference>("streamToDotNet");
using var dataReferenceStream = 
    await dataReference.OpenReadStreamAsync(maxAllowedSize: 10_000_000);

var outputPath = Path.Combine(Path.GetTempPath(), "file.txt");
using var outputFileStream = File.OpenWrite(outputPath);
await dataReferenceStream.CopyToAsync(outputFileStream);
```

In the preceding example:

* `JS` is an injected [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) instance. [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) is registered by the Blazor framework.
* The `dataReferenceStream` is written to disk (`file.txt`) at the current user's temporary folder path ([GetTempPath](/en-us/dotnet/api/system.io.path.gettemppath)).

[Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0#stream-from-net-to-javascript) covers the reverse operation, streaming from .NET to JavaScript using a [DotNetStreamReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetstreamreference).

[ASP.NET Core Blazor file uploads](../file-uploads?view=aspnetcore-10.0) covers how to upload a file in Blazor. For a forms example that streams `<textarea>` data in a server-side component, see [Troubleshoot ASP.NET Core Blazor forms](../forms/troubleshoot?view=aspnetcore-10.0#large-form-payloads-and-the-signalr-message-size-limit).

## JavaScript `[JSImport]`/`[JSExport]` interop

*This section applies to client-side components.*

As an alternative to interacting with JavaScript (JS) in client-side components using Blazor's JS interop mechanism based on the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) interface, a JS `[JSImport]`/`[JSExport]` interop API is available to apps targeting .NET 7 or later.

For more information, see [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](import-export-interop?view=aspnetcore-10.0).

## Disposal of JavaScript interop object references

Examples throughout the JavaScript (JS) interop articles demonstrate typical object disposal patterns:

* When calling .NET from JS, as described in this article, dispose of a created [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) either from .NET or from JS to avoid leaking .NET memory.

* When calling JS from .NET, as described in [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0), dispose any created [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference)/[IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference)/`JSObjectReference` either from .NET or from JS to avoid leaking JS memory.

* [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0)
* [`InteropComponent.razor` example (`dotnet/AspNetCore` GitHub repository `main` branch)](https://github.com/dotnet/AspNetCore/blob/main/src/Components/test/testassets/BasicTestApp/InteropComponent.razor): The `main` branch represents the product unit's current development for the next release of ASP.NET Core. To select the branch for a different release (for example, `release/{VERSION}`, where the `{VERSION}` placeholder is the release version), use the **Switch branches or tags** dropdown list to select the branch. For a branch that no longer exists, use the **Tags** tab to find the API (for example, `v7.0.0`).
* [Interaction with the DOM](./?view=aspnetcore-10.0#interaction-with-the-dom)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))
* [Handle errors in ASP.NET Core Blazor apps](../fundamentals/handle-errors?view=aspnetcore-10.0#javascript-interop) (*JavaScript interop* section)
* [Threat mitigation: .NET methods invoked from the browser](../security/interactive-server-side-rendering?view=aspnetcore-10.0#net-methods-invoked-from-the-browser)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/file-downloads
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/file-downloads.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor file downloads

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to download files in Blazor apps.

## File downloads

This article covers approaches for the following scenarios, where a file shouldn't be opened by a browser but downloaded and saved on the client:

* [Stream file content to a raw binary data buffer on the client](#download-from-a-stream): Typically, this approach is used for relatively small files (< 250 MB).
* [Download a file via a URL without streaming](#download-from-a-url): Usually, this approach is used for relatively large files (> 250 MB).

When downloading files from a different origin than the app, Cross-Origin Resource Sharing (CORS) considerations apply. For more information, see the [Cross-Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) section.

## Security considerations

Use caution when providing users with the ability to download files from a server. Cyberattackers may execute [Denial of Service (DoS)](/en-us/windows-hardware/drivers/ifs/denial-of-service) attacks, [API exploitation attacks](https://developer.mozilla.org/docs/Web/HTML/Element/a#security_and_privacy), or attempt to compromise networks and servers in other ways.

Security steps that reduce the likelihood of a successful attack are:

* Download files from a dedicated file download area on the server, preferably from a non-system drive. Using a dedicated location makes it easier to impose security restrictions on downloadable files. Disable execute permissions on the file download area.
* Client-side security checks are easy to circumvent by malicious users. Always perform client-side security checks on the server, too.
* Don't receive files from users or other untrusted sources and then make the files available for immediate download without performing security checks on the files. For more information, see [Upload files in ASP.NET Core](../mvc/models/file-uploads?view=aspnetcore-10.0#security-considerations).

## Download from a stream

*This section applies to files that are typically up to 250 MB in size.*

The recommended approach for downloading relatively small files (< 250 MB) is to stream file content to a raw binary data buffer on the client with [JavaScript (JS) interop](javascript-interoperability/?view=aspnetcore-10.0). This approach is effective for components that adopt an interactive render mode but not components that adopt static server-side rendering (static SSR).

The recommended approach for downloading relatively small files (< 250 MB) is to stream file content to a raw binary data buffer on the client with [JavaScript (JS) interop](javascript-interoperability/?view=aspnetcore-10.0).

Warning

The approach in this section reads the file's content into a [JS `ArrayBuffer`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). This approach loads the entire file into the client's memory, which can impair performance. To download relatively large files (>= 250 MB), we recommend following the guidance in the [Download from a URL](#download-from-a-url) section.

The following `downloadFileFromStream` JS function:

* Reads the provided stream into an [`ArrayBuffer`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer).
* Creates a [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob) to wrap the `ArrayBuffer`.
* Creates an object URL to serve as the file's download address.
* Creates an [`HTMLAnchorElement`](https://developer.mozilla.org/docs/Web/API/HTMLAnchorElement) (`<a>` element).
* Assigns the file's name (`fileName`) and URL (`url`) for the download.
* Triggers the download by firing a [`click` event](https://developer.mozilla.org/docs/Web/API/HTMLElement/click) on the anchor element.
* Removes the anchor element.
* Revokes the object URL (`url`) by calling [`URL.revokeObjectURL`](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL). **This is an important step to ensure memory isn't leaked on the client.**

```
<script>
  window.downloadFileFromStream = async (fileName, contentStreamReference) => {
    const arrayBuffer = await contentStreamReference.arrayBuffer();
    const blob = new Blob([arrayBuffer]);
    const url = URL.createObjectURL(blob);
    const anchorElement = document.createElement('a');
    anchorElement.href = url;
    anchorElement.download = fileName ?? '';
    anchorElement.click();
    anchorElement.remove();
    URL.revokeObjectURL(url);
  }
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](javascript-interoperability/location-of-javascript?view=aspnetcore-10.0).

The following component:

* Uses native byte-streaming interop to ensure efficient transfer of the file to the client.

* Has a method named `GetFileStream` to retrieve a [Stream](/en-us/dotnet/api/system.io.stream) for the file that's downloaded to clients. Alternative approaches include retrieving a file from storage or generating a file dynamically in C# code. For this demonstration, the app creates a 50 KB file of random data from a new byte array (`new byte[]`). The bytes are wrapped with a [MemoryStream](/en-us/dotnet/api/system.io.memorystream) to serve as the example's dynamically-generated binary file.

* The `DownloadFileFromStream` method:

  * Retrieves the [Stream](/en-us/dotnet/api/system.io.stream) from `GetFileStream`.
  * Specifies a file name when file is saved on the user's machine. The following example names the file `quote.txt`.
  * Wraps the [Stream](/en-us/dotnet/api/system.io.stream) in a [DotNetStreamReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetstreamreference), which allows streaming the file data to the client.
  * Invokes the `downloadFileFromStream` JS function to accept the data on the client.

`FileDownload1.razor`:

```
@page "/file-download-1"
@using System.IO
@inject IJSRuntime JS

<PageTitle>File Download 1</PageTitle>

<h1>File Download Example 1</h1>

<button @onclick="DownloadFileFromStream">
    Download File From Stream
</button>

@code {
    private Stream GetFileStream()
    {
        var randomBinaryData = new byte[50 * 1024];
        var fileStream = new MemoryStream(randomBinaryData);

        return fileStream;
    }

    private async Task DownloadFileFromStream()
    {
        var fileStream = GetFileStream();
        var fileName = "log.bin";

        using var streamRef = new DotNetStreamReference(stream: fileStream);

        await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
    }
}
```

```
@page "/file-download-1"
@using System.IO
@inject IJSRuntime JS

<PageTitle>File Download 1</PageTitle>

<h1>File Download Example 1</h1>

<button @onclick="DownloadFileFromStream">
    Download File From Stream
</button>

@code {
    private Stream GetFileStream()
    {
        var randomBinaryData = new byte[50 * 1024];
        var fileStream = new MemoryStream(randomBinaryData);

        return fileStream;
    }

    private async Task DownloadFileFromStream()
    {
        var fileStream = GetFileStream();
        var fileName = "log.bin";

        using var streamRef = new DotNetStreamReference(stream: fileStream);

        await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
    }
}
```

```
@page "/file-download-1"
@using System.IO
@inject IJSRuntime JS

<h1>File Download Example</h1>

<button @onclick="DownloadFileFromStream">
    Download File From Stream
</button>

@code {
    private Stream GetFileStream()
    {
        var randomBinaryData = new byte[50 * 1024];
        var fileStream = new MemoryStream(randomBinaryData);

        return fileStream;
    }

    private async Task DownloadFileFromStream()
    {
        var fileStream = GetFileStream();
        var fileName = "log.bin";

        using var streamRef = new DotNetStreamReference(stream: fileStream);

        await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
    }
}
```

```
@page "/file-download-1"
@using System.IO
@inject IJSRuntime JS

<h1>File Download Example</h1>

<button @onclick="DownloadFileFromStream">
    Download File From Stream
</button>

@code {
    private Stream GetFileStream()
    {
        var randomBinaryData = new byte[50 * 1024];
        var fileStream = new MemoryStream(randomBinaryData);

        return fileStream;
    }

    private async Task DownloadFileFromStream()
    {
        var fileStream = GetFileStream();
        var fileName = "log.bin";

        using var streamRef = new DotNetStreamReference(stream: fileStream);

        await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
    }
}
```

For a component in a server-side app that must return a [Stream](/en-us/dotnet/api/system.io.stream) for a physical file, the component can call [File.OpenRead](/en-us/dotnet/api/system.io.file.openread), as the following example demonstrates:

```
private Stream GetFileStream() => File.OpenRead(@"{PATH}");
```

In the preceding example, the `{PATH}` placeholder is the path to the file. The `@` prefix indicates that the string is a [*verbatim string literal*](/en-us/dotnet/csharp/programming-guide/strings/#verbatim-string-literals), which permits the use of backslashes (`\`) in a Windows OS path and embedded double-quotes (`""`) for a single quote in the path. Alternatively, avoid the string literal (`@`) and use either of the following approaches:

* Use escaped backslashes (`\\`) and quotes (`\"`).
* Use forward slashes (`/`) in the path, which are supported across platforms in ASP.NET Core apps, and escaped quotes (`\"`).

## Download from a URL

*This section applies to files that are relatively large, typically 250 MB or larger.*

The recommended approach for downloading relatively large files (>= 250 MB) with interactively-rendered components or files of any size for statically-rendered components is to use JS to trigger an anchor element with the file's name and URL.

The recommended approach for downloading relatively large files (>= 250 MB) is to use JS to trigger an anchor element with the file's name and URL.

The example in this section uses a download file named `quote.txt`, which is placed in a folder named `files` in the app's web root (`wwwroot` folder). The use of the `files` folder is only for demonstration purposes. You can organize downloadable files in any folder layout within the web root (`wwwroot` folder) that you prefer, including serving the files directly from the `wwwroot` folder.

`wwwroot/files/quote.txt`:

```
When victory is ours, we'll wipe every trace of the Thals and their city from the face of this land. We will avenge the deaths of all Kaleds who've fallen in the cause of right and justice and build a peace which will be a monument to their sacrifice. Our battle cry will be "Total extermination of the Thals!"

- General Ravon (Guy Siner, http://guysiner.com/)
  Dr. Who: Genesis of the Daleks (https://www.bbc.co.uk/programmes/p00vd5g2)
  Copyright 1975 BBC (https://www.bbc.co.uk/)
```

```
When victory is ours, we'll wipe every trace of the Thals and their city from the face of this land. We will avenge the deaths of all Kaleds who've fallen in the cause of right and justice and build a peace which will be a monument to their sacrifice. Our battle cry will be "Total extermination of the Thals!"

- General Ravon (Guy Siner, http://guysiner.com/)
  Dr. Who: Genesis of the Daleks (https://www.bbc.co.uk/programmes/p00vd5g2)
  Copyright 1975 BBC (https://www.bbc.co.uk/)
```

```
When victory is ours, we'll wipe every trace of the Thals and their city from the face of this land. We will avenge the deaths of all Kaleds who've fallen in the cause of right and justice and build a peace which will be a monument to their sacrifice. Our battle cry will be "Total extermination of the Thals!"

- General Ravon (Guy Siner, http://guysiner.com/)
  Dr. Who: Genesis of the Daleks (https://www.bbc.co.uk/programmes/p00vd5g2)
  Copyright 1975 BBC (https://www.bbc.co.uk/)
```

```
When victory is ours, we'll wipe every trace of the Thals and their city from the face of this land. We will avenge the deaths of all Kaleds who've fallen in the cause of right and justice and build a peace which will be a monument to their sacrifice. Our battle cry will be "Total extermination of the Thals!"

- General Ravon (Guy Siner, http://guysiner.com/)
  Dr. Who: Genesis of the Daleks (https://www.bbc.co.uk/programmes/p00vd5g2)
  Copyright 1975 BBC (https://www.bbc.co.uk/)
```

The following `triggerFileDownload` JS function:

* Creates an [`HTMLAnchorElement`](https://developer.mozilla.org/docs/Web/API/HTMLAnchorElement) (`<a>` element).
* Assigns the file's name (`fileName`) and URL (`url`) for the download.
* Triggers the download by firing a [`click` event](https://developer.mozilla.org/docs/Web/API/HTMLElement/click) on the anchor element.
* Removes the anchor element.

```
<script>
  window.triggerFileDownload = (fileName, url) => {
    const anchorElement = document.createElement('a');
    anchorElement.href = url;
    anchorElement.download = fileName ?? '';
    anchorElement.click();
    anchorElement.remove();
  }
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](javascript-interoperability/location-of-javascript?view=aspnetcore-10.0).

The following example component downloads the file from the same origin that the app uses. If the file download is attempted from a different origin, configure Cross-Origin Resource Sharing (CORS). For more information, see the [Cross-Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) section.

`FileDownload2.razor`:

```
@page "/file-download-2"
@inject IJSRuntime JS

<PageTitle>File Download 2</PageTitle>

<h1>File Download Example 2</h1>

<button @onclick="DownloadFileFromURL">
    Download File From URL
</button>

@code {
    private async Task DownloadFileFromURL()
    {
        var fileName = "quote.txt";
        var fileURL = "/files/quote.txt";
        await JS.InvokeVoidAsync("triggerFileDownload", fileName, fileURL);
    }
}
```

For interactive components, the button in the preceding example calls the `DownloadFileFromURL` handler to invoke the JavaScript (JS) function `triggerFileDownload`.

If the component adopts static server-side rendering (static SSR), add an event handler for the button ([`addEventListener`](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)) to call `triggerFileDownload` following the guidance in [ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](javascript-interoperability/static-server-rendering?view=aspnetcore-10.0).

```
@page "/file-download-2"
@inject IJSRuntime JS

<PageTitle>File Download 2</PageTitle>

<h1>File Download Example 2</h1>

<button @onclick="DownloadFileFromURL">
    Download File From URL
</button>

@code {
    private async Task DownloadFileFromURL()
    {
        var fileName = "quote.txt";
        var fileURL = "/files/quote.txt";
        await JS.InvokeVoidAsync("triggerFileDownload", fileName, fileURL);
    }
}
```

For interactive components, the button in the preceding example calls the `DownloadFileFromURL` handler to invoke the JavaScript (JS) function `triggerFileDownload`.

If the component adopts static server-side rendering (static SSR), add an event handler for the button ([`addEventListener`](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)) to call `triggerFileDownload` following the guidance in [ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](javascript-interoperability/static-server-rendering?view=aspnetcore-10.0).

```
@page "/file-download-2"
@inject IJSRuntime JS

<h1>File Download Example 2</h1>

<button @onclick="DownloadFileFromURL">
    Download File From URL
</button>

@code {
    private async Task DownloadFileFromURL()
    {
        var fileName = "quote.txt";
        var fileURL = "https://localhost:5001/files/quote.txt";
        await JS.InvokeVoidAsync("triggerFileDownload", fileName, fileURL);
    }
}
```

Change the port in the preceding example to match the localhost development port of your environment.

```
@page "/file-download-2"
@inject IJSRuntime JS

<h1>File Download Example 2</h1>

<button @onclick="DownloadFileFromURL">
    Download File From URL
</button>

@code {
    private async Task DownloadFileFromURL()
    {
        var fileName = "quote.txt";
        var fileURL = "https://localhost:5001/files/quote.txt";
        await JS.InvokeVoidAsync("triggerFileDownload", fileName, fileURL);
    }
}
```

Change the port in the preceding example to match the localhost development port of your environment.

## Cross-Origin Resource Sharing (CORS)

Without taking further steps to enable [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/docs/Web/HTTP/CORS) for files that don't have the same origin as the app, downloading files won't pass CORS checks made by the browser.

For more information on CORS with ASP.NET Core apps and other Microsoft products and services that host files for download, see the following resources:

* [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0)
* [Using Azure CDN with CORS (Azure documentation)](/en-us/azure/cdn/cdn-cors)
* [Cross-Origin Resource Sharing (CORS) support for Azure Storage (REST documentation)](/en-us/rest/api/storageservices/cross-origin-resource-sharing--cors--support-for-the-azure-storage-services)
* [Core Cloud Services - Set up CORS for your website and storage assets (Learn module)](/en-us/training/modules/set-up-cors-website-storage/)
* [IIS CORS module Configuration Reference (IIS documentation)](/en-us/iis/extensions/cors-module/cors-module-configuration-reference)

## Related content

* [ASP.NET Core Blazor static files](fundamentals/static-files?view=aspnetcore-10.0)
* [ASP.NET Core Blazor JavaScript interoperability (JS interop)](javascript-interoperability/?view=aspnetcore-10.0)
* [JavaScript location in ASP.NET Core Blazor apps](javascript-interoperability/location-of-javascript?view=aspnetcore-10.0)
* [ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](javascript-interoperability/static-server-rendering?view=aspnetcore-10.0)
* [`<a>`: The Anchor element: Security and privacy (MDN documentation)](https://developer.mozilla.org/docs/Web/HTML/Element/a#security_and_privacy)
* [ASP.NET Core Blazor file uploads](file-uploads?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](fundamentals/?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/request-features
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/request-features.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Request Features in ASP.NET Core

By [Steve Smith](https://ardalis.com/)

The `HttpContext` API that applications and middleware use to process requests has an abstraction layer underneath it called *feature interfaces*. Each feature interface provides a granular subset of the functionality exposed by `HttpContext`. These interfaces can be added, modified, wrapped, replaced, or even removed by the server or middleware as the request is processed without having to re-implement the entire `HttpContext`. They can also be used to mock functionality when testing.

## Feature collections

The [Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features#microsoft-aspnetcore-http-httpcontext-features) property of `HttpContext` provides access to the collection of feature interfaces for the current request. Since the feature collection is mutable even within the context of a request, middleware can be used to modify the collection and add support for additional features. Some advanced features are only available by accessing the associated interface through the feature collection.

## Feature interfaces

ASP.NET Core defines a number of common HTTP feature interfaces in [Microsoft.AspNetCore.Http.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.features), which are shared by various servers and middleware to identify the features that they support. Servers and middleware may also provide their own interfaces with additional functionality.

Most feature interfaces provide optional, light-up functionality, and their associated `HttpContext` APIs provide defaults if the feature isn't present. A few interfaces are indicated in the following content as required because they provide core request and response functionality and must be implemented in order to process the request.

The following feature interfaces are from [Microsoft.AspNetCore.Http.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.features):

[IHttpRequestFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttprequestfeature): Defines the structure of an HTTP request, including the protocol, path, query string, headers, and body. This feature is required in order to process requests.

[IHttpResponseFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpresponsefeature): Defines the structure of an HTTP response, including the status code, headers, and body of the response. This feature is required in order to process requests.

[IHttpResponseBodyFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpresponsebodyfeature): Defines different ways of writing out the response body, using either a `Stream`, a `PipeWriter`, or a file. This feature is required in order to process requests. This replaces `IHttpResponseFeature.Body` and `IHttpSendFileFeature`.

[IHttpAuthenticationFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.authentication.ihttpauthenticationfeature): Holds the [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) currently associated with the request.

[IFormFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iformfeature): Used to parse and cache incoming HTTP and multipart form submissions.

[IHttpBodyControlFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpbodycontrolfeature): Used to control if synchronous IO operations are allowed for the request or response bodies.

`IHttpActivityFeature`: Used to add `Activity` information for diagnostic listeners.

[IHttpBufferingFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpbufferingfeature): Defines methods for disabling buffering of requests and/or responses.

[IHttpConnectionFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpconnectionfeature): Defines properties for the connection id and local and remote addresses and ports.

[IHttpMaxRequestBodySizeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature): Controls the maximum allowed request body size for the current request.

`IHttpRequestBodyDetectionFeature`: Indicates if the request can have a body.

[IHttpRequestIdentifierFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttprequestidentifierfeature): Adds a property that can be implemented to uniquely identify requests.

[IHttpRequestLifetimeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttprequestlifetimefeature): Defines support for aborting connections or detecting if a request has been terminated prematurely, such as by a client disconnect.

[IHttpRequestTrailersFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttprequesttrailersfeature): Provides access to the request trailer headers, if any.

[IHttpResetFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpresetfeature): Used to send reset messages for protocols that support them such as HTTP/2 or HTTP/3.

[IHttpResponseTrailersFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpresponsetrailersfeature): Enables the application to provide response trailer headers if supported.

[IHttpSendFileFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpsendfilefeature): Defines a method for sending files asynchronously.

[IHttpUpgradeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpupgradefeature): Defines support for [HTTP Upgrades](https://www.rfc-editor.org/rfc/rfc9110#field.upgrade), which allow the client to specify which additional protocols it would like to use if the server wishes to switch protocols.

[IHttpWebSocketFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpwebsocketfeature): Defines an API for supporting WebSockets.

[IHttpsCompressionFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpscompressionfeature): Controls if response compression should be used over HTTPS connections.

[IItemsFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iitemsfeature): Stores the [Items](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iitemsfeature.items#microsoft-aspnetcore-http-features-iitemsfeature-items) collection for per request application state.

[IQueryFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iqueryfeature): Parses and caches the query string.

[IRequestBodyPipeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.irequestbodypipefeature): Represents the request body as a [PipeReader](/en-us/dotnet/api/system.io.pipelines.pipereader).

[IRequestCookiesFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.irequestcookiesfeature): Parses and caches the request `Cookie` header values.

[IResponseCookiesFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iresponsecookiesfeature): Controls how response cookies are applied to the `Set-Cookie` header.

[IServerVariablesFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iservervariablesfeature): This feature provides access to request server variables such as those provided by IIS.

[IServiceProvidersFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iserviceprovidersfeature): Provides access to an [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) with scoped request services.

[ISessionFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.isessionfeature): Defines `ISessionFactory` and [ISession](/en-us/dotnet/api/microsoft.aspnetcore.http.isession) abstractions for supporting user sessions. `ISessionFeature` is implemented by the [SessionMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.session.sessionmiddleware) (see [Session in ASP.NET Core](app-state?view=aspnetcore-10.0)).

[ITlsConnectionFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.itlsconnectionfeature): Defines an API for retrieving client certificates.

[ITlsTokenBindingFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.itlstokenbindingfeature): Defines methods for working with TLS token binding parameters.

[ITrackingConsentFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.itrackingconsentfeature): Used to query, grant, and withdraw user consent regarding the storage of user information related to site activity and functionality.

## Related content

* [Web server implementations in ASP.NET Core](servers/?view=aspnetcore-10.0)
* [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app/part-1
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/movie-database-app/part-1.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor movie database app (Part 1 - Create a Blazor Web App)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article is the first part of the Blazor movie database app tutorial that teaches you the basics of building an ASP.NET Core Blazor Web App with features to manage a movie database.

This part of the tutorial series covers how to create a Blazor Web App that adopts static server-side rendering (static SSR). Static SSR means that content is rendered on the server and sent to the client for display in response to individual requests.

## Prerequisites

[Visual Studio (latest release)](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2022) with the **ASP.NET and web development** workload

Latest releases of:

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [.NET SDK](https://dotnet.microsoft.com/download/dotnet)

The Visual Studio Code (VS Code) instructions for ASP.NET Core development in this tutorial use the [.NET CLI](/en-us/dotnet/core/tools/), which is part of the .NET SDK. .NET CLI commands are issued in VS Code's integrated [**Terminal**](https://code.visualstudio.com/docs/editor/integrated-terminal), which defaults to a [PowerShell command shell](/en-us/powershell/). The **Terminal** is opened by selecting **New Terminal** from the **Terminal** menu in the menu bar.

[.NET SDK (latest release)](https://dotnet.microsoft.com/download/dotnet)

The [.NET CLI](/en-us/dotnet/core/tools/) is part of the .NET SDK. To issue commands that affect the project, open the command shell to the project's root folder.

## Create a Blazor Web App

In Visual Studio:

* Select **Create a new project** from the **Start Window** or select **File** > **New** > **Project** from the menu bar.

* In the **Create a new project** dialog, select **Blazor Web App** from the list of project templates. Select the **Next** button.

* In the **Configure your new project** dialog, name the project `BlazorWebAppMovies` in the **Project name** field, including matching the capitalization. Using this exact project name is important to ensure that the namespaces match for code that you copy from the tutorial into the app that you're building.

* Confirm that the **Location** for the app is suitable. Set the **Place solution and project in the same directory** checkbox to match your preferred solution file location. Select the **Next** button.

* In the **Additional information** dialog, use the following settings:

  * **Framework**: Confirm that the [latest framework](https://dotnet.microsoft.com/download/dotnet) is selected. If Visual Studio's **Framework** dropdown list doesn't include the latest available .NET framework, [update Visual Studio](/en-us/visualstudio/install/update-visual-studio) and restart the tutorial.
  * **Authentication type**: **None**
  * **Configure for HTTPS**: Selected
  * **Interactive render mode**: **Server**
  * **Interactivity location**: **Per page/component**
  * **Include sample pages**: Selected
  * **Do not use top-level statements**: Not selected
  * **Use the .dev.localhost TLD in the application URL**: Not selected
  * Select **Create**.

The Visual Studio instructions in parts of this tutorial series use EF Core commands to add database migrations and update the database. EF Core commands are issued using [Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services). More information is provided later in this tutorial series.

This tutorial assumes that you have familiarity with VS Code. If you're new to VS Code, see the [VS Code documentation](https://code.visualstudio.com/docs). The videos listed by the [Introductory Videos page](https://code.visualstudio.com/docs/getstarted/introvideos) are designed to give you an overview of VS Code's features.

Confirm that you have the latest [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) and [.NET SDK](https://dotnet.microsoft.com/download/dotnet) installed.

In VS Code:

Create a new project:

* Go to the **Explorer** view and select the **Create .NET Project** button. Alternatively, you can bring up the **Command Palette** using `Ctrl`+`Shift`+`P`, and then type "`.NET`" to find and select the **.NET: New Project** command.

* Select the **Blazor Web App** project template from the list.

* In the **Project Location** dialog, create or select a folder for the project.

* In the **Command Palette**, name the project `BlazorWebAppMovies`, including matching the capitalization. Using this exact project name is important to ensure that the namespaces match for code that you copy from the tutorial into the app that you're building.

* Select **Create project** to create the app.

Confirm that you have the latest [.NET SDK](https://dotnet.microsoft.com/download/dotnet) installed.

In a command shell:

* Use the `cd` command to change to the directory to where you want to create the project folder (for example, `cd c:/users/Bernie_Kopell/Documents`).

* Use the [`dotnet new` command](/en-us/dotnet/core/tools/dotnet-new) with the [`blazor` project template](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazor) to create a new Blazor Web App project. The [`-o|--output` option](/en-us/dotnet/core/tools/dotnet-new#options) passed to the command creates the project in a new folder at the current shell directory location. Name the project `BlazorWebAppMovies`, including matching the capitalization, so the namespaces match for code that you copy from the tutorial to the app.

  ```
  dotnet new blazor -o BlazorWebAppMovies
  ```

## Run the app

Press `F5` on the keyboard to run the app.

Visual Studio displays the following dialog when a project isn't configured to use SSL:

Select **Yes** if you trust the ASP.NET Core SSL certificate.

The following dialog is displayed:

Select **Yes** to acknowledge the risk and install the certificate.

Visual Studio:

* Compiles and runs the app.
* Launches the default browser at `https://localhost:{PORT}`, which displays the app's UI. The `{PORT}` placeholder is the random port assigned to the app when the app is created. If you need to change the port due to a local port conflict, change the port in the project's `Properties/launchSettings.json` file.

Navigate the pages of the app to confirm that the app is working normally.

In VS Code, press `F5` to run the app.

At the **Select debugger** prompt in the **Command Palette** at the top of the VS Code UI, select **C#**. At the next prompt, select the default launch configuration (`C#: BlazorWebAppMovies [Default Configuration]`).

The default browser is launched at `http://localhost:{PORT}`, which displays the app's UI. The `{PORT}` placeholder is the random port assigned to the app when the app is created. If you need to change the port due to a local port conflict, change the port in the project's `Properties/launchSettings.json` file.

Navigate the pages of the app to confirm that the app is working normally.

In a command shell opened to the project's root folder, execute the [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) command to compile and start the app:

```
dotnet watch
```

The app is compiled and run. The app is launched at `http://localhost:{PORT}`, where the `{PORT}` placeholder is the random port assigned to the app when the app is created. If you need to change the port due to a local port conflict, change the port in the project's `Properties/launchSettings.json` file.

Navigate the pages of the app to confirm that the app is working normally.

Note

When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

## Examine the project files

The following sections contain an overview of the project's folders and files.

If you're building the app, you don't need to make changes to the project files in the following sections. As you read the descriptions of the folders and files, examine them in the project.

If you're only reading the articles and not building the app, you can refer to the completed sample app in the [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples). Select the latest version folder in the repository. The sample folder for this tutorial's project is named `BlazorWebAppMovies`. The sample app is the *finished version* of the app after following all of the steps of the tutorial series. Code in the sample doesn't always match steps of the tutorial before the end of the series.

### `Properties` folder

The `Properties` folder holds development environment configuration in the `launchSettings.json` file.

### `wwwroot` folder

The `wwwroot` folder contains static assets, such as image, JavaScript (`.js`), and stylesheet (`.css`) files.

### `Components`, `Components/Pages`, and `Components/Layout` folders

These folders contain *Razor components*, often referred to as "components," and supporting files. A component is a self-contained portion of user interface (UI) with optional processing logic. Components can be nested, reused, and shared among projects.

Components are implemented using a combination of C# and HTML markup in [Razor](../../../mvc/views/razor?view=aspnetcore-10.0) component files with the `.razor` file extension.

Typically, components that are nested within other components and not directly reachable ("routable") at a URL are placed in the `Components` folder. Components that are routable via a URL are usually placed in the `Components/Pages` folder.

The `Components/Layout` folder contains the following layout components and stylesheets:

* `MainLayout` component (`MainLayout.razor`): The app's main layout component.
* `MainLayout.razor.css`: Stylesheet for the app's main layout.
* `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. This component uses several `NavLink` components to render navigation links to other Razor components.
* `NavMenu.razor.css`: Stylesheet for the app's navigation menu.
* `ReconnectModal` component (`ReconnectModal.razor`): Reflects the server-side connection state in the UI.
* `ReconnectModal.razor.css`: Stylesheet for the `ReconnectModal` component.
* `ReconnectModal.razor.js`: JavaScript file for the `ReconnectModal` component.

### `Components/_Imports.razor` file

The imports file (`_Imports.razor`) includes common *Razor directives* to include in the app's Razor components. Razor directives are reserved keywords prefixed with `@` that appear in Razor markup and change the way component markup or component elements are compiled or function.

### `Components/App.razor` file

The `App` component (`App.razor`) is the root component of the app that includes:

* HTML markup.
* The `Routes` component.
* The Blazor script (`<script>` tag for `blazor.web.js`).

The root component is the first component that the app loads.

### `Components/Routes.razor` file

The `Routes` component (`Routes.razor`) sets up routing for the app.

### `appsettings.json` file

The `appsettings.json` file contains configuration data, such as connection strings.

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](../../security/?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

### `Program.cs` file

The `Program.cs` file contains code to create the app and configure the request processing pipeline of the app.

The order of the lines in the Blazor Web App project template changes across releases of .NET, so the order of the lines in the `Program.cs` file might not match the order of the lines covered in this section.

A [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) creates the app with preconfigured defaults:

```
var builder = WebApplication.CreateBuilder(args);
```

Razor component services are added to the app by calling [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents), which enables Razor components to render and execute code on the server, and [AddInteractiveServerComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serverrazorcomponentsbuilderextensions.addinteractiveservercomponents) adds services to support rendering Interactive Server components:

```
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
```

The [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (held by the `app` variable in the following code) is built:

```
var app = builder.Build();
```

Next, the HTTP request pipeline is configured.

When the app isn't running in the `Development` environment:

* Exception handler middleware ([UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)) processes errors and displays a custom error page.
* [HTTP Strict Transport Security (HSTS) protocol middleware](../../../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) ([UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)) processes [HSTS](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html).

```
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}
```

By default, an ASP.NET Core app doesn't provide a status code page for HTTP error status codes, such as *404 - Not Found*. When the app sets an HTTP 400-599 error status code without a body, it returns the status code and an empty response body. However, an app generated from the Blazor Web App project template calls [UseStatusCodePagesWithReExecute](/en-us/dotnet/api/microsoft.aspnetcore.builder.statuscodepagesextensions.usestatuscodepageswithreexecute) to add status code pages middleware to the request pipeline for pages that aren't found, which generates the response body by re-executing the request pipeline using the path to the Not Found error page (`/not-found`):

```
app.UseStatusCodePagesWithReExecute("/not-found", 
    createScopeForStatusCodePages: true);
```

HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) enforces the HTTPS protocol by redirecting HTTP requests to HTTPS if an HTTPS port is available:

```
app.UseHttpsRedirection();
```

Antiforgery middleware ([UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery)) enforces antiforgery protection for form processing:

```
app.UseAntiforgery();
```

Map Static Assets routing endpoint conventions ([MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets)) maps static files, such as images, scripts, and stylesheets, produced during the build as endpoints:

```
app.MapStaticAssets();
```

Static file middleware ([UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)) serves static files, such as images, scripts, and stylesheets from the `wwwroot` folder:

```
app.UseStaticFiles();
```

[MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) maps components defined in the root `App` component to the given .NET assembly and renders routable components, and [AddInteractiveServerRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.serverrazorcomponentsendpointconventionbuilderextensions.addinteractiveserverrendermode) configures interactive server-side rendering (interactive SSR) support for the app:

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

Note

The extension methods [AddInteractiveServerComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serverrazorcomponentsbuilderextensions.addinteractiveservercomponents) on [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) and [AddInteractiveServerRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.serverrazorcomponentsendpointconventionbuilderextensions.addinteractiveserverrendermode) on [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) make the app capable of adopting interactive SSR, which isn't relevant until the last part of the tutorial series on interactivity. Over the next several articles, the app's components only adopt static SSR.

The app is run by calling [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.run) on the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`):

```
app.Run();
```

## Troubleshoot with the completed sample

If you run into a problem while following the tutorial that you can't resolve from the text, compare your code to the completed project in the Blazor samples repository:

[Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)

Select the latest version folder. The sample folder for this tutorial's project is named `BlazorWebAppMovies`.

## Related content

When using VS Code or the .NET CLI, this tutorial series adopts insecure HTTP protocol to ease the transition of adopting SSL/HTTPS security for Linux and macOS users. For information on adopting SSL/HTTPS, see [Enforce HTTPS in ASP.NET Core](../../../security/enforcing-ssl?view=aspnetcore-10.0).

## Next steps

[Next: Add and scaffold a model](part-2?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/javascript-interoperability/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor JavaScript interoperability (JS interop)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

A Blazor app can invoke JavaScript (JS) functions from .NET methods and .NET methods from JS functions. These scenarios are called *JavaScript interoperability* (*JS interop*).

Further JS interop guidance is provided in the following articles:

* [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0)
* [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](call-dotnet-from-javascript?view=aspnetcore-10.0)
* [ASP.NET Core Blazor JavaScript interoperability (JS interop) performance best practices](../performance/javascript-interoperability?view=aspnetcore-10.0)

Note

JavaScript `[JSImport]`/`[JSExport]` interop API is available for client-side components in ASP.NET Core in .NET 7 or later.

For more information, see [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](import-export-interop?view=aspnetcore-10.0).

## Compression for interactive server components with untrusted data

With compression, which is enabled by default, avoid creating secure (authenticated/authorized) interactive server-side components that render data from untrusted sources. Untrusted sources include route parameters, query strings, data from JS interop, and any other source of data that a third-party user can control (databases, external services). For more information, see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#websocket-compression-for-interactive-server-components) and [Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering](../security/interactive-server-side-rendering?view=aspnetcore-9.0#interactive-server-components-with-websocket-compression-enabled).

## JavaScript interop abstractions and features package

The [`@microsoft/dotnet-js-interop` package (`npmjs.com`)](https://www.npmjs.com/package/@microsoft/dotnet-js-interop) ([`Microsoft.JSInterop` NuGet package](https://www.nuget.org/packages/Microsoft.JSInterop)) provides abstractions and features for interop between .NET and JavaScript (JS) code. Reference source is available in the [`dotnet/aspnetcore` GitHub repository (`/src/JSInterop` folder)](https://github.com/dotnet/aspnetcore/tree/main/src/JSInterop). For more information, see the GitHub repository's `README.md` file.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Additional resources for writing JS interop scripts in TypeScript:

* [TypeScript](https://www.typescriptlang.org/)
* [Tutorial: Create an ASP.NET Core app with TypeScript in Visual Studio](/en-us/visualstudio/javascript/tutorial-aspnet-with-typescript)
* [Manage npm packages in Visual Studio](/en-us/visualstudio/javascript/npm-package-management)

## Interaction with the DOM

Only mutate the DOM with JavaScript (JS) when the object doesn't interact with Blazor. Blazor maintains representations of the DOM and interacts directly with DOM objects. If an element rendered by Blazor is modified externally using JS directly or via JS Interop, the DOM may no longer match Blazor's internal representation, which can result in undefined behavior. Undefined behavior may merely interfere with the presentation of elements or their functions but may also introduce security risks to the app or server.

This guidance not only applies to your own JS interop code but also to any JS libraries that the app uses, including anything provided by a third-party framework, such as [Bootstrap JS](https://getbootstrap.com/) and [jQuery](https://jquery.com/).

In a few documentation examples, JS interop is used to mutate an element *purely for demonstration purposes* as part of an example. In those cases, a warning appears in the text.

For more information, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0#capture-references-to-elements).

## JavaScript class with a field of type function

A JavaScript class with a field of type function isn't supported by Blazor JS interop. Use Javascript functions in classes.

❌Unsupported: `GreetingHelpers.sayHello` in the following class as a field of type function isn't discovered by Blazor's JS interop and can't be executed from C# code:

```
export class GreetingHelpers {
  sayHello = function() {
    ...
  }
}
```

✔️Supported: `GreetingHelpers.sayHello` in the following class as a function is supported:

```
export class GreetingHelpers {
  sayHello() {
    ...
  }
}
```

Arrow functions are also supported:

```
export class GreetingHelpers {
  sayHello = () => {
    ...
  }
}
```

## Avoid inline event handlers

A JavaScript function can be invoked directly from an inline event handler. In the following example, `alertUser` is a JavaScript function called when the button is selected by the user:

```
<button onclick="alertUser">Click Me!</button>
```

However, the use of inline event handlers is a *poor design choice* for calling JavaScript functions:

* Mixing HTML markup and JavaScript code often leads to unmaintainable code.
* Inline event handler execution may be blocked by a [Content Security Policy (CSP) (MDN documentation)](https://developer.mozilla.org/docs/Web/HTTP/CSP).

We recommend avoiding inline event handlers in favor of approaches that assign handlers in JavaScript with [`addEventListener`](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener), as the following example demonstrates:

`AlertUser.razor.js`:

```
export function alertUser() {
  alert('The button was selected!');
}

export function addHandlers() {
  const btn = document.getElementById("btn");
  btn.addEventListener("click", alertUser);
}
```

`AlertUser.razor`:

```
@page "/alert-user"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Alert User</h1>

<p>
    <button id="btn">Click Me!</button>
</p>

@code {
    private IJSObjectReference? module;

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./Components/Pages/AlertUser.razor.js");

            await module.InvokeVoidAsync("addHandlers");
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

In the preceding example, [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) is trapped during module disposal in case Blazor's SignalR circuit is lost. If the preceding code is used in a Blazor WebAssembly app, there's no SignalR connection to lose, so you can remove the `try`-`catch` block and leave the line that disposes the module (`await module.DisposeAsync();`). For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit).

For more information, see the following resources:

* [JavaScript location in ASP.NET Core Blazor apps](location-of-javascript?view=aspnetcore-10.0)
* [Introduction to events (MDN documentation)](https://developer.mozilla.org/docs/Learn/JavaScript/Building_blocks/Events#inline_event_handlers_%E2%80%94_dont_use_these)

## Asynchronous JavaScript calls

JS interop calls are asynchronous, regardless of whether the called code is synchronous or asynchronous. Calls are asynchronous to ensure that components are compatible across server-side and client-side rendering models. When adopting server-side rendering, JS interop calls must be asynchronous because they're sent over a network connection. For apps that exclusively adopt client-side rendering, synchronous JS interop calls are supported.

For more information, see the following articles:

* [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0#synchronous-js-interop-in-client-side-components)
* [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](call-dotnet-from-javascript?view=aspnetcore-10.0#synchronous-js-interop-in-client-side-components)

For more information, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0#synchronous-js-interop-in-client-side-components).

## Object serialization

Blazor uses [System.Text.Json](/en-us/dotnet/api/system.text.json) for serialization with the following requirements and default behaviors:

* Types must have a default constructor, [`get`/`set` accessors](/en-us/dotnet/csharp/programming-guide/classes-and-structs/using-properties) must be public, and fields are never serialized.
* Global default serialization isn't customizable to avoid breaking existing component libraries, impacts on performance and security, and reductions in reliability.
* Serializing .NET member names results in lowercase JSON key names.
* JSON is deserialized as [JsonElement](/en-us/dotnet/api/system.text.json.jsonelement) C# instances, which permit mixed casing. Internal casting for assignment to C# model properties works as expected in spite of any case differences between JSON key names and C# property names.
* Complex framework types might be [trimmed away by the IL Trimmer on publish](../host-and-deploy/configure-trimmer?view=aspnetcore-10.0#failure-to-preserve-types-used-by-a-published-app) and not present for JS interop or JSON serialization/deserialization. We recommend creating custom types for types that the IL Trimmer trims away.
* Blazor always relies on [reflection for JSON serialization](/en-us/dotnet/standard/serialization/system-text-json/reflection-vs-source-generation), including when using C# [source generation](/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview). Setting `JsonSerializerIsReflectionEnabledByDefault` to `false` in the app's project file results in an error when serialization is attempted.

[JsonConverter](/en-us/dotnet/api/system.text.json.serialization.jsonconverter) API is available for custom serialization. Properties can be annotated with a [`[JsonConverter]` attribute](/en-us/dotnet/api/system.text.json.serialization.jsonconverterattribute) to override default serialization for an existing data type.

For more information, see the following resources in the .NET documentation:

* [JSON serialization and deserialization (marshalling and unmarshalling) in .NET](/en-us/dotnet/standard/serialization/system-text-json-overview)
* [How to customize property names and values with `System.Text.Json`](/en-us/dotnet/standard/serialization/system-text-json-customize-properties)
* [How to write custom converters for JSON serialization (marshalling) in .NET](/en-us/dotnet/standard/serialization/system-text-json-converters-how-to)

Blazor supports optimized byte array JS interop that avoids encoding/decoding byte arrays into Base64. The app can apply custom serialization and pass the resulting bytes. For more information, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0#byte-array-support).

Blazor supports unmarshalled JS interop when a high volume of .NET objects are rapidly serialized or when large .NET objects or many .NET objects must be serialized. For more information, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0#unmarshalled-javascript-interop).

## DOM cleanup tasks during component disposal

Don't execute JS interop code for DOM cleanup tasks during component disposal. Instead, use the [`MutationObserver`](https://developer.mozilla.org/docs/Web/API/MutationObserver) pattern in JavaScript (JS) on the client for the following reasons:

* The component may have been removed from the DOM by the time your cleanup code executes in `Dispose{Async}`.
* During server-side rendering, the Blazor renderer may have been disposed by the framework by the time your cleanup code executes in `Dispose{Async}`.

The [`MutationObserver`](https://developer.mozilla.org/docs/Web/API/MutationObserver) pattern allows you to run a function when an element is removed from the DOM.

In the following example, the `DOMCleanup` component:

* Contains a `<div>` with an `id` of `cleanupDiv`. The `<div>` element is removed from the DOM along with the rest of the component's DOM markup when the component is removed from the DOM.
* Loads the `DOMCleanup` JS class from the `DOMCleanup.razor.js` file and calls its `createObserver` function to set up the `MutationObserver` callback. These tasks are accomplished in the [`OnAfterRenderAsync` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync).

`DOMCleanup.razor`:

```
@page "/dom-cleanup"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>DOM Cleanup Example</h1>

<div id="cleanupDiv"></div>

@code {
    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./Components/Pages/DOMCleanup.razor.js");

            await module.InvokeVoidAsync("DOMCleanup.createObserver");
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

In the preceding example, [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) is trapped during module disposal in case Blazor's SignalR circuit is lost. If the preceding code is used in a Blazor WebAssembly app, there's no SignalR connection to lose, so you can remove the `try`-`catch` block and leave the line that disposes the module (`await module.DisposeAsync();`). For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit).

In the following example, the `MutationObserver` callback is executed each time a DOM change occurs. Execute your cleanup code when the `if` statement confirms that the target element (`cleanupDiv`) was removed (`if (targetRemoved) { ... }`). It's important to disconnect and delete the `MutationObserver` to avoid a memory leak after your cleanup code executes.

`DOMCleanup.razor.js` placed side-by-side with the preceding `DOMCleanup` component:

```
export class DOMCleanup {
  static observer;

  static createObserver() {
    const target = document.querySelector('#cleanupDiv');

    this.observer = new MutationObserver(function (mutations) {
      const targetRemoved = mutations.some(function (mutation) {
        const nodes = Array.from(mutation.removedNodes);
        return nodes.indexOf(target) !== -1;
      });

      if (targetRemoved) {
        // Cleanup resources here
        // ...

        // Disconnect and delete MutationObserver
        this.observer && this.observer.disconnect();
        delete this.observer;
      }
    });

    this.observer.observe(target.parentNode, { childList: true });
  }
}

window.DOMCleanup = DOMCleanup;
```

The preceding approaches attach the `MutationObserver` to `target.parentNode`, which works until `parentNode` itself is removed from the DOM. This is a common scenario, for example, when navigating to a new page, which causes the entire page component to be removed from the DOM. In such cases, any child components observing changes within the page aren't cleaned up properly.

Don't assume that observing `document.body`, instead of `target.parentNode`, is a better target. Observing `document.body` has performance implications because callback logic is executed for *all* DOM updates, whether or not they have anything to do with your element. Use either of the following approaches:

* In cases where you can identify a suitable ancestor node to observe, use `MutationObserver` with it. Ideally, this ancestor is scoped to the changes that you want to observe, rather than `document.body`.
* Instead of using `MutationObserver`, consider using a [custom element and `disconnectedCallback`](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements). The event always fires when your custom element is disconnected, no matter where it resides in the DOM relative to the DOM change.

## JavaScript interop calls without a circuit

*This section only applies to server-side apps.*

JavaScript (JS) interop calls can't be issued after Blazor's SignalR circuit is disconnected. Without a circuit during component disposal or at any other time that a circuit doesn't exist, the following method calls fail and log a message that the circuit is disconnected as a [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception):

* JS interop method calls

  * [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync)
  * [JSRuntimeExtensions.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokeasync)
  * [JSRuntimeExtensions.InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync)
  * [InvokeConstructorAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference.invokeconstructorasync)
  * [InvokeConstructor](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference.invokeconstructor)
  * [GetValueAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference.getvalueasync)
  * [SetValueAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference.setvalueasync)

* `Dispose`/`DisposeAsync` calls on any [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference).

- JS interop method calls

  * [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync)
  * [JSRuntimeExtensions.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokeasync)
  * [JSRuntimeExtensions.InvokeVoidAsync](/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync)

- `Dispose`/`DisposeAsync` calls on any [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference).

In order to avoid logging [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) or to log custom information, catch the exception in a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement.

For the following component disposal example:

* The server-side component implements [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable).
* `module` is an [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) for a JS module.
* [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) is caught and not logged.
* Optionally, you can log custom information in the `catch` statement at whatever log level you prefer. The following example doesn't log custom information because it assumes the developer doesn't care about when or where circuits are disconnected during component disposal.

```
async ValueTask IAsyncDisposable.DisposeAsync()
{
    try
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
    catch (JSDisconnectedException)
    {
    }
}
```

If you must clean up your own JS objects or execute other JS code on the client after a circuit is lost in a server-side Blazor app, use the [`MutationObserver`](https://developer.mozilla.org/docs/Web/API/MutationObserver) pattern in JS on the client. The [`MutationObserver`](https://developer.mozilla.org/docs/Web/API/MutationObserver) pattern allows you to run a function when an element is removed from the DOM.

For more information, see the following articles:

* [Handle errors in ASP.NET Core Blazor apps](../fundamentals/handle-errors?view=aspnetcore-10.0#javascript-interop): The *JavaScript interop* section discusses error handling in JS interop scenarios.
* [ASP.NET Core Razor component disposal](../components/component-disposal?view=aspnetcore-10.0): The article describes how to implement disposal patterns in Razor components.

## Cached JavaScript files

JavaScript (JS) files and other static assets aren't generally cached on clients during development in the [`Development` environment](../../fundamentals/?view=aspnetcore-10.0#environments). During development, static asset requests include the [`Cache-Control` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control) with a value of [`no-cache`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control#cacheability) or [`max-age`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control#expiration) with a value of zero (`0`).

During production in the [`Production` environment](../../fundamentals/?view=aspnetcore-10.0#environments), JS files are usually cached by clients.

To disable client-side caching in browsers, developers usually adopt one of the following approaches:

* Disable caching when the browser's developer tools console is open. Guidance can be found in the developer tools documentation of each browser maintainer:

  * [Chrome DevTools](https://developer.chrome.com/docs/devtools/)
  * [Microsoft Edge Developer Tools overview](/en-us/microsoft-edge/devtools-guide-chromium/)

* Perform a manual browser refresh of any webpage of the Blazor app to reload JS files from the server. ASP.NET Core's HTTP response caching middleware always honors a valid no-cache [`Cache-Control` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control) sent by a client.

For more information, see:

* [ASP.NET Core Blazor environments](../fundamentals/environments?view=aspnetcore-10.0)
* [Response caching in ASP.NET Core](../../performance/caching/response?view=aspnetcore-10.0)

## Size limits on JavaScript interop calls

*This section only applies to interactive components in server-side apps. For client-side components, the framework doesn't impose a limit on the size of JavaScript (JS) interop inputs and outputs.*

For interactive components in server-side apps, JS interop calls passing data from the client to the server are limited in size by the maximum incoming SignalR message size permitted for hub methods, which is enforced by [HubOptions.MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize) (default: 32 KB). JS to .NET SignalR messages larger than [MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize) throw an error. The framework doesn't impose a limit on the size of a SignalR message from the hub to a client. For more information on the size limit, error messages, and guidance on dealing with message size limits, see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#maximum-receive-message-size).

## Determine where the app is running

If it's relevant for the app to know where code is running for JS interop calls, use [OperatingSystem.IsBrowser](/en-us/dotnet/api/system.operatingsystem.isbrowser) to determine if the component is executing in the context of browser on WebAssembly.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/enforcing-ssl.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Enforce HTTPS in ASP.NET Core

By [David Galvan](https://www.linkedin.com/in/dave-galvan/) and [Rick Anderson](https://twitter.com/RickAndMSFT)

To enforce incoming requests to your ASP.NET Core apps to use HTTPS/TLS, you can:

* Require HTTPS for all requests.
* Redirect all HTTP requests to HTTPS.

No API can prevent a client from sending sensitive data on the first request.

This article describes how to configure your ASP.NET Core apps to require HTTPS/TLS or redirect HTTP requests to HTTPS/TLS for secure interaction. Troubleshooting steps are provided for various platforms to resolve untrusted certificate issues.

## API projects

Projects that use Web APIs should either:

* Not listen on HTTP.
* Close the connection with status code 400 (Bad Request) and not serve the request.

To disable HTTP redirection in an API, set the `ASPNETCORE_URLS` environment variable or use the `--urls` command line flag. For more information, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0) and [8 ways to set the URLs for an ASP.NET Core app](https://andrewlock.net/8-ways-to-set-the-urls-for-an-aspnetcore-app/) by Andrew Lock.

Warning

Do **not** use [RequireHttpsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requirehttpsattribute) on Web APIs that receive sensitive information. `RequireHttpsAttribute` uses HTTP status codes to redirect browsers from HTTP to HTTPS. API clients might not understand or obey redirects from HTTP to HTTPS, and they might send information over HTTP.

### HSTS and API projects

The secure approach for [HTTP Strict Transport Security (HSTS) protocol](#hsts) is to configure API projects to only listen to and respond over HTTPS.

Warning

The default API projects don't include [HSTS](https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security) because it's generally a browser only instruction. Other callers, such as phone or desktop apps, do **not** obey the instruction. Even within browsers, a single authenticated call to an API over HTTP has risks on insecure networks.

### HTTP redirect to HTTPS (ERR\_INVALID\_REDIRECT on CORS preflight request)

When a request to an endpoint using HTTP is redirected to HTTPS with the [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) method, the redirection fails with the `ERR_INVALID_REDIRECT` error on the CORS preflight request.

API projects can reject HTTP requests rather than use the `UseHttpsRedirection` method to redirect requests to HTTPS.

## Require HTTPS

For production ASP.NET Core web apps, the following approach is recommended:

* To redirect HTTP requests to HTTPS, use HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)).

* To send HSTS headers to clients, use HSTS middleware via the [UseHsts](#hsts) method.

Note

Apps deployed in a reverse proxy configuration allow the proxy to handle connection security (HTTPS). If the proxy also handles HTTPS redirection, there's no need to use HTTPS redirection middleware. If the proxy server also handles writing HSTS headers (for example, [native HSTS support in Internet Information Services (IIS) 10.0 version 1709 or later](/en-us/iis/get-started/whats-new-in-iis-10-version-1709/iis-10-version-1709-hsts#iis-100-version-1709-native-hsts-support)), then the app doesn't require HSTS middleware. For more information, see [Opt-out of HTTPS/HSTS on project creation](#opt-out-of-httpshsts-on-project-creation).

### HTTPS redirection middleware (`UseHttpsRedirection`)

The following code calls the [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) method in the *Program.cs* file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding highlighted code:

* Uses the default [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-redirectstatuscode) property with the [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) code.
* Uses the default [HttpsRedirectionOptions.HttpsPort](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.httpsport#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-httpsport) property (passing null), unless overridden by the `ASPNETCORE_HTTPS_PORT` environment variable or [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

The recommended approach is to use temporary redirects rather than permanent redirects. Link caching can cause unstable behavior in development environments. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, see the [Configure permanent redirects in production](#configure-permanent-redirects-in-production) section. Use [HSTS](#hsts) to signal to clients that only secure resource requests should be sent to the app (only in production).

Note

Don't confuse the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for HTTPS redirection middleware, with the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration.

### Port configuration

A port must be available for the middleware to redirect an insecure request to HTTPS. If no port is available:

* Redirection to HTTPS doesn't occur.
* The middleware logs the warning *Failed to determine the https port for redirect*.

Specify the HTTPS port by using any of the following approaches:

* Set [HttpsRedirectionOptions.HttpsPort](#options).

* Set the `https_port` [host setting](../fundamentals/host/generic-host?view=aspnetcore-10.0#https-port):

  * In host configuration.

  * By setting the `ASPNETCORE_HTTPS_PORT` environment variable.

  * By adding a top-level entry in the *appsettings.json* file:

    ```
    {
      "https_port": 443,
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*"
    }
    ```

* Indicate a port with the secure scheme by using the [ASPNETCORE\_URLS environment variable](../fundamentals/host/generic-host?view=aspnetcore-10.0#server-urls). The environment variable configures the server. The middleware indirectly discovers the HTTPS port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature). This approach doesn't work in reverse proxy deployments.

* The ASP.NET Core web templates set an HTTPS URL in the *Properties/launchsettings.json* file for both Kestrel and IIS Express. The *launchsettings.json* file is used on the local machine only.

* Configure an HTTPS URL endpoint for a public-facing edge deployment of [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server or [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) server. Only **one HTTPS port** is used by the app. The middleware discovers the port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

Note

When an app runs in a reverse proxy configuration, [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature) isn't available. Set the port by using one of the other approaches described in this section.

### Edge deployments

When [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) or [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) is used as a public-facing edge server, Kestrel or HTTP.sys must be configured to listen on both:

* The secure port where the client is redirected (typically, 443 in production and 5001 in development).
* The insecure port (typically, 80 in production and 5000 in development).

The insecure port must be accessible by the client for the app to receive an insecure request and redirect the client to the secure port.

For more information, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0) or [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

### Deployment scenarios

Any firewall between the client and server must also have communication ports open for traffic.

If requests are forwarded in a reverse proxy configuration, use [forwarded headers middleware](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0) before calling HTTPS redirection middleware. Forwarded headers middleware updates the `Request.Scheme` by using the `X-Forwarded-Proto` header. The middleware permits redirect URIs and other security policies to work correctly. When forwarded headers middleware isn't used, the back-end app might not receive the correct scheme and get caught in a redirect loop. A common end user error message is there are too many redirects.

When deploying to Azure App Service, follow the guidance in [Enable HTTPS for a custom domain in Azure App Service](/en-us/azure/app-service/configure-ssl-bindings).

### Options

The following highlighted code calls the [AddHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpsredirectionservicesextensions.addhttpsredirection) method to configure middleware options:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = Status307TemporaryRedirect;
    options.HttpsPort = 5001;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

Calling `AddHttpsRedirection` is only necessary to change the values of `HttpsPort` or `RedirectStatusCode`.

The preceding highlighted code:

* Sets the [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode) property to the [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) code, which is the default value. Use the fields of the [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) class for assignments to `RedirectStatusCode`.
* Sets the HTTPS port to 5001.

#### Configure permanent redirects in production

The middleware defaults to sending a [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) code with all redirects. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, wrap the middleware options configuration in a conditional check for a non-`Development` environment.

The following code shows configuration of services in the *Program.cs* file:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (!builder.Environment.IsDevelopment())
{
    builder.Services.AddHttpsRedirection(options =>
    {
        options.RedirectStatusCode = Status308PermanentRedirect;
        options.HttpsPort = 443;
    });
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");

    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

## HTTPS redirection middleware alternative approach

An alternative to using HTTPS redirection middleware (with the `UseHttpsRedirection` method) is to use URL rewriting middleware (via the `AddRedirectToHttps` method). `AddRedirectToHttps` can also set the status code and port when the redirect is executed. For more information, see [URL rewriting middleware](../fundamentals/url-rewriting?view=aspnetcore-10.0).

When the app redirects to HTTPS without the requirement for other redirect rules, the recommendation is to use HTTPS redirection middleware (`UseHttpsRedirection`) as described in this article.

## HTTP Strict Transport Security (HSTS) protocol

Per [OWASP](https://owasp.org/about/), [HSTS](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html) is an opt-in security enhancement specified by a web app via a response header. When a [browser that supports HSTS](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html#browser-support) receives this header:

* The browser stores configuration for the domain that prevents sending any communication over HTTP. The browser forces all communication over HTTPS.
* The browser prevents the user from using untrusted or invalid certificates. The browser disables prompts that allow a user to temporarily trust such a certificate.

Because the client enforces [HSTS](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security), there are some limitations:

* The client must support HSTS.
* HSTS requires at least one successful HTTPS request to establish the HSTS policy.
* The application must check every HTTP request and redirect or reject the HTTP request.

ASP.NET Core implements HSTS with the [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts) extension method. The following code calls `UseHsts` when the app isn't in [development mode](../fundamentals/environments?view=aspnetcore-10.0):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

`UseHsts` isn't recommended in development because the HSTS settings are highly cacheable by browsers. By default, `UseHsts` excludes the local loopback address.

For production environments that are implementing HTTPS for the first time, set the initial [HstsOptions.MaxAge](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.hstsoptions.maxage) property value to a small amount by using one of the [TimeSpan](/en-us/dotnet/api/system.timespan) methods. Set the value from hours to no more than a single day, in case you need to revert the HTTPS infrastructure to HTTP. After you're confident in the sustainability of the HTTPS configuration, increase the HSTS `max-age` value (commonly, one year).

The following highlighted code:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = Status307TemporaryRedirect;
    options.HttpsPort = 5001;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

* Sets the preload parameter of the `Strict-Transport-Security` header. Preload isn't part of the [RFC 6797 HSTS specification](https://datatracker.ietf.org/doc/html/rfc6797). Web browsers support preload of HSTS sites on fresh install. For more information, see <https://hstspreload.org/>.
* Enables the `includeSubDomain` directive, which applies the HSTS policy to host subdomains. For more information, see [RFC 6797 HSTS specification (Section 6.1.2)](https://datatracker.ietf.org/doc/html/rfc6797#section-6.1.2).
* Explicitly sets the `max-age` parameter of the `Strict-Transport-Security` header to 60 days. If not set, it defaults to 30 days. For more information, see the `max-age` directive in [RFC 6797 HSTS specification (Section 6.1.1)](https://datatracker.ietf.org/doc/html/rfc6797#section-6.1.1).
* Adds `example.com` to the list of hosts to exclude.

`UseHsts` excludes the following loopback hosts:

* `localhost`: The IPv4 loopback address.
* `127.0.0.1`: The IPv4 loopback address.
* `[::1]`: The IPv6 loopback address.

## Opt out of HTTPS/HSTS on project creation

In some back-end service scenarios where connection security is handled at the public-facing edge of the network, configuring connection security at each node isn't required. Web apps that are generated from the templates in Visual Studio or from the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command enable [HTTPS redirection](#require-https) and [HSTS](#hsts). For deployments that don't require these scenarios, you can opt out of HTTPS/HSTS when the app is created from the template.

To opt out of HTTPS/HSTS:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

When you create a new ASP.NET Core web app, unselect the **Configure for HTTPS** option:

Use the `--no-https` option with the `dotnet new webapp` command, for example:

```
dotnet new webapp --no-https
```

[]()

## Trust the ASP.NET Core HTTPS development certificate

The .NET SDK includes an HTTPS development certificate. The certificate is installed as part of the first-run experience. For example, the `dotnet --info` command produces a variation of the following output:

```
ASP.NET Core
------------
Successfully installed the ASP.NET Core HTTPS Development Certificate.
To trust the certificate run 'dotnet dev-certs https --trust' (Windows and macOS only).
For establishing trust on other platforms refer to the platform specific documentation.
For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054.
```

Installing the .NET SDK installs the ASP.NET Core HTTPS development certificate to the local user certificate store. The certificate is installed, but it isn't trusted. To trust the certificate, perform the one-time step to run the `dotnet dev-certs` tool:

```
dotnet dev-certs https --trust
```

The following command provides help on the `dotnet dev-certs` tool:

```
dotnet dev-certs https --help
```

Warning

Don't create a development certificate in an environment planned for redistribution, such as a container image or virtual machine. This scenario can lead to spoofing and elevation of privilege. To help prevent this situation, set the `DOTNET_GENERATE_ASPNET_CERTIFICATE` environment variable to `false` before calling the .NET CLI for the first time. This approach skips the automatic generation of the ASP.NET Core development certificate during the CLI's first-run experience.

## Set up developer certificate for Docker

To configure the developer certificate for Docker, see [GitHub dotnet/aspnetcore.docs issue #6199](https://github.com/dotnet/AspNetCore.Docs/issues/6199) - *How to set up the dev certificate when using Docker in development*.

## Linux-specific considerations

Linux distributions differ substantially in how they mark certificates as trusted.

The `dotnet dev-certs` tool is expected to be broadly applicable, but official support is available for Ubuntu and Fedora only. The support specifically aims to ensure trust in Firefox and Chromium-based browsers (Microsoft Edge, Chrome, and Chromium).

### Dependencies

* To establish OpenSSL trust, the `openssl` tool must be on the path.
* To establish browser trust (for example in Microsoft Edge or Firefox), the `certutil` tool must be on the path.

### OpenSSL trust

When an ASP.NET Core development certificate is trusted, the certificate is exported to a folder in the current user's home directory. To have [OpenSSL](https://www.openssl.org/) (and clients that consume it) pick up this folder, you need to set the `SSL_CERT_DIR` environment variable. You can set the variable in a single session by running a command like `export SSL_CERT_DIR=$HOME/.aspnet/dev-certs/trust:/usr/lib/ssl/certs` (the exact value is in the output when `--verbose` is passed) or by adding it your (distro- and shell-specific) configuration file (for example *.profile*).

This approach is required to make tools like `curl` trust the development certificate. Alternatively, you can pass `-CAfile` or `-CApath` to each individual `curl` invocation.

Note

Requires 1.1.1h or later or 3.0.0 or later, depending on which major version you're using.

If OpenSSL trust gets into a bad state (for example if `dotnet dev-certs https --clean` fails to remove it), you can frequently resolve the situation by using the [c\_rehash](https://docs.openssl.org/master/man1/openssl-rehash/) tool.

### Overrides

If you're using another browser with its own Network Security Services (NSS) store, you can use the `DOTNET_DEV_CERTS_NSSDB_PATHS` environment variable to specify a colon-delimited list of NSS directories (for example, the directory containing `cert9.db`). You can then add the development certificate location to the list in the variable.

If you store the certificates you want OpenSSL to trust in a specific directory, you can use the `DOTNET_DEV_CERTS_OPENSSL_CERTIFICATE_DIRECTORY` environment variable to indicate the certificate location.

Warning

If you set either variable, be sure to set the same values each time trust is updated. If the values change, the tool doesn't know about certificates in the former locations (for example, during certificate cleanup).

### Using sudo

As on other platforms, development certificates are stored and trusted separately for each user.

If you run `dotnet dev-certs` as a different user (for example, by using `sudo`), then *that* specific user (for example `root`) trusts the development certificate.

### Trust HTTPS certificate on Linux with linux-dev-certs

[linux-dev-certs](https://github.com/tmds/linux-dev-certs) is an open-source, community-supported, .NET global tool that provides a convenient way to create and trust a developer certificate on Linux. Microsoft doesn't maintain or support the tool.

The following commands install the tool and create a trusted developer certificate:

```
dotnet tool update -g linux-dev-certs
dotnet linux-dev-certs install
```

For more information or to report issues, see the [linux-dev-certs GitHub repository](https://github.com/tmds/linux-dev-certs).

### Trust the certificate on SUSE Linux Enterprise Server (SLES) and openSUSE

`dotnet dev-certs https --trust` isn't officially supported on SLES. Use the following steps to export the development certificate and trust it in Chromium-based browsers, Firefox, and command-line tools. The steps were verified on SLES 15 SP7 and openSUSE Leap 15.6.

Warning

The following instructions are intended for development purposes only. Don't use the development certificate in a production environment.

Note

Adding the certificate to the system trust store (`/etc/pki/trust/anchors/` followed by `update-ca-certificates`) doesn't work for the development certificate. The certificate isn't a certificate authority (`CA:FALSE`), so `p11-kit` lists it as an anchor but doesn't include it in the generated bundles, and OpenSSL continues to reject it. Trust it per application as shown below instead.

#### Install dependencies

The `certutil` tool used to manage browser certificate stores is provided by the `mozilla-nss-tools` package:

```
sudo zypper install mozilla-nss-tools openssl
```

#### Export the development certificate

Replace `${CertificateDirectory}` with a directory outside your source repositories, for example `$HOME/.aspnet/https`:

```
mkdir -p ${CertificateDirectory}
dotnet dev-certs https -ep ${CertificateDirectory}/aspnetcore.crt --format PEM --no-password
```

#### Trust the certificate in Chromium-based browsers (Microsoft Edge, Chrome, Chromium)

```
certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n aspnetcore -i ${CertificateDirectory}/aspnetcore.crt
```

If `$HOME/.pki/nssdb` doesn't exist yet, create it first with `certutil -d sql:$HOME/.pki/nssdb -N --empty-password`. Restart the browser after importing.

#### Trust the certificate in Firefox

Replace `${UserProfile}` with the name of your Firefox profile directory (see `about:profiles` in Firefox):

```
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -A -t "C,," -n aspnetcore -i ${CertificateDirectory}/aspnetcore.crt
```

#### Trust the certificate in curl and other OpenSSL clients

Pass the exported certificate explicitly:

```
curl --cacert ${CertificateDirectory}/aspnetcore.crt https://localhost:5001
```

Alternatively, point `SSL_CERT_DIR` at a directory that contains the certificate as described in [OpenSSL trust](#openssl-trust).

#### Remove the development certificate

```
certutil -d sql:$HOME/.pki/nssdb -D -n aspnetcore
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -D -n aspnetcore
rm ${CertificateDirectory}/aspnetcore.crt
dotnet dev-certs https --clean
```

### Trust the certificate on Red Hat Enterprise Linux (RHEL)

`dotnet dev-certs https --trust` isn't officially supported on RHEL. The following steps mirror the [SLES instructions](#trust-the-certificate-on-suse-linux-enterprise-server-sles-and-opensuse), adapted for RHEL package names.

Warning

The following instructions are intended for development purposes only. Don't use the development certificate in a production environment.

Note

As on SLES, adding the certificate to the system trust store (`/etc/pki/ca-trust/source/anchors/` followed by `update-ca-trust`) doesn't work for the development certificate. The certificate isn't a certificate authority (`CA:FALSE`), so trust it per application as shown below instead.

#### Install dependencies

The `certutil` tool used to manage browser certificate stores is provided by the `nss-tools` package:

```
sudo dnf install nss-tools openssl
```

#### Export the development certificate

Replace `${CertificateDirectory}` with a directory outside your source repositories, for example `$HOME/.aspnet/https`:

```
mkdir -p ${CertificateDirectory}
dotnet dev-certs https -ep ${CertificateDirectory}/aspnetcore.crt --format PEM --no-password
```

#### Trust the certificate in Chromium-based browsers (Microsoft Edge, Chrome, Chromium)

```
certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n aspnetcore -i ${CertificateDirectory}/aspnetcore.crt
```

If `$HOME/.pki/nssdb` doesn't exist yet, create it first with `certutil -d sql:$HOME/.pki/nssdb -N --empty-password`. Restart the browser after importing.

#### Trust the certificate in Firefox

Replace `${UserProfile}` with the name of your Firefox profile directory (see `about:profiles` in Firefox):

```
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -A -t "C,," -n aspnetcore -i ${CertificateDirectory}/aspnetcore.crt
```

#### Trust the certificate in curl and other OpenSSL clients

Pass the exported certificate explicitly:

```
curl --cacert ${CertificateDirectory}/aspnetcore.crt https://localhost:5001
```

Alternatively, create an OpenSSL hashed certificate directory and point `SSL_CERT_DIR` at it:

```
mkdir -p ${CertificateDirectory}/certs
cp ${CertificateDirectory}/aspnetcore.crt ${CertificateDirectory}/certs/
openssl rehash ${CertificateDirectory}/certs
export SSL_CERT_DIR=${CertificateDirectory}/certs
```

#### Remove the development certificate

```
certutil -d sql:$HOME/.pki/nssdb -D -n aspnetcore
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -D -n aspnetcore
rm ${CertificateDirectory}/aspnetcore.crt
dotnet dev-certs https --clean
```

## Troubleshoot certificate problems (certificate not trusted)

Sometimes when an ASP.NET Core HTTPS development certificate is [installed and trusted](#trust), the browser warns that the certificate is untrusted. The following sections provide help for troubleshooting this issue.

The ASP.NET Core HTTPS development certificate is used by [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0).

To repair the IIS Express certificate, see [Stack Overflow issue #20036984 / answer #20048613](https://stackoverflow.com/questions/20036984/how-do-i-restore-a-missing-iis-express-ssl-certificate/20048613#20048613) - *How do I restore a missing IIS Express SSL Certificate?*

### All platforms - certificate not trusted

For all platforms, try to resolve the untrusted certificate issues with the following steps:

1. Run the following commands:

   ```
   dotnet dev-certs https --clean
   dotnet dev-certs https --trust
   ```

2. Close any open browser instances, and open the app in a new browser window.

   The browser cache stores whether a certificate is trusted. The close/open process helps to refresh the browser cache settings for certificates.

The `dotnet dev-certs https` commands usually solve most browser trust issues. If the `dotnet dev-certs https --clean` command fails and the browser still doesn't trust the certificate, try the platform-specific suggestions in the following sections.

### Docker - certificate not trusted

If you're using Docker, try to resolve the issue with the following steps:

1. Delete the *C:\Users{USER}\AppData\Roaming\ASP.NET\Https* folder.

2. Clean the solution. Delete the *bin* and *obj* folders.

3. Restart the development tool. For example, Visual Studio or Visual Studio Code.

### Windows - certificate not trusted

If you're working in Windows, complete the following troubleshooting steps:

1. Check the certificates in the certificate store. Look for a `localhost` certificate with the `ASP.NET Core HTTPS development certificate` friendly name in two folders:

   * *Current User > Personal > Certificates*
   * *Current User > Trusted root certification authorities > Certificates*

2. Remove all certificates from both Personal and Trusted root certification authorities.

   Important

   Do **not** remove the IIS Express localhost certificate.

3. Run the following commands:

   ```
   dotnet dev-certs https --clean
   dotnet dev-certs https --trust
   ```

4. Close any open browser instances, and open the app in a new browser window.

### OS X - certificate not trusted

If you're working with OS X, try to resolve the issue with the following steps:

1. Open KeyChain Access, and then select the System keychain.

2. Check for the presence of a localhost certificate.

3. Confirm the certificate shows the plus (`+`) symbol on the icon, which indicates the certificate is trusted for all users.

4. Remove the certificate from the system keychain.

5. Run the following commands:

   ```
   dotnet dev-certs https --clean
   dotnet dev-certs https --trust
   ```

6. Close any open browser instances, and open the app in a new browser window.

For more information about troubleshooting certificate issues with Visual Studio, see [GitHub dotnet/aspnetcore issue #16892)](https://github.com/dotnet/AspNetCore/issues/16892) - *HTTPS Error using IIS Express*.

### Linux - certificate not trusted

If you're running Linux, follow these steps to troubleshoot the untrusted certificate:

1. Confirm the certificate you're investigating is the user HTTPS developer certificate planned for use by the Kestrel server.

2. Check the current user default HTTPS developer Kestrel certificate at the following location:

   ```
   ls -la ~/.dotnet/corefx/cryptography/x509stores/my
   ```

   The HTTPS developer Kestrel certificate file is the SHA1 thumbprint. When the file is deleted with the `dotnet dev-certs https --clean` command, the file is regenerated when needed with a different thumbprint.

3. Verify the thumbprint of the exported certificate matches by running the following command:

   ```
   openssl x509 -noout -fingerprint -sha1 -inform pem -in /usr/local/share/ca-certificates/aspnet/https.crt
   ```

   If the certificate thumbprint doesn't match, investigate the following conditions:

   * Check if the certificate is old.

   * Check if the certificate is an exported developer certificate for the root user.

     * If it is, export the certificate.

4. Check the root user certificate in the following folder:

   ```
   ls -la /root/.dotnet/corefx/cryptography/x509stores/my
   ```

### IIS Express SSL certificate used with Visual Studio

To fix problems with the IIS Express certificate, select **Repair** in the Visual Studio installer. For more information, see [GitHub dotnet/aspnetcore issue #16892)](https://github.com/dotnet/AspNetCore/issues/16892) - *HTTPS Error using IIS Express*.

### Group policy prevents trusting self-signed certificates

In some cases, group policy can prevent self-signed certificates from being trusted. For more information, see [GitHub dotnet/aspnetcore issue #21173](https://github.com/dotnet/aspnetcore/issues/21173) - *Error trusting HTTPS developer certificate*.

## Related content

* [Configure ASP.NET Core to work with proxy servers and load balancers](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [Host ASP.NET Core on Linux with Nginx: HTTPS configuration](../host-and-deploy/linux-nginx?view=aspnetcore-10.0#https-configuration)
* [Set up SSL on IIS 7 or later](/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis)
* [Configure endpoints for Kestrel web server](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0)
* [OWASP HSTS browser support](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html#browser-support)

Note

If you're using .NET 9 or later SDK, see the updated Linux procedures in the [.NET 9 version of this article](?view=aspnetcore-9.0\&preserve-view=true).

Warning

## API projects

Do **not** use [RequireHttpsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requirehttpsattribute) on Web APIs that receive sensitive information. `RequireHttpsAttribute` uses HTTP status codes to redirect browsers from HTTP to HTTPS. API clients may not understand or obey redirects from HTTP to HTTPS. Such clients may send information over HTTP. Web APIs should either:

* Not listen on HTTP.
* Close the connection with status code 400 (Bad Request) and not serve the request.

To disable HTTP redirection in an API, set the `ASPNETCORE_URLS` environment variable or use the `--urls` command line flag. For more information, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0) and [8 ways to set the URLs for an ASP.NET Core app](https://andrewlock.net/8-ways-to-set-the-urls-for-an-aspnetcore-app/) by Andrew Lock.

## HSTS and API projects

The default API projects don't include [HSTS](#hsts) because [HSTS](https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security) is generally a browser only instruction. Other callers, such as phone or desktop apps, do **not** obey the instruction. Even within browsers, a single authenticated call to an API over HTTP has risks on insecure networks. The secure approach is to configure API projects to only listen to and respond over HTTPS.

[]()

### HTTP redirection to HTTPS causes ERR\_INVALID\_REDIRECT on the CORS preflight request

Requests to an endpoint using HTTP that are redirected to HTTPS by [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) fail with `ERR_INVALID_REDIRECT` on the CORS preflight request.

API projects can reject HTTP requests rather than use `UseHttpsRedirection` to redirect requests to HTTPS.

## Require HTTPS

We recommend that production ASP.NET Core web apps use:

* HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) to redirect HTTP requests to HTTPS.
* HSTS middleware ([UseHsts](#http-strict-transport-security-hsts-protocol)) to send HTTP Strict Transport Security (HSTS) protocol headers to clients.

Note

Apps deployed in a reverse proxy configuration allow the proxy to handle connection security (HTTPS). If the proxy also handles HTTPS redirection, there's no need to use HTTPS redirection middleware. If the proxy server also handles writing HSTS headers (for example, [native HSTS support in IIS 10.0 (1709) or later](/en-us/iis/get-started/whats-new-in-iis-10-version-1709/iis-10-version-1709-hsts#iis-100-version-1709-native-hsts-support)), HSTS middleware isn't required by the app. For more information, see [Opt-out of HTTPS/HSTS on project creation](#opt-out-of-httpshsts-on-project-creation).

### HTTPS redirection middleware (`UseHttpsRedirection`)

The following code calls [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) in the `Program.cs` file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding highlighted code:

* Uses the default [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-redirectstatuscode) ([Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect)).
* Uses the default [HttpsRedirectionOptions.HttpsPort](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.httpsport#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-httpsport) (null) unless overridden by the `ASPNETCORE_HTTPS_PORT` environment variable or [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

We recommend using temporary redirects rather than permanent redirects. Link caching can cause unstable behavior in development environments. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, see the [Configure permanent redirects in production](#configure-permanent-redirects-in-production) section. We recommend using [HSTS](#http-strict-transport-security-hsts-protocol) to signal to clients that only secure resource requests should be sent to the app (only in production).

### Port configuration

A port must be available for the middleware to redirect an insecure request to HTTPS. If no port is available:

* Redirection to HTTPS doesn't occur.
* The middleware logs the warning "Failed to determine the https port for redirect."

Specify the HTTPS port using any of the following approaches:

* Set [HttpsRedirectionOptions.HttpsPort](#options).

* Set the `https_port` [host setting](../fundamentals/host/generic-host?view=aspnetcore-10.0#https_port):

  * In host configuration.

  * By setting the `ASPNETCORE_HTTPS_PORT` environment variable.

  * By adding a top-level entry in `appsettings.json`:

    ```
    {
      "https_port": 443,
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*"
    }
    ```

* Indicate a port with the secure scheme using the [ASPNETCORE\_URLS environment variable](../fundamentals/host/generic-host?view=aspnetcore-10.0#urls). The environment variable configures the server. The middleware indirectly discovers the HTTPS port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature). This approach doesn't work in reverse proxy deployments.

* The ASP.NET Core web templates set an HTTPS URL in `Properties/launchsettings.json` for both Kestrel and IIS Express. `launchsettings.json` is only used on the local machine.

* Configure an HTTPS URL endpoint for a public-facing edge deployment of [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server or [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) server. Only **one HTTPS port** is used by the app. The middleware discovers the port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

Note

When an app is run in a reverse proxy configuration, [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature) isn't available. Set the port using one of the other approaches described in this section.

### Edge deployments

When [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) or [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) is used as a public-facing edge server, Kestrel or HTTP.sys must be configured to listen on both:

* The secure port where the client is redirected (typically, 443 in production and 5001 in development).
* The insecure port (typically, 80 in production and 5000 in development).

The insecure port must be accessible by the client in order for the app to receive an insecure request and redirect the client to the secure port.

For more information, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration) or [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

### Deployment scenarios

Any firewall between the client and server must also have communication ports open for traffic.

If requests are forwarded in a reverse proxy configuration, use [forwarded headers middleware](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0) before calling HTTPS redirection middleware. Forwarded headers middleware updates the `Request.Scheme`, using the `X-Forwarded-Proto` header. The middleware permits redirect URIs and other security policies to work correctly. When forwarded headers middleware isn't used, the backend app might not receive the correct scheme and end up in a redirect loop. A common end user error message is that too many redirects have occurred.

When deploying to Azure App Service, follow the guidance in [Tutorial: Bind an existing custom SSL certificate to Azure Web Apps](/en-us/azure/app-service/app-service-web-tutorial-custom-ssl).

### Options

The following highlighted code calls [AddHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpsredirectionservicesextensions.addhttpsredirection) to configure middleware options:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = Status307TemporaryRedirect;
    options.HttpsPort = 5001;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

Calling `AddHttpsRedirection` is only necessary to change the values of `HttpsPort` or `RedirectStatusCode`.

The preceding highlighted code:

* Sets [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode) to [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect), which is the default value. Use the fields of the [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) class for assignments to `RedirectStatusCode`.
* Sets the HTTPS port to 5001.

#### Configure permanent redirects in production

The middleware defaults to sending a [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) with all redirects. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, wrap the middleware options configuration in a conditional check for a non-`Development` environment.

When configuring services in `Program.cs`:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (!builder.Environment.IsDevelopment())
{
    builder.Services.AddHttpsRedirection(options =>
    {
        options.RedirectStatusCode = Status308PermanentRedirect;
        options.HttpsPort = 443;
    });
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");

    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

## HTTPS redirection middleware alternative approach

An alternative to using HTTPS redirection middleware (`UseHttpsRedirection`) is to use URL rewriting middleware (`AddRedirectToHttps`). `AddRedirectToHttps` can also set the status code and port when the redirect is executed. For more information, see [URL rewriting middleware](../fundamentals/url-rewriting?view=aspnetcore-10.0).

When redirecting to HTTPS without the requirement for additional redirect rules, we recommend using HTTPS redirection middleware (`UseHttpsRedirection`) described in this article.

[]()

## HTTP Strict Transport Security (HSTS) protocol

Per [OWASP](https://owasp.org/about), [HTTP Strict Transport Security (HSTS)](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html) is an opt-in security enhancement that's specified by a web app through the use of a response header. When a [browser that supports HSTS](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html#browser-support) receives this header:

* The browser stores configuration for the domain that prevents sending any communication over HTTP. The browser forces all communication over HTTPS.
* The browser prevents the user from using untrusted or invalid certificates. The browser disables prompts that allow a user to temporarily trust such a certificate.

Because [HSTS](https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security) is enforced by the client, it has some limitations:

* The client must support HSTS.
* HSTS requires at least one successful HTTPS request to establish the HSTS policy.
* The application must check every HTTP request and redirect or reject the HTTP request.

ASP.NET Core implements HSTS with the [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts) extension method. The following code calls `UseHsts` when the app isn't in [development mode](../fundamentals/environments?view=aspnetcore-10.0):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

`UseHsts` isn't recommended in development because the HSTS settings are highly cacheable by browsers. By default, `UseHsts` excludes the local loopback address.

For production environments that are implementing HTTPS for the first time, set the initial [HstsOptions.MaxAge](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.hstsoptions.maxage) to a small value using one of the [TimeSpan](/en-us/dotnet/api/system.timespan) methods. Set the value from hours to no more than a single day in case you need to revert the HTTPS infrastructure to HTTP. After you're confident in the sustainability of the HTTPS configuration, increase the HSTS `max-age` value; a commonly used value is one year.

The following highlighted code:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = Status307TemporaryRedirect;
    options.HttpsPort = 5001;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

* Sets the preload parameter of the `Strict-Transport-Security` header. Preload isn't part of the [RFC HSTS specification](https://tools.ietf.org/html/rfc6797), but is supported by web browsers to preload HSTS sites on fresh install. For more information, see <https://hstspreload.org/>.
* Enables [includeSubDomain](https://tools.ietf.org/html/rfc6797#section-6.1.2), which applies the HSTS policy to Host subdomains.
* Explicitly sets the `max-age` parameter of the `Strict-Transport-Security` header to 60 days. If not set, defaults to 30 days. For more information, see the [max-age directive](https://tools.ietf.org/html/rfc6797#section-6.1.1).
* Adds `example.com` to the list of hosts to exclude.

`UseHsts` excludes the following loopback hosts:

* `localhost` : The IPv4 loopback address.
* `127.0.0.1` : The IPv4 loopback address.
* `[::1]` : The IPv6 loopback address.

## Opt-out of HTTPS/HSTS on project creation

In some backend service scenarios where connection security is handled at the public-facing edge of the network, configuring connection security at each node isn't required. Web apps that are generated from the templates in Visual Studio or from the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command enable [HTTPS redirection](#require-https) and [HSTS](#http-strict-transport-security-hsts-protocol). For deployments that don't require these scenarios, you can opt-out of HTTPS/HSTS when the app is created from the template.

To opt-out of HTTPS/HSTS:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

Uncheck the **Configure for HTTPS** checkbox.

Use the `--no-https` option. For example

```
dotnet new webapp --no-https
```

[]()

## Trust the ASP.NET Core HTTPS development certificate on Windows and macOS

For the Firefox browser, see the next section.

The .NET Core SDK includes an HTTPS development certificate. The certificate is installed as part of the first-run experience. For example, `dotnet --info` produces a variation of the following output:

```
ASP.NET Core
------------
Successfully installed the ASP.NET Core HTTPS Development Certificate.
To trust the certificate run 'dotnet dev-certs https --trust' (Windows and macOS only).
For establishing trust on other platforms refer to the platform specific documentation.
For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054.
```

Installing the .NET Core SDK installs the ASP.NET Core HTTPS development certificate to the local user certificate store. The certificate has been installed, but it's not trusted. To trust the certificate, perform the one-time step to run the `dotnet dev-certs` tool:

```
dotnet dev-certs https --trust
```

The following command provides help on the `dotnet dev-certs` tool:

```
dotnet dev-certs https --help
```

Warning

Do not create a development certificate in an environment that will be redistributed, such as a container image or virtual machine. Doing so can lead to spoofing and elevation of privilege. To help prevent this, set the `DOTNET_GENERATE_ASPNET_CERTIFICATE` environment variable to `false` prior to calling the .NET CLI for the first time. This will skip the automatic generation of the ASP.NET Core development certificate during the CLI's first-run experience.

[]()

### Trust the HTTPS certificate with Firefox to prevent SEC\_ERROR\_INADEQUATE\_KEY\_USAGE error

The Firefox browser uses its own certificate store, and therefore doesn't trust the [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview) or [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) developer certificates.

There are two approaches to trusting the HTTPS certificate with Firefox, create a policy file or configure with the FireFox browser. Configuring with the browser creates the policy file, so the two approaches are equivalent.

#### Create a policy file to trust HTTPS certificate with Firefox

Create a policy file (`policies.json`) at:

* Windows: `%PROGRAMFILES%\Mozilla Firefox\distribution\`
* MacOS: `Firefox.app/Contents/Resources/distribution`
* Linux: See [Trust the certificate with Firefox on Linux](#trust-ff-linux) in this article.

Add the following JSON to the Firefox policy file:

```
{
  "policies": {
    "Certificates": {
      "ImportEnterpriseRoots": true
    }
  }
}
```

The preceding policy file makes Firefox trust certificates from the trusted certificates in the Windows certificate store. The next section provides an alternative approach to create the preceding policy file by using the Firefox browser.

[]()

### Configure trust of HTTPS certificate using Firefox browser

Set `security.enterprise_roots.enabled` = `true` using the following instructions:

1. Enter `about:config` in the FireFox browser.
2. Select **Accept the Risk and Continue** if you accept the risk.
3. Select **Show All**
4. Set `security.enterprise_roots.enabled` = `true`
5. Exit and restart Firefox

For more information, see [Setting Up Certificate Authorities (CAs) in Firefox](https://support.mozilla.org/kb/setting-certificate-authorities-firefox) and the [mozilla/policy-templates/README file](https://github.com/mozilla/policy-templates/blob/master/README.md).

## How to set up a developer certificate for Docker

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/6199).

## Trust HTTPS certificate on Linux

Establishing trust is distribution and browser specific. The following sections provide instructions for some popular distributions and the Chromium browsers (Edge and Chrome) and for Firefox.

### Ubuntu trust the certificate for service-to-service communication

The following instructions don't work for some Ubuntu versions, such as 20.04. For more information, see GitHub issue [dotnet/AspNetCore.Docs #23686](https://github.com/dotnet/AspNetCore.Docs/issues/23686).

1. Install [OpenSSL](https://www.openssl.org/) 1.1.1h or later. See your distribution for instructions on how to update OpenSSL.

2. Run the following commands:

   ```
   dotnet dev-certs https
   sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
   sudo update-ca-certificates
   ```

The preceding commands:

* Ensure the current user's developer certificate is created.
* Exports the certificate with elevated permissions needed for the `ca-certificates` folder, using the current user's environment.
* Removing the `-E` flag exports the root user certificate, generating it if necessary. Each newly generated certificate has a different thumbprint. When running as root, `sudo` and `-E` are not needed.

The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

[]()

### Trust HTTPS certificate on Linux using Edge or Chrome

* [Ubuntu](#tabpanel_2_linux-ubuntu)
* [Red Hat Enterprise Linux](#tabpanel_2_linux-rhel)
* [SUSE Linux Enterprise Server](#tabpanel_2_linux-sles)

For chromium browsers on Linux:

* Install the `libnss3-tools` for your distribution.

* Create or verify the `$HOME/.pki/nssdb` folder exists on the machine.

* Export the certificate with the following command:

  ```
  dotnet dev-certs https
  sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
  ```

  The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

* Run the following commands:

  ```
  certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n localhost -i /usr/local/share/ca-certificates/aspnet/https.crt
  ```

* Exit and restart the browser.

[]()

#### Trust the certificate with Firefox on Linux

* Export the certificate with the following command:

  ```
  dotnet dev-certs https
  sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
  ```

  The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

* Create a JSON file at `/usr/lib/firefox/distribution/policies.json` with the following command:

```
cat <<EOF | sudo tee /usr/lib/firefox/distribution/policies.json
{
    "policies": {
        "Certificates": {
            "Install": [
                "/usr/local/share/ca-certificates/aspnet/https.crt"
            ]
        }
    }
}
EOF
```

Note: Ubuntu 21.10 Firefox comes as a snap package and the installation folder is `/snap/firefox/current/usr/lib/firefox`.

See [Configure trust of HTTPS certificate using Firefox browser](#trust-ff-ba) in this article for an alternative way to configure the policy file using the browser.

Warning

The following instructions are intended for development purposes only. Do not use the certificates generated in these instructions for a production environment.

These instructions use Mozilla's *legacy* tool `certutil` at `https://firefox-source-docs.mozilla.org/security/nss/legacy/tools/nss_tools_certutil/index.html`. Instructions may be updated as modern utilities and practices are discovered.

Caution

Improper use of TLS certificates could lead to spoofing.

Tip

Instructions for valid production certificates can be found in the RHEL Documentation. [RHEL8 TLS Certificates](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/securing_networks/index#creating-and-managing-tls-keys-and-certificates_securing-networks) [RHEL9 TLS Certificates](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html-single/securing_networks/index#creating-and-managing-tls-keys-and-certificates_securing-networks) [RHEL9 Certificate System](https://access.redhat.com/documentation/en-us/red_hat_certificate_system/9)

### Install Dependencies

```
dnf install nss-tools
```

### Export The ASP.NET Core Development Certificate

Important

Replace `${ProjectDirectory}` with your projects directory. Replace `${CertificateName}` with a name you'll be able to identify in the future.

```
cd ${ProjectDirectory}
dotnet dev-certs https -ep ${ProjectDirectory}/${CertificateName}.crt --format PEM
```

Caution

If using git, add your certificate to your `${ProjectDirectory}/.gitignore` or `${ProjectDirectory}/.git/info/exclude`. View the [git documentation](https://git-scm.com/docs/gitignore) for information about these files.

Tip

You can move your exported certificate outside of your Git repository and replace the occurrences of `${ProjectDirectory}`, in the following instructions, with the new location.

### Import The ASP.NET Core Development Certificate

Important

Replace `${UserProfile}` with the profile you intend to use. Do not replace `$HOME`, it is the environment variable to your user directory.

#### Chromium-based Browsers

```
certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
```

#### Mozilla Firefox

```
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -A -t "P,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -A -t "C,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
```

#### Create An Alias To Test With Curl

Important

Don't delete the exported certificate if you plan to test with curl. You'll need to create an alias referencing it in your `$SHELL`'s profile

```
alias curl="curl --cacert ${ProjectDirectory}/${CertificateName}.crt"
```

### Cleaning up the Development Certificates

```
certutil -d sql:$HOME/.pki/nssdb -D -n ${CertificateName}
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -D -n ${CertificateName}
rm ${ProjectDirectory}/${CertificateName}.crt
dotnet dev-certs https --clean
```

Note

Remove the curl alias you created earlier

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/28292)

[]()

### Trust the certificate with Fedora 34

See:

* [This GitHub comment](https://github.com/dotnet/aspnetcore/issues/32361#issuecomment-837111639)
* [Fedora: Using Shared System Certificates](https://docs.fedoraproject.org/en-US/quick-docs/using-shared-system-certificates/)
* [Set up a .NET development environment](https://fedoramagazine.org/set-up-a-net-development-environment/) on Fedora.

### Trust the certificate with other distros

See [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/32842).

## Trust HTTPS certificate from Windows Subsystem for Linux

The following instructions don't work for some Linux distributions, such as Ubuntu 20.04. For more information, see GitHub issue [dotnet/AspNetCore.Docs #23686](https://github.com/dotnet/AspNetCore.Docs/issues/23686).

The [Windows Subsystem for Linux (WSL)](/en-us/windows/wsl/about) generates an HTTPS self-signed development certificate, which by default isn't trusted in Windows. The easiest way to have Windows trust the WSL certificate, is to configure WSL to use the same certificate as Windows:

* On ***Windows***, export the developer certificate to a file:

  ```
  dotnet dev-certs https -ep https.pfx -p $CREDENTIAL_PLACEHOLDER$ --trust
  ```

  Where `$CREDENTIAL_PLACEHOLDER$` is a password.

* In a WSL window, import the exported certificate on the WSL instance:

  ```
  dotnet dev-certs https --clean --import <<path-to-pfx>> --password $CREDENTIAL_PLACEHOLDER$
  ```

The preceding approach is a one time operation per certificate and per WSL distribution. It's easier than exporting the certificate over and over. If you update or regenerate the certificate on windows, you might need to run the preceding commands again.

[]()

## Troubleshoot certificate problems such as certificate not trusted

This section provides help when the ASP.NET Core HTTPS development certificate has been [installed and trusted](#trust), but you still have browser warnings that the certificate is not trusted. The ASP.NET Core HTTPS development certificate is used by [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0).

To repair the IIS Express certificate, see [this Stackoverflow](https://stackoverflow.com/a/20048613/502537) issue.

### All platforms - certificate not trusted

Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances open. Open a new browser window to app. Certificate trust is cached by browsers.

### dotnet dev-certs https --clean Fails

The preceding commands solve most browser trust issues. If the browser is still not trusting the certificate, follow the platform-specific suggestions that follow.

### Docker - certificate not trusted

* Delete the *C:\Users{USER}\AppData\Roaming\ASP.NET\Https* folder.
* Clean the solution. Delete the *bin* and *obj* folders.
* Restart the development tool. For example, Visual Studio or Visual Studio Code.

### Windows - certificate not trusted

* Check the certificates in the certificate store. There should be a `localhost` certificate with the `ASP.NET Core HTTPS development certificate` friendly name both under `Current User > Personal > Certificates` and `Current User > Trusted root certification authorities > Certificates`
* Remove all the found certificates from both Personal and Trusted root certification authorities. Do **not** remove the IIS Express localhost certificate.
* Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances open. Open a new browser window to app.

### OS X - certificate not trusted

* Open KeyChain Access.
* Select the System keychain.
* Check for the presence of a localhost certificate.
* Check that it contains a `+` symbol on the icon to indicate it's trusted for all users.
* Remove the certificate from the system keychain.
* Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances open. Open a new browser window to app.

See [HTTPS Error using IIS Express (dotnet/AspNetCore #16892)](https://github.com/dotnet/AspNetCore/issues/16892) for troubleshooting certificate issues with Visual Studio.

### Linux certificate not trusted

Check that the certificate being configured for trust is the user HTTPS developer certificate that will be used by the Kestrel server.

Check the current user default HTTPS developer Kestrel certificate at the following location:

```
ls -la ~/.dotnet/corefx/cryptography/x509stores/my
```

The HTTPS developer Kestrel certificate file is the SHA1 thumbprint. When the file is deleted via `dotnet dev-certs https --clean`, it's regenerated when needed with a different thumbprint. Check the thumbprint of the exported certificate matches with the following command:

```
openssl x509 -noout -fingerprint -sha1 -inform pem -in /usr/local/share/ca-certificates/aspnet/https.crt
```

If the certificate doesn't match, it could be one of the following:

* An old certificate.
* An exported a developer certificate for the root user. For this case, export the certificate.

The root user certificate can be checked at:

```
ls -la /root/.dotnet/corefx/cryptography/x509stores/my
```

### IIS Express SSL certificate used with Visual Studio

To fix problems with the IIS Express certificate, select **Repair** from the Visual Studio installer. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/16892).

### Group policy prevents self-signed certificates from being trusted

In some cases, group policy may prevent self-signed certificates from being trusted. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/21173).

## Additional information

* [Configure ASP.NET Core to work with proxy servers and load balancers](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [Host ASP.NET Core on Linux with Nginx: HTTPS configuration](../host-and-deploy/linux-nginx?view=aspnetcore-10.0#https-configuration)
* [How to Set Up SSL on IIS](/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis)
* [Configure endpoints for Kestrel web server](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0)
* [OWASP HSTS browser support](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html#browser-support)

Warning

## API projects

Do **not** use [RequireHttpsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requirehttpsattribute) on Web APIs that receive sensitive information. `RequireHttpsAttribute` uses HTTP status codes to redirect browsers from HTTP to HTTPS. API clients may not understand or obey redirects from HTTP to HTTPS. Such clients may send information over HTTP. Web APIs should either:

* Not listen on HTTP.
* Close the connection with status code 400 (Bad Request) and not serve the request.

To disable HTTP redirection in an API, set the `ASPNETCORE_URLS` environment variable or use the `--urls` command line flag. For more information, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0) and [5 ways to set the URLs for an ASP.NET Core app](https://andrewlock.net/5-ways-to-set-the-urls-for-an-aspnetcore-app/) by Andrew Lock.

## HSTS and API projects

The default API projects don't include [HSTS](#hsts) because HSTS is generally a browser only instruction. Other callers, such as phone or desktop apps, do **not** obey the instruction. Even within browsers, a single authenticated call to an API over HTTP has risks on insecure networks. The secure approach is to configure API projects to only listen to and respond over HTTPS.

## Require HTTPS

We recommend that production ASP.NET Core web apps use:

* HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) to redirect HTTP requests to HTTPS.
* HSTS middleware ([UseHsts](#http-strict-transport-security-hsts-protocol)) to send HTTP Strict Transport Security (HSTS) protocol headers to clients.

Note

Apps deployed in a reverse proxy configuration allow the proxy to handle connection security (HTTPS). If the proxy also handles HTTPS redirection, there's no need to use HTTPS redirection middleware. If the proxy server also handles writing HSTS headers (for example, [native HSTS support in IIS 10.0 (1709) or later](/en-us/iis/get-started/whats-new-in-iis-10-version-1709/iis-10-version-1709-hsts#iis-100-version-1709-native-hsts-support)), HSTS middleware isn't required by the app. For more information, see [Opt-out of HTTPS/HSTS on project creation](#opt-out-of-httpshsts-on-project-creation).

### UseHttpsRedirection

The following code calls `UseHttpsRedirection` in the `Startup` class:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // 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.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

The preceding highlighted code:

* Uses the default [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-redirectstatuscode) ([Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect)).
* Uses the default [HttpsRedirectionOptions.HttpsPort](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.httpsport#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-httpsport) (null) unless overridden by the `ASPNETCORE_HTTPS_PORT` environment variable or [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

We recommend using temporary redirects rather than permanent redirects. Link caching can cause unstable behavior in development environments. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, see the [Configure permanent redirects in production](#configure-permanent-redirects-in-production) section. We recommend using [HSTS](#http-strict-transport-security-hsts-protocol) to signal to clients that only secure resource requests should be sent to the app (only in production).

### Port configuration

A port must be available for the middleware to redirect an insecure request to HTTPS. If no port is available:

* Redirection to HTTPS doesn't occur.
* The middleware logs the warning "Failed to determine the https port for redirect."

Specify the HTTPS port using any of the following approaches:

* Set [HttpsRedirectionOptions.HttpsPort](#options).

* Set the `https_port` [host setting](../fundamentals/host/generic-host?view=aspnetcore-10.0#https_port):

  * In host configuration.

  * By setting the `ASPNETCORE_HTTPS_PORT` environment variable.

  * By adding a top-level entry in `appsettings.json`:

    ```
    {
        "https_port": 443,
        "Logging": {
            "LogLevel": {
                "Default": "Information",
                "Microsoft": "Warning",
                "Microsoft.Hosting.Lifetime": "Information"
            }
        },
        "AllowedHosts": "*"
    }
    ```

* Indicate a port with the secure scheme using the [ASPNETCORE\_URLS environment variable](../fundamentals/host/generic-host?view=aspnetcore-10.0#urls). The environment variable configures the server. The middleware indirectly discovers the HTTPS port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature). This approach doesn't work in reverse proxy deployments.

* In development, set an HTTPS URL in `launchsettings.json`. Enable HTTPS when IIS Express is used.

* Configure an HTTPS URL endpoint for a public-facing edge deployment of [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server or [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) server. Only **one HTTPS port** is used by the app. The middleware discovers the port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

Note

When an app is run in a reverse proxy configuration, [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature) isn't available. Set the port using one of the other approaches described in this section.

### Edge deployments

When Kestrel or HTTP.sys is used as a public-facing edge server, Kestrel or HTTP.sys must be configured to listen on both:

* The secure port where the client is redirected (typically, 443 in production and 5001 in development).
* The insecure port (typically, 80 in production and 5000 in development).

The insecure port must be accessible by the client in order for the app to receive an insecure request and redirect the client to the secure port.

For more information, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration) or [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

### Deployment scenarios

Any firewall between the client and server must also have communication ports open for traffic.

If requests are forwarded in a reverse proxy configuration, use [forwarded headers middleware](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0) before calling HTTPS redirection middleware. Forwarded headers middleware updates the `Request.Scheme`, using the `X-Forwarded-Proto` header. The middleware permits redirect URIs and other security policies to work correctly. When forwarded headers middleware isn't used, the backend app might not receive the correct scheme and end up in a redirect loop. A common end user error message is that too many redirects have occurred.

When deploying to Azure App Service, follow the guidance in [Tutorial: Bind an existing custom SSL certificate to Azure Web Apps](/en-us/azure/app-service/app-service-web-tutorial-custom-ssl).

### Options

The following highlighted code calls [AddHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpsredirectionservicesextensions.addhttpsredirection) to configure middleware options:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddHsts(options =>
    {
        options.Preload = true;
        options.IncludeSubDomains = true;
        options.MaxAge = TimeSpan.FromDays(60);
        options.ExcludedHosts.Add("example.com");
        options.ExcludedHosts.Add("www.example.com");
    });

    services.AddHttpsRedirection(options =>
    {
        options.RedirectStatusCode = (int) HttpStatusCode.TemporaryRedirect;
        options.HttpsPort = 5001;
    });
}
```

Calling `AddHttpsRedirection` is only necessary to change the values of `HttpsPort` or `RedirectStatusCode`.

The preceding highlighted code:

* Sets [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode) to [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect), which is the default value. Use the fields of the [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) class for assignments to `RedirectStatusCode`.
* Sets the HTTPS port to 5001.

#### Configure permanent redirects in production

The middleware defaults to sending a [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) with all redirects. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, wrap the middleware options configuration in a conditional check for a non-`Development` environment.

When configuring services in `Startup.cs`:

```
public void ConfigureServices(IServiceCollection services)
{
    // IWebHostEnvironment (stored in _env) is injected into the Startup class.
    if (!_env.IsDevelopment())
    {
        services.AddHttpsRedirection(options =>
        {
            options.RedirectStatusCode = (int) HttpStatusCode.PermanentRedirect;
            options.HttpsPort = 443;
        });
    }
}
```

## HTTPS redirection middleware alternative approach

An alternative to using HTTPS redirection middleware (`UseHttpsRedirection`) is to use URL rewriting middleware (`AddRedirectToHttps`). `AddRedirectToHttps` can also set the status code and port when the redirect is executed. For more information, see [URL rewriting middleware](../fundamentals/url-rewriting?view=aspnetcore-10.0).

When redirecting to HTTPS without the requirement for additional redirect rules, we recommend using HTTPS redirection middleware (`UseHttpsRedirection`) described in this article.

[]()

## HTTP Strict Transport Security (HSTS) protocol

Per [OWASP](https://owasp.org/about), [HTTP Strict Transport Security (HSTS)](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html) is an opt-in security enhancement that's specified by a web app through the use of a response header. When a [browser that supports HSTS](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html#browser-support) receives this header:

* The browser stores configuration for the domain that prevents sending any communication over HTTP. The browser forces all communication over HTTPS.
* The browser prevents the user from using untrusted or invalid certificates. The browser disables prompts that allow a user to temporarily trust such a certificate.

Because HSTS is enforced by the client, it has some limitations:

* The client must support HSTS.
* HSTS requires at least one successful HTTPS request to establish the HSTS policy.
* The application must check every HTTP request and redirect or reject the HTTP request.

ASP.NET Core implements HSTS with the `UseHsts` extension method. The following code calls `UseHsts` when the app isn't in [development mode](../fundamentals/environments?view=aspnetcore-10.0):

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // 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.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

`UseHsts` isn't recommended in development because the HSTS settings are highly cacheable by browsers. By default, `UseHsts` excludes the local loopback address.

For production environments that are implementing HTTPS for the first time, set the initial [HstsOptions.MaxAge](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.hstsoptions.maxage) to a small value using one of the [TimeSpan](/en-us/dotnet/api/system.timespan) methods. Set the value from hours to no more than a single day in case you need to revert the HTTPS infrastructure to HTTP. After you're confident in the sustainability of the HTTPS configuration, increase the HSTS `max-age` value; a commonly used value is one year.

The following code:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddHsts(options =>
    {
        options.Preload = true;
        options.IncludeSubDomains = true;
        options.MaxAge = TimeSpan.FromDays(60);
        options.ExcludedHosts.Add("example.com");
        options.ExcludedHosts.Add("www.example.com");
    });

    services.AddHttpsRedirection(options =>
    {
        options.RedirectStatusCode = (int) HttpStatusCode.TemporaryRedirect;
        options.HttpsPort = 5001;
    });
}
```

* Sets the preload parameter of the `Strict-Transport-Security` header. Preload isn't part of the [RFC HSTS specification](https://tools.ietf.org/html/rfc6797), but is supported by web browsers to preload HSTS sites on fresh install. For more information, see <https://hstspreload.org/>.
* Enables [includeSubDomain](https://tools.ietf.org/html/rfc6797#section-6.1.2), which applies the HSTS policy to Host subdomains.
* Explicitly sets the `max-age` parameter of the `Strict-Transport-Security` header to 60 days. If not set, defaults to 30 days. For more information, see the [max-age directive](https://tools.ietf.org/html/rfc6797#section-6.1.1).
* Adds `example.com` to the list of hosts to exclude.

`UseHsts` excludes the following loopback hosts:

* `localhost` : The IPv4 loopback address.
* `127.0.0.1` : The IPv4 loopback address.
* `[::1]` : The IPv6 loopback address.

## Opt-out of HTTPS/HSTS on project creation

In some backend service scenarios where connection security is handled at the public-facing edge of the network, configuring connection security at each node isn't required. Web apps that are generated from the templates in Visual Studio or from the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command enable [HTTPS redirection](#require-https) and [HSTS](#http-strict-transport-security-hsts-protocol). For deployments that don't require these scenarios, you can opt-out of HTTPS/HSTS when the app is created from the template.

To opt-out of HTTPS/HSTS:

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

Uncheck the **Configure for HTTPS** checkbox.

Use the `--no-https` option. For example

```
dotnet new webapp --no-https
```

[]()

## Trust the ASP.NET Core HTTPS development certificate on Windows and macOS

For the Firefox browser, see the next section.

The .NET Core SDK includes an HTTPS development certificate. The certificate is installed as part of the first-run experience. For example, running `dotnet new webapp` for the first time produces a variation of the following output:

```
Installed an ASP.NET Core HTTPS development certificate.
To trust the certificate, run 'dotnet dev-certs https --trust'
Learn about HTTPS: https://aka.ms/dotnet-https
```

Installing the .NET Core SDK installs the ASP.NET Core HTTPS development certificate to the local user certificate store. The certificate has been installed, but it's not trusted. To trust the certificate, perform the one-time step to run the `dotnet dev-certs` tool:

```
dotnet dev-certs https --trust
```

The following command provides help on the `dotnet dev-certs` tool:

```
dotnet dev-certs https --help
```

Warning

Do not create a development certificate in an environment that will be redistributed, such as a container image or virtual machine. Doing so can lead to spoofing and elevation of privilege. To help prevent this, set the `DOTNET_GENERATE_ASPNET_CERTIFICATE` environment variable to `false` prior to calling the .NET CLI for the first time. This will skip the automatic generation of the ASP.NET Core development certificate during the CLI's first-run experience.

[]()

### Trust the HTTPS certificate with Firefox to prevent SEC\_ERROR\_INADEQUATE\_KEY\_USAGE error

The Firefox browser uses its own certificate store, and therefore doesn't trust the [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview) or [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) developer certificates.

There are two approaches to trusting the HTTPS certificate with Firefox, create a policy file or configure with the FireFox browser. Configuring with the browser creates the policy file, so the two approaches are equivalent.

#### Create a policy file to trust HTTPS certificate with Firefox

Create a policy file (`policies.json`) at:

* Windows: `%PROGRAMFILES%\Mozilla Firefox\distribution\`
* MacOS: `Firefox.app/Contents/Resources/distribution`
* Linux: See [Trust the certificate with Firefox on Linux](#trust-ff-linux) later in this article.

Add the following JSON to the Firefox policy file:

```
{
  "policies": {
    "Certificates": {
      "ImportEnterpriseRoots": true
    }
  }
}
```

The preceding policy file makes Firefox trust certificates from the trusted certificates in the Windows certificate store. The next section provides an alternative approach to create the preceding policy file by using the Firefox browser.

[]()

### Configure trust of HTTPS certificate using Firefox browser

Set `security.enterprise_roots.enabled` = `true` using the following instructions:

1. Enter `about:config` in the FireFox browser.
2. Select **Accept the Risk and Continue** if you accept the risk.
3. Select **Show All**.
4. Set `security.enterprise_roots.enabled` = `true`.
5. Exit and restart Firefox.

For more information, see [Setting Up Certificate Authorities (CAs) in Firefox](https://support.mozilla.org/kb/setting-certificate-authorities-firefox) and the [mozilla/policy-templates/README file](https://github.com/mozilla/policy-templates/blob/master/README.md).

## How to set up a developer certificate for Docker

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/6199).

## Trust HTTPS certificate on Linux

Establishing trust is distribution and browser specific. The following sections provide instructions for some popular distributions and the Chromium browsers (Edge and Chrome) and for Firefox.

### Ubuntu trust the certificate for service-to-service communication

1. Install [OpenSSL](https://www.openssl.org/) 1.1.1h or later. See your distribution for instructions on how to update OpenSSL.

2. Run the following commands:

   ```
   dotnet dev-certs https
   sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
   sudo update-ca-certificates
   ```

The preceding commands:

* Ensure the current user's developer certificate is created.
* Export the certificate with elevated permissions needed for the `ca-certificates` folder, using the current user's environment.
* Remove the `-E` flag to export the root user certificate, generating it if necessary. Each newly generated certificate has a different thumbprint. When running as root, `sudo` and `-E` are not needed.

The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

[]()

### Trust HTTPS certificate on Linux using Edge or Chrome

For chromium browsers on Linux:

* Install the `libnss3-tools` for your distribution.

* Create or verify the `$HOME/.pki/nssdb` folder exists on the machine.

* Export the certificate with the following command:

  ```
  dotnet dev-certs https
  sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
  ```

  The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

* Run the following commands:

  ```
  certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n localhost -i /usr/local/share/ca-certificates/aspnet/https.crt
  ```

* Exit and restart the browser.

[]()

### Trust the certificate with Firefox on Linux

* Export the certificate with the following command:

  ```
  dotnet dev-certs https
  sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
  ```

  The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

* Create a JSON file at `/usr/lib/firefox/distribution/policies.json` with the following contents:

```
cat <<EOF | sudo tee /usr/lib/firefox/distribution/policies.json
{
    "policies": {
        "Certificates": {
            "Install": [
                "/usr/local/share/ca-certificates/aspnet/https.crt"
            ]
        }
    }
}
EOF
```

See [Configure trust of HTTPS certificate using Firefox browser](#trust-ff-ba) in this article for an alternative way to configure the policy file using the browser.

[]()

### Trust the certificate with Fedora 34

#### Firefox on Fedora

```
echo 'pref("general.config.filename", "firefox.cfg");
pref("general.config.obscure_value", 0);' > ./autoconfig.js

echo '//Enable policies.json
lockPref("browser.policies.perUserDir", false);' > firefox.cfg

echo "{
    \"policies\": {
        \"Certificates\": {
            \"Install\": [
                \"aspnetcore-localhost-https.crt\"
            ]
        }
    }
}" > policies.json

dotnet dev-certs https -ep localhost.crt --format PEM

sudo mv autoconfig.js /usr/lib64/firefox/
sudo mv firefox.cfg /usr/lib64/firefox/
sudo mv policies.json /usr/lib64/firefox/distribution/
mkdir -p ~/.mozilla/certificates
cp localhost.crt ~/.mozilla/certificates/aspnetcore-localhost-https.crt
rm localhost.crt
```

#### Trust dotnet-to-dotnet on Fedora

```
sudo cp localhost.crt /etc/pki/tls/certs/localhost.pem
sudo update-ca-trust
rm localhost.crt
```

See [this GitHub comment](https://github.com/dotnet/aspnetcore/issues/32361#issuecomment-837111639) for more information.

### Trust the certificate with other distros

See [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/32842).

## Trust HTTPS certificate from Windows Subsystem for Linux

The [Windows Subsystem for Linux (WSL)](/en-us/windows/wsl/about) generates an HTTPS self-signed development certificate. To configure the Windows certificate store to trust the WSL certificate:

* Export the developer certificate to a file on ***Windows***:

  ```
  dotnet dev-certs https -ep C:\<<path-to-folder>>\aspnetcore.pfx -p $CREDENTIAL_PLACEHOLDER$
  ```

  Where `$CREDENTIAL_PLACEHOLDER$` is a password.

* In a WSL window, import the exported certificate on the WSL instance:

  ```
  dotnet dev-certs https --clean --import /mnt/c/<<path-to-folder>>/aspnetcore.pfx -p $CREDENTIAL_PLACEHOLDER$
  ```

The preceding approach is a one time operation per certificate and per WSL distribution. It's easier than exporting the certificate over and over. If you update or regenerate the certificate on windows, you might need to run the preceding commands again.

[]()

## Troubleshoot certificate problems such as certificate not trusted

This section provides help when the ASP.NET Core HTTPS development certificate has been [installed and trusted](#trust), but you still have browser warnings that the certificate is not trusted. The ASP.NET Core HTTPS development certificate is used by [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0).

To repair the IIS Express certificate, see [this Stackoverflow](https://stackoverflow.com/a/20048613/502537) issue.

### All platforms - certificate not trusted

Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances that are open. Open a new browser window to the app. Certificate trust is cached by browsers.

### dotnet dev-certs https --clean fails

The preceding commands solve most browser trust issues. If the browser is still not trusting the certificate, follow the platform-specific suggestions that follow.

### Docker - certificate not trusted

* Delete the *C:\Users{USER}\AppData\Roaming\ASP.NET\Https* folder.
* Clean the solution. Delete the *bin* and *obj* folders.
* Restart the development tool. For example, Visual Studio, Visual Studio Code, or Visual Studio for Mac.

### Windows - certificate not trusted

* Check the certificates in the certificate store. There should be a `localhost` certificate with the `ASP.NET Core HTTPS development certificate` friendly name both under `Current User > Personal > Certificates` and `Current User > Trusted root certification authorities > Certificates`
* Remove all the found certificates from both Personal and Trusted root certification authorities. Do **not** remove the IIS Express localhost certificate.
* Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances that are open. Open a new browser window to the app. Certificate trust is cached by browsers.

### OS X - certificate not trusted

* Open KeyChain Access.
* Select the System keychain.
* Check for the presence of a localhost certificate.
* Check that it contains a `+` symbol on the icon to indicate it's trusted for all users.
* Remove the certificate from the system keychain.
* Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances that are open. Open a new browser window to the app. Certificate trust is cached by browsers.

See [HTTPS Error using IIS Express (dotnet/AspNetCore #16892)](https://github.com/dotnet/AspNetCore/issues/16892) for troubleshooting certificate issues with Visual Studio.

### Linux certificate not trusted

Check that the certificate being configured for trust is the user HTTPS developer certificate that will be used by the Kestrel server.

Check the current user default HTTPS developer Kestrel certificate at the following location:

```
ls -la ~/.dotnet/corefx/cryptography/x509stores/my
```

The HTTPS developer Kestrel certificate file is the SHA1 thumbprint. When the file is deleted via `dotnet dev-certs https --clean`, it's regenerated when needed with a different thumbprint. Check the thumbprint of the exported certificate matches with the following command:

```
openssl x509 -noout -fingerprint -sha1 -inform pem -in /usr/local/share/ca-certificates/aspnet/https.crt
```

If the certificate doesn't match, it could be one of the following:

* An old certificate.
* An exported a developer certificate for the root user. For this case, export the certificate.

The root user certificate can be checked at:

```
ls -la /root/.dotnet/corefx/cryptography/x509stores/my
```

### IIS Express SSL certificate used with Visual Studio

To fix problems with the IIS Express certificate, select **Repair** from the Visual Studio installer. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/16892).

## Additional information

* [Configure ASP.NET Core to work with proxy servers and load balancers](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [Host ASP.NET Core on Linux with Nginx: HTTPS configuration](../host-and-deploy/linux-nginx?view=aspnetcore-10.0#https-configuration)
* [How to Set Up SSL on IIS](/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis)
* [OWASP HSTS browser support](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html#browser-support)
* [`dotnet dev-certs`](/en-us/dotnet/core/tools/dotnet-dev-certs)

Note

If you're using .NET 9 or later SDK, see the updated Linux procedures in the [.NET 9 version of this article](?view=aspnetcore-9.0\&preserve-view=true).

Warning

## API projects

Do **not** use [RequireHttpsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requirehttpsattribute) on Web APIs that receive sensitive information. `RequireHttpsAttribute` uses HTTP status codes to redirect browsers from HTTP to HTTPS. API clients may not understand or obey redirects from HTTP to HTTPS. Such clients may send information over HTTP. Web APIs should either:

* Not listen on HTTP.
* Close the connection with status code 400 (Bad Request) and not serve the request.

To disable HTTP redirection in an API, set the `ASPNETCORE_URLS` environment variable or use the `--urls` command line flag. For more information, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0) and [8 ways to set the URLs for an ASP.NET Core app](https://andrewlock.net/8-ways-to-set-the-urls-for-an-aspnetcore-app/) by Andrew Lock.

## HSTS and API projects

The default API projects don't include [HSTS](#hsts) because [HSTS](https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security) is generally a browser only instruction. Other callers, such as phone or desktop apps, do **not** obey the instruction. Even within browsers, a single authenticated call to an API over HTTP has risks on insecure networks. The secure approach is to configure API projects to only listen to and respond over HTTPS.

[]()

### HTTP redirection to HTTPS causes ERR\_INVALID\_REDIRECT on the CORS preflight request

Requests to an endpoint using HTTP that are redirected to HTTPS by [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) fail with `ERR_INVALID_REDIRECT` on the CORS preflight request.

API projects can reject HTTP requests rather than use `UseHttpsRedirection` to redirect requests to HTTPS.

## Require HTTPS

We recommend that production ASP.NET Core web apps use:

* HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) to redirect HTTP requests to HTTPS.
* HSTS middleware ([UseHsts](#http-strict-transport-security-hsts-protocol)) to send HTTP Strict Transport Security (HSTS) protocol headers to clients.

Note

Apps deployed in a reverse proxy configuration allow the proxy to handle connection security (HTTPS). If the proxy also handles HTTPS redirection, there's no need to use HTTPS redirection middleware. If the proxy server also handles writing HSTS headers (for example, [native HSTS support in IIS 10.0 (1709) or later](/en-us/iis/get-started/whats-new-in-iis-10-version-1709/iis-10-version-1709-hsts#iis-100-version-1709-native-hsts-support)), HSTS middleware isn't required by the app. For more information, see [Opt-out of HTTPS/HSTS on project creation](#opt-out-of-httpshsts-on-project-creation).

### HTTPS redirection middleware (`UseHttpsRedirection`)

The following code calls [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) in the `Program.cs` file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding highlighted code:

* Uses the default [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-redirectstatuscode) ([Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect)).
* Uses the default [HttpsRedirectionOptions.HttpsPort](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.httpsport#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-httpsport) (null) unless overridden by the `ASPNETCORE_HTTPS_PORT` environment variable or [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

We recommend using temporary redirects rather than permanent redirects. Link caching can cause unstable behavior in development environments. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, see the [Configure permanent redirects in production](#configure-permanent-redirects-in-production) section. We recommend using [HSTS](#http-strict-transport-security-hsts-protocol) to signal to clients that only secure resource requests should be sent to the app (only in production).

### Port configuration

A port must be available for the middleware to redirect an insecure request to HTTPS. If no port is available:

* Redirection to HTTPS doesn't occur.
* The middleware logs the warning "Failed to determine the https port for redirect."

Specify the HTTPS port using any of the following approaches:

* Set [HttpsRedirectionOptions.HttpsPort](#options).

* Set the `https_port` [host setting](../fundamentals/host/generic-host?view=aspnetcore-10.0#https_port):

  * In host configuration.

  * By setting the `ASPNETCORE_HTTPS_PORT` environment variable.

  * By adding a top-level entry in `appsettings.json`:

    ```
    {
      "https_port": 443,
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*"
    }
    ```

* Indicate a port with the secure scheme using the [ASPNETCORE\_URLS environment variable](../fundamentals/host/generic-host?view=aspnetcore-10.0#urls). The environment variable configures the server. The middleware indirectly discovers the HTTPS port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature). This approach doesn't work in reverse proxy deployments.

* The ASP.NET Core web templates set an HTTPS URL in `Properties/launchsettings.json` for both Kestrel and IIS Express. `launchsettings.json` is only used on the local machine.

* Configure an HTTPS URL endpoint for a public-facing edge deployment of [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server or [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) server. Only **one HTTPS port** is used by the app. The middleware discovers the port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

Note

When an app is run in a reverse proxy configuration, [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature) isn't available. Set the port using one of the other approaches described in this section.

### Edge deployments

When [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) or [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) is used as a public-facing edge server, Kestrel or HTTP.sys must be configured to listen on both:

* The secure port where the client is redirected (typically, 443 in production and 5001 in development).
* The insecure port (typically, 80 in production and 5000 in development).

The insecure port must be accessible by the client in order for the app to receive an insecure request and redirect the client to the secure port.

For more information, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration) or [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

### Deployment scenarios

Any firewall between the client and server must also have communication ports open for traffic.

If requests are forwarded in a reverse proxy configuration, use [forwarded headers middleware](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0) before calling HTTPS redirection middleware. Forwarded headers middleware updates the `Request.Scheme`, using the `X-Forwarded-Proto` header. The middleware permits redirect URIs and other security policies to work correctly. When forwarded headers middleware isn't used, the backend app might not receive the correct scheme and end up in a redirect loop. A common end user error message is that too many redirects have occurred.

When deploying to Azure App Service, follow the guidance in [Tutorial: Bind an existing custom SSL certificate to Azure Web Apps](/en-us/azure/app-service/app-service-web-tutorial-custom-ssl).

### Options

The following highlighted code calls [AddHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpsredirectionservicesextensions.addhttpsredirection) to configure middleware options:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = Status307TemporaryRedirect;
    options.HttpsPort = 5001;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

Calling `AddHttpsRedirection` is only necessary to change the values of `HttpsPort` or `RedirectStatusCode`.

The preceding highlighted code:

* Sets [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode) to [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect), which is the default value. Use the fields of the [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) class for assignments to `RedirectStatusCode`.
* Sets the HTTPS port to 5001.

#### Configure permanent redirects in production

The middleware defaults to sending a [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) with all redirects. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, wrap the middleware options configuration in a conditional check for a non-`Development` environment.

When configuring services in `Program.cs`:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (!builder.Environment.IsDevelopment())
{
    builder.Services.AddHttpsRedirection(options =>
    {
        options.RedirectStatusCode = Status308PermanentRedirect;
        options.HttpsPort = 443;
    });
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");

    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

## HTTPS redirection middleware alternative approach

An alternative to using HTTPS redirection middleware (`UseHttpsRedirection`) is to use URL rewriting middleware (`AddRedirectToHttps`). `AddRedirectToHttps` can also set the status code and port when the redirect is executed. For more information, see [URL rewriting middleware](../fundamentals/url-rewriting?view=aspnetcore-10.0).

When redirecting to HTTPS without the requirement for additional redirect rules, we recommend using HTTPS redirection middleware (`UseHttpsRedirection`) described in this article.

[]()

## HTTP Strict Transport Security (HSTS) protocol

Per [OWASP](https://owasp.org/about), [HTTP Strict Transport Security (HSTS)](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html) is an opt-in security enhancement that's specified by a web app through the use of a response header. When a [browser that supports HSTS](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html#browser-support) receives this header:

* The browser stores configuration for the domain that prevents sending any communication over HTTP. The browser forces all communication over HTTPS.
* The browser prevents the user from using untrusted or invalid certificates. The browser disables prompts that allow a user to temporarily trust such a certificate.

Because [HSTS](https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security) is enforced by the client, it has some limitations:

* The client must support HSTS.
* HSTS requires at least one successful HTTPS request to establish the HSTS policy.
* The application must check every HTTP request and redirect or reject the HTTP request.

ASP.NET Core implements HSTS with the [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts) extension method. The following code calls `UseHsts` when the app isn't in [development mode](../fundamentals/environments?view=aspnetcore-10.0):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

`UseHsts` isn't recommended in development because the HSTS settings are highly cacheable by browsers. By default, `UseHsts` excludes the local loopback address.

For production environments that are implementing HTTPS for the first time, set the initial [HstsOptions.MaxAge](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.hstsoptions.maxage) to a small value using one of the [TimeSpan](/en-us/dotnet/api/system.timespan) methods. Set the value from hours to no more than a single day in case you need to revert the HTTPS infrastructure to HTTP. After you're confident in the sustainability of the HTTPS configuration, increase the HSTS `max-age` value; a commonly used value is one year.

The following highlighted code:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = Status307TemporaryRedirect;
    options.HttpsPort = 5001;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

* Sets the preload parameter of the `Strict-Transport-Security` header. Preload isn't part of the [RFC HSTS specification](https://tools.ietf.org/html/rfc6797), but is supported by web browsers to preload HSTS sites on fresh install. For more information, see <https://hstspreload.org/>.
* Enables [includeSubDomain](https://tools.ietf.org/html/rfc6797#section-6.1.2), which applies the HSTS policy to Host subdomains.
* Explicitly sets the `max-age` parameter of the `Strict-Transport-Security` header to 60 days. If not set, defaults to 30 days. For more information, see the [max-age directive](https://tools.ietf.org/html/rfc6797#section-6.1.1).
* Adds `example.com` to the list of hosts to exclude.

`UseHsts` excludes the following loopback hosts:

* `localhost` : The IPv4 loopback address.
* `127.0.0.1` : The IPv4 loopback address.
* `[::1]` : The IPv6 loopback address.

## Opt-out of HTTPS/HSTS on project creation

In some backend service scenarios where connection security is handled at the public-facing edge of the network, configuring connection security at each node isn't required. Web apps that are generated from the templates in Visual Studio or from the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command enable [HTTPS redirection](#require-https) and [HSTS](#http-strict-transport-security-hsts-protocol). For deployments that don't require these scenarios, you can opt-out of HTTPS/HSTS when the app is created from the template.

To opt-out of HTTPS/HSTS:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

Uncheck the **Configure for HTTPS** checkbox.

Use the `--no-https` option. For example

```
dotnet new webapp --no-https
```

[]()

## Trust the ASP.NET Core HTTPS development certificate on Windows and macOS

For the Firefox browser, see the next section.

The .NET Core SDK includes an HTTPS development certificate. The certificate is installed as part of the first-run experience. For example, `dotnet --info` produces a variation of the following output:

```
ASP.NET Core
------------
Successfully installed the ASP.NET Core HTTPS Development Certificate.
To trust the certificate run 'dotnet dev-certs https --trust' (Windows and macOS only).
For establishing trust on other platforms refer to the platform specific documentation.
For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054.
```

Installing the .NET Core SDK installs the ASP.NET Core HTTPS development certificate to the local user certificate store. The certificate has been installed, but it's not trusted. To trust the certificate, perform the one-time step to run the `dotnet dev-certs` tool:

```
dotnet dev-certs https --trust
```

The following command provides help on the `dotnet dev-certs` tool:

```
dotnet dev-certs https --help
```

Warning

Do not create a development certificate in an environment that will be redistributed, such as a container image or virtual machine. Doing so can lead to spoofing and elevation of privilege. To help prevent this, set the `DOTNET_GENERATE_ASPNET_CERTIFICATE` environment variable to `false` prior to calling the .NET CLI for the first time. This will skip the automatic generation of the ASP.NET Core development certificate during the CLI's first-run experience.

[]()

### Trust the HTTPS certificate with Firefox to prevent SEC\_ERROR\_INADEQUATE\_KEY\_USAGE error

The Firefox browser uses its own certificate store, and therefore doesn't trust the [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview) or [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) developer certificates.

There are two approaches to trusting the HTTPS certificate with Firefox, create a policy file or configure with the FireFox browser. Configuring with the browser creates the policy file, so the two approaches are equivalent.

#### Create a policy file to trust HTTPS certificate with Firefox

Create a policy file (`policies.json`) at:

* Windows: `%PROGRAMFILES%\Mozilla Firefox\distribution\`
* MacOS: `Firefox.app/Contents/Resources/distribution`
* Linux: See [Trust the certificate with Firefox on Linux](#trust-ff-linux) in this article.

Add the following JSON to the Firefox policy file:

```
{
  "policies": {
    "Certificates": {
      "ImportEnterpriseRoots": true
    }
  }
}
```

The preceding policy file makes Firefox trust certificates from the trusted certificates in the Windows certificate store. The next section provides an alternative approach to create the preceding policy file by using the Firefox browser.

[]()

### Configure trust of HTTPS certificate using Firefox browser

Set `security.enterprise_roots.enabled` = `true` using the following instructions:

1. Enter `about:config` in the FireFox browser.
2. Select **Accept the Risk and Continue** if you accept the risk.
3. Select **Show All**
4. Set `security.enterprise_roots.enabled` = `true`
5. Exit and restart Firefox

For more information, see [Setting Up Certificate Authorities (CAs) in Firefox](https://support.mozilla.org/kb/setting-certificate-authorities-firefox) and the [mozilla/policy-templates/README file](https://github.com/mozilla/policy-templates/blob/master/README.md).

## How to set up a developer certificate for Docker

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/6199).

## Trust HTTPS certificate on Linux

Establishing trust is distribution and browser specific. The following sections provide instructions for some popular distributions and the Chromium browsers (Edge and Chrome) and for Firefox.

### Ubuntu trust the certificate for service-to-service communication

The following instructions don't work for some Ubuntu versions, such as 20.04. For more information, see GitHub issue [dotnet/AspNetCore.Docs #23686](https://github.com/dotnet/AspNetCore.Docs/issues/23686).

1. Install [OpenSSL](https://www.openssl.org/) 1.1.1h or later. See your distribution for instructions on how to update OpenSSL.

2. Run the following commands:

   ```
   dotnet dev-certs https
   sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
   sudo update-ca-certificates
   ```

The preceding commands:

* Ensure the current user's developer certificate is created.
* Exports the certificate with elevated permissions needed for the `ca-certificates` folder, using the current user's environment.
* Removing the `-E` flag exports the root user certificate, generating it if necessary. Each newly generated certificate has a different thumbprint. When running as root, `sudo` and `-E` are not needed.

The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

[]()

### Trust HTTPS certificate on Linux using Edge or Chrome

* [Ubuntu](#tabpanel_2_linux-ubuntu)
* [Red Hat Enterprise Linux](#tabpanel_2_linux-rhel)
* [SUSE Linux Enterprise Server](#tabpanel_2_linux-sles)

For chromium browsers on Linux:

* Install the `libnss3-tools` for your distribution.

* Create or verify the `$HOME/.pki/nssdb` folder exists on the machine.

* Export the certificate with the following command:

  ```
  dotnet dev-certs https
  sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
  ```

  The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

* Run the following commands:

  ```
  certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n localhost -i /usr/local/share/ca-certificates/aspnet/https.crt
  ```

* Exit and restart the browser.

[]()

#### Trust the certificate with Firefox on Linux

* Export the certificate with the following command:

  ```
  dotnet dev-certs https
  sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
  ```

  The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

* Create a JSON file at `/usr/lib/firefox/distribution/policies.json` with the following command:

```
cat <<EOF | sudo tee /usr/lib/firefox/distribution/policies.json
{
    "policies": {
        "Certificates": {
            "Install": [
                "/usr/local/share/ca-certificates/aspnet/https.crt"
            ]
        }
    }
}
EOF
```

Note: Ubuntu 21.10 Firefox comes as a snap package and the installation folder is `/snap/firefox/current/usr/lib/firefox`.

See [Configure trust of HTTPS certificate using Firefox browser](#trust-ff-ba) in this article for an alternative way to configure the policy file using the browser.

Warning

The following instructions are intended for development purposes only. Do not use the certificates generated in these instructions for a production environment.

These instructions use Mozilla's *legacy* tool `certutil` at `https://firefox-source-docs.mozilla.org/security/nss/legacy/tools/nss_tools_certutil/index.html`. Instructions may be updated as modern utilities and practices are discovered.

Caution

Improper use of TLS certificates could lead to spoofing.

Tip

Instructions for valid production certificates can be found in the RHEL Documentation. [RHEL8 TLS Certificates](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/securing_networks/index#creating-and-managing-tls-keys-and-certificates_securing-networks) [RHEL9 TLS Certificates](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html-single/securing_networks/index#creating-and-managing-tls-keys-and-certificates_securing-networks) [RHEL9 Certificate System](https://access.redhat.com/documentation/en-us/red_hat_certificate_system/9)

### Install Dependencies

```
dnf install nss-tools
```

### Export The ASP.NET Core Development Certificate

Important

Replace `${ProjectDirectory}` with your projects directory. Replace `${CertificateName}` with a name you'll be able to identify in the future.

```
cd ${ProjectDirectory}
dotnet dev-certs https -ep ${ProjectDirectory}/${CertificateName}.crt --format PEM
```

Caution

If using git, add your certificate to your `${ProjectDirectory}/.gitignore` or `${ProjectDirectory}/.git/info/exclude`. View the [git documentation](https://git-scm.com/docs/gitignore) for information about these files.

Tip

You can move your exported certificate outside of your Git repository and replace the occurrences of `${ProjectDirectory}`, in the following instructions, with the new location.

### Import The ASP.NET Core Development Certificate

Important

Replace `${UserProfile}` with the profile you intend to use. Do not replace `$HOME`, it is the environment variable to your user directory.

#### Chromium-based Browsers

```
certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
```

#### Mozilla Firefox

```
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -A -t "P,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -A -t "C,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
```

#### Create An Alias To Test With Curl

Important

Don't delete the exported certificate if you plan to test with curl. You'll need to create an alias referencing it in your `$SHELL`'s profile

```
alias curl="curl --cacert ${ProjectDirectory}/${CertificateName}.crt"
```

### Cleaning up the Development Certificates

```
certutil -d sql:$HOME/.pki/nssdb -D -n ${CertificateName}
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -D -n ${CertificateName}
rm ${ProjectDirectory}/${CertificateName}.crt
dotnet dev-certs https --clean
```

Note

Remove the curl alias you created earlier

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/28292)

[]()

### Trust the certificate with Fedora 34

See:

* [This GitHub comment](https://github.com/dotnet/aspnetcore/issues/32361#issuecomment-837111639)
* [Fedora: Using Shared System Certificates](https://docs.fedoraproject.org/en-US/quick-docs/using-shared-system-certificates/)
* [Set up a .NET development environment](https://fedoramagazine.org/set-up-a-net-development-environment/) on Fedora.

### Trust the certificate with other distros

See [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/32842).

## Trust HTTPS certificate from Windows Subsystem for Linux

The following instructions don't work for some Linux distributions, such as Ubuntu 20.04. For more information, see GitHub issue [dotnet/AspNetCore.Docs #23686](https://github.com/dotnet/AspNetCore.Docs/issues/23686).

The [Windows Subsystem for Linux (WSL)](/en-us/windows/wsl/about) generates an HTTPS self-signed development certificate, which by default isn't trusted in Windows. The easiest way to have Windows trust the WSL certificate, is to configure WSL to use the same certificate as Windows:

* On ***Windows***, export the developer certificate to a file:

  ```
  dotnet dev-certs https -ep https.pfx -p $CREDENTIAL_PLACEHOLDER$ --trust
  ```

  Where `$CREDENTIAL_PLACEHOLDER$` is a password.

* In a WSL window, import the exported certificate on the WSL instance:

  ```
  dotnet dev-certs https --clean --import <<path-to-pfx>> --password $CREDENTIAL_PLACEHOLDER$
  ```

The preceding approach is a one time operation per certificate and per WSL distribution. It's easier than exporting the certificate over and over. If you update or regenerate the certificate on windows, you might need to run the preceding commands again.

[]()

## Troubleshoot certificate problems such as certificate not trusted

This section provides help when the ASP.NET Core HTTPS development certificate has been [installed and trusted](#trust), but you still have browser warnings that the certificate is not trusted. The ASP.NET Core HTTPS development certificate is used by [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0).

To repair the IIS Express certificate, see [this Stackoverflow](https://stackoverflow.com/a/20048613/502537) issue.

### All platforms - certificate not trusted

Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances open. Open a new browser window to app. Certificate trust is cached by browsers.

### dotnet dev-certs https --clean Fails

The preceding commands solve most browser trust issues. If the browser is still not trusting the certificate, follow the platform-specific suggestions that follow.

### Docker - certificate not trusted

* Delete the *C:\Users{USER}\AppData\Roaming\ASP.NET\Https* folder.
* Clean the solution. Delete the *bin* and *obj* folders.
* Restart the development tool. For example, Visual Studio or Visual Studio Code.

### Windows - certificate not trusted

* Check the certificates in the certificate store. There should be a `localhost` certificate with the `ASP.NET Core HTTPS development certificate` friendly name both under `Current User > Personal > Certificates` and `Current User > Trusted root certification authorities > Certificates`
* Remove all the found certificates from both Personal and Trusted root certification authorities. Do **not** remove the IIS Express localhost certificate.
* Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances open. Open a new browser window to app.

### OS X - certificate not trusted

* Open KeyChain Access.
* Select the System keychain.
* Check for the presence of a localhost certificate.
* Check that it contains a `+` symbol on the icon to indicate it's trusted for all users.
* Remove the certificate from the system keychain.
* Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances open. Open a new browser window to app.

See [HTTPS Error using IIS Express (dotnet/AspNetCore #16892)](https://github.com/dotnet/AspNetCore/issues/16892) for troubleshooting certificate issues with Visual Studio.

### Linux certificate not trusted

Check that the certificate being configured for trust is the user HTTPS developer certificate that will be used by the Kestrel server.

Check the current user default HTTPS developer Kestrel certificate at the following location:

```
ls -la ~/.dotnet/corefx/cryptography/x509stores/my
```

The HTTPS developer Kestrel certificate file is the SHA1 thumbprint. When the file is deleted via `dotnet dev-certs https --clean`, it's regenerated when needed with a different thumbprint. Check the thumbprint of the exported certificate matches with the following command:

```
openssl x509 -noout -fingerprint -sha1 -inform pem -in /usr/local/share/ca-certificates/aspnet/https.crt
```

If the certificate doesn't match, it could be one of the following:

* An old certificate.
* An exported a developer certificate for the root user. For this case, export the certificate.

The root user certificate can be checked at:

```
ls -la /root/.dotnet/corefx/cryptography/x509stores/my
```

### IIS Express SSL certificate used with Visual Studio

To fix problems with the IIS Express certificate, select **Repair** from the Visual Studio installer. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/16892).

### Group policy prevents self-signed certificates from being trusted

In some cases, group policy may prevent self-signed certificates from being trusted. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/21173).

## Additional information

* [Configure ASP.NET Core to work with proxy servers and load balancers](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [Host ASP.NET Core on Linux with Nginx: HTTPS configuration](../host-and-deploy/linux-nginx?view=aspnetcore-10.0#https-configuration)
* [How to Set Up SSL on IIS](/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis)
* [Configure endpoints for Kestrel web server](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0)
* [OWASP HSTS browser support](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html#browser-support)

Note

If you're using .NET 9 or later SDK, see the updated Linux procedures in the [.NET 9 version of this article](?view=aspnetcore-9.0\&preserve-view=true).

Warning

## API projects

Do **not** use [RequireHttpsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requirehttpsattribute) on Web APIs that receive sensitive information. `RequireHttpsAttribute` uses HTTP status codes to redirect browsers from HTTP to HTTPS. API clients may not understand or obey redirects from HTTP to HTTPS. Such clients may send information over HTTP. Web APIs should either:

* Not listen on HTTP.
* Close the connection with status code 400 (Bad Request) and not serve the request.

To disable HTTP redirection in an API, set the `ASPNETCORE_URLS` environment variable or use the `--urls` command line flag. For more information, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0) and [8 ways to set the URLs for an ASP.NET Core app](https://andrewlock.net/8-ways-to-set-the-urls-for-an-aspnetcore-app/) by Andrew Lock.

## HSTS and API projects

The default API projects don't include [HSTS](#hsts) because [HSTS](https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security) is generally a browser only instruction. Other callers, such as phone or desktop apps, do **not** obey the instruction. Even within browsers, a single authenticated call to an API over HTTP has risks on insecure networks. The secure approach is to configure API projects to only listen to and respond over HTTPS.

[]()

### HTTP redirection to HTTPS causes ERR\_INVALID\_REDIRECT on the CORS preflight request

Requests to an endpoint using HTTP that are redirected to HTTPS by [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) fail with `ERR_INVALID_REDIRECT` on the CORS preflight request.

API projects can reject HTTP requests rather than use `UseHttpsRedirection` to redirect requests to HTTPS.

## Require HTTPS

We recommend that production ASP.NET Core web apps use:

* HTTPS redirection middleware ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) to redirect HTTP requests to HTTPS.
* HSTS middleware ([UseHsts](#http-strict-transport-security-hsts-protocol)) to send HTTP Strict Transport Security (HSTS) protocol headers to clients.

Note

Apps deployed in a reverse proxy configuration allow the proxy to handle connection security (HTTPS). If the proxy also handles HTTPS redirection, there's no need to use HTTPS redirection middleware. If the proxy server also handles writing HSTS headers (for example, [native HSTS support in IIS 10.0 (1709) or later](/en-us/iis/get-started/whats-new-in-iis-10-version-1709/iis-10-version-1709-hsts#iis-100-version-1709-native-hsts-support)), HSTS middleware isn't required by the app. For more information, see [Opt-out of HTTPS/HSTS on project creation](#opt-out-of-httpshsts-on-project-creation).

### HTTPS redirection middleware (`UseHttpsRedirection`)

The following code calls [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) in the `Program.cs` file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding highlighted code:

* Uses the default [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-redirectstatuscode) ([Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect)).
* Uses the default [HttpsRedirectionOptions.HttpsPort](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.httpsport#microsoft-aspnetcore-httpspolicy-httpsredirectionoptions-httpsport) (null) unless overridden by the `ASPNETCORE_HTTPS_PORT` environment variable or [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

We recommend using temporary redirects rather than permanent redirects. Link caching can cause unstable behavior in development environments. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, see the [Configure permanent redirects in production](#configure-permanent-redirects-in-production) section. We recommend using [HSTS](#http-strict-transport-security-hsts-protocol) to signal to clients that only secure resource requests should be sent to the app (only in production).

Note

Don't confuse the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for HTTPS redirection middleware, with the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration.

### Port configuration

A port must be available for the middleware to redirect an insecure request to HTTPS. If no port is available:

* Redirection to HTTPS doesn't occur.
* The middleware logs the warning "Failed to determine the https port for redirect."

Specify the HTTPS port using any of the following approaches:

* Set [HttpsRedirectionOptions.HttpsPort](#options).

* Set the `https_port` [host setting](../fundamentals/host/generic-host?view=aspnetcore-10.0#https_port):

  * In host configuration.

  * By setting the `ASPNETCORE_HTTPS_PORT` environment variable.

  * By adding a top-level entry in `appsettings.json`:

    ```
    {
      "https_port": 443,
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*"
    }
    ```

* Indicate a port with the secure scheme using the [ASPNETCORE\_URLS environment variable](../fundamentals/host/generic-host?view=aspnetcore-10.0#urls). The environment variable configures the server. The middleware indirectly discovers the HTTPS port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature). This approach doesn't work in reverse proxy deployments.

* The ASP.NET Core web templates set an HTTPS URL in `Properties/launchsettings.json` for both Kestrel and IIS Express. `launchsettings.json` is only used on the local machine.

* Configure an HTTPS URL endpoint for a public-facing edge deployment of [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server or [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) server. Only **one HTTPS port** is used by the app. The middleware discovers the port via [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature).

Note

When an app is run in a reverse proxy configuration, [IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature) isn't available. Set the port using one of the other approaches described in this section.

### Edge deployments

When [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) or [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) is used as a public-facing edge server, Kestrel or HTTP.sys must be configured to listen on both:

* The secure port where the client is redirected (typically, 443 in production and 5001 in development).
* The insecure port (typically, 80 in production and 5000 in development).

The insecure port must be accessible by the client in order for the app to receive an insecure request and redirect the client to the secure port.

For more information, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration) or [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

### Deployment scenarios

Any firewall between the client and server must also have communication ports open for traffic.

If requests are forwarded in a reverse proxy configuration, use [forwarded headers middleware](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0) before calling HTTPS redirection middleware. Forwarded headers middleware updates the `Request.Scheme`, using the `X-Forwarded-Proto` header. The middleware permits redirect URIs and other security policies to work correctly. When forwarded headers middleware isn't used, the backend app might not receive the correct scheme and end up in a redirect loop. A common end user error message is that too many redirects have occurred.

When deploying to Azure App Service, follow the guidance in [Tutorial: Bind an existing custom SSL certificate to Azure Web Apps](/en-us/azure/app-service/app-service-web-tutorial-custom-ssl).

### Options

The following highlighted code calls [AddHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpsredirectionservicesextensions.addhttpsredirection) to configure middleware options:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = Status307TemporaryRedirect;
    options.HttpsPort = 5001;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

Calling `AddHttpsRedirection` is only necessary to change the values of `HttpsPort` or `RedirectStatusCode`.

The preceding highlighted code:

* Sets [HttpsRedirectionOptions.RedirectStatusCode](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.httpsredirectionoptions.redirectstatuscode) to [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect), which is the default value. Use the fields of the [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) class for assignments to `RedirectStatusCode`.
* Sets the HTTPS port to 5001.

#### Configure permanent redirects in production

The middleware defaults to sending a [Status307TemporaryRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status307temporaryredirect#microsoft-aspnetcore-http-statuscodes-status307temporaryredirect) with all redirects. If you prefer to send a permanent redirect status code when the app is in a non-`Development` environment, wrap the middleware options configuration in a conditional check for a non-`Development` environment.

When configuring services in `Program.cs`:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (!builder.Environment.IsDevelopment())
{
    builder.Services.AddHttpsRedirection(options =>
    {
        options.RedirectStatusCode = Status308PermanentRedirect;
        options.HttpsPort = 443;
    });
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");

    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

## HTTPS redirection middleware alternative approach

An alternative to using HTTPS redirection middleware (`UseHttpsRedirection`) is to use URL rewriting middleware (`AddRedirectToHttps`). `AddRedirectToHttps` can also set the status code and port when the redirect is executed. For more information, see [URL rewriting middleware](../fundamentals/url-rewriting?view=aspnetcore-10.0).

When redirecting to HTTPS without the requirement for additional redirect rules, we recommend using HTTPS redirection middleware (`UseHttpsRedirection`) described in this article.

[]()

## HTTP Strict Transport Security (HSTS) protocol

Per [OWASP](https://owasp.org/about), [HTTP Strict Transport Security (HSTS)](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html) is an opt-in security enhancement that's specified by a web app through the use of a response header. When a [browser that supports HSTS](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html#browser-support) receives this header:

* The browser stores configuration for the domain that prevents sending any communication over HTTP. The browser forces all communication over HTTPS.
* The browser prevents the user from using untrusted or invalid certificates. The browser disables prompts that allow a user to temporarily trust such a certificate.

Because [HSTS](https://developer.mozilla.org/docs/Web/HTTP/Headers/Strict-Transport-Security) is enforced by the client, it has some limitations:

* The client must support HSTS.
* HSTS requires at least one successful HTTPS request to establish the HSTS policy.
* The application must check every HTTP request and redirect or reject the HTTP request.

ASP.NET Core implements HSTS with the [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts) extension method. The following code calls `UseHsts` when the app isn't in [development mode](../fundamentals/environments?view=aspnetcore-10.0):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

`UseHsts` isn't recommended in development because the HSTS settings are highly cacheable by browsers. By default, `UseHsts` excludes the local loopback address.

For production environments that are implementing HTTPS for the first time, set the initial [HstsOptions.MaxAge](/en-us/dotnet/api/microsoft.aspnetcore.httpspolicy.hstsoptions.maxage) to a small value using one of the [TimeSpan](/en-us/dotnet/api/system.timespan) methods. Set the value from hours to no more than a single day in case you need to revert the HTTPS infrastructure to HTTP. After you're confident in the sustainability of the HTTPS configuration, increase the HSTS `max-age` value; a commonly used value is one year.

The following highlighted code:

```
using static Microsoft.AspNetCore.Http.StatusCodes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

builder.Services.AddHttpsRedirection(options =>
{
    options.RedirectStatusCode = Status307TemporaryRedirect;
    options.HttpsPort = 5001;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

* Sets the preload parameter of the `Strict-Transport-Security` header. Preload isn't part of the [RFC HSTS specification](https://tools.ietf.org/html/rfc6797), but is supported by web browsers to preload HSTS sites on fresh install. For more information, see <https://hstspreload.org/>.
* Enables [includeSubDomain](https://tools.ietf.org/html/rfc6797#section-6.1.2), which applies the HSTS policy to Host subdomains.
* Explicitly sets the `max-age` parameter of the `Strict-Transport-Security` header to 60 days. If not set, defaults to 30 days. For more information, see the [max-age directive](https://tools.ietf.org/html/rfc6797#section-6.1.1).
* Adds `example.com` to the list of hosts to exclude.

`UseHsts` excludes the following loopback hosts:

* `localhost` : The IPv4 loopback address.
* `127.0.0.1` : The IPv4 loopback address.
* `[::1]` : The IPv6 loopback address.

## Opt-out of HTTPS/HSTS on project creation

In some backend service scenarios where connection security is handled at the public-facing edge of the network, configuring connection security at each node isn't required. Web apps that are generated from the templates in Visual Studio or from the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command enable [HTTPS redirection](#require-https) and [HSTS](#http-strict-transport-security-hsts-protocol). For deployments that don't require these scenarios, you can opt-out of HTTPS/HSTS when the app is created from the template.

To opt-out of HTTPS/HSTS:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

Uncheck the **Configure for HTTPS** checkbox.

Use the `--no-https` option. For example

```
dotnet new webapp --no-https
```

[]()

## Trust the ASP.NET Core HTTPS development certificate on Windows and macOS

For the Firefox browser, see the next section.

The .NET SDK includes an HTTPS development certificate. The certificate is installed as part of the first-run experience. For example, `dotnet --info` produces a variation of the following output:

```
ASP.NET Core
------------
Successfully installed the ASP.NET Core HTTPS Development Certificate.
To trust the certificate run 'dotnet dev-certs https --trust' (Windows and macOS only).
For establishing trust on other platforms refer to the platform specific documentation.
For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054.
```

Installing the .NET SDK installs the ASP.NET Core HTTPS development certificate to the local user certificate store. The certificate has been installed, but it's not trusted. To trust the certificate, perform the one-time step to run the `dotnet dev-certs` tool:

```
dotnet dev-certs https --trust
```

The following command provides help on the `dotnet dev-certs` tool:

```
dotnet dev-certs https --help
```

Warning

Do not create a development certificate in an environment that will be redistributed, such as a container image or virtual machine. Doing so can lead to spoofing and elevation of privilege. To help prevent this, set the `DOTNET_GENERATE_ASPNET_CERTIFICATE` environment variable to `false` prior to calling the .NET CLI for the first time. This will skip the automatic generation of the ASP.NET Core development certificate during the CLI's first-run experience.

[]()

### Trust the HTTPS certificate with Firefox to prevent SEC\_ERROR\_INADEQUATE\_KEY\_USAGE error

The Firefox browser uses its own certificate store, and therefore doesn't trust the [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview) or [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) developer certificates.

There are two approaches to trusting the HTTPS certificate with Firefox, create a policy file or configure with the FireFox browser. Configuring with the browser creates the policy file, so the two approaches are equivalent.

#### Create a policy file to trust HTTPS certificate with Firefox

Create a policy file (`policies.json`) at:

* Windows: `%PROGRAMFILES%\Mozilla Firefox\distribution\`
* MacOS: `Firefox.app/Contents/Resources/distribution`
* Linux: See [Trust the certificate with Firefox on Linux](/en-us/aspnet/core/security/enforcing-ssl?tabs=visual-studio%2Clinux-ubuntu%2Clinux-sles#trust-the-certificate-with-firefox-on-linux) in this article.

Add the following JSON to the Firefox policy file:

```
{
  "policies": {
    "Certificates": {
      "ImportEnterpriseRoots": true
    }
  }
}
```

The preceding policy file makes Firefox trust certificates from the trusted certificates in the Windows certificate store. The next section provides an alternative approach to create the preceding policy file by using the Firefox browser.

[]()

### Configure trust of HTTPS certificate using Firefox browser

Set `security.enterprise_roots.enabled` = `true` using the following instructions:

1. Enter `about:config` in the FireFox browser.
2. Select **Accept the Risk and Continue** if you accept the risk.
3. Select **Show All**
4. Set `security.enterprise_roots.enabled` = `true`
5. Exit and restart Firefox

For more information, see [Setting Up Certificate Authorities (CAs) in Firefox](https://support.mozilla.org/kb/setting-certificate-authorities-firefox) and the [mozilla/policy-templates/README file](https://github.com/mozilla/policy-templates/blob/master/README.md).

## How to set up a developer certificate for Docker

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/6199).

## Trust HTTPS certificate on Linux

Establishing trust is distribution and browser specific. The following sections provide instructions for some popular distributions and the Chromium browsers (Edge and Chrome) and for Firefox.

### Trust HTTPS certificate on Linux with linux-dev-certs

[linux-dev-certs](https://github.com/tmds/linux-dev-certs) is an open-source, community-supported, .NET global tool that provides a convenient way to create and trust a developer certificate on Linux. The tool is not maintained or supported by Microsoft.

The following commands install the tool and create a trusted developer certificate:

```
dotnet tool update -g linux-dev-certs
dotnet linux-dev-certs install
```

For more information or to report issues, see the [linux-dev-certs GitHub repository](https://github.com/tmds/linux-dev-certs).

### Ubuntu trust the certificate for service-to-service communication

The following instructions don't work for some Ubuntu versions, such as 20.04. For more information, see GitHub issue [dotnet/AspNetCore.Docs #23686](https://github.com/dotnet/AspNetCore.Docs/issues/23686).

1. Install [OpenSSL](https://www.openssl.org/) 1.1.1h or later. See your distribution for instructions on how to update OpenSSL.

2. Run the following commands:

   ```
   dotnet dev-certs https
   sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
   sudo update-ca-certificates
   ```

The preceding commands:

* Ensure the current user's developer certificate is created.
* Exports the certificate with elevated permissions needed for the `ca-certificates` folder, using the current user's environment.
* Removing the `-E` flag exports the root user certificate, generating it if necessary. Each newly generated certificate has a different thumbprint. When running as root, `sudo` and `-E` are not needed.

The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

[]()

### Trust HTTPS certificate on Linux using Edge or Chrome

* [Ubuntu](#tabpanel_2_linux-ubuntu)
* [Red Hat Enterprise Linux](#tabpanel_2_linux-rhel)
* [SUSE Linux Enterprise Server](#tabpanel_2_linux-sles)

For chromium browsers on Linux:

* Install the `libnss3-tools` for your distribution.

* Create or verify the `$HOME/.pki/nssdb` folder exists on the machine.

* Export the certificate with the following command:

  ```
  dotnet dev-certs https
  sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
  ```

  The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

* Run the following commands:

  ```
  certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n localhost -i /usr/local/share/ca-certificates/aspnet/https.crt
  ```

* Exit and restart the browser.

[]()

#### Trust the certificate with Firefox on Linux

* Export the certificate with the following command:

  ```
  dotnet dev-certs https
  sudo -E dotnet dev-certs https -ep /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
  ```

  The path in the preceding command is specific for Ubuntu. For other distributions, select an appropriate path or use the path for the Certificate Authorities (CAs).

* Create a JSON file at `/usr/lib/firefox/distribution/policies.json` with the following command:

```
cat <<EOF | sudo tee /usr/lib/firefox/distribution/policies.json
{
    "policies": {
        "Certificates": {
            "Install": [
                "/usr/local/share/ca-certificates/aspnet/https.crt"
            ]
        }
    }
}
EOF
```

Note: Ubuntu 21.10 Firefox comes as a snap package and the installation folder is `/snap/firefox/current/usr/lib/firefox`.

See [Configure trust of HTTPS certificate using Firefox browser](#trust-ff-ba) in this article for an alternative way to configure the policy file using the browser.

Warning

The following instructions are intended for development purposes only. Do not use the certificates generated in these instructions for a production environment.

These instructions use Mozilla's *legacy* tool `certutil` at `https://firefox-source-docs.mozilla.org/security/nss/legacy/tools/nss_tools_certutil/index.html`. Instructions may be updated as modern utilities and practices are discovered.

Caution

Improper use of TLS certificates could lead to spoofing.

Tip

Instructions for valid production certificates can be found in the RHEL Documentation. [RHEL8 TLS Certificates](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/securing_networks/index#creating-and-managing-tls-keys-and-certificates_securing-networks) [RHEL9 TLS Certificates](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html-single/securing_networks/index#creating-and-managing-tls-keys-and-certificates_securing-networks) [RHEL9 Certificate System](https://access.redhat.com/documentation/en-us/red_hat_certificate_system/9)

### Install Dependencies

```
dnf install nss-tools
```

### Export The ASP.NET Core Development Certificate

Important

Replace `${ProjectDirectory}` with your projects directory. Replace `${CertificateName}` with a name you'll be able to identify in the future.

```
cd ${ProjectDirectory}
dotnet dev-certs https -ep ${ProjectDirectory}/${CertificateName}.crt --format PEM
```

Caution

If using git, add your certificate to your `${ProjectDirectory}/.gitignore` or `${ProjectDirectory}/.git/info/exclude`. View the [git documentation](https://git-scm.com/docs/gitignore) for information about these files.

Tip

You can move your exported certificate outside of your Git repository and replace the occurrences of `${ProjectDirectory}`, in the following instructions, with the new location.

### Import The ASP.NET Core Development Certificate

Important

Replace `${UserProfile}` with the profile you intend to use. Do not replace `$HOME`, it is the environment variable to your user directory.

#### Chromium-based Browsers

```
certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
```

#### Mozilla Firefox

```
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -A -t "P,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -A -t "C,," -n ${CertificateName} -i ${ProjectDirectory}/${CertificateName}.crt
```

#### Create An Alias To Test With Curl

Important

Don't delete the exported certificate if you plan to test with curl. You'll need to create an alias referencing it in your `$SHELL`'s profile

```
alias curl="curl --cacert ${ProjectDirectory}/${CertificateName}.crt"
```

### Cleaning up the Development Certificates

```
certutil -d sql:$HOME/.pki/nssdb -D -n ${CertificateName}
certutil -d sql:$HOME/.mozilla/firefox/${UserProfile}/ -D -n ${CertificateName}
rm ${ProjectDirectory}/${CertificateName}.crt
dotnet dev-certs https --clean
```

Note

Remove the curl alias you created earlier

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/28292)

[]()

### Trust the certificate with Fedora 34

See:

* [This GitHub comment](https://github.com/dotnet/aspnetcore/issues/32361#issuecomment-837111639)
* [Fedora: Using Shared System Certificates](https://docs.fedoraproject.org/en-US/quick-docs/using-shared-system-certificates/)
* [Set up a .NET development environment](https://fedoramagazine.org/set-up-a-net-development-environment/) on Fedora.

### Trust the certificate with other distros

See [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/32842).

## Trust HTTPS certificate from Windows Subsystem for Linux

The following instructions don't work for some Linux distributions, such as Ubuntu 20.04. For more information, see GitHub issue [dotnet/AspNetCore.Docs #23686](https://github.com/dotnet/AspNetCore.Docs/issues/23686).

The [Windows Subsystem for Linux (WSL)](/en-us/windows/wsl/about) generates an HTTPS self-signed development certificate, which by default isn't trusted in Windows. The easiest way to have Windows trust the WSL certificate, is to configure WSL to use the same certificate as Windows:

* On ***Windows***, export the developer certificate to a file:

  ```
  dotnet dev-certs https -ep https.pfx -p $CREDENTIAL_PLACEHOLDER$ --trust
  ```

  Where `$CREDENTIAL_PLACEHOLDER$` is a password.

* In a WSL window, import the exported certificate on the WSL instance:

  ```
  dotnet dev-certs https --clean --import <<path-to-pfx>> --password $CREDENTIAL_PLACEHOLDER$
  ```

The preceding approach is a one time operation per certificate and per WSL distribution. It's easier than exporting the certificate over and over. If you update or regenerate the certificate on windows, you might need to run the preceding commands again.

[]()

## Troubleshoot certificate problems such as certificate not trusted

This section provides help when the ASP.NET Core HTTPS development certificate has been [installed and trusted](#trust), but you still have browser warnings that the certificate is not trusted. The ASP.NET Core HTTPS development certificate is used by [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0).

To repair the IIS Express certificate, see [this Stackoverflow](https://stackoverflow.com/a/20048613/502537) issue.

### All platforms - certificate not trusted

Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances open. Open a new browser window to app. Certificate trust is cached by browsers.

### dotnet dev-certs https --clean Fails

The preceding commands solve most browser trust issues. If the browser is still not trusting the certificate, follow the platform-specific suggestions that follow.

### Docker - certificate not trusted

* Delete the *C:\Users{USER}\AppData\Roaming\ASP.NET\Https* folder.
* Clean the solution. Delete the *bin* and *obj* folders.
* Restart the development tool. For example, Visual Studio or Visual Studio Code.

### Windows - certificate not trusted

* Check the certificates in the certificate store. There should be a `localhost` certificate with the `ASP.NET Core HTTPS development certificate` friendly name both under `Current User > Personal > Certificates` and `Current User > Trusted root certification authorities > Certificates`
* Remove all the found certificates from both Personal and Trusted root certification authorities. Do **not** remove the IIS Express localhost certificate.
* Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances open. Open a new browser window to app.

### OS X - certificate not trusted

* Open KeyChain Access.
* Select the System keychain.
* Check for the presence of a localhost certificate.
* Check that it contains a `+` symbol on the icon to indicate it's trusted for all users.
* Remove the certificate from the system keychain.
* Run the following commands:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

Close any browser instances open. Open a new browser window to app.

See [HTTPS Error using IIS Express (dotnet/AspNetCore #16892)](https://github.com/dotnet/AspNetCore/issues/16892) for troubleshooting certificate issues with Visual Studio.

### Linux certificate not trusted

Check that the certificate being configured for trust is the user HTTPS developer certificate that will be used by the Kestrel server.

Check the current user default HTTPS developer Kestrel certificate at the following location:

```
ls -la ~/.dotnet/corefx/cryptography/x509stores/my
```

The HTTPS developer Kestrel certificate file is the SHA1 thumbprint. When the file is deleted via `dotnet dev-certs https --clean`, it's regenerated when needed with a different thumbprint. Check the thumbprint of the exported certificate matches with the following command:

```
openssl x509 -noout -fingerprint -sha1 -inform pem -in /usr/local/share/ca-certificates/aspnet/https.crt
```

If the certificate doesn't match, it could be one of the following:

* An old certificate.
* An exported a developer certificate for the root user. For this case, export the certificate.

The root user certificate can be checked at:

```
ls -la /root/.dotnet/corefx/cryptography/x509stores/my
```

### IIS Express SSL certificate used with Visual Studio

To fix problems with the IIS Express certificate, select **Repair** from the Visual Studio installer. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/16892).

### Group policy prevents self-signed certificates from being trusted

In some cases, group policy may prevent self-signed certificates from being trusted. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/21173).

## Additional information

* [Configure ASP.NET Core to work with proxy servers and load balancers](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)
* [Host ASP.NET Core on Linux with Nginx: HTTPS configuration](../host-and-deploy/linux-nginx?view=aspnetcore-10.0#https-configuration)
* [How to Set Up SSL on IIS](/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis)
* [Configure endpoints for Kestrel web server](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0)
* [OWASP HSTS browser support](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.html#browser-support)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-29

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/hosting-bundle
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/hosting-bundle.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# The .NET Hosting Bundle

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The .NET Hosting bundle is an installer for the .NET Runtime and the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0). The bundle allows ASP.NET Core apps to run with IIS.

## Install the .NET Hosting Bundle

Important

If the Hosting Bundle is installed before IIS, the bundle installation must be repaired. Run the Hosting Bundle installer again after installing IIS.

If the Hosting Bundle is installed after installing the 64-bit (x64) version of .NET, SDKs might appear to be missing ([No .NET SDKs were detected](../../test/troubleshoot?view=aspnetcore-10.0#no-net-sdks-were-detected)). To resolve the problem, see [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0#missing-sdk-after-installing-the-net-hosting-bundle).

Breaking changes and security advisories are reported on the [Announcements repo](https://github.com/aspnet/Announcements/issues). Announcements can be limited to a specific version by selecting a **Label** filter.

## Direct download

Download the installer using the following links:

* Current version: [.NET Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)
* [Previous and pre-release versions](https://dotnet.microsoft.com/en-us/download/dotnet)

## Visual C++ Redistributable Requirement

On older versions of Windows, for example Windows Server 2012 R2, install the Visual Studio C++ 2015, 2017, 2019 Redistributable. Otherwise, a confusing error message in the Windows Event Log reports that `The data is the error.`

[Current x64 VS C++ redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) [Current x86 VS C++ redistributable](https://aka.ms/vs/17/release/vc_redist.x86.exe)

## Earlier versions of the installer

To obtain an earlier version of the installer:

1. Navigate to the [Download .NET](https://dotnet.microsoft.com/download/dotnet-core) page.
2. Select the desired .NET version.
3. In the **Run apps - Runtime** column, find the row of the .NET runtime version desired.
4. Download the installer using the **Hosting Bundle** link.

Warning

Some installers contain release versions that have reached their end of life (EOL) and are no longer supported by Microsoft. For more information, see the [support policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core).

The [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0) is forward and backward compatible with [in-support releases of .NET](https://dotnet.microsoft.com/platform/support/policy/dotnet-core#lifecycle).

## Options

1. The following parameters are available when running the installer from an administrator command shell:

   * `OPT_NO_ANCM=1`: Skip installing the ASP.NET Core Module.
   * `OPT_NO_RUNTIME=1`: Skip installing the .NET runtime. Used when the server only hosts [self-contained deployments (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).
   * `OPT_NO_SHAREDFX=1`: Skip installing the ASP.NET Shared Framework (ASP.NET runtime). Used when the server only hosts [self-contained deployments (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).
   * `OPT_NO_X86=1`: Skip installing x86 runtimes. Use this parameter when you know that you won't be hosting 32-bit apps. If there's any chance that you will host both 32-bit and 64-bit apps in the future, don't use this parameter and install both runtimes.
   * `OPT_NO_SHARED_CONFIG_CHECK=1`: Disable the check for using an IIS Shared Configuration when the shared configuration (`applicationHost.config`) is on the same machine as the IIS installation. *Only available for ASP.NET Core 2.2 or later Hosting Bundler installers.* For more information, see [Advanced configuration](advanced?view=aspnetcore-10.0#aspnet-core-module-with-an-iis-shared-configuration).

Note

For information on IIS Shared Configuration, see [ASP.NET Core Module with IIS Shared Configuration](../aspnet-core-module?view=aspnetcore-10.0#aspnet-core-module-with-an-iis-shared-configuration).

Note

When running the Hosting Bundle installer with options set, the value for each option is saved in the registry. Subsequent installs from the same Major.Minor version band use the same options, unless another set of options is explicitly passed from the command line. If the first install of the hosting bundle has no options passed, each option gets a default value of `0` written in to the registry. A value of `0` implies that the option is off, meaning the user is not opting out of the given component.

## Restart IIS

After the Hosting Bundle is installed, a manual IIS restart may be required. For example, the `dotnet` CLI tooling (command) might not exist on the PATH for running IIS worker processes.

To manually restart IIS, stop the Windows Process Activation Service (WAS) and then restart the World Wide Web Publishing Service (W3SVC) and any dependent services. Execute the following commands in an elevated command shell:

```
net stop was /y
net start w3svc
```

## Module version and Hosting Bundle installer logs

To determine the version of the installed ASP.NET Core Module:

1. On the hosting system, navigate to `%PROGRAMFILES%\IIS\Asp.Net Core Module\V2`.
2. Locate the `aspnetcorev2.dll` file.
3. Right-click the file and select **Properties** from the contextual menu.
4. Select the **Details** tab. The **File version** and **Product version** represent the installed version of the module.

The Hosting Bundle installer logs for the module are found at `C:\Users\%UserName%\AppData\Local\Temp`. The file is named `dd_DotNetCoreWinSvrHosting__{TIMESTAMP}_000_AspNetCoreModule_x64.log`, where the placeholder `{TIMESTAMP}` is the timestamp of the file.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/webassembly-build-tools-and-aot
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/webassembly-build-tools-and-aot.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes the build tools for standalone Blazor WebAssembly apps and how to compile an app ahead of deployment with ahead-of-time (AOT) compilation.

## .NET WebAssembly build tools

The .NET WebAssembly build tools are based on [Emscripten](https://emscripten.org/), a compiler toolchain for the web platform.

To install the build tools as a .NET workload, use ***either*** of the following approaches:

* For the **ASP.NET and web development** workload in the Visual Studio installer, select the **.NET WebAssembly build tools** option from the list of optional components. The option ensures the following:

  * The workload is installed for the latest .NET SDK.
  * When a new version of Visual Studio is released and it contains a new .NET SDK, the option installs the workload for the new SDK.

* Alternatively, execute the following command in an *administrative command shell* to install the latest workload to the latest .NET SDK available on the system:

  ```
  dotnet workload install wasm-tools
  ```

To target a prior .NET release with a given .NET SDK, install the `wasm-tools-net{MAJOR VERSION}` workload:

* The `{MAJOR VERSION}` placeholder is replaced with the major version number of the .NET release you want to target (for example, `wasm-tools-net8` for .NET 8).
* Workloads are installed per .NET SDK. Installing the `wasm-tools` workload for one SDK doesn't make it available to other SDKs on the system.
* You must install the appropriate workload for each .NET SDK version you intend to use.

The following list shows which workload to install for each .NET SDK, depending on the apps that you plan to target. Although multiple rows may contain the same workload name, the workloads always differ slightly for each particular .NET SDK.

* Using the .NET 10 SDK

  * Targeting .NET 10 requires `wasm-tools`.
  * Targeting .NET 9 requires `wasm-tools-net9`.
  * Targeting .NET 8 requires `wasm-tools-net8`.

* Using the .NET 9 SDK

  * Targeting .NET 9 requires `wasm-tools`.
  * Targeting .NET 8 requires `wasm-tools-net8`.

* Using the .NET 8 SDK: Targeting .NET 8 requires `wasm-tools`.

The Emscripten compiler toolchain depends on [Python](https://www.python.org/), which is bundled by default with the .NET WebAssembly build tools workload on Windows and macOS. Python isn't bundled for Linux users, resulting in "unable to find python in $PATH" errors if Python isn't available. Linux users should install Python through their package manager or [download Python for Linux/Unix](https://www.python.org/downloads/source/) and manually install it on their system so that it is available in `$PATH`.

## Ahead-of-time (AOT) compilation

Blazor WebAssembly supports ahead-of-time (AOT) compilation, where you can compile your .NET code directly into WebAssembly. AOT compilation results in runtime performance improvements at the expense of a larger app size.

Without enabling AOT compilation, Blazor WebAssembly apps run on the browser using a .NET [Intermediate Language (IL)](/en-us/dotnet/standard/glossary#il) interpreter implemented in WebAssembly with partial [just-in-time (JIT)](/en-us/dotnet/standard/glossary#jit) runtime support, informally referred to as the *Jiterpreter*. Because the .NET IL code is interpreted, apps typically run slower than they would on a server-side .NET JIT runtime without any IL interpretation. AOT compilation addresses this performance issue by compiling an app's .NET code directly into WebAssembly for native WebAssembly execution by the browser. The AOT performance improvement can yield dramatic improvements for apps that execute CPU-intensive tasks. The drawback to using AOT compilation is that AOT-compiled apps are generally larger than their IL-interpreted counterparts, so they usually take longer to download to the client when first requested.

Without enabling AOT compilation, Blazor WebAssembly apps run on the browser using a .NET [Intermediate Language (IL)](/en-us/dotnet/standard/glossary#il) interpreter implemented in WebAssembly. Because the .NET code is interpreted, apps typically run slower than they would on a server-side .NET [just-in-time (JIT)](/en-us/dotnet/standard/glossary#jit) runtime. AOT compilation addresses this performance issue by compiling an app's .NET code directly into WebAssembly for native WebAssembly execution by the browser. The AOT performance improvement can yield dramatic improvements for apps that execute CPU-intensive tasks. The drawback to using AOT compilation is that AOT-compiled apps are generally larger than their IL-interpreted counterparts, so they usually take longer to download to the client when first requested.

For guidance on installing the .NET WebAssembly build tools, see [ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation](webassembly-build-tools-and-aot?view=aspnetcore-10.0).

To enable WebAssembly AOT compilation, add the `<RunAOTCompilation>` property set to `true` to the Blazor WebAssembly app's project file:

```
<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
```

To compile the app to WebAssembly, publish the app. Publishing the `Release` configuration ensures the .NET Intermediate Language (IL) linking is also run to reduce the size of the published app:

```
dotnet publish -c Release
```

WebAssembly AOT compilation is only performed when the project is published. AOT compilation isn't used when the project is run during development (`Development` environment) because AOT compilation usually takes several minutes on small projects and potentially much longer for larger projects. Reducing the build time for AOT compilation is under development for future releases of ASP.NET Core.

The size of an AOT-compiled Blazor WebAssembly app is generally larger than the size of the app if compiled into .NET IL:

* Although the size difference depends on the app, most AOT-compiled apps are about twice the size of their IL-compiled versions. This means that using AOT compilation trades off load-time performance for runtime performance. Whether this tradeoff is worth using AOT compilation depends on your app. Blazor WebAssembly apps that are CPU intensive generally benefit the most from AOT compilation.

* The larger size of an AOT-compiled app is due to two conditions:

  * More code is required to represent high-level .NET IL instructions in native WebAssembly.
  * AOT doesn't trim out managed DLLs when the app is published. Blazor requires the DLLs for [reflection metadata](/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/) and to support certain .NET runtime features. Requiring the DLLs on the client increases the download size but provides a more compatible .NET experience.

Note

For [Mono](https://github.com/mono/mono)/WebAssembly MSBuild properties and targets, see [`WasmApp.Common.targets` (`dotnet/runtime` GitHub repository)](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/build/WasmApp.Common.targets). Official documentation for common MSBuild properties is planned per [Document blazor msbuild configuration options (`dotnet/docs` #27395)](https://github.com/dotnet/docs/issues/27395).

## Performance

For performance guidance, see [ASP.NET Core Blazor WebAssembly runtime performance](performance/webassembly-runtime-performance?view=aspnetcore-10.0):

* Heap size for some mobile device browsers
* Runtime relinking
* Single Instruction, Multiple Data (SIMD)
* Trim .NET IL after ahead-of-time (AOT) compilation (.NET 8 or later)

## Exception handling

Exception handling is enabled by default. To disable exception handling, add the `<WasmEnableExceptionHandling>` property with a value of `false` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <WasmEnableExceptionHandling>false</WasmEnableExceptionHandling>
</PropertyGroup>
```

To enable WebAssembly exception handling, add the `<WasmEnableExceptionHandling>` property with a value of `true` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <WasmEnableExceptionHandling>true</WasmEnableExceptionHandling>
</PropertyGroup>
```

For more information, see the following resources:

* [Configuring and hosting .NET WebAssembly applications: EH - Exception handling](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md#eh---exception-handling)
* [Exception handling](https://github.com/WebAssembly/exception-handling/blob/master/proposals/exception-handling/Exceptions.md)

## Related content

* [ASP.NET Core Blazor WebAssembly runtime performance](performance/webassembly-runtime-performance?view=aspnetcore-10.0)
* [ASP.NET Core Blazor WebAssembly native dependencies](webassembly-native-dependencies?view=aspnetcore-10.0)
* [Webcil packaging format for .NET assemblies](host-and-deploy/webassembly/?view=aspnetcore-10.0#webcil-packaging-format-for-net-assemblies)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/authn-and-authz.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Authentication and authorization in ASP.NET Core SignalR

This article describes how to authenticate and authorize users in ASP.NET Core applications with SignalR.

## Authenticate users connecting to a SignalR hub

SignalR can be used with [ASP.NET Core authentication](../security/authentication/identity?view=aspnetcore-10.0) to associate a user with each connection. In a hub, authentication data can be accessed from the [HubConnectionContext.User](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubconnectioncontext.user#microsoft-aspnetcore-signalr-hubconnectioncontext-user) property. Authentication allows the hub to call methods on all connections associated with a user. For more information, see [Manage users and groups in SignalR](groups?view=aspnetcore-10.0). Multiple connections can be associated with a single user.

The following code is an example that uses SignalR and ASP.NET Core authentication:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chat");

app.Run();
```

Note

If a token expires during the lifetime of a connection, by default the connection continues to work. `LongPolling` and `ServerSentEvents` connections fail on subsequent requests if they don't send new access tokens. For connections to close when the authentication token expires, set the [CloseOnAuthenticationExpiration](configuration?view=aspnetcore-10.0#configure-advanced-http-options) option.

### User and role changes during the connection lifetime

SignalR captures the authenticated user when a connection is established and caches it for the lifetime of the connection. The cached principal is exposed to hub methods through the `Context.User` property ([HubCallerContext.User](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext.user#microsoft-aspnetcore-signalr-hubcallercontext-user)) and is used to authorize hub method invocations. SignalR doesn't automatically revalidate the user during the life of the connection, regardless of the authentication scheme. This behavior applies to all schemes, including cookie authentication and bearer token authentication.

Changes to a user's identity, roles, or claims that occur after the connection is established aren't reflected on an existing connection. This behavior applies even when the underlying transport makes new HTTP requests, such as the `LongPolling` and `ServerSentEvents` transports. Although the ASP.NET Core authentication middleware reauthenticates each of these HTTP requests, SignalR continues to use the principal that was cached when the connection was established.

Consider the following scenario:

* An app uses cookie authentication and the `LongPolling` transport. The same behavior applies to bearer token authentication and the `ServerSentEvents` transport.
* A user is signed in with the `Editor` role and has an open SignalR connection.
* The app removes the `Editor` role from that user.

On the next long-poll request, the authentication middleware might authenticate the user without the `Editor` role. For example, this can happen if roles are loaded from a data store or the cookie is refreshed. However, the hub continues to authorize the user's `[Authorize(Roles = "Editor")]` hub method invocations because `Context.User` ([HubCallerContext.User](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext.user#microsoft-aspnetcore-signalr-hubcallercontext-user)) still holds the principal that was cached before the role was removed. The user can keep calling `Editor`-only hub methods until the connection is closed.

To enforce updated authorization on an active connection, take one of the following approaches:

* Close affected connections so that clients reconnect and reauthenticate. For bearer token authentication, the [CloseOnAuthenticationExpiration](configuration?view=aspnetcore-10.0#configure-advanced-http-options) option closes connections when the authentication token expires.
* Perform authorization checks in hub methods against current data, such as the user's current roles or claims from a data store, instead of relying only on the cached principal.

In .NET 11 and later, a client can refresh the credentials for an active connection without reconnecting. When the client presents an updated token, the server re-authenticates it and replaces the cached `Context.User` in place, so later hub method invocations authorize against the refreshed roles and claims. The refreshed principal must map to the same SignalR user, so a refresh updates roles and claims but doesn't change the connection's user identity or routing. For more information, see [Authentication refresh](#authentication-refresh).

### Cookie authentication

In a browser-based app, cookie authentication allows existing user credentials to automatically flow to SignalR connections. When the browser client is used, no extra configuration is needed. If the user is signed in to an app, the SignalR connection automatically inherits this authentication.

Cookies are a browser-specific way to send access tokens, but nonbrowser clients can send them. When the [.NET client](dotnet-client?view=aspnetcore-10.0) is used, the `Cookies` property can be configured in the `.WithUrl` call to provide a cookie. However, using cookie authentication from the .NET client requires the app to provide an API to exchange authentication data for a cookie.

Important

Starting with ASP.NET Core 10, known API endpoints no longer redirect to login pages when using cookie authentication. Instead, they return 401/403 status codes. For details, see [API endpoint authentication behavior in ASP.NET Core](../security/authentication/api-endpoint-auth?view=aspnetcore-10.0).

### Bearer token authentication

The client can provide an access token instead of using a cookie. The server validates the token and uses it to identify the user. For transports that make multiple HTTP requests (for example, `LongPolling` and `ServerSentEvents`), authentication runs on each request, but SignalR caches the resulting principal for the lifetime of the connection. As with cookie authentication, SignalR doesn't automatically revalidate the user during the life of the connection to check for token revocation or for changes to the user's roles or claims. For more information, see [User and role changes during the connection lifetime](#user-and-role-changes-during-the-connection-lifetime).

In the JavaScript client, the token can be provided by using the [accessTokenFactory](configuration?view=aspnetcore-10.0#configure-bearer-authentication) option.

```
// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();
```

In the .NET client, there's a similar [AccessTokenProvider](configuration?view=aspnetcore-10.0#configure-bearer-authentication) property that can be used to configure the token:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();
```

Note

The access token function is called before **every** HTTP request made by SignalR. If the token needs to be renewed in order to keep the connection active, do the renewal from within this function and return the updated token. The token might need to be renewed so it doesn't expire during the connection.

In standard web APIs, bearer tokens are sent in an HTTP header. However, browsers can't set custom headers, such as `Authorization`, on the WebSocket and Server-Sent Events APIs. Because of this browser limitation, SignalR sends the token as a query string parameter for those transports when the client runs in a browser.

The query string fallback is a browser API constraint, not a limitation of SignalR or the WebSocket protocol. Non-browser clients, such as the [.NET client](dotnet-client?view=aspnetcore-10.0), send the token in the `Authorization` header. A reverse proxy, such as [YARP](https://dotnet.github.io/yarp/) fronting a backend-for-frontend (BFF), can also add the `Authorization` header to the proxied WebSocket request, so the backend authenticates with the header instead of the query string.

#### Built-in JWT authentication

On the server, bearer token authentication is configured by using the [JSON web token (JWT) Bearer middleware](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer):

```
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddAuthentication(options =>
{
    // Identity made Cookie authentication the default.
    // However, we want JWT Bearer Auth to be the default.
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
  {
      // Configure the Authority to the expected value for
      // the authentication provider. This ensures the token
      // is appropriately validated.
      options.Authority = "Authority URL"; // TODO: Update URL

      // We have to hook the OnMessageReceived event in order to
      // allow the JWT authentication handler to read the access
      // token from the query string when a WebSocket or
      // Server-Sent Events request comes in.

      // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
      // due to a limitation in Browser APIs. We restrict it to only calls to the
      // SignalR hub in this code.
      // See https://learn.microsoft.com/aspnet/core/signalr/security#access-token-logging
      // for more information about security considerations when using
      // the query string to transmit the access token.
      options.Events = new JwtBearerEvents
      {
          OnMessageReceived = context =>
          {
              var accessToken = context.Request.Query["access_token"];

              // If the request is for our hub...
              var path = context.HttpContext.Request.Path;
              if (!string.IsNullOrEmpty(accessToken) &&
                  (path.StartsWithSegments("/hubs/chat")))
              {
                  // Read the token out of the query string
                  context.Token = accessToken;
              }
              return Task.CompletedTask;
          }
      };
  });

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

// Change to use Name as the user identifier for SignalR
// WARNING: This requires that the source of your JWT token
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages
// intended for a different user!
builder.Services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

// Change to use email as the user identifier for SignalR
// builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

// WARNING: use *either* the NameUserIdProvider *or* the
// EmailBasedUserIdProvider, but do not use both.

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();
```

Note

The query string is used on browsers when connecting with WebSockets and Server-Sent Events due to browser API limitations. When you use HTTPS, the TLS connection secures the query string values. However, many servers log query string values. For more information, see [Security considerations in ASP.NET Core SignalR](security?view=aspnetcore-10.0). SignalR uses headers to transmit tokens in environments that support them, such as the .NET and Java clients.

#### Identity Server JWT authentication

When using Duende IdentityServer, add a [PostConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.postconfigureoptions-1) service to the project:

```
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}
```

Register the service after adding services for authentication (with the [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) method) and the authentication handler for Identity Server (with the [AddIdentityServerJwt](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilderextensions.addidentityserverjwt) method):

```
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
        ConfigureJwtBearerOptions>());

builder.Services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.
```

### Authentication refresh

A SignalR connection can outlive the access token that established it. When [CloseOnAuthenticationExpiration](configuration?view=aspnetcore-10.0#configure-advanced-http-options) is enabled, the server closes the connection after the token expires, and the client must reconnect to continue. Messages sent during the gap are missed, and group and user routing are disrupted until the client reconnects.

Authentication refresh, available in .NET 11 and later, lets a client update the credentials for an active connection without reconnecting. The server re-authenticates the refresh request through the normal endpoint authorization pipeline and replaces the connection's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) in place, as long as the refreshed principal maps to the same SignalR user.

#### Enable authentication refresh on the server

Enable authentication refresh in the hub's `MapHub` options by setting `EnableAuthenticationRefresh` to `true`. Enable it together with `CloseOnAuthenticationExpiration` so that a connection whose token expires without being refreshed in time is closed, rather than left open with stale credentials:

```
app.MapHub<ChatHub>("/chat", options =>
{
    options.CloseOnAuthenticationExpiration = true;
    options.EnableAuthenticationRefresh = true;
});
```

When authentication refresh is enabled and the authentication ticket has an expiration, the negotiate response reports the remaining token lifetime so the client can schedule refreshes.

Setting `EnableAuthenticationRefresh` to `true` adds a `/refresh` endpoint to the app's route table, alongside the hub's existing `/negotiate` endpoint. For a hub mapped at `/chat`, the endpoint is `/chat/refresh`. The endpoint isn't registered when `EnableAuthenticationRefresh` is `false`, so it only appears in the route table for hubs that opt in.

The endpoint behaves as follows:

* It accepts `POST` only. Any other HTTP method returns a 405 status code.

* The client identifies its connection with the connection token in the `id` query string, the same way that send, poll, and delete requests do.

* The request is authenticated by the app's normal authentication and authorization middleware before it reaches the endpoint, and the hub's authorization metadata applies to it.

* A successful refresh returns a 200 status code with a JSON body. When the refreshed ticket has an expiration, the body contains a `tokenLifetimeSeconds` property that the client uses to reschedule automatic refreshes.

* A rejected or invalid refresh returns a JSON body with an `error` property. The status code indicates the reason:

  * 403: The connection's identity check or `OnAuthenticationRefresh` rejected the refresh.
  * 404: The connection token doesn't match a known connection.
  * 400: The request omits the connection token, the connection negotiated protocol version 0, or either the connection's current user or the refreshed principal is a Windows identity.

Include the `/refresh` route when configuring a reverse proxy, firewall, or web application firewall in front of the app, and when writing route-scoped authorization or CORS policies. A proxy that forwards `/negotiate` but blocks `/refresh` causes every refresh attempt to fail, and connections close when their tokens expire if `CloseOnAuthenticationExpiration` is enabled.

To inspect or reject a refresh, set the `OnAuthenticationRefresh` callback. It runs after the refresh request is authenticated but before the connection's user is replaced. Return `false` to reject the refresh, in which case the endpoint responds with an HTTP 403 status code and the connection keeps its current user. The callback is an additional check on top of the built-in verification that the refreshed principal maps to the same SignalR user. It can reject a refresh, but it can't approve one that fails the built-in check:

```
app.MapHub<ChatHub>("/chat", options =>
{
    options.CloseOnAuthenticationExpiration = true;
    options.EnableAuthenticationRefresh = true;
    options.OnAuthenticationRefresh = context =>
    {
        if (!context.NewUser.HasClaim("tenant", "contoso"))
        {
            return Task.FromResult(false);
        }

        return Task.FromResult(true);
    };
});
```

The callback receives an `AuthenticationRefreshContext` with the following properties:

* `PreviousUser`: The [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) currently associated with the connection. Compare it with `NewUser` to detect claim changes, such as a change of tenant or scope.
* `NewUser`: The principal produced by authenticating the refresh request. This principal replaces `PreviousUser` only if the callback returns `true` and the built-in identity check passes.
* `NewExpiration`: The expiration that the refreshed authentication ticket takes if the refresh is accepted, or `null` when the ticket doesn't set one. `MaximumAuthenticationExpiration` is applied before the callback runs.
* `ConnectionId`: The ID of the connection being refreshed.
* `HttpContext`: The [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) of the refresh request.

An exception thrown by the callback isn't caught by SignalR. The exception propagates out of the refresh request, and the connection keeps its current user.

The refreshed principal must map to the same SignalR user as the connection. If it maps to a different user ID, the refresh is rejected: the endpoint responds with an HTTP 403 status code, and the connection keeps its current user and stays connected. A refresh never changes the connection's `Context.UserIdentifier` or reroutes messages sent with `Clients.User`, even for a successful refresh. The routing identifier is fixed when the connection starts. To change it, reconnect the client.

To bound how far a refresh can extend a connection's authentication expiration, set `MaximumAuthenticationExpiration`. The refreshed expiration is capped to at most this amount of time from the current time, even when the token reports a longer lifetime. This cap applies whenever authentication refresh is enabled, including when the token doesn't set an expiration of its own. In that case, the cap gives the connection a known expiration, so the negotiate response reports a token lifetime and the client can schedule automatic refreshes. The value must be greater than zero and doesn't apply to Windows authentication, which is never tracked or refreshed.

#### Refresh authentication from the .NET client

The .NET client refreshes credentials using the `AccessTokenProvider` configured on the connection. Each refresh calls `AccessTokenProvider` to fetch a fresh access token rather than reusing the token cached when the connection started.

To refresh explicitly, call `RefreshAuthenticationAsync`, which returns the new token lifetime reported by the server:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chat", options =>
    {
        options.AccessTokenProvider = GetAccessTokenAsync;
    })
    .Build();

await connection.StartAsync();

TimeSpan? newLifetime = await connection.RefreshAuthenticationAsync();
```

Automatic refresh is enabled by default. The client schedules a refresh ahead of the reported expiration whenever the server reports a token lifetime. To change the timing or to turn automatic refresh off, call `WithAuthenticationRefresh` and configure `AuthenticationRefreshOptions`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chat", options =>
    {
        options.AccessTokenProvider = GetAccessTokenAsync;
    })
    .WithAuthenticationRefresh(options =>
    {
        options.RefreshBeforeExpiration = TimeSpan.FromMinutes(2);
    })
    .Build();
```

`AuthenticationRefreshOptions` provides the following settings:

* `EnableAutoRefresh`: Enables automatic refresh before the token expires. Defaults to `true`. The client only schedules a refresh when the server reports a token lifetime. If the server doesn't report a lifetime, no automatic refresh is scheduled, and `RefreshAuthenticationAsync` can still be called manually.
* `RefreshBeforeExpiration`: How far ahead of the reported expiration to refresh. Defaults to five minutes.

To observe refreshes, handle the `AuthenticationRefreshed` and `AuthenticationRefreshFailed` events on the connection. Both automatic and manual refreshes raise these events:

```
connection.AuthenticationRefreshed += context =>
{
    Console.WriteLine(
        $"Authentication refreshed. New lifetime: {context.NewTokenLifetime}");
    return Task.CompletedTask;
};

connection.AuthenticationRefreshFailed += context =>
{
    Console.WriteLine(
        $"Authentication refresh failed: {context.Exception}");
    return Task.CompletedTask;
};
```

#### Refresh authentication from the JavaScript client

The JavaScript client refreshes credentials using the `accessTokenFactory` configured on the connection. Each refresh calls `accessTokenFactory` to fetch a fresh access token.

To refresh explicitly, call `refreshAuthentication`, which resolves with the new token lifetime in seconds reported by the server:

```
const newLifetimeInSeconds = await connection.refreshAuthentication();
```

To refresh automatically before the token expires, call `withAuthenticationRefresh` and optionally configure `IAuthenticationRefreshOptions`. Handle refresh results with `onAuthenticationRefreshed` and `onAuthenticationRefreshFailed`:

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chat", {
        accessTokenFactory: () => getAccessToken()
    })
    .withAuthenticationRefresh({
        refreshBeforeExpirationInMilliseconds: 120000
    })
    .build();

connection.onAuthenticationRefreshed(context => {
    console.log(
        `Authentication refreshed. New lifetime: ${context.newTokenLifetimeInSeconds}`);
});

connection.onAuthenticationRefreshFailed(context => {
    console.log(`Authentication refresh failed: ${context.error}`);
});
```

`IAuthenticationRefreshOptions` provides the following settings:

* `enableAutoRefresh`: Enables automatic refresh before the token expires. Defaults to `true`. As with the .NET client, automatic refresh is only scheduled when the server reports a token lifetime.
* `refreshBeforeExpirationInMilliseconds`: How far ahead of the reported expiration to refresh, in milliseconds. Defaults to 300,000 (five minutes).

The context passed to `onAuthenticationRefreshed` provides:

* `connection`: The `HubConnection` that was refreshed.
* `newTokenLifetimeInSeconds`: The new token lifetime reported by the server, or `undefined` when the server doesn't report one.
* `refreshedAt`: The time the refresh completed.

The context passed to `onAuthenticationRefreshFailed` provides `connection` and `error`. Both automatic and manual refreshes invoke these handlers, and a failed call to `refreshAuthentication` also rejects with the same error.

A refresh fails when the server responds with any status code other than 200. That includes the HTTP 403 status code returned when the refreshed principal maps to a different SignalR user or when `OnAuthenticationRefresh` rejects the refresh, which is a policy decision rather than a transient error. A failed refresh doesn't close the connection on its own and isn't retried automatically. The connection continues with its existing credentials until the token expires, and is then closed if `CloseOnAuthenticationExpiration` is enabled. Handle `onAuthenticationRefreshFailed` to prompt the user to sign in again, and call `refreshAuthentication` after the app obtains new credentials.

#### React to a refresh in the hub

Override `OnAuthenticationRefreshedAsync` in the hub to run code after the refreshed principal is applied to the connection. `Context.User` reflects the refreshed principal. As described earlier, `Context.UserIdentifier` and SignalR user routing don't change on a refresh:

```
public class ChatHub : Hub
{
    public override Task OnAuthenticationRefreshedAsync()
    {
        return Clients.Caller.SendAsync(
            "AuthenticationRefreshed", Context.UserIdentifier);
    }
}
```

A hub method that's already running keeps the `Context.User` it started with. Later invocations observe the refreshed `Context.User`. For more information about how SignalR caches the authenticated user, see [User and role changes during the connection lifetime](#user-and-role-changes-during-the-connection-lifetime).

Authentication refresh requires a connection that negotiated protocol version 1 or later. Automatic refresh is scheduled only when the server reports a token lifetime, which typically comes from an authentication scheme that sets an expiration, such as bearer tokens. Windows authentication doesn't report an expiration and isn't tracked or refreshed by this feature.

### Cookies versus bearer tokens

Cookies are specific to browsers. Sending them from other kinds of clients adds complexity compared to sending bearer tokens. Cookie authentication isn't recommended unless the app only needs to authenticate users from the browser client. Bearer token authentication is the recommended approach when using clients other than the browser client.

### Windows authentication

If [Windows authentication](../security/authentication/windowsauth?view=aspnetcore-10.0) is configured in the app, SignalR can use that identity to secure hubs. However, to send messages to individual users, add a custom User ID provider. The Windows authentication system doesn't provide the "Name Identifier" claim. SignalR uses the claim to determine the user name.

Add a new class that implements `IUserIdProvider` and retrieve one of the claims from the user for use as the identifier. For example, to use the "Name" claim (which is the Windows username in the form `[Domain]/[Username]`), create the following class:

```
public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}
```

Rather than `ClaimTypes.Name`, use any value from the `User`, such as the Windows SID identifier, and so on.

Note

The specified value must be unique among all the users in the system. Otherwise, a message intended for one user might end up reaching a different user.

Register this component in the `Program.cs` file:

```
using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.SignalR;
using SignalRAuthenticationSample;

var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();

services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
services.AddRazorPages();

services.AddSignalR();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

var app = builder.Build();

// Code removed for brevity.
```

In the .NET client, Windows authentication must be enabled by setting the [UseDefaultCredentials](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.client.httpconnectionoptions.usedefaultcredentials) property:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();
```

Windows authentication is supported in Microsoft Edge, but not in all browsers. For example, in Chrome and Safari, attempting to use Windows authentication and WebSockets fails. When Windows authentication fails, the client attempts to fall back to other transports, which might work.

### Use claims to customize identity handling

An app that authenticates users can derive SignalR user IDs from user claims. To specify how SignalR creates user IDs, implement `IUserIdProvider` and register the implementation.

The sample code demonstrates how to use claims to select the user's email address as the identifying property.

Note

The specified value must be unique among all the users in the system. Otherwise, a message intended for one user might end up reaching a different user.

```
public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value!;
    }
}
```

The account registration adds a claim with type `ClaimTypes.Email` to the ASP.NET identity database.

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
                                                                          .ToList();
    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
        var result = await _userManager.CreateAsync(user, Input.Password);

        // Add the email claim and value for this user.
        await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));

        // Remaining code removed for brevity.
```

Register this component in the `Program.cs` file:

```
builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();
```

## Authorize users to access hubs and hub methods

By default, an unauthenticated user can call all methods in a hub. To require authentication, apply the [AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute to the hub:

```
[Authorize]
public class ChatHub: Hub
{
}
```

The constructor arguments and properties of the `[Authorize]` attribute can be used to restrict access to only users matching specific [authorization policies](../security/authorization/policies?view=aspnetcore-10.0). For example, with the custom authorization policy called `MyAuthorizationPolicy`, only users matching that policy can access the hub by using the following code:

```
[Authorize("MyAuthorizationPolicy")]
public class ChatPolicyHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Clients.All.SendAsync("ReceiveSystemMessage", 
                                    $"{Context.UserIdentifier} joined.");
        await base.OnConnectedAsync();
    }
    // Code removed for brevity.
```

The `[Authorize]` attribute can be applied to individual hub methods. If the current user doesn't match the policy applied to the method, an error is returned to the caller:

```
[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... Send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... Ban a user from the chat room (something only Administrators can do) ...
    }
}
```

### Use authorization handlers to customize hub method authorization

SignalR provides a custom resource to authorization handlers when a hub method requires authorization. The resource is an instance of [HubInvocationContext](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubinvocationcontext). The `HubInvocationContext` includes the [HubCallerContext](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext), the name of the hub method being invoked, and the arguments to the hub method.

Consider the example of a chat room that allows multiple organizations to sign in via Microsoft Entra ID. Anyone with a Microsoft account can sign in to chat, but only members of the owning organization should be able to ban users or view users' chat histories. Also, there might be a need to restrict some functionality from specific users. In this scenario, notice how the `DomainRestrictedRequirement` serves as a custom [IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement). Because the `HubInvocationContext` resource parameter is passed in, the internal logic can inspect the context in which the hub is being called, and make decisions on allowing the user to execute individual hub methods:

```
[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}
```

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

namespace SignalRAuthenticationSample;

public class DomainRestrictedRequirement :
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>,
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement,
        HubInvocationContext resource)
    {
        if (context.User.Identity != null &&
          !string.IsNullOrEmpty(context.User.Identity.Name) && 
          IsUserAllowedToDoThis(resource.HubMethodName,
                               context.User.Identity.Name) &&
          context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
                context.Succeed(requirement);
            
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") &&
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}
```

In the `Program.cs` file, add the new policy, providing the custom `DomainRestrictedRequirement` requirement as a parameter to create the `DomainRestricted` policy:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var services = builder.Services;

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
services.AddDatabaseDeveloperPageExceptionFilter();

services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthorization(options =>
   {
       options.AddPolicy("DomainRestricted", policy =>
       {
           policy.Requirements.Add(new DomainRestrictedRequirement());
       });
   });

services.AddRazorPages();

var app = builder.Build();

// Code removed for brevity.
```

In the preceding example, the `DomainRestrictedRequirement` class is both an `IAuthorizationRequirement` and its own `AuthorizationHandler` for that requirement. It's acceptable to split these two components into separate classes to separate concerns. The approach in this example provides the benefit of not having to inject the `AuthorizationHandler` during startup because the requirement and the handler are the same thing.

As an alternative to registering the `DomainRestricted` policy and referencing it with `[Authorize("DomainRestricted")]`, you can apply an [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) attribute directly to a hub or hub method. SignalR combines the attribute's requirements into the effective policy for the method invocation. For more information, see [Custom authorization policies with \`IAuthorizationRequirementData\`](../security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata?view=aspnetcore-10.0).

## Additional resources

* [Bearer token authentication in ASP.NET Core (blog)](https://devblogs.microsoft.com/dotnet/bearer-token-authentication-in-asp-net-core/)
* [Resource-based authorization](../security/authorization/resource-based?view=aspnetcore-10.0)
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/authn-and-authz/samples/) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/authn-and-authz/samples/) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

## Authenticate users connecting to a SignalR hub

SignalR can be used with [ASP.NET Core authentication](../security/authentication/identity?view=aspnetcore-10.0) to associate a user with each connection. In a hub, authentication data can be accessed from the [HubConnectionContext.User](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubconnectioncontext.user#microsoft-aspnetcore-signalr-hubconnectioncontext-user) property. Authentication allows the hub to call methods on all connections associated with a user. For more information, see [Manage users and groups in SignalR](groups?view=aspnetcore-10.0). Multiple connections may be associated with a single user.

The following is an example of `Startup.Configure` which uses SignalR and ASP.NET Core authentication:

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chat");
        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}
```

Note

If a token expires during the lifetime of a connection, the connection continues to work. `LongPolling` and `ServerSentEvents` connections fail on subsequent requests if they don't send new access tokens.

### Cookie authentication

In a browser-based app, cookie authentication allows your existing user credentials to automatically flow to SignalR connections. When using the browser client, no additional configuration is needed. If the user is logged in to your app, the SignalR connection automatically inherits this authentication.

Cookies are a browser-specific way to send access tokens, but non-browser clients can send them. When using the [.NET Client](dotnet-client?view=aspnetcore-10.0), the `Cookies` property can be configured in the `.WithUrl` call to provide a cookie. However, using cookie authentication from the .NET client requires the app to provide an API to exchange authentication data for a cookie.

### Bearer token authentication

The client can provide an access token instead of using a cookie. The server validates the token and uses it to identify the user. This validation is done only when the connection is established. During the life of the connection, the server doesn't automatically revalidate to check for token revocation.

In the JavaScript client, the token can be provided using the [accessTokenFactory](configuration?view=aspnetcore-10.0#configure-bearer-authentication) option.

```
// Connect, using the token we got.
this.connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat", { accessTokenFactory: () => this.loginToken })
    .build();
```

In the .NET client, there's a similar [AccessTokenProvider](configuration?view=aspnetcore-10.0#configure-bearer-authentication) property that can be used to configure the token:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
    })
    .Build();
```

Note

The access token function you provide is called before **every** HTTP request made by SignalR. If you need to renew the token in order to keep the connection active (because it may expire during the connection), do so from within this function and return the updated token.

In standard web APIs, bearer tokens are sent in an HTTP header. However, browsers can't set custom headers, such as `Authorization`, on the WebSocket and Server-Sent Events APIs. Because of this browser limitation, SignalR sends the token as a query string parameter for those transports when the client runs in a browser.

The query string fallback is a browser API constraint, not a limitation of SignalR or the WebSocket protocol. Non-browser clients, such as the [.NET client](dotnet-client?view=aspnetcore-10.0), send the token in the `Authorization` header. A reverse proxy, such as [YARP](https://dotnet.github.io/yarp/) fronting a backend-for-frontend (BFF), can also add the `Authorization` header to the proxied WebSocket request, so the backend authenticates with the header instead of the query string.

#### Built-in JWT authentication

On the server, bearer token authentication is configured using the [JWT bearer middleware](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer):

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication(options =>
        {
            // Identity made Cookie authentication the default.
            // However, we want JWT Bearer Auth to be the default.
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            // Configure the Authority to the expected value for your authentication provider
            // This ensures the token is appropriately validated
            options.Authority = /* TODO: Insert Authority URL here */;

            // We have to hook the OnMessageReceived event in order to
            // allow the JWT authentication handler to read the access
            // token from the query string when a WebSocket or
            // Server-Sent Events request comes in.

            // Sending the access token in the query string is required when using WebSockets or ServerSentEvents
            // due to a limitation in Browser APIs. We restrict it to only calls to the
            // SignalR hub in this code.
            // See https://learn.microsoft.com/aspnet/core/signalr/security#access-token-logging
            // for more information about security considerations when using
            // the query string to transmit the access token.
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    var accessToken = context.Request.Query["access_token"];

                    // If the request is for our hub...
                    var path = context.HttpContext.Request.Path;
                    if (!string.IsNullOrEmpty(accessToken) &&
                        (path.StartsWithSegments("/hubs/chat")))
                    {
                        // Read the token out of the query string
                        context.Token = accessToken;
                    }
                    return Task.CompletedTask;
                }
            };
        });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddSignalR();

    // Change to use Name as the user identifier for SignalR
    // WARNING: This requires that the source of your JWT token
    // ensures that the Name claim is unique!
    // If the Name claim isn't unique, users could receive messages
    // intended for a different user!
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

    // Change to use email as the user identifier for SignalR
    // services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();

    // WARNING: use *either* the NameUserIdProvider *or* the
    // EmailBasedUserIdProvider, but do not use both.
}
```

Note

The query string is used on browsers when connecting with WebSockets and Server-Sent Events due to browser API limitations. When using HTTPS, query string values are secured by the TLS connection. However, many servers log query string values. For more information, see [Security considerations in ASP.NET Core SignalR](security?view=aspnetcore-10.0). SignalR uses headers to transmit tokens in environments which support them (such as the .NET and Java clients).

#### Identity Server JWT authentication

When using Identity Server, add a [PostConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.postconfigureoptions-1) service to the project:

```
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;

                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}
```

Register the service in `Startup.ConfigureServices` after adding services for authentication ([AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication)) and the authentication handler for Identity Server ([AddIdentityServerJwt](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilderextensions.addidentityserverjwt)):

```
services.AddAuthentication()
    .AddIdentityServerJwt();
services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
        ConfigureJwtBearerOptions>());
```

### Cookies vs. bearer tokens

Cookies are specific to browsers. Sending them from other kinds of clients adds complexity compared to sending bearer tokens. Consequently, cookie authentication isn't recommended unless the app only needs to authenticate users from the browser client. Bearer token authentication is the recommended approach when using clients other than the browser client.

### Windows authentication

If [Windows authentication](../security/authentication/windowsauth?view=aspnetcore-10.0) is configured in your app, SignalR can use that identity to secure hubs. However, to send messages to individual users, you need to add a custom User ID provider. The Windows authentication system doesn't provide the "Name Identifier" claim. SignalR uses the claim to determine the user name.

Add a new class that implements `IUserIdProvider` and retrieve one of the claims from the user to use as the identifier. For example, to use the "Name" claim (which is the Windows username in the form `[Domain]\[Username]`), create the following class:

```
public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}
```

Rather than `ClaimTypes.Name`, you can use any value from the `User` (such as the Windows SID identifier, and so on).

Note

The value you choose must be unique among all the users in your system. Otherwise, a message intended for one user could end up going to a different user.

Register this component in your `Startup.ConfigureServices` method.

```
public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

    services.AddSignalR();
    services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
}
```

In the .NET Client, Windows Authentication must be enabled by setting the [UseDefaultCredentials](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.client.httpconnectionoptions.usedefaultcredentials) property:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options =>
    {
        options.UseDefaultCredentials = true;
    })
    .Build();
```

Windows authentication is supported in Internet Explorer and Microsoft Edge, but not in all browsers. For example, in Chrome and Safari, attempting to use Windows authentication and WebSockets fails. When Windows authentication fails, the client attempts to fall back to other transports which might work.

### Use claims to customize identity handling

An app that authenticates users can derive SignalR user IDs from user claims. To specify how SignalR creates user IDs, implement `IUserIdProvider` and register the implementation.

The sample code demonstrates how you would use claims to select the user's email address as the identifying property.

Note

The value you choose must be unique among all the users in your system. Otherwise, a message intended for one user could end up going to a different user.

```
public class EmailBasedUserIdProvider : IUserIdProvider
{
    public virtual string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
    }
}
```

The account registration adds a claim with type `ClaimTypes.Email` to the ASP.NET identity database.

```
// create a new user
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);

// add the email claim and value for this user
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Input.Email));
```

Register this component in your `Startup.ConfigureServices`.

```
services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();
```

## Authorize users to access hubs and hub methods

By default, all methods in a hub can be called by an unauthenticated user. To require authentication, apply the [AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute to the hub:

```
[Authorize]
public class ChatHub: Hub
{
}
```

You can use the constructor arguments and properties of the `[Authorize]` attribute to restrict access to only users matching specific [authorization policies](../security/authorization/policies?view=aspnetcore-10.0). For example, if you have a custom authorization policy called `MyAuthorizationPolicy` you can ensure that only users matching that policy can access the hub using the following code:

```
[Authorize("MyAuthorizationPolicy")]
public class ChatHub : Hub
{
}
```

Individual hub methods can have the `[Authorize]` attribute applied as well. If the current user doesn't match the policy applied to the method, an error is returned to the caller:

```
[Authorize]
public class ChatHub : Hub
{
    public async Task Send(string message)
    {
        // ... send a message to all users ...
    }

    [Authorize("Administrators")]
    public void BanUser(string userName)
    {
        // ... ban a user from the chat room (something only Administrators can do) ...
    }
}
```

### Use authorization handlers to customize hub method authorization

SignalR provides a custom resource to authorization handlers when a hub method requires authorization. The resource is an instance of `HubInvocationContext`. The `HubInvocationContext` includes the `HubCallerContext`, the name of the hub method being invoked, and the arguments to the hub method.

Consider the example of a chat room allowing multiple organization sign-in via Microsoft Entra ID. Anyone with a Microsoft account can sign in to chat, but only members of the owning organization should be able to ban users or view users' chat histories. Furthermore, we might want to restrict certain functionality from certain users. Using the updated features in ASP.NET Core 3.0, this is entirely possible. Note how the `DomainRestrictedRequirement` serves as a custom `IAuthorizationRequirement`. Now that the `HubInvocationContext` resource parameter is being passed in, the internal logic can inspect the context in which the Hub is being called and make decisions on allowing the user to execute individual Hub methods.

```
[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}

public class DomainRestrictedRequirement :
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>,
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement,
        HubInvocationContext resource)
    {
        if (IsUserAllowedToDoThis(resource.HubMethodName, context.User.Identity.Name) &&
            context.User.Identity.Name.EndsWith("@microsoft.com"))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName,
        string currentUsername)
    {
        return !(currentUsername.Equals("asdf42@microsoft.com") &&
            hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase));
    }
}
```

In `Startup.ConfigureServices`, add the new policy, providing the custom `DomainRestrictedRequirement` requirement as a parameter to create the `DomainRestricted` policy.

```
public void ConfigureServices(IServiceCollection services)
{
    // ... other services ...

    services
        .AddAuthorization(options =>
        {
            options.AddPolicy("DomainRestricted", policy =>
            {
                policy.Requirements.Add(new DomainRestrictedRequirement());
            });
        });
}
```

In the preceding example, the `DomainRestrictedRequirement` class is both an `IAuthorizationRequirement` and its own `AuthorizationHandler` for that requirement. It's acceptable to split these two components into separate classes to separate concerns. A benefit of the example's approach is there's no need to inject the `AuthorizationHandler` during startup, as the requirement and the handler are the same thing.

## Additional resources

* [Bearer Token Authentication in ASP.NET Core](https://blogs.msdn.microsoft.com/webdev/2016/10/27/bearer-token-authentication-in-asp-net-core/)
* [Resource-based authorization](../security/authorization/resource-based?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/spa/react
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/spa/react.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use React with ASP.NET Core

To get started, follow the [Create an ASP.NET Core app with React](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-react) tutorial in the Visual Studio documentation.

The ASP.NET Core with React project template provides a convenient starting point for ASP.NET Core apps using React and [Create React App](https://create-react-app.dev/) (CRA) to implement a rich, client-side user interface (UI).

The project template is equivalent to creating both an ASP.NET Core project to act as a web API and a CRA React project to act as a UI. This project combination offers the convenience of hosting both projects in a single ASP.NET Core project that can be built and published as a single unit.

The project template isn't meant for server-side rendering (SSR). For SSR with React and Node.js, consider [Next.js](https://nextjs.org/) or [Razzle](https://razzlejs.org/).

## Create a new app

Create a new project from a command prompt using the command `dotnet new react` in an empty directory. For example, the following commands create the app in a `my-new-app` directory and switch to that directory:

```
dotnet new react -o my-new-app
cd my-new-app
```

Run the app from either Visual Studio or the .NET CLI:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

Open the generated `.csproj` file, and run the app as normal from there.

The build process restores npm dependencies on the first run, which can take several minutes. Subsequent builds are much faster.

Ensure you have an environment variable called `ASPNETCORE_ENVIRONMENT` with value of `Development`. On Windows (in non-PowerShell prompts), run `SET ASPNETCORE_ENVIRONMENT=Development`. On Linux or macOS, run `export ASPNETCORE_ENVIRONMENT=Development`.

Run [dotnet build](/en-us/dotnet/core/tools/dotnet-build) to verify your app builds correctly. On the first run, the build process restores npm dependencies, which can take several minutes. Subsequent builds are much faster.

Run [dotnet run](/en-us/dotnet/core/tools/dotnet-run) to start the app.

The project template creates an ASP.NET Core app and a React app. The ASP.NET Core app is intended to be used for data access, authorization, and other server-side concerns. The React app, residing in the `ClientApp` subdirectory, is intended to be used for all UI concerns.

## Add pages, images, styles, modules, etc.

The `ClientApp` directory is a standard CRA React app. See the official [CRA documentation](https://create-react-app.dev/docs/getting-started/) for more information.

There are slight differences between the React app created by this template and the one created by CRA itself; however, the app's capabilities are unchanged. The app created by the template contains a [Bootstrap](https://getbootstrap.com/)-based layout and a basic routing example.

## Install npm packages

To install third-party npm packages, use a command prompt in the `ClientApp` subdirectory. For example:

```
cd ClientApp
npm install <package_name>
```

## Publish and deploy

In development, the app runs in a mode optimized for developer convenience. For example, JavaScript bundles include source maps (so that when debugging, you can see your original source code). The app watches JavaScript, HTML, and CSS file changes on disk and automatically recompiles and reloads when it sees those files change.

In production, serve a version of your app that's optimized for performance. This is configured to happen automatically. When you publish, the build configuration emits a minified, transpiled build of your client-side code. Unlike the development build, the production build doesn't require Node.js to be installed on the server.

You can use standard [ASP.NET Core hosting and deployment methods](../../host-and-deploy/?view=aspnetcore-10.0).

## Run the CRA server independently

The project is configured to start its own instance of the CRA development server in the background when the ASP.NET Core app starts in development mode. This is convenient because it means you don't have to run a separate server manually.

There's a drawback to this default setup. Each time you modify your C# code and your ASP.NET Core app needs to restart, the CRA server restarts. A few seconds are required to start back up. If you're making frequent C# code edits and don't want to wait for the CRA server to restart, run the CRA server externally, independently of the ASP.NET Core process.

To run the CRA server externally, switch to the `ClientApp` subdirectory in a command prompt and launch the CRA development server:

```
cd ClientApp
npm start
```

When you start your ASP.NET Core app, it won't launch a CRA server. The instance you started manually is used instead. This enables it to start and restart faster. It's no longer waiting for your React app to rebuild each time.

## Configure proxy middleware for SignalR

For more information, see [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware).

The updated React project template provides a convenient starting point for ASP.NET Core apps using React and [create-react-app](https://github.com/facebookincubator/create-react-app) (CRA) conventions to implement a rich, client-side user interface (UI).

The template is equivalent to creating both an ASP.NET Core project to act as an API backend, and a standard CRA React project to act as a UI, but with the convenience of hosting both in a single app project that can be built and published as a single unit.

The React project template isn't meant for server-side rendering (SSR). For SSR with React and Node.js, consider [Next.js](https://github.com/zeit/next.js/) or [Razzle](https://github.com/jaredpalmer/razzle).

## Create a new app

Create a new project from a command prompt using the command `dotnet new react` in an empty directory. For example, the following commands create the app in a `my-new-app` directory and switch to that directory:

```
dotnet new react -o my-new-app
cd my-new-app
```

Run the app from either Visual Studio or the .NET CLI:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

Open the generated `.csproj` file, and run the app as normal from there.

The build process restores npm dependencies on the first run, which can take several minutes. Subsequent builds are much faster.

Ensure you have an environment variable called `ASPNETCORE_ENVIRONMENT` with value of `Development`. On Windows (in non-PowerShell prompts), run `SET ASPNETCORE_ENVIRONMENT=Development`. On Linux or macOS, run `export ASPNETCORE_ENVIRONMENT=Development`.

Run [dotnet build](/en-us/dotnet/core/tools/dotnet-build) to verify your app builds correctly. On the first run, the build process restores npm dependencies, which can take several minutes. Subsequent builds are much faster.

Run [dotnet run](/en-us/dotnet/core/tools/dotnet-run) to start the app.

The project template creates an ASP.NET Core app and a React app. The ASP.NET Core app is intended to be used for data access, authorization, and other server-side concerns. The React app, residing in the `ClientApp` subdirectory, is intended to be used for all UI concerns.

## Add pages, images, styles, modules, etc.

The `ClientApp` directory is a standard CRA React app. See the official [CRA documentation](https://create-react-app.dev/docs/getting-started/) for more information.

There are slight differences between the React app created by this template and the one created by CRA itself; however, the app's capabilities are unchanged. The app created by the template contains a [Bootstrap](https://getbootstrap.com/)-based layout and a basic routing example.

## Install npm packages

To install third-party npm packages, use a command prompt in the `ClientApp` subdirectory. For example:

```
cd ClientApp
npm install --save <package_name>
```

## Publish and deploy

In development, the app runs in a mode optimized for developer convenience. For example, JavaScript bundles include source maps (so that when debugging, you can see your original source code). The app watches JavaScript, HTML, and CSS file changes on disk and automatically recompiles and reloads when it sees those files change.

In production, serve a version of your app that's optimized for performance. This is configured to happen automatically. When you publish, the build configuration emits a minified, transpiled build of your client-side code. Unlike the development build, the production build doesn't require Node.js to be installed on the server.

You can use standard [ASP.NET Core hosting and deployment methods](../../host-and-deploy/?view=aspnetcore-10.0).

## Run the CRA server independently

The project is configured to start its own instance of the CRA development server in the background when the ASP.NET Core app starts in development mode. This is convenient because it means you don't have to run a separate server manually.

There's a drawback to this default setup. Each time you modify your C# code and your ASP.NET Core app needs to restart, the CRA server restarts. A few seconds are required to start back up. If you're making frequent C# code edits and don't want to wait for the CRA server to restart, run the CRA server externally, independently of the ASP.NET Core process. To do so:

1. Add a `.env` file to the `ClientApp` subdirectory with the following setting:

   ```
   BROWSER=none
   ```

   This will prevent your web browser from opening when starting the CRA server externally.

2. In a command prompt, switch to the `ClientApp` subdirectory, and launch the CRA development server:

   ```
   cd ClientApp
   npm start
   ```

3. Modify your ASP.NET Core app to use the external CRA server instance instead of launching one of its own. In your *Startup* class, replace the `spa.UseReactDevelopmentServer` invocation with the following:

   ```
   spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
   ```

When you start your ASP.NET Core app, it won't launch a CRA server. The instance you started manually is used instead. This enables it to start and restart faster. It's no longer waiting for your React app to rebuild each time.

Important

"Server-side rendering" is not a supported feature of this template. Our goal with this template is to meet parity with "create-react-app". As such, scenarios and features not included in a "create-react-app" project (such as SSR) are not supported and are left as an exercise for the user.

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-storage-providers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/implementation/key-storage-providers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Key storage providers in ASP.NET Core

The data protection system [employs a discovery mechanism by default](../configuration/default-settings?view=aspnetcore-10.0) to determine where cryptographic keys should be persisted. The developer can override the default discovery mechanism and manually specify the location.

Warning

If you specify an explicit key persistence location, the data protection system deregisters the default key encryption at rest mechanism, so keys are no longer encrypted at rest. It's recommended that you additionally [specify an explicit key encryption mechanism](key-encryption-at-rest?view=aspnetcore-10.0) for production deployments.

## File system

To configure a file system-based key repository, call the [PersistKeysToFileSystem](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.persistkeystofilesystem) configuration routine as shown below. Provide a [DirectoryInfo](/en-us/dotnet/api/system.io.directoryinfo) pointing to the repository where keys should be stored:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys\"));
}
```

The `DirectoryInfo` can point to a directory on the local machine, or it can point to a folder on a network share. If pointing to a directory on the local machine (and the scenario is that only apps on the local machine require access to use this repository), consider using [Windows DPAPI](key-encryption-at-rest?view=aspnetcore-10.0) (on Windows) to encrypt the keys at rest. Otherwise, consider using an [X.509 certificate](key-encryption-at-rest?view=aspnetcore-10.0) to encrypt keys at rest.

## Azure Storage

The [`Azure.Extensions.AspNetCore.DataProtection.Blobs` NuGet package](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Blobs) provides API for storing data protection keys in Azure Blob Storage. Keys can be shared across several instances of a web app. Apps can share authentication cookies or CSRF protection across multiple servers.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

To interact with [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) locally using developer credentials, either sign into your storage account in Visual Studio or sign in with the [Azure CLI](/en-us/cli/azure/). If you haven't already installed the Azure CLI, see [How to install the Azure CLI](/en-us/cli/azure/install-azure-cli). You can execute the following command in the Developer PowerShell panel in Visual Studio or from a command shell when not using Visual Studio:

```
az login
```

For more information, see [Sign-in to Azure using developer tooling](/en-us/dotnet/azure/sdk/authentication/local-development-dev-accounts#sign-in-to-azure-using-developer-tooling).

Configure Azure Blob Storage to maintain data protection keys:

* Create an Azure storage account.

* Create a container to hold the data protection key file.

* We recommend using Azure Managed Identity and role-based access control (RBAC) to access the key storage blob. ***You don't need to create a key file and upload it to the container of the storage account.*** The framework creates the file for you. To inspect the contents of a key file, use the context menu's **View/edit** command at the end of a key row in the portal.

Note

If you plan to use a blob URI with a shared access signature (SAS) instead of a Managed Identity, use a text editor to create an XML key file on your local machine:

```
<?xml version="1.0" encoding="utf-8"?>
<repository>
</repository>
```

Upload the key file to the container of the storage account. Use the context menu's **View/edit** command at the end of the key row in the portal to confirm that the blob contains the preceding content. By creating the file manually, you're able to obtain the blob URI with SAS from the portal for configuring the app in a later step.

* Create an Azure Managed Identity (or add a role to the existing Managed Identity that you plan to use) with the **Storage Blob Data Contributor** role. Assign the Managed Identity to the Azure App Service that's hosting the deployment: **Settings** > **Identity** > **User assigned** > **Add**.

  Note

  If you also plan to run an app locally with an authorized user for blob access using the [Azure CLI](/en-us/cli/azure/) or Visual Studio's Azure Service Authentication, add your developer Azure user account in **Access Control (IAM)** with the **Storage Blob Data Contributor** role. If you want to use the Azure CLI through Visual Studio, execute the `az login` command from the Developer PowerShell panel and follow the prompts to authenticate with the tenant.

To configure the Azure Blob Storage provider, call one of the [PersistKeysToAzureBlobStorage](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azuredataprotectionbuilderextensions.persistkeystoazureblobstorage) overloads in the app. The following example uses the overload that accepts a blob URI and token credential ([TokenCredential](/en-us/dotnet/api/azure.core.tokencredential)), relying on an Azure Managed Identity for role-based access control (RBAC).

Other overloads are based on:

* A blob URI and storage shared key credential ([StorageSharedKeyCredential](/en-us/dotnet/api/azure.storage.storagesharedkeycredential)).
* A blob URI with a shared access signature (SAS).
* A connection string, container name, and blob name.
* A blob client ([BlobClient](/en-us/dotnet/api/azure.storage.blobs.blobclient)).

For more information on the Azure SDK's API and authentication, see [Authenticate .NET apps to Azure services using the Azure Identity library](/en-us/dotnet/azure/sdk/authentication/). For logging guidance, see [Logging with the Azure SDK for .NET: Logging without client registration](/en-us/dotnet/azure/sdk/logging#logging-without-client-registration). For apps using dependency injection, an app can call [AddAzureClientsCore](/en-us/dotnet/api/microsoft.extensions.azure.azureclientservicecollectionextensions.addazureclientscore), passing `true` for `enableLogForwarding`, to create and wire up the logging infrastructure.

In the `Program` file where services are registered:

```
TokenCredential? credential;

if (builder.Environment.IsProduction())
{
    credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
    // Local development and testing only
    DefaultAzureCredentialOptions options = new()
    {
        // Specify the tenant ID to use the dev credentials when running the app locally
        // in Visual Studio.
        VisualStudioTenantId = "{TENANT ID}",
        SharedTokenCacheTenantId = "{TENANT ID}"
    };

    credential = new DefaultAzureCredential(options);
}

builder.Services.AddDataProtection()
    .SetApplicationName("{APPLICATION NAME}")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential);
```

`{MANAGED IDENTITY CLIENT ID}`: The Azure Managed Identity Client ID (GUID).

`{TENANT ID}`: Tenant ID.

`{APPLICATION NAME}`: [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the unique name of this app within the data protection system. The value should match across deployments of the app.

`{BLOB URI}`: Full URI to the key file. The URI is generated by Azure Storage when you create the key file. Do not use a SAS.

**Alternative shared-access signature (SAS) approach**: As an alternative to using a Managed Identity for access to the key blob in Azure Blob Storage, you can call the [PersistKeysToAzureBlobStorage](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azuredataprotectionbuilderextensions.persistkeystoazureblobstorage) overload that accepts a blob URI with a SAS token:

```
builder.Services.AddDataProtection()
    .SetApplicationName("{APPLICATION NAME}")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI WITH SAS}"));
```

In `Startup.ConfigureServices`:

```
TokenCredential? credential;

if (_env.IsProduction())
{
    credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
    // Local development and testing only
    DefaultAzureCredentialOptions options = new()
    {
        // Specify the tenant ID to use the dev credentials when running the app locally
        // in Visual Studio.
        VisualStudioTenantId = "{TENANT ID}",
        SharedTokenCacheTenantId = "{TENANT ID}"
    };

    credential = new DefaultAzureCredential(options);
}

services.AddDataProtection()
    .SetApplicationName("{APPLICATION NAME}")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential);
```

`{MANAGED IDENTITY CLIENT ID}`: The Azure Managed Identity Client ID (GUID).

`{TENANT ID}`: Tenant ID.

`{APPLICATION NAME}`: [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the unique name of this app within the data protection system. The value should match across deployments of the app.

`{BLOB URI}`: Full URI to the key file. The URI is generated by Azure Storage when you create the key file. Do not use a SAS.

Example:

> https\://contoso.blob.core.windows.net/data-protection/keys.xml

**Alternative shared-access signature (SAS) approach**: As an alternative to using a Managed Identity for access to the key blob in Azure Blob Storage, you can call the [PersistKeysToAzureBlobStorage](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azuredataprotectionbuilderextensions.persistkeystoazureblobstorage) overload that accepts a blob URI with a SAS token:

```
services.AddDataProtection()
    .SetApplicationName("{APPLICATION NAME}")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI WITH SAS}"));
```

`{APPLICATION NAME}`: [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the unique name of this app within the data protection system. The value should match across deployments of the app.

`{BLOB URI WITH SAS}`: The full URI where the key file should be stored with the SAS token as a query string parameter. The URI is generated by Azure Storage when you request a SAS for the uploaded key file. In the following example, the container name is `data-protection`, and the storage account name is `contoso`. The key file is named `keys.xml`. The shared access signature (SAS) query string is at the end of the URI (`{SHARED ACCESS SIGNATURE}` placeholder).

Example:

> https\://contoso.blob.core.windows.net/data-protection/keys.xml{SHARED ACCESS SIGNATURE}

If the web app is running as an Azure service, a connection string can be used to authenticate to Azure Storage using [BlobContainerClient](/en-us/dotnet/api/azure.storage.blobs.blobcontainerclient), as seen in the following example.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../../?view=aspnetcore-10.0#secure-authentication-flows).

The optional call to [CreateIfNotExistsAsync](/en-us/dotnet/api/azure.storage.blobs.blobcontainerclient.createifnotexistsasync) provisions the container automatically if it doesn't exist.

The connection string (`{CONNECTION STRING}` placeholder) to the storage account can be found in the Entra or Azure portal under the "Access Keys" section or by running the following Azure CLI command:

```
az storage account show-connection-string --name <account_name> --resource-group <resource_group>
```

In the `Program` file where services are registered:

```
string connectionString = "{CONNECTION STRING}";
string containerName = "{CONTAINER NAME}";
string blobName = "keys.xml";
var container = new BlobContainerClient(connectionString, containerName);
await container.CreateIfNotExistsAsync();
BlobClient blobClient = container.GetBlobClient(blobName);

builder.Services.AddDataProtection().PersistKeysToAzureBlobStorage(blobClient);
```

In `Startup.ConfigureServices`:

```
string connectionString = "{CONNECTION STRING}";
string containerName = "{CONTAINER NAME}";
string blobName = "keys.xml";
var container = new BlobContainerClient(connectionString, containerName);
await container.CreateIfNotExistsAsync();
BlobClient blobClient = container.GetBlobClient(blobName);

services.AddDataProtection().PersistKeysToAzureBlobStorage(blobClient);
```

## Redis

The [Microsoft.AspNetCore.DataProtection.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.StackExchangeRedis/) package allows storing data protection keys in a Redis cache. Keys can be shared across several instances of a web app. Apps can share authentication cookies or CSRF protection across multiple servers.

The [Microsoft.AspNetCore.DataProtection.Redis](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Redis/) package allows storing data protection keys in a Redis cache. Keys can be shared across several instances of a web app. Apps can share authentication cookies or CSRF protection across multiple servers.

To configure on Redis, call one of the [PersistKeysToStackExchangeRedis](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.stackexchangeredisdataprotectionbuilderextensions.persistkeystostackexchangeredis) overloads:

```
public void ConfigureServices(IServiceCollection services)
{
    var redis = ConnectionMultiplexer.Connect("<URI>");
    services.AddDataProtection()
        .PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys");
}
```

To configure on Redis, call one of the [PersistKeysToRedis](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.redisdataprotectionbuilderextensions.persistkeystoredis) overloads:

```
public void ConfigureServices(IServiceCollection services)
{
    var redis = ConnectionMultiplexer.Connect("<URI>");
    services.AddDataProtection()
        .PersistKeysToRedis(redis, "DataProtection-Keys");
}
```

Warning

When using Redis to persist data protection keys, be aware that Redis doesn't persist data by default when restarting. This can cause Data Protection to issue new keys, invalidating previously protected data.

You can configure Redis to enable data persistence to mitigate this: Redis documentation has information on [how to configure persistence](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/). If you're using [Azure Managed Redis](/en-us/azure/redis/how-to-persistence), ensure you have enabled data persistence. [Azure Cache for Redis](/en-us/azure/azure-cache-for-redis/cache-how-to-premium-persistence?tabs=premium) needs a premium or higher tier to enable data persistence.

For more information, see the following topics:

* [StackExchange.Redis ConnectionMultiplexer](https://github.com/StackExchange/StackExchange.Redis/blob/main/docs/Basics.md)
* [Azure Redis Cache](/en-us/azure/redis/dotnet-how-to-use-azure-redis-cache)
* [ASP.NET Core DataProtection samples](https://github.com/dotnet/AspNetCore/tree/2.2.0/src/DataProtection/samples)

## Registry

**Only applies to Windows deployments.**

Sometimes the app might not have write access to the file system. Consider a scenario where an app is running as a virtual service account (such as *w3wp.exe*'s app pool identity). In these cases, the administrator can provision a registry key that's accessible by the service account identity. Call the [PersistKeysToRegistry](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.persistkeystoregistry) extension method as shown below. Provide a [RegistryKey](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.repositories.registryxmlrepository.registrykey) pointing to the location where cryptographic keys should be stored:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToRegistry(Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Sample\keys", true));
}
```

Important

We recommend using [Windows DPAPI](key-encryption-at-rest?view=aspnetcore-10.0) to encrypt the keys at rest.

## Entity Framework Core

The [Microsoft.AspNetCore.DataProtection.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/) package provides a mechanism for storing data protection keys to a database using Entity Framework Core. The `Microsoft.AspNetCore.DataProtection.EntityFrameworkCore` NuGet package must be added to the project file, it's not part of the [Microsoft.AspNetCore.App metapackage](../../../fundamentals/metapackage-app?view=aspnetcore-10.0).

With this package, keys can be shared across multiple instances of a web app.

To configure the EF Core provider, call the [PersistKeysToDbContext](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.entityframeworkcoredataprotectionextensions.persistkeystodbcontext) method:

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));

    // Add a DbContext to store your Database Keys
    services.AddDbContext<MyKeysContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("MyKeysConnection")));

    // using Microsoft.AspNetCore.DataProtection;
    services.AddDataProtection()
        .PersistKeysToDbContext<MyKeysContext>();

    services.AddDefaultIdentity<IdentityUser>()
        .AddDefaultUI(UIFramework.Bootstrap4)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
```

The generic parameter, `TContext`, must inherit from [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) and implement [IDataProtectionKeyContext](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.entityframeworkcore.idataprotectionkeycontext):

```
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using WebApp1.Data;

namespace WebApp1
{
    class MyKeysContext : DbContext, IDataProtectionKeyContext
    {
        // A recommended constructor overload when using EF Core 
        // with dependency injection.
        public MyKeysContext(DbContextOptions<MyKeysContext> options) 
            : base(options) { }

        // This maps to the table that stores keys.
        public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
    }
}
```

Create the `DataProtectionKeys` table.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

Execute the following commands in the **Package Manager Console** (PMC) window:

```
Add-Migration AddDataProtectionKeys -Context MyKeysContext
Update-Database -Context MyKeysContext
```

Execute the following commands in a command shell:

```
dotnet ef migrations add AddDataProtectionKeys --context MyKeysContext
dotnet ef database update --context MyKeysContext
```

`MyKeysContext` is the `DbContext` defined in the preceding code sample. If you're using a `DbContext` with a different name, substitute your `DbContext` name for `MyKeysContext`.

The `DataProtectionKeys` class/entity adopts the structure shown in the following table.

| Property/Field | CLR Type | SQL Type                             |
| -------------- | -------- | ------------------------------------ |
| `Id`           | `int`    | `int`, PK, `IDENTITY(1,1)`, not null |
| `FriendlyName` | `string` | `nvarchar(MAX)`, null                |
| `Xml`          | `string` | `nvarchar(MAX)`, null                |

## Custom key repository

If the in-box mechanisms aren't appropriate, the developer can specify their own key persistence mechanism by providing a custom [IXmlRepository](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.repositories.ixmlrepository).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/script-tag-helper
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/script-tag-helper.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Script Tag Helper in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

The [Script Tag Helper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.scripttaghelper) generates a link to a primary or fall back script file. Typically the primary script file is on a [Content Delivery Network](/en-us/office365/enterprise/content-delivery-networks#what-exactly-is-a-cdn) (CDN).

A CDN:

* Provides several [performance advantages](/en-us/office365/enterprise/content-delivery-networks#how-do-cdns-make-services-work-faster) vs hosting the asset with the web app.
* Should not be relied on as the only source for the asset. CDNs are not always available, therefore a reliable fallback should be used. Typically the fallback is the site hosting the web app.

The Script Tag Helper allows you to specify a CDN for the script file and a fallback when the CDN is not available. The Script Tag Helper provides the performance advantage of a CDN with the robustness of local hosting.

The following Razor markup shows a `script` element with a fallback:

```
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.js"
        asp-fallback-src="~/lib/jquery/dist/jquery.js"
        asp-fallback-test="window.jQuery"
        crossorigin="anonymous"
        integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT">
</script>
```

Don't use the `<script>` element's [defer](https://developer.mozilla.org/docs/Web/HTML/Element/script) attribute to defer loading the CDN script. The Script Tag Helper renders JavaScript that immediately executes the [asp-fallback-test](#asp-fallback-test) expression. The expression fails if loading the CDN script is deferred.

## Commonly used Script Tag Helper attributes

See [Script Tag Helper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.scripttaghelper) for all the Script Tag Helper attributes, properties, and methods.

### src

Address of the external script to use.

### asp-append-version

When `asp-append-version` is specified with a `true` value along with a [`src`](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs#L116) attribute, a unique version is generated.

For a Tag Helper to generate a version for a static file outside `wwwroot`, see [Serve files from multiple locations](../../../../fundamentals/static-files?view=aspnetcore-10.0#serve-files-from-multiple-locations)

### asp-fallback-src

The URL of a Script tag to fallback to in the case the primary one fails.

### asp-fallback-src-exclude

A comma-separated list of globbed file patterns of JavaScript scripts to exclude from the fallback list, in the case the primary one fails. The glob patterns are assessed relative to the application's `webroot` setting. Must be used in conjunction with `asp-fallback-src-include`.

### asp-fallback-src-include

A comma-separated list of globbed file patterns of JavaScript scripts to fallback to in the case the primary one fails. The glob patterns are assessed relative to the application's `webroot` setting.

### asp-fallback-test

The script method defined in the primary script to use for the fallback test. For more information, see [FallbackTestExpression](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.scripttaghelper.fallbacktestexpression#microsoft-aspnetcore-mvc-taghelpers-scripttaghelper-fallbacktestexpression).

### asp-order

When a set of `ITagHelper` instances are executed, their `Init(TagHelperContext)` methods are first invoked in the specified order; then their `ProcessAsync(TagHelperContext, TagHelperOutput)` methods are invoked in the specified order. Lower values are executed first.

### asp-src-exclude

A comma-separated list of globbed file patterns of JavaScript scripts to exclude from loading. The glob patterns are assessed relative to the application's `webroot` setting. Must be used in conjunction with `asp-src-include`.

### asp-src-include

A comma-separated list of globbed file patterns of JavaScript scripts to load. The glob patterns are assessed relative to the application's `webroot` setting.

### asp-suppress-fallback-integrity

Boolean value that determines if an integrity hash will be compared with the asp-fallback-src value.

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Custom authorization policies with `IAuthorizationRequirementData`

Use the [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) interface to specify requirements associated with the authorization policy in attribute definitions.

This article uses a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint within the app and focuses on testing JWT-based authorization. For a demonstration of similar guidance in an MVC app with a controller, see the [Custom authorization policies with \`IAuthorizationRequirementData\` in ASP.NET Core MVC](../../mvc/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata-in-mvc?view=aspnetcore-10.0).

You can apply [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) attributes to SignalR hubs and hub methods, MVC controllers and actions, and Blazor's [`AuthorizeView`](../../blazor/security/?view=aspnetcore-10.0#authorizeview-component) and [`AuthorizeRouteView`](../../blazor/security/authentication-state?view=aspnetcore-10.0\&pivots=server#implement-a-custom-authenticationstateprovider) components, not just to endpoints. For apps that target releases earlier than .NET 11, these attributes are only enforced on Minimal API and routed endpoints.

[IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) isn't supported for authorization attributes applied to:

* SignalR hubs and hub methods
* MVC controllers and actions
* Blazor's [`AuthorizeView`](../../blazor/security/?view=aspnetcore-10.0#authorizeview-component) and [`AuthorizeRouteView`](../../blazor/security/authentication-state?view=aspnetcore-10.0\&pivots=server#implement-a-custom-authenticationstateprovider) components

Support for the preceding is available for apps that target .NET 11 or later.

## Sample app

The Blazor Web App sample for this article is the [`AuthRequirementsDataBWA` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/AuthRequirementsDataBWA) ([how to download](../../overview?view=aspnetcore-10.0#how-to-download-a-sample)). The sample app implements a minimum age handler for users accessing a Minimal API endpoint at `/api/greetings/hello`, requiring a user to present a birth date claim indicating that they're at least 21 years old.

## Minimum age authorize attribute

The `MinimumAgeAuthorizeAttribute` implementation of [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) sets an authorization age:

```
using Microsoft.AspNetCore.Authorization;

namespace AuthRequirementsDataBWA.Authorization;

class MinimumAgeAuthorizeAttribute(int age) : AuthorizeAttribute, 
    IAuthorizationRequirement, IAuthorizationRequirementData
{
    public int Age { get; set; } = age;

    public IEnumerable<IAuthorizationRequirement> GetRequirements()
    {
        yield return this;
    }
}
```

## Minimum age authorization handler

The `MinimumAgeAuthorizationHandler` class handles the single [IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement) provided by `MinimumAgeAuthorizeAttribute`, as specified by the generic parameter `MinimumAgeAuthorizeAttribute`.

The `HandleRequirementAsync` method:

* Gets the user's birth date claim.
* Obtains the user's age from the claim.
* Adjusts age if the user hasn't had a birthday this year.
* Marks the authorization requirement succeeded if the user meets the age requirement.
* Implements logging for demonstration purposes.

```
using System.Globalization;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;

namespace AuthRequirementsDataBWA.Authorization;

class MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler> logger) 
    : AuthorizationHandler<MinimumAgeAuthorizeAttribute>
{
    // Check whether a given minimum age requirement is satisfied.
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        MinimumAgeAuthorizeAttribute requirement)
    {
        logger.LogInformation(
            "Evaluating authorization requirement for age >= {age}", 
            requirement.Age);

        // Get the user's birth date claim.
        var dateOfBirthClaim = 
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);

        if (dateOfBirthClaim != null)
        {
            // If the user has a date of birth claim, obtain their age.
            var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value, 
                CultureInfo.InvariantCulture);
            var age = DateTime.Now.Year - dateOfBirth.Year;

            // Adjust age if the user hasn't had a birthday yet this year.
            if (dateOfBirth > DateTime.Now.AddYears(-age))
            {
                age--;
            }

            // If the user meets the age requirement, mark the authorization
            // requirement succeeded.
            if (age >= requirement.Age)
            {
                logger.LogInformation(
                    "Minimum age authorization requirement {age} satisfied", 
                    requirement.Age);
                context.Succeed(requirement);
            }
            else
            {
                logger.LogInformation(
                    "Current user's DateOfBirth claim ({dateOfBirth}) doesn't " +
                    "satisfy the minimum age authorization requirement {age}",
                    dateOfBirthClaim.Value,
                    requirement.Age);
            }
        }
        else
        {
            logger.LogInformation("No DateOfBirth claim present");
        }

        return Task.CompletedTask;
    }
}
```

The `MinimumAgeAuthorizationHandler` is registered as a singleton [IAuthorizationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandler) service in the app's `Program` file:

```
builder.Services.AddSingleton<IAuthorizationHandler,
    MinimumAgeAuthorizationHandler>();
```

A [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint is configured in the app's `Program` file with the [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) extension method and the `MinimumAgeAuthorizeAttribute`:

```
app.MapGet("/api/greetings/hello", (HttpContext context) =>
{
    return $"Hello {context.User.Identity?.Name}!";
}).RequireAuthorization(new MinimumAgeAuthorizeAttribute(21));
```

The endpoint displays the user's name when they satisfy the minimum age policy, using an age of 21 years old supplied to a `MinimumAgeAuthorizeAttribute` instance.

If the user's birth date claim indicates that they're at least 21 years old, the endpoint displays the greeting string, issuing a 200 (OK) status code. If the user is missing the birth date claim or the claim indicates that they aren't at least 21 years old, the greeting isn't displayed and a 403 (Forbidden) status code is issued.

Note

For MVC controller guidance that demonstrates the same behavior, see [Custom authorization policies with \`IAuthorizationRequirementData\` in ASP.NET Core MVC](../../mvc/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata-in-mvc?view=aspnetcore-10.0).

JWT bearer authentication services are added in the app's `Program` file:

```
builder.Services.AddAuthentication().AddJwtBearer();
```

The app settings file (`appsettings.Development.json`) configures the audience and issuer for JWT bearer authentication:

```
"Authentication": {
  "Schemes": {
    "Bearer": {
      "ValidAudiences": [
        "https://localhost:5001",
        "http://localhost:5000"
      ],
      "ValidIssuer": "dotnet-user-jwts"
    }
  }
}
```

In the preceding example, the localhost audience matches the localhost address specified by `applicationUrl` in the app's launch profile (`Properties/launchSettings.json`).

## Demonstration

Test the sample with [`dotnet user-jwts`](../authentication/jwt-authn?view=aspnetcore-10.0) and curl.

From the project's folder in a command shell, execute the following command to create a JWT bearer token with a birth date claim that makes the user over 21 years old:

```
dotnet user-jwts create --claim http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth=1989-01-01
```

The output produces a token after "`Token:`" in the command shell:

```
New JWT saved with ID '{JWT ID}'.
Name: {USER}
Custom Claims: [http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth=1989-01-01]

Token: {TOKEN}
```

Set the value of the token (where the `{TOKEN}` placeholder appears in the preceding output) aside for use later.

You can decode the token in an online JWT decoder, such as [`jwt.ms`](https://jwt.ms/) to see its contents, revealing that it contains a `birthdate` claim with the user's birth date:

```
{
  "alg": "HS256",
  "typ": "JWT"
}.{
  "unique_name": "{USER}",
  "sub": "{USER}",
  "jti": "6cd613ed",
  "birthdate": "1989-01-01",
  "aud": [
    "https://localhost:5001",
    "http://localhost:5000"
  ],
  "nbf": 1773663513,
  "exp": 1781612313,
  "iat": 1773663515,
  "iss": "dotnet-user-jwts"
}.[Signature]
```

Execute the command again with a `dateofbirth` value that makes the user under the age of 21:

```
dotnet user-jwts create --claim http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth=2020-01-01
```

Set the value of second token aside.

Start the app in Visual Studio or with the `dotnet watch` command in a command shell:

```
dotnet watch
```

In a command shell, use the .NET CLI to execute the following `curl.exe` command to request the `api/greetings/hello` endpoint. Replace the `{TOKEN}` placeholder with the first JWT bearer token that you saved earlier:

```
curl.exe -i -H "Authorization: Bearer {TOKEN}" https://localhost:5001/api/greetings/hello
```

The output indicates success because the user's birth date claim indicates that they're at least 21 years old:

```
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Thu, 15 May 2025 22:58:10 GMT
Server: Kestrel
Transfer-Encoding: chunked

Hello {USER}!
```

Logging indicates that the age requirement was met:

> MinimumAgeAuthorizationHandler: Information: Evaluating authorization requirement for age >= 21\
> MinimumAgeAuthorizationHandler: Information: Minimum age authorization requirement 21 satisfied

Re-execute the `curl.exe` command with the second token, which indicates the user is under 21 years old. The output indicates that the requirement isn't met. Access to the endpoint is forbidden (status code 403):

```
HTTP/1.1 403 Forbidden
Content-Length: 0
Date: Thu, 15 May 2025 22:58:36 GMT
Server: Kestrel
```

Logging indicates that the age requirement wasn't met:

> MinimumAgeAuthorizationHandler: Information: Evaluating authorization requirement for age >= 21\
> MinimumAgeAuthorizationHandler: Information: Current user's DateOfBirth claim (2020-01-01) doesn't satisfy the minimum age authorization requirement 21

## Related content

[Custom authorization policies with \`IAuthorizationRequirementData\` in ASP.NET Core MVC](../../mvc/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata-in-mvc?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/jwt-authn
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/jwt-authn.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Manage JSON Web Tokens in development with dotnet user-jwts

By [Rick Anderson](https://twitter.com/RickAndMSFT)

The `dotnet user-jwts` command line tool can create and manage app specific local [JSON Web Tokens](https://www.jwt.io/introduction#what-is-json-web-token) (JWTs).

This article provides syntax details for the command and examples.

## Synopsis

```
dotnet user-jwts [<PROJECT>] [command]
dotnet user-jwts [command] -h|--help
```

## Description

Creates and manages project specific local JSON Web Tokens.

## Arguments

`PROJECT | SOLUTION`

The MSBuild project to apply a command on. If a project isn't specified, MSBuild searches the current working directory for a file that has a file extension that ends in *proj*. It then uses that file to obtain the project information for the command.

## Commands

| Command  | Description                                          |
| -------- | ---------------------------------------------------- |
| `clear`  | Delete all issued JWTs for a project.                |
| `create` | Issue a new JSON Web Token.                          |
| `remove` | Delete a given JWT.                                  |
| `key`    | Display or reset the signing key used to issue JWTs. |
| `list`   | List the JWTs issued for the project.                |
| `print`  | Display the details of a given JWT.                  |

### Options for the create command

Usage: `dotnet user-jwts create [options]`

| Option             | Description                                                                                                                                                                                                                                                |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-p \\| --project` | The path of the project to operate on. Defaults to the project in the current directory.                                                                                                                                                                   |
| `--scheme`         | The scheme name to use for the generated token. Defaults to `Bearer`.                                                                                                                                                                                      |
| `-n \\| --name`    | The name of the user to create the JWT for. Defaults to the current environment user.                                                                                                                                                                      |
| `--audience`       | The audiences to create the JWT for. Defaults to the URLs configured in the project's *launchSettings.json* file.                                                                                                                                          |
| `--issuer`         | The issuer of the JWT. Defaults to `dotnet-user-jwts`.                                                                                                                                                                                                     |
| `--scope`          | A scope claim to add to the JWT. Specify once for each scope.                                                                                                                                                                                              |
| `--role`           | A role claim to add to the JWT. Specify once for each role.                                                                                                                                                                                                |
| `--claim`          | Claims to add to the JWT. Specify once for each claim in the format `name=value`.                                                                                                                                                                          |
| `--not-before`     | The UTC date and time at which the JWT becomes valid, in the format `yyyy-MM-dd [[HH:mm[[:ss]]]]`. Defaults to the date and time the JWT is created.                                                                                                       |
| `--expires-on`     | The UTC date and time at which the JWT expires, in the format `yyyy-MM-dd [[[ [HH:mm]]:ss]]`. Defaults to six months after the `--not-before` date. Don't use this option with the `--valid-for` option.                                                   |
| `--valid-for`      | The amount of time the JWT remains valid. When the time is reached, the JWT expires. Specify a number followed by the duration type (`d` days, `h` hours, `m` minutes, `s` seconds), such as `365d`. Don't use this option with the `--expires-on` option. |
| `-o \\| --output`  | The format to use for displaying output from the command: `default`, `token`, or `json`.                                                                                                                                                                   |
| `-h \\| --help`    | Show help information for the command.                                                                                                                                                                                                                     |

## Use with file-based apps

`dotnet user-jwts` works with file-based apps through the `--file` option. Point `--file` at the app's C# file (`.cs`).

In the following example, the app is a file named `app.cs` without a project file, and the command is executed in a command shell from the app's root directory:

```
dotnet user-jwts create --file app.cs
```

The `--file` option applies to every `user-jwts` command (`create`, `clear`, `key`, `list`, `print`, and `remove`), the same way the `--project` option does. When neither `--file` nor `--project` is specified, the tool searches the current directory for an MSBuild project.

Rules:

* The path passed to `--file` must be a C# file with the `.cs` file extension.
* The `--file` and `--project` options can't be used together.

## Examples

Run the following commands to create an empty web project and add the [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer) NuGet package:

```
dotnet new web -o MyJWT
cd MyJWT
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
```

Replace the contents of the *Program.cs* file with the following code:

```
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/", () => "Hello, World!");
app.MapGet("/secret", (ClaimsPrincipal user) => $"Hello {user.Identity?.Name}. My secret")
    .RequireAuthorization();

app.Run();
```

In the preceding code, a GET request to the `/secret` endpoint returns a `401 Unauthorized` error. A production app might get the JWT from a [Security token service](/en-us/entra/identity-platform/security-tokens), perhaps in response to signing in with credentials. When you use the API during local development, the `dotnet user-jwts` command line tool can be used to create and manage app-specific local JWTs.

The `user-jwts` tool is similar in concept to the [user-secrets](../app-secrets?view=aspnetcore-10.0) tool. It can be used to manage values for the app that are valid only for the developer on the local machine. In fact, the `user-jwts` tool utilizes the `user-secrets` infrastructure to manage the key that the JWTs are signed with. This approach ensures the key is stored safely in the user profile.

The `user-jwts` tool hides implementation details, such as where and how the values are stored. The tool can be used without knowing the implementation details.

The values are stored in a JSON file in the local machine's user profile folder:

* **Windows**: *%APPDATA%\Microsoft\UserSecrets\<secrets\_GUID>\user-jwts.json*

* **Linux/macOS**: *\~/.microsoft/usersecrets/\<secrets\_GUID>/user-jwts.json*

### Create a JWT

The following command creates a local JWT:

```
dotnet user-jwts create
```

The preceding command creates a JWT and updates the project `appsettings.Development.json` file with JSON similar to the following example:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Authentication": {
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "http://localhost:8401",
          "https://localhost:44308",
          "http://localhost:5182",
          "https://localhost:7076"
        ],
        "ValidIssuer": "dotnet-user-jwts"
      }
    }
  }
}
```

Copy the JWT and the `ID` created in the preceding command. Use a tool like Curl to test the `/secret` endpoint, where `{token}` is the previously generated JWT:

```
curl -i -H "Authorization: Bearer {token}" https://localhost:{port}/secret
```

### Display JWT security information

The following command displays the JWT security information, including expiration, scopes, roles, token header and payload, and the compact token:

```
dotnet user-jwts print {ID} --show-all
```

### Create a token for a specific user and scope

The following command creates a JWT for a user named `MyTestUser`. For the supported `create` options, see the [Options for the create command](#options-for-the-create-command) section.

```
dotnet user-jwts create --name MyTestUser --scope "myapi:secrets"
```

The preceding command has output similar to the following example:

```
New JWT saved with ID '43e0b748'.
Name: MyTestUser
Scopes: myapi:secrets

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.{Remaining token deleted}
```

The preceding token can be used to test the `/secret2` endpoint in the following code:

```
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();

var app = builder.Build();

app.MapGet("/", () => "Hello, World!");
app.MapGet("/secret", (ClaimsPrincipal user) => $"Hello {user.Identity?.Name}. My secret")
    .RequireAuthorization();
app.MapGet("/secret2", () => "This is a different secret!")
    .RequireAuthorization(p => p.RequireClaim("scope", "myapi:secrets"));

app.Run();
```

## Related content

* [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer)
* [JSON Web Tokens](https://www.jwt.io/introduction#what-is-json-web-token)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/application-model
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/application-model.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Work with the application model in ASP.NET Core

By [Steve Smith](https://ardalis.com/)

ASP.NET Core MVC defines an *application model* representing the components of an MVC app. Read and manipulate this model to modify how MVC elements behave. By default, MVC follows certain conventions to determine which classes are considered controllers, which methods on those classes are actions, and how parameters and routing behave. Customize this behavior to suit an app's needs by creating custom conventions and applying them globally or as attributes.

## Models and Providers (`IApplicationModelProvider`)

The ASP.NET Core MVC application model includes both abstract interfaces and concrete implementation classes that describe an MVC application. This model is the result of MVC discovering the app's controllers, actions, action parameters, routes, and filters according to default conventions. By working with the application model, modify an app to follow different conventions from the default MVC behavior. The parameters, names, routes, and filters are all used as configuration data for actions and controllers.

The ASP.NET Core MVC Application Model has the following structure:

* ApplicationModel
  * Controllers (ControllerModel)
    * Actions (ActionModel)
      * Parameters (ParameterModel)

Each level of the model has access to a common `Properties` collection, and lower levels can access and overwrite property values set by higher levels in the hierarchy. The properties are persisted to the [ActionDescriptor.Properties](/en-us/dotnet/api/microsoft.aspnetcore.mvc.abstractions.actiondescriptor.properties#microsoft-aspnetcore-mvc-abstractions-actiondescriptor-properties) when the actions are created. Then when a request is being handled, any properties a convention added or modified can be accessed through [ActionContext.ActionDescriptor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actioncontext.actiondescriptor#microsoft-aspnetcore-mvc-actioncontext-actiondescriptor). Using properties is a great way to configure filters, model binders, and other app model aspects on a per-action basis.

Note

The [ActionDescriptor.Properties](/en-us/dotnet/api/microsoft.aspnetcore.mvc.abstractions.actiondescriptor.properties#microsoft-aspnetcore-mvc-abstractions-actiondescriptor-properties) collection isn't thread safe (for writes) after app startup. Conventions are the best way to safely add data to this collection.

ASP.NET Core MVC loads the application model using a provider pattern, defined by the [IApplicationModelProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iapplicationmodelprovider) interface. This section covers some of the internal implementation details of how this provider functions. Use of the provider pattern is an advanced subject, primarily for framework use. Most apps should use conventions, not the provider pattern.

Implementations of the [IApplicationModelProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iapplicationmodelprovider) interface "wrap" one another, where each implementation calls [OnProvidersExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.abstractions.iactioninvokerprovider.onprovidersexecuting) in ascending order based on its [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iapplicationmodelprovider.order#microsoft-aspnetcore-mvc-applicationmodels-iapplicationmodelprovider-order) property. The [OnProvidersExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.abstractions.iactioninvokerprovider.onprovidersexecuted) method is then called in reverse order. The framework defines several providers:

First (`Order=-1000`):

* `DefaultApplicationModelProvider`

Then (`Order=-990`):

* `AuthorizationApplicationModelProvider`
* `CorsApplicationModelProvider`

Note

The order in which two providers with the same value for `Order` are called is undefined and shouldn't be relied upon.

Note

[IApplicationModelProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iapplicationmodelprovider) is an advanced concept for framework authors to extend. In general, apps should use conventions, and frameworks should use providers. The key distinction is that providers always run before conventions.

The `DefaultApplicationModelProvider` establishes many of the default behaviors used by ASP.NET Core MVC. Its responsibilities include:

* Adding global filters to the context
* Adding controllers to the context
* Adding public controller methods as actions
* Adding action method parameters to the context
* Applying route and other attributes

Some built-in behaviors are implemented by the `DefaultApplicationModelProvider`. This provider is responsible for constructing the [ControllerModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.controllermodel), which in turn references [ActionModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.actionmodel), [PropertyModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.propertymodel), and [ParameterModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.parametermodel) instances. The `DefaultApplicationModelProvider` class is an internal framework implementation detail that may change in the future.

The `AuthorizationApplicationModelProvider` is responsible for applying the behavior associated with the [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) and [AllowAnonymousFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.allowanonymousfilter) attributes. For more information, see [Simple authorization in ASP.NET Core](../../security/authorization/simple?view=aspnetcore-10.0).

The `CorsApplicationModelProvider` implements behavior associated with [IEnableCorsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.ienablecorsattribute) and [IDisableCorsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.idisablecorsattribute). For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../../security/cors?view=aspnetcore-10.0).

Information on the framework's internal providers described in this section aren't available via the [.NET API browser](/en-us/dotnet/api/). However, the providers may be inspected in the [ASP.NET Core reference source (dotnet/aspnetcore GitHub repository)](https://github.com/dotnet/aspnetcore). Use GitHub search to find the providers by name and select the version of the source with the **Switch branches/tags** dropdown list.

## Conventions

The application model defines convention abstractions that provide a simpler way to customize the behavior of the models than overriding the entire model or provider. These abstractions are the recommended way to modify an app's behavior. Conventions provide a way to write code that dynamically applies customizations. While [filters](filters?view=aspnetcore-10.0) provide a means of modifying the framework's behavior, customizations permit control over how the whole app works together.

The following conventions are available:

* [IApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iapplicationmodelconvention)
* [IControllerModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.icontrollermodelconvention)
* [IActionModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iactionmodelconvention)
* [IParameterModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iparametermodelconvention)

Conventions are applied by adding them to MVC options or by implementing attributes and applying them to controllers, actions, or action parameters (similar to [filters](filters?view=aspnetcore-10.0)).Unlike filters, conventions are only executed when the app is starting, not as part of each request.

Note

For information on Razor Pages route and application model provider conventions, see [Razor Pages route and app conventions in ASP.NET Core](../../razor-pages/razor-pages-conventions?view=aspnetcore-10.0).

## Modify the `ApplicationModel`

The following convention is used to add a property to the application model:

```
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class ApplicationDescription : IApplicationModelConvention
    {
        private readonly string _description;

        public ApplicationDescription(string description)
        {
            _description = description;
        }

        public void Apply(ApplicationModel application)
        {
            application.Properties["description"] = _description;
        }
    }
}
```

Application model conventions are applied as options when MVC is added in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Conventions.Add(new ApplicationDescription("My Application Description"));
        options.Conventions.Add(new NamespaceRoutingConvention());
    });
}
```

Properties are accessible from the [ActionDescriptor.Properties](/en-us/dotnet/api/microsoft.aspnetcore.mvc.abstractions.actiondescriptor.properties#microsoft-aspnetcore-mvc-abstractions-actiondescriptor-properties) collection within controller actions:

```
public class AppModelController : Controller
{
    public string Description()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }
}
```

## Modify the `ControllerModel` description

The controller model can also include custom properties. Custom properties override existing properties with the same name specified in the application model. The following convention attribute adds a description at the controller level:

```
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention
    {
        private readonly string _description;

        public ControllerDescriptionAttribute(string description)
        {
            _description = description;
        }

        public void Apply(ControllerModel controllerModel)
        {
            controllerModel.Properties["description"] = _description;
        }
    }
}
```

This convention is applied as an attribute on a controller:

```
[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
    public string Index()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }
```

## Modify the `ActionModel` description

A separate attribute convention can be applied to individual actions, overriding behavior already applied at the application or controller level:

```
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class ActionDescriptionAttribute : Attribute, IActionModelConvention
    {
        private readonly string _description;

        public ActionDescriptionAttribute(string description)
        {
            _description = description;
        }

        public void Apply(ActionModel actionModel)
        {
            actionModel.Properties["description"] = _description;
        }
    }
}
```

Applying this to an action within the controller demonstrates how it overrides the controller-level convention:

```
[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
    public string Index()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }

    [ActionDescription("Action Description")]
    public string UseActionDescriptionAttribute()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }
}
```

## Modify the `ParameterModel`

The following convention can be applied to action parameters to modify their [BindingInfo](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindinginfo). The following convention requires that the parameter be a route parameter. Other potential binding sources, such as query string values, are ignored:

```
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace AppModelSample.Conventions
{
    public class MustBeInRouteParameterModelConvention : Attribute, IParameterModelConvention
    {
        public void Apply(ParameterModel model)
        {
            if (model.BindingInfo == null)
            {
                model.BindingInfo = new BindingInfo();
            }
            model.BindingInfo.BindingSource = BindingSource.Path;
        }
    }
}
```

The attribute may be applied to any action parameter:

```
public class ParameterModelController : Controller
{
    // Will bind:  /ParameterModel/GetById/123
    // WON'T bind: /ParameterModel/GetById?id=123
    public string GetById([MustBeInRouteParameterModelConvention]int id)
    {
        return $"Bound to id: {id}";
    }
}
```

To apply the convention to all action parameters, add the `MustBeInRouteParameterModelConvention` to [MvcOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions) in `Startup.ConfigureServices`:

```
options.Conventions.Add(new MustBeInRouteParameterModelConvention());
```

## Modify the `ActionModel` name

The following convention modifies the [ActionModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.actionmodel) to update the *name* of the action to which it's applied. The new name is provided as a parameter to the attribute. This new name is used by routing, so it affects the route used to reach this action method:

```
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class CustomActionNameAttribute : Attribute, IActionModelConvention
    {
        private readonly string _actionName;

        public CustomActionNameAttribute(string actionName)
        {
            _actionName = actionName;
        }

        public void Apply(ActionModel actionModel)
        {
            // this name will be used by routing
            actionModel.ActionName = _actionName;
        }
    }
}
```

This attribute is applied to an action method in the `HomeController`:

```
// Route: /Home/MyCoolAction
[CustomActionName("MyCoolAction")]
public string SomeName()
{
    return ControllerContext.ActionDescriptor.ActionName;
}
```

Even though the method name is `SomeName`, the attribute overrides the MVC convention of using the method name and replaces the action name with `MyCoolAction`. Thus, the route used to reach this action is `/Home/MyCoolAction`.

Note

This example in this section is essentially the same as using the built-in [ActionNameAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionnameattribute).

## Custom routing convention

Use an [IApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.iapplicationmodelconvention) to customize how routing works. For example, the following convention incorporates controllers' namespaces into their routes, replacing `.` in the namespace with `/` in the route:

```
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;

namespace AppModelSample.Conventions
{
    public class NamespaceRoutingConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            foreach (var controller in application.Controllers)
            {
                var hasAttributeRouteModels = controller.Selectors
                    .Any(selector => selector.AttributeRouteModel != null);

                if (!hasAttributeRouteModels
                    && controller.ControllerName.Contains("Namespace")) // affect one controller in this sample
                {
                    // Replace the . in the namespace with a / to create the attribute route
                    // Ex: MySite.Admin namespace will correspond to MySite/Admin attribute route
                    // Then attach [controller], [action] and optional {id?} token.
                    // [Controller] and [action] is replaced with the controller and action
                    // name to generate the final template
                    controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel()
                    {
                        Template = controller.ControllerType.Namespace.Replace('.', '/') + "/[controller]/[action]/{id?}"
                    };
                }
            }

            // You can continue to put attribute route templates for the controller actions depending on the way you want them to behave
        }
    }
}
```

The convention is added as an option in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Conventions.Add(new ApplicationDescription("My Application Description"));
        options.Conventions.Add(new NamespaceRoutingConvention());
    });
}
```

Tip

Add conventions to [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) via [MvcOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions) using the following approach. The `{CONVENTION}` placeholder is the convention to add:

```
services.Configure<MvcOptions>(c => c.Conventions.Add({CONVENTION}));
```

The following example applies a convention to routes that aren't using attribute routing where the controller has `Namespace` in its name:

```
using Microsoft.AspNetCore.Mvc;

namespace AppModelSample.Controllers
{
    public class NamespaceRoutingController : Controller
    {
        // using NamespaceRoutingConvention
        // route: /AppModelSample/Controllers/NamespaceRouting/Index
        public string Index()
        {
            return "This demonstrates namespace routing.";
        }
    }
}
```

## Application model usage in `WebApiCompatShim`

ASP.NET Core MVC uses a different set of conventions from ASP.NET Web API 2. Using custom conventions, you can modify an ASP.NET Core MVC app's behavior to be consistent with that of a web API app. Microsoft ships the [`WebApiCompatShim` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.WebApiCompatShim) specifically for this purpose.

Note

For more information on migration from ASP.NET Web API, see [Migrate from ASP.NET Web API to ASP.NET Core](../../migration/fx-to-core/areas/webapi?view=aspnetcore-10.0).

To use the Web API Compatibility Shim:

* Add the `Microsoft.AspNetCore.Mvc.WebApiCompatShim` package to the project.
* Add the conventions to MVC by calling [AddWebApiConventions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webapicompatshimmvcbuilderextensions.addwebapiconventions) in `Startup.ConfigureServices`:

```
services.AddMvc().AddWebApiConventions();
```

The conventions provided by the shim are only applied to parts of the app that have had certain attributes applied to them. The following four attributes are used to control which controllers should have their conventions modified by the shim's conventions:

* [UseWebApiActionConventionsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.usewebapiactionconventionsattribute)
* [UseWebApiOverloadingAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.usewebapioverloadingattribute)
* [UseWebApiParameterConventionsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.usewebapiparameterconventionsattribute)
* [UseWebApiRoutesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.usewebapiroutesattribute)

### Action conventions

[UseWebApiActionConventionsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.usewebapiactionconventionsattribute) is used to map the HTTP method to actions based on their name (for instance, `Get` would map to `HttpGet`). It only applies to actions that don't use attribute routing.

### Overloading

[UseWebApiOverloadingAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.usewebapioverloadingattribute) is used to apply the [WebApiOverloadingApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.webapioverloadingapplicationmodelconvention) convention. This convention adds an [OverloadActionConstraint](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.overloadactionconstraint) to the action selection process, which limits candidate actions to those for which the request satisfies all non-optional parameters.

### Parameter conventions

[UseWebApiParameterConventionsAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.usewebapiparameterconventionsattribute) is used to apply the [WebApiParameterConventionsApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.webapiparameterconventionsapplicationmodelconvention) action convention. This convention specifies that simple types used as action parameters are bound from the URI by default, while complex types are bound from the request body.

### Routes

[UseWebApiRoutesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim.usewebapiroutesattribute) controls whether the `WebApiApplicationModelConvention` controller convention is applied. When enabled, this convention is used to add support for [areas](areas?view=aspnetcore-10.0) to the route and indicates the controller is in the `api` area.

In addition to a set of conventions, the compatibility package includes a [System.Web.Http.ApiController](/en-us/dotnet/api/system.web.http.apicontroller) base class that replaces the one provided by web API. This allows your web API controllers written for web API and inheriting from its `ApiController` to work while running on ASP.NET Core MVC. All of the [`UseWebApi*`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.webapicompatshim) attributes listed earlier are applied to the base controller class. The `ApiController` exposes properties, methods, and result types that are compatible with those found in web API.

## Use `ApiExplorer` to document an app

The application model exposes an [ApiExplorerModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.apiexplorermodel) property at each level that can be used to traverse the app's structure. This can be used to [generate help pages for web APIs using tools like Swagger](../../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0). The `ApiExplorer` property exposes an [IsVisible](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.apiexplorermodel.isvisible#microsoft-aspnetcore-mvc-applicationmodels-apiexplorermodel-isvisible) property that can be set to specify which parts of the app's model should be exposed. Configure this setting using a convention:

```
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class EnableApiExplorerApplicationConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            application.ApiExplorer.IsVisible = true;
        }
    }
}
```

Using this approach (and additional conventions if required), API visibility is enabled or disabled at any level within an app.

### Custom API description providers with `IApiDescriptionProvider`

Starting with .NET 9, ASP.NET Core includes built-in OpenAPI document generation in the [`Microsoft.AspNetCore.OpenApi`](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi) package. To programmatically inspect or modify the generated OpenAPI output, use document, operation, and schema transformers rather than implementing [IApiDescriptionProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.iapidescriptionprovider) directly. For more information, see [Generate OpenAPI documents](../../fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0).

Note

[IApiDescriptionProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.iapidescriptionprovider) is an advanced extensibility point intended for framework and library authors. Most apps don't need to implement it. Starting with .NET 9, use the built-in OpenAPI document, operation, and schema transformers to customize generated API documentation. For more information, see [Generate OpenAPI documents](../../fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0).

ASP.NET Core uses [IApiDescriptionProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.iapidescriptionprovider) implementations to discover endpoints and generate [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription) metadata. Tools such as Swashbuckle and NSwag inspect these `ApiDescription` instances when producing API documentation.

Implement [IApiDescriptionProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.iapidescriptionprovider) to programmatically inspect or modify `ApiDescription` instances produced by the framework:

* [OnProvidersExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.iapidescriptionprovider.onprovidersexecuting): Executes in ascending order of the [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.iapidescriptionprovider.order#microsoft-aspnetcore-mvc-apiexplorer-iapidescriptionprovider-order) property to construct `ApiDescription` metadata for discovered endpoints.
* [OnProvidersExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.iapidescriptionprovider.onprovidersexecuted): Executes in reverse order after all providers have executed, allowing customization or enrichment of generated `ApiDescription` instances.

The following example demonstrates a custom `IApiDescriptionProvider` that adds custom metadata properties to discovered API descriptions:

```
using Microsoft.AspNetCore.Mvc.ApiExplorer;

public class CustomApiDescriptionProvider : IApiDescriptionProvider
{
    // Execute after the framework's default ApiDescriptionProvider (Order = -1000)
    public int Order => 0;

    public void OnProvidersExecuting(ApiDescriptionProviderContext context)
    {
        // No action required during initial execution phase
    }

    public void OnProvidersExecuted(ApiDescriptionProviderContext context)
    {
        foreach (var apiDescription in context.Results)
        {
            // Enrich or modify ApiDescription metadata
            apiDescription.Properties["CustomMetadata"] = "CustomValue";
        }
    }
}
```

Register the custom provider with dependency injection using [TryAddEnumerable](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.extensions.servicecollectiondescriptorextensions.tryaddenumerable) in `Program.cs`:

```
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Transient<IApiDescriptionProvider, CustomApiDescriptionProvider>());

var app = builder.Build();
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-06

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/layout
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/layout.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Layout in ASP.NET Core

By [Steve Smith](https://ardalis.com/) and [Dave Brock](https://twitter.com/daveabrock)

Pages and views frequently share visual and programmatic elements. This article demonstrates how to:

* Use common layouts.
* Share directives.
* Run common code before rendering pages or views.

This document discusses layouts for the two different approaches to ASP.NET Core MVC: Razor Pages and controllers with views. For this article, the differences are minimal:

* Razor Pages are in the *Pages* folder.
* Controllers with views uses a *Views* folder for views.

## What is a Layout

Most web apps have a common layout that provides the user with a consistent experience as they navigate from page to page. The layout typically includes common user interface elements such as the app header, navigation or menu elements, and footer.

Common HTML structures such as scripts and stylesheets are also frequently used by many pages within an app. All of these shared elements might be defined in a *layout* file, which any view used within the app can then reference. Layouts reduce duplicate code in views.

By convention, the default layout for an ASP.NET Core app is named `_Layout.cshtml`. The layout files for new ASP.NET Core projects created with the templates are:

* Razor Pages: `Pages/Shared/_Layout.cshtml`

* Controller with views: `Views/Shared/_Layout.cshtml`

The layout defines a top level template for views in the app. Apps don't require a layout. Apps can define more than one layout, with different views specifying different layouts.

The following code shows the layout file for a template created project with a controller and views:

```
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebApplication1</title>

    <environment include="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
        <link rel="stylesheet" href="~/css/site.css" />
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
        <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
    </environment>
</head>
<body>
    <nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a asp-page="/Index" class="navbar-brand">WebApplication1</a>
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li><a asp-page="/Index">Home</a></li>
                    <li><a asp-page="/About">About</a></li>
                    <li><a asp-page="/Contact">Contact</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <partial name="_CookieConsentPartial" />

    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; 2018 - WebApplication1</p>
        </footer>
    </div>

    <environment include="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
        <script src="~/js/site.js" asp-append-version="true"></script>
    </environment>
    <environment exclude="Development">
        <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
                asp-fallback-test="window.jQuery"
                crossorigin="anonymous"
                integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT">
        </script>
        <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
                crossorigin="anonymous"
                integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
        </script>
        <script src="~/js/site.min.js" asp-append-version="true"></script>
    </environment>

    @RenderSection("Scripts", required: false)
</body>
</html>
```

## Specifying a Layout

Razor views have a `Layout` property. Individual views specify a layout by setting this property:

```
@{
    Layout = "_Layout";
}
```

The layout specified can use a full path (for example, `/Pages/Shared/_Layout.cshtml` or `/Views/Shared/_Layout.cshtml`) or a partial name (example: `_Layout`). When a partial name is provided, the Razor view engine searches for the layout file using its standard discovery process. The folder where the handler method (or controller) exists is searched first, followed by the *Shared* folder. This discovery process is identical to the process used to discover [partial views](partial?view=aspnetcore-10.0#partial-view-discovery).

By default, every layout must call `RenderBody`. Wherever the call to `RenderBody` is placed, the contents of the view will be rendered.

[]()

### Sections

A layout can optionally reference one or more *sections*, by calling `RenderSection`. Sections provide a way to organize where certain page elements should be placed. Each call to `RenderSection` can specify whether that section is required or optional:

```
<script type="text/javascript" src="~/scripts/global.js"></script>

@RenderSection("Scripts", required: false)
```

If a required section isn't found, an exception is thrown. Individual views specify the content to be rendered within a section using the `@section` Razor syntax. If a page or view defines a section, it must be rendered (or an error will occur).

An example `@section` definition in Razor Pages view:

```
@section Scripts {
     <script type="text/javascript" src="~/scripts/main.js"></script>
}
```

In the preceding code, `scripts/main.js` is added to the `scripts` section on a page or view. Other pages or views in the same app might not require this script and wouldn't define a scripts section.

The following markup uses the [Partial Tag Helper](tag-helpers/built-in/partial-tag-helper?view=aspnetcore-10.0) to render `_ValidationScriptsPartial.cshtml`:

```
@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

[Scaffolding Identity](../../security/authentication/scaffold-identity?view=aspnetcore-10.0) generated the preceding markup.

Sections defined in a page or view are available only in its immediate layout page. They can't be referenced from partials, view components, or other parts of the view system.

### Ignoring sections

By default, the layout page must render the body and all sections in a content page. The Razor view engine enforces this by tracking whether the body and each section have been rendered.

To instruct the view engine to ignore the body or sections, call the `IgnoreBody` and `IgnoreSection` methods.

The body and every section in a Razor page must be either rendered or ignored.

[]()

## Importing Shared Directives

Views and pages can use Razor directives to import namespaces and use [dependency injection](dependency-injection?view=aspnetcore-10.0). Directives shared by many views might be specified in a common `_ViewImports.cshtml` file. The `_ViewImports` file supports the following directives:

* `@addTagHelper`
* `@removeTagHelper`
* `@tagHelperPrefix`
* `@using`
* `@model`
* `@inherits`
* `@inject`
* `@namespace`

The file doesn't support other Razor features, such as functions and section definitions.

A sample `_ViewImports.cshtml` file:

```
@using WebApplication1
@using WebApplication1.Models
@using WebApplication1.Models.AccountViewModels
@using WebApplication1.Models.ManageViewModels
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

The `_ViewImports.cshtml` file for an ASP.NET Core MVC app is typically placed in the *Pages* (or *Views*) folder. A `_ViewImports.cshtml` file can be placed within any folder, in which case it will only be applied to pages or views within that folder and its subfolders. `_ViewImports` files are processed starting at the root level and then for each folder leading up to the location of the page or view itself. `_ViewImports` settings specified at the root level might be overridden at the folder level.

For example, suppose:

* The root level `_ViewImports.cshtml` file includes `@model MyModel1` and `@addTagHelper *, MyTagHelper1`.
* A subfolder `_ViewImports.cshtml` file includes `@model MyModel2` and `@addTagHelper *, MyTagHelper2`.

Pages and views in the subfolder will have access to both Tag Helpers and the `MyModel2` model.

If multiple `_ViewImports.cshtml` files are found in the file hierarchy, the combined behavior of the directives are:

* `@addTagHelper`, `@removeTagHelper`: All run, in order.
* `@tagHelperPrefix`: The closest one to the view overrides any others.
* `@model`: The closest one to the view overrides any others.
* `@inherits`: The closest one to the view overrides any others.
* `@using`: All are included; duplicates are ignored.
* `@inject`: For each property, the closest one to the view overrides any others with the same property name.

[]()

## Running Code Before Each View

Code that needs to run before each view or page should be placed in the `_ViewStart.cshtml` file. By convention, the `_ViewStart.cshtml` file is located in the *Pages* (or *Views*) folder. The statements listed in `_ViewStart.cshtml` are run before every full view (not layouts, and not partial views). Like [ViewImports.cshtml](layout?view=aspnetcore-10.0#viewimports), `_ViewStart.cshtml` is hierarchical. If a `_ViewStart.cshtml` file is defined in the view or pages folder, it will be run after the one defined in the root of the *Pages* (or *Views*) folder (if any).

A sample `_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The preceding file specifies that all views will use the `_Layout.cshtml` layout.

`_ViewStart.cshtml` and `_ViewImports.cshtml` are **not** typically placed in the */Pages/Shared* (or */Views/Shared*) folder. The app-level versions of these files should be placed directly in the */Pages* (or */Views*) folder.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-04

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/webapi
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/webapi.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Web API to ASP.NET Core

ASP.NET Core combines ASP.NET 4.x's MVC and Web API app models into a single programming model known as ASP.NET Core MVC.

This article shows how to migrate the Products controller created in [Getting Started with ASP.NET Web API 2](/en-us/aspnet/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api) to ASP.NET Core.

## Create the new ASP.NET Core Web API project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

1. From the **File** menu, select **New** > **Project**.

2. Enter *Web API* in the search box.

3. Select the **ASP.NET Core Web API** template and select **Next**.

4. In the **Configure your new project** dialog, name the project *ProductsCore* and select **Next**.

5. In the **Additional information** dialog:

   1. Confirm the **Framework** is **.NET 6.0 (Long-term support)**.
   2. Confirm the checkbox for **Use controllers(uncheck to use Minimal APIs)** is checked.
   3. Uncheck **Enable OpenAPI support**.
   4. Select **Create**.

1) Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

2) Change directories (`cd`) to the folder that will contain the project folder.

3) Run the following commands to create a new web API project and open it in Visual Studio Code:

   ```
   dotnet new webapi -o ProductsCore --no-openapi
   cd ProductsCore
   code -r ../ProductsCore
   ```

## Remove the *WeatherForecast* template files

1. Remove the `WeatherForecast.cs` and `Controllers/WeatherForecastController.cs` example files from the new *ProductsCore* project.
2. Open *Properties\launchSettings.json*.
3. Change `launchUrl` properties from `weatherforcast` to `productscore`.

## The configuration for ASP.NET Core Web API

ASP.NET Core doesn't use the *App\_Start* folder or the *Global.asax* file. The *web.config* file is added at publish time. For more information, see [web.config file](../../../host-and-deploy/iis/web-config?view=aspnetcore-10.0).

The `Program.cs` file:

* Replaces *Global.asax*.
* Handles all app startup tasks.

For more information, see [App startup in ASP.NET Core](../../../fundamentals/startup?view=aspnetcore-10.0).

The following shows the application startup code in the ASP.NET Core `Program.cs` file:

```
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## Copy the *Product* model

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

1. In **Solution Explorer**, right-click the project. Select **Add** > **New Folder**. Name the folder *Models*.
2. Right-click the **Models** folder. Select **Add** > **Class**. Name the class *Product* and select **Add**.
3. Replace the template model code with the following:

1) Add a folder named *Models*.
2) Add a *Product* class to the *Models* folder with the following code:

```
namespace ProductsCore.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public string? Category { get; set; }
        public decimal Price { get; set; }
    }
}
```

The preceding highlighted code changes the following:

* The `?` annotation has been added to declare the `Name` and `Category` properties as nullable reference types.

By utilizing the [Nullable feature introduced in C# 8](/en-us/dotnet/csharp/whats-new/csharp-8#nullable-reference-types), ASP.NET Core can provide additional code flow analysis and compile-time safety in the handling of reference types. For example, protecting against `null` reference exceptions.

In this case, the intent is that the `Name` and `Category` can be nullable types.

ASP.NET Core in .NET 6 projects enable nullable reference types by default. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

## Copy the *ProductsController*

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

1. Right-click the **Controllers** folder.
2. Select **Add > Controller...**.
3. In **Add New Scaffolded Item** dialog, select **Mvc Controller - Empty** then select **Add**.
4. Name the controller *ProductsController* and select **Add**.
5. Replace the template controller code with the following:

1) Add a *ProductsController* class to the *Controllers* folder with the following code:

```
using Microsoft.AspNetCore.Mvc;
using ProductsCore.Models;

namespace ProductsCore.Controllers;

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    Product[] products = new Product[]
    {
            new Product
            {
                Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1
            },
            new Product
            {
                Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M
            },
            new Product
            {
                Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M
            }
    };

    [HttpGet]
    public IEnumerable<Product> GetAllProducts()
    {
        return products;
    }

    [HttpGet("{id}")]
    public ActionResult<Product> GetProduct(int id)
    {
        var product = products.FirstOrDefault((p) => p.Id == id);
        if (product == null)
        {
            return NotFound();
        }
        return product;
    }
}
```

The preceding highlighted code changes the following, to migrate to ASP.NET Core:

* Removes using statements for the following ASP.NET 4.x components that don't exist in ASP.NET Core:

  * `ApiController` class
  * `System.Web.Http` namespace
  * `IHttpActionResult` interface

* Changes the `using ProductsApp.Models;` statement to `using ProductsCore.Models;`.

* Sets the root namespace to `ProductsCore`.

* Changes `ApiController` to [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase).

* Adds `using Microsoft.AspNetCore.Mvc;` to resolve the `ControllerBase` reference.

* Changes the `GetProduct` action's return type from `IHttpActionResult` to `ActionResult<Product>`. For more info, see [Controller action return types](/en-us/aspnet/core/web-api/action-return-types).

* Simplifies the `GetProduct` action's `return` statement to the following statement:

  ```
  return product;
  ```

* Adds the following attributes which are explained in the next sections:

  * `[Route("api/[controller]")]`
  * `[ApiController]`
  * `[HttpGet]`
  * `[HttpGet("{id}")]`

## Routing

ASP.NET Core provides a minimal hosting model in which the endpoint routing middleware wraps the entire middleware pipeline, therefore routes can be added directly to the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) without an explicit call to [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) or [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) to register routes.

`UseRouting` can still be used to specify where route matching happens, but `UseRouting` doesn't need to be explicitly called if routes should be matched at the beginning of the middleware pipeline.

```
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

**Note:** Routes added directly to the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) execute at the ***end*** of the pipeline.

## Routing in the migrated `ProductsController`

The migrated `ProductsController` contains the following highlighted attributes:

```
using Microsoft.AspNetCore.Mvc;
using ProductsCore.Models;

namespace ProductsCore.Controllers;

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    Product[] products = new Product[]
    {
            new Product
            {
                Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1
            },
            new Product
            {
                Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M
            },
            new Product
            {
                Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M
            }
    };

    [HttpGet]
    public IEnumerable<Product> GetAllProducts()
    {
        return products;
    }

    [HttpGet("{id}")]
    public ActionResult<Product> GetProduct(int id)
    {
        var product = products.FirstOrDefault((p) => p.Id == id);
        if (product == null)
        {
            return NotFound();
        }
        return product;
    }
}
```

* The [`[Route]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute) attribute [configures the controller's attribute routing](../../../fundamentals/routing?view=aspnetcore-10.0) pattern.

* The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute makes attribute routing a requirement for all actions in this controller.

* [Attribute routing supports tokens](../../../mvc/controllers/routing?view=aspnetcore-10.0#token-replacement-in-route-templates-controller-action-area), such as `[controller]` and `[action]`. At runtime, each token is replaced with the name of the controller or action, respectively, to which the attribute has been applied. The tokens:

  * Reduces or eliminates the need to use hard coded strings for the route.
  * Ensure routes remain synchronized with the corresponding controllers and actions when automatic rename refactorings are applied.

* HTTP Get requests are enabled for `ProductController` actions with the following attributes:

  * [`[HttpGet]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute) attribute applied to the `GetAllProducts` action.
  * `[HttpGet("{id}")]` attribute applied to the `GetProduct` action.

Run the migrated project, and browse to `/api/products`. For example: https\://localhost:`<port>`/api/products. A full list of three products appears. Browse to `/api/products/1`. The first product appears.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/migration/) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Additional resources

* [Create web APIs with ASP.NET Core](../../../web-api/?view=aspnetcore-10.0)
* [Controller action return types in ASP.NET Core web API](../../../web-api/action-return-types?view=aspnetcore-10.0)
* [Compatibility version for ASP.NET Core MVC](../../../mvc/compatibility-version?view=aspnetcore-10.0)

This article demonstrates the steps required to migrate from ASP.NET 4.x Web API to ASP.NET Core MVC.

## Prerequisites

* [Visual Studio 2019 16.4 or later](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the **ASP.NET and web development** workload
* [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)

## Review ASP.NET 4.x Web API project

This article uses the *ProductsApp* project created in [Getting Started with ASP.NET Web API 2](/en-us/aspnet/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api). In that project, a basic ASP.NET 4.x Web API project is configured as follows.

In `Global.asax.cs`, a call is made to `WebApiConfig.Register`:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Routing;

namespace ProductsApp
{
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);
        }
    }
}
```

The `WebApiConfig` class is found in the *App\_Start* folder and has a static `Register` method:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace ProductsApp
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}
```

The preceding class:

* Configures [attribute routing](/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2), although it's not actually being used.
* Configures the routing table. The sample code expects URLs to match the format `/api/{controller}/{id}`, with `{id}` being optional.

The following sections demonstrate migration of the Web API project to ASP.NET Core MVC.

## Create the destination project

Create a new blank solution in Visual Studio and add the ASP.NET 4.x Web API project to migrate:

1. From the **File** menu, select **New** > **Project**.
2. Select the **Blank Solution** template and select **Next**.
3. Name the solution *WebAPIMigration*. Select **Create**.
4. Add the existing *ProductsApp* project to the solution.

Add a new API project to migrate to:

1. Add a new **ASP.NET Core Web Application** project to the solution.
2. In the **Configure your new project** dialog, Name the project *ProductsCore*, and select **Create**.
3. In the **Create a new ASP.NET Core Web Application** dialog, confirm that **.NET Core** and **ASP.NET Core 3.1** are selected. Select the **API** project template, and select **Create**.
4. Remove the `WeatherForecast.cs` and `Controllers/WeatherForecastController.cs` example files from the new *ProductsCore* project.

The solution now contains two projects. The following sections explain migrating the *ProductsApp* project's contents to the *ProductsCore* project.

## Migrate configuration

ASP.NET Core doesn't use the *App\_Start* folder or the *Global.asax* file. Additionally, the *web.config* file is added at publish time.

The `Startup` class:

* Replaces *Global.asax*.
* Handles all app startup tasks.

For more information, see [App startup in ASP.NET Core](../../../fundamentals/startup?view=aspnetcore-10.0).

## Migrate models and controllers

The following code shows the `ProductsController` to be updated for ASP.NET Core:

```
using ProductsApp.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;

namespace ProductsApp.Controllers
{
    public class ProductsController : ApiController
    {
        Product[] products = new Product[] 
        { 
            new Product
            {
                Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1
            }, 
            new Product
            {
                Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M
            }, 
            new Product
            {
                Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M
            } 
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products;
        }

        public IHttpActionResult GetProduct(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product);
        }
    }
}
```

Update the `ProductsController` for ASP.NET Core:

1. Copy `Controllers/ProductsController.cs` and the *Models* folder from the original project to the new one.
2. Change the copied files' root namespace to `ProductsCore`.
3. Update the `using ProductsApp.Models;` statement to `using ProductsCore.Models;`.

The following components don't exist in ASP.NET Core:

* `ApiController` class
* `System.Web.Http` namespace
* `IHttpActionResult` interface

Make the following changes:

1. Change `ApiController` to [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase). Add `using Microsoft.AspNetCore.Mvc;` to resolve the `ControllerBase` reference.

2. Delete `using System.Web.Http;`.

3. Change the `GetProduct` action's return type from `IHttpActionResult` to `ActionResult<Product>`.

4. Simplify the `GetProduct` action's `return` statement to the following:

   ```
   return product;
   ```

## Configure routing

The ASP.NET Core *API* project template includes endpoint routing configuration in the generated code.

The following [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) calls:

* Register route matching and endpoint execution in the [middleware](../../../fundamentals/middleware/?view=aspnetcore-10.0) pipeline.
* Replace the *ProductsApp* project's `App_Start/WebApiConfig.cs` file.

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

Configure routing as follows:

1. Mark the `ProductsController` class with the following attributes:

   ```
   [Route("api/[controller]")]
   [ApiController]
   ```

   The preceding [`[Route]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute) attribute configures the controller's attribute routing pattern. The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute makes attribute routing a requirement for all actions in this controller.

   Attribute routing supports tokens, such as `[controller]` and `[action]`. At runtime, each token is replaced with the name of the controller or action, respectively, to which the attribute has been applied. The tokens:

   * Reduce the number of magic strings in the project.
   * Ensure routes remain synchronized with the corresponding controllers and actions when automatic rename refactorings are applied.

2. Enable HTTP Get requests to the `ProductsController` actions:

   * Apply the [`[HttpGet]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute) attribute to the `GetAllProducts` action.
   * Apply the `[HttpGet("{id}")]` attribute to the `GetProduct` action.

Run the migrated project, and browse to `/api/products`. A full list of three products appears. Browse to `/api/products/1`. The first product appears.

## Additional resources

* [Create web APIs with ASP.NET Core](../../../web-api/?view=aspnetcore-10.0)
* [Controller action return types in ASP.NET Core web API](../../../web-api/action-return-types?view=aspnetcore-10.0)
* [Compatibility version for ASP.NET Core MVC](../../../mvc/compatibility-version?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/min-api-filters
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/min-api-filters.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Filters in Minimal API apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Fiyaz Bin Hasan](https://github.com/fiyazbinhasan), [Martin Costello](https://twitter.com/martin_costello), and [Rick Anderson](https://twitter.com/RickAndMSFT)

Minimal API filters allow developers to implement business logic that supports the following tasks:

* Run code before and after the endpoint handler
* Inspect and modify parameters provided during an endpoint handler invocation
* Intercept the response behavior of an endpoint handler

Filters are helpful in many scenarios:

* Validate request parameters and body sent to an endpoint
* Log information about the request and response
* Validate a request targets a supported API version

This article describes how to use filters in your Minimal API apps, such as for validating request data sent to your app and logging the response.

## Work with filters

Filters are registered by providing a [Delegate](/en-us/dotnet/csharp/programming-guide/delegates/) that takes a [EndpointFilterInvocationContext](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http.Abstractions/src/EndpointFilterInvocationContext.cs) and returns a [EndpointFilterDelegate](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http.Abstractions/src/EndpointFilterDelegate.cs). The `EndpointFilterInvocationContext` provides access to the `HttpContext` of the request and an `Arguments` list. The list specifies the arguments passed to the handler in the order in which they appear in the declaration of the handler.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

string ColorName(string color) => $"Color specified: {color}!";

app.MapGet("/colorSelector/{color}", ColorName)
    .AddEndpointFilter(async (invocationContext, next) =>
    {
        var color = invocationContext.GetArgument<string>(0);

        if (color == "Red")
        {
            return Results.Problem("Red not allowed!");
        }
        return await next(invocationContext);
    });

app.Run();
```

The preceding code:

* Calls the `AddEndpointFilter` extension method to add a filter to the `/colorSelector/{color}` endpoint.
* Returns the color specified, except for the value `"Red"`.
* Returns [Results.Problem](/en-us/dotnet/api/microsoft.aspnetcore.http.results.problem) when the `/colorSelector/Red` is requested.
* Uses `next` as the `EndpointFilterDelegate` and `invocationContext` as the `EndpointFilterInvocationContext` to invoke the next filter in the pipeline, or the request delegate if the last filter is already invoked.

The filter runs before the endpoint handler. When multiple `AddEndpointFilter` invocations are made on a handler:

* The execution order of filter code called *before* the call to `EndpointFilterDelegate` (`next`) is First In, First Out (FIFO).
* The execution order of filter code called *after* the call to `EndpointFilterDelegate` (`next`) is First In, Last Out (FILO).

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
    {
        app.Logger.LogInformation("             Endpoint");
        return "Test of multiple filters";
    })
    .AddEndpointFilter(async (efiContext, next) =>
    {
        app.Logger.LogInformation("Before first filter");
        var result = await next(efiContext);
        app.Logger.LogInformation("After first filter");
        return result;
    })
    .AddEndpointFilter(async (efiContext, next) =>
    {
        app.Logger.LogInformation(" Before 2nd filter");
        var result = await next(efiContext);
        app.Logger.LogInformation(" After 2nd filter");
        return result;
    })
    .AddEndpointFilter(async (efiContext, next) =>
    {
        app.Logger.LogInformation("     Before 3rd filter");
        var result = await next(efiContext);
        app.Logger.LogInformation("     After 3rd filter");
        return result;
    });

app.Run();
```

In the preceding code, the filters and endpoint log the following output:

```
Before first filter
    Before 2nd filter
        Before 3rd filter
            Endpoint
        After 3rd filter
    After 2nd filter
After first filter
```

The following code uses filters that implement the `IEndpointFilter` interface:

```
using Filters.EndpointFilters;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
    {
        app.Logger.LogInformation("Endpoint");
        return "Test of multiple filters";
    })
    .AddEndpointFilter<AEndpointFilter>()
    .AddEndpointFilter<BEndpointFilter>()
    .AddEndpointFilter<CEndpointFilter>();

app.Run();
```

In the preceding code, the logs for the filters and handlers show the run order:

```
AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
      Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next
```

Filters that implement the `IEndpointFilter` interface are shown in the following example:

```

namespace Filters.EndpointFilters;

public abstract class ABCEndpointFilters : IEndpointFilter
{
    protected readonly ILogger Logger;
    private readonly string _methodName;

    protected ABCEndpointFilters(ILoggerFactory loggerFactory)
    {
        Logger = loggerFactory.CreateLogger<ABCEndpointFilters>();
        _methodName = GetType().Name;
    }

    public virtual async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        Logger.LogInformation("{MethodName} Before next", _methodName);
        var result = await next(context);
        Logger.LogInformation("{MethodName} After next", _methodName);
        return result;
    }
}

class AEndpointFilter : ABCEndpointFilters
{
    public AEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}

class BEndpointFilter : ABCEndpointFilters
{
    public BEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}

class CEndpointFilter : ABCEndpointFilters
{
    public CEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}
```

## Validate an object with a filter

Consider a filter that validates a `Todo` object:

```
app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;
    
    await db.SaveChangesAsync();

    return Results.NoContent();
}).AddEndpointFilter(async (efiContext, next) =>
{
    var tdparam = efiContext.GetArgument<Todo>(0);

    var validationError = Utilities.IsValid(tdparam);

    if (!string.IsNullOrEmpty(validationError))
    {
        return Results.Problem(validationError);
    }
    return await next(efiContext);
});
```

In the preceding code:

* The `EndpointFilterInvocationContext` object provides access to the parameters associated with a particular request issued to the endpoint via the `GetArguments` method.
* The filter is registered by using a `delegate` that takes a `EndpointFilterInvocationContext` and returns a `EndpointFilterDelegate`.

In addition to being passed as delegates, filters can be registered by implementing the `IEndpointFilter` interface. The following code shows the preceding filter encapsulated in a class that implements `IEndpointFilter`:

```
public class TodoIsValidFilter : IEndpointFilter
{
    private ILogger _logger;

    public TodoIsValidFilter(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<TodoIsValidFilter>();
    }

    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext, 
        EndpointFilterDelegate next)
    {
        var todo = efiContext.GetArgument<Todo>(0);

        var validationError = Utilities.IsValid(todo!);

        if (!string.IsNullOrEmpty(validationError))
        {
            _logger.LogWarning(validationError);
            return Results.Problem(validationError);
        }
        return await next(efiContext);
    }
}
```

Filters that implement the `IEndpointFilter` interface can resolve dependencies from [Dependency Injection (DI)](../dependency-injection?view=aspnetcore-10.0), as shown in the previous code. Although filters can resolve dependencies from DI, filters themselves **can't** be resolved from DI.

The `ToDoIsValidFilter` is applied to the following endpoints:

```
app.MapPut("/todoitems2/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
}).AddEndpointFilter<TodoIsValidFilter>();

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
}).AddEndpointFilter<TodoIsValidFilter>();
```

The following filter validates the `Todo` object and modifies the `Name` property:

```
public class TodoIsValidUcFilter : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext, 
        EndpointFilterDelegate next)
    {
        var todo = efiContext.GetArgument<Todo>(0);
        todo.Name = todo.Name!.ToUpper();

        var validationError = Utilities.IsValid(todo!);

        if (!string.IsNullOrEmpty(validationError))
        {
            return Results.Problem(validationError);
        }
        return await next(efiContext);
    }
}
```

## Register a filter by using an endpoint filter factory

In some scenarios, it might be necessary to cache some of the information provided in the [MethodInfo](/en-us/dotnet/api/system.reflection.methodinfo) in a filter. Suppose you want to verify that the handler attached to an endpoint filter has a first parameter that evaluates to a `Todo` type.

```
app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;
    
    await db.SaveChangesAsync();

    return Results.NoContent();
}).AddEndpointFilterFactory((filterFactoryContext, next) =>
{
    var parameters = filterFactoryContext.MethodInfo.GetParameters();
    if (parameters.Length >= 1 && parameters[0].ParameterType == typeof(Todo))
    {
        return async invocationContext =>
        {
            var todoParam = invocationContext.GetArgument<Todo>(0);

            var validationError = Utilities.IsValid(todoParam);

            if (!string.IsNullOrEmpty(validationError))
            {
                return Results.Problem(validationError);
            }
            return await next(invocationContext);
        };
    }
    return invocationContext => next(invocationContext); 
});
```

In the preceding code:

* The `EndpointFilterFactoryContext` object provides access to the [MethodInfo](/en-us/dotnet/api/system.reflection.methodinfo) associated with the endpoint's handler.
* The signature of the handler is examined by inspecting `MethodInfo` for the expected type signature. If the expected signature is found, the validation filter is registered onto the endpoint. This factory pattern is useful to register a filter that depends on the signature of the target endpoint handler.
* If a matching signature isn't found, a pass-through filter is registered.

## Register a filter on controller actions

In some scenarios, it might be necessary to apply the same filter logic for both route-handler based endpoints and controller actions. For this scenario, you can invoke `AddEndpointFilter` on `ControllerActionEndpointConventionBuilder` to support executing the same filter logic on actions and endpoints.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapController()
    .AddEndpointFilter(async (efiContext, next) =>
    {
        efiContext.HttpContext.Items["endpointFilterCalled"] = true;
        var result = await next(efiContext);
        return result;
    });

app.Run();
```

## Related content

* [View or download sample code](https://github.com/aspnet/Docs/tree/main/aspnetcore/fundamentals/minimal-apis/min-api-filters/samples/7) ([How to download](../?view=aspnetcore-10.0#how-to-download-a-sample))
* [ValidationFilterRouteHandlerBuilderExtensions (Validation extension methods)](https://github.com/DamianEdwards/MinimalApis.Extensions/blob/main/src/MinimalApis.Extensions/Filters/ValidationFilterRouteHandlerBuilderExtensions.cs)
* [Tutorial: Create a Minimal API with ASP.NET Core](../../tutorials/min-web-api?view=aspnetcore-10.0)
* [Authentication and authorization in Minimal APIs](security?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-29

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/purpose-strings
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/consumer-apis/purpose-strings.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Purpose strings in ASP.NET Core

[]()

Components which consume `IDataProtectionProvider` must pass a unique *purposes* parameter to the `CreateProtector` method. The purposes *parameter* is inherent to the security of the data protection system, as it provides isolation between cryptographic consumers, even if the root cryptographic keys are the same.

When a consumer specifies a purpose, the purpose string is used along with the root cryptographic keys to derive cryptographic subkeys unique to that consumer. This isolates the consumer from all other cryptographic consumers in the application: no other component can read its payloads, and it cannot read any other component's payloads. This isolation also renders infeasible entire categories of attack against the component.

In the diagram above, `IDataProtector` instances A and B **cannot** read each other's payloads, only their own.

The purpose string doesn't have to be secret. It should simply be unique in the sense that no other well-behaved component will ever provide the same purpose string.

Tip

Using the namespace and type name of the component consuming the data protection APIs is a good rule of thumb, as in practice this information will never conflict.

A Contoso-authored component which is responsible for minting bearer tokens might use Contoso.Security.BearerToken as its purpose string. Or - even better - it might use Contoso.Security.BearerToken.v1 as its purpose string. Appending the version number allows a future version to use Contoso.Security.BearerToken.v2 as its purpose, and the different versions would be completely isolated from one another as far as payloads go.

Since the purposes parameter to `CreateProtector` is a string array, the above could've been instead specified as `[ "Contoso.Security.BearerToken", "v1" ]`. This allows establishing a hierarchy of purposes and opens up the possibility of multi-tenancy scenarios with the data protection system.

[]()

Warning

Components shouldn't allow untrusted user input to be the sole source of input for the purposes chain.

For example, consider a component Contoso.Messaging.SecureMessage which is responsible for storing secure messages. If the secure messaging component were to call `CreateProtector([ username ])`, then a malicious user might create an account with username "Contoso.Security.BearerToken" in an attempt to get the component to call `CreateProtector([ "Contoso.Security.BearerToken" ])`, thus inadvertently causing the secure messaging system to mint payloads that could be perceived as authentication tokens.

A better purposes chain for the messaging component would be `CreateProtector([ "Contoso.Messaging.SecureMessage", $"User: {username}" ])`, which provides proper isolation.

The isolation provided by and behaviors of `IDataProtectionProvider`, `IDataProtector`, and purposes are as follows:

* For a given `IDataProtectionProvider` object, the `CreateProtector` method will create an `IDataProtector` object uniquely tied to both the `IDataProtectionProvider` object which created it and the purposes parameter which was passed into the method.

* The purpose parameter must not be null. (If purposes is specified as an array, this means that the array must not be of zero length and all elements of the array must be non-null.) An empty string purpose is technically allowed but is discouraged.

* Two purposes arguments are equivalent if and only if they contain the same strings (using an ordinal comparer) in the same order. A single purpose argument is equivalent to the corresponding single-element purposes array.

* Two `IDataProtector` objects are equivalent if and only if they're created from equivalent `IDataProtectionProvider` objects with equivalent purposes parameters.

* For a given `IDataProtector` object, a call to `Unprotect(protectedData)` will return the original `unprotectedData` if and only if `protectedData := Protect(unprotectedData)` for an equivalent `IDataProtector` object.

Note

We're not considering the case where some component intentionally chooses a purpose string which is known to conflict with another component. Such a component would essentially be considered malicious, and this system isn't intended to provide security guarantees in the event that malicious code is already running inside of the worker process.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-06-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/authn-and-authz
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/authn-and-authz.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Authentication and authorization in gRPC for ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/grpc/authn-and-authz/samples/6.x/) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

## Authenticate users calling a gRPC service

Use gRPC with [ASP.NET Core authentication](../security/authentication/identity?view=aspnetcore-10.0) to associate a user with each call.

The following example of `Program.cs` uses gRPC and ASP.NET Core authentication:

```
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapGrpcService<GreeterService>();
```

Note

The order in which you register the ASP.NET Core authentication middleware matters. Always call `UseAuthentication` and `UseAuthorization` after `UseRouting` and before `UseEndpoints`.

Configure the authentication mechanism your app uses during a call. Add authentication configuration in `Program.cs`. The configuration differs depending on the authentication mechanism your app uses.

After you set up authentication, access the user in gRPC service methods through the `ServerCallContext`.

```
public override Task<BuyTicketsResponse> BuyTickets(
    BuyTicketsRequest request, ServerCallContext context)
{
    var user = context.GetHttpContext().User;

    // ... access data from ClaimsPrincipal ...
}
```

### Bearer token authentication

The client provides an access token for authentication. The server validates the token and uses it to identify the user.

On the server, configure bearer token authentication by using the [JWT bearer middleware](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer).

In the .NET gRPC client, send the token with calls by using the `Metadata` collection. Send entries in the `Metadata` collection with a gRPC call as HTTP headers:

```
public bool DoAuthenticatedCall(
    Ticketer.TicketerClient client, string token)
{
    var headers = new Metadata();
    headers.Add("Authorization", $"Bearer {token}");

    var request = new BuyTicketsRequest { Count = 1 };
    var response = await client.BuyTicketsAsync(request, headers);

    return response.Success;
}
```

#### Set the bearer token with `CallCredentials`

Configuring `ChannelCredentials` on a channel is an alternative way to send the token to the service with gRPC calls. A `ChannelCredentials` can include `CallCredentials`, which provide a way to automatically set `Metadata`.

Benefits of using `CallCredentials`:

* Authentication is centrally configured on the channel. You don't need to manually provide the token to the gRPC call.
* The `CallCredentials.FromInterceptor` callback is asynchronous. Call credentials can fetch a credential token from an external system if required. Asynchronous methods inside the callback should use the `CancellationToken` on `AuthInterceptorContext`.

Note

`CallCredentials` are only applied if the channel is secured with TLS. Sending authentication headers over an insecure connection has security implications and shouldn't be done in production environments. An app can configure a channel to ignore this behavior and always use `CallCredentials` by setting `UnsafeUseInsecureChannelCallCredentials` on a channel.

The credential in the following example configures the channel to send the token with every gRPC call:

```
private static GrpcChannel CreateAuthenticatedChannel(ITokenProvder tokenProvider)
{
    var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
    {
        var token = await tokenProvider.GetTokenAsync(context.CancellationToken);
        metadata.Add("Authorization", $"Bearer {token}");
    });

    var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
    {
        Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
    });
    return channel;
}
```

#### Bearer token with gRPC client factory

gRPC client factory can create clients that send a bearer token by using `AddCallCredentials`. This method is available in [Grpc.Net.ClientFactory](https://www.nuget.org/packages/Grpc.Net.ClientFactory) version 2.46.0 or later.

The delegate that you pass to `AddCallCredentials` runs for each gRPC call:

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddCallCredentials((context, metadata) =>
    {
        if (!string.IsNullOrEmpty(_token))
        {
            metadata.Add("Authorization", $"Bearer {_token}");
        }
        return Task.CompletedTask;
    });
```

Dependency injection (DI) can be combined with `AddCallCredentials`. An overload passes `IServiceProvider` to the delegate, which can be used to get a service [constructed from DI using scoped and transient services](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes).

Consider an app that has:

* A user-defined `ITokenProvider` for getting a bearer token. Register `ITokenProvider` in DI with a scoped lifetime.
* gRPC client factory is configured to create clients that are injected into gRPC services and Web API controllers.
* gRPC calls should use `ITokenProvider` to get a bearer token.

```
public interface ITokenProvider
{
    Task<string> GetTokenAsync(CancellationToken cancellationToken);
}

public class AppTokenProvider : ITokenProvider
{
    private string _token;

    public async Task<string> GetTokenAsync(CancellationToken cancellationToken)
    {
        if (_token == null)
        {
            // App code to resolve the token here.
        }

        return _token;
    }
}
```

```
builder.Services.AddScoped<ITokenProvider, AppTokenProvider>();

builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddCallCredentials(async (context, metadata, serviceProvider) =>
    {
        var provider = serviceProvider.GetRequiredService<ITokenProvider>();
        var token = await provider.GetTokenAsync(context.CancellationToken);
        metadata.Add("Authorization", $"Bearer {token}");
    }));
```

The preceding code:

* Defines `ITokenProvider` and `AppTokenProvider`. These types handle resolving the authentication token for gRPC calls.
* Registers the `AppTokenProvider` type with DI in a scoped lifetime. `AppTokenProvider` caches the token so that only the first call in the scope is required to calculate it.
* Registers the `GreeterClient` type with client factory.
* Configures `AddCallCredentials` for this client. The delegate runs each time a call is made and adds the token returned by `ITokenProvider` to the metadata.

### Client certificate authentication

A client could alternatively provide a client certificate for authentication. [Certificate authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) happens at the TLS level, long before it ever gets to ASP.NET Core. When the request enters ASP.NET Core, the [client certificate authentication package](../security/authentication/certauth?view=aspnetcore-10.0) allows you to resolve the certificate to a `ClaimsPrincipal`.

Note

Configure the server to accept client certificates. For information on accepting client certificates in Kestrel, IIS, and Azure, see [Configure certificate authentication in ASP.NET Core](../security/authentication/certauth?view=aspnetcore-10.0#configure-your-server-to-require-certificates).

In the .NET gRPC client, add the client certificate to `HttpClientHandler` and use it to create the gRPC client:

```
public Ticketer.TicketerClient CreateClientWithCert(
    string baseAddress,
    X509Certificate2 certificate)
{
    // Add client cert to the handler
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(certificate);

    // Create the gRPC channel
    var channel = GrpcChannel.ForAddress(baseAddress, new GrpcChannelOptions
    {
        HttpHandler = handler
    });

    return new Ticketer.TicketerClient(channel);
}
```

### Other authentication mechanisms

Many ASP.NET Core supported authentication mechanisms work with gRPC:

* Microsoft Entra ID
* Client Certificate
* IdentityServer
* JWT Token
* OAuth 2.0
* OpenID Connect
* WS-Federation

For more information on configuring authentication on the server, see [ASP.NET Core authentication](../security/authentication/identity?view=aspnetcore-10.0).

Configuring the gRPC client to use authentication depends on the authentication mechanism you're using. The previous bearer token example shows how to send authentication metadata with gRPC calls, while the client certificate example shows how to configure TLS client authentication:

* Strongly typed gRPC clients use `HttpClient` internally. Configure authentication on [HttpClientHandler](/en-us/dotnet/api/system.net.http.httpclienthandler), or add custom [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) instances to the `HttpClient`.
* Each gRPC call has an optional `CallOptions` argument. Send custom headers by using the option's headers collection.

Note

You can't use Windows Authentication (NTLM/Kerberos/Negotiate) with gRPC. gRPC requires HTTP/2, and HTTP/2 doesn't support Windows Authentication.

## Authorize users to access services and service methods

By default, unauthenticated users can call all methods in a service. To require authentication, apply the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute to the service:

```
[Authorize]
public class TicketerService : Ticketer.TicketerBase
{
}
```

Use the constructor arguments and properties of the `[Authorize]` attribute to restrict access to only users matching specific [authorization policies](../security/authorization/policies?view=aspnetcore-10.0). For example, if you have a custom authorization policy called `MyAuthorizationPolicy`, ensure that only users matching that policy can access the service by using the following code:

```
[Authorize("MyAuthorizationPolicy")]
public class TicketerService : Ticketer.TicketerBase
{
}
```

You can also apply the `[Authorize]` attribute to individual service methods. If the current user doesn't match the policies applied to **both** the method and the class, the caller receives an error:

```
[Authorize]
public class TicketerService : Ticketer.TicketerBase
{
    public override Task<AvailableTicketsResponse> GetAvailableTickets(
        Empty request, ServerCallContext context)
    {
        // ... buy tickets for the current user ...
    }

    [Authorize("Administrators")]
    public override Task<BuyTicketsResponse> RefundTickets(
        BuyTicketsRequest request, ServerCallContext context)
    {
        // ... refund tickets (something only Administrators can do) ..
    }
}
```

### Authorization extension methods

You can also control authorization by using standard ASP.NET Core authorization extension methods, such as [`AllowAnonymous`](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.allowanonymous) and [`RequireAuthorization`](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization).

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();

var app = builder.Build();
app.MapGrpcService<TicketerService>().RequireAuthorization("Administrators");
app.Run();
```

## Additional resources

* [Bearer Token authentication in ASP.NET Core](https://blogs.msdn.microsoft.com/webdev/2016/10/27/bearer-token-authentication-in-asp-net-core/)
* [Configure Client Certificate authentication in ASP.NET Core](../security/authentication/certauth?view=aspnetcore-10.0)
* [Configure interceptors in a gRPC client factory in .NET](clientfactory?view=aspnetcore-10.0#configure-interceptors)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/grpc/authn-and-authz/samples/) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

## Authenticate users calling a gRPC service

Use gRPC with [ASP.NET Core authentication](../security/authentication/identity?view=aspnetcore-10.0) to associate a user with each call.

The following example shows `Startup.Configure` that uses gRPC and ASP.NET Core authentication:

```
public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<GreeterService>();
    });
}
```

Note

The order in which you register the ASP.NET Core authentication middleware matters. Always call `UseAuthentication` and `UseAuthorization` after `UseRouting` and before `UseEndpoints`.

Configure the authentication mechanism your app uses during a call. Add authentication configuration in `Startup.ConfigureServices`. The configuration differs depending on the authentication mechanism your app uses.

After you set up authentication, access the user in gRPC service methods through the `ServerCallContext`.

```
public override Task<BuyTicketsResponse> BuyTickets(
    BuyTicketsRequest request, ServerCallContext context)
{
    var user = context.GetHttpContext().User;

    // ... access data from ClaimsPrincipal ...
}
```

### Bearer token authentication

The client provides an access token for authentication. The server validates the token and uses it to identify the user.

On the server, configure bearer token authentication by using the [JWT bearer middleware](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer).

In the .NET gRPC client, send the token with calls by using the `Metadata` collection. Send entries in the `Metadata` collection with a gRPC call as HTTP headers:

```
public bool DoAuthenticatedCall(
    Ticketer.TicketerClient client, string token)
{
    var headers = new Metadata();
    headers.Add("Authorization", $"Bearer {token}");

    var request = new BuyTicketsRequest { Count = 1 };
    var response = await client.BuyTicketsAsync(request, headers);

    return response.Success;
}
```

#### Set the bearer token with `CallCredentials`

Configuring `ChannelCredentials` on a channel is an alternative way to send the token to the service with gRPC calls. A `ChannelCredentials` can include `CallCredentials`, which provide a way to automatically set `Metadata`.

Benefits of using `CallCredentials`:

* Authentication is centrally configured on the channel. You don't need to manually provide the token to the gRPC call.
* The `CallCredentials.FromInterceptor` callback is asynchronous. Call credentials can fetch a credential token from an external system if required. Asynchronous methods inside the callback should use the `CancellationToken` on `AuthInterceptorContext`.

Note

`CallCredentials` are only applied if the channel is secured with TLS. Sending authentication headers over an insecure connection has security implications and shouldn't be done in production environments. An app can configure a channel to ignore this behavior and always use `CallCredentials` by setting `UnsafeUseInsecureChannelCallCredentials` on a channel.

The credential in the following example configures the channel to send the token with every gRPC call:

```
private static GrpcChannel CreateAuthenticatedChannel(ITokenProvder tokenProvider)
{
    var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
    {
        var token = await tokenProvider.GetTokenAsync(context.CancellationToken);
        metadata.Add("Authorization", $"Bearer {token}");
    });

    var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
    {
        Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
    });
    return channel;
}
```

#### Bearer token with gRPC client factory

gRPC client factory can create clients that send a bearer token by using `AddCallCredentials`. This method is available in [Grpc.Net.ClientFactory](https://www.nuget.org/packages/Grpc.Net.ClientFactory) version 2.46.0 or later.

The delegate that you pass to `AddCallCredentials` runs for each gRPC call:

```
services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddCallCredentials((context, metadata) =>
    {
        if (!string.IsNullOrEmpty(_token))
        {
            metadata.Add("Authorization", $"Bearer {_token}");
        }
        return Task.CompletedTask;
    });
```

Dependency injection (DI) can be combined with `AddCallCredentials`. An overload passes `IServiceProvider` to the delegate, which can be used to get a service [constructed from DI using scoped and transient services](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes).

Consider an app that has:

* A user-defined `ITokenProvider` for getting a bearer token. Register `ITokenProvider` in DI with a scoped lifetime.
* gRPC client factory is configured to create clients that are injected into gRPC services and Web API controllers.
* gRPC calls should use `ITokenProvider` to get a bearer token.

```
public interface ITokenProvider
{
    Task<string> GetTokenAsync(CancellationToken cancellationToken);
}

public class AppTokenProvider : ITokenProvider
{
    private string _token;

    public async Task<string> GetTokenAsync(CancellationToken cancellationToken)
    {
        if (_token == null)
        {
            // App code to resolve the token here.
        }

        return _token;
    }
}
```

```
services.AddScoped<ITokenProvider, AppTokenProvider>();

services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddCallCredentials(async (context, metadata, serviceProvider) =>
    {
        var provider = serviceProvider.GetRequiredService<ITokenProvider>();
        var token = await provider.GetTokenAsync(context.CancellationToken);
        metadata.Add("Authorization", $"Bearer {token}");
    }));
```

The preceding code:

* Defines `ITokenProvider` and `AppTokenProvider`. These types handle resolving the authentication token for gRPC calls.
* Registers the `AppTokenProvider` type with DI in a scoped lifetime. `AppTokenProvider` caches the token so that only the first call in the scope is required to calculate it.
* Registers the `GreeterClient` type with client factory.
* Configures `AddCallCredentials` for this client. The delegate runs each time a call is made and adds the token returned by `ITokenProvider` to the metadata.

### Client certificate authentication

A client could alternatively provide a client certificate for authentication. [Certificate authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) happens at the TLS level, long before it ever gets to ASP.NET Core. When the request enters ASP.NET Core, the [client certificate authentication package](../security/authentication/certauth?view=aspnetcore-10.0) allows you to resolve the certificate to a `ClaimsPrincipal`.

Note

Configure the server to accept client certificates. For information on accepting client certificates in Kestrel, IIS, and Azure, see [Configure certificate authentication in ASP.NET Core](../security/authentication/certauth?view=aspnetcore-10.0#configure-your-server-to-require-certificates).

In the .NET gRPC client, add the client certificate to `HttpClientHandler` and use it to create the gRPC client:

```
public Ticketer.TicketerClient CreateClientWithCert(
    string baseAddress,
    X509Certificate2 certificate)
{
    // Add client cert to the handler
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(certificate);

    // Create the gRPC channel
    var channel = GrpcChannel.ForAddress(baseAddress, new GrpcChannelOptions
    {
        HttpHandler = handler
    });

    return new Ticketer.TicketerClient(channel);
}
```

### Other authentication mechanisms

Many ASP.NET Core supported authentication mechanisms work with gRPC:

* Microsoft Entra ID
* Client Certificate
* IdentityServer
* JWT Token
* OAuth 2.0
* OpenID Connect
* WS-Federation

For more information on configuring authentication on the server, see [ASP.NET Core authentication](../security/authentication/identity?view=aspnetcore-10.0).

Configuring the gRPC client to use authentication depends on the authentication mechanism you're using. The previous bearer token and client certificate examples show a couple of ways you can configure the gRPC client to send authentication metadata with gRPC calls:

* Strongly typed gRPC clients use `HttpClient` internally. Configure authentication on [HttpClientHandler](/en-us/dotnet/api/system.net.http.httpclienthandler), or add custom [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) instances to the `HttpClient`.
* Each gRPC call has an optional `CallOptions` argument. Send custom headers by using the option's headers collection.

Note

You can't use Windows Authentication (NTLM/Kerberos/Negotiate) with gRPC. gRPC requires HTTP/2, and HTTP/2 doesn't support Windows Authentication.

## Authorize users to access services and service methods

By default, unauthenticated users can call all methods in a service. To require authentication, apply the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute to the service:

```
[Authorize]
public class TicketerService : Ticketer.TicketerBase
{
}
```

Use the constructor arguments and properties of the `[Authorize]` attribute to restrict access to only users matching specific [authorization policies](../security/authorization/policies?view=aspnetcore-10.0). For example, if you have a custom authorization policy called `MyAuthorizationPolicy`, ensure that only users matching that policy can access the service by using the following code:

```
[Authorize("MyAuthorizationPolicy")]
public class TicketerService : Ticketer.TicketerBase
{
}
```

You can also apply the `[Authorize]` attribute to individual service methods. If the current user doesn't match the policies applied to **both** the method and the class, the caller receives an error:

```
[Authorize]
public class TicketerService : Ticketer.TicketerBase
{
    public override Task<AvailableTicketsResponse> GetAvailableTickets(
        Empty request, ServerCallContext context)
    {
        // ... buy tickets for the current user ...
    }

    [Authorize("Administrators")]
    public override Task<BuyTicketsResponse> RefundTickets(
        BuyTicketsRequest request, ServerCallContext context)
    {
        // ... refund tickets (something only Administrators can do) ..
    }
}
```

## Additional resources

* [Bearer Token authentication in ASP.NET Core](https://blogs.msdn.microsoft.com/webdev/2016/10/27/bearer-token-authentication-in-asp-net-core/)
* [Configure Client Certificate authentication in ASP.NET Core](../security/authentication/certauth?view=aspnetcore-10.0)
* [Configure interceptors in a gRPC client factory in .NET](clientfactory?view=aspnetcore-10.0#configure-interceptors)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/samesite
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/samesite.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Work with SameSite cookies in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

SameSite is an [IETF](https://ietf.org/about/) draft standard designed to provide some protection against cross-site request forgery (CSRF) attacks. Originally drafted in [2016](https://tools.ietf.org/html/draft-west-first-party-cookies-07), the draft standard was updated in [2019](https://tools.ietf.org/html/draft-west-cookie-incrementalism-00). The updated standard is not backward compatible with the previous standard, with the following being the most noticeable differences:

* Cookies without SameSite header are treated as `SameSite=Lax` by default.
* `SameSite=None` must be used to allow cross-site cookie use.
* Cookies that assert `SameSite=None` must also be marked as `Secure`.
* Applications that use [`<iframe>`](https://developer.mozilla.org/docs/Web/HTML/Element/iframe) may experience issues with `sameSite=Lax` or `sameSite=Strict` cookies because `<iframe>` is treated as cross-site scenarios.
* The value `SameSite=None` is not allowed by the [2016 standard](https://tools.ietf.org/html/draft-west-first-party-cookies-07) and causes some implementations to treat such cookies as `SameSite=Strict`. See [Supporting older browsers](#sob) in this document.

The `SameSite=Lax` setting works for most application cookies. Some forms of authentication like [OpenID Connect](https://openid.net/developers/how-connect-works/) (OIDC) and [WS-Federation](https://auth0.com/docs/protocols/ws-fed) default to POST based redirects. The POST based redirects trigger the SameSite browser protections, so SameSite is disabled for these components. Most [OAuth](https://oauth.net/) logins are not affected due to differences in how the request flows.

Each ASP.NET Core component that emits cookies needs to decide if SameSite is appropriate.

## SameSite and Identity

ASP.NET Core [Identity](authentication/identity?view=aspnetcore-10.0) is largely unaffected by [SameSite cookies](samesite?view=aspnetcore-10.0) except for advanced scenarios like `IFrames` or `OpenIdConnect` integration.

When using `Identity`, do ***not*** add any cookie providers or call `services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)`, `Identity` takes care of that.

## SameSite test sample code

The following sample can be downloaded and tested:

| Sample                                                                                              | Document                                                                                  |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [Razor Pages](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore31RazorPages) | [ASP.NET Core 3.1 Razor Pages SameSite cookie sample](samesite/rp31?view=aspnetcore-10.0) |

## .NET support for the sameSite attribute

.NET supports the 2019 draft standard for SameSite. Developers are able to programmatically control the value of the sameSite attribute using the `HttpCookie.SameSite` property. Setting the `SameSite` property to `Strict`, `Lax`, or `None` results in those values being written on the network with the cookie. Setting to `SameSiteMode.Unspecified` indicates no sameSite should be sent with the cookie.

```
    var cookieOptions = new CookieOptions
    {
        // Set the secure flag, which Chrome's changes will require for SameSite none.
        // Note this will also require you to be running on HTTPS.
        Secure = true,

        // Set the cookie to HTTP only which is good practice unless you really do need
        // to access it client side in scripts.
        HttpOnly = true,

        // Add the SameSite attribute, this will emit the attribute with a value of none.
        SameSite = SameSiteMode.None

        // The client should follow its default cookie policy.
        // SameSite = SameSiteMode.Unspecified
    };

    // Add the cookie to the response cookie collection
    Response.Cookies.Append("MyCookie", "cookieValue", cookieOptions);
}
```

## API usage with SameSite

[HttpContext.Response.Cookies.Append](/en-us/dotnet/api/microsoft.aspnetcore.http.iresponsecookies.append) defaults to `Unspecified`, meaning no SameSite attribute added to the cookie and the client will use its default behavior (Lax for new browsers, None for old ones). The following code shows how to change the cookie SameSite value to `SameSiteMode.Lax`:

```
HttpContext.Response.Cookies.Append(
                     "name", "value",
                     new CookieOptions() { SameSite = SameSiteMode.Lax });
```

All ASP.NET Core components that emit cookies override the preceding defaults with settings appropriate for their scenarios. The overridden preceding default values haven't changed.

| Component                                                                                                                     | cookie                                                                                                                                                                                                                                 | Default       |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| [CookieBuilder](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder)                                                    | [SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.samesite#microsoft-aspnetcore-http-cookiebuilder-samesite)                                                                                                        | `Unspecified` |
| [Session](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.session#microsoft-aspnetcore-http-httpcontext-session)      | [SessionOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.cookie#microsoft-aspnetcore-builder-sessionoptions-cookie)                                                                                       | `Lax`         |
| [CookieTempDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.cookietempdataprovider)                      | [CookieTempDataProviderOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cookietempdataprovideroptions.cookie#microsoft-aspnetcore-mvc-cookietempdataprovideroptions-cookie)                                                  | `Lax`         |
| [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery)                                               | [AntiforgeryOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie#microsoft-aspnetcore-antiforgery-antiforgeryoptions-cookie)                                                                   | `Strict`      |
| [Cookie Authentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie)                | [CookieAuthenticationOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieauthenticationoptions.cookiename#microsoft-aspnetcore-builder-cookieauthenticationoptions-cookiename)                                        | `Lax`         |
| [AddTwitter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.twitterextensions.addtwitter)                         | [TwitterOptions.StateCookie](/en-us/dotnet/api/microsoft.aspnetcore.authentication.twitter.twitteroptions.statecookie#microsoft-aspnetcore-authentication-twitter-twitteroptions-statecookie)                                          | `Lax`         |
| [RemoteAuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationhandler-1) | [RemoteAuthenticationOptions.CorrelationCookie](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.correlationcookie#microsoft-aspnetcore-authentication-remoteauthenticationoptions-correlationcookie) | `None`        |
| [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect)       | [OpenIdConnectOptions.NonceCookie](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.noncecookie#microsoft-aspnetcore-authentication-openidconnect-openidconnectoptions-noncecookie)            | `None`        |
| [HttpContext.Response.Cookies.Append](/en-us/dotnet/api/microsoft.aspnetcore.http.iresponsecookies.append)                    | [CookieOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.cookieoptions)                                                                                                                                                             | `Unspecified` |

ASP.NET Core 3.1 or later provides the following SameSite support:

* Redefines the behavior of `SameSiteMode.None` to emit `SameSite=None`
* Adds a new value `SameSiteMode.Unspecified` to omit the SameSite attribute.
* All cookies APIs default to `Unspecified`. Some components that use cookies set values more specific to their scenarios. See the table above for examples.

In ASP.NET Core 3.0 or later the SameSite defaults were changed to avoid conflicting with inconsistent client defaults. The following APIs have changed the default from `SameSiteMode.Lax `to `-1` to avoid emitting a SameSite attribute for these cookies:

* [CookieOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.cookieoptions) used with [HttpContext.Response.Cookies.Append](/en-us/dotnet/api/microsoft.aspnetcore.http.iresponsecookies.append)
* [CookieBuilder](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder) used as a factory for `CookieOptions`
* [CookiePolicyOptions.MinimumSameSitePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.minimumsamesitepolicy#microsoft-aspnetcore-builder-cookiepolicyoptions-minimumsamesitepolicy)

## History and changes

SameSite support was first implemented in ASP.NET Core in 2.0 using the [2016 draft standard](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1). The 2016 standard was opt-in. ASP.NET Core opted-in by setting several cookies to `Lax` by default. After encountering several [issues](https://github.com/aspnet/Announcements/issues/318) with authentication, most SameSite usage was [disabled](https://github.com/aspnet/Announcements/issues/348).

[Patches](https://devblogs.microsoft.com/dotnet/net-core-November-2019/) were issued in November 2019 to update from the 2016 standard to the 2019 standard. The [2019 draft of the SameSite specification](https://github.com/aspnet/Announcements/issues/390):

* Is **not** backwards compatible with the 2016 draft. For more information, see [Supporting older browsers](#sob) in this document.
* Specifies cookies are treated as `SameSite=Lax` by default.
* Specifies cookies that explicitly assert `SameSite=None` in order to enable cross-site delivery should be marked as `Secure`. `None` is a new entry to opt out.
* Is supported by patches issued for ASP.NET Core 2.1, 2.2, and 3.0. ASP.NET Core 3.1 or later has additional SameSite support.
* Is scheduled to be enabled by [Chrome](https://chromestatus.com/feature/5088147346030592) by default in [Feb 2020](https://blog.chromium.org/2019/10/developers-get-ready-for-new.html). Browsers started moving to this standard in 2019.

## APIs impacted by the change from the 2016 SameSite draft standard to the 2019 draft standard

* [Http.SameSiteMode](/en-us/dotnet/api/microsoft.aspnetcore.http.samesitemode)
* [CookieOptions.SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookieoptions.samesite#microsoft-aspnetcore-http-cookieoptions-samesite)
* [CookieBuilder.SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.samesite#microsoft-aspnetcore-http-cookiebuilder-samesite)
* [CookiePolicyOptions.MinimumSameSitePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.minimumsamesitepolicy#microsoft-aspnetcore-builder-cookiepolicyoptions-minimumsamesitepolicy)
* [Microsoft.Net.Http.Headers.SameSiteMode](/en-us/dotnet/api/microsoft.net.http.headers.samesitemode)
* [Microsoft.Net.Http.Headers.SetCookieHeaderValue.SameSite](/en-us/dotnet/api/microsoft.net.http.headers.setcookieheadervalue.samesite#microsoft-net-http-headers-setcookieheadervalue-samesite)

[]()

## Supporting older browsers

The 2016 SameSite standard mandated that unknown values must be treated as `SameSite=Strict` values. Apps accessed from older browsers which support the 2016 SameSite standard may break when they get a SameSite property with a value of `None`. Web apps must implement browser detection if they intend to support older browsers. ASP.NET Core doesn't implement browser detection because User-Agents values are highly volatile and change frequently. An extension point in [Microsoft.AspNetCore.CookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.cookiepolicy) allows plugging in User-Agent specific logic.

In `Program.cs`, add code that calls [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy) before calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) or *any* method that writes cookies:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
    options.OnAppendCookie = cookieContext =>
        CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    options.OnDeleteCookie = cookieContext =>
        CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});

void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
    if (options.SameSite == SameSiteMode.None)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        if (MyUserAgentDetectionLib.DisallowsSameSiteNone(userAgent))
        {
            options.SameSite = SameSiteMode.Unspecified;
        }
    }
}

    builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseCookiePolicy();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

In `Program.cs`, add code similar to the following highlighted code:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
    options.OnAppendCookie = cookieContext =>
        CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    options.OnDeleteCookie = cookieContext =>
        CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});

void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
    if (options.SameSite == SameSiteMode.None)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        if (MyUserAgentDetectionLib.DisallowsSameSiteNone(userAgent))
        {
            options.SameSite = SameSiteMode.Unspecified;
        }
    }
}

    builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseCookiePolicy();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

In the preceding sample, `MyUserAgentDetectionLib.DisallowsSameSiteNone` is a user supplied library that detects if the user agent doesn't support SameSite `None`:

```
if (MyUserAgentDetectionLib.DisallowsSameSiteNone(userAgent))
{
    options.SameSite = SameSiteMode.Unspecified;
}
```

The following code shows a sample `DisallowsSameSiteNone` method:

Warning

The following code is for demonstration only:

* It should not be considered complete.
* It is not maintained or supported.

```
public static bool DisallowsSameSiteNone(string userAgent)
{
    // Check if a null or empty string has been passed in, since this
    // will cause further interrogation of the useragent to fail.
     if (String.IsNullOrWhiteSpace(userAgent))
        return false;
    
    // Cover all iOS based browsers here. This includes:
    // - Safari on iOS 12 for iPhone, iPod Touch, iPad
    // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
    // - Chrome on iOS 12 for iPhone, iPod Touch, iPad
    // All of which are broken by SameSite=None, because they use the iOS networking
    // stack.
    if (userAgent.Contains("CPU iPhone OS 12") ||
        userAgent.Contains("iPad; CPU OS 12"))
    {
        return true;
    }

    // Cover Mac OS X based browsers that use the Mac OS networking stack. 
    // This includes:
    // - Safari on Mac OS X.
    // This does not include:
    // - Chrome on Mac OS X
    // Because they do not use the Mac OS networking stack.
    if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
        userAgent.Contains("Version/") && userAgent.Contains("Safari"))
    {
        return true;
    }

    // Cover Chrome 50-69, because some versions are broken by SameSite=None, 
    // and none in this range require it.
    // Note: this covers some pre-Chromium Edge versions, 
    // but pre-Chromium Edge does not require SameSite=None.
    if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
    {
        return true;
    }

    return false;
}
```

## Test apps for SameSite problems

Apps that interact with remote sites such as through third-party login need to:

* Test the interaction on multiple browsers.
* Apply the [CookiePolicy browser detection and mitigation](#sob) discussed in this document.

Test web apps using a client version that can opt-in to the new SameSite behavior. Chrome, Firefox, and Chromium Edge all have new opt-in feature flags that can be used for testing. After your app applies the SameSite patches, test it with older client versions, especially Safari. For more information, see [Supporting older browsers](#sob) in this document.

### Test with Chrome

Chrome 78+ gives misleading results because it has a temporary mitigation in place. The Chrome 78+ temporary mitigation allows cookies less than two minutes old. Chrome 76 or 77 with the appropriate test flags enabled provides more accurate results. To test the new SameSite behavior toggle `chrome://flags/#same-site-by-default-cookies` to **Enabled**. Older versions of Chrome (75 and below) are reported to fail with the new `None` setting. See [Supporting older browsers](#sob) in this document.

Google does not make older chrome versions available. Follow the instructions at [Download Chromium](https://www.chromium.org/getting-involved/download-chromium) to test older versions of Chrome. Do **not** download Chrome from links provided by searching for older versions of chrome.

* [Chromium 76 Win64](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/664998/)
* [Chromium 74 Win64](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/638880/)

Starting in Canary version `80.0.3975.0`, the Lax+POST temporary mitigation can be disabled for testing purposes using the new flag `--enable-features=SameSiteDefaultChecksMethodRigorously` to allow testing of sites and services in the eventual end state of the feature where the mitigation has been removed. For more information, see The Chromium Projects [SameSite Updates](https://www.chromium.org/updates/same-site)

### Test with Safari

Safari 12 strictly implemented the prior draft and fails when the new `None` value is in a cookie. `None` is avoided via the browser detection code [Supporting older browsers](#sob) in this document. Test Safari 12, Safari 13, and WebKit based OS style logins using MSAL, ADAL or whatever library you are using. The problem is dependent on the underlying OS version. OSX Mojave (10.14) and iOS 12 are known to have compatibility problems with the new SameSite behavior. Upgrading the OS to OSX Catalina (10.15) or iOS 13 fixes the problem. Safari does not currently have an opt-in flag for testing the new spec behavior.

### Test with Firefox

Firefox support for the new standard can be tested on version 68+ by opting in on the `about:config` page with the feature flag `network.cookie.sameSite.laxByDefault`. There haven't been reports of compatibility issues with older versions of Firefox.

### Test with Edge browser

Edge supports the old SameSite standard. Edge version 44 doesn't have any known compatibility problems with the new standard.

### Test with Edge (Chromium)

SameSite flags are set on the `edge://flags/#same-site-by-default-cookies` page. No compatibility issues were discovered with Edge Chromium.

### Test with Electron

Versions of Electron include older versions of Chromium. For example, the version of Electron used by Teams is Chromium 66, which exhibits the older behavior. You must perform your own compatibility testing with the version of Electron your product uses. See [Supporting older browsers](#sob) in the following section.

## Additional resources

* [Chromium Blog:Developers: Get Ready for New SameSite=None; Secure Cookie Settings](https://blog.chromium.org/2019/10/developers-get-ready-for-new.html)
* [SameSite cookies explained](https://web.dev/samesite-cookies-explained/)
* [November 2019 Patches](https://devblogs.microsoft.com/dotnet/net-core-November-2019/)

| Sample                                                                                              | Document                                                                                  |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [Razor Pages](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore31RazorPages) | [ASP.NET Core 3.1 Razor Pages SameSite cookie sample](samesite/rp31?view=aspnetcore-10.0) |

The following sample can be downloaded and tested:

| Sample                                                                                              | Document                                                                                  |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [Razor Pages](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore31RazorPages) | [ASP.NET Core 3.1 Razor Pages SameSite cookie sample](samesite/rp31?view=aspnetcore-10.0) |

## .NET Core support for the sameSite attribute

.NET Core 3.1 or later support the 2019 draft standard for SameSite. Developers are able to programmatically control the value of the sameSite attribute using the `HttpCookie.SameSite` property. Setting the `SameSite` property to Strict, Lax, or None results in those values being written on the network with the cookie. Setting it equal to `(SameSiteMode)(-1)` indicates that no sameSite attribute should be included on the network with the cookie

```
var cookieOptions = new CookieOptions
{
    // Set the secure flag, which Chrome's changes will require for SameSite none.
    // Note this will also require you to be running on HTTPS.
    Secure = true,

    // Set the cookie to HTTP only which is good practice unless you really do need
    // to access it client side in scripts.
    HttpOnly = true,

    // Add the SameSite attribute, this will emit the attribute with a value of none.
    // To not emit the attribute at all set
    // SameSite = (SameSiteMode)(-1)
    SameSite = SameSiteMode.None
};

// Add the cookie to the response cookie collection
Response.Cookies.Append("MyCookie", "cookieValue", cookieOptions);
```

.NET Core 3.1 or later support the updated SameSite values and adds an extra enum value, `SameSiteMode.Unspecified` to the `SameSiteMode` enum. This new value indicates no sameSite should be sent with the cookie.

## API usage with SameSite

[HttpContext.Response.Cookies.Append](/en-us/dotnet/api/microsoft.aspnetcore.http.iresponsecookies.append) defaults to `Unspecified`, meaning no SameSite attribute added to the cookie and the client will use its default behavior (Lax for new browsers, None for old ones). The following code shows how to change the cookie SameSite value to `SameSiteMode.Lax`:

```
HttpContext.Response.Cookies.Append(
                     "name", "value",
                     new CookieOptions() { SameSite = SameSiteMode.Lax });
```

All ASP.NET Core components that emit cookies override the preceding defaults with settings appropriate for their scenarios. The overridden preceding default values haven't changed.

| Component                                                                                                                     | cookie                                                                                                                                                                                                                                 | Default       |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| [CookieBuilder](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder)                                                    | [SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.samesite#microsoft-aspnetcore-http-cookiebuilder-samesite)                                                                                                        | `Unspecified` |
| [Session](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.session#microsoft-aspnetcore-http-httpcontext-session)      | [SessionOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.cookie#microsoft-aspnetcore-builder-sessionoptions-cookie)                                                                                       | `Lax`         |
| [CookieTempDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.cookietempdataprovider)                      | [CookieTempDataProviderOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cookietempdataprovideroptions.cookie#microsoft-aspnetcore-mvc-cookietempdataprovideroptions-cookie)                                                  | `Lax`         |
| [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery)                                               | [AntiforgeryOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie#microsoft-aspnetcore-antiforgery-antiforgeryoptions-cookie)                                                                   | `Strict`      |
| [Cookie Authentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie)                | [CookieAuthenticationOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieauthenticationoptions.cookiename#microsoft-aspnetcore-builder-cookieauthenticationoptions-cookiename)                                        | `Lax`         |
| [AddTwitter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.twitterextensions.addtwitter)                         | [TwitterOptions.StateCookie](/en-us/dotnet/api/microsoft.aspnetcore.authentication.twitter.twitteroptions.statecookie#microsoft-aspnetcore-authentication-twitter-twitteroptions-statecookie)                                          | `Lax`         |
| [RemoteAuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationhandler-1) | [RemoteAuthenticationOptions.CorrelationCookie](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.correlationcookie#microsoft-aspnetcore-authentication-remoteauthenticationoptions-correlationcookie) | `None`        |
| [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect)       | [OpenIdConnectOptions.NonceCookie](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.noncecookie#microsoft-aspnetcore-authentication-openidconnect-openidconnectoptions-noncecookie)            | `None`        |
| [HttpContext.Response.Cookies.Append](/en-us/dotnet/api/microsoft.aspnetcore.http.iresponsecookies.append)                    | [CookieOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.cookieoptions)                                                                                                                                                             | `Unspecified` |

ASP.NET Core 3.1 or later provides the following SameSite support:

* Redefines the behavior of `SameSiteMode.None` to emit `SameSite=None`
* Adds a new value `SameSiteMode.Unspecified` to omit the SameSite attribute.
* All cookies APIs default to `Unspecified`. Some components that use cookies set values more specific to their scenarios. See the table above for examples.

In ASP.NET Core 3.0 or later the SameSite defaults were changed to avoid conflicting with inconsistent client defaults. The following APIs have changed the default from `SameSiteMode.Lax `to `-1` to avoid emitting a SameSite attribute for these cookies:

* [CookieOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.cookieoptions) used with [HttpContext.Response.Cookies.Append](/en-us/dotnet/api/microsoft.aspnetcore.http.iresponsecookies.append)
* [CookieBuilder](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder) used as a factory for `CookieOptions`
* [CookiePolicyOptions.MinimumSameSitePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.minimumsamesitepolicy#microsoft-aspnetcore-builder-cookiepolicyoptions-minimumsamesitepolicy)

## History and changes

SameSite support was first implemented in ASP.NET Core in 2.0 using the [2016 draft standard](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1). The 2016 standard was opt-in. ASP.NET Core opted-in by setting several cookies to `Lax` by default. After encountering several [issues](https://github.com/aspnet/Announcements/issues/318) with authentication, most SameSite usage was [disabled](https://github.com/aspnet/Announcements/issues/348).

[Patches](https://devblogs.microsoft.com/dotnet/net-core-November-2019/) were issued in November 2019 to update from the 2016 standard to the 2019 standard. The [2019 draft of the SameSite specification](https://github.com/aspnet/Announcements/issues/390):

* Is **not** backwards compatible with the 2016 draft. For more information, see [Supporting older browsers](#sob) in this document.
* Specifies cookies are treated as `SameSite=Lax` by default.
* Specifies cookies that explicitly assert `SameSite=None` in order to enable cross-site delivery should be marked as `Secure`. `None` is a new entry to opt out.
* Is supported by patches issued for ASP.NET Core 2.1, 2.2, and 3.0. ASP.NET Core 3.1 has additional SameSite support.
* Is scheduled to be enabled by [Chrome](https://chromestatus.com/feature/5088147346030592) by default in [Feb 2020](https://blog.chromium.org/2019/10/developers-get-ready-for-new.html). Browsers started moving to this standard in 2019.

## APIs impacted by the change from the 2016 SameSite draft standard to the 2019 draft standard

* [Http.SameSiteMode](/en-us/dotnet/api/microsoft.aspnetcore.http.samesitemode)
* [CookieOptions.SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookieoptions.samesite#microsoft-aspnetcore-http-cookieoptions-samesite)
* [CookieBuilder.SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.samesite#microsoft-aspnetcore-http-cookiebuilder-samesite)
* [CookiePolicyOptions.MinimumSameSitePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.minimumsamesitepolicy#microsoft-aspnetcore-builder-cookiepolicyoptions-minimumsamesitepolicy)
* [Microsoft.Net.Http.Headers.SameSiteMode](/en-us/dotnet/api/microsoft.net.http.headers.samesitemode)
* [Microsoft.Net.Http.Headers.SetCookieHeaderValue.SameSite](/en-us/dotnet/api/microsoft.net.http.headers.setcookieheadervalue.samesite#microsoft-net-http-headers-setcookieheadervalue-samesite)

[]()

## Supporting older browsers

The 2016 SameSite standard mandated that unknown values must be treated as `SameSite=Strict` values. Apps accessed from older browsers which support the 2016 SameSite standard may break when they get a SameSite property with a value of `None`. Web apps must implement browser detection if they intend to support older browsers. ASP.NET Core doesn't implement browser detection because User-Agents values are highly volatile and change frequently. An extension point in [Microsoft.AspNetCore.CookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.cookiepolicy) allows plugging in User-Agent specific logic.

In `Startup.Configure`, add code that calls [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy) before calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) or *any* method that writes cookies:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseCookiePolicy();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

In `Startup.ConfigureServices`, add code similar to the following:

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
        options.OnAppendCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
        options.OnDeleteCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    });

    services.AddRazorPages();
}

private void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
    if (options.SameSite == SameSiteMode.None)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        if (MyUserAgentDetectionLib.DisallowsSameSiteNone(userAgent))
        {
            options.SameSite = SameSiteMode.Unspecified;
        }
    }
}
```

In the preceding sample, `MyUserAgentDetectionLib.DisallowsSameSiteNone` is a user supplied library that detects if the user agent doesn't support SameSite `None`:

```
if (MyUserAgentDetectionLib.DisallowsSameSiteNone(userAgent))
{
    options.SameSite = SameSiteMode.Unspecified;
}
```

The following code shows a sample `DisallowsSameSiteNone` method:

Warning

The following code is for demonstration only:

* It should not be considered complete.
* It is not maintained or supported.

```
public static bool DisallowsSameSiteNone(string userAgent)
{
    // Check if a null or empty string has been passed in, since this
    // will cause further interrogation of the useragent to fail.
     if (String.IsNullOrWhiteSpace(userAgent))
        return false;
    
    // Cover all iOS based browsers here. This includes:
    // - Safari on iOS 12 for iPhone, iPod Touch, iPad
    // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
    // - Chrome on iOS 12 for iPhone, iPod Touch, iPad
    // All of which are broken by SameSite=None, because they use the iOS networking
    // stack.
    if (userAgent.Contains("CPU iPhone OS 12") ||
        userAgent.Contains("iPad; CPU OS 12"))
    {
        return true;
    }

    // Cover Mac OS X based browsers that use the Mac OS networking stack. 
    // This includes:
    // - Safari on Mac OS X.
    // This does not include:
    // - Chrome on Mac OS X
    // Because they do not use the Mac OS networking stack.
    if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
        userAgent.Contains("Version/") && userAgent.Contains("Safari"))
    {
        return true;
    }

    // Cover Chrome 50-69, because some versions are broken by SameSite=None, 
    // and none in this range require it.
    // Note: this covers some pre-Chromium Edge versions, 
    // but pre-Chromium Edge does not require SameSite=None.
    if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
    {
        return true;
    }

    return false;
}
```

## Test apps for SameSite problems

Apps that interact with remote sites such as through third-party login need to:

* Test the interaction on multiple browsers.
* Apply the [CookiePolicy browser detection and mitigation](#sob) discussed in this document.

Test web apps using a client version that can opt-in to the new SameSite behavior. Chrome, Firefox, and Chromium Edge all have new opt-in feature flags that can be used for testing. After your app applies the SameSite patches, test it with older client versions, especially Safari. For more information, see [Supporting older browsers](#sob) in this document.

### Test with Chrome

Chrome 78+ gives misleading results because it has a temporary mitigation in place. The Chrome 78+ temporary mitigation allows cookies less than two minutes old. Chrome 76 or 77 with the appropriate test flags enabled provides more accurate results. To test the new SameSite behavior toggle `chrome://flags/#same-site-by-default-cookies` to **Enabled**. Older versions of Chrome (75 and below) are reported to fail with the new `None` setting. See [Supporting older browsers](#sob) in this document.

Google does not make older chrome versions available. Follow the instructions at [Download Chromium](https://www.chromium.org/getting-involved/download-chromium) to test older versions of Chrome. Do **not** download Chrome from links provided by searching for older versions of chrome.

* [Chromium 76 Win64](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/664998/)
* [Chromium 74 Win64](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/638880/)

Starting in Canary version `80.0.3975.0`, the Lax+POST temporary mitigation can be disabled for testing purposes using the new flag `--enable-features=SameSiteDefaultChecksMethodRigorously` to allow testing of sites and services in the eventual end state of the feature where the mitigation has been removed. For more information, see The Chromium Projects [SameSite Updates](https://www.chromium.org/updates/same-site)

### Test with Safari

Safari 12 strictly implemented the prior draft and fails when the new `None` value is in a cookie. `None` is avoided via the browser detection code [Supporting older browsers](#sob) in this document. Test Safari 12, Safari 13, and WebKit based OS style logins using MSAL, ADAL or whatever library you are using. The problem is dependent on the underlying OS version. OSX Mojave (10.14) and iOS 12 are known to have compatibility problems with the new SameSite behavior. Upgrading the OS to OSX Catalina (10.15) or iOS 13 fixes the problem. Safari does not currently have an opt-in flag for testing the new spec behavior.

### Test with Firefox

Firefox support for the new standard can be tested on version 68+ by opting in on the `about:config` page with the feature flag `network.cookie.sameSite.laxByDefault`. There haven't been reports of compatibility issues with older versions of Firefox.

### Test with Edge browser

Edge supports the old SameSite standard. Edge version 44 doesn't have any known compatibility problems with the new standard.

### Test with Edge (Chromium)

SameSite flags are set on the `edge://flags/#same-site-by-default-cookies` page. No compatibility issues were discovered with Edge Chromium.

### Test with Electron

Versions of Electron include older versions of Chromium. For example, the version of Electron used by Teams is Chromium 66, which exhibits the older behavior. You must perform your own compatibility testing with the version of Electron your product uses. See [Supporting older browsers](#sob) in the following section.

## Additional resources

* [Chromium Blog:Developers: Get Ready for New SameSite=None; Secure Cookie Settings](https://blog.chromium.org/2019/10/developers-get-ready-for-new.html)
* [SameSite cookies explained](https://web.dev/samesite-cookies-explained/)
* [November 2019 Patches](https://devblogs.microsoft.com/dotnet/net-core-November-2019/)

| Sample                                                                                              | Document                                                                                  |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [Razor Pages](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore31RazorPages) | [ASP.NET Core 3.1 Razor Pages SameSite cookie sample](samesite/rp31?view=aspnetcore-10.0) |

The following samples can be downloaded and tested:

| Sample                                                                                              | Document                                                                                  |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [MVC](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore21MVC)                | [ASP.NET Core 2.1 MVC SameSite cookie sample](samesite/mvc21?view=aspnetcore-10.0)        |
| [Razor Pages](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore21RazorPages) | [ASP.NET Core 2.1 Razor Pages SameSite cookie sample](samesite/rp21?view=aspnetcore-10.0) |

## December patch behavior changes

The specific behavior change for .NET Framework and .NET Core 2.1 is how the `SameSite` property interprets the `None` value. Before the patch a value of `None` meant "Do not emit the attribute at all", after the patch it means "Emit the attribute with a value of `None`". After the patch a `SameSite` value of `(SameSiteMode)(-1)` causes the attribute not to be emitted.

The default SameSite value for forms authentication and session state cookies was changed from `None` to `Lax`.

## API usage with SameSite

[HttpContext.Response.Cookies.Append](/en-us/dotnet/api/microsoft.aspnetcore.http.iresponsecookies.append) defaults to `Unspecified`, meaning no SameSite attribute added to the cookie and the client will use its default behavior (Lax for new browsers, None for old ones). The following code shows how to change the cookie SameSite value to `SameSiteMode.Lax`:

```
HttpContext.Response.Cookies.Append(
                     "name", "value",
                     new CookieOptions() { SameSite = SameSiteMode.Lax });
```

All ASP.NET Core components that emit cookies override the preceding defaults with settings appropriate for their scenarios. The overridden preceding default values haven't changed.

| Component                                                                                                                     | cookie                                                                                                                                                                                                                                 | Default       |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| [CookieBuilder](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder)                                                    | [SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.samesite#microsoft-aspnetcore-http-cookiebuilder-samesite)                                                                                                        | `Unspecified` |
| [Session](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.session#microsoft-aspnetcore-http-httpcontext-session)      | [SessionOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.cookie#microsoft-aspnetcore-builder-sessionoptions-cookie)                                                                                       | `Lax`         |
| [CookieTempDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.cookietempdataprovider)                      | [CookieTempDataProviderOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cookietempdataprovideroptions.cookie#microsoft-aspnetcore-mvc-cookietempdataprovideroptions-cookie)                                                  | `Lax`         |
| [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery)                                               | [AntiforgeryOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie#microsoft-aspnetcore-antiforgery-antiforgeryoptions-cookie)                                                                   | `Strict`      |
| [Cookie Authentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie)                | [CookieAuthenticationOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieauthenticationoptions.cookiename#microsoft-aspnetcore-builder-cookieauthenticationoptions-cookiename)                                        | `Lax`         |
| [AddTwitter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.twitterextensions.addtwitter)                         | [TwitterOptions.StateCookie](/en-us/dotnet/api/microsoft.aspnetcore.authentication.twitter.twitteroptions.statecookie#microsoft-aspnetcore-authentication-twitter-twitteroptions-statecookie)                                          | `Lax`         |
| [RemoteAuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationhandler-1) | [RemoteAuthenticationOptions.CorrelationCookie](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.correlationcookie#microsoft-aspnetcore-authentication-remoteauthenticationoptions-correlationcookie) | `None`        |
| [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect)       | [OpenIdConnectOptions.NonceCookie](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.noncecookie#microsoft-aspnetcore-authentication-openidconnect-openidconnectoptions-noncecookie)            | `None`        |
| [HttpContext.Response.Cookies.Append](/en-us/dotnet/api/microsoft.aspnetcore.http.iresponsecookies.append)                    | [CookieOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.cookieoptions)                                                                                                                                                             | `Unspecified` |

## History and changes

SameSite support was first implemented in ASP.NET Core in 2.0 using the [2016 draft standard](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1). The 2016 standard was opt-in. ASP.NET Core opted-in by setting several cookies to `Lax` by default. After encountering several [issues](https://github.com/aspnet/Announcements/issues/318) with authentication, most SameSite usage was [disabled](https://github.com/aspnet/Announcements/issues/348).

[Patches](https://devblogs.microsoft.com/dotnet/net-core-November-2019/) were issued in November 2019 to update from the 2016 standard to the 2019 standard. The [2019 draft of the SameSite specification](https://github.com/aspnet/Announcements/issues/390):

* Is **not** backwards compatible with the 2016 draft. For more information, see [Supporting older browsers](#sob) in this document.
* Specifies cookies are treated as `SameSite=Lax` by default.
* Specifies cookies that explicitly assert `SameSite=None` in order to enable cross-site delivery should be marked as `Secure`. `None` is a new entry to opt out.
* Is supported by patches issued for ASP.NET Core 2.1, 2.2, and 3.0. ASP.NET Core 3.1 has additional SameSite support.
* Is scheduled to be enabled by [Chrome](https://chromestatus.com/feature/5088147346030592) by default in [Feb 2020](https://blog.chromium.org/2019/10/developers-get-ready-for-new.html). Browsers started moving to this standard in 2019.

## APIs impacted by the change from the 2016 SameSite draft standard to the 2019 draft standard

* [Http.SameSiteMode](/en-us/dotnet/api/microsoft.aspnetcore.http.samesitemode)
* [CookieOptions.SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookieoptions.samesite#microsoft-aspnetcore-http-cookieoptions-samesite)
* [CookieBuilder.SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.samesite#microsoft-aspnetcore-http-cookiebuilder-samesite)
* [CookiePolicyOptions.MinimumSameSitePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.minimumsamesitepolicy#microsoft-aspnetcore-builder-cookiepolicyoptions-minimumsamesitepolicy)
* [Microsoft.Net.Http.Headers.SameSiteMode](/en-us/dotnet/api/microsoft.net.http.headers.samesitemode)
* [Microsoft.Net.Http.Headers.SetCookieHeaderValue.SameSite](/en-us/dotnet/api/microsoft.net.http.headers.setcookieheadervalue.samesite#microsoft-net-http-headers-setcookieheadervalue-samesite)

[]()

## Supporting older browsers

The 2016 SameSite standard mandated that unknown values must be treated as `SameSite=Strict` values. Apps accessed from older browsers which support the 2016 SameSite standard may break when they get a SameSite property with a value of `None`. Web apps must implement browser detection if they intend to support older browsers. ASP.NET Core doesn't implement browser detection because User-Agents values are highly volatile and change frequently. An extension point in [Microsoft.AspNetCore.CookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.cookiepolicy) allows plugging in User-Agent specific logic.

In `Startup.Configure`, add code that calls [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy) before calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) or *any* method that writes cookies:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseCookiePolicy();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

In `Startup.ConfigureServices`, add code similar to the following:

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.MinimumSameSitePolicy = (SameSiteMode)(-1);
        options.OnAppendCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
        options.OnDeleteCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    });

    services.AddRazorPages();
}

private void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
    if (options.SameSite == SameSiteMode.None)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        if (MyUserAgentDetectionLib.DisallowsSameSiteNone(userAgent))
        {
            options.SameSite = (SameSiteMode)(-1);
        }

    }
}
```

In the preceding sample, `MyUserAgentDetectionLib.DisallowsSameSiteNone` is a user supplied library that detects if the user agent doesn't support SameSite `None`:

```
if (MyUserAgentDetectionLib.DisallowsSameSiteNone(userAgent))
{
    options.SameSite = SameSiteMode.Unspecified;
}
```

The following code shows a sample `DisallowsSameSiteNone` method:

Warning

The following code is for demonstration only:

* It should not be considered complete.
* It is not maintained or supported.

```
public static bool DisallowsSameSiteNone(string userAgent)
{
    // Check if a null or empty string has been passed in, since this
    // will cause further interrogation of the useragent to fail.
     if (String.IsNullOrWhiteSpace(userAgent))
        return false;
    
    // Cover all iOS based browsers here. This includes:
    // - Safari on iOS 12 for iPhone, iPod Touch, iPad
    // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
    // - Chrome on iOS 12 for iPhone, iPod Touch, iPad
    // All of which are broken by SameSite=None, because they use the iOS networking
    // stack.
    if (userAgent.Contains("CPU iPhone OS 12") ||
        userAgent.Contains("iPad; CPU OS 12"))
    {
        return true;
    }

    // Cover Mac OS X based browsers that use the Mac OS networking stack. 
    // This includes:
    // - Safari on Mac OS X.
    // This does not include:
    // - Chrome on Mac OS X
    // Because they do not use the Mac OS networking stack.
    if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
        userAgent.Contains("Version/") && userAgent.Contains("Safari"))
    {
        return true;
    }

    // Cover Chrome 50-69, because some versions are broken by SameSite=None, 
    // and none in this range require it.
    // Note: this covers some pre-Chromium Edge versions, 
    // but pre-Chromium Edge does not require SameSite=None.
    if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
    {
        return true;
    }

    return false;
}
```

## Test apps for SameSite problems

Apps that interact with remote sites such as through third-party login need to:

* Test the interaction on multiple browsers.
* Apply the [CookiePolicy browser detection and mitigation](#sob) discussed in this document.

Test web apps using a client version that can opt-in to the new SameSite behavior. Chrome, Firefox, and Chromium Edge all have new opt-in feature flags that can be used for testing. After your app applies the SameSite patches, test it with older client versions, especially Safari. For more information, see [Supporting older browsers](#sob) in this document.

### Test with Chrome

Chrome 78+ gives misleading results because it has a temporary mitigation in place. The Chrome 78+ temporary mitigation allows cookies less than two minutes old. Chrome 76 or 77 with the appropriate test flags enabled provides more accurate results. To test the new SameSite behavior toggle `chrome://flags/#same-site-by-default-cookies` to **Enabled**. Older versions of Chrome (75 and below) are reported to fail with the new `None` setting. See [Supporting older browsers](#sob) in this document.

Google does not make older chrome versions available. Follow the instructions at [Download Chromium](https://www.chromium.org/getting-involved/download-chromium) to test older versions of Chrome. Do **not** download Chrome from links provided by searching for older versions of chrome.

* [Chromium 76 Win64](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/664998/)
* [Chromium 74 Win64](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/638880/)

Starting in Canary version `80.0.3975.0`, the Lax+POST temporary mitigation can be disabled for testing purposes using the new flag `--enable-features=SameSiteDefaultChecksMethodRigorously` to allow testing of sites and services in the eventual end state of the feature where the mitigation has been removed. For more information, see The Chromium Projects [SameSite Updates](https://www.chromium.org/updates/same-site)

### Test with Safari

Safari 12 strictly implemented the prior draft and fails when the new `None` value is in a cookie. `None` is avoided via the browser detection code [Supporting older browsers](#sob) in this document. Test Safari 12, Safari 13, and WebKit based OS style logins using MSAL, ADAL or whatever library you are using. The problem is dependent on the underlying OS version. OSX Mojave (10.14) and iOS 12 are known to have compatibility problems with the new SameSite behavior. Upgrading the OS to OSX Catalina (10.15) or iOS 13 fixes the problem. Safari does not currently have an opt-in flag for testing the new spec behavior.

### Test with Firefox

Firefox support for the new standard can be tested on version 68+ by opting in on the `about:config` page with the feature flag `network.cookie.sameSite.laxByDefault`. There haven't been reports of compatibility issues with older versions of Firefox.

### Test with Edge browser

Edge supports the old SameSite standard. Edge version 44 doesn't have any known compatibility problems with the new standard.

### Test with Edge (Chromium)

SameSite flags are set on the `edge://flags/#same-site-by-default-cookies` page. No compatibility issues were discovered with Edge Chromium.

### Test with Electron

Versions of Electron include older versions of Chromium. For example, the version of Electron used by Teams is Chromium 66, which exhibits the older behavior. You must perform your own compatibility testing with the version of Electron your product uses. See [Supporting older browsers](#sob) in the following section.

## Additional resources

* [Chromium Blog:Developers: Get Ready for New SameSite=None; Secure Cookie Settings](https://blog.chromium.org/2019/10/developers-get-ready-for-new.html)
* [SameSite cookies explained](https://web.dev/samesite-cookies-explained/)
* [November 2019 Patches](https://devblogs.microsoft.com/dotnet/net-core-November-2019/)

| Sample                                                                                              | Document                                                                                  |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [MVC](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore21MVC)                | [ASP.NET Core 2.1 MVC SameSite cookie sample](samesite/mvc21?view=aspnetcore-10.0)        |
| [Razor Pages](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore21RazorPages) | [ASP.NET Core 2.1 Razor Pages SameSite cookie sample](samesite/rp21?view=aspnetcore-10.0) |

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/complex-data-model
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/complex-data-model.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Create a complex data model - ASP.NET MVC with EF Core

In the previous tutorials, you worked with a simple data model that was composed of three entities. In this tutorial, you'll add more entities and relationships and you'll customize the data model by specifying formatting, validation, and database mapping rules.

When you're finished, the entity classes will make up the completed data model that's shown in the following illustration:

In this tutorial, you:

* Customize the Data model
* Make changes to Student entity
* Create Instructor entity
* Create OfficeAssignment entity
* Modify Course entity
* Create Department entity
* Modify Enrollment entity
* Update the database context
* Seed database with test data
* Add a migration
* Change the connection string
* Update the database

## Prerequisites

* [Using EF Core migrations](migrations?view=aspnetcore-10.0)

## Customize the Data model

In this section you'll see how to customize the data model by using attributes that specify formatting, validation, and database mapping rules. Then in several of the following sections you'll create the complete School data model by adding attributes to the classes you already created and creating new classes for the remaining entity types in the model.

### The DataType attribute

For student enrollment dates, all of the web pages currently display the time along with the date, although all you care about for this field is the date. By using data annotation attributes, you can make one code change that will fix the display format in every view that shows the data. To see an example of how to do that, you'll add an attribute to the `EnrollmentDate` property in the `Student` class.

In `Models/Student.cs`, add a `using` statement for the `System.ComponentModel.DataAnnotations` namespace and add `DataType` and `DisplayFormat` attributes to the `EnrollmentDate` property, as shown in the following example:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The `DataType` attribute is used to specify a data type that's more specific than the database intrinsic type. In this case we only want to keep track of the date, not the date and time. The `DataType` Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress, and more. The `DataType` attribute can also enable the application to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`, and a date selector can be provided for `DataType.Date` in browsers that support HTML5. The `DataType` attribute emits HTML 5 `data-` (pronounced data dash) attributes that HTML 5 browsers can understand. The `DataType` attributes don't provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's CultureInfo.

The `DisplayFormat` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
```

The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied when the value is displayed in a text box for editing. (You might not want that for some fields -- for example, for currency values, you might not want the currency symbol in the text box for editing.)

You can use the `DisplayFormat` attribute by itself, but it's generally a good idea to use the `DataType` attribute also. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the following benefits that you don't get with `DisplayFormat`:

* The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate currency symbol, email links, some client-side input validation, etc.).

* By default, the browser will render data using the correct format based on your locale.

For more information, see the [\<input> tag helper documentation](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper).

Run the app, go to the Students Index page and notice that times are no longer displayed for the enrollment dates. The same will be true for any view that uses the Student model.

### The StringLength attribute

You can also specify data validation rules and validation error messages using attributes. The `StringLength` attribute sets the maximum length in the database and provides client side and server side validation for ASP.NET Core MVC. You can also specify the minimum string length in this attribute, but the minimum value has no impact on the database schema.

Suppose you want to ensure that users don't enter more than 50 characters for a name. To add this limitation, add `StringLength` attributes to the `LastName` and `FirstMidName` properties, as shown in the following example:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50)]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The `StringLength` attribute won't prevent a user from entering white space for a name. You can use the `RegularExpression` attribute to apply restrictions to the input. For example, the following code requires the first character to be upper case and the remaining characters to be alphabetical:

```
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
```

The `MaxLength` attribute provides functionality similar to the `StringLength` attribute but doesn't provide client side validation.

The database model has now changed in a way that requires a change in the database schema. You'll use migrations to update the schema without losing any data that you may have added to the database by using the application UI.

Save your changes and build the project. Then open the command window in the project folder and enter the following commands:

```
dotnet ef migrations add MaxLengthOnNames
```

```
dotnet ef database update
```

The `migrations add` command warns that data loss may occur, because the change makes the maximum length shorter for two columns. Migrations creates a file named `<timeStamp>_MaxLengthOnNames.cs`. This file contains code in the `Up` method that will update the database to match the current data model. The `database update` command ran that code.

The timestamp prefixed to the migrations file name is used by Entity Framework to order the migrations. You can create multiple migrations before running the update-database command, and then all of the migrations are applied in the order in which they were created.

Run the app, select the **Students** tab, click **Create New**, and try to enter either name longer than 50 characters. The application should prevent you from doing this.

### The Column attribute

You can also use attributes to control how your classes and properties are mapped to the database. Suppose you had used the name `FirstMidName` for the first-name field because the field might also contain a middle name. But you want the database column to be named `FirstName`, because users who will be writing ad-hoc queries against the database are accustomed to that name. To make this mapping, you can use the `Column` attribute.

The `Column` attribute specifies that when the database is created, the column of the `Student` table that maps to the `FirstMidName` property will be named `FirstName`. In other words, when your code refers to `Student.FirstMidName`, the data will come from or be updated in the `FirstName` column of the `Student` table. If you don't specify column names, they're given the same name as the property name.

In the `Student.cs` file, add a `using` statement for `System.ComponentModel.DataAnnotations.Schema` and add the column name attribute to the `FirstMidName` property, as shown in the following highlighted code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50)]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The addition of the `Column` attribute changes the model backing the `SchoolContext`, so it won't match the database.

Save your changes and build the project. Then open the command window in the project folder and enter the following commands to create another migration:

```
dotnet ef migrations add ColumnFirstName
```

```
dotnet ef database update
```

In **SQL Server Object Explorer**, open the Student table designer by double-clicking the **Student** table.

Before you applied the first two migrations, the name columns were of type nvarchar(MAX). They're now nvarchar(50) and the column name has changed from FirstMidName to FirstName.

Note

If you try to compile before you finish creating all of the entity classes in the following sections, you might get compiler errors.

## Changes to Student entity

In `Models/Student.cs`, replace the code you added earlier with the following code. The changes are highlighted.

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50)]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

### The Required attribute

The `Required` attribute makes the name properties required fields. The `Required` attribute isn't needed for non-nullable types such as value types (DateTime, int, double, float, etc.). Types that can't be null are automatically treated as required fields.

The `Required` attribute must be used with `MinimumLength` for the `MinimumLength` to be enforced.

```
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
```

### The Display attribute

The `Display` attribute specifies that the caption for the text boxes should be "First Name", "Last Name", "Full Name", and "Enrollment Date" instead of the property name in each instance (which has no space dividing the words).

### The FullName calculated property

`FullName` is a calculated property that returns a value that's created by concatenating two other properties. Therefore it has only a get accessor, and no `FullName` column will be generated in the database.

## Create Instructor entity

Create `Models/Instructor.cs`, replacing the template code with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}
```

Notice that several properties are the same in the Student and Instructor entities. In the [Implementing Inheritance](inheritance?view=aspnetcore-10.0) tutorial later in this series, you'll refactor this code to eliminate the redundancy.

You can put multiple attributes on one line, so you could also write the `HireDate` attributes as follows:

```
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
```

### The CourseAssignments and OfficeAssignment navigation properties

The `CourseAssignments` and `OfficeAssignment` properties are navigation properties.

An instructor can teach any number of courses, so `CourseAssignments` is defined as a collection.

```
public ICollection<CourseAssignment> CourseAssignments { get; set; }
```

If a navigation property can hold multiple entities, its type must be a list in which entries can be added, deleted, and updated. You can specify `ICollection<T>` or a type such as `List<T>` or `HashSet<T>`. If you specify `ICollection<T>`, EF creates a `HashSet<T>` collection by default.

The reason why these are `CourseAssignment` entities is explained below in the section about many-to-many relationships.

Contoso University business rules state that an instructor can only have at most one office, so the `OfficeAssignment` property holds a single OfficeAssignment entity (which may be null if no office is assigned).

```
public OfficeAssignment OfficeAssignment { get; set; }
```

## Create OfficeAssignment entity

Create `Models/OfficeAssignment.cs` with the following code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}
```

### The Key attribute

There's a one-to-zero-or-one relationship between the `Instructor` and the `OfficeAssignment` entities. An office assignment only exists in relation to the instructor it's assigned to, and therefore its primary key is also its foreign key to the `Instructor` entity. But the Entity Framework can't automatically recognize `InstructorID` as the primary key of this entity because its name doesn't follow the `ID` or `classnameID` naming convention. Therefore, the `Key` attribute is used to identify it as the key:

```
[Key]
public int InstructorID { get; set; }
```

You can also use the `Key` attribute if the entity does have its own primary key but you want to name the property something other than classnameID or ID.

By default, EF treats the key as non-database-generated because the column is for an identifying relationship.

### The Instructor navigation property

The Instructor entity has a nullable `OfficeAssignment` navigation property (because an instructor might not have an office assignment), and the OfficeAssignment entity has a non-nullable `Instructor` navigation property (because an office assignment can't exist without an instructor -- `InstructorID` is non-nullable). When an Instructor entity has a related OfficeAssignment entity, each entity will have a reference to the other one in its navigation property.

You could put a `[Required]` attribute on the Instructor navigation property to specify that there must be a related instructor, but you don't have to do that because the `InstructorID` foreign key (which is also the key to this table) is non-nullable.

## Modify Course entity

In `Models/Course.cs`, replace the code you added earlier with the following code. The changes are highlighted.

```
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}
```

The course entity has a foreign key property `DepartmentID` which points to the related Department entity and it has a `Department` navigation property.

The Entity Framework doesn't require you to add a foreign key property to your data model when you have a navigation property for a related entity. EF automatically creates foreign keys in the database wherever they're needed and creates [shadow properties](/en-us/ef/core/modeling/shadow-properties) for them. But having the foreign key in the data model can make updates simpler and more efficient. For example, when you fetch a `Course` entity to edit, the `Department` entity is null if you don't load it, so when you update the `Course` entity, you would have to first fetch the `Department` entity. When the foreign key property `DepartmentID` is included in the data model, you don't need to fetch the `Department` entity before you update.

### The DatabaseGenerated attribute

The `DatabaseGenerated` attribute with the `None` parameter on the `CourseID` property specifies that primary key values are provided by the user rather than generated by the database.

```
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
```

By default, Entity Framework assumes that primary key values are generated by the database. That's what you want in most scenarios. However, for `Course` entities, you'll use a user-specified course number such as a 1000 series for one department, a 2000 series for another department, and so on.

The `DatabaseGenerated` attribute can also be used to generate default values, as in the case of database columns used to record the date a row was created or updated. For more information, see [Generated Properties](/en-us/ef/core/modeling/generated-properties).

### Foreign key and navigation properties

The foreign key properties and navigation properties in the `Course` entity reflect the following relationships:

A course is assigned to one department, so there's a `DepartmentID` foreign key and a `Department` navigation property for the reasons mentioned above.

```
public int DepartmentID { get; set; }
public Department Department { get; set; }
```

A course can have any number of students enrolled in it, so the `Enrollments` navigation property is a collection:

```
public ICollection<Enrollment> Enrollments { get; set; }
```

A course may be taught by multiple instructors, so the `CourseAssignments` navigation property is a collection (the type `CourseAssignment` is explained [later](#many-to-many-relationships)):

```
public ICollection<CourseAssignment> CourseAssignments { get; set; }
```

## Create Department entity

Create `Models/Department.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}
```

### The Column attribute

Earlier you used the `Column` attribute to change column name mapping. In the code for the `Department` entity, the `Column` attribute is being used to change SQL data type mapping so that the column will be defined using the SQL Server `money` type in the database:

```
[Column(TypeName="money")]
public decimal Budget { get; set; }
```

Column mapping is generally not required, because the Entity Framework chooses the appropriate SQL Server data type based on the CLR type that you define for the property. The CLR `decimal` type maps to a SQL Server `decimal` type. But in this case you know that the column will be holding currency amounts, and the money data type is more appropriate for that.

### Foreign key and navigation properties

The foreign key and navigation properties reflect the following relationships:

A department may or may not have an administrator, and an administrator is always an instructor. Therefore the `InstructorID` property is included as the foreign key to the Instructor entity, and a question mark is added after the `int` type designation to mark the property as nullable. The navigation property is named `Administrator` but holds an Instructor entity:

```
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
```

A department may have many courses, so there's a Courses navigation property:

```
public ICollection<Course> Courses { get; set; }
```

Note

By convention, the Entity Framework enables cascade delete for non-nullable foreign keys and for many-to-many relationships. This can result in circular cascade delete rules, which will cause an exception when you try to add a migration. For example, if you didn't define the `Department.InstructorID` property as nullable, EF would configure a cascade delete rule to delete the department when you delete the instructor, which isn't what you want to have happen. If your business rules required the `InstructorID` property to be non-nullable, you would have to use the following fluent API statement to disable cascade delete on the relationship:

```
modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)
```

## Modify Enrollment entity

In `Models/Enrollment.cs`, replace the code you added earlier with the following code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}
```

### Foreign key and navigation properties

The foreign key properties and navigation properties reflect the following relationships:

An enrollment record is for a single course, so there's a `CourseID` foreign key property and a `Course` navigation property:

```
public int CourseID { get; set; }
public Course Course { get; set; }
```

An enrollment record is for a single student, so there's a `StudentID` foreign key property and a `Student` navigation property:

```
public int StudentID { get; set; }
public Student Student { get; set; }
```

## Many-to-Many relationships

There's a many-to-many relationship between the `Student` and `Course` entities, and the `Enrollment` entity functions as a many-to-many join table *with payload* in the database. "With payload" means that the `Enrollment` table contains additional data besides foreign keys for the joined tables (in this case, a primary key and a `Grade` property).

The following illustration shows what these relationships look like in an entity diagram. (This diagram was generated using the Entity Framework Power Tools for EF 6.x; creating the diagram isn't part of the tutorial, it's just being used here as an illustration.)

Each relationship line has a 1 at one end and an asterisk (\*) at the other, indicating a one-to-many relationship.

If the `Enrollment` table didn't include grade information, it would only need to contain the two foreign keys `CourseID` and `StudentID`. In that case, it would be a many-to-many join table without payload (or a pure join table) in the database. The `Instructor` and `Course` entities have that kind of many-to-many relationship, and your next step is to create an entity class to function as a join table without payload.

EF Core supports implicit join tables for many-to-many relationships, but this tutorial has not been updated to use an implicit join table. See [Many-to-Many Relationships](../ef-rp/complex-data-model?view=aspnetcore-10.0#many-to-many-relationships), the Razor Pages version of this tutorial which has been updated.

## The CourseAssignment entity

Create `Models/CourseAssignment.cs` with the following code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}
```

### Join entity names

A join table is required in the database for the Instructor-to-Courses many-to-many relationship, and it has to be represented by an entity set. It's common to name a join entity `EntityName1EntityName2`, which in this case would be `CourseInstructor`. However, we recommend that you choose a name that describes the relationship. Data models start out simple and grow, with no-payload joins frequently getting payloads later. If you start with a descriptive entity name, you won't have to change the name later. Ideally, the join entity would have its own natural (possibly single word) name in the business domain. For example, Books and Customers could be linked through Ratings. For this relationship, `CourseAssignment` is a better choice than `CourseInstructor`.

### Composite key

Since the foreign keys are not nullable and together uniquely identify each row of the table, there's no need for a separate primary key. The `InstructorID` and `CourseID` properties should function as a composite primary key. The only way to identify composite primary keys to EF is by using the *fluent API* (it can't be done by using attributes). You'll see how to configure the composite primary key in the next section.

The composite key ensures that while you can have multiple rows for one course, and multiple rows for one instructor, you can't have multiple rows for the same instructor and course. The `Enrollment` join entity defines its own primary key, so duplicates of this sort are possible. To prevent such duplicates, you could add a unique index on the foreign key fields, or configure `Enrollment` with a primary composite key similar to `CourseAssignment`. For more information, see [Indexes](/en-us/ef/core/modeling/indexes).

## Update the database context

Add the following highlighted code to the `Data/SchoolContext.cs` file:

```
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}
```

This code adds the new entities and configures the CourseAssignment entity's composite primary key.

## About a fluent API alternative

The code in the `OnModelCreating` method of the `DbContext` class uses the *fluent API* to configure EF behavior. The API is called "fluent" because it's often used by stringing a series of method calls together into a single statement, as in this example from the [EF Core documentation](/en-us/ef/core/modeling/#use-fluent-api-to-configure-a-model):

```
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}
```

In this tutorial, you're using the fluent API only for database mapping that you can't do with attributes. However, you can also use the fluent API to specify most of the formatting, validation, and mapping rules that you can do by using attributes. Some attributes such as `MinimumLength` can't be applied with the fluent API. As mentioned previously, `MinimumLength` doesn't change the schema, it only applies a client and server side validation rule.

Some developers prefer to use the fluent API exclusively so that they can keep their entity classes "clean." You can mix attributes and fluent API if you want, and there are a few customizations that can only be done by using fluent API, but in general the recommended practice is to choose one of these two approaches and use that consistently as much as possible. If you do use both, note that wherever there's a conflict, Fluent API overrides attributes.

For more information about attributes vs. fluent API, see [Methods of configuration](/en-us/ef/core/modeling/).

## Entity Diagram Showing Relationships

The following illustration shows the diagram that the Entity Framework Power Tools create for the completed School model.

Besides the one-to-many relationship lines (1 to \*), you can see here the one-to-zero-or-one relationship line (1 to 0..1) between the `Instructor` and `OfficeAssignment` entities and the zero-or-one-to-many relationship line (0..1 to \*) between the Instructor and Department entities.

## Seed database with test data

Replace the code in the `Data/DbInitializer.cs` file with the following code in order to provide seed data for the new entities you've created.

```
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            foreach (Instructor i in instructors)
            {
                context.Instructors.Add(i);
            }
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            foreach (Department d in departments)
            {
                context.Departments.Add(d);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            foreach (OfficeAssignment o in officeAssignments)
            {
                context.OfficeAssignments.Add(o);
            }
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            foreach (CourseAssignment ci in courseInstructors)
            {
                context.CourseAssignments.Add(ci);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}
```

As you saw in the first tutorial, most of this code simply creates new entity objects and loads sample data into properties as required for testing. Notice how the many-to-many relationships are handled: the code creates relationships by creating entities in the `Enrollments` and `CourseAssignment` join entity sets.

## Add a migration

Save your changes and build the project. Then open the command window in the project folder and enter the `migrations add` command (don't do the update-database command yet):

```
dotnet ef migrations add ComplexDataModel
```

You get a warning about possible data loss.

```
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'
```

If you tried to run the `database update` command at this point (don't do it yet), you would get the following error:

> The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK\_dbo.Course\_dbo.Department\_DepartmentID". The conflict occurred in database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

Sometimes when you execute migrations with existing data, you need to insert stub data into the database to satisfy foreign key constraints. The generated code in the `Up` method adds a non-nullable `DepartmentID` foreign key to the `Course` table. If there are already rows in the Course table when the code runs, the `AddColumn` operation fails because SQL Server doesn't know what value to put in the column that can't be null. For this tutorial you'll run the migration on a new database, but in a production application you'd have to make the migration handle existing data, so the following directions show an example of how to do that.

To make this migration work with existing data you have to change the code to give the new column a default value, and create a stub department named "Temp" to act as the default department. As a result, existing Course rows will all be related to the "Temp" department after the `Up` method runs.

* Open the `{timestamp}_ComplexDataModel.cs` file.

* Comment out the line of code that adds the DepartmentID column to the Course table.

  ```
  migrationBuilder.AlterColumn<string>(
      name: "Title",
      table: "Course",
      maxLength: 50,
      nullable: true,
      oldClrType: typeof(string),
      oldNullable: true);
              
  //migrationBuilder.AddColumn<int>(
  //    name: "DepartmentID",
  //    table: "Course",
  //    nullable: false,
  //    defaultValue: 0);
  ```

* Add the following highlighted code after the code that creates the Department table:

  ```
  migrationBuilder.CreateTable(
      name: "Department",
      columns: table => new
      {
          DepartmentID = table.Column<int>(nullable: false)
              .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
          Budget = table.Column<decimal>(type: "money", nullable: false),
          InstructorID = table.Column<int>(nullable: true),
          Name = table.Column<string>(maxLength: 50, nullable: true),
          StartDate = table.Column<DateTime>(nullable: false)
      },
      constraints: table =>
      {
          table.PrimaryKey("PK_Department", x => x.DepartmentID);
          table.ForeignKey(
              name: "FK_Department_Instructor_InstructorID",
              column: x => x.InstructorID,
              principalTable: "Instructor",
              principalColumn: "ID",
              onDelete: ReferentialAction.Restrict);
      });

  migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
  // Default value for FK points to department created above, with
  // defaultValue changed to 1 in following AddColumn statement.

  migrationBuilder.AddColumn<int>(
      name: "DepartmentID",
      table: "Course",
      nullable: false,
      defaultValue: 1);
  ```

In a production application, you would write code or scripts to add Department rows and relate Course rows to the new Department rows. You would then no longer need the "Temp" department or the default value on the `Course.DepartmentID` column.

Save your changes and build the project.

## Change the connection string

You now have new code in the `DbInitializer` class that adds seed data for the new entities to an empty database. To make EF create a new empty database, change the name of the database in the connection string in `appsettings.json` to ContosoUniversity3 or some other name that you haven't used on the computer you're using.

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
```

Save your change to `appsettings.json`.

Note

As an alternative to changing the database name, you can delete the database. Use **SQL Server Object Explorer** (SSOX) or the `database drop` CLI command:

```
dotnet ef database drop
```

## Update the database

After you have changed the database name or deleted the database, run the `database update` command in the command window to execute the migrations.

```
dotnet ef database update
```

Run the app to cause the `DbInitializer.Initialize` method to run and populate the new database.

Open the database in SSOX as you did earlier, and expand the **Tables** node to see that all of the tables have been created. (If you still have SSOX open from the earlier time, click the **Refresh** button.)

Run the app to trigger the initializer code that seeds the database.

Right-click the **CourseAssignment** table and select **View Data** to verify that it has data in it.

## Get the code

[Download or view the completed application.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final)

## Next steps

In this tutorial, you:

* Customized the Data model
* Made changes to Student entity
* Created Instructor entity
* Created OfficeAssignment entity
* Modified Course entity
* Created Department entity
* Modified Enrollment entity
* Updated the database context
* Seeded database with test data
* Added a migration
* Changed the connection string
* Updated the database

Advance to the next tutorial to learn more about how to access related data.

[Next: Access related data](read-related-data?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/intro.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tag Helpers in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

## What are Tag Helpers

Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files. For example, the built-in `ImageTagHelper` can append a version number to the image name. Whenever the image changes, the server generates a new unique version for the image, so clients are guaranteed to get the current image (instead of a stale cached image). There are many built-in Tag Helpers for common tasks - such as creating forms, links, loading assets and more - and even more available in public GitHub repositories and as NuGet packages. Tag Helpers are authored in C#, and they target HTML elements based on element name, attribute name, or parent tag. For example, the built-in `LabelTagHelper` can target the HTML `<label>` element when the `LabelTagHelper` attributes are applied. If you're familiar with HTML Helpers, Tag Helpers reduce the explicit transitions between HTML and C# in Razor views. In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper, but it's important to recognize that Tag Helpers don't replace HTML Helpers. There's not a Tag Helper for each HTML Helper. [Tag Helpers compared to HTML Helpers](#tag-helpers-compared-to-html-helpers) explains the differences in more detail.

Tag Helpers aren't supported in Razor components. For more information, see [ASP.NET Core Razor components](../../../blazor/components/?view=aspnetcore-10.0#tag-helpers-arent-supported-in-components).

## What Tag Helpers provide

### An HTML-friendly development experience

Usually, Razor markup using Tag Helpers looks like standard HTML. Front-end designers conversant with HTML/CSS/JavaScript can edit Razor without learning C# Razor syntax.

### A rich IntelliSense environment for creating HTML and Razor markup

This is in sharp contrast to HTML Helpers, the previous approach to server-side creation of markup in Razor views. [Tag Helpers compared to HTML Helpers](#tag-helpers-compared-to-html-helpers) explains the differences in more detail. [IntelliSense support for Tag Helpers](#intellisense-support-for-tag-helpers) explains the IntelliSense environment. Even developers experienced with Razor C# syntax are more productive using Tag Helpers than writing C# Razor markup.

### A way to make you more productive and able to produce more robust, reliable, and maintainable code using information only available on the server

For example, historically the mantra on updating images was to change the name of the image when you change the image. Images should be aggressively cached for performance reasons, and unless you change the name of an image, you risk clients getting a stale copy. Historically, after an image was edited, the name had to be changed and each reference to the image in the web app needed to be updated. Not only is this very labor intensive, it's also error prone (you could miss a reference, accidentally enter the wrong string, etc.) The built-in `ImageTagHelper` can do this for you automatically. The `ImageTagHelper` can append a version number to the image name, so whenever the image changes, the server automatically generates a new unique version for the image. Clients are guaranteed to get the current image. This robustness and labor savings comes essentially free by using the `ImageTagHelper`.

Most built-in Tag Helpers target standard HTML elements and provide server-side attributes for the element. For example, the `<input>` element used in many views in the *Views/Account* folder contains the `asp-for` attribute. This attribute extracts the name of the specified model property into the rendered HTML. Consider a Razor view with the following model:

```
public class Movie
{
    public int ID { get; set; }
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; }
    public decimal Price { get; set; }
}
```

The following Razor markup:

```
<label asp-for="Movie.Title"></label>
```

Generates the following HTML:

```
<label for="Movie_Title">Title</label>
```

The `asp-for` attribute is made available by the `For` property in the [LabelTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.labeltaghelper). See [Author Tag Helpers](authoring?view=aspnetcore-10.0) for more information.

## Managing Tag Helper scope

Tag Helpers scope is controlled by a combination of `@addTagHelper`, `@removeTagHelper`, and the "!" opt-out character.

[]()

### `@addTagHelper` makes Tag Helpers available

If you create a new ASP.NET Core web app named *AuthoringTagHelpers*, the following `Views/_ViewImports.cshtml` file will be added to your project:

```
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers
```

The `@addTagHelper` directive makes Tag Helpers available to the view. In this case, the view file is `Pages/_ViewImports.cshtml`, which by default is inherited by all files in the *Pages* folder and subfolders; making Tag Helpers available. The preceding code uses the wildcard syntax ("\*") to specify that all Tag Helpers in the specified assembly (*Microsoft.AspNetCore.Mvc.TagHelpers*) will be available to every view file in the *Views* directory or subdirectory. The first parameter after `@addTagHelper` specifies the Tag Helpers to load (we're using "\*" for all Tag Helpers), and the second parameter "Microsoft.AspNetCore.Mvc.TagHelpers" specifies the assembly containing the Tag Helpers. *Microsoft.AspNetCore.Mvc.TagHelpers* is the assembly for the built-in ASP.NET Core Tag Helpers.

To expose all of the Tag Helpers in this project (which creates an assembly named *AuthoringTagHelpers*), you would use the following:

```
@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AuthoringTagHelpers
```

If your project contains an `EmailTagHelper` with the default namespace (`AuthoringTagHelpers.TagHelpers.EmailTagHelper`), you can provide the fully qualified name (FQN) of the Tag Helper:

```
@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper, AuthoringTagHelpers
```

To add a Tag Helper to a view using an FQN, you first add the FQN (`AuthoringTagHelpers.TagHelpers.EmailTagHelper`), and then the assembly name (*AuthoringTagHelpers*). Most developers prefer to use the "\*" wildcard syntax. The wildcard syntax allows you to insert the wildcard character "\*" as the suffix in an FQN. For example, any of the following directives will bring in the `EmailTagHelper`:

```
@addTagHelper AuthoringTagHelpers.TagHelpers.E*, AuthoringTagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.Email*, AuthoringTagHelpers
```

As mentioned previously, adding the `@addTagHelper` directive to the `Views/_ViewImports.cshtml` file makes the Tag Helper available to all view files in the *Views* directory and subdirectories. You can use the `@addTagHelper` directive in specific view files if you want to opt-in to exposing the Tag Helper to only those views.

[]()

### `@removeTagHelper` removes Tag Helpers

The `@removeTagHelper` has the same two parameters as `@addTagHelper`, and it removes a Tag Helper that was previously added. For example, `@removeTagHelper` applied to a specific view removes the specified Tag Helper from the view. Using `@removeTagHelper` in a `Views/Folder/_ViewImports.cshtml` file removes the specified Tag Helper from all of the views in *Folder*.

### Controlling Tag Helper scope with the `_ViewImports.cshtml` file

You can add a `_ViewImports.cshtml` to any view folder, and the view engine applies the directives from both that file and the `Views/_ViewImports.cshtml` file. If you added an empty `Views/Home/_ViewImports.cshtml` file for the *Home* views, there would be no change because the `_ViewImports.cshtml` file is additive. Any `@addTagHelper` directives you add to the `Views/Home/_ViewImports.cshtml` file (that aren't in the default `Views/_ViewImports.cshtml` file) would expose those Tag Helpers to views only in the *Home* folder.

[]()

### Opting out of individual elements

You can disable a Tag Helper at the element level with the Tag Helper opt-out character ("!"). For example, `Email` validation is disabled in the `<span>` with the Tag Helper opt-out character:

```
<!span asp-validation-for="Email" class="text-danger"></!span>
```

You must apply the Tag Helper opt-out character to the opening and closing tag. (The Visual Studio editor automatically adds the opt-out character to the closing tag when you add one to the opening tag). After you add the opt-out character, the element and Tag Helper attributes are no longer displayed in a distinctive font.

[]()

### Using `@tagHelperPrefix` to make Tag Helper usage explicit

The `@tagHelperPrefix` directive allows you to specify a tag prefix string to enable Tag Helper support and to make Tag Helper usage explicit. For example, you could add the following markup to the `Views/_ViewImports.cshtml` file:

```
@tagHelperPrefix th:
```

In the code image that follows, the Tag Helper prefix is set to `th:`, so only those elements using the prefix `th:` support Tag Helpers (Tag Helper-enabled elements have a distinctive font). The `<label>` and `<input>` elements have the Tag Helper prefix and are Tag Helper-enabled, while the `<span>` element doesn't.

The same hierarchy rules that apply to `@addTagHelper` also apply to `@tagHelperPrefix`.

## Self-closing Tag Helpers

Many Tag Helpers can't be used as self-closing tags. Some Tag Helpers are designed to be self-closing tags. Using a Tag Helper that wasn't designed to be self-closing suppresses the rendered output. Self-closing a Tag Helper results in a self-closing tag in the rendered output. For more information, see [this note](authoring?view=aspnetcore-10.0#self-closing) in [Authoring Tag Helpers](authoring?view=aspnetcore-10.0).

## C# in Tag Helpers attribute/declaration

Tag Helpers don't allow C# in the element's attribute or tag declaration area. For example, the following code isn't valid:

```
<input asp-for="LastName"  
       @(Model?.LicenseId == null ? "disabled" : string.Empty) />
```

The preceding code can be written as:

```
<input asp-for="LastName" 
       disabled="@(Model?.LicenseId == null)" />
```

Normally, the `@` operator inserts a textual representation of an expression into the rendered HTML markup. However, when an expression evaluates to logical `false`, the framework removes the attribute instead. In the preceding example, the `disabled` attribute is set to `true` if either `Model` or `LicenseId` is `null`.

## Tag helper initializers

While attributes can be used to configure individual instances of tag helpers, [ITagHelperInitializer\<TTagHelper>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.itaghelperinitializer-1) can be used to configure all tag helper instances of a specific kind. Consider the following example of a tag helper initializer that configures the `asp-append-version` attribute or `AppendVersion` property for all instances of `ScriptTagHelper` in the app:

```
public class AppendVersionTagHelperInitializer : ITagHelperInitializer<ScriptTagHelper>
{
    public void Initialize(ScriptTagHelper helper, ViewContext context)
    {
        helper.AppendVersion = true;
    }
}
```

To use the initializer, configure it by registering it as part of the application's startup:

```
builder.Services.AddSingleton
    <ITagHelperInitializer<ScriptTagHelper>, AppendVersionTagHelperInitializer>();
```

## Tag Helper automatic version generation outside of wwwroot

For a Tag Helper to generate a version for a static file outside `wwwroot`, see [Serve files from multiple locations](../../../fundamentals/static-files?view=aspnetcore-10.0#serve-files-from-multiple-locations)

## IntelliSense support for Tag Helpers

Consider writing an HTML `<label>` element. As soon as you enter `<l` in the Visual Studio editor, IntelliSense displays matching elements:

Not only do you get HTML help, but also the icon (the "@" symbol with "<>" under it).

The icon identifies the element as targeted by Tag Helpers. Pure HTML elements (such as the `fieldset`) display the "<>" icon.

A pure HTML `<label>` tag displays the HTML tag (with the default Visual Studio color theme) in a brown font, the attributes in red, and the attribute values in blue.

After you enter `<label`, IntelliSense lists the available HTML/CSS attributes and the Tag Helper-targeted attributes:

IntelliSense statement completion allows you to enter the tab key to complete the statement with the selected value:

As soon as a Tag Helper attribute is entered, the tag and attribute fonts change. Using the default Visual Studio "Blue" or "Light" color theme, the font is bold purple. If you're using the "Dark" theme, the font is bold teal. The images in this document were taken using the default theme.

You can enter the Visual Studio *CompleteWord* shortcut (Ctrl +spacebar is the [default](/en-us/visualstudio/ide/default-keyboard-shortcuts-in-visual-studio)) inside the double quotes (""), and you're now in C#, just like you would be in a C# class. IntelliSense displays all the methods and properties on the page model. The methods and properties are available because the property type is `ModelExpression`. In the following image, I'm editing the `Register` view, so the `RegisterViewModel` is available.

IntelliSense lists the properties and methods available to the model on the page. The rich IntelliSense environment helps you select the CSS class:

## Tag Helpers compared to HTML Helpers

Tag Helpers attach to HTML elements in Razor views, while HTML Helpers are invoked as methods interspersed with HTML in Razor views. Consider the following Razor markup, which creates an HTML label with the CSS class "caption":

```
@Html.Label("FirstName", "First Name:", new {@class="caption"})
```

The at (`@`) symbol tells Razor this is the start of code. The next two parameters ("FirstName" and "First Name:") are strings, so [IntelliSense](/en-us/visualstudio/ide/using-intellisense) can't help. The last argument:

```
new {@class="caption"}
```

Is an anonymous object used to represent attributes. Because `class` is a reserved keyword in C#, you use the `@` symbol to force C# to interpret `@class=` as a symbol (property name). To a front-end designer (someone familiar with HTML/CSS/JavaScript and other client technologies but not familiar with C# and Razor), most of the line is foreign. The entire line must be authored with no help from IntelliSense.

Using the `LabelTagHelper`, the same markup can be written as:

```
<label class="caption" asp-for="FirstName"></label>
```

With the Tag Helper version, as soon as you enter `<l` in the Visual Studio editor, IntelliSense displays matching elements:

IntelliSense helps you write the entire line.

The following code image shows the Form portion of the `Views/Account/Register.cshtml` Razor view generated from the ASP.NET 4.5.x MVC template included with Visual Studio.

The Visual Studio editor displays C# code with a grey background. For example, the `AntiForgeryToken` HTML Helper:

```
@Html.AntiForgeryToken()
```

is displayed with a grey background. Most of the markup in the Register view is C#. Compare that to the equivalent approach using Tag Helpers:

The markup is much cleaner and easier to read, edit, and maintain than the HTML Helpers approach. The C# code is reduced to the minimum that the server needs to know about. The Visual Studio editor displays markup targeted by a Tag Helper in a distinctive font.

Consider the *Email* group:

```
<div class="form-group">
    <label asp-for="Email" class="col-md-2 control-label"></label>
    <div class="col-md-10">
        <input asp-for="Email" class="form-control" />
        <span asp-validation-for="Email" class="text-danger"></span>
    </div>
</div>
```

Each of the "asp-" attributes has a value of "Email", but "Email" isn't a string. In this context, "Email" is the C# model expression property for the `RegisterViewModel`.

The Visual Studio editor helps you write **all** of the markup in the Tag Helper approach of the register form, while Visual Studio provides no help for most of the code in the HTML Helpers approach. [IntelliSense support for Tag Helpers](#intellisense-support-for-tag-helpers) goes into detail on working with Tag Helpers in the Visual Studio editor.

## Tag Helpers compared to Web Server Controls

* Tag Helpers don't own the element they're associated with; they participate in the rendering of the element and content. ASP.NET Web Server Controls are declared and invoked on a page.

* ASP.NET Web Server Controls have a non-trivial lifecycle that can make developing and debugging difficult.

* Web Server controls allow you to add functionality to the client DOM elements by using a client control. Tag Helpers have no DOM.

* Web Server controls include automatic browser detection. Tag Helpers have no knowledge of the browser.

* Multiple Tag Helpers can act on the same element (see [Avoiding Tag Helper conflicts](authoring?view=aspnetcore-10.0#avoid-tag-helper-conflicts)) while you typically can't compose Web Server controls.

* Tag Helpers can modify the tag and content of HTML elements that they're scoped to, but don't directly modify anything else on a page. Web Server controls have a less specific scope and can perform actions that affect other parts of your page; enabling unintended side effects.

* Web Server controls use type converters to convert strings into objects. With Tag Helpers, you work natively in C#, so you don't need to do type conversion.

* Web Server controls use [System.ComponentModel](/en-us/dotnet/api/system.componentmodel) to implement the run-time and design-time behavior of components and controls. `System.ComponentModel` includes the base classes and interfaces for implementing attributes and type converters, binding to data sources, and licensing components. Contrast that to Tag Helpers, which typically derive from `TagHelper`, and the `TagHelper` base class exposes only two methods, `Process` and `ProcessAsync`.

## Customizing the Tag Helper element font

You can customize the font and colorization from **Tools** > **Options** > **Environment** > **Fonts and Colors**:

## Built-in ASP.NET Core Tag Helpers

**[Anchor](built-in/anchor-tag-helper?view=aspnetcore-10.0)**

**[Cache](built-in/cache-tag-helper?view=aspnetcore-10.0)**

**[Component](built-in/component-tag-helper?view=aspnetcore-10.0)**

**[Distributed Cache](built-in/distributed-cache-tag-helper?view=aspnetcore-10.0)**

**[Environment](built-in/environment-tag-helper?view=aspnetcore-10.0)**

**[Form](../working-with-forms?view=aspnetcore-10.0#the-form-tag-helper)**

**[Form Action](../working-with-forms?view=aspnetcore-10.0#the-form-action-tag-helper)**

**[Image](built-in/image-tag-helper?view=aspnetcore-10.0)**

**[Input](../working-with-forms?view=aspnetcore-10.0#the-input-tag-helper)**

**[Label](../working-with-forms?view=aspnetcore-10.0#the-label-tag-helper)**

**[Link](built-in/link-tag-helper?view=aspnetcore-10.0)**

**[Partial](built-in/partial-tag-helper?view=aspnetcore-10.0)**

**[Persist Component State](built-in/persist-component-state?view=aspnetcore-10.0)**

**[Script](built-in/script-tag-helper?view=aspnetcore-10.0)**

**[Select](../working-with-forms?view=aspnetcore-10.0#the-select-tag-helper)**

**[Textarea](../working-with-forms?view=aspnetcore-10.0#the-textarea-tag-helper)**

**[Validation Message](../working-with-forms?view=aspnetcore-10.0#the-validation-message-tag-helper)**

**[Validation Summary](../working-with-forms?view=aspnetcore-10.0#the-validation-summary-tag-helper)**

## Related content

* [Author Tag Helpers](authoring?view=aspnetcore-10.0)
* [Working with Forms](../working-with-forms?view=aspnetcore-10.0)
* [TagHelperSamples on GitHub](https://github.com/dpaquette/TagHelperSamples) contains Tag Helper samples for working with [Bootstrap](https://getbootstrap.com/).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/app-state.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Session and state management in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Kirk Larkin](https://twitter.com/serpent5), and [Diana LaRose](https://github.com/DianaLaRose)

HTTP is a stateless protocol. By default, HTTP requests are independent messages that don't retain user values. This article describes several approaches to preserve user data between requests.

For Blazor state management guidance, which adds to or supersedes the guidance in this article, see [ASP.NET Core Blazor state management overview](../blazor/state-management/?view=aspnetcore-10.0).

## State management

State can be stored using several approaches. Each approach is described later in this article.

| Storage approach                       | Storage mechanism                                                 |
| -------------------------------------- | ----------------------------------------------------------------- |
| [Cookies](#cookies)                    | HTTP cookies. May include data stored using server-side app code. |
| [Session state](#session-state)        | HTTP cookies and server-side app code                             |
| [TempData](#tempdata)                  | HTTP cookies or session state                                     |
| [Query strings](#query-strings)        | HTTP query strings                                                |
| [Hidden fields](#hidden-fields)        | HTTP form fields                                                  |
| [HttpContext.Items](#httpcontextitems) | Server-side app code                                              |
| [Cache](#cache)                        | Server-side app code                                              |

## SignalR/Blazor Server and HTTP context-based state management

[SignalR](../signalr/introduction?view=aspnetcore-10.0) apps shouldn't use session state and other state management approaches that rely upon a stable HTTP context to store information. SignalR apps can store per-connection state in [`Context.Items` in the hub](../signalr/hubs?view=aspnetcore-10.0). For more information and alternative state management approaches for Blazor Server apps, see [ASP.NET Core Blazor state management overview](../blazor/state-management/?view=aspnetcore-10.0) and [ASP.NET Core Blazor server-side state management](../blazor/state-management/server?view=aspnetcore-10.0).

## Cookies

Cookies store data across requests. Because cookies are sent with every request, their size should be kept to a minimum. Ideally, only an identifier should be stored in a cookie with the data stored by the app. Most browsers restrict cookie size to 4096 bytes. Only a limited number of cookies are available for each domain.

Because cookies are subject to tampering, they must be validated by the app. Cookies can be deleted by users and expire on clients. However, cookies are generally the most durable form of data persistence on the client.

Cookies are often used for personalization, where content is customized for a known user. The user is only identified and not authenticated in most cases. The cookie can store the user's name, account name, or unique user ID such as a GUID. The cookie can be used to access the user's personalized settings, such as their preferred website background color.

See the [European Union General Data Protection Regulations (GDPR)](https://ec.europa.eu/info/law/law-topic/data-protection) when issuing cookies and dealing with privacy concerns. For more information, see [General Data Protection Regulation (GDPR) support in ASP.NET Core](../security/gdpr?view=aspnetcore-10.0).

## Session state

Session state is an ASP.NET Core scenario for storage of user data while the user browses a web app. Session state uses a store maintained by the app to persist data across requests from a client. The session data is backed by a cache and considered ephemeral data. The site should continue to function without the session data. Critical application data should be stored in the user database and cached in session only as a performance optimization.

Session isn't supported in [SignalR](/en-us/aspnet/signalr/) apps because a [SignalR Hub](../signalr/hubs?view=aspnetcore-10.0) may execute independent of an HTTP context. For example, this can occur when a long polling request is held open by a hub beyond the lifetime of the request's HTTP context.

ASP.NET Core maintains session state by providing a cookie to the client that contains a session ID. The cookie session ID:

* Is sent to the app with each request.
* Is used by the app to fetch the session data.

Session state exhibits the following behaviors:

* The session cookie is specific to the browser. Sessions aren't shared across browsers.

* Session cookies are deleted when the browser session ends.

* If a cookie is received for an expired session, a new session is created that uses the same session cookie.

* Empty sessions aren't retained. The session must have at least one value set to persist the session across requests. When a session isn't retained, a new session ID is generated for each new request.

* The app retains a session for a limited time after the last request. The app either sets the session timeout or uses the default value of 20 minutes. Session state is ideal for storing user data:

  * That's specific to a particular session.
  * Where the data doesn't require permanent storage across sessions.

* Session data is deleted either when the [ISession.Clear](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.clear) implementation is called or when the session expires.

* There's no default mechanism to inform app code that a client browser has been closed or when the session cookie is deleted or expired on the client.

* Session state cookies aren't marked essential by default. Session state isn't functional unless tracking is permitted by the site visitor. For more information, see [General Data Protection Regulation (GDPR) support in ASP.NET Core](../security/gdpr?view=aspnetcore-10.0#tempdata-provider-and-session-state-cookies-arent-essential).

* **Note**: There is no replacement for the cookieless session feature from the ASP.NET Framework because it's considered insecure and can lead to session fixation attacks.

Warning

Don't store sensitive data in session state. The user might not close the browser and clear the session cookie. Some browsers maintain valid session cookies across browser windows. A session might not be restricted to a single user. The next user might continue to browse the app with the same session cookie.

The in-memory cache provider stores session data in the memory of the server where the app resides. In a server farm scenario:

* Use *sticky sessions* to tie each session to a specific app instance on an individual server. [Azure App Service](https://azure.microsoft.com/services/app-service/) uses [Application Request Routing (ARR)](/en-us/iis/extensions/planning-for-arr/using-the-application-request-routing-module) to enforce sticky sessions by default. However, sticky sessions can affect scalability and complicate web app updates. A better approach is to use a Redis, SQL Server, or Azure Postgres distributed cache, which doesn't require sticky sessions. For more information, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0).
* The session cookie is encrypted via [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector). Data Protection must be properly configured to read session cookies on each machine. For more information, see [ASP.NET Core Data Protection Overview](../security/data-protection/introduction?view=aspnetcore-10.0) and [Key storage providers](../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0).

### Configure session state

Middleware for managing session state is included in the framework. To enable the session middleware, `Program.cs` must contain:

* Any of the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) memory caches. The `IDistributedCache` implementation is used as a backing store for session. For more information, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0).
* A call to [AddSession](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.sessionservicecollectionextensions.addsession)
* A call to [UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession)

The following code shows how to set up the in-memory session provider with a default in-memory implementation of `IDistributedCache`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromSeconds(10);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();
```

The preceding code sets a short timeout to simplify testing.

The order of middleware is important. Call `UseSession` after `UseRouting` and before `MapRazorPages` and `MapDefaultControllerRoute`. See [Middleware Ordering](middleware/?view=aspnetcore-10.0#middleware-order).

[HttpContext.Session](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.session#microsoft-aspnetcore-http-httpcontext-session) is available after session state is configured.

`HttpContext.Session` can't be accessed before `UseSession` has been called.

A new session with a new session cookie can't be created after the app has begun writing to the response stream. The exception is recorded in the web server log and not displayed in the browser.

### Load session state asynchronously

The default session provider in ASP.NET Core loads session records from the underlying [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) backing store asynchronously only if the [ISession.LoadAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.loadasync) method is explicitly called before the [TryGetValue](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.trygetvalue), [Set](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.set), or [Remove](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.remove) methods. If `LoadAsync` isn't called first, the underlying session record is loaded synchronously, which can incur a performance penalty at scale.

To have apps enforce this pattern, wrap the [DistributedSessionStore](/en-us/dotnet/api/microsoft.aspnetcore.session.distributedsessionstore) and [DistributedSession](/en-us/dotnet/api/microsoft.aspnetcore.session.distributedsession) implementations with versions that throw an exception if the `LoadAsync` method isn't called before `TryGetValue`, `Set`, or `Remove`. Register the wrapped versions in the services container.

### Session options

To override session defaults, use [SessionOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions).

| Option                                                                                                                                           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Cookie](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.cookie#microsoft-aspnetcore-builder-sessionoptions-cookie)                | Determines the settings used to create the cookie. [Name](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.name#microsoft-aspnetcore-http-cookiebuilder-name) defaults to [SessionDefaults.CookieName](/en-us/dotnet/api/microsoft.aspnetcore.session.sessiondefaults.cookiename#microsoft-aspnetcore-session-sessiondefaults-cookiename) (`.AspNetCore.Session`). [Path](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.path#microsoft-aspnetcore-http-cookiebuilder-path) defaults to [SessionDefaults.CookiePath](/en-us/dotnet/api/microsoft.aspnetcore.session.sessiondefaults.cookiepath#microsoft-aspnetcore-session-sessiondefaults-cookiepath) (`/`). [SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.samesite#microsoft-aspnetcore-http-cookiebuilder-samesite) defaults to [SameSiteMode.Lax](/en-us/dotnet/api/microsoft.aspnetcore.http.samesitemode#microsoft-aspnetcore-http-samesitemode-lax) (`1`). [HttpOnly](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.httponly#microsoft-aspnetcore-http-cookiebuilder-httponly) defaults to `true`. [IsEssential](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.isessential#microsoft-aspnetcore-http-cookiebuilder-isessential) defaults to `false`. |
| [IdleTimeout](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.idletimeout#microsoft-aspnetcore-builder-sessionoptions-idletimeout) | The `IdleTimeout` indicates how long the session can be idle before its contents are abandoned. Each session access resets the timeout. This setting only applies to the content of the session, not the cookie. The default is 20 minutes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| [IOTimeout](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.iotimeout#microsoft-aspnetcore-builder-sessionoptions-iotimeout)       | The maximum amount of time allowed to load a session from the store or to commit it back to the store. This setting may only apply to asynchronous operations. This timeout can be disabled using [InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan). The default is 1 minute.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

Session uses a cookie to track and identify requests from a single browser. By default, this cookie is named `.AspNetCore.Session`, and it uses a path of `/`. Because the cookie default doesn't specify a domain, it isn't made available to the client-side script on the page (because [HttpOnly](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.httponly#microsoft-aspnetcore-http-cookiebuilder-httponly) defaults to `true`).

To override cookie session defaults, use [SessionOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.Cookie.Name = ".AdventureWorks.Session";
    options.IdleTimeout = TimeSpan.FromSeconds(10);
    options.Cookie.IsEssential = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();
```

The app uses the [IdleTimeout](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.idletimeout#microsoft-aspnetcore-builder-sessionoptions-idletimeout) property to determine how long a session can be idle before its contents in the server's cache are abandoned. This property is independent of the cookie expiration. Each request that passes through the [session middleware](/en-us/dotnet/api/microsoft.aspnetcore.session.sessionmiddleware) resets the timeout.

Session state is *non-locking*. If two requests simultaneously attempt to modify the contents of a session, the last request overrides the first. `Session` is implemented as a *coherent session*, which means that all the contents are stored together. When two requests seek to modify different session values, the last request may override session changes made by the first.

### Set and get Session values

Session state is accessed from a Razor Pages [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class or MVC [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) class with [HttpContext.Session](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.session#microsoft-aspnetcore-http-httpcontext-session). This property is an [ISession](/en-us/dotnet/api/microsoft.aspnetcore.http.isession) implementation.

The `ISession` implementation provides several extension methods to set and retrieve integer and string values. The extension methods are in the [Microsoft.AspNetCore.Http](/en-us/dotnet/api/microsoft.aspnetcore.http) namespace.

`ISession` extension methods:

* [Get(ISession, String)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.get)
* [GetInt32(ISession, String)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.getint32)
* [GetString(ISession, String)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.getstring)
* [SetInt32(ISession, String, Int32)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.setint32)
* [SetString(ISession, String, String)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.setstring)

The following example retrieves the session value for the `IndexModel.SessionKeyName` key (`_Name` in the sample app) in a Razor Pages page:

```
@page
@using Microsoft.AspNetCore.Http
@model IndexModel

...

Name: @HttpContext.Session.GetString(IndexModel.SessionKeyName)
```

The following example shows how to set and get an integer and a string:

```
public class IndexModel : PageModel
{
    public const string SessionKeyName = "_Name";
    public const string SessionKeyAge = "_Age";

    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        if (string.IsNullOrEmpty(HttpContext.Session.GetString(SessionKeyName)))
        {
            HttpContext.Session.SetString(SessionKeyName, "The Doctor");
            HttpContext.Session.SetInt32(SessionKeyAge, 73);
        }
        var name = HttpContext.Session.GetString(SessionKeyName);
        var age = HttpContext.Session.GetInt32(SessionKeyAge).ToString();

        _logger.LogInformation("Session Name: {Name}", name);
        _logger.LogInformation("Session Age: {Age}", age);
    }
}
```

The following markup displays the session values on a Razor Page:

```
@page
@model PrivacyModel
@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<div class="text-center">
<p><b>Name:</b> @HttpContext.Session.GetString("_Name");<b>Age:

</b> @HttpContext.Session.GetInt32("_Age").ToString()</p>
</div>
```

All session data must be serialized to enable a distributed cache scenario, even when using the in-memory cache. String and integer serializers are provided by the extension methods of [ISession](/en-us/dotnet/api/microsoft.aspnetcore.http.isession). Complex types must be serialized by the user using another mechanism, such as JSON.

Use the following sample code to serialize objects:

```
public static class SessionExtensions
{
    public static void Set<T>(this ISession session, string key, T value)
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    public static T? Get<T>(this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default : JsonSerializer.Deserialize<T>(value);
    }
}
```

The following example shows how to set and get a serializable object with the `SessionExtensions` class:

```
using Microsoft.AspNetCore.Mvc.RazorPages;
using Web.Extensions;    // SessionExtensions

namespace SessionSample.Pages
{
    public class Index6Model : PageModel
    {
        const string SessionKeyTime = "_Time";
        public string? SessionInfo_SessionTime { get; private set; }
        private readonly ILogger<Index6Model> _logger;

        public Index6Model(ILogger<Index6Model> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
            var currentTime = DateTime.Now;

            // Requires SessionExtensions from sample.
            if (HttpContext.Session.Get<DateTime>(SessionKeyTime) == default)
            {
                HttpContext.Session.Set<DateTime>(SessionKeyTime, currentTime);
            }
            _logger.LogInformation("Current Time: {Time}", currentTime);
            _logger.LogInformation("Session Time: {Time}", 
                           HttpContext.Session.Get<DateTime>(SessionKeyTime));

        }
    }
}
```

Warning

Storing a live object in the session should be used with caution, as there are many problems that can occur with serialized objects. For more information, see [Sessions should be allowed to store objects (dotnet/aspnetcore #18159)](https://github.com/dotnet/aspnetcore/issues/18159).

## TempData

ASP.NET Core exposes the Razor Pages [TempData](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.tempdata#microsoft-aspnetcore-mvc-razorpages-pagemodel-tempdata) or Controller [TempData](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.tempdata#microsoft-aspnetcore-mvc-controller-tempdata). This property stores data until it's read in another request. The [Keep(String)](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.itempdatadictionary.keep) and [Peek(string)](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.itempdatadictionary.peek) methods can be used to examine the data without deletion at the end of the request. [Keep](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.itempdatadictionary.keep) marks all items in the dictionary for retention. `TempData` is:

* Useful for redirection when data is required for more than a single request.
* Implemented by `TempData` providers using either cookies or session state.

## TempData samples

Consider the following page that creates a customer:

```
public class CreateModel : PageModel
{
    private readonly RazorPagesContactsContext _context;

    public CreateModel(RazorPagesContactsContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";

        return RedirectToPage("./IndexPeek");
    }
}
```

The following page displays `TempData["Message"]`:

```
@page
@model IndexModel

<h1>Peek Contacts</h1>

@{
    if (TempData.Peek("Message") != null)
    {
        <h3>Message: @TempData.Peek("Message")</h3>
    }
}

@*Content removed for brevity.*@
```

In the preceding markup, at the end of the request, `TempData["Message"]` is **not** deleted because `Peek` is used. Refreshing the page displays the contents of `TempData["Message"]`.

The following markup is similar to the preceding code, but uses `Keep` to preserve the data at the end of the request:

```
@page
@model IndexModel

<h1>Contacts Keep</h1>

@{
    if (TempData["Message"] != null)
    {
        <h3>Message: @TempData["Message"]</h3>
    }
    TempData.Keep("Message");
}

@*Content removed for brevity.*@
```

Navigating between the *IndexPeek* and *IndexKeep* pages won't delete `TempData["Message"]`.

The following code displays `TempData["Message"]`, but at the end of the request, `TempData["Message"]` is deleted:

```
@page
@model IndexModel

<h1>Index no Keep or Peek</h1>

@{
    if (TempData["Message"] != null)
    {
        <h3>Message: @TempData["Message"]</h3>
    }
}

@*Content removed for brevity.*@
```

### TempData providers

The cookie-based TempData provider is used by default to store TempData in cookies.

The cookie data is encrypted using [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector), encoded with [Base64UrlTextEncoder](/en-us/dotnet/api/microsoft.aspnetcore.webutilities.base64urltextencoder), then chunked. The maximum cookie size is less than [4096 bytes](http://www.faqs.org/rfcs/rfc2965.html) due to encryption and chunking. The cookie data isn't compressed because compressing encrypted data can lead to security problems such as the [CRIME](https://wikipedia.org/wiki/CRIME_\(security_exploit\)) and [BREACH](https://wikipedia.org/wiki/BREACH_\(security_exploit\)) attacks. For more information on the cookie-based TempData provider, see [CookieTempDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.cookietempdataprovider).

### Choose a TempData provider

Choosing a TempData provider involves several considerations, such as:

* Does the app already use session state? If so, using the session state TempData provider has no additional cost to the app beyond the size of the data.
* Does the app use TempData only sparingly for relatively small amounts of data, up to 500 bytes? If so, the cookie TempData provider adds a small cost to each request that carries TempData. If not, the session state TempData provider can be beneficial to avoid round-tripping a large amount of data in each request until the TempData is consumed.
* Does the app run in a server farm on multiple servers? If so, there's no additional configuration required to use the cookie TempData provider outside of Data Protection. For more information, see [ASP.NET Core Data Protection Overview](../security/data-protection/introduction?view=aspnetcore-10.0) and [Key storage providers](../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0).

Most web clients such as web browsers enforce limits on the maximum size of each cookie and the total number of cookies. When using the cookie TempData provider, verify the app won't exceed [these limits](http://www.faqs.org/rfcs/rfc2965.html). Consider the total size of the data. Account for increases in cookie size due to encryption and chunking.

### Configure the TempData provider

The cookie-based TempData provider is enabled by default.

To enable the session-based TempData provider, use the [AddSessionStateTempDataProvider](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcviewfeaturesmvcbuilderextensions.addsessionstatetempdataprovider) extension method. Only one call to `AddSessionStateTempDataProvider` is required:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages()
                    .AddSessionStateTempDataProvider();
builder.Services.AddControllersWithViews()
                    .AddSessionStateTempDataProvider();

builder.Services.AddSession();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();
```

## Query strings

A limited amount of data can be passed from one request to another by adding it to the new request's query string. This is useful for capturing state in a persistent manner that allows links with embedded state to be shared through email or social networks. Because URL query strings are public, never use query strings for sensitive data.

In addition to unintended sharing, including data in query strings can expose the app to [Cross-Site Request Forgery (CSRF)](https://owasp.org/www-community/attacks/csrf) attacks. Any preserved session state must protect against CSRF attacks. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-10.0).

## Hidden fields

Data can be saved in hidden form fields and posted back on the next request. This is common in multi-page forms. Because the client can potentially tamper with the data, the app must always revalidate the data stored in hidden fields.

## `HttpContext.Items`

The [HttpContext.Items](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.items#microsoft-aspnetcore-http-httpcontext-items) collection is used to store data while processing a single request. The collection's contents are discarded after a request is processed. The `Items` collection is often used to allow components or middleware to communicate when they operate at different points in time during a request and have no direct way to pass parameters.

In the following example, [middleware](middleware/?view=aspnetcore-10.0) adds `isVerified` to the `Items` collection:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

ILogger logger = app.Logger;

app.Use(async (context, next) =>
{
    // context.Items["isVerified"] is null
    logger.LogInformation($"Before setting: Verified: {context.Items["isVerified"]}");
    context.Items["isVerified"] = true;
    await next.Invoke();
});

app.Use(async (context, next) =>
{
    // context.Items["isVerified"] is true
    logger.LogInformation($"Next: Verified: {context.Items["isVerified"]}");
    await next.Invoke();
});

app.MapGet("/", async context =>
{
    await context.Response.WriteAsync($"Verified: {context.Items["isVerified"]}");
});

app.Run();
```

For middleware that's only used in a single app, it's unlikely that using a fixed `string` key would cause a key collision. However, to avoid the possibility of a key collision altogether, an `object` can be used as an item key. This approach is particularly useful for middleware that's shared between apps and also has the advantage of eliminating the use of key strings in the code. The following example shows how to use an `object` key defined in a middleware class:

```
public class HttpContextItemsMiddleware
{
    private readonly RequestDelegate _next;
    public static readonly object HttpContextItemsMiddlewareKey = new();

    public HttpContextItemsMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        httpContext.Items[HttpContextItemsMiddlewareKey] = "K-9";

        await _next(httpContext);
    }
}

public static class HttpContextItemsMiddlewareExtensions
{
    public static IApplicationBuilder 
        UseHttpContextItemsMiddleware(this IApplicationBuilder app)
    {
        return app.UseMiddleware<HttpContextItemsMiddleware>();
    }
}
```

Other code can access the value stored in `HttpContext.Items` using the key exposed by the middleware class:

```
public class Index2Model : PageModel
{
    private readonly ILogger<Index2Model> _logger;

    public Index2Model(ILogger<Index2Model> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        HttpContext.Items
            .TryGetValue(HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey,
                out var middlewareSetValue);

        _logger.LogInformation("Middleware value {MV}",
            middlewareSetValue?.ToString() ?? "Middleware value not set!");
    }
}
```

## Cache

Caching is an efficient way to store and retrieve data. The app can control the lifetime of cached items. For more information, see [Response caching in ASP.NET Core](../performance/caching/response?view=aspnetcore-10.0).

Cached data isn't associated with a specific request, user, or session. **Do not cache user-specific data that may be retrieved by other user requests.**

To cache application wide data, see [Cache in-memory in ASP.NET Core](../performance/caching/memory?view=aspnetcore-10.0).

## Checking session state

[ISession.IsAvailable](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.isavailable#microsoft-aspnetcore-http-isession-isavailable) is intended to check for transient failures. Calling `IsAvailable` before the session middleware runs throws an `InvalidOperationException`.

Libraries that need to test session availability can use `HttpContext.Features.Get<ISessionFeature>()?.Session != null`.

## Common errors

* "Unable to resolve service for type 'Microsoft.Extensions.Caching.Distributed.IDistributedCache' while attempting to activate 'Microsoft.AspNetCore.Session.DistributedSessionStore'."

  This is typically caused by failing to configure at least one `IDistributedCache` implementation. For more information, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0) and [Cache in-memory in ASP.NET Core](../performance/caching/memory?view=aspnetcore-10.0).

If the session middleware fails to persist a session:

* The middleware logs the exception and the request continues normally.
* This leads to unpredictable behavior.

The session middleware can fail to persist a session if the backing store isn't available. For example, a user stores a shopping cart in session. The user adds an item to the cart but the commit fails. The app doesn't know about the failure so it reports to the user that the item was added to their cart, which isn't true.

The recommended approach to check for errors is to call `await feature.Session.CommitAsync` when the app is done writing to the session. [CommitAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.commitasync) throws an exception if the backing store is unavailable. If `CommitAsync` fails, the app can process the exception. [LoadAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.loadasync) throws under the same conditions when the data store is unavailable.

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Kirk Larkin](https://twitter.com/serpent5), and [Diana LaRose](https://github.com/DianaLaRose)

HTTP is a stateless protocol. By default, HTTP requests are independent messages that don't retain user values. This article describes several approaches to preserve user data between requests.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/app-state/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## State management

State can be stored using several approaches. Each approach is described later in this article.

| Storage approach                       | Storage mechanism                                                 |
| -------------------------------------- | ----------------------------------------------------------------- |
| [Cookies](#cookies)                    | HTTP cookies. May include data stored using server-side app code. |
| [Session state](#session-state)        | HTTP cookies and server-side app code                             |
| [TempData](#tempdata)                  | HTTP cookies or session state                                     |
| [Query strings](#query-strings)        | HTTP query strings                                                |
| [Hidden fields](#hidden-fields)        | HTTP form fields                                                  |
| [HttpContext.Items](#httpcontextitems) | Server-side app code                                              |
| [Cache](#cache)                        | Server-side app code                                              |

## SignalR/Blazor Server and HTTP context-based state management

[SignalR](../signalr/introduction?view=aspnetcore-10.0) apps shouldn't use session state and other state management approaches that rely upon a stable HTTP context to store information. SignalR apps can store per-connection state in [`Context.Items` in the hub](../signalr/hubs?view=aspnetcore-10.0). For more information and alternative state management approaches for Blazor Server apps, see [ASP.NET Core Blazor state management overview](../blazor/state-management/?view=aspnetcore-10.0) and [ASP.NET Core Blazor server-side state management](../blazor/state-management/server?view=aspnetcore-10.0).

## Cookies

Cookies store data across requests. Because cookies are sent with every request, their size should be kept to a minimum. Ideally, only an identifier should be stored in a cookie with the data stored by the app. Most browsers restrict cookie size to 4096 bytes. Only a limited number of cookies are available for each domain.

Because cookies are subject to tampering, they must be validated by the app. Cookies can be deleted by users and expire on clients. However, cookies are generally the most durable form of data persistence on the client.

Cookies are often used for personalization, where content is customized for a known user. The user is only identified and not authenticated in most cases. The cookie can store the user's name, account name, or unique user ID such as a GUID. The cookie can be used to access the user's personalized settings, such as their preferred website background color.

See the [European Union General Data Protection Regulations (GDPR)](https://ec.europa.eu/info/law/law-topic/data-protection) when issuing cookies and dealing with privacy concerns. For more information, see [General Data Protection Regulation (GDPR) support in ASP.NET Core](../security/gdpr?view=aspnetcore-10.0).

## Session state

Session state is an ASP.NET Core scenario for storage of user data while the user browses a web app. Session state uses a store maintained by the app to persist data across requests from a client. The session data is backed by a cache and considered ephemeral data. The site should continue to function without the session data. Critical application data should be stored in the user database and cached in session only as a performance optimization.

Session isn't supported in [SignalR](/en-us/aspnet/signalr/) apps because a [SignalR Hub](../signalr/hubs?view=aspnetcore-10.0) may execute independent of an HTTP context. For example, this can occur when a long polling request is held open by a hub beyond the lifetime of the request's HTTP context.

ASP.NET Core maintains session state by providing a cookie to the client that contains a session ID. The cookie session ID:

* Is sent to the app with each request.
* Is used by the app to fetch the session data.

Session state exhibits the following behaviors:

* The session cookie is specific to the browser. Sessions aren't shared across browsers.

* Session cookies are deleted when the browser session ends.

* If a cookie is received for an expired session, a new session is created that uses the same session cookie.

* Empty sessions aren't retained. The session must have at least one value set to persist the session across requests. When a session isn't retained, a new session ID is generated for each new request.

* The app retains a session for a limited time after the last request. The app either sets the session timeout or uses the default value of 20 minutes. Session state is ideal for storing user data:

  * That's specific to a particular session.
  * Where the data doesn't require permanent storage across sessions.

* Session data is deleted either when the [ISession.Clear](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.clear) implementation is called or when the session expires.

* There's no default mechanism to inform app code that a client browser has been closed or when the session cookie is deleted or expired on the client.

* Session state cookies aren't marked essential by default. Session state isn't functional unless tracking is permitted by the site visitor. For more information, see [General Data Protection Regulation (GDPR) support in ASP.NET Core](../security/gdpr?view=aspnetcore-10.0#tempdata-provider-and-session-state-cookies-arent-essential).

Warning

Don't store sensitive data in session state. The user might not close the browser and clear the session cookie. Some browsers maintain valid session cookies across browser windows. A session might not be restricted to a single user. The next user might continue to browse the app with the same session cookie.

The in-memory cache provider stores session data in the memory of the server where the app resides. In a server farm scenario:

* Use *sticky sessions* to tie each session to a specific app instance on an individual server. [Azure App Service](https://azure.microsoft.com/services/app-service/) uses [Application Request Routing (ARR)](/en-us/iis/extensions/planning-for-arr/using-the-application-request-routing-module) to enforce sticky sessions by default. However, sticky sessions can affect scalability and complicate web app updates. A better approach is to use a Redis, SQL Server, or Azure Postgres distributed cache, which doesn't require sticky sessions. For more information, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0).
* The session cookie is encrypted via [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector). Data Protection must be properly configured to read session cookies on each machine. For more information, see [ASP.NET Core Data Protection Overview](../security/data-protection/introduction?view=aspnetcore-10.0) and [Key storage providers](../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0).

### Configure session state

The [Microsoft.AspNetCore.Session](https://www.nuget.org/packages/Microsoft.AspNetCore.Session/) package:

* Is included implicitly by the framework.
* Provides middleware for managing session state.

To enable the session middleware, `Startup` must contain:

* Any of the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) memory caches. The `IDistributedCache` implementation is used as a backing store for session. For more information, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0).
* A call to [AddSession](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.sessionservicecollectionextensions.addsession) in `ConfigureServices`.
* A call to [UseSession](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionmiddlewareextensions.usesession) in `Configure`.

The following code shows how to set up the in-memory session provider with a default in-memory implementation of `IDistributedCache`:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDistributedMemoryCache();

        services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromSeconds(10);
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
        });

        services.AddControllersWithViews();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseSession();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
            endpoints.MapRazorPages();
        });
    }
}
```

The preceding code sets a short timeout to simplify testing.

The order of middleware is important. Call `UseSession` after `UseRouting` and before `UseEndpoints`. For more information, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0#middleware-order).

[HttpContext.Session](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.session#microsoft-aspnetcore-http-httpcontext-session) is available after session state is configured.

`HttpContext.Session` can't be accessed before `UseSession` has been called.

A new session with a new session cookie can't be created after the app has begun writing to the response stream. The exception is recorded in the web server log and not displayed in the browser.

### Load session state asynchronously

The default session provider in ASP.NET Core loads session records from the underlying [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) backing store asynchronously only if the [ISession.LoadAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.loadasync) method is explicitly called before the [TryGetValue](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.trygetvalue), [Set](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.set), or [Remove](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.remove) methods. If `LoadAsync` isn't called first, the underlying session record is loaded synchronously, which can incur a performance penalty at scale.

To have apps enforce this pattern, wrap the [DistributedSessionStore](/en-us/dotnet/api/microsoft.aspnetcore.session.distributedsessionstore) and [DistributedSession](/en-us/dotnet/api/microsoft.aspnetcore.session.distributedsession) implementations with versions that throw an exception if the `LoadAsync` method isn't called before `TryGetValue`, `Set`, or `Remove`. Register the wrapped versions in the services container.

### Session options

To override session defaults, use [SessionOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions).

| Option                                                                                                                                           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Cookie](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.cookie#microsoft-aspnetcore-builder-sessionoptions-cookie)                | Determines the settings used to create the cookie. [Name](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.name#microsoft-aspnetcore-http-cookiebuilder-name) defaults to [SessionDefaults.CookieName](/en-us/dotnet/api/microsoft.aspnetcore.session.sessiondefaults.cookiename#microsoft-aspnetcore-session-sessiondefaults-cookiename) (`.AspNetCore.Session`). [Path](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.path#microsoft-aspnetcore-http-cookiebuilder-path) defaults to [SessionDefaults.CookiePath](/en-us/dotnet/api/microsoft.aspnetcore.session.sessiondefaults.cookiepath#microsoft-aspnetcore-session-sessiondefaults-cookiepath) (`/`). [SameSite](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.samesite#microsoft-aspnetcore-http-cookiebuilder-samesite) defaults to [SameSiteMode.Lax](/en-us/dotnet/api/microsoft.aspnetcore.http.samesitemode#microsoft-aspnetcore-http-samesitemode-lax) (`1`). [HttpOnly](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.httponly#microsoft-aspnetcore-http-cookiebuilder-httponly) defaults to `true`. [IsEssential](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.isessential#microsoft-aspnetcore-http-cookiebuilder-isessential) defaults to `false`. |
| [IdleTimeout](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.idletimeout#microsoft-aspnetcore-builder-sessionoptions-idletimeout) | The `IdleTimeout` indicates how long the session can be idle before its contents are abandoned. Each session access resets the timeout. This setting only applies to the content of the session, not the cookie. The default is 20 minutes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| [IOTimeout](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.iotimeout#microsoft-aspnetcore-builder-sessionoptions-iotimeout)       | The maximum amount of time allowed to load a session from the store or to commit it back to the store. This setting may only apply to asynchronous operations. This timeout can be disabled using [InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan). The default is 1 minute.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

Session uses a cookie to track and identify requests from a single browser. By default, this cookie is named `.AspNetCore.Session`, and it uses a path of `/`. Because the cookie default doesn't specify a domain, it isn't made available to the client-side script on the page (because [HttpOnly](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.httponly#microsoft-aspnetcore-http-cookiebuilder-httponly) defaults to `true`).

To override cookie session defaults, use [SessionOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions):

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedMemoryCache();

    services.AddSession(options =>
    {
        options.Cookie.Name = ".AdventureWorks.Session";
        options.IdleTimeout = TimeSpan.FromSeconds(10);
        options.Cookie.IsEssential = true;
    });

    services.AddControllersWithViews();
    services.AddRazorPages();
}
```

The app uses the [IdleTimeout](/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.idletimeout#microsoft-aspnetcore-builder-sessionoptions-idletimeout) property to determine how long a session can be idle before its contents in the server's cache are abandoned. This property is independent of the cookie expiration. Each request that passes through the [session middleware](/en-us/dotnet/api/microsoft.aspnetcore.session.sessionmiddleware) resets the timeout.

Session state is *non-locking*. If two requests simultaneously attempt to modify the contents of a session, the last request overrides the first. `Session` is implemented as a *coherent session*, which means that all the contents are stored together. When two requests seek to modify different session values, the last request may override session changes made by the first.

### Set and get Session values

Session state is accessed from a Razor Pages [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class or MVC [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) class with [HttpContext.Session](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.session#microsoft-aspnetcore-http-httpcontext-session). This property is an [ISession](/en-us/dotnet/api/microsoft.aspnetcore.http.isession) implementation.

The `ISession` implementation provides several extension methods to set and retrieve integer and string values. The extension methods are in the [Microsoft.AspNetCore.Http](/en-us/dotnet/api/microsoft.aspnetcore.http) namespace.

`ISession` extension methods:

* [Get(ISession, String)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.get)
* [GetInt32(ISession, String)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.getint32)
* [GetString(ISession, String)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.getstring)
* [SetInt32(ISession, String, Int32)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.setint32)
* [SetString(ISession, String, String)](/en-us/dotnet/api/microsoft.aspnetcore.http.sessionextensions.setstring)

The following example retrieves the session value for the `IndexModel.SessionKeyName` key (`_Name` in the sample app) in a Razor Pages page:

```
@page
@using Microsoft.AspNetCore.Http
@model IndexModel

...

Name: @HttpContext.Session.GetString(IndexModel.SessionKeyName)
```

The following example shows how to set and get an integer and a string:

```
public class IndexModel : PageModel
{
    public const string SessionKeyName = "_Name";
    public const string SessionKeyAge = "_Age";
    const string SessionKeyTime = "_Time";

    public string SessionInfo_Name { get; private set; }
    public string SessionInfo_Age { get; private set; }
    public string SessionInfo_CurrentTime { get; private set; }
    public string SessionInfo_SessionTime { get; private set; }
    public string SessionInfo_MiddlewareValue { get; private set; }

    public void OnGet()
    {
        // Requires: using Microsoft.AspNetCore.Http;
        if (string.IsNullOrEmpty(HttpContext.Session.GetString(SessionKeyName)))
        {
            HttpContext.Session.SetString(SessionKeyName, "The Doctor");
            HttpContext.Session.SetInt32(SessionKeyAge, 773);
        }

        var name = HttpContext.Session.GetString(SessionKeyName);
        var age = HttpContext.Session.GetInt32(SessionKeyAge);
```

All session data must be serialized to enable a distributed cache scenario, even when using the in-memory cache. String and integer serializers are provided by the extension methods of [ISession](/en-us/dotnet/api/microsoft.aspnetcore.http.isession). Complex types must be serialized by the user using another mechanism, such as JSON.

Use the following sample code to serialize objects:

```
public static class SessionExtensions
{
    public static void Set<T>(this ISession session, string key, T value)
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    public static T Get<T>(this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default : JsonSerializer.Deserialize<T>(value);
    }
}
```

The following example shows how to set and get a serializable object with the `SessionExtensions` class:

```
// Requires SessionExtensions from sample download.
if (HttpContext.Session.Get<DateTime>(SessionKeyTime) == default)
{
    HttpContext.Session.Set<DateTime>(SessionKeyTime, currentTime);
}
```

## TempData

ASP.NET Core exposes the Razor Pages [TempData](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.tempdata#microsoft-aspnetcore-mvc-razorpages-pagemodel-tempdata) or Controller [TempData](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.tempdata#microsoft-aspnetcore-mvc-controller-tempdata). This property stores data until it's read in another request. The [Keep(String)](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.itempdatadictionary.keep) and [Peek(string)](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.itempdatadictionary.peek) methods can be used to examine the data without deletion at the end of the request. [Keep](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.itempdatadictionary.keep) marks all items in the dictionary for retention. `TempData` is:

* Useful for redirection when data is required for more than a single request.
* Implemented by `TempData` providers using either cookies or session state.

## TempData samples

Consider the following page that creates a customer:

```
public class CreateModel : PageModel
{
    private readonly RazorPagesContactsContext _context;

    public CreateModel(RazorPagesContactsContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";

        return RedirectToPage("./IndexPeek");
    }
}
```

The following page displays `TempData["Message"]`:

```
@page
@model IndexModel

<h1>Peek Contacts</h1>

@{
    if (TempData.Peek("Message") != null)
    {
        <h3>Message: @TempData.Peek("Message")</h3>
    }
}

@*Content removed for brevity.*@
```

In the preceding markup, at the end of the request, `TempData["Message"]` is **not** deleted because `Peek` is used. Refreshing the page displays the contents of `TempData["Message"]`.

The following markup is similar to the preceding code, but uses `Keep` to preserve the data at the end of the request:

```
@page
@model IndexModel

<h1>Contacts Keep</h1>

@{
    if (TempData["Message"] != null)
    {
        <h3>Message: @TempData["Message"]</h3>
    }
    TempData.Keep("Message");
}

@*Content removed for brevity.*@
```

Navigating between the *IndexPeek* and *IndexKeep* pages won't delete `TempData["Message"]`.

The following code displays `TempData["Message"]`, but at the end of the request, `TempData["Message"]` is deleted:

```
@page
@model IndexModel

<h1>Index no Keep or Peek</h1>

@{
    if (TempData["Message"] != null)
    {
        <h3>Message: @TempData["Message"]</h3>
    }
}

@*Content removed for brevity.*@
```

### TempData providers

The cookie-based TempData provider is used by default to store TempData in cookies.

The cookie data is encrypted using [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector), encoded with [Base64UrlTextEncoder](/en-us/dotnet/api/microsoft.aspnetcore.webutilities.base64urltextencoder), then chunked. The maximum cookie size is less than [4096 bytes](http://www.faqs.org/rfcs/rfc2965.html) due to encryption and chunking. The cookie data isn't compressed because compressing encrypted data can lead to security problems such as the [CRIME](https://wikipedia.org/wiki/CRIME_\(security_exploit\)) and [BREACH](https://wikipedia.org/wiki/BREACH_\(security_exploit\)) attacks. For more information on the cookie-based TempData provider, see [CookieTempDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.cookietempdataprovider).

### Choose a TempData provider

Choosing a TempData provider involves several considerations, such as:

* Does the app already use session state? If so, using the session state TempData provider has no additional cost to the app beyond the size of the data.
* Does the app use TempData only sparingly for relatively small amounts of data, up to 500 bytes? If so, the cookie TempData provider adds a small cost to each request that carries TempData. If not, the session state TempData provider can be beneficial to avoid round-tripping a large amount of data in each request until the TempData is consumed.
* Does the app run in a server farm on multiple servers? If so, there's no additional configuration required to use the cookie TempData provider outside of Data Protection (see [ASP.NET Core Data Protection Overview](../security/data-protection/introduction?view=aspnetcore-10.0) and [Key storage providers](../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0)).

Most web clients such as web browsers enforce limits on the maximum size of each cookie and the total number of cookies. When using the cookie TempData provider, verify the app won't exceed [these limits](http://www.faqs.org/rfcs/rfc2965.html). Consider the total size of the data. Account for increases in cookie size due to encryption and chunking.

### Configure the TempData provider

The cookie-based TempData provider is enabled by default.

To enable the session-based TempData provider, use the [AddSessionStateTempDataProvider](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcviewfeaturesmvcbuilderextensions.addsessionstatetempdataprovider) extension method. Only one call to `AddSessionStateTempDataProvider` is required:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews()
        .AddSessionStateTempDataProvider();
    services.AddRazorPages()
        .AddSessionStateTempDataProvider();

    services.AddSession();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseSession();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
        endpoints.MapRazorPages();
    });
}
```

## Query strings

A limited amount of data can be passed from one request to another by adding it to the new request's query string. This is useful for capturing state in a persistent manner that allows links with embedded state to be shared through email or social networks. Because URL query strings are public, never use query strings for sensitive data.

In addition to unintended sharing, including data in query strings can expose the app to [Cross-Site Request Forgery (CSRF)](https://owasp.org/www-community/attacks/csrf) attacks. Any preserved session state must protect against CSRF attacks. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-10.0).

## Hidden fields

Data can be saved in hidden form fields and posted back on the next request. This is common in multi-page forms. Because the client can potentially tamper with the data, the app must always revalidate the data stored in hidden fields.

## HttpContext.Items

The [HttpContext.Items](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.items#microsoft-aspnetcore-http-httpcontext-items) collection is used to store data while processing a single request. The collection's contents are discarded after a request is processed. The `Items` collection is often used to allow components or middleware to communicate when they operate at different points in time during a request and have no direct way to pass parameters.

In the following example, [middleware](middleware/?view=aspnetcore-10.0) adds `isVerified` to the `Items` collection:

```
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    app.UseRouting();

    app.Use(async (context, next) =>
    {
        logger.LogInformation($"Before setting: Verified: {context.Items["isVerified"]}");
        context.Items["isVerified"] = true;
        await next.Invoke();
    });

    app.Use(async (context, next) =>
    {
        logger.LogInformation($"Next: Verified: {context.Items["isVerified"]}");
        await next.Invoke();
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync($"Verified: {context.Items["isVerified"]}");
        });
    });
}
```

For middleware that's only used in a single app, fixed `string` keys are acceptable. Middleware shared between apps should use unique object keys to avoid key collisions. The following example shows how to use a unique object key defined in a middleware class:

```
public class HttpContextItemsMiddleware
{
    private readonly RequestDelegate _next;
    public static readonly object HttpContextItemsMiddlewareKey = new Object();

    public HttpContextItemsMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        httpContext.Items[HttpContextItemsMiddlewareKey] = "K-9";

        await _next(httpContext);
    }
}

public static class HttpContextItemsMiddlewareExtensions
{
    public static IApplicationBuilder 
        UseHttpContextItemsMiddleware(this IApplicationBuilder app)
    {
        return app.UseMiddleware<HttpContextItemsMiddleware>();
    }
}
```

Other code can access the value stored in `HttpContext.Items` using the key exposed by the middleware class:

```
HttpContext.Items
    .TryGetValue(HttpContextItemsMiddleware.HttpContextItemsMiddlewareKey, 
        out var middlewareSetValue);
SessionInfo_MiddlewareValue = 
    middlewareSetValue?.ToString() ?? "Middleware value not set!";
```

This approach also has the advantage of eliminating the use of key strings in the code.

## Cache

Caching is an efficient way to store and retrieve data. The app can control the lifetime of cached items. For more information, see [Response caching in ASP.NET Core](../performance/caching/response?view=aspnetcore-10.0).

Cached data isn't associated with a specific request, user, or session. **Do not cache user-specific data that may be retrieved by other user requests.**

To cache application wide data, see [Cache in-memory in ASP.NET Core](../performance/caching/memory?view=aspnetcore-10.0).

## Common errors

* "Unable to resolve service for type 'Microsoft.Extensions.Caching.Distributed.IDistributedCache' while attempting to activate 'Microsoft.AspNetCore.Session.DistributedSessionStore'."

  This is typically caused by failing to configure at least one `IDistributedCache` implementation. For more information, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0) and [Cache in-memory in ASP.NET Core](../performance/caching/memory?view=aspnetcore-10.0).

If the session middleware fails to persist a session:

* The middleware logs the exception and the request continues normally.
* This leads to unpredictable behavior.

The session middleware can fail to persist a session if the backing store isn't available. For example, a user stores a shopping cart in session. The user adds an item to the cart but the commit fails. The app doesn't know about the failure so it reports to the user that the item was added to their cart, which isn't true.

The recommended approach to check for errors is to call `await feature.Session.CommitAsync` when the app is done writing to the session. [CommitAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.commitasync) throws an exception if the backing store is unavailable. If `CommitAsync` fails, the app can process the exception. [LoadAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.isession.loadasync) throws under the same conditions when the data store is unavailable.

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/app-state/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))
* [Host ASP.NET Core in a web farm](../host-and-deploy/web-farm?view=aspnetcore-10.0)

[Host ASP.NET Core in a web farm](../host-and-deploy/web-farm?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/diagnostics
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/diagnostics.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Logging and diagnostics in ASP.NET Core SignalR

By [Ashley Stanton-Nurse](https://github.com/analogrelay)

This article provides guidance for gathering diagnostics from your ASP.NET Core SignalR app to help troubleshoot issues.

## Server-side logging

Warning

Server-side logs may contain sensitive information from your app. **Never** post raw logs from production apps to public forums like GitHub.

Since SignalR is part of ASP.NET Core, it uses the ASP.NET Core logging system. In the default configuration, SignalR logs minimal information, but the logging level can be configured. See the documentation on [ASP.NET Core logging](../fundamentals/logging/?view=aspnetcore-10.0) for details on configuring ASP.NET Core logging.

SignalR uses two logger categories:

* `Microsoft.AspNetCore.SignalR`: For logs related to Hub Protocols, activating Hubs, invoking methods, and other Hub-related activities.
* `Microsoft.AspNetCore.Http.Connections`: For logs related to transports, such as WebSockets, Long Polling, Server-Sent Events, and low-level SignalR infrastructure.

To enable detailed logs from SignalR, configure both of the preceding prefixes to the `Debug` level in your `appsettings.json` file by adding the following items to the `LogLevel` subsection in `Logging`:

```
{
    "Logging": {
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information",
            "Microsoft.AspNetCore.SignalR": "Debug",
            "Microsoft.AspNetCore.Http.Connections": "Debug"
        }
    }
}
```

The logging levels for the SignalR logger categories can also be configured in code within the `CreateWebHostBuilder` method:

```
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Debug);
            logging.AddFilter("Microsoft.AspNetCore.Http.Connections", LogLevel.Debug);
        })
        .UseStartup<Startup>();
```

If you aren't using JSON-based configuration, set the following configuration values in your configuration system:

* `Logging:LogLevel:Microsoft.AspNetCore.SignalR` = `Debug`
* `Logging:LogLevel:Microsoft.AspNetCore.Http.Connections` = `Debug`

Check the documentation for your configuration system to determine how to specify nested configuration values. For example, when using environment variables, two `_` characters are used instead of the `:` (for example, `Logging__LogLevel__Microsoft.AspNetCore.SignalR`).

We recommend using the `Debug` level when gathering more detailed diagnostics for your app. The `Trace` level produces low-level diagnostics and is rarely needed to diagnose issues in your app.

## Access server-side logs

How server-side logs are accessed depends on the environment in which the app is running.

### As a console app outside IIS

If you're running in a console app, the [Console logger](../fundamentals/logging/?view=aspnetcore-10.0#console) should be enabled by default. SignalR logs appear in the console.

### Within IIS Express from Visual Studio

Visual Studio displays the log output in the **Output** window. Select the **ASP.NET Core Web Server** drop down option.

### Azure App Service

Enable the **Application Logging (Filesystem)** option in the **Diagnostics logs** section of the Azure App Service portal and configure the **Level** to `Verbose`. Logs should be available from the **Log streaming** service and in logs on the file system of the App Service. For more information, see [Azure log streaming](../fundamentals/logging/?view=aspnetcore-10.0#azure-log-streaming).

### Other environments

For more information on configuring logging providers suitable for different deployment environments, such as Docker, Kubernetes, or Windows Service, see [Logging in .NET and ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0).

## JavaScript client logging

Warning

Client-side logs may contain sensitive information from your app. **Never** post raw logs from production apps to public forums like GitHub.

When using the JavaScript client, you can configure logging options using the `configureLogging` method on `HubConnectionBuilder`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/my/hub/url")
    .configureLogging(signalR.LogLevel.Debug)
    .build();
```

Disable framework logging by specifying `signalR.LogLevel.None` in the `configureLogging` method. Note that some logging is emitted directly by the browser and can't be disabled via setting the log level.

The following table shows log levels available to the JavaScript client. Setting the log level to one of these values enables logging at that level and all levels above it in the table.

| Level         | Description                                                                |
| ------------- | -------------------------------------------------------------------------- |
| `None`        | No messages are logged.                                                    |
| `Critical`    | Messages that indicate a failure in the entire app.                        |
| `Error`       | Messages that indicate a failure in the current operation.                 |
| `Warning`     | Messages that indicate a non-fatal problem.                                |
| `Information` | Informational messages.                                                    |
| `Debug`       | Diagnostic messages useful for debugging.                                  |
| `Trace`       | Very detailed diagnostic messages designed for diagnosing specific issues. |

Once you've configured the verbosity, the logs will be written to the Browser Console (or Standard Output in a NodeJS app).

If you want to send logs to a custom logging system, you can provide a JavaScript object implementing the `ILogger` interface. The only method that needs to be implemented is `log`, which takes the level of the event and the message associated with the event. For example:

```
import { ILogger, LogLevel, HubConnectionBuilder } from "@microsoft/signalr";

export class MyLogger implements ILogger {
    log(logLevel: LogLevel, message: string) {
        // Use `message` and `logLevel` to record the log message to your own system
    }
}

// later on, when configuring your connection...

let connection = new HubConnectionBuilder()
    .withUrl("/my/hub/url")
    .configureLogging(new MyLogger())
    .build();
```

```
import { ILogger, LogLevel, HubConnectionBuilder } from "@aspnet/signalr";

export class MyLogger implements ILogger {
    log(logLevel: LogLevel, message: string) {
        // Use `message` and `logLevel` to record the log message to your own system
    }
}

// later on, when configuring your connection...

let connection = new HubConnectionBuilder()
    .withUrl("/my/hub/url")
    .configureLogging(new MyLogger())
    .build();
```

## .NET client logging

Warning

Client-side logs may contain sensitive information from your app. **Never** post raw logs from production apps to public forums like GitHub.

To get logs from the .NET client, you can use the `ConfigureLogging` method on `HubConnectionBuilder`. This works the same way as the `ConfigureLogging` method on `WebHostBuilder` and `HostBuilder`. You can configure the same logging providers you use in ASP.NET Core. However, you have to manually install and enable the NuGet packages for the individual logging providers.

To add .NET client logging to a Blazor WebAssembly app, see [ASP.NET Core Blazor logging](../blazor/fundamentals/logging?view=aspnetcore-10.0#signalr-net-client-logging).

### Console logging

In order to enable Console logging, add the [Microsoft.Extensions.Logging.Console](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Console) package. Then, use the `AddConsole` method to configure the console logger:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/my/hub/url")
    .ConfigureLogging(logging =>
    {
        // Log to the Console
        logging.AddConsole();

        // This will set ALL logging to Debug level
        logging.SetMinimumLevel(LogLevel.Debug);
    })
    .Build();
```

### Debug output window logging

Logs can be configured to go to the **Output** window in Visual Studio. Install the [Microsoft.Extensions.Logging.Debug](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Debug) package and use the `AddDebug` method:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/my/hub/url")
    .ConfigureLogging(logging =>
    {
        // Log to the Output Window
        logging.AddDebug();

        // This will set ALL logging to Debug level
        logging.SetMinimumLevel(LogLevel.Debug)
    })
    .Build();
```

### Other logging providers

SignalR supports other logging providers such as Serilog, Seq, NLog, or any other logging system that integrates with `Microsoft.Extensions.Logging`. If your logging system provides an `ILoggerProvider`, you can register it with `AddProvider`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/my/hub/url")
    .ConfigureLogging(logging =>
    {
        // Log to your custom provider
        logging.AddProvider(new MyCustomLoggingProvider());

        // This will set ALL logging to Debug level
        logging.SetMinimumLevel(LogLevel.Debug)
    })
    .Build();
```

### Control verbosity

When logging from other places in the app, changing the default level to `Debug` may be too verbose. A Filter can be used to configure the logging level for SignalR logs. This can be done in code, in much the same way as on the server:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/my/hub/url")
    .ConfigureLogging(logging =>
    {
        // Register your providers

        // Set the default log level to Information, but to Debug for SignalR-related loggers.
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Debug);
        logging.AddFilter("Microsoft.AspNetCore.Http.Connections", LogLevel.Debug);
    })
    .Build();
```

## Tracing in SignalR

SignalR hub server and the SignalR client provide information about SignalR connections and messages using [DiagnosticSource](/en-us/dotnet/api/system.diagnostics.diagnosticsource) and [Activity](/en-us/dotnet/api/system.diagnostics.activity). SignalR has an ActivitySource for both the hub server and client, available starting with .NET 9.

An ActivitySource is a component used in distributed tracing to create and manage activities (or spans) that represent operations in your application. These activities can be used to:

* Track the flow of requests and operations across different components and services.
* Provide valuable insights into the performance and behavior of your application.

### .NET SignalR server ActivitySource

The SignalR ActivitySource named `Microsoft.AspNetCore.SignalR.Server` emits events for hub method calls:

* Every method is its own activity, so anything that emits an activity during the hub method call is under the hub method activity.
* Hub method activities don't have a parent. This means they aren't bundled under the long-running SignalR connection.

The following example uses the [Aspire dashboard](/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash#use-the-dashboard-with-aspire-projects) and the [OpenTelemetry](https://www.nuget.org/packages/OpenTelemetry.Extensions.Hosting) packages:

```
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
```

Add the following startup code to the `Program.cs` file:

```
using OpenTelemetry.Trace;
using SignalRChat.Hubs;

// Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable depending on where your OTEL endpoint is.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        if (builder.Environment.IsDevelopment())
        {
            // View all traces only in development environment.
            tracing.SetSampler(new AlwaysOnSampler());
        }

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.AspNetCore.SignalR.Server");
    });

builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());

var app = builder.Build();
```

The following example output is from the Aspire Dashboard:

ASP.NET Core also provides its own metrics on `Microsoft.AspNetCore.Hosting` event source.

### .NET SignalR client ActivitySource

The SignalR `ActivitySource` named `Microsoft.AspNetCore.SignalR.Client` emits events for a SignalR client:

* Hub invocations create a client span. Other SignalR clients, such as the JavaScript client, don't support tracing. This feature will be added to more clients in future releases.
* Hub invocations on the client and server support [context propagation](https://opentelemetry.io/docs/concepts/context-propagation/). Propagating the trace context enables true distributed tracing. It's now possible to see invocations flow from the client to the server and back.

Here's how these new activities look in the [Aspire dashboard](/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash#standalone-mode):

## Network traces

Warning

A network trace contains the full contents of every message sent by your app. **Never** post raw network traces from production apps to public forums like GitHub.

If you encounter an issue, a network trace can sometimes provide a valuable information. This is especially helpful when filing an issue on our issue tracker.

## Collect a network trace with Fiddler (preferred option)

This method works for all apps.

Fiddler is a powerful tool for collecting HTTP traces. Install it from [telerik.com/fiddler](https://www.telerik.com/fiddler), launch it, and then run your app and reproduce the issue. Fiddler is available for Windows, and there are beta versions for macOS and Linux.

If you connect using HTTPS, there are some extra steps to ensure Fiddler can decrypt the HTTPS traffic. For more information, see the [Fiddler documentation](https://docs.telerik.com/fiddler/Configure-Fiddler/Tasks/DecryptHTTPS).

After collecting the trace, export it by selecting **File** > **Save** > **All Sessions** from the menu bar

## Collect a network trace with tcpdump (macOS and Linux only)

This method works for all apps.

Raw TCP traces can be collected using tcpdump by running the following command from a command shell. You may need to be `root` or prefix the command with `sudo` if you get a permissions error:

```
tcpdump -i [interface] -w trace.pcap
```

Replace `[interface]` with the network interface you wish to capture on. Usually, this is something like `/dev/eth0` (for a standard Ethernet interface) or `/dev/lo0` (for localhost traffic). For more information, see the `tcpdump` manual page on your host system.

## Collect a network trace in the browser

This method only works for browser-based apps.

Most browser developer tools consoles have a "Network" tab that allows network activity to be captured between the browser and the server. In modern browsers, you can inspect WebSocket messages by selecting the connection under the **WS** filter and opening the messages tab. For Server-Sent Events, select the request and inspect the event stream tab. However, exported HAR files might not include full WebSocket message frames depending on browser support. When full payload capture of those transports is needed, using a tool like Fiddler or tcpdump is recommended.

### Microsoft Edge

1. Open the Dev Tools by pressing F12
2. Select the Network Tab
3. Refresh the page (if needed) and reproduce the problem
4. Select the Save icon in the toolbar to export the trace as a "HAR" file:

### Google Chrome

1. Open the Dev Tools by pressing F12
2. Select the Network Tab
3. Refresh the page (if needed) and reproduce the problem
4. Right click anywhere in the list of requests and choose "Save as HAR with content":

### Mozilla Firefox

1. Open the Dev Tools by pressing F12
2. Select the Network Tab
3. Refresh the page (if needed) and reproduce the problem
4. Right click anywhere in the list of requests and choose "Save All As HAR"

## Attach diagnostics files to GitHub issues

Diagnostics files can be attached to GitHub issues by renaming them so they have a `.txt` extension and then dragging and dropping them on to the issue.

Note

Please don't paste the content of log files or network traces into a GitHub issue. These logs and traces can be large, and GitHub usually truncates them.

## Metrics

Metrics are a representation of data measures over intervals of time. For example, requests per second. Metrics data allows observation of the state of an app at a high level. .NET gRPC metrics are emitted using [EventCounter](/en-us/dotnet/api/system.diagnostics.tracing.eventcounter).

### SignalR server metrics

SignalR server metrics are reported on the [Microsoft.AspNetCore.Http.Connections](/en-us/dotnet/api/microsoft.aspnetcore.http.connections) event source.

| Name                    | Description                 |
| ----------------------- | --------------------------- |
| `connections-started`   | Total connections started   |
| `connections-stopped`   | Total connections stopped   |
| `connections-timed-out` | Total connections timed out |
| `current-connections`   | Current connections         |
| `connections-duration`  | Average connection duration |

### Observe metrics

[dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters) is a performance monitoring tool for ad-hoc health monitoring and first-level performance investigation. Monitor a .NET app with `Microsoft.AspNetCore.Http.Connections` as the provider name. For example:

```
> dotnet-counters monitor --process-id 37016 --counters Microsoft.AspNetCore.Http.Connections

Press p to pause, r to resume, q to quit.
    Status: Running
[Microsoft.AspNetCore.Http.Connections]
    Average Connection Duration (ms)       16,040.56
    Current Connections                         1
    Total Connections Started                   8
    Total Connections Stopped                   7
    Total Connections Timed Out                 0
```

## Related content

* [ASP.NET Core SignalR configuration](configuration?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-09

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-azure-active-directory-b2c
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/hosted-with-azure-active-directory-b2c.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure a hosted ASP.NET Core Blazor WebAssembly app with Azure Active Directory B2C

Important

The Hosted Blazor WebAssembly project template was removed from the framework with the release of .NET 8 (November, 2023). The guidance in this article is only supported for .NET 7 or earlier. Hosted Blazor WebAssembly apps that are upgraded each release continue to receive product support. Alternatively, refactor the app into either a standalone Blazor WebAssembly app or a Blazor Web App.

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

This article explains how to create a [hosted Blazor WebAssembly solution](../../hosting-models?view=aspnetcore-7.0#blazor-webassembly) that uses [Azure Active Directory (AAD) B2C](/en-us/azure/active-directory-b2c/overview) for authentication.

* Modify the default access token scope scheme
* Run the app

### Create a tenant in Azure

Follow the guidance in [Tutorial: Create an Azure Active Directory B2C tenant](/en-us/azure/active-directory-b2c/tutorial-create-tenant) to create an AAD B2C tenant.

Before proceeding with this article's guidance, confirm that you've [selected the correct directory for the AAD B2C tenant](/en-us/azure/active-directory-b2c/tutorial-create-tenant#select-your-b2c-tenant-directory).

### Register a server API app in Azure

Register an AAD B2C app for the *Server API app*:

1. Navigate to **Azure AD B2C** in the Azure portal. Select **App registrations** in the sidebar. Select the **New registration** button.
2. Provide a **Name** for the app (for example, **Blazor Server AAD B2C**).
3. For **Supported account types**, select the multi-tenant option: **Accounts in any identity provider or organizational directory (for authenticating users with user flows)**
4. The *Server API app* doesn't require a **Redirect URI** in this scenario, so leave the **Select a platform** dropdown list unselected and don't enter a redirect URI.
5. Confirm that **Permissions** > **Grant admin consent to openid and offline\_access permissions** is selected.
6. Select **Register**.

Record the following information:

* *Server API app* Application (client) ID (for example, `00001111-aaaa-2222-bbbb-3333cccc4444`)
* AAD B2C instance (for example, `https://contoso.b2clogin.com/`, which includes the trailing slash). The instance is the scheme and host of an Azure B2C app registration, which can be found by opening the **Endpoints** window from the **App registrations** page in the Azure portal.
* Primary/Publisher/Tenant domain (for example, `contoso.onmicrosoft.com`): The domain is available as the **Publisher domain** in the **Branding** blade of the Azure portal for the registered app.

Select **Expose an API** from the sidebar and follow these steps:

1. Select **Add a scope**.
2. Select **Save and continue**.
3. Provide a **Scope name** (for example, `API.Access`).
4. Provide an **Admin consent display name** (for example, `Access API`).
5. Provide an **Admin consent description** (for example, `Allows the app to access server app API endpoints.`).
6. Confirm that the **State** is set to **Enabled**.
7. Select **Add scope**.

Record the following information:

* App ID URI GUID (for example, record `00001111-aaaa-2222-bbbb-3333cccc4444` from `https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444`)
* Scope name (for example, `API.Access`)

### Register a client app in Azure

Register an AAD B2C app for the *Client app*:

1. Navigate to **Azure AD B2C** in the Azure portal. Select **App registrations** in the sidebar. Select the **New registration** button.
2. Provide a **Name** for the app (for example, **Blazor Client AAD B2C**).
3. For **Supported account types**, select the multi-tenant option: **Accounts in any identity provider or organizational directory (for authenticating users with user flows)**
4. Set the **Redirect URI** dropdown list to **Single-page application (SPA)** and provide a redirect URI value of `https://localhost/authentication/login-callback`. If you know the production redirect URI for the Azure default host (for example, `azurewebsites.net`) or the custom domain host (for example, `contoso.com`), you can also add the production redirect URI at the same time that you're providing the `localhost` redirect URI. Be sure to include the port number for non-`:443` ports in any production redirect URIs that you add.
5. Confirm that **Permissions** > **Grant admin consent to openid and offline\_access permissions** is selected.
6. Select **Register**.

Note

Supplying the port number for a `localhost` AAD B2C redirect URI isn't required. For more information, see [Redirect URI (reply URL) restrictions and limitations: Localhost exceptions (Entra documentation)](/en-us/entra/identity-platform/reply-url#localhost-exceptions).

Record the *Client app* Application (client) ID (for example, `11112222-bbbb-3333-cccc-4444dddd5555`).

In **Authentication** > **Platform configurations** > **Single-page application**:

1. Confirm the redirect URI of `https://localhost/authentication/login-callback` is present.
2. In the **Implicit grant** section, ensure that the checkboxes for **Access tokens** and **ID tokens** aren't selected. **Implicit grant isn't recommended for Blazor apps using MSAL v2.0 or later.** For more information, see [Secure ASP.NET Core Blazor WebAssembly](./?view=aspnetcore-7.0#use-the-authorization-code-flow-with-pkce).
3. The remaining defaults for the app are acceptable for this experience.
4. Select the **Save** button if you made changes.

In **API permissions** from the sidebar:

1. Select **Add a permission** followed by **My APIs**.
2. Select the *Server API app* from the **Name** column (for example, **Blazor Server AAD B2C**). You must be an owner of the app registration (and the API app registration if it's a separate app) in order to see the API in the **My APIs** area of the Azure portal. For more information, see [Assign application owner (Microsoft Entra documentation)](/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis#assign-application-owner).
3. Open the **API** list if it isn't already open.
4. Enable access to the API (for example, `API.Access`) with the checkbox.
5. Select **Add permissions**.
6. Select the **Grant admin consent for {TENANT NAME}** button. Select **Yes** to confirm.

Important

If you don't have the authority to grant admin consent to the tenant in the last step of **API permissions** configuration because consent to use the app is delegated to users, then you must take the following additional steps:

* The app must use a [trusted publisher domain](/en-us/entra/identity-platform/howto-configure-publisher-domain).
* In the **`Server`** app's configuration in the Azure portal, select **Expose an API**. Under **Authorized client applications**, select the button to **Add a client application**. Add the **`Client`** app's Application (client) ID (for example, `11112222-bbbb-3333-cccc-4444dddd5555`).

Return to **Azure AD B2C** in the Azure portal. Select **User flows** and use the following guidance: [Create a sign-up and sign-in user flow](/en-us/azure/active-directory-b2c/tutorial-create-user-flows). At a minimum, select **Application claims** for the sign-up/sign-in user flow and then the **Display Name** user attribute checkbox to populate the `context.User.Identity?.Name`/`context.User.Identity.Name` in the `LoginDisplay` component (`Shared/LoginDisplay.razor`).

Record the sign-up and sign-in user flow name created for the app (for example, `B2C_1_signupsignin1`).

### Create the Blazor app

Replace the placeholders in the following command with the information recorded earlier and execute the command in a command shell:

```
dotnet new blazorwasm -au IndividualB2C --aad-b2c-instance "{AAD B2C INSTANCE}" --api-client-id "{SERVER API APP CLIENT ID}" --app-id-uri "{SERVER API APP ID URI GUID}" --client-id "{CLIENT APP CLIENT ID}" --default-scope "{DEFAULT SCOPE}" --domain "{TENANT DOMAIN}" -ho -o {PROJECT NAME} -ssp "{SIGN UP OR SIGN IN POLICY}"
```

Warning

**Avoid using dashes (`-`) in the app name `{PROJECT NAME}` that break the formation of the OIDC app identifier.** Logic in the Blazor WebAssembly project template uses the project name for an OIDC app identifier in the solution's configuration. Pascal case (`BlazorSample`) or underscores (`Blazor_Sample`) are acceptable alternatives. For more information, see [Dashes in a hosted Blazor WebAssembly project name break OIDC security (dotnet/aspnetcore #35337)](https://github.com/dotnet/aspnetcore/issues/35337).

| Placeholder                    | Azure portal name                              | Example                                                                                      |
| ------------------------------ | ---------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `{AAD B2C INSTANCE}`           | Instance                                       | `https://contoso.b2clogin.com/` (includes the trailing slash)                                |
| `{PROJECT NAME}`               | —                                              | `BlazorSample`                                                                               |
| `{CLIENT APP CLIENT ID}`       | Application (client) ID for the **Client** app | `11112222-bbbb-3333-cccc-4444dddd5555`                                                       |
| `{DEFAULT SCOPE}`              | Scope name                                     | `API.Access`                                                                                 |
| `{SERVER API APP CLIENT ID}`   | Application (client) ID for the **Server** app | `00001111-aaaa-2222-bbbb-3333cccc4444`                                                       |
| `{SERVER API APP ID URI GUID}` | Application ID URI GUID                        | `00001111-aaaa-2222-bbbb-3333cccc4444` (GUID ONLY, matches the `{SERVER API APP CLIENT ID}`) |
| `{SIGN UP OR SIGN IN POLICY}`  | Sign-up/sign-in user flow                      | `B2C_1_signupsignin1`                                                                        |
| `{TENANT DOMAIN}`              | Primary/Publisher/Tenant domain                | `contoso.onmicrosoft.com`                                                                    |

The output location specified with the `-o|--output` option creates a project folder if it doesn't exist and becomes part of the project's name. **Avoid using dashes (`-`) in the app name that break the formation of the OIDC app identifier (see the earlier WARNING).**

## Custom policies

The Microsoft Authentication Library ([Microsoft.Authentication.WebAssembly.Msal](/en-us/dotnet/api/microsoft.authentication.webassembly.msal), [NuGet package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal/)) doesn't support [AAD B2C custom policies](/en-us/azure/active-directory-b2c/user-flow-overview).

## Configure `User.Identity.Name`

*The guidance in this section covers optionally populating `User.Identity.Name` with the value from the `name` claim.*

The **Server** app API populates `User.Identity.Name` with the value from the `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name` claim type (for example, `aaaabbbb-0000-cccc-1111-dddd2222eeee@contoso.onmicrosoft.com`).

To configure the app to receive the value from the `name` claim type:

* Add a namespace for [Microsoft.AspNetCore.Authentication.JwtBearer](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer) to the `Program` file:

  ```
  using Microsoft.AspNetCore.Authentication.JwtBearer;
  ```

* Configure the [TokenValidationParameters.NameClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.nameclaimtype#microsoft-identitymodel-tokens-tokenvalidationparameters-nameclaimtype) of the [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions) in the `Program` file:

  ```
  builder.Services.Configure<JwtBearerOptions>(
      JwtBearerDefaults.AuthenticationScheme, options =>
      {
          options.TokenValidationParameters.NameClaimType = "name";
      });
  ```

  ```
  @using Microsoft.Extensions.Options
  @inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> RemoteOptions
  ```

  Modify the component's redirect in the `OnInitialized` method:

  ```
  - Navigation.NavigateToLogin("authentication/login");
  + Navigation.NavigateToLogin(RemoteOptions.Get(Options.DefaultName)
  +     .AuthenticationPaths.LogInPath);
  ```

  Note

  If other paths differ from the project template's paths or [framework's default paths](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationDefaults.cs), manage them in the same fashion.

## Parts of the solution

This section describes the parts of a solution generated from the Blazor WebAssembly project template and describes how the solution's **Client** and **Server** projects are configured for reference. There's no specific guidance to follow in this section for a basic working application if you created the app using the guidance in the [Walkthrough](#walkthrough) section. The guidance in this section is helpful for updating an app to authenticate and authorize users. However, an alternative approach to updating an app is to create a new app from the guidance in the [Walkthrough](#walkthrough) section and moving the app's components, classes, and resources to the new app.

### `appsettings.json` configuration

*This section pertains to the solution's **Server** app.*

The `appsettings.json` file contains the options to configure the JWT bearer handler used to validate access tokens:

```
{
  "AzureAdB2C": {
    "Instance": "https://{TENANT}.b2clogin.com/",
    "ClientId": "{SERVER API APP CLIENT ID}",
    "Domain": "{TENANT DOMAIN}",
    "Scopes": "{DEFAULT SCOPE}",
    "SignUpSignInPolicyId": "{SIGN UP OR SIGN IN POLICY}"
  }
}
```

Example:

```
{
  "AzureAdB2C": {
    "Instance": "https://contoso.b2clogin.com/",
    "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
    "Domain": "contoso.onmicrosoft.com",
    "Scopes": "API.Access",
    "SignUpSignInPolicyId": "B2C_1_signupsignin1",
  }
}
```

### Authentication package

*This section pertains to the solution's **Server** app.*

The support for authenticating and authorizing calls to ASP.NET Core web APIs with the Microsoft identity platform is provided by the [`Microsoft.Identity.Web`](https://www.nuget.org/packages/Microsoft.Identity.Web) package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

The **Server** app of a hosted Blazor solution created from the Blazor WebAssembly template includes the [`Microsoft.Identity.Web.UI`](https://www.nuget.org/packages/Microsoft.Identity.Web) package. The package adds UI for user authentication in web apps and isn't used by the Blazor framework. If the **Server** app won't be used to authenticate users directly, it's safe to remove the package reference from the **Server** app's project file.

### Authentication service support

*This section pertains to the solution's **Server** app.*

The `AddAuthentication` method sets up authentication services within the app and configures the JWT bearer handler as the default authentication method. The [AddMicrosoftIdentityWebApi](/en-us/dotnet/api/microsoft.identity.web.microsoftidentitywebapiauthenticationbuilderextensions.addmicrosoftidentitywebapi) method configures services to protect the web API with Microsoft identity platform v2.0. This method expects an `AzureAdB2C` section in the app's configuration with the necessary settings to initialize authentication options.

```
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAdB2C"));
```

Note

When a single authentication scheme is registered, the authentication scheme is automatically used as the app's default scheme, and it isn't necessary to state the scheme to [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) or via [AuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions). For more information, see [Overview of ASP.NET Core Authentication](../../../security/authentication/?view=aspnetcore-7.0#defaultscheme) and the [ASP.NET Core announcement (aspnet/Announcements #490)](https://github.com/aspnet/Announcements/issues/490).

[UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) ensure that:

* The app attempts to parse and validate tokens on incoming requests.
* Any request attempting to access a protected resource without proper credentials fails.

```
app.UseAuthorization();
```

### WeatherForecast controller

*This section pertains to the solution's **Server** app.*

The `WeatherForecast` controller (`Controllers/WeatherForecastController.cs`) exposes a protected API with the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) applied to the controller. It's **important** to understand that:

* The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) in this API controller is the only thing that protects this API from unauthorized access.
* The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) used in the Blazor WebAssembly app only serves as a hint to the app that the user should be authorized for the app to work correctly.

```
[Authorize]
[ApiController]
[Route("[controller]")]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAdB2C:Scopes")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        ...
    }
}
```

### `wwwroot/appsettings.json` configuration

*This section pertains to the solution's **Client** app.*

Configuration is supplied by the `wwwroot/appsettings.json` file:

```
{
  "AzureAdB2C": {
    "Authority": "{AAD B2C INSTANCE}{TENANT DOMAIN}/{SIGN UP OR SIGN IN POLICY}",
    "ClientId": "{CLIENT APP CLIENT ID}",
    "ValidateAuthority": false
  }
}
```

In the preceding configuration, the `{AAD B2C INSTANCE}` includes a trailing slash.

Example:

```
{
  "AzureAdB2C": {
    "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_signupsignin1",
    "ClientId": "11112222-bbbb-3333-cccc-4444dddd5555",
    "ValidateAuthority": false
  }
}
```

### Authentication package

*This section pertains to the solution's **Client** app.*

When an app is created to use an Individual B2C Account (`IndividualB2C`), the app automatically receives a package reference for the [Microsoft Authentication Library](/en-us/entra/identity-platform/msal-overview) ([`Microsoft.Authentication.WebAssembly.Msal`](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal)). The package provides a set of primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the [`Microsoft.Authentication.WebAssembly.Msal`](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal) package to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

The [`Microsoft.Authentication.WebAssembly.Msal`](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal) package transitively adds the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) package to the app.

### Authentication service support

*This section pertains to the solution's **Client** app.*

Support for [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances is added that include access tokens when making requests to the server project.

In the `Program` file:

```
builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI", client => 
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("{PROJECT NAME}.ServerAPI"));
```

The `{PROJECT NAME}` placeholder is the project name at solution creation. For example, providing a project name of `BlazorSample` produces a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) of `BlazorSample.ServerAPI`.

Support for authenticating users is registered in the service container with the [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) extension method provided by the [`Microsoft.Authentication.WebAssembly.Msal`](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal) package. This method sets up the services required for the app to interact with the Identity Provider (IP).

In the `Program` file:

```
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
```

The `{SCOPE URI}` is the default access token scope (for example, `https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444/API.Access` or the custom URI that you configured in the Azure portal).

The [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) method accepts a callback to configure the parameters required to authenticate an app. The values required for configuring the app can be obtained from the Azure portal AAD configuration when you register the app.

### Access token scopes

*This section pertains to the solution's **Client** app.*

The default access token scopes represent the list of access token scopes that are:

* Included in the sign in request.
* Used to provision an access token immediately after authentication.

All scopes must belong to the same app per Microsoft Entra ID rules. Additional scopes can be added for additional API apps as needed:

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
```

Specify additional scopes with `AdditionalScopesToConsent`:

```
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE URI}");
```

Note

[AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent) isn't able to provision delegated user permissions for Microsoft Graph via the Microsoft Entra ID consent UI when a user first uses an app registered in Microsoft Azure. For more information, see [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-7.0\&pivots=graph-sdk-5#defaultaccesstokenscopes-versus-additionalscopestoconsent).

Example default access token scope:

```
options.ProviderOptions.DefaultAccessTokenScopes.Add(
    "https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444/API.Access");
```

For more information, see the following sections of the *Additional scenarios* article:

* [Request additional access tokens](additional-scenarios?view=aspnetcore-7.0#request-additional-access-tokens)
* [Attach tokens to outgoing requests](additional-scenarios?view=aspnetcore-7.0#attach-tokens-to-outgoing-requests)

### Login mode

*This section pertains to the solution's **Client** app.*

The framework defaults to pop-up login mode and falls back to redirect login mode if a pop-up can't be opened. Configure MSAL to use redirect login mode by setting the `LoginMode` property of [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions) to `redirect`:

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.LoginMode = "redirect";
});
```

The default setting is `popup`, and the string value isn't case-sensitive.

### Imports file

*This section pertains to the solution's **Client** app.*

The [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespace is made available throughout the app via the imports file (`_Imports.razor`):

```
...
@using Microsoft.AspNetCore.Components.Authorization
...
```

### Index page

*This section pertains to the solution's **Client** app.*

The Index page (`wwwroot/index.html`) page includes a script that defines the `AuthenticationService` in JavaScript. `AuthenticationService` handles the low-level details of the OIDC protocol. The app internally calls methods defined in the script to perform the authentication operations.

```
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
```

```
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string? Action { get; set; }
}
```

```
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using {APP NAMESPACE}.Shared
@attribute [Authorize]
@inject HttpClient Http

...

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}
```

    * [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)
    * [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)
    * [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

  > info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

```
@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthenticationState { get; set; }

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}
```

### Inspect the content of a JSON Web Token (JWT)

To decode a JSON Web Token (JWT), use Microsoft's [jwt.ms](https://jwt.ms/) tool. Values in the UI never leave your browser.

Example encoded JWT (shortened for display):

> eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V\_bU55Ug\_PW4pLPr20tTS8Ct7\_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2\_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

```
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
  "sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]
```

## Related content

* [Configure an app's publisher domain](/en-us/entra/identity-platform/howto-configure-publisher-domain)
* [Microsoft Entra ID app manifest: identifierUris attribute](/en-us/entra/identity-platform/reference-app-manifest#identifieruris-attribute)
* [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-7.0)
* [Build a custom version of the Authentication.MSAL JavaScript library](additional-scenarios?view=aspnetcore-7.0#build-a-custom-version-of-the-authenticationmsal-javascript-library)
* [Unauthenticated or unauthorized web API requests in an app with a secure default client](additional-scenarios?view=aspnetcore-7.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client)
* [Cloud authentication with Azure Active Directory B2C in ASP.NET Core](../../../security/authentication/azure-ad-b2c?view=aspnetcore-7.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-11-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/virtualization
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/virtualization.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Razor component virtualization

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to use component virtualization in ASP.NET Core Blazor apps.

## Virtualization

Improve the perceived performance of component rendering using the Blazor framework's built-in virtualization support with the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component. Virtualization is a technique for limiting UI rendering to just the parts that are currently visible. For example, virtualization is helpful when the app must render a long list of items and only a subset of items is required to be visible at any given time.

Use the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component when:

* Rendering a set of data items in a loop.
* Most of the items aren't visible due to scrolling.

When the user scrolls to an arbitrary point in the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component's list of items, the component calculates the visible items to show. Unseen items aren't rendered.

Without virtualization, a typical list might use a C# [`foreach`](/en-us/dotnet/csharp/language-reference/keywords/foreach-in) loop to render each item in a list. In the following example:

* `allFlights` is a collection of airplane flights.
* The `FlightSummary` component displays details about each flight.
* The [`@key` directive attribute](element-component-model-relationships?view=aspnetcore-10.0) preserves the relationship of each `FlightSummary` component to its rendered flight by the flight's `FlightId`.

```
<div style="height:500px;overflow-y:scroll">
    @foreach (var flight in allFlights)
    {
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    }
</div>
```

If the collection contains thousands of flights, rendering the flights takes a long time and users experience a noticeable UI lag. Most of the flights fall outside of the height of the `<div>` element, so most of them aren't seen.

Instead of rendering the entire list of flights at once, replace the [`foreach`](/en-us/dotnet/csharp/language-reference/keywords/foreach-in) loop in the preceding example with the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component:

* Specify `allFlights` as a fixed item source to [Virtualize\<TItem>.Items](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.items). Only the currently visible flights are rendered by the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component.

  If a non-generic collection supplies the items, for example a collection of [DataRow](/en-us/dotnet/api/system.data.datarow), follow the guidance in the [Item provider delegate](#item-provider-delegate) section to supply the items.

* Specify a context for each flight with the `Context` parameter. In the following example, `flight` is used as the context, which provides access to each flight's members.

```
<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights" Context="flight">
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    </Virtualize>
</div>
```

If a context isn't specified with the `Context` parameter, use the value of `context` in the item content template to access each flight's members:

```
<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights">
        <FlightSummary @key="context.FlightId" Details="@context.Summary" />
    </Virtualize>
</div>
```

The [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component:

* Calculates the number of items to render based on the height of the container and the size of the rendered items.
* Recalculates and rerenders the items as the user scrolls.
* Only fetches the slice of records from an external API that correspond to the currently visible region, including overscan, when `ItemsProvider` is used instead of `Items` (see the [Item provider delegate](#item-provider-delegate) section).

The item content for the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component can include:

* Plain HTML and Razor code, as the preceding example shows.
* One or more Razor components.
* A mix of HTML/Razor and Razor components.

## Item provider delegate

If you don't want to load all of the items into memory or the collection isn't a generic [ICollection\<T>](/en-us/dotnet/api/system.collections.generic.icollection-1), you can specify an items provider delegate method to the component's [Virtualize\<TItem>.ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.itemsprovider) parameter that asynchronously retrieves the requested items on demand. In the following example, the `LoadEmployees` method provides the items to the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component:

```
<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <p>
        @employee.FirstName @employee.LastName has the 
        job title of @employee.JobTitle.
    </p>
</Virtualize>
```

The items provider receives an [ItemsProviderRequest](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.itemsproviderrequest), which specifies the required number of items starting at a specific start index. The items provider then retrieves the requested items from a database or other service and returns them as an [ItemsProviderResult\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.itemsproviderresult-1) along with a count of the total items. The items provider can choose to retrieve the items with each request or cache them so that they're readily available.

A [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component can only accept **one item source** from its parameters, so don't attempt to simultaneously use an items provider and assign a collection to `Items`. If both are assigned, an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) is thrown when the component's parameters are set at runtime.

The following example loads employees from an `EmployeeService` (not shown). The `totalEmployees` field would typically be assigned by calling a method on the same service (for example, `EmployeesService.GetEmployeesCountAsync`) elsewhere, such as during component initialization.

```
private async ValueTask<ItemsProviderResult<Employee>> LoadEmployees(
    ItemsProviderRequest request)
{
    var numEmployees = Math.Min(request.Count, totalEmployees - request.StartIndex);
    var employees = await EmployeesService.GetEmployeesAsync(request.StartIndex, 
        numEmployees, request.CancellationToken);

    return new ItemsProviderResult<Employee>(employees, totalEmployees);
}
```

In the following example, a collection of [DataRow](/en-us/dotnet/api/system.data.datarow) is a non-generic collection, so an items provider delegate is used for virtualization:

```
<Virtualize Context="row" ItemsProvider="GetRows">
    ...
</Virtualize>

@code{
    ...

    private ValueTask<ItemsProviderResult<DataRow>> GetRows(ItemsProviderRequest request) => 
        new(new ItemsProviderResult<DataRow>(
            dataTable.Rows.OfType<DataRow>().Skip(request.StartIndex).Take(request.Count),
            dataTable.Rows.Count));
}
```

[Virtualize\<TItem>.RefreshDataAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.refreshdataasync) instructs the component to rerequest data from its [ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.itemsprovider). This is useful when external data changes. There's usually no need to call [RefreshDataAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.refreshdataasync) when using [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.items).

[RefreshDataAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.refreshdataasync) updates a [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component's data without causing a rerender. If [RefreshDataAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.refreshdataasync) is invoked from a Blazor event handler or component lifecycle method, triggering a render isn't required because a render is automatically triggered at the end of the event handler or lifecycle method. If [RefreshDataAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.refreshdataasync) is triggered separately from a background task or event, such as in the following `ForecastUpdated` delegate, call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) to update the UI at the end of the background task or event:

```
<Virtualize ... @ref="virtualizeComponent">
    ...
</Virtualize>

...

private Virtualize<FetchData>? virtualizeComponent;

protected override void OnInitialized()
{
    WeatherForecastSource.ForecastUpdated += async () => 
    {
        await InvokeAsync(async () =>
        {
            await virtualizeComponent?.RefreshDataAsync();
            StateHasChanged();
        });
    });
}
```

In the preceding example:

* [RefreshDataAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.refreshdataasync) is called first to obtain new data for the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component.
* `StateHasChanged` is called to rerender the component.

## Placeholder

Because requesting items from a remote data source might take some time, you have the option to render a placeholder with item content:

* Use a [Placeholder](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.placeholder) (`<Placeholder>...</Placeholder>`) to display content until the item data is available.
* Use [Virtualize\<TItem>.ItemContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.itemcontent) to set the item template for the list.

```
<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <ItemContent>
        <p>
            @employee.FirstName @employee.LastName has the 
            job title of @employee.JobTitle.
        </p>
    </ItemContent>
    <Placeholder>
        <p>
            Loading&hellip;
        </p>
    </Placeholder>
</Virtualize>
```

## Empty content

Use the [EmptyContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.emptycontent#microsoft-aspnetcore-components-web-virtualization-virtualize-1-emptycontent) parameter to supply content when the component has loaded and either [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.items) is empty or [ItemsProviderResult\<TItem>.TotalItemCount](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.itemsproviderresult-1.totalitemcount) is zero.

`EmptyContent.razor`:

```
@page "/empty-content"

<PageTitle>Empty Content</PageTitle>

<h1>Empty Content Example</h1>

<Virtualize Items="stringList">
    <ItemContent>
        <p>
            @context
        </p>
    </ItemContent>
    <EmptyContent>
        <p>
            There are no strings to display.
        </p>
    </EmptyContent>
</Virtualize>

@code {
    private List<string>? stringList;

    protected override void OnInitialized() => stringList ??= [];
}
```

```
@page "/empty-content"

<PageTitle>Empty Content</PageTitle>

<h1>Empty Content Example</h1>

<Virtualize Items="stringList">
    <ItemContent>
        <p>
            @context
        </p>
    </ItemContent>
    <EmptyContent>
        <p>
            There are no strings to display.
        </p>
    </EmptyContent>
</Virtualize>

@code {
    private List<string>? stringList;

    protected override void OnInitialized() => stringList ??= [];
}
```

Change the `OnInitialized` method lambda to see the component display strings:

```
protected override void OnInitialized() =>
    stringList ??= [ "Here's a string!", "Here's another string!" ];
```

## Item size

The height of each item in pixels can be set initially with [Virtualize\<TItem>.ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.itemsize) (default: 50). The following example sets the initial height of each item from 50 pixels to 25 pixels:

```
<Virtualize Context="employee" Items="employees" ItemSize="25">
    ...
</Virtualize>
```

The [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component measures actual item heights as they enter the viewport and maintains a running average of measured heights. All items use this running average for positioning (or the default [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.itemsize) parameter before any measurements exist).

The height of each item in pixels can be set with [Virtualize\<TItem>.ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.itemsize) (default: 50). The following example changes the height of each item from the default of 50 pixels to 25 pixels:

```
<Virtualize Context="employee" Items="employees" ItemSize="25">
    ...
</Virtualize>
```

The [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component measures the rendering size (height) of individual items *after* the initial render occurs. Use [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.itemsize) to provide an exact item size in advance to assist with accurate initial render performance and to ensure the correct scroll position for page reloads. If the default [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.itemsize) causes some items to render outside of the currently visible view, a second rerender is triggered. To correctly maintain the browser's scroll position in a virtualized list, the initial render must be correct. If not, users might view the wrong items.

## Overscan count

[Virtualize\<TItem>.OverscanCount](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.overscancount) determines how many additional items are rendered before and after the visible region. This setting helps to reduce the frequency of rendering during scrolling. However, higher values result in more elements rendered in the page (default: 15). The following example changes the overscan count from the default of 15 items to 17 items:

```
<Virtualize Context="employee" Items="employees" OverscanCount="17">
    ...
</Virtualize>
```

[Virtualize\<TItem>.OverscanCount](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.overscancount) determines how many additional items are rendered before and after the visible region. This setting helps to reduce the frequency of rendering during scrolling. However, higher values result in more elements rendered in the page (default: 3). The following example changes the overscan count from the default of three items to four items:

```
<Virtualize Context="employee" Items="employees" OverscanCount="4">
    ...
</Virtualize>
```

## Scroll to a specific item

The [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component provides two ways to control scroll position: `InitialItemIndex` for the first render and `ScrollToItemAsync` for programmatic scrolling after the component is rendered.

### `InitialItemIndex` parameter

Set `InitialItemIndex` to open the list at a specific item index on the first interactive render. This is a one-shot parameter—changes after first render are ignored. Out-of-range values are clamped.

```
<Virtualize Items="allFlights" Context="flight" InitialItemIndex="500">
    <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
</Virtualize>
```

### `ScrollToItemAsync` method

Call `ScrollToItemAsync` to programmatically scroll to an item after the first render. The scroll is instant (no animation). The method returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) that completes when the target item is aligned to the top of the viewport. Cancellation is supported via a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken).

If multiple calls occur, the last call wins—earlier calls complete normally but only the final target is honored. If the user scrolls during a programmatic scroll, the user's scroll takes precedence. Calling before the first interactive render throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception).

```
<Virtualize Items="allFlights" Context="flight" @ref="virtualizeComponent">
    <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
</Virtualize>

<button @onclick="ScrollToFlight">Go to flight 200</button>

@code {
    private Virtualize<Flight>? virtualizeComponent;

    private async Task ScrollToFlight()
    {
        if (virtualizeComponent is not null)
        {
            await virtualizeComponent.ScrollToItemAsync(200);
        }
    }
}
```

## Control viewport scroll position behavior when items are dynamically added

Assign a `VirtualizeAnchorMode` value to the `AnchorMode` parameter to control how the viewport behaves at list edges when items are dynamically added:

* `None`: No edge pinning. The viewport stays at the current scroll position regardless of item changes.
* `Start`: Pins the viewport to the start of the list. When the user is at a scroll position near the top of the list and new items arrive at the start, the viewport stays at the top showing the newest items. For example, this pinning behavior is useful for a news feed user experience.
* `End`: Pins the viewport to the end of the list. When the user is at a scroll position near the bottom of the list and new items arrive at the end, the viewport auto-scrolls to show them. If the user has scrolled away, auto-scroll disengages until they return to the bottom. For example, this pinning behavior is useful for a chat or logging user experience.

The following example pins the viewport to the start of a virtualized flight list:

```
<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights" Context="flight" AnchorMode="Start">
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    </Virtualize>
</div>
```

Modes can be combined. For example, assigning `Start | End` pins both edges. Combining `None` with other modes is supported but doesn't change the combined value.

`Virtualize.ItemComparer` gets or sets a comparer used to detect whether items were prepended or appended when using class-typed items with an [ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.itemsprovider) (for more information, see the [Item provider delegate](#item-provider-delegate) section).

The comparer determines if the first loaded item changed between provider calls, which indicates items were inserted at the top. For records, the default comparer's value-equality behavior (`EqualityComparer<T>.Default`) works automatically. For an in-memory [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.items) assignment, an `ItemComparer` comparer isn't required because the component can detect prepends automatically. In cases where non-primative objects are virtualized and the framework can't detect if an item is prepended or appended, assign an [IEqualityComparer\<T>](/en-us/dotnet/api/system.collections.generic.iequalitycomparer-1) to the `Virtualize` component:

```
<Virtualize ItemsProvider="LoadFlights" AnchorMode="Start" 
    ItemComparer="itemComparer">
    ...
</Virtualize>

@code {
    private static readonly IEqualityComparer<Flight> itemComparer =
        EqualityComparer<Flight>.Create((a, b) => 
            a.Index == b.Index, item => item.Index);

    private async ValueTask<ItemsProviderResult<Flight>> LoadFlights(
        ItemsProviderRequest request)
    {
        ...
    }
}
```

## State changes

When making changes to items rendered by the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component, call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) to enqueue re-evaluation and rerendering of the component. For more information, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0).

## Keyboard scroll support

To allow users to scroll virtualized content using their keyboard, ensure that the virtualized elements or scroll container itself is focusable. If you fail to take this step, keyboard scrolling doesn't work in Chromium-based browsers.

For example, you can use a `tabindex` attribute on the scroll container:

```
<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights">
        <div class="flight-info">...</div>
    </Virtualize>
</div>
```

To learn more about the meaning of `tabindex` value `-1`, `0`, or other values, see [`tabindex`](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/tabindex).

## Advanced styles and scroll detection

The [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component is only designed to support specific element layout mechanisms. To understand which element layouts work correctly, the following explains how `Virtualize` detects which elements should be visible for display in the correct place.

If your source code looks like the following:

```
<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights" ItemSize="100">
        <div class="flight-info">Flight @context.Id</div>
    </Virtualize>
</div>
```

At runtime, the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component renders a DOM structure similar to the following:

```
<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <div data-blazor-virtualize-reserved-height="1100" aria-hidden="true"></div>
    <div class="flight-info">Flight 12</div>
    <div class="flight-info">Flight 13</div>
    <div class="flight-info">Flight 14</div>
    <div class="flight-info">Flight 15</div>
    <div class="flight-info">Flight 16</div>
    <div data-blazor-virtualize-reserved-height="3400" aria-hidden="true"></div>
</div>
```

```
<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <div style="height:1100px"></div>
    <div class="flight-info">Flight 12</div>
    <div class="flight-info">Flight 13</div>
    <div class="flight-info">Flight 14</div>
    <div class="flight-info">Flight 15</div>
    <div class="flight-info">Flight 16</div>
    <div style="height:3400px"></div>
</div>
```

The actual number of rows rendered and the size of the spacers vary according to your styling and `Items` collection size. However, notice that there are spacer `div` elements injected before and after your content. These serve two purposes:

* To provide an offset before and after your content, causing currently-visible items to appear at the correct location in the scroll range and the scroll range itself to represent the total size of all content.
* To detect when the user is scrolling beyond the current visible range, meaning that different content must be rendered.

Note

To learn how to control the spacer HTML element tag, see the [Control the spacer element tag name](#control-the-spacer-element-tag-name) section later in this article.

The spacer elements internally use an [Intersection Observer](https://developer.mozilla.org/docs/Web/API/Intersection_Observer_API) to receive notification when they're becoming visible. `Virtualize` depends on receiving these events.

`Virtualize` works under the following conditions:

* **All rendered content items, including [placeholder content](#placeholder), are of identical height.** This makes it possible to calculate which content corresponds to a given scroll position without first fetching every data item and rendering the data into a DOM element.

* **Both the spacers and the content rows are rendered in a single vertical stack with every item filling the entire horizontal width.** In typical use cases, `Virtualize` works with `div` elements. If you're using CSS to create a more advanced layout, bear in mind the following requirements:

  * Scroll container styling requires a `display` with any of the following values:

    * `block` (the default for a `div`).
    * `table-row-group` (the default for a `tbody`).
    * `flex` with `flex-direction` set to `column`. Ensure that immediate children of the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component don't shrink under flex rules. For example, add `.mycontainer > div { flex-shrink: 0 }`.

  * Content row styling requires a `display` with either of the following values:

    * `block` (the default for a `div`).
    * `table-row` (the default for a `tr`).

  * Don't use CSS to interfere with the layout for the spacer elements. The spacer elements have a `display` value of `block`, except if the parent is a table row group, in which case they default to `table-row`. Don't try to influence spacer element width or height, including by causing them to have a border or `content` pseudo-elements.

Any approach that stops the spacers and content elements from rendering as a single vertical stack, or causes the content items to vary in height, prevents correct functioning of the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component.

## Root-level virtualization

The [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component supports using the document itself as the scroll root, as an alternative to having some other element with `overflow-y: scroll`. In the following example, the `<html>` or `<body>` elements are styled in a component with `overflow-y: scroll`:

```
<HeadContent>
    <style>
        html, body { overflow-y: scroll }
    </style>
</HeadContent>
```

The [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component supports using the document itself as the scroll root, as an alternative to having some other element with `overflow-y: scroll`. When using the document as the scroll root, avoid styling the `<html>` or `<body>` elements with `overflow-y: scroll` because it causes the [intersection observer](#advanced-styles-and-scroll-detection) to treat the full scrollable height of the page as the visible region, instead of just the window viewport.

You can reproduce this problem by creating a large virtualized list (for example, 100,000 items) and attempt to use the document as the scroll root with `html { overflow-y: scroll }` in the page CSS styles. Although it may work correctly at times, the browser attempts to render all 100,000 items at least once at the start of rendering, which may cause a browser tab lockup.

To work around this problem prior to the release of .NET 7, either avoid styling `<html>`/`<body>` elements with `overflow-y: scroll` or adopt an alternative approach. In the following example, the height of the `<html>` element is set to just over 100% of the viewport height:

```
<HeadContent>
    <style>
        html { min-height: calc(100vh + 0.3px) }
    </style>
</HeadContent>
```

The [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component supports using the document itself as the scroll root, as an alternative to having some other element with `overflow-y: scroll`. When using the document as the scroll root, avoid styling the `<html>` or `<body>` elements with `overflow-y: scroll` because it causes the full scrollable height of the page to be treated as the visible region, instead of just the window viewport.

You can reproduce this problem by creating a large virtualized list (for example, 100,000 items) and attempt to use the document as the scroll root with `html { overflow-y: scroll }` in the page CSS styles. Although it may work correctly at times, the browser attempts to render all 100,000 items at least once at the start of rendering, which may cause a browser tab lockup.

To work around this problem prior to the release of .NET 7, either avoid styling `<html>`/`<body>` elements with `overflow-y: scroll` or adopt an alternative approach. In the following example, the height of the `<html>` element is set to just over 100% of the viewport height:

```
<style>
    html { min-height: calc(100vh + 0.3px) }
</style>
```

## Control the spacer element tag name

If the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component is placed inside an element that requires a specific child tag name, [SpacerElement](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1.spacerelement#microsoft-aspnetcore-components-web-virtualization-virtualize-1-spacerelement) allows you to obtain or set the virtualization spacer tag name. The default value is `div`. For the following example, the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component renders inside a table body element ([`tbody`](https://developer.mozilla.org/docs/Web/HTML/Element/tbody)), so the appropriate child element for a table row ([`tr`](https://developer.mozilla.org/docs/Web/HTML/Element/tr)) is set as the spacer.

`VirtualizedTable.razor`:

```
@page "/virtualized-table"

<PageTitle>Virtualized Table</PageTitle>

<HeadContent>
    <style>
        html, body {
            overflow-y: scroll
        }
    </style>
</HeadContent>

<h1>Virtualized Table Example</h1>

<table id="virtualized-table">
    <thead style="position: sticky; top: 0; background-color: silver">
        <tr>
            <th>Item</th>
            <th>Another column</th>
        </tr>
    </thead>
    <tbody>
        <Virtualize Items="fixedItems" ItemSize="30" SpacerElement="tr">
            <tr @key="context" style="height: 30px;" id="row-@context">
                <td>Item @context</td>
                <td>Another value</td>
            </tr>
        </Virtualize>
    </tbody>
</table>

@code {
    private List<int> fixedItems = Enumerable.Range(0, 1000).ToList();
}
```

In the preceding example, the document root is used as the scroll container, so the `html` and `body` elements are styled with `overflow-y: scroll`. For more information, see the following resources:

* [Root-level virtualization](#root-level-virtualization) section
* [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-10.0)

## Content Security Policy (CSP) compliance

CSP violations are avoided because `Virtualize` components:

* Render calculated spacer and placeholder heights as numeric values in `data-blazor-virtualize-reserved-height` attributes.
* When required, render the trailing spacer's vertical offset as a numeric value in a `data-blazor-virtualize-loop-breaker-transform` attribute to hide the spacer.

A JS [`MutationObserver`](https://developer.mozilla.org/docs/Web/API/MutationObserver) validates the attribute values and applies them via the [CSS Object Model (CSSOM)](https://developer.mozilla.org/docs/Web/API/CSS_Object_Model) as pixel-based `height` and `transform` styles.

The `Virtualize` component renders dynamic inline `style` attributes on its spacer elements because spacer heights are calculated at runtime based on scroll position, item count, and average item size, which change on every scroll interaction. To avoid CSP violations, render CSS height in a `data-blazor-virtualize-reserved-height` attribute instead of a `style` attribute, which makes the rendered component compatible with strict [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP) configurations.

In the following example, the height is set to 3,400 pixels:

```
<div data-blazor-virtualize-reserved-height="3400" aria-hidden="true"></div>
```

The `Virtualize` component renders dynamic inline `style` attributes on its spacer elements because spacer heights are calculated at runtime based on scroll position, item count, and average item size, which change on every scroll interaction. Apps are required to relax `style-src` with `'unsafe-inline'` to allow inline styles for the component to function.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-10

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/dependencyinjection
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/dependencyinjection.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Dependency injection in requirement handlers in ASP.NET Core

[Authorization handlers must be registered](policies?view=aspnetcore-10.0#handler-registration) in the service collection during configuration using [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

Suppose you had a repository of rules you wanted to evaluate inside an authorization handler and that repository was registered in the service collection. Authorization resolves and injects that into the constructor.

For example, to use the .NET logging infrastructure, inject [ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory) into the handler, as shown in the following example:

```
public class SampleAuthorizationHandler : AuthorizationHandler<SampleRequirement>
{
    private readonly ILogger _logger;

    public SampleAuthorizationHandler(ILoggerFactory loggerFactory)
        => _logger = loggerFactory.CreateLogger(GetType().FullName);

    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, SampleRequirement requirement)
    {
        _logger.LogInformation("Inside my handler");

        // ...

        return Task.CompletedTask;
    }
}
```

The preceding handler can be registered with any [service lifetime](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes). The following code uses [AddSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addsingleton) to register the preceding handler:

```
builder.Services.AddSingleton<IAuthorizationHandler, SampleAuthorizationHandler>();
```

An instance of the handler is created when the app starts, and DI injects the registered `ILoggerFactory` into its constructor.

Note

Don't register authorization handlers that use Entity Framework (EF) as singletons.

[Authorization handlers must be registered](policies?view=aspnetcore-10.0#handler-registration) in the service collection during configuration using [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

Suppose you had a repository of rules you wanted to evaluate inside an authorization handler and that repository was registered in the service collection. Authorization resolves and injects that into the constructor.

For example, to use the .NET logging infrastructure, inject [ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory) into the handler, as shown in the following example:

```
public class SampleAuthorizationHandler : AuthorizationHandler<SampleRequirement>
{
    private readonly ILogger _logger;

    public SampleAuthorizationHandler(ILoggerFactory loggerFactory)
        => _logger = loggerFactory.CreateLogger(GetType().FullName);

    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, SampleRequirement requirement)
    {
        _logger.LogInformation("Inside my handler");

        // ...

        return Task.CompletedTask;
    }
}
```

The preceding handler can be registered with any [service lifetime](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes). The following code uses [AddSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addsingleton) to register the preceding handler:

```
services.AddSingleton<IAuthorizationHandler, SampleAuthorizationHandler>();
```

An instance of the handler is created when the app starts, and DI injects the registered `ILoggerFactory` into its constructor.

Note

Don't register authorization handlers that use Entity Framework (EF) as singletons.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/publish-to-azure-web-app
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/publish-to-azure-web-app.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Publish an ASP.NET Core SignalR app to Azure App Service

By [Brady Gaster](https://twitter.com/bradygaster)

[Azure App Service](/en-us/azure/app-service/app-service-web-overview) is a [Microsoft cloud computing](https://azure.microsoft.com/) platform service for hosting web apps, including ASP.NET Core.

Note

This article refers to publishing an ASP.NET Core SignalR app from Visual Studio. For more information, see [SignalR service for Azure](https://azure.microsoft.com/services/signalr-service).

## Publish the app

This article covers publishing using the tools in Visual Studio. Visual Studio Code users can use [Azure CLI](/en-us/cli/azure) commands to publish apps to Azure. For more information, see [Publish an ASP.NET Core app to Azure with command line tools](/en-us/azure/app-service/app-service-web-get-started-dotnet).

1. Right-click on the project in **Solution Explorer** and select **Publish**.

2. Confirm that **App Service** and **Create new** are selected in the **Pick a publish target** dialog.

3. Select **Create Profile** from the **Publish** button drop down.

   Enter the information described in the following table in the **Create App Service** dialog and select **Create**.

   | Item               | Description                                          |
   | ------------------ | ---------------------------------------------------- |
   | **Name**           | Unique name of the app.                              |
   | **Subscription**   | Azure subscription that the app uses.                |
   | **Resource Group** | Group of related resources to which the app belongs. |
   | **Hosting Plan**   | Pricing plan for the web app.                        |

4. Select **Azure SignalR Service** in the **Service Dependencies** section. Select the **+** button:

5. In the **Azure SignalR Service** dialog, select **Create a new Azure SignalR Service instance**.

6. Provide a **Name**, **Resource Group**, and **Location**. Return to the **Azure SignalR Service** dialog and select **Add**.

Visual Studio completes the following tasks:

* Creates a Publish Profile containing publish settings.
* Creates an *Azure Web App* with the provided details.
* Publishes the app.
* Launches a browser, which loads the web app.

The format of the app's URL is `{APP SERVICE NAME}.azurewebsites.net`. For example, an app named `SignalRChatApp` has a URL of `https://signalrchatapp.azurewebsites.net`.

If an HTTP *502.2 - Bad Gateway* error occurs when deploying an app that targets a preview .NET release, see [Deploy ASP.NET Core preview release to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service) to resolve it.

## Configure the app in Azure App Service

Note

*This section only applies to apps not using the Azure SignalR Service.*

If the app uses the Azure SignalR Service, the App Service doesn't require the configuration of WebSockets and session affinity, also called Application Request Routing (ARR) affinity, described in this section. Clients connect their WebSockets to the Azure SignalR Service, not directly to the app.

For apps hosted without the Azure SignalR Service, enable:

* [WebSockets](../fundamentals/websockets?view=aspnetcore-10.0) to allow the WebSockets transport to function. The default setting is **Off**.
* [Session affinity (ARR affinity)](https://azure.github.io/AppService/2016/05/16/Disable-Session-affinity-cookie-\(ARR-cookie\)-for-Azure-web-apps.html) to route requests from a user back to the same App Service instance. The default setting is **On**.

1. In the Azure portal, navigate to the web app in **App Services**.
2. Open **Settings** > **Configuration**.
3. Set **Web sockets** to **On**.
4. Verify that **Session affinity** is set to **On**.

## App Service Plan limits

WebSockets and other transports are limited based on the App Service Plan selected. For more information, see the *Azure Cloud Services limits* and *App Service limits* sections of the [Azure subscription and service limits, quotas, and constraints](/en-us/azure/azure-subscription-service-limits#app-service-limits) article.

## Related content

* [What is Azure SignalR Service?](/en-us/azure/azure-signalr/signalr-overview)
* [Overview of ASP.NET Core SignalR](introduction?view=aspnetcore-10.0)
* [Host and deploy ASP.NET Core](../host-and-deploy/?view=aspnetcore-10.0)
* [Publish an ASP.NET Core app to Azure with Visual Studio](../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0)
* [Publish an ASP.NET Core app to Azure with command line tools](/en-us/azure/app-service/app-service-web-get-started-dotnet)
* [Host and deploy ASP.NET Core Preview apps on Azure](../host-and-deploy/azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app/part-4
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/movie-database-app/part-4.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor movie database app (Part 4 - Work with a database)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article is the fourth part of the Blazor movie database app tutorial that teaches you the basics of building an ASP.NET Core Blazor Web App with features to manage a movie database.

This part of the tutorial series focuses on the database context and directly working with the database's schema and data. Seeding the database with data is also covered.

## Secure authentication flow required for production apps

This tutorial uses a local database that doesn't require user authentication. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production Blazor Web Apps, see the following resources:

* [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0) and the following articles in the *Server* security node

## Database context

The database context, `BlazorWebAppMoviesContext`, connects to the database and maps model objects to database records. The database context was created in the second part of this series. The scaffolded database context code appears in the `Program` file:

```
builder.Services.AddDbContextFactory<BlazorWebAppMoviesContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("BlazorWebAppMoviesContext") ?? 
        throw new InvalidOperationException(
            "Connection string 'BlazorWebAppMoviesContext' not found.")));
```

```
builder.Services.AddDbContextFactory<BlazorWebAppMoviesContext>(options =>
    options.UseSqlite(
        builder.Configuration.GetConnectionString("BlazorWebAppMoviesContext") ?? 
        throw new InvalidOperationException(
            "Connection string 'BlazorWebAppMoviesContext' not found.")));
```

```
builder.Services.AddDbContextFactory<BlazorWebAppMoviesContext>(options =>
    options.UseSqlite(
        builder.Configuration.GetConnectionString("BlazorWebAppMoviesContext") ?? 
        throw new InvalidOperationException(
            "Connection string 'BlazorWebAppMoviesContext' not found.")));
```

[AddDbContextFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkservicecollectionextensions.adddbcontextfactory) registers a factory for the given context as a service in the app's service collection.

[UseSqlServer](/en-us/dotnet/api/microsoft.entityframeworkcore.sqlserverdbcontextoptionsextensions.usesqlserver) or [UseSqlite](/en-us/dotnet/api/microsoft.entityframeworkcore.sqlitedbcontextoptionsbuilderextensions.usesqlite) configures the context to connect to either a Microsoft SQL Server or SQLite database. Other providers are available to connect to additional types of databases.

[GetConnectionString](/en-us/dotnet/api/microsoft.extensions.configuration.configurationextensions.getconnectionstring) uses the ASP.NET Core Configuration system to read the `ConnectionStrings` key for the connection string name provided, which in the preceding example is `BlazorWebAppMoviesContext`.

For local development, configuration obtains the database connection string from the app settings file (`appsettings.json`). The `{CONNECTION STRING}` placeholder in the following example is the connection string:

```
"ConnectionStrings": {
  "BlazorWebAppMoviesContext": "{CONNECTION STRING}"
}
```

The following is an example connection string:

> Server=(localdb)\mssqllocaldb;Database=BlazorWebAppMoviesContext-00001111-aaaa-2222-bbbb-3333cccc4444;Trusted\_Connection=True;MultipleActiveResultSets=true

When the app is deployed to a test/staging or production server, securely store the connection string outside of the project's configuration files.

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](../../security/?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

## Database technology

The Visual Studio version of this tutorial uses SQL Server.

SQL Server Express LocalDB is a lightweight version of the SQL Server Express database engine that's targeted for program development. LocalDB starts on demand and runs in user mode, so there's no complex configuration. Master database files (`*.mdf`) are placed in the `C:/Users/{USER}` directory, where the `{USER}` placeholder is the system's user ID.

From the **View** menu, open **SQL Server Object Explorer** (SSOX).

Right-click on the `Movie` table and select **View Designer**:

The **View Designer** opens:

Note the key icon next to `ID`. EF creates a property named `ID` for the primary key.

Right-click on the `Movie` table and select **View Data**:

The table's data opens in a new tab in Visual Studio:

The VS Code version of this tutorial uses [SQLite](https://www.sqlite.org/), which is a public, self-contained, full-featured SQL database engine.

There are many third-party tools you can use to manage and view SQLite databases. The following image shows [DB Browser for SQLite](https://sqlitebrowser.org/):

In this tutorial, EF Core migrations are used. A migration updates the database schema to match changes in the data model. However, migrations can only make changes to the database that the EF Core provider supports. Resources are listed at the end of this article for further reading.

The VS Code version of this tutorial uses [SQLite](https://www.sqlite.org/), which is a public, self-contained, full-featured SQL database engine.

There are many third-party tools you can use to manage and view SQLite databases. The following image shows [DB Browser for SQLite](https://sqlitebrowser.org/):

In this tutorial, EF Core migrations are used. A migration updates the database schema to match changes in the data model. However, migrations can only make changes to the database that the EF Core provider supports. Resources are listed at the end of this article for further reading.

## Seed the database

Seeding code can create a set of records for development testing or even be used to create the initial data for a new production database.

In the `Data` folder, create a new class named `SeedData` with the following code.

`Data/SeedData.cs`:

```
using Microsoft.EntityFrameworkCore;
using BlazorWebAppMovies.Models;

namespace BlazorWebAppMovies.Data;

public class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using var context = new BlazorWebAppMoviesContext(
            serviceProvider.GetRequiredService<
                DbContextOptions<BlazorWebAppMoviesContext>>());

        if (context == null || context.Movie == null)
        {
            throw new NullReferenceException(
                "Null BlazorWebAppMoviesContext or Movie DbSet");
        }

        if (context.Movie.Any())
        {
            return;
        }

        context.Movie.AddRange(
            new Movie
            {
                Title = "Mad Max",
                ReleaseDate = new DateOnly(1979, 4, 12),
                Genre = "Sci-fi (Cyberpunk)",
                Price = 2.51M,
            },
            new Movie
            {
                Title = "The Road Warrior",
                ReleaseDate = new DateOnly(1981, 12, 24),
                Genre = "Sci-fi (Cyberpunk)",
                Price = 2.78M,
            },
            new Movie
            {
                Title = "Mad Max: Beyond Thunderdome",
                ReleaseDate = new DateOnly(1985, 7, 10),
                Genre = "Sci-fi (Cyberpunk)",
                Price = 3.55M,
            },
            new Movie
            {
                Title = "Mad Max: Fury Road",
                ReleaseDate = new DateOnly(2015, 5, 15),
                Genre = "Sci-fi (Cyberpunk)",
                Price = 8.43M,
            },
            new Movie
            {
                Title = "Furiosa: A Mad Max Saga",
                ReleaseDate = new DateOnly(2024, 5, 24),
                Genre = "Sci-fi (Cyberpunk)",
                Price = 13.49M,
            });

        context.SaveChanges();
    }
}
```

A database context instance is obtained from the dependency injection (DI) container. If movies are present, `return` is called to avoid seeding the database. When the database is empty, the [*Mad Max* franchise](https://warnerbros.fandom.com/wiki/Mad_Max_\(franchise\)) (©[Warner Bros. Entertainment](https://www.warnerbros.com/)) movies are seeded.

To execute the seed initializer, add the following code to the `Program` file immediately after the line that builds the app (`var app = builder.Build();`). The [`using` statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) ensures that the database context is disposed after the seeding operation completes.

```
using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}
```

If the database contains records from earlier testing, run the app and delete the entities that you created in the database. Stop the app by closing the browser's window.

If the database contains records from earlier testing, run the app and delete the entities that you created in the database. Stop the app by closing the browser's window and pressing `Shift`+`F5` on the keyboard in VS Code.

If the database contains records from earlier testing, run the app and delete the entities that you created in the database. Stop the app by closing the browser's window and pressing `Ctrl`+`C` (Windows) in the command shell.

When the database is empty, run the app.

Navigate to the movies `Index` page to see the seeded movies:

## Bind a form to a model

Review the `Edit` component (`Components/Pages/MoviePages/Edit.razor`).

When an HTTP GET request is made for the `Edit` component page (for example at the relative URL: `/movies/edit?id=6`):

* The [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) method fetches the movie with an `Id` of `6` from the database and assigns it to the `Movie` property.
* The [EditForm.Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model#microsoft-aspnetcore-components-forms-editform-model) parameter specifies the top-level model object for the form. An edit context is constructed for the form using the assigned model.
* The form is displayed with the values from the movie.

When the `Edit` page is posted to the server, the form values on the page are bound to the `Movie` property because the [`[SupplyParameterFromForm]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromformattribute) is annotated on the `Movie` property:

```
[SupplyParameterFromForm]
private Movie? Movie { get; set; }
```

If the model state has errors when the form is posted, for example if `ReleaseDate` can't be converted into a date, the form is redisplayed with the submitted values. If no model errors exist, the movie is saved using the form's posted values.

## Concurrency exception handling

Examine the `UpdateMovie` method of the `Edit` component (`Components/Pages/MoviePages/Edit.razor`):

```
private async Task UpdateMovie()
{
    using var context = DbFactory.CreateDbContext();
    context.Attach(Movie!).State = EntityState.Modified;

    try
    {
        await context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie!.Id))
        {
            NavigationManager.NotFound();
        }
        else
        {
            throw;
        }
    }

    NavigationManager.NavigateTo("/movies");
}
```

Important

Due to a bug in the Blazor CRUD template, a `return` statement is missing from the `UpdateMovie` method after [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound) is called. The purpose of calling `return` is to ensure the handler (the `UpdateMovie` method) selects only the Not Found outcome, independently of a given database provider synchronously or asynchronously executing [SaveChangesAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechangesasync). We're in the process of updating the `Edit` component template, and this article will be updated when the scaffolder generates the correct code.

After the line that calls [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.notfound), add a `return` statement:

```
return;
```

The `catch` block should look like the following example after the `return` statement is added:

```
catch (DbUpdateConcurrencyException)
{
    if (!MovieExists(Movie!.Id))
    {
        NavigationManager.NotFound();

        return;
    }
    else
    {
        throw;
    }
}
```

Concurrency exceptions are detected when one client deletes the movie and a different client posts changes to the movie.

To test how concurrency is handled by the preceding code:

1. Select **Edit** for a movie, make changes, but don't select **Save**.
2. In a different browser window, open the app to the movie `Index` page and select the **Delete** link for the same movie to delete the movie.
3. In the previous browser window, post changes to the movie by selecting the **Save** button.
4. The browser is navigated to the Not Found page with a 404 (Not Found) status code.

```
private async Task UpdateMovie()
{
    using var context = DbFactory.CreateDbContext();
    context.Attach(Movie!).State = EntityState.Modified;

    try
    {
        await context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie!.Id))
        {
            NavigationManager.NavigateTo("notfound");
        }
        else
        {
            throw;
        }
    }

    NavigationManager.NavigateTo("/movies");
}
```

Concurrency exceptions are detected when one client deletes the movie and a different client posts changes to the movie.

To test how concurrency is handled by the preceding code:

1. Select **Edit** for a movie, make changes, but don't select **Save**.
2. In a different browser window, open the app to the movie `Index` page and select the **Delete** link for the same movie to delete the movie.
3. In the previous browser window, post changes to the movie by selecting the **Save** button.
4. The browser is navigated to the `notfound` endpoint, which doesn't exist and yields a 404 (Not Found) result.

Additional guidance on handling concurrency with EF Core in Blazor apps is available in the Blazor documentation.

## Stop the app

If the app is running, shut the app down by closing the browser's window.

If the app is running, shut the app down by closing the browser's window and pressing `Shift`+`F5` on the keyboard in VS Code.

If the app is running, shut the app down by closing the browser's window and pressing `Ctrl`+`C` in the command shell.

## Troubleshoot with the completed sample

If you run into a problem while following the tutorial that you can't resolve from the text, compare your code to the completed project in the Blazor samples repository:

[Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)

Select the latest version folder. The sample folder for this tutorial's project is named `BlazorWebAppMovies`.

## Related content

* Configuration articles:

  * [Configuration in ASP.NET Core](../../../fundamentals/configuration/?view=aspnetcore-10.0) (ASP.NET Core Configuration system)
  * [ASP.NET Core Blazor configuration](../../fundamentals/configuration?view=aspnetcore-10.0) (Blazor documentation)
  * [Data seeding (EF Core documentation)](/en-us/ef/core/modeling/data-seeding)

* [Concurrency with EF Core in Blazor apps](../../blazor-ef-core?view=aspnetcore-10.0)

* Database provider resources:

  * EF Core documentation

    * [SQLite EF Core Database Provider Limitations](/en-us/ef/core/providers/sqlite/limitations)
    * [Customize migration code](/en-us/ef/core/managing-schemas/migrations/#customize-migration-code)

  * [SQLite ALTER TABLE statement (SQLite documentation)](https://sqlite.org/lang_altertable.html)

* Blazor Web App security

  * [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0)
  * [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0) and the following articles in the *Server* security node
  * [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](../../security/blazor-web-app-with-oidc?view=aspnetcore-10.0)
  * [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](../../security/blazor-web-app-with-entra?view=aspnetcore-10.0)

## Legal

[*Mad Max*, *The Road Warrior*, *Mad Max: Beyond Thunderdome*, *Mad Max: Fury Road*, and *Furiosa: A Mad Max Saga*](https://warnerbros.fandom.com/wiki/Mad_Max_\(franchise\)) are trademarks and copyrights of [Warner Bros. Entertainment](https://www.warnerbros.com/).

## Next steps

[Previous: Learn about Razor components](part-3?view=aspnetcore-10.0) [Next: Add Validation](part-5?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/test/debug-aspnetcore-source
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/debug-aspnetcore-source.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Debug .NET and ASP.NET Core source code with Visual Studio

To debug .NET and ASP.NET Core source code in Visual Studio:

* In **Tools -> Options -> Debugging -> General**, un-check **Enable Just My Code**.

* Verify **Enable Source Link support** is checked.

* In **Tool -> Options -> Debugging -> Symbols**, enable **Microsoft Symbol Servers**.

When you step into any .NET or ASP.NET Core code, Visual Studio displays the source code. For example:

* Set a break point in `OnGet` in `Pages/Privacy.cshtml.cs` and select the **Privacy** link.

* Select one of the **Download Source and Continue Debugging** options.

The preceding instructions work for basic stepping into functions, but the optimized .NET code often removes local variables and functions. To disable optimizations and allow better source debugging:

* In **Tools -> Options -> Debugging -> General**, enable **Suppress JIT optimization on module load (Managed only)**:
* Add the environment variable and value `COMPlus_ReadyToRun=0` to the `Properties/launchSettings.json` file:
  ```
  {
    "iisSettings": {
      "windowsAuthentication": false,
      "anonymousAuthentication": true,
      "iisExpress": {
        "applicationUrl": "http://localhost:10892",
        "sslPort": 44315
      }
    },
    "profiles": {
      "WebApplication18": {
        "commandName": "Project",
        "dotnetRunMessages": true,
        "launchBrowser": true,
        "applicationUrl": "https://localhost:7045;http://localhost:5045",
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development",
          "COMPlus_ReadyToRun": "0"
        }
      },
      "IIS Express": {
        "commandName": "IISExpress",
        "launchBrowser": true,
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development",
          "COMPlus_ReadyToRun": "0"
        }
      }
    }
  }
  ```

If you have debugged an app before with the previous version of .NET, delete the `%TEMP%/SymbolCache` directory as it can have old PDBs that are out of date.

## Debugging .NET Core on Unix over SSH

* [Debugging .NET Core on Unix over SSH](https://devblogs.microsoft.com/devops/debugging-net-core-on-unix-over-ssh/)
* [Debugging ASP Core on Linux with Visual Studio 2017](https://devblogs.microsoft.com/premier-developer/debugging-asp-core-on-linux-with-visual-studio-2017/)

## Related content

* [JIT Optimization and Debugging](/en-us/visualstudio/debugger/jit-optimization-and-debugging)
* [Limitations of the 'Suppress JIT optimization' option](/en-us/visualstudio/debugger/jit-optimization-and-debugging#limitations-of-the-suppress-jit-optimization-option) To set `COMPlus_ReadyToRun` to `0`
* [.NET Hot Reload support for ASP.NET Core](hot-reload?view=aspnetcore-10.0)
* [Test Execution with Hot Reload](/en-us/visualstudio/test/test-execution-with-hot-reload)
* [Debug ASP.NET Core Blazor apps](../blazor/debug?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/sort-filter-page
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/sort-filter-page.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Add sorting, filtering, and paging - ASP.NET MVC with EF Core

In the previous tutorial, you implemented a set of web pages for basic CRUD operations for Student entities. In this tutorial, you add sorting, filtering, and paging functionality to the Students Index page. You also create a page that does simple grouping.

In this exercise, you add sorting, filtering, and paging functionality to the Students Index page. You also create a page that does simple grouping. The following illustration shows what the page looks like when you're done. The column headings are links the user can select to sort by that column. Selecting a column heading repeatedly causes the sort order to toggle between ascending and descending.

In this tutorial, you:

* Add links to support column sorting
* Add a Search box to support searches
* Add paging to the Students Index
* Add links to support the paging action
* Create an About page for the site

## Prerequisites

* Complete the previous tutorial, [Implement basic CRUD functionality - ASP.NET MVC with EF Core](crud?view=aspnetcore-10.0)

## Add column sort links

To add sorting to the **Students Index** page, you change the `Index` method of the Students controller and add code to the Student Index view.

### Add sorting functionality to the Index method

In the *StudentsController.cs* file, replace the `Index` method with the following code:

```
public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}
```

This code receives a `sortOrder` parameter from the query string in the URL. The query string value is provided by ASP.NET Core MVC as a parameter to the action method. The parameter is a string value: "Name" or "Date." The value is optionally followed by an underscore and the string "desc" to specify descending order. The default sort order is ascending.

The first time the **Index** page is requested, there's no query string. The students are displayed in ascending order by last name, which is the default as established by the fall-through case in the `switch` statement. When the user selects a column heading hyperlink, the appropriate `sortOrder` value is provided in the query string.

The two `ViewData` elements (`NameSortParm` and `DateSortParm`) are used by the view to configure the column heading hyperlinks with the appropriate query string values.

```
public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}
```

These code changes include ternary statements for the elements. The first statement specifies that if the `sortOrder` parameter is null or empty, the `NameSortParm` element is set to "name\_desc." Otherwise, the element is set to an empty string. These two statements enable the view to set the column heading hyperlinks as follows:

| Current sort order   | Last name hyperlink | Date hyperlink |
| -------------------- | ------------------- | -------------- |
| Last Name ascending  | Descending          | Ascending      |
| Last Name descending | Ascending           | Ascending      |
| Date ascending       | Ascending           | Descending     |
| Date descending      | Ascending           | Ascending      |

The method uses LINQ to Entities to specify the column to sort by. The code creates an `IQueryable` variable before the `switch` statement, modifies it in the `switch` statement, and calls the `ToListAsync` method after the `switch` statement. When you create and modify `IQueryable` variables, no query is sent to the database. The query isn't executed until you convert the `IQueryable` object into a collection by calling a method such as `ToListAsync`. Therefore, this code results in a single query that isn't executed until the `return View` statement.

This code can get verbose when you work with a large number of columns. The last tutorial in this series, [Learn about advanced scenarios > Use dynamic LINQ to simplify code](advanced?view=aspnetcore-10.0#dynamic-linq), shows how to write code that lets you pass the name of the `OrderBy` column in a string variable.

### Add column heading hyperlinks to the Student Index view

Replace the code in the *Views/Students/Index.cshtml* file with the following code that adds column heading hyperlinks. The changed lines are highlighted.

```
@model IEnumerable<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a>
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.EnrollmentDate)</a>
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.LastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FirstMidName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

This code uses the information in the `ViewData` properties to set up hyperlinks with the appropriate query string values.

Run the app, select the **Students** tab, and select the **Last Name** and **Enrollment Date** column headings to verify sorting works.

## Add a Search box

To add filtering to the **Students Index** page, you add a text box and a **Submit** button to the view and make corresponding changes in the `Index` method. In the text box, you can enter a string to search for in the Student first name and last name fields.

### Add filtering support to the Index method

In the *StudentsController.cs* file, replace the `Index` method with the following code (the changes are highlighted).

```
public async Task<IActionResult> Index(string sortOrder, string searchString)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    ViewData["CurrentFilter"] = searchString;

    var students = from s in _context.Students
                   select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        students = students.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}
```

You added a `searchString` parameter to the `Index` method. The search string value is received from a text box that you add to the Index view. You also added a `where` clause to the `LINQ` statement that selects only students whose first name or last name match the search string. The statement that adds the `where` clause is executed only if there's a searchable value.

#### Determine how to use the Where method

In this scenario, you call the `Where` method on an `IQueryable` object, and the filter is processed on the server. In some scenarios, you might call the `Where` method as an extension method on an in-memory collection. Suppose you change the reference to `_context.Students`. Instead of calling the Entity Framework `DbSet` method, it references a repository method that returns an `IEnumerable` collection. The result is normally the same, but some cases might be different.

For example, the .NET Framework implementation of the `Contains` method performs a case-sensitive comparison by default. In SQL Server, the determination occurs by the collation setting of the SQL Server instance. The setting defaults to case-insensitive. You might call the `ToUpper` method to make the test explicitly case-insensitive: `Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`. This option ensures the results stay the same if you change the code later to use a repository that returns an `IEnumerable` collection rather than an `IQueryable` object. (When you call the `Contains` method on an `IEnumerable` collection, you get the .NET Framework implementation. When you call it on an `IQueryable` object, you get the database provider implementation.)

However, there's a performance penalty for this solution. The `ToUpper` code puts a function in the `WHERE` clause of the `TSQL SELECT` statement. This behavior prevents the optimizer from using an index. Given that SQL is mostly installed as case-insensitive, it's best to avoid the `ToUpper` code until you migrate to a case-insensitive data store.

### Add a Search Box to the Student Index view

In the *Views/Student/Index.cshtml* file, add the highlighted code immediately before the opening table tag `<table>` in order to create a caption, a text box, and a **Search** button.

```
<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">
    <div class="form-actions no-color">
        <p>
            <label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-action="Index">Back to Full List</a>
        </p>
    </div>
</form>

<table class="table">
```

This code uses the `<form>` [tag helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) to add the search text box and button. By default, the `<form>` tag helper submits form data with a `POST`, which means the parameters are passed in the HTTP message body and not in the URL as query strings. When you specify HTTP `GET`, the form data is passed in the URL as query strings, which enables users to bookmark the URL. The W3C guidelines recommend using `GET` when the action doesn't result in an update.

Run the app, select the **Students** tab, enter a search string, and select **Search** to verify filtering is working.

Notice that the URL contains the search string.

```
http://localhost:5813/Students?SearchString=an
```

If you bookmark this page, you get the filtered list when you use the bookmark. Adding the `method="get"` clause to the `form` tag causes creation of the query string.

At this stage in your development, if you select a column heading sort link, you lose the filter value that you entered in the **Search** box. You discover how to address that situation in the next section.

## Add paging to the Students Index

To add paging to the **Students Index** page, you create a `PaginatedList` class that uses `Skip` and `Take` statements to filter data on the server instead of always retrieving all table rows. Then you make more changes in the `Index` method and add paging buttons to the `Index` view. The following image shows the paging buttons.

In the project folder, create the *PaginatedList.cs* file. Replace the template code with the following code.

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}
```

The `CreateAsync` method in this code takes page size and page number and applies the appropriate `Skip` and `Take` statements to the `IQueryable` object. When the `ToListAsync` method is called on the `IQueryable` object, the method returns a `List` containing only the requested page. The `HasPreviousPage` and `HasNextPage` properties can be used to enable or disable **Previous** and **Next** paging buttons.

A `CreateAsync` method is used instead of a constructor to create the `PaginatedList<T>` object because constructors can't run asynchronous code.

## Add paging to the Index method

In the *StudentsController.cs* file, replace the `Index` method with the following code.

```
public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)
{
    ViewData["CurrentSort"] = sortOrder;
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

    if (searchString != null)
    {
        pageNumber = 1;
    }
    else
    {
        searchString = currentFilter;
    }

    ViewData["CurrentFilter"] = searchString;

    var students = from s in _context.Students
                   select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        students = students.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }

    int pageSize = 3;
    return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
}
```

This code adds a page number parameter, a current sort order parameter, and a current filter parameter to the method signature.

```
public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)
```

The first time the page displays, or a paging or sorting link isn't selected yet, all parameters are null. If a paging link is selected, the page variables contain the page number to display.

The `ViewData` element named `CurrentSort` provides the view with the current sort order. This value must be included in the paging links so the sort order stays the same while paging.

The `ViewData` element named `CurrentFilter` provides the view with the current filter string. This value must be included in the paging links to maintain the filter settings during paging. The value must be restored to the text box when the page displays.

If the search string is changed during paging, the page must be reset to 1 because the new filter can result in different data to display. The search string is changed when a value is entered in the text box and the user selects **Submit**. In this scenario, the `searchString` parameter isn't null.

```
if (searchString != null)
{
    pageNumber = 1;
}
else
{
    searchString = currentFilter;
}
```

At the end of the `Index` method, the `PaginatedList.CreateAsync` method converts the student query to a single page of students in a collection type that supports paging. The single page of students is then passed to the view.

```
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
```

The `PaginatedList.CreateAsync` method takes a page number. The two question marks `??` represent the null-coalescing operator. The null-coalescing operator defines a default value for a nullable type. The expression `(pageNumber ?? 1)` means return the value of `pageNumber` if it has a value, or return 1 if `pageNumber` is null.

## Add paging links

In the *Views/Students/Index.cshtml* file, replace the existing code with the following code. The changes are highlighted.

```
@model PaginatedList<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">
    <div class="form-actions no-color">
        <p>
            <label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-action="Index">Back to Full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
            </th>
            <th>
                First Name
            </th>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}

<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex + 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @nextDisabled">
    Next
</a>
```

The `@model` statement at the top of the page specifies that the view now gets a `PaginatedList<T>` object instead of a `List<T>` object.

The column header links use the query string to pass the current search string to the controller so the user can sort within filter results:

```
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>
```

Tag helpers display the paging buttons:

```
<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @prevDisabled">
   Previous
</a>
```

Run the app and go to the **Students** page.

Select the paging links in different sort orders and make sure paging works. Enter a search string and try paging again to verify paging also works correctly with sorting and filtering.

## Create an About page

For the Contoso University website's **About** page, you display how many students are enrolled for each enrollment date. This functionality requires grouping and simple calculations on the groups. To support this behavior, you complete the following tasks:

* Create a view model class for the data that you need to pass to the view.
* Create the `About` method in the Home controller.
* Create the About view.

### Create the view model

Create a *SchoolViewModels* folder in the *Models* folder.

In the new folder, add an *EnrollmentDateGroup.cs* class file and replace the template code with the following code:

```
using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}
```

### Modify the Home Controller

In the *HomeController.cs* file, add the following `using` statements at the top of the file:

```
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.Extensions.Logging;
```

Add a class variable for the database context immediately after the opening curly brace `{` for the class, and get an instance of the context from ASP.NET Core DI:

```
public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly SchoolContext _context;

    public HomeController(ILogger<HomeController> logger, SchoolContext context)
    {
        _logger = logger;
        _context = context;
    }
```

Add an `About` method with the following code:

```
public async Task<ActionResult> About()
{
    IQueryable<EnrollmentDateGroup> data = 
        from student in _context.Students
        group student by student.EnrollmentDate into dateGroup
        select new EnrollmentDateGroup()
        {
            EnrollmentDate = dateGroup.Key,
            StudentCount = dateGroup.Count()
        };
    return View(await data.AsNoTracking().ToListAsync());
}
```

The `LINQ` statement groups the student entities by enrollment date. It calculates the number of entities in each group, and stores the results in a collection of `EnrollmentDateGroup` view model objects.

### Create the About view

Add a *Views/Home/About.cshtml* file with the following code:

```
@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>

@{
    ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
    <tr>
        <th>
            Enrollment Date
        </th>
        <th>
            Students
        </th>
    </tr>

    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>
```

Run the app and go to the **About** page. The count of students for each enrollment date is displayed in a table.

## Get the code

[Download or view the completed application](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final).

## Next step

[Tutorial: Apply migrations (data model changes) to the Contoso University sample](migrations?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/11/quickgrid-controls-render-as-links
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/breaking-changes/11/quickgrid-controls-render-as-links.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# QuickGrid sorting and pagination controls render as links

In ASP.NET Core 11, sortable `QuickGrid` column headers and `Paginator` controls render as links instead of buttons by default. Apps with CSS or tests that target the previous button markup might need updates.

## Version introduced

.NET 11

## Previous behavior

Previously, sortable column headers and paginator controls rendered as `<button>` elements with click handlers.

## New behavior

Starting in ASP.NET Core 11, sortable column headers and paginator controls render as `<a>` elements with `href` attributes. Pagination and sort state is stored in the URL query string, which lets users share a sorted or paginated view and use browser back/forward navigation. The links also work under static server-side rendering (static SSR) without a JavaScript runtime.

Disabled paginator links use `aria-disabled="true"` instead of the `disabled` attribute, which doesn't apply to links.

## Type of breaking change

This change is a [behavioral change](/en-us/dotnet/core/compatibility/categories#behavioral-change).

## Reason for change

Links allow pagination and sorting to navigate by URL, including under static SSR where button click handlers aren't available. For more information, see [ASP.NET Core Blazor \`QuickGrid\` component](../../blazor/components/quickgrid?view=aspnetcore-11.0#pagination-modes).

## Recommended action

Update custom CSS selectors and UI tests that expect button markup. For example:

```
- button.col-title { ... }
+ button.col-title, a.col-title { ... }
- nav button { ... }
+ nav button, nav a { ... }
- nav button:disabled { ... }
+ nav button:disabled, nav a[aria-disabled="true"] { ... }
```

The built-in QuickGrid CSS supports both markup styles. For apps with multiple grids on the same page, assign unique query parameter names to avoid conflicts. For more information, see [ASP.NET Core Blazor \`QuickGrid\` component](../../blazor/components/quickgrid?view=aspnetcore-11.0#multiple-grids-on-the-same-page).

To restore the previous button markup, set the `AppContext` switch to `false` before rendering the grid:

```
AppContext.SetSwitch(
    "Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting",
    false);
```

The button markup requires an interactive render mode. The switch **doesn't disable URL-state handling**: `QuickGrid` still reads and writes pagination and sort state in the URL query string.

## Affected APIs

None. No public API signatures changed. This change affects the HTML markup rendered by the `QuickGrid` and `Paginator` components.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-29

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/security/authorization/claims
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/security/authorization/claims.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Claim-based authorization in ASP.NET Core MVC

When an identity is created for an app user upon signing into an app, the identity provider may assign one or more [claims](/en-us/dotnet/api/system.security.claims.claim#remarks) to the user's identity. A claim is a name value pair that represents what the subject (a user, an app or service, or a device/computer) is, not what the subject can do. A claim can be evaluated by the app to determine access rights to data and other secured resources during the process of authorization and can also be used to make or express authentication decisions about a subject. An identity can contain multiple claims with multiple values and can contain multiple claims of the same type. This article explains how to add claims checks for authorization in an ASP.NET Core app.

This article uses MVC examples and focuses on MVC scenarios. For Blazor and Razor Pages guidance, see the following resources:

* [Claim-based authorization in ASP.NET Core](../../../security/authorization/claims?view=aspnetcore-10.0)
* [Claim-based authorization in ASP.NET Core Razor Pages](../../../razor-pages/security/authorization/claims?view=aspnetcore-10.0)

## Sample app

The sample app for this article is the [`WebAll` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/claims) ([how to download](../../../overview?view=aspnetcore-10.0#how-to-download-a-sample)). For more information, see the sample's README file (`README.md`).

## Add claim checks

Claim-based authorization checks are declarative and applied to controllers or actions within a controller.

Claims in code specify claims which the current user must possess, and optionally the value the claim must hold to access the requested resource. Claims requirements are policy based. The developer must build and register a policy expressing the claims requirements.

The simplest type of claim policy looks for the presence of a claim and doesn't check the value.

Build and register the policy and call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) (place the call after the line that calls [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)). Registering the policy takes place as part of the Authorization service configuration, typically in the `Program` file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

Build and register the policy and call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) (place the call after the line that calls [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication)). Registering the policy takes place as part of the Authorization service configuration, typically in the `Program` file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorization(options =>
{
   options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

Build and register the policy in `Startup.ConfigureServices` (`Startup.cs`) in the Authorization service's configuration:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("EmployeeOnly", 
        policy => policy.RequireClaim("EmployeeNumber"));
});
```

Call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) in `Startup.Configure` (`Startup.cs`) immediately after [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) is called:

```
app.UseAuthorization();
```

Apply the policy using the `Policy` property on the [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute to specify the policy name. In the following example, the `EmployeeOnly` policy checks for the presence of an `EmployeeNumber` claim on the current identity:

```
[Authorize(Policy = "EmployeeOnly")]
public IActionResult VacationBalance()
{
    return View();
}
```

The `[Authorize]` attribute can be applied to an entire controller, in which case only identities matching the policy are allowed access to any action on the controller:

```
[Authorize(Policy = "EmployeeOnly")]
public class VacationController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    public ActionResult VacationBalance()
    {
        return View();
    }

    [AllowAnonymous]
    public ActionResult VacationPolicy()
    {
        return View();
    }
}
```

If you have a controller that's protected by the `[Authorize]` attribute but want to allow anonymous access to a particular action, apply the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute):

```
[Authorize(Policy = "EmployeeOnly")]
public class VacationController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    public ActionResult VacationBalance()
    {
        return View();
    }

    [AllowAnonymous]
    public ActionResult VacationPolicy()
    {
        return View();
    }
}
```

You can specify a list of allowed values when creating a policy. The following policy only passes for employees whose employee number is 1, 2, 3, 4, or 5:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("Founders", policy =>
        policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Founders", policy =>
                      policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("Founder", policy =>
        policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});
```

### Add a generic claim check

If the claim value isn't a single value or you need more flexible claim evaluation logic, such as pattern matching, checking the claim issuer, or parsing complex claim values, use [RequireAssertion](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireassertion) with [HasClaim](/en-us/dotnet/api/system.security.claims.claimsprincipal.hasclaim). For example, the following policy requires that the user's `email` claim ends with a specific domain:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ContosoOnly", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                c.Type == "email" &&
                c.Value.EndsWith("@contoso.com", StringComparison.OrdinalIgnoreCase))));
```

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ContosoOnly", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                c.Type == "email" &&
                c.Value.EndsWith("@contoso.com", StringComparison.OrdinalIgnoreCase))));
});
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ContosoOnly", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                c.Type == "email" &&
                c.Value.EndsWith("@contoso.com", StringComparison.OrdinalIgnoreCase))));
});
```

For more information, see [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0#use-a-func-to-fulfill-a-policy).

## Evaluate multiple policies

If multiple policies are applied at the controller and action levels, ***all*** policies must pass before access is granted:

```
[Authorize(Policy = "EmployeeOnly")]
public class SalaryController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    public IActionResult Payslip()
    {
        return View();
    }

    [Authorize(Policy = "HumanResources")]
    public IActionResult UpdateSalary()
    {
        return View();
    }
}
```

In the preceding example, any identity that fulfills the `EmployeeOnly` policy can access the `Payslip` action, as that policy is enforced on the controller. However, in order to call the `UpdateSalary` action, the identity must fulfill *both* the `EmployeeOnly` policy and the `HumanResources` policy.

If you want more complicated policies, such as taking a date of birth claim, calculating an age from it then checking the age is 21 or older then you need to write [custom policy handlers](../../../security/authorization/policies?view=aspnetcore-10.0).

## Claim case sensitivity

Claim *values* are compared using [`StringComparison.Ordinal`](/en-us/dotnet/api/system.stringcomparison). This means `Admin` (uppercase `A`) and `admin` (lowercase `a`) are always treated as different claim values, regardless of which authentication handler created the identity.

Separately, the claim *type* comparison (used to locate claims by their type, such as `email`) may be case-sensitive or case-insensitive depending on the [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) implementation. With `Microsoft.IdentityModel` in ASP.NET Core 8.0 or later (used by [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer), [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect), [AddWsFederation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.wsfederationextensions.addwsfederation), and [AddMicrosoftIdentityWebApp](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapp)/[AddMicrosoftIdentityWebApi](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapi)), [CaseSensitiveClaimsIdentity](/en-us/dotnet/api/microsoft.identitymodel.tokens.casesensitiveclaimsidentity) is produced during token validation, which uses case-sensitive claim type matching.

The default [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) provided by the .NET runtime (used in most cases, including all cookie-based flows) still uses case-insensitive claim type matching.

In practice, this distinction rarely matters when the claim type is configured once during identity creation and matched consistently. This also applies to roles when they're represented as claims. Always use consistent casing for claim values and claim types to avoid subtle issues.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/subkeyderivation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/implementation/subkeyderivation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Subkey derivation and authenticated encryption in ASP.NET Core

[]()

Most keys in the key ring will contain some form of entropy and will have algorithmic information stating "CBC-mode encryption + HMAC validation" or "GCM encryption + validation". In these cases, we refer to the embedded entropy as the master keying material (or KM) for this key, and we perform a key derivation function to derive the keys that will be used for the actual cryptographic operations.

Note

Keys are abstract, and a custom implementation might not behave as below. If the key provides its own implementation of `IAuthenticatedEncryptor` rather than using one of our built-in factories, the mechanism described in this section no longer applies.

[]()

## Additional authenticated data and subkey derivation

The `IAuthenticatedEncryptor` interface serves as the core interface for all authenticated encryption operations. Its `Encrypt` method takes two buffers: plaintext and additionalAuthenticatedData (AAD). The plaintext contents flow unchanged the call to `IDataProtector.Protect`, but the AAD is generated by the system and consists of three components:

1. The 32-bit magic header 09 F0 C9 F0 that identifies this version of the data protection system.

2. The 128-bit key id.

3. A variable-length string formed from the purpose chain that created the `IDataProtector` that's performing this operation.

Because the AAD is unique for the tuple of all three components, we can use it to derive new keys from KM instead of using KM itself in all of our cryptographic operations. For every call to `IAuthenticatedEncryptor.Encrypt`, the following key derivation process takes place:

`( K_E, K_H ) = SP800_108_CTR_HMACSHA512(K_M, AAD, contextHeader || keyModifier)`

Here, we're calling the NIST SP800-108 KDF in Counter Mode (see [NIST SP800-108](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-108.pdf), Sec. 5.1) with the following parameters:

* Key derivation key (KDK) = `K_M`

* PRF = HMACSHA512

* label = additionalAuthenticatedData

* context = contextHeader || keyModifier

The context header is of variable length and essentially serves as a thumbprint of the algorithms for which we're deriving `K_E` and `K_H`. The key modifier is a 128-bit string randomly generated for each call to `Encrypt` and serves to ensure with overwhelming probability that KE and KH are unique for this specific authentication encryption operation, even if all other input to the KDF is constant.

For CBC-mode encryption + HMAC validation operations, `| K_E |` is the length of the symmetric block cipher key, and `| K_H |` is the digest size of the HMAC routine. For GCM encryption + validation operations, `| K_H | = 0`.

## CBC-mode encryption + HMAC validation

Once `K_E` is generated via the above mechanism, we generate a random initialization vector and run the symmetric block cipher algorithm to encipher the plaintext. The initialization vector and ciphertext are then run through the HMAC routine initialized with the key `K_H` to produce the MAC. This process and the return value is represented graphically below.

`output:= keyModifier || iv || E_cbc (K_E,iv,data) || HMAC(K_H, iv || E_cbc (K_E,iv,data))`

Note

The `IDataProtector.Protect` implementation will [prepend the magic header and key id](authenticated-encryption-details?view=aspnetcore-10.0) to output before returning it to the caller. Because the magic header and key id are implicitly part of [AAD](subkeyderivation?view=aspnetcore-10.0#data-protection-implementation-subkey-derivation-aad), and because the key modifier is fed as input to the KDF, this means that every single byte of the final returned payload is authenticated by the MAC.

## Galois/Counter Mode encryption + validation

Once `K_E` is generated via the above mechanism, we generate a random 96-bit nonce and run the symmetric block cipher algorithm to encipher the plaintext and produce the 128-bit authentication tag.

`output := keyModifier || nonce || E_gcm (K_E,nonce,data) || authTag`

Note

Even though GCM natively supports the concept of AAD, we're still feeding AAD only to the original KDF, opting to pass an empty string into GCM for its AAD parameter. The reason for this is two-fold. First, [to support agility](context-headers?view=aspnetcore-10.0#data-protection-implementation-context-headers) we never want to use `K_M` directly as the encryption key. Additionally, GCM imposes very strict uniqueness requirements on its inputs. The probability that the GCM encryption routine is ever invoked on two or more distinct sets of input data with the same (key, nonce) pair must not exceed 2^-32. If we fix `K_E` we cannot perform more than 2^32 encryption operations before we run afoul of the 2^-32 limit. This might seem like a very large number of operations, but a high-traffic web server can go through 4 billion requests in mere days, well within the normal lifetime for these keys. To stay compliant of the 2^-32 probability limit, we continue to use a 128-bit key modifier and 96-bit nonce, which radically extends the usable operation count for any given `K_M`. For simplicity of design we share the KDF code path between CBC and GCM operations, and since AAD is already considered in the KDF there's no need to forward it to the GCM routine.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/samesite/mvc21
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/samesite/mvc21.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core 2.1 MVC SameSite cookie sample

ASP.NET Core 2.1 has built-in support for the [SameSite](https://owasp.org/www-community/SameSite) attribute, but it was written to the original standard. The [patched behavior](https://github.com/dotnet/aspnetcore/issues/8212) changed the meaning of `SameSite.None` to emit the sameSite attribute with a value of `None`, rather than not emit the value at all. If you want to not emit the value you can set the `SameSite` property on a cookie to -1.

ASP.NET Core [Identity](../authentication/identity?view=aspnetcore-2.2) is largely unaffected by [SameSite cookies](../samesite?view=aspnetcore-2.2) except for advanced scenarios like `IFrames` or `OpenIdConnect` integration.

When using `Identity`, do ***not*** add any cookie providers or call `services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)`, `Identity` takes care of that.

## Writing the SameSite attribute

Following is an example of how to write a SameSite attribute on a cookie:

```
var cookieOptions = new CookieOptions
{
    // Set the secure flag, which Chrome's changes will require for SameSite none.
    // Note this will also require you to be running on HTTPS
    Secure = true,

    // Set the cookie to HTTP only which is good practice unless you really do need
    // to access it client side in scripts.
    HttpOnly = true,

    // Add the SameSite attribute, this will emit the attribute with a value of none.
    // To not emit the attribute at all set the SameSite property to (SameSiteMode)(-1).
    SameSite = SameSiteMode.None
};

// Add the cookie to the response cookie collection
Response.Cookies.Append(CookieName, "cookieValue", cookieOptions);
```

## Setting Cookie Authentication and Session State cookies

Cookie authentication, session state and [various other components](../samesite?view=aspnetcore-2.2) set their sameSite options via Cookie options, for example

```
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.SameSite = SameSiteMode.None;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.IsEssential = true;
    });

services.AddSession(options =>
{
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.IsEssential = true;
});
```

In the preceding code, both cookie authentication and session state set their sameSite attribute to `None`, emitting the attribute with a `None` value, and also set the Secure attribute to true.

### Run the sample

If you run the [sample project](https://github.com/blowdart/AspNetSameSiteSamples/tree/master/AspNetCore21MVC), load your browser debugger on the initial page and use it to view the cookie collection for the site. To do so in Edge and Chrome press `F12` then select the `Application` tab and click the site URL under the `Cookies` option in the `Storage` section.

You can see from the image above that the cookie created by the sample when you click the "Create SameSite Cookie" button has a SameSite attribute value of `Lax`, matching the value set in the [sample code](#sampleCode).

## Intercepting cookies

In order to intercept cookies, to adjust the none value according to its support in the user's browser agent you must use the `CookiePolicy` middleware. This must be placed into the http request pipeline **before** any components that write cookies and configured within `ConfigureServices()`.

To insert it into the pipeline use `app.UseCookiePolicy()` in the `Configure(IApplicationBuilder, IHostingEnvironment)` method in [Startup.cs](https://github.com/blowdart/AspNetSameSiteSamples/blob/master/AspNetCore21MVC/Startup.cs). For example:

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
       app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();
    app.UseAuthentication();
    app.UseSession();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

Then in the `ConfigureServices(IServiceCollection services)` configure the cookie policy to call out to a helper class when cookies are appended or deleted. For example:

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
        options.OnAppendCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
        options.OnDeleteCookie = cookieContext =>
            CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
    });
}

private void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
    if (options.SameSite == SameSiteMode.None)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        if (SameSite.BrowserDetection.DisallowsSameSiteNone(userAgent))
        {
            options.SameSite = (SameSiteMode)(-1);
        }
    }
}
```

```
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.1.14" />
<PackageReference Include="Microsoft.AspNetCore.CookiePolicy" Version="2.1.14" />
```

### More Information

[Chrome Updates](https://www.chromium.org/updates/same-site) [ASP.NET Core SameSite Documentation](../samesite?view=aspnetcore-2.2) [ASP.NET Core 2.1 SameSite Change Announcement](https://github.com/dotnet/aspnetcore/issues/8212)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2022-08-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/tutorials/maui-blazor-web-app
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/tutorials/maui-blazor-web-app.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a .NET MAUI Blazor Hybrid app with a Blazor Web App

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article shows you how to build a .NET MAUI Blazor Hybrid app with a Blazor Web App that uses a shared user interface via a Razor class library (RCL).

## Prerequisites and preliminary steps

For prerequisites and preliminary steps, see the [Build your first app](/en-us/dotnet/maui/get-started/first-app) tutorial. We recommend using the .NET MAUI Blazor Hybrid tutorial to set up your local system for .NET MAUI development before using the guidance in this article.

## .NET MAUI Blazor Hybrid and Web App solution template

The .NET MAUI Blazor Hybrid and Web App solution template sets up a solution that targets Android, iOS, Mac, Windows and Web that reuses UI. You can choose a Blazor interactive render mode for the web app and it creates the appropriate projects for the app, including a Blazor Web App and a .NET MAUI Blazor Hybrid app. A shared Razor class library (RCL) maintains the Razor components for the app's UI. The template also provides sample code to show you how to use dependency injection to provide different interface implementations for the Blazor Hybrid and Blazor Web App, which is covered in the [Using interfaces to support different device implementations](#using-interfaces-to-support-different-device-implementations) section of this article.

If you haven't already installed the .NET MAUI workload, install it now. The .NET MAUI workload provides the project template:

```
dotnet workload install maui
```

Create a solution from the project template with the following .NET CLI command, replacing the `{INTERACTIVITY}` placeholder with the Blazor render mode that you desire to use:

* `Server`: Adopts the Interactive Server render mode and produces a single-project Blazor app.
* `WebAssembly`: Adopts the Interactive WebAssembly render mode and produces two Blazor projects, a server project and a `.Client` project.
* `Auto`: Adopts the Interactive Auto render mode and produces two Blazor projects, a server project and a `.Client` project.

For more information on the preceding render modes and the projects produced, see [ASP.NET Core Blazor render modes](../../components/render-modes?view=aspnetcore-10.0#render-modes) and the [Use Blazor render modes](#use-blazor-render-modes) section later in this article.

```
dotnet new maui-blazor-web -o MauiBlazorWeb -I {INTERACTIVITY}
```

In the preceding command:

* The `-o|--output` option creates a new folder for the app named `MauiBlazorWeb`.
* The `-I|--InteractivityPlatform` option sets the interactivity render mode. The `{INTERACTIVITY}` placeholder is the interactive Blazor render mode. All three interactive Blazor render modes (`Server`, `WebAssembly`, and `Auto`) are supported by the project template. For more information, see [ASP.NET Core Blazor render modes](../../components/render-modes?view=aspnetcore-10.0#render-modes) and the [Use Blazor render modes](#use-blazor-render-modes) section.

The preceding command produces a Blazor app that adopts global interactivity, which is important because MAUI apps always run interactively and throw errors on Razor component pages that explicitly specify a render mode. For more information, see [BlazorWebView needs a way to enable overriding ResolveComponentForRenderMode (`dotnet/aspnetcore` #51235)](https://github.com/dotnet/aspnetcore/issues/51235).

## .NET MAUI Blazor Hybrid and Web App sample app

[Obtain the sample app](../../fundamentals/?view=aspnetcore-10.0#sample-apps) named `MauiBlazorWeb` from the [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) (`8.0` or `9.0` folder).

The sample app is a starter solution that contains a .NET MAUI Blazor Hybrid (native, cross-platform) app, a Blazor Web App, and a Razor class library (RCL) that contains the shared UI (Razor components) used by the native and web apps.

## Migrating a .NET MAUI Blazor Hybrid solution

Instead of [using the sample app](#net-maui-blazor-hybrid-and-web-app-sample-app), you can migrate an existing .NET MAUI Blazor Hybrid app with the guidance in this section using Visual Studio.

Add new project to the solution with the **Blazor Web App** project template. Select the following options:

* **Project name**: Use the solution's name with `.Web` appended. The examples in this article assume the following naming:

  * Solution: `MauiBlazorWeb`
  * MAUI project: `MauiBlazorWeb.Maui`
  * Blazor Web App: `MauiBlazorWeb.Web`
  * Razor class library (RCL) (added in a later step): `MauiBlazorWeb.Shared`

* **Authentication type**: **None**

* **Configure for HTTPS**: Selected (enabled)

* **Interactive render mode**: **Server**

* **Interactivity location**: **Global**

* **Sample pages**: Unselected (disabled)

The **Interactivity location** setting to **Global** is important because MAUI apps always run interactively and throw errors on Razor component pages that explicitly specify a render mode. If you don't use a global render mode, you must implement the approach described in the [Use Blazor render modes](#use-blazor-render-modes) section after following the guidance in this section. For more information, see [BlazorWebView needs a way to enable overriding ResolveComponentForRenderMode (`dotnet/aspnetcore` #51235)](https://github.com/dotnet/aspnetcore/issues/51235).

Add new **Razor class library** (RCL) project to the solution. The examples in this article assume that the project is named `MauiBlazorWeb.Shared`. Don't select **Support pages and views** when you add the project to the solution.

Add project references to the RCL from both MAUI project and the Blazor Web App project.

Move the `Components` folder and all of its contents from the MAUI project to the RCL. Confirm that the `Components` folder is deleted from the MAUI project.

Tip

When moving a folder or file in Visual Studio, either use keyboard commands or the shortcut menu by right-clicking for a cut and paste operation. Dragging the folder in Visual Studio only copies from one location to another, which requires an extra step to delete the original.

Move the `css` folder from the `wwwroot` folder of the MAUI project to the RCL's `wwwroot` folder.

Delete the following files from the RCL's `wwwroot` folder:

* `background.png`
* `exampleJsInterop.js`

In the RCL, replace the root imports file (`_Imports.razor`) with the one in the RCL's `Components` folder, overwriting the existing file in the RCL and deleting the original in the `Components` folder. After moving the file, open it and rename the last two `@using` statements to match the RCL's namespace. In the following example, the RCL's namespace is `MauiBlazorWeb.Shared`:

```
@using MauiBlazorWeb.Shared
@using MauiBlazorWeb.Shared.Components
```

In the root of the RCL project, delete the following files:

* `Component1.razor`
* `ExampleJsInterop.cs`

In the RCL, open the `Components/Routes.razor` file and change `MauiProgram` to `Routes`:

```
- <Router AppAssembly="@typeof(MauiProgram).Assembly">
+ <Router AppAssembly="@typeof(Routes).Assembly">
```

Open the `MainPage.xaml` file in the MAUI project. Add an `xmlns:shared` reference to the RCL in the [ContentPage](/en-us/dotnet/api/microsoft.maui.controls.contentpage) attributes. In the following example, the RCL's namespace is `MauiBlazorWeb.Shared`. Set the correct value for both the `clr-namespace` and the `assembly`:

```
xmlns:shared="clr-namespace:MauiBlazorWeb.Shared;assembly=MauiBlazorWeb.Shared"
```

Also in the `MainPage.xaml` file, update the [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) root component [ComponentType](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.rootcomponent.componenttype#microsoft-aspnetcore-components-webview-maui-rootcomponent-componenttype) from `local` to `shared`:

```
- <RootComponent Selector="#app" ComponentType="{x:Type local:Components.Routes}" />
+ <RootComponent Selector="#app" ComponentType="{x:Type shared:Components.Routes}" />
```

In the MAUI project, open the `wwwroot/index.html` file and change stylesheets to point to the RCL's static asset path.

Remove the following lines:

```
- <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
- <link rel="stylesheet" href="css/app.css" />
```

Replace the preceding lines with the following markup. In the following example, the RCL's static asset path is `_content/MauiBlazorWeb.Shared/`:

```
<link rel="stylesheet" href="_content/MauiBlazorWeb.Shared/css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="_content/MauiBlazorWeb.Shared/css/app.css" />
```

In the Blazor Web App, open the imports file (`_Imports.razor`) and add the following two `@using` statements for the RCL. In the following example, the RCL's namespace is `MauiBlazorWeb.Shared`:

```
@using MauiBlazorWeb.Shared
@using MauiBlazorWeb.Shared.Components
```

In the Blazor Web App project, open the `App` component (`Components/App.razor`). Remove the `app.css` stylesheet:

```
- <link rel="stylesheet" href="app.css" />
```

Replace the preceding line with the RCL's static asset stylesheet references. In the following example, the RCL's static asset path is `_content/MauiBlazorWeb.Shared/`:

```
<link rel="stylesheet" href="_content/MauiBlazorWeb.Shared/css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="_content/MauiBlazorWeb.Shared/css/app.css" />
```

In the Blazor Web App project, delete the following folder and files:

* `Components/Layout` folder
* `Components/Routes.razor`
* `Components/Pages/Home.razor`
* `wwwroot/app.css`

Open the Blazor Web App's `Program.cs` file and add an additional assembly for the RCL to the app. In the following example, the RCL's namespace is `MauiBlazorWeb.Shared`:

```
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddAdditionalAssemblies(typeof(MauiBlazorWeb.Shared._Imports).Assembly);
```

Run the MAUI project by selecting the project in **Solution Explorer** and using Visual Studio's start button.

Run the Blazor Web App project by selecting the Blazor Web App project in **Solution Explorer** and using Visual Studio's start button with the `https` build configuration.

If you receive a build error that the RCL's assembly can't be resolved, build the RCL project first. If any MAUI project resource errors occur on build, rebuild the MAUI project to clear the errors.

## Use Blazor render modes

Use the guidance in one of the following subsections that matches your app's specifications for applying Blazor [render modes](../../components/render-modes?view=aspnetcore-10.0) in the Blazor Web App but ignore the render mode assignments in the MAUI project.

Render mode specification subsections:

* [Global Server interactivity](#global-server-interactivity)
* [Global Auto or WebAssembly interactivity](#global-auto-or-webassembly-interactivity)
* [Per-page/component Server interactivity](#per-pagecomponent-server-interactivity)
* [Per-page/component Auto interactivity](#per-pagecomponent-auto-interactivity)
* [Per-page/component WebAssembly interactivity](#per-pagecomponent-webassembly-interactivity)

Use the guidance in one of the following subsections that matches your app's specifications for applying Blazor [render modes](../../components/render-modes?view=aspnetcore-10.0) for a given interactivity location in the Blazor Web App but ignore the render mode assignments in the MAUI project.

Render mode and interactivity specification subsections:

* [Global Server interactivity](#global-server-interactivity)
* [Global Auto or WebAssembly interactivity](#global-auto-or-webassembly-interactivity)
* [Per-page/component Server interactivity](#per-pagecomponent-server-interactivity)
* [Per-page/component Auto interactivity](#per-pagecomponent-auto-interactivity)
* [Per-page/component WebAssembly interactivity](#per-pagecomponent-webassembly-interactivity)

### Global Server interactivity

* Interactive render mode: **Server**

* Interactivity location: **Global**

* Solution projects

  * MAUI (`MauiBlazorWeb`)
  * Blazor Web App (`MauiBlazorWeb.Web`)
  * RCL (`MauiBlazorWeb.Shared`): Contains the shared Razor components without setting render modes in each component.

Project references: `MauiBlazorWeb` and `MauiBlazorWeb.Web` have a project reference to `MauiBlazorWeb.Shared`.

* Interactive render mode: **Server**

* Interactivity location: **Global**

* Solution projects

  * MAUI (`MauiBlazorWeb.Maui`)
  * Blazor Web App (`MauiBlazorWeb.Web`)
  * RCL (`MauiBlazorWeb.Shared`): Contains the shared Razor components without setting render modes in each component.

Project references: `MauiBlazorWeb.Maui` and `MauiBlazorWeb.Web` have a project reference to `MauiBlazorWeb.Shared`.

### Global Auto or WebAssembly interactivity

* Interactive render mode: **Auto** or **WebAssembly**

* Interactivity location: **Global**

* Solution projects

  * MAUI (`MauiBlazorWeb`)

  * Blazor Web App

    * Server project: `MauiBlazorWeb.Web`
    * Client project: `MauiBlazorWeb.Web.Client`

  * RCL (`MauiBlazorWeb.Shared`): Contains the shared Razor components without setting render modes in each component.

Project references:

* `MauiBlazorWeb`, `MauiBlazorWeb.Web`, and `MauiBlazorWeb.Web.Client` projects have a project reference to `MauiBlazorWeb.Shared`.
* `MauiBlazorWeb.Web` has a project reference to `MauiBlazorWeb.Web.Client`.

- Interactive render mode: **Auto** or **WebAssembly**

- Interactivity location: **Global**

- Solution projects

  * MAUI (`MauiBlazorWeb.Maui`)

  * Blazor Web App

    * Server project: `MauiBlazorWeb.Web`
    * Client project: `MauiBlazorWeb.Web.Client`

  * RCL (`MauiBlazorWeb.Shared`): Contains the shared Razor components without setting render modes in each component.

Project references:

* `MauiBlazorWeb.Maui`, `MauiBlazorWeb.Web`, and `MauiBlazorWeb.Web.Client` projects have a project reference to `MauiBlazorWeb.Shared`.
* `MauiBlazorWeb.Web` has a project reference to `MauiBlazorWeb.Web.Client`.

### Per-page/component Server interactivity

* Interactive render mode: **Server**

* Interactivity location: **Per-page/component**

* Solution projects

  * MAUI (`MauiBlazorWeb`): Calls `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes` in `MauiProgram.cs`.
  * Blazor Web App (`MauiBlazorWeb.Web`): Doesn't set an `@rendermode` directive attribute on the `HeadOutlet` and `Routes` components of the `App` component (`Components/App.razor`).
  * RCL (`MauiBlazorWeb.Shared`): Contains the shared Razor components that set the `InteractiveServer` render mode in each component.

`MauiBlazorWeb` and `MauiBlazorWeb.Web` have a project reference to `MauiBlazorWeb.Shared`.

Add the following `InteractiveRenderSettings` class to the RCL. The class properties are used to set component render modes.

The MAUI project is interactive by default, so no action is taken at the project level in the MAUI project other than calling `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes`.

For the Blazor Web App on the web client, the property values are assigned from [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode). When the components are loaded into a [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) for the MAUI project's native client, the render modes are unassigned (`null`) because the MAUI project explicitly sets the render mode properties to `null` when `ConfigureBlazorHybridRenderModes` is called.

`InteractiveRenderSettings.cs`:

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace MauiBlazorWeb.Shared;

public static class InteractiveRenderSettings
{
    public static IComponentRenderMode? InteractiveServer { get; set; } =
        RenderMode.InteractiveServer;
    public static IComponentRenderMode? InteractiveAuto { get; set; } =
        RenderMode.InteractiveAuto;
    public static IComponentRenderMode? InteractiveWebAssembly { get; set; } =
        RenderMode.InteractiveWebAssembly;

    public static void ConfigureBlazorHybridRenderModes()
    {
        InteractiveServer = null;
        InteractiveAuto = null;
        InteractiveWebAssembly = null;
    }
}
```

In `MauiProgram.CreateMauiApp` of `MauiProgram.cs`, call `ConfigureBlazorHybridRenderModes`:

```
InteractiveRenderSettings.ConfigureBlazorHybridRenderModes();
```

In the imports file (`_Imports.razor`) of the `.Shared` RCL, replace the `@using` statement for [Microsoft.AspNetCore.Components.Web.RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) with an `@using` statement for `InteractiveRenderSettings` to make the properties of the `InteractiveRenderSettings` class available to components:

```
- @using static Microsoft.AspNetCore.Components.Web.RenderMode
+ @using static InteractiveRenderSettings
```

Note

The assignment of render modes via the RCL's `InteractiveRenderSettings` class properties differs from a typical standalone Blazor Web App. In a Blazor Web App, the render modes are normally provided by [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) in the Blazor Web App's `_Import` file.

### Per-page/component Auto interactivity

* Interactive render mode: **Auto**

* Interactivity location: **Per-page/component**

* Solution projects

  * MAUI (`MauiBlazorWeb`): Calls `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes` in `MauiProgram.cs`.

  * Blazor Web App

    * Server project: `MauiBlazorWeb.Web`: Doesn't set an `@rendermode` directive attribute on the `HeadOutlet` and `Routes` components of the `App` component (`Components/App.razor`).
    * Client project: `MauiBlazorWeb.Web.Client`

  * RCL (`MauiBlazorWeb.Shared`): Contains the shared Razor components that set the `InteractiveAuto` render mode in each component.

Project references:

* `MauiBlazorWeb`, `MauiBlazorWeb.Web`, and `MauiBlazorWeb.Web.Client` have a project reference to `MauiBlazorWeb.Shared`.
* `MauiBlazorWeb.Web` has a project reference to `MauiBlazorWeb.Web.Client`.

Add the following `InteractiveRenderSettings` class is added to the RCL. The class properties are used to set component render modes.

The MAUI project is interactive by default, so no action is taken at the project level in the MAUI project other than calling `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes`.

For the Blazor Web App on the web client, the property values are assigned from [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode). When the components are loaded into a [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) for the MAUI project's native client, the render modes are unassigned (`null`) because the MAUI project explicitly sets the render mode properties to `null` when `ConfigureBlazorHybridRenderModes` is called.

`InteractiveRenderSettings.cs`:

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace MauiBlazorWeb.Shared;

public static class InteractiveRenderSettings
{
    public static IComponentRenderMode? InteractiveServer { get; set; } =
        RenderMode.InteractiveServer;
    public static IComponentRenderMode? InteractiveAuto { get; set; } =
        RenderMode.InteractiveAuto;
    public static IComponentRenderMode? InteractiveWebAssembly { get; set; } =
        RenderMode.InteractiveWebAssembly;

    public static void ConfigureBlazorHybridRenderModes()
    {
        InteractiveServer = null;
        InteractiveAuto = null;
        InteractiveWebAssembly = null;
    }
}
```

In `MauiProgram.CreateMauiApp` of `MauiProgram.cs`, call `ConfigureBlazorHybridRenderModes`:

```
InteractiveRenderSettings.ConfigureBlazorHybridRenderModes();
```

In the imports file (`_Imports.razor`) of the `.Shared.Client` RCL, replace the `@using` statement for [Microsoft.AspNetCore.Components.Web.RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) with an `@using` statement for `InteractiveRenderSettings` to make the properties of the `InteractiveRenderSettings` class available to components:

```
- @using static Microsoft.AspNetCore.Components.Web.RenderMode
+ @using static InteractiveRenderSettings
```

Note

The assignment of render modes via the RCL's `InteractiveRenderSettings` class properties differs from a typical standalone Blazor Web App. In a Blazor Web App, the render modes are normally provided by [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) in the Blazor Web App's `_Import` file.

### Per-page/component WebAssembly interactivity

* Interactive render mode: **WebAssembly**

* Interactivity location: **Per-page/component**

* Solution projects

  * MAUI (`MauiBlazorWeb`)

  * Blazor Web App

    * Server project: `MauiBlazorWeb.Web`: Doesn't set an `@rendermode` directive attribute on the `HeadOutlet` and `Routes` components of the `App` component (`Components/App.razor`).
    * Client project: `MauiBlazorWeb.Web.Client`

  * RCLs

    * `MauiBlazorWeb.Shared`
    * `MauiBlazorWeb.Shared.Client`: Contains the shared Razor components that set the `InteractiveWebAssembly` render mode in each component. The `.Shared.Client` RCL is maintained separately from the `.Shared` RCL because the app should maintain the components that are required to run on WebAssembly separately from the components that run on server and that stay on the server.

Project references:

* `MauiBlazorWeb` and `MauiBlazorWeb.Web` have project references to `MauiBlazorWeb.Shared`.
* `MauiBlazorWeb.Web` has a project reference to `MauiBlazorWeb.Web.Client`.
* `MauiBlazorWeb.Web.Client` and `MauiBlazorWeb.Shared` have a project reference to `MauiBlazorWeb.Shared.Client`.

Add the following [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies) parameter to the `Router` component instance for the `MauiBlazorWeb.Shared.Client` project assembly (via its imports file, `_Imports.razor`) in the `MauiBlazorWeb.Shared` project's `Routes.razor` file:

```
<Router AppAssembly="@typeof(Routes).Assembly" NotFoundPage="typeof(Pages.NotFound)" 
        AdditionalAssemblies="new [] { typeof(MauiBlazorWeb.Shared.Client._Imports).Assembly }">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(Components.Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
```

```
<Router AppAssembly="@typeof(Routes).Assembly" 
        AdditionalAssemblies="new [] { typeof(MauiBlazorWeb.Shared.Client._Imports).Assembly }">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(Components.Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
```

Add the `MauiBlazorWeb.Shared.Client` project assembly (via its imports file, `_Imports.razor`) with the following [AddAdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointconventionbuilderextensions.addadditionalassemblies) call in the `MauiBlazorWeb.Web` project's `Program.cs` file:

```
app.MapRazorComponents<App>()    
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(MauiBlazorWeb.Shared._Imports).Assembly)
    .AddAdditionalAssemblies(typeof(MauiBlazorWeb.Shared.Client._Imports).Assembly); 
```

Add the following `InteractiveRenderSettings` class is added to the `.Shared.Client` RCL. The class properties are used to set component render modes for server-based components.

The MAUI project is interactive by default, so no action is taken at the project level in the MAUI project other than calling `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes`.

For the Blazor Web App on the web client, the property values are assigned from [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode). When the components are loaded into a [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) for the MAUI project's native client, the render modes are unassigned (`null`) because the MAUI project explicitly sets the render mode properties to `null` when `ConfigureBlazorHybridRenderModes` is called.

`InteractiveRenderSettings.cs` (`.Shared.Client` RCL):

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace MauiBlazorWeb.Shared;

public static class InteractiveRenderSettings
{
    public static IComponentRenderMode? InteractiveServer { get; set; } =
        RenderMode.InteractiveServer;
    public static IComponentRenderMode? InteractiveAuto { get; set; } =
        RenderMode.InteractiveAuto;
    public static IComponentRenderMode? InteractiveWebAssembly { get; set; } =
        RenderMode.InteractiveWebAssembly;

    public static void ConfigureBlazorHybridRenderModes()
    {
        InteractiveServer = null;
        InteractiveAuto = null;
        InteractiveWebAssembly = null;
    }
}
```

A slightly different version of the `InteractiveRenderSettings` class is added to the `.Shared` RCL. In the class added to the `.Shared` RCL, `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes` of the `.Shared.Client` RCL is called. This ensures that the render mode of WebAssembly components rendered on the MAUI client are unassigned (`null`) because they're interactive by default on the native client.

`InteractiveRenderSettings.cs` (`.Shared` RCL):

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace MauiBlazorWeb.Shared
{
    public static class InteractiveRenderSettings
    {
        public static IComponentRenderMode? InteractiveServer { get; set; } = 
            RenderMode.InteractiveServer;
        public static IComponentRenderMode? InteractiveAuto { get; set; } = 
            RenderMode.InteractiveAuto;
        public static IComponentRenderMode? InteractiveWebAssembly { get; set; } = 
            RenderMode.InteractiveWebAssembly;

        public static void ConfigureBlazorHybridRenderModes()
        {
            InteractiveServer = null;
            InteractiveAuto = null;
            InteractiveWebAssembly = null;
            MauiBlazorWeb.Shared.Client.InteractiveRenderSettings
                .ConfigureBlazorHybridRenderModes();
        }
    }
}
```

In `MauiProgram.CreateMauiApp` of `MauiProgram.cs`, call `ConfigureBlazorHybridRenderModes`:

```
InteractiveRenderSettings.ConfigureBlazorHybridRenderModes();
```

In the imports file (`_Imports.razor`) of the `.Shared.Client` RCL, replace the `@using` statement for [Microsoft.AspNetCore.Components.Web.RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) with an `@using` statement for `InteractiveRenderSettings` to make the properties of the `InteractiveRenderSettings` class available to components:

```
- @using static Microsoft.AspNetCore.Components.Web.RenderMode
+ @using static InteractiveRenderSettings
```

Note

The assignment of render modes via the RCL's `InteractiveRenderSettings` class properties differs from a typical standalone Blazor Web App. In a Blazor Web App, the render modes are normally provided by [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) in the Blazor Web App's `_Import` file.

### Per-page/component Server interactivity

* Interactive render mode: **Server**

* Interactivity location: **Per-page/component**

* Solution projects

  * MAUI (`MauiBlazorWeb.Maui`): Calls `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes` in `MauiProgram.cs`.
  * Blazor Web App (`MauiBlazorWeb.Web`): Doesn't set an `@rendermode` directive attribute on the `HeadOutlet` and `Routes` components of the `App` component (`Components/App.razor`).
  * RCL (`MauiBlazorWeb.Shared`): Contains the shared Razor components that set the `InteractiveServer` render mode in each component.

`MauiBlazorWeb.Maui` and `MauiBlazorWeb.Web` have a project reference to `MauiBlazorWeb.Shared`.

Add the following `InteractiveRenderSettings` class to the RCL. The class properties are used to set component render modes.

The MAUI project is interactive by default, so no action is taken at the project level in the MAUI project other than calling `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes`.

For the Blazor Web App on the web client, the property values are assigned from [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode). When the components are loaded into a [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) for the MAUI project's native client, the render modes are unassigned (`null`) because the MAUI project explicitly sets the render mode properties to `null` when `ConfigureBlazorHybridRenderModes` is called.

`InteractiveRenderSettings.cs`:

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace MauiBlazorWeb.Shared;

public static class InteractiveRenderSettings
{
    public static IComponentRenderMode? InteractiveServer { get; set; } = 
        RenderMode.InteractiveServer;
    public static IComponentRenderMode? InteractiveAuto { get; set; } = 
        RenderMode.InteractiveAuto;
    public static IComponentRenderMode? InteractiveWebAssembly { get; set; } = 
        RenderMode.InteractiveWebAssembly;

    public static void ConfigureBlazorHybridRenderModes()
    {
        InteractiveServer = null;
        InteractiveAuto = null;
        InteractiveWebAssembly = null;
    }
}
```

In `MauiProgram.CreateMauiApp` of `MauiProgram.cs`, call `ConfigureBlazorHybridRenderModes`:

```
InteractiveRenderSettings.ConfigureBlazorHybridRenderModes();
```

In the imports file (`_Imports.razor`) of the `.Shared` RCL, replace the `@using` statement for [Microsoft.AspNetCore.Components.Web.RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) with an `@using` statement for `InteractiveRenderSettings` to make the properties of the `InteractiveRenderSettings` class available to components:

```
- @using static Microsoft.AspNetCore.Components.Web.RenderMode
+ @using static InteractiveRenderSettings
```

Note

The assignment of render modes via the RCL's `InteractiveRenderSettings` class properties differs from a typical standalone Blazor Web App. In a Blazor Web App, the render modes are normally provided by [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) in the Blazor Web App's `_Import` file.

### Per-page/component Auto interactivity

* Interactive render mode: **Auto**

* Interactivity location: **Per-page/component**

* Solution projects

  * MAUI (`MauiBlazorWeb.Maui`): Calls `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes` in `MauiProgram.cs`.

  * Blazor Web App

    * Server project: `MauiBlazorWeb.Web`: Doesn't set an `@rendermode` directive attribute on the `HeadOutlet` and `Routes` components of the `App` component (`Components/App.razor`).
    * Client project: `MauiBlazorWeb.Web.Client`

  * RCL (`MauiBlazorWeb.Shared`): Contains the shared Razor components that set the `InteractiveAuto` render mode in each component.

Project references:

* `MauiBlazorWeb.Maui`, `MauiBlazorWeb.Web`, and `MauiBlazorWeb.Web.Client` have a project reference to `MauiBlazorWeb.Shared`.
* `MauiBlazorWeb.Web` has a project reference to `MauiBlazorWeb.Web.Client`.

Add the following `InteractiveRenderSettings` class is added to the RCL. The class properties are used to set component render modes.

The MAUI project is interactive by default, so no action is taken at the project level in the MAUI project other than calling `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes`.

For the Blazor Web App on the web client, the property values are assigned from [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode). When the components are loaded into a [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) for the MAUI project's native client, the render modes are unassigned (`null`) because the MAUI project explicitly sets the render mode properties to `null` when `ConfigureBlazorHybridRenderModes` is called.

`InteractiveRenderSettings.cs`:

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace MauiBlazorWeb.Shared;

public static class InteractiveRenderSettings
{
    public static IComponentRenderMode? InteractiveServer { get; set; } = 
        RenderMode.InteractiveServer;
    public static IComponentRenderMode? InteractiveAuto { get; set; } = 
        RenderMode.InteractiveAuto;
    public static IComponentRenderMode? InteractiveWebAssembly { get; set; } = 
        RenderMode.InteractiveWebAssembly;

    public static void ConfigureBlazorHybridRenderModes()
    {
        InteractiveServer = null;
        InteractiveAuto = null;
        InteractiveWebAssembly = null;
    }
}
```

In `MauiProgram.CreateMauiApp` of `MauiProgram.cs`, call `ConfigureBlazorHybridRenderModes`:

```
InteractiveRenderSettings.ConfigureBlazorHybridRenderModes();
```

In the imports file (`_Imports.razor`) of the `.Shared` RCL, replace the `@using` statement for [Microsoft.AspNetCore.Components.Web.RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) with an `@using` statement for `InteractiveRenderSettings` to make the properties of the `InteractiveRenderSettings` class available to components:

```
- @using static Microsoft.AspNetCore.Components.Web.RenderMode
+ @using static InteractiveRenderSettings
```

Note

The assignment of render modes via the RCL's `InteractiveRenderSettings` class properties differs from a typical standalone Blazor Web App. In a Blazor Web App, the render modes are normally provided by [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) in the Blazor Web App's `_Import` file.

### Per-page/component WebAssembly interactivity

* Interactive render mode: **WebAssembly**

* Interactivity location: **Per-page/component**

* Solution projects

  * MAUI (`MauiBlazorWeb.Maui`)

  * Blazor Web App

    * Server project: `MauiBlazorWeb.Web`: Doesn't set an `@rendermode` directive attribute on the `HeadOutlet` and `Routes` components of the `App` component (`Components/App.razor`).
    * Client project: `MauiBlazorWeb.Web.Client`

  * RCLs

    * `MauiBlazorWeb.Shared`
    * `MauiBlazorWeb.Shared.Client`: Contains the shared Razor components that set the `InteractiveWebAssembly` render mode in each component. The `.Shared.Client` RCL is maintained separately from the `.Shared` RCL because the app should maintain the components that are required to run on WebAssembly separately from the components that run on server and that stay on the server.

Project references:

* `MauiBlazorWeb.Maui` and `MauiBlazorWeb.Web` have project references to `MauiBlazorWeb.Shared`.
* `MauiBlazorWeb.Web` has a project reference to `MauiBlazorWeb.Web.Client`.
* `MauiBlazorWeb.Web.Client` and `MauiBlazorWeb.Shared` have a project reference to `MauiBlazorWeb.Shared.Client`.

Add the following [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies) parameter to the `Router` component instance for the `MauiBlazorWeb.Shared.Client` project assembly (via its imports file, `_Imports.razor`) in the `MauiBlazorWeb.Shared` project's `Routes.razor` file:

```
<Router AppAssembly="@typeof(Routes).Assembly" 
        AdditionalAssemblies="new [] { typeof(MauiBlazorWeb.Shared.Client._Imports).Assembly }">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(Components.Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
```

Add the `MauiBlazorWeb.Shared.Client` project assembly (via its imports file, `_Imports.razor`) with the following [AddAdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointconventionbuilderextensions.addadditionalassemblies) call in the `MauiBlazorWeb.Web` project's `Program.cs` file:

```
app.MapRazorComponents<App>()    
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(MauiBlazorWeb.Shared._Imports).Assembly)
    .AddAdditionalAssemblies(typeof(MauiBlazorWeb.Shared.Client._Imports).Assembly); 
```

Add the following `InteractiveRenderSettings` class is added to the `.Shared.Client` RCL. The class properties are used to set component render modes for server-based components.

The MAUI project is interactive by default, so no action is taken at the project level in the MAUI project other than calling `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes`.

For the Blazor Web App on the web client, the property values are assigned from [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode). When the components are loaded into a [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) for the MAUI project's native client, the render modes are unassigned (`null`) because the MAUI project explicitly sets the render mode properties to `null` when `ConfigureBlazorHybridRenderModes` is called.

`InteractiveRenderSettings.cs` (`.Shared.Client` RCL):

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace MauiBlazorWeb.Shared;

public static class InteractiveRenderSettings
{
    public static IComponentRenderMode? InteractiveServer { get; set; } = 
        RenderMode.InteractiveServer;
    public static IComponentRenderMode? InteractiveAuto { get; set; } = 
        RenderMode.InteractiveAuto;
    public static IComponentRenderMode? InteractiveWebAssembly { get; set; } = 
        RenderMode.InteractiveWebAssembly;

    public static void ConfigureBlazorHybridRenderModes()
    {
        InteractiveServer = null;
        InteractiveAuto = null;
        InteractiveWebAssembly = null;
    }
}
```

A slightly different version of the `InteractiveRenderSettings` class is added to the `.Shared` RCL. In the class added to the `.Shared` RCL, `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes` of the `.Shared.Client` RCL is called. This ensures that the render mode of WebAssembly components rendered on the MAUI client are unassigned (`null`) because they're interactive by default on the native client.

`InteractiveRenderSettings.cs` (`.Shared` RCL):

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace MauiBlazorWeb.Shared
{
    public static class InteractiveRenderSettings
    {
        public static IComponentRenderMode? InteractiveServer { get; set; } = 
            RenderMode.InteractiveServer;
        public static IComponentRenderMode? InteractiveAuto { get; set; } = 
            RenderMode.InteractiveAuto;
        public static IComponentRenderMode? InteractiveWebAssembly { get; set; } = 
            RenderMode.InteractiveWebAssembly;

        public static void ConfigureBlazorHybridRenderModes()
        {
            InteractiveServer = null;
            InteractiveAuto = null;
            InteractiveWebAssembly = null;
            MauiBlazorWeb.Shared.Client.InteractiveRenderSettings
                .ConfigureBlazorHybridRenderModes();
        }
    }
}
```

In `MauiProgram.CreateMauiApp` of `MauiProgram.cs`, call `ConfigureBlazorHybridRenderModes`:

```
InteractiveRenderSettings.ConfigureBlazorHybridRenderModes();
```

In the imports file (`_Imports.razor`) of the `.Shared.Client` RCL, replace the `@using` statement for [Microsoft.AspNetCore.Components.Web.RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) with an `@using` statement for `InteractiveRenderSettings` to make the properties of the `InteractiveRenderSettings` class available to components:

```
- @using static Microsoft.AspNetCore.Components.Web.RenderMode
+ @using static InteractiveRenderSettings
```

Note

The assignment of render modes via the RCL's `InteractiveRenderSettings` class properties differs from a typical standalone Blazor Web App. In a Blazor Web App, the render modes are normally provided by [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) via in the Blazor Web App's `_Import` file.

## Using interfaces to support different device implementations

The following example demonstrates how to use an interface to call into different implementations across the web app and the native (MAUI) app. The following example creates a component that displays the device form factor. Use the MAUI abstraction layer for native apps and provide an implementation for the web app.

In the Razor class library (RCL), a `Services` folder contains an `IFormFactor` interface.

`Services/IFormFactor.cs`:

```
namespace MauiBlazorWeb.Shared.Services
{
    public interface IFormFactor
    {
        public string GetFormFactor();
        public string GetPlatform();
    }
}
```

The `Home` component (`Components/Pages/Home.razor`) of the RCL displays the form factor and platform.

`Components/Pages/Home.razor`:

```
@page "/"
@using MauiBlazorWeb.Shared.Services
@inject IFormFactor FormFactor

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app running on <em>@factor</em> using <em>@platform</em>.

@code {
    private string factor => FormFactor.GetFormFactor();
    private string platform => FormFactor.GetPlatform();
}
```

In the Razor class library (RCL), an `Interfaces` folder contains an `IFormFactor` interface.

`Interfaces/IFormFactor.cs`:

```
namespace MauiBlazorWeb.Shared.Interfaces;

public interface IFormFactor
{
    public string GetFormFactor();
    public string GetPlatform();
}
```

The following `DeviceFormFactor` component is present in the RCL's `Components` folder.

`Components/Pages/DeviceFormFactor.razor`:

```
@page "/device-form-factor"
@using MauiBlazorWeb.Shared.Interfaces
@inject IFormFactor FormFactor

<PageTitle>Form Factor</PageTitle>

<h1>Device Form Factor</h1>

<p>You are running on:</p>

<ul>
    <li>Form Factor: @factor</li>
    <li>Platform: @platform</li>
</ul>

<p>
    <em>This component is defined in the MauiBlazorWeb.Shared library.</em>
</p>

@code {
    private string factor => FormFactor.GetFormFactor();
    private string platform => FormFactor.GetPlatform();
}
```

In the RCL, an entry for the `DeviceFormFactor` component is part of the navigation menu in the `NavMenu` component.

In `Components/Layout/NavMenu.razor`:

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="device-form-factor">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Form Factor
    </NavLink>
</div>
```

The web and native apps contain the implementations for `IFormFactor`.

In the Blazor Web App, a folder named `Services` contains the following `FormFactor.cs` file with the `FormFactor` implementation for web app use.

`Services/FormFactor.cs` (`MauiBlazorWeb.Web` project):

```
using MauiBlazorWeb.Shared.Services;

namespace MauiBlazorWeb.Web.Services
{
    public class FormFactor : IFormFactor
    {
        public string GetFormFactor()
        {
            return "Web";
        }

        public string GetPlatform()
        {
            return Environment.OSVersion.ToString();
        }
    }
}
```

```
using MauiBlazorWeb.Shared.Interfaces;

namespace MauiBlazorWeb.Web.Services;

public class FormFactor : IFormFactor
{
    public string GetFormFactor() => "Web";

    public string GetPlatform() => Environment.OSVersion.ToString();
}
```

In the MAUI project, a folder named `Services` contains the following `FormFactor.cs` file with the `FormFactor` implementation for native use. The MAUI abstractions layer is used to write code that works on all native device platforms.

`Services/FormFactor.cs` (`MauiBlazorWeb` project):

```
using MauiBlazorWeb.Shared.Services;

namespace MauiBlazorWeb.Services
{
    public class FormFactor : IFormFactor
    {
        public string GetFormFactor()
        {
            return DeviceInfo.Idiom.ToString();
        }

        public string GetPlatform()
        {
            return DeviceInfo.Platform.ToString() + " - " + DeviceInfo.VersionString;
        }
    }
}
```

`Services/FormFactor.cs` (`MauiBlazorWeb.Maui` project):

```
using MauiBlazorWeb.Shared.Interfaces;

namespace MauiBlazorWeb.Maui.Services;

public class FormFactor : IFormFactor
{
    public string GetFormFactor() => DeviceInfo.Idiom.ToString();

    public string GetPlatform() =>
        DeviceInfo.Platform.ToString() + " - " + DeviceInfo.VersionString;
}
```

Dependency injection is used to obtain the implementations of these services.

In the MAUI project, the `MauiProgram.cs` file has following `using` statements at the top of the file:

```
using MauiBlazorWeb.Services;
using MauiBlazorWeb.Shared.Interfaces;
```

```
using MauiBlazorWeb.Maui.Services;
using MauiBlazorWeb.Shared.Interfaces;
```

Immediately before the call to `builder.Build()`, `FormFactor` is registered to add device-specific services used by the RCL:

```
builder.Services.AddSingleton<IFormFactor, FormFactor>();
```

In the Blazor Web App, the `Program` file has the following `using` statements at the top of the file:

```
using MauiBlazorWeb.Shared.Interfaces;
using MauiBlazorWeb.Web.Services;  
```

Immediately before the call to `builder.Build()`, `FormFactor` is registered to add device-specific services used by the Blazor Web App:

```
builder.Services.AddScoped<IFormFactor, FormFactor>();
```

If the solution also targets WebAssembly via a `.Web.Client` project, an implementation of the preceding API is also required in the `.Web.Client` project.

You can also use compiler preprocessor directives in your RCL to implement different UI depending on the device the app is running on. For this scenario, the app must multi-target the RCL just like the MAUI app does. For an example, see the [`BethMassi/BethTimeUntil` GitHub repository](https://github.com/BethMassi/BethTimeUntil).

## Related content

* [ASP.NET Core Blazor Hybrid authentication and authorization](../security/?view=aspnetcore-10.0\&pivots=maui)
* [ASP.NET Core Blazor render modes](../../components/render-modes?view=aspnetcore-10.0)
* [Reuse Razor components in ASP.NET Core Blazor Hybrid apps](../reuse-razor-components?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/rendering
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/performance/rendering.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor rendering performance best practices

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Optimize rendering speed to minimize rendering workload and improve UI responsiveness, which can yield a *ten-fold or higher improvement* in UI rendering speed.

## Avoid unnecessary rendering of component subtrees

You might be able to remove the majority of a parent component's rendering cost by skipping the rerendering of child component subtrees when an event occurs. You should only be concerned about skipping the rerendering subtrees that are particularly expensive to render and are causing UI lag.

At runtime, components exist in a hierarchy. A root component (the first component loaded) has child components. In turn, the root's children have their own child components, and so on. When an event occurs, such as a user selecting a button, the following process determines which components to rerender:

1. The event is dispatched to the component that rendered the event's handler. After executing the event handler, the component is rerendered.
2. When a component is rerendered, it supplies a new copy of parameter values to each of its child components.
3. After a new set of parameter values is received, Blazor decides whether to rerender the component. Components rerender if [`ShouldRender`](../components/rendering?view=aspnetcore-10.0#suppress-ui-refreshing-shouldrender) returns `true`, which is the default behavior unless overridden, and the parameter values may have changed, for example, if they're mutable objects.

The last two steps of the preceding sequence continue recursively down the component hierarchy. In many cases, the entire subtree is rerendered. Events targeting high-level components can cause expensive rerendering because every component below the high-level component must rerender.

To prevent rendering recursion into a particular subtree, use either of the following approaches:

* Ensure that child component parameters are of specific immutable types†, such as `string`, `int`, `bool`, and `DateTime`. The built-in logic for detecting changes automatically skips rerendering if the immutable parameter values haven't changed. If you render a child component with `<Customer CustomerId="item.CustomerId" />`, where `CustomerId` is an `int` type, then the `Customer` component isn't rerendered unless `item.CustomerId` changes.

* Override [`ShouldRender`](../components/rendering?view=aspnetcore-10.0#suppress-ui-refreshing-shouldrender), returning `false`:

  * When parameters are nonprimitive types or unsupported immutable types†, such as complex custom model types or [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) values, and the parameter values haven't changed,
  * If authoring a UI-only component that doesn't change after the initial render, regardless of parameter value changes.

†For more information, see [the change detection logic in Blazor's reference source (`ChangeDetection.cs`)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ChangeDetection.cs).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

The following airline flight search tool example uses private fields to track the necessary information to detect changes. The previous inbound flight identifier (`prevInboundFlightId`) and previous outbound flight identifier (`prevOutboundFlightId`) track information for the next potential component update. If either of the flight identifiers change when the component's parameters are set in [`OnParametersSet`](../components/lifecycle?view=aspnetcore-10.0#after-parameters-are-set-onparameterssetasync), the component is rerendered because `shouldRender` is set to `true`. If `shouldRender` evaluates to `false` after checking the flight identifiers, an expensive rerender is avoided:

```
@code {
    private int prevInboundFlightId = 0;
    private int prevOutboundFlightId = 0;
    private bool shouldRender;

    [Parameter]
    public FlightInfo? InboundFlight { get; set; }

    [Parameter]
    public FlightInfo? OutboundFlight { get; set; }

    protected override void OnParametersSet()
    {
        shouldRender = InboundFlight?.FlightId != prevInboundFlightId
            || OutboundFlight?.FlightId != prevOutboundFlightId;

        prevInboundFlightId = InboundFlight?.FlightId ?? 0;
        prevOutboundFlightId = OutboundFlight?.FlightId ?? 0;
    }

    protected override bool ShouldRender() => shouldRender;
}
```

An event handler can also set `shouldRender` to `true`. For most components, determining rerendering at the level of individual event handlers usually isn't necessary.

For more information, see the following resources:

* [ASP.NET Core Razor component lifecycle](../components/lifecycle?view=aspnetcore-10.0)
* [ShouldRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.shouldrender)
* [ASP.NET Core Razor component rendering](../components/rendering?view=aspnetcore-10.0#suppress-ui-refreshing-shouldrender)

## Virtualization

When rendering large amounts of UI within a loop, for example, a list or grid with thousands of entries, the sheer quantity of rendering operations can lead to a lag in UI rendering. Given that the user can only see a small number of elements at once without scrolling, it's often wasteful to spend time rendering elements that aren't currently visible.

Blazor provides the [Virtualize\<TItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.web.virtualization.virtualize-1) component to create the appearance and scroll behaviors of an arbitrarily-large list while only rendering the list items that are within the current scroll viewport. For example, a component can render a list with 100,000 entries but only pay the rendering cost of 20 items that are visible.

For more information, see [ASP.NET Core Razor component virtualization](../components/virtualization?view=aspnetcore-10.0).

## Create lightweight, optimized components

Most Razor components don't require aggressive optimization efforts because most components don't repeat in the UI and don't rerender at high frequency. For example, routable components with an `@page` directive and components used to render high-level pieces of the UI, such as dialogs or forms, most likely appear only one at a time and only rerender in response to a user gesture. These components don't usually create high rendering workload, so you can freely use any combination of framework features without much concern about rendering performance.

However, there are common scenarios where components are repeated at scale and often result in poor UI performance:

* Large nested forms with hundreds of individual elements, such as inputs or labels.
* Grids with hundreds of rows or thousands of cells.
* Scatter plots with millions of data points.

If modelling each element, cell, or data point as a separate component instance, there are often so many of them that their rendering performance becomes critical. This section provides advice on making such components lightweight so that the UI remains fast and responsive.

## Avoid thousands of component instances

Each component is a separate island that can render independently of its parents and children. By choosing how to split the UI into a hierarchy of components, you are taking control over the granularity of UI rendering. This can result in either good or poor performance.

By splitting the UI into separate components, you can have smaller portions of the UI rerender when events occur. In a table with many rows that have a button in each row, you may be able to have only that single row rerender by using a child component instead of the whole page or table. However, each component requires additional memory and CPU overhead to deal with its independent state and rendering lifecycle.

In a test performed by the ASP.NET Core product unit engineers, a rendering overhead of around 0.06 ms per component instance was seen in a Blazor WebAssembly app. The test app rendered a simple component that accepts three parameters. Internally, the overhead is largely due to retrieving per-component state from dictionaries and passing and receiving parameters. By multiplication, you can see that adding 2,000 extra component instances would add 0.12 seconds to the rendering time and the UI would begin feeling slow to users.

It's possible to make components more lightweight so that you can have more of them. However, a more powerful technique is often to avoid having so many components to render. The following sections describe two approaches that you can take.

For more information on memory management, see [Manage memory in deployed ASP.NET Core server-side Blazor apps](../host-and-deploy/server/memory-management?view=aspnetcore-10.0).

**Inline child components into their parents:** Consider the following portion of a parent component that renders child components in a loop:

```
<div class="chat">
    @foreach (var message in messages)
    {
        <ChatMessageDisplay Message="message" />
    }
</div>
```

`ChatMessageDisplay.razor`:

```
<div class="chat-message">
    <span class="author">@Message.Author</span>
    <span class="text">@Message.Text</span>
</div>

@code {
    [Parameter]
    public ChatMessage? Message { get; set; }
}
```

The preceding example performs well if thousands of messages aren't shown at once. To show thousands of messages at once, consider *not* factoring out the separate `ChatMessageDisplay` component. Instead, inline the child component into the parent. The following approach avoids the per-component overhead of rendering so many child components at the cost of losing the ability to rerender each child component's markup independently:

```
<div class="chat">
    @foreach (var message in messages)
    {
        <div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>
    }
</div>
```

**Define reusable `RenderFragments` in code:** You might be factoring out child components purely as a way of reusing rendering logic. If that's the case, you can create reusable rendering logic without implementing additional components. In any component's `@code` block, define a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment). Render the fragment from any location as many times as needed:

```
@RenderWelcomeInfo

<p>Render the welcome content a second time:</p>

@RenderWelcomeInfo

@code {
    private RenderFragment RenderWelcomeInfo = @<p>Welcome to your new app!</p>;
}
```

To make [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) code reusable across multiple components, declare the [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) [`public`](/en-us/dotnet/csharp/language-reference/keywords/public) and [`static`](/en-us/dotnet/csharp/language-reference/keywords/static):

```
public static RenderFragment SayHello = @<h1>Hello!</h1>;
```

`SayHello` in the preceding example can be invoked from an unrelated component. This technique is useful for building libraries of reusable markup snippets that render without per-component overhead.

[RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) delegates can accept parameters. The following component passes the message (`message`) to the [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) delegate:

```
<div class="chat">
    @foreach (var message in messages)
    {
        @ChatMessageDisplay(message)
    }
</div>

@code {
    private RenderFragment<ChatMessage> ChatMessageDisplay = message =>
        @<div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>;
}
```

The preceding approach reuses rendering logic without per-component overhead. However, the approach doesn't permit refreshing the subtree of the UI independently, nor does it have the ability to skip rendering the subtree of the UI when its parent renders because there's no component boundary. Assignment to a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) delegate is only supported in Razor component files (`.razor`).

For a non-static field, method, or property that can't be referenced by a field initializer, such as `TitleTemplate` in the following example, use a property instead of a field for the [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment):

```
protected RenderFragment DisplayTitle =>
    @<div>
        @TitleTemplate
    </div>;
```

## Don't receive too many parameters

If a component repeats extremely often, for example, hundreds or thousands of times, the overhead of passing and receiving each parameter builds up.

It's rare that too many parameters severely restricts performance, but it can be a factor. For a `TableCell` component that renders 4,000 times within a grid, each parameter passed to the component adds around 15 ms to the total rendering cost. Passing ten parameters requires around 150 ms and causes a UI rendering lag.

To reduce parameter load, bundle multiple parameters in a custom class. For example, a table cell component might accept a common object. In the following example, `Data` is different for every cell, but `Options` is common across all cell instances:

```
@typeparam TItem

...

@code {
    [Parameter]
    public TItem? Data { get; set; }
    
    [Parameter]
    public GridOptions? Options { get; set; }
}
```

However, keep in mind that bundling primitive parameters into a class isn't always an advantage. While it can reduce parameter count, it also impacts how change detection and rendering behave. Passing non-primitive parameters always triggers a re-render, because Blazor can't know whether arbitrary objects have internally mutable state, whereas passing primitive parameters only triggers a re-render if their values have actually changed.

Also, consider that it might be an improvement not to have a table cell component, as shown in the preceding example, and instead [inline its logic into the parent component](#avoid-thousands-of-component-instances).

Note

When multiple approaches are available for improving performance, benchmarking the approaches is usually required to determine which approach yields the best results.

For more information on generic type parameters (`@typeparam`), see the following resources:

* [Razor syntax reference for ASP.NET Core](../../mvc/views/razor?view=aspnetcore-10.0#typeparam)
* [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#generic-type-parameter-support)
* [ASP.NET Core Blazor templated components](../components/templated-components?view=aspnetcore-10.0)

## Ensure cascading parameters are fixed

The [`CascadingValue` component](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component) has an optional `IsFixed` parameter:

* If `IsFixed` is `false` (the default), every recipient of the cascaded value sets up a subscription to receive change notifications. Each `[CascadingParameter]` is **substantially more expensive** than a regular `[Parameter]` due to the subscription tracking.
* If `IsFixed` is `true` (for example, `<CascadingValue Value="someValue" IsFixed="true">`), recipients receive the initial value but don't set up a subscription to receive updates. Each `[CascadingParameter]` is lightweight and no more expensive than a regular `[Parameter]`.

Setting `IsFixed` to `true` improves performance if there are a large number of other components that receive the cascaded value. Wherever possible, set `IsFixed` to `true` on cascaded values. You can set `IsFixed` to `true` when the supplied value doesn't change over time.

Where a component passes `this` as a cascaded value, `IsFixed` can also be set to `true`, because `this` never changes during the component's lifecycle:

```
<CascadingValue Value="this" IsFixed="true">
    <SomeOtherComponents>
</CascadingValue>
```

For more information, see [ASP.NET Core Blazor cascading values and parameters](../components/cascading-values-and-parameters?view=aspnetcore-10.0).

## Avoid attribute splatting with `CaptureUnmatchedValues`

Components can elect to receive "unmatched" parameter values using the [CaptureUnmatchedValues](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute.captureunmatchedvalues#microsoft-aspnetcore-components-parameterattribute-captureunmatchedvalues) flag:

```
<div @attributes="OtherAttributes">...</div>

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? OtherAttributes { get; set; }
}
```

This approach allows passing arbitrary additional attributes to the element. However, this approach is expensive because the renderer must:

* Match all of the supplied parameters against the set of known parameters to build a dictionary.
* Keep track of how multiple copies of the same attribute overwrite each other.

Use [CaptureUnmatchedValues](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute.captureunmatchedvalues#microsoft-aspnetcore-components-parameterattribute-captureunmatchedvalues) where component rendering performance isn't critical, such as components that aren't repeated frequently. For components that render at scale, such as each item in a large list or in the cells of a grid, try to avoid attribute splatting.

For more information, see [ASP.NET Core Blazor attribute splatting and arbitrary parameters](../components/splat-attributes-and-arbitrary-parameters?view=aspnetcore-10.0).

## Implement `SetParametersAsync` manually

A significant source of per-component rendering overhead is writing incoming parameter values to `[Parameter]` properties. The renderer uses [reflection](/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/) to write the parameter values, which can lead to poor performance at scale.

In some extreme cases, you may wish to avoid the reflection and implement your own parameter-setting logic manually. This may be applicable when:

* A component renders extremely often, for example, when there are hundreds or thousands of copies of the component in the UI.
* A component accepts many parameters.
* You find that the overhead of receiving parameters has an observable impact on UI responsiveness.

In extreme cases, you can override the component's virtual [SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync) method and implement your own component-specific logic. The following example deliberately avoids dictionary lookups:

```
@code {
    [Parameter]
    public int MessageId { get; set; }

    [Parameter]
    public string? Text { get; set; }

    [Parameter]
    public EventCallback<string> TextChanged { get; set; }

    [Parameter]
    public Theme CurrentTheme { get; set; }

    public override Task SetParametersAsync(ParameterView parameters)
    {
        foreach (var parameter in parameters)
        {
            switch (parameter.Name)
            {
                case nameof(MessageId):
                    MessageId = (int)parameter.Value;
                    break;
                case nameof(Text):
                    Text = (string)parameter.Value;
                    break;
                case nameof(TextChanged):
                    TextChanged = (EventCallback<string>)parameter.Value;
                    break;
                case nameof(CurrentTheme):
                    CurrentTheme = (Theme)parameter.Value;
                    break;
                default:
                    throw new ArgumentException($"Unknown parameter: {parameter.Name}");
            }
        }

        return base.SetParametersAsync(ParameterView.Empty);
    }
}
```

In the preceding code, returning the base class [SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync) runs the normal lifecycle method without assigning parameters again.

As you can see in the preceding code, overriding [SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync) and supplying custom logic is complicated and laborious, so we don't generally recommend adopting this approach. In extreme cases, the approach can yield a small rendering improvement, typically under \~10% even at 10,000+ component instances when targeting .NET 10. The potential gains are smaller in later releases because reflection-based parameter assignment is optimized. Only consider this approach in the extreme scenarios listed earlier in this section and benchmark first—the savings are usually dwarfed by other costs, for example, the SignalR [diff (DOM edits)](../components/lifecycle?view=aspnetcore-10.0#lifecycle-events) transport for Interactive Server.

As you can see in the preceding code, overriding [SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync) and supplying custom logic is complicated and laborious, so we don't generally recommend adopting this approach. In extreme cases, the approach can improve rendering performance by 20-25%, but you should only consider this approach in the extreme scenarios listed earlier in this section.

## Don't trigger events too rapidly

Some browser events fire extremely frequently. For example, `onmousemove` and `onscroll` can fire tens or hundreds of times per second. In most cases, you don't need to perform UI updates this frequently. If events are triggered too rapidly, you may harm UI responsiveness or consume excessive CPU time.

Rather than use native events that rapidly fire, consider the use of JS interop to register a callback that fires less frequently. For example, the following component displays the position of the mouse but only updates at most once every 500 ms:

```
@implements IDisposable
@inject IJSRuntime JS

<h1>@message</h1>

<div @ref="mouseMoveElement" style="border:1px dashed red;height:200px;">
    Move mouse here
</div>

@code {
    private ElementReference mouseMoveElement;
    private DotNetObjectReference<MyComponent>? selfReference;
    private string message = "Move the mouse in the box";

    [JSInvokable]
    public void HandleMouseMove(int x, int y)
    {
        message = $"Mouse move at {x}, {y}";
        StateHasChanged();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            selfReference = DotNetObjectReference.Create(this);
            var minInterval = 500;

            await JS.InvokeVoidAsync("onThrottledMouseMove", 
                mouseMoveElement, selfReference, minInterval);
        }
    }

    public void Dispose() => selfReference?.Dispose();
}
```

The corresponding JavaScript code registers the DOM event listener for mouse movement. In this example, the event listener uses [Lodash's `throttle` function](https://lodash.com/docs/4.17.15#throttle) to limit the rate of invocations:

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script>
  function onThrottledMouseMove(elem, component, interval) {
    elem.addEventListener('mousemove', _.throttle(e => {
      component.invokeMethodAsync('HandleMouseMove', e.offsetX, e.offsetY);
    }, interval));
  }
</script>
```

## Avoid rerendering after handling events without state changes

Components inherit from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase), which automatically invokes [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) after the component's event handlers are invoked. In some cases, it might be unnecessary or undesirable to trigger a rerender after an event handler is invoked. For example, an event handler might not modify component state. In these scenarios, the app can leverage the [IHandleEvent](/en-us/dotnet/api/microsoft.aspnetcore.components.ihandleevent) interface to control the behavior of Blazor's event handling.

Note

The approach in this section doesn't flow exceptions to [error boundaries](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries). For more information and demonstration code that supports error boundaries by calling [ComponentBase.DispatchExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.dispatchexceptionasync), see [AsNonRenderingEventHandler + ErrorBoundary = unexpected behavior (`dotnet/aspnetcore` #54543)](https://github.com/dotnet/aspnetcore/issues/54543).

To prevent rerenders for all of a component's event handlers, implement [IHandleEvent](/en-us/dotnet/api/microsoft.aspnetcore.components.ihandleevent) and provide a [IHandleEvent.HandleEventAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.ihandleevent.handleeventasync) task that invokes the event handler without calling [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged).

In the following example, no event handler added to the component triggers a rerender, so `HandleSelect` doesn't result in a rerender when invoked.

`HandleSelect1.razor`:

```
@page "/handle-select-1"
@using Microsoft.Extensions.Logging
@implements IHandleEvent
@inject ILogger<HandleSelect1> Logger

<p>
    Last render DateTime: @dt
</p>

<button @onclick="HandleSelect">
    Select me (Avoids Rerender)
</button>

@code {
    private DateTime dt = DateTime.Now;

    private void HandleSelect()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler doesn't trigger a rerender.");
    }

    Task IHandleEvent.HandleEventAsync(
        EventCallbackWorkItem callback, object? arg) => callback.InvokeAsync(arg);
}
```

In addition to preventing rerenders after event handlers fire in a component in a global fashion, it's possible to prevent rerenders after a single event handler by employing the following utility method.

Add the following `EventUtil` class to a Blazor app. The static actions and functions at the top of the `EventUtil` class provide handlers that cover several combinations of arguments and return types that Blazor uses when handling events.

`EventUtil.cs`:

```
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

public static class EventUtil
{
    public static Action AsNonRenderingEventHandler(Action callback)
        => new SyncReceiver(callback).Invoke;
    public static Action<TValue> AsNonRenderingEventHandler<TValue>(
            Action<TValue> callback)
        => new SyncReceiver<TValue>(callback).Invoke;
    public static Func<Task> AsNonRenderingEventHandler(Func<Task> callback)
        => new AsyncReceiver(callback).Invoke;
    public static Func<TValue, Task> AsNonRenderingEventHandler<TValue>(
            Func<TValue, Task> callback)
        => new AsyncReceiver<TValue>(callback).Invoke;

    private record SyncReceiver(Action callback) 
        : ReceiverBase { public void Invoke() => callback(); }
    private record SyncReceiver<T>(Action<T> callback) 
        : ReceiverBase { public void Invoke(T arg) => callback(arg); }
    private record AsyncReceiver(Func<Task> callback) 
        : ReceiverBase { public Task Invoke() => callback(); }
    private record AsyncReceiver<T>(Func<T, Task> callback) 
        : ReceiverBase { public Task Invoke(T arg) => callback(arg); }

    private record ReceiverBase : IHandleEvent
    {
        public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => 
            item.InvokeAsync(arg);
    }
}
```

Call `EventUtil.AsNonRenderingEventHandler` to call an event handler that doesn't trigger a render when invoked.

In the following example:

* Selecting the first button, which calls `HandleClick1`, triggers a rerender.
* Selecting the second button, which calls `HandleClick2`, doesn't trigger a rerender.
* Selecting the third button, which calls `HandleClick3`, doesn't trigger a rerender and uses [event arguments](../components/event-handling?view=aspnetcore-10.0#event-arguments) ([MouseEventArgs](/en-us/dotnet/api/microsoft.aspnetcore.components.web.mouseeventargs)).

`HandleSelect2.razor`:

```
@page "/handle-select-2"
@using Microsoft.Extensions.Logging
@inject ILogger<HandleSelect2> Logger

<p>
    Last render DateTime: @dt
</p>

<button @onclick="HandleClick1">
    Select me (Rerenders)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler(HandleClick2)">
    Select me (Avoids Rerender)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(HandleClick3)">
    Select me (Avoids Rerender and uses <code>MouseEventArgs</code>)
</button>

@code {
    private DateTime dt = DateTime.Now;

    private void HandleClick1()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler triggers a rerender.");
    }

    private void HandleClick2()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler doesn't trigger a rerender.");
    }
    
    private void HandleClick3(MouseEventArgs args)
    {
        dt = DateTime.Now;

        Logger.LogInformation(
            "This event handler doesn't trigger a rerender. " +
            "Mouse coordinates: {ScreenX}:{ScreenY}", 
            args.ScreenX, args.ScreenY);
    }
}
```

In addition to implementing the [IHandleEvent](/en-us/dotnet/api/microsoft.aspnetcore.components.ihandleevent) interface, leveraging the other best practices described in this article can also help reduce unwanted renders after events are handled. For example, overriding [ShouldRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.shouldrender) in child components of the target component can be used to control rerendering.

## Avoid recreating delegates for many repeated elements or components

Blazor's recreation of [lambda expression delegates](../components/event-handling?view=aspnetcore-10.0#lambda-expressions) for elements or components in a loop can lead to poor performance.

The following component shown in the [event handling article](../components/event-handling?view=aspnetcore-10.0#lambda-expressions) renders a set of buttons. Each button assigns a delegate to its `@onclick` event, which is fine if there aren't many buttons to render.

`EventHandlerExample5.razor`:

```
@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}
```

```
@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}
```

If a large number of buttons are rendered using the preceding approach, rendering speed is adversely impacted leading to a poor user experience. To render a large number of buttons with a callback for click events, the following example uses a collection of button objects that assign each button's `@onclick` delegate to an [Action](/en-us/dotnet/api/system.action). The following approach doesn't require Blazor to rebuild all of the button delegates each time the buttons are rendered:

`LambdaEventPerformance.razor`:

```
@page "/lambda-event-performance"

<h1>@heading</h1>

@foreach (var button in Buttons)
{
    <p>
        <button @key="button.Id" @onclick="button.Action">
            Button #@button.Id
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private List<Button> Buttons { get; set; } = new();

    protected override void OnInitialized()
    {
        for (var i = 0; i < 100; i++)
        {
            var button = new Button();

            button.Id = Guid.NewGuid().ToString();

            button.Action = (e) =>
            {
                UpdateHeading(button, e);
            };

            Buttons.Add(button);
        }
    }

    private void UpdateHeading(Button button, MouseEventArgs e)
    {
        heading = $"Selected #{button.Id} at {e.ClientX}:{e.ClientY}";
    }

    private class Button
    {
        public string? Id { get; set; }
        public Action<MouseEventArgs> Action { get; set; } = e => { };
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/page
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/page.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 3, scaffolded Razor Pages in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT)

This tutorial examines the Razor Pages created by scaffolding in the [previous tutorial](model?view=aspnetcore-10.0).

## The Create, Delete, Details, and Edit pages

Examine the `Pages/MoviePages/Index.cshtml.cs` Page Model:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.MoviePages
{
    public class IndexModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IList<Movie> Movie { get;set; } = default!;

        public async Task OnGetAsync()
        {
            Movie = await _context.Movie.ToListAsync();
        }
    }
}
```

Razor Pages derive from [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel). By convention, the `PageModel` derived class is named `PageNameModel`. For example, the Index page is named `IndexModel`.

The constructor uses [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to add the `RazorPagesMovieContext` to the page:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }
```

For more information on asynchronous programming with Entity Framework, see [Asynchronous code](../../data/ef-rp/intro?view=aspnetcore-10.0#asynchronous-code).

When a `GET` request is made for the page, the `OnGetAsync` method returns a list of movies to the Razor Page. On a Razor Page, `OnGetAsync` or `OnGet` is called to initialize the state of the page. In this case, `OnGetAsync` gets a list of movies and displays them.

When `OnGet` returns `void` or `OnGetAsync` returns `Task`, don't use a return statement. For example, examine the Privacy Page:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesMovie.Pages
{
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }

}
```

When the return type is [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or `Task<IActionResult>`, you must provide a return statement. For example, the `Pages/MoviePages/Create.cshtml.cs OnPostAsync` method:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

Examine the `Pages/MoviePages/Index.cshtml` Razor Page:

```
@page
@model RazorPagesMovie.Pages.MoviePages.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Razor can transition from HTML into C# or into Razor-specific markup. When an `@` symbol is followed by a [Razor reserved keyword](../../mvc/views/razor?view=aspnetcore-10.0#razor-reserved-keywords), it transitions into Razor-specific markup. Otherwise, it transitions into C#.

### The @page directive

The `@page` Razor directive makes the file an MVC action, which means that it can handle requests. `@page` must be the first Razor directive on a page. `@page` and `@model` are examples of transitioning into Razor-specific markup. For more information, see [Razor syntax](../../mvc/views/razor?view=aspnetcore-10.0#razor-syntax).

### The @model directive

```
@page
@model RazorPagesMovie.Pages.MoviePages.IndexModel
```

The `@model` directive specifies the type of the model passed to the Razor Page. In the preceding example, the `@model` line makes the `PageModel` derived class available to the Razor Page. The model is used in the `@Html.DisplayNameFor` and `@Html.DisplayFor` [HTML Helpers](/en-us/aspnet/mvc/overview/older-versions-1/views/creating-custom-html-helpers-cs#understanding-html-helpers) on the page.

Examine the lambda expression used in the following HTML Helper:

```
@Html.DisplayNameFor(model => model.Movie[0].Title)
```

The [DisplayNameFor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper-1.displaynamefor) HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. The lambda expression is inspected rather than evaluated. That means there's no access violation when `model`, `model.Movie`, or `model.Movie[0]` is `null` or empty. When the lambda expression is evaluated, for example, by using `@Html.DisplayFor(modelItem => item.Title)`, the model's property values are evaluated.

### The layout page

Select the menu links **RazorPagesMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The `Pages/Shared/_Layout.cshtml` file implements the menu layout.

Open and examine the `Pages/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates let you:

* Specify the HTML container layout in one place.
* Apply the layout to multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the page-specific views appear, *wrapped* in the layout page. For example, when you select the **Privacy** link, the `Pages/Privacy.cshtml` view renders inside the `RenderBody` method.

### ViewData and layout

Consider the following markup from the `Pages/MoviePages/Index.cshtml` file:

```
@page
@model RazorPagesMovie.Pages.MoviePages.IndexModel

@{
    ViewData["Title"] = "Index";
}
```

The preceding highlighted markup is an example of Razor transitioning into C#. The `{` and `}` characters enclose a block of C# code.

The `PageModel` base class contains a `ViewData` dictionary property that you can use to pass data to a view. Add objects to the `ViewData` dictionary by using a ***key value*** pattern. In the preceding sample, the `Title` property is added to the `ViewData` dictionary.

The `Title` property is used in the `Pages/Shared/_Layout.cshtml` file. The following markup shows the first few lines of the `_Layout.cshtml` file.

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - RazorPagesMovie</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/RazorPagesMovie.styles.css" asp-append-version="true" />
```

### Update the layout

1. Change the `<title>` element in the `Pages/Shared/_Layout.cshtml` file to display **Movie** rather than **RazorPagesMovie**.

   ```
   <!DOCTYPE html>
   <html lang="en">
   <head>
       <meta charset="utf-8" />
       <meta name="viewport" content="width=device-width, initial-scale=1.0" />
       <title>@ViewData["Title"] - Movie</title>
   ```

2. Find the following anchor element in the `Pages/Shared/_Layout.cshtml` file.

   ```
   <a class="navbar-brand" asp-area="" asp-page="/Index">RazorPagesMovie</a>
   ```

3. Replace the preceding element with the following markup:

   ```
   <a class="navbar-brand" asp-page="/MoviePages/Index">RpMovie</a>
   ```

   The preceding anchor element is a [Tag Helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). In this case, it's the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0). The `asp-page="/MoviePages/Index"` Tag Helper attribute and value creates a link to the `/MoviePages/Index` Razor Page. The `asp-area` attribute value is empty, so the area isn't used in the link. See [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0) for more information.

4. Save the changes and test the app by selecting the **RpMovie** link. See the [\_Layout.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie10/Pages/Shared/_Layout.cshtml) file in GitHub if you have any problems.

5. Test the **Home**, **RpMovie**, **Create**, **Edit**, and **Delete** links. Each page sets the title, which you can see in the browser tab. When you bookmark a page, the title is used for the bookmark.

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize the app. See this [GitHub issue 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420) for instructions on adding decimal comma.

The `Layout` property is set in the `Pages/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The preceding markup sets the layout file to `Pages/Shared/_Layout.cshtml` for all Razor files under the *Pages* folder. See [Layout](../../razor-pages/?view=aspnetcore-10.0#layout) for more information.

### The Create page model

Examine the `Pages/MoviePages/Create.cshtml.cs` page model:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.MoviePages
{
    public class CreateModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public CreateModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Movie Movie { get; set; } = default!;

        // To protect from overposting attacks, see https://aka.ms/RazorPagesCRUD
        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Movie.Add(Movie);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}
```

The `OnGet` method initializes any state needed for the page. The Create page doesn't have any state to initialize, so `Page` is returned. Later in the tutorial, an example of `OnGet` initializing state is shown. The `Page` method creates a `PageResult` object that renders the `Create.cshtml` page.

The `Movie` property uses the [\[BindProperty\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute to opt-in to [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0). When the Create form posts the form values, the ASP.NET Core runtime binds the posted values to the `Movie` model.

The `OnPostAsync` method runs when the page posts form data:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors are caught on the client before the form is posted. An example of a model error is posting a value for the date field that can't be converted to a date. Client-side validation and model validation are discussed later in the tutorial.

If there are no model errors:

* The data is saved.
* The browser is redirected to the Index page.

### The Create Razor Page

Examine the `Pages/MoviePages/Create.cshtml` Razor Page file:

```
@page
@model RazorPagesMovie.Pages.MoviePages.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Movie.Title" class="control-label"></label>
                <input asp-for="Movie.Title" class="form-control" />
                <span asp-validation-for="Movie.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.ReleaseDate" class="control-label"></label>
                <input asp-for="Movie.ReleaseDate" class="form-control" />
                <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Genre" class="control-label"></label>
                <input asp-for="Movie.Genre" class="form-control" />
                <span asp-validation-for="Movie.Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Price" class="control-label"></label>
                <input asp-for="Movie.Price" class="form-control" />
                <span asp-validation-for="Movie.Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

* [Visual Studio](#tabpanel_1_visual-studio)

Visual Studio displays the following tags in a distinctive bold font used for Tag Helpers:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The `<form method="post">` element is a [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). The Form Tag Helper automatically includes an [antiforgery token](../../security/anti-request-forgery?view=aspnetcore-10.0).

The scaffolding engine creates Razor markup for each field in the model, except the ID, similar to the following code:

```
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
    <label asp-for="Movie.Title" class="control-label"></label>
    <input asp-for="Movie.Title" class="form-control" />
    <span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
```

The [Validation Tag Helpers](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) (`<div asp-validation-summary` and `<span asp-validation-for`) display validation errors. Validation is covered in more detail later in this series.

The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-label-tag-helper) (`<label asp-for="Movie.Title" class="control-label"></label>`) generates the label caption and `[for]` attribute for the `Title` property.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) (`<input asp-for="Movie.Title" class="form-control">`) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client side.

For more information on Tag Helpers such as `<form method="post">`, see [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

## Next steps

[Previous: Add a model](model?view=aspnetcore-10.0) [Next: Work with a database](sql?view=aspnetcore-10.0)

## The Create, Delete, Details, and Edit pages

Examine the `Pages/Movies/Index.cshtml.cs` Page Model:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class IndexModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IList<Movie> Movie { get;set; } = default!;

        public async Task OnGetAsync()
        {
            Movie = await _context.Movie.ToListAsync();
        }
    }
}
```

Razor Pages are derived from [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel). By convention, the `PageModel` derived class is named `PageNameModel`. For example, the Index page is named `IndexModel`.

The constructor uses [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to add the `RazorPagesMovieContext` to the page:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }
```

See [Asynchronous code](../../data/ef-rp/intro?view=aspnetcore-10.0#asynchronous-code) for more information on asynchronous programming with Entity Framework.

When a `GET` request is made for the page, the `OnGetAsync` method returns a list of movies to the Razor Page. On a Razor Page, `OnGetAsync` or `OnGet` is called to initialize the state of the page. In this case, `OnGetAsync` gets a list of movies and displays them.

When `OnGet` returns `void` or `OnGetAsync` returns `Task`, no return statement is used. For example, examine the Privacy Page:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesMovie.Pages
{
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }

}
```

When the return type is [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or `Task<IActionResult>`, a return statement must be provided. For example, the `Pages/Movies/Create.cshtml.cs OnPostAsync` method:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

[]()Examine the `Pages/Movies/Index.cshtml` Razor Page:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Razor can transition from HTML into C# or into Razor-specific markup. When an `@` symbol is followed by a [Razor reserved keyword](../../mvc/views/razor?view=aspnetcore-10.0#razor-reserved-keywords), it transitions into Razor-specific markup, otherwise it transitions into C#.

### The @page directive

The `@page` Razor directive makes the file an MVC action, which means that it can handle requests. `@page` must be the first Razor directive on a page. `@page` and `@model` are examples of transitioning into Razor-specific markup. See [Razor syntax](../../mvc/views/razor?view=aspnetcore-10.0#razor-syntax) for more information.

[]()

### The @model directive

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
```

The `@model` directive specifies the type of the model passed to the Razor Page. In the preceding example, the `@model` line makes the `PageModel` derived class available to the Razor Page. The model is used in the `@Html.DisplayNameFor` and `@Html.DisplayFor` [HTML Helpers](/en-us/aspnet/mvc/overview/older-versions-1/views/creating-custom-html-helpers-cs#understanding-html-helpers) on the page.

Examine the lambda expression used in the following HTML Helper:

```
@Html.DisplayNameFor(model => model.Movie[0].Title)
```

The [DisplayNameFor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper-1.displaynamefor) HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. The lambda expression is inspected rather than evaluated. That means there is no access violation when `model`, `model.Movie`, or `model.Movie[0]` is `null` or empty. When the lambda expression is evaluated, for example, with `@Html.DisplayFor(modelItem => item.Title)`, the model's property values are evaluated.

### The layout page

Select the menu links **RazorPagesMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Pages/Shared/_Layout.cshtml` file.

Open and examine the `Pages/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow the HTML container layout to be:

* Specified in one place.
* Applied in multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the page-specific views show up, *wrapped* in the layout page. For example, select the **Privacy** link and the `Pages/Privacy.cshtml` view is rendered inside the `RenderBody` method.

[]()

### ViewData and layout

Consider the following markup from the `Pages/Movies/Index.cshtml` file:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}
```

The preceding highlighted markup is an example of Razor transitioning into C#. The `{` and `}` characters enclose a block of C# code.

The `PageModel` base class contains a `ViewData` dictionary property that can be used to pass data to a View. Objects are added to the `ViewData` dictionary using a ***key value*** pattern. In the preceding sample, the `Title` property is added to the `ViewData` dictionary.

The `Title` property is used in the `Pages/Shared/_Layout.cshtml` file. The following markup shows the first few lines of the `_Layout.cshtml` file.

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - RazorPagesMovie</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/RazorPagesMovie.styles.css" asp-append-version="true" />
```

### Update the layout

1. Change the `<title>` element in the `Pages/Shared/_Layout.cshtml` file to display **Movie** rather than **RazorPagesMovie**.

   ```
   <!DOCTYPE html>
   <html lang="en">
   <head>
       <meta charset="utf-8" />
       <meta name="viewport" content="width=device-width, initial-scale=1.0" />
       <title>@ViewData["Title"] - Movie</title>
   ```

2. Find the following anchor element in the `Pages/Shared/_Layout.cshtml` file.

   ```
   <a class="navbar-brand" asp-area="" asp-page="/Index">RazorPagesMovie</a>
   ```

3. Replace the preceding element with the following markup:

   ```
   <a class="navbar-brand" asp-page="/Movies/Index">RpMovie</a>
   ```

   The preceding anchor element is a [Tag Helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). In this case, it's the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0). The `asp-page="/Movies/Index"` Tag Helper attribute and value creates a link to the `/Movies/Index` Razor Page. The `asp-area` attribute value is empty, so the area isn't used in the link. See [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0) for more information.

4. Save the changes and test the app by selecting the **RpMovie** link. See the [\_Layout.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie90/Pages/Shared/_Layout.cshtml) file in GitHub if you have any problems.

5. Test the **Home**, **RpMovie**, **Create**, **Edit**, and **Delete** links. Each page sets the title, which you can see in the browser tab. When you bookmark a page, the title is used for the bookmark.

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize the app. See this [GitHub issue 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420) for instructions on adding decimal comma.

The `Layout` property is set in the `Pages/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The preceding markup sets the layout file to `Pages/Shared/_Layout.cshtml` for all Razor files under the *Pages* folder. See [Layout](../../razor-pages/?view=aspnetcore-10.0#layout) for more information.

### The Create page model

Examine the `Pages/Movies/Create.cshtml.cs` page model:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class CreateModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public CreateModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Movie Movie { get; set; } = default!;

        // To protect from overposting attacks, see https://aka.ms/RazorPagesCRUD
        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Movie.Add(Movie);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}
```

The `OnGet` method initializes any state needed for the page. The Create page doesn't have any state to initialize, so `Page` is returned. Later in the tutorial, an example of `OnGet` initializing state is shown. The `Page` method creates a `PageResult` object that renders the `Create.cshtml` page.

The `Movie` property uses the [\[BindProperty\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute to opt-in to [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0). When the Create form posts the form values, the ASP.NET Core runtime binds the posted values to the `Movie` model.

The `OnPostAsync` method is run when the page posts form data:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be caught on the client-side before the form is posted. An example of a model error is posting a value for the date field that cannot be converted to a date. Client-side validation and model validation are discussed later in the tutorial.

If there are no model errors:

* The data is saved.
* The browser is redirected to the Index page.

### The Create Razor Page

Examine the `Pages/Movies/Create.cshtml` Razor Page file:

```
@page
@model RazorPagesMovie.Pages.Movies.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Movie.Title" class="control-label"></label>
                <input asp-for="Movie.Title" class="form-control" />
                <span asp-validation-for="Movie.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.ReleaseDate" class="control-label"></label>
                <input asp-for="Movie.ReleaseDate" class="form-control" />
                <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Genre" class="control-label"></label>
                <input asp-for="Movie.Genre" class="form-control" />
                <span asp-validation-for="Movie.Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Price" class="control-label"></label>
                <input asp-for="Movie.Price" class="form-control" />
                <span asp-validation-for="Movie.Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

* [Visual Studio](#tabpanel_1_visual-studio)

Visual Studio displays the following tags in a distinctive bold font used for Tag Helpers:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The `<form method="post">` element is a [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). The Form Tag Helper automatically includes an [antiforgery token](../../security/anti-request-forgery?view=aspnetcore-10.0).

The scaffolding engine creates Razor markup for each field in the model, except the ID, similar to the following:

```
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
    <label asp-for="Movie.Title" class="control-label"></label>
    <input asp-for="Movie.Title" class="form-control" />
    <span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
```

The [Validation Tag Helpers](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) (`<div asp-validation-summary` and `<span asp-validation-for`) display validation errors. Validation is covered in more detail later in this series.

The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-label-tag-helper) (`<label asp-for="Movie.Title" class="control-label"></label>`) generates the label caption and `[for]` attribute for the `Title` property.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) (`<input asp-for="Movie.Title" class="form-control">`) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side.

For more information on Tag Helpers such as `<form method="post">`, see [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

## Next steps

[Previous: Add a model](model?view=aspnetcore-10.0) [Next: Work with a database](sql?view=aspnetcore-10.0)

## The Create, Delete, Details, and Edit pages

Examine the `Pages/Movies/Index.cshtml.cs` Page Model:

```
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies;

public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    public IList<Movie> Movie { get;set; }  = default!;

    public async Task OnGetAsync()
    {
        if (_context.Movie != null)
        {
            Movie = await _context.Movie.ToListAsync();
        }
    }
}
```

Razor Pages are derived from [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel). By convention, the `PageModel` derived class is named `PageNameModel`. For example, the Index page is named `IndexModel`.

The constructor uses [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to add the `RazorPagesMovieContext` to the page:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }
```

See [Asynchronous code](../../data/ef-rp/intro?view=aspnetcore-10.0#asynchronous-code) for more information on asynchronous programming with Entity Framework.

When a `GET` request is made for the page, the `OnGetAsync` method returns a list of movies to the Razor Page. On a Razor Page, `OnGetAsync` or `OnGet` is called to initialize the state of the page. In this case, `OnGetAsync` gets a list of movies and displays them.

When `OnGet` returns `void` or `OnGetAsync` returns `Task`, no return statement is used. For example, examine the Privacy Page:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesMovie.Pages
{
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }
}
```

When the return type is [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or `Task<IActionResult>`, a return statement must be provided. For example, the `Pages/Movies/Create.cshtml.cs OnPostAsync` method:

```
public async Task<IActionResult> OnPostAsync()
{
  if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

[]()Examine the `Pages/Movies/Index.cshtml` Razor Page:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Razor can transition from HTML into C# or into Razor-specific markup. When an `@` symbol is followed by a [Razor reserved keyword](../../mvc/views/razor?view=aspnetcore-10.0#razor-reserved-keywords), it transitions into Razor-specific markup, otherwise it transitions into C#.

### The @page directive

The `@page` Razor directive makes the file an MVC action, which means that it can handle requests. `@page` must be the first Razor directive on a page. `@page` and `@model` are examples of transitioning into Razor-specific markup. See [Razor syntax](../../mvc/views/razor?view=aspnetcore-10.0#razor-syntax) for more information.

[]()

### The @model directive

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
```

The `@model` directive specifies the type of the model passed to the Razor Page. In the preceding example, the `@model` line makes the `PageModel` derived class available to the Razor Page. The model is used in the `@Html.DisplayNameFor` and `@Html.DisplayFor` [HTML Helpers](/en-us/aspnet/mvc/overview/older-versions-1/views/creating-custom-html-helpers-cs#understanding-html-helpers) on the page.

Examine the lambda expression used in the following HTML Helper:

```
@Html.DisplayNameFor(model => model.Movie[0].Title)
```

The [DisplayNameFor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper-1.displaynamefor) HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. The lambda expression is inspected rather than evaluated. That means there is no access violation when `model`, `model.Movie`, or `model.Movie[0]` is `null` or empty. When the lambda expression is evaluated, for example, with `@Html.DisplayFor(modelItem => item.Title)`, the model's property values are evaluated.

### The layout page

Select the menu links **RazorPagesMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Pages/Shared/_Layout.cshtml` file.

Open and examine the `Pages/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow the HTML container layout to be:

* Specified in one place.
* Applied in multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the page-specific views show up, *wrapped* in the layout page. For example, select the **Privacy** link and the `Pages/Privacy.cshtml` view is rendered inside the `RenderBody` method.

[]()

### ViewData and layout

Consider the following markup from the `Pages/Movies/Index.cshtml` file:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}
```

The preceding highlighted markup is an example of Razor transitioning into C#. The `{` and `}` characters enclose a block of C# code.

The `PageModel` base class contains a `ViewData` dictionary property that can be used to pass data to a View. Objects are added to the `ViewData` dictionary using a ***key value*** pattern. In the preceding sample, the `Title` property is added to the `ViewData` dictionary.

The `Title` property is used in the `Pages/Shared/_Layout.cshtml` file. The following markup shows the first few lines of the `_Layout.cshtml` file.

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - RazorPagesMovie</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/RazorPagesMovie.styles.css" asp-append-version="true" />
```

### Update the layout

1. Change the `<title>` element in the `Pages/Shared/_Layout.cshtml` file to display **Movie** rather than **RazorPagesMovie**.

   ```
   <!DOCTYPE html>
   <html lang="en">
   <head>
       <meta charset="utf-8" />
       <meta name="viewport" content="width=device-width, initial-scale=1.0" />
       <title>@ViewData["Title"] - Movie</title>
   ```

2. Find the following anchor element in the `Pages/Shared/_Layout.cshtml` file.

   ```
   <a class="navbar-brand" asp-area="" asp-page="/Index">RazorPagesMovie</a>
   ```

3. Replace the preceding element with the following markup:

   ```
   <a class="navbar-brand" asp-page="/Movies/Index">RpMovie</a>
   ```

   The preceding anchor element is a [Tag Helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). In this case, it's the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0). The `asp-page="/Movies/Index"` Tag Helper attribute and value creates a link to the `/Movies/Index` Razor Page. The `asp-area` attribute value is empty, so the area isn't used in the link. See [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0) for more information.

4. Save the changes and test the app by selecting the **RpMovie** link. See the [\_Layout.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie80/Pages/Shared/_Layout.cshtml) file in GitHub if you have any problems.

5. Test the **Home**, **RpMovie**, **Create**, **Edit**, and **Delete** links. Each page sets the title, which you can see in the browser tab. When you bookmark a page, the title is used for the bookmark.

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize the app. See this [GitHub issue 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420) for instructions on adding decimal comma.

The `Layout` property is set in the `Pages/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The preceding markup sets the layout file to `Pages/Shared/_Layout.cshtml` for all Razor files under the *Pages* folder. See [Layout](../../razor-pages/?view=aspnetcore-10.0#layout) for more information.

### The Create page model

Examine the `Pages/Movies/Create.cshtml.cs` page model:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class CreateModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public CreateModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Movie Movie { get; set; } = default!;
        

        // To protect from overposting attacks, see https://aka.ms/RazorPagesCRUD
        public async Task<IActionResult> OnPostAsync()
        {
          if (!ModelState.IsValid || _context.Movie == null || Movie == null)
            {
                return Page();
            }

            _context.Movie.Add(Movie);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}
```

The `OnGet` method initializes any state needed for the page. The Create page doesn't have any state to initialize, so `Page` is returned. Later in the tutorial, an example of `OnGet` initializing state is shown. The `Page` method creates a `PageResult` object that renders the `Create.cshtml` page.

The `Movie` property uses the [\[BindProperty\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute to opt-in to [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0). When the Create form posts the form values, the ASP.NET Core runtime binds the posted values to the `Movie` model.

The `OnPostAsync` method is run when the page posts form data:

```
public async Task<IActionResult> OnPostAsync()
{
  if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be caught on the client-side before the form is posted. An example of a model error is posting a value for the date field that cannot be converted to a date. Client-side validation and model validation are discussed later in the tutorial.

If there are no model errors:

* The data is saved.
* The browser is redirected to the Index page.

### The Create Razor Page

Examine the `Pages/Movies/Create.cshtml` Razor Page file:

```
@page
@model RazorPagesMovie.Pages.Movies.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Movie.Title" class="control-label"></label>
                <input asp-for="Movie.Title" class="form-control" />
                <span asp-validation-for="Movie.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.ReleaseDate" class="control-label"></label>
                <input asp-for="Movie.ReleaseDate" class="form-control" />
                <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Genre" class="control-label"></label>
                <input asp-for="Movie.Genre" class="form-control" />
                <span asp-validation-for="Movie.Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Price" class="control-label"></label>
                <input asp-for="Movie.Price" class="form-control" />
                <span asp-validation-for="Movie.Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Visual Studio displays the following tags in a distinctive bold font used for Tag Helpers:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The following Tag Helpers are shown in the preceding markup:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The `<form method="post">` element is a [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). The Form Tag Helper automatically includes an [antiforgery token](../../security/anti-request-forgery?view=aspnetcore-10.0).

The scaffolding engine creates Razor markup for each field in the model, except the ID, similar to the following:

```
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
    <label asp-for="Movie.Title" class="control-label"></label>
    <input asp-for="Movie.Title" class="form-control" />
    <span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
```

The [Validation Tag Helpers](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) (`<div asp-validation-summary` and `<span asp-validation-for`) display validation errors. Validation is covered in more detail later in this series.

The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-label-tag-helper) (`<label asp-for="Movie.Title" class="control-label"></label>`) generates the label caption and `[for]` attribute for the `Title` property.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) (`<input asp-for="Movie.Title" class="form-control">`) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side.

For more information on Tag Helpers such as `<form method="post">`, see [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

## Next steps

[Previous: Add a model](model?view=aspnetcore-10.0) [Next: Work with a database](sql?view=aspnetcore-10.0)

## The Create, Delete, Details, and Edit pages

Examine the `Pages/Movies/Index.cshtml.cs` Page Model:

```
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies;

public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    public IList<Movie> Movie { get;set; }  = default!;

    public async Task OnGetAsync()
    {
        if (_context.Movie != null)
        {
            Movie = await _context.Movie.ToListAsync();
        }
    }
}
```

Razor Pages are derived from [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel). By convention, the `PageModel` derived class is named `PageNameModel`. For example, the Index page is named `IndexModel`.

The constructor uses [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to add the `RazorPagesMovieContext` to the page:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }
```

See [Asynchronous code](../../data/ef-rp/intro?view=aspnetcore-10.0#asynchronous-code) for more information on asynchronous programming with Entity Framework.

When a `GET` request is made for the page, the `OnGetAsync` method returns a list of movies to the Razor Page. On a Razor Page, `OnGetAsync` or `OnGet` is called to initialize the state of the page. In this case, `OnGetAsync` gets a list of movies and displays them.

When `OnGet` returns `void` or `OnGetAsync` returns `Task`, no return statement is used. For example, examine the Privacy Page:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesMovie.Pages
{
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }
}
```

When the return type is [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or `Task<IActionResult>`, a return statement must be provided. For example, the `Pages/Movies/Create.cshtml.cs OnPostAsync` method:

```
public async Task<IActionResult> OnPostAsync()
{
  if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

[]()Examine the `Pages/Movies/Index.cshtml` Razor Page:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Razor can transition from HTML into C# or into Razor-specific markup. When an `@` symbol is followed by a [Razor reserved keyword](../../mvc/views/razor?view=aspnetcore-10.0#razor-reserved-keywords), it transitions into Razor-specific markup, otherwise it transitions into C#.

### The @page directive

The `@page` Razor directive makes the file an MVC action, which means that it can handle requests. `@page` must be the first Razor directive on a page. `@page` and `@model` are examples of transitioning into Razor-specific markup. See [Razor syntax](../../mvc/views/razor?view=aspnetcore-10.0#razor-syntax) for more information.

[]()

### The @model directive

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
```

The `@model` directive specifies the type of the model passed to the Razor Page. In the preceding example, the `@model` line makes the `PageModel` derived class available to the Razor Page. The model is used in the `@Html.DisplayNameFor` and `@Html.DisplayFor` [HTML Helpers](/en-us/aspnet/mvc/overview/older-versions-1/views/creating-custom-html-helpers-cs#understanding-html-helpers) on the page.

Examine the lambda expression used in the following HTML Helper:

```
@Html.DisplayNameFor(model => model.Movie[0].Title)
```

The [DisplayNameFor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper-1.displaynamefor) HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. The lambda expression is inspected rather than evaluated. That means there is no access violation when `model`, `model.Movie`, or `model.Movie[0]` is `null` or empty. When the lambda expression is evaluated, for example, with `@Html.DisplayFor(modelItem => item.Title)`, the model's property values are evaluated.

### The layout page

Select the menu links **RazorPagesMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Pages/Shared/_Layout.cshtml` file.

Open and examine the `Pages/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow the HTML container layout to be:

* Specified in one place.
* Applied in multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the page-specific views show up, *wrapped* in the layout page. For example, select the **Privacy** link and the `Pages/Privacy.cshtml` view is rendered inside the `RenderBody` method.

[]()

### ViewData and layout

Consider the following markup from the `Pages/Movies/Index.cshtml` file:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}
```

The preceding highlighted markup is an example of Razor transitioning into C#. The `{` and `}` characters enclose a block of C# code.

The `PageModel` base class contains a `ViewData` dictionary property that can be used to pass data to a View. Objects are added to the `ViewData` dictionary using a ***key value*** pattern. In the preceding sample, the `Title` property is added to the `ViewData` dictionary.

The `Title` property is used in the `Pages/Shared/_Layout.cshtml` file. The following markup shows the first few lines of the `_Layout.cshtml` file.

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - RazorPagesMovie</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/RazorPagesMovie.styles.css" asp-append-version="true" />
```

The line `@*Markup removed for brevity.*@` is a Razor comment. Unlike HTML comments `<!-- -->`, Razor comments are not sent to the client. See [MDN web docs: Getting started with HTML](https://developer.mozilla.org/docs/Learn/HTML/Introduction_to_HTML/Getting_started#HTML_comments) for more information.

### Update the layout

1. Change the `<title>` element in the `Pages/Shared/_Layout.cshtml` file to display **Movie** rather than **RazorPagesMovie**.

   ```
   <!DOCTYPE html>
   <html lang="en">
   <head>
       <meta charset="utf-8" />
       <meta name="viewport" content="width=device-width, initial-scale=1.0" />
       <title>@ViewData["Title"] - Movie</title>
   ```

2. Find the following anchor element in the `Pages/Shared/_Layout.cshtml` file.

   ```
   <a class="navbar-brand" asp-area="" asp-page="/Index">RazorPagesMovie</a>
   ```

3. Replace the preceding element with the following markup:

   ```
   <a class="navbar-brand" asp-page="/Movies/Index">RpMovie</a>
   ```

   The preceding anchor element is a [Tag Helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). In this case, it's the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0). The `asp-page="/Movies/Index"` Tag Helper attribute and value creates a link to the `/Movies/Index` Razor Page. The `asp-area` attribute value is empty, so the area isn't used in the link. See [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0) for more information.

4. Save the changes and test the app by selecting the **RpMovie** link. See the [\_Layout.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie70/Pages/Shared/_Layout.cshtml) file in GitHub if you have any problems.

5. Test the **Home**, **RpMovie**, **Create**, **Edit**, and **Delete** links. Each page sets the title, which you can see in the browser tab. When you bookmark a page, the title is used for the bookmark.

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize the app. See this [GitHub issue 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420) for instructions on adding decimal comma.

The `Layout` property is set in the `Pages/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The preceding markup sets the layout file to `Pages/Shared/_Layout.cshtml` for all Razor files under the *Pages* folder. See [Layout](../../razor-pages/?view=aspnetcore-10.0#layout) for more information.

### The Create page model

Examine the `Pages/Movies/Create.cshtml.cs` page model:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class CreateModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public CreateModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Movie Movie { get; set; } = default!;
        

        // To protect from overposting attacks, see https://aka.ms/RazorPagesCRUD
        public async Task<IActionResult> OnPostAsync()
        {
          if (!ModelState.IsValid || _context.Movie == null || Movie == null)
            {
                return Page();
            }

            _context.Movie.Add(Movie);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}
```

The `OnGet` method initializes any state needed for the page. The Create page doesn't have any state to initialize, so `Page` is returned. Later in the tutorial, an example of `OnGet` initializing state is shown. The `Page` method creates a `PageResult` object that renders the `Create.cshtml` page.

The `Movie` property uses the [\[BindProperty\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute to opt-in to [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0). When the Create form posts the form values, the ASP.NET Core runtime binds the posted values to the `Movie` model.

The `OnPostAsync` method is run when the page posts form data:

```
public async Task<IActionResult> OnPostAsync()
{
  if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be caught on the client-side before the form is posted. An example of a model error is posting a value for the date field that cannot be converted to a date. Client-side validation and model validation are discussed later in the tutorial.

If there are no model errors:

* The data is saved.
* The browser is redirected to the Index page.

### The Create Razor Page

Examine the `Pages/Movies/Create.cshtml` Razor Page file:

```
@page
@model RazorPagesMovie.Pages.Movies.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Movie.Title" class="control-label"></label>
                <input asp-for="Movie.Title" class="form-control" />
                <span asp-validation-for="Movie.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.ReleaseDate" class="control-label"></label>
                <input asp-for="Movie.ReleaseDate" class="form-control" />
                <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Genre" class="control-label"></label>
                <input asp-for="Movie.Genre" class="form-control" />
                <span asp-validation-for="Movie.Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Price" class="control-label"></label>
                <input asp-for="Movie.Price" class="form-control" />
                <span asp-validation-for="Movie.Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

Visual Studio displays the following tags in a distinctive bold font used for Tag Helpers:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The following Tag Helpers are shown in the preceding markup:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The `<form method="post">` element is a [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). The Form Tag Helper automatically includes an [antiforgery token](../../security/anti-request-forgery?view=aspnetcore-10.0).

The scaffolding engine creates Razor markup for each field in the model, except the ID, similar to the following:

```
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
    <label asp-for="Movie.Title" class="control-label"></label>
    <input asp-for="Movie.Title" class="form-control" />
    <span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
```

The [Validation Tag Helpers](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) (`<div asp-validation-summary` and `<span asp-validation-for`) display validation errors. Validation is covered in more detail later in this series.

The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-label-tag-helper) (`<label asp-for="Movie.Title" class="control-label"></label>`) generates the label caption and `[for]` attribute for the `Title` property.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) (`<input asp-for="Movie.Title" class="form-control">`) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side.

For more information on Tag Helpers such as `<form method="post">`, see [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

## Next steps

[Previous: Add a model](model?view=aspnetcore-10.0) [Next: Work with a database](sql?view=aspnetcore-10.0)

## The Create, Delete, Details, and Edit pages

Examine the `Pages/Movies/Index.cshtml.cs` Page Model:

```
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class IndexModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IList<Movie> Movie { get;set; } = default!;

        public async Task OnGetAsync()
        {
            if (_context.Movie != null)
            {
                Movie = await _context.Movie.ToListAsync();
            }
        }
    }
}
```

Razor Pages are derived from [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel). By convention, the `PageModel` derived class is named `PageNameModel`. For example, the Index page is named `IndexModel`.

The constructor uses [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to add the `RazorPagesMovieContext` to the page:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }
```

See [Asynchronous code](../../data/ef-rp/intro?view=aspnetcore-10.0#asynchronous-code) for more information on asynchronous programming with Entity Framework.

When a request is made for the page, the `OnGetAsync` method returns a list of movies to the Razor Page. On a Razor Page, `OnGetAsync` or `OnGet` is called to initialize the state of the page. In this case, `OnGetAsync` gets a list of movies and displays them.

When `OnGet` returns `void` or `OnGetAsync` returns `Task`, no return statement is used. For example, examine the Privacy Page:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesMovie.Pages
{
    public class PrivacyModel : PageModel
    {
        private readonly ILogger<PrivacyModel> _logger;

        public PrivacyModel(ILogger<PrivacyModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
        }
    }
}
```

When the return type is [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) or `Task<IActionResult>`, a return statement must be provided. For example, the `Pages/Movies/Create.cshtml.cs` `OnPostAsync` method:

```
public async Task<IActionResult> OnPostAsync()
{
  if (!ModelState.IsValid || _context.Movie == null || Movie == null)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

[]()Examine the `Pages/Movies/Index.cshtml` Razor Page:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Razor can transition from HTML into C# or into Razor-specific markup. When an `@` symbol is followed by a [Razor reserved keyword](../../mvc/views/razor?view=aspnetcore-10.0#razor-reserved-keywords), it transitions into Razor-specific markup, otherwise it transitions into C#.

### The @page directive

The `@page` Razor directive makes the file an MVC action, which means that it can handle requests. `@page` must be the first Razor directive on a page. `@page` and `@model` are examples of transitioning into Razor-specific markup. See [Razor syntax](../../mvc/views/razor?view=aspnetcore-10.0#razor-syntax) for more information.

[]()

### The @model directive

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
```

The `@model` directive specifies the type of the model passed to the Razor Page. In the preceding example, the `@model` line makes the `PageModel` derived class available to the Razor Page. The model is used in the `@Html.DisplayNameFor` and `@Html.DisplayFor` [HTML Helpers](/en-us/aspnet/mvc/overview/older-versions-1/views/creating-custom-html-helpers-cs#understanding-html-helpers) on the page.

Examine the lambda expression used in the following HTML Helper:

```
@Html.DisplayNameFor(model => model.Movie[0].Title)
```

The [DisplayNameFor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper-1.displaynamefor) HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. The lambda expression is inspected rather than evaluated. That means there is no access violation when `model`, `model.Movie`, or `model.Movie[0]` is `null` or empty. When the lambda expression is evaluated, for example, with `@Html.DisplayFor(modelItem => item.Title)`, the model's property values are evaluated.

### The layout page

Select the menu links **RazorPagesMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Pages/Shared/_Layout.cshtml` file.

Open and examine the `Pages/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow the HTML container layout to be:

* Specified in one place.
* Applied in multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the page-specific views show up, *wrapped* in the layout page. For example, select the **Privacy** link and the `Pages/Privacy.cshtml` view is rendered inside the `RenderBody` method.

[]()

### ViewData and layout

Consider the following markup from the `Pages/Movies/Index.cshtml` file:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}
```

The preceding highlighted markup is an example of Razor transitioning into C#. The `{` and `}` characters enclose a block of C# code.

The `PageModel` base class contains a `ViewData` dictionary property that can be used to pass data to a View. Objects are added to the `ViewData` dictionary using a ***key value*** pattern. In the preceding sample, the `Title` property is added to the `ViewData` dictionary.

The `Title` property is used in the `Pages/Shared/_Layout.cshtml` file. The following markup shows the first few lines of the `_Layout.cshtml` file.

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - RazorPagesMovie</title>

     @*Markup removed for brevity.*@
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
```

The line `@*Markup removed for brevity.*@` is a Razor comment. Unlike HTML comments `<!-- -->`, Razor comments are not sent to the client. See [MDN web docs: Getting started with HTML](https://developer.mozilla.org/docs/Learn/HTML/Introduction_to_HTML/Getting_started#HTML_comments) for more information.

### Update the layout

1. Change the `<title>` element in the `Pages/Shared/_Layout.cshtml` file to display **Movie** rather than **RazorPagesMovie**.

   ```
   <!DOCTYPE html>
   <html lang="en">
   <head>
       <meta charset="utf-8" />
       <meta name="viewport" content="width=device-width, initial-scale=1.0" />
       <title>@ViewData["Title"] - Movie</title>
   ```

2. Find the following anchor element in the `Pages/Shared/_Layout.cshtml` file.

   ```
   <a class="navbar-brand" asp-area="" asp-page="/Index">RazorPagesMovie</a>
   ```

3. Replace the preceding element with the following markup:

   ```
   <a class="navbar-brand" asp-page="/Movies/Index">RpMovie</a>
   ```

   The preceding anchor element is a [Tag Helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). In this case, it's the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0). The `asp-page="/Movies/Index"` Tag Helper attribute and value creates a link to the `/Movies/Index` Razor Page. The `asp-area` attribute value is empty, so the area isn't used in the link. See [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0) for more information.

4. Save the changes and test the app by selecting the **RpMovie** link. See the [\_Layout.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie60/Pages/Shared/_Layout.cshtml) file in GitHub if you have any problems.

5. Test the **Home**, **RpMovie**, **Create**, **Edit**, and **Delete** links. Each page sets the title, which you can see in the browser tab. When you bookmark a page, the title is used for the bookmark.

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize the app. See this [GitHub issue 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420) for instructions on adding decimal comma.

The `Layout` property is set in the `Pages/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The preceding markup sets the layout file to `Pages/Shared/_Layout.cshtml` for all Razor files under the *Pages* folder. See [Layout](../../razor-pages/?view=aspnetcore-10.0#layout) for more information.

### The Create page model

Examine the `Pages/Movies/Create.cshtml.cs` page model:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages.Movies
{
    public class CreateModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public CreateModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Movie Movie { get; set; } = default!;
        

        // To protect from overposting attacks, see https://aka.ms/RazorPagesCRUD
        public async Task<IActionResult> OnPostAsync()
        {
          if (!ModelState.IsValid || _context.Movie == null || Movie == null)
            {
                return Page();
            }

            _context.Movie.Add(Movie);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}
```

The `OnGet` method initializes any state needed for the page. The Create page doesn't have any state to initialize, so `Page` is returned. Later in the tutorial, an example of `OnGet` initializing state is shown. The `Page` method creates a `PageResult` object that renders the `Create.cshtml` page.

The `Movie` property uses the [\[BindProperty\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute to opt-in to [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0). When the Create form posts the form values, the ASP.NET Core runtime binds the posted values to the `Movie` model.

The `OnPostAsync` method is run when the page posts form data:

```
public async Task<IActionResult> OnPostAsync()
{
  if (!ModelState.IsValid || _context.Movie == null || Movie == null)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be caught on the client-side before the form is posted. An example of a model error is posting a value for the date field that cannot be converted to a date. Client-side validation and model validation are discussed later in the tutorial.

If there are no model errors:

* The data is saved.
* The browser is redirected to the Index page.

### The Create Razor Page

Examine the `Pages/Movies/Create.cshtml` Razor Page file:

```
@page
@model RazorPagesMovie.Pages.Movies.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Movie.Title" class="control-label"></label>
                <input asp-for="Movie.Title" class="form-control" />
                <span asp-validation-for="Movie.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.ReleaseDate" class="control-label"></label>
                <input asp-for="Movie.ReleaseDate" class="form-control" />
                <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Genre" class="control-label"></label>
                <input asp-for="Movie.Genre" class="form-control" />
                <span asp-validation-for="Movie.Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Price" class="control-label"></label>
                <input asp-for="Movie.Price" class="form-control" />
                <span asp-validation-for="Movie.Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

Visual Studio displays the following tags in a distinctive bold font used for Tag Helpers:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The following Tag Helpers are shown in the preceding markup:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The `<form method="post">` element is a [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). The Form Tag Helper automatically includes an [antiforgery token](../../security/anti-request-forgery?view=aspnetcore-10.0).

The scaffolding engine creates Razor markup for each field in the model, except the ID, similar to the following:

```
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
    <label asp-for="Movie.Title" class="control-label"></label>
    <input asp-for="Movie.Title" class="form-control" />
    <span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
```

The [Validation Tag Helpers](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) (`<div asp-validation-summary` and `<span asp-validation-for`) display validation errors. Validation is covered in more detail later in this series.

The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-label-tag-helper) (`<label asp-for="Movie.Title" class="control-label"></label>`) generates the label caption and `[for]` attribute for the `Title` property.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) (`<input asp-for="Movie.Title" class="form-control">`) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side.

For more information on Tag Helpers such as `<form method="post">`, see [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

## Next steps

[Previous: Add a model](model?view=aspnetcore-10.0) [Next: Work with a database](sql?view=aspnetcore-10.0)

## The Create, Delete, Details, and Edit pages

Examine the `Pages/Movies/Index.cshtml.cs` Page Model:

```
// Unused usings removed.
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{
    public class IndexModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }
        public IList<Movie> Movie { get;set; }

        public async Task OnGetAsync()
        {
            Movie = await _context.Movie.ToListAsync();
        }
    }
}
```

Razor Pages are derived from `PageModel`. By convention, the `PageModel`-derived class is named `<PageName>Model`. The constructor uses [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to add the `RazorPagesMovieContext` to the page:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }
```

See [Asynchronous code](../../data/ef-rp/intro?view=aspnetcore-10.0#asynchronous-code) for more information on asynchronous programming with Entity Framework.

When a request is made for the page, the `OnGetAsync` method returns a list of movies to the Razor Page. On a Razor Page, `OnGetAsync` or `OnGet` is called to initialize the state of the page. In this case, `OnGetAsync` gets a list of movies and displays them.

When `OnGet` returns `void` or `OnGetAsync` returns `Task`, no return statement is used. For example the Privacy Page:

```
public class PrivacyModel : PageModel
{
    private readonly ILogger<PrivacyModel> _logger;

    public PrivacyModel(ILogger<PrivacyModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
    }
}
```

When the return type is `IActionResult` or `Task<IActionResult>`, a return statement must be provided. For example, the `Pages/Movies/Create.cshtml.cs` `OnPostAsync` method:

```
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Movie.Add(Movie);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

[]()Examine the `Pages/Movies/Index.cshtml` Razor Page:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movie[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

Razor can transition from HTML into C# or into Razor-specific markup. When an `@` symbol is followed by a [Razor reserved keyword](../../mvc/views/razor?view=aspnetcore-10.0#razor-reserved-keywords), it transitions into Razor-specific markup, otherwise it transitions into C#.

### The @page directive

The `@page` Razor directive makes the file an MVC action, which means that it can handle requests. `@page` must be the first Razor directive on a page. `@page` and `@model` are examples of transitioning into Razor-specific markup. See [Razor syntax](../../mvc/views/razor?view=aspnetcore-10.0#razor-syntax) for more information.

[]()

### The @model directive

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
```

The `@model` directive specifies the type of the model passed to the Razor Page. In the preceding example, the `@model` line makes the `PageModel`-derived class available to the Razor Page. The model is used in the `@Html.DisplayNameFor` and `@Html.DisplayFor` [HTML Helpers](/en-us/aspnet/mvc/overview/older-versions-1/views/creating-custom-html-helpers-cs#understanding-html-helpers) on the page.

Examine the lambda expression used in the following HTML Helper:

```
@Html.DisplayNameFor(model => model.Movie[0].Title)
```

The [DisplayNameFor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper-1.displaynamefor) HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. The lambda expression is inspected rather than evaluated. That means there is no access violation when `model`, `model.Movie`, or `model.Movie[0]` is `null` or empty. When the lambda expression is evaluated, for example, with `@Html.DisplayFor(modelItem => item.Title)`, the model's property values are evaluated.

### The layout page

Select the menu links **RazorPagesMovie**, **Home**, and **Privacy**. Each page shows the same menu layout. The menu layout is implemented in the `Pages/Shared/_Layout.cshtml` file.

Open and examine the `Pages/Shared/_Layout.cshtml` file.

[Layout](../../mvc/views/layout?view=aspnetcore-10.0) templates allow the HTML container layout to be:

* Specified in one place.
* Applied in multiple pages in the site.

Find the `@RenderBody()` line. `RenderBody` is a placeholder where all the page-specific views show up, *wrapped* in the layout page. For example, select the **Privacy** link and the `Pages/Privacy.cshtml` view is rendered inside the `RenderBody` method.

[]()

### ViewData and layout

Consider the following markup from the `Pages/Movies/Index.cshtml` file:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}
```

The preceding highlighted markup is an example of Razor transitioning into C#. The `{` and `}` characters enclose a block of C# code.

The `PageModel` base class contains a `ViewData` dictionary property that can be used to pass data to a View. Objects are added to the `ViewData` dictionary using a ***key value*** pattern. In the preceding sample, the `Title` property is added to the `ViewData` dictionary.

The `Title` property is used in the `Pages/Shared/_Layout.cshtml` file. The following markup shows the first few lines of the `_Layout.cshtml` file.

```
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - RazorPagesMovie</title>

    @*Markup removed for brevity.*@
```

The line `@*Markup removed for brevity.*@` is a Razor comment. Unlike HTML comments `<!-- -->`, Razor comments are not sent to the client. See [MDN web docs: Getting started with HTML](https://developer.mozilla.org/docs/Learn/HTML/Introduction_to_HTML/Getting_started#HTML_comments) for more information.

### Update the layout

1. Change the `<title>` element in the `Pages/Shared/_Layout.cshtml` file to display **Movie** rather than **RazorPagesMovie**.

   ```
   <!DOCTYPE html>
   <html lang="en">
   <head>
       <meta charset="utf-8" />
       <meta name="viewport" content="width=device-width, initial-scale=1.0" />
       <title>@ViewData["Title"] - Movie</title>
   ```

2. Find the following anchor element in the `Pages/Shared/_Layout.cshtml` file.

   ```
   <a class="navbar-brand" asp-area="" asp-page="/Index">RazorPagesMovie</a>
   ```

3. Replace the preceding element with the following markup:

   ```
   <a class="navbar-brand" asp-page="/Movies/Index">RpMovie</a>
   ```

   The preceding anchor element is a [Tag Helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). In this case, it's the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0). The `asp-page="/Movies/Index"` Tag Helper attribute and value creates a link to the `/Movies/Index` Razor Page. The `asp-area` attribute value is empty, so the area isn't used in the link. See [Areas](../../mvc/controllers/areas?view=aspnetcore-10.0) for more information.

4. Save the changes and test the app by selecting the **RpMovie** link. See the [\_Layout.cshtml](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie30/Pages/Shared/_Layout.cshtml) file in GitHub if you have any problems.

5. Test the **Home**, **RpMovie**, **Create**, **Edit**, and **Delete** links. Each page sets the title, which you can see in the browser tab. When you bookmark a page, the title is used for the bookmark.

Note

You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize the app. See this [GitHub issue 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420) for instructions on adding decimal comma.

The `Layout` property is set in the `Pages/_ViewStart.cshtml` file:

```
@{
    Layout = "_Layout";
}
```

The preceding markup sets the layout file to `Pages/Shared/_Layout.cshtml` for all Razor files under the *Pages* folder. See [Layout](../../razor-pages/?view=aspnetcore-10.0#layout) for more information.

### The Create page model

Examine the `Pages/Movies/Create.cshtml.cs` page model:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
using System;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{
    public class CreateModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public CreateModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Movie Movie { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Movie.Add(Movie);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}
```

The `OnGet` method initializes any state needed for the page. The Create page doesn't have any state to initialize, so `Page` is returned. Later in the tutorial, an example of `OnGet` initializing state is shown. The `Page` method creates a `PageResult` object that renders the `Create.cshtml` page.

The `Movie` property uses the [\[BindProperty\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute to opt-in to [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0). When the Create form posts the form values, the ASP.NET Core runtime binds the posted values to the `Movie` model.

The `OnPostAsync` method is run when the page posts form data:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movie.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

If there are any model errors, the form is redisplayed, along with any form data posted. Most model errors can be caught on the client-side before the form is posted. An example of a model error is posting a value for the date field that cannot be converted to a date. Client-side validation and model validation are discussed later in the tutorial.

If there are no model errors:

* The data is saved.
* The browser is redirected to the Index page.

### The Create Razor Page

Examine the `Pages/Movies/Create.cshtml` Razor Page file:

```
@page
@model RazorPagesMovie.Pages.Movies.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Movie.Title" class="control-label"></label>
                <input asp-for="Movie.Title" class="form-control" />
                <span asp-validation-for="Movie.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.ReleaseDate" class="control-label"></label>
                <input asp-for="Movie.ReleaseDate" class="form-control" />
                <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Genre" class="control-label"></label>
                <input asp-for="Movie.Genre" class="form-control" />
                <span asp-validation-for="Movie.Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Price" class="control-label"></label>
                <input asp-for="Movie.Price" class="form-control" />
                <span asp-validation-for="Movie.Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

Visual Studio displays the following tags in a distinctive bold font used for Tag Helpers:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The following Tag Helpers are shown in the preceding markup:

* `<form method="post">`
* `<div asp-validation-summary="ModelOnly" class="text-danger"></div>`
* `<label asp-for="Movie.Title" class="control-label"></label>`
* `<input asp-for="Movie.Title" class="form-control" />`
* `<span asp-validation-for="Movie.Title" class="text-danger"></span>`

The `<form method="post">` element is a [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). The Form Tag Helper automatically includes an [antiforgery token](../../security/anti-request-forgery?view=aspnetcore-10.0).

The scaffolding engine creates Razor markup for each field in the model, except the ID, similar to the following:

```
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
    <label asp-for="Movie.Title" class="control-label"></label>
    <input asp-for="Movie.Title" class="form-control" />
    <span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
```

The [Validation Tag Helpers](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) (`<div asp-validation-summary` and `<span asp-validation-for`) display validation errors. Validation is covered in more detail later in this series.

The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-label-tag-helper) (`<label asp-for="Movie.Title" class="control-label"></label>`) generates the label caption and `[for]` attribute for the `Title` property.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) (`<input asp-for="Movie.Title" class="form-control">`) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client-side.

For more information on Tag Helpers such as `<form method="post">`, see [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

## Next steps

[Previous: Add a model](model?view=aspnetcore-10.0) [Next: Work with a database](sql?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/blazor-web-app-with-windows-authentication
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/blazor-web-app-with-windows-authentication.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure an ASP.NET Core Blazor Web App with Windows Authentication

This article describes how to secure a Blazor Web App with [Windows Authentication](/en-us/windows-server/security/windows-authentication/windows-authentication-overview) using a sample app. For more information, see [Configure Windows Authentication in ASP.NET Core](../../security/authentication/windowsauth?view=aspnetcore-10.0).

The app specification for the Blazor Web App:

* Adopts the [Interactive Server render mode with global interactivity](../components/render-modes?view=aspnetcore-10.0).
* Establishes an [authorization policy](../../security/authorization/policies?view=aspnetcore-10.0) for a [Windows security identifier](/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers) to access a secure page.

## Sample app

Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the `BlazorWebAppWinAuthServer` folder for .NET 9 or later.

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

## Configuration

The sample app doesn't require configuration to run locally.

When deployed to a host, such as IIS, the app must adopt impersonation to run under the user's account. For more information, see [Configure Windows Authentication in ASP.NET Core](../../security/authentication/windowsauth?view=aspnetcore-10.0#impersonation).

## Sample app code

Inspect the `Program` file in the sample app for the following API calls.

[AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) is called using the [NegotiateDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.negotiate.negotiatedefaults.authenticationscheme) authentication scheme. [AddNegotiate](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.negotiateextensions.addnegotiate) configures the [AuthenticationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder) to use Negotiate (also known as Windows, Kerberos, or NTLM) authentication, and the authentication handler supports Kerberos on Windows and Linux servers:

```
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
    .AddNegotiate();
```

[AddAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.policyservicecollectionextensions.addauthorization) adds authorization policy services. The following code assigns the [AuthorizationOptions.DefaultPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.defaultpolicy), which requires an authenticated user, to the [AuthorizationOptions.FallbackPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.fallbackpolicy). This configuration requires authentication when no policy is produced from authorization metadata:

```
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
```

For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

[AddCascadingAuthenticationState](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cascadingauthenticationstateservicecollectionextensions.addcascadingauthenticationstate) adds cascading authentication state to the service collection. This is equivalent to placing a `CascadingAuthenticationState` component at the root of the app's component hierarchy:

```
builder.Services.AddCascadingAuthenticationState();
```

An [authorization policy](../../security/authorization/policies?view=aspnetcore-10.0) is added for a [Windows security identifier (SID)](/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers). The `S-1-5-113` well-known SID in the following example indicates that the user is a local account, which restricts network sign-in to local accounts instead of "administrator" or equivalent accounts:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("LocalAccount", policy =>
        policy.RequireClaim(
            "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid",
            "S-1-5-113"));   
```

The authorization policy is enforced by the `LocalAccount` component.

`Components/Pages/LocalAccount.razor`:

```
@page "/local-account"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize("LocalAccount")]

<h1>Local Account Only</h1>

<p>
    You can only reach this page by satisfying the
    <code>LocalAccount</code> authorization policy.
</p>
```

The `UserClaims` component lists the user's claims and roles, including the user's Windows security identifiers (SIDs) with SID translations.

`Components/Pages/UserClaims.razor`:

```
@page "/user-claims"
@using System.Security.Claims
@using System.Security.Principal
@using Microsoft.AspNetCore.Components.QuickGrid

<PageTitle>User Claims & Roles</PageTitle>

<h1>User Claims & Roles</h1>

<QuickGrid Items="claims" Pagination="pagination">
    <Paginator State="pagination" />
    <PropertyColumn Property="@(p => p.Type)" Sortable="true" />
    <PropertyColumn Property="@(p => p.Value)" Sortable="true" />
    <PropertyColumn Property="@(p => GetClaimAsHumanReadable(p))" Sortable="true" Title="Translation" />
    <PropertyColumn Property="@(p => p.Issuer)" Sortable="true" />
</QuickGrid>

<h1>User Roles</h1>

@if (roles.Any())
{
    <ul>
        @foreach (var role in roles)
        {
            <li>@role</li>
        }
    </ul>
}
else
{
    <p>No roles available.</p>
}

@code {
    private IQueryable<Claim> claims = Enumerable.Empty<Claim>().AsQueryable();
    private IEnumerable<string> roles = Enumerable.Empty<string>();
    PaginationState pagination = new PaginationState { ItemsPerPage = 10 };

    [CascadingParameter]
    private Task<AuthenticationState>? AuthState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (AuthState == null)
        {
            return;
        }

        var authState = await AuthState;

        claims = authState.User.Claims.AsQueryable();

        roles = authState.User.Claims
            .Where(claim => claim.Type == ClaimTypes.Role)
            .Select(claim => claim.Value);
    }

    private string GetClaimAsHumanReadable(Claim claim)
    {
        if (!OperatingSystem.IsWindows() ||
            claim.Type is not (ClaimTypes.PrimarySid or ClaimTypes.PrimaryGroupSid
                or ClaimTypes.GroupSid))
        {
            // We're either not on Windows or not dealing with a SID Claim that
            // can be translated
            return string.Empty;
        }

        SecurityIdentifier sid = new SecurityIdentifier(claim.Value);

        try
        {
            // Throw an exception if the SID can't be translated
            var account = sid.Translate(typeof(NTAccount));

            return account.ToString();
        }
        catch (IdentityNotMappedException)
        {
            return "Could not be mapped";
        }
    }
}
```

## Related content

* [Configure Windows Authentication in ASP.NET Core](../../security/authentication/windowsauth?view=aspnetcore-10.0)
* [Security identifiers (Windows Server documentation)](/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/aot/native-aot-tutorial
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/aot/native-aot-tutorial.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Publish an ASP.NET Core app using Native AOT

[.NET native ahead-of-time (AOT)](/en-us/dotnet/core/deploying/native-aot/) is available in ASP.NET Core.

Note

Minimal APIs are ***not*** compatible with native AOT.

See [Native AOT deployment](/en-us/dotnet/core/deploying/native-aot/) for more information, including:

* [Limitations of Native AOT deployment](/en-us/dotnet/core/deploying/native-aot/#limitations-of-native-aot-deployment)
* [Supported platforms](/en-us/dotnet/core/deploying/native-aot/#platformarchitecture-restrictions)

## Prerequisites

* [.NET CLI](#tabpanel_1_net-cli)
* [Visual Studio](#tabpanel_1_visual-studio)

- [.NET SDK](https://dotnet.microsoft.com//download)

- On Linux, see [Prerequisites for Native AOT deployment](/en-us/dotnet/core/deploying/native-aot/?tabs=net8plus#prerequisites-for-native-aot-deployment).

- [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) with the **Desktop development with C++** workload installed.

Note

Visual Studio 2022 is required because Native AOT requires [link.exe](/en-us/cpp/build/reference/linker-options) and the Visual C++ static runtime libraries. There are no plans to support Native AOT ***without*** Visual Studio.

[.NET SDK](https://dotnet.microsoft.com//download)

* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) with the following workloads installed:

  * **ASP.NET and web development**
  * **Desktop development with C++**

## Create a web app with Native AOT

Create an ASP.NET Core API app that is configured to work with Native AOT:

* [.NET CLI](#tabpanel_2_net-cli)
* [Visual Studio](#tabpanel_2_visual-studio)

Run the following commands:

```
dotnet new webapiaot -o MyFirstAotWebApi && cd MyFirstAotWebApi
```

Output similar to the following example is displayed:

```
The template "ASP.NET Core Web API (Native AOT)" was created successfully.

Processing post-creation actions...
Restoring C:\Code\Demos\MyFirstAotWebApi\MyFirstAotWebApi.csproj:
  Determining projects to restore...
  Restored C:\Code\Demos\MyFirstAotWebApi\MyFirstAotWebApi.csproj (in 302 ms).
Restore succeeded.
```

1. Create a new `ASP.NET Core Web API (Native AOT)` project.
2. Name the project **MyFirstAotWebApi**.
3. Select **Create**.

## Publish the Native AOT app

Verify the app can be published using Native AOT:

* [.NET CLI](#tabpanel_3_net-cli)
* [Visual Studio](#tabpanel_3_visual-studio)

```
dotnet publish
```

Visual studio doesn't support publishing an AOT app. Use the CLI command:

```
dotnet publish
```

The `dotnet publish` command:

* Compiles the source files.
* Generates source code files that are compiled.
* Passes generated assemblies to a native IL compiler. The IL compiler produces the native executable. The native executable contains the native machine code.

Output similar to the following example is displayed:

```
MSBuild version 17.<version> for .NET
  Determining projects to restore...
  Restored C:\Code\Demos\MyFirstAotWebApi\MyFirstAotWebApi.csproj (in 241 ms).
C:\Code\dotnet\aspnetcore\.dotnet\sdk\8.0.<version>\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIde
ntifierInference.targets(287,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotne
t-support-policy [C:\Code\Demos\MyFirstAotWebApi\MyFirstAotWebApi.csproj]
  MyFirstAotWebApi -> C:\Code\Demos\MyFirstAotWebApi\bin\Release\net8.0\win-x64\MyFirstAotWebApi.dll
  Generating native code
  MyFirstAotWebApi -> C:\Code\Demos\MyFirstAotWebApi\bin\Release\net8.0\win-x64\publish\
```

The output may differ from the preceding example depending on the version of .NET 8 used, directory used, and other factors.

Review the contents of the output directory:

```
dir bin\Release\net8.0\win-x64\publish
```

Output similar to the following example is displayed:

```
    Directory: C:\Code\Demos\MyFirstAotWebApi\bin\Release\net8.0\win-x64\publish

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          30/03/2023  1:41 PM        9480704 MyFirstAotWebApi.exe
-a---          30/03/2023  1:41 PM       43044864 MyFirstAotWebApi.pdb
```

The executable is self-contained and doesn't require a .NET runtime to run. When launched, it behaves the same as the app run in the `Development` environment. Run the AOT app:

```
.\bin\Release\net8.0\win-x64\publish\MyFirstAotWebApi.exe
```

Output similar to the following example is displayed:

```
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Code\Demos\MyFirstAotWebApi
```

## See also

* [ASP.NET Core support for Native AOT](../native-aot?view=aspnetcore-10.0)
* [Native AOT deployment](/en-us/dotnet/core/deploying/native-aot/)
* [Using the configuration binder source generator](https://andrewlock.net/exploring-the-dotnet-8-preview-using-the-new-configuration-binder-source-generator/)
* [The Minimal API AOT compilation template](https://andrewlock.net/exploring-the-dotnet-8-preview-the-minimal-api-aot-template/)
* [Comparing `WebApplication.CreateBuilder` to `CreateSlimBuilder`](https://andrewlock.net/exploring-the-dotnet-8-preview-comparing-createbuilder-to-the-new-createslimbuilder-method/)
* [Exploring the new Minimal API source generator](https://andrewlock.net/exploring-the-dotnet-8-preview-exploring-the-new-minimal-api-source-generator/)
* [Replacing method calls with Interceptors](https://andrewlock.net/exploring-the-dotnet-8-preview-changing-method-calls-with-interceptors/)
* [Configuration-binding source generator](/en-us/dotnet/core/whats-new/dotnet-8#configuration-binding-source-generator)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/configure-oidc-web-authentication.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure OpenID Connect Web (UI) authentication in ASP.NET Core

By [Damien Bowden](https://github.com/damienbod)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/configure-oidc-web-authentication/samples/oidc-net8)

This article covers the following areas:

* What is an OpenID Connect confidential interactive client
* Create an OpenID Connect client in ASP.NET Core
* Examples of OpenID Connect client with code snippets
* Using third party OpenID Connect provider clients
* Backend for frontend (BFF) security architecture
* Advanced features, standards, extending the an OpenID Connect client

For an alternative experience using [Microsoft Authentication Library for .NET](/en-us/entra/msal/dotnet/), [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/), and [Microsoft Entra ID](https://www.microsoft.com/security/business/identity-access/microsoft-entra-id), see [Quickstart: Sign in users and call the Microsoft Graph API from an ASP.NET Core web app (Azure documentation)](/en-us/entra/identity-platform/quickstart-web-app-dotnet-core-sign-in).

For an example using Microsoft Entra External ID OIDC server, see [Sign in users for a sample ASP.NET Core web app in an external tenant](/en-us/entra/external-id/customers/sample-web-app-dotnet-sign-in) and [An ASP.NET Core web app authenticating users against Microsoft Entra External ID using Microsoft Identity Web](/en-us/samples/azure-samples/ms-identity-ciam-dotnet-tutorial/ms-identity-ciam-dotnet-tutorial-1-sign-in-aspnet-core-mvc/).

## What is an OpenID Connect confidential interactive client

[OpenID Connect](https://openid.net/developers/how-connect-works/) can be used to implement authentication in ASP.NET Core applications. The recommended way is to use an OpenID Connect confidential client using the code flow. Using the [Proof Key for Code Exchange by OAuth Public Clients (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636) is recommended for this implementation. Both the application client and the user of the application are authenticated in the confidential flow. The application client uses a client secret or a client assertion to authenticate.

Public OpenID Connect/OAuth clients are no longer recommended for web applications.

The default flow works as shown in the following diagram:

OpenID Connect comes in many variations and all server implementations have slightly different parameters and requirements. Some servers don't support the user info endpoint, some still don't support PKCE and others require special parameters in the token request. Client assertions can be used instead of client secrets. New standards also exist which add extra security on top of the OpenID Connect Core, for example FAPI, CIBA or DPoP for downstream APIs.

Note

From .NET 9, [OAuth 2.0 Pushed Authorization Requests (PAR) RFC 9126](https://datatracker.ietf.org/doc/html/rfc9126) is used per default, if the OpenID Connect server supports this. This is a three step flow and not a two step flow as shown above. (User Info request is an optional step.)

## Create an Open ID Connect code flow client using Razor Pages

The following section shows how to implement an OpenID Connect client in an empty ASP.NET Core Razor page project. The same logic can be applied to any ASP.NET Core web project with only the UI integration being different.

### Add OpenID Connect support

Add the [`Microsoft.AspNetCore.Authentication.OpenIdConnect`](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect) Nuget packages to the ASP.NET Core project.

### Setup the OpenID Connect client

Add the authentication to the web application using the builder.Services in the `Program.cs` file. The configuration is dependent on the OpenID Connect server. Each OpenID Connect server requires small differences in the setup.

The OpenID Connect handler is used for challenges and signout. The cookie is used to handle the session in the web application. The default schemes for the authentication can be specified as required.

For more information, see the [ASP.NET Core `authentication-handler` guidance](./?view=aspnetcore-10.0#authentication-handler).

```
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    var oidcConfig = builder.Configuration.GetSection("OpenIDConnectSettings");

    options.Authority = oidcConfig["Authority"];
    options.ClientId = oidcConfig["ClientId"];
    options.ClientSecret = oidcConfig["ClientSecret"];

    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.ResponseType = OpenIdConnectResponseType.Code;

    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;

    options.MapInboundClaims = false;
    options.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
    options.TokenValidationParameters.RoleClaimType = "roles";
});
```

For details on the different OpenID Connect options, see [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](../../blazor/security/blazor-web-app-with-oidc?view=aspnetcore-10.0).

For the different claims mapping possibilities, see [Map, customize, transform claims in ASP.NET Core](claims?view=aspnetcore-10.0).

Note

The following namespaces are required:

```
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
```

### Setup the configuration properties

Add the OpenID Connect client settings to the application configuration properties. The settings must match the client configuration in the OpenID Connect server. No secrets should be persisted in application settings where they might get accidentally checked in. Secrets should be stored in a secure location like Azure Key Vault in production environments or in user secrets in a development environment. For more information, see [Safe storage of app secrets in development](../app-secrets?view=aspnetcore-10.0).

```
"OpenIDConnectSettings": {
  // OpenID Connect URL. (The base URL for the /.well-known/openid-configuration)
  "Authority": "<Authority>",
  // client ID from the OpenID Connect server
  "ClientId": "<Client ID>",
  //"ClientSecret": "--stored-in-user-secrets-or-key-vault--"
},
```

### Signed-out callback path configuration

The [SignedOutCallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutcallbackpath) (configuration key: "`SignedOutCallbackPath`") is the request path within the app's base path intercepted by the OpenID Connect handler where the user agent is first returned after signing out from the identity provider. The sample app doesn't set a value for the path because the default value of "`/signout-callback-oidc`" is used. After intercepting the request, the OpenID Connect handler redirects to the [SignedOutRedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutredirecturi) or [RedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.redirecturi), if specified.

Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost:{PORT}/signout-callback-oidc

Note

When using Microsoft Entra ID, set the path in the **Web** platform configuration's **Redirect URI** entries in the Entra or Azure portal. A port isn't required for `localhost` addresses when using Entra. Most other OIDC providers require the correct port. If you don't add the signed-out callback path URI to the app's registration in Entra, Entra refuses to redirect the user back to the app and merely asks them to close their browser window.

### Update the ASP.NET Core pipeline method in the program class.

The `UseRouting` method must be implemented before the `UseAuthorization` method.

```
app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();
// Authorization is applied for middleware after the UseAuthorization method
app.UseAuthorization();
app.MapRazorPages();
```

### Force authorization

Add the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the protected Razor pages:

```
[Authorize]
```

A better approach is to force authorization for the whole app and opt out for unsecure pages:

```
var requireAuthPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();

builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(requireAuthPolicy);
```

The fallback policy requires authentication for requests processed by the authorization middleware when no authorization policy is produced from endpoint metadata. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

Opt out of authorization at public endpoints by applying the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) to the public endpoints. For examples, see the [Add a new `Logout.cshtml` and `SignedOut.cshtml` Razor pages to the project](#add-a-new-logoutcshtml-and-signedoutcshtml-razor-pages-to-the-project) and [Implement `Login` page](#implement-login-page) sections.

### Add a new `Logout.cshtml` and `SignedOut.cshtml` Razor pages to the project

A logout is required to sign out both the cookie session and the OpenID Connect session. The whole app needs to redirect to the OpenID Connect server to sign out. After a successful sign out, the app opens the `RedirectUri` route.

Implement a default sign-out page and change the `Logout` razor page code to the following:

```
[Authorize]
public class LogoutModel : PageModel
{
    public IActionResult OnGetAsync()
    {
        return SignOut(new AuthenticationProperties
        {
            RedirectUri = "/SignedOut"
        },
        // Clear auth cookie
        CookieAuthenticationDefaults.AuthenticationScheme,
        // Redirect to OIDC provider signout endpoint
        OpenIdConnectDefaults.AuthenticationScheme);
    }
}
```

The `SignedOut.cshtml` requires the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute):

```
[AllowAnonymous]
public class SignedOutModel : PageModel
{
    public void OnGet()
    {
    }
}
```

### Implement `Login` page

A `Login` Razor page can also be implemented to call the `ChallengeAsync` directly with the required `AuthProperties`. This isn't required if the web app requires authentication and the default challenge is used.

The `Login.cshtml` page requires the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute):

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPageOidc.Pages;

[AllowAnonymous]
public class LoginModel : PageModel
{
    [BindProperty(SupportsGet = true)]
    public string? ReturnUrl { get; set; }

    public async Task OnGetAsync()
    {
        var properties = GetAuthProperties(ReturnUrl);
        await HttpContext.ChallengeAsync(properties);
    }

    private static AuthenticationProperties GetAuthProperties(string? returnUrl)
    {
        const string pathBase = "/";

        // Prevent open redirects.
        if (string.IsNullOrEmpty(returnUrl))
        {
            returnUrl = pathBase;
        }
        else if (!Uri.IsWellFormedUriString(returnUrl, UriKind.Relative))
        {
            returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery;
        }
        else if (returnUrl[0] != '/')
        {
            returnUrl = $"{pathBase}{returnUrl}";
        }

        return new AuthenticationProperties { RedirectUri = returnUrl };
    }
}
```

### Add a login and logout button for the user

```
@if (Context.User.Identity!.IsAuthenticated)
{
	<li class="nav-item">
		<a class="nav-link text-dark" asp-area="" asp-page="/Logout">Logout</a>
	</li>

	<span class="nav-link text-dark">Hi @Context.User.Identity.Name</span>
}
else
{
	<li class="nav-item">
		<a class="nav-link text-dark" asp-area="" asp-page="/Index">Login</a>
	</li>
}
```

## Examples with code snippets

### Example Using User Info endpoint

The OpenID Connect options can be used to map claims, implement handlers or even save the tokens in the session for later usage.

The `Scope` option can be used to request different claims or a refresh token which is sent as information to the OpenID Connect server. Requesting the `offline_access` is asking the server to return a reference token which can be used to refresh the session without authenticating the user of the application again.

```
services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    var oidcConfig = builder.Configuration.GetSection("OpenIDConnectSettings");
    options.Authority = oidcConfig["IdentityProviderUrl"];
    options.ClientSecret = oidcConfig["ClientSecret"];
    options.ClientId = oidcConfig["Audience"];
    options.ResponseType = OpenIdConnectResponseType.Code;

    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.Scope.Add("offline_access");

    options.ClaimActions.Remove("amr");
    options.ClaimActions.MapUniqueJsonKey("website", "website");

    options.GetClaimsFromUserInfoEndpoint = true;
    options.SaveTokens = true;

    // .NET 9 feature
    options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Require;

    options.TokenValidationParameters.NameClaimType = "name";
    options.TokenValidationParameters.RoleClaimType = "role";
});
```

### Implementing Microsoft identity providers

Microsoft has multiple identity providers and OpenID Connect implementations. Microsoft has different OpenID Connect servers:

* Microsoft Entra ID
* Microsoft Entra External ID

If authenticating using one of the Microsoft identity providers in ASP.NET Core, it is recommended to use the [`Microsoft.Identity.Web`](https://github.com/AzureAD/microsoft-identity-web) Nuget packages.

The `Microsoft.Identity.Web` Nuget packages is a Microsoft specific client built on top on the ASP.NET Core OpenID Connect client with some changes to the default client.

## Using third party OpenID Connect provider clients

Many OpenID Connect server implementations create Nuget packages which are optimized for the same OpenID Connect implementation. These packages implement the OpenID Connect client specifics with the extras required by the specific OpenID Connect server. `Microsoft.Identity.Web` is one example of this.

If implementing multiple OpenID Connect clients from different OpenID Connect servers in a single application, it's normally better to revert to the default ASP.NET Core implementation as the different clients overwrite some options which affect the other clients.

[OpenIddict Web providers](https://documentation.openiddict.com/integrations/web-providers) is a client implementation which supports many different server implementations.

[`IdentityModel`](https://github.com/IdentityModel/IdentityModel) is a .NET standard helper library for claims-based identity, OAuth 2.0 and OpenID Connect. This can also be used to help with the client implementation.

## Backend for frontend (BFF) security architecture

It is no longer recommended to implement OpenID Connect public clients for any web apps.

For more information, see the [draft OAuth 2.0 for Browser-Based Applications](https://datatracker.ietf.org/doc/draft-ietf-oauth-browser-based-apps/).

If implementing **web** applications which have no independent backend, we recommend using the [Backend for Frontend (BFF) pattern](/en-us/azure/architecture/patterns/backends-for-frontends) security architecture. This pattern can be implemented in different ways, but the authentication is always implemented in the backend, and no sensitive data is sent to the web client for further authorization or authentication flows.

## Advanced features, standards, extending the OIDC client

### Logging

Debugging OpenID Connect clients can be hard. Personally identifiable information (PII) data is not logged by default. If debugging in development mode, the `IdentityModelEventSource.ShowPII` can be used to log sensitive personal data. Don't deploy an app with `IdentityModelEventSource.ShowPII` to productive servers.

```
//using ...

using Microsoft.IdentityModel.Logging;

var builder = WebApplication.CreateBuilder(args);

//... code 

var app = builder.Build();

IdentityModelEventSource.ShowPII = true;

//... code 

app.Run();
```

For more information, see [Logging](../../fundamentals/logging/?view=aspnetcore-10.0#configuration).

Note

You may want to lower the configured log level to see all the required logs.

### OIDC and OAuth Parameter Customization

The OAuth and OIDC authentication handlers ([AdditionalAuthorizationParameters](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.oauthoptions.additionalauthorizationparameters#microsoft-aspnetcore-authentication-oauth-oauthoptions-additionalauthorizationparameters)) option allows customization of authorization message parameters that are usually included as part of the redirect query string.

## Map claims from OpenID Connect

For more information, see [Map, customize, transform claims in ASP.NET Core](claims?view=aspnetcore-10.0).

## Blazor OpenID Connect

For more information, see [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](../../blazor/security/blazor-web-app-with-oidc?view=aspnetcore-10.0).

## Standards

* [OpenID Connect 1.0](https://openid.net/specs/openid-connect-core-1_0-final.html)
* [Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636)
* [OAuth 2.0 for Browser-Based Applications](https://datatracker.ietf.org/doc/html/rfc10017)
* [The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)
* [OAuth 2.0 Pushed Authorization Requests (PAR) RFC 9126](https://datatracker.ietf.org/doc/html/rfc9126)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/layouts
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/layouts.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor layouts

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to create reusable layout components for Blazor apps.

## Usefulness of Blazor layouts

Some app elements, such as menus, copyright messages, and company logos, are usually part of app's overall presentation. Placing a copy of the markup for these elements into all of the components of an app isn't efficient. Every time that one of these elements is updated, every component that uses the element must be updated. This approach is costly to maintain and can lead to inconsistent content if an update is missed. *Layouts* solve these problems.

A Blazor layout is a Razor component that shares markup with components that reference it. Layouts can use [data binding](data-binding?view=aspnetcore-10.0), [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0), and other features of components.

## Create a layout component

To create a layout component:

* Create a Razor component defined by a Razor template or C# code. Layout components based on a Razor template use the `.razor` file extension just like ordinary Razor components. Because layout components are shared across an app's components, they're usually placed in the app's `Shared` or `Layout` folder. However, layouts can be placed in any location accessible to the components that use it. For example, a layout can be placed in the same folder as the components that use it.
* Inherit the component from [LayoutComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase). The [LayoutComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase) defines a [Body](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase.body#microsoft-aspnetcore-components-layoutcomponentbase-body) property ([RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) type) for the rendered content inside the layout.
* Use the Razor syntax `@Body` to specify the location in the layout markup where the content is rendered.

Note

For more information on [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment), see [ASP.NET Core Razor components](./?view=aspnetcore-10.0#child-content-render-fragments).

The following `DoctorWhoLayout` component shows the Razor template of a layout component. The layout inherits [LayoutComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase) and sets the `@Body` between the navigation bar (`<nav>...</nav>`) and the footer (`<footer>...</footer>`).

`DoctorWhoLayout.razor`:

```
@inherits LayoutComponentBase

<PageTitle>Doctor Who® Database</PageTitle>

<header>
    <h1>Doctor Who® Database</h1>
</header>

<nav>
    <a href="main-list">Main Episode List</a>
    <a href="search">Search</a>
    <a href="new">Add Episode</a>
</nav>

@Body

<footer>
    @TrademarkMessage
</footer>

@code {
    public string TrademarkMessage { get; set; } =
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/ https://www.bbc.com";
}
```

```
@inherits LayoutComponentBase

<PageTitle>Doctor Who® Database</PageTitle>

<header>
    <h1>Doctor Who® Database</h1>
</header>

<nav>
    <a href="main-list">Main Episode List</a>
    <a href="search">Search</a>
    <a href="new">Add Episode</a>
</nav>

@Body

<footer>
    @TrademarkMessage
</footer>

@code {
    public string TrademarkMessage { get; set; } =
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/ https://www.bbc.com";
}
```

```
@inherits LayoutComponentBase

<header>
    <h1>Doctor Who™ Episode Database</h1>
</header>

<nav>
    <a href="main-list">Main Episode List</a>
    <a href="search">Search</a>
    <a href="new">Add Episode</a>
</nav>

@Body

<footer>
    @TrademarkMessage
</footer>

@code {
    public string TrademarkMessage { get; set; } = 
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/";
}
```

```
@inherits LayoutComponentBase

<header>
    <h1>Doctor Who™ Episode Database</h1>
</header>

<nav>
    <a href="main-list">Main Episode List</a>
    <a href="search">Search</a>
    <a href="new">Add Episode</a>
</nav>

@Body

<footer>
    @TrademarkMessage
</footer>

@code {
    public string TrademarkMessage { get; set; } = 
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/";
}
```

```
@inherits LayoutComponentBase

<header>
    <h1>Doctor Who™ Episode Database</h1>
</header>

<nav>
    <a href="main-list">Main Episode List</a>
    <a href="search">Search</a>
    <a href="new">Add Episode</a>
</nav>

@Body

<footer>
    @TrademarkMessage
</footer>

@code {
    public string TrademarkMessage { get; set; } = 
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/";
}
```

```
@inherits LayoutComponentBase

<header>
    <h1>Doctor Who™ Episode Database</h1>
</header>

<nav>
    <a href="main-list">Main Episode List</a>
    <a href="search">Search</a>
    <a href="new">Add Episode</a>
</nav>

@Body

<footer>
    @TrademarkMessage
</footer>

@code {
    public string TrademarkMessage { get; set; } = 
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/";
}
```

## `MainLayout` component

In an app created from a [Blazor project template](../project-structure?view=aspnetcore-10.0), the `MainLayout` component is the app's [default layout](#apply-a-default-layout-to-an-app). Blazor's layout adopts the [Flexbox layout model](https://developer.mozilla.org/docs/Glossary/Flexbox) ([W3C specification](https://www.w3.org/TR/css-flexbox-1/)).

[Blazor's CSS isolation feature](css-isolation?view=aspnetcore-10.0) applies isolated CSS styles to the `MainLayout` component. By convention, the styles are provided by the accompanying stylesheet of the same name, `MainLayout.razor.css`. The ASP.NET Core framework implementation of the stylesheet is available for inspection in the ASP.NET Core reference source (`dotnet/aspnetcore` GitHub repository):

* Blazor Web App: Locate `MainLayout.razor.css` in the `Components/Layout` folder of the server project in the [project template](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp).
* [Blazor WebAssembly `MainLayout.razor.css`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Layout/MainLayout.razor.css).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

[Blazor's CSS isolation feature](css-isolation?view=aspnetcore-10.0) applies isolated CSS styles to the `MainLayout` component. By convention, the styles are provided by the accompanying stylesheet of the same name, `MainLayout.razor.css`.

## Statically-rendered layout components

When a Blazor Web App adopts per-page/component rendering (the `Routes` component doesn't specify an interactive render mode), layout components are rendered statically on the server. Applying an interactive render mode directly to a layout isn't supported because Blazor doesn't support serializing a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) (`@Body` in this case) as a root component parameter. For example, placing `@rendermode InteractiveServer` at the top of the `MainLayout` component results in the following runtime exception:

> System.InvalidOperationException: Cannot pass the parameter 'Body' to component 'MainLayout' with rendermode 'InteractiveServerRenderMode'. This is because the parameter is of the delegate type 'Microsoft.AspNetCore.Components.RenderFragment', which is arbitrary code and cannot be serialized.

This applies to any layout component that inherits from [LayoutComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase) in an app that adopts per-page/component rendering prior to the release of .NET 11.

In apps that target .NET 10 or earlier, you can adopt the following approach in a Blazor Web App that adopts per-page/component rendering.

Create a wrapper component that's capable of interactivity. In the following example, a wrapper component contains a [Blazor section](sections?view=aspnetcore-10.0) that can receive content from a child component.

In the imports file (`_Imports.razor`), add an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive for sections ([Microsoft.AspNetCore.Components.Sections](/en-us/dotnet/api/microsoft.aspnetcore.components.sections)):

```
@using Microsoft.AspNetCore.Components.Sections
```

Create the following interactive wrapper component in the `Pages` folder.

`Pages/InteractiveWrapper.razor`:

```
@rendermode InteractiveServer

<div>
    <SectionOutlet SectionName="top-bar" />
</div>

@ChildContent

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

The `Counter` component can use the wrapper component and set interactive section content. In the following example, a counter button is placed in the section.

`Pages/Counter.razor`:

```
@page "/counter"
@rendermode InteractiveServer

<InteractiveWrapper>

    <SectionContent SectionName="top-bar">
        <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    </SectionContent>

    <PageTitle>Counter</PageTitle>

    <h1>Counter</h1>

    <p role="status">Current count: @currentCount</p>

    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

</InteractiveWrapper>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

Other components around the app can also wrap content in the `InteractiveWrapper` component and set interactive section content.

## Apply a layout

### Make the layout namespace available

Layout file locations and namespaces changed over time for the Blazor framework. Depending on the version of Blazor and type of Blazor app that you're building, you may need to indicate the layout's namespace when using it. When referencing a layout implementation and the layout isn't found without indicating the layout's namespace, take any of the following approaches:

* Add an `@using` directive to the imports file (`_Imports.razor`) for the location of the layouts. In the following example, a folder of layouts with the name `Layout` is inside a `Components` folder, and the app's namespace is `BlazorSample`:

  ```
  @using BlazorSample.Components.Layout
  ```

* Add an `@using` directive at the top of the component definition where the layout is used:

  ```
  @using BlazorSample.Components.Layout
  @layout DoctorWhoLayout
  ```

* Fully qualify the namespace of the layout where it's used:

  ```
  @layout BlazorSample.Components.Layout.DoctorWhoLayout
  ```

### Apply a layout to a component

Use the [`@layout`](../../mvc/views/razor?view=aspnetcore-10.0#layout) Razor directive to apply a layout to a routable Razor component that has an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive. The compiler converts `@layout` into a [LayoutAttribute](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutattribute) and applies the attribute to the component class.

The content of the following `Episodes` component is inserted into the `DoctorWhoLayout` at the position of `@Body`.

`Episodes.razor`:

```
@page "/episodes"
@layout DoctorWhoLayout

<h2>Doctor Who® Episodes</h2>

<ul>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfknq">
            <em>The Ribos Operation</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfdsb">
            <em>The Sunmakers</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vhc26">
            <em>Nightmare of Eden</em>
        </a>
    </li>
</ul>
```

```
@page "/episodes"
@layout DoctorWhoLayout

<h2>Doctor Who® Episodes</h2>

<ul>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfknq">
            <em>The Ribos Operation</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfdsb">
            <em>The Sunmakers</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vhc26">
            <em>Nightmare of Eden</em>
        </a>
    </li>
</ul>
```

```
@page "/episodes"
@layout DoctorWhoLayout

<h2>Episodes</h2>

<ul>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfknq">
            <em>The Ribos Operation</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfdsb">
            <em>The Sun Makers</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vhc26">
            <em>Nightmare of Eden</em>
        </a>
    </li>
</ul>
```

```
@page "/episodes"
@layout DoctorWhoLayout

<h2>Episodes</h2>

<ul>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfknq">
            <em>The Ribos Operation</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfdsb">
            <em>The Sun Makers</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vhc26">
            <em>Nightmare of Eden</em>
        </a>
    </li>
</ul>
```

```
@page "/episodes"
@layout DoctorWhoLayout

<h2>Episodes</h2>

<ul>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfknq">
            <em>The Ribos Operation</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vfdsb">
            <em>The Sun Makers</em>
        </a>
    </li>
    <li>
        <a href="https://www.bbc.co.uk/programmes/p00vhc26">
            <em>Nightmare of Eden</em>
        </a>
    </li>
</ul>
```

The following rendered HTML markup is produced by the preceding `DoctorWhoLayout` and `Episodes` component. Extraneous markup doesn't appear in order to focus on the content provided by the two components involved:

* The H1 "database" heading (`<h1>...</h1>`) in the header (`<header>...</header>`), navigation bar (`<nav>...</nav>`), and trademark information in the footer (`<footer>...</footer>`) come from the `DoctorWhoLayout` component.
* The H2 "episodes" heading (`<h2>...</h2>`) and episode list (`<ul>...</ul>`) come from the `Episodes` component.

```
<header>
    <h1 ...>...</h1>
</header>

<nav>
    ...
</nav>

<h2>...</h2>

<ul>
    <li>...</li>
    <li>...</li>
    <li>...</li>
</ul>

<footer>
    ...
</footer>
```

Specifying the layout directly in a component overrides a *default layout*:

* Set by an `@layout` directive imported from an imports file (`_Imports.razor`), as described in the following [Apply a layout to a folder of components](#apply-a-layout-to-a-folder-of-components) section.
* Set as the app's default layout, as described in the [Apply a default layout to an app](#apply-a-default-layout-to-an-app) section later in this article.

### Apply a layout to a folder of components

Every folder of an app can optionally contain an imports file named `_Imports.razor`. The compiler includes the directives specified in the imports file in all of the Razor templates in the same folder and recursively in all of its subfolders. Therefore, an imports file containing `@layout DoctorWhoLayout` ensures that all of the components in a folder use the `DoctorWhoLayout` component. There's no need to repeatedly add `@layout DoctorWhoLayout` to all of the Razor components (`.razor`) within the folder and subfolders.

`_Imports.razor`:

```
@layout DoctorWhoLayout
...
```

The imports file is similar to the [\_ViewImports.cshtml file for Razor views and pages](../../mvc/views/layout?view=aspnetcore-10.0#importing-shared-directives) but applied specifically to Razor component files.

Specifying a layout in the imports file overrides a layout specified as the router's [default app layout](#apply-a-default-layout-to-an-app), which is described in the following section.

Warning

Do **not** add a Razor `@layout` directive to the root imports file, which results in an infinite loop of layouts. To control the default app layout, specify the layout in the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. For more information, see the following [Apply a default layout to an app](#apply-a-default-layout-to-an-app) section.

The same condition results when using an imports file to apply a layout to a folder of components with the `@layout` directive and the layout component itself is in the same folder or folder hierarchy of the imports file. An infinite loop of applying the layout occurs because the `@layout` directive is also applied to the layout component. To avoid recursion problems, we recommend storing layout components in their own folder (for example, `Layouts`), away from where imports files are applying them.

Note

The [`@layout`](../../mvc/views/razor?view=aspnetcore-10.0#layout) Razor directive only applies a layout to routable Razor components with an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive.

### Apply a default layout to an app

Specify the default app layout in the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component's [RouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.routeview) component. Use the [DefaultLayout](/en-us/dotnet/api/microsoft.aspnetcore.components.routeview.defaultlayout) parameter to set the layout type:

```
<RouteView RouteData="routeData" DefaultLayout="typeof({LAYOUT})" />
```

In the preceding example, the `{LAYOUT}` placeholder is the layout (for example, `DoctorWhoLayout` if the layout file name is `DoctorWhoLayout.razor`). You may need to identify the layout's namespace depending on the .NET version and type of Blazor app. For more information, see the [Make the layout namespace available](#make-the-layout-namespace-available) section.

Specifying the layout as a default layout in the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component's [RouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.routeview) is a useful practice because you can override the layout on a per-component or per-folder basis, as described in the preceding sections of this article. We recommend using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component to set the app's default layout because it's the most general and flexible approach for using layouts.

### Apply a layout to arbitrary content (`LayoutView` component)

To set a layout for arbitrary Razor template content, specify the layout with a [LayoutView](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutview) component. You can use a [LayoutView](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutview) in any Razor component. The following example sets a layout component named `ErrorLayout` for the component that includes the following Razor markup.

```
<LayoutView Layout="typeof(ErrorLayout)">
    <h1>Page not found</h1>
    <p>Sorry, there's nothing at this address.</p>
</LayoutView>
```

You may need to identity the layout's namespace depending on the .NET version and type of Blazor app. For more information, see the [Make the layout namespace available](#make-the-layout-namespace-available) section.

Note

With the release of .NET 5.0.1 and for any additional 5.x releases, the `Router` component includes the `PreferExactMatches` parameter set to `@true`. For more information, see [Migrate from ASP.NET Core 3.1 to .NET 5](../../migration/31-to-50?view=aspnetcore-10.0#changes-to-blazor-app-routing-logic-in-501-and-further-5x-releases-up-to-60).

## Nested layouts

A component can reference a layout that in turn references another layout. For example, nested layouts are used to create a multi-level menu structures.

The following example shows how to use nested layouts. The `Episodes` component shown in the [Apply a layout to a component](#apply-a-layout-to-a-component) section is the component to display. The component references the `DoctorWhoLayout` component.

The following `DoctorWhoLayout` component is a modified version of the example shown earlier in this article. The header and footer elements are removed, and the layout references another layout, `ProductionsLayout`. The `Episodes` component is rendered where `@Body` appears in the `DoctorWhoLayout`.

`DoctorWhoLayout.razor`:

```
@inherits LayoutComponentBase
@layout ProductionsLayout

<PageTitle>Doctor Who® Database</PageTitle>

<h1>Doctor Who® Database</h1>

<nav>
    <a href="main-episode-list">Main Episode List</a>
    <a href="episode-search">Search</a>
    <a href="new-episode">Add Episode</a>
</nav>

@Body

<div>
    @TrademarkMessage
</div>

@code {
    public string TrademarkMessage { get; set; } =
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/ https://www.bbc.com";
}
```

```
@inherits LayoutComponentBase
@layout ProductionsLayout

<PageTitle>Doctor Who® Database</PageTitle>

<h1>Doctor Who® Database</h1>

<nav>
    <a href="main-episode-list">Main Episode List</a>
    <a href="episode-search">Search</a>
    <a href="new-episode">Add Episode</a>
</nav>

@Body

<div>
    @TrademarkMessage
</div>

@code {
    public string TrademarkMessage { get; set; } =
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/ https://www.bbc.com";
}
```

```
@inherits LayoutComponentBase
@layout ProductionsLayout

<h1>Doctor Who™ Episode Database</h1>

<nav>
    <a href="main-episode-list">Main Episode List</a>
    <a href="episode-search">Search</a>
    <a href="new-episode">Add Episode</a>
</nav>

@Body

<div>
    @TrademarkMessage
</div>

@code {
    public string TrademarkMessage { get; set; } = 
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/";
}
```

```
@inherits LayoutComponentBase
@layout ProductionsLayout

<h1>Doctor Who™ Episode Database</h1>

<nav>
    <a href="main-episode-list">Main Episode List</a>
    <a href="episode-search">Search</a>
    <a href="new-episode">Add Episode</a>
</nav>

@Body

<div>
    @TrademarkMessage
</div>

@code {
    public string TrademarkMessage { get; set; } = 
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/";
}
```

```
@inherits LayoutComponentBase
@layout ProductionsLayout

<h1>Doctor Who™ Episode Database</h1>

<nav>
    <a href="main-episode-list">Main Episode List</a>
    <a href="episode-search">Search</a>
    <a href="new-episode">Add Episode</a>
</nav>

@Body

<div>
    @TrademarkMessage
</div>

@code {
    public string TrademarkMessage { get; set; } = 
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/";
}
```

```
@inherits LayoutComponentBase
@layout ProductionsLayout

<h1>Doctor Who™ Episode Database</h1>

<nav>
    <a href="main-episode-list">Main Episode List</a>
    <a href="episode-search">Search</a>
    <a href="new-episode">Add Episode</a>
</nav>

@Body

<div>
    @TrademarkMessage
</div>

@code {
    public string TrademarkMessage { get; set; } = 
        "Doctor Who is a registered trademark of the BBC. " +
        "https://www.doctorwho.tv/";
}
```

The `ProductionsLayout` component contains the top-level layout elements, where the header (`<header>...</header>`) and footer (`<footer>...</footer>`) elements now reside. The `DoctorWhoLayout` with the `Episodes` component is rendered where `@Body` appears.

`ProductionsLayout.razor`:

```
@inherits LayoutComponentBase

<header>
    <h1>Productions</h1>
</header>

<nav>
    <a href="main-production-list">Main Production List</a>
    <a href="production-search">Search</a>
    <a href="new-production">Add Production</a>
</nav>

@Body

<footer>
    Footer of Productions Layout
</footer>
```

```
@inherits LayoutComponentBase

<header>
    <h1>Productions</h1>
</header>

<nav>
    <a href="main-production-list">Main Production List</a>
    <a href="production-search">Search</a>
    <a href="new-production">Add Production</a>
</nav>

@Body

<footer>
    Footer of Productions Layout
</footer>
```

```
@inherits LayoutComponentBase

<header>
    <h1>Productions</h1>
</header>

<nav>
    <a href="main-production-list">Main Production List</a>
    <a href="production-search">Search</a>
    <a href="new-production">Add Production</a>
</nav>

@Body

<footer>
    Footer of Productions Layout
</footer>
```

```
@inherits LayoutComponentBase

<header>
    <h1>Productions</h1>
</header>

<nav>
    <a href="main-production-list">Main Production List</a>
    <a href="production-search">Search</a>
    <a href="new-production">Add Production</a>
</nav>

@Body

<footer>
    Footer of Productions Layout
</footer>
```

```
@inherits LayoutComponentBase

<header>
    <h1>Productions</h1>
</header>

<nav>
    <a href="main-production-list">Main Production List</a>
    <a href="production-search">Search</a>
    <a href="new-production">Add Production</a>
</nav>

@Body

<footer>
    Footer of Productions Layout
</footer>
```

```
@inherits LayoutComponentBase

<header>
    <h1>Productions</h1>
</header>

<nav>
    <a href="main-production-list">Main Production List</a>
    <a href="production-search">Search</a>
    <a href="new-production">Add Production</a>
</nav>

@Body

<footer>
    Footer of Productions Layout
</footer>
```

The following rendered HTML markup is produced by the preceding nested layout. Extraneous markup doesn't appear in order to focus on the nested content provided by the three components involved:

* The header (`<header>...</header>`), production navigation bar (`<nav>...</nav>`), and footer (`<footer>...</footer>`) elements and their content come from the `ProductionsLayout` component.
* The H1 "database" heading (`<h1>...</h1>`), episode navigation bar (`<nav>...</nav>`), and trademark information (`<div>...</div>`) come from the `DoctorWhoLayout` component.
* The H2 "episodes" heading (`<h2>...</h2>`) and episode list (`<ul>...</ul>`) come from the `Episodes` component.

```
<header>
    ...
</header>

<nav>
    <a href="main-production-list">Main Production List</a>
    <a href="production-search">Search</a>
    <a href="new-production">Add Production</a>
</nav>

<h1>...</h1>

<nav>
    <a href="main-episode-list">Main Episode List</a>
    <a href="episode-search">Search</a>
    <a href="new-episode">Add Episode</a>
</nav>

<h2>...</h2>

<ul>
    <li>...</li>
    <li>...</li>
    <li>...</li>
</ul>

<div>
    ...
</div>

<footer>
    ...
</footer>
```

## Share a Razor Pages layout with integrated components

When routable components are integrated into a Razor Pages app, the app's shared layout can be used with the components. For more information, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](integration?view=aspnetcore-10.0).

## Sections

To control the content in a layout from a child Razor component, see [ASP.NET Core Blazor sections](sections?view=aspnetcore-10.0).

## Related content

* [Layout in ASP.NET Core](../../mvc/views/layout?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host and deploy ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

In general, to deploy an ASP.NET Core app to a hosting environment:

* Deploy the published app to a folder on the hosting server.
* Set up a process manager that starts the app when requests arrive and restarts the app after it crashes or the server reboots.
* For configuration of a reverse proxy, set up a reverse proxy to forward requests to the app.

For Blazor host and deploy guidance, which adds to or supersedes the guidance in this node, see [Host and deploy ASP.NET Core Blazor](../blazor/host-and-deploy/?view=aspnetcore-10.0).

## Publish to a folder

The [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command compiles app code and copies the files required to run the app into a *publish* folder. When deploying from Visual Studio, the `dotnet publish` step occurs automatically before the files are copied to the deployment destination.

### Run the published app locally

To run the published app locally, run `dotnet <ApplicationName>.dll` from the *publish* folder.

## Publish settings files

`*.json` files are published by default. To publish other settings files, specify them in an [`<ItemGroup><Content Include= ... />`](/en-us/visualstudio/msbuild/common-msbuild-project-items#content) element in the project file. The following example publishes XML files:

```
<ItemGroup>
  <Content Include="**\*.xml" Exclude="bin\**\*;obj\**\*"
    CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
```

### Folder contents

The *publish* folder contains one or more app assembly files, dependencies, and optionally the .NET runtime.

A .NET Core app can be published as *self-contained deployment* or *framework-dependent deployment*. If the app is self-contained, the assembly files that contain the .NET runtime are included in the *publish* folder. If the app is framework-dependent, the .NET runtime files aren't included because the app has a reference to a version of .NET that's installed on the server. The default deployment model is framework-dependent. For more information, see [.NET Core application deployment](/en-us/dotnet/core/deploying/).

In addition to *.exe* and *.dll* files, the *publish* folder for an ASP.NET Core app typically contains configuration files, static assets, and MVC views. For more information, see [ASP.NET Core directory structure](directory-structure?view=aspnetcore-10.0).

## Set up a process manager

An ASP.NET Core app is a console app that must be started when a server boots and restarted if it crashes. To automate starts and restarts, a process manager is required. The most common process managers for ASP.NET Core are:

* Linux
  * [Nginx](linux-nginx?view=aspnetcore-10.0)

* Windows

  * [IIS](iis/?view=aspnetcore-10.0)
  * [Windows Service](windows-service?view=aspnetcore-10.0)

## Set up a reverse proxy

If the app uses the [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server, [Nginx](linux-nginx?view=aspnetcore-10.0), or [IIS](iis/?view=aspnetcore-10.0) can be used as a reverse proxy server. A reverse proxy server receives HTTP requests from the Internet and forwards them to Kestrel.

Either configuration—with or without a reverse proxy server—is a supported hosting configuration. For more information, see [When to use Kestrel with a reverse proxy](../fundamentals/servers/kestrel/when-to-use-a-reverse-proxy?view=aspnetcore-10.0).

Either configuration—with or without a reverse proxy server—is a supported hosting configuration. For more information, see [When to use Kestrel with a reverse proxy](../fundamentals/servers/kestrel?view=aspnetcore-10.0#when-to-use-kestrel-with-a-reverse-proxy).

## Proxy server and load balancer scenarios

Additional configuration might be required for apps hosted behind proxy servers and load balancers. Without additional configuration, an app might not have access to the scheme (HTTP/HTTPS) and the remote IP address where a request originated. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0).

## Use Visual Studio and MSBuild to automate deployments

Deployment often requires additional tasks besides copying the output from [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) to a server. For example, extra files might be required or excluded from the *publish* folder. Visual Studio uses [MSBuild](/en-us/visualstudio/msbuild/msbuild) for web deployment, and MSBuild can be customized to do many other tasks during deployment. For more information, see [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](visual-studio-publish-profiles?view=aspnetcore-10.0) and the [Using MSBuild and Team Foundation Build](https://www.microsoftpressstore.com/store/inside-the-microsoft-build-engine-using-msbuild-and-9780735645240) book.

By using [the Publish Web feature](../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0), apps can be deployed directly from Visual Studio to the Azure App Service. Azure DevOps Services supports [continuous deployment to Azure App Service](/en-us/azure/devops/pipelines/targets/webapp). For more information, see [DevOps for ASP.NET Core Developers](/en-us/dotnet/architecture/devops-for-aspnet-developers).

## Publish to Azure

See [Publish an ASP.NET Core app to Azure with Visual Studio](../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0) for instructions on how to publish an app to Azure using Visual Studio. An additional example is provided by [Create an ASP.NET Core web app in Azure](/en-us/azure/app-service/app-service-web-get-started-dotnet).

## Publish with MSDeploy on Windows

See [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](visual-studio-publish-profiles?view=aspnetcore-10.0) for instructions on how to publish an app with a Visual Studio publish profile, including from a Windows command prompt using the [dotnet msbuild](/en-us/dotnet/core/tools/dotnet-msbuild) command.

## Internet Information Services (IIS)

For deployments to Internet Information Services (IIS) with configuration provided by the *web.config* file, see the articles under [Host ASP.NET Core on Windows with IIS](iis/?view=aspnetcore-10.0).

## Host in a web farm

For information on configuration for hosting ASP.NET Core apps in a web farm environment (for example, deployment of multiple instances of your app for scalability), see [Host ASP.NET Core in a web farm](web-farm?view=aspnetcore-10.0).

## Host on Docker

For more information, see [Host ASP.NET Core in Docker containers](docker/?view=aspnetcore-10.0).

## Perform health checks

Use health checks middleware to perform health checks on an app and its dependencies. For more information, see [Health checks in ASP.NET Core](health-checks?view=aspnetcore-10.0).

## Additional resources

* [.NET application publishing overview](/en-us/dotnet/core/deploying)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)
* [ASP.NET Hosting](https://dotnet.microsoft.com/apps/aspnet/hosting)

In general, to deploy an ASP.NET Core app to a hosting environment:

* Deploy the published app to a folder on the hosting server.
* Set up a process manager that starts the app when requests arrive and restarts the app after it crashes or the server reboots.
* For configuration of a reverse proxy, set up a reverse proxy to forward requests to the app.

## Publish to a folder

The [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command compiles app code and copies the files required to run the app into a *publish* folder. When deploying from Visual Studio, the `dotnet publish` step occurs automatically before the files are copied to the deployment destination.

### Folder contents

The *publish* folder contains one or more app assembly files, dependencies, and optionally the .NET runtime.

A .NET Core app can be published as *self-contained deployment* or *framework-dependent deployment*. If the app is self-contained, the assembly files that contain the .NET runtime are included in the *publish* folder. If the app is framework-dependent, the .NET runtime files aren't included because the app has a reference to a version of .NET that's installed on the server. The default deployment model is framework-dependent. For more information, see [.NET Core application deployment](/en-us/dotnet/core/deploying/).

In addition to *.exe* and *.dll* files, the *publish* folder for an ASP.NET Core app typically contains configuration files, static assets, and MVC views. For more information, see [ASP.NET Core directory structure](directory-structure?view=aspnetcore-10.0).

## Set up a process manager

An ASP.NET Core app is a console app that must be started when a server boots and restarted if it crashes. To automate starts and restarts, a process manager is required. The most common process managers for ASP.NET Core are:

* Linux
  * [Nginx](linux-nginx?view=aspnetcore-10.0)

* Windows

  * [IIS](iis/?view=aspnetcore-10.0)
  * [Windows Service](windows-service?view=aspnetcore-10.0)

## Set up a reverse proxy

If the app uses the [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) server, [Nginx](linux-nginx?view=aspnetcore-10.0), or [IIS](iis/?view=aspnetcore-10.0) can be used as a reverse proxy server. A reverse proxy server receives HTTP requests from the Internet and forwards them to Kestrel.

Either configuration—with or without a reverse proxy server—is a supported hosting configuration. For more information, see [When to use Kestrel with a reverse proxy](../fundamentals/servers/kestrel?view=aspnetcore-10.0#when-to-use-kestrel-with-a-reverse-proxy).

## Proxy server and load balancer scenarios

Additional configuration might be required for apps hosted behind proxy servers and load balancers. Without additional configuration, an app might not have access to the scheme (HTTP/HTTPS) and the remote IP address where a request originated. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](proxy-load-balancer?view=aspnetcore-10.0).

## Use Visual Studio and MSBuild to automate deployments

Deployment often requires additional tasks besides copying the output from [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) to a server. For example, extra files might be required or excluded from the *publish* folder. Visual Studio uses MSBuild for web deployment, and MSBuild can be customized to do many other tasks during deployment. For more information, see [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](visual-studio-publish-profiles?view=aspnetcore-10.0) and the [Using MSBuild and Team Foundation Build](https://www.microsoftpressstore.com/store/inside-the-microsoft-build-engine-using-msbuild-and-9780735645240) book.

By using [the Publish Web feature](../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0), apps can be deployed directly from Visual Studio to the Azure App Service. Azure DevOps Services supports [continuous deployment to Azure App Service](/en-us/azure/devops/pipelines/targets/webapp). For more information, see [DevOps for ASP.NET Core Developers](/en-us/dotnet/architecture/devops-for-aspnet-developers).

## Publish to Azure

See [Publish an ASP.NET Core app to Azure with Visual Studio](../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0) for instructions on how to publish an app to Azure using Visual Studio. An additional example is provided by [Create an ASP.NET Core web app in Azure](/en-us/azure/app-service/app-service-web-get-started-dotnet).

## Publish with MSDeploy on Windows

See [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](visual-studio-publish-profiles?view=aspnetcore-10.0) for instructions on how to publish an app with a Visual Studio publish profile, including from a Windows command prompt using the [dotnet msbuild](/en-us/dotnet/core/tools/dotnet-msbuild) command.

## Internet Information Services (IIS)

For deployments to Internet Information Services (IIS) with configuration provided by the *web.config* file, see the articles under [Host ASP.NET Core on Windows with IIS](iis/?view=aspnetcore-10.0).

## Host in a web farm

For information on configuration for hosting ASP.NET Core apps in a web farm environment (for example, deployment of multiple instances of your app for scalability), see [Host ASP.NET Core in a web farm](web-farm?view=aspnetcore-10.0).

## Host on Docker

For more information, see [Host ASP.NET Core in Docker containers](docker/?view=aspnetcore-10.0).

## Additional resources

* [.NET application publishing overview](/en-us/dotnet/core/deploying)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)
* [ASP.NET Hosting](https://dotnet.microsoft.com/apps/aspnet/hosting)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0023
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0023.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0023: Route conflict detected between route handlers

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0023      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

[ASP0022](asp0022?view=aspnetcore-10.0) warns on route conflicts in [Minimal API](../fundamentals/apis?view=aspnetcore-10.0) apps, this analyzer warns on route conflicts in ASP.NET Core MVC apps.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/test/troubleshoot-azure-iis
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot-azure-iis.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Troubleshoot ASP.NET Core on Azure App Service and IIS

This article provides information on common app startup errors and instructions on how to diagnose errors when an app is deployed to Azure App Service or IIS:

[App startup errors](#app-startup-errors)\
Explains common startup HTTP status code scenarios.

[Troubleshoot on Azure App Service](#troubleshoot-on-azure-app-service)\
Provides troubleshooting advice for apps deployed to Azure App Service.

[Troubleshoot on IIS](#troubleshoot-on-iis)\
Provides troubleshooting advice for apps deployed to IIS or running on IIS Express locally. The guidance applies to both Windows Server and Windows desktop deployments.

[Clear package caches](#clear-package-caches)\
Explains what to do when incoherent packages break an app when performing major upgrades or changing package versions.

[Related content](#related-content)\
Lists additional troubleshooting topics.

## App startup errors

In Visual Studio, the ASP.NET Core project default server is Kestrel. Visual studio can be configured to use [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview). A *502.5 - Process Failure* or a *500.30 - Start Failure* that occurs when debugging locally with IIS Express can be diagnosed using the advice in this topic.

### 403.14 Forbidden

The app fails to start. The following error is logged:

```
The Web server is configured to not list the contents of this directory.
```

The error is usually caused by a broken deployment on the hosting system, which includes any of the following scenarios:

* The app is deployed to the wrong folder on the hosting system.
* The deployment process failed to move all of the app's files and folders to the deployment folder on the hosting system.
* The *web.config* file is missing from the deployment, or the *web.config* file contents are malformed.

Perform the following steps:

1. Delete all of the files and folders from the deployment folder on the hosting system.

2. Redeploy the contents of the app's *publish* folder to the hosting system using your normal method of deployment, such as Visual Studio, PowerShell, or manual deployment:

   * Confirm that the *web.config* file is present in the deployment and that its contents are correct.
   * When hosting on Azure App Service, confirm that the app is deployed to the `D:\home\site\wwwroot` folder.
   * When the app is hosted by IIS, confirm that the app is deployed to the IIS **Physical path** shown in **IIS Manager**'s **Basic Settings**.

3. Confirm that all of the app's files and folders are deployed by comparing the deployment on the hosting system to the contents of the project's *publish* folder.

For more information on the layout of a published ASP.NET Core app, see [ASP.NET Core directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0). For more information on the *web.config* file, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#configuration-with-webconfig).

### 500 Internal Server Error

The app starts, but an error prevents the server from fulfilling the request.

This error occurs within the app's code during startup or while creating a response. The response may contain no content, or the response may appear as a *500 Internal Server Error* in the browser. The Application Event Log usually states that the app started normally. From the server's perspective, that's correct. The app did start, but it can't generate a valid response. Run the app at a command prompt on the server or enable the ASP.NET Core Module stdout log to troubleshoot the problem.

This error also may occur when the .NET Hosting Bundle isn't installed or is corrupted. Installing or repairing the installation of the .NET Hosting Bundle (for IIS) or Visual Studio (for IIS Express) may fix the problem.

### 500.0 In-Process Handler Load Failure

The worker process fails. The app doesn't start.

An unknown error occurred loading [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) components. Take one of the following actions:

* Contact [Microsoft Support](https://support.microsoft.com/oas/default.aspx?prid=15832) (select **Developer Tools** then **ASP.NET Core**).
* Ask a question on Stack Overflow.
* File an issue on our [GitHub repository](https://github.com/dotnet/AspNetCore).

### 500.30 In-Process Startup Failure

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) attempts to start the .NET CLR in-process, but it fails to start. The cause of a process startup failure can usually be determined from entries in the Application Event Log and the ASP.NET Core Module stdout log.

Common failure conditions:

* The app is misconfigured due to targeting a version of the ASP.NET Core shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are installed on the target machine.
* Using Azure Key Vault, lack of permissions to the Key Vault. Check the access policies in the targeted Key Vault to ensure that the correct permissions are granted.

### 500.31 ANCM Failed to Find Native Dependencies

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) attempts to start the .NET runtime in-process, but it fails to start. The most common cause of this startup failure is when the `Microsoft.NETCore.App` or `Microsoft.AspNetCore.App` runtime isn't installed. If the app is deployed to target ASP.NET Core 3.0 and that version doesn't exist on the machine, this error occurs. An example error message follows:

```
The specified framework 'Microsoft.NETCore.App', version '3.0.0' was not found.
  - The following frameworks were found:
      2.2.1 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
      3.0.0-preview5-27626-15 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
      3.0.0-preview6-27713-13 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
      3.0.0-preview6-27714-15 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
      3.0.0-preview6-27723-08 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
```

The error message lists all the installed .NET versions and the version requested by the app. To fix this error, either:

* Install the appropriate version of .NET on the machine.
* Change the app to target a version of .NET that's present on the machine.
* Publish the app as a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).

When running in development (the `ASPNETCORE_ENVIRONMENT` environment variable is set to `Development`), the specific error is written to the HTTP response. The cause of a process startup failure is also found in the Application Event Log.

### 500.32 ANCM Failed to Load dll

The worker process fails. The app doesn't start.

The most common cause for this error is that the app is published for an incompatible processor architecture. If the worker process is running as a 32-bit app and the app was published to target 64-bit, this error occurs.

To fix this error, either:

* Republish the app for the same processor architecture as the worker process.
* Publish the app as a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-executables-fde).

### 500.33 ANCM Request Handler Load Failure

The worker process fails. The app doesn't start.

The app didn't reference the `Microsoft.AspNetCore.App` framework. Only apps targeting the `Microsoft.AspNetCore.App` framework can be hosted by the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0).

To fix this error, confirm that the app is targeting the `Microsoft.AspNetCore.App` framework. Check the `.runtimeconfig.json` to verify the framework targeted by the app.

### 500.34 ANCM Mixed Hosting Models Not Supported

The worker process can't run both an in-process app and an out-of-process app in the same process.

To fix this error, run apps in separate IIS application pools.

### 500.35 ANCM Multiple In-Process Applications in same Process

The worker process can't run multiple in-process apps in the same process.

To fix this error, run apps in separate IIS application pools.

### 500.36 ANCM Out-Of-Process Handler Load Failure

The out-of-process request handler, *aspnetcorev2\_outofprocess.dll*, isn't next to the *aspnetcorev2.dll* file. This indicates a corrupted installation of the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0).

To fix this error, repair the installation of the [.NET Hosting Bundle](../host-and-deploy/iis/?view=aspnetcore-10.0#install-the-net-hosting-bundle) (for IIS) or Visual Studio (for IIS Express).

### 500.37 ANCM Failed to Start Within Startup Time Limit

ANCM failed to start within the provided startup time limit. By default, the timeout is 120 seconds.

This error can occur when starting a large number of apps on the same machine. Check for CPU/Memory usage spikes on the server during startup. You may need to stagger the startup process of multiple apps.

### 500.38 ANCM Application DLL Not Found

ANCM failed to locate the application DLL, which should be next to the executable.

This error occurs when hosting an app packaged as a [single-file executable](/en-us/dotnet/core/whats-new/dotnet-core-3-0#single-file-executables) using the in-process hosting model. The in-process model requires that the ANCM load the .NET app into the existing IIS process. This scenario isn't supported by the single-file deployment model. Use **one** of the following approaches in the app's project file to fix this error:

1. Disable single-file publishing by setting the `PublishSingleFile` MSBuild property to `false`.
2. Switch to the out-of-process hosting model by setting the `AspNetCoreHostingModel` MSBuild property to `OutOfProcess`.

### 502.5 Process Failure

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) attempts to start the worker process but it fails to start. The cause of a process startup failure can usually be determined from entries in the Application Event Log and the ASP.NET Core Module stdout log.

A common failure condition is the app is misconfigured due to targeting a version of the ASP.NET Core shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are installed on the target machine. The *shared framework* is the set of assemblies (`.dll` files) that are installed on the machine and referenced by a metapackage such as `Microsoft.AspNetCore.App`. The metapackage reference can specify a minimum required version.

The *502.5 Process Failure* error page is returned when a hosting or app misconfiguration causes the worker process to fail:

### Failed to start application (ErrorCode '0x800700c1')

```
EventID: 1010
Source: IIS AspNetCore Module V2
Failed to start application '/LM/W3SVC/6/ROOT/', ErrorCode '0x800700c1'.
```

The app failed to start because the app's assembly (*.dll*) couldn't be loaded.

This error occurs when there's a bitness mismatch between the published app and the w3wp/iisexpress process.

Confirm that the app pool's 32-bit setting is correct:

1. Select the app pool in IIS Manager's **Application Pools**.

2. Select **Advanced Settings** under **Edit Application Pool** in the **Actions** panel.

3. Set **Enable 32-Bit Applications**:

   * If deploying a 32-bit (x86) app, set the value to `True`.
   * If deploying a 64-bit (x64) app, set the value to `False`.

Confirm that there isn't a conflict between a `<Platform>` MSBuild property in the project file and the published bitness of the app.

### Failed to start application (ErrorCode '0x800701b1')

```
EventID: 1010
Source: IIS AspNetCore Module V2
Failed to start application '/LM/W3SVC/3/ROOT', ErrorCode '0x800701b1'.
```

The app failed to start because a Windows Service failed to load.

One common service that needs to be enabled is the "null" service. The following command enables the `null` Windows Service:

```
sc.exe start null
```

### Connection reset

If an error occurs after the headers are sent, it's too late for the server to send a **500 Internal Server Error** when an error occurs. This often happens when an error occurs during the serialization of complex objects for a response. This type of error appears as a *connection reset* error on the client. [Application logging](../fundamentals/logging/?view=aspnetcore-10.0) can help troubleshoot these types of errors.

### Default startup limits

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) is configured with a default *startupTimeLimit* of 120 seconds. When left at the default value, an app may take up to two minutes to start before the module logs a process failure. For information on configuring the module, see [Attributes of the aspNetCore element](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#attributes-of-the-aspnetcore-element).

## Troubleshoot on Azure App Service

Important

**ASP.NET Core preview releases with Azure App Service**

ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core preview release, see [Deploy ASP.NET Core preview release to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service).

### Azure App Services Log stream

The Azure App Services Log streams logging information as it occurs. To view streaming logs:

1. In the Azure portal, open the app in **App Services**.
2. In the left pane, navigate to **Monitoring** > **App Service Logs**.
3. Select **File System** for **Web Server Logging**. Optionally enable **Application logging**.
4. In the left pane, navigate to **Monitoring** > **Log stream**, and then select **Application logs** or **Web Server Logs**.

The following images shows the application logs output:

Streaming logs have some latency and might not display immediately.

### Application Event Log (Azure App Service)

To access the Application Event Log, use the **Diagnose and solve problems** blade in the Azure portal:

1. In the Azure portal, open the app in **App Services**.
2. Select **Diagnose and solve problems**.
3. Select the **Diagnostic Tools** heading.
4. Under **Support Tools**, select the **Application Events** button.
5. Examine the latest error provided by the *IIS AspNetCoreModule* or *IIS AspNetCoreModule V2* entry in the **Source** column.

An alternative to using the **Diagnose and solve problems** blade is to examine the Application Event Log file directly using [Kudu](https://github.com/projectkudu/kudu/wiki):

1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
3. Open the **LogFiles** folder.
4. Select the pencil icon next to the `eventlog.xml` file.
5. Examine the log. Scroll to the bottom of the log to see the most recent events.

### Run the app in the Kudu console

Many startup errors don't produce useful information in the Application Event Log. You can run the app in the [Kudu](https://github.com/projectkudu/kudu/wiki) Remote Execution Console to discover the error:

1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.

#### Test a 32-bit (x86) app

**Current release**

1. `cd d:\home\site\wwwroot`

2. Run the app:

   * If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

     ```
     dotnet .\{ASSEMBLY NAME}.dll
     ```

   * If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

     ```
     {ASSEMBLY NAME}.exe
     ```

The console output from the app, showing any errors, is piped to the Kudu console.

**Framework-dependent deployment running on a preview release**

*Requires installing the ASP.NET Core {VERSION} (x86) Runtime site extension.*

1. `cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x32` (`{X.Y}` is the runtime version)
2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

The console output from the app, showing any errors, is piped to the Kudu console.

#### Test a 64-bit (x64) app

**Current release**

* If the app is a 64-bit (x64) [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

  1. `cd D:\Program Files\dotnet`
  2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

* If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

  1. `cd D:\home\site\wwwroot`
  2. Run the app: `{ASSEMBLY NAME}.exe`

The console output from the app, showing any errors, is piped to the Kudu console.

**Framework-dependent deployment running on a preview release**

*Requires installing the ASP.NET Core {VERSION} (x64) Runtime site extension.*

1. `cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x64` (`{X.Y}` is the runtime version)
2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

The console output from the app, showing any errors, is piped to the Kudu console.

### ASP.NET Core Module stdout log (Azure App Service)

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created. Only use stdout logging to troubleshoot app startup problems.

For general logging in an ASP.NET Core app after startup, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

The ASP.NET Core Module stdout log often records useful error messages not found in the Application Event Log. To enable and view stdout logs:

1. In the Azure portal, navigate to the web app.
2. In the **App Service** blade, enter **kudu** in the search box.
3. Select **Advanced Tools** > **Go**.
4. Select **Debug console > CMD**.
5. Navigate to *site/wwwroot*
6. Select the pencil icon to edit the *web.config* file.
7. In the `<aspNetCore />` element, set `stdoutLogEnabled="true"` and select **Save**.

Disable stdout logging when troubleshooting is complete by setting `stdoutLogEnabled="false"`.

For more information, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

[]()

### ASP.NET Core Module debug log (Azure App Service)

The ASP.NET Core Module debug log provides additional, deeper logging from the ASP.NET Core Module. To enable and view stdout logs:

1. To enable the enhanced diagnostic log, perform either of the following:

   * Follow the instructions in [Enhanced diagnostic logs](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs) to configure the app for an enhanced diagnostic logging. Redeploy the app.

   * Add the `<handlerSettings>` shown in [Enhanced diagnostic logs](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs) to the live app's *web.config* file using the Kudu console:

     1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
     2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
     3. Open the folders to the path **site** > **wwwroot**. Edit the *web.config* file by selecting the pencil button. Add the `<handlerSettings>` section as shown in [Enhanced diagnostic logs](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs). Select the **Save** button.

2. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.

3. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.

4. Open the folders to the path **site** > **wwwroot**. If you didn't supply a path for the *aspnetcore-debug.log* file, the file appears in the list. If you supplied a path, navigate to the location of the log file.

5. Open the log file with the pencil button next to the file name.

Disable debug logging when troubleshooting is complete:

To disable the enhanced debug log, perform either of the following:

* Remove the `<handlerSettings>` from the *web.config* file locally and redeploy the app.
* Use the Kudu console to edit the *web.config* file and remove the `<handlerSettings>` section. Save the file.

For more information, see [Log creation and redirection with the ASP.NET Core Module](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs).

Warning

Failure to disable the debug log can lead to app or server failure. There's no limit on log file size. Only use debug logging to troubleshoot app startup problems.

For general logging in an ASP.NET Core app after startup, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

### Slow or nonresponsive app (Azure App Service)

When an app responds slowly or doesn't respond to a request, see [Troubleshoot slow web app performance issues in Azure App Service](/en-us/azure/app-service/app-service-web-troubleshoot-performance-degradation).

### Monitoring blades

Monitoring blades provide an alternative troubleshooting experience to the methods described earlier in the topic. These blades can be used to diagnose 500-series errors.

Confirm that the ASP.NET Core Extensions are installed. If the extensions aren't installed, install them manually:

1. In the **DEVELOPMENT TOOLS** blade section, select the **Extensions** blade.
2. The **ASP.NET Core Extensions** should appear in the list.
3. If the extensions aren't installed, select the **Add** button.
4. Choose the **ASP.NET Core Extensions** from the list.
5. Select **OK** to accept the legal terms.
6. Select **OK** on the **Add extension** blade.
7. An informational pop-up message indicates when the extensions are successfully installed.

If stdout logging isn't enabled, follow these steps:

1. In the Azure portal, select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
3. Open the folders to the path **site** > **wwwroot** and scroll down to reveal the *web.config* file at the bottom of the list.
4. Click the pencil icon next to the *web.config* file.
5. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to: `\\?\%home%\LogFiles\stdout`.
6. Select **Save** to save the updated *web.config* file.

Proceed to activate diagnostic logging:

1. In the Azure portal, select the **Diagnostics logs** blade.
2. Select the **On** switch for **Application Logging (Filesystem)** and **Detailed error messages**. Select the **Save** button at the top of the blade.
3. To include failed request tracing, also known as Failed Request Event Buffering (FREB) logging, select the **On** switch for **Failed request tracing**.
4. Select the **Log stream** blade, which is listed immediately under the **Diagnostics logs** blade in the portal.
5. Make a request to the app.
6. Within the log stream data, the cause of the error is indicated.

Be sure to disable stdout logging when troubleshooting is complete.

To view the failed request tracing logs (FREB logs):

1. Navigate to the **Diagnose and solve problems** blade in the Azure portal.
2. Select **Failed Request Tracing Logs** from the **SUPPORT TOOLS** area of the sidebar.

See [Failed request traces section of the Enable diagnostics logging for web apps in Azure App Service topic](/en-us/azure/app-service/web-sites-enable-diagnostic-log#failed-request-traces) and the [Application performance FAQs for Web Apps in Azure: How do I turn on failed request tracing?](/en-us/azure/app-service/app-service-web-availability-performance-application-issues-faq#how-do-i-turn-on-failed-request-tracing) for more information.

For more information, see [Enable diagnostics logging for web apps in Azure App Service](/en-us/azure/app-service/web-sites-enable-diagnostic-log).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created.

For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

## Troubleshoot on IIS

### Application Event Log (IIS)

Access the Application Event Log:

1. Open the Start menu, search for *Event Viewer*, and select the **Event Viewer** app.
2. In **Event Viewer**, open the **Windows Logs** node.
3. Select **Application** to open the Application Event Log.
4. Search for errors associated with the failing app. Errors have a value of *IIS AspNetCore Module* or *IIS Express AspNetCore Module* in the *Source* column.

### Run the app at a command prompt

Many startup errors don't produce useful information in the Application Event Log. You can find the cause of some errors by running the app at a command prompt on the hosting system.

#### Framework-dependent deployment

If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

1. At a command prompt, navigate to the deployment folder and run the app by executing the app's assembly with *dotnet.exe*. In the following command, substitute the name of the app's assembly for \<assembly\_name>: `dotnet .\<assembly_name>.dll`.
2. The console output from the app, showing any errors, is written to the console window.
3. If the errors occur when making a request to the app, make a request to the host and port where Kestrel listens. Using the default host and post, make a request to `http://localhost:5000/`. If the app responds normally at the Kestrel endpoint address, the problem is more likely related to the hosting configuration and less likely within the app.

#### Self-contained deployment

If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

1. At a command prompt, navigate to the deployment folder and run the app's executable. In the following command, substitute the name of the app's assembly for \<assembly\_name>: `<assembly_name>.exe`.
2. The console output from the app, showing any errors, is written to the console window.
3. If the errors occur when making a request to the app, make a request to the host and port where Kestrel listens. Using the default host and post, make a request to `http://localhost:5000/`. If the app responds normally at the Kestrel endpoint address, the problem is more likely related to the hosting configuration and less likely within the app.

### ASP.NET Core Module stdout log (IIS)

To enable and view stdout logs:

1. Navigate to the site's deployment folder on the hosting system.
2. If the *logs* folder isn't present, create the folder. For instructions on how to enable MSBuild to create the *logs* folder in the deployment automatically, see the [Directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0) topic.
3. Edit the *web.config* file. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to point to the *logs* folder (for example, `.\logs\stdout`). `stdout` in the path is the log file name prefix. A timestamp, process id, and file extension are added automatically when the log is created. Using `stdout` as the file name prefix, a typical log file is named *stdout\_20180205184032\_5412.log*.
4. Ensure your application pool's identity has write permissions to the *logs* folder.
5. Save the updated *web.config* file.
6. Make a request to the app.
7. Navigate to the *logs* folder. Find and open the most recent stdout log.
8. Study the log for errors.

Disable stdout logging when troubleshooting is complete:

1. Edit the *web.config* file.
2. Set **stdoutLogEnabled** to `false`.
3. Save the file.

For more information, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created.

For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

### ASP.NET Core Module debug log (IIS)

Add the following handler settings to the app's *web.config* file to enable ASP.NET Core Module debug log:

```
<aspNetCore ...>
  <handlerSettings>
    <handlerSetting name="debugLevel" value="file" />
    <handlerSetting name="debugFile" value="c:\temp\ancm.log" />
  </handlerSettings>
</aspNetCore>
```

Confirm that the path specified for the log exists and that the app pool's identity has write permissions to the location.

For more information, see [Log creation and redirection with the ASP.NET Core Module](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs).

### Enable the Developer Exception Page

The `ASPNETCORE_ENVIRONMENT` [environment variable can be added to web.config](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#setting-environment-variables) to run the app in the `Development` environment. As long as the environment isn't overridden in app startup by `UseEnvironment` on the host builder, setting the environment variable allows the [Developer Exception Page](../fundamentals/error-handling?view=aspnetcore-10.0) to appear when the app is run.

```
<aspNetCore processPath="dotnet"
      arguments=".\MyApp.dll"
      stdoutLogEnabled="false"
      stdoutLogFile=".\logs\stdout"
      hostingModel="InProcess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
  </environmentVariables>
</aspNetCore>
```

Setting the environment variable for `ASPNETCORE_ENVIRONMENT` is only recommended for use on staging and testing servers that aren't exposed to the Internet. Remove the environment variable from the *web.config* file after troubleshooting. For information on setting environment variables in *web.config*, see [environmentVariables child element of aspNetCore](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#setting-environment-variables).

### Obtain data from an app

If an app is capable of responding to requests, obtain request, connection, and additional data from the app using terminal inline middleware. For more information and sample code, see [Troubleshoot and debug ASP.NET Core projects](troubleshoot?view=aspnetcore-10.0#obtain-data-from-an-app).

### Slow or non-responding app (IIS)

A *crash dump* is a snapshot of the system's memory and can help determine the cause of an app crash, startup failure, or slow app.

#### App crashes or encounters an exception

Obtain and analyze a dump from [Windows Error Reporting (WER)](/en-us/windows/desktop/wer/windows-error-reporting):

1. Create a folder to hold crash dump files at `c:\dumps`. The app pool must have write access to the folder.

2. Run the [EnableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot-azure-iis/scripts/EnableDumps.ps1):

   * If the app uses the [in-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#in-process-hosting-model), run the script for *w3wp.exe*:

     ```
     .\EnableDumps w3wp.exe c:\dumps
     ```

   * If the app uses the [out-of-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model), run the script for *dotnet.exe*:

     ```
     .\EnableDumps dotnet.exe c:\dumps
     ```

3. Run the app under the conditions that cause the crash to occur.

4. After the crash has occurred, run the [DisableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot-azure-iis/scripts/DisableDumps.ps1):

   * If the app uses the [in-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#in-process-hosting-model), run the script for *w3wp.exe*:

     ```
     .\DisableDumps w3wp.exe
     ```

   * If the app uses the [out-of-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model), run the script for *dotnet.exe*:

     ```
     .\DisableDumps dotnet.exe
     ```

After an app crashes and dump collection is complete, the app is allowed to terminate normally. The PowerShell script configures WER to collect up to five dumps per app.

Warning

Crash dumps might take up a large amount of disk space (up to several gigabytes each).

#### App doesn't respond, fails during startup, or runs normally

When an app stops responding but doesn't crash, fails during startup, or runs normally, see [User-Mode Dump Files: Choosing the Best Tool](/en-us/windows-hardware/drivers/debugger/user-mode-dump-files#choosing-the-best-tool) to select an appropriate tool to produce the dump.

#### Analyze the dump

A dump can be analyzed using several approaches. For more information, see [Analyzing a User-Mode Dump File](/en-us/windows-hardware/drivers/debugger/analyzing-a-user-mode-dump-file).

## Clear package caches

A functioning app may fail immediately after upgrading either the .NET SDK on the development machine or changing package versions within the app. In some cases, incoherent packages may break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:

1. Delete the *bin* and *obj* folders.

2. Clear the package caches by executing [dotnet nuget locals all --clear](/en-us/dotnet/core/tools/dotnet-nuget-locals) from a command shell.

   Clearing package caches can also be accomplished with the [nuget.exe](https://www.nuget.org/downloads) tool and executing the command `nuget locals all -clear`. *nuget.exe* isn't a bundled install with the Windows desktop operating system and must be obtained separately from the [NuGet website](https://www.nuget.org/downloads).

3. Restore and rebuild the project.

4. Delete all of the files in the deployment folder on the server prior to redeploying the app.

## Related content

* [Debug .NET and ASP.NET Core source code with Visual Studio](debug-aspnetcore-source?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](troubleshoot?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core](../fundamentals/error-handling?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)

### Azure documentation

* [Application Insights for ASP.NET Core](/en-us/azure/application-insights/app-insights-asp-net-core)
* [Remote debugging web apps section of Troubleshoot a web app in Azure App Service using Visual Studio](/en-us/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio#remotedebug)
* [Azure App Service diagnostics overview](/en-us/azure/app-service/app-service-diagnostics)
* [How to: Monitor Apps in Azure App Service](/en-us/azure/app-service/web-sites-monitor)
* [Troubleshoot a web app in Azure App Service using Visual Studio](/en-us/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio)
* [Troubleshoot HTTP errors of "502 bad gateway" and "503 service unavailable" in your Azure web apps](/en-us/azure/app-service/app-service-web-troubleshoot-http-502-http-503)
* [Troubleshoot slow web app performance issues in Azure App Service](/en-us/azure/app-service/app-service-web-troubleshoot-performance-degradation)
* [Application performance FAQs for Web Apps in Azure](/en-us/azure/app-service/app-service-web-availability-performance-application-issues-faq)
* [Azure Web App sandbox (App Service runtime execution limitations)](https://github.com/projectkudu/kudu/wiki/Azure-Web-App-sandbox)

### Visual Studio documentation

* [Remote Debug ASP.NET Core on IIS in Azure in Visual Studio 2017](/en-us/visualstudio/debugger/remote-debugging-azure)
* [Remote Debug ASP.NET Core on a Remote IIS Computer in Visual Studio 2017](/en-us/visualstudio/debugger/remote-debugging-aspnet-on-a-remote-iis-computer)
* [Learn to debug using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)

### Visual Studio Code documentation

* [Debugging with Visual Studio Code](https://code.visualstudio.com/docs/editor/debugging)

This article provides information on common app startup errors and instructions on how to diagnose errors when an app is deployed to Azure App Service or IIS:

[App startup errors](#app-startup-errors)\
Explains common startup HTTP status code scenarios.

[Troubleshoot on Azure App Service](#troubleshoot-on-azure-app-service)\
Provides troubleshooting advice for apps deployed to Azure App Service.

[Troubleshoot on IIS](#troubleshoot-on-iis)\
Provides troubleshooting advice for apps deployed to IIS or running on IIS Express locally. The guidance applies to both Windows Server and Windows desktop deployments.

[Clear package caches](#clear-package-caches)\
Explains what to do when incoherent packages break an app when performing major upgrades or changing package versions.

[Related content](#related-content)\
Lists additional troubleshooting topics.

## App startup errors

In Visual Studio, an ASP.NET Core project defaults to [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview) hosting during debugging. A *502.5 - Process Failure* or a *500.30 - Start Failure* that occurs when debugging locally can be diagnosed using the advice in this topic.

### 403.14 Forbidden

The app fails to start. The following error is logged:

```
The Web server is configured to not list the contents of this directory.
```

The error is usually caused by a broken deployment on the hosting system, which includes any of the following scenarios:

* The app is deployed to the wrong folder on the hosting system.
* The deployment process failed to move all of the app's files and folders to the deployment folder on the hosting system.
* The *web.config* file is missing from the deployment, or the *web.config* file contents are malformed.

Perform the following steps:

1. Delete all of the files and folders from the deployment folder on the hosting system.

2. Redeploy the contents of the app's *publish* folder to the hosting system using your normal method of deployment, such as Visual Studio, PowerShell, or manual deployment:

   * Confirm that the *web.config* file is present in the deployment and that its contents are correct.
   * When hosting on Azure App Service, confirm that the app is deployed to the `D:\home\site\wwwroot` folder.
   * When the app is hosted by IIS, confirm that the app is deployed to the IIS **Physical path** shown in **IIS Manager**'s **Basic Settings**.

3. Confirm that all of the app's files and folders are deployed by comparing the deployment on the hosting system to the contents of the project's *publish* folder.

For more information on the layout of a published ASP.NET Core app, see [ASP.NET Core directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0). For more information on the *web.config* file, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#configuration-with-webconfig).

### 500 Internal Server Error

The app starts, but an error prevents the server from fulfilling the request.

This error occurs within the app's code during startup or while creating a response. The response may contain no content, or the response may appear as a *500 Internal Server Error* in the browser. The Application Event Log usually states that the app started normally. From the server's perspective, that's correct. The app did start, but it can't generate a valid response. Run the app at a command prompt on the server or enable the ASP.NET Core Module stdout log to troubleshoot the problem.

This error also may occur when the .NET Core Hosting Bundle isn't installed or is corrupted. Installing or repairing the installation of the .NET Core Hosting Bundle (for IIS) or Visual Studio (for IIS Express) may fix the problem.

### 500.0 In-Process Handler Load Failure

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) fails to find the .NET Core CLR and find the in-process request handler (*aspnetcorev2\_inprocess.dll*). Check that:

* The app targets either the [Microsoft.AspNetCore.Server.IIS](https://www.nuget.org/packages/Microsoft.AspNetCore.Server.IIS) NuGet package or the [Microsoft.AspNetCore.App metapackage](../fundamentals/metapackage-app?view=aspnetcore-10.0).
* The version of the ASP.NET Core shared framework that the app targets is installed on the target machine.

### 500.0 Out-Of-Process Handler Load Failure

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) fails to find the out-of-process hosting request handler. Make sure the *aspnetcorev2\_outofprocess.dll* is present in a subfolder next to *aspnetcorev2.dll*.

### 502.5 Process Failure

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) attempts to start the worker process but it fails to start. The cause of a process startup failure can usually be determined from entries in the Application Event Log and the ASP.NET Core Module stdout log.

A common failure condition is the app is misconfigured due to targeting a version of the ASP.NET Core shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are installed on the target machine. The *shared framework* is the set of assemblies (`.dll` files) that are installed on the machine and referenced by a metapackage such as `Microsoft.AspNetCore.App`. The metapackage reference can specify a minimum required version.

The *502.5 Process Failure* error page is returned when a hosting or app misconfiguration causes the worker process to fail:

### Failed to start application (ErrorCode '0x800700c1')

```
EventID: 1010
Source: IIS AspNetCore Module V2
Failed to start application '/LM/W3SVC/6/ROOT/', ErrorCode '0x800700c1'.
```

The app failed to start because the app's assembly (*.dll*) couldn't be loaded.

This error occurs when there's a bitness mismatch between the published app and the w3wp/iisexpress process.

Confirm that the app pool's 32-bit setting is correct:

1. Select the app pool in IIS Manager's **Application Pools**.

2. Select **Advanced Settings** under **Edit Application Pool** in the **Actions** panel.

3. Set **Enable 32-Bit Applications**:

   * If deploying a 32-bit (x86) app, set the value to `True`.
   * If deploying a 64-bit (x64) app, set the value to `False`.

Confirm that there isn't a conflict between a `<Platform>` MSBuild property in the project file and the published bitness of the app.

### Connection reset

If an error occurs after the headers are sent, it's too late for the server to send a **500 Internal Server Error** when an error occurs. This often happens when an error occurs during the serialization of complex objects for a response. This type of error appears as a *connection reset* error on the client. [Application logging](../fundamentals/logging/?view=aspnetcore-10.0) can help troubleshoot these types of errors.

### Default startup limits

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) is configured with a default *startupTimeLimit* of 120 seconds. When left at the default value, an app may take up to two minutes to start before the module logs a process failure. For information on configuring the module, see [Attributes of the aspNetCore element](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#attributes-of-the-aspnetcore-element).

## Troubleshoot on Azure App Service

Important

**ASP.NET Core preview releases with Azure App Service**

ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core preview release, see [Deploy ASP.NET Core preview release to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service).

### Application Event Log (Azure App Service)

To access the Application Event Log, use the **Diagnose and solve problems** blade in the Azure portal:

1. In the Azure portal, open the app in **App Services**.
2. Select **Diagnose and solve problems**.
3. Select the **Diagnostic Tools** heading.
4. Under **Support Tools**, select the **Application Events** button.
5. Examine the latest error provided by the *IIS AspNetCoreModule* or *IIS AspNetCoreModule V2* entry in the **Source** column.

An alternative to using the **Diagnose and solve problems** blade is to examine the Application Event Log file directly using [Kudu](https://github.com/projectkudu/kudu/wiki):

1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
3. Open the **LogFiles** folder.
4. Select the pencil icon next to the `eventlog.xml` file.
5. Examine the log. Scroll to the bottom of the log to see the most recent events.

### Run the app in the Kudu console

Many startup errors don't produce useful information in the Application Event Log. You can run the app in the [Kudu](https://github.com/projectkudu/kudu/wiki) Remote Execution Console to discover the error:

1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.

#### Test a 32-bit (x86) app

**Current release**

1. `cd d:\home\site\wwwroot`

2. Run the app:

   * If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

     ```
     dotnet .\{ASSEMBLY NAME}.dll
     ```

   * If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

     ```
     {ASSEMBLY NAME}.exe
     ```

The console output from the app, showing any errors, is piped to the Kudu console.

**Framework-dependent deployment running on a preview release**

*Requires installing the ASP.NET Core {VERSION} (x86) Runtime site extension.*

1. `cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x32` (`{X.Y}` is the runtime version)
2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

The console output from the app, showing any errors, is piped to the Kudu console.

#### Test a 64-bit (x64) app

**Current release**

* If the app is a 64-bit (x64) [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

  1. `cd D:\Program Files\dotnet`
  2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

* If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

  1. `cd D:\home\site\wwwroot`
  2. Run the app: `{ASSEMBLY NAME}.exe`

The console output from the app, showing any errors, is piped to the Kudu console.

**Framework-dependent deployment running on a preview release**

*Requires installing the ASP.NET Core {VERSION} (x64) Runtime site extension.*

1. `cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x64` (`{X.Y}` is the runtime version)
2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

The console output from the app, showing any errors, is piped to the Kudu console.

### ASP.NET Core Module stdout log (Azure App Service)

The ASP.NET Core Module stdout log often records useful error messages not found in the Application Event Log. To enable and view stdout logs:

1. Navigate to the **Diagnose and solve problems** blade in the Azure portal.
2. Under **SELECT PROBLEM CATEGORY**, select the **Web App Down** button.
3. Under **Suggested Solutions** > **Enable Stdout Log Redirection**, select the button to **Open Kudu Console to edit Web.Config**.
4. In the Kudu **Diagnostic Console**, open the folders to the path **site** > **wwwroot**. Scroll down to reveal the *web.config* file at the bottom of the list.
5. Click the pencil icon next to the *web.config* file.
6. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to: `\\?\%home%\LogFiles\stdout`.
7. Select **Save** to save the updated *web.config* file.
8. Make a request to the app.
9. Return to the Azure portal. Select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
10. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
11. Select the **LogFiles** folder.
12. Inspect the **Modified** column and select the pencil icon to edit the stdout log with the latest modification date.
13. When the log file opens, the error is displayed.

Disable stdout logging when troubleshooting is complete:

1. In the Kudu **Diagnostic Console**, return to the path **site** > **wwwroot** to reveal the *web.config* file. Open the **web.config** file again by selecting the pencil icon.
2. Set **stdoutLogEnabled** to `false`.
3. Select **Save** to save the file.

For more information, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created. Only use stdout logging to troubleshoot app startup problems.

For general logging in an ASP.NET Core app after startup, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

### ASP.NET Core Module debug log (Azure App Service)

The ASP.NET Core Module debug log provides additional, deeper logging from the ASP.NET Core Module. To enable and view stdout logs:

1. To enable the enhanced diagnostic log, perform either of the following:

   * Follow the instructions in [Enhanced diagnostic logs](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs) to configure the app for an enhanced diagnostic logging. Redeploy the app.

   * Add the `<handlerSettings>` shown in [Enhanced diagnostic logs](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs) to the live app's *web.config* file using the Kudu console:

     1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
     2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
     3. Open the folders to the path **site** > **wwwroot**. Edit the *web.config* file by selecting the pencil button. Add the `<handlerSettings>` section as shown in [Enhanced diagnostic logs](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs). Select the **Save** button.

2. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.

3. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.

4. Open the folders to the path **site** > **wwwroot**. If you didn't supply a path for the *aspnetcore-debug.log* file, the file appears in the list. If you supplied a path, navigate to the location of the log file.

5. Open the log file with the pencil button next to the file name.

Disable debug logging when troubleshooting is complete:

To disable the enhanced debug log, perform either of the following:

* Remove the `<handlerSettings>` from the *web.config* file locally and redeploy the app.
* Use the Kudu console to edit the *web.config* file and remove the `<handlerSettings>` section. Save the file.

For more information, see [Log creation and redirection with the ASP.NET Core Module](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs).

Warning

Failure to disable the debug log can lead to app or server failure. There's no limit on log file size. Only use debug logging to troubleshoot app startup problems.

For general logging in an ASP.NET Core app after startup, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

### Slow or non-responding app (Azure App Service)

When an app responds slowly or doesn't respond to a request, see the following articles:

* [Troubleshoot slow web app performance issues in Azure App Service](/en-us/azure/app-service/app-service-web-troubleshoot-performance-degradation)
* [Use Crash Diagnoser Site Extension to Capture Dump for Intermittent Exception issues or performance issues on Azure Web App](/en-us/azure/app-service/troubleshoot-performance-degradation)

### Monitoring blades

Monitoring blades provide an alternative troubleshooting experience to the methods described earlier in the topic. These blades can be used to diagnose 500-series errors.

Confirm that the ASP.NET Core Extensions are installed. If the extensions aren't installed, install them manually:

1. In the **DEVELOPMENT TOOLS** blade section, select the **Extensions** blade.
2. The **ASP.NET Core Extensions** should appear in the list.
3. If the extensions aren't installed, select the **Add** button.
4. Choose the **ASP.NET Core Extensions** from the list.
5. Select **OK** to accept the legal terms.
6. Select **OK** on the **Add extension** blade.
7. An informational pop-up message indicates when the extensions are successfully installed.

If stdout logging isn't enabled, follow these steps:

1. In the Azure portal, select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
3. Open the folders to the path **site** > **wwwroot** and scroll down to reveal the *web.config* file at the bottom of the list.
4. Click the pencil icon next to the *web.config* file.
5. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to: `\\?\%home%\LogFiles\stdout`.
6. Select **Save** to save the updated *web.config* file.

Proceed to activate diagnostic logging:

1. In the Azure portal, select the **Diagnostics logs** blade.
2. Select the **On** switch for **Application Logging (Filesystem)** and **Detailed error messages**. Select the **Save** button at the top of the blade.
3. To include failed request tracing, also known as Failed Request Event Buffering (FREB) logging, select the **On** switch for **Failed request tracing**.
4. Select the **Log stream** blade, which is listed immediately under the **Diagnostics logs** blade in the portal.
5. Make a request to the app.
6. Within the log stream data, the cause of the error is indicated.

Be sure to disable stdout logging when troubleshooting is complete.

To view the failed request tracing logs (FREB logs):

1. Navigate to the **Diagnose and solve problems** blade in the Azure portal.
2. Select **Failed Request Tracing Logs** from the **SUPPORT TOOLS** area of the sidebar.

See [Failed request traces section of the Enable diagnostics logging for web apps in Azure App Service topic](/en-us/azure/app-service/web-sites-enable-diagnostic-log#failed-request-traces) and the [Application performance FAQs for Web Apps in Azure: How do I turn on failed request tracing?](/en-us/azure/app-service/app-service-web-availability-performance-application-issues-faq#how-do-i-turn-on-failed-request-tracing) for more information.

For more information, see [Enable diagnostics logging for web apps in Azure App Service](/en-us/azure/app-service/web-sites-enable-diagnostic-log).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created.

For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

## Troubleshoot on IIS

### Application Event Log (IIS)

Access the Application Event Log:

1. Open the Start menu, search for *Event Viewer*, and select the **Event Viewer** app.
2. In **Event Viewer**, open the **Windows Logs** node.
3. Select **Application** to open the Application Event Log.
4. Search for errors associated with the failing app. Errors have a value of *IIS AspNetCore Module* or *IIS Express AspNetCore Module* in the *Source* column.

### Run the app at a command prompt

Many startup errors don't produce useful information in the Application Event Log. You can find the cause of some errors by running the app at a command prompt on the hosting system.

#### Framework-dependent deployment

If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

1. At a command prompt, navigate to the deployment folder and run the app by executing the app's assembly with *dotnet.exe*. In the following command, substitute the name of the app's assembly for \<assembly\_name>: `dotnet .\<assembly_name>.dll`.
2. The console output from the app, showing any errors, is written to the console window.
3. If the errors occur when making a request to the app, make a request to the host and port where Kestrel listens. Using the default host and post, make a request to `http://localhost:5000/`. If the app responds normally at the Kestrel endpoint address, the problem is more likely related to the hosting configuration and less likely within the app.

#### Self-contained deployment

If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

1. At a command prompt, navigate to the deployment folder and run the app's executable. In the following command, substitute the name of the app's assembly for \<assembly\_name>: `<assembly_name>.exe`.
2. The console output from the app, showing any errors, is written to the console window.
3. If the errors occur when making a request to the app, make a request to the host and port where Kestrel listens. Using the default host and post, make a request to `http://localhost:5000/`. If the app responds normally at the Kestrel endpoint address, the problem is more likely related to the hosting configuration and less likely within the app.

### ASP.NET Core Module stdout log (IIS)

To enable and view stdout logs:

1. Navigate to the site's deployment folder on the hosting system.
2. If the *logs* folder isn't present, create the folder. For instructions on how to enable MSBuild to create the *logs* folder in the deployment automatically, see the [Directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0) topic.
3. Edit the *web.config* file. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to point to the *logs* folder (for example, `.\logs\stdout`). `stdout` in the path is the log file name prefix. A timestamp, process id, and file extension are added automatically when the log is created. Using `stdout` as the file name prefix, a typical log file is named *stdout\_20180205184032\_5412.log*.
4. Ensure your application pool's identity has write permissions to the *logs* folder.
5. Save the updated *web.config* file.
6. Make a request to the app.
7. Navigate to the *logs* folder. Find and open the most recent stdout log.
8. Study the log for errors.

Disable stdout logging when troubleshooting is complete:

1. Edit the *web.config* file.
2. Set **stdoutLogEnabled** to `false`.
3. Save the file.

For more information, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created.

For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

### ASP.NET Core Module debug log (IIS)

Add the following handler settings to the app's *web.config* file to enable ASP.NET Core Module debug log:

```
<aspNetCore ...>
  <handlerSettings>
    <handlerSetting name="debugLevel" value="file" />
    <handlerSetting name="debugFile" value="c:\temp\ancm.log" />
  </handlerSettings>
</aspNetCore>
```

Confirm that the path specified for the log exists and that the app pool's identity has write permissions to the location.

For more information, see [Log creation and redirection with the ASP.NET Core Module](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs).

### Enable the Developer Exception Page

The `ASPNETCORE_ENVIRONMENT` [environment variable can be added to web.config](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#setting-environment-variables) to run the app in the `Development` environment. As long as the environment isn't overridden in app startup by `UseEnvironment` on the host builder, setting the environment variable allows the [Developer Exception Page](../fundamentals/error-handling?view=aspnetcore-10.0) to appear when the app is run.

```
<aspNetCore processPath="dotnet"
      arguments=".\MyApp.dll"
      stdoutLogEnabled="false"
      stdoutLogFile=".\logs\stdout"
      hostingModel="InProcess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
  </environmentVariables>
</aspNetCore>
```

Setting the environment variable for `ASPNETCORE_ENVIRONMENT` is only recommended for use on staging and testing servers that aren't exposed to the Internet. Remove the environment variable from the *web.config* file after troubleshooting. For information on setting environment variables in *web.config*, see [environmentVariables child element of aspNetCore](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#setting-environment-variables).

### Obtain data from an app

If an app is capable of responding to requests, obtain request, connection, and additional data from the app using terminal inline middleware. For more information and sample code, see [Troubleshoot and debug ASP.NET Core projects](troubleshoot?view=aspnetcore-10.0#obtain-data-from-an-app).

### Slow or nonresponding app (IIS)

A *crash dump* is a snapshot of the system's memory and can help determine the cause of an app crash, startup failure, or slow app.

#### App crashes or encounters an exception

Obtain and analyze a dump from [Windows Error Reporting (WER)](/en-us/windows/desktop/wer/windows-error-reporting):

1. Create a folder to hold crash dump files at `c:\dumps`. The app pool must have write access to the folder.

2. Run the [EnableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot-azure-iis/scripts/EnableDumps.ps1):

   * If the app uses the [in-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#in-process-hosting-model), run the script for *w3wp.exe*:

     ```
     .\EnableDumps w3wp.exe c:\dumps
     ```

   * If the app uses the [out-of-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model), run the script for *dotnet.exe*:

     ```
     .\EnableDumps dotnet.exe c:\dumps
     ```

3. Run the app under the conditions that cause the crash to occur.

4. After the crash has occurred, run the [DisableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot-azure-iis/scripts/DisableDumps.ps1):

   * If the app uses the [in-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#in-process-hosting-model), run the script for *w3wp.exe*:

     ```
     .\DisableDumps w3wp.exe
     ```

   * If the app uses the [out-of-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model), run the script for *dotnet.exe*:

     ```
     .\DisableDumps dotnet.exe
     ```

After an app crashes and dump collection is complete, the app is allowed to terminate normally. The PowerShell script configures WER to collect up to five dumps per app.

Warning

Crash dumps might take up a large amount of disk space (up to several gigabytes each).

#### App doesn't respond, fails during startup, or runs normally

When an app stops responding but doesn't crash, fails during startup, or runs normally, see [User-Mode Dump Files: Choosing the Best Tool](/en-us/windows-hardware/drivers/debugger/user-mode-dump-files#choosing-the-best-tool) to select an appropriate tool to produce the dump.

#### Analyze the dump

A dump can be analyzed using several approaches. For more information, see [Analyzing a User-Mode Dump File](/en-us/windows-hardware/drivers/debugger/analyzing-a-user-mode-dump-file).

## Clear package caches

A functioning app may fail immediately after upgrading either the .NET Core SDK on the development machine or changing package versions within the app. In some cases, incoherent packages may break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:

1. Delete the *bin* and *obj* folders.

2. Clear the package caches by executing [dotnet nuget locals all --clear](/en-us/dotnet/core/tools/dotnet-nuget-locals) from a command shell.

   Clearing package caches can also be accomplished with the [nuget.exe](https://www.nuget.org/downloads) tool and executing the command `nuget locals all -clear`. *nuget.exe* isn't a bundled install with the Windows desktop operating system and must be obtained separately from the [NuGet website](https://www.nuget.org/downloads).

3. Restore and rebuild the project.

4. Delete all of the files in the deployment folder on the server prior to redeploying the app.

## Additional resources

* [Troubleshoot and debug ASP.NET Core projects](troubleshoot?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core](../fundamentals/error-handling?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)

### Azure documentation

* [Application Insights for ASP.NET Core](/en-us/azure/application-insights/app-insights-asp-net-core)
* [Remote debugging web apps section of Troubleshoot a web app in Azure App Service using Visual Studio](/en-us/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio#remotedebug)
* [Azure App Service diagnostics overview](/en-us/azure/app-service/app-service-diagnostics)
* [How to: Monitor Apps in Azure App Service](/en-us/azure/app-service/web-sites-monitor)
* [Troubleshoot a web app in Azure App Service using Visual Studio](/en-us/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio)
* [Troubleshoot HTTP errors of "502 bad gateway" and "503 service unavailable" in your Azure web apps](/en-us/azure/app-service/app-service-web-troubleshoot-http-502-http-503)
* [Troubleshoot slow web app performance issues in Azure App Service](/en-us/azure/app-service/app-service-web-troubleshoot-performance-degradation)
* [Application performance FAQs for Web Apps in Azure](/en-us/azure/app-service/app-service-web-availability-performance-application-issues-faq)
* [Azure Web App sandbox (App Service runtime execution limitations)](https://github.com/projectkudu/kudu/wiki/Azure-Web-App-sandbox)

### Visual Studio documentation

* [Remote Debug ASP.NET Core on IIS in Azure in Visual Studio 2017](/en-us/visualstudio/debugger/remote-debugging-azure)
* [Remote Debug ASP.NET Core on a Remote IIS Computer in Visual Studio 2017](/en-us/visualstudio/debugger/remote-debugging-aspnet-on-a-remote-iis-computer)
* [Learn to debug using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)

### Visual Studio Code documentation

* [Debugging with Visual Studio Code](https://code.visualstudio.com/docs/editor/debugging)

This article provides information on common app startup errors and instructions on how to diagnose errors when an app is deployed to Azure App Service or IIS:

[App startup errors](#app-startup-errors)\
Explains common startup HTTP status code scenarios.

[Troubleshoot on Azure App Service](#troubleshoot-on-azure-app-service)\
Provides troubleshooting advice for apps deployed to Azure App Service.

[Troubleshoot on IIS](#troubleshoot-on-iis)\
Provides troubleshooting advice for apps deployed to IIS or running on IIS Express locally. The guidance applies to both Windows Server and Windows desktop deployments.

[Clear package caches](#clear-package-caches)\
Explains what to do when incoherent packages break an app when performing major upgrades or changing package versions.

[Related content](#related-content)\
Lists additional troubleshooting topics.

## App startup errors

In Visual Studio, an ASP.NET Core project defaults to [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview) hosting during debugging. A *502.5 Process Failure* that occurs when debugging locally can be diagnosed using the advice in this topic.

### 403.14 Forbidden

The app fails to start. The following error is logged:

```
The Web server is configured to not list the contents of this directory.
```

The error is usually caused by a broken deployment on the hosting system, which includes any of the following scenarios:

* The app is deployed to the wrong folder on the hosting system.
* The deployment process failed to move all of the app's files and folders to the deployment folder on the hosting system.
* The *web.config* file is missing from the deployment, or the *web.config* file contents are malformed.

Perform the following steps:

1. Delete all of the files and folders from the deployment folder on the hosting system.

2. Redeploy the contents of the app's *publish* folder to the hosting system using your normal method of deployment, such as Visual Studio, PowerShell, or manual deployment:

   * Confirm that the *web.config* file is present in the deployment and that its contents are correct.
   * When hosting on Azure App Service, confirm that the app is deployed to the `D:\home\site\wwwroot` folder.
   * When the app is hosted by IIS, confirm that the app is deployed to the IIS **Physical path** shown in **IIS Manager**'s **Basic Settings**.

3. Confirm that all of the app's files and folders are deployed by comparing the deployment on the hosting system to the contents of the project's *publish* folder.

For more information on the layout of a published ASP.NET Core app, see [ASP.NET Core directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0). For more information on the *web.config* file, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#configuration-with-webconfig).

### 500 Internal Server Error

The app starts, but an error prevents the server from fulfilling the request.

This error occurs within the app's code during startup or while creating a response. The response may contain no content, or the response may appear as a *500 Internal Server Error* in the browser. The Application Event Log usually states that the app started normally. From the server's perspective, that's correct. The app did start, but it can't generate a valid response. Run the app at a command prompt on the server or enable the ASP.NET Core Module stdout log to troubleshoot the problem.

This error also may occur when the .NET Core Hosting Bundle isn't installed or is corrupted. Installing or repairing the installation of the .NET Core Hosting Bundle (for IIS) or Visual Studio (for IIS Express) may fix the problem.

### 502.5 Process Failure

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) attempts to start the worker process but it fails to start. The cause of a process startup failure can usually be determined from entries in the Application Event Log and the ASP.NET Core Module stdout log.

A common failure condition is the app is misconfigured due to targeting a version of the ASP.NET Core shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are installed on the target machine. The *shared framework* is the set of assemblies (`.dll` files) that are installed on the machine and referenced by a metapackage such as `Microsoft.AspNetCore.App`. The metapackage reference can specify a minimum required version.

The *502.5 Process Failure* error page is returned when a hosting or app misconfiguration causes the worker process to fail:

### Failed to start application (ErrorCode '0x800700c1')

```
EventID: 1010
Source: IIS AspNetCore Module V2
Failed to start application '/LM/W3SVC/6/ROOT/', ErrorCode '0x800700c1'.
```

The app failed to start because the app's assembly (*.dll*) couldn't be loaded.

This error occurs when there's a bitness mismatch between the published app and the w3wp/iisexpress process.

Confirm that the app pool's 32-bit setting is correct:

1. Select the app pool in IIS Manager's **Application Pools**.

2. Select **Advanced Settings** under **Edit Application Pool** in the **Actions** panel.

3. Set **Enable 32-Bit Applications**:

   * If deploying a 32-bit (x86) app, set the value to `True`.
   * If deploying a 64-bit (x64) app, set the value to `False`.

Confirm that there isn't a conflict between a `<Platform>` MSBuild property in the project file and the published bitness of the app.

### Connection reset

If an error occurs after the headers are sent, it's too late for the server to send a **500 Internal Server Error** when an error occurs. This often happens when an error occurs during the serialization of complex objects for a response. This type of error appears as a *connection reset* error on the client. [Application logging](../fundamentals/logging/?view=aspnetcore-10.0) can help troubleshoot these types of errors.

### Default startup limits

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) is configured with a default *startupTimeLimit* of 120 seconds. When left at the default value, an app may take up to two minutes to start before the module logs a process failure. For information on configuring the module, see [Attributes of the aspNetCore element](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#attributes-of-the-aspnetcore-element).

## Troubleshoot on Azure App Service

Important

**ASP.NET Core preview releases with Azure App Service**

ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core preview release, see [Deploy ASP.NET Core preview release to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service).

### Application Event Log (Azure App Service)

To access the Application Event Log, use the **Diagnose and solve problems** blade in the Azure portal:

1. In the Azure portal, open the app in **App Services**.
2. Select **Diagnose and solve problems**.
3. Select the **Diagnostic Tools** heading.
4. Under **Support Tools**, select the **Application Events** button.
5. Examine the latest error provided by the *IIS AspNetCoreModule* or *IIS AspNetCoreModule V2* entry in the **Source** column.

An alternative to using the **Diagnose and solve problems** blade is to examine the Application Event Log file directly using [Kudu](https://github.com/projectkudu/kudu/wiki):

1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
3. Open the **LogFiles** folder.
4. Select the pencil icon next to the `eventlog.xml` file.
5. Examine the log. Scroll to the bottom of the log to see the most recent events.

### Run the app in the Kudu console

Many startup errors don't produce useful information in the Application Event Log. You can run the app in the [Kudu](https://github.com/projectkudu/kudu/wiki) Remote Execution Console to discover the error:

1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.

#### Test a 32-bit (x86) app

**Current release**

1. `cd d:\home\site\wwwroot`

2. Run the app:

   * If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

     ```
     dotnet .\{ASSEMBLY NAME}.dll
     ```

   * If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

     ```
     {ASSEMBLY NAME}.exe
     ```

The console output from the app, showing any errors, is piped to the Kudu console.

**Framework-dependent deployment running on a preview release**

*Requires installing the ASP.NET Core {VERSION} (x86) Runtime site extension.*

1. `cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x32` (`{X.Y}` is the runtime version)
2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

The console output from the app, showing any errors, is piped to the Kudu console.

#### Test a 64-bit (x64) app

**Current release**

* If the app is a 64-bit (x64) [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

  1. `cd D:\Program Files\dotnet`
  2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

* If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

  1. `cd D:\home\site\wwwroot`
  2. Run the app: `{ASSEMBLY NAME}.exe`

The console output from the app, showing any errors, is piped to the Kudu console.

**Framework-dependent deployment running on a preview release**

*Requires installing the ASP.NET Core {VERSION} (x64) Runtime site extension.*

1. `cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x64` (`{X.Y}` is the runtime version)
2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

The console output from the app, showing any errors, is piped to the Kudu console.

### ASP.NET Core Module stdout log (Azure App Service)

The ASP.NET Core Module stdout log often records useful error messages not found in the Application Event Log. To enable and view stdout logs:

1. Navigate to the **Diagnose and solve problems** blade in the Azure portal.
2. Under **SELECT PROBLEM CATEGORY**, select the **Web App Down** button.
3. Under **Suggested Solutions** > **Enable Stdout Log Redirection**, select the button to **Open Kudu Console to edit Web.Config**.
4. In the Kudu **Diagnostic Console**, open the folders to the path **site** > **wwwroot**. Scroll down to reveal the *web.config* file at the bottom of the list.
5. Click the pencil icon next to the *web.config* file.
6. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to: `\\?\%home%\LogFiles\stdout`.
7. Select **Save** to save the updated *web.config* file.
8. Make a request to the app.
9. Return to the Azure portal. Select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
10. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
11. Select the **LogFiles** folder.
12. Inspect the **Modified** column and select the pencil icon to edit the stdout log with the latest modification date.
13. When the log file opens, the error is displayed.

Disable stdout logging when troubleshooting is complete:

1. In the Kudu **Diagnostic Console**, return to the path **site** > **wwwroot** to reveal the *web.config* file. Open the **web.config** file again by selecting the pencil icon.
2. Set **stdoutLogEnabled** to `false`.
3. Select **Save** to save the file.

For more information, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created. Only use stdout logging to troubleshoot app startup problems.

For general logging in an ASP.NET Core app after startup, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

### Slow or nonresponding app (Azure App Service)

When an app responds slowly or doesn't respond to a request, see the following articles:

* [Troubleshoot slow web app performance issues in Azure App Service](/en-us/azure/app-service/app-service-web-troubleshoot-performance-degradation)
* [Use Crash Diagnoser Site Extension to Capture Dump for Intermittent Exception issues or performance issues on Azure Web App](/en-us/azure/app-service/troubleshoot-performance-degradation/)

### Monitoring blades

Monitoring blades provide an alternative troubleshooting experience to the methods described earlier in the topic. These blades can be used to diagnose 500-series errors.

Confirm that the ASP.NET Core Extensions are installed. If the extensions aren't installed, install them manually:

1. In the **DEVELOPMENT TOOLS** blade section, select the **Extensions** blade.
2. The **ASP.NET Core Extensions** should appear in the list.
3. If the extensions aren't installed, select the **Add** button.
4. Choose the **ASP.NET Core Extensions** from the list.
5. Select **OK** to accept the legal terms.
6. Select **OK** on the **Add extension** blade.
7. An informational pop-up message indicates when the extensions are successfully installed.

If stdout logging isn't enabled, follow these steps:

1. In the Azure portal, select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
3. Open the folders to the path **site** > **wwwroot** and scroll down to reveal the *web.config* file at the bottom of the list.
4. Click the pencil icon next to the *web.config* file.
5. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to: `\\?\%home%\LogFiles\stdout`.
6. Select **Save** to save the updated *web.config* file.

Proceed to activate diagnostic logging:

1. In the Azure portal, select the **Diagnostics logs** blade.
2. Select the **On** switch for **Application Logging (Filesystem)** and **Detailed error messages**. Select the **Save** button at the top of the blade.
3. To include failed request tracing, also known as Failed Request Event Buffering (FREB) logging, select the **On** switch for **Failed request tracing**.
4. Select the **Log stream** blade, which is listed immediately under the **Diagnostics logs** blade in the portal.
5. Make a request to the app.
6. Within the log stream data, the cause of the error is indicated.

Be sure to disable stdout logging when troubleshooting is complete.

To view the failed request tracing logs (FREB logs):

1. Navigate to the **Diagnose and solve problems** blade in the Azure portal.
2. Select **Failed Request Tracing Logs** from the **SUPPORT TOOLS** area of the sidebar.

See [Failed request traces section of the Enable diagnostics logging for web apps in Azure App Service topic](/en-us/azure/app-service/web-sites-enable-diagnostic-log#failed-request-traces) and the [Application performance FAQs for Web Apps in Azure: How do I turn on failed request tracing?](/en-us/azure/app-service/app-service-web-availability-performance-application-issues-faq#how-do-i-turn-on-failed-request-tracing) for more information.

For more information, see [Enable diagnostics logging for web apps in Azure App Service](/en-us/azure/app-service/web-sites-enable-diagnostic-log).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created.

For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

## Troubleshoot on IIS

### Application Event Log (IIS)

Access the Application Event Log:

1. Open the Start menu, search for *Event Viewer*, and select the **Event Viewer** app.
2. In **Event Viewer**, open the **Windows Logs** node.
3. Select **Application** to open the Application Event Log.
4. Search for errors associated with the failing app. Errors have a value of *IIS AspNetCore Module* or *IIS Express AspNetCore Module* in the *Source* column.

### Run the app at a command prompt

Many startup errors don't produce useful information in the Application Event Log. You can find the cause of some errors by running the app at a command prompt on the hosting system.

#### Framework-dependent deployment

If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

1. At a command prompt, navigate to the deployment folder and run the app by executing the app's assembly with *dotnet.exe*. In the following command, substitute the name of the app's assembly for \<assembly\_name>: `dotnet .\<assembly_name>.dll`.
2. The console output from the app, showing any errors, is written to the console window.
3. If the errors occur when making a request to the app, make a request to the host and port where Kestrel listens. Using the default host and post, make a request to `http://localhost:5000/`. If the app responds normally at the Kestrel endpoint address, the problem is more likely related to the hosting configuration and less likely within the app.

#### Self-contained deployment

If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

1. At a command prompt, navigate to the deployment folder and run the app's executable. In the following command, substitute the name of the app's assembly for \<assembly\_name>: `<assembly_name>.exe`.
2. The console output from the app, showing any errors, is written to the console window.
3. If the errors occur when making a request to the app, make a request to the host and port where Kestrel listens. Using the default host and post, make a request to `http://localhost:5000/`. If the app responds normally at the Kestrel endpoint address, the problem is more likely related to the hosting configuration and less likely within the app.

### ASP.NET Core Module stdout log (IIS)

To enable and view stdout logs:

1. Navigate to the site's deployment folder on the hosting system.
2. If the *logs* folder isn't present, create the folder. For instructions on how to enable MSBuild to create the *logs* folder in the deployment automatically, see the [Directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0) topic.
3. Edit the *web.config* file. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to point to the *logs* folder (for example, `.\logs\stdout`). `stdout` in the path is the log file name prefix. A timestamp, process id, and file extension are added automatically when the log is created. Using `stdout` as the file name prefix, a typical log file is named *stdout\_20180205184032\_5412.log*.
4. Ensure your application pool's identity has write permissions to the *logs* folder.
5. Save the updated *web.config* file.
6. Make a request to the app.
7. Navigate to the *logs* folder. Find and open the most recent stdout log.
8. Study the log for errors.

Disable stdout logging when troubleshooting is complete:

1. Edit the *web.config* file.
2. Set **stdoutLogEnabled** to `false`.
3. Save the file.

For more information, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created.

For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

### Enable the Developer Exception Page

The `ASPNETCORE_ENVIRONMENT` [environment variable can be added to web.config](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#setting-environment-variables) to run the app in the `Development` environment. As long as the environment isn't overridden in app startup by `UseEnvironment` on the host builder, setting the environment variable allows the [Developer Exception Page](../fundamentals/error-handling?view=aspnetcore-10.0) to appear when the app is run.

```
<aspNetCore processPath="dotnet"
      arguments=".\MyApp.dll"
      stdoutLogEnabled="false"
      stdoutLogFile=".\logs\stdout">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
  </environmentVariables>
</aspNetCore>
```

Setting the environment variable for `ASPNETCORE_ENVIRONMENT` is only recommended for use on staging and testing servers that aren't exposed to the Internet. Remove the environment variable from the *web.config* file after troubleshooting. For information on setting environment variables in *web.config*, see [environmentVariables child element of aspNetCore](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#setting-environment-variables).

### Obtain data from an app

If an app is capable of responding to requests, obtain request, connection, and additional data from the app using terminal inline middleware. For more information and sample code, see [Troubleshoot and debug ASP.NET Core projects](troubleshoot?view=aspnetcore-10.0#obtain-data-from-an-app).

### Slow or nonresponding app (IIS)

A *crash dump* is a snapshot of the system's memory and can help determine the cause of an app crash, startup failure, or slow app.

#### App crashes or encounters an exception

Obtain and analyze a dump from [Windows Error Reporting (WER)](/en-us/windows/desktop/wer/windows-error-reporting):

1. Create a folder to hold crash dump files at `c:\dumps`. The app pool must have write access to the folder.

2. Run the [EnableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot-azure-iis/scripts/EnableDumps.ps1):

   * If the app uses the [in-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#in-process-hosting-model), run the script for *w3wp.exe*:

     ```
     .\EnableDumps w3wp.exe c:\dumps
     ```

   * If the app uses the [out-of-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model), run the script for *dotnet.exe*:

     ```
     .\EnableDumps dotnet.exe c:\dumps
     ```

3. Run the app under the conditions that cause the crash to occur.

4. After the crash has occurred, run the [DisableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot-azure-iis/scripts/DisableDumps.ps1):

   * If the app uses the [in-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#in-process-hosting-model), run the script for *w3wp.exe*:

     ```
     .\DisableDumps w3wp.exe
     ```

   * If the app uses the [out-of-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model), run the script for *dotnet.exe*:

     ```
     .\DisableDumps dotnet.exe
     ```

After an app crashes and dump collection is complete, the app is allowed to terminate normally. The PowerShell script configures WER to collect up to five dumps per app.

Warning

Crash dumps might take up a large amount of disk space (up to several gigabytes each).

#### App doesn't respond, fails during startup, or runs normally

When an app stops responding but doesn't crash, fails during startup, or runs normally, see [User-Mode Dump Files: Choosing the Best Tool](/en-us/windows-hardware/drivers/debugger/user-mode-dump-files#choosing-the-best-tool) to select an appropriate tool to produce the dump.

#### Analyze the dump

A dump can be analyzed using several approaches. For more information, see [Analyzing a User-Mode Dump File](/en-us/windows-hardware/drivers/debugger/analyzing-a-user-mode-dump-file).

## Clear package caches

A functioning app may fail immediately after upgrading either the .NET Core SDK on the development machine or changing package versions within the app. In some cases, incoherent packages may break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:

1. Delete the *bin* and *obj* folders.

2. Clear the package caches by executing [dotnet nuget locals all --clear](/en-us/dotnet/core/tools/dotnet-nuget-locals) from a command shell.

   Clearing package caches can also be accomplished with the [nuget.exe](https://www.nuget.org/downloads) tool and executing the command `nuget locals all -clear`. *nuget.exe* isn't a bundled install with the Windows desktop operating system and must be obtained separately from the [NuGet website](https://www.nuget.org/downloads).

3. Restore and rebuild the project.

4. Delete all of the files in the deployment folder on the server prior to redeploying the app.

## Additional resources

* [Troubleshoot and debug ASP.NET Core projects](troubleshoot?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core](../fundamentals/error-handling?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)

### Azure documentation

* [Application Insights for ASP.NET Core](/en-us/azure/application-insights/app-insights-asp-net-core)
* [Remote debugging web apps section of Troubleshoot a web app in Azure App Service using Visual Studio](/en-us/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio#remotedebug)
* [Azure App Service diagnostics overview](/en-us/azure/app-service/app-service-diagnostics)
* [How to: Monitor Apps in Azure App Service](/en-us/azure/app-service/web-sites-monitor)
* [Troubleshoot a web app in Azure App Service using Visual Studio](/en-us/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio)
* [Troubleshoot HTTP errors of "502 bad gateway" and "503 service unavailable" in your Azure web apps](/en-us/azure/app-service/app-service-web-troubleshoot-http-502-http-503)
* [Troubleshoot slow web app performance issues in Azure App Service](/en-us/azure/app-service/app-service-web-troubleshoot-performance-degradation)
* [Application performance FAQs for Web Apps in Azure](/en-us/azure/app-service/app-service-web-availability-performance-application-issues-faq)
* [Azure Web App sandbox (App Service runtime execution limitations)](https://github.com/projectkudu/kudu/wiki/Azure-Web-App-sandbox)

### Visual Studio documentation

* [Remote Debug ASP.NET Core on IIS in Azure in Visual Studio 2017](/en-us/visualstudio/debugger/remote-debugging-azure)
* [Remote Debug ASP.NET Core on a Remote IIS Computer in Visual Studio 2017](/en-us/visualstudio/debugger/remote-debugging-aspnet-on-a-remote-iis-computer)
* [Learn to debug using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)

### Visual Studio Code documentation

* [Debugging with Visual Studio Code](https://code.visualstudio.com/docs/editor/debugging)

This article provides information on common app startup errors and instructions on how to diagnose errors when an app is deployed to Azure App Service or IIS:

[App startup errors](#app-startup-errors)\
Explains common startup HTTP status code scenarios.

[Troubleshoot on Azure App Service](#troubleshoot-on-azure-app-service)\
Provides troubleshooting advice for apps deployed to Azure App Service.

[Troubleshoot on IIS](#troubleshoot-on-iis)\
Provides troubleshooting advice for apps deployed to IIS or running on IIS Express locally. The guidance applies to both Windows Server and Windows desktop deployments.

[Clear package caches](#clear-package-caches)\
Explains what to do when incoherent packages break an app when performing major upgrades or changing package versions.

[Related content](#related-content)\
Lists additional troubleshooting topics.

## App startup errors

In Visual Studio, the ASP.NET Core project default server is Kestrel. Visual studio can be configured to use [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview). A *502.5 - Process Failure* or a *500.30 - Start Failure* that occurs when debugging locally with IIS Express can be diagnosed using the advice in this topic.

### 403.14 Forbidden

The app fails to start. The following error is logged:

```
The Web server is configured to not list the contents of this directory.
```

The error is usually caused by a broken deployment on the hosting system, which includes any of the following scenarios:

* The app is deployed to the wrong folder on the hosting system.
* The deployment process failed to move all of the app's files and folders to the deployment folder on the hosting system.
* The *web.config* file is missing from the deployment, or the *web.config* file contents are malformed.

Perform the following steps:

1. Delete all of the files and folders from the deployment folder on the hosting system.

2. Redeploy the contents of the app's *publish* folder to the hosting system using your normal method of deployment, such as Visual Studio, PowerShell, or manual deployment:

   * Confirm that the *web.config* file is present in the deployment and that its contents are correct.
   * When hosting on Azure App Service, confirm that the app is deployed to the `D:\home\site\wwwroot` folder.
   * When the app is hosted by IIS, confirm that the app is deployed to the IIS **Physical path** shown in **IIS Manager**'s **Basic Settings**.

3. Confirm that all of the app's files and folders are deployed by comparing the deployment on the hosting system to the contents of the project's *publish* folder.

For more information on the layout of a published ASP.NET Core app, see [ASP.NET Core directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0). For more information on the *web.config* file, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#configuration-with-webconfig).

### 500 Internal Server Error

The app starts, but an error prevents the server from fulfilling the request.

This error occurs within the app's code during startup or while creating a response. The response may contain no content, or the response may appear as a *500 Internal Server Error* in the browser. The Application Event Log usually states that the app started normally. From the server's perspective, that's correct. The app did start, but it can't generate a valid response. Run the app at a command prompt on the server or enable the ASP.NET Core Module stdout log to troubleshoot the problem.

This error also may occur when the .NET Hosting Bundle isn't installed or is corrupted. Installing or repairing the installation of the .NET Hosting Bundle (for IIS) or Visual Studio (for IIS Express) may fix the problem.

### 500.0 In-Process Handler Load Failure

The worker process fails. The app doesn't start.

An unknown error occurred loading [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) components. Take one of the following actions:

* Contact [Microsoft Support](https://support.microsoft.com/oas/default.aspx?prid=15832) (select **Developer Tools** then **ASP.NET Core**).
* Ask a question on Stack Overflow.
* File an issue on our [GitHub repository](https://github.com/dotnet/AspNetCore).

### 500.30 In-Process Startup Failure

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) attempts to start the .NET CLR in-process, but it fails to start. The cause of a process startup failure can usually be determined from entries in the Application Event Log and the ASP.NET Core Module stdout log.

Common failure conditions:

* The app is misconfigured due to targeting a version of the ASP.NET Core shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are installed on the target machine.
* Using Azure Key Vault, lack of permissions to the Key Vault. Check the access policies in the targeted Key Vault to ensure that the correct permissions are granted.

### 500.31 ANCM Failed to Find Native Dependencies

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) attempts to start the .NET runtime in-process, but it fails to start. The most common cause of this startup failure is when the `Microsoft.NETCore.App` or `Microsoft.AspNetCore.App` runtime isn't installed. If the app is deployed to target ASP.NET Core 3.0 and that version doesn't exist on the machine, this error occurs. An example error message follows:

```
The specified framework 'Microsoft.NETCore.App', version '3.0.0' was not found.
  - The following frameworks were found:
      2.2.1 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
      3.0.0-preview5-27626-15 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
      3.0.0-preview6-27713-13 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
      3.0.0-preview6-27714-15 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
      3.0.0-preview6-27723-08 at [C:\Program Files\dotnet\x64\shared\Microsoft.NETCore.App]
```

The error message lists all the installed .NET versions and the version requested by the app. To fix this error, either:

* Install the appropriate version of .NET on the machine.
* Change the app to target a version of .NET that's present on the machine.
* Publish the app as a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).

When running in development (the `ASPNETCORE_ENVIRONMENT` environment variable is set to `Development`), the specific error is written to the HTTP response. The cause of a process startup failure is also found in the Application Event Log.

### 500.32 ANCM Failed to Load dll

The worker process fails. The app doesn't start.

The most common cause for this error is that the app is published for an incompatible processor architecture. If the worker process is running as a 32-bit app and the app was published to target 64-bit, this error occurs.

To fix this error, either:

* Republish the app for the same processor architecture as the worker process.
* Publish the app as a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-executables-fde).

### 500.33 ANCM Request Handler Load Failure

The worker process fails. The app doesn't start.

The app didn't reference the `Microsoft.AspNetCore.App` framework. Only apps targeting the `Microsoft.AspNetCore.App` framework can be hosted by the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0).

To fix this error, confirm that the app is targeting the `Microsoft.AspNetCore.App` framework. Check the `.runtimeconfig.json` to verify the framework targeted by the app.

### 500.34 ANCM Mixed Hosting Models Not Supported

The worker process can't run both an in-process app and an out-of-process app in the same process.

To fix this error, run apps in separate IIS application pools.

### 500.35 ANCM Multiple In-Process Applications in same Process

The worker process can't run multiple in-process apps in the same process.

To fix this error, run apps in separate IIS application pools.

### 500.36 ANCM Out-Of-Process Handler Load Failure

The out-of-process request handler, *aspnetcorev2\_outofprocess.dll*, isn't next to the *aspnetcorev2.dll* file. This indicates a corrupted installation of the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0).

To fix this error, repair the installation of the [.NET Hosting Bundle](../host-and-deploy/iis/?view=aspnetcore-10.0#install-the-net-hosting-bundle) (for IIS) or Visual Studio (for IIS Express).

### 500.37 ANCM Failed to Start Within Startup Time Limit

ANCM failed to start within the provided startup time limit. By default, the timeout is 120 seconds.

This error can occur when starting a large number of apps on the same machine. Check for CPU/Memory usage spikes on the server during startup. You may need to stagger the startup process of multiple apps.

### 500.38 ANCM Application DLL Not Found

ANCM failed to locate the application DLL, which should be next to the executable.

This error occurs when hosting an app packaged as a [single-file executable](/en-us/dotnet/core/whats-new/dotnet-core-3-0#single-file-executables) using the in-process hosting model. The in-process model requires that the ANCM load the .NET app into the existing IIS process. This scenario isn't supported by the single-file deployment model. Use **one** of the following approaches in the app's project file to fix this error:

1. Disable single-file publishing by setting the `PublishSingleFile` MSBuild property to `false`.
2. Switch to the out-of-process hosting model by setting the `AspNetCoreHostingModel` MSBuild property to `OutOfProcess`.

### 502.5 Process Failure

The worker process fails. The app doesn't start.

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) attempts to start the worker process but it fails to start. The cause of a process startup failure can usually be determined from entries in the Application Event Log and the ASP.NET Core Module stdout log.

A common failure condition is the app is misconfigured due to targeting a version of the ASP.NET Core shared framework that isn't present. Check which versions of the ASP.NET Core shared framework are installed on the target machine. The *shared framework* is the set of assemblies (`.dll` files) that are installed on the machine and referenced by a metapackage such as `Microsoft.AspNetCore.App`. The metapackage reference can specify a minimum required version.

The *502.5 Process Failure* error page is returned when a hosting or app misconfiguration causes the worker process to fail:

### Failed to start application (ErrorCode '0x800700c1')

```
EventID: 1010
Source: IIS AspNetCore Module V2
Failed to start application '/LM/W3SVC/6/ROOT/', ErrorCode '0x800700c1'.
```

The app failed to start because the app's assembly (*.dll*) couldn't be loaded.

This error occurs when there's a bitness mismatch between the published app and the w3wp/iisexpress process.

Confirm that the app pool's 32-bit setting is correct:

1. Select the app pool in IIS Manager's **Application Pools**.

2. Select **Advanced Settings** under **Edit Application Pool** in the **Actions** panel.

3. Set **Enable 32-Bit Applications**:

   * If deploying a 32-bit (x86) app, set the value to `True`.
   * If deploying a 64-bit (x64) app, set the value to `False`.

Confirm that there isn't a conflict between a `<Platform>` MSBuild property in the project file and the published bitness of the app.

### Failed to start application (ErrorCode '0x800701b1')

```
EventID: 1010
Source: IIS AspNetCore Module V2
Failed to start application '/LM/W3SVC/3/ROOT', ErrorCode '0x800701b1'.
```

The app failed to start because a Windows Service failed to load.

One common service that needs to be enabled is the "null" service. The following command enables the `null` Windows Service:

```
sc.exe start null
```

### Connection reset

If an error occurs after the headers are sent, it's too late for the server to send a **500 Internal Server Error** when an error occurs. This often happens when an error occurs during the serialization of complex objects for a response. This type of error appears as a *connection reset* error on the client. [Application logging](../fundamentals/logging/?view=aspnetcore-10.0) can help troubleshoot these types of errors.

### Default startup limits

The [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) is configured with a default *startupTimeLimit* of 120 seconds. When left at the default value, an app may take up to two minutes to start before the module logs a process failure. For information on configuring the module, see [Attributes of the aspNetCore element](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#attributes-of-the-aspnetcore-element).

## Troubleshoot on Azure App Service

Important

**ASP.NET Core preview releases with Azure App Service**

ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core preview release, see [Deploy ASP.NET Core preview release to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service).

### Azure App Services Log stream

The Azure App Services Log streams logging information as it occurs. To view streaming logs:

1. In the Azure portal, open the app in **App Services**.
2. In the left pane, navigate to **Monitoring** > **App Service Logs**.
3. Select **File System** for **Web Server Logging**. Optionally enable **Application logging**.
4. In the left pane, navigate to **Monitoring** > **Log stream**, and then select **Application logs** or **Web Server Logs**.

The following images shows the application logs output:

Streaming logs have some latency and might not display immediately.

### Application Event Log (Azure App Service)

To access the Application Event Log, use the **Diagnose and solve problems** blade in the Azure portal:

1. In the Azure portal, open the app in **App Services**.
2. Select **Diagnose and solve problems**.
3. Select the **Diagnostic Tools** heading.
4. Under **Support Tools**, select the **Application Events** button.
5. Examine the latest error provided by the *IIS AspNetCoreModule* or *IIS AspNetCoreModule V2* entry in the **Source** column.

An alternative to using the **Diagnose and solve problems** blade is to examine the Application Event Log file directly using [Kudu](https://github.com/projectkudu/kudu/wiki):

1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
3. Open the **LogFiles** folder.
4. Select the pencil icon next to the `eventlog.xml` file.
5. Examine the log. Scroll to the bottom of the log to see the most recent events.

### Run the app in the Kudu console

Many startup errors don't produce useful information in the Application Event Log. You can run the app in the [Kudu](https://github.com/projectkudu/kudu/wiki) Remote Execution Console to discover the error:

1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.

#### Test a 32-bit (x86) app

**Current release**

1. `cd d:\home\site\wwwroot`

2. Run the app:

   * If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

     ```
     dotnet .\{ASSEMBLY NAME}.dll
     ```

   * If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

     ```
     {ASSEMBLY NAME}.exe
     ```

The console output from the app, showing any errors, is piped to the Kudu console.

**Framework-dependent deployment running on a preview release**

*Requires installing the ASP.NET Core {VERSION} (x86) Runtime site extension.*

1. `cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x32` (`{X.Y}` is the runtime version)
2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

The console output from the app, showing any errors, is piped to the Kudu console.

#### Test a 64-bit (x64) app

**Current release**

* If the app is a 64-bit (x64) [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

  1. `cd D:\Program Files\dotnet`
  2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

* If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

  1. `cd D:\home\site\wwwroot`
  2. Run the app: `{ASSEMBLY NAME}.exe`

The console output from the app, showing any errors, is piped to the Kudu console.

**Framework-dependent deployment running on a preview release**

*Requires installing the ASP.NET Core {VERSION} (x64) Runtime site extension.*

1. `cd D:\home\SiteExtensions\AspNetCoreRuntime.{X.Y}.x64` (`{X.Y}` is the runtime version)
2. Run the app: `dotnet \home\site\wwwroot\{ASSEMBLY NAME}.dll`

The console output from the app, showing any errors, is piped to the Kudu console.

### ASP.NET Core Module stdout log (Azure App Service)

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created. Only use stdout logging to troubleshoot app startup problems.

For general logging in an ASP.NET Core app after startup, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

The ASP.NET Core Module stdout log often records useful error messages not found in the Application Event Log. To enable and view stdout logs:

1. In the Azure portal, navigate to the web app.
2. In the **App Service** blade, enter **kudu** in the search box.
3. Select **Advanced Tools** > **Go**.
4. Select **Debug console > CMD**.
5. Navigate to *site/wwwroot*
6. Select the pencil icon to edit the *web.config* file.
7. In the `<aspNetCore />` element, set `stdoutLogEnabled="true"` and select **Save**.

Disable stdout logging when troubleshooting is complete by setting `stdoutLogEnabled="false"`.

For more information, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

[]()

### ASP.NET Core Module debug log (Azure App Service)

The ASP.NET Core Module debug log provides additional, deeper logging from the ASP.NET Core Module. To enable and view stdout logs:

1. To enable the enhanced diagnostic log, perform either of the following:

   * Follow the instructions in [Enhanced diagnostic logs](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs) to configure the app for an enhanced diagnostic logging. Redeploy the app.

   * Add the `<handlerSettings>` shown in [Enhanced diagnostic logs](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs) to the live app's *web.config* file using the Kudu console:

     1. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
     2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
     3. Open the folders to the path **site** > **wwwroot**. Edit the *web.config* file by selecting the pencil button. Add the `<handlerSettings>` section as shown in [Enhanced diagnostic logs](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs). Select the **Save** button.

2. Open **Advanced Tools** in the **Development Tools** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.

3. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.

4. Open the folders to the path **site** > **wwwroot**. If you didn't supply a path for the *aspnetcore-debug.log* file, the file appears in the list. If you supplied a path, navigate to the location of the log file.

5. Open the log file with the pencil button next to the file name.

Disable debug logging when troubleshooting is complete:

To disable the enhanced debug log, perform either of the following:

* Remove the `<handlerSettings>` from the *web.config* file locally and redeploy the app.
* Use the Kudu console to edit the *web.config* file and remove the `<handlerSettings>` section. Save the file.

For more information, see [Log creation and redirection with the ASP.NET Core Module](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs).

Warning

Failure to disable the debug log can lead to app or server failure. There's no limit on log file size. Only use debug logging to troubleshoot app startup problems.

For general logging in an ASP.NET Core app after startup, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

### Slow or nonresponding app (Azure App Service)

When an app responds slowly or doesn't respond to a request, see [Troubleshoot slow web app performance issues in Azure App Service](/en-us/azure/app-service/app-service-web-troubleshoot-performance-degradation).

### Monitoring blades

Monitoring blades provide an alternative troubleshooting experience to the methods described earlier in the topic. These blades can be used to diagnose 500-series errors.

Confirm that the ASP.NET Core Extensions are installed. If the extensions aren't installed, install them manually:

1. In the **DEVELOPMENT TOOLS** blade section, select the **Extensions** blade.
2. The **ASP.NET Core Extensions** should appear in the list.
3. If the extensions aren't installed, select the **Add** button.
4. Choose the **ASP.NET Core Extensions** from the list.
5. Select **OK** to accept the legal terms.
6. Select **OK** on the **Add extension** blade.
7. An informational pop-up message indicates when the extensions are successfully installed.

If stdout logging isn't enabled, follow these steps:

1. In the Azure portal, select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window.
2. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**.
3. Open the folders to the path **site** > **wwwroot** and scroll down to reveal the *web.config* file at the bottom of the list.
4. Click the pencil icon next to the *web.config* file.
5. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to: `\\?\%home%\LogFiles\stdout`.
6. Select **Save** to save the updated *web.config* file.

Proceed to activate diagnostic logging:

1. In the Azure portal, select the **Diagnostics logs** blade.
2. Select the **On** switch for **Application Logging (Filesystem)** and **Detailed error messages**. Select the **Save** button at the top of the blade.
3. To include failed request tracing, also known as Failed Request Event Buffering (FREB) logging, select the **On** switch for **Failed request tracing**.
4. Select the **Log stream** blade, which is listed immediately under the **Diagnostics logs** blade in the portal.
5. Make a request to the app.
6. Within the log stream data, the cause of the error is indicated.

Be sure to disable stdout logging when troubleshooting is complete.

To view the failed request tracing logs (FREB logs):

1. Navigate to the **Diagnose and solve problems** blade in the Azure portal.
2. Select **Failed Request Tracing Logs** from the **SUPPORT TOOLS** area of the sidebar.

See [Failed request traces section of the Enable diagnostics logging for web apps in Azure App Service topic](/en-us/azure/app-service/web-sites-enable-diagnostic-log#failed-request-traces) and the [Application performance FAQs for Web Apps in Azure: How do I turn on failed request tracing?](/en-us/azure/app-service/app-service-web-availability-performance-application-issues-faq#how-do-i-turn-on-failed-request-tracing) for more information.

For more information, see [Enable diagnostics logging for web apps in Azure App Service](/en-us/azure/app-service/web-sites-enable-diagnostic-log).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created.

For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

## Troubleshoot on IIS

### Application Event Log (IIS)

Access the Application Event Log:

1. Open the Start menu, search for *Event Viewer*, and select the **Event Viewer** app.
2. In **Event Viewer**, open the **Windows Logs** node.
3. Select **Application** to open the Application Event Log.
4. Search for errors associated with the failing app. Errors have a value of *IIS AspNetCore Module* or *IIS Express AspNetCore Module* in the *Source* column.

### Run the app at a command prompt

Many startup errors don't produce useful information in the Application Event Log. You can find the cause of some errors by running the app at a command prompt on the hosting system.

#### Framework-dependent deployment

If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd):

1. At a command prompt, navigate to the deployment folder and run the app by executing the app's assembly with *dotnet.exe*. In the following command, substitute the name of the app's assembly for \<assembly\_name>: `dotnet .\<assembly_name>.dll`.
2. The console output from the app, showing any errors, is written to the console window.
3. If the errors occur when making a request to the app, make a request to the host and port where Kestrel listens. Using the default host and post, make a request to `http://localhost:5000/`. If the app responds normally at the Kestrel endpoint address, the problem is more likely related to the hosting configuration and less likely within the app.

#### Self-contained deployment

If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd):

1. At a command prompt, navigate to the deployment folder and run the app's executable. In the following command, substitute the name of the app's assembly for \<assembly\_name>: `<assembly_name>.exe`.
2. The console output from the app, showing any errors, is written to the console window.
3. If the errors occur when making a request to the app, make a request to the host and port where Kestrel listens. Using the default host and post, make a request to `http://localhost:5000/`. If the app responds normally at the Kestrel endpoint address, the problem is more likely related to the hosting configuration and less likely within the app.

### ASP.NET Core Module stdout log (IIS)

To enable and view stdout logs:

1. Navigate to the site's deployment folder on the hosting system.
2. If the *logs* folder isn't present, create the folder. For instructions on how to enable MSBuild to create the *logs* folder in the deployment automatically, see the [Directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0) topic.
3. Edit the *web.config* file. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to point to the *logs* folder (for example, `.\logs\stdout`). `stdout` in the path is the log file name prefix. A timestamp, process id, and file extension are added automatically when the log is created. Using `stdout` as the file name prefix, a typical log file is named *stdout\_20180205184032\_5412.log*.
4. Ensure your application pool's identity has write permissions to the *logs* folder.
5. Save the updated *web.config* file.
6. Make a request to the app.
7. Navigate to the *logs* folder. Find and open the most recent stdout log.
8. Study the log for errors.

Disable stdout logging when troubleshooting is complete:

1. Edit the *web.config* file.
2. Set **stdoutLogEnabled** to `false`.
3. Save the file.

For more information, see [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

Warning

Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created.

For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](../fundamentals/logging/?view=aspnetcore-10.0#third-party-logging-providers).

### ASP.NET Core Module debug log (IIS)

Add the following handler settings to the app's *web.config* file to enable ASP.NET Core Module debug log:

```
<aspNetCore ...>
  <handlerSettings>
    <handlerSetting name="debugLevel" value="file" />
    <handlerSetting name="debugFile" value="c:\temp\ancm.log" />
  </handlerSettings>
</aspNetCore>
```

Confirm that the path specified for the log exists and that the app pool's identity has write permissions to the location.

For more information, see [Log creation and redirection with the ASP.NET Core Module](../host-and-deploy/iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs).

### Enable the Developer Exception Page

The `ASPNETCORE_ENVIRONMENT` [environment variable can be added to web.config](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#setting-environment-variables) to run the app in the `Development` environment. As long as the environment isn't overridden in app startup by `UseEnvironment` on the host builder, setting the environment variable allows the [Developer Exception Page](../fundamentals/error-handling?view=aspnetcore-10.0) to appear when the app is run.

```
<aspNetCore processPath="dotnet"
      arguments=".\MyApp.dll"
      stdoutLogEnabled="false"
      stdoutLogFile=".\logs\stdout"
      hostingModel="InProcess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
  </environmentVariables>
</aspNetCore>
```

Setting the environment variable for `ASPNETCORE_ENVIRONMENT` is only recommended for use on staging and testing servers that aren't exposed to the Internet. Remove the environment variable from the *web.config* file after troubleshooting. For information on setting environment variables in *web.config*, see [environmentVariables child element of aspNetCore](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#setting-environment-variables).

### Obtain data from an app

If an app is capable of responding to requests, obtain request, connection, and additional data from the app using terminal inline middleware. For more information and sample code, see [Troubleshoot and debug ASP.NET Core projects](troubleshoot?view=aspnetcore-10.0#obtain-data-from-an-app).

### Slow or nonresponding app (IIS)

A *crash dump* is a snapshot of the system's memory and can help determine the cause of an app crash, startup failure, or slow app.

#### App crashes or encounters an exception

Obtain and analyze a dump from [Windows Error Reporting (WER)](/en-us/windows/desktop/wer/windows-error-reporting):

1. Create a folder to hold crash dump files at `c:\dumps`. The app pool must have write access to the folder.

2. Run the [EnableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot-azure-iis/scripts/EnableDumps.ps1):

   * If the app uses the [in-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#in-process-hosting-model), run the script for *w3wp.exe*:

     ```
     .\EnableDumps w3wp.exe c:\dumps
     ```

   * If the app uses the [out-of-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model), run the script for *dotnet.exe*:

     ```
     .\EnableDumps dotnet.exe c:\dumps
     ```

3. Run the app under the conditions that cause the crash to occur.

4. After the crash has occurred, run the [DisableDumps PowerShell script](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/troubleshoot-azure-iis/scripts/DisableDumps.ps1):

   * If the app uses the [in-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#in-process-hosting-model), run the script for *w3wp.exe*:

     ```
     .\DisableDumps w3wp.exe
     ```

   * If the app uses the [out-of-process hosting model](../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model), run the script for *dotnet.exe*:

     ```
     .\DisableDumps dotnet.exe
     ```

After an app crashes and dump collection is complete, the app is allowed to terminate normally. The PowerShell script configures WER to collect up to five dumps per app.

Warning

Crash dumps might take up a large amount of disk space (up to several gigabytes each).

#### App doesn't respond, fails during startup, or runs normally

When an app stops responding but doesn't crash, fails during startup, or runs normally, see [User-Mode Dump Files: Choosing the Best Tool](/en-us/windows-hardware/drivers/debugger/user-mode-dump-files#choosing-the-best-tool) to select an appropriate tool to produce the dump.

#### Analyze the dump

A dump can be analyzed using several approaches. For more information, see [Analyzing a User-Mode Dump File](/en-us/windows-hardware/drivers/debugger/analyzing-a-user-mode-dump-file).

## Clear package caches

A functioning app may fail immediately after upgrading either the .NET SDK on the development machine or changing package versions within the app. In some cases, incoherent packages may break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:

1. Delete the *bin* and *obj* folders.

2. Clear the package caches by executing [dotnet nuget locals all --clear](/en-us/dotnet/core/tools/dotnet-nuget-locals) from a command shell.

   Clearing package caches can also be accomplished with the [nuget.exe](https://www.nuget.org/downloads) tool and executing the command `nuget locals all -clear`. *nuget.exe* isn't a bundled install with the Windows desktop operating system and must be obtained separately from the [NuGet website](https://www.nuget.org/downloads).

3. Restore and rebuild the project.

4. Delete all of the files in the deployment folder on the server prior to redeploying the app.

## Related content

* [Debug .NET and ASP.NET Core source code with Visual Studio](debug-aspnetcore-source?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](troubleshoot?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core](../fundamentals/error-handling?view=aspnetcore-10.0)
* [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)

### Azure documentation

* [Application Insights for ASP.NET Core](/en-us/azure/application-insights/app-insights-asp-net-core)
* [Remote debugging web apps section of Troubleshoot a web app in Azure App Service using Visual Studio](/en-us/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio#remotedebug)
* [Azure App Service diagnostics overview](/en-us/azure/app-service/app-service-diagnostics)
* [How to: Monitor Apps in Azure App Service](/en-us/azure/app-service/web-sites-monitor)
* [Troubleshoot a web app in Azure App Service using Visual Studio](/en-us/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio)
* [Troubleshoot HTTP errors of "502 bad gateway" and "503 service unavailable" in your Azure web apps](/en-us/azure/app-service/app-service-web-troubleshoot-http-502-http-503)
* [Troubleshoot slow web app performance issues in Azure App Service](/en-us/azure/app-service/app-service-web-troubleshoot-performance-degradation)
* [Application performance FAQs for Web Apps in Azure](/en-us/azure/app-service/app-service-web-availability-performance-application-issues-faq)
* [Azure Web App sandbox (App Service runtime execution limitations)](https://github.com/projectkudu/kudu/wiki/Azure-Web-App-sandbox)

### Visual Studio documentation

* [Remote Debug ASP.NET Core on IIS in Azure in Visual Studio 2017](/en-us/visualstudio/debugger/remote-debugging-azure)
* [Remote Debug ASP.NET Core on a Remote IIS Computer in Visual Studio 2017](/en-us/visualstudio/debugger/remote-debugging-aspnet-on-a-remote-iis-computer)
* [Learn to debug using Visual Studio](/en-us/visualstudio/debugger/getting-started-with-the-debugger)

### Visual Studio Code documentation

* [Debugging with Visual Studio Code](https://code.visualstudio.com/docs/editor/debugging)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/project-structure
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/project-structure.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor project structure

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes the files and folders that make up a Blazor app generated from a Blazor project template.

## Blazor Web App

Blazor Web App project template: `blazor`

The Blazor Web App project template provides a single starting point for using Razor components (`.razor`) to build any style of web UI, both server-side rendered and client-side rendered. It combines the strengths of Blazor Server and Blazor WebAssembly with server-side and client-side rendering, streaming rendering, enhanced navigation and form handling, and the ability to add interactivity using either Blazor Server or Blazor WebAssembly on a per-component basis.

If both client-side rendering (CSR) and interactive server-side rendering (interactive SSR) are selected on app creation, the project template uses the Interactive Auto render mode. The automatic rendering mode initially uses interactive SSR while the .NET app bundle and runtime are downloaded to the browser. After the .NET WebAssembly runtime is activated, rendering switches to CSR.

The Blazor Web App template enables both static and interactive server-side rendering using a single project. If you also enable Interactive WebAssembly rendering, the project includes an additional client project (`.Client`) for your WebAssembly-based components. The built output from the client project is downloaded to the browser and executed on the client. Components using the Interactive WebAssembly or Interactive Auto render modes must be located in the `.Client` project.

The component folder structure of the `.Client` project differs from the Blazor Web App's main project folder structure because the main project is a standard ASP.NET Core project. The main project must take into account other assets for ASP.NET Core projects that are unrelated to Blazor. You're welcome to use whatever component folder structure you wish in the `.Client` project. You're free to mirror the component folder layout of the main project in the `.Client` project if you wish. Note that namespaces might require adjustments for such assets as layout files if you move components into different folders than the project template uses.

More information on components and render modes is found in the [ASP.NET Core Razor components](components/?view=aspnetcore-10.0) and [ASP.NET Core Blazor render modes](components/render-modes?view=aspnetcore-10.0) articles.

Based on the interactive render mode selected at app creation, the `Layout` folder is either in the server project in the `Components` folder or at the root of the `.Client` project. The folder contains the following layout components and stylesheets:

* The `MainLayout` component (`MainLayout.razor`) is the app's [layout component](components/layouts?view=aspnetcore-10.0).
* The `MainLayout.razor.css` is the collocated (next to the component) stylesheet for the app's main layout.
* The `NavMenu` component (`NavMenu.razor`) implements sidebar navigation. The component includes [`NavLink` components](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which render navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component indicates to the user which component is currently displayed.
* The `NavMenu.razor.css` is the collocated stylesheet for the app's navigation menu.
* The `ReconnectModal` component reflects the server-side connection state in the UI and is included when the app's interactive render mode is either Interactive Server or Interactive Auto. For more information, see [ASP.NET Core Blazor SignalR guidance](fundamentals/signalr?view=aspnetcore-10.0#reflect-the-server-side-connection-state-in-the-ui).
* The `ReconnectModal.razor.css` is the collocated stylesheet for the `ReconnectModal` component.
* The `ReconnectModal.razor.js` is the collocated JavaScript file for the `ReconnectModal` component.

- The `MainLayout` component (`MainLayout.razor`) is the app's [layout component](components/layouts?view=aspnetcore-10.0).
- The `MainLayout.razor.css` is the collocated (next to the component) stylesheet for the app's main layout.
- The `NavMenu` component (`NavMenu.razor`) implements sidebar navigation. The component includes [`NavLink` components](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which render navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component indicates to the user which component is currently displayed.
- The `NavMenu.razor.css` is the collocated stylesheet for the app's navigation menu.

The `Routes` component (`Routes.razor`) is either in the server project or the `.Client` project and sets up routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. For client-side interactive components, the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

The `Components` folder of the server project holds the app's server-side Razor components. Shared components are often placed at the root of the `Components` folder, while layout and page components are usually placed in folders within the `Components` folder.

The `Components/Pages` folder of the server project contains the app's routable server-side Razor components. The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive.

The `NotFound` component (`NotFound.razor`) implements a Not Found page to display when content isn't found for a request path. For more information, see [ASP.NET Core Blazor navigation](fundamentals/navigation?view=aspnetcore-10.0#not-found-responses).

The `App` component (`App.razor`) is the root component of the app with HTML `<head>` markup, the `Routes` component, and the Blazor `<script>` tag. The root component is the first component that the app loads.

An imports file (`_Imports.razor`) in each of the server and `.Client` projects includes common Razor directives for Razor components of either project, such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

The `Properties` folder of the server project holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

Note

When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

The `wwwroot` folder of the server project is the [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the server project that holds the app's public static assets.

The `Program.cs` file of the server project is the project's entry point that sets up the ASP.NET Core web application [host](../fundamentals/host/generic-host?view=aspnetcore-10.0#host-definition) and contains the app's startup logic, including service registrations, configuration, logging, and request processing pipeline:

* Services for Razor components are added by calling [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents). [AddInteractiveServerComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serverrazorcomponentsbuilderextensions.addinteractiveservercomponents) adds services to support rendering Interactive Server components. [AddInteractiveWebAssemblyComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addinteractivewebassemblycomponents) adds services to support rendering Interactive WebAssembly components.
* [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) discovers available components and specifies the root component for the app (the first component loaded), which by default is the `App` component (`App.razor`). [AddInteractiveServerRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.serverrazorcomponentsendpointconventionbuilderextensions.addinteractiveserverrendermode) configures interactive server-side rendering (interactive SSR) for the app. [AddInteractiveWebAssemblyRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.webassemblyrazorcomponentsendpointconventionbuilderextensions.addinteractivewebassemblyrendermode) configures the Interactive WebAssembly render mode for the app.

The app settings files (`appsettings.Development.json`, `appsettings.json`) in either the server or `.Client` project provide [configuration settings](fundamentals/configuration?view=aspnetcore-10.0). In the server project, settings files are at the root of the project. In the `.Client` project, settings files are consumed from the [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder, `wwwroot`.

In the `.Client` project:

* The `Pages` folder contains routable client-side Razor components. The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive.

* The `wwwroot` folder is the [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the `.Client` project that holds the app's public static assets.

* The `Program.cs` file is the project's entry point that sets up the WebAssembly [host](../fundamentals/host/generic-host?view=aspnetcore-10.0#host-definition) and contains the project's startup logic, including service registrations, configuration, logging, and request processing pipeline.

Additional files and folders may appear in an app produced from a Blazor Web App project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

## Blazor Server

Blazor Server project templates: `blazorserver`, `blazorserver-empty`

The Blazor Server templates create the initial files and directory structure for a Blazor Server app:

* If the `blazorserver` template is used, the app is populated with the following:

  * Demonstration code for a `FetchData` component that loads data from a weather forecast service (`WeatherForecastService`) and user interaction with a `Counter` component.
  * [Bootstrap](https://getbootstrap.com/) frontend toolkit.

* If the `blazorserver-empty` template is used, the app is created without demonstration code and Bootstrap.

Project structure:

* `Data` folder: Contains the `WeatherForecast` class and implementation of the `WeatherForecastService` that provides example weather data to the app's `FetchData` component.

* `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`) and the root Razor page of a Blazor Server app. The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following:

  * `_Host.cshtml`: The root page of the app implemented as a Razor Page:

    * When any page of the app is initially requested, this page is rendered and returned in the response.
    * The Host page specifies where the root `App` component (`App.razor`) is rendered.

  * `Counter` component (`Counter.razor`): Implements the Counter page.

  * `Error` component (`Error.razor`): Rendered when an unhandled exception occurs in the app.

  * `FetchData` component (`FetchData.razor`): Implements the Fetch data page.

  * `Index` component (`Index.razor`): Implements the Home page.

* `Properties` folder: Holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

* `Shared` folder: Contains the following shared components and stylesheets:

  * `MainLayout` component (`MainLayout.razor`): The app's [layout component](components/layouts?view=aspnetcore-10.0).
  * `MainLayout.razor.css`: Stylesheet for the app's main layout.
  * `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. Includes the [`NavLink` component](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which renders navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component automatically indicates a selected state when its component is loaded, which helps the user understand which component is currently displayed.
  * `NavMenu.razor.css`: Stylesheet for the app's navigation menu.
  * `SurveyPrompt` component (`SurveyPrompt.razor`): Blazor survey component.

* `wwwroot` folder: The [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the app containing the app's public static assets.

* `_Imports.razor`: Includes common Razor directives to include in the app's components (`.razor`), such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

* `App.razor`: The root component of the app that sets up client-side routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

* `appsettings.json` and environmental app settings files: Provide [configuration settings](fundamentals/configuration?view=aspnetcore-10.0) for the app.

* `Program.cs`: The app's entry point that sets up the ASP.NET Core [host](../fundamentals/host/generic-host?view=aspnetcore-10.0) and contains the app's startup logic, including service registrations and request processing pipeline configuration:

  * Specifies the app's [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) services. Services are added by calling [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor), and the `WeatherForecastService` is added to the service container for use by the example `FetchData` component.

  * Configures the app's request handling pipeline:

    * [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) is called to set up an endpoint for the real-time connection with the browser. The connection is created with [SignalR](../signalr/introduction?view=aspnetcore-10.0), which is a framework for adding real-time web functionality to apps.
    * [`MapFallbackToPage("/_Host")`](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.mapfallbacktopage) is called to set up the root page of the app (`Pages/_Host.cshtml`) and enable navigation.

Additional files and folders may appear in an app produced from a Blazor Server project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

Blazor Server project template: `blazorserver`

The Blazor Server template creates the initial files and directory structure for a Blazor Server app. The app is populated with demonstration code for a `FetchData` component that loads data from a registered service, `WeatherForecastService`, and user interaction with a `Counter` component.

* `Data` folder: Contains the `WeatherForecast` class and implementation of the `WeatherForecastService` that provides example weather data to the app's `FetchData` component.

* `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`) and the root Razor page of a Blazor Server app. The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following:

  * `_Host.cshtml`: The root page of the app implemented as a Razor Page:

    * When any page of the app is initially requested, this page is rendered and returned in the response.
    * The Host page specifies where the root `App` component (`App.razor`) is rendered.

  * `_Layout.cshtml`: The layout page for the `_Host.cshtml` root page of the app.

  * `Counter` component (`Counter.razor`): Implements the Counter page.

  * `Error` component (`Error.razor`): Rendered when an unhandled exception occurs in the app.

  * `FetchData` component (`FetchData.razor`): Implements the Fetch data page.

  * `Index` component (`Index.razor`): Implements the Home page.

* `Properties` folder: Holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

* `Shared` folder: Contains the following shared components and stylesheets:

  * `MainLayout` component (`MainLayout.razor`): The app's [layout component](components/layouts?view=aspnetcore-10.0).
  * `MainLayout.razor.css`: Stylesheet for the app's main layout.
  * `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. Includes the [`NavLink` component](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which renders navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component automatically indicates a selected state when its component is loaded, which helps the user understand which component is currently displayed.
  * `NavMenu.razor.css`: Stylesheet for the app's navigation menu.
  * `SurveyPrompt` component (`SurveyPrompt.razor`): Blazor survey component.

* `wwwroot` folder: The [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the app containing the app's public static assets.

* `_Imports.razor`: Includes common Razor directives to include in the app's components (`.razor`), such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

* `App.razor`: The root component of the app that sets up client-side routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

* `appsettings.json` and environmental app settings files: Provide [configuration settings](fundamentals/configuration?view=aspnetcore-10.0) for the app.

* `Program.cs`: The app's entry point that sets up the ASP.NET Core [host](../fundamentals/host/generic-host?view=aspnetcore-10.0) and contains the app's startup logic, including service registrations and request processing pipeline configuration:

  * Specifies the app's [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) services. Services are added by calling [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor), and the `WeatherForecastService` is added to the service container for use by the example `FetchData` component.

  * Configures the app's request handling pipeline:

    * [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) is called to set up an endpoint for the real-time connection with the browser. The connection is created with [SignalR](../signalr/introduction?view=aspnetcore-10.0), which is a framework for adding real-time web functionality to apps.
    * [`MapFallbackToPage("/_Host")`](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.mapfallbacktopage) is called to set up the root page of the app (`Pages/_Host.cshtml`) and enable navigation.

Additional files and folders may appear in an app produced from a Blazor Server project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

Blazor Server project template: `blazorserver`

The Blazor Server template creates the initial files and directory structure for a Blazor Server app. The app is populated with demonstration code for a `FetchData` component that loads data from a registered service, `WeatherForecastService`, and user interaction with a `Counter` component.

* `Data` folder: Contains the `WeatherForecast` class and implementation of the `WeatherForecastService` that provides example weather data to the app's `FetchData` component.

* `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`) and the root Razor page of a Blazor Server app. The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following:

  * `_Host.cshtml`: The root page of the app implemented as a Razor Page:

    * When any page of the app is initially requested, this page is rendered and returned in the response.
    * The Host page specifies where the root `App` component (`App.razor`) is rendered.

  * `Counter` component (`Counter.razor`): Implements the Counter page.

  * `Error` component (`Error.razor`): Rendered when an unhandled exception occurs in the app.

  * `FetchData` component (`FetchData.razor`): Implements the Fetch data page.

  * `Index` component (`Index.razor`): Implements the Home page.

* `Properties` folder: Holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

* `Shared` folder: Contains the following shared components and stylesheets:

  * `MainLayout` component (`MainLayout.razor`): The app's [layout component](components/layouts?view=aspnetcore-10.0).
  * `MainLayout.razor.css`: Stylesheet for the app's main layout.
  * `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. Includes the [`NavLink` component](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which renders navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component automatically indicates a selected state when its component is loaded, which helps the user understand which component is currently displayed.
  * `NavMenu.razor.css`: Stylesheet for the app's navigation menu.
  * `SurveyPrompt` component (`SurveyPrompt.razor`): Blazor survey component.

* `wwwroot` folder: The [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the app containing the app's public static assets.

* `_Imports.razor`: Includes common Razor directives to include in the app's components (`.razor`), such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

* `App.razor`: The root component of the app that sets up client-side routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

* `appsettings.json` and environmental app settings files: Provide [configuration settings](fundamentals/configuration?view=aspnetcore-10.0) for the app.

* `Program.cs`: The app's entry point that sets up the ASP.NET Core [host](../fundamentals/host/generic-host?view=aspnetcore-10.0).

* `Startup.cs`: Contains the app's startup logic. The `Startup` class defines two methods:

  * `ConfigureServices`: Configures the app's [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) services. Services are added by calling [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor), and the `WeatherForecastService` is added to the service container for use by the example `FetchData` component.

  * `Configure`: Configures the app's request handling pipeline:

    * [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) is called to set up an endpoint for the real-time connection with the browser. The connection is created with [SignalR](../signalr/introduction?view=aspnetcore-10.0), which is a framework for adding real-time web functionality to apps.
    * [`MapFallbackToPage("/_Host")`](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.mapfallbacktopage) is called to set up the root page of the app (`Pages/_Host.cshtml`) and enable navigation.

Additional files and folders may appear in an app produced from a Blazor Server project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

Blazor Server project template: `blazorserver`

The Blazor Server template creates the initial files and directory structure for a Blazor Server app. The app is populated with demonstration code for a `FetchData` component that loads data from a registered service, `WeatherForecastService`, and user interaction with a `Counter` component.

* `Data` folder: Contains the `WeatherForecast` class and implementation of the `WeatherForecastService` that provides example weather data to the app's `FetchData` component.

* `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`) and the root Razor page of a Blazor Server app. The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following:

  * `_Host.cshtml`: The root page of the app implemented as a Razor Page:

    * When any page of the app is initially requested, this page is rendered and returned in the response.
    * The Host page specifies where the root `App` component (`App.razor`) is rendered.

  * `Counter` component (`Counter.razor`): Implements the Counter page.

  * `Error` component (`Error.razor`): Rendered when an unhandled exception occurs in the app.

  * `FetchData` component (`FetchData.razor`): Implements the Fetch data page.

  * `Index` component (`Index.razor`): Implements the Home page.

* `Properties` folder: Holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

* `Shared` folder: Contains the following shared components:

  * `MainLayout` component (`MainLayout.razor`): The app's [layout component](components/layouts?view=aspnetcore-10.0).
  * `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. Includes the [`NavLink` component](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which renders navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component automatically indicates a selected state when its component is loaded, which helps the user understand which component is currently displayed.
  * `SurveyPrompt` component (`SurveyPrompt.razor`): Blazor survey component.

* `wwwroot` folder: The [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the app containing the app's public static assets.

* `_Imports.razor`: Includes common Razor directives to include in the app's components (`.razor`), such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

* `App.razor`: The root component of the app that sets up client-side routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

* `appsettings.json` and environmental app settings files: Provide [configuration settings](fundamentals/configuration?view=aspnetcore-10.0) for the app.

* `Program.cs`: The app's entry point that sets up the ASP.NET Core [host](../fundamentals/host/generic-host?view=aspnetcore-10.0).

* `Startup.cs`: Contains the app's startup logic. The `Startup` class defines two methods:

  * `ConfigureServices`: Configures the app's [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) services. Services are added by calling [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor), and the `WeatherForecastService` is added to the service container for use by the example `FetchData` component.

  * `Configure`: Configures the app's request handling pipeline:

    * [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) is called to set up an endpoint for the real-time connection with the browser. The connection is created with [SignalR](../signalr/introduction?view=aspnetcore-10.0), which is a framework for adding real-time web functionality to apps.
    * [`MapFallbackToPage("/_Host")`](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.mapfallbacktopage) is called to set up the root page of the app (`Pages/_Host.cshtml`) and enable navigation.

Additional files and folders may appear in an app produced from a Blazor Server project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

## Standalone Blazor WebAssembly

Standalone Blazor WebAssembly project template: `blazorwasm`

The Blazor WebAssembly template creates the initial files and directory structure for a standalone Blazor WebAssembly app:

* If the `blazorwasm` template is used, the app is populated with the following:

  * Demonstration code for a `Weather` component that loads data from a static asset (`weather.json`) and user interaction with a `Counter` component.
  * [Bootstrap](https://getbootstrap.com/) frontend toolkit.

* The `blazorwasm` template can also be generated without sample pages and styling.

Project structure:

* `Layout` folder: Contains the following layout components and stylesheets:

  * `MainLayout` component (`MainLayout.razor`): The app's [layout component](components/layouts?view=aspnetcore-10.0).
  * `MainLayout.razor.css`: Stylesheet for the app's main layout.
  * `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. Includes the [`NavLink` component](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which renders navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component automatically indicates a selected state when its component is loaded, which helps the user understand which component is currently displayed.
  * `NavMenu.razor.css`: Stylesheet for the app's navigation menu.

- `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`). The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following components:

  * `Counter` component (`Counter.razor`): Implements the Counter page.
  * `Index` component (`Index.razor`): Implements the Home page.
  * `Weather` component (`Weather.razor`): Implements the Weather page.
  * `NotFound` component (`NotFound.razor`): Implements a Not Found page to display when content isn't found for a request path. For more information, see [ASP.NET Core Blazor navigation](fundamentals/navigation?view=aspnetcore-10.0#not-found-responses).

* `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`). The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following components:

  * `Counter` component (`Counter.razor`): Implements the Counter page.
  * `Index` component (`Index.razor`): Implements the Home page.
  * `Weather` component (`Weather.razor`): Implements the Weather page.

- `_Imports.razor`: Includes common Razor directives to include in the app's components (`.razor`), such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

- `App.razor`: The root component of the app that sets up client-side routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

- `Properties` folder: Holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

- `wwwroot` folder: The [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the app containing the app's public static assets, including `appsettings.json` and environmental app settings files for [configuration settings](fundamentals/configuration?view=aspnetcore-10.0) and sample weather data (`sample-data/weather.json`). The `index.html` webpage is the root page of the app implemented as an HTML page:

  * When any page of the app is initially requested, this page is rendered and returned in the response.
  * The page specifies where the root `App` component is rendered. The component is rendered at the location of the `div` DOM element with an `id` of `app` (`<div id="app">Loading...</div>`).

- `Program.cs`: The app's entry point that sets up the WebAssembly host:

  * The `App` component is the root component of the app. The `App` component is specified as the `div` DOM element with an `id` of `app` (`<div id="app">Loading...</div>` in `wwwroot/index.html`) to the root component collection (`builder.RootComponents.Add<App>("#app")`).
  * [Services](fundamentals/dependency-injection?view=aspnetcore-10.0) are added and configured (for example, `builder.Services.AddSingleton<IMyDependency, MyDependency>()`).

Additional files and folders may appear in an app produced from a Blazor WebAssembly project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

## Blazor WebAssembly

Blazor WebAssembly project templates: `blazorwasm`, `blazorwasm-empty`

The Blazor WebAssembly templates create the initial files and directory structure for a Blazor WebAssembly app:

* If the `blazorwasm` template is used, the app is populated with the following:

  * Demonstration code for a `FetchData` component that loads data from a static asset (`weather.json`) and user interaction with a `Counter` component.
  * [Bootstrap](https://getbootstrap.com/) frontend toolkit.

* If the `blazorwasm-empty` template is used, the app is created without demonstration code and Bootstrap.

Project structure:

* `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`). The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following components:

  * `Counter` component (`Counter.razor`): Implements the Counter page.
  * `FetchData` component (`FetchData.razor`): Implements the Fetch data page.
  * `Index` component (`Index.razor`): Implements the Home page.

* `Properties` folder: Holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

* `Shared` folder: Contains the following shared components and stylesheets:

  * `MainLayout` component (`MainLayout.razor`): The app's [layout component](components/layouts?view=aspnetcore-10.0).
  * `MainLayout.razor.css`: Stylesheet for the app's main layout.
  * `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. Includes the [`NavLink` component](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which renders navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component automatically indicates a selected state when its component is loaded, which helps the user understand which component is currently displayed.
  * `NavMenu.razor.css`: Stylesheet for the app's navigation menu.
  * `SurveyPrompt` component (`SurveyPrompt.razor`) (*ASP.NET Core in .NET 7 or earlier*): Blazor survey component.

* `wwwroot` folder: The [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the app containing the app's public static assets, including `appsettings.json` and environmental app settings files for [configuration settings](fundamentals/configuration?view=aspnetcore-10.0). The `index.html` webpage is the root page of the app implemented as an HTML page:

  * When any page of the app is initially requested, this page is rendered and returned in the response.
  * The page specifies where the root `App` component is rendered. The component is rendered at the location of the `div` DOM element with an `id` of `app` (`<div id="app">Loading...</div>`).

* `_Imports.razor`: Includes common Razor directives to include in the app's components (`.razor`), such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

* `App.razor`: The root component of the app that sets up client-side routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

* `Program.cs`: The app's entry point that sets up the WebAssembly host:

  * The `App` component is the root component of the app. The `App` component is specified as the `div` DOM element with an `id` of `app` (`<div id="app">Loading...</div>` in `wwwroot/index.html`) to the root component collection (`builder.RootComponents.Add<App>("#app")`).
  * [Services](fundamentals/dependency-injection?view=aspnetcore-10.0) are added and configured (for example, `builder.Services.AddSingleton<IMyDependency, MyDependency>()`).

Additional files and folders may appear in an app produced from a Blazor WebAssembly project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

A *hosted Blazor WebAssembly solution* includes the following ASP.NET Core projects:

* "Client": The Blazor WebAssembly app.
* "Server": An app that serves the Blazor WebAssembly app and weather data to clients.
* "Shared": A project that maintains common classes, methods, and resources.

The solution is generated from the Blazor WebAssembly project template in Visual Studio with the **ASP.NET Core Hosted** checkbox selected or with the `-ho|--hosted` option using the .NET CLI's `dotnet new blazorwasm` command. For more information, see [Tooling for ASP.NET Core Blazor](tooling?view=aspnetcore-10.0).

The project structure of the client-side app in a hosted Blazor Webassembly solution ("Client" project) is the same as the project structure for a standalone Blazor WebAssembly app. Additional files in a hosted Blazor WebAssembly solution:

* The "Server" project includes a weather forecast controller at `Controllers/WeatherForecastController.cs` that returns weather data to the "Client" project's `FetchData` component.
* The "Shared" project includes a weather forecast class at `WeatherForecast.cs` that represents weather data for the "Client" and "Server" projects.

## Blazor WebAssembly

Blazor WebAssembly project template: `blazorwasm`

The Blazor WebAssembly template creates the initial files and directory structure for a Blazor WebAssembly app. The app is populated with demonstration code for a `FetchData` component that loads data from a static asset, `weather.json`, and user interaction with a `Counter` component.

* `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`). The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following components:

  * `Counter` component (`Counter.razor`): Implements the Counter page.
  * `FetchData` component (`FetchData.razor`): Implements the Fetch data page.
  * `Index` component (`Index.razor`): Implements the Home page.

* `Properties` folder: Holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

* `Shared` folder: Contains the following shared components and stylesheets:

  * `MainLayout` component (`MainLayout.razor`): The app's [layout component](components/layouts?view=aspnetcore-10.0).
  * `MainLayout.razor.css`: Stylesheet for the app's main layout.
  * `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. Includes the [`NavLink` component](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which renders navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component automatically indicates a selected state when its component is loaded, which helps the user understand which component is currently displayed.
  * `NavMenu.razor.css`: Stylesheet for the app's navigation menu.
  * `SurveyPrompt` component (`SurveyPrompt.razor`): Blazor survey component.

* `wwwroot` folder: The [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the app containing the app's public static assets, including `appsettings.json` and environmental app settings files for [configuration settings](fundamentals/configuration?view=aspnetcore-10.0). The `index.html` webpage is the root page of the app implemented as an HTML page:

  * When any page of the app is initially requested, this page is rendered and returned in the response.
  * The page specifies where the root `App` component is rendered. The component is rendered at the location of the `div` DOM element with an `id` of `app` (`<div id="app">Loading...</div>`).

* `_Imports.razor`: Includes common Razor directives to include in the app's components (`.razor`), such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

* `App.razor`: The root component of the app that sets up client-side routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

* `Program.cs`: The app's entry point that sets up the WebAssembly host:

  * The `App` component is the root component of the app. The `App` component is specified as the `div` DOM element with an `id` of `app` (`<div id="app">Loading...</div>` in `wwwroot/index.html`) to the root component collection (`builder.RootComponents.Add<App>("#app")`).
  * [Services](fundamentals/dependency-injection?view=aspnetcore-10.0) are added and configured (for example, `builder.Services.AddSingleton<IMyDependency, MyDependency>()`).

Additional files and folders may appear in an app produced from a Blazor WebAssembly project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

A *hosted Blazor WebAssembly solution* includes the following ASP.NET Core projects:

* "Client": The Blazor WebAssembly app.
* "Server": An app that serves the Blazor WebAssembly app and weather data to clients.
* "Shared": A project that maintains common classes, methods, and resources.

The solution is generated from the Blazor WebAssembly project template in Visual Studio with the **ASP.NET Core Hosted** checkbox selected or with the `-ho|--hosted` option using the .NET CLI's `dotnet new blazorwasm` command. For more information, see [Tooling for ASP.NET Core Blazor](tooling?view=aspnetcore-10.0).

The project structure of the client-side app in a hosted Blazor Webassembly solution ("Client" project) is the same as the project structure for a standalone Blazor WebAssembly app. Additional files in a hosted Blazor WebAssembly solution:

* The "Server" project includes a weather forecast controller at `Controllers/WeatherForecastController.cs` that returns weather data to the "Client" project's `FetchData` component.
* The "Shared" project includes a weather forecast class at `WeatherForecast.cs` that represents weather data for the "Client" and "Server" projects.

## Blazor WebAssembly

Blazor WebAssembly project template: `blazorwasm`

The Blazor WebAssembly template creates the initial files and directory structure for a Blazor WebAssembly app. The app is populated with demonstration code for a `FetchData` component that loads data from a static asset, `weather.json`, and user interaction with a `Counter` component.

* `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`). The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following components:

  * `Counter` component (`Counter.razor`): Implements the Counter page.
  * `FetchData` component (`FetchData.razor`): Implements the Fetch data page.
  * `Index` component (`Index.razor`): Implements the Home page.

* `Properties` folder: Holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

* `Shared` folder: Contains the following shared components and stylesheets:

  * `MainLayout` component (`MainLayout.razor`): The app's [layout component](components/layouts?view=aspnetcore-10.0).
  * `MainLayout.razor.css`: Stylesheet for the app's main layout.
  * `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. Includes the [`NavLink` component](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which renders navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component automatically indicates a selected state when its component is loaded, which helps the user understand which component is currently displayed.
  * `NavMenu.razor.css`: Stylesheet for the app's navigation menu.
  * `SurveyPrompt` component (`SurveyPrompt.razor`): Blazor survey component.

* `wwwroot` folder: The [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the app containing the app's public static assets, including `appsettings.json` and environmental app settings files for [configuration settings](fundamentals/configuration?view=aspnetcore-10.0). The `index.html` webpage is the root page of the app implemented as an HTML page:

  * When any page of the app is initially requested, this page is rendered and returned in the response.
  * The page specifies where the root `App` component is rendered. The component is rendered at the location of the `div` DOM element with an `id` of `app` (`<div id="app">Loading...</div>`).

* `_Imports.razor`: Includes common Razor directives to include in the app's components (`.razor`), such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

* `App.razor`: The root component of the app that sets up client-side routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

* `Program.cs`: The app's entry point that sets up the WebAssembly host:

  * The `App` component is the root component of the app. The `App` component is specified as the `div` DOM element with an `id` of `app` (`<div id="app">Loading...</div>` in `wwwroot/index.html`) to the root component collection (`builder.RootComponents.Add<App>("#app")`).
  * [Services](fundamentals/dependency-injection?view=aspnetcore-10.0) are added and configured (for example, `builder.Services.AddSingleton<IMyDependency, MyDependency>()`).

Additional files and folders may appear in an app produced from a Blazor WebAssembly project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

A *hosted Blazor WebAssembly solution* includes the following ASP.NET Core projects:

* "Client": The Blazor WebAssembly app.
* "Server": An app that serves the Blazor WebAssembly app and weather data to clients.
* "Shared": A project that maintains common classes, methods, and resources.

The solution is generated from the Blazor WebAssembly project template in Visual Studio with the **ASP.NET Core Hosted** checkbox selected or with the `-ho|--hosted` option using the .NET CLI's `dotnet new blazorwasm` command. For more information, see [Tooling for ASP.NET Core Blazor](tooling?view=aspnetcore-10.0).

The project structure of the client-side app in a hosted Blazor Webassembly solution ("Client" project) is the same as the project structure for a standalone Blazor WebAssembly app. Additional files in a hosted Blazor WebAssembly solution:

* The "Server" project includes a weather forecast controller at `Controllers/WeatherForecastController.cs` that returns weather data to the "Client" project's `FetchData` component.
* The "Shared" project includes a weather forecast class at `WeatherForecast.cs` that represents weather data for the "Client" and "Server" projects.

## Blazor WebAssembly

Blazor WebAssembly project template: `blazorwasm`

The Blazor WebAssembly template creates the initial files and directory structure for a Blazor WebAssembly app. The app is populated with demonstration code for a `FetchData` component that loads data from a static asset, `weather.json`, and user interaction with a `Counter` component.

* `Pages` folder: Contains the Blazor app's routable Razor components (`.razor`). The route for each page is specified using the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. The template includes the following components:

  * `Counter` component (`Counter.razor`): Implements the Counter page.
  * `FetchData` component (`FetchData.razor`): Implements the Fetch data page.
  * `Index` component (`Index.razor`): Implements the Home page.

* `Properties` folder: Holds [development environment configuration](../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson) in the `launchSettings.json` file.

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

* `Shared` folder: Contains the following shared components:

  * `MainLayout` component (`MainLayout.razor`): The app's [layout component](components/layouts?view=aspnetcore-10.0).
  * `NavMenu` component (`NavMenu.razor`): Implements sidebar navigation. Includes the [`NavLink` component](fundamentals/navigation?view=aspnetcore-10.0#navlink-component) ([NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink)), which renders navigation links to other Razor components. The [NavLink](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navlink) component automatically indicates a selected state when its component is loaded, which helps the user understand which component is currently displayed.
  * `SurveyPrompt` component (`SurveyPrompt.razor`): Blazor survey component.

* `wwwroot` folder: The [Web Root](../fundamentals/?view=aspnetcore-10.0#web-root) folder for the app containing the app's public static assets, including `appsettings.json` and environmental app settings files for [configuration settings](fundamentals/configuration?view=aspnetcore-10.0). The `index.html` webpage is the root page of the app implemented as an HTML page:

  * When any page of the app is initially requested, this page is rendered and returned in the response.
  * The page specifies where the root `App` component is rendered. The component is rendered at the location of the `app` DOM element (`<app>Loading...</app>`).

* `_Imports.razor`: Includes common Razor directives to include in the app's components (`.razor`), such as [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directives for namespaces.

* `App.razor`: The root component of the app that sets up client-side routing using the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component intercepts browser navigation and renders the page that matches the requested address.

* `Program.cs`: The app's entry point that sets up the WebAssembly host:

  * The `App` component is the root component of the app. The `App` component is specified as the `app` DOM element (`<app>Loading...</app>` in `wwwroot/index.html`) to the root component collection (`builder.RootComponents.Add<App>("app")`).
  * [Services](fundamentals/dependency-injection?view=aspnetcore-10.0) are added and configured (for example, `builder.Services.AddSingleton<IMyDependency, MyDependency>()`).

Additional files and folders may appear in an app produced from a Blazor WebAssembly project template when additional options are configured. For example, generating an app with ASP.NET Core Identity includes additional assets for authentication and authorization features.

A *hosted Blazor WebAssembly solution* includes the following ASP.NET Core projects:

* "Client": The Blazor WebAssembly app.
* "Server": An app that serves the Blazor WebAssembly app and weather data to clients.
* "Shared": A project that maintains common classes, methods, and resources.

The solution is generated from the Blazor WebAssembly project template in Visual Studio with the **ASP.NET Core Hosted** checkbox selected or with the `-ho|--hosted` option using the .NET CLI's `dotnet new blazorwasm` command. For more information, see [Tooling for ASP.NET Core Blazor](tooling?view=aspnetcore-10.0).

The project structure of the client-side app in a hosted Blazor Webassembly solution ("Client" project) is the same as the project structure for a standalone Blazor WebAssembly app. Additional files in a hosted Blazor WebAssembly solution:

* The "Server" project includes a weather forecast controller at `Controllers/WeatherForecastController.cs` that returns weather data to the "Client" project's `FetchData` component.
* The "Shared" project includes a weather forecast class at `WeatherForecast.cs` that represents weather data for the "Client" and "Server" projects.

## Location of the Blazor script

The Blazor script is served as a static web asset with automatic compression and fingerprinting. For more information, see [ASP.NET Core Blazor static files](fundamentals/static-files?view=aspnetcore-10.0).

In a Blazor Web App, the Blazor script is located in the `Components/App.razor` file:

```
<script src="@Assets["_framework/blazor.web.js"]"></script>
```

In a Blazor Server app, the Blazor script is located in the `Pages/_Host.cshtml` file:

```
<script src="_framework/blazor.server.js"></script>
```

The Blazor script is served from an embedded resource in the ASP.NET Core shared framework.

In a Blazor Web App, the Blazor script is located in the `Components/App.razor` file:

```
<script src="_framework/blazor.web.js"></script>
```

In a Blazor Server app, the Blazor script is located in the `Pages/_Host.cshtml` file:

```
<script src="_framework/blazor.server.js"></script>
```

The Blazor script is served from an embedded resource in the ASP.NET Core shared framework.

In a Blazor Server app, the Blazor script is located in the `Pages/_Host.cshtml` file:

```
<script src="_framework/blazor.server.js"></script>
```

The Blazor script is served from an embedded resource in the ASP.NET Core shared framework.

In a Blazor Server app, the Blazor script is located in the `Pages/_Layout.cshtml` file:

```
<script src="_framework/blazor.server.js"></script>
```

The Blazor script is served from an embedded resource in the ASP.NET Core shared framework.

In a Blazor Server app, the Blazor script is located in the `Pages/_Host.cshtml` file:

```
<script src="_framework/blazor.server.js"></script>
```

For a Blazor Web App or a Blazor Server app, the project must contain at least one Razor component file (`.razor`) in order to automatically include the Blazor script when the app is published. If the project doesn't contain at least one Razor component, set the `RequiresAspNetWebAssets` MSBuild property to `true` in the app's project file to include the Blazor script:

```
<RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>
```

In a Blazor WebAssembly app, the Blazor script content is located in the `wwwroot/index.html` file:

```
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
```

When the app is published, the `{fingerprint}` placeholder is automatically replaced with a unique hash for cache busting.

```
<script src="_framework/blazor.webassembly.js"></script>
```

## Location of `<head>` and `<body>` content

In a Blazor Web App, `<head>` and `<body>` content is located in the `Components/App.razor` file.

In a Blazor Server app, `<head>` and `<body>` content is located in the `Pages/_Host.cshtml` file.

In a Blazor Server app, `<head>` and `<body>` content is located in the `Pages/_Layout.cshtml` file.

In a Blazor Server app, `<head>` and `<body>` content is located in the `Pages/_Host.cshtml` file.

In a Blazor WebAssembly app, `<head>` and `<body>` content is located in the `wwwroot/index.html` file.

## Dual Blazor Server/Blazor WebAssembly app

To create an app that can run as either a Blazor Server app or a Blazor WebAssembly app, one approach is to place all of the app logic and components into a [Razor class library (RCL)](components/class-libraries?view=aspnetcore-10.0) and reference the RCL from separate Blazor Server and Blazor WebAssembly projects. For common services whose implementations differ based on the hosting model, define the service interfaces in the RCL and implement the services in the Blazor Server and Blazor WebAssembly projects.

## Related content

* [Tooling for ASP.NET Core Blazor](tooling?view=aspnetcore-10.0)
* [ASP.NET Core Blazor hosting models](hosting-models?view=aspnetcore-10.0)
* [APIs overview](../fundamentals/apis?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](fundamentals/?view=aspnetcore-10.0#sample-apps))

- [Tooling for ASP.NET Core Blazor](tooling?view=aspnetcore-10.0)
- [ASP.NET Core Blazor hosting models](hosting-models?view=aspnetcore-10.0)
- [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](fundamentals/?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/caching/hybrid.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# HybridCache library in ASP.NET Core

This article explains how to configure and use the `HybridCache` library in an ASP.NET Core app. For an introduction to the library, see [the `HybridCache` section of the Caching overview](overview?view=aspnetcore-10.0#hybridcache).

## Get the library

Install the `Microsoft.Extensions.Caching.Hybrid` package.

```
dotnet add package Microsoft.Extensions.Caching.Hybrid
```

## Register the service

Add the `HybridCache` service to the [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container by calling [AddHybridCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.hybridcacheserviceextensions.addhybridcache):

```
// Add services to the container.
var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddHybridCache();
```

The preceding code registers the `HybridCache` service with default options. The registration API can also configure [options](#options) and [serialization](#serialization).

## Get and store cache entries

The `HybridCache` service provides a [GetOrCreateAsync](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache.getorcreateasync) method with two overloads, taking a key and:

* A factory method.
* State, and a factory method.

The method uses the key to try to retrieve the object from the primary cache. If the item isn't found in the primary cache (a cache miss), it then checks the secondary cache if one is configured. If it doesn't find the data there (another cache miss), it calls the factory method to get the object from the data source. It then stores the object in both primary and secondary caches. The factory method is never called if the object is found in the primary or secondary cache (a cache hit).

A `HybridCache` instance ensures that only one concurrent caller for a given key calls the factory method, and all other callers using the same instance wait for the result of that call. The cancellation token passed to the factory is canceled when all callers awaiting the shared operation have canceled. This coordination doesn't extend to other `HybridCache` instances, even if they use the same secondary distributed cache.

### The main `GetOrCreateAsync` overload

The stateless overload of `GetOrCreateAsync` is recommended for most scenarios. The code to call it is relatively simple. Here's an example:

```
public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            cancellationToken: token
        );
    }

    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        // Placeholder for retrieving data from a real backing store (database, HTTP API, etc.).
        // A real implementation would await that call; this stub returns synchronously,
        // so it intentionally has no await (and produces compiler warning CS1998).
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}
```

## Cache key guidance

The `key` passed to `GetOrCreateAsync` must uniquely identify the data being cached:

* In terms of the identifier values used to retrieve that data from its source.
* In terms of other data cached in the application.

Both types of uniqueness are usually ensured by using string concatenation to make a single key string composed of different parts concatenated into one string. For example:

```
cache.GetOrCreateAsync($"/orders/{region}/{orderId}", ...);
```

Or

```
cache.GetOrCreateAsync($"user_prefs_{userId}", ...);
```

It's the caller's responsibility to ensure that a key scheme is valid and can't cause data to become confused.

Avoid using external user input directly in cache keys. For example, don't use raw strings from user interfaces as cache keys. Doing so can expose your app to security risks, such as unauthorized access or denial-of-service attacks caused by flooding the cache with random or meaningless keys. In the preceding valid examples, the *order* and *user preference* data are clearly separated and use trusted identifiers:

* `orderid` and `userId` are internally generated identifiers.
* `region` might be an enum or string from a predefined list of known regions.

No significance is placed on tokens such as `/` or `_`. The entire key value is treated as an opaque identifying string. In this case, you could omit the `/` and `_` with no change to the way the cache functions, but a delimiter is usually used to avoid ambiguity - for example `$"order{customerId}{orderId}"` could cause confusion between:

* `customerId` 42 with `orderId` 123
* `customerId` 421 with `orderId` 23

Both of the preceding examples would generate the cache key `order42123`.

This guidance applies equally to any `string`-based cache API, such as `HybridCache`, `IDistributedCache`, and `IMemoryCache`.

Notice that the inline interpolated string syntax (`$"..."` in the preceding examples of valid keys) is directly inside the `GetOrCreateAsync` call. This syntax is recommended when using `HybridCache`, as it allows for planned future improvements that bypass the need to allocate a `string` for the key in many scenarios.

### Additional key considerations

* Keys can be restricted to valid maximum lengths. For example, the default `HybridCache` implementation (via `AddHybridCache(...)`) restricts keys to 1024 characters by default. That number is configurable via `HybridCacheOptions.MaximumKeyLength`, with longer keys bypassing the cache mechanisms to prevent saturation.
* Keys must be valid Unicode sequences. If invalid Unicode sequences are passed, the behavior is undefined.
* When using an out-of-process secondary cache such as `IDistributedCache`, the backend implementation may impose additional restrictions. As a hypothetical example, a particular backend might use case-insensitive key logic. The default `HybridCache` (via `AddHybridCache(...)`) detects this scenario to prevent confusion attacks or alias attacks (using bitwise string equality). However, this scenario might still result in conflicting keys becoming overwritten or evicted sooner than expected.

### The alternative `GetOrCreateAsync` overload

The alternative overload might reduce some overhead from [captured variables](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#capture-of-outer-variables-and-variable-scope-in-lambda-expressions) and per-instance callbacks, but at the expense of more complex code. For most scenarios, the performance increase doesn't outweigh the code complexity. Here's an example that uses the alternative overload:

```
public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            (name, id, obj: this),
            static async (state, token) =>
            await state.obj.GetDataFromTheSourceAsync(state.name, state.id, token),
            cancellationToken: token
        );
    }

    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        // Placeholder for retrieving data from a real backing store (database, HTTP API, etc.).
        // A real implementation would await that call. This stub returns synchronously and
        // intentionally has no await, so the compiler emits warning CS1998 for this method.
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}
```

### The `SetAsync` method

In many scenarios, `GetOrCreateAsync` is the only API needed. But `HybridCache` also has [SetAsync](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache.setasync) to store an object in cache without trying to retrieve it first.

## Remove cache entries by key

When the underlying data for a cache entry changes before it expires, remove the entry explicitly by calling [RemoveAsync](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache.removeasync) with the key to the entry. An overload lets you specify a collection of key values.

When an entry is removed, it is removed from both the primary and secondary caches.

## Remove cache entries by tag

Tags can be used to group cache entries and invalidate them together.

Set tags when calling `GetOrCreateAsync`, as shown in the following example:

```
public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        var tags = new List<string> { "tag1", "tag2", "tag3" };
        var entryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(1),
            LocalCacheExpiration = TimeSpan.FromMinutes(1)
        };
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            entryOptions,
            tags,
            cancellationToken: token
        );
    }
    
    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        // Placeholder for retrieving data from a real backing store (database, HTTP API, etc.).
        // A real implementation would await that call. This stub returns synchronously and
        // intentionally has no await, so the compiler emits warning CS1998 for this method.
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}
```

Remove all entries for a specified tag by calling [RemoveByTagAsync](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache.removebytagasync) with the tag value. An overload lets you specify a collection of tag values.

Neither `IMemoryCache` nor `IDistributedCache` has direct support for the concept of tags, so tag-based invalidation is a logical operation, not a physical deletion. Calling `RemoveByTagAsync` doesn't remove values from either the local or distributed cache. Instead, it establishes an "ignore anything created before this point" rule for entries associated with that tag.

When `GetOrCreateAsync` later retrieves a cached value for that tag, `HybridCache` checks whether the value was created before the tag was invalidated. If so, the value is treated as a cache miss and the factory method is called to fetch fresh data. The old values remain in `IMemoryCache` and `IDistributedCache` until they expire in the usual way based on their configured lifetime.

## Removing all cache entries

The asterisk tag (`*`) is reserved as a wildcard and is disallowed against individual values. Calling `RemoveByTagAsync("*")` has the effect of invalidating *all* `HybridCache` data, even data that doesn't have any tags. As with individual tags, this is a *logical* operation, and individual values continue to exist until they expire naturally. Glob-style matches aren't supported. For example, you can't use `RemoveByTagAsync("foo*")` to remove everything starting with `foo`.

### Additional tag considerations

* The system doesn't limit the number of tags you can use, but large sets of tags might negatively impact performance.
* Tags can't be empty, just whitespace, or the reserved value `*`.

## Options

The `AddHybridCache` method can be used to configure global defaults. The following example shows how to configure some of the available options:

```
// Add services to the container.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddHybridCache(options =>
    {
        options.MaximumPayloadBytes = 1024 * 1024;
        options.MaximumKeyLength = 1024;
        options.DefaultEntryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(5),
            LocalCacheExpiration = TimeSpan.FromMinutes(5)
        };
    });
```

The `GetOrCreateAsync` method can also take a `HybridCacheEntryOptions` object to override the global defaults for a specific cache entry. Here's an example:

```
public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        var tags = new List<string> { "tag1", "tag2", "tag3" };
        var entryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(1),
            LocalCacheExpiration = TimeSpan.FromMinutes(1)
        };
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            entryOptions,
            tags,
            cancellationToken: token
        );
    }
    
    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        // Placeholder for retrieving data from a real backing store (database, HTTP API, etc.).
        // A real implementation would await that call. This stub returns synchronously and
        // intentionally has no await, so the compiler emits warning CS1998 for this method.
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}
```

For more information about the options, see the source code:

* [HybridCacheOptions](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcacheoptions) class.
* [HybridCacheEntryOptions](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcacheentryoptions) class.

## Limits

The following properties of `HybridCacheOptions` let you configure limits that apply to all cache entries:

* MaximumPayloadBytes - Maximum size of a cache entry. Default value is 1 MB. Attempts to store values over this size are logged, and the value isn't stored in cache.
* MaximumKeyLength - Maximum length of a cache key. Default value is 1024 characters. Attempts to store values over this size are logged, and the value isn't stored in cache.

## Serialization

Use of a secondary, out-of-process cache requires serialization. Serialization is configured as part of registering the `HybridCache` service. Type-specific and general-purpose serializers can be configured via the [AddSerializer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.hybridcachebuilderextensions.addserializer) and [AddSerializerFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.hybridcachebuilderextensions.addserializerfactory) methods, chained from the `AddHybridCache` call. By default, the library handles `string` and `byte[]` internally, and uses `System.Text.Json` for everything else. `HybridCache` can also use other serializers, such as protobuf or XML.

The following example configures the service to use a type-specific protobuf serializer:

```
// Add services to the container.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddHybridCache(options =>
    {
        options.DefaultEntryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromSeconds(10),
            LocalCacheExpiration = TimeSpan.FromSeconds(5)
        };
    }).AddSerializer<SomeProtobufMessage, 
        GoogleProtobufSerializer<SomeProtobufMessage>>();
```

The following example configures the service to use a general-purpose protobuf serializer that can handle many protobuf types:

```
// Add services to the container.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromSeconds(10),
        LocalCacheExpiration = TimeSpan.FromSeconds(5)
    };
}).AddSerializerFactory<GoogleProtobufSerializerFactory>();
```

The secondary cache requires a data store, such as Redis, SQL Server, or Postgres. To use [Azure Cache for Redis](https://azure.microsoft.com/products/cache), for example:

* Install the `Microsoft.Extensions.Caching.StackExchangeRedis` package.

* Create an instance of Azure Cache for Redis.

* Get a connection string that connects to the Redis instance. Find the connection string by selecting **Show access keys** on the **Overview** page in the Azure portal.

* Store the connection string in the app's configuration. For example, use a [user secrets file](../../security/app-secrets?view=aspnetcore-10.0) that looks like the following JSON, with the connection string in the `ConnectionStrings` section. Replace `<the connection string>` with the actual connection string:

  ```
  {
    "ConnectionStrings": {
      "RedisConnectionString": "<the connection string>"
    }
  }
  ```

* Register in DI the `IDistributedCache` implementation that the Redis package provides. To do that, call `AddStackExchangeRedisCache`, and pass in the connection string. For example:

  ```
  builder.Services.AddStackExchangeRedisCache(options =>
  {
      options.Configuration = 
          builder.Configuration.GetConnectionString("RedisConnectionString");
  });
  ```

* The Redis `IDistributedCache` implementation is now available from the app's DI container. `HybridCache` uses it as the secondary cache and uses the serializer configured for it.

For more information, see the [HybridCache serialization sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/hybrid/samples/9.x/HCMinimal2).

## Cache storage

By default `HybridCache` uses [MemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache) for its primary cache storage. Cache entries are stored in-process, so each server has a separate cache that's lost whenever the server process is restarted. For secondary out-of-process storage, such as Redis, SQL Server, or Postgres, `HybridCache` uses [the configured `IDistributedCache` implementation](distributed?view=aspnetcore-10.0), if any. But even without an `IDistributedCache` implementation, the `HybridCache` service still provides in-process caching and [stampede protection](https://en.wikipedia.org/wiki/Cache_stampede).

Note

When invalidating cache entries by key or by tags, they're invalidated in the current server and in the secondary out-of-process storage. However, the in-memory cache in other servers isn't affected.

Note

When the primary cache is the default [MemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache), `HybridCache` doesn't use the `IDistributedCache` implementation registered by [AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache) as its secondary cache. That implementation stores entries in the same process, so it would only duplicate the primary cache. If it's the only `IDistributedCache` registered, `HybridCache` caches in-process only, and cache entries aren't shared between servers. To share cache entries between servers, register an out-of-process implementation, such as Redis.

## Optimize performance

To optimize performance, configure `HybridCache` to reuse objects and avoid `byte[]` allocations.

### Reuse objects

By reusing instances, `HybridCache` can reduce the overhead of CPU and object allocations associated with per-call deserialization. This can lead to performance improvements in scenarios where the cached objects are large or accessed frequently.

In typical existing code that uses `IDistributedCache`, every retrieval of an object from the cache results in deserialization. This behavior means that each concurrent caller gets a separate instance of the object, which can't interact with other instances. The result is thread safety, as there's no risk of concurrent modifications to the same object instance.

Because much `HybridCache` usage will be adapted from existing `IDistributedCache` code, `HybridCache` preserves this behavior by default to avoid introducing concurrency bugs. However, objects are inherently thread-safe if:

* They're immutable types.
* The code doesn't modify them.

In such cases, inform `HybridCache` that it's safe to reuse instances by making both of the following changes:

* Marking the type as `sealed`. The `sealed` keyword in C# means that the class can't be inherited.
* Applying the `[ImmutableObject(true)]` attribute to the type. The `[ImmutableObject(true)]` attribute indicates that the object's state can't be changed after it's created.

### Avoid `byte[]` allocations

`HybridCache` also provides optional APIs for `IDistributedCache` implementations, to avoid `byte[]` allocations. This feature is implemented by the preview versions of the `Microsoft.Extensions.Caching.StackExchangeRedis`, `Microsoft.Extensions.Caching.SqlServer`, and `Microsoft.Extensions.Caching.Postgres` packages. For more information, see [IBufferDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.ibufferdistributedcache). Here are the .NET CLI commands to install the packages:

```
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
```

```
dotnet add package Microsoft.Extensions.Caching.SqlServer
```

```
dotnet add package Microsoft.Extensions.Caching.Postgres
```

## Custom HybridCache implementations

A concrete implementation of the `HybridCache` abstract class is included in the shared framework and is provided via dependency injection. But developers are welcome to provide or consume custom implementations of the API, for example [FusionCache](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/MicrosoftHybridCache.md).

## Use Hybrid Cache with Native AOT

The following Native AOT-specific considerations apply to `HybridCache`:

* **Serialization**

  Native AOT doesn't support runtime reflection-based serialization. If you cache custom types, you must use source generators or explicitly configure serializers that are compatible with AOT, like `System.Text.Json` source generation. `HybridCache` is still under development, and simplifying the way to use it with AOT is a high priority for that development. For more information, see pull request [dotnet/extensions#6475](https://github.com/dotnet/extensions/pull/6475)

* **Trimming**

  Make sure all types you cache are referenced in a way that prevents them from being trimmed by the AOT compiler. Using source generators for serialization helps with this requirement. For more information, see [ASP.NET Core support for Native AOT](../../fundamentals/native-aot?view=aspnetcore-10.0).

If you set up serialization and trimming correctly, `HybridCache` behaves the same way in Native AOT as in regular ASP.NET Core apps.

## Compatibility

The `HybridCache` library supports older .NET runtimes, down to .NET Framework 4.7.2 and .NET Standard 2.0.

## Related content

For more information, see [the `HybridCache` source code](https://source.dot.net/#Microsoft.Extensions.Caching.Abstractions/Hybrid/HybridCache.cs)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-06

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/synchronization-context
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/synchronization-context.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor synchronization context

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Blazor uses a synchronization context ([SynchronizationContext](/en-us/dotnet/api/system.threading.synchronizationcontext)) to enforce a single logical thread of execution. A component's [lifecycle methods](lifecycle?view=aspnetcore-10.0) and event callbacks raised by Blazor are executed on the synchronization context.

Blazor's server-side synchronization context attempts to emulate a single-threaded environment so that it closely matches the WebAssembly model in the browser, which is single threaded. This emulation is scoped only to an individual circuit, meaning two different circuits can run in parallel. At any given point in time within a circuit, work is performed on exactly one thread, which yields the impression of a single logical thread. No two operations execute concurrently within the same circuit.

A single logical thread of execution doesn't imply a single asynchronous control flow. A component is re-entrant at any point where it awaits an incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task). [Lifecycle methods](lifecycle?view=aspnetcore-10.0) or [component disposal methods](component-disposal?view=aspnetcore-10.0) may be called before the asynchronous control flow resumes after awaiting a [Task](/en-us/dotnet/api/system.threading.tasks.task) to complete. Therefore, a component must ensure that it's in a valid state before awaiting a potentially incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task). In particular, a component must ensure that it's in a valid state for rendering when [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) or [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) return. If either of these methods return an incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task), they must ensure that the part of the method that completes synchronously leaves the component in a valid state for rendering.

Another implication of re-entrant components is that a method can't defer a [Task](/en-us/dotnet/api/system.threading.tasks.task) until after the method returns by passing it to [ComponentBase.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync). Calling [ComponentBase.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync) may only defer the [Task](/en-us/dotnet/api/system.threading.tasks.task) until the next [`await` operator](/en-us/dotnet/csharp/language-reference/operators/await) is reached.

Components might implement [IDisposable](/en-us/dotnet/api/system.idisposable) or [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable) to call asynchronous methods using a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) from a [CancellationTokenSource](/en-us/dotnet/api/system.threading.cancellationtokensource) that's canceled when the component is disposed. However, this really depends on the scenario. It's up to the component author to determine whether that's the correct behavior. For example, if implementing a `SaveButton` component that persists some local data to a database when a save button is selected, the component author may indeed intend to discard the changes if the user selects the button and quickly navigates to another page, which could dispose the component before the asynchronous save completes.

A disposable component can check for disposal after awaiting any [Task](/en-us/dotnet/api/system.threading.tasks.task) that doesn't receive the component's [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken). Incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task)s may also prevent garbage collection of a disposed component.

[ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) ignores exceptions caused by [Task](/en-us/dotnet/api/system.threading.tasks.task) cancellation (more precisely, it ignores *all* exceptions if the awaited [Task](/en-us/dotnet/api/system.threading.tasks.task)s are canceled), so component methods don't need to handle [TaskCanceledException](/en-us/dotnet/api/system.threading.tasks.taskcanceledexception) and [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception).

[ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) can't follow the preceding guidelines because it doesn't conceptualize what constitutes a valid state for a derived component and it doesn't itself implement [IDisposable](/en-us/dotnet/api/system.idisposable) or [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable). If [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) returns an incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task) that doesn't use a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) and the component is disposed before the [Task](/en-us/dotnet/api/system.threading.tasks.task) completes, [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) still calls [OnParametersSet](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparametersset) and awaits [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync). If a disposable component doesn't use a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken), [OnParametersSet](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparametersset) and [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) should check if the component is disposed.

## Avoid thread-blocking calls

Generally, don't call the following methods in components. The following methods block the execution thread and thus block the app from resuming work until the underlying [Task](/en-us/dotnet/api/system.threading.tasks.task) is complete:

* [Result](/en-us/dotnet/api/system.threading.tasks.task-1.result)
* [Wait](/en-us/dotnet/api/system.threading.tasks.task.wait)
* [WaitAny](/en-us/dotnet/api/system.threading.tasks.task.waitany)
* [WaitAll](/en-us/dotnet/api/system.threading.tasks.task.waitall)
* [Sleep](/en-us/dotnet/api/system.threading.thread.sleep)
* [GetResult](/en-us/dotnet/api/system.runtime.compilerservices.taskawaiter.getresult)

Note

Blazor documentation examples that use the thread-blocking methods mentioned in this section are only using the methods for demonstration purposes, not as recommended coding guidance. For example, a few component code demonstrations simulate a long-running process by calling [Thread.Sleep](/en-us/dotnet/api/system.threading.thread.sleep).

## Invoke component methods externally to update state

In the event a component must be updated based on an external event, such as a timer or other notification, use the `InvokeAsync` method, which dispatches code execution to Blazor's synchronization context. For example, consider the following *notifier service* that can notify any listening component about updated state. The `Update` method can be called from anywhere in the app.

`TimerService.cs`:

```
namespace BlazorSample;

public class TimerService(NotifierService notifier, 
    ILogger<TimerService> logger) : IDisposable
{
    private int elapsedCount;
    private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger = logger;
    private readonly NotifierService notifier = notifier;
    private PeriodicTimer? timer;

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("ElapsedCount {Count}", elapsedCount);
                }
            }
        }
    }

    public void Stop()
    {
        if (timer is not null)
        {
            timer.Dispose();
            timer = null;
            logger.LogInformation("Stopped");
        }
    }

    public void Dispose()
    {
        timer?.Dispose();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
```

```
namespace BlazorSample;

public class TimerService(NotifierService notifier, 
    ILogger<TimerService> logger) : IDisposable
{
    private int elapsedCount;
    private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger = logger;
    private readonly NotifierService notifier = notifier;
    private PeriodicTimer? timer;

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("ElapsedCount {Count}", elapsedCount);
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
```

```
public class TimerService : IDisposable
{
    private int elapsedCount;
    private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private PeriodicTimer? timer;

    public TimerService(NotifierService notifier,
        ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
```

```
public class TimerService : IDisposable
{
    private int elapsedCount;
    private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private PeriodicTimer? timer;

    public TimerService(NotifierService notifier,
        ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
```

```
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
```

```
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new Timer();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
```

`NotifierService.cs`:

```
namespace BlazorSample;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
```

```
namespace BlazorSample;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
```

```
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
```

```
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
```

```
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}
```

```
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}
```

Register the services:

* For client-side development, register the services as singletons in the client-side `Program` file:

  ```
  builder.Services.AddSingleton<NotifierService>();
  builder.Services.AddSingleton<TimerService>();
  ```

* For server-side development, register the services as scoped in the server `Program` file:

  ```
  builder.Services.AddScoped<NotifierService>();
  builder.Services.AddScoped<TimerService>();
  ```

Use the `NotifierService` to update a component.

`Notifications.razor`:

```
@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<PageTitle>Notifications</PageTitle>

<h1>Notifications Example</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<button @onclick="StopTimer">Stop Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized() => Notifier.Notify += OnNotify;

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer() => _ = Task.Run(Timer.Start);

    private void StopTimer() => Timer.Stop();

    public void Dispose() => Notifier.Notify -= OnNotify;
}
```

`Notifications.razor`:

```
@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<PageTitle>Notifications</PageTitle>

<h1>Notifications Example</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized() => Notifier.Notify += OnNotify;

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer() => _ = Task.Run(Timer.Start);

    public void Dispose() => Notifier.Notify -= OnNotify;
}
```

`ReceiveNotifications.razor`:

```
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}
```

`ReceiveNotifications.razor`:

```
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}
```

`ReceiveNotifications.razor`:

```
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        Timer.Start();
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}
```

`ReceiveNotifications.razor`:

```
@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key != null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        Timer.Start();
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}
```

In the preceding example:

* The timer is initiated outside of Blazor's synchronization context with `_ = Task.Run(Timer.Start)`.
* `NotifierService` invokes the component's `OnNotify` method. `InvokeAsync` is used to switch to the correct context and enqueue a rerender. For more information, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0).
* The component implements [IDisposable](/en-us/dotnet/api/system.idisposable). The `OnNotify` delegate is unsubscribed in the `Dispose` method, which is called by the framework when the component is disposed. For more information, see [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

- `NotifierService` invokes the component's `OnNotify` method outside of Blazor's synchronization context. `InvokeAsync` is used to switch to the correct context and enqueue a rerender. For more information, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0).
- The component implements [IDisposable](/en-us/dotnet/api/system.idisposable). The `OnNotify` delegate is unsubscribed in the `Dispose` method, which is called by the framework when the component is disposed. For more information, see [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

Important

If a Razor component defines an event that's triggered from a background thread, the component might be required to capture and restore the execution context ([ExecutionContext](/en-us/dotnet/api/system.threading.executioncontext)) at the time the handler is registered. For more information, see [Calling `InvokeAsync(StateHasChanged)` causes page to fallback to default culture (`dotnet/aspnetcore` #28521)](https://github.com/dotnet/aspnetcore/issues/28521#issuecomment-1112513408).

To dispatch caught exceptions from the background `TimerService` to the component to treat the exceptions like normal lifecycle event exceptions, see the [Handle caught exceptions outside of a Razor component's lifecycle](#handle-caught-exceptions-outside-of-a-razor-components-lifecycle) section.

## Handle caught exceptions outside of a Razor component's lifecycle

Use [ComponentBase.DispatchExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.dispatchexceptionasync) in a Razor component to process exceptions thrown outside of the component's lifecycle call stack. This permits the component's code to treat exceptions as though they're lifecycle method exceptions. Thereafter, Blazor's error handling mechanisms, such as [error boundaries](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries), can process the exceptions.

Note

[ComponentBase.DispatchExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.dispatchexceptionasync) is used in Razor component files (`.razor`) that inherit from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase). When creating components that [implement ](./?view=aspnetcore-10.0#component-classes)[IComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.icomponent) directly, use [RenderHandle.DispatchExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.renderhandle.dispatchexceptionasync).

To handle caught exceptions outside of a Razor component's lifecycle, pass the exception to [DispatchExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.renderhandle.dispatchexceptionasync) and await the result:

```
try
{
    ...
}
catch (Exception ex)
{
    await DispatchExceptionAsync(ex);
}
```

A common scenario for the preceding approach is when a component starts an asynchronous operation but doesn't await a [Task](/en-us/dotnet/api/system.threading.tasks.task), often called the *fire and forget* pattern because the method is *fired* (started) and the result of the method is *forgotten* (thrown away). If the operation fails, you may want the component to treat the failure as a component lifecycle exception for any of the following goals:

* Put the component into a faulted state, for example, to trigger an [error boundary](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries).
* Terminate the circuit if there's no error boundary.
* Trigger the same logging that occurs for lifecycle exceptions.

In the following example, the user selects the **Send report** button to trigger a background method, `ReportSender.SendAsync`, that sends a report. In most cases, a component awaits the [Task](/en-us/dotnet/api/system.threading.tasks.task) of an asynchronous call and updates the UI to indicate the operation completed. In the following example, the `SendReport` method doesn't await a [Task](/en-us/dotnet/api/system.threading.tasks.task) and doesn't report the result to the user. Because the component intentionally discards the [Task](/en-us/dotnet/api/system.threading.tasks.task) in `SendReport`, any asynchronous failures occur off of the normal lifecycle call stack, hence aren't seen by Blazor:

```
<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = ReportSender.SendAsync();
    }
}
```

To treat failures like lifecycle method exceptions, explicitly dispatch exceptions back to the component with [DispatchExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.dispatchexceptionasync), as the following example demonstrates:

```
<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = SendReportAsync();
    }

    private async Task SendReportAsync()
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    }
}
```

An alternative approach leverages [Task.Run](/en-us/dotnet/api/system.threading.tasks.task.run):

```
private void SendReport()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}
```

For a working demonstration, implement the timer notification example in [Invoke component methods externally to update state](synchronization-context?view=aspnetcore-10.0#invoke-component-methods-externally-to-update-state). In a Blazor app, add the following files from the timer notification example and register the services in the `Program` file as the section explains:

* `TimerService.cs`
* `NotifierService.cs`
* `Notifications.razor`

The example uses a timer outside of a Razor component's lifecycle, where an unhandled exception normally isn't processed by Blazor's error handling mechanisms, such as an [error boundary](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries).

First, change the code in `TimerService.cs` to create an artificial exception outside of the component's lifecycle. In the `while` loop of `TimerService.cs`, throw an exception when the `elapsedCount` reaches a value of two:

```
if (elapsedCount == 2)
{
    throw new Exception("I threw an exception! Somebody help me!");
}
```

Place an [error boundary](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries) in the app's main layout. Replace the `<article>...</article>` markup with the following markup.

In `MainLayout.razor`:

```
<article class="content px-4">
    <ErrorBoundary>
        <ChildContent>
            @Body
        </ChildContent>
        <ErrorContent>
            <p class="alert alert-danger" role="alert">
                Oh, dear! Oh, my! - George Takei
            </p>
        </ErrorContent>
    </ErrorBoundary>
</article>
```

In Blazor Web Apps with the error boundary only applied to a static `MainLayout` component, the boundary is only active during the static server-side rendering (static SSR) phase. The boundary doesn't activate just because a component further down the component hierarchy is interactive. To enable interactivity broadly for the `MainLayout` component and the rest of the components further down the component hierarchy, enable interactive rendering for the `HeadOutlet` and `Routes` component instances in the `App` component (`Components/App.razor`). The following example adopts the Interactive Server (`InteractiveServer`) render mode:

```
<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />
```

If you run the app at this point, the exception is thrown when the elapsed count reaches a value of two. However, the UI doesn't change. The error boundary doesn't show the error content.

To dispatch exceptions from the timer service back to the `Notifications` component, the following changes are made to the component:

* Start the timer in a [`try-catch` statement](/en-us/dotnet/csharp/language-reference/statements/exception-handling-statements#the-try-catch-statement). In the `catch` clause of the `try-catch` block, exceptions are dispatched back to the component by passing the [Exception](/en-us/dotnet/api/system.exception) to [DispatchExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.dispatchexceptionasync) and awaiting the result.
* In the `StartTimer` method, start the asynchronous timer service in the [Action](/en-us/dotnet/api/system.action) delegate of [Task.Run](/en-us/dotnet/api/system.threading.tasks.task.run) and intentionally discard the returned [Task](/en-us/dotnet/api/system.threading.tasks.task).

The `StartTimer` method of the `Notifications` component (`Notifications.razor`):

```
private void StartTimer()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await Timer.Start();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}
```

When the timer service executes and reaches a count of two, the exception is dispatched to the Razor component, which in turn triggers the error boundary to display the error content of the `<ErrorBoundary>` in the `MainLayout` component:

> Oh, dear! Oh, my! - George Takei

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata-in-mvc
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata-in-mvc.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Custom authorization policies with `IAuthorizationRequirementData` in ASP.NET Core MVC

This article provides a demonstration on how to use [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) to define custom authorization policies in ASP.NET Core MVC. For general guidance on this subject, see [Custom authorization policies with \`IAuthorizationRequirementData\`](../../../security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata?view=aspnetcore-10.0).

## Sample app

The MVC sample for this article is the [`AuthRequirementsData` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/AuthRequirementsData) ([how to download](../../../overview?view=aspnetcore-10.0#how-to-download-a-sample)). The sample app implements a minimum age handler for users, requiring a user to present a birth date claim indicating that they're at least 21 years old.

## Demonstration

Test the sample with [`dotnet user-jwts`](../../../security/authentication/jwt-authn?view=aspnetcore-10.0) and curl.

From the project's folder in a command shell, execute the following command to create a JWT bearer token with a birth date claim that makes the user over 21 years old:

```
dotnet user-jwts create --claim http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth=1989-01-01
```

The output produces a token after "`Token:`" in the command shell:

```
New JWT saved with ID '{JWT ID}'.
Name: {USER}
Custom Claims: [http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth=1989-01-01]

Token: {TOKEN}
```

Set the value of the token (where the `{TOKEN}` placeholder appears in the preceding output) aside for use later.

You can decode the token in an online JWT decoder, such as [`jwt.ms`](https://jwt.ms/) to see its contents, revealing that it contains a `birthdate` claim with the user's birth date:

```
{
  "alg": "HS256",
  "typ": "JWT"
}.{
  "unique_name": "guard",
  "sub": "guard",
  "jti": "6cd613ed",
  "birthdate": "1989-01-01",
  "aud": [
    "https://localhost:5001",
    "http://localhost:5000"
  ],
  "nbf": 1773663513,
  "exp": 1781612313,
  "iat": 1773663515,
  "iss": "dotnet-user-jwts"
}.[Signature]
```

Execute the command again with a `dateofbirth` value that makes the user under the age of 21:

```
dotnet user-jwts create --claim http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth=2020-01-01
```

Set the value of the second token aside.

Start the app in Visual Studio or with the `dotnet watch` command in a command shell:

```
dotnet watch
```

In a command shell, use the .NET CLI to execute the following `curl.exe` command to request the `api/greetings/hello` endpoint. Replace the `{TOKEN}` placeholder with the first JWT bearer token that you saved earlier:

```
curl.exe -i -H "Authorization: Bearer {TOKEN}" https://localhost:5001/api/greetings/hello
```

The output indicates success because the user's birth date claim indicates that they're at least 21 years old:

```
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Thu, 15 May 2025 22:58:10 GMT
Server: Kestrel
Transfer-Encoding: chunked

Hello {USER}!
```

Logging indicates that the age requirement was met:

> MinimumAgeAuthorizationHandler: Information: Evaluating authorization requirement for age >= 21\
> MinimumAgeAuthorizationHandler: Information: Minimum age authorization requirement 21 satisfied

Re-execute the `curl.exe` command with the second token, which indicates the user is under 21 years old. The output indicates that the requirement isn't met. Access to the endpoint is forbidden (status code 403):

```
HTTP/1.1 403 Forbidden
Content-Length: 0
Date: Thu, 15 May 2025 22:58:36 GMT
Server: Kestrel
```

Logging indicates that the age requirement wasn't met:

> MinimumAgeAuthorizationHandler: Information: Evaluating authorization requirement for age >= 21\
> MinimumAgeAuthorizationHandler: Information: Current user's DateOfBirth claim (2020-01-01) doesn't satisfy the minimum age authorization requirement 21

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-03-16

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/httpsys
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/httpsys.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# HTTP.sys web server implementation in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Tom Dykstra](https://github.com/tdykstra) and [Chris Ross](https://github.com/Tratcher)

[HTTP.sys](/en-us/iis/get-started/introduction-to-iis/introduction-to-iis-architecture#hypertext-transfer-protocol-stack-httpsys) is a [web server for ASP.NET Core](./?view=aspnetcore-10.0) that only runs on Windows. HTTP.sys is an alternative to [Kestrel](kestrel?view=aspnetcore-10.0) server and offers some features that Kestrel doesn't provide.

Important

HTTP.sys isn't compatible with the [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) and can't be used with IIS or IIS Express.

HTTP.sys supports the following features:

* [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0)
* Port sharing
* HTTPS with SNI
* HTTP/2 over TLS (Windows 10 or later)
* HTTP/3 over TLS (Windows 11 or later)
* Direct file transmission
* Response caching
* WebSockets (Windows 8 or later)
* Customizable security descriptors
* Automatic memory pool eviction

Supported Windows versions:

* Windows 7 or later
* Windows Server 2008 R2 or later

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/servers/httpsys/samples) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## When to use HTTP.sys

HTTP.sys is useful for deployments where:

* There's a need to expose the server directly to the Internet without using IIS.

* An internal deployment requires a feature not available in Kestrel. For more information, see [Kestrel vs. HTTP.sys](./?view=aspnetcore-10.0#kestrel-vs-httpsys)

HTTP.sys is mature technology that protects against many types of attacks and provides the robustness, security, and scalability of a full-featured web server. IIS itself runs as an HTTP listener on top of HTTP.sys.

## HTTP/2 support

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is enabled for ASP.NET Core apps when the following base requirements are met:

* Windows Server 2016/Windows 10 or later
* [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) connection
* TLS 1.2 or later connection

If an HTTP/2 connection is established, [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) reports `HTTP/2`.

HTTP/2 is enabled by default. If an HTTP/2 connection isn't established, the connection falls back to HTTP/1.1. In a future release of Windows, HTTP/2 configuration flags will be available, including the ability to disable HTTP/2 with HTTP.sys.

## HTTP/3 support

[HTTP/3](https://datatracker.ietf.org/doc/rfc9114/) is enabled for ASP.NET Core apps when the following base requirements are met:

* Windows Server 2022/Windows 11 or later
* An `https` url binding is used.
* The [EnableHttp3 registry key](https://techcommunity.microsoft.com/t5/networking-blog/enabling-http-3-support-on-windows-server-2022/ba-p/2676880) is set.

The preceding Windows 11 Build versions may require the use of a [Windows Insider](https://www.microsoft.com/en-us/windowsinsider/) build.

HTTP/3 is discovered as an upgrade from HTTP/1.1 or HTTP/2 via the `alt-svc` header. That means the first request will normally use HTTP/1.1 or HTTP/2 before switching to HTTP/3. Http.Sys doesn't automatically add the `alt-svc` header, it must be added by the application. The following code is a middleware example that adds the `alt-svc` response header.

```
app.Use((context, next) =>
{
    context.Response.Headers.AltSvc = "h3=\":443\"";
    return next(context);
});
```

Place the preceding code early in the request pipeline.

Http.Sys also supports sending an AltSvc HTTP/2 protocol message rather than a response header to notify the client that HTTP/3 is available. See the [EnableAltSvc registry key](https://techcommunity.microsoft.com/t5/networking-blog/enabling-http-3-support-on-windows-server-2022/ba-p/2676880).

Note

This requires netsh sslcert bindings that use host names rather than IP addresses. Replace `ipport` with `hostnameport` in the `netsh http add sslcert` commands below, and replace the IP address with the hostname, for example, `www.example.com`. There's also a known issue where using `hostnameport` fails unless the `certstorename` parameter is specified. By default, use `certstorename=MY`.

## Kernel mode authentication with Kerberos

HTTP.sys delegates to kernel mode authentication with the Kerberos authentication protocol. User mode authentication isn't supported with Kerberos and HTTP.sys. The machine account must be used to decrypt the Kerberos token/ticket that's obtained from Active Directory and forwarded by the client to the server to authenticate the user. Register the Service Principal Name (SPN) for the host, not the user of the app.

### Enable channel binding token (CBT) hardening

Channel binding tokens (CBT) tie Windows authentication to the underlying TLS channel, which helps mitigate NTLM relay and man-in-the-middle attacks. For HTTPS endpoints that use Windows authentication with HTTP.sys, you can opt in to CBT hardening by setting the `Microsoft.AspNetCore.Server.HttpSys.EnableCBTHardening` AppContext switch to `true`.

Enable the switch in your project's `runtimeconfig.template.json` file:

```
{
  "configProperties": {
    "Microsoft.AspNetCore.Server.HttpSys.EnableCBTHardening": true
  }
}
```

Or set the switch programmatically before building the host in `Program.cs`:

```
AppContext.SetSwitch("Microsoft.AspNetCore.Server.HttpSys.EnableCBTHardening", true);
```

Warning

CBT hardening is off by default. Enabling it can cause Windows authentication to fail for clients or proxies that don't support channel binding. Test thoroughly in your environment before enabling in production.

### Support for kernel-mode response buffering

In some scenarios, high volumes of small writes with high latency can cause significant performance impact to `HTTP.sys`. This impact is due to the lack of a [Pipe](/en-us/dotnet/api/system.io.pipelines.pipe) buffer in the `HTTP.sys` implementation. To improve performance in these scenarios, support for response buffering is included in `HTTP.sys`. Enable buffering by setting [HttpSysOptions.EnableKernelResponseBuffering](https://github.com/dotnet/aspnetcore/blob/main/src/Servers/HttpSys/src/HttpSysOptions.cs#L120) to `true`. Response buffering should be enabled by an app that does synchronous I/O, or asynchronous I/O with no more than one outstanding write at a time. In these scenarios, response buffering can significantly improve throughput over high-latency connections.

Apps that use asynchronous I/O and that may have more than one write outstanding at a time should ***not*** use this flag. Enabling this flag can result in higher CPU and memory usage by HTTP.Sys.

## How to use HTTP.sys

### Configure the ASP.NET Core app to use HTTP.sys

Call the [UseHttpSys](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderhttpsysextensions.usehttpsys) extension method when building the host, specifying any required [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions). The following example sets options to their default values:

```
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys(options =>
{
    options.AllowSynchronousIO = false;
    options.Authentication.Schemes = AuthenticationSchemes.None;
    options.Authentication.AllowAnonymous = true;
    options.MaxConnections = null;
    options.MaxRequestBodySize = 30_000_000;
    options.UrlPrefixes.Add("http://localhost:5005");
});

builder.Services.AddRazorPages();

var app = builder.Build();
```

Additional HTTP.sys configuration is handled through [registry settings](https://support.microsoft.com/help/820129/http-sys-registry-settings-for-windows).

For more information about HTTP.sys options, see [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions).

### Customize security descriptors

A *request queue* in HTTP.sys is a kernel-level structure that temporarily stores incoming HTTP requests until your application is ready to process them. Manage access to the request queue by using the [RequestQueueSecurityDescriptor](https://source.dot.net/#Microsoft.AspNetCore.Server.HttpSys/HttpSysOptions.cs,a556950881fd2d87) property on [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions). Set it to a [GenericSecurityDescriptor](/en-us/dotnet/api/system.security.accesscontrol.genericsecuritydescriptor) instance when configuring your HTTP.sys server.

By customizing the security descriptor, you can allow or deny specific users or groups access to the request queue. This is useful in scenarios where you want to restrict or delegate HTTP.sys request handling at the operating system level.

For example, the following code allows all authenticated users but denies guests:

```
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.AspNetCore.Server.HttpSys;

// Create a new security descriptor
var securityDescriptor = new CommonSecurityDescriptor(isContainer: false, isDS: false, sddlForm: string.Empty);

// Create a discretionary access control list (DACL)
var dacl = new DiscretionaryAcl(isContainer: false, isDS: false, capacity: 2);
dacl.AddAccess(
    AccessControlType.Allow,
    new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),
    -1,
    InheritanceFlags.None,
    PropagationFlags.None
);
dacl.AddAccess(
    AccessControlType.Deny,
    new SecurityIdentifier(WellKnownSidType.BuiltinGuestsSid, null),
    -1,
    InheritanceFlags.None,
    PropagationFlags.None
);

// Assign the DACL to the security descriptor
securityDescriptor.DiscretionaryAcl = dacl;

// Configure HTTP.sys options
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseHttpSys(options =>
{
    options.RequestQueueSecurityDescriptor = securityDescriptor;
});
```

The `RequestQueueSecurityDescriptor` property applies only when creating a new request queue. The property doesn't affect existing request queues.

[]()

**MaxRequestBodySize**

The maximum allowed size of any request body in bytes. When set to `null`, the maximum request body size is unlimited. This limit has no effect on upgraded connections, which are always unlimited.

The recommended method to override the limit in an ASP.NET Core MVC app for a single `IActionResult` is to use the [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) attribute on an action method:

```
[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()
```

An exception is thrown if the app attempts to configure the limit on a request after the app has started reading the request. An `IsReadOnly` property can be used to indicate if the `MaxRequestBodySize` property is in a read-only state, meaning it's too late to configure the limit.

If the app should override [MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.maxrequestbodysize#microsoft-aspnetcore-server-httpsys-httpsysoptions-maxrequestbodysize) per-request, use the [IHttpMaxRequestBodySizeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature):

```
app.Use((context, next) =>
{
    context.Features.GetRequiredFeature<IHttpMaxRequestBodySizeFeature>()
                                             .MaxRequestBodySize = 10 * 1024;

    var server = context.RequestServices
        .GetRequiredService<IServer>();
    var serverAddressesFeature = server.Features
                                 .GetRequiredFeature<IServerAddressesFeature>();

    var addresses = string.Join(", ", serverAddressesFeature.Addresses);

    var loggerFactory = context.RequestServices
        .GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("Sample");

    logger.LogInformation("Addresses: {addresses}", addresses);

    return next(context);
});
```

If using Visual Studio, make sure the app isn't configured to run IIS or IIS Express.

In Visual Studio, the default launch profile is for IIS Express. To run the project as a console app, manually change the selected profile, as shown in the following screenshot:

### Configure Windows Server

1. Determine the ports to open for the app and use [Windows Firewall](/en-us/windows/security/threat-protection/windows-firewall/create-an-inbound-port-rule) or the [New-NetFirewallRule](/en-us/powershell/module/netsecurity/new-netfirewallrule) PowerShell cmdlet to open firewall ports to allow traffic to reach HTTP.sys. In the following commands and app configuration, port 443 is used.

2. When deploying to an Azure VM, open the ports in the [Network Security Group](/en-us/azure/virtual-machines/windows/nsg-quickstart-portal). In the following commands and app configuration, port 443 is used.

3. Obtain and install X.509 certificates, if required.

   On Windows, create self-signed certificates using the [New-SelfSignedCertificate PowerShell cmdlet](/en-us/powershell/module/pki/new-selfsignedcertificate). For an unsupported example, see [UpdateIISExpressSSLForChrome.ps1](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/includes/make-x509-cert/UpdateIISExpressSSLForChrome.ps1).

   Install either self-signed or CA-signed certificates in the server's **Local Machine** > **Personal** store.

4. If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd), install .NET, .NET Framework, or both (if the app is a .NET app targeting the .NET Framework).

   * **.NET**: If the app requires .NET, obtain and run the **.NET Runtime** installer from [.NET Downloads](https://dotnet.microsoft.com/download). Don't install the full SDK on the server.
   * **.NET Framework**: If the app requires .NET Framework, see the [.NET Framework installation guide](/en-us/dotnet/framework/install/). Install the required .NET Framework. The installer for the latest .NET Framework is available from the [.NET Downloads](https://dotnet.microsoft.com/download) page.

   If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd), the app includes the runtime in its deployment. No framework installation is required on the server.

5. Configure URLs and ports in the app.

   By default, ASP.NET Core binds to `http://localhost:5000`. To configure URL prefixes and ports, options include:

   * [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls)
   * `urls` command-line argument
   * `ASPNETCORE_URLS` environment variable
   * [UrlPrefixes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.urlprefixes#microsoft-aspnetcore-server-httpsys-httpsysoptions-urlprefixes)

   The following code example shows how to use [UrlPrefixes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.urlprefixes#microsoft-aspnetcore-server-httpsys-httpsysoptions-urlprefixes) with the server's local IP address `10.0.0.4` on port 443:

   ```
   var builder = WebApplication.CreateBuilder(args);

   builder.WebHost.UseHttpSys(options =>
   {
       options.UrlPrefixes.Add("https://10.0.0.4:443");
   });

   builder.Services.AddRazorPages();

   var app = builder.Build();
   ```

   An advantage of `UrlPrefixes` is that an error message is generated immediately for improperly formatted prefixes.

   The settings in `UrlPrefixes` override `UseUrls`/`urls`/`ASPNETCORE_URLS` settings. Therefore, an advantage of `UseUrls`, `urls`, and the `ASPNETCORE_URLS` environment variable is that it's easier to switch between Kestrel and HTTP.sys.

   HTTP.sys recognizes two types of wild cards in URL prefixes:

   * `*` is a *weak binding*, also known as a *fallback binding*. If the URL prefix is `http://*:5000`, and something else is bound to port 5000, this binding won't be used.
   * `+` is a *strong binding*. If the URL prefix is `http://+:5000`, this binding will be used before other port 5000 bindings.

   For more information, see [UrlPrefix Strings](/en-us/windows/win32/http/urlprefix-strings).

   Warning

   Top-level wildcard bindings (`http://*:80/` and `http://+:80`) should **not** be used. Top-level wildcard bindings create app security vulnerabilities. This applies to both strong and weak wildcards. Use explicit host names or IP addresses rather than wildcards. Subdomain wildcard binding (for example, `*.mysub.com`) isn't a security risk if you control the entire parent domain (as opposed to `*.com`, which is vulnerable). For more information, see [RFC 9110: Section 7.2: Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#field.host).

   Most configurations for apps and containers define only a port for listening, like port 80, without specifying other constraints like the host or path. `HTTP_PORTS` and `HTTPS_PORTS` are config keys that specify the listening ports for the Kestrel and HTTP.sys servers. You can specify the keys as environment variables defined with the `DOTNET_` or `ASPNETCORE_` prefixes, or set them directly through any other config input, such as the `appsettings.json` file. Each configuration is a semicolon-delimited list of port values, as shown in the following example:

   ```
   ASPNETCORE_HTTP_PORTS=80;8080
   ASPNETCORE_HTTPS_PORTS=443;8081
   ```

   The configuration in the example is shorthand for the following specification, which defines the scheme (HTTP or HTTPS) and any host or IP:

   ```
   ASPNETCORE_URLS=http://*:80/;http://*:8080/;https://*:443/;https://*:8081/
   ```

   The `HTTP_PORTS` and `HTTPS_PORTS` configuration keys are lower priority. If other URLs or values are set directly in code, they can override the configuration keys. Configure certificates separately using server-specific mechanics for HTTPS.

   Note

   Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

   These configuration keys are equivalent to top-level wildcard bindings. They're convenient for development and container scenarios, but avoid wildcards when running on a machine that may also host other services.

6. Preregister URL prefixes on the server.

   The built-in tool for configuring HTTP.sys is *netsh.exe*. *netsh.exe* is used to reserve URL prefixes and assign X.509 certificates. The tool requires administrator privileges.

   Use the *netsh.exe* tool to register URLs for the app:

   ```
   netsh http add urlacl url=<URL> user=<USER>
   ```

   * `<URL>`: The fully qualified Uniform Resource Locator (URL). Don't use a wildcard binding. Use a valid hostname or local IP address. *The URL must include a trailing slash.*
   * `<USER>`: Specifies the user or user-group name.

   In the following example, the local IP address of the server is `10.0.0.4`:

   ```
   netsh http add urlacl url=https://10.0.0.4:443/ user=Users
   ```

   When a URL is registered, the tool responds with `URL reservation successfully added`.

   To delete a registered URL, use the `delete urlacl` command:

   ```
   netsh http delete urlacl url=<URL>
   ```

7. Register X.509 certificates on the server.

   Use the *netsh.exe* tool to register certificates for the app:

   ```
   netsh http add sslcert ipport=<IP>:<PORT> certhash=<THUMBPRINT> appid="{<GUID>}"
   ```

   * `<IP>`: Specifies the local IP address for the binding. Don't use a wildcard binding. Use a valid IP address.
   * `<PORT>`: Specifies the port for the binding.
   * `<THUMBPRINT>`: The X.509 certificate thumbprint.
   * `<GUID>`: A developer-generated GUID to represent the app for informational purposes.

   For reference purposes, store the GUID in the app as a package tag:

   * In Visual Studio:

     * Open the app's project properties by right-clicking on the app in **Solution Explorer** and selecting **Properties**.
     * Select the **Package** tab.
     * Enter the GUID that you created in the **Tags** field.

   * When not using Visual Studio:

     * Open the app's project file.

     * Add a `<PackageTags>` property to a new or existing `<PropertyGroup>` with the GUID that you created:

       ```
       <PropertyGroup>
         <PackageTags>00001111-aaaa-2222-bbbb-3333cccc4444</PackageTags>
       </PropertyGroup>
       ```

   In the following example:

   * The local IP address of the server is `10.0.0.4`.
   * An online random GUID generator provides the `appid` value.

   ```
   netsh http add sslcert 
       ipport=10.0.0.4:443 
       certhash=b66ee04419d4ee37464ab8785ff02449980eae10 
       appid="{00001111-aaaa-2222-bbbb-3333cccc4444}"
   ```

   When a certificate is registered, the tool responds with `SSL Certificate successfully added`.

   To delete a certificate registration, use the `delete sslcert` command:

   ```
   netsh http delete sslcert ipport=<IP>:<PORT>
   ```

   Reference documentation for *netsh.exe*:

   * [Netsh Commands for Hypertext Transfer Protocol (HTTP)](/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/cc725882\(v=ws.10\))
   * [UrlPrefix Strings](/en-us/windows/win32/http/urlprefix-strings)

8. Run the app.

   Administrator privileges aren't required to run the app when binding to localhost using HTTP (not HTTPS) with a port number greater than 1024. For other configurations (for example, using a local IP address or binding to port 443), run the app with administrator privileges.

   The app responds at the server's public IP address. In this example, the server is reached from the Internet at its public IP address of `104.214.79.47`.

   A development certificate is used in this example. The page loads securely after bypassing the browser's untrusted certificate warning.

## Proxy server and load balancer scenarios

For apps hosted by HTTP.sys that interact with requests from the Internet or a corporate network, additional configuration might be required when hosting behind proxy servers and load balancers. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

[]()

## Get detailed timing information with IHttpSysRequestTimingFeature

[IHttpSysRequestTimingFeature](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.ihttpsysrequesttimingfeature) provides detailed timing information for requests:

* Timestamps are obtained using [QueryPerformanceCounter](/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter).
* The timestamp frequency can be obtained via [QueryPerformanceFrequency](/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency).
* The index of the timing can be cast to [HttpSysRequestTimingType](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysrequesttimingtype) to know what the timing represents.
* The value may be 0 if the timing isn't available for the current request.
* Requires Windows 10 version 2004, Windows Server 2022, or later.

```
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys();

var app = builder.Build();

app.Use((context, next) =>
{
    var feature = context.Features.GetRequiredFeature<IHttpSysRequestTimingFeature>();
    
    var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("Sample");

    var timestamps = feature.Timestamps;

    for (var i = 0; i < timestamps.Length; i++)
    {
        var timestamp = timestamps[i];
        var timingType = (HttpSysRequestTimingType)i;

        logger.LogInformation("Timestamp {timingType}: {timestamp}",
                                          timingType, timestamp);
    }

    return next(context);
});

app.MapGet("/", () => Results.Ok());

app.Run();
```

[IHttpSysRequestTimingFeature.TryGetTimestamp](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.ihttpsysrequesttimingfeature.trygettimestamp) retrieves the timestamp for the provided timing type:

```
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys();

var app = builder.Build();

app.Use((context, next) =>
{
    var feature = context.Features.GetRequiredFeature<IHttpSysRequestTimingFeature>();

    var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("Sample");

    var timingType = HttpSysRequestTimingType.RequestRoutingEnd;

    if (feature.TryGetTimestamp(timingType, out var timestamp))
    {
        logger.LogInformation("Timestamp {timingType}: {timestamp}",
                                          timingType, timestamp);
    }
    else
    {
        logger.LogInformation("Timestamp {timingType}: not available for the "
                                           + "current request",    timingType);
    }

    return next(context);
});

app.MapGet("/", () => Results.Ok());

app.Run();
```

\[IHttpSysRequestTimingFeature.TryGetElapsedTime]\(/dotnet/api/microsoft.aspnetcore.server.httpsys.ihttpsysrequesttimingfeature.trygetelapsedtime yields the elapsed time between two specified timings:

```
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys();

var app = builder.Build();

app.Use((context, next) =>
{
    var feature = context.Features.GetRequiredFeature<IHttpSysRequestTimingFeature>();

    var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("Sample");

    var startingTimingType = HttpSysRequestTimingType.RequestRoutingStart;
    var endingTimingType = HttpSysRequestTimingType.RequestRoutingEnd;

    if (feature.TryGetElapsedTime(startingTimingType, endingTimingType, out var elapsed))
    {
        logger.LogInformation(
            "Elapsed time {startingTimingType} to {endingTimingType}: {elapsed}",
            startingTimingType,
            endingTimingType,
            elapsed);
    }
    else
    {
        logger.LogInformation(
            "Elapsed time {startingTimingType} to {endingTimingType}:"
            + " not available for the current request.",
            startingTimingType,
            endingTimingType);
    }

    return next(context);
});

app.MapGet("/", () => Results.Ok());

app.Run();
```

## Advanced HTTP/2 features to support gRPC

Additional HTTP/2 features in HTTP.sys support gRPC, including support for response trailers and sending reset frames.

Requirements to run gRPC with HTTP.sys:

* Windows 11 Build 22000 or later, Windows Server 2022 Build 20348 or later.
* TLS 1.2 or later connection.

### Trailers

HTTP Trailers are similar to HTTP Headers, except they are sent after the response body is sent. For IIS and HTTP.sys, only HTTP/2 response trailers are supported.

```
if (httpContext.Response.SupportsTrailers())
{
    httpContext.Response.DeclareTrailer("trailername");	

    // Write body
    httpContext.Response.WriteAsync("Hello world");

    httpContext.Response.AppendTrailer("trailername", "TrailerValue");
}
```

In the preceding example code:

* `SupportsTrailers` ensures that trailers are supported for the response.
* `DeclareTrailer` adds the given trailer name to the `Trailer` response header. Declaring a response's trailers is optional, but recommended. If `DeclareTrailer` is called, it must be before the response headers are sent.
* `AppendTrailer` appends the trailer.

### Reset

Reset allows for the server to reset a HTTP/2 request with a specified error code. A reset request is considered aborted.

```
var resetFeature = httpContext.Features.Get<IHttpResetFeature>();
resetFeature.Reset(errorCode: 2);
```

`Reset` in the preceding code example specifies the `INTERNAL_ERROR` error code. For more information about HTTP/2 error codes, visit the [HTTP/2 specification error code section](https://tools.ietf.org/html/rfc7540#page-50).

## Tracing

For information about how to get traces from HTTP.sys, see [HTTP.sys Manageability Scenarios](/en-us/windows/win32/http/http-sys-manageability-scenarios).

## Automatic eviction from memory pool

The memory pools used by Kestrel, IIS, and HTTP.sys automatically evict memory blocks when the application is idle or under low load. The feature runs automatically and doesn't need to be enabled or configured manually.

In versions of .NET earlier than 10, memory allocated by the pool remains reserved, even when not in use. This automatic eviction feature reduces overall memory usage and helps applications stay responsive under varying workloads.

### Use memory pool metrics

The default memory pool used by the ASP.NET Core server implementations includes metrics, which can be used to monitor and analyze memory usage patterns. The metrics are under the name `"Microsoft.AspNetCore.MemoryPool"`.

For information about metrics and how to use them, see [ASP.NET Core metrics](../../metrics/overview?view=aspnetcore-10.0).

## Manage memory pools

Besides using memory pools efficiently by evicting unneeded memory blocks, ASP.NET Core provides a built-in [IMemoryPoolFactory](https://source.dot.net/#Microsoft.AspNetCore.Connections.Abstractions/IMemoryPoolFactory.cs) and an implementation. It makes the implementation available to your application through dependency injection.

The following code example shows a simple background service that uses the built-in memory pool factory implementation to create memory pools. These pools benefit from the automatic eviction feature:

```
public class MyBackgroundService : BackgroundService
{
    private readonly MemoryPool<byte> _memoryPool;

    public MyBackgroundService(IMemoryPoolFactory<byte> factory)
    {
        _memoryPool = factory.Create();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(20, stoppingToken);
                // do work that needs memory
                // consider checking _memoryPool.MaxBufferSize
                var rented = _memoryPool.Rent(100);
                rented.Dispose();
            }
            catch (OperationCanceledException)
            {
                return;
            }
        }
    }
}
```

To use a custom memory pool factory, make a class that implements `IMemoryPoolFactory` and register it with dependency injection, as the following example does. Memory pools created this way do not benefit from the automatic eviction feature unless you implement similar eviction logic in your custom factory:

```
services.AddSingleton<IMemoryPoolFactory<byte>,
CustomMemoryPoolFactory>();

public class CustomMemoryPoolFactory : IMemoryPoolFactory<byte>
{
    public MemoryPool<byte> Create()
    {
        // Return a custom MemoryPool implementation
        // or the default, as is shown here.
        return MemoryPool<byte>.Shared;
    }
}
```

When you're using a memory pool, be aware of the pool's [MaxBufferSize](/en-us/dotnet/api/system.buffers.memorypool-1.maxbuffersize#system-buffers-memorypool-1-maxbuffersize).

[HTTP.sys](/en-us/iis/get-started/introduction-to-iis/introduction-to-iis-architecture#hypertext-transfer-protocol-stack-httpsys) is a [web server for ASP.NET Core](./?view=aspnetcore-10.0) that only runs on Windows. HTTP.sys is an alternative to [Kestrel](kestrel?view=aspnetcore-10.0) server and offers some features that Kestrel doesn't provide.

Important

HTTP.sys isn't compatible with the [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) and can't be used with IIS or IIS Express.

HTTP.sys supports the following features:

* [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0)
* Port sharing
* HTTPS with SNI
* HTTP/2 over TLS (Windows 10 or later)
* Direct file transmission
* Response caching
* WebSockets (Windows 8 or later)

Supported Windows versions:

* Windows 7 or later
* Windows Server 2008 R2 or later

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/servers/httpsys/samples) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## When to use HTTP.sys

HTTP.sys is useful for deployments where:

* There's a need to expose the server directly to the Internet without using IIS.

* An internal deployment requires a feature not available in Kestrel. For more information, see [Kestrel vs. HTTP.sys](./?view=aspnetcore-10.0#kestrel-vs-httpsys)

HTTP.sys is mature technology that protects against many types of attacks and provides the robustness, security, and scalability of a full-featured web server. IIS itself runs as an HTTP listener on top of HTTP.sys.

## HTTP/2 support

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is enabled for ASP.NET Core apps when the following base requirements are met:

* Windows Server 2016/Windows 10 or later
* [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) connection
* TLS 1.2 or later connection

If an HTTP/2 connection is established, [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) reports `HTTP/2`.

HTTP/2 is enabled by default. If an HTTP/2 connection isn't established, the connection falls back to HTTP/1.1. In a future release of Windows, HTTP/2 configuration flags will be available, including the ability to disable HTTP/2 with HTTP.sys.

## HTTP/3 support

[HTTP/3](https://datatracker.ietf.org/doc/rfc9114/) is enabled for ASP.NET Core apps when the following base requirements are met:

* Windows Server 2022/Windows 11 or later
* An `https` url binding is used.
* The [EnableHttp3 registry key](https://techcommunity.microsoft.com/t5/networking-blog/enabling-http-3-support-on-windows-server-2022/ba-p/2676880) is set.

The preceding Windows 11 Build versions may require the use of a [Windows Insider](https://insider.windows.com) build.

HTTP/3 is discovered as an upgrade from HTTP/1.1 or HTTP/2 via the `alt-svc` header. That means the first request will normally use HTTP/1.1 or HTTP/2 before switching to HTTP/3. Http.Sys doesn't automatically add the `alt-svc` header, it must be added by the application. The following code is a middleware example that adds the `alt-svc` response header.

```
app.Use((context, next) =>
{
    context.Response.Headers.AltSvc = "h3=\":443\"";
    return next(context);
});
```

Place the preceding code early in the request pipeline.

Http.Sys also supports sending an AltSvc HTTP/2 protocol message rather than a response header to notify the client that HTTP/3 is available. See the [EnableAltSvc registry key](https://techcommunity.microsoft.com/t5/networking-blog/enabling-http-3-support-on-windows-server-2022/ba-p/2676880). This requires netsh sslcert bindings that use host names rather than IP addresses.

## Kernel mode authentication with Kerberos

HTTP.sys delegates to kernel mode authentication with the Kerberos authentication protocol. User mode authentication isn't supported with Kerberos and HTTP.sys. The machine account must be used to decrypt the Kerberos token/ticket that's obtained from Active Directory and forwarded by the client to the server to authenticate the user. Register the Service Principal Name (SPN) for the host, not the user of the app.

## How to use HTTP.sys

### Configure the ASP.NET Core app to use HTTP.sys

Call the [UseHttpSys](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderhttpsysextensions.usehttpsys) extension method when building the host, specifying any required [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions). The following example sets options to their default values:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseHttpSys(options =>
            {
                options.AllowSynchronousIO = false;
                options.Authentication.Schemes = AuthenticationSchemes.None;
                options.Authentication.AllowAnonymous = true;
                options.MaxConnections = null;
                options.MaxRequestBodySize = 30000000;
                options.UrlPrefixes.Add("http://localhost:5005");
            });
            webBuilder.UseStartup<Startup>();
        });
```

Additional HTTP.sys configuration is handled through [registry settings](https://support.microsoft.com/help/820129/http-sys-registry-settings-for-windows).

For more information about HTTP.sys options, see [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions).

[]()

**MaxRequestBodySize**

The maximum allowed size of any request body in bytes. When set to `null`, the maximum request body size is unlimited. This limit has no effect on upgraded connections, which are always unlimited.

The recommended method to override the limit in an ASP.NET Core MVC app for a single `IActionResult` is to use the [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) attribute on an action method:

```
[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()
```

An exception is thrown if the app attempts to configure the limit on a request after the app has started reading the request. An `IsReadOnly` property can be used to indicate if the `MaxRequestBodySize` property is in a read-only state, meaning it's too late to configure the limit.

If the app should override [MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.maxrequestbodysize#microsoft-aspnetcore-server-httpsys-httpsysoptions-maxrequestbodysize) per-request, use the [IHttpMaxRequestBodySizeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature):

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, 
    ILogger<Startup> logger, IServer server)
{
    app.Use(async (context, next) =>
    {
        context.Features.Get<IHttpMaxRequestBodySizeFeature>()
            .MaxRequestBodySize = 10 * 1024;

        var serverAddressesFeature = 
            app.ServerFeatures.Get<IServerAddressesFeature>();
        var addresses = string.Join(", ", serverAddressesFeature?.Addresses);

        logger.LogInformation("Addresses: {Addresses}", addresses);

        await next.Invoke();
    });

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseStaticFiles();
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

If using Visual Studio, make sure the app isn't configured to run IIS or IIS Express.

In Visual Studio, the default launch profile is for IIS Express. To run the project as a console app, manually change the selected profile, as shown in the following screenshot:

### Configure Windows Server

1. Determine the ports to open for the app and use [Windows Firewall](/en-us/windows/security/threat-protection/windows-firewall/create-an-inbound-port-rule) or the [New-NetFirewallRule](/en-us/powershell/module/netsecurity/new-netfirewallrule) PowerShell cmdlet to open firewall ports to allow traffic to reach HTTP.sys. In the following commands and app configuration, port 443 is used.

2. When deploying to an Azure VM, open the ports in the [Network Security Group](/en-us/azure/virtual-machines/windows/nsg-quickstart-portal). In the following commands and app configuration, port 443 is used.

3. Obtain and install X.509 certificates, if required.

   On Windows, create self-signed certificates using the [New-SelfSignedCertificate PowerShell cmdlet](/en-us/powershell/module/pki/new-selfsignedcertificate). For an unsupported example, see [UpdateIISExpressSSLForChrome.ps1](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/includes/make-x509-cert/UpdateIISExpressSSLForChrome.ps1).

   Install either self-signed or CA-signed certificates in the server's **Local Machine** > **Personal** store.

4. If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd), install .NET, .NET Framework, or both (if the app is a .NET app targeting the .NET Framework).

   * **.NET**: If the app requires .NET, obtain and run the **.NET Runtime** installer from [.NET Downloads](https://dotnet.microsoft.com/download). Don't install the full SDK on the server.
   * **.NET Framework**: If the app requires .NET Framework, see the [.NET Framework installation guide](/en-us/dotnet/framework/install/). Install the required .NET Framework. The installer for the latest .NET Framework is available from the [.NET Downloads](https://dotnet.microsoft.com/download) page.

   If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd), the app includes the runtime in its deployment. No framework installation is required on the server.

5. Configure URLs and ports in the app.

   By default, ASP.NET Core binds to `http://localhost:5000`. To configure URL prefixes and ports, options include:

   * [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls)
   * `urls` command-line argument
   * `ASPNETCORE_URLS` environment variable
   * [UrlPrefixes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.urlprefixes#microsoft-aspnetcore-server-httpsys-httpsysoptions-urlprefixes)

   The following code example shows how to use [UrlPrefixes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.urlprefixes#microsoft-aspnetcore-server-httpsys-httpsysoptions-urlprefixes) with the server's local IP address `10.0.0.4` on port 443:

   ```
   public static IHostBuilder CreateHostBuilder(string[] args) =>
       Host.CreateDefaultBuilder(args)
           .ConfigureWebHostDefaults(webBuilder =>
           {
               webBuilder.UseHttpSys(options =>
               {
                   options.UrlPrefixes.Add("https://10.0.0.4:443");
               });
               webBuilder.UseStartup<Startup>();
           });
   ```

   An advantage of `UrlPrefixes` is that an error message is generated immediately for improperly formatted prefixes.

   The settings in `UrlPrefixes` override `UseUrls`/`urls`/`ASPNETCORE_URLS` settings. Therefore, an advantage of `UseUrls`, `urls`, and the `ASPNETCORE_URLS` environment variable is that it's easier to switch between Kestrel and HTTP.sys.

   HTTP.sys uses the [HTTP Server API UrlPrefix string formats](/en-us/windows/win32/http/urlprefix-strings).

   Warning

   Top-level wildcard bindings (`http://*:80/` and `http://+:80`) should **not** be used. Top-level wildcard bindings create app security vulnerabilities. This applies to both strong and weak wildcards. Use explicit host names or IP addresses rather than wildcards. Subdomain wildcard binding (for example, `*.mysub.com`) isn't a security risk if you control the entire parent domain (as opposed to `*.com`, which is vulnerable). For more information, see [RFC 9110: Section 7.2: Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#field.host).

6. Preregister URL prefixes on the server.

   The built-in tool for configuring HTTP.sys is *netsh.exe*. *netsh.exe* is used to reserve URL prefixes and assign X.509 certificates. The tool requires administrator privileges.

   Use the *netsh.exe* tool to register URLs for the app:

   ```
   netsh http add urlacl url=<URL> user=<USER>
   ```

   * `<URL>`: The fully qualified Uniform Resource Locator (URL). Don't use a wildcard binding. Use a valid hostname or local IP address. *The URL must include a trailing slash.*
   * `<USER>`: Specifies the user or user-group name.

   In the following example, the local IP address of the server is `10.0.0.4`:

   ```
   netsh http add urlacl url=https://10.0.0.4:443/ user=Users
   ```

   When a URL is registered, the tool responds with `URL reservation successfully added`.

   To delete a registered URL, use the `delete urlacl` command:

   ```
   netsh http delete urlacl url=<URL>
   ```

7. Register X.509 certificates on the server.

   Use the *netsh.exe* tool to register certificates for the app:

   ```
   netsh http add sslcert ipport=<IP>:<PORT> certhash=<THUMBPRINT> appid="{<GUID>}"
   ```

   * `<IP>`: Specifies the local IP address for the binding. Don't use a wildcard binding. Use a valid IP address.
   * `<PORT>`: Specifies the port for the binding.
   * `<THUMBPRINT>`: The X.509 certificate thumbprint.
   * `<GUID>`: A developer-generated GUID to represent the app for informational purposes.

   For reference purposes, store the GUID in the app as a package tag:

   * In Visual Studio:

     * Open the app's project properties by right-clicking on the app in **Solution Explorer** and selecting **Properties**.
     * Select the **Package** tab.
     * Enter the GUID that you created in the **Tags** field.

   * When not using Visual Studio:

     * Open the app's project file.

     * Add a `<PackageTags>` property to a new or existing `<PropertyGroup>` with the GUID that you created:

       ```
       <PropertyGroup>
         <PackageTags>00001111-aaaa-2222-bbbb-3333cccc4444</PackageTags>
       </PropertyGroup>
       ```

   In the following example:

   * The local IP address of the server is `10.0.0.4`.
   * An online random GUID generator provides the `appid` value.

   ```
   netsh http add sslcert 
       ipport=10.0.0.4:443 
       certhash=b66ee04419d4ee37464ab8785ff02449980eae10 
       appid="{00001111-aaaa-2222-bbbb-3333cccc4444}"
   ```

   When a certificate is registered, the tool responds with `SSL Certificate successfully added`.

   To delete a certificate registration, use the `delete sslcert` command:

   ```
   netsh http delete sslcert ipport=<IP>:<PORT>
   ```

   Reference documentation for *netsh.exe*:

   * [Netsh Commands for Hypertext Transfer Protocol (HTTP)](/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/cc725882\(v=ws.10\))
   * [UrlPrefix Strings](/en-us/windows/win32/http/urlprefix-strings)

8. Run the app.

   Administrator privileges aren't required to run the app when binding to localhost using HTTP (not HTTPS) with a port number greater than 1024. For other configurations (for example, using a local IP address or binding to port 443), run the app with administrator privileges.

   The app responds at the server's public IP address. In this example, the server is reached from the Internet at its public IP address of `104.214.79.47`.

   A development certificate is used in this example. The page loads securely after bypassing the browser's untrusted certificate warning.

## Proxy server and load balancer scenarios

For apps hosted by HTTP.sys that interact with requests from the Internet or a corporate network, additional configuration might be required when hosting behind proxy servers and load balancers. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

## Advanced HTTP/2 features to support gRPC

Additional HTTP/2 features in HTTP.sys support gRPC, including support for response trailers and sending reset frames.

Requirements to run gRPC with HTTP.sys:

* Windows 11 Build 22000 or later, Windows Server 2022 Build 20348 or later.
* TLS 1.2 or later connection.

### Trailers

HTTP Trailers are similar to HTTP Headers, except they are sent after the response body is sent. For IIS and HTTP.sys, only HTTP/2 response trailers are supported.

```
if (httpContext.Response.SupportsTrailers())
{
    httpContext.Response.DeclareTrailer("trailername");	

    // Write body
    httpContext.Response.WriteAsync("Hello world");

    httpContext.Response.AppendTrailer("trailername", "TrailerValue");
}
```

In the preceding example code:

* `SupportsTrailers` ensures that trailers are supported for the response.
* `DeclareTrailer` adds the given trailer name to the `Trailer` response header. Declaring a response's trailers is optional, but recommended. If `DeclareTrailer` is called, it must be before the response headers are sent.
* `AppendTrailer` appends the trailer.

### Reset

Reset allows for the server to reset a HTTP/2 request with a specified error code. A reset request is considered aborted.

```
var resetFeature = httpContext.Features.Get<IHttpResetFeature>();
resetFeature.Reset(errorCode: 2);
```

`Reset` in the preceding code example specifies the `INTERNAL_ERROR` error code. For more information about HTTP/2 error codes, visit the [HTTP/2 specification error code section](https://tools.ietf.org/html/rfc7540#page-50).

[HTTP.sys](/en-us/iis/get-started/introduction-to-iis/introduction-to-iis-architecture#hypertext-transfer-protocol-stack-httpsys) is a [web server for ASP.NET Core](./?view=aspnetcore-10.0) that only runs on Windows. HTTP.sys is an alternative to [Kestrel](kestrel?view=aspnetcore-10.0) server and offers some features that Kestrel doesn't provide.

Important

HTTP.sys isn't compatible with the [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) and can't be used with IIS or IIS Express.

HTTP.sys supports the following features:

* [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0)
* Port sharing
* HTTPS with SNI
* HTTP/2 over TLS (Windows 10 or later)
* Direct file transmission
* Response caching
* WebSockets (Windows 8 or later)

Supported Windows versions:

* Windows 7 or later
* Windows Server 2008 R2 or later

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/servers/httpsys/samples) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## When to use HTTP.sys

HTTP.sys is useful for deployments where:

* There's a need to expose the server directly to the Internet without using IIS.

* An internal deployment requires a feature not available in Kestrel. For more information, see [Kestrel vs. HTTP.sys](./?view=aspnetcore-10.0#kestrel-vs-httpsys)

HTTP.sys is mature technology that protects against many types of attacks and provides the robustness, security, and scalability of a full-featured web server. IIS itself runs as an HTTP listener on top of HTTP.sys.

## HTTP/2 support

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is enabled for ASP.NET Core apps if the following base requirements are met:

* Windows Server 2016/Windows 10 or later
* [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) connection
* TLS 1.2 or later connection

If an HTTP/2 connection is established, [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) reports `HTTP/2`.

HTTP/2 is enabled by default. If an HTTP/2 connection isn't established, the connection falls back to HTTP/1.1. In a future release of Windows, HTTP/2 configuration flags will be available, including the ability to disable HTTP/2 with HTTP.sys.

## Kernel mode authentication with Kerberos

HTTP.sys delegates to kernel mode authentication with the Kerberos authentication protocol. User mode authentication isn't supported with Kerberos and HTTP.sys. The machine account must be used to decrypt the Kerberos token/ticket that's obtained from Active Directory and forwarded by the client to the server to authenticate the user. Register the Service Principal Name (SPN) for the host, not the user of the app.

## How to use HTTP.sys

### Configure the ASP.NET Core app to use HTTP.sys

Call the [UseHttpSys](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderhttpsysextensions.usehttpsys) extension method when building the host, specifying any required [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions). The following example sets options to their default values:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseHttpSys(options =>
            {
                options.AllowSynchronousIO = false;
                options.Authentication.Schemes = AuthenticationSchemes.None;
                options.Authentication.AllowAnonymous = true;
                options.MaxConnections = null;
                options.MaxRequestBodySize = 30000000;
                options.UrlPrefixes.Add("http://localhost:5005");
            });
            webBuilder.UseStartup<Startup>();
        });
```

Additional HTTP.sys configuration is handled through [registry settings](https://support.microsoft.com/help/820129/http-sys-registry-settings-for-windows).

**HTTP.sys options**

| Property                                                                                                                                                                                             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Default                                                                                                                  |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| [AllowSynchronousIO](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.allowsynchronousio#microsoft-aspnetcore-server-httpsys-httpsysoptions-allowsynchronousio)                  | Control whether synchronous input/output is allowed for the `HttpContext.Request.Body` and `HttpContext.Response.Body`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `false`                                                                                                                  |
| [Authentication.AllowAnonymous](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.authenticationmanager.allowanonymous#microsoft-aspnetcore-server-httpsys-authenticationmanager-allowanonymous) | Allow anonymous requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `true`                                                                                                                   |
| [Authentication.Schemes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.authenticationmanager.schemes#microsoft-aspnetcore-server-httpsys-authenticationmanager-schemes)                      | Specify the allowed authentication schemes. May be modified at any time prior to disposing the listener. Values are provided by the [AuthenticationSchemes enum](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.authenticationschemes): `Basic`, `Kerberos`, `Negotiate`, `None`, and `NTLM`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `None`                                                                                                                   |
| [EnableResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.enableresponsecaching#microsoft-aspnetcore-server-httpsys-httpsysoptions-enableresponsecaching)         | Attempt [kernel-mode](/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode) caching for responses with eligible headers. The response may not include `Set-Cookie`, `Vary`, or `Pragma` headers. It must include a `Cache-Control` header that's `public` and either a `shared-max-age` or `max-age` value, or an `Expires` header.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `true`                                                                                                                   |
| [Http503Verbosity](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.http503verbosity#microsoft-aspnetcore-server-httpsys-httpsysoptions-http503verbosity)                        | The HTTP.sys behavior when rejecting requests due to throttling conditions.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | [Http503VerbosityLevel. Basic](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.http503verbositylevel)              |
| [MaxAccepts](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.maxaccepts#microsoft-aspnetcore-server-httpsys-httpsysoptions-maxaccepts)                                          | The maximum number of concurrent accepts.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | 5 × [Environment. ProcessorCount](/en-us/dotnet/api/system.environment.processorcount#system-environment-processorcount) |
| [MaxConnections](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.maxconnections#microsoft-aspnetcore-server-httpsys-httpsysoptions-maxconnections)                              | The maximum number of concurrent connections to accept. Use `-1` for infinite. Use `null` to use the registry's machine-wide setting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `null` (machine-wide setting)                                                                                            |
| [MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.maxrequestbodysize#microsoft-aspnetcore-server-httpsys-httpsysoptions-maxrequestbodysize)                  | See the [MaxRequestBodySize](#maxrequestbodysize) section.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | 30000000 bytes (\~28.6 MB)                                                                                               |
| [RequestQueueLimit](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.requestqueuelimit#microsoft-aspnetcore-server-httpsys-httpsysoptions-requestqueuelimit)                     | The maximum number of requests that can be queued.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | 1000                                                                                                                     |
| `RequestQueueMode`                                                                                                                                                                                   | This indicates whether the server is responsible for creating and configuring the request queue, or if it should attach to an existing queue. Most existing configuration options do not apply when attaching to an existing queue.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `RequestQueueMode.Create`                                                                                                |
| `RequestQueueName`                                                                                                                                                                                   | The name of the HTTP.sys request queue.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `null` (Anonymous queue)                                                                                                 |
| [ThrowWriteExceptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.throwwriteexceptions#microsoft-aspnetcore-server-httpsys-httpsysoptions-throwwriteexceptions)            | Indicate if response body writes that fail due to client disconnects should throw exceptions or complete normally.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `false` (complete normally)                                                                                              |
| [Timeouts](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.timeouts#microsoft-aspnetcore-server-httpsys-httpsysoptions-timeouts)                                                | Expose the HTTP.sys [TimeoutManager](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.timeoutmanager) configuration, which may also be configured in the registry. Follow the API links to learn more about each setting, including default values:- [TimeoutManager.DrainEntityBody](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.timeoutmanager.drainentitybody#microsoft-aspnetcore-server-httpsys-timeoutmanager-drainentitybody): Time allowed for the HTTP Server API to drain the entity body on a Keep-Alive connection.
- [TimeoutManager.EntityBody](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.timeoutmanager.entitybody#microsoft-aspnetcore-server-httpsys-timeoutmanager-entitybody): Time allowed for the request entity body to arrive.
- [TimeoutManager.HeaderWait](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.timeoutmanager.headerwait#microsoft-aspnetcore-server-httpsys-timeoutmanager-headerwait): Time allowed for the HTTP Server API to parse the request header.
- [TimeoutManager.IdleConnection](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.timeoutmanager.idleconnection#microsoft-aspnetcore-server-httpsys-timeoutmanager-idleconnection): Time allowed for an idle connection.
- [TimeoutManager.MinSendBytesPerSecond](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.timeoutmanager.minsendbytespersecond#microsoft-aspnetcore-server-httpsys-timeoutmanager-minsendbytespersecond): The minimum send rate for the response.
- [TimeoutManager.RequestQueue](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.timeoutmanager.requestqueue#microsoft-aspnetcore-server-httpsys-timeoutmanager-requestqueue): Time allowed for the request to remain in the request queue before the app picks it up. |                                                                                                                          |
| [UrlPrefixes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.urlprefixes#microsoft-aspnetcore-server-httpsys-httpsysoptions-urlprefixes)                                       | Specify the [UrlPrefixCollection](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.urlprefixcollection) to register with HTTP.sys. The most useful is [UrlPrefixCollection.Add](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.urlprefixcollection.add), which is used to add a prefix to the collection. These may be modified at any time prior to disposing the listener.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |                                                                                                                          |

[]()

**MaxRequestBodySize**

The maximum allowed size of any request body in bytes. When set to `null`, the maximum request body size is unlimited. This limit has no effect on upgraded connections, which are always unlimited.

The recommended method to override the limit in an ASP.NET Core MVC app for a single `IActionResult` is to use the [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) attribute on an action method:

```
[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()
```

An exception is thrown if the app attempts to configure the limit on a request after the app has started reading the request. An `IsReadOnly` property can be used to indicate if the `MaxRequestBodySize` property is in a read-only state, meaning it's too late to configure the limit.

If the app should override [MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.maxrequestbodysize#microsoft-aspnetcore-server-httpsys-httpsysoptions-maxrequestbodysize) per-request, use the [IHttpMaxRequestBodySizeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature):

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, 
    ILogger<Startup> logger, IServer server)
{
    app.Use(async (context, next) =>
    {
        context.Features.Get<IHttpMaxRequestBodySizeFeature>()
            .MaxRequestBodySize = 10 * 1024;

        var serverAddressesFeature = 
            app.ServerFeatures.Get<IServerAddressesFeature>();
        var addresses = string.Join(", ", serverAddressesFeature?.Addresses);

        logger.LogInformation("Addresses: {Addresses}", addresses);

        await next.Invoke();
    });

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseStaticFiles();
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

If using Visual Studio, make sure the app isn't configured to run IIS or IIS Express.

In Visual Studio, the default launch profile is for IIS Express. To run the project as a console app, manually change the selected profile, as shown in the following screen shot:

### Configure Windows Server

1. Determine the ports to open for the app and use [Windows Firewall](/en-us/windows/security/threat-protection/windows-firewall/create-an-inbound-port-rule) or the [New-NetFirewallRule](/en-us/powershell/module/netsecurity/new-netfirewallrule) PowerShell cmdlet to open firewall ports to allow traffic to reach HTTP.sys. In the following commands and app configuration, port 443 is used.

2. When deploying to an Azure VM, open the ports in the [Network Security Group](/en-us/azure/virtual-machines/windows/nsg-quickstart-portal). In the following commands and app configuration, port 443 is used.

3. Obtain and install X.509 certificates, if required.

   On Windows, create self-signed certificates using the [New-SelfSignedCertificate PowerShell cmdlet](/en-us/powershell/module/pki/new-selfsignedcertificate). For an unsupported example, see [UpdateIISExpressSSLForChrome.ps1](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/includes/make-x509-cert/UpdateIISExpressSSLForChrome.ps1).

   Install either self-signed or CA-signed certificates in the server's **Local Machine** > **Personal** store.

4. If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd), install .NET, .NET Framework, or both (if the app is a .NET app targeting the .NET Framework).

   * **.NET**: If the app requires .NET, obtain and run the **.NET Runtime** installer from [.NET Downloads](https://dotnet.microsoft.com/download). Don't install the full SDK on the server.
   * **.NET Framework**: If the app requires .NET Framework, see the [.NET Framework installation guide](/en-us/dotnet/framework/install/). Install the required .NET Framework. The installer for the latest .NET Framework is available from the [.NET Downloads](https://dotnet.microsoft.com/download) page.

   If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd), the app includes the runtime in its deployment. No framework installation is required on the server.

5. Configure URLs and ports in the app.

   By default, ASP.NET Core binds to `http://localhost:5000`. To configure URL prefixes and ports, options include:

   * [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls)
   * `urls` command-line argument
   * `ASPNETCORE_URLS` environment variable
   * [UrlPrefixes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.urlprefixes#microsoft-aspnetcore-server-httpsys-httpsysoptions-urlprefixes)

   The following code example shows how to use [UrlPrefixes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.urlprefixes#microsoft-aspnetcore-server-httpsys-httpsysoptions-urlprefixes) with the server's local IP address `10.0.0.4` on port 443:

   ```
   public static IHostBuilder CreateHostBuilder(string[] args) =>
       Host.CreateDefaultBuilder(args)
           .ConfigureWebHostDefaults(webBuilder =>
           {
               webBuilder.UseHttpSys(options =>
               {
                   options.UrlPrefixes.Add("https://10.0.0.4:443");
               });
               webBuilder.UseStartup<Startup>();
           });
   ```

   An advantage of `UrlPrefixes` is that an error message is generated immediately for improperly formatted prefixes.

   The settings in `UrlPrefixes` override `UseUrls`/`urls`/`ASPNETCORE_URLS` settings. Therefore, an advantage of `UseUrls`, `urls`, and the `ASPNETCORE_URLS` environment variable is that it's easier to switch between Kestrel and HTTP.sys.

   HTTP.sys uses the [HTTP Server API UrlPrefix string formats](/en-us/windows/win32/http/urlprefix-strings).

   Warning

   Top-level wildcard bindings (`http://*:80/` and `http://+:80`) should **not** be used. Top-level wildcard bindings create app security vulnerabilities. This applies to both strong and weak wildcards. Use explicit host names or IP addresses rather than wildcards. Subdomain wildcard binding (for example, `*.mysub.com`) isn't a security risk if you control the entire parent domain (as opposed to `*.com`, which is vulnerable). For more information, see [RFC 9110: Section 7.2: Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#field.host).

6. Preregister URL prefixes on the server.

   The built-in tool for configuring HTTP.sys is *netsh.exe*. *netsh.exe* is used to reserve URL prefixes and assign X.509 certificates. The tool requires administrator privileges.

   Use the *netsh.exe* tool to register URLs for the app:

   ```
   netsh http add urlacl url=<URL> user=<USER>
   ```

   * `<URL>`: The fully qualified Uniform Resource Locator (URL). Don't use a wildcard binding. Use a valid hostname or local IP address. *The URL must include a trailing slash.*
   * `<USER>`: Specifies the user or user-group name.

   In the following example, the local IP address of the server is `10.0.0.4`:

   ```
   netsh http add urlacl url=https://10.0.0.4:443/ user=Users
   ```

   When a URL is registered, the tool responds with `URL reservation successfully added`.

   To delete a registered URL, use the `delete urlacl` command:

   ```
   netsh http delete urlacl url=<URL>
   ```

7. Register X.509 certificates on the server.

   Use the *netsh.exe* tool to register certificates for the app:

   ```
   netsh http add sslcert ipport=<IP>:<PORT> certhash=<THUMBPRINT> appid="{<GUID>}"
   ```

   * `<IP>`: Specifies the local IP address for the binding. Don't use a wildcard binding. Use a valid IP address.
   * `<PORT>`: Specifies the port for the binding.
   * `<THUMBPRINT>`: The X.509 certificate thumbprint.
   * `<GUID>`: A developer-generated GUID to represent the app for informational purposes.

   For reference purposes, store the GUID in the app as a package tag:

   * In Visual Studio:

     * Open the app's project properties by right-clicking on the app in **Solution Explorer** and selecting **Properties**.
     * Select the **Package** tab.
     * Enter the GUID that you created in the **Tags** field.

   * When not using Visual Studio:

     * Open the app's project file.

     * Add a `<PackageTags>` property to a new or existing `<PropertyGroup>` with the GUID that you created:

       ```
       <PropertyGroup>
         <PackageTags>00001111-aaaa-2222-bbbb-3333cccc4444</PackageTags>
       </PropertyGroup>
       ```

   In the following example:

   * The local IP address of the server is `10.0.0.4`.
   * An online random GUID generator provides the `appid` value.

   ```
   netsh http add sslcert 
       ipport=10.0.0.4:443 
       certhash=b66ee04419d4ee37464ab8785ff02449980eae10 
       appid="{00001111-aaaa-2222-bbbb-3333cccc4444}"
   ```

   When a certificate is registered, the tool responds with `SSL Certificate successfully added`.

   To delete a certificate registration, use the `delete sslcert` command:

   ```
   netsh http delete sslcert ipport=<IP>:<PORT>
   ```

   Reference documentation for *netsh.exe*:

   * [Netsh Commands for Hypertext Transfer Protocol (HTTP)](/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/cc725882\(v=ws.10\))
   * [UrlPrefix Strings](/en-us/windows/win32/http/urlprefix-strings)

8. Run the app.

   Administrator privileges aren't required to run the app when binding to localhost using HTTP (not HTTPS) with a port number greater than 1024. For other configurations (for example, using a local IP address or binding to port 443), run the app with administrator privileges.

   The app responds at the server's public IP address. In this example, the server is reached from the Internet at its public IP address of `104.214.79.47`.

   A development certificate is used in this example. The page loads securely after bypassing the browser's untrusted certificate warning.

## Proxy server and load balancer scenarios

For apps hosted by HTTP.sys that interact with requests from the Internet or a corporate network, additional configuration might be required when hosting behind proxy servers and load balancers. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

## Advanced HTTP/2 features to support gRPC

Additional HTTP/2 features in HTTP.sys support gRPC, including support for response trailers and sending reset frames.

Requirements to run gRPC with HTTP.sys:

* Windows 10, OS Build 19041.508 or later
* TLS 1.2 or later connection

### Trailers

HTTP Trailers are similar to HTTP Headers, except they are sent after the response body is sent. For IIS and HTTP.sys, only HTTP/2 response trailers are supported.

```
if (httpContext.Response.SupportsTrailers())
{
    httpContext.Response.DeclareTrailer("trailername");	

    // Write body
    httpContext.Response.WriteAsync("Hello world");

    httpContext.Response.AppendTrailer("trailername", "TrailerValue");
}
```

In the preceding example code:

* `SupportsTrailers` ensures that trailers are supported for the response.
* `DeclareTrailer` adds the given trailer name to the `Trailer` response header. Declaring a response's trailers is optional, but recommended. If `DeclareTrailer` is called, it must be before the response headers are sent.
* `AppendTrailer` appends the trailer.

### Reset

Reset allows for the server to reset a HTTP/2 request with a specified error code. A reset request is considered aborted.

```
var resetFeature = httpContext.Features.Get<IHttpResetFeature>();
resetFeature.Reset(errorCode: 2);
```

`Reset` in the preceding code example specifies the `INTERNAL_ERROR` error code. For more information about HTTP/2 error codes, visit the [HTTP/2 specification error code section](https://tools.ietf.org/html/rfc7540#page-50).

[HTTP.sys](/en-us/iis/get-started/introduction-to-iis/introduction-to-iis-architecture#hypertext-transfer-protocol-stack-httpsys) is a [web server for ASP.NET Core](./?view=aspnetcore-10.0) that only runs on Windows. HTTP.sys is an alternative to [Kestrel](kestrel?view=aspnetcore-10.0) server and offers some features that Kestrel doesn't provide.

Important

HTTP.sys isn't compatible with the [ASP.NET Core Module](../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) and can't be used with IIS or IIS Express.

HTTP.sys supports the following features:

* [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0)
* Port sharing
* HTTPS with SNI
* HTTP/2 over TLS (Windows 10 or later)
* HTTP/3 over TLS (Windows 11 or later)
* Direct file transmission
* Response caching
* WebSockets (Windows 8 or later)
* Customizable security descriptors

Supported Windows versions:

* Windows 7 or later
* Windows Server 2008 R2 or later

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/servers/httpsys/samples) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## When to use HTTP.sys

HTTP.sys is useful for deployments where:

* There's a need to expose the server directly to the Internet without using IIS.

* An internal deployment requires a feature not available in Kestrel. For more information, see [Kestrel vs. HTTP.sys](./?view=aspnetcore-10.0#kestrel-vs-httpsys)

HTTP.sys is mature technology that protects against many types of attacks and provides the robustness, security, and scalability of a full-featured web server. IIS itself runs as an HTTP listener on top of HTTP.sys.

## HTTP/2 support

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is enabled for ASP.NET Core apps when the following base requirements are met:

* Windows Server 2016/Windows 10 or later
* [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) connection
* TLS 1.2 or later connection

If an HTTP/2 connection is established, [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) reports `HTTP/2`.

HTTP/2 is enabled by default. If an HTTP/2 connection isn't established, the connection falls back to HTTP/1.1. In a future release of Windows, HTTP/2 configuration flags will be available, including the ability to disable HTTP/2 with HTTP.sys.

## HTTP/3 support

[HTTP/3](https://datatracker.ietf.org/doc/rfc9114/) is enabled for ASP.NET Core apps when the following base requirements are met:

* Windows Server 2022/Windows 11 or later
* An `https` url binding is used.
* The [EnableHttp3 registry key](https://techcommunity.microsoft.com/t5/networking-blog/enabling-http-3-support-on-windows-server-2022/ba-p/2676880) is set.

The preceding Windows 11 Build versions may require the use of a [Windows Insider](https://www.microsoft.com/en-us/windowsinsider/) build.

HTTP/3 is discovered as an upgrade from HTTP/1.1 or HTTP/2 via the `alt-svc` header. That means the first request will normally use HTTP/1.1 or HTTP/2 before switching to HTTP/3. Http.Sys doesn't automatically add the `alt-svc` header, it must be added by the application. The following code is a middleware example that adds the `alt-svc` response header.

```
app.Use((context, next) =>
{
    context.Response.Headers.AltSvc = "h3=\":443\"";
    return next(context);
});
```

Place the preceding code early in the request pipeline.

Http.Sys also supports sending an AltSvc HTTP/2 protocol message rather than a response header to notify the client that HTTP/3 is available. See the [EnableAltSvc registry key](https://techcommunity.microsoft.com/t5/networking-blog/enabling-http-3-support-on-windows-server-2022/ba-p/2676880). This requires netsh sslcert bindings that use host names rather than IP addresses.

## Kernel mode authentication with Kerberos

HTTP.sys delegates to kernel mode authentication with the Kerberos authentication protocol. User mode authentication isn't supported with Kerberos and HTTP.sys. The machine account must be used to decrypt the Kerberos token/ticket that's obtained from Active Directory and forwarded by the client to the server to authenticate the user. Register the Service Principal Name (SPN) for the host, not the user of the app.

### Enable channel binding token (CBT) hardening

Channel binding tokens (CBT) tie Windows authentication to the underlying TLS channel, which helps mitigate NTLM relay and man-in-the-middle attacks. For HTTPS endpoints that use Windows authentication with HTTP.sys, you can opt in to CBT hardening by setting the `Microsoft.AspNetCore.Server.HttpSys.EnableCBTHardening` AppContext switch to `true`.

Enable the switch in your project's `runtimeconfig.template.json` file:

```
{
  "configProperties": {
    "Microsoft.AspNetCore.Server.HttpSys.EnableCBTHardening": true
  }
}
```

Or set the switch programmatically before building the host in `Program.cs`:

```
AppContext.SetSwitch("Microsoft.AspNetCore.Server.HttpSys.EnableCBTHardening", true);
```

Warning

CBT hardening is off by default. Enabling it can cause Windows authentication to fail for clients or proxies that don't support channel binding. Test thoroughly in your environment before enabling in production.

### Support for kernel-mode response buffering

In some scenarios, high volumes of small writes with high latency can cause significant performance impact to `HTTP.sys`. This impact is due to the lack of a [Pipe](/en-us/dotnet/api/system.io.pipelines.pipe) buffer in the `HTTP.sys` implementation. To improve performance in these scenarios, support for response buffering is included in `HTTP.sys`. Enable buffering by setting [HttpSysOptions.EnableKernelResponseBuffering](https://github.com/dotnet/aspnetcore/blob/main/src/Servers/HttpSys/src/HttpSysOptions.cs#L120) to `true`. Response buffering should be enabled by an app that does synchronous I/O, or asynchronous I/O with no more than one outstanding write at a time. In these scenarios, response buffering can significantly improve throughput over high-latency connections.

Apps that use asynchronous I/O and that may have more than one write outstanding at a time should ***not*** use this flag. Enabling this flag can result in higher CPU and memory usage by HTTP.Sys.

## How to use HTTP.sys

### Configure the ASP.NET Core app to use HTTP.sys

Call the [UseHttpSys](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderhttpsysextensions.usehttpsys) extension method when building the host, specifying any required [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions). The following example sets options to their default values:

```
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys(options =>
{
    options.AllowSynchronousIO = false;
    options.Authentication.Schemes = AuthenticationSchemes.None;
    options.Authentication.AllowAnonymous = true;
    options.MaxConnections = null;
    options.MaxRequestBodySize = 30_000_000;
    options.UrlPrefixes.Add("http://localhost:5005");
});

builder.Services.AddRazorPages();

var app = builder.Build();
```

Additional HTTP.sys configuration is handled through [registry settings](https://support.microsoft.com/help/820129/http-sys-registry-settings-for-windows).

For more information about HTTP.sys options, see [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions).

[]()

**MaxRequestBodySize**

The maximum allowed size of any request body in bytes. When set to `null`, the maximum request body size is unlimited. This limit has no effect on upgraded connections, which are always unlimited.

The recommended method to override the limit in an ASP.NET Core MVC app for a single `IActionResult` is to use the [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) attribute on an action method:

```
[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()
```

An exception is thrown if the app attempts to configure the limit on a request after the app has started reading the request. An `IsReadOnly` property can be used to indicate if the `MaxRequestBodySize` property is in a read-only state, meaning it's too late to configure the limit.

If the app should override [MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.maxrequestbodysize#microsoft-aspnetcore-server-httpsys-httpsysoptions-maxrequestbodysize) per-request, use the [IHttpMaxRequestBodySizeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature):

```
app.Use((context, next) =>
{
    context.Features.GetRequiredFeature<IHttpMaxRequestBodySizeFeature>()
                                             .MaxRequestBodySize = 10 * 1024;

    var server = context.RequestServices
        .GetRequiredService<IServer>();
    var serverAddressesFeature = server.Features
                                 .GetRequiredFeature<IServerAddressesFeature>();

    var addresses = string.Join(", ", serverAddressesFeature.Addresses);

    var loggerFactory = context.RequestServices
        .GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("Sample");

    logger.LogInformation("Addresses: {addresses}", addresses);

    return next(context);
});
```

If using Visual Studio, make sure the app isn't configured to run IIS or IIS Express.

In Visual Studio, the default launch profile is for IIS Express. To run the project as a console app, manually change the selected profile, as shown in the following screenshot:

### Configure Windows Server

1. Determine the ports to open for the app and use [Windows Firewall](/en-us/windows/security/threat-protection/windows-firewall/create-an-inbound-port-rule) or the [New-NetFirewallRule](/en-us/powershell/module/netsecurity/new-netfirewallrule) PowerShell cmdlet to open firewall ports to allow traffic to reach HTTP.sys. In the following commands and app configuration, port 443 is used.

2. When deploying to an Azure VM, open the ports in the [Network Security Group](/en-us/azure/virtual-machines/windows/nsg-quickstart-portal). In the following commands and app configuration, port 443 is used.

3. Obtain and install X.509 certificates, if required.

   On Windows, create self-signed certificates using the [New-SelfSignedCertificate PowerShell cmdlet](/en-us/powershell/module/pki/new-selfsignedcertificate). For an unsupported example, see [UpdateIISExpressSSLForChrome.ps1](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/includes/make-x509-cert/UpdateIISExpressSSLForChrome.ps1).

   Install either self-signed or CA-signed certificates in the server's **Local Machine** > **Personal** store.

4. If the app is a [framework-dependent deployment](/en-us/dotnet/core/deploying/#framework-dependent-deployments-fdd), install .NET, .NET Framework, or both (if the app is a .NET app targeting the .NET Framework).

   * **.NET**: If the app requires .NET, obtain and run the **.NET Runtime** installer from [.NET Downloads](https://dotnet.microsoft.com/download). Don't install the full SDK on the server.
   * **.NET Framework**: If the app requires .NET Framework, see the [.NET Framework installation guide](/en-us/dotnet/framework/install/). Install the required .NET Framework. The installer for the latest .NET Framework is available from the [.NET Downloads](https://dotnet.microsoft.com/download) page.

   If the app is a [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd), the app includes the runtime in its deployment. No framework installation is required on the server.

5. Configure URLs and ports in the app.

   By default, ASP.NET Core binds to `http://localhost:5000`. To configure URL prefixes and ports, options include:

   * [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls)
   * `urls` command-line argument
   * `ASPNETCORE_URLS` environment variable
   * [UrlPrefixes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.urlprefixes#microsoft-aspnetcore-server-httpsys-httpsysoptions-urlprefixes)

   The following code example shows how to use [UrlPrefixes](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.urlprefixes#microsoft-aspnetcore-server-httpsys-httpsysoptions-urlprefixes) with the server's local IP address `10.0.0.4` on port 443:

   ```
   var builder = WebApplication.CreateBuilder(args);

   builder.WebHost.UseHttpSys(options =>
   {
       options.UrlPrefixes.Add("https://10.0.0.4:443");
   });

   builder.Services.AddRazorPages();

   var app = builder.Build();
   ```

   An advantage of `UrlPrefixes` is that an error message is generated immediately for improperly formatted prefixes.

   The settings in `UrlPrefixes` override `UseUrls`/`urls`/`ASPNETCORE_URLS` settings. Therefore, an advantage of `UseUrls`, `urls`, and the `ASPNETCORE_URLS` environment variable is that it's easier to switch between Kestrel and HTTP.sys.

   HTTP.sys recognizes two types of wild cards in URL prefixes:

   * `*` is a *weak binding*, also known as a *fallback binding*. If the URL prefix is `http://*:5000`, and something else is bound to port 5000, this binding won't be used.
   * `+` is a *strong binding*. If the URL prefix is `http://+:5000`, this binding will be used before other port 5000 bindings.

   For more information, see [UrlPrefix Strings](/en-us/windows/win32/http/urlprefix-strings).

   Warning

   Top-level wildcard bindings (`http://*:80/` and `http://+:80`) should **not** be used. Top-level wildcard bindings create app security vulnerabilities. This applies to both strong and weak wildcards. Use explicit host names or IP addresses rather than wildcards. Subdomain wildcard binding (for example, `*.mysub.com`) isn't a security risk if you control the entire parent domain (as opposed to `*.com`, which is vulnerable). For more information, see [RFC 9110: Section 7.2: Host and :authority](https://www.rfc-editor.org/rfc/rfc9110#field.host).

   Most configurations for apps and containers define only a port for listening, like port 80, without specifying other constraints like the host or path. `HTTP_PORTS` and `HTTPS_PORTS` are config keys that specify the listening ports for the Kestrel and HTTP.sys servers. You can specify the keys as environment variables defined with the `DOTNET_` or `ASPNETCORE_` prefixes, or set them directly through any other config input, such as the `appsettings.json` file. Each configuration is a semicolon-delimited list of port values, as shown in the following example:

   ```
   ASPNETCORE_HTTP_PORTS=80;8080
   ASPNETCORE_HTTPS_PORTS=443;8081
   ```

   The configuration in the example is shorthand for the following specification, which defines the scheme (HTTP or HTTPS) and any host or IP:

   ```
   ASPNETCORE_URLS=http://*:80/;http://*:8080/;https://*:443/;https://*:8081/
   ```

   The `HTTP_PORTS` and `HTTPS_PORTS` configuration keys are lower priority. If other URLs or values are set directly in code, they can override the configuration keys. Configure certificates separately using server-specific mechanics for HTTPS.

   Note

   Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

   These configuration keys are equivalent to top-level wildcard bindings. They're convenient for development and container scenarios, but avoid wildcards when running on a machine that may also host other services.

6. Preregister URL prefixes on the server.

   The built-in tool for configuring HTTP.sys is *netsh.exe*. *netsh.exe* is used to reserve URL prefixes and assign X.509 certificates. The tool requires administrator privileges.

   Use the *netsh.exe* tool to register URLs for the app:

   ```
   netsh http add urlacl url=<URL> user=<USER>
   ```

   * `<URL>`: The fully qualified Uniform Resource Locator (URL). Don't use a wildcard binding. Use a valid hostname or local IP address. *The URL must include a trailing slash.*
   * `<USER>`: Specifies the user or user-group name.

   In the following example, the local IP address of the server is `10.0.0.4`:

   ```
   netsh http add urlacl url=https://10.0.0.4:443/ user=Users
   ```

   When a URL is registered, the tool responds with `URL reservation successfully added`.

   To delete a registered URL, use the `delete urlacl` command:

   ```
   netsh http delete urlacl url=<URL>
   ```

7. Register X.509 certificates on the server.

   Use the *netsh.exe* tool to register certificates for the app:

   ```
   netsh http add sslcert ipport=<IP>:<PORT> certhash=<THUMBPRINT> appid="{<GUID>}"
   ```

   * `<IP>`: Specifies the local IP address for the binding. Don't use a wildcard binding. Use a valid IP address.
   * `<PORT>`: Specifies the port for the binding.
   * `<THUMBPRINT>`: The X.509 certificate thumbprint.
   * `<GUID>`: A developer-generated GUID to represent the app for informational purposes.

   For reference purposes, store the GUID in the app as a package tag:

   * In Visual Studio:

     * Open the app's project properties by right-clicking on the app in **Solution Explorer** and selecting **Properties**.
     * Select the **Package** tab.
     * Enter the GUID that you created in the **Tags** field.

   * When not using Visual Studio:

     * Open the app's project file.

     * Add a `<PackageTags>` property to a new or existing `<PropertyGroup>` with the GUID that you created:

       ```
       <PropertyGroup>
         <PackageTags>00001111-aaaa-2222-bbbb-3333cccc4444</PackageTags>
       </PropertyGroup>
       ```

   In the following example:

   * The local IP address of the server is `10.0.0.4`.
   * An online random GUID generator provides the `appid` value.

   ```
   netsh http add sslcert 
       ipport=10.0.0.4:443 
       certhash=b66ee04419d4ee37464ab8785ff02449980eae10 
       appid="{00001111-aaaa-2222-bbbb-3333cccc4444}"
   ```

   When a certificate is registered, the tool responds with `SSL Certificate successfully added`.

   To delete a certificate registration, use the `delete sslcert` command:

   ```
   netsh http delete sslcert ipport=<IP>:<PORT>
   ```

   Reference documentation for *netsh.exe*:

   * [Netsh Commands for Hypertext Transfer Protocol (HTTP)](/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/cc725882\(v=ws.10\))
   * [UrlPrefix Strings](/en-us/windows/win32/http/urlprefix-strings)

8. Run the app.

   Administrator privileges aren't required to run the app when binding to localhost using HTTP (not HTTPS) with a port number greater than 1024. For other configurations (for example, using a local IP address or binding to port 443), run the app with administrator privileges.

   The app responds at the server's public IP address. In this example, the server is reached from the Internet at its public IP address of `104.214.79.47`.

   A development certificate is used in this example. The page loads securely after bypassing the browser's untrusted certificate warning.

## Proxy server and load balancer scenarios

For apps hosted by HTTP.sys that interact with requests from the Internet or a corporate network, additional configuration might be required when hosting behind proxy servers and load balancers. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

[]()

## Get detailed timing information with IHttpSysRequestTimingFeature

[IHttpSysRequestTimingFeature](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.ihttpsysrequesttimingfeature) provides detailed timing information for requests:

* Timestamps are obtained using [QueryPerformanceCounter](/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter).
* The timestamp frequency can be obtained via [QueryPerformanceFrequency](/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency).
* The index of the timing can be cast to [HttpSysRequestTimingType](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysrequesttimingtype) to know what the timing represents.
* The value may be 0 if the timing isn't available for the current request.
* Requires Windows 10 version 2004, Windows Server 2022, or later.

```
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys();

var app = builder.Build();

app.Use((context, next) =>
{
    var feature = context.Features.GetRequiredFeature<IHttpSysRequestTimingFeature>();
    
    var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("Sample");

    var timestamps = feature.Timestamps;

    for (var i = 0; i < timestamps.Length; i++)
    {
        var timestamp = timestamps[i];
        var timingType = (HttpSysRequestTimingType)i;

        logger.LogInformation("Timestamp {timingType}: {timestamp}",
                                          timingType, timestamp);
    }

    return next(context);
});

app.MapGet("/", () => Results.Ok());

app.Run();
```

[IHttpSysRequestTimingFeature.TryGetTimestamp](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.ihttpsysrequesttimingfeature.trygettimestamp) retrieves the timestamp for the provided timing type:

```
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys();

var app = builder.Build();

app.Use((context, next) =>
{
    var feature = context.Features.GetRequiredFeature<IHttpSysRequestTimingFeature>();

    var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("Sample");

    var timingType = HttpSysRequestTimingType.RequestRoutingEnd;

    if (feature.TryGetTimestamp(timingType, out var timestamp))
    {
        logger.LogInformation("Timestamp {timingType}: {timestamp}",
                                          timingType, timestamp);
    }
    else
    {
        logger.LogInformation("Timestamp {timingType}: not available for the "
                                           + "current request",    timingType);
    }

    return next(context);
});

app.MapGet("/", () => Results.Ok());

app.Run();
```

\[IHttpSysRequestTimingFeature.TryGetElapsedTime]\(/dotnet/api/microsoft.aspnetcore.server.httpsys.ihttpsysrequesttimingfeature.trygetelapsedtime yields the elapsed time between two specified timings:

```
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys();

var app = builder.Build();

app.Use((context, next) =>
{
    var feature = context.Features.GetRequiredFeature<IHttpSysRequestTimingFeature>();

    var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("Sample");

    var startingTimingType = HttpSysRequestTimingType.RequestRoutingStart;
    var endingTimingType = HttpSysRequestTimingType.RequestRoutingEnd;

    if (feature.TryGetElapsedTime(startingTimingType, endingTimingType, out var elapsed))
    {
        logger.LogInformation(
            "Elapsed time {startingTimingType} to {endingTimingType}: {elapsed}",
            startingTimingType,
            endingTimingType,
            elapsed);
    }
    else
    {
        logger.LogInformation(
            "Elapsed time {startingTimingType} to {endingTimingType}:"
            + " not available for the current request.",
            startingTimingType,
            endingTimingType);
    }

    return next(context);
});

app.MapGet("/", () => Results.Ok());

app.Run();
```

## Advanced HTTP/2 features to support gRPC

Additional HTTP/2 features in HTTP.sys support gRPC, including support for response trailers and sending reset frames.

Requirements to run gRPC with HTTP.sys:

* Windows 11 Build 22000 or later, Windows Server 2022 Build 20348 or later.
* TLS 1.2 or later connection.

### Trailers

HTTP Trailers are similar to HTTP Headers, except they are sent after the response body is sent. For IIS and HTTP.sys, only HTTP/2 response trailers are supported.

```
if (httpContext.Response.SupportsTrailers())
{
    httpContext.Response.DeclareTrailer("trailername");	

    // Write body
    httpContext.Response.WriteAsync("Hello world");

    httpContext.Response.AppendTrailer("trailername", "TrailerValue");
}
```

In the preceding example code:

* `SupportsTrailers` ensures that trailers are supported for the response.
* `DeclareTrailer` adds the given trailer name to the `Trailer` response header. Declaring a response's trailers is optional, but recommended. If `DeclareTrailer` is called, it must be before the response headers are sent.
* `AppendTrailer` appends the trailer.

### Reset

Reset allows for the server to reset a HTTP/2 request with a specified error code. A reset request is considered aborted.

```
var resetFeature = httpContext.Features.Get<IHttpResetFeature>();
resetFeature.Reset(errorCode: 2);
```

`Reset` in the preceding code example specifies the `INTERNAL_ERROR` error code. For more information about HTTP/2 error codes, visit the [HTTP/2 specification error code section](https://tools.ietf.org/html/rfc7540#page-50).

## Tracing

For information about how to get traces from HTTP.sys, see [HTTP.sys Manageability Scenarios](/en-us/windows/win32/http/http-sys-manageability-scenarios).

## Related content

* [Enable Windows Authentication with HTTP.sys](../../security/authentication/windowsauth?view=aspnetcore-10.0#httpsys)
* [HTTP Server API](/en-us/windows/win32/http/http-api-start-page)
* [aspnet/HttpSysServer GitHub repository (source code)](https://github.com/aspnet/HttpSysServer/)
* [The host](../?view=aspnetcore-10.0#host)
* [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor tutorials

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The following tutorials provide basic working experiences for building Blazor apps.

For an overview of Blazor, see [ASP.NET Core Blazor](../?view=aspnetcore-10.0).

* [Build your first Blazor app](https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/intro)

* [Build a Blazor todo list app](build-a-blazor-app?view=aspnetcore-10.0) (Blazor Web App)

* [Build a Blazor movie database app (Overview)](movie-database-app/?view=aspnetcore-10.0) (Blazor Web App)

* [Use ASP.NET Core SignalR with Blazor](signalr-blazor?view=aspnetcore-10.0) (Blazor Web App)

* [ASP.NET Core Blazor Hybrid tutorials](../hybrid/tutorials/?view=aspnetcore-10.0)

* Microsoft Learn

  * [Blazor Learning Path](/en-us/training/paths/build-web-apps-with-blazor/)
  * [Blazor Learn Modules](/en-us/training/browse/?expanded=dotnet\&products=blazor)

For an overview of Blazor, see [ASP.NET Core Blazor](../?view=aspnetcore-10.0). For more information on the Blazor hosting models, Blazor Server and Blazor WebAssembly, see [ASP.NET Core Blazor hosting models](../hosting-models?view=aspnetcore-10.0).

* [Build your first Blazor app](https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/intro) (Blazor Server)

* [Build a Blazor todo list app](build-a-blazor-app?view=aspnetcore-10.0) (Blazor Server or Blazor WebAssembly)

* [Use ASP.NET Core SignalR with Blazor](signalr-blazor?view=aspnetcore-10.0) (Blazor Server or Blazor WebAssembly)

* [ASP.NET Core Blazor Hybrid tutorials](../hybrid/tutorials/?view=aspnetcore-10.0)

* Microsoft Learn

  * [Blazor Learning Path](/en-us/training/paths/build-web-apps-with-blazor/)
  * [Blazor Learn Modules](/en-us/training/browse/?expanded=dotnet\&products=blazor)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/web-api/advanced/formatting.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Format response data in ASP.NET Core Web API

ASP.NET Core MVC supports formatting response data, using specified formats or in response to a client's request.

## Format-specific Action Results

Some action result types are specific to a particular format, such as [JsonResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonresult) and [ContentResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.contentresult). Actions can return results that always use a specified format, ignoring a client's request for a different format. For example, returning `JsonResult` returns JSON-formatted data and returning `ContentResult` returns plain-text-formatted string data.

An action isn't required to return any specific type. ASP.NET Core supports any object return value. Results from actions that return objects that aren't [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) types are serialized using the appropriate [IOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.ioutputformatter) implementation. For more information, see [Controller action return types in ASP.NET Core web API](../action-return-types?view=aspnetcore-10.0).

By default, the built-in helper method [ControllerBase.Ok](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.ok) returns JSON-formatted data:

```
[HttpGet]
public IActionResult Get() =>
    Ok(_todoItemStore.GetList());
```

The sample code returns a list of todo items. Using the F12 browser developer tools or [http-repl](../http-repl/?view=aspnetcore-10.0) with the previous code displays:

* The response header containing **content-type:** `application/json; charset=utf-8`.
* The request headers. For example, the `Accept` header. The `Accept` header is ignored by the preceding code.

To return plain text formatted data, use [ContentResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.contentresult) and the [Content](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.content) helper:

```
[HttpGet("Version")]
public ContentResult GetVersion() =>
    Content("v1.0.0");
```

In the preceding code, the `Content-Type` returned is `text/plain`.

For actions with multiple return types, return `IActionResult`. For example, when returning different HTTP status codes based on the result of the operation.

## Content negotiation

Content negotiation occurs when the client specifies an [Accept header](https://www.rfc-editor.org/rfc/rfc9110#field.accept). The default format used by ASP.NET Core is [JSON](https://json.org/). Content negotiation is:

* Implemented by [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult).
* Built into the status code-specific action results returned from the helper methods. The action results helper methods are based on `ObjectResult`.

When a model type is returned, the return type is `ObjectResult`.

The following action method uses the `Ok` and `NotFound` helper methods:

```
[HttpGet("{id:long}")]
public IActionResult GetById(long id)
{
    var todo = _todoItemStore.GetById(id);

    if (todo is null)
    {
        return NotFound();
    }

    return Ok(todo);
}
```

By default, ASP.NET Core supports the following media types:

* `application/json`
* `text/json`
* `text/plain`

Tools such as [Fiddler](https://www.telerik.com/fiddler) or curl can set the `Accept` request header to specify the return format. When the `Accept` header contains a type the server supports, that type is returned. The next section shows how to add additional formatters.

Controller actions can return POCOs (Plain Old CLR Objects). When a POCO is returned, the runtime automatically creates an `ObjectResult` that wraps the object. The client gets the formatted serialized object. If the object being returned is `null`, a `204 No Content` response is returned.

The following example returns an object type:

```
[HttpGet("{id:long}")]
public TodoItem? GetById(long id) =>
    _todoItemStore.GetById(id);
```

In the preceding code, a request for a valid todo item returns a `200 OK` response. A request for an invalid todo item returns a `204 No Content` response.

### The Accept header

Content *negotiation* takes place when an `Accept` header appears in the request. When a request contains an accept header, ASP.NET Core:

* Enumerates the media types in the accept header in preference order.
* Tries to find a formatter that can produce a response in one of the formats specified.

If no formatter is found that can satisfy the client's request, ASP.NET Core:

* Returns `406 Not Acceptable` if [MvcOptions.ReturnHttpNotAcceptable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.returnhttpnotacceptable#microsoft-aspnetcore-mvc-mvcoptions-returnhttpnotacceptable) is set to `true`, or -
* Tries to find the first formatter that can produce a response.

If no formatter is configured for the requested format, the first formatter that can format the object is used. If no `Accept` header appears in the request:

* The first formatter that can handle the object is used to serialize the response.
* There isn't any negotiation taking place. The server is determining what format to return.

If the Accept header contains `*/*`, the Header is ignored unless `RespectBrowserAcceptHeader` is set to true on [MvcOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions).

### Browsers and content negotiation

Unlike typical API clients, web browsers supply `Accept` headers. Web browsers specify many formats, including wildcards. By default, when the framework detects that the request is coming from a browser:

* The `Accept` header is ignored.
* The content is returned using the first registered output formatter that can handle the response type, unless otherwise configured.

This approach provides a more consistent experience across browsers when consuming APIs.

To configure an app to respect browser accept headers, set the [RespectBrowserAcceptHeader](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.respectbrowseracceptheader#microsoft-aspnetcore-mvc-mvcoptions-respectbrowseracceptheader) property to `true`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.RespectBrowserAcceptHeader = true;
});
```

### Configure formatters

Apps that need to support extra formats can add the appropriate NuGet packages and configure support. There are separate formatters for input and output. Input formatters are used by [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0). Output formatters are used to format responses. For information on creating a custom formatter, see [Custom Formatters](custom-formatters?view=aspnetcore-10.0).

### Add XML format support

To configure XML formatters implemented using [XmlSerializer](/en-us/dotnet/api/system.xml.serialization.xmlserializer), call [AddXmlSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvcbuilderextensions.addxmlserializerformatters):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddXmlSerializerFormatters();
```

When using the preceding code, controller methods return the appropriate format based on the request's `Accept` header.

### Configure `System.Text.Json`-based formatters

To configure features for the `System.Text.Json`-based formatters, use [Microsoft.AspNetCore.Mvc.JsonOptions.JsonSerializerOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions.jsonserializeroptions#microsoft-aspnetcore-mvc-jsonoptions-jsonserializeroptions). The following highlighted code configures PascalCase formatting instead of the default camelCase formatting:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.PropertyNamingPolicy = null;
    });
```

The following action method calls [ControllerBase.Problem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.problem) to create a [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails) response:

```
[HttpGet("Error")]
public IActionResult GetError() =>
    Problem("Something went wrong.");
```

A `ProblemDetails` response is always camelCase, even when the app sets the format to PascalCase. `ProblemDetails` follows [RFC 7807](https://tools.ietf.org/html/rfc7807#appendix-A), which specifies lowercase.

To configure output serialization options for specific actions, use `JsonResult`. For example:

```
[HttpGet]
public IActionResult Get() =>
    new JsonResult(
        _todoItemStore.GetList(),
        new JsonSerializerOptions
        {
            PropertyNamingPolicy = null
        });
```

### Add `Newtonsoft.Json`-based JSON format support

The default JSON formatters use `System.Text.Json`. To use the `Newtonsoft.Json`-based formatters, install the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package and configure it in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddNewtonsoftJson();
```

In the preceding code, the call to `AddNewtonsoftJson` configures the following Web API, MVC, and Razor Pages features to use `Newtonsoft.Json`:

* Input and output formatters that read and write JSON
* [JsonResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonresult)
* [JSON Patch](../jsonpatch?view=aspnetcore-10.0)
* [IJsonHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ijsonhelper)
* [TempData](../../fundamentals/app-state?view=aspnetcore-10.0#tempdata)

Some features may not work well with `System.Text.Json`-based formatters and require a reference to the `Newtonsoft.Json`-based formatters. Continue using the `Newtonsoft.Json`-based formatters when the app:

* Uses `Newtonsoft.Json` attributes. For example, `[JsonProperty]` or `[JsonIgnore]`.
* Customizes the serialization settings.
* Relies on features that `Newtonsoft.Json` provides.

To configure features for the `Newtonsoft.Json`-based formatters, use [SerializerSettings](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcnewtonsoftjsonoptions.serializersettings):

```
builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ContractResolver = new DefaultContractResolver();
    });
```

To configure output serialization options for specific actions, use `JsonResult`. For example:

```
[HttpGet]
public IActionResult GetNewtonsoftJson() =>
    new JsonResult(
        _todoItemStore.GetList(),
        new JsonSerializerSettings
        {
            ContractResolver = new DefaultContractResolver()
        });
```

### Specify a format

To restrict the response formats, apply the [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) filter. Like most [Filters](../../mvc/controllers/filters?view=aspnetcore-10.0), `[Produces]` can be applied at the action, controller, or global scope:

```
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class TodoItemsController : ControllerBase
```

The preceding [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) filter:

* Forces all actions within the controller to return JSON-formatted responses for POCOs (Plain Old CLR Objects) or [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult) and its derived types.
* Return JSON-formatted responses even if other formatters are configured and the client specifies a different format.

For more information, see [Filters](../../mvc/controllers/filters?view=aspnetcore-10.0).

### Special case formatters

Some special cases are implemented using built-in formatters. By default, `string` return types are formatted as *text/plain* (*text/html* if requested via the `Accept` header). This behavior can be deleted by removing the [StringOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.stringoutputformatter). Formatters are removed in `Program.cs`. Actions that have a model object return type return `204 No Content` when returning `null`. This behavior can be deleted by removing the [HttpNoContentOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.httpnocontentoutputformatter). The following code removes the `StringOutputFormatter` and `HttpNoContentOutputFormatter`.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    // using Microsoft.AspNetCore.Mvc.Formatters;
    options.OutputFormatters.RemoveType<StringOutputFormatter>();
    options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
});
```

Without the `StringOutputFormatter`, the built-in JSON formatter formats `string` return types. If the built-in JSON formatter is removed and an XML formatter is available, the XML formatter formats `string` return types. Otherwise, `string` return types return `406 Not Acceptable`.

Without the `HttpNoContentOutputFormatter`, null objects are formatted using the configured formatter. For example:

* The JSON formatter returns a response with a body of `null`.
* The XML formatter returns an empty XML element with the attribute `xsi:nil="true"` set.

## Response format URL mappings

Clients can request a particular format as part of the URL, for example:

* In the query string or part of the path.
* By using a format-specific file extension such as .xml or .json.

The mapping from request path should be specified in the route the API is using. For example:

```
[ApiController]
[Route("api/[controller]")]
[FormatFilter]
public class TodoItemsController : ControllerBase
{
    private readonly TodoItemStore _todoItemStore;

    public TodoItemsController(TodoItemStore todoItemStore) =>
        _todoItemStore = todoItemStore;

    [HttpGet("{id:long}.{format?}")]
    public TodoItem? GetById(long id) =>
        _todoItemStore.GetById(id);
```

The preceding route allows the requested format to be specified using an optional file extension. The [`[FormatFilter]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatfilterattribute) attribute checks for the existence of the format value in the `RouteData` and maps the response format to the appropriate formatter when the response is created.

| Route                   | Formatter                          |
| ----------------------- | ---------------------------------- |
| `/api/todoitems/5`      | The default output formatter       |
| `/api/todoitems/5.json` | The JSON formatter (if configured) |
| `/api/todoitems/5.xml`  | The XML formatter (if configured)  |

## Polymorphic deserialization

Built-in features provide a limited range of polymorphic serialization but no support for deserialization at all. Deserialization requires a custom converter. See [Polymorphic deserialization](/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#support-polymorphic-deserialization) for a complete sample of polymorphic deserialization.

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/formatting/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

ASP.NET Core MVC has support for formatting response data. Response data can be formatted using specific formats or in response to client requested format.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/formatting/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Format-specific Action Results

Some action result types are specific to a particular format, such as [JsonResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonresult) and [ContentResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.contentresult). Actions can return results that are formatted in a particular format, regardless of client preferences. For example, returning `JsonResult` returns JSON-formatted data. Returning `ContentResult` or a string returns plain-text-formatted string data.

An action isn't required to return any specific type. ASP.NET Core supports any object return value. Results from actions that return objects that aren't [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) types are serialized using the appropriate [IOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.ioutputformatter) implementation. For more information, see [Controller action return types in ASP.NET Core web API](../action-return-types?view=aspnetcore-10.0).

The built-in helper method [Ok](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.ok) returns JSON-formatted data:

```
// GET: api/authors
[HttpGet]
public ActionResult Get()
{
    return Ok(_authors.List());
}
```

The sample download returns the list of authors. Using the F12 browser developer tools or [http-repl](../http-repl/?view=aspnetcore-10.0) with the previous code:

* The response header containing **content-type:** `application/json; charset=utf-8` is displayed.
* The request headers are displayed. For example, the `Accept` header. The `Accept` header is ignored by the preceding code.

To return plain text formatted data, use [ContentResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.contentresult) and the [Content](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.content) helper:

```
// GET api/authors/about
[HttpGet("About")]
public ContentResult About()
{
    return Content("An API listing authors of docs.asp.net.");
}
```

In the preceding code, the `Content-Type` returned is `text/plain`. Returning a string delivers `Content-Type` of `text/plain`:

```
// GET api/authors/version
[HttpGet("version")]
public string Version()
{
    return "Version 1.0.0";
}
```

For actions with multiple return types, return `IActionResult`. For example, returning different HTTP status codes based on the result of operations performed.

## Content negotiation

Content negotiation occurs when the client specifies an [Accept header](https://www.rfc-editor.org/rfc/rfc9110#field.accept). The default format used by ASP.NET Core is [JSON](https://json.org/). Content negotiation is:

* Implemented by [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult).
* Built into the status code-specific action results returned from the helper methods. The action results helper methods are based on `ObjectResult`.

When a model type is returned, the return type is `ObjectResult`.

The following action method uses the `Ok` and `NotFound` helper methods:

```
// GET: api/authors/search?namelike=th
[HttpGet("Search")]
public IActionResult Search(string namelike)
{
    var result = _authors.GetByNameSubstring(namelike);
    if (!result.Any())
    {
        return NotFound(namelike);
    }
    return Ok(result);
}
```

By default, ASP.NET Core supports `application/json`, `text/json`, and `text/plain` media types. Tools such as [Fiddler](https://www.telerik.com/fiddler) or [http-repl](../http-repl/?view=aspnetcore-10.0) can set the `Accept` request header to specify the return format. When the `Accept` header contains a type the server supports, that type is returned. The next section shows how to add additional formatters.

Controller actions can return POCOs (Plain Old CLR Objects). When a POCO is returned, the runtime automatically creates an `ObjectResult` that wraps the object. The client gets the formatted serialized object. If the object being returned is `null`, a `204 No Content` response is returned.

Returning an object type:

```
// GET api/authors/RickAndMSFT
[HttpGet("{alias}")]
public Author Get(string alias)
{
    return _authors.GetByAlias(alias);
}
```

In the preceding code, a request for a valid author alias returns a `200 OK` response with the author's data. A request for an invalid alias returns a `204 No Content` response.

### The Accept header

Content *negotiation* takes place when an `Accept` header appears in the request. When a request contains an accept header, ASP.NET Core:

* Enumerates the media types in the accept header in preference order.
* Tries to find a formatter that can produce a response in one of the formats specified.

If no formatter is found that can satisfy the client's request, ASP.NET Core:

* Returns `406 Not Acceptable` if [MvcOptions.ReturnHttpNotAcceptable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.returnhttpnotacceptable#microsoft-aspnetcore-mvc-mvcoptions-returnhttpnotacceptable) is set to `true`, or -
* Tries to find the first formatter that can produce a response.

If no formatter is configured for the requested format, the first formatter that can format the object is used. If no `Accept` header appears in the request:

* The first formatter that can handle the object is used to serialize the response.
* There isn't any negotiation taking place. The server is determining what format to return.

If the Accept header contains `*/*`, the Header is ignored unless `RespectBrowserAcceptHeader` is set to true on [MvcOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions).

### Browsers and content negotiation

Unlike typical API clients, web browsers supply `Accept` headers. Web browsers specify many formats, including wildcards. By default, when the framework detects that the request is coming from a browser:

* The `Accept` header is ignored.
* The content is returned using the first registered output formatter that can handle the response type, unless otherwise configured.

This approach provides a more consistent experience across browsers when consuming APIs.

To configure an app to honor browser accept headers, set [RespectBrowserAcceptHeader](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.respectbrowseracceptheader#microsoft-aspnetcore-mvc-mvcoptions-respectbrowseracceptheader) to `true`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.RespectBrowserAcceptHeader = true; // false by default
    });
}
```

### Configure formatters

Apps that need to support additional formats can add the appropriate NuGet packages and configure support. There are separate formatters for input and output. Input formatters are used by [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0). Output formatters are used to format responses. For information on creating a custom formatter, see [Custom Formatters](custom-formatters?view=aspnetcore-10.0).

### Add XML format support

XML formatters implemented using [XmlSerializer](/en-us/dotnet/api/system.xml.serialization.xmlserializer) are configured by calling [AddXmlSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvcbuilderextensions.addxmlserializerformatters):

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddXmlSerializerFormatters();
}
```

The preceding code serializes results using `XmlSerializer`.

When using the preceding code, controller methods return the appropriate format based on the request's `Accept` header.

### Configure `System.Text.Json` based formatters

Features for the `System.Text.Json` based formatters can be configured using [Microsoft.AspNetCore.Mvc.JsonOptions.JsonSerializerOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions.jsonserializeroptions#microsoft-aspnetcore-mvc-jsonoptions-jsonserializeroptions). The default formatting is camelCase. The following highlighted code sets PascalCase formatting:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddJsonOptions(options =>
            options.JsonSerializerOptions.PropertyNamingPolicy = null);
}
```

The following action method calls [ControllerBase.Problem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.problem) to create a [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails) response:

```
[HttpGet("error")]
public IActionResult GetError()
{
    return Problem("Something went wrong!");
}
```

With the preceding code:

* `https://localhost:5001/WeatherForecast/temperature` returns PascalCase.
* `https://localhost:5001/WeatherForecast/error` returns camelCase. The error response is always camelCase, even when the app sets the format to PascalCase. `ProblemDetails` follows [RFC 7807](https://tools.ietf.org/html/rfc7807#appendix-A), which specifies lower case

The following code sets PascalCase and adds a custom converter:

```
services.AddControllers().AddJsonOptions(options =>
{
    // Use the default property (Pascal) casing.
    options.JsonSerializerOptions.PropertyNamingPolicy = null;

    // Configure a custom converter.
    options.JsonSerializerOptions.Converters.Add(new MyCustomJsonConverter());
});
```

Output serialization options, on a per-action basis, can be configured using `JsonResult`. For example:

```
public IActionResult Get()
{
    return Json(model, new JsonSerializerOptions
    {
        WriteIndented = true,
    });
}
```

### Add Newtonsoft.Json-based JSON format support

The default JSON formatters are based on `System.Text.Json`. Support for `Newtonsoft.Json` based formatters and features is available by installing the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package and configuring it in `Startup.ConfigureServices`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddNewtonsoftJson();
}
```

In the preceding code, the call to `AddNewtonsoftJson` configures the following Web API, MVC, and Razor Pages features to use `Newtonsoft.Json`:

* Input and output formatters that read and write JSON
* [JsonResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonresult)
* [JSON Patch](../jsonpatch?view=aspnetcore-10.0)
* [IJsonHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ijsonhelper)
* [TempData](../../fundamentals/app-state?view=aspnetcore-10.0#tempdata)

Some features may not work well with `System.Text.Json`-based formatters and require a reference to the `Newtonsoft.Json`-based formatters. Continue using the `Newtonsoft.Json`-based formatters when the app:

* Uses `Newtonsoft.Json` attributes. For example, `[JsonProperty]` or `[JsonIgnore]`.
* Customizes the serialization settings.
* Relies on features that `Newtonsoft.Json` provides.

Features for the `Newtonsoft.Json`-based formatters can be configured using `Microsoft.AspNetCore.Mvc.MvcNewtonsoftJsonOptions.SerializerSettings`:

```
services.AddControllers().AddNewtonsoftJson(options =>
{
    // Use the default property (Pascal) casing
    options.SerializerSettings.ContractResolver = new DefaultContractResolver();

    // Configure a custom converter
    options.SerializerSettings.Converters.Add(new MyCustomJsonConverter());
});
```

Output serialization options, on a per-action basis, can be configured using `JsonResult`. For example:

```
public IActionResult Get()
{
    return Json(model, new JsonSerializerSettings
    {
        Formatting = Formatting.Indented,
    });
}
```

### Specify a format

To restrict the response formats, apply the [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) filter. Like most [Filters](../../mvc/controllers/filters?view=aspnetcore-10.0), `[Produces]` can be applied at the action, controller, or global scope:

```
[ApiController]
[Route("[controller]")]
[Produces("application/json")]
public class WeatherForecastController : ControllerBase
{
```

The preceding [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) filter:

* Forces all actions within the controller to return JSON-formatted responses for POCOs (Plain Old CLR Objects) or [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult) and its derived types.
* If other formatters are configured and the client specifies a different format, JSON is returned.

For more information, see [Filters](../../mvc/controllers/filters?view=aspnetcore-10.0).

### Special case formatters

Some special cases are implemented using built-in formatters. By default, `string` return types are formatted as *text/plain* (*text/html* if requested via the `Accept` header). This behavior can be deleted by removing the [StringOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.stringoutputformatter). Formatters are removed in the `ConfigureServices` method. Actions that have a model object return type return `204 No Content` when returning `null`. This behavior can be deleted by removing the [HttpNoContentOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.httpnocontentoutputformatter). The following code removes the `StringOutputFormatter` and `HttpNoContentOutputFormatter`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        // requires using Microsoft.AspNetCore.Mvc.Formatters;
        options.OutputFormatters.RemoveType<StringOutputFormatter>();
        options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
    });
}
```

Without the `StringOutputFormatter`, the built-in JSON formatter formats `string` return types. If the built-in JSON formatter is removed and an XML formatter is available, the XML formatter formats `string` return types. Otherwise, `string` return types return `406 Not Acceptable`.

Without the `HttpNoContentOutputFormatter`, null objects are formatted using the configured formatter. For example:

* The JSON formatter returns a response with a body of `null`.
* The XML formatter returns an empty XML element with the attribute `xsi:nil="true"` set.

## Response format URL mappings

Clients can request a particular format as part of the URL, for example:

* In the query string or part of the path.
* By using a format-specific file extension such as .xml or .json.

The mapping from request path should be specified in the route the API is using. For example:

```
[Route("api/[controller]")]
[ApiController]
[FormatFilter]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}.{format?}")]
    public Product Get(int id)
    {
```

The preceding route allows the requested format to be specified as an optional file extension. The [`[FormatFilter]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatfilterattribute) attribute checks for the existence of the format value in the `RouteData` and maps the response format to the appropriate formatter when the response is created.

| Route                  | Formatter                          |
| ---------------------- | ---------------------------------- |
| `/api/products/5`      | The default output formatter       |
| `/api/products/5.json` | The JSON formatter (if configured) |
| `/api/products/5.xml`  | The XML formatter (if configured)  |

ASP.NET Core MVC supports formatting response data, using specified formats or in response to a client's request.

## Format-specific Action Results

Some action result types are specific to a particular format, such as [JsonResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonresult) and [ContentResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.contentresult). Actions can return results that always use a specified format, ignoring a client's request for a different format. For example, returning `JsonResult` returns JSON-formatted data and returning `ContentResult` returns plain-text-formatted string data.

An action isn't required to return any specific type. ASP.NET Core supports any object return value. Results from actions that return objects that aren't [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) types are serialized using the appropriate [IOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.ioutputformatter) implementation. For more information, see [Controller action return types in ASP.NET Core web API](../action-return-types?view=aspnetcore-10.0).

By default, the built-in helper method [ControllerBase.Ok](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.ok) returns JSON-formatted data:

```
[HttpGet]
public IActionResult Get()
    => Ok(_todoItemStore.GetList());
```

The sample code returns a list of todo items. Using the F12 browser developer tools or [http-repl](../http-repl/?view=aspnetcore-10.0) with the previous code displays:

* The response header containing **content-type:** `application/json; charset=utf-8`.
* The request headers. For example, the `Accept` header. The `Accept` header is ignored by the preceding code.

To return plain text formatted data, use [ContentResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.contentresult) and the [Content](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.content) helper:

```
[HttpGet("Version")]
public ContentResult GetVersion()
    => Content("v1.0.0");
```

In the preceding code, the `Content-Type` returned is `text/plain`.

For actions with multiple return types, return `IActionResult`. For example, when returning different HTTP status codes based on the result of the operation.

## Content negotiation

Content negotiation occurs when the client specifies an [Accept header](https://www.rfc-editor.org/rfc/rfc9110#field.accept). The default format used by ASP.NET Core is [JSON](https://json.org/). Content negotiation is:

* Implemented by [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult).
* Built into the status code-specific action results returned from the helper methods. The action results helper methods are based on `ObjectResult`.

When a model type is returned, the return type is `ObjectResult`.

The following action method uses the `Ok` and `NotFound` helper methods:

```
[HttpGet("{id:long}")]
public IActionResult GetById(long id)
{
    var todo = _todoItemStore.GetById(id);

    if (todo is null)
    {
        return NotFound();
    }

    return Ok(todo);
}
```

By default, ASP.NET Core supports the following media types:

* `application/json`
* `text/json`
* `text/plain`

Tools such as [Fiddler](https://www.telerik.com/fiddler) or [http-repl](../http-repl/?view=aspnetcore-10.0) can set the `Accept` request header to specify the return format. When the `Accept` header contains a type the server supports, that type is returned. The next section shows how to add additional formatters.

Controller actions can return POCOs (Plain Old CLR Objects). When a POCO is returned, the runtime automatically creates an `ObjectResult` that wraps the object. The client gets the formatted serialized object. If the object being returned is `null`, a `204 No Content` response is returned.

The following example returns an object type:

```
[HttpGet("{id:long}")]
public TodoItem? GetById(long id)
    => _todoItemStore.GetById(id);
```

In the preceding code, a request for a valid todo item returns a `200 OK` response. A request for an invalid todo item returns a `204 No Content` response.

### The Accept header

Content *negotiation* takes place when an `Accept` header appears in the request. When a request contains an accept header, ASP.NET Core:

* Enumerates the media types in the accept header in preference order.
* Tries to find a formatter that can produce a response in one of the formats specified.

If no formatter is found that can satisfy the client's request, ASP.NET Core:

* Returns `406 Not Acceptable` if [MvcOptions.ReturnHttpNotAcceptable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.returnhttpnotacceptable#microsoft-aspnetcore-mvc-mvcoptions-returnhttpnotacceptable) is set to `true`, or -
* Tries to find the first formatter that can produce a response.

If no formatter is configured for the requested format, the first formatter that can format the object is used. If no `Accept` header appears in the request:

* The first formatter that can handle the object is used to serialize the response.
* There isn't any negotiation taking place. The server is determining what format to return.

If the Accept header contains `*/*`, the Header is ignored unless `RespectBrowserAcceptHeader` is set to true on [MvcOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions).

### Browsers and content negotiation

Unlike typical API clients, web browsers supply `Accept` headers. Web browsers specify many formats, including wildcards. By default, when the framework detects that the request is coming from a browser:

* The `Accept` header is ignored.
* The content is returned using the first registered output formatter that can handle the response type, unless otherwise configured.

This approach provides a more consistent experience across browsers when consuming APIs.

To configure an app to respect browser accept headers, set the [RespectBrowserAcceptHeader](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.respectbrowseracceptheader#microsoft-aspnetcore-mvc-mvcoptions-respectbrowseracceptheader) property to `true`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.RespectBrowserAcceptHeader = true;
});
```

## Configure formatters

Apps that need to support extra formats can add the appropriate NuGet packages and configure support. There are separate formatters for input and output. Input formatters are used by [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0). Output formatters are used to format responses. For information on creating a custom formatter, see [Custom Formatters](custom-formatters?view=aspnetcore-10.0).

### Add XML format support

To configure XML formatters implemented using [XmlSerializer](/en-us/dotnet/api/system.xml.serialization.xmlserializer), call [AddXmlSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvcbuilderextensions.addxmlserializerformatters):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddXmlSerializerFormatters();
```

When using the preceding code, controller methods return the appropriate format based on the request's `Accept` header.

### Configure `System.Text.Json`-based formatters

To configure features for the `System.Text.Json`-based formatters, use [Microsoft.AspNetCore.Mvc.JsonOptions.JsonSerializerOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions.jsonserializeroptions#microsoft-aspnetcore-mvc-jsonoptions-jsonserializeroptions). The following highlighted code configures PascalCase formatting instead of the default camelCase formatting:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.PropertyNamingPolicy = null;
    });
```

To configure output serialization options for specific actions, use [JsonResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonresult). For example:

```
[HttpGet]
public IActionResult Get() 
    => new JsonResult(
        _todoItemStore.GetList(),
        new JsonSerializerOptions { PropertyNamingPolicy = null });
```

### Add `Newtonsoft.Json`-based JSON format support

The default JSON formatters use `System.Text.Json`. To use the `Newtonsoft.Json`-based formatters, install the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson/) NuGet package and configure it in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddNewtonsoftJson();
```

In the preceding code, the call to `AddNewtonsoftJson` configures the following Web API, MVC, and Razor Pages features to use `Newtonsoft.Json`:

* Input and output formatters that read and write JSON
* [JsonResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonresult)
* [JSON Patch](../jsonpatch?view=aspnetcore-10.0)
* [IJsonHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ijsonhelper)
* [TempData](../../fundamentals/app-state?view=aspnetcore-10.0#tempdata)

Some features may not work well with `System.Text.Json`-based formatters and require a reference to the `Newtonsoft.Json`-based formatters. Continue using the `Newtonsoft.Json`-based formatters when the app:

* Uses `Newtonsoft.Json` attributes. For example, `[JsonProperty]` or `[JsonIgnore]`.
* Customizes the serialization settings.
* Relies on features that `Newtonsoft.Json` provides.

To configure features for the `Newtonsoft.Json`-based formatters, use [SerializerSettings](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcnewtonsoftjsonoptions.serializersettings):

```
builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ContractResolver = new DefaultContractResolver();
    });
```

To configure output serialization options for specific actions, use `JsonResult`. For example:

```
[HttpGet]
public IActionResult GetNewtonsoftJson()
    => new JsonResult(
        _todoItemStore.GetList(),
        new JsonSerializerSettings { ContractResolver = new DefaultContractResolver() });
```

### Format `ProblemDetails` and `ValidationProblemDetails` responses

The following action method calls [ControllerBase.Problem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.problem) to create a [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails) response:

```
[HttpGet("Error")]
public IActionResult GetError()
    => Problem("Something went wrong.");
```

A `ProblemDetails` response is always camelCase, even when the app sets the format to PascalCase. `ProblemDetails` follows [RFC 7807](https://tools.ietf.org/html/rfc7807#appendix-A), which specifies lowercase.

When the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute is applied to a controller class, the controller creates a [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails) response when [Model Validation](../../mvc/models/validation?view=aspnetcore-10.0) fails. This response includes a dictionary that uses the model's property names as error keys, unchanged. For example, the following model includes a single property that requires validation:

```
public class SampleModel
{
    [Range(1, 10)]
    public int Value { get; set; }
}
```

By default, the `ValidationProblemDetails` response returned when the `Value` property is invalid uses an error key of `Value`, as shown in the following example:

```
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-00000000000000000000000000000000-000000000000000-00",
  "errors": {
    "Value": [
      "The field Value must be between 1 and 10."
    ]
  }
}
```

To format the property names used as error keys, add an implementation of [IMetadataDetailsProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.imetadatadetailsprovider) to the [MvcOptions.ModelMetadataDetailsProviders](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.modelmetadatadetailsproviders) collection. The following example adds a `System.Text.Json`-based implementation, `SystemTextJsonValidationMetadataProvider`, which formats property names as camelCase by default:

```
builder.Services.AddControllers();

builder.Services.Configure<MvcOptions>(options =>
{
    options.ModelMetadataDetailsProviders.Add(
        new SystemTextJsonValidationMetadataProvider());
});
```

`SystemTextJsonValidationMetadataProvider` also accepts an implementation of [JsonNamingPolicy](/en-us/dotnet/api/system.text.json.jsonnamingpolicy) in its constructor, which specifies a custom naming policy for formatting property names.

To set a custom name for a property within a model, use the [\[JsonPropertyName\]](/en-us/dotnet/api/system.text.json.serialization.jsonpropertynameattribute) attribute on the property:

```
public class SampleModel
{
    [Range(1, 10)]
    [JsonPropertyName("sampleValue")]
    public int Value { get; set; }
}
```

The `ValidationProblemDetails` response returned for the preceding model when the `Value` property is invalid uses an error key of `sampleValue`, as shown in the following example:

```
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-00000000000000000000000000000000-000000000000000-00",
  "errors": {
    "sampleValue": [
      "The field Value must be between 1 and 10."
    ]
  }
}
```

To format the `ValidationProblemDetails` response using `Newtonsoft.Json`, use `NewtonsoftJsonValidationMetadataProvider`:

```
builder.Services.AddControllers()
    .AddNewtonsoftJson();

builder.Services.Configure<MvcOptions>(options =>
{
    options.ModelMetadataDetailsProviders.Add(
        new NewtonsoftJsonValidationMetadataProvider());
});
```

By default, `NewtonsoftJsonValidationMetadataProvider` formats property names as camelCase. `NewtonsoftJsonValidationMetadataProvider` also accepts an implementation of `NamingPolicy` in its constructor, which specifies a custom naming policy for formatting property names. To set a custom name for a property within a model, use the `[JsonProperty]` attribute.

## Specify a format

To restrict the response formats, apply the [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) filter. Like most [Filters](../../mvc/controllers/filters?view=aspnetcore-10.0), `[Produces]` can be applied at the action, controller, or global scope:

```
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class TodoItemsController : ControllerBase
```

The preceding [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) filter:

* Forces all actions within the controller to return JSON-formatted responses for POCOs (Plain Old CLR Objects) or [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult) and its derived types.
* Return JSON-formatted responses even if other formatters are configured and the client specifies a different format.

For more information, see [Filters](../../mvc/controllers/filters?view=aspnetcore-10.0).

## Special case formatters

Some special cases are implemented using built-in formatters. By default, `string` return types are formatted as *text/plain* (*text/html* if requested via the `Accept` header). This behavior can be deleted by removing the [StringOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.stringoutputformatter). Formatters are removed in `Program.cs`. Actions that have a model object return type return `204 No Content` when returning `null`. This behavior can be deleted by removing the [HttpNoContentOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.httpnocontentoutputformatter). The following code removes the `StringOutputFormatter` and `HttpNoContentOutputFormatter`.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    // using Microsoft.AspNetCore.Mvc.Formatters;
    options.OutputFormatters.RemoveType<StringOutputFormatter>();
    options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
});
```

Without the `StringOutputFormatter`, the built-in JSON formatter formats `string` return types. If the built-in JSON formatter is removed and an XML formatter is available, the XML formatter formats `string` return types. Otherwise, `string` return types return `406 Not Acceptable`.

Without the `HttpNoContentOutputFormatter`, null objects are formatted using the configured formatter. For example:

* The JSON formatter returns a response with a body of `null`.
* The XML formatter returns an empty XML element with the attribute `xsi:nil="true"` set.

## Response format URL mappings

Clients can request a particular format as part of the URL, for example:

* In the query string or part of the path.
* By using a format-specific file extension such as .xml or .json.

The mapping from request path should be specified in the route the API is using. For example:

```
[ApiController]
[Route("api/[controller]")]
[FormatFilter]
public class TodoItemsController : ControllerBase
{
    private readonly TodoItemStore _todoItemStore;

    public TodoItemsController(TodoItemStore todoItemStore)
        => _todoItemStore = todoItemStore;

    [HttpGet("{id:long}.{format?}")]
    public TodoItem? GetById(long id)
        => _todoItemStore.GetById(id);
```

The preceding route allows the requested format to be specified using an optional file extension. The [`[FormatFilter]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatfilterattribute) attribute checks for the existence of the format value in the `RouteData` and maps the response format to the appropriate formatter when the response is created.

| Route                   | Formatter                          |
| ----------------------- | ---------------------------------- |
| `/api/todoitems/5`      | The default output formatter       |
| `/api/todoitems/5.json` | The JSON formatter (if configured) |
| `/api/todoitems/5.xml`  | The XML formatter (if configured)  |

## Polymorphic deserialization

Built-in features provide a limited range of polymorphic serialization but no support for deserialization at all. Deserialization requires a custom converter. See [Polymorphic deserialization](/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#support-polymorphic-deserialization) for a complete sample of polymorphic deserialization.

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/formatting/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/cors
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/cors.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Enable Cross-Origin Requests (CORS) in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Kirk Larkin](https://twitter.com/serpent5)

This article shows how **C**ross-**O**rigin **R**esource **S**haring ([CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS)) is enabled in an ASP.NET Core app.

Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction is called the *same-origin policy*. The same-origin policy prevents a malicious site from reading sensitive data from another site. Sometimes, you might want to allow other sites to make cross-origin requests to your app. For more information, see the [Mozilla CORS article](https://developer.mozilla.org/docs/Web/HTTP/CORS).

[Cross Origin Resource Sharing](https://www.w3.org/TR/cors/) (CORS):

* Is a W3C standard that allows a server to relax the same-origin policy.
* Is **not** a security feature, CORS relaxes security. An API is not safer by allowing CORS. For more information, see [How CORS works](#how-cors).
* Allows a server to explicitly allow some cross-origin requests while rejecting others.
* Is safer and more flexible than earlier techniques, such as [JSONP](/en-us/dotnet/framework/wcf/samples/jsonp).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/8.0/Cors/Web2API) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Same origin

Two URLs have the same origin if they have identical schemes, hosts, and ports ([RFC 6454](https://tools.ietf.org/html/rfc6454)).

These two URLs have the same origin:

* `https://example.com/foo.html`
* `https://example.com/bar.html`

These URLs have different origins than the previous two URLs:

* `https://example.net`: Different domain
* `https://contoso.example.com/foo.html`: Different subdomain
* `http://example.com/foo.html`: Different scheme
* `https://example.com:9000/foo.html`: Different port

## Enable CORS

There are three ways to enable CORS:

* In middleware using a [named policy](#np) or [default policy](#dp).
* Using [endpoint routing](#ecors6).
* With the [\[EnableCors\]](#attr) attribute.

Using the [\[EnableCors\]](#attr) attribute with a named policy provides the finest control in limiting endpoints that support CORS.

Warning

[UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) must be called in the correct order. For more information, see [Middleware order](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order). For example, `UseCors` must be called before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching) when using `UseResponseCaching`.

Each approach is detailed in the following sections.

[]()

## CORS with named policy and middleware

CORS middleware handles cross-origin requests. The following code applies a CORS policy to all the app's endpoints with the specified origins:

```
var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

// services.AddResponseCaching();

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The preceding code:

* Sets the policy name to `_myAllowSpecificOrigins`. The policy name is arbitrary.
* Calls the [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) extension method and specifies the `_myAllowSpecificOrigins` CORS policy. `UseCors` adds the CORS middleware. The call to `UseCors` must be placed after `UseRouting`, but before `UseAuthorization`. For more information, see [Middleware order](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).
* Calls [AddCors](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.corsservicecollectionextensions.addcors) with a [lambda expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). The lambda takes a [CorsPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder) object. [Configuration options](#cors-policy-options), such as `WithOrigins`, are described later in this article.
* Enables the `_myAllowSpecificOrigins` CORS policy for all controller endpoints. See [endpoint routing](#ecors6) to apply a CORS policy to specific endpoints.
* When using [response caching middleware](../performance/caching/middleware?view=aspnetcore-10.0), call [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching).

With endpoint routing, the CORS middleware **must** be configured to execute between the calls to `UseRouting` and `UseEndpoints`.

The [AddCors](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccorsmvccorebuilderextensions.addcors) method call adds CORS services to the app's service container:

```
var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

// services.AddResponseCaching();

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();
```

For more information, see [CORS policy options](#cpo6) in this document.

The [CorsPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder) methods can be chained, as shown in the following code:

```
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(MyAllowSpecificOrigins,
                          policy =>
                          {
                              policy.WithOrigins("http://example.com",
                                                  "http://www.contoso.com")
                                                  .AllowAnyHeader()
                                                  .AllowAnyMethod();
                          });
});

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();
```

Note: The specified URL must **not** contain a trailing slash (`/`). If the URL terminates with `/`, the comparison returns `false` and no header is returned.

[]()

## UseCors and UseStaticFiles order

Typically, `UseStaticFiles` is called before `UseCors`. Apps that use JavaScript to retrieve static files cross site must call `UseCors` before `UseStaticFiles`.

[]()

### CORS with default policy and middleware

The following highlighted code enables the default CORS policy:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com");
        });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The preceding code applies the default CORS policy to all controller endpoints.

[]()

## Enable Cors with endpoint routing

With endpoint routing, CORS can be enabled on a per-endpoint basis using the [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) set of extension methods:

```
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/echo",
        context => context.Response.WriteAsync("echo"))
        .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapControllers()
             .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapGet("/echo2",
        context => context.Response.WriteAsync("echo2"));

    endpoints.MapRazorPages();
});

app.Run();
```

In the preceding code:

* `app.UseCors` enables the CORS middleware. Because a default policy hasn't been configured, `app.UseCors()` alone doesn't enable CORS.
* The `/echo` and controller endpoints allow cross-origin requests using the specified policy.
* The `/echo2` and Razor Pages endpoints do **not** allow cross-origin requests because no default policy was specified.

The [\[DisableCors\]](#dc6) attribute does **not** disable CORS that has been enabled by endpoint routing with `RequireCors`.

See [Test CORS with \[EnableCors\] attribute and RequireCors method](#tcer) for instructions on testing code similar to the preceding.

[]()

## Enable CORS with attributes

Enabling CORS with the [\[EnableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute and applying a named policy to only those endpoints that require CORS provides the finest control.

The [\[EnableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute provides an alternative to applying CORS globally. The `[EnableCors]` attribute enables CORS for selected endpoints, rather than all endpoints:

* `[EnableCors]` specifies the default policy.
* `[EnableCors("{Policy String}")]` specifies a named policy.

The `[EnableCors]` attribute can be applied to:

* Razor Page `PageModel`
* Controller
* Controller action method

Different policies can be applied to controllers, page models, or action methods with the `[EnableCors]` attribute. When the `[EnableCors]` attribute is applied to a controller, page model, or action method, and CORS is enabled in middleware, **both** policies are applied. **We recommend against combining policies. Use the** `[EnableCors]` **attribute or middleware, not both in the same app.**

The following code applies a different policy to each method:

```
[Route("api/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
    // GET api/values
    [EnableCors("AnotherPolicy")]
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { "green widget", "red widget" };
    }

    // GET api/values/5
    [EnableCors("Policy1")]
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return id switch
        {
            1 => "green widget",
            2 => "red widget",
            _ => NotFound(),
        };
    }
}
```

The following code creates two CORS policies:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("Policy1",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com");
        });

    options.AddPolicy("AnotherPolicy",
        policy =>
        {
            policy.WithOrigins("http://www.contoso.com")
                                .AllowAnyHeader()
                                .AllowAnyMethod();
        });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

For the finest control of limiting CORS requests:

* Use `[EnableCors("MyPolicy")]` with a named policy.
* Don't define a default policy.
* Don't use [endpoint routing](#ecors6).

The code in the next section meets the preceding list.

[]()

### Disable CORS

The [\[DisableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.disablecorsattribute) attribute does **not** disable CORS that has been enabled by [endpoint routing](#ecors6).

The following code defines the CORS policy `"MyPolicy"`:

```
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com")
                    .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints => {
    endpoints.MapControllers();
    endpoints.MapRazorPages();
});

app.Run();
```

The following code disables CORS for the `GetValues2` action:

```
[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}
```

The preceding code:

* Doesn't enable CORS with [endpoint routing](#ecors6).
* Doesn't define a [default CORS policy](#dp).
* Uses [\[EnableCors("MyPolicy")\]](#attr) to enable the `"MyPolicy"` CORS policy for the controller.
* Disables CORS for the `GetValues2` method.

See [Test CORS](#testc6) for instructions on testing the preceding code.

[]()

## CORS policy options

This section describes the various options that can be set in a CORS policy:

* [Set the allowed origins](#set-the-allowed-origins)
* [Set the allowed HTTP methods](#set-the-allowed-http-methods)
* [Set the allowed request headers](#set-the-allowed-request-headers)
* [Set the exposed response headers](#set-the-exposed-response-headers)
* [Credentials in cross-origin requests](#credentials-in-cross-origin-requests)
* [Set the preflight expiration time](#set-the-preflight-expiration-time)

[AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corsoptions.addpolicy) is called in `Program.cs`. For some options, it may be helpful to read the [How CORS works](#how-cors) section first.

## Set the allowed origins

[AllowAnyOrigin](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyorigin): Allows CORS requests from all origins with any scheme (`http` or `https`). `AllowAnyOrigin` is insecure because *any website* can make cross-origin requests to the app.

Warning

Specifying `AllowAnyOrigin` and `AllowCredentials` is an insecure configuration and can result in cross-site request forgery. The CORS service returns an invalid CORS response when an app is configured with both methods. Bypassing the built-in checks by using `SetIsOriginAllowed(_ => true)` together with `AllowCredentials` is also an insecure configuration.

`AllowAnyOrigin` affects preflight requests and the `Access-Control-Allow-Origin` header. For more information, see the [Preflight requests](#preflight-requests) section.

[SetIsOriginAllowedToAllowWildcardSubdomains](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.setisoriginallowedtoallowwildcardsubdomains): Sets the [IsOriginAllowed](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicy.isoriginallowed) property of the policy to be a function that allows origins to match a configured wildcard domain when evaluating if the origin is allowed.

```
var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                .SetIsOriginAllowedToAllowWildcardSubdomains();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

In the preceding code, `SetIsOriginAllowedToAllowWildcardSubdomains` is called with the wildcard origin `"https://*.example.com"`. This configuration allows CORS requests from any subdomain of `example.com`, such as `https://subdomain.example.com` or `https://api.example.com`. The `*` wildcard character must be included in the origin to enable wildcard subdomain matching.

### Set the allowed HTTP methods

[AllowAnyMethod](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanymethod):

* Allows any HTTP method:
* Affects preflight requests and the `Access-Control-Allow-Methods` header. For more information, see the [Preflight requests](#preflight-requests) section.

### Set the allowed request headers

To allow specific headers to be sent in a CORS request, called *author request headers*, call [WithHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withheaders) and specify the allowed headers:

```
using Microsoft.Net.Http.Headers;

var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
       policy =>
       {
           policy.WithOrigins("http://example.com")
                  .WithHeaders(HeaderNames.ContentType, "x-custom-header");
       });
});

builder.Services.AddControllers();

var app = builder.Build();
```

To allow all author request headers, call [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyheader):

```
var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .AllowAnyHeader();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

`AllowAnyHeader` affects preflight requests and the [Access-Control-Request-Headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Method) header. For more information, see the [Preflight requests](#preflight-requests) section.

A CORS middleware policy match to specific headers specified by `WithHeaders` is only possible when the headers sent in `Access-Control-Request-Headers` exactly match the headers stated in `WithHeaders`.

For instance, consider an app configured as follows:

```
app.UseCors(policy => policy.WithHeaders(HeaderNames.CacheControl));
```

CORS middleware declines a preflight request with the following request header because `Content-Language` ([HeaderNames.ContentLanguage](/en-us/dotnet/api/microsoft.net.http.headers.headernames.contentlanguage#microsoft-net-http-headers-headernames-contentlanguage)) isn't listed in `WithHeaders`:

```
Access-Control-Request-Headers: Cache-Control, Content-Language
```

The app returns a `204 No Content` response but doesn't send the CORS headers back. Therefore, the browser doesn't attempt the cross-origin request.

### Set the exposed response headers

By default, the browser doesn't expose all of the response headers to the app. For more information, see [W3C Cross-Origin Resource Sharing (Terminology): Simple Response Header](https://www.w3.org/TR/cors/#simple-response-header).

The response headers that are available by default are:

* `Cache-Control`
* `Content-Language`
* `Content-Type`
* `Expires`
* `Last-Modified`
* `Pragma`

The CORS specification calls these headers *simple response headers*. To make other headers available to the app, call [WithExposedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withexposedheaders):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyExposeResponseHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .WithExposedHeaders("x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

### Credentials in cross-origin requests

Credentials require special handling in a CORS request. By default, the browser doesn't send credentials with a cross-origin request. Credentials include cookies and HTTP authentication schemes. To send credentials with a cross-origin request, the client must set `XMLHttpRequest.withCredentials` to `true`.

Using `XMLHttpRequest` directly:

```
var xhr = new XMLHttpRequest();
xhr.open('get', 'https://www.example.com/api/test');
xhr.withCredentials = true;
```

Using jQuery:

```
$.ajax({
  type: 'get',
  url: 'https://www.example.com/api/test',
  xhrFields: {
    withCredentials: true
  }
});
```

Using the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API):

```
fetch('https://www.example.com/api/test', {
    credentials: 'include'
});
```

The server must allow the credentials. To allow cross-origin credentials, call [AllowCredentials](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowcredentials):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyMyAllowCredentialsPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com")
                   .AllowCredentials();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

The HTTP response includes an `Access-Control-Allow-Credentials` header, which tells the browser that the server allows credentials for a cross-origin request.

If the browser sends credentials but the response doesn't include a valid `Access-Control-Allow-Credentials` header, the browser doesn't expose the response to the app, and the cross-origin request fails.

Allowing cross-origin credentials is a security risk. A website at another domain can send a signed-in user's credentials to the app on the user's behalf without the user's knowledge.

The CORS specification also states that setting origins to `"*"` (all origins) is invalid if the `Access-Control-Allow-Credentials` header is present.

[]()

## Preflight requests

For some CORS requests, the browser sends an additional [OPTIONS](https://developer.mozilla.org/docs/Web/HTTP/Methods/OPTIONS) request before making the actual request. This request is called a [preflight request](https://developer.mozilla.org/docs/Glossary/Preflight_request). The browser can skip the preflight request if **all** the following conditions are true:

* The request method is GET, HEAD, or POST.

* The app doesn't set request headers other than `Accept`, `Accept-Language`, `Content-Language`, `Content-Type`, or `Last-Event-ID`.

* The `Content-Type` header, if set, has one of the following values:

  * `application/x-www-form-urlencoded`
  * `multipart/form-data`
  * `text/plain`

The rule on request headers set for the client request applies to headers that the app sets by calling `setRequestHeader` on the `XMLHttpRequest` object. The CORS specification calls these headers *author request headers*. The rule doesn't apply to headers the browser can set, such as `User-Agent`, `Host`, or `Content-Length`.

Note

This article contains URLs created by deploying the [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/cors/samples/8.0/Cors) to two Azure web sites, `https://cors3.azurewebsites.net` and `https://cors.azurewebsites.net`.

The following is an example response similar to the preflight request made from the **\[Put test]** button in the [Test CORS](#testc6) section of this document.

```
General:
Request URL: https://cors3.azurewebsites.net/api/values/5
Request Method: OPTIONS
Status Code: 204 No Content

Response Headers:
Access-Control-Allow-Methods: PUT,DELETE,GET
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f8...8;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Vary: Origin

Request Headers:
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Method: PUT
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0
```

The preflight request uses the [HTTP OPTIONS](https://developer.mozilla.org/docs/Web/HTTP/Methods/OPTIONS) method. It may include the following headers:

* [Access-Control-Request-Method](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Method): The HTTP method that will be used for the actual request.
* [Access-Control-Request-Headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Headers): A list of request headers that the app sets on the actual request. As stated earlier, this doesn't include headers that the browser sets, such as `User-Agent`.

If the preflight request is denied, the app returns a `204 No Content` response but doesn't set the CORS headers. Therefore, the browser doesn't attempt the cross-origin request. For an example of a denied preflight request, see the [Test CORS](#testc6) section of this document.

Using the F12 tools, the console app shows an error similar to one of the following, depending on the browser:

* Firefox: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at `https://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5`. (Reason: CORS request did not succeed). [Learn More](https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/CORSDidNotSucceed)
* Chromium based: Access to fetch at 'https\://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5' from origin 'https\://cors3.azurewebsites.net' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

To allow specific headers, call [WithHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withheaders):

```
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAllowHeadersPolicy",
        policy =>
        {
        policy.WithOrigins("http://example.com")
                   .WithHeaders(HeaderNames.ContentType, "x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

To allow all author request headers, call [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyheader):

```
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAllowAllHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .AllowAnyHeader();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

Browsers aren't consistent in how they set `Access-Control-Request-Headers`. If either:

* Headers are set to anything other than `"*"`
* [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicy.allowanyheader) is called: Include at least `Accept`, `Content-Type`, and `Origin`, plus any custom headers that you want to support.

[]()

### Automatic preflight request code

When the CORS policy is applied either:

* Globally by calling `app.UseCors` in `Program.cs`.
* Using the `[EnableCors]` attribute.

ASP.NET Core responds to the preflight OPTIONS request.

The [Test CORS](#testc6) section of this document demonstrates this behavior.

[]()

### \[HttpOptions] attribute for preflight requests

When CORS is enabled with the appropriate policy, ASP.NET Core generally responds to CORS preflight requests automatically.

The following code uses the [\[HttpOptions\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpoptionsattribute) attribute to create endpoints for OPTIONS requests:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }
```

See [Test CORS with \[EnableCors\] attribute and RequireCors method](#tcer) for instructions on testing the preceding code.

### Set the preflight expiration time

The `Access-Control-Max-Age` header specifies how long the response to the preflight request can be cached. To set this header, call [SetPreflightMaxAge](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.setpreflightmaxage):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MySetPreflightExpirationPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com")
                   .SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

## Enable CORS on an endpoint

[]()

## How CORS works

This section describes what happens in a [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) request at the level of the HTTP messages.

* CORS is **not** a security feature. CORS is a W3C standard that allows a server to relax the same-origin policy.
  * For example, a malicious actor could use [Cross-Site Scripting (XSS)](cross-site-scripting?view=aspnetcore-10.0) against your site and execute a cross-site request to their CORS enabled site to steal information.

* An API isn't safer by allowing CORS.

  * It's up to the client (browser) to enforce CORS. The server executes the request and returns the response, it's the client that returns an error and blocks the response. For example, any of the following tools will display the server response:

    * [Fiddler](https://www.telerik.com/fiddler)
    * [.NET HttpClient](/en-us/dotnet/csharp/tutorials/console-webapiclient)
    * A web browser by entering the URL in the address bar.

* It's a way for a server to allow browsers to execute a cross-origin [XHR](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest) or [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) request that otherwise would be forbidden.
  * Browsers without CORS can't do cross-origin requests. Before CORS, [JSONP](https://www.w3schools.com/js/js_json_jsonp.asp) was used to circumvent this restriction. JSONP doesn't use XHR, it uses the `<script>` tag to receive the response. Scripts are allowed to be loaded cross-origin.

The [CORS specification](https://www.w3.org/TR/cors/) introduced several new HTTP headers that enable cross-origin requests. If a browser supports CORS, it sets these headers automatically for cross-origin requests. Custom JavaScript code isn't required to enable CORS.

The following is an example of a cross-origin request from the **Values** test button to `https://cors1.azurewebsites.net/api/values`. The `Origin` header:

* Provides the domain of the site that's making the request.
* Is required and must be different from the host.

**General headers**

```
Request URL: https://cors1.azurewebsites.net/api/values
Request Method: GET
Status Code: 200 OK
```

**Response headers**

```
Content-Encoding: gzip
Content-Type: text/plain; charset=utf-8
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Powered-By: ASP.NET
```

**Request headers**

```
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: cors1.azurewebsites.net
Origin: https://cors3.azurewebsites.net
Referer: https://cors3.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 ...
```

In `OPTIONS` requests, the server sets the **Response headers** `Access-Control-Allow-Origin: {allowed origin}` header in the response. For example, in the [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/8.0/Cors/Web2API), the `Delete [EnableCors]` button `OPTIONS` request contains the following headers:

**General headers**

```
Request URL: https://cors3.azurewebsites.net/api/TodoItems2/MyDelete2/5
Request Method: OPTIONS
Status Code: 204 No Content
```

**Response headers**

```
Access-Control-Allow-Headers: Content-Type,x-custom-header
Access-Control-Allow-Methods: PUT,DELETE,GET,OPTIONS
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors3.azurewebsites.net
Vary: Origin
X-Powered-By: ASP.NET
```

**Request headers**

```
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: DELETE
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/test?number=2
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0
```

In the preceding **Response headers**, the server sets the [Access-Control-Allow-Origin](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header in the response. The `https://cors1.azurewebsites.net` value of this header matches the `Origin` header from the request.

If [AllowAnyOrigin](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyorigin) is called, the `Access-Control-Allow-Origin: *`, the wildcard value, is returned. `AllowAnyOrigin` allows any origin.

If the response doesn't include the `Access-Control-Allow-Origin` header, the cross-origin request fails. Specifically, the browser disallows the request. Even if the server returns a successful response, the browser doesn't make the response available to the client app.

[]()

### HTTP redirection to HTTPS causes ERR\_INVALID\_REDIRECT on the CORS preflight request

Requests to an endpoint using HTTP that are redirected to HTTPS by [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) fail with `ERR_INVALID_REDIRECT on the CORS preflight request`.

API projects can reject HTTP requests rather than use `UseHttpsRedirection` to redirect requests to HTTPS.

[]()

## CORS in IIS

When deploying to IIS, CORS has to run before Windows Authentication if the server isn't configured to allow anonymous access. To support this scenario, the [IIS CORS module](https://www.iis.net/downloads/microsoft/iis-cors-module) needs to be installed and configured for the app.

[]()

## Test CORS

The [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/8.0/Cors/Web2API) has code to test CORS. See [how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample). The sample is an API project with Razor Pages added:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                    "http://www.contoso.com",
                    "https://cors1.azurewebsites.net",
                    "https://cors3.azurewebsites.net",
                    "https://localhost:44398",
                    "https://localhost:5001")
                .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();
app.MapRazorPages();

app.Run();
```

Warning

`WithOrigins("https://localhost:<port>");` should only be used for testing a sample app similar to the [download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/cors/samples/8.0/Cors).

Note

If you're using `launchSettings.json` in Visual Studio or [configuring C# debug settings in VS Code](https://code.visualstudio.com/docs/csharp/debugger-settings), and using IIS Express to debug locally, ensure that you've configured IIS Express for `"anonymousAuthentication": true`. When `"anonymousAuthentication"` is `false`, the ASP.NET Core web environment host will not see any preflight requests. In particular, if you're using NTLM authentication (`"windowsAuthentication": true`), the first step of the NTLM challenge-response is to send the web browser a 401 challenge, which can make it challenging to verify your preflight route is configured correctly.

The following `ValuesController` provides the endpoints for testing:

```
[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}
```

[MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.

Test the preceding sample code by using one of the following approaches:

* Run the sample with `dotnet run` using the default URL of `https://localhost:5001`.
* Run the sample from Visual Studio with the port set to 44398 for a URL of `https://localhost:44398`.

Using a browser with the F12 tools:

* Select the **Values** button and review the headers in the **Network** tab.

* Select the **PUT test** button. See [Display OPTIONS requests](#options) for instructions on displaying the OPTIONS request. The **PUT test** creates two requests, an OPTIONS preflight request and the PUT request.

* Select the **`GetValues2 [DisableCors]`** button to trigger a failed CORS request. As mentioned in the document, the response returns 200 success, but the CORS request is not made. Select the **Console** tab to see the CORS error. Depending on the browser, an error similar to the following is displayed:

  Access to fetch at `'https://cors1.azurewebsites.net/api/values/GetValues2'` from origin `'https://cors3.azurewebsites.net'` has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

CORS-enabled endpoints can be tested with a tool, such as [curl](https://curl.haxx.se/) or [Fiddler](https://www.telerik.com/fiddler). When using a tool, the origin of the request specified by the `Origin` header must differ from the host receiving the request. If the request isn't *cross-origin* based on the value of the `Origin` header:

* There's no need for CORS middleware to process the request.
* CORS headers aren't returned in the response.

The following command uses `curl` to issue an OPTIONS request with information:

```
curl -X OPTIONS https://cors3.azurewebsites.net/api/TodoItems2/5 -i
```

[]()

### Test CORS with \[EnableCors] attribute and RequireCors method

Consider the following code which uses [endpoint routing](#ecors6) to enable CORS on a per-endpoint basis using `RequireCors`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                    "http://www.contoso.com",
                    "https://cors1.azurewebsites.net",
                    "https://cors3.azurewebsites.net",
                    "https://localhost:44398",
                    "https://localhost:5001")
                .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/echo",
        context => context.Response.WriteAsync("echo"))
        .RequireCors("MyPolicy");

    endpoints.MapControllers();
    endpoints.MapRazorPages();
});

app.Run();
```

Notice that only the `/echo` endpoint is using the `RequireCors` to allow cross-origin requests using the specified policy. The controllers below enable CORS using \[EnableCors] attribute.

The following `TodoItems1Controller` provides endpoints for testing:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems1Controller : ControllerBase 
{
    // PUT: api/TodoItems1/5
    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id) {
        if (id < 1) {
            return Content($"ID = {id}");
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // Delete: api/TodoItems1/5
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // GET: api/TodoItems1
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors("MyPolicy")]
    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    // Delete: api/TodoItems1/MyDelete2/5
    [EnableCors("MyPolicy")]
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}
```

The **Delete \[EnableCors]** and **GET \[EnableCors]** buttons succeed, because the endpoints have `[EnableCors]` and respond to preflight requests. The other endpoints fails. The **GET** button fails, because the [JavaScript](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/8.0/Cors/Web2API/wwwroot/js/MyJS.js) sends:

```
 headers: {
      "Content-Type": "x-custom-header"
 },
```

The following `TodoItems2Controller` provides similar endpoints, but includes explicit code to respond to OPTIONS requests:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // [EnableCors] // Not needed as OPTIONS path provided.
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // [EnableCors] //  Warning ASP0023 Route '{id}' conflicts with another action route.
    //                  An HTTP request that matches multiple routes results in an ambiguous
    //                  match error.
    [EnableCors("MyPolicy")] // Required for this path.
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors("MyPolicy")]  // Required for this path.
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}
```

The preceding code can be tested by deploying the sample to Azure. In the **Controller** drop down list, select **Preflight** and then **Set Controller**. All the CORS calls to the `TodoItems2Controller` endpoints succeed.

## Related content

* [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/docs/Web/HTTP/CORS)
* [IIS CORS module Configuration Reference](/en-us/iis/extensions/cors-module/cors-module-configuration-reference)

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Kirk Larkin](https://twitter.com/serpent5)

This article shows how to enable CORS in an ASP.NET Core app.

Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction is called the *same-origin policy*. The same-origin policy prevents a malicious site from reading sensitive data from another site. Sometimes, you might want to allow other sites to make cross-origin requests to your app. For more information, see the [Mozilla CORS article](https://developer.mozilla.org/docs/Web/HTTP/CORS).

[Cross Origin Resource Sharing](https://www.w3.org/TR/cors/) (CORS):

* Is a W3C standard that allows a server to relax the same-origin policy.
* Is **not** a security feature, CORS relaxes security. An API is not safer by allowing CORS. For more information, see [How CORS works](#how-cors).
* Allows a server to explicitly allow some cross-origin requests while rejecting others.
* Is safer and more flexible than earlier techniques, such as [JSONP](/en-us/dotnet/framework/wcf/samples/jsonp).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/8.0/Cors/Web2API) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Same origin

Two URLs have the same origin if they have identical schemes, hosts, and ports ([RFC 6454](https://tools.ietf.org/html/rfc6454)).

These two URLs have the same origin:

* `https://example.com/foo.html`
* `https://example.com/bar.html`

These URLs have different origins than the previous two URLs:

* `https://example.net`: Different domain
* `https://www.example.com/foo.html`: Different subdomain
* `http://example.com/foo.html`: Different scheme
* `https://example.com:9000/foo.html`: Different port

## Enable CORS

There are three ways to enable CORS:

* In middleware using a [named policy](#np) or [default policy](#dp).
* Using [endpoint routing](#ecors6).
* With the [\[EnableCors\]](#attr) attribute.

Using the [\[EnableCors\]](#attr) attribute with a named policy provides the finest control in limiting endpoints that support CORS.

Warning

[UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) must be called in the correct order. For more information, see [Middleware order](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order). For example, `UseCors` must be called before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching) when using `UseResponseCaching`.

Each approach is detailed in the following sections.

[]()

## CORS with named policy and middleware

CORS middleware handles cross-origin requests. The following code applies a CORS policy to all the app's endpoints with the specified origins:

```
var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

// services.AddResponseCaching();

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The preceding code:

* Sets the policy name to `_myAllowSpecificOrigins`. The policy name is arbitrary.
* Calls the [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) extension method and specifies the `_myAllowSpecificOrigins` CORS policy. `UseCors` adds the CORS middleware. The call to `UseCors` must be placed after `UseRouting`, but before `UseAuthorization`. For more information, see [Middleware order](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).
* Calls [AddCors](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.corsservicecollectionextensions.addcors) with a [lambda expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). The lambda takes a [CorsPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder) object. [Configuration options](#cors-policy-options), such as `WithOrigins`, are described later in this article.
* Enables the `_myAllowSpecificOrigins` CORS policy for all controller endpoints. See [endpoint routing](#ecors6) to apply a CORS policy to specific endpoints.
* When using [response caching middleware](../performance/caching/middleware?view=aspnetcore-10.0), call [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching).

With endpoint routing, the CORS middleware **must** be configured to execute between the calls to `UseRouting` and `UseEndpoints`.

The [AddCors](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccorsmvccorebuilderextensions.addcors) method call adds CORS services to the app's service container:

```
var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

// services.AddResponseCaching();

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();
```

For more information, see [CORS policy options](#cpo6) in this document.

The [CorsPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder) methods can be chained, as shown in the following code:

```
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(MyAllowSpecificOrigins,
                          policy =>
                          {
                              policy.WithOrigins("http://example.com",
                                                  "http://www.contoso.com")
                                                  .AllowAnyHeader()
                                                  .AllowAnyMethod();
                          });
});

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();
```

Note: The specified URL must **not** contain a trailing slash (`/`). If the URL terminates with `/`, the comparison returns `false` and no header is returned.

[]()

Warning

`UseCors` must be placed after `UseRouting` and before `UseAuthorization`. This is to ensure that CORS headers are included in the response for both authorized and unauthorized calls.

## UseCors and UseStaticFiles order

Typically, `UseStaticFiles` is called before `UseCors`. Apps that use JavaScript to retrieve static files cross site must call `UseCors` before `UseStaticFiles`.

[]()

### CORS with default policy and middleware

The following highlighted code enables the default CORS policy:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com");
        });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The preceding code applies the default CORS policy to all controller endpoints.

[]()

## Enable Cors with endpoint routing

With endpoint routing, CORS can be enabled on a per-endpoint basis using the [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) set of extension methods:

```
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/echo",
        context => context.Response.WriteAsync("echo"))
        .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapControllers()
             .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapGet("/echo2",
        context => context.Response.WriteAsync("echo2"));

    endpoints.MapRazorPages();
});

app.Run();
```

In the preceding code:

* `app.UseCors` enables the CORS middleware. Because a default policy hasn't been configured, `app.UseCors()` alone doesn't enable CORS.
* The `/echo` and controller endpoints allow cross-origin requests using the specified policy.
* The `/echo2` and Razor Pages endpoints do **not** allow cross-origin requests because no default policy was specified.

The [\[DisableCors\]](#dc6) attribute does **not** disable CORS that has been enabled by endpoint routing with `RequireCors`.

In .NET 7, the `[EnableCors]` attribute must pass a parameter or an [ASP0023](../diagnostics/asp0023?view=aspnetcore-10.0) warning is generated from an ambiguous match on the route. .NET 8 or later doesn't generate the `ASP0023` warning.

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // [EnableCors] // Not needed as OPTIONS path provided.
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // [EnableCors] //  Warning ASP0023 Route '{id}' conflicts with another action route.
    //                  An HTTP request that matches multiple routes results in an ambiguous
    //                  match error.
    [EnableCors("MyPolicy")] // Required for this path.
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors("MyPolicy")]  // Required for this path.
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}
```

See [Test CORS with \[EnableCors\] attribute and RequireCors method](#tcer) for instructions on testing code similar to the preceding.

[]()

## Enable CORS with attributes

Enabling CORS with the [\[EnableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute and applying a named policy to only those endpoints that require CORS provides the finest control.

The [\[EnableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute provides an alternative to applying CORS globally. The `[EnableCors]` attribute enables CORS for selected endpoints, rather than all endpoints:

* `[EnableCors]` specifies the default policy.
* `[EnableCors("{Policy String}")]` specifies a named policy.

The `[EnableCors]` attribute can be applied to:

* Razor Page `PageModel`
* Controller
* Controller action method

Different policies can be applied to controllers, page models, or action methods with the `[EnableCors]` attribute. When the `[EnableCors]` attribute is applied to a controller, page model, or action method, and CORS is enabled in middleware, **both** policies are applied. **We recommend against combining policies. Use the** `[EnableCors]` **attribute or middleware, not both in the same app.**

The following code applies a different policy to each method:

```
[Route("api/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
    // GET api/values
    [EnableCors("AnotherPolicy")]
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { "green widget", "red widget" };
    }

    // GET api/values/5
    [EnableCors("Policy1")]
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return id switch
        {
            1 => "green widget",
            2 => "red widget",
            _ => NotFound(),
        };
    }
}
```

The following code creates two CORS policies:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("Policy1",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com");
        });

    options.AddPolicy("AnotherPolicy",
        policy =>
        {
            policy.WithOrigins("http://www.contoso.com")
                                .AllowAnyHeader()
                                .AllowAnyMethod();
        });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

For the finest control of limiting CORS requests:

* Use `[EnableCors("MyPolicy")]` with a named policy.
* Don't define a default policy.
* Don't use [endpoint routing](#ecors6).

The code in the next section meets the preceding list.

[]()

### Disable CORS

The [\[DisableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.disablecorsattribute) attribute does **not** disable CORS that has been enabled by [endpoint routing](#ecors6).

The following code defines the CORS policy `"MyPolicy"`:

```
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com")
                    .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints => {
    endpoints.MapControllers();
    endpoints.MapRazorPages();
});

app.Run();
```

The following code disables CORS for the `GetValues2` action:

```
[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}
```

The preceding code:

* Doesn't enable CORS with [endpoint routing](#ecors6).
* Doesn't define a [default CORS policy](#dp).
* Uses [\[EnableCors("MyPolicy")\]](#attr) to enable the `"MyPolicy"` CORS policy for the controller.
* Disables CORS for the `GetValues2` method.

See [Test CORS](#testc6) for instructions on testing the preceding code.

[]()

## CORS policy options

This section describes the various options that can be set in a CORS policy:

* [Set the allowed origins](#set-the-allowed-origins)
* [Set the allowed HTTP methods](#set-the-allowed-http-methods)
* [Set the allowed request headers](#set-the-allowed-request-headers)
* [Set the exposed response headers](#set-the-exposed-response-headers)
* [Credentials in cross-origin requests](#credentials-in-cross-origin-requests)
* [Set the preflight expiration time](#set-the-preflight-expiration-time)

[AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corsoptions.addpolicy) is called in `Program.cs`. For some options, it may be helpful to read the [How CORS works](#how-cors) section first.

## Set the allowed origins

[AllowAnyOrigin](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyorigin): Allows CORS requests from all origins with any scheme (`http` or `https`). `AllowAnyOrigin` is insecure because *any website* can make cross-origin requests to the app.

Note

Specifying `AllowAnyOrigin` and `AllowCredentials` is an insecure configuration and can result in cross-site request forgery. The CORS service returns an invalid CORS response when an app is configured with both methods.

`AllowAnyOrigin` affects preflight requests and the `Access-Control-Allow-Origin` header. For more information, see the [Preflight requests](#preflight-requests) section.

[SetIsOriginAllowedToAllowWildcardSubdomains](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.setisoriginallowedtoallowwildcardsubdomains): Sets the [IsOriginAllowed](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicy.isoriginallowed) property of the policy to be a function that allows origins to match a configured wildcard domain when evaluating if the origin is allowed.

```
var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                .SetIsOriginAllowedToAllowWildcardSubdomains();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

In the preceding code, `SetIsOriginAllowedToAllowWildcardSubdomains` is called with the wildcard origin `"https://*.example.com"`. This configuration allows CORS requests from any subdomain of `example.com`, such as `https://subdomain.example.com` or `https://api.example.com`. The `*` wildcard character must be included in the origin to enable wildcard subdomain matching.

### Set the allowed HTTP methods

[AllowAnyMethod](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanymethod):

* Allows any HTTP method:
* Affects preflight requests and the `Access-Control-Allow-Methods` header. For more information, see the [Preflight requests](#preflight-requests) section.

### Set the allowed request headers

To allow specific headers to be sent in a CORS request, called [author request headers](https://xhr.spec.whatwg.org/#request), call [WithHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withheaders) and specify the allowed headers:

```
using Microsoft.Net.Http.Headers;

var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
       policy =>
       {
           policy.WithOrigins("http://example.com")
                  .WithHeaders(HeaderNames.ContentType, "x-custom-header");
       });
});

builder.Services.AddControllers();

var app = builder.Build();
```

To allow all [author request headers](https://www.w3.org/TR/cors/#author-request-headers), call [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyheader):

```
var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .AllowAnyHeader();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

`AllowAnyHeader` affects preflight requests and the [Access-Control-Request-Headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Method) header. For more information, see the [Preflight requests](#preflight-requests) section.

A CORS middleware policy match to specific headers specified by `WithHeaders` is only possible when the headers sent in `Access-Control-Request-Headers` exactly match the headers stated in `WithHeaders`.

For instance, consider an app configured as follows:

```
app.UseCors(policy => policy.WithHeaders(HeaderNames.CacheControl));
```

CORS middleware declines a preflight request with the following request header because `Content-Language` ([HeaderNames.ContentLanguage](/en-us/dotnet/api/microsoft.net.http.headers.headernames.contentlanguage#microsoft-net-http-headers-headernames-contentlanguage)) isn't listed in `WithHeaders`:

```
Access-Control-Request-Headers: Cache-Control, Content-Language
```

The app returns a *200 OK* response but doesn't send the CORS headers back. Therefore, the browser doesn't attempt the cross-origin request.

### Set the exposed response headers

By default, the browser doesn't expose all of the response headers to the app. For more information, see [W3C Cross-Origin Resource Sharing (Terminology): Simple Response Header](https://www.w3.org/TR/cors/#simple-response-header).

The response headers that are available by default are:

* `Cache-Control`
* `Content-Language`
* `Content-Type`
* `Expires`
* `Last-Modified`
* `Pragma`

The CORS specification calls these headers *simple response headers*. To make other headers available to the app, call [WithExposedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withexposedheaders):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyExposeResponseHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .WithExposedHeaders("x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

### Credentials in cross-origin requests

Credentials require special handling in a CORS request. By default, the browser doesn't send credentials with a cross-origin request. Credentials include cookies and HTTP authentication schemes. To send credentials with a cross-origin request, the client must set `XMLHttpRequest.withCredentials` to `true`.

Using `XMLHttpRequest` directly:

```
var xhr = new XMLHttpRequest();
xhr.open('get', 'https://www.example.com/api/test');
xhr.withCredentials = true;
```

Using jQuery:

```
$.ajax({
  type: 'get',
  url: 'https://www.example.com/api/test',
  xhrFields: {
    withCredentials: true
  }
});
```

Using the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API):

```
fetch('https://www.example.com/api/test', {
    credentials: 'include'
});
```

The server must allow the credentials. To allow cross-origin credentials, call [AllowCredentials](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowcredentials):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyMyAllowCredentialsPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com")
                   .AllowCredentials();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

The HTTP response includes an `Access-Control-Allow-Credentials` header, which tells the browser that the server allows credentials for a cross-origin request.

If the browser sends credentials but the response doesn't include a valid `Access-Control-Allow-Credentials` header, the browser doesn't expose the response to the app, and the cross-origin request fails.

Allowing cross-origin credentials is a security risk. A website at another domain can send a signed-in user's credentials to the app on the user's behalf without the user's knowledge.

The CORS specification also states that setting origins to `"*"` (all origins) is invalid if the `Access-Control-Allow-Credentials` header is present.

[]()

## Preflight requests

For some CORS requests, the browser sends an additional [OPTIONS](https://developer.mozilla.org/docs/Web/HTTP/Methods/OPTIONS) request before making the actual request. This request is called a [preflight request](https://developer.mozilla.org/docs/Glossary/Preflight_request). The browser can skip the preflight request if **all** the following conditions are true:

* The request method is GET, HEAD, or POST.

* The app doesn't set request headers other than `Accept`, `Accept-Language`, `Content-Language`, `Content-Type`, or `Last-Event-ID`.

* The `Content-Type` header, if set, has one of the following values:

  * `application/x-www-form-urlencoded`
  * `multipart/form-data`
  * `text/plain`

The rule on request headers set for the client request applies to headers that the app sets by calling `setRequestHeader` on the `XMLHttpRequest` object. The CORS specification calls these headers [author request headers](https://www.w3.org/TR/cors/#author-request-headers). The rule doesn't apply to headers the browser can set, such as `User-Agent`, `Host`, or `Content-Length`.

The following is an example response similar to the preflight request made from the **\[Put test]** button in the [Test CORS](#testc6) section of this document.

```
General:
Request URL: https://cors3.azurewebsites.net/api/values/5
Request Method: OPTIONS
Status Code: 204 No Content

Response Headers:
Access-Control-Allow-Methods: PUT,DELETE,GET
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f8...8;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Vary: Origin

Request Headers:
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Method: PUT
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0
```

The preflight request uses the [HTTP OPTIONS](https://developer.mozilla.org/docs/Web/HTTP/Methods/OPTIONS) method. It may include the following headers:

* [Access-Control-Request-Method](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Method): The HTTP method that will be used for the actual request.
* [Access-Control-Request-Headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Headers): A list of request headers that the app sets on the actual request. As stated earlier, this doesn't include headers that the browser sets, such as `User-Agent`.
* [Access-Control-Allow-Methods](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Methods)

If the preflight request is denied, the app returns a `200 OK` response but doesn't set the CORS headers. Therefore, the browser doesn't attempt the cross-origin request. For an example of a denied preflight request, see the [Test CORS](#testc6) section of this document.

Using the F12 tools, the console app shows an error similar to one of the following, depending on the browser:

* Firefox: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at `https://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5`. (Reason: CORS request did not succeed). [Learn More](https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/CORSDidNotSucceed)
* Chromium based: Access to fetch at 'https\://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5' from origin 'https\://cors3.azurewebsites.net' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

To allow specific headers, call [WithHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withheaders):

```
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAllowHeadersPolicy",
        policy =>
        {
        policy.WithOrigins("http://example.com")
                   .WithHeaders(HeaderNames.ContentType, "x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

To allow all [author request headers](https://www.w3.org/TR/cors/#author-request-headers), call [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyheader):

```
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAllowAllHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .AllowAnyHeader();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

Browsers aren't consistent in how they set `Access-Control-Request-Headers`. If either:

* Headers are set to anything other than `"*"`
* [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicy.allowanyheader) is called: Include at least `Accept`, `Content-Type`, and `Origin`, plus any custom headers that you want to support.

[]()

### Automatic preflight request code

When the CORS policy is applied either:

* Globally by calling `app.UseCors` in `Program.cs`.
* Using the `[EnableCors]` attribute.

ASP.NET Core responds to the preflight OPTIONS request.

The [Test CORS](#testc6) section of this document demonstrates this behavior.

[]()

### \[HttpOptions] attribute for preflight requests

When CORS is enabled with the appropriate policy, ASP.NET Core generally responds to CORS preflight requests automatically.

The following code uses the [\[HttpOptions\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpoptionsattribute) attribute to create endpoints for OPTIONS requests:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }
```

See [Test CORS with \[EnableCors\] attribute and RequireCors method](#tcer) for instructions on testing the preceding code.

### Set the preflight expiration time

The `Access-Control-Max-Age` header specifies how long the response to the preflight request can be cached. To set this header, call [SetPreflightMaxAge](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.setpreflightmaxage):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MySetPreflightExpirationPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com")
                   .SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

## Enable CORS on an endpoint

[]()

## How CORS works

This section describes what happens in a [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) request at the level of the HTTP messages.

* CORS is **not** a security feature. CORS is a W3C standard that allows a server to relax the same-origin policy.
  * For example, a malicious actor could use [Cross-Site Scripting (XSS)](cross-site-scripting?view=aspnetcore-10.0) against your site and execute a cross-site request to their CORS enabled site to steal information.

* An API isn't safer by allowing CORS.

  * It's up to the client (browser) to enforce CORS. The server executes the request and returns the response, it's the client that returns an error and blocks the response. For example, any of the following tools will display the server response:

    * [Fiddler](https://www.telerik.com/fiddler)
    * [.NET HttpClient](/en-us/dotnet/csharp/tutorials/console-webapiclient)
    * A web browser by entering the URL in the address bar.

* It's a way for a server to allow browsers to execute a cross-origin [XHR](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest) or [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) request that otherwise would be forbidden.
  * Browsers without CORS can't do cross-origin requests. Before CORS, [JSONP](https://www.w3schools.com/js/js_json_jsonp.asp) was used to circumvent this restriction. JSONP doesn't use XHR, it uses the `<script>` tag to receive the response. Scripts are allowed to be loaded cross-origin.

The [CORS specification](https://www.w3.org/TR/cors/) introduced several new HTTP headers that enable cross-origin requests. If a browser supports CORS, it sets these headers automatically for cross-origin requests. Custom JavaScript code isn't required to enable CORS.

Select the **PUT** test button on the deployed sample. The `Origin` header:

* Provides the domain of the site that's making the request.
* Is required and must be different from the host.

**General headers**

```
Request URL: https://cors1.azurewebsites.net/api/values
Request Method: GET
Status Code: 200 OK
```

**Response headers**

```
Content-Encoding: gzip
Content-Type: text/plain; charset=utf-8
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Powered-By: ASP.NET
```

**Request headers**

```
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: cors1.azurewebsites.net
Origin: https://cors3.azurewebsites.net
Referer: https://cors3.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 ...
```

In `OPTIONS` requests, the server sets the **Response headers** `Access-Control-Allow-Origin: {allowed origin}` header in the response. For example, in the [sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/8.0/Cors/Web2API), the `Delete [EnableCors]` button `OPTIONS` request contains the following headers:

**General headers**

```
Request URL: https://cors3.azurewebsites.net/api/TodoItems2/MyDelete2/5
Request Method: OPTIONS
Status Code: 204 No Content
```

**Response headers**

```
Access-Control-Allow-Headers: Content-Type,x-custom-header
Access-Control-Allow-Methods: PUT,DELETE,GET,OPTIONS
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors3.azurewebsites.net
Vary: Origin
X-Powered-By: ASP.NET
```

**Request headers**

```
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: DELETE
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/test?number=2
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0
```

In the preceding **Response headers**, the server sets the [Access-Control-Allow-Origin](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header in the response. The `https://cors1.azurewebsites.net` value of this header matches the `Origin` header from the request.

If [AllowAnyOrigin](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyorigin) is called, the `Access-Control-Allow-Origin: *`, the wildcard value, is returned. `AllowAnyOrigin` allows any origin.

If the response doesn't include the `Access-Control-Allow-Origin` header, the cross-origin request fails. Specifically, the browser disallows the request. Even if the server returns a successful response, the browser doesn't make the response available to the client app.

[]()

### HTTP redirection to HTTPS causes ERR\_INVALID\_REDIRECT on the CORS preflight request

Requests to an endpoint using HTTP that are redirected to HTTPS by [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) fail with `ERR_INVALID_REDIRECT on the CORS preflight request`.

API projects can reject HTTP requests rather than use `UseHttpsRedirection` to redirect requests to HTTPS.

[]()

## CORS in IIS

When deploying to IIS, CORS has to run before Windows Authentication if the server isn't configured to allow anonymous access. To support this scenario, the [IIS CORS module](https://www.iis.net/downloads/microsoft/iis-cors-module) needs to be installed and configured for the app.

[]()

## Test CORS

The [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/8.0/Cors/Web2API) has code to test CORS. See [how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample). The sample is an API project with Razor Pages added:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                    "http://www.contoso.com",
                    "https://cors1.azurewebsites.net",
                    "https://cors3.azurewebsites.net",
                    "https://localhost:44398",
                    "https://localhost:5001")
                .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();
app.MapRazorPages();

app.Run();
```

Warning

`WithOrigins("https://localhost:<port>");` should only be used for testing a sample app similar to the [download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/cors/samples/8.0/Cors).

The following `ValuesController` provides the endpoints for testing:

```
[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}
```

[MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.

Test the preceding sample code by using one of the following approaches:

* Run the sample with `dotnet run` using the default URL of `https://localhost:5001`.
* Run the sample from Visual Studio with the port set to 44398 for a URL of `https://localhost:44398`.

Using a browser with the F12 tools:

* Select the **Values** button and review the headers in the **Network** tab.

* Select the **PUT test** button. See [Display OPTIONS requests](#options) for instructions on displaying the OPTIONS request. The **PUT test** creates two requests, an OPTIONS preflight request and the PUT request.

* Select the **`GetValues2 [DisableCors]`** button to trigger a failed CORS request. As mentioned in the document, the response returns 200 success, but the CORS request is not made. Select the **Console** tab to see the CORS error. Depending on the browser, an error similar to the following is displayed:

  Access to fetch at `'https://cors1.azurewebsites.net/api/values/GetValues2'` from origin `'https://cors3.azurewebsites.net'` has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

CORS-enabled endpoints can be tested with a tool, such as [curl](https://curl.haxx.se/) or [Fiddler](https://www.telerik.com/fiddler). When using a tool, the origin of the request specified by the `Origin` header must differ from the host receiving the request. If the request isn't *cross-origin* based on the value of the `Origin` header:

* There's no need for CORS middleware to process the request.
* CORS headers aren't returned in the response.

The following command uses `curl` to issue an OPTIONS request with information:

```
curl -X OPTIONS https://cors3.azurewebsites.net/api/TodoItems2/5 -i
```

[]()

### Test CORS with \[EnableCors] attribute and RequireCors method

Consider the following code which uses [endpoint routing](#ecors6) to enable CORS on a per-endpoint basis using `RequireCors`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                    "http://www.contoso.com",
                    "https://cors1.azurewebsites.net",
                    "https://cors3.azurewebsites.net",
                    "https://localhost:44398",
                    "https://localhost:5001")
                .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/echo",
        context => context.Response.WriteAsync("echo"))
        .RequireCors("MyPolicy");

    endpoints.MapControllers();
    endpoints.MapRazorPages();
});

app.Run();
```

Notice that only the `/echo` endpoint is using the `RequireCors` to allow cross-origin requests using the specified policy. The controllers below enable CORS using \[EnableCors] attribute.

The following `TodoItems1Controller` provides endpoints for testing:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems1Controller : ControllerBase 
{
    // PUT: api/TodoItems1/5
    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id) {
        if (id < 1) {
            return Content($"ID = {id}");
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // Delete: api/TodoItems1/5
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // GET: api/TodoItems1
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors("MyPolicy")]
    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    // Delete: api/TodoItems1/MyDelete2/5
    [EnableCors("MyPolicy")]
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}
```

The **Delete \[EnableCors]** and **GET \[EnableCors]** buttons succeed, because the endpoints have `[EnableCors]` and respond to preflight requests. The other endpoints fails. The **GET** button fails, because the [JavaScript](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/8.0/Cors/Web2API/wwwroot/js/MyJS.js) sends:

```
 headers: {
      "Content-Type": "x-custom-header"
 },
```

The following `TodoItems2Controller` provides similar endpoints, but includes explicit code to respond to OPTIONS requests:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // [EnableCors] // Not needed as OPTIONS path provided.
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // [EnableCors] //  Warning ASP0023 Route '{id}' conflicts with another action route.
    //                  An HTTP request that matches multiple routes results in an ambiguous
    //                  match error.
    [EnableCors("MyPolicy")] // Required for this path.
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors("MyPolicy")]  // Required for this path.
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}
```

The preceding code can be tested by deploying the sample to Azure.In the **Controller** drop down list, select **Preflight** and then **Set Controller**. All the CORS calls to the `TodoItems2Controller` endpoints succeed.

## Related content

* [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/docs/Web/HTTP/CORS)
* [IIS CORS module Configuration Reference](/en-us/iis/extensions/cors-module/cors-module-configuration-reference)

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Kirk Larkin](https://twitter.com/serpent5)

This article shows how to enable CORS in an ASP.NET Core app.

Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction is called the *same-origin policy*. The same-origin policy prevents a malicious site from reading sensitive data from another site. Sometimes, you might want to allow other sites to make cross-origin requests to your app. For more information, see the [Mozilla CORS article](https://developer.mozilla.org/docs/Web/HTTP/CORS).

[Cross Origin Resource Sharing](https://www.w3.org/TR/cors/) (CORS):

* Is a W3C standard that allows a server to relax the same-origin policy.
* Is **not** a security feature, CORS relaxes security. An API is not safer by allowing CORS. For more information, see [How CORS works](#how-cors).
* Allows a server to explicitly allow some cross-origin requests while rejecting others.
* Is safer and more flexible than earlier techniques, such as [JSONP](/en-us/dotnet/framework/wcf/samples/jsonp).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/6.0/Cors/WebAPI) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Same origin

Two URLs have the same origin if they have identical schemes, hosts, and ports ([RFC 6454](https://tools.ietf.org/html/rfc6454)).

These two URLs have the same origin:

* `https://example.com/foo.html`
* `https://example.com/bar.html`

These URLs have different origins than the previous two URLs:

* `https://example.net`: Different domain
* `https://www.example.com/foo.html`: Different subdomain
* `http://example.com/foo.html`: Different scheme
* `https://example.com:9000/foo.html`: Different port

## Enable CORS

There are three ways to enable CORS:

* In middleware using a [named policy](#np) or [default policy](#dp).
* Using [endpoint routing](#ecors6).
* With the [\[EnableCors\]](#attr) attribute.

Using the [\[EnableCors\]](#attr) attribute with a named policy provides the finest control in limiting endpoints that support CORS.

Warning

[UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) must be called in the correct order. For more information, see [Middleware order](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order). For example, `UseCors` must be called before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching) when using `UseResponseCaching`.

Each approach is detailed in the following sections.

[]()

## CORS with named policy and middleware

CORS middleware handles cross-origin requests. The following code applies a CORS policy to all the app's endpoints with the specified origins:

```
var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

// services.AddResponseCaching();

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The preceding code:

* Sets the policy name to `_myAllowSpecificOrigins`. The policy name is arbitrary.
* Calls the [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) extension method and specifies the `_myAllowSpecificOrigins` CORS policy. `UseCors` adds the CORS middleware. The call to `UseCors` must be placed after `UseRouting`, but before `UseAuthorization`. For more information, see [Middleware order](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).
* Calls [AddCors](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.corsservicecollectionextensions.addcors) with a [lambda expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). The lambda takes a [CorsPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder) object. [Configuration options](#cors-policy-options), such as `WithOrigins`, are described later in this article.
* Enables the `_myAllowSpecificOrigins` CORS policy for all controller endpoints. See [endpoint routing](#ecors6) to apply a CORS policy to specific endpoints.
* When using [response caching middleware](../performance/caching/middleware?view=aspnetcore-10.0), call [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching).

With endpoint routing, the CORS middleware **must** be configured to execute between the calls to `UseRouting` and `UseEndpoints`.

The [AddCors](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccorsmvccorebuilderextensions.addcors) method call adds CORS services to the app's service container:

```
var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

// services.AddResponseCaching();

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();
```

For more information, see [CORS policy options](#cpo6) in this document.

The [CorsPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder) methods can be chained, as shown in the following code:

```
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(MyAllowSpecificOrigins,
                          policy =>
                          {
                              policy.WithOrigins("http://example.com",
                                                  "http://www.contoso.com")
                                                  .AllowAnyHeader()
                                                  .AllowAnyMethod();
                          });
});

builder.Services.AddControllers();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors(MyAllowSpecificOrigins);

app.UseAuthorization();

app.MapControllers();

app.Run();
```

Note: The specified URL must **not** contain a trailing slash (`/`). If the URL terminates with `/`, the comparison returns `false` and no header is returned.

[]()

Warning

`UseCors` must be placed after `UseRouting` and before `UseAuthorization`. This is to ensure that CORS headers are included in the response for both authorized and unauthorized calls.

## UseCors and UseStaticFiles order

Typically, `UseStaticFiles` is called before `UseCors`. Apps that use JavaScript to retrieve static files cross site must call `UseCors` before `UseStaticFiles`.

[]()

### CORS with default policy and middleware

The following highlighted code enables the default CORS policy:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com");
        });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The preceding code applies the default CORS policy to all controller endpoints.

[]()

## Enable Cors with endpoint routing

Enabling CORS on a per-endpoint basis using `RequireCors` ***does not support [automatic preflight requests](#apf).*** For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/20709) and [Test CORS with endpoint routing and \[HttpOptions\]](#tcer).

With endpoint routing, CORS can be enabled on a per-endpoint basis using the [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) set of extension methods:

```
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy =>
                      {
                          policy.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/echo",
        context => context.Response.WriteAsync("echo"))
        .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapControllers()
             .RequireCors(MyAllowSpecificOrigins);

    endpoints.MapGet("/echo2",
        context => context.Response.WriteAsync("echo2"));

    endpoints.MapRazorPages();
});

app.Run();
```

In the preceding code:

* `app.UseCors` enables the CORS middleware. Because a default policy hasn't been configured, `app.UseCors()` alone doesn't enable CORS.
* The `/echo` and controller endpoints allow cross-origin requests using the specified policy.
* The `/echo2` and Razor Pages endpoints do **not** allow cross-origin requests because no default policy was specified.

The [\[DisableCors\]](#dc6) attribute does **not** disable CORS that has been enabled by endpoint routing with `RequireCors`.

See [Test CORS with endpoint routing and \[HttpOptions\]](#tcer) for instructions on testing code similar to the preceding.

[]()

## Enable CORS with attributes

Enabling CORS with the [\[EnableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute and applying a named policy to only those endpoints that require CORS provides the finest control.

The [\[EnableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute provides an alternative to applying CORS globally. The `[EnableCors]` attribute enables CORS for selected endpoints, rather than all endpoints:

* `[EnableCors]` specifies the default policy.
* `[EnableCors("{Policy String}")]` specifies a named policy.

The `[EnableCors]` attribute can be applied to:

* Razor Page `PageModel`
* Controller
* Controller action method

Different policies can be applied to controllers, page models, or action methods with the `[EnableCors]` attribute. When the `[EnableCors]` attribute is applied to a controller, page model, or action method, and CORS is enabled in middleware, **both** policies are applied. **We recommend against combining policies. Use the** `[EnableCors]` **attribute or middleware, not both in the same app.**

The following code applies a different policy to each method:

```
[Route("api/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
    // GET api/values
    [EnableCors("AnotherPolicy")]
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { "green widget", "red widget" };
    }

    // GET api/values/5
    [EnableCors("Policy1")]
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return id switch
        {
            1 => "green widget",
            2 => "red widget",
            _ => NotFound(),
        };
    }
}
```

The following code creates two CORS policies:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("Policy1",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com");
        });

    options.AddPolicy("AnotherPolicy",
        policy =>
        {
            policy.WithOrigins("http://www.contoso.com")
                                .AllowAnyHeader()
                                .AllowAnyMethod();
        });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

For the finest control of limiting CORS requests:

* Use `[EnableCors("MyPolicy")]` with a named policy.
* Don't define a default policy.
* Don't use [endpoint routing](#ecors6).

The code in the next section meets the preceding list.

[]()

### Disable CORS

The [\[DisableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.disablecorsattribute) attribute does **not** disable CORS that has been enabled by [endpoint routing](#ecors6).

The following code defines the CORS policy `"MyPolicy"`:

```
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com",
                                "http://www.contoso.com")
                    .WithMethods("PUT", "DELETE", "GET");
        });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();
app.MapRazorPages();

app.Run();
```

The following code disables CORS for the `GetValues2` action:

```
[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}
```

The preceding code:

* Doesn't enable CORS with [endpoint routing](#ecors6).
* Doesn't define a [default CORS policy](#dp).
* Uses [\[EnableCors("MyPolicy")\]](#attr) to enable the `"MyPolicy"` CORS policy for the controller.
* Disables CORS for the `GetValues2` method.

See [Test CORS](#testc6) for instructions on testing the preceding code.

[]()

## CORS policy options

This section describes the various options that can be set in a CORS policy:

* [Set the allowed origins](#set-the-allowed-origins)
* [Set the allowed HTTP methods](#set-the-allowed-http-methods)
* [Set the allowed request headers](#set-the-allowed-request-headers)
* [Set the exposed response headers](#set-the-exposed-response-headers)
* [Credentials in cross-origin requests](#credentials-in-cross-origin-requests)
* [Set the preflight expiration time](#set-the-preflight-expiration-time)

[AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corsoptions.addpolicy) is called in `Program.cs`. For some options, it may be helpful to read the [How CORS works](#how-cors) section first.

## Set the allowed origins

[AllowAnyOrigin](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyorigin): Allows CORS requests from all origins with any scheme (`http` or `https`). `AllowAnyOrigin` is insecure because *any website* can make cross-origin requests to the app.

Note

Specifying `AllowAnyOrigin` and `AllowCredentials` is an insecure configuration and can result in cross-site request forgery. The CORS service returns an invalid CORS response when an app is configured with both methods.

`AllowAnyOrigin` affects preflight requests and the `Access-Control-Allow-Origin` header. For more information, see the [Preflight requests](#preflight-requests) section.

[SetIsOriginAllowedToAllowWildcardSubdomains](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.setisoriginallowedtoallowwildcardsubdomains): Sets the [IsOriginAllowed](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicy.isoriginallowed) property of the policy to be a function that allows origins to match a configured wildcard domain when evaluating if the origin is allowed.

```
var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                .SetIsOriginAllowedToAllowWildcardSubdomains();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

In the preceding code, `SetIsOriginAllowedToAllowWildcardSubdomains` is called with the wildcard origin `"https://*.example.com"`. This configuration allows CORS requests from any subdomain of `example.com`, such as `https://subdomain.example.com` or `https://api.example.com`. The `*` wildcard character must be included in the origin to enable wildcard subdomain matching.

### Set the allowed HTTP methods

[AllowAnyMethod](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanymethod):

* Allows any HTTP method:
* Affects preflight requests and the `Access-Control-Allow-Methods` header. For more information, see the [Preflight requests](#preflight-requests) section.

### Set the allowed request headers

To allow specific headers to be sent in a CORS request, called [author request headers](https://xhr.spec.whatwg.org/#request), call [WithHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withheaders) and specify the allowed headers:

```
using Microsoft.Net.Http.Headers;

var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
       policy =>
       {
           policy.WithOrigins("http://example.com")
                  .WithHeaders(HeaderNames.ContentType, "x-custom-header");
       });
});

builder.Services.AddControllers();

var app = builder.Build();
```

To allow all [author request headers](https://www.w3.org/TR/cors/#author-request-headers), call [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyheader):

```
var MyAllowSpecificOrigins = "_MyAllowSubdomainPolicy";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .AllowAnyHeader();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

`AllowAnyHeader` affects preflight requests and the [Access-Control-Request-Headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Method) header. For more information, see the [Preflight requests](#preflight-requests) section.

A CORS middleware policy match to specific headers specified by `WithHeaders` is only possible when the headers sent in `Access-Control-Request-Headers` exactly match the headers stated in `WithHeaders`.

For instance, consider an app configured as follows:

```
app.UseCors(policy => policy.WithHeaders(HeaderNames.CacheControl));
```

CORS middleware declines a preflight request with the following request header because `Content-Language` ([HeaderNames.ContentLanguage](/en-us/dotnet/api/microsoft.net.http.headers.headernames.contentlanguage#microsoft-net-http-headers-headernames-contentlanguage)) isn't listed in `WithHeaders`:

```
Access-Control-Request-Headers: Cache-Control, Content-Language
```

The app returns a *200 OK* response but doesn't send the CORS headers back. Therefore, the browser doesn't attempt the cross-origin request.

### Set the exposed response headers

By default, the browser doesn't expose all of the response headers to the app. For more information, see [W3C Cross-Origin Resource Sharing (Terminology): Simple Response Header](https://www.w3.org/TR/cors/#simple-response-header).

The response headers that are available by default are:

* `Cache-Control`
* `Content-Language`
* `Content-Type`
* `Expires`
* `Last-Modified`
* `Pragma`

The CORS specification calls these headers *simple response headers*. To make other headers available to the app, call [WithExposedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withexposedheaders):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyExposeResponseHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .WithExposedHeaders("x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

### Credentials in cross-origin requests

Credentials require special handling in a CORS request. By default, the browser doesn't send credentials with a cross-origin request. Credentials include cookies and HTTP authentication schemes. To send credentials with a cross-origin request, the client must set `XMLHttpRequest.withCredentials` to `true`.

Using `XMLHttpRequest` directly:

```
var xhr = new XMLHttpRequest();
xhr.open('get', 'https://www.example.com/api/test');
xhr.withCredentials = true;
```

Using jQuery:

```
$.ajax({
  type: 'get',
  url: 'https://www.example.com/api/test',
  xhrFields: {
    withCredentials: true
  }
});
```

Using the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API):

```
fetch('https://www.example.com/api/test', {
    credentials: 'include'
});
```

The server must allow the credentials. To allow cross-origin credentials, call [AllowCredentials](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowcredentials):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyMyAllowCredentialsPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com")
                   .AllowCredentials();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

The HTTP response includes an `Access-Control-Allow-Credentials` header, which tells the browser that the server allows credentials for a cross-origin request.

If the browser sends credentials but the response doesn't include a valid `Access-Control-Allow-Credentials` header, the browser doesn't expose the response to the app, and the cross-origin request fails.

Allowing cross-origin credentials is a security risk. A website at another domain can send a signed-in user's credentials to the app on the user's behalf without the user's knowledge.

The CORS specification also states that setting origins to `"*"` (all origins) is invalid if the `Access-Control-Allow-Credentials` header is present.

[]()

## Preflight requests

For some CORS requests, the browser sends an additional [OPTIONS](https://developer.mozilla.org/docs/Web/HTTP/Methods/OPTIONS) request before making the actual request. This request is called a [preflight request](https://developer.mozilla.org/docs/Glossary/Preflight_request). The browser can skip the preflight request if **all** the following conditions are true:

* The request method is GET, HEAD, or POST.

* The app doesn't set request headers other than `Accept`, `Accept-Language`, `Content-Language`, `Content-Type`, or `Last-Event-ID`.

* The `Content-Type` header, if set, has one of the following values:

  * `application/x-www-form-urlencoded`
  * `multipart/form-data`
  * `text/plain`

The rule on request headers set for the client request applies to headers that the app sets by calling `setRequestHeader` on the `XMLHttpRequest` object. The CORS specification calls these headers [author request headers](https://www.w3.org/TR/cors/#author-request-headers). The rule doesn't apply to headers the browser can set, such as `User-Agent`, `Host`, or `Content-Length`.

The following is an example response similar to the preflight request made from the **\[Put test]** button in the [Test CORS](#testc6) section of this document.

```
General:
Request URL: https://cors3.azurewebsites.net/api/values/5
Request Method: OPTIONS
Status Code: 204 No Content

Response Headers:
Access-Control-Allow-Methods: PUT,DELETE,GET
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f8...8;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Vary: Origin

Request Headers:
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Method: PUT
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0
```

The preflight request uses the [HTTP OPTIONS](https://developer.mozilla.org/docs/Web/HTTP/Methods/OPTIONS) method. It may include the following headers:

* [Access-Control-Request-Method](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Method): The HTTP method that will be used for the actual request.
* [Access-Control-Request-Headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Headers): A list of request headers that the app sets on the actual request. As stated earlier, this doesn't include headers that the browser sets, such as `User-Agent`.
* [Access-Control-Allow-Methods](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Methods)

If the preflight request is denied, the app returns a `200 OK` response but doesn't set the CORS headers. Therefore, the browser doesn't attempt the cross-origin request. For an example of a denied preflight request, see the [Test CORS](#testc6) section of this document.

Using the F12 tools, the console app shows an error similar to one of the following, depending on the browser:

* Firefox: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at `https://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5`. (Reason: CORS request did not succeed). [Learn More](https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/CORSDidNotSucceed)
* Chromium based: Access to fetch at 'https\://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5' from origin 'https\://cors3.azurewebsites.net' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

To allow specific headers, call [WithHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withheaders):

```
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAllowHeadersPolicy",
        policy =>
        {
        policy.WithOrigins("http://example.com")
                   .WithHeaders(HeaderNames.ContentType, "x-custom-header");
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

To allow all [author request headers](https://www.w3.org/TR/cors/#author-request-headers), call [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyheader):

```
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MyAllowAllHeadersPolicy",
        policy =>
        {
            policy.WithOrigins("https://*.example.com")
                   .AllowAnyHeader();
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

Browsers aren't consistent in how they set `Access-Control-Request-Headers`. If either:

* Headers are set to anything other than `"*"`
* [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicy.allowanyheader) is called: Include at least `Accept`, `Content-Type`, and `Origin`, plus any custom headers that you want to support.

[]()

### Automatic preflight request code

When the CORS policy is applied either:

* Globally by calling `app.UseCors` in `Program.cs`.
* Using the `[EnableCors]` attribute.

ASP.NET Core responds to the preflight OPTIONS request.

Enabling CORS on a per-endpoint basis using `RequireCors` currently does **not** support automatic preflight requests.

The [Test CORS](#testc6) section of this document demonstrates this behavior.

[]()

### \[HttpOptions] attribute for preflight requests

When CORS is enabled with the appropriate policy, ASP.NET Core generally responds to CORS preflight requests automatically. In some scenarios, this may not be the case. For example, using [CORS with endpoint routing](#ecors6).

The following code uses the [\[HttpOptions\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpoptionsattribute) attribute to create endpoints for OPTIONS requests:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }
```

See [Test CORS with endpoint routing and \[HttpOptions\]](#tcer) for instructions on testing the preceding code.

### Set the preflight expiration time

The `Access-Control-Max-Age` header specifies how long the response to the preflight request can be cached. To set this header, call [SetPreflightMaxAge](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.setpreflightmaxage):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("MySetPreflightExpirationPolicy",
        policy =>
        {
            policy.WithOrigins("http://example.com")
                   .SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
        });
});

builder.Services.AddControllers();

var app = builder.Build();
```

[]()

## How CORS works

This section describes what happens in a [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) request at the level of the HTTP messages.

* CORS is **not** a security feature. CORS is a W3C standard that allows a server to relax the same-origin policy.
  * For example, a malicious actor could use [Cross-Site Scripting (XSS)](cross-site-scripting?view=aspnetcore-10.0) against your site and execute a cross-site request to their CORS enabled site to steal information.

* An API isn't safer by allowing CORS.

  * It's up to the client (browser) to enforce CORS. The server executes the request and returns the response, it's the client that returns an error and blocks the response. For example, any of the following tools will display the server response:

    * [Fiddler](https://www.telerik.com/fiddler)
    * [.NET HttpClient](/en-us/dotnet/csharp/tutorials/console-webapiclient)
    * A web browser by entering the URL in the address bar.

* It's a way for a server to allow browsers to execute a cross-origin [XHR](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest) or [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) request that otherwise would be forbidden.
  * Browsers without CORS can't do cross-origin requests. Before CORS, [JSONP](https://www.w3schools.com/js/js_json_jsonp.asp) was used to circumvent this restriction. JSONP doesn't use XHR, it uses the `<script>` tag to receive the response. Scripts are allowed to be loaded cross-origin.

The [CORS specification](https://www.w3.org/TR/cors/) introduced several new HTTP headers that enable cross-origin requests. If a browser supports CORS, it sets these headers automatically for cross-origin requests. Custom JavaScript code isn't required to enable CORS.

The following is an example of a cross-origin request from the **Values** test button to `https://cors1.azurewebsites.net/api/values`. The `Origin` header:

* Provides the domain of the site that's making the request.
* Is required and must be different from the host.

**General headers**

```
Request URL: https://cors1.azurewebsites.net/api/values
Request Method: GET
Status Code: 200 OK
```

**Response headers**

```
Content-Encoding: gzip
Content-Type: text/plain; charset=utf-8
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Powered-By: ASP.NET
```

**Request headers**

```
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: cors1.azurewebsites.net
Origin: https://cors3.azurewebsites.net
Referer: https://cors3.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 ...
```

In `OPTIONS` requests, the server sets the **Response headers** `Access-Control-Allow-Origin: {allowed origin}` header in the response. For example, the deployed [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/3.1/Cors/WebAPI), Delete button `OPTIONS` request contains the following headers:

**General headers**

```
Request URL: https://cors3.azurewebsites.net/api/TodoItems2/MyDelete2/5
Request Method: OPTIONS
Status Code: 204 No Content
```

**Response headers**

```
Access-Control-Allow-Headers: Content-Type,x-custom-header
Access-Control-Allow-Methods: PUT,DELETE,GET,OPTIONS
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors3.azurewebsites.net
Vary: Origin
X-Powered-By: ASP.NET
```

**Request headers**

```
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: DELETE
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/test?number=2
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0
```

In the preceding **Response headers**, the server sets the [Access-Control-Allow-Origin](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header in the response. The `https://cors1.azurewebsites.net` value of this header matches the `Origin` header from the request.

If [AllowAnyOrigin](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyorigin) is called, the `Access-Control-Allow-Origin: *`, the wildcard value, is returned. `AllowAnyOrigin` allows any origin.

If the response doesn't include the `Access-Control-Allow-Origin` header, the cross-origin request fails. Specifically, the browser disallows the request. Even if the server returns a successful response, the browser doesn't make the response available to the client app.

[]()

### HTTP redirection to HTTPS causes ERR\_INVALID\_REDIRECT on the CORS preflight request

Requests to an endpoint using HTTP that are redirected to HTTPS by [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) fail with `ERR_INVALID_REDIRECT on the CORS preflight request`.

API projects can reject HTTP requests rather than use `UseHttpsRedirection` to redirect requests to HTTPS.

[]()

### Display OPTIONS requests

By default, the Chrome and Edge browsers don't show OPTIONS requests on the network tab of the F12 tools. To display OPTIONS requests in these browsers:

* `chrome://flags/#out-of-blink-cors` or `edge://flags/#out-of-blink-cors`
* disable the flag.
* restart.

Firefox shows OPTIONS requests by default.

## CORS in IIS

When deploying to IIS, CORS has to run before Windows Authentication if the server isn't configured to allow anonymous access. To support this scenario, the [IIS CORS module](https://www.iis.net/downloads/microsoft/iis-cors-module) needs to be installed and configured for the app.

[]()

## Test CORS

The [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/3.1/Cors/WebAPI) has code to test CORS. See [how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample). The sample is an API project with Razor Pages added:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
                policy =>
                {
                    policy.WithOrigins("http://example.com",
                        "http://www.contoso.com",
                        "https://cors1.azurewebsites.net",
                        "https://cors3.azurewebsites.net",
                        "https://localhost:44398",
                        "https://localhost:5001")
                            .WithMethods("PUT", "DELETE", "GET");
                });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();
app.MapRazorPages();

app.Run();
```

Warning

`WithOrigins("https://localhost:<port>");` should only be used for testing a sample app similar to the [download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/cors/samples/3.1/Cors).

The following `ValuesController` provides the endpoints for testing:

```
[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}
```

[MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.

Test the preceding sample code by using one of the following approaches:

* Run the sample with `dotnet run` using the default URL of `https://localhost:5001`.
* Run the sample from Visual Studio with the port set to 44398 for a URL of `https://localhost:44398`.

Using a browser with the F12 tools:

* Select the **Values** button and review the headers in the **Network** tab.

* Select the **PUT test** button. See [Display OPTIONS requests](#options) for instructions on displaying the OPTIONS request. The **PUT test** creates two requests, an OPTIONS preflight request and the PUT request.

* Select the **`GetValues2 [DisableCors]`** button to trigger a failed CORS request. As mentioned in the document, the response returns 200 success, but the CORS request is not made. Select the **Console** tab to see the CORS error. Depending on the browser, an error similar to the following is displayed:

  Access to fetch at `'https://cors1.azurewebsites.net/api/values/GetValues2'` from origin `'https://cors3.azurewebsites.net'` has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

CORS-enabled endpoints can be tested with a tool, such as [curl](https://curl.haxx.se/) or [Fiddler](https://www.telerik.com/fiddler). When using a tool, the origin of the request specified by the `Origin` header must differ from the host receiving the request. If the request isn't *cross-origin* based on the value of the `Origin` header:

* There's no need for CORS middleware to process the request.
* CORS headers aren't returned in the response.

The following command uses `curl` to issue an OPTIONS request with information:

```
curl -X OPTIONS https://cors3.azurewebsites.net/api/TodoItems2/5 -i
```

[]()

### Test CORS with endpoint routing and \[HttpOptions]

Enabling CORS on a per-endpoint basis using `RequireCors` currently does **not** support [automatic preflight requests](#apf). Consider the following code which uses [endpoint routing to enable CORS](#ecors6):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "MyPolicy",
                policy =>
                {
                    policy.WithOrigins("http://example.com",
                        "http://www.contoso.com",
                        "https://cors1.azurewebsites.net",
                        "https://cors3.azurewebsites.net",
                        "https://localhost:44398",
                        "https://localhost:5001")
                            .WithMethods("PUT", "DELETE", "GET");
                });
});

builder.Services.AddControllers();
builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseCors();

app.UseAuthorization();

app.MapControllers();
app.MapRazorPages();

app.Run();
```

The following `TodoItems1Controller` provides endpoints for testing:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems1Controller : ControllerBase
{
    // PUT: api/TodoItems1/5
    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return Content($"ID = {id}");
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // Delete: api/TodoItems1/5
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // GET: api/TodoItems1
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors]
    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    // Delete: api/TodoItems1/MyDelete2/5
    [EnableCors]
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}
```

Test the preceding code from the test page (`https://cors1.azurewebsites.net/test?number=1`) of the deployed [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/3.1/Cors/WebAPI).

The **Delete \[EnableCors]** and **GET \[EnableCors]** buttons succeed, because the endpoints have `[EnableCors]` and respond to preflight requests. The other endpoints fails. The **GET** button fails, because the [JavaScript](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/3.1/Cors/WebAPI/wwwroot/js/MyJS.js) sends:

```
 headers: {
      "Content-Type": "x-custom-header"
 },
```

The following `TodoItems2Controller` provides similar endpoints, but includes explicit code to respond to OPTIONS requests:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // [EnableCors] // Not needed as OPTIONS path provided
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    [EnableCors]  // Rquired for this path
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors]  // Rquired for this path
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}
```

The preceding code can be tested by deploying the sample to Azure.In the **Controller** drop down list, select **Preflight** and then **Set Controller**. All the CORS calls to the `TodoItems2Controller` endpoints succeed.

## Additional resources

* [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/docs/Web/HTTP/CORS)
* [IIS CORS module Configuration Reference](/en-us/iis/extensions/cors-module/cors-module-configuration-reference)

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Kirk Larkin](https://twitter.com/serpent5)

This article shows how to enable CORS in an ASP.NET Core app.

Browser security prevents a web page from making requests to a different domain than the one that served the web page. This restriction is called the *same-origin policy*. The same-origin policy prevents a malicious site from reading sensitive data from another site. Sometimes, you might want to allow other sites to make cross-origin requests to your app. For more information, see the [Mozilla CORS article](https://developer.mozilla.org/docs/Web/HTTP/CORS).

[Cross Origin Resource Sharing](https://www.w3.org/TR/cors/) (CORS):

* Is a W3C standard that allows a server to relax the same-origin policy.
* Is **not** a security feature, CORS relaxes security. An API is not safer by allowing CORS. For more information, see [How CORS works](#how-cors).
* Allows a server to explicitly allow some cross-origin requests while rejecting others.
* Is safer and more flexible than earlier techniques, such as [JSONP](/en-us/dotnet/framework/wcf/samples/jsonp).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/3.1/Cors/WebAPI) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Same origin

Two URLs have the same origin if they have identical schemes, hosts, and ports ([RFC 6454](https://tools.ietf.org/html/rfc6454)).

These two URLs have the same origin:

* `https://example.com/foo.html`
* `https://example.com/bar.html`

These URLs have different origins than the previous two URLs:

* `https://example.net`: Different domain
* `https://www.example.com/foo.html`: Different subdomain
* `http://example.com/foo.html`: Different scheme
* `https://example.com:9000/foo.html`: Different port

## Enable CORS

There are three ways to enable CORS:

* In middleware using a [named policy](#np) or [default policy](#dp).
* Using [endpoint routing](#ecors).
* With the [\[EnableCors\]](#attr) attribute.

Using the [\[EnableCors\]](#attr) attribute with a named policy provides the finest control in limiting endpoints that support CORS.

Warning

[UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) must be called in the correct order. For more information, see [Middleware order](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order). For example, `UseCors` must be called before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching) when using `UseResponseCaching`.

Each approach is detailed in the following sections.

[]()

## CORS with named policy and middleware

CORS middleware handles cross-origin requests. The following code applies a CORS policy to all the app's endpoints with the specified origins:

```
public class Startup
{
    readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy(name: MyAllowSpecificOrigins,
                              policy =>
                              {
                                  policy.WithOrigins("http://example.com",
                                                      "http://www.contoso.com");
                              });
        });

        // services.AddResponseCaching();
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors(MyAllowSpecificOrigins);

        // app.UseResponseCaching();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}
```

The preceding code:

* Sets the policy name to `_myAllowSpecificOrigins`. The policy name is arbitrary.
* Calls the [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) extension method and specifies the `_myAllowSpecificOrigins` CORS policy. `UseCors` adds the CORS middleware. The call to `UseCors` must be placed after `UseRouting`, but before `UseAuthorization`. For more information, see [Middleware order](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).
* Calls [AddCors](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.corsservicecollectionextensions.addcors) with a [lambda expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). The lambda takes a [CorsPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder) object. [Configuration options](#cors-policy-options), such as `WithOrigins`, are described later in this article.
* Enables the `_myAllowSpecificOrigins` CORS policy for all controller endpoints. See [endpoint routing](#ecors) to apply a CORS policy to specific endpoints.
* When using [response caching middleware](../performance/caching/middleware?view=aspnetcore-10.0), call [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) before [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching).

With endpoint routing, the CORS middleware **must** be configured to execute between the calls to `UseRouting` and `UseEndpoints`.

See [Test CORS](#testc) for instructions on testing code similar to the preceding code.

The [AddCors](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccorsmvccorebuilderextensions.addcors) method call adds CORS services to the app's service container:

```
public class Startup
{
    readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy(name: MyAllowSpecificOrigins,
                              policy =>
                              {
                                  policy.WithOrigins("http://example.com",
                                                      "http://www.contoso.com");
                              });
        });

        // services.AddResponseCaching();
        services.AddControllers();
    }
```

For more information, see [CORS policy options](#cpo) in this document.

The [CorsPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder) methods can be chained, as shown in the following code:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddPolicy(MyAllowSpecificOrigins,
                          policy =>
                          {
                              policy.WithOrigins("http://example.com",
                                                  "http://www.contoso.com")
                                                  .AllowAnyHeader()
                                                  .AllowAnyMethod();
                          });
    });

    services.AddControllers();
}
```

Note: The specified URL must **not** contain a trailing slash (`/`). If the URL terminates with `/`, the comparison returns `false` and no header is returned.

[]()

### CORS with default policy and middleware

The following highlighted code enables the default CORS policy:

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddDefaultPolicy(
                policy =>
                {
                    policy.WithOrigins("http://example.com",
                                        "http://www.contoso.com");
                });
        });

        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}
```

The preceding code applies the default CORS policy to all controller endpoints.

[]()

## Enable Cors with endpoint routing

Enabling CORS on a per-endpoint basis using `RequireCors` ***does not support [automatic preflight requests](#apf).*** For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/20709) and [Test CORS with endpoint routing and \[HttpOptions\]](#tcer).

With endpoint routing, CORS can be enabled on a per-endpoint basis using the [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) set of extension methods:

```
public class Startup
{
    readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy(name: MyAllowSpecificOrigins,
                              policy =>
                              {
                                  policy.WithOrigins("http://example.com",
                                                      "http://www.contoso.com");
                              });
        });

        services.AddControllers();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/echo",
                context => context.Response.WriteAsync("echo"))
                .RequireCors(MyAllowSpecificOrigins);

            endpoints.MapControllers()
                     .RequireCors(MyAllowSpecificOrigins);

            endpoints.MapGet("/echo2",
                context => context.Response.WriteAsync("echo2"));

            endpoints.MapRazorPages();
        });
    }
}
```

In the preceding code:

* `app.UseCors` enables the CORS middleware. Because a default policy hasn't been configured, `app.UseCors()` alone doesn't enable CORS.
* The `/echo` and controller endpoints allow cross-origin requests using the specified policy.
* The `/echo2` and Razor Pages endpoints do **not** allow cross-origin requests because no default policy was specified.

The [\[DisableCors\]](#dc) attribute does **not** disable CORS that has been enabled by endpoint routing with `RequireCors`.

See [Test CORS with endpoint routing and \[HttpOptions\]](#tcer) for instructions on testing code similar to the preceding.

[]()

## Enable CORS with attributes

Enabling CORS with the [\[EnableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute and applying a named policy to only those endpoints that require CORS provides the finest control.

The [\[EnableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attribute provides an alternative to applying CORS globally. The `[EnableCors]` attribute enables CORS for selected endpoints, rather than all endpoints:

* `[EnableCors]` specifies the default policy.
* `[EnableCors("{Policy String}")]` specifies a named policy.

The `[EnableCors]` attribute can be applied to:

* Razor Page `PageModel`
* Controller
* Controller action method

Different policies can be applied to controllers, page models, or action methods with the `[EnableCors]` attribute. When the `[EnableCors]` attribute is applied to a controller, page model, or action method, and CORS is enabled in middleware, **both** policies are applied. **We recommend against combining policies. Use the** `[EnableCors]` **attribute or middleware, not both in the same app.**

The following code applies a different policy to each method:

```
[Route("api/[controller]")]
[ApiController]
public class WidgetController : ControllerBase
{
    // GET api/values
    [EnableCors("AnotherPolicy")]
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { "green widget", "red widget" };
    }

    // GET api/values/5
    [EnableCors("Policy1")]
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return id switch
        {
            1 => "green widget",
            2 => "red widget",
            _ => NotFound(),
        };
    }
}
```

The following code creates two CORS policies:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy("Policy1",
                policy =>
                {
                    policy.WithOrigins("http://example.com",
                                        "http://www.contoso.com");
                });

            options.AddPolicy("AnotherPolicy",
                policy =>
                {
                    policy.WithOrigins("http://www.contoso.com")
                                        .AllowAnyHeader()
                                        .AllowAnyMethod();
                });
        });

        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}
```

For the finest control of limiting CORS requests:

* Use `[EnableCors("MyPolicy")]` with a named policy.
* Don't define a default policy.
* Don't use [endpoint routing](#ecors).

The code in the next section meets the preceding list.

See [Test CORS](#testc) for instructions on testing code similar to the preceding code.

[]()

### Disable CORS

The [\[DisableCors\]](/en-us/dotnet/api/microsoft.aspnetcore.cors.disablecorsattribute) attribute does **not** disable CORS that has been enabled by [endpoint routing](#ecors).

The following code defines the CORS policy `"MyPolicy"`:

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy(name: "MyPolicy",
                policy =>
                {
                    policy.WithOrigins("http://example.com",
                                        "http://www.contoso.com")
                            .WithMethods("PUT", "DELETE", "GET");
                });
        });

        services.AddControllers();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapRazorPages();
        });
    }
}
```

The following code disables CORS for the `GetValues2` action:

```
[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}
```

The preceding code:

* Doesn't enable CORS with [endpoint routing](#ecors).
* Doesn't define a [default CORS policy](#dp).
* Uses [\[EnableCors("MyPolicy")\]](#attr) to enable the `"MyPolicy"` CORS policy for the controller.
* Disables CORS for the `GetValues2` method.

See [Test CORS](#testc) for instructions on testing the preceding code.

[]()

## CORS policy options

This section describes the various options that can be set in a CORS policy:

* [Set the allowed origins](#set-the-allowed-origins)
* [Set the allowed HTTP methods](#set-the-allowed-http-methods)
* [Set the allowed request headers](#set-the-allowed-request-headers)
* [Set the exposed response headers](#set-the-exposed-response-headers)
* [Credentials in cross-origin requests](#credentials-in-cross-origin-requests)
* [Set the preflight expiration time](#set-the-preflight-expiration-time)

[AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corsoptions.addpolicy) is called in `Startup.ConfigureServices`. For some options, it may be helpful to read the [How CORS works](#how-cors) section first.

## Set the allowed origins

[AllowAnyOrigin](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyorigin): Allows CORS requests from all origins with any scheme (`http` or `https`). `AllowAnyOrigin` is insecure because *any website* can make cross-origin requests to the app.

Note

Specifying `AllowAnyOrigin` and `AllowCredentials` is an insecure configuration and can result in cross-site request forgery. The CORS service returns an invalid CORS response when an app is configured with both methods.

`AllowAnyOrigin` affects preflight requests and the `Access-Control-Allow-Origin` header. For more information, see the [Preflight requests](#preflight-requests) section.

[SetIsOriginAllowedToAllowWildcardSubdomains](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.setisoriginallowedtoallowwildcardsubdomains): Sets the [IsOriginAllowed](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicy.isoriginallowed) property of the policy to be a function that allows origins to match a configured wildcard domain when evaluating if the origin is allowed.

```
options.AddPolicy("MyAllowSubdomainPolicy",
    policy =>
    {
        policy.WithOrigins("https://*.example.com")
            .SetIsOriginAllowedToAllowWildcardSubdomains();
    });
```

In the preceding code, `SetIsOriginAllowedToAllowWildcardSubdomains` is called with the wildcard origin `"https://*.example.com"`. This configuration allows CORS requests from any subdomain of `example.com`, such as `https://subdomain.example.com` or `https://api.example.com`. The `*` wildcard character must be included in the origin to enable wildcard subdomain matching.

### Set the allowed HTTP methods

[AllowAnyMethod](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanymethod):

* Allows any HTTP method:
* Affects preflight requests and the `Access-Control-Allow-Methods` header. For more information, see the [Preflight requests](#preflight-requests) section.

### Set the allowed request headers

To allow specific headers to be sent in a CORS request, called [author request headers](https://xhr.spec.whatwg.org/#request), call [WithHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withheaders) and specify the allowed headers:

```
options.AddPolicy("MyAllowHeadersPolicy",
    policy =>
    {
        // requires using Microsoft.Net.Http.Headers;
        policy.WithOrigins("http://example.com")
               .WithHeaders(HeaderNames.ContentType, "x-custom-header");
    });
```

To allow all [author request headers](https://www.w3.org/TR/cors/#author-request-headers), call [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyheader):

```
options.AddPolicy("MyAllowAllHeadersPolicy",
    policy =>
    {
        policy.WithOrigins("https://*.example.com")
               .AllowAnyHeader();
    });
```

`AllowAnyHeader` affects preflight requests and the [Access-Control-Request-Headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Method) header. For more information, see the [Preflight requests](#preflight-requests) section.

A CORS middleware policy match to specific headers specified by `WithHeaders` is only possible when the headers sent in `Access-Control-Request-Headers` exactly match the headers stated in `WithHeaders`.

For instance, consider an app configured as follows:

```
app.UseCors(policy => policy.WithHeaders(HeaderNames.CacheControl));
```

CORS middleware declines a preflight request with the following request header because `Content-Language` ([HeaderNames.ContentLanguage](/en-us/dotnet/api/microsoft.net.http.headers.headernames.contentlanguage#microsoft-net-http-headers-headernames-contentlanguage)) isn't listed in `WithHeaders`:

```
Access-Control-Request-Headers: Cache-Control, Content-Language
```

The app returns a *200 OK* response but doesn't send the CORS headers back. Therefore, the browser doesn't attempt the cross-origin request.

### Set the exposed response headers

By default, the browser doesn't expose all of the response headers to the app. For more information, see [W3C Cross-Origin Resource Sharing (Terminology): Simple Response Header](https://www.w3.org/TR/cors/#simple-response-header).

The response headers that are available by default are:

* `Cache-Control`
* `Content-Language`
* `Content-Type`
* `Expires`
* `Last-Modified`
* `Pragma`

The CORS specification calls these headers *simple response headers*. To make other headers available to the app, call [WithExposedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withexposedheaders):

```
options.AddPolicy("MyExposeResponseHeadersPolicy",
    policy =>
    {
        policy.WithOrigins("https://*.example.com")
               .WithExposedHeaders("x-custom-header");
    });
```

### Credentials in cross-origin requests

Credentials require special handling in a CORS request. By default, the browser doesn't send credentials with a cross-origin request. Credentials include cookies and HTTP authentication schemes. To send credentials with a cross-origin request, the client must set `XMLHttpRequest.withCredentials` to `true`.

Using `XMLHttpRequest` directly:

```
var xhr = new XMLHttpRequest();
xhr.open('get', 'https://www.example.com/api/test');
xhr.withCredentials = true;
```

Using jQuery:

```
$.ajax({
  type: 'get',
  url: 'https://www.example.com/api/test',
  xhrFields: {
    withCredentials: true
  }
});
```

Using the [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API):

```
fetch('https://www.example.com/api/test', {
    credentials: 'include'
});
```

The server must allow the credentials. To allow cross-origin credentials, call [AllowCredentials](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowcredentials):

```
options.AddPolicy("MyMyAllowCredentialsPolicy",
    policy =>
    {
        policy.WithOrigins("http://example.com")
               .AllowCredentials();
    });
```

The HTTP response includes an `Access-Control-Allow-Credentials` header, which tells the browser that the server allows credentials for a cross-origin request.

If the browser sends credentials but the response doesn't include a valid `Access-Control-Allow-Credentials` header, the browser doesn't expose the response to the app, and the cross-origin request fails.

Allowing cross-origin credentials is a security risk. A website at another domain can send a signed-in user's credentials to the app on the user's behalf without the user's knowledge.

The CORS specification also states that setting origins to `"*"` (all origins) is invalid if the `Access-Control-Allow-Credentials` header is present.

[]()

## Preflight requests

For some CORS requests, the browser sends an additional [OPTIONS](https://developer.mozilla.org/docs/Web/HTTP/Methods/OPTIONS) request before making the actual request. This request is called a [preflight request](https://developer.mozilla.org/docs/Glossary/Preflight_request). The browser can skip the preflight request if **all** the following conditions are true:

* The request method is GET, HEAD, or POST.

* The app doesn't set request headers other than `Accept`, `Accept-Language`, `Content-Language`, `Content-Type`, or `Last-Event-ID`.

* The `Content-Type` header, if set, has one of the following values:

  * `application/x-www-form-urlencoded`
  * `multipart/form-data`
  * `text/plain`

The rule on request headers set for the client request applies to headers that the app sets by calling `setRequestHeader` on the `XMLHttpRequest` object. The CORS specification calls these headers [author request headers](https://www.w3.org/TR/cors/#author-request-headers). The rule doesn't apply to headers the browser can set, such as `User-Agent`, `Host`, or `Content-Length`.

The following is an example response similar to the preflight request made from the **\[Put test]** button in the [Test CORS](#testc) section of this document.

```
General:
Request URL: https://cors3.azurewebsites.net/api/values/5
Request Method: OPTIONS
Status Code: 204 No Content

Response Headers:
Access-Control-Allow-Methods: PUT,DELETE,GET
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f8...8;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Vary: Origin

Request Headers:
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Method: PUT
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0
```

The preflight request uses the [HTTP OPTIONS](https://developer.mozilla.org/docs/Web/HTTP/Methods/OPTIONS) method. It may include the following headers:

* [Access-Control-Request-Method](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Request-Method): The HTTP method that will be used for the actual request.
* [Access-Control-Request-Headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Headers): A list of request headers that the app sets on the actual request. As stated earlier, this doesn't include headers that the browser sets, such as `User-Agent`.
* [Access-Control-Allow-Methods](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Methods)

If the preflight request is denied, the app returns a `200 OK` response but doesn't set the CORS headers. Therefore, the browser doesn't attempt the cross-origin request. For an example of a denied preflight request, see the [Test CORS](#testc) section of this document.

Using the F12 tools, the console app shows an error similar to one of the following, depending on the browser:

* Firefox: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at `https://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5`. (Reason: CORS request did not succeed). [Learn More](https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/CORSDidNotSucceed)
* Chromium based: Access to fetch at 'https\://cors1.azurewebsites.net/api/TodoItems1/MyDelete2/5' from origin 'https\://cors3.azurewebsites.net' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

To allow specific headers, call [WithHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withheaders):

```
options.AddPolicy("MyAllowHeadersPolicy",
    policy =>
    {
        // requires using Microsoft.Net.Http.Headers;
        policy.WithOrigins("http://example.com")
               .WithHeaders(HeaderNames.ContentType, "x-custom-header");
    });
```

To allow all [author request headers](https://www.w3.org/TR/cors/#author-request-headers), call [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyheader):

```
options.AddPolicy("MyAllowAllHeadersPolicy",
    policy =>
    {
        policy.WithOrigins("https://*.example.com")
               .AllowAnyHeader();
    });
```

Browsers aren't consistent in how they set `Access-Control-Request-Headers`. If either:

* Headers are set to anything other than `"*"`
* [AllowAnyHeader](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicy.allowanyheader) is called: Include at least `Accept`, `Content-Type`, and `Origin`, plus any custom headers that you want to support.

[]()

### Automatic preflight request code

When the CORS policy is applied either:

* Globally by calling `app.UseCors` in `Startup.Configure`.
* Using the `[EnableCors]` attribute.

ASP.NET Core responds to the preflight OPTIONS request.

Enabling CORS on a per-endpoint basis using `RequireCors` currently does **not** support automatic preflight requests.

The [Test CORS](#testc) section of this document demonstrates this behavior.

[]()

### \[HttpOptions] attribute for preflight requests

When CORS is enabled with the appropriate policy, ASP.NET Core generally responds to CORS preflight requests automatically. In some scenarios, this may not be the case. For example, using [CORS with endpoint routing](#ecors).

The following code uses the [\[HttpOptions\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpoptionsattribute) attribute to create endpoints for OPTIONS requests:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }
```

See [Test CORS with endpoint routing and \[HttpOptions\]](#tcer) for instructions on testing the preceding code.

### Set the preflight expiration time

The `Access-Control-Max-Age` header specifies how long the response to the preflight request can be cached. To set this header, call [SetPreflightMaxAge](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.setpreflightmaxage):

```
options.AddPolicy("MySetPreflightExpirationPolicy",
    policy =>
    {
        policy.WithOrigins("http://example.com")
               .SetPreflightMaxAge(TimeSpan.FromSeconds(2520));
    });
```

[]()

## How CORS works

This section describes what happens in a [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) request at the level of the HTTP messages.

* CORS is **not** a security feature. CORS is a W3C standard that allows a server to relax the same-origin policy.
  * For example, a malicious actor could use [Cross-Site Scripting (XSS)](cross-site-scripting?view=aspnetcore-10.0) against your site and execute a cross-site request to their CORS enabled site to steal information.

* An API isn't safer by allowing CORS.

  * It's up to the client (browser) to enforce CORS. The server executes the request and returns the response, it's the client that returns an error and blocks the response. For example, any of the following tools will display the server response:

    * [Fiddler](https://www.telerik.com/fiddler)
    * [.NET HttpClient](/en-us/dotnet/csharp/tutorials/console-webapiclient)
    * A web browser by entering the URL in the address bar.

* It's a way for a server to allow browsers to execute a cross-origin [XHR](https://developer.mozilla.org/docs/Web/API/XMLHttpRequest) or [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) request that otherwise would be forbidden.
  * Browsers without CORS can't do cross-origin requests. Before CORS, [JSONP](https://www.w3schools.com/js/js_json_jsonp.asp) was used to circumvent this restriction. JSONP doesn't use XHR, it uses the `<script>` tag to receive the response. Scripts are allowed to be loaded cross-origin.

The [CORS specification](https://www.w3.org/TR/cors/) introduced several new HTTP headers that enable cross-origin requests. If a browser supports CORS, it sets these headers automatically for cross-origin requests. Custom JavaScript code isn't required to enable CORS.

The following is an example of a cross-origin request from the **Values** test button to `https://cors1.azurewebsites.net/api/values`. The `Origin` header:

* Provides the domain of the site that's making the request.
* Is required and must be different from the host.

**General headers**

```
Request URL: https://cors1.azurewebsites.net/api/values
Request Method: GET
Status Code: 200 OK
```

**Response headers**

```
Content-Encoding: gzip
Content-Type: text/plain; charset=utf-8
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors1.azurewebsites.net
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Powered-By: ASP.NET
```

**Request headers**

```
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: cors1.azurewebsites.net
Origin: https://cors3.azurewebsites.net
Referer: https://cors3.azurewebsites.net/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 ...
```

In `OPTIONS` requests, the server sets the **Response headers** `Access-Control-Allow-Origin: {allowed origin}` header in the response. For example, the deployed [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/3.1/Cors/WebAPI), Delete button `OPTIONS` request contains the following headers:

**General headers**

```
Request URL: https://cors3.azurewebsites.net/api/TodoItems2/MyDelete2/5
Request Method: OPTIONS
Status Code: 204 No Content
```

**Response headers**

```
Access-Control-Allow-Headers: Content-Type,x-custom-header
Access-Control-Allow-Methods: PUT,DELETE,GET,OPTIONS
Access-Control-Allow-Origin: https://cors1.azurewebsites.net
Server: Microsoft-IIS/10.0
Set-Cookie: ARRAffinity=8f...;Path=/;HttpOnly;Domain=cors3.azurewebsites.net
Vary: Origin
X-Powered-By: ASP.NET
```

**Request headers**

```
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: DELETE
Connection: keep-alive
Host: cors3.azurewebsites.net
Origin: https://cors1.azurewebsites.net
Referer: https://cors1.azurewebsites.net/test?number=2
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0
```

In the preceding **Response headers**, the server sets the [Access-Control-Allow-Origin](https://developer.mozilla.org/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header in the response. The `https://cors1.azurewebsites.net` value of this header matches the `Origin` header from the request.

If [AllowAnyOrigin](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.allowanyorigin) is called, the `Access-Control-Allow-Origin: *`, the wildcard value, is returned. `AllowAnyOrigin` allows any origin.

If the response doesn't include the `Access-Control-Allow-Origin` header, the cross-origin request fails. Specifically, the browser disallows the request. Even if the server returns a successful response, the browser doesn't make the response available to the client app.

[]()

### Display OPTIONS requests

By default, the Chrome and Edge browsers don't show OPTIONS requests on the network tab of the F12 tools. To display OPTIONS requests in these browsers:

* `chrome://flags/#out-of-blink-cors` or `edge://flags/#out-of-blink-cors`
* disable the flag.
* restart.

Firefox shows OPTIONS requests by default.

## CORS in IIS

When deploying to IIS, CORS has to run before Windows Authentication if the server isn't configured to allow anonymous access. To support this scenario, the [IIS CORS module](https://www.iis.net/downloads/microsoft/iis-cors-module) needs to be installed and configured for the app.

[]()

## Test CORS

The [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/3.1/Cors/WebAPI) has code to test CORS. See [how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample). The sample is an API project with Razor Pages added:

```
public class StartupTest2
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy(name: "MyPolicy",
                policy =>
                {
                    policy.WithOrigins("http://example.com",
                        "http://www.contoso.com",
                        "https://cors1.azurewebsites.net",
                        "https://cors3.azurewebsites.net",
                        "https://localhost:44398",
                        "https://localhost:5001")
                            .WithMethods("PUT", "DELETE", "GET");
                });
        });

        services.AddControllers();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapRazorPages();
        });
    }
}
```

Warning

`WithOrigins("https://localhost:<port>");` should only be used for testing a sample app similar to the [download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/cors/samples/3.1/Cors).

The following `ValuesController` provides the endpoints for testing:

```
[EnableCors("MyPolicy")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public IActionResult Get() =>
        ControllerContext.MyDisplayRouteInfo();

    // GET api/values/5
    [HttpGet("{id}")]
    public IActionResult Get(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // PUT api/values/5
    [HttpPut("{id}")]
    public IActionResult Put(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);


    // GET: api/values/GetValues2
    [DisableCors]
    [HttpGet("{action}")]
    public IActionResult GetValues2() =>
        ControllerContext.MyDisplayRouteInfo();

}
```

[MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.

Test the preceding sample code by using one of the following approaches:

* Run the sample with `dotnet run` using the default URL of `https://localhost:5001`.
* Run the sample from Visual Studio with the port set to 44398 for a URL of `https://localhost:44398`.

Using a browser with the F12 tools:

* Select the **Values** button and review the headers in the **Network** tab.

* Select the **PUT test** button. See [Display OPTIONS requests](#options) for instructions on displaying the OPTIONS request. The **PUT test** creates two requests, an OPTIONS preflight request and the PUT request.

* Select the **`GetValues2 [DisableCors]`** button to trigger a failed CORS request. As mentioned in the document, the response returns 200 success, but the CORS request is not made. Select the **Console** tab to see the CORS error. Depending on the browser, an error similar to the following is displayed:

  Access to fetch at `'https://cors1.azurewebsites.net/api/values/GetValues2'` from origin `'https://cors3.azurewebsites.net'` has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

CORS-enabled endpoints can be tested with a tool, such as [curl](https://curl.haxx.se/) or [Fiddler](https://www.telerik.com/fiddler). When using a tool, the origin of the request specified by the `Origin` header must differ from the host receiving the request. If the request isn't *cross-origin* based on the value of the `Origin` header:

* There's no need for CORS middleware to process the request.
* CORS headers aren't returned in the response.

The following command uses `curl` to issue an OPTIONS request with information:

```
curl -X OPTIONS https://cors3.azurewebsites.net/api/TodoItems2/5 -i
```

[]()

### Test CORS with endpoint routing and \[HttpOptions]

Enabling CORS on a per-endpoint basis using `RequireCors` currently does **not** support [automatic preflight requests](#apf). Consider the following code which uses [endpoint routing to enable CORS](#ecors):

```
public class StartupEndPointBugTest
{
    readonly string MyPolicy = "_myPolicy";

    // .WithHeaders(HeaderNames.ContentType, "x-custom-header")
    // forces browsers to require a preflight request with GET

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy(name: MyPolicy,
                policy =>
                {
                    policy.WithOrigins("http://example.com",
                                        "http://www.contoso.com",
                                        "https://cors1.azurewebsites.net",
                                        "https://cors3.azurewebsites.net",
                                        "https://localhost:44398",
                                        "https://localhost:5001")
                           .WithHeaders(HeaderNames.ContentType, "x-custom-header")
                           .WithMethods("PUT", "DELETE", "GET", "OPTIONS");
                });
        });

        services.AddControllers();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseCors();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers().RequireCors(MyPolicy);
            endpoints.MapRazorPages();
        });
    }
}
```

The following `TodoItems1Controller` provides endpoints for testing:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems1Controller : ControllerBase
{
    // PUT: api/TodoItems1/5
    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return Content($"ID = {id}");
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // Delete: api/TodoItems1/5
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    // GET: api/TodoItems1
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors]
    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    // Delete: api/TodoItems1/MyDelete2/5
    [EnableCors]
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}
```

Test the preceding code from the test page (`https://cors1.azurewebsites.net/test?number=1`) of the deployed [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/3.1/Cors/WebAPI).

The **Delete \[EnableCors]** and **GET \[EnableCors]** buttons succeed, because the endpoints have `[EnableCors]` and respond to preflight requests. The other endpoints fails. The **GET** button fails, because the [JavaScript](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/cors/samples/3.1/Cors/WebAPI/wwwroot/js/MyJS.js) sends:

```
 headers: {
      "Content-Type": "x-custom-header"
 },
```

The following `TodoItems2Controller` provides similar endpoints, but includes explicit code to respond to OPTIONS requests:

```
[Route("api/[controller]")]
[ApiController]
public class TodoItems2Controller : ControllerBase
{
    // OPTIONS: api/TodoItems2/5
    [HttpOptions("{id}")]
    public IActionResult PreflightRoute(int id)
    {
        return NoContent();
    }

    // OPTIONS: api/TodoItems2 
    [HttpOptions]
    public IActionResult PreflightRoute()
    {
        return NoContent();
    }

    [HttpPut("{id}")]
    public IActionResult PutTodoItem(int id)
    {
        if (id < 1)
        {
            return BadRequest();
        }

        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // [EnableCors] // Not needed as OPTIONS path provided
    [HttpDelete("{id}")]
    public IActionResult MyDelete(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);

    [EnableCors]  // Rquired for this path
    [HttpGet]
    public IActionResult GetTodoItems() =>
        ControllerContext.MyDisplayRouteInfo();

    [HttpGet("{action}")]
    public IActionResult GetTodoItems2() =>
        ControllerContext.MyDisplayRouteInfo();

    [EnableCors]  // Rquired for this path
    [HttpDelete("{action}/{id}")]
    public IActionResult MyDelete2(int id) =>
        ControllerContext.MyDisplayRouteInfo(id);
}
```

The preceding code can be tested by deploying the sample to Azure.In the **Controller** drop down list, select **Preflight** and then **Set Controller**. All the CORS calls to the `TodoItems2Controller` endpoints succeed.

## Additional resources

* [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/docs/Web/HTTP/CORS)
* [IIS CORS module Configuration Reference](/en-us/iis/extensions/cors-module/cors-module-configuration-reference)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/change-tokens
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/change-tokens.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Detect changes with change tokens in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

A *change token* is a general-purpose, low-level building block used to track state changes.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/change-tokens/samples/) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## IChangeToken interface

[IChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken) propagates notifications that a change has occurred. `IChangeToken` resides in the [Microsoft.Extensions.Primitives](/en-us/dotnet/api/microsoft.extensions.primitives) namespace. The [Microsoft.Extensions.Primitives](https://www.nuget.org/packages/Microsoft.Extensions.Primitives/) NuGet package is implicitly provided to the ASP.NET Core apps.

`IChangeToken` has two properties:

* [ActiveChangeCallbacks](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.activechangecallbacks#microsoft-extensions-primitives-ichangetoken-activechangecallbacks) indicate if the token proactively raises callbacks. If `ActiveChangedCallbacks` is set to `false`, a callback is never called, and the app must poll `HasChanged` for changes. It's also possible for a token to never be cancelled if no changes occur or the underlying change listener is disposed or disabled.
* [HasChanged](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.haschanged#microsoft-extensions-primitives-ichangetoken-haschanged) receives a value that indicates if a change has occurred.

The `IChangeToken` interface includes the [RegisterChangeCallback(Action\<Object>, Object)](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.registerchangecallback) method, which registers a callback that's invoked when the token has changed. `HasChanged` must be set before the callback is invoked.

## ChangeToken class

[ChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.changetoken) is a static class used to propagate notifications that a change has occurred. `ChangeToken` resides in the [Microsoft.Extensions.Primitives](/en-us/dotnet/api/microsoft.extensions.primitives) namespace. The [Microsoft.Extensions.Primitives](https://www.nuget.org/packages/Microsoft.Extensions.Primitives/) NuGet package is implicitly provided to the ASP.NET Core apps.

The [ChangeToken.OnChange(Func\<IChangeToken>, Action)](/en-us/dotnet/api/microsoft.extensions.primitives.changetoken.onchange) method registers an `Action` to call whenever the token changes:

* `Func<IChangeToken>` produces the token.
* `Action` is called when the token changes.

The [ChangeToken.OnChange\<TState>(Func\<IChangeToken>, Action\<TState>, TState)](/en-us/dotnet/api/microsoft.extensions.primitives.changetoken.onchange) overload takes an additional `TState` parameter that's passed into the token consumer `Action`.

`OnChange` returns an [IDisposable](/en-us/dotnet/api/system.idisposable). Calling [Dispose](/en-us/dotnet/api/system.idisposable.dispose) stops the token from listening for further changes and releases the token's resources.

## Example uses of change tokens in ASP.NET Core

Change tokens are used in prominent areas of ASP.NET Core to monitor for changes to objects:

* For monitoring changes to files, [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider)'s [Watch](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider.watch) method creates an `IChangeToken` for the specified files or folder to watch.
* `IChangeToken` tokens can be added to cache entries to trigger cache evictions on change.
* For `TOptions` changes, the default [OptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.optionsmonitor-1) implementation of [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) has an overload that accepts one or more [IOptionsChangeTokenSource\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionschangetokensource-1) instances. Each instance returns an `IChangeToken` to register a change notification callback for tracking options changes.

## Monitor for configuration changes

By default, ASP.NET Core templates use [JSON configuration files](configuration/?view=aspnetcore-10.0#json-configuration-provider) (`appsettings.json`, `appsettings.Development.json`, and `appsettings.Production.json`) to load app configuration settings.

These files are configured using the [AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean)](/en-us/dotnet/api/microsoft.extensions.configuration.jsonconfigurationextensions.addjsonfile) extension method on [ConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbuilder) that accepts a `reloadOnChange` parameter. `reloadOnChange` indicates if configuration should be reloaded on file changes. This setting appears in the [Host](/en-us/dotnet/api/microsoft.extensions.hosting.host) convenience method [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder):

```
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, 
          reloadOnChange: true);
```

File-based configuration is represented by [FileConfigurationSource](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationsource). `FileConfigurationSource` uses [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider) to monitor files.

By default, the `IFileMonitor` is provided by a [PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider), which uses [FileSystemWatcher](/en-us/dotnet/api/system.io.filesystemwatcher) to monitor for configuration file changes.

The sample app demonstrates two implementations for monitoring configuration changes. If any of the `appsettings` files change, both of the file monitoring implementations execute custom code—the sample app writes a message to the console.

A configuration file's `FileSystemWatcher` can trigger multiple token callbacks for a single configuration file change. To ensure that the custom code is only run once when multiple token callbacks are triggered, the sample's implementation checks file hashes. The sample uses SHA1 file hashing. A retry is implemented with an exponential back-off.

`Utilities/Utilities.cs`:

```
public static byte[] ComputeHash(string filePath)
{
    var runCount = 1;

    while(runCount < 4)
    {
        try
        {
            if (File.Exists(filePath))
            {
                using (var fs = File.OpenRead(filePath))
                {
                    return System.Security.Cryptography.SHA1
                        .Create().ComputeHash(fs);
                }
            }
            else 
            {
                throw new FileNotFoundException();
            }
        }
        catch (IOException ex)
        {
            if (runCount == 3)
            {
                throw;
            }

            Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
            runCount++;
        }
    }

    return new byte[20];
}
```

### Simple startup change token

Register a token consumer `Action` callback for change notifications to the configuration reload token.

In `Startup.Configure`:

```
ChangeToken.OnChange(
    () => config.GetReloadToken(),
    (state) => InvokeChanged(state),
    env);
```

`config.GetReloadToken()` provides the token. The callback is the `InvokeChanged` method:

```
private void InvokeChanged(IWebHostEnvironment env)
{
    byte[] appsettingsHash = ComputeHash("appSettings.json");
    byte[] appsettingsEnvHash = 
        ComputeHash($"appSettings.{env.EnvironmentName}.json");

    if (!_appsettingsHash.SequenceEqual(appsettingsHash) || 
        !_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
    {
        _appsettingsHash = appsettingsHash;
        _appsettingsEnvHash = appsettingsEnvHash;

        WriteConsole("Configuration changed (Simple Startup Change Token)");
    }
}
```

The `state` of the callback is used to pass in the `IWebHostEnvironment`, which is useful for specifying the correct `appsettings` configuration file to monitor (for example, `appsettings.Development.json` when in the `Development` environment). File hashes are used to prevent the `WriteConsole` statement from running multiple times due to multiple token callbacks when the configuration file has only changed once.

This system runs as long as the app is running and can't be disabled by the user.

### Monitor configuration changes as a service

The sample implements:

* Basic startup token monitoring.
* Monitoring as a service.
* A mechanism to enable and disable monitoring.

The sample establishes an `IConfigurationMonitor` interface.

`Extensions/ConfigurationMonitor.cs`:

```
public interface IConfigurationMonitor
{
    bool MonitoringEnabled { get; set; }
    string CurrentState { get; set; }
}
```

The constructor of the implemented class, `ConfigurationMonitor`, registers a callback for change notifications:

```
public ConfigurationMonitor(IConfiguration config, IWebHostEnvironment env)
{
    _env = env;

    ChangeToken.OnChange<IConfigurationMonitor>(
        () => config.GetReloadToken(),
        InvokeChanged,
        this);
}

public bool MonitoringEnabled { get; set; } = false;
public string CurrentState { get; set; } = "Not monitoring";
```

`config.GetReloadToken()` supplies the token. `InvokeChanged` is the callback method. The `state` in this instance is a reference to the `IConfigurationMonitor` instance that's used to access the monitoring state. Two properties are used:

* `MonitoringEnabled`: Indicates if the callback should run its custom code.
* `CurrentState`: Describes the current monitoring state for use in the UI.

The `InvokeChanged` method is similar to the earlier approach, except that it:

* Doesn't run its code unless `MonitoringEnabled` is `true`.
* Outputs the current `state` in its `WriteConsole` output.

```
private void InvokeChanged(IConfigurationMonitor state)
{
    if (MonitoringEnabled)
    {
        byte[] appsettingsHash = ComputeHash("appSettings.json");
        byte[] appsettingsEnvHash = 
            ComputeHash($"appSettings.{_env.EnvironmentName}.json");

        if (!_appsettingsHash.SequenceEqual(appsettingsHash) || 
            !_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
        {
            string message = $"State updated at {DateTime.Now}";
          

            _appsettingsHash = appsettingsHash;
            _appsettingsEnvHash = appsettingsEnvHash;

            WriteConsole("Configuration changed (ConfigurationMonitor Class) " +
                $"{message}, state:{state.CurrentState}");
        }
    }
}
```

An instance `ConfigurationMonitor` is registered as a service in `Startup.ConfigureServices`:

```
services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();
```

The Index page offers the user control over configuration monitoring. The instance of `IConfigurationMonitor` is injected into the `IndexModel`.

`Pages/Index.cshtml.cs`:

```
public IndexModel(
    IConfiguration config, 
    IConfigurationMonitor monitor, 
    FileService fileService)
{
    _config = config;
    _monitor = monitor;
    _fileService = fileService;
}
```

The configuration monitor (`_monitor`) is used to enable or disable monitoring and set the current state for UI feedback:

```
public IActionResult OnPostStartMonitoring()
{
    _monitor.MonitoringEnabled = true;
    _monitor.CurrentState = "Monitoring!";

    return RedirectToPage();
}

public IActionResult OnPostStopMonitoring()
{
    _monitor.MonitoringEnabled = false;
    _monitor.CurrentState = "Not monitoring";

    return RedirectToPage();
}
```

When `OnPostStartMonitoring` is triggered, monitoring is enabled, and the current state is cleared. When `OnPostStopMonitoring` is triggered, monitoring is disabled, and the state is set to reflect that monitoring isn't occurring.

Buttons in the UI enable and disable monitoring.

`Pages/Index.cshtml`:

```
<button class="btn btn-success" asp-page-handler="StartMonitoring">
    Start Monitoring
</button>

<button class="btn btn-danger" asp-page-handler="StopMonitoring">
    Stop Monitoring
</button>
```

## Monitor cached file changes

File content can be cached in-memory using [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache). In-memory caching is described in the [Cache in-memory](../performance/caching/memory?view=aspnetcore-10.0) topic. Without taking additional steps, such as the implementation described below, *stale* (outdated) data is returned from a cache if the source data changes.

For example, not taking into account the status of a cached source file when renewing a [sliding expiration](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryoptions.slidingexpiration#microsoft-extensions-caching-memory-memorycacheentryoptions-slidingexpiration) period leads to stale cached file data. Each request for the data renews the sliding expiration period, but the file is never reloaded into the cache. Any app features that use the file's cached content are subject to possibly receiving stale content.

Using change tokens in a file caching scenario prevents the presence of stale file content in the cache. The sample app demonstrates an implementation of the approach.

The sample uses `GetFileContent` to:

* Return file content.
* Implement a retry algorithm with exponential back-off to cover cases where a file access problem temporarily delays reading the file's content.

`Utilities/Utilities.cs`:

```
public async static Task<string> GetFileContent(string filePath)
{
    var runCount = 1;

    while(runCount < 4)
    {
        try
        {
            if (File.Exists(filePath))
            {
                using (var fileStreamReader = File.OpenText(filePath))
                {
                    return await fileStreamReader.ReadToEndAsync();
                }
            }
            else 
            {
                throw new FileNotFoundException();
            }
        }
        catch (IOException ex)
        {
            if (runCount == 3)
            {
                throw;
            }

            Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
            runCount++;
        }
    }

    return null;
}
```

A `FileService` is created to handle cached file lookups. The `GetFileContent` method call of the service attempts to obtain file content from the in-memory cache and return it to the caller (`Services/FileService.cs`).

If cached content isn't found using the cache key, the following actions are taken:

1. The file content is obtained using `GetFileContent`.
2. A change token is obtained from the file provider with [IFileProviders.Watch](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider.watch). The token's callback is triggered when the file is modified.
3. The file content is cached with a [sliding expiration](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryoptions.slidingexpiration#microsoft-extensions-caching-memory-memorycacheentryoptions-slidingexpiration) period. The change token is attached with [MemoryCacheEntryExtensions.AddExpirationToken](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryextensions.addexpirationtoken) to evict the cache entry if the file changes while it's cached.

In the following example, files are stored in the app's [content root](./?view=aspnetcore-10.0#content-root). `IWebHostEnvironment.ContentRootFileProvider` is used to obtain an [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider) pointing at the app's `IWebHostEnvironment.ContentRootPath`. The `filePath` is obtained with [IFileInfo.PhysicalPath](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.physicalpath#microsoft-extensions-fileproviders-ifileinfo-physicalpath).

```
public class FileService
{
    private readonly IMemoryCache _cache;
    private readonly IFileProvider _fileProvider;
    private List<string> _tokens = new List<string>();

    public FileService(IMemoryCache cache, IWebHostEnvironment env)
    {
        _cache = cache;
        _fileProvider = env.ContentRootFileProvider;
    }

    public async Task<string> GetFileContents(string fileName)
    {
        var filePath = _fileProvider.GetFileInfo(fileName).PhysicalPath;
        string fileContent;

        // Try to obtain the file contents from the cache.
        if (_cache.TryGetValue(filePath, out fileContent))
        {
            return fileContent;
        }

        // The cache doesn't have the entry, so obtain the file 
        // contents from the file itself.
        fileContent = await GetFileContent(filePath);

        if (fileContent != null)
        {
            // Obtain a change token from the file provider whose
            // callback is triggered when the file is modified.
            var changeToken = _fileProvider.Watch(fileName);

            // Configure the cache entry options for a five minute
            // sliding expiration and use the change token to
            // expire the file in the cache if the file is
            // modified.
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(5))
                .AddExpirationToken(changeToken);

            // Put the file contents into the cache.
            _cache.Set(filePath, fileContent, cacheEntryOptions);

            return fileContent;
        }

        return string.Empty;
    }
}
```

The `FileService` is registered in the service container along with the memory caching service.

In `Startup.ConfigureServices`:

```
services.AddMemoryCache();
services.AddSingleton<FileService>();
```

The page model loads the file's content using the service.

In the Index page's `OnGet` method (`Pages/Index.cshtml.cs`):

```
var fileContent = await _fileService.GetFileContents("poem.txt");
```

## CompositeChangeToken class

For representing one or more `IChangeToken` instances in a single object, use the [CompositeChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.compositechangetoken) class.

```
var firstCancellationTokenSource = new CancellationTokenSource();
var secondCancellationTokenSource = new CancellationTokenSource();

var firstCancellationToken = firstCancellationTokenSource.Token;
var secondCancellationToken = secondCancellationTokenSource.Token;

var firstCancellationChangeToken = new CancellationChangeToken(firstCancellationToken);
var secondCancellationChangeToken = new CancellationChangeToken(secondCancellationToken);

var compositeChangeToken = 
    new CompositeChangeToken(
        new List<IChangeToken> 
        {
            firstCancellationChangeToken, 
            secondCancellationChangeToken
        });
```

`HasChanged` on the composite token reports `true` if any represented token `HasChanged` is `true`. `ActiveChangeCallbacks` on the composite token reports `true` if any represented token `ActiveChangeCallbacks` is `true`. If multiple concurrent change events occur, the composite change callback is invoked one time.

A *change token* is a general-purpose, low-level building block used to track state changes.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/change-tokens/samples/) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## IChangeToken interface

[IChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken) propagates notifications that a change has occurred. `IChangeToken` resides in the [Microsoft.Extensions.Primitives](/en-us/dotnet/api/microsoft.extensions.primitives) namespace. For apps that don't use the [Microsoft.AspNetCore.App metapackage](metapackage-app?view=aspnetcore-10.0), create a package reference for the [Microsoft.Extensions.Primitives](https://www.nuget.org/packages/Microsoft.Extensions.Primitives/) NuGet package.

`IChangeToken` has two properties:

* [ActiveChangeCallbacks](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.activechangecallbacks#microsoft-extensions-primitives-ichangetoken-activechangecallbacks) indicate if the token proactively raises callbacks. If `ActiveChangedCallbacks` is set to `false`, a callback is never called, and the app must poll `HasChanged` for changes. It's also possible for a token to never be cancelled if no changes occur or the underlying change listener is disposed or disabled.
* [HasChanged](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.haschanged#microsoft-extensions-primitives-ichangetoken-haschanged) receives a value that indicates if a change has occurred.

The `IChangeToken` interface includes the [RegisterChangeCallback(Action\<Object>, Object)](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.registerchangecallback) method, which registers a callback that's invoked when the token has changed. `HasChanged` must be set before the callback is invoked.

## ChangeToken class

[ChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.changetoken) is a static class used to propagate notifications that a change has occurred. `ChangeToken` resides in the [Microsoft.Extensions.Primitives](/en-us/dotnet/api/microsoft.extensions.primitives) namespace. For apps that don't use the [Microsoft.AspNetCore.App metapackage](metapackage-app?view=aspnetcore-10.0), create a package reference for the [Microsoft.Extensions.Primitives](https://www.nuget.org/packages/Microsoft.Extensions.Primitives/) NuGet package.

The [ChangeToken.OnChange(Func\<IChangeToken>, Action)](/en-us/dotnet/api/microsoft.extensions.primitives.changetoken.onchange) method registers an `Action` to call whenever the token changes:

* `Func<IChangeToken>` produces the token.
* `Action` is called when the token changes.

The [ChangeToken.OnChange\<TState>(Func\<IChangeToken>, Action\<TState>, TState)](/en-us/dotnet/api/microsoft.extensions.primitives.changetoken.onchange) overload takes an additional `TState` parameter that's passed into the token consumer `Action`.

`OnChange` returns an [IDisposable](/en-us/dotnet/api/system.idisposable). Calling [Dispose](/en-us/dotnet/api/system.idisposable.dispose) stops the token from listening for further changes and releases the token's resources.

## Example uses of change tokens in ASP.NET Core

Change tokens are used in prominent areas of ASP.NET Core to monitor for changes to objects:

* For monitoring changes to files, [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider)'s [Watch](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider.watch) method creates an `IChangeToken` for the specified files or folder to watch.
* `IChangeToken` tokens can be added to cache entries to trigger cache evictions on change.
* For `TOptions` changes, the default [OptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.optionsmonitor-1) implementation of [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) has an overload that accepts one or more [IOptionsChangeTokenSource\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionschangetokensource-1) instances. Each instance returns an `IChangeToken` to register a change notification callback for tracking options changes.

## Monitor for configuration changes

By default, ASP.NET Core templates use [JSON configuration files](configuration/?view=aspnetcore-10.0#json-configuration-provider) (`appsettings.json`, `appsettings.Development.json`, and `appsettings.Production.json`) to load app configuration settings.

These files are configured using the [AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean)](/en-us/dotnet/api/microsoft.extensions.configuration.jsonconfigurationextensions.addjsonfile) extension method on [ConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbuilder) that accepts a `reloadOnChange` parameter. `reloadOnChange` indicates if configuration should be reloaded on file changes. This setting appears in the [WebHost](/en-us/dotnet/api/microsoft.aspnetcore.webhost) convenience method [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder):

```
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, 
          reloadOnChange: true);
```

File-based configuration is represented by [FileConfigurationSource](/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationsource). `FileConfigurationSource` uses [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider) to monitor files.

By default, the `IFileMonitor` is provided by a [PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider), which uses [FileSystemWatcher](/en-us/dotnet/api/system.io.filesystemwatcher) to monitor for configuration file changes.

The sample app demonstrates two implementations for monitoring configuration changes. If any of the `appsettings` files change, both of the file monitoring implementations execute custom code—the sample app writes a message to the console.

A configuration file's `FileSystemWatcher` can trigger multiple token callbacks for a single configuration file change. To ensure that the custom code is only run once when multiple token callbacks are triggered, the sample's implementation checks file hashes. The sample uses SHA1 file hashing. A retry is implemented with an exponential back-off.

`Utilities/Utilities.cs`:

```
public static byte[] ComputeHash(string filePath)
{
    var runCount = 1;

    while(runCount < 4)
    {
        try
        {
            if (File.Exists(filePath))
            {
                using (var fs = File.OpenRead(filePath))
                {
                    return System.Security.Cryptography.SHA1
                        .Create().ComputeHash(fs);
                }
            }
            else 
            {
                throw new FileNotFoundException();
            }
        }
        catch (IOException ex)
        {
            if (runCount == 3)
            {
                throw;
            }

            Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
            runCount++;
        }
    }

    return new byte[20];
}
```

### Simple startup change token

Register a token consumer `Action` callback for change notifications to the configuration reload token.

In `Startup.Configure`:

```
ChangeToken.OnChange(
    () => config.GetReloadToken(),
    (state) => InvokeChanged(state),
    env);
```

`config.GetReloadToken()` provides the token. The callback is the `InvokeChanged` method:

```
private void InvokeChanged(IHostingEnvironment env)
{
    byte[] appsettingsHash = ComputeHash("appSettings.json");
    byte[] appsettingsEnvHash = 
        ComputeHash($"appSettings.{env.EnvironmentName}.json");

    if (!_appsettingsHash.SequenceEqual(appsettingsHash) || 
        !_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
    {
        _appsettingsHash = appsettingsHash;
        _appsettingsEnvHash = appsettingsEnvHash;

        WriteConsole("Configuration changed (Simple Startup Change Token)");
    }
}
```

The `state` of the callback is used to pass in the `IHostingEnvironment`, which is useful for specifying the correct `appsettings` configuration file to monitor (for example, `appsettings.Development.json` when in the `Development` environment). File hashes are used to prevent the `WriteConsole` statement from running multiple times due to multiple token callbacks when the configuration file has only changed once.

This system runs as long as the app is running and can't be disabled by the user.

### Monitor configuration changes as a service

The sample implements:

* Basic startup token monitoring.
* Monitoring as a service.
* A mechanism to enable and disable monitoring.

The sample establishes an `IConfigurationMonitor` interface.

`Extensions/ConfigurationMonitor.cs`:

```
public interface IConfigurationMonitor
{
    bool MonitoringEnabled { get; set; }
    string CurrentState { get; set; }
}
```

The constructor of the implemented class, `ConfigurationMonitor`, registers a callback for change notifications:

```
public ConfigurationMonitor(IConfiguration config, IHostingEnvironment env)
{
    _env = env;

    ChangeToken.OnChange<IConfigurationMonitor>(
        () => config.GetReloadToken(),
        InvokeChanged,
        this);
}

public bool MonitoringEnabled { get; set; } = false;
public string CurrentState { get; set; } = "Not monitoring";
```

`config.GetReloadToken()` supplies the token. `InvokeChanged` is the callback method. The `state` in this instance is a reference to the `IConfigurationMonitor` instance that's used to access the monitoring state. Two properties are used:

* `MonitoringEnabled`: Indicates if the callback should run its custom code.
* `CurrentState`: Describes the current monitoring state for use in the UI.

The `InvokeChanged` method is similar to the earlier approach, except that it:

* Doesn't run its code unless `MonitoringEnabled` is `true`.
* Outputs the current `state` in its `WriteConsole` output.

```
private void InvokeChanged(IConfigurationMonitor state)
{
    if (MonitoringEnabled)
    {
        byte[] appsettingsHash = ComputeHash("appSettings.json");
        byte[] appsettingsEnvHash = 
            ComputeHash($"appSettings.{_env.EnvironmentName}.json");

        if (!_appsettingsHash.SequenceEqual(appsettingsHash) || 
            !_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
        {
            string message = $"State updated at {DateTime.Now}";
          

            _appsettingsHash = appsettingsHash;
            _appsettingsEnvHash = appsettingsEnvHash;

            WriteConsole("Configuration changed (ConfigurationMonitor Class) " +
                $"{message}, state:{state.CurrentState}");
        }
    }
}
```

An instance `ConfigurationMonitor` is registered as a service in `Startup.ConfigureServices`:

```
services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();
```

The Index page offers the user control over configuration monitoring. The instance of `IConfigurationMonitor` is injected into the `IndexModel`.

`Pages/Index.cshtml.cs`:

```
public IndexModel(
    IConfiguration config, 
    IConfigurationMonitor monitor, 
    FileService fileService)
{
    _config = config;
    _monitor = monitor;
    _fileService = fileService;
}
```

The configuration monitor (`_monitor`) is used to enable or disable monitoring and set the current state for UI feedback:

```
public IActionResult OnPostStartMonitoring()
{
    _monitor.MonitoringEnabled = true;
    _monitor.CurrentState = "Monitoring!";

    return RedirectToPage();
}

public IActionResult OnPostStopMonitoring()
{
    _monitor.MonitoringEnabled = false;
    _monitor.CurrentState = "Not monitoring";

    return RedirectToPage();
}
```

When `OnPostStartMonitoring` is triggered, monitoring is enabled, and the current state is cleared. When `OnPostStopMonitoring` is triggered, monitoring is disabled, and the state is set to reflect that monitoring isn't occurring.

Buttons in the UI enable and disable monitoring.

`Pages/Index.cshtml`:

```
<button class="btn btn-success" asp-page-handler="StartMonitoring">
    Start Monitoring
</button>

<button class="btn btn-danger" asp-page-handler="StopMonitoring">
    Stop Monitoring
</button>
```

## Monitor cached file changes

File content can be cached in-memory using [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache). In-memory caching is described in the [Cache in-memory](../performance/caching/memory?view=aspnetcore-10.0) topic. Without taking additional steps, such as the implementation described below, *stale* (outdated) data is returned from a cache if the source data changes.

For example, not taking into account the status of a cached source file when renewing a [sliding expiration](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryoptions.slidingexpiration#microsoft-extensions-caching-memory-memorycacheentryoptions-slidingexpiration) period leads to stale cached file data. Each request for the data renews the sliding expiration period, but the file is never reloaded into the cache. Any app features that use the file's cached content are subject to possibly receiving stale content.

Using change tokens in a file caching scenario prevents the presence of stale file content in the cache. The sample app demonstrates an implementation of the approach.

The sample uses `GetFileContent` to:

* Return file content.
* Implement a retry algorithm with exponential back-off to cover cases where a file access problem temporarily delays reading the file's content.

`Utilities/Utilities.cs`:

```
public async static Task<string> GetFileContent(string filePath)
{
    var runCount = 1;

    while(runCount < 4)
    {
        try
        {
            if (File.Exists(filePath))
            {
                using (var fileStreamReader = File.OpenText(filePath))
                {
                    return await fileStreamReader.ReadToEndAsync();
                }
            }
            else 
            {
                throw new FileNotFoundException();
            }
        }
        catch (IOException ex)
        {
            if (runCount == 3 || ex.HResult != -2147024864)
            {
                throw;
            }
            else
            {
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
                runCount++;
            }
        }
    }

    return null;
}
```

A `FileService` is created to handle cached file lookups. The `GetFileContent` method call of the service attempts to obtain file content from the in-memory cache and return it to the caller (`Services/FileService.cs`).

If cached content isn't found using the cache key, the following actions are taken:

1. The file content is obtained using `GetFileContent`.
2. A change token is obtained from the file provider with [IFileProviders.Watch](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider.watch). The token's callback is triggered when the file is modified.
3. The file content is cached with a [sliding expiration](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryoptions.slidingexpiration#microsoft-extensions-caching-memory-memorycacheentryoptions-slidingexpiration) period. The change token is attached with [MemoryCacheEntryExtensions.AddExpirationToken](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheentryextensions.addexpirationtoken) to evict the cache entry if the file changes while it's cached.

In the following example, files are stored in the app's [content root](./?view=aspnetcore-10.0#content-root). [IHostingEnvironment.ContentRootFileProvider](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingenvironment.contentrootfileprovider#microsoft-aspnetcore-hosting-ihostingenvironment-contentrootfileprovider) is used to obtain an [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider) pointing at the app's [ContentRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingenvironment.contentrootpath#microsoft-aspnetcore-hosting-ihostingenvironment-contentrootpath). The `filePath` is obtained with [IFileInfo.PhysicalPath](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.physicalpath#microsoft-extensions-fileproviders-ifileinfo-physicalpath).

```
public class FileService
{
    private readonly IMemoryCache _cache;
    private readonly IFileProvider _fileProvider;
    private List<string> _tokens = new List<string>();

    public FileService(IMemoryCache cache, IHostingEnvironment env)
    {
        _cache = cache;
        _fileProvider = env.ContentRootFileProvider;
    }

    public async Task<string> GetFileContents(string fileName)
    {
        var filePath = _fileProvider.GetFileInfo(fileName).PhysicalPath;
        string fileContent;

        // Try to obtain the file contents from the cache.
        if (_cache.TryGetValue(filePath, out fileContent))
        {
            return fileContent;
        }

        // The cache doesn't have the entry, so obtain the file 
        // contents from the file itself.
        fileContent = await GetFileContent(filePath);

        if (fileContent != null)
        {
            // Obtain a change token from the file provider whose
            // callback is triggered when the file is modified.
            var changeToken = _fileProvider.Watch(fileName);

            // Configure the cache entry options for a five minute
            // sliding expiration and use the change token to
            // expire the file in the cache if the file is
            // modified.
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(5))
                .AddExpirationToken(changeToken);

            // Put the file contents into the cache.
            _cache.Set(filePath, fileContent, cacheEntryOptions);

            return fileContent;
        }

        return string.Empty;
    }
}
```

The `FileService` is registered in the service container along with the memory caching service.

In `Startup.ConfigureServices`:

```
services.AddMemoryCache();
services.AddSingleton<FileService>();
```

The page model loads the file's content using the service.

In the Index page's `OnGet` method (`Pages/Index.cshtml.cs`):

```
var fileContent = await _fileService.GetFileContents("poem.txt");
```

## CompositeChangeToken class

For representing one or more `IChangeToken` instances in a single object, use the [CompositeChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.compositechangetoken) class.

```
var firstCancellationTokenSource = new CancellationTokenSource();
var secondCancellationTokenSource = new CancellationTokenSource();

var firstCancellationToken = firstCancellationTokenSource.Token;
var secondCancellationToken = secondCancellationTokenSource.Token;

var firstCancellationChangeToken = new CancellationChangeToken(firstCancellationToken);
var secondCancellationChangeToken = new CancellationChangeToken(secondCancellationToken);

var compositeChangeToken = 
    new CompositeChangeToken(
        new List<IChangeToken> 
        {
            firstCancellationChangeToken, 
            secondCancellationChangeToken
        });
```

`HasChanged` on the composite token reports `true` if any represented token `HasChanged` is `true`. `ActiveChangeCallbacks` on the composite token reports `true` if any represented token `ActiveChangeCallbacks` is `true`. If multiple concurrent change events occur, the composite change callback is invoked one time.

## Related content

* [Cache in-memory in ASP.NET Core](../performance/caching/memory?view=aspnetcore-10.0)
* [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0)
* [Response caching in ASP.NET Core](../performance/caching/response?view=aspnetcore-10.0)
* [Response caching middleware in ASP.NET Core](../performance/caching/middleware?view=aspnetcore-10.0)
* [Cache Tag Helper in ASP.NET Core MVC](../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0)
* [Distributed Cache Tag Helper in ASP.NET Core](../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-16

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/httpcontext
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/httpcontext.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# `IHttpContextAccessor`/`HttpContext` in ASP.NET Core Blazor apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) generally should be avoided with interactive rendering because a valid [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) isn't always available.

[IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) can be used during static server-side rendering (static SSR), for example in statically-rendered root components, and when [using a token handler for web API calls](../security/additional-scenarios?view=aspnetcore-10.0#use-a-token-handler-for-web-api-calls) on the server. **We recommend avoiding [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) when static SSR or code running on the server can't be guaranteed.**

[HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) can be used as a [cascading parameter](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingparameterattribute) only in statically-rendered root components or during static SSR for general tasks, such as inspecting and modifying headers or other properties in the `App` component (`App.razor`). The value is `null` during interactive rendering.

```
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
```

For additional context in *advanced* edge cases†, see the discussion in the following articles:

* [HttpContext is valid in Interactive Server Rendering Blazor page (`dotnet/AspNetCore.Docs` #34301)](https://github.com/dotnet/AspNetCore.Docs/issues/34301)
* [Security implications of using IHttpContextAccessor in Blazor Server (`dotnet/aspnetcore` #45699)](https://github.com/dotnet/aspnetcore/issues/45699)

†Most developers building and maintaining Blazor apps don't need to delve into advanced concepts when the general guidance in this article is followed. The most important concept to keep in mind is that [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is fundamentally a server-based, request-response feature that's only generally available on the server during static SSR and only created when a user's circuit is established.

## Don't set or modify headers after the response starts

Attempting to set or modify a header after the first rendering (after the response starts) results in an error:

> System.InvalidOperationException: 'Headers are read-only, response has already started.'

Examples of situations that result in this error include:

* Calling [SignInManager\<TUser>.PasswordSignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.passwordsigninasync), which must set headers for Identity to function correctly, while adopting [streaming rendering](rendering?view=aspnetcore-10.0#streaming-rendering).
* Attempting to set or modify a header after the response has started during interactive rendering.

For guidance on setting headers before the response starts, see [ASP.NET Core Blazor startup](../fundamentals/startup?view=aspnetcore-10.0#control-headers-in-c-code).

**Don't use [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor)/[HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) directly or indirectly in the Razor components of server-side Blazor apps.** Blazor apps run outside of the ASP.NET Core pipeline context. The [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) isn't guaranteed to be available within the [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor), and [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) isn't guaranteed to hold the context that started the Blazor app.

The recommended approach for passing request state to the Blazor app is through root component parameters during the app's initial rendering. Alternatively, the app can copy the data into a scoped service in the root component's initialization lifecycle event for use across the app. For more information, see [ASP.NET Core Blazor additional server-side security scenarios](../security/additional-scenarios?view=aspnetcore-10.0#pass-tokens-to-a-server-side-blazor-app).

A critical aspect of server-side Blazor security is that the user attached to a given circuit might become updated at some point after the Blazor circuit is established but the [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) ***isn't updated***. For more information on addressing this situation with custom services, see [ASP.NET Core Blazor additional server-side security scenarios](../security/additional-scenarios?view=aspnetcore-10.0#circuit-handler-to-capture-users-for-custom-services).

For guidance on [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) and [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) in ASP.NET Core SignalR, see [IHttpContextAccessor/HttpContext in ASP.NET Core SignalR](../../signalr/httpcontext?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/config-filters
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/config-filters.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP Configuration Filters

## Introduction

YARP configuration for routes, clusters, and destinations can be loaded from [configuration files](config-files?view=aspnetcore-10.0) or from [configuration providers](config-providers?view=aspnetcore-10.0). Configuration filters can be used to modify that raw input before it's validated and applied.

Filters can be used for a variety of purposes such as:

* Supplementing config fields with data from other sources like the deployment environment
* Applying system defaults
* Applying common settings and enforce policies
* Substituting placeholder values
* Normalization and error correction

## AddConfigFilter

Configuration filters are registered in the Dependency Injection system using the [AddConfigFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.reverseproxyservicecollectionextensions) API. Any number of unique filters can be added and will be applied in the order added.

```
// Load the configuration and register a config filter
services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddConfigFilter<CustomConfigFilter>();
```

## IProxyConfigFilter

Configuration filters are written using the [IProxyConfigFilter](/en-us/dotnet/api/yarp.reverseproxy.configuration.iproxyconfigfilter) interface. Because filters are registered in DI they may define a constructor to inject any dependencies.

Filters are called for each route and cluster each time configuration is loaded or re-loaded. They can choose to return the original input unmodified or a modified copy. The C# 9 [records syntax](/en-us/dotnet/csharp/language-reference/builtin-types/record#nondestructive-mutation) is convenient for making modified copies.

This example fills in destination addresses from environment variables and sets the route's Order field to 1.

```
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Yarp.ReverseProxy.Configuration;

namespace Yarp.Sample;

public class CustomConfigFilter : IProxyConfigFilter
{
    // Matches {{env_var_name}}
    private readonly Regex _exp = new("\\{\\{(\\w+)\\}\\}");

    // This sample looks at the destination addresses and any of the form {{key}} will be modified, looking up the key
    // as an environment variable. This is useful when hosted in Azure etc, as it enables a simple way to replace
    // destination addresses via the management console
    public ValueTask<ClusterConfig> ConfigureClusterAsync(ClusterConfig origCluster, CancellationToken cancel)
    {
        // Each cluster has a dictionary of destinations, which is read-only, so we'll create a new one with our updates 
        var newDests = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase);

        foreach (var d in origCluster.Destinations)
        {
            var origAddress = d.Value.Address;
            if (_exp.IsMatch(origAddress))
            {
                // Get the name of the env variable from the destination and lookup value
                var lookup = _exp.Matches(origAddress)[0].Groups[1].Value;
                var newAddress = Environment.GetEnvironmentVariable(lookup);

                if (string.IsNullOrWhiteSpace(newAddress))
                {
                    throw new ArgumentException($"Configuration Filter Error: Substitution for '{lookup}' in cluster '{d.Key}' not found as an environment variable.");
                }

                // using c# 9 "with" to clone and initialize a new record
                var modifiedDest = d.Value with { Address = newAddress };
                newDests.Add(d.Key, modifiedDest);
            }
            else
            {
                newDests.Add(d.Key, d.Value);
            }
        }

        return new ValueTask<ClusterConfig>(origCluster with { Destinations = newDests });
    }

    public ValueTask<RouteConfig> ConfigureRouteAsync(RouteConfig route, ClusterConfig cluster, CancellationToken cancel)
    {
        // Example: do not let config based routes take priority over code based routes.
        // Lower numbers are higher priority. Code routes default to 0.
        if (route.Order.HasValue && route.Order.Value < 1)
        {
            return new ValueTask<RouteConfig>(route with { Order = 1 });
        }

        return new ValueTask<RouteConfig>(route);
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-23

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/diagnosing-yarp-issues
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/diagnosing-yarp-issues.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP Diagnosing YARP-based proxies

When using a reverse proxy, there is an additional hop from the client to the proxy, and then from the proxy to destination for things to go wrong. This topic should provide some hints and tips for how to debug and diagnose issues when they occur. It assumes that the proxy is already running, and so does not include problems at startup such as configuration errors.

## Logging

The first step to being able to tell what is going on with YARP is to turn on [logging](/en-us/aspnet/core/fundamentals/logging/#configuration). This is a configuration flag so can be changed on the fly. YARP is implemented as a middleware component for ASP.NET Core, so you need to enable logging for both YARP and ASP.NET to get the complete picture of what is going on.

By default ASP.NET will log to the console, and the configuration file can be used to control the level of logging.

```
  //Sets the Logging level for ASP.NET
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      // Uncomment to hide diagnostic messages from runtime and proxy
      // "Microsoft": "Warning",
      // "Yarp" : "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
```

You want logging information from the `Microsoft.AspNetCore.\*` and `Yarp.ReverseProxy.\*` providers. The preceding example emits `Information`-level events from both providers to the console. Changing the level to `Debug` shows additional entries. ASP.NET implements change detection for configuration files, so you can edit the `appsettings.json` file (or `appsettings.development.json` for the `Development` environment) while the project is running and observe changes to the log output.

Note

Settings in the `appsettings.development.json` file override settings in `appsettings.json` when running in the `Development` environment, so make sure that if you are editing `appsettings.json` that the values aren't overridden.

### Understanding log entries

The logging output is directly tied to the way that ASP.NET Core processes requests. It's important to realize that as middleware, YARP is relying on much of the ASP.NET functionality to process the requests, for example the following is for the processing of a request with "Debug" mode enabled:

| Level | Log Message                                                                                                                                                           | Description                                                                                     |
| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| dbug  | Microsoft.AspNetCore.Server.Kestrel.Connections\[39] Connection id "0HMCD0JK7K51U" accepted.                                                                          | Connections are independent of requests, so this is a new connection                            |
| dbug  | Microsoft.AspNetCore.Server.Kestrel.Connections\[1] Connection id "0HMCD0JK7K51U" started.                                                                            |                                                                                                 |
| info  | Microsoft.AspNetCore.Hosting.Diagnostics\[1] Request starting HTTP/1.1 GET http\://localhost:5000/ - -                                                                | This is the incoming request to ASP.NET                                                         |
| dbug  | Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware\[0] Wildcard detected, all requests with hosts will be allowed.                                            | My configuration does not tie endpoints to specific hostnames                                   |
| dbug  | Microsoft.AspNetCore.Routing.Matching.DfaMatcher\[1001] 1 candidate(s) found for the request path '/'                                                                 | This shows what possible matches there are for the route                                        |
| dbug  | Microsoft.AspNetCore.Routing.Matching.DfaMatcher\[1005] Endpoint 'minimumroute' with route pattern '{\*\*catch-all}' is valid for the request path '/'                | The minimum route from YARPs configuration has matched                                          |
| dbug  | Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware\[1] Request matched endpoint 'minimumroute'                                                                    |                                                                                                 |
| info  | Microsoft.AspNetCore.Routing.EndpointMiddleware\[0] Executing endpoint 'minimumroute'                                                                                 |                                                                                                 |
| info  | Yarp.ReverseProxy.Forwarder.HttpForwarder\[9] Proxying to `http://www.example.com/`                                                                                   | YARP is proxying the request to example.com                                                     |
| info  | Microsoft.AspNetCore.Routing.EndpointMiddleware\[1] Executed endpoint 'minimumroute'                                                                                  |                                                                                                 |
| dbug  | Microsoft.AspNetCore.Server.Kestrel.Connections\[9] Connection id "0HMCD0JK7K51U" completed keep alive response.                                                      | The response has finished, but connection can be kept alive.                                    |
| info  | Microsoft.AspNetCore.Hosting.Diagnostics\[2] Request finished HTTP/1.1 GET http\://localhost:5000/ - - - 200 1256 text/html;+charset=utf-8 12.7797ms                  | The response completed with status code 200, responding with 1256 bytes as text/html in \~13ms. |
| dbug  | Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets\[6] Connection id "0HMCD0JK7K51U" received FIN.                                                                 | Diagnostic information about the connection to determine who closed it and how cleanly          |
| dbug  | Microsoft.AspNetCore.Server.Kestrel.Connections\[10] Connection id "0HMCD0JK7K51U" disconnecting.                                                                     |                                                                                                 |
| dbug  | Microsoft.AspNetCore.Server.Kestrel.Connections\[2] Connection id "0HMCD0JK7K51U" stopped.                                                                            |                                                                                                 |
| dbug  | Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets\[7] Connection id "0HMCD0JK7K51U" sending FIN because: "The Socket transport's send loop completed gracefully." |                                                                                                 |

The above gives general information about the request and how it was processed.

### Using ASP.NET request logging

ASP.NET includes a middleware component that can be used to provide more details about the request and response. The `UseHttpLogging` component can be added to the request pipeline, which adds additional entries to the log detailing the incoming and outgoing request headers.

```
app.UseHttpLogging();
// Enable endpoint routing, required for the reverse proxy
app.UseRouting();
// Register the reverse proxy routes
app.MapReverseProxy();
```

For example:

```
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
      Request:
      Protocol: HTTP/1.1
      Method: GET
      Scheme: http
      PathBase:
      Path: /
      Accept: */*
      Host: localhost:5000
      User-Agent: curl/7.55.1
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: text/html; charset=utf-8
      Date: Tue, 12 Oct 2021 23:29:20 GMT
      Server: ECS,(sec/97A5)
      Age: 113258
      Cache-Control: [Redacted]
      ETag: [Redacted]
      Expires: Tue, 19 Oct 2021 23:29:20 GMT
      Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
      Vary: [Redacted]
      Content-Length: 1256
      X-Cache: [Redacted]
```

## Using telemetry events

We recommend reading [Networking telemetry in .NET](/en-us/dotnet/fundamentals/networking/telemetry/overview) as a primer on how to consume networking telemetry in .NET.

The [Metrics sample](https://github.com/microsoft/reverse-proxy/tree/main/samples/ReverseProxy.Metrics.Sample) shows how to listen to events from the different providers that collect telemetry as part of YARP. The most important from a diagnostics perspective are:

* `ForwarderTelemetryConsumer`
* `HttpClientTelemetryConsumer`

To use either of these, create a class implementing a [`Yarp.Telemetry.Consumption` interface](/en-us/dotnet/api/yarp.telemetry.consumption#interfaces), such as [IForwarderTelemetryConsumer](/en-us/dotnet/api/yarp.telemetry.consumption.iforwardertelemetryconsumer):

```
public class ForwarderTelemetry : IForwarderTelemetryConsumer
{
    /// Called before forwarding a request.
    public void OnForwarderStart(DateTime timestamp, string destinationPrefix)
    {
        Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => " +
            $"OnForwarderStart :: Destination prefix: {destinationPrefix}");
    }

    /// Called after forwarding a request.
    public void OnForwarderStop(DateTime timestamp, int statusCode)
    {
        Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => " +
            $"OnForwarderStop :: Status: {statusCode}");
    }

    /// Called before <see cref="OnForwarderStop(DateTime, int)"/> if forwarding the request failed.
    public void OnForwarderFailed(DateTime timestamp, ForwarderError error)
    {
        Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => " +
            $"OnForwarderFailed :: Error: {error.ToString()}");
    }

    /// Called when reaching a given stage of forwarding a request.
    public void OnForwarderStage(DateTime timestamp, ForwarderStage stage)
    {
        Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => " +
            $"OnForwarderStage :: Stage: {stage.ToString()}");
    }

    /// Called periodically while a content transfer is active.
    public void OnContentTransferring(DateTime timestamp, bool isRequest, long contentLength,
        long iops, TimeSpan readTime, TimeSpan writeTime)
    {
        Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => " +
            $"OnContentTransferring :: Is request: {isRequest}, Content length: {contentLength}, " +
            $"IOps: {iops}, Read time: {readTime:s\\.fff}, Write time: {writeTime:s\\.fff}");
    }

    /// Called after transferring the request or response content.
    public void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength,
        long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime)
    {
        Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => " +
            $"OnContentTransferred :: Is request: {isRequest}, Content length: {contentLength}, " +
            $"IOps: {iops}, Read time: {readTime:s\\.fff}, Write time: {writeTime:s\\.fff}");
    }

    /// Called before forwarding a request from `ForwarderMiddleware`, therefore is not called for direct forwarding scenarios.
    public void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId,
        string destinationId)
    {
        Console.WriteLine($"Forwarder Telemetry [{timestamp:HH:mm:ss.fff}] => " +
            $"OnForwarderInvoke:: Cluster id: {clusterId}, Route Id: {routeId}, Destination: {destinationId}");
    }
}
```

Register the class as part of services, for example:

```
services.AddTelemetryConsumer<ForwarderTelemetry>();

// Add the reverse proxy to capability to the server
var proxyBuilder = services.AddReverseProxy();
// Initialize the reverse proxy from the "ReverseProxy" section of configuration
proxyBuilder.LoadFromConfig(Configuration.GetSection("ReverseProxy"));
```

Details are logged on each part of the request, for example:

```
Forwarder Telemetry [06:40:48.186] => OnForwarderInvoke::
    Cluster id: minimumcluster, Route Id: minimumroute, Destination: example.com
Forwarder Telemetry [06:41:00.269] => OnForwarderStart ::
    Destination prefix: http://www.example.com/
Forwarder Telemetry [06:41:00.298] => OnForwarderStage :: Stage: SendAsyncStart
Forwarder Telemetry [06:41:00.507] => OnForwarderStage :: Stage: SendAsyncStop
Forwarder Telemetry [06:41:00.530] => OnForwarderStage :: Stage:
    ResponseContentTransferStart
Forwarder Telemetry [06:41:03.655] => OnForwarderStop :: Status: 200
```

The events for telemetry are fired as they occur, so you can [obtain the `HttpContext`](/en-us/aspnet/core/fundamentals/http-context#use-httpcontext-from-custom-components) and the YARP feature from it:

```
services.AddTelemetryConsumer<ForwarderTelemetry>();
services.AddHttpContextAccessor();
...
public void OnForwarderInvoke(DateTime timestamp, string clusterId, string routeId,
    string destinationId)
{
    var context = new HttpContextAccessor().HttpContext;
    var YarpFeature = context.GetReverseProxyFeature();

    var dests = from d in YarpFeature.AvailableDestinations
        select d.Model.Config.Address;

    Console.WriteLine($"Destinations: {string.Join(", ", dests)}");
}
```

## Using custom middleware

Another way to inspect the state for requests is to insert additional middleware into the request pipeline. You can insert between the other stages to see the state of the request.

```
// We can customize the proxy pipeline and add/remove/replace steps
app.MapReverseProxy(proxyPipeline =>
{
    // Use a custom proxy middleware, defined below
    proxyPipeline.Use(MyCustomProxyStep);
    // Don't forget to include these two middleware when you make a custom proxy pipeline (if you need them).
    proxyPipeline.UseSessionAffinity();
    proxyPipeline.UseLoadBalancing();
});
...
public Task MyCustomProxyStep(HttpContext context, Func<Task> next)
{
    // Can read data from the request via the context
    foreach (var header in context.Request.Headers)
    {
        Console.WriteLine($"{header.Key}: {header.Value}");
    }

    // The context also stores a ReverseProxyFeature which holds proxy specific data such as the cluster, route and destinations
    var proxyFeature = context.GetReverseProxyFeature();
    Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(proxyFeature.Route.Config));

    // Important - required to move to the next step in the proxy pipeline
    return next();
}
```

You can also use [ASP.NET middleware](/en-us/aspnet/core/fundamentals/middleware/write) within Configure that will enable you to inspect the request before the proxy pipeline.

Note

The proxy streams the response from the destination server back to the client, so the response headers and body aren't readily accessible via middleware.

## Using the debugger

A debugger, such as Visual Studio, can be attached to the proxy process. However, unless you have existing middleware, there isn't a good place in the app code to break and inspect the state of the request. Therefore, the debugger is best used in conjunction with one of the preceding techniques so that you have distinct places to insert breakpoints.

## Network tracing

It can be attractive to use network tracing tools like [Fiddler](https://www.telerik.com/fiddler) or [Wireshark](https://www.wireshark.org/) to try to monitor what's happening either side of the proxy. However, be cautious using both tools:

* Fiddler registers itself as a proxy and relies on apps using the default proxy to be able to monitor traffic. This works for inbound traffic from a browser to YARP but won't capture the outbound requests, as YARP is configured to not use the proxy settings for outbound traffic.
* On Windows, Wireshark uses Npcap to capture packet data for network traffic, so it captures both inbound and outbound traffic and can be used to monitor HTTP traffic.
* HTTPS traffic is encrypted and isn't automatically decryptable by network monitoring tools. Each tool has workarounds that may enable traffic to be monitored, but they require risky use of certificates and changes to trust relationships. Because YARP is making outbound requests, techniques for tricking browsers don't apply to the YARP process.

The protocol choice for outbound traffic is made based on the destination URL in the cluster configuration. If traffic monitoring is used for diagnostics, changing the outbound URLs to `http://`, if possible, may be the simplest approach to enable the monitoring tools to work, provided the issues being diagnosed aren't transport protocol related.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-23

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/bl0002
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/bl0002.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# BL0002: Component has multiple CaptureUnmatchedValues parameters

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | BL0002       |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

More than one parameter on a type deriving from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) is annotated with `CaptureUnmatchedValues = true`.

## Rule description

For a component, exactly one parameter is expected to have the [CaptureUnmatchedValues](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute.captureunmatchedvalues#microsoft-aspnetcore-components-parameterattribute-captureunmatchedvalues) set to `true`.

```
@code
{
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> Parameter1 { get; set; }

    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> Parameter2 { get; set; }
}
```

## How to fix violations

Limit a single parameter to have `CaptureUnmatchedValues` set.

```
@code
{
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> Parameter1 { get; set; }

    [Parameter] public Dictionary<string, object> Parameter2 { get; set; }
}
```

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-05-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/multiple-hosted-webassembly
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/multiple-hosted-webassembly.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Multiple hosted ASP.NET Core Blazor WebAssembly apps

Note

This isn't the latest version of this article. For the latest version of this article, see the [.NET 7 version](?view=aspnetcore-7.0\&preserve-view=true).

This article explains how to configure a hosted Blazor WebAssembly app to host multiple Blazor WebAssembly apps.

## Configuration

Select the version of this article that matches your hosting requirements, either port/domain hosting (for example, `:5001`/`:5002` or `firstapp.com`/`secondapp.com`) or route subpath hosting (for example, `/FirstApp` and `/SecondApp`).

With the current hosting selection, this article covers port/domain hosting (for example, `:5001`/`:5002` or `firstapp.com`/`secondapp.com`).

In the following examples:

* The project name of the hosted Blazor WebAssembly app is `MultipleBlazorApps` in a folder named `MultipleBlazorApps`.
* The three projects in the solution before a second client app is added are `MultipleBlazorApps.Client` in the `Client` folder, `MultipleBlazorApps.Server` in the `Server` folder, and `MultipleBlazorApps.Shared` in the `Shared` folder.
* The initial (first) client app is the default client project of the solution created from the Blazor WebAssembly project template.
* A second client app is added to the solution, `MultipleBlazorApps.SecondClient` in a folder named `SecondClient`.
* Optionally, the server project (`MultipleBlazorApps.Server`) can serve pages or views as a Razor Pages or MVC app.
* The first client app is accessible in a browser at port 5001 or with a host of `firstapp.com`. The second client app is accessible in a browser at port 5002 or with a host of `secondapp.com`.

With the current selection, this article covers route subpath hosting (for example, `/FirstApp` and `/SecondApp`).

In the following examples:

* The project name of the hosted Blazor WebAssembly app is `MultipleBlazorApps` in a folder named `MultipleBlazorApps`.
* The three projects in the solution before a second client app is added are `MultipleBlazorApps.Client` in the `Client` folder, `MultipleBlazorApps.Server` in the `Server` folder, and `MultipleBlazorApps.Shared` in the `Shared` folder.
* The initial (first) client app is the default client project of the solution created from the Blazor WebAssembly project template.
* A second client app is added to the solution, `MultipleBlazorApps.SecondClient` in a folder named `SecondClient`.
* Optionally, the server project (`MultipleBlazorApps.Server`) can serve pages or views as a formal Razor Pages or MVC app.
* Both client apps use the default port defined by the `MultipleBlazorApps.Server` project's `Properties/launchSettings.json` file in its `applicationUrl` value. The first client app is accessible in a browser at the `/FirstApp` subpath. The second client app is accessible in a browser at the `/SecondApp` subpath.

The examples shown in this article require additional configuration for:

* Accessing the apps directly at the example host domains, `firstapp.com` and `secondapp.com`.

* Certificates for the client apps to enable TLS/HTTPS security.

* Configuring the server app as a Razor Pages app for the following features:

  * Integration of Razor components into pages or views.
  * Prerendering Razor components.

The preceding configurations are beyond the scope of this article. For more information, see the following resources:

* [Host and deploy ASP.NET Core](../../../host-and-deploy/?view=aspnetcore-7.0)
* [Enforce HTTPS in ASP.NET Core](../../../security/enforcing-ssl?view=aspnetcore-7.0)
* [Integrate ASP.NET Core Razor components with MVC or Razor Pages in hosted Blazor WebAssembly solutions](../../components/integration-hosted-webassembly?view=aspnetcore-7.0)

Use an existing hosted Blazor WebAssembly [solution](../../tooling?view=aspnetcore-7.0#visual-studio-solution-file-sln) or create a [new hosted Blazor WebAssembly solution](../../tooling?view=aspnetcore-7.0) from the Blazor WebAssembly project template by passing the `-ho|--hosted` option if using the .NET CLI or selecting the **ASP.NET Core Hosted** checkbox in Visual Studio when the project is created in the IDE.

Use a folder for the solution named `MultipleBlazorApps` and name the project `MultipleBlazorApps`.

Create a new folder in the solution named `SecondClient`. In the new folder, add a second Blazor WebAssembly client app named `MultipleBlazorApps.SecondClient`. Add the project as a standalone Blazor WebAssembly app. To create a standalone Blazor WebAssembly app, don't pass the `-ho|--hosted` option if using the .NET CLI or don't use the **ASP.NET Core Hosted** checkbox if using Visual Studio.

Make the following changes to the `MultipleBlazorApps.SecondClient` project:

* Copy the `FetchData` component (`Pages/FetchData.razor`) from the `Client/Pages` folder to the `SecondClient/Pages` folder. This step is required because a standalone Blazor WebAssembly app doesn't call a **Server** project's controller for weather data, it uses a static data file. By copying the `FetchData` component to the added project, the second client app also makes a web API call to the server API for weather data.
* Delete the `SecondClient/wwwroot/sample-data` folder, as the `weather.json` file in the folder isn't used.

The following table describes the solution's folders and project names after the `SecondClient` folder and `MultipleBlazorApps.SecondClient` project are added.

| Physical folder | Project name                      | Description                   |
| --------------- | --------------------------------- | ----------------------------- |
| `Client`        | `MultipleBlazorApps.Client`       | Blazor WebAssembly client app |
| `SecondClient`  | `MultipleBlazorApps.SecondClient` | Blazor WebAssembly client app |
| `Server`        | `MultipleBlazorApps.Server`       | ASP.NET Core server app       |
| `Shared`        | `MultipleBlazorApps.Shared`       | Shared resources project      |

The `MultipleBlazorApps.Server` project serves the two Blazor WebAssembly client apps and provides weather data to the client apps' `FetchData` components via an MVC controller. Optionally, the `MultipleBlazorApps.Server` project can also serve pages or views, as a traditional Razor Pages or MVC app. Steps to enable serving pages or views are covered later in this article.

Note

The demonstration in this article uses static web asset path names of `FirstApp` for the `MultipleBlazorApps.Client` project and `SecondApp` for the `MultipleBlazorApps.SecondClient` project. The names "`FirstApp`" and "`SecondApp`" are merely for demonstration purposes. Other names are acceptable to distinguish the client apps, such as `App1`/`App2`, `Client1`/`Client2`, `1`/`2`, or any similar naming scheme.

When routing requests to the client apps by a port or a domain, "`FirstApp`" and "`SecondApp`" are used ***internally*** to route requests and serve responses for static assets and aren't seen in the browser's address bar.

Note

The demonstration in this article uses static web asset path names of `FirstApp` for the `MultipleBlazorApps.Client` project and `SecondApp` for the `MultipleBlazorApps.SecondClient` project. The names "`FirstApp`" and "`SecondApp`" are merely for demonstration purposes. Other names are acceptable to distinguish the client apps, such as `App1`/`App2`, `Client1`/`Client2`, `1`/`2`, or any similar naming scheme.

"`FirstApp`" and "`SecondApp`" also appear in the browser's address bar because requests are routed to the two client apps using these names. Other valid URL route segments are supported, and the route segments don't strictly need to match the names used to route static web assets internally. Using "`FirstApp`" and "`SecondApp`" for both the internal static asset routing and app request routing is merely for convenance in this article's examples.

In the first client app's project file (`MultipleBlazorApps.Client.csproj`), add a [`<StaticWebAssetBasePath>` property](../../fundamentals/static-files?view=aspnetcore-7.0#static-web-asset-base-path) to a `<PropertyGroup>` with a value of `FirstApp` to set the base path for the project's static assets:

```
<StaticWebAssetBasePath>FirstApp</StaticWebAssetBasePath>
```

In the `MultipleBlazorApps.SecondClient` app's project file (`MultipleBlazorApps.SecondClient.csproj`):

* Add a `<StaticWebAssetBasePath>` property to a `<PropertyGroup>` with a value of `SecondApp`:

  ```
  <StaticWebAssetBasePath>SecondApp</StaticWebAssetBasePath>
  ```

* Add a project reference for the `MultipleBlazorApps.Shared` project to an `<ItemGroup>`:

  ```
  <ItemGroup>
    <ProjectReference Include="..\Shared\MultipleBlazorApps.Shared.csproj" />
  </ItemGroup>
  ```

In the server app's project file (`Server/MultipleBlazorApps.Server.csproj`), create a project reference for the added `MultipleBlazorApps.SecondClient` client app in an `<ItemGroup>`:

```
<ProjectReference Include="..\SecondClient\MultipleBlazorApps.SecondClient.csproj" />
```

In the server app's `Properties/launchSettings.json` file, configure the `applicationUrl` of the Kestrel profile (`MultipleBlazorApps.Server`) to access the client apps at ports 5001 and 5002. If you configure your local environment to use the example domains, URLs for `applicationUrl` can use `firstapp.com` and `secondapp.com` and not use the ports.

Note

The use of ports in this demonstration allows access to the client projects in a local browser without the need to configure a local hosting environment so that web browsers can access the client apps via the host configurations, `firstapp.com` and `secondapp.com`. In production scenarios, a typical configuration is to use subdomains to distinguish the client apps.

For example:

* The ports are dropped from the configuration of this demonstration.
* The hosts are changed to use subdomains, such as `www.contoso.com` for site visitors and `admin.contoso.com` for administrators.
* Additional hosts can be included for additional client apps, and at least one more host is required if the server app is also a Razor Pages or MVC app that serves pages or views.

If you plan to serve pages or views from the server app, use the following `applicationUrl` setting in the `Properties/launchSettings.json` file, which permits the following access:

* Optionally, the Razor Pages or MVC app (`MultipleBlazorApps.Server` project) responds to requests at port 5000.
* Responses to requests for the first client (`MultipleBlazorApps.Client` project) are at port 5001.
* Responses to requests for the second client (`MultipleBlazorApps.SecondClient` project) are at port 5002.

```
"applicationUrl": "https://localhost:5000;https://localhost:5001;https://localhost:5002",
```

If you don't plan for the server app to serve pages or views and only serve the Blazor WebAssembly client apps, use the following setting, which permits the following access:

* The first client app responds on port 5001.
* The second client app responds on port 5002.

```
"applicationUrl": "https://localhost:5001;https://localhost:5002",
```

In the server app's `Program.cs` file, remove the following code, which appears after the call to [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection):

* If you plan to serve pages or views from the server app, delete the following lines of code:

  ```
  - app.UseBlazorFrameworkFiles();
  ```

  ```
  - app.MapFallbackToFile("index.html");
  ```

* If you plan for the server app to only serve the Blazor WebAssembly client apps, delete the following code:

  ```
  - app.UseBlazorFrameworkFiles();

  ...

  - app.UseRouting();

  - app.MapRazorPages();
  - app.MapControllers();
  - app.MapFallbackToFile("index.html");
  ```

  Leave static file middleware in place:

  ```
  app.UseStaticFiles();
  ```

- Add middleware that maps requests to the client apps. The following example configures the middleware to run when the request port is either 5001 for the first client app or 5002 for the second client app, or the request host is either `firstapp.com` for the first client app or `secondapp.com` for the second client app.

  Note

  Use of the hosts (`firstapp.com`/`secondapp.com`) on a local system with a local browser requires additional configuration that's beyond the scope of this article. For local testing of this scenario, we recommend using ports. Typical production apps are configured to use subdomains, such as `www.contoso.com` for site visitors and `admin.contoso.com` for administrators. With the proper DNS and server configuration, which is beyond the scope of this article and depends on the technologies used, the app responds to requests at whatever hosts are named in the following code.

  Where you removed the `app.UseBlazorFrameworkFiles();` line from `Program.cs`, place the following code:

  ```
  app.MapWhen(ctx => ctx.Request.Host.Port == 5001 || 
      ctx.Request.Host.Equals("firstapp.com"), first =>
  {
      first.Use((ctx, nxt) =>
      {
          ctx.Request.Path = "/FirstApp" + ctx.Request.Path;
          return nxt();
      });

      first.UseBlazorFrameworkFiles("/FirstApp");
      first.UseStaticFiles();
      first.UseStaticFiles("/FirstApp");
      first.UseRouting();

      first.UseEndpoints(endpoints =>
      {
          endpoints.MapControllers();
          endpoints.MapFallbackToFile("/FirstApp/{*path:nonfile}", 
              "FirstApp/index.html");
      });
  });

  app.MapWhen(ctx => ctx.Request.Host.Port == 5002 || 
      ctx.Request.Host.Equals("secondapp.com"), second =>
  {
      second.Use((ctx, nxt) =>
      {
          ctx.Request.Path = "/SecondApp" + ctx.Request.Path;
          return nxt();
      });

      second.UseBlazorFrameworkFiles("/SecondApp");
      second.UseStaticFiles();
      second.UseStaticFiles("/SecondApp");
      second.UseRouting();

      second.UseEndpoints(endpoints =>
      {
          endpoints.MapControllers();
          endpoints.MapFallbackToFile("/SecondApp/{*path:nonfile}", 
              "SecondApp/index.html");
      });
  });
  ```

  Warning

  API that relies on the [Host header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Host), such as [HttpRequest.Host](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.host) and [RequireHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.requirehost), are subject to potential spoofing by clients.

  To prevent host and port spoofing, use one of the following approaches:

  * Use [HttpContext.Connection](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.connection) ([ConnectionInfo.LocalPort](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.localport#microsoft-aspnetcore-http-connectioninfo-localport)) where the ports are checked.
  * Employ [Host filtering](../../../fundamentals/servers/kestrel/host-filtering?view=aspnetcore-7.0).

* Add middleware that maps requests to the client apps. The following example configures the middleware to run when the request subpath is `/FirstApp` for the first client app or `/SecondApp` for the second client app.

  Where you removed the `app.UseBlazorFrameworkFiles();` line from `Program.cs`, place the following code:

  ```
  app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp", 
      StringComparison.OrdinalIgnoreCase), first =>
  {
      first.UseBlazorFrameworkFiles("/FirstApp");
      first.UseStaticFiles();
      first.UseStaticFiles("/FirstApp");
      first.UseRouting();

      first.UseEndpoints(endpoints =>
      {
          endpoints.MapControllers();
          endpoints.MapFallbackToFile("/FirstApp/{*path:nonfile}",
              "FirstApp/index.html");
      });
  });

  app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp", 
      StringComparison.OrdinalIgnoreCase), second =>
  {
      second.UseBlazorFrameworkFiles("/SecondApp");
      second.UseStaticFiles();
      second.UseStaticFiles("/SecondApp");
      second.UseRouting();

      second.UseEndpoints(endpoints =>
      {
          endpoints.MapControllers();
          endpoints.MapFallbackToFile("/SecondApp/{*path:nonfile}",
              "SecondApp/index.html");
      });
  });
  ```

* Set the base path in each client app:

  In the first client app's `index.html` file (`Client/wwwroot/index.html`), update the `<base>` tag value to reflect the subpath. The trailing slash is required:

  ```
  <base href="/FirstApp/" />
  ```

  In the second client app's `index.html` file (`SecondClient/wwwroot/index.html`), update the `<base>` tag value to reflect the subpath. The trailing slash is required:

  ```
  <base href="/SecondApp/" />
  ```

For more information on [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), see [ASP.NET Core Blazor static files](../../fundamentals/static-files?view=aspnetcore-7.0).

For more information on `UseBlazorFrameworkFiles` and `MapFallbackToFile`, see the following resources:

* [Microsoft.AspNetCore.Builder.ComponentsWebAssemblyApplicationBuilderExtensions.UseBlazorFrameworkFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentswebassemblyapplicationbuilderextensions.useblazorframeworkfiles) ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/Server/src/ComponentsWebAssemblyApplicationBuilderExtensions.cs))
* [Microsoft.AspNetCore.Builder.StaticFilesEndpointRouteBuilderExtensions.MapFallbackToFile](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfilesendpointroutebuilderextensions.mapfallbacktofile) ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/StaticFiles/src/StaticFilesEndpointRouteBuilderExtensions.cs))

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Requests from the client apps to `/WeatherForecast` in the server API are either to `/FirstApp/WeatherForecast` or `/SecondApp/WeatherForecast` depending on which client app makes the request. Therefore, the controller routes that return weather data from the server API require a modification to include the path segments.

In the server app's weather forecast controller (`Controllers/WeatherForecastController.cs`), replace the existing route (`[Route("[controller]")]`) to `WeatherForecastController` with the following routes, which take into account the client request paths:

```
[Route("FirstApp/[controller]")]
[Route("SecondApp/[controller]")]
```

If you plan to serve pages from the server app, add an `Index` Razor page to the `Pages` folder of the server app:

`Pages/Index.cshtml`:

```
@page
@model MultipleBlazorApps.Server.Pages.IndexModel
@{
    ViewData["Title"] = "Home";
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Home</title>
</head>
<body>
    <div class="main">
        <div class="content px-4">

            <div>
                <h1>Welcome</h1>
                <p>Hello from Razor Pages!</p>
            </div>
        </div>
    </div>
</body>
</html>
```

`Pages/Index.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MultipleBlazorApps.Server.Pages;

public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}
```

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MultipleBlazorApps.Server.Pages
{
    public class IndexModel : PageModel
    {
        public void OnGet()
        {
        }
    }
}
```

Note

The preceding `Index` page is a minimal example purely for demonstration purposes. If the app requires additional Razor Pages assets, such as a layout, styles, scripts, and imports, obtain them from an app created from the Razor Pages project template. For more information, see [Razor Pages architecture and concepts in ASP.NET Core](../../../razor-pages/?view=aspnetcore-7.0).

If you plan to serve MVC views from the server app, add an `Index` view and a `Home` controller:

`Views/Home/Index.cshtml`:

```
@{
    ViewData["Title"] = "Home";
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Home</title>
</head>
<body>
    <div class="main">
        <div class="content px-4">

            <div>
                <h1>Welcome</h1>
                <p>Hello from MVC!</p>
            </div>
        </div>
    </div>
</body>
</html>
```

`Controllers/HomeController.cs`:

```
using Microsoft.AspNetCore.Mvc;

namespace MultipleBlazorApps.Server.Controllers;

public class HomeController : Controller
{
    public IActionResult Index() => View();
}
```

Note

The preceding `Index` view is a minimal example purely for demonstration purposes. If the app requires additional MVC assets, such as a layout, styles, scripts, and imports, obtain them from an app created from the MVC project template. For more information, see [Get started with ASP.NET Core MVC](../../../tutorials/first-mvc-app/start-mvc?view=aspnetcore-7.0).

For more information on using the Razor components from either of the client apps in pages or views of the server app, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages in hosted Blazor WebAssembly solutions](../../components/integration-hosted-webassembly?view=aspnetcore-7.0).

## Run the app

Run the `MultipleBlazorApps.Server` project:

* Access the initial client app at `https://localhost:5001`.
* Access the added client app at `https://localhost:5002`.
* If the server app is configured to serve pages or views, access the `Index` page or view at `https://localhost:5000`.

- Access the initial client app at `https://localhost:{DEFAULT PORT}/FirstApp`.
- Access the added client app at `https://localhost:{DEFAULT PORT}/SecondApp`.
- If the server app is configured to serve pages or views, access the `Index` page or view at `https://localhost:{DEFAULT PORT}`.

In the preceding example URLs, the `{DEFAULT PORT}` placeholder is the default port defined by the `MultipleBlazorApps.Server` project's `Properties/launchSettings.json` file in its `applicationUrl` value.

Important

When running the app with the `dotnet watch` (or `dotnet run`) command (.NET CLI), confirm that the command shell is open in the `Server` folder of the solution.

When using Visual Studio's start button to run the app, confirm that the `MultipleBlazorApps.Server` project is set as the startup project (highlighted in Solution Explorer).

## Static assets

When an asset is in a client app's `wwwroot` folder, provide the static asset request path in components:

```
<img alt="..." src="{PATH AND FILE NAME}" />
```

The `{PATH AND FILE NAME}` placeholder is the path and file name under `wwwroot`.

For example, the source for a Jeep image (`jeep-yj.png`) in the `vehicle` folder of `wwwroot`:

```
<img alt="Jeep Wrangler YJ" src="vehicle/jeep-yj.png" />
```

## Razor class library (RCL) support

Add the [Razor class library (RCL)](../../components/class-libraries?view=aspnetcore-7.0) to the solution as a new project:

* Right-click the solution in **Solution Explorer** and select **Add** > **New Project**.
* Use the **Razor Class Library** project template to create the project. The examples in this section use the project name `ComponentLibrary`, which is also the RCL's assembly name. Don't select the **Support pages and views** checkbox.

For each hosted Blazor WebAssembly client app, create a project reference for the RCL project by right-clicking each client project in **Solution Explorer** and selecting **Add** > **Project Reference**.

Use components from the RCL in the client apps with either of the following approaches:

* Place an [`@using`](../../../mvc/views/razor?view=aspnetcore-7.0#using) directive at the top of the component for the RCL's namespace and add Razor syntax for the component. The following example is for an RCL with the assembly name `ComponentLibrary`:

  ```
  @using ComponentLibrary

  ...

  <Component1 />
  ```

* Provide the RCL's namespace along with the Razor syntax for the component. This approach doesn't require an [`@using`](../../../mvc/views/razor?view=aspnetcore-7.0#using) directive at the top of the component file. The following example is for an RCL with the assembly name `ComponentLibrary`:

  ```
  <ComponentLibrary.Component1 />
  ```

Note

An [`@using`](../../../mvc/views/razor?view=aspnetcore-7.0#using) directive can also be placed into each client app's `_Import.razor` file, which makes the RCL's namespace globally available to components in that project.

When any other static asset is in the `wwwroot` folder of an RCL, reference the static asset in a client app per the guidance in [Reusable Razor UI in class libraries with ASP.NET Core](../../../razor-pages/ui-class?view=aspnetcore-7.0#consume-content-from-a-referenced-rcl):

```
<img alt="..." src="_content/{PACKAGE ID}/{PATH AND FILE NAME}" />
```

The `{PACKAGE ID}` placeholder is the RCL's [package ID](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties). The package ID defaults to the project's assembly name if `<PackageId>` isn't specified in the project file. The `{PATH AND FILE NAME}` placeholder is path and file name under `wwwroot`.

The following example shows the markup for a Jeep image (`jeep-yj.png`) in the `vehicle` folder of the RCL's `wwwroot` folder. The following example is for an RCL with the assembly name `ComponentLibrary`:

```
<img alt="Jeep Wrangler YJ" src="_content/ComponentLibrary/vehicle/jeep-yj.png" />
```

## Related content

* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../../components/class-libraries?view=aspnetcore-7.0)
* [Reusable Razor UI in class libraries with ASP.NET Core](../../../razor-pages/ui-class?view=aspnetcore-7.0)
* [ASP.NET Core Blazor CSS isolation](../../components/css-isolation?view=aspnetcore-7.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-11-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/search
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/search.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 7, add search to an ASP.NET Core MVC app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

In this section, you add search capability to the `Index` action method that lets you search movies by *genre* or *name*.

Update the `Index` method found inside `Controllers/MoviesController.cs` with the following code:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

The following line in the `Index` action method creates a [LINQ](/en-us/dotnet/standard/using-linq) query to select the movies:

```
var movies = from m in _context.Movie
             select m;
```

The query is *only defined* at this point, it has **not** been run against the database.

If the `searchString` parameter contains a string, the movies query is modified to filter on the value of the search string:

```
if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
```

The `s => s.Title!.ToUpper().Contains(searchString.ToUpper())` code above is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/standard/using-linq) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/api/system.linq.enumerable.where) method or `Contains` (used in the code above). LINQ queries are not executed when they're defined or when they're modified by calling a method such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. That means that the evaluation of an expression is delayed until its realized value is actually iterated over or the `ToListAsync` method is called. For more information about deferred query execution, see [Query Execution](/en-us/dotnet/framework/data/adonet/ef/language-reference/query-execution).

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [How to use case-insensitive query with Sqlite provider? (`dotnet/efcore` #11414)](https://github.com/dotnet/efcore/issues/11414)
* [How to make a SQLite column case insensitive (`dotnet/AspNetCore.Docs` #22314)](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to `/Movies/Index`. Append a query string such as `?searchString=Ghost` to the URL. The filtered movies are displayed.

If you change the signature of the `Index` method to have a parameter named `id`, the `id` parameter will match the optional `{id}` placeholder for the default routes set in `Program.cs`.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();
```

Change the parameter to `id` and change all occurrences of `searchString` to `id`.

The previous `Index` method:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

The updated `Index` method with `id` parameter:

```
public async Task<IActionResult> Index(string id)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

You can now pass the search title as route data (a URL segment) instead of as a query string value.

However, you can't expect users to modify the URL every time they want to search for a movie. So now you'll add UI elements to help them filter movies. If you changed the signature of the `Index` method to test how to pass the route-bound `ID` parameter, change it back so that it takes a parameter named `searchString`:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

Open the `Views/Movies/Index.cshtml` file, and add the `<form>` markup highlighted in the following code snippet:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
```

The HTML `<form>` tag uses the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0), so when you submit the form, the filter string is posted to the `Index` action of the movies controller. Save your changes and then test the filter.

There's no `[HttpPost]` overload of the `Index` method as you might expect. You don't need it, because the method isn't changing the state of the app, just filtering data.

You could add the following `[HttpPost] Index` method.

```
[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}
```

The `notUsed` parameter is used to create an overload for the `Index` method. We'll talk about that later in the tutorial.

If you add this method, the action invoker would match the `[HttpPost] Index` method, and the `[HttpPost] Index` method would run as shown in the following image.

However, even if you add this `[HttpPost]` version of the `Index` method, there's a limitation in how this has all been implemented. Imagine that you want to bookmark a particular search or you want to send a link to friends that they can click in order to see the same filtered list of movies. Notice that the URL for the HTTP POST request is the same as the URL for the GET request (localhost:{PORT}/Movies/Index) -- there's no search information in the URL. The search string information is sent to the server as a [form field value](https://developer.mozilla.org/docs/Learn/HTML/Forms/Sending_and_retrieving_form_data). You can verify that with the browser Developer tools or the excellent [Fiddler tool](https://www.telerik.com/fiddler).

The following image shows the Chrome browser Developer tools with the **Network** and **Headers** tabs selected:

The **Network** and **Payload** tabs are selected to view form data:

You can see the search parameter and [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token in the request body. Note, as mentioned in the previous tutorial, the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates an [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) antiforgery token. We're not modifying data, so we don't need to validate the token in the controller method.

Because the search parameter is in the request body and not the URL, you can't capture that search information to bookmark or share with others. Fix this by specifying the request should be `HTTP GET` in the `form` tag found in the `Views/Movies/Index.cshtml` file.

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
```

Now when you submit a search, the URL contains the search query string. Searching will also go to the `HttpGet Index` action method, even if you have a `HttpPost Index` method.

## Add Search by genre

Add the following `MovieGenreViewModel` class to the *Models* folder:

```
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel
{
    public List<Movie>? Movies { get; set; }
    public SelectList? Genres { get; set; }
    public string? MovieGenre { get; set; }
    public string? SearchString { get; set; }
}
```

The movie-genre view model will contain:

* A list of movies.
* A `SelectList` containing the list of genres. This allows the user to select a genre from the list.
* `MovieGenre`, which contains the selected genre.
* `SearchString`, which contains the text users enter in the search text box.

Replace the `Index` method in `MoviesController.cs` with the following code:

```
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}
```

The following code is a `LINQ` query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres (we don't want our select list to have duplicate genres).

When the user searches for the item, the search value is retained in the search box.

## Add search by genre to the Index view

Update `Index.cshtml` found in *Views/Movies/* as follows:

```
@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Examine the lambda expression used in the following HTML Helper:

`@Html.DisplayNameFor(model => model.Movies![0].Title)`

In the preceding code, the `DisplayNameFor` HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. Since the lambda expression is inspected rather than evaluated, you don't receive an access violation when `model`, `model.Movies`, or `model.Movies[0]` are `null` or empty. When the lambda expression is evaluated (for example, `@Html.DisplayFor(modelItem => item.Title)`), the model's property values are evaluated. The `!` after `model.Movies` is the [null-forgiving operator](/en-us/dotnet/csharp/language-reference/operators/null-forgiving), which is used to declare that `Movies` isn't null.

Test the app by searching by genre, by movie title, and by both:

[Previous](controller-methods-views?view=aspnetcore-10.0) [Next](new-field?view=aspnetcore-10.0)

In this section, you add search capability to the `Index` action method that lets you search movies by *genre* or *name*.

Update the `Index` method found inside `Controllers/MoviesController.cs` with the following code:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

The following line in the `Index` action method creates a [LINQ](/en-us/dotnet/standard/using-linq) query to select the movies:

```
var movies = from m in _context.Movie
             select m;
```

The query is *only defined* at this point, it has **not** been run against the database.

If the `searchString` parameter contains a string, the movies query is modified to filter on the value of the search string:

```
if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
```

The `s => s.Title!.ToUpper().Contains(searchString.ToUpper())` code above is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/standard/using-linq) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/api/system.linq.enumerable.where) method or `Contains` (used in the code above). LINQ queries are not executed when they're defined or when they're modified by calling a method such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. That means that the evaluation of an expression is delayed until its realized value is actually iterated over or the `ToListAsync` method is called. For more information about deferred query execution, see [Query Execution](/en-us/dotnet/framework/data/adonet/ef/language-reference/query-execution).

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [How to use case-insensitive query with Sqlite provider? (`dotnet/efcore` #11414)](https://github.com/dotnet/efcore/issues/11414)
* [How to make a SQLite column case insensitive (`dotnet/AspNetCore.Docs` #22314)](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to `/Movies/Index`. Append a query string such as `?searchString=Ghost` to the URL. The filtered movies are displayed.

If you change the signature of the `Index` method to have a parameter named `id`, the `id` parameter will match the optional `{id}` placeholder for the default routes set in `Program.cs`.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

Change the parameter to `id` and change all occurrences of `searchString` to `id`.

The previous `Index` method:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

The updated `Index` method with `id` parameter:

```
public async Task<IActionResult> Index(string id)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

You can now pass the search title as route data (a URL segment) instead of as a query string value.

However, you can't expect users to modify the URL every time they want to search for a movie. So now you'll add UI elements to help them filter movies. If you changed the signature of the `Index` method to test how to pass the route-bound `ID` parameter, change it back so that it takes a parameter named `searchString`:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

Open the `Views/Movies/Index.cshtml` file, and add the `<form>` markup highlighted below:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
```

The HTML `<form>` tag uses the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0), so when you submit the form, the filter string is posted to the `Index` action of the movies controller. Save your changes and then test the filter.

There's no `[HttpPost]` overload of the `Index` method as you might expect. You don't need it, because the method isn't changing the state of the app, just filtering data.

You could add the following `[HttpPost] Index` method.

```
[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}
```

The `notUsed` parameter is used to create an overload for the `Index` method. We'll talk about that later in the tutorial.

If you add this method, the action invoker would match the `[HttpPost] Index` method, and the `[HttpPost] Index` method would run as shown in the image below.

However, even if you add this `[HttpPost]` version of the `Index` method, there's a limitation in how this has all been implemented. Imagine that you want to bookmark a particular search or you want to send a link to friends that they can click in order to see the same filtered list of movies. Notice that the URL for the HTTP POST request is the same as the URL for the GET request (localhost:{PORT}/Movies/Index) -- there's no search information in the URL. The search string information is sent to the server as a [form field value](https://developer.mozilla.org/docs/Learn/HTML/Forms/Sending_and_retrieving_form_data). You can verify that with the browser Developer tools or the excellent [Fiddler tool](https://www.telerik.com/fiddler).

The following image shows the Chrome browser Developer tools with the **Network** and **Headers** tabs selected:

The **Network** and **Payload** tabs are selected to view form data:

You can see the search parameter and [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token in the request body. Note, as mentioned in the previous tutorial, the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates an [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) antiforgery token. We're not modifying data, so we don't need to validate the token in the controller method.

Because the search parameter is in the request body and not the URL, you can't capture that search information to bookmark or share with others. Fix this by specifying the request should be `HTTP GET` in the `form` tag found in the `Views/Movies/Index.cshtml` file.

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
```

Now when you submit a search, the URL contains the search query string. Searching will also go to the `HttpGet Index` action method, even if you have a `HttpPost Index` method.

## Add Search by genre

Add the following `MovieGenreViewModel` class to the *Models* folder:

```
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel
{
    public List<Movie>? Movies { get; set; }
    public SelectList? Genres { get; set; }
    public string? MovieGenre { get; set; }
    public string? SearchString { get; set; }
}
```

The movie-genre view model will contain:

* A list of movies.
* A `SelectList` containing the list of genres. This allows the user to select a genre from the list.
* `MovieGenre`, which contains the selected genre.
* `SearchString`, which contains the text users enter in the search text box.

Replace the `Index` method in `MoviesController.cs` with the following code:

```
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}
```

The following code is a `LINQ` query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres (we don't want our select list to have duplicate genres).

When the user searches for the item, the search value is retained in the search box.

## Add search by genre to the Index view

Update `Index.cshtml` found in *Views/Movies/* as follows:

```
@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Examine the lambda expression used in the following HTML Helper:

`@Html.DisplayNameFor(model => model.Movies![0].Title)`

In the preceding code, the `DisplayNameFor` HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. Since the lambda expression is inspected rather than evaluated, you don't receive an access violation when `model`, `model.Movies`, or `model.Movies[0]` are `null` or empty. When the lambda expression is evaluated (for example, `@Html.DisplayFor(modelItem => item.Title)`), the model's property values are evaluated. The `!` after `model.Movies` is the [null-forgiving operator](/en-us/dotnet/csharp/language-reference/operators/null-forgiving), which is used to declare that `Movies` isn't null.

Test the app by searching by genre, by movie title, and by both:

[Previous](controller-methods-views?view=aspnetcore-10.0) [Next](new-field?view=aspnetcore-10.0)

In this section, you add search capability to the `Index` action method that lets you search movies by *genre* or *name*.

Update the `Index` method found inside `Controllers/MoviesController.cs` with the following code:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

The following line in the `Index` action method creates a [LINQ](/en-us/dotnet/standard/using-linq) query to select the movies:

```
var movies = from m in _context.Movie
             select m;
```

The query is *only defined* at this point, it has **not** been run against the database.

If the `searchString` parameter contains a string, the movies query is modified to filter on the value of the search string:

```
if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
```

The `s => s.Title!.ToUpper().Contains(searchString.ToUpper())` code above is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/standard/using-linq) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/api/system.linq.enumerable.where) method or `Contains` (used in the code above). LINQ queries are not executed when they're defined or when they're modified by calling a method such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. That means that the evaluation of an expression is delayed until its realized value is actually iterated over or the `ToListAsync` method is called. For more information about deferred query execution, see [Query Execution](/en-us/dotnet/framework/data/adonet/ef/language-reference/query-execution).

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [How to use case-insensitive query with Sqlite provider? (`dotnet/efcore` #11414)](https://github.com/dotnet/efcore/issues/11414)
* [How to make a SQLite column case insensitive (`dotnet/AspNetCore.Docs` #22314)](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to `/Movies/Index`. Append a query string such as `?searchString=Ghost` to the URL. The filtered movies are displayed.

If you change the signature of the `Index` method to have a parameter named `id`, the `id` parameter will match the optional `{id}` placeholder for the default routes set in `Program.cs`.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

Change the parameter to `id` and change all occurrences of `searchString` to `id`.

The previous `Index` method:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

The updated `Index` method with `id` parameter:

```
public async Task<IActionResult> Index(string id)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

You can now pass the search title as route data (a URL segment) instead of as a query string value.

However, you can't expect users to modify the URL every time they want to search for a movie. So now you'll add UI elements to help them filter movies. If you changed the signature of the `Index` method to test how to pass the route-bound `ID` parameter, change it back so that it takes a parameter named `searchString`:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

Open the `Views/Movies/Index.cshtml` file, and add the `<form>` markup highlighted below:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
```

The HTML `<form>` tag uses the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0), so when you submit the form, the filter string is posted to the `Index` action of the movies controller. Save your changes and then test the filter.

There's no `[HttpPost]` overload of the `Index` method as you might expect. You don't need it, because the method isn't changing the state of the app, just filtering data.

You could add the following `[HttpPost] Index` method.

```
[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}
```

The `notUsed` parameter is used to create an overload for the `Index` method. We'll talk about that later in the tutorial.

If you add this method, the action invoker would match the `[HttpPost] Index` method, and the `[HttpPost] Index` method would run as shown in the image below.

However, even if you add this `[HttpPost]` version of the `Index` method, there's a limitation in how this has all been implemented. Imagine that you want to bookmark a particular search or you want to send a link to friends that they can click in order to see the same filtered list of movies. Notice that the URL for the HTTP POST request is the same as the URL for the GET request (localhost:{PORT}/Movies/Index) -- there's no search information in the URL. The search string information is sent to the server as a [form field value](https://developer.mozilla.org/docs/Learn/HTML/Forms/Sending_and_retrieving_form_data). You can verify that with the browser Developer tools or the excellent [Fiddler tool](https://www.telerik.com/fiddler). The image below shows the Chrome browser Developer tools:

You can see the search parameter and [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token in the request body. Note, as mentioned in the previous tutorial, the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates an [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) antiforgery token. We're not modifying data, so we don't need to validate the token in the controller method.

Because the search parameter is in the request body and not the URL, you can't capture that search information to bookmark or share with others. Fix this by specifying the request should be `HTTP GET` found in the `Views/Movies/Index.cshtml` file.

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
```

Now when you submit a search, the URL contains the search query string. Searching will also go to the `HttpGet Index` action method, even if you have a `HttpPost Index` method.

The following markup shows the change to the `form` tag:

```
<form asp-controller="Movies" asp-action="Index" method="get">
```

## Add Search by genre

Add the following `MovieGenreViewModel` class to the *Models* folder:

```
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel
{
    public List<Movie>? Movies { get; set; }
    public SelectList? Genres { get; set; }
    public string? MovieGenre { get; set; }
    public string? SearchString { get; set; }
}
```

The movie-genre view model will contain:

* A list of movies.
* A `SelectList` containing the list of genres. This allows the user to select a genre from the list.
* `MovieGenre`, which contains the selected genre.
* `SearchString`, which contains the text users enter in the search text box.

Replace the `Index` method in `MoviesController.cs` with the following code:

```
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}
```

The following code is a `LINQ` query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres (we don't want our select list to have duplicate genres).

When the user searches for the item, the search value is retained in the search box.

## Add search by genre to the Index view

Update `Index.cshtml` found in *Views/Movies/* as follows:

```
@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Examine the lambda expression used in the following HTML Helper:

`@Html.DisplayNameFor(model => model.Movies![0].Title)`

In the preceding code, the `DisplayNameFor` HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. Since the lambda expression is inspected rather than evaluated, you don't receive an access violation when `model`, `model.Movies`, or `model.Movies[0]` are `null` or empty. When the lambda expression is evaluated (for example, `@Html.DisplayFor(modelItem => item.Title)`), the model's property values are evaluated. The `!` after `model.Movies` is the [null-forgiving operator](/en-us/dotnet/csharp/language-reference/operators/null-forgiving), which is used to declare that `Movies` isn't null.

Test the app by searching by genre, by movie title, and by both:

[Previous](controller-methods-views?view=aspnetcore-10.0) [Next](new-field?view=aspnetcore-10.0)

In this section, you add search capability to the `Index` action method that lets you search movies by *genre* or *name*.

Update the `Index` method found inside `Controllers/MoviesController.cs` with the following code:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

The following line in the `Index` action method creates a [LINQ](/en-us/dotnet/standard/using-linq) query to select the movies:

```
var movies = from m in _context.Movie
             select m;
```

The query is *only defined* at this point, it has **not** been run against the database.

If the `searchString` parameter contains a string, the movies query is modified to filter on the value of the search string:

```
if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
```

The `s => s.Title!.ToUpper().Contains(searchString.ToUpper())` code above is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/standard/using-linq) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/api/system.linq.enumerable.where) method or `Contains` (used in the code above). LINQ queries are not executed when they're defined or when they're modified by calling a method such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. That means that the evaluation of an expression is delayed until its realized value is actually iterated over or the `ToListAsync` method is called. For more information about deferred query execution, see [Query Execution](/en-us/dotnet/framework/data/adonet/ef/language-reference/query-execution).

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [How to use case-insensitive query with Sqlite provider? (`dotnet/efcore` #11414)](https://github.com/dotnet/efcore/issues/11414)
* [How to make a SQLite column case insensitive (`dotnet/AspNetCore.Docs` #22314)](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to `/Movies/Index`. Append a query string such as `?searchString=Ghost` to the URL. The filtered movies are displayed.

If you change the signature of the `Index` method to have a parameter named `id`, the `id` parameter will match the optional `{id}` placeholder for the default routes set in `Program.cs`.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

Change the parameter to `id` and change all occurrences of `searchString` to `id`.

The previous `Index` method:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

The updated `Index` method with `id` parameter:

```
public async Task<IActionResult> Index(string id)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

You can now pass the search title as route data (a URL segment) instead of as a query string value.

However, you can't expect users to modify the URL every time they want to search for a movie. So now you'll add UI elements to help them filter movies. If you changed the signature of the `Index` method to test how to pass the route-bound `ID` parameter, change it back so that it takes a parameter named `searchString`:

```
public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    return View(await movies.ToListAsync());
}
```

Open the `Views/Movies/Index.cshtml` file, and add the `<form>` markup highlighted below:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
```

The HTML `<form>` tag uses the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0), so when you submit the form, the filter string is posted to the `Index` action of the movies controller. Save your changes and then test the filter.

There's no `[HttpPost]` overload of the `Index` method as you might expect. You don't need it, because the method isn't changing the state of the app, just filtering data.

You could add the following `[HttpPost] Index` method.

```
[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}
```

The `notUsed` parameter is used to create an overload for the `Index` method. We'll talk about that later in the tutorial.

If you add this method, the action invoker would match the `[HttpPost] Index` method, and the `[HttpPost] Index` method would run as shown in the image below.

However, even if you add this `[HttpPost]` version of the `Index` method, there's a limitation in how this has all been implemented. Imagine that you want to bookmark a particular search or you want to send a link to friends that they can click in order to see the same filtered list of movies. Notice that the URL for the HTTP POST request is the same as the URL for the GET request (localhost:{PORT}/Movies/Index) -- there's no search information in the URL. The search string information is sent to the server as a [form field value](https://developer.mozilla.org/docs/Learn/HTML/Forms/Sending_and_retrieving_form_data). You can verify that with the browser Developer tools or the excellent [Fiddler tool](https://www.telerik.com/fiddler). The image below shows the Chrome browser Developer tools:

You can see the search parameter and [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token in the request body. Note, as mentioned in the previous tutorial, the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates an [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) antiforgery token. We're not modifying data, so we don't need to validate the token in the controller method.

Because the search parameter is in the request body and not the URL, you can't capture that search information to bookmark or share with others. Fix this by specifying the request should be `HTTP GET` found in the `Views/Movies/Index.cshtml` file.

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
```

Now when you submit a search, the URL contains the search query string. Searching will also go to the `HttpGet Index` action method, even if you have a `HttpPost Index` method.

The following markup shows the change to the `form` tag:

```
<form asp-controller="Movies" asp-action="Index" method="get">
```

## Add Search by genre

Add the following `MovieGenreViewModel` class to the *Models* folder:

```
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel
{
    public List<Movie>? Movies { get; set; }
    public SelectList? Genres { get; set; }
    public string? MovieGenre { get; set; }
    public string? SearchString { get; set; }
}
```

The movie-genre view model will contain:

* A list of movies.
* A `SelectList` containing the list of genres. This allows the user to select a genre from the list.
* `MovieGenre`, which contains the selected genre.
* `SearchString`, which contains the text users enter in the search text box.

Replace the `Index` method in `MoviesController.cs` with the following code:

```
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}
```

The following code is a `LINQ` query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres (we don't want our select list to have duplicate genres).

When the user searches for the item, the search value is retained in the search box.

## Add search by genre to the Index view

Update `Index.cshtml` found in *Views/Movies/* as follows:

```
@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Examine the lambda expression used in the following HTML Helper:

`@Html.DisplayNameFor(model => model.Movies![0].Title)`

In the preceding code, the `DisplayNameFor` HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. Since the lambda expression is inspected rather than evaluated, you don't receive an access violation when `model`, `model.Movies`, or `model.Movies[0]` are `null` or empty. When the lambda expression is evaluated (for example, `@Html.DisplayFor(modelItem => item.Title)`), the model's property values are evaluated. The `!` after `model.Movies` is the [null-forgiving operator](/en-us/dotnet/csharp/language-reference/operators/null-forgiving), which is used to declare that `Movies` isn't null.

Test the app by searching by genre, by movie title, and by both:

[Previous](controller-methods-views?view=aspnetcore-10.0) [Next](new-field?view=aspnetcore-10.0)

In this section, you add search capability to the `Index` action method that lets you search movies by *genre* or *name*.

Update the `Index` method found inside `Controllers/MoviesController.cs` with the following code:

```
public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}
```

The first line of the `Index` action method creates a [LINQ](/en-us/dotnet/standard/using-linq) query to select the movies:

```
var movies = from m in _context.Movie
             select m;
```

The query is *only defined* at this point, it has **not** been run against the database.

If the `searchString` parameter contains a string, the movies query is modified to filter on the value of the search string:

```
if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.Contains(searchString));
}
```

The `s => s.Title!.Contains(searchString)` code above is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/standard/using-linq) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/api/system.linq.enumerable.where) method or `Contains` (used in the code above). LINQ queries are not executed when they're defined or when they're modified by calling a method such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. That means that the evaluation of an expression is delayed until its realized value is actually iterated over or the `ToListAsync` method is called. For more information about deferred query execution, see [Query Execution](/en-us/dotnet/framework/data/adonet/ef/language-reference/query-execution).

Note: The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the c# code shown above. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. In SQLite, with the default collation, it's case sensitive.

Navigate to `/Movies/Index`. Append a query string such as `?searchString=Ghost` to the URL. The filtered movies are displayed.

If you change the signature of the `Index` method to have a parameter named `id`, the `id` parameter will match the optional `{id}` placeholder for the default routes set in `Program.cs`.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

Change the parameter to `id` and change all occurrences of `searchString` to `id`.

The previous `Index` method:

```
public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}
```

The updated `Index` method with `id` parameter:

```
public async Task<IActionResult> Index(string id)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.Contains(id));
    }

    return View(await movies.ToListAsync());
}
```

You can now pass the search title as route data (a URL segment) instead of as a query string value.

However, you can't expect users to modify the URL every time they want to search for a movie. So now you'll add UI elements to help them filter movies. If you changed the signature of the `Index` method to test how to pass the route-bound `ID` parameter, change it back so that it takes a parameter named `searchString`:

```
public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}
```

Open the `Views/Movies/Index.cshtml` file, and add the `<form>` markup highlighted below:

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
```

The HTML `<form>` tag uses the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0), so when you submit the form, the filter string is posted to the `Index` action of the movies controller. Save your changes and then test the filter.

There's no `[HttpPost]` overload of the `Index` method as you might expect. You don't need it, because the method isn't changing the state of the app, just filtering data.

You could add the following `[HttpPost] Index` method.

```
[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}
```

The `notUsed` parameter is used to create an overload for the `Index` method. We'll talk about that later in the tutorial.

If you add this method, the action invoker would match the `[HttpPost] Index` method, and the `[HttpPost] Index` method would run as shown in the image below.

However, even if you add this `[HttpPost]` version of the `Index` method, there's a limitation in how this has all been implemented. Imagine that you want to bookmark a particular search or you want to send a link to friends that they can click in order to see the same filtered list of movies. Notice that the URL for the HTTP POST request is the same as the URL for the GET request (localhost:{PORT}/Movies/Index) -- there's no search information in the URL. The search string information is sent to the server as a [form field value](https://developer.mozilla.org/docs/Learn/HTML/Forms/Sending_and_retrieving_form_data). You can verify that with the browser Developer tools or the excellent [Fiddler tool](https://www.telerik.com/fiddler). The image below shows the Chrome browser Developer tools:

You can see the search parameter and [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token in the request body. Note, as mentioned in the previous tutorial, the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates an [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) antiforgery token. We're not modifying data, so we don't need to validate the token in the controller method.

Because the search parameter is in the request body and not the URL, you can't capture that search information to bookmark or share with others. Fix this by specifying the request should be `HTTP GET` found in the `Views/Movies/Index.cshtml` file.

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
```

Now when you submit a search, the URL contains the search query string. Searching will also go to the `HttpGet Index` action method, even if you have a `HttpPost Index` method.

The following markup shows the change to the `form` tag:

```
<form asp-controller="Movies" asp-action="Index" method="get">
```

## Add Search by genre

Add the following `MovieGenreViewModel` class to the *Models* folder:

```
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
    public class MovieGenreViewModel
    {
        public List<Movie>? Movies { get; set; }
        public SelectList? Genres { get; set; }
        public string? MovieGenre { get; set; }
        public string? SearchString { get; set; }
    }
}
```

The movie-genre view model will contain:

* A list of movies.
* A `SelectList` containing the list of genres. This allows the user to select a genre from the list.
* `MovieGenre`, which contains the selected genre.
* `SearchString`, which contains the text users enter in the search text box.

Replace the `Index` method in `MoviesController.cs` with the following code:

```
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}
```

The following code is a `LINQ` query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres (we don't want our select list to have duplicate genres).

When the user searches for the item, the search value is retained in the search box.

## Add search by genre to the Index view

Update `Index.cshtml` found in *Views/Movies/* as follows:

```
@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Examine the lambda expression used in the following HTML Helper:

`@Html.DisplayNameFor(model => model.Movies[0].Title)`

In the preceding code, the `DisplayNameFor` HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. Since the lambda expression is inspected rather than evaluated, you don't receive an access violation when `model`, `model.Movies`, or `model.Movies[0]` are `null` or empty. When the lambda expression is evaluated (for example, `@Html.DisplayFor(modelItem => item.Title)`), the model's property values are evaluated.

Test the app by searching by genre, by movie title, and by both:

[Previous](controller-methods-views?view=aspnetcore-10.0) [Next](new-field?view=aspnetcore-10.0)

In this section, you add search capability to the `Index` action method that lets you search movies by *genre* or *name*.

Update the `Index` method found inside `Controllers/MoviesController.cs` with the following code:

```
public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}
```

The first line of the `Index` action method creates a [LINQ](/en-us/dotnet/standard/using-linq) query to select the movies:

```
var movies = from m in _context.Movie
             select m;
```

The query is *only* defined at this point, it has **not** been run against the database.

If the `searchString` parameter contains a string, the movies query is modified to filter on the value of the search string:

```
if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title.Contains(searchString));
}
```

The `s => s.Title.Contains()` code above is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/standard/using-linq) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/api/system.linq.enumerable.where) method or `Contains` (used in the code above). LINQ queries are not executed when they're defined or when they're modified by calling a method such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. That means that the evaluation of an expression is delayed until its realized value is actually iterated over or the `ToListAsync` method is called. For more information about deferred query execution, see [Query Execution](/en-us/dotnet/framework/data/adonet/ef/language-reference/query-execution).

Note: The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the c# code shown above. The case sensitivity on the query depends on the database and the collation. On SQL Server, [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. In SQLite, with the default collation, it's case sensitive.

Navigate to `/Movies/Index`. Append a query string such as `?searchString=Ghost` to the URL. The filtered movies are displayed.

If you change the signature of the `Index` method to have a parameter named `id`, the `id` parameter will match the optional `{id}` placeholder for the default routes set in `Startup.cs`.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

Change the parameter to `id` and all occurrences of `searchString` change to `id`.

The previous `Index` method:

```
public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}
```

The updated `Index` method with `id` parameter:

```
public async Task<IActionResult> Index(string id)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title.Contains(id));
    }

    return View(await movies.ToListAsync());
}
```

You can now pass the search title as route data (a URL segment) instead of as a query string value.

However, you can't expect users to modify the URL every time they want to search for a movie. So now you'll add UI elements to help them filter movies. If you changed the signature of the `Index` method to test how to pass the route-bound `ID` parameter, change it back so that it takes a parameter named `searchString`:

```
public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}
```

Open the `Views/Movies/Index.cshtml` file, and add the `<form>` markup highlighted below:

```
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
```

The HTML `<form>` tag uses the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0), so when you submit the form, the filter string is posted to the `Index` action of the movies controller. Save your changes and then test the filter.

There's no `[HttpPost]` overload of the `Index` method as you might expect. You don't need it, because the method isn't changing the state of the app, just filtering data.

You could add the following `[HttpPost] Index` method.

```
[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}
```

The `notUsed` parameter is used to create an overload for the `Index` method. We'll talk about that later in the tutorial.

If you add this method, the action invoker would match the `[HttpPost] Index` method, and the `[HttpPost] Index` method would run as shown in the image below.

However, even if you add this `[HttpPost]` version of the `Index` method, there's a limitation in how this has all been implemented. Imagine that you want to bookmark a particular search or you want to send a link to friends that they can click in order to see the same filtered list of movies. Notice that the URL for the HTTP POST request is the same as the URL for the GET request (localhost:{PORT}/Movies/Index) -- there's no search information in the URL. The search string information is sent to the server as a [form field value](https://developer.mozilla.org/docs/Learn/HTML/Forms/Sending_and_retrieving_form_data). You can verify that with the browser Developer tools or the excellent [Fiddler tool](https://www.telerik.com/fiddler). The image below shows the Chrome browser Developer tools:

You can see the search parameter and [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token in the request body. Note, as mentioned in the previous tutorial, the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates an [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) antiforgery token. We're not modifying data, so we don't need to validate the token in the controller method.

Because the search parameter is in the request body and not the URL, you can't capture that search information to bookmark or share with others. Fix this by specifying the request should be `HTTP GET` found in the `Views/Movies/Index.cshtml` file.

```
@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <label>Title: <input type="text" name="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
```

Now when you submit a search, the URL contains the search query string. Searching will also go to the `HttpGet Index` action method, even if you have a `HttpPost Index` method.

The following markup shows the change to the `form` tag:

```
<form asp-controller="Movies" asp-action="Index" method="get">
```

## Add Search by genre

Add the following `MovieGenreViewModel` class to the *Models* folder:

```
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
    public class MovieGenreViewModel
    {
        public List<Movie> Movies { get; set; }
        public SelectList Genres { get; set; }
        public string MovieGenre { get; set; }
        public string SearchString { get; set; }
    }
}
```

The movie-genre view model will contain:

* A list of movies.
* A `SelectList` containing the list of genres. This allows the user to select a genre from the list.
* `MovieGenre`, which contains the selected genre.
* `SearchString`, which contains the text users enter in the search text box.

Replace the `Index` method in `MoviesController.cs` with the following code:

```
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;

    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}
```

The following code is a `LINQ` query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres (we don't want our select list to have duplicate genres).

When the user searches for the item, the search value is retained in the search box.

## Add search by genre to the Index view

Update `Index.cshtml` found in *Views/Movies/* as follows:

```
@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Examine the lambda expression used in the following HTML Helper:

`@Html.DisplayNameFor(model => model.Movies[0].Title)`

In the preceding code, the `DisplayNameFor` HTML Helper inspects the `Title` property referenced in the lambda expression to determine the display name. Since the lambda expression is inspected rather than evaluated, you don't receive an access violation when `model`, `model.Movies`, or `model.Movies[0]` are `null` or empty. When the lambda expression is evaluated (for example, `@Html.DisplayFor(modelItem => item.Title)`), the model's property values are evaluated.

Test the app by searching by genre, by movie title, and by both:

[Previous](controller-methods-views?view=aspnetcore-10.0) [Next](new-field?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-azure-active-directory-b2c
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/standalone-with-azure-active-directory-b2c.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure an ASP.NET Core Blazor WebAssembly standalone app with Azure Active Directory B2C

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

This article explains how to create a [standalone Blazor WebAssembly app](../../hosting-models?view=aspnetcore-10.0#blazor-webassembly) that uses [Azure Active Directory (AAD) B2C](/en-us/azure/active-directory-b2c/overview) for authentication.

For additional security scenario coverage after reading this article, see [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0).

## Walkthrough

The subsections of the walkthrough explain how to:

* Create a tenant in Azure
* Register an app in Azure
* Create the Blazor app
* Run the app

### Create a tenant in Azure

Follow the guidance in [Tutorial: Create an Azure Active Directory B2C tenant](/en-us/azure/active-directory-b2c/tutorial-create-tenant) to create an AAD B2C tenant.

Before proceeding with this article's guidance, confirm that you've [selected the correct directory for the AAD B2C tenant](/en-us/azure/active-directory-b2c/tutorial-create-tenant#select-your-b2c-tenant-directory).

### Register an app in Azure

Register an AAD B2C app:

1. Navigate to **Azure AD B2C** in the Azure portal. Select **App registrations** in the sidebar. Select the **New registration** button.
2. Provide a **Name** for the app (for example, **Blazor Standalone AAD B2C**).
3. For **Supported account types**, select the multi-tenant option: **Accounts in any organizational directory or any identity provider. For authenticating users with Azure AD B2C.**
4. Set the **Redirect URI** dropdown list to **Single-page application (SPA)** and provide the following redirect URI: `https://localhost/authentication/login-callback`. If you know the production redirect URI for the Azure default host (for example, `azurewebsites.net`) or the custom domain host (for example, `contoso.com`), you can also add the production redirect URI at the same time that you're providing the `localhost` redirect URI. Be sure to include the port number for non-`:443` ports in any production redirect URIs that you add.
5. If you're using an [unverified publisher domain](/en-us/entra/identity-platform/howto-configure-publisher-domain), confirm that **Permissions** > **Grant admin consent to openid and offline\_access permissions** is selected. If the publisher domain is verified, this checkbox isn't present.
6. Select **Register**.

Note

Supplying the port number for a `localhost` AAD B2C redirect URI isn't required. For more information, see [Redirect URI (reply URL) restrictions and limitations: Localhost exceptions (Entra documentation)](/en-us/entra/identity-platform/reply-url#localhost-exceptions).

Record the following information:

* Application (client) ID (for example, `00001111-aaaa-2222-bbbb-3333cccc4444`).
* AAD B2C instance (for example, `https://contoso.b2clogin.com/`, which includes the trailing slash): The instance is the scheme and host of an Azure B2C app registration, which can be found by opening the **Endpoints** window from the **App registrations** page in the Azure portal.
* AAD B2C Primary/Publisher/Tenant domain (for example, `contoso.onmicrosoft.com`): The domain is available as the **Publisher domain** in the **Branding** blade of the Azure portal for the registered app.

In **Authentication** > **Platform configurations** > **Single-page application**:

1. Confirm the redirect URI of `https://localhost/authentication/login-callback` is present.
2. In the **Implicit grant** section, ensure that the checkboxes for **Access tokens** and **ID tokens** aren't selected. **Implicit grant isn't recommended for Blazor apps using MSAL v2.0 or later.** For more information, see [Secure ASP.NET Core Blazor WebAssembly](./?view=aspnetcore-10.0#use-the-authorization-code-flow-with-pkce).
3. The remaining defaults for the app are acceptable for this experience.
4. Select the **Save** button if you made changes.

In **Home** > **Azure AD B2C** > **User flows**:

[Create a sign-up and sign-in user flow](/en-us/azure/active-directory-b2c/tutorial-create-user-flows)

At a minimum, select the **Application claims** > **Display Name** user attribute to populate the `context.User.Identity?.Name`/`context.User.Identity.Name` in the `LoginDisplay` component (`Shared/LoginDisplay.razor`).

Record the sign-up and sign-in user flow name created for the app (for example, `B2C_1_signupsignin`).

### Create the Blazor app

In an empty folder, replace the placeholders in the following command with the information recorded earlier and execute the command in a command shell:

```
dotnet new blazorwasm -au IndividualB2C --aad-b2c-instance "{AAD B2C INSTANCE}" --client-id "{CLIENT ID}" --domain "{TENANT DOMAIN}" -o {PROJECT NAME} -ssp "{SIGN UP OR SIGN IN POLICY}"
```

| Placeholder                   | Azure portal name               | Example                                                       |
| ----------------------------- | ------------------------------- | ------------------------------------------------------------- |
| `{AAD B2C INSTANCE}`          | Instance                        | `https://contoso.b2clogin.com/` (includes the trailing slash) |
| `{PROJECT NAME}`              | —                               | `BlazorSample`                                                |
| `{CLIENT ID}`                 | Application (client) ID         | `00001111-aaaa-2222-bbbb-3333cccc4444`                        |
| `{SIGN UP OR SIGN IN POLICY}` | Sign-up/sign-in user flow       | `B2C_1_signupsignin1`                                         |
| `{TENANT DOMAIN}`             | Primary/Publisher/Tenant domain | `contoso.onmicrosoft.com`                                     |

The output location specified with the `-o|--output` option creates a project folder if it doesn't exist and becomes part of the project's name.

Add a pair of [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions) for `openid` and `offline_access` [DefaultAccessTokenScopes](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.defaultaccesstokenscopes#microsoft-authentication-webassembly-msal-models-msalprovideroptions-defaultaccesstokenscopes):

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
    options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
});
```

After creating the app, you should be able to:

* Log into the app using a Microsoft Entra ID user account.

* Request access tokens for Microsoft APIs. For more information, see:

  * [Access token scopes](#access-token-scopes)
  * [Quickstart: Configure an application to expose web APIs](/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis).

  ```
  @using Microsoft.Extensions.Options
  @inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> RemoteOptions
  ```

  Modify the component's redirect in the `OnInitialized` method:

  ```
  - Navigation.NavigateToLogin("authentication/login");
  + Navigation.NavigateToLogin(RemoteOptions.Get(Options.DefaultName)
  +     .AuthenticationPaths.LogInPath);
  ```

  Note

  If other paths differ from the project template's paths or [framework's default paths](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationDefaults.cs), manage them in the same fashion.

## Parts of the app

This section describes the parts of an app generated from the Blazor WebAssembly project template and how the app is configured. There's no specific guidance to follow in this section for a basic working application if you created the app using the guidance in the [Walkthrough](#walkthrough) section. The guidance in this section is helpful for updating an app to authenticate and authorize users. However, an alternative approach to updating an app is to create a new app from the guidance in the [Walkthrough](#walkthrough) section and moving the app's components, classes, and resources to the new app.

### Authentication package

When an app is created to use an Individual B2C Account (`IndividualB2C`), the app automatically receives a package reference for the [Microsoft Authentication Library](/en-us/entra/identity-platform/msal-overview) ([`Microsoft.Authentication.WebAssembly.Msal`](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal)). The package provides a set of primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the [`Microsoft.Authentication.WebAssembly.Msal` package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal) to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

The [`Microsoft.Authentication.WebAssembly.Msal` package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal) transitively adds the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) to the app.

### Authentication service support

Support for authenticating users is registered in the service container with the [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) extension method provided by the [`Microsoft.Authentication.WebAssembly.Msal` package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal). This method sets up all of the services required for the app to interact with the Identity Provider (IP).

In the `Program` file:

```
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
});
```

The [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) method accepts a callback to configure the parameters required to authenticate an app. The values required for configuring the app can be obtained from the configuration when you register the app.

Configuration is supplied by the `wwwroot/appsettings.json` file:

```
{
  "AzureAdB2C": {
    "Authority": "{AAD B2C INSTANCE}{TENANT DOMAIN}/{SIGN UP OR SIGN IN POLICY}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": false
  }
}
```

In the preceding configuration, the `{AAD B2C INSTANCE}` includes a trailing slash.

Example:

```
{
  "AzureAdB2C": {
    "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_signupsignin1",
    "ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
    "ValidateAuthority": false
  }
}
```

### Access token scopes

The Blazor WebAssembly template doesn't automatically configure the app to request an access token for a secure API. To provision an access token as part of the sign-in flow, add the scope to the default access token scopes of the [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions):

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});
```

Specify additional scopes with `AdditionalScopesToConsent`:

```
options.ProviderOptions.AdditionalScopesToConsent.Add("{ADDITIONAL SCOPE URI}");
```

Note

[AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent) isn't able to provision delegated user permissions for Microsoft Graph via the Microsoft Entra ID consent UI when a user first uses an app registered in Microsoft Azure. For more information, see [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0\&pivots=graph-sdk-5#defaultaccesstokenscopes-versus-additionalscopestoconsent).

For more information, see the following sections of the *Additional scenarios* article:

* [Request additional access tokens](additional-scenarios?view=aspnetcore-10.0#request-additional-access-tokens)
* [Attach tokens to outgoing requests](additional-scenarios?view=aspnetcore-10.0#attach-tokens-to-outgoing-requests)

### Login mode

The framework defaults to pop-up login mode and falls back to redirect login mode if a pop-up can't be opened. Configure MSAL to use redirect login mode by setting the `LoginMode` property of [MsalProviderOptions](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions) to `redirect`:

```
builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.LoginMode = "redirect";
});
```

The default setting is `popup`, and the string value isn't case-sensitive.

### Imports file

The [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespace is made available throughout the app via the imports file (`_Imports.razor`):

```
...
@using Microsoft.AspNetCore.Components.Authorization
...
```

### Index page

The Index page (`wwwroot/index.html`) page includes a script that defines the `AuthenticationService` in JavaScript. `AuthenticationService` handles the low-level details of the OIDC protocol. The app internally calls methods defined in the script to perform the authentication operations.

```
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
```

```
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string? Action { get; set; }
}
```

Note

[Nullable reference types (NRTs) and .NET compiler null-state static analysis](../../../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis) is supported in ASP.NET Core in .NET 6 or later. Prior to the release of ASP.NET Core in .NET 6, the `string` type appears without the null type designation (`?`).

### Custom policies

The Microsoft Authentication Library ([Microsoft.Authentication.WebAssembly.Msal](/en-us/dotnet/api/microsoft.authentication.webassembly.msal), [NuGet package](https://www.nuget.org/packages/Microsoft.Authentication.WebAssembly.Msal/)) doesn't support [AAD B2C custom policies](/en-us/azure/active-directory-b2c/user-flow-overview).

## Troubleshoot

### Logging

To enable debug or trace logging for Blazor WebAssembly authentication, see the *Client-side authentication logging* section of [ASP.NET Core Blazor logging](../../fundamentals/logging?view=aspnetcore-10.0) with the article version selector set to ASP.NET Core in .NET 7 or later.

### Common errors

* Misconfiguration of the app or Identity Provider (IP)

  The most common errors are caused by incorrect configuration. The following are a few examples:

  * Depending on the requirements of the scenario, a missing or incorrect Authority, Instance, Tenant ID, Tenant domain, Client ID, or Redirect URI prevents an app from authenticating clients.
  * Incorrect request scopes prevent clients from accessing server web API endpoints.
  * Incorrect or missing server API permissions prevent clients from accessing server web API endpoints.
  * Running the app at a different port than is configured in the Redirect URI of the IP's app registration. Note that a port isn't required for Microsoft Entra ID and an app running at a `localhost` development testing address, but the app's port configuration and the port where the app is running must match for non-`localhost` addresses.

  Configuration sections of this article's guidance show examples of the correct configuration. Carefully check each section of the article looking for app and IP misconfiguration.

  If the configuration appears correct:

  * Analyze application logs.

  * Examine the network traffic between the client app and the IP or server app with the browser's developer tools. Often, an exact error message or a message with a clue to what's causing the problem is returned to the client by the IP or server app after making a request. Developer tools guidance is found in the following articles:

    * [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)
    * [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)
    * [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

  > info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

```
@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthenticationState { get; set; }

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}
```

### Inspect the content of a JSON Web Token (JWT)

To decode a JSON Web Token (JWT), use Microsoft's [jwt.ms](https://jwt.ms/) tool. Values in the UI never leave your browser.

Example encoded JWT (shortened for display):

> eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V\_bU55Ug\_PW4pLPr20tTS8Ct7\_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2\_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

```
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
  "sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]
```

## Related content

* [Identity and account types for single- and multitenant apps](/en-us/security/zero-trust/develop/identity-supported-account-types)
* [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-10.0)
* [Build a custom version of the Authentication.MSAL JavaScript library](additional-scenarios?view=aspnetcore-10.0#build-a-custom-version-of-the-authenticationmsal-javascript-library)
* [Unauthenticated or unauthorized web API requests in an app with a secure default client](additional-scenarios?view=aspnetcore-10.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client)
* [Cloud authentication with Azure Active Directory B2C in ASP.NET Core](../../../security/authentication/azure-ad-b2c?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/header-routing
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/header-routing.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP Header Based Routing

Proxy routes specified in [config](config-files?view=aspnetcore-10.0) or via [code](config-providers?view=aspnetcore-10.0) must include at least a path or host to match against. In addition to these, a route can also specify one or more headers that must be present on the request.

### Precedence

The default route match precedence order is

1. path
2. method
3. host
4. headers
5. query parameters

That means a route which specifies methods and no headers will match before a route which specifies headers and no methods. This can be overridden by setting the `Order` property on a route (see example in [config properties](config-files?view=aspnetcore-10.0#review-all-config-properties)).

## Configuration

Headers are specified in the `Match` section of a proxy route.

If multiple headers rules are specified on a route then all must match for a route to be taken. OR logic must be implemented either within a header rule or as separate routes.

Configuration:

```
"Routes": {
  "route1" : {
    "ClusterId": "cluster1",
    "Match": {
      "Path": "{**catch-all}",
      "Headers": [
        {
          "Name": "header1",
          "Values": [ "value1" ],
          "Mode": "ExactHeader"
        }
      ]
    }
  },
  "route2" : {
    "ClusterId": "cluster1",
    "Match": {
      "Path": "{**catch-all}",
      "Headers": [
        {
          "Name": "header2",
          "Values": [ "1prefix", "2prefix" ],
          "Mode": "HeaderPrefix"
        }
      ]
    }
  },
  "route3" : {
    "ClusterId": "cluster1",
    "Match": {
      "Path": "{**catch-all}",
      "Headers": [
        {
          "Name": "header3",
          "Mode": "Exists"
        }
      ]
    }
  },
  "route4" : {
    "ClusterId": "cluster1",
    "Match": {
      "Path": "{**catch-all}",
      "Headers": [
        {
          "Name": "header4",
          "Values": [ "value1", "value2" ],
          "Mode": "ExactHeader"
        },
        {
          "Name": "header5",
          "Mode": "Exists"
        }
      ]
    }
  },
  "route5" : {
    "ClusterId": "cluster1",
    "Match": {
      "Path": "{**catch-all}",
      "Headers": [
        {
          "Name": "header5",
          "Values": [ "value1", "value2" ],
          "Mode": "Contains"
        },
        {
          "Name": "header6",
          "Mode": "Exists"
        }
      ]
    }
  },
  "route6" : {
    "ClusterId": "cluster1",
    "Match": {
      "Path": "{**catch-all}",
      "Headers": [
        {
          "Name": "header6",
          "Values": [ "value1", "value2" ],
          "Mode": "NotContains"
        },
        {
          "Name": "header7",
          "Mode": "Exists"
        }
      ]
    }
  },
  "route7" : {
    "ClusterId": "cluster1",
    "Match": {
      "Path": "{**catch-all}",
      "Headers": [
        {
          "Name": "header7",
          "Mode": "NotExists"
        }
      ]
    }
  }
}
```

Code:

```
var routes = new[]
{
    new RouteConfig()
    {
        RouteId = "route1",
        ClusterId = "cluster1",
        Match = new RouteMatch
        {
            Path = "{**catch-all}",
            Headers = new[]
            {
                new RouteHeader()
                {
                    Name = "Header1",
                    Values = new[] { "value1" },
                    Mode = HeaderMatchMode.ExactHeader
                }
            }
        }
    },
    new RouteConfig()
    {
        RouteId = "route2",
        ClusterId = "cluster1",
        Match = new RouteMatch
        {
            Path = "{**catch-all}",
            Headers = new[]
            {
                new RouteHeader()
                {
                    Name = "Header2",
                    Values = new[] { "1prefix", "2prefix" },
                    Mode = HeaderMatchMode.HeaderPrefix
                }
            }
        }
    },
    new RouteConfig()
    {
        RouteId = "route3",
        ClusterId = "cluster1",
        Match = new RouteMatch
        {
            Path = "{**catch-all}",
            Headers = new[]
            {
                new RouteHeader()
                {
                    Name = "Header3",
                    Mode = HeaderMatchMode.Exists
                }
            }
        }
    },
    new RouteConfig()
    {
        RouteId = "route4",
        ClusterId = "cluster1",
        Match = new RouteMatch
        {
            Path = "{**catch-all}",
            Headers = new[]
            {
                new RouteHeader()
                {
                    Name = "Header4",
                    Values = new[] { "value1", "value2" },
                    Mode = HeaderMatchMode.ExactHeader
                },
                new RouteHeader()
                {
                    Name = "Header5",
                    Mode = HeaderMatchMode.Exists
                }
            }
        }
    },
    new RouteConfig()
    {
        RouteId = "route5",
        ClusterId = "cluster1",
        Match = new RouteMatch
        {
            Path = "{**catch-all}",
            Headers = new[]
            {
                new RouteHeader()
                {
                    Name = "Header5",
                    Values = new[] { "value1", "value2" },
                    Mode = HeaderMatchMode.Contains
                }
            }
        }
    },
    new RouteConfig()
    {
        RouteId = "route6",
        ClusterId = "cluster1",
        Match = new RouteMatch
        {
            Path = "{**catch-all}",
            Headers = new[]
            {
                new RouteHeader()
                {
                     Name = "Header6",
                    Values = new[] { "value1", "value2" },
                    Mode = HeaderMatchMode.NotContains
                }
            }
        }
    },
    new RouteConfig()
    {
        RouteId = "route7",
        ClusterId = "cluster1",
        Match = new RouteMatch
        {
            Path = "{**catch-all}",
            Headers = new[]
            {
                new RouteHeader()
                {
                    Name = "Header7",
                    Mode = HeaderMatchMode.NotExists
                }
            }
        }
    }
};
```

## Contract

[RouteHeader](/en-us/dotnet/api/yarp.reverseproxy.configuration.routeheader) defines the code contract and is mapped from config.

### Name

The header name to check for on the request. A non-empty value is required. This field is case-insensitive per the HTTP RFCs.

Note

YARP doesn't normalize dashes (`-`) and underscores (`_`) in header names. They're treated as different characters, so a name such as `my-header` doesn't match a request header named `my_header`.

### Values

A list of possible values to search for. The header must match at least one of these values according to the specified `Mode` except for the 'NotContains'. At least one value is required unless `Mode` is set to `Exists` or `NotExists`.

### Mode

[HeaderMatchMode](/en-us/dotnet/api/yarp.reverseproxy.configuration.headermatchmode) specifies how to match the values against the request header. The default is `ExactHeader`.

* ExactHeader - Any of the headers with the given name must match in its entirety, subject to the value of `IsCaseSensitive`. If a header contains multiple values (separated by `,` or `;`), they are split before matching. A single pair of quotes will also be stripped from the value before matching.
* HeaderPrefix - Any of the headers with the given name must match by prefix, subject to the value of `IsCaseSensitive`. If a header contains multiple values (separated by `,` or `;`), they are split before matching. A single pair of quotes will also be stripped from the value before matching.
* Exists - The header must exist and contain any non-empty value. If there are multiple headers with the same name, the rule will also match.
* Contains - Any of the headers with the given name must contain any of the match values, subject to the value of `IsCaseSensitive`.
* NotContains - None of the headers with the given name may contain any of the match values, subject to the value of `IsCaseSensitive`.

### IsCaseSensitive

Indicates if the value match should be performed as case sensitive or insensitive. The default is `false`, insensitive.

## Examples

These examples use the configuration specified above.

### Scenario 1 - Exact Header Match

A request with the following header will match against route1.

```
Header1: Value1
```

If a header contains multiple values, each one will be matched separately. The following request will match.

```
Header1: Value1, Value2
```

The same holds if multiple values are split across multiple headers with the same name.

```
Header1: Value1
Header1: Value2
```

A single pair of enclosing quotes may be stripped from the value before matching. The following request will match.

```
Header1: "Value1"
```

Multiple pairs of quotes will *not* match.

```
Header1: ""Value1""
```

### Scenario 2 - Multiple Values

Route2 defined multiple values to search for in a header ("1prefix", "2prefix"), any of the values are acceptable. It also specified the `Mode` as `HeaderPrefix`, so any header that starts with those values is acceptable.

Any of the following headers will match route2.

```
Header2: 1prefix
```

```
Header2: 2prefix
```

```
Header2: 1prefix-extra
```

```
Header2: 2prefix-extra
```

If a header contains multiple values, each one will be matched separately. The following request will match.

```
Header2: foo, 1prefix, 2prefix
```

The same holds if multiple values are split across multiple headers with the same name.

```
Header2: 1prefix
Header2: 2prefix
```

A single pair of enclosing quotes may be stripped from the value before matching. The following request will match.

```
Header2: "2prefix"
```

Multiple pairs of quotes will *not* match.

```
Header2: ""2prefix""
```

### Scenario 3 - Exists

Route3 only requires that the header "Header3" exists with any non-empty value

The following is an example that will match route3.

```
Header3: value
```

An empty header will *not* match.

```
Header3:
```

This mode does support headers with multiple values and multiple headers with the same name since it does not look at the header contents. The following will match.

```
Header3: value1, value2
```

```
Header3: value1
Header3: value2
```

```
Header3:
Header3:
```

### Scenario 4 - Multiple Headers

Route4 requires both `header4` and `header5`, each matching according to their specified `Mode`. The following headers will match route4:

```
Header4: value1
Header5: AnyValue
```

```
Header4: value2
Header5: AnyValue
```

These will *not* match route4 because they are missing one of the required headers:

```
Header4: value2
```

```
Header5: AnyValue
```

### Scenario 5 - NotExists

Route7 requires that the header "Header7" must not exists

The following headers will match route7:

```
NotHeader7: AnyValue
```

The following headers will *not* match route7 because the header "Header7" exists.

```
Header7: AnyValue
```

```
Header7:
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/transforms-request
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/transforms-request.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Request transforms

Request transforms include the request path, query, HTTP version, method, and headers. In code these are represented by the [RequestTransformContext](/en-us/dotnet/api/yarp.reverseproxy.transforms.requesttransformcontext) object and processed by implementations of the abstract class [RequestTransform](/en-us/dotnet/api/yarp.reverseproxy.transforms.requesttransform).

Notes:

* The proxy request scheme (http/https), authority, and path base, are taken from the destination server address (`https://localhost:10001/Path/Base` in the example above) and should not be modified by transforms.
* The Host header can be overridden by transforms independent of the authority, see [RequestHeader](#requestheader) below.
* The request's original PathBase property is not used when constructing the proxy request, see [X-Forwarded](#x-forwarded).
* All incoming request headers are copied to the proxy request by default with the exception of the Host header (see `Defaults` [Defaults](timeouts?view=aspnetcore-10.0#defaults)). [X-Forwarded](#x-forwarded) headers are also added by default. These behaviors can be configured using the following transforms. Additional request headers can be specified, or request headers can be excluded by setting them to an empty value.

The following are built in transforms identified by their primary config key. These transforms are applied in the order they are specified in the route configuration.

## PathPrefix

**Modifies the request path adding a prefix value**

| Key        | Value                      | Required |
| ---------- | -------------------------- | -------- |
| PathPrefix | A path starting with a '/' | yes      |

Config:

```
{ "PathPrefix": "/prefix" }
```

Code:

```
routeConfig = routeConfig.WithTransformPathPrefix(prefix: "/prefix");
```

```
transformBuilderContext.AddPathPrefix(prefix: "/prefix");
```

Example:\
`/request/path` becomes `/prefix/request/path`

This will prefix the request path with the given value.

## PathRemovePrefix

**Modifies the request path removing a prefix value**

| Key              | Value                      | Required |
| ---------------- | -------------------------- | -------- |
| PathRemovePrefix | A path starting with a '/' | yes      |

Config:

```
{ "PathRemovePrefix": "/prefix" }
```

Code:

```
routeConfig = routeConfig.WithTransformPathRemovePrefix(prefix: "/prefix");
```

```
transformBuilderContext.AddPathRemovePrefix(prefix: "/prefix");
```

Example:\
`/prefix/request/path` becomes `/request/path`\
`/prefix2/request/path` is not modified

This will remove the matching prefix from the request path. Matches are made on path segment boundaries (`/`). If the prefix does not match then no changes are made.

## PathSet

**Replaces the request path with the specified value**

| Key     | Value                      | Required |
| ------- | -------------------------- | -------- |
| PathSet | A path starting with a '/' | yes      |

Config:

```
{ "PathSet": "/newpath" }
```

Code:

```
routeConfig = routeConfig.WithTransformPathSet(path: "/newpath");
```

```
transformBuilderContext.AddPathSet(path: "/newpath");
```

Example:\
`/request/path` becomes `/newpath`

This will set the request path with the given value.

## PathPattern

**Replaces the request path using a pattern template**

| Key         | Value                               | Required |
| ----------- | ----------------------------------- | -------- |
| PathPattern | A path template starting with a '/' | yes      |

Config:

```
{ "PathPattern": "/my/{plugin}/api/{**remainder}" }
```

Code:

```
routeConfig = routeConfig.WithTransformPathRouteValues(
    pattern: new PathString("/my/{plugin}/api/{**remainder}"));
```

```
transformBuilderContext.AddPathRouteValues(
    pattern: new PathString("/my/{plugin}/api/{**remainder}"));
```

This will set the request path with the given value and replace any `{}` segments with the associated route value. `{}` segments without a matching route value are removed. The final `{}` segment can be marked as `{**remainder}` to indicate this is a catch-all segment that may contain multiple path segments. See ASP.NET Core's [routing docs](/en-us/aspnet/core/fundamentals/routing#route-template-reference) for more information about route templates.

Example:

| Step             | Value                               |
| ---------------- | ----------------------------------- |
| Route definition | `/api/{plugin}/stuff/{**remainder}` |
| Request path     | `/api/v1/stuff/more/stuff`          |
| Plugin value     | `v1`                                |
| Remainder value  | `more/stuff`                        |
| PathPattern      | `/my/{plugin}/api/{**remainder}`    |
| Result           | `/my/v1/api/more/stuff`             |

## QueryValueParameter

**Adds or replaces parameters in the request query string**

| Key                 | Value                            | Required |
| ------------------- | -------------------------------- | -------- |
| QueryValueParameter | Name of a query string parameter | yes      |
| Set/Append          | Static value                     | yes      |

Config:

```
{
  "QueryValueParameter": "foo",
  "Append": "bar"
}
```

Code:

```
routeConfig = routeConfig.WithTransformQueryValue(
    queryKey: "foo", value: "bar", append: true);
```

```
transformBuilderContext.AddQueryValue(
    queryKey: "foo", value: "bar", append: true);
```

This will add a query string parameter with the name `foo` and sets it to the static value `bar`.

Example:

| Step                | Value                |
| ------------------- | -------------------- |
| Query               | `?a=b`               |
| QueryValueParameter | `foo`                |
| Append              | `remainder`          |
| Result              | `?a=b&foo=remainder` |

## QueryRouteParameter

**Adds or replaces a query string parameter with a value from the route configuration**

| Key                 | Value                            | Required |
| ------------------- | -------------------------------- | -------- |
| QueryRouteParameter | Name of a query string parameter | yes      |
| Set/Append          | The name of a route value        | yes      |

Config:

```
{
  "QueryRouteParameter": "foo",
  "Append": "remainder"
}
```

Code:

```
routeConfig = routeConfig.WithTransformQueryRouteValue(
    queryKey: "foo", routeValueKey: "remainder", append: true);
```

```
transformBuilderContext.AddQueryRouteValue(
    queryKey: "foo", routeValueKey: "remainder", append: true);
```

This will add a query string parameter with the name `foo` and sets it to the value of the associated route value.

Example:

| Step                | Value               |
| ------------------- | ------------------- |
| Route definition    | `/api/{*remainder}` |
| Request path        | `/api/more/stuff`   |
| Remainder value     | `more/stuff`        |
| QueryRouteParameter | `foo`               |
| Append              | `remainder`         |
| Result              | `?foo=more/stuff`   |

## QueryRemoveParameter

**Removes the specified parameter from the request query string**

| Key                  | Value                            | Required |
| -------------------- | -------------------------------- | -------- |
| QueryRemoveParameter | Name of a query string parameter | yes      |

Config:

```
{ "QueryRemoveParameter": "foo" }
```

Code:

```
routeConfig = routeConfig.WithTransformQueryRemoveKey(queryKey: "foo");
```

```
transformBuilderContext.AddQueryRemoveKey(queryKey: "foo");
```

This will remove a query string parameter with the name `foo` if present on the request.

Example:

| Step                 | Value        |
| -------------------- | ------------ |
| Request path         | `?a=b&foo=c` |
| QueryRemoveParameter | `foo`        |
| Result               | `?a=b`       |

## HttpMethodChange

**Changes the http method used in the request**

| Key              | Value                      | Required |
| ---------------- | -------------------------- | -------- |
| HttpMethodChange | The http method to replace | yes      |
| Set              | The new http method        | yes      |

Config:

```
{
  "HttpMethodChange": "PUT",
  "Set": "POST"
}
```

Code:

```
routeConfig = routeConfig.WithTransformHttpMethodChange(
    fromHttpMethod: HttpMethods.Put, toHttpMethod: HttpMethods.Post);
```

```
transformBuilderContext.AddHttpMethodChange(
    fromHttpMethod: HttpMethods.Put, toHttpMethod: HttpMethods.Post);
```

This will change PUT requests to POST.

## RequestHeadersCopy

**Sets whether incoming request headers are copied to the outbound request**

| Key                | Value      | Default | Required |
| ------------------ | ---------- | ------- | -------- |
| RequestHeadersCopy | true/false | true    | yes      |

Config:

```
{ "RequestHeadersCopy": "false" }
```

Code:

```
routeConfig = routeConfig.WithTransformCopyRequestHeaders(copy: false);
```

```
transformBuilderContext.CopyRequestHeaders = false;
```

This sets if all incoming request headers are copied to the proxy request. This setting is enabled by default and can by disabled by configuring the transform with a `false` value. Transforms that reference specific headers will still be run if this is disabled.

## RequestHeaderOriginalHost

**Specifies if the incoming request Host header should be copied to the proxy request**

| Key                       | Value      | Default | Required |
| ------------------------- | ---------- | ------- | -------- |
| RequestHeaderOriginalHost | true/false | false   | yes      |

Config:

```
{ "RequestHeaderOriginalHost": "true" }
```

```
routeConfig = routeConfig.WithTransformUseOriginalHostHeader(useOriginal: true);
```

```
transformBuilderContext.AddOriginalHost(true);
```

This specifies if the incoming request Host header should be copied to the proxy request. This setting is disabled by default and can be enabled by configuring the transform with a `true` value. Transforms that directly reference the `Host` header will override this transform.

## RequestHeader

**Adds or replaces request headers**

| Key           | Value            | Required |
| ------------- | ---------------- | -------- |
| RequestHeader | The header name  | yes      |
| Set/Append    | The header value | yes      |

Config:

```
{
  "RequestHeader": "MyHeader",
  "Set": "MyValue"
}
```

Code:

```
routeConfig = routeConfig.WithTransformRequestHeader(
    headerName: "MyHeader", value: "MyValue", append: false);
```

```
transformBuilderContext.AddRequestHeader(
    headerName: "MyHeader", value: "MyValue", append: false);
```

Example:

```
MyHeader: MyValue
```

This sets or appends the value for the named header. Set replaces any existing header. Append adds an additional header with the given value. Note: setting "" as a header value is not recommended and can cause an undefined behavior.

## RequestHeaderRouteValue

**Adds or replaces a header with a value from the route configuration**

| Key           | Value                            | Required |
| ------------- | -------------------------------- | -------- |
| RequestHeader | Name of a query string parameter | yes      |
| Set/Append    | The name of a route value        | yes      |

Config:

```
{
  "RequestHeaderRouteValue": "MyHeader",
  "Set": "MyRouteKey"
}
```

Code:

```
routeConfig = routeConfig.WithTransformRequestHeaderRouteValue(
    headerName: "MyHeader", routeValueKey: "key", append: false);
```

```
transformBuilderContext.AddRequestHeaderRouteValue(
    headerName: "MyHeader", routeValueKey: "key", append: false);
```

Example:

| Step                   | Value               |
| ---------------------- | ------------------- |
| Route definition       | `/api/{*remainder}` |
| Request path           | `/api/more/stuff`   |
| Remainder value        | `more/stuff`        |
| RequestHeaderFromRoute | `foo`               |
| Append                 | `remainder`         |
| Result                 | `foo: more/stuff`   |

This sets or appends the value for the named header with a value from the route configuration. Set replaces any existing header. Append adds an additional header with the given value. Note: setting "" as a header value is not recommended and can cause an undefined behavior.

## RequestHeaderRemove

**Removes request headers**

| Key                 | Value           | Required |
| ------------------- | --------------- | -------- |
| RequestHeaderRemove | The header name | yes      |

Config:

```
{
  "RequestHeaderRemove": "MyHeader"
}
```

Code:

```
routeConfig = routeConfig.WithTransformRequestHeaderRemove(headerName: "MyHeader");
```

```
transformBuilderContext.AddRequestHeaderRemove(headerName: "MyHeader");
```

Example:

```
MyHeader: MyValue
AnotherHeader: AnotherValue
```

This removes the named header.

## RequestHeadersAllowed

| Key                   | Value                                               | Required |
| --------------------- | --------------------------------------------------- | -------- |
| RequestHeadersAllowed | A semicolon separated list of allowed header names. | yes      |

Config:

```
{
  "RequestHeadersAllowed": "Header1;header2"
}
```

Code:

```
routeConfig = routeConfig.WithTransformRequestHeadersAllowed("Header1", "header2");
```

```
transformBuilderContext.AddRequestHeadersAllowed("Header1", "header2");
```

YARP copies most request headers to the proxy request by default (see [RequestHeadersCopy](transforms?view=aspnetcore-10.0#requestheaderscopy)). Some security models only allow specific headers to be proxied. This transform disables RequestHeadersCopy and only copies the given headers. Other transforms that modify or append to existing headers may be affected if not included in the allow list.

Note that there are some headers YARP does not copy by default since they are connection specific or otherwise security sensitive (e.g. `Connection`, `Alt-Svc`). Putting those header names in the allow list will bypass that restriction but is strongly discouraged as it may negatively affect the functionality of the proxy or cause security vulnerabilities.

Example:

```
Header1: value1
Header2: value2
AnotherHeader: AnotherValue
```

Only header1 and header2 are copied to the proxy request.

## X-Forwarded

**Adds headers with information about the original client request**

| Key          | Value                                                                                 | Default            | Required |
| ------------ | ------------------------------------------------------------------------------------- | ------------------ | -------- |
| X-Forwarded  | Default action (Set, Append, Remove, Off) to apply to all X-Forwarded-\* listed below | Set                | yes      |
| For          | Action to apply to this header                                                        | \* See X-Forwarded | no       |
| Proto        | Action to apply to this header                                                        | \* See X-Forwarded | no       |
| Host         | Action to apply to this header                                                        | \* See X-Forwarded | no       |
| Prefix       | Action to apply to this header                                                        | \* See X-Forwarded | no       |
| HeaderPrefix | The header name prefix                                                                | "X-Forwarded-"     | no       |

Action "Off" completely disables the transform.

Config:

```
{
  "X-Forwarded": "Set",
  "For": "Remove",
  "Proto": "Append",
  "Prefix": "Off",
  "HeaderPrefix": "X-Forwarded-"
}
```

Code:

```
routeConfig = routeConfig.WithTransformXForwarded(
  headerPrefix = "X-Forwarded-",
  ForwardedTransformActions xDefault = ForwardedTransformActions.Set,
  ForwardedTransformActions? xFor = null,
  ForwardedTransformActions? xHost = null,
  ForwardedTransformActions? xProto = null,
  ForwardedTransformActions? xPrefix = null);
```

```
transformBuilderContext.AddXForwarded(ForwardedTransformActions.Set);
transformBuilderContext.AddXForwardedFor(headerName: "X-Forwarded-For", ForwardedTransformActions.Append);
transformBuilderContext.AddXForwardedHost(headerName: "X-Forwarded-Host", ForwardedTransformActions.Append);
transformBuilderContext.AddXForwardedProto(headerName: "X-Forwarded-Proto", ForwardedTransformActions.Off);
transformBuilderContext.AddXForwardedPrefix(headerName: "X-Forwarded-Prefix", ForwardedTransformActions.Remove);
```

Example:

```
X-Forwarded-For: 5.5.5.5
X-Forwarded-Proto: https
X-Forwarded-Host: IncomingHost:5000
X-Forwarded-Prefix: /path/base
```

Disable default headers:

```
{ "X-Forwarded": "Off" }
```

```
transformBuilderContext.UseDefaultForwarders = false;
```

When the proxy connects to the destination server, the connection is independent from the one the client made to the proxy. The destination server likely needs original connection information for security checks and to properly generate absolute URIs for links and redirects. To enable information about the client connection to be passed to the destination a set of extra headers can be added. Until the `Forwarded` standard was created, a common solution is to use `X-Forwarded-*` headers. There is no official standard that defines the `X-Forwarded-*` headers and implementations vary, check your destination server for support.

This transform is enabled by default even if not specified in the route config.

Set the `X-Forwarded` value to a comma separated list containing the headers you need to enable. All for headers are enabled by default. All can be disabled by specifying the value `"Off"`.

The Prefix specifies the header name prefix to use for each header. With the default `X-Forwarded-` prefix the resulting headers will be `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-Prefix`.

Transform action specifies how each header should be combined with an existing header of the same name. It can be "Set", "Append", "Remove, or "Off" (completely disable the transform). A request traversing multiple proxies may accumulate a list of such headers and the destination server will need to evaluate the list to determine the original value. If action is "Set" and the associated value is not available on the request (e.g. RemoteIpAddress is null), any existing header is still removed to prevent spoofing.

The {Prefix}For header value is taken from `HttpContext.Connection.RemoteIpAddress` representing the prior caller's IP address. The port is not included. IPv6 addresses do not include the bounding `[]` brackets.

The {Prefix}Proto header value is taken from `HttpContext.Request.Scheme` indicating if the prior caller used HTTP or HTTPS.

The {Prefix}Host header value is taken from the incoming request's Host header. This is independent of RequestHeaderOriginalHost specified above. Unicode/IDN hosts are punycode encoded.

The {Prefix}Prefix header value is taken from `HttpContext.Request.PathBase`. The PathBase property is not used when generating the proxy request so the destination server will need the original value to correctly generate links and directs. The value is in the percent encoded Uri format.

## Forwarded

**Adds a header with information about the original client request**

| Key       | Value                                                                                                             | Default | Required |
| --------- | ----------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| Forwarded | A comma separated list containing any of these values: for,by,proto,host                                          | (none)  | yes      |
| ForFormat | Random/RandomAndPort/RandomAndRandomPort/Unknown/UnknownAndPort/UnknownAndRandomPort/Ip/IpAndPort/IpAndRandomPort | Random  | no       |
| ByFormat  | Random/RandomAndPort/RandomAndRandomPort/Unknown/UnknownAndPort/UnknownAndRandomPort/Ip/IpAndPort/IpAndRandomPort | Random  | no       |
| Action    | Action to apply to this header (Set, Append, Remove, Off)                                                         | Set     | no       |

Config:

```
{
  "Forwarded": "by,for,host,proto",
  "ByFormat": "Random",
  "ForFormat": "IpAndPort",
  "Action": "Append"
},
```

Code:

```
routeConfig = routeConfig.WithTransformForwarded(
    useHost: true, useProto: true, forFormat: NodeFormat.IpAndPort, 
    ByFormat: NodeFormat.Random, action: ForwardedTransformAction.Append);
```

```
transformBuilderContext.AddForwarded(
    useHost: true, useProto: true, forFormat: NodeFormat.IpAndPort, 
    ByFormat: NodeFormat.Random, action: ForwardedTransformAction.Append);
```

Example:

```
Forwarded: proto=https;host="localhost:5001";for="[::1]:20173";by=_YQuN68tm6
```

The `Forwarded` header is defined by [RFC 7239](https://tools.ietf.org/html/rfc7239). It consolidates many of the same functions as the unofficial X-Forwarded headers, flowing information to the destination server that would otherwise be obscured by using a proxy.

Enabling this transform will disable the default X-Forwarded transforms as they carry similar information in another format. The X-Forwarded transforms can still be explicitly enabled.

Action: This specifies how the transform should handle an existing Forwarded header. It can be "Set", "Append", "Remove, or "Off" (completely disable the transform). A request traversing multiple proxies may accumulate a list of such headers and the destination server will need to evaluate the list to determine the original value.

Proto: This value is taken from `HttpContext.Request.Scheme` indicating if the prior caller used HTTP or HTTPS.

Host: This value is taken from the incoming request's Host header. This is independent of RequestHeaderOriginalHost specified above. Unicode/IDN hosts are punycode encoded.

For: This value identifies the prior caller. IP addresses are taken from `HttpContext.Connection.RemoteIpAddress`. See ByFormat and ForFormat below for details.

By: This value identifies where the proxy received the request. IP addresses are taken from `HttpContext.Connection.LocalIpAddress`. See ByFormat and ForFormat below for details.

ByFormat and ForFormat:

The RFC allows a [variety of formats](https://tools.ietf.org/html/rfc7239#section-6) for the By and For fields. It requires that the default format uses an obfuscated identifier identified here as Random.

| Format               | Description                                                                                                                                                                                    | Example                      |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| Random               | An obfuscated identifier that is generated randomly per request. This allows for diagnostic tracing scenarios while limiting the flow of uniquely identifying information for privacy reasons. | `by=_YQuN68tm6`              |
| RandomAndPort        | The Random identifier plus the port.                                                                                                                                                           | `by="_YQuN68tm6:80"`         |
| RandomAndRandomPort  | The Random identifier plus another random identifier for the port.                                                                                                                             | `by="_YQuN68tm6:_jDw5Cf3tQ"` |
| Unknown              | This can be used when the identity of the preceding entity is not known, but the proxy server still wants to signal that the request was forwarded.                                            | `by=unknown`                 |
| UnknownAndPort       | The Unknown identifier plus the port if available.                                                                                                                                             | `by="unknown:80"`            |
| UnknownAndRandomPort | The Unknown identifier plus random identifier for the port.                                                                                                                                    | `by="unknown:_jDw5Cf3tQ"`    |
| Ip                   | An IPv4 address or an IPv6 address including brackets.                                                                                                                                         | `by="[::1]"`                 |
| IpAndPort            | The IP address plus the port.                                                                                                                                                                  | `by="[::1]:80"`              |
| IpAndRandomPort      | The IP address plus random identifier for the port.                                                                                                                                            | `by="[::1]:_jDw5Cf3tQ"`      |

## ClientCert

**Forwards the client cert used on the inbound connection as a header to destination**

| Key        | Value           | Required |
| ---------- | --------------- | -------- |
| ClientCert | The header name | yes      |

Config:

```
{ "ClientCert": "X-Client-Cert" }
```

Code:

```
routeConfig = routeConfig.WithTransformClientCertHeader(headerName: "X-Client-Cert");
```

```
transformBuilderContext.AddClientCertHeader(headerName: "X-Client-Cert");
```

Example:

```
X-Client-Cert: SSdtIGEgY2VydGlmaWNhdGU...
```

As the inbound and outbound connections are independent, there needs to be a way to pass any inbound client certificate to the destination server. This transform causes the client certificate taken from `HttpContext.Connection.ClientCertificate` to be Base64 encoded and set as the value for the given header name. The destination server may need that certificate to authenticate the client. There is no standard that defines this header and implementations vary, check your destination server for support.

Servers do minimal validation on the incoming client certificate by default. The certificate should be validated either in the proxy or the destination, see the [client certificate auth](/en-us/aspnet/core/security/authentication/certauth) docs for details.

This transform will only apply if the client certificate is already present on the connection. See the [optional certs doc](/en-us/aspnet/core/security/authentication/certauth#optional-client-certificates) if it needs to be requested from the client on a per-route basis.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core built-in Tag Helpers

By [Peter Kellner](https://peterkellner.net)

For an overview of Tag Helpers, see [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0).

There are built-in Tag Helpers which aren't listed in this document. The unlisted Tag Helpers are used internally by the [Razor](../../razor?view=aspnetcore-10.0) view engine. The Tag Helper for the `~` (tilde) character is unlisted. The tilde Tag Helper expands to the root path of the website.

## Built-in ASP.NET Core Tag Helpers

**[Anchor](anchor-tag-helper?view=aspnetcore-10.0)**

**[Cache](cache-tag-helper?view=aspnetcore-10.0)**

**[Component](component-tag-helper?view=aspnetcore-10.0)**

**[Distributed Cache](distributed-cache-tag-helper?view=aspnetcore-10.0)**

**[Environment](environment-tag-helper?view=aspnetcore-10.0)**

**[Form](../../working-with-forms?view=aspnetcore-10.0#the-form-tag-helper)**

**[Form Action](../../working-with-forms?view=aspnetcore-10.0#the-form-action-tag-helper)**

**[Image](image-tag-helper?view=aspnetcore-10.0)**

**[Input](../../working-with-forms?view=aspnetcore-10.0#the-input-tag-helper)**

**[Label](../../working-with-forms?view=aspnetcore-10.0#the-label-tag-helper)**

**[Link](link-tag-helper?view=aspnetcore-10.0)**

**[Partial](partial-tag-helper?view=aspnetcore-10.0)**

**[Persist Component State](persist-component-state?view=aspnetcore-10.0)**

**[Script](script-tag-helper?view=aspnetcore-10.0)**

**[Select](../../working-with-forms?view=aspnetcore-10.0#the-select-tag-helper)**

**[Textarea](../../working-with-forms?view=aspnetcore-10.0#the-textarea-tag-helper)**

**[Validation Message](../../working-with-forms?view=aspnetcore-10.0#the-validation-message-tag-helper)**

**[Validation Summary](../../working-with-forms?view=aspnetcore-10.0#the-validation-summary-tag-helper)**

## Related content

* [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0)
* [Tag Helper Components in ASP.NET Core](../th-components?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/browser
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/browser.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use gRPC in browser apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

It's not possible to directly call a gRPC service from a browser. gRPC uses HTTP/2 features, and no browser provides the level of control required over web requests to support a gRPC client.

gRPC on ASP.NET Core offers two browser-compatible solutions, gRPC-Web and gRPC JSON transcoding.

## gRPC-Web

gRPC-Web allows browser apps to call gRPC services with the gRPC-Web client and Protobuf.

* Similar to normal gRPC, but it has a slightly different wire-protocol, which makes it compatible with HTTP/1.1 and browsers.
* Requires the browser app to generate a gRPC client from a `.proto` file.
* Allows browser apps to benefit from the high-performance and low network usage of binary messages.

.NET has built-in support for gRPC-Web. For more information, see [gRPC-Web in ASP.NET Core gRPC apps](grpcweb?view=aspnetcore-10.0).

## gRPC JSON transcoding

gRPC JSON transcoding allows browser apps to call gRPC services as if they were RESTful APIs with JSON.

* The browser app doesn't need to generate a gRPC client or know anything about gRPC.
* RESTful APIs are automatically created from gRPC services by annotating the `.proto` file with HTTP metadata.
* Allows an app to support both gRPC and JSON web APIs without duplicating the effort of building separate services for both.

.NET has built-in support for creating JSON web APIs from gRPC services. For more information, see [gRPC JSON transcoding in ASP.NET Core gRPC apps](json-transcoding?view=aspnetcore-10.0).

Note

gRPC JSON transcoding requires .NET 7 or later.

## Related content

* [gRPC-Web in ASP.NET Core gRPC apps](grpcweb?view=aspnetcore-10.0)
* [gRPC JSON transcoding in ASP.NET Core gRPC apps](json-transcoding?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/caching/distributed.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Distributed caching in ASP.NET Core

By [Mohsin Nasir](https://github.com/mohsinnasir) and [smandia](https://github.com/smandia)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

A distributed cache is a cache shared by multiple app servers. The cache is typically maintained as an external service for the app servers that access it. A distributed cache can improve the performance and scalability of an ASP.NET Core app, especially when a cloud service or a server farm hosts the app.

A distributed cache has several advantages over other caching scenarios where cached data is stored on individual app servers.

When cached data is distributed, the data:

* Is *coherent* (consistent) across requests to multiple servers.
* Survives server restarts and app deployments.
* Doesn't use local memory.

Distributed cache configuration is implementation specific. This article describes how to configure SQL Server, Redis, or Postgres distributed caches. Non-Microsoft implementations are also available, such as [NCache](http://www.alachisoft.com/ncache/aspnet-core-idistributedcache-ncache.html) ([NCache on GitHub](https://github.com/Alachisoft/NCache)), Azure Cosmos DB, and Postgres. Regardless of which implementation is selected, the app interacts with the cache by using the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/distributed/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

## Prerequisites

Add a package reference for the distributed cache provider used:

* For a Redis distributed cache, [Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis).
* For SQL Server, [Microsoft.Extensions.Caching.SqlServer](https://www.nuget.org/packages/Microsoft.Extensions.Caching.SqlServer).
* For Postgres, [Microsoft.Extensions.Caching.Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres).
* For Azure Cosmos DB, [Microsoft.Extensions.Caching.Cosmos](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Cosmos).
* For the NCache distributed cache, [NCache.Microsoft.Extensions.Caching.OpenSource](https://www.nuget.org/packages/NCache.Microsoft.Extensions.Caching.OpenSource).

## Use the IDistributedCache interface

The [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface provides the following methods to manipulate items in the distributed cache implementation:

* [Get](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.get), [GetAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.getasync): Accepts a string key and retrieves a cached item as a `byte[]` array if found in the cache.
* [Set](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.set), [SetAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.setasync): Adds an item (as `byte[]` array) to the cache by using a string key.
* [Refresh](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.refresh), [RefreshAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.refreshasync): Refreshes an item in the cache based on its key, resetting its sliding expiration timeout (if any).
* [Remove](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.remove), [RemoveAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.removeasync): Removes a cache item based on its string key.

### Store objects other than byte arrays

[IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) stores values as `byte[]` arrays. To cache other types, serialize them first. The following extension methods use [System.Text.Json](/en-us/dotnet/api/system.text.json) to store any serializable type as UTF-8 JSON:

```
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;

public static class DistributedCacheExtensions
{
    public static Task SetAsync<T>(this IDistributedCache cache, string key, T value,
        DistributedCacheEntryOptions options, CancellationToken token = default) =>
        cache.SetAsync(key, JsonSerializer.SerializeToUtf8Bytes(value), options, token);

    public static async Task<T?> GetAsync<T>(this IDistributedCache cache, string key,
        CancellationToken token = default)
    {
        var bytes = await cache.GetAsync(key, token);
        return bytes is null ? default : JsonSerializer.Deserialize<T>(bytes);
    }
}
```

`GetAsync<T>` returns the type's default value, such as `null`, when the key isn't in the cache.

Tip

The [`HybridCache` library](hybrid?view=aspnetcore-10.0) serializes values for you and adds an in-process primary cache and stampede protection on top of the distributed cache.

## Establish distributed caching services

Register an implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) in the *Program.cs* file. The following framework-provided implementations are described in this article:

* [Distributed Redis cache](#distributed-redis-cache)
* [Distributed memory cache](#distributed-memory-cache)
* [Distributed SQL Server cache](#distributed-sql-server-cache)
* [Distributed Postgres cache](#distributed-postgres-cache)
* [Distributed NCache cache](#distributed-ncache-cache)
* [Distributed Azure Cosmos DB cache](#distributed-azure-cosmos-db-cache)

### Distributed Redis cache

The distributed Redis cache delivers the best performance and is recommended for production apps. [Redis](https://redis.io/) is an open source in-memory data store, which is often used as a distributed cache. You can configure an [Azure Cache for Redis](/en-us/azure/azure-cache-for-redis/cache-overview) for an Azure-hosted ASP.NET Core app, and use an Azure Cache for Redis for local development. For more information, see [Review cache recommendations](#review-cache-recommendations).

An app configures the cache implementation with a [RedisCache](/en-us/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscache) instance by calling the [AddStackExchangeRedisCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangerediscacheservicecollectionextensions.addstackexchangerediscache) method. For [output caching](output?view=aspnetcore-10.0#cache-storage), use the [AddStackExchangeRedisOutputCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangeredisoutputcacheservicecollectionextensions.addstackexchangeredisoutputcache) method.

1. Create an instance of Azure Cache for Redis.

2. Copy the primary connection string (StackExchange.Redis) to [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0).

   * **For local development**: Save the connection string with [Secret Manager](../../security/app-secrets?view=aspnetcore-10.0#secret-manager).

   * **For Azure**: Save the connection string in a secure store such as [Azure Key Vault](/en-us/azure/key-vault/general/overview).

The following code enables the Azure Cache for Redis:

```
builder.Services.AddStackExchangeRedisCache(options =>
 {
     options.Configuration = builder.Configuration.GetConnectionString("MyRedisConStr");
     options.InstanceName = "SampleInstance";
 });
```

The preceding code assumes the primary connection string (StackExchange.Redis) is saved in configuration with the key name `MyRedisConStr`.

For more information, see [Azure Managed Redis](/en-us/azure/redis/overview).

For a discussion on alternative approaches to a local Redis cache, see [GitHub /dotnet/aspnetcore issue #19542](https://github.com/dotnet/AspNetCore.Docs/issues/19542).

### Distributed memory cache

The distributed memory cache ([AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache)) is a framework-provided implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) that stores items in memory. However, the distributed memory cache isn't an actual distributed cache. The app instance stores the cached items on the server where the app is running.

The distributed memory cache is a useful implementation for development and testing scenarios. It's also useful for a single server in a production scenario where memory consumption isn't an issue. Implementing the distributed memory cache abstracts cached data storage. It allows for implementing a true distributed caching solution in the future if multiple nodes or fault tolerance become necessary.

The sample app makes use of the distributed memory cache when the app runs in the `Development` environment in the *Program.cs* file.

```
builder.Services.AddDistributedMemoryCache();
```

### Distributed SQL Server cache

The distributed SQL Server cache implementation ([AddDistributedSqlServerCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.sqlservercachingservicesextensions.adddistributedsqlservercache)) allows the distributed cache to use a SQL Server database as its backing store. To create a SQL Server cached item table in a SQL Server instance, you can use the `sql-cache` tool. The tool creates a table with the name and schema that you specify.

Create a table in SQL Server by running the `sql-cache create` command. Provide the SQL Server instance (`Data Source`), database (`Initial Catalog`), schema (for example, `dbo`), and table name (for example, `TestCache`):

```
dotnet sql-cache create "Data Source=(localdb)/MSSQLLocalDB;Initial Catalog=DistCache;Integrated Security=True;" dbo TestCache
```

When the tool succeeds, a message is logged:

```
Table and index were created successfully.
```

The table created by the `sql-cache` tool has the following schema:

Note

An app should manipulate cache values by using an instance of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache), not an instance of [SqlServerCache](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercache).

The sample app implements the [SqlServerCache](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercache) class in a nondevelopment (`Development`) environment in the *Program.cs* file:

```
builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString(
        "DistCache_ConnectionString");
    options.SchemaName = "dbo";
    options.TableName = "TestCache";
});
```

Note

Properties like [ConnectionString](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercacheoptions.connectionstring) (and optionally, [SchemaName](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercacheoptions.schemaname) and [TableName](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercacheoptions.tablename)) are typically stored outside of source control. For example, the [Secret Manager](../../security/app-secrets?view=aspnetcore-10.0) or the *appsettings.json* or *appsettings.{Environment}.json* file might store the properties. The connection string can contain credentials that should be kept out of source control systems.

For more information, see [SQL Database on Azure](/en-us/azure/sql-database/).

### Distributed Postgres cache

[Azure Database for PostgreSQL](/en-us/azure/postgresql) can be used as a distributed cache backing store via the `IDistributedCache` interface. Azure Database for PostgreSQL is a fully managed, AI-ready Database-as-a-Service (DBaaS) offering built on the open-source PostgreSQL engine. The design supports mission-critical workloads with predictable performance, robust security, high availability, and seamless scalability.

After installing the [Microsoft.Extensions.Caching.Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres) NuGet package, configure your distributed cache as follows:

1. Register the Service.

   ```
   using Microsoft.Extensions.DependencyInjection;

   var builder = WebApplication.CreateBuilder(args);

   // Register the Postgres distributed cache.
   builder.Services.AddDistributedPostgresCache(options => {
      options.ConnectionString = builder.Configuration.GetConnectionString("PostgresCache");
      options.SchemaName = builder.Configuration.GetValue<string>("PostgresCache:SchemaName", "public");
      options.TableName = builder.Configuration.GetValue<string>("PostgresCache:TableName", "cache");
      options.CreateIfNotExists = builder.Configuration.GetValue<bool>("PostgresCache:CreateIfNotExists", true);
      options.UseWAL = builder.Configuration.GetValue<bool>("PostgresCache:UseWAL", false);

      // Optional: Configure expiration settings.

      var expirationInterval = builder.Configuration.GetValue<string>("PostgresCache:ExpiredItemsDeletionInterval");
      if (!string.IsNullOrEmpty(expirationInterval) && TimeSpan.TryParse(expirationInterval, out var interval)) {
          options.ExpiredItemsDeletionInterval = interval;
      }

      var slidingExpiration = builder.Configuration.GetValue<string>("PostgresCache:DefaultSlidingExpiration");
      if (!string.IsNullOrEmpty(slidingExpiration) && TimeSpan.TryParse(slidingExpiration, out var sliding)) {
          options.DefaultSlidingExpiration = sliding;
      }
   });

   var app = builder.Build();
   ```

2. Use the cache.

   ```
   public class MyService {
       private readonly IDistributedCache _cache; 

       public MyService(IDistributedCache cache) {
           _cache = cache;
       }

       public async Task<string> GetDataAsync(string key) {
           var cachedData = await _cache.GetStringAsync(key);

           if (cachedData == null) {

               // Fetch the data from source.
               var data = await FetchDataFromSource();

               // Cache the data with options.
               var options = new DistributedCacheEntryOptions {
                  AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
                  SlidingExpiration = TimeSpan.FromMinutes(5)
               };

               await _cache.SetStringAsync(key, data, options);
               return data;
           }

           return cachedData;
       }
   }
   ```

### Distributed NCache cache

[NCache](https://github.com/Alachisoft/NCache) is an open source in-memory distributed cache developed natively in .NET. NCache works both locally and configured as a distributed cache cluster for an ASP.NET Core app running in Azure or on other hosting platforms.

To install and configure NCache on your local machine, see the [Getting Started Guide](https://www.alachisoft.com/resources/docs/ncache/getting-started/).

To configure NCache:

1. Install the [NCache SDK NuGet package](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/), which supports NCache Opensource for .NET Framework and .NET Core apps.

2. Configure the cache cluster in the [client configuration](https://www.alachisoft.com/resources/docs/ncache/admin-guide/client-config.html) (the *client.ncconf* file).

3. Add the following code to the *Program.cs* file:

```
builder.Services.AddNCacheDistributedCache(configuration =>
{
    configuration.CacheName = "democache";
    configuration.EnableLogs = true;
    configuration.ExceptionsEnabled = true;
});
```

### Distributed Azure Cosmos DB cache

[Azure Cosmos DB](/en-us/azure/cosmos-db/overview) can be configured in ASP.NET Core as a session state provider by using the `IDistributedCache` interface. Azure Cosmos DB is a fully managed NoSQL and relational database for modern app development that offers high availability, scalability, and low-latency access to data for mission-critical applications.

After you install the [Microsoft.Extensions.Caching.Cosmos](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Cosmos) NuGet package, configure an Azure Cosmos DB distributed cache. You can use an existing Azure Cosmos DB client or create a new one, as described in the following sections.

For more information, see the [Microsoft Caching Extension using Azure Cosmos DB](https://github.com/Azure/Microsoft.Extensions.Caching.Cosmos/blob/master/README.md), the GitHub repository README file for the NuGet package.

#### Reuse an existing client

The easiest way to configure a distributed cache is by reusing an existing Azure Cosmos DB client. In this case, the `CosmosClient` instance isn't disposed when the provider is disposed.

```
services.AddCosmosCache((CosmosCacheOptions cacheOptions) =>
{
    cacheOptions.ContainerName = Configuration["CosmosCacheContainer"];
    cacheOptions.DatabaseName = Configuration["CosmosCacheDatabase"];
    cacheOptions.CosmosClient = existingCosmosClient;
    cacheOptions.CreateIfNotExists = true;
});
```

#### Create a new client

Alternatively, instantiate a new client. In this case, the `CosmosClient` instance is disposed when the provider is disposed.

```
services.AddCosmosCache((CosmosCacheOptions cacheOptions) =>
{
    cacheOptions.ContainerName = Configuration["CosmosCacheContainer"];
    cacheOptions.DatabaseName = Configuration["CosmosCacheDatabase"];
    cacheOptions.ClientBuilder = new CosmosClientBuilder(Configuration["CosmosConnectionString"]);
    cacheOptions.CreateIfNotExists = true;
});
```

## Use the distributed cache

To use the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface, request an instance of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) in the app. The instance is provided by [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

When the sample app starts, the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) instance is injected into the *Program.cs* file. The current time is cached by using the [IHostApplicationLifetime](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime) interface. (For more information, see [.NET Generic Host: IHostApplicationLifetime](../../fundamentals/host/generic-host?view=aspnetcore-10.0#ihostapplicationlifetime).)

```
app.Lifetime.ApplicationStarted.Register(() =>
{
    var currentTimeUTC = DateTime.UtcNow.ToString();
    byte[] encodedCurrentTimeUTC = System.Text.Encoding.UTF8.GetBytes(currentTimeUTC);
    var options = new DistributedCacheEntryOptions()
        .SetSlidingExpiration(TimeSpan.FromSeconds(20));
    app.Services.GetService<IDistributedCache>()
                              .Set("cachedTimeUTC", encodedCurrentTimeUTC, options);
});
```

The sample app injects the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) instance into the `IndexModel` object for use by the Index page.

Each time the Index page loads, the cache is checked for the cached time by using the `OnGetAsync` method. If the cached time isn't expired, the time is displayed. If 20 seconds elapsed since the last time the cached time was accessed (the last time this page loaded), the page displays the message, *Cached Time Expired*.

Immediately update the cached time to the current time by selecting the **Reset Cached Time** option. This action triggers the `OnPostResetCachedTime` handler method.

```
public class IndexModel : PageModel
{
    private readonly IDistributedCache _cache;

    public IndexModel(IDistributedCache cache)
    {
        _cache = cache;
    }

    public string? CachedTimeUTC { get; set; }
    public string? ASP_Environment { get; set; }

    public async Task OnGetAsync()
    {
        CachedTimeUTC = "Cached Time Expired";
        var encodedCachedTimeUTC = await _cache.GetAsync("cachedTimeUTC");

        if (encodedCachedTimeUTC != null)
        {
            CachedTimeUTC = Encoding.UTF8.GetString(encodedCachedTimeUTC);
        }

        ASP_Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
        if (String.IsNullOrEmpty(ASP_Environment))
        {
            ASP_Environment = "Null, so Production";
        }
    }

    public async Task<IActionResult> OnPostResetCachedTime()
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));
        await _cache.SetAsync("cachedTimeUTC", encodedCurrentTimeUTC, options);

        return RedirectToPage();
    }
}
```

Note

You don't need to use a Singleton or Scoped lifetime for [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) instances with a built-in implementation.

You can also create an [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) instance wherever you might need one instead of using DI. However, creating an instance in code can make your code harder to test and violates the [Explicit Dependencies Principle](/en-us/dotnet/architecture/modern-web-apps-azure/architectural-principles#explicit-dependencies).

## Review cache recommendations

When deciding which implementation of the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface is best for your app, consider the following points:

* Existing infrastructure
* Performance requirements
* Cost
* Team experience

Caching solutions usually rely on in-memory storage to provide fast retrieval of cached data, but memory is a limited resource and costly to expand. Only store commonly used data in a cache.

For most apps, a Redis cache provides higher throughput and lower latency than a SQL Server cache. However, benchmarking is recommended to determine the performance characteristics of caching strategies.

If SQL Server is the distributed cache backing store, and the cache and app data storage/retrieval use the same database, performance can be reduced. The recommended approach is to use a dedicated SQL Server instance for the distributed cache backing store.

## Related content

* [Redis Cache on Azure](/en-us/azure/azure-cache-for-redis/)
* [SQL Database on Azure](/en-us/azure/sql-database/)
* [Azure Database for PostgreSQL](/en-us/azure/postgresql/)
* [ASP.NET Core IDistributedCache Provider for NCache in Web Farms](http://www.alachisoft.com/ncache/aspnet-core-idistributedcache-ncache.html) ([NCache on GitHub](https://github.com/Alachisoft/NCache))
* [Repository README file for Microsoft.Extensions.Caching.Cosmos](https://github.com/Azure/Microsoft.Extensions.Caching.Cosmos/blob/master/README.md)
* [Detect changes with change tokens in ASP.NET Core](../../fundamentals/change-tokens?view=aspnetcore-10.0)
* [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0)
* [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../host-and-deploy/web-farm?view=aspnetcore-10.0)

A distributed cache is a cache shared by multiple app servers, typically maintained as an external service to the app servers that access it. A distributed cache can improve the performance and scalability of an ASP.NET Core app, especially when the app is hosted by a cloud service or a server farm.

A distributed cache has several advantages over other caching scenarios where cached data is stored on individual app servers.

When cached data is distributed, the data:

* Is *coherent* (consistent) across requests to multiple servers.
* Survives server restarts and app deployments.
* Doesn't use local memory.

Distributed cache configuration is implementation specific. This article describes how to configure SQL Server, Redis, and Postgres distributed caches. Third party implementations are also available, such as [NCache](http://www.alachisoft.com/ncache/aspnet-core-idistributedcache-ncache.html) ([NCache on GitHub](https://github.com/Alachisoft/NCache)). Regardless of which implementation is selected, the app interacts with the cache using the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/distributed/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

Add a package reference for the distributed cache provider used:

* For a Redis distributed cache, [Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis).

* For SQL Server, [Microsoft.Extensions.Caching.SqlServer](https://www.nuget.org/packages/Microsoft.Extensions.Caching.SqlServer).

* For Postgres, [Microsoft.Extensions.Caching.Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres).

* For the NCache distributed cache, [NCache.Microsoft.Extensions.Caching.OpenSource](https://www.nuget.org/packages/NCache.Microsoft.Extensions.Caching.OpenSource).

* Warning

  This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

## IDistributedCache interface

The [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface provides the following methods to manipulate items in the distributed cache implementation:

* [Get](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.get), [GetAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.getasync): Accepts a string key and retrieves a cached item as a `byte[]` array if found in the cache.
* [Set](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.set), [SetAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.setasync): Adds an item (as `byte[]` array) to the cache using a string key.
* [Refresh](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.refresh), [RefreshAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.refreshasync): Refreshes an item in the cache based on its key, resetting its sliding expiration timeout (if any).
* [Remove](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.remove), [RemoveAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.removeasync): Removes a cache item based on its string key.

## Establish distributed caching services

Register an implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) in `Program.cs`. Framework-provided implementations described in this topic include:

* [Distributed Redis cache](#distributed-redis-cache)
* [Distributed Memory Cache](#distributed-memory-cache)
* [Distributed SQL Server cache](#distributed-sql-server-cache)
* [Distributed Postgres cache](#distributed-postgres-cache)
* [Distributed NCache cache](#distributed-ncache-cache)

### Distributed Redis Cache

We recommend production apps use the Distributed Redis Cache because it's the most performant. For more information see [Recommendations](#recommendations).

[Redis](https://redis.io/) is an open source in-memory data store, which is often used as a distributed cache. You can configure an [Azure Redis Cache](https://azure.microsoft.com/services/cache/) for an Azure-hosted ASP.NET Core app, and use an Azure Redis Cache for local development.

An app configures the cache implementation using a [RedisCache](/en-us/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscache) instance ([AddStackExchangeRedisCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangerediscacheservicecollectionextensions.addstackexchangerediscache)).

1. Create an Azure Cache for Redis.

2. Copy the Primary connection string (StackExchange.Redis) to [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0).

   * Local development: Save the connection string with [Secret Manager](../../security/app-secrets?view=aspnetcore-10.0#secret-manager).
   * Azure: Save the connection string in a secure store such as [Azure Key Vault](/en-us/azure/key-vault/general/overview)

The following code enables the Azure Cache for Redis:

```
builder.Services.AddStackExchangeRedisCache(options =>
 {
     options.Configuration = builder.Configuration.GetConnectionString("MyRedisConStr");
     options.InstanceName = "SampleInstance";
 });
```

The preceding code assumes the Primary connection string (StackExchange.Redis) was saved in configuration with the key name `MyRedisConStr`.

For more information, see [Azure Cache for Redis](/en-us/azure/azure-cache-for-redis/cache-overview).

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/19542) for a discussion on alternative approaches to a local Redis cache.

### Distributed Memory Cache

The Distributed Memory Cache ([AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache)) is a framework-provided implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) that stores items in memory. The Distributed Memory Cache isn't an actual distributed cache. Cached items are stored by the app instance on the server where the app is running.

The Distributed Memory Cache is a useful implementation:

* In development and testing scenarios.
* When a single server is used in production and memory consumption isn't an issue. Implementing the Distributed Memory Cache abstracts cached data storage. It allows for implementing a true distributed caching solution in the future if multiple nodes or fault tolerance become necessary.

The sample app makes use of the Distributed Memory Cache when the app is run in the `Development` environment in `Program.cs`:

```
builder.Services.AddDistributedMemoryCache();
```

### Distributed SQL Server Cache

The Distributed SQL Server Cache implementation ([AddDistributedSqlServerCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.sqlservercachingservicesextensions.adddistributedsqlservercache)) allows the distributed cache to use a SQL Server database as its backing store. To create a SQL Server cached item table in a SQL Server instance, you can use the `sql-cache` tool. The tool creates a table with the name and schema that you specify.

Create a table in SQL Server by running the `sql-cache create` command. Provide the SQL Server instance (`Data Source`), database (`Initial Catalog`), schema (for example, `dbo`), and table name (for example, `TestCache`):

```
dotnet sql-cache create "Data Source=(localdb)/MSSQLLocalDB;Initial Catalog=DistCache;Integrated Security=True;" dbo TestCache
```

A message is logged to indicate that the tool was successful:

```
Table and index were created successfully.
```

The table created by the `sql-cache` tool has the following schema:

Note

An app should manipulate cache values using an instance of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache), not a [SqlServerCache](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercache).

The sample app implements [SqlServerCache](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercache) in a non-`Development` environment in `Program.cs`:

```
builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString(
        "DistCache_ConnectionString");
    options.SchemaName = "dbo";
    options.TableName = "TestCache";
});
```

Note

A [ConnectionString](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercacheoptions.connectionstring) (and optionally, [SchemaName](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercacheoptions.schemaname) and [TableName](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercacheoptions.tablename)) are typically stored outside of source control (for example, stored by the [Secret Manager](../../security/app-secrets?view=aspnetcore-10.0) or in `appsettings.json`/`appsettings.{Environment}.json` files). The connection string may contain credentials that should be kept out of source control systems.

### Distributed Postgres Cache

[Azure Database for PostgreSQL](/en-us/azure/postgresql) can be used as a distributed cache backing store via the `IDistributedCache` interface. Azure Database for PostgreSQL is a fully managed, AI-ready Database-as-a-Service (DBaaS) offering built on the open-source PostgreSQL engine, designed to support mission-critical workloads with predictable performance, robust security, high availability, and seamless scalability.

After installing the [Microsoft.Extensions.Caching.Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres) NuGet package, configure your distributed cache as follows:

1. Register the Service

```
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Register Postgres distributed cache
builder.Services.AddDistributedPostgresCache(options => {
    options.ConnectionString = builder.Configuration.GetConnectionString("PostgresCache");
    options.SchemaName = builder.Configuration.GetValue<string>("PostgresCache:SchemaName", "public");
    options.TableName = builder.Configuration.GetValue<string>("PostgresCache:TableName", "cache");
    options.CreateIfNotExists = builder.Configuration.GetValue<bool>("PostgresCache:CreateIfNotExists", true);
    options.UseWAL = builder.Configuration.GetValue<bool>("PostgresCache:UseWAL", false);

    // Optional: Configure expiration settings

    var expirationInterval = builder.Configuration.GetValue<string>("PostgresCache:ExpiredItemsDeletionInterval");
    if (!string.IsNullOrEmpty(expirationInterval) && TimeSpan.TryParse(expirationInterval, out var interval)) {
        options.ExpiredItemsDeletionInterval = interval;
    }

    var slidingExpiration = builder.Configuration.GetValue<string>("PostgresCache:DefaultSlidingExpiration");
    if (!string.IsNullOrEmpty(slidingExpiration) && TimeSpan.TryParse(slidingExpiration, out var sliding)) {
        options.DefaultSlidingExpiration = sliding;
    }
});

var app = builder.Build();
```

2. Use the Cache

```
public class MyService {
    private readonly IDistributedCache _cache; 

    public MyService(IDistributedCache cache) {
        _cache = cache;
    }

    public async Task<string> GetDataAsync(string key) {
        var cachedData = await _cache.GetStringAsync(key);

        if (cachedData == null) {
            // Fetch data from source
            var data = await FetchDataFromSource();

            // Cache the data with options
            var options = new DistributedCacheEntryOptions {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
                SlidingExpiration = TimeSpan.FromMinutes(5)
            };

            await _cache.SetStringAsync(key, data, options);
            return data;
        }

        return cachedData;
    }
}
```

### Distributed NCache Cache

[NCache](https://github.com/Alachisoft/NCache) is an open source in-memory distributed cache developed natively in .NET and .NET Core. NCache works both locally and configured as a distributed cache cluster for an ASP.NET Core app running in Azure or on other hosting platforms.

To install and configure NCache on your local machine, see [Getting Started Guide for Windows (.NET and .NET Core)](https://www.alachisoft.com/resources/docs/ncache/getting-started-guide-windows/).

To configure NCache:

1. Install [NCache open source NuGet](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/).
2. Configure the cache cluster in [client.ncconf](https://www.alachisoft.com/resources/docs/ncache/admin-guide/client-config.html).
3. Add the following code to `Program.cs`:

```
builder.Services.AddNCacheDistributedCache(configuration =>
{
    configuration.CacheName = "democache";
    configuration.EnableLogs = true;
    configuration.ExceptionsEnabled = true;
});
```

## Use the distributed cache

To use the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface, request an instance of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) in the app. The instance is provided by [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

When the sample app starts, [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) is injected into `Program.cs`. The current time is cached using [IHostApplicationLifetime](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime) (for more information, see [Generic Host: IHostApplicationLifetime](../../fundamentals/host/generic-host?view=aspnetcore-10.0#ihostapplicationlifetime)):

```
app.Lifetime.ApplicationStarted.Register(() =>
{
    var currentTimeUTC = DateTime.UtcNow.ToString();
    byte[] encodedCurrentTimeUTC = System.Text.Encoding.UTF8.GetBytes(currentTimeUTC);
    var options = new DistributedCacheEntryOptions()
        .SetSlidingExpiration(TimeSpan.FromSeconds(20));
    app.Services.GetService<IDistributedCache>()
                              .Set("cachedTimeUTC", encodedCurrentTimeUTC, options);
});
```

The sample app injects [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) into the `IndexModel` for use by the Index page.

Each time the Index page is loaded, the cache is checked for the cached time in `OnGetAsync`. If the cached time hasn't expired, the time is displayed. If 20 seconds have elapsed since the last time the cached time was accessed (the last time this page was loaded), the page displays *Cached Time Expired*.

Immediately update the cached time to the current time by selecting the **Reset Cached Time** button. The button triggers the `OnPostResetCachedTime` handler method.

```
public class IndexModel : PageModel
{
    private readonly IDistributedCache _cache;

    public IndexModel(IDistributedCache cache)
    {
        _cache = cache;
    }

    public string? CachedTimeUTC { get; set; }
    public string? ASP_Environment { get; set; }

    public async Task OnGetAsync()
    {
        CachedTimeUTC = "Cached Time Expired";
        var encodedCachedTimeUTC = await _cache.GetAsync("cachedTimeUTC");

        if (encodedCachedTimeUTC != null)
        {
            CachedTimeUTC = Encoding.UTF8.GetString(encodedCachedTimeUTC);
        }

        ASP_Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
        if (String.IsNullOrEmpty(ASP_Environment))
        {
            ASP_Environment = "Null, so Production";
        }
    }

    public async Task<IActionResult> OnPostResetCachedTime()
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));
        await _cache.SetAsync("cachedTimeUTC", encodedCurrentTimeUTC, options);

        return RedirectToPage();
    }
}
```

> There's ***no*** need to use a Singleton or Scoped lifetime for [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) instances with the built-in implementations.
>
> You can also create an [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) instance wherever you might need one instead of using DI, but creating an instance in code can make your code harder to test and violates the [Explicit Dependencies Principle](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#explicit-dependencies).

## Recommendations

When deciding which implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) is best for your app, consider the following:

* Existing infrastructure
* Performance requirements
* Cost
* Team experience

Caching solutions usually rely on in-memory storage to provide fast retrieval of cached data, but memory is a limited resource and costly to expand. Only store commonly used data in a cache.

For most apps, a Redis cache provides higher throughput and lower latency than a SQL Server cache. However, benchmarking is recommended to determine the performance characteristics of caching strategies.

When SQL Server is used as a distributed cache backing store, use of the same database for the cache and the app's ordinary data storage and retrieval can negatively impact the performance of both. We recommend using a dedicated SQL Server instance for the distributed cache backing store.

## Related content

* [Redis Cache on Azure](/en-us/azure/azure-cache-for-redis/)
* [SQL Database on Azure](/en-us/azure/sql-database/)
* [Azure Database for PostgreSQL](/en-us/azure/postgresql/)
* [ASP.NET Core IDistributedCache Provider for NCache in Web Farms](http://www.alachisoft.com/ncache/aspnet-core-idistributedcache-ncache.html) ([NCache on GitHub](https://github.com/Alachisoft/NCache))
* [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0)
* [Detect changes with change tokens in ASP.NET Core](../../fundamentals/change-tokens?view=aspnetcore-10.0)
* [Response caching in ASP.NET Core](response?view=aspnetcore-10.0)
* [Response caching middleware in ASP.NET Core](middleware?view=aspnetcore-10.0)
* [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0)
* [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../host-and-deploy/web-farm?view=aspnetcore-10.0)

A distributed cache is a cache shared by multiple app servers, typically maintained as an external service to the app servers that access it. A distributed cache can improve the performance and scalability of an ASP.NET Core app, especially when the app is hosted by a cloud service or a server farm.

A distributed cache has several advantages over other caching scenarios where cached data is stored on individual app servers.

When cached data is distributed, the data:

* Is *coherent* (consistent) across requests to multiple servers.
* Survives server restarts and app deployments.
* Doesn't use local memory.

Distributed cache configuration is implementation specific. This article describes how to configure SQL Server, Redis, and Postgres distributed caches. Third party implementations are also available, such as [NCache](http://www.alachisoft.com/ncache/aspnet-core-idistributedcache-ncache.html) ([NCache on GitHub](https://github.com/Alachisoft/NCache)). Regardless of which implementation is selected, the app interacts with the cache using the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/distributed/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

To use a SQL Server distributed cache, add a package reference to the [Microsoft.Extensions.Caching.SqlServer](https://www.nuget.org/packages/Microsoft.Extensions.Caching.SqlServer) package.

To use a Redis distributed cache, add a package reference to the [Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis) package.

To use a Postgres distributed cache, add a package reference to the [Microsoft.Extensions.Caching.Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres) package.

To use NCache distributed cache, add a package reference to the [NCache.Microsoft.Extensions.Caching.OpenSource](https://www.nuget.org/packages/NCache.Microsoft.Extensions.Caching.OpenSource) package.

## IDistributedCache interface

The [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface provides the following methods to manipulate items in the distributed cache implementation:

* [Get](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.get), [GetAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.getasync): Accepts a string key and retrieves a cached item as a `byte[]` array if found in the cache.
* [Set](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.set), [SetAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.setasync): Adds an item (as `byte[]` array) to the cache using a string key.
* [Refresh](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.refresh), [RefreshAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.refreshasync): Refreshes an item in the cache based on its key, resetting its sliding expiration timeout (if any).
* [Remove](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.remove), [RemoveAsync](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache.removeasync): Removes a cache item based on its string key.

## Establish distributed caching services

Register an implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) in `Startup.ConfigureServices`. Framework-provided implementations described in this topic include:

* [Distributed Memory Cache](#distributed-memory-cache)
* [Distributed SQL Server cache](#distributed-sql-server-cache)
* [Distributed Redis cache](#distributed-redis-cache)
* [Distributed Postgres cache](#distributed-postgres-cache)
* [Distributed NCache cache](#distributed-ncache-cache)

### Distributed Memory Cache

The Distributed Memory Cache ([AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache)) is a framework-provided implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) that stores items in memory. The Distributed Memory Cache isn't an actual distributed cache. Cached items are stored by the app instance on the server where the app is running.

The Distributed Memory Cache is a useful implementation:

* In development and testing scenarios.
* When a single server is used in production and memory consumption isn't an issue. Implementing the Distributed Memory Cache abstracts cached data storage. It allows for implementing a true distributed caching solution in the future if multiple nodes or fault tolerance become necessary.

The sample app makes use of the Distributed Memory Cache when the app is run in the `Development` environment in `Startup.ConfigureServices`:

```
services.AddDistributedMemoryCache();
```

### Distributed SQL Server Cache

The Distributed SQL Server Cache implementation ([AddDistributedSqlServerCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.sqlservercachingservicesextensions.adddistributedsqlservercache)) allows the distributed cache to use a SQL Server database as its backing store. To create a SQL Server cached item table in a SQL Server instance, you can use the `sql-cache` tool. The tool creates a table with the name and schema that you specify.

Create a table in SQL Server by running the `sql-cache create` command. Provide the SQL Server instance (`Data Source`), database (`Initial Catalog`), schema (for example, `dbo`), and table name (for example, `TestCache`):

```
dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=DistCache;Integrated Security=True;" dbo TestCache
```

A message is logged to indicate that the tool was successful:

```
Table and index were created successfully.
```

The table created by the `sql-cache` tool has the following schema:

Note

An app should manipulate cache values using an instance of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache), not a [SqlServerCache](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercache).

The sample app implements [SqlServerCache](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercache) in a non-`Development` environment in `Startup.ConfigureServices`:

```
services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString =
        _config["DistCache_ConnectionString"];
    options.SchemaName = "dbo";
    options.TableName = "TestCache";
});
```

Note

A [ConnectionString](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercacheoptions.connectionstring) (and optionally, [SchemaName](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercacheoptions.schemaname) and [TableName](/en-us/dotnet/api/microsoft.extensions.caching.sqlserver.sqlservercacheoptions.tablename)) are typically stored outside of source control (for example, stored by the [Secret Manager](../../security/app-secrets?view=aspnetcore-10.0) or in `appsettings.json`/`appsettings.{Environment}.json` files). The connection string may contain credentials that should be kept out of source control systems.

### Distributed Redis Cache

[Redis](https://redis.io/) is an open source in-memory data store, which is often used as a distributed cache. You can configure an [Azure Redis Cache](https://azure.microsoft.com/services/cache/) for an Azure-hosted ASP.NET Core app, and use an Azure Redis Cache for local development.

An app configures the cache implementation using a [RedisCache](/en-us/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscache) instance ([AddStackExchangeRedisCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.stackexchangerediscacheservicecollectionextensions.addstackexchangerediscache)).

1. Create an Azure Cache for Redis.

2. Copy the Primary connection string (StackExchange.Redis) to [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0).

   * Local development: Save the connection string with [Secret Manager](../../security/app-secrets?view=aspnetcore-10.0#secret-manager).
   * Azure: Save the connection string in a secure store such as [Azure Key Vault](/en-us/azure/key-vault/general/overview)

The following code enables the Azure Cache for Redis:

```
public void ConfigureServices(IServiceCollection services)
{
    if (_hostContext.IsDevelopment())
    {
        services.AddDistributedMemoryCache();
    }
    else
    {
        services.AddStackExchangeRedisCache(options =>
        {
            options.Configuration = _config["MyRedisConStr"];
            options.InstanceName = "SampleInstance";
        });
    }

    services.AddRazorPages();
}
```

The preceding code assumes the Primary connection string (StackExchange.Redis) was saved in configuration with the key name `MyRedisConStr`.

For more information, see [Azure Cache for Redis](/en-us/azure/azure-cache-for-redis/cache-overview).

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/19542) for a discussion on alternative approaches to a local Redis cache.

### Distributed Postgres Cache

[Azure Database for PostgreSQL](/en-us/azure/postgresql) can be used as a distributed cache backing store via the `IDistributedCache` interface. Azure Database for PostgreSQL is a fully managed, AI-ready Database-as-a-Service (DBaaS) offering built on the open-source PostgreSQL engine, designed to support mission-critical workloads with predictable performance, robust security, high availability, and seamless scalability.

After installing the [Microsoft.Extensions.Caching.Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres) NuGet package, configure your distributed cache as follows:

1. Register the Service

```
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Register Postgres distributed cache
builder.Services.AddDistributedPostgresCache(options => {
    options.ConnectionString = builder.Configuration.GetConnectionString("PostgresCache");
    options.SchemaName = builder.Configuration.GetValue<string>("PostgresCache:SchemaName", "public");
    options.TableName = builder.Configuration.GetValue<string>("PostgresCache:TableName", "cache");
    options.CreateIfNotExists = builder.Configuration.GetValue<bool>("PostgresCache:CreateIfNotExists", true);
    options.UseWAL = builder.Configuration.GetValue<bool>("PostgresCache:UseWAL", false);

    // Optional: Configure expiration settings

    var expirationInterval = builder.Configuration.GetValue<string>("PostgresCache:ExpiredItemsDeletionInterval");
    if (!string.IsNullOrEmpty(expirationInterval) && TimeSpan.TryParse(expirationInterval, out var interval)) {
        options.ExpiredItemsDeletionInterval = interval;
    }

    var slidingExpiration = builder.Configuration.GetValue<string>("PostgresCache:DefaultSlidingExpiration");
    if (!string.IsNullOrEmpty(slidingExpiration) && TimeSpan.TryParse(slidingExpiration, out var sliding)) {
        options.DefaultSlidingExpiration = sliding;
    }
});

var app = builder.Build();
```

2. Use the Cache

```
public class MyService {
    private readonly IDistributedCache _cache; 

    public MyService(IDistributedCache cache) {
        _cache = cache;
    }

    public async Task<string> GetDataAsync(string key) {
        var cachedData = await _cache.GetStringAsync(key);

        if (cachedData == null) {
            // Fetch data from source
            var data = await FetchDataFromSource();

            // Cache the data with options
            var options = new DistributedCacheEntryOptions {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
                SlidingExpiration = TimeSpan.FromMinutes(5)
            };

            await _cache.SetStringAsync(key, data, options);
            return data;
        }

        return cachedData;
    }
}
```

### Distributed NCache Cache

[NCache](https://github.com/Alachisoft/NCache) is an open source in-memory distributed cache developed natively in .NET and .NET Core. NCache works both locally and configured as a distributed cache cluster for an ASP.NET Core app running in Azure or on other hosting platforms.

To install and configure NCache on your local machine, see [Getting Started Guide for Windows (.NET and .NET Core)](https://www.alachisoft.com/resources/docs/ncache/getting-started-guide-windows/).

To configure NCache:

1. Install [NCache open source NuGet](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/).

2. Configure the cache cluster in [client.ncconf](https://www.alachisoft.com/resources/docs/ncache/admin-guide/client-config.html).

3. Add the following code to `Startup.ConfigureServices`:

   ```
   services.AddNCacheDistributedCache(configuration =>    
   {        
       configuration.CacheName = "demoClusteredCache";
       configuration.EnableLogs = true;
       configuration.ExceptionsEnabled = true;
   });
   ```

## Use the distributed cache

To use the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface, request an instance of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) from any constructor in the app. The instance is provided by [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

When the sample app starts, [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) is injected into `Startup.Configure`. The current time is cached using [IHostApplicationLifetime](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime) (for more information, see [Generic Host: IHostApplicationLifetime](../../fundamentals/host/generic-host?view=aspnetcore-10.0#ihostapplicationlifetime)):

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
    IHostApplicationLifetime lifetime, IDistributedCache cache)
{
    lifetime.ApplicationStarted.Register(() =>
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));
        cache.Set("cachedTimeUTC", encodedCurrentTimeUTC, options);
    });
```

The sample app injects [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) into the `IndexModel` for use by the Index page.

Each time the Index page is loaded, the cache is checked for the cached time in `OnGetAsync`. If the cached time hasn't expired, the time is displayed. If 20 seconds have elapsed since the last time the cached time was accessed (the last time this page was loaded), the page displays *Cached Time Expired*.

Immediately update the cached time to the current time by selecting the **Reset Cached Time** button. The button triggers the `OnPostResetCachedTime` handler method.

```
public class IndexModel : PageModel
{
    private readonly IDistributedCache _cache;

    public IndexModel(IDistributedCache cache)
    {
        _cache = cache;
    }

    public string CachedTimeUTC { get; set; }

    public async Task OnGetAsync()
    {
        CachedTimeUTC = "Cached Time Expired";
        var encodedCachedTimeUTC = await _cache.GetAsync("cachedTimeUTC");

        if (encodedCachedTimeUTC != null)
        {
            CachedTimeUTC = Encoding.UTF8.GetString(encodedCachedTimeUTC);
        }
    }

    public async Task<IActionResult> OnPostResetCachedTime()
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));
        await _cache.SetAsync("cachedTimeUTC", encodedCurrentTimeUTC, options);

        return RedirectToPage();
    }
}
```

Note

There's no need to use a Singleton or Scoped lifetime for [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) instances (at least for the built-in implementations).

You can also create an [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) instance wherever you might need one instead of using DI, but creating an instance in code can make your code harder to test and violates the [Explicit Dependencies Principle](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#explicit-dependencies).

## Recommendations

When deciding which implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) is best for your app, consider the following:

* Existing infrastructure
* Performance requirements
* Cost
* Team experience

Caching solutions usually rely on in-memory storage to provide fast retrieval of cached data, but memory is a limited resource and costly to expand. Only store commonly used data in a cache.

Generally, a Redis cache provides higher throughput and lower latency than a SQL Server cache. However, benchmarking is usually required to determine the performance characteristics of caching strategies.

When SQL Server is used as a distributed cache backing store, use of the same database for the cache and the app's ordinary data storage and retrieval can negatively impact the performance of both. We recommend using a dedicated SQL Server instance for the distributed cache backing store.

## Related content

* [Redis Cache on Azure](/en-us/azure/azure-cache-for-redis/)
* [SQL Database on Azure](/en-us/azure/sql-database/)
* [Azure Database for PostgreSQL](/en-us/azure/postgresql/)
* [ASP.NET Core IDistributedCache Provider for NCache in Web Farms](http://www.alachisoft.com/ncache/aspnet-core-idistributedcache-ncache.html) ([NCache on GitHub](https://github.com/Alachisoft/NCache))
* [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0)
* [Detect changes with change tokens in ASP.NET Core](../../fundamentals/change-tokens?view=aspnetcore-10.0)
* [Response caching in ASP.NET Core](response?view=aspnetcore-10.0)
* [Response caching middleware in ASP.NET Core](middleware?view=aspnetcore-10.0)
* [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0)
* [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../host-and-deploy/web-farm?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-6.0
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-6.0.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# What's new in ASP.NET Core in .NET 6

This article highlights the most significant changes in ASP.NET Core in .NET 6 with links to relevant documentation.

## ASP.NET Core MVC and Razor improvements

## Minimal APIs

Minimal APIs are architected to create HTTP APIs with minimal dependencies. They are ideal for microservices and apps that want to include only the minimum files, features, and dependencies in ASP.NET Core. For more information, see:

* [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0)
* [Differences between Minimal APIs and APIs with controllers](../tutorials/min-web-api?view=aspnetcore-6.0#differences-between-minimal-apis-and-apis-with-controllers)
* [Minimal APIs quick reference](../fundamentals/minimal-apis?view=aspnetcore-10.0)
* [Code samples migrated to the new minimal hosting model in 6.0](../migration/50-to-60-samples?view=aspnetcore-10.0)

## SignalR

### Long running activity tag for SignalR connections

SignalR uses the new [Microsoft.AspNetCore.Http.Features.IHttpActivityFeature.Activity](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpactivityfeature.activity#microsoft-aspnetcore-http-features-ihttpactivityfeature-activity) to add an `http.long_running` tag to the request activity. `IHttpActivityFeature.Activity` is used by [APM services](https://wikipedia.org/wiki/Application_performance_management) like [Azure Monitor Application Insights](/en-us/azure/azure-monitor/azure-monitor-app-hub) to filter SignalR requests from creating long running request alerts.

### SignalR performance improvements

* Allocate [HubCallerClients](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/server/Core/src/Internal/HubCallerClients.cs) once per connection instead of every hub method call.
* Avoid closure allocation in SignalR `DefaultHubDispatcher.Invoke`. State is passed to a local static function via parameters to avoid a closure allocation. For more information, see [this GitHub pull request](https://github.com/dotnet/aspnetcore/pull/31641).
* Allocate a single [StreamItemMessage](/en-us/dotnet/api/microsoft.aspnetcore.signalr.protocol.streamitemmessage) per stream instead of per stream item in server-to-client streaming. For more information, see [this GitHub pull request](https://github.com/dotnet/aspnetcore/pull/31660).

## Razor compiler

### Razor compiler updated to use source generators

The Razor compiler is now based on [C# source generators](/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview). Source generators run during compilation and inspect what is being compiled to produce additional files that are compiled along with the rest of the project. Using source generators simplifies the Razor compiler and significantly speeds up build times.

### Razor compiler no longer produces a separate Views assembly

The Razor compiler previously utilized a two-step compilation process that produced a separate Views assembly that contained the generated views and pages (`.cshtml` files) defined in the app. The generated types were public and under the `AspNetCore` namespace.

The updated Razor compiler builds the views and pages types into the main project assembly. These types are now generated by default as internal sealed in the `AspNetCoreGeneratedDocument` namespace. This change improves build performance, enables single file deployment, and enables these types to participate in [Hot Reload](../test/hot-reload?view=aspnetcore-10.0).

For more information about this change, see [the related announcement issue](https://github.com/aspnet/Announcements/issues/459) on GitHub.

[]()

## ASP.NET Core performance and API improvements

Many changes were made to reduce allocations and improve performance across the stack:

* Non-allocating [app.Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder.use) extension method. The new overload of `app.Use` requires passing the context to `next` which saves two internal per-request allocations that are required when using the other overload.
* Reduced memory allocations when accessing [HttpRequest.Cookies](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.cookies). For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/10030).
* Use [LoggerMessage.Define](/en-us/dotnet/api/microsoft.extensions.logging.loggermessage.define) for the windows only [HTTP.sys web server](../fundamentals/servers/httpsys?view=aspnetcore-10.0). The [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) extension methods calls have been replaced with calls to `LoggerMessage.Define`.
* Reduce the per connection overhead in [SocketConnection](https://github.com/dotnet/aspnetcore/blob/main/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs) by \~30%. For more information, see [this GitHub pull request](https://github.com/dotnet/aspnetcore/pull/31308).
* Reduce allocations by removing logging delegates in generic types. For more information, see [this GitHub pull request](https://github.com/dotnet/aspnetcore/issues/31340).
* Faster GET access (about 50%) to commonly-used features such as [IHttpRequestFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttprequestfeature), [IHttpResponseFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpresponsefeature), [IHttpResponseBodyFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpresponsebodyfeature), [IRouteValuesFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iroutevaluesfeature), and [IEndpointFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.iendpointfeature). For more information, see [this GitHub pull request](https://github.com/dotnet/aspnetcore/pull/31322).
* Use single instance strings for known header names, even if they aren't in the preserved header block. Using single instance string helps prevent multiple duplicates of the same string in long lived connections, for example, in [Microsoft.AspNetCore.WebSockets](/en-us/dotnet/api/microsoft.aspnetcore.websockets). For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/31305).
* Reuse [HttpProtocol CancellationTokenSource](https://github.com/dotnet/aspnetcore/blob/v6.0.0/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs#L401-L409) in Kestrel. Use the new [CancellationTokenSource.TryReset](/en-us/dotnet/api/system.threading.cancellationtokensource.tryreset) method on `CancellationTokenSource` to reuse tokens if they haven't been canceled. For more information, see [this GitHub issue](https://github.com/dotnet/runtime/issues/48492) and this [video](https://www.youtube.com/watch?v=vNPybpatlUU\&t=1h38m28s).
* Implement and use an [AdaptiveCapacityDictionary](https://github.com/dotnet/aspnetcore/blob/v6.0.0/src/Http/Http/src/Internal/RequestCookieCollection.cs#L24) in [Microsoft.AspNetCore.Http](/en-us/dotnet/api/microsoft.aspnetcore.http) [RequestCookieCollection](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/Internal/RequestCookieCollection.cs) for more efficient access to dictionaries. For more information, see [this GitHub pull request](https://github.com/dotnet/aspnetcore/pull/31360).

### Reduced memory footprint for idle TLS connections

For long running TLS connections where data is only occasionally sent back and forth, we've significantly reduced the memory footprint of ASP.NET Core apps in .NET 6. This should help improve the scalability of scenarios such as WebSocket servers. This was possible due to numerous improvements in [System.IO.Pipelines](/en-us/dotnet/api/system.io.pipelines), [SslStream](/en-us/dotnet/api/system.net.security.sslstream), and Kestrel. The following sections detail some of the improvements that have contributed to the reduced memory footprint:

#### Reduce the size of `System.IO.Pipelines.Pipe`

For every connection that is established, two pipes are allocated in Kestrel:

* The transport layer to the app for the request.
* The application layer to the transport for the response.

By shrinking the size of [System.IO.Pipelines.Pipe](/en-us/dotnet/api/system.io.pipelines.pipe) from 368 bytes to 264 bytes (about a 28.2% reduction), 208 bytes per connection are saved (104 bytes per Pipe).

#### Pool SocketSender

`SocketSender` objects (that subclass [SocketAsyncEventArgs](/en-us/dotnet/api/system.net.sockets.socketasynceventargs)) are around 350 bytes at runtime. Instead of allocating a new `SocketSender` object per connection, they can be pooled. `SocketSender` objects can be pooled because sends are usually very fast. Pooling reduces the per connection overhead. Instead of allocating 350 bytes per connection, only pay 350 bytes per `IOQueue` are allocated. Allocation is done per queue to avoid contention. Our WebSocket server with 5000 idle connections went from allocating \~1.75 MB (350 bytes \* 5000) to allocating \~2.8 kb (350 bytes \* 8) for `SocketSender` objects.

#### Zero bytes reads with SslStream

Bufferless reads are a technique employed in ASP.NET Core to avoid renting memory from the memory pool if there's no data available on the socket. Prior to this change, our WebSocket server with 5000 idle connections required \~200 MB without TLS compared to \~800 MB with TLS. Some of these allocations (4k per connection) were from Kestrel having to hold on to an [ArrayPool\<T>](/en-us/dotnet/api/system.buffers.arraypool-1) buffer while waiting for the reads on [SslStream](/en-us/dotnet/api/system.net.security.sslstream) to complete. Given that these connections were idle, none of reads completed and returned their buffers to the `ArrayPool`, forcing the `ArrayPool` to allocate more memory. The remaining allocations were in `SslStream` itself: 4k buffer for TLS handshakes and 32k buffer for normal reads. In .NET 6, when the user performs a zero byte read on `SslStream` and it has no data available, `SslStream` internally performs a zero-byte read on the underlying wrapped stream. In the best case (idle connection), these changes result in a savings of 40 Kb per connection while still allowing the consumer (Kestrel) to be notified when data is available without holding on to any unused buffers.

#### Zero byte reads with PipeReader

With bufferless reads supported on `SslStream`, an option was added to perform zero byte reads to `StreamPipeReader`, the internal type that adapts a `Stream` into a `PipeReader`. In Kestrel, a`StreamPipeReader` is used to adapt the underlying `SslStream` into a `PipeReader`. Therefore it was necessary to expose these zero byte read semantics on the `PipeReader`.

A `PipeReader` can now be created that supports zero bytes reads over any underlying `Stream` that supports zero byte read semantics (e.g,. `SslStream`, [NetworkStream](/en-us/dotnet/api/system.net.sockets.networkstream), etc) using the following API:

```
var reader = PipeReader.Create(stream, new StreamPipeReaderOptions(useZeroByteReads: true));
```

#### Remove slabs from the `SlabMemoryPool`

To reduce fragmentation of the heap, Kestrel employed a technique where it allocated slabs of memory of 128 KB as part of its memory pool. The slabs were then further divided into 4 KB blocks that were used by Kestrel internally. The slabs had to be larger than 85 KB to force allocation on the large object heap to try and prevent the GC from relocating this array. However, with the introduction of the new GC generation, [Pinned Object Heap](https://devblogs.microsoft.com/dotnet/internals-of-the-poh/) (POH), it no longer makes sense to allocate blocks on slab. Kestrel now directly allocates blocks on the POH, reducing the complexity involved in managing the memory pool. This change should make easier to perform future improvements such as making it easier to shrink the memory pool used by Kestrel.

### IAsyncDisposable supported

[IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable) is now available for controllers, Razor Pages, and View Components. Asynchronous versions have been added to the relevant interfaces in factories and activators:

* The new methods offer a default interface implementation that delegates to the synchronous version and calls [Dispose](/en-us/dotnet/api/system.idisposable.dispose).
* The implementations override the default implementation and handle disposing `IAsyncDisposable` implementations.
* The implementations favor `IAsyncDisposable` over `IDisposable` when both interfaces are implemented.
* Extenders must override the new methods included to support `IAsyncDisposable` instances.

`IAsyncDisposable` is beneficial when working with:

* Asynchronous enumerators, for example, in asynchronous streams.
* Unmanaged resources that have resource-intensive I/O operations to release.

When implementing this interface, use the `DisposeAsync` method to release resources.

Consider a controller that creates and uses a [Utf8JsonWriter](/en-us/dotnet/api/system.text.json.utf8jsonwriter). `Utf8JsonWriter` is an `IAsyncDisposable` resource:

```
public class HomeController : Controller, IAsyncDisposable
{
    private Utf8JsonWriter? _jsonWriter;
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
        _jsonWriter = new Utf8JsonWriter(new MemoryStream());
    }
```

`IAsyncDisposable` must implement `DisposeAsync`:

```
public async ValueTask DisposeAsync()
{
    if (_jsonWriter is not null)
    {
        await _jsonWriter.DisposeAsync();
    }

    _jsonWriter = null;
}
```

### Vcpkg port for SignalR C++ client

[Vcpkg](https://github.com/microsoft/vcpkg) is a cross-platform command-line package manager for C and C++ libraries. We've recently added a port to `vcpkg` to add `CMake` native support for the SignalR C++ client. `vcpkg` also works with MSBuild.

The SignalR client can be added to a CMake project with the following snippet when the vcpkg is included in the toolchain file:

```
find_package(microsoft-signalr CONFIG REQUIRED)
link_libraries(microsoft-signalr::microsoft-signalr)
```

With the preceding snippet, the SignalR C++ client is ready to use [`#include`](https://github.com/halter73/SignalR-Client-Cpp-Sample/blob/main/sample.cpp#L3-L5) and used in a project without any additional configuration. For a complete example of a C++ application that utilizes the SignalR C++ client, see the [halter73/SignalR-Client-Cpp-Sample](https://github.com/halter73/SignalR-Client-Cpp-Sample) repository.

## Blazor

### Project template changes

Several project template changes were made for Blazor apps, including the use of the `Pages/_Layout.cshtml` file for layout content that appeared in the `_Host.cshtml` file for earlier Blazor Server apps. Study the changes by creating an app from a 6.0 project template or accessing the ASP.NET Core reference source for the project templates:

* [Blazor Server](https://github.com/dotnet/aspnetcore/tree/v6.0.0/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp)
* [Blazor WebAssembly](https://github.com/dotnet/aspnetcore/tree/v6.0.0/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp)

### Blazor WebAssembly native dependencies support

Blazor WebAssembly apps can use native dependencies built to run on WebAssembly. For more information, see [ASP.NET Core Blazor WebAssembly native dependencies](../blazor/webassembly-native-dependencies?view=aspnetcore-10.0).

### WebAssembly Ahead-of-time (AOT) compilation and runtime relinking

Blazor WebAssembly supports ahead-of-time (AOT) compilation, where you can compile your .NET code directly into WebAssembly. AOT compilation results in runtime performance improvements at the expense of a larger app size. Relinking the .NET WebAssembly runtime trims unused runtime code and thus improves download speed. For more information, see [Ahead-of-time (AOT) compilation](../blazor/host-and-deploy/webassembly/?view=aspnetcore-6.0#ahead-of-time-aot-compilation) and [Runtime relinking](../blazor/webassembly-build-tools-and-aot?view=aspnetcore-6.0#runtime-relinking).

### Persist prerendered state

Blazor supports persisting state in a prerendered page so that the state doesn't need to be recreated when the app is fully loaded. For more information, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../blazor/components/integration?pivots=server\&view=aspnetcore-6.0#persist-prerendered-state).

### Error boundaries

Error boundaries provide a convenient approach for handling exceptions on the UI level. For more information, see [Handle errors in ASP.NET Core Blazor apps](../blazor/fundamentals/handle-errors?pivots=server\&view=aspnetcore-6.0#error-boundaries).

### SVG support

The [`<foreignObject>` element](https://developer.mozilla.org/docs/Web/SVG/Element/foreignObject) element is supported to display arbitrary HTML within an SVG. For more information, see [ASP.NET Core Razor components](../blazor/components/?view=aspnetcore-6.0#scalable-vector-graphics-svg-images).

### Blazor Server support for byte array transfer in JS Interop

Blazor supports optimized byte array JS interop that avoids encoding and decoding byte arrays into Base64. For more information, see the following resources:

* [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0#byte-array-support)
* [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](../blazor/javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-6.0#byte-array-support)

### Query string enhancements

Support for working with query strings is improved. For more information, see [ASP.NET Core Blazor navigation](../blazor/fundamentals/navigation?view=aspnetcore-6.0#query-strings).

### Binding to select multiple

Binding supports multiple option selection with `<input>` elements. For more information, see the following resources:

* [ASP.NET Core Blazor data binding](../blazor/components/data-binding?view=aspnetcore-6.0#multiple-option-selection-with-input-elements)
* [ASP.NET Core Blazor input components](../blazor/forms/input-components?view=aspnetcore-6.0#multiple-option-selection-with-the-inputselect-component)

### Head (`<head>`) content control

Razor components can modify the HTML `<head>` element content of a page, including setting the page's title (`<title>` element) and modifying metadata (`<meta>` elements). For more information, see [Control `<head>` content in ASP.NET Core Blazor apps](../blazor/components/control-head-content?view=aspnetcore-6.0).

### Generate Angular and React components

Generate framework-specific JavaScript components from Razor components for web frameworks, such as Angular or React. For more information, see [ASP.NET Core Razor components](../blazor/components/?view=aspnetcore-6.0#generate-angular-and-react-components).

### Render components from JavaScript

Render Razor components dynamically from JavaScript for existing JavaScript apps. For more information, see [ASP.NET Core Razor components](../blazor/components/?view=aspnetcore-6.0#render-razor-components-from-javascript).

### Custom elements

Experimental support is available for building custom elements, which use standard HTML interfaces. For more information, see [ASP.NET Core Razor components](../blazor/components/?view=aspnetcore-6.0#blazor-custom-elements).

### Infer component generic types from ancestor components

An ancestor component can cascade a type parameter by name to descendants using the new `[CascadingTypeParameter]` attribute. For more information, see [ASP.NET Core Razor components](../blazor/components/?view=aspnetcore-6.0#cascaded-generic-type-support).

### Dynamically rendered components

Use the new built-in `DynamicComponent` component to render components by type. For more information, see [Dynamically-rendered ASP.NET Core Razor components](../blazor/components/dynamiccomponent?view=aspnetcore-6.0).

### Improved Blazor accessibility

Use the new `FocusOnNavigate` component to set the UI focus to an element based on a CSS selector after navigating from one page to another. For more information, see [ASP.NET Core Blazor routing](../blazor/fundamentals/routing?view=aspnetcore-6.0#focus-an-element-on-navigation).

### Custom event argument support

Blazor supports custom event arguments, which enable you to pass arbitrary data to .NET event handlers with custom events. For more information, see [ASP.NET Core Blazor event handling](../blazor/components/event-handling?view=aspnetcore-6.0#custom-event-arguments).

### Required parameters

Apply the new `[EditorRequired]` attribute to specify a required component parameter. For more information, see [ASP.NET Core Razor components](../blazor/components/?view=aspnetcore-6.0#component-parameters).

### Collocation of JavaScript files with pages, views, and components

Collocate JavaScript files for pages, views, and Razor components as a convenient way to organize scripts in an app. For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](../blazor/javascript-interoperability/?view=aspnetcore-6.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component).

### JavaScript initializers

JavaScript initializers execute logic before and after a Blazor app loads. For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](../blazor/javascript-interoperability/?view=aspnetcore-6.0#javascript-initializers).

### Streaming JavaScript interop

Blazor now supports streaming data directly between .NET and JavaScript. For more information, see the following resources:

* [Stream from .NET to JavaScript](../blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0#stream-from-net-to-javascript)
* [Stream from JavaScript to .NET](../blazor/javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-6.0#stream-from-javascript-to-net)

### Generic type constraints

Generic type parameters are now supported. For more information, see [ASP.NET Core Razor components](../blazor/components/?view=aspnetcore-6.0#generic-type-parameter-support).

### WebAssembly deployment layout

Use a deployment layout to enable Blazor WebAssembly app downloads in restricted security environments. For more information, see [Deployment layout for ASP.NET Core hosted Blazor WebAssembly apps](../blazor/host-and-deploy/webassembly/deployment-layout?view=aspnetcore-6.0).

### New Blazor articles

In addition to the Blazor features described in the preceding sections, new Blazor articles are available on the following subjects:

* [ASP.NET Core Blazor file downloads](../blazor/file-downloads?view=aspnetcore-10.0): Learn how to download a file using native `byte[]` streaming interop to ensure efficient transfer to the client.
* [Display images and documents in ASP.NET Core Blazor](../blazor/images-and-documents?view=aspnetcore-10.0): Discover how to work with images and documents in Blazor apps, including how to stream image and document data.

## Build Blazor Hybrid apps with .NET MAUI, WPF, and Windows Forms

Use Blazor Hybrid to blend desktop and mobile native client frameworks with .NET and Blazor:

* .NET Multi-platform App UI (.NET MAUI) is a cross-platform framework for creating native mobile and desktop apps with C# and XAML.
* Blazor Hybrid apps can be built with Windows Presentation Foundation (WPF) and Windows Forms frameworks.

Important

Blazor Hybrid is in preview and shouldn't be used in production apps until final release.

For more information, see the following resources:

* [Preview ASP.NET Core Blazor Hybrid documentation](../blazor/hybrid/?view=aspnetcore-10.0)
* [What is .NET MAUI?](/en-us/dotnet/maui/what-is-maui)
* [Microsoft .NET Blog (category: ".NET MAUI")](https://devblogs.microsoft.com/dotnet/category/maui/)

## Kestrel

[HTTP/3](https://datatracker.ietf.org/doc/html/draft-ietf-quic-http-34) is currently in draft and therefore subject to change. HTTP/3 support in ASP.NET Core is not released, it's a preview feature included in .NET 6.

Kestrel now supports HTTP/3. For more information, see [Use HTTP/3 with the ASP.NET Core Kestrel web server](../fundamentals/servers/kestrel/http3?view=aspnetcore-10.0) and the blog entry [HTTP/3 support in .NET 6](https://devblogs.microsoft.com/dotnet/http-3-support-in-dotnet-6/).

### New Kestrel logging categories for selected logging

Prior to this change, enabling verbose logging for Kestrel was prohibitively expensive as all of Kestrel shared the `Microsoft.AspNetCore.Server.Kestrel` logging category name. `Microsoft.AspNetCore.Server.Kestrel` is still available, but the following new subcategories allow for more control of logging:

* `Microsoft.AspNetCore.Server.Kestrel` (current category): `ApplicationError`, `ConnectionHeadResponseBodyWrite`, `ApplicationNeverCompleted`, `RequestBodyStart`, `RequestBodyDone`, `RequestBodyNotEntirelyRead`, `RequestBodyDrainTimedOut`, `ResponseMinimumDataRateNotSatisfied`, `InvalidResponseHeaderRemoved`, `HeartbeatSlow`.
* `Microsoft.AspNetCore.Server.Kestrel.BadRequests`: `ConnectionBadRequest`, `RequestProcessingError`, `RequestBodyMinimumDataRateNotSatisfied`.
* `Microsoft.AspNetCore.Server.Kestrel.Connections`: `ConnectionAccepted`, `ConnectionStart`, `ConnectionStop`, `ConnectionPause`, `ConnectionResume`, `ConnectionKeepAlive`, `ConnectionRejected`, `ConnectionDisconnect`, `NotAllConnectionsClosedGracefully`, `NotAllConnectionsAborted`, `ApplicationAbortedConnection`.
* `Microsoft.AspNetCore.Server.Kestrel.Http2`: `Http2ConnectionError`, `Http2ConnectionClosing`, `Http2ConnectionClosed`, `Http2StreamError`, `Http2StreamResetAbort`, `HPackDecodingError`, `HPackEncodingError`, `Http2FrameReceived`, `Http2FrameSending`, `Http2MaxConcurrentStreamsReached`.
* `Microsoft.AspNetCore.Server.Kestrel.Http3`: `Http3ConnectionError`, `Http3ConnectionClosing`, `Http3ConnectionClosed`, `Http3StreamAbort`, `Http3FrameReceived`, `Http3FrameSending`.

Existing rules continue to work, but you can now be more selective on which rules you enable. For example, the observability overhead of enabling `Debug` logging for just bad requests is greatly reduced and can be enabled with the following configuration:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.AspNetCore.Kestrel.BadRequests": "Debug"
    }
  }
```

Log filtering applies rules with the longest matching category prefix. For more information, see [How filtering rules are applied](../fundamentals/logging/?view=aspnetcore-6.0#how-filtering-rules-are-applied)

### Emit KestrelServerOptions via EventSource event

The [KestrelEventSource](https://github.com/dotnet/aspnetcore/blob/v6.0.0/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs) emits a new event containing the JSON-serialized [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelserveroptions) when enabled with verbosity `EventLevel.LogAlways`. This event makes it easier to reason about the server behavior when analyzing collected traces. The following JSON is an example of the event payload:

```
{
  "AllowSynchronousIO": false,
  "AddServerHeader": true,
  "AllowAlternateSchemes": false,
  "AllowResponseHeaderCompression": true,
  "EnableAltSvc": false,
  "IsDevCertLoaded": true,
  "RequestHeaderEncodingSelector": "default",
  "ResponseHeaderEncodingSelector": "default",
  "Limits": {
    "KeepAliveTimeout": "00:02:10",
    "MaxConcurrentConnections": null,
    "MaxConcurrentUpgradedConnections": null,
    "MaxRequestBodySize": 30000000,
    "MaxRequestBufferSize": 1048576,
    "MaxRequestHeaderCount": 100,
    "MaxRequestHeadersTotalSize": 32768,
    "MaxRequestLineSize": 8192,
    "MaxResponseBufferSize": 65536,
    "MinRequestBodyDataRate": "Bytes per second: 240, Grace Period: 00:00:05",
    "MinResponseDataRate": "Bytes per second: 240, Grace Period: 00:00:05",
    "RequestHeadersTimeout": "00:00:30",
    "Http2": {
      "MaxStreamsPerConnection": 100,
      "HeaderTableSize": 4096,
      "MaxFrameSize": 16384,
      "MaxRequestHeaderFieldSize": 16384,
      "InitialConnectionWindowSize": 131072,
      "InitialStreamWindowSize": 98304,
      "KeepAlivePingDelay": "10675199.02:48:05.4775807",
      "KeepAlivePingTimeout": "00:00:20"
    },
    "Http3": {
      "HeaderTableSize": 0,
      "MaxRequestHeaderFieldSize": 16384
    }
  },
  "ListenOptions": [
    {
      "Address": "https://127.0.0.1:7030",
      "IsTls": true,
      "Protocols": "Http1AndHttp2"
    },
    {
      "Address": "https://[::1]:7030",
      "IsTls": true,
      "Protocols": "Http1AndHttp2"
    },
    {
      "Address": "http://127.0.0.1:5030",
      "IsTls": false,
      "Protocols": "Http1AndHttp2"
    },
    {
      "Address": "http://[::1]:5030",
      "IsTls": false,
      "Protocols": "Http1AndHttp2"
    }
  ]
}
```

### New DiagnosticSource event for rejected HTTP requests

Kestrel now emits a new `DiagnosticSource` event for HTTP requests rejected at the server layer. Prior to this change, there was no way to observe these rejected requests. The new `DiagnosticSource` event `Microsoft.AspNetCore.Server.Kestrel.BadRequest` contains a [IBadRequestExceptionFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ibadrequestexceptionfeature) that can be used to introspect the reason for rejecting the request.

```
using Microsoft.AspNetCore.Http.Features;
using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var diagnosticSource = app.Services.GetRequiredService<DiagnosticListener>();
using var badRequestListener = new BadRequestEventListener(diagnosticSource,
    (badRequestExceptionFeature) =>
{
    app.Logger.LogError(badRequestExceptionFeature.Error, "Bad request received");
});
app.MapGet("/", () => "Hello world");

app.Run();

class BadRequestEventListener : IObserver<KeyValuePair<string, object>>, IDisposable
{
    private readonly IDisposable _subscription;
    private readonly Action<IBadRequestExceptionFeature> _callback;

    public BadRequestEventListener(DiagnosticListener diagnosticListener,
                                   Action<IBadRequestExceptionFeature> callback)
    {
        _subscription = diagnosticListener.Subscribe(this!, IsEnabled);
        _callback = callback;
    }
    private static readonly Predicate<string> IsEnabled = (provider) => provider switch
    {
        "Microsoft.AspNetCore.Server.Kestrel.BadRequest" => true,
        _ => false
    };
    public void OnNext(KeyValuePair<string, object> pair)
    {
        if (pair.Value is IFeatureCollection featureCollection)
        {
            var badRequestFeature = featureCollection.Get<IBadRequestExceptionFeature>();

            if (badRequestFeature is not null)
            {
                _callback(badRequestFeature);
            }
        }
    }
    public void OnError(Exception error) { }
    public void OnCompleted() { }
    public virtual void Dispose() => _subscription.Dispose();
}
```

For more information, see [Logging and diagnostics in Kestrel](../fundamentals/servers/kestrel/diagnostics?view=aspnetcore-10.0).

### Create a ConnectionContext from an Accept Socket

The new [SocketConnectionContextFactory](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.sockets.socketconnectioncontextfactory) makes it possible to create a [ConnectionContext](/en-us/dotnet/api/microsoft.aspnetcore.connections.connectioncontext) from an accepted socket. This makes it possible to build a custom socket-based [IConnectionListenerFactory](/en-us/dotnet/api/microsoft.aspnetcore.connections.iconnectionlistenerfactory) without losing out on all the performance work and pooling happening in [SocketConnection](https://github.com/dotnet/aspnetcore/blob/v6.0.0/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs).

See [this example of a custom IConnectionListenerFactory](https://github.com/davidfowl/TcpProxy/blob/main/Backend/DelegatedConnectionListenerFactory.cs) which shows how to use this `SocketConnectionContextFactory`.

### Kestrel is the default launch profile for Visual Studio

The default launch profile for all new dotnet web projects is Kestrel. Starting Kestrel is significantly faster and results in a more responsive experience while developing apps.

IIS Express is still available as a launch profile for scenarios such as Windows Authentication or port sharing.

### Localhost ports for Kestrel are random

See [Template generated ports for Kestrel](#tgp) in this document for more information.

## Authentication and authorization

### Authentication servers

.NET 3 to .NET 5 used [IdentityServer4](https://identityserver4.readthedocs.io) as part of our template to support the issuing of JWT tokens for SPA and Blazor applications. The templates now use the [Duende Identity Server](https://docs.duendesoftware.com).

If you are extending the identity models and are updating existing projects, update the namespaces in your code from `IdentityServer4.IdentityServer` to `Duende.IdentityServer` and follow their migration instructions.

The license model for Duende Identity Server has changed to a reciprocal license, which may require license fees when it's used commercially in production. See the [Duende license page](https://duendesoftware.com/products/identityserver#pricing) for more details.

### Delayed client certificate negotiation

Developers can now opt-in to using delayed client certificate negotiation by specifying [ClientCertificateMode.DelayCertificate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode#microsoft-aspnetcore-server-kestrel-https-clientcertificatemode-delaycertificate) on the [HttpsConnectionAdapterOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions). This only works with HTTP/1.1 connections because HTTP/2 forbids delayed certificate renegotiation. The caller of this API must buffer the request body before requesting the client certificate:

```
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.WebUtilities;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseKestrel(options =>
{
    options.ConfigureHttpsDefaults(adapterOptions =>
    {
        adapterOptions.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
    });
});

var app = builder.Build();
app.Use(async (context, next) =>
{
    bool desiredState = GetDesiredState();
    // Check if your desired criteria is met
    if (desiredState)
    {
        // Buffer the request body
        context.Request.EnableBuffering();
        var body = context.Request.Body;
        await body.DrainAsync(context.RequestAborted);
        body.Position = 0;

        // Request client certificate
        var cert = await context.Connection.GetClientCertificateAsync();

        //  Disable buffering on future requests if the client doesn't provide a cert
    }
    await next(context);
});


app.MapGet("/", () => "Hello World!");
app.Run();
```

### `OnCheckSlidingExpiration` event for controlling cookie renewal

Cookie authentication sliding expiration can now be customized or suppressed using the new [OnCheckSlidingExpiration](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.oncheckslidingexpiration#microsoft-aspnetcore-authentication-cookies-cookieauthenticationevents-oncheckslidingexpiration). For example, this event can be used by a single-page app that needs to periodically ping the server without affecting the authentication session.

## Miscellaneous

### Hot Reload

Quickly make UI and code updates to running apps without losing app state for faster and more productive developer experience using [Hot Reload](../test/hot-reload?view=aspnetcore-10.0). For more information, see [.NET Hot Reload support for ASP.NET Core](../test/hot-reload?view=aspnetcore-10.0) and [Update on .NET Hot Reload progress and Visual Studio 2022 Highlights](https://devblogs.microsoft.com/dotnet/update-on-net-hot-reload-progress-and-visual-studio-2022-highlights/).

### Improved single-page app (SPA) templates

The ASP.NET Core project templates have been updated for Angular and React to use an improved pattern for single-page apps that is more flexible and more closely aligns with common patterns for modern front-end web development.

Previously, the ASP.NET Core template for Angular and React used specialized middleware during development to launch the development server for the front-end framework and then proxy requests from ASP.NET Core to the development server. The logic for launching the front-end development server was specific to the command-line interface for the corresponding front-end framework. Supporting additional front-end frameworks using this pattern meant adding additional logic to ASP.NET Core.

The updated ASP.NET Core templates for Angular and React in .NET 6 flips this arrangement around and take advantage of the built-in proxying support in the development servers of most modern front-end frameworks. When the ASP.NET Core app is launched, the front-end development server is launched just as before, but the development server is configured to proxy requests to the backend ASP.NET Core process. All of the front-end specific configuration to setup proxying is part of the app, not ASP.NET Core. Setting up ASP.NET Core projects to work with other front-end frameworks is now straight-forward: setup the front-end development server for the chosen framework to proxy to the ASP.NET Core backend using the pattern established in the Angular and React templates.

The startup code for the ASP.NET Core app no longer needs any single-page app-specific logic. The logic for starting the front-end development server during development is injecting into the app at runtime by the new [Microsoft.AspNetCore.SpaProxy](https://www.nuget.org/packages/microsoft.aspnetcore.spaproxy) package. Fallback routing is handled using endpoint routing instead of SPA-specific middleware.

Templates that follow this pattern can still be run as a single project in Visual Studio or using `dotnet run` from the command-line. When the app is published, the front-end code in the *ClientApp* folder is built and collected as before into the web root of the host ASP.NET Core app and served as static files. Scripts included in the template configure the front-end development server to use HTTPS using the ASP.NET Core development certificate.

### Draft HTTP/3 support in .NET 6

[HTTP/3](https://datatracker.ietf.org/doc/html/draft-ietf-quic-http-34) is currently in draft and therefore subject to change. HTTP/3 support in ASP.NET Core is not released, it's a preview feature included in .NET 6.

See the blog entry [HTTP/3 support in .NET 6](https://devblogs.microsoft.com/dotnet/http-3-support-in-dotnet-6/).

### Nullable Reference Type Annotations

Portions of the [ASP.NET Core in .NET 6 source code](https://github.com/dotnet/aspnetcore/tree/v6.0.0/src) has had [nullability annotations](/en-us/dotnet/csharp/nullable-migration-strategies) applied.

By utilizing the new [Nullable feature in C# 8](/en-us/dotnet/csharp/whats-new/csharp-8#nullable-reference-types), ASP.NET Core can provide additional compile-time safety in the handling of reference types. For example, protecting against `null` reference exceptions. Projects that have opted in to using nullable annotations may see new build-time warnings from ASP.NET Core APIs.

To enable nullable reference types, add the following property to project files:

```
<PropertyGroup>
    <Nullable>enable</Nullable>
</PropertyGroup>
```

For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

### Source Code Analysis

Several .NET compiler platform analyzers were added that inspect application code for problems such as incorrect middleware configuration or order, routing conflicts, etc. For more information, see [Diagnostic Code Analysis in ASP.NET Core Apps](../diagnostics/code-analysis?view=aspnetcore-10.0).

### Web app template improvements

The web app templates:

* Use the new [minimal hosting model](../migration/50-to-60?view=aspnetcore-10.0#new-hosting-model).
* Significantly reduces the number of files and lines of code required to create an app. For example, the ASP.NET Core empty web app creates one C# file with four lines of code and is a complete app.
* Unifies `Startup.cs` and `Program.cs` into a single `Program.cs` file.
* Uses [top-level statements](/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) to minimize the code required for an app.
* Uses [global `using` directives](/en-us/dotnet/csharp/whats-new/csharp-10#global-using-directives) to eliminate or minimize the number of [`using` statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) lines required.

[]()

### Template generated ports for Kestrel

Random ports are assigned during project creation for use by the Kestrel web server. Random ports help minimize a port conflict when multiple projects are run on the same machine.

When a project is created, a random HTTP port between 5000-5300 and a random HTTPS port between 7000-7300 is specified in the generated `Properties/launchSettings.json` file. The ports can be changed in the `Properties/launchSettings.json` file. If no port is specified, Kestrel defaults to the HTTP 5000 and HTTPS 5001 ports. For more information, see [Configure endpoints for Kestrel web server](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

### New logging defaults

The following changes were made to both `appsettings.json` and `appsettings.Development.json`:

```
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
+ "Microsoft.AspNetCore": "Warning"
```

The change from `"Microsoft": "Warning"` to `"Microsoft.AspNetCore": "Warning"` results in logging all informational messages from the `Microsoft` namespace ***except*** `Microsoft.AspNetCore`. For example, `Microsoft.EntityFrameworkCore` is now logged at the informational level.

### Developer exception page middleware added automatically

In the [development environment](../fundamentals/environments?view=aspnetcore-10.0), the [DeveloperExceptionPageMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.developerexceptionpagemiddleware) is added by default. It's no longer necessary to add the following code to web UI apps:

```
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
```

### Support for Latin1 encoded request headers in HttpSysServer

`HttpSysServer` now supports decoding request headers that are `Latin1` encoded by setting the [UseLatin1RequestHeaders](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.uselatin1requestheaders#microsoft-aspnetcore-server-httpsys-httpsysoptions-uselatin1requestheaders) property on [HttpSysOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions) to `true`:

```
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseHttpSys(o => o.UseLatin1RequestHeaders = true);

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### The ASP.NET Core Module logs include timestamps and PID

The [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) (ANCM) enhanced diagnostic logs include timestamps and [PID](https://wikipedia.org/wiki/Process_identifier) of the process emitting the logs. Logging timestamps and PID makes it easier to diagnose issues with overlapping process restarts in IIS when multiple IIS worker processes are running.

The resulting logs now resemble the sample output show below:

```
[2021-07-28T19:23:44.076Z, PID: 11020] [aspnetcorev2.dll] Initializing logs for 'C:\<path>\aspnetcorev2.dll'. Process Id: 11020. File Version: 16.0.21209.0. Description: IIS ASP.NET Core Module V2. Commit: 96475a2acdf50d7599ba8e96583fa73efbe27912.
[2021-07-28T19:23:44.079Z, PID: 11020] [aspnetcorev2.dll] Resolving hostfxr parameters for application: '.\InProcessWebSite.exe' arguments: '' path: 'C:\Temp\e86ac4e9ced24bb6bacf1a9415e70753\'
[2021-07-28T19:23:44.080Z, PID: 11020] [aspnetcorev2.dll] Known dotnet.exe location: ''
```

### Configurable unconsumed incoming buffer size for IIS

The IIS server previously only buffered 64 KiB of unconsumed request bodies. The 64 KiB buffering resulted in reads being constrained to that maximum size, which impacts the performance with large incoming bodies such as uploads. In .NET 6 , the default buffer size changes from 64 KiB to 1 MiB which should improve throughput for large uploads. In our tests, a 700 MiB upload that used to take 9 seconds now only takes 2.5 seconds.

The downside of a larger buffer size is an increased per-request memory consumption when the app isn't quickly reading from the request body. So, in addition to changing the default buffer size, the buffer size configurable, allowing apps to configure the buffer size based on workload.

### View Components Tag Helpers

Consider a view component with an optional parameter, as shown in the following code:

```
class MyViewComponent
{
    IViewComponentResult Invoke(bool showSomething = false) { ... }
}
```

With ASP.NET Core in .NET 6, the tag helper can be invoked without having to specify a value for the `showSomething` parameter:

```
<vc:my />
```

### Angular template updated to Angular 12

The ASP.NET Core in .NET 6 template for Angular now uses Angular 12.

The React template has been updated to [React 17](https://reactjs.org/blog/2020/10/20/react-v17.html).

### Configurable buffer threshold before writing to disk in Json.NET output formatter

**Note**: We recommend using the [System.Text.Json](/en-us/dotnet/api/system.text.json) output formatter except when the `Newtonsoft.Json` serializer is required for compatibility reasons. The `System.Text.Json` serializer is fully `async` and works efficiently for larger payloads.

The `Newtonsoft.Json` output formatter by default buffers responses up to 32 KiB in memory before buffering to disk. This is to avoid performing synchronous IO, which can result in other side-effects such as thread starvation and application deadlocks. However, if the response is larger than 32 KiB, considerable disk I/O occurs. The memory threshold is now configurable via the [MvcNewtonsoftJsonOptions.OutputFormatterMemoryBufferThreshold](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcnewtonsoftjsonoptions.outputformattermemorybufferthreshold#microsoft-aspnetcore-mvc-mvcnewtonsoftjsonoptions-outputformattermemorybufferthreshold) property before buffering to disk:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages()
            .AddNewtonsoftJson(options =>
            { 
                options.OutputFormatterMemoryBufferThreshold = 48 * 1024;
            });

var app = builder.Build();
```

For more information, see [this GitHub pull request](https://github.com/dotnet/aspnetcore/pull/32747) and the [NewtonsoftJsonOutputFormatterTest.cs](https://github.com/dotnet/aspnetcore/blob/v6.0.0/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs) file.

### Faster get and set for HTTP headers

New APIs were added to expose all common headers available on [Microsoft.Net.Http.Headers.HeaderNames](/en-us/dotnet/api/microsoft.net.http.headers.headernames) as properties on the [IHeaderDictionary](/en-us/dotnet/api/microsoft.aspnetcore.http.iheaderdictionary) resulting in an easier to use API. For example, the in-line middleware in the following code gets and sets both request and response headers using the new APIs:

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Use(async (context, next) =>
{
    var hostHeader = context.Request.Headers.Host;
    app.Logger.LogInformation("Host header: {host}", hostHeader);
    context.Response.Headers.XPoweredBy = "ASP.NET Core 6.0";
    await next.Invoke(context);
    var dateHeader = context.Response.Headers.Date;
    app.Logger.LogInformation("Response date: {date}", dateHeader);
});

app.Run();
```

For implemented headers the get and set accessors are implemented by going directly to the field and bypassing the lookup. For non-implemented headers, the accessors can bypass the initial lookup against implemented headers and directly perform the `Dictionary<string, StringValues>` lookup. Avoiding the lookup results in faster access for both scenarios.

### Async streaming

ASP.NET Core now supports asynchronous streaming from controller actions and responses from the JSON formatter. Returning an `IAsyncEnumerable` from an action no longer buffers the response content in memory before it gets sent. Not buffering helps reduce memory usage when returning large datasets that can be asynchronously enumerated.

Note that Entity Framework Core provides implementations of `IAsyncEnumerable` for querying the database. The improved support for `IAsyncEnumerable` in ASP.NET Core in .NET 6 can make using EF Core with ASP.NET Core more efficient. For example, the following code no longer buffers the product data into memory before sending the response:

```
public IActionResult GetMovies()
{
    return Ok(_context.Movie);
}
```

However, when using lazy loading in EF Core, this new behavior may result in errors due to concurrent query execution while the data is being enumerated. Apps can revert back to the previous behavior by buffering the data:

```
public async Task<IActionResult> GetMovies2()
{
    return Ok(await _context.Movie.ToListAsync());
}
```

See the related [announcement](https://github.com/aspnet/Announcements/issues/463) for additional details about this change in behavior.

### HTTP logging middleware

HTTP logging is a new built-in middleware that logs information about HTTP requests and HTTP responses including the headers and entire body:

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();
app.UseHttpLogging();

app.MapGet("/", () => "Hello World!");

app.Run();
```

Navigating to `/` with the previous code logs information similar to the following output:

```
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
      Request:
      Protocol: HTTP/2
      Method: GET
      Scheme: https
      PathBase: 
      Path: /
      Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
      Accept-Encoding: gzip, deflate, br
      Accept-Language: en-US,en;q=0.9
      Cache-Control: max-age=0
      Connection: close
      Cookie: [Redacted]
      Host: localhost:44372
      User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.30
      sec-ch-ua: [Redacted]
      sec-ch-ua-mobile: [Redacted]
      sec-ch-ua-platform: [Redacted]
      upgrade-insecure-requests: [Redacted]
      sec-fetch-site: [Redacted]
      sec-fetch-mode: [Redacted]
      sec-fetch-user: [Redacted]
      sec-fetch-dest: [Redacted]
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: text/plain; charset=utf-8
```

The preceding output was enabled with the following `appsettings.Development.json` file:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
    }
  }
}
```

HTTP logging provides logs of:

* HTTP Request information
* Common properties
* Headers
* Body
* HTTP Response information

To configure the HTTP logging middleware, specify [HttpLoggingOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions):

```
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(logging =>
{
    // Customize HTTP logging.
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("My-Request-Header");
    logging.ResponseHeaders.Add("My-Response-Header");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
});

var app = builder.Build();
app.UseHttpLogging();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### IConnectionSocketFeature

The [IConnectionSocketFeature](/en-us/dotnet/api/microsoft.aspnetcore.connections.features.iconnectionsocketfeature) request feature provides access to the underlying accept socket associated with the current request. It can be accessed via the [FeatureCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.features.featurecollection) on `HttpContext`.

For example, the following app sets the [LingerState](/en-us/dotnet/api/system.net.sockets.socket.lingerstate#system-net-sockets-socket-lingerstate) property on the accepted socket:

```
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions => listenOptions.Use((connection, next) =>
    {
        var socketFeature = connection.Features.Get<IConnectionSocketFeature>();
        socketFeature.Socket.LingerState = new LingerOption(true, seconds: 10);
        return next();
    }));
});
var app = builder.Build();
app.MapGet("/", (Func<string>)(() => "Hello world"));
await app.RunAsync();
```

### Generic type constraints in Razor

When defining generic type parameters in Razor using the `@typeparam` directive, generic type constraints can now be specified using the standard C# syntax:

### Smaller SignalR, Blazor Server, and MessagePack scripts

The SignalR, MessagePack, and Blazor Server scripts are now significantly smaller, enabling smaller downloads, less JavaScript parsing and compiling by the browser, and faster start-up. The size reductions:

* `signalr.js`: 70%
* `blazor.server.js`: 45%

The smaller scripts are a result of a community contribution from [Ben Adams](https://twitter.com/ben_a_adams). For more information on the details of the size reduction, see [Ben's GitHub pull request](https://github.com/dotnet/aspnetcore/pull/30320).

### Enable Redis profiling sessions

A community contribution from [Gabriel Lucaci](https://twitter.com/_glucaci) enables Redis profiling session with [Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis):

```
using StackExchange.Redis.Profiling;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.ProfilingSession = () => new ProfilingSession();
});
```

For more information, see [StackExchange.Redis Profiling](https://stackexchange.github.io/StackExchange.Redis/Profiling_v2.html).

### Shadow copying in IIS

An experimental feature has been added to the [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) to add support for [shadow copying application assemblies](/en-us/dotnet/framework/app-domains/shadow-copy-assemblies). Currently .NET locks application binaries when running on Windows making it impossible to replace binaries when the app is running. While our recommendation remains to use an [app offline file](../host-and-deploy/app-offline?view=aspnetcore-10.0), we recognize there are certain scenarios (for example FTP deployments) where it isn't possible to do so.

In such scenarios, enable shadow copying by customizing the ASP.NET Core module handler settings. In most cases, ASP.NET Core apps do not have a `web.config` checked into source control that you can modify. In ASP.NET Core, `web.config` is ordinarily generated by the SDK. The following sample `web.config` can be used to get started:

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <!-- To customize the asp.net core module uncomment and edit the following section. 
  For more info see https://go.microsoft.com/fwlink/?linkid=838655 -->

  <system.webServer>
    <handlers>
      <remove name="aspNetCore"/>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout">
      <handlerSettings>
        <handlerSetting name="experimentalEnableShadowCopy" value="true" />
        <handlerSetting name="shadowCopyDirectory" value="../ShadowCopyDirectory/" />
        <!-- Only enable handler logging if you encounter issues-->
        <!--<handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />-->
        <!--<handlerSetting name="debugLevel" value="FILE,TRACE" />-->
      </handlerSettings>
    </aspNetCore>
  </system.webServer>
</configuration>
```

Shadow copying in IIS is an experimental feature that is not guaranteed to be part of ASP.NET Core. Please leave feedback on IIS Shadow copying in [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/23733).

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

## Related content

* [Code samples migrated to the new minimal hosting model in 6.0](../migration/50-to-60-samples?view=aspnetcore-10.0)
* [What's new in .NET 6](/en-us/dotnet/core/whats-new/dotnet-6)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/conventions
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/web-api/advanced/conventions.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use web API conventions

Warning

The web API analyzers used with these conventions are deprecated as of .NET 10 and will be removed in a future release. Setting `IncludeOpenAPIAnalyzers` to `true` emits warning `ASPDEPR007`. For more information, see [IncludeOpenAPIAnalyzers property and MVC API analyzers are deprecated](/en-us/aspnet/core/breaking-changes/10/openapi-analyzers-deprecated?view=aspnetcore-10.0\&preserve-view=true).

Common [API documentation](../../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0) can be extracted and applied to multiple actions, controllers, or all controllers within an assembly. Web API conventions are a substitute for decorating individual actions with [`[ProducesResponseType]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute).

A convention allows you to:

* Define the most common return types and status codes returned from a specific type of action.
* Identify actions that deviate from the defined standard.

Default conventions are available from [Microsoft.AspNetCore.Mvc.DefaultApiConventions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.defaultapiconventions). The conventions are demonstrated with the `ValuesController.cs` added to an **API** project template:

```
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace WebApp1.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody] string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}
```

Actions that follow the patterns in the `ValuesController.cs` work with the default conventions. If the default conventions don't meet your needs, see [Create web API conventions](#create-web-api-conventions).

At runtime, [Microsoft.AspNetCore.Mvc.ApiExplorer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer) understands conventions. `ApiExplorer` is MVC's abstraction to communicate with [OpenAPI](https://www.openapis.org/) (also known as Swagger) document generators. Attributes from the applied convention are associated with an action and are included in the action's OpenAPI documentation. [API analyzers](analyzers?view=aspnetcore-10.0) also understand conventions. If your action is unconventional (for example, it returns a status code that isn't documented by the applied convention), a warning encourages you to document the status code.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/conventions/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Apply web API conventions

Conventions don't compose; each action may be associated with exactly one convention. More specific conventions take precedence over less specific conventions. The selection is non-deterministic when two or more conventions of the same priority apply to an action. The following options exist to apply a convention to an action, from the most specific to the least specific:

1. `Microsoft.AspNetCore.Mvc.ApiConventionMethodAttribute` — Applies to individual actions and specifies the convention type and the convention method that applies.

   In the following example, the default convention type's `Microsoft.AspNetCore.Mvc.DefaultApiConventions.Put` convention method is applied to the `Update` action:

   ```
   // PUT api/contactsconvention/{guid}
   [HttpPut("{id}")]
   [ApiConventionMethod(typeof(DefaultApiConventions), 
                        nameof(DefaultApiConventions.Put))]
   public IActionResult Update(string id, Contact contact)
   {
       var contactToUpdate = _contacts.Get(id);

       if (contactToUpdate == null)
       {
           return NotFound();
       }

       _contacts.Update(contact);

       return NoContent();
   }
   ```

   The `Microsoft.AspNetCore.Mvc.DefaultApiConventions.Put` convention method applies the following attributes to the action:

   ```
   [ProducesDefaultResponseType]
   [ProducesResponseType(StatusCodes.Status204NoContent)]
   [ProducesResponseType(StatusCodes.Status404NotFound)]
   [ProducesResponseType(StatusCodes.Status400BadRequest)]
   ```

   For more information on `[ProducesDefaultResponseType]`, see [Default Response](https://swagger.io/docs/specification/describing-responses/#default-response).

2. `Microsoft.AspNetCore.Mvc.ApiConventionTypeAttribute` applied to a controller — Applies the specified convention type to all actions on the controller. A convention method is marked with hints that determine the actions to which the convention method applies. For more information on hints, see [Create web API conventions](#create-web-api-conventions)).

   In the following example, the default set of conventions is applied to all actions in *ContactsConventionController*:

   ```
   [ApiController]
   [ApiConventionType(typeof(DefaultApiConventions))]
   [Route("api/[controller]")]
   public class ContactsConventionController : ControllerBase
   {
   ```

3. `Microsoft.AspNetCore.Mvc.ApiConventionTypeAttribute` applied to an assembly — Applies the specified convention type to all controllers in the current assembly. As a recommendation, apply assembly-level attributes in the `Startup.cs` file.

   In the following example, the default set of conventions is applied to all controllers in the assembly:

   ```
   [assembly: ApiConventionType(typeof(DefaultApiConventions))]
   namespace ApiConventions
   {
       public class Startup
       {
   ```

## Create web API conventions

If the default API conventions don't meet your needs, create your own conventions. A convention is:

* A static type with methods.
* Capable of defining [response types](#response-types) and [naming requirements](#naming-requirements) on actions.

### Response types

These methods are annotated with `[ProducesResponseType]` or `[ProducesDefaultResponseType]` attributes. For example:

```
public static class MyAppConventions
{
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public static void Find(int id)
    {
    }
}
```

If more specific metadata attributes are absent, applying this convention to an assembly enforces that:

* The convention method applies to any action named `Find`.
* A parameter named `id` is present on the `Find` action.

### Naming requirements

The `[ApiConventionNameMatch]` and `[ApiConventionTypeMatch]` attributes can be applied to the convention method that determines the actions to which they apply. For example:

```
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Find(
    [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
    int id)
{ }
```

In the preceding example:

* The `Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchBehavior.Prefix` option applied to the method indicates that the convention matches any action prefixed with "Find". Examples of matching actions include `Find`, `FindPet`, and `FindById`.
* The `Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchBehavior.Suffix` applied to the parameter indicates that the convention matches methods with exactly one parameter ending in the suffix identifier. Examples include parameters such as `id` or `petId`. `ApiConventionTypeMatch` can be similarly applied to types to constrain the parameter type. A `params[]` argument indicates remaining parameters that don't need to be explicitly matched.

## Related content

* [Video: Create metadata for NSwagClient](/en-us/shows/beginners-series-to-web-apis/generating-api-clients-17-of-18--beginners-series-to-web-apis)
* [Video: Beginner's Series to: Web APIs](/en-us/shows/beginners-series-to-web-apis/)
* [Use web API analyzers](analyzers?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](../../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/test/http-files
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/http-files.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use .http files in Visual Studio 2022

The [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) `.http` file editor provides a convenient way to test ASP.NET Core projects, especially API apps. The editor provides a UI that:

* Creates and updates `.http` files.
* Sends HTTP requests specified in `.http` files.
* Displays the responses.

This article contains documentation for:

* [The `.http` file syntax](#http-file-syntax).
* [How to create an `.http` file](#create-an-http-file).
* [How to send a request from an `.http` file](#send-an-http-request).
* [Where to find `.http` file options that can be configured](#http-file-options).
* [How to create requests in `.http` files by using the Visual Studio 2022 **Endpoints Explorer**](#use-endpoints-explorer).

The `.http` file format and editor was inspired by the Visual Studio Code [REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client). The Visual Studio 2022 `.http` editor recognizes `.rest` as an alternative file extension for the same file format.

## Prerequisites

* [Visual Studio 2022 version 17.8 or later](https://visualstudio.microsoft.com/vs/) with the **ASP.NET and web development** workload installed.

## `.http` file syntax

The following sections explain `.http` file syntax.

## Requests

The format for an HTTP request is `HTTPMethod URL HTTPVersion`, all on one line, where:

* `HTTPMethod` is the HTTP method to use, for example:

  * [OPTIONS](https://developer.mozilla.org/docs/Web/HTTP/Methods/OPTIONS)
  * [GET](https://developer.mozilla.org/docs/Web/HTTP/Methods/GET)
  * [HEAD](https://developer.mozilla.org/docs/Web/HTTP/Methods/HEAD)
  * [POST](https://developer.mozilla.org/docs/Web/HTTP/Methods/POST)
  * [PUT](https://developer.mozilla.org/docs/Web/HTTP/Methods/PUT)
  * [PATCH](https://developer.mozilla.org/docs/Web/HTTP/Methods/PATCH)
  * [DELETE](https://developer.mozilla.org/docs/Web/HTTP/Methods/DELETE)
  * [TRACE](https://developer.mozilla.org/docs/Web/HTTP/Methods/TRACE)
  * [CONNECT](https://developer.mozilla.org/docs/Web/HTTP/Methods/CONNECT)

* `URL` is the URL to send the request to. The URL can include query string parameters. The URL doesn't have to point to a local web project. It can point to any URL that Visual Studio can access.

* `HTTPVersion` is optional and specifies the HTTP version that should be used, that is, `HTTP/1.1`, `HTTP/2`, or `HTTP/3`.

A file can contain multiple requests by using lines with `###` as delimiters. The following example showing three requests in a file illustrates this syntax:

```
GET https://localhost:7220/weatherforecast

###

GET https://localhost:7220/weatherforecast?date=2023-05-11&location=98006

###

GET https://localhost:7220/weatherforecast HTTP/3

###
```

## Request headers

To add one or more headers, add each header on its own line immediately after the request line. Don't include any blank lines between the request line and the first header or between subsequent header lines. The format is `HeaderName: Value`, as shown in the following examples:

```
GET https://localhost:7220/weatherforecast
Date: Wed, 27 Apr 2023 07:28:00 GMT

###

GET https://localhost:7220/weatherforecast
Cache-Control: max-age=604800
Age: 100

###
```

Important

When calling an API that authenticates with headers, do not commit any secrets to a source code repository. See the supported methods for storing secrets later in this article, such as [ASP.NET Core user secrets](#aspnet-core-user-secrets), [Azure Key Vault](#azure-key-vault) and [DPAPI encryption](#dpapi-encryption).

## Request body

Add the request body after a blank line, as shown in the following example:

```
POST https://localhost:7220/weatherforecast
Content-Type: application/json
Accept-Language: en-US,en;q=0.5

{
    "date": "2023-05-10",
    "temperatureC": 30,
    "summary": "Warm"
}

###
```

## Comments

Lines that start with either `#` or `//` are comments. These lines are ignored when Visual Studio sends HTTP requests.

## Variables

A line that starts with `@` defines a variable by using the syntax `@VariableName=Value`. The variable name is case-sensitive and can't contain any spaces. The value can contain any characters, including the value `null` to represent a null value.

Variables can be referenced in requests that are defined later in the file. They're referenced by wrapping their names in double curly braces, `{{` and `}}`. The following example shows two variables defined and used in a request:

```
@hostname=localhost
@port=44320
GET https://{{hostname}}:{{port}}/weatherforecast
```

Variables can be defined using values of other variables that were defined earlier in the file. The following example uses one variable in the request instead of the two shown in the preceding example:

```
@hostname=localhost
@port=44320
@host={{hostname}}:{{port}}
GET https://{{host}}/api/search/tool
```

## Environment files

To give variables different values in different environments, create a file named `http-client.env.json`. Locate the file in the same directory as the `.http` file or in one of its parent directories. Here's an example of an environment file:

```
{
  "dev": {
    "HostAddress": "https://localhost:44320"
  },
  "remote": {
    "HostAddress": "https://contoso.com"
  }
}
```

The environment file is a JSON file that contains one or more named environments, such as "dev" and "remote" in the preceding example. Each named environment contains one or more variables, such as `HostAddress` in the preceding example. Variables from an environment file are referenced the same way as other variables, as shown in the following example:

```
GET {{HostAddress}}/api/search/tool
```

The value that is used for the variable when sending a request is determined by an environment selector dropdown at the upper right corner of the `.http` file editor. The following screenshot shows the selector:

The environment file doesn't have to be in the project folder. Visual Studio looks for an environment file in the folder where the `.http` file exists. If it's not in that folder, Visual Studio looks through the parent directories to find it. When a file named `http-client.env.json` is found, the search ends. The file found nearest to the `.http` file is used.

After creating or editing an `.http` file, you might have to close and reopen the project to see the changes reflected in the environment selector. Press `F6` to select the environment selector.

Visual Studio displays warnings in the following situations:

* The `.http` file references a variable that isn't defined in the `.http` file or in the environment file.
* The environment file contains a variable that isn't referenced in the `.http` file.

A variable defined in an environment file can be the same as one defined in the `.http` file, or it can be different. If a variable is defined in both the `.http` file and the environment file, the value in the `.http` file overrides the value in the environment file.

## Shared variables

`$shared` is a special environment name for values that are the same for multiple environments. For example, consider the following environment file (`http-client.env.json`):

```
{
    "$shared": {
        "HostAddress": "https://localhost:7293"
    },
    "dev1": {
        "username": "dev1user"
    },
    "dev2": {
        "username": "dev2user"
    },
    "staging": {
        "username": "staginguser",
        "HostAddress": "https://staging.contoso.com"
    }
}
```

In the preceding example, the `$shared` environment defines the `HostAddress` variable with the value `localhost:7293`. The `HostAddress` variable with the value `localhost:7293` functions as a default for environments that don't define a `HostAddress`. When the `dev1` or `dev2` environment is defined, the value for `HostAddress` comes from the `$shared` environment because `dev1` and `dev2` don't define a `HostAddress` variable. When the `staging` environment is defined, the value for `HostAddress` is set to `https://staging.contoso.com`, overriding the `$shared` default.

## Request variables

You can pass values from one HTTP request to another within the same `.http` file.

1. Create a single-line comment located just before a request URL to name the following request. For example, the following lines show alternative ways to name the request `login`:

   ```
   # @name login
   https://contoso.com/api/login HTTP/1.1   
   ```

   ```
   // @name login
   https://contoso.com/api/login HTTP/1.1
   ```

2. In subsequent requests in the same HTTP file use the request name to refer to the request.

3. Use the following syntax to extract the specific part of the response that you want.

   ```
   {{<request name>.(response|request).(body|headers).(*|JSONPath|XPath|<header name>)}}.
   ```

   This syntax lets you extract values from the request itself or from the response to it (`request|response`). For either request or response, you can extract values from the body or the headers (`body|headers`).

   When `body` is selected, the `*|JSONPath|XPath` part of the syntax applies:

   * `*` extracts the entire response body.

     Example: `{{login.response.body.*}}`

   * For JSON responses, use [JSONPath](https://www.rfc-editor.org/rfc/rfc9535.html) to extract a specific property or attribute.

     Example: `{{login.response.body.$.token}}`

   * For XML responses, use [XPath](https://www.w3schools.com/xml/xpath_syntax.asp) to extract a specific property or attribute.

     Example: `{{login.response.body./token}}`

   When `headers` is selected, a header name extracts the entire header. Header names are case-insensitive.

   Example: `{{login.response.headers.Location}}`

If you want to refer to the response of a named request, you need to manually trigger the named request to retrieve its response first. When you extract values from the response, you'll get the latest response if the request has been sent more than once.

### Example request variable usage

For example, suppose your HTTP file has a request that authenticates the caller, and you name it `login`. The response body is a JSON document that contains the bearer token in a property named `token`. In subsequent requests, you want to pass in this bearer token in an `Authorization` header. The following example does this:

```
# @name login

POST {{TodoApi_HostAddress}}/users/token 
Content-Type: application/json 

{ 
  "username": "{{myusername}}", 
} 

### 

GET {{TodoApi_HostAddress}}/todos 
Authorization: Bearer {{login.response.body.$.token}}

### 
```

The syntax `{{login.response.body.$.token}}` represents the bearer token:

* **`login`**: Is the request name.
* **`response`**: Refers to the HTTP response object.
* **`body`**: Refers to the body of the HTTP response.
* **`$`**: Represents the root element of the JSON document in the response body.
* **`token`**: Refers to the specific property within the JSON document.

Without using request variables you would need to manually extract the token from the login response and include it in the header of subsequent requests. Request variables enable you to automate this process.

## User-specific environment files

A user-specific value is any value that a developer wants to test with but doesn't want to share with the team. The `http-client.env.json` file is checked in to source control by default, therefore, ***DO NOT*** add user-specific values to this file. Rather, add user-specific values in a file named `http-client.env.json.user`. The `http-client.env.json.user` file is located in the same folder as the `http-client.env.json` file. Files that end with `.user` are excluded from source control by default when using Visual Studio source control features.

When the `http-client.env.json` file is loaded, Visual Studio looks for a sibling `http-client.env.json.user` file. If a variable is defined in an environment in both the `http-client.env.json` file and the `http-client.env.json.user` file, the value in the `http-client.env.json.user` file wins.

Here's an example scenario that shows how a user-specific environment file works. Suppose the `.http` file has the following content:

```
GET {{HostAddress}}/{{Path}}
Accept: application/json
```

And suppose the `http-client.env.json` file contains the following content:

```
{
  "dev": {
    "HostAddress": "https://localhost:7128",
    "Path": "/weatherforecast"
  },
  "remote": {
    "HostAddress": "https://contoso.com",
    "Path": "/weatherforecast"
  }
}
```

And suppose there's a user-specific environment file that contains the following content:

```
{
  "dev": {
    "Path": "/swagger/index.html"
  }
}
```

When the user selects the "dev" environment, the request is sent to `https://localhost:7128/swagger/index.html` because the `Path` value in the `http-client.env.json.user` file overrides the value from the `http-client.env.json` file.

With the same environment files, suppose the variables are defined in the `.http` file:

```
@HostAddress=https://contoso.com
@Path=/weatherforecast

GET {{HostAddress}}/{{Path}}
Accept: application/json
```

In this scenario, the "dev" environment request is sent to `https://contoso.com/weatherforecast` because variable definitions in `.http` files override environment file definitions.

## ASP.NET Core user secrets

To get a value from [user secrets](../security/app-secrets?view=aspnetcore-10.0), use an environment file that is located in the same folder as the ASP.NET Core project. In the environment file, define a variable that has `provider` and `secretName` properties. Set the `provider` value to `AspnetUserSecrets` and set `secretName` to the name of the desired user secret. For example, the following environment file defines a variable named `ApiKeyDev` that gets its value from the `config:ApiKeyDev` user secret:

```
{
  "dev": {
    "ApiKeyDev": {
      "provider": "AspnetUserSecrets",
      "secretName": "config:ApiKeyDev"
    }
  }
}
```

To use this variable in the `.http` file, reference it like a standard variable. For example:

```
GET {{HostAddress}}{{Path}}
X-API-KEY: {{ApiKeyDev}}
```

When the request is sent, the value of the `ApiKeyDev` secret is in the X-API-KEY header.

As you type in the `http` file, the editor shows a completion list for the variable name but doesn't show its value.

## Azure Key Vault

[Azure Key Vault](/en-us/azure/key-vault/general/overview) is one of several key management solutions in Azure that can be used for secrets management. Of the three secrets stores currently supported for `.http` files, Key Vault is the best choice for sharing secrets across different users. The other two options—[ASP.NET User Secrets](#aspnet-core-user-secrets) and [DPAPI encryption](#dpapi-encryption)—aren't easily shared.

To use a value from Azure Key Vault, you must be signed into Visual Studio with an account that has access to the desired Key Vault. Define a variable in an environment file with the metadata to access the secret. The variable is named `AKVSecret` in the following example:

```
{
  "dev": {
    "AKVSecret": {
      "provider": "AzureKeyVault",
      "secretName": "SecretInKeyVault",
      "resourceId": "/subscriptions/3a914c59-8175a9e0e540/resourceGroups/my-key-vault-rg/providers/Microsoft.KeyVault/vaults/my-key-vault-01182024"
    }
  }
}
```

The variable `AKVSecret` pulls its value from Azure Key Vault. The following properties are defined on `AKVSecret`:

| Name       | Description                                             |
| ---------- | ------------------------------------------------------- |
| provider   | For Key Vault, always use `AzureKeyVault`.              |
| secretName | Name of the secret to extract.                          |
| resourceId | Azure resource ID for the specific Key Vault to access. |

The value for the `resourceId` property can be found in the Azure portal. Go to **Settings > Properties** to find it. For `secretName`, use the name of the secret that appears on the **Secrets** page in the Azure portal.

For example, the following `.http` file has a request that uses this secret value.

```
GET {{HostAddress}}{{Path}}
X-AKV-SECRET: {{akvSecret}}
```

## DPAPI encryption

On Windows, there is a [Data Protection API (DPAPI)](/en-us/dotnet/standard/security/how-to-use-data-protection) that can be used to encrypt sensitive data. When DPAPI is used to encrypt data, the encrypted values are always machine-specific, and they're also user-specific in `.http` files. These values can't be shared with other users.

To encrypt a value, use the following console application:

```
using System.Security.Cryptography;
using System.Text;

string stringToEncrypt = "Hello, World!";
byte[] encBytes = ProtectedData.Protect(Encoding.Unicode.GetBytes(stringToEncrypt), optionalEntropy: null, scope: DataProtectionScope.CurrentUser);
string base64 = Convert.ToBase64String(encBytes);
Console.WriteLine(base64);
```

The preceding console application references the [System.Security.Cryptography.ProtectedData](https://www.nuget.org/packages/System.Security.Cryptography.ProtectedData/) NuGet package. To enable the encrypted value to work in the `.http` file, encrypt with the scope set to [DataProtectionScope.CurrentUser](/en-us/dotnet/api/system.security.cryptography.dataprotectionscope#system-security-cryptography-dataprotectionscope-currentuser). The encrypted value is a base64 encoded string that can be copied and pasted into the environment file.

In the environment file, create a variable that has `provider` and `value` properties. Set `provider` to `Encrypted`, and set `value` to the encrypted value. For example, the following environment file defines a variable named `dpapiValue` that gets its value from a string that was encrypted with DPAPI.

```
{
  "dev": {
    "dpapiValue": {
      "provider": "Encrypted",
      "value": "AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAA5qwfg4+Bhk2nsy6ujgg3GAAAAAACAAAAAAAQZgAAAAEAACAAAAAqNXhXc098k1TtKmaI4cUAbJVALMVP1zOR7mhC1RBJegAAAAAOgAAAAAIAACAAAABKu4E9WC/zX5LYZZhOS2pukxMTF9R4yS+XA9HoYF98GzAAAAAzFXatt461ZnVeUWgOV8M/DkqNviWUUjexAXOF/JfpJMw/CdsizQyESus2QjsCtZlAAAAAL7ns3u9mEk6wSMIn+KNsW/vdAw51OaI+HPVrt5vFvXRilTtvGbU/JnxsoIHj0Z7OOxlwOSg1Qdn60zEqmlFJBg=="
    }
  }
}
```

With the preceding environment file, `dpapiValue` can be used in the `.http` file like any other variable. For example:

```
GET {{HostAddress}}{{Path}}
X-DPAPI-Secret: {{dpapiSecret}}
```

When this request is sent, X-DPAPI-Secret has the decrypted secret value.

## Environment variables

To get the value of an environment variable, use `$processEnv`. The following example puts the value of the USERNAME environment variable in the X-UserName header.

```
GET {{HostAddress}}{{Path}}
X-UserName: {{$processEnv USERNAME}}
```

If you try to use `$processEnv` to access an environment variable that doesn't exist, the `.http` file editor displays an error message.

## `.env` files

To get the value of a variable that is defined in a [`.env`](https://github.com/motdotla/dotenv) file, use `$dotenv`. The `.env` file must be in the project folder. The format for `$dotenv` is the same as for `$processEnv`. For example, if the `.env` file has this content:

```
USERNAME=userFromDotenv
```

And the `.http` file has this content:

```
GET {{HostAddress}}{{Path}}
X-UserName: {{$dotenv USERNAME}}
```

The `X-UserName` header will have "userFromDotenv".

When `$dotenv` is entered in the editor, it shows completions for the variables defined in the `.env` file.

Note

`.env` files might not be excluded from source control by default, so be careful to avoid checking in any secret values.

## Random integers

To generate a random integer, use `$randomInt`. The syntax is `{{$randomInt [min max]}}` where the `min` and `max` values are optional.

## Dates and times

* `$datetime` generates a `datetime` string in UTC. The syntax is `{{$datetime [format] [offset option]}}` where the format and offset options are optional.
* `$localDatetime` generates a `datetime` string in the local time zone. The syntax is `{{$localDatetime [format] [offset option]}}` where the format and offset options are optional.
* `$timestamp` generates a `timestamp` in UTC. The `timestamp` is the [number of seconds since the Unix Epoch in UTC time](/en-us/dotnet/api/system.datetimeoffset.tounixtimeseconds#system-datetimeoffset-tounixtimeseconds). The syntax is `{{$timestamp [offset option]}}` where the offset option is optional.

The `[format]` option is one of `rfc1123`, `iso8601`, or a custom format in quotation marks. For example:

```
GET https://localhost:7220/weatherforecast
X-CUSTOM: {{$datetime "dd-MM-yyyy"}}
X-ISO8601: {{$datetime iso8601}}
X-ISO8601L: {{$localDatetime iso8601}}
X-RFC1123: {{$datetime rfc1123}}
X-RFC1123L: {{$localDatetime rfc1123}}
```

Here are some sample values that the preceding examples generate:

```
{
  "headers": {
    "X-Custom": "17-01-2024",
    "X-Iso8601": "2024-01-17T22:59:55.5345770+00:00",
    "X-Iso8601L": "2024-01-17T14:59:55.5345770-08:00",
    "X-Rfc1123": "Wed, 17 Jan 2024 22:59:55 GMT",
    "X-Rfc1123L": "Wed, 17 Jan 2024 14:59:55 -08"
  }
}
```

The `[offset option]` syntax is in the form `number` `unit` where `number` is an integer and `unit` is one of the following values:

| `unit` | Explanation  |
| ------ | ------------ |
| `ms`   | Milliseconds |
| `s`    | Seconds      |
| `m`    | Minutes      |
| `h`    | Hours        |
| `d`    | Days         |
| `w`    | Weeks        |
| `M`    | Months       |
| `y`    | Years        |

For example:

```
GET https://localhost:7220/weatherforecast
X-Custom-Minus-1-Year: {{$datetime "dd-MM-yyyy" -1 y}}
X-RFC1123-Plus-1-Day: {{$datetime rfc1123 1 d}} 
X-Timestamp-Plus-1-Year: {{$timestamp 1 y}}
```

Here are some sample values that the preceding examples generate:

```
{
  "headers": {
    "X-Custom-Minus-1-Year": "17-01-2023",
    "X-Rfc1123-Plus-1-Day": "Thu, 18 Jan 2024 23:02:48 GMT",
    "X-Timestamp-Plus-1-Year": "1737154968"
  }
}
```

## GUIDs

To generate a random GUID, use `$guid`:

```
GET https://localhost:7220/weatherforecast
X-CorrelationId: {{$guid}}
```

## Unsupported syntax

The Visual Studio 2022 `.http` file editor doesn't have all the features that the Visual Studio Code [REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) has. The following list includes some of the more significant features available only in the Visual Studio Code extension:

* Request line that spans more than one line
* Specify file path as body of the request
* Mixed format for body when using multipart/form-data
* GraphQL requests
* cURL request
* Copy/paste as cURL
* Request history
* Save response body to file
* Certificate based authentication
* Prompt variables
* Customize response preview
* Per-request settings
* Null-handling variables, e.g. using null-conditional or nullish coalescing operators (this is also unsupported in Visual Studio Code)

## Create an `.http` file

* In **Solution Explorer**, right-click an ASP.NET Core project.

* In the context menu, select **Add** > **New Item**.

* In the **Add New Item** dialog, select **ASP.NET Core** > **General**.

* Select **HTTP File**, and select **Add**.

## Send an HTTP request

* Add at least one [request](#requests) to an `.http` file and save the file.

* If the request URL points to localhost and the project's port, run the project before trying to send a request to it.

* Select the `Send Request` or `Debug` link that is directly above the request to be sent.

  The request is sent to the specified URL, and the response appears in a separate pane to the right of the editor window.

## `.http` file options

Some aspects of `.http` file behavior can be configured. To see what's available, go to **Tools** > **Options** > **Text Editor** > **Rest**. For example, the timeout setting can be configured on the **Advanced** tab. Here's a screenshot of the **Options** dialog:

## Use Endpoints Explorer

**Endpoints Explorer** is a tool window that shows all the endpoints that a web API defines. The tool enables you to send requests to the endpoints by using an `.http` file.

The initial set of endpoints that **Endpoints Explorer** displays are discovered statically. There are some endpoints that can't be discovered statically. For example, endpoints defined in a class library project can't be discovered until runtime. When you run or debug a web API, Visual Studio version 17.11 Preview discovers endpoints dynamically at run time also and adds those to **Endpoints Explorer**.

### Open Endpoints Explorer

Select **View** > **Other Windows** > **Endpoints Explorer**.

### Add a request to an `.http` file

Right-click a request in **Endpoints Explorer** and select **Generate Request**.

* If an `.http` file with the project name as the file name exists, the request is added to that file.
* Otherwise, an `.http` file is created with the project name as the file name, and the request is added to that file.

The preceding screenshot shows endpoints defined by the ASP.NET Core Web API project template. The following example shows the request that is generated for the selected endpoint:

```
GET {{WebApplication1_HostAddress}}/weatherforecast/
Accept: application/json

###
```

Send the request as described [earlier in this article](#send-an-http-request).

## See also

* [Endpoints Explorer window only recognizes literal strings for routes](https://github.com/dotnet/AspNetCore.Docs/issues/30293)
* [Web API development in Visual Studio 2022](https://devblogs.microsoft.com/visualstudio/web-api-development-in-visual-studio-2022/)
* [Visual Studio Code REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/progressive-web-app/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/progressive-web-app/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor Progressive Web Application (PWA)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

A Blazor Progressive Web Application (PWA) is a single-page application (SPA) that uses modern browser APIs and capabilities to behave like a desktop app.

Blazor WebAssembly is a standards-based client-side web app platform, so it can use any browser API, including PWA APIs required for the following capabilities:

* Working offline and loading instantly, independent of network speed.
* Running in its own app window, not just a browser window.
* Being launched from the host's operating system start menu, dock, or home screen.
* Receiving push notifications from a backend server, even while the user isn't using the app.
* Automatically updating in the background.

The word *progressive* is used to describe these apps because:

* A user might first discover and use the app within their web browser like any other SPA.
* Later, the user progresses to installing it in their OS and enabling push notifications.

## Create a project from the PWA template

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / .NET CLI](#tabpanel_1_visual-studio-code+net-cli)

When creating a new **Blazor WebAssembly App**, select the **Progressive Web Application** checkbox.

Use the following command to create a PWA project in a command shell with the `--pwa` switch:

```
dotnet new blazorwasm -o MyBlazorPwa --pwa
```

In the preceding command, the `-o|--output` option creates a new folder for the app named `MyBlazorPwa`.

Optionally, PWA can be configured for an app created from the **ASP.NET Core Hosted** Blazor WebAssembly project template. The PWA scenario is independent of the hosting model.

## Convert an existing Blazor WebAssembly app into a PWA

Convert an existing Blazor WebAssembly app into a PWA following the guidance in this section.

In the app's project file:

* Add the following `ServiceWorkerAssetsManifest` property to a `PropertyGroup`:

  ```
    ...
    <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
  </PropertyGroup>
  ```

* Add the following `ServiceWorker` item to an `ItemGroup`:

  ```
  <ItemGroup>
    <ServiceWorker Include="wwwroot\service-worker.js" 
      PublishedContent="wwwroot\service-worker.published.js" />
  </ItemGroup>
  ```

To obtain static assets, use **one** of the following approaches:

* Create a separate, new PWA project with the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) command in a command shell:

  ```
  dotnet new blazorwasm -o MyBlazorPwa --pwa
  ```

  In the preceding command, the `-o|--output` option creates a new folder for the app named `MyBlazorPwa`.

  **If you aren't converting an app for the latest release**, pass the `-f|--framework` option. The following example creates the app for .NET 5:

  ```
  dotnet new blazorwasm -o MyBlazorPwa --pwa -f net5.0
  ```

- Navigate to the ASP.NET Core GitHub repository at the following URL, which links to `main` branch reference source and assets. Select the release that you're working with from the **Switch branches or tags** dropdown list that applies to your app.

  [Blazor WebAssembly project template `wwwroot` folder (`dotnet/aspnetcore` GitHub repository `main` branch)](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/wwwroot)

  Note

  Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

  From the source `wwwroot` folder either in the app that you created or from the reference assets in the `dotnet/aspnetcore` GitHub repository, copy the following files into the app's `wwwroot` folder:

  * `icon-192.png`
  * `icon-512.png`
  * `manifest.webmanifest`
  * `service-worker.js`
  * `service-worker.published.js`

In the app's `wwwroot/index.html` file:

* Add `<link>` elements for the manifest and app icon:

  ```
  <link href="manifest.webmanifest" rel="manifest" />
  <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
  <link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
  ```

- Navigate to the ASP.NET Core GitHub repository at the following URL, which links to the `v7.0.0` tag reference source and assets. If you're using .NET 8 or later, change the document version selector at the top of this article to see the updated guidance for this section. Select the release that you're working with from the **Switch branches or tags** dropdown list that applies to your app.

  [Blazor WebAssembly project template `wwwroot` folder (`v7.0.0` tag)](https://github.com/dotnet/aspnetcore/tree/v7.0.0/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/wwwroot)

  Note

  Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

  From the source `wwwroot` folder either in the app that you created or from the reference assets in the `dotnet/aspnetcore` GitHub repository, copy the following files into the app's `wwwroot` folder:

  * `favicon.png`
  * `icon-192.png`
  * `icon-512.png`
  * `manifest.json`
  * `service-worker.js`
  * `service-worker.published.js`

In the app's `wwwroot/index.html` file:

* Add `<link>` elements for the manifest and app icon:

  ```
  <link href="manifest.json" rel="manifest" />
  <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
  <link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
  ```

- Add the following JavaScript inside the closing `</body>` tag immediately after the `blazor.webassembly.js` script tag:

  ```
  <script>
    navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });
  </script>
  ```

  The [`updateViaCache: 'none'` option](https://developer.mozilla.org/docs/Web/API/ServiceWorkerRegistration/updateViaCache) ensures that:

  * The browser doesn't use cached versions of the service worker script.
  * Service worker updates are applied reliably without being blocked by HTTP caching.
  * PWA applications can update their service workers more predictably.

  This addresses caching issues that can prevent service worker updates from being applied correctly, which is particularly important for PWAs that rely on service workers for offline functionality.

## Installation and app manifest

When visiting an app created using the PWA template, users have the option of installing the app into their OS's start menu, dock, or home screen. The way this option is presented depends on the user's browser. When using desktop Chromium-based browsers, such as Edge or Chrome, an **Add** button appears within the URL bar. After the user selects the **Add** button, they receive a confirmation dialog:

On iOS, visitors can install the PWA using Safari's **Share** button and its **Add to Homescreen** option. On Chrome for Android, users should select the **Menu** button in the upper-right corner, followed by **Add to Home screen**.

Once installed, the app appears in its own window without an address bar:

To customize the window's title, color scheme, icon, or other details, see the `manifest.json` file in the project's `wwwroot` directory. The schema of this file is defined by web standards. For more information, see [MDN web docs: Web App Manifest](https://developer.mozilla.org/docs/Web/Manifest).

## Offline support

Apps created using the PWA template option have support for running offline. A user must first visit the app while they're online. The browser automatically downloads and caches all of the resources required to operate offline.

Important

Development support would interfere with the usual development cycle of making changes and testing them. Therefore, offline support is only enabled for *published* apps.

Warning

If you intend to distribute an offline-enabled PWA, there are [several important warnings and caveats](#caveats-for-offline-pwas). These scenarios are inherent to offline PWAs and not specific to Blazor. Be sure to read and understand these caveats before making assumptions about how your offline-enabled app works.

To see how offline support works:

1. Publish the app. For more information, see [Host and deploy ASP.NET Core Blazor](../host-and-deploy/?view=aspnetcore-10.0#publish-the-app).

2. Deploy the app to a server that supports HTTPS, and access the app in a browser at its secure HTTPS address.

3. Open the browser's dev tools and verify that a *Service Worker* is registered for the host on the **Application** tab:

4. Reload the page and examine the **Network** tab. **Service Worker** or **memory cache** are listed as the sources for all of the page's assets:

5. To verify that the browser isn't dependent on network access to load the app, either:

   * Shut down the web server and see how the app continues to function normally, which includes page reloads. Likewise, the app continues to function normally when there's a slow network connection.
   * Instruct the browser to simulate offline mode in the **Network** tab:

Offline support using a service worker is a web standard, not specific to Blazor. For more information on service workers, see [MDN web docs: Service Worker API](https://developer.mozilla.org/docs/Web/API/Service_Worker_API). To learn more about common usage patterns for service workers, see [Google Web: The Service Worker Lifecycle](https://web.dev/service-worker-lifecycle/).

Blazor's PWA template produces two service worker files:

* `wwwroot/service-worker.js`, which is used during development.
* `wwwroot/service-worker.published.js`, which is used after the app is published.

To share logic between the two service worker files, consider the following approach:

* Add a third JavaScript file to hold the common logic.
* Use [`self.importScripts`](https://developer.mozilla.org/docs/Web/API/WorkerGlobalScope/importScripts) to load the common logic into both service worker files.

### Cache-first fetch strategy

The built-in `service-worker.published.js` service worker resolves requests using a *cache-first* strategy. This means that the service worker prefers to return cached content, regardless of whether the user has network access or newer content is available on the server.

The cache-first strategy is valuable because:

* **It ensures reliability.** Network access isn't a boolean state. A user isn't simply online or offline:

  * The user's device may assume it's online, but the network might be so slow as to be impractical to wait for.
  * The network might return invalid results for certain URLs, such as when there's a captive WIFI portal that's currently blocking or redirecting certain requests.

  This is why the browser's `navigator.onLine` API isn't reliable and shouldn't be depended upon.

* **It ensures correctness.** When building a cache of offline resources, the service worker uses content hashing to guarantee it has fetched a complete and self-consistent snapshot of resources at a single instant in time. This cache is then used as an atomic unit. There's no point asking the network for newer resources, since the only versions required are the ones already cached. Anything else risks inconsistency and incompatibility (for example, trying to use versions of .NET assemblies that weren't compiled together).

To prevent the browser from fetching `service-worker-assets.js` from its HTTP cache, for example to resolve temporary integrity check failures when deploying a new version of the service worker, the service worker registration in `wwwroot/index.html` uses [`updateViaCache`](https://developer.mozilla.org/docs/Web/API/ServiceWorkerRegistration/updateViaCache) set to `none`:

```
<script>
  navigator.serviceWorker.register('/service-worker.js', { updateViaCache: 'none' });
</script>
```

The [`updateViaCache: 'none'` option](https://developer.mozilla.org/docs/Web/API/ServiceWorkerRegistration/updateViaCache) ensures that:

* The browser doesn't use cached versions of the service worker script.
* Service worker updates are applied reliably without being blocked by HTTP caching.
* PWA applications can update their service workers more predictably.

This addresses caching issues that can prevent service worker updates from being applied correctly, which is particularly important for PWAs that rely on service workers for offline functionality.

### Background updates

As a mental model, you can think of an offline-first PWA as behaving like a mobile app that can be installed. The app starts up immediately regardless of network connectivity, but the installed app logic comes from a point-in-time snapshot that might not be the latest version.

The Blazor PWA template produces apps that automatically try to update themselves in the background whenever the user visits and has a working network connection. The way this works is as follows:

* During compilation, the project generates a *service worker assets manifest*, which is named `service-worker-assets.js`. The manifest lists all the static resources that the app requires to function offline, such as .NET assemblies, JavaScript files, and CSS, including their content hashes. The resource list is loaded by the service worker so that it knows which resources to cache.
* Each time the user visits the app, the browser re-requests `service-worker.js` and `service-worker-assets.js` in the background. The files are compared byte-for-byte with the existing installed service worker. If the server returns changed content for either of these files, the service worker attempts to install a new version of itself.
* When installing a new version of itself, the service worker creates a new, separate cache for offline resources and starts populating the cache with resources listed in `service-worker-assets.js`. This logic is implemented in the `onInstall` function inside `service-worker.published.js`.
* The process completes successfully when all of the resources are loaded without error and all content hashes match. If successful, the new service worker enters a *waiting for activation* state. As soon as the user closes the app (no remaining app tabs or windows), the new service worker becomes *active* and is used for subsequent app visits. The old service worker and its cache are deleted.
* If the process doesn't complete successfully, the new service worker instance is discarded. The update process is attempted again on the user's next visit, when hopefully the client has a better network connection that can complete the requests.

Customize this process by editing the service worker logic. None of the preceding behavior is specific to Blazor but is merely the default experience provided by the PWA template option. For more information, see [MDN web docs: Service Worker API](https://developer.mozilla.org/docs/Web/API/Service_Worker_API).

### How requests are resolved

As described in the [Cache-first fetch strategy](#cache-first-fetch-strategy) section, the default service worker uses a *cache-first* strategy, meaning that it tries to serve cached content when available. If there is no content cached for a certain URL, for example when requesting data from a backend API, the service worker falls back on a regular network request. The network request succeeds if the server is reachable. This logic is implemented inside `onFetch` function within `service-worker.published.js`.

If the app's Razor components rely on requesting data from backend APIs and you want to provide a friendly user experience for failed requests due to network unavailability, implement logic within the app's components. For example, use [`try/catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) around [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) requests.

### Support server-rendered pages

Consider what happens when the user first navigates to a URL such as `/counter` or any other deep link in the app. In these cases, you don't want to return content cached as `/counter`, but instead need the browser to load the content cached as `/index.html` to start up your Blazor WebAssembly app. These initial requests are known as *navigation* requests, as opposed to:

* `subresource` requests for images, stylesheets, or other files.
* `fetch/XHR` requests for API data.

The default service worker contains special-case logic for navigation requests. The service worker resolves the requests by returning the cached content for `/index.html`, regardless of the requested URL. This logic is implemented in the `onFetch` function inside `service-worker.published.js`.

If your app has certain URLs that must return server-rendered HTML, and not serve `/index.html` from the cache, then you need to edit the logic in your service worker. If all URLs containing `/Identity/` need to be handled as regular online-only requests to the server, then modify `service-worker.published.js` `onFetch` logic. Locate the following code:

```
const shouldServeIndexHtml = event.request.mode === 'navigate';
```

Change the code to the following:

```
const shouldServeIndexHtml = event.request.mode === 'navigate'
  && !event.request.url.includes('/Identity/');
```

If you don't do this, then regardless of network connectivity, the service worker intercepts requests for such URLs and resolves them using `/index.html`.

Add additional endpoints for external authentication providers to the check. In the following example, `/signin-google` for Google authentication is added to the check:

```
const shouldServeIndexHtml = event.request.mode === 'navigate'
  && !event.request.url.includes('/Identity/')
  && !event.request.url.includes('/signin-google');
```

No action is required for the `Development` environment, where content is always fetched from the network.

### Control asset caching

If your project defines the `ServiceWorkerAssetsManifest` MSBuild property, Blazor's build tooling generates a service worker assets manifest with the specified name. The default PWA template produces a project file containing the following property:

```
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
```

The file is placed in the `wwwroot` output directory, so the browser can retrieve this file by requesting `/service-worker-assets.js`. To see the contents of this file, open `/bin/Debug/{TARGET FRAMEWORK}/wwwroot/service-worker-assets.js` in a text editor. However, don't edit the file, as it's regenerated on each build.

The manifest lists:

* Any Blazor-managed resources, such as .NET assemblies and the .NET WebAssembly runtime files required to function offline.
* All resources for publishing to the app's `wwwroot` directory, such as images, stylesheets, and JavaScript files, including static web assets supplied by external projects and NuGet packages.

You can control which of these resources are fetched and cached by the service worker by editing the logic in `onInstall` in `service-worker.published.js`. The service worker fetches and caches files matching typical web file name extensions such as `.html`, `.css`, `.js`, and `.wasm`, plus file types specific to Blazor WebAssembly, such as `.pdb` files (all versions) and `.dll` files (ASP.NET Core in .NET 7 or earlier).

To include additional resources that aren't present in the app's `wwwroot` directory, define extra MSBuild `ItemGroup` entries, as shown in the following example:

```
<ItemGroup>
  <ServiceWorkerAssetsManifestItem Include="MyDirectory\AnotherFile.json"
    RelativePath="MyDirectory\AnotherFile.json" AssetUrl="files/AnotherFile.json" />
</ItemGroup>
```

The `AssetUrl` metadata specifies the base-relative URL that the browser should use when fetching the resource to cache. This can be independent of its original source file name on disk.

Important

Adding a `ServiceWorkerAssetsManifestItem` doesn't cause the file to be published in the app's `wwwroot` directory. The publish output must be controlled separately. The `ServiceWorkerAssetsManifestItem` only causes an additional entry to appear in the service worker assets manifest.

## Push notifications

Like any other PWA, a Blazor WebAssembly PWA can receive push notifications from a backend server. The mechanism is independent of Blazor WebAssembly, and the server can send push notifications at any time, even when the user isn't actively using the app. For more information, see [Push notifications for ASP.NET Core Blazor Progressive Web Applications (PWAs)](push-notifications?view=aspnetcore-10.0).

## Caveats for offline PWAs

Not all apps should attempt to support offline use. Offline support adds significant complexity, while not always being relevant for the use cases required.

Offline support is usually relevant only:

* If the primary data store is local to the browser. For example, the approach is relevant in an app with a UI for an [IoT](https://en.wikipedia.org/wiki/Internet_of_things) device that stores data in `localStorage` or [IndexedDB](https://developer.mozilla.org/docs/Web/API/IndexedDB_API).
* If the app performs a significant amount of work to fetch and cache the backend API data relevant to each user so that they can navigate through the data offline. If the app must support editing, a system for tracking changes and synchronizing data with the backend must be built.
* If the goal is to guarantee that the app loads immediately regardless of network conditions. Implement a suitable user experience around backend API requests to show the progress of requests and behave gracefully when requests fail due to network unavailability.

Additionally, offline-capable PWAs must deal with a range of additional complications. Developers should carefully familiarize themselves with the caveats in the following sections.

### Offline support only when published

During development you typically want to see each change reflected immediately in the browser without going through a background update process. Therefore, Blazor's PWA template enables offline support only when published.

When building an offline-capable app, it's not enough to test the app in the `Development` environment. You must test the app in its published state to understand how it responds to different network conditions.

### Update completion after user navigation away from app

Updates don't complete until the user has navigated away from the app in all tabs. As explained in the [Background updates](#background-updates) section, after you deploy an update to the app, the browser fetches the updated service worker files to begin the update process.

What surprises many developers is that, even when this update completes, it doesn't take effect until the user has navigated away in all tabs. It isn't sufficient to refresh the tab displaying the app, even if it's the only tab displaying the app. Until your app is completely closed, the new service worker remains in the *waiting to activate* status. This isn't specific to Blazor, but rather is a standard web platform behavior.

This commonly troubles developers who are trying to test updates to their service worker or offline cached resources. If you check in the browser's developer tools, you may see something like the following:

For as long as the list of "clients," which are tabs or windows displaying your app, is nonempty, the worker continues waiting. The reason service workers do this is to guarantee consistency. Consistency means that all resources are fetched from the same atomic cache.

When testing changes, you may find it convenient to select the "skipWaiting" link as shown in the preceding screenshot, then reload the page. You can automate this for all users by coding your service worker to [skip the "waiting" phase and immediately activate on update](https://web.dev/service-worker-lifecycle/#skip-waiting). If you skip the waiting phase, you're giving up the guarantee that resources are always fetched consistently from the same cache instance.

### Users may run any historical version of the app

Web developers habitually expect that users only run the latest deployed version of their web app, since that's normal within the traditional web distribution model. However, an offline-first PWA is more akin to a native mobile app, where users aren't necessarily running the latest version.

As explained in the [Background updates](#background-updates) section, after you deploy an update to your app, each existing user continues to use a previous version for at least one further visit because the update occurs in the background and isn't activated until the user thereafter navigates away. Plus, the previous version being used isn't necessarily the previous one you deployed. The previous version can be *any* historical version, depending on when the user last completed an update.

This can be an issue if the frontend and backend parts of your app require agreement about the schema for API requests. You must not deploy backward-incompatible API schema changes until you can be sure that all users have upgraded. Alternatively, block users from using incompatible older versions of the app. This scenario requirement is the same as for native mobile apps. If you deploy a breaking change in server APIs, the client app is broken for users who haven't yet updated.

If possible, don't deploy breaking changes to your backend APIs. If you must do so, consider using [standard Service Worker APIs such as ServiceWorkerRegistration](https://developer.mozilla.org/docs/Web/API/ServiceWorkerRegistration) to determine whether the app is up-to-date, and if not, to prevent usage.

### Interference with server-rendered pages

As described in the [Support server-rendered pages](#support-server-rendered-pages) section, if you want to bypass the service worker's behavior of returning `/index.html` contents for all navigation requests, edit the logic in your service worker.

### All service worker asset manifest contents are cached

As described in the [Control asset caching](#control-asset-caching) section, the file `service-worker-assets.js` is generated during build and lists all assets the service worker should fetch and cache.

Since this list includes everything emitted to `wwwroot`, including content supplied by external packages and projects, you must be careful not to put too much content there. If the `wwwroot` directory contains millions of images, the service worker tries to fetch and cache them all, consuming excessive bandwidth and most likely not completing successfully.

Implement arbitrary logic to control which subset of the manifest's contents should be fetched and cached by editing the `onInstall` function in `service-worker.published.js`.

### Interaction with authentication

The PWA template can be used in conjunction with authentication. An offline-capable PWA can also support authentication when the user has initial network connectivity.

When a user doesn't have network connectivity, they can't authenticate or obtain access tokens. Attempting to visit the login page without network access results in a "network error" message. You must design a UI flow that allows the user perform useful tasks while offline without attempting to authenticate the user or obtain access tokens. Alternatively, you can design the app to gracefully fail when the network isn't available. If the app can't be designed to handle these scenarios, you might not want to enable offline support.

When an app that's designed for online and offline use is online again:

* The app might need to provision a new access token.
* The app must detect if a different user is signed into the service so that it can apply operations to the user's account that were made while they were offline.

To create an offline PWA app that interacts with authentication:

* Replace the [AccountClaimsPrincipalFactory\<TAccount>](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accountclaimsprincipalfactory-1) with a factory that stores the last signed-in user and uses the stored user when the app is offline.
* Enqueue operations while the app is offline and apply them when the app returns online.
* During sign out, clear the stored user.

The [`CarChecker`](https://github.com/SteveSandersonMS/CarChecker) sample app demonstrates the preceding approaches. See the following parts of the app:

* `OfflineAccountClaimsPrincipalFactory` (`Client/Data/OfflineAccountClaimsPrincipalFactory.cs`)
* `LocalVehiclesStore` (`Client/Data/LocalVehiclesStore.cs`)
* `LoginStatus` component (`Client/Shared/LoginStatus.razor`)

## Related content

[Client-side SignalR cross-origin negotiation for authentication](../fundamentals/signalr?view=aspnetcore-10.0#client-side-signalr-cross-origin-negotiation-for-authentication)

* [Troubleshoot integrity PowerShell script](../host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures?view=aspnetcore-10.0#troubleshoot-integrity-powershell-script)
* [Client-side SignalR cross-origin negotiation for authentication](../fundamentals/signalr?view=aspnetcore-10.0#client-side-signalr-cross-origin-negotiation-for-authentication)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/routing.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Routing in ASP.NET Core

By [Ryan Nowak](https://github.com/rynowak), [Kirk Larkin](https://twitter.com/serpent5), and [Rick Anderson](https://twitter.com/RickAndMSFT)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Routing is responsible for matching incoming HTTP requests and dispatching those requests to the app's executable endpoints. [Endpoints](#endpoints) are the app's units of executable request-handling code. Endpoints are defined in the app and configured when the app starts. The endpoint matching process can extract values from the request's URL and provide those values for request processing. Using endpoint information from the app, routing is also able to generate URLs that map to endpoints.

Apps can configure routing using:

* Controllers
* Razor Pages
* SignalR
* gRPC Services
* Endpoint-enabled [middleware](middleware/?view=aspnetcore-10.0) such as [Health Checks](../host-and-deploy/health-checks?view=aspnetcore-10.0).
* Delegates and lambdas registered with routing.

This article covers low-level details of ASP.NET Core routing. For information on configuring routing:

* For controllers, see [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0).
* For Razor Pages conventions, see [Razor Pages route and app conventions in ASP.NET Core](../razor-pages/razor-pages-conventions?view=aspnetcore-10.0).
* For Blazor routing and navigation guidance, which adds to or supersedes the guidance in this article, see [ASP.NET Core Blazor routing](../blazor/fundamentals/routing?view=aspnetcore-10.0) and [ASP.NET Core Blazor navigation](../blazor/fundamentals/navigation?view=aspnetcore-10.0).

## Routing basics

The following code shows a basic example of routing:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding example includes a single endpoint using the [MapGet](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapget) method:

* When an HTTP `GET` request is sent to the root URL `/`:

  * The request delegate executes.
  * `Hello World!` is written to the HTTP response.

* If the request method is not `GET` or the root URL is not `/`, no route matches and an HTTP 404 is returned.

Routing uses a pair of middleware, registered by [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints):

* `UseRouting` adds route matching to the middleware pipeline. This middleware looks at the set of endpoints defined in the app, and selects the [best match](#urlm) based on the request.
* `UseEndpoints` adds endpoint execution to the middleware pipeline. It runs the delegate associated with the selected endpoint.

Apps typically don't need to call `UseRouting` or `UseEndpoints`. [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) configures a middleware pipeline that wraps middleware added in `Program.cs` with `UseRouting` and `UseEndpoints`. However, apps can change the order in which `UseRouting` and `UseEndpoints` run by calling these methods explicitly. For example, the following code makes an explicit call to `UseRouting`:

```
app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");
```

In the preceding code:

* The call to `app.Use` registers a custom middleware that runs at the start of the pipeline.
* The call to `UseRouting` configures the route matching middleware to run *after* the custom middleware.
* The endpoint registered with `MapGet` runs at the end of the pipeline.

If the preceding example didn't include a call to `UseRouting`, the custom middleware would run *after* the route matching middleware.

**Note:** Routes added directly to the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) execute at the ***end*** of the pipeline.

### Endpoints

[]()

The `MapGet` method is used to define an **endpoint**. An endpoint is something that can be:

* Selected, by matching the URL and HTTP method.
* Executed, by running the delegate.

Endpoints that can be matched and executed by the app are configured in `UseEndpoints`. For example, [MapGet](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapget), [MapPost](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mappost), and [similar methods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions) connect request delegates to the routing system. Additional methods can be used to connect ASP.NET Core framework features to the routing system:

* [MapRazorPages for Razor Pages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)
* [MapControllers for controllers](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollers)
* [MapHub\<THub> for SignalR](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubroutebuilder.maphub)
* [MapGrpcService\<TService> for gRPC](../grpc/aspnetcore?view=aspnetcore-10.0)

The following example shows routing with a more sophisticated route template:

```
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");
```

The string `/hello/{name:alpha}` is a **route template**. A route template is used to configure how the endpoint is matched. In this case, the template matches:

* A URL like `/hello/Docs`
* Any URL path that begins with `/hello/` followed by a sequence of alphabetic characters. `:alpha` applies a route constraint that matches only alphabetic characters. [Route constraints](#route-constraints) are explained later in this article.

The second segment of the URL path, `{name:alpha}`:

* Is bound to the `name` parameter.
* Is captured and stored in [HttpRequest.RouteValues](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.routevalues).

The following example shows routing with [health checks](../host-and-deploy/health-checks?view=aspnetcore-10.0) and authorization:

```
app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
```

The preceding example demonstrates how:

* The authorization middleware can be used with routing.
* Endpoints can be used to configure authorization behavior.

The [MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks) call adds a health check endpoint. Chaining [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) on to this call attaches an authorization policy to the endpoint.

Calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) adds the authentication and authorization middleware. These middleware are placed between [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and `UseEndpoints` so that they can:

* See which endpoint was selected by `UseRouting`.
* Apply an authorization policy before [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) dispatches to the endpoint.

[]()

### Endpoint metadata

In the preceding example, there are two endpoints, but only the health check endpoint has an authorization policy attached. If the request matches the health check endpoint, `/healthz`, an authorization check is performed. This demonstrates that endpoints can have extra data attached to them. This extra data is called endpoint **metadata**:

* The metadata can be processed by routing-aware middleware.
* The metadata can be of any .NET type.

## Routing concepts

The routing system builds on top of the middleware pipeline by adding the powerful **endpoint** concept. Endpoints represent units of the app's functionality that are distinct from each other in terms of routing, authorization, and any number of ASP.NET Core's systems.

[]()

### ASP.NET Core endpoint definition

An ASP.NET Core endpoint is:

* Executable: Has a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.requestdelegate#microsoft-aspnetcore-http-endpoint-requestdelegate).
* Extensible: Has a [Metadata](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.metadata) collection.
* Selectable: Optionally, has [routing information](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.routepattern).
* Enumerable: The collection of endpoints can be listed by retrieving the [EndpointDataSource](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource) from [DI](dependency-injection?view=aspnetcore-10.0).

The following code shows how to retrieve and inspect the endpoint matching the current request:

```
app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");
```

The endpoint, if selected, can be retrieved from the `HttpContext`. Its properties can be inspected. Endpoint objects are immutable and cannot be modified after creation. The most common type of endpoint is a [RouteEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint). `RouteEndpoint` includes information that allows it to be selected by the routing system.

In the preceding code, [app.Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) configures an inline [middleware](middleware/?view=aspnetcore-10.0).

[]()

The following code shows that, depending on where `app.Use` is called in the pipeline, there may not be an endpoint:

```
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});
```

The preceding sample adds `Console.WriteLine` statements that display whether or not an endpoint has been selected. For clarity, the sample assigns a display name to the provided `/` endpoint.

The preceding sample also includes calls to `UseRouting` and `UseEndpoints` to control exactly when these middleware run within the pipeline.

Running this code with a URL of `/` displays:

```
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
```

Running this code with any other URL displays:

```
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
```

This output demonstrates that:

* The endpoint is always null before `UseRouting` is called.
* If a match is found, the endpoint is non-null between `UseRouting` and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).
* The `UseEndpoints` middleware is **terminal** when a match is found. [Terminal middleware](#tm) is defined later in this article.
* The middleware after `UseEndpoints` execute only when no match is found.

The `UseRouting` middleware uses the [SetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.setendpoint) method to attach the endpoint to the current context. It's possible to replace the `UseRouting` middleware with custom logic and still get the benefits of using endpoints. Endpoints are a low-level primitive like middleware, and aren't coupled to the routing implementation. Most apps don't need to replace `UseRouting` with custom logic.

The `UseEndpoints` middleware is designed to be used in tandem with the `UseRouting` middleware. The core logic to execute an endpoint isn't complicated. Use [GetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.getendpoint) to retrieve the endpoint, and then invoke its [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.requestdelegate#microsoft-aspnetcore-http-endpoint-requestdelegate) property.

The following code demonstrates how middleware can influence or react to routing:

```
app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
```

```
public class RequiresAuditAttribute : Attribute { }
```

The preceding example demonstrates two important concepts:

* Middleware can run before `UseRouting` to modify the data that routing operates upon.
  * Usually middleware that appears before routing modifies some property of the request, such as [UseRewriter](/en-us/dotnet/api/microsoft.aspnetcore.builder.rewritebuilderextensions.userewriter), [UseHttpMethodOverride](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpmethodoverrideextensions.usehttpmethodoverride), or [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase).

* Middleware can run between `UseRouting` and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) to process the results of routing before the endpoint is executed.

  * Middleware that runs between `UseRouting` and `UseEndpoints`:

    * Usually inspects metadata to understand the endpoints.
    * Often makes security decisions, as done by `UseAuthorization` and `UseCors`.

  * The combination of middleware and metadata allows configuring policies per-endpoint.

The preceding code shows an example of a custom middleware that supports per-endpoint policies. The middleware writes an *audit log* of access to sensitive data to the console. The middleware can be configured to *audit* an endpoint with the `RequiresAuditAttribute` metadata. This sample demonstrates an *opt-in* pattern where only endpoints that are marked as sensitive are audited. It's possible to define this logic in reverse, auditing everything that isn't marked as safe, for example. The endpoint metadata system is flexible. This logic could be designed in whatever way suits the use case.

The preceding sample code is intended to demonstrate the basic concepts of endpoints. **The sample is not intended for production use**. A more complete version of an *audit log* middleware would:

* Log to a file or database.
* Include details such as the user, IP address, name of the sensitive endpoint, and more.

The audit policy metadata `RequiresAuditAttribute` is defined as an `Attribute` for easier use with class-based frameworks such as controllers and SignalR. When using *route to code*:

* Metadata is attached with a builder API.
* Class-based frameworks include all attributes on the corresponding method and class when creating endpoints.

The best practices for metadata types are to define them either as interfaces or attributes. Interfaces and attributes allow code reuse. The metadata system is flexible and doesn't impose any limitations.

[]()

### Compare terminal middleware with routing

The following example demonstrates both terminal middleware and routing:

```
// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");
```

The style of middleware shown with `Approach 1:` is **terminal middleware**. It's called terminal middleware because it does a matching operation:

* The matching operation in the preceding sample is `Path == "/"` for the middleware and `Path == "/Routing"` for routing.
* When a match is successful, it executes some functionality and returns, rather than invoking the `next` middleware.

It's called terminal middleware because it terminates the search, executes some functionality, and then returns.

The following list compares terminal middleware with routing:

* Both approaches allow terminating the processing pipeline:

  * Middleware terminates the pipeline by returning rather than invoking `next`.
  * Endpoints are always terminal.

* Terminal middleware allows positioning the middleware at an arbitrary place in the pipeline:
  * Endpoints execute at the position of [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).

* Terminal middleware allows arbitrary code to determine when the middleware matches:

  * Custom route matching code can be verbose and difficult to write correctly.
  * Routing provides straightforward solutions for typical apps. Most apps don't require custom route matching code.

* Endpoints interface with middleware such as `UseAuthorization` and `UseCors`.
  * Using a terminal middleware with `UseAuthorization` or `UseCors` requires manual interfacing with the authorization system.

An [endpoint](#endpoints) defines both:

* A delegate to process requests.
* A collection of arbitrary metadata. The metadata is used to implement cross-cutting concerns based on policies and configuration attached to each endpoint.

Terminal middleware can be an effective tool, but can require:

* A significant amount of coding and testing.
* Manual integration with other systems to achieve the desired level of flexibility.

Consider integrating with routing before writing a terminal middleware.

Existing terminal middleware that integrates with [Map](middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline) or [MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen) can usually be turned into a routing aware endpoint. [MapHealthChecks](https://github.com/dotnet/AspNetCore/blob/main/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs#L16) demonstrates the pattern for router-ware:

* Write an extension method on [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder).
* Create a nested middleware pipeline using [CreateApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder.createapplicationbuilder).
* Attach the middleware to the new pipeline. In this case, [UseHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckapplicationbuilderextensions.usehealthchecks).
* [Build](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder.build) the middleware pipeline into a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate).
* Call `Map` and provide the new middleware pipeline.
* Return the builder object provided by `Map` from the extension method.

The following code shows use of [MapHealthChecks](../host-and-deploy/health-checks?view=aspnetcore-10.0):

```
app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
```

The preceding sample shows why returning the builder object is important. Returning the builder object allows the app developer to configure policies such as authorization for the endpoint. In this example, the health checks middleware has no direct integration with the authorization system.

The metadata system was created in response to the problems encountered by extensibility authors using terminal middleware. It's problematic for each middleware to implement its own integration with the authorization system.

[]()

### URL matching

* Is the process by which routing matches an incoming request to an [endpoint](#endpoints).
* Is based on data in the URL path and headers.
* Can be extended to consider any data in the request.

When a routing middleware executes, it sets an `Endpoint` and route values to a [request feature](request-features?view=aspnetcore-10.0) on the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) from the current request:

* Calling [HttpContext.GetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.getendpoint) gets the endpoint.
* `HttpRequest.RouteValues` gets the collection of route values.

[Middleware](middleware/?view=aspnetcore-10.0) that runs after the routing middleware can inspect the endpoint and take action. For example, an authorization middleware can interrogate the endpoint's metadata collection for an authorization policy. After all of the middleware in the request processing pipeline is executed, the selected endpoint's delegate is invoked.

The routing system in endpoint routing is responsible for all dispatching decisions. Because the middleware applies policies based on the selected endpoint, it's important that:

* Any decision that can affect dispatching or the application of security policies is made inside the routing system.

Warning

For backward-compatibility, when a Controller or Razor Pages endpoint delegate is executed, the properties of [RouteContext.RouteData](/en-us/dotnet/api/microsoft.aspnetcore.routing.routecontext.routedata) are set to appropriate values based on the request processing performed thus far.

The `RouteContext` type will be marked obsolete in a future release:

* Migrate `RouteData.Values` to `HttpRequest.RouteValues`.
* Migrate `RouteData.DataTokens` to retrieve [IDataTokensMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.idatatokensmetadata) from the endpoint metadata.

URL matching operates in a configurable set of phases. In each phase, the output is a set of matches. The set of matches can be narrowed down further by the next phase. The routing implementation does not guarantee a processing order for matching endpoints. **All** possible matches are processed at once. The URL matching phases occur in the following order. ASP.NET Core:

1. Processes the URL path against the set of endpoints and their route templates, collecting **all** of the matches.
2. Takes the preceding list and removes matches that fail with route constraints applied.
3. Takes the preceding list and removes matches that fail the set of [MatcherPolicy](/en-us/dotnet/api/microsoft.aspnetcore.routing.matcherpolicy) instances.
4. Uses the [EndpointSelector](/en-us/dotnet/api/microsoft.aspnetcore.routing.matching.endpointselector) to make a final decision from the preceding list.

The list of endpoints is prioritized according to:

* The [RouteEndpoint.Order](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.order)
* The [route template precedence](#rtp)

All matching endpoints are processed in each phase until the [EndpointSelector](/en-us/dotnet/api/microsoft.aspnetcore.routing.matching.endpointselector) is reached. The `EndpointSelector` is the final phase. It chooses the highest priority endpoint from the matches as the best match. If there are other matches with the same priority as the best match, an ambiguous match exception is thrown.

The route precedence is computed based on a **more specific** route template being given a higher priority. For example, consider the templates `/hello` and `/{message}`:

* Both match the URL path `/hello`.
* `/hello` is more specific and therefore higher priority.

In general, route precedence does a good job of choosing the best match for the kinds of URL schemes used in practice. Use [Order](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.order#microsoft-aspnetcore-routing-routeendpoint-order) only when necessary to avoid an ambiguity.

Due to the kinds of extensibility provided by routing, it isn't possible for the routing system to compute ahead of time the ambiguous routes. Consider an example such as the route templates `/{message:alpha}` and `/{message:int}`:

* The `alpha` constraint matches only alphabetic characters.
* The `int` constraint matches only numbers.
* These templates have the same route precedence, but there's no single URL they both match.
* If the routing system reported an ambiguity error at startup, it would block this valid use case.

Warning

The order of operations inside [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) doesn't influence the behavior of routing, with one exception. [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) and [MapAreaRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcarearoutebuilderextensions.maparearoute) automatically assign an order value to their endpoints based on the order they are invoked. This simulates long-time behavior of controllers without the routing system providing the same guarantees as older routing implementations.

Endpoint routing in ASP.NET Core:

* Doesn't have the concept of routes.
* Doesn't provide ordering guarantees. All endpoints are processed at once.

[]()

### Route template precedence and endpoint selection order

[Route template precedence](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Template/RoutePrecedence.cs#L16) is a system that assigns each route template a value based on how specific it is. Route template precedence:

* Avoids the need to adjust the order of endpoints in common cases.
* Attempts to match the common-sense expectations of routing behavior.

For example, consider templates `/Products/List` and `/Products/{id}`. It would be reasonable to assume that `/Products/List` is a better match than `/Products/{id}` for the URL path `/Products/List`. This works because the literal segment `/List` is considered to have better precedence than the parameter segment `/{id}`.

The details of how precedence works are coupled to how route templates are defined:

* Templates with more segments are considered more specific.
* A segment with literal text is considered more specific than a parameter segment.
* A parameter segment with a constraint is considered more specific than one without.
* A complex segment is considered as specific as a parameter segment with a constraint.
* Catch-all parameters are the least specific. See **catch-all** in the [Route templates](#rtr) section for important information on catch-all routes.

[]()

### URL generation concepts

URL generation:

* Is the process by which routing can create a URL path based on a set of route values.
* Allows for a logical separation between endpoints and the URLs that access them.

Endpoint routing includes the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) API. `LinkGenerator` is a singleton service available from [DI](dependency-injection?view=aspnetcore-10.0). The `LinkGenerator` API can be used outside of the context of an executing request. [Mvc.IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) and scenarios that rely on [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper), such as [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0), HTML Helpers, and [Action Results](../mvc/controllers/actions?view=aspnetcore-10.0), use the `LinkGenerator` API internally to provide link generating capabilities.

The link generator is backed by the concept of an **address** and **address schemes**. An address scheme is a way of determining the endpoints that should be considered for link generation. For example, the route name and route values scenarios many users are familiar with from controllers and Razor Pages are implemented as an address scheme.

The link generator can link to controllers and Razor Pages via the following extension methods:

* [GetPathByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.getpathbyaction)
* [GetUriByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.geturibyaction)
* [GetPathByPage](/en-us/dotnet/api/microsoft.aspnetcore.routing.pagelinkgeneratorextensions.getpathbypage)
* [GetUriByPage](/en-us/dotnet/api/microsoft.aspnetcore.routing.pagelinkgeneratorextensions.geturibypage)

Overloads of these methods accept arguments that include the `HttpContext`. These methods are functionally equivalent to [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.urlhelper.action) and [Url.Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.page), but offer additional flexibility and options.

The `GetPath*` methods are most similar to `Url.Action` and `Url.Page`, in that they generate a URI containing an absolute path. The `GetUri*` methods always generate an absolute URI containing a scheme and host. The methods that accept an `HttpContext` generate a URI in the context of the executing request. The [ambient](#ambient) route values, URL base path, scheme, and host from the executing request are used unless overridden.

[LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) is called with an address. Generating a URI occurs in two steps:

1. An address is bound to a list of endpoints that match the address.
2. Each endpoint's [RoutePattern](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.routepattern#microsoft-aspnetcore-routing-routeendpoint-routepattern) is evaluated until a route pattern that matches the supplied values is found. The resulting output is combined with the other URI parts supplied to the link generator and returned.

The methods provided by [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) support standard link generation capabilities for any type of address. The most convenient way to use the link generator is through extension methods that perform operations for a specific address type:

| Extension Method                                                                                  | Description                                                         |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [GetPathByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.getpathbyaddress) | Generates a URI with an absolute path based on the provided values. |
| [GetUriByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.geturibyaddress)   | Generates an absolute URI based on the provided values.             |

Warning

Pay attention to the following implications of calling [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) methods:

* Use `GetUri*` extension methods with caution in an app configuration that doesn't validate the `Host` header of incoming requests. If the `Host` header of incoming requests isn't validated, untrusted request input can be sent back to the client in URIs in a view or page. We recommend that all production apps configure their server to validate the `Host` header against known valid values.

* Use [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) with caution in middleware in combination with `Map` or `MapWhen`. `Map*` changes the base path of the executing request, which affects the output of link generation. All of the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) APIs allow specifying a base path. Specify an empty base path to undo the `Map*` affect on link generation.

### Middleware example

In the following example, a middleware uses the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) API to create a link to an action method that lists store products. Using the link generator by injecting it into a class and calling `GenerateLink` is available to any class in an app:

```
public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}
```

[]()

## Route templates

Tokens within `{}` define route parameters that are bound if the route is matched. More than one route parameter can be defined in a route segment, but route parameters must be separated by a literal value. For example:

`{controller=Home}{action=Index}`

isn't a valid route, because there's no literal value between `{controller}` and `{action}`. Route parameters must have a name and may have additional attributes specified.

Literal text other than route parameters (for example, `{id}`) and the path separator `/` must match the text in the URL. Text matching is case-insensitive and based on the decoded representation of the URL's path. To match a literal route parameter delimiter `{` or `}`, escape the delimiter by repeating the character. For example `{{` or `}}`.

Asterisk `*` or double asterisk `**`:

* Can be used as a prefix to a route parameter to bind to the rest of the URI.

* Are called a **catch-all** parameters. For example, `blog/{**slug}`:

  * Matches any URI that starts with `blog/` and has any value following it.
  * The value following `blog/` is assigned to the [slug](https://developer.mozilla.org/docs/Glossary/Slug) route value.

```
public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
```

Catch-all parameters can also match the empty string.

The catch-all parameter escapes the appropriate characters when the route is used to generate a URL, including path separator `/` characters. For example, the route `foo/{*path}` with route values `{ path = "my/path" }` generates `foo/my%2Fpath`. Note the escaped forward slash. To round-trip path separator characters, use the `**` route parameter prefix. The route `foo/{**path}` with `{ path = "my/path" }` generates `foo/my/path`.

URL patterns that attempt to capture a file name with an optional file extension have additional considerations. For example, consider the template `files/{filename}.{ext?}`. When values for both `filename` and `ext` exist, both values are populated. If only a value for `filename` exists in the URL, the route matches because the trailing `.` is optional. The following URLs match this route:

* `/files/myFile.txt`
* `/files/myFile`

Route parameters may have **default values** designated by specifying the default value after the parameter name separated by an equals sign (`=`). For example, `{controller=Home}` defines `Home` as the default value for `controller`. The default value is used if no value is present in the URL for the parameter. Route parameters are made optional by appending a question mark (`?`) to the end of the parameter name. For example, `id?`. The difference between optional values and default route parameters is:

* A route parameter with a default value always produces a value.
* An optional parameter has a value only when a value is provided by the request URL.

Route parameters may have constraints that must match the route value bound from the URL. Adding `:` and constraint name after the route parameter name specifies an inline constraint on a route parameter. If the constraint requires arguments, they're enclosed in parentheses `(...)` after the constraint name. Multiple *inline constraints* can be specified by appending another `:` and constraint name.

The constraint name and arguments are passed to the [IInlineConstraintResolver](/en-us/dotnet/api/microsoft.aspnetcore.routing.iinlineconstraintresolver) service to create an instance of [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) to use in URL processing. For example, the route template `blog/{article:minlength(10)}` specifies a `minlength` constraint with the argument `10`. For more information on route constraints and a list of the constraints provided by the framework, see the [Route constraints](#route-constraints) section.

Route parameters may also have parameter transformers. Parameter transformers transform a parameter's value when generating links and matching actions and pages to URLs. Like constraints, parameter transformers can be added inline to a route parameter by adding a `:` and transformer name after the route parameter name. For example, the route template `blog/{article:slugify}` specifies a `slugify` transformer. For more information on parameter transformers, see the [Parameter transformers](#parameter-transformers) section.

The following table demonstrates example route templates and their behavior.

| Route Template                           | Example Matching URI    | The request URI…                                                            |
| ---------------------------------------- | ----------------------- | --------------------------------------------------------------------------- |
| `hello`                                  | `/hello`                | Only matches the single path `/hello`.                                      |
| `{Page=Home}`                            | `/`                     | Matches and sets `Page` to `Home`.                                          |
| `{Page=Home}`                            | `/Contact`              | Matches and sets `Page` to `Contact`.                                       |
| `{controller}/{action}/{id?}`            | `/Products/List`        | Maps to the `Products` controller and `List` action.                        |
| `{controller}/{action}/{id?}`            | `/Products/Details/123` | Maps to the `Products` controller and `Details` action with`id` set to 123. |
| `{controller=Home}/{action=Index}/{id?}` | `/`                     | Maps to the `Home` controller and `Index` method. `id` is ignored.          |
| `{controller=Home}/{action=Index}/{id?}` | `/Products`             | Maps to the `Products` controller and `Index` method. `id` is ignored.      |

Using a template is generally the simplest approach to routing. Constraints and defaults can also be specified outside the route template.

### Complex segments

Complex segments are processed by matching up literal delimiters from right to left in a [non-greedy](#greedy) way. For example, `[Route("/a{b}c{d}")]` is a complex segment. Complex segments work in a particular way that must be understood to use them successfully. The example in this section demonstrates why complex segments only really work well when the delimiter text doesn't appear inside the parameter values. Using a [regex](/en-us/dotnet/standard/base-types/regular-expressions) and then manually extracting the values is needed for more complex cases.

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

This is a summary of the steps that routing performs with the template `/a{b}c{d}` and the URL path `/abcd`. The `|` is used to help visualize how the algorithm works:

* The first literal, right to left, is `c`. So `/abcd` is searched from right and finds `/ab|c|d`.
* Everything to the right (`d`) is now matched to the route parameter `{d}`.
* The next literal, right to left, is `a`. So `/ab|c|d` is searched starting where we left off, then `a` is found `/|a|b|c|d`.
* The value to the right (`b`) is now matched to the route parameter `{b}`.
* There is no remaining text and no remaining route template, so this is a match.

Here's an example of a negative case using the same template `/a{b}c{d}` and the URL path `/aabcd`. The `|` is used to help visualize how the algorithm works. This case isn't a match, which is explained by the same algorithm:

* The first literal, right to left, is `c`. So `/aabcd` is searched from right and finds `/aab|c|d`.
* Everything to the right (`d`) is now matched to the route parameter `{d}`.
* The next literal, right to left, is `a`. So `/aab|c|d` is searched starting where we left off, then `a` is found `/a|a|b|c|d`.
* The value to the right (`b`) is now matched to the route parameter `{b}`.
* At this point there is remaining text `a`, but the algorithm has run out of route template to parse, so this is not a match.

Since the matching algorithm is [non-greedy](#greedy):

* It matches the smallest amount of text possible in each step.
* Any case where the delimiter value appears inside the parameter values results in not matching.

Regular expressions provide much more control over their matching behavior.

[]()

Greedy matching, also known as *maximal matching* attempts to find the longest possible match in the input text that satisfies the [regex](/en-us/dotnet/standard/base-types/regular-expressions) pattern. Non-greedy matching, also known as *lazy matching*, seeks the shortest possible match in the input text that satisfies the regex pattern.

## Routing with special characters

Routing with special characters can lead to unexpected results. For example, consider a controller with the following action method:

```
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}
```

When `string id` contains the following encoded values, unexpected results might occur:

| ASCII | Encoded |
| ----- | ------- |
| `/`   | `%2F`   |
| ``    | `+`     |

Route parameters are not always URL decoded. This problem may be addressed in the future. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/11544);

## Route constraints

Route constraints execute when a match has occurred to the incoming URL and the URL path is tokenized into route values. Route constraints generally inspect the route value associated via the route template and make a true or false decision about whether the value is acceptable. Some route constraints use data outside the route value to consider whether the request can be routed. For example, the [HttpMethodRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.constraints.httpmethodrouteconstraint) can accept or reject a request based on its HTTP verb. Constraints are used in routing requests and link generation.

Warning

Don't use constraints for input validation. If constraints are used for input validation, invalid input results in a `404` Not Found response. Invalid input should produce a `400` Bad Request with an appropriate error message. Route constraints are used to disambiguate similar routes, not to validate the inputs for a particular route.

The following table demonstrates the default route constraints and their expected behavior:

| constraint          | Example                                     | Example Matches                        | Notes                                                                                                                          |
| ------------------- | ------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `int`               | `{id:int}`                                  | `123456789`, `-123456789`              | Matches any integer                                                                                                            |
| `bool`              | `{active:bool}`                             | `true`, `FALSE`                        | Matches `true` or `false`. Case-insensitive                                                                                    |
| `datetime`          | `{dob:datetime}`                            | `2016-12-31`, `2016-12-31 7:32pm`      | Matches a valid `DateTime` value in the invariant culture. See preceding warning                                               |
| `decimal`           | `{price:decimal}`                           | `49.99`, `-1,000.01`                   | Matches a valid `decimal` value in the invariant culture. See preceding warning                                                |
| `double`            | `{weight:double}`                           | `1.234`, `-1,001.01e8`                 | Matches a valid `double` value in the invariant culture. See preceding warning                                                 |
| `float`             | `{weight:float}`                            | `1.234`, `-1,001.01e8`                 | Matches a valid `float` value in the invariant culture. See preceding warning                                                  |
| `guid`              | `{id:guid}`                                 | `CD2C1638-1638-72D5-1638-DEADBEEF1638` | Matches a valid `Guid` value                                                                                                   |
| `long`              | `{ticks:long}`                              | `123456789`, `-123456789`              | Matches a valid `long` value                                                                                                   |
| `minlength(value)`  | `{username:minlength(4)}`                   | `Rick`                                 | String must be at least 4 characters                                                                                           |
| `maxlength(value)`  | `{filename:maxlength(8)}`                   | `MyFile`                               | String must be no more than 8 characters                                                                                       |
| `length(length)`    | `{filename:length(12)}`                     | `somefile.txt`                         | String must be exactly 12 characters long                                                                                      |
| `length(min,max)`   | `{filename:length(8,16)}`                   | `somefile.txt`                         | String must be at least 8 and no more than 16 characters long                                                                  |
| `min(value)`        | `{age:min(18)}`                             | `19`                                   | Integer value must be at least 18                                                                                              |
| `max(value)`        | `{age:max(120)}`                            | `91`                                   | Integer value must be no more than 120                                                                                         |
| `range(min,max)`    | `{age:range(18,120)}`                       | `91`                                   | Integer value must be at least 18 but no more than 120                                                                         |
| `alpha`             | `{name:alpha}`                              | `Rick`                                 | String must consist of one or more alphabetical characters, `a`-`z` and case-insensitive                                       |
| `regex(expression)` | `{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}` | `123-45-6789`                          | String must match the regular expression. See tips about defining a regular expression                                         |
| `required`          | `{name:required}`                           | `Rick`                                 | Enforces that an explicit value (not an ambient value) is present during URL generation.                                       |
| `file`              | `{filename:file}`                           | `myfile.txt`                           | String can contain path segments, but its last segment must have a dot (`.`) and be followed by one or more non-dot characters |
| `nonfile`           | `{page:nonfile}`                            | `PageName`                             | String must not have a dot in its last path segment that is followed by one or more non-dot (`.`) characters                   |

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

Multiple, colon delimited constraints can be applied to a single parameter. For example, the following constraint restricts a parameter to an integer value of 1 or greater:

```
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
```

Warning

Route constraints that verify the URL and are converted to a CLR type always use the invariant culture. For example, conversion to the CLR type `int` or `DateTime`. These constraints assume that the URL is not localizable. The framework-provided route constraints don't modify the values stored in route values. All route values parsed from the URL are stored as strings. For example, the `float` constraint attempts to convert the route value to a float, but the converted value is used only to verify it can be converted to a float.

### Regular expressions in constraints

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

Regular expressions can be specified as inline constraints using the `regex(...)` route constraint. Methods in the [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) family also accept an object literal of constraints. If that form is used, string values are interpreted as regular expressions.

The following code uses an inline regex constraint:

```
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");
```

The following code uses an object literal to specify a regex constraint:

```
app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });
```

The ASP.NET Core framework adds `RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant` to the regular expression constructor. See [RegexOptions](/en-us/dotnet/api/system.text.regularexpressions.regexoptions) for a description of these members.

Regular expressions use delimiters and tokens similar to those used by routing and the C# language. Regular expression tokens must be escaped. To use the regular expression `^\d{3}-\d{2}-\d{4}$` in an inline constraint, use one of the following:

* Replace `\` characters provided in the string as `\\` characters in the C# source file in order to escape the `\` string escape character.
* [Verbatim string literals](/en-us/dotnet/csharp/language-reference/keywords/string).

To escape routing parameter delimiter characters `{`, `}`, `[`, `]`, double the characters in the expression, for example, `{{`, `}}`, `[[`, `]]`. The following table shows a regular expression and its escaped version:

| Regular expression    | Escaped regular expression     |
| --------------------- | ------------------------------ |
| `^\d{3}-\d{2}-\d{4}$` | `^\\d{{3}}-\\d{{2}}-\\d{{4}}$` |
| `^[a-z]{2}$`          | `^[[a-z]]{{2}}$`               |

Regular expressions used in routing often start with the `^` character and match the starting position of the string. The expressions often end with the `$` character and match the end of the string. The `^` and `$` characters ensure that the regular expression matches the entire route parameter value. Without the `^` and `$` characters, the regular expression matches any substring within the string, which is often undesirable. The following table provides examples and explains why they match or fail to match.

| Expression   | String    | Match | Comment               |
| ------------ | --------- | ----- | --------------------- |
| `[a-z]{2}`   | hello     | Yes   | Substring matches     |
| `[a-z]{2}`   | 123abc456 | Yes   | Substring matches     |
| `[a-z]{2}`   | mz        | Yes   | Matches expression    |
| `[a-z]{2}`   | MZ        | Yes   | Not case sensitive    |
| `^[a-z]{2}$` | hello     | No    | See `^` and `$` above |
| `^[a-z]{2}$` | 123abc456 | No    | See `^` and `$` above |

For more information on regular expression syntax, see [.NET Framework Regular Expressions](/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference).

To constrain a parameter to a known set of possible values, use a regular expression. For example, `{action:regex(^(list|get|create)$)}` only matches the `action` route value to `list`, `get`, or `create`. If passed into the constraints dictionary, the string `^(list|get|create)$` is equivalent. Constraints that are passed in the constraints dictionary that don't match one of the known constraints are also treated as regular expressions. Constraints that are passed within a template that don't match one of the known constraints are not treated as regular expressions.

### Custom route constraints

Custom route constraints can be created by implementing the [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) interface. The `IRouteConstraint` interface contains [Match](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint.match), which returns `true` if the constraint is satisfied and `false` otherwise.

Custom route constraints are rarely needed. Before implementing a custom route constraint, consider alternatives, such as model binding.

The ASP.NET Core [Constraints](https://github.com/dotnet/aspnetcore/tree/main/src/Http/Routing/src/Constraints) folder provides good examples of creating constraints. For example, [GuidRouteConstraint](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs#L18).

To use a custom `IRouteConstraint`, the route constraint type must be registered with the app's [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap) in the service container. A `ConstraintMap` is a dictionary that maps route constraint keys to `IRouteConstraint` implementations that validate those constraints. An app's `ConstraintMap` can be updated in `Program.cs` either as part of an [AddRouting](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.routingservicecollectionextensions.addrouting) call or by configuring [RouteOptions](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions) directly with `builder.Services.Configure<RouteOptions>`. For example:

```
builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));
```

The preceding constraint is applied in the following code:

```
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}
```

The implementation of `NoZeroesRouteConstraint` prevents `0` being used in a route parameter:

```
public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[1-9]*$",
        RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
        TimeSpan.FromMilliseconds(100));

    public bool Match(
        HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}
```

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

The preceding code:

* Prevents `0` in the `{id}` segment of the route.
* Is shown to provide a basic example of implementing a custom constraint. It should not be used in a production app.

The following code is a better approach to preventing an `id` containing a `0` from being processed:

```
[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}
```

The preceding code has the following advantages over the `NoZeroesRouteConstraint` approach:

* It doesn't require a custom constraint.
* It returns a more descriptive error when the route parameter includes `0`.

## Parameter transformers

Parameter transformers:

* Execute when generating a link using [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator).
* Implement [Microsoft.AspNetCore.Routing.IOutboundParameterTransformer](/en-us/dotnet/api/microsoft.aspnetcore.routing.ioutboundparametertransformer).
* Are configured using [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap).
* Take the parameter's route value and transform it to a new string value.
* Result in using the transformed value in the generated link.

For example, a custom `slugify` parameter transformer in route pattern `blog\{article:slugify}` with `Url.Action(new { article = "MyTestArticle" })` generates `blog\my-test-article`.

Consider the following `IOutboundParameterTransformer` implementation:

```
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}
```

To use a parameter transformer in a route pattern, configure it using [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap) in `Program.cs`:

```
builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
```

The ASP.NET Core framework uses parameter transformers to transform the URI where an endpoint resolves. For example, parameter transformers transform the route values used to match an `area`, `controller`, `action`, and `page`:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
```

With the preceding route template, the action `SubscriptionManagementController.GetAll` is matched with the URI `/subscription-management/get-all`. A parameter transformer doesn't change the route values used to generate a link. For example, `Url.Action("GetAll", "SubscriptionManagement")` outputs `/subscription-management/get-all`.

ASP.NET Core provides API conventions for using parameter transformers with generated routes:

* The [Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.routetokentransformerconvention) MVC convention applies a specified parameter transformer to all attribute routes in the app. The parameter transformer transforms attribute route tokens as they are replaced. For more information, see [Use a parameter transformer to customize token replacement](../mvc/controllers/routing?view=aspnetcore-10.0#use-a-parameter-transformer-to-customize-token-replacement).
* Razor Pages uses the [PageRouteTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutetransformerconvention) API convention. This convention applies a specified parameter transformer to all automatically discovered Razor Pages. The parameter transformer transforms the folder and file name segments of Razor Pages routes. For more information, see [Use a parameter transformer to customize page routes](../razor-pages/razor-pages-conventions?view=aspnetcore-10.0#use-a-parameter-transformer-to-customize-page-routes).

[]()

## URL generation reference

This section contains a reference for the algorithm implemented by URL generation. In practice, most complex examples of URL generation use controllers or Razor Pages. See [routing in controllers](../mvc/controllers/routing?view=aspnetcore-10.0) for additional information.

The URL generation process begins with a call to [LinkGenerator.GetPathByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.getpathbyaddress) or a similar method. The method is provided with an address, a set of route values, and optionally information about the current request from `HttpContext`.

The first step is to use the address to resolve a set of candidate endpoints using an [IEndpointAddressScheme\<TAddress>](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointaddressscheme-1) that matches the address's type.

Once the set of candidates is found by the address scheme, the endpoints are ordered and processed iteratively until a URL generation operation succeeds. URL generation does **not** check for ambiguities, the first result returned is the final result.

### Troubleshooting URL generation with logging

The first step in troubleshooting URL generation is setting the logging level of `Microsoft.AspNetCore.Routing` to `TRACE`. `LinkGenerator` logs many details about its processing which can be useful to troubleshoot problems.

See [URL generation reference](#ugr) for details on URL generation.

### Addresses

Addresses are the concept in URL generation used to bind a call into the link generator to a set of candidate endpoints.

Addresses are an extensible concept that come with two implementations by default:

* Using *endpoint name* (`string`) as the address:

  * Provides similar functionality to MVC's route name.
  * Uses the [IEndpointNameMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointnamemetadata) metadata type.
  * Resolves the provided string against the metadata of all registered endpoints.
  * Throws an exception on startup if multiple endpoints use the same name.
  * Recommended for general-purpose use outside of controllers and Razor Pages.

* Using *route values* ([RouteValuesAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.routevaluesaddress)) as the address:

  * Provides similar functionality to controllers and Razor Pages legacy URL generation.
  * Very complex to extend and debug.
  * Provides the implementation used by `IUrlHelper`, Tag Helpers, HTML Helpers, Action Results, etc.

The role of the address scheme is to make the association between the address and matching endpoints by arbitrary criteria:

* The endpoint name scheme performs a basic dictionary lookup.
* The route values scheme has a complex best subset of set algorithm.

[]()

### Ambient values and explicit values

From the current request, routing accesses the route values of the current request `HttpContext.Request.RouteValues`. The values associated with the current request are referred to as the **ambient values**. For the purpose of clarity, the documentation refers to the route values passed in to methods as **explicit values**.

The following example shows ambient values and explicit values. It provides ambient values from the current request and explicit values:

```
public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...
```

The preceding code:

* Returns `/Widget/Index/17`
* Gets [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) via [DI](dependency-injection?view=aspnetcore-10.0).

The following code provides only explicit values and no ambient values:

```
var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;
```

The preceding method returns `/Home/Subscribe/17`

The following code in the `WidgetController` returns `/Widget/Subscribe/17`:

```
var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });
```

The following code provides the controller from ambient values in the current request and explicit values:

```
public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}
```

In the preceding code:

* `/Gadget/Edit/17` is returned.
* [Url](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.url#microsoft-aspnetcore-mvc-controllerbase-url) gets the [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper).
* [Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.action) generates a URL with an absolute path for an action method. The URL contains the specified `action` name and `route` values.

The following code provides ambient values from the current request and explicit values:

```
public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}
```

The preceding code sets `url` to `/Edit/17` when the Edit Razor Page contains the following page directive:

`@page "{id:int}"`

If the Edit page doesn't contain the `"{id:int}"` route template, `url` is `/Edit?id=17`.

The behavior of MVC's [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) adds a layer of complexity in addition to the rules described here:

* `IUrlHelper` always provides the route values from the current request as ambient values.
* [IUrlHelper.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.action) always copies the current `action` and `controller` route values as explicit values unless overridden by the developer.
* [IUrlHelper.Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.page) always copies the current `page` route value as an explicit value unless overridden.
* `IUrlHelper.Page` always overrides the current `handler` route value with `null` as an explicit values unless overridden.

Users are often surprised by the behavioral details of ambient values, because MVC doesn't seem to follow its own rules. For historical and compatibility reasons, certain route values such as `action`, `controller`, `page`, and `handler` have their own special-case behavior.

The equivalent functionality provided by `LinkGenerator.GetPathByAction` and `LinkGenerator.GetPathByPage` duplicates these anomalies of `IUrlHelper` for compatibility.

### URL generation process

Once the set of candidate endpoints are found, the URL generation algorithm:

* Processes the endpoints iteratively.
* Returns the first successful result.

The first step in this process is called **route value invalidation**. Route value invalidation is the process by which routing decides which route values from the ambient values should be used and which should be ignored. Each ambient value is considered and either combined with the explicit values, or ignored.

The best way to think about the role of ambient values is that they attempt to save application developers typing, in some common cases. Traditionally, the scenarios where ambient values are helpful are related to MVC:

* When linking to another action in the same controller, the controller name doesn't need to be specified.
* When linking to another controller in the same area, the area name doesn't need to be specified.
* When linking to the same action method, route values don't need to be specified.
* When linking to another part of the app, you don't want to carry over route values that have no meaning in that part of the app.

Calls to `LinkGenerator` or `IUrlHelper` that return `null` are usually caused by not understanding route value invalidation. Troubleshoot route value invalidation by explicitly specifying more of the route values to see if that solves the problem.

Route value invalidation works on the assumption that the app's URL scheme is hierarchical, with a hierarchy formed from left-to-right. Consider the basic controller route template `{controller}/{action}/{id?}` to get an intuitive sense of how this works in practice. A **change** to a value **invalidates** all of the route values that appear to the right. This reflects the assumption about hierarchy. If the app has an ambient value for `id`, and the operation specifies a different value for the `controller`:

* `id` won't be reused because `{controller}` is to the left of `{id?}`.

Some examples demonstrating this principle:

* If the explicit values contain a value for `id`, the ambient value for `id` is ignored. The ambient values for `controller` and `action` can be used.
* If the explicit values contain a value for `action`, any ambient value for `action` is ignored. The ambient values for `controller` can be used. If the explicit value for `action` is different from the ambient value for `action`, the `id` value won't be used. If the explicit value for `action` is the same as the ambient value for `action`, the `id` value can be used.
* If the explicit values contain a value for `controller`, any ambient value for `controller` is ignored. If the explicit value for `controller` is different from the ambient value for `controller`, the `action` and `id` values won't be used. If the explicit value for `controller` is the same as the ambient value for `controller`, the `action` and `id` values can be used.

This process is further complicated by the existence of attribute routes and dedicated conventional routes. Controller conventional routes such as `{controller}/{action}/{id?}` specify a hierarchy using route parameters. For [dedicated conventional routes](../mvc/controllers/routing?view=aspnetcore-10.0#dcr) and [attribute routes](../mvc/controllers/routing?view=aspnetcore-10.0#ar) to controllers and Razor Pages:

* There is a hierarchy of route values.
* They don't appear in the template.

For these cases, URL generation defines the **required values** concept. Endpoints created by controllers and Razor Pages have required values specified that allow route value invalidation to work.

The route value invalidation algorithm in detail:

* The required value names are combined with the route parameters, then processed from left-to-right.

* For each parameter, the ambient value and explicit value are compared:

  * If the ambient value and explicit value are the same, the process continues.
  * If the ambient value is present and the explicit value isn't, the ambient value is used when generating the URL.
  * If the ambient value isn't present and the explicit value is, reject the ambient value and all subsequent ambient values.
  * If the ambient value and the explicit value are present, and the two values are different, reject the ambient value and all subsequent ambient values.

At this point, the URL generation operation is ready to evaluate route constraints. The set of accepted values is combined with the parameter default values, which is provided to constraints. If the constraints all pass, the operation continues.

Next, the **accepted values** can be used to expand the route template. The route template is processed:

* From left-to-right.

* Each parameter has its accepted value substituted.

* With the following special cases:

  * If the accepted values is missing a value and the parameter has a default value, the default value is used.
  * If the accepted values is missing a value and the parameter is optional, processing continues.
  * If any route parameter to the right of a missing optional parameter has a value, the operation fails.
  * Contiguous default-valued parameters and optional parameters are collapsed where possible.

Note

For endpoints created by controllers and Razor Pages, `controller`, `action`, `page`, and `area` are part of the endpoint's **required values**, so they're always present in the accepted values. As a result:

* They aren't invalidated by a change to a route value that appears to their left, such as a `culture` parameter.
* Inline default values in the route template, such as `{controller=DefaultController}`, aren't applied, because default values are used only when the accepted values are *missing* a value.

For example, with the template `{culture}/api1/{controller=DefaultController}/{action=DefaultAction}/{id?}` and a current request of `/en/api1/MyController/Index/111`, the call `Url.RouteUrl("ApiRoute1", new { culture = "ar" })` generates `/ar/api1/MyController/Index`, not `/ar/api1/DefaultController/DefaultAction`. The `id` value isn't used because it has no required value and is to the right of a changed value. To use the template's default values, specify them explicitly:

```
Url.RouteUrl("ApiRoute1", new { culture = "ar", controller = "DefaultController", action = "DefaultAction" })
```

For a related limitation, see [Problems with route value invalidation](#problems-with-route-value-invalidation).

Values explicitly provided that don't match a segment of the route are added to the query string. The following table shows the result when using the route template `{controller}/{action}/{id?}`.

| Ambient Values                     | Explicit Values                        | Result                  |
| ---------------------------------- | -------------------------------------- | ----------------------- |
| controller = "Home"                | action = "About"                       | `/Home/About`           |
| controller = "Home"                | controller = "Order", action = "About" | `/Order/About`          |
| controller = "Home", color = "Red" | action = "About"                       | `/Home/About`           |
| controller = "Home"                | action = "About", color = "Red"        | `/Home/About?color=Red` |

### Optional route parameter order

Optional route parameters must come after all required route parameters and literals. In the following code, the `id` and `name` parameters must come after the `color` parameter:

```
using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers;

[Route("api/[controller]")]
public class MyController : ControllerBase
{
    // GET /api/my/red/2/joe
    // GET /api/my/red/2
    // GET /api/my
    [HttpGet("{color}/{id:int?}/{name?}")]
    public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
    {
        return Ok($"{color} {id} {name ?? ""}");
    }
}
```

### Problems with route value invalidation

For related information about how required values, such as `controller` and `action`, affect route value invalidation and template default values, see the note in [URL generation process](#url-generation-process).

The following code shows an example of a URL generation scheme that's not supported by routing:

```
app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });
```

In the preceding code, the `culture` route parameter is used for localization. The desire is to have the `culture` parameter always accepted as an ambient value. However, the `culture` parameter is not accepted as an ambient value because of the way required values work:

* In the `"default"` route template, the `culture` route parameter is to the left of `controller`, so changes to `controller` won't invalidate `culture`.
* In the `"blog"` route template, the `culture` route parameter is considered to be to the right of `controller`, which appears in the required values.

## Parse URL paths with `LinkParser`

The [LinkParser](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkparser) class adds support for parsing a URL path into a set of route values. The [ParsePathByEndpointName](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkparserendpointnameaddressextensions.parsepathbyendpointname) method takes an endpoint name and a URL path, and returns a set of route values extracted from the URL path.

In the following example controller, the `GetProduct` action uses a route template of `api/Products/{id}` and has a [Name](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute.name) of `GetProduct`:

```
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...
```

In the same controller class, the `AddRelatedProduct` action expects a URL path, `pathToRelatedProduct`, which can be provided as a query-string parameter:

```
[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...
```

In the preceding example, the `AddRelatedProduct` action extracts the `id` route value from the URL path. For example, with a URL path of `/api/Products/1`, the `relatedProductId` value is set to `1`. This approach allows the API's clients to use URL paths when referring to resources, without requiring knowledge of how such a URL is structured.

## Configure endpoint metadata

The following links provide information on how to configure endpoint metadata:

* [Enable Cors with endpoint routing](../security/cors?view=aspnetcore-10.0#enable-cors-with-endpoint-routing)
* [IAuthorizationPolicyProvider sample](https://github.com/dotnet/AspNetCore/tree/release/3.1/src/Security/samples/CustomPolicyProvider) using a custom `[MinimumAgeAuthorize]` attribute
* [Test authentication with the \[Authorize\] attribute](../security/authentication/identity?view=aspnetcore-10.0#test-identity)
* [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization)
* [Select a scheme with an `[Authorize]` attribute](../security/authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0#select-a-scheme-with-an-authorize-attribute)
* [Apply policies using an `[Authorize]` attribute](../security/authorization/policies?view=aspnetcore-10.0#apply-policies-to-mvc-controllers)
* [Role-based authorization in ASP.NET Core](../security/authorization/roles?view=aspnetcore-10.0)

[]()

## Host matching in routes with RequireHost

[RequireHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.requirehost) applies a constraint to the route which requires the specified host. The `RequireHost` or [\[Host\]](/en-us/dotnet/api/microsoft.aspnetcore.routing.hostattribute) parameter can be a:

* Host: `www.domain.com`, matches `www.domain.com` with any port.
* Host with wildcard: `*.domain.com`, matches `www.domain.com`, `subdomain.domain.com`, or `www.subdomain.domain.com` on any port.
* Port: `*:5000`, matches port 5000 with any host.
* Host and port: `www.domain.com:5000` or `*.domain.com:5000`, matches host and port.

Multiple parameters can be specified using `RequireHost` or `[Host]`. The constraint matches hosts valid for any of the parameters. For example, `[Host("domain.com", "*.domain.com")]` matches `domain.com`, `www.domain.com`, and `subdomain.domain.com`.

The following code uses `RequireHost` to require the specified host on the route:

```
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");
```

The following code uses the `[Host]` attribute on the controller to require any of the specified hosts:

```
[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}
```

When the `[Host]` attribute is applied to both the controller and action method:

* The attribute on the action is used.
* The controller attribute is ignored.

Warning

API that relies on the [Host header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Host), such as [HttpRequest.Host](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.host) and [RequireHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.requirehost), are subject to potential spoofing by clients.

To prevent host and port spoofing, use one of the following approaches:

* Use [HttpContext.Connection](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.connection) ([ConnectionInfo.LocalPort](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.localport#microsoft-aspnetcore-http-connectioninfo-localport)) where the ports are checked.
* Employ [Host filtering](servers/kestrel/host-filtering?view=aspnetcore-10.0).

## Route groups

The [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) extension method helps organize groups of endpoints with a common prefix and reduces repetitive code. Use this method to customize entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata) that add [endpoint metadata](routing?view=aspnetcore-10.0#endpoint-metadata).

For example, the following code creates two similar groups of endpoints:

```
app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
```

```
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}
```

In this scenario, you can use a relative address for the `Location` header in the `201 Created` result:

```
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}
```

The first group of endpoints matches only requests prefixed with `/public/todos` and are accessible without any authentication. The second group of endpoints matches only requests prefixed with `/private/todos` and require authentication.

`QueryPrivateTodos` is a local function that modifies the `TodoDb` parameters of the route handler, to enable them to access and store private todo data. `QueryPrivateTodos` serves as an [endpoint filter factory](minimal-apis/min-api-filters?view=aspnetcore-10.0).

Route groups also support nested groups and complex prefix patterns with route parameters and constraints. In the following example, the route handler mapped to the `user` group can capture the `{org}` and `{group}` route parameters defined in the outer group prefixes.

The prefix can also be empty. This approach can be useful for adding endpoint metadata or filters to a group of endpoints without changing the route pattern.

```
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
```

Adding filters or metadata to a group results in the same behavior as adding them individually to each endpoint (before adding extra filters or metadata that might exist in an inner group or specific endpoint).

```
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});
```

In the preceding example, the outer filter logs the incoming request before the inner filter even though the outer filter is added second. Because the filters are applied to different groups, the order that they're added relative to each other doesn't matter. The order in which filters are added matters when applied to the same group or specific endpoint.

A request to `/outer/inner/` logs the following data:

```
/outer group filter
/inner group filter
MapGet filter
```

## Performance guidance for routing

When an app has performance problems, routing is often suspected as the problem. The reason routing is suspected is that frameworks like controllers and Razor Pages report the amount of time spent inside the framework in their logging messages. When there's a significant difference between the time reported by controllers and the total time of the request:

* Developers eliminate their app code as the source of the problem.
* It's common to assume routing is the cause.

Routing is performance tested using thousands of endpoints. It's unlikely that a typical app will encounter a performance problem just by being too large. The most common root cause of slow routing performance is usually a badly-behaving custom middleware.

This following code sample demonstrates a basic technique for narrowing down the source of delay:

```
var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");
```

To time routing:

* Interleave each middleware with a copy of the timing middleware shown in the preceding code.
* Add a unique identifier to correlate the timing data with the code.

This is a basic way to narrow down the delay when it's significant, for example, more than `10ms`. Subtracting `Time 2` from `Time 1` reports the time spent inside the `UseRouting` middleware.

The following code uses a more compact approach to the preceding timing code:

```
public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
```

```
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseRouting();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.MapGet("/", () => "Timing Test.");
```

### Potentially expensive routing features

The following list provides some insight into routing features that are relatively expensive compared with basic route templates:

* Regular expressions: It's possible to write regular expressions that are complex, or have long running time with a small amount of input.

* Complex segments (`{x}-{y}-{z}`):

  * Are significantly more expensive than parsing a regular URL path segment.
  * Result in many more substrings being allocated.

* Synchronous data access: Many complex apps have database access as part of their routing. Use extensibility points such as [MatcherPolicy](/en-us/dotnet/api/microsoft.aspnetcore.routing.matcherpolicy) and [EndpointSelectorContext](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointselectorcontext), which are asynchronous.

### Guidance for large route tables

By default ASP.NET Core uses a routing algorithm that trades memory for CPU time. This has the nice effect that route matching time is dependent only on the length of the path to match and not the number of routes. However, this approach can be potentially problematic in some cases, when the app has a large number of routes (in the thousands) and there is a high amount of variable prefixes in the routes. For example, if the routes have parameters in early segments of the route, like `{parameter}/some/literal`.

It is unlikely for an app to run into a situation where this is a problem unless:

* There are a high number of routes in the app using this pattern.
* There is a large number of routes in the app.

#### How to determine if an app is running into the large route table problem

* There are two symptoms to look for:

  * The app is slow to start on the first request.
    * Note that this is required but not sufficient. There are many other non-route problems than can cause slow app startup. Check for the condition below to accurately determine the app is running into this situation.
  * The app consumes a lot of memory during startup and a memory dump shows a large number of `Microsoft.AspNetCore.Routing.Matching.DfaNode` instances.

#### How to address this issue

There are several techniques and optimizations that can be applied to routes that largely improve this scenario:

* Apply route constraints to your parameters, for example `{parameter:int}`, `{parameter:guid}`, `{parameter:regex(\\d+)}`, etc. where possible.

  * This allows the routing algorithm to internally optimize the structures used for matching and drastically reduce the memory used.
  * In the vast majority of cases this will suffice to get back to an acceptable behavior.

* Change the routes to move parameters to later segments in the template.
  * This reduces the number of possible "paths" to match an endpoint given a path.

* Use a dynamic route and perform the mapping to a controller/page dynamically.
  * This can be achieved using `MapDynamicControllerRoute` and `MapDynamicPageRoute`.

## Short-circuit middleware after routing

When routing matches an endpoint, it typically lets the rest of the middleware pipeline run before invoking the endpoint logic. Services can reduce resource usage by filtering out known requests early in the pipeline. Use the [ShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.builder.routeshortcircuitendpointconventionbuilderextensions.shortcircuit) extension method to cause routing to invoke the endpoint logic immediately and then end the request. For example, a given route might not need to go through authentication or CORS middleware. The following example short-circuits requests that match the `/short-circuit` route:

```
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
```

The [ShortCircuit(IEndpointConventionBuilder, Nullable\<Int32>)](/en-us/dotnet/api/microsoft.aspnetcore.builder.routeshortcircuitendpointconventionbuilderextensions.shortcircuit#microsoft-aspnetcore-builder-routeshortcircuitendpointconventionbuilderextensions-shortcircuit\(microsoft-aspnetcore-builder-iendpointconventionbuilder-system-nullable\(\(system-int32\)\)\)) method can optionally take a status code.

Use the [MapShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeshortcircuitendpointroutebuilderextensions.mapshortcircuit) method to set up short-circuiting for multiple routes at once, by passing to it a params array of URL prefixes. For example, browsers and bots often probe servers for well known paths like `robots.txt` and `favicon.ico`. If the app doesn't have those files, one line of code can configure both routes:

```
app.MapShortCircuit(404, "robots.txt", "favicon.ico");
```

`MapShortCircuit` returns [IEndpointConventionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iendpointconventionbuilder) so that additional route constraints like host filtering can be added to it.

Short-circuiting doesn't affect middleware placed before `UseRouting`. Short-circuiting an endpoint that also has `[Authorize]` or `[RequireCors]` metadata causes requests to fail with an `InvalidOperationException`. This metadata is applied by [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) or [`[EnableCors]`](/en-us/dotnet/api/microsoft.aspnetcore.cors.enablecorsattribute) attributes or by [RequireCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) or [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) methods. This restriction applies however the endpoint is short-circuited.

To see the effect of short-circuiting middleware, set the "Microsoft" logging category to "Information" in `appsettings.Development.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
```

Run the following code:

```
var app = WebApplication.Create();

app.UseHttpLogging();

app.MapGet("/", () => "No short-circuiting!");
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
app.MapShortCircuit(404, "robots.txt", "favicon.ico");

app.Run();
```

The following example is from the console logs produced by running the `/` endpoint. It includes output from the logging middleware:

```
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: text/plain; charset=utf-8
      Date: Wed, 03 May 2023 21:05:59 GMT
      Server: Kestrel
      Alt-Svc: h3=":5182"; ma=86400
      Transfer-Encoding: chunked
```

The following example is from running the `/short-circuit` endpoint. It doesn't have anything from the logging middleware because the middleware was short-circuited:

```
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[4]
      The endpoint 'HTTP: GET /short-circuit' is being executed without running additional middleware.
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[5]
      The endpoint 'HTTP: GET /short-circuit' has been executed without running additional middleware.
```

Instead of calling [ShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.builder.routeshortcircuitendpointconventionbuilderextensions.shortcircuit) on the endpoint builder, you can apply the [ShortCircuitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.routing.shortcircuitattribute) to short-circuit an endpoint declaratively. The attribute is in the [Microsoft.AspNetCore.Routing](/en-us/dotnet/api/microsoft.aspnetcore.routing) namespace and has the same effect as the extension method: routing invokes the endpoint in [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and ends the request without running the remaining middleware.

The attribute is valid on a method or a class. It isn't inherited, and it can't be applied more than once to the same target.

On a Minimal API route handler:

```
app.MapGet("/health-probe", [ShortCircuit] () => Results.Ok());
```

On an MVC action or an entire controller:

```
[ApiController]
[Route("robots.txt")]
[ShortCircuit]
public class RobotsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Content("User-agent: *\nDisallow:", "text/plain");
}
```

You can optionally specify a status code:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    [HttpGet]
    [ShortCircuit(StatusCodes.Status200OK)]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}
```

When a status code isn't supplied, [ShortCircuitAttribute.StatusCode](/en-us/dotnet/api/microsoft.aspnetcore.routing.shortcircuitattribute.statuscode#microsoft-aspnetcore-routing-shortcircuitattribute-statuscode) is `null` and the endpoint runs normally before the request ends. The attribute implements [IShortCircuitMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.ishortcircuitmetadata), so custom endpoint conventions and metadata inspection can detect short-circuited endpoints through that interface.

Warning

The same restriction that applies to the [ShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.builder.routeshortcircuitendpointconventionbuilderextensions.shortcircuit) and [MapShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeshortcircuitendpointroutebuilderextensions.mapshortcircuit) methods applies to the attribute. An endpoint that's short-circuited and also has `[Authorize]` or `[RequireCors]` metadata fails with an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception). Because `[ShortCircuit]` can be applied to a class, take particular care when placing it on an MVC controller that carries an `[Authorize]` attribute, including one applied through a base class.

## Guidance for library authors

This section contains guidance for library authors building on top of routing. These details are intended to ensure that app developers have a good experience using libraries and frameworks that extend routing.

### Define endpoints

To create a framework that uses routing for URL matching, start by defining a user experience that builds on top of [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).

**DO** build on top of [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder). This allows users to compose your framework with other ASP.NET Core features without confusion. Every ASP.NET Core template includes routing. Assume routing is present and familiar for users.

```
// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");
```

**DO** return a sealed concrete type from a call to `MapMyFramework(...)` that implements [IEndpointConventionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iendpointconventionbuilder). Most framework `Map...` methods follow this pattern. The `IEndpointConventionBuilder` interface:

* Allows for metadata to be composed.
* Is targeted by a variety of extension methods.

Declaring your own type allows you to add your own framework-specific functionality to the builder. It's ok to wrap a framework-declared builder and forward calls to it.

```
// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");
```

**CONSIDER** writing your own [EndpointDataSource](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource). `EndpointDataSource` is the low-level primitive for declaring and updating a collection of endpoints. `EndpointDataSource` is a powerful API used by controllers and Razor Pages. For more information, see [Dynamic endpoint routing](https://www.mariusgundersen.net/dynamic-endpoint-routing/).

The routing tests have a [basic example](https://github.com/dotnet/AspNetCore/blob/main/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs#L17) of a non-updating data source.

**CONSIDER** implementing [GetGroupedEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource.getgroupedendpoints). This gives complete control over running group conventions and the final metadata on the grouped endpoints. For example, this allows custom `EndpointDataSource` implementations to run [endpoint filters](minimal-apis/min-api-filters?view=aspnetcore-10.0) added to groups.

**DO NOT** attempt to register an `EndpointDataSource` by default. Require users to register your framework in [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints). The philosophy of routing is that nothing is included by default, and that `UseEndpoints` is the place to register endpoints.

### Creating routing-integrated middleware

**CONSIDER** defining metadata types as an interface.

**DO** make it possible to use metadata types as an attribute on classes and methods.

```
public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}
```

Frameworks like controllers and Razor Pages support applying metadata attributes to types and methods. If you declare metadata types:

* Make them accessible as [attributes](/en-us/dotnet/csharp/programming-guide/concepts/attributes/).
* Most users are familiar with applying attributes.

Declaring a metadata type as an interface adds another layer of flexibility:

* Interfaces are composable.
* Developers can declare their own types that combine multiple policies.

**DO** make it possible to override metadata, as shown in the following example:

```
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}
```

The best way to follow these guidelines is to avoid defining **marker metadata**:

* Don't just look for the presence of a metadata type.
* Define a property on the metadata and check the property.

The metadata collection is ordered and supports overriding by priority. In the case of controllers, metadata on the action method is most specific.

**DO** make middleware useful with and without routing:

```
app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();
```

As an example of this guideline, consider the authorization middleware. A configured fallback policy applies when the middleware can't produce a policy from authorization metadata, including:

* Endpoints without authorization metadata that produces a policy.
* Requests that don't match an endpoint.

This behavior makes the authorization middleware useful outside of routing and for traditional middleware programming. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

## Debug diagnostics

For detailed routing diagnostic output, set `Logging:LogLevel:Microsoft` to `Debug`. In the `Development` environment, set the log level in `appsettings.Development.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
```

Routing is responsible for matching incoming HTTP requests and dispatching those requests to the app's executable endpoints. [Endpoints](#endpoints) are the app's units of executable request-handling code. Endpoints are defined in the app and configured when the app starts. The endpoint matching process can extract values from the request's URL and provide those values for request processing. Using endpoint information from the app, routing is also able to generate URLs that map to endpoints.

Apps can configure routing using:

* Controllers
* Razor Pages
* SignalR
* gRPC Services
* Endpoint-enabled [middleware](middleware/?view=aspnetcore-10.0) such as [Health Checks](../host-and-deploy/health-checks?view=aspnetcore-10.0).
* Delegates and lambdas registered with routing.

This article covers low-level details of ASP.NET Core routing. For information on configuring routing:

* For controllers, see [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0).
* For Razor Pages conventions, see [Razor Pages route and app conventions in ASP.NET Core](../razor-pages/razor-pages-conventions?view=aspnetcore-10.0).

## Routing basics

The following code shows a basic example of routing:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding example includes a single endpoint using the [MapGet](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapget) method:

* When an HTTP `GET` request is sent to the root URL `/`:

  * The request delegate executes.
  * `Hello World!` is written to the HTTP response.

* If the request method is not `GET` or the root URL is not `/`, no route matches and an HTTP 404 is returned.

Routing uses a pair of middleware, registered by [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints):

* `UseRouting` adds route matching to the middleware pipeline. This middleware looks at the set of endpoints defined in the app, and selects the [best match](#urlm) based on the request.
* `UseEndpoints` adds endpoint execution to the middleware pipeline. It runs the delegate associated with the selected endpoint.

Apps typically don't need to call `UseRouting` or `UseEndpoints`. [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) configures a middleware pipeline that wraps middleware added in `Program.cs` with `UseRouting` and `UseEndpoints`. However, apps can change the order in which `UseRouting` and `UseEndpoints` run by calling these methods explicitly. For example, the following code makes an explicit call to `UseRouting`:

```
app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");
```

In the preceding code:

* The call to `app.Use` registers a custom middleware that runs at the start of the pipeline.
* The call to `UseRouting` configures the route matching middleware to run *after* the custom middleware.
* The endpoint registered with `MapGet` runs at the end of the pipeline.

If the preceding example didn't include a call to `UseRouting`, the custom middleware would run *after* the route matching middleware.

### Endpoints

[]()

The `MapGet` method is used to define an **endpoint**. An endpoint is something that can be:

* Selected, by matching the URL and HTTP method.
* Executed, by running the delegate.

Endpoints that can be matched and executed by the app are configured in `UseEndpoints`. For example, [MapGet](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapget), [MapPost](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mappost), and [similar methods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions) connect request delegates to the routing system. Additional methods can be used to connect ASP.NET Core framework features to the routing system:

* [MapRazorPages for Razor Pages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)
* [MapControllers for controllers](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollers)
* [MapHub\<THub> for SignalR](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubroutebuilder.maphub)
* [MapGrpcService\<TService> for gRPC](../grpc/aspnetcore?view=aspnetcore-10.0)

The following example shows routing with a more sophisticated route template:

```
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");
```

The string `/hello/{name:alpha}` is a **route template**. A route template is used to configure how the endpoint is matched. In this case, the template matches:

* A URL like `/hello/Docs`
* Any URL path that begins with `/hello/` followed by a sequence of alphabetic characters. `:alpha` applies a route constraint that matches only alphabetic characters. [Route constraints](#route-constraints) are explained later in this article.

The second segment of the URL path, `{name:alpha}`:

* Is bound to the `name` parameter.
* Is captured and stored in [HttpRequest.RouteValues](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.routevalues).

The following example shows routing with [health checks](../host-and-deploy/health-checks?view=aspnetcore-10.0) and authorization:

```
app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
```

The preceding example demonstrates how:

* The authorization middleware can be used with routing.
* Endpoints can be used to configure authorization behavior.

The [MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks) call adds a health check endpoint. Chaining [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) on to this call attaches an authorization policy to the endpoint.

Calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) adds the authentication and authorization middleware. These middleware are placed between [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and `UseEndpoints` so that they can:

* See which endpoint was selected by `UseRouting`.
* Apply an authorization policy before [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) dispatches to the endpoint.

[]()

### Endpoint metadata

In the preceding example, there are two endpoints, but only the health check endpoint has an authorization policy attached. If the request matches the health check endpoint, `/healthz`, an authorization check is performed. This demonstrates that endpoints can have extra data attached to them. This extra data is called endpoint **metadata**:

* The metadata can be processed by routing-aware middleware.
* The metadata can be of any .NET type.

## Routing concepts

The routing system builds on top of the middleware pipeline by adding the powerful **endpoint** concept. Endpoints represent units of the app's functionality that are distinct from each other in terms of routing, authorization, and any number of ASP.NET Core's systems.

[]()

### ASP.NET Core endpoint definition

An ASP.NET Core endpoint is:

* Executable: Has a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.requestdelegate#microsoft-aspnetcore-http-endpoint-requestdelegate).
* Extensible: Has a [Metadata](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.metadata) collection.
* Selectable: Optionally, has [routing information](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.routepattern).
* Enumerable: The collection of endpoints can be listed by retrieving the [EndpointDataSource](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource) from [DI](dependency-injection?view=aspnetcore-10.0).

The following code shows how to retrieve and inspect the endpoint matching the current request:

```
app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");
```

The endpoint, if selected, can be retrieved from the `HttpContext`. Its properties can be inspected. Endpoint objects are immutable and cannot be modified after creation. The most common type of endpoint is a [RouteEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint). `RouteEndpoint` includes information that allows it to be selected by the routing system.

In the preceding code, [app.Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) configures an inline [middleware](middleware/?view=aspnetcore-10.0).

[]()

The following code shows that, depending on where `app.Use` is called in the pipeline, there may not be an endpoint:

```
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});
```

The preceding sample adds `Console.WriteLine` statements that display whether or not an endpoint has been selected. For clarity, the sample assigns a display name to the provided `/` endpoint.

The preceding sample also includes calls to `UseRouting` and `UseEndpoints` to control exactly when these middleware run within the pipeline.

Running this code with a URL of `/` displays:

```
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
```

Running this code with any other URL displays:

```
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
```

This output demonstrates that:

* The endpoint is always null before `UseRouting` is called.
* If a match is found, the endpoint is non-null between `UseRouting` and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).
* The `UseEndpoints` middleware is **terminal** when a match is found. [Terminal middleware](#tm) is defined later in this article.
* The middleware after `UseEndpoints` execute only when no match is found.

The `UseRouting` middleware uses the [SetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.setendpoint) method to attach the endpoint to the current context. It's possible to replace the `UseRouting` middleware with custom logic and still get the benefits of using endpoints. Endpoints are a low-level primitive like middleware, and aren't coupled to the routing implementation. Most apps don't need to replace `UseRouting` with custom logic.

The `UseEndpoints` middleware is designed to be used in tandem with the `UseRouting` middleware. The core logic to execute an endpoint isn't complicated. Use [GetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.getendpoint) to retrieve the endpoint, and then invoke its [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.requestdelegate#microsoft-aspnetcore-http-endpoint-requestdelegate) property.

The following code demonstrates how middleware can influence or react to routing:

```
app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
```

```
public class RequiresAuditAttribute : Attribute { }
```

The preceding example demonstrates two important concepts:

* Middleware can run before `UseRouting` to modify the data that routing operates upon.
  * Usually middleware that appears before routing modifies some property of the request, such as [UseRewriter](/en-us/dotnet/api/microsoft.aspnetcore.builder.rewritebuilderextensions.userewriter), [UseHttpMethodOverride](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpmethodoverrideextensions.usehttpmethodoverride), or [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase).

* Middleware can run between `UseRouting` and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) to process the results of routing before the endpoint is executed.

  * Middleware that runs between `UseRouting` and `UseEndpoints`:

    * Usually inspects metadata to understand the endpoints.
    * Often makes security decisions, as done by `UseAuthorization` and `UseCors`.

  * The combination of middleware and metadata allows configuring policies per-endpoint.

The preceding code shows an example of a custom middleware that supports per-endpoint policies. The middleware writes an *audit log* of access to sensitive data to the console. The middleware can be configured to *audit* an endpoint with the `RequiresAuditAttribute` metadata. This sample demonstrates an *opt-in* pattern where only endpoints that are marked as sensitive are audited. It's possible to define this logic in reverse, auditing everything that isn't marked as safe, for example. The endpoint metadata system is flexible. This logic could be designed in whatever way suits the use case.

The preceding sample code is intended to demonstrate the basic concepts of endpoints. **The sample is not intended for production use**. A more complete version of an *audit log* middleware would:

* Log to a file or database.
* Include details such as the user, IP address, name of the sensitive endpoint, and more.

The audit policy metadata `RequiresAuditAttribute` is defined as an `Attribute` for easier use with class-based frameworks such as controllers and SignalR. When using *route to code*:

* Metadata is attached with a builder API.
* Class-based frameworks include all attributes on the corresponding method and class when creating endpoints.

The best practices for metadata types are to define them either as interfaces or attributes. Interfaces and attributes allow code reuse. The metadata system is flexible and doesn't impose any limitations.

[]()

### Compare terminal middleware with routing

The following example demonstrates both terminal middleware and routing:

```
// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");
```

The style of middleware shown with `Approach 1:` is **terminal middleware**. It's called terminal middleware because it does a matching operation:

* The matching operation in the preceding sample is `Path == "/"` for the middleware and `Path == "/Routing"` for routing.
* When a match is successful, it executes some functionality and returns, rather than invoking the `next` middleware.

It's called terminal middleware because it terminates the search, executes some functionality, and then returns.

The following list compares terminal middleware with routing:

* Both approaches allow terminating the processing pipeline:

  * Middleware terminates the pipeline by returning rather than invoking `next`.
  * Endpoints are always terminal.

* Terminal middleware allows positioning the middleware at an arbitrary place in the pipeline:
  * Endpoints execute at the position of [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).

* Terminal middleware allows arbitrary code to determine when the middleware matches:

  * Custom route matching code can be verbose and difficult to write correctly.
  * Routing provides straightforward solutions for typical apps. Most apps don't require custom route matching code.

* Endpoints interface with middleware such as `UseAuthorization` and `UseCors`.
  * Using a terminal middleware with `UseAuthorization` or `UseCors` requires manual interfacing with the authorization system.

An [endpoint](#endpoints) defines both:

* A delegate to process requests.
* A collection of arbitrary metadata. The metadata is used to implement cross-cutting concerns based on policies and configuration attached to each endpoint.

Terminal middleware can be an effective tool, but can require:

* A significant amount of coding and testing.
* Manual integration with other systems to achieve the desired level of flexibility.

Consider integrating with routing before writing a terminal middleware.

Existing terminal middleware that integrates with [Map](middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline) or [MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen) can usually be turned into a routing aware endpoint. [MapHealthChecks](https://github.com/dotnet/AspNetCore/blob/main/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs#L16) demonstrates the pattern for router-ware:

* Write an extension method on [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder).
* Create a nested middleware pipeline using [CreateApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder.createapplicationbuilder).
* Attach the middleware to the new pipeline. In this case, [UseHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckapplicationbuilderextensions.usehealthchecks).
* [Build](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder.build) the middleware pipeline into a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate).
* Call `Map` and provide the new middleware pipeline.
* Return the builder object provided by `Map` from the extension method.

The following code shows use of [MapHealthChecks](../host-and-deploy/health-checks?view=aspnetcore-10.0):

```
app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
```

The preceding sample shows why returning the builder object is important. Returning the builder object allows the app developer to configure policies such as authorization for the endpoint. In this example, the health checks middleware has no direct integration with the authorization system.

The metadata system was created in response to the problems encountered by extensibility authors using terminal middleware. It's problematic for each middleware to implement its own integration with the authorization system.

[]()

### URL matching

* Is the process by which routing matches an incoming request to an [endpoint](#endpoints).
* Is based on data in the URL path and headers.
* Can be extended to consider any data in the request.

When a routing middleware executes, it sets an `Endpoint` and route values to a [request feature](request-features?view=aspnetcore-10.0) on the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) from the current request:

* Calling [HttpContext.GetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.getendpoint) gets the endpoint.
* `HttpRequest.RouteValues` gets the collection of route values.

[Middleware](middleware/?view=aspnetcore-10.0) runs after the routing middleware can inspect the endpoint and take action. For example, an authorization middleware can interrogate the endpoint's metadata collection for an authorization policy. After all of the middleware in the request processing pipeline is executed, the selected endpoint's delegate is invoked.

The routing system in endpoint routing is responsible for all dispatching decisions. Because the middleware applies policies based on the selected endpoint, it's important that:

* Any decision that can affect dispatching or the application of security policies is made inside the routing system.

Warning

For backward-compatibility, when a Controller or Razor Pages endpoint delegate is executed, the properties of [RouteContext.RouteData](/en-us/dotnet/api/microsoft.aspnetcore.routing.routecontext.routedata) are set to appropriate values based on the request processing performed thus far.

The `RouteContext` type will be marked obsolete in a future release:

* Migrate `RouteData.Values` to `HttpRequest.RouteValues`.
* Migrate `RouteData.DataTokens` to retrieve [IDataTokensMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.idatatokensmetadata) from the endpoint metadata.

URL matching operates in a configurable set of phases. In each phase, the output is a set of matches. The set of matches can be narrowed down further by the next phase. The routing implementation does not guarantee a processing order for matching endpoints. **All** possible matches are processed at once. The URL matching phases occur in the following order. ASP.NET Core:

1. Processes the URL path against the set of endpoints and their route templates, collecting **all** of the matches.
2. Takes the preceding list and removes matches that fail with route constraints applied.
3. Takes the preceding list and removes matches that fail the set of [MatcherPolicy](/en-us/dotnet/api/microsoft.aspnetcore.routing.matcherpolicy) instances.
4. Uses the [EndpointSelector](/en-us/dotnet/api/microsoft.aspnetcore.routing.matching.endpointselector) to make a final decision from the preceding list.

The list of endpoints is prioritized according to:

* The [RouteEndpoint.Order](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.order)
* The [route template precedence](#rtp)

All matching endpoints are processed in each phase until the [EndpointSelector](/en-us/dotnet/api/microsoft.aspnetcore.routing.matching.endpointselector) is reached. The `EndpointSelector` is the final phase. It chooses the highest priority endpoint from the matches as the best match. If there are other matches with the same priority as the best match, an ambiguous match exception is thrown.

The route precedence is computed based on a **more specific** route template being given a higher priority. For example, consider the templates `/hello` and `/{message}`:

* Both match the URL path `/hello`.
* `/hello` is more specific and therefore higher priority.

In general, route precedence does a good job of choosing the best match for the kinds of URL schemes used in practice. Use [Order](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.order#microsoft-aspnetcore-routing-routeendpoint-order) only when necessary to avoid an ambiguity.

Due to the kinds of extensibility provided by routing, it isn't possible for the routing system to compute ahead of time the ambiguous routes. Consider an example such as the route templates `/{message:alpha}` and `/{message:int}`:

* The `alpha` constraint matches only alphabetic characters.
* The `int` constraint matches only numbers.
* These templates have the same route precedence, but there's no single URL they both match.
* If the routing system reported an ambiguity error at startup, it would block this valid use case.

Warning

The order of operations inside [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) doesn't influence the behavior of routing, with one exception. [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) and [MapAreaRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcarearoutebuilderextensions.maparearoute) automatically assign an order value to their endpoints based on the order they are invoked. This simulates long-time behavior of controllers without the routing system providing the same guarantees as older routing implementations.

Endpoint routing in ASP.NET Core:

* Doesn't have the concept of routes.
* Doesn't provide ordering guarantees. All endpoints are processed at once.

[]()

### Route template precedence and endpoint selection order

[Route template precedence](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Template/RoutePrecedence.cs#L16) is a system that assigns each route template a value based on how specific it is. Route template precedence:

* Avoids the need to adjust the order of endpoints in common cases.
* Attempts to match the common-sense expectations of routing behavior.

For example, consider templates `/Products/List` and `/Products/{id}`. It would be reasonable to assume that `/Products/List` is a better match than `/Products/{id}` for the URL path `/Products/List`. This works because the literal segment `/List` is considered to have better precedence than the parameter segment `/{id}`.

The details of how precedence works are coupled to how route templates are defined:

* Templates with more segments are considered more specific.
* A segment with literal text is considered more specific than a parameter segment.
* A parameter segment with a constraint is considered more specific than one without.
* A complex segment is considered as specific as a parameter segment with a constraint.
* Catch-all parameters are the least specific. See **catch-all** in the [Route templates](#rtr) section for important information on catch-all routes.

[]()

### URL generation concepts

URL generation:

* Is the process by which routing can create a URL path based on a set of route values.
* Allows for a logical separation between endpoints and the URLs that access them.

Endpoint routing includes the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) API. `LinkGenerator` is a singleton service available from [DI](dependency-injection?view=aspnetcore-10.0). The `LinkGenerator` API can be used outside of the context of an executing request. [Mvc.IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) and scenarios that rely on [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper), such as [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0), HTML Helpers, and [Action Results](../mvc/controllers/actions?view=aspnetcore-10.0), use the `LinkGenerator` API internally to provide link generating capabilities.

The link generator is backed by the concept of an **address** and **address schemes**. An address scheme is a way of determining the endpoints that should be considered for link generation. For example, the route name and route values scenarios many users are familiar with from controllers and Razor Pages are implemented as an address scheme.

The link generator can link to controllers and Razor Pages via the following extension methods:

* [GetPathByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.getpathbyaction)
* [GetUriByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.geturibyaction)
* [GetPathByPage](/en-us/dotnet/api/microsoft.aspnetcore.routing.pagelinkgeneratorextensions.getpathbypage)
* [GetUriByPage](/en-us/dotnet/api/microsoft.aspnetcore.routing.pagelinkgeneratorextensions.geturibypage)

Overloads of these methods accept arguments that include the `HttpContext`. These methods are functionally equivalent to [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.urlhelper.action) and [Url.Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.page), but offer additional flexibility and options.

The `GetPath*` methods are most similar to `Url.Action` and `Url.Page`, in that they generate a URI containing an absolute path. The `GetUri*` methods always generate an absolute URI containing a scheme and host. The methods that accept an `HttpContext` generate a URI in the context of the executing request. The [ambient](#ambient) route values, URL base path, scheme, and host from the executing request are used unless overridden.

[LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) is called with an address. Generating a URI occurs in two steps:

1. An address is bound to a list of endpoints that match the address.
2. Each endpoint's [RoutePattern](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.routepattern#microsoft-aspnetcore-routing-routeendpoint-routepattern) is evaluated until a route pattern that matches the supplied values is found. The resulting output is combined with the other URI parts supplied to the link generator and returned.

The methods provided by [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) support standard link generation capabilities for any type of address. The most convenient way to use the link generator is through extension methods that perform operations for a specific address type:

| Extension Method                                                                                  | Description                                                         |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [GetPathByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.getpathbyaddress) | Generates a URI with an absolute path based on the provided values. |
| [GetUriByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.geturibyaddress)   | Generates an absolute URI based on the provided values.             |

Warning

Pay attention to the following implications of calling [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) methods:

* Use `GetUri*` extension methods with caution in an app configuration that doesn't validate the `Host` header of incoming requests. If the `Host` header of incoming requests isn't validated, untrusted request input can be sent back to the client in URIs in a view or page. We recommend that all production apps configure their server to validate the `Host` header against known valid values.

* Use [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) with caution in middleware in combination with `Map` or `MapWhen`. `Map*` changes the base path of the executing request, which affects the output of link generation. All of the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) APIs allow specifying a base path. Specify an empty base path to undo the `Map*` affect on link generation.

### Middleware example

In the following example, a middleware uses the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) API to create a link to an action method that lists store products. Using the link generator by injecting it into a class and calling `GenerateLink` is available to any class in an app:

```
public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}
```

[]()

## Route templates

Tokens within `{}` define route parameters that are bound if the route is matched. More than one route parameter can be defined in a route segment, but route parameters must be separated by a literal value. For example:

`{controller=Home}{action=Index}`

isn't a valid route, because there's no literal value between `{controller}` and `{action}`. Route parameters must have a name and may have additional attributes specified.

Literal text other than route parameters (for example, `{id}`) and the path separator `/` must match the text in the URL. Text matching is case-insensitive and based on the decoded representation of the URL's path. To match a literal route parameter delimiter `{` or `}`, escape the delimiter by repeating the character. For example `{{` or `}}`.

Asterisk `*` or double asterisk `**`:

* Can be used as a prefix to a route parameter to bind to the rest of the URI.

* Are called a **catch-all** parameters. For example, `blog/{**slug}`:

  * Matches any URI that starts with `blog/` and has any value following it.
  * The value following `blog/` is assigned to the [slug](https://developer.mozilla.org/docs/Glossary/Slug) route value.

```
public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
```

Catch-all parameters can also match the empty string.

The catch-all parameter escapes the appropriate characters when the route is used to generate a URL, including path separator `/` characters. For example, the route `foo/{*path}` with route values `{ path = "my/path" }` generates `foo/my%2Fpath`. Note the escaped forward slash. To round-trip path separator characters, use the `**` route parameter prefix. The route `foo/{**path}` with `{ path = "my/path" }` generates `foo/my/path`.

URL patterns that attempt to capture a file name with an optional file extension have additional considerations. For example, consider the template `files/{filename}.{ext?}`. When values for both `filename` and `ext` exist, both values are populated. If only a value for `filename` exists in the URL, the route matches because the trailing `.` is optional. The following URLs match this route:

* `/files/myFile.txt`
* `/files/myFile`

Route parameters may have **default values** designated by specifying the default value after the parameter name separated by an equals sign (`=`). For example, `{controller=Home}` defines `Home` as the default value for `controller`. The default value is used if no value is present in the URL for the parameter. Route parameters are made optional by appending a question mark (`?`) to the end of the parameter name. For example, `id?`. The difference between optional values and default route parameters is:

* A route parameter with a default value always produces a value.
* An optional parameter has a value only when a value is provided by the request URL.

Route parameters may have constraints that must match the route value bound from the URL. Adding `:` and constraint name after the route parameter name specifies an inline constraint on a route parameter. If the constraint requires arguments, they're enclosed in parentheses `(...)` after the constraint name. Multiple *inline constraints* can be specified by appending another `:` and constraint name.

The constraint name and arguments are passed to the [IInlineConstraintResolver](/en-us/dotnet/api/microsoft.aspnetcore.routing.iinlineconstraintresolver) service to create an instance of [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) to use in URL processing. For example, the route template `blog/{article:minlength(10)}` specifies a `minlength` constraint with the argument `10`. For more information on route constraints and a list of the constraints provided by the framework, see the [Route constraints](#route-constraints) section.

Route parameters may also have parameter transformers. Parameter transformers transform a parameter's value when generating links and matching actions and pages to URLs. Like constraints, parameter transformers can be added inline to a route parameter by adding a `:` and transformer name after the route parameter name. For example, the route template `blog/{article:slugify}` specifies a `slugify` transformer. For more information on parameter transformers, see the [Parameter transformers](#parameter-transformers) section.

The following table demonstrates example route templates and their behavior.

| Route Template                           | Example Matching URI    | The request URI…                                                            |
| ---------------------------------------- | ----------------------- | --------------------------------------------------------------------------- |
| `hello`                                  | `/hello`                | Only matches the single path `/hello`.                                      |
| `{Page=Home}`                            | `/`                     | Matches and sets `Page` to `Home`.                                          |
| `{Page=Home}`                            | `/Contact`              | Matches and sets `Page` to `Contact`.                                       |
| `{controller}/{action}/{id?}`            | `/Products/List`        | Maps to the `Products` controller and `List` action.                        |
| `{controller}/{action}/{id?}`            | `/Products/Details/123` | Maps to the `Products` controller and `Details` action with`id` set to 123. |
| `{controller=Home}/{action=Index}/{id?}` | `/`                     | Maps to the `Home` controller and `Index` method. `id` is ignored.          |
| `{controller=Home}/{action=Index}/{id?}` | `/Products`             | Maps to the `Products` controller and `Index` method. `id` is ignored.      |

Using a template is generally the simplest approach to routing. Constraints and defaults can also be specified outside the route template.

### Complex segments

Complex segments are processed by matching up literal delimiters from right to left in a [non-greedy](#greedy) way. For example, `[Route("/a{b}c{d}")]` is a complex segment. Complex segments work in a particular way that must be understood to use them successfully. The example in this section demonstrates why complex segments only really work well when the delimiter text doesn't appear inside the parameter values. Using a [regex](/en-us/dotnet/standard/base-types/regular-expressions) and then manually extracting the values is needed for more complex cases.

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

This is a summary of the steps that routing performs with the template `/a{b}c{d}` and the URL path `/abcd`. The `|` is used to help visualize how the algorithm works:

* The first literal, right to left, is `c`. So `/abcd` is searched from right and finds `/ab|c|d`.
* Everything to the right (`d`) is now matched to the route parameter `{d}`.
* The next literal, right to left, is `a`. So `/ab|c|d` is searched starting where we left off, then `a` is found `/|a|b|c|d`.
* The value to the right (`b`) is now matched to the route parameter `{b}`.
* There is no remaining text and no remaining route template, so this is a match.

Here's an example of a negative case using the same template `/a{b}c{d}` and the URL path `/aabcd`. The `|` is used to help visualize how the algorithm works. This case isn't a match, which is explained by the same algorithm:

* The first literal, right to left, is `c`. So `/aabcd` is searched from right and finds `/aab|c|d`.
* Everything to the right (`d`) is now matched to the route parameter `{d}`.
* The next literal, right to left, is `a`. So `/aab|c|d` is searched starting where we left off, then `a` is found `/a|a|b|c|d`.
* The value to the right (`b`) is now matched to the route parameter `{b}`.
* At this point there is remaining text `a`, but the algorithm has run out of route template to parse, so this is not a match.

Since the matching algorithm is [non-greedy](#greedy):

* It matches the smallest amount of text possible in each step.
* Any case where the delimiter value appears inside the parameter values results in not matching.

Regular expressions provide much more control over their matching behavior.

[]()

Greedy matching, also known as [lazy matching](https://wikipedia.org/wiki/Regular_expression#Lazy_matching), matches the largest possible string. Non-greedy matches the smallest possible string.

## Routing with special characters

Routing with special characters can lead to unexpected results. For example, consider a controller with the following action method:

```
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}
```

When `string id` contains the following encoded values, unexpected results might occur:

| ASCII | Encoded |
| ----- | ------- |
| `/`   | `%2F`   |
| ``    | `+`     |

Route parameters are not always URL decoded. This problem may be addressed in the future. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/11544);

## Route constraints

Route constraints execute when a match has occurred to the incoming URL and the URL path is tokenized into route values. Route constraints generally inspect the route value associated via the route template and make a true or false decision about whether the value is acceptable. Some route constraints use data outside the route value to consider whether the request can be routed. For example, the [HttpMethodRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.constraints.httpmethodrouteconstraint) can accept or reject a request based on its HTTP verb. Constraints are used in routing requests and link generation.

Warning

Don't use constraints for input validation. If constraints are used for input validation, invalid input results in a `404` Not Found response. Invalid input should produce a `400` Bad Request with an appropriate error message. Route constraints are used to disambiguate similar routes, not to validate the inputs for a particular route.

The following table demonstrates example route constraints and their expected behavior.

| constraint          | Example                                     | Example Matches                        | Notes                                                                                     |
| ------------------- | ------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- |
| `int`               | `{id:int}`                                  | `123456789`, `-123456789`              | Matches any integer                                                                       |
| `bool`              | `{active:bool}`                             | `true`, `FALSE`                        | Matches `true` or `false`. Case-insensitive                                               |
| `datetime`          | `{dob:datetime}`                            | `2016-12-31`, `2016-12-31 7:32pm`      | Matches a valid `DateTime` value in the invariant culture. See preceding warning.         |
| `decimal`           | `{price:decimal}`                           | `49.99`, `-1,000.01`                   | Matches a valid `decimal` value in the invariant culture. See preceding warning.          |
| `double`            | `{weight:double}`                           | `1.234`, `-1,001.01e8`                 | Matches a valid `double` value in the invariant culture. See preceding warning.           |
| `float`             | `{weight:float}`                            | `1.234`, `-1,001.01e8`                 | Matches a valid `float` value in the invariant culture. See preceding warning.            |
| `guid`              | `{id:guid}`                                 | `CD2C1638-1638-72D5-1638-DEADBEEF1638` | Matches a valid `Guid` value                                                              |
| `long`              | `{ticks:long}`                              | `123456789`, `-123456789`              | Matches a valid `long` value                                                              |
| `minlength(value)`  | `{username:minlength(4)}`                   | `Rick`                                 | String must be at least 4 characters                                                      |
| `maxlength(value)`  | `{filename:maxlength(8)}`                   | `MyFile`                               | String must be no more than 8 characters                                                  |
| `length(length)`    | `{filename:length(12)}`                     | `somefile.txt`                         | String must be exactly 12 characters long                                                 |
| `length(min,max)`   | `{filename:length(8,16)}`                   | `somefile.txt`                         | String must be at least 8 and no more than 16 characters long                             |
| `min(value)`        | `{age:min(18)}`                             | `19`                                   | Integer value must be at least 18                                                         |
| `max(value)`        | `{age:max(120)}`                            | `91`                                   | Integer value must be no more than 120                                                    |
| `range(min,max)`    | `{age:range(18,120)}`                       | `91`                                   | Integer value must be at least 18 but no more than 120                                    |
| `alpha`             | `{name:alpha}`                              | `Rick`                                 | String must consist of one or more alphabetical characters, `a`-`z` and case-insensitive. |
| `regex(expression)` | `{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}` | `123-45-6789`                          | String must match the regular expression. See tips about defining a regular expression.   |
| `required`          | `{name:required}`                           | `Rick`                                 | Enforces that an explicit value (not an ambient value) is present during URL generation.  |

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

Multiple, colon delimited constraints can be applied to a single parameter. For example, the following constraint restricts a parameter to an integer value of 1 or greater:

```
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
```

Warning

Route constraints that verify the URL and are converted to a CLR type always use the invariant culture. For example, conversion to the CLR type `int` or `DateTime`. These constraints assume that the URL is not localizable. The framework-provided route constraints don't modify the values stored in route values. All route values parsed from the URL are stored as strings. For example, the `float` constraint attempts to convert the route value to a float, but the converted value is used only to verify it can be converted to a float.

### Regular expressions in constraints

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

Regular expressions can be specified as inline constraints using the `regex(...)` route constraint. Methods in the [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) family also accept an object literal of constraints. If that form is used, string values are interpreted as regular expressions.

The following code uses an inline regex constraint:

```
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");
```

The following code uses an object literal to specify a regex constraint:

```
app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });
```

The ASP.NET Core framework adds `RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant` to the regular expression constructor. See [RegexOptions](/en-us/dotnet/api/system.text.regularexpressions.regexoptions) for a description of these members.

Regular expressions use delimiters and tokens similar to those used by routing and the C# language. Regular expression tokens must be escaped. To use the regular expression `^\d{3}-\d{2}-\d{4}$` in an inline constraint, use one of the following:

* Replace `\` characters provided in the string as `\\` characters in the C# source file in order to escape the `\` string escape character.
* [Verbatim string literals](/en-us/dotnet/csharp/language-reference/keywords/string).

To escape routing parameter delimiter characters `{`, `}`, `[`, `]`, double the characters in the expression, for example, `{{`, `}}`, `[[`, `]]`. The following table shows a regular expression and its escaped version.

| Regular expression    | Escaped regular expression     |
| --------------------- | ------------------------------ |
| `^\d{3}-\d{2}-\d{4}$` | `^\\d{{3}}-\\d{{2}}-\\d{{4}}$` |
| `^[a-z]{2}$`          | `^[[a-z]]{{2}}$`               |

Regular expressions used in routing often start with the `^` character and match the starting position of the string. The expressions often end with the `$` character and match the end of the string. The `^` and `$` characters ensure that the regular expression matches the entire route parameter value. Without the `^` and `$` characters, the regular expression matches any substring within the string, which is often undesirable. The following table provides examples and explains why they match or fail to match.

| Expression   | String    | Match | Comment               |
| ------------ | --------- | ----- | --------------------- |
| `[a-z]{2}`   | hello     | Yes   | Substring matches     |
| `[a-z]{2}`   | 123abc456 | Yes   | Substring matches     |
| `[a-z]{2}`   | mz        | Yes   | Matches expression    |
| `[a-z]{2}`   | MZ        | Yes   | Not case sensitive    |
| `^[a-z]{2}$` | hello     | No    | See `^` and `$` above |
| `^[a-z]{2}$` | 123abc456 | No    | See `^` and `$` above |

For more information on regular expression syntax, see [.NET Framework Regular Expressions](/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference).

To constrain a parameter to a known set of possible values, use a regular expression. For example, `{action:regex(^(list|get|create)$)}` only matches the `action` route value to `list`, `get`, or `create`. If passed into the constraints dictionary, the string `^(list|get|create)$` is equivalent. Constraints that are passed in the constraints dictionary that don't match one of the known constraints are also treated as regular expressions. Constraints that are passed within a template that don't match one of the known constraints are not treated as regular expressions.

### Custom route constraints

Custom route constraints can be created by implementing the [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) interface. The `IRouteConstraint` interface contains [Match](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint.match), which returns `true` if the constraint is satisfied and `false` otherwise.

Custom route constraints are rarely needed. Before implementing a custom route constraint, consider alternatives, such as model binding.

The ASP.NET Core [Constraints](https://github.com/dotnet/aspnetcore/tree/main/src/Http/Routing/src/Constraints) folder provides good examples of creating constraints. For example, [GuidRouteConstraint](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs#L18).

To use a custom `IRouteConstraint`, the route constraint type must be registered with the app's [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap) in the service container. A `ConstraintMap` is a dictionary that maps route constraint keys to `IRouteConstraint` implementations that validate those constraints. An app's `ConstraintMap` can be updated in `Program.cs` either as part of an [AddRouting](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.routingservicecollectionextensions.addrouting) call or by configuring [RouteOptions](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions) directly with `builder.Services.Configure<RouteOptions>`. For example:

```
builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));
```

The preceding constraint is applied in the following code:

```
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}
```

The implementation of `NoZeroesRouteConstraint` prevents `0` being used in a route parameter:

```
public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[1-9]*$",
        RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
        TimeSpan.FromMilliseconds(100));

    public bool Match(
        HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}
```

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

The preceding code:

* Prevents `0` in the `{id}` segment of the route.
* Is shown to provide a basic example of implementing a custom constraint. It should not be used in a production app.

The following code is a better approach to preventing an `id` containing a `0` from being processed:

```
[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}
```

The preceding code has the following advantages over the `NoZeroesRouteConstraint` approach:

* It doesn't require a custom constraint.
* It returns a more descriptive error when the route parameter includes `0`.

## Parameter transformers

Parameter transformers:

* Execute when generating a link using [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator).
* Implement [Microsoft.AspNetCore.Routing.IOutboundParameterTransformer](/en-us/dotnet/api/microsoft.aspnetcore.routing.ioutboundparametertransformer).
* Are configured using [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap).
* Take the parameter's route value and transform it to a new string value.
* Result in using the transformed value in the generated link.

For example, a custom `slugify` parameter transformer in route pattern `blog\{article:slugify}` with `Url.Action(new { article = "MyTestArticle" })` generates `blog\my-test-article`.

Consider the following `IOutboundParameterTransformer` implementation:

```
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}
```

To use a parameter transformer in a route pattern, configure it using [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap) in `Program.cs`:

```
builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
```

The ASP.NET Core framework uses parameter transformers to transform the URI where an endpoint resolves. For example, parameter transformers transform the route values used to match an `area`, `controller`, `action`, and `page`:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
```

With the preceding route template, the action `SubscriptionManagementController.GetAll` is matched with the URI `/subscription-management/get-all`. A parameter transformer doesn't change the route values used to generate a link. For example, `Url.Action("GetAll", "SubscriptionManagement")` outputs `/subscription-management/get-all`.

ASP.NET Core provides API conventions for using parameter transformers with generated routes:

* The [Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.routetokentransformerconvention) MVC convention applies a specified parameter transformer to all attribute routes in the app. The parameter transformer transforms attribute route tokens as they are replaced. For more information, see [Use a parameter transformer to customize token replacement](../mvc/controllers/routing?view=aspnetcore-10.0#use-a-parameter-transformer-to-customize-token-replacement).
* Razor Pages uses the [PageRouteTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutetransformerconvention) API convention. This convention applies a specified parameter transformer to all automatically discovered Razor Pages. The parameter transformer transforms the folder and file name segments of Razor Pages routes. For more information, see [Use a parameter transformer to customize page routes](../razor-pages/razor-pages-conventions?view=aspnetcore-10.0#use-a-parameter-transformer-to-customize-page-routes).

[]()

## URL generation reference

This section contains a reference for the algorithm implemented by URL generation. In practice, most complex examples of URL generation use controllers or Razor Pages. See [routing in controllers](../mvc/controllers/routing?view=aspnetcore-10.0) for additional information.

The URL generation process begins with a call to [LinkGenerator.GetPathByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.getpathbyaddress) or a similar method. The method is provided with an address, a set of route values, and optionally information about the current request from `HttpContext`.

The first step is to use the address to resolve a set of candidate endpoints using an [IEndpointAddressScheme\<TAddress>](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointaddressscheme-1) that matches the address's type.

Once the set of candidates is found by the address scheme, the endpoints are ordered and processed iteratively until a URL generation operation succeeds. URL generation does **not** check for ambiguities, the first result returned is the final result.

### Troubleshooting URL generation with logging

The first step in troubleshooting URL generation is setting the logging level of `Microsoft.AspNetCore.Routing` to `TRACE`. `LinkGenerator` logs many details about its processing which can be useful to troubleshoot problems.

See [URL generation reference](#ugr) for details on URL generation.

### Addresses

Addresses are the concept in URL generation used to bind a call into the link generator to a set of candidate endpoints.

Addresses are an extensible concept that come with two implementations by default:

* Using *endpoint name* (`string`) as the address:

  * Provides similar functionality to MVC's route name.
  * Uses the [IEndpointNameMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointnamemetadata) metadata type.
  * Resolves the provided string against the metadata of all registered endpoints.
  * Throws an exception on startup if multiple endpoints use the same name.
  * Recommended for general-purpose use outside of controllers and Razor Pages.

* Using *route values* ([RouteValuesAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.routevaluesaddress)) as the address:

  * Provides similar functionality to controllers and Razor Pages legacy URL generation.
  * Very complex to extend and debug.
  * Provides the implementation used by `IUrlHelper`, Tag Helpers, HTML Helpers, Action Results, etc.

The role of the address scheme is to make the association between the address and matching endpoints by arbitrary criteria:

* The endpoint name scheme performs a basic dictionary lookup.
* The route values scheme has a complex best subset of set algorithm.

[]()

### Ambient values and explicit values

From the current request, routing accesses the route values of the current request `HttpContext.Request.RouteValues`. The values associated with the current request are referred to as the **ambient values**. For the purpose of clarity, the documentation refers to the route values passed in to methods as **explicit values**.

The following example shows ambient values and explicit values. It provides ambient values from the current request and explicit values:

```
public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...
```

The preceding code:

* Returns `/Widget/Index/17`
* Gets [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) via [DI](dependency-injection?view=aspnetcore-10.0).

The following code provides only explicit values and no ambient values:

```
var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;
```

The preceding method returns `/Home/Subscribe/17`

The following code in the `WidgetController` returns `/Widget/Subscribe/17`:

```
var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });
```

The following code provides the controller from ambient values in the current request and explicit values:

```
public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}
```

In the preceding code:

* `/Gadget/Edit/17` is returned.
* [Url](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.url#microsoft-aspnetcore-mvc-controllerbase-url) gets the [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper).
* [Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.action) generates a URL with an absolute path for an action method. The URL contains the specified `action` name and `route` values.

The following code provides ambient values from the current request and explicit values:

```
public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}
```

The preceding code sets `url` to `/Edit/17` when the Edit Razor Page contains the following page directive:

`@page "{id:int}"`

If the Edit page doesn't contain the `"{id:int}"` route template, `url` is `/Edit?id=17`.

The behavior of MVC's [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) adds a layer of complexity in addition to the rules described here:

* `IUrlHelper` always provides the route values from the current request as ambient values.
* [IUrlHelper.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.action) always copies the current `action` and `controller` route values as explicit values unless overridden by the developer.
* [IUrlHelper.Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.page) always copies the current `page` route value as an explicit value unless overridden.
* `IUrlHelper.Page` always overrides the current `handler` route value with `null` as an explicit values unless overridden.

Users are often surprised by the behavioral details of ambient values, because MVC doesn't seem to follow its own rules. For historical and compatibility reasons, certain route values such as `action`, `controller`, `page`, and `handler` have their own special-case behavior.

The equivalent functionality provided by `LinkGenerator.GetPathByAction` and `LinkGenerator.GetPathByPage` duplicates these anomalies of `IUrlHelper` for compatibility.

### URL generation process

Once the set of candidate endpoints are found, the URL generation algorithm:

* Processes the endpoints iteratively.
* Returns the first successful result.

The first step in this process is called **route value invalidation**. Route value invalidation is the process by which routing decides which route values from the ambient values should be used and which should be ignored. Each ambient value is considered and either combined with the explicit values, or ignored.

The best way to think about the role of ambient values is that they attempt to save application developers typing, in some common cases. Traditionally, the scenarios where ambient values are helpful are related to MVC:

* When linking to another action in the same controller, the controller name doesn't need to be specified.
* When linking to another controller in the same area, the area name doesn't need to be specified.
* When linking to the same action method, route values don't need to be specified.
* When linking to another part of the app, you don't want to carry over route values that have no meaning in that part of the app.

Calls to `LinkGenerator` or `IUrlHelper` that return `null` are usually caused by not understanding route value invalidation. Troubleshoot route value invalidation by explicitly specifying more of the route values to see if that solves the problem.

Route value invalidation works on the assumption that the app's URL scheme is hierarchical, with a hierarchy formed from left-to-right. Consider the basic controller route template `{controller}/{action}/{id?}` to get an intuitive sense of how this works in practice. A **change** to a value **invalidates** all of the route values that appear to the right. This reflects the assumption about hierarchy. If the app has an ambient value for `id`, and the operation specifies a different value for the `controller`:

* `id` won't be reused because `{controller}` is to the left of `{id?}`.

Some examples demonstrating this principle:

* If the explicit values contain a value for `id`, the ambient value for `id` is ignored. The ambient values for `controller` and `action` can be used.
* If the explicit values contain a value for `action`, any ambient value for `action` is ignored. The ambient values for `controller` can be used. If the explicit value for `action` is different from the ambient value for `action`, the `id` value won't be used. If the explicit value for `action` is the same as the ambient value for `action`, the `id` value can be used.
* If the explicit values contain a value for `controller`, any ambient value for `controller` is ignored. If the explicit value for `controller` is different from the ambient value for `controller`, the `action` and `id` values won't be used. If the explicit value for `controller` is the same as the ambient value for `controller`, the `action` and `id` values can be used.

This process is further complicated by the existence of attribute routes and dedicated conventional routes. Controller conventional routes such as `{controller}/{action}/{id?}` specify a hierarchy using route parameters. For [dedicated conventional routes](../mvc/controllers/routing?view=aspnetcore-10.0#dcr) and [attribute routes](../mvc/controllers/routing?view=aspnetcore-10.0#ar) to controllers and Razor Pages:

* There is a hierarchy of route values.
* They don't appear in the template.

For these cases, URL generation defines the **required values** concept. Endpoints created by controllers and Razor Pages have required values specified that allow route value invalidation to work.

The route value invalidation algorithm in detail:

* The required value names are combined with the route parameters, then processed from left-to-right.

* For each parameter, the ambient value and explicit value are compared:

  * If the ambient value and explicit value are the same, the process continues.
  * If the ambient value is present and the explicit value isn't, the ambient value is used when generating the URL.
  * If the ambient value isn't present and the explicit value is, reject the ambient value and all subsequent ambient values.
  * If the ambient value and the explicit value are present, and the two values are different, reject the ambient value and all subsequent ambient values.

At this point, the URL generation operation is ready to evaluate route constraints. The set of accepted values is combined with the parameter default values, which is provided to constraints. If the constraints all pass, the operation continues.

Next, the **accepted values** can be used to expand the route template. The route template is processed:

* From left-to-right.

* Each parameter has its accepted value substituted.

* With the following special cases:

  * If the accepted values is missing a value and the parameter has a default value, the default value is used.
  * If the accepted values is missing a value and the parameter is optional, processing continues.
  * If any route parameter to the right of a missing optional parameter has a value, the operation fails.
  * Contiguous default-valued parameters and optional parameters are collapsed where possible.

Note

For endpoints created by controllers and Razor Pages, `controller`, `action`, `page`, and `area` are part of the endpoint's **required values**, so they're always present in the accepted values. As a result:

* They aren't invalidated by a change to a route value that appears to their left, such as a `culture` parameter.
* Inline default values in the route template, such as `{controller=DefaultController}`, aren't applied, because default values are used only when the accepted values are *missing* a value.

For example, with the template `{culture}/api1/{controller=DefaultController}/{action=DefaultAction}/{id?}` and a current request of `/en/api1/MyController/Index/111`, the call `Url.RouteUrl("ApiRoute1", new { culture = "ar" })` generates `/ar/api1/MyController/Index`, not `/ar/api1/DefaultController/DefaultAction`. The `id` value isn't used because it has no required value and is to the right of a changed value. To use the template's default values, specify them explicitly:

```
Url.RouteUrl("ApiRoute1", new { culture = "ar", controller = "DefaultController", action = "DefaultAction" })
```

For a related limitation, see [Problems with route value invalidation](#problems-with-route-value-invalidation).

Values explicitly provided that don't match a segment of the route are added to the query string. The following table shows the result when using the route template `{controller}/{action}/{id?}`.

| Ambient Values                     | Explicit Values                        | Result                  |
| ---------------------------------- | -------------------------------------- | ----------------------- |
| controller = "Home"                | action = "About"                       | `/Home/About`           |
| controller = "Home"                | controller = "Order", action = "About" | `/Order/About`          |
| controller = "Home", color = "Red" | action = "About"                       | `/Home/About`           |
| controller = "Home"                | action = "About", color = "Red"        | `/Home/About?color=Red` |

### Optional route parameter order

Optional route parameters must come after all required route parameters. In the following code, the `id` and `name` parameters must come after the `color` parameter:

```
using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers;

[Route("api/[controller]")]
public class MyController : ControllerBase
{
    // GET /api/my/red/2/joe
    // GET /api/my/red/2
    // GET /api/my
    [HttpGet("{color}/{id:int?}/{name?}")]
    public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
    {
        return Ok($"{color} {id} {name ?? ""}");
    }
}
```

### Problems with route value invalidation

For related information about how required values, such as `controller` and `action`, affect route value invalidation and template default values, see the note in [URL generation process](#url-generation-process).

The following code shows an example of a URL generation scheme that's not supported by routing:

```
app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });
```

In the preceding code, the `culture` route parameter is used for localization. The desire is to have the `culture` parameter always accepted as an ambient value. However, the `culture` parameter is not accepted as an ambient value because of the way required values work:

* In the `"default"` route template, the `culture` route parameter is to the left of `controller`, so changes to `controller` won't invalidate `culture`.
* In the `"blog"` route template, the `culture` route parameter is considered to be to the right of `controller`, which appears in the required values.

## Parse URL paths with `LinkParser`

The [LinkParser](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkparser) class adds support for parsing a URL path into a set of route values. The [ParsePathByEndpointName](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkparserendpointnameaddressextensions.parsepathbyendpointname) method takes an endpoint name and a URL path, and returns a set of route values extracted from the URL path.

In the following example controller, the `GetProduct` action uses a route template of `api/Products/{id}` and has a [Name](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute.name) of `GetProduct`:

```
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...
```

In the same controller class, the `AddRelatedProduct` action expects a URL path, `pathToRelatedProduct`, which can be provided as a query-string parameter:

```
[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...
```

In the preceding example, the `AddRelatedProduct` action extracts the `id` route value from the URL path. For example, with a URL path of `/api/Products/1`, the `relatedProductId` value is set to `1`. This approach allows the API's clients to use URL paths when referring to resources, without requiring knowledge of how such a URL is structured.

## Configure endpoint metadata

The following links provide information on how to configure endpoint metadata:

* [Enable Cors with endpoint routing](../security/cors?view=aspnetcore-10.0#enable-cors-with-endpoint-routing)
* [IAuthorizationPolicyProvider sample](https://github.com/dotnet/AspNetCore/tree/release/3.1/src/Security/samples/CustomPolicyProvider) using a custom `[MinimumAgeAuthorize]` attribute
* [Test authentication with the \[Authorize\] attribute](../security/authentication/identity?view=aspnetcore-10.0#test-identity)
* [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization)
* [Select a scheme with an `[Authorize]` attribute](../security/authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0#select-a-scheme-with-an-authorize-attribute)
* [Apply policies using an `[Authorize]` attribute](../security/authorization/policies?view=aspnetcore-10.0#apply-policies-to-mvc-controllers)
* [Role-based authorization in ASP.NET Core](../security/authorization/roles?view=aspnetcore-10.0)

[]()

## Host matching in routes with RequireHost

[RequireHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.requirehost) applies a constraint to the route which requires the specified host. The `RequireHost` or [\[Host\]](/en-us/dotnet/api/microsoft.aspnetcore.routing.hostattribute) parameter can be a:

* Host: `www.domain.com`, matches `www.domain.com` with any port.
* Host with wildcard: `*.domain.com`, matches `www.domain.com`, `subdomain.domain.com`, or `www.subdomain.domain.com` on any port.
* Port: `*:5000`, matches port 5000 with any host.
* Host and port: `www.domain.com:5000` or `*.domain.com:5000`, matches host and port.

Multiple parameters can be specified using `RequireHost` or `[Host]`. The constraint matches hosts valid for any of the parameters. For example, `[Host("domain.com", "*.domain.com")]` matches `domain.com`, `www.domain.com`, and `subdomain.domain.com`.

The following code uses `RequireHost` to require the specified host on the route:

```
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");
```

The following code uses the `[Host]` attribute on the controller to require any of the specified hosts:

```
[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}
```

When the `[Host]` attribute is applied to both the controller and action method:

* The attribute on the action is used.
* The controller attribute is ignored.

## Route groups

The [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) extension method helps organize groups of endpoints with a common prefix and reduces repetitive code. Use this method to customize entire groups of endpoints with a single call to methods like [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) and [WithMetadata](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withmetadata) that add [endpoint metadata](routing?view=aspnetcore-10.0#endpoint-metadata).

For example, the following code creates two similar groups of endpoints:

```
app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
```

```
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}
```

In this scenario, you can use a relative address for the `Location` header in the `201 Created` result:

```
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}
```

The first group of endpoints matches only requests prefixed with `/public/todos` and are accessible without any authentication. The second group of endpoints matches only requests prefixed with `/private/todos` and require authentication.

`QueryPrivateTodos` is a local function that modifies the `TodoDb` parameters of the route handler, to enable them to access and store private todo data. `QueryPrivateTodos` serves as an [endpoint filter factory](minimal-apis/min-api-filters?view=aspnetcore-10.0).

Route groups also support nested groups and complex prefix patterns with route parameters and constraints. In the following example, the route handler mapped to the `user` group can capture the `{org}` and `{group}` route parameters defined in the outer group prefixes.

The prefix can also be empty. This approach can be useful for adding endpoint metadata or filters to a group of endpoints without changing the route pattern.

```
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
```

Adding filters or metadata to a group results in the same behavior as adding them individually to each endpoint (before adding extra filters or metadata that might exist in an inner group or specific endpoint).

```
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});
```

In the preceding example, the outer filter logs the incoming request before the inner filter even though the outer filter is added second. Because the filters are applied to different groups, the order that they're added relative to each other doesn't matter. The order in which filters are added matters when applied to the same group or specific endpoint.

A request to `/outer/inner/` logs the following data:

```
/outer group filter
/inner group filter
MapGet filter
```

## Performance guidance for routing

When an app has performance problems, routing is often suspected as the problem. The reason routing is suspected is that frameworks like controllers and Razor Pages report the amount of time spent inside the framework in their logging messages. When there's a significant difference between the time reported by controllers and the total time of the request:

* Developers eliminate their app code as the source of the problem.
* It's common to assume routing is the cause.

Routing is performance tested using thousands of endpoints. It's unlikely that a typical app will encounter a performance problem just by being too large. The most common root cause of slow routing performance is usually a badly-behaving custom middleware.

This following code sample demonstrates a basic technique for narrowing down the source of delay:

```
var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");
```

To time routing:

* Interleave each middleware with a copy of the timing middleware shown in the preceding code.
* Add a unique identifier to correlate the timing data with the code.

This is a basic way to narrow down the delay when it's significant, for example, more than `10ms`. Subtracting `Time 2` from `Time 1` reports the time spent inside the `UseRouting` middleware.

The following code uses a more compact approach to the preceding timing code:

```
public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
```

```
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseRouting();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.MapGet("/", () => "Timing Test.");
```

### Potentially expensive routing features

The following list provides some insight into routing features that are relatively expensive compared with basic route templates:

* Regular expressions: It's possible to write regular expressions that are complex, or have long running time with a small amount of input.

* Complex segments (`{x}-{y}-{z}`):

  * Are significantly more expensive than parsing a regular URL path segment.
  * Result in many more substrings being allocated.

* Synchronous data access: Many complex apps have database access as part of their routing. Use extensibility points such as [MatcherPolicy](/en-us/dotnet/api/microsoft.aspnetcore.routing.matcherpolicy) and [EndpointSelectorContext](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointselectorcontext), which are asynchronous.

### Guidance for large route tables

By default ASP.NET Core uses a routing algorithm that trades memory for CPU time. This has the nice effect that route matching time is dependent only on the length of the path to match and not the number of routes. However, this approach can be potentially problematic in some cases, when the app has a large number of routes (in the thousands) and there is a high amount of variable prefixes in the routes. For example, if the routes have parameters in early segments of the route, like `{parameter}/some/literal`.

It is unlikely for an app to run into a situation where this is a problem unless:

* There are a high number of routes in the app using this pattern.
* There is a large number of routes in the app.

#### How to determine if an app is running into the large route table problem

* There are two symptoms to look for:

  * The app is slow to start on the first request.
    * Note that this is required but not sufficient. There are many other non-route problems than can cause slow app startup. Check for the condition below to accurately determine the app is running into this situation.
  * The app consumes a lot of memory during startup and a memory dump shows a large number of `Microsoft.AspNetCore.Routing.Matching.DfaNode` instances.

#### How to address this issue

There are several techniques and optimizations can be applied to routes that will largely improve this scenario:

* Apply route constraints to your parameters, for example `{parameter:int}`, `{parameter:guid}`, `{parameter:regex(\\d+)}`, etc. where possible.

  * This allows the routing algorithm to internally optimize the structures used for matching and drastically reduce the memory used.
  * In the vast majority of cases this will suffice to get back to an acceptable behavior.

* Change the routes to move parameters to later segments in the template.
  * This reduces the number of possible "paths" to match an endpoint given a path.

* Use a dynamic route and perform the mapping to a controller/page dynamically.
  * This can be achieved using `MapDynamicControllerRoute` and `MapDynamicPageRoute`.

## Guidance for library authors

This section contains guidance for library authors building on top of routing. These details are intended to ensure that app developers have a good experience using libraries and frameworks that extend routing.

### Define endpoints

To create a framework that uses routing for URL matching, start by defining a user experience that builds on top of [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).

**DO** build on top of [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder). This allows users to compose your framework with other ASP.NET Core features without confusion. Every ASP.NET Core template includes routing. Assume routing is present and familiar for users.

```
// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");
```

**DO** return a sealed concrete type from a call to `MapMyFramework(...)` that implements [IEndpointConventionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iendpointconventionbuilder). Most framework `Map...` methods follow this pattern. The `IEndpointConventionBuilder` interface:

* Allows for metadata to be composed.
* Is targeted by a variety of extension methods.

Declaring your own type allows you to add your own framework-specific functionality to the builder. It's ok to wrap a framework-declared builder and forward calls to it.

```
// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");
```

**CONSIDER** writing your own [EndpointDataSource](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource). `EndpointDataSource` is the low-level primitive for declaring and updating a collection of endpoints. `EndpointDataSource` is a powerful API used by controllers and Razor Pages.

The routing tests have a [basic example](https://github.com/dotnet/AspNetCore/blob/main/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs#L17) of a non-updating data source.

**CONSIDER** implementing [GetGroupedEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource.getgroupedendpoints). This gives complete control over running group conventions and the final metadata on the grouped endpoints. For example, this allows custom `EndpointDataSource` implementations to run [endpoint filters](minimal-apis/min-api-filters?view=aspnetcore-10.0) added to groups.

**DO NOT** attempt to register an `EndpointDataSource` by default. Require users to register your framework in [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints). The philosophy of routing is that nothing is included by default, and that `UseEndpoints` is the place to register endpoints.

### Creating routing-integrated middleware

**CONSIDER** defining metadata types as an interface.

**DO** make it possible to use metadata types as an attribute on classes and methods.

```
public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}
```

Frameworks like controllers and Razor Pages support applying metadata attributes to types and methods. If you declare metadata types:

* Make them accessible as [attributes](/en-us/dotnet/csharp/programming-guide/concepts/attributes/).
* Most users are familiar with applying attributes.

Declaring a metadata type as an interface adds another layer of flexibility:

* Interfaces are composable.
* Developers can declare their own types that combine multiple policies.

**DO** make it possible to override metadata, as shown in the following example:

```
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}
```

The best way to follow these guidelines is to avoid defining **marker metadata**:

* Don't just look for the presence of a metadata type.
* Define a property on the metadata and check the property.

The metadata collection is ordered and supports overriding by priority. In the case of controllers, metadata on the action method is most specific.

**DO** make middleware useful with and without routing:

```
app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();
```

As an example of this guideline, consider the authorization middleware. A configured fallback policy applies when the middleware can't produce a policy from authorization metadata, including:

* Endpoints without authorization metadata that produces a policy.
* Requests that don't match an endpoint.

This behavior makes the authorization middleware useful outside of routing and for traditional middleware programming. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

## Debug diagnostics

For detailed routing diagnostic output, set `Logging:LogLevel:Microsoft` to `Debug`. In the `Development` environment, set the log level in `appsettings.Development.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
```

Routing is responsible for matching incoming HTTP requests and dispatching those requests to the app's executable endpoints. [Endpoints](#endpoints) are the app's units of executable request-handling code. Endpoints are defined in the app and configured when the app starts. The endpoint matching process can extract values from the request's URL and provide those values for request processing. Using endpoint information from the app, routing is also able to generate URLs that map to endpoints.

Apps can configure routing using:

* Controllers
* Razor Pages
* SignalR
* gRPC Services
* Endpoint-enabled [middleware](middleware/?view=aspnetcore-10.0) such as [Health Checks](../host-and-deploy/health-checks?view=aspnetcore-10.0).
* Delegates and lambdas registered with routing.

This article covers low-level details of ASP.NET Core routing. For information on configuring routing:

* For controllers, see [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0).
* For Razor Pages conventions, see [Razor Pages route and app conventions in ASP.NET Core](../razor-pages/razor-pages-conventions?view=aspnetcore-10.0).

## Routing basics

The following code shows a basic example of routing:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding example includes a single endpoint using the [MapGet](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapget) method:

* When an HTTP `GET` request is sent to the root URL `/`:

  * The request delegate executes.
  * `Hello World!` is written to the HTTP response.

* If the request method is not `GET` or the root URL is not `/`, no route matches and an HTTP 404 is returned.

Routing uses a pair of middleware, registered by [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints):

* `UseRouting` adds route matching to the middleware pipeline. This middleware looks at the set of endpoints defined in the app, and selects the [best match](#urlm) based on the request.
* `UseEndpoints` adds endpoint execution to the middleware pipeline. It runs the delegate associated with the selected endpoint.

Apps typically don't need to call `UseRouting` or `UseEndpoints`. [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) configures a middleware pipeline that wraps middleware added in `Program.cs` with `UseRouting` and `UseEndpoints`. However, apps can change the order in which `UseRouting` and `UseEndpoints` run by calling these methods explicitly. For example, the following code makes an explicit call to `UseRouting`:

```
app.Use(async (context, next) =>
{
    // ...
    await next(context);
});

app.UseRouting();

app.MapGet("/", () => "Hello World!");
```

In the preceding code:

* The call to `app.Use` registers a custom middleware that runs at the start of the pipeline.
* The call to `UseRouting` configures the route matching middleware to run *after* the custom middleware.
* The endpoint registered with `MapGet` runs at the end of the pipeline.

If the preceding example didn't include a call to `UseRouting`, the custom middleware would run *after* the route matching middleware.

### Endpoints

[]()

The `MapGet` method is used to define an **endpoint**. An endpoint is something that can be:

* Selected, by matching the URL and HTTP method.
* Executed, by running the delegate.

Endpoints that can be matched and executed by the app are configured in `UseEndpoints`. For example, [MapGet](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapget), [MapPost](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mappost), and [similar methods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions) connect request delegates to the routing system. Additional methods can be used to connect ASP.NET Core framework features to the routing system:

* [MapRazorPages for Razor Pages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)
* [MapControllers for controllers](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollers)
* [MapHub\<THub> for SignalR](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubroutebuilder.maphub)
* [MapGrpcService\<TService> for gRPC](../grpc/aspnetcore?view=aspnetcore-10.0)

The following example shows routing with a more sophisticated route template:

```
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");
```

The string `/hello/{name:alpha}` is a **route template**. A route template is used to configure how the endpoint is matched. In this case, the template matches:

* A URL like `/hello/Docs`
* Any URL path that begins with `/hello/` followed by a sequence of alphabetic characters. `:alpha` applies a route constraint that matches only alphabetic characters. [Route constraints](#route-constraints) are explained later in this article.

The second segment of the URL path, `{name:alpha}`:

* Is bound to the `name` parameter.
* Is captured and stored in [HttpRequest.RouteValues](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.routevalues).

The following example shows routing with [health checks](../host-and-deploy/health-checks?view=aspnetcore-10.0) and authorization:

```
app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
```

The preceding example demonstrates how:

* The authorization middleware can be used with routing.
* Endpoints can be used to configure authorization behavior.

The [MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks) call adds a health check endpoint. Chaining [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) on to this call attaches an authorization policy to the endpoint.

Calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) adds the authentication and authorization middleware. These middleware are placed between [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and `UseEndpoints` so that they can:

* See which endpoint was selected by `UseRouting`.
* Apply an authorization policy before [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) dispatches to the endpoint.

[]()

### Endpoint metadata

In the preceding example, there are two endpoints, but only the health check endpoint has an authorization policy attached. If the request matches the health check endpoint, `/healthz`, an authorization check is performed. This demonstrates that endpoints can have extra data attached to them. This extra data is called endpoint **metadata**:

* The metadata can be processed by routing-aware middleware.
* The metadata can be of any .NET type.

## Routing concepts

The routing system builds on top of the middleware pipeline by adding the powerful **endpoint** concept. Endpoints represent units of the app's functionality that are distinct from each other in terms of routing, authorization, and any number of ASP.NET Core's systems.

[]()

### ASP.NET Core endpoint definition

An ASP.NET Core endpoint is:

* Executable: Has a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.requestdelegate#microsoft-aspnetcore-http-endpoint-requestdelegate).
* Extensible: Has a [Metadata](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.metadata) collection.
* Selectable: Optionally, has [routing information](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.routepattern).
* Enumerable: The collection of endpoints can be listed by retrieving the [EndpointDataSource](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource) from [DI](dependency-injection?view=aspnetcore-10.0).

The following code shows how to retrieve and inspect the endpoint matching the current request:

```
app.Use(async (context, next) =>
{
    var currentEndpoint = context.GetEndpoint();

    if (currentEndpoint is null)
    {
        await next(context);
        return;
    }

    Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");

    if (currentEndpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"  - Route Pattern: {routeEndpoint.RoutePattern}");
    }

    foreach (var endpointMetadata in currentEndpoint.Metadata)
    {
        Console.WriteLine($"  - Metadata: {endpointMetadata}");
    }

    await next(context);
});

app.MapGet("/", () => "Inspect Endpoint.");
```

The endpoint, if selected, can be retrieved from the `HttpContext`. Its properties can be inspected. Endpoint objects are immutable and cannot be modified after creation. The most common type of endpoint is a [RouteEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint). `RouteEndpoint` includes information that allows it to be selected by the routing system.

In the preceding code, [app.Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) configures an inline [middleware](middleware/?view=aspnetcore-10.0).

[]()

The following code shows that, depending on where `app.Use` is called in the pipeline, there may not be an endpoint:

```
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});

// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
    Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return "Hello World!";
}).WithDisplayName("Hello");

app.UseEndpoints(_ => { });

// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    await next(context);
});
```

The preceding sample adds `Console.WriteLine` statements that display whether or not an endpoint has been selected. For clarity, the sample assigns a display name to the provided `/` endpoint.

The preceding sample also includes calls to `UseRouting` and `UseEndpoints` to control exactly when these middleware run within the pipeline.

Running this code with a URL of `/` displays:

```
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
```

Running this code with any other URL displays:

```
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
```

This output demonstrates that:

* The endpoint is always null before `UseRouting` is called.
* If a match is found, the endpoint is non-null between `UseRouting` and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).
* The `UseEndpoints` middleware is **terminal** when a match is found. [Terminal middleware](#tm) is defined later in this article.
* The middleware after `UseEndpoints` execute only when no match is found.

The `UseRouting` middleware uses the [SetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.setendpoint) method to attach the endpoint to the current context. It's possible to replace the `UseRouting` middleware with custom logic and still get the benefits of using endpoints. Endpoints are a low-level primitive like middleware, and aren't coupled to the routing implementation. Most apps don't need to replace `UseRouting` with custom logic.

The `UseEndpoints` middleware is designed to be used in tandem with the `UseRouting` middleware. The core logic to execute an endpoint isn't complicated. Use [GetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.getendpoint) to retrieve the endpoint, and then invoke its [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.requestdelegate#microsoft-aspnetcore-http-endpoint-requestdelegate) property.

The following code demonstrates how middleware can influence or react to routing:

```
app.UseHttpMethodOverride();
app.UseRouting();

app.Use(async (context, next) =>
{
    if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
    {
        Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
    }

    await next(context);
});

app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
    .WithMetadata(new RequiresAuditAttribute());
```

```
public class RequiresAuditAttribute : Attribute { }
```

The preceding example demonstrates two important concepts:

* Middleware can run before `UseRouting` to modify the data that routing operates upon.
  * Usually middleware that appears before routing modifies some property of the request, such as [UseRewriter](/en-us/dotnet/api/microsoft.aspnetcore.builder.rewritebuilderextensions.userewriter), [UseHttpMethodOverride](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpmethodoverrideextensions.usehttpmethodoverride), or [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase).

* Middleware can run between `UseRouting` and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) to process the results of routing before the endpoint is executed.

  * Middleware that runs between `UseRouting` and `UseEndpoints`:

    * Usually inspects metadata to understand the endpoints.
    * Often makes security decisions, as done by `UseAuthorization` and `UseCors`.

  * The combination of middleware and metadata allows configuring policies per-endpoint.

The preceding code shows an example of a custom middleware that supports per-endpoint policies. The middleware writes an *audit log* of access to sensitive data to the console. The middleware can be configured to *audit* an endpoint with the `RequiresAuditAttribute` metadata. This sample demonstrates an *opt-in* pattern where only endpoints that are marked as sensitive are audited. It's possible to define this logic in reverse, auditing everything that isn't marked as safe, for example. The endpoint metadata system is flexible. This logic could be designed in whatever way suits the use case.

The preceding sample code is intended to demonstrate the basic concepts of endpoints. **The sample is not intended for production use**. A more complete version of an *audit log* middleware would:

* Log to a file or database.
* Include details such as the user, IP address, name of the sensitive endpoint, and more.

The audit policy metadata `RequiresAuditAttribute` is defined as an `Attribute` for easier use with class-based frameworks such as controllers and SignalR. When using *route to code*:

* Metadata is attached with a builder API.
* Class-based frameworks include all attributes on the corresponding method and class when creating endpoints.

The best practices for metadata types are to define them either as interfaces or attributes. Interfaces and attributes allow code reuse. The metadata system is flexible and doesn't impose any limitations.

[]()

### Compare terminal middleware with routing

The following example demonstrates both terminal middleware and routing:

```
// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/")
    {
        await context.Response.WriteAsync("Terminal Middleware.");
        return;
    }

    await next(context);
});

app.UseRouting();

// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");
```

The style of middleware shown with `Approach 1:` is **terminal middleware**. It's called terminal middleware because it does a matching operation:

* The matching operation in the preceding sample is `Path == "/"` for the middleware and `Path == "/Routing"` for routing.
* When a match is successful, it executes some functionality and returns, rather than invoking the `next` middleware.

It's called terminal middleware because it terminates the search, executes some functionality, and then returns.

The following list compares terminal middleware with routing:

* Both approaches allow terminating the processing pipeline:

  * Middleware terminates the pipeline by returning rather than invoking `next`.
  * Endpoints are always terminal.

* Terminal middleware allows positioning the middleware at an arbitrary place in the pipeline:
  * Endpoints execute at the position of [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).

* Terminal middleware allows arbitrary code to determine when the middleware matches:

  * Custom route matching code can be verbose and difficult to write correctly.
  * Routing provides straightforward solutions for typical apps. Most apps don't require custom route matching code.

* Endpoints interface with middleware such as `UseAuthorization` and `UseCors`.
  * Using a terminal middleware with `UseAuthorization` or `UseCors` requires manual interfacing with the authorization system.

An [endpoint](#endpoints) defines both:

* A delegate to process requests.
* A collection of arbitrary metadata. The metadata is used to implement cross-cutting concerns based on policies and configuration attached to each endpoint.

Terminal middleware can be an effective tool, but can require:

* A significant amount of coding and testing.
* Manual integration with other systems to achieve the desired level of flexibility.

Consider integrating with routing before writing a terminal middleware.

Existing terminal middleware that integrates with [Map](middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline) or [MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen) can usually be turned into a routing aware endpoint. [MapHealthChecks](https://github.com/dotnet/AspNetCore/blob/main/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs#L16) demonstrates the pattern for router-ware:

* Write an extension method on [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder).
* Create a nested middleware pipeline using [CreateApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder.createapplicationbuilder).
* Attach the middleware to the new pipeline. In this case, [UseHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckapplicationbuilderextensions.usehealthchecks).
* [Build](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder.build) the middleware pipeline into a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate).
* Call `Map` and provide the new middleware pipeline.
* Return the builder object provided by `Map` from the extension method.

The following code shows use of [MapHealthChecks](../host-and-deploy/health-checks?view=aspnetcore-10.0):

```
app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecks("/healthz").RequireAuthorization();
```

The preceding sample shows why returning the builder object is important. Returning the builder object allows the app developer to configure policies such as authorization for the endpoint. In this example, the health checks middleware has no direct integration with the authorization system.

The metadata system was created in response to the problems encountered by extensibility authors using terminal middleware. It's problematic for each middleware to implement its own integration with the authorization system.

[]()

### URL matching

* Is the process by which routing matches an incoming request to an [endpoint](#endpoints).
* Is based on data in the URL path and headers.
* Can be extended to consider any data in the request.

When a routing middleware executes, it sets an `Endpoint` and route values to a [request feature](request-features?view=aspnetcore-10.0) on the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) from the current request:

* Calling [HttpContext.GetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.getendpoint) gets the endpoint.
* `HttpRequest.RouteValues` gets the collection of route values.

[Middleware](middleware/?view=aspnetcore-10.0) runs after the routing middleware can inspect the endpoint and take action. For example, an authorization middleware can interrogate the endpoint's metadata collection for an authorization policy. After all of the middleware in the request processing pipeline is executed, the selected endpoint's delegate is invoked.

The routing system in endpoint routing is responsible for all dispatching decisions. Because the middleware applies policies based on the selected endpoint, it's important that:

* Any decision that can affect dispatching or the application of security policies is made inside the routing system.

Warning

For backward-compatibility, when a Controller or Razor Pages endpoint delegate is executed, the properties of [RouteContext.RouteData](/en-us/dotnet/api/microsoft.aspnetcore.routing.routecontext.routedata) are set to appropriate values based on the request processing performed thus far.

The `RouteContext` type will be marked obsolete in a future release:

* Migrate `RouteData.Values` to `HttpRequest.RouteValues`.
* Migrate `RouteData.DataTokens` to retrieve [IDataTokensMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.idatatokensmetadata) from the endpoint metadata.

URL matching operates in a configurable set of phases. In each phase, the output is a set of matches. The set of matches can be narrowed down further by the next phase. The routing implementation does not guarantee a processing order for matching endpoints. **All** possible matches are processed at once. The URL matching phases occur in the following order. ASP.NET Core:

1. Processes the URL path against the set of endpoints and their route templates, collecting **all** of the matches.
2. Takes the preceding list and removes matches that fail with route constraints applied.
3. Takes the preceding list and removes matches that fail the set of [MatcherPolicy](/en-us/dotnet/api/microsoft.aspnetcore.routing.matcherpolicy) instances.
4. Uses the [EndpointSelector](/en-us/dotnet/api/microsoft.aspnetcore.routing.matching.endpointselector) to make a final decision from the preceding list.

The list of endpoints is prioritized according to:

* The [RouteEndpoint.Order](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.order)
* The [route template precedence](#rtp)

All matching endpoints are processed in each phase until the [EndpointSelector](/en-us/dotnet/api/microsoft.aspnetcore.routing.matching.endpointselector) is reached. The `EndpointSelector` is the final phase. It chooses the highest priority endpoint from the matches as the best match. If there are other matches with the same priority as the best match, an ambiguous match exception is thrown.

The route precedence is computed based on a **more specific** route template being given a higher priority. For example, consider the templates `/hello` and `/{message}`:

* Both match the URL path `/hello`.
* `/hello` is more specific and therefore higher priority.

In general, route precedence does a good job of choosing the best match for the kinds of URL schemes used in practice. Use [Order](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.order#microsoft-aspnetcore-routing-routeendpoint-order) only when necessary to avoid an ambiguity.

Due to the kinds of extensibility provided by routing, it isn't possible for the routing system to compute ahead of time the ambiguous routes. Consider an example such as the route templates `/{message:alpha}` and `/{message:int}`:

* The `alpha` constraint matches only alphabetic characters.
* The `int` constraint matches only numbers.
* These templates have the same route precedence, but there's no single URL they both match.
* If the routing system reported an ambiguity error at startup, it would block this valid use case.

Warning

The order of operations inside [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) doesn't influence the behavior of routing, with one exception. [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) and [MapAreaRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcarearoutebuilderextensions.maparearoute) automatically assign an order value to their endpoints based on the order they are invoked. This simulates long-time behavior of controllers without the routing system providing the same guarantees as older routing implementations.

Endpoint routing in ASP.NET Core:

* Doesn't have the concept of routes.
* Doesn't provide ordering guarantees. All endpoints are processed at once.

[]()

### Route template precedence and endpoint selection order

[Route template precedence](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Template/RoutePrecedence.cs#L16) is a system that assigns each route template a value based on how specific it is. Route template precedence:

* Avoids the need to adjust the order of endpoints in common cases.
* Attempts to match the common-sense expectations of routing behavior.

For example, consider templates `/Products/List` and `/Products/{id}`. It would be reasonable to assume that `/Products/List` is a better match than `/Products/{id}` for the URL path `/Products/List`. This works because the literal segment `/List` is considered to have better precedence than the parameter segment `/{id}`.

The details of how precedence works are coupled to how route templates are defined:

* Templates with more segments are considered more specific.
* A segment with literal text is considered more specific than a parameter segment.
* A parameter segment with a constraint is considered more specific than one without.
* A complex segment is considered as specific as a parameter segment with a constraint.
* Catch-all parameters are the least specific. See **catch-all** in the [Route templates](#rtr) section for important information on catch-all routes.

[]()

### URL generation concepts

URL generation:

* Is the process by which routing can create a URL path based on a set of route values.
* Allows for a logical separation between endpoints and the URLs that access them.

Endpoint routing includes the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) API. `LinkGenerator` is a singleton service available from [DI](dependency-injection?view=aspnetcore-10.0). The `LinkGenerator` API can be used outside of the context of an executing request. [Mvc.IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) and scenarios that rely on [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper), such as [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0), HTML Helpers, and [Action Results](../mvc/controllers/actions?view=aspnetcore-10.0), use the `LinkGenerator` API internally to provide link generating capabilities.

The link generator is backed by the concept of an **address** and **address schemes**. An address scheme is a way of determining the endpoints that should be considered for link generation. For example, the route name and route values scenarios many users are familiar with from controllers and Razor Pages are implemented as an address scheme.

The link generator can link to controllers and Razor Pages via the following extension methods:

* [GetPathByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.getpathbyaction)
* [GetUriByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.geturibyaction)
* [GetPathByPage](/en-us/dotnet/api/microsoft.aspnetcore.routing.pagelinkgeneratorextensions.getpathbypage)
* [GetUriByPage](/en-us/dotnet/api/microsoft.aspnetcore.routing.pagelinkgeneratorextensions.geturibypage)

Overloads of these methods accept arguments that include the `HttpContext`. These methods are functionally equivalent to [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.urlhelper.action) and [Url.Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.page), but offer additional flexibility and options.

The `GetPath*` methods are most similar to `Url.Action` and `Url.Page`, in that they generate a URI containing an absolute path. The `GetUri*` methods always generate an absolute URI containing a scheme and host. The methods that accept an `HttpContext` generate a URI in the context of the executing request. The [ambient](#ambient) route values, URL base path, scheme, and host from the executing request are used unless overridden.

[LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) is called with an address. Generating a URI occurs in two steps:

1. An address is bound to a list of endpoints that match the address.
2. Each endpoint's [RoutePattern](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.routepattern#microsoft-aspnetcore-routing-routeendpoint-routepattern) is evaluated until a route pattern that matches the supplied values is found. The resulting output is combined with the other URI parts supplied to the link generator and returned.

The methods provided by [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) support standard link generation capabilities for any type of address. The most convenient way to use the link generator is through extension methods that perform operations for a specific address type:

| Extension Method                                                                                  | Description                                                         |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [GetPathByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.getpathbyaddress) | Generates a URI with an absolute path based on the provided values. |
| [GetUriByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.geturibyaddress)   | Generates an absolute URI based on the provided values.             |

Warning

Pay attention to the following implications of calling [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) methods:

* Use `GetUri*` extension methods with caution in an app configuration that doesn't validate the `Host` header of incoming requests. If the `Host` header of incoming requests isn't validated, untrusted request input can be sent back to the client in URIs in a view or page. We recommend that all production apps configure their server to validate the `Host` header against known valid values.

* Use [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) with caution in middleware in combination with `Map` or `MapWhen`. `Map*` changes the base path of the executing request, which affects the output of link generation. All of the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) APIs allow specifying a base path. Specify an empty base path to undo the `Map*` affect on link generation.

### Middleware example

In the following example, a middleware uses the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) API to create a link to an action method that lists store products. Using the link generator by injecting it into a class and calling `GenerateLink` is available to any class in an app:

```
public class ProductsMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public async Task InvokeAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Plain;

        var productsPath = _linkGenerator.GetPathByAction("Products", "Store");

        await httpContext.Response.WriteAsync(
            $"Go to {productsPath} to see our products.");
    }
}
```

[]()

## Route templates

Tokens within `{}` define route parameters that are bound if the route is matched. More than one route parameter can be defined in a route segment, but route parameters must be separated by a literal value. For example:

`{controller=Home}{action=Index}`

isn't a valid route, because there's no literal value between `{controller}` and `{action}`. Route parameters must have a name and may have additional attributes specified.

Literal text other than route parameters (for example, `{id}`) and the path separator `/` must match the text in the URL. Text matching is case-insensitive and based on the decoded representation of the URL's path. To match a literal route parameter delimiter `{` or `}`, escape the delimiter by repeating the character. For example `{{` or `}}`.

Asterisk `*` or double asterisk `**`:

* Can be used as a prefix to a route parameter to bind to the rest of the URI.

* Are called a **catch-all** parameters. For example, `blog/{**slug}`:

  * Matches any URI that starts with `blog/` and has any value following it.
  * The value following `blog/` is assigned to the [slug](https://developer.mozilla.org/docs/Glossary/Slug) route value.

```
public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
```

Catch-all parameters can also match the empty string.

The catch-all parameter escapes the appropriate characters when the route is used to generate a URL, including path separator `/` characters. For example, the route `foo/{*path}` with route values `{ path = "my/path" }` generates `foo/my%2Fpath`. Note the escaped forward slash. To round-trip path separator characters, use the `**` route parameter prefix. The route `foo/{**path}` with `{ path = "my/path" }` generates `foo/my/path`.

URL patterns that attempt to capture a file name with an optional file extension have additional considerations. For example, consider the template `files/{filename}.{ext?}`. When values for both `filename` and `ext` exist, both values are populated. If only a value for `filename` exists in the URL, the route matches because the trailing `.` is optional. The following URLs match this route:

* `/files/myFile.txt`
* `/files/myFile`

Route parameters may have **default values** designated by specifying the default value after the parameter name separated by an equals sign (`=`). For example, `{controller=Home}` defines `Home` as the default value for `controller`. The default value is used if no value is present in the URL for the parameter. Route parameters are made optional by appending a question mark (`?`) to the end of the parameter name. For example, `id?`. The difference between optional values and default route parameters is:

* A route parameter with a default value always produces a value.
* An optional parameter has a value only when a value is provided by the request URL.

Route parameters may have constraints that must match the route value bound from the URL. Adding `:` and constraint name after the route parameter name specifies an inline constraint on a route parameter. If the constraint requires arguments, they're enclosed in parentheses `(...)` after the constraint name. Multiple *inline constraints* can be specified by appending another `:` and constraint name.

The constraint name and arguments are passed to the [IInlineConstraintResolver](/en-us/dotnet/api/microsoft.aspnetcore.routing.iinlineconstraintresolver) service to create an instance of [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) to use in URL processing. For example, the route template `blog/{article:minlength(10)}` specifies a `minlength` constraint with the argument `10`. For more information on route constraints and a list of the constraints provided by the framework, see the [Route constraints](#route-constraints) section.

Route parameters may also have parameter transformers. Parameter transformers transform a parameter's value when generating links and matching actions and pages to URLs. Like constraints, parameter transformers can be added inline to a route parameter by adding a `:` and transformer name after the route parameter name. For example, the route template `blog/{article:slugify}` specifies a `slugify` transformer. For more information on parameter transformers, see the [Parameter transformers](#parameter-transformers) section.

The following table demonstrates example route templates and their behavior.

| Route Template                           | Example Matching URI    | The request URI…                                                            |
| ---------------------------------------- | ----------------------- | --------------------------------------------------------------------------- |
| `hello`                                  | `/hello`                | Only matches the single path `/hello`.                                      |
| `{Page=Home}`                            | `/`                     | Matches and sets `Page` to `Home`.                                          |
| `{Page=Home}`                            | `/Contact`              | Matches and sets `Page` to `Contact`.                                       |
| `{controller}/{action}/{id?}`            | `/Products/List`        | Maps to the `Products` controller and `List` action.                        |
| `{controller}/{action}/{id?}`            | `/Products/Details/123` | Maps to the `Products` controller and `Details` action with`id` set to 123. |
| `{controller=Home}/{action=Index}/{id?}` | `/`                     | Maps to the `Home` controller and `Index` method. `id` is ignored.          |
| `{controller=Home}/{action=Index}/{id?}` | `/Products`             | Maps to the `Products` controller and `Index` method. `id` is ignored.      |

Using a template is generally the simplest approach to routing. Constraints and defaults can also be specified outside the route template.

### Complex segments

Complex segments are processed by matching up literal delimiters from right to left in a [non-greedy](#greedy) way. For example, `[Route("/a{b}c{d}")]` is a complex segment. Complex segments work in a particular way that must be understood to use them successfully. The example in this section demonstrates why complex segments only really work well when the delimiter text doesn't appear inside the parameter values. Using a [regex](/en-us/dotnet/standard/base-types/regular-expressions) and then manually extracting the values is needed for more complex cases.

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

This is a summary of the steps that routing performs with the template `/a{b}c{d}` and the URL path `/abcd`. The `|` is used to help visualize how the algorithm works:

* The first literal, right to left, is `c`. So `/abcd` is searched from right and finds `/ab|c|d`.
* Everything to the right (`d`) is now matched to the route parameter `{d}`.
* The next literal, right to left, is `a`. So `/ab|c|d` is searched starting where we left off, then `a` is found `/|a|b|c|d`.
* The value to the right (`b`) is now matched to the route parameter `{b}`.
* There is no remaining text and no remaining route template, so this is a match.

Here's an example of a negative case using the same template `/a{b}c{d}` and the URL path `/aabcd`. The `|` is used to help visualize how the algorithm works. This case isn't a match, which is explained by the same algorithm:

* The first literal, right to left, is `c`. So `/aabcd` is searched from right and finds `/aab|c|d`.
* Everything to the right (`d`) is now matched to the route parameter `{d}`.
* The next literal, right to left, is `a`. So `/aab|c|d` is searched starting where we left off, then `a` is found `/a|a|b|c|d`.
* The value to the right (`b`) is now matched to the route parameter `{b}`.
* At this point there is remaining text `a`, but the algorithm has run out of route template to parse, so this is not a match.

Since the matching algorithm is [non-greedy](#greedy):

* It matches the smallest amount of text possible in each step.
* Any case where the delimiter value appears inside the parameter values results in not matching.

Regular expressions provide much more control over their matching behavior.

[]()

Greedy matching, also known as [lazy matching](https://wikipedia.org/wiki/Regular_expression#Lazy_matching), matches the largest possible string. Non-greedy matches the smallest possible string.

## Routing with special characters

Routing with special characters can lead to unexpected results. For example, consider a controller with the following action method:

```
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null || todoItem.Name == null)
    {
        return NotFound();
    }

    return todoItem.Name;
}
```

When `string id` contains the following encoded values, unexpected results might occur:

| ASCII | Encoded |
| ----- | ------- |
| `/`   | `%2F`   |
| ``    | `+`     |

Route parameters are not always URL decoded. This problem may be addressed in the future. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/11544);

## Route constraints

Route constraints execute when a match has occurred to the incoming URL and the URL path is tokenized into route values. Route constraints generally inspect the route value associated via the route template and make a true or false decision about whether the value is acceptable. Some route constraints use data outside the route value to consider whether the request can be routed. For example, the [HttpMethodRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.constraints.httpmethodrouteconstraint) can accept or reject a request based on its HTTP verb. Constraints are used in routing requests and link generation.

Warning

Don't use constraints for input validation. If constraints are used for input validation, invalid input results in a `404` Not Found response. Invalid input should produce a `400` Bad Request with an appropriate error message. Route constraints are used to disambiguate similar routes, not to validate the inputs for a particular route.

The following table demonstrates example route constraints and their expected behavior.

| constraint          | Example                                     | Example Matches                        | Notes                                                                                     |
| ------------------- | ------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- |
| `int`               | `{id:int}`                                  | `123456789`, `-123456789`              | Matches any integer                                                                       |
| `bool`              | `{active:bool}`                             | `true`, `FALSE`                        | Matches `true` or `false`. Case-insensitive                                               |
| `datetime`          | `{dob:datetime}`                            | `2016-12-31`, `2016-12-31 7:32pm`      | Matches a valid `DateTime` value in the invariant culture. See preceding warning.         |
| `decimal`           | `{price:decimal}`                           | `49.99`, `-1,000.01`                   | Matches a valid `decimal` value in the invariant culture. See preceding warning.          |
| `double`            | `{weight:double}`                           | `1.234`, `-1,001.01e8`                 | Matches a valid `double` value in the invariant culture. See preceding warning.           |
| `float`             | `{weight:float}`                            | `1.234`, `-1,001.01e8`                 | Matches a valid `float` value in the invariant culture. See preceding warning.            |
| `guid`              | `{id:guid}`                                 | `CD2C1638-1638-72D5-1638-DEADBEEF1638` | Matches a valid `Guid` value                                                              |
| `long`              | `{ticks:long}`                              | `123456789`, `-123456789`              | Matches a valid `long` value                                                              |
| `minlength(value)`  | `{username:minlength(4)}`                   | `Rick`                                 | String must be at least 4 characters                                                      |
| `maxlength(value)`  | `{filename:maxlength(8)}`                   | `MyFile`                               | String must be no more than 8 characters                                                  |
| `length(length)`    | `{filename:length(12)}`                     | `somefile.txt`                         | String must be exactly 12 characters long                                                 |
| `length(min,max)`   | `{filename:length(8,16)}`                   | `somefile.txt`                         | String must be at least 8 and no more than 16 characters long                             |
| `min(value)`        | `{age:min(18)}`                             | `19`                                   | Integer value must be at least 18                                                         |
| `max(value)`        | `{age:max(120)}`                            | `91`                                   | Integer value must be no more than 120                                                    |
| `range(min,max)`    | `{age:range(18,120)}`                       | `91`                                   | Integer value must be at least 18 but no more than 120                                    |
| `alpha`             | `{name:alpha}`                              | `Rick`                                 | String must consist of one or more alphabetical characters, `a`-`z` and case-insensitive. |
| `regex(expression)` | `{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}` | `123-45-6789`                          | String must match the regular expression. See tips about defining a regular expression.   |
| `required`          | `{name:required}`                           | `Rick`                                 | Used to enforce that a non-parameter value is present during URL generation               |

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

Multiple, colon delimited constraints can be applied to a single parameter. For example, the following constraint restricts a parameter to an integer value of 1 or greater:

```
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
```

Warning

Route constraints that verify the URL and are converted to a CLR type always use the invariant culture. For example, conversion to the CLR type `int` or `DateTime`. These constraints assume that the URL is not localizable. The framework-provided route constraints don't modify the values stored in route values. All route values parsed from the URL are stored as strings. For example, the `float` constraint attempts to convert the route value to a float, but the converted value is used only to verify it can be converted to a float.

### Regular expressions in constraints

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

Regular expressions can be specified as inline constraints using the `regex(...)` route constraint. Methods in the [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) family also accept an object literal of constraints. If that form is used, string values are interpreted as regular expressions.

The following code uses an inline regex constraint:

```
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
    () => "Inline Regex Constraint Matched");
```

The following code uses an object literal to specify a regex constraint:

```
app.MapControllerRoute(
    name: "people",
    pattern: "people/{ssn}",
    constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
    defaults: new { controller = "People", action = "List" });
```

The ASP.NET Core framework adds `RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant` to the regular expression constructor. See [RegexOptions](/en-us/dotnet/api/system.text.regularexpressions.regexoptions) for a description of these members.

Regular expressions use delimiters and tokens similar to those used by routing and the C# language. Regular expression tokens must be escaped. To use the regular expression `^\d{3}-\d{2}-\d{4}$` in an inline constraint, use one of the following:

* Replace `\` characters provided in the string as `\\` characters in the C# source file in order to escape the `\` string escape character.
* [Verbatim string literals](/en-us/dotnet/csharp/language-reference/keywords/string).

To escape routing parameter delimiter characters `{`, `}`, `[`, `]`, double the characters in the expression, for example, `{{`, `}}`, `[[`, `]]`. The following table shows a regular expression and its escaped version.

| Regular expression    | Escaped regular expression     |
| --------------------- | ------------------------------ |
| `^\d{3}-\d{2}-\d{4}$` | `^\\d{{3}}-\\d{{2}}-\\d{{4}}$` |
| `^[a-z]{2}$`          | `^[[a-z]]{{2}}$`               |

Regular expressions used in routing often start with the `^` character and match the starting position of the string. The expressions often end with the `$` character and match the end of the string. The `^` and `$` characters ensure that the regular expression matches the entire route parameter value. Without the `^` and `$` characters, the regular expression matches any substring within the string, which is often undesirable. The following table provides examples and explains why they match or fail to match.

| Expression   | String    | Match | Comment               |
| ------------ | --------- | ----- | --------------------- |
| `[a-z]{2}`   | hello     | Yes   | Substring matches     |
| `[a-z]{2}`   | 123abc456 | Yes   | Substring matches     |
| `[a-z]{2}`   | mz        | Yes   | Matches expression    |
| `[a-z]{2}`   | MZ        | Yes   | Not case sensitive    |
| `^[a-z]{2}$` | hello     | No    | See `^` and `$` above |
| `^[a-z]{2}$` | 123abc456 | No    | See `^` and `$` above |

For more information on regular expression syntax, see [.NET Framework Regular Expressions](/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference).

To constrain a parameter to a known set of possible values, use a regular expression. For example, `{action:regex(^(list|get|create)$)}` only matches the `action` route value to `list`, `get`, or `create`. If passed into the constraints dictionary, the string `^(list|get|create)$` is equivalent. Constraints that are passed in the constraints dictionary that don't match one of the known constraints are also treated as regular expressions. Constraints that are passed within a template that don't match one of the known constraints are not treated as regular expressions.

### Custom route constraints

Custom route constraints can be created by implementing the [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) interface. The `IRouteConstraint` interface contains [Match](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint.match), which returns `true` if the constraint is satisfied and `false` otherwise.

Custom route constraints are rarely needed. Before implementing a custom route constraint, consider alternatives, such as model binding.

The ASP.NET Core [Constraints](https://github.com/dotnet/aspnetcore/tree/main/src/Http/Routing/src/Constraints) folder provides good examples of creating constraints. For example, [GuidRouteConstraint](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs#L18).

To use a custom `IRouteConstraint`, the route constraint type must be registered with the app's [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap) in the service container. A `ConstraintMap` is a dictionary that maps route constraint keys to `IRouteConstraint` implementations that validate those constraints. An app's `ConstraintMap` can be updated in `Program.cs` either as part of an [AddRouting](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.routingservicecollectionextensions.addrouting) call or by configuring [RouteOptions](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions) directly with `builder.Services.Configure<RouteOptions>`. For example:

```
builder.Services.AddRouting(options =>
    options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));
```

The preceding constraint is applied in the following code:

```
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
    [HttpGet("{id:noZeroes}")]
    public IActionResult Get(string id) =>
        Content(id);
}
```

The implementation of `NoZeroesRouteConstraint` prevents `0` being used in a route parameter:

```
public class NoZeroesRouteConstraint : IRouteConstraint
{
    private static readonly Regex _regex = new(
        @"^[1-9]*$",
        RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
        TimeSpan.FromMilliseconds(100));

    public bool Match(
        HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);

        if (routeValueString is null)
        {
            return false;
        }

        return _regex.IsMatch(routeValueString);
    }
}
```

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

The preceding code:

* Prevents `0` in the `{id}` segment of the route.
* Is shown to provide a basic example of implementing a custom constraint. It should not be used in a production app.

The following code is a better approach to preventing an `id` containing a `0` from being processed:

```
[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return Content(id);
}
```

The preceding code has the following advantages over the `NoZeroesRouteConstraint` approach:

* It doesn't require a custom constraint.
* It returns a more descriptive error when the route parameter includes `0`.

## Parameter transformers

Parameter transformers:

* Execute when generating a link using [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator).
* Implement [Microsoft.AspNetCore.Routing.IOutboundParameterTransformer](/en-us/dotnet/api/microsoft.aspnetcore.routing.ioutboundparametertransformer).
* Are configured using [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap).
* Take the parameter's route value and transform it to a new string value.
* Result in using the transformed value in the generated link.

For example, a custom `slugify` parameter transformer in route pattern `blog\{article:slugify}` with `Url.Action(new { article = "MyTestArticle" })` generates `blog\my-test-article`.

Consider the following `IOutboundParameterTransformer` implementation:

```
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value is null)
        {
            return null;
        }

        return Regex.Replace(
            value.ToString()!,
                "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100))
            .ToLowerInvariant();
    }
}
```

To use a parameter transformer in a route pattern, configure it using [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap) in `Program.cs`:

```
builder.Services.AddRouting(options =>
    options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
```

The ASP.NET Core framework uses parameter transformers to transform the URI where an endpoint resolves. For example, parameter transformers transform the route values used to match an `area`, `controller`, `action`, and `page`:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
```

With the preceding route template, the action `SubscriptionManagementController.GetAll` is matched with the URI `/subscription-management/get-all`. A parameter transformer doesn't change the route values used to generate a link. For example, `Url.Action("GetAll", "SubscriptionManagement")` outputs `/subscription-management/get-all`.

ASP.NET Core provides API conventions for using parameter transformers with generated routes:

* The [Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.routetokentransformerconvention) MVC convention applies a specified parameter transformer to all attribute routes in the app. The parameter transformer transforms attribute route tokens as they are replaced. For more information, see [Use a parameter transformer to customize token replacement](../mvc/controllers/routing?view=aspnetcore-10.0#use-a-parameter-transformer-to-customize-token-replacement).
* Razor Pages uses the [PageRouteTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutetransformerconvention) API convention. This convention applies a specified parameter transformer to all automatically discovered Razor Pages. The parameter transformer transforms the folder and file name segments of Razor Pages routes. For more information, see [Use a parameter transformer to customize page routes](../razor-pages/razor-pages-conventions?view=aspnetcore-10.0#use-a-parameter-transformer-to-customize-page-routes).

[]()

## URL generation reference

This section contains a reference for the algorithm implemented by URL generation. In practice, most complex examples of URL generation use controllers or Razor Pages. See [routing in controllers](../mvc/controllers/routing?view=aspnetcore-10.0) for additional information.

The URL generation process begins with a call to [LinkGenerator.GetPathByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.getpathbyaddress) or a similar method. The method is provided with an address, a set of route values, and optionally information about the current request from `HttpContext`.

The first step is to use the address to resolve a set of candidate endpoints using an [IEndpointAddressScheme\<TAddress>](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointaddressscheme-1) that matches the address's type.

Once the set of candidates is found by the address scheme, the endpoints are ordered and processed iteratively until a URL generation operation succeeds. URL generation does **not** check for ambiguities, the first result returned is the final result.

### Troubleshooting URL generation with logging

The first step in troubleshooting URL generation is setting the logging level of `Microsoft.AspNetCore.Routing` to `TRACE`. `LinkGenerator` logs many details about its processing which can be useful to troubleshoot problems.

See [URL generation reference](#ugr) for details on URL generation.

### Addresses

Addresses are the concept in URL generation used to bind a call into the link generator to a set of candidate endpoints.

Addresses are an extensible concept that come with two implementations by default:

* Using *endpoint name* (`string`) as the address:

  * Provides similar functionality to MVC's route name.
  * Uses the [IEndpointNameMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointnamemetadata) metadata type.
  * Resolves the provided string against the metadata of all registered endpoints.
  * Throws an exception on startup if multiple endpoints use the same name.
  * Recommended for general-purpose use outside of controllers and Razor Pages.

* Using *route values* ([RouteValuesAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.routevaluesaddress)) as the address:

  * Provides similar functionality to controllers and Razor Pages legacy URL generation.
  * Very complex to extend and debug.
  * Provides the implementation used by `IUrlHelper`, Tag Helpers, HTML Helpers, Action Results, etc.

The role of the address scheme is to make the association between the address and matching endpoints by arbitrary criteria:

* The endpoint name scheme performs a basic dictionary lookup.
* The route values scheme has a complex best subset of set algorithm.

[]()

### Ambient values and explicit values

From the current request, routing accesses the route values of the current request `HttpContext.Request.RouteValues`. The values associated with the current request are referred to as the **ambient values**. For the purpose of clarity, the documentation refers to the route values passed in to methods as **explicit values**.

The following example shows ambient values and explicit values. It provides ambient values from the current request and explicit values:

```
public class WidgetController : ControllerBase
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator) =>
        _linkGenerator = linkGenerator;

    public IActionResult Index()
    {
        var indexPath = _linkGenerator.GetPathByAction(
            HttpContext, values: new { id = 17 })!;

        return Content(indexPath);
    }

    // ...
```

The preceding code:

* Returns `/Widget/Index/17`
* Gets [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) via [DI](dependency-injection?view=aspnetcore-10.0).

The following code provides only explicit values and no ambient values:

```
var subscribePath = _linkGenerator.GetPathByAction(
    "Subscribe", "Home", new { id = 17 })!;
```

The preceding method returns `/Home/Subscribe/17`

The following code in the `WidgetController` returns `/Widget/Subscribe/17`:

```
var subscribePath = _linkGenerator.GetPathByAction(
    HttpContext, "Subscribe", null, new { id = 17 });
```

The following code provides the controller from ambient values in the current request and explicit values:

```
public class GadgetController : ControllerBase
{
    public IActionResult Index() =>
        Content(Url.Action("Edit", new { id = 17 })!);
}
```

In the preceding code:

* `/Gadget/Edit/17` is returned.
* [Url](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.url#microsoft-aspnetcore-mvc-controllerbase-url) gets the [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper).
* [Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.action) generates a URL with an absolute path for an action method. The URL contains the specified `action` name and `route` values.

The following code provides ambient values from the current request and explicit values:

```
public class IndexModel : PageModel
{
    public void OnGet()
    {
        var editUrl = Url.Page("./Edit", new { id = 17 });

        // ...
    }
}
```

The preceding code sets `url` to `/Edit/17` when the Edit Razor Page contains the following page directive:

`@page "{id:int}"`

If the Edit page doesn't contain the `"{id:int}"` route template, `url` is `/Edit?id=17`.

The behavior of MVC's [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) adds a layer of complexity in addition to the rules described here:

* `IUrlHelper` always provides the route values from the current request as ambient values.
* [IUrlHelper.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.action) always copies the current `action` and `controller` route values as explicit values unless overridden by the developer.
* [IUrlHelper.Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.page) always copies the current `page` route value as an explicit value unless overridden.
* `IUrlHelper.Page` always overrides the current `handler` route value with `null` as an explicit values unless overridden.

Users are often surprised by the behavioral details of ambient values, because MVC doesn't seem to follow its own rules. For historical and compatibility reasons, certain route values such as `action`, `controller`, `page`, and `handler` have their own special-case behavior.

The equivalent functionality provided by `LinkGenerator.GetPathByAction` and `LinkGenerator.GetPathByPage` duplicates these anomalies of `IUrlHelper` for compatibility.

### URL generation process

Once the set of candidate endpoints are found, the URL generation algorithm:

* Processes the endpoints iteratively.
* Returns the first successful result.

The first step in this process is called **route value invalidation**. Route value invalidation is the process by which routing decides which route values from the ambient values should be used and which should be ignored. Each ambient value is considered and either combined with the explicit values, or ignored.

The best way to think about the role of ambient values is that they attempt to save application developers typing, in some common cases. Traditionally, the scenarios where ambient values are helpful are related to MVC:

* When linking to another action in the same controller, the controller name doesn't need to be specified.
* When linking to another controller in the same area, the area name doesn't need to be specified.
* When linking to the same action method, route values don't need to be specified.
* When linking to another part of the app, you don't want to carry over route values that have no meaning in that part of the app.

Calls to `LinkGenerator` or `IUrlHelper` that return `null` are usually caused by not understanding route value invalidation. Troubleshoot route value invalidation by explicitly specifying more of the route values to see if that solves the problem.

Route value invalidation works on the assumption that the app's URL scheme is hierarchical, with a hierarchy formed from left-to-right. Consider the basic controller route template `{controller}/{action}/{id?}` to get an intuitive sense of how this works in practice. A **change** to a value **invalidates** all of the route values that appear to the right. This reflects the assumption about hierarchy. If the app has an ambient value for `id`, and the operation specifies a different value for the `controller`:

* `id` won't be reused because `{controller}` is to the left of `{id?}`.

Some examples demonstrating this principle:

* If the explicit values contain a value for `id`, the ambient value for `id` is ignored. The ambient values for `controller` and `action` can be used.
* If the explicit values contain a value for `action`, any ambient value for `action` is ignored. The ambient values for `controller` can be used. If the explicit value for `action` is different from the ambient value for `action`, the `id` value won't be used. If the explicit value for `action` is the same as the ambient value for `action`, the `id` value can be used.
* If the explicit values contain a value for `controller`, any ambient value for `controller` is ignored. If the explicit value for `controller` is different from the ambient value for `controller`, the `action` and `id` values won't be used. If the explicit value for `controller` is the same as the ambient value for `controller`, the `action` and `id` values can be used.

This process is further complicated by the existence of attribute routes and dedicated conventional routes. Controller conventional routes such as `{controller}/{action}/{id?}` specify a hierarchy using route parameters. For [dedicated conventional routes](../mvc/controllers/routing?view=aspnetcore-10.0#dcr) and [attribute routes](../mvc/controllers/routing?view=aspnetcore-10.0#ar) to controllers and Razor Pages:

* There is a hierarchy of route values.
* They don't appear in the template.

For these cases, URL generation defines the **required values** concept. Endpoints created by controllers and Razor Pages have required values specified that allow route value invalidation to work.

The route value invalidation algorithm in detail:

* The required value names are combined with the route parameters, then processed from left-to-right.

* For each parameter, the ambient value and explicit value are compared:

  * If the ambient value and explicit value are the same, the process continues.
  * If the ambient value is present and the explicit value isn't, the ambient value is used when generating the URL.
  * If the ambient value isn't present and the explicit value is, reject the ambient value and all subsequent ambient values.
  * If the ambient value and the explicit value are present, and the two values are different, reject the ambient value and all subsequent ambient values.

At this point, the URL generation operation is ready to evaluate route constraints. The set of accepted values is combined with the parameter default values, which is provided to constraints. If the constraints all pass, the operation continues.

Next, the **accepted values** can be used to expand the route template. The route template is processed:

* From left-to-right.

* Each parameter has its accepted value substituted.

* With the following special cases:

  * If the accepted values is missing a value and the parameter has a default value, the default value is used.
  * If the accepted values is missing a value and the parameter is optional, processing continues.
  * If any route parameter to the right of a missing optional parameter has a value, the operation fails.
  * Contiguous default-valued parameters and optional parameters are collapsed where possible.

Note

For endpoints created by controllers and Razor Pages, `controller`, `action`, `page`, and `area` are part of the endpoint's **required values**, so they're always present in the accepted values. As a result:

* They aren't invalidated by a change to a route value that appears to their left, such as a `culture` parameter.
* Inline default values in the route template, such as `{controller=DefaultController}`, aren't applied, because default values are used only when the accepted values are *missing* a value.

For example, with the template `{culture}/api1/{controller=DefaultController}/{action=DefaultAction}/{id?}` and a current request of `/en/api1/MyController/Index/111`, the call `Url.RouteUrl("ApiRoute1", new { culture = "ar" })` generates `/ar/api1/MyController/Index`, not `/ar/api1/DefaultController/DefaultAction`. The `id` value isn't used because it has no required value and is to the right of a changed value. To use the template's default values, specify them explicitly:

```
Url.RouteUrl("ApiRoute1", new { culture = "ar", controller = "DefaultController", action = "DefaultAction" })
```

For a related limitation, see [Problems with route value invalidation](#problems-with-route-value-invalidation).

Values explicitly provided that don't match a segment of the route are added to the query string. The following table shows the result when using the route template `{controller}/{action}/{id?}`.

| Ambient Values                     | Explicit Values                        | Result                  |
| ---------------------------------- | -------------------------------------- | ----------------------- |
| controller = "Home"                | action = "About"                       | `/Home/About`           |
| controller = "Home"                | controller = "Order", action = "About" | `/Order/About`          |
| controller = "Home", color = "Red" | action = "About"                       | `/Home/About`           |
| controller = "Home"                | action = "About", color = "Red"        | `/Home/About?color=Red` |

### Problems with route value invalidation

For related information about how required values, such as `controller` and `action`, affect route value invalidation and template default values, see the note in [URL generation process](#url-generation-process).

The following code shows an example of a URL generation scheme that's not supported by routing:

```
app.MapControllerRoute(
    "default",
    "{culture}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    "blog",
    "{culture}/{**slug}",
    new { controller = "Blog", action = "ReadPost" });
```

In the preceding code, the `culture` route parameter is used for localization. The desire is to have the `culture` parameter always accepted as an ambient value. However, the `culture` parameter is not accepted as an ambient value because of the way required values work:

* In the `"default"` route template, the `culture` route parameter is to the left of `controller`, so changes to `controller` won't invalidate `culture`.
* In the `"blog"` route template, the `culture` route parameter is considered to be to the right of `controller`, which appears in the required values.

## Parse URL paths with `LinkParser`

The [LinkParser](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkparser) class adds support for parsing a URL path into a set of route values. The [ParsePathByEndpointName](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkparserendpointnameaddressextensions.parsepathbyendpointname) method takes an endpoint name and a URL path, and returns a set of route values extracted from the URL path.

In the following example controller, the `GetProduct` action uses a route template of `api/Products/{id}` and has a [Name](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute.name) of `GetProduct`:

```
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id}", Name = nameof(GetProduct))]
    public IActionResult GetProduct(string id)
    {
        // ...
```

In the same controller class, the `AddRelatedProduct` action expects a URL path, `pathToRelatedProduct`, which can be provided as a query-string parameter:

```
[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
    string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
    var routeValues = linkParser.ParsePathByEndpointName(
        nameof(GetProduct), pathToRelatedProduct);
    var relatedProductId = routeValues?["id"];

    // ...
```

In the preceding example, the `AddRelatedProduct` action extracts the `id` route value from the URL path. For example, with a URL path of `/api/Products/1`, the `relatedProductId` value is set to `1`. This approach allows the API's clients to use URL paths when referring to resources, without requiring knowledge of how such a URL is structured.

## Configure endpoint metadata

The following links provide information on how to configure endpoint metadata:

* [Enable Cors with endpoint routing](../security/cors?view=aspnetcore-10.0#enable-cors-with-endpoint-routing)
* [IAuthorizationPolicyProvider sample](https://github.com/dotnet/AspNetCore/tree/release/3.1/src/Security/samples/CustomPolicyProvider) using a custom `[MinimumAgeAuthorize]` attribute
* [Test authentication with the \[Authorize\] attribute](../security/authentication/identity?view=aspnetcore-10.0#test-identity)
* [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization)
* [Select a scheme with an `[Authorize]` attribute](../security/authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0#select-a-scheme-with-an-authorize-attribute)
* [Apply policies using an `[Authorize]` attribute](../security/authorization/policies?view=aspnetcore-10.0#apply-policies-to-mvc-controllers)
* [Role-based authorization in ASP.NET Core](../security/authorization/roles?view=aspnetcore-10.0)

[]()

## Host matching in routes with RequireHost

[RequireHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.requirehost) applies a constraint to the route which requires the specified host. The `RequireHost` or [\[Host\]](/en-us/dotnet/api/microsoft.aspnetcore.routing.hostattribute) parameter can be a:

* Host: `www.domain.com`, matches `www.domain.com` with any port.
* Host with wildcard: `*.domain.com`, matches `www.domain.com`, `subdomain.domain.com`, or `www.subdomain.domain.com` on any port.
* Port: `*:5000`, matches port 5000 with any host.
* Host and port: `www.domain.com:5000` or `*.domain.com:5000`, matches host and port.

Multiple parameters can be specified using `RequireHost` or `[Host]`. The constraint matches hosts valid for any of the parameters. For example, `[Host("domain.com", "*.domain.com")]` matches `domain.com`, `www.domain.com`, and `subdomain.domain.com`.

The following code uses `RequireHost` to require the specified host on the route:

```
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");

app.MapHealthChecks("/healthz").RequireHost("*:8080");
```

The following code uses the `[Host]` attribute on the controller to require any of the specified hosts:

```
[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
    public IActionResult Index() =>
        View();

    [Host("example.com")]
    public IActionResult Example() =>
        View();
}
```

When the `[Host]` attribute is applied to both the controller and action method:

* The attribute on the action is used.
* The controller attribute is ignored.

## Performance guidance for routing

When an app has performance problems, routing is often suspected as the problem. The reason routing is suspected is that frameworks like controllers and Razor Pages report the amount of time spent inside the framework in their logging messages. When there's a significant difference between the time reported by controllers and the total time of the request:

* Developers eliminate their app code as the source of the problem.
* It's common to assume routing is the cause.

Routing is performance tested using thousands of endpoints. It's unlikely that a typical app will encounter a performance problem just by being too large. The most common root cause of slow routing performance is usually a badly-behaving custom middleware.

This following code sample demonstrates a basic technique for narrowing down the source of delay:

```
var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseRouting();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();

    logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});

app.MapGet("/", () => "Timing Test.");
```

To time routing:

* Interleave each middleware with a copy of the timing middleware shown in the preceding code.
* Add a unique identifier to correlate the timing data with the code.

This is a basic way to narrow down the delay when it's significant, for example, more than `10ms`. Subtracting `Time 2` from `Time 1` reports the time spent inside the `UseRouting` middleware.

The following code uses a more compact approach to the preceding timing code:

```
public sealed class AutoStopwatch : IDisposable
{
    private readonly ILogger _logger;
    private readonly string _message;
    private readonly Stopwatch _stopwatch;
    private bool _disposed;

    public AutoStopwatch(ILogger logger, string message) =>
        (_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
            _message, _stopwatch.ElapsedMilliseconds);

        _disposed = true;
    }
}
```

```
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseRouting();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.UseAuthorization();

app.Use(async (context, next) =>
{
    using (new AutoStopwatch(logger, $"Time {++timerCount}"))
    {
        await next(context);
    }
});

app.MapGet("/", () => "Timing Test.");
```

### Potentially expensive routing features

The following list provides some insight into routing features that are relatively expensive compared with basic route templates:

* Regular expressions: It's possible to write regular expressions that are complex, or have long running time with a small amount of input.

* Complex segments (`{x}-{y}-{z}`):

  * Are significantly more expensive than parsing a regular URL path segment.
  * Result in many more substrings being allocated.

* Synchronous data access: Many complex apps have database access as part of their routing. Use extensibility points such as [MatcherPolicy](/en-us/dotnet/api/microsoft.aspnetcore.routing.matcherpolicy) and [EndpointSelectorContext](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointselectorcontext), which are asynchronous.

### Guidance for large route tables

By default ASP.NET Core uses a routing algorithm that trades memory for CPU time. This has the nice effect that route matching time is dependent only on the length of the path to match and not the number of routes. However, this approach can be potentially problematic in some cases, when the app has a large number of routes (in the thousands) and there is a high amount of variable prefixes in the routes. For example, if the routes have parameters in early segments of the route, like `{parameter}/some/literal`.

It is unlikely for an app to run into a situation where this is a problem unless:

* There are a high number of routes in the app using this pattern.
* There is a large number of routes in the app.

#### How to determine if an app is running into the large route table problem

* There are two symptoms to look for:

  * The app is slow to start on the first request.
    * Note that this is required but not sufficient. There are many other non-route problems than can cause slow app startup. Check for the condition below to accurately determine the app is running into this situation.
  * The app consumes a lot of memory during startup and a memory dump shows a large number of `Microsoft.AspNetCore.Routing.Matching.DfaNode` instances.

#### How to address this issue

There are several techniques and optimizations can be applied to routes that will largely improve this scenario:

* Apply route constraints to your parameters, for example `{parameter:int}`, `{parameter:guid}`, `{parameter:regex(\\d+)}`, etc. where possible.

  * This allows the routing algorithm to internally optimize the structures used for matching and drastically reduce the memory used.
  * In the vast majority of cases this will suffice to get back to an acceptable behavior.

* Change the routes to move parameters to later segments in the template.
  * This reduces the number of possible "paths" to match an endpoint given a path.

* Use a dynamic route and perform the mapping to a controller/page dynamically.
  * This can be achieved using `MapDynamicControllerRoute` and `MapDynamicPageRoute`.

## Guidance for library authors

This section contains guidance for library authors building on top of routing. These details are intended to ensure that app developers have a good experience using libraries and frameworks that extend routing.

### Define endpoints

To create a framework that uses routing for URL matching, start by defining a user experience that builds on top of [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).

**DO** build on top of [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder). This allows users to compose your framework with other ASP.NET Core features without confusion. Every ASP.NET Core template includes routing. Assume routing is present and familiar for users.

```
// Your framework
app.MapMyFramework(...);

app.MapHealthChecks("/healthz");
```

**DO** return a sealed concrete type from a call to `MapMyFramework(...)` that implements [IEndpointConventionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iendpointconventionbuilder). Most framework `Map...` methods follow this pattern. The `IEndpointConventionBuilder` interface:

* Allows for metadata to be composed.
* Is targeted by a variety of extension methods.

Declaring your own type allows you to add your own framework-specific functionality to the builder. It's ok to wrap a framework-declared builder and forward calls to it.

```
// Your framework
app.MapMyFramework(...)
    .RequireAuthorization()
    .WithMyFrameworkFeature(awesome: true);

app.MapHealthChecks("/healthz");
```

**CONSIDER** writing your own [EndpointDataSource](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource). `EndpointDataSource` is the low-level primitive for declaring and updating a collection of endpoints. `EndpointDataSource` is a powerful API used by controllers and Razor Pages.

The routing tests have a [basic example](https://github.com/dotnet/AspNetCore/blob/main/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs#L17) of a non-updating data source.

**DO NOT** attempt to register an `EndpointDataSource` by default. Require users to register your framework in [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints). The philosophy of routing is that nothing is included by default, and that `UseEndpoints` is the place to register endpoints.

### Creating routing-integrated middleware

**CONSIDER** defining metadata types as an interface.

**DO** make it possible to use metadata types as an attribute on classes and methods.

```
public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}
```

Frameworks like controllers and Razor Pages support applying metadata attributes to types and methods. If you declare metadata types:

* Make them accessible as [attributes](/en-us/dotnet/csharp/programming-guide/concepts/attributes/).
* Most users are familiar with applying attributes.

Declaring a metadata type as an interface adds another layer of flexibility:

* Interfaces are composable.
* Developers can declare their own types that combine multiple policies.

**DO** make it possible to override metadata, as shown in the following example:

```
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}
```

The best way to follow these guidelines is to avoid defining **marker metadata**:

* Don't just look for the presence of a metadata type.
* Define a property on the metadata and check the property.

The metadata collection is ordered and supports overriding by priority. In the case of controllers, metadata on the action method is most specific.

**DO** make middleware useful with and without routing:

```
app.UseAuthorization(new AuthorizationPolicy() { ... });

// Your framework
app.MapMyFramework(...).RequireAuthorization();
```

As an example of this guideline, consider the authorization middleware. A configured fallback policy applies when the middleware can't produce a policy from authorization metadata, including:

* Endpoints without authorization metadata that produces a policy.
* Requests that don't match an endpoint.

This behavior makes the authorization middleware useful outside of routing and for traditional middleware programming. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

## Debug diagnostics

For detailed routing diagnostic output, set `Logging:LogLevel:Microsoft` to `Debug`. In the `Development` environment, set the log level in `appsettings.Development.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
```

Routing is responsible for matching incoming HTTP requests and dispatching those requests to the app's executable endpoints. [Endpoints](#endpoint) are the app's units of executable request-handling code. Endpoints are defined in the app and configured when the app starts. The endpoint matching process can extract values from the request's URL and provide those values for request processing. Using endpoint information from the app, routing is also able to generate URLs that map to endpoints.

Apps can configure routing using:

* Controllers
* Razor Pages
* SignalR
* gRPC Services
* Endpoint-enabled [middleware](middleware/?view=aspnetcore-10.0) such as [Health Checks](../host-and-deploy/health-checks?view=aspnetcore-10.0).
* Delegates and lambdas registered with routing.

This document covers low-level details of ASP.NET Core routing. For information on configuring routing:

* For controllers, see [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0).
* For Razor Pages conventions, see [Razor Pages route and app conventions in ASP.NET Core](../razor-pages/razor-pages-conventions?view=aspnetcore-10.0).

The endpoint routing system described in this document applies to ASP.NET Core 3.0 or later. For information on the previous routing system based on [IRouter](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouter), select the ASP.NET Core 2.1 version using one of the following approaches:

* The version selector for a previous version.
* Select [ASP.NET Core 2.1 routing](?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-2.1).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/routing/samples/3.x/RoutingSample) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

The download samples for this document are enabled by a specific `Startup` class. To run a specific sample, modify `Program.cs` to call the desired `Startup` class.

## Routing basics

All ASP.NET Core templates include routing in the generated code. Routing is registered in the [middleware](middleware/?view=aspnetcore-10.0) pipeline in `Startup.Configure`.

The following code shows a basic example of routing:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}
```

Routing uses a pair of middleware, registered by [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints):

* `UseRouting` adds route matching to the middleware pipeline. This middleware looks at the set of endpoints defined in the app, and selects the [best match](#urlm) based on the request.
* `UseEndpoints` adds endpoint execution to the middleware pipeline. It runs the delegate associated with the selected endpoint.

The preceding example includes a single *route to code* endpoint using the [MapGet](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapget) method:

* When an HTTP `GET` request is sent to the root URL `/`:

  * The request delegate shown executes.
  * `Hello World!` is written to the HTTP response. By default, the root URL `/` is `https://localhost:5001/`.

* If the request method is not `GET` or the root URL is not `/`, no route matches and an HTTP 404 is returned.

### Endpoint

[]()

The `MapGet` method is used to define an **endpoint**. An endpoint is something that can be:

* Selected, by matching the URL and HTTP method.
* Executed, by running the delegate.

Endpoints that can be matched and executed by the app are configured in `UseEndpoints`. For example, [MapGet](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapget), [MapPost](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mappost), and [similar methods](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions) connect request delegates to the routing system. Additional methods can be used to connect ASP.NET Core framework features to the routing system:

* [MapRazorPages for Razor Pages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)
* [MapControllers for controllers](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollers)
* [MapHub\<THub> for SignalR](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubroutebuilder.maphub)
* [MapGrpcService\<TService> for gRPC](../grpc/aspnetcore?view=aspnetcore-10.0)

The following example shows routing with a more sophisticated route template:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/hello/{name:alpha}", async context =>
    {
        var name = context.Request.RouteValues["name"];
        await context.Response.WriteAsync($"Hello {name}!");
    });
});
```

The string `/hello/{name:alpha}` is a **route template**. It is used to configure how the endpoint is matched. In this case, the template matches:

* A URL like `/hello/Ryan`
* Any URL path that begins with `/hello/` followed by a sequence of alphabetic characters. `:alpha` applies a route constraint that matches only alphabetic characters. [Route constraints](#route-constraint-reference) are explained later in this document.

The second segment of the URL path, `{name:alpha}`:

* Is bound to the `name` parameter.
* Is captured and stored in [HttpRequest.RouteValues](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.routevalues).

The endpoint routing system described in this document is new as of ASP.NET Core 3.0. However, all versions of ASP.NET Core support the same set of route template features and route constraints.

The following example shows routing with [health checks](../host-and-deploy/health-checks?view=aspnetcore-10.0) and authorization:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Matches request to an endpoint.
    app.UseRouting();

    // Endpoint aware middleware. 
    // Middleware can use metadata from the matched endpoint.
    app.UseAuthentication();
    app.UseAuthorization();

    // Execute the matched endpoint.
    app.UseEndpoints(endpoints =>
    {
        // Configure the Health Check endpoint and require an authorized user.
        endpoints.MapHealthChecks("/healthz").RequireAuthorization();

        // Configure another endpoint, no authorization requirements.
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}
```

The preceding example demonstrates how:

* The authorization middleware can be used with routing.
* Endpoints can be used to configure authorization behavior.

The [MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks) call adds a health check endpoint. Chaining [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) on to this call attaches an authorization policy to the endpoint.

Calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) adds the authentication and authorization middleware. These middleware are placed between [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and `UseEndpoints` so that they can:

* See which endpoint was selected by `UseRouting`.
* Apply an authorization policy before [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) dispatches to the endpoint.

[]()

### Endpoint metadata

In the preceding example, there are two endpoints, but only the health check endpoint has an authorization policy attached. If the request matches the health check endpoint, `/healthz`, an authorization check is performed. This demonstrates that endpoints can have extra data attached to them. This extra data is called endpoint **metadata**:

* The metadata can be processed by routing-aware middleware.
* The metadata can be of any .NET type.

## Routing concepts

The routing system builds on top of the middleware pipeline by adding the powerful **endpoint** concept. Endpoints represent units of the app's functionality that are distinct from each other in terms of routing, authorization, and any number of ASP.NET Core's systems.

[]()

### ASP.NET Core endpoint definition

An ASP.NET Core endpoint is:

* Executable: Has a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.requestdelegate#microsoft-aspnetcore-http-endpoint-requestdelegate).
* Extensible: Has a [Metadata](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.metadata) collection.
* Selectable: Optionally, has [routing information](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.routepattern).
* Enumerable: The collection of endpoints can be listed by retrieving the [EndpointDataSource](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource) from [DI](dependency-injection?view=aspnetcore-10.0).

The following code shows how to retrieve and inspect the endpoint matching the current request:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();

    app.Use(next => context =>
    {
        var endpoint = context.GetEndpoint();
        if (endpoint is null)
        {
            return Task.CompletedTask;
        }
        
        Console.WriteLine($"Endpoint: {endpoint.DisplayName}");

        if (endpoint is RouteEndpoint routeEndpoint)
        {
            Console.WriteLine("Endpoint has route pattern: " +
                routeEndpoint.RoutePattern.RawText);
        }

        foreach (var metadata in endpoint.Metadata)
        {
            Console.WriteLine($"Endpoint has metadata: {metadata}");
        }

        return Task.CompletedTask;
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}
```

The endpoint, if selected, can be retrieved from the `HttpContext`. Its properties can be inspected. Endpoint objects are immutable and cannot be modified after creation. The most common type of endpoint is a [RouteEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint). `RouteEndpoint` includes information that allows it to be selected by the routing system.

In the preceding code, [app.Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use) configures an in-line [middleware](middleware/?view=aspnetcore-10.0).

[]()

The following code shows that, depending on where `app.Use` is called in the pipeline, there may not be an endpoint:

```
// Location 1: before routing runs, endpoint is always null here
app.Use(next => context =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseRouting();

// Location 2: after routing runs, endpoint will be non-null if routing found a match
app.Use(next => context =>
{
    Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseEndpoints(endpoints =>
{
    // Location 3: runs when this endpoint matches
    endpoints.MapGet("/", context =>
    {
        Console.WriteLine(
            $"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
        return Task.CompletedTask;
    }).WithDisplayName("Hello");
});

// Location 4: runs after UseEndpoints - will only run if there was no match
app.Use(next => context =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});
```

This preceding sample adds `Console.WriteLine` statements that display whether or not an endpoint has been selected. For clarity, the sample assigns a display name to the provided `/` endpoint.

Running this code with a URL of `/` displays:

```
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
```

Running this code with any other URL displays:

```
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
```

This output demonstrates that:

* The endpoint is always null before `UseRouting` is called.
* If a match is found, the endpoint is non-null between `UseRouting` and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).
* The `UseEndpoints` middleware is **terminal** when a match is found. [Terminal middleware](#tm) is defined later in this document.
* The middleware after `UseEndpoints` execute only when no match is found.

The `UseRouting` middleware uses the [SetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.setendpoint) method to attach the endpoint to the current context. It's possible to replace the `UseRouting` middleware with custom logic and still get the benefits of using endpoints. Endpoints are a low-level primitive like middleware, and aren't coupled to the routing implementation. Most apps don't need to replace `UseRouting` with custom logic.

The `UseEndpoints` middleware is designed to be used in tandem with the `UseRouting` middleware. The core logic to execute an endpoint isn't complicated. Use [GetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.getendpoint) to retrieve the endpoint, and then invoke its [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint.requestdelegate#microsoft-aspnetcore-http-endpoint-requestdelegate) property.

The following code demonstrates how middleware can influence or react to routing:

```
public class IntegratedMiddlewareStartup
{ 
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Location 1: Before routing runs. Can influence request before routing runs.
        app.UseHttpMethodOverride();

        app.UseRouting();

        // Location 2: After routing runs. Middleware can match based on metadata.
        app.Use(next => context =>
        {
            var endpoint = context.GetEndpoint();
            if (endpoint?.Metadata.GetMetadata<AuditPolicyAttribute>()?.NeedsAudit
                                                                            == true)
            {
                Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
            }

            return next(context);
        });

        app.UseEndpoints(endpoints =>
        {         
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello world!");
            });

            // Using metadata to configure the audit policy.
            endpoints.MapGet("/sensitive", async context =>
            {
                await context.Response.WriteAsync("sensitive data");
            })
            .WithMetadata(new AuditPolicyAttribute(needsAudit: true));
        });

    } 
}

public class AuditPolicyAttribute : Attribute
{
    public AuditPolicyAttribute(bool needsAudit)
    {
        NeedsAudit = needsAudit;
    }

    public bool NeedsAudit { get; }
}
```

The preceding example demonstrates two important concepts:

* Middleware can run before `UseRouting` to modify the data that routing operates upon.
  * Usually middleware that appears before routing modifies some property of the request, such as [UseRewriter](/en-us/dotnet/api/microsoft.aspnetcore.builder.rewritebuilderextensions.userewriter), [UseHttpMethodOverride](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpmethodoverrideextensions.usehttpmethodoverride), or [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase).

* Middleware can run between `UseRouting` and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) to process the results of routing before the endpoint is executed.

  * Middleware that runs between `UseRouting` and `UseEndpoints`:

    * Usually inspects metadata to understand the endpoints.
    * Often makes security decisions, as done by `UseAuthorization` and `UseCors`.

  * The combination of middleware and metadata allows configuring policies per-endpoint.

The preceding code shows an example of a custom middleware that supports per-endpoint policies. The middleware writes an *audit log* of access to sensitive data to the console. The middleware can be configured to *audit* an endpoint with the `AuditPolicyAttribute` metadata. This sample demonstrates an *opt-in* pattern where only endpoints that are marked as sensitive are audited. It's possible to define this logic in reverse, auditing everything that isn't marked as safe, for example. The endpoint metadata system is flexible. This logic could be designed in whatever way suits the use case.

The preceding sample code is intended to demonstrate the basic concepts of endpoints. **The sample is not intended for production use**. A more complete version of an *audit log* middleware would:

* Log to a file or database.
* Include details such as the user, IP address, name of the sensitive endpoint, and more.

The audit policy metadata `AuditPolicyAttribute` is defined as an `Attribute` for easier use with class-based frameworks such as controllers and SignalR. When using *route to code*:

* Metadata is attached with a builder API.
* Class-based frameworks include all attributes on the corresponding method and class when creating endpoints.

The best practices for metadata types are to define them either as interfaces or attributes. Interfaces and attributes allow code reuse. The metadata system is flexible and doesn't impose any limitations.

[]()

### Comparing a terminal middleware and routing

The following code sample contrasts using middleware with using routing:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Approach 1: Writing a terminal middleware.
    app.Use(next => async context =>
    {
        if (context.Request.Path == "/")
        {
            await context.Response.WriteAsync("Hello terminal middleware!");
            return;
        }

        await next(context);
    });

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        // Approach 2: Using routing.
        endpoints.MapGet("/Movie", async context =>
        {
            await context.Response.WriteAsync("Hello routing!");
        });
    });
}
```

The style of middleware shown with `Approach 1:` is **terminal middleware**. It's called terminal middleware because it does a matching operation:

* The matching operation in the preceding sample is `Path == "/"` for the middleware and `Path == "/Movie"` for routing.
* When a match is successful, it executes some functionality and returns, rather than invoking the `next` middleware.

It's called terminal middleware because it terminates the search, executes some functionality, and then returns.

Comparing a terminal middleware and routing:

* Both approaches allow terminating the processing pipeline:

  * Middleware terminates the pipeline by returning rather than invoking `next`.
  * Endpoints are always terminal.

* Terminal middleware allows positioning the middleware at an arbitrary place in the pipeline:
  * Endpoints execute at the position of [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).

* Terminal middleware allows arbitrary code to determine when the middleware matches:

  * Custom route matching code can be verbose and difficult to write correctly.
  * Routing provides straightforward solutions for typical apps. Most apps don't require custom route matching code.

* Endpoints interface with middleware such as `UseAuthorization` and `UseCors`.
  * Using a terminal middleware with `UseAuthorization` or `UseCors` requires manual interfacing with the authorization system.

An [endpoint](#endpoint) defines both:

* A delegate to process requests.
* A collection of arbitrary metadata. The metadata is used to implement cross-cutting concerns based on policies and configuration attached to each endpoint.

Terminal middleware can be an effective tool, but can require:

* A significant amount of coding and testing.
* Manual integration with other systems to achieve the desired level of flexibility.

Consider integrating with routing before writing a terminal middleware.

Existing terminal middleware that integrates with [Map](middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline) or [MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen) can usually be turned into a routing aware endpoint. [MapHealthChecks](https://github.com/dotnet/AspNetCore/blob/main/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs#L16) demonstrates the pattern for router-ware:

* Write an extension method on [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder).
* Create a nested middleware pipeline using [CreateApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder.createapplicationbuilder).
* Attach the middleware to the new pipeline. In this case, [UseHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckapplicationbuilderextensions.usehealthchecks).
* [Build](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder.build) the middleware pipeline into a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate).
* Call `Map` and provide the new middleware pipeline.
* Return the builder object provided by `Map` from the extension method.

The following code shows use of [MapHealthChecks](../host-and-deploy/health-checks?view=aspnetcore-10.0):

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Matches request to an endpoint.
    app.UseRouting();

    // Endpoint aware middleware. 
    // Middleware can use metadata from the matched endpoint.
    app.UseAuthentication();
    app.UseAuthorization();

    // Execute the matched endpoint.
    app.UseEndpoints(endpoints =>
    {
        // Configure the Health Check endpoint and require an authorized user.
        endpoints.MapHealthChecks("/healthz").RequireAuthorization();

        // Configure another endpoint, no authorization requirements.
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}
```

The preceding sample shows why returning the builder object is important. Returning the builder object allows the app developer to configure policies such as authorization for the endpoint. In this example, the health checks middleware has no direct integration with the authorization system.

The metadata system was created in response to the problems encountered by extensibility authors using terminal middleware. It's problematic for each middleware to implement its own integration with the authorization system.

[]()

### URL matching

* Is the process by which routing matches an incoming request to an [endpoint](#endpoint).
* Is based on data in the URL path and headers.
* Can be extended to consider any data in the request.

When a routing middleware executes, it sets an `Endpoint` and route values to a [request feature](request-features?view=aspnetcore-10.0) on the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) from the current request:

* Calling [HttpContext.GetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.getendpoint) gets the endpoint.
* `HttpRequest.RouteValues` gets the collection of route values.

[Middleware](middleware/?view=aspnetcore-10.0) runs after the routing middleware can inspect the endpoint and take action. For example, an authorization middleware can interrogate the endpoint's metadata collection for an authorization policy. After all of the middleware in the request processing pipeline is executed, the selected endpoint's delegate is invoked.

The routing system in endpoint routing is responsible for all dispatching decisions. Because the middleware applies policies based on the selected endpoint, it's important that:

* Any decision that can affect dispatching or the application of security policies is made inside the routing system.

Warning

For backward-compatibility, when a Controller or Razor Pages endpoint delegate is executed, the properties of [RouteContext.RouteData](/en-us/dotnet/api/microsoft.aspnetcore.routing.routecontext.routedata#microsoft-aspnetcore-routing-routecontext-routedata) are set to appropriate values based on the request processing performed thus far.

The `RouteContext` type will be marked obsolete in a future release:

* Migrate `RouteData.Values` to `HttpRequest.RouteValues`.
* Migrate `RouteData.DataTokens` to retrieve [IDataTokensMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.idatatokensmetadata) from the endpoint metadata.

URL matching operates in a configurable set of phases. In each phase, the output is a set of matches. The set of matches can be narrowed down further by the next phase. The routing implementation does not guarantee a processing order for matching endpoints. **All** possible matches are processed at once. The URL matching phases occur in the following order. ASP.NET Core:

1. Processes the URL path against the set of endpoints and their route templates, collecting **all** of the matches.
2. Takes the preceding list and removes matches that fail with route constraints applied.
3. Takes the preceding list and removes matches that fail the set of [MatcherPolicy](/en-us/dotnet/api/microsoft.aspnetcore.routing.matcherpolicy) instances.
4. Uses the [EndpointSelector](/en-us/dotnet/api/microsoft.aspnetcore.routing.matching.endpointselector) to make a final decision from the preceding list.

The list of endpoints is prioritized according to:

* The [RouteEndpoint.Order](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.order)
* The [route template precedence](#rtp)

All matching endpoints are processed in each phase until the [EndpointSelector](/en-us/dotnet/api/microsoft.aspnetcore.routing.matching.endpointselector) is reached. The `EndpointSelector` is the final phase. It chooses the highest priority endpoint from the matches as the best match. If there are other matches with the same priority as the best match, an ambiguous match exception is thrown.

The route precedence is computed based on a **more specific** route template being given a higher priority. For example, consider the templates `/hello` and `/{message}`:

* Both match the URL path `/hello`.
* `/hello` is more specific and therefore higher priority.

In general, route precedence does a good job of choosing the best match for the kinds of URL schemes used in practice. Use [Order](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.order#microsoft-aspnetcore-routing-routeendpoint-order) only when necessary to avoid an ambiguity.

Due to the kinds of extensibility provided by routing, it isn't possible for the routing system to compute ahead of time the ambiguous routes. Consider an example such as the route templates `/{message:alpha}` and `/{message:int}`:

* The `alpha` constraint matches only alphabetic characters.
* The `int` constraint matches only numbers.
* These templates have the same route precedence, but there's no single URL they both match.
* If the routing system reported an ambiguity error at startup, it would block this valid use case.

Warning

The order of operations inside [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) doesn't influence the behavior of routing, with one exception. [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) and [MapAreaRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcarearoutebuilderextensions.maparearoute) automatically assign an order value to their endpoints based on the order they are invoked. This simulates long-time behavior of controllers without the routing system providing the same guarantees as older routing implementations.

In the legacy implementation of routing, it's possible to implement routing extensibility that has a dependency on the order in which routes are processed. Endpoint routing in ASP.NET Core 3.0 or later:

* Doesn't have a concept of routes.
* Doesn't provide ordering guarantees. All endpoints are processed at once.

[]()

### Route template precedence and endpoint selection order

[Route template precedence](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Template/RoutePrecedence.cs#L16) is a system that assigns each route template a value based on how specific it is. Route template precedence:

* Avoids the need to adjust the order of endpoints in common cases.
* Attempts to match the common-sense expectations of routing behavior.

For example, consider templates `/Products/List` and `/Products/{id}`. It would be reasonable to assume that `/Products/List` is a better match than `/Products/{id}` for the URL path `/Products/List`. This works because the literal segment `/List` is considered to have better precedence than the parameter segment `/{id}`.

The details of how precedence works are coupled to how route templates are defined:

* Templates with more segments are considered more specific.
* A segment with literal text is considered more specific than a parameter segment.
* A parameter segment with a constraint is considered more specific than one without.
* A complex segment is considered as specific as a parameter segment with a constraint.
* Catch-all parameters are the least specific. See **catch-all** in the [Route template reference](#rtr) for important information on catch-all routes.

See the [source code on GitHub](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Template/RoutePrecedence.cs#L189) for a reference of exact values.

[]()

### URL generation concepts

URL generation:

* Is the process by which routing can create a URL path based on a set of route values.
* Allows for a logical separation between endpoints and the URLs that access them.

Endpoint routing includes the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) API. `LinkGenerator` is a singleton service available from [DI](dependency-injection?view=aspnetcore-10.0). The `LinkGenerator` API can be used outside of the context of an executing request. [Mvc.IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) and scenarios that rely on [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper), such as [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0), HTML Helpers, and [Action Results](../mvc/controllers/actions?view=aspnetcore-10.0), use the `LinkGenerator` API internally to provide link generating capabilities.

The link generator is backed by the concept of an **address** and **address schemes**. An address scheme is a way of determining the endpoints that should be considered for link generation. For example, the route name and route values scenarios many users are familiar with from controllers and Razor Pages are implemented as an address scheme.

The link generator can link to controllers and Razor Pages via the following extension methods:

* [GetPathByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.getpathbyaction)
* [GetUriByAction](/en-us/dotnet/api/microsoft.aspnetcore.routing.controllerlinkgeneratorextensions.geturibyaction)
* [GetPathByPage](/en-us/dotnet/api/microsoft.aspnetcore.routing.pagelinkgeneratorextensions.getpathbypage)
* [GetUriByPage](/en-us/dotnet/api/microsoft.aspnetcore.routing.pagelinkgeneratorextensions.geturibypage)

Overloads of these methods accept arguments that include the `HttpContext`. These methods are functionally equivalent to [Url.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.urlhelper.action) and [Url.Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.page), but offer additional flexibility and options.

The `GetPath*` methods are most similar to `Url.Action` and `Url.Page`, in that they generate a URI containing an absolute path. The `GetUri*` methods always generate an absolute URI containing a scheme and host. The methods that accept an `HttpContext` generate a URI in the context of the executing request. The [ambient](#ambient) route values, URL base path, scheme, and host from the executing request are used unless overridden.

[LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) is called with an address. Generating a URI occurs in two steps:

1. An address is bound to a list of endpoints that match the address.
2. Each endpoint's [RoutePattern](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint.routepattern#microsoft-aspnetcore-routing-routeendpoint-routepattern) is evaluated until a route pattern that matches the supplied values is found. The resulting output is combined with the other URI parts supplied to the link generator and returned.

The methods provided by [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) support standard link generation capabilities for any type of address. The most convenient way to use the link generator is through extension methods that perform operations for a specific address type:

| Extension Method                                                                                  | Description                                                         |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [GetPathByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.getpathbyaddress) | Generates a URI with an absolute path based on the provided values. |
| [GetUriByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.geturibyaddress)   | Generates an absolute URI based on the provided values.             |

Warning

Pay attention to the following implications of calling [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) methods:

* Use `GetUri*` extension methods with caution in an app configuration that doesn't validate the `Host` header of incoming requests. If the `Host` header of incoming requests isn't validated, untrusted request input can be sent back to the client in URIs in a view or page. We recommend that all production apps configure their server to validate the `Host` header against known valid values.

* Use [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) with caution in middleware in combination with `Map` or `MapWhen`. `Map*` changes the base path of the executing request, which affects the output of link generation. All of the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) APIs allow specifying a base path. Specify an empty base path to undo the `Map*` affect on link generation.

### Middleware example

In the following example, a middleware uses the [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) API to create a link to an action method that lists store products. Using the link generator by injecting it into a class and calling `GenerateLink` is available to any class in an app:

```
public class ProductsLinkMiddleware
{
    private readonly LinkGenerator _linkGenerator;

    public ProductsLinkMiddleware(RequestDelegate next, LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        var url = _linkGenerator.GetPathByAction("ListProducts", "Store");

        httpContext.Response.ContentType = "text/plain";

        await httpContext.Response.WriteAsync($"Go to {url} to see our products.");
    }
}
```

[]()

## Route template reference

Tokens within `{}` define route parameters that are bound if the route is matched. More than one route parameter can be defined in a route segment, but route parameters must be separated by a literal value. For example, `{controller=Home}{action=Index}` isn't a valid route, since there's no literal value between `{controller}` and `{action}`. Route parameters must have a name and may have additional attributes specified.

Literal text other than route parameters (for example, `{id}`) and the path separator `/` must match the text in the URL. Text matching is case-insensitive and based on the decoded representation of the URL's path. To match a literal route parameter delimiter `{` or `}`, escape the delimiter by repeating the character. For example `{{` or `}}`.

Asterisk `*` or double asterisk `**`:

* Can be used as a prefix to a route parameter to bind to the rest of the URI.

* Are called a **catch-all** parameters. For example, `blog/{**slug}`:

  * Matches any URI that starts with `/blog` and has any value following it.
  * The value following `/blog` is assigned to the [slug](https://developer.mozilla.org/docs/Glossary/Slug) route value.

```
public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
```

Catch-all parameters can also match the empty string.

The catch-all parameter escapes the appropriate characters when the route is used to generate a URL, including path separator `/` characters. For example, the route `foo/{*path}` with route values `{ path = "my/path" }` generates `foo/my%2Fpath`. Note the escaped forward slash. To round-trip path separator characters, use the `**` route parameter prefix. The route `foo/{**path}` with `{ path = "my/path" }` generates `foo/my/path`.

URL patterns that attempt to capture a file name with an optional file extension have additional considerations. For example, consider the template `files/{filename}.{ext?}`. When values for both `filename` and `ext` exist, both values are populated. If only a value for `filename` exists in the URL, the route matches because the trailing `.` is optional. The following URLs match this route:

* `/files/myFile.txt`
* `/files/myFile`

Route parameters may have **default values** designated by specifying the default value after the parameter name separated by an equals sign (`=`). For example, `{controller=Home}` defines `Home` as the default value for `controller`. The default value is used if no value is present in the URL for the parameter. Route parameters are made optional by appending a question mark (`?`) to the end of the parameter name. For example, `id?`. The difference between optional values and default route parameters is:

* A route parameter with a default value always produces a value.
* An optional parameter has a value only when a value is provided by the request URL.

Route parameters may have constraints that must match the route value bound from the URL. Adding `:` and constraint name after the route parameter name specifies an inline constraint on a route parameter. If the constraint requires arguments, they're enclosed in parentheses `(...)` after the constraint name. Multiple *inline constraints* can be specified by appending another `:` and constraint name.

The constraint name and arguments are passed to the [IInlineConstraintResolver](/en-us/dotnet/api/microsoft.aspnetcore.routing.iinlineconstraintresolver) service to create an instance of [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) to use in URL processing. For example, the route template `blog/{article:minlength(10)}` specifies a `minlength` constraint with the argument `10`. For more information on route constraints and a list of the constraints provided by the framework, see the [Route constraint reference](#route-constraint-reference) section.

Route parameters may also have parameter transformers. Parameter transformers transform a parameter's value when generating links and matching actions and pages to URLs. Like constraints, parameter transformers can be added inline to a route parameter by adding a `:` and transformer name after the route parameter name. For example, the route template `blog/{article:slugify}` specifies a `slugify` transformer. For more information on parameter transformers, see the [Parameter transformer reference](#parameter-transformer-reference) section.

The following table demonstrates example route templates and their behavior.

| Route Template                           | Example Matching URI    | The request URI…                                                            |
| ---------------------------------------- | ----------------------- | --------------------------------------------------------------------------- |
| `hello`                                  | `/hello`                | Only matches the single path `/hello`.                                      |
| `{Page=Home}`                            | `/`                     | Matches and sets `Page` to `Home`.                                          |
| `{Page=Home}`                            | `/Contact`              | Matches and sets `Page` to `Contact`.                                       |
| `{controller}/{action}/{id?}`            | `/Products/List`        | Maps to the `Products` controller and `List` action.                        |
| `{controller}/{action}/{id?}`            | `/Products/Details/123` | Maps to the `Products` controller and `Details` action with`id` set to 123. |
| `{controller=Home}/{action=Index}/{id?}` | `/`                     | Maps to the `Home` controller and `Index` method. `id` is ignored.          |
| `{controller=Home}/{action=Index}/{id?}` | `/Products`             | Maps to the `Products` controller and `Index` method. `id` is ignored.      |

Using a template is generally the simplest approach to routing. Constraints and defaults can also be specified outside the route template.

### Complex segments

Complex segments are processed by matching up literal delimiters from right to left in a [non-greedy](#greedy) way. For example, `[Route("/a{b}c{d}")]` is a complex segment. Complex segments work in a particular way that must be understood to use them successfully. The example in this section demonstrates why complex segments only really work well when the delimiter text doesn't appear inside the parameter values. Using a [regex](/en-us/dotnet/standard/base-types/regular-expressions) and then manually extracting the values is needed for more complex cases.

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

This is a summary of the steps that routing performs with the template `/a{b}c{d}` and the URL path `/abcd`. The `|` is used to help visualize how the algorithm works:

* The first literal, right to left, is `c`. So `/abcd` is searched from right and finds `/ab|c|d`.
* Everything to the right (`d`) is now matched to the route parameter `{d}`.
* The next literal, right to left, is `a`. So `/ab|c|d` is searched starting where we left off, then `a` is found `/|a|b|c|d`.
* The value to the right (`b`) is now matched to the route parameter `{b}`.
* There is no remaining text and no remaining route template, so this is a match.

Here's an example of a negative case using the same template `/a{b}c{d}` and the URL path `/aabcd`. The `|` is used to help visualize how the algorithm works. This case isn't a match, which is explained by the same algorithm:

* The first literal, right to left, is `c`. So `/aabcd` is searched from right and finds `/aab|c|d`.
* Everything to the right (`d`) is now matched to the route parameter `{d}`.
* The next literal, right to left, is `a`. So `/aab|c|d` is searched starting where we left off, then `a` is found `/a|a|b|c|d`.
* The value to the right (`b`) is now matched to the route parameter `{b}`.
* At this point there is remaining text `a`, but the algorithm has run out of route template to parse, so this is not a match.

Since the matching algorithm is [non-greedy](#greedy):

* It matches the smallest amount of text possible in each step.
* Any case where the delimiter value appears inside the parameter values results in not matching.

Regular expressions provide much more control over their matching behavior.

[]()

Greedy matching, also known as [lazy matching](https://wikipedia.org/wiki/Regular_expression#Lazy_matching), matches the largest possible string. Non-greedy matches the smallest possible string.

## Route constraint reference

Route constraints execute when a match has occurred to the incoming URL and the URL path is tokenized into route values. Route constraints generally inspect the route value associated via the route template and make a true or false decision about whether the value is acceptable. Some route constraints use data outside the route value to consider whether the request can be routed. For example, the [HttpMethodRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.constraints.httpmethodrouteconstraint) can accept or reject a request based on its HTTP verb. Constraints are used in routing requests and link generation.

Warning

Don't use constraints for input validation. If constraints are used for input validation, invalid input results in a `404` Not Found response. Invalid input should produce a `400` Bad Request with an appropriate error message. Route constraints are used to disambiguate similar routes, not to validate the inputs for a particular route.

The following table demonstrates example route constraints and their expected behavior.

| constraint          | Example                                     | Example Matches                        | Notes                                                                                     |
| ------------------- | ------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- |
| `int`               | `{id:int}`                                  | `123456789`, `-123456789`              | Matches any integer                                                                       |
| `bool`              | `{active:bool}`                             | `true`, `FALSE`                        | Matches `true` or `false`. Case-insensitive                                               |
| `datetime`          | `{dob:datetime}`                            | `2016-12-31`, `2016-12-31 7:32pm`      | Matches a valid `DateTime` value in the invariant culture. See preceding warning.         |
| `decimal`           | `{price:decimal}`                           | `49.99`, `-1,000.01`                   | Matches a valid `decimal` value in the invariant culture. See preceding warning.          |
| `double`            | `{weight:double}`                           | `1.234`, `-1,001.01e8`                 | Matches a valid `double` value in the invariant culture. See preceding warning.           |
| `float`             | `{weight:float}`                            | `1.234`, `-1,001.01e8`                 | Matches a valid `float` value in the invariant culture. See preceding warning.            |
| `guid`              | `{id:guid}`                                 | `CD2C1638-1638-72D5-1638-DEADBEEF1638` | Matches a valid `Guid` value                                                              |
| `long`              | `{ticks:long}`                              | `123456789`, `-123456789`              | Matches a valid `long` value                                                              |
| `minlength(value)`  | `{username:minlength(4)}`                   | `Rick`                                 | String must be at least 4 characters                                                      |
| `maxlength(value)`  | `{filename:maxlength(8)}`                   | `MyFile`                               | String must be no more than 8 characters                                                  |
| `length(length)`    | `{filename:length(12)}`                     | `somefile.txt`                         | String must be exactly 12 characters long                                                 |
| `length(min,max)`   | `{filename:length(8,16)}`                   | `somefile.txt`                         | String must be at least 8 and no more than 16 characters long                             |
| `min(value)`        | `{age:min(18)}`                             | `19`                                   | Integer value must be at least 18                                                         |
| `max(value)`        | `{age:max(120)}`                            | `91`                                   | Integer value must be no more than 120                                                    |
| `range(min,max)`    | `{age:range(18,120)}`                       | `91`                                   | Integer value must be at least 18 but no more than 120                                    |
| `alpha`             | `{name:alpha}`                              | `Rick`                                 | String must consist of one or more alphabetical characters, `a`-`z` and case-insensitive. |
| `regex(expression)` | `{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}` | `123-45-6789`                          | String must match the regular expression. See tips about defining a regular expression.   |
| `required`          | `{name:required}`                           | `Rick`                                 | Used to enforce that a non-parameter value is present during URL generation               |

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

Multiple, colon delimited constraints can be applied to a single parameter. For example, the following constraint restricts a parameter to an integer value of 1 or greater:

```
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
```

Warning

Route constraints that verify the URL and are converted to a CLR type always use the invariant culture. For example, conversion to the CLR type `int` or `DateTime`. These constraints assume that the URL is not localizable. The framework-provided route constraints don't modify the values stored in route values. All route values parsed from the URL are stored as strings. For example, the `float` constraint attempts to convert the route value to a float, but the converted value is used only to verify it can be converted to a float.

### Regular expressions in constraints

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

Regular expressions can be specified as inline constraints using the `regex(...)` route constraint. Methods in the [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute) family also accept an object literal of constraints. If that form is used, string values are interpreted as regular expressions.

The following code uses an inline regex constraint:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
        context => 
        {
            return context.Response.WriteAsync("inline-constraint match");
        });
 });
```

The following code uses an object literal to specify a regex constraint:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "people",
        pattern: "People/{ssn}",
        constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
        defaults: new { controller = "People", action = "List", });
});
```

The ASP.NET Core framework adds `RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant` to the regular expression constructor. See [RegexOptions](/en-us/dotnet/api/system.text.regularexpressions.regexoptions) for a description of these members.

Regular expressions use delimiters and tokens similar to those used by routing and the C# language. Regular expression tokens must be escaped. To use the regular expression `^\d{3}-\d{2}-\d{4}$` in an inline constraint, use one of the following:

* Replace `\` characters provided in the string as `\\` characters in the C# source file in order to escape the `\` string escape character.
* [Verbatim string literals](/en-us/dotnet/csharp/language-reference/keywords/string).

To escape routing parameter delimiter characters `{`, `}`, `[`, `]`, double the characters in the expression, for example, `{{`, `}}`, `[[`, `]]`. The following table shows a regular expression and its escaped version.

| Regular expression    | Escaped regular expression     |
| --------------------- | ------------------------------ |
| `^\d{3}-\d{2}-\d{4}$` | `^\\d{{3}}-\\d{{2}}-\\d{{4}}$` |
| `^[a-z]{2}$`          | `^[[a-z]]{{2}}$`               |

Regular expressions used in routing often start with the `^` character and match the starting position of the string. The expressions often end with the `$` character and match the end of the string. The `^` and `$` characters ensure that the regular expression matches the entire route parameter value. Without the `^` and `$` characters, the regular expression matches any substring within the string, which is often undesirable. The following table provides examples and explains why they match or fail to match.

| Expression   | String    | Match | Comment               |
| ------------ | --------- | ----- | --------------------- |
| `[a-z]{2}`   | hello     | Yes   | Substring matches     |
| `[a-z]{2}`   | 123abc456 | Yes   | Substring matches     |
| `[a-z]{2}`   | mz        | Yes   | Matches expression    |
| `[a-z]{2}`   | MZ        | Yes   | Not case sensitive    |
| `^[a-z]{2}$` | hello     | No    | See `^` and `$` above |
| `^[a-z]{2}$` | 123abc456 | No    | See `^` and `$` above |

For more information on regular expression syntax, see [.NET Framework Regular Expressions](/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference).

To constrain a parameter to a known set of possible values, use a regular expression. For example, `{action:regex(^(list|get|create)$)}` only matches the `action` route value to `list`, `get`, or `create`. If passed into the constraints dictionary, the string `^(list|get|create)$` is equivalent. Constraints that are passed in the constraints dictionary that don't match one of the known constraints are also treated as regular expressions. Constraints that are passed within a template that don't match one of the known constraints are not treated as regular expressions.

### Custom route constraints

Custom route constraints can be created by implementing the [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint) interface. The `IRouteConstraint` interface contains [Match](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint.match), which returns `true` if the constraint is satisfied and `false` otherwise.

Custom route constraints are rarely needed. Before implementing a custom route constraint, consider alternatives, such as model binding.

The ASP.NET Core [Constraints](https://github.com/dotnet/aspnetcore/tree/main/src/Http/Routing/src/Constraints) folder provides good examples of creating constraints. For example, [GuidRouteConstraint](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs#L18).

To use a custom `IRouteConstraint`, the route constraint type must be registered with the app's [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap) in the service container. A `ConstraintMap` is a dictionary that maps route constraint keys to `IRouteConstraint` implementations that validate those constraints. An app's `ConstraintMap` can be updated in `Startup.ConfigureServices` either as part of a [services.AddRouting](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.routingservicecollectionextensions.addrouting) call or by configuring [RouteOptions](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions) directly with `services.Configure<RouteOptions>`. For example:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
    });
}
```

The preceding constraint is applied in the following code:

```
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
    // GET /api/test/3
    [HttpGet("{id:customName}")]
    public IActionResult Get(string id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    // GET /api/test/my/3
    [HttpGet("my/{id:customName}")]
    public IActionResult Get(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
```

[MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.

The implementation of `MyCustomConstraint` prevents `0` being applied to a route parameter:

```
class MyCustomConstraint : IRouteConstraint
{
    private Regex _regex;

    public MyCustomConstraint()
    {
        _regex = new Regex(@"^[1-9]*$",
                            RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
                            TimeSpan.FromMilliseconds(100));
    }
    public bool Match(HttpContext httpContext, IRouter route, string routeKey,
                      RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out object value))
        {
            var parameterValueString = Convert.ToString(value,
                                                        CultureInfo.InvariantCulture);
            if (parameterValueString == null)
            {
                return false;
            }

            return _regex.IsMatch(parameterValueString);
        }

        return false;
    }
}
```

Warning

When using [System.Text.RegularExpressions](/en-us/dotnet/api/system.text.regularexpressions) to process untrusted input, pass a timeout. A malicious user can provide input to `RegularExpressions` causing a [Denial-of-Service attack](https://www.cisa.gov/news-events/news/understanding-denial-service-attacks). ASP.NET Core framework APIs that use `RegularExpressions` pass a timeout.

The preceding code:

* Prevents `0` in the `{id}` segment of the route.
* Is shown to provide a basic example of implementing a custom constraint. It should not be used in a production app.

The following code is a better approach to preventing an `id` containing a `0` from being processed:

```
[HttpGet("{id}")]
public IActionResult Get(string id)
{
    if (id.Contains('0'))
    {
        return StatusCode(StatusCodes.Status406NotAcceptable);
    }

    return ControllerContext.MyDisplayRouteInfo(id);
}
```

The preceding code has the following advantages over the `MyCustomConstraint` approach:

* It doesn't require a custom constraint.
* It returns a more descriptive error when the route parameter includes `0`.

## Parameter transformer reference

Parameter transformers:

* Execute when generating a link using [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator).
* Implement [Microsoft.AspNetCore.Routing.IOutboundParameterTransformer](/en-us/dotnet/api/microsoft.aspnetcore.routing.ioutboundparametertransformer).
* Are configured using [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap).
* Take the parameter's route value and transform it to a new string value.
* Result in using the transformed value in the generated link.

For example, a custom `slugify` parameter transformer in route pattern `blog\{article:slugify}` with `Url.Action(new { article = "MyTestArticle" })` generates `blog\my-test-article`.

Consider the following `IOutboundParameterTransformer` implementation:

```
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) { return null; }

        return Regex.Replace(value.ToString(), 
                             "([a-z])([A-Z])",
                             "$1-$2",
                             RegexOptions.CultureInvariant,
                             TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}
```

To use a parameter transformer in a route pattern, configure it using [ConstraintMap](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeoptions.constraintmap#microsoft-aspnetcore-routing-routeoptions-constraintmap) in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddRouting(options =>
    {
        options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
    });
}
```

The ASP.NET Core framework uses parameter transformers to transform the URI where an endpoint resolves. For example, parameter transformers transform the route values used to match an `area`, `controller`, `action`, and `page`.

```
routes.MapControllerRoute(
    name: "default",
    template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
```

With the preceding route template, the action `SubscriptionManagementController.GetAll` is matched with the URI `/subscription-management/get-all`. A parameter transformer doesn't change the route values used to generate a link. For example, `Url.Action("GetAll", "SubscriptionManagement")` outputs `/subscription-management/get-all`.

ASP.NET Core provides API conventions for using parameter transformers with generated routes:

* The [Microsoft.AspNetCore.Mvc.ApplicationModels.RouteTokenTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.routetokentransformerconvention) MVC convention applies a specified parameter transformer to all attribute routes in the app. The parameter transformer transforms attribute route tokens as they are replaced. For more information, see [Use a parameter transformer to customize token replacement](../mvc/controllers/routing?view=aspnetcore-10.0#use-a-parameter-transformer-to-customize-token-replacement).
* Razor Pages uses the [PageRouteTransformerConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageroutetransformerconvention) API convention. This convention applies a specified parameter transformer to all automatically discovered Razor Pages. The parameter transformer transforms the folder and file name segments of Razor Pages routes. For more information, see [Use a parameter transformer to customize page routes](../razor-pages/razor-pages-conventions?view=aspnetcore-10.0#use-a-parameter-transformer-to-customize-page-routes).

[]()

## URL generation reference

This section contains a reference for the algorithm implemented by URL generation. In practice, most complex examples of URL generation use controllers or Razor Pages. See [routing in controllers](../mvc/controllers/routing?view=aspnetcore-10.0) for additional information.

The URL generation process begins with a call to [LinkGenerator.GetPathByAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator.getpathbyaddress) or a similar method. The method is provided with an address, a set of route values, and optionally information about the current request from `HttpContext`.

The first step is to use the address to resolve a set of candidate endpoints using an [`IEndpointAddressScheme<TAddress>`](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointaddressscheme-1) that matches the address's type.

Once the set of candidates is found by the address scheme, the endpoints are ordered and processed iteratively until a URL generation operation succeeds. URL generation does **not** check for ambiguities, the first result returned is the final result.

### Troubleshooting URL generation with logging

The first step in troubleshooting URL generation is setting the logging level of `Microsoft.AspNetCore.Routing` to `TRACE`. `LinkGenerator` logs many details about its processing which can be useful to troubleshoot problems.

See [URL generation reference](#ugr) for details on URL generation.

### Addresses

Addresses are the concept in URL generation used to bind a call into the link generator to a set of candidate endpoints.

Addresses are an extensible concept that come with two implementations by default:

* Using *endpoint name* (`string`) as the address:

  * Provides similar functionality to MVC's route name.
  * Uses the [IEndpointNameMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointnamemetadata) metadata type.
  * Resolves the provided string against the metadata of all registered endpoints.
  * Throws an exception on startup if multiple endpoints use the same name.
  * Recommended for general-purpose use outside of controllers and Razor Pages.

* Using *route values* ([RouteValuesAddress](/en-us/dotnet/api/microsoft.aspnetcore.routing.routevaluesaddress)) as the address:

  * Provides similar functionality to controllers and Razor Pages legacy URL generation.
  * Very complex to extend and debug.
  * Provides the implementation used by `IUrlHelper`, Tag Helpers, HTML Helpers, Action Results, etc.

The role of the address scheme is to make the association between the address and matching endpoints by arbitrary criteria:

* The endpoint name scheme performs a basic dictionary lookup.
* The route values scheme has a complex best subset of set algorithm.

[]()

### Ambient values and explicit values

From the current request, routing accesses the route values of the current request `HttpContext.Request.RouteValues`. The values associated with the current request are referred to as the **ambient values**. For the purpose of clarity, the documentation refers to the route values passed in to methods as **explicit values**.

The following example shows ambient values and explicit values. It provides ambient values from the current request and explicit values: `{ id = 17, }`:

```
public class WidgetController : Controller
{
    private readonly LinkGenerator _linkGenerator;

    public WidgetController(LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }

    public IActionResult Index()
    {
        var url = _linkGenerator.GetPathByAction(HttpContext,
                                                 null, null,
                                                 new { id = 17, });
        return Content(url);
    }
```

The preceding code:

* Returns `/Widget/Index/17`
* Gets [LinkGenerator](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkgenerator) via [DI](dependency-injection?view=aspnetcore-10.0).

The following code provides no ambient values and explicit values: `{ controller = "Home", action = "Subscribe", id = 17, }`:

```
public IActionResult Index2()
{
    var url = _linkGenerator.GetPathByAction("Subscribe", "Home",
                                             new { id = 17, });
    return Content(url);
}
```

The preceding method returns `/Home/Subscribe/17`

The following code in the `WidgetController` returns `/Widget/Subscribe/17`:

```
var url = _linkGenerator.GetPathByAction("Subscribe", null,
                                         new { id = 17, });
```

The following code provides the controller from ambient values in the current request and explicit values: `{ action = "Edit", id = 17, }`:

```
public class GadgetController : Controller
{
    public IActionResult Index()
    {
        var url = Url.Action("Edit", new { id = 17, });
        return Content(url);
    }
```

In the preceding code:

* `/Gadget/Edit/17` is returned.
* [Url](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.url#microsoft-aspnetcore-mvc-controllerbase-url) gets the [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper).
* [Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.action) generates a URL with an absolute path for an action method. The URL contains the specified `action` name and `route` values.

The following code provides ambient values from the current request and explicit values: `{ page = "./Edit, id = 17, }`:

```
public class IndexModel : PageModel
{
    public void OnGet()
    {
        var url = Url.Page("./Edit", new { id = 17, });
        ViewData["URL"] = url;
    }
}
```

The preceding code sets `url` to `/Edit/17` when the Edit Razor Page contains the following page directive:

`@page "{id:int}"`

If the Edit page doesn't contain the `"{id:int}"` route template, `url` is `/Edit?id=17`.

The behavior of MVC's [IUrlHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper) adds a layer of complexity in addition to the rules described here:

* `IUrlHelper` always provides the route values from the current request as ambient values.
* [IUrlHelper.Action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.action) always copies the current `action` and `controller` route values as explicit values unless overridden by the developer.
* [IUrlHelper.Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.urlhelperextensions.page) always copies the current `page` route value as an explicit value unless overridden.
* `IUrlHelper.Page` always overrides the current `handler` route value with `null` as an explicit values unless overridden.

Users are often surprised by the behavioral details of ambient values, because MVC doesn't seem to follow its own rules. For historical and compatibility reasons, certain route values such as `action`, `controller`, `page`, and `handler` have their own special-case behavior.

The equivalent functionality provided by `LinkGenerator.GetPathByAction` and `LinkGenerator.GetPathByPage` duplicates these anomalies of `IUrlHelper` for compatibility.

### URL generation process

Once the set of candidate endpoints are found, the URL generation algorithm:

* Processes the endpoints iteratively.
* Returns the first successful result.

The first step in this process is called **route value invalidation**. Route value invalidation is the process by which routing decides which route values from the ambient values should be used and which should be ignored. Each ambient value is considered and either combined with the explicit values, or ignored.

The best way to think about the role of ambient values is that they attempt to save application developers typing, in some common cases. Traditionally, the scenarios where ambient values are helpful are related to MVC:

* When linking to another action in the same controller, the controller name doesn't need to be specified.
* When linking to another controller in the same area, the area name doesn't need to be specified.
* When linking to the same action method, route values don't need to be specified.
* When linking to another part of the app, you don't want to carry over route values that have no meaning in that part of the app.

Calls to `LinkGenerator` or `IUrlHelper` that return `null` are usually caused by not understanding route value invalidation. Troubleshoot route value invalidation by explicitly specifying more of the route values to see if that solves the problem.

Route value invalidation works on the assumption that the app's URL scheme is hierarchical, with a hierarchy formed from left-to-right. Consider the basic controller route template `{controller}/{action}/{id?}` to get an intuitive sense of how this works in practice. A **change** to a value **invalidates** all of the route values that appear to the right. This reflects the assumption about hierarchy. If the app has an ambient value for `id`, and the operation specifies a different value for the `controller`:

* `id` won't be reused because `{controller}` is to the left of `{id?}`.

Some examples demonstrating this principle:

* If the explicit values contain a value for `id`, the ambient value for `id` is ignored. The ambient values for `controller` and `action` can be used.
* If the explicit values contain a value for `action`, any ambient value for `action` is ignored. The ambient values for `controller` can be used. If the explicit value for `action` is different from the ambient value for `action`, the `id` value won't be used. If the explicit value for `action` is the same as the ambient value for `action`, the `id` value can be used.
* If the explicit values contain a value for `controller`, any ambient value for `controller` is ignored. If the explicit value for `controller` is different from the ambient value for `controller`, the `action` and `id` values won't be used. If the explicit value for `controller` is the same as the ambient value for `controller`, the `action` and `id` values can be used.

This process is further complicated by the existence of attribute routes and dedicated conventional routes. Controller conventional routes such as `{controller}/{action}/{id?}` specify a hierarchy using route parameters. For [dedicated conventional routes](../mvc/controllers/routing?view=aspnetcore-10.0#dcr) and [attribute routes](../mvc/controllers/routing?view=aspnetcore-10.0#ar) to controllers and Razor Pages:

* There is a hierarchy of route values.
* They don't appear in the template.

For these cases, URL generation defines the **required values** concept. Endpoints created by controllers and Razor Pages have required values specified that allow route value invalidation to work.

The route value invalidation algorithm in detail:

* The required value names are combined with the route parameters, then processed from left-to-right.

* For each parameter, the ambient value and explicit value are compared:

  * If the ambient value and explicit value are the same, the process continues.
  * If the ambient value is present and the explicit value isn't, the ambient value is used when generating the URL.
  * If the ambient value isn't present and the explicit value is, reject the ambient value and all subsequent ambient values.
  * If the ambient value and the explicit value are present, and the two values are different, reject the ambient value and all subsequent ambient values.

At this point, the URL generation operation is ready to evaluate route constraints. The set of accepted values is combined with the parameter default values, which is provided to constraints. If the constraints all pass, the operation continues.

Next, the **accepted values** can be used to expand the route template. The route template is processed:

* From left-to-right.

* Each parameter has its accepted value substituted.

* With the following special cases:

  * If the accepted values is missing a value and the parameter has a default value, the default value is used.
  * If the accepted values is missing a value and the parameter is optional, processing continues.
  * If any route parameter to the right of a missing optional parameter has a value, the operation fails.
  * Contiguous default-valued parameters and optional parameters are collapsed where possible.

Note

For endpoints created by controllers and Razor Pages, `controller`, `action`, `page`, and `area` are part of the endpoint's **required values**, so they're always present in the accepted values. As a result:

* They aren't invalidated by a change to a route value that appears to their left, such as a `culture` parameter.
* Inline default values in the route template, such as `{controller=DefaultController}`, aren't applied, because default values are used only when the accepted values are *missing* a value.

For example, with the template `{culture}/api1/{controller=DefaultController}/{action=DefaultAction}/{id?}` and a current request of `/en/api1/MyController/Index/111`, the call `Url.RouteUrl("ApiRoute1", new { culture = "ar" })` generates `/ar/api1/MyController/Index`, not `/ar/api1/DefaultController/DefaultAction`. The `id` value isn't used because it has no required value and is to the right of a changed value. To use the template's default values, specify them explicitly:

```
Url.RouteUrl("ApiRoute1", new { culture = "ar", controller = "DefaultController", action = "DefaultAction" })
```

For a related limitation, see [Problems with route value invalidation](#problems-with-route-value-invalidation).

Values explicitly provided that don't match a segment of the route are added to the query string. The following table shows the result when using the route template `{controller}/{action}/{id?}`.

| Ambient Values                     | Explicit Values                        | Result                  |
| ---------------------------------- | -------------------------------------- | ----------------------- |
| controller = "Home"                | action = "About"                       | `/Home/About`           |
| controller = "Home"                | controller = "Order", action = "About" | `/Order/About`          |
| controller = "Home", color = "Red" | action = "About"                       | `/Home/About`           |
| controller = "Home"                | action = "About", color = "Red"        | `/Home/About?color=Red` |

### Problems with route value invalidation

For related information about how required values, such as `controller` and `action`, affect route value invalidation and template default values, see the note in [URL generation process](#url-generation-process).

As of ASP.NET Core 3.0, some URL generation schemes used in earlier ASP.NET Core versions don't work well with URL generation. The ASP.NET Core team plans to add features to address these needs in a future release. For now the best solution is to use legacy routing.

The following code shows an example of a URL generation scheme that's not supported by routing.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute("default", 
                                     "{culture}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute("blog", "{culture}/{**slug}", 
                                      new { controller = "Blog", action = "ReadPost", });
});
```

In the preceding code, the `culture` route parameter is used for localization. The desire is to have the `culture` parameter always accepted as an ambient value. However, the `culture` parameter is not accepted as an ambient value because of the way required values work:

* In the `"default"` route template, the `culture` route parameter is to the left of `controller`, so changes to `controller` won't invalidate `culture`.
* In the `"blog"` route template, the `culture` route parameter is considered to be to the right of `controller`, which appears in the required values.

## Configuring endpoint metadata

The following links provide information on configuring endpoint metadata:

* [Enable Cors with endpoint routing](../security/cors?view=aspnetcore-10.0#enable-cors-with-endpoint-routing)
* [IAuthorizationPolicyProvider sample](https://github.com/dotnet/AspNetCore/tree/release/3.1/src/Security/samples/CustomPolicyProvider) using a custom `[MinimumAgeAuthorize]` attribute
* [Test authentication with the \[Authorize\] attribute](../security/authentication/identity?view=aspnetcore-10.0#test-identity)
* [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization)
* [Select a scheme with an `[Authorize]` attribute](../security/authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0#select-a-scheme-with-an-authorize-attribute)
* [Apply policies using an `[Authorize]` attribute](../security/authorization/policies?view=aspnetcore-10.0#apply-policies-to-mvc-controllers)
* [Role-based authorization in ASP.NET Core](../security/authorization/roles?view=aspnetcore-10.0)

[]()

## Host matching in routes with RequireHost

[RequireHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.requirehost) applies a constraint to the route which requires the specified host. The `RequireHost` or [\[Host\]](/en-us/dotnet/api/microsoft.aspnetcore.routing.hostattribute) parameter can be:

* Host: `www.domain.com`, matches `www.domain.com` with any port.
* Host with wildcard: `*.domain.com`, matches `www.domain.com`, `subdomain.domain.com`, or `www.subdomain.domain.com` on any port.
* Port: `*:5000`, matches port 5000 with any host.
* Host and port: `www.domain.com:5000` or `*.domain.com:5000`, matches host and port.

Multiple parameters can be specified using `RequireHost` or `[Host]`. The constraint matches hosts valid for any of the parameters. For example, `[Host("domain.com", "*.domain.com")]` matches `domain.com`, `www.domain.com`, and `subdomain.domain.com`.

The following code uses `RequireHost` to require the specified host on the route:

```
public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", context => context.Response.WriteAsync("Hi Contoso!"))
            .RequireHost("contoso.com");
        endpoints.MapGet("/", context => context.Response.WriteAsync("AdventureWorks!"))
            .RequireHost("adventure-works.com");
        endpoints.MapHealthChecks("/healthz").RequireHost("*:8080");
    });
}
```

The following code uses the `[Host]` attribute on the controller to require any of the specified hosts:

```
[Host("contoso.com", "adventure-works.com")]
public class ProductController : Controller
{
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Host("example.com:8080")]
    public IActionResult Privacy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
```

When the `[Host]` attribute is applied to both the controller and action method:

* The attribute on the action is used.
* The controller attribute is ignored.

## Performance guidance for routing

Most of routing was updated in ASP.NET Core 3.0 to increase performance.

When an app has performance problems, routing is often suspected as the problem. The reason routing is suspected is that frameworks like controllers and Razor Pages report the amount of time spent inside the framework in their logging messages. When there's a significant difference between the time reported by controllers and the total time of the request:

* Developers eliminate their app code as the source of the problem.
* It's common to assume routing is the cause.

Routing is performance tested using thousands of endpoints. It's unlikely that a typical app will encounter a performance problem just by being too large. The most common root cause of slow routing performance is usually a badly-behaving custom middleware.

This following code sample demonstrates a basic technique for narrowing down the source of delay:

```
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseRouting();

    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseAuthorization();

    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();

        logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Timing test.");
        });
    });
}
```

To time routing:

* Interleave each middleware with a copy of the timing middleware shown in the preceding code.
* Add a unique identifier to correlate the timing data with the code.

This is a basic way to narrow down the delay when it's significant, for example, more than `10ms`. Subtracting `Time 2` from `Time 1` reports the time spent inside the `UseRouting` middleware.

The following code uses a more compact approach to the preceding timing code:

```
public sealed class MyStopwatch : IDisposable
{
    ILogger<Startup> _logger;
    string _message;
    Stopwatch _sw;

    public MyStopwatch(ILogger<Startup> logger, string message)
    {
        _logger = logger;
        _message = message;
        _sw = Stopwatch.StartNew();
    }

    private bool disposed = false;


    public void Dispose()
    {
        if (!disposed)
        {
            _logger.LogInformation("{Message }: {ElapsedMilliseconds}ms",
                                    _message, _sw.ElapsedMilliseconds);

            disposed = true;
        }
    }
}
```

```
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    int count = 0;
    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }

    });

    app.UseRouting();

    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });

    app.UseAuthorization();

    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Timing test.");
        });
    });
}
```

### Potentially expensive routing features

The following list provides some insight into routing features that are relatively expensive compared with basic route templates:

* Regular expressions: It's possible to write regular expressions that are complex, or have long running time with a small amount of input.

* Complex segments (`{x}-{y}-{z}`):

  * Are significantly more expensive than parsing a regular URL path segment.
  * Result in many more substrings being allocated.
  * The complex segment logic was not updated in ASP.NET Core 3.0 routing performance update.

* Synchronous data access: Many complex apps have database access as part of their routing. ASP.NET Core 2.2 or earlier routing might not provide the right extensibility points to support database access routing. For example, [IRouteConstraint](/en-us/dotnet/api/microsoft.aspnetcore.routing.irouteconstraint), and [IActionConstraint](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionconstraints.iactionconstraint) are synchronous. Extensibility points such as [MatcherPolicy](/en-us/dotnet/api/microsoft.aspnetcore.routing.matcherpolicy) and [EndpointSelectorContext](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointselectorcontext) are asynchronous.

## Guidance for library authors

This section contains guidance for library authors building on top of routing. These details are intended to ensure that app developers have a good experience using libraries and frameworks that extend routing.

### Define endpoints

To create a framework that uses routing for URL matching, start by defining a user experience that builds on top of [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints).

**DO** build on top of [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder). This allows users to compose your framework with other ASP.NET Core features without confusion. Every ASP.NET Core template includes routing. Assume routing is present and familiar for users.

```
app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...);

    endpoints.MapHealthChecks("/healthz");
});
```

**DO** return a sealed concrete type from a call to `MapMyFramework(...)` that implements [IEndpointConventionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iendpointconventionbuilder). Most framework `Map...` methods follow this pattern. The `IEndpointConventionBuilder` interface:

* Allows composability of metadata.
* Is targeted by a variety of extension methods.

Declaring your own type allows you to add your own framework-specific functionality to the builder. It's ok to wrap a framework-declared builder and forward calls to it.

```
app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization()
                                 .WithMyFrameworkFeature(awesome: true);

    endpoints.MapHealthChecks("/healthz");
});
```

**CONSIDER** writing your own [EndpointDataSource](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointdatasource). `EndpointDataSource` is the low-level primitive for declaring and updating a collection of endpoints. `EndpointDataSource` is a powerful API used by controllers and Razor Pages.

The routing tests have a [basic example](https://github.com/dotnet/AspNetCore/blob/main/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs#L17) of a non-updating data source.

**DO NOT** attempt to register an `EndpointDataSource` by default. Require users to register your framework in [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints). The philosophy of routing is that nothing is included by default, and that `UseEndpoints` is the place to register endpoints.

### Creating routing-integrated middleware

**CONSIDER** defining metadata types as an interface.

**DO** make it possible to use metadata types as an attribute on classes and methods.

```
public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}
```

Frameworks like controllers and Razor Pages support applying metadata attributes to types and methods. If you declare metadata types:

* Make them accessible as [attributes](/en-us/dotnet/csharp/programming-guide/concepts/attributes/).
* Most users are familiar with applying attributes.

Declaring a metadata type as an interface adds another layer of flexibility:

* Interfaces are composable.
* Developers can declare their own types that combine multiple policies.

**DO** make it possible to override metadata, as shown in the following example:

```
public interface ICoolMetadata
{
    bool IsCool { get; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => true;
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
    public bool IsCool => false;
}

[CoolMetadata]
public class MyController : Controller
{
    public void MyCool() { }

    [SuppressCoolMetadata]
    public void Uncool() { }
}
```

The best way to follow these guidelines is to avoid defining **marker metadata**:

* Don't just look for the presence of a metadata type.
* Define a property on the metadata and check the property.

The metadata collection is ordered and supports overriding by priority. In the case of controllers, metadata on the action method is most specific.

**DO** make middleware useful with and without routing.

```
app.UseRouting();

app.UseAuthorization(new AuthorizationPolicy() { ... });

app.UseEndpoints(endpoints =>
{
    // Your framework
    endpoints.MapMyFramework(...).RequireAuthorization();
});
```

As an example of this guideline, consider the authorization middleware. A configured fallback policy applies when the middleware can't produce a policy from authorization metadata, including:

* Endpoints without authorization metadata that produces a policy.
* Requests that don't match an endpoint.

This behavior makes the authorization middleware useful outside of routing and for traditional middleware programming. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

## Debug diagnostics

For detailed routing diagnostic output, set `Logging:LogLevel:Microsoft` to `Debug`. In the `Development` environment, set the log level in `appsettings.Development.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
```

## Related content

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/routing/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-19

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/host/generic-host.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# .NET Generic Host in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The ASP.NET Core templates create instances of [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) and [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication). These objects provide a streamlined way to configure and run web applications without a `Startup` class. For more information on `WebApplicationBuilder` and `WebApplication`, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../../migration/50-to-60?view=aspnetcore-10.0#new-hosting-model).

The ASP.NET Core templates create a .NET Generic Host ([HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder)) instance.

This article provides information on using the .NET Generic Host in ASP.NET Core. For information on using the .NET Generic Host in console apps, see [.NET Generic Host](/en-us/dotnet/core/extensions/generic-host).

## Understand the role of the host

A *host* is an object that encapsulates an application's resources, such as:

* Dependency injection (DI)
* Logging
* Configuration
* `IHostedService` implementations

When a host starts, it calls the [StartAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.startasync) method on each [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) instance registered in the service container's collection of hosted services. In a web app, one of the `IHostedService` implementations is a web service that starts an [HTTP server implementation](../?view=aspnetcore-10.0#servers).

By including all of the app's interdependent resources in a single object, the host enables control of application startup and graceful shutdown.

## Set up a host

The host is typically configured, built, and run by code in the *Program.cs* file.

The following code creates a host with an `IHostedService` implementation added to the DI container:

```
await Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<SampleHostedService>();
    })
    .Build()
    .RunAsync();
```

For an HTTP workload, call the [ConfigureWebHostDefaults](/en-us/dotnet/api/microsoft.extensions.hosting.generichostbuilderextensions.configurewebhostdefaults) method after the [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder) method:

```
await Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    })
    .Build()
    .RunAsync();
```

The `Main` method performs the following tasks:

* Calls a `CreateHostBuilder` method to create and configure a builder object.
* Calls the `Build` and `Run` methods on the builder object.

The ASP.NET Core web templates generate the following code to create a .NET Generic Host instance:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

The following code creates a Generic Host by using non-HTTP workload. The `IHostedService` implementation is added to the DI container:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
               services.AddHostedService<Worker>();
            });
}
```

For an HTTP workload, the ASP.NET Core templates generate the same `Main` method, but the `CreateHostBuilder` method calls the `ConfigureWebHostDefaults` method:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

If the app uses Entity Framework Core, don't change the name or signature of the `CreateHostBuilder` method. The [Entity Framework Core tools](/en-us/ef/core/cli/) expect to find a `CreateHostBuilder` method that configures the host without running the app. For more information, see [Design-time DbContext Creation](/en-us/ef/core/cli/dbcontext-creation).

## Configure default builder settings

The [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder) method performs the following tasks:

* Sets the [content root](../?view=aspnetcore-10.0#content-root) to the path returned by [GetCurrentDirectory](/en-us/dotnet/api/system.io.directory.getcurrentdirectory).

* Loads the host configuration from the following sources:

  * Environment variables prefixed with `DOTNET_`

* Adds the following [logging](../logging/?view=aspnetcore-10.0) providers:

  * Console
  * Debug
  * EventSource
  * EventLog (only when running on Windows)

* Enables [scope validation](../dependency-injection?view=aspnetcore-10.0#scope-validation) and [dependency validation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovideroptions.validateonbuild#microsoft-extensions-dependencyinjection-serviceprovideroptions-validateonbuild) when the environment is `Development`.

The [ConfigureWebHostDefaults](/en-us/dotnet/api/microsoft.extensions.hosting.generichostbuilderextensions.configurewebhostdefaults) method performs the following tasks:

* Loads host configuration from environment variables prefixed with `ASPNETCORE_`.

* Sets [Kestrel](../servers/kestrel?view=aspnetcore-10.0) server as the web server and configures it by using the app's hosting configuration providers. For the Kestrel server's default options, see [Configure options for the ASP.NET Core Kestrel web server](../servers/kestrel/options?view=aspnetcore-10.0).

* Adds [host-filtering middleware](../servers/kestrel/host-filtering?view=aspnetcore-10.0).

* Adds [Forwarded Headers middleware](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0#forwarded-headers) if the `ASPNETCORE_FORWARDEDHEADERS_ENABLED` property is set to `true`.

* Enables IIS integration. For the IIS default options, see [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0#iis-options).

For information on how to override default builder settings, see [Configure settings for all app types](#configure-settings-for-all-app-types) and [Configure settings for web apps](#configure-settings-for-web-apps) later in this article.

## Framework-provided services

The .NET Generic Host automatically registers the following services:

* [IHostApplicationLifetime](#ihostapplicationlifetime)
* [IHostLifetime](#ihostlifetime)
* [IHostEnvironment / IWebHostEnvironment](#ihostenvironment-iwebhostenvironment)

For more information on framework-provided services, see [Dependency injection in ASP.NET Core](../dependency-injection?view=aspnetcore-10.0#framework-provided-services).

### IHostApplicationLifetime

Inject the [IHostApplicationLifetime](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime) (formerly `IApplicationLifetime`) service into any class for handling post-startup and graceful shutdown tasks. Three properties on the interface are cancellation tokens used for registering app start and app stop event handler methods. The interface also includes a `StopApplication` method, which allows apps to request a graceful shutdown.

When performing a graceful shutdown, the host:

* Triggers the [ApplicationStopping](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime.applicationstopping) event handlers, which allows the app to run logic before the shutdown process begins.

* Stops the server, which disables new connections. The server waits for requests on existing connections to complete, for as long as the [shutdown timeout](#shutdown-timeout) allows. The server sends the connection close header for further requests on existing connections.

* Triggers the [ApplicationStopped](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime.applicationstopped) event handlers, which allows the app to run logic after the application has shutdown.

The following example is an `IHostedService` implementation that registers `IHostApplicationLifetime` event handlers:

```
public class HostApplicationLifetimeEventsHostedService : IHostedService
{
    private readonly IHostApplicationLifetime _hostApplicationLifetime;

    public HostApplicationLifetimeEventsHostedService(
        IHostApplicationLifetime hostApplicationLifetime)
        => _hostApplicationLifetime = hostApplicationLifetime;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
        _hostApplicationLifetime.ApplicationStopping.Register(OnStopping);
        _hostApplicationLifetime.ApplicationStopped.Register(OnStopped);

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;

    private void OnStarted()
    {
        // ...
    }

    private void OnStopping()
    {
        // ...
    }

    private void OnStopped()
    {
        // ...
    }
}
```

```
internal class LifetimeEventsHostedService : IHostedService
{
    private readonly ILogger _logger;
    private readonly IHostApplicationLifetime _appLifetime;

    public LifetimeEventsHostedService(
        ILogger<LifetimeEventsHostedService> logger, 
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;
        _appLifetime = appLifetime;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _appLifetime.ApplicationStarted.Register(OnStarted);
        _appLifetime.ApplicationStopping.Register(OnStopping);
        _appLifetime.ApplicationStopped.Register(OnStopped);

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    private void OnStarted()
    {
        _logger.LogInformation("OnStarted has been called.");

        // Perform post-startup activities here
    }

    private void OnStopping()
    {
        _logger.LogInformation("OnStopping has been called.");

        // Perform on-stopping activities here
    }

    private void OnStopped()
    {
        _logger.LogInformation("OnStopped has been called.");

        // Perform post-stopped activities here
    }
}
```

### IHostLifetime

The [IHostLifetime](/en-us/dotnet/api/microsoft.extensions.hosting.ihostlifetime) implementation controls when the host starts and when it stops. The last implementation registered is used.

`Microsoft.Extensions.Hosting.Internal.ConsoleLifetime` is the default `IHostLifetime` implementation.

The `ConsoleLifetime` method performs the following tasks:

* Listens for `Ctrl`+`C`/SIGINT (Windows), `Ctrl`+`C` (macOS), or SIGTERM and calls [StopApplication](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime.stopapplication), which starts the shutdown process.

* Unblocks extensions, such as by running the [RunAsync](#runasync) and [WaitForShutdownAsync](#waitforshutdownasync) methods.

### IHostEnvironment (IWebHostEnvironment)

Inject the [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment) service into a class to get information about the following settings:

* [ApplicationName](#application-name)
* [EnvironmentName](#environment-name)
* [ContentRootPath](#content-root)

Web apps implement the `IWebHostEnvironment` interface, which inherits `IHostEnvironment` and adds the [WebRootPath](#web-root).

## Set up host configuration

Host configuration is used for the properties of the [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment) implementation.

Host configuration is available from the [Configuration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuildercontext.configuration) property inside the [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration) method. After `ConfigureAppConfiguration`, `HostBuilderContext.Configuration` is replaced with the app config.

To add host configuration, call the [ConfigureHostConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configurehostconfiguration) method on the `IHostBuilder` instance. `ConfigureHostConfiguration` can be called multiple times with additive results. The host uses whichever option sets a value last on a given key.

`CreateDefaultBuilder` includes the environment variable provider with the prefix `DOTNET_` and command-line arguments. For web apps, the environment variable provider with prefix `ASPNETCORE_` is added. The prefix is removed when the environment variables are read. For example, the environment variable value for `ASPNETCORE_ENVIRONMENT` becomes the host configuration value for the `environment` key.

The following example creates host configuration:

```
Host.CreateDefaultBuilder(args)
    .ConfigureHostConfiguration(hostConfig =>
    {
        hostConfig.SetBasePath(Directory.GetCurrentDirectory());
        hostConfig.AddJsonFile("hostsettings.json", optional: true);
        hostConfig.AddEnvironmentVariables(prefix: "PREFIX_");
        hostConfig.AddCommandLine(args);
    });
```

```
// using Microsoft.Extensions.Configuration;

Host.CreateDefaultBuilder(args)
    .ConfigureHostConfiguration(configHost =>
    {
        configHost.SetBasePath(Directory.GetCurrentDirectory());
        configHost.AddJsonFile("hostsettings.json", optional: true);
        configHost.AddEnvironmentVariables(prefix: "PREFIX_");
        configHost.AddCommandLine(args);
    });
```

## Create the app configuration

App configuration is created by calling the [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration) method on the `IHostBuilder` instance. `ConfigureAppConfiguration` can be called multiple times with additive results. The app uses whichever option sets a value last on a given key.

The configuration created by `ConfigureAppConfiguration` is available in the [Configuration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuildercontext.configuration) property for subsequent operations and as a service from DI. The host configuration is also added to the app configuration.

For more information, see [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

## Configure settings for all app types

This section lists host settings that apply to both HTTP and non-HTTP workloads.

By default, environment variables used to configure these settings can have a `DOTNET_` or `ASPNETCORE_` prefix, which appear in the following list of settings as the `{PREFIX_}` placeholder.

For more information, see [Configure default builder settings](#configure-default-builder-settings) and [Configuration: Environment variables](../configuration/?view=aspnetcore-10.0#environment-variables-configuration-provider).

### Application name

Defines the name of the assembly that contains the entry point for the application.

**Key**: `applicationName`\
**Type**: *string*\
**Default**: The name of the assembly that has the app entry point.\
**Set using**: Environment variable\
**Environment variable**: `{PREFIX_}APPLICATIONNAME`

The [ApplicationName](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.applicationname) property is set from the host configuration during host construction.

### Content root

Determines where the host begins the search for content files.

**Key**: `contentRoot`\
**Type**: *string*\
**Default**: The folder where the app assembly resides.\
**Set using**: Environment variable or `UseContentRoot` on `IHostBuilder`\
**Environment variable**: `{PREFIX_}CONTENTROOT`

The [ContentRootPath](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath) property identifies where the host begins searching. If the path doesn't exist, the host fails to start.

```
Host.CreateDefaultBuilder(args)
    .UseContentRoot("/path/to/content/root")
    // ...
```

```
Host.CreateDefaultBuilder(args)
    .UseContentRoot("c:\\content-root")
    //...
```

For more information, see:

* [Fundamentals: Content root](../?view=aspnetcore-10.0#content-root)
* [Web root](#web-root)

### Environment name

Provides a name for the environment.

**Key**: `environment`\
**Type**: *string*\
**Default**: `Production`\
**Set using**: Environment variable or call `UseEnvironment` on `IHostBuilder`\
**Environment variable**: `{PREFIX_}ENVIRONMENT`

The [EnvironmentName](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.environmentname) property can be set to any value. Framework-defined values include `Development`, `Staging`, and `Production`. Values aren't case-sensitive.

```
Host.CreateDefaultBuilder(args)
    .UseEnvironment("Development")
    // ...
```

```
Host.CreateDefaultBuilder(args)
    .UseEnvironment("Development")
    //...
```

### Shutdown timeout

Specifies the amount of time to wait for the host to shut down.

**Key**: `shutdownTimeoutSeconds`\
**Type**: *int*\
**Default**: 30 seconds (In .NET 5.0 and earlier, the default is 5 seconds.)\
**Set using**: Environment variable or `HostOptions`\
**Environment variable**: `{PREFIX_}SHUTDOWNTIMEOUTSECONDS`

The [ShutdownTimeout](/en-us/dotnet/api/microsoft.extensions.hosting.hostoptions.shutdowntimeout) property sets the timeout for [StopAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihost.stopasync). The default value is 30 seconds.

During the timeout period, the host:

* Triggers [ApplicationStopping](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime.applicationstopping).
* Attempts to stop hosted services, logging errors for services that fail to stop.

If the timeout period expires before all hosted services stop, any remaining active services stop when the app shuts down. The services stop even if they're still processing. If services require more time to stop, increase the timeout.

The following example sets the timeout to 20 seconds:

```
Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.Configure<HostOptions>(options =>
        {
            options.ShutdownTimeout = TimeSpan.FromSeconds(20);
        });
    });
```

```
Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.Configure<HostOptions>(option =>
        {
            option.ShutdownTimeout = System.TimeSpan.FromSeconds(20);
        });
    });
```

### Reload config on change

Reloads the *appsettings.json* and *appsettings.{Environment}.json* files when the files change. This behavior is by default, as described in [Default app configuration sources](../configuration/?view=aspnetcore-10.0#default-app-configuration-sources).

**Key**: `hostBuilder:reloadConfigOnChange`\
**Type**: *bool* (`true` or `false`)\
**Default**: `true`\
**Set using**: Command-line argument `hostBuilder:reloadConfigOnChange`\
**Environment variable**: `{PREFIX_}hostBuilder:reloadConfigOnChange`

In .NET 5 and later, you can disable the reload behavior by setting the `hostBuilder:reloadConfigOnChange` argument to `false`.

Warning

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For more information, see [Environment variables](../configuration/?view=aspnetcore-10.0#environment-variables-configuration-provider).

## Configure settings for web apps

Some host settings apply only to HTTP workloads. By default, environment variables used to configure these settings can have a `DOTNET_` or `ASPNETCORE_` prefix, which appear in the following list of settings as the `{PREFIX_}` placeholder.

Extension methods on `IWebHostBuilder` are available for these settings. Code samples that show how to call the extension methods assume `webBuilder` is an instance of `IWebHostBuilder`, as in the following example:

```
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        // ...
    });
```

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.CaptureStartupErrors(true);
            webBuilder.UseStartup<Startup>();
        });
```

### Capture startup errors

Controls the capture of startup errors.

**Key**: `captureStartupErrors`\
**Type**: *bool* (`true`/`1` or `false`/`0`)\
**Default**: `false`. If the app runs with Kestrel behind IIS, the default is `true`.\
**Set using**: Configuration or `CaptureStartupErrors`\
**Environment variable**: `{PREFIX_}CAPTURESTARTUPERRORS`

When `false`, errors during startup result in the host exiting. When `true`, the host captures exceptions during startup and attempts to start the server.

```
webBuilder.CaptureStartupErrors(true);
```

```
webBuilder.CaptureStartupErrors(true);
```

### Detailed errors

Determines whether to capture detailed errors.

**Key**: `detailedErrors`\
**Type**: *bool* (`true`/`1` or `false`/`0`)\
**Default**: `false`\
**Set using**: Configuration or `UseSetting`\
**Environment variable**: `{PREFIX_}DETAILEDERRORS`

When enabled, or when the environment is set to `Development`, the app captures detailed errors.

```
webBuilder.UseSetting(WebHostDefaults.DetailedErrorsKey, "true");
```

```
webBuilder.UseSetting(WebHostDefaults.DetailedErrorsKey, "true");
```

### Hosting startup assemblies

Provides a semicolon-delimited string of hosting startup assemblies to load on startup.

**Key**: `hostingStartupAssemblies`\
**Type**: *string*\
**Default**: Empty string\
**Set using**: `UseSetting`\
**Environment variable**: `{PREFIX_}HOSTINGSTARTUPASSEMBLIES`

Although the configuration value defaults to an empty string, the hosting startup assemblies always include the app's assembly. When hosting startup assemblies are provided, they're added to the app's assembly for loading when the app builds its common services during startup.

```
webBuilder.UseSetting(
    WebHostDefaults.HostingStartupAssembliesKey, "assembly1;assembly2");
```

```
webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "assembly1;assembly2");
```

### Hosting startup exclude assemblies

Provides a semicolon-delimited string of hosting startup assemblies to exclude on startup.

**Key**: `hostingStartupExcludeAssemblies`\
**Type**: *string*\
**Default**: Empty string\
**Set using**: Configuration or `UseSetting`\
**Environment variable**: `{PREFIX_}HOSTINGSTARTUPEXCLUDEASSEMBLIES`

```
webBuilder.UseSetting(
    WebHostDefaults.HostingStartupExcludeAssembliesKey, "assembly1;assembly2");
```

```
webBuilder.UseSetting(WebHostDefaults.HostingStartupExcludeAssembliesKey, "assembly1;assembly2");
```

### HTTPS port

Sets the HTTPS port for redirection if you get a non-HTTPS connection.

**Key**: `https_port`\
**Type**: *string*\
**Default**: No default.\
**Set using**: Configuration or `UseSetting`\
**Environment variable**: `{PREFIX_}HTTPS_PORT`

This setting is used in [enforcing HTTPS](../../security/enforcing-ssl?view=aspnetcore-10.0). This setting doesn't cause the server to listen on the specified port. That is, it's possible to accidentally redirect requests to an unused port.

```
webBuilder.UseSetting("https_port", "8080");
```

```
webBuilder.UseSetting("https_port", "8080");
```

### HTTPS ports

Specifies the possible ports to listen on for HTTPS connections.

**Key**: `https_ports`\
**Type**: *string*\
**Default**: No default.\
**Set using**: Configuration or `UseSetting`\
**Environment variable**: `{PREFIX_}HTTPS_PORTS`

```
webBuilder.UseSetting("https_ports", "8080");
```

### Prefer hosting URLs

Indicates whether the host should listen on the URLs configured with the `IWebHostBuilder` instead of URLs configured with the `IServer` implementation.

**Key**: `preferHostingUrls`\
**Type**: *bool* (`true`/`1` or `false`/`0`)\
**Default**: `false`\
**Set using**: Environment variable or `PreferHostingUrls`\
**Environment variable**: `{PREFIX_}PREFERHOSTINGURLS`

```
webBuilder.PreferHostingUrls(true);
```

```
webBuilder.PreferHostingUrls(true);
```

### Prevent hosting startup

Prevents the automatic loading of hosting startup assemblies, including hosting startup assemblies configured by the app's assembly. For more information, see [Use hosting startup assemblies in ASP.NET Core](platform-specific-configuration?view=aspnetcore-10.0).

**Key**: `preventHostingStartup`\
**Type**: *bool* (`true`/`1` or `false`/`0`)\
**Default**: `false`\
**Set using**: Environment variable or `UseSetting`\
**Environment variable**: `{PREFIX_}PREVENTHOSTINGSTARTUP`

```
webBuilder.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true");
```

```
webBuilder.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true");
```

### Startup assembly

Specifies the assembly to search for the `Startup` class.

**Key**: `startupAssembly`\
**Type**: *string*\
**Default**: The application assembly.\
**Set using**: Environment variable or `UseStartup`\
**Environment variable**: `{PREFIX_}STARTUPASSEMBLY`

You can reference the assembly by name (`string`) or type (`TStartup`). If multiple `UseStartup` methods are called, the last call takes precedence.

```
webBuilder.UseStartup("StartupAssemblyName");
```

```
webBuilder.UseStartup<Startup>();
```

```
webBuilder.UseStartup("StartupAssemblyName");
```

```
webBuilder.UseStartup<Startup>();
```

### Suppress status messages

Indicates whether to suppress hosting startup status messages.

**Key**: `suppressStatusMessages`\
**Type**: *bool* (`true`/`1` or `false`/`0`)\
**Default**: `false`\
**Set using**: Configuration or `UseSetting`\
**Environment variable**: `{PREFIX_}SUPPRESSSTATUSMESSAGES`

```
webBuilder.UseSetting(WebHostDefaults.SuppressStatusMessagesKey, "true");
```

```
webBuilder.UseSetting(WebHostDefaults.SuppressStatusMessagesKey, "true");
```

### Server URLs

Indicates the IP addresses or host addresses with ports and protocols that the server should listen on for requests.

**Key**: `urls`\
**Type**: *string*\
**Default**: `http://localhost:5000` and `https://localhost:5001`\
**Set using**: Environment variable or `UseUrls`\
**Environment variable**: `{PREFIX_}URLS`

Set to a semicolon-separated `;` list of URL prefixes to which the server should respond. For example, `http://localhost:123`. Use a wildcard asterisk `*` to indicate that the server should listen for requests on any IP address or hostname by using the specified port and protocol (for example, `http://*:5000`). The protocol (`http://` or `https://`) must be included with each URL. Supported formats vary among servers.

```
webBuilder.UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002");
```

```
webBuilder.UseUrls("http://*:5000;http://localhost:5001;https://hostname:5002");
```

Kestrel has its own endpoint configuration API. For more information, see [Configure endpoints for Kestrel web server](../servers/kestrel/endpoints?view=aspnetcore-10.0).

### Web root

Sets the relative path to the app's static assets.

**Key**: `webroot`\
**Type**: *string*\
**Default**: `wwwroot`\
**Set using**: Environment variable or `UseWebRoot` on `IWebHostBuilder`\
**Environment variable**: `{PREFIX_}WEBROOT`

The [WebRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootpath#microsoft-aspnetcore-hosting-iwebhostenvironment-webrootpath) property determines the relative path to the app's static assets.

The path to `{content root}/wwwroot` must exist. If the path doesn't exist, a no-op file provider is used.

```
webBuilder.UseWebRoot("public");
```

```
webBuilder.UseWebRoot("public");
```

For more information, see:

* [Fundamentals: Web root](../?view=aspnetcore-10.0#web-root)
* [Content root](#content-root)

## Manage the host lifetime

To start and stop the application, call methods on the [IHost](/en-us/dotnet/api/microsoft.extensions.hosting.ihost) implementation. The methods affect all [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) implementations registered in the service container.

The difference between the `Run*` and `Start*` methods is that `Run*` methods wait for the host to complete before returning, whereas `Start*` methods return immediately. The `Run*` methods are typically used in console apps, whereas the `Start*` methods are typically used in long-running services.

### Run

The [Run](/en-us/dotnet/api/microsoft.extensions.hosting.hostingabstractionshostextensions.run) method runs the app and blocks the calling thread until the host is shut down.

### RunAsync

The [RunAsync](/en-us/dotnet/api/microsoft.extensions.hosting.hostingabstractionshostextensions.runasync) method runs the app and returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) object that completes when the cancellation token or shutdown is triggered.

### RunConsoleAsync

The [RunConsoleAsync](/en-us/dotnet/api/microsoft.extensions.hosting.hostinghostbuilderextensions.runconsoleasync) method enables console support, builds and starts the host, and waits for `Ctrl`+`C`/SIGINT (Windows), `Ctrl`+`C` (macOS), or SIGTERM to shut down.

### Start

The [Start](/en-us/dotnet/api/microsoft.extensions.hosting.hostingabstractionshostextensions.start) method launches the host synchronously.

### StartAsync

The [StartAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihost.startasync) method starts the host and returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) object that completes when the cancellation token or shutdown is triggered.

The [WaitForStartAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihostlifetime.waitforstartasync) method is called at the start of `StartAsync`, which waits until it's complete before continuing. This method can be used to delay startup until signaled by an external event.

### StopAsync

The [StopAsync](/en-us/dotnet/api/microsoft.extensions.hosting.hostingabstractionshostextensions.stopasync) method attempts to stop the host within the provided timeout.

### WaitForShutdown

The [WaitForShutdown](/en-us/dotnet/api/microsoft.extensions.hosting.hostingabstractionshostextensions.waitforshutdown) method blocks the calling thread until the IHostLifetime implementation triggers shutdown via `Ctrl`+`C`/SIGINT (Windows), `Ctrl`+`C` (macOS), or SIGTERM.

### WaitForShutdownAsync

The [WaitForShutdownAsync](/en-us/dotnet/api/microsoft.extensions.hosting.hostingabstractionshostextensions.waitforshutdownasync) method returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) object that completes when shutdown is triggered via the given token, and then it calls the [StopAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihost.stopasync) method.

### Control host lifetime

You can exercise direct control of the host lifetime by calling the following methods externally:

```
public class Program
{
    private IHost _host;

    public Program()
    {
        _host = new HostBuilder()
            .Build();
    }

    public async Task StartAsync()
    {
        _host.StartAsync();
    }

    public async Task StopAsync()
    {
        using (_host)
        {
            await _host.StopAsync(TimeSpan.FromSeconds(5));
        }
    }
}
```

## Related content

* [Background tasks with hosted services in ASP.NET Core](hosted-services?view=aspnetcore-10.0)
* [Generic Host source on GitHub](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting/src/Host.cs)

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/json-transcoding
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/json-transcoding.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC JSON transcoding in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

[gRPC](https://grpc.io) is a high-performance Remote Procedure Call (RPC) framework. gRPC uses HTTP/2, streaming, Protobuf, and message contracts to create high-performance, real-time services.

One limitation with gRPC is that not every platform can use it. Browsers don't fully support HTTP/2, making [REST APIs](https://www.redhat.com/topics/api/what-is-a-rest-api) and JSON the primary way to get data into browser apps. Despite the benefits that gRPC brings, REST APIs and JSON have an important place in modern apps. Building gRPC ***and*** JSON Web APIs adds unwanted overhead to app development.

This document discusses how to create JSON Web APIs using gRPC services.

## Overview

gRPC JSON transcoding is an extension for ASP.NET Core that creates RESTful JSON APIs for gRPC services. Once configured, transcoding allows apps to call gRPC services with familiar HTTP concepts:

* HTTP verbs
* URL parameter binding
* JSON requests/responses

gRPC can still be used to call services.

Note

gRPC JSON transcoding replaces [gRPC HTTP API](https://github.com/aspnet/AspLabs/tree/main/src/GrpcHttpApi), an alternative experimental extension.

## Usage

1. Add a package reference to [`Microsoft.AspNetCore.Grpc.JsonTranscoding`](https://www.nuget.org/packages/Microsoft.AspNetCore.Grpc.JsonTranscoding).

2. Register transcoding in server startup code by adding `AddJsonTranscoding`: In the `Program.cs` file, change `builder.Services.AddGrpc();` to `builder.Services.AddGrpc().AddJsonTranscoding();`.

3. Add `<IncludeHttpRuleProtos>true</IncludeHttpRuleProtos>` to the property group in the project file (`.csproj`):

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

     <PropertyGroup>
       <TargetFramework>net8.0</TargetFramework>
       <Nullable>enable</Nullable>
       <ImplicitUsings>enable</ImplicitUsings>
       <InvariantGlobalization>true</InvariantGlobalization>
       <IncludeHttpRuleProtos>true</IncludeHttpRuleProtos>
     </PropertyGroup>
   ```

4. Annotate gRPC methods in your `.proto` files with HTTP bindings and routes:

   ```
   syntax = "proto3";

   option csharp_namespace = "GrpcServiceTranscoding";
   import "google/api/annotations.proto";

   package greet;

   // The greeting service definition.
   service Greeter {
     rpc SayHello (HelloRequest) returns (HelloReply) {
       option (google.api.http) = {
         get: "/v1/greeter/{name}"
       };
     }
   }

   // The request message containing the user's name.
   message HelloRequest {
     string name = 1;
   }

   // The response message containing the greetings.
   message HelloReply {
     string message = 1;
   }
   ```

The `SayHello` gRPC method can now be invoked as gRPC and as a JSON Web API:

* Request: `GET /v1/greeter/world`
* Response: `{ "message": "Hello world" }`

If the server is configured to write logs for each request, server logs show that a gRPC service executes the HTTP call. Transcoding maps the incoming HTTP request to a gRPC message and converts the response message to JSON.

```
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET https://localhost:5001/v1/greeter/world
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /v1/greeter/{name}'
info: Server.GreeterService[0]
      Sending hello to world
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /v1/greeter/{name}'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 1.996ms 200 application/json
```

### Annotate gRPC methods

gRPC methods must be annotated with an HTTP rule before they support transcoding. The HTTP rule includes information about how to call the gRPC method, such as the HTTP method and route.

```
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      get: "/v1/greeter/{name}"
    };
  }
}
```

The proceeding example:

* Defines a `Greeter` service with a `SayHello` method. The method has an HTTP rule specified using the name `google.api.http`.
* The method is accessible with `GET` requests and the `/v1/greeter/{name}` route.
* The `name` field on the request message is bound to a route parameter.

Many options are available for customizing how a gRPC method binds to a RESTful API. For more information about annotating gRPC methods and customizing JSON, see [Configure HTTP and JSON for gRPC JSON transcoding](json-transcoding-binding?view=aspnetcore-10.0).

### Streaming methods

Traditional gRPC over HTTP/2 supports streaming in all directions. Transcoding is limited to server streaming only. Client streaming and bidirectional streaming methods aren't supported.

Server streaming methods use [line-delimited JSON](https://wikipedia.org/wiki/JSON_streaming#Line-delimited_JSON). Each message written using `WriteAsync` is serialized to JSON and followed by a new line.

The following server streaming method writes three messages:

```
public override async Task StreamingFromServer(ExampleRequest request,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    for (var i = 1; i <= 3; i++)
    {
        await responseStream.WriteAsync(new ExampleResponse { Text = $"Message {i}" });
        await Task.Delay(TimeSpan.FromSeconds(1));
    }
}
```

The client receives three line-delimited JSON objects:

```
{"Text":"Message 1"}
{"Text":"Message 2"}
{"Text":"Message 3"}
```

Note that the `WriteIndented` JSON setting doesn't apply to server streaming methods. Pretty printing adds new lines and whitespace to JSON, which can't be used with line-delimited JSON.

View or download [an ASP.NET Core gPRC transcoding and streaming app sample](https://github.com/grpc/grpc-dotnet/tree/master/examples/Transcoder).

## HTTP protocol

The ASP.NET Core gRPC service template, included in the .NET SDK, creates an app that's only configured for HTTP/2. HTTP/2 is a good default when an app only supports traditional gRPC over HTTP/2. Transcoding, however, works with both HTTP/1.1 and HTTP/2. Some platforms, such as UWP or Unity, can't use HTTP/2. To support all client apps, configure the server to enable HTTP/1.1 and HTTP/2.

Update the default protocol in `appsettings.json`:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1AndHttp2"
    }
  }
}
```

Alternatively, [configure Kestrel endpoints in startup code](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Enabling HTTP/1.1 and HTTP/2 on the same port requires TLS for protocol negotiation. For more information about configuring HTTP protocols in a gRPC app, see [ASP.NET Core gRPC protocol negotiation](aspnetcore?view=aspnetcore-10.0#protocol-negotiation).

## gRPC JSON transcoding vs gRPC-Web

Both transcoding and gRPC-Web allow gRPC services to be called from a browser. However, the way each does this is different:

* gRPC-Web lets browser apps call gRPC services from the browser with the gRPC-Web client and Protobuf. gRPC-Web requires the browser app to generate a gRPC client and has the advantage of sending small, fast Protobuf messages.
* Transcoding allows browser apps to call gRPC services as if they were RESTful APIs with JSON. The browser app doesn't need to generate a gRPC client or know anything about gRPC.

The previous `Greeter` service can be called using browser JavaScript APIs:

```
var name = nameInput.value;

fetch('/v1/greeter/' + name)
  .then((response) => response.json())
  .then((result) => {
    console.log(result.message);
    // Hello world
  });
```

## grpc-gateway

[grpc-gateway](https://grpc-ecosystem.github.io/grpc-gateway/) is another technology for creating RESTful JSON APIs from gRPC services. It uses the same `.proto` annotations to map HTTP concepts to gRPC services.

grpc-gateway uses code generation to create a reverse-proxy server. The reverse proxy translates RESTful calls into gRPC+Protobuf and sends the calls over HTTP/2 to the gRPC service. The benefit of this approach is the gRPC service doesn't know about the RESTful JSON APIs. Any gRPC server can use grpc-gateway.

Meanwhile, gRPC JSON transcoding runs inside an ASP.NET Core app. It deserializes JSON into Protobuf messages, then invokes the gRPC service directly. Transcoding in ASP.NET Core offers advantages to .NET app developers:

* Less complex: Both gRPC services and mapped RESTful JSON API run out of one ASP.NET Core app.
* Better performance: Transcoding deserializes JSON to Protobuf messages and invokes the gRPC service directly. There are significant performance benefits in doing this in-process versus making a new gRPC call to a different server.
* Lower cost: Fewer servers result in a smaller monthly hosting bill.

For installation and usage of grpc-gateway, see the [grpc-gateway README](https://github.com/grpc-ecosystem/grpc-gateway/#grpc-gateway).

## Related content

* [Configure HTTP and JSON for gRPC JSON transcoding ASP.NET Core apps](json-transcoding-binding?view=aspnetcore-10.0)
* [Use gRPC in browser apps](browser?view=aspnetcore-10.0)
* [gRPC-Web in ASP.NET Core gRPC apps](grpcweb?view=aspnetcore-10.0)

[gRPC](https://grpc.io) is a high-performance Remote Procedure Call (RPC) framework. gRPC uses HTTP/2, streaming, Protobuf, and message contracts to create high-performance, real-time services.

One limitation with gRPC is that not every platform can use it. Browsers don't fully support HTTP/2, making [REST APIs](https://www.redhat.com/topics/api/what-is-a-rest-api) and JSON the primary way to get data into browser apps. Despite the benefits that gRPC brings, REST APIs and JSON have an important place in modern apps. Building gRPC ***and*** JSON Web APIs adds unwanted overhead to app development.

This document discusses how to create JSON Web APIs using gRPC services.

## Overview

gRPC JSON transcoding is an extension for ASP.NET Core that creates RESTful JSON APIs for gRPC services. Once configured, transcoding allows apps to call gRPC services with familiar HTTP concepts:

* HTTP verbs
* URL parameter binding
* JSON requests/responses

gRPC can still be used to call services.

Note

gRPC JSON transcoding replaces [gRPC HTTP API](https://github.com/aspnet/AspLabs/tree/main/src/GrpcHttpApi), an alternative experimental extension.

## Usage

1. Add a package reference to [`Microsoft.AspNetCore.Grpc.JsonTranscoding`](https://www.nuget.org/packages/Microsoft.AspNetCore.Grpc.JsonTranscoding).
2. Register transcoding in server startup code by adding `AddJsonTranscoding`: In the `Program.cs` file, change `builder.Services.AddGrpc();` to `builder.Services.AddGrpc().AddJsonTranscoding();`.
3. Create the directory structure `/google/api` in the project directory that contains the `.csproj` file.
4. Add [`google/api/http.proto`](https://github.com/dotnet/aspnetcore/blob/main/src/Grpc/JsonTranscoding/test/testassets/Sandbox/google/api/http.proto) and [`google/api/annotations.proto`](https://github.com/dotnet/aspnetcore/blob/main/src/Grpc/JsonTranscoding/test/testassets/Sandbox/google/api/annotations.proto) files to the `/google/api` directory.
5. Annotate gRPC methods in your `.proto` files with HTTP bindings and routes:

```
syntax = "proto3";

option csharp_namespace = "GrpcServiceTranscoding";
import "google/api/annotations.proto";

package greet;

// The greeting service definition.
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      get: "/v1/greeter/{name}"
    };
  }
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}
```

The `SayHello` gRPC method can now be invoked as gRPC and as a JSON Web API:

* Request: `GET /v1/greeter/world`
* Response: `{ "message": "Hello world" }`

If the server is configured to write logs for each request, server logs show that a gRPC service executes the HTTP call. Transcoding maps the incoming HTTP request to a gRPC message and converts the response message to JSON.

```
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET https://localhost:5001/v1/greeter/world
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /v1/greeter/{name}'
info: Server.GreeterService[0]
      Sending hello to world
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /v1/greeter/{name}'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 1.996ms 200 application/json
```

### Annotate gRPC methods

gRPC methods must be annotated with an HTTP rule before they support transcoding. The HTTP rule includes information about how to call the gRPC method, such as the HTTP method and route.

```
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      get: "/v1/greeter/{name}"
    };
  }
}
```

The proceeding example:

* Defines a `Greeter` service with a `SayHello` method. The method has an HTTP rule specified using the name `google.api.http`.
* The method is accessible with `GET` requests and the `/v1/greeter/{name}` route.
* The `name` field on the request message is bound to a route parameter.

Many options are available for customizing how a gRPC method binds to a RESTful API. For more information about annotating gRPC methods and customizing JSON, see [Configure HTTP and JSON for gRPC JSON transcoding](json-transcoding-binding?view=aspnetcore-10.0).

### Streaming methods

Traditional gRPC over HTTP/2 supports streaming in all directions. Transcoding is limited to server streaming only. Client streaming and bidirectional streaming methods aren't supported.

Server streaming methods use [line-delimited JSON](https://wikipedia.org/wiki/JSON_streaming#Line-delimited_JSON). Each message written using `WriteAsync` is serialized to JSON and followed by a new line.

The following server streaming method writes three messages:

```
public override async Task StreamingFromServer(ExampleRequest request,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    for (var i = 1; i <= 3; i++)
    {
        await responseStream.WriteAsync(new ExampleResponse { Text = $"Message {i}" });
        await Task.Delay(TimeSpan.FromSeconds(1));
    }
}
```

The client receives three line-delimited JSON objects:

```
{"Text":"Message 1"}
{"Text":"Message 2"}
{"Text":"Message 3"}
```

Note that the `WriteIndented` JSON setting doesn't apply to server streaming methods. Pretty printing adds new lines and whitespace to JSON, which can't be used with line-delimited JSON.

View or download [an ASP.NET Core gPRC transcoding and streaming app sample](https://github.com/grpc/grpc-dotnet/tree/master/examples/Transcoder).

## HTTP protocol

The ASP.NET Core gRPC service template, included in the .NET SDK, creates an app that's only configured for HTTP/2. HTTP/2 is a good default when an app only supports traditional gRPC over HTTP/2. Transcoding, however, works with both HTTP/1.1 and HTTP/2. Some platforms, such as UWP or Unity, can't use HTTP/2. To support all client apps, configure the server to enable HTTP/1.1 and HTTP/2.

Update the default protocol in `appsettings.json`:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1AndHttp2"
    }
  }
}
```

Alternatively, [configure Kestrel endpoints in startup code](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Enabling HTTP/1.1 and HTTP/2 on the same port requires TLS for protocol negotiation. For more information about configuring HTTP protocols in a gRPC app, see [ASP.NET Core gRPC protocol negotiation](aspnetcore?view=aspnetcore-10.0#protocol-negotiation).

## gRPC JSON transcoding vs gRPC-Web

Both transcoding and gRPC-Web allow gRPC services to be called from a browser. However, the way each does this is different:

* gRPC-Web lets browser apps call gRPC services from the browser with the gRPC-Web client and Protobuf. gRPC-Web requires the browser app to generate a gRPC client and has the advantage of sending small, fast Protobuf messages.
* Transcoding allows browser apps to call gRPC services as if they were RESTful APIs with JSON. The browser app doesn't need to generate a gRPC client or know anything about gRPC.

The previous `Greeter` service can be called using browser JavaScript APIs:

```
var name = nameInput.value;

fetch('/v1/greeter/' + name)
  .then((response) => response.json())
  .then((result) => {
    console.log(result.message);
    // Hello world
  });
```

## grpc-gateway

[grpc-gateway](https://grpc-ecosystem.github.io/grpc-gateway/) is another technology for creating RESTful JSON APIs from gRPC services. It uses the same `.proto` annotations to map HTTP concepts to gRPC services.

grpc-gateway uses code generation to create a reverse-proxy server. The reverse proxy translates RESTful calls into gRPC+Protobuf and sends the calls over HTTP/2 to the gRPC service. The benefit of this approach is the gRPC service doesn't know about the RESTful JSON APIs. Any gRPC server can use grpc-gateway.

Meanwhile, gRPC JSON transcoding runs inside an ASP.NET Core app. It deserializes JSON into Protobuf messages, then invokes the gRPC service directly. Transcoding in ASP.NET Core offers advantages to .NET app developers:

* Less complex: Both gRPC services and mapped RESTful JSON API run out of one ASP.NET Core app.
* Better performance: Transcoding deserializes JSON to Protobuf messages and invokes the gRPC service directly. There are significant performance benefits in doing this in-process versus making a new gRPC call to a different server.
* Lower cost: Fewer servers result in a smaller monthly hosting bill.

For installation and usage of grpc-gateway, see the [grpc-gateway README](https://github.com/grpc-ecosystem/grpc-gateway/#grpc-gateway).

## Related content

* [Configure HTTP and JSON for gRPC JSON transcoding ASP.NET Core apps](json-transcoding-binding?view=aspnetcore-10.0)
* [Use gRPC in browser apps](browser?view=aspnetcore-10.0)
* [gRPC-Web in ASP.NET Core gRPC apps](grpcweb?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0009
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0009.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0009: Do not use Configure with WebApplicationBuilder.WebHost

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0009      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

`Configure` can't be used with the `WebHost` property on `WebApplicationBuilder`.

## Rule description

The `WebApplicationBuilder` doesn't support configuring the `WebHost` before build using the `Configure` extension method.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.Configure(webHostBuilder => {
    webHostBuilder.UseContentRootPath(Path.Combine(Directory.GetCurrentDirectory(), "myContentRoot"));
});

var app = builder.Build();

app.Run();
```

## How to fix violations

To fix a violation of this rule, configure the `WebHost` directly on the `WebApplicationBuilder`. For example, instead of setting the content root path via `Configure`.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.Configure(webHostBuilder =>
{
    webHostBuilder.UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "myContentRoot"));
});

var app = builder.Build();

app.Run();
```

Configure the content root path directly on the `WebApplicationBuilder`.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseContentRoot(Path.Combine(Directory.GetCurrentDirectory(), "myContentRoot"));

var app = builder.Build();

app.Run();
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule. A misconfigured application can result in unexpected behavior at runtime.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/persist-component-state
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/persist-component-state.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Persist Component State Tag Helper in ASP.NET Core

The Persist Component State Tag Helper saves the state of non-routable Razor components rendered in a page or view of a Razor Pages or MVC app.

## Prerequisites

Follow the guidance in the *Use non-routable components in pages or views* section of the [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../../../blazor/components/integration?view=aspnetcore-10.0#use-non-routable-components-in-pages-or-views) article.

Follow the guidance in the *Configuration* section for either:

* [Blazor WebAssembly](../../../../blazor/components/integration?view=aspnetcore-10.0\&pivots=webassembly)
* [Blazor Server](../../../../blazor/components/integration?view=aspnetcore-10.0\&pivots=server)

## Persist state for prerendered components

To persist state for prerendered components, use the [Persist Component State Tag Helper](persist-component-state?view=aspnetcore-10.0) ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs)). Add the Tag Helper's tag, `<persist-component-state />`, inside the closing `</body>` tag of the layout in an app that prerenders components.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

In `Pages/Shared/_Layout.cshtml` for embedded components that are either WebAssembly prerendered (`WebAssemblyPrerendered`) or server prerendered (`ServerPrerendered`):

```
<body>
    ...

    <persist-component-state />
</body>
```

Decide what state to persist using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service. [`PersistentComponentState.RegisterOnPersisting`](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) registers a callback to persist the component state before the app is paused. The state is retrieved when the application resumes.

For more information and examples, see [ASP.NET Core Blazor prerendered state persistence](../../../../blazor/state-management/prerendered-state-persistence?view=aspnetcore-10.0).

To persist state for prerendered components, use the [Persist Component State Tag Helper](persist-component-state?view=aspnetcore-10.0) ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs)). Add the Tag Helper's tag, `<persist-component-state />`, inside the closing `</body>` tag of the `_Host` page in an app that prerenders components.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

In `Pages/_Host.cshtml` of Blazor apps that are either WebAssembly prerendered (`WebAssemblyPrerendered`) in a hosted Blazor WebAssembly app or `ServerPrerendered` in a Blazor Server app:

```
<body>
    ...

    <persist-component-state />
</body>
```

Decide what state to persist using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service. [`PersistentComponentState.RegisterOnPersisting`](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) registers a callback to persist the component state before the app is paused. The state is retrieved when the application resumes.

For more information and examples, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../../../blazor/components/integration?view=aspnetcore-10.0#persist-prerendered-state).

## Related content

* [Component Tag Helper in ASP.NET Core](component-tag-helper?view=aspnetcore-10.0)
* [Prerender ASP.NET Core Razor components](../../../../blazor/components/prerender?view=aspnetcore-10.0)
* [ComponentTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper)
* [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0)
* [ASP.NET Core Razor components](../../../../blazor/components/?view=aspnetcore-10.0)

- [Component Tag Helper in ASP.NET Core](component-tag-helper?view=aspnetcore-10.0)
- [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../../../blazor/components/integration?view=aspnetcore-10.0)
- [ComponentTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper)
- [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0)
- [ASP.NET Core Razor components](../../../../blazor/components/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/objectpool
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/ObjectPool.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Object reuse with ObjectPool in ASP.NET Core

By [Günther Foidl](https://github.com/gfoidl), [Steve Gordon](https://twitter.com/stevejgordon), and [Samson Amaugo](https://github.com/sammychinedu2ky)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[Microsoft.Extensions.ObjectPool](/en-us/dotnet/api/microsoft.extensions.objectpool) is part of the ASP.NET Core infrastructure that supports keeping a group of objects in memory for reuse rather than allowing the objects to be garbage collected. All the static and instance methods in `Microsoft.Extensions.ObjectPool` are thread-safe.

Apps might want to use the object pool if the objects that are being managed are:

* Expensive to allocate/initialize.
* Represent a limited resource.
* Used predictably and frequently.

For example, the ASP.NET Core framework uses the object pool in some places to reuse [StringBuilder](/en-us/dotnet/api/system.text.stringbuilder) instances. `StringBuilder` allocates and manages its own buffers to hold character data. ASP.NET Core regularly uses `StringBuilder` to implement features, and reusing them provides a performance benefit.

Object pooling doesn't always improve performance:

* Unless the initialization cost of an object is high, it's usually slower to get the object from the pool.
* Objects managed by the pool aren't de-allocated until the pool is de-allocated.

Use object pooling only after collecting performance data using realistic scenarios for your app or library.

**NOTE: The ObjectPool doesn't place a limit on the number of objects that it allocates, it places a limit on the number of objects it retains.**

## ObjectPool concepts

When [DefaultObjectPoolProvider](/en-us/dotnet/api/microsoft.extensions.objectpool.defaultobjectpoolprovider) is used and `T` implements `IDisposable`:

* Items that are ***not*** returned to the pool will be disposed.
* When the pool gets disposed by DI, all items in the pool are disposed.

NOTE: After the pool is disposed:

* Calling `Get` throws an `ObjectDisposedException`.
* Calling `Return` disposes the given item.

Important `ObjectPool` types and interfaces:

* [ObjectPool\<T>](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1) : The basic object pool abstraction. Used to get and return objects.
* [PooledObjectPolicy\<T>](/en-us/dotnet/api/microsoft.extensions.objectpool.pooledobjectpolicy-1) : Implement this to customize how an object is created and how it's reset when returned to the pool. This can be passed into an object pool that's constructed directly.
* [IResettable](/en-us/dotnet/api/microsoft.extensions.objectpool.iresettable) : Automatically resets the object when returned to an object pool.

The ObjectPool can be used in an app in multiple ways:

* Instantiating a pool.
* Registering a pool in [Dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) as an instance.
* Registering the `ObjectPoolProvider<>` in DI and using it as a factory.

## How to use ObjectPool

Call [Get](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1.get) to get an object and [Return](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1.return) to return the object. There's no requirement to return every object. If an object isn't returned, it will be garbage collected.

## ObjectPool sample

The following code:

* Adds `ObjectPoolProvider` to the [Dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) container.
* Implements the `IResettable` interface to automatically clear the contents of the buffer when returned to the object pool.

```
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
using System.Security.Cryptography;

var builder = WebApplication.CreateBuilder(args);

builder.Services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

builder.Services.TryAddSingleton<ObjectPool<ReusableBuffer>>(serviceProvider =>
{
    var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
    var policy = new DefaultPooledObjectPolicy<ReusableBuffer>();
    return provider.Create(policy);
});

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

// return the SHA256 hash of a word 
// https://localhost:7214/hash/SamsonAmaugo
app.MapGet("/hash/{name}", (string name, ObjectPool<ReusableBuffer> bufferPool) =>
{

    var buffer = bufferPool.Get();
    try
    {
        // Set the buffer data to the ASCII values of a word
        for (var i = 0; i < name.Length; i++)
        {
            buffer.Data[i] = (byte)name[i];
        }

        Span<byte> hash = stackalloc byte[32];
        SHA256.HashData(buffer.Data.AsSpan(0, name.Length), hash);
        return "Hash: " + Convert.ToHexString(hash);
    }
    finally
    {
        // Data is automatically reset because this type implemented IResettable
        bufferPool.Return(buffer); 
    }
});
app.Run();

public class ReusableBuffer : IResettable
{
    public byte[] Data { get; } = new byte[1024 * 1024]; // 1 MB

    public bool TryReset()
    {
        Array.Clear(Data);
        return true;
    }
}
```

**NOTE:** When the pooled type `T` doesn't implement `IResettable`, then a custom `PooledObjectPolicy<T>` can be used to reset the state of the objects before they are returned to the pool.

[Microsoft.Extensions.ObjectPool](/en-us/dotnet/api/microsoft.extensions.objectpool) is part of the ASP.NET Core infrastructure that supports keeping a group of objects in memory for reuse rather than allowing the objects to be garbage collected. All the static and instance methods in `Microsoft.Extensions.ObjectPool` are thread-safe.

Apps might want to use the object pool if the objects that are being managed are:

* Expensive to allocate/initialize.
* Represent a limited resource.
* Used predictably and frequently.

For example, the ASP.NET Core framework uses the object pool in some places to reuse [StringBuilder](/en-us/dotnet/api/system.text.stringbuilder) instances. `StringBuilder` allocates and manages its own buffers to hold character data. ASP.NET Core regularly uses `StringBuilder` to implement features, and reusing them provides a performance benefit.

Object pooling doesn't always improve performance:

* Unless the initialization cost of an object is high, it's usually slower to get the object from the pool.
* Objects managed by the pool aren't de-allocated until the pool is de-allocated.

Use object pooling only after collecting performance data using realistic scenarios for your app or library.

**NOTE: The ObjectPool doesn't place a limit on the number of objects that it allocates, it places a limit on the number of objects it retains.**

## Concepts

When [DefaultObjectPoolProvider](/en-us/dotnet/api/microsoft.extensions.objectpool.defaultobjectpoolprovider) is used and `T` implements `IDisposable`:

* Items that are ***not*** returned to the pool will be disposed.
* When the pool gets disposed by DI, all items in the pool are disposed.

NOTE: After the pool is disposed:

* Calling `Get` throws an `ObjectDisposedException`.
* Calling `Return` disposes the given item.

Important `ObjectPool` types and interfaces:

* [ObjectPool\<T>](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1) : The basic object pool abstraction. Used to get and return objects.
* [PooledObjectPolicy\<T>](/en-us/dotnet/api/microsoft.extensions.objectpool.pooledobjectpolicy-1) : Implement this to customize how an object is created and how it is reset when returned to the pool. This can be passed into an object pool that is construct directly, or
* [Create](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpoolprovider.create) : Acts as a factory for creating object pools.
* [IResettable](/en-us/dotnet/api/microsoft.extensions.objectpool.iresettable): Automatically resets the object when returned to an object pool.

The ObjectPool can be used in an app in multiple ways:

* Instantiating a pool.
* Registering a pool in [Dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) as an instance.
* Registering the `ObjectPoolProvider<>` in DI and using it as a factory.

## How to use ObjectPool

Call [Get](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1.get) to get an object and [Return](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1.return) to return the object. There's no requirement that you return every object. If you don't return an object, it will be garbage collected.

## ObjectPool sample

The following code:

* Adds `ObjectPoolProvider` to the [Dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) container.
* Adds and configures `ObjectPool<StringBuilder>` to the DI container.
* Adds the `BirthdayMiddleware`.

```
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
using ObjectPoolSample;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

builder.Services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
builder.Services.TryAddSingleton<ObjectPool<StringBuilder>>(serviceProvider =>
{
    var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
    var policy = new Microsoft.Extensions.ObjectPool.StringBuilderPooledObjectPolicy();
    return provider.Create(policy);
});

builder.Services.AddWebEncoders();

var app = builder.Build();

// Test using /?firstname=Steve&lastName=Gordon&day=28&month=9
app.UseMiddleware<BirthdayMiddleware>();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The following code implements `BirthdayMiddleware`

```
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.Extensions.ObjectPool;

namespace ObjectPoolSample;

public class BirthdayMiddleware
{
    private readonly RequestDelegate _next;

    public BirthdayMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, 
                                  ObjectPool<StringBuilder> builderPool)
    {
        if (context.Request.Query.TryGetValue("firstName", out var firstName) &&
            context.Request.Query.TryGetValue("lastName", out var lastName) && 
            context.Request.Query.TryGetValue("month", out var month) &&                 
            context.Request.Query.TryGetValue("day", out var day) &&
            int.TryParse(month, out var monthOfYear) &&
            int.TryParse(day, out var dayOfMonth))
        {                
            var now = DateTime.UtcNow; // Ignoring timezones.

            // Request a StringBuilder from the pool.
            var stringBuilder = builderPool.Get();

            try
            {
                stringBuilder.Append("Hi ")
                    .Append(firstName).Append(" ").Append(lastName).Append(". ");

                var encoder = context.RequestServices.GetRequiredService<HtmlEncoder>();

                if (now.Day == dayOfMonth && now.Month == monthOfYear)
                {
                    stringBuilder.Append("Happy birthday!!!");

                    var html = encoder.Encode(stringBuilder.ToString());
                    await context.Response.WriteAsync(html);
                }
                else
                {
                    var thisYearsBirthday = new DateTime(now.Year, monthOfYear, 
                                                                    dayOfMonth);

                    int daysUntilBirthday = thisYearsBirthday > now 
                        ? (thisYearsBirthday - now).Days 
                        : (thisYearsBirthday.AddYears(1) - now).Days;

                    stringBuilder.Append("There are ")
                        .Append(daysUntilBirthday).Append(" days until your birthday!");

                    var html = encoder.Encode(stringBuilder.ToString());
                    await context.Response.WriteAsync(html);
                }
            }
            finally // Ensure this runs even if the main code throws.
            {
                // Return the StringBuilder to the pool.
                builderPool.Return(stringBuilder); 
            }

            return;
        }

        await _next(context);
    }
}
```

[Microsoft.Extensions.ObjectPool](/en-us/dotnet/api/microsoft.extensions.objectpool) is part of the ASP.NET Core infrastructure that supports keeping a group of objects in memory for reuse rather than allowing the objects to be garbage collected.

You might want to use the object pool if the objects that are being managed are:

* Expensive to allocate/initialize.
* Represent some limited resource.
* Used predictably and frequently.

For example, the ASP.NET Core framework uses the object pool in some places to reuse [StringBuilder](/en-us/dotnet/api/system.text.stringbuilder) instances. `StringBuilder` allocates and manages its own buffers to hold character data. ASP.NET Core regularly uses `StringBuilder` to implement features, and reusing them provides a performance benefit.

Object pooling doesn't always improve performance:

* Unless the initialization cost of an object is high, it's usually slower to get the object from the pool.
* Objects managed by the pool aren't de-allocated until the pool is de-allocated.

Use object pooling only after collecting performance data using realistic scenarios for your app or library.

**WARNING: The `ObjectPool` doesn't implement `IDisposable`. We don't recommend using it with types that need disposal.** `ObjectPool` in ASP.NET Core 3.0 or later supports `IDisposable`.

**NOTE: The ObjectPool doesn't place a limit on the number of objects that it will allocate, it places a limit on the number of objects it will retain.**

## Concepts

[ObjectPool\<T>](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1) - the basic object pool abstraction. Used to get and return objects.

[PooledObjectPolicy\<T>](/en-us/dotnet/api/microsoft.extensions.objectpool.pooledobjectpolicy-1) - implement this to customize how an object is created and how it is *reset* when returned to the pool. This can be passed into an object pool that you construct directly.... OR

[Create](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpoolprovider.create) acts as a factory for creating object pools.

The ObjectPool can be used in an app in multiple ways:

* Instantiating a pool.
* Registering a pool in [Dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) as an instance.
* Registering the `ObjectPoolProvider<>` in DI and using it as a factory.

## How to use ObjectPool

Call [Get](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1.get) to get an object and [Return](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1.return) to return the object. There's no requirement that you return every object. If you don't return an object, it will be garbage collected.

## ObjectPool sample

The following code:

* Adds `ObjectPoolProvider` to the [Dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) container.
* Adds and configures `ObjectPool<StringBuilder>` to the DI container.
* Adds the `BirthdayMiddleware`.

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

        services.TryAddSingleton<ObjectPool<StringBuilder>>(serviceProvider =>
        {
            var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
            var policy = new StringBuilderPooledObjectPolicy();
            return provider.Create(policy);
        });

        services.AddWebEncoders();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        
        // Test using /?firstname=Steve&lastName=Gordon&day=28&month=9
        app.UseMiddleware<BirthdayMiddleware>(); 
    }
}
```

The following code implements `BirthdayMiddleware`

```
public class BirthdayMiddleware
{
    private readonly RequestDelegate _next;

    public BirthdayMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, 
                                  ObjectPool<StringBuilder> builderPool)
    {
        if (context.Request.Query.TryGetValue("firstName", out var firstName) &&
            context.Request.Query.TryGetValue("lastName", out var lastName) && 
            context.Request.Query.TryGetValue("month", out var month) &&                 
            context.Request.Query.TryGetValue("day", out var day) &&
            int.TryParse(month, out var monthOfYear) &&
            int.TryParse(day, out var dayOfMonth))
        {                
            var now = DateTime.UtcNow; // Ignoring timezones.

            // Request a StringBuilder from the pool.
            var stringBuilder = builderPool.Get();

            try
            {
                stringBuilder.Append("Hi ")
                    .Append(firstName).Append(" ").Append(lastName).Append(". ");

                var encoder = context.RequestServices.GetRequiredService<HtmlEncoder>();

                if (now.Day == dayOfMonth && now.Month == monthOfYear)
                {
                    stringBuilder.Append("Happy birthday!!!");

                    var html = encoder.Encode(stringBuilder.ToString());
                    await context.Response.WriteAsync(html);
                }
                else
                {
                    var thisYearsBirthday = new DateTime(now.Year, monthOfYear, 
                                                                    dayOfMonth);

                    int daysUntilBirthday = thisYearsBirthday > now 
                        ? (thisYearsBirthday - now).Days 
                        : (thisYearsBirthday.AddYears(1) - now).Days;

                    stringBuilder.Append("There are ")
                        .Append(daysUntilBirthday).Append(" days until your birthday!");

                    var html = encoder.Encode(stringBuilder.ToString());
                    await context.Response.WriteAsync(html);
                }
            }
            finally // Ensure this runs even if the main code throws.
            {
                // Return the StringBuilder to the pool.
                builderPool.Return(stringBuilder); 
            }

            return;
        }

        await _next(context);
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-04-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/compatibility-version
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/compatibility-version.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Compatibility version for ASP.NET Core MVC

By [Rick Anderson](https://twitter.com/RickAndMSFT)

The [SetCompatibilityVersion](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccoremvcbuilderextensions.setcompatibilityversion) method is a no-op for ASP.NET Core 3.0 apps. That is, calling `SetCompatibilityVersion` with any value of [CompatibilityVersion](/en-us/dotnet/api/microsoft.aspnetcore.mvc.compatibilityversion) has no impact on the application.

* The next minor version of ASP.NET Core may provide a new `CompatibilityVersion` value.
* `CompatibilityVersion` values `Version_2_0` through `Version_2_2` are marked `[Obsolete(...)]`.
* The compatibility version APIs are removed in ASP.NET Core 11. For more information, see [MVC compatibility options removed](/en-us/aspnet/core/breaking-changes/11/mvc-compatibility-options-removed).
* See [Breaking API changes in Antiforgery, CORS, Diagnostics, Mvc, and Routing](https://github.com/aspnet/Announcements/issues/387). This list includes breaking changes for compatibility switches.

To see how `SetCompatibilityVersion` works with ASP.NET Core 2.x apps, select the [ASP.NET Core 2.2 version of this article](?view=aspnetcore-2.2\&preserve-view=true).

The [SetCompatibilityVersion](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvccoremvcbuilderextensions.setcompatibilityversion) method allows an ASP.NET Core 2.x app to opt-in or opt-out of potentially breaking behavior changes introduced in ASP.NET Core MVC 2.1 or 2.2. These potentially breaking behavior changes are generally in how the MVC subsystem behaves and how **your code** is called by the runtime. By opting in, you get the latest behavior, and the long-term behavior of ASP.NET Core.

The following code sets the compatibility mode to ASP.NET Core 2.2:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
```

We recommend you test your app using the latest version (`CompatibilityVersion.Latest`). We anticipate that most apps won't have breaking behavior changes using the latest version.

Apps that call `SetCompatibilityVersion(CompatibilityVersion.Version_2_0)` are protected from potentially breaking behavior changes introduced in the ASP.NET Core 2.1/2.2 MVC versions. This protection:

* Does not apply to all 2.1 or later changes, it's targeted to potentially breaking ASP.NET Core runtime behavior changes in the MVC subsystem.
* Does not extend to ASP.NET Core 3.0.

The default compatibility for ASP.NET Core 2.1 and 2.2 apps that do **not** call `SetCompatibilityVersion` is 2.0 compatibility. That is, not calling `SetCompatibilityVersion` is the same as calling `SetCompatibilityVersion(CompatibilityVersion.Version_2_0)`.

The following code sets the compatibility mode to ASP.NET Core 2.2, except for the following behaviors:

* [AllowCombiningAuthorizeFilters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.allowcombiningauthorizefilters#microsoft-aspnetcore-mvc-mvcoptions-allowcombiningauthorizefilters)
* [InputFormatterExceptionPolicy](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.inputformatterexceptionpolicy#microsoft-aspnetcore-mvc-mvcoptions-inputformatterexceptionpolicy)

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        // Include the 2.2 behaviors
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
        // Except for the following.
        .AddMvcOptions(options =>
        {
            // Don't combine authorize filters (keep 2.0 behavior).
            options.AllowCombiningAuthorizeFilters = false;
            // All exceptions thrown by an IInputFormatter are treated
            // as model state errors (keep 2.0 behavior).
            options.InputFormatterExceptionPolicy =
                InputFormatterExceptionPolicy.AllExceptions;
        });
}
```

For apps that encounter breaking behavior changes, using the appropriate compatibility switches:

* Allows you to use the latest release and opt out of specific breaking behavior changes.
* Gives you time to update your app so it works with the latest changes.

The [MvcOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions) documentation has a good explanation of what changed and why the changes are an improvement for most users.

With ASP.NET Core 3.0, old behaviors supported by compatibility switches have been removed. We feel these are positive changes benefitting nearly all users. By introducing these changes in 2.1 and 2.2, most apps can benefit, while others have time to update.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/environments.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core runtime environments

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

ASP.NET Core configures app behavior based on the runtime *environment*, which usually reflects where the app is running.

Apps usually run in the *Development* environment during local development and testing on a developer's machine with one set of configured behaviors. In contrast, they run in the *Production* environment when deployed to a server with a different set of configured behaviors. Any number of additional environments can be used, such as the *Staging* environment provided by the framework for staging an app prior to live deployment or other environments that developers create.

This article describes app runtime environments, how to use the environment to control app behavior, and how to set the environment.

For Blazor environments guidance, which adds to or supersedes the guidance in this article, see [ASP.NET Core Blazor environments](../blazor/fundamentals/environments?view=aspnetcore-10.0).

## Environments

Although the environment can be any string value, the following environment values are provided by the framework:

* [Development](/en-us/dotnet/api/microsoft.extensions.hosting.environments.development#microsoft-extensions-hosting-environments-development)
* [Staging](/en-us/dotnet/api/microsoft.extensions.hosting.environments.staging#microsoft-extensions-hosting-environments-staging)
* [Production](/en-us/dotnet/api/microsoft.extensions.hosting.environments.production#microsoft-extensions-hosting-environments-production)

The `Production` environment is configured to maximize security, performance, and app reliability. Common developer settings and configuration that differ from the `Development` environment include:

* Enabling [caching](../performance/caching/memory?view=aspnetcore-10.0).
* Bundling and minifying client-side resources, along with potentially serving them from a CDN.
* Disabling diagnostic error pages and enabling friendly error pages.
* Enabling production logging and monitoring. For example, logging is enabled for [Azure Application Insights](/en-us/azure/application-insights/app-insights-asp-net-core).

The last environment setting read by the app determines the app's environment. The app's environment can't be changed while the app is running.

## Logging

Output in the command shell of a running app at startup indicates the app's environment. In the following example, the app is running in the `Staging` environment:

```
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Staging
```

## Environment variables that determine the runtime environment

To determine the runtime environment, ASP.NET Core reads from the following environment variables:

* [`DOTNET_ENVIRONMENT`](configuration/?view=aspnetcore-10.0#default-host-configuration)
* `ASPNETCORE_ENVIRONMENT`

When using [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication), the `DOTNET_ENVIRONMENT` value take precedence over `ASPNETCORE_ENVIRONMENT`. When using [WebHost](/en-us/dotnet/api/microsoft.aspnetcore.webhost), `ASPNETCORE_ENVIRONMENT` takes precedence.

* [`DOTNET_ENVIRONMENT`](configuration/?view=aspnetcore-10.0#default-host-configuration)
* `ASPNETCORE_ENVIRONMENT` when the [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) method is called. The ASP.NET Core web app project templates call `WebApplication.CreateBuilder`. The `ASPNETCORE_ENVIRONMENT` value overrides `DOTNET_ENVIRONMENT`.

- [`DOTNET_ENVIRONMENT`](configuration/?view=aspnetcore-10.0#default-host-configuration)
- `ASPNETCORE_ENVIRONMENT` when [ConfigureWebHostDefaults](/en-us/dotnet/api/microsoft.extensions.hosting.generichostbuilderextensions.configurewebhostdefaults) is called. The ASP.NET Core web app project templates call `ConfigureWebHostDefaults`. The `ASPNETCORE_ENVIRONMENT` value overrides `DOTNET_ENVIRONMENT`.

If the `DOTNET_ENVIRONMENT` and `ASPNETCORE_ENVIRONMENT` environment variables aren't set, the `Production` environment is the default environment.

On Windows and macOS, environment variable names aren't case-sensitive. Linux environment variables are case-sensitive.

## Control code execution by environment

Use [WebApplicationBuilder.Environment](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.environment#microsoft-aspnetcore-builder-webapplicationbuilder-environment) or [WebApplication.Environment](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.environment#microsoft-aspnetcore-builder-webapplication-environment) to conditionally add services or middleware depending on the current environment.

The following code in the app's `Program` file:

* Uses [WebApplication.Environment](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.environment#microsoft-aspnetcore-builder-webapplication-environment) to distinguish the environment.
* Calls [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler), which adds [exception handler middleware](error-handling?view=aspnetcore-10.0) to the request processing pipeline to handle exceptions.
* Calls [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts), which adds [HSTS middleware](../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) to apply the [`Strict-Transport-Security` header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security).

```
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
```

The preceding example checks the current environment for the request processing pipeline. To check the current environment while configuring services, use [`builder.Environment`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.environment) instead of [`app.Environment`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.environment).

Use [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) or [WebApplication.Environment](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.environment#microsoft-aspnetcore-builder-webapplication-environment) to conditionally add services or middleware depending on the current environment.

The following code in `Startup.Configure`:

* Injects [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into `Startup.Configure` to tailor the code to the environment. This approach is useful when the app only requires adjusting `Startup.Configure` for a few environments with minimal code differences per environment. When many code differences exist per environment, consider using [accessing the environment from a `Startup` class](#access-the-environment-from-a-startup-class), which is covered later in this article.
* Calls [UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage) when `ASPNETCORE_ENVIRONMENT` is set to `Development`. The call adds middleware that captures exceptions and generates HTML error responses.
* Calls [UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler) when the value of `ASPNETCORE_ENVIRONMENT` is set to `Production`, `Staging`, or `Testing`. The call adds [exception handler middleware](error-handling?view=aspnetcore-10.0) to the pipeline to handle exceptions.

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    if (env.IsProduction() || env.IsStaging() || env.IsEnvironment("Testing"))
    {
        app.UseExceptionHandler("/Error");
    }

    ...
}
```

The preceding example checks the current environment while building the request pipeline. To check the current environment in `Startup.ConfigureServices` while configuring services, inject [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into the `Startup` class instead of injecting it into `Startup.Configure` and use the injected service to determine the environment in `Startup.ConfigureServices` and `Startup.Configure`.

Within the app, the [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment) provides general information about the app's hosting environment, and the [IHostEnvironment.EnvironmentName](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.environmentname#microsoft-extensions-hosting-ihostenvironment-environmentname) property indicates the app's current environment.

## Control rendered content

Inject [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment) into a server-rendered Razor component and use the service's extension methods and [EnvironmentName](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.environmentname) property to determine the environment for rendering content:

```
@inject IHostEnvironment Env

@if (Env.IsDevelopment())
{
    <div>The environment is Development.</div>
}

@if (!Env.IsDevelopment())
{
    <div>The environment isn't Development.</div>
}

@if (Env.IsStaging() || Env.EnvironmentName == "Testing")
{
    <div>The environment is either Staging or Testing.</div>
}
```

For Blazor Web Apps that require the environment to control client-side rendering, see [Prerender ASP.NET Core Razor components](../blazor/components/prerender?view=aspnetcore-10.0#client-side-services-fail-to-resolve-during-prerendering).

## Set the environment in a command shell when the app is run (`dotnet run`)

Use the [`-e|--environment` option](/en-us/dotnet/core/tools/dotnet-run#options) to set the environment:

```
dotnet run -e Staging
```

## Set the environment with the launch settings file (`launchSettings.json`)

The environment for local development can be set in the `Properties\launchSettings.json` file of the project. Environment values set in `launchSettings.json` override values set by the system environment.

The `launchSettings.json` file:

* Is only used on the local development machine.
* Isn't deployed when the app is published.
* May contain multiple profiles, each configuring a different environment.

The following example sets the `Staging` environment for the `https` launch profile using the `ASPNETCORE_ENVIRONMENT` environment variable:

```
"https": {
  "commandName": "Project",
  "dotnetRunMessages": true,
  "launchBrowser": true,
  "applicationUrl": "https://localhost:7205",
  "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Staging"
  }
}
```

In Visual Studio, there are two approaches for setting the environment via launch profiles:

* Press `Alt`+`Enter` or select **Properties** after right-clicking the project in **Solution Explorer**. Select **Debug** > **General**, followed by selecting the **Open debug launch profiles UI** link.

* With the project selected in **Solution Explorer**, select **{PROJECT NAME} Debug Properties** from the **Debug** menu, where the `{PROJECT NAME}` placeholder is a project name.

The preceding approaches open the **Launch Profiles** dialog where you can edit the environment variable settings in the `launchSettings.json` file. Changes made to project profiles may not take effect until the web server is restarted. Kestrel must be restarted before it can detect changes made to its environment.

Profiles can be selected in the Visual Studio UI next to the Start button (►).

When a solution contains multiple projects, only set the environment for the startup project.

Alternatively, use the [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) command with the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) set to the profile's name. *This approach only supports launch profiles based on the `Project` command.*

```
dotnet run -lp "https"
```

Note

When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

When using [Visual Studio Code](https://code.visualstudio.com/) with the [C# Dev Kit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) ([Getting Started with C# in VS Code](https://code.visualstudio.com/docs/csharp/get-started)), launch profiles are picked up from the app's `launchSettings.json` file.

If the C# Dev Kit isn't used, set the `ASPNETCORE_ENVIRONMENT` environment variable in the `.vscode/launch.json` in the `env` section, along with any other environment variables set in the section:

```
"env": {
    "ASPNETCORE_ENVIRONMENT": "Staging",
    ...
},
```

The `.vscode/launch.json` file is only used by Visual Studio Code.

## Set the environment with an environment variable

It's often useful to set a specific environment for testing with an environment variable or platform setting. If the environment isn't set, it defaults to the `Production` environment, which disables most debugging features. The method for setting the environment depends on the operating system.

### Azure App Service

Apps deployed to [Azure App Service](https://azure.microsoft.com/services/app-service/) adopt the `Production` environment by default.

To set the `ASPNETCORE_ENVIRONMENT` environment variable, see the following resources in the Azure documentation:

* [Configure an App Service app](/en-us/azure/app-service/configure-common?tabs=portal#configure-app-settings)
* [Set up staging environments in Azure App Service](/en-us/azure/app-service/web-sites-staged-publishing)

Azure App Service automatically restarts the app after an app setting is added, changed, or deleted.

### Set environment variable for a process

To set the `ASPNETCORE_ENVIRONMENT` environment variable for the current session (command shell) when the app is started using [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run), use the following commands. After the environment variable is set, the app is started without a launch profile using the [`--no-launch-profile`](/en-us/dotnet/core/tools/dotnet-run#options) option.

1. In the command shell, set the environment variable using the appropriate approach for your operating system.

2. Execute the `dotnet run` command without using a launch profile:

   ```
   dotnet run --no-launch-profile
   ```

When using PowerShell, the preceding steps can be combined in the following two commands. The following example sets the `Staging` environment:

```
$Env:ASPNETCORE_ENVIRONMENT = "Staging"
dotnet run --no-launch-profile
```

### Set environment variable globally

Use the appropriate guidance for your operating system to set the `ASPNETCORE_ENVIRONMENT` environment variable.

When the `ASPNETCORE_ENVIRONMENT` environment variable is set globally, it takes effect for the [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) command in any command shell opened after the value is set. Environment values set by [launch profiles in the `launchSettings.json` file](#set-the-environment-with-the-launch-settings-file-launchsettingsjson) override values set for the system environment.

### Set the environment for apps deployed to IIS

To set the `ASPNETCORE_ENVIRONMENT` environment variable with the `web.config` file, see [web.config file](../host-and-deploy/iis/web-config?view=aspnetcore-10.0#set-environment-variables).

To set the environment variable on deployment to IIS, include the `<EnvironmentName>` property in the [publish profile (.pubxml)](../host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-10.0) or project file. The following example sets the environment in `web.config` to the `Staging` environment when the project is published:

```
<PropertyGroup>
  <EnvironmentName>Staging</EnvironmentName>
</PropertyGroup>
```

To set the `ASPNETCORE_ENVIRONMENT` environment variable for an app running in an isolated Application Pool (supported on IIS 10.0 or later), see [Environment Variables \<environmentVariables>](/en-us/iis/configuration/system.applicationHost/applicationPools/add/environmentVariables/#appcmdexe). When the `ASPNETCORE_ENVIRONMENT` environment variable is set for an Application Pool, its value overrides a setting at the system level.

When hosting an app in IIS and adding or changing the `ASPNETCORE_ENVIRONMENT` environment variable, use ***either*** of the following approaches to have the new value take effect for running apps:

* Execute `net stop was /y` followed by `net start w3svc` in a command shell.
* Restart the server.

### Docker

Set the app's environment using any of the approaches in this section.

#### Use a Dockerfile

Set the `ASPNETCORE_ENVIRONMENT` environment variable within the Dockerfile using the `ENV` instruction:

```
ENV ASPNETCORE_ENVIRONMENT=Staging
```

#### Use Docker Compose

For multi-service apps managed with Docker Compose, define `ASPNETCORE_ENVIRONMENT` environment variables within the `docker-compose.yml` file:

```
version: "3.9"
services:
  web:
    build: .
    ports:
      - "8000:5000"
    environment:
      - ASPNETCORE_ENVIRONMENT=Staging
      - API_KEY=...
```

An environment set at runtime with Docker Compose overrides an environment set by the Dockerfile.

#### Use the `docker run` command

When running the Docker container with the [`docker run` command](https://docs.docker.com/reference/cli/docker/container/run/), Set the `ASPNETCORE_ENVIRONMENT` environment variable with the `-e|--env` option:

```
docker run -e ASPNETCORE_ENVIRONMENT=Staging aspnet_core_image
```

An environment set at runtime with `docker run` overrides an environment set by the Dockerfile.

#### Docker environment file

Set the `ASPNETCORE_ENVIRONMENT` environment variable using a Docker environment file (`.env`).

`env_variables.env`:

```
ASPNETCORE_ENVIRONMENT=Staging
```

Load the file with the `--env-file` option when executing the [`docker run` command](https://docs.docker.com/reference/cli/docker/container/run/):

```
docker run --env-file ./env_variables.env aspnet_core_image
```

An environment set at runtime with `docker run` overrides an environment set by the Dockerfile.

## Set the environment in the app's startup code

To set the environment in code, use [WebApplicationOptions.EnvironmentName](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationoptions.environmentname#microsoft-aspnetcore-builder-webapplicationoptions-environmentname) when creating [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder), as shown in the following example:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    EnvironmentName = Environments.Staging
}); 
```

Call [UseEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.hostinghostbuilderextensions.useenvironment) when building the host. For more information, see [.NET Generic Host in ASP.NET Core](host/generic-host?view=aspnetcore-10.0#environmentname).

## Load configuration by environment

To load configuration by environment, see [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0#json-configuration-provider).

## Access the environment from a `Startup` class

Use of a `Startup` class (`Startup.cs`) with [`Configure`](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configure) and [`ConfigureServices`](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configureservices) methods was required before the release of .NET 6 and remains supported.

Inject [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into the `Startup` constructor to control code execution. This approach is useful when the app requires configuring startup code for only a few environments with minimal code differences per environment.

In the following example, the environment is held in the `_env` field and controls code execution based on the app's environment:

```
public class Startup
{
    private readonly IWebHostEnvironment _env;

    public Startup(IWebHostEnvironment env)
    {
        _env = env;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        if (_env.IsDevelopment())
        {
            ...
        }
        else if (_env.IsStaging())
        {
            ...
        }
        else
        {
            ...
        }
    }

    public void Configure(IApplicationBuilder app)
    {
        if (_env.IsDevelopment())
        {
            ...
        }
        else
        {
            ...
        }

        ...
    }
}
```

## Environment-specific `Startup` class

An app can define multiple `Startup` classes for different environments with the naming convention `Startup{EnvironmentName}` class, where the `{ENVIRONMENT NAME}` placeholder is the environment name.

The class whose name suffix matches the current environment is prioritized. If a matching `Startup{EnvironmentName}` class isn't found, the `Startup` class is used.

To implement environment-based `Startup` classes, create as many `Startup{EnvironmentName}` classes as needed and a fallback `Startup` class:

```
public class StartupDevelopment
{
    ...
}

public class StartupProduction
{
    ...
}

public class Startup
{
    ...
}
```

Where the host builder is created, call [HostingAbstractionsWebHostBuilderExtensions.UseStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usestartup), which accepts an assembly name to load the correct `Startup` class:

```
public static IHostBuilder CreateHostBuilder(string[] args)
{
    var assemblyName = typeof(Startup).GetTypeInfo().Assembly.FullName;

    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup(assemblyName);
        });
}
```

## Environment-specific `Startup` class methods

The `Configure` and `ConfigureServices` methods support environment-specific versions of the form `Configure{ENVIRONMENT NAME}` and `Configure{ENVIRONMENT NAME}Services`, where the `{ENVIRONMENT NAME}` placeholder is the environment name. If a matching environment name isn't found for the named methods, the `ConfigureServices` or `Configure` method is used, respectively.

```
public void ConfigureDevelopmentServices(IServiceCollection services)
{
    ...
}

public void ConfigureStagingServices(IServiceCollection services)
{
    ...
}

public void ConfigureProductionServices(IServiceCollection services)
{
    ...
}

public void ConfigureServices(IServiceCollection services)
{
    ...
}
```

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/environments) ([how to download](../overview?view=aspnetcore-10.0#how-to-download-a-sample))
* [App startup in ASP.NET Core](startup?view=aspnetcore-10.0)
* [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor environments](../blazor/fundamentals/environments?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/publish-to-iis
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/publish-to-iis.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Publish an ASP.NET Core app to IIS

Internet Information Services (IIS) is a flexible, general-purpose web server that runs on Windows and can host ASP.NET Core apps. IIS is a good choice when you need to run ASP.NET Core apps on Windows Server in an on-premises or hybrid environment, need Windows Authentication, or require integration with other IIS features such as URL Rewrite, Application Request Routing, or centralized certificate management.

This tutorial covers the following subjects:

* Install the .NET Hosting Bundle on Windows Server.
* Create an IIS site in IIS Manager.
* Deploy an ASP.NET Core app.

## Prerequisites

* [.NET SDK](/en-us/dotnet/core/sdk) installed on the development machine.
* Windows Server configured with the **Web Server (IIS)** server role. If your server isn't configured to host websites with IIS, follow the guidance in the *IIS configuration* section of the [Host ASP.NET Core on Windows with IIS](../host-and-deploy/iis/?view=aspnetcore-10.0#iis-configuration) article and then return to this tutorial.

Warning

**IIS configuration and website security involve concepts that aren't covered by this tutorial.** Consult the IIS guidance in the [Microsoft IIS documentation](https://www.iis.net/) and the [ASP.NET Core article on hosting with IIS](../host-and-deploy/iis/?view=aspnetcore-10.0) before hosting production apps on IIS.

Important scenarios for IIS hosting not covered by this tutorial include:

* [Creation of a registry hive for ASP.NET Core Data Protection](../host-and-deploy/iis/advanced?view=aspnetcore-10.0#data-protection)
* [Configuration of the app pool's Access Control List (ACL)](../host-and-deploy/iis/advanced?view=aspnetcore-10.0#application-pool-identity)
* To focus on IIS deployment concepts, this tutorial deploys an app without HTTPS security configured in IIS. For more information on hosting an app enabled for HTTPS protocol, see the security topics in the [Related content](#related-content) section of this article. Further guidance for hosting ASP.NET Core apps is provided in the [Host ASP.NET Core on Windows with IIS](../host-and-deploy/iis/?view=aspnetcore-10.0) article.

## Install the .NET Hosting Bundle

Install the *.NET Hosting Bundle* on the IIS server. The bundle installs the .NET Runtime, .NET Library, and the [ASP.NET Core Module](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0). The module allows ASP.NET Core apps to run behind IIS.

ASP.NET Core apps hosted in IIS use the *in-process hosting model* by default (since ASP.NET Core 3.0). In-process hosting runs the app in the same process as the IIS worker process (`w3wp.exe`), which provides better performance than out-of-process hosting. For more information, see [Host ASP.NET Core on Windows with IIS](../host-and-deploy/iis/?view=aspnetcore-10.0).

Download the installer using the following link:

[Current .NET Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

1. Run the installer on the IIS server.

2. Restart the server or execute `net stop was /y` followed by `net start w3svc` in a command shell.

## Create the IIS site

1. On the IIS server, create a folder to contain the app's published folders and files. In a following step, the folder's path is provided to IIS as the physical path to the app. For more information on an app's deployment folder and file layout, see [ASP.NET Core directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0).

2. In IIS Manager, open the server's node in the **Connections** panel. Right-click the **Sites** folder. Select **Add Website** from the contextual menu.

3. Provide a **Site name** and set the **Physical path** to the app's deployment folder that you created. Provide the **Binding** configuration and create the website by selecting **OK**.

   Warning

   Top-level wildcard bindings (`http://*:80/` and `http://+:80`) should **not** be used. Top-level wildcard bindings can open up your app to security vulnerabilities. This applies to both strong and weak wildcards. Use explicit host names rather than wildcards. Subdomain wildcard binding (for example, `*.mysub.com`) doesn't have this security risk if you control the entire parent domain (as opposed to `*.com`, which is vulnerable). See [RFC 9110: HTTP Semantics (Section 7.2. Host and :authority)](https://www.rfc-editor.org/rfc/rfc9110#field.host) for more information.

4. Confirm the process model identity has the proper permissions.

   If the default identity of the app pool (**Process Model** > **Identity**) is changed from `ApplicationPoolIdentity` to another identity, verify that the new identity has the required permissions to access the app's folder, database, and other required resources. For example, the app pool requires read and write access to folders where the app reads and writes files.

## Create an ASP.NET Core app

Create any type of ASP.NET Core server-based app.

Note

This tutorial is based on hosting a server-side ASP.NET Core app with IIS, including a Blazor Web App. For guidance on hosting and deploying a *standalone Blazor WebAssembly app* with IIS, see [Host and deploy ASP.NET Core Blazor WebAssembly with IIS](../blazor/host-and-deploy/webassembly/iis?view=aspnetcore-10.0).

## Publish and deploy the app

*Publish an app* means to produce a compiled app that can be hosted by a server. *Deploy an app* means to move the published app to a hosting system. The publish step is handled by the [.NET SDK](/en-us/dotnet/core/sdk), while the deployment step can be handled by a variety of approaches. This tutorial adopts the *folder* deployment approach, where:

* The app is published to a folder.
* The folder's contents are moved to the IIS site's folder (the **Physical path** to the site in IIS Manager).

ASP.NET Core apps can be published as *framework-dependent* (the server must have .NET installed) or *self-contained* (includes the .NET runtime in the published output). For most IIS deployments, the framework-dependent approach is recommended because the .NET Hosting Bundle provides the required runtime on the server. For more information, see [.NET application deployment](/en-us/dotnet/core/deploying/).

A `web.config` file is generated automatically when the app is published. IIS uses this file to configure the ASP.NET Core Module for the app. Don't remove or manually edit the `web.config` file unless you're making advanced configuration changes.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. Right-click on the project in **Solution Explorer** and select **Publish**.

2. In the **Publish** dialog, select **Folder** as the publish target and select **Next**.

3. Set the **Folder location** path.

   * If you created a folder for the IIS site that's available on the development machine as a network share, provide the path to the share. The current user must have write access to publish to the share.
   * If you're unable to deploy directly to the IIS site folder on the IIS server, publish to a folder on removable media and physically move the published app to the IIS site folder on the server, which is the site's **Physical path** in IIS Manager. Move the contents of the `bin/Release/{TARGET FRAMEWORK}/publish` folder (where `{TARGET FRAMEWORK}` is the target framework moniker, for example `net10.0`) to the IIS site folder on the server, which is the site's **Physical path** in IIS Manager.

4. Select **Finish** and then select **Publish**.

1) In a command shell, publish the app in Release configuration with the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command:

   ```
   dotnet publish --configuration Release
   ```

2) Move the contents of the `bin/Release/{TARGET FRAMEWORK}/publish` folder (where `{TARGET FRAMEWORK}` is the target framework moniker, for example `net10.0`) to the IIS site folder on the server, which is the site's **Physical path** in IIS Manager.

## Browse the website

The app is accessible in a browser after it receives the first request. Make a request to the app at the endpoint binding that you established in IIS Manager for the site.

## Next steps

In this tutorial, you learned how to:

* Install the .NET Hosting Bundle on Windows Server.
* Create an IIS site in IIS Manager.
* Deploy an ASP.NET Core app.

To learn more about hosting ASP.NET Core apps on IIS, see the IIS Overview article:

[Host ASP.NET Core on Windows with IIS](../host-and-deploy/iis/?view=aspnetcore-10.0)

## Related content

### Articles in the ASP.NET Core documentation set

* [ASP.NET Core Module (ANCM) for IIS](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0)
* [ASP.NET Core directory structure](../host-and-deploy/directory-structure?view=aspnetcore-10.0)
* [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Enforce HTTPS in ASP.NET Core](../security/enforcing-ssl?view=aspnetcore-10.0)
* [WebSockets on IIS](../fundamentals/websockets?view=aspnetcore-10.0#enabling-websockets-on-iis)

### Articles pertaining to ASP.NET Core app deployment

* [Publish an ASP.NET Core app to Azure with Visual Studio](publish-to-azure-webapp-using-vs?view=aspnetcore-10.0)
* [Publish an ASP.NET Core app to Azure with Visual Studio Code](publish-to-azure-webapp-using-vscode?view=aspnetcore-10.0)
* [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](../host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-10.0)

### Articles on IIS HTTPS configuration

* [Configuring SSL in IIS Manager](/en-us/iis/manage/configuring-security/configuring-ssl-in-iis-manager)
* [How to Set Up SSL on IIS](/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis)

### Articles on IIS and Windows Server

* [The Official Microsoft IIS Site](https://www.iis.net/)
* [Windows Server technical content library](/en-us/windows-server/windows-server)

### Deployment resources for IIS administrators

* [IIS documentation](/en-us/iis)
* [Getting Started with the IIS Manager in IIS](/en-us/iis/get-started/getting-started-with-iis/getting-started-with-the-iis-manager-in-iis-7-and-iis-8)
* [.NET application deployment](/en-us/dotnet/core/deploying/)
* [IIS modules with ASP.NET Core](../host-and-deploy/iis/modules?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../host-and-deploy/azure-iis-errors-reference?view=aspnetcore-10.0)
* [Sticky sessions with Application Request Routing](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/mvc1002
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/mvc1002.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# MVC1002: Route attribute cannot be applied to page handler methods

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | MVC1002      |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

An attribute implementing [IRouteTemplateProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.iroutetemplateprovider) was applied to a Razor Page handler method.

### Rule description

Razor Page handler methods are selected after routing is completed, and consequently cannot contribute a route. Applying a route attribute such as `HttpGet` or `HttpPost` to a Razor Page handler is not supported.

```
public class IndexModel : PageModel
{
    [HttpGet("/my-url")]
    public IActionResult OnGet() => Page();
}
```

## How to fix violations

Remove the route attribute from the handler. Routes can be specified for a Razor Page using an `@page` directive or by using conventions. For more information, see [custom routes in Razor Pages](../razor-pages/?view=aspnetcore-10.0#custom-routes).

## When to suppress warnings

Don't suppress warnings from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/services
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/services.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Create gRPC services and methods

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

This document explains how to create gRPC services and methods in C#. Topics include:

* How to define services and methods in `.proto` files.
* Generated code using gRPC C# tooling.
* Implementing gRPC services and methods.

## Create new gRPC services

[gRPC services with C#](basics?view=aspnetcore-10.0) introduced gRPC's contract-first approach to API development. Services and messages are defined in `.proto` files. C# tooling then generates code from `.proto` files. For server-side assets, an abstract base type is generated for each service, along with classes for any messages.

The following `.proto` file:

* Defines a `Greeter` service.
* The `Greeter` service defines a `SayHello` call.
* `SayHello` sends a `HelloRequest` message and receives a `HelloReply` message

```
syntax = "proto3";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
```

C# tooling generates the C# `GreeterBase` base type:

```
public abstract partial class GreeterBase
{
    public virtual Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        throw new RpcException(new Status(StatusCode.Unimplemented, ""));
    }
}

public class HelloRequest
{
    public string Name { get; set; }
}

public class HelloReply
{
    public string Message { get; set; }
}
```

By default the generated `GreeterBase` doesn't do anything. Its virtual `SayHello` method will return an `UNIMPLEMENTED` error to any clients that call it. For the service to be useful an app must create a concrete implementation of `GreeterBase`:

```
public class GreeterService : GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" });
    }
}
```

The `ServerCallContext` gives the context for a server-side call.

The service implementation is registered with the app. If the service is hosted by ASP.NET Core gRPC, it should be added to the routing pipeline with the `MapGrpcService` method.

```
app.MapGrpcService<GreeterService>();
```

See [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0) for more information.

## Implement gRPC methods

A gRPC service can have different types of methods. How messages are sent and received by a service depends on the type of method defined. The gRPC method types are:

* Unary
* Server streaming
* Client streaming
* Bi-directional streaming

Streaming calls are specified with the `stream` keyword in the `.proto` file. `stream` can be placed on a call's request message, response message, or both.

```
syntax = "proto3";

service ExampleService {
  // Unary
  rpc UnaryCall (ExampleRequest) returns (ExampleResponse);

  // Server streaming
  rpc StreamingFromServer (ExampleRequest) returns (stream ExampleResponse);

  // Client streaming
  rpc StreamingFromClient (stream ExampleRequest) returns (ExampleResponse);

  // Bi-directional streaming
  rpc StreamingBothWays (stream ExampleRequest) returns (stream ExampleResponse);
}
```

Each call type has a different method signature. Overriding generated methods from the abstract base service type in a concrete implementation ensures the correct arguments and return type are used.

### Unary method

A unary method has the request message as a parameter, and returns the response. A unary call is complete when the response is returned.

```
public override Task<ExampleResponse> UnaryCall(ExampleRequest request,
    ServerCallContext context)
{
    var response = new ExampleResponse();
    return Task.FromResult(response);
}
```

Unary calls are the most similar to [actions on web API controllers](../web-api/?view=aspnetcore-10.0). One important difference gRPC methods have from actions is gRPC methods are not able to bind parts of a request to different method arguments. gRPC methods always have one message argument for the incoming request data. Multiple values can still be sent to a gRPC service by adding fields to the request message:

```
message ExampleRequest {
    int32 pageIndex = 1;
    int32 pageSize = 2;
    bool isDescending = 3;
}
```

### Server streaming method

A server streaming method has the request message as a parameter. Because multiple messages can be streamed back to the caller, `responseStream.WriteAsync` is used to send response messages. A server streaming call is complete when the method returns.

```
public override async Task StreamingFromServer(ExampleRequest request,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    for (var i = 0; i < 5; i++)
    {
        await responseStream.WriteAsync(new ExampleResponse());
        await Task.Delay(TimeSpan.FromSeconds(1));
    }
}
```

The client has no way to send additional messages or data once the server streaming method has started. Some streaming methods are designed to run forever. For continuous streaming methods, a client can cancel the call when it's no longer needed. When cancellation happens the client sends a signal to the server and the [ServerCallContext.CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) is raised. The `CancellationToken` token should be used on the server with async methods so that:

* Any asynchronous work is canceled together with the streaming call.
* The method exits quickly.

```
public override async Task StreamingFromServer(ExampleRequest request,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    while (!context.CancellationToken.IsCancellationRequested)
    {
        await responseStream.WriteAsync(new ExampleResponse());
        await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken);
    }
}
```

### Client streaming method

A client streaming method starts *without* the method receiving a message. The `requestStream` parameter is used to read messages from the client. A client streaming call is complete when a response message is returned:

```
public override async Task<ExampleResponse> StreamingFromClient(
    IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context)
{
    await foreach (var message in requestStream.ReadAllAsync())
    {
        // ...
    }
    return new ExampleResponse();
}
```

### Bi-directional streaming method

A bi-directional streaming method starts *without* the method receiving a message. The `requestStream` parameter is used to read messages from the client. The method can choose to send messages with `responseStream.WriteAsync`. A bi-directional streaming call is complete when the method returns:

```
public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    await foreach (var message in requestStream.ReadAllAsync())
    {
        await responseStream.WriteAsync(new ExampleResponse());
    }
}
```

The preceding code:

* Sends a response for each request.
* Is a basic usage of bi-directional streaming.

It is possible to support more complex scenarios, such as reading requests and sending responses simultaneously:

```
public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    // Read requests in a background task.
    var readTask = Task.Run(async () =>
    {
        await foreach (var message in requestStream.ReadAllAsync())
        {
            // Process request.
        }
    });

    // Send responses until the client signals that it is complete.
    while (!readTask.IsCompleted)
    {
        await responseStream.WriteAsync(new ExampleResponse());
        await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken);
    }
}
```

In a bi-directional streaming method, the client and service can send messages to each other at any time. The best implementation of a bi-directional method varies depending upon requirements.

## Access gRPC request headers

A request message is not the only way for a client to send data to a gRPC service. Header values are available in a service using `ServerCallContext.RequestHeaders`.

```
public override Task<ExampleResponse> UnaryCall(ExampleRequest request,
    ServerCallContext context)
{
    var userAgent = context.RequestHeaders.GetValue("user-agent");
    // ...

    return Task.FromResult(new ExampleResponse());
}
```

## Multi-threading with gRPC streaming methods

There are important considerations to implementing gRPC streaming methods that use multiple threads.

### Reader and writer thread safety

`IAsyncStreamReader<TMessage>` and `IServerStreamWriter<TMessage>` can each be used by only one thread at a time. For a streaming gRPC method, multiple threads can't read new messages with `requestStream.MoveNext()` simultaneously. And multiple threads can't write new messages with `responseStream.WriteAsync(message)` simultaneously.

A safe way to enable multiple threads to interact with a gRPC method is to use the producer-consumer pattern with [System.Threading.Channels](/en-us/dotnet/core/extensions/channels).

```
public override async Task DownloadResults(DataRequest request,
        IServerStreamWriter<DataResult> responseStream, ServerCallContext context)
{
    var channel = Channel.CreateBounded<DataResult>(new BoundedChannelOptions(capacity: 5));

    var consumerTask = Task.Run(async () =>
    {
        // Consume messages from channel and write to response stream.
        await foreach (var message in channel.Reader.ReadAllAsync())
        {
            await responseStream.WriteAsync(message);
        }
    });

    var dataChunks = request.Value.Chunk(size: 10);

    // Write messages to channel from multiple threads.
    await Task.WhenAll(dataChunks.Select(
        async c =>
        {
            var message = new DataResult { BytesProcessed = c.Length };
            await channel.Writer.WriteAsync(message);
        }));

    // Complete writing and wait for consumer to complete.
    channel.Writer.Complete();
    await consumerTask;
}
```

The preceding gRPC server streaming method:

* Creates a bounded channel for producing and consuming `DataResult` messages.
* Starts a task to read messages from the channel and write them to the response stream.
* Writes messages to the channel from multiple threads.

Note

Bidirectional streaming methods take `IAsyncStreamReader<TMessage>` and `IServerStreamWriter<TMessage>` as arguments. It's safe to use these types on separate threads from each other.

### Interacting with a gRPC method after a call ends

A gRPC call ends on the server once the gRPC method exits. The following arguments passed to gRPC methods aren't safe to use after the call has ended:

* `ServerCallContext`
* `IAsyncStreamReader<TMessage>`
* `IServerStreamWriter<TMessage>`

If a gRPC method starts background tasks that use these types, it must complete the tasks before the gRPC method exits. Continuing to use the context, stream reader, or stream writer after the gRPC method exists causes errors and unpredictable behavior.

In the following example, the server streaming method could write to the response stream after the call has finished:

```
public override async Task StreamingFromServer(ExampleRequest request,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    _ = Task.Run(async () =>
    {
        for (var i = 0; i < 5; i++)
        {
            await responseStream.WriteAsync(new ExampleResponse());
            await Task.Delay(TimeSpan.FromSeconds(1));
        }
    });

    await PerformLongRunningWorkAsync();
}
```

For the previous example, the solution is to await the write task before exiting the method:

```
public override async Task StreamingFromServer(ExampleRequest request,
    IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
{
    var writeTask = Task.Run(async () =>
    {
        for (var i = 0; i < 5; i++)
        {
            await responseStream.WriteAsync(new ExampleResponse());
            await Task.Delay(TimeSpan.FromSeconds(1));
        }
    });

    await PerformLongRunningWorkAsync();

    await writeTask;
}
```

## Related content

* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/11/microsoft-openapi-3x
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/breaking-changes/11/microsoft-openapi-3x.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Microsoft.OpenApi upgraded to 3.x

`Microsoft.AspNetCore.OpenApi` in ASP.NET Core 11 takes a dependency on `Microsoft.OpenApi` 3.x. The previous release (.NET 10) depended on `Microsoft.OpenApi` 2.x. This is a major-version bump of a transitive dependency, and several types that document and operation transformers receive have changed shape.

## Version introduced

.NET 11 Preview 3

## Previous behavior

Previously, `Microsoft.AspNetCore.OpenApi` depended on `Microsoft.OpenApi` 2.x. Implementations of the document, operation, and schema transformer interfaces worked against the 2.x object model—for example, against the `OpenApiSchema` shape that exposed nested schemas, references, extensions, and parsing helpers as 2.x concrete types:

```
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;

builder.Services.AddOpenApi(options =>
{
    options.AddSchemaTransformer((schema, context, ct) =>
    {
        // Microsoft.OpenApi 2.x: OpenApiString was a concrete type 
        // in Microsoft.OpenApi.Any.
        schema.Extensions["x-schema-id"] = 
            new OpenApiString(context.JsonTypeInfo.Type.Name);
        return Task.CompletedTask;
    });
});
```

## New behavior

Starting in ASP.NET Core 11, `Microsoft.AspNetCore.OpenApi` depends on `Microsoft.OpenApi` 3.x. The `IOpenApiDocumentTransformer`, `IOpenApiOperationTransformer`, and `IOpenApiSchemaTransformer` interface signatures still expose the concrete `OpenApiDocument`, `OpenApiOperation`, and `OpenApiSchema` parameter types, but the surface area of those types has changed:

* Several formerly concrete types that nested types expose are now interfaces (for example, `IOpenApiSchema`, `IOpenApiAny`).
* The 2.x `OpenApiAny` types (such as `OpenApiString`, `OpenApiInteger`) were removed in favor of [JsonNode](/en-us/dotnet/api/system.text.json.nodes.jsonnode)-based extension values.
* The `ParseNode` parsing infrastructure was removed in favor of visitor- and writer-based APIs.
* The `OpenApiReference` model was reshaped to better distinguish between local and external references.

Transformer code that does anything more than read-only inspection—sets extension values, walks nested schemas, or constructs new schemas—usually needs to be updated.

## Type of breaking change

This change can affect [source compatibility](/en-us/dotnet/core/compatibility/categories#source-compatibility), and also [binary compatibility](/en-us/dotnet/core/compatibility/categories#binary-compatibility) for libraries that were compiled against `Microsoft.OpenApi` 2.x and are loaded into an app that resolves the 3.x assembly at runtime.

## Reason for change

The new `Microsoft.OpenApi` 3.x release adds support for OpenAPI 3.1.0 and 3.2.0 specifications, fixes long-standing issues in the object model, and adopts interface-based abstractions that are easier to extend. For more information, see [dotnet/aspnetcore#65415](https://github.com/dotnet/aspnetcore/pull/65415) and [dotnet/aspnetcore#66998](https://github.com/dotnet/aspnetcore/pull/66998).

## Recommended action

Update any custom document, operation, or schema transformer to compile against `Microsoft.OpenApi` 3.x. The most common changes are:

* Replace 2.x `OpenApiAny` values such as `new OpenApiString("...")` with `JsonNode` values (`JsonValue.Create("...")`).
* Update code that walks nested schemas or references to handle the new interface-based abstractions (for example, `IOpenApiSchema`).
* Update code that used the removed `ParseNode` parsing infrastructure to use the new visitor- and writer-based APIs.
* Review use of `OpenApiReference`, which was reshaped to better distinguish between local and external references.

For step-by-step migration instructions, see the [Microsoft.OpenApi 3.x upgrade guide](https://github.com/microsoft/OpenAPI.NET/blob/main/docs/upgrade-guide-3.md). For the per-release notes covering the 3.0, 3.1, 3.2, and later releases, see the [OpenAPI.NET release notes](https://github.com/microsoft/OpenAPI.NET/releases).

## Affected APIs

* [IOpenApiDocumentTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapidocumenttransformer)
* [IOpenApiOperationTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapioperationtransformer)
* [IOpenApiSchemaTransformer](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapischematransformer)
* [OpenApiOptions](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/update-related-data
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-rp/update-related-data.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 7, Razor Pages with EF Core in ASP.NET Core - Update Related Data

By [Tom Dykstra](https://github.com/tdykstra), [Jon P Smith](https://twitter.com/thereformedprog), and [Rick Anderson](https://twitter.com/RickAndMSFT)

The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. For information about the tutorial series, see [the first tutorial](intro?view=aspnetcore-10.0).

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) and compare that code to what you created by following the tutorial.

This tutorial shows how to update related data. The following illustrations show some of the completed pages.

## Update the Course Create and Edit pages

The scaffolded code for the Course Create and Edit pages has a Department drop-down list that shows `DepartmentID`, an `int`. The drop-down should show the Department name, so both of these pages need a list of department names. To provide that list, use a base class for the Create and Edit pages.

### Create a base class for Course Create and Edit

Create a `Pages/Courses/DepartmentNamePageModel.cs` file with the following code:

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace ContosoUniversity.Pages.Courses
{
    public class DepartmentNamePageModel : PageModel
    {
        public SelectList DepartmentNameSL { get; set; }

        public void PopulateDepartmentsDropDownList(SchoolContext _context,
            object selectedDepartment = null)
        {
            var departmentsQuery = from d in _context.Departments
                                   orderby d.Name // Sort by name.
                                   select d;

            DepartmentNameSL = new SelectList(departmentsQuery.AsNoTracking(),
                nameof(Department.DepartmentID),
                nameof(Department.Name),
                selectedDepartment);
        }
    }
}
```

The preceding code creates a [SelectList](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.selectlist) to contain the list of department names. The `selectedDepartment` parameter allows the calling code to specify the item that will be selected when the drop-down list is rendered. However, when using the Select Tag Helper with `asp-for`, the selected item is determined by the model property value (such as `Course.DepartmentID`), not by the `selectedDepartment` parameter.

Important

When using the Select Tag Helper with `asp-for="Course.DepartmentID"`, the selected option is automatically determined by the value of `Course.DepartmentID`. The `selectedDepartment` parameter passed to the SelectList constructor is ignored in this scenario. The Create and Edit page model classes will derive from `DepartmentNamePageModel`.

### Update the Course Create page model

A Course is assigned to a Department. The base class for the Create and Edit pages provides a `SelectList` for selecting the department. The drop-down list that uses the `SelectList` sets the `Course.DepartmentID` foreign key (FK) property. EF Core uses the `Course.DepartmentID` FK to load the `Department` navigation property.

Update `Pages/Courses/Create.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class CreateModel : DepartmentNamePageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public CreateModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            PopulateDepartmentsDropDownList(_context);
            return Page();
        }

        [BindProperty]
        public Course Course { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            var emptyCourse = new Course();

            if (await TryUpdateModelAsync<Course>(
                 emptyCourse,
                 "course",   // Prefix for form value.
                 s => s.CourseID, s => s.DepartmentID, s => s.Title, s => s.Credits))
            {
                _context.Courses.Add(emptyCourse);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }

            // Repopulate departments dropdown. emptyCourse.DepartmentID determines the selected item.
            PopulateDepartmentsDropDownList(_context, emptyCourse.DepartmentID);
            return Page();
        }
      }
}
```

The preceding code:

* Derives from `DepartmentNamePageModel`.
* Uses [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) to prevent [overposting](crud?view=aspnetcore-10.0#overposting).
* Removes `ViewData["DepartmentID"]`. The `DepartmentNameSL` `SelectList` is a strongly typed model and will be used by the Razor page. Strongly typed models are preferred over weakly typed. For more information, see [Weakly typed data (ViewData and ViewBag)](../../mvc/views/overview?view=aspnetcore-10.0#VD_VB).

### Update the Course Create Razor page

Update `Pages/Courses/Create.cshtml` with the following code:

```
@page
@model ContosoUniversity.Pages.Courses.CreateModel
@{
    ViewData["Title"] = "Create Course";
}
<h2>Create</h2>
<h4>Course</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Course.CourseID" class="control-label"></label>
                <input asp-for="Course.CourseID" class="form-control" />
                <span asp-validation-for="Course.CourseID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Title" class="control-label"></label>
                <input asp-for="Course.Title" class="form-control" />
                <span asp-validation-for="Course.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Credits" class="control-label"></label>
                <input asp-for="Course.Credits" class="form-control" />
                <span asp-validation-for="Course.Credits" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Department" class="control-label"></label>
                <select asp-for="Course.DepartmentID" class="form-control"
                        asp-items="@Model.DepartmentNameSL">
                    <option value="">-- Select Department --</option>
                </select>
                <span asp-validation-for="Course.DepartmentID" class="text-danger" />
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding code makes the following changes:

* Changes the caption from **DepartmentID** to **Department**.
* Replaces `"ViewBag.DepartmentID"` with `DepartmentNameSL` (from the base class).
* Adds the "Select Department" option. This change renders "Select Department" in the drop-down when no department has been selected yet, rather than the first department.
* Adds a validation message when the department isn't selected.

The Razor Page uses the [Select Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-select-tag-helper):

```
<div class="form-group">
    <label asp-for="Course.Department" class="control-label"></label>
    <select asp-for="Course.DepartmentID" class="form-control"
            asp-items="@Model.DepartmentNameSL">
        <option value="">-- Select Department --</option>
    </select>
    <span asp-validation-for="Course.DepartmentID" class="text-danger" />
</div>
```

Test the Create page. The Create page displays the department name rather than the department ID.

### Update the Course Edit page model

Update `Pages/Courses/Edit.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class EditModel : DepartmentNamePageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Course Course { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Course = await _context.Courses
                .Include(c => c.Department).FirstOrDefaultAsync(m => m.CourseID == id);

            if (Course == null)
            {
                return NotFound();
            }

            // Populate departments dropdown. Course.DepartmentID determines the selected item.
            PopulateDepartmentsDropDownList(_context, Course.DepartmentID);
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var courseToUpdate = await _context.Courses.FindAsync(id);

            if (courseToUpdate == null)
            {
                return NotFound();
            }

            if (await TryUpdateModelAsync<Course>(
                 courseToUpdate,
                 "course",   // Prefix for form value.
                   c => c.Credits, c => c.DepartmentID, c => c.Title))
            {
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }

            // Repopulate departments dropdown. courseToUpdate.DepartmentID determines the selected item.
            PopulateDepartmentsDropDownList(_context, courseToUpdate.DepartmentID);
            return Page();
        }       
    }
}
```

The changes are similar to those made in the Create page model. In the preceding code, `PopulateDepartmentsDropDownList` passes in the department ID. When using the Select Tag Helper with `asp-for="Course.DepartmentID"`, the selected item in the drop-down list is determined by the value of `Course.DepartmentID`, not by the `selectedDepartment` parameter passed to the SelectList constructor.

### Update the Course Edit Razor page

Update `Pages/Courses/Edit.cshtml` with the following code:

```
@page
@model ContosoUniversity.Pages.Courses.EditModel

@{
    ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<h4>Course</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Course.CourseID" />
            <div class="form-group">
                <label asp-for="Course.CourseID" class="control-label"></label>
                <div>@Html.DisplayFor(model => model.Course.CourseID)</div>
            </div>
            <div class="form-group">
                <label asp-for="Course.Title" class="control-label"></label>
                <input asp-for="Course.Title" class="form-control" />
                <span asp-validation-for="Course.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Credits" class="control-label"></label>
                <input asp-for="Course.Credits" class="form-control" />
                <span asp-validation-for="Course.Credits" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Department" class="control-label"></label>
                <select asp-for="Course.DepartmentID" class="form-control" 
                        asp-items="@Model.DepartmentNameSL"></select>
                <span asp-validation-for="Course.DepartmentID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding code makes the following changes:

* Displays the course ID. Generally the Primary Key (PK) of an entity isn't displayed. PKs are usually meaningless to users. In this case, the PK is the course number.
* Changes the caption for the Department drop-down from **DepartmentID** to **Department**.
* Replaces `"ViewBag.DepartmentID"` with `DepartmentNameSL`, which is in the base class.

The page contains a hidden field (`<input type="hidden">`) for the course number. Adding a `<label>` tag helper with `asp-for="Course.CourseID"` doesn't eliminate the need for the hidden field. `<input type="hidden">` is required for the course number to be included in the posted data when the user selects **Save**.

## Update the Course page models

[AsNoTracking](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.asnotracking) can improve performance when tracking isn't required.

Update `Pages/Courses/Delete.cshtml.cs` and `Pages/Courses/Details.cshtml.cs` by adding `AsNoTracking` to the `OnGetAsync` methods:

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Course = await _context.Courses
        .AsNoTracking()
        .Include(c => c.Department)
        .FirstOrDefaultAsync(m => m.CourseID == id);

    if (Course == null)
    {
        return NotFound();
    }
    return Page();
}
```

### Update the Course Razor pages

Update `Pages/Courses/Delete.cshtml` with the following code:

```
@page
@model ContosoUniversity.Pages.Courses.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Course</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.CourseID)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.CourseID)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Credits)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Credits)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Department)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Department.Name)
        </dd>
    </dl>
    
    <form method="post">
        <input type="hidden" asp-for="Course.CourseID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>
```

Make the same changes to the Details page.

```
@page
@model ContosoUniversity.Pages.Courses.DetailsModel

@{
    ViewData["Title"] = "Details";
}

<h2>Details</h2>

<div>
    <h4>Course</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.CourseID)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.CourseID)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Credits)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Credits)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Department)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Department.Name)
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Course.CourseID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>
```

## Test the Course pages

Test the create, edit, details, and delete pages.

## Update the instructor Create and Edit pages

Instructors may teach any number of courses. The following image shows the instructor Edit page with an array of course checkboxes.

The checkboxes enable changes to courses an instructor is assigned to. A checkbox is displayed for every course in the database. Courses that the instructor is assigned to are selected. The user can select or clear checkboxes to change course assignments. If the number of courses were much greater, a different UI might work better. But the method of managing a many-to-many relationship shown here wouldn't change. To create or delete relationships, you manipulate a join entity.

### Create a class for assigned courses data

Create `Models/SchoolViewModels/AssignedCourseData.cs` with the following code:

```
namespace ContosoUniversity.Models.SchoolViewModels
{
    public class AssignedCourseData
    {
        public int CourseID { get; set; }
        public string Title { get; set; }
        public bool Assigned { get; set; }
    }
}
```

The `AssignedCourseData` class contains data to create the checkboxes for courses assigned to an instructor.

### Create an Instructor page model base class

Create the `Pages/Instructors/InstructorCoursesPageModel.cs` base class:

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Pages.Instructors
{
    public class InstructorCoursesPageModel : PageModel
    {
        public List<AssignedCourseData> AssignedCourseDataList;

        public void PopulateAssignedCourseData(SchoolContext context,
                                               Instructor instructor)
        {
            var allCourses = context.Courses;
            var instructorCourses = new HashSet<int>(
                instructor.Courses.Select(c => c.CourseID));
            AssignedCourseDataList = new List<AssignedCourseData>();
            foreach (var course in allCourses)
            {
                AssignedCourseDataList.Add(new AssignedCourseData
                {
                    CourseID = course.CourseID,
                    Title = course.Title,
                    Assigned = instructorCourses.Contains(course.CourseID)
                });
            }
        }
    }
}
```

The `InstructorCoursesPageModel` is the base class for the Edit and Create page models. `PopulateAssignedCourseData` reads all `Course` entities to populate `AssignedCourseDataList`. For each course, the code sets the `CourseID`, title, and whether or not the instructor is assigned to the course. A [HashSet](/en-us/dotnet/api/system.collections.generic.hashset-1) is used for efficient lookups.

### Handle office location

Another relationship the edit page has to handle is the one-to-zero-or-one relationship that the Instructor entity has with the `OfficeAssignment` entity. The instructor edit code must handle the following scenarios:

* If the user clears the office assignment, delete the `OfficeAssignment` entity.
* If the user enters an office assignment and it was empty, create a new `OfficeAssignment` entity.
* If the user changes the office assignment, update the `OfficeAssignment` entity.

## Update the Instructor Edit page model

Update `Pages/Instructors/Edit.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class EditModel : InstructorCoursesPageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Instructor Instructor { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Instructor = await _context.Instructors
                .Include(i => i.OfficeAssignment)
                .Include(i => i.Courses)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Instructor == null)
            {
                return NotFound();
            }
            PopulateAssignedCourseData(_context, Instructor);
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id, string[] selectedCourses)
        {
            if (id == null)
            {
                return NotFound();
            }

            var instructorToUpdate = await _context.Instructors
                .Include(i => i.OfficeAssignment)
                .Include(i => i.Courses)
                .FirstOrDefaultAsync(s => s.ID == id);

            if (instructorToUpdate == null)
            {
                return NotFound();
            }

            if (await TryUpdateModelAsync<Instructor>(
                instructorToUpdate,
                "Instructor",
                i => i.FirstMidName, i => i.LastName,
                i => i.HireDate, i => i.OfficeAssignment))
            {
                if (String.IsNullOrWhiteSpace(
                    instructorToUpdate.OfficeAssignment?.Location))
                {
                    instructorToUpdate.OfficeAssignment = null;
                }
                UpdateInstructorCourses(selectedCourses, instructorToUpdate);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            UpdateInstructorCourses(selectedCourses, instructorToUpdate);
            PopulateAssignedCourseData(_context, instructorToUpdate);
            return Page();
        }

        public void UpdateInstructorCourses(string[] selectedCourses,
                                            Instructor instructorToUpdate)
        {
            if (selectedCourses == null)
            {
                instructorToUpdate.Courses = new List<Course>();
                return;
            }

            var selectedCoursesHS = new HashSet<string>(selectedCourses);
            var instructorCourses = new HashSet<int>
                (instructorToUpdate.Courses.Select(c => c.CourseID));
            foreach (var course in _context.Courses)
            {
                if (selectedCoursesHS.Contains(course.CourseID.ToString()))
                {
                    if (!instructorCourses.Contains(course.CourseID))
                    {
                        instructorToUpdate.Courses.Add(course);
                    }
                }
                else
                {
                    if (instructorCourses.Contains(course.CourseID))
                    {
                        var courseToRemove = instructorToUpdate.Courses.Single(
                                                        c => c.CourseID == course.CourseID);
                        instructorToUpdate.Courses.Remove(courseToRemove);
                    }
                }
            }
        }
    }
}
```

The preceding code:

* Gets the current `Instructor` entity from the database using eager loading for the `OfficeAssignment` and `Courses` navigation properties.
* Updates the retrieved `Instructor` entity with values from the model binder. [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) prevents [overposting](crud?view=aspnetcore-10.0#overposting).
* If the office location is blank, sets `Instructor.OfficeAssignment` to null. When `Instructor.OfficeAssignment` is null, the related row in the `OfficeAssignment` table is deleted.
* Calls `PopulateAssignedCourseData` in `OnGetAsync` to provide information for the checkboxes using the `AssignedCourseData` view model class.
* Calls `UpdateInstructorCourses` in `OnPostAsync` to apply information from the checkboxes to the Instructor entity being edited.
* Calls `PopulateAssignedCourseData` and `UpdateInstructorCourses` in `OnPostAsync` if [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) fails. These method calls restore the assigned course data entered on the page when it is redisplayed with an error message.

Since the Razor page doesn't have a collection of Course entities, the model binder can't automatically update the `Courses` navigation property. Instead of using the model binder to update the `Courses` navigation property, that's done in the new `UpdateInstructorCourses` method. Therefore you need to exclude the `Courses` property from model binding. This doesn't require any change to the code that calls [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) because you're using the overload with declared properties and `Courses` isn't in the include list.

If no checkboxes were selected, the code in `UpdateInstructorCourses` initializes the `instructorToUpdate.Courses` with an empty collection and returns:

```
if (selectedCourses == null)
{
    instructorToUpdate.Courses = new List<Course>();
    return;
}
```

The code then loops through all courses in the database and checks each course against the ones currently assigned to the instructor versus the ones that were selected in the page. To facilitate efficient lookups, the latter two collections are stored in `HashSet` objects.

If the checkbox for a course is selected but the course is ***not*** in the `Instructor.Courses` navigation property, the course is added to the collection in the navigation property.

```
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
    if (!instructorCourses.Contains(course.CourseID))
    {
        instructorToUpdate.Courses.Add(course);
    }
}
```

If the checkbox for a course is ***not*** selected, but the course is in the `Instructor.Courses` navigation property, the course is removed from the navigation property.

```
else
{
    if (instructorCourses.Contains(course.CourseID))
    {
        var courseToRemove = instructorToUpdate.Courses.Single(
                                        c => c.CourseID == course.CourseID);
        instructorToUpdate.Courses.Remove(courseToRemove);
    }
}
```

### Update the Instructor Edit Razor page

Update `Pages/Instructors/Edit.cshtml` with the following code:

```
@page
@model ContosoUniversity.Pages.Instructors.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Instructor.ID" />
            <div class="form-group">
                <label asp-for="Instructor.LastName" class="control-label"></label>
                <input asp-for="Instructor.LastName" class="form-control" />
                <span asp-validation-for="Instructor.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.FirstMidName" class="control-label"></label>
                <input asp-for="Instructor.FirstMidName" class="form-control" />
                <span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.HireDate" class="control-label"></label>
                <input asp-for="Instructor.HireDate" class="form-control" />
                <span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
                <input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
                <span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
            </div>
            <div class="form-group">
                <div class="table">
                    <table>
                        <tr>
                            @{
                                int cnt = 0;

                                foreach (var course in Model.AssignedCourseDataList)
                                {
                                    if (cnt++ % 3 == 0)
                                    {
                                        @:</tr><tr>
                                    }
                                    @:<td>
                                        <input type="checkbox"
                                               name="selectedCourses"
                                               value="@course.CourseID"
                                               @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                               @course.CourseID @:  @course.Title
                                    @:</td>
                                }
                                @:</tr>
                            }
                    </table>
                </div>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding code creates an HTML table that has three columns. Each column has a checkbox and a caption containing the course number and title. The checkboxes all have the same name ("selectedCourses"). Using the same name informs the model binder to treat them as a group. The value attribute of each checkbox is set to `CourseID`. When the page is posted, the model binder passes an array that consists of the `CourseID` values for only the checkboxes that are selected.

When the checkboxes are initially rendered, courses assigned to the instructor are selected.

Note: The approach taken here to edit instructor course data works well when there's a limited number of courses. For collections that are much larger, a different UI and a different updating method would be more useable and efficient.

Run the app and test the updated Instructors Edit page. Change some course assignments. The changes are reflected on the Index page.

### Update the Instructor Create page

Update the Instructor Create page model and with code similar to the Edit page:

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class CreateModel : InstructorCoursesPageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<InstructorCoursesPageModel> _logger;

        public CreateModel(SchoolContext context,
                          ILogger<InstructorCoursesPageModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        public IActionResult OnGet()
        {
            var instructor = new Instructor();
            instructor.Courses = new List<Course>();

            // Provides an empty collection for the foreach loop
            // foreach (var course in Model.AssignedCourseDataList)
            // in the Create Razor page.
            PopulateAssignedCourseData(_context, instructor);
            return Page();
        }

        [BindProperty]
        public Instructor Instructor { get; set; }

        public async Task<IActionResult> OnPostAsync(string[] selectedCourses)
        {
            var newInstructor = new Instructor();

            if (selectedCourses.Length > 0)
            {
                newInstructor.Courses = new List<Course>();
                // Load collection with one DB call.
                _context.Courses.Load();
            }

            // Add selected Courses courses to the new instructor.
            foreach (var course in selectedCourses)
            {
                var foundCourse = await _context.Courses.FindAsync(int.Parse(course));
                if (foundCourse != null)
                {
                    newInstructor.Courses.Add(foundCourse);
                }
                else
                {
                    _logger.LogWarning("Course {course} not found", course);
                }
            }

            try
            {
                if (await TryUpdateModelAsync<Instructor>(
                                newInstructor,
                                "Instructor",
                                i => i.FirstMidName, i => i.LastName,
                                i => i.HireDate, i => i.OfficeAssignment))
                {
                    _context.Instructors.Add(newInstructor);
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                return RedirectToPage("./Index");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }

            PopulateAssignedCourseData(_context, newInstructor);
            return Page();
        }
    }
}
```

The preceding code:

* Adds [logging](../../fundamentals/logging/?view=aspnetcore-10.0) for warning and error messages.

* Calls [Load](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.load), which fetches all the Courses in one database call. For small collections this is an optimization when using [FindAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.findasync). `FindAsync` returns the tracked entity without a request to the database.

  ```
  public async Task<IActionResult> OnPostAsync(string[] selectedCourses)
  {
      var newInstructor = new Instructor();

      if (selectedCourses.Length > 0)
      {
          newInstructor.Courses = new List<Course>();
          // Load collection with one DB call.
          _context.Courses.Load();
      }

      // Add selected Courses courses to the new instructor.
      foreach (var course in selectedCourses)
      {
          var foundCourse = await _context.Courses.FindAsync(int.Parse(course));
          if (foundCourse != null)
          {
              newInstructor.Courses.Add(foundCourse);
          }
          else
          {
              _logger.LogWarning("Course {course} not found", course);
          }
      }

      try
      {
          if (await TryUpdateModelAsync<Instructor>(
                          newInstructor,
                          "Instructor",
                          i => i.FirstMidName, i => i.LastName,
                          i => i.HireDate, i => i.OfficeAssignment))
          {
              _context.Instructors.Add(newInstructor);
              await _context.SaveChangesAsync();
              return RedirectToPage("./Index");
          }
          return RedirectToPage("./Index");
      }
      catch (Exception ex)
      {
          _logger.LogError(ex.Message);
      }

      PopulateAssignedCourseData(_context, newInstructor);
      return Page();
  }
  ```

* `_context.Instructors.Add(newInstructor)` creates a new `Instructor` using [many-to-many](/en-us/ef/core/modeling/relationships#many-to-many) relationships without explicitly mapping the join table. [Many-to-many was added in EF 5.0](/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew).

Test the instructor Create page.

Update the Instructor Create Razor page with code similar to the Edit page:

```
@page
@model ContosoUniversity.Pages.Instructors.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Instructor</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Instructor.LastName" class="control-label"></label>
                <input asp-for="Instructor.LastName" class="form-control" />
                <span asp-validation-for="Instructor.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.FirstMidName" class="control-label"></label>
                <input asp-for="Instructor.FirstMidName" class="form-control" />
                <span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.HireDate" class="control-label"></label>
                <input asp-for="Instructor.HireDate" class="form-control" />
                <span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
                <input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
                <span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
            </div>
            <div class="form-group">
                <div class="table">
                    <table>
                        <tr>
                            @{
                                int cnt = 0;

                                foreach (var course in Model.AssignedCourseDataList)
                                {
                                    if (cnt++ % 3 == 0)
                                    {
                                        @:</tr><tr>
                                    }
                                    @:<td>
                                        <input type="checkbox"
                                               name="selectedCourses"
                                               value="@course.CourseID"
                                               @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                               @course.CourseID @:  @course.Title
                                    @:</td>
                                }
                                @:</tr>
                            }
                    </table>
                </div>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

## Update the Instructor Delete page

Update `Pages/Instructors/Delete.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Instructor Instructor { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Instructor = await _context.Instructors.FirstOrDefaultAsync(m => m.ID == id);

            if (Instructor == null)
            {
                return NotFound();
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Instructor instructor = await _context.Instructors
                .Include(i => i.Courses)
                .SingleAsync(i => i.ID == id);

            if (instructor == null)
            {
                return RedirectToPage("./Index");
            }

            var departments = await _context.Departments
                .Where(d => d.InstructorID == id)
                .ToListAsync();
            departments.ForEach(d => d.InstructorID = null);

            _context.Instructors.Remove(instructor);

            await _context.SaveChangesAsync();
            return RedirectToPage("./Index");
        }
    }
}
```

The preceding code makes the following changes:

* Uses eager loading for the `Courses` navigation property. `Courses` must be included or they aren't deleted when the instructor is deleted. To avoid needing to read them, configure cascade delete in the database.

* If the instructor to be deleted is assigned as administrator of any departments, removes the instructor assignment from those departments.

Run the app and test the Delete page.

## Next steps

[Previous tutorial](read-related-data?view=aspnetcore-10.0) [Next tutorial](concurrency?view=aspnetcore-10.0)

This tutorial shows how to update related data. The following illustrations show some of the completed pages.

## Update the Course Create and Edit pages

The scaffolded code for the Course Create and Edit pages has a Department drop-down list that shows Department ID (an integer). The drop-down should show the Department name, so both of these pages need a list of department names. To provide that list, use a base class for the Create and Edit pages.

### Create a base class for Course Create and Edit

Create a `Pages/Courses/DepartmentNamePageModel.cs` file with the following code:

```
using ContosoUniversity.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace ContosoUniversity.Pages.Courses
{
    public class DepartmentNamePageModel : PageModel
    {
        public SelectList DepartmentNameSL { get; set; }

        public void PopulateDepartmentsDropDownList(SchoolContext _context,
            object selectedDepartment = null)
        {
            var departmentsQuery = from d in _context.Departments
                                   orderby d.Name // Sort by name.
                                   select d;

            DepartmentNameSL = new SelectList(departmentsQuery.AsNoTracking(),
                        "DepartmentID", "Name", selectedDepartment);
        }
    }
}
```

The preceding code creates a [SelectList](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.selectlist) to contain the list of department names. The `selectedDepartment` parameter allows the calling code to specify the item that will be selected when the drop-down list is rendered. However, when using the Select Tag Helper with `asp-for`, the selected item is determined by the model property value (such as `Course.DepartmentID`), not by the `selectedDepartment` parameter.

The Create and Edit page model classes will derive from `DepartmentNamePageModel`.

### Update the Course Create page model

A Course is assigned to a Department. The base class for the Create and Edit pages provides a `SelectList` for selecting the department. The drop-down list that uses the `SelectList` sets the `Course.DepartmentID` foreign key (FK) property. EF Core uses the `Course.DepartmentID` FK to load the `Department` navigation property.

Update `Pages/Courses/Create.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class CreateModel : DepartmentNamePageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public CreateModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            PopulateDepartmentsDropDownList(_context);
            return Page();
        }

        [BindProperty]
        public Course Course { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            var emptyCourse = new Course();

            if (await TryUpdateModelAsync<Course>(
                 emptyCourse,
                 "course",   // Prefix for form value.
                 s => s.CourseID, s => s.DepartmentID, s => s.Title, s => s.Credits))
            {
                _context.Courses.Add(emptyCourse);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }

            // Repopulate departments dropdown. emptyCourse.DepartmentID determines the selected item.
            PopulateDepartmentsDropDownList(_context, emptyCourse.DepartmentID);
            return Page();
        }
      }
}
```

The preceding code:

* Derives from `DepartmentNamePageModel`.
* Uses `TryUpdateModelAsync` to prevent [overposting](crud?view=aspnetcore-10.0#overposting).
* Removes `ViewData["DepartmentID"]`. `DepartmentNameSL` from the base class is a strongly typed model and will be used by the Razor page. Strongly typed models are preferred over weakly typed. For more information, see [Weakly typed data (ViewData and ViewBag)](../../mvc/views/overview?view=aspnetcore-10.0#VD_VB).

### Update the Course Create Razor page

Update `Pages/Courses/Create.cshtml` with the following code:

```
@page
@model ContosoUniversity.Pages.Courses.CreateModel
@{
    ViewData["Title"] = "Create Course";
}
<h2>Create</h2>
<h4>Course</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Course.CourseID" class="control-label"></label>
                <input asp-for="Course.CourseID" class="form-control" />
                <span asp-validation-for="Course.CourseID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Title" class="control-label"></label>
                <input asp-for="Course.Title" class="form-control" />
                <span asp-validation-for="Course.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Credits" class="control-label"></label>
                <input asp-for="Course.Credits" class="form-control" />
                <span asp-validation-for="Course.Credits" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Department" class="control-label"></label>
                <select asp-for="Course.DepartmentID" class="form-control"
                        asp-items="@Model.DepartmentNameSL">
                    <option value="">-- Select Department --</option>
                </select>
                <span asp-validation-for="Course.DepartmentID" class="text-danger" />
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding code makes the following changes:

* Changes the caption from **DepartmentID** to **Department**.
* Replaces `"ViewBag.DepartmentID"` with `DepartmentNameSL` (from the base class).
* Adds the "Select Department" option. This change renders "Select Department" in the drop-down when no department has been selected yet, rather than the first department.
* Adds a validation message when the department isn't selected.

The Razor Page uses the [Select Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-select-tag-helper):

```
<div class="form-group">
    <label asp-for="Course.Department" class="control-label"></label>
    <select asp-for="Course.DepartmentID" class="form-control"
            asp-items="@Model.DepartmentNameSL">
        <option value="">-- Select Department --</option>
    </select>
    <span asp-validation-for="Course.DepartmentID" class="text-danger" />
</div>
```

Test the Create page. The Create page displays the department name rather than the department ID.

### Update the Course Edit page model

Update `Pages/Courses/Edit.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class EditModel : DepartmentNamePageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Course Course { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Course = await _context.Courses
                .Include(c => c.Department).FirstOrDefaultAsync(m => m.CourseID == id);

            if (Course == null)
            {
                return NotFound();
            }

            // Populate departments dropdown. Course.DepartmentID determines the selected item.
            PopulateDepartmentsDropDownList(_context, Course.DepartmentID);
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var courseToUpdate = await _context.Courses.FindAsync(id);

            if (courseToUpdate == null)
            {
                return NotFound();
            }

            if (await TryUpdateModelAsync<Course>(
                 courseToUpdate,
                 "course",   // Prefix for form value.
                   c => c.Credits, c => c.DepartmentID, c => c.Title))
            {
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }

            // Repopulate departments dropdown. courseToUpdate.DepartmentID determines the selected item.
            PopulateDepartmentsDropDownList(_context, courseToUpdate.DepartmentID);
            return Page();
        }       
    }
}
```

The changes are similar to those made in the Create page model. In the preceding code, `PopulateDepartmentsDropDownList` passes in the department ID. When using the Select Tag Helper with `asp-for="Course.DepartmentID"`, the selected item in the drop-down list is determined by the value of `Course.DepartmentID`, not by the `selectedDepartment` parameter passed to the SelectList constructor.

### Update the Course Edit Razor page

Update `Pages/Courses/Edit.cshtml` with the following code:

```
@page
@model ContosoUniversity.Pages.Courses.EditModel

@{
    ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<h4>Course</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Course.CourseID" />
            <div class="form-group">
                <label asp-for="Course.CourseID" class="control-label"></label>
                <div>@Html.DisplayFor(model => model.Course.CourseID)</div>
            </div>
            <div class="form-group">
                <label asp-for="Course.Title" class="control-label"></label>
                <input asp-for="Course.Title" class="form-control" />
                <span asp-validation-for="Course.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Credits" class="control-label"></label>
                <input asp-for="Course.Credits" class="form-control" />
                <span asp-validation-for="Course.Credits" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Department" class="control-label"></label>
                <select asp-for="Course.DepartmentID" class="form-control" 
                        asp-items="@Model.DepartmentNameSL"></select>
                <span asp-validation-for="Course.DepartmentID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding code makes the following changes:

* Displays the course ID. Generally the Primary Key (PK) of an entity isn't displayed. PKs are usually meaningless to users. In this case, the PK is the course number.
* Changes the caption for the Department drop-down from **DepartmentID** to **Department**.
* Replaces `"ViewBag.DepartmentID"` with `DepartmentNameSL` (from the base class).

The page contains a hidden field (`<input type="hidden">`) for the course number. Adding a `<label>` tag helper with `asp-for="Course.CourseID"` doesn't eliminate the need for the hidden field. `<input type="hidden">` is required for the course number to be included in the posted data when the user clicks **Save**.

## Update the Course Details and Delete pages

[AsNoTracking](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.asnotracking) can improve performance when tracking isn't required.

### Update the Course page models

Update `Pages/Courses/Delete.cshtml.cs` with the following code to add `AsNoTracking`:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Course Course { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Course = await _context.Courses
                .AsNoTracking()
                .Include(c => c.Department)
                .FirstOrDefaultAsync(m => m.CourseID == id);

            if (Course == null)
            {
                return NotFound();
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Course = await _context.Courses.FindAsync(id);

            if (Course != null)
            {
                _context.Courses.Remove(Course);
                await _context.SaveChangesAsync();
            }

            return RedirectToPage("./Index");
        }
    }
}
```

Make the same change in the `Pages/Courses/Details.cshtml.cs` file:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class DetailsModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DetailsModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public Course Course { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Course = await _context.Courses
                 .AsNoTracking()
                 .Include(c => c.Department)
                 .FirstOrDefaultAsync(m => m.CourseID == id);

            if (Course == null)
            {
                return NotFound();
            }
            return Page();
        }
    }
}
```

### Update the Course Razor pages

Update `Pages/Courses/Delete.cshtml` with the following code:

```
@page
@model ContosoUniversity.Pages.Courses.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Course</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.CourseID)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.CourseID)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Credits)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Credits)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Department)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Department.Name)
        </dd>
    </dl>
    
    <form method="post">
        <input type="hidden" asp-for="Course.CourseID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>
```

Make the same changes to the Details page.

```
@page
@model ContosoUniversity.Pages.Courses.DetailsModel

@{
    ViewData["Title"] = "Details";
}

<h2>Details</h2>

<div>
    <h4>Course</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.CourseID)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.CourseID)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Credits)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Credits)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Course.Department)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Course.Department.Name)
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Course.CourseID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>
```

## Test the Course pages

Test the create, edit, details, and delete pages.

## Update the instructor Create and Edit pages

Instructors may teach any number of courses. The following image shows the instructor Edit page with an array of course checkboxes.

The checkboxes enable changes to courses an instructor is assigned to. A checkbox is displayed for every course in the database. Courses that the instructor is assigned to are selected. The user can select or clear checkboxes to change course assignments. If the number of courses were much greater, a different UI might work better. But the method of managing a many-to-many relationship shown here wouldn't change. To create or delete relationships, you manipulate a join entity.

### Create a class for assigned courses data

Create `Models/SchoolViewModels/AssignedCourseData.cs` with the following code:

```
namespace ContosoUniversity.Models.SchoolViewModels
{
    public class AssignedCourseData
    {
        public int CourseID { get; set; }
        public string Title { get; set; }
        public bool Assigned { get; set; }
    }
}
```

The `AssignedCourseData` class contains data to create the checkboxes for courses assigned to an instructor.

### Create an Instructor page model base class

Create the `Pages/Instructors/InstructorCoursesPageModel.cs` base class:

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Pages.Instructors
{
    public class InstructorCoursesPageModel : PageModel
    {

        public List<AssignedCourseData> AssignedCourseDataList;

        public void PopulateAssignedCourseData(SchoolContext context, 
                                               Instructor instructor)
        {
            var allCourses = context.Courses;
            var instructorCourses = new HashSet<int>(
                instructor.CourseAssignments.Select(c => c.CourseID));
            AssignedCourseDataList = new List<AssignedCourseData>();
            foreach (var course in allCourses)
            {
                AssignedCourseDataList.Add(new AssignedCourseData
                {
                    CourseID = course.CourseID,
                    Title = course.Title,
                    Assigned = instructorCourses.Contains(course.CourseID)
                });
            }
        }

        public void UpdateInstructorCourses(SchoolContext context, 
            string[] selectedCourses, Instructor instructorToUpdate)
        {
            if (selectedCourses == null)
            {
                instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
                return;
            }

            var selectedCoursesHS = new HashSet<string>(selectedCourses);
            var instructorCourses = new HashSet<int>
                (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
            foreach (var course in context.Courses)
            {
                if (selectedCoursesHS.Contains(course.CourseID.ToString()))
                {
                    if (!instructorCourses.Contains(course.CourseID))
                    {
                        instructorToUpdate.CourseAssignments.Add(
                            new CourseAssignment
                            {
                                InstructorID = instructorToUpdate.ID,
                                CourseID = course.CourseID
                            });
                    }
                }
                else
                {
                    if (instructorCourses.Contains(course.CourseID))
                    {
                        CourseAssignment courseToRemove
                            = instructorToUpdate
                                .CourseAssignments
                                .SingleOrDefault(i => i.CourseID == course.CourseID);
                        context.Remove(courseToRemove);
                    }
                }
            }
        }
    }
}
```

The `InstructorCoursesPageModel` is the base class you will use for the Edit and Create page models. `PopulateAssignedCourseData` reads all `Course` entities to populate `AssignedCourseDataList`. For each course, the code sets the `CourseID`, title, and whether or not the instructor is assigned to the course. A [HashSet](/en-us/dotnet/api/system.collections.generic.hashset-1) is used for efficient lookups.

Since the Razor page doesn't have a collection of Course entities, the model binder can't automatically update the `CourseAssignments` navigation property. Instead of using the model binder to update the `CourseAssignments` navigation property, you do that in the new `UpdateInstructorCourses` method. Therefore you need to exclude the `CourseAssignments` property from model binding. This doesn't require any change to the code that calls `TryUpdateModel` because you're using the overload with declared properties and `CourseAssignments` isn't in the include list.

If no checkboxes were selected, the code in `UpdateInstructorCourses` initializes the `CourseAssignments` navigation property with an empty collection and returns:

```
if (selectedCourses == null)
{
    instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
    return;
}
```

The code then loops through all courses in the database and checks each course against the ones currently assigned to the instructor versus the ones that were selected in the page. To facilitate efficient lookups, the latter two collections are stored in `HashSet` objects.

If the checkbox for a course was selected but the course isn't in the `Instructor.CourseAssignments` navigation property, the course is added to the collection in the navigation property.

```
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
    if (!instructorCourses.Contains(course.CourseID))
    {
        instructorToUpdate.CourseAssignments.Add(
            new CourseAssignment
            {
                InstructorID = instructorToUpdate.ID,
                CourseID = course.CourseID
            });
    }
}
```

If the checkbox for a course wasn't selected, but the course is in the `Instructor.CourseAssignments` navigation property, the course is removed from the navigation property.

```
else
{
    if (instructorCourses.Contains(course.CourseID))
    {
        CourseAssignment courseToRemove
            = instructorToUpdate
                .CourseAssignments
                .SingleOrDefault(i => i.CourseID == course.CourseID);
        context.Remove(courseToRemove);
    }
}
```

### Handle office location

Another relationship the edit page has to handle is the one-to-zero-or-one relationship that the Instructor entity has with the `OfficeAssignment` entity. The instructor edit code must handle the following scenarios:

* If the user clears the office assignment, delete the `OfficeAssignment` entity.
* If the user enters an office assignment and it was empty, create a new `OfficeAssignment` entity.
* If the user changes the office assignment, update the `OfficeAssignment` entity.

### Update the Instructor Edit page model

Update `Pages/Instructors/Edit.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class EditModel : InstructorCoursesPageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Instructor Instructor { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Instructor = await _context.Instructors
                .Include(i => i.OfficeAssignment)
                .Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Instructor == null)
            {
                return NotFound();
            }
            PopulateAssignedCourseData(_context, Instructor);
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id, string[] selectedCourses)
        {
            if (id == null)
            {
                return NotFound();
            }

            var instructorToUpdate = await _context.Instructors
                .Include(i => i.OfficeAssignment)
                .Include(i => i.CourseAssignments)
                    .ThenInclude(i => i.Course)
                .FirstOrDefaultAsync(s => s.ID == id);

            if (instructorToUpdate == null)
            {
                return NotFound();
            }

            if (await TryUpdateModelAsync<Instructor>(
                instructorToUpdate,
                "Instructor",
                i => i.FirstMidName, i => i.LastName,
                i => i.HireDate, i => i.OfficeAssignment))
            {
                if (String.IsNullOrWhiteSpace(
                    instructorToUpdate.OfficeAssignment?.Location))
                {
                    instructorToUpdate.OfficeAssignment = null;
                }
                UpdateInstructorCourses(_context, selectedCourses, instructorToUpdate);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            UpdateInstructorCourses(_context, selectedCourses, instructorToUpdate);
            PopulateAssignedCourseData(_context, instructorToUpdate);
            return Page();
        }
    }
}
```

The preceding code:

* Gets the current `Instructor` entity from the database using eager loading for the `OfficeAssignment`, `CourseAssignment`, and `CourseAssignment.Course` navigation properties.
* Updates the retrieved `Instructor` entity with values from the model binder. `TryUpdateModel` prevents [overposting](crud?view=aspnetcore-10.0#overposting).
* If the office location is blank, sets `Instructor.OfficeAssignment` to null. When `Instructor.OfficeAssignment` is null, the related row in the `OfficeAssignment` table is deleted.
* Calls `PopulateAssignedCourseData` in `OnGetAsync` to provide information for the checkboxes using the `AssignedCourseData` view model class.
* Calls `UpdateInstructorCourses` in `OnPostAsync` to apply information from the checkboxes to the Instructor entity being edited.
* Calls `PopulateAssignedCourseData` and `UpdateInstructorCourses` in `OnPostAsync` if `TryUpdateModel` fails. These method calls restore the assigned course data entered on the page when it is redisplayed with an error message.

### Update the Instructor Edit Razor page

Update `Pages/Instructors/Edit.cshtml` with the following code:

```
@page
@model ContosoUniversity.Pages.Instructors.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Instructor.ID" />
            <div class="form-group">
                <label asp-for="Instructor.LastName" class="control-label"></label>
                <input asp-for="Instructor.LastName" class="form-control" />
                <span asp-validation-for="Instructor.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.FirstMidName" class="control-label"></label>
                <input asp-for="Instructor.FirstMidName" class="form-control" />
                <span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.HireDate" class="control-label"></label>
                <input asp-for="Instructor.HireDate" class="form-control" />
                <span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
                <input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
                <span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
            </div>
            <div class="form-group">
                <div class="table">
                    <table>
                        <tr>
                            @{
                                int cnt = 0;

                                foreach (var course in Model.AssignedCourseDataList)
                                {
                                    if (cnt++ % 3 == 0)
                                    {
                                        @:</tr><tr>
                                    }
                                    @:<td>
                                        <input type="checkbox"
                                               name="selectedCourses"
                                               value="@course.CourseID"
                                               @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                               @course.CourseID @:  @course.Title
                                    @:</td>
                                }
                                @:</tr>
                            }
                    </table>
                </div>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding code creates an HTML table that has three columns. Each column has a checkbox and a caption containing the course number and title. The checkboxes all have the same name ("selectedCourses"). Using the same name informs the model binder to treat them as a group. The value attribute of each checkbox is set to `CourseID`. When the page is posted, the model binder passes an array that consists of the `CourseID` values for only the checkboxes that are selected.

When the checkboxes are initially rendered, courses assigned to the instructor are selected.

Note: The approach taken here to edit instructor course data works well when there's a limited number of courses. For collections that are much larger, a different UI and a different updating method would be more useable and efficient.

Run the app and test the updated Instructors Edit page. Change some course assignments. The changes are reflected on the Index page.

### Update the Instructor Create page

Update the Instructor Create page model and Razor page with code similar to the Edit page:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class CreateModel : InstructorCoursesPageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public CreateModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            var instructor = new Instructor();
            instructor.CourseAssignments = new List<CourseAssignment>();

            // Provides an empty collection for the foreach loop
            // foreach (var course in Model.AssignedCourseDataList)
            // in the Create Razor page.
            PopulateAssignedCourseData(_context, instructor);
            return Page();
        }

        [BindProperty]
        public Instructor Instructor { get; set; }

        public async Task<IActionResult> OnPostAsync(string[] selectedCourses)
        {
            var newInstructor = new Instructor();
            if (selectedCourses != null)
            {
                newInstructor.CourseAssignments = new List<CourseAssignment>();
                foreach (var course in selectedCourses)
                {
                    var courseToAdd = new CourseAssignment
                    {
                        CourseID = int.Parse(course)
                    };
                    newInstructor.CourseAssignments.Add(courseToAdd);
                }
            }

            if (await TryUpdateModelAsync<Instructor>(
                newInstructor,
                "Instructor",
                i => i.FirstMidName, i => i.LastName,
                i => i.HireDate, i => i.OfficeAssignment))
            {
                _context.Instructors.Add(newInstructor);                
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            PopulateAssignedCourseData(_context, newInstructor);
            return Page();
        }
    }
}
```

```
@page
@model ContosoUniversity.Pages.Instructors.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Instructor</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Instructor.LastName" class="control-label"></label>
                <input asp-for="Instructor.LastName" class="form-control" />
                <span asp-validation-for="Instructor.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.FirstMidName" class="control-label"></label>
                <input asp-for="Instructor.FirstMidName" class="form-control" />
                <span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.HireDate" class="control-label"></label>
                <input asp-for="Instructor.HireDate" class="form-control" />
                <span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
                <input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
                <span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
            </div>
            <div class="form-group">
                <div class="table">
                    <table>
                        <tr>
                            @{
                                int cnt = 0;

                                foreach (var course in Model.AssignedCourseDataList)
                                {
                                    if (cnt++ % 3 == 0)
                                    {
                                        @:</tr><tr>
                                    }
                                    @:<td>
                                        <input type="checkbox"
                                               name="selectedCourses"
                                               value="@course.CourseID"
                                               @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                               @course.CourseID @:  @course.Title
                                    @:</td>
                                }
                                @:</tr>
                            }
                    </table>
                </div>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Test the instructor Create page.

## Update the Instructor Delete page

Update `Pages/Instructors/Delete.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Instructor Instructor { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Instructor = await _context.Instructors.FirstOrDefaultAsync(m => m.ID == id);

            if (Instructor == null)
            {
                return NotFound();
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Instructor instructor = await _context.Instructors
                .Include(i => i.CourseAssignments)
                .SingleAsync(i => i.ID == id);

            if (instructor == null)
            {
                return RedirectToPage("./Index");
            }

            var departments = await _context.Departments
                .Where(d => d.InstructorID == id)
                .ToListAsync();
            departments.ForEach(d => d.InstructorID = null);

            _context.Instructors.Remove(instructor);

            await _context.SaveChangesAsync();
            return RedirectToPage("./Index");
        }
    }
}
```

The preceding code makes the following changes:

* Uses eager loading for the `CourseAssignments` navigation property. `CourseAssignments` must be included or they aren't deleted when the instructor is deleted. To avoid needing to read them, configure cascade delete in the database.

* If the instructor to be deleted is assigned as administrator of any departments, removes the instructor assignment from those departments.

Run the app and test the Delete page.

## Next steps

[Previous tutorial](read-related-data?view=aspnetcore-10.0) [Next tutorial](concurrency?view=aspnetcore-10.0)

This tutorial demonstrates updating related data. If you run into problems you can't solve, [download or view the completed app.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) [Download instructions](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

The following illustrations shows some of the completed pages.

Examine and test the Create and Edit course pages. Create a new course. The department is selected by its primary key (an integer), not its name. Edit the new course. When you have finished testing, delete the new course.

## Create a base class to share common code

The Courses/Create and Courses/Edit pages each need a list of department names. Create the `Pages/Courses/DepartmentNamePageModel.cshtml.cs` base class for the Create and Edit pages:

```
using ContosoUniversity.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace ContosoUniversity.Pages.Courses
{
    public class DepartmentNamePageModel : PageModel
    {
        public SelectList DepartmentNameSL { get; set; }

        public void PopulateDepartmentsDropDownList(SchoolContext _context,
            object selectedDepartment = null)
        {
            var departmentsQuery = from d in _context.Departments
                                   orderby d.Name // Sort by name.
                                   select d;

            DepartmentNameSL = new SelectList(departmentsQuery.AsNoTracking(),
                        "DepartmentID", "Name", selectedDepartment);
        }
    }
}
```

The preceding code creates a [SelectList](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.selectlist) to contain the list of department names. The `selectedDepartment` parameter allows the calling code to specify the item that will be selected when the drop-down list is rendered. However, when using the Select Tag Helper with `asp-for`, the selected item is determined by the model property value (such as `Course.DepartmentID`), not by the `selectedDepartment` parameter.

The Create and Edit page model classes will derive from `DepartmentNamePageModel`.

## Customize the Courses Pages

When a new course entity is created, it must have a relationship to an existing department. To add a department while creating a course, the base class for Create and Edit contains a drop-down list for selecting the department. The drop-down list sets the `Course.DepartmentID` foreign key (FK) property. EF Core uses the `Course.DepartmentID` FK to load the `Department` navigation property.

Update the Create page model with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class CreateModel : DepartmentNamePageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public CreateModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            PopulateDepartmentsDropDownList(_context);
            return Page();
        }

        [BindProperty]
        public Course Course { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var emptyCourse = new Course();

            if (await TryUpdateModelAsync<Course>(
                 emptyCourse,
                 "course",   // Prefix for form value.
                 s => s.CourseID, s => s.DepartmentID, s => s.Title, s => s.Credits))
            {
                _context.Courses.Add(emptyCourse);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }

            // Repopulate departments dropdown. emptyCourse.DepartmentID determines the selected item.
            PopulateDepartmentsDropDownList(_context, emptyCourse.DepartmentID);
            return Page();
        }
      }
}
```

The preceding code:

* Derives from `DepartmentNamePageModel`.
* Uses `TryUpdateModelAsync` to prevent [overposting](crud?view=aspnetcore-10.0#overposting).
* Replaces `ViewData["DepartmentID"]` with `DepartmentNameSL` (from the base class).

`ViewData["DepartmentID"]` is replaced with the strongly typed `DepartmentNameSL`. Strongly typed models are preferred over weakly typed. For more information, see [Weakly typed data (ViewData and ViewBag)](../../mvc/views/overview?view=aspnetcore-10.0#VD_VB).

### Update the Courses Create page

Update `Pages/Courses/Create.cshtml` with the following code:

```
@page
@model ContosoUniversity.Pages.Courses.CreateModel
@{
    ViewData["Title"] = "Create Course";
}
<h2>Create</h2>
<h4>Course</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Course.CourseID" class="control-label"></label>
                <input asp-for="Course.CourseID" class="form-control" />
                <span asp-validation-for="Course.CourseID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Title" class="control-label"></label>
                <input asp-for="Course.Title" class="form-control" />
                <span asp-validation-for="Course.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Credits" class="control-label"></label>
                <input asp-for="Course.Credits" class="form-control" />
                <span asp-validation-for="Course.Credits" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Department" class="control-label"></label>
                <select asp-for="Course.DepartmentID" class="form-control"
                        asp-items="@Model.DepartmentNameSL">
                    <option value="">-- Select Department --</option>
                </select>
                <span asp-validation-for="Course.DepartmentID" class="text-danger" />
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding markup makes the following changes:

* Changes the caption from **DepartmentID** to **Department**.
* Replaces `"ViewBag.DepartmentID"` with `DepartmentNameSL` (from the base class).
* Adds the "Select Department" option. This change renders "Select Department" rather than the first department.
* Adds a validation message when the department isn't selected.

The Razor Page uses the [Select Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-select-tag-helper):

```
<div class="form-group">
    <label asp-for="Course.Department" class="control-label"></label>
    <select asp-for="Course.DepartmentID" class="form-control"
            asp-items="@Model.DepartmentNameSL">
        <option value="">-- Select Department --</option>
    </select>
    <span asp-validation-for="Course.DepartmentID" class="text-danger" />
</div>
```

Test the Create page. The Create page displays the department name rather than the department ID.

### Update the Courses Edit page.

Replace the code in `Pages/Courses/Edit.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Courses
{
    public class EditModel : DepartmentNamePageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Course Course { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Course = await _context.Courses
                .Include(c => c.Department).FirstOrDefaultAsync(m => m.CourseID == id);

            if (Course == null)
            {
                return NotFound();
            }

            // Populate departments dropdown. Course.DepartmentID determines the selected item.
            PopulateDepartmentsDropDownList(_context,Course.DepartmentID);
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var courseToUpdate = await _context.Courses.FindAsync(id);

            if (await TryUpdateModelAsync<Course>(
                 courseToUpdate,
                 "course",   // Prefix for form value.
                   c => c.Credits, c => c.DepartmentID, c => c.Title))
            {
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }

            // Repopulate departments dropdown. courseToUpdate.DepartmentID determines the selected item.
            PopulateDepartmentsDropDownList(_context, courseToUpdate.DepartmentID);
            return Page();
        }       
    }
}
```

The changes are similar to those made in the Create page model. In the preceding code, `PopulateDepartmentsDropDownList` passes in the department ID. When using the Select Tag Helper with `asp-for="Course.DepartmentID"`, the selected item in the drop-down list is determined by the value of `Course.DepartmentID`, not by the `selectedDepartment` parameter passed to the SelectList constructor.

Update `Pages/Courses/Edit.cshtml` with the following markup:

```
@page
@model ContosoUniversity.Pages.Courses.EditModel

@{
    ViewData["Title"] = "Edit";
}

<h2>Edit</h2>

<h4>Course</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Course.CourseID" />
            <div class="form-group">
                <label asp-for="Course.CourseID" class="control-label"></label>
                <div>@Html.DisplayFor(model => model.Course.CourseID)</div>
            </div>
            <div class="form-group">
                <label asp-for="Course.Title" class="control-label"></label>
                <input asp-for="Course.Title" class="form-control" />
                <span asp-validation-for="Course.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Credits" class="control-label"></label>
                <input asp-for="Course.Credits" class="form-control" />
                <span asp-validation-for="Course.Credits" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Course.Department" class="control-label"></label>
                <select asp-for="Course.DepartmentID" class="form-control" 
                        asp-items="@Model.DepartmentNameSL"></select>
                <span asp-validation-for="Course.DepartmentID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding markup makes the following changes:

* Displays the course ID. Generally the Primary Key (PK) of an entity isn't displayed. PKs are usually meaningless to users. In this case, the PK is the course number.
* Changes the caption from **DepartmentID** to **Department**.
* Replaces `"ViewBag.DepartmentID"` with `DepartmentNameSL` (from the base class).

The page contains a hidden field (`<input type="hidden">`) for the course number. Adding a `<label>` tag helper with `asp-for="Course.CourseID"` doesn't eliminate the need for the hidden field. `<input type="hidden">` is required for the course number to be included in the posted data when the user clicks **Save**.

Test the updated code. Create, edit, and delete a course.

## Add AsNoTracking to the Details and Delete page models

[AsNoTracking](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.asnotracking) can improve performance when tracking isn't required. Add `AsNoTracking` to the Delete and Details page model. The following code shows the updated Delete page model:

```
public class DeleteModel : PageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public DeleteModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Course Course { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Course = await _context.Courses
            .AsNoTracking()
            .Include(c => c.Department)
            .FirstOrDefaultAsync(m => m.CourseID == id);

        if (Course == null)
        {
            return NotFound();
        }
        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Course = await _context.Courses
            .AsNoTracking()
            .FirstOrDefaultAsync(m => m.CourseID == id);

        if (Course != null)
        {
            _context.Courses.Remove(Course);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage("./Index");
    }
}
```

Update the `OnGetAsync` method in the `Pages/Courses/Details.cshtml.cs` file:

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Course = await _context.Courses
         .AsNoTracking()
         .Include(c => c.Department)
         .FirstOrDefaultAsync(m => m.CourseID == id);

    if (Course == null)
    {
        return NotFound();
    }
    return Page();
}
```

### Modify the Delete and Details pages

Update the Delete Razor page with the following markup:

```
@page
@model ContosoUniversity.Pages.Courses.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Course</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Course.CourseID)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Course.CourseID)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Course.Title)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Course.Title)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Course.Credits)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Course.Credits)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Course.Department)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Course.Department.DepartmentID)
        </dd>
    </dl>
    
    <form method="post">
        <input type="hidden" asp-for="Course.CourseID" />
        <input type="submit" value="Delete" class="btn btn-default" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>
```

Make the same changes to the Details page.

### Test the Course pages

Test create, edit, details, and delete.

## Update the instructor pages

The following sections update the instructor pages.

### Add office location

When editing an instructor record, you may want to update the instructor's office assignment. The `Instructor` entity has a one-to-zero-or-one relationship with the `OfficeAssignment` entity. The instructor code must handle:

* If the user clears the office assignment, delete the `OfficeAssignment` entity.
* If the user enters an office assignment and it was empty, create a new `OfficeAssignment` entity.
* If the user changes the office assignment, update the `OfficeAssignment` entity.

Update the instructors Edit page model with the following code:

```
public class EditModel : PageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public EditModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Instructor Instructor { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Instructor = await _context.Instructors
            .Include(i => i.OfficeAssignment)
            .AsNoTracking()
            .FirstOrDefaultAsync(m => m.ID == id);

        if (Instructor == null)
        {
            return NotFound();
        }
        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int? id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var instructorToUpdate = await _context.Instructors
            .Include(i => i.OfficeAssignment)
            .FirstOrDefaultAsync(s => s.ID == id);

        if (await TryUpdateModelAsync<Instructor>(
            instructorToUpdate,
            "Instructor",
            i => i.FirstMidName, i => i.LastName, 
            i => i.HireDate, i => i.OfficeAssignment))
        {
            if (String.IsNullOrWhiteSpace(
                instructorToUpdate.OfficeAssignment?.Location))
            {
                instructorToUpdate.OfficeAssignment = null;
            }
            await _context.SaveChangesAsync();
        }
        return RedirectToPage("./Index");

    }
}
```

The preceding code:

* Gets the current `Instructor` entity from the database using eager loading for the `OfficeAssignment` navigation property.
* Updates the retrieved `Instructor` entity with values from the model binder. `TryUpdateModel` prevents [overposting](crud?view=aspnetcore-10.0#overposting).
* If the office location is blank, sets `Instructor.OfficeAssignment` to null. When `Instructor.OfficeAssignment` is null, the related row in the `OfficeAssignment` table is deleted.

### Update the instructor Edit page

Update `Pages/Instructors/Edit.cshtml` with the office location:

```
@page
@model ContosoUniversity.Pages.Instructors.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Instructor.ID" />
            <div class="form-group">
                <label asp-for="Instructor.LastName" class="control-label"></label>
                <input asp-for="Instructor.LastName" class="form-control" />
                <span asp-validation-for="Instructor.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.FirstMidName" class="control-label"></label>
                <input asp-for="Instructor.FirstMidName" class="form-control" />
                <span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.HireDate" class="control-label"></label>
                <input asp-for="Instructor.HireDate" class="form-control" />
                <span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
                <input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
                <span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Verify you can change an instructors office location.

## Add Course assignments to the instructor Edit page

Instructors may teach any number of courses. In this section, you add the ability to change course assignments. The following image shows the updated instructor Edit page:

`Course` and `Instructor` has a many-to-many relationship. To add and remove relationships, you add and remove entities from the `CourseAssignments` join entity set.

checkboxes enable changes to courses an instructor is assigned to. A checkbox is displayed for every course in the database. Courses that the instructor is assigned to are checked. The user can select or clear checkboxes to change course assignments. If the number of courses were much greater:

* You'd probably use a different user interface to display the courses.
* The method of manipulating a join entity to create or delete relationships wouldn't change.

### Add classes to support Create and Edit instructor pages

Create `Models/SchoolViewModels/AssignedCourseData.cs` with the following code:

```
namespace ContosoUniversity.Models.SchoolViewModels
{
    public class AssignedCourseData
    {
        public int CourseID { get; set; }
        public string Title { get; set; }
        public bool Assigned { get; set; }
    }
}
```

The `AssignedCourseData` class contains data to create the checkboxes for assigned courses by an instructor.

Create the `Pages/Instructors/InstructorCoursesPageModel.cshtml.cs` base class:

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Pages.Instructors
{
    public class InstructorCoursesPageModel : PageModel
    {

        public List<AssignedCourseData> AssignedCourseDataList;

        public void PopulateAssignedCourseData(SchoolContext context, 
                                               Instructor instructor)
        {
            var allCourses = context.Courses;
            var instructorCourses = new HashSet<int>(
                instructor.CourseAssignments.Select(c => c.CourseID));
            AssignedCourseDataList = new List<AssignedCourseData>();
            foreach (var course in allCourses)
            {
                AssignedCourseDataList.Add(new AssignedCourseData
                {
                    CourseID = course.CourseID,
                    Title = course.Title,
                    Assigned = instructorCourses.Contains(course.CourseID)
                });
            }
        }

        public void UpdateInstructorCourses(SchoolContext context, 
            string[] selectedCourses, Instructor instructorToUpdate)
        {
            if (selectedCourses == null)
            {
                instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
                return;
            }

            var selectedCoursesHS = new HashSet<string>(selectedCourses);
            var instructorCourses = new HashSet<int>
                (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
            foreach (var course in context.Courses)
            {
                if (selectedCoursesHS.Contains(course.CourseID.ToString()))
                {
                    if (!instructorCourses.Contains(course.CourseID))
                    {
                        instructorToUpdate.CourseAssignments.Add(
                            new CourseAssignment
                            {
                                InstructorID = instructorToUpdate.ID,
                                CourseID = course.CourseID
                            });
                    }
                }
                else
                {
                    if (instructorCourses.Contains(course.CourseID))
                    {
                        CourseAssignment courseToRemove
                            = instructorToUpdate
                                .CourseAssignments
                                .SingleOrDefault(i => i.CourseID == course.CourseID);
                        context.Remove(courseToRemove);
                    }
                }
            }
        }
    }
}
```

The `InstructorCoursesPageModel` is the base class you will use for the Edit and Create page models. `PopulateAssignedCourseData` reads all `Course` entities to populate `AssignedCourseDataList`. For each course, the code sets the `CourseID`, title, and whether or not the instructor is assigned to the course. A [HashSet](/en-us/dotnet/api/system.collections.generic.hashset-1) is used to create efficient lookups.

### Instructors Edit page model

Update the instructor Edit page model with the following code:

```
public class EditModel : InstructorCoursesPageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public EditModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Instructor Instructor { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Instructor = await _context.Instructors
            .Include(i => i.OfficeAssignment)
            .Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
            .AsNoTracking()
            .FirstOrDefaultAsync(m => m.ID == id);

        if (Instructor == null)
        {
            return NotFound();
        }
        PopulateAssignedCourseData(_context, Instructor);
        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int? id, string[] selectedCourses)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var instructorToUpdate = await _context.Instructors
            .Include(i => i.OfficeAssignment)
            .Include(i => i.CourseAssignments)
                .ThenInclude(i => i.Course)
            .FirstOrDefaultAsync(s => s.ID == id);

        if (await TryUpdateModelAsync<Instructor>(
            instructorToUpdate,
            "Instructor",
            i => i.FirstMidName, i => i.LastName,
            i => i.HireDate, i => i.OfficeAssignment))
        {
            if (String.IsNullOrWhiteSpace(
                instructorToUpdate.OfficeAssignment?.Location))
            {
                instructorToUpdate.OfficeAssignment = null;
            }
            UpdateInstructorCourses(_context, selectedCourses, instructorToUpdate);
            await _context.SaveChangesAsync();
            return RedirectToPage("./Index");
        }
        UpdateInstructorCourses(_context, selectedCourses, instructorToUpdate);
        PopulateAssignedCourseData(_context, instructorToUpdate);
        return Page();
    }
}
```

The preceding code handles office assignment changes.

Update the instructor Razor View:

```
@page
@model ContosoUniversity.Pages.Instructors.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Instructor.ID" />
            <div class="form-group">
                <label asp-for="Instructor.LastName" class="control-label"></label>
                <input asp-for="Instructor.LastName" class="form-control" />
                <span asp-validation-for="Instructor.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.FirstMidName" class="control-label"></label>
                <input asp-for="Instructor.FirstMidName" class="form-control" />
                <span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.HireDate" class="control-label"></label>
                <input asp-for="Instructor.HireDate" class="form-control" />
                <span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
                <input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
                <span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
            </div>
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <table>
                        <tr>
                            @{
                                int cnt = 0;

                                foreach (var course in Model.AssignedCourseDataList)
                                {
                                    if (cnt++ % 3 == 0)
                                    {
                                        @:</tr><tr>
                                    }
                                    @:<td>
                                        <input type="checkbox"
                                               name="selectedCourses"
                                               value="@course.CourseID"
                                               @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                               @course.CourseID @:  @course.Title
                                    @:</td>
                                }
                                @:</tr>
                            }
                    </table>
                </div>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

[]()

Note

When you paste the code in Visual Studio, line breaks are changed in a way that breaks the code. Press Ctrl+Z one time to undo the automatic formatting. Ctrl+Z fixes the line breaks so that they look like what you see here. The indentation doesn't have to be perfect, but the `@:</tr><tr>`, `@:<td>`, `@:</td>`, and `@:</tr>` lines must each be on a single line as shown. With the block of new code selected, press Tab three times to line up the new code with the existing code. Vote on or review the status of this bug [with this link](https://developercommunity.visualstudio.com/content/problem/147795/razor-editor-malforms-pasted-markup-and-creates-in.html).

The preceding code creates an HTML table that has three columns. Each column has a checkbox and a caption containing the course number and title. The checkboxes all have the same name ("selectedCourses"). Using the same name informs the model binder to treat them as a group. The value attribute of each checkbox is set to `CourseID`. When the page is posted, the model binder passes an array that consists of the `CourseID` values for only the checkboxes that are selected.

When the checkboxes are initially rendered, courses assigned to the instructor have checked attributes.

Run the app and test the updated instructors Edit page. Change some course assignments. The changes are reflected on the Index page.

Note: The approach taken here to edit instructor course data works well when there's a limited number of courses. For collections that are much larger, a different UI and a different updating method would be more useable and efficient.

### Update the instructors Create page

Update the instructor Create page model with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class CreateModel : InstructorCoursesPageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public CreateModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            var instructor = new Instructor();
            instructor.CourseAssignments = new List<CourseAssignment>();

            // Provides an empty collection for the foreach loop
            // foreach (var course in Model.AssignedCourseDataList)
            // in the Create Razor page.
            PopulateAssignedCourseData(_context, instructor);
            return Page();
        }

        [BindProperty]
        public Instructor Instructor { get; set; }

        public async Task<IActionResult> OnPostAsync(string[] selectedCourses)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var newInstructor = new Instructor();
            if (selectedCourses != null)
            {
                newInstructor.CourseAssignments = new List<CourseAssignment>();
                foreach (var course in selectedCourses)
                {
                    var courseToAdd = new CourseAssignment
                    {
                        CourseID = int.Parse(course)
                    };
                    newInstructor.CourseAssignments.Add(courseToAdd);
                }
            }

            if (await TryUpdateModelAsync<Instructor>(
                newInstructor,
                "Instructor",
                i => i.FirstMidName, i => i.LastName,
                i => i.HireDate, i => i.OfficeAssignment))
            {
                _context.Instructors.Add(newInstructor);                
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            PopulateAssignedCourseData(_context, newInstructor);
            return Page();
        }
    }
}
```

The preceding code is similar to the `Pages/Instructors/Edit.cshtml.cs` code.

Update the instructor Create Razor page with the following markup:

```
@page
@model ContosoUniversity.Pages.Instructors.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h2>Create</h2>

<h4>Instructor</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Instructor.LastName" class="control-label"></label>
                <input asp-for="Instructor.LastName" class="form-control" />
                <span asp-validation-for="Instructor.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.FirstMidName" class="control-label"></label>
                <input asp-for="Instructor.FirstMidName" class="form-control" />
                <span asp-validation-for="Instructor.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Instructor.HireDate" class="control-label"></label>
                <input asp-for="Instructor.HireDate" class="form-control" />
                <span asp-validation-for="Instructor.HireDate" class="text-danger"></span>
            </div>
            
            <div class="form-group">
                <label asp-for="Instructor.OfficeAssignment.Location" class="control-label"></label>
                <input asp-for="Instructor.OfficeAssignment.Location" class="form-control" />
                <span asp-validation-for="Instructor.OfficeAssignment.Location" class="text-danger" />
            </div>
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <table>
                        <tr>
                            @{
                                int cnt = 0;

                                foreach (var course in Model.AssignedCourseDataList)
                                {
                                    if (cnt++ % 3 == 0)
                                    {
                                        @:</tr><tr>
                                    }
                                    @:<td>
                                        <input type="checkbox"
                                               name="selectedCourses"
                                               value="@course.CourseID"
                                               @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                               @course.CourseID @:  @course.Title
                                    @:</td>
                                }
                                @:</tr>
                            }
                    </table>
                </div>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Test the instructor Create page.

## Update the Delete page

Update the Delete page model with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Instructors
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Instructor Instructor { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            Instructor = await _context.Instructors.SingleAsync(m => m.ID == id);

            if (Instructor == null)
            {
                return NotFound();
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            Instructor instructor = await _context.Instructors
                .Include(i => i.CourseAssignments)
                .SingleAsync(i => i.ID == id);

            var departments = await _context.Departments
                .Where(d => d.InstructorID == id)
                .ToListAsync();
            departments.ForEach(d => d.InstructorID = null);

            _context.Instructors.Remove(instructor);

            await _context.SaveChangesAsync();
            return RedirectToPage("./Index");
        }
    }
}
```

The preceding code makes the following changes:

* Uses eager loading for the `CourseAssignments` navigation property. `CourseAssignments` must be included or they aren't deleted when the instructor is deleted. To avoid needing to read them, configure cascade delete in the database.

* If the instructor to be deleted is assigned as administrator of any departments, removes the instructor assignment from those departments.

## Related content

* [YouTube version of this tutorial (Part 1)](https://www.youtube.com/watch?v=Csh6gkmwc9E)
* [YouTube version of this tutorial (Part 2)](https://www.youtube.com/watch?v=mOAankB_Zgc)

[Previous](read-related-data?view=aspnetcore-10.0) [Next](concurrency?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/transforms
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/transforms.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP Request and Response Transforms

## Introduction

When proxying a request it's common to modify parts of the request or response to adapt to the destination server's requirements or to flow additional data such as the client's original IP address. This process is implemented via Transforms. Types of transforms are defined globally for the application and then individual routes supply the parameters to enable and configure those transforms. The original request objects are not modified by these transforms, only the proxy requests.

Request and response body transforms are not provided by YARP but you can write middleware to do this.

## Defaults

The following transforms are enabled by default for all routes. They can be configured or disabled as shown later in this document.

* Host - Suppress the incoming request's Host header. The proxy request will default to the host name specified in the destination server address. See `RequestHeaderOriginalHost` below.
* X-Forwarded-For - Sets the client's IP address to the X-Forwarded-For header. See `X-Forwarded` below.
* X-Forwarded-Proto - Sets the request's original scheme (http/https) to the X-Forwarded-Proto header. See `X-Forwarded` below.
* X-Forwarded-Host - Sets the request's original Host to the X-Forwarded-Host header. See `X-Forwarded` below.
* X-Forwarded-Prefix - Sets the request's original PathBase, if any, to the X-Forwarded-Prefix header. See `X-Forwarded` below.

For example the following incoming request to `http://IncomingHost:5000/path`:

```
GET /path HTTP/1.1
Host: IncomingHost:5000
Accept: */*
header1: foo
```

would be transformed and proxied to the destination server `https://DestinationHost:6000/` as follows using these defaults:

```
GET /path HTTP/1.1
Host: DestinationHost:6000
Accept: */*
header1: foo
X-Forwarded-For: 5.5.5.5
X-Forwarded-Proto: http
X-Forwarded-Host: IncomingHost:5000
```

## Transform Categories

Transforms fall into a few categories: [Request](transforms-request?view=aspnetcore-10.0), [Response](transforms-response?view=aspnetcore-10.0), and [Response Trailers](transforms-response?view=aspnetcore-10.0#responsetrailer). Request trailers are not supported because they are not supported by the underlying HttpClient.

If the built-in set of transforms is insufficient, then custom transforms can be added via [extensibility](extensibility-transforms?view=aspnetcore-10.0).

## Add transforms

Transforms can be added to routes either through configuration or programmatically.

### From Configuration

Transforms can be configured on [RouteConfig.Transforms](/en-us/dotnet/api/yarp.reverseproxy.configuration.routeconfig) and can be bound from the `Routes` sections of the config file. These can be modified and reloaded without restarting the proxy. A transform is configured using one or more key-value string pairs.

Here is an example of common transforms:

```
{
  "ReverseProxy": {
    "Routes": {
      "route1" : {
        "ClusterId": "cluster1",
        "Match": {
          "Hosts": [ "localhost" ]
        },
        "Transforms": [
          { "PathPrefix": "/apis" },
          {
            "RequestHeader": "header1",
            "Append": "bar"
          },
          {
            "ResponseHeader": "header2",
            "Append": "bar",
            "When": "Always"
          },
          { "ClientCert": "X-Client-Cert" },
          { "RequestHeadersCopy": "true" },
          { "RequestHeaderOriginalHost": "true" },
          {
            "X-Forwarded": "Append",
            "HeaderPrefix": "X-Forwarded-"
          }
        ]
      },
      "route2" : {
        "ClusterId": "cluster1",
        "Match": {
          "Path": "/api/{plugin}/stuff/{**remainder}"
        },
        "Transforms": [
          { "PathPattern": "/foo/{plugin}/bar/{**remainder}" },
          {
            "QueryValueParameter": "q",
            "Append": "plugin"
          }
        ]
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "cluster1/destination1": {
            "Address": "https://localhost:10001/Path/Base"
          }
        }
      }
    }
  }
}
```

All configuration entries are treated as case-insensitive, though the destination server may treat the resulting values as case sensitive or insensitive such as the path.

The details for these transforms are covered later in this document.

Developers that want to integrate their custom transforms with the `Transforms` section of configuration can do so using `ITransformFactory` described below.

### From Code

Transforms can be added to routes programmatically by calling the [AddTransforms](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.reverseproxyservicecollectionextensions.addtransforms) method.

`AddTransforms` can be called after `AddReverseProxy` to provide a callback for configuring transforms. This callback is invoked each time a route is built or rebuilt and allows the developer to inspect the [RouteConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.routeconfig) information and conditionally add transforms for it.

The `AddTransforms` callback provides a [TransformBuilderContext](/en-us/dotnet/api/yarp.reverseproxy.transforms.builder.transformbuildercontext) where transforms can be added or configured. Most transforms provide `TransformBuilderContext` extension methods to make them easier to add. These are extensions documented below with the individual transform descriptions.

The `TransformBuilderContext` also includes an `IServiceProvider` for access to any needed services.

```
services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddTransforms(builderContext =>
    {
        // Added to all routes.
        builderContext.AddPathPrefix("/prefix");

        // Conditionally add a transform for routes that require auth.
        if (!string.IsNullOrEmpty(builderContext.Route.AuthorizationPolicy))
        {
            builderContext.AddRequestTransform(async transformContext =>
            {
                transformContext.ProxyRequest.Headers.Add("CustomHeader", "CustomValue");
            });
        }
    });
```

For more advanced control see `ITransformProvider` described below.

### Direct forwarding

Built-in transforms can also be used with `IHttpForwarder` without configuring routes or clusters. Register the direct forwarding services with `AddHttpForwarder`, resolve `ITransformBuilder` from dependency injection, and call `Create` to configure the transforms. Pass the returned `HttpTransformer` to `IHttpForwarder.SendAsync` or `MapForwarder`.

The `Create` callback accepts a `TransformBuilderContext` with the same transform extension methods as `AddTransforms`. It isn't associated with a route or cluster, so `Route` and `Cluster` aren't populated. For a working example, see [Transforms with direct forwarding](direct-forwarding?view=aspnetcore-10.0#transforms).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/javascript-client
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/javascript-client.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core SignalR JavaScript client

The ASP.NET Core SignalR JavaScript client library enables developers to call server-side SignalR hub code. This article describes how to use the APIs to connect to a SignalR hub, and call the JavaScript hub and client methods.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/javascript-client/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Install the SignalR client package

The SignalR JavaScript client library is delivered as an [npm](https://www.npmjs.com/) package. There are several ways to install the client library:

* Run `npm` commands in Visual Studio **Package Manager Console**.
* Run `npm` commands in the integrated terminal in Visual Studio Code.
* Reference a copy of the client library hosted on a Content Delivery Network (CDN).
* Use [LibMan](../client-side/libman/?view=aspnetcore-10.0) and install specific client library files from a CDN-hosted client library.

### Install with npm

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Run the following `npm` commands in the **Package Manager Console** window:

```
npm init -y
npm install @microsoft/signalr
```

Open an integrated terminal and run the following `npm` commands:

```
npm init -y
npm install @microsoft/signalr
```

npm installs the package contents in the *node\_modules\\@microsoft\signalr\dist\browser* folder.

Complete the setup:

1. Create the *wwwroot/lib/signalr* folder.

2. Copy the *signalr.js* file to the *wwwroot/lib/signalr* folder.

Now you can reference the installed SignalR JavaScript client in the `<script>` element. For example:

```
<script src="~/lib/signalr/signalr.js"></script>
```

### Use a CDN

To use the client library without the npm prerequisite, reference a CDN-hosted copy of the client library. For example:

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>
```

The code example specifies version 6.0.1. To get the latest client library version, choose one of the following CDNs:

* [cdnjs](https://cdnjs.com/libraries/microsoft-signalr)
* [jsDelivr](https://www.jsdelivr.com/package/npm/@microsoft/signalr)
* [unpkg](https://unpkg.com/@microsoft/signalr@10.0.0/dist/browser/signalr.js)

### Install with LibMan

Another approach is to use [LibMan](../client-side/libman/?view=aspnetcore-10.0) and install only specific client library files from the CDN-hosted client library. For example, you can add only the minified JavaScript file to the project.

For details on this approach, see [Add the SignalR client library](../tutorials/signalr?view=aspnetcore-10.0#add-the-signalr-client-library).

## Connect to a hub

The following code creates and starts a connection. The hub name is case insensitive:

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();

async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});

// Start the connection.
start();
```

### Cross-origin connections (CORS)

Typically, browsers load connections from the same domain as the requested page. However, there are occasions when a connection to another domain is required.

For [cross domain requests](security?view=aspnetcore-10.0#cross-origin-resource-sharing), the client code ***must*** use an absolute URL rather than a relative URL. When you use cross domain requests, change `.withUrl("/chathub")` to `.withUrl("https://{App domain name}/chathub")`.

To prevent a malicious site from reading sensitive data from another site, [cross-origin connections](../security/cors?view=aspnetcore-10.0) are disabled by default. To allow a cross-origin request, enable [CORS](../security/cors?view=aspnetcore-10.0):

```
using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        builder =>
        {
            builder.WithOrigins("https://example.com")
                .AllowAnyHeader()
                .WithMethods("GET", "POST")
                .AllowCredentials();
        });
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

// UseCors must be called before MapHub.
app.UseCors();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();
```

The [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) method must be called before calling the [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.hubendpointroutebuilderextensions.maphub) method.

## Call hub methods from the client

JavaScript clients call public methods on hubs via the [invoke](/en-us/javascript/api/%40microsoft/signalr/hubconnection#@microsoft-signalr-hubconnection-invoke) method of the [HubConnection](/en-us/javascript/api/%40microsoft/signalr/hubconnection). The `invoke` method accepts:

* The name of the hub method.
* Any arguments defined in the hub method.

In the following highlighted code, the method name on the hub is `SendMessage`. The second and third arguments passed to `invoke` map to the hub method's `user` and `message` arguments:

```
try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}
```

Calling hub methods from a client is supported only when using the Azure SignalR Service in [*Default* mode](/en-us/azure/azure-signalr/concept-service-mode#default-mode).

The `invoke` method returns a JavaScript [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) object. The `Promise` object is resolved with the return value (if any) when the method on the server returns. If the method on the server throws an error, the `Promise` object is rejected with the error message. To handle these cases, use `async` and `await` or the `Promise` object's `then` and `catch` methods.

JavaScript clients can also call public methods on hubs via the [send](/en-us/javascript/api/%40microsoft/signalr/hubconnection#@microsoft-signalr-hubconnection-send) method of the `HubConnection`. Unlike the `invoke` method, the `send` method doesn't wait for a response from the server. The `send` method returns a JavaScript `Promise` object. The `Promise` object is resolved when the message is sent to the server. If an error occurs when sending the message, the `Promise` object is rejected with the error message. To handle these cases, use `async` and `await` or the `Promise` object's `then` and `catch` methods.

Using `send` ***doesn't*** wait until the server receives the message, so it isn't possible to return data or errors from the server.

## Call client methods from the hub

To receive messages from the hub, define a method by using the [on method](/en-us/javascript/api/%40microsoft/signalr/hubconnection#@microsoft-signalr-hubconnection-on) of the `HubConnection`. The `on` method accepts:

* The name of the JavaScript client method.
* Arguments the hub passes to the method.

In the following example, the method name is `ReceiveMessage`. The argument names are `user` and `message`:

```
connection.on("ReceiveMessage", (user, message) => {
    const li = document.createElement("li");
    li.textContent = `${user}: ${message}`;
    document.getElementById("messageList").appendChild(li);
});
```

The preceding code in `connection.on` runs when server-side code calls it by using the [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.clientproxyextensions.sendasync) method:

```
using Microsoft.AspNetCore.SignalR;
namespace SignalRChat.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

SignalR determines which client method to call by matching the method name and arguments defined in `SendAsync` and `connection.on`.

A **best practice** is to call the [start](/en-us/javascript/api/@microsoft/signalr/hubconnection#@microsoft-signalr-hubconnection-start) method on the `HubConnection` after `on`. This approach ensures the handlers are registered before any messages are received.

## Error handling and logging

Use `console.error` to output errors to the browser's console when the client can't connect or send a message:

```
try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}
```

Set up client-side log tracing by passing a logger and type of event to log when the connection is made. Messages are logged with the specified log level and higher. Available log levels are as follows:

| Log level                      | Logged data                             | Description                                                         |
| ------------------------------ | --------------------------------------- | ------------------------------------------------------------------- |
| `signalR.LogLevel.Error`       | Error messages                          | Logs `Error` messages only.                                         |
| `signalR.LogLevel.Warning`     | Warning messages about potential errors | Logs `Warning` and `Error` messages.                                |
| `signalR.LogLevel.Information` | Status messages without errors          | Logs `Information`, `Warning`, and `Error` messages.                |
| `signalR.LogLevel.Trace`       | Trace messages                          | Logs everything, including data transported between hub and client. |

Use the [configureLogging](/en-us/javascript/api/@microsoft/signalr/hubconnectionbuilder#@microsoft-signalr-hubconnectionbuilder-configurelogging-1) method on [HubConnectionBuilder](/en-us/javascript/api/@microsoft/signalr/hubconnectionbuilder) to configure the log level. Messages are logged to the browser console:

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

## Reconnect clients

To reconnect with clients, you can set up automatic reconnection or configure the connection manually.

### Automatically reconnect

The JavaScript client for SignalR can be configured to automatically reconnect by using the [WithAutomaticReconnect](/en-us/javascript/api/@microsoft/signalr/hubconnectionbuilder#@microsoft-signalr-hubconnectionbuilder-withautomaticreconnect) method on [HubConnectionBuilder](/en-us/javascript/api/@microsoft/signalr/hubconnectionbuilder). It doesn't automatically reconnect by default.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect()
    .build();
```

Without any parameters, [WithAutomaticReconnect](/en-us/javascript/api/@microsoft/signalr/hubconnectionbuilder#@microsoft-signalr-hubconnectionbuilder-withautomaticreconnect) configures the client to wait 0, 2, 10, and 30 seconds respectively before trying each reconnect attempt. After four failed attempts, it stops trying to reconnect.

Before starting any reconnection, the `HubConnection`:

* Transitions to the [HubConnectionState.Reconnecting](/en-us/javascript/api/@microsoft/signalr/hubconnectionstate) state and fires its `onreconnecting` callbacks.
* Doesn't transition to the `Disconnected` state or trigger its `onclose` callbacks like a `HubConnection` without automatic reconnect configured.

The reconnection approach provides an opportunity to warn users that the connection is lost and to disable UI elements.

```
connection.onreconnecting(error => {
    console.assert(connection.state === signalR.HubConnectionState.Reconnecting);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection lost due to error "${error}". Reconnecting.`;
    document.getElementById("messageList").appendChild(li);
});
```

If the client successfully reconnects within its first four attempts, the `HubConnection` transitions back to the `Connected` state and fires its `onreconnected` callbacks. This approach provides an opportunity to inform users the connection is now reestablished.

Because the connection looks entirely new to the server, a new `connectionId` is provided to the `onreconnected` callback.

The `onreconnected` callback's `connectionId` parameter is ***undefined*** if the `HubConnection` is configured to [skip negotiation](configuration?view=aspnetcore-10.0#configure-client-options).

```
connection.onreconnected(connectionId => {
    console.assert(connection.state === signalR.HubConnectionState.Connected);

    document.getElementById("messageInput").disabled = false;

    const li = document.createElement("li");
    li.textContent = `Connection reestablished. Connected with connectionId "${connectionId}".`;
    document.getElementById("messageList").appendChild(li);
});
```

`withAutomaticReconnect` doesn't configure the `HubConnection` to retry initial start failures, so start failures need to be handled manually:

```
async function start() {
    try {
        await connection.start();
        console.assert(connection.state === signalR.HubConnectionState.Connected);
        console.log("SignalR Connected.");
    } catch (err) {
        console.assert(connection.state === signalR.HubConnectionState.Disconnected);
        console.log(err);
        setTimeout(() => start(), 5000);
    }
};
```

If the client doesn't successfully reconnect within its first four attempts, the `HubConnection` transitions to the `Disconnected` state and fires its [onclose](/en-us/javascript/api/@microsoft/signalr/hubconnection#@microsoft-signalr-hubconnection-onclose) callbacks. This approach provides an opportunity to inform users that the connection is permanently lost and to try refreshing the page:

```
connection.onclose(error => {
    console.assert(connection.state === signalR.HubConnectionState.Disconnected);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection closed due to error "${error}". Try refreshing this page to restart the connection.`;
    document.getElementById("messageList").appendChild(li);
});
```

To configure a custom number of reconnect attempts before disconnecting or change the reconnection timing, `withAutomaticReconnect` accepts an array of numbers representing the delay in milliseconds to wait before starting each reconnect attempt.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect([0, 0, 10000])
    .build();

    // .withAutomaticReconnect([0, 2000, 10000, 30000]) yields the default behavior
```

The preceding example configures the `HubConnection` to start attempting reconnects immediately after the connection is lost. The default configuration also waits zero (0) seconds to attempt reconnecting.

* If the first reconnect attempt fails, the second reconnect attempt also starts immediately instead of waiting 2 seconds as defined in the default configuration.

* If the second reconnect attempt fails, the third reconnect attempt starts in 10 seconds, which is the same behavior defined in the default configuration.

* The configured reconnection timing differs from the default behavior by stopping after the third reconnect attempt failure. In the default configuration, one more reconnect attempt is made after another 30 seconds.

For more control over the timing and number of automatic reconnect attempts, `withAutomaticReconnect` accepts an object implementing the `IRetryPolicy` interface, which has a single method named `nextRetryDelayInMilliseconds`. `nextRetryDelayInMilliseconds` takes a single argument with the type `RetryContext`. The `RetryContext` has three properties: `previousRetryCount` (type `number`), `elapsedMilliseconds` (type `number`), and `retryReason` (type `Error`).

* Before the first reconnect attempt, `previousRetryCount` and `elapsedMilliseconds` are both zero (0), and `retryReason` is the Error that caused the lost connection.

* After each failed retry attempt, `previousRetryCount` increments by one, `elapsedMilliseconds` updates to reflect the amount of time spent reconnecting so far in milliseconds, and `retryReason` is the Error that caused the last reconnect attempt to fail.

`nextRetryDelayInMilliseconds` must return either a number representing the number of milliseconds to wait before the next reconnect attempt or `null` if the `HubConnection` should stop reconnecting.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect({
        nextRetryDelayInMilliseconds: retryContext => {
            if (retryContext.elapsedMilliseconds < 60000) {
                // If we've been reconnecting for less than 60 seconds so far,
                // wait between 0 and 10 seconds before the next reconnect attempt.
                return Math.random() * 10000;
            } else {
                // If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
                return null;
            }
        }
    })
    .build();
```

Alternatively, you can write code to reconnect the client manually, as demonstrated in the next section.

### Manually reconnect

The following code demonstrates a typical manual reconnection approach:

1. To start the connection, create a function. In this case, the `start` function.

2. Call the `start` function in the connection's `onclose` event handler.

```
async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});
```

Production implementations typically use an exponential back-off or retry a specified number of times.

## Browser sleeping tab

Some browsers have a *tab freezing* or *tab sleeping* feature that reduces computer resource usage for inactive tabs. However, this functionality can cause SignalR connections to close and cause an undesirable user experience.

Browsers implement heuristics to determine if a tab should be put to sleep, such as:

* Playing audio
* Holding a web lock
* Holding an `IndexedDB` lock
* Being connected to a USB device
* Capturing video or audio
* Being mirrored
* Capturing a window or display

Heuristics can change over time and also differ across browsers. Check the support matrix and figure out what method works best for your scenarios.

To avoid putting an app to sleep, the app should trigger one of the heuristics that the browser uses.

The following code example shows how to use a [Web Lock](https://developer.mozilla.org/docs/Web/API/Web_Locks_API) to keep a tab awake and avoid an unexpected connection closure:

```
var lockResolver;
if (navigator && navigator.locks && navigator.locks.request) {
    const promise = new Promise((res) => {
        lockResolver = res;
    });

    navigator.locks.request('unique_lock_name', { mode: "shared" }, () => {
        return promise;
    });
}
```

In the preceding code:

* Web Locks are experimental. The conditional check confirms the browser supports Web Locks.
* The JavaScript promise resolver (`lockResolver`) is stored so the lock can be released when it's acceptable for the tab to sleep.
* When the connection is closing, the lock is released by calling `lockResolver()`. When the lock is released, the tab is allowed to sleep.

## Refresh authentication without reconnecting

In .NET 11 and later, a client can replace the credentials on an open connection instead of letting the connection close when its access token expires. The server must enable authentication refresh for the hub. Call `withAuthenticationRefresh` to schedule an automatic refresh ahead of the expiration that the server reports:

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { accessTokenFactory: () => getAccessToken() })
    .withAuthenticationRefresh()
    .build();
```

Each refresh calls `accessTokenFactory` again to obtain a new token. To refresh immediately, for example after the app obtains updated claims, call `connection.refreshAuthentication()`.

For the refresh options, the success and failure handlers, and the server configuration that enables the feature, see [SignalR authentication and authorization](authn-and-authz?view=aspnetcore-10.0#authentication-refresh).

## Related content

* [JavaScript tutorial](../tutorials/signalr?view=aspnetcore-10.0)
* [WebPack and TypeScript tutorial](../tutorials/signalr-typescript-webpack?view=aspnetcore-10.0)
* [Hubs](hubs?view=aspnetcore-10.0)
* [Azure SignalR Service serverless documentation](/en-us/azure/azure-signalr/signalr-concept-serverless-development-config)
* [Troubleshoot connection errors](troubleshoot?view=aspnetcore-10.0)

By [Rachel Appel](https://twitter.com/rachelappel)

The ASP.NET Core SignalR JavaScript client library enables developers to call server-side hub code.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/javascript-client/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Install the SignalR client package

The SignalR JavaScript client library is delivered as an [npm](https://www.npmjs.com/) package. The following sections outline different ways to install the client library.

### Install with npm

For Visual Studio, run the following commands from **Package Manager Console** while in the root folder. For Visual Studio Code, run the following commands from the **Integrated Terminal**.

```
npm init -y
npm install @microsoft/signalr
```

npm installs the package contents in the *node\_modules\\@microsoft\signalr\dist\browser* folder. Create a new folder named *signalr* under the *wwwroot\lib* folder. Copy the `signalr.js` file to the *wwwroot\lib\signalr* folder.

Reference the SignalR JavaScript client in the `<script>` element. For example:

```
<script src="~/lib/signalr/signalr.js"></script>
```

### Use a Content Delivery Network (CDN)

To use the client library without the npm prerequisite, reference a CDN-hosted copy of the client library. For example:

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7/signalr.js"></script>
```

The client library is available on the following CDNs:

* [cdnjs](https://cdnjs.com/libraries/microsoft-signalr)
* [jsDelivr](https://www.jsdelivr.com/package/npm/@microsoft/signalr)
* [unpkg](https://unpkg.com/@microsoft/signalr/dist/browser/signalr.js)

### Install with LibMan

[LibMan](../client-side/libman/?view=aspnetcore-10.0) can be used to install specific client library files from the CDN-hosted client library. For example, only add the minified JavaScript file to the project. For details on that approach, see [Add the SignalR client library](../tutorials/signalr?view=aspnetcore-10.0#add-the-signalr-client-library).

## Connect to a hub

The following code creates and starts a connection. The hub's name is case insensitive:

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();

async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});

// Start the connection.
start();
```

### Cross-origin connections

Typically, browsers load connections from the same domain as the requested page. However, there are occasions when a connection to another domain is required.

Important

The client code must use an absolute URL instead of a relative URL. Change `.withUrl("/chathub")` to `.withUrl("https://myappurl/chathub")`.

To prevent a malicious site from reading sensitive data from another site, [cross-origin connections](../security/cors?view=aspnetcore-10.0) are disabled by default. To allow a cross-origin request, enable it in the `Startup` class:

```
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs;

namespace SignalRChat
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddSignalR();

            services.AddCors(options =>
            {
                options.AddDefaultPolicy(builder =>
                {
                    builder.WithOrigins("https://example.com")
                        .AllowCredentials();
                });
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseCors();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapHub<ChatHub>("/chathub");
            });
        }
    }
}
```

## Call hub methods from the client

JavaScript clients call public methods on hubs via the [invoke](/en-us/javascript/api/%40microsoft/signalr/hubconnection#@microsoft-signalr-hubconnection-invoke) method of the [HubConnection](/en-us/javascript/api/%40microsoft/signalr/hubconnection). The `invoke` method accepts:

* The name of the hub method.
* Any arguments defined in the hub method.

In the following example, the method name on the hub is `SendMessage`. The second and third arguments passed to `invoke` map to the hub method's `user` and `message` arguments:

```
try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}
```

Note

Calling hub methods from a client is only supported when using the Azure SignalR Service in [*Default* mode](/en-us/azure/azure-signalr/concept-service-mode#default-mode).

The `invoke` method returns a JavaScript [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise). The `Promise` is resolved with the return value (if any) when the method on the server returns. If the method on the server throws an error, the `Promise` is rejected with the error message. Use `async` and `await` or the `Promise`'s `then` and `catch` methods to handle these cases.

JavaScript clients can also call public methods on hubs via the [send](/en-us/javascript/api/%40microsoft/signalr/hubconnection#send-string--any---) method of the `HubConnection`. Unlike the `invoke` method, the `send` method doesn't wait for a response from the server. The `send` method returns a JavaScript `Promise`. The `Promise` is resolved when the message has been sent to the server. If there is an error sending the message, the `Promise` is rejected with the error message. Use `async` and `await` or the `Promise`'s `then` and `catch` methods to handle these cases.

Note

Using `send` doesn't wait until the server has received the message. Consequently, it's not possible to return data or errors from the server.

## Call client methods from the hub

To receive messages from the hub, define a method using the [on](/en-us/javascript/api/%40microsoft/signalr/hubconnection#@microsoft-signalr-hubconnection-on) method of the `HubConnection`.

* The name of the JavaScript client method.
* Arguments the hub passes to the method.

In the following example, the method name is `ReceiveMessage`. The argument names are `user` and `message`:

```
connection.on("ReceiveMessage", (user, message) => {
    const li = document.createElement("li");
    li.textContent = `${user}: ${message}`;
    document.getElementById("messageList").appendChild(li);
});
```

The preceding code in `connection.on` runs when server-side code calls it using the [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.clientproxyextensions.sendasync) method:

```
public async Task SendMessage(string user, string message)
{
    await Clients.All.SendAsync("ReceiveMessage", user, message);
}
```

SignalR determines which client method to call by matching the method name and arguments defined in `SendAsync` and `connection.on`.

Note

As a best practice, call the [start](/en-us/javascript/api/%40aspnet/signalr/hubconnection#start) method on the `HubConnection` after `on`. Doing so ensures your handlers are registered before any messages are received.

## Error handling and logging

Use `try` and `catch` with `async` and `await` or the `Promise`'s `catch` method to handle client-side errors. Use `console.error` to output errors to the browser's console:

```
try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}
```

Set up client-side log tracing by passing a logger and type of event to log when the connection is made. Messages are logged with the specified log level and higher. Available log levels are as follows:

* `signalR.LogLevel.Error`: Error messages. Logs `Error` messages only.
* `signalR.LogLevel.Warning`: Warning messages about potential errors. Logs `Warning`, and `Error` messages.
* `signalR.LogLevel.Information`: Status messages without errors. Logs `Information`, `Warning`, and `Error` messages.
* `signalR.LogLevel.Trace`: Trace messages. Logs everything, including data transported between hub and client.

Use the [configureLogging](/en-us/javascript/api/%40aspnet/signalr/hubconnectionbuilder#configurelogging) method on [HubConnectionBuilder](/en-us/javascript/api/%40aspnet/signalr/hubconnectionbuilder) to configure the log level. Messages are logged to the browser console:

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

## Reconnect clients

### Automatically reconnect

The JavaScript client for SignalR can be configured to automatically reconnect using the `withAutomaticReconnect` method on [HubConnectionBuilder](/en-us/javascript/api/%40aspnet/signalr/hubconnectionbuilder). It won't automatically reconnect by default.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect()
    .build();
```

Without any parameters, `withAutomaticReconnect()` configures the client to wait 0, 2, 10, and 30 seconds respectively before trying each reconnect attempt, stopping after four failed attempts.

Before starting any reconnect attempts, the `HubConnection` will transition to the `HubConnectionState.Reconnecting` state and fire its `onreconnecting` callbacks instead of transitioning to the `Disconnected` state and triggering its `onclose` callbacks like a `HubConnection` without automatic reconnect configured. This provides an opportunity to warn users that the connection has been lost and to disable UI elements.

```
connection.onreconnecting(error => {
    console.assert(connection.state === signalR.HubConnectionState.Reconnecting);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection lost due to error "${error}". Reconnecting.`;
    document.getElementById("messageList").appendChild(li);
});
```

If the client successfully reconnects within its first four attempts, the `HubConnection` will transition back to the `Connected` state and fire its `onreconnected` callbacks. This provides an opportunity to inform users the connection has been reestablished.

Since the connection looks entirely new to the server, a new `connectionId` will be provided to the `onreconnected` callback.

Warning

The `onreconnected` callback's `connectionId` parameter will be undefined if the `HubConnection` was configured to [skip negotiation](configuration?view=aspnetcore-10.0#configure-client-options).

```
connection.onreconnected(connectionId => {
    console.assert(connection.state === signalR.HubConnectionState.Connected);

    document.getElementById("messageInput").disabled = false;

    const li = document.createElement("li");
    li.textContent = `Connection reestablished. Connected with connectionId "${connectionId}".`;
    document.getElementById("messageList").appendChild(li);
});
```

`withAutomaticReconnect()` won't configure the `HubConnection` to retry initial start failures, so start failures need to be handled manually:

```
async function start() {
    try {
        await connection.start();
        console.assert(connection.state === signalR.HubConnectionState.Connected);
        console.log("SignalR Connected.");
    } catch (err) {
        console.assert(connection.state === signalR.HubConnectionState.Disconnected);
        console.log(err);
        setTimeout(() => start(), 5000);
    }
};
```

If the client doesn't successfully reconnect within its first four attempts, the `HubConnection` will transition to the `Disconnected` state and fire its [onclose](/en-us/javascript/api/%40aspnet/signalr/hubconnection#onclose) callbacks. This provides an opportunity to inform users the connection has been permanently lost and recommend refreshing the page:

```
connection.onclose(error => {
    console.assert(connection.state === signalR.HubConnectionState.Disconnected);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection closed due to error "${error}". Try refreshing this page to restart the connection.`;
    document.getElementById("messageList").appendChild(li);
});
```

In order to configure a custom number of reconnect attempts before disconnecting or change the reconnect timing, `withAutomaticReconnect` accepts an array of numbers representing the delay in milliseconds to wait before starting each reconnect attempt.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect([0, 0, 10000])
    .build();

    // .withAutomaticReconnect([0, 2000, 10000, 30000]) yields the default behavior
```

The preceding example configures the `HubConnection` to start attempting reconnects immediately after the connection is lost. This is also true for the default configuration.

If the first reconnect attempt fails, the second reconnect attempt will also start immediately instead of waiting 2 seconds like it would in the default configuration.

If the second reconnect attempt fails, the third reconnect attempt will start in 10 seconds which is again like the default configuration.

The custom behavior then diverges again from the default behavior by stopping after the third reconnect attempt failure instead of trying one more reconnect attempt in another 30 seconds like it would in the default configuration.

If you want even more control over the timing and number of automatic reconnect attempts, `withAutomaticReconnect` accepts an object implementing the `IRetryPolicy` interface, which has a single method named `nextRetryDelayInMilliseconds`.

`nextRetryDelayInMilliseconds` takes a single argument with the type `RetryContext`. The `RetryContext` has three properties: `previousRetryCount`, `elapsedMilliseconds` and `retryReason` which are a `number`, a `number` and an `Error` respectively. Before the first reconnect attempt, both `previousRetryCount` and `elapsedMilliseconds` will be zero, and the `retryReason` will be the Error that caused the connection to be lost. After each failed retry attempt, `previousRetryCount` will be incremented by one, `elapsedMilliseconds` will be updated to reflect the amount of time spent reconnecting so far in milliseconds, and the `retryReason` will be the Error that caused the last reconnect attempt to fail.

`nextRetryDelayInMilliseconds` must return either a number representing the number of milliseconds to wait before the next reconnect attempt or `null` if the `HubConnection` should stop reconnecting.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect({
        nextRetryDelayInMilliseconds: retryContext => {
            if (retryContext.elapsedMilliseconds < 60000) {
                // If we've been reconnecting for less than 60 seconds so far,
                // wait between 0 and 10 seconds before the next reconnect attempt.
                return Math.random() * 10000;
            } else {
                // If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
                return null;
            }
        }
    })
    .build();
```

Alternatively, you can write code that will reconnect your client manually as demonstrated in [Manually reconnect](#manually-reconnect).

### Manually reconnect

The following code demonstrates a typical manual reconnection approach:

1. A function (in this case, the `start` function) is created to start the connection.
2. Call the `start` function in the connection's `onclose` event handler.

```
async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});
```

Production implementations typically use an exponential back-off or retry a specified number of times.

## Browser sleeping tab

Some browsers have a tab freezing or sleeping feature to reduce computer resource usage for inactive tabs. This can cause SignalR connections to close and may result in an unwanted user experience. Browsers use heuristics to figure out if a tab should be put to sleep, such as:

* Playing audio
* Holding a web lock
* Holding an `IndexedDB` lock
* Being connected to a USB device
* Capturing video or audio
* Being mirrored
* Capturing a window or display

Note

These heuristics may change over time or differ between browsers. Check your support matrix and figure out what method works best for your scenarios.

To avoid putting an app to sleep, the app should trigger one of the heuristics that the browser uses.

The following code example shows how to use a [Web Lock](https://developer.mozilla.org/docs/Web/API/Web_Locks_API) to keep a tab awake and avoid an unexpected connection closure.

```
var lockResolver;
if (navigator && navigator.locks && navigator.locks.request) {
    const promise = new Promise((res) => {
        lockResolver = res;
    });

    navigator.locks.request('unique_lock_name', { mode: "shared" }, () => {
        return promise;
    });
}
```

For the preceding code example:

* Web Locks are experimental. The conditional check confirms that the browser supports Web Locks.
* The promise resolver (`lockResolver`) is stored so that the lock can be released when it's acceptable for the tab to sleep.
* When closing the connection, the lock is released by calling `lockResolver()`. When the lock is released, the tab is allowed to sleep.

## Related content

* [JavaScript API reference](/en-us/javascript/api/@microsoft/signalr)
* [JavaScript tutorial](../tutorials/signalr?view=aspnetcore-10.0)
* [WebPack and TypeScript tutorial](../tutorials/signalr-typescript-webpack?view=aspnetcore-10.0)
* [Hubs](hubs?view=aspnetcore-10.0)
* [.NET client](dotnet-client?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/content-security-policy
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/content-security-policy.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Enforce a Content Security Policy for ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to use a Content Security Policy (CSP) with ASP.NET Core Blazor apps to help protect against certain types of malicious attacks, such as [Cross-Site Scripting (XSS)](../../security/cross-site-scripting?view=aspnetcore-10.0) and [clickjacking](https://developer.mozilla.org/docs/Web/Security/Attacks/Clickjacking) attacks. XSS is a security vulnerability where a cyberattacker places one or more malicious client-side scripts into an app's rendered content. In a clickjacking attack, a user is tricked into interactions with a decoy website that has your app embedded within it.

A CSP helps protect against these types of attacks by informing the browser of valid:

* Sources for loaded content, including scripts, stylesheets, images, and plugins.
* Actions taken by a page, specifying permitted URL targets of forms.
* When your app can be embedded into another website via `<frame>`, `<iframe>`, `<object>`, or `<embed>` tags.

We recommend reading the following MDN resources when implementing a CSP:

* [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP)
* [CSP reference guidance](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy)
* [Content Security Policy (CSP) implementation](https://developer.mozilla.org/docs/Web/Security/Practical_implementation_guides/CSP)

To apply a CSP to an app, the developer specifies several CSP content security *directives* in one or more `Content-Security-Policy` headers or `<meta>` tags. For guidance on applying a CSP to an app in C# code at startup, see [ASP.NET Core Blazor startup](../fundamentals/startup?view=aspnetcore-10.0#control-headers-in-c-code) and the [*The `frame-ancestors` directive*](#the-frame-ancestors-directive) section later in this article.

Policies are evaluated by the browser while a page is loading. The browser inspects the page's sources and determines if they meet the requirements of the content security directives. When policy directives aren't met for a resource, the browser doesn't load the resource. For example, consider a policy that doesn't allow third-party scripts. When a page contains a `<script>` tag with a third-party origin in the `src` attribute, the browser prevents the script from loading.

CSP is supported in most modern desktop and mobile browsers, including Chrome, Edge, Firefox, Opera, and Safari. CSP is recommended for Blazor apps.

Warning

Implementing a CSP minimizes the risk of certain types of security threats and doesn't guarantee that an app is completely safe from XSS and clickjacking attacks. User agents, typically browsers, may allow users to modify or bypass policy enforcement through user preferences, bookmarklets, browser extensions, third-party additions to the user agent, and other such mechanisms. Also, CSPs are only focused on remediating a subset of attacks, not all attacks that can compromise security, such as SQL injection, Cross-Site Request Forgery (CSRF), security misconfiguration, and denial-of-service (DoS) attacks.

## Policy directives

The following directives and sources are commonly used for Blazor apps. Add additional directives and sources as needed. The following directives are used in the [*Apply the policy*](#apply-the-policy) section of this article, where example security policies for Blazor apps are provided:

* [`base-uri`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri): Restricts the URLs for a page's `<base>` tag. Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

* [`default-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/default-src): Indicates a fallback for source directives that aren't explicitly specified by the policy. Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

* [`img-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/img-src): Indicates valid sources for images.

  * Specify `data:` to permit loading images from `data:` URLs.
  * Specify `https:` to permit loading images from HTTPS endpoints.

* [`object-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/object-src): Indicates valid sources for the `<object>`, `<embed>`, and `<applet>` tags. Specify `none` to prevent all URL sources.

* [`script-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/script-src): Indicates valid sources for scripts.

  * Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

  * In a client-side Blazor app:

    * Specify [`wasm-unsafe-eval`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_webassembly_execution) to permit the client-side Blazor Mono runtime to function.
    * Specify any additional hashes with the [`unsafe-hashes`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src#unsafe_hashes) source expression to permit your required *non-framework inline scripts* to load.

  * In a server-side Blazor app, specify hashes to permit required scripts to load.

* [`style-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/style-src): Indicates valid sources for stylesheets.

  * Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.
  * If the app uses inline styles, specify `unsafe-inline` to allow the use of your inline styles.

* [`connect-src`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/connect-src): Restricts the URLs that can be loaded using script interfaces. The scheme sources `http:`, `ws:` (WebSocket protocol), and `wss:` (WebSocket Secure protocol) are specified.

* [upgrade-insecure-requests](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/upgrade-insecure-requests): Indicates that content URLs from insecure (HTTP) sources should be acquired securely over HTTPS.

- [`base-uri`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri): Restricts the URLs for a page's `<base>` tag. Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

- [`default-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/default-src): Indicates a fallback for source directives that aren't explicitly specified by the policy. Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

- [`img-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/img-src): Indicates valid sources for images.

  * Specify `data:` to permit loading images from `data:` URLs.
  * Specify `https:` to permit loading images from HTTPS endpoints.

- [`object-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/object-src): Indicates valid sources for the `<object>`, `<embed>`, and `<applet>` tags. Specify `none` to prevent all URL sources.

- [`script-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/script-src): Indicates valid sources for scripts.

  * Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

  * In a client-side Blazor app:

    * Specify `unsafe-eval` to permit the client-side Blazor Mono runtime to function.
    * Specify any additional hashes to permit your required *non-framework scripts* to load.

  * In a server-side Blazor app, specify hashes to permit required scripts to load.

- [`style-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/style-src): Indicates valid sources for stylesheets.

  * Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.
  * If the app uses inline styles, specify `unsafe-inline` to allow the use of your inline styles.

- [`connect-src`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/connect-src): Restricts the URLs that can be loaded using script interfaces. The scheme sources `http:`, `ws:` (WebSocket protocol), and `wss:` (WebSocket Secure protocol) are specified.

- [`upgrade-insecure-requests`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/upgrade-insecure-requests): Indicates that content URLs from insecure (HTTP) sources should be acquired securely over HTTPS.

* [`base-uri`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri): Restricts the URLs for a page's `<base>` tag. Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

* [`default-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/default-src): Indicates a fallback for source directives that aren't explicitly specified by the policy. Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

* [`img-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/img-src): Indicates valid sources for images.

  * Specify `data:` to permit loading images from `data:` URLs.
  * Specify `https:` to permit loading images from HTTPS endpoints.

* [`object-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/object-src): Indicates valid sources for the `<object>`, `<embed>`, and `<applet>` tags. Specify `none` to prevent all URL sources.

* [`script-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/script-src): Indicates valid sources for scripts.

  * Specify the `https://stackpath.bootstrapcdn.com/` host source for Bootstrap scripts.

  * Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

  * In a client-side Blazor app:

    * Specify `unsafe-eval` to permit the client-side Blazor Mono runtime to function.
    * Specify any additional hashes to permit your required *non-framework scripts* to load.

  * In a server-side Blazor app, specify hashes to permit required scripts to load.

* [`style-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/style-src): Indicates valid sources for stylesheets.

  * Specify the `https://stackpath.bootstrapcdn.com/` host source for Bootstrap stylesheets.
  * Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.
  * Specify `unsafe-inline` to allow the use of inline styles.

* [`connect-src`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/connect-src): Restricts the URLs that can be loaded using script interfaces. The scheme sources `http:`, `ws:` (WebSocket protocol), and `wss:` (WebSocket Secure protocol) are specified.

* [`upgrade-insecure-requests`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/upgrade-insecure-requests): Indicates that content URLs from insecure (HTTP) sources should be acquired securely over HTTPS.

- [`base-uri`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri): Restricts the URLs for a page's `<base>` tag. Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

- [`default-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/default-src): Indicates a fallback for source directives that aren't explicitly specified by the policy. Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

- [`img-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/img-src): Indicates valid sources for images.

  * Specify `data:` to permit loading images from `data:` URLs.
  * Specify `https:` to permit loading images from HTTPS endpoints.

- [`object-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/object-src): Indicates valid sources for the `<object>`, `<embed>`, and `<applet>` tags. Specify `none` to prevent all URL sources.

- [`script-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/script-src): Indicates valid sources for scripts.

  * Specify the `https://stackpath.bootstrapcdn.com/` host source for Bootstrap scripts.

  * Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.

  * In a client-side Blazor app:

    * Specify hashes to permit required scripts to load.
    * Specify `unsafe-eval` to use `eval()` and methods for creating code from strings.

  * In a server-side Blazor app, specify hashes to permit required scripts to load.

- [`style-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/style-src): Indicates valid sources for stylesheets.

  * Specify the `https://stackpath.bootstrapcdn.com/` host source for Bootstrap stylesheets.
  * Specify `self` to indicate that the app's origin, including the scheme and port number, is a valid source.
  * Specify `unsafe-inline` to allow the use of inline styles. The inline declaration is required for the UI for reconnecting the client and server after the initial request. In a future release, inline styling might be removed so that `unsafe-inline` is no longer required.

- [`connect-src`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/connect-src): Restricts the URLs that can be loaded using script interfaces. The scheme sources `http:`, `ws:` (WebSocket protocol), and `wss:` (WebSocket Secure protocol) are specified.

- [`upgrade-insecure-requests`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/upgrade-insecure-requests): Indicates that content URLs from insecure (HTTP) sources should be acquired securely over HTTPS.

The preceding directives are supported by all browsers except Microsoft Internet Explorer.

To obtain SHA hashes for additional inline scripts:

* Apply the CSP shown in the *Apply the policy* section.
* Access the browser's developer tools console while running the app locally. The browser calculates and displays hashes for blocked scripts when a CSP header or `meta` tag is present.
* Copy the hashes provided by the browser to the `script-src` sources. Use single quotes around each hash.

For a Content Security Policy Level 2 browser support matrix, see [Can I use: Content Security Policy Level 2](https://www.caniuse.com/#feat=contentsecuritypolicy2).

## Apply the policy

You can apply a CSP via:

* A response header issued by the host (for example, IIS) or issued by the app (see [Control headers in C# code at startup](../fundamentals/startup?view=aspnetcore-10.0#control-headers-in-c-code)).
* A `<meta>` tag. *This article only demonstrates the `<meta>` tag approach.*

To use a `<meta>` tag to apply the policy:

* Set the value of the `http-equiv` attribute to `Content-Security-Policy`.
* Place the directives in the `content` attribute value. Separate directives with a semicolon (`;`). An ending semicolon for the last directive of the policy string isn't required per the [Content Security Policy Level 3 specification](https://www.w3.org/TR/CSP3/).
* Place the `<meta>` tag in the [`<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content) just inside the opening `<head>` tag. The policy is evaluated and enforced when the CSP markup is parsed, so the policy should appear at the top of `<head>` markup to ensure that it's enforced on all `<script>` and `<link>` tags.

The following sections show example policies. These examples are versioned with this article for each release of Blazor. To use a version appropriate for your release, select the document version with the **Version** dropdown selector on this webpage.

## The `frame-ancestors` directive

The [`frame-ancestors`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) directive specifies valid parents that may embed a page with `<frame>`, `<iframe>`, `<object>`, or `<embed>` tags. A `frame-ancestors` directive can't be applied via a `<meta>` tag-based CSP. The directive must be applied by a response header. A CSP response header can be added by a server host, or app C# code can add or update a CSP with a `frame-ancestors` directive.

Blazor Web Apps (.NET 8 or later) automatically include a response header setting the value to `'self'`:

```
Content-Security-Policy: frame-ancestors 'self'
```

To change the default value to the more restrictive `'none'` and prevent all parents from embedding the app, set the [ContentSecurityFrameAncestorsPolicy](/en-us/dotnet/api/microsoft.aspnetcore.components.server.servercomponentsendpointoptions.contentsecurityframeancestorspolicy) option in the call to [AddInteractiveServerRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.serverrazorcomponentsendpointconventionbuilderextensions.addinteractiveserverrendermode) in the `Program` file. The following only takes effect when WebSocket compression is enabled ([ConfigureWebSocketAcceptContext](/en-us/dotnet/api/microsoft.aspnetcore.components.server.servercomponentsendpointoptions.configurewebsocketacceptcontext#microsoft-aspnetcore-components-server-servercomponentsendpointoptions-configurewebsocketacceptcontext) is set, which is the default for Blazor apps).

```
.AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")
```

In Blazor Server apps, a default `frame-ancestors` directive isn't added to the response headers collection. You can add a CSP header manually with [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) in the request processing pipeline:

```
app.Use(async (context, next) =>
{
    context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none'");
    await next();
});
```

Warning

Avoid setting the `frame-ancestors` directive value to `'null'` when WebSocket compression is enabled (compression is the default) because it makes the app vulnerable to malicious script injection and clickjacking attacks.

For more information, see [CSP: frame-ancestors (MDN documentation)](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors).

### Server-side Blazor apps

The following example is a starting point for further development. At the top of [`<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content), apply the directives described in the [*Policy directives*](#policy-directives) section, along with any other directives that your app specification requires.

For Blazor Web Apps or Blazor Server apps:

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self' 'wasm-unsafe-eval';
    style-src https:;
    connect-src 'self' http: ws: wss:;
    upgrade-insecure-requests;" />
```

Blazor Web Apps have an `ImportMap` component in `<head>` content that renders an inline import map `<script>` tag. To modify the policy to permit the import map to load, see the [Resolving CSP violations with Subresource Integrity (SRI) or a cryptographic nonce](#resolving-csp-violations-with-subresource-integrity-sri-or-a-cryptographic-nonce) section.

For Blazor Web Apps or Blazor Server apps:

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self' 'wasm-unsafe-eval' 'unsafe-hashes' 
        'sha256-qnHnQs7NjQNHHNYv/I9cW+I62HzDJjbnyS/OFzqlix0=';
    style-src https:;
    connect-src 'self' http: ws: wss:;
    upgrade-insecure-requests;" />
```

Blazor Web Apps include an inline `onclick` JavaScript event handler in the `NavMenu` component that requires either of the following changes:

* Add a hash to the `script-src` directive with the `unsafe-hashes` keyword:

  ```
  'unsafe-hashes' 'sha256-qnHnQs7NjQNHHNYv/I9cW+I62HzDJjbnyS/OFzqlix0='
  ```

  For more information, see [CSP: script-src: Unsafe inline script (MDN documentation)](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src#unsafe_inline_script).

* Move the inline JavaScript event handler to a JavaScript file or module that's permitted to load by the policy.

Blazor Web Apps also have an `ImportMap` component in `<head>` content that renders an inline import map `<script>` tag. To modify the policy to permit the import map to load, see the [Resolving CSP violations with Subresource Integrity (SRI) or a cryptographic nonce](#resolving-csp-violations-with-subresource-integrity-sri-or-a-cryptographic-nonce) section.

For Blazor Server apps:

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self';
    style-src 'self';
    connect-src 'self' http: ws: wss:;
    upgrade-insecure-requests">
```

For Blazor Server apps:

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self' https://stackpath.bootstrapcdn.com/;
    style-src 'self' 'unsafe-inline' https://stackpath.bootstrapcdn.com/;
    connect-src 'self' http: ws: wss:;
    upgrade-insecure-requests">
```

For Blazor Server apps:

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self' https://stackpath.bootstrapcdn.com/ 
        'sha256-34WLX60Tw3aG6hylk0plKbZZFXCuepeQ6Hu7OqRf8PI=';
    style-src 'unsafe-inline' https://stackpath.bootstrapcdn.com/;
    connect-src 'self' http: ws: wss:;
    upgrade-insecure-requests">
```

Note

The preceding SHA256 hash is for demonstration purposes. You may need to calculate a new hash for your CSP.

If the app uses one or more `Virtualize` components, they render dynamic inline `style` attributes on their spacer elements because spacer heights are calculated at runtime based on scroll position, item count, and average item size, which change on every scroll interaction. Strict CSP configurations that don't allow inline styles block these `style` attributes and prevent virtualization from working. If you can't upgrade to ASP.NET Core 11.0 or later (where `Virtualize` is CSP-compliant), see [ASP.NET Core Razor component virtualization](../components/virtualization?view=aspnetcore-10.0#content-security-policy-csp-compliance).

Add additional `script-src` and `style-src` hashes as required by the app. During development, use an online tool or browser developer tools to have the hashes calculated for you. For example, the following browser tools console error reports the hash for a required script not covered by the policy:

> Refused to execute inline script because it violates the following Content Security Policy directive: " ... ". Either the 'unsafe-inline' keyword, a hash ('sha256-v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA='), or a nonce ('nonce-...') is required to enable inline execution.

The particular script associated with the error is displayed in the console next to the error.

For guidance on applying a CSP to an app in C# code at startup, see [ASP.NET Core Blazor startup](../fundamentals/startup?view=aspnetcore-10.0#control-headers-in-c-code).

### Client-side Blazor apps

The following example is a starting point for further development. In the [`<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content), apply the directives described in the *Policy directives* section:

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self' 'wasm-unsafe-eval';
    style-src 'self';
    connect-src 'none';
    upgrade-insecure-requests">
```

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self' 'unsafe-eval';
    style-src 'self';
    connect-src 'none';
    upgrade-insecure-requests">
```

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self' 'unsafe-eval' 
        'sha256-v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA=';
    style-src 'self';
    connect-src 'none';
    upgrade-insecure-requests">
```

Note

The `sha256-v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA=` hash represents the [inline](https://github.com/dotnet/aspnetcore/blob/57501251222b199597b9ac16888f362a69eb13c1/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts#L212) script that's used for client-side Blazor apps. This may be removed in the future.

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self' 'unsafe-eval' https://stackpath.bootstrapcdn.com/ 
        'sha256-v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA=';
    style-src 'self' 'unsafe-inline' https://stackpath.bootstrapcdn.com/;
    upgrade-insecure-requests">
```

```
<meta http-equiv="Content-Security-Policy" content="
    base-uri 'self';
    default-src 'self';
    img-src data: https:;
    object-src 'none';
    script-src 'self' 'unsafe-eval' https://stackpath.bootstrapcdn.com/ 
        'sha256-v8ZC9OgMhcnEQ/Me77/R9TlJfzOBqrMTW8e1KuqLaqc=' 
        'sha256-If//FtbPc03afjLezvWHnC3Nbu4fDM04IIzkPaf3pH0=' 
        'sha256-v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA=';
    style-src 'self' 'unsafe-inline' https://stackpath.bootstrapcdn.com/;
    upgrade-insecure-requests">
```

Add additional `script-src` and `style-src` hashes as required by the app. During development, use an online tool or browser developer tools to have the hashes calculated for you. For example, the following browser tools console error reports the hash for a required script not covered by the policy:

> Refused to execute inline script because it violates the following Content Security Policy directive: " ... ". Either the 'unsafe-inline' keyword, a hash ('sha256-v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA='), or a nonce ('nonce-...') is required to enable inline execution.

The particular script associated with the error is displayed in the console next to the error.

## Resolving CSP violations with Subresource Integrity (SRI) or a cryptographic nonce

Two approaches for resolving CSP violations, which are described in the next two sections, are:

* [Subresource Integrity (SRI)](#adopt-subresource-integrity-sri) *Recommended*
* [Cryptographic nonce](#adopt-a-cryptographic-nonce)

### Adopt Subresource Integrity (SRI)

Subresource Integrity (SRI) enables browsers to confirm that fetched resources aren't tampered with in transit. A cryptographic hash provided on the resource must match the hash computed by the browser for the fetched resource and the hash listed in the CSP. Browser compatibility can be assessed at [Can I use? Subresource Integrity](https://caniuse.com/subresource-integrity).

In the following example for a Blazor Server app, an integrity is calculated using a third-party tool and specified for the Blazor script (`blazor.server.js`) and CSP. The Blazor script doesn't dynamically change in this scenario and has a stable SHA hash, so you can hardcode the `integrity` attribute's value.

Warning

Set the [`crossorigin` attribute](https://developer.mozilla.org/docs/Web/HTML/Attributes/crossorigin) on a subresource that's loaded from a different origin without [Cross-Origin Resource Sharing (CORS)](../../security/cors?view=aspnetcore-10.0). If the app's origin is different from where a subresource loads, an `Access-Control-Allow-Origin` header is required that allows the resource to be shared with the requesting origin *or else* the `crossorigin` attribute must be applied to the subresource's tag in the app. Otherwise, the browser adopts the 'fail-open' policy for the subresource, which means the subresource is loaded without checking its integrity.

The `crossorigin` attribute isn't added to the Blazor `<script>` tag in the following example because the Blazor script is loaded from the app's origin.

For more information, see [Cross-Origin Resource Sharing and Subresource Integrity (MDN documentation)](https://developer.mozilla.org/docs/Web/Security/Subresource_Integrity#cross-origin_resource_sharing_and_subresource_integrity).

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Security-Policy" content="
        base-uri 'self';
        default-src 'self';
        img-src data: https:;
        object-src 'none';
        script-src 'sha256-FyuamsHhg0nWZUnu/f5qrt2DlL1XKt5AX+cgRhtxtfg=';
        style-src https:;
        connect-src 'self' http: ws: wss:;
        upgrade-insecure-requests;" />
    ...
</head>
<body>
    ...
    <script src="_framework/blazor.server.js" 
        integrity="sha256-FyuamsHhg0nWZUnu/f5qrt2DlL1XKt5AX+cgRhtxtfg="></script>
</body>
</html>
```

In the following example for a Blazor Web App (.NET 8 or later), an integrity is calculated for the `ImportMap` component (.NET 9 or later). The `ImportMap` integrity value is computed for each app request because the `ImportMap` component renders unique content each time the page is generated for fingerprinted assets.

The rendered import map from the `ImportMap` component is generated by the app at its origin, so the [`crossorigin` attribute](https://developer.mozilla.org/docs/Web/HTML/Attributes/crossorigin) isn't included in the `ImportMap` tag. For more information, see [MDN CSP Guide: Hashes](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP#hashes) and [Subresource Integrity (MDN documentation)](https://developer.mozilla.org/docs/Web/Security/Subresource_Integrity).

```
@using System.Security.Cryptography
<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Security-Policy" content="
        base-uri 'self';
        default-src 'self';
        img-src data: https:;
        object-src 'none';
        script-src 'self' 'wasm-unsafe-eval' 'unsafe-hashes' '@integrity'
            'sha256-qnHnQs7NjQNHHNYv/I9cW+I62HzDJjbnyS/OFzqlix0=';
        style-src https:;
        connect-src 'self' http: ws: wss:;
        upgrade-insecure-requests;" />
    ...
    <ImportMap integrity="@integrity" />
    ...
</head>
...
</html>

@code {
    private string? integrity;

    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    protected override void OnInitialized()
    {
        var metadata = HttpContext?.GetEndpoint()?.Metadata
            .GetOrderedMetadata<ImportMapDefinition>();
        var utf8 = new System.Text.UTF8Encoding();
        var metadataBytes = utf8.GetBytes(
            metadata?.FirstOrDefault<ImportMapDefinition>()?.ToString()
                .ReplaceLineEndings("\n") ?? string.Empty);
        integrity = 
            $"sha256-{Convert.ToBase64String(SHA256.HashData(metadataBytes))}";
    }
}
```

Prior to .NET 6, use `.Replace("\r\n", "\n")` instead of calling [ReplaceLineEndings](/en-us/dotnet/api/system.string.replacelineendings) in the preceding code.

Note

If additional attributes must be splatted on the `ImportMap` components's rendered `<script>` element, you can pass a dictionary of all of the attributes to the `ImportMap` component in its [AdditionalAttributes](/en-us/dotnet/api/microsoft.aspnetcore.components.importmap.additionalattributes) property. The `integrity` attribute name-value pair are passed in the dictionary with the rest of the additional passed attributes.

### Adopt a cryptographic nonce

A cryptographic nonce (*number used once*) enables browsers to confirm that fetched resources aren't tampered with in transit. A single-use cryptographic nonce provided in the CSP must match the nonce indicated on the resource. Browser compatibility can be assessed at [Can I use? Nonce](https://caniuse.com/?search=nonce).

In the following example for a Blazor Web App (.NET 8 or later), a nonce is created for the `ImportMap` component (.NET 9 or later) with a unique value each time the app is loaded.

For more information, see [MDN CSP Guide: Nonces](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP#nonces) and [CSP: script-src: Unsafe inline script (MDN documentation)](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src#unsafe_inline_script).

```
@using System.Security.Cryptography
<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Security-Policy" content="
        base-uri 'self';
        default-src 'self';
        img-src data: https:;
        object-src 'none';
        script-src 'self' 'wasm-unsafe-eval' 'unsafe-hashes' 'nonce-@nonce' 
            'sha256-qnHnQs7NjQNHHNYv/I9cW+I62HzDJjbnyS/OFzqlix0=';
        style-src https:;
        connect-src 'self' http: ws: wss:;
        upgrade-insecure-requests;" />
    ...
    <ImportMap nonce="@nonce" />
    ...
</head>
...
</html>

@code {
    private string? nonce;

    protected override void OnInitialized()
    {
        using (var rng = RandomNumberGenerator.Create())
        {
            var nonceBytes = new byte[32];
            rng.GetBytes(nonceBytes);
            nonce = Convert.ToBase64String(nonceBytes);
        }
    }
}
```

Note

If additional attributes must be splatted on the `ImportMap` components's rendered `<script>` element, you can pass a dictionary of all of the attributes to the `ImportMap` component in its [AdditionalAttributes](/en-us/dotnet/api/microsoft.aspnetcore.components.importmap.additionalattributes) property. The nonce name-value pair is passed in the dictionary with the rest of the additional passed attributes.

## Apply a CSP in non-`Development` environments

When a CSP is applied to a Blazor app's `<head>` content, it interferes with local testing in the `Development` environment. For example, [Browser Link](../../client-side/using-browserlink?view=aspnetcore-10.0) and the browser refresh script fail to load. The following examples demonstrate how to apply the CSP's `<meta>` tag in non-`Development` environments.

Note

The examples in this section don't show the full `<meta>` tag for the CSPs. The complete `<meta>` tags are found in the subsections of the [Apply the policy](#apply-the-policy) section earlier in this article.

Three general approaches are available:

* Apply the CSP via the `App` component, which applies the CSP to all layouts of the app.
* If you need to apply CSPs to different areas of the app, for example a custom CSP for only the admin pages, apply the CSPs on a per-layout basis using the [`<HeadContent>` tag](../components/control-head-content?view=aspnetcore-10.0). For complete effectiveness, every app layout file must adopt the approach.
* The hosting service or server can provide a CSP via a [`Content-Security-Policy` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy) added an app's outgoing responses. Because this approach varies by hosting service or server, it isn't addressed in the following examples. If you wish to adopt this approach, consult the documentation for your hosting service provider or server.

### Blazor Web App approaches

In the `App` component (`Components/App.razor`), inject [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment):

```
@inject IHostEnvironment Env
```

In the `App` component's `<head>` content, apply the CSP when not in the `Development` environment:

```
@if (!Env.IsDevelopment())
{
    <meta ...>
}
```

Alternatively, apply CSPs on a per-layout basis in the `Components/Layout` folder, as the following example demonstrates. Make sure that every layout specifies a CSP.

```
@inject IHostEnvironment Env

@if (!Env.IsDevelopment())
{
    <HeadContent>
        <meta ...>
    </HeadContent>
}
```

### Blazor WebAssembly app approaches

In the `App` component (`App.razor`), inject [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment):

```
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@inject IWebAssemblyHostEnvironment Env
```

In the `App` component's `<head>` content, apply the CSP when not in the `Development` environment:

```
@if (!Env.IsDevelopment())
{
    <HeadContent>
        <meta ...>
    </HeadContent>
}
```

Alternatively, use the preceding code but apply CSPs on a per-layout basis in the `Layout` folder. Make sure that every layout specifies a CSP.

## Meta tag limitations

A `<meta>` tag policy doesn't support the following directives:

* [`frame-ancestors`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors)
* [`report-to`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/report-to)
* [`report-uri`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri)
* [`sandbox`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox)

To support the preceding directives, use a header named `Content-Security-Policy`. The directive string is the header's value.

## Test a policy and receive violation reports

Testing helps confirm that third-party scripts aren't inadvertently blocked when building an initial policy.

To test a policy over a period of time without enforcing the policy directives, set the `<meta>` tag's `http-equiv` attribute or header name of a header-based policy to `Content-Security-Policy-Report-Only`. Failure reports are sent as JSON documents to a specified URL. For more information, see [MDN web docs: Content-Security-Policy-Report-Only](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only).

For reporting on violations while a policy is active, see the following articles:

* [report-to](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/report-to)
* [report-uri](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri)

Although `report-uri` is no longer recommended for use, both directives should be used until `report-to` is supported by all of the major browsers. Don't exclusively use `report-uri` because support for `report-uri` is subject to being dropped *at any time* from browsers. Remove support for `report-uri` in your policies when `report-to` is fully supported. To track adoption of `report-to`, see [Can I use: report-to](https://caniuse.com/#feat=mdn-http_headers_csp_content-security-policy_report-to).

Test and update an app's policy every release.

## Troubleshoot

* Errors appear in the browser's developer tools console. Browsers provide information about:

  * Elements that don't comply with the policy.
  * How to modify the policy to allow for a blocked item.

* A policy is only completely effective when the client's browser supports all of the included directives. For a current browser support matrix, see [Can I use: Content-Security-Policy](https://caniuse.com/#search=Content-Security-Policy).

## Related content

* [Apply a CSP in C# code at startup](../fundamentals/startup?view=aspnetcore-10.0#control-headers-in-c-code)
* [MDN web docs: Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/CSP)
* [MDN web docs: Content-Security-Policy response header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy)
* [Content Security Policy Level 3](https://www.w3.org/TR/CSP3/)
* [Google CSP Evaluator](https://csp-evaluator.withgoogle.com/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/hub-filters
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/hub-filters.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use hub filters in ASP.NET Core SignalR

Hub filters:

* Are available in .NET 5 or later.
* Allow logic to run before and after hub methods are invoked by clients.

This article provides guidance for writing and using hub filters.

## Configure hub filters

Hub filters can be applied globally or per hub type. The order in which filters are added is the order in which the filters run. Global hub filters run before local hub filters.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR(options =>
    {
        // Global filters will run first
        options.AddFilter<CustomFilter>();
    }).AddHubOptions<ChatHub>(options =>
    {
        // Local filters will run second
        options.AddFilter<CustomFilter2>();
    });
}
```

A hub filter can be added in one of the following ways:

* Add a filter by concrete type:

  ```
  hubOptions.AddFilter<TFilter>();
  ```

  This will be resolved from dependency injection (DI) or type activated.

* Add a filter by runtime type:

  ```
  hubOptions.AddFilter(typeof(TFilter));
  ```

  This will be resolved from DI or type activated.

* Add a filter by instance:

  ```
  hubOptions.AddFilter(new MyFilter());
  ```

  This instance will be used like a singleton. All hub method invocations will use the same instance.

Hub filters are created and disposed per hub invocation. If you want to store global state in the filter, or no state, add the hub filter type to DI as a singleton for better performance. Alternatively, add the filter as an instance if you can.

## Create hub filters

Create a filter by declaring a class that inherits from `IHubFilter`, and add the `InvokeMethodAsync` method. There is also `OnConnectedAsync` and `OnDisconnectedAsync` that can optionally be implemented to wrap the `OnConnectedAsync` and `OnDisconnectedAsync` hub methods respectively.

```
public class CustomFilter : IHubFilter
{
    public async ValueTask<object> InvokeMethodAsync(
        HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next)
    {
        Console.WriteLine($"Calling hub method '{invocationContext.HubMethodName}'");
        try
        {
            return await next(invocationContext);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Exception calling '{invocationContext.HubMethodName}': {ex}");
            throw;
        }
    }

    // Optional method
    public Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next)
    {
        return next(context);
    }

    // Optional method
    public Task OnDisconnectedAsync(
        HubLifetimeContext context, Exception exception, Func<HubLifetimeContext, Exception, Task> next)
    {
        return next(context, exception);
    }
}
```

Filters are very similar to middleware. The `next` method invokes the next filter. The final filter will invoke the hub method. Filters can also store the result from awaiting `next` and run logic after the hub method has been called before returning the result from `next`.

To skip a hub method invocation in a filter, throw an exception of type `HubException` instead of calling `next`. The client will receive an error if it was expecting a result.

## Use hub filters

When writing the filter logic, try to make it generic by using attributes on hub methods instead of checking for hub method names.

Consider a filter that will check a hub method argument for banned phrases and replace any phrases it finds with `***`. For this example, assume a `LanguageFilterAttribute` class is defined. The class has a property named `FilterArgument` that can be set when using the attribute.

1. Place the attribute on the hub method that has a string argument to be cleaned:

   ```
   public class ChatHub
   {
       [LanguageFilter(filterArgument = 0)]
       public async Task SendMessage(string message, string username)
       {
           await Clients.All.SendAsync("SendMessage", $"{username} says: {message}");
       }
   }
   ```

2. Define a hub filter to check for the attribute and replace banned phrases in a hub method argument with `***`:

   ```
   public class LanguageFilter : IHubFilter
   {
       // populated from a file or inline
       private List<string> bannedPhrases = new List<string> { "async void", ".Result" };

       public async ValueTask<object> InvokeMethodAsync(HubInvocationContext invocationContext, 
           Func<HubInvocationContext, ValueTask<object>> next)
       {
           var languageFilter = (LanguageFilterAttribute)Attribute.GetCustomAttribute(
               invocationContext.HubMethod, typeof(LanguageFilterAttribute));
           if (languageFilter != null &&
               invocationContext.HubMethodArguments.Count > languageFilter.FilterArgument &&
               invocationContext.HubMethodArguments[languageFilter.FilterArgument] is string str)
           {
               foreach (var bannedPhrase in bannedPhrases)
               {
                   str = str.Replace(bannedPhrase, "***");
               }

               var arguments = invocationContext.HubMethodArguments.ToArray();
               arguments[languageFilter.FilterArgument] = str;
               invocationContext = new HubInvocationContext(invocationContext.Context,
                   invocationContext.ServiceProvider,
                   invocationContext.Hub,
                   invocationContext.HubMethod,
                   arguments);
           }

           return await next(invocationContext);
       }
   }
   ```

3. Register the hub filter in the `Startup.ConfigureServices` method. To avoid reinitializing the banned phrases list for every invocation, the hub filter is registered as a singleton:

   ```
   public void ConfigureServices(IServiceCollection services)
   {
       services.AddSignalR(hubOptions =>
       {
           hubOptions.AddFilter<LanguageFilter>();
       });

       services.AddSingleton<LanguageFilter>();
   }
   ```

## The HubInvocationContext object

The `HubInvocationContext` contains information for the current hub method invocation.

| Property             | Description                                                        | Type                    |
| -------------------- | ------------------------------------------------------------------ | ----------------------- |
| `Context`            | The `HubCallerContext` contains information about the connection.  | `HubCallerContext`      |
| `Hub`                | The instance of the Hub being used for this hub method invocation. | `Hub`                   |
| `HubMethodName`      | The name of the hub method being invoked.                          | `string`                |
| `HubMethodArguments` | The list of arguments being passed to the hub method.              | `IReadOnlyList<string>` |
| `ServiceProvider`    | The scoped service provider for this hub method invocation.        | `IServiceProvider`      |
| `HubMethod`          | The hub method information.                                        | `MethodInfo`            |

## The HubLifetimeContext object

The `HubLifetimeContext` contains information for the `OnConnectedAsync` and `OnDisconnectedAsync` hub methods.

| Property          | Description                                                        | Type               |
| ----------------- | ------------------------------------------------------------------ | ------------------ |
| `Context`         | The `HubCallerContext` contains information about the connection.  | `HubCallerContext` |
| `Hub`             | The instance of the Hub being used for this hub method invocation. | `Hub`              |
| `ServiceProvider` | The scoped service provider for this hub method invocation.        | `IServiceProvider` |

## Authorization and filters

[Authorize attributes on hub methods](authn-and-authz?view=aspnetcore-10.0#use-authorization-handlers-to-customize-hub-method-authorization) run before hub filters.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/22-to-30
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/22-to-30.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core 2.2 to 3.0

By [Scott Addie](https://github.com/scottaddie) and [Rick Anderson](https://twitter.com/RickAndMSFT)

This article explains how to update an existing ASP.NET Core 2.2 project to ASP.NET Core 3.0. It might be helpful to create a new ASP.NET Core 3.0 project to:

* Compare with the ASP.NET Core 2.2 code.
* Copy the relevant changes to your ASP.NET Core 3.0 project.

## Update .NET Core SDK version in global.json

If your solution relies upon a [global.json](/en-us/dotnet/core/tools/global-json) file to target a specific .NET Core SDK version, update its `version` property to the 3.0 version installed on your machine:

```
{
  "sdk": {
    "version": "3.0.100"
  }
}
```

## Update the project file

### Update the Target Framework

ASP.NET Core 3.0 or later only run on .NET Core. Set the [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks) to `netcoreapp3.0`:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

</Project>
```

### Remove obsolete package references

A large number of NuGet packages aren't produced for ASP.NET Core 3.0. Such package references should be removed from your project file. Consider the following project file for an ASP.NET Core 2.2 web app:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App"/>
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
  </ItemGroup>

</Project>
```

The updated project file for ASP.NET Core 3.0:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

</Project>
```

The updated ASP.NET Core 3.0 project file:

* In the `<PropertyGroup>`:

  * Updates the TFM to `netcoreapp3.0`
  * Removes the `<AspNetCoreHostingModel>` element. For more information, see [In-process hosting model](#in-process-hosting-model) in this document.

* In the `<ItemGroup>`:

  * `Microsoft.AspNetCore.App` is removed. For more information, see [Framework reference](#framework-reference) in this document.
  * `Microsoft.AspNetCore.Razor.Design` is removed and in the following list of packages no longer being produced.

To see the full list of packages that are no longer produced, select the following expand list:

Click to expand the list of packages no longer being produced

* Microsoft.AspNetCore
* Microsoft.AspNetCore.All
* Microsoft.AspNetCore.App
* Microsoft.AspNetCore.Antiforgery
* Microsoft.AspNetCore.Authentication
* Microsoft.AspNetCore.Authentication.Abstractions
* Microsoft.AspNetCore.Authentication.Cookies
* Microsoft.AspNetCore.Authentication.Core
* Microsoft.AspNetCore.Authentication.OAuth
* Microsoft.AspNetCore.Authorization.Policy
* Microsoft.AspNetCore.CookiePolicy
* Microsoft.AspNetCore.Cors
* Microsoft.AspNetCore.Diagnostics
* Microsoft.AspNetCore.Diagnostics.HealthChecks
* Microsoft.AspNetCore.HostFiltering
* Microsoft.AspNetCore.Hosting
* Microsoft.AspNetCore.Hosting.Abstractions
* Microsoft.AspNetCore.Hosting.Server.Abstractions
* Microsoft.AspNetCore.Http
* Microsoft.AspNetCore.Http.Abstractions
* Microsoft.AspNetCore.Http.Connections
* Microsoft.AspNetCore.Http.Extensions
* Microsoft.AspNetCore.HttpOverrides
* Microsoft.AspNetCore.HttpsPolicy
* Microsoft.AspNetCore.Identity
* Microsoft.AspNetCore.Localization
* Microsoft.AspNetCore.Localization.Routing
* Microsoft.AspNetCore.Mvc
* Microsoft.AspNetCore.Mvc.Abstractions
* Microsoft.AspNetCore.Mvc.Analyzers
* Microsoft.AspNetCore.Mvc.ApiExplorer
* Microsoft.AspNetCore.Mvc.Api.Analyzers
* Microsoft.AspNetCore.Mvc.Core
* Microsoft.AspNetCore.Mvc.Cors
* Microsoft.AspNetCore.Mvc.DataAnnotations
* Microsoft.AspNetCore.Mvc.Formatters.Json
* Microsoft.AspNetCore.Mvc.Formatters.Xml
* Microsoft.AspNetCore.Mvc.Localization
* Microsoft.AspNetCore.Mvc.Razor
* Microsoft.AspNetCore.Mvc.Razor.ViewCompilation
* Microsoft.AspNetCore.Mvc.RazorPages
* Microsoft.AspNetCore.Mvc.TagHelpers
* Microsoft.AspNetCore.Mvc.ViewFeatures
* Microsoft.AspNetCore.Razor
* Microsoft.AspNetCore.Razor.Runtime
* Microsoft.AspNetCore.Razor.Design
* Microsoft.AspNetCore.ResponseCaching
* Microsoft.AspNetCore.ResponseCaching.Abstractions
* Microsoft.AspNetCore.ResponseCompression
* Microsoft.AspNetCore.Rewrite
* Microsoft.AspNetCore.Routing
* Microsoft.AspNetCore.Routing.Abstractions
* Microsoft.AspNetCore.Server.HttpSys
* Microsoft.AspNetCore.Server.IIS
* Microsoft.AspNetCore.Server.IISIntegration
* Microsoft.AspNetCore.Server.Kestrel
* Microsoft.AspNetCore.Server.Kestrel.Core
* Microsoft.AspNetCore.Server.Kestrel.Https
* Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions
* Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets
* Microsoft.AspNetCore.Session
* Microsoft.AspNetCore.SignalR
* Microsoft.AspNetCore.SignalR.Core
* Microsoft.AspNetCore.StaticFiles
* Microsoft.AspNetCore.WebSockets
* Microsoft.AspNetCore.WebUtilities
* Microsoft.Net.Http.Headers

### Review breaking changes

[Review breaking changes](#break)

### Framework reference

Features of ASP.NET Core that were available through one of the packages listed above are available as part of the `Microsoft.AspNetCore.App` shared framework. The *shared framework* is the set of assemblies (*.dll* files) that are installed on the machine and includes a runtime component and a targeting pack.

* Projects that target the `Microsoft.NET.Sdk.Web` SDK implicitly reference the `Microsoft.AspNetCore.App` framework.

  No additional references are required for these projects:

  ```
  <Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
      <TargetFramework>netcoreapp3.0</TargetFramework>
    </PropertyGroup>
      ...
  </Project>
  ```

* Projects that target `Microsoft.NET.Sdk` or `Microsoft.NET.Sdk.Razor` SDK, should add an explicit `FrameworkReference` to `Microsoft.AspNetCore.App`:

  ```
  <Project Sdk="Microsoft.NET.Sdk.Razor">
    <PropertyGroup>
      <TargetFramework>netcoreapp3.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
      <FrameworkReference Include="Microsoft.AspNetCore.App" />
    </ItemGroup>
      ...
  </Project>
  ```

#### Framework-dependent builds using Docker

Framework-dependent builds of console apps that use a package that depends on the ASP.NET Core shared framework may give the following runtime error:

```
It was not possible to find any compatible framework version
The specified framework 'Microsoft.AspNetCore.App', version '3.0.0' was not found.
  - No frameworks were found.
```

`Microsoft.AspNetCore.App` is the shared framework containing the ASP.NET Core runtime and is only present on the `dotnet/core/aspnet` Docker image. The 3.0 SDK reduces the size of framework-dependent builds using ASP.NET Core by not including duplicate copies of libraries that are available in the shared framework. This is a potential savings of up to 18 MB, but it requires that the ASP.NET Core runtime be present / installed to run the app.

To determine if the app has a dependency (either direct or indirect) on the ASP.NET Core shared framework, examine the `runtimeconfig.json` file generated during a build/publish of your app. The following JSON file shows a dependency on the ASP.NET Core shared framework:

```
{
  "runtimeOptions": {
    "tfm": "netcoreapp3.0",
    "framework": {
      "name": "Microsoft.AspNetCore.App",
      "version": "3.0.0"
    },
    "configProperties": {
      "System.GC.Server": true
    }
  }
}
```

If your app is using Docker, use a base image that includes ASP.NET Core 3.0. For example, `docker pull mcr.microsoft.com/dotnet/core/aspnet:3.0`.

### Add package references for removed assemblies

ASP.NET Core 3.0 removes some assemblies that were previously part of the `Microsoft.AspNetCore.App` package reference. To visualize which assemblies were removed, compare the two shared framework folders. For example, a comparison of versions 2.2.7 and 3.0.0:

To continue using features provided by the removed assemblies, reference the 3.0 versions of the corresponding packages:

* A template-generated web app with **Individual User Accounts** requires adding the following packages:

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

    <PropertyGroup>
      <TargetFramework>netcoreapp3.0</TargetFramework>
      <UserSecretsId>My-secret</UserSecretsId>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.0.0" />
      <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.0.0" />
      <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.0.0" />
      <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
      <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0" />
    </ItemGroup>

  </Project>
  ```

* [Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore)

  For more information on referencing the database provider-specific package, see [Database Providers](/en-us/ef/core/providers/index).

* Identity UI

  Support for [Identity UI](../security/authentication/identity?view=aspnetcore-10.0) can be added by referencing the [Microsoft.AspNetCore.Identity.UI](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.UI) package.

* SPA Services

  * [Microsoft.AspNetCore.SpaServices](https://www.nuget.org/packages/Microsoft.AspNetCore.SpaServices)
  * [Microsoft.AspNetCore.SpaServices.Extensions](https://www.nuget.org/packages/Microsoft.AspNetCore.SpaServices.Extensions)

* Authentication: Support for third-party authentication flows are available as NuGet packages:

  * Facebook OAuth ([Microsoft.AspNetCore.Authentication.Facebook](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook))
  * Google OAuth ([Microsoft.AspNetCore.Authentication.Google](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Google))
  * Microsoft Account authentication ([Microsoft.AspNetCore.Authentication.MicrosoftAccount](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.MicrosoftAccount))
  * OpenID Connect authentication ([Microsoft.AspNetCore.Authentication.OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect))
  * OpenID Connect bearer token ([Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer))
  * Twitter OAuth ([Microsoft.AspNetCore.Authentication.Twitter](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Twitter))
  * WsFederation authentication ([Microsoft.AspNetCore.Authentication.WsFederation](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.WsFederation))

* Formatting and content negotiation support for `System.Net.HttpClient`: The [Microsoft.AspNet.WebApi.Client](https://www.nuget.org/packages/Microsoft.AspNet.WebApi.Client/) NuGet package provides useful extensibility to `System.Net.HttpClient` with APIs such as `ReadAsAsync` and `PostJsonAsync`. However, this package depends on `Newtonsoft.Json`, not `System.Text.Json`. That means, for example, that serialization property names specified by `JsonPropertyNameAttribute` (`System.Text.Json`) are ignored. There's a newer NuGet package that contains similar extension methods but uses `System.Text.Json`: [System.Net.Http.Json](https://www.nuget.org/packages/System.Net.Http.Json).

* Razor runtime compilation: Support for runtime compilation of Razor views and pages is now part of [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation).

* MVC `Newtonsoft.Json` (Json.NET) support: Support for using MVC with `Newtonsoft.Json` is now part of [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson).

## Startup changes

The following image shows the deleted and changed lines in an ASP.NET Core 2.2 Razor Pages Web app:

In the preceding image, deleted code is shown in red. The deleted code doesn't show cookie options code, which was deleted prior to comparing the files.

The following image shows the added and changed lines in an ASP.NET Core 3.0 Razor Pages Web app:

In the preceding image, added code is shown in green. For information on the following changes:

* `services.AddMvc` to `services.AddRazorPages`, see [MVC service registration](#mvc-service-registration) in this document.
* `CompatibilityVersion`, see [Compatibility version for ASP.NET Core MVC](../mvc/compatibility-version?view=aspnetcore-10.0).
* `IHostingEnvironment` to `IWebHostEnvironment`, see [this GitHub announcement](https://github.com/dotnet/AspNetCore/issues/7749).
* `app.UseAuthorization` was added to the templates to show the order authorization middleware must be added. If the app doesn't use authorization, you can safely remove the call to `app.UseAuthorization`.
* `app.UseEndpoints`, see [Razor Pages](#razor-pages) or [Migrate Startup.Configure](#migrate-startupconfigure) in this document.

### Analyzer support

Projects that target `Microsoft.NET.Sdk.Web` implicitly reference analyzers previously shipped as part of the [Microsoft.AspNetCore.Mvc.Analyzers](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Analyzers/) package. No additional references are required to enable these.

If your app uses [API analyzers](../web-api/advanced/analyzers?view=aspnetcore-10.0) previously shipped using the [Microsoft.AspNetCore.Mvc.Api.Analyzers](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Api.Analyzers/) package, edit your project file to reference the analyzers shipped as part of the .NET Core Web SDK:

```
<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>netcoreapp3.0</TargetFramework>
        <IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
    </PropertyGroup>

    ...
</Project>
```

### Razor class library

Razor class library projects that provide UI components for MVC must set the `AddRazorSupportForMvc` property in the project file:

```
<PropertyGroup>
  <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup>
```

### In-process hosting model

Projects default to the [in-process hosting model](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0#in-process-hosting-model) in ASP.NET Core 3.0 or later. You may optionally remove the `<AspNetCoreHostingModel>` property in the project file if its value is `InProcess`.

## Kestrel

### Configuration

Migrate Kestrel configuration to the [web host builder](#hostb) provided by `ConfigureWebHostDefaults` (`Program.cs`):

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(serverOptions =>
            {
                // Set properties and call methods on options
            })
            .UseStartup<Startup>();
        });
```

If the app creates the host manually with `ConfigureWebHost` instead of `ConfigureWebHostDefaults`, call `UseKestrel` on the web host builder:

```
public static void Main(string[] args)
{
    var host = new HostBuilder()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder.UseKestrel(serverOptions =>
            {
                // Set properties and call methods on options
            })
            .UseIISIntegration()
            .UseStartup<Startup>();
        })
        .Build();

    host.Run();
}
```

### Connection middleware replaces Connection Adapters

Connection Adapters (`Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal.IConnectionAdapter`) have been removed from Kestrel. Replace Connection Adapters with connection middleware. Connection middleware is similar to HTTP middleware in the ASP.NET Core pipeline but for lower-level connections. HTTPS and connection logging:

* Have been moved from Connection Adapters to connection middleware.
* These extension methods work as in previous versions of ASP.NET Core.

For more information, see [the TlsFilterConnectionHandler example in the ListenOptions.Protocols section of the Kestrel article](../fundamentals/servers/kestrel?view=aspnetcore-10.0#listenoptionsprotocols).

### Transport abstractions moved and made public

The Kestrel transport layer has been exposed as a public interface in `Connections.Abstractions`. As part of these updates:

* `Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions` and associated types have been removed.
* [NoDelay](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelserveroptions.nodelay#microsoft-aspnetcore-server-kestrel-kestrelserveroptions-nodelay) was moved from [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions) to the transport options.
* `Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.SchedulingMode` was removed from [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelserveroptions).

For more information, see the following GitHub resources:

* [Client/server networking abstractions (dotnet/AspNetCore #10308)](https://github.com/dotnet/AspNetCore/issues/10308)
* [Implement new bedrock listener abstraction and re-plat Kestrel on top (dotnet/AspNetCore #10321)](https://github.com/dotnet/AspNetCore/pull/10321)

### Kestrel Request trailer headers

For apps that target earlier versions of ASP.NET Core:

* Kestrel adds HTTP/1.1 chunked trailer headers into the request headers collection.
* Trailers are available after the request body is read to the end.

This causes some concerns about ambiguity between headers and trailers, so the trailers have been moved to a new collection (`RequestTrailerExtensions`) in 3.0.

HTTP/2 request trailers are:

* Not available in ASP.NET Core 2.2.
* Available in 3.0 as `RequestTrailerExtensions`.

New request extension methods are present to access these trailers. As with HTTP/1.1, trailers are available after the request body is read to the end.

For the 3.0 release, the following `RequestTrailerExtensions` methods are available:

* `GetDeclaredTrailers`: Gets the request `Trailer` header that lists which trailers to expect after the body.
* `SupportsTrailers`: Indicates if the request supports receiving trailer headers.
* `CheckTrailersAvailable`: Checks if the request supports trailers and if they're available to be read. This check doesn't assume that there are trailers to read. There might be no trailers to read even if `true` is returned by this method.
* `GetTrailer`: Gets the requested trailing header from the response. Check `SupportsTrailers` before calling `GetTrailer`, or a [NotSupportedException](/en-us/dotnet/api/system.notsupportedexception) may occur if the request doesn't support trailing headers.

For more information, see [Put request trailers in a separate collection (dotnet/AspNetCore #10410)](https://github.com/dotnet/AspNetCore/pull/10410).

### AllowSynchronousIO disabled

`AllowSynchronousIO` enables or disables synchronous I/O APIs, such as `HttpRequest.Body.Read`, `HttpResponse.Body.Write`, and `Stream.Flush`. These APIs are a source of thread starvation leading to app crashes. In 3.0, `AllowSynchronousIO` is disabled by default. For more information, see [the Synchronous I/O section in the Kestrel article](../fundamentals/servers/kestrel?view=aspnetcore-10.0#synchronous-io).

If synchronous I/O is needed, it can be enabled by configuring the `AllowSynchronousIO` option on the server being used (when calling `ConfigureKestrel`, for example, if using Kestrel). Note that servers (Kestrel, HttpSys, TestServer, etc.) all have their own `AllowSynchronousIO` option that won't affect other servers. Synchronous I/O can be enabled for all servers on a per-request basis using the `IHttpBodyControlFeature.AllowSynchronousIO` option:

```
var syncIOFeature = HttpContext.Features.Get<IHttpBodyControlFeature>();

if (syncIOFeature != null)
{
    syncIOFeature.AllowSynchronousIO = true;
}
```

If you have trouble with [TextWriter](/en-us/dotnet/api/system.io.textwriter) implementations or other streams that call synchronous APIs in [Dispose](/en-us/dotnet/standard/garbage-collection/implementing-dispose), call the new [DisposeAsync](/en-us/dotnet/api/system.io.stream.disposeasync) API instead.

For more information, see [\[Announcement\] AllowSynchronousIO disabled in all servers (dotnet/AspNetCore #7644)](https://github.com/dotnet/AspNetCore/issues/7644).

### Output formatter buffering

[Newtonsoft.Json](https://www.newtonsoft.com/json), [XmlSerializer](/en-us/dotnet/api/system.xml.serialization.xmlserializer), and [DataContractSerializer](/en-us/dotnet/api/system.runtime.serialization.datacontractserializer) based output formatters only support synchronous serialization. To allow these formatters to work with the [AllowSynchronousIO](https://github.com/dotnet/aspnetcore/issues/7644) restrictions of the server, MVC buffers the output of these formatters before writing to disk. As a result of buffering, MVC will include the Content-Length header when responding using these formatters.

[System.Text.Json](/en-us/dotnet/api/system.text.json) supports asynchronous serialization and consequently the `System.Text.Json` based formatter does not buffer. Consider using this formatter for improved performance.

To disable buffering, applications can configure [SuppressOutputFormatterBuffering](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.suppressoutputformatterbuffering#microsoft-aspnetcore-mvc-mvcoptions-suppressoutputformatterbuffering) in their startup:

```
services.AddControllers(options => options.SuppressOutputFormatterBuffering = true)
```

Note that this may result in the application throwing a runtime exception if `AllowSynchronousIO` isn't also configured.

### Microsoft.AspNetCore.Server.Kestrel.Https assembly removed

In ASP.NET Core 2.1, the contents of *Microsoft.AspNetCore.Server.Kestrel.Https.dll* were moved to *Microsoft.AspNetCore.Server.Kestrel.Core.dll*. This was a non-breaking update using `TypeForwardedTo` attributes. For 3.0, the empty *Microsoft.AspNetCore.Server.Kestrel.Https.dll* assembly and the NuGet package have been removed.

Libraries referencing [Microsoft.AspNetCore.Server.Kestrel.Https](https://www.nuget.org/packages/Microsoft.AspNetCore.Server.Kestrel.Https) should update ASP.NET Core dependencies to 2.1 or later.

Apps and libraries targeting ASP.NET Core 2.1 or later should remove any direct references to the [Microsoft.AspNetCore.Server.Kestrel.Https](https://www.nuget.org/packages/Microsoft.AspNetCore.Server.Kestrel.Https) package.

[]()

## Newtonsoft.Json (Json.NET) support

As part of the work to [improve the ASP.NET Core shared framework](https://blogs.msdn.microsoft.com/webdev/2018/10/29/a-first-look-at-changes-coming-in-asp-net-core-3-0/), [Newtonsoft.Json (Json.NET)](https://www.newtonsoft.com/json/help/html/Introduction.htm) has been removed from the ASP.NET Core shared framework.

The default JSON serializer for ASP.NET Core is now [System.Text.Json](/en-us/dotnet/api/system.text.json), which is new in .NET Core 3.0. Consider using `System.Text.Json` when possible. It's high-performance and doesn't require an additional library dependency. However, since `System.Text.Json` is new, it might currently be missing features that your app needs. For more information, see [How to migrate from Newtonsoft.Json to System.Text.Json](/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to).

### Use Newtonsoft.Json in an ASP.NET Core 3.0 SignalR project

* Install the [Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson) NuGet package.

* On the client, chain an `AddNewtonsoftJsonProtocol` method call to the `HubConnectionBuilder` instance:

  ```
  new HubConnectionBuilder()
      .WithUrl("/chathub")
      .AddNewtonsoftJsonProtocol(...)
      .Build();
  ```

* On the server, chain an `AddNewtonsoftJsonProtocol` method call to the `AddSignalR` method call in `Startup.ConfigureServices`:

  ```
  services.AddSignalR()
      .AddNewtonsoftJsonProtocol(...);
  ```

### Use Newtonsoft.Json in an ASP.NET Core 3.0 MVC project

* Install the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson) package.

* Update `Startup.ConfigureServices` to call `AddNewtonsoftJson`.

  ```
  services.AddMvc()
      .AddNewtonsoftJson();
  ```

  `AddNewtonsoftJson` is compatible with the new MVC service registration methods:

  * `AddRazorPages`
  * `AddControllersWithViews`
  * `AddControllers`

  ```
  services.AddControllers()
      .AddNewtonsoftJson();
  ```

  `Newtonsoft.Json` settings can be set in the call to `AddNewtonsoftJson`:

  ```
  services.AddMvc()
      .AddNewtonsoftJson(options =>
             options.SerializerSettings.ContractResolver =
                new CamelCasePropertyNamesContractResolver());
  ```

  **Note:** If the `AddNewtonsoftJson` method isn't available, make sure that you installed the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson) package. A common error is to install the [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json/) package instead of the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson) package.

For more information, see [Add Newtonsoft.Json-based JSON format support](../web-api/advanced/formatting?view=aspnetcore-10.0#add-newtonsoftjson-based-json-format-support).

## MVC service registration

ASP.NET Core 3.0 adds new options for registering MVC scenarios inside `Startup.ConfigureServices`.

Three new top-level extension methods related to MVC scenarios on `IServiceCollection` are available. Templates use these new methods instead of `AddMvc`. However, `AddMvc` continues to behave as it has in previous releases.

The following example adds support for controllers and API-related features, but not views or pages. The API template uses this code:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
}
```

The following example adds support for controllers, API-related features, and views, but not pages. The Web Application (MVC) template uses this code:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
}
```

The following example adds support for Razor Pages and minimal controller support. The Web Application template uses this code:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
}
```

The new methods can also be combined. The following example is equivalent to calling `AddMvc` in ASP.NET Core 2.2:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
}
```

## Routing startup code

If an app calls `UseMvc` or `UseSignalR`, migrate the app to [Endpoint Routing](../fundamentals/routing?view=aspnetcore-10.0) if possible. To improve Endpoint Routing compatibility with previous versions of MVC, we've reverted some of the changes in URL generation introduced in ASP.NET Core 2.2. If you experienced problems using Endpoint Routing in 2.2, expect improvements in ASP.NET Core 3.0 with the following exceptions:

* If the app implements `IRouter` or inherits from `Route`, use [DynamicRouteValuesTransformer](https://github.com/dotnet/AspNetCore.Docs/issues/12997) as the replacement.

* If the app directly accesses `RouteData.Routers` inside MVC to parse URLs, you can replace this with use of [LinkParser.ParsePathByEndpointName](/en-us/dotnet/api/microsoft.aspnetcore.routing.linkparserendpointnameaddressextensions.parsepathbyendpointname).

  * Define the route with a route name.
  * Use `LinkParser.ParsePathByEndpointName` and pass in the desired route name.

Endpoint Routing supports the same route pattern syntax and route pattern authoring features as `IRouter`. Endpoint Routing supports `IRouteConstraint`. Endpoint routing supports `[Route]`, `[HttpGet]`, and the other MVC routing attributes.

For most applications, only `Startup` requires changes.

### Migrate Startup.Configure

General advice:

* Add `UseRouting`.

* If the app calls `UseStaticFiles`, place `UseStaticFiles` **before** `UseRouting`.

* If the app uses authentication/authorization features such as `AuthorizePage` or `[Authorize]`, place the call to `UseAuthentication` and `UseAuthorization`: **after**, `UseRouting` and `UseCors`, but before `UseEndpoints`:

  ```
  public void Configure(IApplicationBuilder app)
  {
    ...

    app.UseStaticFiles();

    app.UseRouting();
    app.UseCors();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints => {
       endpoints.MapControllers();
    });
  ```

* Replace `UseMvc` or `UseSignalR` with `UseEndpoints`.

* If the app uses [CORS](../security/cors?view=aspnetcore-10.0) scenarios, such as `[EnableCors]`, place the call to `UseCors` before any other middleware that use CORS (for example, place `UseCors` before `UseAuthentication`, `UseAuthorization`, and `UseEndpoints`).

* Replace `IHostingEnvironment` with `IWebHostEnvironment` and add a `using` statement for the [Microsoft.AspNetCore.Hosting](/en-us/dotnet/api/microsoft.aspnetcore.hosting) namespace.

* Replace `IApplicationLifetime` with [IHostApplicationLifetime](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime) ([Microsoft.Extensions.Hosting](/en-us/dotnet/api/microsoft.extensions.hosting) namespace).

* Replace `EnvironmentName` with [Environments](/en-us/dotnet/api/microsoft.extensions.hosting.environments) ([Microsoft.Extensions.Hosting](/en-us/dotnet/api/microsoft.extensions.hosting) namespace).

The following code is an example of `Startup.Configure` in a typical ASP.NET Core 2.2 app:

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();

    app.UseAuthentication();

    app.UseSignalR(hubs =>
    {
        hubs.MapHub<ChatHub>("/chat");
    });

    app.UseMvc(routes =>
    {
        routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}
```

After updating the previous `Startup.Configure` code:

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();

    app.UseRouting();

    app.UseCors();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chat");
        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}
```

Warning

For most apps, calls to `UseAuthentication`, `UseAuthorization`, and `UseCors` must appear between the calls to `UseRouting` and `UseEndpoints` to be effective.

### Health Checks

Health Checks use endpoint routing with the Generic Host. In `Startup.Configure`, call `MapHealthChecks` on the endpoint builder with the endpoint URL or relative path:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
});
```

Health Checks endpoints can:

* Specify one or more permitted hosts/ports.
* Require authorization.
* Require CORS.

For more information, see [Health checks in ASP.NET Core](../host-and-deploy/health-checks?view=aspnetcore-10.0).

### Security middleware guidance

Support for authorization and CORS is unified around the [middleware](../fundamentals/middleware/?view=aspnetcore-10.0) approach. This allows use of the same middleware and functionality across these scenarios. An updated authorization middleware is provided in this release, and CORS middleware is enhanced so that it can understand the attributes used by MVC controllers.

#### CORS

Previously, CORS could be difficult to configure. Middleware was provided for use in some use cases, but MVC filters were intended to be used **without** the middleware in other use cases. With ASP.NET Core 3.0, we recommend that all apps that require CORS use the CORS middleware in tandem with Endpoint Routing. `UseCors` can be provided with a default policy, and `[EnableCors]` and `[DisableCors]` attributes can be used to override the default policy where required.

In the following example:

* CORS is enabled for all endpoints with the `default` named policy.
* The `MyController` class disables CORS with the `[DisableCors]` attribute.

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseCors("default");

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

[DisableCors]
public class MyController : ControllerBase
{
    ...
}
```

#### Authorization

In earlier versions of ASP.NET Core, authorization support was provided via the `[Authorize]` attribute. Authorization middleware wasn't available. In ASP.NET Core 3.0, authorization middleware is required. We recommend placing the ASP.NET Core authorization middleware (`UseAuthorization`) immediately after `UseAuthentication`. The authorization middleware can also be configured with a default policy, which can be overridden.

In ASP.NET Core 3.0 or later, `UseAuthorization` is called in `Startup.Configure`, and the following `HomeController` requires a signed in user:

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

public class HomeController : Controller
{
    [Authorize]
    public IActionResult BuyWidgets()
    {
        ...
    }
}
```

When using endpoint routing, we recommend against configuring [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) and instead relying on the Authorization middleware. If the app uses an `AuthorizeFilter` as a global filter in MVC, we recommend refactoring the code to provide a policy in the call to `AddAuthorization`.

The `DefaultPolicy` is initially configured to require authentication, so no additional configuration is required. In the following example, MVC endpoints are marked as `RequireAuthorization` so that all requests must be authorized based on the `DefaultPolicy`. However, the `HomeController` allows access without the user signing into the app due to `[AllowAnonymous]`:

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute().RequireAuthorization();
    });
}

[AllowAnonymous]
public class HomeController : Controller
{
    ...
}
```

### Authorization for specific endpoints

Authorization can also be configured for specific classes of endpoints. The following code is an example of converting an MVC app that configured a global `AuthorizeFilter` to an app with a specific policy requiring authorization:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    static readonly string _RequireAuthenticatedUserPolicy = 
                            "RequireAuthenticatedUserPolicy";
    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
        services.AddDefaultIdentity<IdentityUser>(
                 options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();

        // Pre 3.0:
        // services.AddMvc(options => options.Filters.Add(new AuthorizeFilter(...));

        services.AddControllersWithViews();
        services.AddRazorPages();
        services.AddAuthorization(o => o.AddPolicy(_RequireAuthenticatedUserPolicy,
                        builder => builder.RequireAuthenticatedUser()));

    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute()
                .RequireAuthorization(_RequireAuthenticatedUserPolicy);
            endpoints.MapRazorPages();
        });
    }
}
```

Policies can also be customized. The `DefaultPolicy` is configured to require authentication:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
        services.AddDefaultIdentity<IdentityUser>(
                 options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();

        services.AddControllersWithViews();
        services.AddRazorPages();
        services.AddAuthorization(options =>
        {
            options.DefaultPolicy = new AuthorizationPolicyBuilder()
              .RequireAuthenticatedUser()
              .Build();
        });

    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute().RequireAuthorization();
            endpoints.MapRazorPages();
        });
    }
}
```

```
[AllowAnonymous]
public class HomeController : Controller
{
```

Alternatively, endpoint requests processed by authorization middleware can require authorization without `[Authorize]` or `RequireAuthorization()` by configuring a fallback policy. The default policy is selected by `[Authorize]` or `RequireAuthorization()` when no policy name is specified. The fallback policy is selected when no policy is produced from authorization metadata. The fallback policy is `null` by default, so authorization isn't required in that case. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

The following example is the same as the preceding default policy example but uses a fallback policy to require authentication for endpoint requests except when `[AllowAnonymous]` is specified:

```
public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
          .RequireAuthenticatedUser()
          .Build();
    });
}

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

[AllowAnonymous]
public class HomeController : Controller
{
    ...
}
```

Authorization by middleware works without the framework having any specific knowledge of authorization. For instance, [health checks](../host-and-deploy/health-checks?view=aspnetcore-10.0) has no specific knowledge of authorization, but health checks can have a configurable authorization policy applied by the middleware.

Additionally, each endpoint can customize its authorization requirements. In the following example, `UseAuthorization` processes authorization with the `DefaultPolicy`, but the `/healthz` health check endpoint requires an `admin` user:

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints
            .MapHealthChecks("/healthz")
            .RequireAuthorization(new AuthorizeAttribute(){ Roles = "admin", });
    });
}
```

Protection is implemented for some scenarios. Endpoints middleware throws an exception if an authorization or CORS policy is skipped due to missing middleware. Analyzer support to provide additional feedback about misconfiguration is in progress.

#### Custom authorization handlers

If the app uses custom [authorization handlers](../security/authorization/policies?view=aspnetcore-10.0#authorization-handlers), endpoint routing passes a different resource type to handlers than MVC. Handlers that expect the authorization handler context resource to be of type [AuthorizationFilterContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.authorizationfiltercontext) (the resource type [provided by MVC filters](../mvc/security/authorization/policies?view=aspnetcore-10.0#access-mvc-request-context-in-handlers)) will need to be updated to handle resources of type [RouteEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint) (the resource type given to authorization handlers by endpoint routing).

MVC still uses `AuthorizationFilterContext` resources, so if the app uses MVC authorization filters along with endpoint routing authorization, it may be necessary to handle both types of resources.

### SignalR

Mapping of SignalR hubs now takes place inside `UseEndpoints`.

Map each hub with `MapHub`. As in previous versions, each hub is explicitly listed.

In the following example, support for the `ChatHub` SignalR hub is added:

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>();
    });
}
```

There is a new option for controlling message size limits from clients. For example, in `Startup.ConfigureServices`:

```
services.AddSignalR(hubOptions =>
{
    hubOptions.MaximumReceiveMessageSize = 32768;
});
```

In ASP.NET Core 2.2, you could set the `TransportMaxBufferSize` and that would effectively control the maximum message size. In ASP.NET Core 3.0, that option now only controls the maximum size before backpressure is observed.

#### SignalR assemblies in shared framework

ASP.NET Core SignalR server-side assemblies are now installed with the .NET Core SDK. For more information, see [Remove obsolete package references](#remove-obsolete-package-references) in this document.

### MVC controllers

Mapping of controllers now takes place inside `UseEndpoints`.

Add `MapControllers` if the app uses attribute routing. Since routing includes support for many frameworks in ASP.NET Core 3.0 or later, adding attribute-routed controllers is opt-in.

Replace the following:

* `MapRoute` with `MapControllerRoute`
* `MapAreaRoute` with `MapAreaControllerRoute`

Since routing now includes support for more than just MVC, the terminology has changed to make these methods clearly state what they do. Conventional routes such as `MapControllerRoute`/`MapAreaControllerRoute`/`MapDefaultControllerRoute` are applied in the order that they're added. Place more specific routes (such as routes for an area) first.

In the following example:

* `MapControllers` adds support for attribute-routed controllers.
* `MapAreaControllerRoute` adds a conventional route for controllers in an area.
* `MapControllerRoute` adds a conventional route for controllers.

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapAreaControllerRoute(
            "admin",
            "admin",
            "Admin/{controller=Home}/{action=Index}/{id?}");
        endpoints.MapControllerRoute(
            "default", "{controller=Home}/{action=Index}/{id?}");
    });
}
```

#### Async suffix removal from controller action names

In ASP.NET Core 3.0, ASP.NET Core MVC removes the `Async` suffix from controller action names. Both routing and link generation are impacted by this new default. For example:

```
public class ProductsController : Controller
{
    public async Task<IActionResult> ListAsync()
    {
        var model = await _dbContext.Products.ToListAsync();
        return View(model);
    }
}
```

Prior to ASP.NET Core 3.0:

* The preceding action could be accessed at the *Products/ListAsync* route.

* Link generation required specifying the `Async` suffix. For example:

  ```
  <a asp-controller="Products" asp-action="ListAsync">List</a>
  ```

In ASP.NET Core 3.0:

* The preceding action can be accessed at the *Products/List* route.

* Link generation doesn't require specifying the `Async` suffix. For example:

  ```
  <a asp-controller="Products" asp-action="List">List</a>
  ```

This change doesn't affect names specified using the [`[ActionName]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionnameattribute) attribute. The default behavior can be disabled with the following code in `Startup.ConfigureServices`:

```
services.AddMvc(options =>
    options.SuppressAsyncSuffixInActionNames = false);
```

#### Changes to link generation

There are some differences in link generation (using `Url.Link` and similar APIs, for example). These include:

* By default, when using endpoint routing, casing of route parameters in generated URIs is not necessarily preserved. This behavior can be controlled with the `IOutboundParameterTransformer` interface.
* Generating a URI for an invalid route (a controller/action or page that doesn't exist) will produce an empty string under endpoint routing instead of producing an invalid URI.
* Ambient values (route parameters from the current context) are not automatically used in link generation with endpoint routing. Previously, when generating a link to another action (or page), unspecified route values would be inferred from the *current* routes ambient values. When using endpoint routing, all route parameters must be specified explicitly during link generation.

### Razor Pages

Mapping Razor Pages now takes place inside `UseEndpoints`.

Add `MapRazorPages` if the app uses Razor Pages. Since Endpoint Routing includes support for many frameworks, adding Razor Pages is now opt-in.

In the following `Startup.Configure` method, `MapRazorPages` adds support for Razor Pages:

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

### Use MVC without Endpoint Routing

Using MVC via `UseMvc` or `UseMvcWithDefaultRoute` in ASP.NET Core 3.0 requires an explicit opt-in inside `Startup.ConfigureServices`. This is required because MVC must know whether it can rely on the authorization and CORS middleware during initialization. An analyzer is provided that warns if the app attempts to use an unsupported configuration.

If the app requires legacy `IRouter` support, disable `EnableEndpointRouting` using any of the following approaches in `Startup.ConfigureServices`:

```
services.AddMvc(options => options.EnableEndpointRouting = false);
```

```
services.AddControllers(options => options.EnableEndpointRouting = false);
```

```
services.AddControllersWithViews(options => options.EnableEndpointRouting = false);
```

```
services.AddRazorPages().AddMvcOptions(options => options.EnableEndpointRouting = false);
```

### Health checks

Health checks can be used as a *router-ware* with Endpoint Routing.

Add `MapHealthChecks` to use health checks with Endpoint Routing. The `MapHealthChecks` method accepts arguments similar to `UseHealthChecks`. The advantage of using `MapHealthChecks` over `UseHealthChecks` is the ability to apply authorization and to have greater fine-grained control over the matching policy.

In the following example, `MapHealthChecks` is called for a health check endpoint at `/healthz`:

```
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHealthChecks("/healthz", new HealthCheckOptions() { });
    });
}
```

[]()

## HostBuilder replaces WebHostBuilder

The ASP.NET Core 3.0 templates use [Generic Host](../fundamentals/host/generic-host?view=aspnetcore-10.0). Previous versions used [Web Host](../fundamentals/host/web-host?view=aspnetcore-10.0). The following code shows the ASP.NET Core 3.0 template generated `Program` class:

```
// requires using Microsoft.AspNetCore.Hosting;
// requires using Microsoft.Extensions.Hosting;

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

The following code shows the ASP.NET Core 2.2 template-generated `Program` class:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}
```

[IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) remains in 3.0 and is the type of the `webBuilder` seen in the preceding code sample. [WebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder) will be deprecated in a future release and replaced by `HostBuilder`.

The most significant change from `WebHostBuilder` to `HostBuilder` is in [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0). When using `HostBuilder`, you can only inject the following into `Startup`'s constructor:

* [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration)
* [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment)
* [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment)

The `HostBuilder` DI constraints:

* Enable the DI container to be built only one time.
* Avoids the resulting object lifetime issues like resolving multiple instances of singletons.

For more information, see [Avoiding Startup service injection in ASP.NET Core 3](https://andrewlock.net/avoiding-startup-service-injection-in-asp-net-core-3/).

## AddAuthorization moved to a different assembly

The ASP.NET Core 2.2 and lower `AddAuthorization` methods in *Microsoft.AspNetCore.Authorization.dll*:

* Have been renamed `AddAuthorizationCore`.
* Have been moved to *Microsoft.AspNetCore.Authorization.Policy.dll*.

Apps that are using both *Microsoft.AspNetCore.Authorization.dll* and *Microsoft.AspNetCore.Authorization.Policy.dll* aren't impacted.

Apps that are not using *Microsoft.AspNetCore.Authorization.Policy.dll* should do one of the following:

* Add a reference to *Microsoft.AspNetCore.Authorization.Policy.dll*. This approach works for most apps and is all that is required.
* Switch to using `AddAuthorizationCore`

For more information, see [Breaking change in `AddAuthorization(o =>`) overload lives in a different assembly #386](https://github.com/aspnet/Announcements/issues/386).

## Identity UI

Identity UI updates for ASP.NET Core 3.0:

* Add a package reference to [Microsoft.AspNetCore.Identity.UI](https://nuget.org/packages/Microsoft.AspNetCore.Identity.UI).
* Apps that don't use Razor Pages must call `MapRazorPages`. See [Razor Pages](#razor-pages) in this document.
* Bootstrap 4 is the default UI framework. Set an `IdentityUIFrameworkVersion` project property to change the default. For more information, see [this GitHub announcement](https://github.com/aspnet/Announcements/issues/380).

## SignalR

The SignalR JavaScript client has changed from `@aspnet/signalr` to `@microsoft/signalr`. To react to this change, change the references in `package.json` files, `require` statements, and ECMAScript `import` statements.

### System.Text.Json is the default protocol

`System.Text.Json` is now the default Hub protocol used by both the client and server.

In `Startup.ConfigureServices`, call `AddJsonProtocol` to set serializer options.

**Server:**

```
services.AddSignalR(...)
        .AddJsonProtocol(options =>
        {
            options.PayloadSerializerOptions.WriteIndented = false;
        })
```

**Client:**

```
new HubConnectionBuilder()
    .WithUrl("/chathub")
    .AddJsonProtocol(options =>
    {
        options.PayloadSerializerOptions.WriteIndented = false;
    })
    .Build();
```

### Switch to Newtonsoft.Json

If you're using [features of Newtonsoft.Json that aren't supported in System.Text.Json](/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to), you can switch back to `Newtonsoft.Json`. See [Use Newtonsoft.Json in an ASP.NET Core 3.0 SignalR project](#use-newtonsoftjson-in-an-aspnet-core-30-signalr-project) earlier in this article.

## Redis distributed caches

The [Microsoft.Extensions.Caching.Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Redis) package isn't available for ASP.NET Core 3.0 or later apps. Replace the package reference with [Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis). For more information, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0).

## Opt in to runtime compilation

Prior to ASP.NET Core 3.0, runtime compilation of views was an implicit feature of the framework. Runtime compilation supplements build-time compilation of views. It allows the framework to compile Razor views and pages (`.cshtml` files) when the files are modified, without having to rebuild the entire app. This feature supports the scenario of making a quick edit in the IDE and refreshing the browser to view the changes.

In ASP.NET Core 3.0, runtime compilation is an opt-in scenario. Build-time compilation is the only mechanism for view compilation that's enabled by default. The runtime relies on Visual Studio or [dotnet-watch](../tutorials/dotnet-watch?view=aspnetcore-10.0) in Visual Studio Code to rebuild the project when it detects changes to `.cshtml` files. In Visual Studio, changes to `.cs`, `.cshtml`, or `.razor` files in the project being run (`Ctrl+F5`), but not debugged (`F5`), trigger recompilation of the project.

To enable runtime compilation in your ASP.NET Core 3.0 project:

1. Install the [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation) NuGet package.

2. Update `Startup.ConfigureServices` to call `AddRazorRuntimeCompilation`:

   For ASP.NET Core MVC, use the following code:

   ```
   services.AddControllersWithViews()
       .AddRazorRuntimeCompilation(...);
   ```

   For ASP.NET Core Razor Pages, use the following code:

   ```
   services.AddRazorPages()
       .AddRazorRuntimeCompilation(...);
   ```

The sample at <https://github.com/aspnet/samples/tree/main/samples/aspnetcore/mvc/runtimecompilation> shows an example of enabling runtime compilation conditionally in `Development` environments.

For more information on Razor file compilation, see [Razor file compilation in ASP.NET Core](../mvc/views/view-compilation?view=aspnetcore-10.0).

## Migrate libraries via multi-targeting

Libraries often need to support multiple versions of ASP.NET Core. Most libraries that were compiled against previous versions of ASP.NET Core should continue working without issues. The following conditions require the app to be cross-compiled:

* The library relies on a feature that has a binary [breaking change](#breaking-changes).
* The library wants to take advantage of new features in ASP.NET Core 3.0.

For example:

```
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netcoreapp3.0;netstandard2.0</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
    <PackageReference Include="Microsoft.AspNetCore" Version="2.1.0" />
  </ItemGroup>
</Project>
```

Use `#ifdefs` to enable ASP.NET Core 3.0-specific APIs:

```
var webRootFileProvider =
#if NETCOREAPP3_0
    GetRequiredService<IWebHostEnvironment>().WebRootFileProvider;
#elif NETSTANDARD2_0
    GetRequiredService<IHostingEnvironment>().WebRootFileProvider;
#else
#error unknown target framework
#endif
```

For more information on using ASP.NET Core APIs in a class library, see [Use ASP.NET Core APIs in a class library](../fundamentals/target-aspnetcore?view=aspnetcore-10.0).

## Miscellaneous changes

The validation system in .NET Core 3.0 or later treats non-nullable parameters or bound properties as if they had a `[Required]` attribute. For more information, see [\[Required\] attribute](../mvc/models/validation?view=aspnetcore-3.1#required-attribute).

### Publish

Delete the *bin* and *obj* folders in the project directory.

## TestServer

For apps that use [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver) directly with the [Generic Host](../fundamentals/host/web-host?view=aspnetcore-10.0), create the `TestServer` on an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) in [ConfigureWebHost](/en-us/dotnet/api/microsoft.extensions.hosting.generichostwebhostbuilderextensions.configurewebhost):

```
[Fact]
public async Task GenericCreateAndStartHost_GetTestServer()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .Configure(app => { });
        })
    .StartAsync();

    var response = await host.GetTestServer().CreateClient().GetAsync("/");

    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
```

[]()

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

For more information, see the following resources:

* [Complete list of breaking changes in the ASP.NET Core 3.0 release](https://github.com/aspnet/Announcements/issues?page=1\&q=is%3Aissue+is%3Aopen+label%3A%22Breaking+change%22+label%3A3.0.0)
* [Breaking API changes in Antiforgery, CORS, Diagnostics, MVC, and Routing](https://github.com/aspnet/Announcements/issues/387). This list includes breaking changes for compatibility switches.

## Endpoint routing with catch-all parameter

```
public static void Main(string[] args)
{
   AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior", 
                         true);
   CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
```

## .NET Core 3.0 on Azure App Service

The rollout of .NET Core to Azure App Service is finished. .NET Core 3.0 is available in all Azure App Service datacenters.

## ASP.NET Core Module (ANCM)

If the [ASP.NET Core Module (ANCM)](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) wasn't a selected component when Visual Studio was installed or if a prior version of the ANCM was installed on the system, download the latest [.NET Core Hosting Bundle Installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer) and run the installer. For more information, see [Hosting Bundle](../host-and-deploy/iis/hosting-bundle?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/memory-management
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel/memory-management.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Memory management in Kestrel

By [Tom Dykstra](https://github.com/tdykstra)

This article provides guidance for managing memory in Kestrel, including automatic eviction from memory pools and using memory pool metrics.

## Automatic eviction from memory pool

The memory pools used by Kestrel, IIS, and HTTP.sys automatically evict memory blocks when the application is idle or under low load. The feature runs automatically and doesn't need to be enabled or configured manually.

This automatic eviction feature reduces overall memory usage and helps applications stay responsive under varying workloads. In versions of .NET earlier than 10, memory allocated by the pool remained reserved even when not in use.

### Use memory pool metrics

The default memory pool used by the ASP.NET Core server implementations includes metrics, which can be used to monitor and analyze memory usage patterns. The metrics are under the name `"Microsoft.AspNetCore.MemoryPool"`.

For information about metrics and how to use them, see [ASP.NET Core metrics](../../../metrics/overview?view=aspnetcore-10.0).

## Manage memory pools

Besides using memory pools efficiently by evicting unneeded memory blocks, ASP.NET Core provides a built-in [IMemoryPoolFactory](https://source.dot.net/#Microsoft.AspNetCore.Connections.Abstractions/IMemoryPoolFactory.cs) interface and its default implementation, which are available through dependency injection.

The following code example shows a simple background service that uses the built-in memory pool factory implementation to create memory pools. These pools benefit from the automatic eviction feature:

```
public class MyBackgroundService : BackgroundService
{
    private readonly MemoryPool<byte> _memoryPool;

    public MyBackgroundService(IMemoryPoolFactory<byte> factory)
    {
        _memoryPool = factory.Create();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(20, stoppingToken);
                // do work that needs memory
                // consider checking _memoryPool.MaxBufferSize
                var rented = _memoryPool.Rent(100);
                rented.Dispose();
            }
            catch (OperationCanceledException)
            {
                return;
            }
        }
    }
}
```

To use a custom memory pool factory, make a class that implements `IMemoryPoolFactory` and register it with dependency injection, as the following example does. Memory pools created this way do not benefit from the automatic eviction feature unless you implement similar eviction logic in your custom factory:

```
services.AddSingleton<IMemoryPoolFactory<byte>,
CustomMemoryPoolFactory>();

public class CustomMemoryPoolFactory : IMemoryPoolFactory<byte>
{
    public MemoryPool<byte> Create()
    {
        // Return a custom MemoryPool implementation
        // or the default, as is shown here.
        return MemoryPool<byte>.Shared;
    }
}
```

When you're using a memory pool, be aware of the pool's [MaxBufferSize](/en-us/dotnet/api/system.buffers.memorypool-1.maxbuffersize#system-buffers-memorypool-1-maxbuffersize).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/filter
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/filter.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Filter methods for Razor Pages in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

Razor Page filters [IPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter) and [IAsyncPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter) allow Razor Pages to run code before and after a Razor Page handler is run. Razor Page filters are similar to [ASP.NET Core MVC action filters](../mvc/controllers/filters?view=aspnetcore-10.0#action-filters), except they can't be applied to individual page handler methods.

Razor Page filters:

* Run code after a handler method has been selected, but before model binding occurs.
* Run code before the handler method executes, after model binding is complete.
* Run code after the handler method executes.
* Can be implemented on a page or globally.
* Cannot be applied to specific page handler methods.
* Can have constructor dependencies populated by [Dependency Injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI). For more information, see [ServiceFilterAttribute](../mvc/controllers/filters?view=aspnetcore-10.0#servicefilterattribute) and [TypeFilterAttribute](../mvc/controllers/filters?view=aspnetcore-10.0#typefilterattribute).

While page constructors and middleware enable executing custom code before a handler method executes, only Razor Page filters enable access to [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.httpcontext#microsoft-aspnetcore-mvc-razorpages-pagemodel-httpcontext) and the page. Middleware has access to the `HttpContext`, but not to the "page context". Filters have a [FilterContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.filtercontext) derived parameter, which provides access to `HttpContext`. Here's a sample for a page filter: [Implement a filter attribute](#ifa) that adds a header to the response, something that can't be done with constructors or middleware. Access to the page context, which includes access to the instances of the page and it's model, are only available when executing filters, handlers, or the body of a Razor Page.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/filter/samples/3.1) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

Razor Page filters provide the following methods, which can be applied globally or at the page level:

* Synchronous methods:

  * [OnPageHandlerSelected](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter.onpagehandlerselected) : Called after a handler method has been selected, but before model binding occurs.
  * [OnPageHandlerExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter.onpagehandlerexecuting) : Called before the handler method executes, after model binding is complete.
  * [OnPageHandlerExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter.onpagehandlerexecuted) : Called after the handler method executes, before the action result.

* Asynchronous methods:

  * [OnPageHandlerSelectionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter.onpagehandlerselectionasync) : Called asynchronously after the handler method has been selected, but before model binding occurs.
  * [OnPageHandlerExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter.onpagehandlerexecutionasync) : Called asynchronously before the handler method is invoked, after model binding is complete.

Implement **either** the synchronous or the async version of a filter interface, **not** both. The framework checks first to see if the filter implements the async interface, and if so, it calls that. If not, it calls the synchronous interface's methods. If both interfaces are implemented, only the async methods are called. The same rule applies to overrides in pages, implement the synchronous or the async version of the override, not both.

## Implement Razor Page filters globally

The following code implements `IAsyncPageFilter`:

```
public class SampleAsyncPageFilter : IAsyncPageFilter
{
    private readonly IConfiguration _config;

    public SampleAsyncPageFilter(IConfiguration config)
    {
        _config = config;
    }

    public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
    {
        var key = _config["UserAgentID"];
        context.HttpContext.Request.Headers.TryGetValue("user-agent",
                                                        out StringValues value);
        ProcessUserAgent.Write(context.ActionDescriptor.DisplayName,
                               "SampleAsyncPageFilter.OnPageHandlerSelectionAsync",
                               value, key.ToString());

        return Task.CompletedTask;
    }

    public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,
                                                  PageHandlerExecutionDelegate next)
    {
        // Do post work.
        await next.Invoke();
    }
}
```

In the preceding code, `ProcessUserAgent.Write` is user supplied code that works with the user agent string.

The following code enables the `SampleAsyncPageFilter` in the `Startup` class:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.Filters.Add(new SampleAsyncPageFilter(Configuration));
        });
}
```

The following code calls [AddFolderApplicationModelConvention](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.pageconventioncollection.addfolderapplicationmodelconvention) to apply the `SampleAsyncPageFilter` to only pages in */Movies*:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages(options =>
    {
        options.Conventions.AddFolderApplicationModelConvention(
            "/Movies",
            model => model.Filters.Add(new SampleAsyncPageFilter(Configuration)));
    });
}
```

The following code implements the synchronous `IPageFilter`:

```
public class SamplePageFilter : IPageFilter
{
    private readonly IConfiguration _config;

    public SamplePageFilter(IConfiguration config)
    {
        _config = config;
    }

    public void OnPageHandlerSelected(PageHandlerSelectedContext context)
    {
        var key = _config["UserAgentID"];
        context.HttpContext.Request.Headers.TryGetValue("user-agent", out StringValues value);
        ProcessUserAgent.Write(context.ActionDescriptor.DisplayName,
                               "SamplePageFilter.OnPageHandlerSelected",
                               value, key.ToString());
    }

    public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
    {
        Debug.WriteLine("Global sync OnPageHandlerExecuting called.");
    }

    public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
    {
        Debug.WriteLine("Global sync OnPageHandlerExecuted called.");
    }
}
```

The following code enables the `SamplePageFilter`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.Filters.Add(new SamplePageFilter(Configuration));
        });
}
```

## Implement Razor Page filters by overriding filter methods

The following code overrides the asynchronous Razor Page filters:

```
public class IndexModel : PageModel
{
    private readonly IConfiguration _config;

    public IndexModel(IConfiguration config)
    {
        _config = config;
    }

    public override Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
    {
        Debug.WriteLine("/IndexModel OnPageHandlerSelectionAsync");
        return Task.CompletedTask;
    }

    public async override Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, 
                                                           PageHandlerExecutionDelegate next)
    {
        var key = _config["UserAgentID"];
        context.HttpContext.Request.Headers.TryGetValue("user-agent", out StringValues value);
        ProcessUserAgent.Write(context.ActionDescriptor.DisplayName,
                               "/IndexModel-OnPageHandlerExecutionAsync",
                                value, key.ToString());

        await next.Invoke();
    }
}
```

[]()

## Implement a filter attribute

The built-in attribute-based filter [OnResultExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter.onresultexecutionasync) filter can be subclassed. The following filter adds a header to the response:

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace PageFilter.Filters
{
    public class AddHeaderAttribute  : ResultFilterAttribute
    {
        private readonly string _name;
        private readonly string _value;

        public AddHeaderAttribute (string name, string value)
        {
            _name = name;
            _value = value;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(_name, new string[] { _value });
        }
    }
}
```

The following code applies the `AddHeader` attribute:

```
using Microsoft.AspNetCore.Mvc.RazorPages;
using PageFilter.Filters;

namespace PageFilter.Movies
{
    [AddHeader("Author", "Rick")]
    public class TestModel : PageModel
    {
        public void OnGet()
        {

        }
    }
}
```

Use a tool such as the browser developer tools to examine the headers. Under **Response Headers**, `author: Rick` is displayed.

See [Overriding the default order](../mvc/controllers/filters?view=aspnetcore-10.0#overriding-the-default-order) for instructions on overriding the order.

See [Cancellation and short circuiting](../mvc/controllers/filters?view=aspnetcore-10.0#cancellation-and-short-circuiting) for instructions to short-circuit the filter pipeline from a filter.

[]()

## Authorize filter attribute

The [Authorize](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute can be applied to a `PageModel`:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace PageFilter.Pages
{
    [Authorize]
    public class ModelWithAuthFilterModel : PageModel
    {
        public IActionResult OnGet() => Page();
    }
}
```

By [Rick Anderson](https://twitter.com/RickAndMSFT)

Razor Page filters [IPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter) and [IAsyncPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter) allow Razor Pages to run code before and after a Razor Page handler is run. Razor Page filters are similar to [ASP.NET Core MVC action filters](../mvc/controllers/filters?view=aspnetcore-10.0#action-filters), except they can't be applied to individual page handler methods.

Razor Page filters:

* Run code after a handler method has been selected, but before model binding occurs.
* Run code before the handler method executes, after model binding is complete.
* Run code after the handler method executes.
* Can be implemented on a page or globally.
* Cannot be applied to specific page handler methods.

Code can be run before a handler method executes using the page constructor or middleware, but only Razor Page filters have access to [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.httpcontext). Filters have a [FilterContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.filtercontext) derived parameter, which provides access to `HttpContext`. For example, the [Implement a filter attribute](#ifa) sample adds a header to the response, something that can't be done with constructors or middleware.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/filter/samples/PageFilter) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

Razor Page filters provide the following methods, which can be applied globally or at the page level:

* Synchronous methods:

  * [OnPageHandlerSelected](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter.onpagehandlerselected) : Called after a handler method has been selected, but before model binding occurs.
  * [OnPageHandlerExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter.onpagehandlerexecuting) : Called before the handler method executes, after model binding is complete.
  * [OnPageHandlerExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter.onpagehandlerexecuted) : Called after the handler method executes, before the action result.

* Asynchronous methods:

  * [OnPageHandlerSelectionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter.onpagehandlerselectionasync) : Called asynchronously after the handler method has been selected, but before model binding occurs.
  * [OnPageHandlerExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter.onpagehandlerexecutionasync) : Called asynchronously before the handler method is invoked, after model binding is complete.

Note

Implement **either** the synchronous or the async version of a filter interface, not both. The framework checks first to see if the filter implements the async interface, and if so, it calls that. If not, it calls the synchronous interface's methods. If both interfaces are implemented, only the async methods are called. The same rule applies to overrides in pages, implement the synchronous or the async version of the override, not both.

## Implement Razor Page filters globally

The following code implements `IAsyncPageFilter`:

```
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace PageFilter.Filters
{
    public class SampleAsyncPageFilter : IAsyncPageFilter
    {
        private readonly ILogger _logger;

        public SampleAsyncPageFilter(ILogger logger)
        {
            _logger = logger;
        }

        public async Task OnPageHandlerSelectionAsync(
                                            PageHandlerSelectedContext context)
        {
            _logger.LogDebug("Global OnPageHandlerSelectionAsync called.");
            await Task.CompletedTask;
        }

        public async Task OnPageHandlerExecutionAsync(
                                            PageHandlerExecutingContext context,
                                            PageHandlerExecutionDelegate next)
        {
            _logger.LogDebug("Global OnPageHandlerExecutionAsync called.");
            await next.Invoke();
        }
    }
}
```

In the preceding code, [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) is not required. It's used in the sample to provide trace information for the application.

The following code enables the `SampleAsyncPageFilter` in the `Startup` class:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Filters.Add(new SampleAsyncPageFilter(_logger));
    });
}
```

The following code shows the complete `Startup` class:

```
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PageFilter.Filters;

namespace PageFilter
{
    public class Startup
    {
        ILogger _logger;
        public Startup(ILoggerFactory loggerFactory, IConfiguration configuration)
        {
            _logger = loggerFactory.CreateLogger<GlobalFiltersLogger>();
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(options =>
            {
                options.Filters.Add(new SampleAsyncPageFilter(_logger));
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseMvc();
        }
    }
}
```

The following code calls `AddFolderApplicationModelConvention` to apply the `SampleAsyncPageFilter` to only pages in */subFolder*:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
       .AddRazorPagesOptions(options =>
       {
           options.Conventions.AddFolderApplicationModelConvention(
               "/subFolder",
               model => model.Filters.Add(new SampleAsyncPageFilter(_logger)));
       });
}
```

The following code implements the synchronous `IPageFilter`:

```
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace PageFilter.Filters
{
    public class SamplePageFilter : IPageFilter
    {
        private readonly ILogger _logger;

        public SamplePageFilter(ILogger logger)
        {
            _logger = logger;
        }

        public void OnPageHandlerSelected(PageHandlerSelectedContext context)
        {
            _logger.LogDebug("Global sync OnPageHandlerSelected called.");
        }

        public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
        {
            _logger.LogDebug("Global sync PageHandlerExecutingContext called.");
        }

        public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
        {
            _logger.LogDebug("Global sync OnPageHandlerExecuted called.");
        }
    }
}
```

The following code enables the `SamplePageFilter`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Filters.Add(new SamplePageFilter(_logger));
    });
}
```

## Implement Razor Page filters by overriding filter methods

The following code overrides the synchronous Razor Page filters:

```
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace PageFilter.Pages
{
    public class IndexModel : PageModel
    {
        private readonly ILogger _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }
        public string Message { get; set; }

        public void OnGet()
        {
            _logger.LogDebug("IndexModel/OnGet");
        }
        
        public override void OnPageHandlerSelected(
                                    PageHandlerSelectedContext context)
        {
            _logger.LogDebug("IndexModel/OnPageHandlerSelected");          
        }

        public override void OnPageHandlerExecuting(
                                    PageHandlerExecutingContext context)
        {
            Message = "Message set in handler executing";
            _logger.LogDebug("IndexModel/OnPageHandlerExecuting");
        }


        public override void OnPageHandlerExecuted(
                                    PageHandlerExecutedContext context)
        {
            _logger.LogDebug("IndexModel/OnPageHandlerExecuted");
        }
    }
}
```

[]()

## Implement a filter attribute

The built-in attribute-based filter [OnResultExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter.onresultexecutionasync) filter can be subclassed. The following filter adds a header to the response:

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace PageFilter.Filters
{
    public class AddHeaderAttribute  : ResultFilterAttribute
    {
        private readonly string _name;
        private readonly string _value;

        public AddHeaderAttribute (string name, string value)
        {
            _name = name;
            _value = value;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(_name, new string[] { _value });
        }
    }
}
```

The following code applies the `AddHeader` attribute:

```
[AddHeader("Author", "Rick")]
public class ContactModel : PageModel
{
    private readonly ILogger _logger;

    public ContactModel(ILogger<ContactModel> logger)
    {
        _logger = logger;
    }
    public string Message { get; set; }

    public async Task OnGetAsync()
    {
        Message = "Your contact page.";
        _logger.LogDebug("Contact/OnGet");
        await Task.CompletedTask;
    }
}
```

See [Overriding the default order](../mvc/controllers/filters?view=aspnetcore-10.0#overriding-the-default-order) for instructions on overriding the order.

See [Cancellation and short circuiting](../mvc/controllers/filters?view=aspnetcore-10.0#cancellation-and-short-circuiting) for instructions to short-circuit the filter pipeline from a filter.

[]()

## Authorize filter attribute

The [Authorize](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute can be applied to a `PageModel`:

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace PageFilter.Pages
{
    [Authorize]
    public class ModelWithAuthFilterModel : PageModel
    {
        public IActionResult OnGet() => Page();
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/app-offline
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/app-offline.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# App Offline file (`app_offline.htm`)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The App Offline file (`app_offline.htm`) is used by the ASP.NET Core Module to shut down an app.

If a file with the name `app_offline.htm` is detected in the root directory of an app, the ASP.NET Core Module attempts to gracefully shut down the app and stop processing incoming requests. If the app is still running after the number of seconds defined in `shutdownTimeLimit`, the ASP.NET Core Module stops the running process.

While the `app_offline.htm` file is present, the ASP.NET Core Module responds to requests by sending back the contents of the `app_offline.htm` file. The `app_offline.htm` must be less than 4 GB. When the `app_offline.htm` file is removed, the next request starts the app.

When using the out-of-process hosting model, the app might not shut down immediately if there's an open connection. For example, a WebSocket connection may delay app shut down.

## Locked deployment files

Files in the deployment folder are locked when the app is running. Locked files can't be overwritten during deployment.

`app_offline.htm` is the primary mechanism to release locked files. `app_offline.htm` is used by Web Deploy to properly stop and start the app.

`app_offline.htm` can be manually used to start and stop the app (requires PowerShell 5 or later):

```
$pathToApp = '{PATH TO APP}'


New-Item -Path $pathToApp -Name "app_offline.htm" -ItemType "file"

# Provide script commands here to deploy the app

Remove-Item -Path $pathToApp\app_offline.htm
```

In the preceding PowerShell script:

* The placeholder `{PATH TO APP}` is the path to the app.
* The `New-Item` command stops the app pool.
* The `Remove-Item` command starts the app pool.
* Commands between the `New-Item` command and the `Remove-Item` command are provided by the developer to deploy the app.

Files can also be unlocked by manually stopping the app pool in the IIS Manager on the server. Don't use the `app_offline.htm` file when using the IIS Manager to stop and restart the app pool.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/11/blazor-enhanced-nav-preloading-disabled
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/breaking-changes/11/blazor-enhanced-nav-preloading-disabled.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Blazor enhanced navigation no longer preloads resources

Blazor no longer emits `<link rel="modulepreload">` or `<link rel="preload">` resource hints for pages reached through enhanced navigation. Resource preloading still works for the initial page load and for normal (non-enhanced) navigation.

## Version introduced

.NET 11

## Previous behavior

Previously, Blazor's `ResourcePreloader` emitted preload `<link>` elements for resources used by the destination page on every server-rendered request, including requests served as an enhanced navigation.

When an enhanced navigation occurred, the new preload elements were merged into the existing DOM. The merge often added and removed `<link>` elements in a different order from the prior page, which caused some browsers to issue more preload requests than needed. The Blazor WebAssembly runtime also doesn't restart on an enhanced navigation, so preload hints emitted for it after the first page were redundant.

## New behavior

Starting in ASP.NET Core 11, `ResourcePreloader` only emits preload elements when the response isn't an enhanced navigation. Pages reached by enhanced navigation no longer receive the implicit preload hints that the framework added before.

The previous behavior on the initial page load (the first request, before any enhanced navigation occurs) is unchanged. Pages that the user navigates to without enhanced navigation (for example, full-page reloads or external navigations) also still receive preload hints.

## Type of breaking change

This change is a [behavioral change](/en-us/dotnet/core/compatibility/categories#behavioral-change).

## Reason for change

The implicit preload hints during enhanced navigation produced redundant browser preload requests because the WebAssembly runtime doesn't restart and the DOM-merge step often produced an unstable order of `<link>` elements. Removing the hints for enhanced navigation gives more predictable network behavior.

For more information, see [\[Blazor\] Disable preloading for enhanced navigation (`dotnet/aspnetcore` #63544)](https://github.com/dotnet/aspnetcore/pull/63544), and please don't comment on the closed PR. If you have feedback on this change, open a new issue on the `dotnet/aspnetcore` GitHub repository.

## Recommended action

If you relied on the implicit preload hints during enhanced navigation, add explicit `<link rel="preload">` or `<link rel="modulepreload">` tags in your `<HeadContent>` for the specific resources you want to preload:

```
<HeadContent>
    <link rel="modulepreload" href="_content/MyLib/big-module.js" />
</HeadContent>
```

Alternatively, opt the relevant links out of enhanced navigation if preloading is more important to you than the snappier in-place navigation. For more information, see [enhanced navigation and form handling](/en-us/aspnet/core/blazor/fundamentals/routing#enhanced-navigation-and-form-handling).

## Affected APIs

None. No public API surface changed. The change affects the implicit resource preloading performed by Blazor server-side rendering.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/cross-site-scripting
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/cross-site-scripting.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Prevent Cross-Site Scripting (XSS) in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

Cross-Site Scripting (XSS) is a security vulnerability that enables a cyberattacker to place client side scripts (usually JavaScript) into web pages. When other users load affected pages, the cyberattacker's scripts run. The cyberattacker can then steal cookies and session tokens, change the contents of the web page through DOM manipulation, or redirect the browser to another page. XSS vulnerabilities generally occur when an application takes user input and outputs it to a page without validating, encoding, or escaping it.

This article applies primarily to ASP.NET Core MVC with views, Razor Pages, and other apps that return HTML that can be vulnerable to XSS. Web APIs that return data in the form of HTML, XML, or JSON can trigger XSS attacks in their client apps if they don't properly sanitize user input. This behavior depends on how much trust the client app places in the API. If an API accepts user-generated content and returns it in an HTML response, the data is open to attack. A cyberattacker can inject malicious scripts into the content that executes when the response is rendered in the user's browser.

To prevent XSS attacks, web APIs should implement input validation and output encoding. Input validation ensures that user input meets expected criteria and doesn't include malicious code. Output encoding ensures that any data returned by the API is properly sanitized so it can't be executed as code by the user's browser. For more information, see [GitHub dotnet/aspnetcore.docs issue #28789](https://github.com/dotnet/AspNetCore.Docs/issues/28789).

## Protect your application against XSS

At a basic level, XSS works by tricking your application into inserting a `<script>` tag into your rendered page, or by inserting an `On*` event into an element.

To avoid introducing XSS into the application, developers should implement the following prevention techniques:

* Never put untrusted data into your HTML input, unless you follow the other techniques listed in this section.

  Untrusted data is any data controllable by a cyberattacker. Examples include HTML form inputs, query strings, HTTP headers, or even data sourced from a database. A cyberattacker might be able to breach your database even if they can't breach your application.

* Before you put untrusted data into an HTML element, ensure the data is HTML encoded.

  HTML encoding takes characters such as the left angle bracket or *less than* (`<`) and changes them into a safe form like (`&lt;`).

* Before you put untrusted data into an HTML attribute, ensure the data is HTML-attribute encoded.

  This specialized form of HTML encoding handles double quote (`"`), single quote (`'`), ampersand (`&`), and less than (`<`) characters. When dealing with untrusted input, use HTML encoding for general HTML content and HTML attribute encoding for HTML attributes.

* Before you put untrusted data into JavaScript, place the data in an HTML element whose contents you retrieve at runtime.

  If you can't follow this technique, ensure the data is JavaScript encoded. JavaScript encoding converts dangerous characters for JavaScript into a hexadecimal equivalent value. For example, JavaScript encoding changes the less than (`<`) character into the hex value `\u003C`.

* Before you put untrusted data into a URL query string, ensure the data is URL encoded.

## Explore HTML encoding with Razor

The Razor engine used in MVC automatically encodes all output sourced from variables, unless you work to prevent this behavior. It uses HTML attribute encoding rules whenever you use the at symbol `@` directive. Because HTML attribute encoding is a superset of HTML encoding, you don't have to consider whether to use HTML encoding or HTML-attribute encoding. You must ensure that you only use the at symbol `@` in an HTML context, and not when attempting to insert untrusted input directly into JavaScript. [Razor Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) also encode input you use in tag parameters.

Consider the following Razor view:

```
@{
    var untrustedInput = "<\"123\">";
}

@untrustedInput
```

This view outputs the contents of the `untrustedInput` variable. The variable includes some characters used in XSS attacks: less than (`<`), double quote (`"`), and right angle bracket or *greater than* (`>`). Examining the source shows the rendered output encoded as:

```
&lt;&quot;123&quot;&gt;
```

Warning

ASP.NET Core MVC provides an `HtmlString` class that isn't automatically encoded upon output. This class should never be used in combination with untrusted input because it exposes an XSS vulnerability.

## Explore JavaScript encoding with Razor

In some instances, you might want to insert a value into JavaScript to process in your view. There are two ways to accomplish this task. The safest way to insert values is to place the value in a data attribute of a tag and retrieve it in your JavaScript. For example:

```
@{
    var untrustedInput = "<script>alert(1)</script>";
}

<div id="injectedData"
     data-untrustedinput="@untrustedInput" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it can lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or, you can use createElement() to dynamically create document elements.
    // This instance uses textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>
```

The preceding markup generates the following HTML:

```
<div id="injectedData"
     data-untrustedinput="&lt;script&gt;alert(1)&lt;/script&gt;" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it can lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or, you can use createElement() to dynamically create document elements.
    // This instance uses textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>
```

The preceding code generates the following output:

```
<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(1)</script>
```

Warning

Don't concatenate untrusted input in JavaScript to create DOM elements or use `document.write()` on dynamically generated content.

Instead, use one of the following approaches to prevent code from being exposed to DOM-based XSS:

* Call `createElement()` and assign property values with appropriate methods or properties, such as `node.textContent=` or `node.InnerText=`.
* Call the `document.CreateTextNode()` method and append it in the appropriate DOM location.
* Call the `element.SetAttribute()` method.
* Use the `element[attribute]=` assignment.

## Access encoders in code

You can use HTML, JavaScript, and URL encoders in your code in two ways:

* Inject them via [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0).
* Use the default encoders contained in the `System.Text.Encodings.Web` namespace.

When you use the default encoders, any customizations applied to character ranges (so they're treated as safe) don't take effect. The default encoders use the safest encoding rules possible.

To use the configurable encoders via dependency injection, your constructors should take an `HtmlEncoder`, `JavaScriptEncoder`, and `UrlEncoder` parameter, as appropriate.

For example:

```
public class HomeController : Controller
{
    HtmlEncoder _htmlEncoder;
    JavaScriptEncoder _javaScriptEncoder;
    UrlEncoder _urlEncoder;

    public HomeController(HtmlEncoder htmlEncoder,
                          JavaScriptEncoder javascriptEncoder,
                          UrlEncoder urlEncoder)
    {
        _htmlEncoder = htmlEncoder;
        _javaScriptEncoder = javascriptEncoder;
        _urlEncoder = urlEncoder;
    }
}
```

## Encode URL parameters

If you want to build a URL query string with untrusted input as a value, use the `UrlEncoder` parameter to encode the value:

```
var example = "\"Quoted Value with spaces and &\"";
var encodedValue = _urlEncoder.Encode(example);
```

After encoding, the `encodedValue` variable contains the string `%22Quoted%20Value%20with%20spaces%20and%20%26%22`. Spaces, quotes, punctuation, and other unsafe characters are percent encoded to their hexadecimal value. For example, a space character is converted to `%20`.

Warning

Don't use untrusted input as part of a URL path. Always pass untrusted input as a query string value.

## Customize the encoders

By default, encoders use a safe list limited to the Basic Latin Unicode range. All characters outside of the indicated range are encoded as their character code equivalents. This behavior also affects rendering by Razor Tag Helpers and HTML Helpers because they use the encoders to output your strings.

The purpose for this behavior is to protect against unknown or future browser bugs. Previous browser bugs tripped up parsing based on the processing of non-English characters. If your web site makes heavy use of non-Latin characters, such as Chinese, Cyrillic, or others, this behavior is probably not suited for your configuration.

You can customize the encoder safe lists to include Unicode ranges appropriate to the app during startup. Make the customizations in the *Program.cs* file.

For example, you can use the default configuration with a Razor HTML Helper similar to the following HTML:

```
<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>
```

The preceding markup is rendered with Chinese text encoded:

```
<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>
```

To widen the range of characters treated as safe by the encoder, insert the following line into the *Program.cs* file:

```
builder.Services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));
```

You can customize the encoder safe lists to include Unicode ranges appropriate to your application during startup, in `ConfigureServices()`.

For example, using the default configuration you might use a Razor HtmlHelper like so;

```
<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>
```

When you view the source of the web page you'll see it has been rendered as follows, with the Chinese text encoded;

```
<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>
```

To widen the characters treated as safe by the encoder you would insert the following line into the `ConfigureServices()` method in `startup.cs`;

```
services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));
```

This example widens the safe list to include the Unicode Range [CJK Unified Ideographs](https://wikipedia.org/wiki/CJK_Unified_Ideographs). The following output shows the rendered view for the wider range of safe characters:

```
<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>
```

Safe list ranges are specified as Unicode code charts, not languages. The [Unicode standard](https://unicode.org/) has a list of [code charts](https://www.unicode.org/charts/index.html) you can use to find the chart that contains your characters. Each encoder (HTML, JavaScript, URL) must be configured separately.

Note

Customization of the safe list only affects encoders sourced via dependency injection. If you directly access an encoder via `System.Text.Encodings.Web.*Encoder.Default`, only the default safe list is used, Basic Latin.

## Determine when and where to encode

In general, the accepted practice is that encoding takes place at the point of output and encoded values should never be stored in a database.

Encoding at the point of output allows you to change the use of data. For example, change from HTML to a query string value. This approach enables you to easily search your data without having to encode values before searching. It also allows you to take advantage of any changes or bug fixes made to encoders.

## Use validation as an XSS prevention technique

Validation can be a useful tool in limiting XSS attacks. For example, a numeric string containing only the characters 0-9 doesn't trigger an XSS attack.

Validation is more complicated when HTML is accepted in user input. Parsing HTML input can be difficult, and sometimes, impossible. Markdown, coupled with a parser that strips embedded HTML, is a safer option for accepting rich input.

Never rely on validation alone. Always encode untrusted input before output, no matter what validation or sanitization is performed.

## Related content

* [Dependency injection in ASP.NET Core](../fundamentals/dependency-injection?view=aspnetcore-10.0)
* [Unicode 17.0 Character Code Charts](https://www.unicode.org/charts/index.html)
* [Razor Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/policyschemes.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Policy schemes in ASP.NET Core

Authentication policy schemes make it easier to have a single logical authentication scheme potentially use multiple approaches. For example, a policy scheme might use Google authentication for challenges, and cookie authentication for everything else. Authentication policy schemes make it:

* Easy to forward any authentication action to another scheme.
* Forward dynamically based on the request.

All authentication schemes that use derived [AuthenticationSchemeOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationschemeoptions) and the associated [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1):

* Are automatically policy schemes in ASP.NET Core 2.1 or later.
* Can be enabled via configuring the scheme's options.

```
public class AuthenticationSchemeOptions
{
    /// <summary>
    /// If set, this specifies a default scheme that authentication handlers should 
    /// forward all authentication operations to, by default. The default forwarding 
    /// logic checks in this order:
    /// 1. The most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut 
    /// 2. The ForwardDefaultSelector
    /// 3. ForwardDefault
    /// The first non null result is used as the target scheme to forward to.
    /// </summary>
    public string ForwardDefault { get; set; }

    /// <summary>
    /// If set, this specifies the target scheme that this scheme should forward 
    /// AuthenticateAsync calls to. For example:
    /// Context.AuthenticateAsync("ThisScheme") => 
    ///                Context.AuthenticateAsync("ForwardAuthenticateValue");
    /// Set the target to the current scheme to disable forwarding and allow 
    /// normal processing.
    /// </summary>
    public string ForwardAuthenticate { get; set; }

    /// <summary>
    /// If set, this specifies the target scheme that this scheme should forward 
    /// ChallengeAsync calls to. For example:
    /// Context.ChallengeAsync("ThisScheme") =>
    ///                         Context.ChallengeAsync("ForwardChallengeValue");
    /// Set the target to the current scheme to disable forwarding and allow normal
    /// processing.
    /// </summary>
    public string ForwardChallenge { get; set; }

    /// <summary>
    /// If set, this specifies the target scheme that this scheme should forward 
    /// ForbidAsync calls to.For example:
    /// Context.ForbidAsync("ThisScheme") 
    ///                               => Context.ForbidAsync("ForwardForbidValue");
    /// Set the target to the current scheme to disable forwarding and allow normal 
    /// processing.
    /// </summary>
    public string ForwardForbid { get; set; }

    /// <summary>
    /// If set, this specifies the target scheme that this scheme should forward 
    /// SignInAsync calls to. For example:
    /// Context.SignInAsync("ThisScheme") => 
    ///                                Context.SignInAsync("ForwardSignInValue");
    /// Set the target to the current scheme to disable forwarding and allow normal 
    /// processing.
    /// </summary>
    public string ForwardSignIn { get; set; }

    /// <summary>
    /// If set, this specifies the target scheme that this scheme should forward 
    /// SignOutAsync calls to. For example:
    /// Context.SignOutAsync("ThisScheme") => 
    ///                              Context.SignOutAsync("ForwardSignOutValue");
    /// Set the target to the current scheme to disable forwarding and allow normal 
    /// processing.
    /// </summary>
    public string ForwardSignOut { get; set; }

    /// <summary>
    /// Used to select a default scheme for the current request that authentication
    /// handlers should forward all authentication operations to by default. The 
    /// default forwarding checks in this order:
    /// 1. The most specific ForwardAuthenticate/Challenge/Forbid/SignIn/SignOut
    /// 2. The ForwardDefaultSelector
    /// 3. ForwardDefault. 
    /// The first non null result will be used as the target scheme to forward to.
    /// </summary>
    public Func<HttpContext, string> ForwardDefaultSelector { get; set; }
}
```

Framework default forwarding logic checks the following settings:

1. [ForwardAuthenticate](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationschemeoptions.forwardauthenticate#microsoft-aspnetcore-authentication-authenticationschemeoptions-forwardauthenticate)
2. [ForwardChallenge](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationschemeoptions.forwardchallenge#microsoft-aspnetcore-authentication-authenticationschemeoptions-forwardchallenge)
3. [ForwardForbid](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationschemeoptions.forwardforbid#microsoft-aspnetcore-authentication-authenticationschemeoptions-forwardforbid)
4. [ForwardSignIn](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationschemeoptions.forwardsignin#microsoft-aspnetcore-authentication-authenticationschemeoptions-forwardsignin)
5. [ForwardSignOut](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationschemeoptions.forwardsignout#microsoft-aspnetcore-authentication-authenticationschemeoptions-forwardsignout)
6. [ForwardDefaultSelector](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationschemeoptions.forwarddefaultselector#microsoft-aspnetcore-authentication-authenticationschemeoptions-forwarddefaultselector)
7. [ForwardDefault](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationschemeoptions.forwarddefault#microsoft-aspnetcore-authentication-authenticationschemeoptions-forwarddefault)

The first non-`null` result is used as the target scheme for forwarding.

## JWT with multiple schemes

The [AddPolicyScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder.addpolicyscheme) method can define multiple authentication schemes and implement logic to select the appropriate scheme based on token properties (for example, issuer, claims). This approach allows for greater flexibility within a single API.

```
services.AddAuthentication(options =>
{
	options.DefaultScheme = Consts.MY_POLICY_SCHEME;
	options.DefaultChallengeScheme = Consts.MY_POLICY_SCHEME;

})
.AddJwtBearer(Consts.MY_FIRST_SCHEME, options =>
{
	options.Authority = "https://your-authority";
	options.Audience = "https://your-audience";
	options.TokenValidationParameters = new TokenValidationParameters
	{
		ValidateIssuer = true,
		ValidateAudience = true,
		ValidateIssuerSigningKey = true,
		ValidAudiences = Configuration.GetSection("ValidAudiences").Get<string[]>(),
		ValidIssuers = Configuration.GetSection("ValidIssuers").Get<string[]>()
	};
})
.AddJwtBearer(Consts.MY_AAD_SCHEME, jwtOptions =>
{
	jwtOptions.Authority = Configuration["AzureAd:Authority"];
	jwtOptions.Audience = Configuration["AzureAd:Audience"]; 
})
.AddPolicyScheme(Consts.MY_POLICY_SCHEME, displayName: null, options =>
{
	options.ForwardDefaultSelector = context =>
	{
		string authorization = context.Request.Headers[HeaderNames.Authorization];
		if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
		{
			var token = authorization.Substring("Bearer ".Length).Trim();
			var jwtHandler = new JsonWebTokenHandler();

			if(jwtHandler.CanReadToken(token)) // it's a self contained access token and not encrypted
			{
				var issuer = jwtHandler.ReadJsonWebToken(token).Issuer; //.Equals("B2C-Authority"))
				if (issuer == Consts.MY_THIRD_PARTY_ISS) // Third party identity provider
				{
					return Consts.MY_THIRD_PARTY_SCHEME;
				}

				if (issuer == Consts.MY_AAD_ISS) // AAD
				{
					return Consts.MY_AAD_SCHEME;
				}
			}
		}

		// We don't know with it is
		return Consts.MY_AAD_SCHEME;
	};
});
```

## Examples

The following example shows a higher level scheme that combines lower level schemes. Google authentication is used for challenges, and cookie authentication is used for everything else:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
       .AddCookie(options => options.ForwardChallenge = "Google")
       .AddGoogle(options => { });
}
```

The following example enables dynamic selection of schemes on a per request basis. That is, how to mix cookies and API authentication:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options =>
        {
            // For example, can foward any requests that start with /api 
            // to the api scheme.
            options.ForwardDefaultSelector = ctx => 
               ctx.Request.Path.StartsWithSegments("/api") ? "Api" : null;
        })
        .AddYourApiAuth("Api");
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/key-vault-configuration.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use Azure Key Vault configuration provider in ASP.NET Core

This article explains how to use the [Azure Key Vault](/en-us/azure/key-vault/) configuration provider to load app configuration values from Key Vault secrets. Key Vault is a cloud-based service that helps safeguard cryptographic keys and secrets used by apps and services. Common scenarios for using Key Vault with ASP.NET Core apps include:

* Controlling access to sensitive configuration data.
* Meeting the requirement for FIPS 140-2 Level 2 validated Hardware Security Modules (HSMs) when storing configuration data.

## Packages

Add package references for the following packages:

* [Azure.Extensions.AspNetCore.Configuration.Secrets](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.Configuration.Secrets)
* [Azure.Identity](https://www.nuget.org/packages/Azure.Identity)

## Sample app

The sample app runs in either of two modes determined by the `#define` preprocessor directive at the top of the *Program.cs* file:

* `Certificate`: Demonstrates how to use a Key Vault Client ID and X.509 certificate to access secrets stored in a key vault. You can run the sample from any location, whether deployed to Azure App Service or any host that can serve an ASP.NET Core app.

* `Managed`: Demonstrates how to use [Managed identities for Azure resources](/en-us/entra/identity/managed-identities-azure-resources/overview). The managed identity authenticates the app to Key Vault with Managed identities for Azure resources without storing credentials in the application code or configuration. The `Managed` version of the sample must be deployed to Azure. Follow the guidance in the [Use the managed identities for Azure resources](#use-managed-identities-for-azure-resources) section.

For more information about configuring a sample app by using `#define`, see [Preprocessor directives in sample code](../fundamentals/?view=aspnetcore-10.0#preprocessor-directives-in-sample-code).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/key-vault-configuration/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Secret storage in the Development environment

Set secrets locally by using the [Secret Manager](app-secrets?view=aspnetcore-10.0) tool. When the sample app runs on the local machine in the `Development` environment, secrets are loaded from the local user secrets store.

Secret Manager requires a `<UserSecretsId>` property in the application project file. Set the property value (`{GUID}`) to any unique GUID:

```
<PropertyGroup>
  <UserSecretsId>{GUID}</UserSecretsId>
</PropertyGroup>
```

Secrets are created as name-value pairs. Hierarchical values (configuration sections) use colons (`:`) as separators in [ASP.NET Core configuration](../fundamentals/configuration/?view=aspnetcore-10.0) key names.

Secret Manager is used in a command shell (or terminal) opened to the project's [content root](../fundamentals/?view=aspnetcore-10.0#content-root), where `{SECRET NAME}` is the name and `{SECRET VALUE}` is the value:

```
dotnet user-secrets set "{SECRET NAME}" "{SECRET VALUE}"
```

Run the following commands in a command shell from the project's [content root](../fundamentals/?view=aspnetcore-10.0#content-root). The commands set the secrets for the sample app:

```
dotnet user-secrets set "SecretName" "secret_value_1_dev"
dotnet user-secrets set "Section:SecretName" "secret_value_2_dev"
```

When these secrets are stored in Key Vault, as described in the [Secret storage in the Production environment with Key Vault](#secret-storage-in-the-production-environment-with-azure-key-vault) section, the `_dev` suffix changes to `_prod`. The suffix provides a visual cue in the application output indicating the source of the configuration values.

## Secret storage in the Production environment with Key Vault

Create an Azure key vault and store the sample application secrets by completing the following steps. For more information, see [Quickstart: Set and retrieve a secret from Key Vault with the Azure CLI](/en-us/azure/key-vault/quick-create-cli).

1. Open Azure Cloud Shell by using any one of the following methods in the [Azure portal](https://portal.azure.com/):

   * Select **Try It** in the upper-right corner of a code block. Use the search string "Azure CLI" in the text box.
   * Open Cloud Shell in your browser with the **Launch Cloud Shell** button.
   * Select the **Cloud Shell** button on the menu in the upper-right corner of the Azure portal.

   For more information, see the [Azure CLI documentation](/en-us/cli/azure/) and [Overview of Azure Cloud Shell](/en-us/azure/cloud-shell/overview).

2. If you aren't already authenticated, sign in with the `az login` command.

3. Create a resource group with the following command, where `{RESOURCE GROUP NAME}` is the name for the new resource group and `{LOCATION}` is the Azure region:

   ```
   az group create --name "{RESOURCE GROUP NAME}" --location {LOCATION}
   ```

4. Create an Azure key vault in the resource group with the following command, where `{KEY VAULT NAME}` is the name for the new vault and `{LOCATION}` is the Azure region:

   ```
   az keyvault create --name {KEY VAULT NAME} --resource-group "{RESOURCE GROUP NAME}" --location {LOCATION}
   ```

5. Create secrets in the vault as name-value pairs.

   Key Vault secret names can consist of alphanumeric characters and dashes (`-`). Hierarchical values (configuration sections) use two dashes or hyphens (`--`) as a delimiter. The colon character isn't allowed in Key Vault secret names. Colons delimit a section from a subkey in [ASP.NET Core configuration](../fundamentals/configuration/?view=aspnetcore-10.0). The two-dash sequence is replaced with a colon when the secrets are loaded into the application configuration.

   The following secrets are for use with the sample app. The values include a `_prod` suffix that distinguishes them from the `_dev` suffix values loaded in the `Development` environment via Secret Manager. Replace `{KEY VAULT NAME}` with the name of the key vault you created earlier:

   ```
   az keyvault secret set --vault-name {KEY VAULT NAME} --name "SecretName" --value "secret_value_1_prod"
   az keyvault secret set --vault-name {KEY VAULT NAME} --name "Section--SecretName" --value "secret_value_2_prod"
   ```

## Use Application ID and X.509 certificate for non-Azure-hosted apps

Configure Key Vault and the app to use a Microsoft Entra ID Application ID and X.509 certificate to authenticate to a vault **when the app is hosted outside of Azure**. For more information, see [About keys, secrets, and certificates](/en-us/azure/key-vault/general/about-keys-secrets-certificates).

Note

Although using an Application ID and X.509 certificate is supported for apps hosted in Azure, this approach isn't recommended. Instead, use [Managed identities for Azure resources](#use-managed-identities-for-azure-resources) when hosting an app in Azure. Managed identities don't require storing a certificate in the app or in the `Development` environment.

The sample app uses an Application ID and X.509 certificate when the `#define` preprocessor directive at the top of the *Program.cs* file is set to `Certificate`.

1. Create a PKCS#12 archive (*.pfx*) certificate. Options for creating certificates include [New-SelfSignedCertificate on Windows](/en-us/powershell/module/pki/new-selfsignedcertificate) and [OpenSSL](https://www.openssl.org/).

2. Install the certificate into the current user's personal certificate store. Marking the key as exportable is optional. Note the certificate's thumbprint, which is used later in this process.

3. Export the PKCS#12 archive (*.pfx*) certificate as a DER-encoded certificate (*.cer*).

4. Register the app with Microsoft Entra ID (**App registrations**).

5. Upload the DER-encoded certificate (*.cer*) to Microsoft Entra ID:

   1. Select the app in Microsoft Entra ID.
   2. Go to **Certificates & secrets**.
   3. Select **Upload certificate** to upload the certificate, which contains the public key. A *.cer*, *.pem*, or *.crt* certificate is acceptable.

6. Store the key vault name, Application ID, and certificate thumbprint in the application *appsettings.json* file.

7. In the Azure portal, go to **Key Vaults**.

8. Select the key vault you created in the [Secret storage in the Production environment with Azure Key Vault](#secret-storage-in-the-production-environment-with-azure-key-vault) section.

9. Select **Access policies**, and then select **Add Access Policy**.

10. Open **Secret permissions** and provide the app with **Get** and **List** permissions.

11. Choose **Select principal**. Select the registered app by name, and then choose **Select**.

12. Select **OK**, and then select **Save**.

13. Deploy the app.

The `Certificate` sample app obtains its configuration values from [IConfigurationRoot](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationroot) with the same name as the secret name:

* Nonhierarchical values: The value for `SecretName` is obtained with `config["SecretName"]`.

* Hierarchical values (sections): Use colon (`:`) notation or the [GetSection](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getsection) method. Use one of the following approaches to obtain the configuration value:

  * `config["Section:SecretName"]`
  * `config.GetSection("Section")["SecretName"]`

The operating system manages the X.509 certificate. The app calls the [AddAzureKeyVault](/en-us/dotnet/api/microsoft.extensions.configuration.azurekeyvaultconfigurationextensions.addazurekeyvault) method with values supplied by the *appsettings.json* file:

```

using System.Security.Cryptography.X509Certificates;
using Azure.Identity;

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsProduction())
{
    using var x509Store = new X509Store(StoreLocation.CurrentUser);

    x509Store.Open(OpenFlags.ReadOnly);

    var x509Certificate = x509Store.Certificates
        .Find(
            X509FindType.FindByThumbprint,
            builder.Configuration["AzureADCertThumbprint"],
            validOnly: false)
        .OfType<X509Certificate2>()
        .Single();

    builder.Configuration.AddAzureKeyVault(
        new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
        new ClientCertificateCredential(
            builder.Configuration["AzureADDirectoryId"],
            builder.Configuration["AzureADApplicationId"],
            x509Certificate));
}

var app = builder.Build();
```

Example values:

* Key vault name: `contosovault`
* Application ID: `00001111-aaaa-2222-bbbb-3333cccc4444`
* Certificate thumbprint: `fe14593dd66b2406c5269d742d04b6e1ab03adb1`

The values in the *appsettings.json* file:

```
{
  "KeyVaultName": "Key Vault Name",
  "AzureADApplicationId": "Azure AD Application ID",
  "AzureADCertThumbprint": "Azure AD Certificate Thumbprint",
  "AzureADDirectoryId": "Azure AD Directory ID"
}
```

When you run the app, a webpage shows the loaded secret values. In the `Development` environment, secret values load with the `_dev` suffix. In the `Production` environment, the values load with the `_prod` suffix.

## Use managed identities for Azure resources

**An app deployed to Azure** can take advantage of [Managed identities for Azure resources](/en-us/entra/identity/managed-identities-azure-resources/overview). A managed identity allows the app to authenticate with Azure Key Vault by using Microsoft Entra ID authentication without storing credentials in the application code or configuration.

The sample app uses a system-assigned managed identity when the `#define` preprocessor directive at the top of the *Program.cs* file is set to `Managed`. To create a managed identity for an Azure App Service app, see [How to use managed identities for App Service and Azure Functions](/en-us/azure/app-service/overview-managed-identity). After the managed identity is created, take note of the application Object ID shown in the Azure portal on the **Identity** panel of the App Service.

Enter the key vault name into the application *appsettings.json* file. The sample app doesn't require an Application ID and Password (Client Secret) when set to the `Managed` version, so you can ignore those configuration entries. The app is deployed to Azure, and Azure authenticates the app to access Key Vault only by using the vault name stored in the *appsettings.json* file.

Deploy the sample app to Azure App Service.

Using the Azure CLI and the app's Object ID, provide the app with `list` and `get` permissions to access the key vault:

```
az keyvault set-policy --name {KEY VAULT NAME} --object-id {OBJECT ID} --secret-permissions get list
```

**Restart the app** by using the Azure CLI, Azure PowerShell, or the Azure portal.

The sample app creates an instance of the [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) class. The credential attempts to obtain an access token from environment for Azure resources:

```
using Azure.Identity;

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsProduction())
{
    builder.Configuration.AddAzureKeyVault(
        new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
        new DefaultAzureCredential());
}
```

Note

The preceding example uses the [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) class to simplify authentication when developing apps that deploy to Azure. This approach combines credentials used in Azure hosting environments with credentials used in local development. When you move the implementation to production, an alternative is a better choice, such as the [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential) class. For more information, see [Authenticate Azure-hosted .NET apps to Azure resources by using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

Example name for the key vault: `contosovault`

The name value in the *appsettings.json* file:

```
{
  "KeyVaultName": "Key Vault Name"
}
```

For apps that use a user-assigned managed identity, configure the managed identity's Client ID by using one of the following approaches:

* Set the `AZURE_CLIENT_ID` environment variable.

* Set the [DefaultAzureCredentialOptions.ManagedIdentityClientId](/en-us/dotnet/api/azure.identity.defaultazurecredentialoptions.managedidentityclientid) property when calling the `AddAzureKeyVault` method:

  ```
  builder.Configuration.AddAzureKeyVault(
      new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
      new DefaultAzureCredential(new DefaultAzureCredentialOptions
      {
          ManagedIdentityClientId = builder.Configuration["AzureADManagedIdentityClientId"]
      }));
  ```

When you run the app, a webpage shows the loaded secret values. In the `Development` environment, secret values have the `_dev` suffix because they're provided by Secret Manager. In the `Production` environment, the values load with the `_prod` suffix because they're provided by Azure Key Vault.

If you receive an `Access denied` error, confirm the app is registered with Microsoft Entra ID and has access to the vault. Confirm you restarted the service in Azure.

For information on using the provider with a managed identity and Azure Pipelines, see [Connect to Azure with an Azure Resource Manager service connection](/en-us/azure/devops/pipelines/library/connect-to-azure#create-an-azure-resource-manager-service-connection-to-a-vm-with-a-managed-service-identity).

## Configuration options

The `AddAzureKeyVault` method can accept an [AzureKeyVaultConfigurationOptions](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.azurekeyvaultconfigurationoptions) object:

```
// using Azure.Extensions.AspNetCore.Configuration.Secrets;

builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
    new DefaultAzureCredential(),
    new AzureKeyVaultConfigurationOptions
    {
        // ...
    });
```

The `AzureKeyVaultConfigurationOptions` object contains the following properties:

| Property                                                                                                                               | Description                                                                                                                                                             |
| -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Manager](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.azurekeyvaultconfigurationoptions.manager)               | Specifies a [KeyVaultSecretManager](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.keyvaultsecretmanager) instance used to control secret loading. |
| [ReloadInterval](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.azurekeyvaultconfigurationoptions.reloadinterval) | Specifies the `TimeSpan` to wait between attempts at polling the key vault for changes. The default value is `null` (configuration isn't reloaded).                     |

## Use a key name prefix

The `AddAzureKeyVault` method provides an overload that accepts an implementation of [KeyVaultSecretManager](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.keyvaultsecretmanager), which allows you to control how Key Vault secrets are converted into configuration keys. For example, you can implement the interface to load secret values based on a prefix value you provide at app startup. This technique allows you, for example, to load secrets based on the version of the app.

Important

Don't use prefixes on Key Vault secrets to:

* Place secrets for multiple apps into the same key vault.
* Place environmental secrets (for example, `Development` versus `Production` secrets) into the same key vault.

Different apps and development/production environments should use separate key vaults to isolate app environments for the highest level of security.

In the following example, a secret is established in Key Vault (by using Secret Manager for the `Development` environment) for `5000-AppSecret` (periods aren't allowed in Key Vault secret names). This secret represents an app secret for version 5.0.0.0 of the app. For another version of the app, 5.1.0.0, a secret is added to the same key vault (by using Secret Manager) for `5100-AppSecret`. Each app version loads its versioned secret value into its configuration as `AppSecret`, removing the version as it loads the secret.

The `AddAzureKeyVault` method is called with a custom `KeyVaultSecretManager` implementation:

```
// using Azure.Extensions.AspNetCore.Configuration.Secrets;

builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
    new DefaultAzureCredential(),
    new SamplePrefixKeyVaultSecretManager("5000"));
```

The implementation reacts to the version prefixes of secrets to load the proper secret into configuration:

* The `Load` method loads a secret when its name starts with the prefix. Other secrets aren't loaded.

* The `GetKey` method:

  * Removes the prefix from the secret name.
  * Replaces two dashes (`--`) in any name with the `KeyDelimiter`. This delimiter (usually a colon `:`) is used in configuration because Key Vault doesn't allow a colon (`:`) in secret names.

```
public class SamplePrefixKeyVaultSecretManager : KeyVaultSecretManager
{
    private readonly string _prefix;

    public SamplePrefixKeyVaultSecretManager(string prefix)
        => _prefix = $"{prefix}-";

    public override bool Load(SecretProperties properties)
        => properties.Name.StartsWith(_prefix);

    public override string GetKey(KeyVaultSecret secret)
        => secret.Name[_prefix.Length..].Replace("--", ConfigurationPath.KeyDelimiter);
}
```

A provider algorithm calls the `Load` method and iterates through the key vault secrets to find the version-prefixed secrets. When a version prefix is found with `Load`, the algorithm uses the `GetKey` method to return the configuration name of the secret name. It removes the version prefix from the secret's name. The rest of the secret name is returned for loading into the application configuration name-value pairs.

When you implement this approach, complete the following steps:

1. Specify the application version in the application project file. In the following example, the application version is set to `5.0.0.0`:

   ```
   <PropertyGroup>
     <Version>5.0.0.0</Version>
   </PropertyGroup>
   ```

2. Confirm the `<UserSecretsId>` property is defined in the application project file, where `{GUID}` is a user-supplied GUID:

   ```
   <PropertyGroup>
     <UserSecretsId>{GUID}</UserSecretsId>
   </PropertyGroup>
   ```

   Save the following secrets locally by using [Secret Manager](app-secrets?view=aspnetcore-10.0):

   ```
   dotnet user-secrets set "5000-AppSecret" "5.0.0.0_secret_value_dev"
   dotnet user-secrets set "5100-AppSecret" "5.1.0.0_secret_value_dev"
   ```

3. Save the secrets in Azure Key Vault by using the following Azure CLI commands:

   ```
   az keyvault secret set --vault-name {KEY VAULT NAME} --name "5000-AppSecret" --value "5.0.0.0_secret_value_prod"
   az keyvault secret set --vault-name {KEY VAULT NAME} --name "5100-AppSecret" --value "5.1.0.0_secret_value_prod"
   ```

The process completes the following tasks:

* When the app runs, the implementation loads the key vault secrets. The string secret for `5000-AppSecret` is matched to the application version specified in the app's project file (`5.0.0.0`).

* The version, `5000` (with the dash), is stripped from the key name. Throughout the app, reading configuration with the key `AppSecret` loads the secret value.

* If the application version is changed in the project file to `5.1.0.0` and the app is run again, the secret value returned is `5.1.0.0_secret_value_dev` in the `Development` environment and `5.1.0.0_secret_value_prod` in `Production`.

Note

You can also provide your own [SecretClient](/en-us/dotnet/api/azure.security.keyvault.secrets.secretclient) implementation to the `AddAzureKeyVault` method. A custom client enables sharing a single instance of the client across the app.

## Bind an array to a class

The provider can read configuration values into an array for binding to a POCO array.

When reading from a configuration source that allows keys to contain colon (`:`) separators, a numeric key segment is used to distinguish the keys that make up an array (`:0:`, `:1:`, … `:{n}:`). For more information, see [Configuration: Bind an array to a class](../fundamentals/configuration/?view=aspnetcore-10.0#bind-an-array).

Key Vault keys can't use a colon (`:`) as a separator. The approach described in this article uses double dashes (`--`) as a separator for hierarchical values (sections). Array keys are stored in Key Vault with double dashes and numeric key segments (`--0--`, `--1--`, … `--{n}--`).

Examine the following [Serilog](https://serilog.net/) logging provider configuration supplied in a JSON file. There are two object literals defined in the `WriteTo` array that reflect two Serilog *sinks*, which describe destinations for logging output:

```
"Serilog": {
  "WriteTo": [
    {
      "Name": "AzureTableStorage",
      "Args": {
        "storageTableName": "logs",
        "connectionString": "DefaultEnd...ountKey=Eby8...GMGw=="
      }
    },
    {
      "Name": "AzureDocumentDB",
      "Args": {
        "endpointUrl": "https://contoso.documents.azure.com:443",
        "authorizationKey": "Eby8...GMGw=="
      }
    }
  ]
}
```

The configuration in the JSON file is stored in Key Vault by using double dash (`--`) notation and numeric segments:

| Key                                           | Value                                     |
| --------------------------------------------- | ----------------------------------------- |
| `Serilog--WriteTo--0--Name`                   | `AzureTableStorage`                       |
| `Serilog--WriteTo--0--Args--storageTableName` | `logs`                                    |
| `Serilog--WriteTo--0--Args--connectionString` | `DefaultEnd...ountKey=Eby8...GMGw==`      |
| `Serilog--WriteTo--1--Name`                   | `AzureDocumentDB`                         |
| `Serilog--WriteTo--1--Args--endpointUrl`      | `https://contoso.documents.azure.com:443` |
| `Serilog--WriteTo--1--Args--authorizationKey` | `Eby8...GMGw==`                           |

## Reload secrets

By default, the configuration provider caches secrets for the application lifetime. The app ignores secrets that are later disabled or updated in the key vault.

To reload secrets, call the [IConfigurationRoot.Reload](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationroot.reload) method:

```
config.Reload();
```

To reload secrets periodically, at a specified interval, set the [AzureKeyVaultConfigurationOptions.ReloadInterval](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.azurekeyvaultconfigurationoptions.reloadinterval) property. For more information, see [Configuration options](#configuration-options).

## Disabled and expired secrets

Expired secrets are included by default in the configuration provider. To exclude values for these secrets in app configuration, update the expired secret or provide the configuration by using a custom configuration provider:

```
class SampleKeyVaultSecretManager : KeyVaultSecretManager
{
  public override bool Load(SecretProperties properties) =>
    properties.ExpiresOn.HasValue &&
    properties.ExpiresOn.Value > DateTimeOffset.Now;
}
```

Pass this custom `KeyVaultSecretManager` provider to the `AddAzureKeyVault` method:

```
// using Azure.Extensions.AspNetCore.Configuration.Secrets;

builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
    new DefaultAzureCredential(),
    new SampleKeyVaultSecretManager());
```

Disabled secrets can't be retrieved from Key Vault and are never included.

Note

The preceding example uses the [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) class to simplify authentication when developing apps that deploy to Azure. This approach combines credentials used in Azure hosting environments with credentials used in local development. When you move the implementation to production, an alternative is a better choice, such as the [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential) class. For more information, see [Authenticate Azure-hosted .NET apps to Azure resources by using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

## Troubleshoot configuration load issues

When the app fails to load configuration by using the provider, an error message is written to the [ASP.NET Core logging infrastructure](../fundamentals/logging/?view=aspnetcore-10.0).

The following conditions can prevent configuration from loading:

* The app or certificate isn't configured correctly in Microsoft Entra ID.
* The vault doesn't exist in Azure Key Vault.
* The app isn't authorized to access the vault.
* The access policy doesn't include `Get` and `List` permissions.
* In the vault, the configuration data (name-value pair) is incorrectly named, missing, or disabled.
* The app has the wrong key vault name (`KeyVaultName`), Microsoft Entra ID Application ID (`AzureADApplicationId`), Microsoft Entra ID certificate thumbprint (`AzureADCertThumbprint`), or Microsoft Entra ID Directory ID (`AzureADDirectoryId`).
* When the Key Vault access policy is added for the app, the policy is successfully created, but the user didn't select **Save** in the **Access policies** dialog.

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/key-vault-configuration/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Configuration in ASP.NET Core](../fundamentals/configuration/?view=aspnetcore-10.0)
* [Azure Key Vault documentation](/en-us/azure/key-vault/)
* [Import HSM-protected keys to Azure Key Vault](/en-us/azure/key-vault/keys/hsm-protected-keys)
* [Quickstart: Azure Key Vault secret client library for .NET](/en-us/azure/key-vault/secrets/quick-create-net)
* [Tutorial: Use Azure Key Vault with a virtual machine in .NET](/en-us/azure/key-vault/general/tutorial-net-virtual-machine)

This article explains how to use the [Azure Key Vault](/en-us/azure/key-vault/) configuration provider to load app configuration values from Azure Key Vault secrets. Azure Key Vault is a cloud-based service that helps safeguard cryptographic keys and secrets used by apps and services. Common scenarios for using Azure Key Vault with ASP.NET Core apps include:

* Controlling access to sensitive configuration data.
* Meeting the requirement for FIPS 140-2 Level 2 validated Hardware Security Modules (HSMs) when storing configuration data.

## Packages

Add package references for the following packages:

* [Azure.Extensions.AspNetCore.Configuration.Secrets](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.Configuration.Secrets)
* [Azure.Identity](https://www.nuget.org/packages/Azure.Identity)

## Sample app

The sample app runs in either of two modes determined by the `#define` preprocessor directive at the top of `Program.cs`:

* `Certificate`: Demonstrates using an Azure Key Vault Client ID and X.509 certificate to access secrets stored in Azure Key Vault. This sample can be run from any location, whether deployed to Azure App Service or any host that can serve an ASP.NET Core app.
* `Managed`: Demonstrates how to use [Managed identities for Azure resources](/en-us/azure/active-directory/managed-identities-azure-resources/overview). The managed identity authenticates the app to Azure Key Vault with Managed identities for Azure resources without credentials stored in the app's code or configuration. When using managed identities to authenticate, a Managed identities for Azure resources app ID and Password (Client Secret) aren't required. The `Managed` version of the sample must be deployed to Azure. Follow the guidance in the [Use the managed identities for Azure resources](#use-managed-identities-for-azure-resources) section.

For more information configuring a sample app using preprocessor directives (`#define`), see [Overview of ASP.NET Core](../overview?view=aspnetcore-10.0#preprocessor-directives-in-sample-code).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/key-vault-configuration/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Secret storage in the `Development` environment

Set secrets locally using [Secret Manager](app-secrets?view=aspnetcore-10.0). When the sample app runs on the local machine in the `Development` environment, secrets are loaded from the local user secrets store.

Secret Manager requires a `<UserSecretsId>` property in the app's project file. Set the property value (`{GUID}`) to any unique GUID:

```
<PropertyGroup>
  <UserSecretsId>{GUID}</UserSecretsId>
</PropertyGroup>
```

Secrets are created as name-value pairs. Hierarchical values (configuration sections) use a `:` (colon) as a separator in [ASP.NET Core configuration](../fundamentals/configuration/?view=aspnetcore-10.0) key names.

Secret Manager is used from a command shell opened to the project's [content root](../fundamentals/?view=aspnetcore-10.0#content-root), where `{SECRET NAME}` is the name and `{SECRET VALUE}` is the value:

```
dotnet user-secrets set "{SECRET NAME}" "{SECRET VALUE}"
```

Execute the following commands in a command shell from the project's [content root](../fundamentals/?view=aspnetcore-10.0#content-root) to set the secrets for the sample app:

```
dotnet user-secrets set "SecretName" "secret_value_1_dev"
dotnet user-secrets set "Section:SecretName" "secret_value_2_dev"
```

When these secrets are stored in Azure Key Vault in the [Secret storage in the `Production` environment with Azure Key Vault](#secret-storage-in-the-production-environment-with-azure-key-vault) section, the `_dev` suffix is changed to `_prod`. The suffix provides a visual cue in the app's output indicating the source of the configuration values.

## Secret storage in the `Production` environment with Azure Key Vault

Complete the following steps to create an Azure Key Vault and store the sample app's secrets in it. For more information, see [Quickstart: Set and retrieve a secret from Azure Key Vault using Azure CLI](/en-us/azure/key-vault/quick-create-cli).

1. Open Azure Cloud Shell using any one of the following methods in the [Azure portal](https://portal.azure.com/):

   * Select **Try It** in the upper-right corner of a code block. Use the search string "Azure CLI" in the text box.
   * Open Cloud Shell in your browser with the **Launch Cloud Shell** button.
   * Select the **Cloud Shell** button on the menu in the upper-right corner of the Azure portal.

   For more information, see [Azure CLI](/en-us/cli/azure/) and [Overview of Azure Cloud Shell](/en-us/azure/cloud-shell/overview).

2. If you aren't already authenticated, sign in with the `az login` command.

3. Create a resource group with the following command, where `{RESOURCE GROUP NAME}` is the new resource group's name and `{LOCATION}` is the Azure region:

   ```
   az group create --name "{RESOURCE GROUP NAME}" --location {LOCATION}
   ```

4. Create a Key Vault in the resource group with the following command, where `{KEY VAULT NAME}` is the new vault's name and `{LOCATION}` is the Azure region:

   ```
   az keyvault create --name {KEY VAULT NAME} --resource-group "{RESOURCE GROUP NAME}" --location {LOCATION}
   ```

5. Create secrets in the vault as name-value pairs.

   Azure Key Vault secret names are limited to alphanumeric characters and dashes. Hierarchical values (configuration sections) use `--` (two dashes) as a delimiter, as colons aren't allowed in Key Vault secret names. Colons delimit a section from a subkey in [ASP.NET Core configuration](../fundamentals/configuration/?view=aspnetcore-10.0). The two-dash sequence is replaced with a colon when the secrets are loaded into the app's configuration.

   The following secrets are for use with the sample app. The values include a `_prod` suffix to distinguish them from the `_dev` suffix values loaded in the `Development` environment from Secret Manager. Replace `{KEY VAULT NAME}` with the name of the Key Vault you created in the prior step:

   ```
   az keyvault secret set --vault-name {KEY VAULT NAME} --name "SecretName" --value "secret_value_1_prod"
   az keyvault secret set --vault-name {KEY VAULT NAME} --name "Section--SecretName" --value "secret_value_2_prod"
   ```

## Use Application ID and X.509 certificate for non-Azure-hosted apps

Configure Azure Key Vault and the app to use a Microsoft Entra ID Application ID and X.509 certificate to authenticate to a vault **when the app is hosted outside of Azure**. For more information, see [About keys, secrets, and certificates](/en-us/azure/key-vault/about-keys-secrets-and-certificates).

Note

Although using an Application ID and X.509 certificate is supported for apps hosted in Azure, it's not recommended. Instead, use [Managed identities for Azure resources](#use-managed-identities-for-azure-resources) when hosting an app in Azure. Managed identities don't require storing a certificate in the app or in the `Development` environment.

The sample app uses an Application ID and X.509 certificate when the `#define` preprocessor directive at the top of `Program.cs` is set to `Certificate`.

1. Create a PKCS#12 archive (*.pfx*) certificate. Options for creating certificates include [New-SelfSignedCertificate on Windows](/en-us/powershell/module/pki/new-selfsignedcertificate) and [OpenSSL](https://www.openssl.org/).

2. Install the certificate into the current user's personal certificate store. Marking the key as exportable is optional. Note the certificate's thumbprint, which is used later in this process.

3. Export the PKCS#12 archive (*.pfx*) certificate as a DER-encoded certificate (*.cer*).

4. Register the app with Microsoft Entra ID (**App registrations**).

5. Upload the DER-encoded certificate (*.cer*) to Microsoft Entra ID:

   1. Select the app in Microsoft Entra ID.
   2. Navigate to **Certificates & secrets**.
   3. Select **Upload certificate** to upload the certificate, which contains the public key. A *.cer*, *.pem*, or *.crt* certificate is acceptable.

6. Store the Key Vault name, Application ID, and certificate thumbprint in the app's `appsettings.json` file.

7. Navigate to **Key Vaults** in the Azure portal.

8. Select the Key Vault you created in the [Secret storage in the `Production` environment with Azure Key Vault](#secret-storage-in-the-production-environment-with-azure-key-vault) section.

9. Select **Access policies**.

10. Select **Add Access Policy**.

11. Open **Secret permissions** and provide the app with **Get** and **List** permissions.

12. Select **Select principal** and select the registered app by name. Select the **Select** button.

13. Select **OK**.

14. Select **Save**.

15. Deploy the app.

The `Certificate` sample app obtains its configuration values from [IConfigurationRoot](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationroot) with the same name as the secret name:

* Non-hierarchical values: The value for `SecretName` is obtained with `config["SecretName"]`.

* Hierarchical values (sections): Use `:` (colon) notation or the [GetSection](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getsection) method. Use either of these approaches to obtain the configuration value:

  * `config["Section:SecretName"]`
  * `config.GetSection("Section")["SecretName"]`

The X.509 certificate is managed by the OS. The app calls [AddAzureKeyVault](/en-us/dotnet/api/microsoft.extensions.configuration.azurekeyvaultconfigurationextensions.addazurekeyvault) with values supplied by the `appsettings.json` file:

```
// using System.Linq;
// using System.Security.Cryptography.X509Certificates;
// using Azure.Extensions.AspNetCore.Configuration.Secrets;
// using Azure.Identity;

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            if (context.HostingEnvironment.IsProduction())
            {
                var builtConfig = config.Build();

                using var store = new X509Store(StoreLocation.CurrentUser);
                store.Open(OpenFlags.ReadOnly);
                var certs = store.Certificates.Find(
                    X509FindType.FindByThumbprint,
                    builtConfig["AzureADCertThumbprint"], false);

                config.AddAzureKeyVault(new Uri($"https://{builtConfig["KeyVaultName"]}.vault.azure.net/"),
                                        new ClientCertificateCredential(builtConfig["AzureADDirectoryId"], builtConfig["AzureADApplicationId"], certs.OfType<X509Certificate2>().Single()),
                                        new KeyVaultSecretManager());

                store.Close();
            }
        })
        .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
```

Example values:

* Key Vault name: `contosovault`
* Application ID: `00001111-aaaa-2222-bbbb-3333cccc4444`
* Certificate thumbprint: `fe14593dd66b2406c5269d742d04b6e1ab03adb1`

`appsettings.json`:

```
{
  "KeyVaultName": "Key Vault Name",
  "AzureADApplicationId": "Azure AD Application ID",
  "AzureADCertThumbprint": "Azure AD Certificate Thumbprint",
  "AzureADDirectoryId": "Azure AD Directory ID"
}
```

When you run the app, a webpage shows the loaded secret values. In the `Development` environment, secret values load with the `_dev` suffix. In the `Production` environment, the values load with the `_prod` suffix.

## Use managed identities for Azure resources

**An app deployed to Azure** can take advantage of [Managed identities for Azure resources](/en-us/azure/active-directory/managed-identities-azure-resources/overview). A managed identity allows the app to authenticate with Azure Key Vault using Microsoft Entra ID authentication without credentials (Application ID and Password/Client Secret) stored in the app.

The sample app uses managed identities for Azure resources when the `#define` preprocessor directive at the top of `Program.cs` is set to `Managed`.

Enter the vault name into the app's `appsettings.json` file. The sample app doesn't require an Application ID and Password (Client Secret) when set to the `Managed` version, so you can ignore those configuration entries. The app is deployed to Azure, and Azure authenticates the app to access Azure Key Vault only using the vault name stored in the `appsettings.json` file.

Deploy the sample app to Azure App Service.

An app deployed to Azure App Service is automatically registered with Microsoft Entra ID when the service is created. Obtain the Object ID from the deployment for use in the following command. The Object ID is shown in the Azure portal on the **Identity** panel of the App Service.

Using Azure CLI and the app's Object ID, provide the app with `list` and `get` permissions to access the vault:

```
az keyvault set-policy --name {KEY VAULT NAME} --object-id {OBJECT ID} --secret-permissions get list
```

**Restart the app** using Azure CLI, PowerShell, or the Azure portal.

The sample app:

* Creates an instance of the [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) class. The credential attempts to obtain an access token from environment for Azure resources.
* A new [SecretClient](/en-us/dotnet/api/azure.security.keyvault.secrets.secretclient) is created with the `DefaultAzureCredential` instance.
* The `SecretClient` instance is used with a [KeyVaultSecretManager](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.keyvaultsecretmanager) instance, which loads secret values and replaces double-dashes (`--`) with colons (`:`) in key names.

```
// using Azure.Security.KeyVault.Secrets;
// using Azure.Identity;
// using Azure.Extensions.AspNetCore.Configuration.Secrets;

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            if (context.HostingEnvironment.IsProduction())
            {
                var builtConfig = config.Build();
                var secretClient = new SecretClient(
                    new Uri($"https://{builtConfig["KeyVaultName"]}.vault.azure.net/"),
                    new DefaultAzureCredential());
                config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager());
            }
        })
        .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
```

Note

The preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential). For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

Key Vault name example value: `contosovault`

`appsettings.json`:

```
{
  "KeyVaultName": "Key Vault Name"
}
```

When you run the app, a webpage shows the loaded secret values. In the `Development` environment, secret values have the `_dev` suffix because they're provided by Secret Manager. In the `Production` environment, the values load with the `_prod` suffix because they're provided by Azure Key Vault.

If you receive an `Access denied` error, confirm that the app is registered with Microsoft Entra ID and provided access to the vault. Confirm that you've restarted the service in Azure.

For information on using the provider with a managed identity and Azure Pipelines, see [Create an Azure Resource Manager service connection to a VM with a managed service identity](/en-us/azure/devops/pipelines/library/connect-to-azure#create-an-azure-resource-manager-service-connection-to-a-vm-with-a-managed-service-identity).

## Configuration options

`AddAzureKeyVault` can accept an [AzureKeyVaultConfigurationOptions](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.azurekeyvaultconfigurationoptions) object:

```
config.AddAzureKeyVault(
    new SecretClient(
        new Uri("Your Key Vault Endpoint"),
        new DefaultAzureCredential(),
        new AzureKeyVaultConfigurationOptions())
    {
        ...
    });
```

Note

The preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential). For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

The `AzureKeyVaultConfigurationOptions` object contains the following properties.

| Property                                                                                                                               | Description                                                                                                                                                 |
| -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Manager](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.azurekeyvaultconfigurationoptions.manager)               | [KeyVaultSecretManager](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.keyvaultsecretmanager) instance used to control secret loading. |
| [ReloadInterval](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.azurekeyvaultconfigurationoptions.reloadinterval) | `TimeSpan` to wait between attempts at polling the vault for changes. The default value is `null` (configuration isn't reloaded).                           |

## Use a key name prefix

`AddAzureKeyVault` provides an overload that accepts an implementation of [KeyVaultSecretManager](/en-us/dotnet/api/azure.extensions.aspnetcore.configuration.secrets.keyvaultsecretmanager), which allows you to control how Key Vault secrets are converted into configuration keys. For example, you can implement the interface to load secret values based on a prefix value you provide at app startup. This technique allows you, for example, to load secrets based on the version of the app.

Warning

Don't use prefixes on Key Vault secrets to:

* Place secrets for multiple apps into the same vault.
* Place environmental secrets (for example, *development* versus *production* secrets) into the same vault.

Different apps and development/production environments should use separate Key Vaults to isolate app environments for the highest level of security.

In the following example, a secret is established in Key Vault (and using Secret Manager for the `Development` environment) for `5000-AppSecret` (periods aren't allowed in Key Vault secret names). This secret represents an app secret for version 5.0.0.0 of the app. For another version of the app, 5.1.0.0, a secret is added to the vault (and using Secret Manager) for `5100-AppSecret`. Each app version loads its versioned secret value into its configuration as `AppSecret`, removing the version as it loads the secret.

`AddAzureKeyVault` is called with a custom `KeyVaultSecretManager` implementation:

```
config.AddAzureKeyVault(
    $"https://{builtConfig["KeyVaultName"]}.vault.azure.net/",
    builtConfig["AzureADApplicationId"],
    certs.OfType<X509Certificate2>().Single(),
    new PrefixKeyVaultSecretManager(versionPrefix));
```

The implementation reacts to the version prefixes of secrets to load the proper secret into configuration:

* `Load` loads a secret when its name starts with the prefix. Other secrets aren't loaded.

* `GetKey`:

  * Removes the prefix from the secret name.
  * Replaces two dashes in any name with the `KeyDelimiter`, which is the delimiter used in configuration (usually a colon). Azure Key Vault doesn't allow a colon in secret names.

```
public class PrefixKeyVaultSecretManager : KeyVaultSecretManager
{
    private readonly string _prefix;

    public PrefixKeyVaultSecretManager(string prefix)
    {
        _prefix = $"{prefix}-";
    }

    public override bool Load(SecretProperties secret)
    {
        return secret.Name.StartsWith(_prefix);
    }

    public override string GetKey(KeyVaultSecret secret)
    {
        return secret.Name
            .Substring(_prefix.Length)
            .Replace("--", ConfigurationPath.KeyDelimiter);
    }
}
```

The `Load` method is called by a provider algorithm that iterates through the vault secrets to find the version-prefixed secrets. When a version prefix is found with `Load`, the algorithm uses the `GetKey` method to return the configuration name of the secret name. It removes the version prefix from the secret's name. The rest of the secret name is returned for loading into the app's configuration name-value pairs.

When this approach is implemented:

1. The app's version specified in the app's project file. In the following example, the app's version is set to `5.0.0.0`:

   ```
   <PropertyGroup>
     <Version>5.0.0.0</Version>
   </PropertyGroup>
   ```

2. Confirm that a `<UserSecretsId>` property is present in the app's project file, where `{GUID}` is a user-supplied GUID:

   ```
   <PropertyGroup>
     <UserSecretsId>{GUID}</UserSecretsId>
   </PropertyGroup>
   ```

   Save the following secrets locally with [Secret Manager](app-secrets?view=aspnetcore-10.0):

   ```
   dotnet user-secrets set "5000-AppSecret" "5.0.0.0_secret_value_dev"
   dotnet user-secrets set "5100-AppSecret" "5.1.0.0_secret_value_dev"
   ```

3. Secrets are saved in Azure Key Vault using the following Azure CLI commands:

   ```
   az keyvault secret set --vault-name {KEY VAULT NAME} --name "5000-AppSecret" --value "5.0.0.0_secret_value_prod"
   az keyvault secret set --vault-name {KEY VAULT NAME} --name "5100-AppSecret" --value "5.1.0.0_secret_value_prod"
   ```

4. When the app is run, the Key Vault secrets are loaded. The string secret for `5000-AppSecret` is matched to the app's version specified in the app's project file (`5.0.0.0`).

5. The version, `5000` (with the dash), is stripped from the key name. Throughout the app, reading configuration with the key `AppSecret` loads the secret value.

6. If the app's version is changed in the project file to `5.1.0.0` and the app is run again, the secret value returned is `5.1.0.0_secret_value_dev` in the `Development` environment and `5.1.0.0_secret_value_prod` in `Production`.

Note

You can also provide your own [SecretClient](/en-us/dotnet/api/azure.security.keyvault.secrets.secretclient) implementation to `AddAzureKeyVault`. A custom client permits sharing a single instance of the client across the app.

## Bind an array to a class

The provider can read configuration values into an array for binding to a POCO array.

When reading from a configuration source that allows keys to contain colon (`:`) separators, a numeric key segment is used to distinguish the keys that make up an array (`:0:`, `:1:`, … `:{n}:`). For more information, see [Configuration: Bind an array to a class](../fundamentals/configuration/?view=aspnetcore-10.0#bind-an-array).

Azure Key Vault keys can't use a colon as a separator. The approach described in this article uses double dashes (`--`) as a separator for hierarchical values (sections). Array keys are stored in Azure Key Vault with double dashes and numeric key segments (`--0--`, `--1--`, … `--{n}--`).

Examine the following [Serilog](https://serilog.net/) logging provider configuration provided by a JSON file. There are two object literals defined in the `WriteTo` array that reflect two Serilog *sinks*, which describe destinations for logging output:

```
"Serilog": {
  "WriteTo": [
    {
      "Name": "AzureTableStorage",
      "Args": {
        "storageTableName": "logs",
        "connectionString": "DefaultEnd...ountKey=Eby8...GMGw=="
      }
    },
    {
      "Name": "AzureDocumentDB",
      "Args": {
        "endpointUrl": "https://contoso.documents.azure.com:443",
        "authorizationKey": "Eby8...GMGw=="
      }
    }
  ]
}
```

The configuration shown in the preceding JSON file is stored in Azure Key Vault using double dash (`--`) notation and numeric segments:

| Key                                           | Value                                     |
| --------------------------------------------- | ----------------------------------------- |
| `Serilog--WriteTo--0--Name`                   | `AzureTableStorage`                       |
| `Serilog--WriteTo--0--Args--storageTableName` | `logs`                                    |
| `Serilog--WriteTo--0--Args--connectionString` | `DefaultEnd...ountKey=Eby8...GMGw==`      |
| `Serilog--WriteTo--1--Name`                   | `AzureDocumentDB`                         |
| `Serilog--WriteTo--1--Args--endpointUrl`      | `https://contoso.documents.azure.com:443` |
| `Serilog--WriteTo--1--Args--authorizationKey` | `Eby8...GMGw==`                           |

## Reload secrets

Secrets are cached until [IConfigurationRoot.Reload](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationroot.reload) is called. Subsequently disabled or updated secrets in the vault aren't respected by the app until `Reload` is executed.

```
Configuration.Reload();
```

## Disabled and expired secrets

Expired secrets are included by default in the configuration provider. To exclude values for these secrets in app configuration, update the expired secret or provide the configuration using a custom configuration provider:

```
class SampleKeyVaultSecretManager : KeyVaultSecretManager
{
  public override bool Load(SecretProperties properties) =>
    properties.ExpiresOn.HasValue &&
    properties.ExpiresOn.Value > DateTimeOffset.Now;
}
```

Pass this custom `KeyVaultSecretManager` to `AddAzureKeyVault`:

```
// using Azure.Extensions.AspNetCore.Configuration.Secrets;

config.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
    new DefaultAzureCredential(),
    new SampleKeyVaultSecretManager());
```

Disabled secrets cannot be retrieved from Key Vault and are never included.

Note

The preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential). For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

## Troubleshoot

When the app fails to load configuration using the provider, an error message is written to the [ASP.NET Core Logging infrastructure](../fundamentals/logging/?view=aspnetcore-10.0). The following conditions will prevent configuration from loading:

* The app or certificate isn't configured correctly in Microsoft Entra ID.
* The vault doesn't exist in Azure Key Vault.
* The app isn't authorized to access the vault.
* The access policy doesn't include `Get` and `List` permissions.
* In the vault, the configuration data (name-value pair) is incorrectly named, missing, or disabled.
* The app has the wrong Key Vault name (`KeyVaultName`), Microsoft Entra ID Application ID (`AzureADApplicationId`), or Microsoft Entra ID certificate thumbprint (`AzureADCertThumbprint`), or Microsoft Entra ID Directory ID (`AzureADDirectoryId`).
* When adding the Key Vault access policy for the app, the policy was created, but the **Save** button wasn't selected in the **Access policies** UI.

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/key-vault-configuration/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Configuration in ASP.NET Core](../fundamentals/configuration/?view=aspnetcore-10.0)
* [Microsoft Azure: Key Vault Documentation](/en-us/azure/key-vault/)
* [How to generate and transfer HSM-protected keys for Azure Key Vault](/en-us/azure/key-vault/key-vault-hsm-protected-keys)
* [Quickstart: Set and retrieve a secret from Azure Key Vault by using a .NET web app](/en-us/azure/key-vault/quick-create-net)
* [Tutorial: How to use Azure Key Vault with Azure Windows Virtual Machine in .NET](/en-us/azure/key-vault/tutorial-net-windows-virtual-machine)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/concurrency
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-rp/concurrency.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 8, Razor Pages with EF Core in ASP.NET Core - Concurrency

[Tom Dykstra](https://github.com/tdykstra), and [Jon P Smith](https://twitter.com/thereformedprog)

The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. For information about the tutorial series, see [the first tutorial](intro?view=aspnetcore-10.0).

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) and compare that code to what you created by following the tutorial.

This tutorial shows how to handle conflicts when multiple users update an entity concurrently.

## Concurrency conflicts

A concurrency conflict occurs when:

* A user navigates to the edit page for an entity.
* Another user updates the same entity before the first user's change is written to the database.

If concurrency detection isn't enabled, whoever updates the database last overwrites the other user's changes. If this risk is acceptable, the cost of programming for concurrency might outweigh the benefit.

### Pessimistic concurrency

One way to prevent concurrency conflicts is to use database locks. This is called pessimistic concurrency. Before the app reads a database row that it intends to update, it requests a lock. Once a row is locked for update access, no other users are allowed to lock the row until the first lock is released.

Managing locks has disadvantages. It can be complex to program and can cause performance problems as the number of users increases. Entity Framework Core provides no built-in support for pessimistic concurrency.

### Optimistic concurrency

Optimistic concurrency allows concurrency conflicts to happen, and then reacts appropriately when they do. For example, Jane visits the Department edit page and changes the budget for the English department from $350,000.00 to $0.00.

Before Jane clicks **Save**, John visits the same page and changes the Start Date field from 9/1/2007 to 9/1/2013.

Jane clicks **Save** first and sees her change take effect, since the browser displays the Index page with zero as the Budget amount.

John clicks **Save** on an Edit page that still shows a budget of $350,000.00. What happens next is determined by how you handle concurrency conflicts:

* Keep track of which property a user has modified and update only the corresponding columns in the database.

  In the scenario, no data would be lost. Different properties were updated by the two users. The next time someone browses the English department, they will see both Jane's and John's changes. This method of updating can reduce the number of conflicts that could result in data loss. This approach has some disadvantages:

  * Can't avoid data loss if competing changes are made to the same property.
  * Is generally not practical in a web app. It requires maintaining significant state in order to keep track of all fetched values and new values. Maintaining large amounts of state can affect app performance.
  * Can increase app complexity compared to concurrency detection on an entity.

* Let John's change overwrite Jane's change.

  The next time someone browses the English department, they will see 9/1/2013 and the fetched $350,000.00 value. This approach is called a *Client Wins* or *Last in Wins* scenario. All values from the client take precedence over what's in the data store. The scaffolded code does no concurrency handling, Client Wins happens automatically.

* Prevent John's change from being updated in the database. Typically, the app would:

  * Display an error message.
  * Show the current state of the data.
  * Allow the user to reapply the changes.

  This is called a *Store Wins* scenario. The data-store values take precedence over the values submitted by the client. The Store Wins scenario is used in this tutorial. This method ensures that no changes are overwritten without a user being alerted.

[]()

## Conflict detection in EF Core

Properties configured as [concurrency tokens](/en-us/ef/core/modeling/concurrency) are used to implement optimistic concurrency control. When an update or delete operation is triggered by [SaveChanges](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechanges) or [SaveChangesAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechangesasync), the value of the concurrency token in the database is compared against the original value read by EF Core:

* If the values match, the operation can complete.
* If the values do not match, EF Core assumes that another user has performed a conflicting operation, aborts the current transaction, and throws a [DbUpdateConcurrencyException](/en-us/dotnet/api/microsoft.entityframeworkcore.dbupdateconcurrencyexception).

Another user or process performing an operation that conflicts with the current operation is known as *concurrency conflict*.

On relational databases EF Core checks for the value of the concurrency token in the `WHERE` clause of `UPDATE` and `DELETE` statements to detect a concurrency conflict.

The data model must be configured to enable conflict detection by including a tracking column that can be used to determine when a row has been changed. EF provides two approaches for [concurrency tokens](/en-us/ef/core/modeling/concurrency):

* Applying [`[ConcurrencyCheck]`](/en-us/dotnet/api/system.componentmodel.dataannotations.concurrencycheckattribute) or [IsConcurrencyToken](/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.iproperty.isconcurrencytoken#microsoft-entityframeworkcore-metadata-iproperty-isconcurrencytoken) to a property on the model. This approach is not recommended. For more information, see [Concurrency Tokens in EF Core](/en-us/ef/core/modeling/concurrency).

* Applying [TimestampAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.timestampattribute) or [IsRowVersion](/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.builders.propertybuilder.isrowversion) to a concurrency token in the model. This is the approach used in this tutorial.

The SQL Server approach and SQLite implementation details are slightly different. A difference file is shown later in the tutorial listing the differences. The Visual Studio tab shows the SQL Server approach. The Visual Studio Code tab shows the approach for non-SQL Server databases, such as SQLite.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

- In the model, include a tracking column that is used to determine when a row has been changed.
- Apply the [TimestampAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.timestampattribute) to the concurrency property.

Update the `Models/Department.cs` file with the following highlighted code:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
                       ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        [Timestamp]
        public byte[] ConcurrencyToken { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}
```

The [TimestampAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.timestampattribute) is what identifies the column as a concurrency tracking column. The fluent API is an alternative way to specify the tracking property:

```
modelBuilder.Entity<Department>()
  .Property<byte[]>("ConcurrencyToken")
  .IsRowVersion();
```

The [`[Timestamp]`](/en-us/dotnet/api/system.componentmodel.dataannotations.timestampattribute) attribute on an entity property generates the following code in the [ModelBuilder](/en-us/dotnet/api/microsoft.entityframeworkcore.modelbuilder) method:

```
 b.Property<byte[]>("ConcurrencyToken")
     .IsConcurrencyToken()
     .ValueGeneratedOnAddOrUpdate()
     .HasColumnType("rowversion");
```

The preceding code:

* Sets the property type `ConcurrencyToken` to byte array. `byte[]` is the required type for SQL Server.
* Calls [IsConcurrencyToken](/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.builders.propertybuilder.isconcurrencytoken). `IsConcurrencyToken` configures the property as a concurrency token. On updates, the concurrency token value in the database is compared to the original value to ensure it has not changed since the instance was retrieved from the database. If it has changed, a [DbUpdateConcurrencyException](/en-us/dotnet/api/microsoft.entityframeworkcore.dbupdateconcurrencyexception) is thrown and changes are not applied.
* Calls [ValueGeneratedOnAddOrUpdate](/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.builders.propertybuilder.valuegeneratedonaddorupdate), which configures the `ConcurrencyToken` property to have a value automatically generated when adding or updating an entity.
* `HasColumnType("rowversion")` sets the column type in the SQL Server database to [rowversion](/en-us/sql/t-sql/data-types/rowversion-transact-sql).

The following code shows a portion of the T-SQL generated by EF Core when the `Department` name is updated:

```
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
```

The preceding highlighted code shows the `WHERE` clause containing `ConcurrencyToken`. If the database `ConcurrencyToken` doesn't equal the `ConcurrencyToken` parameter `@p2`, no rows are updated.

The following highlighted code shows the T-SQL that verifies exactly one row was updated:

```
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
```

[@@ROWCOUNT](/en-us/sql/t-sql/functions/rowcount-transact-sql) returns the number of rows affected by the last statement. If no rows are updated, EF Core throws a `DbUpdateConcurrencyException`.

* In the model, include a tracking column that can be used to determine when a row has been changed. In this sample, a `GUID` is used. Update the `Models/Department.cs` file with the following highlighted code:

  ```
  using System;
  using System.Collections.Generic;
  using System.ComponentModel.DataAnnotations;
  using System.ComponentModel.DataAnnotations.Schema;

  namespace ContosoUniversity.Models
  {
      public class Department
      {
          public int DepartmentID { get; set; }

          [StringLength(50, MinimumLength = 3)]
          public string Name { get; set; }

          [DataType(DataType.Currency)]
          [Column(TypeName = "money")]
          public decimal Budget { get; set; }

          [DataType(DataType.Date)]
          [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
                         ApplyFormatInEditMode = true)]
          [Display(Name = "Start Date")]
          public DateTime StartDate { get; set; }

          public int? InstructorID { get; set; }

          public Guid ConcurrencyToken { get; set; } = Guid.NewGuid();

          public Instructor Administrator { get; set; }
          public ICollection<Course> Courses { get; set; }
      }
  }
  ```

* Update the `Data/SchoolContext.cs` file by calling [IsConcurrencyToken](/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.iproperty.isconcurrencytoken#microsoft-entityframeworkcore-metadata-iproperty-isconcurrencytoken) on the `Department.ConcurrencyToken` property:

  ```
  public class SchoolContext : DbContext
  {
      public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
      {
      }

      public DbSet<Course> Courses { get; set; }
      public DbSet<Enrollment> Enrollments { get; set; }
      public DbSet<Student> Students { get; set; }
      public DbSet<Department> Departments { get; set; }
      public DbSet<Instructor> Instructors { get; set; }
      public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
      
      protected override void OnModelCreating(ModelBuilder modelBuilder)
      {
          modelBuilder.Entity<Course>().ToTable(nameof(Course))
              .HasMany(c => c.Instructors)
              .WithMany(i => i.Courses);
          modelBuilder.Entity<Student>().ToTable(nameof(Student));
          modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
          modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
          modelBuilder.Entity<Department>()
              .Property(d => d.ConcurrencyToken)
              .IsConcurrencyToken();
      }
  }
  ```

  `IsConcurrencyToken` configures the property as a concurrency token. On updates, the concurrency token value in the database is compared to the original value to ensure it has not changed since the instance was retrieved from the database. If it has changed, a [DbUpdateConcurrencyException](/en-us/dotnet/api/microsoft.entityframeworkcore.dbupdateconcurrencyexception) is thrown and changes are not applied.

* In the code that updates the entity, update the value of the concurrency token. In this sample, the method that updates the entity uses the following code. This step will be done later in the tutorial.

  ```
  departmentToUpdate.ConcurrencyToken = Guid.NewGuid();
  ```

An alternative to updating the concurrency token in the update method is to set up a trigger so it's automatically updated by the database. For more information, see [SQLite and EF Core Concurrency Tokens](https://www.bricelam.net/2020/08/07/sqlite-and-efcore-concurrency-tokens.html).

## Add a migration

Adding the `ConcurrencyToken` property changes the data model, which requires a migration.

Build the project.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

Run the following commands in the PMC:

```
Add-Migration RowVersion
Update-Database
```

The preceding commands:

* Creates the `Migrations/{time stamp}_RowVersion.cs` migration file.
* Updates the `Migrations/SchoolContextModelSnapshot.cs` file. The update adds the following code to the `BuildModel` method:

```
 b.Property<byte[]>("ConcurrencyToken")
     .IsConcurrencyToken()
     .ValueGeneratedOnAddOrUpdate()
     .HasColumnType("rowversion");
```

Run the following commands in a terminal:

```
dotnet ef migrations add RowVersion
dotnet ef database update
```

The preceding commands:

* Creates the `Migrations/{time stamp}_RowVersion.cs` migration file.
* Updates the `Migrations/SchoolContextModelSnapshot.cs` file. The update adds the following highlighted code to the `BuildModel` method:

```
b.Property<Guid>("ConcurrencyToken")
    .IsConcurrencyToken()
    .HasColumnType("TEXT");
```

[]()

## Scaffold Department pages

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Follow the instructions in [Scaffold Student pages](intro?view=aspnetcore-10.0#scaffold-student-pages) with the following exceptions:

* Create a *Pages/Departments* folder.
* Use `Department` for the model class.
* Use the existing context class instead of creating a new one.

- Create a *Pages/Departments* folder.

- Run the following command to scaffold the Department pages.

  **On Windows:**

  ```
  dotnet aspnet-codegenerator razorpage -m Department -dc SchoolContext -udl -outDir Pages\Departments --referenceScriptLibraries
  ```

  **On Linux or macOS:**

  ```
  dotnet aspnet-codegenerator razorpage -m Department -dc SchoolContext -udl -outDir Pages/Departments --referenceScriptLibraries
  ```

### Add a utility class

In the project folder, create the `Utility` class with the following code:

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

```
namespace ContosoUniversity
{
    public static class Utility
    {
        public static string GetLastChars(byte[] token)
        {
            return token[7].ToString();
        }
    }
}
```

```
using System;

namespace ContosoUniversity
{
    public static class Utility
    {
        public static string GetLastChars(Guid token)
        {
            return token.ToString().Substring(
                                    token.ToString().Length - 3);
        }
    }
}
```

The `Utility` class provides the `GetLastChars` method used to display the last few characters of the concurrency token. The following code shows the code that works with both SQLite ad SQL Server:

```
#if SQLiteVersion
using System;

namespace ContosoUniversity
{
    public static class Utility
    {
        public static string GetLastChars(Guid token)
        {
            return token.ToString().Substring(
                                    token.ToString().Length - 3);
        }
    }
}
#else
namespace ContosoUniversity
{
    public static class Utility
    {
        public static string GetLastChars(byte[] token)
        {
            return token[7].ToString();
        }
    }
}
#endif
```

The `#if SQLiteVersion` [preprocessor directive](/en-us/dotnet/csharp/language-reference/preprocessor-directives/) isolates the differences in the SQLite and SQL Server versions and helps:

* The author maintain one code base for both versions.
* SQLite developers deploy the app to Azure and use SQL Azure.

Build the project.

## Update the Index page

The scaffolding tool created a `ConcurrencyToken` column for the Index page, but that field wouldn't be displayed in a production app. In this tutorial, the last portion of the `ConcurrencyToken` is displayed to help show how concurrency handling works. The last portion isn't guaranteed to be unique by itself.

Update *Pages\Departments\Index.cshtml* page:

* Replace Index with Departments.
* Change the code containing `ConcurrencyToken` to show just the last few characters.
* Replace `FirstMidName` with `FullName`.

The following code shows the updated page:

```
@page
@model ContosoUniversity.Pages.Departments.IndexModel

@{
    ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Budget)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].StartDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Administrator)
            </th>
            <th>
                Token
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Department)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    @Utility.GetLastChars(item.ConcurrencyToken)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

## Update the Edit page model

Update `Pages/Departments/Edit.cshtml.cs` with the following code:

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            // Fetch current department from DB.
            // ConcurrencyToken may have changed.
            var departmentToUpdate = await _context.Departments
                .Include(i => i.Administrator)
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            // Set ConcurrencyToken to value read in OnGetAsync
            _context.Entry(departmentToUpdate).Property(
                 d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await SetDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current ConcurrencyToken so next postback
                    // matches unless an new concurrency issue happens.
                    Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
                    // Clear the model error for the next postback.
                    ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            // ModelState contains the posted data because of the deletion error
            // and overides the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

        private async Task SetDbErrorMessage(Department dbValues,
                Department clientValues, SchoolContext context)
        {

            if (dbValues.Name != clientValues.Name)
            {
                ModelState.AddModelError("Department.Name",
                    $"Current value: {dbValues.Name}");
            }
            if (dbValues.Budget != clientValues.Budget)
            {
                ModelState.AddModelError("Department.Budget",
                    $"Current value: {dbValues.Budget:c}");
            }
            if (dbValues.StartDate != clientValues.StartDate)
            {
                ModelState.AddModelError("Department.StartDate",
                    $"Current value: {dbValues.StartDate:d}");
            }
            if (dbValues.InstructorID != clientValues.InstructorID)
            {
                Instructor dbInstructor = await _context.Instructors
                   .FindAsync(dbValues.InstructorID);
                ModelState.AddModelError("Department.InstructorID",
                    $"Current value: {dbInstructor?.FullName}");
            }

            ModelState.AddModelError(string.Empty,
                "The record you attempted to edit "
              + "was modified by another user after you. The "
              + "edit operation was canceled and the current values in the database "
              + "have been displayed. If you still want to edit this record, click "
              + "the Save button again.");
        }
    }
}
```

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System;                // For GUID in SQLite version
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            // Fetch current department from DB.
            // ConcurrencyToken may have changed.
            var departmentToUpdate = await _context.Departments
                .Include(i => i.Administrator)
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            departmentToUpdate.ConcurrencyToken = Guid.NewGuid();

            // Set ConcurrencyToken to value read in OnGetAsync
            _context.Entry(departmentToUpdate).Property(d => d.ConcurrencyToken)
                                   .OriginalValue = Department.ConcurrencyToken;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await SetDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current ConcurrencyToken so next postback
                    // matches unless an new concurrency issue happens.
                    Department.ConcurrencyToken = dbValues.ConcurrencyToken;
                    // Clear the model error for the next postback.
                    ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            // ModelState contains the posted data because of the deletion error
            // and overides the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

        private async Task SetDbErrorMessage(Department dbValues,
                Department clientValues, SchoolContext context)
        {

            if (dbValues.Name != clientValues.Name)
            {
                ModelState.AddModelError("Department.Name",
                    $"Current value: {dbValues.Name}");
            }
            if (dbValues.Budget != clientValues.Budget)
            {
                ModelState.AddModelError("Department.Budget",
                    $"Current value: {dbValues.Budget:c}");
            }
            if (dbValues.StartDate != clientValues.StartDate)
            {
                ModelState.AddModelError("Department.StartDate",
                    $"Current value: {dbValues.StartDate:d}");
            }
            if (dbValues.InstructorID != clientValues.InstructorID)
            {
                Instructor dbInstructor = await _context.Instructors
                   .FindAsync(dbValues.InstructorID);
                ModelState.AddModelError("Department.InstructorID",
                    $"Current value: {dbInstructor?.FullName}");
            }

            ModelState.AddModelError(string.Empty,
                "The record you attempted to edit "
              + "was modified by another user after you. The "
              + "edit operation was canceled and the current values in the database "
              + "have been displayed. If you still want to edit this record, click "
              + "the Save button again.");
        }
    }
}
```

[]()

### The concurrency updates

[OriginalValue](/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyentry.originalvalue#microsoft-entityframeworkcore-changetracking-propertyentry-originalvalue) is updated with the `ConcurrencyToken` value from the entity when it was fetched in the `OnGetAsync` method. EF Core generates a `SQL UPDATE` command with a `WHERE` clause containing the original `ConcurrencyToken` value. If no rows are affected by the `UPDATE` command, a `DbUpdateConcurrencyException` exception is thrown. No rows are affected by the `UPDATE` command when no rows have the original `ConcurrencyToken` value.

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

```
public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // Fetch current department from DB.
    // ConcurrencyToken may have changed.
    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    // Set ConcurrencyToken to value read in OnGetAsync
    _context.Entry(departmentToUpdate).Property(
         d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
```

```
public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // Fetch current department from DB.
    // ConcurrencyToken may have changed.
    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    departmentToUpdate.ConcurrencyToken = Guid.NewGuid();

    // Set ConcurrencyToken to value read in OnGetAsync
    _context.Entry(departmentToUpdate).Property(d => d.ConcurrencyToken)
                           .OriginalValue = Department.ConcurrencyToken;
```

In the preceding highlighted code:

* The value in `Department.ConcurrencyToken` is the value when the entity was fetched in the `Get` request for the `Edit` page. The value is provided to the `OnPost` method by a hidden field in the Razor page that displays the entity to be edited. The hidden field value is copied to `Department.ConcurrencyToken` by the model binder.

* `OriginalValue` is what EF Core uses in the `WHERE` clause. Before the highlighted line of code executes:

  * `OriginalValue` has the value that was in the database when `FirstOrDefaultAsync` was called in this method.
  * This value might be different from what was displayed on the Edit page.

* The highlighted code makes sure that EF Core uses the original `ConcurrencyToken` value from the displayed `Department` entity in the SQL `UPDATE` statement's `WHERE` clause.

The following code shows the `Department` model. `Department` is initialized in the:

* `OnGetAsync` method by the EF query.
* `OnPostAsync` method by the hidden field in the Razor page using [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0):

- [Visual Studio](#tabpanel_7_visual-studio)
- [Visual Studio Code](#tabpanel_7_visual-studio-code)

```
public class EditModel : PageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public EditModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Department Department { get; set; }
    // Replace ViewData["InstructorID"] 
    public SelectList InstructorNameSL { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Department = await _context.Departments
            .Include(d => d.Administrator)  // eager loading
            .AsNoTracking()                 // tracking not required
            .FirstOrDefaultAsync(m => m.DepartmentID == id);

        if (Department == null)
        {
            return NotFound();
        }

        // Use strongly typed data rather than ViewData.
        InstructorNameSL = new SelectList(_context.Instructors,
            "ID", "FirstMidName");

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch current department from DB.
        // ConcurrencyToken may have changed.
        var departmentToUpdate = await _context.Departments
            .Include(i => i.Administrator)
            .FirstOrDefaultAsync(m => m.DepartmentID == id);

        if (departmentToUpdate == null)
        {
            return HandleDeletedDepartment();
        }

        // Set ConcurrencyToken to value read in OnGetAsync
        _context.Entry(departmentToUpdate).Property(
             d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;
```

```
public class EditModel : PageModel
{
    private readonly ContosoUniversity.Data.SchoolContext _context;

    public EditModel(ContosoUniversity.Data.SchoolContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Department Department { get; set; }
    // Replace ViewData["InstructorID"] 
    public SelectList InstructorNameSL { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Department = await _context.Departments
            .Include(d => d.Administrator)  // eager loading
            .AsNoTracking()                 // tracking not required
            .FirstOrDefaultAsync(m => m.DepartmentID == id);

        if (Department == null)
        {
            return NotFound();
        }

        // Use strongly typed data rather than ViewData.
        InstructorNameSL = new SelectList(_context.Instructors,
            "ID", "FirstMidName");

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch current department from DB.
        // ConcurrencyToken may have changed.
        var departmentToUpdate = await _context.Departments
            .Include(i => i.Administrator)
            .FirstOrDefaultAsync(m => m.DepartmentID == id);

        if (departmentToUpdate == null)
        {
            return HandleDeletedDepartment();
        }

        departmentToUpdate.ConcurrencyToken = Guid.NewGuid();

        // Set ConcurrencyToken to value read in OnGetAsync
        _context.Entry(departmentToUpdate).Property(d => d.ConcurrencyToken)
                               .OriginalValue = Department.ConcurrencyToken;
```

The preceding code shows the `ConcurrencyToken` value of the `Department` entity from the `HTTP POST` request is set to the `ConcurrencyToken` value from the `HTTP GET` request.

When a concurrency error happens, the following highlighted code gets the client values (the values posted to this method) and the database values.

```
if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await SetDbErrorMessage(dbValues, clientValues, _context);

        // Save the current ConcurrencyToken so next postback
        // matches unless an new concurrency issue happens.
        Department.ConcurrencyToken = dbValues.ConcurrencyToken;
        // Clear the model error for the next postback.
        ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
    }
```

The following code adds a custom error message for each column that has database values different from what was posted to `OnPostAsync`:

```
private async Task SetDbErrorMessage(Department dbValues,
        Department clientValues, SchoolContext context)
{

    if (dbValues.Name != clientValues.Name)
    {
        ModelState.AddModelError("Department.Name",
            $"Current value: {dbValues.Name}");
    }
    if (dbValues.Budget != clientValues.Budget)
    {
        ModelState.AddModelError("Department.Budget",
            $"Current value: {dbValues.Budget:c}");
    }
    if (dbValues.StartDate != clientValues.StartDate)
    {
        ModelState.AddModelError("Department.StartDate",
            $"Current value: {dbValues.StartDate:d}");
    }
    if (dbValues.InstructorID != clientValues.InstructorID)
    {
        Instructor dbInstructor = await _context.Instructors
           .FindAsync(dbValues.InstructorID);
        ModelState.AddModelError("Department.InstructorID",
            $"Current value: {dbInstructor?.FullName}");
    }

    ModelState.AddModelError(string.Empty,
        "The record you attempted to edit "
      + "was modified by another user after you. The "
      + "edit operation was canceled and the current values in the database "
      + "have been displayed. If you still want to edit this record, click "
      + "the Save button again.");
}
```

The following highlighted code sets the `ConcurrencyToken` value to the new value retrieved from the database. The next time the user clicks **Save**, only concurrency errors that happen since the last display of the Edit page will be caught.

```
if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await SetDbErrorMessage(dbValues, clientValues, _context);

        // Save the current ConcurrencyToken so next postback
        // matches unless an new concurrency issue happens.
        Department.ConcurrencyToken = dbValues.ConcurrencyToken;
        // Clear the model error for the next postback.
        ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
    }
```

The [`ModelState.Remove`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.remove) statement is required because `ModelState` has the previous `ConcurrencyToken` value. In the Razor Page, the `ModelState` value for a field takes precedence over the model property values when both are present.

### SQL Server vs SQLite code differences

The following shows the differences between the SQL Server and SQLite versions:

```
+ using System;    // For GUID on SQLite

+ departmentToUpdate.ConcurrencyToken = Guid.NewGuid();

 _context.Entry(departmentToUpdate)
    .Property(d => d.ConcurrencyToken).OriginalValue = Department.ConcurrencyToken;

- Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
+ Department.ConcurrencyToken = dbValues.ConcurrencyToken;
```

### Update the Edit Razor page

Update `Pages/Departments/Edit.cshtml` with the following code:

```
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.ConcurrencyToken" />
            <div class="form-group">
                <label>Version</label>
                @Utility.GetLastChars(Model.Department.ConcurrencyToken)
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding code:

* Updates the `page` directive from `@page` to `@page "{id:int}"`.
* Adds a hidden row version. `ConcurrencyToken` must be added so postback binds the value.
* Displays the last byte of `ConcurrencyToken` for debugging purposes.
* Replaces `ViewData` with the strongly-typed `InstructorNameSL`.

### Test concurrency conflicts with the Edit page

Open two browsers instances of Edit on the English department:

* Run the app and select Departments.
* Right-click the **Edit** hyperlink for the English department and select **Open in new tab**.
* In the first tab, click the **Edit** hyperlink for the English department.

The two browser tabs display the same information.

Change the name in the first browser tab and click **Save**.

The browser shows the Index page with the changed value and updated `ConcurrencyToken`indicator. Note the updated `ConcurrencyToken`indicator, it's displayed on the second postback in the other tab.

Change a different field in the second browser tab.

Click **Save**. You see error messages for all fields that don't match the database values:

This browser window didn't intend to change the Name field. Copy and paste the current value (Languages) into the Name field. Tab out. Client-side validation removes the error message.

Click **Save** again. The value you entered in the second browser tab is saved. You see the saved values in the Index page.

## Update the Delete page model

Update `Pages/Departments/Delete.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.ConcurrencyToken value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}
```

The Delete page detects concurrency conflicts when the entity has changed after it was fetched. `Department.ConcurrencyToken` is the row version when the entity was fetched. When EF Core creates the `SQL DELETE` command, it includes a WHERE clause with `ConcurrencyToken`. If the `SQL DELETE` command results in zero rows affected:

* The `ConcurrencyToken` in the `SQL DELETE` command doesn't match `ConcurrencyToken` in the database.
* A `DbUpdateConcurrencyException` exception is thrown.
* `OnGetAsync` is called with the `concurrencyError`.

### Update the Delete Razor page

Update `Pages/Departments/Delete.cshtml` with the following code:

```
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.ConcurrencyToken)
        </dt>
        <dd class="col-sm-10">
            @Utility.GetLastChars(Model.Department.ConcurrencyToken)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Administrator.FullName)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Department.DepartmentID" />
        <input type="hidden" asp-for="Department.ConcurrencyToken" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>
```

The preceding code makes the following changes:

* Updates the `page` directive from `@page` to `@page "{id:int}"`.
* Adds an error message.
* Replaces FirstMidName with FullName in the **Administrator** field.
* Changes `ConcurrencyToken` to display the last byte.
* Adds a hidden row version. `ConcurrencyToken` must be added so postback binds the value.

### Test concurrency conflicts

Create a test department.

Open two browsers instances of Delete on the test department:

* Run the app and select Departments.
* Right-click the **Delete** hyperlink for the test department and select **Open in new tab**.
* Click the **Edit** hyperlink for the test department.

The two browser tabs display the same information.

Change the budget in the first browser tab and click **Save**.

The browser shows the Index page with the changed value and updated `ConcurrencyToken`indicator. Note the updated `ConcurrencyToken`indicator, it's displayed on the second postback in the other tab.

Delete the test department from the second tab. A concurrency error is display with the current values from the database. Clicking **Delete** deletes the entity, unless `ConcurrencyToken` has been updated.

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

This tutorial shows how to handle conflicts when multiple users update an entity concurrently (at the same time).

## Concurrency conflicts

A concurrency conflict occurs when:

* A user navigates to the edit page for an entity.
* Another user updates the same entity before the first user's change is written to the database.

If concurrency detection isn't enabled, whoever updates the database last overwrites the other user's changes. If this risk is acceptable, the cost of programming for concurrency might outweigh the benefit.

### Pessimistic concurrency (locking)

One way to prevent concurrency conflicts is to use database locks. This is called pessimistic concurrency. Before the app reads a database row that it intends to update, it requests a lock. Once a row is locked for update access, no other users are allowed to lock the row until the first lock is released.

Managing locks has disadvantages. It can be complex to program and can cause performance problems as the number of users increases. Entity Framework Core provides no built-in support for it, and this tutorial doesn't show how to implement it.

### Optimistic concurrency

Optimistic concurrency allows concurrency conflicts to happen, and then reacts appropriately when they do. For example, Jane visits the Department edit page and changes the budget for the English department from $350,000.00 to $0.00.

Before Jane clicks **Save**, John visits the same page and changes the Start Date field from 9/1/2007 to 9/1/2013.

Jane clicks **Save** first and sees her change take effect, since the browser displays the Index page with zero as the Budget amount.

John clicks **Save** on an Edit page that still shows a budget of $350,000.00. What happens next is determined by how you handle concurrency conflicts:

* You can keep track of which property a user has modified and update only the corresponding columns in the database.

  In the scenario, no data would be lost. Different properties were updated by the two users. The next time someone browses the English department, they will see both Jane's and John's changes. This method of updating can reduce the number of conflicts that could result in data loss. This approach has some disadvantages:

  * Can't avoid data loss if competing changes are made to the same property.
  * Is generally not practical in a web app. It requires maintaining significant state in order to keep track of all fetched values and new values. Maintaining large amounts of state can affect app performance.
  * Can increase app complexity compared to concurrency detection on an entity.

* You can let John's change overwrite Jane's change.

  The next time someone browses the English department, they will see 9/1/2013 and the fetched $350,000.00 value. This approach is called a *Client Wins* or *Last in Wins* scenario. (All values from the client take precedence over what's in the data store.) If you don't do any coding for concurrency handling, Client Wins happens automatically.

* You can prevent John's change from being updated in the database. Typically, the app would:

  * Display an error message.
  * Show the current state of the data.
  * Allow the user to reapply the changes.

  This is called a *Store Wins* scenario. (The data-store values take precedence over the values submitted by the client.) You implement the Store Wins scenario in this tutorial. This method ensures that no changes are overwritten without a user being alerted.

## Conflict detection in EF Core

EF Core throws `DbConcurrencyException` exceptions when it detects conflicts. The data model has to be configured to enable conflict detection. Options for enabling conflict detection include the following:

* Configure EF Core to include the original values of columns configured as [concurrency tokens](/en-us/ef/core/modeling/concurrency) in the Where clause of Update and Delete commands.

  When `SaveChanges` is called, the Where clause looks for the original values of any properties annotated with the [ConcurrencyCheckAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.concurrencycheckattribute) attribute. The update statement won't find a row to update if any of the concurrency token properties changed since the row was first read. EF Core interprets that as a concurrency conflict. For database tables that have many columns, this approach can result in very large Where clauses, and can require large amounts of state. Therefore this approach is generally not recommended, and it isn't the method used in this tutorial.

* In the database table, include a tracking column that can be used to determine when a row has been changed.

  In a SQL Server database, the data type of the tracking column is `rowversion`. The `rowversion` value is a sequential number that's incremented each time the row is updated. In an Update or Delete command, the Where clause includes the original value of the tracking column (the original row version number). If the row being updated has been changed by another user, the value in the `rowversion` column is different than the original value. In that case, the Update or Delete statement can't find the row to update because of the Where clause. EF Core throws a concurrency exception when no rows are affected by an Update or Delete command.

## Add a tracking property

In `Models/Department.cs`, add a tracking property named RowVersion:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        [Timestamp]
        public byte[] RowVersion { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}
```

The [TimestampAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.timestampattribute) attribute is what identifies the column as a concurrency tracking column. The fluent API is an alternative way to specify the tracking property:

```
modelBuilder.Entity<Department>()
  .Property<byte[]>("RowVersion")
  .IsRowVersion();
```

* [Visual Studio](#tabpanel_8_visual-studio)
* [Visual Studio Code](#tabpanel_8_visual-studio-code)

For a SQL Server database, the `[Timestamp]` attribute on an entity property defined as byte array:

* Causes the column to be included in DELETE and UPDATE WHERE clauses.
* Sets the column type in the database to [rowversion](/en-us/sql/t-sql/data-types/rowversion-transact-sql).

The database generates a sequential row version number that's incremented each time the row is updated. In an `Update` or `Delete` command, the `Where` clause includes the fetched row version value. If the row being updated has changed since it was fetched:

* The current row version value doesn't match the fetched value.
* The `Update` or `Delete` commands don't find a row because the `Where` clause looks for the fetched row version value.
* A `DbUpdateConcurrencyException` is thrown.

The following code shows a portion of the T-SQL generated by EF Core when the Department name is updated:

```
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
```

The preceding highlighted code shows the `WHERE` clause containing `RowVersion`. If the database `RowVersion` doesn't equal the `RowVersion` parameter (`@p2`), no rows are updated.

The following highlighted code shows the T-SQL that verifies exactly one row was updated:

```
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
```

[@@ROWCOUNT](/en-us/sql/t-sql/functions/rowcount-transact-sql) returns the number of rows affected by the last statement. If no rows are updated, EF Core throws a `DbUpdateConcurrencyException`.

For a SQLite database, the `[Timestamp]` attribute on an entity property defined as byte array:

* Causes the column to be included in DELETE and UPDATE WHERE clauses.
* Maps to a BLOB column type.

Database triggers update the RowVersion column with a new random byte array whenever a row is updated. In an `Update` or `Delete` command, the `Where` clause includes the fetched value of the RowVersion column. If the row being updated has changed since it was fetched:

* The current row version value doesn't match the fetched value.
* The `Update` or `Delete` command doesn't find a row because the `Where` clause looks for the original row version value.
* A `DbUpdateConcurrencyException` is thrown.

### Update the database

Adding the `RowVersion` property changes the data model, which requires a migration.

Build the project.

* [Visual Studio](#tabpanel_9_visual-studio)
* [Visual Studio Code](#tabpanel_9_visual-studio-code)

- Run the following command in the PMC:

  ```
  Add-Migration RowVersion
  ```

* Run the following command in a terminal:

  ```
  dotnet ef migrations add RowVersion
  ```

This command:

* Creates the `Migrations/{time stamp}_RowVersion.cs` migration file.

* Updates the `Migrations/SchoolContextModelSnapshot.cs` file. The update adds the following highlighted code to the `BuildModel` method:

  ```
  modelBuilder.Entity("ContosoUniversity.Models.Department", b =>
      {
          b.Property<int>("DepartmentID")
              .ValueGeneratedOnAdd()
              .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

          b.Property<decimal>("Budget")
              .HasColumnType("money");

          b.Property<int?>("InstructorID");

          b.Property<string>("Name")
              .HasMaxLength(50);

          b.Property<byte[]>("RowVersion")
              .IsConcurrencyToken()
              .ValueGeneratedOnAddOrUpdate();

          b.Property<DateTime>("StartDate");

          b.HasKey("DepartmentID");

          b.HasIndex("InstructorID");

          b.ToTable("Department");
      });
  ```

- [Visual Studio](#tabpanel_10_visual-studio)
- [Visual Studio Code](#tabpanel_10_visual-studio-code)

* Run the following command in the PMC:

  ```
  Update-Database
  ```

- Open the `Migrations/<timestamp>_RowVersion.cs` file and add the highlighted code:

  ```
  #if SQLiteVersion

  using System;
  using Microsoft.EntityFrameworkCore.Migrations;

  namespace ContosoUniversity.Migrations
  {
      public partial class RowVersion : Migration
      {
          protected override void Up(MigrationBuilder migrationBuilder)
          {
              migrationBuilder.AddColumn<byte[]>(
                  name: "RowVersion",
                  table: "Department",
                  rowVersion: true,
                  nullable: true);

              migrationBuilder.Sql(
              @"
                  UPDATE Department
                  SET RowVersion = randomblob(8)
              ");

              migrationBuilder.Sql(
              @"
                  CREATE TRIGGER SetRowVersionOnUpdate
                  AFTER UPDATE ON Department
                  BEGIN
                      UPDATE Department
                      SET RowVersion = randomblob(8)
                      WHERE rowid = NEW.rowid;
                  END
              ");

              migrationBuilder.Sql(
              @"
                  CREATE TRIGGER SetRowVersionOnInsert
                  AFTER INSERT ON Department
                  BEGIN
                      UPDATE Department
                      SET RowVersion = randomblob(8)
                      WHERE rowid = NEW.rowid;
                  END
              ");
          }

          protected override void Down(MigrationBuilder migrationBuilder)
          {
              migrationBuilder.DropColumn(
                  name: "RowVersion",
                  table: "Department");
          }
      }
  }

  #endif
  ```

  The preceding code:

  * Updates existing rows with random blob values.
  * Adds database triggers that set the RowVersion column to a random blob value whenever a row is updated.

- Run the following command in a terminal:

  ```
  dotnet ef database update
  ```

[]()

## Scaffold Department pages

* [Visual Studio](#tabpanel_11_visual-studio)
* [Visual Studio Code](#tabpanel_11_visual-studio-code)

- Follow the instructions in [Scaffold Student pages](intro?view=aspnetcore-10.0#scaffold-student-pages) with the following exceptions:

- Create a *Pages/Departments* folder.

- Use `Department` for the model class.

  * Use the existing context class instead of creating a new one.

* Create a *Pages/Departments* folder.

* Run the following command to scaffold the Department pages.

  **On Windows:**

  ```
  dotnet aspnet-codegenerator razorpage -m Department -dc SchoolContext -udl -outDir Pages\Departments --referenceScriptLibraries
  ```

  **On Linux or macOS:**

  ```
  dotnet aspnet-codegenerator razorpage -m Department -dc SchoolContext -udl -outDir Pages/Departments --referenceScriptLibraries
  ```

Build the project.

## Update the Index page

The scaffolding tool created a `RowVersion` column for the Index page, but that field wouldn't be displayed in a production app. In this tutorial, the last byte of the `RowVersion` is displayed to help show how concurrency handling works. The last byte isn't guaranteed to be unique by itself.

Update *Pages\Departments\Index.cshtml* page:

* Replace Index with Departments.
* Change the code containing `RowVersion` to show just the last byte of the byte array.
* Replace FirstMidName with FullName.

The following code shows the updated page:

```
@page
@model ContosoUniversity.Pages.Departments.IndexModel

@{
    ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Budget)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].StartDate)
                </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Administrator)
            </th>
            <th>
                RowVersion
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Department)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    @item.RowVersion[7]
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

## Update the Edit page model

Update `Pages/Departments/Edit.cshtml.cs` with the following code:

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var departmentToUpdate = await _context.Departments
                .Include(i => i.Administrator)
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            _context.Entry(departmentToUpdate)
                .Property("RowVersion").OriginalValue = Department.RowVersion;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await setDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current RowVersion so next postback
                    // matches unless an new concurrency issue happens.
                    Department.RowVersion = (byte[])dbValues.RowVersion;
                    // Clear the model error for the next postback.
                    ModelState.Remove("Department.RowVersion");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            var deletedDepartment = new Department();
            // ModelState contains the posted data because of the deletion error
            // and will overide the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

        private async Task setDbErrorMessage(Department dbValues,
                Department clientValues, SchoolContext context)
        {

            if (dbValues.Name != clientValues.Name)
            {
                ModelState.AddModelError("Department.Name",
                    $"Current value: {dbValues.Name}");
            }
            if (dbValues.Budget != clientValues.Budget)
            {
                ModelState.AddModelError("Department.Budget",
                    $"Current value: {dbValues.Budget:c}");
            }
            if (dbValues.StartDate != clientValues.StartDate)
            {
                ModelState.AddModelError("Department.StartDate",
                    $"Current value: {dbValues.StartDate:d}");
            }
            if (dbValues.InstructorID != clientValues.InstructorID)
            {
                Instructor dbInstructor = await _context.Instructors
                   .FindAsync(dbValues.InstructorID);
                ModelState.AddModelError("Department.InstructorID",
                    $"Current value: {dbInstructor?.FullName}");
            }

            ModelState.AddModelError(string.Empty,
                "The record you attempted to edit "
              + "was modified by another user after you. The "
              + "edit operation was canceled and the current values in the database "
              + "have been displayed. If you still want to edit this record, click "
              + "the Save button again.");
        }
    }
}
```

The [OriginalValue](/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyentry.originalvalue#microsoft-entityframeworkcore-changetracking-propertyentry-originalvalue) is updated with the `rowVersion` value from the entity when it was fetched in the `OnGetAsync` method. EF Core generates a SQL UPDATE command with a WHERE clause containing the original `RowVersion` value. If no rows are affected by the UPDATE command (no rows have the original `RowVersion` value), a `DbUpdateConcurrencyException` exception is thrown.

```
public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    _context.Entry(departmentToUpdate)
        .Property("RowVersion").OriginalValue = Department.RowVersion;
```

In the preceding highlighted code:

* The value in `Department.RowVersion` is what was in the entity when it was originally fetched in the Get request for the Edit page. The value is provided to the `OnPost` method by a hidden field in the Razor page that displays the entity to be edited. The hidden field value is copied to `Department.RowVersion` by the model binder.
* `OriginalValue` is what EF Core will use in the Where clause. Before the highlighted line of code executes, `OriginalValue` has the value that was in the database when `FirstOrDefaultAsync` was called in this method, which might be different from what was displayed on the Edit page.
* The highlighted code makes sure that EF Core uses the original `RowVersion` value from the displayed `Department` entity in the SQL UPDATE statement's Where clause.

When a concurrency error happens, the following highlighted code gets the client values (the values posted to this method) and the database values.

```
if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await setDbErrorMessage(dbValues, clientValues, _context);

        // Save the current RowVersion so next postback
        // matches unless an new concurrency issue happens.
        Department.RowVersion = (byte[])dbValues.RowVersion;
        // Clear the model error for the next postback.
        ModelState.Remove("Department.RowVersion");
    }
```

The following code adds a custom error message for each column that has database values different from what was posted to `OnPostAsync`:

```
private async Task setDbErrorMessage(Department dbValues,
        Department clientValues, SchoolContext context)
{

    if (dbValues.Name != clientValues.Name)
    {
        ModelState.AddModelError("Department.Name",
            $"Current value: {dbValues.Name}");
    }
    if (dbValues.Budget != clientValues.Budget)
    {
        ModelState.AddModelError("Department.Budget",
            $"Current value: {dbValues.Budget:c}");
    }
    if (dbValues.StartDate != clientValues.StartDate)
    {
        ModelState.AddModelError("Department.StartDate",
            $"Current value: {dbValues.StartDate:d}");
    }
    if (dbValues.InstructorID != clientValues.InstructorID)
    {
        Instructor dbInstructor = await _context.Instructors
           .FindAsync(dbValues.InstructorID);
        ModelState.AddModelError("Department.InstructorID",
            $"Current value: {dbInstructor?.FullName}");
    }

    ModelState.AddModelError(string.Empty,
        "The record you attempted to edit "
      + "was modified by another user after you. The "
      + "edit operation was canceled and the current values in the database "
      + "have been displayed. If you still want to edit this record, click "
      + "the Save button again.");
}
```

The following highlighted code sets the `RowVersion` value to the new value retrieved from the database. The next time the user clicks **Save**, only concurrency errors that happen since the last display of the Edit page will be caught.

```
if (await TryUpdateModelAsync<Department>(
    departmentToUpdate,
    "Department",
    s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
    try
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        var exceptionEntry = ex.Entries.Single();
        var clientValues = (Department)exceptionEntry.Entity;
        var databaseEntry = exceptionEntry.GetDatabaseValues();
        if (databaseEntry == null)
        {
            ModelState.AddModelError(string.Empty, "Unable to save. " +
                "The department was deleted by another user.");
            return Page();
        }

        var dbValues = (Department)databaseEntry.ToObject();
        await setDbErrorMessage(dbValues, clientValues, _context);

        // Save the current RowVersion so next postback
        // matches unless an new concurrency issue happens.
        Department.RowVersion = (byte[])dbValues.RowVersion;
        // Clear the model error for the next postback.
        ModelState.Remove("Department.RowVersion");
    }
```

The `ModelState.Remove` statement is required because `ModelState` has the old `RowVersion` value. In the Razor Page, the `ModelState` value for a field takes precedence over the model property values when both are present.

### Update the Edit page

Update `Pages/Departments/Edit.cshtml` with the following code:

```
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.RowVersion" />
            <div class="form-group">
                <label>RowVersion</label>
                @Model.Department.RowVersion[7]
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding code:

* Updates the `page` directive from `@page` to `@page "{id:int}"`.
* Adds a hidden row version. `RowVersion` must be added so postback binds the value.
* Displays the last byte of `RowVersion` for debugging purposes.
* Replaces `ViewData` with the strongly-typed `InstructorNameSL`.

### Test concurrency conflicts with the Edit page

Open two browsers instances of Edit on the English department:

* Run the app and select Departments.
* Right-click the **Edit** hyperlink for the English department and select **Open in new tab**.
* In the first tab, click the **Edit** hyperlink for the English department.

The two browser tabs display the same information.

Change the name in the first browser tab and click **Save**.

The browser shows the Index page with the changed value and updated rowVersion indicator. Note the updated rowVersion indicator, it's displayed on the second postback in the other tab.

Change a different field in the second browser tab.

Click **Save**. You see error messages for all fields that don't match the database values:

This browser window didn't intend to change the Name field. Copy and paste the current value (Languages) into the Name field. Tab out. Client-side validation removes the error message.

Click **Save** again. The value you entered in the second browser tab is saved. You see the saved values in the Index page.

## Update the Delete page model

Update `Pages/Departments/Delete.cshtml.cs` with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.rowVersion value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}
```

The Delete page detects concurrency conflicts when the entity has changed after it was fetched. `Department.RowVersion` is the row version when the entity was fetched. When EF Core creates the SQL DELETE command, it includes a WHERE clause with `RowVersion`. If the SQL DELETE command results in zero rows affected:

* The `RowVersion` in the SQL DELETE command doesn't match `RowVersion` in the database.
* A DbUpdateConcurrencyException exception is thrown.
* `OnGetAsync` is called with the `concurrencyError`.

### Update the Delete page

Update `Pages/Departments/Delete.cshtml` with the following code:

```
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<p class="text-danger">@Model.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Department.Name)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Budget)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Budget)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.StartDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.StartDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.RowVersion)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.RowVersion[7])
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Administrator)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Administrator.FullName)
        </dd>
    </dl>
    
    <form method="post">
        <input type="hidden" asp-for="Department.DepartmentID" />
        <input type="hidden" asp-for="Department.RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-danger" /> |
            <a asp-page="./Index">Back to List</a>
        </div>
</form>
</div>
```

The preceding code makes the following changes:

* Updates the `page` directive from `@page` to `@page "{id:int}"`.
* Adds an error message.
* Replaces FirstMidName with FullName in the **Administrator** field.
* Changes `RowVersion` to display the last byte.
* Adds a hidden row version. `RowVersion` must be added so postback binds the value.

### Test concurrency conflicts

Create a test department.

Open two browsers instances of Delete on the test department:

* Run the app and select Departments.
* Right-click the **Delete** hyperlink for the test department and select **Open in new tab**.
* Click the **Edit** hyperlink for the test department.

The two browser tabs display the same information.

Change the budget in the first browser tab and click **Save**.

The browser shows the Index page with the changed value and updated rowVersion indicator. Note the updated rowVersion indicator, it's displayed on the second postback in the other tab.

Delete the test department from the second tab. A concurrency error is display with the current values from the database. Clicking **Delete** deletes the entity, unless `RowVersion` has been updated.

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

This tutorial shows how to handle conflicts when multiple users update an entity concurrently (at the same time). If you run into problems you can't solve, [download or view the completed app.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) [Download instructions](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

## Concurrency conflicts

A concurrency conflict occurs when:

* A user navigates to the edit page for an entity.
* Another user updates the same entity before the first user's change is written to the DB.

If concurrency detection isn't enabled, when concurrent updates occur:

* The last update wins. That is, the last update values are saved to the DB.
* The first of the current updates are lost.

### Optimistic concurrency

Optimistic concurrency allows concurrency conflicts to happen, and then reacts appropriately when they do. For example, Jane visits the Department edit page and changes the budget for the English department from $350,000.00 to $0.00.

Before Jane clicks **Save**, John visits the same page and changes the Start Date field from 9/1/2007 to 9/1/2013.

Jane clicks **Save** first and sees her change when the browser displays the Index page.

John clicks **Save** on an Edit page that still shows a budget of $350,000.00. What happens next is determined by how you handle concurrency conflicts.

Optimistic concurrency includes the following options:

* You can keep track of which property a user has modified and update only the corresponding columns in the DB.

  In the scenario, no data would be lost. Different properties were updated by the two users. The next time someone browses the English department, they will see both Jane's and John's changes. This method of updating can reduce the number of conflicts that could result in data loss. This approach:

  * Can't avoid data loss if competing changes are made to the same property.
  * Is generally not practical in a web app. It requires maintaining significant state in order to keep track of all fetched values and new values. Maintaining large amounts of state can affect app performance.
  * Can increase app complexity compared to concurrency detection on an entity.

* You can let John's change overwrite Jane's change.

  The next time someone browses the English department, they will see 9/1/2013 and the fetched $350,000.00 value. This approach is called a *Client Wins* or *Last in Wins* scenario. (All values from the client take precedence over what's in the data store.) If you don't do any coding for concurrency handling, Client Wins happens automatically.

* You can prevent John's change from being updated in the DB. Typically, the app would:

  * Display an error message.
  * Show the current state of the data.
  * Allow the user to reapply the changes.

  This is called a *Store Wins* scenario. (The data-store values take precedence over the values submitted by the client.) You implement the Store Wins scenario in this tutorial. This method ensures that no changes are overwritten without a user being alerted.

## Handling concurrency

When a property is configured as a [concurrency token](/en-us/ef/core/modeling/concurrency):

* EF Core verifies that property has not been modified after it was fetched. The check occurs when [SaveChanges](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechanges) or [SaveChangesAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechangesasync) is called.
* If the property has been changed after it was fetched, a [DbUpdateConcurrencyException](/en-us/dotnet/api/microsoft.entityframeworkcore.dbupdateconcurrencyexception) is thrown.

The DB and data model must be configured to support throwing `DbUpdateConcurrencyException`.

### Detecting concurrency conflicts on a property

Concurrency conflicts can be detected at the property level with the [ConcurrencyCheck](/en-us/dotnet/api/system.componentmodel.dataannotations.concurrencycheckattribute) attribute. The attribute can be applied to multiple properties on the model. For more information, see [Data Annotations-ConcurrencyCheck](/en-us/ef/core/modeling/concurrency#data-annotations).

The `[ConcurrencyCheck]` attribute isn't used in this tutorial.

### Detecting concurrency conflicts on a row

To detect concurrency conflicts, a [rowversion](/en-us/sql/t-sql/data-types/rowversion-transact-sql) tracking column is added to the model. `rowversion` :

* Is SQL Server specific. Other databases may not provide a similar feature.
* Is used to determine that an entity has not been changed since it was fetched from the DB.

The DB generates a sequential `rowversion` number that's incremented each time the row is updated. In an `Update` or `Delete` command, the `Where` clause includes the fetched value of `rowversion`. If the row being updated has changed:

* `rowversion` doesn't match the fetched value.
* The `Update` or `Delete` commands don't find a row because the `Where` clause includes the fetched `rowversion`.
* A `DbUpdateConcurrencyException` is thrown.

In EF Core, when no rows have been updated by an `Update` or `Delete` command, a concurrency exception is thrown.

### Add a tracking property to the Department entity

In `Models/Department.cs`, add a tracking property named RowVersion:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        [Timestamp]
        public byte[] RowVersion { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}
```

The [Timestamp](/en-us/dotnet/api/system.componentmodel.dataannotations.timestampattribute) attribute specifies that this column is included in the `Where` clause of `Update` and `Delete` commands. The attribute is called `Timestamp` because previous versions of SQL Server used a SQL `timestamp` data type before the SQL `rowversion` type replaced it.

The fluent API can also specify the tracking property:

```
modelBuilder.Entity<Department>()
  .Property<byte[]>("RowVersion")
  .IsRowVersion();
```

The following code shows a portion of the T-SQL generated by EF Core when the Department name is updated:

```
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
```

The preceding highlighted code shows the `WHERE` clause containing `RowVersion`. If the DB `RowVersion` doesn't equal the `RowVersion` parameter (`@p2`), no rows are updated.

The following highlighted code shows the T-SQL that verifies exactly one row was updated:

```
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
```

[@@ROWCOUNT](/en-us/sql/t-sql/functions/rowcount-transact-sql) returns the number of rows affected by the last statement. In no rows are updated, EF Core throws a `DbUpdateConcurrencyException`.

You can see the T-SQL EF Core generates in the output window of Visual Studio.

### Update the DB

Adding the `RowVersion` property changes the DB model, which requires a migration.

Build the project. Enter the following in a command window:

```
dotnet ef migrations add RowVersion
dotnet ef database update
```

The preceding commands:

* Adds the `Migrations/{time stamp}_RowVersion.cs` migration file.

* Updates the `Migrations/SchoolContextModelSnapshot.cs` file. The update adds the following highlighted code to the `BuildModel` method:

  ```
  ```

* Runs migrations to update the DB.

[]()

## Scaffold the Departments model

* [Visual Studio](#tabpanel_12_visual-studio)
* [Visual Studio Code](#tabpanel_12_visual-studio-code)

Follow the instructions in [Scaffold the student model](intro?view=aspnetcore-10.0#scaffold-student-pages) and use `Department` for the model class.

Run the following command:

```
dotnet aspnet-codegenerator razorpage -m Department -dc SchoolContext -udl -outDir Pages\Departments --referenceScriptLibraries
```

The preceding command scaffolds the `Department` model. Open the project in Visual Studio.

Build the project.

### Update the Departments Index page

The scaffolding engine created a `RowVersion` column for the Index page, but that field shouldn't be displayed. In this tutorial, the last byte of the `RowVersion` is displayed to help understand concurrency. The last byte isn't guaranteed to be unique. A real app wouldn't display `RowVersion` or the last byte of `RowVersion`.

Update the Index page:

* Replace Index with Departments.
* Replace the markup containing `RowVersion` with the last byte of `RowVersion`.
* Replace FirstMidName with FullName.

The following markup shows the updated page:

```
@page
@model ContosoUniversity.Pages.Departments.IndexModel

@{
    ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Budget)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].StartDate)
                </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Administrator)
            </th>
            <th>
                RowVersion
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Department) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Budget)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.StartDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Administrator.FullName)
            </td>
            <td>
                @item.RowVersion[7]
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

### Update the Edit page model

Update `Pages/Departments/Edit.cshtml.cs` with the following code:

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var departmentToUpdate = await _context.Departments
                .Include(i => i.Administrator)
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            // null means Department was deleted by another user.
            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            // Update the RowVersion to the value when this entity was
            // fetched. If the entity has been updated after it was
            // fetched, RowVersion won't match the DB RowVersion and
            // a DbUpdateConcurrencyException is thrown.
            // A second postback will make them match, unless a new 
            // concurrency issue happens.
            _context.Entry(departmentToUpdate)
                .Property("RowVersion").OriginalValue = Department.RowVersion;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await SetDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current RowVersion so next postback
                    // matches unless an new concurrency issue happens.
                    Department.RowVersion = (byte[])dbValues.RowVersion;
                    // Must clear the model error for the next postback.
                    ModelState.Remove("Department.RowVersion");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            // ModelState contains the posted data because of the deletion error and will overide the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
            return Page();
        }

        private async Task SetDbErrorMessage(Department dbValues,
                Department clientValues, SchoolContext context)
        {

            if (dbValues.Name != clientValues.Name)
            {
                ModelState.AddModelError("Department.Name",
                    $"Current value: {dbValues.Name}");
            }
            if (dbValues.Budget != clientValues.Budget)
            {
                ModelState.AddModelError("Department.Budget",
                    $"Current value: {dbValues.Budget:c}");
            }
            if (dbValues.StartDate != clientValues.StartDate)
            {
                ModelState.AddModelError("Department.StartDate",
                    $"Current value: {dbValues.StartDate:d}");
            }
            if (dbValues.InstructorID != clientValues.InstructorID)
            {
                Instructor dbInstructor = await _context.Instructors
                   .FindAsync(dbValues.InstructorID);
                ModelState.AddModelError("Department.InstructorID",
                    $"Current value: {dbInstructor?.FullName}");
            }

            ModelState.AddModelError(string.Empty,
                "The record you attempted to edit "
              + "was modified by another user after you. The "
              + "edit operation was canceled and the current values in the database "
              + "have been displayed. If you still want to edit this record, click "
              + "the Save button again.");
        }
    }
}
```

To detect a concurrency issue, the [OriginalValue](/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyentry.originalvalue) is updated with the `rowVersion` value from the entity it was fetched. EF Core generates a SQL UPDATE command with a WHERE clause containing the original `RowVersion` value. If no rows are affected by the UPDATE command (no rows have the original `RowVersion` value), a `DbUpdateConcurrencyException` exception is thrown.

```
public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    // null means Department was deleted by another user.
    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    // Update the RowVersion to the value when this entity was
    // fetched. If the entity has been updated after it was
    // fetched, RowVersion won't match the DB RowVersion and
    // a DbUpdateConcurrencyException is thrown.
    // A second postback will make them match, unless a new 
    // concurrency issue happens.
    _context.Entry(departmentToUpdate)
        .Property("RowVersion").OriginalValue = Department.RowVersion;
```

In the preceding code, `Department.RowVersion` is the value when the entity was fetched. `OriginalValue` is the value in the DB when `FirstOrDefaultAsync` was called in this method.

The following code gets the client values (the values posted to this method) and the DB values:

```
try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
    var exceptionEntry = ex.Entries.Single();
    var clientValues = (Department)exceptionEntry.Entity;
    var databaseEntry = exceptionEntry.GetDatabaseValues();
    if (databaseEntry == null)
    {
        ModelState.AddModelError(string.Empty, "Unable to save. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await SetDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}
```

The following code adds a custom error message for each column that has DB values different from what was posted to `OnPostAsync`:

```
private async Task SetDbErrorMessage(Department dbValues,
        Department clientValues, SchoolContext context)
{

    if (dbValues.Name != clientValues.Name)
    {
        ModelState.AddModelError("Department.Name",
            $"Current value: {dbValues.Name}");
    }
    if (dbValues.Budget != clientValues.Budget)
    {
        ModelState.AddModelError("Department.Budget",
            $"Current value: {dbValues.Budget:c}");
    }
    if (dbValues.StartDate != clientValues.StartDate)
    {
        ModelState.AddModelError("Department.StartDate",
            $"Current value: {dbValues.StartDate:d}");
    }
    if (dbValues.InstructorID != clientValues.InstructorID)
    {
        Instructor dbInstructor = await _context.Instructors
           .FindAsync(dbValues.InstructorID);
        ModelState.AddModelError("Department.InstructorID",
            $"Current value: {dbInstructor?.FullName}");
    }

    ModelState.AddModelError(string.Empty,
        "The record you attempted to edit "
      + "was modified by another user after you. The "
      + "edit operation was canceled and the current values in the database "
      + "have been displayed. If you still want to edit this record, click "
      + "the Save button again.");
}
```

The following highlighted code sets the `RowVersion` value to the new value retrieved from the DB. The next time the user clicks **Save**, only concurrency errors that happen since the last display of the Edit page will be caught.

```
try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
    var exceptionEntry = ex.Entries.Single();
    var clientValues = (Department)exceptionEntry.Entity;
    var databaseEntry = exceptionEntry.GetDatabaseValues();
    if (databaseEntry == null)
    {
        ModelState.AddModelError(string.Empty, "Unable to save. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await SetDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}
```

The `ModelState.Remove` statement is required because `ModelState` has the old `RowVersion` value. In the Razor Page, the `ModelState` value for a field takes precedence over the model property values when both are present.

## Update the Edit page

Update `Pages/Departments/Edit.cshtml` with the following markup:

```
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.RowVersion" />
            <div class="form-group">
                <label>RowVersion</label>
                @Model.Department.RowVersion[7]
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The preceding markup:

* Updates the `page` directive from `@page` to `@page "{id:int}"`.
* Adds a hidden row version. `RowVersion` must be added so post back binds the value.
* Displays the last byte of `RowVersion` for debugging purposes.
* Replaces `ViewData` with the strongly-typed `InstructorNameSL`.

## Test concurrency conflicts with the Edit page

Open two browsers instances of Edit on the English department:

* Run the app and select Departments.
* Right-click the **Edit** hyperlink for the English department and select **Open in new tab**.
* In the first tab, click the **Edit** hyperlink for the English department.

The two browser tabs display the same information.

Change the name in the first browser tab and click **Save**.

The browser shows the Index page with the changed value and updated rowVersion indicator. Note the updated rowVersion indicator, it's displayed on the second postback in the other tab.

Change a different field in the second browser tab.

Click **Save**. You see error messages for all fields that don't match the DB values:

This browser window didn't intend to change the Name field. Copy and paste the current value (Languages) into the Name field. Tab out. Client-side validation removes the error message.

Click **Save** again. The value you entered in the second browser tab is saved. You see the saved values in the Index page.

## Update the Delete page

Update the Delete page model with the following code:

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.rowVersion value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}
```

The Delete page detects concurrency conflicts when the entity has changed after it was fetched. `Department.RowVersion` is the row version when the entity was fetched. When EF Core creates the SQL DELETE command, it includes a WHERE clause with `RowVersion`. If the SQL DELETE command results in zero rows affected:

* The `RowVersion` in the SQL DELETE command doesn't match `RowVersion` in the DB.
* A DbUpdateConcurrencyException exception is thrown.
* `OnGetAsync` is called with the `concurrencyError`.

### Update the Delete page

Update `Pages/Departments/Delete.cshtml` with the following code:

```
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<p class="text-danger">@Model.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Department.Name)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Budget)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Budget)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.StartDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.StartDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.RowVersion)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.RowVersion[7])
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Administrator)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Administrator.FullName)
        </dd>
    </dl>
    
    <form method="post">
        <input type="hidden" asp-for="Department.DepartmentID" />
        <input type="hidden" asp-for="Department.RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-page="./Index">Back to List</a>
        </div>
</form>
</div>
```

The preceding code makes the following changes:

* Updates the `page` directive from `@page` to `@page "{id:int}"`.
* Adds an error message.
* Replaces FirstMidName with FullName in the **Administrator** field.
* Changes `RowVersion` to display the last byte.
* Adds a hidden row version. `RowVersion` must be added so post back binds the value.

### Test concurrency conflicts with the Delete page

Create a test department.

Open two browsers instances of Delete on the test department:

* Run the app and select Departments.
* Right-click the **Delete** hyperlink for the test department and select **Open in new tab**.
* Click the **Edit** hyperlink for the test department.

The two browser tabs display the same information.

Change the budget in the first browser tab and click **Save**.

The browser shows the Index page with the changed value and updated rowVersion indicator. Note the updated rowVersion indicator, it's displayed on the second postback in the other tab.

Delete the test department from the second tab. A concurrency error is display with the current values from the DB. Clicking **Delete** deletes the entity, unless `RowVersion` has been updated.

See [Inheritance](../ef-mvc/inheritance?view=aspnetcore-10.0) on how to inherit a data model.

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

## Related content

* [Concurrency Tokens in EF Core](/en-us/ef/core/modeling/concurrency)
* [Handle concurrency in EF Core](/en-us/ef/core/saving/concurrency)
* [Debugging ASP.NET Core 2.x source](https://github.com/dotnet/AspNetCore.Docs/issues/4155)

- [Concurrency Tokens in EF Core](/en-us/ef/core/modeling/concurrency)
- [Handle concurrency in EF Core](/en-us/ef/core/saving/concurrency)
- [YouTube version of this tutorial(Handling Concurrency Conflicts)](https://youtu.be/EosxHTFgYps)
- [YouTube version of this tutorial(Part 2)](https://www.youtube.com/watch?v=kcxERLnaGO0)
- [YouTube version of this tutorial(Part 3)](https://www.youtube.com/watch?v=d4RbpfvELRs)

## Next steps

This is the last tutorial in the series. Additional topics are covered in the [MVC version of this tutorial series](../ef-mvc/?view=aspnetcore-10.0).

[Previous tutorial](update-related-data?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/retries
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/retries.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Transient fault handling with gRPC retries

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

gRPC retries is a feature that allows gRPC clients to automatically retry failed calls. This article discusses how to configure a retry policy to make resilient, fault tolerant gRPC apps in .NET.

gRPC retries requires [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client) version 2.36.0 or later.

## Transient fault handling

gRPC calls can be interrupted by transient faults. Transient faults include:

* Momentary loss of network connectivity.
* Temporary unavailability of a service.
* Timeouts due to server load.

When a gRPC call is interrupted, the client throws an `RpcException` with details about the error. The client app must catch the exception and choose how to handle the error.

```
var client = new Greeter.GreeterClient(channel);
try
{
    var response = await client.SayHelloAsync(
        new HelloRequest { Name = ".NET" });

    Console.WriteLine("From server: " + response.Message);
}
catch (RpcException ex)
{
    // Write logic to inspect the error and retry
    // if the error is from a transient fault.
}
```

Duplicating retry logic throughout an app is verbose and error prone. Fortunately the .NET gRPC client has a built-in support for automatic retries.

## Configure a gRPC retry policy

A retry policy is configured once when a gRPC channel is created:

```
var defaultMethodConfig = new MethodConfig
{
    Names = { MethodName.Default },
    RetryPolicy = new RetryPolicy
    {
        MaxAttempts = 5,
        InitialBackoff = TimeSpan.FromSeconds(1),
        MaxBackoff = TimeSpan.FromSeconds(5),
        BackoffMultiplier = 1.5,
        RetryableStatusCodes = { StatusCode.Unavailable }
    }
};

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
    ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }
});
```

The preceding code:

* Creates a `MethodConfig`. Retry policies can be configured per-method and methods are matched using the `Names` property. This method is configured with `MethodName.Default`, so it's applied to all gRPC methods called by this channel.
* Configures a retry policy. This policy instructs clients to automatically retry gRPC calls that fail with the status code `Unavailable`.
* Configures the created channel to use the retry policy by setting `GrpcChannelOptions.ServiceConfig`.

gRPC clients created with the channel will automatically retry failed calls:

```
var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(
    new HelloRequest { Name = ".NET" });

Console.WriteLine("From server: " + response.Message);
```

### When retries are valid

Calls are retried when:

* The failing status code matches a value in `RetryableStatusCodes`.
* The previous number of attempts is less than `MaxAttempts`.
* The call hasn't been committed.
* The deadline hasn't been exceeded.

A gRPC call becomes committed in two scenarios:

* The client receives response headers. Response headers are sent by the server when `ServerCallContext.WriteResponseHeadersAsync` is called, or when the first message is written to the server response stream.
* The client's outgoing message (or messages if streaming) has exceeded the client's maximum buffer size. `MaxRetryBufferSize` and `MaxRetryBufferPerCallSize` are [configured on the channel](configuration?view=aspnetcore-10.0#configure-client-options).

Committed calls won't retry, regardless of the status code or the previous number of attempts.

### Streaming calls

Streaming calls can be used with gRPC retries, but there are important considerations when they are used together:

* **Server streaming**, **bidirectional streaming**: Streaming RPCs that return multiple messages from the server won't retry after the first message has been received. Apps must add additional logic to manually re-establish server and bidirectional streaming calls.
* **Client streaming**, **bidirectional streaming**: Streaming RPCs that send multiple messages to the server won't retry if the outgoing messages have exceeded the client's maximum buffer size. The maximum buffer size can be increased with configuration.

For more information, see [When retries are valid](#when-retries-are-valid).

### Retry backoff delay

The backoff delay between retry attempts is configured with `InitialBackoff`, `MaxBackoff`, and `BackoffMultiplier`. More information about each option is available in the [gRPC retry options section](#grpc-retry-options).

The actual delay between retry attempts is randomized. A randomized delay between 0 and the current backoff determines when the next retry attempt is made. Consider that even with exponential backoff configured, increasing the current backoff between attempts, the actual delay between attempts isn't always larger. The delay is randomized to prevent retries from multiple calls from clustering together and potentially overloading the server.

### Detect retries with metadata

gRPC retries can be detected by the presence of `grpc-previous-rpc-attempts` metadata. The `grpc-previous-rpc-attempts` metadata:

* Is automatically added to retried calls and sent to the server.
* Value represents the number of preceding retry attempts.
* Value is always an integer.

Consider the following retry scenario:

1. Client makes a gRPC call to the server.
2. Server fails and returns a retriable status code response.
3. Client retries the gRPC call. Because there was one previous attempt, `grpc-previous-rpc-attempts` metadata has a value of `1`. Metadata is sent to the server with the retry.
4. Server succeeds and returns OK.
5. Client reports success. `grpc-previous-rpc-attempts` is in the response metadata and has a value of `1`.

The `grpc-previous-rpc-attempts` metadata is not present on the initial gRPC call, is `1` for the first retry, `2` for the second retry, and so on.

### gRPC retry options

The following table describes options for configuring gRPC retry policies.

| Option                 | Description                                                                                                                                                                                                                                                                                             |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MaxAttempts`          | The maximum number of call attempts, including the original attempt. This value is limited by `GrpcChannelOptions.MaxRetryAttempts` which defaults to 5. A value is required and must be greater than 1.                                                                                                |
| `InitialBackoff`       | The initial backoff delay between retry attempts. A randomized delay between 0 and the current backoff determines when the next retry attempt is made. After each attempt, the current backoff is multiplied by `BackoffMultiplier`. A value is required and must be greater than zero.                 |
| `MaxBackoff`           | The maximum backoff places an upper limit on exponential backoff growth. A value is required and must be greater than zero.                                                                                                                                                                             |
| `BackoffMultiplier`    | The backoff will be multiplied by this value after each retry attempt and will increase exponentially when the multiplier is greater than 1. A value is required and must be greater than zero.                                                                                                         |
| `RetryableStatusCodes` | A collection of status codes. A gRPC call that fails with a matching status will be automatically retried. For more information about status codes, see [Status codes and their use in gRPC](https://grpc.github.io/grpc/core/md_doc_statuscodes.html). At least one retryable status code is required. |

## Hedging

Hedging is an alternative retry strategy. Hedging enables aggressively sending multiple copies of a single gRPC call without waiting for a response. Hedged gRPC calls may be executed multiple times on the server and the first successful result is used. It's important that hedging is only enabled for methods that are safe to execute multiple times without adverse effect.

Hedging has pros and cons when compared to retries:

* An advantage to hedging is it might return a successful result faster. It allows for multiple simultaneously gRPC calls and will complete when the first successful result is available.
* A disadvantage to hedging is it can be wasteful. Multiple calls could be made and all succeed. Only the first result is used and the rest are discarded.

## Configure a gRPC hedging policy

A hedging policy is configured like a retry policy. Note that a hedging policy can't be combined with a retry policy.

```
var defaultMethodConfig = new MethodConfig
{
    Names = { MethodName.Default },
    HedgingPolicy = new HedgingPolicy
    {
        MaxAttempts = 5,
        NonFatalStatusCodes = { StatusCode.Unavailable }
    }
};

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
    ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }
});
```

### gRPC hedging options

The following table describes options for configuring gRPC hedging policies.

| Option                | Description                                                                                                                                                                                                                                                                                                                                                                                         |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MaxAttempts`         | The hedging policy will send up to this number of calls. `MaxAttempts` represents the total number of all attempts, including the original attempt. This value is limited by `GrpcChannelOptions.MaxRetryAttempts` which defaults to 5. A value is required and must be 2 or greater.                                                                                                               |
| `HedgingDelay`        | The first call is sent immediately, subsequent hedging calls are delayed by this value. When the delay is set to zero or `null`, all hedged calls are sent immediately. `HedgingDelay` is optional and defaults to zero. A value must be zero or greater.                                                                                                                                           |
| `NonFatalStatusCodes` | A collection of status codes which indicate other hedge calls may still succeed. If a non-fatal status code is returned by the server, hedged calls will continue. Otherwise, outstanding requests will be canceled and the error returned to the app. For more information about status codes, see [Status codes and their use in gRPC](https://grpc.github.io/grpc/core/md_doc_statuscodes.html). |

## Related content

* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)
* [Retry general guidance - Best practices for cloud applications](/en-us/azure/architecture/best-practices/transient-faults)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/openapi/aspnetcore-openapi.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Generate OpenAPI documents

The [`Microsoft.AspNetCore.OpenApi`](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi) package provides built-in support for OpenAPI document generation in ASP.NET Core. The package provides the following features:

* Support for generating [OpenAPI version 3.1](https://spec.openapis.org/oas/v3.1.1.html) documents starting with .NET 10, and [OpenAPI version 3.2](https://spec.openapis.org/oas/v3.2.0.html) documents starting with .NET 11.
* Support for [JSON Schema draft 2020-12](https://json-schema.org/specification-links#2020-12).
* Support for generating OpenAPI documents at run time and accessing them via an endpoint on the app.
* Support for "transformer" APIs that allow modifying the generated document.
* Support for generating multiple OpenAPI documents from a single app.
* Takes advantage of JSON schema support provided by [System.Text.Json](/en-us/dotnet/api/system.text.json).
* Compatible with native AoT.

Starting in .NET 11, the default OpenAPI version for generated documents is 3.2. In .NET 10, the default is 3.1. To change the version, explicitly set the [OpenApiVersion](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.openapiversion) property of the [OpenApiOptions](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions) in the `configureOptions` delegate parameter of [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi):

```
builder.Services.AddOpenApi(options =>
{
    // Specify the OpenAPI version to use
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0;
});
```

When generating the OpenAPI document at build time, the OpenAPI version can be selected by setting the `--openapi-version` in the `OpenApiGenerateDocumentsOptions` MSBuild item.

```
<!-- Configure build-time OpenAPI generation to produce an OpenAPI 3.1 document -->
<OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_1</OpenApiGenerateDocumentsOptions>
```

## Package installation

Install the [`Microsoft.AspNetCore.OpenApi` package](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi):

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

Run the following command from the **Package Manager Console**:

```
Install-Package Microsoft.AspNetCore.OpenApi
```

Run the following command in a command shell opened to the directory that contains the app's project file:

```
dotnet add package Microsoft.AspNetCore.OpenApi
```

## Configure OpenAPI document generation

The following code:

* Adds OpenAPI services using the [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi) extension method on the app builder's service collection.
* Maps an endpoint for viewing the OpenAPI document in JSON format with the [MapOpenApi](/en-us/dotnet/api/microsoft.aspnetcore.builder.openapiendpointroutebuilderextensions.mapopenapi) extension method on the app.

```
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

Launch the app and navigate to `https://localhost:{PORT}/openapi/v1.json` to view the generated OpenAPI document, where the `{PORT}` placeholder is the port.

OpenAPI document generation applies the following behaviors:

* Unknown HTTP methods are excluded from the generated document. For example, the `QUERY` method is an HTTP method that isn't yet recognized by the OpenAPI specification, so it's gracefully excluded from the generated document.
* Numbers and dates are formatted using the invariant culture, so the generated document is consistent regardless of the server's culture settings.

## Options to Customize OpenAPI document generation

The following sections demonstrate how to customize OpenAPI document generation.

### Generate OpenAPI document in YAML format

The OpenAPI document can be generated in either JSON or YAML format. By default, the OpenAPI document is generated in JSON format. To generate the OpenAPI document in YAML format, specify the endpoint in the [MapOpenApi](/en-us/dotnet/api/microsoft.aspnetcore.builder.openapiendpointroutebuilderextensions.mapopenapi) call with a "`.yaml`" or "`.yml`" suffix, as shown in the following example, where the `{documentName}` placeholder is the document name:

```
app.MapOpenApi("/openapi/{documentName}.yaml");
```

Generating OpenAPI documents in YAML format at build time isn't supported but planned for a future preview.

### Customize the OpenAPI document name

Each OpenAPI document in an app has a unique name. The default document name that is registered is `v1`:

```
builder.Services.AddOpenApi(); // Document name is v1
```

The document name can be modified by passing the name as a parameter to the [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi) call:

```
builder.Services.AddOpenApi("internal"); // Document name is internal
```

The document name surfaces in several places in the OpenAPI implementation.

When fetching the generated OpenAPI document, the document name is provided as the `documentName` parameter argument in the request. The following requests resolve the `v1` and `internal` documents.

```
GET http://localhost:5000/openapi/v1.json
GET http://localhost:5000/openapi/internal.json
```

### Customize the OpenAPI version of a generated document

By default, OpenAPI document generation creates a document that is compliant with the OpenAPI specification. The following code demonstrates how to modify the default version of the OpenAPI document:

```
builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_0;
});
```

### Customize the OpenAPI endpoint route

By default, the OpenAPI endpoint registered via a call to [MapOpenApi](/en-us/dotnet/api/microsoft.aspnetcore.builder.openapiendpointroutebuilderextensions.mapopenapi) exposes the document at the `/openapi/{documentName}.json` endpoint. The following code demonstrates how to customize the route at which the OpenAPI document is registered:

```
app.MapOpenApi("/openapi/{documentName}/openapi.json");
```

It's possible, but not recommended, to remove the `documentName` route parameter from the endpoint route. When the `documentName` route parameter is removed from the endpoint route, the framework attempts to resolve the document name from the query parameter. Not providing the `documentName` in either the route or query can result in unexpected behavior.

### Customize the OpenAPI endpoint

Because the OpenAPI document is served via a route handler endpoint, any customization that is available to standard minimal endpoints is available to the OpenAPI endpoint.

#### Limit OpenAPI document access to authorized users

The OpenAPI endpoint doesn't enable authorization checks by default. However, authorization checks can be applied to the OpenAPI document. In the following code, access to the OpenAPI document is limited to users with the `tester` role:

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("ApiTesterPolicy", b => b.RequireRole("tester"));
});
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi()
    .RequireAuthorization("ApiTesterPolicy");

app.MapGet("/", () => "Hello world!");

app.Run();
```

#### Cache generated OpenAPI document

The OpenAPI document is regenerated every time a request to the OpenAPI endpoint is sent. Regeneration enables transformers to incorporate dynamic app state into their operation. For example, regenerating a request with details of the HTTP context. When applicable, the OpenAPI document can be cached to avoid executing the document generation pipeline on each HTTP request.

```
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromMinutes(10)));
});
builder.Services.AddOpenApi();

var app = builder.Build();

app.UseOutputCache();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi()
        .CacheOutput();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

## Generate multiple OpenAPI documents

In some scenarios, it's helpful to generate multiple OpenAPI documents with different content from a single ASP.NET Core API app. These scenarios include generating OpenAPI documentation for different:

* Audiences, such as public and internal APIs.
* Versions of an API.
* Parts of an app, such as a frontend and backend API.

To generate multiple OpenAPI documents, call the [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi) extension method once for each document, specifying a different document name in the first parameter each time.

```
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");
```

Each invocation of [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi) can specify its own set of options, so you can choose to use the same or different customizations for each OpenAPI document.

The framework uses the [ShouldInclude](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.shouldinclude#microsoft-aspnetcore-openapi-openapioptions-shouldinclude) delegate method of [OpenApiOptions](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions) to determine which endpoints to include in each document.

For each document, the [ShouldInclude](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.shouldinclude#microsoft-aspnetcore-openapi-openapioptions-shouldinclude) delegate method is called for each endpoint in the app, passing the [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription) object for the endpoint. The method returns a boolean value indicating whether the endpoint should be included in the document. The [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription) object:

* Contains information about the endpoint, such as the HTTP method, route, and response types.
* Metadata attached to the endpoint via attributes or extension methods.

The default implementation of this delegate uses the [GroupName](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription.groupname#microsoft-aspnetcore-mvc-apiexplorer-apidescription-groupname) field of [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription). The delegate is set on an endpoint using either the [WithGroupName](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withgroupname) extension method or the [EndpointGroupNameAttribute](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointgroupnameattribute) attribute. `WithGroupName` or the `EndpointGroupName` attribute determines which endpoints to include in the document. Any endpoint that hasn't been assigned a group name is included in all OpenAPI documents.

```
// Include endpoints without a group name or with a group name
// that matches the document name
ShouldInclude = (description) => description.GroupName == null || 
    description.GroupName == DocumentName;    
```

You can customize the [ShouldInclude](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.shouldinclude#microsoft-aspnetcore-openapi-openapioptions-shouldinclude) delegate method to include or exclude endpoints based on any criteria.

## Generate OpenAPI documents at build time

Note

Starting with .NET 9, ASP.NET Core includes built-in OpenAPI support. The [`Microsoft.AspNetCore.OpenApi`](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi) package provides OpenAPI document generation at runtime, and adding the [`Microsoft.Extensions.ApiDescription.Server` ](https://www.nuget.org/packages/Microsoft.Extensions.ApiDescription.Server)package enables build-time document generation.

ASP.NET Core generates OpenAPI documents only. Interactive UIs such as **Swagger UI** or **Scalar** are not included by default and must be added separately. For guidance on using these UI options, see [Use the generated OpenAPI documents](using-openapi-documents?view=aspnetcore-10.0).

In typical web apps, OpenAPI documents are generated at runtime and served via an HTTP request to the app server.

In some scenarios, it's helpful to generate the OpenAPI document during the app's build step. These scenarios include generating OpenAPI documentation that is:

* Committed into source control.
* Used for spec-based integration testing.
* Served statically from the web server.

To add support for generating OpenAPI documents at build time, install the [`Microsoft.Extensions.ApiDescription.Server` package](https://www.nuget.org/packages/Microsoft.Extensions.ApiDescription.Server):

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

Run the following command from the **Package Manager Console**:

```
Install-Package Microsoft.Extensions.ApiDescription.Server
```

Run the following command in a command shell opened to the directory that contains the app's project file:

```
dotnet add package Microsoft.Extensions.ApiDescription.Server
```

Upon installation, this package:

* Automatically generates the Open API documents associated with the app during build.
* Populates the Open API documents in the app's output directory.

If multiple documents are registered ***and*** the document name is ***not*** `v1`, the project name is post-fixed with the document name. Example: `{ProjectName}_{DocumentName}.json`. The `{ProjectName}` placeholder is the project name, and the `{DocumentName}` placeholder is the document name.

* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [.NET CLI](#tabpanel_3_netcore-cli)

```
dotnet build
type obj\{ProjectName}.json
```

```
dotnet build
cat obj/{ProjectName}.json
```

### View build-time OpenAPI logs (Terminal Logger)

When `Microsoft.Extensions.ApiDescription.Server` runs the **GetDocument** step during `dotnet build`, progress messages aren't visible with the .NET Terminal Logger at default verbosity in .NET 8 or later. To surface these messages while building, use either of the following options with the `dotnet build` command.

Set the Terminal Logger's verbosity with the `-tlp` option set to `v=d` (verbosity = detailed):

```
dotnet build -tlp:v=d
```

Disable the Terminal Logger and use legacy-style logs with the [`--tl` option](/en-us/dotnet/core/tools/dotnet-build#options) set to `off`:

```
dotnet build --tl:off
```

### Customize build-time document generation

#### Modify the output directory of the generated Open API file

By default, the generated OpenAPI document is emitted to the app's output directory. To modify the location of the emitted file, set the target path in the `OpenApiDocumentsDirectory` property:

```
<PropertyGroup>
  <OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
</PropertyGroup>
```

The value of `OpenApiDocumentsDirectory` is resolved relative to the project file. Using the `.` value, as seen in the preceding example, emits the OpenAPI document in the same directory as the project file.

#### Modify the output file name

By default, the generated OpenAPI document has the same name as the app's project file. To modify the name of the emitted file, set the `--file-name` argument in the `OpenApiGenerateDocumentsOptions` property:

```
<PropertyGroup>
  <OpenApiGenerateDocumentsOptions>--file-name my-open-api</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
```

#### Select the OpenAPI document to generate

Some apps may be configured to emit multiple OpenAPI documents. Multiple OpenAPI documents may be generated for different versions of an API or to distinguish between public and internal APIs. By default, the build-time document generator emits files for all documents that are configured in an app. To only emit for a single document name, set the `--document-name` argument in the `OpenApiGenerateDocumentsOptions` property:

```
<PropertyGroup>
  <OpenApiGenerateDocumentsOptions>--document-name v2</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
```

#### Select the app environment

Starting in .NET 11, set the `OpenApiGenerationEnvironment` property to select the app environment used during build-time OpenAPI document generation. The property sets the host's environment for the generation process, equivalent to setting the `ASPNETCORE_ENVIRONMENT` or `DOTNET_ENVIRONMENT` environment variable:

```
<PropertyGroup>
  <OpenApiGenerationEnvironment>Development</OpenApiGenerationEnvironment>
</PropertyGroup>
```

Selecting the environment enables environment-specific configuration, such as settings from `appsettings.Development.json`, and environment-dependent document transformations to affect the generated document. The property doesn't change the environment used when the app runs normally.

### Customize runtime behavior during build-time document generation

Build-time OpenAPI document generation functions by launching the apps entrypoint with a mock server implementation. A mock server is required to produce accurate OpenAPI documents because all information in the OpenAPI document can't be statically analyzed. Because the apps entrypoint is invoked, any logic in the apps startup is invoked. This includes code that injects services into the [DI container](../dependency-injection?view=aspnetcore-10.0) or reads from configuration. In some scenarios, it's necessary to restrict the code paths when the app's entry point is invoked from build-time document generation. These scenarios include:

* Not reading from certain configuration strings.
* Not registering database-related services.

In order to restrict invoking these code paths by the build-time generation pipeline, they can be conditioned behind a check of the entry assembly:

```
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

if (Assembly.GetEntryAssembly()?.GetName().Name != "GetDocument.Insider")
{
    builder.AddServiceDefaults();
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

var myKeyValue = app.Configuration["MyKey"];

app.MapGet("/", () => {
    return Results.Ok($"The value of MyKey is: {myKeyValue}");
})
.WithName("TestKey");

app.Run();
```

[`AddServiceDefaults`](https://source.dot.net/#TestingAppHost1.ServiceDefaults/Extensions.cs,0f0d863053754768,references) adds common Aspire services such as service discovery, resilience, health checks, and OpenTelemetry.

## Trimming and Native AOT

OpenAPI in ASP.NET Core supports trimming and native AOT. The following steps create and publish an OpenAPI app with trimming and native AOT.

Create a new ASP.NET Core Web API (Native AOT) project:

```
dotnet new webapiaot
```

Publish the app:

```
dotnet publish
```

Minimal APIs provide built-in support for generating information about endpoints in an app via the `Microsoft.AspNetCore.OpenApi` package. Exposing the generated OpenAPI definition via a visual UI requires a third-party package. For information about support for OpenAPI in controller-based APIs, see the [.NET 9 version of this article](?view=aspnetcore-9.0\&preserve-view=true).

The following code is generated by the ASP.NET Core Web API template and uses OpenAPI:

```
using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateTime.Now.AddDays(index),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

In the preceding highlighted code:

* `Microsoft.AspNetCore.OpenApi` is explained in the next section.
* [AddEndpointsApiExplorer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.endpointmetadataapiexplorerservicecollectionextensions.addendpointsapiexplorer) : Configures the app to use the API Explorer to discover and describe endpoints with default annotations. `WithOpenApi` overrides matching, default annotations generated by the API Explorer with those produced from the `Microsoft.AspNetCore.OpenApi` package.
* `UseSwagger`adds the [Swagger middleware](../../tutorials/getting-started-with-swashbuckle?view=aspnetcore-10.0#add-and-configure-swagger-middleware).
* \`UseSwaggerUI\` enables an embedded version of the Swagger UI tool.
* [WithName](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withname): The [IEndpointNameMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointnamemetadata) on the endpoint is used for link generation and is treated as the operation ID in the given endpoint's OpenAPI specification.
* [`WithOpenApi`](/en-us/dotnet/api/microsoft.aspnetcore.builder.openapiendpointconventionbuilderextensions.withopenapi) is explained later in this article.

[]()

## `Microsoft.AspNetCore.OpenApi` NuGet package

ASP.NET Core provides the [`Microsoft.AspNetCore.OpenApi`](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi/) package to interact with OpenAPI specifications for endpoints. The package acts as a link between the OpenAPI models that are defined in the `Microsoft.AspNetCore.OpenApi` package and the endpoints that are defined in Minimal APIs. The package provides an API that examines an endpoint's parameters, responses, and metadata to construct an OpenAPI annotation type that is used to describe an endpoint.

`Microsoft.AspNetCore.OpenApi` is added as a PackageReference to a project file:

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

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>    
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.*-*" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>

</Project>
```

When using [`Swashbuckle.AspNetCore`](https://www.nuget.org/packages/Swashbuckle.AspNetCore/) with `Microsoft.AspNetCore.OpenApi`, `Swashbuckle.AspNetCore` 6.4.0 or later must be used. [`Microsoft.OpenApi`](https://www.nuget.org/packages/Microsoft.OpenApi/) 1.4.3 or later must be used to leverage copy constructors in `WithOpenApi` invocations.

## Add OpenAPI annotations to endpoints via `WithOpenApi`

Calling [`WithOpenApi`](/en-us/dotnet/api/microsoft.aspnetcore.builder.openapiendpointconventionbuilderextensions.withopenapi) on the endpoint adds to the endpoint's metadata. This metadata can be:

* Consumed in third-party packages like [Swashbuckle.AspNetCore](https://www.nuget.org/packages/Swashbuckle.AspNetCore/).
* Displayed in the Swagger user interface or in YAML or JSON generated to define the API.

```
app.MapPost("/todoitems/{id}", async (int id, Todo todo, TodoDb db) =>
{
    todo.Id = id;
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi();
```

### Modify the OpenAPI annotation in `WithOpenApi`

The [`WithOpenApi`](/en-us/dotnet/api/microsoft.aspnetcore.builder.openapiendpointconventionbuilderextensions.withopenapi) method accepts a function that can be used to modify the OpenAPI annotation. For example, in the following code, a description is added to the first parameter of the endpoint:

```
app.MapPost("/todo2/{id}", async (int id, Todo todo, TodoDb db) =>
{
    todo.Id = id;
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi(generatedOperation =>
{
    var parameter = generatedOperation.Parameters[0];
    parameter.Description = "The ID associated with the created Todo";
    return generatedOperation;
});
```

## Add operation IDs to OpenAPI

Operation IDs are used to uniquely identify a given endpoint in OpenAPI. The [`WithName`](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withname) extension method can be used to set the operation ID used for a method.

```
app.MapGet("/todoitems2", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithName("GetToDoItems");
```

Alternatively, the `OperationId` property can be set directly on the OpenAPI annotation.

```
app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        OperationId = "GetTodos"
    });
```

## Add tags to the OpenAPI description

OpenAPI supports using [tag objects](https://swagger.io/docs/specification/grouping-operations-with-tags/) to categorize operations. These tags are typically used to group operations in the Swagger UI. These tags can be added to an operation by invoking the [WithTags](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.withtags) extension method on the endpoint with the desired tags.

```
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithTags("TodoGroup");
```

Alternatively, the list of `OpenApiTags` can be set on the OpenAPI annotation via the `WithOpenApi` extension method.

```
app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Tags = new List<OpenApiTag> { new() { Name = "Todos" } }
    });
```

## Add endpoint summary or description

The endpoint summary and description can be added by invoking the `WithOpenApi` extension method. In the following code, the summaries are set directly on the OpenAPI annotation.

```
app.MapGet("/todoitems2", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Summary = "This is a summary",
        Description = "This is a description"
    });
```

## Exclude OpenAPI description

In the following sample, the `/skipme` endpoint is excluded from generating an OpenAPI description:

```
using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/swag", () => "Hello Swagger!")
    .WithOpenApi();
app.MapGet("/skipme", () => "Skipping Swagger.")
                    .ExcludeFromDescription();

app.Run();
```

## Mark an API as obsolete

To mark an endpoint as obsolete, set the `Deprecated` property on the OpenAPI annotation.

```
app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Deprecated = true
    });
```

## Describe response types

OpenAPI supports providing a description of the responses returned from an API. Minimal APIs support three strategies for setting the response type of an endpoint:

* Via the [`Produces`](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.produces) extension method on the endpoint
* Via the [`ProducesResponseType`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) attribute on the route handler
* By returning [`TypedResults`](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) from the route handler

The `Produces` extension method can be used to add `Produces` metadata to an endpoint. When no parameters are provided, the extension method populates metadata for the targeted type under a `200` status code and an `application/json` content type.

```
app
    .MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .Produces<IList<Todo>>();
```

Using [`TypedResults`](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) in the implementation of an endpoint's route handler automatically includes the response type metadata for the endpoint. For example, the following code automatically annotates the endpoint with a response under the `200` status code with an `application/json` content type.

```
app.MapGet("/todos", async (TodoDb db) =>
{
    var todos = await db.Todos.ToListAsync());
    return TypedResults.Ok(todos);
});
```

### Set responses for `ProblemDetails`

When setting the response type for endpoints that may return a ProblemDetails response, the [ProducesProblem](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.producesproblem) extension method, [ProducesValidationProblem](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.producesvalidationproblem), or [`TypedResults.Problem`](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.problem?) can be used to add the appropriate annotation to the endpoint's metadata. Note that the `ProducesProblem` and `ProducesValidationProblem` extension methods can't be used with [route groups](../minimal-apis/route-handlers?view=aspnetcore-10.0#route-groups) in .NET 8 or earlier.

When there are no explicit annotations provided by one of the strategies above, the framework attempts to determine a default response type by examining the signature of the response. This default response is populated under the `200` status code in the OpenAPI definition.

### Multiple response types

If an endpoint can return different response types in different scenarios, you can provide metadata in the following ways:

* Call the [`Produces`](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.produces) extension method multiple times, as shown in the following example:

  ```
  app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
           await db.Todos.FindAsync(id) 
           is Todo todo
           ? Results.Ok(todo) 
           : Results.NotFound())
     .Produces<Todo>(StatusCodes.Status200OK)
     .Produces(StatusCodes.Status404NotFound);
  ```

* Use [`Results<TResult1,TResult2,TResultN>`](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.results-6) in the signature and [`TypedResults`](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) in the body of the handler, as shown in the following example:

  ```
  app.MapGet("/book/{id}", Results<Ok<Book>, NotFound> 
      (int id, List<Book> bookList) =>
      {
          return bookList.FirstOrDefault((i) => i.Id == id) is Book book
          ? TypedResults.Ok(book)
          : TypedResults.NotFound();
      });
  ```

  The `Results<TResult1,TResult2,TResultN>` [union types](https://en.wikipedia.org/wiki/Union_type) declare that a route handler returns multiple `IResult`-implementing concrete types, and any of those types that implement `IEndpointMetadataProvider` will contribute to the endpoint's metadata.

  The union types implement implicit cast operators. These operators enable the compiler to automatically convert the types specified in the generic arguments to an instance of the union type. This capability has the added benefit of providing compile-time checking that a route handler only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to `Results<TResult1,TResult2,TResultN>` results in a compilation error.

## Describe request body and parameters

In addition to describing the types that are returned by an endpoint, OpenAPI also supports annotating the inputs that are consumed by an API. These inputs fall into two categories:

* Parameters that appear in the path, query string, headers, or cookies
* Data transmitted as part of the request body

The framework infers the types for request parameters in the path, query, and header string automatically based on the signature of the route handler.

To define the type of inputs transmitted as the request body, configure the properties by using the [`Accepts`](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.accepts) extension method to define the object type and content type that are expected by the request handler. In the following example, the endpoint accepts a `Todo` object in the request body with an expected content-type of `application/xml`.

```
app.MapPost("/todos/{id}", (int id, Todo todo) => ...)
  .Accepts<Todo>("application/xml");
```

In addition to the [`Accepts`](/en-us/dotnet/api/microsoft.aspnetcore.http.openapiroutehandlerbuilderextensions.accepts) extension method, A parameter type can describe its own annotation by implementing the [`IEndpointParameterMetadataProvider`](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.iendpointparametermetadataprovider) interface. For example, the following `Todo` type adds an annotation that requires a request body with an `application/xml` content-type.

```
public class Todo : IEndpointParameterMetadataProvider
{
    public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ConsumesAttribute(typeof(Todo), isOptional: false, "application/xml"));
    }
}
```

When no explicit annotation is provided, the framework attempts to determine the default request type if there's a request body parameter in the endpoint handler. The inference uses the following heuristics to produce the annotation:

* Request body parameters that are read from a form via the [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) attribute are described with the `multipart/form-data` content-type.
* All other request body parameters are described with the `application/json` content-type.
* The request body is treated as optional if it's nullable or if the [`AllowEmpty`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute.microsoft-aspnetcore-http-metadata-ifrombodymetadata-allowempty) property is set on the [`FromBody`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) attribute.

## Support API versioning

Minimal APIs support API versioning via the [Asp.Versioning.Http package](https://www.nuget.org/packages/Asp.Versioning.Http). Examples of configuring versioning with Minimal APIs can be found in [the API versioning repo](https://github.com/dotnet/aspnet-api-versioning/tree/3857a332057d970ad11bac0edfdbff8a559a215d/examples/AspNetCore/WebApi).

## ASP.NET Core OpenAPI source code on GitHub

* [`WithOpenApi`](https://github.com/dotnet/aspnetcore/blob/8a4b4deb09c04134f22f8d39aae21d212282004f/src/OpenApi/src/OpenApiRouteHandlerBuilderExtensions.cs)
* [`OpenApiGenerator`](https://github.com/dotnet/aspnetcore/blob/main/src/OpenApi/src/Services/OpenApiGenerator.cs)

## Related content

* [Authentication and authorization in Minimal APIs](../minimal-apis/security?view=aspnetcore-10.0)

A Minimal API app can describe the [OpenAPI specification](https://swagger.io/specification/) for route handlers using [Swashbuckle](https://www.nuget.org/packages/Swashbuckle.AspNetCore/).

For information about support for OpenAPI in controller-based APIs, see the [.NET 9 version of this article](?view=aspnetcore-9.0\&preserve-view=true).

The following code is a typical ASP.NET Core app with OpenAPI support:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = builder.Environment.ApplicationName,
                               Version = "v1" });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger(); // UseSwaggerUI Protected by if (env.IsDevelopment())
    app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
                                    $"{builder.Environment.ApplicationName} v1"));
}

app.MapGet("/swag", () => "Hello Swagger!");

app.Run();
```

## Exclude OpenAPI description

In the following sample, the `/skipme` endpoint is excluded from generating an OpenAPI description:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(); // UseSwaggerUI Protected by if (env.IsDevelopment())
}

app.MapGet("/swag", () => "Hello Swagger!");
app.MapGet("/skipme", () => "Skipping Swagger.")
                    .ExcludeFromDescription();

app.Run();
```

## Describe response types

The following example uses the built-in result types to customize the response:

```
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
```

## Add operation ids to OpenAPI

```
app.MapGet("/todoitems2", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithName("GetToDoItems");
```

## Add tags to the OpenAPI description

The following code uses an [OpenAPI grouping tag](https://swagger.io/docs/specification/grouping-operations-with-tags/):

```
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithTags("TodoGroup");
```

The [`Microsoft.AspNetCore.OpenApi`](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi) package provides built-in support for OpenAPI document generation in ASP.NET Core. The package provides the following features:

* Support for generating OpenAPI documents at run time and accessing them via an endpoint on the app.
* Support for "transformer" APIs that allow modifying the generated document.
* Support for generating multiple OpenAPI documents from a single app.
* Takes advantage of JSON schema support provided by [`System.Text.Json`](/en-us/dotnet/api/system.text.json).
* Is compatible with native AoT.

## Package installation

Install the `Microsoft.AspNetCore.OpenApi` package:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

Run the following command from the **Package Manager Console**:

```
Install-Package Microsoft.AspNetCore.OpenApi
```

Run the following command:

```
dotnet add package Microsoft.AspNetCore.OpenApi
```

## Configure OpenAPI document generation

The following code:

* Adds OpenAPI services using the [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi) extension method on the app builder's service collection.
* Maps an endpoint for viewing the OpenAPI document in JSON format with the [MapOpenApi](/en-us/dotnet/api/microsoft.aspnetcore.builder.openapiendpointroutebuilderextensions.mapopenapi) extension method on the app.

```
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

Launch the app and navigate to `https://localhost:<port>/openapi/v1.json` to view the generated OpenAPI document.

## Options to Customize OpenAPI document generation

The following sections demonstrate how to customize OpenAPI document generation.

### Customize the OpenAPI document name

Each OpenAPI document in an app has a unique name. The default document name that is registered is `v1`.

```
builder.Services.AddOpenApi(); // Document name is v1
```

The document name can be modified by passing the name as a parameter to the [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi) call.

```
builder.Services.AddOpenApi("internal"); // Document name is internal
```

The document name surfaces in several places in the OpenAPI implementation.

When fetching the generated OpenAPI document, the document name is provided as the `documentName` parameter argument in the request. The following requests resolve the `v1` and `internal` documents.

```
GET http://localhost:5000/openapi/v1.json
GET http://localhost:5000/openapi/internal.json
```

### Customize the OpenAPI version of a generated document

By default, OpenAPI document generation creates a document that is compliant with [v3.0 of the OpenAPI specification](https://spec.openapis.org/oas/v3.0.0). The following code demonstrates how to modify the default version of the OpenAPI document:

```
builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = OpenApiSpecVersion.OpenApi2_0;
});
```

### Customize the OpenAPI endpoint route

By default, the OpenAPI endpoint registered via a call to [MapOpenApi](/en-us/dotnet/api/microsoft.aspnetcore.builder.openapiendpointroutebuilderextensions.mapopenapi) exposes the document at the `/openapi/{documentName}.json` endpoint. The following code demonstrates how to customize the route at which the OpenAPI document is registered:

```
app.MapOpenApi("/openapi/{documentName}/openapi.json");
```

It's possible, but not recommended, to remove the `documentName` route parameter from the endpoint route. When the `documentName` route parameter is removed from the endpoint route, the framework attempts to resolve the document name from the query parameter. Not providing the `documentName` in either the route or query can result in unexpected behavior.

### Customize the OpenAPI endpoint

Because the OpenAPI document is served via a route handler endpoint, any customization that is available to standard minimal endpoints is available to the OpenAPI endpoint.

#### Limit OpenAPI document access to authorized users

The OpenAPI endpoint doesn't enable any authorization checks by default. However, authorization checks can be applied to the OpenAPI document. In the following code, access to the OpenAPI document is limited to those with the `tester` role:

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("ApiTesterPolicy", b => b.RequireRole("tester"));
});
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi()
    .RequireAuthorization("ApiTesterPolicy");

app.MapGet("/", () => "Hello world!");

app.Run();
```

#### Cache generated OpenAPI document

The OpenAPI document is regenerated every time a request to the OpenAPI endpoint is sent. Regeneration enables transformers to incorporate dynamic app state into their operation. For example, regenerating a request with details of the HTTP context. When applicable, the OpenAPI document can be cached to avoid executing the document generation pipeline on each HTTP request.

```
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromMinutes(10)));
});
builder.Services.AddOpenApi();

var app = builder.Build();

app.UseOutputCache();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi()
        .CacheOutput();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

## Generate multiple OpenAPI documents

In some scenarios, it's helpful to generate multiple OpenAPI documents with different content from a single ASP.NET Core API app. These scenarios include:

* Generating OpenAPI documentation for different audiences, such as public and internal APIs.
* Generating OpenAPI documentation for different versions of an API.
* Generating OpenAPI documentation for different parts of an app, such as a frontend and backend API.

To generate multiple OpenAPI documents, call the [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi) extension method once for each document, specifying a different document name in the first parameter each time.

```
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");
```

Each invocation of [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi) can specify its own set of options, so that you can choose to use the same or different customizations for each OpenAPI document.

The framework uses the [ShouldInclude](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.shouldinclude#microsoft-aspnetcore-openapi-openapioptions-shouldinclude) delegate method of [OpenApiOptions](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions) to determine which endpoints to include in each document.

For each document, the [ShouldInclude](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.shouldinclude#microsoft-aspnetcore-openapi-openapioptions-shouldinclude) delegate method is called for each endpoint in the app, passing the [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription) object for the endpoint. The method returns a boolean value indicating whether the endpoint should be included in the document. The [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription) object:

* contains information about the endpoint, such as the HTTP method, route, and response types
* Metadata attached to the endpoint via attributes or extension methods.

The default implementation of this delegate uses the [GroupName](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription.groupname#microsoft-aspnetcore-mvc-apiexplorer-apidescription-groupname) field of [ApiDescription](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer.apidescription). The delegate is set on an endpoint using either the [WithGroupName](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.withgroupname) extension method or the [EndpointGroupNameAttribute](/en-us/dotnet/api/microsoft.aspnetcore.routing.endpointgroupnameattribute) attribute. `WithGroupName` or the `EndpointGroupName` attribute determines which endpoints to include in the document. Any endpoint that has not been assigned a group name is included all OpenAPI documents.

```
    // Include endpoints without a group name or with a group name that matches the document name
    ShouldInclude = (description) => description.GroupName == null || description.GroupName == DocumentName;    
```

You can customize the [ShouldInclude](/en-us/dotnet/api/microsoft.aspnetcore.openapi.openapioptions.shouldinclude#microsoft-aspnetcore-openapi-openapioptions-shouldinclude) delegate method to include or exclude endpoints based on any criteria you choose.

## Generate OpenAPI documents at build-time

In typical web apps, OpenAPI documents are generated at run-time and served via an HTTP request to the app server.

In some scenarios, it's helpful to generate the OpenAPI document during the app's build step. These scenarios include:

* Generating OpenAPI documentation that is committed into source control.
* Generating OpenAPI documentation that is used for spec-based integration testing.
* Generating OpenAPI documentation that is served statically from the web server.

To add support for generating OpenAPI documents at build time, install the `Microsoft.Extensions.ApiDescription.Server` package:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

Run the following command from the **Package Manager Console**:

```
Install-Package Microsoft.Extensions.ApiDescription.Server
```

Run the following command in the directory that contains the project file:

```
dotnet add package Microsoft.Extensions.ApiDescription.Server
```

Upon installation, this package:

* Automatically generates the Open API documents associated with the app during build.
* Populates the Open API documents in the app's output directory.

If multiple documents are registered, ***and*** document name is ***not*** `v1`, it's post-fixed with the document name. E.g., `{ProjectName}_{DocumentName}.json`.

* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [.NET CLI](#tabpanel_3_netcore-cli)

```
dotnet build
type obj\{ProjectName}.json
```

```
dotnet build
cat obj/{ProjectName}.json
```

### Customizing build-time document generation

#### Modifying the output directory of the generated Open API file

By default, the generated OpenAPI document will be emitted to the app's output directory. To modify the location of the emitted file, set the target path in the `OpenApiDocumentsDirectory` property.

```
<PropertyGroup>
  <OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
</PropertyGroup>
```

The value of `OpenApiDocumentsDirectory` is resolved relative to the project file. Using the `.` value above will emit the OpenAPI document in the same directory as the project file.

#### Modifying the output file name

By default, the generated OpenAPI document will have the same name as the app's project file. To modify the name of the emitted file, set the `--file-name` argument in the `OpenApiGenerateDocumentsOptions` property.

```
<PropertyGroup>
  <OpenApiGenerateDocumentsOptions>--file-name my-open-api</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
```

#### Selecting the OpenAPI document to generate

Some apps may be configured to emit multiple OpenAPI documents. Multiple OpenAPI documents may be generated for different versions of an API or to distinguish between public and internal APIs. By default, the build-time document generator emits files for all documents that are configured in an app. To only emit for a single document name, set the `--document-name` argument in the `OpenApiGenerateDocumentsOptions` property.

```
<PropertyGroup>
  <OpenApiGenerateDocumentsOptions>--document-name v2</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
```

### Customizing run-time behavior during build-time document generation

Build-time OpenAPI document generation functions by launching the apps entrypoint with a mock server implementation. A mock server is required to produce accurate OpenAPI documents because all information in the OpenAPI document can't be statically analyzed. Because the apps entrypoint is invoked, any logic in the apps startup is invoked. This includes code that injects services into the [DI container](../dependency-injection?view=aspnetcore-10.0) or reads from configuration. In some scenarios, it's necessary to restrict the code paths that will run when the apps entry point is being invoked from build-time document generation. These scenarios include:

* Not reading from certain configuration strings.
* Not registering database-related services.

In order to restrict these code paths from being invoked by the build-time generation pipeline, they can be conditioned behind a check of the entry assembly:

```
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

if (Assembly.GetEntryAssembly()?.GetName().Name != "GetDocument.Insider")
{
    builder.AddServiceDefaults();
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

var myKeyValue = app.Configuration["MyKey"];

app.MapGet("/", () => {
    return Results.Ok($"The value of MyKey is: {myKeyValue}");
})
.WithName("TestKey");

app.Run();
```

[AddServiceDefaults](https://source.dot.net/#TestingAppHost1.ServiceDefaults/Extensions.cs,0f0d863053754768,references) adds common Aspire services such as service discovery, resilience, health checks, and OpenTelemetry.

## Trimming and Native AOT

OpenAPI in ASP.NET Core supports trimming and native AOT. The following steps create and publish an OpenAPI app with trimming and native AOT:

Create a new ASP.NET Core Web API (Native AOT) project.

```
dotnet new webapiaot
```

Add the Microsoft.AspNetCore.OpenAPI package.

```
dotnet add package Microsoft.AspNetCore.OpenApi
```

Update `Program.cs` to enable generating OpenAPI documents.

```
+ builder.Services.AddOpenApi();

var app = builder.Build();

+ app.MapOpenApi();
```

Publish the app.

```
dotnet publish
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/social/facebook-logins.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Facebook external login setup in ASP.NET Core

By [Valeriy Novytskyy](https://github.com/01binary) and [Rick Anderson](https://twitter.com/RickAndMSFT)

This tutorial with code examples shows how to enable your users to sign in with their Facebook account using a sample ASP.NET Core project created on the [previous page](./?view=aspnetcore-10.0).

## Create the app in Facebook

* Add the [Microsoft.AspNetCore.Authentication.Facebook](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook) NuGet package to the project.

* Follow the [Facebook App Registration](https://developers.facebook.com/docs/apps/register) instructions to create a Facebook app and obtain your **App ID** and **App Secret**.

* Follow the [Facebook Login for Web](https://developers.facebook.com/docs/facebook-login/web) instructions to configure Facebook Login for your app. Add your development URI with */signin-facebook* appended (for example: `https://localhost:44320/signin-facebook`) to the **Valid OAuth Redirect URIs**.

  The Facebook authentication configured later in this tutorial automatically handles requests at the */signin-facebook* route to implement the OAuth flow.

Note

The URI */signin-facebook* is set as the default callback of the Facebook authentication provider. You can change the default callback URI while configuring the Facebook authentication middleware via the inherited [RemoteAuthenticationOptions.CallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.callbackpath) property of the [FacebookOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.facebook.facebookoptions) class.

* Follow the [Facebook Login Permissions](https://developers.facebook.com/docs/facebook-login/permissions) guide to enable the **email** permission for your app. The ASP.NET Core Facebook authentication middleware requests the `email` scope by default. If the **email** permission isn't enabled on your Facebook app, authentication may fail or the user's email address will be missing after sign-in.

* Make a note of your **App ID** and **App Secret**. You add both into your ASP.NET Core application in the next section.

* When deploying the site, revisit the **Facebook Login** setup page and register a new public URI.

## Store the Facebook app ID and secret

Store sensitive settings such as the Facebook app ID and secret values with [Secret Manager](../../app-secrets?view=aspnetcore-10.0). For this sample, use the following steps:

1. Initialize the project for secret storage per the instructions at [Enable secret storage](../../app-secrets?view=aspnetcore-10.0#enable-secret-storage).

2. Store the sensitive settings in the local secret store with the secret keys `Authentication:Facebook:AppId` and `Authentication:Facebook:AppSecret`:

   ```
   dotnet user-secrets set "Authentication:Facebook:AppId" "<app-id>"
   dotnet user-secrets set "Authentication:Facebook:AppSecret" "<app-secret>"
   ```

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For example, [Bash](https://linuxhint.com/bash-environment-variables/) doesn't support colon (`:`) as a separator. All platforms support the double underscore (`__`) syntax and automatically replace it with a colon (`:`).

## Configure Facebook Authentication

Add the Authentication service to the `Startup.ConfigureServices`:

```
services.AddAuthentication().AddFacebook(facebookOptions =>
{
    facebookOptions.AppId = Configuration["Authentication:Facebook:AppId"];
    facebookOptions.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
});
```

Add the Authentication service to the `Program`:

```
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;

services.AddAuthentication().AddFacebook(facebookOptions =>
    {
        facebookOptions.AppId = configuration["Authentication:Facebook:AppId"];
        facebookOptions.AppSecret = configuration["Authentication:Facebook:AppSecret"];
    });
```

The [AddAuthentication(IServiceCollection, String)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-string\)) overload sets the [DefaultScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultscheme) property. The [AddAuthentication(IServiceCollection, Action\<AuthenticationOptions>)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-action\(\(microsoft-aspnetcore-authentication-authenticationoptions\)\)\)) overload allows configuring authentication options, which can be used to set up default authentication schemes for different purposes. Subsequent calls to `AddAuthentication` override previously configured [AuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.authenticationoptions) properties.

[AuthenticationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder) extension methods that register an authentication handler may only be called once per authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display name.

## Sign in with Facebook

* Run the app and select **Log in**.
* Under **Use another service to log in.**, select Facebook.
* You are redirected to **Facebook** for authentication.
* Enter your Facebook credentials.
* You are redirected back to your site where you can set your email.

You are now logged in using your Facebook credentials.

[]()

## React to cancel authorize external sign-in

[AccessDeniedPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.accessdeniedpath#microsoft-aspnetcore-authentication-remoteauthenticationoptions-accessdeniedpath) can provide a redirect path to the user agent when the user doesn't approve the requested authorization demand.

The following code sets the `AccessDeniedPath` to `"/AccessDeniedPathInfo"`:

```
services.AddAuthentication().AddFacebook(options =>
{
    options.AppId = Configuration["Authentication:Facebook:AppId"];
    options.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
    options.AccessDeniedPath = "/AccessDeniedPathInfo";
});
```

We recommend the `AccessDeniedPath` page contains the following information:

* Remote authentication was canceled.
* This app requires authentication.
* To try sign-in again, select the Login link.

### Test AccessDeniedPath

* Navigate to [facebook.com](https://www.facebook.com/)
* If you are signed in, you must sign out.
* Run the app and select Facebook sign-in.
* Select **Not now**. You are redirected to the specified `AccessDeniedPath` page.

## Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (`https`), host, and client IP address. Apps don't automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (`https`) results in the app generating incorrect insecure redirect URLs.

Use forwarded headers middleware to make the original request information available to the app for request processing.

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

For more information on configuration options supported by Facebook authentication, see the [FacebookOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.facebookoptions) API reference. Configuration options can be used to:

* Request different information about the user.
* Add query string arguments to customize the login experience.

## Troubleshooting

* **ASP.NET Core 2.x only:** If Identity isn't configured by calling `services.AddIdentity` in `ConfigureServices`, attempting to authenticate will result in *ArgumentException: The 'SignInScheme' option must be provided*. The project template used in this tutorial ensures that this is done.
* If the site database has not been created by applying the initial migration, you get *A database operation failed while processing the request* error. Tap **Apply Migrations** to create the database and refresh to continue past the error.
* If you receive an error during Facebook sign-in or the user's email address is missing after sign-in, verify that the **email** permission is enabled for your Facebook app. See the [Facebook Login Permissions](https://developers.facebook.com/docs/facebook-login/permissions) guide for details on enabling permissions in the Facebook Developer portal.

## Next steps

* This article showed how you can authenticate with Facebook. You can follow a similar approach to authenticate with other providers listed on the [previous page](./?view=aspnetcore-10.0).

* Once you publish your web site to Azure web app, you should reset the `AppSecret` in the Facebook developer portal.

* Set the `Authentication:Facebook:AppId` and `Authentication:Facebook:AppSecret` as application settings in the Azure portal. The configuration system is set up to read keys from environment variables.

## Related content

[Multiple authentication providers](./?view=aspnetcore-10.0#multiple-authentication-providers)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/server/memory-management
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/server/memory-management.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Manage memory in deployed ASP.NET Core server-side Blazor apps

This article explains how to manage memory use in deployed ASP.NET Core server-side Blazor apps.

On the server, a new circuit is created for each user session. Each user session corresponds to rendering a single document in the browser. For example, multiple tabs create multiple sessions.

Blazor maintains a constant connection to the browser, called a *circuit*, that initiated the session. Connections can be lost at any time for any of several reasons, such as when the user loses network connectivity or abruptly closes the browser. When a connection is lost, Blazor has a recovery mechanism that places a limited number of circuits in a "disconnected" pool, giving clients a limited amount of time to reconnect and re-establish the session (default: 3 minutes).

After that time, Blazor releases the circuit and discards the session. From that point on, the circuit is eligible for garbage collection (GC) and is claimed when a collection for the circuit's GC generation is triggered. One important aspect to understand is that circuits have a long lifetime, which means that most of the objects rooted by the circuit eventually reach Gen 2. As a result, you might not see those objects released until a Gen 2 collection happens.

## Measure memory usage in general

Prerequisites:

* The app must be published in **Release** configuration. **Debug** configuration measurements aren't relevant, as the generated code isn't representative of the code used for a production deployment.

* The app must run without a debugger attached, as this might also affect the behavior of the app and spoil the results. In Visual Studio, start the app without debugging by selecting **Debug** > **Start Without Debugging** from the menu bar or `Ctrl`+`F5` using the keyboard.

* Consider the different types of memory to understand how much memory is actually used by .NET. Generally, developers inspect app memory usage in Task Manager on Windows OS, which typically offers an upper bound of the actual memory in use. For more information, consult the following articles:

  * [.NET Memory Performance Analysis](https://github.com/Maoni0/mem-doc/blob/master/doc/.NETMemoryPerformanceAnalysis.md): In particular, see the section on [Memory Fundamentals](https://github.com/Maoni0/mem-doc/blob/master/doc/.NETMemoryPerformanceAnalysis.md#Memory-Fundamentals).
  * [Work flow of diagnosing memory performance issues (three-part series)](https://devblogs.microsoft.com/dotnet/work-flow-of-diagnosing-memory-performance-issues-part-0/): Links to the three articles of the series are at the top of each article in the series.

## Memory usage applied to Blazor

We compute the memory used by blazor as follows:

(**Active Circuits** × **Per-circuit Memory**) + (**Disconnected Circuits** × **Per-circuit Memory**)

The amount of memory a circuit uses and the maximum potential active circuits that an app can maintain is largely dependent on how the app is written. The maximum number of possible active circuits is roughly described by:

**Maximum Available Memory** / **Per-circuit Memory** = **Maximum Potential Active Circuits**

For a memory leak to occur in Blazor, the following must be true:

* The memory must be allocated by the framework, not the app. If you allocate a 1 GB array in the app, the app must manage the disposal of the array.
* The memory must not be actively used, which means the circuit isn't active and has been evicted from the disconnected circuits cache. If you have the maximum active circuits running, running out of memory is a scale issue, not a memory leak.
* A garbage collection (GC) for the circuit's GC generation has run, but the garbage collector hasn't been able to claim the circuit because another object in the framework is holding a strong reference to the circuit.

In other cases, there's no memory leak. If the circuit is active (connected or disconnected), the circuit is still in use.

If a collection for the circuit's GC generation doesn't run, the memory isn't released because the garbage collector doesn't need to free the memory at that time.

If a collection for a GC generation runs and frees the circuit, you must validate the memory against the GC stats, not the process, as .NET might decide to keep the virtual memory active.

If the memory isn't freed, you must find a circuit that isn't either active or disconnected and that's rooted by another object in the framework. In any other case, the inability to free memory is an app issue in developer code.

## Reduce memory usage

Adopt any of the following strategies to reduce an app's memory usage:

* Publish the app in Release configuration.
* Run a published version of the app.
* Don't attach a debugger to the running app.
* Does triggering a Gen 2 forced, compacting collection (`GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true))` free the memory?
* Consider if your app is allocating objects on the large object heap.
* Are you testing the memory growth after the app is warmed up with requests and processing? Typically, there are caches that are populated when code executes for the first time that add a constant amount of memory to the footprint of the app.
* Limit the total amount of memory used by the .NET process. For more information, see [Runtime configuration options for garbage collection](/en-us/dotnet/core/runtime-config/garbage-collector).
* Reduce the number of disconnected circuits.
* Reduce the time a circuit is allowed to be in the disconnected state.
* Trigger a garbage collection manually to perform a collection during downtime periods.
* Configure the garbage collection in Workstation mode, which aggressively triggers garbage collection, instead of Server mode.

## Heap size for some mobile device browsers

When building a Blazor app that runs on the client and targets mobile device browsers, especially Safari on iOS, decreasing the maximum memory for the app with the MSBuild property `EmccMaximumHeapSize` may be required. For more information, see [Host and deploy ASP.NET Core Blazor WebAssembly](../webassembly/?view=aspnetcore-10.0#decrease-maximum-heap-size-for-some-mobile-device-browsers).

## Additional actions and considerations

* Capture a memory dump of the process when memory demands are high and identify the objects are taking the most memory and where are those objects are rooted (what holds a reference to them).
* You can examine the statistics on how memory in your app is behaving using `dotnet-counters`. For more information see [Investigate performance counters (dotnet-counters)](/en-us/dotnet/core/diagnostics/dotnet-counters).
* Even when a GC is triggered, .NET holds on to the memory instead of returning it to the OS immediately, as it's likely that it will reuse the memory the near future. This avoids committing and decommitting memory constantly, which is expensive. You'll see this reflected if you use `dotnet-counters` because you'll see the GCs happen and the amount of used memory go down to 0 (zero), but you won't see the working set counter decrease, which is the sign that .NET is holding on to the memory to reuse it. For more information on project file (`.csproj`) settings to control this behavior, see [Runtime configuration options for garbage collection](/en-us/dotnet/core/runtime-config/garbage-collector).
* Server GC doesn't trigger garbage collections until it determines it's absolutely necessary to do so to avoid freezing your app and considers that your app is the only thing running on the machine, so it can use all the memory in the system. If the system has 50 GB, the garbage collector seeks to use the full 50 GB of available memory before it triggers a Gen 2 collection.
* For information on disconnected circuit retention configuration, see [ASP.NET Core Blazor SignalR guidance](../../fundamentals/signalr?view=aspnetcore-10.0#server-side-circuit-handler-options).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/protocols
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/protocols.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use ASP.NET Core with HTTP/2 on IIS

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Justin Kotalik](https://github.com/jkotalik)

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is supported with ASP.NET Core in the following IIS deployment scenarios:

* Windows Server 2016 or later / Windows 10 or later
* IIS 10 or later
* TLS 1.2 or later connection
* When [hosting out-of-process](./?view=aspnetcore-10.0#out-of-process-hosting-model): Public-facing edge server connections use HTTP/2, but the reverse proxy connection to the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) uses HTTP/1.1.

For an in-process deployment when an HTTP/2 connection is established, [`HttpRequest.Protocol`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) reports `HTTP/2`. For an out-of-process deployment when an HTTP/2 connection is established, [`HttpRequest.Protocol`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) reports `HTTP/1.1`.

For more information on the in-process and out-of-process hosting models, see [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0).

HTTP/2 is enabled by default for HTTPS/TLS connections. Connections fall back to HTTP/1.1 if an HTTP/2 connection isn't established. For more information on HTTP/2 configuration with IIS deployments, see [HTTP/2 on IIS](/en-us/iis/get-started/whats-new-in-iis-10/http2-on-iis).

## Advanced HTTP/2 features to support gRPC

Additional HTTP/2 features in IIS support gRPC, including support for response trailers and sending reset frames.

Requirements to run gRPC on IIS:

* In-process hosting.
* Windows 11 Build 22000 or later, Windows Server 2022 Build 20348 or later.
* TLS 1.2 or later connection.

### Trailers

HTTP Trailers are similar to HTTP Headers, except they are sent after the response body is sent. For IIS and HTTP.sys, only HTTP/2 response trailers are supported.

```
if (httpContext.Response.SupportsTrailers())
{
    httpContext.Response.DeclareTrailer("trailername");	

    // Write body
    httpContext.Response.WriteAsync("Hello world");

    httpContext.Response.AppendTrailer("trailername", "TrailerValue");
}
```

In the preceding example code:

* `SupportsTrailers` ensures that trailers are supported for the response.
* `DeclareTrailer` adds the given trailer name to the `Trailer` response header. Declaring a response's trailers is optional, but recommended. If `DeclareTrailer` is called, it must be before the response headers are sent.
* `AppendTrailer` appends the trailer.

### Reset

Reset allows for the server to reset a HTTP/2 request with a specified error code. A reset request is considered aborted.

```
var resetFeature = httpContext.Features.Get<IHttpResetFeature>();
resetFeature.Reset(errorCode: 2);
```

`Reset` in the preceding code example specifies the `INTERNAL_ERROR` error code. For more information about HTTP/2 error codes, visit the [HTTP/2 specification error code section](https://tools.ietf.org/html/rfc7540#page-50).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Account confirmation and password recovery in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure an ASP.NET Core Blazor WebAssembly app with ASP.NET Core Identity with email confirmation and password recovery.

Note

This article only applies standalone Blazor WebAssembly apps with ASP.NET Core Identity. To implement email confirmation and password recovery for Blazor Web Apps, see [Account confirmation and password recovery in ASP.NET Core Blazor](../../account-confirmation-and-password-recovery?view=aspnetcore-10.0).

## Namespaces and article code examples

The namespaces used by the examples in this article are:

* `Backend` for the backend server web API project ("server project" in this article).
* `BlazorWasmAuth` for the front-end client standalone Blazor WebAssembly app ("client project" in this article).

These namespaces correspond to the projects in the `BlazorWebAssemblyStandaloneWithIdentity` sample solution in the [`dotnet/blazor-samples` GitHub repository](https://github.com/dotnet/blazor-samples). For more information, see [Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](./?view=aspnetcore-10.0#sample-apps).

If you aren't using the `BlazorWebAssemblyStandaloneWithIdentity` sample solution, change the namespaces in the code examples to use the namespaces of your projects.

## Select and configure an email provider for the server project

In this article, [Mailchimp's Transactional API](https://mailchimp.com/developer/transactional/api/) is used via [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) to send email. We recommend using an email service to send email rather than SMTP. SMTP is difficult to configure and secure properly. Whichever email service you use, access their guidance for .NET apps, create an account, configure an API key for their service, and install any NuGet packages required.

In the server project, create a class to hold the secret email provider API key. The example in this article uses a class named `AuthMessageSenderOptions` with an `EmailAuthKey` property to hold the key.

`AuthMessageSenderOptions.cs`:

```
namespace Backend;

public class AuthMessageSenderOptions
{
    public string? EmailAuthKey { get; set; }
}
```

Register the `AuthMessageSenderOptions` configuration instance in the server project's `Program` file:

```
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);
```

## Configure a secret for the email provider's security key

Receive the email provider's security key from the provider and use it in the following guidance.

Use either or both of the following approaches to supply the secret to the app:

* [Secret Manager tool](#secret-manager-tool): The Secret Manager tool stores private data on the local machine and is only used during local development.
* [Azure Key Vault](#azure-key-vault): You can store the secret in a key vault for use in any environment, including for the `Development` environment when working locally. Some developers prefer to use key vaults for staging and production deployments and use the [Secret Manager tool](#secret-manager-tool) for local development.

We strongly recommend that you avoid storing secrets in project code or configuration files. Use secure authentication flows, such as either or both of the approaches in this section.

## Secret Manager Tool

If the server project has already been initialized for the [Secret Manager tool](../../../../security/app-secrets?view=aspnetcore-10.0), it will already have a user secrets identifier (`<UserSecretsId>`) in its project file (`.csproj`). In Visual Studio, you can tell if the user secrets ID is present by looking at the **Properties** panel when the project is selected in **Solution Explorer**. If the app hasn't been initialized, execute the following command in a command shell opened to the server project's directory. In Visual Studio, you can use the Developer PowerShell command prompt (use the `cd` command to change the directory to the server project after you open the command shell).

```
dotnet user-secrets init
```

Set the API key with the Secret Manager tool. In the following example, the key name is `EmailAuthKey` to match `AuthMessageSenderOptions.EmailAuthKey`, and the key is represented by the `{KEY}` placeholder. Execute the following command with the API key:

```
dotnet user-secrets set "EmailAuthKey" "{KEY}"
```

If using Visual Studio, you can confirm the secret is set by right-clicking the server project in **Solution Explorer** and selecting **Manage User Secrets**.

For more information, see [Safe storage of app secrets in development](../../../../security/app-secrets?view=aspnetcore-10.0).

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](../../?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

### Azure Key Vault

[Azure Key Vault](https://azure.microsoft.com/products/key-vault/) provides a safe approach for providing the app's client secret to the app.

To create a key vault and set a secret, see [About Azure Key Vault secrets (Azure documentation)](/en-us/azure/key-vault/secrets/about-secrets), which cross-links resources to get started with Azure Key Vault. To implement the code in this section, record the key vault URI and the secret name from Azure when you create the key vault and secret. When you set the access policy for the secret in the **Access policies** panel:

* Only the **Get** secret permission is required.
* Select the application as the **Principal** for the secret.

Confirm in the Azure or Entra portal that the app has been granted access to the secret that you created for the email provider key.

Important

A key vault secret is created with an expiration date. Be sure to track when a key vault secret is going to expire and create a new secret for the app prior to that date passing.

If Microsoft Identity packages aren't already part of the app's package registrations, add the following packages to the server project for Azure Identity and Azure Key Vault. These packages are transitively provided by Microsoft Identity Web packages, so you only need to add them if the app isn't referencing [`Microsoft.Identity.Web`](https://www.nuget.org/packages/Microsoft.Identity.Web):

* [`Azure.Identity`](https://www.nuget.org/packages/Azure.Identity)
* [`Azure.Security.KeyVault.Secrets`](https://www.nuget.org/packages/Azure.Security.KeyVault.Secrets)

Add the following `AzureHelper` class to the server project. The `GetKeyVaultSecret` method retrieves a secret from a key vault. Adjust the namespace (`BlazorSample.Helpers`) to match your project namespace scheme.

`Helpers/AzureHelper.cs`:

```
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

namespace BlazorSample.Helpers;

public static class AzureHelper
{
    public static string GetKeyVaultSecret(string tenantId, string vaultUri, string secretName)
    {
        DefaultAzureCredentialOptions options = new()
        {
            // Specify the tenant ID to use the dev credentials when running the app locally
            // in Visual Studio.
            VisualStudioTenantId = tenantId,
            SharedTokenCacheTenantId = tenantId
        };

        var client = new SecretClient(new Uri(vaultUri), new DefaultAzureCredential(options));
        var secret = client.GetSecretAsync(secretName).Result;

        return secret.Value.Value;
    }
}
```

Note

The preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential). For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

Where services are registered in the server project's `Program` file, obtain and bind the secret with [Options configuration](../../../../fundamentals/configuration/options?view=aspnetcore-10.0):

```
var tenantId = builder.Configuration.GetValue<string>("AzureAd:TenantId")!;
var vaultUri = builder.Configuration.GetValue<string>("AzureAd:VaultUri")!;

var emailAuthKey = AzureHelper.GetKeyVaultSecret(
    tenantId, vaultUri, "EmailAuthKey");

var authMessageSenderOptions = 
    new AuthMessageSenderOptions() { EmailAuthKey = emailAuthKey };
builder.Configuration.GetSection(authMessageSenderOptions.EmailAuthKey)
    .Bind(authMessageSenderOptions);
```

If you wish to control the environment where the preceding code operates, for example to avoid running the code locally because you've opted to use the [Secret Manager tool](#secret-manager-tool) for local development, you can wrap the preceding code in a conditional statement that checks the environment:

```
if (!context.HostingEnvironment.IsDevelopment())
{
    ...
}
```

In the `AzureAd` section, which you may need to add if it isn't already present, of `appsettings.json` in the server project, add the following `TenantId` and `VaultUri` configuration keys and values, if they aren't already present:

```
"AzureAd": {
  "TenantId": "{TENANT ID}",
  "VaultUri": "{VAULT URI}"
}
```

In the preceding example:

* The `{TENANT ID}` placeholder is the app's tenant ID in Azure.
* The `{VAULT URI}` placeholder is the key vault URI. Include the trailing slash on the URI.

Example:

```
"TenantId": "00001111-aaaa-2222-bbbb-3333cccc4444",
"VaultUri": "https://contoso.vault.azure.net/"
```

Configuration is used to facilitate supplying dedicated key vaults and secret names based on the app's environmental configuration files. For example, you can supply different configuration values for `appsettings.Development.json` in development, `appsettings.Staging.json` when staging, and `appsettings.Production.json` for the production deployment. For more information, see [ASP.NET Core Blazor configuration](../../../fundamentals/configuration?view=aspnetcore-10.0).

## Implement `IEmailSender` in the server project

The following example is based on Mailchimp's Transactional API using [Mandrill.net](https://www.nuget.org/packages/Mandrill.net). For a different provider, refer to their documentation on how to implement sending an email message.

Add the [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) NuGet package to the server project.

Add the following `EmailSender` class to implement [IEmailSender\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.iemailsender-1). In the following example, `AppUser` is an [IdentityUser](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser). The message HTML markup can be further customized. As long as the `message` passed to `MandrillMessage` starts with the `<` character, the Mandrill.net API assumes that the message body is composed in HTML.

`EmailSender.cs`:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Mandrill;
using Mandrill.Model;

namespace Backend;

public class EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
    ILogger<EmailSender> logger) : IEmailSender<AppUser>
{
    private readonly ILogger logger = logger;

    public AuthMessageSenderOptions Options { get; } = optionsAccessor.Value;

    public Task SendConfirmationLinkAsync(AppUser user, string email,
        string confirmationLink) => SendEmailAsync(email, "Confirm your email",
        "<html lang=\"en\"><head></head><body>Please confirm your account by " +
        $"<a href='{confirmationLink}'>clicking here</a>.</body></html>");

    public Task SendPasswordResetLinkAsync(AppUser user, string email,
        string resetLink) => SendEmailAsync(email, "Reset your password",
        "<html lang=\"en\"><head></head><body>Please reset your password by " +
        $"<a href='{resetLink}'>clicking here</a>.</body></html>");

    public Task SendPasswordResetCodeAsync(AppUser user, string email,
        string resetCode) => SendEmailAsync(email, "Reset your password",
        "<html lang=\"en\"><head></head><body>Please reset your password " +
        $"using the following code:<br>{resetCode}</body></html>");

    public async Task SendEmailAsync(string toEmail, string subject, string message)
    {
        if (string.IsNullOrEmpty(Options.EmailAuthKey))
        {
            throw new Exception("Null EmailAuthKey");
        }

        await Execute(Options.EmailAuthKey, subject, message, toEmail);
    }

    public async Task Execute(string apiKey, string subject, string message, 
        string toEmail)
    {
        var api = new MandrillApi(apiKey);
        var mandrillMessage = new MandrillMessage("sarah@contoso.com", toEmail, 
            subject, message);
        await api.Messages.SendAsync(mandrillMessage);

        logger.LogInformation("Email to {EmailAddress} sent!", toEmail);
    }
}
```

Note

Body content for messages might require special encoding for the email service provider. If links in the message body can't be followed in the email message, consult the service provider's documentation to troubleshoot the problem.

Add the following [IEmailSender\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.iemailsender-1) service registration to the server project's `Program` file:

```
builder.Services.AddTransient<IEmailSender<AppUser>, EmailSender>();
```

## Configure the server project to require email confirmation

In the server project's `Program` file, require a confirmed email address to sign in to the app.

Locate the line that calls [AddIdentityCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentitycore) and set the [RequireConfirmedEmail](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinoptions.requireconfirmedemail#microsoft-aspnetcore-identity-signinoptions-requireconfirmedemail) property to `true`:

```
- builder.Services.AddIdentityCore<AppUser>()
+ builder.Services.AddIdentityCore<AppUser>(o => o.SignIn.RequireConfirmedEmail = true)
```

## Update the client project's account registration response

In the client project's `Register` component (`Components/Identity/Register.razor`), change the message to users on a successful account registration to instruct them to confirm their account. The following example includes a link to trigger Identity on the server to resend the confirmation email.

```
- You successfully registered and can <a href="login">login</a> to the app.
+ You successfully registered. You must now confirm your account by clicking 
+ the link in the email that was sent to you. After confirming your account, 
+ you can <a href="login">login</a> to the app. 
+ <a href="resendConfirmationEmail">Resend confirmation email</a>
```

## Update seed data code to confirm seeded accounts

In the server project's seed data class (`SeedData.cs`), change the code in the `InitializeAsync` method to confirm the seeded accounts, which avoids requiring email address confirmation for each test run of the solution with one of the accounts:

```
- if (appUser is not null && user.RoleList is not null)
- {
-     await userManager.AddToRolesAsync(appUser, user.RoleList);
- }
+ if (appUser is not null)
+ {
+     if (user.RoleList is not null)
+     {
+         await userManager.AddToRolesAsync(appUser, user.RoleList);
+     }
+ 
+     var token = await userManager.GenerateEmailConfirmationTokenAsync(appUser);
+     await userManager.ConfirmEmailAsync(appUser, token);
+ }
```

## Enable account confirmation after a site has users

Enabling account confirmation on a site with users locks out all the existing users. Existing users are locked out because their accounts aren't confirmed. Use one of the following approaches, which are beyond the scope of this article:

* Update the database to mark all existing users as confirmed.
* Batch-send emails with confirmation links to all existing users, which requires each user to confirm their own account.

## Password recovery

Password recovery requires the server app to adopt an email provider in order to send password reset codes to users. Therefore, follow the guidance earlier in this article to adopt an email provider:

* [Select and configure an email provider for the server project](#select-and-configure-an-email-provider-for-the-server-project)
* [Configure a secret for the email provider's security key](#configure-a-secret-for-the-email-providers-security-key)
* [Implement `IEmailSender` in the server project](#implement-iemailsender-in-the-server-project)

Password recovery is a two-step process:

1. A POST request is made to the `/forgotPassword` endpoint provided by [MapIdentityApi](/en-us/dotnet/api/microsoft.aspnetcore.routing.identityapiendpointroutebuilderextensions.mapidentityapi) in the server project. A message in the UI instructs the user to check their email for a reset code.
2. A POST request is made to the `/resetPassword` endpoint of the server project with the user's email address, password reset code, and new password.

The preceding steps are demonstrated by the following implementation guidance for the [sample solution](./?view=aspnetcore-10.0#sample-apps).

In the client project, add the following method signatures to the `IAccountManagement` class (`Identity/IAccountManagement.cs`):

```
public Task<bool> ForgotPasswordAsync(string email);

public Task<FormResult> ResetPasswordAsync(string email, string resetCode, 
    string newPassword);
```

In the client project, add implementations for the preceding methods in the `CookieAuthenticationStateProvider` class (`Identity/CookieAuthenticationStateProvider.cs`):

```
public async Task<bool> ForgotPasswordAsync(string email)
{
    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "forgotPassword", new
            {
                email
            });

        if (result.IsSuccessStatusCode)
        {
            return true;
        }
    }
    catch { }

    return false;
}

public async Task<FormResult> ResetPasswordAsync(string email, string resetCode, 
    string newPassword)
{
    string[] defaultDetail = [ "An unknown error prevented password reset." ];

    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "resetPassword", new
            {
                email,
                resetCode,
                newPassword
            });

        if (result.IsSuccessStatusCode)
        {
            return new FormResult { Succeeded = true };
        }

        var details = await result.Content.ReadAsStringAsync();
        var problemDetails = JsonDocument.Parse(details);
        var errors = new List<string>();
        var errorList = problemDetails.RootElement.GetProperty("errors");

        foreach (var errorEntry in errorList.EnumerateObject())
        {
            if (errorEntry.Value.ValueKind == JsonValueKind.String)
            {
                errors.Add(errorEntry.Value.GetString()!);
            }
            else if (errorEntry.Value.ValueKind == JsonValueKind.Array)
            {
                errors.AddRange(
                    errorEntry.Value.EnumerateArray().Select(
                        e => e.GetString() ?? string.Empty)
                    .Where(e => !string.IsNullOrEmpty(e)));
            }
        }

        return new FormResult
        {
            Succeeded = false,
            ErrorList = problemDetails == null ? defaultDetail : [.. errors]
        };
    }
    catch { }

    return new FormResult
    {
        Succeeded = false,
        ErrorList = defaultDetail
    };
}
```

In the client project, add the following `ForgotPassword` component.

`Components/Identity/ForgotPassword.razor`:

```
@page "/forgot-password"
@using System.ComponentModel.DataAnnotations
@using BlazorWasmAuth.Identity
@inject IAccountManagement Acct

<PageTitle>Forgot your password?</PageTitle>

<h1>Forgot your password?</h1>
<p>Provide your email address and select the <b>Reset password</b> button.</p>
<hr />
<div class="row">
    <div class="col-md-4">
        @if (!passwordResetCodeSent)
        {
            <EditForm Model="Input" FormName="forgot-password" 
                      OnValidSubmit="OnValidSubmitStep1Async" method="post">
                <DataAnnotationsValidator />
                <ValidationSummary class="text-danger" role="alert" />

                <div class="form-floating mb-3">
                    <InputText @bind-Value="Input.Email" 
                        id="Input.Email" class="form-control" 
                        autocomplete="username" aria-required="true" 
                        placeholder="name@example.com" />
                    <label for="Input.Email" class="form-label">
                        Email
                    </label>
                    <ValidationMessage For="() => Input.Email" 
                        class="text-danger" />
                </div>
                <button type="submit" class="w-100 btn btn-lg btn-primary">
                    Request reset code
                </button>
            </EditForm>
        }
        else
        {
            if (passwordResetSuccess)
            {
                if (errors)
                {
                    foreach (var error in errorList)
                    {
                        <div class="alert alert-danger">@error</div>
                    }
                }
                else
                {
                    <div>
                        Your password was reset. You may <a href="login">login</a> 
                        to the app with your new password.
                    </div>
                }
            }
            else
            {
                <div>
                    A password reset code has been sent to your email address. 
                    Obtain the code from the email for this form.
                </div>
                <EditForm Model="Reset" FormName="reset-password" 
                    OnValidSubmit="OnValidSubmitStep2Async" method="post">
                    <DataAnnotationsValidator />
                    <ValidationSummary class="text-danger" role="alert" />

                    <div class="form-floating mb-3">
                        <InputText @bind-Value="Reset.ResetCode" 
                            id="Reset.ResetCode" class="form-control" 
                            autocomplete="username" aria-required="true" />
                        <label for="Reset.ResetCode" class="form-label">
                            Reset code
                        </label>
                        <ValidationMessage For="() => Reset.ResetCode" 
                            class="text-danger" />
                    </div>
                    <div class="form-floating mb-3">
                        <InputText type="password" @bind-Value="Reset.NewPassword" 
                            id="Reset.NewPassword" class="form-control" 
                            autocomplete="new-password" aria-required="true" 
                            placeholder="password" />
                        <label for="Reset.NewPassword" class="form-label">
                            New Password
                        </label>
                        <ValidationMessage For="() => Reset.NewPassword" 
                            class="text-danger" />
                    </div>
                    <div class="form-floating mb-3">
                        <InputText type="password" 
                            @bind-Value="Reset.ConfirmPassword" 
                            id="Reset.ConfirmPassword" class="form-control" 
                            autocomplete="new-password" aria-required="true" 
                            placeholder="password" />
                        <label for="Reset.ConfirmPassword" class="form-label">
                            Confirm Password
                        </label>
                        <ValidationMessage For="() => Reset.ConfirmPassword" 
                            class="text-danger" />
                    </div>
                    <button type="submit" class="w-100 btn btn-lg btn-primary">
                        Reset password
                    </button>
                </EditForm>
            }
        }
    </div>
</div>

@code {
    private bool passwordResetCodeSent, passwordResetSuccess, errors;
    private string[] errorList = [];

    [SupplyParameterFromForm(FormName = "forgot-password")]
    private InputModel Input { get; set; } = new();

    [SupplyParameterFromForm(FormName = "reset-password")]
    private ResetModel Reset { get; set; } = new();

    private async Task OnValidSubmitStep1Async()
    {
        passwordResetCodeSent = await Acct.ForgotPasswordAsync(Input.Email);
    }

    private async Task OnValidSubmitStep2Async()
    {
        var result = await Acct.ResetPasswordAsync(Input.Email, Reset.ResetCode, 
            Reset.NewPassword);

        if (result.Succeeded)
        {
            passwordResetSuccess = true;

        }
        else
        {
            errors = true;
            errorList = result.ErrorList;
        }
    }

    private sealed class InputModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; } = string.Empty;
    }

    private sealed class ResetModel
    {
        [Required]
        [Base64String]
        public string ResetCode { get; set; } = string.Empty;

        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at " +
            "max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string NewPassword { get; set; } = string.Empty;

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("NewPassword", ErrorMessage = "The new password and confirmation " +
            "password don't match.")]
        public string ConfirmPassword { get; set; } = string.Empty;
    }
}
```

In the `Login` component (`Components/Identity/Login.razor`) of the client project immediately before the closing `</NotAuthorized>` tag, add a forgot password link to reach the `ForgotPassword` component:

```
<div>
    <a href="forgot-password">Forgot password</a>
</div>
```

## Email and activity timeout

The default inactivity timeout is 14 days. In the server project, the following code sets the inactivity timeout to five days with sliding expiration:

```
builder.Services.ConfigureApplicationCookie(options => {
    options.ExpireTimeSpan = TimeSpan.FromDays(5);
    options.SlidingExpiration = true;
});
```

## Change all ASP.NET Core Data Protection token lifespans

In the server project, the following code changes Data Protection tokens' timeout period to three hours:

```
builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
    options.TokenLifespan = TimeSpan.FromHours(3));
```

To change the email token lifespan, add a custom [DataProtectorTokenProvider\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.dataprotectortokenprovider-1) and [DataProtectionTokenProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.dataprotectiontokenprovideroptions) to the server project.

`CustomTokenProvider.cs`:

```
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

namespace BlazorSample;

public class CustomEmailConfirmationTokenProvider<TUser>
    : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomEmailConfirmationTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<EmailConfirmationTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
        : base(dataProtectionProvider, options, logger)
    {
    }
}

public class EmailConfirmationTokenProviderOptions 
    : DataProtectionTokenProviderOptions
{
    public EmailConfirmationTokenProviderOptions()
    {
        Name = "EmailDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(4);
    }
}

public class CustomPasswordResetTokenProvider<TUser> 
    : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomPasswordResetTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<PasswordResetTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
        : base(dataProtectionProvider, options, logger)
    {
    }
}

public class PasswordResetTokenProviderOptions : 
    DataProtectionTokenProviderOptions
{
    public PasswordResetTokenProviderOptions()
    {
        Name = "PasswordResetDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(3);
    }
}
```

Configure the services to use the custom token provider in the server project's `Program` file:

```
builder.Services.AddIdentityCore<AppUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<AppUser>)));
        options.Tokens.EmailConfirmationTokenProvider = 
            "CustomEmailConfirmation";
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

builder.Services
    .AddTransient<CustomEmailConfirmationTokenProvider<AppUser>>();
```

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-09

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-with-dotnet-on-web-workers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/blazor-with-dotnet-on-web-workers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor with .NET on Web Workers

Modern Blazor WebAssembly apps often handle CPU-intensive work alongside rich UI updates. Tasks such as image processing, document parsing, or data crunching can easily freeze the browser's main thread. Web Workers let you push that work to a background thread. Combined with the .NET WebAssembly runtime, you can execute C# code in the background while the UI stays responsive.

The Blazor Web Worker project template (`blazorwebworker`) contains a Web Worker client for Blazor WebAssembly apps. The template generates the required JavaScript worker scripts, a C# `WebWorkerClient` class, and a starter `WorkerMethods.cs` file, which removes the need to write the interop layer manually. To learn about Web Workers with React or other custom JavaScript frontends, see [.NET on Web Workers](../client-side/dotnet-on-webworkers?view=aspnetcore-10.0).

Note

Use of a Web Worker is primarily useful with Blazor WebAssembly apps to improve performance but can be implemented for the client-side project (`.Client`) of a Blazor Web App. Typically, a Blazor Web App performs demanding work server-side, where the server can handle the work faster than the client and the app avoids the small delay setting up a Web Worker. However, use of a Web Worker via the Blazor Web Worker template client-side in a Blazor Web App is supported.

## Create the projects

Create a Blazor WebAssembly app and a .NET Web Worker class library:

```
dotnet new blazorwasm -n SampleApp
dotnet new blazorwebworker -n WebWorker
```

Add a project reference from the app to the worker library:

```
cd SampleApp
dotnet add reference ../WebWorker/WebWorker.csproj
```

## Customize the generated worker methods

The template already enables the [AllowUnsafeBlocks](/en-us/dotnet/api/microsoft.build.tasks.csc.allowunsafeblocks#microsoft-build-tasks-csc-allowunsafeblocks) property in the worker project and includes a starter `WorkerMethods.cs` file with a [`[JSExport]` attribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsexportattribute) example. Add or update methods in the worker project as needed.

Worker methods are `static` methods marked with [`[JSExport]`](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsexportattribute) in a `static partial class`.

Due to `[JSExport]` limitations, worker methods should use JS interop-friendly types such as primitives and strings. For complex data, serialize to JSON in the worker and deserialize it in the Blazor app.

`WebWorker\WorkerMethods.cs`:

```
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using System.Text.Json;

namespace WebWorker;

[SupportedOSPlatform("browser")]
public static partial class WorkerMethods
{
    [JSExport]
    public static string Greet(string name) => $"Hello, {name}!";

    [JSExport]
    public static string GetUsers()
    {
        var users = new List<User> { new("Alice", 30), new("Bob", 25) };
        return JsonSerializer.Serialize(users);
    }
}

public record User(string Name, int Age);
```

## Use the worker from a component

Inject `IJSRuntime` and use `WebWorkerClient.CreateAsync` to create a worker instance. The client manages the JavaScript messaging layer on your behalf.

`Pages/Home.razor`:

```
@page "/"
@using WebWorker
@implements IAsyncDisposable
@inject IJSRuntime JSRuntime

<PageTitle>Home</PageTitle>

<h1>Web Worker demo</h1>

<button class="btn btn-primary" @onclick="CallWorker" disabled="@(worker is null)">
    Call Worker
</button>

@if (!string.IsNullOrEmpty(greeting))
{
    <p>@greeting</p>
}

@if (users is not null)
{
    <ul>
        @foreach (var user in users)
        {
            <li>@user.Name (age @user.Age)</li>
        }
    </ul>
}
```

`Pages/Home.razor.cs`:

```
using System.Text.Json;
using System.Runtime.Versioning;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using WebWorker;

namespace SampleApp.Pages;

[SupportedOSPlatform("browser")]
public partial class Home : ComponentBase, IAsyncDisposable
{
    private WebWorkerClient? worker;
    private string greeting = string.Empty;
    private List<User>? users;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            worker = await WebWorkerClient.CreateAsync(JSRuntime);
            StateHasChanged();
        }
    }

    private async Task CallWorker()
    {
        if (worker is null)
        {
            return;
        }

        greeting = await worker.InvokeAsync<string>(
            "WebWorker.WorkerMethods.Greet", ["World"]);

        var usersJson = await worker.InvokeAsync<string>(
            "WebWorker.WorkerMethods.GetUsers", []);

        users = JsonSerializer.Deserialize<List<User>>(usersJson) ?? [];
    }

    public async ValueTask DisposeAsync()
    {
        if (worker is not null)
        {
            await worker.DisposeAsync();
        }
    }
}
```

## Template output

The `dotnet new blazorwebworker` template generates a class library with the following structure:

```
WebWorker/
├── WebWorker.csproj
├── WebWorkerClient.cs
├── WorkerMethods.cs
└── wwwroot/
    ├── dotnet-web-worker-client.js
    └── dotnet-web-worker.js
```

* `WebWorkerClient.cs`: C# client that manages worker lifecycle and communication.
* `WorkerMethods.cs`: Starter file for adding `[JSExport]` methods that run inside the worker.
* `dotnet-web-worker-client.js`: JavaScript class that creates the worker, dispatches messages, and resolves pending requests.
* `dotnet-web-worker.js`: Worker entry point that boots the .NET WebAssembly runtime and dynamically resolves `[JSExport]` methods by name.

## `WebWorkerClient` API

The `WebWorkerClient` class exposes an async API for communicating with a Web Worker:

```
public sealed class WebWorkerClient : IAsyncDisposable
{
    public static async Task<WebWorkerClient> CreateAsync(
        IJSRuntime jsRuntime,
        int timeoutMs = 60000,
        string? assemblyName = null,
        CancellationToken cancellationToken = default);

    public async Task<TResult> InvokeAsync<TResult>(
        string method,
        object[] args,
        int timeoutMs = 60000,
        CancellationToken cancellationToken = default);

    public async Task InvokeVoidAsync(
        string method,
        object[] args,
        int timeoutMs = 60000,
        CancellationToken cancellationToken = default);

    public async ValueTask DisposeAsync();
}
```

* `CreateAsync`: Initializes the worker and waits for the .NET runtime to be ready inside the worker thread. The `assemblyName` parameter defaults to the worker project's assembly.
* `InvokeAsync<TResult>`: Calls a `[JSExport]` method on the worker by its full name (`AssemblyName.ClassName.MethodName`) and returns the result.
* `InvokeVoidAsync`: Calls a `[JSExport]` method that doesn't return a value.
* `DisposeAsync`: Terminates the worker and releases resources. Use `await using` or call explicitly.

The guidance in this article mirrors the concepts from the React-focused *.NET on Web Workers* walkthrough, but adapts every step to a Blazor frontend. It highlights the same QR-code generation scenario implemented in this repository. To learn about Web Workers with React, see [.NET on Web Workers](../client-side/dotnet-on-webworkers?view=aspnetcore-10.0).

Note

Use of a Web Worker is primarily useful with Blazor WebAssembly apps to improve performance but can be implemented for the client-side project (`.Client`) of a Blazor Web App. Typically, a Blazor Web App performs demanding work server-side, where the server can handle the work faster than the client and the app avoids the small delay setting up a Web Worker. However, use of a Web Worker client-side in a Blazor Web App is supported.

## Sample app

Explore a complete working implementation in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples). The sample is available for .NET 10 or later and named `DotNetOnWebWorkersBlazorWebAssembly`.

## Prerequisites

Before diving into the implementation, ensure the necessary tools are installed. The [.NET SDK 8.0 or later](https://dotnet.microsoft.com/download) is required.

## Create the Blazor WebAssembly project

Create a Blazor WebAssembly app:

```
dotnet new blazorwasm -o WebWorkersOnBlazor
cd WebWorkersOnBlazor
```

Add a package reference for [`QRCoder`](https://www.nuget.org/packages/QRCoder) to simulate heavy computations.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Warning

[`Shane32/QRCoder`](https://github.com/Shane32/QRCoder)/[`QRCoder` NuGet package](https://www.nuget.org/packages/QRCoder) isn't owned or maintained by Microsoft and isn't covered by any Microsoft Support Agreement or license. Use caution when adopting a third-party library, especially for security features. Confirm that the library follows official specifications and adopts security best practices. Keep the library's version current to obtain the latest bug fixes.

Enable the [AllowUnsafeBlocks](/en-us/dotnet/api/microsoft.build.tasks.csc.allowunsafeblocks#microsoft-build-tasks-csc-allowunsafeblocks) property in app's project file, which is required whenever you use [`[JSImport]` attribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsimportattribute) or [`[JSExport]` attribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsexportattribute) in WebAssembly projects:

```
<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
```

Warning

The JS interop API requires enabling [AllowUnsafeBlocks](/en-us/dotnet/api/microsoft.build.tasks.csc.allowunsafeblocks#microsoft-build-tasks-csc-allowunsafeblocks). Be careful when implementing your own unsafe code in .NET apps, which can introduce security and stability risks. For more information, see [Unsafe code, pointer types, and function pointers](/en-us/dotnet/csharp/language-reference/unsafe-code).

## Add the Web Worker code

Create the following file to expose .NET code to JavaScript using the [`[JSExport]` attribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsexportattribute):

`Workers/QRGenerator.razor.cs`:

```
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using QRCoder;

[SupportedOSPlatform("browser")]
public partial class QRGenerator
{
    private static readonly int MaxQrSize = 20;

    [JSExport]
    internal static byte[] Generate(string text, int qrSize)
    {
        if (qrSize >= MaxQrSize)
        {
            throw new Exception($"QR code size must be less than {MaxQrSize}.");
        }

        var generator = new QRCodeGenerator();
        QRCodeData data = generator.CreateQrCode(text, QRCodeGenerator.ECCLevel.Q);
        var qrCode = new BitmapByteQRCode(data);

        return qrCode.GetGraphic(qrSize);
    }
}
```

Create a matching Razor component file (`.razor`) to act as an empty stub so that the build packs the worker script alongside the component assets:

`Workers/QRGenerator.razor`:

```
// dummy file to let blazor handle Worker.razor.js file loading
```

Add the following JavaScript file. The script boots the .NET runtime in the worker, then listens for messages from the main thread. `postMessage` is used to send either a `result` or an `error` payload.

`Workers/QRGenerator.razor.js`:

```
import { dotnet } from '../_framework/dotnet.js';

let assemblyExports;
let startupError;

try {
  const { getAssemblyExports, getConfig } = await dotnet.create();
  const config = getConfig();
  assemblyExports = await getAssemblyExports(config.mainAssemblyName);
} catch (err) {
  startupError = err.message;
}

self.addEventListener('message', async e => {
  try {
    if (!assemblyExports) {
      throw new Error(startupError || 'worker exports not loaded');
    }

    let result;
    switch (e.data.command) {
      case 'generateQR':
        result = assemblyExports.QRGenerator.Generate(e.data.text, e.data.size);
        break;
      default:
        throw new Error(`Unknown command: ${e.data.command}`);
    }

    self.postMessage({ command: 'response', 
      requestId: e.data.requestId, result });
  } catch (err) {
    self.postMessage({ command: 'response', 
      requestId: e.data.requestId, error: err.message });
  }
});
```

## Bridge the worker to the Blazor UI

Create the following JavaScript file that manages the worker instance and exposes helper functions to Blazor.

`Clients/Client.razor.js`:

```
const pendingRequests = {};
let pendingRequestId = 0;

const dotnetWorker = 
  new Worker('./Workers/QRGenerator.razor.js', { type: 'module' });

dotnetWorker.addEventListener('message', e => {
  switch (e.data.command) {
    case 'response':
      const request = pendingRequests[e.data.requestId];
      delete pendingRequests[e.data.requestId];
      if (e.data.error) {
        request.reject(new Error(e.data.error));
      }
      request.resolve(e.data.result);
      break;
    default:
      console.log('Worker said:', e.data);
  }
});

function sendRequestToWorker(request) {
  pendingRequestId++;
  const promise = new Promise((resolve, reject) => {
    pendingRequests[pendingRequestId] = { resolve, reject };
  });

  dotnetWorker.postMessage({ ...request, requestId: pendingRequestId });
  return promise;
}

export async function generateQR(text, size) {
  const response = await sendRequestToWorker({ command: 'generateQR', text, size });
  const blob = new Blob([response], { type: 'image/png' });
  return URL.createObjectURL(blob);
}
```

Similarly as the worker, the `Client` script requires a matching `.razor` file with an empty stub to assure that the JS file is considered a part of the component.

`Clients/Client.razor`:

```
// dummy file to let blazor handle Client.razor.js file loading
```

Add the following `Client`, which exposes the JavaScript module to Blazor components using the [`[JSImport]` attribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsimportattribute). `InitClient` ensures the worker JS module is only loaded once per browser session.

`Clients/Client.razor.cs`:

```
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;

[SupportedOSPlatform("browser")]
public partial class Client
{
    private static bool workerStarted;

    public static async Task InitClient()
    {
        if (workerStarted)
        {
            return;
        }

        workerStarted = true;

        await JSHost.ImportAsync(
            moduleName: nameof(Client), 
            moduleUrl: "../Clients/Client.razor.js");
    }

    [JSImport("generateQR", nameof(Client))]
    public static partial Task<string> GenerateQR(string text, int size);
}
```

## Demonstrate the flow

You can use the app's Home page to demonstrate the flow.

`Pages/Home.razor`:

```
@page "/"
@using Components
@namespace Pages

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

<Popup @ref="popup" />

<div class="input-container">
    <div class="form-group">
        <label for="textInput">Generate a QR from text:</label>
        <input type="text" class="form-control" id="textInput" @bind="text" placeholder="Text" />
    </div>

    <div class="form-group">
        <label for="numberInput">Set size of QR (in pixels):</label>
        <input type="number" class="form-control" id="numberInput" @bind="size" />
    </div>

    <div class="form-group">
        <button class="btn btn-primary" @onclick="GenerateQR">Generate QR</button>
    </div>

    @if (!string.IsNullOrWhiteSpace(imageUrl))
    {
        <div class="form-group">
            <img class="image" src="@imageUrl" id="qrImage" alt="Image" />
        </div>
    }
</div>
```

The following code-behind file initializes the client and generates the QR code. The [`OnAfterRenderAsync` lifecycle method](components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync) code guarantees that the JavaScript module is loaded before the user clicks the button, while the `GenerateQR` handler makes a single asynchronous worker request.

`Home.razor.cs`:

```
using Microsoft.AspNetCore.Components;
using System.Runtime.Versioning;
using Components;

namespace Pages;

[SupportedOSPlatform("browser")]
public partial class Home : ComponentBase
{
    private string imageUrl = string.Empty;
    private string? text;
    private int size = 5;
    private Popup popup = new();

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Client.InitClient();
    }

    private async Task GenerateQR()
    {
        try
        {
            if (text is not null)
            {
                imageUrl = await Client.GenerateQR(text, size);
            }
        }
        catch(Exception ex)
        {
            imageUrl = string.Empty;
            popup.Show(title: "Error", message: ex.Message);
        }

        await InvokeAsync(StateHasChanged);
    }
}
```

## Next steps

* Swap the QR code sample for your own CPU-intensive domain logic.
* Move long-running workflows into dedicated worker instances per feature area.
* Explore shared array buffers or Atomics when you need higher-throughput synchronization between Blazor and workers.

## Related content

[.NET on Web Workers](../client-side/dotnet-on-webworkers?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/cors
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/cors.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP Cross-Origin Requests (CORS)

## Introduction

The reverse proxy can handle cross-origin requests before they are proxied to the destination servers. This can reduce load on the destination servers and ensure consistent policies are implemented across your applications.

## Defaults

The requests won't be automatically matched for cors preflight requests unless enabled in the route or application configuration.

## Configuration

CORS policies can be specified per route via [RouteConfig.CorsPolicy](/en-us/dotnet/api/yarp.reverseproxy.configuration.routeconfig) and can be bound from the `Routes` sections of the config file. As with other route properties, this can be modified and reloaded without restarting the proxy. Policy names are case insensitive.

Example:

```
{
  "ReverseProxy": {
    "Routes": {
      "route1" : {
        "ClusterId": "cluster1",
        "CorsPolicy": "customPolicy",
        "Match": {
          "Hosts": [ "localhost" ]
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "cluster1/destination1": {
            "Address": "https://localhost:10001/"
          }
        }
      }
    }
  }
}
```

[CORS policies](/en-us/aspnet/core/security/cors#cors-with-named-policy-and-middleware) are an ASP.NET Core concept that the proxy utilizes. The proxy provides the above configuration to specify a policy per route and the rest is handled by existing ASP.NET Core CORS middleware.

CORS policies can be configured in the application as follows:

```
services.AddCors(options =>
{
    options.AddPolicy("customPolicy", builder =>
    {
        builder.AllowAnyOrigin();
    });
});
```

Then add the CORS middleware.

```
app.UseCors();

app.MapReverseProxy();
```

### DefaultPolicy

Specifying the value `default` in a route's `CorsPolicy` parameter means that route will use the policy defined in [CorsOptions.DefaultPolicy](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corsoptions.defaultpolicyname).

### Disable CORS

Specifying the value `disable` in a route's `CorsPolicy` parameter means the CORS middleware will refuse the CORS requests.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/security/maui-blazor-web-identity
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/security/maui-blazor-web-identity.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# .NET MAUI Blazor Hybrid and Web App with ASP.NET Core Identity

This sample app demonstrates how to build a .NET MAUI Blazor Hybrid app with a Blazor Web App that uses ASP.NET Core Identity. It shows how to share common UI and authenticate with ASP.NET Core Identity local accounts. Although ASP.NET Core Identity is used, you can use this pattern for any authentication provider from a MAUI Blazor Hybrid client.

The sample:

* Sets up the UI to show or hide pages based on user authentication.
* Sets up ASP.NET Core Identity endpoints for remote clients.
* Logs users in, logs users out, and refreshes tokens from the MAUI client.
* Saves and retrieves tokens in secure device storage.
* Calls a secure endpoint (`/api/weather`) from the client for weather data.

## Prerequisites and preliminary steps

For prerequisites and preliminary steps, see the [Build your first app](/en-us/dotnet/maui/get-started/first-app) tutorial. We recommend using the MAUI Blazor Hybrid tutorial to set up your local system for MAUI development before using the guidance in this article and the sample app.

## Sample app

[Obtain the sample app](../../fundamentals/?view=aspnetcore-10.0#sample-apps) in the `MauiBlazorWebIdentity` folder of the [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) (.NET 9 or later).

The sample app is a starter solution that contains a native, cross-platform MAUI Blazor Hybrid app, a Blazor Web App, and a Razor class library (RCL) that contains the shared UI (Razor components) used by the native and web apps.

1. Clone this repository or download a ZIP archive of the repository. For more information, see [How to download a sample](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).
2. Make sure you have [the latest .NET installed with the MAUI workload](/en-us/dotnet/maui/get-started/installation).
3. Open the solution in Visual Studio (2022 or later) or VS Code with the .NET MAUI extension installed.
4. Set the `MauiBlazorWeb` MAUI project as the startup project. In Visual Studio, right-click the project and select **Set as Startup Project**.
5. Start the `MauiBlazorWeb.Web` project without debugging. In Visual Studio, right-click on the project and select **Debug** > **Start without Debugging**.
6. Inspect the Identity endpoints via [OpenAPI documentation](../../../fundamentals/openapi/overview?view=aspnetcore-10.0). You can add a third-party OpenAPI-compliant visual UI/endpoint tester.
7. Navigate to `https://localhost:7157/account/register` to register a user in the Blazor Web App. Immediately after the user is registered, use the **Click here to confirm your account** link in the UI to confirm the user's email address because a real email sender isn't registered for account confirmation.
8. Start (`F5`) the `MauiBlazorWeb` MAUI project. You can set the debug target to either **Windows** or an Android emulator.
9. Notice you can only see the `Home` and `Login` pages.
10. Log in with the user that you registered.
11. Notice you can now see the shared `Counter` and `Weather` pages.
12. Log out and notice you can only see the `Home` and `Login` pages again.
13. Navigate to `https://localhost:7157/` in a browser and the web app behaves the same.

## Shared UI

The shared UI is in the `MauiBlazorWeb.Shared` project. This project contains the Razor components that are shared between the MAUI and Blazor Web App projects (Home, Counter, and Weather pages). The `Counter` and `Weather` components are protected by [`[Authorize]` attributes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute), so users can't navigate to them unless they're logged into the app.

In the [Razor directives](../../components/?view=aspnetcore-10.0#razor-syntax) at the tops of the `Counter` component (`MauiBlazorWeb.Shared/Pages/Counter.razor`) and `Weather` component (`MauiBlazorWeb.Shared/Pages/Weather.razor`) files:

```
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
```

## MAUI app and Blazor Web App routing

The `Routes` component uses an [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) to route users based on their authentication status. If a user isn't authenticated, they're redirected to the `Login` page. For more information, see [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0#customize-unauthorized-content-with-the-router-component).

In `MauiBlazorWeb.Web/Components/Routes.razor`:

```
<AuthorizeRouteView ...>
    <Authorizing>
        Authorizing...
    </Authorizing>
    <NotAuthorized>
        <Login />
    </NotAuthorized>
</AuthorizeRouteView>   
```

The [`NavMenu` component](../../project-structure?view=aspnetcore-10.0#blazor-web-app) contains the navigation menu that uses an [`AuthorizeView` component](../../security/?view=aspnetcore-10.0#authorizeview-component) to show or hide links based on the user's authentication status.

In `MauiBlazorWeb.Web/Components/Layout/NavMenu.razor`:

```
<AuthorizeView>
    <NotAuthorized>
       ...
    </NotAuthorized>
    <Authorized>
        ...
    </Authorized>
</AuthorizeView>
```

### Server project

The Blazor Web App project (`MauiBlazorWeb.Web`) of the sample app contains the ASP.NET Core Identity pages and uses the [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) framework class to manage logins and users on the server. In order for the MAUI client (or any external client) to authenticate, the Identity endpoints must be registered and exposed. In the `Program` file, Identity endpoints are set up with the calls to:

* [AddIdentityApiEndpoints](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentityapiendpoints)
* [MapIdentityApi](/en-us/dotnet/api/microsoft.aspnetcore.routing.identityapiendpointroutebuilderextensions.mapidentityapi)
* `MapAdditionalIdentityEndpoints` (`MauiBlazorWeb.Web/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs`).

In `MauiBlazorWeb.Web/Program.cs`:

```
builder.Services.AddIdentityApiEndpoints<ApplicationUser>(...)
    .AddEntityFrameworkStores<ApplicationDbContext>();

...

app.MapGroup("/identity").MapIdentityApi<ApplicationUser>();

...

app.MapAdditionalIdentityEndpoints();
```

Important

ASP.NET Core Identity pages and the implementation of the [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) framework class to manage logins and users is generated automatically when you create a project from the Blazor Web App project template with **Individual Accounts**.

This article focuses on using the provided sample app; but when creating a new project from the Blazor Web App template, you must remove the generated call to [AddIdentityCookies](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitycookieauthenticationbuilderextensions.addidentitycookies) on [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication). The call isn't necessary when implementing API such as `MapAdditionalIdentityEndpoints` in the sample app and results in an error if left in the app.

### Log in from the MAUI client

The `Login` component (`/identity/login` endpoint) is where the user logs in. The component injects the `MauiAuthenticationStateProvider` (`MauiBlazorWeb/Services/MauiAuthenticationStateProvider.cs`) and uses the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) to authenticate the user and redirect them to the homepage if successful. When the state changes, the [`AuthorizeView` component](../../security/?view=aspnetcore-10.0#authorizeview-component) shows the appropriate links based on the user's authentication status.

In `MauiBlazorWeb/Components/Pages/Login.razor`:

```
private async Task LoginUser()
{
    await AuthStateProvider.LogInAsync(LoginModel);

    if (AuthStateProvider.LoginStatus != LoginStatus.Success)
    {
        loginFailureHidden = false;
        return;
    }

    Navigation.NavigateTo("");
}
```

Note

This sample only implements Login and Logout pages on the MAUI client, but you can build Register and other management pages against the exposed Identity endpoints for more functionality. For more information on Identity endpoints, see [Use Identity to secure a Web API backend for SPAs](../../../security/authentication/identity-api-authorization?view=aspnetcore-10.0).

### MAUI Authentication State Provider (`MauiAuthenticationStateProvider`)

The `MauiAuthenticationStateProvider` class (`MauiBlazorWeb/Services/MauiAuthenticationStateProvider.cs`) is responsible for managing the user's authentication state and providing the [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) to the app. The `MauiAuthenticationStateProvider` class uses an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to make requests to the server to authenticate the user. For more information, see [ASP.NET Core Blazor Hybrid authentication and authorization](./?view=aspnetcore-10.0\&pivots=maui).

The `MauiAuthenticationStateProvider` class uses the `HttpClientHelper` (`MauiBlazorWeb/Services/HttpClientHelper.cs`) to handle calling localhost via the emulators and simulators for testing. For more information on calling local services from emulators and simulators, see [Connect to local web services from Android emulators and iOS simulators (.NET MAUI documentation)](/en-us/dotnet/maui/data-cloud/local-web-services).

The MAUI Authentication State Provider also uses the `TokenStorage` class (`MauiBlazorWeb/Services/TokenStorage.cs`) that uses [`SecureStorage` API (.NET MAUI documentation)](/en-us/dotnet/maui/platform-integration/storage/secure-storage) to store the user's token securely on the device. It refreshes the token near token expiration to avoid user logins.

### MAUI `MauiProgram` file

The MAUI project's `MauiProgram` file (`MauiProgram.cs`) is where the `MauiAuthenticationStateProvider` is registered in the DI container. Authorization core components are also registered, where API such as [`AuthorizeView` component](../../security/?view=aspnetcore-10.0#authorizeview-component) are defined.

In `MauiBlazorWeb/MauiProgram.cs`, core functionality is added by calling [AddAuthorizationCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authorizationservicecollectionextensions.addauthorizationcore):

```
builder.Services.AddAuthorizationCore();
```

The app's custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) (`MauiAuthenticationStateProvider`) is registered in `MauiProgram.cs`:

```
builder.Services.AddScoped<MauiAuthenticationStateProvider>();
```

Use the `MauiAuthenticationStateProvider` when the app requires an [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider):

```
builder.Services.AddScoped<AuthenticationStateProvider>(s => 
    (MauiAuthenticationStateProvider)
        s.GetRequiredService<MauiAuthenticationStateProvider>());
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Overview of ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

ASP.NET Core is a cross-platform, high-performance, open-source framework for building modern web apps using [.NET](/en-us/dotnet/core/introduction). The framework is built for large-scale app development and can handle any size workload, making it a robust choice for enterprise-level apps.

Key features:

* Lightweight and modular HTTP request pipeline.
* [Kestrel](fundamentals/servers/kestrel?view=aspnetcore-10.0): A [high-performance](https://github.com/aspnet/benchmarks) and cross-platform HTTP server.
* Integrated [dependency injection](fundamentals/dependency-injection?view=aspnetcore-10.0).
* [Environment-based configuration](fundamentals/configuration/?view=aspnetcore-10.0).
* Rich logging, tracing, and runtime metrics.
* [Blazor](blazor/?view=aspnetcore-10.0): Create rich interactive web UI components using [C#](/en-us/dotnet/csharp/)—no JavaScript required.
* Integrate seamlessly with popular client-side frameworks and libraries, including [Angular](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-angular), [React](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-react), [Vue](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-vue), and [Bootstrap](https://getbootstrap.com/).
* [Minimal APIs](fundamentals/minimal-apis?view=aspnetcore-10.0): Build fast web APIs with minimal code and configuration by fluently declaring API routes and endpoints.
* [SignalR](/en-us/aspnet/signalr/): Add real-time web functionality.
* [gRPC](grpc/?view=aspnetcore-10.0): High performance Remote Procedure Call (RPC) services.
* Security: Built-in security features for [authentication](security/authentication/?view=aspnetcore-10.0), [authorization](security/authorization/introduction?view=aspnetcore-10.0), and [data protection](security/data-protection/introduction?view=aspnetcore-10.0).
* Testing: Easily create unit and integration tests.
* Tooling: Maximize your development productivity with [Visual Studio](https://visualstudio.microsoft.com/) and [Visual Studio Code](https://code.visualstudio.com/).

- Lightweight and modular HTTP request pipeline.
- [Kestrel](fundamentals/servers/kestrel?view=aspnetcore-10.0): A [high-performance](https://github.com/aspnet/benchmarks) and cross-platform HTTP server.
- Integrated [dependency injection](fundamentals/dependency-injection?view=aspnetcore-10.0).
- [Environment-based configuration](fundamentals/configuration/?view=aspnetcore-10.0).
- Rich logging, tracing, and runtime metrics.
- [Blazor](blazor/?view=aspnetcore-10.0): Create rich interactive web UI components using [C#](/en-us/dotnet/csharp/)—no JavaScript required.
- Integrate seamlessly with popular client-side frameworks and libraries, including [Angular](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-angular), [React](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-react), [Vue](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-vue), and [Bootstrap](https://getbootstrap.com/).
- [SignalR](/en-us/aspnet/signalr/): Add real-time web functionality.
- [gRPC](grpc/?view=aspnetcore-10.0): High performance Remote Procedure Call (RPC) services.
- Security: Built-in security features for [authentication](security/authentication/?view=aspnetcore-10.0), [authorization](security/authorization/introduction?view=aspnetcore-10.0), and [data protection](security/data-protection/introduction?view=aspnetcore-10.0).
- Testing: Easily create unit and integration tests.
- Tooling: Maximize your development productivity with [Visual Studio](https://visualstudio.microsoft.com/) and [Visual Studio Code](https://code.visualstudio.com/).

* Lightweight and modular HTTP request pipeline.
* [Kestrel](fundamentals/servers/kestrel?view=aspnetcore-10.0): A [high-performance](https://github.com/aspnet/benchmarks) and cross-platform HTTP server.
* Integrated [dependency injection](fundamentals/dependency-injection?view=aspnetcore-10.0).
* [Environment-based configuration](fundamentals/configuration/?view=aspnetcore-10.0).
* Rich logging, tracing, and runtime metrics.
* Develop apps and APIs using [Razor Pages](razor-pages/?view=aspnetcore-10.0) and [Model-View-Controller (MVC)](mvc/overview?view=aspnetcore-10.0) frameworks.
* Integrate seamlessly with popular client-side frameworks and libraries, including [Angular](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-angular), [React](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-react), [Vue](/en-us/visualstudio/javascript/tutorial-asp-net-core-with-vue), and [Bootstrap](https://getbootstrap.com/).
* [SignalR](/en-us/aspnet/signalr/): Add real-time web functionality.
* [gRPC](grpc/?view=aspnetcore-10.0): High performance Remote Procedure Call (RPC) services.
* Security: Built-in security features for [authentication](security/authentication/?view=aspnetcore-10.0), [authorization](security/authorization/introduction?view=aspnetcore-10.0), and [data protection](security/data-protection/introduction?view=aspnetcore-10.0).
* Testing: Easily create unit and integration tests.
* Tooling: Maximize your development productivity with [Visual Studio](https://visualstudio.microsoft.com/) and [Visual Studio Code](https://code.visualstudio.com/).

## Why choose ASP.NET Core?

* **Unified framework**: ASP.NET Core is a complete and fully integrated web framework with built-in production-ready components to handle all of your web development needs.
* **Full stack productivity**: Build more apps faster by enabling your team to work full stack, from the frontend to the backend, using a single development framework.
* **Secure by design**: ASP.NET Core is built with security as a top concern and includes built-in support for authentication, authorization, and data protection.
* **Cloud-ready**: Whether you're deploying to your own data centers or to the cloud, ASP.NET Core simplifies deployment, monitoring, and configuration.
* **Performance & scalability**: Handle the most demanding workloads with ASP.NET Core's industry leading performance.
* **Trusted and mature**: ASP.NET Core is used and proven at hyperscale by some of the largest services in the world, including Bing, Xbox, Microsoft 365, and Azure.

## Get started

Are you ready to start your ASP.NET Core learning journey? It's time to build your first web app with ASP.NET Core!

[Get started with ASP.NET Core](get-started?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/signalr.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Get started with ASP.NET Core SignalR

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This tutorial teaches the basics of building a real-time app using SignalR. You learn how to:

* Create a web project.
* Add the SignalR client library.
* Create a SignalR hub.
* Configure the project to use SignalR.
* Add code that sends messages from any client to all connected clients.

At the end, you'll have a working chat app:

## Create a web app project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

Start the latest version of Visual Studio and select **Create a new project**.

In the **Create a new project** dialog, select **ASP.NET Core Web App (Razor Pages)**, and then select **Next**.

In the **Configure your new project** dialog, enter `SignalRChat` for **Project name**. It's important to name the project `SignalRChat`, including matching the capitalization, so the namespaces match the code in the tutorial.

Select **Next**.

In the **Additional information** dialog, select **.NET 10.0 (Long Term Support)** and then select **Create**.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs)

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).
* Change to the directory (`cd`) that will contain the project.
* Run the following commands:

```
dotnet new webapp -o SignalRChat
code -r SignalRChat
```

The `dotnet new` command creates a new Razor Pages project in the `SignalRChat` folder.

The `code` command opens the `SignalRChat` folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

## Add the SignalR client library

The SignalR server library is included in the ASP.NET Core shared framework. The JavaScript client library isn't automatically included in the project. For this tutorial, use Library Manager (LibMan) to get the client library from [unpkg](https://unpkg.com/). `unpkg`is a fast, global content delivery network for everything on [npm](https://www.npmjs.com/).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

In **Solution Explorer**, right-click the project, and select **Add** > **Client-Side Library**.

In the **Add Client-Side Library** dialog:

* Select **unpkg** for **Provider**
* Enter `@microsoft/signalr@latest` for **Library**.
* Select **Choose specific files**, expand the *dist/browser* folder, and select `signalr.js` and `signalr.min.js`.
* Set **Target Location** to `wwwroot/js/signalr/`.
* Select **Install**.

LibMan creates a `wwwroot/js/signalr` folder and copies the selected files to it. A `libman.json` file is created with the following code:

```
{
  "version": "3.0",
  "defaultProvider": "unpkg",
  "libraries": [
    {
      "library": "@microsoft/signalr@latest",
      "destination": "wwwroot/lib/microsoft/signalr/",
      "files": [
        "dist/browser/signalr.js",
        "dist/browser/signalr.min.js"
      ]
    }
  ]
}
```

In the integrated terminal, run the following commands to install LibMan after uninstalling any previous version, if one exists.

```
dotnet tool uninstall -g Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Navigate to the project folder, which contains the `SignalRChat.csproj` file.

Run the following command to get the SignalR client library by using LibMan. It may take a few seconds before displaying output.

```
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js
```

The parameters specify the following options:

* Use the unpkg provider.
* Copy files to the `wwwroot/js/signalr` destination.
* Copy only the specified files.

The output looks similar to the following:

```
Downloading file https://unpkg.com/@microsoft/signalr@latest/dist/browser/signalr.js...
wwwroot/js/signalr/dist/browser/signalr.js written to disk
Installed library "@microsoft/signalr@latest" to "wwwroot/js/signalr"
```

## Create a SignalR hub

A *hub* is a class that serves as a high-level pipeline that handles client-server communication.

In the SignalRChat project folder, create a `Hubs` folder.

In the `Hubs` folder, create the `ChatHub` class with the following code:

```
using Microsoft.AspNetCore.SignalR;

namespace SignalRChat.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
```

The `ChatHub` class inherits from the SignalR [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub) class. The `Hub` class manages connections, groups, and messaging.

The `SendMessage` method can be called by a connected client to send a message to all clients. JavaScript client code that calls the method is shown later in the tutorial. SignalR code is asynchronous to provide maximum scalability.

## Configure SignalR

The SignalR server must be configured to pass SignalR requests to SignalR. Add the following highlighted code to the `Program.cs` file.

```
using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapHub<ChatHub>("/chatHub");
app.MapRazorPages()
   .WithStaticAssets();

app.Run();
```

The preceding highlighted code adds SignalR to the ASP.NET Core dependency injection and routing systems.

## Add SignalR client code

Replace the content in `Pages/Index.cshtml` with the following code:

```
@page
<div class="container">
    <div class="row p-1">
        <div class="col-1">User</div>
        <div class="col-5"><input type="text" id="userInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-1">Message</div>
        <div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-6 text-end">
            <input type="button" id="sendButton" value="Send Message" />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <hr />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <ul id="messagesList"></ul>
        </div>
    </div>
</div>
<script src="~/lib/microsoft/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
```

The preceding markup:

* Creates text boxes and a submit button.
* Creates a list with `id="messagesList"` for displaying messages that are received from the SignalR hub.
* Includes script references to SignalR and the `chat.js` app code is created in the next step.

In the `wwwroot/js` folder, create a `chat.js` file with the following code:

```
"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

//Disable the send button until connection is established.
document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var li = document.createElement("li");
    document.getElementById("messagesList").appendChild(li);
    // We can assign user-supplied strings to an element's textContent because it
    // is not interpreted as markup. If you're assigning in any other way, you 
    // should be aware of possible script injection concerns.
    li.textContent = `${user} says ${message}`;
});

connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
```

The preceding JavaScript:

* Creates and starts a connection.
* Adds to the submit button a handler that sends messages to the hub.
* Adds to the connection object a handler that receives messages from the hub and adds them to the list.

## Run the app

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

Select `Ctrl`+`F5` to run the app without debugging.

Select `Ctrl`+`F5` to run the app without debugging.

Copy the URL from the address bar, open another browser instance or tab, and paste the URL in the address bar.

Choose either browser, enter a name and message, and select the **Send Message** button.

The name and message are displayed on both pages instantly.

Tip

If the app doesn't work, open the browser developer tools (F12) and go to the console. Look for possible errors related to HTML and JavaScript code. For example, if `signalr.js` was put in a different folder than directed, the reference to that file won't work resulting in a 404 error in the console. If an `ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY` error has occurred in Chrome, run the following commands to update the development certificate:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore). For more information on Azure SignalR Service, see [What is Azure SignalR Service?](/en-us/azure/azure-signalr/signalr-overview).

## Next steps

* [Use hubs](../signalr/hubs?view=aspnetcore-10.0)
* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#use-strongly-typed-hubs)
* [Authentication and authorization in ASP.NET Core SignalR](../signalr/authn-and-authz?view=aspnetcore-10.0)
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/javascript-client/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

This tutorial teaches the basics of building a real-time app using SignalR. You learn how to:

* Create a web project.
* Add the SignalR client library.
* Create a SignalR hub.
* Configure the project to use SignalR.
* Add code that sends messages from any client to all connected clients.

At the end, you'll have a working chat app:

## Create a web app project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

Start Visual Studio 2022 and select **Create a new project**.

In the **Create a new project** dialog, select **ASP.NET Core Web App (Razor Pages)**, and then select **Next**.

In the **Configure your new project** dialog, enter `SignalRChat` for **Project name**. It's important to name the project `SignalRChat`, including matching the capitalization, so the namespaces match the code in the tutorial.

Select **Next**.

In the **Additional information** dialog, select **.NET 8.0 (Long Term Support)** and then select **Create**.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs)

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).
* Change to the directory (`cd`) that will contain the project.
* Run the following commands:

```
dotnet new webapp -o SignalRChat
code -r SignalRChat
```

The `dotnet new` command creates a new Razor Pages project in the `SignalRChat` folder.

The `code` command opens the `SignalRChat` folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

## Add the SignalR client library

The SignalR server library is included in the ASP.NET Core shared framework. The JavaScript client library isn't automatically included in the project. For this tutorial, use Library Manager (LibMan) to get the client library from [unpkg](https://unpkg.com/). `unpkg`is a fast, global content delivery network for everything on [npm](https://www.npmjs.com/).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

In **Solution Explorer**, right-click the project, and select **Add** > **Client-Side Library**.

In the **Add Client-Side Library** dialog:

* Select **unpkg** for **Provider**
* Enter `@microsoft/signalr@latest` for **Library**.
* Select **Choose specific files**, expand the *dist/browser* folder, and select `signalr.js` and `signalr.min.js`.
* Set **Target Location** to `wwwroot/js/signalr/`.
* Select **Install**.

LibMan creates a `wwwroot/js/signalr` folder and copies the selected files to it.

In the integrated terminal, run the following commands to install LibMan after uninstalling any previous version, if one exists.

```
dotnet tool uninstall -g Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Navigate to the project folder, which contains the `SignalRChat.csproj` file.

Run the following command to get the SignalR client library by using LibMan. It may take a few seconds before displaying output.

```
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js
```

The parameters specify the following options:

* Use the unpkg provider.
* Copy files to the `wwwroot/js/signalr` destination.
* Copy only the specified files.

The output looks similar to the following:

```
Downloading file https://unpkg.com/@microsoft/signalr@latest/dist/browser/signalr.js...
wwwroot/js/signalr/dist/browser/signalr.js written to disk
Installed library "@microsoft/signalr@latest" to "wwwroot/js/signalr"
```

## Create a SignalR hub

A *hub* is a class that serves as a high-level pipeline that handles client-server communication.

In the SignalRChat project folder, create a `Hubs` folder.

In the `Hubs` folder, create the `ChatHub` class with the following code:

```
using Microsoft.AspNetCore.SignalR;

namespace SignalRChat.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
```

The `ChatHub` class inherits from the SignalR [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub) class. The `Hub` class manages connections, groups, and messaging.

The `SendMessage` method can be called by a connected client to send a message to all clients. JavaScript client code that calls the method is shown later in the tutorial. SignalR code is asynchronous to provide maximum scalability.

## Configure SignalR

The SignalR server must be configured to pass SignalR requests to SignalR. Add the following highlighted code to the `Program.cs` file.

```
using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();
```

The preceding highlighted code adds SignalR to the ASP.NET Core dependency injection and routing systems.

## Add SignalR client code

Replace the content in `Pages/Index.cshtml` with the following code:

```
@page
<div class="container">
    <div class="row p-1">
        <div class="col-1">User</div>
        <div class="col-5"><input type="text" id="userInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-1">Message</div>
        <div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-6 text-end">
            <input type="button" id="sendButton" value="Send Message" />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <hr />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <ul id="messagesList"></ul>
        </div>
    </div>
</div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
```

The preceding markup:

* Creates text boxes and a submit button.
* Creates a list with `id="messagesList"` for displaying messages that are received from the SignalR hub.
* Includes script references to SignalR and the `chat.js` app code is created in the next step.

In the `wwwroot/js` folder, create a `chat.js` file with the following code:

```
"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

//Disable the send button until connection is established.
document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var li = document.createElement("li");
    document.getElementById("messagesList").appendChild(li);
    // We can assign user-supplied strings to an element's textContent because it
    // is not interpreted as markup. If you're assigning in any other way, you 
    // should be aware of possible script injection concerns.
    li.textContent = `${user} says ${message}`;
});

connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
```

The preceding JavaScript:

* Creates and starts a connection.
* Adds to the submit button a handler that sends messages to the hub.
* Adds to the connection object a handler that receives messages from the hub and adds them to the list.

## Run the app

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

Select `Ctrl`+`F5` to run the app without debugging.

Select `Ctrl`+`F5` to run the app without debugging.

Copy the URL from the address bar, open another browser instance or tab, and paste the URL in the address bar.

Choose either browser, enter a name and message, and select the **Send Message** button.

The name and message are displayed on both pages instantly.

Tip

If the app doesn't work, open the browser developer tools (F12) and go to the console. Look for possible errors related to HTML and JavaScript code. For example, if `signalr.js` was put in a different folder than directed, the reference to that file won't work resulting in a 404 error in the console. If an `ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY` error has occurred in Chrome, run the following commands to update the development certificate:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore). For more information on Azure SignalR Service, see [What is Azure SignalR Service?](/en-us/azure/azure-signalr/signalr-overview).

## Next steps

* [Use hubs](../signalr/hubs?view=aspnetcore-10.0)
* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#use-strongly-typed-hubs)
* [Authentication and authorization in ASP.NET Core SignalR](../signalr/authn-and-authz?view=aspnetcore-10.0)
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/javascript-client/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

This tutorial teaches the basics of building a real-time app using SignalR. You learn how to:

* Create a web project.
* Add the SignalR client library.
* Create a SignalR hub.
* Configure the project to use SignalR.
* Add code that sends messages from any client to all connected clients.

At the end, you'll have a working chat app:

## Create a web app project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

Start Visual Studio 2022 and select **Create a new project**.

In the **Create a new project** dialog, select **ASP.NET Core Web App**, and then select **Next**.

In the **Configure your new project** dialog, enter `SignalRChat` for **Project name**. It's important to name the project `SignalRChat`, including matching the capitalization, so the namespaces match the code in the tutorial.

Select **Next**.

In the **Additional information** dialog, select **.NET 7.0 (Standard Term Support)** and then select **Create**.

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs)

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).
* Change to the directory (`cd`) that will contain the project.
* Run the following commands:

```
dotnet new webapp -o SignalRChat
code -r SignalRChat
```

The `dotnet new` command creates a new Razor Pages project in the `SignalRChat` folder.

The `code` command opens the \`SignalRChat1 folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Select **File** > **New Project**.

In Visual Studio 2022 for Mac select **Web and Console** > **App** > **Web Application** > **Continue**.

In the **Configure your new Web Application** dialog:

* Confirm that **Authentication** is set to **No Authentication**.
* Confirm that **Target framework** is set to the latest .NET 7.x version.
* Select **Continue**.

Name the project `SignalRChat` and select **Continue**.

## Add the SignalR client library

The SignalR server library is included in the ASP.NET Core shared framework. The JavaScript client library isn't automatically included in the project. For this tutorial, use Library Manager (LibMan) to get the client library from [unpkg](https://unpkg.com/). `unpkg`is a fast, global content delivery network for everything on [npm](https://www.npmjs.com/).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

In **Solution Explorer**, right-click the project, and select **Add** > **Client-Side Library**.

In the **Add Client-Side Library** dialog:

* Select **unpkg** for **Provider**
* Enter `@microsoft/signalr@latest` for **Library**.
* Select **Choose specific files**, expand the *dist/browser* folder, and select `signalr.js` and `signalr.min.js`.
* Set **Target Location** to `wwwroot/js/signalr/`.
* Select **Install**.

LibMan creates a `wwwroot/js/signalr` folder and copies the selected files to it.

In the integrated terminal, run the following commands to install LibMan after uninstalling any previous version, if one exists.

```
dotnet tool uninstall -g Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Navigate to the project folder, which contains the `SignalRChat.csproj` file.

Run the following command to get the SignalR client library by using LibMan. It may take a few seconds before displaying output.

```
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js
```

The parameters specify the following options:

* Use the unpkg provider.
* Copy files to the `wwwroot/js/signalr` destination.
* Copy only the specified files.

The output looks like the following example:

```
wwwroot/js/signalr/dist/browser/signalr.js written to disk
wwwroot/js/signalr/dist/browser/signalr.js written to disk
Installed library "@microsoft/signalr@latest" to "wwwroot/js/signalr"
```

In the **Terminal**, run the following commands to install LibMan after uninstalling any previous version, if one exists.

```
dotnet tool uninstall -g Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Navigate to the project folder, which contains the `SignalRChat.csproj` file.

Run the following command to get the SignalR client library by using LibMan:

```
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.js
```

The parameters specify the following options:

* Use the unpkg provider.
* Copy files to the `wwwroot/js/signalr` destination.
* Copy only the specified files.

The output looks like the following example:

```
wwwroot/js/signalr/dist/browser/signalr.js written to disk
wwwroot/js/signalr/dist/browser/signalr.js written to disk
Installed library "@microsoft/signalr@latest" to "wwwroot/js/signalr"
```

## Create a SignalR hub

A *hub* is a class that serves as a high-level pipeline that handles client-server communication.

In the SignalRChat project folder, create a `Hubs` folder.

In the `Hubs` folder, create the `ChatHub` class with the following code:

```
using Microsoft.AspNetCore.SignalR;

namespace SignalRChat.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
```

The `ChatHub` class inherits from the SignalR [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub) class. The `Hub` class manages connections, groups, and messaging.

The `SendMessage` method can be called by a connected client to send a message to all clients. JavaScript client code that calls the method is shown later in the tutorial. SignalR code is asynchronous to provide maximum scalability.

## Configure SignalR

The SignalR server must be configured to pass SignalR requests to SignalR. Add the following highlighted code to the `Program.cs` file.

```
using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();
```

The preceding highlighted code adds SignalR to the ASP.NET Core dependency injection and routing systems.

## Add SignalR client code

Replace the content in `Pages/Index.cshtml` with the following code:

```
@page
<div class="container">
    <div class="row p-1">
        <div class="col-1">User</div>
        <div class="col-5"><input type="text" id="userInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-1">Message</div>
        <div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-6 text-end">
            <input type="button" id="sendButton" value="Send Message" />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <hr />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <ul id="messagesList"></ul>
        </div>
    </div>
</div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
```

The preceding markup:

* Creates text boxes and a submit button.
* Creates a list with `id="messagesList"` for displaying messages that are received from the SignalR hub.
* Includes script references to SignalR and the `chat.js` app code is created in the next step.

In the `wwwroot/js` folder, create a `chat.js` file with the following code:

```
"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

//Disable the send button until connection is established.
document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var li = document.createElement("li");
    document.getElementById("messagesList").appendChild(li);
    // We can assign user-supplied strings to an element's textContent because it
    // is not interpreted as markup. If you're assigning in any other way, you 
    // should be aware of possible script injection concerns.
    li.textContent = `${user} says ${message}`;
});

connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
```

The preceding JavaScript:

* Creates and starts a connection.
* Adds to the submit button a handler that sends messages to the hub.
* Adds to the connection object a handler that receives messages from the hub and adds them to the list.

## Run the app

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)

Select `Ctrl`+`F5` to run the app without debugging.

Select `Ctrl`+`F5` to run the app without debugging.

Select **Debug** > **Start Without Debugging** to run the app without debugging.

Copy the URL from the address bar, open another browser instance or tab, and paste the URL in the address bar.

Choose either browser, enter a name and message, and select the **Send Message** button.

The name and message are displayed on both pages instantly.

Tip

If the app doesn't work, open the browser developer tools (F12) and go to the console. Look for possible errors related to HTML and JavaScript code. For example, if `signalr.js` was put in a different folder than directed, the reference to that file won't work resulting in a 404 error in the console. If an `ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY` error has occurred in Chrome, run the following commands to update the development certificate:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore). For more information on Azure SignalR Service, see [What is Azure SignalR Service?](/en-us/azure/azure-signalr/signalr-overview).

## Next steps

* [Use hubs](../signalr/hubs?view=aspnetcore-10.0)
* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#strongly-typed-hubs)
* [Authentication and authorization in ASP.NET Core SignalR](../signalr/authn-and-authz?view=aspnetcore-10.0)
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/javascript-client/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

This tutorial teaches the basics of building a real-time app using SignalR. You learn how to:

* Create a web project.
* Add the SignalR client library.
* Create a SignalR hub.
* Configure the project to use SignalR.
* Add code that sends messages from any client to all connected clients.

At the end, you'll have a working chat app:

## Create a web app project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

Start Visual Studio 2022 and select **Create a new project**.

In the **Create a new project** dialog, select **ASP.NET Core Web App**, and then select **Next**.

In the **Configure your new project** dialog, enter `SignalRChat` for **Project name**. It's important to name the project `SignalRChat`, including matching the capitalization, so the namespaces match the code in the tutorial.

Select **Next**.

In the **Additional information** dialog, select **.NET 6.0 (Long-term support)** and then select **Create**.

Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

Change to the directory (`cd`) that will contain the project.

Run the following commands:

```
dotnet new webapp -o SignalRChat
code -r SignalRChat
```

Visual Studio Code displays a dialog box that asks **Do you trust the authors of the files in this folder**. Select:

* The checkbox **trust the authors of all files in the parent folder**
* **Yes, I trust the authors** (because dotnet generated the files).

The `dotnet new` command creates a new Razor Pages project in the `SignalRChat` folder.

The `code` command opens the \`SignalRChat1 folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Select **File** > **New Solution**.

In Visual Studio 2022 for Mac select **Web and Console** > **App** > **Web Application** > **Continue**.

In the **Configure your new Web Application** dialog:

* Confirm that **Authentication** is set to **No Authentication**.
* Confirm that **Target framework** is set to the latest .NET 6.x version.
* Select **Continue**.

Name the project `SignalRChat` and select **Continue**.

## Add the SignalR client library

The SignalR server library is included in the ASP.NET Core shared framework. The JavaScript client library isn't automatically included in the project. For this tutorial, use Library Manager (LibMan) to get the client library from [unpkg](https://unpkg.com/). `unpkg`is a fast, global content delivery network for everything on [npm](https://www.npmjs.com/).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

In **Solution Explorer**, right-click the project, and select **Add** > **Client-Side Library**.

In the **Add Client-Side Library** dialog:

* Select **unpkg** for **Provider**
* Enter `@microsoft/signalr@latest` for **Library**.
* Select **Choose specific files**, expand the *dist/browser* folder, and select `signalr.js` and `signalr.min.js`.
* Set **Target Location** to `wwwroot/js/signalr/`.
* Select **Install**.

LibMan creates a `wwwroot/js/signalr` folder and copies the selected files to it.

In the integrated terminal, run the following commands to install LibMan after uninstalling any previous version, if one exists.

```
dotnet tool uninstall -g Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Run the following command to get the SignalR client library by using LibMan. It may take a few seconds before displaying output.

```
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.js
```

The parameters specify the following options:

* Use the unpkg provider.
* Copy files to the `wwwroot/js/signalr` destination.
* Copy only the specified files.

The output looks like the following example:

```
wwwroot/js/signalr/dist/browser/signalr.js written to disk
wwwroot/js/signalr/dist/browser/signalr.js written to disk
Installed library "@microsoft/signalr@latest" to "wwwroot/js/signalr"
```

In the **Terminal**, run the following commands to install LibMan after uninstalling any previous version, if one exists.

```
dotnet tool uninstall -g Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Navigate to the project folder (the one that contains the `SignalRChat.csproj` file).

Run the following command to get the SignalR client library by using LibMan:

```
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.js
```

The parameters specify the following options:

* Use the unpkg provider.
* Copy files to the `wwwroot/js/signalr` destination.
* Copy only the specified files.

The output looks like the following example:

```
wwwroot/js/signalr/dist/browser/signalr.js written to disk
wwwroot/js/signalr/dist/browser/signalr.js written to disk
Installed library "@microsoft/signalr@latest" to "wwwroot/js/signalr"
```

## Create a SignalR hub

A *hub* is a class that serves as a high-level pipeline that handles client-server communication.

In the SignalRChat project folder, create a `Hubs` folder.

In the `Hubs` folder, create the `ChatHub` class with the following code:

```
using Microsoft.AspNetCore.SignalR;

namespace SignalRChat.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
```

The `ChatHub` class inherits from the SignalR [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub) class. The `Hub` class manages connections, groups, and messaging.

The `SendMessage` method can be called by a connected client to send a message to all clients. JavaScript client code that calls the method is shown later in the tutorial. SignalR code is asynchronous to provide maximum scalability.

## Configure SignalR

The SignalR server must be configured to pass SignalR requests to SignalR. Add the following highlighted code to the `Program.cs` file.

```
using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();
```

The preceding highlighted code adds SignalR to the ASP.NET Core dependency injection and routing systems.

## Add SignalR client code

Replace the content in `Pages/Index.cshtml` with the following code:

```
@page
    <div class="container">
        <div class="row p-1">
            <div class="col-1">User</div>
            <div class="col-5"><input type="text" id="userInput" /></div>
        </div>
        <div class="row p-1">
            <div class="col-1">Message</div>
            <div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
        </div>
        <div class="row p-1">
            <div class="col-6 text-end">
                <input type="button" id="sendButton" value="Send Message" />
            </div>
        </div>
        <div class="row p-1">
            <div class="col-6">
                <hr />
            </div>
        </div>
        <div class="row p-1">
            <div class="col-6">
                <ul id="messagesList"></ul>
            </div>
        </div>
    </div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
```

The preceding markup:

* Creates text boxes and a submit button.
* Creates a list with `id="messagesList"` for displaying messages that are received from the SignalR hub.
* Includes script references to SignalR and the `chat.js` app code is created in the next step.

In the `wwwroot/js` folder, create a `chat.js` file with the following code:

```
"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

//Disable the send button until connection is established.
document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var li = document.createElement("li");
    document.getElementById("messagesList").appendChild(li);
    // We can assign user-supplied strings to an element's textContent because it
    // is not interpreted as markup. If you're assigning in any other way, you 
    // should be aware of possible script injection concerns.
    li.textContent = `${user} says ${message}`;
});

connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
```

The preceding JavaScript:

* Creates and starts a connection.
* Adds to the submit button a handler that sends messages to the hub.
* Adds to the connection object a handler that receives messages from the hub and adds them to the list.

## Run the app

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)

Press CTRL+F5 to run the app without debugging.

Select Ctrl+F5 to run the app without the debugger.

From the menu, select **Run > Start Without Debugging**.

Copy the URL from the address bar, open another browser instance or tab, and paste the URL in the address bar.

Choose either browser, enter a name and message, and select the **Send Message** button.

The name and message are displayed on both pages instantly.

Tip

If the app doesn't work, open the browser developer tools (F12) and go to the console. Look for possible errors related to HTML and JavaScript code. For example, if `signalr.js` was put in a different folder than directed, the reference to that file won't work resulting in a 404 error in the console. If an `ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY` error has occurred in Chrome, run the following commands to update the development certificate:

```
dotnet dev-certs https --clean
dotnet dev-certs https --trust
```

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore). For more information on Azure SignalR Service, see [What is Azure SignalR Service?](/en-us/azure/azure-signalr/signalr-overview).

## Next steps

* [Use hubs](../signalr/hubs?view=aspnetcore-10.0)
* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#strongly-typed-hubs)
* [Authentication and authorization in ASP.NET Core SignalR](../signalr/authn-and-authz?view=aspnetcore-10.0)
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/javascript-client/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

This tutorial teaches the basics of building a real-time app using SignalR. You learn how to:

* Create a web project.
* Add the SignalR client library.
* Create a SignalR hub.
* Configure the project to use SignalR.
* Add code that sends messages from any client to all connected clients.

At the end, you'll have a working chat app:

## Create a web app project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- From the menu, select **File > New Project**.
- In the **Create a new project** dialog, select **ASP.NET Core Web Application**, and then select **Next**.
- In the **Configure your new project** dialog, name the project *SignalRChat*, and then select **Create**.
- In the **Create a new ASP.NET Core web Application** dialog, select **.NET Core** and **ASP.NET Core 3.1**.
- Select **Web Application** to create a project that uses Razor Pages, and then select **Create**.

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal) to the folder in which the new project folder will be created.
* Run the following commands:

```
dotnet new webapp -o SignalRChat
cd SignalRChat
code -r .
```

* From the menu, select **File > New Solution**.
* Select **.NET Core > App > Web Application** (Don't select **Web Application (Model-View-Controller)**), and then select **Next**.
* Make sure the **Target Framework** is set to **.NET Core 3.1**, and then select **Next**.
* Name the project *SignalRChat*, and then select **Create**.

## Add the SignalR client library

The SignalR server library is included in the ASP.NET Core 3.1 shared framework. The JavaScript client library isn't automatically included in the project. For this tutorial, you use Library Manager (LibMan) to get the client library from *unpkg*. unpkg is a content delivery network (CDN) that can deliver anything found in npm, the Node.js package manager.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

- In **Solution Explorer**, right-click the project, and select **Add** > **Client-Side Library**.
- In the **Add Client-Side Library** dialog, for **Provider** select **unpkg**.
- For **Library**, enter `@microsoft/signalr@latest`.
- Select **Choose specific files**, expand the *dist/browser* folder, and select `signalr.js` and `signalr.min.js`.
- Set **Target Location** to *wwwroot/js/signalr/*
- Select **Install**

LibMan creates a *wwwroot/js/signalr* folder and copies the selected files to it.

* In the integrated terminal, run the following command to install LibMan.

```
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

* Run the following command to get the SignalR client library by using LibMan. You might have to wait a few seconds before seeing output.

```
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.js
```

The parameters specify the following options:

* Use the unpkg provider.
* Copy files to the *wwwroot/js/signalr* destination.
* Copy only the specified files.

The output looks like the following example:

```
wwwroot/js/signalr/dist/browser/signalr.js written to disk
wwwroot/js/signalr/dist/browser/signalr.js written to disk
Installed library "@microsoft/signalr@latest" to "wwwroot/js/signalr"
```

* In the **Terminal**, run the following command to install LibMan.

```
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

* Navigate to the project folder (the one that contains the `SignalRChat.csproj` file).

* Run the following command to get the SignalR client library by using LibMan.

```
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.js
```

The parameters specify the following options:

* Use the unpkg provider.
* Copy files to the *wwwroot/js/signalr* destination.
* Copy only the specified files.

The output looks like the following example:

```
wwwroot/js/signalr/dist/browser/signalr.js written to disk
wwwroot/js/signalr/dist/browser/signalr.js written to disk
Installed library "@microsoft/signalr@latest" to "wwwroot/js/signalr"
```

## Create a SignalR hub

A *hub* is a class that serves as a high-level pipeline that handles client-server communication.

* In the SignalRChat project folder, create a *Hubs* folder.
* In the *Hubs* folder, create a `ChatHub.cs` file with the following code:

```
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRChat.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
```

The `ChatHub` class inherits from the SignalR `Hub` class. The `Hub` class manages connections, groups, and messaging.

The `SendMessage` method can be called by a connected client to send a message to all clients. JavaScript client code that calls the method is shown later in the tutorial. SignalR code is asynchronous to provide maximum scalability.

## Configure SignalR

The SignalR server must be configured to pass SignalR requests to SignalR.

* Add the following highlighted code to the `Startup.cs` file.

  ```
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Threading.Tasks;
  using Microsoft.AspNetCore.Builder;
  using Microsoft.AspNetCore.Hosting;
  using Microsoft.AspNetCore.HttpsPolicy;
  using Microsoft.Extensions.Configuration;
  using Microsoft.Extensions.DependencyInjection;
  using Microsoft.Extensions.Hosting;
  using SignalRChat.Hubs;

  namespace SignalRChat
  {
      public class Startup
      {
          public Startup(IConfiguration configuration)
          {
              Configuration = configuration;
          }

          public IConfiguration Configuration { get; }

          // This method gets called by the runtime. Use this method to add services to the container.
          public void ConfigureServices(IServiceCollection services)
          {
              services.AddRazorPages();
              services.AddSignalR();
          }

          // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
          public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
          {
              if (env.IsDevelopment())
              {
                  app.UseDeveloperExceptionPage();
              }
              else
              {
                  app.UseExceptionHandler("/Error");
                  // 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.UseHttpsRedirection();
              app.UseStaticFiles();

              app.UseRouting();

              app.UseAuthorization();

              app.UseEndpoints(endpoints =>
              {
                  endpoints.MapRazorPages();
                  endpoints.MapHub<ChatHub>("/chatHub");
              });
          }
      }
  }
  ```

  These changes add SignalR to the ASP.NET Core dependency injection and routing systems.

## Add SignalR client code

* Replace the content in `Pages/Index.cshtml` with the following code:

  ```
  @page
      <div class="container">
          <div class="row p-1">
              <div class="col-1">User</div>
              <div class="col-5"><input type="text" id="userInput" /></div>
          </div>
          <div class="row p-1">
              <div class="col-1">Message</div>
              <div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
          </div>
          <div class="row p-1">
              <div class="col-6 text-end">
                  <input type="button" id="sendButton" value="Send Message" />
              </div>
          </div>
          <div class="row p-1">
              <div class="col-6">
                  <hr />
              </div>
          </div>
          <div class="row p-1">
              <div class="col-6">
                  <ul id="messagesList"></ul>
              </div>
          </div>
      </div>
  <script src="~/js/signalr/dist/browser/signalr.js"></script>
  <script src="~/js/chat.js"></script>
  ```

  The preceding code:

  * Creates text boxes for name and message text, and a submit button.
  * Creates a list with `id="messagesList"` for displaying messages that are received from the SignalR hub.
  * Includes script references to SignalR and the `chat.js` application code that you create in the next step.

* In the *wwwroot/js* folder, create a `chat.js` file with the following code:

  ```
  "use strict";

  var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

  //Disable send button until connection is established
  document.getElementById("sendButton").disabled = true;

  connection.on("ReceiveMessage", function (user, message) {
      var li = document.createElement("li");
      document.getElementById("messagesList").appendChild(li);
      // We can assign user-supplied strings to an element's textContent because it
      // is not interpreted as markup. If you're assigning in any other way, you 
      // should be aware of possible script injection concerns.
      li.textContent = `${user} says ${message}`;
  });

  connection.start().then(function () {
      document.getElementById("sendButton").disabled = false;
  }).catch(function (err) {
      return console.error(err.toString());
  });

  document.getElementById("sendButton").addEventListener("click", function (event) {
      var user = document.getElementById("userInput").value;
      var message = document.getElementById("messageInput").value;
      connection.invoke("SendMessage", user, message).catch(function (err) {
          return console.error(err.toString());
      });
      event.preventDefault();
  });
  ```

  The preceding code:

  * Creates and starts a connection.
  * Adds to the submit button a handler that sends messages to the hub.
  * Adds to the connection object a handler that receives messages from the hub and adds them to the list.

## Run the app

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)

- Press **CTRL+F5** to run the app without debugging.

* In the integrated terminal, run the following command:

  ```
  dotnet watch run -p SignalRChat.csproj
  ```

- From the menu, select **Run > Start Without Debugging**.

* Copy the URL from the address bar, open another browser instance or tab, and paste the URL in the address bar.
* Choose either browser, enter a name and message, and select the **Send Message** button. The name and message are displayed on both pages instantly.

Tip

* If the app doesn't work, open your browser developer tools (F12) and go to the console. You might see errors related to your HTML and JavaScript code. For example, suppose you put `signalr.js` in a different folder than directed. In that case the reference to that file won't work and you'll see a 404 error in the console.

* If you get the error ERR\_SPDY\_INADEQUATE\_TRANSPORT\_SECURITY in Chrome, run these commands to update your development certificate:

  ```
  dotnet dev-certs https --clean
  dotnet dev-certs https --trust
  ```

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore).

## Next steps

* [Use hubs](../signalr/hubs?view=aspnetcore-10.0)
* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#strongly-typed-hubs)
* [Authentication and authorization in ASP.NET Core SignalR](../signalr/authn-and-authz?view=aspnetcore-10.0)
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/signalr/javascript-client/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/dependency-injection.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Dependency injection in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Kirk Larkin](https://twitter.com/serpent5), [Steve Smith](https://ardalis.com/), and [Brandon Dahler](https://github.com/brandondahler)

ASP.NET Core supports the dependency injection (DI) software design pattern, which is a technique for achieving [Inversion of Control (IoC)](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#dependency-inversion) between classes and their dependencies.

This article provides information on DI in ASP.NET Core web apps. For information on DI in all types of apps, including apps other than web apps, see [Dependency injection in .NET](/en-us/dotnet/core/extensions/dependency-injection/overview).

Guidance that adds to or supersedes the guidance in this article is found the following articles:

* [ASP.NET Core Blazor dependency injection](../blazor/fundamentals/dependency-injection?view=aspnetcore-10.0)
* [Dependency injection into controllers in ASP.NET Core](../mvc/controllers/dependency-injection?view=aspnetcore-10.0)
* [Use DI services to configure options](configuration/options?view=aspnetcore-10.0#use-di-services-to-configure-options)

Code examples in this article are based on [Blazor](../blazor/?view=aspnetcore-10.0). To see Razor Pages examples, see the [7.0 version of this article](?view=aspnetcore-7.0\&preserve-view=true).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/dependency-injection) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

When using the sample code in this article in a local Blazor Web App for demonstration purposes, adopt an [interactive render mode](../blazor/components/render-modes?view=aspnetcore-10.0).

## Overview of dependency injection

A *dependency* is an object that another object depends on. Consider the following `MyDependency` class with a `WriteMessage` method:

```
public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage: {message}");
    }
}
```

A class can create an instance of the `MyDependency` class to call its `WriteMessage` method. In the following example, the `MyDependency` class is a dependency of a Razor component.

`Pages/DependencyExample1.razor`:

```
@page "/dependency-example-1"

<button @onclick="WriteMessage">Write message</button>

@code {
    private readonly MyDependency dependency = new MyDependency();

    private void WriteMessage() =>
        dependency.WriteMessage("DependencyExample1.WriteMessage called");
}
```

A class can create an instance of the `MyDependency` class to call its `WriteMessage` method. In the following example, the `MyDependency` class is a dependency of an `IndexModel` page class.

`Pages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet called");
    }
}
```

The consuming class creates and directly depends on the `MyDependency` class. Taking a direct dependency, such as in the previous example, is problematic and should be avoided for the following reasons:

* To replace `MyDependency` with a different implementation while retaining `MyDependency` in the app, the consuming class must be modified. This situation is made worse when `MyDependency` is also a direct dependency of several other classes: All of the direct dependencies on `MyDependency` in the app must be changed.
* If `MyDependency` has dependencies, they must also be configured by the consuming class. In a large project with multiple classes depending on `MyDependency`, the configuration code becomes scattered around the app.
* The consuming class is difficult to [unit test](/en-us/dotnet/core/testing/#unit-tests) because it directly creates `MyDependency`, making it difficult to substitute a custom service for testing, such as an in-memory database or a different database provider.

DI addresses these problems through:

* The use of an interface or base class to abstract the dependency implementation.
* Registration of the dependency in a *service container*, also called a *DI container*. ASP.NET Core provides a built-in service container, [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider). Services are typically registered in the app's `Program` file (.NET 6 or later) or the app's `Startup` file (.NET 5 or earlier).
* *Injection* of the service into classes where it's used. The framework creates instances of dependencies and disposes of them when they're no longer required.

In the following example, the `IMyDependency` interface defines the `WriteMessage` method signature.

`Interfaces/IMyDependency.cs`:

```
public interface IMyDependency
{
    void WriteMessage(string message);
}
```

The preceding interface is implemented by the following concrete type, `MyDependency`.

`Services/MyDependency.cs`:

```
public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage: {message}");
    }
}
```

The app registers the `IMyDependency` service with the concrete type `MyDependency` where services are added to the service container, usually either in the `Program` file (.NET 6 or later) or `Startup.ConfigureServices` method (.NET 5 or earlier). The [AddScoped](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addscoped) method registers the service with a scoped lifetime, which is the lifetime of a [Blazor circuit](../blazor/hosting-models?view=aspnetcore-10.0#blazor-server) (.NET 8 or later) or a single request in an MVC or Razor Pages app. [Service lifetimes](#service-lifetimes) are described later in this article.

```
builder.Services.AddScoped<IMyDependency, MyDependency>();
```

```
services.AddScoped<IMyDependency, MyDependency>();
```

The `IMyDependency` service is requested and used to call the `WriteMessage` method, as the following Razor component demonstrates.

`Pages/DependencyExample2.razor`:

```
@page "/dependency-example-2"
@inject IMyDependency Dependency

<button @onclick="WriteMessage">Write message</button>

@code {
    private void WriteMessage() =>
        Dependency.WriteMessage("DependencyExample2.WriteMessage called");
}
```

The `IMyDependency` service is requested and used to call the `WriteMessage` method, as the following page model class demonstrates.

`Pages/Index.cshtml.cs`:

```
public class IndexModel(IMyDependency dependency) : PageModel
{
    public void OnGet()
    {
        dependency.WriteMessage("IndexModel.OnGet called");
    }
}
```

By using the DI pattern, the class that consumes the dependency:

* Doesn't use the concrete type `MyDependency`, only the `IMyDependency` interface it implements. That makes it easy to change the implementation without modifying the consumer.
* Doesn't directly create an instance of `MyDependency` or dispose of it. The dependency is created and disposed by the service container.

The `IMyDependency` interface implementation can be improved by using the built-in [logging API](logging/?view=aspnetcore-10.0), which is injected as a dependency in the following example.

`Services/MyDependency.cs`:

```
public class MyDependency(ILogger<MyDependency> logger) : IMyDependency
{
    public void WriteMessage(string message)
    {
        logger.LogInformation($"MyDependency.WriteMessage: {message}");
    }
}
```

`MyDependency` depends on [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1), a [framework-provided service](#framework-provided-services).

It's common to use DI in a chained fashion. Each requested dependency in turn requests its own dependencies. The container resolves the dependencies in the graph and returns the fully resolved service. The collective set of dependencies that must be resolved is typically referred to as a *dependency tree*, *dependency graph*, or *object graph*.

The container resolves [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) by taking advantage of [(generic) open types](/en-us/dotnet/csharp/language-reference/language-specification/types#843-open-and-closed-types), eliminating the need to register every [(generic) constructed type](/en-us/dotnet/csharp/language-reference/language-specification/types#84-constructed-types).

In DI terminology, a service:

* Is typically an object that provides a service to other objects, such as the preceding `IMyDependency` service.
* Isn't related to a web service, although the service might use a web service.

The `IMyDependency` implementations shown in the preceding examples were written to demonstrate general DI principles, not to implement logging. Most apps shouldn't need to create loggers, as the preceding examples show. The following code demonstrates directly using the [framework's built-in logging API](logging/?view=aspnetcore-10.0), which doesn't require the registration of a custom service (`IMyDependency`).

`Pages/LoggingExample.razor`:

```
@page "/logging-example"
@inject ILogger<LoggingExample> Logger

<button @onclick="WriteMessage">Write message</button>

@code {
    private void WriteMessage() => 
        Logger.LogInformation("LoggingExample.WriteMessage called");
}
```

`Pages/IndexModel.cshtml.cs`:

```
public class IndexModel(ILogger<IndexModel> logger) : PageModel
{ 
    public void OnGet()
    {
        logger.LogInformation("IndexModel.OnGet called");
    }
}
```

## Services injected into `Startup`

Services can be injected into the `Startup` constructor and the `Startup.Configure` method.

The Generic Host ([IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder)) of ASP.NET Core 3.0 or later, uses a single service container throughout the app's lifecycle after a temporary "root" service provider uses essential services for the host to start and configure the app's main service container. Most services, including custom services and framework services not involved in host startup, aren't configured or available in the service container when the `Startup` constructor is called. Only the following services can be injected into the `Startup` constructor when using the Generic Host:

* [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment)
* [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment)
* [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration)

By restricting the available services available in the `Startup` class constructor, the Generic Host prevents you from trying to use a service before it's created or available and from creating multiple instances of custom or framework singleton services, where a singleton service created in the temporary service container could be different from one created in the final service container.

Any service registered with the service container can be injected into the `Startup.Configure` method. In the following example, an [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) is injected:

```
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    ...
}
```

For more information, see [App startup in ASP.NET Core](startup?view=aspnetcore-10.0) and [Access configuration in Startup](configuration/?view=aspnetcore-10.0#access-configuration-in-startup).

## Service registration methods

For general guidance on service registrations, see [Service registration](/en-us/dotnet/core/extensions/dependency-injection/service-registration).

It's common to use multiple implementations when mocking types for testing. For more information, see [Integration tests in ASP.NET Core](../test/integration-tests?view=aspnetcore-10.0#inject-mock-services).

Registering a service with only an implementation type is equivalent to registering the service with the same implementation and service type:

```
builder.Services.AddSingleton<MyDependency>();
```

```
services.AddSingleton<MyDependency>();
```

Service registration methods can be used to register multiple service instances of the same service type. In the following example, [AddSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addsingleton) is called twice with `IMyDependency` as the service type. The second call to `AddSingleton` overrides the previous one when resolved as `IMyDependency` and adds to the previous one when multiple services are resolved via `IEnumerable<IMyDependency>`.

```
builder.Services.AddSingleton<IMyDependency, MyDependency>();
builder.Services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}
```

```
services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}
```

## Register groups of services with extension methods

The ASP.NET Core framework convention for registering a group of related services is to use a single `Add{GROUP NAME}` extension method to register all of the services required by a framework feature, where the `{GROUP NAME}` placeholder is a descriptive group name. For example, the [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) extension method registers services required for server-side rendering of Razor components.

Consider the following example that configures options and registers services:

```
builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();
```

```
services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();
```

Related groups of registrations can be moved to an extension method to register services. In the following example:

* The `AddConfig` extension method binds configuration data to strongly-typed C# classes and registers the classes in the service container.
* The `AddDependencyGroup` extension method adds additional class (service) dependencies.

```
namespace Microsoft.Extensions.DependencyInjection;

public static class ConfigServiceCollectionExtensions
{
    public static IServiceCollection AddConfig(
        this IServiceCollection services, IConfiguration config)
    {
        services.Configure<PositionOptions>(
            config.GetSection(PositionOptions.Position));
        services.Configure<ColorOptions>(
            config.GetSection(ColorOptions.Color));

        return services;
    }

    public static IServiceCollection AddDependencyGroup(
        this IServiceCollection services)
    {
        services.AddScoped<IMyDependency, MyDependency>();
        services.AddScoped<IMyDependency2, MyDependency2>();

        return services;
    }
}
```

The following code calls the preceding `AddConfig` and `AddDependencyGroup` extension methods to register the services:

```
builder.Services
    .AddConfig(builder.Configuration)
    .AddDependencyGroup();
```

```
services
    .AddConfig(builder.Configuration)
    .AddDependencyGroup();
```

We recommend that apps follow the naming convention of creating extension methods in the [Microsoft.Extensions.DependencyInjection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection) namespace, which:

* Encapsulates groups of service registrations.
* Provides convenient [IntelliSense](/en-us/visualstudio/ide/using-intellisense) access to the service.

## Service lifetimes

For general guidance on service lifetimes, see [Service lifetimes](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes). For additional service lifetime guidance that applies to Blazor apps, see [ASP.NET Core Blazor dependency injection](../blazor/fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope).

To use scoped services in middleware, use one of the following approaches:

* Inject the service into the middleware's `Invoke` or `InvokeAsync` method. Using [constructor injection](../mvc/controllers/dependency-injection?view=aspnetcore-10.0#constructor-injection) throws a runtime exception because it forces the scoped service to behave like a singleton. The sample in the [Lifetime and registration options](#lifetime-and-registration-options) section demonstrates the `InvokeAsync` approach.
* Use [factory-based middleware](middleware/extensibility?view=aspnetcore-10.0). Middleware registered using this approach is activated per client request (connection), which allows scoped services to be injected into the middleware's constructor.

For more information, see the following resources:

* [Write custom ASP.NET Core middleware](middleware/write?view=aspnetcore-10.0#per-request-middleware-dependencies)
* For more information on using keyed services with Razor components, see [ASP.NET Core Blazor dependency injection](../blazor/fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetime).

## Keyed services

*Keyed services* registers and retrieves services using keys. A service is associated with a key by calling any of the following extension methods for service registration:

* [AddKeyedSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addkeyedsingleton)
* [AddKeyedScoped](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addkeyedscoped)
* [AddKeyedTransient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addkeyedtransient)

The following example demonstrates keyed services with the following API:

* `IStringCache` is the service interface with a `Get` method signature.
* `StringCache1` and `StringCache2` are concrete service implementations for `IStringCache`.

`Interfaces/IStringCache.cs`:

```
public interface IStringCache
{
    string Get(int key);
}
```

`Services/StringCache1.cs`:

```
public class StringCache1 : IStringCache
{
    public string Get(int key) => $"Resolving {key} from StringCache1.";
}
```

`Services/StringCache2.cs`:

```
public class StringCache2 : IStringCache
{
    public string Get(int key) => $"Resolving {key} from StringCache2.";
}
```

Access a registered service by specifying the key with the [`[FromKeyedServices]` attribute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute).

Keyed service and example Minimal API endpoints in the `Program` file:

```
builder.Services.AddKeyedSingleton<IStringCache, StringCache1>("cache1");
builder.Services.AddKeyedSingleton<IStringCache, StringCache2>("cache2");

...

app.MapGet("/cache1", ([FromKeyedServices("cache1")] IStringCache stringCache1) => 
    stringCache1.Get(1));
app.MapGet("/cache2", ([FromKeyedServices("cache2")] IStringCache stringCache2) =>
    stringCache2.Get(2));
```

Example use in a Razor componment (`Pages/KeyedServicesExample.razor`) using the `[Inject]` attribute. Use the [InjectAttribute.Key](/en-us/dotnet/api/microsoft.aspnetcore.components.injectattribute.key#microsoft-aspnetcore-components-injectattribute-key) property to specify the key for the service to inject:

```
@page "/keyed-services-example"

@Cache?.Get(3)

@code {
    [Inject(Key = "cache1")]
    public IStringCache? Cache { get; set; }
}
```

For more information on using keyed services with Razor components, see [ASP.NET Core Blazor dependency injection](../blazor/fundamentals/dependency-injection?view=aspnetcore-10.0#inject-keyed-services-into-components).

Example use in a SignalR hub (`Hubs/MyHub1.cs`) with primary constructor injection:

```
using Microsoft.AspNetCore.SignalR;

public class MyHub1([FromKeyedServices("cache2")] IStringCache cache) : Hub
{
    public void Method()
    {
        Console.WriteLine(cache.Get(4));
    }
}
```

Example use in a SignalR hub (`Hubs/MyHub2.cs`) with method injection:

```
using Microsoft.AspNetCore.SignalR;

public class MyHub2 : Hub
{
    public void Method([FromKeyedServices("cache2")] IStringCache cache)
    {
        Console.WriteLine(cache.Get(5));
    }
}
```

For more information on DI with SignalR hubs, see [Use hubs in ASP.NET Core SignalR](../signalr/hubs?view=aspnetcore-10.0#inject-services-into-a-hub).

Middleware supports keyed services in both the middleware's constructor and its `Invoke`/`InvokeAsync` method:

```
internal class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next,
        [FromKeyedServices("cache1")] IStringCache cache)
    {
        _next = next;

        Console.WriteLine(cache.Get(6));
    }

    public Task Invoke(HttpContext context,
        [FromKeyedServices("cache2")] IStringCache cache)
        {
            Console.WriteLine(cache.Get(7));

            return _next(context);
        } 
}
```

In the app processing pipeline of the `Program` file (.NET 6 or later) or the `Startup.Configure` method (.NET 5 or earlier):

```
app.UseMiddleware<MyMiddleware>();
```

For more information on creating middleware, see [Write custom ASP.NET Core middleware](middleware/write?view=aspnetcore-10.0).

## Constructor injection behavior

For more information on constructor injection behavior, see the following resources:

* [Constructor injection behavior](/en-us/dotnet/core/extensions/dependency-injection/overview#constructor-injection-behavior)
* [ASP.NET Core Blazor dependency injection](../blazor/fundamentals/dependency-injection?view=aspnetcore-10.0#constructor-injection)

## Entity Framework contexts

For guidance on EF Core in server-side Blazor apps, see [ASP.NET Core Blazor with Entity Framework Core (EF Core)](../blazor/blazor-ef-core?view=aspnetcore-10.0).

By default, Entity Framework contexts are added to the service container using the [scoped lifetime](#service-lifetimes) because web app database operations are normally scoped to the client request. To use a different lifetime, specify the lifetime by using an [AddDbContext](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkservicecollectionextensions.adddbcontext) overload. Services of a given lifetime shouldn't use a database context with a lifetime that's shorter than the service's lifetime.

## Lifetime and registration options

To demonstrate the difference between service lifetimes and their registration options, consider the following interfaces that represent a task as an operation with an identifier, `OperationId`. Depending on how the lifetime of an operation's service is configured for the following interfaces, the container provides either the same or different instances of the service when requested by a class.

`IOperation.cs`:

```
public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
```

The following `Operation` class implements all of the preceding interfaces. The `Operation` constructor generates a GUID and stores the last four characters in the `OperationId` property.

`Operation.cs`:

```
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}
```

The following code creates multiple registrations of the `Operation` class according to the named lifetimes.

Where services are registered:

```
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
```

```
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
```

The following example demonstrates object lifetimes both within and between requests. The `Operation` component and the middleware request each kind of `IOperation` type and log the `OperationId` for each.

`Pages/OperationExample.razor`:

```
@page "/operation-example"
@inject IOperationTransient TransientOperation
@inject IOperationScoped ScopedOperation
@inject IOperationSingleton SingletonOperation

<ul>
    <li>Transient: @TransientOperation.OperationId</li>
    <li>Scoped: @ScopedOperation.OperationId</li>
    <li>Singleton: @SingletonOperation.OperationId</li>
</ul>
```

The following example demonstrates object lifetimes both within and between requests. The `IndexModel` and the middleware request each kind of `IOperation` type and log the `OperationId` for each.

`IndexModel.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationScoped _scopedOperation;
    private readonly IOperationSingleton _singletonOperation;

    public IndexModel(ILogger<IndexModel> logger,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void OnGet()
    {
        _logger.LogInformation($"Transient: {_transientOperation.OperationId}");
        _logger.LogInformation($"Scoped: {_scopedOperation.OperationId}");
        _logger.LogInformation($"Singleton: {_singletonOperation.OperationId}");
    }
}
```

Middleware can also resolve and use the same services. Scoped and transient services must be resolved in the `InvokeAsync` method.

`MyMiddleware.cs`:

```
public class MyMiddleware(ILogger<IndexModel> logger,
    IOperationSingleton singletonOperation)
{
    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, IOperationScoped scopedOperation)
    {
        logger.LogInformation($"Transient: {transientOperation.OperationId}");
        logger.LogInformation($"Scoped: {scopedOperation.OperationId}");
        logger.LogInformation($"Singleton: {singletonOperation.OperationId}");

        await _next(context);
    }
}
```

```
public class MyMiddleware
{
    private readonly ILogger<IndexModel> _logger;
    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(ILogger<IndexModel> logger,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _singletonOperation = singletonOperation;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, IOperationScoped scopedOperation)
    {
        _logger.LogInformation($"Transient: {transientOperation.OperationId}");
        _logger.LogInformation($"Scoped: {scopedOperation.OperationId}");
        _logger.LogInformation($"Singleton: {_singletonOperation.OperationId}");

        await _next(context);
    }
}
```

In the app processing pipeline of the `Program` file (.NET 6 or later) or the `Startup.Configure` method (.NET 5 or earlier):

```
app.UseMiddleware<MyMiddleware>();
```

For more information on creating middleware, see [Write custom ASP.NET Core middleware](middleware/write?view=aspnetcore-10.0).

Output from the preceding examples shows:

* *Transient* objects are always different. The transient `OperationId` value is different for the Razor component and in the middleware.
* *Scoped* objects are the same for a given request but differ across new Blazor circuits.
* *Singleton* objects are the same for every request or Blazor circuit.

- *Transient* objects are always different. The transient `OperationId` value is different for the page and in the middleware.
- *Scoped* objects are the same for a given request but differ across new requests.
- *Singleton* objects are the same for every request.

## Resolve a service at app startup

The following code shows how to resolve a scoped service for a limited duration when the app starts:

```
var app = builder.Build();

using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;
    var dependency = services.GetRequiredService<IMyDependency>();
    dependency.WriteMessage("Call services from main");
}
```

## Scope validation

For guidance on scope validation, see the following resources:

* [.NET dependency injection: Scope validation](/en-us/dotnet/core/extensions/dependency-injection/overview#scope-validation)
* [ASP.NET Core Web Host: Scope validation](host/web-host?view=aspnetcore-10.0#scope-validation)

## Request Services

Services and their dependencies within an ASP.NET Core request are exposed through [HttpContext.RequestServices](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestservices#microsoft-aspnetcore-http-httpcontext-requestservices).

The framework creates a scope per request, and `RequestServices` exposes the scoped service provider. All scoped services are valid for as long as the request is active.

Note

Prefer requesting dependencies as constructor parameters over resolving services from `RequestServices`. Requesting dependencies as constructor parameters yields classes that are easier to test.

## Design services for dependency injection

When designing services for DI:

* Avoid stateful, static classes and members. Avoid creating global state by designing apps to use singleton services instead.
* Avoid direct instantiation of dependent classes within services. Direct instantiation couples the code to a particular implementation.
* Make services small, well-factored, and easily tested.

If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the [Single Responsibility Principle (SRP)](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#single-responsibility). Attempt to refactor the class by moving some of its responsibilities into new classes. Keep in mind that Razor Pages page model classes and MVC controller classes should focus on UI concerns.

### Disposal of services

The container calls [Dispose](/en-us/dotnet/api/system.idisposable.dispose) for the [IDisposable](/en-us/dotnet/api/system.idisposable) types it creates. Services resolved from the container should never be disposed by the developer. If a type or factory is registered as a singleton, the container disposes the singleton automatically.

In the following example, the services are created by the service container and disposed automatically.

`Services/Service1.cs`:

```
public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        Console.WriteLine("Service1.Dispose");
        _disposed = true;

        GC.SuppressFinalize(this);
    }
}
```

`Services/Service2.cs`:

```
public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        Console.WriteLine("Service2.Dispose");
        _disposed = true;

        GC.SuppressFinalize(this);
    }
}
```

`Services/Service3.cs`:

```
public interface IService3
{
    public void Write(string message);
}

public class Service3(string myKey) : IService3, IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, Key = {myKey}");
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        Console.WriteLine("Service3.Dispose");
        _disposed = true;

        GC.SuppressFinalize(this);
    }
}
```

In `appsettings.Development.json`:

```
"Key": "Value from appsettings.Development.json"
```

Where services are registered by the app:

```
builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var key = builder.Configuration["Key"] ?? string.Empty;
builder.Services.AddSingleton<IService3>(sp => new Service3(key));
```

```
services.AddScoped<Service1>();
services.AddSingleton<Service2>();

var myKey = builder.Configuration["Key"] ?? string.Empty;
services.AddSingleton<IService3>(sp => new Service3(myKey));
```

`Pages/DisposalExample.razor`:

```
@page "/disposal-example"
@inject Service1 Service1
@inject Service2 Service2
@inject IService3 Service3

@code {
    protected override void OnInitialized()
    {
        Service1.Write("DisposalExample.OnInitialized");
        Service2.Write("DisposalExample.OnInitialized");
        Service3.Write("DisposalExample.OnInitialized");
    }
}
```

The debug console shows the following output after each refresh of the Index page:

```
Service1: DisposalExample.OnInitialized
Service2: DisposalExample.OnInitialized
Service3: DisposalExample.OnInitialized, Key = Value from appsettings.Development.json
Service1.Dispose
```

To see the entry for the disposal of `Service1`, navigate away from the `DisposalExample` component to trigger its disposal.

`Pages/Index.cshtml.cs`:

```
public class IndexModel(
    Service1 service1, Service2 service2, IService3 service3) 
    : PageModel
{
    public void OnGet()
    {
        service1.Write("IndexModel.OnGet");
        service2.Write("IndexModel.OnGet");
        service3.Write("IndexModel.OnGet");
    }
}
```

The debug console shows the following output after each refresh of the Index page:

```
Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, Key = Value from appsettings.Development.json
Service1.Dispose
```

### Services not created by the service container

Consider the following code:

```
builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());
```

```
services.AddSingleton(new Service1());
services.AddSingleton(new Service2());
```

For the preceding code:

* The service instances aren't created by the service container.
* The framework doesn't dispose of the services automatically.
* The developer is responsible for disposing of the services.

### `IDisposable` guidance for transient and shared instances

For more information, see [Dependency injection in .NET: IDisposable guidance for transient and shared instance](/en-us/dotnet/core/extensions/dependency-injection/guidelines#idisposable-guidance-for-transient-and-shared-instances).

## Default service container replacement

For more information, see [Dependency injection in .NET: Default service container replacement](/en-us/dotnet/core/extensions/dependency-injection/guidelines#default-service-container-replacement).

## Recommendations

For more information, see [Dependency injection guidelines: Recommendations](/en-us/dotnet/core/extensions/dependency-injection/guidelines#recommendations).

Avoid using the *service locator pattern*. For example, don't invoke [GetService](/en-us/dotnet/api/system.iserviceprovider.getservice) to obtain a service instance when you can use DI instead:

**Incorrect:**

**Correct**:

```
public class MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
{
    public void MyMethod()
    {
        var option = optionsMonitor.CurrentValue.Option;

        ...
    }
}
```

Another service locator variation to avoid is injecting a factory that resolves dependencies at runtime. Both of these practices mix [Inversion of Control](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#dependency-inversion) strategies.

Avoid static access to [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) (for example, [IHttpContextAccessor.HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor.httpcontext)).

DI is an *alternative* to static/global object access patterns. You might not be able to realize the benefits of DI if you mix it with static object access.

## Recommended patterns for multitenancy in dependency injection

[Orchard Core](https://github.com/OrchardCMS/OrchardCore) is an app framework for building modular, multitenant apps on ASP.NET Core. For more information, see the [Orchard Core Documentation](https://docs.orchardcore.net).

For examples of how to build modular and multitenant apps using just the Orchard Core Framework without its CMS-specific features, see the [Orchard Core samples](https://github.com/OrchardCMS/OrchardCore.Samples).

## Framework-provided services

The `Program` file (.NET 6 or later) or the `Startup` file (.NET 5 or earlier) registers services that the app uses, including platform features, such as Entity Framework Core and services to support Razor components in Blazor (.NET 8 or later). Initially, the `IServiceCollection` has services defined by the framework depending on [how the host was configured](./?view=aspnetcore-10.0#host). For apps based on the ASP.NET Core templates, the framework registers more than 250 services.

The following table describes a small sample of framework-registered services.

| Service type                                                                                                                                         | Lifetime  |
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| [Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory](/en-us/dotnet/api/microsoft.aspnetcore.hosting.builder.iapplicationbuilderfactory) | Transient |
| [IHostApplicationLifetime](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime)                                                  | Singleton |
| [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment)                                                            | Singleton |
| [Microsoft.AspNetCore.Hosting.IStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartup)                                                     | Singleton |
| [Microsoft.AspNetCore.Hosting.IStartupFilter](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter)                                         | Transient |
| [Microsoft.AspNetCore.Hosting.Server.IServer](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.iserver)                                         | Singleton |
| [Microsoft.AspNetCore.Http.IHttpContextFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextfactory)                                     | Transient |
| [Microsoft.Extensions.Logging.ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1)                                     | Singleton |
| [Microsoft.Extensions.Logging.ILoggerFactory](/en-us/dotnet/api/microsoft.extensions.logging.iloggerfactory)                                         | Singleton |
| [Microsoft.Extensions.ObjectPool.ObjectPoolProvider](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpoolprovider)                           | Singleton |
| [Microsoft.Extensions.Options.IConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigureoptions-1)                      | Transient |
| [Microsoft.Extensions.Options.IOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptions-1)                                        | Singleton |
| [System.Diagnostics.DiagnosticSource](/en-us/dotnet/api/system.diagnostics.diagnosticsource)                                                         | Singleton |
| [System.Diagnostics.DiagnosticListener](/en-us/dotnet/api/system.diagnostics.diagnosticlistener)                                                     | Singleton |

## Related content

* [ASP.NET Core Blazor dependency injection](../blazor/fundamentals/dependency-injection?view=aspnetcore-10.0)
* [Inject services into a SignalR hub](../signalr/hubs?view=aspnetcore-10.0#inject-services-into-a-hub)
* [Dependency injection into views in ASP.NET Core](../mvc/views/dependency-injection?view=aspnetcore-10.0)
* [Dependency injection into controllers in ASP.NET Core](../mvc/controllers/dependency-injection?view=aspnetcore-10.0)
* [Dependency injection in requirement handlers in ASP.NET Core](../security/authorization/dependencyinjection?view=aspnetcore-10.0)
* [NDC Conference Patterns for DI app development](https://www.youtube.com/watch?v=x-C-CNBVTaY)
* [App startup in ASP.NET Core](startup?view=aspnetcore-10.0)
* [Factory-based middleware activation in ASP.NET Core](middleware/extensibility?view=aspnetcore-10.0)
* [Understand dependency injection basics in .NET](/en-us/dotnet/core/extensions/dependency-injection/basics)
* [Dependency injection guidelines](/en-us/dotnet/core/extensions/dependency-injection/guidelines)
* [Tutorial: Use dependency injection in .NET](/en-us/dotnet/core/extensions/dependency-injection/usage)
* [.NET dependency injection](/en-us/dotnet/core/extensions/dependency-injection/overview)
* [ASP.NET Core Dependency Injection: What is the IServiceCollection?](https://www.stevejgordon.co.uk/aspnet-core-dependency-injection-what-is-the-iservicecollection)
* [Four ways to dispose IDisposables in ASP.NET Core](https://andrewlock.net/four-ways-to-dispose-idisposables-in-asp-net-core/)
* [Writing Clean Code in ASP.NET Core with Dependency Injection (MSDN)](/en-us/archive/msdn-magazine/2016/may/asp-net-writing-clean-code-in-asp-net-core-with-dependency-injection)
* [Explicit Dependencies Principle](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#explicit-dependencies)
* [Inversion of Control Containers and the Dependency Injection Pattern (Martin Fowler)](https://www.martinfowler.com/articles/injection.html)
* [How to register a service with multiple interfaces in ASP.NET Core DI (Andrew Lock)](https://andrewlock.net/how-to-register-a-service-with-multiple-interfaces-for-in-asp-net-core-di/)
* [Avoiding Startup service injection in ASP.NET Core 3 (Andrew Lock)](https://andrewlock.net/avoiding-startup-service-injection-in-asp-net-core-3/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/openapi/overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# OpenAPI support in ASP.NET Core API apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

ASP.NET Core supports the generation of OpenAPI documents in controller-based and Minimal API apps. The [OpenAPI specification](https://spec.openapis.org/oas/latest.html) is a programming language-agnostic standard for documenting HTTP APIs. ASP.NET Core apps support this standard through a combination of built-in APIs and open-source libraries. There are three key aspects to OpenAPI integration in an application:

* Generating information about the endpoints in the app.
* Gathering the information into a format that matches the OpenAPI schema.
* Exposing the generated OpenAPI document through a visual UI or a serialized file.

ASP.NET Core provides first-party support for generating information about endpoints in an app through the `Microsoft.AspNetCore.OpenApi` package.

The ASP.NET Core Web API template generates the following code that uses OpenAPI:

```
using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateTime.Now.AddDays(index),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

In the preceding highlighted code:

* `AddOpenApi` registers services required for OpenAPI document generation into the application's DI container.
* `MapOpenApi` adds an endpoint into the application for viewing the OpenAPI document serialized into JSON. The OpenAPI endpoint is restricted to the `Development` environment to minimize the risk of exposing sensitive information and reduce the vulnerabilities in production.

[]()

## `Microsoft.AspNetCore.OpenApi` NuGet package

The [`Microsoft.AspNetCore.OpenApi`](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi/) package provides the following features:

* Support for generating OpenAPI documents at runtime and accessing them through an endpoint on the application.
* Support for "transformer" APIs that modify the generated document.

To use the `Microsoft.AspNetCore.OpenApi` package, add it as a PackageReference to a project file:

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

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

  <PropertyGroup>
    <OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
    <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.*-*" />
    <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.*-*">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>
```

To learn more about the `Microsoft.AspNetCore.OpenApi` package, see [Generate OpenAPI documents](aspnetcore-openapi?view=aspnetcore-10.0).

## `Microsoft.Extensions.ApiDescription.Server` NuGet package

The [`Microsoft.Extensions.ApiDescription.Server`](https://www.nuget.org/packages/Microsoft.Extensions.ApiDescription.Server/) package supports generating OpenAPI documents at build time and serializing them.

To use `Microsoft.Extensions.ApiDescription.Server`, add it as a PackageReference to a project file. Enable document generation at build time by setting the `OpenApiGenerateDocumentsOnBuild` property to `true`. By default, the generated OpenAPI document is saved to the `obj` directory, but you can customize the output directory by setting the `OpenApiDocumentsDirectory` property.

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

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

  <PropertyGroup>
    <OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
    <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.*-*" />
    <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.*-*">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>
```

## API v. API operation v. API endpoint

The following sections explain the differences between an API, an API endpoint, and an API operation in the context of ASP.NET Core and OpenAPI documentation.

### API (Application Programming Interface)

An API is a set of rules and protocols for building and interacting with software applications. It defines how different software components should communicate. In general web development, "API" typically refers to a web service that exposes functionality over HTTP.

In ASP.NET Core, an API is usually built using controllers or Minimal APIs, which handle incoming HTTP requests and return responses.

ASP.NET Core's internal naming conventions sometimes use "API" differently. For instance, in [API Explorer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer), an "ApiDescription" actually represents an [API Operation](#api-operation) rather than the full API service. This distinction reflects internal naming conventions and sometimes differ from the broader definition used here.

See [Introduction to the ApiExplorer in ASP.NET Core](https://andrewlock.net/introduction-to-the-apiexplorer-in-asp-net-core/) for more information on API Explorer.

### API Operation

An API operation represents a specific action or capability that an API provides. In ASP.NET Core, this corresponds to:

* Controller action methods in MVC-style APIs
* Route handlers in Minimal APIs

Each operation is defined by its HTTP method (`GET`, `POST`, `PUT`, etc.), path, parameters, and responses.

### API Endpoint

An API endpoint is a specific URL:

* That represents a specific resource or functionality exposed by the API.
* Provides the exact address that a client needs to send an HTTP request to in order to interact with a particular API operation.

An endpoint is a combination of the API's base URL and a specific path to the desired resource, along with the supported HTTP methods:

* For controller-based APIs, endpoints combine the route template with controller and action.
* For Minimal APIs, endpoints are explicitly defined with `app.MapGet()`, `app.MapPost()`, etc.

For example, the `api/products/{id}` endpoint that supports the following operations:

* `GET /api/products/{id}`
* `PUT /api/products/{id}`
* `PATCH /api/products/{id}`
* `Delete /api/products/{id}`
* `HEAD /api/products/{id}`

Endpoints often include query parameters, for example, `GET /api/products?category=electronics&sort=price`

### OpenAPI Documentation

In the context of OpenAPI, the documentation describes the API as a whole, including all its endpoints and operations. OpenAPI provides a structured way to document APIs, making it easier for developers to understand how to interact with them.

API Operations are the primary focus of OpenAPI documentation. The [OpenAPI specification](https://spec.openapis.org/oas/latest.html) organizes documentation by operations, which are grouped by paths (endpoints). Each operation is described with details such as parameters, request bodies, responses, and more. This structured format allows tools to generate client libraries, server stubs, and interactive documentation automatically.

In an OpenAPI document:

* The entire document describes the API as a whole
* Each path item (like `/api/products/{id}`) represents an endpoint
* Under each path, the HTTP methods (`GET`, `POST`, `PUT`, etc.) define the operations
* Each operation contains details about parameters, request body, responses, etc.

Example in OpenAPI JSON format:

```
json{
  "paths": {
    "/api/products/{id}": {  // This is the endpoint
      "get": {  // This is the operation
        "summary": "Get a product by ID",
        "parameters": [...],
        "responses": {...}
      },
      "put": {  // Another operation on the same endpoint
        "summary": "Update a product",
        "parameters": [...],
        "responses": {...}
      }
    }
  }
}
```

## API, API operation, and API endpoint comparison

The following table summarizes the differences between an API, an API operation, and an API endpoint.

| Concept             | API Operation                                                      | API Endpoint                                                                                                               |
| ------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| **Definition**      | A logical description of an API action: method + path + behavior   | The actual configured HTTP route that listens for requests                                                                 |
| **Level**           | Conceptual, what action can happen                                 | Concrete, what URL and method are matched                                                                                  |
| **Tied to**         | OpenAPI API design/specification                                   | ASP.NET Core routing at runtime                                                                                            |
| **Describes**       | What the API does for example, "create product"                    | Where and how to call it, for example, `POST https://localhost:7099/api/products`, `POST https://contoso.com/api/products` |
| **In ASP.NET Core** | Controller actions or Minimal API methods, before routing resolves | Endpoint objects resolved at runtime                                                                                       |

## ASP.NET Core OpenAPI source code on GitHub

* [AddOpenApi](https://github.com/dotnet/aspnetcore/blob/main/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs)
* [OpenApiDocumentService](https://github.com/dotnet/aspnetcore/blob/main/src/OpenApi/src/Services/OpenApiDocumentService.cs)
* [OpenApiOptions](https://github.com/dotnet/aspnetcore/blob/main/src/OpenApi/src/Services/OpenApiOptions.cs)

## Related content

* [Authentication and authorization in Minimal APIs](../minimal-apis/security?view=aspnetcore-10.0)

The [OpenAPI specification](https://spec.openapis.org/oas/latest.html) is a programming language-agnostic standard for documenting HTTP APIs. This standard is supported in Minimal APIs through a combination of built-in APIs and open-source libraries. There are three key aspects to OpenAPI integration in an application:

* Generating information about the endpoints in the app.
* Gathering the information into a format that matches the OpenAPI schema.
* Exposing the generated OpenAPI schema via a visual UI or a serialized file.

Minimal APIs provide built-in support for generating information about endpoints in an app via the `Microsoft.AspNetCore.OpenApi` package. Exposing the generated OpenAPI definition via a visual UI requires a third-party package.

For information about support for OpenAPI in controller-based APIs, see the [.NET 9 version of this article](?view=aspnetcore-9.0\&preserve-view=true).

ASP.NET Core supports the generation of OpenAPI documents in controller-based and Minimal API apps. The [OpenAPI specification](https://spec.openapis.org/oas/latest.html) is a programming language-agnostic standard for documenting HTTP APIs. This standard is supported in ASP.NET Core apps through a combination of built-in APIs and open-source libraries. There are three key aspects to OpenAPI integration in an application:

* Generating information about the endpoints in the app.
* Gathering the information into a format that matches the OpenAPI schema.
* Exposing the generated OpenAPI document via a visual UI or a serialized file.

ASP.NET Core provides first-party support for generating information about endpoints in an app through the `Microsoft.AspNetCore.OpenApi` package.

The following code is generated by the ASP.NET Core Web API template and uses OpenAPI:

```
using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateTime.Now.AddDays(index),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

In the preceding highlighted code:

* `AddOpenApi` registers services required for OpenAPI document generation into the application's DI container.
* `MapOpenApi` adds an endpoint into the application for viewing the OpenAPI document serialized into JSON. The OpenAPI endpoint is restricted to the `Development` environment to minimize the risk of exposing sensitive information and reduce the vulnerabilities in production.

[]()

## `Microsoft.AspNetCore.OpenApi` NuGet package

The [`Microsoft.AspNetCore.OpenApi`](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi/) package provides the following features:

* Support for generating OpenAPI documents at run time and accessing them via an endpoint on the application.
* Support for "transformer" APIs that modify the generated document.

To use the `Microsoft.AspNetCore.OpenApi` package, add it as a PackageReference to a project file:

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

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <PropertyGroup>
    <OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
    <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.*-*" />
    <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.*-*">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>
```

To learn more about the `Microsoft.AspNetCore.OpenApi` package, see [Generate OpenAPI documents](aspnetcore-openapi?view=aspnetcore-10.0).

## `Microsoft.Extensions.ApiDescription.Server` NuGet package

The [`Microsoft.Extensions.ApiDescription.Server`](https://www.nuget.org/packages/Microsoft.Extensions.ApiDescription.Server/) package provides support for generating OpenAPI documents at build time and serializing them.

To use `Microsoft.Extensions.ApiDescription.Server`, add it as a PackageReference to a project file. Document generation at build time is enabled by setting the `OpenApiGenerateDocumentsOnBuild` property to `true`. By default, the generated OpenAPI document is saved to the `obj` directory, but you can customize the output directory by setting the `OpenApiDocumentsDirectory` property.

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

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <PropertyGroup>
    <OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
    <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.*-*" />
    <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.*-*">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>
```

## API v. API operation v. API endpoint

The following sections explain the differences between an API, an API endpoint, and an API operation in the context of ASP.NET Core and OpenAPI documentation.

### API (Application Programming Interface)

An API is a set of rules and protocols for building and interacting with software applications. It defines how different software components should communicate. In general web development, "API" typically refers to a web service that exposes functionality over HTTP.

In ASP.NET Core, an API is usually built using controllers or Minimal APIs, which handle incoming HTTP requests and return responses.

ASP.NET Core's internal naming conventions sometimes use "API" differently. For instance, in [API Explorer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorer), an "ApiDescription" actually represents an [API Operation](#api-operation) rather than the full API service. This distinction reflects internal naming conventions and sometimes differ from the broader definition used here.

See [Introduction to the ApiExplorer in ASP.NET Core](https://andrewlock.net/introduction-to-the-apiexplorer-in-asp-net-core/) for more information on API Explorer.

### API Operation

An API operation represents a specific action or capability that an API provides. In ASP.NET Core, this corresponds to:

* Controller action methods in MVC-style APIs
* Route handlers in Minimal APIs

Each operation is defined by its HTTP method (`GET`, `POST`, `PUT`, etc.), path, parameters, and responses.

### API Endpoint

An API endpoint is a specific URL:

* That represents a specific resource or functionality exposed by the API.
* Provides the exact address that a client needs to send an HTTP request to in order to interact with a particular API operation.

An endpoint is a combination of the API's base URL and a specific path to the desired resource, along with the supported HTTP methods:

* For controller-based APIs, endpoints combine the route template with controller and action.
* For Minimal APIs, endpoints are explicitly defined with `app.MapGet()`, `app.MapPost()`, etc.

For example, the `api/products/{id}` endpoint that supports the following operations:

* `GET /api/products/{id}`
* `PUT /api/products/{id}`
* `PATCH /api/products/{id}`
* `Delete /api/products/{id}`
* `HEAD /api/products/{id}`

Endpoints often include query parameters, for example, `GET /api/products?category=electronics&sort=price`

### OpenAPI Documentation

In the context of OpenAPI, the documentation describes the API as a whole, including all its endpoints and operations. OpenAPI provides a structured way to document APIs, making it easier for developers to understand how to interact with them.

API Operations are the primary focus of OpenAPI documentation. The [OpenAPI specification](https://spec.openapis.org/oas/latest.html) organizes documentation by operations, which are grouped by paths (endpoints). Each operation is described with details such as parameters, request bodies, responses, and more. This structured format allows tools to generate client libraries, server stubs, and interactive documentation automatically.

In an OpenAPI document:

* The entire document describes the API as a whole
* Each path item (like `/api/products/{id}`) represents an endpoint
* Under each path, the HTTP methods (`GET`, `POST`, `PUT`, etc.) define the operations
* Each operation contains details about parameters, request body, responses, etc.

Example in OpenAPI JSON format:

```
json{
  "paths": {
    "/api/products/{id}": {  // This is the endpoint
      "get": {  // This is the operation
        "summary": "Get a product by ID",
        "parameters": [...],
        "responses": {...}
      },
      "put": {  // Another operation on the same endpoint
        "summary": "Update a product",
        "parameters": [...],
        "responses": {...}
      }
    }
  }
}
```

## API, API operation, and API endpoint comparison

The following table summarizes the differences between an API, an API operation, and an API endpoint.

| Concept             | API Operation                                                      | API Endpoint                                                                                                               |
| ------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| **Definition**      | A logical description of an API action: method + path + behavior   | The actual configured HTTP route that listens for requests                                                                 |
| **Level**           | Conceptual, what action can happen                                 | Concrete, what URL and method are matched                                                                                  |
| **Tied to**         | OpenAPI API design/specification                                   | ASP.NET Core routing at runtime                                                                                            |
| **Describes**       | What the API does for example, "create product"                    | Where and how to call it, for example, `POST https://localhost:7099/api/products`, `POST https://contoso.com/api/products` |
| **In ASP.NET Core** | Controller actions or Minimal API methods, before routing resolves | Endpoint objects resolved at runtime                                                                                       |

## ASP.NET Core OpenAPI source code on GitHub

* [AddOpenApi](https://github.com/dotnet/aspnetcore/blob/main/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs)
* [OpenApiDocumentService](https://github.com/dotnet/aspnetcore/blob/main/src/OpenApi/src/Services/OpenApiDocumentService.cs)
* [OpenApiOptions](https://github.com/dotnet/aspnetcore/blob/main/src/OpenApi/src/Services/OpenApiOptions.cs)

## Related content

* [Authentication and authorization in Minimal APIs](../minimal-apis/security?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/file-providers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# File Providers in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Steve Smith](https://ardalis.com/)

ASP.NET Core abstracts file system access through the use of File Providers. File Providers are used throughout the ASP.NET Core framework. For example:

* [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) exposes the app's [content root](./?view=aspnetcore-10.0#content-root) and [web root](./?view=aspnetcore-10.0#web-root) as `IFileProvider` types.
* [Static file middleware](static-files?view=aspnetcore-10.0) uses File Providers to locate static files.
* [Razor](../mvc/views/razor?view=aspnetcore-10.0) uses File Providers to locate pages and views.
* .NET tooling uses File Providers and glob patterns to specify which files should be published.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/file-providers/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## File Provider interfaces

The primary interface is [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider). `IFileProvider` exposes methods to:

* Obtain file information ([IFileInfo](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo)).
* Obtain directory information ([IDirectoryContents](/en-us/dotnet/api/microsoft.extensions.fileproviders.idirectorycontents)).
* Set up change notifications (using an [IChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken)).

`IFileInfo` provides methods and properties for working with files:

* [Exists](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.exists#microsoft-extensions-fileproviders-ifileinfo-exists)
* [IsDirectory](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.isdirectory#microsoft-extensions-fileproviders-ifileinfo-isdirectory)
* [Name](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.name#microsoft-extensions-fileproviders-ifileinfo-name)
* [Length](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.length#microsoft-extensions-fileproviders-ifileinfo-length) (in bytes)
* [LastModified](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.lastmodified#microsoft-extensions-fileproviders-ifileinfo-lastmodified) date

You can read from the file using the [IFileInfo.CreateReadStream](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.createreadstream) method.

The `FileProviderSample` sample app demonstrates how to configure a File Provider in `Startup.ConfigureServices` for use throughout the app via [dependency injection](dependency-injection?view=aspnetcore-10.0).

## File Provider implementations

The following table lists implementations of `IFileProvider`.

| Implementation                                                      | Description                                                                                |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| [Composite File Provider](#composite-file-provider)                 | Used to provide combined access to files and directories from one or more other providers. |
| [Manifest Embedded File Provider](#manifest-embedded-file-provider) | Used to access files embedded in assemblies.                                               |
| [Physical File Provider](#physical-file-provider)                   | Used to access the system's physical files.                                                |

### Physical File Provider

The [PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider) provides access to the physical file system. `PhysicalFileProvider` uses the [System.IO.File](/en-us/dotnet/api/system.io.file) type (for the physical provider) and scopes all paths to a directory and its children. This scoping prevents access to the file system outside of the specified directory and its children. The most common scenario for creating and using a `PhysicalFileProvider` is to request an `IFileProvider` in a constructor through [dependency injection](dependency-injection?view=aspnetcore-10.0).

Warning

`PhysicalFileProvider` scopes access to its root directory and child paths, but this doesn't guarantee a security sandbox. Symbolic links under the root can still expose files outside the root directory.

When instantiating this provider directly, an absolute directory path is required and serves as the base path for all requests made using the provider. Glob patterns aren't supported in the directory path.

The following code shows how to use `PhysicalFileProvider` to obtain directory contents and file information:

```
var provider = new PhysicalFileProvider(applicationRoot);
var contents = provider.GetDirectoryContents(string.Empty);
var filePath = Path.Combine("wwwroot", "js", "site.js");
var fileInfo = provider.GetFileInfo(filePath);
```

Types in the preceding example:

* `provider` is an `IFileProvider`.
* `contents` is an `IDirectoryContents`.
* `fileInfo` is an `IFileInfo`.

The File Provider can be used to iterate through the directory specified by `applicationRoot` or call `GetFileInfo` to obtain a file's information. Glob patterns can't be passed to the `GetFileInfo` method. The File Provider has no access outside of the `applicationRoot` directory.

The `FileProviderSample` sample app creates the provider in the `Startup.ConfigureServices` method using [IHostEnvironment.ContentRootFileProvider](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootfileprovider#microsoft-extensions-hosting-ihostenvironment-contentrootfileprovider):

```
var physicalProvider = _env.ContentRootFileProvider;
```

### Manifest Embedded File Provider

The [ManifestEmbeddedFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.manifestembeddedfileprovider) is used to access files embedded within assemblies. The `ManifestEmbeddedFileProvider` uses a manifest compiled into the assembly to reconstruct the original paths of the embedded files.

To generate a manifest of the embedded files:

1. Add the [`Microsoft.Extensions.FileProviders.Embedded`](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet package to your project.

2. Set the `<GenerateEmbeddedFilesManifest>` property to `true`. Specify the files to embed with [`<EmbeddedResource>`](/en-us/dotnet/core/tools/csproj#default-compilation-includes-in-net-core-projects):

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
       <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="3.1.0" />
     </ItemGroup>

     <ItemGroup>
       <EmbeddedResource Include="Resource.txt" />
     </ItemGroup>

   </Project>
   ```

Use [glob patterns](#glob-patterns) to specify one or more files to embed into the assembly.

The `FileProviderSample` sample app creates an `ManifestEmbeddedFileProvider` and passes the currently executing assembly to its constructor.

`Startup.cs`:

```
var manifestEmbeddedProvider = 
    new ManifestEmbeddedFileProvider(typeof(Program).Assembly);
```

Additional overloads allow you to:

* Specify a relative file path.
* Scope files to a last modified date.
* Name the embedded resource containing the embedded file manifest.

| Overload                                                                 | Description                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ManifestEmbeddedFileProvider(Assembly, String)`                         | Accepts an optional `root` relative path parameter. Specify the `root` to scope calls to [GetDirectoryContents](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider.getdirectorycontents) to those resources under the provided path.                                                                                                                                                             |
| `ManifestEmbeddedFileProvider(Assembly, String, DateTimeOffset)`         | Accepts an optional `root` relative path parameter and a `lastModified` date ([DateTimeOffset](/en-us/dotnet/api/system.datetimeoffset)) parameter. The `lastModified` date scopes the last modification date for the [IFileInfo](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo) instances returned by the [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider). |
| `ManifestEmbeddedFileProvider(Assembly, String, String, DateTimeOffset)` | Accepts an optional `root` relative path, `lastModified` date, and `manifestName` parameters. The `manifestName` represents the name of the embedded resource containing the manifest.                                                                                                                                                                                                                           |

### Composite File Provider

The [CompositeFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.compositefileprovider) combines `IFileProvider` instances, exposing a single interface for working with files from multiple providers. When creating the `CompositeFileProvider`, pass one or more `IFileProvider` instances to its constructor.

In the `FileProviderSample` sample app, a `PhysicalFileProvider` and a `ManifestEmbeddedFileProvider` provide files to a `CompositeFileProvider` registered in the app's service container. The following code is found in the project's `Startup.ConfigureServices` method:

```
var physicalProvider = _env.ContentRootFileProvider;
var manifestEmbeddedProvider = 
    new ManifestEmbeddedFileProvider(typeof(Program).Assembly);
var compositeProvider = 
    new CompositeFileProvider(physicalProvider, manifestEmbeddedProvider);

services.AddSingleton<IFileProvider>(compositeProvider);
```

## Watch for changes

The [IFileProvider.Watch](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider.watch) method provides a scenario to watch one or more files or directories for changes. The `Watch` method:

* Accepts a file path string, which can use [glob patterns](#glob-patterns) to specify multiple files.
* Returns an [IChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken).

The resulting change token exposes:

* [HasChanged](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.haschanged#microsoft-extensions-primitives-ichangetoken-haschanged): A property that can be inspected to determine if a change has occurred.
* [RegisterChangeCallback](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.registerchangecallback): Called when changes are detected to the specified path string. Each change token only calls its associated callback in response to a single change. To enable constant monitoring, use a [TaskCompletionSource\<TResult>](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) (shown below) or recreate `IChangeToken` instances in response to changes.

The `WatchConsole` sample app writes a message whenever a `.txt` file in the `TextFiles` directory is modified:

```
private static readonly string _fileFilter = Path.Combine("TextFiles", "*.txt");

public static void Main(string[] args)
{
    Console.WriteLine($"Monitoring for changes with filter '{_fileFilter}' (Ctrl + C to quit)...");

    while (true)
    {
        MainAsync().GetAwaiter().GetResult();
    }
}

private static async Task MainAsync()
{
    var fileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory());
    IChangeToken token = fileProvider.Watch(_fileFilter);
    var tcs = new TaskCompletionSource<object>();

    token.RegisterChangeCallback(state =>
        ((TaskCompletionSource<object>)state).TrySetResult(null), tcs);

    await tcs.Task.ConfigureAwait(false);

    Console.WriteLine("file changed");
}
```

Some file systems, such as Docker containers and network shares, may not reliably send change notifications. Set the `DOTNET_USE_POLLING_FILE_WATCHER` environment variable to `1` or `true` to poll the file system for changes every four seconds (not configurable).

### Glob patterns

File system paths use wildcard patterns called *glob (or globbing) patterns*. Specify groups of files with these patterns. The two wildcard characters are `*` and `**`:

**`*`**\
Matches anything at the current folder level, any filename, or any file extension. Matches are terminated by `/` and `.` characters in the file path.

**`**`**\
Matches anything across multiple directory levels. Can be used to recursively match many files within a directory hierarchy.

The following table provides common examples of glob patterns.

| Pattern                        | Description                                                                                         |
| ------------------------------ | --------------------------------------------------------------------------------------------------- |
| `directory/file.txt`           | Matches a specific file in a specific directory.                                                    |
| `directory/*.txt`              | Matches all files with `.txt` extension in a specific directory.                                    |
| `directory/*/appsettings.json` | Matches all `appsettings.json` files in directories exactly one level below the `directory` folder. |
| `directory/**/*.txt`           | Matches all files with a `.txt` extension found anywhere under the `directory` folder.              |

ASP.NET Core abstracts file system access through the use of File Providers. File Providers are used throughout the ASP.NET Core framework:

* [IHostingEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostingenvironment) exposes the app's [content root](./?view=aspnetcore-10.0#content-root) and [web root](./?view=aspnetcore-10.0#web-root) as `IFileProvider` types.
* [Static file middleware](static-files?view=aspnetcore-10.0) uses File Providers to locate static files.
* [Razor](../mvc/views/razor?view=aspnetcore-10.0) uses File Providers to locate pages and views.
* .NET tooling uses File Providers and glob patterns to specify which files should be published.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/file-providers/samples) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## File Provider interfaces

The primary interface is [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider). `IFileProvider` exposes methods to:

* Obtain file information ([IFileInfo](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo)).
* Obtain directory information ([IDirectoryContents](/en-us/dotnet/api/microsoft.extensions.fileproviders.idirectorycontents)).
* Set up change notifications (using an [IChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken)).

`IFileInfo` provides methods and properties for working with files:

* [Exists](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.exists#microsoft-extensions-fileproviders-ifileinfo-exists)
* [IsDirectory](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.isdirectory#microsoft-extensions-fileproviders-ifileinfo-isdirectory)
* [Name](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.name#microsoft-extensions-fileproviders-ifileinfo-name)
* [Length](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.length#microsoft-extensions-fileproviders-ifileinfo-length) (in bytes)
* [LastModified](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.lastmodified#microsoft-extensions-fileproviders-ifileinfo-lastmodified) date

You can read from the file using the [IFileInfo.CreateReadStream](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.createreadstream) method.

The sample app demonstrates how to configure a File Provider in `Startup.ConfigureServices` for use throughout the app via [dependency injection](dependency-injection?view=aspnetcore-10.0).

## File Provider implementations

Three implementations of `IFileProvider` are available.

| Implementation                                                | Description                                                                                                          |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| [PhysicalFileProvider](#physicalfileprovider)                 | The physical provider is used to access the system's physical files.                                                 |
| [ManifestEmbeddedFileProvider](#manifestembeddedfileprovider) | The manifest embedded provider is used to access files embedded in assemblies.                                       |
| [CompositeFileProvider](#compositefileprovider)               | The composite provider is used to provide combined access to files and directories from one or more other providers. |

### PhysicalFileProvider

The [PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider) provides access to the physical file system. `PhysicalFileProvider` uses the [System.IO.File](/en-us/dotnet/api/system.io.file) type (for the physical provider) and scopes all paths to a directory and its children. This scoping prevents access to the file system outside of the specified directory and its children. The most common scenario for creating and using a `PhysicalFileProvider` is to request an `IFileProvider` in a constructor through [dependency injection](dependency-injection?view=aspnetcore-10.0).

When instantiating this provider directly, a directory path is required and serves as the base path for all requests made using the provider.

The following code shows how to create a `PhysicalFileProvider` and use it to obtain directory contents and file information:

```
var provider = new PhysicalFileProvider(applicationRoot);
var contents = provider.GetDirectoryContents(string.Empty);
var fileInfo = provider.GetFileInfo("wwwroot/js/site.js");
```

Types in the preceding example:

* `provider` is an `IFileProvider`.
* `contents` is an `IDirectoryContents`.
* `fileInfo` is an `IFileInfo`.

The File Provider can be used to iterate through the directory specified by `applicationRoot` or call `GetFileInfo` to obtain a file's information. The File Provider has no access outside of the `applicationRoot` directory.

The sample app creates the provider in the app's `Startup.ConfigureServices` class using [IHostingEnvironment.ContentRootFileProvider](/en-us/dotnet/api/microsoft.extensions.hosting.ihostingenvironment.contentrootfileprovider#microsoft-extensions-hosting-ihostingenvironment-contentrootfileprovider):

```
var physicalProvider = _env.ContentRootFileProvider;
```

### ManifestEmbeddedFileProvider

The [ManifestEmbeddedFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.manifestembeddedfileprovider) is used to access files embedded within assemblies. The `ManifestEmbeddedFileProvider` uses a manifest compiled into the assembly to reconstruct the original paths of the embedded files.

To generate a manifest of the embedded files, set the `<GenerateEmbeddedFilesManifest>` property to `true`. Specify the files to embed with [\<EmbeddedResource>](/en-us/dotnet/core/tools/csproj#default-compilation-includes-in-net-core-projects):

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

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
    <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="Resource.txt" />
  </ItemGroup>

</Project>
```

Use [glob patterns](#glob-patterns) to specify one or more files to embed into the assembly.

The sample app creates an `ManifestEmbeddedFileProvider` and passes the currently executing assembly to its constructor.

`Startup.cs`:

```
var manifestEmbeddedProvider = 
    new ManifestEmbeddedFileProvider(typeof(Program).Assembly);
```

Additional overloads allow you to:

* Specify a relative file path.
* Scope files to a last modified date.
* Name the embedded resource containing the embedded file manifest.

| Overload                                                                 | Description                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ManifestEmbeddedFileProvider(Assembly, String)`                         | Accepts an optional `root` relative path parameter. Specify the `root` to scope calls to [GetDirectoryContents](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider.getdirectorycontents) to those resources under the provided path.                                                                                                                                                             |
| `ManifestEmbeddedFileProvider(Assembly, String, DateTimeOffset)`         | Accepts an optional `root` relative path parameter and a `lastModified` date ([DateTimeOffset](/en-us/dotnet/api/system.datetimeoffset)) parameter. The `lastModified` date scopes the last modification date for the [IFileInfo](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileinfo) instances returned by the [IFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider). |
| `ManifestEmbeddedFileProvider(Assembly, String, String, DateTimeOffset)` | Accepts an optional `root` relative path, `lastModified` date, and `manifestName` parameters. The `manifestName` represents the name of the embedded resource containing the manifest.                                                                                                                                                                                                                           |

### CompositeFileProvider

The [CompositeFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.compositefileprovider) combines `IFileProvider` instances, exposing a single interface for working with files from multiple providers. When creating the `CompositeFileProvider`, pass one or more `IFileProvider` instances to its constructor.

In the sample app, a `PhysicalFileProvider` and a `ManifestEmbeddedFileProvider` provide files to a `CompositeFileProvider` registered in the app's service container:

```
var physicalProvider = _env.ContentRootFileProvider;
var manifestEmbeddedProvider = 
    new ManifestEmbeddedFileProvider(typeof(Program).Assembly);
var compositeProvider = 
    new CompositeFileProvider(physicalProvider, manifestEmbeddedProvider);

services.AddSingleton<IFileProvider>(compositeProvider);
```

## Watch for changes

The [IFileProvider.Watch](/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider.watch) method provides a scenario to watch one or more files or directories for changes. `Watch` accepts a path string, which can use [glob patterns](#glob-patterns) to specify multiple files. `Watch` returns an [IChangeToken](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken). The change token exposes:

* [HasChanged](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.haschanged#microsoft-extensions-primitives-ichangetoken-haschanged): A property that can be inspected to determine if a change has occurred.
* [RegisterChangeCallback](/en-us/dotnet/api/microsoft.extensions.primitives.ichangetoken.registerchangecallback): Called when changes are detected to the specified path string. Each change token only calls its associated callback in response to a single change. To enable constant monitoring, use a [TaskCompletionSource\<TResult>](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) (shown below) or recreate `IChangeToken` instances in response to changes.

In the sample app, the *WatchConsole* console app is configured to display a message whenever a text file is modified:

```
private static PhysicalFileProvider _fileProvider = 
    new PhysicalFileProvider(Directory.GetCurrentDirectory());

public static void Main(string[] args)
{
    Console.WriteLine("Monitoring quotes.txt for changes (Ctrl-c to quit)...");

    while (true)
    {
        MainAsync().GetAwaiter().GetResult();
    }
}

private static async Task MainAsync()
{
    IChangeToken token = _fileProvider.Watch("quotes.txt");
    var tcs = new TaskCompletionSource<object>();

    token.RegisterChangeCallback(state => 
        ((TaskCompletionSource<object>)state).TrySetResult(null), tcs);

    await tcs.Task.ConfigureAwait(false);

    Console.WriteLine("quotes.txt changed");
}
```

Some file systems, such as Docker containers and network shares, may not reliably send change notifications. Set the `DOTNET_USE_POLLING_FILE_WATCHER` environment variable to `1` or `true` to poll the file system for changes every four seconds (not configurable).

## Glob patterns

File system paths use wildcard patterns called *glob (or globbing) patterns*. Specify groups of files with these patterns. The two wildcard characters are `*` and `**`:

**`*`**\
Matches anything at the current folder level, any filename, or any file extension. Matches are terminated by `/` and `.` characters in the file path.

**`**`**\
Matches anything across multiple directory levels. Can be used to recursively match many files within a directory hierarchy.

**Glob pattern examples**

**`directory/file.txt`**\
Matches a specific file in a specific directory.

**`directory/*.txt`**\
Matches all files with *.txt* extension in a specific directory.

**`directory/*/appsettings.json`**\
Matches all `appsettings.json` files in directories exactly one level below the *directory* folder.

**`directory/**/*.txt`**\
Matches all files with *.txt* extension found anywhere under the *directory* folder.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-configuration
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/identity-configuration.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure ASP.NET Core Identity

ASP.NET Core Identity uses default values for settings such as password policy, lockout, and cookie configuration. These settings can be overridden at application startup.

## Identity options

The [IdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions) class represents the options that can be used to configure the Identity system. [IdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions) must be set **after** calling [AddIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentity) or [AddDefaultIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionuiextensions.adddefaultidentity).

### Claims Identity

[IdentityOptions.ClaimsIdentity](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.claimsidentity#microsoft-aspnetcore-identity-identityoptions-claimsidentity) specifies the [ClaimsIdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions) with the properties shown in the following table.

| Property                                                                                                               | Description                                                     | Default                                                                                                                                          |
| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [RoleClaimType](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions.roleclaimtype)                   | Gets or sets the claim type used for a role claim.              | [ClaimTypes.Role](/en-us/dotnet/api/system.security.claims.claimtypes.role#system-security-claims-claimtypes-role)                               |
| [SecurityStampClaimType](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions.securitystampclaimtype) | Gets or sets the claim type used for the security stamp claim.  | `AspNet.Identity.SecurityStamp`                                                                                                                  |
| [UserIdClaimType](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions.useridclaimtype)               | Gets or sets the claim type used for the user identifier claim. | [ClaimTypes.NameIdentifier](/en-us/dotnet/api/system.security.claims.claimtypes.nameidentifier#system-security-claims-claimtypes-nameidentifier) |
| [UserNameClaimType](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions.usernameclaimtype)           | Gets or sets the claim type used for the user name claim.       | [ClaimTypes.Name](/en-us/dotnet/api/system.security.claims.claimtypes.name#system-security-claims-claimtypes-name)                               |

### Lockout

Lockout is set in the [PasswordSignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.passwordsigninasync#microsoft-aspnetcore-identity-signinmanager-1-passwordsigninasync\(system-string-system-string-system-boolean-system-boolean\)) method:

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");

    ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(Input.Email,
             Input.Password, Input.RememberMe,
             lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation("User logged in.");
            return LocalRedirect(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning("User account locked out.");
            return RedirectToPage("./Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return Page();
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}
```

The preceding code is based on the [`Login` Identity template](https://github.com/dotnet/aspnetcore/blob/1dcf7acfacf0fe154adcc23270cb0da11ff44ace/src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Login.cshtml.cs#L131-L132).

Lockout options are set in `Program.cs`:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using RPauth.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
                                       options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.Configure<IdentityOptions>(options =>
{
    // Default Lockout settings.
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code sets the [IdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions) [LockoutOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions) with default values.

A successful authentication resets the failed access attempts count and resets the clock.

[IdentityOptions.Lockout](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.lockout) specifies the [LockoutOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions) with the properties shown in the table.

| Property                                                                                                          | Description                                                                             | Default   |
| ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------- |
| [AllowedForNewUsers](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions.allowedfornewusers)           | Determines if a new user can be locked out.                                             | `true`    |
| [DefaultLockoutTimeSpan](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions.defaultlockouttimespan)   | The amount of time a user is locked out when a lockout occurs.                          | 5 minutes |
| [MaxFailedAccessAttempts](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions.maxfailedaccessattempts) | The number of failed access attempts until a user is locked out, if lockout is enabled. | 5         |

### Password

By default, Identity requires that passwords contain an uppercase character, lowercase character, a digit, and a non-alphanumeric character. Passwords must be at least six characters long.

Passwords are configured with:

* [PasswordOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions) in `Program.cs`.

* [`[StringLength]` attributes](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute) of `Password` properties if Identity is [scaffolded into the app](scaffold-identity?view=aspnetcore-10.0). `InputModel` `Password` properties are found in the following files:

  * `Areas/Identity/Pages/Account/Register.cshtml.cs`
  * `Areas/Identity/Pages/Account/ResetPassword.cshtml.cs`

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using RPauth.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
                                options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.Configure<IdentityOptions>(options =>
{
    // Default Password settings.
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequireUppercase = true;
    options.Password.RequiredLength = 6;
    options.Password.RequiredUniqueChars = 1;
});

var app = builder.Build();

// Remaining code removed for brevity.
```

[IdentityOptions.Password](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.password) specifies the [PasswordOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions) with the properties shown in the table.

| Property                                                                                                         | Description                                                                                            | Default |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------- |
| [RequireDigit](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requiredigit)                     | Requires a number between 0-9 in the password.                                                         | `true`  |
| [RequiredLength](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requiredlength)                 | The minimum length of the password.                                                                    | 6       |
| [RequireLowercase](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requirelowercase)             | Requires a lowercase character in the password.                                                        | `true`  |
| [RequireNonAlphanumeric](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requirenonalphanumeric) | Requires a non-alphanumeric character in the password.                                                 | `true`  |
| [RequiredUniqueChars](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requireduniquechars)       | Only applies to ASP.NET Core 2.0 or later. Requires the number of distinct characters in the password. | 1       |
| [RequireUppercase](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requireuppercase)             | Requires an uppercase character in the password.                                                       | `true`  |

### Sign-in

The following code sets `SignIn` settings (to default values):

```
builder.Services.Configure<IdentityOptions>(options =>
{
    // Default SignIn settings.
    options.SignIn.RequireConfirmedEmail = false;
    options.SignIn.RequireConfirmedPhoneNumber = false;
});
```

[IdentityOptions.SignIn](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.signin#microsoft-aspnetcore-identity-identityoptions-signin) specifies the [SignInOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinoptions) with the properties shown in the table.

| Property                                                                                                                 | Description                                   | Default |
| ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | ------- |
| [RequireConfirmedEmail](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinoptions.requireconfirmedemail)             | Requires a confirmed email to sign in.        | `false` |
| [RequireConfirmedPhoneNumber](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinoptions.requireconfirmedphonenumber) | Requires a confirmed phone number to sign in. | `false` |

### Tokens

[IdentityOptions.Tokens](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.tokens) specifies the [TokenOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions) with the properties shown in the table.

| Property                                                                                                                      | Description                                                                                                                                                                                  |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [AuthenticatorTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.authenticatortokenprovider)         | Gets or sets the `AuthenticatorTokenProvider` used to validate two-factor sign-ins with an authenticator.                                                                                    |
| [ChangeEmailTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.changeemailtokenprovider)             | Gets or sets the `ChangeEmailTokenProvider` used to generate tokens used in email change confirmation emails.                                                                                |
| [ChangePhoneNumberTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.changephonenumbertokenprovider) | Gets or sets the `ChangePhoneNumberTokenProvider` used to generate tokens used when changing phone numbers.                                                                                  |
| [EmailConfirmationTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.emailconfirmationtokenprovider) | Gets or sets the token provider used to generate tokens used in account confirmation emails.                                                                                                 |
| [PasswordResetTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.passwordresettokenprovider)         | Gets or sets the [IUserTwoFactorTokenProvider\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.iusertwofactortokenprovider-1) used to generate tokens used in password reset emails. |
| [ProviderMap](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.providermap)                                       | Used to construct a [User Token Provider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenproviderdescriptor) with the key used as the provider's name.                                 |

### User

```
builder.Services.Configure<IdentityOptions>(options =>
{
    // Default User settings.
    options.User.AllowedUserNameCharacters =
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = false;

});
```

[IdentityOptions.User](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.user) specifies the [UserOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.useroptions) with the properties shown in the table.

| Property                                                                                                           | Description                                | Default                                                                 |
| ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | ----------------------------------------------------------------------- |
| [AllowedUserNameCharacters](/en-us/dotnet/api/microsoft.aspnetcore.identity.useroptions.allowedusernamecharacters) | Allowed characters in the username.        | abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 -.\_@+ |
| [RequireUniqueEmail](/en-us/dotnet/api/microsoft.aspnetcore.identity.useroptions.requireuniqueemail)               | Requires each user to have a unique email. | `false`                                                                 |

[]()

### Cookie settings

Configure the app's cookie in `Program.cs`. [ConfigureApplicationCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.configureapplicationcookie#microsoft-extensions-dependencyinjection-identityservicecollectionextensions-configureapplicationcookie\(microsoft-extensions-dependencyinjection-iservicecollection-system-action\(\(microsoft-aspnetcore-authentication-cookies-cookieauthenticationoptions\)\)\)) must be called **after** calling `AddIdentityCore`, `AddIdentity`, or `AddDefaultIdentity`.

```
builder.Services.ConfigureApplicationCookie(options =>
{
    options.AccessDeniedPath = "/Identity/Account/AccessDenied";
    options.Cookie.Name = "YourAppCookieName";
    options.Cookie.HttpOnly = true;
    options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
    options.LoginPath = "/Identity/Account/Login";
    // ReturnUrlParameter requires 
    //using Microsoft.AspNetCore.Authentication.Cookies;
    options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
    options.SlidingExpiration = true;
});
```

For more information, see [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions).

## Password Hasher options

[PasswordHasherOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions) gets and sets options for password hashing.

| Option                                                                                                                                                                             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [CompatibilityMode](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.compatibilitymode#microsoft-aspnetcore-identity-passwordhasheroptions-compatibilitymode) | The compatibility mode used when hashing new passwords. Defaults to [IdentityV3](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhashercompatibilitymode#microsoft-aspnetcore-identity-passwordhashercompatibilitymode-identityv3). The first byte of a hashed password, called a *format marker*, specifies the version of the hashing algorithm used to hash the password. When verifying a password against a hash, the [VerifyHashedPassword](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasher-1.verifyhashedpassword) method selects the correct algorithm based on the first byte. A client is able to authenticate regardless of which version of the algorithm was used to hash the password. Setting the compatibility mode affects the hashing of *new passwords*. |
| [IterationCount](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.iterationcount#microsoft-aspnetcore-identity-passwordhasheroptions-iterationcount)          | The number of iterations used when hashing passwords using PBKDF2. This value is only used when the [CompatibilityMode](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.compatibilitymode#microsoft-aspnetcore-identity-passwordhasheroptions-compatibilitymode) is set to [IdentityV3](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhashercompatibilitymode#microsoft-aspnetcore-identity-passwordhashercompatibilitymode-identityv3). The value must be a positive integer and defaults to `100000`.                                                                                                                                                                                                                                                            |

In the following example, the [IterationCount](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.iterationcount#microsoft-aspnetcore-identity-passwordhasheroptions-iterationcount) is set to `12000` in `Program.cs`:

```
// using Microsoft.AspNetCore.Identity;

builder.Services.Configure<PasswordHasherOptions>(option =>
{
    option.IterationCount = 12000;
});
```

## Globally require all users to be authenticated

For guidance, see [Require global user authentication](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication).

[]()

## ISecurityStampValidator and SignOut everywhere

Apps need to react to events involving security sensitive actions by regenerating the users [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal). For example, the `ClaimsPrincipal` should be regenerated when joining a role, changing the password, or other security sensitive events. Identity uses the [ISecurityStampValidator](/en-us/dotnet/api/microsoft.aspnetcore.identity.isecuritystampvalidator) interface to regenerate the `ClaimsPrincipal`. The default implementation of Identity registers a [SecurityStampValidator](/en-us/dotnet/api/microsoft.aspnetcore.identity.securitystampvalidator) with the main [application cookie](#cs6) and the two-factor cookie. The validator hooks into the [OnValidatePrincipal](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.onvalidateprincipal#microsoft-aspnetcore-authentication-cookies-cookieauthenticationevents-onvalidateprincipal) event of each cookie to call into Identity to verify that the user's security stamp claim is unchanged from what's stored in the cookie. The validator calls in at regular intervals. The call interval is a tradeoff between hitting the datastore too frequently and not often enough. Checking with a long interval results in stale claims. Call `userManager.UpdateSecurityStampAsync(user)`to force existing cookies to be invalided the next time they are checked. Most of the Identity UI account and manage pages call `userManager.UpdateSecurityStampAsync(user)` after changing the password or adding a login. Apps can call `userManager.UpdateSecurityStampAsync(user)` to implement a sign out everywhere action.

Changing the validation interval is shown in the following highlighted code:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebClaimsPrincipal.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") 
    ?? throw new InvalidOperationException("'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => 
options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

// Force Identity's security stamp to be validated every minute.
builder.Services.Configure<SecurityStampValidatorOptions>(o => 
                   o.ValidationInterval = TimeSpan.FromMinutes(1));

builder.Services.AddRazorPages();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

ASP.NET Core Identity uses default values for settings such as password policy, lockout, and cookie configuration. These settings can be overridden in the `Startup` class.

## Identity options

The [IdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions) class represents the options that can be used to configure the Identity system. `IdentityOptions` must be set **after** calling `AddIdentity` or `AddDefaultIdentity`.

### Claims Identity

[IdentityOptions.ClaimsIdentity](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.claimsidentity#microsoft-aspnetcore-identity-identityoptions-claimsidentity) specifies the [ClaimsIdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions) with the properties shown in the following table.

| Property                                                                                                               | Description                                                     | Default                                                                                                                                          |
| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [RoleClaimType](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions.roleclaimtype)                   | Gets or sets the claim type used for a role claim.              | [ClaimTypes.Role](/en-us/dotnet/api/system.security.claims.claimtypes.role#system-security-claims-claimtypes-role)                               |
| [SecurityStampClaimType](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions.securitystampclaimtype) | Gets or sets the claim type used for the security stamp claim.  | `AspNet.Identity.SecurityStamp`                                                                                                                  |
| [UserIdClaimType](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions.useridclaimtype)               | Gets or sets the claim type used for the user identifier claim. | [ClaimTypes.NameIdentifier](/en-us/dotnet/api/system.security.claims.claimtypes.nameidentifier#system-security-claims-claimtypes-nameidentifier) |
| [UserNameClaimType](/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions.usernameclaimtype)           | Gets or sets the claim type used for the user name claim.       | [ClaimTypes.Name](/en-us/dotnet/api/system.security.claims.claimtypes.name#system-security-claims-claimtypes-name)                               |

### Lockout

Lockout is set in the [PasswordSignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.passwordsigninasync#microsoft-aspnetcore-identity-signinmanager-1-passwordsigninasync\(system-string-system-string-system-boolean-system-boolean\)) method:

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(Input.Email, 
            Input.Password, Input.RememberMe, 
            lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation("User logged in.");
            return LocalRedirect(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl,
                Input.RememberMe });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning("User account locked out.");
            return RedirectToPage("./Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return Page();
        }
    }

    // If we got this far, something failed, redisplay form
    return Page();
}
```

The preceding code is based on the `Login` Identity template.

Lockout options are set in `StartUp.ConfigureServices`:

```
services.Configure<IdentityOptions>(options =>
{
    // Default Lockout settings.
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;
});
```

The preceding code sets the [IdentityOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions) [LockoutOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions) with default values.

A successful authentication resets the failed access attempts count and resets the clock.

[IdentityOptions.Lockout](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.lockout) specifies the [LockoutOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions) with the properties shown in the table.

| Property                                                                                                          | Description                                                                             | Default   |
| ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------- |
| [AllowedForNewUsers](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions.allowedfornewusers)           | Determines if a new user can be locked out.                                             | `true`    |
| [DefaultLockoutTimeSpan](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions.defaultlockouttimespan)   | The amount of time a user is locked out when a lockout occurs.                          | 5 minutes |
| [MaxFailedAccessAttempts](/en-us/dotnet/api/microsoft.aspnetcore.identity.lockoutoptions.maxfailedaccessattempts) | The number of failed access attempts until a user is locked out, if lockout is enabled. | 5         |

### Password

By default, Identity requires that passwords contain an uppercase character, lowercase character, a digit, and a non-alphanumeric character. Passwords must be at least six characters long.

Passwords are configured with:

* [PasswordOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions) in `Startup.ConfigureServices`.

* [`[StringLength]` attributes](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute) of `Password` properties if Identity is [scaffolded into the app](scaffold-identity?view=aspnetcore-10.0). `InputModel` `Password` properties are found in the following files:

  * `Areas/Identity/Pages/Account/Register.cshtml.cs`
  * `Areas/Identity/Pages/Account/ResetPassword.cshtml.cs`

```
services.Configure<IdentityOptions>(options =>
{
    // Default Password settings.
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequireUppercase = true;
    options.Password.RequiredLength = 6;
    options.Password.RequiredUniqueChars = 1;
});
```

[IdentityOptions.Password](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.password) specifies the [PasswordOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions) with the properties shown in the table.

| Property                                                                                                         | Description                                                                                            | Default |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------- |
| [RequireDigit](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requiredigit)                     | Requires a number between 0-9 in the password.                                                         | `true`  |
| [RequiredLength](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requiredlength)                 | The minimum length of the password.                                                                    | 6       |
| [RequireLowercase](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requirelowercase)             | Requires a lowercase character in the password.                                                        | `true`  |
| [RequireNonAlphanumeric](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requirenonalphanumeric) | Requires a non-alphanumeric character in the password.                                                 | `true`  |
| [RequiredUniqueChars](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requireduniquechars)       | Only applies to ASP.NET Core 2.0 or later. Requires the number of distinct characters in the password. | 1       |
| [RequireUppercase](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions.requireuppercase)             | Requires an uppercase character in the password.                                                       | `true`  |

### Sign-in

The following code sets `SignIn` settings (to default values):

```
services.Configure<IdentityOptions>(options =>
{
    // Default SignIn settings.
    options.SignIn.RequireConfirmedEmail = false;
    options.SignIn.RequireConfirmedPhoneNumber = false;
});
```

[IdentityOptions.SignIn](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.signin#microsoft-aspnetcore-identity-identityoptions-signin) specifies the [SignInOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinoptions) with the properties shown in the table.

| Property                                                                                                                 | Description                                   | Default |
| ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | ------- |
| [RequireConfirmedEmail](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinoptions.requireconfirmedemail)             | Requires a confirmed email to sign in.        | `false` |
| [RequireConfirmedPhoneNumber](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinoptions.requireconfirmedphonenumber) | Requires a confirmed phone number to sign in. | `false` |

### Tokens

[IdentityOptions.Tokens](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.tokens) specifies the [TokenOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions) with the properties shown in the table.

| Property                                                                                                                      | Description                                                                                                                                                                                  |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [AuthenticatorTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.authenticatortokenprovider)         | Gets or sets the `AuthenticatorTokenProvider` used to validate two-factor sign-ins with an authenticator.                                                                                    |
| [ChangeEmailTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.changeemailtokenprovider)             | Gets or sets the `ChangeEmailTokenProvider` used to generate tokens used in email change confirmation emails.                                                                                |
| [ChangePhoneNumberTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.changephonenumbertokenprovider) | Gets or sets the `ChangePhoneNumberTokenProvider` used to generate tokens used when changing phone numbers.                                                                                  |
| [EmailConfirmationTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.emailconfirmationtokenprovider) | Gets or sets the token provider used to generate tokens used in account confirmation emails.                                                                                                 |
| [PasswordResetTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.passwordresettokenprovider)         | Gets or sets the [IUserTwoFactorTokenProvider\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.iusertwofactortokenprovider-1) used to generate tokens used in password reset emails. |
| [ProviderMap](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenoptions.providermap)                                       | Used to construct a [User Token Provider](/en-us/dotnet/api/microsoft.aspnetcore.identity.tokenproviderdescriptor) with the key used as the provider's name.                                 |

### User

```
services.Configure<IdentityOptions>(options =>
{
    // Default User settings.
    options.User.AllowedUserNameCharacters =
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = false;

});
```

[IdentityOptions.User](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityoptions.user) specifies the [UserOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.useroptions) with the properties shown in the table.

| Property                                                                                                           | Description                                | Default                                                                 |
| ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | ----------------------------------------------------------------------- |
| [AllowedUserNameCharacters](/en-us/dotnet/api/microsoft.aspnetcore.identity.useroptions.allowedusernamecharacters) | Allowed characters in the username.        | abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 -.\_@+ |
| [RequireUniqueEmail](/en-us/dotnet/api/microsoft.aspnetcore.identity.useroptions.requireuniqueemail)               | Requires each user to have a unique email. | `false`                                                                 |

### Cookie settings

Configure the app's cookie in `Startup.ConfigureServices`. [ConfigureApplicationCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.configureapplicationcookie#microsoft-extensions-dependencyinjection-identityservicecollectionextensions-configureapplicationcookie\(microsoft-extensions-dependencyinjection-iservicecollection-system-action\(\(microsoft-aspnetcore-authentication-cookies-cookieauthenticationoptions\)\)\)) must be called **after** calling `AddIdentityCore`, `AddIdentity`, or `AddDefaultIdentity`.

```
services.ConfigureApplicationCookie(options =>
{
    options.AccessDeniedPath = "/Identity/Account/AccessDenied";
    options.Cookie.Name = "YourAppCookieName";
    options.Cookie.HttpOnly = true;
    options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
    options.LoginPath = "/Identity/Account/Login";
    // ReturnUrlParameter requires 
    //using Microsoft.AspNetCore.Authentication.Cookies;
    options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
    options.SlidingExpiration = true;
});
```

For more information, see [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions).

## Password Hasher options

[PasswordHasherOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions) gets and sets options for password hashing.

| Option                                                                                                                                                                             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [CompatibilityMode](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.compatibilitymode#microsoft-aspnetcore-identity-passwordhasheroptions-compatibilitymode) | The compatibility mode used when hashing new passwords. Defaults to [IdentityV3](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhashercompatibilitymode#microsoft-aspnetcore-identity-passwordhashercompatibilitymode-identityv3). The first byte of a hashed password, called a *format marker*, specifies the version of the hashing algorithm used to hash the password. When verifying a password against a hash, the [VerifyHashedPassword](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasher-1.verifyhashedpassword) method selects the correct algorithm based on the first byte. A client is able to authenticate regardless of which version of the algorithm was used to hash the password. Setting the compatibility mode affects the hashing of *new passwords*. |
| [IterationCount](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.iterationcount#microsoft-aspnetcore-identity-passwordhasheroptions-iterationcount)          | The number of iterations used when hashing passwords using PBKDF2. This value is only used when the [CompatibilityMode](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.compatibilitymode#microsoft-aspnetcore-identity-passwordhasheroptions-compatibilitymode) is set to [IdentityV3](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhashercompatibilitymode#microsoft-aspnetcore-identity-passwordhashercompatibilitymode-identityv3). The value must be a positive integer and defaults to `10000`.                                                                                                                                                                                                                                                             |

In the following example, the [IterationCount](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.iterationcount#microsoft-aspnetcore-identity-passwordhasheroptions-iterationcount) is set to `12000` in `Startup.ConfigureServices`:

```
// using Microsoft.AspNetCore.Identity;

services.Configure<PasswordHasherOptions>(option =>
{
    option.IterationCount = 12000;
});
```

## Globally require all users to be authenticated

For guidance, see [Require global user authentication](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/static-files
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/static-files.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor static files

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes Blazor app configuration for serving static files.

For general information on serving static files with Map Static Assets routing endpoint conventions, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0) before reading this article.

## Preloaded Blazor framework static assets

In Blazor Web Apps, framework static assets are automatically preloaded using [`Link` headers](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Link), which allows the browser to preload resources before the initial page is fetched and rendered.

In standalone Blazor WebAssembly apps, framework assets are scheduled for high priority downloading and caching early in browser `index.html` page processing when:

* The `OverrideHtmlAssetPlaceholders` MSBuild property in the app's project file (`.csproj`) is set to `true`:

  ```
  <PropertyGroup>
    <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
  </PropertyGroup>
  ```

* The following `<link>` element containing [`rel="preload"`](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/rel/preload) is present in the `<head>` content of `wwwroot/index.html`:

  ```
  <link rel="preload" id="webassembly" />
  ```

Note

Blazor Web App enhanced navigation doesn't attempt to preload resources in .NET 11 or later. For more information, see [Blazor enhanced navigation no longer preloads resources](/en-us/aspnet/core/breaking-changes/11/blazor-enhanced-nav-preloading-disabled).

## Static asset delivery in server-side Blazor apps

Serving static assets is managed by either routing endpoint conventions or a middleware described in the following table.

| Feature                                        | API                                                                                                                          | .NET Version      | Description                                                                                                                                                |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Map Static Assets routing endpoint conventions | [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) | .NET 9 or later   | Optimizes the delivery of static assets to clients.                                                                                                        |
| Static file middleware                         | [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)                         | All .NET versions | Serves static assets to clients without the optimizations of Map Static Assets but useful for some tasks that Map Static Assets isn't capable of managing. |

Map Static Assets can replace [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) in most situations. However, Map Static Assets is optimized for serving the assets from known locations in the app at build and publish time. If the app serves assets from other locations, such as disk or embedded resources, [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) should be used.

Map Static Assets ([MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets)) also replaces calling [UseBlazorFrameworkFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentswebassemblyapplicationbuilderextensions.useblazorframeworkfiles) in apps that serve Blazor WebAssembly framework files, and explicitly calling [UseBlazorFrameworkFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentswebassemblyapplicationbuilderextensions.useblazorframeworkfiles) in a Blazor Web App isn't necessary because the API is automatically called when invoking [AddInteractiveWebAssemblyComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addinteractivewebassemblycomponents).

When [Interactive WebAssembly or Interactive Auto render modes](./?view=aspnetcore-10.0#render-modes) are enabled:

* Blazor creates an endpoint to expose the resource collection as a JS module.
* The URL is emitted to the body of the request as persisted component state when a WebAssembly component is rendered into the page.
* During WebAssembly boot, Blazor retrieves the URL, imports the module, and calls a function to retrieve the asset collection and reconstruct it in memory. The URL is specific to the content and cached forever, so this overhead cost is only paid once per user until the app is updated.
* The resource collection is also exposed at a human-readable URL (`_framework/resource-collection.js`), so JS has access to the resource collection for [enhanced navigation](navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling) or to implement features of other frameworks and third-party components.

Static file middleware ([UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)) is useful in the following situations that Map Static Assets ([MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets)) can't handle:

* Serving files from disk that aren't part of the build or publish process, for example, files added to the application folder during or after deployment.
* Applying a path prefix to Blazor WebAssembly static asset files, which is covered in the [Prefix for Blazor WebAssembly assets](#prefix-for-blazor-webassembly-assets) section.
* Configuring file mappings of extensions to specific content types and setting static file options, which is covered in the [File mappings and static file options](#file-mappings-and-static-file-options) section.

For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

## Deliver assets with Map Static Assets routing endpoint conventions

*This section applies to server-side Blazor apps.*

Assets are delivered via the [ComponentBase.Assets](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.assets#microsoft-aspnetcore-components-componentbase-assets) property, which resolves the fingerprinted URL for a given asset. In the following example, Bootstrap, the Blazor project template app stylesheet (`app.css`), and the [CSS isolation stylesheet](../components/css-isolation?view=aspnetcore-10.0) (based on an app's namespace of `BlazorSample`) are linked in a root component, typically the `App` component (`Components/App.razor`):

```
<link rel="stylesheet" href="@Assets["bootstrap/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["BlazorSample.styles.css"]" />
```

## `ImportMap` component

*This section applies to Blazor Web Apps that call [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents).*

The `ImportMap` component ([ImportMap](/en-us/dotnet/api/microsoft.aspnetcore.components.importmap)) represents an import map element (`<script type="importmap"></script>`) that defines the import map for module scripts. The Import Map component is placed in `<head>` content of the root component, typically the `App` component (`Components/App.razor`).

```
<ImportMap />
```

If a custom [ImportMapDefinition](/en-us/dotnet/api/microsoft.aspnetcore.components.importmapdefinition) isn't assigned to an Import Map component, the import map is generated based on the app's assets.

Note

[ImportMapDefinition](/en-us/dotnet/api/microsoft.aspnetcore.components.importmapdefinition) instances are expensive to create, so we recommended caching them when creating an additional instance.

The following examples demonstrate custom import map definitions and the import maps that they create.

Basic import map:

```
new ImportMapDefinition(
    new Dictionary<string, string>
    {
        { "jquery", "https://cdn.example.com/jquery.js" },
    },
    null,
    null);
```

The preceding code results in the following import map:

```
{
  "imports": {
    "jquery": "https://cdn.example.com/jquery.js"
  }
}
```

Scoped import map:

```
new ImportMapDefinition(
    null,
    new Dictionary<string, IReadOnlyDictionary<string, string>>
    {
        ["/scoped/"] = new Dictionary<string, string>
        {
            { "jquery", "https://cdn.example.com/jquery.js" },
        }
    },
    null);
```

The preceding code results in the following import map:

```
{
  "scopes": {
    "/scoped/": {
      "jquery": "https://cdn.example.com/jquery.js"
    }
  }
}
```

Import map with integrity:

```
new ImportMapDefinition(
    new Dictionary<string, string>
    {
        { "jquery", "https://cdn.example.com/jquery.js" },
    },
    null,
    new Dictionary<string, string>
    {
        { "https://cdn.example.com/jquery.js", "sha384-abc123" },
    });
```

The preceding code results in the following import map:

```
{
  "imports": {
    "jquery": "https://cdn.example.com/jquery.js"
  },
  "integrity": {
    "https://cdn.example.com/jquery.js": "sha384-abc123"
  }
}
```

Combine import map definitions ([ImportMapDefinition](/en-us/dotnet/api/microsoft.aspnetcore.components.importmapdefinition)) with [ImportMapDefinition.Combine](/en-us/dotnet/api/microsoft.aspnetcore.components.importmapdefinition.combine).

Import map created from a [ResourceAssetCollection](/en-us/dotnet/api/microsoft.aspnetcore.components.resourceassetcollection) that maps static assets to their corresponding unique URLs:

```
ImportMapDefinition.FromResourceCollection(
    new ResourceAssetCollection(
    [
        new ResourceAsset(
            "jquery.fingerprint.js",
            [
                new ResourceAssetProperty("integrity", "sha384-abc123"),
                new ResourceAssetProperty("label", "jquery.js"),
            ])
    ]));
```

The preceding code results in the following import map:

```
{
  "imports": {
    "./jquery.js": "./jquery.fingerprint.js"
  },
  "integrity": {
    "jquery.fingerprint.js": "sha384-abc123"
  }
}
```

## Import map Content Security Policy (CSP) violations

*This section applies to Blazor Web Apps that call [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents).*

The `ImportMap` component is rendered as an inline `<script>` tag, which violates a strict [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP) that sets the `default-src` or `script-src` directive.

For examples of how to address the policy violation with Subresource Integrity (SRI) or a cryptographic nonce, see [Resolving CSP violations with Subresource Integrity (SRI) or a nonce](../security/content-security-policy?view=aspnetcore-10.0#resolving-csp-violations-with-subresource-integrity-sri-or-a-cryptographic-nonce).

Configure static file middleware to serve static assets to clients by calling [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) in the app's request processing pipeline. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

In releases prior to .NET 8, Blazor framework static files, such as the Blazor script, are served via static file middleware. In .NET 8 or later, Blazor framework static files are mapped using endpoint routing, and static file middleware is no longer used.

## Fingerprint client-side static assets in standalone Blazor WebAssembly apps

In standalone Blazor WebAssembly apps during build and publish, the framework overrides placeholders in `index.html` with values computed during build to fingerprint static assets for client-side rendering. A [fingerprint](https://wikipedia.org/wiki/Fingerprint_\(computing\)) is placed into the `blazor.webassembly.js` script file name, and an import map is generated for other .NET assets.

The following configuration must be present in the `wwwwoot/index.html` file of a standalone Blazor WebAssembly app to adopt fingerprinting:

```
<head>
    ...
    <script type="importmap"></script>
    ...
</head>

<body>
    ...
    <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
    ...
</body>

</html>
```

In the project file (`.csproj`), the `<OverrideHtmlAssetPlaceholders>` property is set to `true`:

```
<PropertyGroup>
  <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
```

When resolving imports for JavaScript interop, the import map is used by the browser resolve fingerprinted files.

In the following example, all developer-supplied JS files are modules with a `.js` file extension.

A module named `scripts.js` in the app's `wwwroot/js` folder is fingerprinted by adding `#[.{fingerprint}]` before the file extension (`.js`):

```
<script type="module" src="js/scripts#[.{fingerprint}].js"></script>
```

Specify the fingerprint expression with the `<StaticWebAssetFingerprintPattern>` property in the app's project file (`.csproj`):

```
<ItemGroup>
  <StaticWebAssetFingerprintPattern Include="JSModule" Pattern="*.js" 
    Expression="#[.{fingerprint}]!" />
</ItemGroup>
```

Any JS file (`*.js`) in `index.html` with the fingerprint marker is fingerprinted by the framework, including when the app is published.

## Fingerprint client-side static assets in Blazor Web Apps

For client-side rendering (CSR) in Blazor Web Apps (Interactive Auto or Interactive WebAssembly render modes), static asset server-side [fingerprinting](https://wikipedia.org/wiki/Fingerprint_\(computing\)) is enabled by adopting [Map Static Assets routing endpoint conventions (`MapStaticAssets`)](../../fundamentals/static-files?view=aspnetcore-10.0), [`ImportMap` component](static-files?view=aspnetcore-10.0#importmap-component), and the [ComponentBase.Assets](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.assets#microsoft-aspnetcore-components-componentbase-assets) property (`@Assets["..."]`). For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

To fingerprint additional JavaScript modules for CSR, use the `<StaticWebAssetFingerprintPattern>` item in the app's project file (`.csproj`). In the following example, a fingerprint is added for all developer-supplied `.mjs` files in the app:

```
<ItemGroup>
  <StaticWebAssetFingerprintPattern Include="JSModule" Pattern="*.mjs" 
    Expression="#[.{fingerprint}]!" />
</ItemGroup>
```

When resolving imports for JavaScript interop, the import map is used by the browser resolve fingerprinted files.

## Summary of static file `<link>` `href` formats

*This section applies to all .NET releases and Blazor apps.*

The following tables summarize static file `<link>` `href` formats by .NET release.

For the location of `<head>` content where static file links are placed, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content). Static asset links can also be supplied using [`<HeadContent>` components](../components/control-head-content?view=aspnetcore-10.0) in individual Razor components.

For the location of `<head>` content where static file links are placed, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content).

### .NET 9 or later

| App type                      | `href` value        | Examples                                                                                                                                  |
| ----------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Blazor Web App                | `@Assets["{PATH}"]` | `<link rel="stylesheet" href="@Assets["app.css"]" />` `<link href="@Assets["_content/ComponentLib/styles.css"]" rel="stylesheet" />`      |
| Blazor Server†                | `@Assets["{PATH}"]` | `<link href="@Assets["css/site.css"]" rel="stylesheet" />` `<link href="@Assets["_content/ComponentLib/styles.css"]" rel="stylesheet" />` |
| Standalone Blazor WebAssembly | `{PATH}`            | `<link rel="stylesheet" href="css/app.css" />` `<link href="_content/ComponentLib/styles.css" rel="stylesheet" />`                        |

### .NET 8.x

| App type                      | `href` value | Examples                                                                                                            |
| ----------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------- |
| Blazor Web App                | `{PATH}`     | `<link rel="stylesheet" href="app.css" />` `<link href="_content/ComponentLib/styles.css" rel="stylesheet" />`      |
| Blazor Server†                | `{PATH}`     | `<link href="css/site.css" rel="stylesheet" />` `<link href="_content/ComponentLib/styles.css" rel="stylesheet" />` |
| Standalone Blazor WebAssembly | `{PATH}`     | `<link rel="stylesheet" href="css/app.css" />` `<link href="_content/ComponentLib/styles.css" rel="stylesheet" />`  |

### .NET 7.x or earlier

| App type                   | `href` value | Examples                                                                                                            |
| -------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------- |
| Blazor Server†             | `{PATH}`     | `<link href="css/site.css" rel="stylesheet" />` `<link href="_content/ComponentLib/styles.css" rel="stylesheet" />` |
| Hosted Blazor WebAssembly‡ | `{PATH}`     | `<link href="css/app.css" rel="stylesheet" />` `<link href="_content/ComponentLib/styles.css" rel="stylesheet" />`  |
| Blazor WebAssembly         | `{PATH}`     | `<link href="css/app.css" rel="stylesheet" />` `<link href="_content/ComponentLib/styles.css" rel="stylesheet" />`  |

†Blazor Server is supported in .NET 8 or later but is no longer a project template after .NET 7.\
‡We recommend updating Hosted Blazor WebAssembly apps to Blazor Web Apps when adopting .NET 8 or later.

## Static Web Asset Project Mode

*This section applies to the `.Client` project of a Blazor Web App.*

The required `<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>` setting in the `.Client` project of a Blazor Web App reverts Blazor WebAssembly static asset behaviors back to the defaults, so that the project behaves as part of the hosted project. The Blazor WebAssembly SDK (`Microsoft.NET.Sdk.BlazorWebAssembly`) configures static web assets in a specific way to work in "standalone" mode with a server simply consuming the outputs from the library. This isn't appropriate for a Blazor Web App, where the WebAssembly portion of the app is a logical part of the host and must behave more like a library. For example, the project doesn't expose the styles bundle (for example, `BlazorSample.Client.styles.css`) and instead only provides the host with the project bundle, so that the host can include it in its own styles bundle.

Changing the value (`Default`) of `<StaticWebAssetProjectMode>` or removing the property from the `.Client` project isn't supported.

## Prefix for Blazor WebAssembly assets

*This section applies to Blazor Web Apps.*

Use the [WebAssemblyComponentsEndpointOptions.PathPrefix](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.server.webassemblycomponentsendpointoptions.pathprefix#microsoft-aspnetcore-components-webassembly-server-webassemblycomponentsendpointoptions-pathprefix) endpoint option to set the path string that indicates the prefix for Blazor WebAssembly assets. The path must correspond to a referenced Blazor WebAssembly application project.

```
endpoints.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode(options => 
        options.PathPrefix = "{PATH PREFIX}");
```

In the preceding example, the `{PATH PREFIX}` placeholder is the path prefix and must start with a forward slash (`/`).

In the following example, the path prefix is set to `/path-prefix`:

```
endpoints.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode(options => 
        options.PathPrefix = "/path-prefix");
```

## Static web asset base path

*This section applies to standalone Blazor WebAssembly apps.*

Publishing the app places the app's static assets, including Blazor framework files (`_framework` folder assets), at the root path (`/`) in published output. The `<StaticWebAssetBasePath>` property specified in the project file (`.csproj`) sets the base path to a non-root path:

```
<PropertyGroup>
  <StaticWebAssetBasePath>{PATH}</StaticWebAssetBasePath>
</PropertyGroup>
```

In the preceding example, the `{PATH}` placeholder is the path.

Without setting the `<StaticWebAssetBasePath>` property, a standalone app is published at `/BlazorStandaloneSample/bin/Release/{TFM}/publish/wwwroot/`.

In the preceding example, the `{TFM}` placeholder is the [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks).

If the `<StaticWebAssetBasePath>` property in a standalone Blazor WebAssembly app sets the published static asset path to `app1`, the root path to the app in published output is `/app1`.

In the standalone Blazor WebAssembly app's project file (`.csproj`):

```
<PropertyGroup>
  <StaticWebAssetBasePath>app1</StaticWebAssetBasePath>
</PropertyGroup>
```

In published output, the path to the standalone Blazor WebAssembly app is `/BlazorStandaloneSample/bin/Release/{TFM}/publish/wwwroot/app1/`.

In the preceding example, the `{TFM}` placeholder is the [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks).

*This section applies to standalone Blazor WebAssembly apps and hosted Blazor WebAssembly solutions.*

Publishing the app places the app's static assets, including Blazor framework files (`_framework` folder assets), at the root path (`/`) in published output. The `<StaticWebAssetBasePath>` property specified in the project file (`.csproj`) sets the base path to a non-root path:

```
<PropertyGroup>
  <StaticWebAssetBasePath>{PATH}</StaticWebAssetBasePath>
</PropertyGroup>
```

In the preceding example, the `{PATH}` placeholder is the path.

Without setting the `<StaticWebAssetBasePath>` property, the client app of a hosted solution or a standalone app is published at the following paths:

* In the **Server** project of a hosted Blazor WebAssembly solution: `/BlazorHostedSample/Server/bin/Release/{TFM}/publish/wwwroot/`
* In a standalone Blazor WebAssembly app: `/BlazorStandaloneSample/bin/Release/{TFM}/publish/wwwroot/`

If the `<StaticWebAssetBasePath>` property in the **Client** project of a hosted Blazor WebAssembly app or in a standalone Blazor WebAssembly app sets the published static asset path to `app1`, the root path to the app in published output is `/app1`.

In the **Client** app's project file (`.csproj`) or the standalone Blazor WebAssembly app's project file (`.csproj`):

```
<PropertyGroup>
  <StaticWebAssetBasePath>app1</StaticWebAssetBasePath>
</PropertyGroup>
```

In published output:

* Path to the client app in the **Server** project of a hosted Blazor WebAssembly solution: `/BlazorHostedSample/Server/bin/Release/{TFM}/publish/wwwroot/app1/`
* Path to a standalone Blazor WebAssembly app: `/BlazorStandaloneSample/bin/Release/{TFM}/publish/wwwroot/app1/`

The `<StaticWebAssetBasePath>` property is most commonly used to control the paths to published static assets of multiple Blazor WebAssembly apps in a single hosted deployment. For more information, see [Multiple hosted ASP.NET Core Blazor WebAssembly apps](../host-and-deploy/webassembly/multiple-hosted-webassembly?view=aspnetcore-10.0). The property is also effective in standalone Blazor WebAssembly apps.

In the preceding examples, the `{TFM}` placeholder is the [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks).

## File mappings and static file options

*This section applies to server-side static files.*

To create additional file mappings with a [FileExtensionContentTypeProvider](/en-us/dotnet/api/microsoft.aspnetcore.staticfiles.fileextensioncontenttypeprovider) or configure other [StaticFileOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileoptions), use **one** of the following approaches. In the following examples, the `{EXTENSION}` placeholder is the file extension, and the `{CONTENT TYPE}` placeholder is the content type.

* Configure options through [dependency injection (DI)](dependency-injection?view=aspnetcore-10.0) in the `Program` file using [StaticFileOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileoptions):

  ```
  using Microsoft.AspNetCore.StaticFiles;

  ...

  var provider = new FileExtensionContentTypeProvider();
  provider.Mappings["{EXTENSION}"] = "{CONTENT TYPE}";

  builder.Services.Configure<StaticFileOptions>(options =>
  {
      options.ContentTypeProvider = provider;
  });
  ```

  This approach configures the same file provider used to serve the Blazor script. Make sure that your custom configuration doesn't interfere with serving the Blazor script. For example, don't remove the mapping for JavaScript files by configuring the provider with `provider.Mappings.Remove(".js")`.

* Use two calls to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) in the `Program` file:

  * Configure the custom file provider in the first call with [StaticFileOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileoptions).
  * The second middleware serves the Blazor script, which uses the default static files configuration provided by the Blazor framework.

  ```
  using Microsoft.AspNetCore.StaticFiles;

  ...

  var provider = new FileExtensionContentTypeProvider();
  provider.Mappings["{EXTENSION}"] = "{CONTENT TYPE}";

  app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider });
  app.UseStaticFiles();
  ```

* You can avoid interfering with serving `_framework/blazor.server.js` by using [MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen) to execute a custom static files middleware:

  ```
  app.MapWhen(ctx => !ctx.Request.Path
      .StartsWithSegments("/_framework/blazor.server.js"),
          subApp => subApp.UseStaticFiles(new StaticFileOptions() { ... }));
  ```

## Serve files from multiple locations

*The guidance in this section only applies to Blazor Web Apps.*

To serve files from multiple locations with a [CompositeFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.compositefileprovider):

* Add the namespace for [Microsoft.Extensions.FileProviders](/en-us/dotnet/api/microsoft.extensions.fileproviders) to the top of the `Program` file of the server project.

* In the server project's `Program` file ***before*** the call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

  * Create a [PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider) with the path to the static assets.
  * Create a [CompositeFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.compositefileprovider) from the [WebRootFileProvider](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootfileprovider#microsoft-aspnetcore-hosting-iwebhostenvironment-webrootfileprovider) and the [PhysicalFileProvider](/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider). Assign the composite file provider back to the app's [WebRootFileProvider](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment.webrootfileprovider#microsoft-aspnetcore-hosting-iwebhostenvironment-webrootfileprovider).

Example:

Create a new folder in the server project named `AdditionalStaticAssets`. Place an image into the folder.

Add the following `using` statement to the top of the server project's `Program` file:

```
using Microsoft.Extensions.FileProviders;
```

In the server project's `Program` file ***before*** any calls to [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) and [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles), add the following code:

```
var secondaryProvider = new PhysicalFileProvider(
    Path.Combine(builder.Environment.ContentRootPath, "AdditionalStaticAssets"));
app.Environment.WebRootFileProvider = new CompositeFileProvider(
    app.Environment.WebRootFileProvider, secondaryProvider);
```

In the app's `Home` component (`Home.razor`) markup, reference the image with an `<img>` tag:

```
<img src="{IMAGE FILE NAME}" alt="{ALT TEXT}" />
```

In the preceding example:

* The `{IMAGE FILE NAME}` placeholder is the image file name. There's no need to provide a path segment if the image file is at the root of the `AdditionalStaticAssets` folder.
* The `{ALT TEXT}` placeholder is the image alternate text.

Run the app.

## Related content

* [ASP.NET Core Blazor app base path](../host-and-deploy/app-base-path?view=aspnetcore-10.0)
* [Avoid file capture in a route parameter](routing?view=aspnetcore-10.0#avoid-file-capture-in-a-route-parameter)

- [ASP.NET Core Blazor app base path](../host-and-deploy/app-base-path?view=aspnetcore-10.0)
- [Avoid file capture in a route parameter](routing?view=aspnetcore-10.0#avoid-file-capture-in-a-route-parameter)
- [Multiple hosted ASP.NET Core Blazor WebAssembly apps](../host-and-deploy/webassembly/multiple-hosted-webassembly?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/authoring.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Author Tag Helpers in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/tag-helpers/authoring/samples) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Get started with Tag Helpers

This tutorial provides an introduction to programming Tag Helpers. [Introduction to Tag Helpers](intro?view=aspnetcore-10.0) describes the benefits that Tag Helpers provide.

A tag helper is any class that implements the `ITagHelper` interface. However, when you author a tag helper, you generally derive from `TagHelper`, doing so gives you access to the `Process` method.

1. Create a new ASP.NET Core project called **AuthoringTagHelpers**. You won't need authentication for this project.

2. Create a folder to hold the Tag Helpers called *TagHelpers*. The *TagHelpers* folder is *not* required, but it's a reasonable convention. Now let's get started writing some simple tag helpers.

## A minimal Tag Helper

In this section, you write a tag helper that updates an email tag. For example:

```
<email>Support</email>
```

The server will use our email tag helper to convert that markup into the following:

```
<a href="mailto:Support@contoso.com">Support@contoso.com</a>
```

That is, an anchor tag that makes this an email link. You might want to do this if you are writing a blog engine and need it to send email for marketing, support, and other contacts, all to the same domain.

1. Add the following `EmailTagHelper` class to the *TagHelpers* folder.

   ```

   using Microsoft.AspNetCore.Razor.TagHelpers;
   using System.Threading.Tasks;

   namespace AuthoringTagHelpers.TagHelpers
   {
       public class EmailTagHelper : TagHelper
       {
           public override void Process(TagHelperContext context, TagHelperOutput output)
           {
               output.TagName = "a";    // Replaces <email> with <a> tag
           }
       }
   }
   ```

   * Tag helpers use a naming convention that targets elements of the root class name (minus the *TagHelper* portion of the class name). In this example, the root name of **EmailTagHelper** is *email*, so the `<email>` tag will be targeted. This naming convention should work for most tag helpers, later on I'll show how to override it.

   * The `EmailTagHelper` class derives from `TagHelper`. The `TagHelper` class provides methods and properties for writing Tag Helpers.

   * The overridden `Process` method controls what the tag helper does when executed. The `TagHelper` class also provides an asynchronous version (`ProcessAsync`) with the same parameters.

   * The context parameter to `Process` (and `ProcessAsync`) contains information associated with the execution of the current HTML tag.

   * The output parameter to `Process` (and `ProcessAsync`) contains a stateful HTML element representative of the original source used to generate an HTML tag and content.

   * Our class name has a suffix of **TagHelper**, which is *not* required, but it's considered a best practice convention. You could declare the class as:

   ```
   public class Email : TagHelper
   ```

2. To make the `EmailTagHelper` class available to all our Razor views, add the `addTagHelper` directive to the `Views/_ViewImports.cshtml` file:

   ```
   @using AuthoringTagHelpers
   @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
   @addTagHelper *, AuthoringTagHelpers
   ```

   The code above uses the wildcard syntax to specify all the tag helpers in our assembly will be available. The first string after `@addTagHelper` specifies the tag helper to load (Use "\*" for all tag helpers), and the second string "AuthoringTagHelpers" specifies the assembly the tag helper is in. Also, note that the second line brings in the ASP.NET Core MVC tag helpers using the wildcard syntax (those helpers are discussed in [Introduction to Tag Helpers](intro?view=aspnetcore-10.0).) It's the `@addTagHelper` directive that makes the tag helper available to the Razor view. Alternatively, you can provide the fully qualified name (FQN) of a tag helper as shown below:

```
@using AuthoringTagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper AuthoringTagHelpers.TagHelpers.EmailTagHelper, AuthoringTagHelpers
```

To add a tag helper to a view using a FQN, you first add the FQN (`AuthoringTagHelpers.TagHelpers.EmailTagHelper`), and then the **assembly name** (*AuthoringTagHelpers*, not necessarily the `namespace`). Most developers will prefer to use the wildcard syntax. [Introduction to Tag Helpers](intro?view=aspnetcore-10.0) goes into detail on tag helper adding, removing, hierarchy, and wildcard syntax.

1. Update the markup in the `Views/Home/Contact.cshtml` file with these changes:

   ```
   @{
       ViewData["Title"] = "Contact";
   }
   <h2>@ViewData["Title"].</h2>
   <h3>@ViewData["Message"]</h3>

   <address>
       One Microsoft Way<br />
       Redmond, WA 98052<br />
       <abbr title="Phone">P:</abbr>
       425.555.0100
   </address>

   <address>
       <strong>Support:</strong><email>Support</email><br />
       <strong>Marketing:</strong><email>Marketing</email>
   </address>
   ```

2. Run the app and use your favorite browser to view the HTML source so you can verify that the email tags are replaced with anchor markup (For example, `<a>Support</a>`). *Support* and *Marketing* are rendered as a links, but they don't have an `href` attribute to make them functional. We'll fix that in the next section.

## SetAttribute and SetContent

In this section, we'll update the `EmailTagHelper` so that it will create a valid anchor tag for email. We'll update it to take information from a Razor view (in the form of a `mail-to` attribute) and use that in generating the anchor.

Update the `EmailTagHelper` class with the following:

```
public class EmailTagHelper : TagHelper
{
    private const string EmailDomain = "contoso.com";

    // Can be passed via <email mail-to="..." />. 
    // PascalCase gets translated into kebab-case.
    public string MailTo { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "a";    // Replaces <email> with <a> tag

        var address = MailTo + "@" + EmailDomain;
        output.Attributes.SetAttribute("href", "mailto:" + address);
        output.Content.SetContent(address);
    }
}
```

* Pascal-cased class and property names for tag helpers are translated into their [kebab case](https://stackoverflow.com/questions/11273282/whats-the-name-for-dash-separated-case/12273101). Therefore, to use the `MailTo` attribute, you'll use `<email mail-to="value"/>` equivalent.

* The last line sets the completed content for our minimally functional tag helper.

* The highlighted line shows the syntax for adding attributes:

```
public override void Process(TagHelperContext context, TagHelperOutput output)
{
    output.TagName = "a";    // Replaces <email> with <a> tag

    var address = MailTo + "@" + EmailDomain;
    output.Attributes.SetAttribute("href", "mailto:" + address);
    output.Content.SetContent(address);
}
```

That approach works for the attribute "href" as long as it doesn't currently exist in the attributes collection. You can also use the `output.Attributes.Add` method to add a tag helper attribute to the end of the collection of tag attributes.

1. Update the markup in the `Views/Home/Contact.cshtml` file with these changes:

   ```
   @{
       ViewData["Title"] = "Contact Copy";
   }
   <h2>@ViewData["Title"].</h2>
   <h3>@ViewData["Message"]</h3>

   <address>
       One Microsoft Way Copy Version <br />
       Redmond, WA 98052-6399<br />
       <abbr title="Phone">P:</abbr>
       425.555.0100
   </address>

   <address>
       <strong>Support:</strong><email mail-to="Support"></email><br />
       <strong>Marketing:</strong><email mail-to="Marketing"></email>
   </address>
   ```

2. Run the app and verify that it generates the correct links.

[]()

Note

If you were to write the email tag self-closing (`<email mail-to="Rick" />`), the final output would also be self-closing. To enable the ability to write the tag with only a start tag (`<email mail-to="Rick">`) you must mark the class with the following:

```
[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)] 
public class EmailVoidTagHelper : TagHelper
{
    private const string EmailDomain = "contoso.com";
    // Code removed for brevity
```

With a self-closing email tag helper, the output would be `<a href="mailto:Rick@contoso.com" />`. Self-closing anchor tags are not valid HTML, so you wouldn't want to create one, but you might want to create a tag helper that's self-closing. Tag helpers set the type of the `TagMode` property after reading a tag.

You can also map a different attribute name to a property using the [`[HtmlAttributeName]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.razor.taghelpers.htmlattributenameattribute).

To map an attribute named `recipient` to the `MailTo` property:

```
[HtmlAttributeName("recipient")]
public string? MailTo { get; set; }
```

Tag Helper for the `recipient` attribute:

```
<email recipient="…"/>
```

### ProcessAsync

In this section, we'll write an asynchronous email helper.

1. Replace the `EmailTagHelper` class with the following code:

   ```
   public class EmailTagHelper : TagHelper
   {
       private const string EmailDomain = "contoso.com";
       public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
       {
           output.TagName = "a";                                 // Replaces <email> with <a> tag
           var content = await output.GetChildContentAsync();
           var target = content.GetContent() + "@" + EmailDomain;
           output.Attributes.SetAttribute("href", "mailto:" + target);
           output.Content.SetContent(target);
       }
   }
   ```

   **Notes:**

   * This version uses the asynchronous `ProcessAsync` method. The asynchronous `GetChildContentAsync` returns a `Task` containing the `TagHelperContent`.

   * Use the `output` parameter to get contents of the HTML element.

2. Make the following change to the `Views/Home/Contact.cshtml` file so the tag helper can get the target email.

   ```
   @{
       ViewData["Title"] = "Contact";
   }
   <h2>@ViewData["Title"].</h2>
   <h3>@ViewData["Message"]</h3>

   <address>
       One Microsoft Way<br />
       Redmond, WA 98052<br />
       <abbr title="Phone">P:</abbr>
       425.555.0100
   </address>

   <address>
       <strong>Support:</strong><email>Support</email><br />
       <strong>Marketing:</strong><email>Marketing</email>
   </address>
   ```

3. Run the app and verify that it generates valid email links.

### RemoveAll, PreContent.SetHtmlContent and PostContent.SetHtmlContent

1. Add the following `BoldTagHelper` class to the *TagHelpers* folder.

   ```
   using Microsoft.AspNetCore.Razor.TagHelpers;

   namespace AuthoringTagHelpers.TagHelpers
   {
       [HtmlTargetElement(Attributes = "bold")]
       public class BoldTagHelper : TagHelper
       {
           public override void Process(TagHelperContext context, TagHelperOutput output)
           {
               output.Attributes.RemoveAll("bold");
               output.PreContent.SetHtmlContent("<strong>");
               output.PostContent.SetHtmlContent("</strong>");
           }
       }
   }
   ```

   * The `[HtmlTargetElement]` attribute passes an attribute parameter that specifies that any HTML element that contains an HTML attribute named "bold" will match, and the `Process` override method in the class will run. In our sample, the `Process` method removes the "bold" attribute and surrounds the containing markup with `<strong></strong>`.

   * Because you don't want to replace the existing tag content, you must write the opening `<strong>` tag with the `PreContent.SetHtmlContent` method and the closing `</strong>` tag with the `PostContent.SetHtmlContent` method.

2. Modify the `About.cshtml` view to contain a `bold` attribute value. The completed code is shown below.

   ```
   @{
       ViewData["Title"] = "About";
   }
   <h2>@ViewData["Title"].</h2>
   <h3>@ViewData["Message"]</h3>

   <p bold>Use this area to provide additional information.</p>

   <bold> Is this bold?</bold>
   ```

3. Run the app. You can use your favorite browser to inspect the source and verify the markup.

   The `[HtmlTargetElement]` attribute above only targets HTML markup that provides an attribute name of "bold". The `<bold>` element wasn't modified by the tag helper.

4. Comment out the `[HtmlTargetElement]` attribute line and it will default to targeting `<bold>` tags, that is, HTML markup of the form `<bold>`. Remember, the default naming convention will match the class name **Bold**TagHelper to `<bold>` tags.

5. Run the app and verify that the `<bold>` tag is processed by the tag helper.

Decorating a class with multiple `[HtmlTargetElement]` attributes results in a logical-OR of the targets. For example, using the code below, a bold tag or a bold attribute will match.

```
[HtmlTargetElement("bold")]
[HtmlTargetElement(Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.Attributes.RemoveAll("bold");
        output.PreContent.SetHtmlContent("<strong>");
        output.PostContent.SetHtmlContent("</strong>");
    }
}
```

When multiple attributes are added to the same statement, the runtime treats them as a logical-AND. For example, in the code below, an HTML element must be named "bold" with an attribute named "bold" (`<bold bold />`) to match.

```
[HtmlTargetElement("bold", Attributes = "bold")]
```

You can also use the `[HtmlTargetElement]` to change the name of the targeted element. For example if you wanted the `BoldTagHelper` to target `<MyBold>` tags, you would use the following attribute:

```
[HtmlTargetElement("MyBold")]
```

## Pass a model to a Tag Helper

1. Add a *Models* folder.

2. Add the following `WebsiteContext` class to the *Models* folder:

   ```
   using System;

   namespace AuthoringTagHelpers.Models
   {
       public class WebsiteContext
       {
           public Version Version { get; set; }
           public int CopyrightYear { get; set; }
           public bool Approved { get; set; }
           public int TagsToShow { get; set; }
       }
   }
   ```

3. Add the following `WebsiteInformationTagHelper` class to the *TagHelpers* folder.

   ```
   using System;
   using AuthoringTagHelpers.Models;
   using Microsoft.AspNetCore.Razor.TagHelpers;

   namespace AuthoringTagHelpers.TagHelpers
   {
       public class WebsiteInformationTagHelper : TagHelper
       {
           public WebsiteContext Info { get; set; }

         public override void Process(TagHelperContext context, TagHelperOutput output)
         {
            output.TagName = "section";
            output.Content.SetHtmlContent(
   $@"<ul><li><strong>Version:</strong> {Info.Version}</li>
   <li><strong>Copyright Year:</strong> {Info.CopyrightYear}</li>
   <li><strong>Approved:</strong> {Info.Approved}</li>
   <li><strong>Number of tags to show:</strong> {Info.TagsToShow}</li></ul>");
            output.TagMode = TagMode.StartTagAndEndTag;
         }
      }
   }
   ```

   * As mentioned previously, tag helpers translates Pascal-cased C# class names and properties for tag helpers into [kebab case](https://wiki.c2.com/?KebabCase). Therefore, to use the `WebsiteInformationTagHelper` in Razor, you'll write `<website-information />`.

   * You are not explicitly identifying the target element with the `[HtmlTargetElement]` attribute, so the default of `website-information` will be targeted. If you applied the following attribute (note it's not kebab case but matches the class name):

   ```
   [HtmlTargetElement("WebsiteInformation")]
   ```

   The kebab case tag `<website-information />` wouldn't match. If you want use the `[HtmlTargetElement]` attribute, you would use kebab case as shown below:

   ```
   [HtmlTargetElement("Website-Information")]
   ```

   * Elements that are self-closing have no content. For this example, the Razor markup will use a self-closing tag, but the tag helper will be creating a [section](https://www.w3.org/TR/html5/sections.html#the-section-element) element (which isn't self-closing and you are writing content inside the `section` element). Therefore, you need to set `TagMode` to `StartTagAndEndTag` to write output. Alternatively, you can comment out the line setting `TagMode` and write markup with a closing tag. (Example markup is provided later in this tutorial.)

   * The `$` (dollar sign) in the following line uses an [interpolated string](/en-us/dotnet/csharp/language-reference/keywords/interpolated-strings):

   ```
   $@"<ul><li><strong>Version:</strong> {Info.Version}</li>
   ```

4. Add the following markup to the `About.cshtml` view. The highlighted markup displays the web site information.

   ```
   @using AuthoringTagHelpers.Models
   @{
       ViewData["Title"] = "About";
       WebsiteContext webContext = new WebsiteContext {
                                       Version = new Version(1, 3),
                                       CopyrightYear = 1638,
                                       Approved = true,
                                       TagsToShow = 131 };
   }
   <h2>@ViewData["Title"].</h2>
   <h3>@ViewData["Message"]</h3>

   <p bold>Use this area to provide additional information.</p>

   <bold> Is this bold?</bold>

   <h3> web site info </h3>
   <website-information info="webContext" />
   ```

   Note

   In the Razor markup shown below:

   ```
   <website-information info="webContext" />
   ```

   Razor knows the `info` attribute is a class, not a string, and you want to write C# code. Any non-string tag helper attribute should be written without the `@` character.

5. Run the app, and navigate to the About view to see the web site information.

   Note

   You can use the following markup with a closing tag and remove the line with `TagMode.StartTagAndEndTag` in the tag helper:

   ```
   <website-information info="webContext" >
   </website-information>
   ```

## Condition Tag Helper

The condition tag helper renders output when passed a true value.

1. Add the following `ConditionTagHelper` class to the *TagHelpers* folder.

   ```
   using Microsoft.AspNetCore.Razor.TagHelpers;

   namespace AuthoringTagHelpers.TagHelpers
   {
       [HtmlTargetElement(Attributes = nameof(Condition))]
       public class ConditionTagHelper : TagHelper
       {
           public bool Condition { get; set; }

           public override void Process(TagHelperContext context, TagHelperOutput output)
           {
               if (!Condition)
               {
                   output.SuppressOutput();
               }
           }
       }
   }
   ```

2. Replace the contents of the `Views/Home/Index.cshtml` file with the following markup:

   ```
   @using AuthoringTagHelpers.Models
   @model WebsiteContext

   @{
       ViewData["Title"] = "Home Page";
   }

   <div>
       <h3>Information about our website (outdated):</h3>
       <Website-InforMation info="Model" />
       <div condition="Model.Approved">
           <p>
               This website has <strong surround="em">@Model.Approved</strong> been approved yet.
               Visit www.contoso.com for more information.
           </p>
       </div>
   </div>
   ```

3. Replace the `Index` method in the `Home` controller with the following code:

   ```
   public IActionResult Index(bool approved = false)
   {
       return View(new WebsiteContext
       {
           Approved = approved,
           CopyrightYear = 2015,
           Version = new Version(1, 3, 3, 7),
           TagsToShow = 20
       });
   }
   ```

4. Run the app and browse to the home page. The markup in the conditional `div` won't be rendered. Append the query string `?approved=true` to the URL (for example, `http://localhost:1235/Home/Index?approved=true`). `approved` is set to true and the conditional markup will be displayed.

Note

Use the [nameof](/en-us/dotnet/csharp/language-reference/keywords/nameof) operator to specify the attribute to target rather than specifying a string as you did with the bold tag helper:

```
[HtmlTargetElement(Attributes = nameof(Condition))]
 //   [HtmlTargetElement(Attributes = "condition")]
 public class ConditionTagHelper : TagHelper
{
   public bool Condition { get; set; }

   public override void Process(TagHelperContext context, TagHelperOutput output)
   {
      if (!Condition)
      {
         output.SuppressOutput();
      }
   }
}
```

The [nameof](/en-us/dotnet/csharp/language-reference/keywords/nameof) operator will protect the code should it ever be refactored (we might want to change the name to `RedCondition`).

### Avoid Tag Helper conflicts

In this section, you write a pair of auto-linking tag helpers. The first will replace markup containing a URL starting with HTTP to an HTML anchor tag containing the same URL (and thus yielding a link to the URL). The second will do the same for a URL starting with WWW.

Because these two helpers are closely related and you may refactor them in the future, we'll keep them in the same file.

1. Add the following `AutoLinkerHttpTagHelper` class to the *TagHelpers* folder.

   ```
   [HtmlTargetElement("p")]
   public class AutoLinkerHttpTagHelper : TagHelper
   {
       public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
       {
           var childContent = await output.GetChildContentAsync();
           // Find Urls in the content and replace them with their anchor tag equivalent.
           output.Content.SetHtmlContent(Regex.Replace(
                childContent.GetContent(),
                @"\b(?:https?://)(\S+)\b",
                 "<a target=\"_blank\" href=\"$0\">$0</a>"));  // http link version}
       }
   }
   ```

   Note

   The `AutoLinkerHttpTagHelper` class targets `p` elements and uses [Regex](/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference) to create the anchor.

2. Add the following markup to the end of the `Views/Home/Contact.cshtml` file:

   ```
   @{
       ViewData["Title"] = "Contact";
   }
   <h2>@ViewData["Title"].</h2>
   <h3>@ViewData["Message"]</h3>

   <address>
       One Microsoft Way<br />
       Redmond, WA 98052<br />
       <abbr title="Phone">P:</abbr>
       425.555.0100
   </address>

   <address>
       <strong>Support:</strong><email>Support</email><br />
       <strong>Marketing:</strong><email>Marketing</email>
   </address>

   <p>Visit us at http://docs.asp.net or at www.microsoft.com</p>
   ```

3. Run the app and verify that the tag helper renders the anchor correctly.

4. Update the `AutoLinker` class to include the `AutoLinkerWwwTagHelper` which will convert www text to an anchor tag that also contains the original www text. The updated code is highlighted below:

   ```
       [HtmlTargetElement("p")]
       public class AutoLinkerHttpTagHelper : TagHelper
       {
           public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
           {
               var childContent = await output.GetChildContentAsync();
               // Find Urls in the content and replace them with their anchor tag equivalent.
               output.Content.SetHtmlContent(Regex.Replace(
                    childContent.GetContent(),
                    @"\b(?:https?://)(\S+)\b",
                     "<a target=\"_blank\" href=\"$0\">$0</a>"));  // http link version}
           }
       }

       [HtmlTargetElement("p")]
       public class AutoLinkerWwwTagHelper : TagHelper
       {
           public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
           {
               var childContent = await output.GetChildContentAsync();
               // Find Urls in the content and replace them with their anchor tag equivalent.
               output.Content.SetHtmlContent(Regex.Replace(
                   childContent.GetContent(),
                    @"\b(www\.)(\S+)\b",
                    "<a target=\"_blank\" href=\"http://$0\">$0</a>"));  // www version
           }
       }
   }
   ```

5. Run the app. Notice the www text is rendered as a link but the HTTP text isn't. If you put a break point in both classes, you can see that the HTTP tag helper class runs first. The problem is that the tag helper output is cached, and when the WWW tag helper is run, it overwrites the cached output from the HTTP tag helper. Later in the tutorial we'll see how to control the order that tag helpers run in. We'll fix the code with the following:

   ```
     public class AutoLinkerHttpTagHelper : TagHelper
     {
         public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
         {
             var childContent = output.Content.IsModified ? output.Content.GetContent() :
                 (await output.GetChildContentAsync()).GetContent();

             // Find Urls in the content and replace them with their anchor tag equivalent.
             output.Content.SetHtmlContent(Regex.Replace(
                  childContent,
                  @"\b(?:https?://)(\S+)\b",
                   "<a target=\"_blank\" href=\"$0\">$0</a>"));  // http link version}
         }
     }

     [HtmlTargetElement("p")]
     public class AutoLinkerWwwTagHelper : TagHelper
     {
         public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
         {
             var childContent = output.Content.IsModified ? output.Content.GetContent() : 
                 (await output.GetChildContentAsync()).GetContent();

             // Find Urls in the content and replace them with their anchor tag equivalent.
             output.Content.SetHtmlContent(Regex.Replace(
                  childContent,
                  @"\b(www\.)(\S+)\b",
                  "<a target=\"_blank\" href=\"http://$0\">$0</a>"));  // www version
         }
     }
   ```

   Note

   In the first edition of the auto-linking tag helpers, you got the content of the target with the following code:

   ```
   var childContent = await output.GetChildContentAsync();
   ```

   That is, you call `GetChildContentAsync` using the `TagHelperOutput` passed into the `ProcessAsync` method. As mentioned previously, because the output is cached, the last tag helper to run wins. You fixed that problem with the following code:

   ```
   var childContent = output.Content.IsModified ? output.Content.GetContent() : 
       (await output.GetChildContentAsync()).GetContent();
   ```

   The code above checks to see if the content has been modified, and if it has, it gets the content from the output buffer.

6. Run the app and verify that the two links work as expected. While it might appear our auto linker tag helper is correct and complete, it has a subtle problem. If the WWW tag helper runs first, the www links won't be correct. Update the code by adding the `Order` overload to control the order that the tag runs in. The `Order` property determines the execution order relative to other tag helpers targeting the same element. The default order value is zero and instances with lower values are executed first.

   ```
   public class AutoLinkerHttpTagHelper : TagHelper
   {
       // This filter must run before the AutoLinkerWwwTagHelper as it searches and replaces http and 
       // the AutoLinkerWwwTagHelper adds http to the markup.
       public override int Order
       {
           get  {  return int.MinValue;   }
       }
   ```

   The preceding code guarantees that the HTTP tag helper runs before the WWW tag helper. Change `Order` to `MaxValue` and verify that the markup generated for the WWW tag is incorrect.

## Inspect and retrieve child content

The tag helpers provide several properties to retrieve content.

* The result of `GetChildContentAsync` can be appended to `output.Content`.
* You can inspect the result of `GetChildContentAsync` with `GetContent`.
* If you modify `output.Content`, the TagHelper body won't be executed or rendered unless you call `GetChildContentAsync` as in our auto-linker sample:

```
public class AutoLinkerHttpTagHelper : TagHelper
{
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var childContent = output.Content.IsModified ? output.Content.GetContent() :
            (await output.GetChildContentAsync()).GetContent();

        // Find Urls in the content and replace them with their anchor tag equivalent.
        output.Content.SetHtmlContent(Regex.Replace(
             childContent,
             @"\b(?:https?://)(\S+)\b",
              "<a target=\"_blank\" href=\"$0\">$0</a>"));  // http link version}
    }
}
```

* Multiple calls to `GetChildContentAsync` returns the same value and doesn't re-execute the `TagHelper` body unless you pass in a false parameter indicating not to use the cached result.

## Load minified partial view TagHelper

In production environments, performance can be improved by loading minified partial views. To take advantage of minified partial view in production:

* Create/set up a pre-build process that minifies partial views.
* Use the following code to load minified partial views in non-development environments.

```
public class MinifiedVersionPartialTagHelper : PartialTagHelper
    {
        public MinifiedVersionPartialTagHelper(ICompositeViewEngine viewEngine, 
                                IViewBufferScope viewBufferScope)
                               : base(viewEngine, viewBufferScope)
        {

        }

        public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            // Append ".min" to load the minified partial view.
            if (!IsDevelopment())
            {
                Name += ".min";
            }

            return base.ProcessAsync(context, output);
        }

        private bool IsDevelopment()
        {
            return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") 
                                                 == EnvironmentName.Development;
        }
    }
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/analyzers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/web-api/advanced/analyzers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use web API analyzers

Warning

The `IncludeOpenAPIAnalyzers` MSBuild property and its associated MVC API analyzers are deprecated as of .NET 10 and will be removed in a future release. When `IncludeOpenAPIAnalyzers` is set to `true`, the build emits warning `ASPDEPR007`. Migrate to the [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) pattern, which provides compile-time response-type guarantees without separate analyzers. For more information, see [IncludeOpenAPIAnalyzers property and MVC API analyzers are deprecated](/en-us/aspnet/core/breaking-changes/10/openapi-analyzers-deprecated?view=aspnetcore-10.0\&preserve-view=true).

ASP.NET Core provides an MVC analyzers package intended for use with web API projects. The analyzers work with controllers annotated with [ApiControllerAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute), while building on [web API conventions](conventions?view=aspnetcore-10.0).

The analyzers package notifies you of any controller action that:

* Returns an undeclared status code.
* Returns an undeclared success result.
* Documents a status code that isn't returned.
* Includes an explicit model validation check.

## Reference the analyzer package

The analyzers are included in the .NET SDK. To enable the analyzer in your project, include the `IncludeOpenAPIAnalyzers` property in the project file:

```
<PropertyGroup>
 <IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
</PropertyGroup>
```

## Analyzers for web API conventions

OpenAPI documents contain status codes and response types that an action may return. In ASP.NET Core MVC, attributes such as [ProducesResponseTypeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute) and [ProducesAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) are used to document an action. [ASP.NET Core web API documentation with Swagger / OpenAPI](../../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0) goes into further detail on documenting your web API.

One of the analyzers in the package inspects controllers annotated with [ApiControllerAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) and identifies actions that don't entirely document their responses. Consider the following example:

```
// GET api/contacts/{guid}
[HttpGet("{id}", Name = "GetById")]
[ProducesResponseType(typeof(Contact), StatusCodes.Status200OK)]
public IActionResult Get(string id)
{
    var contact = _contacts.Get(id);

    if (contact == null)
    {
        return NotFound();
    }

    return Ok(contact);
}
```

The preceding action documents the HTTP 200 success return type but doesn't document the HTTP 404 failure status code. The analyzer reports the missing documentation for the HTTP 404 status code as a warning. An option to fix the problem is provided.

## Analyzers require Microsoft.NET.Sdk.Web

Analyzers don't work with library projects or projects referencing `Sdk="Microsoft.NET.Sdk"`.

## Related content

* [Use web API conventions](conventions?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](../../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)
* [Create web APIs with ASP.NET Core](../?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/web-sdk
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/web-sdk.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Web SDK

## Overview

`Microsoft.NET.Sdk.Web` is an [MSBuild project SDK](/en-us/visualstudio/msbuild/how-to-use-project-sdk) for building ASP.NET Core apps. It's possible to build an ASP.NET Core app without this SDK, however, the Web SDK is:

* Tailored towards providing a first-class experience.
* The recommended target for most users.

Use the Web.SDK in a project:

```
<Project Sdk="Microsoft.NET.Sdk.Web">
  <!-- omitted for brevity -->
</Project>
```

Features enabled by using the Web SDK:

* Implicitly references:

  * The [ASP.NET Core shared framework](../fundamentals/metapackage-app?view=aspnetcore-10.0).
  * [Analyzers](/en-us/visualstudio/extensibility/getting-started-with-roslyn-analyzers) designed for building ASP.NET Core apps.

* The Web SDK imports MSBuild targets that enable the use of publish profiles and publishing using WebDeploy.

## Properties

| Property                             | Description                                                                                           |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `DisableImplicitFrameworkReferences` | Disables implicit reference to the `Microsoft.AspNetCore.App` shared framework.                       |
| `DisableImplicitAspNetCoreAnalyzers` | Disables implicit reference to ASP.NET Core analyzers.                                                |
| `DisableImplicitComponentsAnalyzers` | Disables implicit reference to Razor Components analyzers when building Blazor (server) applications. |

For more information on tasks, targets, properties, implicit blobs, globs, publishing, methods, and more, see the [README file](https://github.com/dotnet/sdk/tree/main/src/WebSdk) in the [WebSdk](https://github.com/dotnet/sdk/tree/main/src/WebSdk) repository.

### Overview

`Microsoft.NET.Sdk.Web` is an [MSBuild project SDK](/en-us/visualstudio/msbuild/how-to-use-project-sdk) for building ASP.NET Core apps. It's possible to build an ASP.NET Core app without this SDK, however, the Web SDK is:

* Tailored towards providing a first-class experience.
* The recommended target for most users.

Use the Web.SDK in a project:

```
<Project Sdk="Microsoft.NET.Sdk.Web">
  <!-- omitted for brevity -->
</Project>
```

The Web SDK imports MSBuild targets that enable the use of publish profiles and publishing using WebDeploy.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-04-10

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/security/security-considerations
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/security/security-considerations.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor Hybrid security considerations

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes security considerations for Blazor Hybrid apps.

Blazor Hybrid apps that render web content execute .NET code inside a platform Web View. The .NET code interacts with the web content via an interop channel between the .NET code and the Web View.

The web content rendered into the Web View can come from assets provided by the app from either of the following locations:

* The `wwwroot` folder in the app.
* A source external to the app. For example, a network source, such as the Internet.

A trust boundary exists between the .NET code and the code that runs inside the Web View. .NET code is provided by the app and any trusted third-party packages that you've installed. After the app is built, the .NET code Web View content sources can't change.

In contrast to the .NET code sources of content, content sources from the code that runs inside the Web View can come not only from the app but also from external sources. For example, static assets from an external Content Delivery Network (CDN) might be used or rendered by an app's Web View.

Consider the code inside the Web View as **untrusted** in the same way that code running inside the browser for a web app isn't trusted. The same threats and general security recommendations apply to untrusted resources in Blazor Hybrid apps as for other types of apps.

If possible, avoid loading content from a third-party origin. To mitigate risk, you might be able to serve content directly from the app by downloading the external assets, verifying that they're safe to serve to users, and placing them into the app's `wwwroot` folder for packaging with the rest of the app. When the external content is downloaded for inclusion in the app, we recommend scanning it for viruses and malware before placing it into the `wwwroot` folder of the app.

If your app must reference content from an external origin, we recommend that you use common web security approaches to provide the app with an opportunity to block the content from loading if the content is compromised:

* Serve content securely with TLS/HTTPS.
* Institute a [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP). For more information, see [Enforce a Content Security Policy for ASP.NET Core Blazor](../../security/content-security-policy?view=aspnetcore-10.0).
* Perform [Subresource Integrity (SRI)](https://developer.mozilla.org/docs/Web/Security/Subresource_Integrity) checks.

Even if all of the resources are packed into the app and don't load from any external origin, remain cautious about problems in the resources' code that run inside the Web View, as the resources might have vulnerabilities that could allow [cross-site scripting (XSS)](../../security/interactive-server-side-rendering?view=aspnetcore-10.0#cross-site-scripting-xss) attacks.

In general, the Blazor framework protects against XSS by dealing with HTML in safe ways. However, some programming patterns allow Razor components to inject raw HTML into rendered output, such as rendering content from an untrusted source. For example, rendering HTML content directly from a database should be avoided. Additionally, JavaScript libraries used by the app might manipulate HTML in unsafe ways to inadvertently or deliberately render unsafe output.

For these reasons, it's best to apply the same protections against XSS that are normally applied to web apps. Prevent loading scripts from unknown sources and don't implement potentially unsafe JavaScript features, such as [`eval`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/eval) and other unsafe JavaScript primitives. Establishing a CSP is recommended to reduce these security risks.

If the code inside the Web View is compromised, the code gains access to all of the content inside the Web View and might interact with the host via the interop channel. For that reason, any content coming from the Web View (events, JS interop) must be treated as **untrusted** and validated in the same way as for other sensitive contexts, such as in a compromised Blazor Server app that can lead to malicious attacks on the host system.

Don't store sensitive information, such as credentials, security tokens, or sensitive user data, in the context of the Web View, as it makes the information available to a cyberattacker if the Web View is compromised. There are safer alternatives, such as handling the sensitive information directly within the native portion of the app.

## External content rendered in an `iframe`

When using an [`iframe`](https://developer.mozilla.org/docs/Web/HTML/Element/iframe) to display external content within a Blazor Hybrid page, we recommend that users leverage [sandboxing features](https://developer.mozilla.org/docs/Web/HTML/Element/iframe) to ensure that the content is isolated from the parent page containing the app. In the following Razor component example, the [`sandbox` attribute](https://developer.mozilla.org/docs/Web/HTML/Element/iframe) is present for the `<iframe>` tag to apply sandboxing features to the `admin.html` page:

```
<iframe sandbox src="https://contoso.com/admin.html" />
```

Warning

The [`sandbox` attribute](https://developer.mozilla.org/docs/Web/HTML/Element/iframe) isn't supported in early browser versions. For more information, see [Can I use: `sandbox`](https://caniuse.com/?search=sandbox).

## Links to external URLs

Links to URLs outside of the app are opened in an appropriate external app, not loaded within the Web View. We don't recommend overriding the default behavior.

## Keep the Web View current in deployed apps

The [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) control uses the currently-installed, platform-specific native Web View. Since the native Web View is periodically updated with support for new APIs and fixes for security issues, it may be necessary to ensure that an app is using a Web View version that meets the app's requirements.

Use one of the following approaches to keep the Web View current in deployed apps:

* **On all platforms**: Check the Web View version and prompt the user to take any necessary steps to update it.
* **Only on Windows**: Package a fixed-version Web View within the app, using it in place of the system's shared Web View. For more information, see the [Use and distribute a fixed version of the Windows `WebView2` Runtime](#use-and-distribute-a-fixed-version-of-the-windows-webview2-runtime) section.

### Android

The Android Web View is distributed and updated via the [Google Play Store](https://play.google.com/store/apps/details?id=com.google.android.webview). Check the Web View version by reading the [`User-Agent`](https://developer.mozilla.org/docs/Web/HTTP/Headers/User-Agent) string. Read the Web View's [`navigator.userAgent`](https://developer.mozilla.org/docs/Web/API/Navigator/userAgent) property using [JavaScript interop](../../javascript-interoperability/?view=aspnetcore-10.0) and optionally cache the value using a singleton service if the user agent string is required outside of a Razor component context.

When using the Android Emulator:

* Use an emulated device with **Google Play Services** preinstalled. Emulated devices without Google Play Services preinstalled aren't supported.
* Install Google Chrome from the Google Play Store. If Google Chrome is already installed, [update Chrome from the Google Play Store](https://support.google.com/chrome/answer/95414?hl=en\&co=GENIE.Platform%3DAndroid). If an emulated device doesn't have the latest version of Chrome installed, it might not have the latest version of the Android Web View installed.

### iOS/Mac Catalyst

iOS and Mac Catalyst both use [`WKWebView`](https://developer.apple.com/documentation/webkit/wkwebview), a Safari-based control, which is updated by the operating system. Similar to the [Android](#android) case, determine the Web View version by reading the Web View's [`User-Agent`](https://developer.mozilla.org/docs/Web/HTTP/Headers/User-Agent) string.

### Windows (.NET MAUI, WPF, Windows Forms)

On Windows, the Chromium-based [Microsoft Edge `WebView2`](/en-us/microsoft-edge/webview2/) is required for Blazor Hybrid/`BlazorWebView` apps.

The newest installed version of `WebView2`, known as the *Evergreen distribution*, is used. If you wish to ship a specific version of `WebView2` with the app, use the *Fixed Version distribution* (see the [Use and distribute a fixed version of the Windows `WebView2` Runtime](#use-and-distribute-a-fixed-version-of-the-windows-webview2-runtime) section).

For more information on checking the currently-installed `WebView2` version and the distribution modes, see the [`WebView2` distribution documentation](/en-us/microsoft-edge/webview2/concepts/distribution).

## Use and distribute a fixed version of the Windows `WebView2` Runtime

Follow the guidance in this section to use and distribute a fixed version of the Chromium-based [Microsoft Edge `WebView2`](/en-us/microsoft-edge/webview2/) Runtime for Blazor Hybrid/`BlazorWebView` apps.

Download the **Fixed Version** installer packages from [Microsoft Edge WebView2: Download the WebView2 Runtime](https://developer.microsoft.com/microsoft-edge/webview2).

The following steps place the packages into `Runtimex86`, `Runtimex64`, and `RuntimeARM64` folders in the parent folder of the app's solution folder, maintaining the files outside of the project to keep the **Solution Explorer** view of the project focused on the app's developer assets.

After downloading a runtime, use the [`expand` command](/en-us/windows-server/administration/windows-commands/expand) in a command shell to expand the `.cab` file from the system's `C:\Users\{USER}\Downloads` folder into the solution's root folder:

```
expand {PATH TO THE PACKAGE} -F:* {PATH TO THE DESTINATION FOLDER}
```

Placeholders:

* `{PATH TO THE PACKAGE}`: The path to the package.
* `{PATH TO THE DESTINATION FOLDER}`: The path to the destination folder, which is the project's solution folder.

The following example uses:

* The x86 runtime package `.cab` file (`Microsoft.WebView2.FixedVersionRuntime.114.0.1823.79.x86.cab`).
* A package path of `C:\Users\{USER}\Downloads\`, where the `{USER}` placeholder is the Windows user profile name.
* A solution folder path of `C:\src\MySolution\`.

```
expand "C:\Users\{USER}\Downloads\Microsoft.WebView2.FixedVersionRuntime.114.0.1823.79.x86.cab" -F:* C:\src\MySolution\
```

Rename the folder to `Runtimex86` with the [`Rename-Item` PowerShell cmdlet](/en-us/powershell/module/microsoft.powershell.management/rename-item):

```
rename-item "C:\src\MySolution\Microsoft.WebView2.FixedVersionRuntime.114.0.1823.79.x86" "C:\src\MySolution\Runtimex86"
```

Repeat the preceding steps for the x64 and ARM64 runtimes.

Add the following code to the `CreateMauiApp` method in `MauiProgram.cs`:

```
#if WINDOWS
    string relativePath = @"Runtime";
    string basePath = AppContext.BaseDirectory;
    string wvrPath = Path.Combine(basePath, relativePath);
    Environment.SetEnvironmentVariable("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", wvrPath);
#endif
```

In the app's project file (`.csproj`), add the following after the existing `<MauiAsset>` element:

```
<MauiAsset Include="..\Runtimex86\**" 
           LogicalName="Runtime\%(RecursiveDir)%(Filename)%(Extension)" 
           Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows' And ('$(PlatformTarget)' == 'x86')" />
<MauiAsset Include="..\Runtimex64\**" 
           LogicalName="Runtime\%(RecursiveDir)%(Filename)%(Extension)" 
           Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows' And ('$(PlatformTarget)' == 'x64')" />
<MauiAsset Include="..\RuntimeARM64\**" 
           LogicalName="Runtime\%(RecursiveDir)%(Filename)%(Extension)" 
           Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows' And ('$(PlatformTarget)' == 'ARM64')" />
```

## Related content

* [ASP.NET Core Blazor Hybrid authentication and authorization](./?view=aspnetcore-10.0)
* [ASP.NET Core Blazor authentication and authorization](../../security/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-9.0.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# What's new in ASP.NET Core in .NET 9

This article highlights the most significant changes in ASP.NET Core in .NET 9 with links to relevant documentation.

## Static asset delivery optimization

[`MapStaticAssets` routing endpoint conventions](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) is a new feature that optimizes the delivery of static assets in ASP.NET Core apps.

For information on static asset delivery for Blazor apps, see [ASP.NET Core Blazor static files](../blazor/fundamentals/static-files?view=aspnetcore-9.0).

Following production best practices for serving static assets requires a significant amount of work and technical expertise. Without optimizations like compression, caching, and [fingerprints](https://en.wikipedia.org/wiki/Fingerprint_\(computing\)):

* The browser has to make additional requests on every page load.
* More bytes than necessary are transferred through the network.
* Sometimes stale versions of files are served to clients.

Creating performant web apps requires optimizing asset delivery to the browser. Possible optimizations include:

* Serve a given asset once until the file changes or the browser clears its cache. Set the [ETag](https://developer.mozilla.org/docs/Web/HTTP/Headers/ETag) header.
* Prevent the browser from using old or stale assets after an app is updated. Set the [Last-Modified](https://developer.mozilla.org/docs/Web/HTTP/Headers/Last-Modified) header.
* Set up proper [caching headers](https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control).
* Use [caching middleware](../performance/caching/middleware?view=aspnetcore-10.0).
* Serve [compressed](/en-us/aspnet/core/performance/response-compression) versions of the assets when possible.
* Use a [CDN](/en-us/microsoft-365/enterprise/content-delivery-networks?view=o365-worldwide\&preserve-view=true) to serve the assets closer to the user.
* Minimize the size of assets served to the browser. This optimization doesn't include minification.

[MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets) is a new feature that optimizes the delivery of static assets in an app. It's designed to work with all UI frameworks, including Blazor, Razor Pages, and MVC. It's typically a drop-in replacement for [`UseStaticFiles`](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

+app.MapStaticAssets();
-app.UseStaticFiles();
app.MapRazorPages();

app.Run();
```

`MapStaticAssets` operates by combining build and publish-time processes to collect information about all the static resources in an app. This information is then utilized by the runtime library to efficiently serve these files to the browser.

`MapStaticAssets` can replace [`UseStaticFiles`](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) in most situations, however, it's optimized for serving the assets that the app has knowledge of at build and publish time. If the app serves assets from other locations, such as disk or embedded resources, `UseStaticFiles` should be used.

`MapStaticAssets` provides the following benefits not found with `UseStaticFiles`:

* Build time compression for all the assets in the app:

  * `gzip` during development and `gzip + brotli` during publish.
  * All assets are compressed with the goal of reducing the size of the assets to the minimum.

* Content based `ETags`: The `Etags` for each resource are the [Base64](https://developer.mozilla.org/docs/Glossary/Base64) encoded string of the [SHA-256](/en-us/dotnet/api/system.security.cryptography.sha256?view=net-8.0\&preserve-view=true) hash of the content. This ensures that the browser only redownloads a file if its contents have changed.

The following table shows the original and compressed sizes of the CSS and JS files in the default Razor Pages template.

| File              | Original | Compressed | % Reduction |
| ----------------- | -------- | ---------- | ----------- |
| bootstrap.min.css | 163      | 17.5       | 89.26%      |
| jquery.js         | 89.6     | 28         | 68.75%      |
| bootstrap.min.js  | 78.5     | 20         | 74.52%      |
| **Total**         | 331.1    | 65.5       | 80.20%      |

The following table shows the original and compressed sizes using the [Fluent UI Blazor components library](https://www.fluentui-blazor.net/).

| File       | Original | Compressed | % Reduction |
| ---------- | -------- | ---------- | ----------- |
| fluent.js  | 384      | 73         | 80.99%      |
| fluent.css | 94       | 11         | 88.30%      |
| **Total**  | 478      | 84         | 82.43%      |

For a total of 478 KB uncompressed to 84 KB compressed.

The following table shows the original and compressed sizes using the [MudBlazor](https://mudblazor.com) Blazor components library.

| File              | Original | Compressed | Reduction |
| ----------------- | -------- | ---------- | --------- |
| MudBlazor.min.css | 541      | 37.5       | 93.07%    |
| MudBlazor.min.js  | 47.4     | 9.2        | 80.59%    |
| **Total**         | 588.4    | 46.7       | 92.07%    |

Optimization happens automatically when using `MapStaticAssets`. When a library is added or updated, for example with new JavaScript or CSS, the assets are optimized as part of the build. Optimization is especially beneficial to mobile environments that can have a lower bandwidth or an unreliable connections.

For more information on the new file delivery features, see the following resources:

* [Serve static files in ASP.NET Core apps](../fundamentals/static-files?view=aspnetcore-9.0)
* [ASP.NET Core Blazor static files](../blazor/fundamentals/static-files?view=aspnetcore-9.0)

### Customize build-time compression with MSBuild properties

Several MSBuild properties can be used to customize or disable build-time compression behavior:

* `CompressionEnabled`: Completely disables compression when set to `false`.
* `CompressionExcludePatterns`: Semicolon-delimited list of glob patterns (for example, `**\*.js`) to exclude specific files from being considered for compression.

The following example excludes JavaScript files from compression:

```
<PropertyGroup>
  <CompressionExcludePatterns>$(CompressionExcludePatterns);**\*.js</CompressionExcludePatterns>
</PropertyGroup>
```

To completely disable compression:

```
<PropertyGroup>
  <CompressionEnabled>false</CompressionEnabled>
</PropertyGroup>
```

For more information, see [Serve static files in ASP.NET Core apps](../fundamentals/static-files?view=aspnetcore-9.0#msbuild-properties).

### Enabling dynamic compression on the server vs using `MapStaticAssets`

`MapStaticAssets` has the following advantages over dynamic compression on the server:

* Is simpler because there is no server specific configuration.
* Is more performant because the assets are compressed at build time.
* Allows the developer to spend extra time during the build process to ensure that the assets are the minimum size.

Consider the following table comparing MudBlazor compression with IIS dynamic compression and `MapStaticAssets`.

| IIS gzip | `MapStaticAssets` | `MapStaticAssets` reduction |
| -------- | ----------------- | --------------------------- |
| ≅ 90     | 37.5              | 59%                         |

## Blazor

This section describes new features for Blazor.

### .NET MAUI Blazor Hybrid and Web App solution template

A new solution template makes it easier to create .NET MAUI native and Blazor web client apps that share the same UI. This template shows how to create client apps that maximize code reuse and target Android, iOS, Mac, Windows, and Web.

Key features of this template include:

* The ability to choose a Blazor interactive render mode for the web app.
* Automatic creation of the appropriate projects, including a Blazor Web App (global Interactive Auto rendering) and a .NET MAUI Blazor Hybrid app.
* The created projects use a shared Razor class library (RCL) to maintain the UI's Razor components.
* Sample code is included that demonstrates how to use dependency injection to provide different interface implementations for the Blazor Hybrid app and the Blazor Web App.

To get started, install the [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) and install the .NET MAUI workload, which contains the template:

```
dotnet workload install maui
```

Create a solution from the project template in a command shell using the following command:

```
dotnet new maui-blazor-web
```

The template is also available in Visual Studio.

Note

Currently, an exception occurs if Blazor rendering modes are defined at the per-page/component level. For more information, see [BlazorWebView needs a way to enable overriding ResolveComponentForRenderMode (`dotnet/aspnetcore` #51235)](https://github.com/dotnet/aspnetcore/issues/51235).

For more information, see [Build a .NET MAUI Blazor Hybrid app with a Blazor Web App](../blazor/hybrid/tutorials/maui-blazor-web-app?view=aspnetcore-9.0).

### Detect rendering location, interactivity, and assigned render mode at runtime

We've introduced a new API designed to simplify the process of querying component states at runtime. This API provides the following capabilities:

* **Determine the current execution location of the component**: This can be useful for debugging and optimizing component performance.
* **Check if the component is running in an interactive environment**: This can be helpful for components that have different behaviors based on the interactivity of their environment.
* **Retrieve the assigned render mode for the component**: Understanding the render mode can help in optimizing the rendering process and improving the overall performance of a component.

For more information, see [ASP.NET Core Blazor render modes](../blazor/components/render-modes?view=aspnetcore-9.0#detect-rendering-location-interactivity-and-assigned-render-mode-at-runtime).

### Improved server-side reconnection experience:

The following enhancements have been made to the default server-side reconnection experience:

* When the user navigates back to an app with a disconnected circuit, reconnection is attempted immediately rather than waiting for the duration of the next reconnect interval. This improves the user experience when navigating to an app in a browser tab that has gone to sleep.

* When a reconnection attempt reaches the server but the server has already released the circuit, a page refresh occurs automatically. This prevents the user from having to manually refresh the page if it's likely going to result in a successful reconnection.

* Reconnect timing uses a computed backoff strategy. By default, the first several reconnection attempts occur in rapid succession without a retry interval before computed delays are introduced between attempts. You can customize the retry interval behavior by specifying a function to compute the retry interval, as the following exponential backoff example demonstrates:

  ```
  Blazor.start({
    circuit: {
      reconnectionOptions: {
        retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
          previousAttempts >= maxRetries ? null : previousAttempts * 1000
      },
    },
  });
  ```

* The styling of the default reconnect UI has been modernized.

For more information, see [ASP.NET Core Blazor SignalR guidance](../blazor/fundamentals/signalr?view=aspnetcore-9.0#adjust-the-server-side-reconnection-retry-count-and-interval).

### Simplified authentication state serialization for Blazor Web Apps

New APIs make it easier to add authentication to an existing Blazor Web App. When you create a new Blazor Web App with authentication using **Individual Accounts** and you enable WebAssembly-based interactivity, the project includes a custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) in both the server and client projects.

These providers flow the user's authentication state to the browser. Authenticating on the server rather than the client allows the app to access authentication state during prerendering and before the .NET WebAssembly runtime is initialized.

The custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) implementations use the [Persistent Component State service](../blazor/state-management/prerendered-state-persistence?view=aspnetcore-9.0) ([PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate)) to serialize the authentication state into HTML comments and read it back from WebAssembly to create a new [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) instance.

This works well if you've started from the Blazor Web App project template and selected the **Individual Accounts** option, but it's a lot of code to implement yourself or copy if you're trying to add authentication to an existing project. There are now APIs, which are now part of the Blazor Web App project template, that can be called in the server and client projects to add this functionality:

* [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization): Adds the necessary services to serialize the authentication state on the server.
* [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization): Adds the necessary services to deserialize the authentication state in the browser.

By default, the API only serializes the server-side name and role claims for access in the browser. An option can be passed to [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization) to include all claims.

For more information, see the following sections of [ASP.NET Core Blazor authentication and authorization](../blazor/security/?view=aspnetcore-9.0):

* [Blazor Identity UI (Individual Accounts)](../blazor/security/?view=aspnetcore-9.0#blazor-identity-ui-individual-accounts)
* [Manage authentication state in Blazor Web Apps](../blazor/security/?view=aspnetcore-9.0#manage-authentication-state-in-blazor-web-apps)

### Add static server-side rendering (SSR) pages to a globally-interactive Blazor Web App

With the release of .NET 9, it's now simpler to add static SSR pages to apps that adopt global interactivity.

This approach is only useful when the app has specific pages that can't work with interactive Server or WebAssembly rendering. For example, adopt this approach for pages that depend on reading/writing HTTP cookies and can only work in a request/response cycle instead of interactive rendering. For pages that work with interactive rendering, you shouldn't force them to use static SSR rendering, as it's less efficient and less responsive for the end user.

Mark any Razor component page with the new [`[ExcludeFromInteractiveRouting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.excludefrominteractiveroutingattribute) assigned with the `@attribute` Razor directive:

```
@attribute [ExcludeFromInteractiveRouting]
```

Applying the attribute causes navigation to the page to exit from interactive routing. Inbound navigation is forced to perform a full-page reload instead resolving the page via interactive routing. The full-page reload forces the top-level root component, typically the `App` component (`App.razor`), to rerender from the server, allowing the app to switch to a different top-level render mode.

The [RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.razorcomponentsendpointhttpcontextextensions.acceptsinteractiverouting) extension method allows the component to detect whether the [`[ExcludeFromInteractiveRouting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.excludefrominteractiveroutingattribute) is applied to the current page.

In the `App` component, use the pattern in the following example:

* Pages that aren't annotated with the [`[ExcludeFromInteractiveRouting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.excludefrominteractiveroutingattribute) default to the `InteractiveServer` render mode with global interactivity. You can replace `InteractiveServer` with `InteractiveWebAssembly` or `InteractiveAuto` to specify a different default global render mode.
* Pages annotated with the [`[ExcludeFromInteractiveRouting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.excludefrominteractiveroutingattribute) adopt static SSR (`PageRenderMode` is assigned `null`).

```
<!DOCTYPE html>
<html>
<head>
    ...
    <HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
    <Routes @rendermode="@PageRenderMode" />
    ...
</body>
</html>

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? PageRenderMode
        => HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}
```

An alternative to using the [RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.razorcomponentsendpointhttpcontextextensions.acceptsinteractiverouting) extension method is to read endpoint metadata manually using `HttpContext.GetEndpoint()?.Metadata`.

This feature is covered by the reference documentation in [ASP.NET Core Blazor render modes](../blazor/components/render-modes?view=aspnetcore-9.0#static-ssr-pages-in-a-globally-interactive-app).

### Constructor injection

Razor components support constructor injection.

In the following example, the partial (code-behind) class injects the `NavigationManager` service using a [primary constructor](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors):

```
public partial class ConstructorInjection(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}
```

For more information, see [ASP.NET Core Blazor dependency injection](../blazor/fundamentals/dependency-injection?view=aspnetcore-9.0#request-a-service-in-a-component).

### Websocket compression for Interactive Server components

By default, Interactive Server components enable compression for [WebSocket connections](../fundamentals/websockets?view=aspnetcore-10.0) and set a `frame-ancestors` [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP) directive set to `'self'` ([MDN's CSP reference guidance](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy)), which only permits embedding the app in an `<iframe>` of the origin from which the app is served when compression is enabled or when a configuration for the WebSocket context is provided.

Compression can be disabled by setting `ConfigureWebSocketOptions` to `null`, which reduces the [vulnerability of the app to attack](../blazor/security/interactive-server-side-rendering?view=aspnetcore-10.0#interactive-server-components-with-websocket-compression-enabled) but may result in reduced performance:

```
.AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)
```

Configure a stricter `frame-ancestors` CSP with a value of `'none'` (single quotes required), which allows WebSocket compression but prevents browsers from embedding the app into any `<iframe>`:

```
.AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")
```

For more information, see the following resources:

* [ASP.NET Core Blazor SignalR guidance](../blazor/fundamentals/signalr?view=aspnetcore-9.0#websocket-compression-for-interactive-server-components)
* [Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering](../blazor/security/interactive-server-side-rendering?view=aspnetcore-9.0#interactive-server-components-with-websocket-compression-enabled)

### Handle keyboard composition events in Blazor

The new `KeyboardEventArgs.IsComposing` property indicates if the keyboard event [is part of a composition session](https://w3c.github.io/uievents/#dom-keyboardevent-iscomposing). Tracking the composition state of keyboard events is crucial for handling international character input methods.

### Added `OverscanCount` parameter to `QuickGrid`

The [`QuickGrid`](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid) component now exposes an `OverscanCount` property that specifies how many additional rows are rendered before and after the visible region when virtualization is enabled.

The default `OverscanCount` is 3. The following example increases the `OverscanCount` to 4:

```
<QuickGrid ItemsProvider="itemsProvider" Virtualize="true" OverscanCount="4">
    ...
</QuickGrid>
```

### `InputNumber` component supports the `type="range"` attribute

The [InputNumber\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputnumber-1) component now supports the [`type="range"` attribute](https://developer.mozilla.org/docs/Web/HTML/Element/input/range), which creates a range input that supports model binding and form validation, typically rendered as a slider or dial control rather than a text box:

```
<EditForm Model="Model" OnSubmit="Submit" FormName="EngineForm">
    <div>
        <label>
            Nacelle Count (2-6): 
            <InputNumber @bind-Value="Model!.NacelleCount" max="6" min="2" 
                step="1" type="range" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private EngineSpecifications? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() {}

    public class EngineSpecifications
    {
        [Required, Range(minimum: 2, maximum: 6)]
        public int NacelleCount { get; set; }
    }
}
```

### New enhanced navigation events

Trigger JavaScript callbacks either before or after enhanced navigation with new event listeners:

* `blazor.addEventListener("enhancednavigationstart", {CALLBACK})`
* `blazor.addEventListener("enhancednavigationend", {CALLBACK})`

For more information, see [ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](../blazor/javascript-interoperability/static-server-rendering?view=aspnetcore-9.0).

### Client-side request streaming

Interactive WebAssembly rendering in Blazor now supports client-side request streaming using the `request.SetBrowserRequestStreamingEnabled(true)` option on `HttpRequestMessage`.

For more information, see the following resources:

* [Call a web API from an ASP.NET Core Blazor app](../blazor/call-web-api?view=aspnetcore-9.0#client-side-request-streaming)
* [ASP.NET Core Blazor file uploads](../blazor/file-uploads?view=aspnetcore-9.0#file-size-read-and-upload-limits)

## SignalR

This section describes new features for SignalR.

### Polymorphic type support in SignalR Hubs

Hub methods can now accept a base class instead of the derived class to enable polymorphic scenarios. The base type needs to be [annotated to allow polymorphism](/en-us/dotnet/standard/serialization/system-text-json/polymorphism).

```
public class MyHub : Hub
{
    public void Method(JsonPerson person)
    {
        if (person is JsonPersonExtended)
        {
        }
        else if (person is JsonPersonExtended2)
        {
        }
        else
        {
        }
    }
}

[JsonPolymorphic]
[JsonDerivedType(typeof(JsonPersonExtended), nameof(JsonPersonExtended))]
[JsonDerivedType(typeof(JsonPersonExtended2), nameof(JsonPersonExtended2))]
private class JsonPerson
{
    public string Name { get; set; }
    public Person Child { get; set; }
    public Person Parent { get; set; }
}

private class JsonPersonExtended : JsonPerson
{
    public int Age { get; set; }
}

private class JsonPersonExtended2 : JsonPerson
{
    public string Location { get; set; }
}
```

### Improved Activities for SignalR

SignalR now has an ActivitySource for both the hub server and client.

#### .NET SignalR server ActivitySource

The SignalR ActivitySource named `Microsoft.AspNetCore.SignalR.Server` emits events for hub method calls:

* Every method is its own activity, so anything that emits an activity during the hub method call is under the hub method activity.
* Hub method activities don't have a parent. This means they are not bundled under the long-running SignalR connection.

The following example uses the [Aspire dashboard](/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash#use-the-dashboard-with-aspire-projects) and the [OpenTelemetry](https://www.nuget.org/packages/OpenTelemetry.Extensions.Hosting) packages:

```
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
```

Add the following startup code to the `Program.cs` file:

```
using OpenTelemetry.Trace;
using SignalRChat.Hubs;

// Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable depending on where your OTEL endpoint is.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        if (builder.Environment.IsDevelopment())
        {
            // View all traces only in development environment.
            tracing.SetSampler(new AlwaysOnSampler());
        }

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.AspNetCore.SignalR.Server");
    });

builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());

var app = builder.Build();
```

The following is example output from the Aspire Dashboard:

#### .NET SignalR client ActivitySource

The SignalR ActivitySource named `Microsoft.AspNetCore.SignalR.Client` emits events for a SignalR client:

* The .NET SignalR client has an `ActivitySource` named `Microsoft.AspNetCore.SignalR.Client`. Hub invocations now create a client span. Note that other SignalR clients, such as the JavaScript client, don't support tracing. This feature will be added to more clients in future releases.
* Hub invocations on the client and server support [context propagation](https://opentelemetry.io/docs/concepts/context-propagation/). Propagating the trace context enables true distributed tracing. It's now possible to see invocations flow from the client to the server and back.

Here's how these new activities look in the [Aspire dashboard](/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash#standalone-mode):

### SignalR supports trimming and Native AOT

Continuing the [Native AOT journey](../fundamentals/native-aot?view=aspnetcore-10.0) started in .NET 8, we have enabled trimming and native ahead-of-time (AOT) compilation support for both SignalR client and server scenarios. You can now take advantage of the performance benefits of using Native AOT in applications that use SignalR for real-time web communications.

#### Getting started

Install the latest [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0).

Create a solution from the `webapiaot` template in a command shell using the following command:

```
dotnet new webapiaot -o SignalRChatAOTExample
```

Replace the contents of the `Program.cs` file with the following SignalR code:

```
using Microsoft.AspNetCore.SignalR;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddSignalR();
builder.Services.Configure<JsonHubProtocolOptions>(o =>
{
    o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");
app.MapGet("/", () => Results.Content("""
<!DOCTYPE html>
<html>
<head>
    <title>SignalR Chat</title>
</head>
<body>
    <input id="userInput" placeholder="Enter your name" />
    <input id="messageInput" placeholder="Type a message" />
    <button onclick="sendMessage()">Send</button>
    <ul id="messages"></ul>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.min.js"></script>
    <script>
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("/chatHub")
            .build();

        connection.on("ReceiveMessage", (user, message) => {
            const li = document.createElement("li");
            li.textContent = `${user}: ${message}`;
            document.getElementById("messages").appendChild(li);
        });

        async function sendMessage() {
            const user = document.getElementById("userInput").value;
            const message = document.getElementById("messageInput").value;
            await connection.invoke("SendMessage", user, message);
        }

        connection.start().catch(err => console.error(err));
    </script>
</body>
</html>
""", "text/html"));

app.Run();

[JsonSerializable(typeof(string))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

The preceding example produces a native Windows executable of 10 MB and a Linux executable of 10.9 MB.

#### Limitations

* Only the JSON protocol is currently supported:

  * As shown in the preceding code, apps that use JSON serialization and Native AOT must use the `System.Text.Json` Source Generator.
  * This follows the same approach as Minimal APIs.

* On the SignalR server, Hub method parameters of type `IAsyncEnumerable<T>` and `ChannelReader<T>` where `T` is a ValueType (`struct`) aren't supported. Using these types results in a runtime exception at startup in development and in the published app. For more information, see [SignalR: Using IAsyncEnumerable\<T> and ChannelReader\<T> with ValueTypes in native AOT (`dotnet/aspnetcore` #56179)](https://github.com/dotnet/aspnetcore/issues/56179).

* [Strongly typed hubs](../signalr/hubs?view=aspnetcore-10.0#use-strongly-typed-hubs) aren't supported with Native AOT (`PublishAot`). Using strongly typed hubs with Native AOT will result in warnings during build and publish, and a runtime exception. Using strongly typed hubs with trimming (`PublishedTrimmed`) is supported.
  * Make sure to copy the names from `[HubMethodName(...)]` applied to the strongly typed hub interface methods into the `SendAsync` calls.

* Only `Task`, `Task<T>`, `ValueTask`, or `ValueTask<T>` are supported for async return types.

## Minimal APIs

This section describes new features for Minimal APIs.

### Added `InternalServerError` and `InternalServerError<TValue>` to `TypedResults`

The [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) class is a helpful vehicle for returning strongly-typed HTTP status code-based responses from a Minimal API. `TypedResults` now includes factory methods and types for returning "500 Internal Server Error" responses from endpoints. Here's an example that returns a 500 response:

```
var app = WebApplication.Create();

app.MapGet("/", () => TypedResults.InternalServerError("Something went wrong!"));

app.Run();
```

### Call `ProducesProblem` and `ProducesValidationProblem` on route groups

The `ProducesProblem` and `ProducesValidationProblem` extension methods have been updated to support their use on route groups. These methods indicate that all endpoints in a route group can return `ProblemDetails` or `ValidationProblemDetails` responses for the purposes of OpenAPI metadata.

```
var app = WebApplication.Create();

var todos = app.MapGroup("/todos")
    .ProducesProblem();

todos.MapGet("/", () => new Todo(1, "Create sample app", false));
todos.MapPost("/", (Todo todo) => Results.Ok(todo));

app.Run();

record Todo(int Id, string Title, bool IsCompleted);
```

### `Problem` and `ValidationProblem` result types support construction with `IEnumerable<KeyValuePair<string, object?>>` values

Prior to .NET 9, constructing [Problem](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.problem) and [ValidationProblem](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.validationproblem) result types in Minimal APIs required that the `errors` and `extensions` properties be initialized with an implementation of `IDictionary<string, object?>`. In this release, these construction APIs support overloads that consume `IEnumerable<KeyValuePair<string, object?>>`.

```
var app = WebApplication.Create();

app.MapGet("/", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions",
                                                       extensions: extensions);
});
```

Thanks to Joseph Goldman for this contribution!

## OpenAPI

This section describes new features for OpenAPI

### Built-in support for OpenAPI document generation

The [OpenAPI specification](https://www.openapis.org/) is a standard for describing HTTP APIs. The standard allows developers to define the shape of APIs that can be plugged into client generators, server generators, testing tools, documentation, and more. In .NET 9, ASP.NET Core provides built-in support for generating OpenAPI documents representing controller-based or Minimal APIs via the [Microsoft.AspNetCore.OpenApi](https://nuget.org/packages/Microsoft.AspNetCore.OpenApi) package.

The following highlighted code calls:

* `AddOpenApi` to register the required dependencies into the app's DI container.
* `MapOpenApi` to register the required OpenAPI endpoints in the app's routes.

```
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/hello/{name}", (string name) => $"Hello {name}"!);

app.Run();
```

Install the [`Microsoft.AspNetCore.OpenApi`](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi) package in the project using the following command:

```
dotnet add package Microsoft.AspNetCore.OpenApi
```

Run the app and navigate to `openapi/v1.json` to view the generated OpenAPI document:

OpenAPI documents can also be generated at build-time by adding the [`Microsoft.Extensions.ApiDescription.Server`](https://www.nuget.org/packages/Microsoft.Extensions.ApiDescription.Server) package:

```
dotnet add package Microsoft.Extensions.ApiDescription.Server
```

To modify the location of the emitted OpenAPI documents, set the target path in the OpenApiDocumentsDirectory property in the app's project file:

```
<PropertyGroup>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
</PropertyGroup>
```

Run `dotnet build` and inspect the generated JSON file in the project directory.

ASP.NET Core's built-in OpenAPI document generation provides support for various customizations and options. It provides document, operation, and schema transformers and has the ability to manage multiple OpenAPI documents for the same application.

To learn more about ASP.NET Core's new OpenAPI document capabilities, see [the new Microsoft.AspNetCore.OpenApi docs](https://aka.ms/aspnet/openapi).

### Microsoft.AspNetCore.OpenApi supports trimming and Native AOT

OpenAPI in ASP.NET Core supports trimming and native AOT. The following steps create and publish an OpenAPI app with trimming and native AOT:

Create a new ASP.NET Core Web API (Native AOT) project.

```
dotnet new webapiaot
```

Add the Microsoft.AspNetCore.OpenAPI package.

```
dotnet add package Microsoft.AspNetCore.OpenApi
```

Update `Program.cs` to enable generating OpenAPI documents.

```
+ builder.Services.AddOpenApi();

var app = builder.Build();

+ app.MapOpenApi();
```

Publish the app.

```
dotnet publish
```

## Authentication and authorization

This section describes new features for authentication and authorization.

### OpenIdConnectHandler adds support for Pushed Authorization Requests (PAR)

We'd like to thank [Joe DeCock](https://github.com/josephdecock) from [Duende Software](https://github.com/DuendeSoftware) for adding Pushed Authorization Requests (PAR) to ASP.NET Core's [OpenIdConnectHandler](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnecthandler). Joe described the background and motivation for enabling PAR in [his API proposal](https://github.com/dotnet/aspnetcore/issues/51686) as follows:

> Pushed Authorization Requests (PAR) is a relatively new [OAuth standard](https://datatracker.ietf.org/doc/html/rfc9126) that improves the security of OAuth and OIDC flows by moving authorization parameters from the front channel to the back channel. That is, moving authorization parameters from redirect URLs in the browser to direct machine to machine http calls on the back end.
>
> This prevents a cyberattacker in the browser from:
>
> * Seeing authorization parameters, which could leak PII.
> * Tampering with those parameters. For example, the cyberattacker could change the scope of access being requested.
>
> Pushing the authorization parameters also keeps request URLs short. Authorize parameters can get very long when using more complex OAuth and OIDC features such as [Rich Authorization Requests](https://oauth.net/2/rich-authorization-requests/). URLs that are long cause issues in many browsers and networking infrastructures.
>
> The use of PAR is encouraged by the [FAPI working group](https://openid.net/wg/fapi/) within the OpenID Foundation. For example, [the FAPI2.0 Security Profile](https://openid.bitbucket.io/fapi/fapi-2_0-security-profile.html) requires the use of PAR. This security profile is used by many of the groups working on open banking (primarily in Europe), in health care, and in other industries with high security requirements.
>
> PAR is supported by a number of identity providers, including
>
> * [Duende IdentityServer](https://duendesoftware.com/products/identityserver)
> * [Curity](https://curity.io/product/)
> * [Keycloak](https://www.keycloak.org/)
> * [Authlete](https://www.authlete.com/developers/tutorial/oidc/)

For .NET 9, we have decided to enable PAR by default if the identity provider's discovery document advertises support for PAR, since it should provide enhanced security for providers that support it. The identity provider's discovery document is usually found at `.well-known/openid-configuration`. If this causes problems, you can disable PAR via [OpenIdConnectOptions.PushedAuthorizationBehavior](https://source.dot.net/#Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs,99014cc0333b1603) as follows:

```
builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect("oidc", oidcOptions =>
    {
        // Other provider-specific configuration goes here.

        // The default value is PushedAuthorizationBehavior.UseIfAvailable.

        // 'OpenIdConnectOptions' does not contain a definition for 'PushedAuthorizationBehavior'
        // and no accessible extension method 'PushedAuthorizationBehavior' accepting a first argument
        // of type 'OpenIdConnectOptions' could be found
        oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
    });
```

To ensure that authentication only succeeds if PAR is used, use [PushedAuthorizationBehavior.Require](https://source.dot.net/#Microsoft.AspNetCore.Authentication.OpenIdConnect/PushedAuthorizationBehavior.cs,3af73de8f33b70c5) instead. This change also introduces a new [OnPushAuthorization](https://source.dot.net/#Microsoft.AspNetCore.Authentication.OpenIdConnect/Events/OpenIdConnectEvents.cs,6a21c8f3a90753c1) event to [OpenIdConnectEvents](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents) which can be used customize the pushed authorization request or handle it manually. See the [API proposal](https://github.com/dotnet/aspnetcore/issues/51686) for more details.

### OIDC and OAuth Parameter Customization

The OAuth and OIDC authentication handlers now have an `AdditionalAuthorizationParameters` option to make it easier to customize authorization message parameters that are usually included as part of the redirect query string. In .NET 8 or earlier, this requires a custom [OnRedirectToIdentityProvider](/en-us/dotnet/api/microsoft.aspnetcore.authentication.wsfederation.wsfederationevents.onredirecttoidentityprovider#microsoft-aspnetcore-authentication-wsfederation-wsfederationevents-onredirecttoidentityprovider) callback or overridden [BuildChallengeUrl](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.oauthhandler-1.buildchallengeurl) method in a custom handler. Here's an example of .NET 8 code:

```
builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        context.ProtocolMessage.SetParameter("prompt", "login");
        context.ProtocolMessage.SetParameter("audience", "https://api.example.com");
        return Task.CompletedTask;
    };
});
```

The preceding example can now be simplified to the following code:

```
builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.AdditionalAuthorizationParameters.Add("prompt", "login");
    options.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com");
});
```

### Configure HTTP.sys extended authentication flags

You can now configure the [`HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING`](/en-us/windows/win32/api/http/ns-http-http_server_authentication_info) and [`HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL`](/en-us/windows/win32/api/http/ns-http-http_server_authentication_info) HTTP.sys flags by using the new `EnableKerberosCredentialCaching` and `CaptureCredentials` properties on the HTTP.sys [AuthenticationManager](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.authenticationmanager) to optimize how Windows authentication is handled. For example:

```
webBuilder.UseHttpSys(options =>
{
    options.Authentication.Schemes = AuthenticationSchemes.Negotiate;
    options.Authentication.EnableKerberosCredentialCaching = true;
    options.Authentication.CaptureCredentials = true;
});
```

## Miscellaneous

The following sections describe miscellaneous new features.

### New `HybridCache` library

Important

`HybridCache` is currently still in preview but will be fully released *after* .NET 9 in a future minor release of .NET Extensions.

The [HybridCache](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache) API bridges some gaps in the existing [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) and [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) APIs. It also adds new capabilities, such as:

* **"Stampede" protection** to prevent parallel fetches of the same work.
* Configurable serialization.

`HybridCache` is designed to be a drop-in replacement for existing `IDistributedCache` and `IMemoryCache` usage, and it provides a simple API for adding new caching code. It provides a unified API for both in-process and out-of-process caching.

To see how the `HybridCache` API is simplified, compare it to code that uses `IDistributedCache`. Here's an example of what using `IDistributedCache` looks like:

```
public class SomeService(IDistributedCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
        var bytes = await cache.GetAsync(key, token); // Try to get from cache.
        SomeInformation info;
        if (bytes is null)
        {
            // Cache miss; get the data from the real source.
            info = await SomeExpensiveOperationAsync(name, id, token);

            // Serialize and cache it.
            bytes = SomeSerializer.Serialize(info);
            await cache.SetAsync(key, bytes, token);
        }
        else
        {
            // Cache hit; deserialize it.
            info = SomeSerializer.Deserialize<SomeInformation>(bytes);
        }
        return info;
    }

    // This is the work we're trying to cache.
    private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
        CancellationToken token = default)
    { /* ... */ }
}
```

That's a lot of work to get right each time, including things like serialization. And in the cache miss scenario, you could end up with multiple concurrent threads, all getting a cache miss, all fetching the underlying data, all serializing it, and all sending that data to the cache.

To simplify and improve this code with `HybridCache`, we first need to add the new library `Microsoft.Extensions.Caching.Hybrid`:

```
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0" />
```

Register the `HybridCache` service, like you would register an `IDistributedCache` implementation:

```
builder.Services.AddHybridCache(); // Not shown: optional configuration API.
```

Now most caching concerns can be offloaded to `HybridCache`:

```
public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // Unique key for this combination.
            async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
            token: token
        );
    }
}
```

We provide a concrete implementation of the `HybridCache` abstract class via dependency injection, but it's intended that developers can provide custom implementations of the API. The `HybridCache` implementation deals with everything related to caching, including concurrent operation handling. The `cancel` token here represents the combined cancellation of *all* concurrent callers—not just the cancellation of the caller we can see (that is, `token`).

High throughput scenarios can be further optimized by using the `TState` pattern, to avoid some overhead from captured variables and per-instance callbacks:

```
public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync(string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // unique key for this combination
            (name, id), // all of the state we need for the final call, if needed
            static async (state, token) =>
                await SomeExpensiveOperationAsync(state.name, state.id, token),
            token: token
        );
    }
}
```

`HybridCache` uses the configured `IDistributedCache` implementation, if any, for secondary out-of-process caching, for example, using Redis. But even without an `IDistributedCache`, the `HybridCache` service will still provide in-process caching and "stampede" protection.

#### A note on object reuse

In typical existing code that uses `IDistributedCache`, every retrieval of an object from the cache results in deserialization. This behavior means that each concurrent caller gets a separate instance of the object, which cannot interact with other instances. The result is thread safety, as there's no risk of concurrent modifications to the same object instance.

Because a lot of `HybridCache` usage will be adapted from existing `IDistributedCache` code, `HybridCache` preserves this behavior by default to avoid introducing concurrency bugs. However, a given use case is inherently thread-safe:

* If the types being cached are immutable.
* If the code doesn't modify them.

In such cases, inform `HybridCache` that it's safe to reuse instances by making both of the following changes:

* Marking the type as `sealed`. The `sealed` keyword in C# means that the class can't be inherited.
* Applying the `[ImmutableObject(true)]` attribute to it. The `[ImmutableObject(true)]` attribute indicates that the object's state can't be changed after it's created.

By reusing instances, `HybridCache` can reduce the overhead of CPU and object allocations associated with per-call deserialization. This can lead to performance improvements in scenarios where the cached objects are large or accessed frequently.

#### Other `HybridCache` features

Like `IDistributedCache`, `HybridCache` supports removal by key with a `RemoveKeyAsync` method.

`HybridCache` also provides optional APIs for `IDistributedCache` implementations, to avoid `byte[]` allocations. This feature is implemented by the preview versions of the `Microsoft.Extensions.Caching.StackExchangeRedis`, `Microsoft.Extensions.Caching.SqlServer`, and `Microsoft.Extensions.Caching.Postgres` packages.

Serialization is configured as part of registering the service, with support for type-specific and generalized serializers via the `WithSerializer` and `.WithSerializerFactory` methods, chained from the `AddHybridCache` call. By default, the library handles `string` and `byte[]` internally, and uses `System.Text.Json` for everything else, but you can use protobuf, xml, or anything else.

`HybridCache` supports older .NET runtimes, down to .NET Framework 4.7.2 and .NET Standard 2.0.

For more information about `HybridCache`, see [HybridCache library in ASP.NET Core](../performance/caching/hybrid?view=aspnetcore-10.0)

### Developer exception page improvements

The [ASP.NET Core developer exception page](../fundamentals/error-handling?view=aspnetcore-10.0#developer-exception-page) is displayed when an app throws an unhandled exception during development. The developer exception page provides detailed information about the exception and request.

Preview 3 added endpoint metadata to the developer exception page. ASP.NET Core uses endpoint metadata to control endpoint behavior, such as routing, response caching, rate limiting, OpenAPI generation, and more. The following image shows the new metadata information in the `Routing` section of the developer exception page:

While testing the developer exception page, small quality of life improvements were identified. They shipped in Preview 4:

* Better text wrapping. Long cookies, query string values, and method names no longer add horizontal browser scroll bars.
* Bigger text which is found in modern designs.
* More consistent table sizes.

The following animated image shows the new developer exception page:

### Dictionary debugging improvements

The debugging display of dictionaries and other key-value collections has an improved layout. The key is displayed in the debugger's key column instead of being concatenated with the value. The following images show the old and new display of a dictionary in the debugger.

Before:

After:

ASP.NET Core has many key-value collections. This improved debugging experience applies to:

* HTTP headers
* Query strings
* Forms
* Cookies
* View data
* Route data
* Features

### Fix for 503's during app recycle in IIS

By default there is now a 1 second delay between when IIS is notified of a recycle or shutdown and when ANCM tells the managed server to start shutting down. The delay is configurable via the `ANCM_shutdownDelay` environment variable or by setting the `shutdownDelay` handler setting. Both values are in milliseconds. The delay is mainly to reduce the likelihood of a race where:

* IIS hasn't started queuing requests to go to the new app.
* ANCM starts rejecting new requests that come into the old app.

Slower machines or machines with heavier CPU usage may want to adjust this value to reduce 503 likelihood.

Example of setting `shutdownDelay`:

```
<aspNetCore processPath="dotnet" arguments="myapp.dll" stdoutLogEnabled="false" stdoutLogFile=".logsstdout">
  <handlerSettings>
    <!-- Milliseconds to delay shutdown by.
    this doesn't mean incoming requests will be delayed by this amount,
    but the old app instance will start shutting down after this timeout occurs -->
    <handlerSetting name="shutdownDelay" value="5000" />
  </handlerSettings>
</aspNetCore>
```

The fix is in the globally installed ANCM module that comes from the hosting bundle.

### ASP0026: Analyzer to warn when \[Authorize] is overridden by \[AllowAnonymous] from "farther away"

It seems intuitive that an `[Authorize]` attribute placed "closer" to an MVC action than an `[AllowAnonymous]` attribute would override the `[AllowAnonymous]` attribute and force authorization. However, this is not necessarily the case. What does matter is the relative order of the attributes.

Note

The `[AllowAnonymous]` attribute doesn't disable authentication entirely. When credentials are sent to an endpoint with `[AllowAnonymous]`, the endpoint still authenticates those credentials and establishes the user's identity. The `[AllowAnonymous]` attribute only means that authentication is **not required**—the endpoint will run as anonymous only when no credentials are provided. This behavior can be useful for endpoints that need to work for both authenticated and anonymous users.

The following code shows examples where a closer `[Authorize]` attribute gets overridden by an `[AllowAnonymous]` attribute that is farther away.

```
[AllowAnonymous]
public class MyController
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on the class
    public IActionResult Private() => null;
}
```

```
[AllowAnonymous]
public class MyControllerAnon : ControllerBase
{
}

[Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
public class MyControllerInherited : MyControllerAnon
{
}

public class MyControllerInherited2 : MyControllerAnon
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
    public IActionResult Private() => null;
}
```

```
[AllowAnonymous]
[Authorize] // Overridden by the preceding [AllowAnonymous]
public class MyControllerMultiple : ControllerBase
{
}
```

In .NET 9 Preview 6, we've introduced an analyzer that will highlight instances like these where a closer `[Authorize]` attribute gets overridden by an `[AllowAnonymous]` attribute that is farther away from an MVC action. The warning points to the overridden `[Authorize]` attribute with the following message:

```
ASP0026 [Authorize] overridden by [AllowAnonymous] from farther away
```

The correct action to take if you see this warning depends on the intention behind the attributes. The farther away `[AllowAnonymous]` attribute should be removed if it's unintentionally exposing the endpoint to anonymous users. If the `[AllowAnonymous]` attribute was intended to override a closer `[Authorize]` attribute, you can repeat the `[AllowAnonymous]` attribute after the `[Authorize]` attribute to clarify the intent.

```
[AllowAnonymous]
public class MyController
{
    // This produces no warning because the second, "closer" [AllowAnonymous]
    // clarifies that [Authorize] is intentionally overridden.
    // Specifying AuthenticationSchemes can be useful for endpoints that
    // allow but don't require authenticated users. When credentials are sent,
    // they will be authenticated; when no credentials are sent, the endpoint
    // allows anonymous access.
    [Authorize(AuthenticationSchemes = "Cookies")]
    [AllowAnonymous]
    public IActionResult Privacy() => null;
}
```

### Improved Kestrel connection metrics

We've made a significant improvement to Kestrel's connection metrics by including metadata about why a connection failed. The [`kestrel.connection.duration`](/en-us/dotnet/core/diagnostics/built-in-metrics-aspnetcore#metric-kestrelconnectionduration) metric now includes the connection close reason in the `error.type` attribute.

Here is a small sample of the `error.type` values:

* `tls_handshake_failed` - The connection requires TLS, and the TLS handshake failed.
* `connection_reset` - The connection was unexpectedly closed by the client while requests were in progress.
* `request_headers_timeout` - Kestrel closed the connection because it didn't receive request headers in time.
* `max_request_body_size_exceeded` - Kestrel closed the connection because uploaded data exceeded max size.

Previously, diagnosing Kestrel connection issues required a server to record detailed, low-level logging. However, logs can be expensive to generate and store, and it can be difficult to find the right information among the noise.

Metrics are a much cheaper alternative that can be left on in a production environment with minimal impact. Collected metrics can [drive dashboards and alerts](../metrics/overview?view=aspnetcore-10.0#show-metrics-on-a-grafana-dashboard). Once a problem is identified at a high-level with metrics, further investigation using logging and other tooling can begin.

We expect improved connection metrics to be useful in many scenarios:

* Investigating performance issues caused by short connection lifetimes.
* Observing ongoing external attacks on Kestrel that impact performance and stability.
* Recording attempted external attacks on Kestrel that Kestrel's built-in security hardening prevented.

For more information, see [ASP.NET Core metrics](../metrics/overview?view=aspnetcore-10.0).

### Customize Kestrel named pipe endpoints

Kestrel's named pipe support has been improved with advanced customization options. The new `CreateNamedPipeServerStream` method on the named pipe options allows pipes to be customized per-endpoint.

An example of where this is useful is a Kestrel app that requires two pipe endpoints with different [access security](/en-us/windows/win32/ipc/named-pipe-security-and-access-rights). The `CreateNamedPipeServerStream` option can be used to create pipes with custom security settings, depending on the pipe name.

```
var builder = WebApplication.CreateBuilder();

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenNamedPipe("pipe1");
    options.ListenNamedPipe("pipe2");
});

builder.WebHost.UseNamedPipes(options =>
{
    options.CreateNamedPipeServerStream = (context) =>
    {
        var pipeSecurity = CreatePipeSecurity(context.NamedPipeEndpoint.PipeName);

        return NamedPipeServerStreamAcl.Create(context.NamedPipeEndPoint.PipeName, PipeDirection.InOut,
            NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte,
            context.PipeOptions, inBufferSize: 0, outBufferSize: 0, pipeSecurity);
    };
});
```

### `ExceptionHandlerMiddleware` option to choose the status code based on the exception type

A new option when configuring the `ExceptionHandlerMiddleware` enables app developers to choose what status code to return when an exception occurs during request handling. The new option changes the status code being set in the `ProblemDetails` response from the `ExceptionHandlerMiddleware`.

```
app.UseExceptionHandler(new ExceptionHandlerOptions
{
    StatusCodeSelector = ex => ex is TimeoutException
        ? StatusCodes.Status503ServiceUnavailable
        : StatusCodes.Status500InternalServerError,
});
```

### Opt-out of HTTP metrics on certain endpoints and requests

.NET 9 introduces the ability to opt-out of HTTP metrics for specific endpoints and requests. Opting out of recording metrics is beneficial for endpoints frequently called by automated systems, such as health checks. Recording metrics for these requests is generally unnecessary.

HTTP requests to an endpoint can be excluded from metrics by adding metadata. Either:

* Add the [`[DisableHttpMetrics]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.http.disablehttpmetricsattribute) to the Web API controller, SignalR hub or gRPC service.
* Call [DisableHttpMetrics](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpmetricsendpointconventionbuilderextensions.disablehttpmetrics) when mapping endpoints in app startup:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();

var app = builder.Build();
app.MapHealthChecks("/healthz").DisableHttpMetrics();
app.Run();
```

The `MetricsDisabled` property has been added to `IHttpMetricsTagsFeature` for:

* Advanced scenarios where a request doesn't map to an endpoint.
* Dynamically disabling metrics collection for specific HTTP requests.

```
// Middleware that conditionally opts-out HTTP requests.
app.Use(async (context, next) =>
{
    var metricsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
    if (metricsFeature != null &&
        context.Request.Headers.ContainsKey("x-disable-metrics"))
    {
        metricsFeature.MetricsDisabled = true;
    }

    await next(context);
});
```

### Data Protection support for deleting keys

Prior to .NET 9, data protection keys were ***not*** deletable by design, to prevent data loss. Deleting a key renders its protected data irretrievable. Given their small size, the accumulation of these keys generally posed minimal impact. However, to accommodate extremely long-running services, we have introduced the option to delete keys. Generally, only old keys should be deleted. Only delete keys when you can accept the risk of data loss in exchange for storage savings. We recommend data protection keys should ***not*** be deleted.

```
using Microsoft.AspNetCore.DataProtection.KeyManagement;

var services = new ServiceCollection();
services.AddDataProtection();

var serviceProvider = services.BuildServiceProvider();

var keyManager = serviceProvider.GetService<IKeyManager>();

if (keyManager is IDeletableKeyManager deletableKeyManager)
{
    var utcNow = DateTimeOffset.UtcNow;
    var yearAgo = utcNow.AddYears(-1);

    if (!deletableKeyManager.DeleteKeys(key => key.ExpirationDate < yearAgo))
    {
        Console.WriteLine("Failed to delete keys.");
    }
    else
    {
        Console.WriteLine("Old keys deleted successfully.");
    }
}
else
{
    Console.WriteLine("Key manager does not support deletion.");
}
```

### Middleware supports Keyed DI

Middleware now supports [Keyed DI](/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-9.0\&preserve-view=true#keyed-services) in both the constructor and the `Invoke`/`InvokeAsync` method:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<MySingletonClass>("test");
builder.Services.AddKeyedScoped<MyScopedClass>("test2");

var app = builder.Build();
app.UseMiddleware<MyMiddleware>();
app.Run();

internal class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next,
        [FromKeyedServices("test")] MySingletonClass service)
    {
        _next = next;
    }

    public Task Invoke(HttpContext context,
        [FromKeyedServices("test2")]
            MyScopedClass scopedService) => _next(context);
}
```

#### Trust the ASP.NET Core HTTPS development certificate on Linux

On Ubuntu and Fedora based Linux distros, `dotnet dev-certs https --trust` now configures ASP.NET Core HTTPS development certificate as a trusted certificate for:

* Chromium browsers, for example, Google Chrome, Microsoft Edge, and Chromium.
* Mozilla Firefox and Mozilla derived browsers.
* .NET APIs, for example, [HttpClient](/en-us/dotnet/api/system.net.http.httpclient)

Previously, `--trust` only worked on Windows and macOS. Certificate trust is applied per-user.

To establish trust in OpenSSL, the `dev-certs` tool:

* Puts the certificate in `~/.aspnet/dev-certs/trust`
* Runs a simplified version of OpenSSL's [c\_rehash tool](https://docs.openssl.org/1.0.2/man1/c_rehash/) on the directory.
* Asks the user to update the `SSL_CERT_DIR` environment variable.

To establish trust in dotnet, the tool puts the certificate in the `My/Root` certificate store.

To establish trust in [NSS databases](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/developer_guide/che-nsslib), if any, the tool searches the home directory for Firefox profiles, `~/.pki/nssdb`, and `~/snap/chromium/current/.pki/nssdb`. For each directory found, the tool adds an entry to the `nssdb`.

### Templates updated to latest Bootstrap, jQuery, and jQuery Validation versions

The ASP.NET Core project templates and libraries have been updated to use the latest versions of Bootstrap, jQuery, and jQuery Validation, specifically:

* Bootstrap 5.3.3
* jQuery 3.7.1
* jQuery Validation 1.21.0

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/security/authorization/simple
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/security/authorization/simple.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Simple authorization in ASP.NET Core Razor Pages

Authorization in ASP.NET Core is controlled with the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and its various parameters. In its most basic form, applying the `[Authorize]` attribute to a Razor component, controller, action, or Razor Page, limits access to that component to authenticated users.

This article covers scenarios that pertain to Razor Pages apps. For more information, see [Simple authorization in ASP.NET Core](../../../security/authorization/simple?view=aspnetcore-10.0).

## `[Authorize]` attribute

Apply the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the page model class that derives from [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel). In the following example, only authenticated users can reach the `LogoutModel` page:

```
[Authorize]
public class LogoutModel : PageModel
{
    public async Task OnGetAsync() { ... }
    public async Task<IActionResult> OnPostAsync() { ... }
}
```

The [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) also supports role-based or policy-based authorization. For role-based authorization, use the [Roles](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles#microsoft-aspnetcore-authorization-authorizeattribute-roles) parameter. In the following example, the user can only access the page if they're in the `Admin` or `Superuser` role:

```
[Authorize(Roles = "Admin, Superuser")]
public class OrderModel : PageModel
{
    ...
}
```

For policy-based authorization, use the [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) parameter. In the following example, the user can only access the page if they satisfy the requirements of the `Over21` [authorization policy](../../../security/authorization/policies?view=aspnetcore-10.0):

```
[Authorize(Policy = "Over21")]
public class LicenseApplicationModel : PageModel
{
    ...
}
```

If neither [Roles](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.roles#microsoft-aspnetcore-authorization-authorizeattribute-roles) nor [Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy#microsoft-aspnetcore-authorization-authorizeattribute-policy) is specified, [`[Authorize]`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) uses the default policy:

* Authenticated (signed-in) users are authorized.
* Unauthenticated (signed-out) users are unauthorized.

For guidance on [AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and Razor Page handlers, see the [`[Authorize]` attribute in Razor Pages apps](#authorize-attribute-in-razor-pages-apps) section.

Use the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) to allow access by non-authenticated users to individual actions:

```
[AllowAnonymous]
```

For information on how to require authentication for all app users, see [Require global user authentication](../../../security/authorization/policies?view=aspnetcore-10.0#require-global-user-authentication).

## `[Authorize]` attribute in Razor Pages apps

The [AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) can't be applied to Razor Page handlers. For example, the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) can't be applied to `OnGet`, `OnPost`, or any other page handler. In a Razor Pages app, consider using an ASP.NET Core MVC controller for pages with different authorization requirements for different handlers. Using a controller when different authorization requirements are required is the least complex approach we recommend.

If you decide not to use an MVC controller, the following two approaches can be used to apply authorization to Razor Page handler methods:

* Use separate pages for page handlers requiring different authorization. Move shared content into one or more [partial views](../../../mvc/views/partial?view=aspnetcore-10.0). When possible, this is the recommended approach.
* For content that must share a common page, write a filter that performs authorization as part of [IAsyncPageFilter.OnPageHandlerExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter.onpagehandlerexecutionasync). This approach is demonstrated by the following example.

The `AuthorizeIndexPageHandlerFilter` implements the authorization filter:

```
public class AuthorizeIndexPageHandlerFilter(
    IAuthorizationPolicyProvider policyProvider,
    IPolicyEvaluator policyEvaluator) : IAsyncPageFilter, IOrderedFilter
{
    private readonly IAuthorizationPolicyProvider policyProvider = 
        policyProvider;
    private readonly IPolicyEvaluator policyEvaluator = policyEvaluator;

    // Run late in the execution pipeline
    public int Order => 10000;

    public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,
        PageHandlerExecutionDelegate next)
    {
        var attribute = context.HandlerMethod?.MethodInfo?
            .GetCustomAttribute<AuthorizePageHandlerAttribute>();

        if (attribute is null)
        {
            await next();
            return;
        }

        var policy = await AuthorizationPolicy
            .CombineAsync(policyProvider, new[] { attribute });

        if (policy is null)
        {
            await next();
            return;
        }

        var httpContext = context.HttpContext;
        var authenticateResult = await policyEvaluator
            .AuthenticateAsync(policy, httpContext);
        var authorizeResult = await policyEvaluator
            .AuthorizeAsync(policy, authenticateResult, httpContext,
                context.ActionDescriptor);

        if (authorizeResult.Challenged)
        {
            context.Result = policy.AuthenticationSchemes.Count > 0
                ? new ChallengeResult(policy.AuthenticationSchemes.ToArray())
                : new ChallengeResult();

            return;
        }
        else if (authorizeResult.Forbidden)
        {
            context.Result = policy.AuthenticationSchemes.Count > 0
                ? new ForbidResult(policy.AuthenticationSchemes.ToArray())
                : new ForbidResult();

            return;
        }

        await next();
    }

    public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
        => Task.CompletedTask;
}
```

The `AuthorizePageHandlerAttribute` provides an `[AuthorizePageHandler]` attribute to the app:

```
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class AuthorizePageHandlerAttribute(string policy = null) 
    : Attribute, IAuthorizeData
{
    public string Policy { get; set; } = policy;
    public string Roles { get; set; }
    public string AuthenticationSchemes { get; set; }
}
```

The `[AuthorizePageHandler]` attribute is applied to page handlers. In the following example, the attribute is set on the `OnPostAuthorized` page handler:

```
[TypeFilter(typeof(AuthorizeIndexPageHandlerFilter))]
public class IndexModel : PageModel
{
    ...

    [AuthorizePageHandler]
    public void OnPostAuthorized() { ... }
}
```

Warning

The preceding approach does ***not***:

* Compose with authorization attributes applied to the page, page model, or globally. Composing authorization attributes results in authentication and authorization executing multiple times when you have one more [`[Authorize]` attributes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) or [AuthorizeFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.authorization.authorizefilter) instances also applied to the page.
* Work in conjunction with the rest of the ASP.NET Core authentication and authorization system. Verify that this approach works correctly for the app.

There are no plans to support the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) on Razor Page handlers.

## Related content

* [Simple authorization in ASP.NET Core](../../../security/authorization/simple?view=aspnetcore-10.0)
* [Simple authorization in ASP.NET Core MVC](../../../mvc/security/authorization/simple?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/modules
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/modules.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# IIS modules with ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Some of the native IIS modules and all of the IIS managed modules aren't able to process requests for ASP.NET Core apps. In many cases, ASP.NET Core offers an alternative to the scenarios addressed by IIS native and managed modules.

## Native modules

The table indicates native IIS modules that are functional with ASP.NET Core apps and the ASP.NET Core Module.

| Module                                                                                        | Functional with ASP.NET Core apps | ASP.NET Core Option                                                                                             |
| --------------------------------------------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Anonymous Authentication** `AnonymousAuthenticationModule`                                  | Yes                               |                                                                                                                 |
| **Basic Authentication** `BasicAuthenticationModule`                                          | Yes                               |                                                                                                                 |
| **Client Certification Mapping Authentication** `CertificateMappingAuthenticationModule`      | Yes                               |                                                                                                                 |
| **CGI** `CgiModule`                                                                           | No                                |                                                                                                                 |
| **Configuration Validation** `ConfigurationValidationModule`                                  | Yes                               |                                                                                                                 |
| **HTTP Errors** `CustomErrorModule`                                                           | No                                | [Status code pages middleware](../../fundamentals/error-handling?view=aspnetcore-10.0#usestatuscodepages)       |
| **Custom Logging** `CustomLoggingModule`                                                      | Yes                               |                                                                                                                 |
| **Default Document** `DefaultDocumentModule`                                                  | No                                | [Default files middleware](../../fundamentals/static-files?view=aspnetcore-10.0#serve-a-default-document)       |
| **Digest Authentication** `DigestAuthenticationModule`                                        | Yes                               |                                                                                                                 |
| **Directory Browsing** `DirectoryListingModule`                                               | No                                | [Directory-browsing middleware](../../fundamentals/static-files?view=aspnetcore-10.0#enable-directory-browsing) |
| **Dynamic Compression** `DynamicCompressionModule`                                            | Yes                               | [Response compression middleware](../../performance/response-compression?view=aspnetcore-10.0)                  |
| **Failed Requests Tracing** `FailedRequestsTracingModule`                                     | Yes                               | [ASP.NET Core Logging](../../fundamentals/logging/?view=aspnetcore-10.0#dotnet-trace-tooling)                   |
| **File Caching** `FileCacheModule`                                                            | No                                | [Response caching middleware](../../performance/caching/middleware?view=aspnetcore-10.0)                        |
| **HTTP Caching** `HttpCacheModule`                                                            | No                                | [Response caching middleware](../../performance/caching/middleware?view=aspnetcore-10.0)                        |
| **HTTP Logging** `HttpLoggingModule`                                                          | Yes                               | [ASP.NET Core Logging](../../fundamentals/logging/?view=aspnetcore-10.0)                                        |
| **HTTP Redirection** `HttpRedirectionModule`                                                  | Yes                               | [URL rewriting middleware](../../fundamentals/url-rewriting?view=aspnetcore-10.0)                               |
| **HTTP Tracing** `TracingModule`                                                              | Yes                               |                                                                                                                 |
| **IIS Client Certificate Mapping Authentication** `IISCertificateMappingAuthenticationModule` | Yes                               |                                                                                                                 |
| **IP and Domain Restrictions** `IpRestrictionModule`                                          | Yes                               |                                                                                                                 |
| **ISAPI Filters** `IsapiFilterModule`                                                         | Yes                               | [Middleware](../../fundamentals/middleware/?view=aspnetcore-10.0)                                               |
| **ISAPI** `IsapiModule`                                                                       | Yes                               | [Middleware](../../fundamentals/middleware/?view=aspnetcore-10.0)                                               |
| **Protocol Support** `ProtocolSupportModule`                                                  | Yes                               |                                                                                                                 |
| **Request Filtering** `RequestFilteringModule`                                                | Yes                               | [URL rewriting middleware `IRule`](../../fundamentals/url-rewriting?view=aspnetcore-10.0#irule-based-rule)      |
| **Request Monitor** `RequestMonitorModule`                                                    | Yes                               |                                                                                                                 |
| **URL Rewriting**† `RewriteModule`                                                            | Yes                               | [URL rewriting middleware](../../fundamentals/url-rewriting?view=aspnetcore-10.0)                               |
| **Server-Side Includes** `ServerSideIncludeModule`                                            | No                                |                                                                                                                 |
| **Static Compression** `StaticCompressionModule`                                              | No                                | [Response compression middleware](../../performance/response-compression?view=aspnetcore-10.0)                  |
| **Static Content** `StaticFileModule`                                                         | No                                | [Static file middleware](../../fundamentals/static-files?view=aspnetcore-10.0)                                  |
| **Token Caching** `TokenCacheModule`                                                          | Yes                               |                                                                                                                 |
| **URI Caching** `UriCacheModule`                                                              | Yes                               |                                                                                                                 |
| **URL Authorization** `UrlAuthorizationModule`                                                | Yes                               | [ASP.NET Core Identity](../../security/authentication/identity?view=aspnetcore-10.0)                            |
| **WebDav** `WebDAV`                                                                           | No                                |                                                                                                                 |
| **Windows Authentication** `WindowsAuthenticationModule`                                      | Yes                               |                                                                                                                 |

†The URL Rewrite Module's `isFile` and `isDirectory` match types don't work with ASP.NET Core apps due to the changes in [directory structure](../directory-structure?view=aspnetcore-10.0).

## Managed modules

Managed modules are *not* functional with hosted ASP.NET Core apps when the app pool's .NET CLR version is set to **No Managed Code**. ASP.NET Core offers middleware alternatives in several cases.

| Module                  | ASP.NET Core Option                                                                           |
| ----------------------- | --------------------------------------------------------------------------------------------- |
| AnonymousIdentification |                                                                                               |
| DefaultAuthentication   |                                                                                               |
| FileAuthorization       |                                                                                               |
| FormsAuthentication     | [Cookie authentication middleware](../../security/authentication/cookie?view=aspnetcore-10.0) |
| OutputCache             | [Response caching middleware](../../performance/caching/middleware?view=aspnetcore-10.0)      |
| Profile                 |                                                                                               |
| RoleManager             |                                                                                               |
| ScriptModule-4.0        |                                                                                               |
| Session                 | [Session middleware](../../fundamentals/app-state?view=aspnetcore-10.0)                       |
| UrlAuthorization        |                                                                                               |
| UrlMappingsModule       | [URL rewriting middleware](../../fundamentals/url-rewriting?view=aspnetcore-10.0)             |
| UrlRoutingModule-4.0    | [ASP.NET Core Identity](../../security/authentication/identity?view=aspnetcore-10.0)          |
| WindowsAuthentication   |                                                                                               |

## IIS Manager application changes

When using IIS Manager to configure settings, the *web.config* file of the app is changed. If deploying an app and including *web.config*, any changes made with IIS Manager are overwritten by the deployed *web.config* file. If changes are made to the server's *web.config* file, copy the updated *web.config* file on the server to the local project immediately.

## Disabling IIS modules

If an IIS module is configured at the server level that must be disabled for an app, an addition to the app's *web.config* file can disable the module. Either leave the module in place and deactivate it using a configuration setting (if available) or remove the module from the app.

### Module deactivation

Many modules offer a configuration setting that allows them to be disabled without removing the module from the app. This is the simplest and quickest way to deactivate a module. For example, the HTTP Redirection Module can be disabled with the `<httpRedirect>` element in *web.config*:

```
<configuration>
  <system.webServer>
    <httpRedirect enabled="false" />
  </system.webServer>
</configuration>
```

For more information on disabling modules with configuration settings, follow the links in the *Child Elements* section of [IIS \<system.webServer>](/en-us/iis/configuration/system.webServer/).

### Module removal

If opting to remove a module with a setting in *web.config*, unlock the module and unlock the `<modules>` section of *web.config* first:

1. Unlock the module at the server level. Select the IIS server in the IIS Manager **Connections** sidebar. Open the **Modules** in the **IIS** area. Select the module in the list. In the **Actions** sidebar on the right, select **Unlock**. If the action entry for the module appears as **Lock**, the module is already unlocked, and no action is required. Unlock as many modules as you plan to remove from *web.config* later.

2. Deploy the app without a `<modules>` section in *web.config*. If an app is deployed with a *web.config* containing the `<modules>` section without having unlocked the section first in the IIS Manager, the Configuration Manager throws an exception when attempting to unlock the section. Therefore, deploy the app without a `<modules>` section.

3. Unlock the `<modules>` section of *web.config*. In the **Connections** sidebar, select the website in **Sites**. In the **Management** area, open the **Configuration Editor**. Use the navigation controls to select the `system.webServer/modules` section. In the **Actions** sidebar on the right, select to **Unlock** the section. If the action entry for the module section appears as **Lock Section**, the module section is already unlocked, and no action is required.

4. Add a `<modules>` section to the app's local *web.config* file with a `<remove>` element to remove the module from the app. Add multiple `<remove>` elements to remove multiple modules. If *web.config* changes are made on the server, immediately make the same changes to the project's *web.config* file locally. Removing a module using this approach doesn't affect the use of the module with other apps on the server.

   ```
   <configuration>
    <system.webServer>
      <modules>
        <remove name="MODULE_NAME" />
      </modules>
    </system.webServer>
   </configuration>
   ```

In order to add or remove modules for IIS Express using *web.config*, modify *applicationHost.config* to unlock the `<modules>` section:

1. Open *{APPLICATION ROOT}\\.vs\config\applicationhost.config*.

2. Locate the `<section>` element for IIS modules and change `overrideModeDefault` from `Deny` to `Allow`:

   ```
   <section name="modules"
            allowDefinition="MachineToApplication"
            overrideModeDefault="Allow" />
   ```

3. Locate the `<location path="" overrideMode="Allow"><system.webServer><modules>` section. For any modules that you wish to remove, set `lockItem` from `true` to `false`. In the following example, the CGI Module is unlocked:

   ```
   <add name="CgiModule" lockItem="false" />
   ```

4. After the `<modules>` section and individual modules are unlocked, you're free to add or remove IIS modules using the app's *web.config* file for running the app on IIS Express.

An IIS module can also be removed with *Appcmd.exe*. Provide the `MODULE_NAME` and `APPLICATION_NAME` in the command:

```
Appcmd.exe delete module MODULE_NAME /app.name:APPLICATION_NAME
```

For example, remove the `DynamicCompressionModule` from the Default Web Site:

```
%windir%\system32\inetsrv\appcmd.exe delete module DynamicCompressionModule /app.name:"Default Web Site"
```

## Minimum module configuration

The only modules required to run an ASP.NET Core app are the Anonymous Authentication Module and the ASP.NET Core Module.

The URI Caching Module (`UriCacheModule`) allows IIS to cache website configuration at the URL level. Without this module, IIS must read and parse configuration on every request, even when the same URL is repeatedly requested. Parsing the configuration every request results in a significant performance penalty. *Although the URI Caching Module isn't strictly required for a hosted ASP.NET Core app to run, we recommend that the URI Caching Module be enabled for all ASP.NET Core deployments.*

The HTTP Caching Module (`HttpCacheModule`) implements the IIS output cache and also the logic for caching items in the HTTP.sys cache. Without this module, content is no longer cached in kernel mode, and cache profiles are ignored. Removing the HTTP Caching Module usually has adverse effects on performance and resource usage. *Although the HTTP Caching Module isn't strictly required for a hosted ASP.NET Core app to run, we recommend that the HTTP Caching Module be enabled for all ASP.NET Core deployments.*

## Related content

* [Introduction to IIS Architectures: Modules in IIS](/en-us/iis/get-started/introduction-to-iis/introduction-to-iis-architecture#modules-in-iis)
* [IIS Modules Overview](/en-us/iis/get-started/introduction-to-iis/iis-modules-overview)
* [Customizing IIS 7.0 Roles and Modules](/en-us/previous-versions/tn-archive/cc627313\(v=technet.10\))
* [IIS \<system.webServer>](/en-us/iis/configuration/system.webServer/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/preventing-open-redirects
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/preventing-open-redirects.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Prevent open redirect attacks in ASP.NET Core

A web app that redirects to a URL that's specified via the request such as the query string or form data can potentially be tampered with to redirect users to an external, malicious URL. This tampering is called an open redirection attack.

Whenever your application logic redirects to a specified URL, you must verify that the redirection URL hasn't been tampered with. ASP.NET Core has built-in functionality to help protect apps from open redirect (also known as open redirection) attacks.

## What is an open redirect attack?

Web applications frequently redirect users to a login page when they access resources that require authentication. The redirection typically includes a `returnUrl` query string parameter so that the user can be returned to the originally requested URL after they have successfully logged in. After the user authenticates, they're redirected to the URL they had originally requested.

Because the destination URL is specified in the query string of the request, a malicious user could tamper with the query string. A tampered query string could allow the site to redirect the user to an external, malicious site. This technique is called an open redirect (or redirection) attack.

### An example attack

A malicious user can develop an attack intended to allow the malicious user access to a user's credentials or sensitive information. To begin the attack, the malicious user convinces the user to click a link to your site's login page with a `returnUrl` query string value added to the URL. For example, consider an app at `contoso.com` that includes a login page at `http://contoso.com/Account/LogOn?returnUrl=/Home/About`. The attack follows these steps:

1. The user clicks a malicious link to `http://contoso.com/Account/LogOn?returnUrl=http://contoso1.com/Account/LogOn` (the second URL is "contoso**1**.com", not "contoso.com").
2. The user logs in successfully.
3. The user is redirected (by the site) to `http://contoso1.com/Account/LogOn` (a malicious site that looks exactly like real site).
4. The user logs in again (giving malicious site their credentials) and is redirected back to the real site.

The user likely believes that their first attempt to log in failed and that their second attempt is successful. The user most likely remains unaware that their credentials are compromised.

In addition to login pages, some sites provide redirect pages or endpoints. Imagine your app has a page with an open redirect, `/Home/Redirect`. A cyberattacker could create, for example, a link in an email that goes to `[yoursite]/Home/Redirect?url=http://phishingsite.com/Home/Login`. A typical user will look at the URL and see it begins with your site name. Trusting that, they will click the link. The open redirect would then send the user to the phishing site, which looks identical to yours, and the user would likely login to what they believe is your site.

## Protecting against open redirect attacks

When developing web applications, treat all user-provided data as untrustworthy. If your application has functionality that redirects the user based on the contents of the URL, ensure that such redirects are only done locally within your app (or to a known URL, not any URL that may be supplied in the query string).

### LocalRedirect

Use the [LocalRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.results.localredirect) result in a Minimal API endpoint:

```
app.MapGet("/SomeEndpoint", (string redirectUri) =>
{
    ...

    return Results.LocalRedirect(redirectUri);
});
```

Use the [LocalRedirect](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.localredirect) helper method in MVC actions:

```
public IActionResult SomeAction(string redirectUrl)
{
    return LocalRedirect(redirectUrl);
}
```

`LocalRedirect` throws an exception if a non-local URL is specified. Otherwise, it behaves just like the `Redirect` method.

### IUrlHelper.IsLocalUrl

Use the [IsLocalUrl](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iurlhelper.islocalurl) method to test URLs before redirecting:

The following example shows how to check whether a URL is local before redirecting.

```
private IActionResult RedirectToLocal(string returnUrl)
{
    if (Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }
    else
    {
        return RedirectToAction(nameof(HomeController.Index), "Home");
    }
}
```

The `IUrlHelper.IsLocalUrl` method protects users from being inadvertently redirected to a malicious site. You can log the details of the URL that was provided when a non-local URL is supplied in a situation where you expected a local URL. Logging redirect URLs may help in diagnosing redirection attacks.

### Detect if URL is local using `RedirectHttpResult.IsLocalUrl`

The [`RedirectHttpResult.IsLocalUrl(url)`](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/RedirectHttpResult.cs,c0ece2e6266cb369) helper method detects if a URL is local. A URL is considered local if the following are true:

* It doesn't have the [host](https://developer.mozilla.org/docs/Web/API/URL/host) or [authority](https://developer.mozilla.org/docs/Web/URI/Authority) section.
* It has an [absolute path](https://developer.mozilla.org/docs/Learn_web_development/Howto/Web_mechanics/What_is_a_URL#absolute_urls_vs._relative_urls).

URLs using [virtual paths](/en-us/previous-versions/aspnet/ms178116\(v=vs.100\)) `"~/"` are also local.

`IsLocalUrl` is useful for validating URLs before redirecting to them to prevent [open redirection attacks](https://brightsec.com/blog/open-redirect-vulnerabilities/).

```
if (RedirectHttpResult.IsLocalUrl(url))
{
    return Results.LocalRedirect(url);
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/extensibility-third-party-container
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/middleware/extensibility-third-party-container.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Middleware activation with a third-party container in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article demonstrates how to use [IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) and [IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) as an extensibility point for [middleware](./?view=aspnetcore-10.0) activation with a third-party container. For introductory information on `IMiddlewareFactory` and `IMiddleware`, see [Factory-based middleware activation in ASP.NET Core](extensibility?view=aspnetcore-10.0).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/middleware/extensibility-third-party-container/samples/) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app demonstrates middleware activation by an `IMiddlewareFactory` implementation, `SimpleInjectorMiddlewareFactory`. The sample uses the [Simple Injector](https://simpleinjector.org) dependency injection (DI) container.

The sample's middleware implementation records the value provided by a query string parameter (`key`). The middleware uses an injected database context (a scoped service) to record the query string value in an in-memory database.

Note

The sample app uses [Simple Injector](https://github.com/simpleinjector/SimpleInjector) purely for demonstration purposes. Use of Simple Injector isn't an endorsement. Middleware activation approaches described in the Simple Injector documentation and GitHub issues are recommended by the maintainers of Simple Injector. For more information, see the [Simple Injector documentation](https://simpleinjector.readthedocs.io/en/latest/index.html) and [Simple Injector GitHub repository](https://github.com/simpleinjector/SimpleInjector).

## IMiddlewareFactory

[IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) provides methods to create middleware.

In the sample app, a middleware factory is implemented to create a `SimpleInjectorActivatedMiddleware` instance. The middleware factory uses the Simple Injector container to resolve the middleware:

```
public class SimpleInjectorMiddlewareFactory : IMiddlewareFactory
{
    private readonly Container _container;

    public SimpleInjectorMiddlewareFactory(Container container)
    {
        _container = container;
    }

    public IMiddleware Create(Type middlewareType)
    {
        return _container.GetInstance(middlewareType) as IMiddleware;
    }

    public void Release(IMiddleware middleware)
    {
        // The container is responsible for releasing resources.
    }
}
```

## IMiddleware

[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) defines middleware for the app's request pipeline.

Middleware activated by an `IMiddlewareFactory` implementation (`Middleware/SimpleInjectorActivatedMiddleware.cs`):

```
public class SimpleInjectorActivatedMiddleware : IMiddleware
{
    private readonly AppDbContext _db;

    public SimpleInjectorActivatedMiddleware(AppDbContext db)
    {
        _db = db;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            _db.Add(new Request()
                {
                    DT = DateTime.UtcNow, 
                    MiddlewareActivation = "SimpleInjectorActivatedMiddleware", 
                    Value = keyValue
                });

            await _db.SaveChangesAsync();
        }

        await next(context);
    }
}
```

An extension is created for the middleware (`Middleware/MiddlewareExtensions.cs`):

```
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseSimpleInjectorActivatedMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SimpleInjectorActivatedMiddleware>();
    }
}
```

`Startup.ConfigureServices` must perform several tasks:

* Set up the Simple Injector container.
* Register the factory and middleware.
* Make the app's database context available from the Simple Injector container.

```
private Container _container = new Container();

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    // Replace the default middleware factory with the 
    // SimpleInjectorMiddlewareFactory.
    services.AddTransient<IMiddlewareFactory>(_ =>
    {
        return new SimpleInjectorMiddlewareFactory(_container);
    });

    // Wrap ASP.NET Core requests in a Simple Injector execution 
    // context.
    services.UseSimpleInjectorAspNetRequestScoping(_container);

    // Provide the database context from the Simple 
    // Injector container whenever it's requested from 
    // the default service container.
    services.AddScoped<AppDbContext>(provider => 
        _container.GetInstance<AppDbContext>());

    _container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

    _container.Register<AppDbContext>(() => 
    {
        var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
        optionsBuilder.UseInMemoryDatabase("InMemoryDb");
        return new AppDbContext(optionsBuilder.Options);
    }, Lifestyle.Scoped);

    _container.Register<SimpleInjectorActivatedMiddleware>();

    _container.Verify();
}
```

The middleware is registered in the request processing pipeline in `Startup.Configure`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseSimpleInjectorActivatedMiddleware();

    app.UseStaticFiles();
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

This article demonstrates how to use [IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) and [IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) as an extensibility point for [middleware](./?view=aspnetcore-10.0) activation with a third-party container. For introductory information on `IMiddlewareFactory` and `IMiddleware`, see [Factory-based middleware activation in ASP.NET Core](extensibility?view=aspnetcore-10.0).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/middleware/extensibility-third-party-container/samples/) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app demonstrates middleware activation by an `IMiddlewareFactory` implementation, `SimpleInjectorMiddlewareFactory`. The sample uses the [Simple Injector](https://simpleinjector.org) dependency injection (DI) container.

The sample's middleware implementation records the value provided by a query string parameter (`key`). The middleware uses an injected database context (a scoped service) to record the query string value in an in-memory database.

Note

The sample app uses [Simple Injector](https://github.com/simpleinjector/SimpleInjector) purely for demonstration purposes. Use of Simple Injector isn't an endorsement. Middleware activation approaches described in the Simple Injector documentation and GitHub issues are recommended by the maintainers of Simple Injector. For more information, see the [Simple Injector documentation](https://simpleinjector.readthedocs.io/en/latest/index.html) and [Simple Injector GitHub repository](https://github.com/simpleinjector/SimpleInjector).

## IMiddlewareFactory

[IMiddlewareFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddlewarefactory) provides methods to create middleware.

In the sample app, a middleware factory is implemented to create a `SimpleInjectorActivatedMiddleware` instance. The middleware factory uses the Simple Injector container to resolve the middleware:

```
public class SimpleInjectorMiddlewareFactory : IMiddlewareFactory
{
    private readonly Container _container;

    public SimpleInjectorMiddlewareFactory(Container container)
    {
        _container = container;
    }

    public IMiddleware Create(Type middlewareType)
    {
        return _container.GetInstance(middlewareType) as IMiddleware;
    }

    public void Release(IMiddleware middleware)
    {
        // The container is responsible for releasing resources.
    }
}
```

## IMiddleware

[IMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware) defines middleware for the app's request pipeline.

Middleware activated by an `IMiddlewareFactory` implementation (`Middleware/SimpleInjectorActivatedMiddleware.cs`):

```
public class SimpleInjectorActivatedMiddleware : IMiddleware
{
    private readonly AppDbContext _db;

    public SimpleInjectorActivatedMiddleware(AppDbContext db)
    {
        _db = db;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            _db.Add(new Request()
                {
                    DT = DateTime.UtcNow, 
                    MiddlewareActivation = "SimpleInjectorActivatedMiddleware", 
                    Value = keyValue
                });

            await _db.SaveChangesAsync();
        }

        await next(context);
    }
}
```

An extension is created for the middleware (`Middleware/MiddlewareExtensions.cs`):

```
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseSimpleInjectorActivatedMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SimpleInjectorActivatedMiddleware>();
    }
}
```

`Startup.ConfigureServices` must perform several tasks:

* Set up the Simple Injector container.
* Register the factory and middleware.
* Make the app's database context available from the Simple Injector container.

```
private Container _container = new Container();

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    // Replace the default middleware factory with the 
    // SimpleInjectorMiddlewareFactory.
    services.AddTransient<IMiddlewareFactory>(_ =>
    {
        return new SimpleInjectorMiddlewareFactory(_container);
    });

    // Wrap ASP.NET Core requests in a Simple Injector execution 
    // context.
    services.UseSimpleInjectorAspNetRequestScoping(_container);

    // Provide the database context from the Simple 
    // Injector container whenever it's requested from 
    // the default service container.
    services.AddScoped<AppDbContext>(provider => 
        _container.GetInstance<AppDbContext>());

    _container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

    _container.Register<AppDbContext>(() => 
    {
        var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
        optionsBuilder.UseInMemoryDatabase("InMemoryDb");
        return new AppDbContext(optionsBuilder.Options);
    }, Lifestyle.Scoped);

    _container.Register<SimpleInjectorActivatedMiddleware>();

    _container.Verify();
}
```

The middleware is registered in the request processing pipeline in `Startup.Configure`:

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseSimpleInjectorActivatedMiddleware();

    app.UseStaticFiles();
    app.UseMvc();
}
```

## Related content

* [Middleware](./?view=aspnetcore-10.0)
* [Factory-based middleware activation](extensibility?view=aspnetcore-10.0)
* [Simple Injector GitHub repository](https://github.com/simpleinjector/SimpleInjector)
* [Simple Injector documentation](https://simpleinjector.readthedocs.io/en/latest/index.html)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/middleware/write.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Write custom ASP.NET Core middleware

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Fiyaz Hasan](https://twitter.com/FiyazBinHasan), [Rick Anderson](https://twitter.com/RickAndMSFT), and [Steve Smith](https://ardalis.com/)

Middleware is software that's assembled into an app pipeline to handle requests and responses. ASP.NET Core provides a rich set of built-in middleware components, but in some scenarios you might want to write a custom middleware.

This topic describes how to write *convention-based* middleware. For an approach that uses strong typing and per-request activation, see [Factory-based middleware activation in ASP.NET Core](extensibility?view=aspnetcore-10.0).

## Middleware class

Middleware is generally encapsulated in a class and exposed with an extension method. Consider the following inline middleware, which sets the culture for the current request from a query string:

```
using System.Globalization;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();

app.Use(async (context, next) =>
{
    var cultureQuery = context.Request.Query["culture"];
    if (!string.IsNullOrWhiteSpace(cultureQuery))
    {
        var culture = new CultureInfo(cultureQuery);

        CultureInfo.CurrentCulture = culture;
        CultureInfo.CurrentUICulture = culture;
    }

    // Call the next delegate/middleware in the pipeline.
    await next(context);
});

app.Run(async (context) =>
{
    await context.Response.WriteAsync(
        $"CurrentCulture.DisplayName: {CultureInfo.CurrentCulture.DisplayName}");
});

app.Run();
```

The preceding highlighted inline middleware is used to demonstrate creating a middleware component by calling [Microsoft.AspNetCore.Builder.UseExtensions.Use](/en-us/dotnet/api/microsoft.aspnetcore.builder.useextensions.use). The preceding `Use` extension method adds a middleware [delegate](/en-us/dotnet/csharp/programming-guide/delegates/) defined in-line to the application's request pipeline.

There are two overloads available for the `Use` extension:

* One takes a [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) and a `Func<Task>`. Invoke the `Func<Task>` without any parameters.
* The other takes a `HttpContext` and a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate). Invoke the `RequestDelegate` by passing the `HttpContext`.

Prefer using the later overload as it saves two internal per-request allocations that are required when using the other overload.

Test the middleware by passing in the culture. For example, request `https://localhost:5001/?culture=es-es`.

For ASP.NET Core's built-in localization support, see [Globalization and localization in ASP.NET Core](../localization?view=aspnetcore-10.0).

The following code moves the middleware delegate to a class:

```
using System.Globalization;

namespace Middleware.Example;

public class RequestCultureMiddleware
{
    private readonly RequestDelegate _next;

    public RequestCultureMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var cultureQuery = context.Request.Query["culture"];
        if (!string.IsNullOrWhiteSpace(cultureQuery))
        {
            var culture = new CultureInfo(cultureQuery);

            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = culture;
        }

        // Call the next delegate/middleware in the pipeline.
        await _next(context);
    }
}
```

The middleware class must include:

* A public constructor with a parameter of type [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate).

* A public method named `Invoke` or `InvokeAsync`. This method must:

  * Return a `Task`.
  * Accept a first parameter of type [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext).

Additional parameters for the constructor and `Invoke`/`InvokeAsync` are populated by [dependency injection (DI)](../dependency-injection?view=aspnetcore-10.0).

Typically, an extension method is created to expose the middleware through [IApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder):

```
using System.Globalization;

namespace Middleware.Example;

public class RequestCultureMiddleware
{
    private readonly RequestDelegate _next;

    public RequestCultureMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var cultureQuery = context.Request.Query["culture"];
        if (!string.IsNullOrWhiteSpace(cultureQuery))
        {
            var culture = new CultureInfo(cultureQuery);

            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = culture;
        }

        // Call the next delegate/middleware in the pipeline.
        await _next(context);
    }
}

public static class RequestCultureMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestCulture(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestCultureMiddleware>();
    }
}
```

The following code calls the middleware from `Program.cs`:

```
using Middleware.Example;
using System.Globalization;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();

app.UseRequestCulture();

app.Run(async (context) =>
{
    await context.Response.WriteAsync(
        $"CurrentCulture.DisplayName: {CultureInfo.CurrentCulture.DisplayName}");
});

app.Run();
```

## Middleware dependencies

Middleware should follow the [Explicit Dependencies Principle](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#explicit-dependencies) by exposing its dependencies in its constructor. Middleware is constructed once per *application lifetime*.

Middleware components can resolve their dependencies from [dependency injection (DI)](../dependency-injection?view=aspnetcore-10.0) through constructor parameters. [UseMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.builder.usemiddlewareextensions.usemiddleware) can also accept additional parameters directly.

## Per-request middleware dependencies

Middleware is constructed at app startup and therefore has application life time. [Scoped lifetime](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes#scoped) services used by middleware constructors aren't shared with other dependency-injected types during each request. To share a *scoped* service between middleware and other types, add these services to the `InvokeAsync` method's signature. The `InvokeAsync` method can accept additional parameters that are populated by DI:

```
namespace Middleware.Example;

public class MyCustomMiddleware
{
    private readonly RequestDelegate _next;

    public MyCustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    // IMessageWriter is injected into InvokeAsync
    public async Task InvokeAsync(HttpContext httpContext, IMessageWriter svc)
    {
        svc.Write(DateTime.Now.Ticks.ToString());
        await _next(httpContext);
    }
}

public static class MyCustomMiddlewareExtensions
{
    public static IApplicationBuilder UseMyCustomMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyCustomMiddleware>();
    }
}
```

[Lifetime and registration options](../dependency-injection?view=aspnetcore-10.0#lifetime-and-registration-options) contains a complete sample of middleware with *scoped* lifetime services.

The following code is used to test the preceding middleware:

```
using Middleware.Example;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMessageWriter, LoggingMessageWriter>();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseMyCustomMiddleware();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The `IMessageWriter` interface and implementation:

```
namespace Middleware.Example;

public interface IMessageWriter
{
    void Write(string message);
}

public class LoggingMessageWriter : IMessageWriter
{

    private readonly ILogger<LoggingMessageWriter> _logger;

    public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) =>
        _logger = logger;

    public void Write(string message) =>
        _logger.LogInformation(message);
}
```

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Steve Smith](https://ardalis.com/)

Middleware is software that's assembled into an app pipeline to handle requests and responses. ASP.NET Core provides a rich set of built-in middleware components, but in some scenarios you might want to write a custom middleware.

Note

This topic describes how to write *convention-based* middleware. For an approach that uses strong typing and per-request activation, see [Factory-based middleware activation in ASP.NET Core](extensibility?view=aspnetcore-10.0).

## Middleware class

Middleware is generally encapsulated in a class and exposed with an extension method. Consider the following middleware, which sets the culture for the current request from a query string:

```
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            await next();
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}
```

The preceding sample code is used to demonstrate creating a middleware component. For ASP.NET Core's built-in localization support, see [Globalization and localization in ASP.NET Core](../localization?view=aspnetcore-10.0).

Test the middleware by passing in the culture. For example, request `https://localhost:5001/?culture=no`.

The following code moves the middleware delegate to a class:

```
using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
    public class RequestCultureMiddleware
    {
        private readonly RequestDelegate _next;

        public RequestCultureMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;

            }

            // Call the next delegate/middleware in the pipeline
            await _next(context);
        }
    }
}
```

The middleware class must include:

* A public constructor with a parameter of type [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate).

* A public method named `Invoke` or `InvokeAsync`. This method must:

  * Return a `Task`.
  * Accept a first parameter of type [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext).

Additional parameters for the constructor and `Invoke`/`InvokeAsync` are populated by [dependency injection (DI)](../dependency-injection?view=aspnetcore-10.0).

## Middleware dependencies

Middleware should follow the [Explicit Dependencies Principle](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#explicit-dependencies) by exposing its dependencies in its constructor. Middleware is constructed once per *application lifetime*. See the [Per-request middleware dependencies](#per-request-middleware-dependencies) section if you need to share services with middleware within a request.

Middleware components can resolve their dependencies from [dependency injection (DI)](../dependency-injection?view=aspnetcore-10.0) through constructor parameters. [UseMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.builder.usemiddlewareextensions.usemiddleware) can also accept additional parameters directly.

## Per-request middleware dependencies

Because middleware is constructed at app startup, not per-request, *scoped* lifetime services used by middleware constructors aren't shared with other dependency-injected types during each request. If you must share a *scoped* service between your middleware and other types, add these services to the `InvokeAsync` method's signature. The `InvokeAsync` method can accept additional parameters that are populated by DI:

```
public class CustomMiddleware
{
    private readonly RequestDelegate _next;

    public CustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    // IMyScopedService is injected into InvokeAsync
    public async Task InvokeAsync(HttpContext httpContext, IMyScopedService svc)
    {
        svc.MyProperty = 1000;
        await _next(httpContext);
    }
}
```

[Lifetime and registration options](../dependency-injection?view=aspnetcore-10.0#lifetime-and-registration-options) contains a complete sample of middleware with *scoped* lifetime services.

## Middleware extension method

The following extension method exposes the middleware through [IApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder):

```
using Microsoft.AspNetCore.Builder;

namespace Culture
{
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestCulture(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestCultureMiddleware>();
        }
    }
}
```

The following code calls the middleware from `Startup.Configure`:

```
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestCulture();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });
    }
}
```

## Related content

* [Sample code used in this article](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/middleware/write/6sample)
* [UseExtensions source on GitHub](https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs)
* [Lifetime and registration options](../dependency-injection?view=aspnetcore-10.0#lifetime-and-registration-options) contains a complete sample of middleware with *scoped*, *transient*, and *singleton* lifetime services.
* [DEEP DIVE: HOW IS THE ASP.NET CORE MIDDLEWARE PIPELINE BUILT](https://www.stevejgordon.co.uk/how-is-the-asp-net-core-middleware-pipeline-built)
* [ASP.NET Core middleware](./?view=aspnetcore-10.0)
* [Test ASP.NET Core middleware](../../test/middleware?view=aspnetcore-10.0)
* [Migrate HTTP modules to ASP.NET Core middleware](../../migration/fx-to-core/areas/http-modules?view=aspnetcore-10.0)
* [App startup in ASP.NET Core](../startup?view=aspnetcore-10.0)
* [Request Features in ASP.NET Core](../request-features?view=aspnetcore-10.0)
* [Factory-based middleware activation in ASP.NET Core](extensibility?view=aspnetcore-10.0)
* [Middleware activation with a third-party container in ASP.NET Core](extensibility-third-party-container?view=aspnetcore-10.0)

- [Lifetime and registration options](../dependency-injection?view=aspnetcore-10.0#lifetime-and-registration-options) contains a complete sample of middleware with *scoped*, *transient*, and *singleton* lifetime services.
- [ASP.NET Core middleware](./?view=aspnetcore-10.0)
- [Test ASP.NET Core middleware](../../test/middleware?view=aspnetcore-10.0)
- [Migrate HTTP modules to ASP.NET Core middleware](../../migration/fx-to-core/areas/http-modules?view=aspnetcore-10.0)
- [App startup in ASP.NET Core](../startup?view=aspnetcore-10.0)
- [Request Features in ASP.NET Core](../request-features?view=aspnetcore-10.0)
- [Factory-based middleware activation in ASP.NET Core](extensibility?view=aspnetcore-10.0)
- [Middleware activation with a third-party container in ASP.NET Core](extensibility-third-party-container?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0004
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0004.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0004: Do not use action results with route handlers

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0004      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A route handler delegate returns a value that implements [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult).

## Rule description

Route handler endpoints do not support executing MVC's `IActionResult` instances. Returning an `IActionResult` that doesn't implement `IResult` results in serializing the result instance rather than executing the result.

```
app.MapGet("/todos/{id}", (int id) => new JsonResult(new Todo { .. }));
```

## How to fix violations

To fix a violation of this rule, make sure that endpoint's route handler returns an [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) type by using the [Results](/en-us/dotnet/api/microsoft.aspnetcore.http.results) extension methods.

```
app.MapGet("/todos/{id}", (int id) => Results.Json(new Todo { .. }));
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule. Returning an `IActionResult` that doesn't implement `IResult` results in serializing the result instance rather than executing the result.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/passkeys/blazor
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/passkeys/blazor.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Implement passkeys in ASP.NET Core Blazor Web Apps

This guide explains how to implement [passkey support](./?view=aspnetcore-10.0) for a new or existing Blazor Web App with ASP.NET Core Identity.

For an overview of passkeys and general configuration guidance, see [Enable Web Authentication API (WebAuthn) passkeys](./?view=aspnetcore-10.0).

## Prerequisites

[.NET SDK](https://dotnet.microsoft.com/download) (.NET 10 or later)

## Create a Blazor Web App

Use the following guidance to create a new Blazor Web App with ASP.NET Core Identity, which includes passkeys support.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [.NET CLI](#tabpanel_1_net-cli)

Note

Visual Studio 2022 or later and .NET 10 or later SDK are required.

In Visual Studio:

* Select **Create a new project** from the **Start Window** or select **File** > **New** > **Project** from the menu bar.

* In the **Create a new project** dialog, select **Blazor Web App** from the list of project templates. Select the **Next** button.

* In the **Configure your new project** dialog, name the project `BlazorWebAppPasskeys` in the **Project name** field, including matching the capitalization. Using this exact project name is important to ensure that the namespaces match for code that you copy from the article into the app that you're building.

* Confirm that the **Location** for the app is suitable. Leave the **Place solution and project in the same directory** checkbox selected. Select the **Next** button.

* In the **Additional information** dialog, set the **Authentication type** to **Individual Accounts**. Use the following settings for the other options:

  * **Framework**: Latest framework release (.NET 10 or later)
  * **Configure for HTTPS**: Selected
  * **Interactive render mode**: **Server**
  * **Interactivity location**: **Global**
  * **Include sample pages**: Selected
  * **Do not use top-level statements**: Not selected
  * **Use the .dev.localhost TLD in the application URL**: Not selected
  * Select **Create**.

This guidance assumes that you have familiarity with VS Code. If you're new to VS Code, see the [VS Code documentation](https://code.visualstudio.com/docs). The videos listed by the [Introductory Videos page](https://code.visualstudio.com/docs/getstarted/introvideos) are designed to give you an overview of VS Code's features.

In VS Code:

* Go to the **Explorer** view and select the **Create .NET Project** button. Alternatively, you can bring up the **Command Palette** using `Ctrl`+`Shift`+`P`, and then type "`.NET`" and find and select the **.NET: New Project** command.

* Select the **Blazor Web App** project template from the list.

* In the **Project Location** dialog, create or select a folder for the project.

* In the **Command Palette**, name the project `BlazorWebAppPasskeys`, including matching the capitalization. Using this exact project name is important to ensure that the namespaces match for code that you copy from the article into the app that you're building.

* Select **Create project** from the **Command Palette**.

In a command shell:

Change to the directory using the `cd` command to where you want to create the project folder (for example, `cd c:/users/Bernie_Kopell/Documents`).

Use the [`dotnet new` command](/en-us/dotnet/core/tools/dotnet-new) with the [`blazor` project template](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazor) to create a new Blazor Web App project. The [`-o|--output` option](/en-us/dotnet/core/tools/dotnet-new#options) passed to the command creates the project in a new folder named `BlazorWebAppPasskeys` at the current directory location.

Important

Name the project `BlazorWebAppPasskeys`, including matching the capitalization, so the namespaces match for code that you copy from the article to the app.

```
dotnet new blazor -au Individual -o BlazorWebAppPasskeys
```

The preceding instructions create a Blazor Web App with:

* ASP.NET Core Identity configured for user authentication using the [`-au|--authentication` option](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazor).
* Entity Framework Core with SQLite for data storage.
* Passkey registration and authentication endpoints.
* UI components for managing passkeys.

Note

Currently, only the Blazor Web App project template includes built-in passkey support.

## Run the application

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [.NET CLI](#tabpanel_2_net-cli)

Press `F5` to run the app with debugging or `Ctrl`+`F5` to run the app without debugging.

Press `F5` to run the app with debugging or `Ctrl`+`F5` to run the app without debugging.

In a command shell opened to the root folder of the server `BlazorWebAppPasskeys` project, execute the following command:

```
dotnet watch
```

The following guidance relies upon an app that was created with **Individual Accounts** for the app's **Authentication type** or [scaffolding Identity into an existing app](../scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-blazor-project).

## Prerequisites

* An existing Blazor Web App (.NET 10 or later) with ASP.NET Core Identity
* [.NET SDK](https://dotnet.microsoft.com/download) (.NET 10 or later)

For migration guidance, see [Migrate an ASP.NET Core app](../../../migration/?view=aspnetcore-10.0).

## Reference source guidance

The links in this article to .NET reference source load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Update Identity schema version

In `Program.cs`, update the Identity configuration to use schema version 3, which includes passkey support:

```
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
    options.SignIn.RequireConfirmedAccount = true;
    options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
```

## Create and run a database migration

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [.NET CLI](#tabpanel_3_net-cli)

In Visual Studio **Solution Explorer**, double-click **Connected Services**. In the **Service Dependencies** area, select the ellipsis (`...`) followed by **Add migration** in the **SQL Server Express LocalDB** area.

Give the migration a **Migration name** of `AddPasskeySupport` to describe the migration. Wait for the database context to load in the **DbContext class names** field. Select **Finish** to create the migration. Select the **Close** button when the operation completes.

Select the ellipsis (`...`) again followed by the **Update database** command.

The **Update database with the latest migration** dialog opens. Wait for the **DbContext class names** field to update and for prior migrations to load. Select the **Finish** button. Select the **Close** button when the operation completes.

Use the following command in the **Terminal** (**Terminal** menu > **New Terminal**) to add a migration for the new data annotations:

```
dotnet ef migrations add AddPasskeySupport
```

To apply the migration to the database, execute the following command:

```
dotnet ef database update
```

To add a migration for the new data annotations, execute the following command in a command shell opened to the project's root folder:

```
dotnet ef migrations add AddPasskeySupport
```

To apply the migration to the database, execute the following command:

```
dotnet ef database update
```

## Create passkey model classes

Add the following model classes to the project in the `Components/Account` folder and update the `BlazorWebCSharp._1.Components.Account` namespace to match the app (for example: `Contoso.Components.Account`):

* [`Components/Account/PasskeyInputModel.cs`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/PasskeyInputModel.cs): Holds the JSON passkey credential for passkey sign-in operations (`Login` component) and adding passkeys (`Passkeys` component).

* [`Components/Account/PasskeyOperation.cs`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/PasskeyOperation.cs): Defines the authentication action to be performed (`PassKeySubmit` component), either registering a new passkey (`Create`/0) or authenticating with an existing passkey (`Request`/1).

- [`Components/Account/PasskeyAuthenticators.cs`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/PasskeyAuthenticators.cs): Maps Authenticator Attestation GUIDs (AAGUIDs) to friendly display names, so a new passkey from a known authenticator is named automatically instead of prompting the user.

## Create the `PasskeySubmit` component

Add the following `PasskeySubmit` component to handle passkey operations:

[`Components/Account/Shared/PasskeySubmit.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor)

## Add the JavaScript for passkey operations

Add the following JavaScript file to handle WebAuthn API interactions:

[`Components/Account/Shared/PasskeySubmit.razor.js`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js)

## Add passkey endpoints

Update the `IdentityComponentsEndpointRouteBuilderExtensions.cs` file (or create the file if it doesn't exist and call `MapAdditionalIdentityEndpoints` in the [`Program` file](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Program.cs#L129-L130)) to include the passkey-specific endpoints:

[`/PasskeyCreationOptions` and `/PasskeyRequestOptions` endpoints](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs#L80-L132)

## Update the Login page

Replace the existing `Login` component with the following component and update the `BlazorWebCSharp._1.Data` namespace to match the app (for example: `Contoso.Components.Account.Data`):

[`Components/Account/Pages/Login.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor)

## Create passkey management pages for adding and renaming passkeys

Add the following `Passkeys` component for managing passkeys and update the `BlazorWebCSharp._1.Data` namespace to match the app (for example: `Contoso.Components.Account.Data`):

[`Components/Account/Pages/Manage/Passkeys.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor)

Add the following `RenamePasskey` component for renaming passkeys and update the `BlazorWebCSharp._1.Data` namespace to match the app (for example: `Contoso.Components.Account.Data`):

[`Components/Account/Pages/Manage/RenamePasskey.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor)

## Update the manage navigation menu

Add a link to the passkey management page in the app's `ManageNavMenu` component.

In `Components/Account/Shared/ManageNavMenu.razor`, add the following [`NavLink` component](../../../blazor/fundamentals/navigation?view=aspnetcore-10.0#navlink-component) for the `Passkeys` component:

```
<li class="nav-item">
    <NavLink class="nav-link" href="Account/Manage/Passkeys">Passkeys</NavLink>
</li>
```

## Include the JavaScript file

In the `App` component (`Components/App.razor`), locate the [Blazor script](../../../blazor/project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) tag:

```
<script src="@Assets["_framework/blazor.web.js"]"></script>
```

Immediately after the Blazor script tag, add a reference to the `PasskeySubmit` JavaScript module:

```
<script src="@Assets["Components/Account/Shared/PasskeySubmit.razor.js"]" type="module"></script>
```

## Register a passkey

To test passkey functionality:

1. Register a new account or sign in with an existing account.
2. Navigate to **Manage your account** (select the username in the navigation menu).
3. Select **Passkeys** from the navigation menu.
4. Select **Add a new passkey**
5. Follow the browser's prompts to create a passkey using your device's authenticator.

## Sign in with a passkey

After a passkey is registered:

1. Sign out of the app.
2. On the login page, enter your email address.
3. Select **Log in with a passkey**.
4. Follow the browser's prompts to authenticate with your passkey.
5. Navigate to `Account/Manage/Passkeys` to add, rename, or delete passkeys.
6. If the passkey supports passkey autofill (conditional UI) for login, test the passkey autofill feature by selecting the email input field when you have saved passkeys.

## Mitigate `PublicKeyCredential.toJSON` error (`TypeError: Illegal invocation`)

Some password managers don't implement the [`PublicKeyCredential.toJSON` method](https://developer.mozilla.org/docs/Web/API/PublicKeyCredential/toJSON) correctly, which is required for [`JSON.stringify`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) to work when serializing passkey credentials. When registering or authenticating a user with an app based on the Blazor Web App project template, the following error is thrown when attempting to add a passkey:

> Error: Could not add a passkey: Illegal invocation

For guidance on mitigating this error, see [Enable Web Authentication API (WebAuthn) passkeys](./?view=aspnetcore-10.0#mitigate-publickeycredentialtojson-error-typeerror-illegal-invocation).

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-01

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/razor.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Razor syntax reference for ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Taylor Mullen](https://twitter.com/ntaylormullen), and [Dan Vicarel](https://github.com/Rabadash8820)

Razor is a markup syntax for embedding .NET based code into webpages. The Razor syntax consists of Razor markup, C#, and HTML. Files containing Razor generally have a `.cshtml` file extension. Razor is also found in [Razor component](../../blazor/components/?view=aspnetcore-10.0) files (`.razor`). Razor syntax is similar to the templating engines of various JavaScript single-page application (SPA) frameworks, such as Angular, React, VueJs, and Svelte. For more information, see [The features described in this article are obsolete as of ASP.NET Core 3.0](../../client-side/spa-services?view=aspnetcore-10.0).

[Introduction to ASP.NET Web Programming Using the Razor Syntax](/en-us/aspnet/web-pages/overview/getting-started/introducing-razor-syntax-c) provides many samples of programming with Razor syntax. Although the article was written for ASP.NET rather than ASP.NET Core, most of the samples apply to ASP.NET Core.

## Rendering HTML

The default Razor language is HTML. Rendering HTML from Razor markup is no different than rendering HTML from an HTML file. HTML markup in `.cshtml` Razor files is rendered by the server unchanged.

## Razor syntax

Razor supports C# and uses the `@` symbol to transition from HTML to C#. Razor evaluates C# expressions and renders them in the HTML output.

When an `@` symbol is followed by a [Razor reserved keyword](#razor-reserved-keywords), it transitions into Razor-specific markup. Otherwise, it transitions into plain HTML.

To escape an `@` symbol in Razor markup, use a second `@` symbol:

```
<p>@@Username</p>
```

The code is rendered in HTML with a single `@` symbol:

```
<p>@Username</p>
```

HTML attributes and content containing email addresses don't treat the `@` symbol as a transition character. The email addresses in the following example are untouched by Razor parsing:

```
<a href="mailto:Support@contoso.com">Support@contoso.com</a>
```

### Scalable Vector Graphics (SVG)

[SVG](https://developer.mozilla.org/docs/Web/SVG) [foreignObject](https://developer.mozilla.org/docs/Web/SVG/Reference/Element/foreignObject) elements are supported:

```
@{
    string message = "foreignObject example with Scalable Vector Graphics (SVG)";
}

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
    <rect x="0" y="0" rx="10" ry="10" width="200" height="200" stroke="black" 
        fill="none" />
    <foreignObject x="20" y="20" width="160" height="160">
        <p>@message</p>
    </foreignObject>
</svg>
```

## Implicit Razor expressions

Implicit Razor expressions start with `@` followed by C# code:

```
<p>@DateTime.Now</p>
<p>@DateTime.IsLeapYear(2016)</p>
```

Except for the C# `await` keyword, implicit expressions must not contain spaces. If the C# statement has a clear ending, spaces can be intermingled:

```
<p>@await DoSomething("hello", "world")</p>
```

Implicit expressions **cannot** contain C# generics, as the characters inside the brackets (`<>`) are interpreted as an HTML tag. The following code is **not** valid:

```
<p>@GenericMethod<int>()</p>
```

The preceding code generates a compiler error similar to one of the following:

* The "int" element wasn't closed. All elements must be either self-closing or have a matching end tag.
* Cannot convert method group 'GenericMethod' to non-delegate type 'object'. Did you intend to invoke the method?\`

Generic method calls must be wrapped in an [explicit Razor expression](#explicit-razor-expressions) or a [Razor code block](#razor-code-blocks).

## Explicit Razor expressions

Explicit Razor expressions consist of an `@` symbol with balanced parenthesis. To render last week's time, the following Razor markup is used:

```
<p>Last week this time: @(DateTime.Now - TimeSpan.FromDays(7))</p>
```

Any content within the `@()` parenthesis is evaluated and rendered to the output.

Implicit expressions, described in the previous section, generally can't contain spaces. In the following code, one week isn't subtracted from the current time:

```
<p>Last week: @DateTime.Now - TimeSpan.FromDays(7)</p>
```

The code renders the following HTML:

```
<p>Last week: 7/7/2016 4:39:52 PM - TimeSpan.FromDays(7)</p>
```

Explicit expressions can be used to concatenate text with an expression result:

```
@{
    var joe = new Person("Joe", 33);
}

<p>Age@(joe.Age)</p>
```

Without the explicit expression, `<p>Age@joe.Age</p>` is treated as an email address, and `<p>Age@joe.Age</p>` is rendered. When written as an explicit expression, `<p>Age33</p>` is rendered.

Explicit expressions can be used to render output from generic methods in `.cshtml` files. The following markup shows how to correct the error shown earlier caused by the brackets of a C# generic. The code is written as an explicit expression:

```
<p>@(GenericMethod<int>())</p>
```

## Expression encoding

C# expressions that evaluate to a string are HTML encoded. C# expressions that evaluate to `IHtmlContent` are rendered directly through `IHtmlContent.WriteTo`. C# expressions that don't evaluate to `IHtmlContent` are converted to a string by `ToString` and encoded before they're rendered.

```
@("<span>Hello World</span>")
```

The preceding code renders the following HTML:

```
&lt;span&gt;Hello World&lt;/span&gt;
```

The HTML is shown in the browser as plain text:

\<span>Hello World\</span>

`HtmlHelper.Raw` output isn't encoded but rendered as HTML markup.

Warning

Using `HtmlHelper.Raw` on unsanitized user input is a security risk. User input might contain malicious JavaScript or other exploits. Sanitizing user input is difficult. Avoid using `HtmlHelper.Raw` with user input.

```
@Html.Raw("<span>Hello World</span>")
```

The code renders the following HTML:

```
<span>Hello World</span>
```

## Razor code blocks

Razor code blocks start with `@` and are enclosed by `{}`. Unlike expressions, C# code inside code blocks isn't rendered. Code blocks and expressions in a view share the same scope and are defined in order:

```
@{
    var quote = "The future depends on what you do today. - Mahatma Gandhi";
}

<p>@quote</p>

@{
    quote = "Hate cannot drive out hate, only love can do that. - Martin Luther King, Jr.";
}

<p>@quote</p>
```

The code renders the following HTML:

```
<p>The future depends on what you do today. - Mahatma Gandhi</p>
<p>Hate cannot drive out hate, only love can do that. - Martin Luther King, Jr.</p>
```

In code blocks, declare [local functions](/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions) with markup to serve as templating methods:

```
@{
    void RenderName(string name)
    {
        <p>Name: <strong>@name</strong></p>
    }

    RenderName("Mahatma Gandhi");
    RenderName("Martin Luther King, Jr.");
}
```

The code renders the following HTML:

```
<p>Name: <strong>Mahatma Gandhi</strong></p>
<p>Name: <strong>Martin Luther King, Jr.</strong></p>
```

### Implicit transitions

The default language in a code block is C#, but the Razor Page can transition back to HTML:

```
@{
    var inCSharp = true;
    <p>Now in HTML, was in C# @inCSharp</p>
}
```

### Explicit delimited transition

To define a subsection of a code block that should render HTML, surround the characters for rendering with the Razor `<text>` tag:

```
@for (var i = 0; i < people.Length; i++)
{
    var person = people[i];
    <text>Name: @person.Name</text>
}
```

Use this approach to render HTML that isn't surrounded by an HTML tag. Without an HTML or Razor tag, a Razor runtime error occurs.

The `<text>` tag is useful to control whitespace when rendering content:

* Only the content between the `<text>` tag is rendered.
* No whitespace before or after the `<text>` tag appears in the HTML output.

### Explicit line transition

To render the rest of an entire line as HTML inside a code block, use `@:` syntax:

```
@for (var i = 0; i < people.Length; i++)
{
    var person = people[i];
    @:Name: @person.Name
}
```

Without the `@:` in the code, a Razor runtime error is generated.

Extra `@` characters in a Razor file can cause compiler errors at statements later in the block. These extra `@` compiler errors:

* Can be difficult to understand because the actual error occurs before the reported error.
* Is common after combining multiple implicit and explicit expressions into a single code block.

### Conditional attribute rendering

Razor automatically omits attributes that aren't required. If the value passed in is `null` or `false`, the attribute isn't rendered.

For example, consider the following Razor markup:

```
<div class="@false">False</div>
<div class="@null">Null</div>
<div class="@("")">Empty</div>
<div class="@("false")">False String</div>
<div class="@("active")">String</div>
<input type="checkbox" checked="@true" name="true" />
<input type="checkbox" checked="@false" name="false" />
<input type="checkbox" checked="@null" name="null" />
```

The preceding Razor markup generates the following HTML:

```
<div>False</div>
<div>Null</div>
<div class="">Empty</div>
<div class="false">False String</div>
<div class="active">String</div>
<input type="checkbox" checked="checked" name="true">
<input type="checkbox" name="false">
<input type="checkbox" name="null">
```

Razor retains `data-` attributes if their values are `null` or `false`.

Consider the following Razor markup:

```
<div data-id="@null" data-active="@false"></div>
```

The preceding Razor markup generates the following HTML:

```
<div data-id="" data-active="False"></div>
```

## Control structures

Control structures are an extension of code blocks. All aspects of code blocks (transitioning to markup, inline C#) also apply to the following structures:

### Conditionals `@if, else if, else, and @switch`

`@if` controls when code runs:

```
@if (value % 2 == 0)
{
    <p>The value was even.</p>
}
```

`else` and `else if` don't require the `@` symbol:

```
@if (value % 2 == 0)
{
    <p>The value was even.</p>
}
else if (value >= 1337)
{
    <p>The value is large.</p>
}
else
{
    <p>The value is odd and small.</p>
}
```

The following markup shows how to use a switch statement:

```
@switch (value)
{
    case 1:
        <p>The value is 1!</p>
        break;
    case 1337:
        <p>Your number is 1337!</p>
        break;
    default:
        <p>Your number wasn't 1 or 1337.</p>
        break;
}
```

### Looping `@for, @foreach, @while, and @do while`

Templated HTML can be rendered with looping control statements. To render a list of people:

```
@{
    var people = new Person[]
    {
          new Person("Weston", 33),
          new Person("Johnathon", 41),
          ...
    };
}
```

The following looping statements are supported:

`@for`

```
@for (var i = 0; i < people.Length; i++)
{
    var person = people[i];
    <p>Name: @person.Name</p>
    <p>Age: @person.Age</p>
}
```

`@foreach`

```
@foreach (var person in people)
{
    <p>Name: @person.Name</p>
    <p>Age: @person.Age</p>
}
```

`@while`

```
@{ var i = 0; }
@while (i < people.Length)
{
    var person = people[i];
    <p>Name: @person.Name</p>
    <p>Age: @person.Age</p>

    i++;
}
```

`@do while`

```
@{ var i = 0; }
@do
{
    var person = people[i];
    <p>Name: @person.Name</p>
    <p>Age: @person.Age</p>

    i++;
} while (i < people.Length);
```

### Compound `@using`

In C#, a `using` statement is used to ensure an object is disposed. In Razor, the same mechanism is used to create HTML Helpers that contain additional content. In the following code, HTML Helpers render a `<form>` tag with the `@using` statement:

```
@using (Html.BeginForm())
{
    <div>
        <label>Email: <input type="email" id="Email" value=""></label>
        <button>Register</button>
    </div>
}
```

### `@try, catch, finally`

Exception handling is similar to C#:

```
@try
{
    throw new InvalidOperationException("You did something invalid.");
}
catch (Exception ex)
{
    <p>The exception message: @ex.Message</p>
}
finally
{
    <p>The finally statement.</p>
}
```

### `@lock`

Razor has the capability to protect critical sections with lock statements:

```
@lock (SomeLock)
{
    // Do critical section work
}
```

### Comments

Razor supports C# and HTML comments:

```
@{
    /* C# comment */
    // Another C# comment
}
<!-- HTML comment -->
```

The code renders the following HTML:

```
<!-- HTML comment -->
```

Razor comments are removed by the server before the webpage is rendered. Razor uses `@* *@` to delimit comments. The following code is commented out, so the server doesn't render any markup:

```
@*
    @{
        /* C# comment */
        // Another C# comment
    }
    <!-- HTML comment -->
*@
```

## Directives

Razor directives are represented by implicit expressions with reserved keywords following the `@` symbol. A directive typically changes the way a view is compiled or functions.

Understanding how Razor generates code for a view makes it easier to understand how directives work.

```
@{
    var quote = "Getting old ain't for wimps! - Anonymous";
}

<div>Quote of the Day: @quote</div>
```

The code generates a class similar to the following:

```
public class _Views_Something_cshtml : RazorPage<dynamic>
{
    public override async Task ExecuteAsync()
    {
        var output = "Getting old ain't for wimps! - Anonymous";

        WriteLiteral("/r/n<div>Quote of the Day: ");
        Write(output);
        WriteLiteral("</div>");
    }
}
```

Later in this article, the section [Inspect the Razor C# class generated for a view](#inspect-the-razor-c-class-generated-for-a-view) explains how to view this generated class.

### `@attribute`

The `@attribute` directive adds the given attribute to the class of the generated page or view. The following example adds the `[Authorize]` attribute:

```
@attribute [Authorize]
```

The `@attribute` directive can also be used to supply a constant-based route template in a Razor component. In the following example, the `@page` directive in a component is replaced with the `@attribute` directive and the constant-based route template in `Constants.CounterRoute`, which is set elsewhere in the app to "`/counter`":

```
- @page "/counter"
+ @attribute [Route(Constants.CounterRoute)]
```

### `@code`

*This scenario only applies to Razor components (`.razor`).*

The `@code` block enables a [Razor component](../../blazor/components/?view=aspnetcore-10.0) to add C# members (fields, properties, and methods) to a component:

```
@code {
    // C# members (fields, properties, and methods)
}
```

For Razor components, `@code` is an alias of [`@functions`](#functions) and recommended over `@functions`. More than one `@code` block is permissible.

### `@functions`

The `@functions` directive enables adding C# members (fields, properties, and methods) to the generated class:

```
@functions {
    // C# members (fields, properties, and methods)
}
```

In [Razor components](../../blazor/components/?view=aspnetcore-10.0), use `@code` over `@functions` to add C# members.

For example:

```
@functions {
    public string GetHello()
    {
        return "Hello";
    }
}

<div>From method: @GetHello()</div> 
```

The code generates the following HTML markup:

```
<div>From method: Hello</div>
```

The following code is the generated Razor C# class:

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor;

public class _Views_Home_Test_cshtml : RazorPage<dynamic>
{
    // Functions placed between here 
    public string GetHello()
    {
        return "Hello";
    }
    // And here.
#pragma warning disable 1998
    public override async Task ExecuteAsync()
    {
        WriteLiteral("\r\n<div>From method: ");
        Write(GetHello());
        WriteLiteral("</div>\r\n");
    }
#pragma warning restore 1998
```

`@functions` methods serve as templating methods when they have markup:

```
@{
    RenderName("Mahatma Gandhi");
    RenderName("Martin Luther King, Jr.");
}

@functions {
    private void RenderName(string name)
    {
        <p>Name: <strong>@name</strong></p>
    }
}
```

The code renders the following HTML:

```
<p>Name: <strong>Mahatma Gandhi</strong></p>
<p>Name: <strong>Martin Luther King, Jr.</strong></p>
```

### `@implements`

The `@implements` directive implements an interface for the generated class.

The following example implements [System.IDisposable](/en-us/dotnet/api/system.idisposable) so that the [Dispose](/en-us/dotnet/api/system.idisposable.dispose) method can be called:

```
@implements IDisposable

<h1>Example</h1>

@functions {
    private bool _isDisposed;

    ...

    public void Dispose() => _isDisposed = true;
}
```

### `@inherits`

The `@inherits` directive provides full control of the class the view inherits:

```
@inherits TypeNameOfClassToInheritFrom
```

The following code is a custom Razor page type:

```
using Microsoft.AspNetCore.Mvc.Razor;

public abstract class CustomRazorPage<TModel> : RazorPage<TModel>
{
    public string CustomText { get; } = 
        "Gardyloo! - A Scottish warning yelled from a window before dumping" +
        "a slop bucket on the street below.";
}
```

The `CustomText` is displayed in a view:

```
@inherits CustomRazorPage<TModel>

<div>Custom text: @CustomText</div>
```

The code renders the following HTML:

```
<div>
    Custom text: Gardyloo! - A Scottish warning yelled from a window before dumping
    a slop bucket on the street below.
</div>
```

`@model` and `@inherits` can be used in the same view. `@inherits` can be in a `_ViewImports.cshtml` file that the view imports:

```
@inherits CustomRazorPage<TModel>
```

The following code is an example of a strongly-typed view:

```
@inherits CustomRazorPage<TModel>

<div>The Login Email: @Model.Email</div>
<div>Custom text: @CustomText</div>
```

If "rick\@contoso.com" is passed in the model, the view generates the following HTML markup:

```
<div>The Login Email: rick@contoso.com</div>
<div>
    Custom text: Gardyloo! - A Scottish warning yelled from a window before dumping
    a slop bucket on the street below.
</div>
```

### `@inject`

The `@inject` directive enables the Razor Page to inject a service from the [service container](../../fundamentals/dependency-injection?view=aspnetcore-10.0) into a view. For more information, see [Dependency injection into views](dependency-injection?view=aspnetcore-10.0).

### `@layout`

*This scenario only applies to Razor components (`.razor`).*

The `@layout` directive specifies a layout for routable Razor components that have an [`@page`](#page) directive. Layout components are used to avoid code duplication and inconsistency. For more information, see [ASP.NET Core Blazor layouts](../../blazor/components/layouts?view=aspnetcore-10.0).

### `@model`

*This scenario only applies to MVC views and Razor Pages (`.cshtml`).*

The `@model` directive specifies the type of the model passed to a view or page:

```
@model TypeNameOfModel
```

In an ASP.NET Core MVC or Razor Pages app created with individual accounts, `Views/Account/Login.cshtml` contains the following model declaration:

```
@model LoginViewModel
```

The class generated inherits from `RazorPage<LoginViewModel>`:

```
public class _Views_Account_Login_cshtml : RazorPage<LoginViewModel>
```

Razor exposes a `Model` property for accessing the model passed to the view:

```
<div>The Login Email: @Model.Email</div>
```

The `@model` directive specifies the type of the `Model` property. The directive specifies the `T` in `RazorPage<T>` that the generated class that the view derives from. If the `@model` directive isn't specified, the `Model` property is of type `dynamic`. For more information, see [Strongly typed models and the @model keyword](../../tutorials/first-mvc-app/adding-model?view=aspnetcore-10.0#strongly-typed-models-and-the--keyword).

### `@namespace`

The `@namespace` directive:

* Sets the namespace of the class of the generated Razor page, MVC view, or Razor component.
* Sets the root derived namespaces of a pages, views, or components classes from the closest imports file in the directory tree, `_ViewImports.cshtml` (views or pages) or `_Imports.razor` (Razor components).

```
@namespace Your.Namespace.Here
```

For the Razor Pages example shown in the following table.

* Each page imports `Pages/_ViewImports.cshtml`.
* `Pages/_ViewImports.cshtml` contains `@namespace Hello.World`.
* Each page has `Hello.World` as the root of it's namespace.

| Page                                        | Namespace                             |
| ------------------------------------------- | ------------------------------------- |
| `Pages/Index.cshtml`                        | `Hello.World`                         |
| `Pages/MorePages/Page.cshtml`               | `Hello.World.MorePages`               |
| `Pages/MorePages/EvenMorePages/Page.cshtml` | `Hello.World.MorePages.EvenMorePages` |

The preceding relationships apply to import files used with MVC views and Razor components.

When multiple import files have a `@namespace` directive, the file closest to the page, view, or component in the directory tree is used to set the root namespace.

If the `EvenMorePages` folder in the preceding example has an imports file with `@namespace Another.Planet` (or the `Pages/MorePages/EvenMorePages/Page.cshtml` file contains `@namespace Another.Planet`), the result is shown in the following table.

| Page                                        | Namespace               |
| ------------------------------------------- | ----------------------- |
| `Pages/Index.cshtml`                        | `Hello.World`           |
| `Pages/MorePages/Page.cshtml`               | `Hello.World.MorePages` |
| `Pages/MorePages/EvenMorePages/Page.cshtml` | `Another.Planet`        |

### `@page`

The `@page` directive has different effects depending on the type of the file where it appears. The directive:

* In a `.cshtml` file indicates that the file is a Razor Page. For more information, see [Custom routes](../../razor-pages/?view=aspnetcore-10.0#custom-routes) and [Razor Pages architecture and concepts in ASP.NET Core](../../razor-pages/?view=aspnetcore-10.0).
* Specifies that a Razor component should handle requests directly. For more information, see [ASP.NET Core Blazor routing](../../blazor/fundamentals/routing?view=aspnetcore-10.0).

### `@preservewhitespace`

*This scenario only applies to Razor components (`.razor`).*

When set to `false` (default), whitespace in the rendered markup from Razor components (`.razor`) is removed if:

* Leading or trailing within an element.
* Leading or trailing within a `RenderFragment` parameter. For example, child content passed to another component.
* It precedes or follows a C# code block, such as `@if` or `@foreach`.

### `@rendermode`

*This scenario only applies to Razor components (`.razor`).*

Sets the render mode of a Razor component:

* `InteractiveServer`: Applies interactive server rendering using Blazor Server.
* `InteractiveWebAssembly`: Applies interactive WebAssembly rendering using Blazor WebAssembly.
* `InteractiveAuto`: Initially applies interactive WebAssembly rendering using Blazor Server, and then applies interactive WebAssembly rendering using WebAssembly on subsequent visits after the Blazor bundle is downloaded.

For a component instance:

```
<... @rendermode="InteractiveServer" />
```

In the component definition:

```
@rendermode InteractiveServer
```

Note

Blazor templates include a static `using` directive for [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) in the app's imports file (`Components/_Imports.razor`) for shorter `@rendermode` syntax:

```
@using static Microsoft.AspNetCore.Components.Web.RenderMode
```

Without the preceding directive, components must specify the static [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.components.web.rendermode) class in `@rendermode` syntax explicitly:

```
<Dialog @rendermode="RenderMode.InteractiveServer" />
```

For more information, including guidance on disabling prerendering with the directive/directive attribute, see [ASP.NET Core Blazor render modes](../../blazor/components/render-modes?view=aspnetcore-10.0).

### `@section`

*This scenario only applies to MVC views and Razor Pages (`.cshtml`).*

The `@section` directive is used in conjunction with [MVC and Razor Pages layouts](layout?view=aspnetcore-10.0) to enable views or pages to render content in different parts of the HTML page. For more information, see [Layout in ASP.NET Core](layout?view=aspnetcore-10.0).

### `@typeparam`

*This scenario only applies to Razor components (`.razor`).*

The `@typeparam` directive declares a [generic type parameter](/en-us/dotnet/csharp/programming-guide/generics/generic-type-parameters) for the generated component class:

```
@typeparam TEntity
```

Generic types with [`where`](/en-us/dotnet/csharp/language-reference/keywords/where-generic-type-constraint) type constraints are supported:

```
@typeparam TEntity where TEntity : IEntity
```

For more information, see the following articles:

* [ASP.NET Core Razor component generic type support](../../blazor/components/generic-type-support?view=aspnetcore-10.0)
* [ASP.NET Core Blazor templated components](../../blazor/components/templated-components?view=aspnetcore-10.0)

### `@using`

The `@using` directive adds the C# `using` directive to the generated view:

```
@using System.IO
@{
    var dir = Directory.GetCurrentDirectory();
}
<p>@dir</p>
```

In [Razor components](../../blazor/components/?view=aspnetcore-10.0), `@using` also controls which components are in scope.

## Directive attributes

Razor directive attributes are represented by implicit expressions with reserved keywords following the `@` symbol. A directive attribute typically changes the way an element is compiled or functions.

### `@attributes`

*This scenario only applies to Razor components (`.razor`).*

`@attributes` allows a component to render non-declared attributes. For more information, see [ASP.NET Core Blazor attribute splatting and arbitrary parameters](../../blazor/components/splat-attributes-and-arbitrary-parameters?view=aspnetcore-10.0).

### `@bind`

*This scenario only applies to Razor components (`.razor`).*

Data binding in components is accomplished with the `@bind` attribute. For more information, see [ASP.NET Core Blazor data binding](../../blazor/components/data-binding?view=aspnetcore-10.0).

### `@bind:culture`

*This scenario only applies to Razor components (`.razor`).*

Use the `@bind:culture` attribute with the [`@bind`](#bind) attribute to provide a [System.Globalization.CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo) for parsing and formatting a value. For more information, see [ASP.NET Core Blazor globalization and localization](../../blazor/globalization-localization?view=aspnetcore-10.0#globalization).

### `@formname`

*This scenario only applies to Razor components (`.razor`).*

`@formname` assigns a form name to a Razor component's plain HTML form or a form based on [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) ([`Editform` documentation](../../blazor/forms/binding?view=aspnetcore-10.0#editformeditcontext-model)). The value of `@formname` should be unique, which prevents form collisions in the following situations:

* A form is placed in a component with multiple forms.
* A form is sourced from an external class library, commonly a NuGet package, for a component with multiple forms, and the app author doesn't control the source code of the library to set a different external form name than a name used by another form in the component.

For more information and examples, see [ASP.NET Core Blazor forms overview](../../blazor/forms/?view=aspnetcore-10.0).

### `@on{EVENT}`

*This scenario only applies to Razor components (`.razor`).*

Razor provides event handling features for components. For more information, see [ASP.NET Core Blazor event handling](../../blazor/components/event-handling?view=aspnetcore-10.0).

### `@on{EVENT}:preventDefault`

*This scenario only applies to Razor components (`.razor`).*

Prevents the default action for the event.

### `@on{EVENT}:stopPropagation`

*This scenario only applies to Razor components (`.razor`).*

Stops event propagation for the event.

### `@key`

*This scenario only applies to Razor components (`.razor`).*

The `@key` directive attribute causes the components diffing algorithm to guarantee preservation of elements or components based on the key's value. For more information, see [Retain element, component, and model relationships in ASP.NET Core Blazor](../../blazor/components/element-component-model-relationships?view=aspnetcore-10.0).

### `@ref`

*This scenario only applies to Razor components (`.razor`).*

Component references (`@ref`) provide a way to reference a component instance so that you can issue commands to that instance. For more information, see [ASP.NET Core Razor components](../../blazor/components/?view=aspnetcore-10.0#capture-references-to-components).

## Templated Razor delegates

*This scenario only applies to MVC views and Razor Pages (`.cshtml`).*

Razor templates allow you to define a UI snippet with the following format:

```
@<tag>...</tag>
```

The following example illustrates how to specify a templated Razor delegate as a [Func\<T,TResult>](/en-us/dotnet/api/system.func-2). The [dynamic type](/en-us/dotnet/csharp/programming-guide/types/using-type-dynamic) is specified for the parameter of the method that the delegate encapsulates. An [object type](/en-us/dotnet/csharp/language-reference/builtin-types/reference-types) is specified as the return value of the delegate. The template is used with a [List\<T>](/en-us/dotnet/api/system.collections.generic.list-1) of `Pet` that has a `Name` property.

```
public class Pet
{
    public string Name { get; set; }
}
```

```
@{
    Func<dynamic, object> petTemplate = @<p>You have a pet named <strong>@item.Name</strong>.</p>;

    var pets = new List<Pet>
    {
        new Pet { Name = "Rin Tin Tin" },
        new Pet { Name = "Mr. Bigglesworth" },
        new Pet { Name = "K-9" }
    };
}
```

The template is rendered with `pets` supplied by a `foreach` statement:

```
@foreach (var pet in pets)
{
    @petTemplate(pet)
}
```

Rendered output:

```
<p>You have a pet named <strong>Rin Tin Tin</strong>.</p>
<p>You have a pet named <strong>Mr. Bigglesworth</strong>.</p>
<p>You have a pet named <strong>K-9</strong>.</p>
```

You can also supply an inline Razor template as an argument to a method. In the following example, the `Repeat` method receives a Razor template. The method uses the template to produce HTML content with repeats of items supplied from a list:

```
@using Microsoft.AspNetCore.Html

@functions {
    public static IHtmlContent Repeat(IEnumerable<dynamic> items, int times,
        Func<dynamic, IHtmlContent> template)
    {
        var html = new HtmlContentBuilder();

        foreach (var item in items)
        {
            for (var i = 0; i < times; i++)
            {
                html.AppendHtml(template(item));
            }
        }

        return html;
    }
}
```

Using the list of pets from the prior example, the `Repeat` method is called with:

* [List\<T>](/en-us/dotnet/api/system.collections.generic.list-1) of `Pet`.
* Number of times to repeat each pet.
* Inline template to use for the list items of an unordered list.

```
<ul>
    @Repeat(pets, 3, @<li>@item.Name</li>)
</ul>
```

Rendered output:

```
<ul>
    <li>Rin Tin Tin</li>
    <li>Rin Tin Tin</li>
    <li>Rin Tin Tin</li>
    <li>Mr. Bigglesworth</li>
    <li>Mr. Bigglesworth</li>
    <li>Mr. Bigglesworth</li>
    <li>K-9</li>
    <li>K-9</li>
    <li>K-9</li>
</ul>
```

## Tag Helpers

*This scenario only applies to MVC views and Razor Pages (`.cshtml`).*

There are three directives that pertain to [Tag Helpers](tag-helpers/intro?view=aspnetcore-10.0).

| Directive                                                                                  | Function                                                                                   |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| [`@addTagHelper`](tag-helpers/intro?view=aspnetcore-10.0#add-helper-label)                 | Makes Tag Helpers available to a view.                                                     |
| [`@removeTagHelper`](tag-helpers/intro?view=aspnetcore-10.0#remove-razor-directives-label) | Removes Tag Helpers previously added from a view.                                          |
| [`@tagHelperPrefix`](tag-helpers/intro?view=aspnetcore-10.0#prefix-razor-directives-label) | Specifies a tag prefix to enable Tag Helper support and to make Tag Helper usage explicit. |

## Razor reserved keywords

### Razor keywords

* `page`
* `namespace`
* `functions`
* `inherits`
* `model`
* `section`
* `helper` (Not currently supported by ASP.NET Core)

Razor keywords are escaped with `@(Razor Keyword)` (for example, `@(functions)`).

### C# Razor keywords

* `case`
* `do`
* `default`
* `for`
* `foreach`
* `if`
* `else`
* `lock`
* `switch`
* `try`
* `catch`
* `finally`
* `using`
* `while`

C# Razor keywords must be double-escaped with `@(@C# Razor Keyword)` (for example, `@(@case)`). The first `@` escapes the Razor parser. The second `@` escapes the C# parser.

### Reserved keywords not used by Razor

* `class`

## Inspect the Razor C# class generated for a view

The [Razor SDK](../../razor-pages/sdk?view=aspnetcore-10.0) handles compilation of Razor files. By default, the generated code files aren't emitted. To enable emitting the code files, set the `EmitCompilerGeneratedFiles` directive in the project file (`.csproj`) to `true`:

```
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
```

When building a 6.0 project (`net6.0`) in the `Debug` build configuration, the Razor SDK generates an `obj/Debug/net6.0/generated/` directory in the project root. Its subdirectory contains the emitted Razor page code files.

The [Razor SDK](../../razor-pages/sdk?view=aspnetcore-10.0) handles compilation of Razor files. When building a project, the Razor SDK generates an `obj/{BUILD CONFIGURATION}/{TARGET FRAMEWORK MONIKER}/Razor` directory in the project root. The directory structure within the `Razor` directory mirrors the project's directory structure.

Consider the following directory structure in an ASP.NET Core Razor Pages 2.1 project:

```
 Areas/
   Admin/
     Pages/
       Index.cshtml
       Index.cshtml.cs
 Pages/
   Shared/
     _Layout.cshtml
   _ViewImports.cshtml
   _ViewStart.cshtml
   Index.cshtml
   Index.cshtml.cs
```

Building the project in `Debug` configuration yields the following `obj` directory:

```
 obj/
   Debug/
     netcoreapp2.1/
       Razor/
         Areas/
           Admin/
             Pages/
               Index.g.cshtml.cs
         Pages/
           Shared/
             _Layout.g.cshtml.cs
           _ViewImports.g.cshtml.cs
           _ViewStart.g.cshtml.cs
           Index.g.cshtml.cs
```

To view the generated class for `Pages/Index.cshtml`, open `obj/Debug/netcoreapp2.1/Razor/Pages/Index.g.cshtml.cs`.

## View lookups and case sensitivity

The Razor view engine performs case-sensitive lookups for views. However, the underlying file system determines the actual lookup:

* File based source:

  * On operating systems with case insensitive file systems (for example, Windows), physical file provider lookups are case insensitive. For example, `return View("Test")` results in matches for `/Views/Home/Test.cshtml`, `/Views/home/test.cshtml`, and any other casing variant.
  * On case-sensitive file systems (for example, Linux, OSX, and with `EmbeddedFileProvider`), lookups are case-sensitive. For example, `return View("Test")` specifically matches `/Views/Home/Test.cshtml`.

* Precompiled views: With ASP.NET Core 2.0 or later, looking up precompiled views is case insensitive on all operating systems. The behavior is identical to physical file provider's behavior on Windows. If two precompiled views differ only in case, the result of lookup is non-deterministic.

Developers are encouraged to match the casing of file and directory names to the casing of:

* Area, controller, and action names.
* Razor Pages.

Matching case ensures the deployments find their views regardless of the underlying file system.

## Imports used by Razor

The following imports are generated by the ASP.NET Core web templates to support Razor Files:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
```

## Related content

[Introduction to ASP.NET Web Programming Using the Razor Syntax](/en-us/aspnet/web-pages/overview/getting-started/introducing-razor-syntax-c) provides many samples of programming with Razor syntax.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/policies.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Policy-based authorization in ASP.NET Core

An ASP.NET Core authorization policy is a set of one or more authorization requirements that the framework evaluates to decide whether a user is allowed to access a resource. A policy can be registered with a name and applied to resources that require it.

This article explains:

* How to require authenticated users by default.
* How to create requirements.
* How to register and apply policies.
* How named, default, and fallback policies are selected.
* Authorization handlers for single and multiple requirement evaluation.
* How multiple requirements in a single policy are evaluated.

A named policy is applied with `[Authorize(Policy = "...")]` (Razor components, pages, and controllers) or `RequireAuthorization(...)` (endpoints), and the framework uses handlers to evaluate the requirements behind a policy. [IAuthorizationPolicyProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider) ([Custom authorization policy providers in ASP.NET Core](custom-authorization-policy-providers?view=aspnetcore-10.0) documentation) generates policies dynamically instead of registering them at app startup.

[Role-based authorization](roles?view=aspnetcore-10.0) and [claims-based authorization](claims?view=aspnetcore-10.0) use a requirement, a requirement handler, and a preconfigured authorization policy. These building blocks support the expression of authorization evaluations in code.

This article uses Razor component examples and focuses on [Blazor](../../blazor/?view=aspnetcore-10.0) authorization scenarios for ASP.NET Core 3.1 or later. For Razor Pages and MVC guidance that applies to all releases of ASP.NET Core, see the following resources after reading this article:

* [Policy-based authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/policies?view=aspnetcore-10.0)
* [Policy-based authorization in ASP.NET Core MVC](../../mvc/security/authorization/policies?view=aspnetcore-10.0)

Some examples in this article (ASP.NET Core 8.0 or later) use *primary constructors*, available in C# 12 (.NET 8) or later. For more information, see [Declare primary constructors for classes and structs (C# documentation tutorial)](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) and [Primary constructors (C# Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors).

## Require global user authentication

For a server-side app where most or all endpoints require authentication, set a fallback policy that requires an authenticated user. This secure-by-default approach protects newly added endpoints that don't specify authorization metadata.

The fallback policy, not the default policy, applies to endpoints that don't specify authorization metadata. The default policy applies when an endpoint selects it with `[Authorize]` or `RequireAuthorization()` without a policy name. For complete policy selection rules, see the [Default and fallback policies](#default-and-fallback-policies) section.

```
var requireAuthPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();

builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(requireAuthPolicy);
```

```
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});
```

In `Startup.ConfigureServices`:

```
services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});
```

Apply `[AllowAnonymous]` or call `AllowAnonymous()` for endpoints that are intentionally public.

The fallback policy applies to requests processed by the authorization middleware. For example:

* A request that doesn't match an endpoint uses the fallback policy if the authorization middleware runs for the request.
* Static files served by static file middleware before the authorization middleware aren't protected by the fallback policy.
* Public endpoints can depend on static assets that must also allow anonymous access.

For more information, see [Static files in ASP.NET Core](../../fundamentals/static-files?view=aspnetcore-10.0#static-file-authorization) and [Server-side Blazor app authorization patterns](../../blazor/security/additional-scenarios?view=aspnetcore-10.0#server-side-blazor-app-authorization-patterns). Blazor WebAssembly apps don't support a server-side fallback authorization policy. For Blazor WebAssembly authorization patterns, see [Secure ASP.NET Core Blazor WebAssembly](../../blazor/security/webassembly/?view=aspnetcore-10.0#blazor-webassembly-authorization-patterns).

## Default and fallback policies

The authorization middleware combines the authorization metadata for an endpoint into a policy. The following table describes how the metadata determines which policy is used:

| Authorization metadata                                                                  | Policy or behavior                                                                                                                                                                                                                                                                                     |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| None                                                                                    | The [AuthorizationOptions.FallbackPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.fallbackpolicy) is used, if it's configured. By default, the fallback policy is `null`, so authorization isn't required.                                                           |
| `[Authorize]` or `RequireAuthorization()` without a policy name                         | The [AuthorizationOptions.DefaultPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.defaultpolicy) is used unless the endpoint also has an explicit `AuthorizationPolicy` instance. By default, the default policy requires an authenticated user.                      |
| `[Authorize(Policy = "{POLICY NAME}")]` or `RequireAuthorization("{POLICY NAME}")`      | The named policy is used.                                                                                                                                                                                                                                                                              |
| `[Authorize(Roles = "{ROLES}")]`                                                        | A policy is built with the specified roles. The default policy isn't added for this authorization declaration.                                                                                                                                                                                         |
| `RequireAuthorization(policy)` with an `AuthorizationPolicy` instance                   | The explicit policy is used. If explicit policy metadata is present, bare authorization data and authentication-scheme-only authorization data don't add the default policy.                                                                                                                           |
| `[Authorize(AuthenticationSchemes = "{SCHEME}")]` without a policy name or roles        | The specified authentication scheme is used. The default policy is also used unless the endpoint has an explicit `AuthorizationPolicy` instance.                                                                                                                                                       |
| Multiple `[Authorize]` attributes or policy-selecting `RequireAuthorization(...)` calls | The selected named policies, roles, authentication schemes, and explicit policies are combined. Bare authorization data adds the default policy only when no explicit `AuthorizationPolicy` instance is present. All requirements in the combined policy must succeed. The fallback policy isn't used. |
| `[AllowAnonymous]` or `AllowAnonymous()`                                                | The authorization middleware doesn't enforce an authorization failure for the endpoint. Authentication can still run and populate [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user).                                         |

The fallback policy isn't combined with a named or default policy. For the declarations shown in the preceding table, it's selected only when no authorization policy is produced from the endpoint's authorization metadata. For example, `[Authorize]` uses the default policy instead of the fallback policy, and `[Authorize(Policy = "{POLICY NAME}")]` uses the named policy instead of the fallback policy.

Requirements supplied as endpoint metadata through [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) are added after policy selection. If no other authorization metadata produces a policy, these requirements are combined with the fallback policy when a fallback policy is configured.

In ASP.NET Core 8.0 through 10.0, attributes that implement [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) are only enforced on Minimal API and routed endpoints. For version and hosting model support, see [Custom authorization policies with \`IAuthorizationRequirementData\`](custom-authorization-policies-with-iauthorizationrequirementdata?view=aspnetcore-10.0).

## Requirements and policy registration

An authorization policy consists of one or more *requirements*, which are used by a policy to evaluate authorization for the current user principal. A requirement implements [IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement), which is an empty marker interface.

When a requirement doesn't contain data or have properties (parameters), it acts as an empty marker to trigger an associated *authorization handler* ([IAuthorizationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandler)) for processing authorization (described in detail later in this article). Because the handler in this case relies entirely on the HTTP context, user claims, or backend data to make a decision about the user meeting the requirement, the requirement class itself doesn't require internal data or parameters. The requirement only instructs the framework which rule to evaluate.

For example, consider the following minimum age requirement (`MinimumAgeRequirement`), which is implemented merely as a marker class:

```
public class MinimumAgeRequirement : IAuthorizationRequirement { }
```

The preceding requirement is used to create a policy that confirms the user is over a specific age that the handler checks. An `AuthorizationHandler<MinimumAgeRequirement>` inspects the `AuthorizationHandlerContext.User`. If the user has a birth date claim that indicates they're over a certain age, the requirement succeeds. The requirement object doesn't require any properties (parameters) in this case. The next example demonstrates the complete implementation of a minimum age requirement that has a parameter to set the minimum age.

Consider the following `MinimumAgeRequirement` requirement, which describes a single parameter, a minimum age, to evaluate for user authorization:

```
using Microsoft.AspNetCore.Authorization;

namespace BlazorWebAppAuthorization.Policies.Requirements;

public class MinimumAgeRequirement(int minimumAge) : IAuthorizationRequirement
{
    public int MinimumAge { get; } = minimumAge;
}
```

```
using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public MinimumAgeRequirement(int minimumAge) =>
        MinimumAge = minimumAge;

    public int MinimumAge { get; }
}
```

```
using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}
```

A policy is registered as part of the authorization service configuration in the app's `Program` file by calling [AuthorizationBuilder.AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder.addpolicy). The following example creates an `AtLeast21` policy with a single requirement of a minimum age, and it sets the minimum age to 21 years old.

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("AtLeast21", policy => 
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
```

A policy is registered as part of the authorization service configuration in the app's `Program` file by calling [AuthorizationBuilder.AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder.addpolicy). The following example creates an `AtLeast21` policy with a single requirement of a minimum age, and it sets the minimum age to 21 years old:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
```

A policy is registered as part of the authorization service configuration in `Startup.ConfigureServices` (`Startup.cs`) by calling [AuthorizationBuilder.AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder.addpolicy). The following example creates an `AtLeast21` policy with a single requirement of a minimum age, and it sets the minimum age to 21 years old:

```
services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
```

If an authorization policy contains multiple authorization requirements, all of the requirements must pass in order for the policy evaluation to succeed. In other words, multiple authorization requirements added to a single authorization policy are treated on an **AND** basis.

## Apply policies to Razor components

Apply policies to Razor components using the `[Authorize]` attribute with the policy name:

```
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "CustomerServiceMember")]
```

If multiple policies are applied, ***all*** policies must pass before access is granted:

```
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "CustomerServiceMember")]
@attribute [Authorize(Policy = "HumanResourcesMember")]
```

## Apply policies to endpoints

Apply policies to endpoints by using [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) with the policy name. For example:

```
app.MapGet("/helloworld", () => "Hello World!")
    .RequireAuthorization("AtLeast21");
```

## Apply policies in MVC and Razor Pages apps

For guidance on applying policies in Razor Pages and MVC apps, see the following resources:

* [Policy-based authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/policies?view=aspnetcore-10.0)
* [Policy-based authorization in ASP.NET Core MVC](../../mvc/security/authorization/policies?view=aspnetcore-10.0)

## Authorization service interface (`IAuthorizationService`)

[IAuthorizationService](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice) is primarily responsible for determining if authorization is successful when an [IAuthorizationService.AuthorizeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice.authorizeasync) overload is called:

* `AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)`: Checks if a user meets a specific set of authorization requirements for a specified resource.
* `AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)`: Checks if a user meets a specific authorization policy for a specified resource.

If a resource isn't required for policy evaluation, `null` is passed for the resource.

The preceding methods return an [AuthorizationResult](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationresult) wrapped in a [Task](/en-us/dotnet/api/system.threading.tasks.task).

Each [IAuthorizationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandler) is responsible for checking if requirements are met via [IAuthorizationHandler.HandleAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandler.handleasync). The [AuthorizationHandlerContext](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext) class contains the authorization information used by the [IAuthorizationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandler) implementation. [IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement) is a marker interface with no methods that serves as the mechanism for tracking whether authorization is successful. When [AuthorizationHandlerContext.Succeed](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.succeed) is called with the [IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement), the policy is met:

```
context.Succeed(requirement);
```

## Authorization handlers

An authorization handler is responsible for the evaluation of a requirement's properties. The authorization handler evaluates the requirements against a provided [AuthorizationHandlerContext](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext) to determine if access is allowed.

A requirement can have [multiple handlers](#why-would-i-want-multiple-handlers-for-a-requirement). A handler may inherit [AuthorizationHandler\<TRequirement>](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandler-1), where `TRequirement` is the requirement to handle. Alternatively, a handler may implement [IAuthorizationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandler) directly to handle more than one type of requirement.

### Use a handler for one requirement

The following example shows a one-to-one relationship in which a minimum age handler handles a single requirement:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Policies.Requirements;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = 
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        var calculatedAge = DateTime.Today.Year - dateOfBirth.Year;

        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
```

```
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = 
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        var calculatedAge = DateTime.Today.Year - dateOfBirth.Year;

        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
```

```
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
        {
            // Use the following if targeting a version of
            // .NET Framework older than 4.6:
            // return Task.FromResult(0);
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);

        var calculatedAge = DateTime.Today.Year - dateOfBirth.Year;

        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        // Use the following if targeting a version of
        // .NET Framework older than 4.6:
        // return Task.FromResult(0);
        return Task.CompletedTask;
    }
}
```

The preceding code determines if the current user principal has a date of birth claim. Authorization can't occur when the claim is missing, in which case a completed task is returned. When a claim is present, the user's age is calculated. If the user meets the minimum age defined by the requirement, authorization is considered successful. When authorization is successful, [`context.Succeed`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.succeed) is invoked with the satisfied requirement as its sole parameter.

### Use a handler for multiple requirements

The following example shows a one-to-many relationship in which a permission handler can handle three different types of requirements:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Policies.Requirements;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource)
                    || IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission || requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        return Task.CompletedTask;
    }

    private static bool IsOwner(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }

    private static bool IsSponsor(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }
}
```

```
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource) ||
                    IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission ||
                        requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        // Use the following if targeting a version of
        // .NET Framework older than 4.6:
        // return Task.FromResult(0);
        return Task.CompletedTask;
    }

    private bool IsOwner(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }

    private bool IsSponsor(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }
}
```

The preceding code traverses [PendingRequirements](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.pendingrequirements)—a property containing requirements not marked as successful. For a `ReadPermission` requirement, the user must be either an owner or a sponsor to access the requested resource. For an `EditPermission` or `DeletePermission` requirement, they must be an owner to access the requested resource.

### Handler registration

Register handlers in the services collection during configuration. The following example registers a minimum age handler (`MinimumAgeHandler`) as a singleton service, but a handler can be registered using any of the built-in [service lifetimes](../../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes):

```
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
```

```
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
```

It's possible to bundle both a requirement and a handler into a single class implementing both [IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement) and [IAuthorizationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandler). This bundling creates a tight coupling between the handler and requirement and is only recommended for simple requirements and handlers. Creating a class that implements both interfaces removes the need to register the handler in the service container due to the built-in [PassThroughAuthorizationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.passthroughauthorizationhandler) that allows requirements to handle themselves.

See the [implementation of the ASP.NET Core `AssertionRequirement` class](https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Core/src/AssertionRequirement.cs) for an example where the [AssertionRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.assertionrequirement) is both a requirement and the handler in a fully self-contained class. The [AssertionRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.infrastructure.assertionrequirement) framework's API allows you to validate access using inline lambda expressions instead of writing separate, boilerplate requirement and handler classes.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### What should a handler return?

The `Handle` method in the [handler example](#use-a-handler-for-one-requirement) returns no value. How is a status of either success or failure indicated?

* A handler indicates success by calling [`context.Succeed`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.succeed), passing the successfully validated requirement ([IAuthorizationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirement)).

* A handler isn't required to handle failures generally, as other handlers for the same requirement may succeed.

* To guarantee failure, even if other requirement handlers succeed, call [`context.Fail`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.fail).

If a handler calls [`context.Succeed`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.succeed) or [`context.Fail`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.fail), all other handlers are still called. This allows requirements to produce side effects, such as logging, which takes place even if another handler successfully validates or fails on a requirement. When set to `false`, the [InvokeHandlersAfterFailure](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.invokehandlersafterfailure) property short-circuits the execution of handlers when [`context.Fail`](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.fail) is called. [InvokeHandlersAfterFailure](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.invokehandlersafterfailure) defaults to `true`, in which case all handlers are called.

Note

Authorization handlers are called even if authentication fails. Also handlers can execute in any order, so do ***not*** depend on the order of calling handlers.

### Why would I want multiple handlers for a requirement?

In cases where you want evaluation to be on an **OR** basis, implement multiple handlers for a single requirement. For example, assume that the Contoso Corporation has doors that only open with key cards. If you leave your key card at home, the receptionist prints a temporary sticker and opens the door for you. In this scenario, the app has a single requirement but multiple handlers, each one examining a single requirement.

In the following example implementations:

* `BuildingEntryRequirement` is the building entry requirement.
* `BadgeEntryHandler` (the individual has a badge) and `TemporaryStickerHandler` (the individual has a temporary sticker) are separate handlers, each examining a single requirement.

`BuildingEntryRequirement.cs`:

```
using Microsoft.AspNetCore.Authorization;

namespace BlazorWebAppAuthorization.Policies.Requirements;

public class BuildingEntryRequirement : IAuthorizationRequirement { }
```

`BadgeEntryHandler.cs`:

```
using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Policies.Requirements;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "BadgeId"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
```

`TemporaryStickerHandler.cs`:

```
using Microsoft.AspNetCore.Authorization;
using BlazorWebAppAuthorization.Policies.Requirements;

namespace BlazorWebAppAuthorization.Policies.Handlers;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => 
            c.Type == "TemporaryBadgeId" &&
            c.Issuer == "https://contososecurity"))
        {
            // Code to check expiration date omitted for brevity.
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
```

`BuildingEntryRequirement.cs`:

```
using Microsoft.AspNetCore.Authorization;

public class BuildingEntryRequirement : IAuthorizationRequirement
{
}
```

`BadgeEntryHandler.cs`:

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                    BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => 
            c.Type == "BadgeId" &&
            c.Issuer == "https://contososecurity"))
        {
            context.Succeed(requirement);
        }

        // Use the following if targeting a version of
        // .NET Framework older than 4.6:
        // return Task.FromResult(0);
        return Task.CompletedTask;
    }
}
```

`TemporaryStickerHandler.cs`:

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
        BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => 
            c.Type == "TemporaryBadgeId" &&
            c.Issuer == "https://contososecurity"))
        {
            // We'd also check the expiration date on the sticker.
            context.Succeed(requirement);
        }

        // Use the following if targeting a version of
        // .NET Framework older than 4.6:
        // return Task.FromResult(0);
        return Task.CompletedTask;
    }
}
```

Ensure that both handlers are [registered](policies?view=aspnetcore-10.0#handler-registration). If either of the handlers succeed when a policy evaluates the `BuildingEntryRequirement`, the policy evaluation succeeds.

## Use a `Func` to fulfill a policy

There are situations where fulfilling a policy is simple to express in code with a `Func<AuthorizationHandlerContext, bool>` delegate when configuring a policy with the `RequireAssertion` policy builder. For example, the preceding `BadgeEntryHandler` can be rewritten as follows:

```
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
            (c.Type == "BadgeId" || c.Type == "TemporaryBadgeId")
            && c.Issuer == "https://contososecurity")));
});

// <snippet_minimumAgeHandlerRegistration>
```

```
services.AddAuthorization(options =>
{
     options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                (c.Type == "BadgeId" ||
                 c.Type == "TemporaryBadgeId") &&
                 c.Issuer == "https://microsoftsecurity")));
});
```

## Authorization via an external service sample

The [Authorization via an external service sample (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/AuthorizationExternalService) shows how to implement additional authorization requirements with an external authorization service. The solution's `Contoso.API` project is secured with [Microsoft Entra ID](/en-us/entra/fundamentals/what-is-entra). An additional authorization check from the `Contoso.Security.API` project returns a payload describing whether the `Contoso.API` client app can invoke the `GetWeather` API.

### Configure the sample

The following demonstration relies on using [NSwag (Swagger/OpenAPI)](https://github.com/RicoSuter/NSwag) or [cURL](https://curl.se/) in a command shell.

In the `Contoso.Security.API` project, set the `AllowedClients` placeholder (`{CLIENT ID}`) to any test GUID value (for example, `00001111-aaaa-2222-bbbb-3333cccc4444`):

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AllowedClients": [
    "{CLIENT ID (FOR THE CLIENT CALLING CONTOSO.API)}"
  ]
}
```

In a command shell opened to the `Contoso.API` project, use [`dotnet user-jwts`](../authentication/jwt-authn?view=aspnetcore-10.0) to generate an access token with an `appid` claim for the client app's ID, which was created in the preceding step (for example, `00001111-aaaa-2222-bbbb-3333cccc4444`).

```
dotnet user-jwts create --claim appid={GUID}
```

Example:

```
dotnet user-jwts create --claim appid=00001111-aaaa-2222-bbbb-3333cccc4444
```

The output produces a token after "`Token:`" in the command shell:

```
New JWT saved with ID '{JWT ID}'.
Name: {USER}
Custom Claims: [appid=00001111-aaaa-2222-bbbb-3333cccc4444]

Token: {TOKEN}
```

Set the value of the token (where the `{TOKEN}` placeholder appears in the preceding output) aside for use later.

You can decode the token in an online JWT decoder, such as [`jwt.ms`](https://jwt.ms/) to see its contents, revealing that it contains an `appid` claim with the client app's ID:

```
{
  "alg": "HS256",
  "typ": "JWT"
}.{
  "unique_name": "{USER}",
  "sub": "{USER}",
  "jti": "14ed7729",
  "appid": "{CLIENT ID}",
  "aud": [
    "https://localhost:7250",
    "http://localhost:7251"
  ],
  "nbf": 1780660887,
  "exp": 1788609687,
  "iat": 1780660888,
  "iss": "dotnet-user-jwts"
}.[Signature]
```

Execute the command again with an incorrect client ID (`appid`) value:

```
dotnet user-jwts create --claim appid=aaaabbbb-0000-cccc-1111-dddd2222eeee
```

Set the value of the second token aside.

Start both the `Contoso.API` and `Contoso.Security.API` projects in Visual Studio or with the `dotnet watch` command in a command shell:

```
dotnet watch
```

* [Swagger UI](#tabpanel_1_swagger-ui)
* [cURL in a command shell](#tabpanel_1_curl-command-shell)

In the Swagger UI of the `Contoso.API` project (`https://localhost:7250/swagger/index.html`), select the **Authorize** button.

In the **Available authorizations: Bearer** window, enter the access token. Select the **Authorize** button. Close the **Available authorizations** window.

Under **default**, select the **Get** button for the `/WeatherForecast` endpoint. Select the **Try it out** button. Select the **Execute** button.

The output under **Responses** > **Server response** > **Response body** shows the weather forecast JSON returned by the `Contoso.API` project.

Perform the same steps with the access token that was generated with an invalid client app ID. The response is *403 - Forbidden*.

In a command shell, use the .NET CLI to execute the following `curl.exe` command to request the `WeatherForecast` endpoint. Replace the `{TOKEN}` placeholder with the first JWT bearer token that you saved earlier:

```
curl.exe -i -H "Authorization: Bearer {TOKEN}" https://localhost:7250/WeatherForecast
```

The output indicates success because the user's birth date claim indicates that they're at least 21 years old:

```
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 05 Jun 2026 12:48:58 GMT
Server: Kestrel
Transfer-Encoding: chunked

[{ ... WEATHER DATA ... }]
```

Execute the command again using the second token with the invalid client ID (`appid`). The result indicates a policy failure:

```
HTTP/1.1 403 Forbidden
Content-Length: 0
Date: Fri, 05 Jun 2026 13:09:56 GMT
Server: Kestrel
```

You can add breakpoints in the `Contoso.Security.API.SecurityPolicyController` and see that the passed client ID (`appid`) is used to assert whether it is allowed to obtain weather data.

You can also send the client ID directly to the `Contoso.Security.API` either via the Swagger UI or cURL in a command shell (for example: `https://localhost:7123/SecurityPolicy/{CLIENT ID}`) to see it return either `true` or `false` for `canGetWeather`.

```
curl.exe -i -H "Authorization: Bearer {TOKEN}" https://localhost:7123/SecurityPolicy/{CLIENT ID}
```

With the correct client ID (`appid`), `canGetWeather` is `true`:

```
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 05 Jun 2026 13:19:49 GMT
Server: Kestrel
Transfer-Encoding: chunked

{"canGetWeather":true}
```

## Related content

* [Quickstart: Configure an application to expose a web API](/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis)
* [Authorization via an external service sample (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/AuthorizationExternalService)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/app-download-size
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/performance/app-download-size.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor app download size performance best practices

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

## Runtime relinking

For information on how runtime relinking minimizes an app's download size, see [ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation](../webassembly-build-tools-and-aot?view=aspnetcore-10.0#runtime-relinking).

## Use `System.Text.Json`

Blazor's JS interop implementation relies on [System.Text.Json](/en-us/dotnet/api/system.text.json), which is a high-performance JSON serialization library with low memory allocation. Using [System.Text.Json](/en-us/dotnet/api/system.text.json) shouldn't result in additional app payload size over adding one or more alternate JSON libraries.

For migration guidance, see [How to migrate from `Newtonsoft.Json` to `System.Text.Json`](/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to).

## Intermediate Language (IL) trimming

*This section only applies to client-side Blazor scenarios.*

Trimming unused assemblies from a Blazor WebAssembly app reduces the app's size by removing unused code in the app's binaries. For more information, see [Configure the Trimmer for ASP.NET Core Blazor](../host-and-deploy/configure-trimmer?view=aspnetcore-10.0).

[Linking a Blazor WebAssembly app](../host-and-deploy/configure-linker?view=aspnetcore-10.0) reduces the app's size by trimming unused code in the app's binaries. The Intermediate Language (IL) Linker is only enabled when building in `Release` configuration. To benefit from this, publish the app for deployment using the [`dotnet publish`](/en-us/dotnet/core/tools/dotnet-publish) command with the [-c|--configuration](/en-us/dotnet/core/tools/dotnet-publish#options) option set to `Release`:

```
dotnet publish -c Release
```

## Lazy load assemblies

*This section only applies to client-side Blazor scenarios.*

Load assemblies at runtime when the assemblies are required by a route. For more information, see [Lazy load assemblies in ASP.NET Core Blazor WebAssembly](../webassembly-lazy-load-assemblies?view=aspnetcore-10.0).

## Compression

*This section only applies to Blazor WebAssembly apps.*

When a Blazor WebAssembly app is published, the output is statically compressed during publish to reduce the app's size and remove the overhead for runtime compression. Blazor relies on the server to perform content negotiation and serve statically-compressed files.

After an app is deployed, verify that the app serves compressed files. Inspect the **Network** tab in a browser's [developer tools](https://developer.mozilla.org/docs/Glossary/Developer_Tools) and verify that the files are served with `Content-Encoding: br` (Brotli compression) or `Content-Encoding: gz` (Gzip compression). If the host isn't serving compressed files, follow the instructions in [Host and deploy ASP.NET Core Blazor WebAssembly](../host-and-deploy/webassembly/?view=aspnetcore-10.0#compression).

## Disable unused features

*This section only applies to client-side Blazor scenarios.*

Blazor WebAssembly's runtime includes the following .NET features that can be disabled for a smaller payload size.

Blazor WebAssembly carries globalization resources required to display values, such as dates and currency, in the user's culture. If the app doesn't require localization, you may configure the app to [support the invariant culture](../globalization-localization?view=aspnetcore-10.0#invariant-globalization), which is based on the `en-US` culture. Apply the `<InvariantGlobalization>` MSBuild property with a value of `true` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
```

Adopting [invariant globalization](../globalization-localization?view=aspnetcore-10.0#invariant-globalization) only results in using non-localized timezone names. To trim timezone code and data from the app, apply the `<InvariantTimezone>` MSBuild property with a value of `true` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <InvariantTimezone>true</InvariantTimezone>
</PropertyGroup>
```

Note

[`<BlazorEnableTimeZoneSupport>`](app-download-size?view=aspnetcore-10.0#disable-unused-features) overrides an earlier `<InvariantTimezone>` setting. We recommend removing the `<BlazorEnableTimeZoneSupport>` setting.

A data file is included to make timezone information correct. If the app doesn't require this feature, consider disabling it by setting the `<BlazorEnableTimeZoneSupport>` MSBuild property to `false` in the app's project file:

```
<PropertyGroup>
  <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
</PropertyGroup>
```

Collation information is included to make APIs such as [StringComparison.InvariantCultureIgnoreCase](/en-us/dotnet/api/system.stringcomparison#system-stringcomparison-invariantcultureignorecase) work correctly. If you're certain that the app doesn't require the collation data, consider disabling it by setting the `BlazorWebAssemblyPreserveCollationData` MSBuild property in the app's project file to `false`:

```
<PropertyGroup>
  <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
</PropertyGroup>
```

## Related content

[Configuring and hosting .NET WebAssembly applications](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/extensibility/key-management
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/extensibility/key-management.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Key management extensibility in ASP.NET Core

Read the [key management](../implementation/key-management?view=aspnetcore-10.0#data-protection-implementation-key-management) section before reading this section, as it explains some of the fundamental concepts behind these APIs.

**Warning**: Types that implement any of the following interfaces should be thread-safe for multiple callers.

## Key

The `IKey` interface is the basic representation of a key in cryptosystem. The term key is used here in the abstract sense, not in the literal sense of "cryptographic key material". A key has the following properties:

* Activation, creation, and expiration dates

* Revocation status

* Key identifier (a GUID)

Additionally, `IKey` exposes a `CreateEncryptor` method which can be used to create an [IAuthenticatedEncryptor](core-crypto?view=aspnetcore-10.0#data-protection-extensibility-core-crypto-iauthenticatedencryptor) instance tied to this key.

Additionally, `IKey` exposes a `CreateEncryptorInstance` method which can be used to create an [IAuthenticatedEncryptor](core-crypto?view=aspnetcore-10.0#data-protection-extensibility-core-crypto-iauthenticatedencryptor) instance tied to this key.

Note

There's no API to retrieve the raw cryptographic material from an `IKey` instance.

## IKeyManager

The `IKeyManager` interface represents an object responsible for general key storage, retrieval, and manipulation. It exposes three high-level operations:

* Create a new key and persist it to storage.

* Get all keys from storage.

* Revoke one or more keys and persist the revocation information to storage.

Warning

Writing an `IKeyManager` is a very advanced task, and the majority of developers shouldn't attempt it. Instead, most developers should take advantage of the facilities offered by the [XmlKeyManager](#xmlkeymanager) class.

## XmlKeyManager

The `XmlKeyManager` type is the in-box concrete implementation of `IKeyManager`. It provides several useful facilities, including key escrow and encryption of keys at rest. Keys in this system are represented as XML elements (specifically, [XElement](/en-us/dotnet/csharp/programming-guide/concepts/linq/xelement-class-overview)).

`XmlKeyManager` depends on several other components in the course of fulfilling its tasks:

* `AlgorithmConfiguration`, which dictates the algorithms used by new keys.

* `IXmlRepository`, which controls where keys are persisted in storage.

* `IXmlEncryptor` \[optional], which allows encrypting keys at rest.

* `IKeyEscrowSink` \[optional], which provides key escrow services.

- `IXmlRepository`, which controls where keys are persisted in storage.

- `IXmlEncryptor` \[optional], which allows encrypting keys at rest.

- `IKeyEscrowSink` \[optional], which provides key escrow services.

Below are high-level diagrams which indicate how these components are wired together within `XmlKeyManager`.

*Key Creation / CreateNewKey*

In the implementation of `CreateNewKey`, the `AlgorithmConfiguration` component is used to create a unique `IAuthenticatedEncryptorDescriptor`, which is then serialized as XML. If a key escrow sink is present, the raw (unencrypted) XML is provided to the sink for long-term storage. The unencrypted XML is then run through an `IXmlEncryptor` (if required) to generate the encrypted XML document. This encrypted document is persisted to long-term storage via the `IXmlRepository`. (If no `IXmlEncryptor` is configured, the unencrypted document is persisted in the `IXmlRepository`.)

*Key Creation / CreateNewKey*

In the implementation of `CreateNewKey`, the `IAuthenticatedEncryptorConfiguration` component is used to create a unique `IAuthenticatedEncryptorDescriptor`, which is then serialized as XML. If a key escrow sink is present, the raw (unencrypted) XML is provided to the sink for long-term storage. The unencrypted XML is then run through an `IXmlEncryptor` (if required) to generate the encrypted XML document. This encrypted document is persisted to long-term storage via the `IXmlRepository`. (If no `IXmlEncryptor` is configured, the unencrypted document is persisted in the `IXmlRepository`.)

*Key Retrieval / GetAllKeys*

In the implementation of `GetAllKeys`, the XML documents representing keys and revocations are read from the underlying `IXmlRepository`. If these documents are encrypted, the system will automatically decrypt them. `XmlKeyManager` creates the appropriate `IAuthenticatedEncryptorDescriptorDeserializer` instances to deserialize the documents back into `IAuthenticatedEncryptorDescriptor` instances, which are then wrapped in individual `IKey` instances. This collection of `IKey` instances is returned to the caller.

Further information on the particular XML elements can be found in the [key storage format document](../implementation/key-storage-format?view=aspnetcore-10.0#data-protection-implementation-key-storage-format).

## IXmlRepository

The `IXmlRepository` interface represents a type that can persist XML to and retrieve XML from a backing store. It exposes two APIs:

* `GetAllElements` :`IReadOnlyCollection<XElement>`

* `StoreElement(XElement element, string friendlyName)`

Implementations of `IXmlRepository` don't need to parse the XML passing through them. They should treat the XML documents as opaque and let higher layers worry about generating and parsing the documents.

There are four built-in concrete types which implement `IXmlRepository`:

* [FileSystemXmlRepository](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.repositories.filesystemxmlrepository)
* [RegistryXmlRepository](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.repositories.registryxmlrepository)
* [AzureStorage.AzureBlobXmlRepository](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azurestorage.azureblobxmlrepository)
* [RedisXmlRepository](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.stackexchangeredis.redisxmlrepository)

- [FileSystemXmlRepository](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.repositories.filesystemxmlrepository)
- [RegistryXmlRepository](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.repositories.registryxmlrepository)
- [AzureStorage.AzureBlobXmlRepository](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azurestorage.azureblobxmlrepository)
- [RedisXmlRepository](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.redisxmlrepository)

See the [key storage providers document](../implementation/key-storage-providers?view=aspnetcore-10.0) for more information.

Registering a custom `IXmlRepository` is appropriate when using a different backing store (for example, Azure Table Storage).

To change the default repository application-wide, register a custom `IXmlRepository` instance:

```
services.Configure<KeyManagementOptions>(options => options.XmlRepository = new MyCustomXmlRepository());
```

```
services.AddSingleton<IXmlRepository>(new MyCustomXmlRepository());
```

## IXmlEncryptor

The `IXmlEncryptor` interface represents a type that can encrypt a plaintext XML element. It exposes a single API:

* Encrypt(XElement plaintextElement) : EncryptedXmlInfo

If a serialized `IAuthenticatedEncryptorDescriptor` contains any elements marked as "requires encryption", then `XmlKeyManager` will run those elements through the configured `IXmlEncryptor`'s `Encrypt` method, and it will persist the enciphered element rather than the plaintext element to the `IXmlRepository`. The output of the `Encrypt` method is an `EncryptedXmlInfo` object. This object is a wrapper which contains both the resultant enciphered `XElement` and the Type which represents an `IXmlDecryptor` which can be used to decipher the corresponding element.

There are four built-in concrete types which implement `IXmlEncryptor`:

* [CertificateXmlEncryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.xmlencryption.certificatexmlencryptor)
* [DpapiNGXmlEncryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.xmlencryption.dpapingxmlencryptor)
* [DpapiXmlEncryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.xmlencryption.dpapixmlencryptor)
* [NullXmlEncryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.xmlencryption.nullxmlencryptor)

See the [key encryption at rest document](../implementation/key-encryption-at-rest?view=aspnetcore-10.0) for more information.

To change the default key-encryption-at-rest mechanism application-wide, register a custom `IXmlEncryptor` instance:

```
services.Configure<KeyManagementOptions>(options => options.XmlEncryptor = new MyCustomXmlEncryptor());
```

```
services.AddSingleton<IXmlEncryptor>(new MyCustomXmlEncryptor());
```

## IXmlDecryptor

The `IXmlDecryptor` interface represents a type that knows how to decrypt an `XElement` that was enciphered via an `IXmlEncryptor`. It exposes a single API:

* Decrypt(XElement encryptedElement) : XElement

The `Decrypt` method undoes the encryption performed by `IXmlEncryptor.Encrypt`. Generally, each concrete `IXmlEncryptor` implementation will have a corresponding concrete `IXmlDecryptor` implementation.

Types which implement `IXmlDecryptor` should have one of the following two public constructors:

* .ctor(IServiceProvider)
* .ctor()

Note

The `IServiceProvider` passed to the constructor may be null.

## IKeyEscrowSink

The `IKeyEscrowSink` interface represents a type that can perform escrow of sensitive information. Recall that serialized descriptors might contain sensitive information (such as cryptographic material), and this is what led to the introduction of the [IXmlEncryptor](#ixmlencryptor) type in the first place. However, accidents happen, and key rings can be deleted or become corrupted.

The escrow interface provides an emergency escape hatch, allowing access to the raw serialized XML before it's transformed by any configured [IXmlEncryptor](#ixmlencryptor). The interface exposes a single API:

* Store(Guid keyId, XElement element)

It's up to the `IKeyEscrowSink` implementation to handle the provided element in a secure manner consistent with business policy. One possible implementation could be for the escrow sink to encrypt the XML element using a known corporate X.509 certificate where the certificate's private key has been escrowed; the `CertificateXmlEncryptor` type can assist with this. The `IKeyEscrowSink` implementation is also responsible for persisting the provided element appropriately.

By default no escrow mechanism is enabled, though server administrators can [configure this globally](../configuration/machine-wide-policy?view=aspnetcore-10.0). It can also be configured programmatically via the `IDataProtectionBuilder.AddKeyEscrowSink` method as shown in the sample below. The `AddKeyEscrowSink` method overloads mirror the `IServiceCollection.AddSingleton` and `IServiceCollection.AddInstance` overloads, as `IKeyEscrowSink` instances are intended to be singletons. If multiple `IKeyEscrowSink` instances are registered, each one will be called during key generation, so keys can be escrowed to multiple mechanisms simultaneously.

There's no API to read material from an `IKeyEscrowSink` instance. This is consistent with the design theory of the escrow mechanism: it's intended to make the key material accessible to a trusted authority, and since the application is itself not a trusted authority, it shouldn't have access to its own escrowed material.

The following sample code demonstrates creating and registering an `IKeyEscrowSink` where keys are escrowed such that only members of "CONTOSODomain Admins" can recover them.

Note

To run this sample, you must be on a domain-joined Windows 8 / Windows Server 2012 machine, and the domain controller must be Windows Server 2012 or later.

```
using System;
using System.IO;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class Program
{
    public static void Main(string[] args)
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddDataProtection()
            .PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys"))
            .ProtectKeysWithDpapi()
            .AddKeyEscrowSink(sp => new MyKeyEscrowSink(sp));
        var services = serviceCollection.BuildServiceProvider();

        // get a reference to the key manager and force a new key to be generated
        Console.WriteLine("Generating new key...");
        var keyManager = services.GetService<IKeyManager>();
        keyManager.CreateNewKey(
            activationDate: DateTimeOffset.Now,
            expirationDate: DateTimeOffset.Now.AddDays(7));
    }

    // A key escrow sink where keys are escrowed such that they
    // can be read by members of the CONTOSO\Domain Admins group.
    private class MyKeyEscrowSink : IKeyEscrowSink
    {
        private readonly IXmlEncryptor _escrowEncryptor;

        public MyKeyEscrowSink(IServiceProvider services)
        {
            // Assuming I'm on a machine that's a member of the CONTOSO
            // domain, I can use the Domain Admins SID to generate an
            // encrypted payload that only they can read. Sample SID from
            // https://technet.microsoft.com/library/cc778824(v=ws.10).aspx.
            _escrowEncryptor = new DpapiNGXmlEncryptor(
                "SID=S-1-5-21-1004336348-1177238915-682003330-512",
                DpapiNGProtectionDescriptorFlags.None,
                new LoggerFactory());
        }

        public void Store(Guid keyId, XElement element)
        {
            // Encrypt the key element to the escrow encryptor.
            var encryptedXmlInfo = _escrowEncryptor.Encrypt(element);

            // A real implementation would save the escrowed key to a
            // write-only file share or some other stable storage, but
            // in this sample we'll just write it out to the console.
            Console.WriteLine($"Escrowing key {keyId}");
            Console.WriteLine(encryptedXmlInfo.EncryptedElement);

            // Note: We cannot read the escrowed key material ourselves.
            // We need to get a member of CONTOSO\Domain Admins to read
            // it for us in the event we need to recover it.
        }
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Generating new key...
 * Escrowing key 38e74534-c1b8-4b43-aea1-79e856a822e5
 * <encryptedKey>
 *   <!-- This key is encrypted with Windows DPAPI-NG. -->
 *   <!-- Rule: SID=S-1-5-21-1004336348-1177238915-682003330-512 -->
 *   <value>MIIIfAYJKoZIhvcNAQcDoIIIbTCCCGkCAQ...T5rA4g==</value>
 * </encryptedKey>
 */
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/add-user-data
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/add-user-data.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Add, download, and delete custom user data to Identity in an ASP.NET Core project

This article shows how to:

* Add custom user data to an ASP.NET Core web app.
* Mark the custom user data model with the [PersonalDataAttribute](/en-us/dotnet/api/microsoft.aspnetcore.identity.personaldataattribute) attribute so it's automatically available for download and deletion. Making the data able to be downloaded and deleted helps meet [GDPR](../gdpr?view=aspnetcore-10.0) requirements.

The project sample is created from a Razor Pages web app, but the instructions are similar for an ASP.NET Core MVC web app.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/add-user-data) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

[.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)

## Quickstart: Add two custom properties

For the common case of adding two custom properties such as `FirstName` and `LastName`:

1. Create a new ASP.NET Core project with Individual Accounts authentication
2. Scaffold Identity to override the Register and Manage/Index pages
3. Add custom properties such as `FirstName` and `LastName` to the user class, with the `[PersonalData]` attribute.
4. Update the Register and Manage/Index pages to include input fields for your properties.
5. Create and apply a migration.
6. Test registration and profile management.

The sections below provide detailed step-by-step instructions for this process.

## Create a web app

You can create either a Razor Pages or MVC web app. Both use the same Identity Razor Pages UI.

### Razor Pages

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project/Solution...**.
- Select **ASP.NET Core Web App (Razor Pages)** > **Next**
- Name the project **WebApp1** if you want it to match the namespace of the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/add-user-data) code. Select **Next**.
- Select **Authentication type** > **Individual Accounts** > **Create**
- Build and run the project.

```
dotnet new webapp -au Individual -o WebApp1
```

### MVC

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project/Solution...**.
- Select **ASP.NET Core Web App (Model-View-Controller)** > **Next**
- Name the project **WebApp1** if you want it to match the namespace of the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/add-user-data) code. Select **Next**.
- Select **Authentication type** > **Individual Accounts** > **Create**
- Build and run the project.

```
dotnet new mvc -au Individual -o WebApp1
```

Note

Identity UI is implemented as a Razor Class Library. When you create an MVC project with authentication, the Identity pages are served from the Razor Class Library, which means your MVC project uses Razor Pages for Identity even though the rest of the project uses MVC controllers and views.

## Run the Identity scaffolder

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

- From **Solution Explorer**, right-click on the project > **Add** > **New Scaffolded Item**.

- From the left pane of the **Add Scaffold** dialog, select **Identity** > **Add**.

- In the **Add Identity** dialog, the following options:

  * Select the existing layout file `~/Pages/Shared/_Layout.cshtml` for Razor Pages or `~/Views/Shared/_Layout.cshtml` for MVC

  * Select the following files to override:

    * **Account/Register**
    * **Account/Manage/Index**

  * Select the **+** button to create a new **Data context class**. Accept the type (**WebApp1.Models.WebApp1Context** if the project is named **WebApp1**).

  * Select the **+** button to create a new **User class**. Accept the type (**WebApp1User** if the project is named **WebApp1**) > **Add**.

- Select **Add**.

If you have not previously installed the ASP.NET Core scaffolder, install it now:

```
dotnet tool install -g dotnet-aspnet-codegenerator
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Add a package reference to [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design/) to the project (.csproj) file. Run the following command in the project directory:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet restore
```

Run the following command to list the Identity scaffolder options:

```
dotnet aspnet-codegenerator identity -h
```

In the project folder, run the Identity scaffolder:

```
dotnet aspnet-codegenerator identity -u WebApp1User -fi Account.Register;Account.Manage.Index
```

PowerShell uses semicolon as a command separator. When using PowerShell, escape the semi-colons in the file list or put the file list in double quotes.

Follow the instructions in [Migrations, UseAuthentication, and layout](scaffold-identity?view=aspnetcore-10.0#efm) to perform the following steps:

* Create a migration and update the database.

* Add `UseAuthentication` to [`Program.cs`](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/add-user-data/samples/10.x/SampleApp/Program.cs#L24)

* Add `<partial name="_LoginPartial" />` to the layout file.

* Test the app:

  * Register a user
  * Select the new user name (next to the **Logout** link). You might need to expand the window or select the navigation bar icon to show the user name and other links.
  * Select the **Personal Data** tab.
  * Select the **Download** button and examine the `PersonalData.json` file.
  * Test the **Delete** button, which deletes the logged on user.

## Add custom user data to the Identity DB

Update the `IdentityUser` derived class with custom properties. If you named the project WebApp1, the file is named `Areas/Identity/Data/WebApp1User.cs`. Update the file with the following code:

```
using Microsoft.AspNetCore.Identity;

namespace WebApp1.Areas.Identity.Data;

public class WebApp1User : IdentityUser
{
    [PersonalData]
    public string? Name { get; set; }
    [PersonalData]
    public DateTime DOB { get; set; }
}
```

Properties with the [PersonalData](/en-us/dotnet/api/microsoft.aspnetcore.identity.personaldataattribute) attribute are:

* Deleted when the `Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml` Razor Page calls `UserManager.Delete`.
* Included in the downloaded data by the `Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml` Razor Page.

### Update the `Account/Manage/Index.cshtml` page

Update the `InputModel` in `Areas/Identity/Pages/Account/Manage/Index.cshtml.cs` with the following highlighted code:

```
public class IndexModel : PageModel
{
    private readonly UserManager<WebApp1User> _userManager;
    private readonly SignInManager<WebApp1User> _signInManager;

    public IndexModel(
        UserManager<WebApp1User> userManager,
        SignInManager<WebApp1User> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    /// <summary>
    ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
    ///     directly from your code. This API may change or be removed in future releases.
    /// </summary>
    public string Username { get; set; }

    // Remaining API warnings omited.

    [TempData]
    public string StatusMessage { get; set; }

    [BindProperty]
    public InputModel Input { get; set; }

    public class InputModel
    {
        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Full name")]
        public string Name { get; set; }

        [Required]
        [Display(Name = "Birth Date")]
        [DataType(DataType.Date)]
        public DateTime DOB { get; set; }

        [Phone]
        [Display(Name = "Phone number")]
        public string PhoneNumber { get; set; }
    }

    private async Task LoadAsync(WebApp1User user)
    {
        var userName = await _userManager.GetUserNameAsync(user);
        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);

        Username = userName;

        Input = new InputModel
        {
            Name = user.Name,
            DOB = user.DOB,
            PhoneNumber = phoneNumber
        };
    }

    public async Task<IActionResult> OnGetAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        await LoadAsync(user);
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        if (!ModelState.IsValid)
        {
            await LoadAsync(user);
            return Page();
        }

        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
        if (Input.PhoneNumber != phoneNumber)
        {
            var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
            if (!setPhoneResult.Succeeded)
            {
                StatusMessage = "Unexpected error when trying to set phone number.";
                return RedirectToPage();
            }
        }

        if (Input.Name != user.Name)
        {
            user.Name = Input.Name;
        }

        if (Input.DOB != user.DOB)
        {
            user.DOB = Input.DOB;
        }

        await _userManager.UpdateAsync(user);
        await _signInManager.RefreshSignInAsync(user);
        StatusMessage = "Your profile has been updated";
        return RedirectToPage();
    }
}
```

Update the `Areas/Identity/Pages/Account/Manage/Index.cshtml` with the following highlighted markup:

```
@page
@model IndexModel
@{
    ViewData["Title"] = "Profile";
    ViewData["ActivePage"] = ManageNavPages.Index;
}

<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
    <div class="col-md-6">
        <form id="profile-form" method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-floating">
                <input asp-for="Username" class="form-control" disabled />
                <label asp-for="Username" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.Name" class="form-control" />
                <label asp-for="Input.Name" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.DOB" class="form-control" />
                <label asp-for="Input.DOB" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.PhoneNumber" class="form-control" />
                <label asp-for="Input.PhoneNumber" class="form-label"></label>
                <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
            </div>
            <button id="update-profile-button" type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
        </form>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

### Update the `Account/Register.cshtml` page

Update the `InputModel` in `Areas/Identity/Pages/Account/Register.cshtml.cs` with the following highlighted code:

```
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<WebApp1User> _signInManager;
        private readonly UserManager<WebApp1User> _userManager;
        private readonly IUserStore<WebApp1User> _userStore;
        private readonly IUserEmailStore<WebApp1User> _emailStore;
        private readonly ILogger<RegisterModel> _logger;
        private readonly IEmailSender _emailSender;

        public RegisterModel(
            UserManager<WebApp1User> userManager,
            IUserStore<WebApp1User> userStore,
            SignInManager<WebApp1User> signInManager,
            ILogger<RegisterModel> logger,
            IEmailSender emailSender)
        {
            _userManager = userManager;
            _userStore = userStore;
            _emailStore = GetEmailStore();
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
        }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        [BindProperty]
        public InputModel Input { get; set; }

        // Remaining API warnings omited.
        public string ReturnUrl { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public class InputModel
        {
            [Required]
            [DataType(DataType.Text)]
            [Display(Name = "Full name")]
            public string Name { get; set; }

            [Required]
            [Display(Name = "Birth Date")]
            [DataType(DataType.Date)]
            public DateTime DOB { get; set; }

            [Required]
            [EmailAddress]
            [Display(Name = "Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "Password")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "Confirm password")]
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
            public string ConfirmPassword { get; set; }
        }


        public async Task OnGetAsync(string returnUrl = null)
        {
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                var user = CreateUser();

                user.Name = Input.Name;
                user.DOB = Input.DOB;

                await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
                await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
                var result = await _userManager.CreateAsync(user, Input.Password);

                if (result.Succeeded)
                {
                    _logger.LogInformation("User created a new account with password.");

                    var userId = await _userManager.GetUserIdAsync(user);
                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                        protocol: Request.Scheme);

                    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    }
                    else
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    }
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }

        private WebApp1User CreateUser()
        {
            try
            {
                return Activator.CreateInstance<WebApp1User>();
            }
            catch
            {
                throw new InvalidOperationException($"Can't create an instance of '{nameof(WebApp1User)}'. " +
                    $"Ensure that '{nameof(WebApp1User)}' is not an abstract class and has a parameterless constructor, or alternatively " +
                    $"override the register page in /Areas/Identity/Pages/Account/Register.cshtml");
            }
        }

        private IUserEmailStore<WebApp1User> GetEmailStore()
        {
            if (!_userManager.SupportsUserEmail)
            {
                throw new NotSupportedException("The default UI requires a user store with email support.");
            }
            return (IUserEmailStore<WebApp1User>)_userStore;
        }
    }
}
```

Update the `Areas/Identity/Pages/Account/Register.cshtml` with the following highlighted markup:

```
@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h2>Create a new account.</h2>
            <hr />
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>

            <div class="form-floating">
                <input asp-for="Input.Name" class="form-control" />
                <label asp-for="Input.Name"></label>
                <span asp-validation-for="Input.Name" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.DOB" class="form-control" />
                <label asp-for="Input.DOB"></label>
                <span asp-validation-for="Input.DOB" class="text-danger"></span>
            </div>

            <div class="form-floating">
                <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
                <label asp-for="Input.Email"></label>
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.Password"></label>
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.ConfirmPassword"></label>
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
        </form>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <h3>Use another service to register.</h3>
            <hr />
            @{
                if ((Model.ExternalLogins?.Count ?? 0) == 0)
                {
                    <div>
                        <p>
                            There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
                            about setting up this ASP.NET application to support logging in via external services</a>.
                        </p>
                    </div>
                }
                else
                {
                    <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                        <div>
                            <p>
                                @foreach (var provider in Model.ExternalLogins!)
                                {
                                    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                                }
                            </p>
                        </div>
                    </form>
                }
            }
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

Build the project.

### Update the layout

See [Layout changes](scaffold-identity?view=aspnetcore-10.0#layout-changes) for instructions to add sign-in and sign-out links to every page.

### Add a migration for the custom user data

* [Visual Studio](#tabpanel_4_visual-studio)
* [.NET CLI](#tabpanel_4_net-cli)

In the Visual Studio **Package Manager Console**:

```
Add-Migration CustomUserData
Update-Database
```

```
dotnet ef migrations add CustomUserData
dotnet ef database update
```

## Test create, view, download, delete custom user data

Test the app:

* Register a new user.
* View the custom user data on the `/Identity/Account/Manage` page.
* Download and view the users personal data from the `/Identity/Account/Manage/PersonalData` page.

## Troubleshooting

If you encounter issues:

| Symptom                                         | Likely Cause                               | Solution                                                                             |
| ----------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------ |
| Custom fields don't appear in registration form | Forgot to update Register.cshtml           | Ensure you've scaffolded and modified `Areas/Identity/Pages/Account/Register.cshtml` |
| Custom data not saving                          | Forgot to add migration or update database | Run `dotnet ef migrations add CustomUserData` then `dotnet ef database update`       |
| Custom fields not in downloaded data            | Missing `[PersonalData]` attribute         | Add `[PersonalData]` attribute to properties in your user class                      |
| Build errors after scaffolding                  | Package version mismatch                   | Ensure all Identity packages match your target framework version                     |

[.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)

Note

The code samples and instructions for .NET 9 reference the 10.x sample folder. The code is identical between .NET 9 and .NET 10 for this scenario. Only the target framework and package versions differ in the project file.

## Quick start: Add two custom properties

For the common case of adding two custom properties such as `FirstName` and `LastName`:

1. Create a new ASP.NET Core project with Individual Accounts authentication
2. Scaffold Identity to override the Register and Manage/Index pages
3. Add custom properties such as `FirstName` and `LastName` to the user class, with the `[PersonalData]` attribute.
4. Update the Register and Manage/Index pages to include input fields for your properties
5. Create and apply a migration
6. Test registration and profile management

The sections below provide detailed step-by-step instructions for this process.

## Create a web app

You can create either a Razor Pages or MVC web app. Both use the same Identity Razor Pages UI.

### Razor Pages

* [Visual Studio](#tabpanel_5_visual-studio)
* [.NET CLI](#tabpanel_5_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project**. Name the project **WebApp1** if you want it to match the namespace of the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/add-user-data) code.
- Select **ASP.NET Core Web App** > **Next**
- Select **Authentication type** > **Individual Accounts** > **Create**
- Build and run the project.

```
dotnet new webapp -au Individual -o WebApp1
```

### MVC

* [Visual Studio](#tabpanel_6_visual-studio)
* [.NET CLI](#tabpanel_6_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project**. Name the project **WebApp1** if you want it to match the namespace of the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/add-user-data) code.
- Select **ASP.NET Core Web App (Model-View-Controller)** > **Next**
- Select **Authentication type** > **Individual Accounts** > **Create**
- Build and run the project.

```
dotnet new mvc -au Individual -o WebApp1
```

Note

Identity UI is implemented as a Razor Class Library. When you create an MVC project with authentication, the Identity pages are served from the Razor Class Library, which means your MVC project uses Razor Pages for Identity even though the rest of the project uses MVC controllers and views.

## Run the Identity scaffolder

* [Visual Studio](#tabpanel_7_visual-studio)
* [.NET CLI](#tabpanel_7_net-cli)

- From **Solution Explorer**, right-click on the project > **Add** > **New Scaffolded Item**.

- From the left pane of the **Add Scaffold** dialog, select **Identity** > **Add**.

- In the **Add Identity** dialog, the following options:

  * Select the existing layout file `~/Pages/Shared/_Layout.cshtml` for Razor Pages or `~/Views/Shared/_Layout.cshtml` for MVC

  * Select the following files to override:

    * **Account/Register**
    * **Account/Manage/Index**

  * Select the **+** button to create a new **Data context class**. Accept the type (**WebApp1.Models.WebApp1Context** if the project is named **WebApp1**).

  * Select the **+** button to create a new **User class**. Accept the type (**WebApp1User** if the project is named **WebApp1**) > **Add**.

- Select **Add**.

If you have not previously installed the ASP.NET Core scaffolder, install it now:

```
dotnet tool install -g dotnet-aspnet-codegenerator
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Add a package reference to [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design/) to the project (.csproj) file. Run the following command in the project directory:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet restore
```

Run the following command to list the Identity scaffolder options:

```
dotnet aspnet-codegenerator identity -h
```

In the project folder, run the Identity scaffolder:

```
dotnet aspnet-codegenerator identity -u WebApp1User -fi Account.Register;Account.Manage.Index
```

PowerShell uses semicolon as a command separator. When using PowerShell, escape the semi-colons in the file list or put the file list in double quotes.

Follow the instructions in [Migrations, UseAuthentication, and layout](scaffold-identity?view=aspnetcore-10.0#efm) to perform the following steps:

* Create a migration and update the database.

* Add `UseAuthentication` to [`Program.cs`](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/add-user-data/samples/10.x/SampleApp/Program.cs#L24)

* Add `<partial name="_LoginPartial" />` to the layout file.

* Test the app:

  * Register a user
  * Select the new user name (next to the **Logout** link). You might need to expand the window or select the navigation bar icon to show the user name and other links.
  * Select the **Personal Data** tab.
  * Select the **Download** button and examine the `PersonalData.json` file.
  * Test the **Delete** button, which deletes the logged on user.

## Add custom user data to the Identity DB

Update the `IdentityUser` derived class with custom properties. If you named the project WebApp1, the file is named `Areas/Identity/Data/WebApp1User.cs`. Update the file with the following code:

```
using Microsoft.AspNetCore.Identity;

namespace WebApp1.Areas.Identity.Data;

public class WebApp1User : IdentityUser
{
    [PersonalData]
    public string? Name { get; set; }
    [PersonalData]
    public DateTime DOB { get; set; }
}
```

Properties with the [PersonalData](/en-us/dotnet/api/microsoft.aspnetcore.identity.personaldataattribute) attribute are:

* Deleted when the `Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml` Razor Page calls `UserManager.Delete`.
* Included in the downloaded data by the `Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml` Razor Page.

### Update the `Account/Manage/Index.cshtml` page

Update the `InputModel` in `Areas/Identity/Pages/Account/Manage/Index.cshtml.cs` with the following highlighted code:

```
public class IndexModel : PageModel
{
    private readonly UserManager<WebApp1User> _userManager;
    private readonly SignInManager<WebApp1User> _signInManager;

    public IndexModel(
        UserManager<WebApp1User> userManager,
        SignInManager<WebApp1User> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    /// <summary>
    ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
    ///     directly from your code. This API may change or be removed in future releases.
    /// </summary>
    public string Username { get; set; }

    // Remaining API warnings omited.

    [TempData]
    public string StatusMessage { get; set; }

    [BindProperty]
    public InputModel Input { get; set; }

    public class InputModel
    {
        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Full name")]
        public string Name { get; set; }

        [Required]
        [Display(Name = "Birth Date")]
        [DataType(DataType.Date)]
        public DateTime DOB { get; set; }

        [Phone]
        [Display(Name = "Phone number")]
        public string PhoneNumber { get; set; }
    }

    private async Task LoadAsync(WebApp1User user)
    {
        var userName = await _userManager.GetUserNameAsync(user);
        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);

        Username = userName;

        Input = new InputModel
        {
            Name = user.Name,
            DOB = user.DOB,
            PhoneNumber = phoneNumber
        };
    }

    public async Task<IActionResult> OnGetAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        await LoadAsync(user);
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        if (!ModelState.IsValid)
        {
            await LoadAsync(user);
            return Page();
        }

        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
        if (Input.PhoneNumber != phoneNumber)
        {
            var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
            if (!setPhoneResult.Succeeded)
            {
                StatusMessage = "Unexpected error when trying to set phone number.";
                return RedirectToPage();
            }
        }

        if (Input.Name != user.Name)
        {
            user.Name = Input.Name;
        }

        if (Input.DOB != user.DOB)
        {
            user.DOB = Input.DOB;
        }

        await _userManager.UpdateAsync(user);
        await _signInManager.RefreshSignInAsync(user);
        StatusMessage = "Your profile has been updated";
        return RedirectToPage();
    }
}
```

Update the `Areas/Identity/Pages/Account/Manage/Index.cshtml` with the following highlighted markup:

```
@page
@model IndexModel
@{
    ViewData["Title"] = "Profile";
    ViewData["ActivePage"] = ManageNavPages.Index;
}

<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
    <div class="col-md-6">
        <form id="profile-form" method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-floating">
                <input asp-for="Username" class="form-control" disabled />
                <label asp-for="Username" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.Name" class="form-control" />
                <label asp-for="Input.Name" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.DOB" class="form-control" />
                <label asp-for="Input.DOB" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.PhoneNumber" class="form-control" />
                <label asp-for="Input.PhoneNumber" class="form-label"></label>
                <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
            </div>
            <button id="update-profile-button" type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
        </form>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

### Update the `Account/Register.cshtml` page

Update the `InputModel` in `Areas/Identity/Pages/Account/Register.cshtml.cs` with the following highlighted code:

```
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<WebApp1User> _signInManager;
        private readonly UserManager<WebApp1User> _userManager;
        private readonly IUserStore<WebApp1User> _userStore;
        private readonly IUserEmailStore<WebApp1User> _emailStore;
        private readonly ILogger<RegisterModel> _logger;
        private readonly IEmailSender _emailSender;

        public RegisterModel(
            UserManager<WebApp1User> userManager,
            IUserStore<WebApp1User> userStore,
            SignInManager<WebApp1User> signInManager,
            ILogger<RegisterModel> logger,
            IEmailSender emailSender)
        {
            _userManager = userManager;
            _userStore = userStore;
            _emailStore = GetEmailStore();
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
        }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        [BindProperty]
        public InputModel Input { get; set; }

        // Remaining API warnings omited.
        public string ReturnUrl { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public class InputModel
        {
            [Required]
            [DataType(DataType.Text)]
            [Display(Name = "Full name")]
            public string Name { get; set; }

            [Required]
            [Display(Name = "Birth Date")]
            [DataType(DataType.Date)]
            public DateTime DOB { get; set; }

            [Required]
            [EmailAddress]
            [Display(Name = "Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "Password")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "Confirm password")]
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
            public string ConfirmPassword { get; set; }
        }


        public async Task OnGetAsync(string returnUrl = null)
        {
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                var user = CreateUser();

                user.Name = Input.Name;
                user.DOB = Input.DOB;

                await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
                await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
                var result = await _userManager.CreateAsync(user, Input.Password);

                if (result.Succeeded)
                {
                    _logger.LogInformation("User created a new account with password.");

                    var userId = await _userManager.GetUserIdAsync(user);
                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                        protocol: Request.Scheme);

                    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    }
                    else
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    }
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }

        private WebApp1User CreateUser()
        {
            try
            {
                return Activator.CreateInstance<WebApp1User>();
            }
            catch
            {
                throw new InvalidOperationException($"Can't create an instance of '{nameof(WebApp1User)}'. " +
                    $"Ensure that '{nameof(WebApp1User)}' is not an abstract class and has a parameterless constructor, or alternatively " +
                    $"override the register page in /Areas/Identity/Pages/Account/Register.cshtml");
            }
        }

        private IUserEmailStore<WebApp1User> GetEmailStore()
        {
            if (!_userManager.SupportsUserEmail)
            {
                throw new NotSupportedException("The default UI requires a user store with email support.");
            }
            return (IUserEmailStore<WebApp1User>)_userStore;
        }
    }
}
```

Update the `Areas/Identity/Pages/Account/Register.cshtml` with the following highlighted markup:

```
@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h2>Create a new account.</h2>
            <hr />
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>

            <div class="form-floating">
                <input asp-for="Input.Name" class="form-control" />
                <label asp-for="Input.Name"></label>
                <span asp-validation-for="Input.Name" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.DOB" class="form-control" />
                <label asp-for="Input.DOB"></label>
                <span asp-validation-for="Input.DOB" class="text-danger"></span>
            </div>

            <div class="form-floating">
                <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
                <label asp-for="Input.Email"></label>
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.Password"></label>
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.ConfirmPassword"></label>
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
        </form>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <h3>Use another service to register.</h3>
            <hr />
            @{
                if ((Model.ExternalLogins?.Count ?? 0) == 0)
                {
                    <div>
                        <p>
                            There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
                            about setting up this ASP.NET application to support logging in via external services</a>.
                        </p>
                    </div>
                }
                else
                {
                    <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                        <div>
                            <p>
                                @foreach (var provider in Model.ExternalLogins!)
                                {
                                    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                                }
                            </p>
                        </div>
                    </form>
                }
            }
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

Build the project.

### Update the layout

See [Layout changes](scaffold-identity?view=aspnetcore-10.0#layout-changes) for instructions to add sign-in and sign-out links to every page.

### Add a migration for the custom user data

* [Visual Studio](#tabpanel_8_visual-studio)
* [.NET CLI](#tabpanel_8_net-cli)

In the Visual Studio **Package Manager Console**:

```
Add-Migration CustomUserData
Update-Database
```

```
dotnet ef migrations add CustomUserData
dotnet ef database update
```

## Test create, view, download, delete custom user data

Test the app:

* Register a new user.
* View the custom user data on the `/Identity/Account/Manage` page.
* Download and view the users personal data from the `/Identity/Account/Manage/PersonalData` page.

## Troubleshooting

If you encounter issues:

| Symptom                                         | Likely Cause                               | Solution                                                                             |
| ----------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------ |
| Custom fields don't appear in registration form | Forgot to update Register.cshtml           | Ensure you've scaffolded and modified `Areas/Identity/Pages/Account/Register.cshtml` |
| Custom data not saving                          | Forgot to add migration or update database | Run `dotnet ef migrations add CustomUserData` then `dotnet ef database update`       |
| Custom fields not in downloaded data            | Missing `[PersonalData]` attribute         | Add `[PersonalData]` attribute to properties in your user class                      |
| Build errors after scaffolding                  | Package version mismatch                   | Ensure all Identity packages match your target framework version                     |

[.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)

Note

The code samples and instructions for .NET 8 reference the 10.x sample folder. The code is identical between .NET 8 and .NET 10 for this scenario. Only the target framework and package versions differ in the project file.

## Quick start: Add two custom properties

For a simple scenario where you want to add just a couple of custom properties, such as `FirstName` and `LastName` to your Identity user:

1. Create a new ASP.NET Core project with Individual Accounts authentication
2. Scaffold Identity to override the Register and Manage/Index pages
3. Add your custom properties to the user class (e.g., `FirstName`, `LastName`) with the `[PersonalData]` attribute
4. Update the Register and Manage/Index pages to include input fields for your properties
5. Create and apply a migration
6. Test registration and profile management

The sections below provide detailed step-by-step instructions for this process.

## Create a web app

You can create either a Razor Pages or MVC web app. Both use the same Identity Razor Pages UI.

### Razor Pages

* [Visual Studio](#tabpanel_9_visual-studio)
* [.NET CLI](#tabpanel_9_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project**. Name the project **WebApp1** if you want it to match the namespace of the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/add-user-data) code.
- Select **ASP.NET Core Web App** > **Next**
- Select **Authentication type** > **Individual Accounts** > **Create**
- Build and run the project.

```
dotnet new webapp -au Individual -o WebApp1
```

### MVC

* [Visual Studio](#tabpanel_10_visual-studio)
* [.NET CLI](#tabpanel_10_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project**. Name the project **WebApp1** if you want it to match the namespace of the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/add-user-data) code.
- Select **ASP.NET Core Web App (Model-View-Controller)** > **Next**
- Select **Authentication type** > **Individual Accounts** > **Create**
- Build and run the project.

```
dotnet new mvc -au Individual -o WebApp1
```

Note

Identity UI is implemented as a Razor Class Library. When you create an MVC project with authentication, the Identity pages are served from the Razor Class Library, which means your MVC project uses Razor Pages for Identity even though the rest of the project uses MVC controllers and views.

## Run the Identity scaffolder

* [Visual Studio](#tabpanel_11_visual-studio)
* [.NET CLI](#tabpanel_11_net-cli)

- From **Solution Explorer**, right-click on the project > **Add** > **New Scaffolded Item**.

- From the left pane of the **Add Scaffold** dialog, select **Identity** > **Add**.

- In the **Add Identity** dialog, the following options:

  * Select the existing layout file `~/Pages/Shared/_Layout.cshtml` for Razor Pages or `~/Views/Shared/_Layout.cshtml` for MVC

  * Select the following files to override:

    * **Account/Register**
    * **Account/Manage/Index**

  * Select the **+** button to create a new **Data context class**. Accept the type (**WebApp1.Models.WebApp1Context** if the project is named **WebApp1**).

  * Select the **+** button to create a new **User class**. Accept the type (**WebApp1User** if the project is named **WebApp1**) > **Add**.

- Select **Add**.

If you have not previously installed the ASP.NET Core scaffolder, install it now:

```
dotnet tool install -g dotnet-aspnet-codegenerator
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Add a package reference to [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design/) to the project (.csproj) file. Run the following command in the project directory:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet restore
```

Run the following command to list the Identity scaffolder options:

```
dotnet aspnet-codegenerator identity -h
```

In the project folder, run the Identity scaffolder:

```
dotnet aspnet-codegenerator identity -u WebApp1User -fi Account.Register;Account.Manage.Index
```

PowerShell uses semicolon as a command separator. When using PowerShell, escape the semi-colons in the file list or put the file list in double quotes.

Follow the instructions in [Migrations, UseAuthentication, and layout](scaffold-identity?view=aspnetcore-10.0#efm) to perform the following steps:

* Create a migration and update the database.

* Add `UseAuthentication` to [`Program.cs`](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/add-user-data/samples/10.x/SampleApp/Program.cs#L24)

* Add `<partial name="_LoginPartial" />` to the layout file.

* Test the app:

  * Register a user
  * Select the new user name (next to the **Logout** link). You might need to expand the window or select the navigation bar icon to show the user name and other links.
  * Select the **Personal Data** tab.
  * Select the **Download** button and examine the `PersonalData.json` file.
  * Test the **Delete** button, which deletes the logged on user.

## Add custom user data to the Identity DB

Update the `IdentityUser` derived class with custom properties. If you named the project WebApp1, the file is named `Areas/Identity/Data/WebApp1User.cs`. Update the file with the following code:

```
using Microsoft.AspNetCore.Identity;

namespace WebApp1.Areas.Identity.Data;

public class WebApp1User : IdentityUser
{
    [PersonalData]
    public string? Name { get; set; }
    [PersonalData]
    public DateTime DOB { get; set; }
}
```

Properties with the [PersonalData](/en-us/dotnet/api/microsoft.aspnetcore.identity.personaldataattribute) attribute are:

* Deleted when the `Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml` Razor Page calls `UserManager.Delete`.
* Included in the downloaded data by the `Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml` Razor Page.

### Update the `Account/Manage/Index.cshtml` page

Update the `InputModel` in `Areas/Identity/Pages/Account/Manage/Index.cshtml.cs` with the following highlighted code:

```
public class IndexModel : PageModel
{
    private readonly UserManager<WebApp1User> _userManager;
    private readonly SignInManager<WebApp1User> _signInManager;

    public IndexModel(
        UserManager<WebApp1User> userManager,
        SignInManager<WebApp1User> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    /// <summary>
    ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
    ///     directly from your code. This API may change or be removed in future releases.
    /// </summary>
    public string Username { get; set; }

    // Remaining API warnings omited.

    [TempData]
    public string StatusMessage { get; set; }

    [BindProperty]
    public InputModel Input { get; set; }

    public class InputModel
    {
        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Full name")]
        public string Name { get; set; }

        [Required]
        [Display(Name = "Birth Date")]
        [DataType(DataType.Date)]
        public DateTime DOB { get; set; }

        [Phone]
        [Display(Name = "Phone number")]
        public string PhoneNumber { get; set; }
    }

    private async Task LoadAsync(WebApp1User user)
    {
        var userName = await _userManager.GetUserNameAsync(user);
        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);

        Username = userName;

        Input = new InputModel
        {
            Name = user.Name,
            DOB = user.DOB,
            PhoneNumber = phoneNumber
        };
    }

    public async Task<IActionResult> OnGetAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        await LoadAsync(user);
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        if (!ModelState.IsValid)
        {
            await LoadAsync(user);
            return Page();
        }

        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
        if (Input.PhoneNumber != phoneNumber)
        {
            var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
            if (!setPhoneResult.Succeeded)
            {
                StatusMessage = "Unexpected error when trying to set phone number.";
                return RedirectToPage();
            }
        }

        if (Input.Name != user.Name)
        {
            user.Name = Input.Name;
        }

        if (Input.DOB != user.DOB)
        {
            user.DOB = Input.DOB;
        }

        await _userManager.UpdateAsync(user);
        await _signInManager.RefreshSignInAsync(user);
        StatusMessage = "Your profile has been updated";
        return RedirectToPage();
    }
}
```

Update the `Areas/Identity/Pages/Account/Manage/Index.cshtml` with the following highlighted markup:

```
@page
@model IndexModel
@{
    ViewData["Title"] = "Profile";
    ViewData["ActivePage"] = ManageNavPages.Index;
}

<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
    <div class="col-md-6">
        <form id="profile-form" method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-floating">
                <input asp-for="Username" class="form-control" disabled />
                <label asp-for="Username" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.Name" class="form-control" />
                <label asp-for="Input.Name" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.DOB" class="form-control" />
                <label asp-for="Input.DOB" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.PhoneNumber" class="form-control" />
                <label asp-for="Input.PhoneNumber" class="form-label"></label>
                <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
            </div>
            <button id="update-profile-button" type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
        </form>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

### Update the `Account/Register.cshtml` page

Update the `InputModel` in `Areas/Identity/Pages/Account/Register.cshtml.cs` with the following highlighted code:

```
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<WebApp1User> _signInManager;
        private readonly UserManager<WebApp1User> _userManager;
        private readonly IUserStore<WebApp1User> _userStore;
        private readonly IUserEmailStore<WebApp1User> _emailStore;
        private readonly ILogger<RegisterModel> _logger;
        private readonly IEmailSender _emailSender;

        public RegisterModel(
            UserManager<WebApp1User> userManager,
            IUserStore<WebApp1User> userStore,
            SignInManager<WebApp1User> signInManager,
            ILogger<RegisterModel> logger,
            IEmailSender emailSender)
        {
            _userManager = userManager;
            _userStore = userStore;
            _emailStore = GetEmailStore();
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
        }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        [BindProperty]
        public InputModel Input { get; set; }

        // Remaining API warnings omited.
        public string ReturnUrl { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public class InputModel
        {
            [Required]
            [DataType(DataType.Text)]
            [Display(Name = "Full name")]
            public string Name { get; set; }

            [Required]
            [Display(Name = "Birth Date")]
            [DataType(DataType.Date)]
            public DateTime DOB { get; set; }

            [Required]
            [EmailAddress]
            [Display(Name = "Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "Password")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "Confirm password")]
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
            public string ConfirmPassword { get; set; }
        }


        public async Task OnGetAsync(string returnUrl = null)
        {
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                var user = CreateUser();

                user.Name = Input.Name;
                user.DOB = Input.DOB;

                await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
                await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
                var result = await _userManager.CreateAsync(user, Input.Password);

                if (result.Succeeded)
                {
                    _logger.LogInformation("User created a new account with password.");

                    var userId = await _userManager.GetUserIdAsync(user);
                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                        protocol: Request.Scheme);

                    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    }
                    else
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    }
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }

        private WebApp1User CreateUser()
        {
            try
            {
                return Activator.CreateInstance<WebApp1User>();
            }
            catch
            {
                throw new InvalidOperationException($"Can't create an instance of '{nameof(WebApp1User)}'. " +
                    $"Ensure that '{nameof(WebApp1User)}' is not an abstract class and has a parameterless constructor, or alternatively " +
                    $"override the register page in /Areas/Identity/Pages/Account/Register.cshtml");
            }
        }

        private IUserEmailStore<WebApp1User> GetEmailStore()
        {
            if (!_userManager.SupportsUserEmail)
            {
                throw new NotSupportedException("The default UI requires a user store with email support.");
            }
            return (IUserEmailStore<WebApp1User>)_userStore;
        }
    }
}
```

Update the `Areas/Identity/Pages/Account/Register.cshtml` with the following highlighted markup:

```
@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h2>Create a new account.</h2>
            <hr />
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>

            <div class="form-floating">
                <input asp-for="Input.Name" class="form-control" />
                <label asp-for="Input.Name"></label>
                <span asp-validation-for="Input.Name" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.DOB" class="form-control" />
                <label asp-for="Input.DOB"></label>
                <span asp-validation-for="Input.DOB" class="text-danger"></span>
            </div>

            <div class="form-floating">
                <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
                <label asp-for="Input.Email"></label>
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.Password"></label>
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.ConfirmPassword"></label>
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
        </form>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <h3>Use another service to register.</h3>
            <hr />
            @{
                if ((Model.ExternalLogins?.Count ?? 0) == 0)
                {
                    <div>
                        <p>
                            There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
                            about setting up this ASP.NET application to support logging in via external services</a>.
                        </p>
                    </div>
                }
                else
                {
                    <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                        <div>
                            <p>
                                @foreach (var provider in Model.ExternalLogins!)
                                {
                                    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                                }
                            </p>
                        </div>
                    </form>
                }
            }
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

Build the project.

### Update the layout

See [Layout changes](scaffold-identity?view=aspnetcore-10.0#layout-changes) for instructions to add sign-in and sign-out links to every page.

### Add a migration for the custom user data

* [Visual Studio](#tabpanel_12_visual-studio)
* [.NET CLI](#tabpanel_12_net-cli)

In the Visual Studio **Package Manager Console**:

```
Add-Migration CustomUserData
Update-Database
```

```
dotnet ef migrations add CustomUserData
dotnet ef database update
```

## Test create, view, download, delete custom user data

Test the app:

* Register a new user.
* View the custom user data on the `/Identity/Account/Manage` page.
* Download and view the users personal data from the `/Identity/Account/Manage/PersonalData` page.

## Troubleshooting

If you encounter issues:

| Symptom                                         | Likely Cause                               | Solution                                                                             |
| ----------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------ |
| Custom fields don't appear in registration form | Forgot to update Register.cshtml           | Ensure you've scaffolded and modified `Areas/Identity/Pages/Account/Register.cshtml` |
| Custom data not saving                          | Forgot to add migration or update database | Run `dotnet ef migrations add CustomUserData` then `dotnet ef database update`       |
| Custom fields not in downloaded data            | Missing `[PersonalData]` attribute         | Add `[PersonalData]` attribute to properties in your user class                      |
| Build errors after scaffolding                  | Package version mismatch                   | Ensure all Identity packages match your target framework version                     |

[.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

## Create a Razor web app

* [Visual Studio](#tabpanel_13_visual-studio)
* [.NET CLI](#tabpanel_13_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project**. Name the project **WebApp1** if you want it to match the namespace of the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/authentication/add-user-data) code.
- Select **ASP.NET Core Web Application** > **OK**
- Select **Web Application** > **OK**
- Build and run the project.

```
dotnet new webapp -o WebApp1
```

## Run the Identity scaffolder

* [Visual Studio](#tabpanel_14_visual-studio)
* [.NET CLI](#tabpanel_14_net-cli)

- From **Solution Explorer**, right-click on the project > **Add** > **New Scaffolded Item**.

- From the left pane of the **Add Scaffold** dialog, select **Identity** > **Add**.

- In the **Add Identity** dialog, the following options:

  * Select the existing layout file `~/Pages/Shared/_Layout.cshtml`

  * Select the following files to override:

    * **Account/Register**
    * **Account/Manage/Index**

  * Select the **+** button to create a new **Data context class**. Accept the type (**WebApp1.Models.WebApp1Context** if the project is named **WebApp1**).

  * Select the **+** button to create a new **User class**. Accept the type (**WebApp1User** if the project is named **WebApp1**) > **Add**.

- Select **Add**.

If you have not previously installed the ASP.NET Core scaffolder, install it now:

```
dotnet tool install -g dotnet-aspnet-codegenerator
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Add a package reference to [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design/) to the project (.csproj) file. Run the following command in the project directory:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet restore
```

Run the following command to list the Identity scaffolder options:

```
dotnet aspnet-codegenerator identity -h
```

In the project folder, run the Identity scaffolder:

```
dotnet aspnet-codegenerator identity -u WebApp1User -fi Account.Register;Account.Manage.Index
```

PowerShell uses semicolon as a command separator. When using PowerShell, escape the semi-colons in the file list or put the file list in double quotes.

Follow the instructions in [Migrations, UseAuthentication, and layout](scaffold-identity?view=aspnetcore-10.0#efm) to perform the following steps:

* Create a migration and update the database.

* Add `UseAuthentication` to [`Program.cs`](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/add-user-data/samples/6.x/SampleApp/Program.cs#L24)

* Add `<partial name="_LoginPartial" />` to the layout file.

* Test the app:

  * Register a user
  * Select the new user name (next to the **Logout** link). You might need to expand the window or select the navigation bar icon to show the user name and other links.
  * Select the **Personal Data** tab.
  * Select the **Download** button and examine the `PersonalData.json` file.
  * Test the **Delete** button, which deletes the logged on user.

## Add custom user data to the Identity DB

Update the `IdentityUser` derived class with custom properties. If you named the project WebApp1, the file is named `Areas/Identity/Data/WebApp1User.cs`. Update the file with the following code:

```
using Microsoft.AspNetCore.Identity;

namespace WebApp1.Areas.Identity.Data;

public class WebApp1User : IdentityUser
{
    [PersonalData]
    public string? Name { get; set; }
    [PersonalData]
    public DateTime DOB { get; set; }
}
```

Properties with the [PersonalData](/en-us/dotnet/api/microsoft.aspnetcore.identity.personaldataattribute) attribute are:

* Deleted when the `Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml` Razor Page calls `UserManager.Delete`.
* Included in the downloaded data by the `Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml` Razor Page.

### Update the `Account/Manage/Index.cshtml` page

Update the `InputModel` in `Areas/Identity/Pages/Account/Manage/Index.cshtml.cs` with the following highlighted code:

```
public class IndexModel : PageModel
{
    private readonly UserManager<WebApp1User> _userManager;
    private readonly SignInManager<WebApp1User> _signInManager;

    public IndexModel(
        UserManager<WebApp1User> userManager,
        SignInManager<WebApp1User> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    /// <summary>
    ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
    ///     directly from your code. This API may change or be removed in future releases.
    /// </summary>
    public string Username { get; set; }

    // Remaining API warnings ommited.

    [TempData]
    public string StatusMessage { get; set; }

    [BindProperty]
    public InputModel Input { get; set; }

    public class InputModel
    {
        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Full name")]
        public string Name { get; set; }

        [Required]
        [Display(Name = "Birth Date")]
        [DataType(DataType.Date)]
        public DateTime DOB { get; set; }

        [Phone]
        [Display(Name = "Phone number")]
        public string PhoneNumber { get; set; }
    }

    private async Task LoadAsync(WebApp1User user)
    {
        var userName = await _userManager.GetUserNameAsync(user);
        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);

        Username = userName;

        Input = new InputModel
        {
            Name = user.Name,
            DOB = user.DOB,
            PhoneNumber = phoneNumber
        };
    }

    public async Task<IActionResult> OnGetAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        await LoadAsync(user);
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        if (!ModelState.IsValid)
        {
            await LoadAsync(user);
            return Page();
        }

        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
        if (Input.PhoneNumber != phoneNumber)
        {
            var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
            if (!setPhoneResult.Succeeded)
            {
                StatusMessage = "Unexpected error when trying to set phone number.";
                return RedirectToPage();
            }
        }

        if (Input.Name != user.Name)
        {
            user.Name = Input.Name;
        }

        if (Input.DOB != user.DOB)
        {
            user.DOB = Input.DOB;
        }

        await _userManager.UpdateAsync(user);
        await _signInManager.RefreshSignInAsync(user);
        StatusMessage = "Your profile has been updated";
        return RedirectToPage();
    }
}
```

Update the `Areas/Identity/Pages/Account/Manage/Index.cshtml` with the following highlighted markup:

```
@page
@model IndexModel
@{
    ViewData["Title"] = "Profile";
    ViewData["ActivePage"] = ManageNavPages.Index;
}

<h3>@ViewData["Title"]</h3>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
    <div class="col-md-6">
        <form id="profile-form" method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-floating">
                <input asp-for="Username" class="form-control" disabled />
                <label asp-for="Username" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.Name" class="form-control" />
                <label asp-for="Input.Name" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.DOB" class="form-control" />
                <label asp-for="Input.DOB" class="form-label"></label>
            </div>
            <div class="form-floating">
                <input asp-for="Input.PhoneNumber" class="form-control" />
                <label asp-for="Input.PhoneNumber" class="form-label"></label>
                <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
            </div>
            <button id="update-profile-button" type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
        </form>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

### Update the `Account/Register.cshtml` page

Update the `InputModel` in `Areas/Identity/Pages/Account/Register.cshtml.cs` with the following highlighted code:

```
    public class RegisterModel : PageModel
    {
        private readonly SignInManager<WebApp1User> _signInManager;
        private readonly UserManager<WebApp1User> _userManager;
        private readonly IUserStore<WebApp1User> _userStore;
        private readonly IUserEmailStore<WebApp1User> _emailStore;
        private readonly ILogger<RegisterModel> _logger;
        private readonly IEmailSender _emailSender;

        public RegisterModel(
            UserManager<WebApp1User> userManager,
            IUserStore<WebApp1User> userStore,
            SignInManager<WebApp1User> signInManager,
            ILogger<RegisterModel> logger,
            IEmailSender emailSender)
        {
            _userManager = userManager;
            _userStore = userStore;
            _emailStore = GetEmailStore();
            _signInManager = signInManager;
            _logger = logger;
            _emailSender = emailSender;
        }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        [BindProperty]
        public InputModel Input { get; set; }

        // Remaining API warnings ommited.
        public string ReturnUrl { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public class InputModel
        {
            [Required]
            [DataType(DataType.Text)]
            [Display(Name = "Full name")]
            public string Name { get; set; }

            [Required]
            [Display(Name = "Birth Date")]
            [DataType(DataType.Date)]
            public DateTime DOB { get; set; }

            [Required]
            [EmailAddress]
            [Display(Name = "Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "Password")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "Confirm password")]
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
            public string ConfirmPassword { get; set; }
        }


        public async Task OnGetAsync(string returnUrl = null)
        {
            ReturnUrl = returnUrl;
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl ??= Url.Content("~/");
            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            if (ModelState.IsValid)
            {
                var user = CreateUser();

                user.Name = Input.Name;
                user.DOB = Input.DOB;

                await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
                await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
                var result = await _userManager.CreateAsync(user, Input.Password);

                if (result.Succeeded)
                {
                    _logger.LogInformation("User created a new account with password.");

                    var userId = await _userManager.GetUserIdAsync(user);
                    var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                    var callbackUrl = Url.Page(
                        "/Account/ConfirmEmail",
                        pageHandler: null,
                        values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                        protocol: Request.Scheme);

                    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                    if (_userManager.Options.SignIn.RequireConfirmedAccount)
                    {
                        return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
                    }
                    else
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        return LocalRedirect(returnUrl);
                    }
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }

        private WebApp1User CreateUser()
        {
            try
            {
                return Activator.CreateInstance<WebApp1User>();
            }
            catch
            {
                throw new InvalidOperationException($"Can't create an instance of '{nameof(WebApp1User)}'. " +
                    $"Ensure that '{nameof(WebApp1User)}' is not an abstract class and has a parameterless constructor, or alternatively " +
                    $"override the register page in /Areas/Identity/Pages/Account/Register.cshtml");
            }
        }

        private IUserEmailStore<WebApp1User> GetEmailStore()
        {
            if (!_userManager.SupportsUserEmail)
            {
                throw new NotSupportedException("The default UI requires a user store with email support.");
            }
            return (IUserEmailStore<WebApp1User>)_userStore;
        }
    }
}
```

Update the `Areas/Identity/Pages/Account/Register.cshtml` with the following highlighted markup:

```
@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h2>Create a new account.</h2>
            <hr />
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>

            <div class="form-floating">
                <input asp-for="Input.Name" class="form-control" />
                <label asp-for="Input.Name"></label>
                <span asp-validation-for="Input.Name" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.DOB" class="form-control" />
                <label asp-for="Input.DOB"></label>
                <span asp-validation-for="Input.DOB" class="text-danger"></span>
            </div>

            <div class="form-floating">
                <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" />
                <label asp-for="Input.Email"></label>
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.Password"></label>
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-floating">
                <input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" />
                <label asp-for="Input.ConfirmPassword"></label>
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
        </form>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <h3>Use another service to register.</h3>
            <hr />
            @{
                if ((Model.ExternalLogins?.Count ?? 0) == 0)
                {
                    <div>
                        <p>
                            There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
                            about setting up this ASP.NET application to support logging in via external services</a>.
                        </p>
                    </div>
                }
                else
                {
                    <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                        <div>
                            <p>
                                @foreach (var provider in Model.ExternalLogins!)
                                {
                                    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                                }
                            </p>
                        </div>
                    </form>
                }
            }
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

Build the project.

### Update the layout

See [Layout changes](scaffold-identity?view=aspnetcore-10.0#layout-changes) for instructions to add sign-in and sign-out links to every page.

### Add a migration for the custom user data

* [Visual Studio](#tabpanel_15_visual-studio)
* [.NET CLI](#tabpanel_15_net-cli)

In the Visual Studio **Package Manager Console**:

```
Add-Migration CustomUserData
Update-Database
```

```
dotnet ef migrations add CustomUserData
dotnet ef database update
```

## Test create, view, download, delete custom user data

Test the app:

* Register a new user.
* View the custom user data on the `/Identity/Account/Manage` page.
* Download and view the users personal data from the `/Identity/Account/Manage/PersonalData` page.

[.NET Core 3.0 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.0)

## Create a Razor web app

* [Visual Studio](#tabpanel_16_visual-studio)
* [.NET CLI](#tabpanel_16_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project**. Name the project **WebApp1** if you want it to match the namespace of the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/authentication/add-user-data) code.
- Select **ASP.NET Core Web Application** > **OK**
- Select **Web Application** > **OK**
- Build and run the project.

```
dotnet new webapp -o WebApp1
```

## Run the Identity scaffolder

* [Visual Studio](#tabpanel_17_visual-studio)
* [.NET CLI](#tabpanel_17_net-cli)

- From **Solution Explorer**, right-click on the project > **Add** > **New Scaffolded Item**.

- From the left pane of the **Add Scaffold** dialog, select **Identity** > **Add**.

- In the **Add Identity** dialog, the following options:

  * Select the existing layout file `~/Pages/Shared/_Layout.cshtml`

  * Select the following files to override:

    * **Account/Register**
    * **Account/Manage/Index**

  * Select the **+** button to create a new **Data context class**. Accept the type (**WebApp1.Models.WebApp1Context** if the project is named **WebApp1**).

  * Select the **+** button to create a new **User class**. Accept the type (**WebApp1User** if the project is named **WebApp1**) > **Add**.

- Select **Add**.

If you have not previously installed the ASP.NET Core scaffolder, install it now:

```
dotnet tool install -g dotnet-aspnet-codegenerator
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Add a package reference to [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design/) to the project (.csproj) file. Run the following command in the project directory:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet restore
```

Run the following command to list the Identity scaffolder options:

```
dotnet aspnet-codegenerator identity -h
```

In the project folder, run the Identity scaffolder:

```
dotnet aspnet-codegenerator identity -u WebApp1User -fi Account.Register;Account.Manage.Index
```

PowerShell uses semicolon as a command separator. When using PowerShell, escape the semi-colons in the file list or put the file list in double quotes.

Follow the instructions in [Migrations, UseAuthentication, and layout](scaffold-identity?view=aspnetcore-10.0#efm) to perform the following steps:

* Create a migration and update the database.

* Add `UseAuthentication` to `Startup.Configure`.

* Add `<partial name="_LoginPartial" />` to the layout file.

* Test the app:

  * Register a user
  * Select the new user name (next to the **Logout** link). You might need to expand the window or select the navigation bar icon to show the user name and other links.
  * Select the **Personal Data** tab.
  * Select the **Download** button and examine the `PersonalData.json` file.
  * Test the **Delete** button, which deletes the logged on user.

## Add custom user data to the Identity DB

Update the `IdentityUser` derived class with custom properties. If you named the project WebApp1, the file is named `Areas/Identity/Data/WebApp1User.cs`. Update the file with the following code:

```
using System;
using Microsoft.AspNetCore.Identity;

namespace WebApp1.Areas.Identity.Data
{
    public class WebApp1User : IdentityUser
    {
        [PersonalData]
        public string Name { get; set; }
        [PersonalData]
        public DateTime DOB { get; set; }
    }
}
```

Properties with the [PersonalData](/en-us/dotnet/api/microsoft.aspnetcore.identity.personaldataattribute) attribute are:

* Deleted when the `Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml` Razor Page calls `UserManager.Delete`.
* Included in the downloaded data by the `Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml` Razor Page.

### Update the Account/Manage/Index.cshtml page

Update the `InputModel` in `Areas/Identity/Pages/Account/Manage/Index.cshtml.cs` with the following highlighted code:

```
public partial class IndexModel : PageModel
{
    private readonly UserManager<WebApp1User> _userManager;
    private readonly SignInManager<WebApp1User> _signInManager;

    public IndexModel(
        UserManager<WebApp1User> userManager,
        SignInManager<WebApp1User> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    public string Username { get; set; }

    [TempData]
    public string StatusMessage { get; set; }

    [BindProperty]
    public InputModel Input { get; set; }

    public class InputModel
    {
        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Full name")]
        public string Name { get; set; }

        [Required]
        [Display(Name = "Birth Date")]
        [DataType(DataType.Date)]
        public DateTime DOB { get; set; }

        [Phone]
        [Display(Name = "Phone number")]
        public string PhoneNumber { get; set; }
    }

    private async Task LoadAsync(WebApp1User user)
    {
        var userName = await _userManager.GetUserNameAsync(user);
        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);

        Username = userName;

        Input = new InputModel
        {
            Name = user.Name,
            DOB = user.DOB,
            PhoneNumber = phoneNumber
        };
    }

    public async Task<IActionResult> OnGetAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound(
                $"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        await LoadAsync(user);
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound(
                $"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        if (!ModelState.IsValid)
        {
            await LoadAsync(user);
            return Page();
        }

        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
        if (Input.PhoneNumber != phoneNumber)
        {
            var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, 
                Input.PhoneNumber);

            if (!setPhoneResult.Succeeded)
            {
                var userId = await _userManager.GetUserIdAsync(user);
                throw new InvalidOperationException(
                    $"Unexpected error occurred setting phone number for user with ID '{userId}'.");
            }
        }
        
        if (Input.Name != user.Name)
        {
            user.Name = Input.Name;
        }

        if (Input.DOB != user.DOB)
        {
            user.DOB = Input.DOB;
        }

        await _userManager.UpdateAsync(user);

        await _signInManager.RefreshSignInAsync(user);
        StatusMessage = "Your profile has been updated";
        return RedirectToPage();
    }
}
```

Update the `Areas/Identity/Pages/Account/Manage/Index.cshtml` with the following highlighted markup:

```
@page
@model IndexModel
@{
    ViewData["Title"] = "Profile";
    ViewData["ActivePage"] = ManageNavPages.Index;
}

<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" model="Model.StatusMessage" />
<div class="row">
    <div class="col-md-6">
        <form id="profile-form" method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Username"></label>
                <input asp-for="Username" class="form-control" disabled />
            </div>
            <div class="form-group">
                <label asp-for="Input.Name"></label>
                <input asp-for="Input.Name" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="Input.DOB"></label>
                <input asp-for="Input.DOB" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="Input.PhoneNumber"></label>
                <input asp-for="Input.PhoneNumber" class="form-control" />
                <span asp-validation-for="Input.PhoneNumber" 
                    class="text-danger"></span>
            </div>
            <button id="update-profile-button" type="submit" 
                class="btn btn-primary">Save</button>
        </form>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

### Update the Account/Register.cshtml page

Update the `InputModel` in `Areas/Identity/Pages/Account/Register.cshtml.cs` with the following highlighted code:

```
[AllowAnonymous]
public class RegisterModel : PageModel
{
    private readonly SignInManager<WebApp1User> _signInManager;
    private readonly UserManager<WebApp1User> _userManager;
    private readonly ILogger<RegisterModel> _logger;
    private readonly IEmailSender _emailSender;

    public RegisterModel(
        UserManager<WebApp1User> userManager,
        SignInManager<WebApp1User> signInManager,
        ILogger<RegisterModel> logger,
        IEmailSender emailSender)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _logger = logger;
        _emailSender = emailSender;
    }

    [BindProperty]
    public InputModel Input { get; set; }

    public string ReturnUrl { get; set; }

    public IList<AuthenticationScheme> ExternalLogins { get; set; }

    public class InputModel
    {
        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Full name")]
        public string Name { get; set; }

        [Required]
        [Display(Name = "Birth Date")]
        [DataType(DataType.Date)]
        public DateTime DOB { get; set; }

        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }

    public async Task OnGetAsync(string returnUrl = null)
    {
        ReturnUrl = returnUrl;
        ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
    }

    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
    {
        returnUrl = returnUrl ?? Url.Content("~/");
        ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
        if (ModelState.IsValid)
        {
            var user = new WebApp1User {
                Name = Input.Name,
                DOB = Input.DOB,
                UserName = Input.Email, 
                Email = Input.Email 
            };
            var result = await _userManager.CreateAsync(user, Input.Password);
            if (result.Succeeded)
            {
                _logger.LogInformation("User created a new account with password.");

                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = user.Id, code = code },
                    protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, 
                    "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                if (_userManager.Options.SignIn.RequireConfirmedAccount)
                {
                    return RedirectToPage("RegisterConfirmation", new { email = Input.Email });
                }
                else
                {
                    await _signInManager.SignInAsync(user, isPersistent: false);
                    return LocalRedirect(returnUrl);
                }
            }
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
        }

        // If we got this far, something failed, redisplay form
        return Page();
    }
}
```

Update the `Areas/Identity/Pages/Account/Register.cshtml` with the following highlighted markup:

```
@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h4>Create a new account.</h4>
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>

            <div class="form-group">
                <label asp-for="Input.Name"></label>
                <input asp-for="Input.Name" class="form-control" />
                <span asp-validation-for="Input.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.DOB"></label>
                <input asp-for="Input.DOB" class="form-control" />
                <span asp-validation-for="Input.DOB" class="text-danger"></span>
            </div>
            
            <div class="form-group">
                <label asp-for="Input.Email"></label>
                <input asp-for="Input.Email" class="form-control" />
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Password"></label>
                <input asp-for="Input.Password" class="form-control" />
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.ConfirmPassword"></label>
                <input asp-for="Input.ConfirmPassword" class="form-control" />
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Register</button>
        </form>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <h4>Use another service to register.</h4>
            <hr />
            @{
                if ((Model.ExternalLogins?.Count ?? 0) == 0)
                {
                    <div>
                        <p>
                            There are no external authentication services configured. See 
                             <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                            for details on setting up this ASP.NET application to support 
                            logging in via external services.
                        </p>
                    </div>
                }
                else
                {
                    <form id="external-account" asp-page="./ExternalLogin" 
                        asp-route-returnUrl="@Model.ReturnUrl" method="post" 
                        class="form-horizontal">
                        <div>
                            <p>
                                @foreach (var provider in Model.ExternalLogins)
                                {
                                    <button type="submit" class="btn btn-primary" name="provider" 
                                        value="@provider.Name" 
                                        title="Log in using your @provider.DisplayName account">
                                            @provider.DisplayName</button>
                                }
                            </p>
                        </div>
                    </form>
                }
            }
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

Build the project.

### Add a migration for the custom user data

* [Visual Studio](#tabpanel_18_visual-studio)
* [.NET CLI](#tabpanel_18_net-cli)

In the Visual Studio **Package Manager Console**:

```
Add-Migration CustomUserData
Update-Database
```

```
dotnet ef migrations add CustomUserData
dotnet ef database update
```

## Test create, view, download, delete custom user data

Test the app:

* Register a new user.
* View the custom user data on the `/Identity/Account/Manage` page.
* Download and view the users personal data from the `/Identity/Account/Manage/PersonalData` page.

## Add claims to Identity using `IUserClaimsPrincipalFactory<ApplicationUser>`

Note

This section isn't an extension of the previous tutorial. To apply the following steps to the app built using the tutorial, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/18797).

Additional claims can be added to ASP.NET Core Identity by using the `IUserClaimsPrincipalFactory<T>` interface. This class can be added to the app in the `Startup.ConfigureServices` method. Add the custom implementation of the class as follows:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, 
        AdditionalUserClaimsPrincipalFactory>();
```

The demo code uses the `ApplicationUser` class. This class adds an `IsAdmin` property which is used to add the additional claim.

```
public class ApplicationUser : IdentityUser
{
    public bool IsAdmin { get; set; }
}
```

The `AdditionalUserClaimsPrincipalFactory` implements the `UserClaimsPrincipalFactory` interface. A new role claim is added to the `ClaimsPrincipal`.

```
public class AdditionalUserClaimsPrincipalFactory 
        : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
    public AdditionalUserClaimsPrincipalFactory( 
        UserManager<ApplicationUser> userManager,
        RoleManager<IdentityRole> roleManager, 
        IOptions<IdentityOptions> optionsAccessor) 
        : base(userManager, roleManager, optionsAccessor)
    {}

    public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
    {
        var principal = await base.CreateAsync(user);
        var identity = (ClaimsIdentity)principal.Identity;

        var claims = new List<Claim>();
        if (user.IsAdmin)
        {
            claims.Add(new Claim(JwtClaimTypes.Role, "admin"));
        }
        else
        {
            claims.Add(new Claim(JwtClaimTypes.Role, "user"));
        }

        identity.AddClaims(claims);
        return principal;
    }
}
```

The additional claim can then be used in the app. In a Razor Page, the `IAuthorizationService` instance can be used to access the claim value.

```
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

@if ((await AuthorizationService.AuthorizeAsync(User, "IsAdmin")).Succeeded)
{
    <ul class="mr-auto navbar-nav">
        <li class="nav-item">
            <a class="nav-link" asp-controller="Admin" asp-action="Index">ADMIN</a>
        </li>
    </ul>
}
```

[.NET Core 2.2 or later SDK](https://dotnet.microsoft.com/download/dotnet-core)

## Create a Razor web app

* [Visual Studio](#tabpanel_19_visual-studio)
* [.NET CLI](#tabpanel_19_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project**. Name the project **WebApp1** if you want it to match the namespace of the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/authentication/add-user-data) code.
- Select **ASP.NET Core Web Application** > **OK**
- Select **ASP.NET Core 2.2** in the dropdown
- Select **Web Application** > **OK**
- Build and run the project.

```
dotnet new webapp -o WebApp1
```

## Run the Identity scaffolder

* [Visual Studio](#tabpanel_20_visual-studio)
* [.NET CLI](#tabpanel_20_net-cli)

- From **Solution Explorer**, right-click on the project > **Add** > **New Scaffolded Item**.

- From the left pane of the **Add Scaffold** dialog, select **Identity** > **Add**.

- In the **Add Identity** dialog, the following options:

  * Select the existing layout file `~/Pages/Shared/_Layout.cshtml`

  * Select the following files to override:

    * **Account/Register**
    * **Account/Manage/Index**

  * Select the **+** button to create a new **Data context class**. Accept the type (**WebApp1.Models.WebApp1Context** if the project is named **WebApp1**).

  * Select the **+** button to create a new **User class**. Accept the type (**WebApp1User** if the project is named **WebApp1**) > **Add**.

- Select **Add**.

If you have not previously installed the ASP.NET Core scaffolder, install it now:

```
dotnet tool install -g dotnet-aspnet-codegenerator
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Add a package reference to [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design/) to the project (.csproj) file. Run the following command in the project directory:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet restore
```

Run the following command to list the Identity scaffolder options:

```
dotnet aspnet-codegenerator identity -h
```

In the project folder, run the Identity scaffolder:

```
dotnet aspnet-codegenerator identity -u WebApp1User -fi Account.Register;Account.Manage.Index
```

PowerShell uses semicolon as a command separator. When using PowerShell, escape the semi-colons in the file list or put the file list in double quotes.

Follow the instructions in [Migrations, UseAuthentication, and layout](scaffold-identity?view=aspnetcore-10.0#efm) to perform the following steps:

* Create a migration and update the database.

* Add `UseAuthentication` to `Startup.Configure`.

* Add `<partial name="_LoginPartial" />` to the layout file.

* Test the app:

  * Register a user
  * Select the new user name (next to the **Logout** link). You might need to expand the window or select the navigation bar icon to show the user name and other links.
  * Select the **Personal Data** tab.
  * Select the **Download** button and examine the `PersonalData.json` file.
  * Test the **Delete** button, which deletes the logged on user.

## Add custom user data to the Identity DB

Update the `IdentityUser` derived class with custom properties. If you named the project WebApp1, the file is named `Areas/Identity/Data/WebApp1User.cs`. Update the file with the following code:

```
using Microsoft.AspNetCore.Identity;
using System;

namespace WebApp1.Areas.Identity.Data
{
    public class WebApp1User : IdentityUser
    {
        [PersonalData]
        public string Name { get; set; }
        [PersonalData]
        public DateTime DOB { get; set; }
    }
}
```

Properties with the [PersonalData](/en-us/dotnet/api/microsoft.aspnetcore.identity.personaldataattribute) attribute are:

* Deleted when the `Areas/Identity/Pages/Account/Manage/DeletePersonalData.cshtml` Razor Page calls `UserManager.Delete`.
* Included in the downloaded data by the `Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml` Razor Page.

### Update the Account/Manage/Index.cshtml page

Update the `InputModel` in `Areas/Identity/Pages/Account/Manage/Index.cshtml.cs` with the following highlighted code:

```
public partial class IndexModel : PageModel
{
    private readonly UserManager<WebApp1User> _userManager;
    private readonly SignInManager<WebApp1User> _signInManager;
    private readonly IEmailSender _emailSender;

    public IndexModel(
        UserManager<WebApp1User> userManager,
        SignInManager<WebApp1User> signInManager,
        IEmailSender emailSender)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _emailSender = emailSender;
    }

    public string Username { get; set; }
    public bool IsEmailConfirmed { get; set; }

    [TempData]
    public string StatusMessage { get; set; }

    [BindProperty]
    public InputModel Input { get; set; }

    public class InputModel
    {
        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Full name")]
        public string Name { get; set; }

        [Required]
        [Display(Name = "Birth Date")]
        [DataType(DataType.Date)]
        public DateTime DOB { get; set; }

        [Required]
        [EmailAddress]
        public string Email { get; set; }

        [Phone]
        [Display(Name = "Phone number")]
        public string PhoneNumber { get; set; }
    }

    public async Task<IActionResult> OnGetAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        var userName = await _userManager.GetUserNameAsync(user);
        var email = await _userManager.GetEmailAsync(user);
        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);

        Username = userName;

        Input = new InputModel
        {
            Name = user.Name,
            DOB = user.DOB,
            Email = email,
            PhoneNumber = phoneNumber
        };

        IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }

        var email = await _userManager.GetEmailAsync(user);
        if (Input.Email != email)
        {
            var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
            if (!setEmailResult.Succeeded)
            {
                var userId = await _userManager.GetUserIdAsync(user);
                throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'.");
            }
        }

        if (Input.Name != user.Name)
        {
            user.Name = Input.Name;
        }

        if (Input.DOB != user.DOB)
        {
            user.DOB = Input.DOB;
        }

        var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
        if (Input.PhoneNumber != phoneNumber)
        {
            var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
            if (!setPhoneResult.Succeeded)
            {
                var userId = await _userManager.GetUserIdAsync(user);
                throw new InvalidOperationException($"Unexpected error occurred setting phone number for user with ID '{userId}'.");
            }
        }

        await _userManager.UpdateAsync(user);

        await _signInManager.RefreshSignInAsync(user);
        StatusMessage = "Your profile has been updated";
        return RedirectToPage();
    }

    public async Task<IActionResult> OnPostSendVerificationEmailAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var user = await _userManager.GetUserAsync(User);
        if (user == null)
        {
            return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
        }


        var userId = await _userManager.GetUserIdAsync(user);
        var email = await _userManager.GetEmailAsync(user);
        var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
        var callbackUrl = Url.Page(
            "/Account/ConfirmEmail",
            pageHandler: null,
            values: new { userId = userId, code = code },
            protocol: Request.Scheme);
        await _emailSender.SendEmailAsync(
            email,
            "Confirm your email",
            $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

        StatusMessage = "Verification email sent. Please check your email.";
        return RedirectToPage();
    }
}
```

Update the `Areas/Identity/Pages/Account/Manage/Index.cshtml` with the following highlighted markup:

```
@page
@model IndexModel
@{
    ViewData["Title"] = "Profile";
    ViewData["ActivePage"] = ManageNavPages.Index;
}

<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
    <div class="col-md-6">
        <form id="profile-form" method="post">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Username"></label>
                <input asp-for="Username" class="form-control" disabled />
            </div>
            <div class="form-group">
                <label asp-for="Input.Email"></label>
                @if (Model.IsEmailConfirmed)
                {
                    <div class="input-group">
                        <input asp-for="Input.Email" class="form-control" />
                        <span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
                    </div>
                }
                else
                {
                    <input asp-for="Input.Email" class="form-control" />
                    <button id="email-verification" type="submit" asp-page-handler="SendVerificationEmail" class="btn btn-link">Send verification email</button>
                }
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Name"></label>
                <input asp-for="Input.Name" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="Input.DOB"></label>
                <input asp-for="Input.DOB" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="Input.PhoneNumber"></label>
                <input asp-for="Input.PhoneNumber" class="form-control" />
                <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
            </div>
            <button id="update-profile-button" type="submit" class="btn btn-primary">Save</button>
        </form>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

### Update the Account/Register.cshtml page

Update the `InputModel` in `Areas/Identity/Pages/Account/Register.cshtml.cs` with the following highlighted code:

```
[AllowAnonymous]
public class RegisterModel : PageModel
{
    private readonly SignInManager<WebApp1User> _signInManager;
    private readonly UserManager<WebApp1User> _userManager;
    private readonly ILogger<RegisterModel> _logger;
    private readonly IEmailSender _emailSender;

    public RegisterModel(
        UserManager<WebApp1User> userManager,
        SignInManager<WebApp1User> signInManager,
        ILogger<RegisterModel> logger,
        IEmailSender emailSender)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _logger = logger;
        _emailSender = emailSender;
    }

    [BindProperty]
    public InputModel Input { get; set; }

    public string ReturnUrl { get; set; }

    public class InputModel
    {
        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Full name")]
        public string Name { get; set; }

        [Required]
        [Display(Name = "Birth Date")]
        [DataType(DataType.Date)]
        public DateTime DOB { get; set; }

        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }

    public void OnGet(string returnUrl = null)
    {
        ReturnUrl = returnUrl;
    }

    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
    {
        returnUrl = returnUrl ?? Url.Content("~/");
        if (ModelState.IsValid)
        {
            var user = new WebApp1User {
                Name = Input.Name,
                DOB = Input.DOB,
                UserName = Input.Email,
                Email = Input.Email
            };
            var result = await _userManager.CreateAsync(user, Input.Password);
            if (result.Succeeded)
            {
                _logger.LogInformation("User created a new account with password.");

                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { userId = user.Id, code = code },
                    protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                await _signInManager.SignInAsync(user, isPersistent: false);
                return LocalRedirect(returnUrl);
            }
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
        }

        // If we got this far, something failed, redisplay form
        return Page();
    }
}
```

Update the `Areas/Identity/Pages/Account/Register.cshtml` with the following highlighted markup:

```
@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h4>Create a new account.</h4>
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>

            <div class="form-group">
                <label asp-for="Input.Name"></label>
                <input asp-for="Input.Name" class="form-control" />
                <span asp-validation-for="Input.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.DOB"></label>
                <input asp-for="Input.DOB" class="form-control" />
                <span asp-validation-for="Input.DOB" class="text-danger"></span>
            </div>

            <div class="form-group">
                <label asp-for="Input.Email"></label>
                <input asp-for="Input.Email" class="form-control" />
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Password"></label>
                <input asp-for="Input.Password" class="form-control" />
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.ConfirmPassword"></label>
                <input asp-for="Input.ConfirmPassword" class="form-control" />
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Register</button>
        </form>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
```

Build the project.

### Add a migration for the custom user data

The custom properties added to the `WebApp1User` Identity user class need to be reflected in the database. A migration is needed now to generate the SQL required to add the new columns, and then the database needs to be updated to apply those changes.

The Identity scaffolder created a new `DbContext` (`WebApp1Context`) alongside the original `ApplicationDbContext` from the project template. You must specify the new `WebApp1Context` Identity `DbContext` to use when running migration commands.

* [Visual Studio](#tabpanel_21_visual-studio)
* [.NET CLI](#tabpanel_21_net-cli)

In the Visual Studio **Package Manager Console**:

```
Add-Migration CustomUserData -Context WebApp1Context
Update-Database -Context WebApp1Context
```

```
dotnet ef migrations add CustomUserData --context WebApp1Context
dotnet ef database update --context WebApp1Context
```

## Test create, view, download, delete custom user data

Test the app:

* Register a new user.
* View the custom user data on the `/Identity/Account/Manage` page.
* Download and view the users personal data from the `/Identity/Account/Manage/PersonalData` page.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/visual-studio-publish-profiles
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/visual-studio-publish-profiles.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Sayed Ibrahim Hashimi](https://github.com/sayedihashimi) and [Rick Anderson](https://twitter.com/RickAndMSFT)

This document focuses on using Visual Studio 2022 or later to create and use [publish profiles](/en-us/visualstudio/deployment/publish-overview). The publish profiles created with Visual Studio can be used with MSBuild and Visual Studio. For instructions on publishing to Azure, see [Publish an ASP.NET Core app to Azure with Visual Studio](../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0).

For the most current and detailed information on:

* Publishing with Visual studio, see [Overview of Visual Studio Publish](/en-us/visualstudio/deployment/publish-overview)
* MSBuild, see [MSBuild](/en-us/visualstudio/msbuild/msbuild)
* Publishing with MSBuild, see [Microsoft.NET.Sdk.Publish](https://github.com/dotnet/sdk/tree/main/src/WebSdk#microsoftnetsdkpublish)

The `dotnet new mvc` command produces a project file containing the following root-level [\<Project> element](/en-us/visualstudio/msbuild/project-element-msbuild):

```
<Project Sdk="Microsoft.NET.Sdk.Web">
    <!-- omitted for brevity -->
</Project>
```

The preceding `<Project>` element's `Sdk` attribute imports the MSBuild [properties](/en-us/visualstudio/msbuild/msbuild-properties) and [targets](/en-us/visualstudio/msbuild/msbuild-targets) from *$(MSBuildSDKsPath)\Microsoft.NET.Sdk.Web\Sdk\Sdk.props* and *$(MSBuildSDKsPath)\Microsoft.NET.Sdk.Web\Sdk\Sdk.targets*, respectively. The default location for `$(MSBuildSDKsPath)` (with Visual Studio 2022) is the *%programfiles%\Microsoft Visual Studio\2022\Preview\MSBuild\Sdks* folder.

`Microsoft.NET.Sdk.Web` ([Web SDK](../razor-pages/web-sdk?view=aspnetcore-10.0)) depends on other SDKs, including `Microsoft.NET.Sdk` ([.NET SDK](/en-us/dotnet/core/project-sdk/msbuild-props)) and `Microsoft.NET.Sdk.Razor` ([Razor SDK](../razor-pages/sdk?view=aspnetcore-10.0)). The MSBuild properties and targets associated with each dependent SDK are imported. Publish targets import the appropriate set of targets based on the publish method used.

When MSBuild or Visual Studio loads a project, the following high-level actions occur:

* Build project
* Compute files to publish
* Publish files to destination

## Compute project items

When the project is loaded, the [MSBuild project items](/en-us/visualstudio/msbuild/common-msbuild-project-items) (files) are computed. The item type determines how the file is processed. By default, `.cs` files are included in the `Compile` item list. Files in the `Compile` item list are compiled.

The `Content` item list contains files that are published in addition to the build outputs. By default, files matching the patterns `wwwroot\**`, `**\*.config`, and `**\*.json` are included in the `Content` item list. For example, the `wwwroot\**` [globbing pattern](https://gruntjs.com/configuring-tasks#globbing-patterns) matches all files in the *wwwroot* folder and its subfolders.

The [Web SDK](../razor-pages/web-sdk?view=aspnetcore-10.0) imports the [Razor SDK](../razor-pages/sdk?view=aspnetcore-10.0). As a result, files matching the patterns `**\*.cshtml` and `**\*.razor` are also included in the `Content` item list.

The [Web SDK](../razor-pages/web-sdk?view=aspnetcore-10.0) imports the [Razor SDK](../razor-pages/sdk?view=aspnetcore-10.0). As a result, files matching the `**\*.cshtml` pattern are also included in the `Content` item list.

To explicitly add a file to the publish list, add the file directly in the `.csproj` file as shown in the [Include Files](#include-files) section.

When selecting the **Publish** button in Visual Studio, or when publishing from the command line:

* The properties/items are computed (the files that are needed to build).
* **Visual Studio only**: NuGet packages are restored. (Restore needs to be explicit by the user on the CLI.)
* The project builds.
* The publish items are computed (the files that are needed to publish).
* The project is published (the computed files are copied to the publish destination).

When an ASP.NET Core project references `Microsoft.NET.Sdk.Web` in the project file, an `app_offline.htm` file is placed at the root of the web app directory. When the file is present, the ASP.NET Core Module gracefully shuts down the app and serves the `app_offline.htm` file during the deployment. For more information, see the [ASP.NET Core Module configuration reference](aspnet-core-module?view=aspnetcore-10.0#app_offlinehtm).

## Basic command-line publishing

Command-line publishing works on all .NET-supported platforms and doesn't require Visual Studio. In the following examples, the .NET CLI's [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command is run from the project directory (which contains the `.csproj` file). If the project folder isn't the current working directory, explicitly pass in the project file path. For example:

```
dotnet publish C:\Webs\Web1
```

Run the following commands to create and publish a web app:

```
dotnet new mvc
dotnet publish
```

The `dotnet publish` command produces a variation of the following output:

```
C:\Webs\Web1>dotnet publish
Restore complete (0.4s)
  Web1 succeeded (9.2s) → bin\Release\net9.0\publish\
```

The default publish folder format is `bin\Debug\{TARGET FRAMEWORK MONIKER}`. For example, `bin\Release\net9.0\`

The following command specifies a `Release` build and the publishing directory:

```
dotnet publish -c Release -o C:\MyWebs\test
```

The `dotnet publish` command calls MSBuild, which invokes the `Publish` target. Any parameters passed to `dotnet publish` are passed to MSBuild. The `-c` and `-o` parameters map to MSBuild's `Configuration` and `OutputPath` properties, respectively.

[MSBuild properties](/en-us/visualstudio/msbuild/msbuild-command-line-reference) can be passed using either of the following formats:

* `-p:<NAME>=<VALUE>`
* `/p:<NAME>=<VALUE>`

For example, the following command publishes a `Release` build to a network share. The network share is specified with forward slashes (*//r8/*) and works on all .NET supported platforms.

```
dotnet publish -c Release /p:PublishDir=//r8/release/AdminWeb
```

Confirm that the published app for deployment isn't running. Files in the *publish* folder are locked when the app is running. Deployment can't occur because locked files can't be copied.

See the [Microsoft.NET.Sdk.Publish](https://github.com/dotnet/sdk/tree/main/src/WebSdk#microsoftnetsdkpublish) readme file for more information.

## Publish profiles

This section uses Visual Studio 2022 or later to create a publishing profile. Once the profile is created, publishing from Visual Studio or the command line is available. Publish profiles can simplify the publishing process, and any number of profiles can exist.

Create a publish profile in Visual Studio by choosing one of the following paths:

* Right-click the project in **Solution Explorer** and select **Publish**.
* Select **Publish {PROJECT NAME}** from the **Build** menu.

The **Publish** tab of the app capabilities page is displayed. Several publish targets are available, including:

* Azure
* Docker Container Registry
* Folder
* IIS, FTP, Web Deploy (for any web server)
* Import Profile

To determine the most appropriate publish target, see [What publishing options are right for me](/en-us/visualstudio/deployment/deploying-applications-services-and-components-resources#what-publishing-options-are-right-for-me).

When the **Folder** publish target is selected, specify a folder path to store the published assets. The default folder path is *bin\\{PROJECT CONFIGURATION}\\{TARGET FRAMEWORK MONIKER}\publish\\*. For example, *bin\Release\netcoreapp2.2\publish\\*. Select the **Create Profile** button to finish.

Once a publish profile is created, the **Publish** tab's content changes. The newly created profile appears in a drop-down list. Below the drop-down list, select **Create new profile** to create another new profile.

Visual Studio's publish tool produces a `Properties/PublishProfiles/{PROFILE NAME}.pubxml` MSBuild file describing the publish profile. The *.pubxml* file:

* Contains publish configuration settings and is consumed by the publishing process.
* Can be modified to customize the build and publish process.

When publishing to an Azure target, the *.pubxml* file:

* Contains the Azure subscription identifier.
* Shouldn't be checked into source control because the subscription identifier is sensitive information.

Sensitive information, for example, the publish password, is encrypted on a per user/machine level. The `Properties/PublishProfiles/{PROFILE NAME}.pubxml.user` file contains the information needed by MSBuild to retrieve the user name and password.

For an overview of how to publish an ASP.NET Core web app, see [Host and deploy ASP.NET Core](./?view=aspnetcore-10.0). The MSBuild tasks and targets necessary to publish an ASP.NET Core web app are open-source in the [dotnet/websdk repository](https://github.com/dotnet/sdk/tree/main/src/WebSdk).

`dotnet publish` and `dotnet build`:

* Can use folder, MSDeploy, and [Kudu](https://github.com/projectkudu/kudu/wiki) publish profiles. Because MSDeploy lacks cross-platform support, MSDeploy options are supported only on Windows.
* Support Kudu APIs to publish to Azure from any platform. Visual Studio publish supports the Kudu APIs, but it's supported by WebSDK for cross-platform publish to Azure.

Don't pass `DeployOnBuild` to the `dotnet publish` command.

For more information, see [Microsoft.NET.Sdk.Publish](https://github.com/dotnet/sdk/tree/main/src/WebSdk#microsoftnetsdkpublish).

## Folder publish example

When publishing with a profile named `FolderProfile`, use any of the following commands:

```
dotnet publish /p:Configuration=Release /p:PublishProfile=FolderProfile
```

```
dotnet build /p:DeployOnBuild=true /p:PublishProfile=FolderProfile
```

```
msbuild /p:DeployOnBuild=true /p:PublishProfile=FolderProfile
```

The .NET CLI's [dotnet build](/en-us/dotnet/core/tools/dotnet-build) command calls `msbuild` to run the build and publish process. The `dotnet build` and `msbuild` commands are equivalent when passing in a folder profile. When calling `msbuild` directly on Windows, the .NET Framework version of MSBuild is used. Calling `dotnet build` on a non-folder profile:

* Invokes `msbuild`, which uses MSDeploy.
* Results in a failure (even when running on Windows). To publish with a non-folder profile, call `msbuild` directly.

The following folder publish profile was created with Visual Studio and publishes to a network share:

```
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
  <PropertyGroup>
    <DeleteExistingFiles>false</DeleteExistingFiles>
    <ExcludeApp_Data>false</ExcludeApp_Data>
    <LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <PublishProvider>FileSystem</PublishProvider>
    <PublishUrl>\\r8\Release\AdminWeb</PublishUrl>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <_TargetId>Folder</_TargetId>
  </PropertyGroup>
</Project>
```

In the preceding example:

* The `<ExcludeApp_Data>` property is present to satisfy an XML schema requirement. The `<ExcludeApp_Data>` property has no effect on the publish process, even if there's an *App\_Data* folder in the project root. The *App\_Data* folder doesn't receive special treatment as it does in ASP.NET 4.x projects.

* The `<LastUsedBuildConfiguration>` property is set to `Release`. When publishing from Visual Studio, the value of `<LastUsedBuildConfiguration>` is set using the value when the publish process is started. `<LastUsedBuildConfiguration>` is special and shouldn't be overridden in an imported MSBuild file. This property can, however, be overridden from the command line using one of the following approaches.

  * Using the .NET CLI:

    ```
    dotnet publish /p:Configuration=Release /p:PublishProfile=FolderProfile
    ```

    ```
    dotnet build -c Release /p:DeployOnBuild=true /p:PublishProfile=FolderProfile
    ```

  * Using MSBuild:

    ```
    msbuild /p:Configuration=Release /p:DeployOnBuild=true /p:PublishProfile=FolderProfile
    ```

## Publish to an MSDeploy endpoint from the command line

See [Microsoft.NET.Sdk.Publish](https://github.com/dotnet/sdk/tree/main/src/WebSdk#microsoftnetsdkpublish).

## Set the environment

Include the `<EnvironmentName>` property in the publish profile (*.pubxml*) or project file to set the app's [environment](../fundamentals/environments?view=aspnetcore-10.0):

```
<PropertyGroup>
  <EnvironmentName>Development</EnvironmentName>
</PropertyGroup>
```

If you require *web.config* transformations (for example, setting environment variables based on the configuration, profile, or environment), see [Transform web.config](iis/transform-webconfig?view=aspnetcore-10.0).

## Exclude files

When publishing ASP.NET Core web apps, the following assets are included:

* Build artifacts

* Folders and files matching the following globbing patterns:

  * `**\*.config` (for example, *web.config*)
  * `**\*.json` (for example, `appsettings.json`)
  * `wwwroot\**`

MSBuild supports [globbing patterns](https://gruntjs.com/configuring-tasks#globbing-patterns). For example, the following `<Content>` element suppresses the copying of text (*.txt*) files in the *wwwroot\content* folder and its subfolders:

```
<ItemGroup>
  <Content Update="wwwroot/content/**/*.txt" CopyToPublishDirectory="Never" />
</ItemGroup>
```

The preceding markup can be added to a publish profile or the `.csproj` file. When added to the `.csproj` file, the rule is added to all publish profiles in the project.

The following `<MsDeploySkipRules>` element excludes all files from the *wwwroot\content* folder:

```
<ItemGroup>
  <MsDeploySkipRules Include="CustomSkipFolder">
    <ObjectName>dirPath</ObjectName>
    <AbsolutePath>wwwroot\\content</AbsolutePath>
  </MsDeploySkipRules>
</ItemGroup>
```

`<MsDeploySkipRules>` won't delete the *skip* targets from the deployment site. `<Content>` targeted files and folders are deleted from the deployment site. For example, suppose a deployed web app had the following files:

* `Views/Home/About1.cshtml`
* `Views/Home/About2.cshtml`
* `Views/Home/About3.cshtml`

If the following `<MsDeploySkipRules>` elements are added, those files wouldn't be deleted on the deployment site.

```
<ItemGroup>
  <MsDeploySkipRules Include="CustomSkipFile">
    <ObjectName>filePath</ObjectName>
    <AbsolutePath>Views\\Home\\About1.cshtml</AbsolutePath>
  </MsDeploySkipRules>

  <MsDeploySkipRules Include="CustomSkipFile">
    <ObjectName>filePath</ObjectName>
    <AbsolutePath>Views\\Home\\About2.cshtml</AbsolutePath>
  </MsDeploySkipRules>

  <MsDeploySkipRules Include="CustomSkipFile">
    <ObjectName>filePath</ObjectName>
    <AbsolutePath>Views\\Home\\About3.cshtml</AbsolutePath>
  </MsDeploySkipRules>
</ItemGroup>
```

The preceding `<MsDeploySkipRules>` elements prevent the *skipped* files from being deployed. It won't delete those files once they're deployed.

The following `<Content>` element deletes the targeted files at the deployment site:

```
<ItemGroup>
  <Content Update="Views/Home/About?.cshtml" CopyToPublishDirectory="Never" />
</ItemGroup>
```

Using command-line deployment with the preceding `<Content>` element yields a variation of the following output:

```
MSDeployPublish:
  Starting Web deployment task from source: manifest(C:\Webs\Web1\obj\Release\{TARGET FRAMEWORK MONIKER}\PubTmp\Web1.SourceManifest.
  xml) to Destination: auto().
  Deleting file (Web11112\Views\Home\About1.cshtml).
  Deleting file (Web11112\Views\Home\About2.cshtml).
  Deleting file (Web11112\Views\Home\About3.cshtml).
  Updating file (Web11112\web.config).
  Updating file (Web11112\Web1.deps.json).
  Updating file (Web11112\Web1.dll).
  Updating file (Web11112\Web1.pdb).
  Updating file (Web11112\Web1.runtimeconfig.json).
  Successfully executed Web deployment task.
  Publish Succeeded.
Done Building Project "C:\Webs\Web1\Web1.csproj" (default targets).
```

## Include files

The following sections outline different approaches for file inclusion at publish time. The [General file inclusion](#general-file-inclusion) section uses the `DotNetPublishFiles` item, which is provided by a publish targets file in the [Web SDK](../razor-pages/web-sdk?view=aspnetcore-10.0). The [Selective file inclusion](#selective-file-inclusion) section uses the `ResolvedFileToPublish` item, which is provided by a publish targets file in the [.NET SDK](/en-us/dotnet/core/project-sdk/msbuild-props). Because the Web SDK depends on the .NET SDK, either item can be used in an ASP.NET Core project.

### General file inclusion

The following example's `<ItemGroup>` element demonstrates copying a folder located outside of the project directory to a folder of the published site. Any files added to the following markup's `<ItemGroup>` are included by default.

```
<ItemGroup>
  <_CustomFiles Include="$(MSBuildProjectDirectory)/../images/**/*" />
  <DotNetPublishFiles Include="@(_CustomFiles)">
    <DestinationRelativePath>wwwroot/images/%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
  </DotNetPublishFiles>
</ItemGroup>
```

The preceding markup:

* Can be added to the `.csproj` file or the publish profile. If it's added to the `.csproj` file, it's included in each publish profile in the project.
* Declares a `_CustomFiles` item to store files matching the `Include` attribute's globbing pattern. The *images* folder referenced in the pattern is located outside of the project directory. A [reserved property](/en-us/visualstudio/msbuild/msbuild-reserved-and-well-known-properties), named `$(MSBuildProjectDirectory)`, resolves to the project file's absolute path.
* Provides a list of files to the `DotNetPublishFiles` item. By default, the item's `<DestinationRelativePath>` element is empty. The default value is overridden in the markup and uses [well-known item metadata](/en-us/visualstudio/msbuild/msbuild-well-known-item-metadata) such as `%(RecursiveDir)`. The inner text represents the *wwwroot/images* folder of the published site.

### Selective file inclusion

The highlighted markup in the following example demonstrates:

* Copying a file located outside of the project into the published site's *wwwroot* folder. The file name of *ReadMe2.md* is maintained.
* Excluding the *wwwroot\Content* folder.
* Excluding *Views\Home\About2.cshtml*.

```
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" 
         xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <PublishProvider>FileSystem</PublishProvider>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish />
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <PublishFramework />
    <ProjectGuid>
      <GUID Here=""></GUID>
    </ProjectGuid>
    <publishUrl>bin\Release\PublishOutput</publishUrl>
    <DeleteExistingFiles>False</DeleteExistingFiles>
  </PropertyGroup>
  <ItemGroup>
    <ResolvedFileToPublish Include="..\ReadMe2.md">
      <RelativePath>wwwroot\ReadMe2.md</RelativePath>
    </ResolvedFileToPublish>

    <Content Update="wwwroot\Content\**\*" CopyToPublishDirectory="Never" />
    <Content Update="Views\Home\About2.cshtml" CopyToPublishDirectory="Never" />
  </ItemGroup>
</Project>
```

The preceding example uses the `ResolvedFileToPublish` item, whose default behavior is to always copy the files provided in the `Include` attribute to the published site. Override the default behavior by including a `<CopyToPublishDirectory>` child element with inner text of either `Never` or `PreserveNewest`. For example:

```
<ResolvedFileToPublish Include="..\ReadMe2.md">
  <RelativePath>wwwroot\ReadMe2.md</RelativePath>
  <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
```

For more deployment samples, see the [Web SDK README file](https://github.com/dotnet/sdk/tree/main/src/WebSdk).

## Run a target before or after publishing

The built-in `BeforePublish` and `AfterPublish` targets execute a target before or after the publish target. Add the following elements to the publish profile to log console messages both before and after publishing:

```
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project>
  <PropertyGroup>
    <WebPublishMethod>MSDeploy</WebPublishMethod>
    <ResourceId>/subscriptions/SomeGuid/resourcegroups/TP_RG/providers/Microsoft.Web/sites/TP22</ResourceId>
    <ResourceGroup>TP_RG</ResourceGroup>
    <PublishProvider>AzureWebSite</PublishProvider>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish>https://tp22.azurewebsites.net</SiteUrlToLaunchAfterPublish>
    <LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
    <ExcludeApp_Data>false</ExcludeApp_Data>
    <ProjectGuid>GuidHere</ProjectGuid>
    <MSDeployServiceURL>something.scm.azurewebsites.net:443</MSDeployServiceURL>
    <DeployIisAppPath>myDeploysIISpath</DeployIisAppPath>
    <RemoteSitePhysicalPath />
    <SkipExtraFilesOnServer>true</SkipExtraFilesOnServer>
    <MSDeployPublishMethod>WMSVC</MSDeployPublishMethod>
    <EnableMSDeployBackup>true</EnableMSDeployBackup>
    <EnableMsDeployAppOffline>true</EnableMsDeployAppOffline>
    <UserName />
    <_SavePWD>false</_SavePWD>
    <_DestinationType>AzureWebSite</_DestinationType>
    <InstallAspNetCoreSiteExtension>false</InstallAspNetCoreSiteExtension>
  </PropertyGroup>
    <Target Name="CustomActionsBeforePublish" BeforeTargets="BeforePublish">
        <Message Text="Inside BeforePublish" Importance="high" />
    </Target>
    <Target Name="CustomActionsAfterPublish" AfterTargets="AfterPublish">
        <Message Text="Inside AfterPublish" Importance="high" />
    </Target>
</Project>
```

## Publish to a server using an untrusted certificate

Add the `<AllowUntrustedCertificate>` property with a value of `True` to the publish profile:

```
<PropertyGroup>
  <AllowUntrustedCertificate>True</AllowUntrustedCertificate>
</PropertyGroup>
```

## The Kudu service

To view the files in an Azure App Service web app deployment, use the [Kudu service](https://github.com/projectkudu/kudu/wiki/Accessing-the-kudu-service). Append the `scm` token to the web app name. For example:

| URL                                    | Result       |
| -------------------------------------- | ------------ |
| `http://mysite.azurewebsites.net/`     | Web App      |
| `http://mysite.scm.azurewebsites.net/` | Kudu service |

Select the [Debug Console](https://github.com/projectkudu/kudu/wiki/Kudu-console) menu item to view, edit, delete, or add files.

## Related content

* [Web SDK README file](https://github.com/dotnet/sdk/tree/main/src/WebSdk)
* [Web SDK GitHub repository](https://github.com/dotnet/websdk/issues): File issues and request features for deployment.
* [Web Deploy](https://www.iis.net/downloads/microsoft/web-deploy) (MSDeploy) simplifies deployment of web apps and websites to IIS servers.
* [Transform web.config](iis/transform-webconfig?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/validation-advanced
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/forms/validation-advanced.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor advanced form validation

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article demonstrates reusable validator components and remote validation. For common form validation APIs, including data annotations, direct [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext) validation, message display, styling, state, and submit behavior, see [ASP.NET Core Blazor forms validation](validation?view=aspnetcore-10.0).

For model-based validation rules shared by Blazor and Minimal APIs, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0).

For browser validation in static server-side rendering (static SSR), see [ASP.NET Core Blazor client-side form validation in static SSR](validation-client-side?view=aspnetcore-10.0).

## Build a validator component

A validator component encapsulates validation that uses a form's `EditContext` and [ValidationMessageStore](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationmessagestore). This is useful when the same validation behavior is used by several forms or when errors arrive from a service rather than from validation attributes on the model.

The component:

* Receives the form's `EditContext` as a cascading parameter.
* Creates a message store for its errors.
* Clears stale form errors when validation is requested.
* Clears a field's stale errors when the field changes.
* Exposes methods for displaying and clearing errors.
* Unsubscribes its event handlers when disposed.

`CustomValidation.razor`:

```
@implements IDisposable

@code {
    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }

    private ValidationMessageStore messages = default!;

    protected override void OnInitialized()
    {
        if (CurrentEditContext is null)
        {
            throw new InvalidOperationException(
                "CustomValidation requires a cascading EditContext.");
        }

        messages = new ValidationMessageStore(CurrentEditContext);
        CurrentEditContext.OnValidationRequested +=
            HandleValidationRequested;
        CurrentEditContext.OnFieldChanged += HandleFieldChanged;
    }

    public void DisplayErrors(IDictionary<string, string[]> errors)
    {
        foreach (var error in errors)
        {
            messages.Add(
                CurrentEditContext!.Field(error.Key),
                error.Value);
        }

        CurrentEditContext!.NotifyValidationStateChanged();
    }

    public void ClearErrors()
    {
        messages.Clear();
        CurrentEditContext!.NotifyValidationStateChanged();
    }

    private void HandleValidationRequested(
        object? sender, ValidationRequestedEventArgs e) =>
        ClearErrors();

    private void HandleFieldChanged(
        object? sender, FieldChangedEventArgs e)
    {
        messages.Clear(e.FieldIdentifier);
        CurrentEditContext!.NotifyValidationStateChanged();
    }

    public void Dispose()
    {
        if (CurrentEditContext is not null)
        {
            CurrentEditContext.OnValidationRequested -=
                HandleValidationRequested;
            CurrentEditContext.OnFieldChanged -= HandleFieldChanged;
        }
    }
}
```

Place the component inside an `EditForm` and capture a component reference when the form or a service should display errors:

```
<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="customValidation" />
    <ValidationSummary />

    ...
</EditForm>

@code {
    private CustomValidation? customValidation;
}
```

The component can be used alongside `DataAnnotationsValidator`. Each validator has its own message store associated with the same `EditContext`, and `ValidationMessage` and `ValidationSummary` display messages from both validators.

To implement a business rule inside the validator component instead of accepting external errors, run the rule from `HandleValidationRequested` or `HandleFieldChanged` and add its messages to `messages`. For a smaller example that performs this directly in a form component, see [ASP.NET Core Blazor forms validation](validation?view=aspnetcore-10.0#add-validation-through-editcontext).

[]()

### Add asynchronous validation

The same component pattern supports asynchronous work:

* In an `OnValidationRequested` handler, call `e.AddAsyncValidator` to register form-level work. `EditForm` awaits it before invoking `OnValidSubmit` or `OnInvalidSubmit`.
* In an `OnFieldChanged` handler, call `EditContext.RegisterAsyncFieldValidator` to start validation for that field. Starting another validation for the same field supersedes and cancels the previous operation.

For form-level asynchronous validation:

```
private void HandleValidationRequested(
    object? sender, ValidationRequestedEventArgs e) =>
    e.AddAsyncValidator(ValidateAsync);

private async Task ValidateAsync(CancellationToken cancellationToken)
{
    var field = CurrentEditContext!.Field(nameof(Model.Username));
    messages.Clear(field);

    var available = await Http.GetFromJsonAsync<bool>(
        $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}",
        cancellationToken);

    if (!available)
    {
        messages.Add(field, "The username is already taken.");
    }

    CurrentEditContext.NotifyValidationStateChanged();
}
```

For field-level asynchronous validation:

```
private void HandleFieldChanged(
    object? sender, FieldChangedEventArgs e)
{
    CurrentEditContext!.RegisterAsyncFieldValidator(
        e.FieldIdentifier,
        token => ValidateFieldAsync(e.FieldIdentifier, token));
}
```

Pass the supplied cancellation token to I/O. Clear prior messages before starting the operation, avoid publishing partial results after an exception, and call `NotifyValidationStateChanged` after updating messages.

An operation canceled because it was superseded or because the validation pass was canceled is discarded. Other exceptions place the field or form in the faulted state. For displaying pending and faulted state, see [ASP.NET Core Blazor forms validation](validation?view=aspnetcore-10.0#respond-to-validation-state).

For a complete component that combines form-level and per-field asynchronous validation, see the following sample:

```
@implements IDisposable
@inject HttpClient Http

@* A validator component that runs asynchronous validation, both when the whole form is
   validated on submit and per field as the user edits the Username field. *@

@code {
    [CascadingParameter]
    private EditContext? CurrentEditContext { get; set; }

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

    private ValidationMessageStore? messages;

    protected override void OnInitialized()
    {
        ArgumentNullException.ThrowIfNull(CurrentEditContext);
        messages = new ValidationMessageStore(CurrentEditContext);
        CurrentEditContext.OnValidationRequested += OnValidationRequested;
        CurrentEditContext.OnFieldChanged += OnFieldChanged;
    }

    // Registers asynchronous work for the whole form. EditForm awaits it before invoking
    // OnValidSubmit.
    private void OnValidationRequested(
        object? sender, ValidationRequestedEventArgs e) =>
        e.AddAsyncValidator(ValidateUsernameAsync);

    // Registers asynchronous work for a single field. A new registration supersedes and
    // cancels any validation already in flight for the field.
    private void OnFieldChanged(object? sender, FieldChangedEventArgs e)
    {
        if (e.FieldIdentifier.FieldName != nameof(RegistrationModel.Username))
        {
            return;
        }

        CurrentEditContext!.RegisterAsyncFieldValidator(
            e.FieldIdentifier,
            token => CheckAsync(e.FieldIdentifier, token));
    }

    private Task ValidateUsernameAsync(CancellationToken token) =>
        CheckAsync(CurrentEditContext!.Field(nameof(Model.Username)), token);

    private async Task CheckAsync(FieldIdentifier field, CancellationToken token)
    {
        messages!.Clear(field);

        var available = await Http.GetFromJsonAsync<bool>(
            $"api/usernames/available?value={Uri.EscapeDataString(Model.Username)}",
            token);

        if (!available)
        {
            messages.Add(field, "The username is already taken.");
        }

        CurrentEditContext!.NotifyValidationStateChanged();
    }

    public void Dispose()
    {
        if (CurrentEditContext is not null)
        {
            CurrentEditContext.OnValidationRequested -= OnValidationRequested;
            CurrentEditContext.OnFieldChanged -= OnFieldChanged;
        }
    }
}
```

For asynchronous validation attributes on the model, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0#asynchronous-validation-support).

Validator component code runs where the component runs. In Interactive WebAssembly, it runs in the browser, while it runs on the server over the circuit in Interactive Server.

In static SSR, validator component code runs on the server during the form post and doesn't provide live .NET field validation between requests.

[]()[]()

## Remote validation from Interactive WebAssembly

Remote validation sends form data from an Interactive WebAssembly component to a server endpoint and adds returned field errors to the form's `EditContext`. It's useful when a rule requires private server data, an external service, or other logic that shouldn't run in the browser.

The form:

1. Runs data annotations validation locally.
2. Sends locally valid input to the endpoint from `OnValidSubmit`.
3. Receives field-keyed validation errors from the server.
4. Adds remote errors to the form through the validator component.

`OnValidSubmit` only means that local validation succeeded. Process or save the model only after remote validation also succeeds.

Important

Don't send private validation data or business rules to the browser. The server must validate every request independently because client-side validation can be bypassed.

This example validates remotely when the form is submitted. For live per-field remote checks, use the asynchronous field-validation pattern from [Add asynchronous validation](#add-asynchronous-validation).

If the WebAssembly form is prerendered, its client-side services must also be available during prerendering. For the available approaches, see [Prerender ASP.NET Core Razor components](../components/prerender?view=aspnetcore-10.0#client-side-services-fail-to-resolve-during-prerendering).

### Validate with a Minimal API

Call [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation) in the server project to validate supported endpoint parameters before the handler runs.

The [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation) APIs used for generated validation metadata are experimental in .NET 10. For details, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0#experimental-api-in-apps-that-target-net-10).

If the model is declared in the `.Client` project, register its generated validation metadata in both projects as described in [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0#register-validation-across-assemblies).

The endpoint adds a private business rule and returns errors keyed by model member name:

```
app.MapPost("/api/starships/validate", (StarshipModel model) =>
{
    Dictionary<string, string[]> errors = [];

    if (model.Classification == "Defense" &&
        string.IsNullOrWhiteSpace(model.Description))
    {
        errors[nameof(model.Description)] =
            ["A defense ship requires a description."];
    }

    if (errors.Count > 0)
    {
        return Results.ValidationProblem(errors);
    }

    return Results.NoContent();
});
```

```
app.MapPost("/api/starships/validate", (StarshipModel model) =>
{
    Dictionary<string, string[]> errors = [];

    if (model.Classification == "Defense" &&
        string.IsNullOrWhiteSpace(model.Description))
    {
        errors[nameof(model.Description)] =
            ["A defense ship requires a description."];
    }

    if (errors.Count > 0)
    {
        return Results.ValidationProblem(errors);
    }

    return Results.NoContent();
});
```

Automatic validation rejects invalid data annotations before the handler runs. [ValidationProblem](/en-us/dotnet/api/microsoft.aspnetcore.http.results.validationproblem) returns `400 Bad Request` with an `errors` property containing field-keyed messages. Successful validation returns `204 No Content`.

### Validate with an API controller

In a hosted Blazor WebAssembly solution, place the shared model in the `Shared` project and validate it with an API controller in the `Server` project. The `[ApiController]` attribute automatically rejects invalid data annotations before the action runs.

```
[ApiController]
[Route("api/starships/validate")]
public class StarshipValidationController : ControllerBase
{
    [HttpPost]
    public IActionResult Validate(StarshipModel model)
    {
        if (model.Classification == "Defense" &&
            string.IsNullOrWhiteSpace(model.Description))
        {
            ModelState.AddModelError(
                nameof(model.Description),
                "A defense ship requires a description.");
        }

        if (!ModelState.IsValid)
        {
            return ValidationProblem(ModelState);
        }

        return NoContent();
    }
}
```

Register and map controllers in the server project. The controller returns `400 Bad Request` with a `ValidationProblemDetails` response when validation fails and `204 No Content` when it succeeds.

### Call the endpoint and display errors

Register an `HttpClient` in the WebAssembly project with the app's base address:

```
builder.Services.AddScoped(sp =>
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
```

```
builder.Services.AddScoped(sp =>
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
```

Place the `CustomValidation` component from [Build a validator component](#build-a-validator-component) in the form:

```
@page "/"
@using System.Net
@using System.Net.Http.Json
@using BlazorWebAppRemoteValidation.Client.Models
@inject HttpClient Http

<PageTitle>Remote validation</PageTitle>

<h1>Remote validation</h1>

<p>Local validation runs before the form calls the remote endpoint. The endpoint validates the model again and applies a private business rule that requires a description for defense ships.</p>

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="remoteErrors" />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText id="identifier" @bind-Value="Model.Identifier" />
        </label>
        <ValidationMessage For="() => Model.Identifier" />
    </p>

    <p>
        <label>
            Classification:
            <InputSelect id="classification" @bind-Value="Model.Classification">
                <option value="">Select...</option>
                <option value="Exploration">Exploration</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
        <ValidationMessage For="() => Model.Classification" />
    </p>

    <p>
        <label>
            Description:
            <InputText id="description" @bind-Value="Model.Description" />
        </label>
        <ValidationMessage For="() => Model.Description" />
    </p>

    <button id="submit" type="submit">Submit</button>
</EditForm>

@if (accepted)
{
    <p id="accepted" role="status">The server accepted the form.</p>
}

@code {
    private StarshipModel Model { get; } = new();
    private CustomValidation? remoteErrors;
    private bool accepted;

    private async Task Submit()
    {
        accepted = false;

        using var response = await Http.PostAsJsonAsync(
            "api/starships/validate", Model);

        if (response.IsSuccessStatusCode)
        {
            accepted = true;
            return;
        }

        if (response.StatusCode == HttpStatusCode.BadRequest)
        {
            var problem = await response.Content
                .ReadFromJsonAsync<ValidationProblemResponse>()
                ?? throw new InvalidOperationException(
                    "The validation response didn't contain a response body.");

            remoteErrors!.DisplayErrors(problem.Errors);
            return;
        }

        response.EnsureSuccessStatusCode();
    }

    private sealed record ValidationProblemResponse(
        Dictionary<string, string[]> Errors);
}
```

```
@using System.Net
@using System.Net.Http.Json
@inject HttpClient Http

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <CustomValidation @ref="remoteErrors" />
    <ValidationSummary />

    ...
</EditForm>

@code {
    private StarshipModel Model { get; } = new StarshipModel();
    private CustomValidation? remoteErrors;

    private async Task Submit()
    {
        using var response = await Http.PostAsJsonAsync(
            "api/starships/validate", Model);

        if (response.IsSuccessStatusCode)
        {
            // Process or save the model.
            return;
        }

        if (response.StatusCode == HttpStatusCode.BadRequest)
        {
            var problem = await response.Content
                .ReadFromJsonAsync<ValidationProblemResponse>();

            if (problem is not null)
            {
                remoteErrors!.DisplayErrors(problem.Errors);
            }

            return;
        }

        response.EnsureSuccessStatusCode();
    }

    private sealed class ValidationProblemResponse
    {
        public Dictionary<string, string[]> Errors { get; set; } =
            new Dictionary<string, string[]>();
    }
}
```

The validator component clears a remote field error when that field changes, so the user can correct the value and submit again. Protect the endpoint according to the application's security requirements; authentication and authorization are outside the scope of this validation example.

The [complete remote-validation sample](https://github.com/dotnet/blazor-samples/tree/main/11.0/BlazorWebAppRemoteValidation) includes the host endpoint, shared model, cross-assembly validation registration, validator component, and Interactive WebAssembly form.

The [.NET 10 remote-validation sample](https://github.com/dotnet/blazor-samples/tree/main/10.0/BlazorWebAppRemoteValidation) demonstrates the same validation flow in an Interactive Auto app with authentication and a server-side proxy.

## Related content

* [ASP.NET Core Blazor forms validation](validation?view=aspnetcore-10.0)
* [ASP.NET Core Blazor client-side form validation in static SSR](validation-client-side?view=aspnetcore-10.0)
* [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0)
* [Minimal APIs quick reference](../../fundamentals/minimal-apis?view=aspnetcore-10.0)

- [ASP.NET Core Blazor forms validation](validation?view=aspnetcore-10.0)
- [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0)
- [Minimal APIs quick reference](../../fundamentals/minimal-apis?view=aspnetcore-10.0)

[ASP.NET Core Blazor forms validation](validation?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/performance
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/performance.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Performance best practices with gRPC

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

gRPC is designed for high-performance services. This document explains how to get the best performance possible from gRPC.

## Reuse gRPC channels

A gRPC channel should be reused when making gRPC calls. Reusing a channel allows calls to be multiplexed through an existing HTTP/2 connection.

If a new channel is created for each gRPC call then the amount of time it takes to complete can increase significantly. Each call will require multiple network round-trips between the client and the server to create a new HTTP/2 connection:

1. Opening a socket
2. Establishing TCP connection
3. Negotiating TLS
4. Starting HTTP/2 connection
5. Making the gRPC call

Channels are safe to share and reuse between gRPC calls:

* gRPC clients are created with channels. gRPC clients are lightweight objects and don't need to be cached or reused.
* Multiple gRPC clients can be created from a channel, including different types of clients.
* A channel and clients created from the channel can safely be used by multiple threads.
* Clients created from the channel can make multiple simultaneous calls.

gRPC client factory offers a centralized way to configure channels. It automatically reuses underlying channels. For more information, see [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0).

## Connection concurrency

HTTP/2 connections typically have a limit on the number of [maximum concurrent streams (active HTTP requests)](https://httpwg.github.io/specs/rfc7540.html#rfc.section.5.1.2) on a connection at one time. By default, most servers set this limit to 100 concurrent streams.

A gRPC channel uses a single HTTP/2 connection, and concurrent calls are multiplexed on that connection. When the number of active calls reaches the connection stream limit, additional calls are queued in the client. Queued calls wait for active calls to complete before they are sent. Applications with high load, or long running streaming gRPC calls, could see performance issues caused by calls queuing because of this limit.

.NET 5 introduces the [SocketsHttpHandler.EnableMultipleHttp2Connections](/en-us/dotnet/api/system.net.http.socketshttphandler.enablemultiplehttp2connections#system-net-http-socketshttphandler-enablemultiplehttp2connections) property. When set to `true`, additional HTTP/2 connections are created by a channel when the concurrent stream limit is reached. When a `GrpcChannel` is created its internal `SocketsHttpHandler` is automatically configured to create additional HTTP/2 connections. If an app configures its own handler, consider setting `EnableMultipleHttp2Connections` to `true`:

```
var channel = GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
{
    HttpHandler = new SocketsHttpHandler
    {
        EnableMultipleHttp2Connections = true,

        // ...configure other handler settings
    }
});
```

.NET Framework apps that make gRPC calls [must be configured to use `WinHttpHandler`](netstandard?view=aspnetcore-10.0#net-framework). .NET Framework apps can set the [WinHttpHandler.EnableMultipleHttp2Connections](/en-us/dotnet/api/system.net.http.winhttphandler.enablemultiplehttp2connections#system-net-http-winhttphandler-enablemultiplehttp2connections) property to `true` to create additional connections.

There are a couple of workarounds for .NET Core 3.1 apps:

* Create separate gRPC channels for areas of the app with high load. For example, the `Logger` gRPC service might have a high load. Use a separate channel to create the `LoggerClient` in the app.
* Use a pool of gRPC channels, for example, create a list of gRPC channels. `Random` is used to pick a channel from the list each time a gRPC channel is needed. Using `Random` randomly distributes calls over multiple connections.

Important

Increasing the maximum concurrent stream limit on the server is another way to solve this problem. In Kestrel this is configured with [MaxStreamsPerConnection](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.maxstreamsperconnection#microsoft-aspnetcore-server-kestrel-core-http2limits-maxstreamsperconnection).

Increasing the maximum concurrent stream limit is not recommended. Too many streams on a single HTTP/2 connection introduces new performance issues:

* Thread contention between streams trying to write to the connection.
* Connection packet loss causes all calls to be blocked at the TCP layer.

## `ServerGarbageCollection` in client apps

The .NET garbage collector has two modes: workstation garbage collection (GC) and server garbage collection. Each is each tuned for different workloads. ASP.NET Core apps use server GC by default.

Highly concurrent apps generally perform better with server GC. If a gRPC client app is sending and receiving a high number of gRPC calls at the same time, then there may be a performance benefit in updating the app to use server GC.

To enable server GC, set `<ServerGarbageCollection>` in the app's project file:

```
<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
```

For more information about garbage collection, see [Workstation and server garbage collection](/en-us/dotnet/standard/garbage-collection/workstation-server-gc).

Note

ASP.NET Core apps use server GC by default. Enabling `<ServerGarbageCollection>` is only useful in non-server gRPC client apps, for example in a gRPC client console app.

## Asynchronous calls in client apps

Prefer using [asynchronous programming with async and await](/en-us/dotnet/csharp/asynchronous-programming/) when calling gRPC methods. Making gRPC calls with blocking, such as using `Task.Result` or `Task.Wait()`, prevents other tasks from using a thread. This can lead to thread pool starvation, poor performance, and the app to hang with a deadlock.

All gRPC method types generate asynchronous APIs on gRPC clients. The exception is unary methods, which generate both async *and* blocking methods.

Consider the following gRPC service defined in a *.proto* file:

```
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}
```

Its generated `GreeterClient` type has two .NET methods for calling `SayHello`:

* `GreeterClient.SayHelloAsync` - calls the `Greeter.SayHello` service asynchronously. Can be awaited.
* `GreeterClient.SayHello` - calls the `Greeter.SayHello` service and blocks until complete.

The blocking `GreeterClient.SayHello` method shouldn't be used in asynchronous code. It can cause performance and reliability issues.

## Load balancing

Some load balancers don't work effectively with gRPC. L4 (transport) load balancers operate at a connection level, by distributing TCP connections across endpoints. This approach works well for loading balancing API calls made with HTTP/1.1. Concurrent calls made with HTTP/1.1 are sent on different connections, allowing calls to be load balanced across endpoints.

Because L4 load balancers operate at a connection level, they don't work well with gRPC. gRPC uses HTTP/2, which multiplexes multiple calls on a single TCP connection. All gRPC calls over that connection go to one endpoint.

There are two options to effectively load balance gRPC:

* Client-side load balancing
* L7 (application) proxy load balancing

Note

Only gRPC calls can be load balanced between endpoints. Once a streaming gRPC call is established, all messages sent over the stream go to one endpoint.

### Client-side load balancing

With client-side load balancing, the client knows about endpoints. For each gRPC call, it selects a different endpoint to send the call to. Client-side load balancing is a good choice when latency is important. There's no proxy between the client and the service, so the call is sent to the service directly. The downside to client-side load balancing is that each client must keep track of the available endpoints that it should use.

Lookaside client load balancing is a technique where load balancing state is stored in a central location. Clients periodically query the central location for information to use when making load balancing decisions.

For more information, see [gRPC client-side load balancing](loadbalancing?view=aspnetcore-10.0).

### Proxy load balancing

An L7 (application) proxy works at a higher level than an L4 (transport) proxy. L7 proxies understand HTTP/2. The proxy receives gRPC calls multiplexed on one HTTP/2 connection and distributes them across multiple backend endpoints. Using a proxy is simpler than client-side load balancing, but adds extra latency to gRPC calls.

There are many L7 proxies available. Some options are:

* [Envoy](https://www.envoyproxy.io/) - A popular open source proxy.
* [Linkerd](https://linkerd.io/) - Service mesh for Kubernetes.
* [YARP: Yet Another Reverse Proxy](https://dotnet.github.io/yarp/) - An open source proxy written in .NET.

## Inter-process communication

gRPC calls between a client and service are usually sent over TCP sockets. TCP is great for communicating across a network, but [inter-process communication (IPC)](https://wikipedia.org/wiki/Inter-process_communication) is more efficient when the client and service are on the same machine.

Consider using a transport like Unix domain sockets or named pipes for gRPC calls between processes on the same machine. For more information, see [Inter-process communication with gRPC](interprocess?view=aspnetcore-10.0).

## Keep alive pings

Important

Keep alive pings require the cooperation of the server. Do not enable keep alive pings in the client without verifying that the server supports them. A server which does not support keep alive pings will usually ignore the first few pings, and will then send a `GOAWAY` message, closing the active HTTP/2 connection.

Keep alive pings can be used to keep HTTP/2 connections alive during periods of inactivity. Having an existing HTTP/2 connection ready when an app resumes activity allows for the initial gRPC calls to be made quickly, without a delay caused by the connection being reestablished.

Keep alive pings are configured on [SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler):

```
var handler = new SocketsHttpHandler
{
    PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
    KeepAlivePingDelay = TimeSpan.FromSeconds(60),
    KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
    EnableMultipleHttp2Connections = true
};

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
    HttpHandler = handler
});
```

The preceding code configures a channel that sends a keep alive ping to the server every 60 seconds during periods of inactivity. The ping ensures the server and any proxies in use won't close the connection because of inactivity.

Note

Keep alive pings only help keep the connection alive. Long-running gRPC calls on the connection may still be terminated by the server or intermediary proxies for inactivity.

## Flow control

HTTP/2 flow control is a feature that prevents apps from being overwhelmed with data. When using flow control:

* Each HTTP/2 connection and request has an available buffer window. The buffer window is how much data the app can receive at once.
* Flow control activates if the buffer window is filled up. When activated, the sending app pauses sending more data.
* Once the receiving app has processed data, then space in the buffer window is available. The sending app resumes sending data.

Flow control can have a negative impact on performance when receiving large messages. If the buffer window is smaller than incoming message payloads or there's latency between the client and server, then data can be sent in start/stop bursts.

Flow control performance issues can be fixed by increasing buffer window size. In Kestrel, this is configured with [InitialConnectionWindowSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialconnectionwindowsize#microsoft-aspnetcore-server-kestrel-core-http2limits-initialconnectionwindowsize) and [InitialStreamWindowSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialstreamwindowsize#microsoft-aspnetcore-server-kestrel-core-http2limits-initialstreamwindowsize) at app startup:

```
builder.WebHost.ConfigureKestrel(options =>
{
    var http2 = options.Limits.Http2;
    http2.InitialConnectionWindowSize = 1024 * 1024 * 2; // 2 MB
    http2.InitialStreamWindowSize = 1024 * 1024; // 1 MB
});
```

Recommendations:

* If a gRPC service often receives messages larger than 768 KB, Kestrel's default stream window size, then consider increasing the connection and stream window size.
* The connection window size should always be equal to or greater than the stream window size. A stream is part of the connection, and the sender is limited by both.

For more information about how flow control works, see [HTTP/2 Flow Control (blog post)](https://medium.com/coderscorner/http-2-flow-control-77e54f7fd518).

Important

Increasing Kestrel's window size allows Kestrel to buffer more data on behalf of the app, which possibly increases memory usage. Avoid configuring an unnecessarily large window size.

## Gracefully complete streaming calls

Try to complete streaming calls gracefully. Gracefully completing calls avoids unnecessary errors and allow servers to reuse internal data structures between requests.

A call is completed gracefully when the client and server have finished sending messages and the peer has read all the messages.

Client request stream:

1. The client has finished writing messages to the request stream and completes the stream with `call.RequestStream.CompleteAsync()`.
2. The server has read all messages from the request stream. Depending on how you're reading messages, either `requestStream.MoveNext()` returns `false` or `requestStream.ReadAllAsync()` has finished.

Server response stream:

1. The server has finished writing messages to the response stream and the server method has exited.
2. The client has read all messages from the response stream. Depending on how you're reading messages, either `call.ResponseStream.MoveNext()` returns `false` or `call.ResponseStream.ReadAllAsync()` has finished.

For an example of gracefully completing a bi-direction streaming call, see [making a bi-directional streaming call](client?view=aspnetcore-10.0#bi-directional-streaming-call).

Server streaming calls don't have a request stream. This means that the only way a client can communicate to the server that the stream should stop is by canceling it. If the overhead from canceled calls is impacting the app then consider changing the server streaming call to a bi-directional streaming call. In a bi-directional streaming call the client completing the request stream can be a signal to the server to end the call.

## Dispose streaming calls

Always dispose streaming calls once they're no longer needed. The type returned when starting streaming calls implements `IDisposable`. Disposing a call once it is no longer needed ensures it is stopped and all resources are cleaned up.

In the following example, the [using declaration](/en-us/dotnet/csharp/language-reference/keywords/using-directive) on the `AccumulateCount()` call ensures it's always disposed if an unexpected error occurs.

```
var client = new Counter.CounterClient(channel);
using var call = client.AccumulateCount();

for (var i = 0; i < 3; i++)
{
    await call.RequestStream.WriteAsync(new CounterRequest { Count = 1 });
}
await call.RequestStream.CompleteAsync();

var response = await call;
Console.WriteLine($"Count: {response.Count}");
// Count: 3
```

Ideally streaming calls should be [completed gracefully](#gracefully-complete-streaming-calls). Disposing the call ensures the HTTP request between the client and the server is canceled if an unexpected error occurs. Streaming calls that are accidentally left running don't just leak memory and resources on the client, but are left running on the server as well. Many leaked streaming calls could impact the stability of the app.

Disposing of a streaming call that has already completed gracefully doesn't have any negative impact.

## Replace unary calls with streaming

gRPC bidirectional streaming can be used to replace unary gRPC calls in high-performance scenarios. Once a bidirectional stream has started, streaming messages back and forth is faster than sending messages with multiple unary gRPC calls. Streamed messages are sent as data on an existing HTTP/2 request and eliminates the overhead of creating a new HTTP/2 request for each unary call.

Example service:

```
public override async Task SayHello(IAsyncStreamReader<HelloRequest> requestStream,
    IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
{
    await foreach (var request in requestStream.ReadAllAsync())
    {
        var helloReply = new HelloReply { Message = "Hello " + request.Name };

        await responseStream.WriteAsync(helloReply);
    }
}
```

Example client:

```
var client = new Greet.GreeterClient(channel);
using var call = client.SayHello();

Console.WriteLine("Type a name then press enter.");
while (true)
{
    var text = Console.ReadLine();

    // Send and receive messages over the stream
    await call.RequestStream.WriteAsync(new HelloRequest { Name = text });
    await call.ResponseStream.MoveNext();

    Console.WriteLine($"Greeting: {call.ResponseStream.Current.Message}");
}
```

Replacing unary calls with bidirectional streaming for performance reasons is an advanced technique and is not appropriate in many situations.

Using streaming calls is a good choice when:

1. High throughput or low latency is required.
2. gRPC and HTTP/2 are identified as a performance bottleneck.
3. A worker in the client is sending or receiving regular messages with a gRPC service.

Be aware of the additional complexity and limitations of using streaming calls instead of unary:

1. A stream can be interrupted by a service or connection error. Logic is required to restart stream if there is an error.
2. `RequestStream.WriteAsync` is not safe for multi-threading. Only one message can be written to a stream at a time. Sending messages from multiple threads over a single stream requires a producer/consumer queue like [Channel\<T>](/en-us/dotnet/api/system.threading.channels.channel-1) to marshall messages.
3. A gRPC streaming method is limited to receiving one type of message and sending one type of message. For example, `rpc StreamingCall(stream RequestMessage) returns (stream ResponseMessage)` receives `RequestMessage` and sends `ResponseMessage`. Protobuf's support for unknown or conditional messages using `Any` and `oneof` can work around this limitation.

## Binary payloads

Binary payloads are supported in Protobuf with the `bytes` scalar value type. A generated property in C# uses `ByteString` as the property type.

```
syntax = "proto3";

message PayloadResponse {
    bytes data = 1;
}  
```

Protobuf is a binary format that efficiently serializes large binary payloads with minimal overhead. Text based formats like JSON require [encoding bytes to base64](https://en.wikipedia.org/wiki/Base64) and add 33% to the message size.

When working with large `ByteString` payloads there are some best practices to avoid unnecessary copies and allocations that are discussed below.

### Send binary payloads

`ByteString` instances are normally created using `ByteString.CopyFrom(byte[] data)`. This method allocates a new `ByteString` and a new `byte[]`. Data is copied into the new byte array.

Additional allocations and copies can be avoided by using `UnsafeByteOperations.UnsafeWrap(ReadOnlyMemory<byte> bytes)` to create `ByteString` instances.

```
var data = await File.ReadAllBytesAsync(path);

var payload = new PayloadResponse();
payload.Data = UnsafeByteOperations.UnsafeWrap(data);
```

Bytes are not copied with `UnsafeByteOperations.UnsafeWrap` so they must not be modified while the `ByteString` is in use.

`UnsafeByteOperations.UnsafeWrap` requires [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/) version 3.15.0 or later.

### Read binary payloads

Data can be efficiently read from `ByteString` instances by using `ByteString.Memory` and `ByteString.Span` properties.

```
var byteString = UnsafeByteOperations.UnsafeWrap(new byte[] { 0, 1, 2 });
var data = byteString.Span;

for (var i = 0; i < data.Length; i++)
{
    Console.WriteLine(data[i]);
}
```

These properties allow code to read data directly from a `ByteString` without allocations or copies.

Most .NET APIs have `ReadOnlyMemory<byte>` and `byte[]` overloads, so `ByteString.Memory` is the recommended way to use the underlying data. However, there are circumstances where an app might need to get the data as a byte array. If a byte array is required then the [MemoryMarshal.TryGetArray](/en-us/dotnet/api/system.runtime.interopservices.memorymarshal.trygetarray) method can be used to get an array from a `ByteString` without allocating a new copy of the data.

```
var byteString = GetByteString();

ByteArrayContent content;
if (MemoryMarshal.TryGetArray(byteString.Memory, out var segment))
{
    // Success. Use the ByteString's underlying array.
    content = new ByteArrayContent(segment.Array, segment.Offset, segment.Count);
}
else
{
    // TryGetArray didn't succeed. Fall back to creating a copy of the data with ToByteArray.
    content = new ByteArrayContent(byteString.ToByteArray());
}

var httpRequest = new HttpRequestMessage();
httpRequest.Content = content;
```

The preceding code:

* Attempts to get an array from `ByteString.Memory` with [MemoryMarshal.TryGetArray](/en-us/dotnet/api/system.runtime.interopservices.memorymarshal.trygetarray).
* Uses the `ArraySegment<byte>` if it was successfully retrieved. The segment has a reference to the array, offset and count.
* Otherwise, falls back to allocating a new array with `ByteString.ToByteArray()`.

### gRPC services and large binary payloads

gRPC and Protobuf can send and receive large binary payloads. Although binary Protobuf is more efficient than text-based JSON at serializing binary payloads, there are still important performance characteristics to keep in mind when working with large binary payloads.

gRPC is a message-based RPC framework, which means:

* The entire message is loaded into memory before gRPC can send it.
* When the message is received, the entire message is deserialized into memory.

Binary payloads are allocated as a byte array. For example, a 10 MB binary payload allocates a 10 MB byte array. Messages with large binary payloads can allocate byte arrays on the [large object heap](/en-us/dotnet/standard/garbage-collection/large-object-heap). Large allocations impact server performance and scalability.

Advice for creating high-performance applications with large binary payloads:

* **Avoid** large binary payloads in gRPC messages. A byte array larger than 85,000 bytes is considered a large object. Keeping below that size avoids allocating on the large object heap.

* **Consider** splitting large binary payloads [using gRPC streaming](client?view=aspnetcore-10.0#client-streaming-call). Binary data is chunked and streamed over multiple messages. For more information on how to stream files, see examples in the grpc-dotnet repository:

  * [gRPC streaming file download](https://github.com/grpc/grpc-dotnet/tree/master/examples#downloader).
  * [gRPC streaming file upload](https://github.com/grpc/grpc-dotnet/tree/master/examples#uploader).

* **Consider** not using gRPC for large binary data. In ASP.NET Core, web APIs can be used alongside gRPC services. An HTTP endpoint can access the request and response stream body directly:

  * [Read the request body in a Minimal API](../fundamentals/minimal-apis?view=aspnetcore-10.0#read-the-request-body)
  * [Return stream response in a Minimal API](../fundamentals/minimal-apis?view=aspnetcore-10.0#stream)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/loadbalancing
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/loadbalancing.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC client-side load balancing

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

Client-side load balancing is a feature that allows gRPC clients to distribute load optimally across available servers. This article discusses how to configure client-side load balancing to create scalable, high-performance gRPC apps in .NET.

Client-side load balancing requires:

* .NET 5 or later.
* [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client) version 2.45.0 or later.

## Configure gRPC client-side load balancing

Client-side load balancing is configured when a channel is created. The two components to consider when using load balancing:

* The resolver, which resolves the addresses for the channel. Resolvers support getting addresses from an external source. This is also known as service discovery.
* The load balancer, which creates connections and picks the address that a gRPC call will use.

Built-in implementations of resolvers and load balancers are included in [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client). Load balancing can also be extended by [writing custom resolvers and load balancers](#write-custom-resolvers-and-load-balancers).

Addresses, connections and other load balancing state is stored in a `GrpcChannel` instance. A channel must be reused when making gRPC calls for load balancing to work correctly.

Note

Some load balancing configuration uses dependency injection (DI). Apps that don't use DI can create a [ServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollection) instance.

If an app already has DI setup, like an ASP.NET Core website, then types should be registered with the existing DI instance. `GrpcChannelOptions.ServiceProvider` is configured by getting an [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) from DI.

## Configure resolver

The resolver is configured using the address a channel is created with. The [URI scheme](https://wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax) of the address specifies the resolver.

| Scheme   | Type                    | Description                                                                                                                   |
| -------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `dns`    | `DnsResolverFactory`    | Resolves addresses by querying the hostname for [DNS address records](https://wikipedia.org/wiki/List_of_DNS_record_types#A). |
| `static` | `StaticResolverFactory` | Resolves addresses that the app has specified. Recommended if an app already knows the addresses it calls.                    |

A channel doesn't directly call a URI that matches a resolver. Instead, a matching resolver is created and used to resolve the addresses.

For example, using `GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure })`:

* The `dns` scheme maps to `DnsResolverFactory`. A new instance of a DNS resolver is created for the channel.
* The resolver makes a DNS query for `my-example-host` and gets two results: `127.0.0.100` and `127.0.0.101`.
* The load balancer uses `127.0.0.100:80` and `127.0.0.101:80` to create connections and make gRPC calls.

#### DnsResolverFactory

The `DnsResolverFactory` creates a resolver designed to get addresses from an external source. DNS resolution is commonly used to load balance over pod instances that have a [Kubernetes headless services](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services).

```
var channel = GrpcChannel.ForAddress(
    "dns:///my-example-host",
    new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new Greet.GreeterClient(channel);

var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
```

The preceding code:

* Configures the created channel with the address `dns:///my-example-host`.

  * The `dns` scheme maps to `DnsResolverFactory`.
  * `my-example-host` is the hostname to resolve.
  * No port is specified in the address, so gRPC calls are sent to port 80. This is the default port for unsecured channels. A port can optionally be specified after the hostname. For example, `dns:///my-example-host:8080` configures gRPC calls to be sent to port 8080.

* Doesn't specify a load balancer. The channel defaults to a pick first load balancer.

* Starts the gRPC call `SayHello`:

  * DNS resolver gets addresses for the hostname `my-example-host`.
  * Pick first load balancer attempts to connect to one of the resolved addresses.
  * The call is sent to the first address the channel successfully connects to.

##### DNS address caching

Performance is important when load balancing. The latency of resolving addresses is eliminated from gRPC calls by caching the addresses. A resolver will be invoked when making the first gRPC call, and subsequent calls use the cache.

Addresses are automatically refreshed if a connection is interrupted. Refreshing is important in scenarios where addresses change at runtime. For example, in Kubernetes a [restarted pod](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/) triggers the DNS resolver to refresh and get the pod's new address.

By default, a DNS resolver is refreshed if a connection is interrupted. The DNS resolver can also optionally refresh itself on a periodic interval. This can be useful for quickly detecting new pod instances.

```
services.AddSingleton<ResolverFactory>(
    sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));
```

The preceding code creates a `DnsResolverFactory` with a refresh interval and registers it with dependency injection. For more information on using a custom-configured resolver, see [Configure custom resolvers and load balancers](#configure-custom-resolvers-and-load-balancers).

#### StaticResolverFactory

A static resolver is provided by `StaticResolverFactory`. This resolver:

* Doesn't call an external source. Instead, the client app configures the addresses.
* Is designed for situations where an app already knows the addresses it calls.

```
var factory = new StaticResolverFactory(addr => new[]
{
    new BalancerAddress("localhost", 80),
    new BalancerAddress("localhost", 81)
});

var services = new ServiceCollection();
services.AddSingleton<ResolverFactory>(factory);

var channel = GrpcChannel.ForAddress(
    "static:///my-example-host",
    new GrpcChannelOptions
    {
        Credentials = ChannelCredentials.Insecure,
        ServiceProvider = services.BuildServiceProvider()
    });
var client = new Greet.GreeterClient(channel);
```

The preceding code:

* Creates a `StaticResolverFactory`. This factory knows about two addresses: `localhost:80` and `localhost:81`.

* Registers the factory with dependency injection (DI).

* Configures the created channel with:

  * The address `static:///my-example-host`. The `static` scheme maps to a static resolver.
  * Sets `GrpcChannelOptions.ServiceProvider` with the DI service provider.

This example creates a new [ServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollection) for DI. Suppose an app already has DI setup, like an ASP.NET Core website. In that case, types should be registered with the existing DI instance. `GrpcChannelOptions.ServiceProvider` is configured by getting an [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) from DI.

## Configure load balancer

A load balancer is specified in a [`service config`](https://github.com/grpc/grpc/blob/master/doc/service_config.md) using the `ServiceConfig.LoadBalancingConfigs` collection. Two load balancers are built-in and map to load balancer config names:

| Name          | Type                            | Description                                                                                                                                                                                         |
| ------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pick_first`  | `PickFirstLoadBalancerFactory`  | Attempts to connect to addresses until a connection is successfully made. gRPC calls are all made to the first successful connection.                                                               |
| `round_robin` | `RoundRobinLoadBalancerFactory` | Attempts to connect to all addresses. gRPC calls are distributed across all successful connections using [round-robin](https://www.nginx.com/resources/glossary/round-robin-load-balancing/) logic. |

`service config` is an abbreviation of service configuration and is represented by the `ServiceConfig` type. There are a couple of ways a channel can get a `service config` with a load balancer configured:

* An app can specify a `service config` when a channel is created using `GrpcChannelOptions.ServiceConfig`.
* Alternatively, a resolver can resolve a `service config` for a channel. This feature allows an external source to specify how its callers should perform load balancing. Whether a resolver supports resolving a `service config` is dependent on the resolver implementation. Disable this feature with `GrpcChannelOptions.DisableResolverServiceConfig`.
* If no `service config` is provided, or the `service config` doesn't have a load balancer configured, the channel defaults to `PickFirstLoadBalancerFactory`.

```
var channel = GrpcChannel.ForAddress(
    "dns:///my-example-host",
    new GrpcChannelOptions
    {
        Credentials = ChannelCredentials.Insecure,
        ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new RoundRobinConfig() } }
    });
var client = new Greet.GreeterClient(channel);

var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
```

The preceding code:

* Specifies a `RoundRobinLoadBalancerFactory` in the `service config`.

* Starts the gRPC call `SayHello`:

  * `DnsResolverFactory` creates a resolver that gets addresses for the hostname `my-example-host`.
  * Round-robin load balancer attempts to connect to all resolved addresses.
  * gRPC calls are distributed evenly using round-robin logic.

## Configure channel credentials

A channel must know whether gRPC calls are sent using [transport security](security?view=aspnetcore-10.0#transport-security). `http` and `https` are no longer part of the address, the scheme now specifies a resolver, so `Credentials` must be configured on channel options when using load balancing.

* `ChannelCredentials.SecureSsl` - gRPC calls are secured with [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246). Equivalent to an `https` address.
* `ChannelCredentials.Insecure` - gRPC calls don't use transport security. Equivalent to an `http` address.

```
var channel = GrpcChannel.ForAddress(
    "dns:///my-example-host",
    new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new Greet.GreeterClient(channel);

var response = await client.SayHelloAsync(new HelloRequest { Name = "world" });
```

## Use load balancing with gRPC client factory

[gRPC client factory](clientfactory?view=aspnetcore-10.0) can be configured to use load balancing:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("dns:///my-example-host");
    })
    .ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);

builder.Services.AddSingleton<ResolverFactory>(
    sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));

var app = builder.Build();
```

The preceding code:

* Configures the client with a load-balancing address.
* Specifies channel credentials.
* Registers DI types with the app's [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection).

## Write custom resolvers and load balancers

Client-side load balancing is extensible:

* Implement `Resolver` to create a custom resolver and resolve addresses from a new data source.
* Implement `LoadBalancer` to create a custom load balancer with new load balancing behavior.

Important

The APIs used to extend client-side load balancing are experimental. They can change without notice.

### Create a custom resolver

A resolver:

* Implements `Resolver` and is created by a `ResolverFactory`. Create a custom resolver by implementing these types.
* Is responsible for resolving the addresses a load balancer uses.
* Can optionally provide a service configuration.

```
public class FileResolver : PollingResolver
{
    private readonly Uri _address;
    private readonly int _port;

    public FileResolver(Uri address, int defaultPort, ILoggerFactory loggerFactory)
        : base(loggerFactory)
    {
        _address = address;
        _port = defaultPort;
    }

    public override async Task ResolveAsync(CancellationToken cancellationToken)
    {
        // Load JSON from a file on disk and deserialize into endpoints.
        var jsonString = await File.ReadAllTextAsync(_address.LocalPath);
        var results = JsonSerializer.Deserialize<string[]>(jsonString);
        var addresses = results.Select(r => new BalancerAddress(r, _port)).ToArray();

        // Pass the results back to the channel.
        Listener(ResolverResult.ForResult(addresses));
    }
}

public class FileResolverFactory : ResolverFactory
{
    // Create a FileResolver when the URI has a 'file' scheme.
    public override string Name => "file";

    public override Resolver Create(ResolverOptions options)
    {
        return new FileResolver(options.Address, options.DefaultPort, options.LoggerFactory);
    }
}
```

In the preceding code:

* `FileResolverFactory` implements `ResolverFactory`. It maps to the `file` scheme and creates `FileResolver` instances.

* `FileResolver` implements `PollingResolver`. `PollingResolver` is an abstract base type that makes it easy to implement a resolver with asynchronous logic by overriding `ResolveAsync`.

* In `ResolveAsync`:

  * The file URI is converted to a local path. For example, `file:///c:/addresses.json` becomes `c:\addresses.json`.
  * JSON is loaded from disk and converted into a collection of addresses.
  * Listener is called with results to let the channel know that addresses are available.

### Create a custom load balancer

A load balancer:

* Implements `LoadBalancer` and is created by a `LoadBalancerFactory`. Create a custom load balancer and factory by implementing these types.
* Is given addresses from a resolver and creates `Subchannel` instances.
* Tracks state about the connection and creates a `SubchannelPicker`. The channel internally uses the picker to pick addresses when making gRPC calls.

The `SubchannelsLoadBalancer` is:

* An abstract base class that implements `LoadBalancer`.
* Manages creating `Subchannel` instances from addresses.
* Makes it easy to implement a custom picking policy over a collection of subchannels.

```
public class RandomBalancer : SubchannelsLoadBalancer
{
    public RandomBalancer(IChannelControlHelper controller, ILoggerFactory loggerFactory)
        : base(controller, loggerFactory)
    {
    }

    protected override SubchannelPicker CreatePicker(List<Subchannel> readySubchannels)
    {
        return new RandomPicker(readySubchannels);
    }

    private class RandomPicker : SubchannelPicker
    {
        private readonly List<Subchannel> _subchannels;

        public RandomPicker(List<Subchannel> subchannels)
        {
            _subchannels = subchannels;
        }

        public override PickResult Pick(PickContext context)
        {
            // Pick a random subchannel.
            return PickResult.ForSubchannel(_subchannels[Random.Shared.Next(0, _subchannels.Count)]);
        }
    }
}

public class RandomBalancerFactory : LoadBalancerFactory
{
    // Create a RandomBalancer when the name is 'random'.
    public override string Name => "random";

    public override LoadBalancer Create(LoadBalancerOptions options)
    {
        return new RandomBalancer(options.Controller, options.LoggerFactory);
    }
}
```

In the preceding code:

* `RandomBalancerFactory` implements `LoadBalancerFactory`. It maps to the `random` policy name and creates `RandomBalancer` instances.
* `RandomBalancer` implements `SubchannelsLoadBalancer`. It creates a `RandomPicker` that randomly picks a subchannel.

## Configure custom resolvers and load balancers

Custom resolvers and load balancers need to be registered with dependency injection (DI) when they are used. There are a couple of options:

* If an app is already using DI, such as an ASP.NET Core web app, they can be registered with the existing DI configuration. An [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) can be resolved from DI and passed to the channel using `GrpcChannelOptions.ServiceProvider`.

* If an app isn't using DI then create:

  * A [ServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollection) with types registered with it.
  * A service provider using [BuildServiceProvider](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectioncontainerbuilderextensions.buildserviceprovider).

```
var services = new ServiceCollection();
services.AddSingleton<ResolverFactory, FileResolverFactory>();
services.AddSingleton<LoadBalancerFactory, RandomLoadBalancerFactory>();

var channel = GrpcChannel.ForAddress(
    "file:///c:/data/addresses.json",
    new GrpcChannelOptions
    {
        Credentials = ChannelCredentials.Insecure,
        ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new LoadBalancingConfig("random") } },
        ServiceProvider = services.BuildServiceProvider()
    });
var client = new Greet.GreeterClient(channel);
```

The preceding code:

* Creates a `ServiceCollection` and registers new resolver and load balancer implementations.

* Creates a channel configured to use the new implementations:

  * `ServiceCollection` is built into an `IServiceProvider` and set to `GrpcChannelOptions.ServiceProvider`.
  * Channel address is `file:///c:/data/addresses.json`. The `file` scheme maps to `FileResolverFactory`.
  * `service config` load balancer name is `random`. Maps to `RandomLoadBalancerFactory`.

## Why load balancing is important

HTTP/2 multiplexes multiple calls on a single TCP connection. If gRPC and HTTP/2 are used with a network load balancer (NLB), the connection is forwarded to a server, and all gRPC calls are sent to that one server. The other server instances on the NLB are idle.

Network load balancers are a common solution for load balancing because they are fast and lightweight. For example, Kubernetes by default uses a network load balancer to balance connections between pod instances. However, network load balancers are not effective at distributing load when used with gRPC and HTTP/2.

### Proxy or client-side load balancing?

gRPC and HTTP/2 can be effectively load balanced using either an application load balancer proxy or client-side load balancing. Both of these options allow individual gRPC calls to be distributed across available servers. Deciding between proxy and client-side load balancing is an architectural choice. There are pros and cons for each.

* **Proxy**: gRPC calls are sent to the proxy, the proxy makes a load balancing decision, and the gRPC call is sent on to the final endpoint. The proxy is responsible for knowing about endpoints. Using a proxy adds:

  * An additional network hop to gRPC calls.
  * Latency and consumes additional resources.
  * Proxy server must be setup and configured correctly.

* **Client-side load balancing**: The gRPC client makes a load balancing decision when a gRPC call is started. The gRPC call is sent directly to the final endpoint. When using client-side load balancing:

  * The client is responsible for knowing about available endpoints and making load balancing decisions.
  * Additional client configuration is required.
  * High-performance, load balanced gRPC calls eliminate the need for a proxy.

## Related content

* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/owin
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/owin.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# OWIN integration

The `Microsoft.AspNetCore.SystemWebAdapters.Owin` library enables ASP.NET Core applications to use OWIN middleware during migration from ASP.NET Framework. This integration is valuable when migrating applications that rely on OWIN-based middleware, particularly for authentication, authorization, or custom pipeline components.

Warning

When using OWIN integration, you may need to suppress build warnings for .NET Framework package dependencies. Add the following to your project file:

```
<PropertyGroup>
  <NoWarn>$(NoWarn);NU1701</NoWarn>
</PropertyGroup>
```

Running .NET Framework code on .NET Core is supported as long as the APIs used exist and have the same behavior. It is recommended to use this as part of the migration process, but to not stay in this state for too long.

Some packages will need to be manually updated to supported version if used. These include:

* EntityFramework must be >= 6.5.1 for best support on .NET Core

## Why use OWIN integration

OWIN integration provides several benefits during migration:

* **Preserve existing middleware**: Reuse OWIN middleware from your ASP.NET Framework application without immediate rewrites
* **Gradual migration**: Migrate middleware components incrementally while maintaining application functionality
* **Authentication continuity**: Maintain authentication state and share cookies between ASP.NET Framework and ASP.NET Core applications during migration
* **Familiar patterns**: Continue using OWIN APIs and patterns that your team already understands

## Integration patterns

The library provides three distinct integration patterns, each suited for different migration scenarios:

1. **OWIN pipeline as main pipeline middleware**: Add OWIN middleware directly to the ASP.NET Core middleware pipeline
2. **OWIN pipeline as authentication handler**: Use OWIN middleware as an ASP.NET Core authentication handler
3. **OWIN pipeline in emulated HttpApplication events**: Run OWIN middleware within the emulated `HttpApplication` pipeline

## OWIN pipeline as middleware

This pattern incorporates OWIN middleware into the ASP.NET Core middleware pipeline without using emulated `HttpApplication` events.

### When to use this pattern

Use this approach when:

* You want standard middleware pipeline behavior
* Your application doesn't rely on `HttpApplication` events
* You're gradually migrating OWIN middleware to ASP.NET Core patterns

### Setup

Add OWIN middleware to the main pipeline:

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UseOwin(owinApp =>
{
    owinApp.UseMyOwinMiddleware();
});

app.UseRouting();
app.MapControllers();

app.Run();
```

### Access services

Access services when configuring OWIN middleware:

```
app.UseOwin((owinApp, services) =>
{
    var configuration = services.GetRequiredService<IConfiguration>();
    owinApp.UseMyOwinMiddleware(configuration);
});
```

## OWIN authentication handler

This pattern uses OWIN middleware as an ASP.NET Core authentication handler, integrating with the `AuthenticationBuilder` API.

### When to use this pattern

Use this approach when:

* Migrating authentication logic from ASP.NET Framework
* Sharing authentication cookies between ASP.NET Framework and ASP.NET Core applications
* Using OWIN authentication middleware such as cookie authentication or OAuth providers
* Working with ASP.NET Framework Identity

### Setup

Register OWIN authentication as an ASP.NET Core authentication handler:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddOwinAuthentication((owinApp, services) =>
    {
        owinApp.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login")
        });
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// Additional middleware or endpoint mapping

app.Run();
```

### Custom authentication scheme

Specify a custom authentication scheme name:

```
builder.Services
    .AddAuthentication()
    .AddOwinAuthentication("MyOwinScheme", (owinApp, services) =>
    {
        owinApp.UseMyOwinAuthenticationMiddleware();
    });
```

When no scheme name is provided, the authentication handler uses `OwinAuthenticationDefaults.AuthenticationScheme` as the default scheme. The authentication scheme name determines:

* **Default authentication**: If set as the default scheme, it automatically authenticates requests without requiring an `[Authorize]` attribute
* **Challenge behavior**: Controls which handler responds to authentication challenges (such as redirecting to a login page)
* **Authentication selection**: Allows you to explicitly authenticate against a specific scheme using `HttpContext.AuthenticateAsync("scheme-name")`

For applications with multiple authentication schemes, you can configure the default authentication and challenge schemes:

```
builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = "MyOwinScheme";
        options.DefaultChallengeScheme = "MyOwinScheme";
    })
    .AddOwinAuthentication("MyOwinScheme", (owinApp, services) =>
    {
        owinApp.UseMyOwinAuthenticationMiddleware();
    });
```

For more information about authentication schemes and how they work in ASP.NET Core, see [Overview of ASP.NET Core Authentication](../../../security/authentication/?view=aspnetcore-10.0) and [Use cookie authentication without ASP.NET Core Identity](../../../security/authentication/cookie?view=aspnetcore-10.0).

### Access OWIN authentication

Continue using the OWIN `IAuthenticationManager` interface:

```
var authManager = HttpContext.GetOwinContext().Authentication;

authManager.SignIn(identity);
authManager.SignOut();
```

### [ClaimsPrincipal.Current](/en-us/dotnet/api/system.security.claims.claimsprincipal.current#system-security-claims-claimsprincipal-current)

If code is expecting to use [ClaimsPrincipal.Current](/en-us/dotnet/api/system.security.claims.claimsprincipal.current#system-security-claims-claimsprincipal-current), then see [Migrate from static ClaimsPrincipal access](claimsprincipal-current?view=aspnetcore-10.0) for options to enable setting that property.

## Migrating ASP.NET Framework Identity

A common migration scenario involves ASP.NET Framework Identity with OWIN cookie authentication. This example shows how to maintain compatibility during migration.

### Configure data protection

Configure data protection to match ASP.NET Framework settings for cookie sharing:

```
var builder = WebApplication.CreateBuilder(args);

var sharedApplicationName = "CommonMvcAppName";
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Path.GetTempPath(), "sharedkeys", sharedApplicationName)))
    .SetApplicationName(sharedApplicationName);
```

### Configure OWIN authentication

Set up OWIN authentication with Identity services:

```
builder.Services
    .AddAuthentication()
    .AddOwinAuthentication("AppAuthenticationScheme", (app, services) =>
    {
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

        var dataProtector = services.GetDataProtector(
            "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
            "AppAuthenticationScheme",
            "v2");

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider
            {
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
            },
            CookieName = ".AspNet.ApplicationCookie",
            TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector))
        });
    });
```

### Use in controllers

Access OWIN-registered services in controllers:

```
using Microsoft.AspNet.Identity.Owin;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SystemWebAdapters;
using Microsoft.Owin.Security;

[Authorize]
public class AccountController : Controller
{
    public ApplicationSignInManager SignInManager =>
        HttpContext.GetOwinContext().Get<ApplicationSignInManager>();

    public ApplicationUserManager UserManager =>
        HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();

    private IAuthenticationManager AuthenticationManager =>
        HttpContext.GetOwinContext().Authentication;

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        var result = await SignInManager.PasswordSignInAsync(
            model.Email, 
            model.Password, 
            model.RememberMe, 
            shouldLockout: false);

        if (result == SignInStatus.Success)
        {
            return RedirectToAction("Index", "Home");
        }

        ModelState.AddModelError("", "Invalid login attempt.");
        return View(model);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Logout()
    {
        AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
        return RedirectToAction("Index", "Home");
    }
}
```

### Key configuration points

When migrating ASP.NET Framework Identity:

* **Data protection**: Configure the same `ApplicationName` and key storage location in both ASP.NET Framework and ASP.NET Core
* **Per-request services**: Use `app.CreatePerOwinContext<T>()` to register services created once per request
* **Data protection bridge**: Use `AspNetTicketDataFormat` with `DataProtectorShim` to bridge ASP.NET Core's `IDataProtector` to OWIN
* **Service access**: Access OWIN-registered services through `HttpContext.GetOwinContext()`
* **Cookie names**: Ensure the `CookieName` matches between applications when sharing authentication state

## OWIN pipeline in HttpApplication events

This pattern runs OWIN middleware within the emulated `HttpApplication` event pipeline, similar to how OWIN works in ASP.NET Framework's integrated pipeline mode.

### When to use this pattern

Use this approach when:

* Your application relies on the ASP.NET Framework integrated pipeline
* OWIN middleware needs to execute at specific `HttpApplication` event stages
* You're migrating applications that use both `HttpApplication` events and OWIN together

### Setup

Configure the OWIN pipeline to run within `HttpApplication` events:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddSystemWebAdapters()
    .AddOwinApp(app =>
    {
        app.UseMyOwinMiddleware();
        app.UseStageMarker(PipelineStage.Authenticate);
    });

var app = builder.Build();

app.UseSystemWebAdapters();
app.Run();
```

### Access services

Access the `IServiceProvider` when configuring the OWIN pipeline:

```
builder.Services
    .AddSystemWebAdapters()
    .AddOwinApp((app, services) =>
    {
        var configuration = services.GetRequiredService<IConfiguration>();
        app.UseMyOwinMiddleware(configuration);
        app.UseStageMarker(PipelineStage.Authenticate);
    });
```

### HttpApplication event mapping

The OWIN pipeline integrates with these `HttpApplication` events:

* `AuthenticateRequest` / `PostAuthenticateRequest`
* `AuthorizeRequest` / `PostAuthorizeRequest`
* `ResolveRequestCache` / `PostResolveRequestCache`
* `MapRequestHandler` / `PostMapRequestHandler`
* `AcquireRequestState` / `PostAcquireRequestState`
* `PreRequestHandlerExecute`

Use `.UseStageMarker(PipelineStage)` to control when OWIN middleware executes relative to these events.

## Migration strategy

When incorporating OWIN middleware into your ASP.NET Core application:

1. **Identify OWIN dependencies**: Determine which OWIN middleware your application uses
2. **Choose integration pattern**: Select the appropriate pattern based on your application's needs
3. **Configure data protection**: Set up shared data protection for authentication cookie sharing
4. **Test authentication flow**: Verify authentication works correctly in the ASP.NET Core application
5. **Gradual conversion**: Plan to migrate OWIN middleware to native ASP.NET Core middleware over time
6. **Monitor compatibility**: Ensure OWIN middleware behavior matches expectations during migration

## Related content

* [ASP.NET Framework to Core Authentication Migration](authentication?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../../../fundamentals/middleware/?view=aspnetcore-10.0)
* [Overview of ASP.NET Core Authentication](../../../security/authentication/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/out-of-process-hosting
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/out-of-process-hosting.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Out-of-process hosting with IIS and ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        ...
```

For more information on `CreateDefaultBuilder`, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0#default-builder-settings).

**Out-of-process hosting model**

To configure IIS options, include a service configuration for [IISOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisoptions) in [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartup.configureservices). The following example prevents the app from populating `HttpContext.Connection.ClientCertificate`:

```
services.Configure<IISOptions>(options => 
{
    options.ForwardClientCertificate = false;
});
```

| Option                      | Default | Setting                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AutomaticAuthentication`   | `true`  | If `true`, [IIS integration middleware](#enable-the-iisintegration-components) sets the `HttpContext.User` authenticated by [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0). If `false`, the middleware only provides an identity for `HttpContext.User` and responds to challenges when explicitly requested by the `AuthenticationScheme`. Windows Authentication must be enabled in IIS for `AutomaticAuthentication` to function. For more information, see the [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0) topic. |
| `AuthenticationDisplayName` | `null`  | Sets the display name shown to users on login pages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `ForwardClientCertificate`  | `true`  | If `true` and the `MS-ASPNETCORE-CLIENTCERT` request header is present, the `HttpContext.Connection.ClientCertificate` is populated.                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |

### Proxy server and load balancer scenarios

The [IIS integration middleware](#enable-the-iisintegration-components) and the ASP.NET Core Module are configured to forward the:

* Scheme (HTTP/HTTPS).
* Remote IP address where the request originated.

The [IIS integration middleware](#enable-the-iisintegration-components) configures forwarded headers middleware.

Additional configuration might be required for apps hosted behind additional proxy servers and load balancers. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0).

### Out-of-process hosting model

To configure an app for out-of-process hosting, set the value of the `<AspNetCoreHostingModel>` property to `OutOfProcess` in the project file (`.csproj`):

```
<PropertyGroup>
  <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>
```

In-process hosting is set with `InProcess`, which is the default value.

The value of `<AspNetCoreHostingModel>` is case insensitive, so `inprocess` and `outofprocess` are valid values.

[Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) server is used instead of IIS HTTP Server (`IISHttpServer`).

For out-of-process, [`CreateDefaultBuilder`](../../fundamentals/host/generic-host?view=aspnetcore-10.0#default-builder-settings) calls [UseIISIntegration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiisintegration) to:

* Configure the port and base path the server should listen on when running behind the ASP.NET Core Module.
* Configure the host to capture startup errors.

### Process name

`Process.GetCurrentProcess().ProcessName` reports `w3wp`/`iisexpress` (in-process) or `dotnet` (out-of-process).

Many native modules, such as Windows Authentication, remain active. To learn more about IIS modules active with the ASP.NET Core Module, see [IIS modules with ASP.NET Core](modules?view=aspnetcore-10.0).

The ASP.NET Core Module can also:

* Set environment variables for the worker process.
* Log stdout output to file storage for troubleshooting startup issues.
* Forward Windows authentication tokens.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-management
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/implementation/key-management.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Key management in ASP.NET Core

[]()

The data protection system automatically manages the lifetime of master keys used to protect and unprotect payloads. Each key can exist in one of four stages:

* Created - the key exists in the key ring but has not yet been activated. The key shouldn't be used for new Protect operations until sufficient time has elapsed that the key has had a chance to propagate to all machines that are consuming this key ring.

* Active - the key exists in the key ring and should be used for all new Protect operations.

* Expired - the key has run its natural lifetime and should no longer be used for new Protect operations.

* Revoked - the key is compromised and must not be used for new Protect operations.

Created, active, and expired keys may all be used to unprotect incoming payloads. Revoked keys by default may not be used to unprotect payloads, but the application developer can [override this behavior](../consumer-apis/dangerous-unprotect?view=aspnetcore-10.0#data-protection-consumer-apis-dangerous-unprotect) if necessary.

Warning

The developer might be tempted to delete a key from the key ring (for example by deleting the corresponding file from the file system). At that point, all data protected by the key is permanently undecipherable, and there's no emergency override like there's with revoked keys. Deleting a key is truly destructive behavior.

## Default key selection

When the data protection system reads the key ring from the backing repository, it will attempt to locate a "default" key from the key ring. The default key is used for new Protect operations.

The general heuristic is that the data protection system chooses the key with the most recent activation date as the default key. (There's a small fudge factor to allow for server-to-server clock skew.) If the key is expired or revoked, and if the application has not disabled automatic key generation, then a new key will be generated with immediate activation per the [key expiration and rolling](key-management?view=aspnetcore-10.0#data-protection-implementation-key-management-expiration) policy below.

The reason the data protection system generates a new key immediately rather than falling back to a different key is that new key generation should be treated as an implicit expiration of all keys that were activated prior to the new key. The general idea is that new keys may have been configured with different algorithms or encryption-at-rest mechanisms than old keys, and the system should prefer the current configuration over falling back.

There's an exception. If the application developer has [disabled automatic key generation](../configuration/overview?view=aspnetcore-10.0#disableautomatickeygeneration), then the data protection system must choose something as the default key. In this fallback scenario, the system will choose the non-revoked key with the most recent activation date, with preference given to keys that have had time to propagate to other machines in the cluster. The fallback system may end up choosing an expired default key as a result. The fallback system will never choose a revoked key as the default key, and if the key ring is empty or every key has been revoked then the system will produce an error upon initialization.

[]()

## Key expiration and rolling

When a key is created, it's automatically given an activation date of { now + 2 days } and an expiration date of { now + 90 days }. The 2-day delay before activation gives the key time to propagate through the system. That is, it allows other applications pointing at the backing store to observe the key at their next auto-refresh period, thus maximizing the chances that when the key ring does become active it has propagated to all applications that might need to use it.

If the default key will expire within 2 days and if the key ring doesn't already have a key that will be active upon expiration of the default key, then the data protection system will automatically persist a new key to the key ring. This new key has an activation date of { default key's expiration date } and an expiration date of { now + 90 days }. This allows the system to automatically roll keys on a regular basis with no interruption of service.

There might be circumstances where a key will be created with immediate activation. One example would be when the application hasn't run for a time and all keys in the key ring are expired. When this happens, the key is given an activation date of { now } without the normal 2-day activation delay.

The default key lifetime is 90 days, though this is configurable as in the following example.

```
services.AddDataProtection()
       // use 14-day lifetime instead of 90-day lifetime
       .SetDefaultKeyLifetime(TimeSpan.FromDays(14));
```

An administrator can also change the default system-wide, though an explicit call to `SetDefaultKeyLifetime` will override any system-wide policy. The default key lifetime cannot be shorter than 7 days.

## Automatic key ring refresh

When the data protection system initializes, it reads the key ring from the underlying repository and caches it in memory. This cache allows Protect and Unprotect operations to proceed without hitting the backing store. The system will automatically check the backing store for changes approximately every 24 hours or when the current default key expires, whichever comes first.

Warning

Developers should very rarely (if ever) need to use the key management APIs directly. The data protection system will perform automatic key management as described above.

The data protection system exposes an interface `IKeyManager` that can be used to inspect and make changes to the key ring. The DI system that provided the instance of `IDataProtectionProvider` can also provide an instance of `IKeyManager` for your consumption. Alternatively, you can pull the `IKeyManager` straight from the `IServiceProvider` as in the example below.

Any operation which modifies the key ring (creating a new key explicitly or performing a revocation) will invalidate the in-memory cache in the process performing that operation. It doesn't automatically invalidate caches in other processes sharing the repository. The next call to `Protect` or `Unprotect` will cause the data protection system to reread the key ring and recreate the cache.

Important

In a multi-instance deployment, Data Protection key-ring caches are maintained independently by each process. Revoking a key in one instance updates the shared key repository and invalidates that instance's cache, but it doesn't immediately invalidate caches in other running instances.

Other instances normally observe the revocation during their next key-ring refresh or another local refresh event. Until then, a warmed instance might continue to unprotect data created with the revoked key or use that key for new protection operations.

If revocation must take effect immediately across the deployment, persist the revocation and then restart or recycle every application instance, or use an application-specific mechanism that causes every instance to reload its key ring. Verify that all instances have refreshed before considering revocation complete. Plan for the availability impact of coordinated restarts.

The sample below demonstrates using the `IKeyManager` interface to inspect and manipulate the key ring, including revoking existing keys and generating a new key manually.

```
using System;
using System.IO;
using System.Threading;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;

public class Program
{
    public static void Main(string[] args)
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddDataProtection()
            // point at a specific folder and use DPAPI to encrypt keys
            .PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys"))
            .ProtectKeysWithDpapi();
        var services = serviceCollection.BuildServiceProvider();

        // perform a protect operation to force the system to put at least
        // one key in the key ring
        services.GetDataProtector("Sample.KeyManager.v1").Protect("payload");
        Console.WriteLine("Performed a protect operation.");
        Thread.Sleep(2000);

        // get a reference to the key manager
        var keyManager = services.GetService<IKeyManager>();

        // list all keys in the key ring
        var allKeys = keyManager.GetAllKeys();
        Console.WriteLine($"The key ring contains {allKeys.Count} key(s).");
        foreach (var key in allKeys)
        {
            Console.WriteLine($"Key {key.KeyId:B}: Created = {key.CreationDate:u}, IsRevoked = {key.IsRevoked}");
        }

        // revoke all keys in the key ring
        keyManager.RevokeAllKeys(DateTimeOffset.Now, reason: "Revocation reason here.");
        Console.WriteLine("Revoked all existing keys.");

        // add a new key to the key ring with immediate activation and a 1-month expiration
        keyManager.CreateNewKey(
            activationDate: DateTimeOffset.Now,
            expirationDate: DateTimeOffset.Now.AddMonths(1));
        Console.WriteLine("Added a new key.");

        // list all keys in the key ring
        allKeys = keyManager.GetAllKeys();
        Console.WriteLine($"The key ring contains {allKeys.Count} key(s).");
        foreach (var key in allKeys)
        {
            Console.WriteLine($"Key {key.KeyId:B}: Created = {key.CreationDate:u}, IsRevoked = {key.IsRevoked}");
        }
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Performed a protect operation.
 * The key ring contains 1 key(s).
 * Key {1b948618-be1f-440b-b204-64ff5a152552}: Created = 2015-03-18 22:20:49Z, IsRevoked = False
 * Revoked all existing keys.
 * Added a new key.
 * The key ring contains 2 key(s).
 * Key {1b948618-be1f-440b-b204-64ff5a152552}: Created = 2015-03-18 22:20:49Z, IsRevoked = True
 * Key {2266fc40-e2fb-48c6-8ce2-5fde6b1493f7}: Created = 2015-03-18 22:20:51Z, IsRevoked = False
 */
```

## Key storage

The data protection system has a heuristic whereby it attempts to deduce an appropriate key storage location and encryption-at-rest mechanism automatically. The key persistence mechanism is also configurable by the app developer. The following documents discuss the in-box implementations of these mechanisms:

* [Key storage providers in ASP.NET Core](key-storage-providers?view=aspnetcore-10.0)
* [Key encryption at rest in Windows and Azure using ASP.NET Core](key-encryption-at-rest?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/health-checks.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Health checks in ASP.NET Core

By [Glenn Condron](https://github.com/glennc) and [Juergen Gutsch](https://twitter.com/sharpcms)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

ASP.NET Core offers health checks middleware and libraries for reporting the health of app infrastructure components.

Health checks are exposed by an app as HTTP endpoints. Health check endpoints can be configured for various real-time monitoring scenarios:

* Health probes can be used by container orchestrators and load balancers to check an app's status. For example, a container orchestrator may respond to a failing health check by halting a rolling deployment or restarting a container. A load balancer might react to an unhealthy app by routing traffic away from the failing instance to a healthy instance.
* Use of memory, disk, and other physical server resources can be monitored for healthy status.
* Health checks can test an app's dependencies, such as databases and external service endpoints, to confirm availability and normal functioning.

Health checks are typically used with an external monitoring service or container orchestrator to check the status of an app. Before adding health checks to an app, decide on which monitoring system to use. The monitoring system dictates what types of health checks to create and how to configure their endpoints.

## Basic health probe

For many apps, a basic health probe configuration that reports the app's availability to process requests (*liveness*) is sufficient to discover the status of the app.

The basic configuration registers health check services and calls the health checks middleware to respond at a URL endpoint with a health response. By default, no specific health checks are registered to test any particular dependency or subsystem. The app is considered healthy if it can respond at the health endpoint URL. The default response writer writes [HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus) as a plaintext response to the client. The `HealthStatus` is [HealthStatus.Healthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-healthy), [HealthStatus.Degraded](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-degraded), or [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy).

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Program.cs`. Create a health check endpoint by calling [MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks).

The following example creates a health check endpoint at `/healthz`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/healthz");

app.Run();
```

### Docker `HEALTHCHECK`

[Docker](docker/?view=aspnetcore-10.0) includes a built-in `HEALTHCHECK` directive that you can use to check the status of an app that uses the basic health check configuration:

```
HEALTHCHECK CMD curl --fail http://localhost:5000/healthz || exit 1
```

The preceding example uses `curl` to make an HTTP request to the health check endpoint at `/healthz`. `curl` isn't included in the .NET Linux container images, but you can add it by installing the required package in the Dockerfile. Containers that use images based on Alpine Linux can use the included `wget` in place of `curl`.

## Create health checks

Create health checks by implementing the [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) interface. The [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) method returns a [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) that indicates the health as `Healthy`, `Degraded`, or `Unhealthy`. The result is written as a plaintext response with a configurable status code. See the [Health check options](#health-check-options) section. [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) can also return optional key-value pairs.

The following example demonstrates the layout of a health check:

```
public class SampleHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        var isHealthy = true;

        // ...

        if (isHealthy)
        {
            return Task.FromResult(
                HealthCheckResult.Healthy("A healthy result."));
        }

        return Task.FromResult(
            new HealthCheckResult(
                context.Registration.FailureStatus, "An unhealthy result."));
    }
}
```

The health check's logic is placed in the [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) method. The preceding example sets a dummy variable, `isHealthy`, to `true`. If the value of `isHealthy` is set to `false`, the [HealthCheckRegistration.FailureStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.failurestatus#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-failurestatus) status is returned.

If [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) throws an exception during the check, a new [HealthReportEntry](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry) is returned with its [HealthReportEntry.Status](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry.status#microsoft-extensions-diagnostics-healthchecks-healthreportentry-status) set to the [FailureStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.failurestatus#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-failurestatus). This status is defined by [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) (see the [Register health check services](#register-health-check-services) section) and includes the [inner exception](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry.exception#microsoft-extensions-diagnostics-healthchecks-healthreportentry-exception) that caused the check failure. The [Description](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry.description#microsoft-extensions-diagnostics-healthchecks-healthreportentry-description) is set to the exception's message.

## Register health check services

To register a health check service, call [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) in `Program.cs`:

```
builder.Services.AddHealthChecks()
    .AddCheck<SampleHealthCheck>("Sample");
```

The [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) overload shown in the following example sets the failure status ([HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus)) to report when the health check reports a failure. If the failure status is set to `null` (default), [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) is reported. This overload is a useful scenario for library authors, where the failure status indicated by the library is enforced by the app when a health check failure occurs if the health check implementation honors the setting.

*Tags* can be used to filter health checks. Tags are described in the [Filter health checks](#filter-health-checks) section.

```
builder.Services.AddHealthChecks()
    .AddCheck<SampleHealthCheck>(
        "Sample",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "sample" });
```

[AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderdelegateextensions.addcheck) can also execute a lambda function. In the following example, the health check always returns a healthy result:

```
builder.Services.AddHealthChecks()
    .AddCheck("Sample", () => HealthCheckResult.Healthy("A healthy result."));
```

Call [AddTypeActivatedCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addtypeactivatedcheck) to pass arguments to a health check implementation. In the following example, a type-activated health check accepts an integer and a string in its constructor:

```
public class SampleHealthCheckWithArgs : IHealthCheck
{
    private readonly int _arg1;
    private readonly string _arg2;

    public SampleHealthCheckWithArgs(int arg1, string arg2)
        => (_arg1, _arg2) = (arg1, arg2);

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        // ...

        return Task.FromResult(HealthCheckResult.Healthy("A healthy result."));
    }
}
```

To register the preceding health check, call `AddTypeActivatedCheck` with the integer and string passed as arguments:

```
builder.Services.AddHealthChecks()
    .AddTypeActivatedCheck<SampleHealthCheckWithArgs>(
        "Sample",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "sample" },
        args: new object[] { 1, "Arg" });
```

## Use Health Checks Routing

In `Program.cs`, call `MapHealthChecks` on the endpoint builder with the endpoint URL or relative path:

```
app.MapHealthChecks("/healthz");
```

### Require host

Call `RequireHost` to specify one or more permitted hosts for the health check endpoint. Use Unicode rather than punycode for hosts, and you can include a port. If you don't supply a collection, any host is accepted:

```
app.MapHealthChecks("/healthz")
    .RequireHost("www.contoso.com:5001");
```

To restrict the health check endpoint to respond only on a specific port, specify a port in the call to `RequireHost`. Typically, use this approach in a container environment to expose a port for monitoring services:

```
app.MapHealthChecks("/healthz")
    .RequireHost("*:5001");
```

Warning

API that relies on the [Host header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Host), such as [HttpRequest.Host](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.host) and [RequireHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.requirehost), are subject to potential spoofing by clients.

To prevent host and port spoofing, use one of the following approaches:

* Use [HttpContext.Connection](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.connection) ([ConnectionInfo.LocalPort](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.localport#microsoft-aspnetcore-http-connectioninfo-localport)) where the ports are checked.
* Employ [Host filtering](../fundamentals/servers/kestrel/host-filtering?view=aspnetcore-10.0).

To prevent unauthorized clients from spoofing the port, call [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization):

```
app.MapHealthChecks("/healthz")
    .RequireHost("*:5001")
    .RequireAuthorization();
```

For more information, see [Host matching in routes with RequireHost](../fundamentals/routing?view=aspnetcore-10.0#host-matching-in-routes-with-requirehost).

### Require authorization

Call [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) to run authorization middleware on the health check request endpoint. A `RequireAuthorization` overload accepts one or more authorization policies. If a policy isn't provided, the default authorization policy is used:

```
app.MapHealthChecks("/healthz")
    .RequireAuthorization();
```

### Enable Cross-Origin Requests (CORS)

Although running health checks manually from a browser isn't a common scenario, CORS middleware can be enabled by calling [`RequireCors`](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsEndpointConventionBuilderExtensions.cs#L20-#L40) on the health checks endpoints. The [`RequireCors`](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) overload accepts a CORS policy builder delegate (`CorsPolicyBuilder`) or a policy name. For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0).

## Health check options

[HealthCheckOptions](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions) provide an opportunity to customize health check behavior:

* [Filter health checks](#filter-health-checks)
* [Customize the HTTP status code](#customize-the-http-status-code)
* [Suppress cache headers](#suppress-cache-headers)
* [Customize output](#customize-output)

### Filter health checks

By default, the health checks middleware runs all registered health checks. To run a subset of health checks, provide a function that returns a boolean to the [Predicate](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.predicate#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-predicate) option.

The following example filters the health checks so that only those tagged with `sample` run:

```
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    Predicate = healthCheck => healthCheck.Tags.Contains("sample")
});
```

### Customize the HTTP status code

Use [ResultStatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.resultstatuscodes#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-resultstatuscodes) to customize the mapping of health status to HTTP status codes. The following [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) assignments are the default values used by the middleware. Change the status code values to meet your requirements:

```
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    ResultStatusCodes =
    {
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Degraded] = StatusCodes.Status200OK,
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
});
```

### Suppress cache headers

[AllowCachingResponses](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.allowcachingresponses#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-allowcachingresponses) controls whether the health checks middleware adds HTTP headers to a probe response to prevent response caching. If the value is `false` (default), the middleware sets or overrides the `Cache-Control`, `Expires`, and `Pragma` headers to prevent response caching. If the value is `true`, the middleware doesn't modify the cache headers of the response:

```
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    AllowCachingResponses = true
});
```

### Customize output

To customize the output of a health checks report, set the [HealthCheckOptions.ResponseWriter](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.responsewriter) property to a delegate that writes the response:

```
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    ResponseWriter = WriteResponse
});
```

The default delegate writes a minimal plaintext response with the string value of [`HealthReport.Status`](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreport.status#microsoft-extensions-diagnostics-healthchecks-healthreport-status). The following custom delegate outputs a custom JSON response using [System.Text.Json](/en-us/dotnet/api/system.text.json):

```
private static Task WriteResponse(HttpContext context, HealthReport healthReport)
{
    context.Response.ContentType = "application/json; charset=utf-8";

    var options = new JsonWriterOptions { Indented = true };

    using var memoryStream = new MemoryStream();
    using (var jsonWriter = new Utf8JsonWriter(memoryStream, options))
    {
        jsonWriter.WriteStartObject();
        jsonWriter.WriteString("status", healthReport.Status.ToString());
        jsonWriter.WriteStartObject("results");

        foreach (var healthReportEntry in healthReport.Entries)
        {
            jsonWriter.WriteStartObject(healthReportEntry.Key);
            jsonWriter.WriteString("status",
                healthReportEntry.Value.Status.ToString());
            jsonWriter.WriteString("description",
                healthReportEntry.Value.Description);
            jsonWriter.WriteStartObject("data");

            foreach (var item in healthReportEntry.Value.Data)
            {
                jsonWriter.WritePropertyName(item.Key);

                JsonSerializer.Serialize(jsonWriter, item.Value,
                    item.Value?.GetType() ?? typeof(object));
            }

            jsonWriter.WriteEndObject();
            jsonWriter.WriteEndObject();
        }

        jsonWriter.WriteEndObject();
        jsonWriter.WriteEndObject();
    }

    return context.Response.WriteAsync(
        Encoding.UTF8.GetString(memoryStream.ToArray()));
}
```

The health checks API doesn't provide built-in support for complex JSON return formats because the format is specific to your choice of monitoring system. Customize the response in the preceding examples as needed. For more information on JSON serialization with `System.Text.Json`, see [How to serialize and deserialize JSON in .NET](/en-us/dotnet/standard/serialization/system-text-json-how-to).

## Database probe

A health check can specify a database query to run as a boolean test to indicate if the database is responding normally.

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks), a health check library for ASP.NET Core apps, includes a health check that runs against a SQL Server database. `AspNetCore.Diagnostics.HealthChecks` executes a `SELECT 1` query against the database to confirm the connection to the database is healthy.

Warning

When checking a database connection by using a query, choose a query that returns quickly. The query approach runs the risk of overloading the database and degrading its performance. In most cases, running a test query isn't necessary. Merely making a successful connection to the database is sufficient. If you find it necessary to run a query, choose a simple SELECT query, such as `SELECT 1`.

To use this SQL Server health check, include a package reference to the [`AspNetCore.HealthChecks.SqlServer`](https://www.nuget.org/packages/AspNetCore.HealthChecks.SqlServer) NuGet package. The following example registers the SQL Server health check:

```
var conStr = builder.Configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrEmpty(conStr))
{
    throw new InvalidOperationException(
                       "Could not find a connection string named 'DefaultConnection'.");
}
builder.Services.AddHealthChecks()
    .AddSqlServer(conStr);
```

Note

Microsoft doesn't maintain or support [`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks).

## Entity Framework Core DbContext probe

The `DbContext` check confirms that the app can communicate with the database configured for an EF Core `DbContext`. Apps that support the `DbContext` check:

* Use [Entity Framework (EF) Core](/en-us/ef/core/).
* Include a package reference to the [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore) NuGet package.

[AddDbContextCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkcorehealthchecksbuilderextensions.adddbcontextcheck) registers a health check for a [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext). The `DbContext` is supplied to the method as the `TContext`. An overload is available to configure the failure status, tags, and a custom test query.

By default:

* The `DbContextHealthCheck` calls EF Core's `CanConnectAsync` method. You can customize what operation is run when checking health by using `AddDbContextCheck` method overloads.
* The name of the health check is the name of the `TContext` type.

The following example registers a `DbContext` and an associated `DbContextHealthCheck`:

```
builder.Services.AddDbContext<SampleDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddHealthChecks()
    .AddDbContextCheck<SampleDbContext>();
```

## Separate readiness and liveness probes

In some hosting scenarios, use a pair of health checks to distinguish two app states:

* *Readiness* indicates if the app is running normally but isn't ready to receive requests.
* *Liveness* indicates if an app has crashed and must be restarted.

Consider the following example: An app must download a large configuration file before it's ready to process requests. You don't want to restart the app if the initial download fails because the app can retry downloading the file several times. Use a *liveness probe* to describe the liveness of the process, no other checks are run. You also want to prevent requests from being sent to the app before the configuration file download succeeds. Use a *readiness probe* to indicate a "not ready" state until the download succeeds and the app is ready to receive requests.

The following background task simulates a startup process that takes roughly 15 seconds. Once it completes, the task sets the `StartupHealthCheck.StartupCompleted` property to true:

```
public class StartupBackgroundService : BackgroundService
{
    private readonly StartupHealthCheck _healthCheck;

    public StartupBackgroundService(StartupHealthCheck healthCheck)
        => _healthCheck = healthCheck;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Simulate the effect of a long-running task.
        await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);

        _healthCheck.StartupCompleted = true;
    }
}
```

The `StartupHealthCheck` reports the completion of the long-running startup task and exposes the `StartupCompleted` property that the background service sets:

```
public class StartupHealthCheck : IHealthCheck
{
    private volatile bool _isReady;

    public bool StartupCompleted
    {
        get => _isReady;
        set => _isReady = value;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        if (StartupCompleted)
        {
            return Task.FromResult(HealthCheckResult.Healthy("The startup task has completed."));
        }

        return Task.FromResult(HealthCheckResult.Unhealthy("That startup task is still running."));
    }
}
```

Register the health check with [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) in `Program.cs` along with the hosted service. Because the hosted service sets the property on the health check, also register the health check in the service container as a singleton:

```
builder.Services.AddHostedService<StartupBackgroundService>();
builder.Services.AddSingleton<StartupHealthCheck>();

builder.Services.AddHealthChecks()
    .AddCheck<StartupHealthCheck>(
        "Startup",
        tags: new[] { "ready" });
```

To create two different health check endpoints, call `MapHealthChecks` twice:

```
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = healthCheck => healthCheck.Tags.Contains("ready")
});

app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
    Predicate = _ => false
});
```

The preceding example creates the following health check endpoints:

* `/healthz/ready` for the readiness check. The readiness check filters health checks to those tagged with `ready`.
* `/healthz/live` for the liveness check. The liveness check filters out all health checks by returning `false` in the [HealthCheckOptions.Predicate](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.predicate) delegate. For more information on filtering health checks, see [Filter health checks](#filter-health-checks) in this article.

Before the startup task completes, the `/healthz/ready` endpoint reports an `Unhealthy` status. Once the startup task completes, this endpoint reports a `Healthy` status. The `/healthz/live` endpoint excludes all checks and reports a `Healthy` status for all calls.

### Kubernetes example

Using separate readiness and liveness checks is useful in an environment such as [Kubernetes](https://kubernetes.io/). In Kubernetes, an app might need to run time-consuming startup work before it can accept requests, such as a test of the underlying database availability. By using separate checks, the orchestrator can tell whether the app is functioning but not yet ready or if the app failed to start. For more information on readiness and liveness probes in Kubernetes, see [Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/) in the Kubernetes documentation.

The following example demonstrates a Kubernetes readiness probe configuration:

```
spec:
  template:
  spec:
    readinessProbe:
      # an http probe
      httpGet:
        path: /healthz/ready
        port: 80
      # length of time to wait for a pod to initialize
      # after pod startup, before applying health checking
      initialDelaySeconds: 30
      timeoutSeconds: 1
    ports:
      - containerPort: 80
```

## Distribute a health check library

To distribute a health check as a library:

1. Write a health check that implements the [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) interface as a standalone class. The class can rely on [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0), type activation, and [named options](../fundamentals/configuration/options?view=aspnetcore-10.0) to access configuration data.

2. Write an extension method with parameters that the consuming app calls in its `Program.cs` method. Consider the following example health check, which accepts `arg1` and `arg2` as constructor parameters:

   ```
   public SampleHealthCheckWithArgs(int arg1, string arg2)
       => (_arg1, _arg2) = (arg1, arg2);
   ```

   The preceding signature indicates that the health check requires custom data to process the health check probe logic. The data is provided to the delegate used to create the health check instance when the health check is registered with an extension method. In the following example, the caller specifies:

   * `arg1`: An integer data point for the health check.
   * `arg2`: A string argument for the health check.
   * `name`: An optional health check name. If `null`, a default value is used.
   * `failureStatus`: An optional [HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus), which is reported for a failure status. If `null`, [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) is used.
   * `tags`: An optional `IEnumerable<string>` collection of tags.

   ```
   public static class SampleHealthCheckBuilderExtensions
   {
       private const string DefaultName = "Sample";

       public static IHealthChecksBuilder AddSampleHealthCheck(
           this IHealthChecksBuilder healthChecksBuilder,
           int arg1,
           string arg2,
           string? name = null,
           HealthStatus? failureStatus = null,
           IEnumerable<string>? tags = default)
       {
           return healthChecksBuilder.Add(
               new HealthCheckRegistration(
                   name ?? DefaultName,
                   _ => new SampleHealthCheckWithArgs(arg1, arg2),
                   failureStatus,
                   tags));
       }
   }
   ```

## Health Check Publisher

When an [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) is added to the service container, the health check system periodically executes your health checks and calls [PublishAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher.publishasync) with the result. This process is useful in a push-based health monitoring system scenario that expects each process to call the monitoring system periodically to determine health.

[HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions) let you set the:

* [Delay](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.delay#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-delay): The initial delay after the app starts before executing [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) instances. The delay happens once at startup and doesn't apply to later iterations. The default value is five seconds.
* [Period](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.period#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-period): The period of [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) execution. The default value is 30 seconds.
* [Predicate](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.predicate#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-predicate): If [Predicate](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.predicate#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-predicate) is `null` (default), the health check publisher service runs all registered health checks. To run a subset of health checks, provide a function that filters the set of checks. The predicate is evaluated each period.
* [Timeout](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.timeout#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-timeout): The timeout for executing the health checks for all [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) instances. Use [InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan) to execute without a timeout. The default value is 30 seconds.

The following example shows the layout of a health publisher:

```
public class SampleHealthCheckPublisher : IHealthCheckPublisher
{
    public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
    {
        if (report.Status == HealthStatus.Healthy)
        {
            // ...
        }
        else
        {
            // ...
        }

        return Task.CompletedTask;
    }
}
```

The [HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions) class provides properties for configuring the behavior of the health check publisher.

The following example registers a health check publisher as a singleton and configures [HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions):

```
builder.Services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.FromSeconds(2);
    options.Predicate = healthCheck => healthCheck.Tags.Contains("sample");
});

builder.Services.AddSingleton<IHealthCheckPublisher, SampleHealthCheckPublisher>();
```

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks):

* Includes publishers for several systems, including [Application Insights](/en-us/azure/application-insights/app-insights-overview).
* Is ***not*** maintained or supported by Microsoft.

### Individual Healthchecks

You can set [`Delay` and `Period`](https://github.com/dotnet/aspnetcore/blob/main/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs#L161-L185) on each [HealthCheckRegistration](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration) individually. Set these values when you want to run some health checks at a different rate than the period set in [HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions).

The following code sets the `Delay` and `Period` for the `SampleHealthCheck1`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks()
   .Add(new HealthCheckRegistration(
       name: "SampleHealthCheck1",
       instance: new SampleHealthCheck(),
       failureStatus: null,
       tags: null,
       timeout: default)
   {
       Delay = TimeSpan.FromSeconds(40),
       Period = TimeSpan.FromSeconds(30)
   });

var app = builder.Build();

app.MapHealthChecks("/healthz");

app.Run();
```

## Dependency Injection and Health Checks

It's possible to use dependency injection to consume an instance of a specific `Type` inside a Health Check class. Dependency injection can be useful to inject options or a global configuration to a Health Check. Using dependency injection is ***not*** a common scenario to configure Health Checks. Usually, each Health Check is quite specific to the actual test and is configured using `IHealthChecksBuilder` extension methods.

The following example shows a sample Health Check that retrieves a configuration object via dependency injection:

```
public class SampleHealthCheckWithDI : IHealthCheck
{
    private readonly SampleHealthCheckWithDiConfig _config;

    public SampleHealthCheckWithDI(SampleHealthCheckWithDiConfig config)
        => _config = config;

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        var isHealthy = true;

        // use _config ...

        if (isHealthy)
        {
            return Task.FromResult(
                HealthCheckResult.Healthy("A healthy result."));
        }

        return Task.FromResult(
            new HealthCheckResult(
                context.Registration.FailureStatus, "An unhealthy result."));
    }
}
```

The `SampleHealthCheckWithDiConfig` and the Health check needs to be added to the service container :

```
builder.Services.AddSingleton<SampleHealthCheckWithDiConfig>(new SampleHealthCheckWithDiConfig
{
    BaseUriToCheck = new Uri("https://sample.contoso.com/api/")
});
builder.Services.AddHealthChecks()
    .AddCheck<SampleHealthCheckWithDI>(
        "With Dependency Injection",
        tags: new[] { "inject" });
```

## UseHealthChecks vs. MapHealthChecks

You can make health checks accessible to callers in two ways:

* [UseHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckapplicationbuilderextensions.usehealthchecks) registers middleware for handling health checks requests in the middleware pipeline.
* [MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks) registers a health checks endpoint. The endpoint is matched and executed along with other endpoints in the app.

By using `MapHealthChecks` instead of `UseHealthChecks`, you can use endpoint aware middleware, such as authorization, and you get greater fine-grained control over the matching policy. By using `UseHealthChecks` instead of `MapHealthChecks`, you control exactly where health checks runs in the middleware pipeline.

[UseHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckapplicationbuilderextensions.usehealthchecks):

* Terminates the pipeline when a request matches the health check endpoint. [Short-circuiting](../fundamentals/middleware/?view=aspnetcore-10.0) is often desirable because it avoids unnecessary work, such as logging and other middleware.
* Is primarily used for configuring the health checks middleware in the pipeline.
* Can match any path on a port with a `null` or empty `PathString`. Allows performing a health check on any request made to the specified port.
* [Source code](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HealthChecks/src/Builder/HealthCheckApplicationBuilderExtensions.cs)

[MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks) allows:

* Terminating the pipeline when a request matches the health check endpoint, by calling [ShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.builder.routeshortcircuitendpointconventionbuilderextensions.shortcircuit). For example, `app.MapHealthChecks("/healthz").ShortCircuit();`. For more information, see [Short-circuit middleware after routing](../fundamentals/routing?view=aspnetcore-10.0#short-circuit-middleware-after-routing).

* Mapping specific routes or endpoints for health checks.

* Customization of the URL or path where the health check endpoint is accessible.

* Mapping multiple health check endpoints with different routes or configurations. Multiple endpoint support:

  * Enables separate endpoints for different types of health checks or components.
  * Is used to differentiate between different aspects of the app's health or apply specific configurations to subsets of health checks.

* [Source code](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs)

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/health-checks/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

Note

This article was partially created with the help of artificial intelligence. Before publishing, an author reviewed and revised the content as needed. See [Our principles for using AI-generated content in Microsoft Learn](https://aka.ms/ai-content-principles).

ASP.NET Core offers health checks middleware and libraries for reporting the health of app infrastructure components.

Health checks are exposed by an app as HTTP endpoints. Health check endpoints can be configured for various real-time monitoring scenarios:

* Health probes can be used by container orchestrators and load balancers to check an app's status. For example, a container orchestrator may respond to a failing health check by halting a rolling deployment or restarting a container. A load balancer might react to an unhealthy app by routing traffic away from the failing instance to a healthy instance.
* Use of memory, disk, and other physical server resources can be monitored for healthy status.
* Health checks can test an app's dependencies, such as databases and external service endpoints, to confirm availability and normal functioning.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/health-checks/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app includes examples of the scenarios described in this article. To run the sample app for a given scenario, use the [dotnet run](/en-us/dotnet/core/tools/dotnet-run) command from the project's folder in a command shell. See the sample app's `README.md` file and the scenario descriptions in this article for details on how to use the sample app.

## Prerequisites

Health checks are typically used with an external monitoring service or container orchestrator to check the status of an app. Before adding health checks to an app, decide on which monitoring system to use. The monitoring system dictates what types of health checks to create and how to configure their endpoints.

The [`Microsoft.AspNetCore.Diagnostics.HealthChecks`](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics.HealthChecks) package is referenced implicitly for ASP.NET Core apps. To run health checks using Entity Framework Core, add a reference to the [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore) package.

The sample app provides startup code to demonstrate health checks for several scenarios. The [database probe](#database-probe) scenario checks the health of a database connection using [`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks). The [DbContext probe](#entity-framework-core-dbcontext-probe) scenario checks a database using an EF Core `DbContext`. To explore the database scenarios, the sample app:

* Creates a database and provides its connection string in the `appsettings.json` file.

* Has the following package references in its project file:

  * [`AspNetCore.HealthChecks.SqlServer`](https://www.nuget.org/packages/AspNetCore.HealthChecks.SqlServer)
  * [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore)

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

Another health check scenario demonstrates how to filter health checks to a management port. The sample app requires you to create a `Properties/launchSettings.json` file that includes the management URL and management port. For more information, see the [Filter by port](#filter-by-port) section.

## Basic health probe

For many apps, a basic health probe configuration that reports the app's availability to process requests (*liveness*) is sufficient to discover the status of the app.

The basic configuration registers health check services and calls the health checks middleware to respond at a URL endpoint with a health response. By default, no specific health checks are registered to test any particular dependency or subsystem. The app is considered healthy if it can respond at the health endpoint URL. The default response writer writes the status ([HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus)) as a plaintext response back to the client, indicating either a [HealthStatus.Healthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-healthy), [HealthStatus.Degraded](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-degraded), or [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) status.

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Startup.ConfigureServices`. Create a health check endpoint by calling `MapHealthChecks` in `Startup.Configure`.

In the sample app, the health check endpoint is created at `/health` (`BasicStartup.cs`):

```
public class BasicStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHealthChecks();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHealthChecks("/health");
        });
    }
}
```

To run the basic configuration scenario using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario basic
```

### Docker example

[Docker](docker/?view=aspnetcore-10.0) offers a built-in `HEALTHCHECK` directive that can be used to check the status of an app that uses the basic health check configuration:

```
HEALTHCHECK CMD curl --fail http://localhost:5000/health || exit
```

## Create health checks

Health checks are created by implementing the [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) interface. The [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) method returns a [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) that indicates the health as `Healthy`, `Degraded`, or `Unhealthy`. The result is written as a plaintext response with a configurable status code (configuration is described in the [Health check options](#health-check-options) section). [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) can also return optional key-value pairs.

The following `ExampleHealthCheck` class demonstrates the layout of a health check. The health checks logic is placed in the `CheckHealthAsync` method. The following example sets a dummy variable, `healthCheckResultHealthy`, to `true`. If the value of `healthCheckResultHealthy` is set to `false`, the [HealthCheckRegistration.FailureStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.failurestatus#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-failurestatus) status is returned.

```
public class ExampleHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        var healthCheckResultHealthy = true;

        if (healthCheckResultHealthy)
        {
            return Task.FromResult(
                HealthCheckResult.Healthy("A healthy result."));
        }

        return Task.FromResult(
            new HealthCheckResult(context.Registration.FailureStatus, 
            "An unhealthy result."));
    }
}
```

If [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) throws an exception during the check, a new [HealthReportEntry](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry) is returned with its [HealthReportEntry.Status](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry.status#microsoft-extensions-diagnostics-healthchecks-healthreportentry-status) set to the [FailureStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.failurestatus#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-failurestatus), which is defined by [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) (see the [Register health check services](#register-health-check-services) section) and includes the [inner exception](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry.exception#microsoft-extensions-diagnostics-healthchecks-healthreportentry-exception) that initially caused the check failure. The [Description](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry.description#microsoft-extensions-diagnostics-healthchecks-healthreportentry-description) is set to the exception's message.

## Register health check services

The `ExampleHealthCheck` type is added to health check services with [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) in `Startup.ConfigureServices`:

```
services.AddHealthChecks()
    .AddCheck<ExampleHealthCheck>("example_health_check");
```

The [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) overload shown in the following example sets the failure status ([HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus)) to report when the health check reports a failure. If the failure status is set to `null` (default), [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) is reported. This overload is a useful scenario for library authors, where the failure status indicated by the library is enforced by the app when a health check failure occurs if the health check implementation honors the setting.

*Tags* can be used to filter health checks (described further in the [Filter health checks](#filter-health-checks) section).

```
services.AddHealthChecks()
    .AddCheck<ExampleHealthCheck>(
        "example_health_check",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "example" });
```

[AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) can also execute a lambda function. In the following example, the health check name is specified as `Example` and the check always returns a healthy state:

```
services.AddHealthChecks()
    .AddCheck("Example", () =>
        HealthCheckResult.Healthy("Example is OK!"), tags: new[] { "example" });
```

Call [AddTypeActivatedCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addtypeactivatedcheck) to pass arguments to a health check implementation. In the following example, `TestHealthCheckWithArgs` accepts an integer and a string for use when [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) is called:

```
private class TestHealthCheckWithArgs : IHealthCheck
{
    public TestHealthCheckWithArgs(int i, string s)
    {
        I = i;
        S = s;
    }

    public int I { get; set; }

    public string S { get; set; }

    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, 
        CancellationToken cancellationToken = default)
    {
        ...
    }
}
```

`TestHealthCheckWithArgs` is registered by calling `AddTypeActivatedCheck` with the integer and string passed to the implementation:

```
services.AddHealthChecks()
    .AddTypeActivatedCheck<TestHealthCheckWithArgs>(
        "test", 
        failureStatus: HealthStatus.Degraded, 
        tags: new[] { "example" }, 
        args: new object[] { 5, "string" });
```

## Use Health Checks Routing

In `Startup.Configure`, call `MapHealthChecks` on the endpoint builder with the endpoint URL or relative path:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
});
```

### Require host

Call `RequireHost` to specify one or more permitted hosts for the health check endpoint. Hosts should be Unicode rather than punycode and may include a port. If a collection isn't supplied, any host is accepted.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health").RequireHost("www.contoso.com:5001");
});
```

For more information, see the [Filter by port](#filter-by-port) section.

### Require authorization

Call `RequireAuthorization` to run authorization middleware on the health check request endpoint. A `RequireAuthorization` overload accepts one or more authorization policies. If a policy isn't provided, the default authorization policy is used.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health").RequireAuthorization();
});
```

### Enable Cross-Origin Requests (CORS)

Although running health checks manually from a browser isn't a common use scenario, CORS middleware can be enabled by calling `RequireCors` on health checks endpoints. A `RequireCors` overload accepts a CORS policy builder delegate (`CorsPolicyBuilder`) or a policy name. If a policy isn't provided, the default CORS policy is used. For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0).

## Health check options

[HealthCheckOptions](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions) provide an opportunity to customize health check behavior:

* [Filter health checks](#filter-health-checks)
* [Customize the HTTP status code](#customize-the-http-status-code)
* [Suppress cache headers](#suppress-cache-headers)
* [Customize output](#customize-output)

### Filter health checks

By default, health checks middleware runs all registered health checks. To run a subset of health checks, provide a function that returns a boolean to the [Predicate](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.predicate#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-predicate) option. In the following example, the `Bar` health check is filtered out by its tag (`bar_tag`) in the function's conditional statement, where `true` is only returned if the health check's [Tags](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.tags#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-tags) property matches `foo_tag` or `baz_tag`:

In `Startup.ConfigureServices`:

```
services.AddHealthChecks()
    .AddCheck("Foo", () =>
        HealthCheckResult.Healthy("Foo is OK!"), tags: new[] { "foo_tag" })
    .AddCheck("Bar", () =>
        HealthCheckResult.Unhealthy("Bar is unhealthy!"), tags: new[] { "bar_tag" })
    .AddCheck("Baz", () =>
        HealthCheckResult.Healthy("Baz is OK!"), tags: new[] { "baz_tag" });
```

In `Startup.Configure`, the `Predicate` filters out the 'Bar' health check. Only Foo and Baz execute:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        Predicate = (check) => check.Tags.Contains("foo_tag") ||
            check.Tags.Contains("baz_tag")
    });
});
```

### Customize the HTTP status code

Use [ResultStatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.resultstatuscodes#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-resultstatuscodes) to customize the mapping of health status to HTTP status codes. The following [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) assignments are the default values used by the middleware. Change the status code values to meet your requirements.

In `Startup.Configure`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        ResultStatusCodes =
        {
            [HealthStatus.Healthy] = StatusCodes.Status200OK,
            [HealthStatus.Degraded] = StatusCodes.Status200OK,
            [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
        }
    });
});
```

### Suppress cache headers

[AllowCachingResponses](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.allowcachingresponses#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-allowcachingresponses) controls whether the health checks middleware adds HTTP headers to a probe response to prevent response caching. If the value is `false` (default), the middleware sets or overrides the `Cache-Control`, `Expires`, and `Pragma` headers to prevent response caching. If the value is `true`, the middleware doesn't modify the cache headers of the response.

In `Startup.Configure`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        AllowCachingResponses = true
    });
});
```

### Customize output

In `Startup.Configure`, set the [HealthCheckOptions.ResponseWriter](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.responsewriter) option to a delegate for writing the response:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        ResponseWriter = WriteResponse
    });
});
```

The default delegate writes a minimal plaintext response with the string value of [`HealthReport.Status`](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreport.status#microsoft-extensions-diagnostics-healthchecks-healthreport-status). The following custom delegates output a custom JSON response.

The first example from the sample app demonstrates how to use [System.Text.Json](/en-us/dotnet/api/system.text.json):

```
private static Task WriteResponse(HttpContext context, HealthReport result)
{
    context.Response.ContentType = "application/json; charset=utf-8";

    var options = new JsonWriterOptions
    {
        Indented = true
    };

    using (var stream = new MemoryStream())
    {
        using (var writer = new Utf8JsonWriter(stream, options))
        {
            writer.WriteStartObject();
            writer.WriteString("status", result.Status.ToString());
            writer.WriteStartObject("results");
            foreach (var entry in result.Entries)
            {
                writer.WriteStartObject(entry.Key);
                writer.WriteString("status", entry.Value.Status.ToString());
                writer.WriteString("description", entry.Value.Description);
                writer.WriteStartObject("data");
                foreach (var item in entry.Value.Data)
                {
                    writer.WritePropertyName(item.Key);
                    JsonSerializer.Serialize(
                        writer, item.Value, item.Value?.GetType() ??
                        typeof(object));
                }
                writer.WriteEndObject();
                writer.WriteEndObject();
            }
            writer.WriteEndObject();
            writer.WriteEndObject();
        }

        var json = Encoding.UTF8.GetString(stream.ToArray());

        return context.Response.WriteAsync(json);
    }
}
```

The second example demonstrates how to use [`Newtonsoft.Json`](https://www.nuget.org/packages/Newtonsoft.Json):

```
private static Task WriteResponse(HttpContext context, HealthReport result)
{
    context.Response.ContentType = "application/json";

    var json = new JObject(
        new JProperty("status", result.Status.ToString()),
        new JProperty("results", new JObject(result.Entries.Select(pair =>
            new JProperty(pair.Key, new JObject(
                new JProperty("status", pair.Value.Status.ToString()),
                new JProperty("description", pair.Value.Description),
                new JProperty("data", new JObject(pair.Value.Data.Select(
                    p => new JProperty(p.Key, p.Value))))))))));

    return context.Response.WriteAsync(
        json.ToString(Formatting.Indented));
}
```

In the sample app, comment out the `SYSTEM_TEXT_JSON` [preprocessor directive](../overview?view=aspnetcore-10.0#preprocessor-directives-in-sample-code) in `CustomWriterStartup.cs` to enable the `Newtonsoft.Json` version of `WriteResponse`.

The health checks API doesn't provide built-in support for complex JSON return formats because the format is specific to your choice of monitoring system. Customize the response in the preceding examples as needed. For more information on JSON serialization with `System.Text.Json`, see [How to serialize and deserialize JSON in .NET](/en-us/dotnet/standard/serialization/system-text-json-how-to).

## Database probe

A health check can specify a database query to run as a boolean test to indicate if the database is responding normally.

The sample app uses [`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks), a health check library for ASP.NET Core apps, to run a health check on a SQL Server database. `AspNetCore.Diagnostics.HealthChecks` executes a `SELECT 1` query against the database to confirm the connection to the database is healthy.

Warning

When checking a database connection with a query, choose a query that returns quickly. The query approach runs the risk of overloading the database and degrading its performance. In most cases, running a test query isn't necessary. Merely making a successful connection to the database is sufficient. If you find it necessary to run a query, choose a simple SELECT query, such as `SELECT 1`.

Include a package reference to [`AspNetCore.HealthChecks.SqlServer`](https://www.nuget.org/packages/AspNetCore.HealthChecks.SqlServer).

Supply a valid database connection string in the `appsettings.json` file of the sample app. The app uses a SQL Server database named `HealthCheckSample`:

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=HealthCheckSample;Trusted_Connection=True;MultipleActiveResultSets=true;ConnectRetryCount=0"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "Console": {
      "IncludeScopes": "true"
    }
  },
  "AllowedHosts": "*"
}
```

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Startup.ConfigureServices`. The sample app calls the `AddSqlServer` method with the database's connection string (`DbHealthStartup.cs`):

```
services.AddHealthChecks()
    .AddSqlServer(Configuration["ConnectionStrings:DefaultConnection"]);
```

A health check endpoint is created by calling `MapHealthChecks` in `Startup.Configure`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
}
```

To run the database probe scenario using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario db
```

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

## Entity Framework Core DbContext probe

The `DbContext` check confirms that the app can communicate with the database configured for an EF Core `DbContext`. The `DbContext` check is supported in apps that:

* Use [Entity Framework (EF) Core](/en-us/ef/core/).
* Include a package reference to [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore).

`AddDbContextCheck<TContext>` registers a health check for a `DbContext`. The `DbContext` is supplied as the `TContext` to the method. An overload is available to configure the failure status, tags, and a custom test query.

By default:

* The `DbContextHealthCheck` calls EF Core's `CanConnectAsync` method. You can customize what operation is run when checking health using `AddDbContextCheck` method overloads.
* The name of the health check is the name of the `TContext` type.

In the sample app, `AppDbContext` is provided to `AddDbContextCheck` and registered as a service in `Startup.ConfigureServices` (`DbContextHealthStartup.cs`):

```
services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>();

services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(
        Configuration["ConnectionStrings:DefaultConnection"]);
});
```

A health check endpoint is created by calling `MapHealthChecks` in `Startup.Configure`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
}
```

To run the `DbContext` probe scenario using the sample app, confirm that the database specified by the connection string doesn't exist in the SQL Server instance. If the database exists, delete it.

Execute the following command from the project's folder in a command shell:

```
dotnet run --scenario dbcontext
```

After the app is running, check the health status by making a request to the `/health` endpoint in a browser. The database and `AppDbContext` don't exist, so app provides the following response:

```
Unhealthy
```

Trigger the sample app to create the database. Make a request to `/createdatabase`. The app responds:

```
Creating the database...
Done!
Navigate to /health to see the health status.
```

Make a request to the `/health` endpoint. The database and context exist, so app responds:

```
Healthy
```

Trigger the sample app to delete the database. Make a request to `/deletedatabase`. The app responds:

```
Deleting the database...
Done!
Navigate to /health to see the health status.
```

Make a request to the `/health` endpoint. The app provides an unhealthy response:

```
Unhealthy
```

## Separate readiness and liveness probes

In some hosting scenarios, a pair of health checks is used to distinguish two app states:

* *Readiness* indicates if the app is running normally but isn't ready to receive requests.
* *Liveness* indicates if an app has crashed and must be restarted.

Consider the following example: An app must download a large configuration file before it's ready to process requests. We don't want the app to be restarted if the initial download fails because the app can retry downloading the file several times. We use a *liveness probe* to describe the liveness of the process, no other checks are run. We also want to prevent requests from being sent to the app before the configuration file download has succeeded. We use a *readiness probe* to indicate a "not ready" state until the download succeeds and the app is ready to receive requests.

The sample app contains a health check to report the completion of long-running startup task in a [Hosted Service](../fundamentals/host/hosted-services?view=aspnetcore-10.0). The `StartupHostedServiceHealthCheck` exposes a property, `StartupTaskCompleted`, that the hosted service can set to `true` when its long-running task is finished (`StartupHostedServiceHealthCheck.cs`):

```
public class StartupHostedServiceHealthCheck : IHealthCheck
{
    private volatile bool _startupTaskCompleted = false;

    public string Name => "slow_dependency_check";

    public bool StartupTaskCompleted
    {
        get => _startupTaskCompleted;
        set => _startupTaskCompleted = value;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        if (StartupTaskCompleted)
        {
            return Task.FromResult(
                HealthCheckResult.Healthy("The startup task is finished."));
        }

        return Task.FromResult(
            HealthCheckResult.Unhealthy("The startup task is still running."));
    }
}
```

The long-running background task is started by a [Hosted Service](../fundamentals/host/hosted-services?view=aspnetcore-10.0) (`Services/StartupHostedService`). At the conclusion of the task, `StartupHostedServiceHealthCheck.StartupTaskCompleted` is set to `true`:

```
public class StartupHostedService : IHostedService, IDisposable
{
    private readonly int _delaySeconds = 15;
    private readonly ILogger _logger;
    private readonly StartupHostedServiceHealthCheck _startupHostedServiceHealthCheck;

    public StartupHostedService(ILogger<StartupHostedService> logger,
        StartupHostedServiceHealthCheck startupHostedServiceHealthCheck)
    {
        _logger = logger;
        _startupHostedServiceHealthCheck = startupHostedServiceHealthCheck;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Startup Background Service is starting.");

        // Simulate the effect of a long-running startup task.
        Task.Run(async () =>
        {
            await Task.Delay(_delaySeconds * 1000);

            _startupHostedServiceHealthCheck.StartupTaskCompleted = true;

            _logger.LogInformation("Startup Background Service has started.");
        });

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Startup Background Service is stopping.");

        return Task.CompletedTask;
    }

    public void Dispose()
    {
    }
}
```

The health check is registered with [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) in `Startup.ConfigureServices` along with the hosted service. Because the hosted service must set the property on the health check, the health check is also registered in the service container (`LivenessProbeStartup.cs`):

```
services.AddHostedService<StartupHostedService>();
services.AddSingleton<StartupHostedServiceHealthCheck>();

services.AddHealthChecks()
    .AddCheck<StartupHostedServiceHealthCheck>(
        "hosted_service_startup",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "ready" });

services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.FromSeconds(2);
    options.Predicate = (check) => check.Tags.Contains("ready");
});

services.AddSingleton<IHealthCheckPublisher, ReadinessPublisher>();
```

A health check endpoint is created by calling `MapHealthChecks` in `Startup.Configure`. In the sample app, the health check endpoints are created at:

* `/health/ready` for the readiness check. The readiness check filters health checks to the health check with the `ready` tag.
* `/health/live` for the liveness check. The liveness check filters out the `StartupHostedServiceHealthCheck` by returning `false` in the [`HealthCheckOptions.Predicate`](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.predicate#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-predicate) (for more information, see [Filter health checks](#filter-health-checks))

In the following example code:

* The readiness check uses all registered checks with the 'ready' tag.
* The `Predicate` excludes all checks and returns a 200-Ok.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health/ready", new HealthCheckOptions()
    {
        Predicate = (check) => check.Tags.Contains("ready"),
    });

    endpoints.MapHealthChecks("/health/live", new HealthCheckOptions()
    {
        Predicate = (_) => false
    });
}
```

To run the readiness/liveness configuration scenario using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario liveness
```

In a browser, visit `/health/ready` several times until 15 seconds have passed. The health check reports `Unhealthy` for the first 15 seconds. After 15 seconds, the endpoint reports `Healthy`, which reflects the completion of the long-running task by the hosted service.

This example also creates a Health Check Publisher ([IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) implementation) that runs the first readiness check with a two-second delay. For more information, see the [Health Check Publisher](#health-check-publisher) section.

### Kubernetes example

Using separate readiness and liveness checks is useful in an environment such as [Kubernetes](https://kubernetes.io/). In Kubernetes, an app might be required to run time-consuming startup work before accepting requests, such as a test of the underlying database availability. Using separate checks allows the orchestrator to distinguish whether the app is functioning but not yet ready or if the app has failed to start. For more information on readiness and liveness probes in Kubernetes, see [Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/) in the Kubernetes documentation.

The following example demonstrates a Kubernetes readiness probe configuration:

```
spec:
  template:
  spec:
    readinessProbe:
      # an http probe
      httpGet:
        path: /health/ready
        port: 80
      # length of time to wait for a pod to initialize
      # after pod startup, before applying health checking
      initialDelaySeconds: 30
      timeoutSeconds: 1
    ports:
      - containerPort: 80
```

## Metric-based probe with a custom response writer

The sample app demonstrates a memory health check with a custom response writer.

`MemoryHealthCheck` reports a degraded status if the app uses more than a given threshold of memory (1 GB in the sample app). The [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) includes Garbage Collector (GC) information for the app (`MemoryHealthCheck.cs`):

```
public class MemoryHealthCheck : IHealthCheck
{
    private readonly IOptionsMonitor<MemoryCheckOptions> _options;

    public MemoryHealthCheck(IOptionsMonitor<MemoryCheckOptions> options)
    {
        _options = options;
    }

    public string Name => "memory_check";

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        var options = _options.Get(context.Registration.Name);

        // Include GC information in the reported diagnostics.
        var allocated = GC.GetTotalMemory(forceFullCollection: false);
        var data = new Dictionary<string, object>()
        {
            { "AllocatedBytes", allocated },
            { "Gen0Collections", GC.CollectionCount(0) },
            { "Gen1Collections", GC.CollectionCount(1) },
            { "Gen2Collections", GC.CollectionCount(2) },
        };
        var status = (allocated < options.Threshold) ?
            HealthStatus.Healthy : context.Registration.FailureStatus;

        return Task.FromResult(new HealthCheckResult(
            status,
            description: "Reports degraded status if allocated bytes " +
                $">= {options.Threshold} bytes.",
            exception: null,
            data: data));
    }
}
```

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Startup.ConfigureServices`. Instead of enabling the health check by passing it to [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck), the `MemoryHealthCheck` is registered as a service. All [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) registered services are available to the health check services and middleware. We recommend registering health check services as Singleton services.

In `CustomWriterStartup.cs` of the sample app:

```
services.AddHealthChecks()
    .AddMemoryHealthCheck("memory");
```

A health check endpoint is created by calling `MapHealthChecks` in `Startup.Configure`. A `WriteResponse` delegate is provided to the [ResponseWriter](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.responsewriter#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-responsewriter) property to output a custom JSON response when the health check executes:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        ResponseWriter = WriteResponse
    });
}
```

The `WriteResponse` delegate formats the `CompositeHealthCheckResult` into a JSON object and yields JSON output for the health check response. For more information, see the [Customize output](#customize-output) section.

To run the metric-based probe with custom response writer output using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario writer
```

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) includes metric-based health check scenarios, including disk storage and maximum value liveness checks.

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

## Filter by port

Call `RequireHost` on `MapHealthChecks` with a URL pattern that specifies a port to restrict health check requests to the port specified. This approach is typically used in a container environment to expose a port for monitoring services.

The sample app configures the port using the [Environment Variable Configuration Provider](../fundamentals/configuration/?view=aspnetcore-10.0#environment-variables). The port is set in the `launchSettings.json` file and passed to the configuration provider via an environment variable. You must also configure the server to listen to requests on the management port.

To use the sample app to demonstrate management port configuration, create the `launchSettings.json` file in a `Properties` folder.

The following `Properties/launchSettings.json` file in the sample app isn't included in the sample app's project files and must be created manually:

```
{
  "profiles": {
    "SampleApp": {
      "commandName": "Project",
      "commandLineArgs": "",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_URLS": "http://localhost:5000/;http://localhost:5001/",
        "ASPNETCORE_MANAGEMENTPORT": "5001"
      },
      "applicationUrl": "http://localhost:5000/"
    }
  }
}
```

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Startup.ConfigureServices`. Create a health check endpoint by calling `MapHealthChecks` in `Startup.Configure`.

In the sample app, a call to `RequireHost` on the endpoint in `Startup.Configure` specifies the management port from configuration:

```
endpoints.MapHealthChecks("/health")
    .RequireHost($"*:{Configuration["ManagementPort"]}");
```

Endpoints are created in the sample app in `Startup.Configure`. In the following example code:

* The readiness check uses all registered checks with the 'ready' tag.
* The `Predicate` excludes all checks and returns a 200-Ok.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health/ready", new HealthCheckOptions()
    {
        Predicate = (check) => check.Tags.Contains("ready"),
    });

    endpoints.MapHealthChecks("/health/live", new HealthCheckOptions()
    {
        Predicate = (_) => false
    });
}
```

Note

You can avoid creating the `launchSettings.json` file in the sample app by setting the management port explicitly in code. In `Program.cs` where the [HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder) is created, add a call to [ListenAnyIP](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenanyip) and provide the app's management port endpoint. In `Configure` of `ManagementPortStartup.cs`, specify the management port with `RequireHost`:

`Program.cs`:

```
return new HostBuilder()
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseKestrel()
            .ConfigureKestrel(serverOptions =>
            {
                serverOptions.ListenAnyIP(5001);
            })
            .UseStartup(startupType);
    })
    .Build();
```

`ManagementPortStartup.cs`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health").RequireHost("*:5001");
});
```

To run the management port configuration scenario using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario port
```

## Distribute a health check library

To distribute a health check as a library:

1. Write a health check that implements the [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) interface as a standalone class. The class can rely on [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0), type activation, and [named options](../fundamentals/configuration/options?view=aspnetcore-10.0) to access configuration data.

   In the health checks logic of `CheckHealthAsync`:

   * `data1` and `data2` are used in the method to run the probe's health check logic.
   * `AccessViolationException` is handled.

   When an [AccessViolationException](/en-us/dotnet/api/system.accessviolationexception) occurs, the [FailureStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.failurestatus#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-failurestatus) is returned with the [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) to allow users to configure the health checks failure status.

   ```
   using System;
   using System.Threading;
   using System.Threading.Tasks;
   using Microsoft.Extensions.Diagnostics.HealthChecks;

   namespace SampleApp
   {
       public class ExampleHealthCheck : IHealthCheck
       {
           private readonly string _data1;
           private readonly int? _data2;

           public ExampleHealthCheck(string data1, int? data2)
           {
               _data1 = data1 ?? throw new ArgumentNullException(nameof(data1));
               _data2 = data2 ?? throw new ArgumentNullException(nameof(data2));
           }

           public async Task<HealthCheckResult> CheckHealthAsync(
               HealthCheckContext context, CancellationToken cancellationToken)
           {
               try
               {
                   return HealthCheckResult.Healthy();
               }
               catch (AccessViolationException ex)
               {
                   return new HealthCheckResult(
                       context.Registration.FailureStatus,
                       description: "An access violation occurred during the check.",
                       exception: ex,
                       data: null);
               }
           }
       }
   }
   ```

2. Write an extension method with parameters that the consuming app calls in its `Startup.Configure` method. In the following example, assume the following health check method signature:

   ```
   ExampleHealthCheck(string, string, int )
   ```

   The preceding signature indicates that the `ExampleHealthCheck` requires additional data to process the health check probe logic. The data is provided to the delegate used to create the health check instance when the health check is registered with an extension method. In the following example, the caller specifies optional:

   * health check name (`name`). If `null`, `example_health_check` is used.
   * string data point for the health check (`data1`).
   * integer data point for the health check (`data2`). If `null`, `1` is used.
   * failure status ([HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus)). The default is `null`. If `null`, [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) is reported for a failure status.
   * tags (`IEnumerable<string>`).

   ```
   using System.Collections.Generic;
   using Microsoft.Extensions.Diagnostics.HealthChecks;

   public static class ExampleHealthCheckBuilderExtensions
   {
       const string DefaultName = "example_health_check";

       public static IHealthChecksBuilder AddExampleHealthCheck(
           this IHealthChecksBuilder builder,
           string name = default,
           string data1,
           int data2 = 1,
           HealthStatus? failureStatus = default,
           IEnumerable<string> tags = default)
       {
           return builder.Add(new HealthCheckRegistration(
               name ?? DefaultName,
               sp => new ExampleHealthCheck(data1, data2),
               failureStatus,
               tags));
       }
   }
   ```

## Health Check Publisher

When an [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) is added to the service container, the health check system periodically executes your health checks and calls `PublishAsync` with the result. This is useful in a push-based health monitoring system scenario that expects each process to call the monitoring system periodically in order to determine health.

The [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) interface has a single method:

```
Task PublishAsync(HealthReport report, CancellationToken cancellationToken);
```

[HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions) allow you to set:

* [Delay](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.delay#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-delay): The initial delay applied after the app starts before executing [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) instances. The delay is applied once at startup and doesn't apply to subsequent iterations. The default value is five seconds.
* [Period](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.period#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-period): The period of [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) execution. The default value is 30 seconds.
* [Predicate](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.predicate#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-predicate): If [Predicate](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.predicate#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-predicate) is `null` (default), the health check publisher service runs all registered health checks. To run a subset of health checks, provide a function that filters the set of checks. The predicate is evaluated each period.
* [Timeout](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.timeout#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-timeout): The timeout for executing the health checks for all [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) instances. Use [InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan) to execute without a timeout. The default value is 30 seconds.

In the sample app, `ReadinessPublisher` is an [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) implementation. The health check status is logged for each check at a log level of:

* Information ([LogInformation](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.loginformation)) if the health checks status is [Healthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-healthy).
* Error ([LogError](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.logerror)) if the status is either [Degraded](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-degraded) or [Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy).

```
public class ReadinessPublisher : IHealthCheckPublisher
{
    private readonly ILogger _logger;

    public ReadinessPublisher(ILogger<ReadinessPublisher> logger)
    {
        _logger = logger;
    }

    // The following example is for demonstration purposes only. Health Checks
    // Middleware already logs health checks results. A real-world readiness
    // check in a production app might perform a set of more expensive or
    // time-consuming checks to determine if other resources are responding
    // properly.
    public Task PublishAsync(HealthReport report,
        CancellationToken cancellationToken)
    {
        if (report.Status == HealthStatus.Healthy)
        {
            _logger.LogInformation("{Timestamp} Readiness Probe Status: {Result}",
                DateTime.UtcNow, report.Status);
        }
        else
        {
            _logger.LogError("{Timestamp} Readiness Probe Status: {Result}",
                DateTime.UtcNow, report.Status);
        }

        cancellationToken.ThrowIfCancellationRequested();

        return Task.CompletedTask;
    }
}
```

In the sample app's `LivenessProbeStartup` example, the `StartupHostedService` readiness check has a two second startup delay and runs the check every 30 seconds. To activate the [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) implementation, the sample registers `ReadinessPublisher` as a singleton service in the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container:

```
services.AddHostedService<StartupHostedService>();
services.AddSingleton<StartupHostedServiceHealthCheck>();

services.AddHealthChecks()
    .AddCheck<StartupHostedServiceHealthCheck>(
        "hosted_service_startup",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "ready" });

services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.FromSeconds(2);
    options.Predicate = (check) => check.Tags.Contains("ready");
});

services.AddSingleton<IHealthCheckPublisher, ReadinessPublisher>();
```

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) includes publishers for several systems, including [Application Insights](/en-us/azure/application-insights/app-insights-overview).

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

## Restrict health checks with MapWhen

Use [MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen) to conditionally branch the request pipeline for health check endpoints.

In the following example, `MapWhen` branches the request pipeline to activate health checks middleware if a GET request is received for the `api/HealthCheck` endpoint:

```
app.MapWhen(
    context => context.Request.Method == HttpMethod.Get.Method && 
        context.Request.Path.StartsWith("/api/HealthCheck"),
    builder => builder.UseHealthChecks());

app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
});
```

For more information, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline).

ASP.NET Core offers health checks middleware and libraries for reporting the health of app infrastructure components.

Health checks are exposed by an app as HTTP endpoints. Health check endpoints can be configured for various real-time monitoring scenarios:

* Health probes can be used by container orchestrators and load balancers to check an app's status. For example, a container orchestrator may respond to a failing health check by halting a rolling deployment or restarting a container. A load balancer might react to an unhealthy app by routing traffic away from the failing instance to a healthy instance.
* Use of memory, disk, and other physical server resources can be monitored for healthy status.
* Health checks can test an app's dependencies, such as databases and external service endpoints, to confirm availability and normal functioning.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/health-checks/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample app includes examples of the scenarios described in this article. To run the sample app for a given scenario, use the [dotnet run](/en-us/dotnet/core/tools/dotnet-run) command from the project's folder in a command shell. See the sample app's `README.md` file and the scenario descriptions in this article for details on how to use the sample app.

## Prerequisites

Health checks are typically used with an external monitoring service or container orchestrator to check the status of an app. Before adding health checks to an app, decide on which monitoring system to use. The monitoring system dictates what types of health checks to create and how to configure their endpoints.

The [`Microsoft.AspNetCore.Diagnostics.HealthChecks`](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics.HealthChecks) package is referenced implicitly for ASP.NET Core apps. To run health checks using Entity Framework Core, add a package reference to the [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore) package.

The sample app provides startup code to demonstrate health checks for several scenarios. The [database probe](#database-probe) scenario checks the health of a database connection using [`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks). The [DbContext probe](#entity-framework-core-dbcontext-probe) scenario checks a database using an EF Core `DbContext`. To explore the database scenarios, the sample app:

* Creates a database and provides its connection string in the `appsettings.json` file.

* Has the following package references in its project file:

  * [`AspNetCore.HealthChecks.SqlServer`](https://www.nuget.org/packages/AspNetCore.HealthChecks.SqlServer)
  * [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore)

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

Another health check scenario demonstrates how to filter health checks to a management port. The sample app requires you to create a `Properties/launchSettings.json` file that includes the management URL and management port. For more information, see the [Filter by port](#filter-by-port) section.

## Basic health probe

For many apps, a basic health probe configuration that reports the app's availability to process requests (*liveness*) is sufficient to discover the status of the app.

The basic configuration registers health check services and calls the health checks middleware to respond at a URL endpoint with a health response. By default, no specific health checks are registered to test any particular dependency or subsystem. The app is considered healthy if it can respond at the health endpoint URL. The default response writer writes the status ([HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus)) as a plaintext response back to the client, indicating either a [HealthStatus.Healthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-healthy), [HealthStatus.Degraded](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-degraded), or [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) status.

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Startup.ConfigureServices`. Create a health check endpoint by calling `MapHealthChecks` in `Startup.Configure`.

In the sample app, the health check endpoint is created at `/health` (`BasicStartup.cs`):

```
public class BasicStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHealthChecks();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHealthChecks("/health");
        });
    }
}
```

To run the basic configuration scenario using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario basic
```

### Docker example

[Docker](docker/?view=aspnetcore-10.0) offers a built-in `HEALTHCHECK` directive that can be used to check the status of an app that uses the basic health check configuration:

```
HEALTHCHECK CMD curl --fail http://localhost:5000/health || exit
```

## Create health checks

Health checks are created by implementing the [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) interface. The [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) method returns a [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) that indicates the health as `Healthy`, `Degraded`, or `Unhealthy`. The result is written as a plaintext response with a configurable status code (configuration is described in the [Health check options](#health-check-options) section). [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) can also return optional key-value pairs.

The following `ExampleHealthCheck` class demonstrates the layout of a health check. The health checks logic is placed in the `CheckHealthAsync` method. The following example sets a dummy variable, `healthCheckResultHealthy`, to `true`. If the value of `healthCheckResultHealthy` is set to `false`, the [`HealthCheckResult.Unhealthy`](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult.unhealthy) status is returned.

```
public class ExampleHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        var healthCheckResultHealthy = true;

        if (healthCheckResultHealthy)
        {
            return Task.FromResult(
                HealthCheckResult.Healthy("A healthy result."));
        }

        return Task.FromResult(
            HealthCheckResult.Unhealthy("An unhealthy result."));
    }
}
```

## Register health check services

The `ExampleHealthCheck` type is added to health check services with [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) in `Startup.ConfigureServices`:

```
services.AddHealthChecks()
    .AddCheck<ExampleHealthCheck>("example_health_check");
```

The [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) overload shown in the following example sets the failure status ([HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus)) to report when the health check reports a failure. If the failure status is set to `null` (default), [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) is reported. This overload is a useful scenario for library authors, where the failure status indicated by the library is enforced by the app when a health check failure occurs if the health check implementation honors the setting.

*Tags* can be used to filter health checks (described further in the [Filter health checks](#filter-health-checks) section).

```
services.AddHealthChecks()
    .AddCheck<ExampleHealthCheck>(
        "example_health_check",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "example" });
```

[AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) can also execute a lambda function. In the following example, the health check name is specified as `Example` and the check always returns a healthy state:

```
services.AddHealthChecks()
    .AddCheck("Example", () =>
        HealthCheckResult.Healthy("Example is OK!"), tags: new[] { "example" });
```

Call [AddTypeActivatedCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addtypeactivatedcheck) to pass arguments to a health check implementation. In the following example, `TestHealthCheckWithArgs` accepts an integer and a string for use when [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) is called:

```
private class TestHealthCheckWithArgs : IHealthCheck
{
    public TestHealthCheckWithArgs(int i, string s)
    {
        I = i;
        S = s;
    }

    public int I { get; set; }

    public string S { get; set; }

    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, 
        CancellationToken cancellationToken = default)
    {
        ...
    }
}
```

`TestHealthCheckWithArgs` is registered by calling `AddTypeActivatedCheck` with the integer and string passed to the implementation:

```
services.AddHealthChecks()
    .AddTypeActivatedCheck<TestHealthCheckWithArgs>(
        "test", 
        failureStatus: HealthStatus.Degraded, 
        tags: new[] { "example" }, 
        args: new object[] { 5, "string" });
```

## Use Health Checks Routing

In `Startup.Configure`, call `MapHealthChecks` on the endpoint builder with the endpoint URL or relative path:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
});
```

### Require host

Call `RequireHost` to specify one or more permitted hosts for the health check endpoint. Hosts should be Unicode rather than punycode and may include a port. If a collection isn't supplied, any host is accepted.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health").RequireHost("www.contoso.com:5001");
});
```

For more information, see the [Filter by port](#filter-by-port) section.

### Require authorization

Call `RequireAuthorization` to run authorization middleware on the health check request endpoint. A `RequireAuthorization` overload accepts one or more authorization policies. If a policy isn't provided, the default authorization policy is used.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health").RequireAuthorization();
});
```

### Enable Cross-Origin Requests (CORS)

Although running health checks manually from a browser isn't a common use scenario, CORS middleware can be enabled by calling `RequireCors` on health checks endpoints. A `RequireCors` overload accepts a CORS policy builder delegate (`CorsPolicyBuilder`) or a policy name. If a policy isn't provided, the default CORS policy is used. For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0).

## Health check options

[HealthCheckOptions](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions) provide an opportunity to customize health check behavior:

* [Filter health checks](#filter-health-checks)
* [Customize the HTTP status code](#customize-the-http-status-code)
* [Suppress cache headers](#suppress-cache-headers)
* [Customize output](#customize-output)

### Filter health checks

By default, health checks middleware runs all registered health checks. To run a subset of health checks, provide a function that returns a boolean to the [Predicate](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.predicate#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-predicate) option. In the following example, the `Bar` health check is filtered out by its tag (`bar_tag`) in the function's conditional statement, where `true` is only returned if the health check's [Tags](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.tags#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-tags) property matches `foo_tag` or `baz_tag`:

In `Startup.ConfigureServices`:

```
services.AddHealthChecks()
    .AddCheck("Foo", () =>
        HealthCheckResult.Healthy("Foo is OK!"), tags: new[] { "foo_tag" })
    .AddCheck("Bar", () =>
        HealthCheckResult.Unhealthy("Bar is unhealthy!"), tags: new[] { "bar_tag" })
    .AddCheck("Baz", () =>
        HealthCheckResult.Healthy("Baz is OK!"), tags: new[] { "baz_tag" });
```

In `Startup.Configure`, the `Predicate` filters out the 'Bar' health check. Only Foo and Baz execute:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        Predicate = (check) => check.Tags.Contains("foo_tag") ||
            check.Tags.Contains("baz_tag")
    });
});
```

### Customize the HTTP status code

Use [ResultStatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.resultstatuscodes#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-resultstatuscodes) to customize the mapping of health status to HTTP status codes. The following [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) assignments are the default values used by the middleware. Change the status code values to meet your requirements.

In `Startup.Configure`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        ResultStatusCodes =
        {
            [HealthStatus.Healthy] = StatusCodes.Status200OK,
            [HealthStatus.Degraded] = StatusCodes.Status200OK,
            [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
        }
    });
});
```

### Suppress cache headers

[AllowCachingResponses](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.allowcachingresponses#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-allowcachingresponses) controls whether the health checks middleware adds HTTP headers to a probe response to prevent response caching. If the value is `false` (default), the middleware sets or overrides the `Cache-Control`, `Expires`, and `Pragma` headers to prevent response caching. If the value is `true`, the middleware doesn't modify the cache headers of the response.

In `Startup.Configure`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        AllowCachingResponses = true
    });
});
```

### Customize output

In `Startup.Configure`, set the [HealthCheckOptions.ResponseWriter](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.responsewriter) option to a delegate for writing the response:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        ResponseWriter = WriteResponse
    });
});
```

The default delegate writes a minimal plaintext response with the string value of [`HealthReport.Status`](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreport.status#microsoft-extensions-diagnostics-healthchecks-healthreport-status). The following custom delegates output a custom JSON response.

The first example from the sample app demonstrates how to use [System.Text.Json](/en-us/dotnet/api/system.text.json):

```
private static Task WriteResponse(HttpContext context, HealthReport result)
{
    context.Response.ContentType = "application/json; charset=utf-8";

    var options = new JsonWriterOptions
    {
        Indented = true
    };

    using (var stream = new MemoryStream())
    {
        using (var writer = new Utf8JsonWriter(stream, options))
        {
            writer.WriteStartObject();
            writer.WriteString("status", result.Status.ToString());
            writer.WriteStartObject("results");
            foreach (var entry in result.Entries)
            {
                writer.WriteStartObject(entry.Key);
                writer.WriteString("status", entry.Value.Status.ToString());
                writer.WriteString("description", entry.Value.Description);
                writer.WriteStartObject("data");
                foreach (var item in entry.Value.Data)
                {
                    writer.WritePropertyName(item.Key);
                    JsonSerializer.Serialize(
                        writer, item.Value, item.Value?.GetType() ??
                        typeof(object));
                }
                writer.WriteEndObject();
                writer.WriteEndObject();
            }
            writer.WriteEndObject();
            writer.WriteEndObject();
        }

        var json = Encoding.UTF8.GetString(stream.ToArray());

        return context.Response.WriteAsync(json);
    }
}
```

The second example demonstrates how to use [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json):

```
private static Task WriteResponse(HttpContext context, HealthReport result)
{
    context.Response.ContentType = "application/json";

    var json = new JObject(
        new JProperty("status", result.Status.ToString()),
        new JProperty("results", new JObject(result.Entries.Select(pair =>
            new JProperty(pair.Key, new JObject(
                new JProperty("status", pair.Value.Status.ToString()),
                new JProperty("description", pair.Value.Description),
                new JProperty("data", new JObject(pair.Value.Data.Select(
                    p => new JProperty(p.Key, p.Value))))))))));

    return context.Response.WriteAsync(
        json.ToString(Formatting.Indented));
}
```

In the sample app, comment out the `SYSTEM_TEXT_JSON` [preprocessor directive](../overview?view=aspnetcore-10.0#preprocessor-directives-in-sample-code) in `CustomWriterStartup.cs` to enable the `Newtonsoft.Json` version of `WriteResponse`.

The health checks API doesn't provide built-in support for complex JSON return formats because the format is specific to your choice of monitoring system. Customize the response in the preceding examples as needed. For more information on JSON serialization with `System.Text.Json`, see [How to serialize and deserialize JSON in .NET](/en-us/dotnet/standard/serialization/system-text-json-how-to).

## Database probe

A health check can specify a database query to run as a boolean test to indicate if the database is responding normally.

The sample app uses [`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks), a health check library for ASP.NET Core apps, to run a health check on a SQL Server database. `AspNetCore.Diagnostics.HealthChecks` executes a `SELECT 1` query against the database to confirm the connection to the database is healthy.

Warning

When checking a database connection with a query, choose a query that returns quickly. The query approach runs the risk of overloading the database and degrading its performance. In most cases, running a test query isn't necessary. Merely making a successful connection to the database is sufficient. If you find it necessary to run a query, choose a simple SELECT query, such as `SELECT 1`.

Include a package reference to [`AspNetCore.HealthChecks.SqlServer`](https://www.nuget.org/packages/AspNetCore.HealthChecks.SqlServer).

Supply a valid database connection string in the `appsettings.json` file of the sample app. The app uses a SQL Server database named `HealthCheckSample`:

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=HealthCheckSample;Trusted_Connection=True;MultipleActiveResultSets=true;ConnectRetryCount=0"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "Console": {
      "IncludeScopes": "true"
    }
  },
  "AllowedHosts": "*"
}
```

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Startup.ConfigureServices`. The sample app calls the `AddSqlServer` method with the database's connection string (`DbHealthStartup.cs`):

```
services.AddHealthChecks()
    .AddSqlServer(Configuration["ConnectionStrings:DefaultConnection"]);
```

A health check endpoint is created by calling `MapHealthChecks` in `Startup.Configure`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
}
```

To run the database probe scenario using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario db
```

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

## Entity Framework Core DbContext probe

The `DbContext` check confirms that the app can communicate with the database configured for an EF Core `DbContext`. The `DbContext` check is supported in apps that:

* Use [Entity Framework (EF) Core](/en-us/ef/core/).
* Include a package reference to [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore).

`AddDbContextCheck<TContext>` registers a health check for a `DbContext`. The `DbContext` is supplied as the `TContext` to the method. An overload is available to configure the failure status, tags, and a custom test query.

By default:

* The `DbContextHealthCheck` calls EF Core's `CanConnectAsync` method. You can customize what operation is run when checking health using `AddDbContextCheck` method overloads.
* The name of the health check is the name of the `TContext` type.

In the sample app, `AppDbContext` is provided to `AddDbContextCheck` and registered as a service in `Startup.ConfigureServices` (`DbContextHealthStartup.cs`):

```
services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>();

services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(
        Configuration["ConnectionStrings:DefaultConnection"]);
});
```

A health check endpoint is created by calling `MapHealthChecks` in `Startup.Configure`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
}
```

To run the `DbContext` probe scenario using the sample app, confirm that the database specified by the connection string doesn't exist in the SQL Server instance. If the database exists, delete it.

Execute the following command from the project's folder in a command shell:

```
dotnet run --scenario dbcontext
```

After the app is running, check the health status by making a request to the `/health` endpoint in a browser. The database and `AppDbContext` don't exist, so app provides the following response:

```
Unhealthy
```

Trigger the sample app to create the database. Make a request to `/createdatabase`. The app responds:

```
Creating the database...
Done!
Navigate to /health to see the health status.
```

Make a request to the `/health` endpoint. The database and context exist, so app responds:

```
Healthy
```

Trigger the sample app to delete the database. Make a request to `/deletedatabase`. The app responds:

```
Deleting the database...
Done!
Navigate to /health to see the health status.
```

Make a request to the `/health` endpoint. The app provides an unhealthy response:

```
Unhealthy
```

## Separate readiness and liveness probes

In some hosting scenarios, a pair of health checks is used to distinguish two app states:

* *Readiness* indicates if the app is running normally but isn't ready to receive requests.
* *Liveness* indicates if an app has crashed and must be restarted.

Consider the following example: An app must download a large configuration file before it's ready to process requests. We don't want the app to be restarted if the initial download fails because the app can retry downloading the file several times. We use a *liveness probe* to describe the liveness of the process, no other checks are run. We also want to prevent requests from being sent to the app before the configuration file download has succeeded. We use a *readiness probe* to indicate a "not ready" state until the download succeeds and the app is ready to receive requests.

The sample app contains a health check to report the completion of long-running startup task in a [Hosted Service](../fundamentals/host/hosted-services?view=aspnetcore-10.0). The `StartupHostedServiceHealthCheck` exposes a property, `StartupTaskCompleted`, that the hosted service can set to `true` when its long-running task is finished (`StartupHostedServiceHealthCheck.cs`):

```
public class StartupHostedServiceHealthCheck : IHealthCheck
{
    private volatile bool _startupTaskCompleted = false;

    public string Name => "slow_dependency_check";

    public bool StartupTaskCompleted
    {
        get => _startupTaskCompleted;
        set => _startupTaskCompleted = value;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        if (StartupTaskCompleted)
        {
            return Task.FromResult(
                HealthCheckResult.Healthy("The startup task is finished."));
        }

        return Task.FromResult(
            HealthCheckResult.Unhealthy("The startup task is still running."));
    }
}
```

The long-running background task is started by a [Hosted Service](../fundamentals/host/hosted-services?view=aspnetcore-10.0) (`Services/StartupHostedService`). At the conclusion of the task, `StartupHostedServiceHealthCheck.StartupTaskCompleted` is set to `true`:

```
public class StartupHostedService : IHostedService, IDisposable
{
    private readonly int _delaySeconds = 15;
    private readonly ILogger _logger;
    private readonly StartupHostedServiceHealthCheck _startupHostedServiceHealthCheck;

    public StartupHostedService(ILogger<StartupHostedService> logger,
        StartupHostedServiceHealthCheck startupHostedServiceHealthCheck)
    {
        _logger = logger;
        _startupHostedServiceHealthCheck = startupHostedServiceHealthCheck;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Startup Background Service is starting.");

        // Simulate the effect of a long-running startup task.
        Task.Run(async () =>
        {
            await Task.Delay(_delaySeconds * 1000);

            _startupHostedServiceHealthCheck.StartupTaskCompleted = true;

            _logger.LogInformation("Startup Background Service has started.");
        });

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Startup Background Service is stopping.");

        return Task.CompletedTask;
    }

    public void Dispose()
    {
    }
}
```

The health check is registered with [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) in `Startup.ConfigureServices` along with the hosted service. Because the hosted service must set the property on the health check, the health check is also registered in the service container (`LivenessProbeStartup.cs`):

```
services.AddHostedService<StartupHostedService>();
services.AddSingleton<StartupHostedServiceHealthCheck>();

services.AddHealthChecks()
    .AddCheck<StartupHostedServiceHealthCheck>(
        "hosted_service_startup",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "ready" });

services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.FromSeconds(2);
    options.Predicate = (check) => check.Tags.Contains("ready");
});

services.AddSingleton<IHealthCheckPublisher, ReadinessPublisher>();
```

A health check endpoint is created by calling `MapHealthChecks` in `Startup.Configure`. In the sample app, the health check endpoints are created at:

* `/health/ready` for the readiness check. The readiness check filters health checks to the health check with the `ready` tag.
* `/health/live` for the liveness check. The liveness check filters out the `StartupHostedServiceHealthCheck` by returning `false` in the [`HealthCheckOptions.Predicate`](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.predicate#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-predicate) (for more information, see [Filter health checks](#filter-health-checks))

In the following example code:

* The readiness check uses all registered checks with the 'ready' tag.
* The `Predicate` excludes all checks and returns a 200-Ok.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health/ready", new HealthCheckOptions()
    {
        Predicate = (check) => check.Tags.Contains("ready"),
    });

    endpoints.MapHealthChecks("/health/live", new HealthCheckOptions()
    {
        Predicate = (_) => false
    });
}
```

To run the readiness/liveness configuration scenario using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario liveness
```

In a browser, visit `/health/ready` several times until 15 seconds have passed. The health check reports `Unhealthy` for the first 15 seconds. After 15 seconds, the endpoint reports `Healthy`, which reflects the completion of the long-running task by the hosted service.

This example also creates a Health Check Publisher ([IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) implementation) that runs the first readiness check with a two-second delay. For more information, see the [Health Check Publisher](#health-check-publisher) section.

### Kubernetes example

Using separate readiness and liveness checks is useful in an environment such as [Kubernetes](https://kubernetes.io/). In Kubernetes, an app might be required to run time-consuming startup work before accepting requests, such as a test of the underlying database availability. Using separate checks allows the orchestrator to distinguish whether the app is functioning but not yet ready or if the app has failed to start. For more information on readiness and liveness probes in Kubernetes, see [Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/) in the Kubernetes documentation.

The following example demonstrates a Kubernetes readiness probe configuration:

```
spec:
  template:
  spec:
    readinessProbe:
      # an http probe
      httpGet:
        path: /health/ready
        port: 80
      # length of time to wait for a pod to initialize
      # after pod startup, before applying health checking
      initialDelaySeconds: 30
      timeoutSeconds: 1
    ports:
      - containerPort: 80
```

## Metric-based probe with a custom response writer

The sample app demonstrates a memory health check with a custom response writer.

`MemoryHealthCheck` reports a degraded status if the app uses more than a given threshold of memory (1 GB in the sample app). The [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) includes Garbage Collector (GC) information for the app (`MemoryHealthCheck.cs`):

```
public class MemoryHealthCheck : IHealthCheck
{
    private readonly IOptionsMonitor<MemoryCheckOptions> _options;

    public MemoryHealthCheck(IOptionsMonitor<MemoryCheckOptions> options)
    {
        _options = options;
    }

    public string Name => "memory_check";

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        var options = _options.Get(context.Registration.Name);

        // Include GC information in the reported diagnostics.
        var allocated = GC.GetTotalMemory(forceFullCollection: false);
        var data = new Dictionary<string, object>()
        {
            { "AllocatedBytes", allocated },
            { "Gen0Collections", GC.CollectionCount(0) },
            { "Gen1Collections", GC.CollectionCount(1) },
            { "Gen2Collections", GC.CollectionCount(2) },
        };
        var status = (allocated < options.Threshold) ?
            HealthStatus.Healthy : context.Registration.FailureStatus;

        return Task.FromResult(new HealthCheckResult(
            status,
            description: "Reports degraded status if allocated bytes " +
                $">= {options.Threshold} bytes.",
            exception: null,
            data: data));
    }
}
```

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Startup.ConfigureServices`. Instead of enabling the health check by passing it to [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck), the `MemoryHealthCheck` is registered as a service. All [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) registered services are available to the health check services and middleware. We recommend registering health check services as Singleton services.

In `CustomWriterStartup.cs` of the sample app:

```
services.AddHealthChecks()
    .AddMemoryHealthCheck("memory");
```

A health check endpoint is created by calling `MapHealthChecks` in `Startup.Configure`. A `WriteResponse` delegate is provided to the [ResponseWriter](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.responsewriter#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-responsewriter) property to output a custom JSON response when the health check executes:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health", new HealthCheckOptions()
    {
        ResponseWriter = WriteResponse
    });
}
```

The `WriteResponse` delegate formats the `CompositeHealthCheckResult` into a JSON object and yields JSON output for the health check response. For more information, see the [Customize output](#customize-output) section.

To run the metric-based probe with custom response writer output using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario writer
```

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) includes metric-based health check scenarios, including disk storage and maximum value liveness checks.

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

## Filter by port

Call `RequireHost` on `MapHealthChecks` with a URL pattern that specifies a port to restrict health check requests to the port specified. This approach is typically used in a container environment to expose a port for monitoring services.

The sample app configures the port using the [Environment Variable Configuration Provider](../fundamentals/configuration/?view=aspnetcore-10.0#environment-variables). The port is set in the `launchSettings.json` file and passed to the configuration provider via an environment variable. You must also configure the server to listen to requests on the management port.

To use the sample app to demonstrate management port configuration, create the `launchSettings.json` file in a `Properties` folder.

The following `Properties/launchSettings.json` file in the sample app isn't included in the sample app's project files and must be created manually:

```
{
  "profiles": {
    "SampleApp": {
      "commandName": "Project",
      "commandLineArgs": "",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ASPNETCORE_URLS": "http://localhost:5000/;http://localhost:5001/",
        "ASPNETCORE_MANAGEMENTPORT": "5001"
      },
      "applicationUrl": "http://localhost:5000/"
    }
  }
}
```

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Startup.ConfigureServices`. Create a health check endpoint by calling `MapHealthChecks` in `Startup.Configure`.

In the sample app, a call to `RequireHost` on the endpoint in `Startup.Configure` specifies the management port from configuration:

```
endpoints.MapHealthChecks("/health")
    .RequireHost($"*:{Configuration["ManagementPort"]}");
```

Endpoints are created in the sample app in `Startup.Configure`. In the following example code:

* The readiness check uses all registered checks with the 'ready' tag.
* The `Predicate` excludes all checks and returns a 200-Ok.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health/ready", new HealthCheckOptions()
    {
        Predicate = (check) => check.Tags.Contains("ready"),
    });

    endpoints.MapHealthChecks("/health/live", new HealthCheckOptions()
    {
        Predicate = (_) => false
    });
}
```

Note

You can avoid creating the `launchSettings.json` file in the sample app by setting the management port explicitly in code. In `Program.cs` where the [HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder) is created, add a call to [ListenAnyIP](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenanyip) and provide the app's management port endpoint. In `Configure` of `ManagementPortStartup.cs`, specify the management port with `RequireHost`:

`Program.cs`:

```
return new HostBuilder()
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseKestrel()
            .ConfigureKestrel(serverOptions =>
            {
                serverOptions.ListenAnyIP(5001);
            })
            .UseStartup(startupType);
    })
    .Build();
```

`ManagementPortStartup.cs`:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health").RequireHost("*:5001");
});
```

To run the management port configuration scenario using the sample app, execute the following command from the project's folder in a command shell:

```
dotnet run --scenario port
```

## Distribute a health check library

To distribute a health check as a library:

1. Write a health check that implements the [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) interface as a standalone class. The class can rely on [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0), type activation, and [named options](../fundamentals/configuration/options?view=aspnetcore-10.0) to access configuration data.

   In the health checks logic of `CheckHealthAsync`:

   * `data1` and `data2` are used in the method to run the probe's health check logic.
   * `AccessViolationException` is handled.

   When an [AccessViolationException](/en-us/dotnet/api/system.accessviolationexception) occurs, the [FailureStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.failurestatus#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-failurestatus) is returned with the [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) to allow users to configure the health checks failure status.

   ```
   using System;
   using System.Threading;
   using System.Threading.Tasks;
   using Microsoft.Extensions.Diagnostics.HealthChecks;

   namespace SampleApp
   {
       public class ExampleHealthCheck : IHealthCheck
       {
           private readonly string _data1;
           private readonly int? _data2;

           public ExampleHealthCheck(string data1, int? data2)
           {
               _data1 = data1 ?? throw new ArgumentNullException(nameof(data1));
               _data2 = data2 ?? throw new ArgumentNullException(nameof(data2));
           }

           public async Task<HealthCheckResult> CheckHealthAsync(
               HealthCheckContext context, CancellationToken cancellationToken)
           {
               try
               {
                   return HealthCheckResult.Healthy();
               }
               catch (AccessViolationException ex)
               {
                   return new HealthCheckResult(
                       context.Registration.FailureStatus,
                       description: "An access violation occurred during the check.",
                       exception: ex,
                       data: null);
               }
           }
       }
   }
   ```

2. Write an extension method with parameters that the consuming app calls in its `Startup.Configure` method. In the following example, assume the following health check method signature:

   ```
   ExampleHealthCheck(string, string, int )
   ```

   The preceding signature indicates that the `ExampleHealthCheck` requires additional data to process the health check probe logic. The data is provided to the delegate used to create the health check instance when the health check is registered with an extension method. In the following example, the caller specifies optional:

   * health check name (`name`). If `null`, `example_health_check` is used.
   * string data point for the health check (`data1`).
   * integer data point for the health check (`data2`). If `null`, `1` is used.
   * failure status ([HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus)). The default is `null`. If `null`, [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) is reported for a failure status.
   * tags (`IEnumerable<string>`).

   ```
   using System.Collections.Generic;
   using Microsoft.Extensions.Diagnostics.HealthChecks;

   public static class ExampleHealthCheckBuilderExtensions
   {
       const string DefaultName = "example_health_check";

       public static IHealthChecksBuilder AddExampleHealthCheck(
           this IHealthChecksBuilder builder,
           string name = default,
           string data1,
           int data2 = 1,
           HealthStatus? failureStatus = default,
           IEnumerable<string> tags = default)
       {
           return builder.Add(new HealthCheckRegistration(
               name ?? DefaultName,
               sp => new ExampleHealthCheck(data1, data2),
               failureStatus,
               tags));
       }
   }
   ```

## Health Check Publisher

When an [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) is added to the service container, the health check system periodically executes your health checks and calls `PublishAsync` with the result. This is useful in a push-based health monitoring system scenario that expects each process to call the monitoring system periodically in order to determine health.

The [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) interface has a single method:

```
Task PublishAsync(HealthReport report, CancellationToken cancellationToken);
```

[HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions) allow you to set:

* [Delay](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.delay#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-delay): The initial delay applied after the app starts before executing [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) instances. The delay is applied once at startup and doesn't apply to subsequent iterations. The default value is five seconds.
* [Period](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.period#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-period): The period of [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) execution. The default value is 30 seconds.
* [Predicate](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.predicate#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-predicate): If [Predicate](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.predicate#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-predicate) is `null` (default), the health check publisher service runs all registered health checks. To run a subset of health checks, provide a function that filters the set of checks. The predicate is evaluated each period.
* [Timeout](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.timeout#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-timeout): The timeout for executing the health checks for all [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) instances. Use [InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan) to execute without a timeout. The default value is 30 seconds.

In the sample app, `ReadinessPublisher` is an [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) implementation. The health check status is logged for each check at a log level of:

* Information ([LogInformation](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.loginformation)) if the health checks status is [Healthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-healthy).
* Error ([LogError](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.logerror)) if the status is either [Degraded](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-degraded) or [Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy).

```
public class ReadinessPublisher : IHealthCheckPublisher
{
    private readonly ILogger _logger;

    public ReadinessPublisher(ILogger<ReadinessPublisher> logger)
    {
        _logger = logger;
    }

    // The following example is for demonstration purposes only. Health Checks
    // Middleware already logs health checks results. A real-world readiness
    // check in a production app might perform a set of more expensive or
    // time-consuming checks to determine if other resources are responding
    // properly.
    public Task PublishAsync(HealthReport report,
        CancellationToken cancellationToken)
    {
        if (report.Status == HealthStatus.Healthy)
        {
            _logger.LogInformation("{Timestamp} Readiness Probe Status: {Result}",
                DateTime.UtcNow, report.Status);
        }
        else
        {
            _logger.LogError("{Timestamp} Readiness Probe Status: {Result}",
                DateTime.UtcNow, report.Status);
        }

        cancellationToken.ThrowIfCancellationRequested();

        return Task.CompletedTask;
    }
}
```

In the sample app's `LivenessProbeStartup` example, the `StartupHostedService` readiness check has a two second startup delay and runs the check every 30 seconds. To activate the [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) implementation, the sample registers `ReadinessPublisher` as a singleton service in the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container:

```
services.AddHostedService<StartupHostedService>();
services.AddSingleton<StartupHostedServiceHealthCheck>();

services.AddHealthChecks()
    .AddCheck<StartupHostedServiceHealthCheck>(
        "hosted_service_startup",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "ready" });

services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.FromSeconds(2);
    options.Predicate = (check) => check.Tags.Contains("ready");
});

services.AddSingleton<IHealthCheckPublisher, ReadinessPublisher>();
```

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) includes publishers for several systems, including [Application Insights](/en-us/azure/application-insights/app-insights-overview).

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

## Restrict health checks with MapWhen

Use [MapWhen](/en-us/dotnet/api/microsoft.aspnetcore.builder.mapwhenextensions.mapwhen) to conditionally branch the request pipeline for health check endpoints.

In the following example, `MapWhen` branches the request pipeline to activate health checks middleware if a GET request is received for the `api/HealthCheck` endpoint:

```
app.MapWhen(
    context => context.Request.Method == HttpMethod.Get.Method && 
        context.Request.Path.StartsWith("/api/HealthCheck"),
    builder => builder.UseHealthChecks());

app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
});
```

For more information, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline).

ASP.NET Core offers health checks middleware and libraries for reporting the health of app infrastructure components.

Health checks are exposed by an app as HTTP endpoints. Health check endpoints can be configured for various real-time monitoring scenarios:

* Health probes can be used by container orchestrators and load balancers to check an app's status. For example, a container orchestrator may respond to a failing health check by halting a rolling deployment or restarting a container. A load balancer might react to an unhealthy app by routing traffic away from the failing instance to a healthy instance.
* Use of memory, disk, and other physical server resources can be monitored for healthy status.
* Health checks can test an app's dependencies, such as databases and external service endpoints, to confirm availability and normal functioning.

Health checks are typically used with an external monitoring service or container orchestrator to check the status of an app. Before adding health checks to an app, decide on which monitoring system to use. The monitoring system dictates what types of health checks to create and how to configure their endpoints.

## Basic health probe

For many apps, a basic health probe configuration that reports the app's availability to process requests (*liveness*) is sufficient to discover the status of the app.

The basic configuration registers health check services and calls the health checks middleware to respond at a URL endpoint with a health response. By default, no specific health checks are registered to test any particular dependency or subsystem. The app is considered healthy if it can respond at the health endpoint URL. The default response writer writes [HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus) as a plaintext response to the client. The `HealthStatus` is [HealthStatus.Healthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-healthy), [HealthStatus.Degraded](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-degraded), or [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy).

Register health check services with [AddHealthChecks](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthcheckservicecollectionextensions.addhealthchecks) in `Program.cs`. Create a health check endpoint by calling [MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks).

The following example creates a health check endpoint at `/healthz`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/healthz");

app.Run();
```

### Docker `HEALTHCHECK`

[Docker](docker/?view=aspnetcore-10.0) offers a built-in `HEALTHCHECK` directive that can be used to check the status of an app that uses the basic health check configuration:

```
HEALTHCHECK CMD curl --fail http://localhost:5000/healthz || exit
```

The preceding example uses `curl` to make an HTTP request to the health check endpoint at `/healthz`. `curl` isn't included in the .NET Linux container images, but it can be added by installing the required package in the Dockerfile. Containers that use images based on Alpine Linux can use the included `wget` in place of `curl`.

## Create health checks

Health checks are created by implementing the [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) interface. The [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) method returns a [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) that indicates the health as `Healthy`, `Degraded`, or `Unhealthy`. The result is written as a plaintext response with a configurable status code. Configuration is described in the [Health check options](#health-check-options) section. [HealthCheckResult](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckresult) can also return optional key-value pairs.

The following example demonstrates the layout of a health check:

```
public class SampleHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        var isHealthy = true;

        // ...

        if (isHealthy)
        {
            return Task.FromResult(
                HealthCheckResult.Healthy("A healthy result."));
        }

        return Task.FromResult(
            new HealthCheckResult(
                context.Registration.FailureStatus, "An unhealthy result."));
    }
}
```

The health check's logic is placed in the [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) method. The preceding example sets a dummy variable, `isHealthy`, to `true`. If the value of `isHealthy` is set to `false`, the [HealthCheckRegistration.FailureStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.failurestatus#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-failurestatus) status is returned.

If [CheckHealthAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck.checkhealthasync) throws an exception during the check, a new [HealthReportEntry](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry) is returned with its [HealthReportEntry.Status](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry.status#microsoft-extensions-diagnostics-healthchecks-healthreportentry-status) set to the [FailureStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckregistration.failurestatus#microsoft-extensions-diagnostics-healthchecks-healthcheckregistration-failurestatus). This status is defined by [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) (see the [Register health check services](#register-health-check-services) section) and includes the [inner exception](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry.exception#microsoft-extensions-diagnostics-healthchecks-healthreportentry-exception) that caused the check failure. The [Description](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreportentry.description#microsoft-extensions-diagnostics-healthchecks-healthreportentry-description) is set to the exception's message.

## Register health check services

To register a health check service, call [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) in `Program.cs`:

```
builder.Services.AddHealthChecks()
    .AddCheck<SampleHealthCheck>("Sample");
```

The [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) overload shown in the following example sets the failure status ([HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus)) to report when the health check reports a failure. If the failure status is set to `null` (default), [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) is reported. This overload is a useful scenario for library authors, where the failure status indicated by the library is enforced by the app when a health check failure occurs if the health check implementation honors the setting.

*Tags* can be used to filter health checks. Tags are described in the [Filter health checks](#filter-health-checks) section.

```
builder.Services.AddHealthChecks()
    .AddCheck<SampleHealthCheck>(
        "Sample",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "sample" });
```

[AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderdelegateextensions.addcheck) can also execute a lambda function. In the following example, the health check always returns a healthy result:

```
builder.Services.AddHealthChecks()
    .AddCheck("Sample", () => HealthCheckResult.Healthy("A healthy result."));
```

Call [AddTypeActivatedCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addtypeactivatedcheck) to pass arguments to a health check implementation. In the following example, a type-activated health check accepts an integer and a string in its constructor:

```
public class SampleHealthCheckWithArgs : IHealthCheck
{
    private readonly int _arg1;
    private readonly string _arg2;

    public SampleHealthCheckWithArgs(int arg1, string arg2)
        => (_arg1, _arg2) = (arg1, arg2);

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        // ...

        return Task.FromResult(HealthCheckResult.Healthy("A healthy result."));
    }
}
```

To register the preceding health check, call `AddTypeActivatedCheck` with the integer and string passed as arguments:

```
builder.Services.AddHealthChecks()
    .AddTypeActivatedCheck<SampleHealthCheckWithArgs>(
        "Sample",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "sample" },
        args: new object[] { 1, "Arg" });
```

## Use Health Checks Routing

In `Program.cs`, call `MapHealthChecks` on the endpoint builder with the endpoint URL or relative path:

```
app.MapHealthChecks("/healthz");
```

### Require host

Call `RequireHost` to specify one or more permitted hosts for the health check endpoint. Hosts should be Unicode rather than punycode and may include a port. If a collection isn't supplied, any host is accepted:

```
app.MapHealthChecks("/healthz")
    .RequireHost("www.contoso.com:5001");
```

To restrict the health check endpoint to respond only on a specific port, specify a port in the call to `RequireHost`. This approach is typically used in a container environment to expose a port for monitoring services:

```
app.MapHealthChecks("/healthz")
    .RequireHost("*:5001");
```

Warning

API that relies on the [Host header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Host), such as [HttpRequest.Host](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.host) and [RequireHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.requirehost), are subject to potential spoofing by clients.

To prevent host and port spoofing, use one of the following approaches:

* Use [HttpContext.Connection](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.connection) ([ConnectionInfo.LocalPort](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.localport#microsoft-aspnetcore-http-connectioninfo-localport)) where the ports are checked.
* Employ [Host filtering](../fundamentals/servers/kestrel/host-filtering?view=aspnetcore-10.0).

To prevent unauthorized clients from spoofing the port, call [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization):

```
app.MapHealthChecks("/healthz")
    .RequireHost("*:5001")
    .RequireAuthorization();
```

For more information, see [Host matching in routes with RequireHost](../fundamentals/routing?view=aspnetcore-10.0#host-matching-in-routes-with-requirehost).

### Require authorization

Call [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) to run authorization middleware on the health check request endpoint. A `RequireAuthorization` overload accepts one or more authorization policies. If a policy isn't provided, the default authorization policy is used:

```
app.MapHealthChecks("/healthz")
    .RequireAuthorization();
```

### Enable Cross-Origin Requests (CORS)

Although running health checks manually from a browser isn't a common scenario, CORS middleware can be enabled by calling [`RequireCors`](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsEndpointConventionBuilderExtensions.cs#L20-#L40) on the health checks endpoints. The [`RequireCors`](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsendpointconventionbuilderextensions.requirecors) overload accepts a CORS policy builder delegate (`CorsPolicyBuilder`) or a policy name. For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0).

## Health check options

[HealthCheckOptions](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions) provide an opportunity to customize health check behavior:

* [Filter health checks](#filter-health-checks)
* [Customize the HTTP status code](#customize-the-http-status-code)
* [Suppress cache headers](#suppress-cache-headers)
* [Customize output](#customize-output)

### Filter health checks

By default, the health checks middleware runs all registered health checks. To run a subset of health checks, provide a function that returns a boolean to the [Predicate](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.predicate#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-predicate) option.

The following example filters the health checks so that only those tagged with `sample` run:

```
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    Predicate = healthCheck => healthCheck.Tags.Contains("sample")
});
```

### Customize the HTTP status code

Use [ResultStatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.resultstatuscodes#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-resultstatuscodes) to customize the mapping of health status to HTTP status codes. The following [StatusCodes](/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes) assignments are the default values used by the middleware. Change the status code values to meet your requirements:

```
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    ResultStatusCodes =
    {
        [HealthStatus.Healthy] = StatusCodes.Status200OK,
        [HealthStatus.Degraded] = StatusCodes.Status200OK,
        [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
    }
});
```

### Suppress cache headers

[AllowCachingResponses](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.allowcachingresponses#microsoft-aspnetcore-diagnostics-healthchecks-healthcheckoptions-allowcachingresponses) controls whether the health checks middleware adds HTTP headers to a probe response to prevent response caching. If the value is `false` (default), the middleware sets or overrides the `Cache-Control`, `Expires`, and `Pragma` headers to prevent response caching. If the value is `true`, the middleware doesn't modify the cache headers of the response:

```
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    AllowCachingResponses = true
});
```

### Customize output

To customize the output of a health checks report, set the [HealthCheckOptions.ResponseWriter](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.responsewriter) property to a delegate that writes the response:

```
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    ResponseWriter = WriteResponse
});
```

The default delegate writes a minimal plaintext response with the string value of [`HealthReport.Status`](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthreport.status#microsoft-extensions-diagnostics-healthchecks-healthreport-status). The following custom delegate outputs a custom JSON response using [System.Text.Json](/en-us/dotnet/api/system.text.json):

```
private static Task WriteResponse(HttpContext context, HealthReport healthReport)
{
    context.Response.ContentType = "application/json; charset=utf-8";

    var options = new JsonWriterOptions { Indented = true };

    using var memoryStream = new MemoryStream();
    using (var jsonWriter = new Utf8JsonWriter(memoryStream, options))
    {
        jsonWriter.WriteStartObject();
        jsonWriter.WriteString("status", healthReport.Status.ToString());
        jsonWriter.WriteStartObject("results");

        foreach (var healthReportEntry in healthReport.Entries)
        {
            jsonWriter.WriteStartObject(healthReportEntry.Key);
            jsonWriter.WriteString("status",
                healthReportEntry.Value.Status.ToString());
            jsonWriter.WriteString("description",
                healthReportEntry.Value.Description);
            jsonWriter.WriteStartObject("data");

            foreach (var item in healthReportEntry.Value.Data)
            {
                jsonWriter.WritePropertyName(item.Key);

                JsonSerializer.Serialize(jsonWriter, item.Value,
                    item.Value?.GetType() ?? typeof(object));
            }

            jsonWriter.WriteEndObject();
            jsonWriter.WriteEndObject();
        }

        jsonWriter.WriteEndObject();
        jsonWriter.WriteEndObject();
    }

    return context.Response.WriteAsync(
        Encoding.UTF8.GetString(memoryStream.ToArray()));
}
```

The health checks API doesn't provide built-in support for complex JSON return formats because the format is specific to your choice of monitoring system. Customize the response in the preceding examples as needed. For more information on JSON serialization with `System.Text.Json`, see [How to serialize and deserialize JSON in .NET](/en-us/dotnet/standard/serialization/system-text-json-how-to).

## Database probe

A health check can specify a database query to run as a boolean test to indicate if the database is responding normally.

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks), a health check library for ASP.NET Core apps, includes a health check that runs against a SQL Server database. `AspNetCore.Diagnostics.HealthChecks` executes a `SELECT 1` query against the database to confirm the connection to the database is healthy.

Warning

When checking a database connection with a query, choose a query that returns quickly. The query approach runs the risk of overloading the database and degrading its performance. In most cases, running a test query isn't necessary. Merely making a successful connection to the database is sufficient. If you find it necessary to run a query, choose a simple SELECT query, such as `SELECT 1`.

To use this SQL Server health check, include a package reference to the [`AspNetCore.HealthChecks.SqlServer`](https://www.nuget.org/packages/AspNetCore.HealthChecks.SqlServer) NuGet package. The following example registers the SQL Server health check:

```
builder.Services.AddHealthChecks()
    .AddSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection"));
```

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

## Entity Framework Core DbContext probe

The `DbContext` check confirms that the app can communicate with the database configured for an EF Core `DbContext`. The `DbContext` check is supported in apps that:

* Use [Entity Framework (EF) Core](/en-us/ef/core/).
* Include a package reference to the [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore) NuGet package.

[AddDbContextCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkcorehealthchecksbuilderextensions.adddbcontextcheck) registers a health check for a [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext). The `DbContext` is supplied to the method as the `TContext`. An overload is available to configure the failure status, tags, and a custom test query.

By default:

* The `DbContextHealthCheck` calls EF Core's `CanConnectAsync` method. You can customize what operation is run when checking health using `AddDbContextCheck` method overloads.
* The name of the health check is the name of the `TContext` type.

The following example registers a `DbContext` and an associated `DbContextHealthCheck`:

```
builder.Services.AddDbContext<SampleDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddHealthChecks()
    .AddDbContextCheck<SampleDbContext>();
```

## Separate readiness and liveness probes

In some hosting scenarios, a pair of health checks is used to distinguish two app states:

* *Readiness* indicates if the app is running normally but isn't ready to receive requests.
* *Liveness* indicates if an app has crashed and must be restarted.

Consider the following example: An app must download a large configuration file before it's ready to process requests. We don't want the app to be restarted if the initial download fails because the app can retry downloading the file several times. We use a *liveness probe* to describe the liveness of the process, no other checks are run. We also want to prevent requests from being sent to the app before the configuration file download has succeeded. We use a *readiness probe* to indicate a "not ready" state until the download succeeds and the app is ready to receive requests.

The following background task simulates a startup process that takes roughly 15 seconds. Once it completes, the task sets the `StartupHealthCheck.StartupCompleted` property to true:

```
public class StartupBackgroundService : BackgroundService
{
    private readonly StartupHealthCheck _healthCheck;

    public StartupBackgroundService(StartupHealthCheck healthCheck)
        => _healthCheck = healthCheck;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Simulate the effect of a long-running task.
        await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);

        _healthCheck.StartupCompleted = true;
    }
}
```

The `StartupHealthCheck` reports the completion of the long-running startup task and exposes the `StartupCompleted` property that gets set by the background service:

```
public class StartupHealthCheck : IHealthCheck
{
    private volatile bool _isReady;

    public bool StartupCompleted
    {
        get => _isReady;
        set => _isReady = value;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        if (StartupCompleted)
        {
            return Task.FromResult(HealthCheckResult.Healthy("The startup task has completed."));
        }

        return Task.FromResult(HealthCheckResult.Unhealthy("That startup task is still running."));
    }
}
```

The health check is registered with [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) in `Program.cs` along with the hosted service. Because the hosted service must set the property on the health check, the health check is also registered in the service container as a singleton:

```
builder.Services.AddHostedService<StartupBackgroundService>();
builder.Services.AddSingleton<StartupHealthCheck>();

builder.Services.AddHealthChecks()
    .AddCheck<StartupHealthCheck>(
        "Startup",
        tags: new[] { "ready" });
```

To create two different health check endpoints, call `MapHealthChecks` twice:

```
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = healthCheck => healthCheck.Tags.Contains("ready")
});

app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
    Predicate = _ => false
});
```

The preceding example creates the following health check endpoints:

* `/healthz/ready` for the readiness check. The readiness check filters health checks to those tagged with `ready`.
* `/healthz/live` for the liveness check. The liveness check filters out all health checks by returning `false` in the [HealthCheckOptions.Predicate](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.healthchecks.healthcheckoptions.predicate) delegate. For more information on filtering health checks, see [Filter health checks](#filter-health-checks) in this article.

Before the startup task completes, the `/healthz/ready` endpoint reports an `Unhealthy` status. Once the startup task completes, this endpoint reports a `Healthy` status. The `/healthz/live` endpoint excludes all checks and reports a `Healthy` status for all calls.

### Kubernetes example

Using separate readiness and liveness checks is useful in an environment such as [Kubernetes](https://kubernetes.io/). In Kubernetes, an app might be required to run time-consuming startup work before accepting requests, such as a test of the underlying database availability. Using separate checks allows the orchestrator to distinguish whether the app is functioning but not yet ready or if the app has failed to start. For more information on readiness and liveness probes in Kubernetes, see [Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/) in the Kubernetes documentation.

The following example demonstrates a Kubernetes readiness probe configuration:

```
spec:
  template:
  spec:
    readinessProbe:
      # an http probe
      httpGet:
        path: /healthz/ready
        port: 80
      # length of time to wait for a pod to initialize
      # after pod startup, before applying health checking
      initialDelaySeconds: 30
      timeoutSeconds: 1
    ports:
      - containerPort: 80
```

## Distribute a health check library

To distribute a health check as a library:

1. Write a health check that implements the [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) interface as a standalone class. The class can rely on [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0), type activation, and [named options](../fundamentals/configuration/options?view=aspnetcore-10.0) to access configuration data.

2. Write an extension method with parameters that the consuming app calls in its `Program.cs` method. Consider the following example health check, which accepts `arg1` and `arg2` as constructor parameters:

   ```
   public SampleHealthCheckWithArgs(int arg1, string arg2)
       => (_arg1, _arg2) = (arg1, arg2);
   ```

   The preceding signature indicates that the health check requires custom data to process the health check probe logic. The data is provided to the delegate used to create the health check instance when the health check is registered with an extension method. In the following example, the caller specifies:

   * `arg1`: An integer data point for the health check.
   * `arg2`: A string argument for the health check.
   * `name`: An optional health check name. If `null`, a default value is used.
   * `failureStatus`: An optional [HealthStatus](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus), which is reported for a failure status. If `null`, [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy) is used.
   * `tags`: An optional `IEnumerable<string>` collection of tags.

   ```
   public static class SampleHealthCheckBuilderExtensions
   {
       private const string DefaultName = "Sample";

       public static IHealthChecksBuilder AddSampleHealthCheck(
           this IHealthChecksBuilder healthChecksBuilder,
           int arg1,
           string arg2,
           string? name = null,
           HealthStatus? failureStatus = null,
           IEnumerable<string>? tags = default)
       {
           return healthChecksBuilder.Add(
               new HealthCheckRegistration(
                   name ?? DefaultName,
                   _ => new SampleHealthCheckWithArgs(arg1, arg2),
                   failureStatus,
                   tags));
       }
   }
   ```

## Health Check Publisher

When an [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) is added to the service container, the health check system periodically executes your health checks and calls [PublishAsync](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher.publishasync) with the result. This process is useful in a push-based health monitoring system scenario that expects each process to call the monitoring system periodically to determine health.

[HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions) allow you to set the:

* [Delay](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.delay#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-delay): The initial delay applied after the app starts before executing [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) instances. The delay is applied once at startup and doesn't apply to later iterations. The default value is five seconds.
* [Period](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.period#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-period): The period of [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) execution. The default value is 30 seconds.
* [Predicate](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.predicate#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-predicate): If [Predicate](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.predicate#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-predicate) is `null` (default), the health check publisher service runs all registered health checks. To run a subset of health checks, provide a function that filters the set of checks. The predicate is evaluated each period.
* [Timeout](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions.timeout#microsoft-extensions-diagnostics-healthchecks-healthcheckpublisheroptions-timeout): The timeout for executing the health checks for all [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) instances. Use [InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan) to execute without a timeout. The default value is 30 seconds.

The following example demonstrates the layout of a health publisher:

```
public class SampleHealthCheckPublisher : IHealthCheckPublisher
{
    public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
    {
        if (report.Status == HealthStatus.Healthy)
        {
            // ...
        }
        else
        {
            // ...
        }

        return Task.CompletedTask;
    }
}
```

The [HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions) class provides properties for configuring the behavior of the health check publisher.

The following example registers a health check publisher as a singleton and configures [HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions):

```
builder.Services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.FromSeconds(2);
    options.Predicate = healthCheck => healthCheck.Tags.Contains("sample");
});

builder.Services.AddSingleton<IHealthCheckPublisher, SampleHealthCheckPublisher>();
```

Note

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) includes publishers for several systems, including [Application Insights](/en-us/azure/application-insights/app-insights-overview).

[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) isn't maintained or supported by Microsoft.

## Dependency Injection and Health Checks

It's possible to use dependency injection to consume an instance of a specific `Type` inside a Health Check class. Dependency injection can be useful to inject options or a global configuration to a Health Check. Using dependency injection is ***not*** a common scenario to configure Health Checks. Usually, each Health Check is quite specific to the actual test and is configured using `IHealthChecksBuilder` extension methods.

The following example shows a sample Health Check that retrieves a configuration object via dependency injection:

```
public class SampleHealthCheckWithDI : IHealthCheck
{
    private readonly SampleHealthCheckWithDiConfig _config;

    public SampleHealthCheckWithDI(SampleHealthCheckWithDiConfig config)
        => _config = config;

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        var isHealthy = true;

        // use _config ...

        if (isHealthy)
        {
            return Task.FromResult(
                HealthCheckResult.Healthy("A healthy result."));
        }

        return Task.FromResult(
            new HealthCheckResult(
                context.Registration.FailureStatus, "An unhealthy result."));
    }
}
```

The `SampleHealthCheckWithDiConfig` and the Health check needs to be added to the service container :

```
builder.Services.AddSingleton<SampleHealthCheckWithDiConfig>(new SampleHealthCheckWithDiConfig
{
    BaseUriToCheck = new Uri("https://sample.contoso.com/api/")
});
builder.Services.AddHealthChecks()
    .AddCheck<SampleHealthCheckWithDI>(
        "With Dependency Injection",
        tags: new[] { "inject" });
```

## UseHealthChecks vs. MapHealthChecks

There are two ways to make health checks accessible to callers:

* [UseHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckapplicationbuilderextensions.usehealthchecks) registers middleware for handling health checks requests in the middleware pipeline.
* [MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks) registers a health checks endpoint. The endpoint is matched and executed along with other endpoints in the app.

The advantage of using `MapHealthChecks` over `UseHealthChecks` is the ability to use endpoint aware middleware, such as authorization, and to have greater fine-grained control over the matching policy. The primary advantage of using `UseHealthChecks` over `MapHealthChecks` is controlling exactly where health checks runs in the middleware pipeline.

[UseHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckapplicationbuilderextensions.usehealthchecks):

* Terminates the pipeline when a request matches the health check endpoint. [Short-circuiting](../fundamentals/middleware/?view=aspnetcore-10.0) is often desirable because it avoids unnecessary work, such as logging and other middleware.
* Is primarily used for configuring the health checks middleware in the pipeline.
* Can match any path on a port with a `null` or empty `PathString`. Allows performing a health check on any request made to the specified port.
* [Source code](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HealthChecks/src/Builder/HealthCheckApplicationBuilderExtensions.cs)

[MapHealthChecks](/en-us/dotnet/api/microsoft.aspnetcore.builder.healthcheckendpointroutebuilderextensions.maphealthchecks) allows:

* Mapping specific routes or endpoints for health checks.

* Customization of the URL or path where the health check endpoint is accessible.

* Mapping multiple health check endpoints with different routes or configurations. Multiple endpoint support:

  * Enables separate endpoints for different types of health checks or components.
  * Is used to differentiate between different aspects of the app's health or apply specific configurations to subsets of health checks.

* [Source code](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs)

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/host-and-deploy/health-checks/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

Note

This article was partially created with the help of artificial intelligence. Before publishing, an author reviewed and revised the content as needed. See [Our principles for using AI-generated content in Microsoft Learn](https://aka.ms/ai-content-principles).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/consumer-apis/password-hashing.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Hash passwords in ASP.NET Core

This article shows how to call the [KeyDerivation.Pbkdf2](/en-us/dotnet/api/microsoft.aspnetcore.cryptography.keyderivation.keyderivation.pbkdf2) method, which allows hashing a password with the PBKDF2 algorithm, as described in [RFC 2898, Section 5.2](https://datatracker.ietf.org/doc/html/rfc2898#section-5.2).

The `KeyDerivation.Pbkdf2` API is a low-level cryptographic primitive. The intended use is for integrating apps into an existing protocol or cryptographic system.

Warning

`KeyDerivation.Pbkdf2` shouldn't be used in new apps that support password-based sign in and which need to store hashed passwords in a datastore. New apps should use the [PasswordHasher](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasher-1) class. For more information, see [Exploring the ASP.NET Core Identity PasswordHasher](https://andrewlock.net/exploring-the-asp-net-core-identity-passwordhasher/).

The data protection code base includes a NuGet package [Microsoft.AspNetCore.Cryptography.KeyDerivation](https://www.nuget.org/packages/Microsoft.AspNetCore.Cryptography.KeyDerivation/) that contains cryptographic key derivation functions. This package is a standalone component and has no dependencies on the rest of the data protection system. The package can be used independently. The source exists alongside the data protection code base as a convenience.

## Generate key with 'KeyDerivation.Pbkdf2'

The following code shows how to use the `KeyDerivation.Pbkdf2` method to generate a shared secret key.

Warning

Don't call the `KeyDerivation.Pbkdf2` method to hash a password for storage in a datastore.

```
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using System.Security.Cryptography;

Console.Write("Enter a password: ");
string? password = Console.ReadLine();

// Generate a 128-bit salt using a sequence of
// cryptographically strong random bytes.
byte[] salt = RandomNumberGenerator.GetBytes(128 / 8); // divide by 8 to convert bits to bytes
Console.WriteLine($"Salt: {Convert.ToBase64String(salt)}");

// derive a 256-bit subkey (use HMACSHA256 with 100,000 iterations)
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
    password: password!,
    salt: salt,
    prf: KeyDerivationPrf.HMACSHA256,
    iterationCount: 100000,
    numBytesRequested: 256 / 8));

Console.WriteLine($"Hashed: {hashed}");

/*
 * SAMPLE OUTPUT
 *
 * Enter a password: Xtw9NMgx
 * Salt: CGYzqeN4plZekNC88Umm1Q==
 * Hashed: Gt9Yc4AiIvmsC1QQbe2RZsCIqvoYlst2xbz0Fs8aHnw=
 */
```

```
using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;

public class Program
{
    public static void Main(string[] args)
    {
        Console.Write("Enter a password: ");
        string password = Console.ReadLine();

        // generate a 128-bit salt using a cryptographically strong random sequence of nonzero values
        byte[] salt = new byte[128 / 8];
        using (var rngCsp = new RNGCryptoServiceProvider())
        {
            rngCsp.GetNonZeroBytes(salt);
        }
        Console.WriteLine($"Salt: {Convert.ToBase64String(salt)}");

        // derive a 256-bit subkey (use HMACSHA256 with 100,000 iterations)
        string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
            password: password,
            salt: salt,
            prf: KeyDerivationPrf.HMACSHA256,
            iterationCount: 100000,
            numBytesRequested: 256 / 8));
        Console.WriteLine($"Hashed: {hashed}");
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Enter a password: Xtw9NMgx
 * Salt: CGYzqeN4plZekNC88Umm1Q==
 * Hashed: Gt9Yc4AiIvmsC1QQbe2RZsCIqvoYlst2xbz0Fs8aHnw=
 */
 
```

For a real-world use case of the ASP.NET Core Identity `PasswordHasher` type, see the [source code](https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Extensions.Core/src/PasswordHasher.cs) on GitHub.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Related content

* [Exploring the ASP.NET Core Identity 'PasswordHasher' type](https://andrewlock.net/exploring-the-asp-net-core-identity-passwordhasher/)
* [KeyDerivation.Pbkdf2](/en-us/dotnet/api/microsoft.aspnetcore.cryptography.keyderivation.keyderivation.pbkdf2)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/migrations
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/migrations.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Part 5, apply migrations to the Contoso University sample

In this tutorial, you start using the EF Core migrations feature for managing data model changes. In later tutorials, you'll add more migrations as you change the data model.

In this tutorial, you:

* Learn about migrations
* Create an initial migration
* Examine Up and Down methods
* Learn about the data model snapshot
* Apply the migration

## Prerequisites

* [Sorting, filtering, and paging](sort-filter-page?view=aspnetcore-10.0)

## About migrations

When you develop a new application, your data model changes frequently, and each time the model changes, it gets out of sync with the database. You started these tutorials by configuring the Entity Framework to create the database if it doesn't exist. Then each time you change the data model -- add, remove, or change entity classes or change your DbContext class -- you can delete the database and EF creates a new one that matches the model, and seeds it with test data.

This method of keeping the database in sync with the data model works well until you deploy the application to production. When the application is running in production it's usually storing data that you want to keep, and you don't want to lose everything each time you make a change such as adding a new column. The EF Core Migrations feature solves this problem by enabling EF to update the database schema instead of creating a new database.

To work with migrations, you can use the **Package Manager Console** (PMC) or the CLI. These tutorials show how to use CLI commands. Information about the PMC is at [the end of this tutorial](#pmc).

## Drop the database

Install EF Core tools as a [global tool](/en-us/ef/core/miscellaneous/cli/dotnet) and delete the database:

```
dotnet tool install --global dotnet-ef
dotnet ef database drop
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

The following section explains how to run CLI commands.

## Create an initial migration

Save your changes and build the project. Then open a command window and navigate to the project folder. Here's a quick way to do that:

* In **Solution Explorer**, right-click the project and choose **Open Folder in File Explorer** from the context menu.

* Enter "cmd" in the address bar and press Enter.

Enter the following command in the command window:

```
dotnet ef migrations add InitialCreate
```

In the preceding commands, output similar to the following is displayed:

```
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core initialized 'SchoolContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Done. To undo this action, use 'ef migrations remove'
```

If you see an error message "*cannot access the file ... ContosoUniversity.dll because it is being used by another process.*", find the IIS Express icon in the Windows System Tray, and right-click it, then click **ContosoUniversity > Stop Site**.

## Examine Up and Down methods

When you executed the `migrations add` command, EF generated the code that will create the database from scratch. This code is in the *Migrations* folder, in the file named `<timestamp>_InitialCreate.cs`. The `Up` method of the `InitialCreate` class creates the database tables that correspond to the data model entity sets, and the `Down` method deletes them, as shown in the following example.

```
public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Course",
            columns: table => new
            {
                CourseID = table.Column<int>(nullable: false),
                Credits = table.Column<int>(nullable: false),
                Title = table.Column<string>(nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Course", x => x.CourseID);
            });

        // Additional code not shown
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Enrollment");
        // Additional code not shown
    }
}
```

Migrations calls the `Up` method to implement the data model changes for a migration. When you enter a command to roll back the update, Migrations calls the `Down` method.

This code is for the initial migration that was created when you entered the `migrations add InitialCreate` command. The migration name parameter ("InitialCreate" in the example) is used for the file name and can be whatever you want. It's best to choose a word or phrase that summarizes what is being done in the migration. For example, you might name a later migration "AddDepartmentTable".

If you created the initial migration when the database already exists, the database creation code is generated but it doesn't have to run because the database already matches the data model. When you deploy the app to another environment where the database doesn't exist yet, this code will run to create your database, so it's a good idea to test it first. That's why you dropped the database earlier -- so that migrations can create a new one from scratch.

## The data model snapshot

Migrations creates a *snapshot* of the current database schema in `Migrations/SchoolContextModelSnapshot.cs`. When you add a migration, EF determines what changed by comparing the data model to the snapshot file.

Use the [dotnet ef migrations remove](/en-us/ef/core/miscellaneous/cli/dotnet#dotnet-ef-migrations-remove) command to remove a migration. `dotnet ef migrations remove` deletes the migration and ensures the snapshot is correctly reset. If `dotnet ef migrations remove` fails, use `dotnet ef migrations remove -v` to get more information on the failure.

See [EF Core Migrations in Team Environments](/en-us/ef/core/managing-schemas/migrations/teams) for more information about how the snapshot file is used.

## Apply the migration

In the command window, enter the following command to create the database and tables in it.

```
dotnet ef database update
```

The output from the command is similar to the `migrations add` command, except that you see logs for the SQL commands that set up the database. Most of the logs are omitted in the following sample output. If you prefer not to see this level of detail in log messages, you can change the log level in the `appsettings.Development.json` file. For more information, see [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0).

```
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core initialized 'SchoolContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (274ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      CREATE DATABASE [ContosoUniversity2];
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (60ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
      IF SERVERPROPERTY('EngineEdition') <> 5
      BEGIN
          ALTER DATABASE [ContosoUniversity2] SET READ_COMMITTED_SNAPSHOT ON;
      END;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (15ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [__EFMigrationsHistory] (
          [MigrationId] nvarchar(150) NOT NULL,
          [ProductVersion] nvarchar(32) NOT NULL,
          CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
      );

<logs omitted for brevity>

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
      VALUES (N'20190327172701_InitialCreate', N'5.0-rtm');
Done.
```

Use **SQL Server Object Explorer** to inspect the database as you did in the first tutorial. You'll notice the addition of an \_\_EFMigrationsHistory table that keeps track of which migrations have been applied to the database. View the data in that table and you'll see one row for the first migration. (The last log in the preceding CLI output example shows the INSERT statement that creates this row.)

Run the application to verify that everything still works the same as before.

[]()

## Compare CLI and PMC

The EF tooling for managing migrations is available from .NET CLI commands or from PowerShell cmdlets in the Visual Studio **Package Manager Console** (PMC) window. This tutorial shows how to use the CLI, but you can use the PMC if you prefer.

The EF commands for the PMC commands are in the [Microsoft.EntityFrameworkCore.Tools](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Tools) package. This package is included in the [Microsoft.AspNetCore.App metapackage](../../fundamentals/metapackage-app?view=aspnetcore-10.0), so you don't need to add a package reference if your app has a package reference for `Microsoft.AspNetCore.App`.

**Important:** This isn't the same package as the one you install for the CLI by editing the `.csproj` file. The name of this one ends in `Tools`, unlike the CLI package name which ends in `Tools.DotNet`.

For more information about the CLI commands, see [.NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).

For more information about the PMC commands, see [Package Manager Console (Visual Studio)](/en-us/ef/core/miscellaneous/cli/powershell).

## Get the code

[Download or view the completed application.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples)

## Next step

Advance to the next tutorial to begin looking at more advanced topics about expanding the data model. Along the way you'll create and apply additional migrations.

[Create and apply additional migrations](complex-data-model?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-05-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/component-disposal
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/component-disposal.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Razor component disposal

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains the ASP.NET Core Razor component disposal process with [IDisposable](/en-us/dotnet/api/system.idisposable) and [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable).

If a component implements [IDisposable](/en-us/dotnet/api/system.idisposable) or [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable), the framework calls for resource disposal when the component is removed from the UI. Don't rely on the exact timing of when these methods are executed. For example, [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable) can be triggered before or after an asynchronous [Task](/en-us/dotnet/api/system.threading.tasks.task) awaited in [`OnInitalizedAsync`](lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) or [`OnParametersSetAsync`](lifecycle?view=aspnetcore-10.0#after-parameters-are-set-onparameterssetasync) is called or completes. Also, object disposal code shouldn't assume that objects created during initialization or other lifecycle methods exist.

Components shouldn't need to implement [IDisposable](/en-us/dotnet/api/system.idisposable) and [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable) simultaneously. If both are implemented, the framework only executes the asynchronous overload.

Developer code must ensure that [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable) implementations don't take a long time to complete.

For more information, see the introductory remarks of [ASP.NET Core Blazor synchronization context](synchronization-context?view=aspnetcore-10.0).

## Disposal of JavaScript interop object references

Examples throughout the [JavaScript (JS) interop articles](../javascript-interoperability/?view=aspnetcore-10.0) demonstrate typical object disposal patterns:

* When calling JS from .NET, as described in [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0), dispose any created [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference)/[IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference)/[JSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.implementation.jsobjectreference) either from .NET or from JS to avoid leaking JS memory.

* When calling .NET from JS, as described in [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0), dispose any created [DotNetObjectReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetobjectreference) either from .NET or from JS to avoid leaking .NET memory.

JS interop object references are implemented as a map keyed by an identifier on the side of the JS interop call that creates the reference. When object disposal is initiated from either the .NET or JS side, Blazor removes the entry from the map, and the object can be garbage collected as long as no other strong reference to the object is present.

At a minimum, always dispose objects created on the .NET side to avoid leaking .NET managed memory.

## DOM cleanup tasks during component disposal

For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](../javascript-interoperability/?view=aspnetcore-10.0#dom-cleanup-tasks-during-component-disposal).

For guidance on [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) when a circuit is disconnected, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](../javascript-interoperability/?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit). For general JavaScript interop error handling guidance, see the *JavaScript interop* section in [Handle errors in ASP.NET Core Blazor apps](../fundamentals/handle-errors?view=aspnetcore-10.0#javascript-interop).

## Synchronous `IDisposable`

For synchronous disposal tasks, use [IDisposable.Dispose](/en-us/dotnet/api/system.idisposable.dispose).

The following component:

* Implements [IDisposable](/en-us/dotnet/api/system.idisposable) with the [`@implements`](../../mvc/views/razor?view=aspnetcore-10.0#implements) Razor directive.
* Disposes of `obj`, which is a type that implements [IDisposable](/en-us/dotnet/api/system.idisposable).
* A null check is performed because `obj` is created in a lifecycle method (not shown).

```
@implements IDisposable

...

@code {
    ...

    public void Dispose()
    {
        obj?.Dispose();
    }
}
```

If a single object requires disposal, a lambda can be used to dispose of the object when [Dispose](/en-us/dotnet/api/system.idisposable.dispose) is called. The following example appears in the [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system) article and demonstrates the use of a lambda expression for the disposal of a [Timer](/en-us/dotnet/api/system.timers.timer).

`TimerDisposal1.razor`:

```
@page "/timer-disposal-1"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 1</PageTitle>

<h1>Timer Disposal Example 1</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

`TimerDisposal1.razor`:

```
@page "/timer-disposal-1"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 1</PageTitle>

<h1>Timer Disposal Example 1</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

`CounterWithTimerDisposal1.razor`:

```
@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

`CounterWithTimerDisposal1.razor`:

```
@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

`CounterWithTimerDisposal1.razor`:

```
@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

`CounterWithTimerDisposal1.razor`:

```
@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
```

Note

In the preceding example, the call to [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is wrapped by a call to [ComponentBase.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync) because the callback is invoked outside of Blazor's synchronization context. For more information, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system).

If the object is created in a lifecycle method, such as [`OnInitialized{Async}`](lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync), check for `null` before calling `Dispose`.

`TimerDisposal2.razor`:

```
@page "/timer-disposal-2"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 2</PageTitle>

<h1>Timer Disposal Example 2</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer? timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}
```

`TimerDisposal2.razor`:

```
@page "/timer-disposal-2"
@using System.Timers
@implements IDisposable

<PageTitle>Timer Disposal 2</PageTitle>

<h1>Timer Disposal Example 2</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer? timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}
```

`CounterWithTimerDisposal2.razor`:

```
@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer? timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}
```

`CounterWithTimerDisposal2.razor`:

```
@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer? timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}
```

`CounterWithTimerDisposal2.razor`:

```
@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}
```

`CounterWithTimerDisposal2.razor`:

```
@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>Current count: @currentCount</p>

@code {
    private int currentCount = 0;
    private Timer timer;

    protected override void OnInitialized()
    {
        timer = new Timer(1000);
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer?.Dispose();
}
```

For more information, see:

* [Cleaning up unmanaged resources (.NET documentation)](/en-us/dotnet/standard/garbage-collection/unmanaged)
* [Null-conditional operators ?. and ?\[\]](/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-)

## Asynchronous `IAsyncDisposable`

For asynchronous disposal tasks, use [IAsyncDisposable.DisposeAsync](/en-us/dotnet/api/system.iasyncdisposable.disposeasync).

The following component:

* Implements [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable) with the [`@implements`](../../mvc/views/razor?view=aspnetcore-10.0#implements) Razor directive.
* Disposes of `obj`, which is an unmanaged type that implements [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable).
* A null check is performed because `obj` is created in a lifecycle method (not shown).

```
@implements IAsyncDisposable

...

@code {
    ...

    public async ValueTask DisposeAsync()
    {
        if (obj is not null)
        {
            await obj.DisposeAsync();
        }
    }
}
```

For more information, see:

* [ASP.NET Core Blazor synchronization context](synchronization-context?view=aspnetcore-10.0)
* [Cleaning up unmanaged resources (.NET documentation)](/en-us/dotnet/standard/garbage-collection/unmanaged)
* [Null-conditional operators ?. and ?\[\]](/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-)

## Assignment of `null` to disposed objects

Usually, there's no need to assign `null` to disposed objects after calling [Dispose](/en-us/dotnet/api/system.idisposable.dispose)/[DisposeAsync](/en-us/dotnet/api/system.iasyncdisposable.disposeasync). Rare cases for assigning `null` include the following:

* If the object's type is poorly implemented and doesn't tolerate repeat calls to [Dispose](/en-us/dotnet/api/system.idisposable.dispose)/[DisposeAsync](/en-us/dotnet/api/system.iasyncdisposable.disposeasync), assign `null` after disposal to gracefully skip further calls to [Dispose](/en-us/dotnet/api/system.idisposable.dispose)/[DisposeAsync](/en-us/dotnet/api/system.iasyncdisposable.disposeasync).
* If a long-lived process continues to hold a reference to a disposed object, assigning `null` allows the [garbage collector](/en-us/dotnet/standard/garbage-collection/fundamentals) to free the object in spite of the long-lived process holding a reference to it.

These are unusual scenarios. For objects that are implemented correctly and behave normally, there's no point in assigning `null` to disposed objects. In the rare cases where an object must be assigned `null`, we recommend documenting the reason and seeking a solution that prevents the need to assign `null`.

## `StateHasChanged`

Note

Calling [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) in `Dispose` and `DisposeAsync` isn't supported. [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) might be invoked as part of tearing down the renderer, so requesting UI updates at that point isn't supported.

## Event handlers

Always unsubscribe event handlers from .NET events. The following [Blazor form](../forms/?view=aspnetcore-10.0) examples show how to unsubscribe an event handler in the `Dispose` method.

Private field and lambda approach:

```
@implements IDisposable

<EditForm ... EditContext="editContext" ...>
    ...
    <button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
    ...

    private EventHandler<FieldChangedEventArgs>? fieldChanged;

    protected override void OnInitialized()
    {
        editContext = new(model);

        fieldChanged = (_, __) =>
        {
            ...
        };

        editContext.OnFieldChanged += fieldChanged;
    }

    public void Dispose()
    {
        editContext.OnFieldChanged -= fieldChanged;
    }
}
```

Private method approach:

```
@implements IDisposable

<EditForm ... EditContext="editContext" ...>
    ...
    <button type="submit" disabled="@formInvalid">Submit</button>
</EditForm>

@code {
    ...

    protected override void OnInitialized()
    {
        editContext = new(model);
        editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        ...
    }

    public void Dispose()
    {
        editContext.OnFieldChanged -= HandleFieldChanged;
    }
}
```

For more information on the [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component and forms, see [ASP.NET Core Blazor forms overview](../forms/?view=aspnetcore-10.0) and the other forms articles in the *Forms* node.

## Anonymous functions, methods, and expressions

When [anonymous functions](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/anonymous-functions), methods, or expressions, are used, it isn't necessary to implement [IDisposable](/en-us/dotnet/api/system.idisposable) and unsubscribe delegates. However, failing to unsubscribe a delegate is a problem **when the object exposing the event outlives the lifetime of the component registering the delegate**. When this occurs, a memory leak results because the registered delegate keeps the original object alive. Therefore, only use the following approaches when you know that the event delegate disposes quickly. When in doubt about the lifetime of objects that require disposal, subscribe a delegate method and properly dispose the delegate as the earlier examples show.

Anonymous lambda method approach (explicit disposal not required):

```
private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
{
    formInvalid = !editContext.Validate();
    StateHasChanged();
}

protected override void OnInitialized()
{
    editContext = new(starship);
    editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e);
}
```

Anonymous lambda expression approach (explicit disposal not required):

```
private ValidationMessageStore? messageStore;

[CascadingParameter]
private EditContext? CurrentEditContext { get; set; }

protected override void OnInitialized()
{
    ...

    messageStore = new(CurrentEditContext);

    CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear();
    CurrentEditContext.OnFieldChanged += (s, e) => 
        messageStore.Clear(e.FieldIdentifier);
}
```

The full example of the preceding code with anonymous lambda expressions appears in the [ASP.NET Core Blazor advanced form validation](../forms/validation-advanced?view=aspnetcore-10.0#validator-components) article.

For more information, see [Cleaning up unmanaged resources](/en-us/dotnet/standard/garbage-collection/unmanaged) and the topics that follow it on implementing the `Dispose` and `DisposeAsync` methods.

## Disposal during JS interop

Trap [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) in potential cases where loss of Blazor's SignalR circuit prevents JS interop calls and results an unhandled exception.

For more information, see the following resources:

* [JavaScript isolation in JavaScript modules](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules)
* [JavaScript interop calls without a circuit](../javascript-interoperability/?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0017
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0017.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0017: Invalid route pattern

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0017      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A route pattern is invalid.

## Rule description

This diagnostic is emitted when a route pattern is invalid. In the example below, the route pattern contains an invalid token.

```
var app = WebApplication.Create();

app.MapGet("/{id", (int id) => ...);
```

## How to fix violations

To fix a violation of this rule, correct the error identified in the analyzer message.

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/additional-claims
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/social/additional-claims.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Persist additional claims and tokens from external providers in ASP.NET Core

An ASP.NET Core app can establish additional claims and tokens from external authentication providers, such as Facebook, Google, Microsoft, and Twitter. Each provider reveals different information about users on its platform, but the pattern for receiving and transforming user data into additional claims is the same.

## Prerequisites

Decide which external authentication providers to support in the app. For each provider, register the app and obtain a client ID and client secret. For more information, see [Using external login providers with Identity in ASP.NET Core](./?view=aspnetcore-10.0). The sample app uses the [Google authentication provider](google-logins?view=aspnetcore-10.0).

## Set the client ID and client secret

The OAuth authentication provider establishes a trust relationship with an app using a client ID and client secret. Client ID and client secret values are created for the app by the external authentication provider when the app is registered with the provider. Each external provider that the app uses must be configured independently with the provider's client ID and client secret. For more information, see the external authentication provider topics that apply:

* [Facebook authentication](facebook-logins?view=aspnetcore-10.0)
* [Google authentication](google-logins?view=aspnetcore-10.0)
* [Microsoft authentication](microsoft-logins?view=aspnetcore-10.0)
* [Twitter authentication](twitter-logins?view=aspnetcore-10.0)
* [Other authentication providers](other-logins?view=aspnetcore-10.0)
* [OpenIdConnect](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2)

Optional claims sent in the ID or access token from the authentication provider are usually configured in the provider's online portal. For example, Microsoft Entra ID permits assigning optional claims to the app's ID token in the app registration's **Token configuration** blade. For more information, see [How to: Provide optional claims to your app (Azure documentation)](/en-us/azure/active-directory/develop/active-directory-optional-claims). For other providers, consult their external documentation sets.

The sample app configures the Google authentication provider with a client ID and client secret provided by Google:

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebGoogOauth.Data;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;

builder.Services.AddAuthentication().AddGoogleOpenIdConnect(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnTicketReceived = ctx =>
    {
        List<AuthenticationToken>? tokens = ctx.Properties?.GetTokens().ToList();

        tokens?.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        if (tokens is not null)
        {
            ctx.Properties?.StoreTokens(tokens);
        }

        return Task.CompletedTask;
    };
});

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => 
                                  options.SignIn.RequireConfirmedAccount = true)
                                 .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

// Remaining code removed for brevity.
```

## Establish the authentication scope

Specify the list of permissions to retrieve from the provider by specifying the [Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.oauthoptions.scope). Authentication scopes for common external providers appear in the following table.

| Provider  | Scope                                                            |
| --------- | ---------------------------------------------------------------- |
| Facebook  | `https://www.facebook.com/dialog/oauth`                          |
| Google    | `profile`, `email`, `openid`                                     |
| Microsoft | `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` |
| Twitter   | `https://api.twitter.com/oauth/authenticate`                     |

In the sample app, Google's `profile`, `email`, and `openid` scopes are automatically added by the framework when `Microsoft.Extensions.DependencyInjection.GoogleOpenIdConnectExtensions.AddGoogleOpenIdConnect` is called on the [AuthenticationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder). If the app requires additional scopes, add them to the options. In the following example, the Google `https://www.googleapis.com/auth/user.birthday.read` scope is added to retrieve a user's birthday:

```
options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");
```

## Map user data keys and create claims

In the provider's options, specify a [MapJsonKey](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.mapjsonkey) or [MapJsonSubKey](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.mapjsonsubkey) for each key or subkey in the external provider's JSON user data for the app identity to read on sign in. For more information on claim types, see [ClaimTypes](/en-us/dotnet/api/system.security.claims.claimtypes).

The sample app creates locale (`urn:google:locale`) and picture (`urn:google:picture`) claims from the `locale` and `picture` keys in Google user data:

```
builder.Services.AddAuthentication().AddGoogleOpenIdConnect(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnTicketReceived = ctx =>
    {
        List<AuthenticationToken>? tokens = ctx.Properties?.GetTokens().ToList();

        tokens?.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        if (tokens is not null)
        {
            ctx.Properties?.StoreTokens(tokens);
        }

        return Task.CompletedTask;
    };
});
```

In `Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync`, an [IdentityUser](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser) (`ApplicationUser`) is signed into the app with [SignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.signinasync). During the sign in process, the [UserManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1) can store an `ApplicationUser` claims for user data available from the [Principal](/en-us/dotnet/api/microsoft.aspnetcore.identity.externallogininfo.principal).

In the sample app, `OnPostConfirmationAsync` (`Account/ExternalLogin.cshtml.cs`) establishes the locale (`urn:google:locale`) and picture (`urn:google:picture`) claims for the signed in `ApplicationUser`, including a claim for [GivenName](/en-us/dotnet/api/system.security.claims.claimtypes.givenname#system-security-claims-claimtypes-givenname):

```
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information during confirmation.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

        var result = await _userManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);
            if (result.Succeeded)
            {
                _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);

                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                // using Microsoft.AspNetCore.Authentication;
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = false;

                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code },
                    protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                // If account confirmation is required, we need to show the link if we don't have a real email sender
                if (_userManager.Options.SignIn.RequireConfirmedAccount)
                {
                    return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                }

                await _signInManager.SignInAsync(user, props, info.LoginProvider);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    ProviderDisplayName = info.ProviderDisplayName;
    ReturnUrl = returnUrl;
    return Page();
}
```

By default, a user's claims are stored in the authentication cookie. If the authentication cookie is too large, it can cause the app to fail because:

* The browser detects that the cookie header is too long.
* The overall size of the request is too large.

If a large amount of user data is required for processing user requests:

* Limit the number and size of user claims for request processing to only what the app requires.
* Use a custom [ITicketStore](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.iticketstore) for the cookie authentication middleware's [SessionStore](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions.sessionstore#microsoft-aspnetcore-authentication-cookies-cookieauthenticationoptions-sessionstore) to store identity across requests. Preserve large quantities of identity information on the server while only sending a small session identifier key to the client.

## Save the access token

[SaveTokens](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens) defines whether access and refresh tokens should be stored in the [AuthenticationProperties](/en-us/dotnet/api/microsoft.aspnetcore.http.authentication.authenticationproperties) after a successful authorization. `SaveTokens` is set to `false` by default to reduce the size of the final authentication cookie.

The sample app sets the value of `SaveTokens` to `true` in [OpenIdConnectOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions):

```
builder.Services.AddAuthentication().AddGoogleOpenIdConnect(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnTicketReceived = ctx =>
    {
        List<AuthenticationToken>? tokens = ctx.Properties?.GetTokens().ToList();

        tokens?.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        if (tokens is not null)
        {
            ctx.Properties?.StoreTokens(tokens);
        }

        return Task.CompletedTask;
    };
});
```

When `OnPostConfirmationAsync` executes, store the access token ([ExternalLoginInfo.AuthenticationTokens](/en-us/dotnet/api/microsoft.aspnetcore.identity.externallogininfo.authenticationtokens)) from the external provider in the `ApplicationUser`'s `AuthenticationProperties`.

The sample app saves the access token in `OnPostConfirmationAsync` (new user registration) and `OnGetCallbackAsync` (previously registered user) in `Account/ExternalLogin.cshtml.cs`:

```
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information during confirmation.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

        var result = await _userManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);
            if (result.Succeeded)
            {
                _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);

                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                // using Microsoft.AspNetCore.Authentication;
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = false;

                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code },
                    protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                // If account confirmation is required, we need to show the link if we don't have a real email sender
                if (_userManager.Options.SignIn.RequireConfirmedAccount)
                {
                    return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                }

                await _signInManager.SignInAsync(user, props, info.LoginProvider);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    ProviderDisplayName = info.ProviderDisplayName;
    ReturnUrl = returnUrl;
    return Page();
}
```

Note

For information on passing tokens to the Razor components of a server-side Blazor app, see [ASP.NET Core Blazor additional server-side security scenarios](../../../blazor/security/additional-scenarios?view=aspnetcore-10.0#pass-tokens-to-a-server-side-blazor-app).

## How to add additional custom tokens

To demonstrate how to add a custom token, which is stored as part of `SaveTokens`, the sample app adds an [AuthenticationToken](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationtoken) with the current [DateTime](/en-us/dotnet/api/system.datetime) for an [AuthenticationToken.Name](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationtoken.name) of `TicketCreated`:

```
builder.Services.AddAuthentication().AddGoogleOpenIdConnect(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnTicketReceived = ctx =>
    {
        List<AuthenticationToken>? tokens = ctx.Properties?.GetTokens().ToList();

        tokens?.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        if (tokens is not null)
        {
            ctx.Properties?.StoreTokens(tokens);
        }

        return Task.CompletedTask;
    };
});
```

## Create and add claims

The framework provides common actions and extension methods for creating and adding claims to the collection. For more information, see the [ClaimActionCollectionMapExtensions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions) and [ClaimActionCollectionUniqueExtensions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionuniqueextensions).

Users can define custom actions by deriving from [ClaimAction](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction) and implementing the abstract [Run](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction.run) method.

For more information, see [Microsoft.AspNetCore.Authentication.OAuth.Claims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims).

## Add and update user claims

Claims are copied from external providers to the user database on first registration, not on sign in. If additional claims are enabled in an app after a user registers to use the app, call [SignInManager.RefreshSignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) on a user to force the generation of a new authentication cookie.

In the `Development` environment working with test user accounts, delete and recreate the user account. For production systems, new claims added to the app can be backfilled into user accounts. After [scaffolding the `ExternalLogin` page](../scaffold-identity?view=aspnetcore-10.0) into the app at `Areas/Pages/Identity/Account/Manage`, add the following code to the `ExternalLoginModel` in the `ExternalLogin.cshtml.cs` file.

Add a dictionary of added claims. Use the dictionary keys to hold the claim types, and use the values to hold a default value. Add the following line to the top of the class. The following example assumes that one claim is added for the user's Google picture with a generic headshot image as the default value:

```
private readonly IReadOnlyDictionary<string, string> _claimsToSync =
     new Dictionary<string, string>()
     {
             { "urn:google:picture", "https://localhost:5001/headshot.png" },
     };
```

Replace the default code of the `OnGetCallbackAsync` method with the following code. The code loops through the claims dictionary. Claims are added (backfilled) or updated for each user. When claims are added or updated, the user sign-in is refreshed using the [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1), preserving the existing authentication properties (`AuthenticationProperties`).

```
private readonly IReadOnlyDictionary<string, string> _claimsToSync =
     new Dictionary<string, string>()
     {
             { "urn:google:picture", "https://localhost:5001/headshot.png" },
     };

public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    if (remoteError != null)
    {
        ErrorMessage = $"Error from external provider: {remoteError}";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    // Sign in the user with this external login provider if the user already has a login.
    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
    if (result.Succeeded)
    {
        _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
        if (_claimsToSync.Count > 0)
        {
            var user = await _userManager.FindByLoginAsync(info.LoginProvider,
                info.ProviderKey);
            var userClaims = await _userManager.GetClaimsAsync(user);
            bool refreshSignIn = false;

            foreach (var addedClaim in _claimsToSync)
            {
                var userClaim = userClaims
                    .FirstOrDefault(c => c.Type == addedClaim.Key);

                if (info.Principal.HasClaim(c => c.Type == addedClaim.Key))
                {
                    var externalClaim = info.Principal.FindFirst(addedClaim.Key);

                    if (userClaim == null)
                    {
                        await _userManager.AddClaimAsync(user,
                            new Claim(addedClaim.Key, externalClaim.Value));
                        refreshSignIn = true;
                    }
                    else if (userClaim.Value != externalClaim.Value)
                    {
                        await _userManager
                            .ReplaceClaimAsync(user, userClaim, externalClaim);
                        refreshSignIn = true;
                    }
                }
                else if (userClaim == null)
                {
                    // Fill with a default value
                    await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key,
                        addedClaim.Value));
                    refreshSignIn = true;
                }
            }

            if (refreshSignIn)
            {
                await _signInManager.RefreshSignInAsync(user);
            }
        }

        return LocalRedirect(returnUrl);
    }
    if (result.IsLockedOut)
    {
        return RedirectToPage("./Lockout");
    }
    else
    {
        // If the user does not have an account, then ask the user to create an account.
        ReturnUrl = returnUrl;
        ProviderDisplayName = info.ProviderDisplayName;
        if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input = new InputModel
            {
                Email = info.Principal.FindFirstValue(ClaimTypes.Email)
            };
        }
        return Page();
    }
}
```

A similar approach is taken when claims change while a user is signed in but a backfill step isn't required. To update a user's claims, call the following on the user:

* [UserManager.ReplaceClaimAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1) on the user for claims stored in the identity database.
* [SignInManager.RefreshSignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) on the user to force the generation of a new authentication cookie.

## Remove claim actions and claims

[ClaimActionCollection.Remove(String)](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimactioncollection.remove) removes all claim actions for the given [ClaimType](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction.claimtype#microsoft-aspnetcore-authentication-oauth-claims-claimaction-claimtype) from the collection. [ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String)](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.deleteclaim) deletes a claim of the given [ClaimType](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction.claimtype#microsoft-aspnetcore-authentication-oauth-claims-claimaction-claimtype) from the identity. [DeleteClaim](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.deleteclaim) is primarily used with [OpenID Connect (OIDC)](/en-us/azure/active-directory/develop/v2-protocols-oidc) to remove protocol-generated claims.

## Sample app output

Run the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/social/additional-claims/samples) and select the **MyClaims** link:

```
User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT
```

## Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (`https`), host, and client IP address. Apps don't automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (`https`) results in the app generating incorrect insecure redirect URLs.

Use forwarded headers middleware to make the original request information available to the app for request processing.

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/social/additional-claims/samples) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

An ASP.NET Core app can establish additional claims and tokens from external authentication providers, such as Facebook, Google, Microsoft, and Twitter. Each provider reveals different information about users on its platform, but the pattern for receiving and transforming user data into additional claims is the same.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/social/additional-claims/samples) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

Decide which external authentication providers to support in the app. For each provider, register the app and obtain a client ID and client secret. For more information, see [Using external login providers with Identity in ASP.NET Core](./?view=aspnetcore-10.0). The sample app uses the [Google authentication provider](google-logins?view=aspnetcore-10.0).

## Set the client ID and client secret

The OAuth authentication provider establishes a trust relationship with an app using a client ID and client secret. Client ID and client secret values are created for the app by the external authentication provider when the app is registered with the provider. Each external provider that the app uses must be configured independently with the provider's client ID and client secret. For more information, see the external authentication provider topics that apply to your scenario:

* [Facebook authentication](facebook-logins?view=aspnetcore-10.0)
* [Google authentication](google-logins?view=aspnetcore-10.0)
* [Microsoft authentication](microsoft-logins?view=aspnetcore-10.0)
* [Twitter authentication](twitter-logins?view=aspnetcore-10.0)
* [Other authentication providers](other-logins?view=aspnetcore-10.0)
* [OpenIdConnect](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2)

Optional claims sent in the ID or access token from the authentication provider are usually configured in the provider's online portal. For example, Microsoft Entra ID permits you to assign optional claims to the app's ID token in the app registration's **Token configuration** blade. For more information, see [How to: Provide optional claims to your app (Azure documentation)](/en-us/azure/active-directory/develop/active-directory-optional-claims). For other providers, consult their external documentation sets.

The sample app configures the Google authentication provider with a client ID and client secret provided by Google:

```
services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});
```

## Establish the authentication scope

Specify the list of permissions to retrieve from the provider by specifying the [Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.oauthoptions.scope). Authentication scopes for common external providers appear in the following table.

| Provider  | Scope                                                            |
| --------- | ---------------------------------------------------------------- |
| Facebook  | `https://www.facebook.com/dialog/oauth`                          |
| Google    | `profile`, `email`, `openid`                                     |
| Microsoft | `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` |
| Twitter   | `https://api.twitter.com/oauth/authenticate`                     |

In the sample app, Google's `profile`, `email`, and `openid` scopes are automatically added by the framework when [AddGoogle](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.googleextensions.addgoogle) is called on the [AuthenticationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder). If the app requires additional scopes, add them to the options. In the following example, the Google `https://www.googleapis.com/auth/user.birthday.read` scope is added to retrieve a user's birthday:

```
options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");
```

## Map user data keys and create claims

In the provider's options, specify a [MapJsonKey](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.mapjsonkey) or [MapJsonSubKey](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.mapjsonsubkey) for each key/subkey in the external provider's JSON user data for the app identity to read on sign in. For more information on claim types, see [ClaimTypes](/en-us/dotnet/api/system.security.claims.claimtypes).

The sample app creates locale (`urn:google:locale`) and picture (`urn:google:picture`) claims from the `locale` and `picture` keys in Google user data:

```
services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});
```

In `Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync`, an [IdentityUser](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser) (`ApplicationUser`) is signed into the app with [SignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.signinasync). During the sign in process, the [UserManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1) can store an `ApplicationUser` claims for user data available from the [Principal](/en-us/dotnet/api/microsoft.aspnetcore.identity.externallogininfo.principal).

In the sample app, `OnPostConfirmationAsync` (`Account/ExternalLogin.cshtml.cs`) establishes the locale (`urn:google:locale`) and picture (`urn:google:picture`) claims for the signed in `ApplicationUser`, including a claim for [GivenName](/en-us/dotnet/api/system.security.claims.claimtypes.givenname#system-security-claims-claimtypes-givenname):

```
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}
```

By default, a user's claims are stored in the authentication cookie. If the authentication cookie is too large, it can cause the app to fail because:

* The browser detects that the cookie header is too long.
* The overall size of the request is too large.

If a large amount of user data is required for processing user requests:

* Limit the number and size of user claims for request processing to only what the app requires.
* Use a custom [ITicketStore](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.iticketstore) for the cookie authentication middleware's [SessionStore](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions.sessionstore#microsoft-aspnetcore-authentication-cookies-cookieauthenticationoptions-sessionstore) to store identity across requests. Preserve large quantities of identity information on the server while only sending a small session identifier key to the client.

## Save the access token

[SaveTokens](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens) defines whether access and refresh tokens should be stored in the [AuthenticationProperties](/en-us/dotnet/api/microsoft.aspnetcore.http.authentication.authenticationproperties) after a successful authorization. `SaveTokens` is set to `false` by default to reduce the size of the final authentication cookie.

The sample app sets the value of `SaveTokens` to `true` in [GoogleOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.google.googleoptions):

```
services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});
```

When `OnPostConfirmationAsync` executes, store the access token ([ExternalLoginInfo.AuthenticationTokens](/en-us/dotnet/api/microsoft.aspnetcore.identity.externallogininfo.authenticationtokens)) from the external provider in the `ApplicationUser`'s `AuthenticationProperties`.

The sample app saves the access token in `OnPostConfirmationAsync` (new user registration) and `OnGetCallbackAsync` (previously registered user) in `Account/ExternalLogin.cshtml.cs`:

```
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}
```

Note

For information on passing tokens to the Razor components of a server-side Blazor app, see [ASP.NET Core Blazor additional server-side security scenarios](../../../blazor/security/additional-scenarios?view=aspnetcore-10.0#pass-tokens-to-a-server-side-blazor-app).

## How to add additional custom tokens

To demonstrate how to add a custom token, which is stored as part of `SaveTokens`, the sample app adds an [AuthenticationToken](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationtoken) with the current [DateTime](/en-us/dotnet/api/system.datetime) for an [AuthenticationToken.Name](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationtoken.name) of `TicketCreated`:

```
services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});
```

## Creating and adding claims

The framework provides common actions and extension methods for creating and adding claims to the collection. For more information, see the [ClaimActionCollectionMapExtensions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions) and [ClaimActionCollectionUniqueExtensions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionuniqueextensions).

Users can define custom actions by deriving from [ClaimAction](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction) and implementing the abstract [Run](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction.run) method.

For more information, see [Microsoft.AspNetCore.Authentication.OAuth.Claims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims).

## Add and update user claims

Claims are copied from external providers to the user database on first registration, not on sign in. If additional claims are enabled in an app after a user registers to use the app, call [SignInManager.RefreshSignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) on a user to force the generation of a new authentication cookie.

In the `Development` environment working with test user accounts, you can simply delete and recreate the user account. For production systems, new claims added to the app can be backfilled into user accounts. After [scaffolding the `ExternalLogin` page](../scaffold-identity?view=aspnetcore-10.0) into the app at `Areas/Pages/Identity/Account/Manage`, add the following code to the `ExternalLoginModel` in the `ExternalLogin.cshtml.cs` file.

Add a dictionary of added claims. Use the dictionary keys to hold the claim types, and use the values to hold a default value. Add the following line to the top of the class. The following example assumes that one claim is added for the user's Google picture with a generic headshot image as the default value:

```
private readonly IReadOnlyDictionary<string, string> _claimsToSync = 
    new Dictionary<string, string>()
    {
        { "urn:google:picture", "https://localhost:5001/headshot.png" },
    };
```

Replace the default code of the `OnGetCallbackAsync` method with the following code. The code loops through the claims dictionary. Claims are added (backfilled) or updated for each user. When claims are added or updated, the user sign-in is refreshed using the [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1), preserving the existing authentication properties (`AuthenticationProperties`).

```
public async Task<IActionResult> OnGetCallbackAsync(
    string returnUrl = null, string remoteError = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    if (remoteError != null)
    {
        ErrorMessage = $"Error from external provider: {remoteError}";

        return RedirectToPage("./Login", new {ReturnUrl = returnUrl });
    }

    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = "Error loading external login information.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    // Sign in the user with this external login provider if the user already has a 
    // login.
    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, 
        info.ProviderKey, isPersistent: false, bypassTwoFactor : true);

    if (result.Succeeded)
    {
        _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", 
            info.Principal.Identity.Name, info.LoginProvider);

        if (_claimsToSync.Count > 0)
        {
            var user = await _userManager.FindByLoginAsync(info.LoginProvider, 
                info.ProviderKey);
            var userClaims = await _userManager.GetClaimsAsync(user);
            bool refreshSignIn = false;

            foreach (var addedClaim in _claimsToSync)
            {
                var userClaim = userClaims
                    .FirstOrDefault(c => c.Type == addedClaim.Key);

                if (info.Principal.HasClaim(c => c.Type == addedClaim.Key))
                {
                    var externalClaim = info.Principal.FindFirst(addedClaim.Key);

                    if (userClaim == null)
                    {
                        await _userManager.AddClaimAsync(user, 
                            new Claim(addedClaim.Key, externalClaim.Value));
                        refreshSignIn = true;
                    }
                    else if (userClaim.Value != externalClaim.Value)
                    {
                        await _userManager
                            .ReplaceClaimAsync(user, userClaim, externalClaim);
                        refreshSignIn = true;
                    }
                }
                else if (userClaim == null)
                {
                    // Fill with a default value
                    await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key, 
                        addedClaim.Value));
                    refreshSignIn = true;
                }
            }

            if (refreshSignIn)
            {
                await _signInManager.RefreshSignInAsync(user);
            }
        }

        return LocalRedirect(returnUrl);
    }

    if (result.IsLockedOut)
    {
        return RedirectToPage("./Lockout");
    }
    else
    {
        // If the user does not have an account, then ask the user to create an 
        // account.
        ReturnUrl = returnUrl;
        ProviderDisplayName = info.ProviderDisplayName;

        if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input = new InputModel
            {
                Email = info.Principal.FindFirstValue(ClaimTypes.Email)
            };
        }

        return Page();
    }
}
```

A similar approach is taken when claims change while a user is signed in but a backfill step isn't required. To update a user's claims, call the following on the user:

* [UserManager.ReplaceClaimAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1) on the user for claims stored in the identity database.
* [SignInManager.RefreshSignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) on the user to force the generation of a new authentication cookie.

## Removal of claim actions and claims

[ClaimActionCollection.Remove(String)](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimactioncollection.remove) removes all claim actions for the given [ClaimType](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction.claimtype#microsoft-aspnetcore-authentication-oauth-claims-claimaction-claimtype) from the collection. [ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String)](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.deleteclaim) deletes a claim of the given [ClaimType](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction.claimtype#microsoft-aspnetcore-authentication-oauth-claims-claimaction-claimtype) from the identity. [DeleteClaim](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.deleteclaim) is primarily used with [OpenID Connect (OIDC)](/en-us/azure/active-directory/develop/v2-protocols-oidc) to remove protocol-generated claims.

## Sample app output

```
User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT
```

## Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (`https`), host, and client IP address. Apps don't automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (`https`) results in the app generating incorrect insecure redirect URLs.

Use forwarded headers middleware to make the original request information available to the app for request processing.

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

An ASP.NET Core app can establish additional claims and tokens from external authentication providers, such as Facebook, Google, Microsoft, and Twitter. Each provider reveals different information about users on its platform, but the pattern for receiving and transforming user data into additional claims is the same.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/social/additional-claims/samples) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Prerequisites

Decide which external authentication providers to support in the app. For each provider, register the app and obtain a client ID and client secret. For more information, see [Using external login providers with Identity in ASP.NET Core](./?view=aspnetcore-10.0). The sample app uses the [Google authentication provider](google-logins?view=aspnetcore-10.0).

## Set the client ID and client secret

The OAuth authentication provider establishes a trust relationship with an app using a client ID and client secret. Client ID and client secret values are created for the app by the external authentication provider when the app is registered with the provider. Each external provider that the app uses must be configured independently with the provider's client ID and client secret. For more information, see the external authentication provider topics that apply to your scenario:

* [Facebook authentication](facebook-logins?view=aspnetcore-10.0)
* [Google authentication](google-logins?view=aspnetcore-10.0)
* [Microsoft authentication](microsoft-logins?view=aspnetcore-10.0)
* [Twitter authentication](twitter-logins?view=aspnetcore-10.0)
* [Other authentication providers](other-logins?view=aspnetcore-10.0)
* [OpenIdConnect](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2)

The sample app configures the Google authentication provider with a client ID and client secret provided by Google:

```
services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});
```

## Establish the authentication scope

Specify the list of permissions to retrieve from the provider by specifying the [Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.oauthoptions.scope). Authentication scopes for common external providers appear in the following table.

| Provider  | Scope                                                            |
| --------- | ---------------------------------------------------------------- |
| Facebook  | `https://www.facebook.com/dialog/oauth`                          |
| Google    | `https://www.googleapis.com/auth/userinfo.profile`               |
| Microsoft | `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` |
| Twitter   | `https://api.twitter.com/oauth/authenticate`                     |

In the sample app, Google's `userinfo.profile` scope is automatically added by the framework when [AddGoogle](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.googleextensions.addgoogle) is called on the [AuthenticationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder). If the app requires additional scopes, add them to the options. In the following example, the Google `https://www.googleapis.com/auth/user.birthday.read` scope is added in order to retrieve a user's birthday:

```
options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");
```

## Map user data keys and create claims

In the provider's options, specify a [MapJsonKey](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.mapjsonkey) or [MapJsonSubKey](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.mapjsonsubkey) for each key/subkey in the external provider's JSON user data for the app identity to read on sign in. For more information on claim types, see [ClaimTypes](/en-us/dotnet/api/system.security.claims.claimtypes).

The sample app creates locale (`urn:google:locale`) and picture (`urn:google:picture`) claims from the `locale` and `picture` keys in Google user data:

```
services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});
```

In `Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync`, an [IdentityUser](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser) (`ApplicationUser`) is signed into the app with [SignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.signinasync). During the sign in process, the [UserManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1) can store an `ApplicationUser` claims for user data available from the [Principal](/en-us/dotnet/api/microsoft.aspnetcore.identity.externallogininfo.principal).

In the sample app, `OnPostConfirmationAsync` (`Account/ExternalLogin.cshtml.cs`) establishes the locale (`urn:google:locale`) and picture (`urn:google:picture`) claims for the signed in `ApplicationUser`, including a claim for [GivenName](/en-us/dotnet/api/system.security.claims.claimtypes.givenname#system-security-claims-claimtypes-givenname):

```
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}
```

By default, a user's claims are stored in the authentication cookie. If the authentication cookie is too large, it can cause the app to fail because:

* The browser detects that the cookie header is too long.
* The overall size of the request is too large.

If a large amount of user data is required for processing user requests:

* Limit the number and size of user claims for request processing to only what the app requires.
* Use a custom [ITicketStore](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.iticketstore) for the cookie authentication middleware's [SessionStore](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions.sessionstore#microsoft-aspnetcore-authentication-cookies-cookieauthenticationoptions-sessionstore) to store identity across requests. Preserve large quantities of identity information on the server while only sending a small session identifier key to the client.

## Save the access token

[SaveTokens](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens) defines whether access and refresh tokens should be stored in the [AuthenticationProperties](/en-us/dotnet/api/microsoft.aspnetcore.http.authentication.authenticationproperties) after a successful authorization. `SaveTokens` is set to `false` by default to reduce the size of the final authentication cookie.

The sample app sets the value of `SaveTokens` to `true` in [GoogleOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.google.googleoptions):

```
services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});
```

When `OnPostConfirmationAsync` executes, store the access token ([ExternalLoginInfo.AuthenticationTokens](/en-us/dotnet/api/microsoft.aspnetcore.identity.externallogininfo.authenticationtokens)) from the external provider in the `ApplicationUser`'s `AuthenticationProperties`.

The sample app saves the access token in `OnPostConfirmationAsync` (new user registration) and `OnGetCallbackAsync` (previously registered user) in `Account/ExternalLogin.cshtml.cs`:

```
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}
```

## How to add additional custom tokens

To demonstrate how to add a custom token, which is stored as part of `SaveTokens`, the sample app adds an [AuthenticationToken](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationtoken) with the current [DateTime](/en-us/dotnet/api/system.datetime) for an [AuthenticationToken.Name](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationtoken.name) of `TicketCreated`:

```
services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});
```

## Creating and adding claims

The framework provides common actions and extension methods for creating and adding claims to the collection. For more information, see the [ClaimActionCollectionMapExtensions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions) and [ClaimActionCollectionUniqueExtensions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionuniqueextensions).

Users can define custom actions by deriving from [ClaimAction](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction) and implementing the abstract [Run](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction.run) method.

For more information, see [Microsoft.AspNetCore.Authentication.OAuth.Claims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims).

## Removal of claim actions and claims

[ClaimActionCollection.Remove(String)](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimactioncollection.remove) removes all claim actions for the given [ClaimType](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction.claimtype#microsoft-aspnetcore-authentication-oauth-claims-claimaction-claimtype) from the collection. [ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String)](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.deleteclaim) deletes a claim of the given [ClaimType](/en-us/dotnet/api/microsoft.aspnetcore.authentication.oauth.claims.claimaction.claimtype#microsoft-aspnetcore-authentication-oauth-claims-claimaction-claimtype) from the identity. [DeleteClaim](/en-us/dotnet/api/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions.deleteclaim) is primarily used with [OpenID Connect (OIDC)](/en-us/azure/active-directory/develop/v2-protocols-oidc) to remove protocol-generated claims.

## Sample app output

```
User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT
```

## Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (`https`), host, and client IP address. Apps don't automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (`https`) results in the app generating incorrect insecure redirect URLs.

Use forwarded headers middleware to make the original request information available to the app for request processing.

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

## Related content

* [dotnet/AspNetCore engineering SocialSample app](https://github.com/dotnet/AspNetCore/tree/main/src/Security/Authentication/samples/SocialSample): The linked sample app is on the [dotnet/AspNetCore GitHub repo's](https://github.com/dotnet/AspNetCore) `main` engineering branch. The `main` branch contains code under active development for the next release of ASP.NET Core. To see a version of the sample app for a released version of ASP.NET Core, use the **Branch** drop down list to select a release branch (for example `release/{X.Y}`).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host ASP.NET Core on Windows with IIS

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Internet Information Services (IIS) is a flexible, secure, and manageable Web Server for hosting web apps, including ASP.NET Core.

## Supported platforms

The following operating systems are supported:

* Windows 7 or later
* Windows Server 2012 R2 or later

Apps published for 32-bit (x86) or 64-bit (x64) deployment are supported. Deploy a 32-bit app with a 32-bit (x86) .NET SDK unless the app:

* Requires the larger virtual memory address space available to a 64-bit app.
* Requires the larger IIS stack size.
* Has 64-bit native dependencies.

## Install the ASP.NET Core Module/Hosting Bundle

Download the latest installer using the following link:

[Current .NET Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

For more details instructions on how to install the ASP.NET Core Module, or installing different versions, see [Install the .NET Hosting Bundle](hosting-bundle?view=aspnetcore-10.0).

To download previous versions of the hosting bundle, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/28642).

## Get started

For getting started with hosting a website on IIS, see our [getting started guide](../../tutorials/publish-to-iis?view=aspnetcore-10.0).

For getting started with hosting a website on Azure App Services, see our [deploying to Azure App Service guide](../azure-apps/?view=aspnetcore-10.0).

## Configuration

For configuration guidance, see [Advanced configuration](advanced?view=aspnetcore-10.0).

## Deployment resources for IIS administrators

* [IIS documentation](/en-us/iis)
* [Getting Started with the IIS Manager in IIS](/en-us/iis/get-started/getting-started-with-iis/getting-started-with-the-iis-manager-in-iis-7-and-iis-8)
* [.NET application deployment](/en-us/dotnet/core/deploying/)
* [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0)
* [ASP.NET Core directory structure](../directory-structure?view=aspnetcore-10.0)
* [IIS modules with ASP.NET Core](modules?view=aspnetcore-10.0)
* [Troubleshoot ASP.NET Core on Azure App Service and IIS](../../test/troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../azure-iis-errors-reference?view=aspnetcore-10.0)

## Overlapped recycle

In general, we recommend using a pattern like [blue-green deployments](https://www.martinfowler.com/bliki/BlueGreenDeployment.html) for zero-downtime deployments. Features like Overlapped Recycle help, but don't guarantee that you can do a zero-downtime deployment. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/10117).

## Optional client certificates

For information on apps that must protect a subset of the app with a certificate, see [Optional client certificates](../../security/authentication/certauth?view=aspnetcore-10.0#optional-client-certificates).

## Additional resources

* [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)
* [Overview of ASP.NET Core](../../overview?view=aspnetcore-10.0)
* [The Official Microsoft IIS Site](https://www.iis.net/)
* [Windows Server technical content library](/en-us/windows-server/windows-server)
* [HTTP/2 on IIS](/en-us/iis/get-started/whats-new-in-iis-10/http2-on-iis)
* [Transform web.config](transform-webconfig?view=aspnetcore-10.0)

For a tutorial experience on publishing an ASP.NET Core app to an IIS server, see [Publish an ASP.NET Core app to IIS](../../tutorials/publish-to-iis?view=aspnetcore-10.0).

[Install the .NET Hosting Bundle](#install-the-net-hosting-bundle)

## Supported operating systems

The following operating systems are supported:

* Windows 7 or later
* Windows Server 2012 R2 or later

[HTTP.sys server](../../fundamentals/servers/httpsys?view=aspnetcore-10.0) (formerly called WebListener) doesn't work in a reverse proxy configuration with IIS. Use the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).

For information on hosting in Azure, see [Deploy ASP.NET Core apps to Azure App Service](../azure-apps/?view=aspnetcore-10.0).

For troubleshooting guidance, see [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0).

## Supported platforms

Apps published for 32-bit (x86) or 64-bit (x64) deployment are supported. Deploy a 32-bit app with a 32-bit (x86) .NET SDK unless the app:

* Requires the larger virtual memory address space available to a 64-bit app.
* Requires the larger IIS stack size.
* Has 64-bit native dependencies.

Apps published for 32-bit (x86) must have 32-bit enabled for their IIS Application Pools. For more information, see the [Create the IIS site](#create-the-iis-site) section.

Use a 64-bit (x64) .NET SDK to publish a 64-bit app. A 64-bit runtime must be present on the host system.

## Hosting models

### In-process hosting model

Using in-process hosting, an ASP.NET Core app runs in the same process as its IIS worker process. In-process hosting provides improved performance over out-of-process hosting because requests aren't proxied over the loopback adapter, a network interface that returns outgoing network traffic back to the same machine. IIS handles process management with the [Windows Process Activation Service (WAS)](/en-us/iis/manage/provisioning-and-managing-iis/features-of-the-windows-process-activation-service-was).

The [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0):

* Performs app initialization.

  * Loads the [CoreCLR](/en-us/dotnet/standard/glossary#coreclr).
  * Calls `Program.Main`.

* Handles the lifetime of the IIS native request.

The following diagram illustrates the relationship between IIS, the ASP.NET Core Module, and an app hosted in-process:

In-process hosting is opt-in for existing apps. The ASP.NET Core web templates use the in-process hosting model.

`CreateDefaultBuilder` adds an [IServer](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.iserver) instance by calling the [UseIIS](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiis) method to boot the [CoreCLR](/en-us/dotnet/standard/glossary#coreclr) and host the app inside of the IIS worker process (`w3wp.exe` or `iisexpress.exe`). Performance tests indicate that hosting a .NET app in-process delivers significantly higher request throughput compared to hosting the app out-of-process and proxying requests to [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).

The in-process hosting model can't load apps published as a single file executable.

### Out-of-process hosting model

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        ...
```

For more information on `CreateDefaultBuilder`, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0#default-builder-settings).

### IIS options

**In-process hosting model**

To configure IIS Server options, include a service configuration for [IISServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisserveroptions) in [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartup.configureservices). The following example disables AutomaticAuthentication:

```
services.Configure<IISServerOptions>(options => 
{
    options.AutomaticAuthentication = false;
});
```

| Option                      | Default    | Setting                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| --------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AutomaticAuthentication`   | `true`     | If `true`, IIS Server sets the `HttpContext.User` authenticated by [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0). If `false`, the server only provides an identity for `HttpContext.User` and responds to challenges when explicitly requested by the `AuthenticationScheme`. Windows Authentication must be enabled in IIS for `AutomaticAuthentication` to function. For more information, see [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).                                 |
| `AuthenticationDisplayName` | `null`     | Sets the display name shown to users on login pages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `AllowSynchronousIO`        | `false`    | Whether synchronous I/O is allowed for the `HttpContext.Request` and the `HttpContext.Response`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `MaxRequestBodySize`        | `30000000` | Gets or sets the max request body size for the `HttpRequest`. Note that IIS itself has the limit `maxAllowedContentLength`, which will be processed before the `MaxRequestBodySize` set in the `IISServerOptions`. Changing the `MaxRequestBodySize` won't affect the `maxAllowedContentLength`. To increase `maxAllowedContentLength`, add an entry in the `web.config` to set `maxAllowedContentLength` to a higher value. For more details, see [Configuration](/en-us/iis/configuration/system.webServer/security/requestFiltering/requestLimits/#configuration). |

**Out-of-process hosting model**

To configure IIS options, include a service configuration for [IISOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisoptions) in [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartup.configureservices). The following example prevents the app from populating `HttpContext.Connection.ClientCertificate`:

```
services.Configure<IISOptions>(options => 
{
    options.ForwardClientCertificate = false;
});
```

| Option                      | Default | Setting                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| --------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AutomaticAuthentication`   | `true`  | If `true`, [IIS integration middleware](#enable-the-iisintegration-components) sets the `HttpContext.User` authenticated by [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0). If `false`, the middleware only provides an identity for `HttpContext.User` and responds to challenges when explicitly requested by the `AuthenticationScheme`. Windows Authentication must be enabled in IIS for `AutomaticAuthentication` to function. For more information, see the [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0) article. |
| `AuthenticationDisplayName` | `null`  | Sets the display name shown to users on login pages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `ForwardClientCertificate`  | `true`  | If `true` and the `MS-ASPNETCORE-CLIENTCERT` request header is present, the `HttpContext.Connection.ClientCertificate` is populated.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

### Proxy server and load balancer scenarios

The [IIS integration middleware](#enable-the-iisintegration-components) and the ASP.NET Core Module are configured to forward the:

* Scheme (HTTP/HTTPS).
* Remote IP address where the request originated.

The [IIS integration middleware](#enable-the-iisintegration-components) configures forwarded headers middleware.

Additional configuration might be required for apps hosted behind additional proxy servers and load balancers. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0).

### `web.config` file

The `web.config` file configures the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0). An MSBuild target (`_TransformWebConfig`) handles creating, transforming, and publishing the `web.config` file when the project is published. This target is present in the Web SDK targets (`Microsoft.NET.Sdk.Web`). The SDK is set at the top of the project file:

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

If a `web.config` file isn't present in the project, the file is created with the correct `processPath` and `arguments` to configure the ASP.NET Core Module and moved to [published output](../directory-structure?view=aspnetcore-10.0).

If a `web.config` file is present in the project, the file is transformed with the correct `processPath` and `arguments` to configure the ASP.NET Core Module and moved to published output. The transformation doesn't modify IIS configuration settings in the file.

The `web.config` file might provide additional IIS configuration settings that control active IIS modules. For information on IIS modules that are capable of processing requests with ASP.NET Core apps, see the [IIS modules](modules?view=aspnetcore-10.0) article.

To prevent the Web SDK from transforming the `web.config` file, use the `<IsTransformWebConfigDisabled>` property in the project file:

```
<PropertyGroup>
  <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
```

When disabling the Web SDK from transforming the file, the developer should manually set the `processPath` and `arguments`. For more information, see [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0).

### `web.config` file location

In order to set up the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0) correctly, the `web.config` file must be present at the [content root](../../fundamentals/?view=aspnetcore-10.0#content-root) path (typically the app base path) of the deployed app. This is the same location as the website physical path provided to IIS. The `web.config` file is required at the root of the app to enable the publishing of multiple apps using Web Deploy.

Sensitive files exist on the app's physical path, such as `{ASSEMBLY}.runtimeconfig.json`, `{ASSEMBLY}.xml` (XML Documentation comments), and `{ASSEMBLY}.deps.json`, where the placeholder `{ASSEMBLY}` is the assembly name. When the `web.config` file is present and the site starts normally, IIS doesn't serve these sensitive files if they're requested. If the `web.config` file is missing, incorrectly named, or unable to configure the site for normal startup, IIS might serve sensitive files publicly.

**The `web.config` file must be present in the deployment at all times, correctly named, and able to configure the site for normal start up. Never remove the `web.config` file from a production deployment.**

### Transform web.config

If you need to transform `web.config` on publish, see [Transform web.config](transform-webconfig?view=aspnetcore-10.0). You might need to transform `web.config` on publish to set environment variables based on the configuration, profile, or environment.

## IIS configuration

**Windows Server operating systems**

**Windows desktop operating systems**

   To enable Windows Authentication, expand the following nodes: **World Wide Web Services** > **Security**. Select the **Windows Authentication** feature. For more information, see [Windows Authentication \<windowsAuthentication>](/en-us/iis/configuration/system.webServer/security/authentication/windowsAuthentication/) and [Configure Windows authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).

   **WebSockets (Optional)**\
   WebSockets is supported with ASP.NET Core 1.1 or later. To enable WebSockets, expand the following nodes: **World Wide Web Services** > **Application Development Features**. Select the **WebSocket Protocol** feature. For more information, see [WebSockets](../../fundamentals/websockets?view=aspnetcore-10.0).

6. If the IIS installation requires a restart, restart the system.

## Install the .NET Hosting Bundle

Install the *.NET Hosting Bundle* on the hosting system. The bundle installs the .NET Runtime, .NET Library, and the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0). The module allows ASP.NET Core apps to run behind IIS.

Important

If the Hosting Bundle is installed before IIS, the bundle installation must be repaired. Run the Hosting Bundle installer again after installing IIS.

If the Hosting Bundle is installed after installing the 64-bit (x64) version of .NET, SDKs might appear to be missing ([No .NET SDKs were detected](../../test/troubleshoot?view=aspnetcore-10.0#no-net-sdks-were-detected)). To resolve the problem, see [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0#missing-sdk-after-installing-the-net-hosting-bundle).

### Direct download (current version)

Download the installer using the following link:

[Current .NET Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

### Earlier versions of the installer

To obtain an earlier version of the installer:

1. Navigate to the [Download .NET](https://dotnet.microsoft.com/download/dotnet-core) page.
2. Select the desired .NET version.
3. In the **Run apps - Runtime** column, find the row of the .NET runtime version desired.
4. Download the installer using the **Hosting Bundle** link.

Warning

Some installers contain release versions that Microsoft no longer supports. For more information, see the [support policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core).

### Install the Hosting Bundle

1. Run the installer on the server. The following parameters are available when running the installer from an administrator command shell:

   * `OPT_NO_ANCM=1`: Skip installing the ASP.NET Core Module.
   * `OPT_NO_RUNTIME=1`: Skip installing the .NET runtime. Used when the server only hosts [self-contained deployments (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).
   * `OPT_NO_SHAREDFX=1`: Skip installing the ASP.NET Shared Framework (ASP.NET runtime). Used when the server only hosts [self-contained deployments (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).
   * `OPT_NO_X86=1`: Skip installing x86 runtimes. Use this parameter when you know that you won't be hosting 32-bit apps. If there's any chance that you'll host both 32-bit and 64-bit apps in the future, don't use this parameter and install both runtimes.
   * `OPT_NO_SHARED_CONFIG_CHECK=1`: Disable the check for using an IIS Shared Configuration when the shared configuration (`applicationHost.config`) is on the same machine as the IIS installation. *Only available for ASP.NET Core 2.2 or later Hosting Bundler installers.* For more information, see [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0#aspnet-core-module-with-an-iis-shared-configuration).

2. Restarting IIS picks up a change to the system PATH, which is an environment variable, made by the installer. To restart the web server, stop the Windows Process Activation Service (WAS) and then restart the World Wide Web Publishing Service (W3SVC). Restart the system **or** execute the following commands in an elevated command shell:

   ```
   net stop was /y
   net start w3svc
   ```

ASP.NET Core doesn't adopt roll-forward behavior for patch releases of shared framework packages. After upgrading the shared framework by installing a new hosting bundle, restart the system **or** execute the following commands in an elevated command shell:

```
net stop was /y
net start w3svc
```

Note

For information on IIS Shared Configuration, see [ASP.NET Core Module with IIS Shared Configuration](../aspnet-core-module?view=aspnetcore-10.0#aspnet-core-module-with-an-iis-shared-configuration).

## Install Web Deploy when publishing with Visual Studio

When deploying apps to servers with [Web Deploy](/en-us/iis/install/installing-publishing-technologies/installing-and-configuring-web-deploy-on-iis-80-or-later), install the latest version of Web Deploy on the server. To install Web Deploy, use the [Web Platform Installer (WebPI)](https://www.microsoft.com/web/downloads/platform.aspx) or see [IIS Downloads: Web Deploy](https://www.iis.net/downloads/microsoft/web-deploy). The preferred method is to use WebPI. WebPI offers a standalone setup and a configuration for hosting providers.

5. Right-click the site's app pool and select **Basic Settings** from the contextual menu.

6. In the **Edit Application Pool** window, set the **.NET CLR version** to **No Managed Code**:

   ASP.NET Core runs in a separate process and manages the runtime. ASP.NET Core doesn't rely on loading the desktop CLR (.NET CLR). The Core Common Language Runtime (CoreCLR) for .NET is booted to host the app in the worker process. Setting the **.NET CLR version** to **No Managed Code** is optional but recommended.

7. *ASP.NET Core 2.2 or later*:

   * For a 32-bit (x86) [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) published with a 32-bit SDK that uses the [in-process hosting model](#in-process-hosting-model), enable the Application Pool for 32-bit. In IIS Manager, navigate to **Application Pools** in the **Connections** sidebar. Select the app's Application Pool. In the **Actions** sidebar, select **Advanced Settings**. Set **Enable 32-Bit Applications** to `True`.

   * For a 64-bit (x64) [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) that uses the [in-process hosting model](#in-process-hosting-model), disable the app pool for 32-bit (x86) processes. In IIS Manager, navigate to **Application Pools** in the **Connections** sidebar. Select the app's Application Pool. In the **Actions** sidebar, select **Advanced Settings**. Set **Enable 32-Bit Applications** to `False`.

8. Confirm the process model identity has the proper permissions.

   If the default identity of the app pool (**Process Model** > **Identity**) is changed from **ApplicationPoolIdentity** to another identity, verify that the new identity has the required permissions to access the app's folder, database, and other required resources. For example, the app pool requires read and write access to folders where the app reads and writes files.

**Windows Authentication configuration (Optional)**\
For more information, see [Configure Windows authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).

## Deploy the app

Deploy the app to the IIS **Physical path** folder that was established in the [Create the IIS site](#create-the-iis-site) section. [Web Deploy](/en-us/iis/publish/using-web-deploy/introduction-to-web-deploy) is the recommended mechanism for deployment, but several options exist for moving the app from the project's `publish` folder to the hosting system's deployment folder.

### Web Deploy with Visual Studio

See the [Visual Studio publish profiles for ASP.NET Core app deployment](../visual-studio-publish-profiles?view=aspnetcore-10.0#publish-profiles) article to learn how to create a publish profile for use with Web Deploy. If the hosting provider provides a Publish Profile or support for creating one, download their profile and import it using the Visual Studio **Publish** dialog:

### Web Deploy outside of Visual Studio

[Web Deploy](/en-us/iis/publish/using-web-deploy/introduction-to-web-deploy) can also be used outside of Visual Studio from the command line. For more information, see [Web Deployment Tool](/en-us/iis/publish/using-web-deploy/use-the-web-deployment-tool).

### Alternatives to Web Deploy

Use any of several methods to move the app to the hosting system, such as manual copy, [Xcopy](/en-us/windows-server/administration/windows-commands/xcopy), [Robocopy](/en-us/windows-server/administration/windows-commands/robocopy), or [PowerShell](/en-us/powershell/).

For more information on ASP.NET Core deployment to IIS, see the [Deployment resources for IIS administrators](#deployment-resources-for-iis-administrators) section.

## Browse the website

After the app is deployed to the hosting system, make a request to one of the app's public endpoints.

In the following example, the site is bound to an IIS **Host name** of `www.mysite.com` on **Port** `80`. A request is made to `http://www.mysite.com`:

## Locked deployment files

Files in the deployment folder are locked when the app is running. Locked files can't be overwritten during deployment. To release locked files in a deployment, stop the app pool using **one** of the following approaches:

* Use Web Deploy and reference `Microsoft.NET.Sdk.Web` in the project file. An `app_offline.htm` file is placed at the root of the web app directory. When the file is present, the ASP.NET Core Module gracefully shuts down the app and serves the `app_offline.htm` file during the deployment. For more information, see the [ASP.NET Core Module configuration reference](../aspnet-core-module?view=aspnetcore-10.0#app_offlinehtm).

* Manually stop the app pool in the IIS Manager on the server.

* Use PowerShell to drop `app_offline.htm` (requires PowerShell 5 or later):

  ```
  $pathToApp = 'PATH_TO_APP'

  # Stop the AppPool
  New-Item -Path $pathToApp app_offline.htm

  # Provide script commands here to deploy the app

  # Restart the AppPool
  Remove-Item -Path $pathToApp app_offline.htm
  ```

## Data protection

The [ASP.NET Core Data Protection stack](../../security/data-protection/introduction?view=aspnetcore-10.0) is used by several ASP.NET Core [middlewares](../../fundamentals/middleware/?view=aspnetcore-10.0), including middleware used in authentication. Even if Data Protection APIs aren't called by user code, data protection should be configured with a deployment script or in user code to create a persistent cryptographic [key store](../../security/data-protection/implementation/key-management?view=aspnetcore-10.0). If data protection isn't configured, the keys are held in memory and discarded when the app restarts.

If the key ring is stored in memory when the app restarts:

* All cookie-based authentication tokens are invalidated.
* Users are required to sign in again on their next request.
* Any data protected with the key ring can no longer be decrypted. This might include [CSRF tokens](../../security/anti-request-forgery?view=aspnetcore-10.0#aspnet-core-antiforgery-configuration) and [ASP.NET Core MVC TempData cookies](../../fundamentals/app-state?view=aspnetcore-10.0#tempdata).

To configure data protection under IIS to persist the key ring, use **one** of the following approaches:

* **Create Data Protection Registry Keys**

  Data protection keys used by ASP.NET Core apps are stored in the registry external to the apps. To persist the keys for a given app, create registry keys for the app pool.

  For standalone, non-webfarm IIS installations, the [Data Protection Provision-AutoGenKeys.ps1 PowerShell script](https://github.com/dotnet/AspNetCore/blob/main/src/DataProtection/Provision-AutoGenKeys.ps1) can be used for each app pool used with an ASP.NET Core app. This script creates a registry key in the HKLM registry that's accessible only to the worker process account of the app's app pool. Keys are encrypted at rest using DPAPI with a machine-wide key.

  In web farm scenarios, an app can be configured to use a UNC path to store its data protection key ring. By default, the data protection keys aren't encrypted. Ensure that the file permissions for the network share are limited to the Windows account the app runs under. An X509 certificate can be used to protect keys at rest. Consider a mechanism to allow users to upload certificates: Place certificates into the user's trusted certificate store and ensure they're available on all machines where the user's app runs. See [Configure ASP.NET Core Data Protection](../../security/data-protection/configuration/overview?view=aspnetcore-10.0) for details.

* **Configure the IIS Application Pool to load the user profile**

  This setting is in the **Process Model** section under the **Advanced Settings** for the app pool. Set **Load User Profile** to `True`. When set to `True`, keys are stored in the user profile directory and protected using DPAPI with a key specific to the user account. Keys are persisted to the *%LOCALAPPDATA%/ASP.NET/DataProtection-Keys* folder.

  The app pool's [setProfileEnvironment attribute](/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel#configuration) must also be enabled. The default value of `setProfileEnvironment` is `true`. In some scenarios (for example, Windows OS), `setProfileEnvironment` is set to `false`. If keys aren't stored in the user profile directory as expected:

  1. Navigate to the *%windir%/system32/inetsrv/config* folder.
  2. Open the *applicationHost.config* file.
  3. Locate the `<system.applicationHost><applicationPools><applicationPoolDefaults><processModel>` element.
  4. Confirm that the `setProfileEnvironment` attribute isn't present, which defaults the value to `true`, or explicitly set the attribute's value to `true`.

* **Use the file system as a key ring store**

  Adjust the app code to [use the file system as a key ring store](../../security/data-protection/configuration/overview?view=aspnetcore-10.0). Use an X509 certificate to protect the key ring and ensure the certificate is a trusted certificate. If the certificate is self-signed, place the certificate in the Trusted Root store.

  When using IIS in a web farm:

  * Use a file share that all machines can access.
  * Deploy an X509 certificate to each machine. Configure [data protection in code](../../security/data-protection/configuration/overview?view=aspnetcore-10.0).

* **Set a machine-wide policy for data protection**

  The data protection system has limited support for setting a default [machine-wide policy](../../security/data-protection/configuration/machine-wide-policy?view=aspnetcore-10.0) for all apps that consume the Data Protection APIs. For more information, see [ASP.NET Core Data Protection Overview](../../security/data-protection/introduction?view=aspnetcore-10.0).

## Virtual Directories

[IIS Virtual Directories](/en-us/iis/get-started/planning-your-iis-architecture/understanding-sites-applications-and-virtual-directories-on-iis#virtual-directories) aren't supported with ASP.NET Core apps. An app can be hosted as a [sub-application](#sub-applications).

## Sub-applications

An ASP.NET Core app can be hosted as an [IIS sub-application (sub-app)](/en-us/iis/get-started/planning-your-iis-architecture/understanding-sites-applications-and-virtual-directories-on-iis#applications). The sub-app's path becomes part of the root app's URL.

Static asset links within the sub-app should use tilde-slash (`~/`) notation. Tilde-slash notation triggers a [Tag Helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) to prepend the sub-app's pathbase to the rendered relative link. For a sub-app at `/subapp_path`, an image linked with `src="~/image.png"` is rendered as `src="/subapp_path/image.png"`. The root app's static file middleware doesn't process the static file request. The sub-app's static file middleware processes the request.

## Configuration of IIS with web.config

The `<system.webServer>` section of *web.config* for IIS scenarios that are functional for ASP.NET Core apps with the ASP.NET Core Module influences IIS configuration. For example, IIS configuration is functional for dynamic compression. If IIS is configured at the server level to use dynamic compression, the `<urlCompression>` element in the app's *web.config* file can disable it for an ASP.NET Core app.

For more information, see the following articles:

* [Configuration reference for \<system.webServer>](/en-us/iis/configuration/system.webServer/)
* [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0)
* [IIS modules with ASP.NET Core](modules?view=aspnetcore-10.0)

To set environment variables for individual apps running in isolated app pools (supported for IIS 10.0 or later), see the *AppCmd.exe command* section of the [Environment Variables \<environmentVariables>](/en-us/iis/configuration/system.applicationHost/applicationPools/add/environmentVariables/#appcmdexe) article in the IIS reference documentation.

### Sections that aren't used by ASP.NET Core

Configuration sections of ASP.NET apps in *web.config* aren't used by ASP.NET Core apps for configuration:

* `<system.web>`
* `<appSettings>`
* `<connectionStrings>`
* `<location>`

ASP.NET Core apps are configured using other configuration providers. For more information, see [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) and [.NET run-time configuration settings](/en-us/dotnet/core/run-time-config/)

## Application Pools

The hosting model determines app pool isolation:

* In-process hosting: Apps are required to run in separate app pools.
* Out-of-process hosting: We recommend isolating the apps from each other by running each app in its own app pool.

The IIS **Add Website** dialog defaults to a single app pool per app. When a **Site name** is provided, the text is automatically transferred to the **Application pool** textbox. A new app pool is created using the site name when the site is added.

## Application Pool Identity

An app pool identity account allows an app to run under a unique account without having to create and manage domains or local accounts. On IIS 8.0 or later, the IIS Admin Worker Process (WAS) creates a virtual account with the name of the new app pool and runs the app pool's worker processes under this account by default. In the IIS Management Console under **Advanced Settings** for the app pool, ensure that the **Identity** is set to use **ApplicationPoolIdentity**:

The IIS management process creates a secure identifier with the name of the app pool in the Windows Security System. Resources can be secured using this identity. However, this identity isn't a real user account and doesn't show up in the Windows User Management Console.

If the IIS worker process requires elevated access to the app, modify the Access Control List (ACL) for the directory containing the app:

1. Open Windows Explorer and navigate to the directory.

2. Right-click on the directory and select **Properties**.

3. Under the **Security** tab, select the **Edit** button and then the **Add** button.

4. Select the **Locations** button and make sure the system is selected.

5. Enter `IIS AppPool\{APP POOL NAME}`, where the placeholder `{APP POOL NAME}` is the app pool name, in **Enter the object names to select** area. Select the **Check Names** button. For the *DefaultAppPool* check the names using `IIS AppPool\DefaultAppPool`. When the **Check Names** button is selected, a value of `DefaultAppPool` is indicated in the object names area. It isn't possible to enter the app pool name directly into the object names area. Use the `IIS AppPool\{APP POOL NAME}` format, where the placeholder `{APP POOL NAME}` is the app pool name, when checking for the object name.

6. Select **OK**.

7. Read & execute permissions should be granted by default. Provide additional permissions as needed.

Access can also be granted at a command prompt using the **ICACLS** tool. Using the `DefaultAppPool` as an example, the following command is used:

```
ICACLS C:\sites\MyWebApp /grant "IIS AppPool\DefaultAppPool":F
```

For more information, see the [icacls](/en-us/windows-server/administration/windows-commands/icacls) article.

## HTTP/2 support

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is supported with ASP.NET Core in the following IIS deployment scenarios:

* In-process

  * Windows Server 2016/Windows 10 or later; IIS 10 or later
  * TLS 1.2 or later connection

* Out-of-process

  * Windows Server 2016/Windows 10 or later; IIS 10 or later
  * Public-facing edge server connections use HTTP/2, but the reverse proxy connection to the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) uses HTTP/1.1.
  * TLS 1.2 or later connection

* [Application Initialization Module](#application-initialization-module): App's hosted [in-process](#in-process-hosting-model) or [out-of-process](#out-of-process-hosting-model) can be configured to start automatically on a worker process restart or server restart.
* [Idle Timeout](#idle-timeout): App's hosted [in-process](#in-process-hosting-model) can be configured not to timeout during periods of inactivity.

  3. The default **Start Mode** is **OnDemand**. Set the **Start Mode** to **AlwaysRunning**. Select **OK**.
  4. Open the **Sites** node in the **Connections** panel.
  5. Right-click the app and select **Manage Website** > **Advanced Settings**.
  6. The default **Preload Enabled** setting is **False**. Set **Preload Enabled** to **True**. Select **OK**.

* Using `web.config`, add the `<applicationInitialization>` element with `doAppInitAfterRestart` set to `true` to the `<system.webServer>` elements in the app's web.config\` file:

  ```
  <?xml version="1.0" encoding="utf-8"?>
  <configuration>
    <location path="." inheritInChildApplications="false">
      <system.webServer>
        <applicationInitialization doAppInitAfterRestart="true" />
      </system.webServer>
    </location>
  </configuration>
  ```

### Idle Timeout

*Only applies to apps hosted in-process.*

To prevent the app from idling, set the app pool's idle timeout using IIS Manager:

1. Select **Application Pools** in the **Connections** panel.
2. Right-click the app's app pool in the list and select **Advanced Settings**.
3. The default **Idle Time-out (minutes)** is **20** minutes. Set the **Idle Time-out (minutes)** to **0** (zero). Select **OK**.
4. Recycle the worker process.

To prevent apps hosted [out-of-process](#out-of-process-hosting-model) from timing out, use either of the following approaches:

* Ping the app from an external service in order to keep it running.
* If the app only hosts background services, avoid IIS hosting and use a [Windows Service to host the ASP.NET Core app](../windows-service?view=aspnetcore-10.0).

### Application Initialization Module and Idle Timeout additional resources

* [IIS 8.0 Application Initialization](/en-us/iis/get-started/whats-new-in-iis-8/iis-80-application-initialization)
* [Application Initialization \<applicationInitialization>](/en-us/iis/configuration/system.webserver/applicationinitialization/).
* [Process Model Settings for an Application Pool \<processModel>](/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel).

## Deployment resources for IIS administrators

* [IIS documentation](/en-us/iis)
* [Getting Started with the IIS Manager in IIS](/en-us/iis/get-started/getting-started-with-iis/getting-started-with-the-iis-manager-in-iis-7-and-iis-8)
* [.NET application deployment](/en-us/dotnet/core/deploying/)
* [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0)
* [ASP.NET Core directory structure](../directory-structure?view=aspnetcore-10.0)
* [IIS modules with ASP.NET Core](modules?view=aspnetcore-10.0)
* [Troubleshoot ASP.NET Core on Azure App Service and IIS](../../test/troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../azure-iis-errors-reference?view=aspnetcore-10.0)

## Additional resources

* [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)
* [Overview of ASP.NET Core](../../overview?view=aspnetcore-10.0)
* [The Official Microsoft IIS Site](https://www.iis.net/)
* [Windows Server technical content library](/en-us/windows-server/windows-server)
* [HTTP/2 on IIS](/en-us/iis/get-started/whats-new-in-iis-10/http2-on-iis)
* [Transform web.config](transform-webconfig?view=aspnetcore-10.0)

For a tutorial experience on publishing an ASP.NET Core app to an IIS server, see [Publish an ASP.NET Core app to IIS](../../tutorials/publish-to-iis?view=aspnetcore-10.0).

[Install the .NET Hosting Bundle](#install-the-net-hosting-bundle)

## Supported operating systems

The following operating systems are supported:

* Windows 7 or later
* Windows Server 2008 R2 or later

[HTTP.sys server](../../fundamentals/servers/httpsys?view=aspnetcore-10.0) (formerly called WebListener) doesn't work in a reverse proxy configuration with IIS. Use the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).

For information on hosting in Azure, see [Deploy ASP.NET Core apps to Azure App Service](../azure-apps/?view=aspnetcore-10.0).

For troubleshooting guidance, see [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0).

## Supported platforms

Apps published for 32-bit (x86) or 64-bit (x64) deployment are supported. Deploy a 32-bit app with a 32-bit (x86) .NET SDK unless the app:

* Requires the larger virtual memory address space available to a 64-bit app.
* Requires the larger IIS stack size.
* Has 64-bit native dependencies.

Use a 64-bit (x64) .NET SDK to publish a 64-bit app. A 64-bit runtime must be present on the host system.

ASP.NET Core ships with [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0), a default, cross-platform HTTP server.

When using [IIS](/en-us/iis/get-started/introduction-to-iis/introduction-to-iis-architecture) or [IIS Express](/en-us/iis/extensions/introduction-to-iis-express/iis-express-overview), the app runs in a process separate from the IIS worker process (*out-of-process*) with the [Kestrel server](../../fundamentals/servers/?view=aspnetcore-10.0#kestrel).

Because ASP.NET Core apps run in a process separate from the IIS worker process, the module handles process management. The module starts the process for the ASP.NET Core app when the first request arrives and restarts the app if it shuts down or crashes. This is essentially the same behavior as seen with apps that run in-process that are managed by the [Windows Process Activation Service (WAS)](/en-us/iis/manage/provisioning-and-managing-iis/features-of-the-windows-process-activation-service-was).

The following diagram illustrates the relationship between IIS, the ASP.NET Core Module, and an app hosted out-of-process:

Requests arrive from the web to the kernel-mode HTTP.sys driver. The driver routes the requests to IIS on the website's configured port, usually 80 (HTTP) or 443 (HTTPS). The module forwards the requests to Kestrel on a random port for the app, which isn't port 80 or 443.

The module specifies the port via an environment variable at startup, and the [IIS integration middleware](./?view=aspnetcore-10.0#enable-the-iisintegration-components) configures the server to listen on `http://localhost:{port}`. Additional checks are performed, and requests that don't originate from the module are rejected. The module doesn't support HTTPS forwarding, so requests are forwarded over HTTP even if received by IIS over HTTPS.

After Kestrel picks up the request from the module, the request is pushed into the ASP.NET Core middleware pipeline. The middleware pipeline handles the request and passes it on as an `HttpContext` instance to the app's logic. Middleware added by IIS Integration updates the scheme, remote IP, and pathbase to account for forwarding the request to Kestrel. The app's response is passed back to IIS, which pushes it back out to the HTTP client that initiated the request.

`CreateDefaultBuilder` configures [Kestrel](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) server as the web server and enables IIS Integration by configuring the base path and port for the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0).

The ASP.NET Core Module generates a dynamic port to assign to the backend process. `CreateDefaultBuilder` calls the [UseIISIntegration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiisintegration) method. `UseIISIntegration` configures Kestrel to listen on the dynamic port at the localhost IP address (`127.0.0.1`). If the dynamic port is 1234, Kestrel listens at `127.0.0.1:1234`. This configuration replaces other URL configurations provided by:

* `UseUrls`
* [Kestrel's Listen API](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration)
* [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) (or [command-line --urls option](../../fundamentals/host/web-host?view=aspnetcore-10.0#override-configuration))

Calls to `UseUrls` or Kestrel's `Listen` API aren't required when using the module. If `UseUrls` or `Listen` is called, Kestrel listens on the port specified only when running the app without IIS.

For ASP.NET Core Module configuration guidance, see [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0).

For more information on hosting, see [Host in ASP.NET Core](../../fundamentals/?view=aspnetcore-10.0#host).

## Application configuration

### Enable the IISIntegration components

When building a host in `CreateWebHostBuilder` (`Program.cs`), call [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder) to enable IIS integration:

```
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        ...
```

For more information on `CreateDefaultBuilder`, see [ASP.NET Core Web Host](../../fundamentals/host/web-host?view=aspnetcore-10.0#set-up-a-host).

### IIS options

| Option                      | Default | Setting                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AutomaticAuthentication`   | `true`  | If `true`, IIS Server sets the `HttpContext.User` authenticated by [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0). If `false`, the server only provides an identity for `HttpContext.User` and responds to challenges when explicitly requested by the `AuthenticationScheme`. Windows Authentication must be enabled in IIS for `AutomaticAuthentication` to function. For more information, see [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0). |
| `AuthenticationDisplayName` | `null`  | Sets the display name shown to users on login pages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

To configure IIS options, include a service configuration for [IISOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.iisoptions) in [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartup.configureservices). The following example prevents the app from populating `HttpContext.Connection.ClientCertificate`:

```
services.Configure<IISOptions>(options => 
{
    options.ForwardClientCertificate = false;
});
```

| Option                      | Default | Setting                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| --------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AutomaticAuthentication`   | `true`  | If `true`, [IIS integration middleware](#enable-the-iisintegration-components) sets the `HttpContext.User` authenticated by [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0). If `false`, the middleware only provides an identity for `HttpContext.User` and responds to challenges when explicitly requested by the `AuthenticationScheme`. Windows Authentication must be enabled in IIS for `AutomaticAuthentication` to function. For more information, see the [Windows Authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0) article. |
| `AuthenticationDisplayName` | `null`  | Sets the display name shown to users on login pages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `ForwardClientCertificate`  | `true`  | If `true` and the `MS-ASPNETCORE-CLIENTCERT` request header is present, the `HttpContext.Connection.ClientCertificate` is populated.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

### Proxy server and load balancer scenarios

The [IIS integration middleware](#enable-the-iisintegration-components), which configures forwarded headers middleware, and the ASP.NET Core Module are configured to forward the scheme (HTTP/HTTPS) and the remote IP address where the request originated. Additional configuration might be required for apps hosted behind additional proxy servers and load balancers. For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0).

### web.config file

The *web.config* file configures the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0). MSBuild target (`_TransformWebConfig`) handles creating, transforming, and publishing the *web.config* file when the project is published. This target is present in the Web SDK targets (`Microsoft.NET.Sdk.Web`). The SDK is set at the top of the project file:

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

If a *web.config* file isn't present in the project, the file is created with the correct *processPath* and *arguments* to configure the ASP.NET Core Module and moved to [published output](../directory-structure?view=aspnetcore-10.0).

If a *web.config* file is present in the project, the file is transformed with the correct *processPath* and *arguments* to configure the ASP.NET Core Module and moved to published output. The transformation doesn't modify IIS configuration settings in the file.

The *web.config* file might provide additional IIS configuration settings that control active IIS modules. For information on IIS modules that are capable of processing requests with ASP.NET Core apps, see the [IIS modules](modules?view=aspnetcore-10.0) article.

To prevent the Web SDK from transforming the *web.config* file, use the **\<IsTransformWebConfigDisabled>** property in the project file:

```
<PropertyGroup>
  <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
```

When disabling the Web SDK from transforming the file, the developer should manually set the *processPath* and *arguments*. For more information, see [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0).

### web.config file location

In order to set up the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0) correctly, the *web.config* file must be present at the [content root](../../fundamentals/?view=aspnetcore-10.0#content-root) path (typically the app base path) of the deployed app. This is the same location as the website physical path provided to IIS. The *web.config* file is required at the root of the app to enable the publishing of multiple apps using Web Deploy.

Sensitive files exist on the app's physical path, such as *\<assembly>.runtimeconfig.json*, *\<assembly>.xml* (XML Documentation comments), and *\<assembly>.deps.json*. When the *web.config* file is present and the site starts normally, IIS doesn't serve these sensitive files if they're requested. If the *web.config* file is missing, incorrectly named, or unable to configure the site for normal startup, IIS might serve sensitive files publicly.

**The *web.config* file must be present in the deployment at all times, correctly named, and able to configure the site for normal start up. Never remove the *web.config* file from a production deployment.**

### Transform web.config

If you need to transform *web.config* on publish (for example, set environment variables based on the configuration, profile, or environment), see [Transform web.config](transform-webconfig?view=aspnetcore-10.0).

## IIS configuration

**Windows Server operating systems**

Enable the **Web Server (IIS)** server role and establish role services.

1. Use the **Add Roles and Features** wizard from the **Manage** menu or the link in **Server Manager**. On the **Server Roles** step, check the box for **Web Server (IIS)**.

2. After the **Features** step, the **Role services** step loads for Web Server (IIS). Select the IIS role services desired or accept the default role services provided.

   **Windows Authentication (Optional)**\
   To enable Windows Authentication, expand the following nodes: **Web Server** > **Security**. Select the **Windows Authentication** feature. For more information, see [Windows Authentication \<windowsAuthentication>](/en-us/iis/configuration/system.webServer/security/authentication/windowsAuthentication/) and [Configure Windows authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).

   **WebSockets (Optional)**\
   WebSockets is supported with ASP.NET Core 1.1 or later. To enable WebSockets, expand the following nodes: **Web Server** > **Application Development**. Select the **WebSocket Protocol** feature. For more information, see [WebSockets](../../fundamentals/websockets?view=aspnetcore-10.0).

3. Proceed through the **Confirmation** step to install the web server role and services. A server/IIS restart isn't required after installing the **Web Server (IIS)** role.

**Windows desktop operating systems**

   To enable Windows Authentication, expand the following nodes: **World Wide Web Services** > **Security**. Select the **Windows Authentication** feature. For more information, see [Windows Authentication \<windowsAuthentication>](/en-us/iis/configuration/system.webServer/security/authentication/windowsAuthentication/) and [Configure Windows authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).

   **WebSockets (Optional)**\
   WebSockets is supported with ASP.NET Core 1.1 or later. To enable WebSockets, expand the following nodes: **World Wide Web Services** > **Application Development Features**. Select the **WebSocket Protocol** feature. For more information, see [WebSockets](../../fundamentals/websockets?view=aspnetcore-10.0).

6. If the IIS installation requires a restart, restart the system.

## Install the .NET Hosting Bundle

Install the *.NET Hosting Bundle* on the hosting system. The bundle installs the .NET Runtime, .NET Library, and the [ASP.NET Core Module](../aspnet-core-module?view=aspnetcore-10.0). The module allows ASP.NET Core apps to run behind IIS.

Important

If the Hosting Bundle is installed before IIS, the bundle installation must be repaired. Run the Hosting Bundle installer again after installing IIS.

If the Hosting Bundle is installed after installing the 64-bit (x64) version of .NET, SDKs might appear to be missing ([No .NET SDKs were detected](../../test/troubleshoot?view=aspnetcore-10.0#no-net-sdks-were-detected)). To resolve the problem, see [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0#missing-sdk-after-installing-the-net-hosting-bundle).

### Download

1. Navigate to the [Download .NET](https://dotnet.microsoft.com/download/dotnet-core) page.
2. Select the desired .NET version.
3. In the **Run apps - Runtime** column, find the row of the .NET runtime version desired.
4. Download the installer using the **Hosting Bundle** link.

Warning

Some installers contain release versions that Microsoft no longer supports. For more information, see the [support policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core).

### Install the Hosting Bundle

1. Run the installer on the server. The following parameters are available when running the installer from an administrator command shell:

   * `OPT_NO_ANCM=1`: Skip installing the ASP.NET Core Module.
   * `OPT_NO_RUNTIME=1`: Skip installing the .NET runtime. Used when the server only hosts [self-contained deployments (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).
   * `OPT_NO_SHAREDFX=1`: Skip installing the ASP.NET Shared Framework (ASP.NET runtime). Used when the server only hosts [self-contained deployments (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).
   * `OPT_NO_X86=1`: Skip installing x86 runtimes. Use this parameter when you know that you won't be hosting 32-bit apps. If there's any chance that you'll host both 32-bit and 64-bit apps in the future, don't use this parameter and install both runtimes.
   * `OPT_NO_SHARED_CONFIG_CHECK=1`: Disable the check for using an IIS Shared Configuration when the shared configuration (*applicationHost.config*) is on the same machine as the IIS installation. *Only available for ASP.NET Core 2.2 or later Hosting Bundler installers.* For more information, see [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0#aspnet-core-module-with-an-iis-shared-configuration).

2. Restart the system or execute the following commands in a command shell:

   ```
   net stop was /y
   net start w3svc
   ```

   Restarting IIS picks up a change to the system PATH, which is an environment variable, made by the installer.

It isn't necessary to manually stop individual sites in IIS when installing the Hosting Bundle. Hosted apps (IIS sites) restart when IIS restarts. Apps start up again when they receive their first request, including from the [Application Initialization Module](#application-initialization-module-and-idle-timeout).

ASP.NET Core adopts roll-forward behavior for patch releases of shared framework packages. When apps hosted by IIS restart with IIS, the apps load with the latest patch releases of their referenced packages when they receive their first request. If IIS isn't restarted, apps restart and exhibit roll-forward behavior when their worker processes are recycled and they receive their first request.

Note

For information on IIS Shared Configuration, see [ASP.NET Core Module with IIS Shared Configuration](../aspnet-core-module?view=aspnetcore-10.0#aspnet-core-module-with-an-iis-shared-configuration).

## Install Web Deploy when publishing with Visual Studio

When deploying apps to servers with [Web Deploy](/en-us/iis/install/installing-publishing-technologies/installing-and-configuring-web-deploy-on-iis-80-or-later), install the latest version of Web Deploy on the server. To install Web Deploy, use the [Web Platform Installer (WebPI)](https://www.microsoft.com/web/downloads/platform.aspx) or [IIS Downloads: Web Deploy](https://www.iis.net/downloads/microsoft/web-deploy). The preferred method is to use WebPI. WebPI offers a standalone setup and a configuration for hosting providers.

5. Right-click the site's app pool and select **Basic Settings** from the contextual menu.

6. In the **Edit Application Pool** window, set the **.NET CLR version** to **No Managed Code**:

   ASP.NET Core runs in a separate process and manages the runtime. ASP.NET Core doesn't rely on loading the desktop CLR (.NET CLR)—the Core Common Language Runtime (CoreCLR) for .NET is booted to host the app in the worker process. Setting the **.NET CLR version** to **No Managed Code** is optional but recommended.

7. *ASP.NET Core 2.2 or later*: For a 64-bit (x64) [self-contained deployment](/en-us/dotnet/core/deploying/#self-contained-deployments-scd) that uses the [in-process hosting model](#in-process-hosting-model), disable the app pool for 32-bit (x86) processes.

   In the **Actions** sidebar of IIS Manager > **Application Pools**, select **Set Application Pool Defaults** or **Advanced Settings**. Locate **Enable 32-Bit Applications** and set the value to `False`. This setting doesn't affect apps deployed for [out-of-process hosting](../aspnet-core-module?view=aspnetcore-10.0#out-of-process-hosting-model).

8. Confirm the process model identity has the proper permissions.

   If the default identity of the app pool (**Process Model** > **Identity**) is changed from **ApplicationPoolIdentity** to another identity, verify that the new identity has the required permissions to access the app's folder, database, and other required resources. For example, the app pool requires read and write access to folders where the app reads and writes files.

**Windows Authentication configuration (Optional)**\
For more information, see [Configure Windows authentication](../../security/authentication/windowsauth?view=aspnetcore-10.0).

## Deploy the app

Deploy the app to the IIS **Physical path** folder that was established in the [Create the IIS site](#create-the-iis-site) section. [Web Deploy](/en-us/iis/publish/using-web-deploy/introduction-to-web-deploy) is the recommended mechanism for deployment, but several options exist for moving the app from the project's *publish* folder to the hosting system's deployment folder.

### Web Deploy with Visual Studio

See the [Visual Studio publish profiles for ASP.NET Core app deployment](../visual-studio-publish-profiles?view=aspnetcore-10.0#publish-profiles) article to learn how to create a publish profile for use with Web Deploy. If the hosting provider provides a Publish Profile or support for creating one, download their profile and import it using the Visual Studio **Publish** dialog:

### Web Deploy outside of Visual Studio

[Web Deploy](/en-us/iis/publish/using-web-deploy/introduction-to-web-deploy) can also be used outside of Visual Studio from the command line. For more information, see [Web Deployment Tool](/en-us/iis/publish/using-web-deploy/use-the-web-deployment-tool).

### Alternatives to Web Deploy

Use any of several methods to move the app to the hosting system, such as manual copy, [Xcopy](/en-us/windows-server/administration/windows-commands/xcopy), [Robocopy](/en-us/windows-server/administration/windows-commands/robocopy), or [PowerShell](/en-us/powershell/).

For more information on ASP.NET Core deployment to IIS, see the [Deployment resources for IIS administrators](#deployment-resources-for-iis-administrators) section.

## Browse the website

After the app is deployed to the hosting system, make a request to one of the app's public endpoints.

In the following example, the site is bound to an IIS **Host name** of `www.mysite.com` on **Port** `80`. A request is made to `http://www.mysite.com`:

## Locked deployment files

Files in the deployment folder are locked when the app is running. Locked files can't be overwritten during deployment. To release locked files in a deployment, stop the app pool using **one** of the following approaches:

* Use Web Deploy and reference `Microsoft.NET.Sdk.Web` in the project file. An `app_offline.htm` file is placed at the root of the web app directory. When the file is present, the ASP.NET Core Module gracefully shuts down the app and serves the `app_offline.htm` file during the deployment. For more information, see the [ASP.NET Core Module configuration reference](../aspnet-core-module?view=aspnetcore-10.0#app_offlinehtm).

* Manually stop the app pool in the IIS Manager on the server.

* Use PowerShell to drop `app_offline.htm` (requires PowerShell 5 or later):

  ```
  $pathToApp = 'PATH_TO_APP'

  # Stop the AppPool
  New-Item -Path $pathToApp app_offline.htm

  # Provide script commands here to deploy the app

  # Restart the AppPool
  Remove-Item -Path $pathToApp app_offline.htm
  ```

## Data protection

The [ASP.NET Core Data Protection stack](../../security/data-protection/introduction?view=aspnetcore-10.0) is used by several ASP.NET Core [middlewares](../../fundamentals/middleware/?view=aspnetcore-10.0), including middleware used in authentication. Even if Data Protection APIs aren't called by user code, data protection should be configured with a deployment script or in user code to create a persistent cryptographic [key store](../../security/data-protection/implementation/key-management?view=aspnetcore-10.0). If data protection isn't configured, the keys are held in memory and discarded when the app restarts.

If the key ring is stored in memory when the app restarts:

* All cookie-based authentication tokens are invalidated.
* Users are required to sign in again on their next request.
* Any data protected with the key ring can no longer be decrypted. This might include [CSRF tokens](../../security/anti-request-forgery?view=aspnetcore-10.0#aspnet-core-antiforgery-configuration) and [ASP.NET Core MVC TempData cookies](../../fundamentals/app-state?view=aspnetcore-10.0#tempdata).

To configure data protection under IIS to persist the key ring, use **one** of the following approaches:

* **Create Data Protection Registry Keys**

  Data protection keys used by ASP.NET Core apps are stored in the registry external to the apps. To persist the keys for a given app, create registry keys for the app pool.

  For standalone, non-webfarm IIS installations, the [Data Protection Provision-AutoGenKeys.ps1 PowerShell script](https://github.com/dotnet/AspNetCore/blob/main/src/DataProtection/Provision-AutoGenKeys.ps1) can be used for each app pool used with an ASP.NET Core app. This script creates a registry key in the HKLM registry that's accessible only to the worker process account of the app's app pool. Keys are encrypted at rest using DPAPI with a machine-wide key.

  In web farm scenarios, an app can be configured to use a UNC path to store its data protection key ring. By default, the data protection keys aren't encrypted. Ensure that the file permissions for the network share are limited to the Windows account the app runs under. An X509 certificate can be used to protect keys at rest. Consider a mechanism to allow users to upload certificates: Place certificates into the user's trusted certificate store and ensure they're available on all machines where the user's app runs. See [Configure ASP.NET Core Data Protection](../../security/data-protection/configuration/overview?view=aspnetcore-10.0) for details.

* **Configure the IIS Application Pool to load the user profile**

  This setting is in the **Process Model** section under the **Advanced Settings** for the app pool. Set **Load User Profile** to `True`. When set to `True`, keys are stored in the user profile directory and protected using DPAPI with a key specific to the user account. Keys are persisted to the *%LOCALAPPDATA%/ASP.NET/DataProtection-Keys* folder.

  The app pool's [setProfileEnvironment attribute](/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel#configuration) must also be enabled. The default value of `setProfileEnvironment` is `true`. In some scenarios (for example, Windows OS), `setProfileEnvironment` is set to `false`. If keys aren't stored in the user profile directory as expected:

  1. Navigate to the *%windir%/system32/inetsrv/config* folder.
  2. Open the *applicationHost.config* file.
  3. Locate the `<system.applicationHost><applicationPools><applicationPoolDefaults><processModel>` element.
  4. Confirm that the `setProfileEnvironment` attribute isn't present, which defaults the value to `true`, or explicitly set the attribute's value to `true`.

* **Use the file system as a key ring store**

  Adjust the app code to [use the file system as a key ring store](../../security/data-protection/configuration/overview?view=aspnetcore-10.0). Use an X509 certificate to protect the key ring and ensure the certificate is a trusted certificate. If the certificate is self-signed, place the certificate in the Trusted Root store.

  When using IIS in a web farm:

  * Use a file share that all machines can access.
  * Deploy an X509 certificate to each machine. Configure [data protection in code](../../security/data-protection/configuration/overview?view=aspnetcore-10.0).

* **Set a machine-wide policy for data protection**

  The data protection system has limited support for setting a default [machine-wide policy](../../security/data-protection/configuration/machine-wide-policy?view=aspnetcore-10.0) for all apps that consume the Data Protection APIs. For more information, see [ASP.NET Core Data Protection Overview](../../security/data-protection/introduction?view=aspnetcore-10.0).

## Virtual Directories

[IIS Virtual Directories](/en-us/iis/get-started/planning-your-iis-architecture/understanding-sites-applications-and-virtual-directories-on-iis#virtual-directories) aren't supported with ASP.NET Core apps. An app can be hosted as a [sub-application](#sub-applications).

## Sub-applications

An ASP.NET Core app can be hosted as an [IIS sub-application (sub-app)](/en-us/iis/get-started/planning-your-iis-architecture/understanding-sites-applications-and-virtual-directories-on-iis#applications). The sub-app's path becomes part of the root app's URL.

A sub-app shouldn't include the ASP.NET Core Module as a handler. If the module is added as a handler in a sub-app's *web.config* file, a *500.19 Internal Server Error* referencing the faulty config file is received when attempting to browse the sub-app.

The following example shows a published *web.config* file for an ASP.NET Core sub-app:

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <aspNetCore processPath="dotnet" 
      arguments=".\MyApp.dll" 
      stdoutLogEnabled="false" 
      stdoutLogFile=".\logs\stdout" />
  </system.webServer>
</configuration>
```

When hosting a non-ASP.NET Core sub-app underneath an ASP.NET Core app, explicitly remove the inherited handler in the sub-app's *web.config* file:

```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <remove name="aspNetCore" />
    </handlers>
    <aspNetCore processPath="dotnet" 
      arguments=".\MyApp.dll" 
      stdoutLogEnabled="false" 
      stdoutLogFile=".\logs\stdout" />
  </system.webServer>
</configuration>
```

Static asset links within the sub-app should use tilde-slash (`~/`) notation. Tilde-slash notation triggers a [Tag Helper](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) to prepend the sub-app's pathbase to the rendered relative link. For a sub-app at `/subapp_path`, an image linked with `src="~/image.png"` is rendered as `src="/subapp_path/image.png"`. The root app's static file middleware doesn't process the static file request. The sub-app's static file middleware processes the request.

## Configuration of IIS with web.config

The `<system.webServer>` section of *web.config* for IIS scenarios that are functional for ASP.NET Core apps with the ASP.NET Core Module influences IIS configuration. For example, IIS configuration is functional for dynamic compression. If IIS is configured at the server level to use dynamic compression, the `<urlCompression>` element in the app's *web.config* file can disable it for an ASP.NET Core app.

For more information, see the following articles:

* [Configuration reference for \<system.webServer>](/en-us/iis/configuration/system.webServer/)
* [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0)
* [IIS modules with ASP.NET Core](modules?view=aspnetcore-10.0)

To set environment variables for individual apps running in isolated app pools (supported for IIS 10.0 or later), see the *AppCmd.exe command* section of the [Environment Variables \<environmentVariables>](/en-us/iis/configuration/system.applicationHost/applicationPools/add/environmentVariables/#appcmdexe) article in the IIS reference documentation.

### Sections that aren't used by ASP.NET Core

Configuration sections of ASP.NET 4.x apps in *web.config* aren't used by ASP.NET Core apps for configuration:

* `<system.web>`
* `<appSettings>`
* `<connectionStrings>`
* `<location>`

ASP.NET Core apps are configured using other configuration providers. For more information, see [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0).

## Application Pools

When hosting multiple websites on a server, we recommend isolating the apps from each other by running each app in its own app pool. The IIS **Add Website** dialog defaults to this configuration. When a **Site name** is provided, the text is automatically transferred to the **Application pool** textbox. A new app pool is created using the site name when the site is added.

## Application Pool Identity

An app pool identity account allows an app to run under a unique account without having to create and manage domains or local accounts. On IIS 8.0 or later, the IIS Admin Worker Process (WAS) creates a virtual account with the name of the new app pool and runs the app pool's worker processes under this account by default. In the IIS Management Console under **Advanced Settings** for the app pool, ensure that the **Identity** is set to use **ApplicationPoolIdentity**:

The IIS management process creates a secure identifier with the name of the app pool in the Windows Security System. Resources can be secured using this identity. However, this identity isn't a real user account and doesn't show up in the Windows User Management Console.

If the IIS worker process requires elevated access to the app, modify the Access Control List (ACL) for the directory containing the app:

1. Open Windows Explorer and navigate to the directory.

2. Right-click on the directory and select **Properties**.

3. Under the **Security** tab, select the **Edit** button and then the **Add** button.

4. Select the **Locations** button and make sure the system is selected.

5. Enter **IIS AppPool\\\<app\_pool\_name>** in **Enter the object names to select** area. Select the **Check Names** button. For the *DefaultAppPool* check the names using **IIS AppPool\DefaultAppPool**. When the **Check Names** button is selected, a value of **DefaultAppPool** is indicated in the object names area. It isn't possible to enter the app pool name directly into the object names area. Use the **IIS AppPool\\\<app\_pool\_name>** format when checking for the object name.

6. Select **OK**.

7. Read & execute permissions should be granted by default. Provide additional permissions as needed.

Access can also be granted at a command prompt using the **ICACLS** tool. Using the *DefaultAppPool* as an example, the following command is used:

```
ICACLS C:\sites\MyWebApp /grant "IIS AppPool\DefaultAppPool":F
```

For more information, see the [icacls](/en-us/windows-server/administration/windows-commands/icacls) article.

## HTTP/2 support

[HTTP/2](https://httpwg.org/specs/rfc7540.html) is supported for out-of-process deployments that meet the following base requirements:

* Windows Server 2016/Windows 10 or later; IIS 10 or later
* Public-facing edge server connections use HTTP/2, but the reverse proxy connection to the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0) uses HTTP/1.1.
* Target framework: Not applicable to out-of-process deployments, since IIS handles the HTTP/2 connection entirely.
* TLS 1.2 or later connection

If an HTTP/2 connection is established, [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) reports `HTTP/1.1`.

HTTP/2 is enabled by default. Connections fall back to HTTP/1.1 if an HTTP/2 connection isn't established. For more information on HTTP/2 configuration with IIS deployments, see [HTTP/2 on IIS](/en-us/iis/get-started/whats-new-in-iis-10/http2-on-iis).

## CORS preflight requests

*This section only applies to ASP.NET Core apps that target the .NET Framework.*

For an ASP.NET Core app that targets the .NET Framework, OPTIONS requests aren't passed to the app by default in IIS. To learn how to configure the app's IIS handlers in *web.config* to pass OPTIONS requests, see [Enable cross-origin requests in ASP.NET Web API 2: How CORS Works](/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api#how-cors-works).

## Deployment resources for IIS administrators

* [IIS documentation](/en-us/iis)
* [Getting Started with the IIS Manager in IIS](/en-us/iis/get-started/getting-started-with-iis/getting-started-with-the-iis-manager-in-iis-7-and-iis-8)
* [.NET application deployment](/en-us/dotnet/core/deploying/)
* [ASP.NET Core Module (ANCM) for IIS](../aspnet-core-module?view=aspnetcore-10.0)
* [ASP.NET Core directory structure](../directory-structure?view=aspnetcore-10.0)
* [IIS modules with ASP.NET Core](modules?view=aspnetcore-10.0)
* [Troubleshoot ASP.NET Core on Azure App Service and IIS](../../test/troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Common error troubleshooting for Azure App Service and IIS with ASP.NET Core](../azure-iis-errors-reference?view=aspnetcore-10.0)

## Additional resources

* [Publish an ASP.NET Core app to IIS](../../tutorials/publish-to-iis?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../../test/troubleshoot?view=aspnetcore-10.0)
* [Overview of ASP.NET Core](../../overview?view=aspnetcore-10.0)
* [The Official Microsoft IIS Site](https://www.iis.net/)
* [Windows Server technical content library](/en-us/windows-server)
* [HTTP/2 on IIS](/en-us/iis/get-started/whats-new-in-iis-10/http2-on-iis)
* [Transform web.config](transform-webconfig?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-web-api
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-web-api.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Create a controller-based web API with ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Tim Deschryver](https://timdeschryver.dev/) and [Rick Anderson](https://twitter.com/RickAndMSFT)

This tutorial teaches the basics of building a controller-based web API that uses a database. Another approach to creating APIs in ASP.NET Core is to create *Minimal APIs*. For help with choosing between Minimal APIs and controller-based APIs, see [APIs overview](../fundamentals/apis?view=aspnetcore-10.0). For a tutorial on creating a Minimal API, see [Tutorial: Create a Minimal API with ASP.NET Core](min-web-api?view=aspnetcore-10.0).

## Overview

This tutorial creates the following API:

| API                              | Description               | Request body | Response body        |
| -------------------------------- | ------------------------- | ------------ | -------------------- |
| `GET /api/todoitems`             | Get all to-do items       | None         | Array of to-do items |
| `GET /api/todoitems/{id}`        | Get an item by ID         | None         | To-do item           |
| `POST /api/todoitems`            | Add a new item            | To-do item   | To-do item           |
| `PUT /api/todoitems/{id}`        | Update an existing item   | To-do item   | None                 |
| `DELETE /api/todoitems/{id}`     | Delete an item            | None         | None                 |

The following diagram shows the design of the app.

## Create a Web API project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- From the **File** menu, select **New** > **Project**.

- Enter *Web API* in the search box.

- Select the **ASP.NET Core Web API** template and select **Next**.

- In the **Configure your new project dialog**, name the project *TodoApi* and select **Next**.

- In the **Additional information** dialog:

  * Confirm the **Framework** is **.NET 9.0 (Standard Term Support)**.
  * Confirm the checkbox for **Enable OpenAPI support** is checked.
  * Confirm the checkbox for **Use controllers (uncheck to use Minimal APIs)** is checked.
  * Select **Create**.

## Add a NuGet package

A NuGet package must be added to support the database used in this tutorial.

* From the **Tools** menu, select **NuGet Package Manager > Manage NuGet Packages for Solution**.
* Select the **Browse** tab.
* Enter **Microsoft.EntityFrameworkCore.InMemory** in the search box, and then select `Microsoft.EntityFrameworkCore.InMemory`.
* Select the **Project** checkbox in the right pane and then select **Install**.

- Open the [integrated terminal](https://code.visualstudio.com/docs/terminal/basics).

- Change directories (`cd`) to the folder that will contain the project folder.

- Run the following commands:

  ```
  dotnet new webapi --use-controllers -o TodoApi
  cd TodoApi
  dotnet add package Microsoft.EntityFrameworkCore.InMemory
  code -r .
  ```

  These commands:

  * Create a new web API project and open it in Visual Studio Code.
  * Add a NuGet package that is needed for the next section.
  * Open the *TodoApi* folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

### Run the project

The project template creates a `WeatherForecast` API with support for [OpenAPI](../fundamentals/openapi/overview?view=aspnetcore-10.0).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog when a project is not yet configured to use SSL:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio launches a terminal window and displays the URL of the running app. The API is hosted at `https://localhost:<port>`, where `<port>` is a randomly chosen port number set at the project creation.

```
...
info: Microsoft.Hosting.Lifetime[14]
   Now listening on: https://localhost:7260
info: Microsoft.Hosting.Lifetime[14]
   Now listening on: http://localhost:7261
info: Microsoft.Hosting.Lifetime[0]
   Application started. Press Ctrl+C to shut down.
...
```

`Ctrl`+*click* the HTTPS URL in the output to test the web app in a browser. There's no endpoint at `https://localhost:<port>`, so the browser returns [HTTP 404 Not Found](https://developer.mozilla.org/docs/Web/HTTP/Status/404).

Append `/weatherforecast` to the URL to test the WeatherForecast API. The browser displays JSON similar to the following example:

```
[
    {
        "date": "2025-07-16",
        "temperatureC": 52,
        "temperatureF": 125,
        "summary": "Mild"
    },
    {
        "date": "2025-07-17",
        "temperatureC": 36,
        "temperatureF": 96,
        "summary": "Warm"
    },
    {
        "date": "2025-07-18",
        "temperatureC": 39,
        "temperatureF": 102,
        "summary": "Cool"
    },
    {
        "date": "2025-07-19",
        "temperatureC": 10,
        "temperatureF": 49,
        "summary": "Bracing"
    },
    {
        "date": "2025-07-20",
        "temperatureC": -1,
        "temperatureF": 31,
        "summary": "Chilly"
    }
]
```

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Run the app:

* Run the following command to start the app on the `https` profile:

  ```
  dotnet run --launch-profile https
  ```

The output shows messages similar to the following, indicating that the app is running and awaiting requests:

```
...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:{port}
...
```

* `Ctrl`+*click* the HTTPS URL in the output to test the web app in a browser.

* The default browser is launched to `https://localhost:<port>`, where `<port>` is the randomly chosen port number displayed in the output. There's no endpoint at `https://localhost:<port>`, so the browser returns [HTTP 404 Not Found](https://developer.mozilla.org/docs/Web/HTTP/Status/404).

* Append `/weatherforecast` to the URL to test the WeatherForecast API. The browser displays JSON similar to the following example:

```
[
    {
        "date": "2025-07-16",
        "temperatureC": 52,
        "temperatureF": 125,
        "summary": "Mild"
    },
    {
        "date": "2025-07-17",
        "temperatureC": 36,
        "temperatureF": 96,
        "summary": "Warm"
    },
    {
        "date": "2025-07-18",
        "temperatureC": 39,
        "temperatureF": 102,
        "summary": "Cool"
    },
    {
        "date": "2025-07-19",
        "temperatureC": 10,
        "temperatureF": 49,
        "summary": "Bracing"
    },
    {
        "date": "2025-07-20",
        "temperatureC": -1,
        "temperatureF": 31,
        "summary": "Chilly"
    }
]
```

* After testing the web app using the following instruction, press `Ctrl`+`C` in the integrated terminal to close it.

### Test the project

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

This tutorial uses [Endpoints Explorer and .http files](../test/http-files?view=aspnetcore-10.0#use-endpoints-explorer) to test the API.

## Create API testing UI with Swagger

There are many available web API testing tools to choose from, and you can follow this tutorial's introductory API test steps with your preferred tool.

  ```
  dotnet add package NSwag.AspNetCore
  ```

The previous command adds the [NSwag.AspNetCore](https://www.nuget.org/packages/NSwag.AspNetCore/) package, which contains tools to generate Swagger documents and UI. Because our project is using OpenAPI, we only use the NSwag package to generate the Swagger UI.

### Configure Swagger middleware

* In `Program.cs`, add the following highlighted code:

```
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.UseSwaggerUi(options =>
    {
        options.DocumentPath = "/openapi/v1.json";
    });
}
```

The previous code enables the Swagger middleware for serving the generated JSON document using the Swagger UI. Swagger is only enabled in a development environment. Enabling Swagger in a production environment could expose potentially sensitive details about the API's structure and implementation.

The app uses the OpenAPI document generated by OpenApi, located at `/openapi/v1.json`, to generate the UI. View the generated OpenAPI specification for the `WeatherForecast` API while the project is running by navigating to `https://localhost:<port>/openapi/v1.json` in your browser.

The OpenAPI specification is a document in JSON format that describes the structure and capabilities of your API, including endpoints, request/response formats, parameters, and more. It's essentially a blueprint of your API that can be used by various tools to understand and interact with your API.

## Add a model class

A *model* is a set of classes that represent the data that the app manages. The model for this app is the `TodoItem` class.

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

- In **Solution Explorer**, right-click the project. Select **Add** > **New Folder**. Name the folder `Models`.
- Right-click the `Models` folder and select **Add** > **Class**. Name the class *TodoItem* and select **Add**.
- Replace the template code with the following:

```
namespace TodoApi.Models;

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

* Add a folder named `Models`.
* Add a `TodoItem.cs` file to the `Models` folder with the following code:

```
namespace TodoApi.Models;

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

The `Id` property functions as the unique key in a relational database.

Model classes can go anywhere in the project, but the `Models` folder is used by convention.

## Add a database context

The *database context* is the main class that coordinates Entity Framework functionality for a data model. This class is created by deriving from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

- Right-click the `Models` folder and select **Add** > **Class**. Name the class *TodoContext* and click **Add**.

- Enter the following code:

  ```
  using Microsoft.EntityFrameworkCore;

  namespace TodoApi.Models;

  public class TodoContext : DbContext
  {
      public TodoContext(DbContextOptions<TodoContext> options)
          : base(options)
      {
      }

      public DbSet<TodoItem> TodoItems { get; set; } = null!;
  }
  ```

* Add a `TodoContext.cs` file to the `Models` folder.

* Enter the following code:

  ```
  using Microsoft.EntityFrameworkCore;

  namespace TodoApi.Models;

  public class TodoContext : DbContext
  {
      public TodoContext(DbContextOptions<TodoContext> options)
          : base(options)
      {
      }

      public DbSet<TodoItem> TodoItems { get; set; } = null!;
  }
  ```

## Register the database context

In ASP.NET Core, services such as the DB context must be registered with the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container. The container provides the service to controllers.

Update `Program.cs` with the following highlighted code:

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));

// <snippet_UseSwagger>
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.UseSwaggerUi(options =>
    {
        options.DocumentPath = "/openapi/v1.json";
    });
}
// </snippet_UseSwagger>

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The preceding code:

* Adds `using` directives.
* Adds the database context to the DI container.
* Specifies that the database context will use an in-memory database.

## Scaffold a controller

* [Visual Studio](#tabpanel_8_visual-studio)
* [Visual Studio Code](#tabpanel_8_visual-studio-code)

- Right-click the `Controllers` folder.

- Select **Add** > **New Scaffolded Item**.

- Select **API Controller with actions, using Entity Framework**, and then select **Add**.

- In the **Add API Controller with actions, using Entity Framework** dialog:

  * Select **TodoItem (TodoApi.Models)** in the **Model class**.
  * Select **TodoContext (TodoApi.Models)** in the **Data context class**.
  * Select **Add**.

  If the scaffolding operation fails, select **Add** to try scaffolding a second time.

This step adds the `Microsoft.VisualStudio.Web.CodeGeneration.Design` and `Microsoft.EntityFrameworkCore.Tools` NuGet packages to the project. These packages are required for scaffolding.

Make sure that all of your changes so far are saved.

* Right-click (or Command-click on macOS) the **TodoAPI** project and select **Open in Terminal**. The terminal opens at the `TodoAPI` project folder. Run the following commands:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet tool uninstall -g dotnet-aspnet-codegenerator
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet tool update -g dotnet-aspnet-codegenerator
```

The preceding commands:

* Add NuGet packages required for scaffolding.
* Install the scaffolding engine (`dotnet-aspnet-codegenerator`) after uninstalling any possible previous version.

For Linux, add the .NET tools directory to the system path with the following command:

```
echo 'export PATH=$HOME/.dotnet/tools:$PATH' >> ~/.bashrc
source ~/.bashrc
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Build the project.

Run the following command:

```
dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
```

The preceding command scaffolds the `TodoItemsController`.

The generated code:

* Marks the class with the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute. This attribute indicates that the controller responds to web API requests. For information about specific behaviors that the attribute enables, see [Create web APIs with ASP.NET Core](../web-api/?view=aspnetcore-10.0).
* Uses DI to inject the database context (`TodoContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

The ASP.NET Core templates for:

* Controllers with views include `[action]` in the route template.
* API controllers don't include `[action]` in the route template.

When the `[action]` token isn't in the route template, the [action](../mvc/controllers/routing?view=aspnetcore-10.0#action) name (method name) isn't included in the endpoint. That is, the action's associated method name isn't used in the matching route.

Open the generated `Controllers/TodoItemsController.cs` file to review the scaffolded code. Scaffolding generates a complete, working controller with action methods that create, read, update, and delete `TodoItem` data:

* `GetTodoItems`: Get all to-do items.
* `GetTodoItem`: Get a to-do item by ID.
* `PostTodoItem`: Create a to-do item.
* `PutTodoItem`: Update an existing to-do item.
* `DeleteTodoItem`: Delete a to-do item.

The following sections examine these generated methods, starting with `PostTodoItem`.

## Examine the `PostTodoItem` create method

Examine the scaffolded `PostTodoItem` method. The generated code already works. The return statement uses the [nameof](/en-us/dotnet/csharp/language-reference/operators/nameof) operator so the action name isn't hard-coded as a string. The commented-out line shows the equivalent hard-coded form that `nameof` replaces:

```
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
    _context.TodoItems.Add(todoItem);
    await _context.SaveChangesAsync();

    //    return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
    return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
}
```

The preceding code is an `HTTP POST` method, as indicated by the [`[HttpPost]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute) attribute. The method gets the value of the `TodoItem` from the body of the HTTP request.

For more information, see [Attribute routing with Http\[Verb\] attributes](../mvc/controllers/routing?view=aspnetcore-10.0#verb).

The [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) method:

* Returns an [HTTP 201 status code](https://developer.mozilla.org/docs/Web/HTTP/Status/201) if successful. `HTTP 201` is the standard response for an `HTTP POST` method that creates a new resource on the server.
* Adds a [Location](https://developer.mozilla.org/docs/Web/HTTP/Headers/Location) header to the response. The `Location` header specifies the [URI](https://developer.mozilla.org/docs/Glossary/URI) of the newly created to-do item. For more information, see [10.2.2 201 Created](https://www.rfc-editor.org/rfc/rfc9110.html#section-10.2.2).
* References the `GetTodoItem` action to create the `Location` header's URI.

[]()

### Test PostTodoItem

* [Visual Studio](#tabpanel_9_visual-studio)
* [Visual Studio Code](#tabpanel_9_visual-studio-code)

- Select **View** > **Other Windows** > **Endpoints Explorer**.

- Right-click the **POST** endpoint and select **Generate request**.

  A new file is created in the project folder named `TodoApi.http`, with contents similar to the following example:

  ```
  @TodoApi_HostAddress = https://localhost:49738

  POST {{TodoApi_HostAddress}}/api/todoitems
  Content-Type: application/json

  {
    //TodoItem
  }

  ###
  ```

  * The first line creates a variable that is used for all of the endpoints.
  * The next line defines a POST request.
  * The lines after the POST request line defines the headers, and a placeholder for the request body.
  * The triple hashtag (`###`) line is a request delimiter: what comes after it is for a different request.

- The POST request expects a `TodoItem`. To define the todo, replace the `//TodoItem` comment with the following JSON:

  ```
  {
    "name": "walk dog",
    "isComplete": true
  }
  ```

  The TodoApi.http file should now look like the following example, but with your port number:

  ```
  @TodoApi_HostAddress = https://localhost:7260

  Post {{TodoApi_HostAddress}}/api/todoitems
  Content-Type: application/json

  {
    "name": "walk dog",
    "isComplete": true
  }

  ###
  ```

- Run the app.

- Select the **Send request** link that is above the `POST` request line.

  The POST request is sent to the app and the response is displayed in the **Response** pane.

* With the app still running, in the browser, navigate to `https://localhost:<port>/swagger` to display the API testing page generated by Swagger. Click on **TodoItems** to expand the operations.

* On the Swagger API testing page, select **Post /api/todoitems** > **Try it out**.

* Note that the **Request body** field contains a generated example format reflecting the parameters for the API.

* In the request body enter JSON for a to-do item, without specifying the optional `id`:

  ```
  {
    "name": "walk dog",
    "isComplete": true
  }
  ```

* Select **Execute**.

* Swagger provides a **Responses** pane below the **Execute** button.

Note a few of the useful details:

* cURL: Swagger provides an example cURL command in Unix/Linux syntax, which can be run at the command line with any bash shell that uses Unix/Linux syntax, including Git Bash from [Git for Windows](https://git-scm.com/downloads).
* Request URL: A simplified representation of the HTTP request made by Swagger UI's JavaScript code for the API call. Actual requests can include details such as headers and query parameters and a request body.
* Server response: Includes the response body and headers. The response body shows the `id` was set to `1`.
* Response Code: A 201 `HTTP` status code was returned, indicating that the request was successfully processed and resulted in the creation of a new resource.

### Test the location header URI

* [Visual Studio](#tabpanel_10_visual-studio)
* [Visual Studio Code](#tabpanel_10_visual-studio-code)

Test the app by calling the `GET` endpoints from a browser or by using **Endpoints Explorer**. The following steps are for **Endpoints Explorer**.

* In **Endpoints Explorer**, right-click the first **GET** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

  ```
  GET {{TodoApi_HostAddress}}/api/todoitems

  ###
  ```

* Select the **Send request** link that is above the new `GET` request line.

  The GET request is sent to the app and the response is displayed in the **Response** pane.

* The response body is similar to the following JSON:

  ```
  [
    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
  ]
  ```

* In **Endpoints Explorer**, right-click the `/api/todoitems/{id}` **GET** endpoint and select **Generate request**. The following content is added to the `TodoApi.http` file:

  ```
  @id=0
  GET {{TodoApi_HostAddress}}/api/todoitems/{{id}}

  ###
  ```

* Assign `{@id}` to `1` (instead of `0`).

* Select the **Send request** link that is above the new GET request line.

  The GET request is sent to the app and the response is displayed in the **Response** pane.

* The response body is similar to the following JSON:

  ```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
  ```

Test the app by calling the endpoints from a browser or Swagger.

* In Swagger select **GET /api/todoitems** > **Try it out** > **Execute**.

* Alternatively, call **GET /api/todoitems** from a browser by entering the URI `https://localhost:<port>/api/todoitems`. For example, `https://localhost:7260/api/todoitems`

The call to `GET /api/todoitems` produces a response similar to the following:

```
[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
```

* Call **GET /api/todoitems/{id}** in Swagger to return data from a specific id:

  * Select **GET /api/todoitems** > **Try it out**.
  * Set the **id** field to `1` and select **Execute**.

* Alternatively, call **GET /api/todoitems** from a browser by entering the URI `https://localhost:<port>/api/todoitems/1`. For example, `https://localhost:7260/api/todoitems/1`

* The response is similar to the following:

  ```
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
  ```

## Examine the GET methods

Two GET endpoints are implemented:

* `GET /api/todoitems`
* `GET /api/todoitems/{id}`

The previous section showed an example of the `/api/todoitems/{id}` route.

Follow the [POST](#post7) instructions to add another todo item, and then test the `/api/todoitems` route using Swagger.

This app uses an in-memory database. If the app is stopped and started, the preceding GET request doesn't return any data. If no data is returned, [POST](#post7) data to the app.

## Routing and URL paths

The [`[HttpGet]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute) attribute denotes a method that responds to an `HTTP GET` request. The URL path for each method is constructed as follows:

* Start with the template string in the controller's `Route` attribute:

  ```
  [Route("api/[controller]")]
  [ApiController]
  public class TodoItemsController : ControllerBase
  ```

* Replace `[controller]` with the name of the controller, which by convention is the controller class name minus the "Controller" suffix. For this sample, the controller class name is **TodoItems**Controller, so the controller name is "TodoItems". ASP.NET Core [routing](../mvc/controllers/routing?view=aspnetcore-10.0) is case insensitive.

* If the `[HttpGet]` attribute has a route template (for example, `[HttpGet("products")]`), append that to the path. This sample doesn't use a template. For more information, see [Attribute routing with Http\[Verb\] attributes](../mvc/controllers/routing?view=aspnetcore-10.0#verb).

In the following `GetTodoItem` method, `"{id}"` is a placeholder variable for the unique identifier of the to-do item. When `GetTodoItem` is invoked, the value of `"{id}"` in the URL is provided to the method in its `id` parameter.

```
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}
```

## Return values

The return type of the `GetTodoItems` and `GetTodoItem` methods is [ActionResult\<T> type](../web-api/action-return-types?view=aspnetcore-10.0#actionresultt-type). ASP.NET Core automatically serializes the object to [JSON](https://www.json.org/) and writes the JSON into the body of the response message. The response code for this return type is [200 OK](https://developer.mozilla.org/docs/Web/HTTP/Status/200), assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.

`ActionResult` return types can represent a wide range of HTTP status codes. For example, `GetTodoItem` can return two different status values:

* If no item matches the requested ID, the method returns a [404 status](https://developer.mozilla.org/docs/Web/HTTP/Status/404) [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) error code.
* Otherwise, the method returns 200 with a JSON response body. Returning `item` results in an `HTTP 200` response.

## The PutTodoItem method

Examine the `PutTodoItem` method:

```
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id)
    {
        return BadRequest();
    }

    _context.Entry(todoItem).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!TodoItemExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return NoContent();
}
```

`PutTodoItem` is similar to `PostTodoItem`, except it uses `HTTP PUT`. The response is [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a `PUT` request requires the client to send the entire updated entity, not just the changes. To support partial updates, use [HTTP PATCH](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute).

### Test the PutTodoItem method

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.

Use the `PUT` method to update the `TodoItem` that has Id = 1 and set its name to `"feed fish"`. Note the response is [`HTTP 204 No Content`](https://developer.mozilla.org/docs/Web/HTTP/Status/204).

* [Visual Studio](#tabpanel_11_visual-studio)
* [Visual Studio Code](#tabpanel_11_visual-studio-code)

- In **Endpoints Explorer**, right-click the **PUT** endpoint, and select **Generate request**.

  The following content is added to the `TodoApi.http` file:

  ```
  PUT {{TodoApi_HostAddress}}/api/todoitems/{{id}}
  Content-Type: application/json

  {
    //TodoItem
  }

  ###
  ```

- In the PUT request line, replace `{{id}}` with `1`.

- Replace the `//TodoItem` placeholder with the following lines:

  ```
  PUT {{TodoApi_HostAddress}}/api/todoitems/1
  Content-Type: application/json

  {
    "id": 1,
    "name": "feed fish",
    "isComplete": false
  }
  ```

- Select the **Send request** link that is above the new PUT request line.

  The PUT request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

Use Swagger to send a PUT request:

* Select **Put /api/todoitems/{id}** > **Try it out**.

* Set the **id** field to `1`.

* Set the request body to the following JSON:

  ```
  {
    "id": 1,
    "name": "feed fish",
    "isComplete": false
  }
  ```

* Select **Execute**.

## The DeleteTodoItem method

Examine the `DeleteTodoItem` method:

```
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);
    if (todoItem == null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(todoItem);
    await _context.SaveChangesAsync();

    return NoContent();
}
```

### Test the DeleteTodoItem method

Use the `DELETE` method to delete the `TodoItem` that has Id = 1. Note the response is [`HTTP 204 No Content`](https://developer.mozilla.org/docs/Web/HTTP/Status/204).

* [Visual Studio](#tabpanel_12_visual-studio)
* [Visual Studio Code](#tabpanel_12_visual-studio-code)

- In **Endpoints Explorer**, right-click the **DELETE** endpoint and select **Generate request**.

  A DELETE request is added to `TodoApi.http`.

- Replace `{{id}}` in the DELETE request line with `1`. The DELETE request should look like the following example:

  ```
  DELETE {{TodoApi_HostAddress}}/api/todoitems/{{id}}

  ###
  ```

- Select the **Send request** link for the DELETE request.

  The DELETE request is sent to the app and the response is displayed in the **Response** pane. The response body is empty, and the status code is 204.

Use Swagger to send a DELETE request:

* Select **DELETE /api/todoitems/{id}** > **Try it out**.

* Set the **ID** field to `1` and select **Execute**.

  The DELETE request is sent to the app and the response is displayed in the **Responses** pane. The response body is empty, and the **Server response** status code is 204.

## Test with other tools

There are many other tools that can be used to test web APIs, for example:

* [http-repl](../web-api/http-repl/?view=aspnetcore-10.0)
* [curl](https://terminalcheatsheet.com/guides/curl-rest-api). Swagger uses `curl` and shows the `curl` commands it submits.
* [Fiddler](https://www.telerik.com/fiddler)

[]()

## Prevent over-posting

Currently the sample app exposes the entire `TodoItem` object. Production apps typically limit the data that's input and returned using a subset of the model. There are multiple reasons behind this, and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. **DTO** is used in this tutorial.

A DTO may be used to:

* Prevent over-posting.
* Hide properties that clients are not supposed to view.
* Omit some properties in order to reduce payload size.
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.

To demonstrate the DTO approach, update the `TodoItem` class to include a secret field:

```
namespace TodoApi.Models;

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}
```

The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

Verify you can post and get the secret field.

Create a DTO model in a **Models/TodoItemDTO.cs** file:

```
namespace TodoApi.Models;

public class TodoItemDTO
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

Update the `TodoItemsController` to use `TodoItemDTO`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Controllers;

[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{
    private readonly TodoContext _context;

    public TodoItemsController(TodoContext context)
    {
        _context = context;
    }

    // GET: api/TodoItems
    [HttpGet]
    public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
    {
        return await _context.TodoItems
            .Select(x => ItemToDTO(x))
            .ToListAsync();
    }

    // GET: api/TodoItems/5
    // <snippet_GetByID>
    [HttpGet("{id}")]
    public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
    {
        var todoItem = await _context.TodoItems.FindAsync(id);

        if (todoItem == null)
        {
            return NotFound();
        }

        return ItemToDTO(todoItem);
    }
    // </snippet_GetByID>

    // PUT: api/TodoItems/5
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    // <snippet_Update>
    [HttpPut("{id}")]
    public async Task<IActionResult> PutTodoItem(long id, TodoItemDTO todoDTO)
    {
        if (id != todoDTO.Id)
        {
            return BadRequest();
        }

        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
        {
            return NotFound();
        }

        todoItem.Name = todoDTO.Name;
        todoItem.IsComplete = todoDTO.IsComplete;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
        {
            return NotFound();
        }

        return NoContent();
    }
    // </snippet_Update>

    // POST: api/TodoItems
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    // <snippet_Create>
    [HttpPost]
    public async Task<ActionResult<TodoItemDTO>> PostTodoItem(TodoItemDTO todoDTO)
    {
        var todoItem = new TodoItem
        {
            IsComplete = todoDTO.IsComplete,
            Name = todoDTO.Name
        };

        _context.TodoItems.Add(todoItem);
        await _context.SaveChangesAsync();

        return CreatedAtAction(
            nameof(GetTodoItem),
            new { id = todoItem.Id },
            ItemToDTO(todoItem));
    }
    // </snippet_Create>

    // DELETE: api/TodoItems/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteTodoItem(long id)
    {
        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
        {
            return NotFound();
        }

        _context.TodoItems.Remove(todoItem);
        await _context.SaveChangesAsync();

        return NoContent();
    }

    private bool TodoItemExists(long id)
    {
        return _context.TodoItems.Any(e => e.Id == id);
    }

    private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
       new TodoItemDTO
       {
           Id = todoItem.Id,
           Name = todoItem.Name,
           IsComplete = todoItem.IsComplete
       };
}
```

Verify you can't post or get the secret field.

## Call the web API with JavaScript

See [Tutorial: Call an ASP.NET Core web API with JavaScript](web-api-javascript?view=aspnetcore-10.0).

## Web API video series

See [Video: Beginner's Series to: Web APIs](/en-us/shows/beginners-series-to-web-apis/).

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

[]()

## Add authentication support to a web API

[Duende Software](https://duendesoftware.com/) might require you to pay a license fee for production use of Duende Identity Server. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../migration/50-to-60?view=aspnetcore-10.0#project-templates-use-duende-identity-server).

For more information, see the [Duende Identity Server documentation (Duende Software website)](https://docs.duendesoftware.com).

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore).

## Related content

[View or download sample code for this tutorial](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-web-api/samples). See [how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

For more information, see the following resources:

* [Create web APIs with ASP.NET Core](../web-api/?view=aspnetcore-10.0)
* [Tutorial: Create a Minimal API with ASP.NET Core](min-web-api?view=aspnetcore-10.0)
* [Use the generated OpenAPI documents](../fundamentals/openapi/using-openapi-documents?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](web-api-help-pages-using-swagger?view=aspnetcore-10.0)
* [Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8](../data/ef-rp/intro?view=aspnetcore-10.0)
* [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0)
* [Controller action return types in ASP.NET Core web API](../web-api/action-return-types?view=aspnetcore-10.0)
* [Deploy ASP.NET Core apps to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0)
* [Host and deploy ASP.NET Core](../host-and-deploy/?view=aspnetcore-10.0)
* [Create a web API with ASP.NET Core](/en-us/training/modules/build-web-api-aspnet-core/)

This tutorial teaches the basics of building a controller-based web API that uses a database. Another approach to creating APIs in ASP.NET Core is to create *Minimal APIs*. For help with choosing between Minimal APIs and controller-based APIs, see [APIs overview](../fundamentals/apis?view=aspnetcore-10.0). For a tutorial on creating a Minimal API, see [Tutorial: Create a Minimal API with ASP.NET Core](min-web-api?view=aspnetcore-10.0).

## Overview

This tutorial creates the following API:

| API                              | Description               | Request body | Response body        |
| -------------------------------- | ------------------------- | ------------ | -------------------- |
| `GET /api/todoitems`             | Get all to-do items       | None         | Array of to-do items |
| `GET /api/todoitems/{id}`        | Get an item by ID         | None         | To-do item           |
| `POST /api/todoitems`            | Add a new item            | To-do item   | To-do item           |
| `PUT /api/todoitems/{id}`        | Update an existing item   | To-do item   | None                 |
| `DELETE /api/todoitems/{id}`     | Delete an item            | None         | None                 |

The following diagram shows the design of the app.

## Create a web project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- From the **File** menu, select **New** > **Project**.

- Enter *Web API* in the search box.

- Select the **ASP.NET Core Web API** template and select **Next**.

- In the **Configure your new project dialog**, name the project *TodoApi* and select **Next**.

- In the **Additional information** dialog:

  * Confirm the **Framework** is **.NET 8.0 (Long Term Support)**.
  * Confirm the checkbox for **Use controllers(uncheck to use Minimal APIs)** is checked.
  * Confirm the checkbox for **Enable OpenAPI support** is checked.
  * Select **Create**.

## Add a NuGet package

A NuGet package must be added to support the database used in this tutorial.

* From the **Tools** menu, select **NuGet Package Manager > Manage NuGet Packages for Solution**.
* Select the **Browse** tab.
* Enter **Microsoft.EntityFrameworkCore.InMemory** in the search box, and then select `Microsoft.EntityFrameworkCore.InMemory`.
* Select the **Project** checkbox in the right pane and then select **Install**.

- Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

- Change directories (`cd`) to the folder that will contain the project folder.

- Run the following commands:

  ```
  dotnet new webapi --use-controllers -o TodoApi
  cd TodoApi
  dotnet add package Microsoft.EntityFrameworkCore.InMemory
  code -r ../TodoApi
  ```

  These commands:

  * Create a new web API project and open it in Visual Studio Code.
  * Add a NuGet package that is needed for the next section.
  * Open the *TodoApi* folder in the current instance of Visual Studio Code.

  * Select **Web and Console** > **App** > **API**.
  * Select **Continue**.

* In the **Configure your new API** dialog, make the following selections:

  * Confirm the **Target framework** is **.NET 8.0**.
  * Confirm the checkbox for **Enable OpenAPI support** is checked.
  * Confirm the checkbox for **Use controllers** is checked.
  * Select **Create**.

* Enter the following:

  * **Project name:** TodoApi
  * **Solution name:** TodoApi
  * Select **Create**.

## Add a NuGet package

* In the Visual Studio for Mac 2022 toolbar, select **Project** > **Manage NuGet Packages...**.
* In the search box, enter **Microsoft.EntityFrameworkCore.InMemory**.
* In the results window, check `Microsoft.EntityFrameworkCore.InMemory`.
* Select **Add Package**
* In the **Select Projects** window, select **Ok**.
* In the **License Agreement** window, select **Agree**.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

### Test the project

The project template creates a `WeatherForecast` API with support for [Swagger](web-api-help-pages-using-swagger?view=aspnetcore-10.0).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog when a project is not yet configured to use SSL:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio launches the default browser and navigates to `https://localhost:<port>/swagger/index.html`, where `<port>` is a randomly chosen port number set at the project creation.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Run the app:

* Run the following command to start the app on the `https` profile:

  ```
  dotnet run --launch-profile https
  ```

The output shows messages similar to the following, indicating that the app is running and awaiting requests:

```
...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:{port}
...
```

* `Ctrl`+*click* the HTTPS URL in the output to test the web app in a browser.

* The default browser is launched to `https://localhost:<port>/swagger/index.html`, where `<port>` is the randomly chosen port number displayed in the output. There's no endpoint at `https://localhost:<port>`, so the browser returns [HTTP 404 Not Found](https://developer.mozilla.org/docs/Web/HTTP/Status/404). Append `/swagger` to the URL, `https://localhost:<port>/swagger`.

After testing the web app in the following instruction, press `Ctrl`+`C` in the integrated terminal to shut it down.

Select **Debug** > **Start Debugging** to launch the app. Visual Studio for Mac launches a browser and navigates to `https://localhost:<port>/swagger/index.html`, where `<port>` is a randomly chosen port number set at the project creation.

The Swagger page `/swagger/index.html` is displayed. Select **GET** > **Try it out** > **Execute**. The page displays:

* The [Curl](https://curl.haxx.se/) command to test the WeatherForecast API.
* The URL to test the WeatherForecast API.
* The response code, body, and headers.
* A drop-down list box with media types and the example value and schema.

If the Swagger page doesn't appear, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/21647).

Swagger is used to generate useful documentation and help pages for web APIs. This tutorial uses Swagger to test the app. For more information on Swagger, see [ASP.NET Core web API documentation with Swagger / OpenAPI](web-api-help-pages-using-swagger?view=aspnetcore-10.0).

Copy and paste the **Request URL** in the browser: `https://localhost:<port>/weatherforecast`

JSON similar to the following example is returned:

```
[
    {
        "date": "2019-07-16T19:04:05.7257911-06:00",
        "temperatureC": 52,
        "temperatureF": 125,
        "summary": "Mild"
    },
    {
        "date": "2019-07-17T19:04:05.7258461-06:00",
        "temperatureC": 36,
        "temperatureF": 96,
        "summary": "Warm"
    },
    {
        "date": "2019-07-18T19:04:05.7258467-06:00",
        "temperatureC": 39,
        "temperatureF": 102,
        "summary": "Cool"
    },
    {
        "date": "2019-07-19T19:04:05.7258471-06:00",
        "temperatureC": 10,
        "temperatureF": 49,
        "summary": "Bracing"
    },
    {
        "date": "2019-07-20T19:04:05.7258474-06:00",
        "temperatureC": -1,
        "temperatureF": 31,
        "summary": "Chilly"
    }
]
```

## Add a model class

A *model* is a set of classes that represent the data that the app manages. The model for this app is the `TodoItem` class.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)

- In **Solution Explorer**, right-click the project. Select **Add** > **New Folder**. Name the folder `Models`.
- Right-click the `Models` folder and select **Add** > **Class**. Name the class *TodoItem* and select **Add**.
- Replace the template code with the following:

* Add a folder named `Models`.
* Add a `TodoItem.cs` file to the `Models` folder with the following code:

- Control-click the **TodoAPI** project and select **Add** > **New Folder**. Name the folder `Models`.

- Control-click the `Models` folder, and select **Add** > **New Class...** > **General** > **Empty Class**.

- Name the class *TodoItem*, and then select **Create**.

- Replace the template code with the following:

```
namespace TodoApi.Models;

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

The `Id` property functions as the unique key in a relational database.

Model classes can go anywhere in the project, but the `Models` folder is used by convention.

## Add a database context

The *database context* is the main class that coordinates Entity Framework functionality for a data model. This class is created by deriving from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_5_visual-studio-code+visual-studio-mac)

- Right-click the `Models` folder and select **Add** > **Class**. Name the class *TodoContext* and click **Add**.

* Add a `TodoContext.cs` file to the `Models` folder.

- Enter the following code:

  ```
  using Microsoft.EntityFrameworkCore;

  namespace TodoApi.Models;

  public class TodoContext : DbContext
  {
      public TodoContext(DbContextOptions<TodoContext> options)
          : base(options)
      {
      }

      public DbSet<TodoItem> TodoItems { get; set; } = null!;
  }
  ```

## Register the database context

In ASP.NET Core, services such as the DB context must be registered with the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container. The container provides the service to controllers.

Update `Program.cs` with the following highlighted code:

```
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The preceding code:

* Adds `using` directives.
* Adds the database context to the DI container.
* Specifies that the database context will use an in-memory database.

## Scaffold a controller

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_6_visual-studio-code+visual-studio-mac)

- Right-click the `Controllers` folder.

- Select **Add** > **New Scaffolded Item**.

- Select **API Controller with actions, using Entity Framework**, and then select **Add**.

- In the **Add API Controller with actions, using Entity Framework** dialog:

  * Select **TodoItem (TodoApi.Models)** in the **Model class**.
  * Select **TodoContext (TodoApi.Models)** in the **Data context class**.
  * Select **Add**.

  If the scaffolding operation fails, select **Add** to try scaffolding a second time.

Make sure that all of your changes so far are saved.

* Control-click the **TodoAPI** project and select **Open in Terminal**. The terminal opens at the `TodoAPI` project folder. Run the following commands:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet tool uninstall -g dotnet-aspnet-codegenerator
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet tool update -g dotnet-aspnet-codegenerator
```

The preceding commands:

* Add NuGet packages required for scaffolding.
* Install the scaffolding engine (`dotnet-aspnet-codegenerator`) after uninstalling any possible previous version.

For Linux, add the .NET tools directory to the system path with the following command:

```
echo 'export PATH=$HOME/.dotnet/tools:$PATH' >> ~/.bashrc
source ~/.bashrc
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Build the project.

Run the following command:

```
dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
```

The preceding command scaffolds the `TodoItemsController`.

The generated code:

* Marks the class with the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute. This attribute indicates that the controller responds to web API requests. For information about specific behaviors that the attribute enables, see [Create web APIs with ASP.NET Core](../web-api/?view=aspnetcore-10.0).
* Uses DI to inject the database context (`TodoContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

The ASP.NET Core templates for:

* Controllers with views include `[action]` in the route template.
* API controllers don't include `[action]` in the route template.

When the `[action]` token isn't in the route template, the [action](../mvc/controllers/routing?view=aspnetcore-10.0#action) name (method name) isn't included in the endpoint. That is, the action's associated method name isn't used in the matching route.

Open the generated `Controllers/TodoItemsController.cs` file to review the scaffolded code. Scaffolding generates a complete, working controller with action methods that create, read, update, and delete `TodoItem` data:

* `GetTodoItems`: Get all to-do items.
* `GetTodoItem`: Get a to-do item by ID.
* `PostTodoItem`: Create a to-do item.
* `PutTodoItem`: Update an existing to-do item.
* `DeleteTodoItem`: Delete a to-do item.

The following sections examine these generated methods, starting with `PostTodoItem`.

## Examine the `PostTodoItem` create method

Examine the scaffolded `PostTodoItem` method, which already works as generated. The return statement uses the [nameof](/en-us/dotnet/csharp/language-reference/operators/nameof) operator so the action name isn't hard-coded as a string. The commented-out line shows the equivalent hard-coded form that `nameof` replaces:

```
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
    _context.TodoItems.Add(todoItem);
    await _context.SaveChangesAsync();

    //    return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
    return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
}
```

The preceding code is an `HTTP POST` method, as indicated by the [`[HttpPost]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute) attribute. The method gets the value of the `TodoItem` from the body of the HTTP request.

For more information, see [Attribute routing with Http\[Verb\] attributes](../mvc/controllers/routing?view=aspnetcore-10.0#verb).

The [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) method:

* Returns an [HTTP 201 status code](https://developer.mozilla.org/docs/Web/HTTP/Status/201) if successful. `HTTP 201` is the standard response for an `HTTP POST` method that creates a new resource on the server.
* Adds a [Location](https://developer.mozilla.org/docs/Web/HTTP/Headers/Location) header to the response. The `Location` header specifies the [URI](https://developer.mozilla.org/docs/Glossary/URI) of the newly created to-do item. For more information, see [10.2.2 201 Created](https://www.rfc-editor.org/rfc/rfc9110.html#section-10.2.2).
* References the `GetTodoItem` action to create the `Location` header's URI.

[]()

### Test PostTodoItem

* Press Ctrl+F5 to run the app.

* In the Swagger browser window, select **POST /api/TodoItems**, and then select **Try it out**.

* In the **Request body** input window, update the JSON. For example,

  ```
  {
    "name": "walk dog",
    "isComplete": true
  }
  ```

* Select **Execute**

### Test the location header URI

In the preceding POST, the Swagger UI shows the [location header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Location) under **Response headers**. For example, `location: https://localhost:7260/api/TodoItems/1`. The location header shows the URI to the created resource.

To test the location header:

* In the Swagger browser window, select **GET /api/TodoItems/{id}**, and then select **Try it out**.

* Enter `1` in the `id` input box, and then select **Execute**.

## Examine the GET methods

Two GET endpoints are implemented:

* `GET /api/todoitems`
* `GET /api/todoitems/{id}`

The previous section showed an example of the `/api/todoitems/{id}` route.

Follow the [POST](#post7) instructions to add another todo item, and then test the `/api/todoitems` route using Swagger.

This app uses an in-memory database. If the app is stopped and started, the preceding GET request doesn't return any data. If no data is returned, [POST](#post7) data to the app.

## Routing and URL paths

The [`[HttpGet]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute) attribute denotes a method that responds to an `HTTP GET` request. The URL path for each method is constructed as follows:

* Start with the template string in the controller's `Route` attribute:

  ```
  [Route("api/[controller]")]
  [ApiController]
  public class TodoItemsController : ControllerBase
  ```

* Replace `[controller]` with the name of the controller, which by convention is the controller class name minus the "Controller" suffix. For this sample, the controller class name is **TodoItems**Controller, so the controller name is "TodoItems". ASP.NET Core [routing](../mvc/controllers/routing?view=aspnetcore-10.0) is case insensitive.

* If the `[HttpGet]` attribute has a route template (for example, `[HttpGet("products")]`), append that to the path. This sample doesn't use a template. For more information, see [Attribute routing with Http\[Verb\] attributes](../mvc/controllers/routing?view=aspnetcore-10.0#verb).

In the following `GetTodoItem` method, `"{id}"` is a placeholder variable for the unique identifier of the to-do item. When `GetTodoItem` is invoked, the value of `"{id}"` in the URL is provided to the method in its `id` parameter.

```
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}
```

## Return values

The return type of the `GetTodoItems` and `GetTodoItem` methods is [ActionResult\<T> type](../web-api/action-return-types?view=aspnetcore-10.0#actionresultt-type). ASP.NET Core automatically serializes the object to [JSON](https://www.json.org/) and writes the JSON into the body of the response message. The response code for this return type is [200 OK](https://developer.mozilla.org/docs/Web/HTTP/Status/200), assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.

`ActionResult` return types can represent a wide range of HTTP status codes. For example, `GetTodoItem` can return two different status values:

* If no item matches the requested ID, the method returns a [404 status](https://developer.mozilla.org/docs/Web/HTTP/Status/404) [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) error code.
* Otherwise, the method returns 200 with a JSON response body. Returning `item` results in an `HTTP 200` response.

## The PutTodoItem method

Examine the `PutTodoItem` method:

```
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id)
    {
        return BadRequest();
    }

    _context.Entry(todoItem).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!TodoItemExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return NoContent();
}
```

`PutTodoItem` is similar to `PostTodoItem`, except it uses `HTTP PUT`. The response is [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a `PUT` request requires the client to send the entire updated entity, not just the changes. To support partial updates, use [HTTP PATCH](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute).

### Test the PutTodoItem method

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.

Using the Swagger UI, use the PUT button to update the `TodoItem` that has Id = 1 and set its name to `"feed fish"`. Note the response is [`HTTP 204 No Content`](https://developer.mozilla.org/docs/Web/HTTP/Status/204).

## The DeleteTodoItem method

Examine the `DeleteTodoItem` method:

```
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);
    if (todoItem == null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(todoItem);
    await _context.SaveChangesAsync();

    return NoContent();
}
```

### Test the DeleteTodoItem method

Use the Swagger UI to delete the `TodoItem` that has Id = 1. Note the response is [`HTTP 204 No Content`](https://developer.mozilla.org/docs/Web/HTTP/Status/204).

## Test with other tools

There are many other tools that can be used to test web APIs, for example:

* [Visual Studio Endpoints Explorer and .http files](../test/http-files?view=aspnetcore-10.0)
* [http-repl](../web-api/http-repl/?view=aspnetcore-10.0)
* [curl](https://terminalcheatsheet.com/guides/curl-rest-api). Swagger uses `curl` and shows the `curl` commands it submits.
* [Fiddler](https://www.telerik.com/fiddler)

For more information, see:

* [Minimal API tutorial: test with .http files and Endpoints Explorer](min-web-api?view=aspnetcore-10.0)

[]()

## Prevent over-posting

Currently the sample app exposes the entire `TodoItem` object. Production apps typically limit the data that's input and returned using a subset of the model. There are multiple reasons behind this, and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. **DTO** is used in this tutorial.

A DTO may be used to:

* Prevent over-posting.
* Hide properties that clients are not supposed to view.
* Omit some properties in order to reduce payload size.
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.

To demonstrate the DTO approach, update the `TodoItem` class to include a secret field:

```
namespace TodoApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
        public string? Secret { get; set; }
    }
}
```

The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

Verify you can post and get the secret field.

Create a DTO model:

```
namespace TodoApi.Models;

public class TodoItemDTO
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

Update the `TodoItemsController` to use `TodoItemDTO`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Controllers;

[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{
    private readonly TodoContext _context;

    public TodoItemsController(TodoContext context)
    {
        _context = context;
    }

    // GET: api/TodoItems
    [HttpGet]
    public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
    {
        return await _context.TodoItems
            .Select(x => ItemToDTO(x))
            .ToListAsync();
    }

    // GET: api/TodoItems/5
    // <snippet_GetByID>
    [HttpGet("{id}")]
    public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
    {
        var todoItem = await _context.TodoItems.FindAsync(id);

        if (todoItem == null)
        {
            return NotFound();
        }

        return ItemToDTO(todoItem);
    }
    // </snippet_GetByID>

    // PUT: api/TodoItems/5
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    // <snippet_Update>
    [HttpPut("{id}")]
    public async Task<IActionResult> PutTodoItem(long id, TodoItemDTO todoDTO)
    {
        if (id != todoDTO.Id)
        {
            return BadRequest();
        }

        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
        {
            return NotFound();
        }

        todoItem.Name = todoDTO.Name;
        todoItem.IsComplete = todoDTO.IsComplete;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
        {
            return NotFound();
        }

        return NoContent();
    }
    // </snippet_Update>

    // POST: api/TodoItems
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    // <snippet_Create>
    [HttpPost]
    public async Task<ActionResult<TodoItemDTO>> PostTodoItem(TodoItemDTO todoDTO)
    {
        var todoItem = new TodoItem
        {
            IsComplete = todoDTO.IsComplete,
            Name = todoDTO.Name
        };

        _context.TodoItems.Add(todoItem);
        await _context.SaveChangesAsync();

        return CreatedAtAction(
            nameof(GetTodoItem),
            new { id = todoItem.Id },
            ItemToDTO(todoItem));
    }
    // </snippet_Create>

    // DELETE: api/TodoItems/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteTodoItem(long id)
    {
        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
        {
            return NotFound();
        }

        _context.TodoItems.Remove(todoItem);
        await _context.SaveChangesAsync();

        return NoContent();
    }

    private bool TodoItemExists(long id)
    {
        return _context.TodoItems.Any(e => e.Id == id);
    }

    private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
       new TodoItemDTO
       {
           Id = todoItem.Id,
           Name = todoItem.Name,
           IsComplete = todoItem.IsComplete
       };
}
```

Verify you can't post or get the secret field.

## Call the web API with JavaScript

See [Tutorial: Call an ASP.NET Core web API with JavaScript](web-api-javascript?view=aspnetcore-10.0).

## Web API video series

See [Video: Beginner's Series to: Web APIs](/en-us/shows/beginners-series-to-web-apis/).

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

[]()

## Add authentication support to a web API

[Duende Software](https://duendesoftware.com/) might require you to pay a license fee for production use of Duende Identity Server. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../migration/50-to-60?view=aspnetcore-10.0#project-templates-use-duende-identity-server).

For more information, see the [Duende Identity Server documentation (Duende Software website)](https://docs.duendesoftware.com).

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore).

## Related content

[View or download sample code for this tutorial](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-web-api/samples). See [how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

For more information, see the following resources:

* [Create web APIs with ASP.NET Core](../web-api/?view=aspnetcore-10.0)
* [Tutorial: Create a Minimal API with ASP.NET Core](min-web-api?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](web-api-help-pages-using-swagger?view=aspnetcore-10.0)
* [Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8](../data/ef-rp/intro?view=aspnetcore-10.0)
* [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0)
* [Controller action return types in ASP.NET Core web API](../web-api/action-return-types?view=aspnetcore-10.0)
* [Deploy ASP.NET Core apps to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0)
* [Host and deploy ASP.NET Core](../host-and-deploy/?view=aspnetcore-10.0)
* [Create a web API with ASP.NET Core](/en-us/training/modules/build-web-api-aspnet-core/)

This tutorial teaches the basics of building a controller-based web API that uses a database. Another approach to creating APIs in ASP.NET Core is to create *Minimal APIs*. For help with choosing between Minimal APIs and controller-based APIs, see [APIs overview](../fundamentals/apis?view=aspnetcore-10.0). For a tutorial on creating a Minimal API, see [Tutorial: Create a Minimal API with ASP.NET Core](min-web-api?view=aspnetcore-10.0).

## Overview

This tutorial creates the following API:

| API                              | Description               | Request body | Response body        |
| -------------------------------- | ------------------------- | ------------ | -------------------- |
| `GET /api/todoitems`             | Get all to-do items       | None         | Array of to-do items |
| `GET /api/todoitems/{id}`        | Get an item by ID         | None         | To-do item           |
| `POST /api/todoitems`            | Add a new item            | To-do item   | To-do item           |
| `PUT /api/todoitems/{id}`        | Update an existing item   | To-do item   | None                 |
| `DELETE /api/todoitems/{id}`     | Delete an item            | None         | None                 |

The following diagram shows the design of the app.

## Create a web project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- From the **File** menu, select **New** > **Project**.

- Enter *Web API* in the search box.

- Select the **ASP.NET Core Web API** template and select **Next**.

- In the **Configure your new project dialog**, name the project *TodoApi* and select **Next**.

- In the **Additional information** dialog:

  * Confirm the **Framework** is **.NET 8.0 (Long Term Support)**.
  * Confirm the checkbox for **Use controllers(uncheck to use Minimal APIs)** is checked.
  * Confirm the checkbox for **Enable OpenAPI support** is checked.
  * Select **Create**.

## Add a NuGet package

A NuGet package must be added to support the database used in this tutorial.

* From the **Tools** menu, select **NuGet Package Manager > Manage NuGet Packages for Solution**.
* Select the **Browse** tab.
* Enter **Microsoft.EntityFrameworkCore.InMemory** in the search box, and then select `Microsoft.EntityFrameworkCore.InMemory`.
* Select the **Project** checkbox in the right pane and then select **Install**.

- Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

- Change directories (`cd`) to the folder that will contain the project folder.

- Run the following commands:

  ```
  dotnet new webapi --use-controllers -o TodoApi
  cd TodoApi
  dotnet add package Microsoft.EntityFrameworkCore.InMemory
  code -r ../TodoApi
  ```

  These commands:

  * Create a new web API project and open it in Visual Studio Code.
  * Add a NuGet package that is needed for the next section.
  * Open the *TodoApi* folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

### Test the project

The project template creates a `WeatherForecast` API with support for [Swagger](web-api-help-pages-using-swagger?view=aspnetcore-10.0).

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog when a project is not yet configured to use SSL:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio launches the default browser and navigates to `https://localhost:<port>/swagger/index.html`, where `<port>` is a randomly chosen port number set at the project creation.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Run the app:

* Run the following command to start the app on the `https` profile:

  ```
  dotnet run --launch-profile https
  ```

The output shows messages similar to the following, indicating that the app is running and awaiting requests:

```
...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:{port}
...
```

* `Ctrl`+*click* the HTTPS URL in the output to test the web app in a browser.

* The default browser is launched to `https://localhost:<port>/swagger/index.html`, where `<port>` is the randomly chosen port number displayed in the output. There's no endpoint at `https://localhost:<port>`, so the browser returns [HTTP 404 Not Found](https://developer.mozilla.org/docs/Web/HTTP/Status/404). Append `/swagger` to the URL, `https://localhost:<port>/swagger`.

After testing the web app in the following instruction, press `Ctrl`+`C` in the integrated terminal to shut it down.

The Swagger page `/swagger/index.html` is displayed. Select **GET** > **Try it out** > **Execute**. The page displays:

* The [Curl](https://curl.haxx.se/) command to test the WeatherForecast API.
* The URL to test the WeatherForecast API.
* The response code, body, and headers.
* A drop-down list box with media types and the example value and schema.

If the Swagger page doesn't appear, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/21647).

Swagger is used to generate useful documentation and help pages for web APIs. This tutorial uses Swagger to test the app. For more information on Swagger, see [ASP.NET Core web API documentation with Swagger / OpenAPI](web-api-help-pages-using-swagger?view=aspnetcore-10.0).

Copy and paste the **Request URL** in the browser: `https://localhost:<port>/weatherforecast`

JSON similar to the following example is returned:

```
[
    {
        "date": "2019-07-16T19:04:05.7257911-06:00",
        "temperatureC": 52,
        "temperatureF": 125,
        "summary": "Mild"
    },
    {
        "date": "2019-07-17T19:04:05.7258461-06:00",
        "temperatureC": 36,
        "temperatureF": 96,
        "summary": "Warm"
    },
    {
        "date": "2019-07-18T19:04:05.7258467-06:00",
        "temperatureC": 39,
        "temperatureF": 102,
        "summary": "Cool"
    },
    {
        "date": "2019-07-19T19:04:05.7258471-06:00",
        "temperatureC": 10,
        "temperatureF": 49,
        "summary": "Bracing"
    },
    {
        "date": "2019-07-20T19:04:05.7258474-06:00",
        "temperatureC": -1,
        "temperatureF": 31,
        "summary": "Chilly"
    }
]
```

## Add a model class

A *model* is a set of classes that represent the data that the app manages. The model for this app is the `TodoItem` class.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code](#tabpanel_4_visual-studio-code)

- In **Solution Explorer**, right-click the project. Select **Add** > **New Folder**. Name the folder `Models`.
- Right-click the `Models` folder and select **Add** > **Class**. Name the class *TodoItem* and select **Add**.
- Replace the template code with the following:

* Add a folder named `Models`.
* Add a `TodoItem.cs` file to the `Models` folder with the following code:

```
namespace TodoApi.Models;

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

The `Id` property functions as the unique key in a relational database.

Model classes can go anywhere in the project, but the `Models` folder is used by convention.

## Add a database context

The *database context* is the main class that coordinates Entity Framework functionality for a data model. This class is created by deriving from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

- Right-click the `Models` folder and select **Add** > **Class**. Name the class *TodoContext* and click **Add**.

* Add a `TodoContext.cs` file to the `Models` folder.

- Enter the following code:

  ```
  using Microsoft.EntityFrameworkCore;

  namespace TodoApi.Models;

  public class TodoContext : DbContext
  {
      public TodoContext(DbContextOptions<TodoContext> options)
          : base(options)
      {
      }

      public DbSet<TodoItem> TodoItems { get; set; } = null!;
  }
  ```

## Register the database context

In ASP.NET Core, services such as the DB context must be registered with the [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) container. The container provides the service to controllers.

Update `Program.cs` with the following highlighted code:

```
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The preceding code:

* Adds `using` directives.
* Adds the database context to the DI container.
* Specifies that the database context will use an in-memory database.

## Scaffold a controller

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

- Right-click the `Controllers` folder.

- Select **Add** > **New Scaffolded Item**.

- Select **API Controller with actions, using Entity Framework**, and then select **Add**.

- In the **Add API Controller with actions, using Entity Framework** dialog:

  * Select **TodoItem (TodoApi.Models)** in the **Model class**.
  * Select **TodoContext (TodoApi.Models)** in the **Data context class**.
  * Select **Add**.

  If the scaffolding operation fails, select **Add** to try scaffolding a second time.

Make sure that all of your changes so far are saved.

* Control-click the **TodoAPI** project and select **Open in Terminal**. The terminal opens at the `TodoAPI` project folder. Run the following commands:

```
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet tool uninstall -g dotnet-aspnet-codegenerator
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet tool update -g dotnet-aspnet-codegenerator
```

The preceding commands:

* Add NuGet packages required for scaffolding.
* Install the scaffolding engine (`dotnet-aspnet-codegenerator`) after uninstalling any possible previous version.

For Linux, add the .NET tools directory to the system path with the following command:

```
echo 'export PATH=$HOME/.dotnet/tools:$PATH' >> ~/.bashrc
source ~/.bashrc
```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Build the project.

Run the following command:

```
dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
```

The preceding command scaffolds the `TodoItemsController`.

The generated code:

* Marks the class with the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute. This attribute indicates that the controller responds to web API requests. For information about specific behaviors that the attribute enables, see [Create web APIs with ASP.NET Core](../web-api/?view=aspnetcore-10.0).
* Uses DI to inject the database context (`TodoContext`) into the controller. The database context is used in each of the [CRUD](https://wikipedia.org/wiki/Create,_read,_update_and_delete) methods in the controller.

The ASP.NET Core templates for:

* Controllers with views include `[action]` in the route template.
* API controllers don't include `[action]` in the route template.

When the `[action]` token isn't in the route template, the [action](../mvc/controllers/routing?view=aspnetcore-10.0#action) name (method name) isn't included in the endpoint. That is, the action's associated method name isn't used in the matching route.

Open the generated `Controllers/TodoItemsController.cs` file to review the scaffolded code. Scaffolding generates a complete, working controller with action methods that create, read, update, and delete `TodoItem` data:

* `GetTodoItems`: Get all to-do items.
* `GetTodoItem`: Get a to-do item by ID.
* `PostTodoItem`: Create a to-do item.
* `PutTodoItem`: Update an existing to-do item.
* `DeleteTodoItem`: Delete a to-do item.

The following sections examine these generated methods, starting with `PostTodoItem`.

## Examine the `PostTodoItem` create method

Examine the scaffolded `PostTodoItem` method. The generated code already works. The return statement uses the [nameof](/en-us/dotnet/csharp/language-reference/operators/nameof) operator so the action name isn't hard-coded as a string. The commented-out line shows the equivalent hard-coded form that `nameof` replaces:

```
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
    _context.TodoItems.Add(todoItem);
    await _context.SaveChangesAsync();

    //    return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
    return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
}
```

The preceding code is an `HTTP POST` method, as indicated by the [`[HttpPost]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute) attribute. The method gets the value of the `TodoItem` from the body of the HTTP request.

For more information, see [Attribute routing with Http\[Verb\] attributes](../mvc/controllers/routing?view=aspnetcore-10.0#verb).

The [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) method:

* Returns an [HTTP 201 status code](https://developer.mozilla.org/docs/Web/HTTP/Status/201) if successful. `HTTP 201` is the standard response for an `HTTP POST` method that creates a new resource on the server.
* Adds a [Location](https://developer.mozilla.org/docs/Web/HTTP/Headers/Location) header to the response. The `Location` header specifies the [URI](https://developer.mozilla.org/docs/Glossary/URI) of the newly created to-do item. For more information, see [10.2.2 201 Created](https://www.rfc-editor.org/rfc/rfc9110.html#section-10.2.2).
* References the `GetTodoItem` action to create the `Location` header's URI.

[]()

### Test PostTodoItem

* Press Ctrl+F5 to run the app.

* In the Swagger browser window, select **POST /api/TodoItems**, and then select **Try it out**.

* In the **Request body** input window, update the JSON. For example,

  ```
  {
    "name": "walk dog",
    "isComplete": true
  }
  ```

* Select **Execute**

### Test the location header URI

In the preceding POST, the Swagger UI shows the [location header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Location) under **Response headers**. For example, `location: https://localhost:7260/api/TodoItems/1`. The location header shows the URI to the created resource.

To test the location header:

* In the Swagger browser window, select **GET /api/TodoItems/{id}**, and then select **Try it out**.

* Enter `1` in the `id` input box, and then select **Execute**.

## Examine the GET methods

Two GET endpoints are implemented:

* `GET /api/todoitems`
* `GET /api/todoitems/{id}`

The previous section showed an example of the `/api/todoitems/{id}` route.

Follow the [POST](#post7) instructions to add another todo item, and then test the `/api/todoitems` route using Swagger.

This app uses an in-memory database. If the app is stopped and started, the preceding GET request doesn't return any data. If no data is returned, [POST](#post7) data to the app.

## Routing and URL paths

The [`[HttpGet]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute) attribute denotes a method that responds to an `HTTP GET` request. The URL path for each method is constructed as follows:

* Start with the template string in the controller's `Route` attribute:

  ```
  [Route("api/[controller]")]
  [ApiController]
  public class TodoItemsController : ControllerBase
  ```

* Replace `[controller]` with the name of the controller, which by convention is the controller class name minus the "Controller" suffix. For this sample, the controller class name is **TodoItems**Controller, so the controller name is "TodoItems". ASP.NET Core [routing](../mvc/controllers/routing?view=aspnetcore-10.0) is case insensitive.

* If the `[HttpGet]` attribute has a route template (for example, `[HttpGet("products")]`), append that to the path. This sample doesn't use a template. For more information, see [Attribute routing with Http\[Verb\] attributes](../mvc/controllers/routing?view=aspnetcore-10.0#verb).

In the following `GetTodoItem` method, `"{id}"` is a placeholder variable for the unique identifier of the to-do item. When `GetTodoItem` is invoked, the value of `"{id}"` in the URL is provided to the method in its `id` parameter.

```
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}
```

## Return values

The return type of the `GetTodoItems` and `GetTodoItem` methods is [ActionResult\<T> type](../web-api/action-return-types?view=aspnetcore-10.0#actionresultt-type). ASP.NET Core automatically serializes the object to [JSON](https://www.json.org/) and writes the JSON into the body of the response message. The response code for this return type is [200 OK](https://developer.mozilla.org/docs/Web/HTTP/Status/200), assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.

`ActionResult` return types can represent a wide range of HTTP status codes. For example, `GetTodoItem` can return two different status values:

* If no item matches the requested ID, the method returns a [404 status](https://developer.mozilla.org/docs/Web/HTTP/Status/404) [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) error code.
* Otherwise, the method returns 200 with a JSON response body. Returning `item` results in an `HTTP 200` response.

## The PutTodoItem method

Examine the `PutTodoItem` method:

```
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id)
    {
        return BadRequest();
    }

    _context.Entry(todoItem).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!TodoItemExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return NoContent();
}
```

`PutTodoItem` is similar to `PostTodoItem`, except it uses `HTTP PUT`. The response is [204 (No Content)](https://www.rfc-editor.org/rfc/rfc9110#status.204). According to the HTTP specification, a `PUT` request requires the client to send the entire updated entity, not just the changes. To support partial updates, use [HTTP PATCH](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute).

### Test the PutTodoItem method

This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.

Using the Swagger UI, use the PUT button to update the `TodoItem` that has Id = 1 and set its name to `"feed fish"`. Note the response is [`HTTP 204 No Content`](https://developer.mozilla.org/docs/Web/HTTP/Status/204).

## The DeleteTodoItem method

Examine the `DeleteTodoItem` method:

```
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);
    if (todoItem == null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(todoItem);
    await _context.SaveChangesAsync();

    return NoContent();
}
```

### Test the DeleteTodoItem method

Use the Swagger UI to delete the `TodoItem` that has Id = 1. Note the response is [`HTTP 204 No Content`](https://developer.mozilla.org/docs/Web/HTTP/Status/204).

## Test with other tools

There are many other tools that can be used to test web APIs, for example:

* [Visual Studio Endpoints Explorer and .http files](../test/http-files?view=aspnetcore-10.0)
* [http-repl](../web-api/http-repl/?view=aspnetcore-10.0)
* [curl](https://terminalcheatsheet.com/guides/curl-rest-api). Swagger uses `curl` and shows the `curl` commands it submits.
* [Fiddler](https://www.telerik.com/fiddler)

For more information, see:

* [Minimal API tutorial: test with .http files and Endpoints Explorer](min-web-api?view=aspnetcore-10.0)

[]()

## Prevent over-posting

Currently the sample app exposes the entire `TodoItem` object. Production apps typically limit the data that's input and returned using a subset of the model. There are multiple reasons behind this, and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. **DTO** is used in this tutorial.

A DTO may be used to:

* Prevent over-posting.
* Hide properties that clients are not supposed to view.
* Omit some properties in order to reduce payload size.
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.

To demonstrate the DTO approach, update the `TodoItem` class to include a secret field:

```
namespace TodoApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
        public string? Secret { get; set; }
    }
}
```

The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

Verify you can post and get the secret field.

Create a DTO model:

```
namespace TodoApi.Models;

public class TodoItemDTO
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
```

Update the `TodoItemsController` to use `TodoItemDTO`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Controllers;

[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{
    private readonly TodoContext _context;

    public TodoItemsController(TodoContext context)
    {
        _context = context;
    }

    // GET: api/TodoItems
    [HttpGet]
    public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
    {
        return await _context.TodoItems
            .Select(x => ItemToDTO(x))
            .ToListAsync();
    }

    // GET: api/TodoItems/5
    // <snippet_GetByID>
    [HttpGet("{id}")]
    public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
    {
        var todoItem = await _context.TodoItems.FindAsync(id);

        if (todoItem == null)
        {
            return NotFound();
        }

        return ItemToDTO(todoItem);
    }
    // </snippet_GetByID>

    // PUT: api/TodoItems/5
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    // <snippet_Update>
    [HttpPut("{id}")]
    public async Task<IActionResult> PutTodoItem(long id, TodoItemDTO todoDTO)
    {
        if (id != todoDTO.Id)
        {
            return BadRequest();
        }

        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
        {
            return NotFound();
        }

        todoItem.Name = todoDTO.Name;
        todoItem.IsComplete = todoDTO.IsComplete;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
        {
            return NotFound();
        }

        return NoContent();
    }
    // </snippet_Update>

    // POST: api/TodoItems
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    // <snippet_Create>
    [HttpPost]
    public async Task<ActionResult<TodoItemDTO>> PostTodoItem(TodoItemDTO todoDTO)
    {
        var todoItem = new TodoItem
        {
            IsComplete = todoDTO.IsComplete,
            Name = todoDTO.Name
        };

        _context.TodoItems.Add(todoItem);
        await _context.SaveChangesAsync();

        return CreatedAtAction(
            nameof(GetTodoItem),
            new { id = todoItem.Id },
            ItemToDTO(todoItem));
    }
    // </snippet_Create>

    // DELETE: api/TodoItems/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteTodoItem(long id)
    {
        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
        {
            return NotFound();
        }

        _context.TodoItems.Remove(todoItem);
        await _context.SaveChangesAsync();

        return NoContent();
    }

    private bool TodoItemExists(long id)
    {
        return _context.TodoItems.Any(e => e.Id == id);
    }

    private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
       new TodoItemDTO
       {
           Id = todoItem.Id,
           Name = todoItem.Name,
           IsComplete = todoItem.IsComplete
       };
}
```

Verify you can't post or get the secret field.

## Call the web API with JavaScript

See [Tutorial: Call an ASP.NET Core web API with JavaScript](web-api-javascript?view=aspnetcore-10.0).

## Web API video series

See [Video: Beginner's Series to: Web APIs](/en-us/shows/beginners-series-to-web-apis/).

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

[]()

## Add authentication support to a web API

[Duende Software](https://duendesoftware.com/) might require you to pay a license fee for production use of Duende Identity Server. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../migration/50-to-60?view=aspnetcore-10.0#project-templates-use-duende-identity-server).

For more information, see the [Duende Identity Server documentation (Duende Software website)](https://docs.duendesoftware.com).

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore).

## Related content

[View or download sample code for this tutorial](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-web-api/samples). See [how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

For more information, see the following resources:

* [Create web APIs with ASP.NET Core](../web-api/?view=aspnetcore-10.0)
* [Tutorial: Create a Minimal API with ASP.NET Core](min-web-api?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](web-api-help-pages-using-swagger?view=aspnetcore-10.0)
* [Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8](../data/ef-rp/intro?view=aspnetcore-10.0)
* [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0)
* [Controller action return types in ASP.NET Core web API](../web-api/action-return-types?view=aspnetcore-10.0)
* [Deploy ASP.NET Core apps to Azure App Service](../host-and-deploy/azure-apps/?view=aspnetcore-10.0)
* [Host and deploy ASP.NET Core](../host-and-deploy/?view=aspnetcore-10.0)
* [Create a web API with ASP.NET Core](/en-us/training/modules/build-web-api-aspnet-core/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/azure-iis-errors-reference
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/azure-iis-errors-reference.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Common error troubleshooting for Azure App Service and IIS with ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes the most common errors and provides troubleshooting advice when hosting ASP.NET Core apps on Azure Apps Service and IIS.

See [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0) information on common app startup errors and instructions on how to diagnose errors.

Collect the following information:

* Browser behavior such as status code and error message.

* Application Event Log entries.

  * Azure App Service: See [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

  * IIS

    1. Select **Start** on the **Windows** menu, type *Event Viewer*, and press **Enter**.
    2. After the **Event Viewer** opens, expand **Windows Logs** > **Application** in the sidebar.

* ASP.NET Core Module stdout and debug log entries.

  * Azure App Service: See [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).
  * IIS: Follow the instructions in the [Log creation and redirection](aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection) and [Enhanced diagnostic logs](iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs) sections of the ASP.NET Core Module article.

Compare error information to the following common errors. If a match is found, follow the troubleshooting advice.

The list of errors in this article isn't exhaustive. If you encounter an error not listed here, open a new issue using the **Content feedback** button at the bottom of this article with detailed instructions on how to reproduce the error.

Important

**ASP.NET Core preview releases with Azure App Service**

ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core preview release, see [Deploy ASP.NET Core preview release to Azure App Service](azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service).

## OS upgrade removed the 32-bit ASP.NET Core Module

**Application Log:** The Module DLL **C:\WINDOWS\system32\inetsrv\aspnetcore.dll** failed to load. The data is the error.

Troubleshooting:

Non-OS files in the **C:\Windows\SysWOW64\inetsrv** directory aren't preserved during an OS upgrade. If the ASP.NET Core Module is installed prior to an OS upgrade and then any app pool is run in 32-bit mode after an OS upgrade, this issue is encountered. After an OS upgrade, repair the ASP.NET Core Module. See [Install the .NET Hosting bundle](iis/?view=aspnetcore-10.0#install-the-net-hosting-bundle). Select **Repair** when the installer is run.

## Missing site extension, 32-bit (x86) and 64-bit (x64) site extensions installed, or wrong process bitness set

*Applies to apps hosted by Azure App Services.*

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure

* **Application Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. Could not find inprocess request handler. Captured output from invoking hostfxr: It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}-preview-\*' was not found. Failed to start application '/LM/W3SVC/1416782824/ROOT', ErrorCode '0x8000ffff'.

* **ASP.NET Core Module stdout Log:** It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}-preview-\*' was not found.

* **ASP.NET Core Module Debug Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine. Failed HRESULT returned: 0x8000ffff. Could not find inprocess request handler. It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}-preview-\*' was not found.

Troubleshooting:

* If running the app on a preview runtime, install either the 32-bit (x86) **or** 64-bit (x64) site extension that matches the bitness of the app and the app's runtime version. **Don't install both extensions or multiple runtime versions of the extension.**

  * ASP.NET Core {RUNTIME VERSION} (x86) Runtime
  * ASP.NET Core {RUNTIME VERSION} (x64) Runtime

  Restart the app. Wait several seconds for the app to restart.

* If running the app on a preview runtime and both the 32-bit (x86) and 64-bit (x64) [site extensions](azure-apps/?view=aspnetcore-10.0#install-the-preview-site-extension) are installed, uninstall the site extension that doesn't match the bitness of the app. After removing the site extension, restart the app. Wait several seconds for the app to restart.

* If running the app on a preview runtime and the site extension's bitness matches that of the app, confirm that the preview site extension's *runtime version* matches the app's runtime version.

* Confirm that the app's **Platform** in **Application Settings** matches the bitness of the app.

For more information, see [Deploy ASP.NET Core apps to Azure App Service](azure-apps/?view=aspnetcore-10.0#install-the-preview-site-extension).

## An x86 app is deployed but the app pool isn't enabled for 32-bit apps

* **Browser:** HTTP Error 500.30 - ANCM In-Process Start Failure

* **Application Log:** Application '/LM/W3SVC/5/ROOT' with physical root '{PATH}' hit unexpected managed exception, exception code = '0xe0434352'. Please check the stderr logs for more information. Application '/LM/W3SVC/5/ROOT' with physical root '{PATH}' failed to load clr and managed application. CLR worker thread exited prematurely

* **ASP.NET Core Module stdout Log:** The log file is created but empty.

* **ASP.NET Core Module Debug Log:** Failed HRESULT returned: 0x8007023e

This scenario is trapped by the SDK when publishing a self-contained app. The SDK produces an error if the RID doesn't match the platform target (for example, `win10-x64` RID with `<PlatformTarget>x86</PlatformTarget>` in the project file).

Troubleshooting:

For an x86 framework-dependent deployment (`<PlatformTarget>x86</PlatformTarget>`), enable the IIS app pool for 32-bit apps. In IIS Manager, open the app pool's **Advanced Settings** and set **Enable 32-Bit Applications** to **True**.

## Platform conflicts with RID

* **Browser:** HTTP Error 502.5 - Process Failure

* **Application Log:** Application 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY}' with physical root 'C:{PATH}' failed to start process with commandline '"C:{PATH}{ASSEMBLY}.{exe|dll}" ', ErrorCode = '0x80004005 : ff.

* **ASP.NET Core Module stdout Log:** Unhandled Exception: System.BadImageFormatException: Could not load file or assembly '{ASSEMBLY}.dll'. An attempt was made to load a program with an incorrect format.

Troubleshooting:

* Confirm that the app runs locally on Kestrel. A process failure might be the result of a problem within the app. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

* If this exception occurs for an Azure Apps deployment when upgrading an app and deploying newer assemblies, manually delete all files from the prior deployment. Lingering incompatible assemblies can result in a `System.BadImageFormatException` exception when deploying an upgraded app.

## URI endpoint wrong or stopped website

* **Browser:** ERR\_CONNECTION\_REFUSED **--OR--** Unable to connect

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** The log file isn't created.

Troubleshooting:

* Confirm the correct URI endpoint for the app is in use. Check the bindings.

* Confirm that the IIS website isn't in the *Stopped* state.

## CoreWebEngine or W3SVC server features disabled

**OS Exception:** The IIS 7.0 CoreWebEngine and W3SVC features must be installed to use the ASP.NET Core Module.

Troubleshooting:

Confirm that the proper role and features are enabled. See [IIS Configuration](iis/?view=aspnetcore-10.0#iis-configuration).

## Incorrect website physical path or app missing

* **Browser:** 403 Forbidden - Access is denied **--OR--** 403.14 Forbidden - The Web server is configured to not list the contents of this directory.

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** The log file isn't created.

Troubleshooting:

Check the IIS website **Basic Settings** and the physical app folder. Confirm that the app is in the folder at the IIS website **Physical path**.

## Incorrect role, ASP.NET Core Module not installed, or incorrect permissions

* **Browser:** 500.19 Internal Server Error - The requested page cannot be accessed because the related configuration data for the page is invalid. **--OR--** This page can't be displayed

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** The log file isn't created.

Troubleshooting:

* Confirm that the proper role is enabled. See [IIS Configuration](iis/?view=aspnetcore-10.0#iis-configuration).

* Open **Programs & Features** or **Apps & features** and confirm that **Windows Server Hosting** is installed. If **Windows Server Hosting** isn't present in the list of installed programs, download and install the .NET Hosting Bundle.

  [Current .NET Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

  For more information, see [Install the .NET Hosting Bundle](iis/?view=aspnetcore-10.0#install-the-net-hosting-bundle).

* Make sure that the **Application Pool** > **Process Model** > **Identity** is set to **ApplicationPoolIdentity** or the custom identity has the correct permissions to access the app's deployment folder.

* If you uninstalled the ASP.NET Core Hosting Bundle and installed an earlier version of the hosting bundle, the *applicationHost.config* file doesn't include a section for the ASP.NET Core Module. Open *applicationHost.config* at *%windir%/System32/inetsrv/config* and find the `<configuration><configSections><sectionGroup name="system.webServer">` section group. If the section for the ASP.NET Core Module is missing from the section group, add the section element:

  ```
  <section name="aspNetCore" overrideModeDefault="Allow" />
  ```

  Alternatively, install the latest version of the ASP.NET Core Hosting Bundle. The latest version is backwards-compatible with supported ASP.NET Core apps.

## Incorrect processPath, missing PATH variable, Hosting Bundle not installed, system/IIS not restarted, VC++ Redistributable not installed, or dotnet.exe access violation

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure

* **Application Log:** Application 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY}' with physical root 'C:{PATH}' failed to start process with commandline '"{...}" ', ErrorCode = '0x80070002 : 0. Application '{PATH}' wasn't able to start. Executable was not found at '{PATH}'. Failed to start application '/LM/W3SVC/2/ROOT', ErrorCode '0x8007023e'.

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** Event Log: 'Application '{PATH}' wasn't able to start. Executable was not found at '{PATH}'. Failed HRESULT returned: 0x8007023e

Troubleshooting:

* Confirm that the app runs locally on Kestrel. A process failure might be the result of a problem within the app. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

* Check the *processPath* attribute on the `<aspNetCore>` element in *web.config* to confirm that it's `dotnet` for a framework-dependent deployment (FDD) or `.\{ASSEMBLY}.exe` for a [self-contained deployment (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).

* For an FDD, *dotnet.exe* might not be accessible via the PATH settings. Confirm that *C:\Program Files\dotnet\\* exists in the System PATH settings.

* For an FDD, *dotnet.exe* might not be accessible for the user identity of the app pool. Confirm that the app pool user identity has access to the *C:\Program Files\dotnet* directory. Confirm that there are no deny rules configured for the app pool user identity on the *C:\Program Files\dotnet* and app directories.

* An FDD may have been deployed and .NET installed without restarting IIS. Either restart the server or restart IIS by executing **net stop was /y** followed by **net start w3svc** from a command prompt.

* An FDD may have been deployed without installing the .NET runtime on the hosting system. If the .NET runtime hasn't been installed, run the **.NET Hosting Bundle installer** on the system.

  [Current .NET Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

  For more information, see [Install the .NET Hosting Bundle](iis/?view=aspnetcore-10.0#install-the-net-hosting-bundle).

  If a specific runtime is required, download the runtime from the [.NET Downloads](https://dotnet.microsoft.com/download/dotnet) page and install it on the system. Complete the installation by restarting the system or restarting IIS by executing **net stop was /y** followed by **net start w3svc** from a command prompt.

## Incorrect arguments of \<aspNetCore> element

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure

* **Application Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine. Could not find inprocess request handler. Captured output from invoking hostfxr: Did you mean to run dotnet SDK commands? Please install dotnet SDK from: <https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409> Failed to start application '/LM/W3SVC/3/ROOT', ErrorCode '0x8000ffff'.

* **ASP.NET Core Module stdout Log:** Did you mean to run dotnet SDK commands? Please install dotnet SDK from: <https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409>

* **ASP.NET Core Module Debug Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine. Failed HRESULT returned: 0x8000ffff Could not find inprocess request handler. Captured output from invoking hostfxr: Did you mean to run dotnet SDK commands? Please install dotnet SDK from: <https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409> Failed HRESULT returned: 0x8000ffff

Troubleshooting:

* Confirm that the app runs locally on Kestrel. A process failure might be the result of a problem within the app. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

* Examine the *arguments* attribute on the `<aspNetCore>` element in *web.config* to confirm that it's either (a) `.\{ASSEMBLY}.dll` for a framework-dependent deployment (FDD); or (b) not present, an empty string (`arguments=""`), or a list of the app's arguments (`arguments="{ARGUMENT_1}, {ARGUMENT_2}, ... {ARGUMENT_X}"`) for a self-contained deployment (SCD).

## Missing .NET shared framework

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure

* **Application Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine. Could not find inprocess request handler. Captured output from invoking hostfxr: It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}' was not found.

Failed to start application '/LM/W3SVC/5/ROOT', ErrorCode '0x8000ffff'.

* **ASP.NET Core Module stdout Log:** It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}' was not found.

* **ASP.NET Core Module Debug Log:** Failed HRESULT returned: 0x8000ffff

Troubleshooting:

For a framework-dependent deployment (FDD), confirm that the correct runtime installed on the system.

## Stopped Application Pool

* **Browser:** 503 Service Unavailable

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** The log file isn't created.

Troubleshooting:

Confirm that the Application Pool isn't in the *Stopped* state.

## Sub-application includes a \<handlers> section

* **Browser:** HTTP Error 500.19 - Internal Server Error

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The root app's log file is created and shows normal operation. The sub-app's log file isn't created.

* **ASP.NET Core Module Debug Log:** The root app's log file is created and shows normal operation. The sub-app's log file isn't created.

Troubleshooting:

Confirm that the sub-app's *web.config* file doesn't include a `<handlers>` section or that the sub-app doesn't inherit the parent app's handlers.

The parent app's `<system.webServer>` section of *web.config* is placed inside of a `<location>` element. The [InheritInChildApplications](/en-us/dotnet/api/system.configuration.sectioninformation.inheritinchildapplications) property is set to `false` to indicate that the settings specified within the [\<location>](/en-us/iis/manage/managing-your-configuration-settings/understanding-iis-configuration-delegation#the-concept-of-location) element aren't inherited by apps that reside in a subdirectory of the parent app. For more information, see [ASP.NET Core Module (ANCM) for IIS](aspnet-core-module?view=aspnetcore-10.0).

## stdout log path incorrect

* **Browser:** The app responds normally.

* **Application Log:** Could not start stdout redirection in C:\Program Files\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Exception message: HRESULT 0x80070005 returned at {PATH}\aspnetcoremodulev2\commonlib\fileoutputmanager.cpp:84. Could not stop stdout redirection in C:\Program Files\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Exception message: HRESULT 0x80070002 returned at {PATH}. Could not start stdout redirection in {PATH}\aspnetcorev2\_inprocess.dll.

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module debug Log:** Could not start stdout redirection in C:\Program Files\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Exception message: HRESULT 0x80070005 returned at {PATH}\aspnetcoremodulev2\commonlib\fileoutputmanager.cpp:84. Could not stop stdout redirection in C:\Program Files\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Exception message: HRESULT 0x80070002 returned at {PATH}. Could not start stdout redirection in {PATH}\aspnetcorev2\_inprocess.dll.

Troubleshooting:

* The `stdoutLogFile` path specified in the `<aspNetCore>` element of *web.config* doesn't exist. For more information, see [ASP.NET Core Module: Log creation and redirection](aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

* The app pool user doesn't have write access to the stdout log path.

## Application configuration general issue

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure **--OR--** HTTP Error 500.30 - ANCM In-Process Start Failure

* **Application Log:** Variable

* **ASP.NET Core Module stdout Log:** The log file is created but empty or created with normal entries until the point of the app failing.

* **ASP.NET Core Module Debug Log:** Variable

Troubleshooting:

The process failed to start, most likely due to an app configuration or programming issue.

For more information, see the following articles:

* [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)

This topic describes the most common errors and provides troubleshooting advice when hosting ASP.NET Core apps on Azure Apps Service and IIS.

See [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0) information on common app startup errors and instructions on how to diagnose errors.

Collect the following information:

* Browser behavior such as status code and error message.

* Application Event Log entries

  * Azure App Service: See [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

  * IIS

    1. Select **Start** on the **Windows** menu, type *Event Viewer*, and press **Enter**.
    2. After the **Event Viewer** opens, expand **Windows Logs** > **Application** in the sidebar.

* ASP.NET Core Module stdout and debug log entries

  * Azure App Service: See [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).
  * IIS: Follow the instructions in the [Log creation and redirection](aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection) and [Enhanced diagnostic logs](iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs) sections of the ASP.NET Core Module topic.

Compare error information to the following common errors. If a match is found, follow the troubleshooting advice.

The list of errors in this topic isn't exhaustive. If you encounter an error not listed here, open a new issue using the **Content feedback** button at the bottom of this topic with detailed instructions on how to reproduce the error.

Important

**ASP.NET Core preview releases with Azure App Service**

ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core preview release, see [Deploy ASP.NET Core preview release to Azure App Service](azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service).

## OS upgrade removed the 32-bit ASP.NET Core Module

**Application Log:** The Module DLL **C:\WINDOWS\system32\inetsrv\aspnetcore.dll** failed to load. The data is the error.

Troubleshooting:

Non-OS files in the **C:\Windows\SysWOW64\inetsrv** directory aren't preserved during an OS upgrade. If the ASP.NET Core Module is installed prior to an OS upgrade and then any app pool is run in 32-bit mode after an OS upgrade, this issue is encountered. After an OS upgrade, repair the ASP.NET Core Module. See [Install the .NET Core Hosting bundle](iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle). Select **Repair** when the installer is run.

## Missing site extension, 32-bit (x86) and 64-bit (x64) site extensions installed, or wrong process bitness set

*Applies to apps hosted by Azure App Services.*

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure

* **Application Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. Could not find inprocess request handler. Captured output from invoking hostfxr: It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}-preview-\*' was not found. Failed to start application '/LM/W3SVC/1416782824/ROOT', ErrorCode '0x8000ffff'.

* **ASP.NET Core Module stdout Log:** It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}-preview-\*' was not found.

* **ASP.NET Core Module Debug Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine. Failed HRESULT returned: 0x8000ffff. Could not find inprocess request handler. It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}-preview-\*' was not found.

Troubleshooting:

* If running the app on a preview runtime, install either the 32-bit (x86) **or** 64-bit (x64) site extension that matches the bitness of the app and the app's runtime version. **Don't install both extensions or multiple runtime versions of the extension.**

  * ASP.NET Core {RUNTIME VERSION} (x86) Runtime
  * ASP.NET Core {RUNTIME VERSION} (x64) Runtime

  Restart the app. Wait several seconds for the app to restart.

* If running the app on a preview runtime and both the 32-bit (x86) and 64-bit (x64) [site extensions](azure-apps/?view=aspnetcore-10.0#install-the-preview-site-extension) are installed, uninstall the site extension that doesn't match the bitness of the app. After removing the site extension, restart the app. Wait several seconds for the app to restart.

* If running the app on a preview runtime and the site extension's bitness matches that of the app, confirm that the preview site extension's *runtime version* matches the app's runtime version.

* Confirm that the app's **Platform** in **Application Settings** matches the bitness of the app.

For more information, see [Deploy ASP.NET Core apps to Azure App Service](azure-apps/?view=aspnetcore-10.0#install-the-preview-site-extension).

## An x86 app is deployed but the app pool isn't enabled for 32-bit apps

* **Browser:** HTTP Error 500.30 - ANCM In-Process Start Failure

* **Application Log:** Application '/LM/W3SVC/5/ROOT' with physical root '{PATH}' hit unexpected managed exception, exception code = '0xe0434352'. Please check the stderr logs for more information. Application '/LM/W3SVC/5/ROOT' with physical root '{PATH}' failed to load clr and managed application. CLR worker thread exited prematurely

* **ASP.NET Core Module stdout Log:** The log file is created but empty.

* **ASP.NET Core Module Debug Log:** Failed HRESULT returned: 0x8007023e

This scenario is trapped by the SDK when publishing a self-contained app. The SDK produces an error if the RID doesn't match the platform target (for example, `win10-x64` RID with `<PlatformTarget>x86</PlatformTarget>` in the project file).

Troubleshooting:

For an x86 framework-dependent deployment (`<PlatformTarget>x86</PlatformTarget>`), enable the IIS app pool for 32-bit apps. In IIS Manager, open the app pool's **Advanced Settings** and set **Enable 32-Bit Applications** to **True**.

## Platform conflicts with RID

* **Browser:** HTTP Error 502.5 - Process Failure

* **Application Log:** Application 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY}' with physical root 'C:{PATH}' failed to start process with commandline '"C:{PATH}{ASSEMBLY}.{exe|dll}" ', ErrorCode = '0x80004005 : ff.

* **ASP.NET Core Module stdout Log:** Unhandled Exception: System.BadImageFormatException: Could not load file or assembly '{ASSEMBLY}.dll'. An attempt was made to load a program with an incorrect format.

Troubleshooting:

* Confirm that the app runs locally on Kestrel. A process failure might be the result of a problem within the app. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

* If this exception occurs for an Azure Apps deployment when upgrading an app and deploying newer assemblies, manually delete all files from the prior deployment. Lingering incompatible assemblies can result in a `System.BadImageFormatException` exception when deploying an upgraded app.

## URI endpoint wrong or stopped website

* **Browser:** ERR\_CONNECTION\_REFUSED **--OR--** Unable to connect

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** The log file isn't created.

Troubleshooting:

* Confirm the correct URI endpoint for the app is in use. Check the bindings.

* Confirm that the IIS website isn't in the *Stopped* state.

## CoreWebEngine or W3SVC server features disabled

**OS Exception:** The IIS 7.0 CoreWebEngine and W3SVC features must be installed to use the ASP.NET Core Module.

Troubleshooting:

Confirm that the proper role and features are enabled. See [IIS Configuration](iis/?view=aspnetcore-10.0#iis-configuration).

## Incorrect website physical path or app missing

* **Browser:** 403 Forbidden - Access is denied **--OR--** 403.14 Forbidden - The Web server is configured to not list the contents of this directory.

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** The log file isn't created.

Troubleshooting:

Check the IIS website **Basic Settings** and the physical app folder. Confirm that the app is in the folder at the IIS website **Physical path**.

## Incorrect role, ASP.NET Core Module not installed, or incorrect permissions

* **Browser:** 500.19 Internal Server Error - The requested page cannot be accessed because the related configuration data for the page is invalid. **--OR--** This page can't be displayed

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** The log file isn't created.

Troubleshooting:

* Confirm that the proper role is enabled. See [IIS Configuration](iis/?view=aspnetcore-10.0#iis-configuration).

* Open **Programs & Features** or **Apps & features** and confirm that **Windows Server Hosting** is installed. If **Windows Server Hosting** isn't present in the list of installed programs, download and install the .NET Core Hosting Bundle.

  [Current .NET Core Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

  For more information, see [Install the .NET Core Hosting Bundle](iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle).

* Make sure that the **Application Pool** > **Process Model** > **Identity** is set to **ApplicationPoolIdentity** or the custom identity has the correct permissions to access the app's deployment folder.

* If you uninstalled the ASP.NET Core Hosting Bundle and installed an earlier version of the hosting bundle, the *applicationHost.config* file doesn't include a section for the ASP.NET Core Module. Open *applicationHost.config* at *%windir%/System32/inetsrv/config* and find the `<configuration><configSections><sectionGroup name="system.webServer">` section group. If the section for the ASP.NET Core Module is missing from the section group, add the section element:

  ```
  <section name="aspNetCore" overrideModeDefault="Allow" />
  ```

  Alternatively, install the latest version of the ASP.NET Core Hosting Bundle. The latest version is backwards-compatible with supported ASP.NET Core apps.

## Incorrect processPath, missing PATH variable, Hosting Bundle not installed, system/IIS not restarted, VC++ Redistributable not installed, or dotnet.exe access violation

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure

* **Application Log:** Application 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY}' with physical root 'C:{PATH}' failed to start process with commandline '"{...}" ', ErrorCode = '0x80070002 : 0. Application '{PATH}' wasn't able to start. Executable was not found at '{PATH}'. Failed to start application '/LM/W3SVC/2/ROOT', ErrorCode '0x8007023e'.

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** Event Log: 'Application '{PATH}' wasn't able to start. Executable was not found at '{PATH}'. Failed HRESULT returned: 0x8007023e

Troubleshooting:

* Confirm that the app runs locally on Kestrel. A process failure might be the result of a problem within the app. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

* Check the *processPath* attribute on the `<aspNetCore>` element in *web.config* to confirm that it's `dotnet` for a framework-dependent deployment (FDD) or `.\{ASSEMBLY}.exe` for a [self-contained deployment (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).

* For an FDD, *dotnet.exe* might not be accessible via the PATH settings. Confirm that *C:\Program Files\dotnet\\* exists in the System PATH settings.

* For an FDD, *dotnet.exe* might not be accessible for the user identity of the app pool. Confirm that the app pool user identity has access to the *C:\Program Files\dotnet* directory. Confirm that there are no deny rules configured for the app pool user identity on the *C:\Program Files\dotnet* and app directories.

* An FDD may have been deployed and .NET Core installed without restarting IIS. Either restart the server or restart IIS by executing **net stop was /y** followed by **net start w3svc** from a command prompt.

* An FDD may have been deployed without installing the .NET Core runtime on the hosting system. If the .NET Core runtime hasn't been installed, run the **.NET Core Hosting Bundle installer** on the system.

  [Current .NET Core Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

  For more information, see [Install the .NET Core Hosting Bundle](iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle).

  If a specific runtime is required, download the runtime from the [.NET Downloads](https://dotnet.microsoft.com/download/dotnet) page and install it on the system. Complete the installation by restarting the system or restarting IIS by executing **net stop was /y** followed by **net start w3svc** from a command prompt.

## Incorrect arguments of \<aspNetCore> element

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure

* **Application Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine. Could not find inprocess request handler. Captured output from invoking hostfxr: Did you mean to run dotnet SDK commands? Please install dotnet SDK from: <https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409> Failed to start application '/LM/W3SVC/3/ROOT', ErrorCode '0x8000ffff'.

* **ASP.NET Core Module stdout Log:** Did you mean to run dotnet SDK commands? Please install dotnet SDK from: <https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409>

* **ASP.NET Core Module Debug Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine. Failed HRESULT returned: 0x8000ffff Could not find inprocess request handler. Captured output from invoking hostfxr: Did you mean to run dotnet SDK commands? Please install dotnet SDK from: <https://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409> Failed HRESULT returned: 0x8000ffff

Troubleshooting:

* Confirm that the app runs locally on Kestrel. A process failure might be the result of a problem within the app. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

* Examine the *arguments* attribute on the `<aspNetCore>` element in *web.config* to confirm that it's either (a) `.\{ASSEMBLY}.dll` for a framework-dependent deployment (FDD); or (b) not present, an empty string (`arguments=""`), or a list of the app's arguments (`arguments="{ARGUMENT_1}, {ARGUMENT_2}, ... {ARGUMENT_X}"`) for a self-contained deployment (SCD).

## Missing .NET Core shared framework

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure

* **Application Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine. Could not find inprocess request handler. Captured output from invoking hostfxr: It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}' was not found.

Failed to start application '/LM/W3SVC/5/ROOT', ErrorCode '0x8000ffff'.

* **ASP.NET Core Module stdout Log:** It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}' was not found.

* **ASP.NET Core Module Debug Log:** Failed HRESULT returned: 0x8000ffff

Troubleshooting:

For a framework-dependent deployment (FDD), confirm that the correct runtime installed on the system.

## Stopped Application Pool

* **Browser:** 503 Service Unavailable

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module Debug Log:** The log file isn't created.

Troubleshooting:

Confirm that the Application Pool isn't in the *Stopped* state.

## Sub-application includes a \<handlers> section

* **Browser:** HTTP Error 500.19 - Internal Server Error

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The root app's log file is created and shows normal operation. The sub-app's log file isn't created.

* **ASP.NET Core Module Debug Log:** The root app's log file is created and shows normal operation. The sub-app's log file isn't created.

Troubleshooting:

Confirm that the sub-app's *web.config* file doesn't include a `<handlers>` section or that the sub-app doesn't inherit the parent app's handlers.

The parent app's `<system.webServer>` section of *web.config* is placed inside of a `<location>` element. The [InheritInChildApplications](/en-us/dotnet/api/system.configuration.sectioninformation.inheritinchildapplications) property is set to `false` to indicate that the settings specified within the [\<location>](/en-us/iis/manage/managing-your-configuration-settings/understanding-iis-configuration-delegation#the-concept-of-location) element aren't inherited by apps that reside in a subdirectory of the parent app. For more information, see [ASP.NET Core Module (ANCM) for IIS](aspnet-core-module?view=aspnetcore-10.0).

## stdout log path incorrect

* **Browser:** The app responds normally.

* **Application Log:** Could not start stdout redirection in C:\Program Files\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Exception message: HRESULT 0x80070005 returned at {PATH}\aspnetcoremodulev2\commonlib\fileoutputmanager.cpp:84. Could not stop stdout redirection in C:\Program Files\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Exception message: HRESULT 0x80070002 returned at {PATH}. Could not start stdout redirection in {PATH}\aspnetcorev2\_inprocess.dll.

* **ASP.NET Core Module stdout Log:** The log file isn't created.

* **ASP.NET Core Module debug Log:** Could not start stdout redirection in C:\Program Files\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Exception message: HRESULT 0x80070005 returned at {PATH}\aspnetcoremodulev2\commonlib\fileoutputmanager.cpp:84. Could not stop stdout redirection in C:\Program Files\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll. Exception message: HRESULT 0x80070002 returned at {PATH}. Could not start stdout redirection in {PATH}\aspnetcorev2\_inprocess.dll.

Troubleshooting:

* The `stdoutLogFile` path specified in the `<aspNetCore>` element of *web.config* doesn't exist. For more information, see [ASP.NET Core Module: Log creation and redirection](aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

* The app pool user doesn't have write access to the stdout log path.

## Application configuration general issue

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure **--OR--** HTTP Error 500.30 - ANCM In-Process Start Failure

* **Application Log:** Variable

* **ASP.NET Core Module stdout Log:** The log file is created but empty or created with normal entries until the point of the app failing.

* **ASP.NET Core Module Debug Log:** Variable

Troubleshooting:

The process failed to start, most likely due to an app configuration or programming issue.

For more information, see the following topics:

* [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)

This topic describes common errors and provides troubleshooting advice for specific errors when hosting ASP.NET Core apps on Azure Apps Service and IIS.

For general troubleshooting guidance, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

Collect the following information:

* Browser behavior (status code and error message)

* Application Event Log entries

  * Azure App Service: See [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

  * IIS

    1. Select **Start** on the **Windows** menu, type *Event Viewer*, and press **Enter**.
    2. After the **Event Viewer** opens, expand **Windows Logs** > **Application** in the sidebar.

* ASP.NET Core Module stdout and debug log entries

  * Azure App Service: See [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).
  * IIS: Follow the instructions in the [Log creation and redirection](aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection) and [Enhanced diagnostic logs](iis/logging-and-diagnostics?view=aspnetcore-10.0#enhanced-diagnostic-logs) sections of the ASP.NET Core Module topic.

Compare error information to the following common errors. If a match is found, follow the troubleshooting advice.

The list of errors in this topic isn't exhaustive. If you encounter an error not listed here, open a new issue using the **Content feedback** button at the bottom of this topic with detailed instructions on how to reproduce the error.

Important

**ASP.NET Core preview releases with Azure App Service**

ASP.NET Core preview releases aren't deployed to Azure App Service by default. To host an app that uses an ASP.NET Core preview release, see [Deploy ASP.NET Core preview release to Azure App Service](azure-apps/?view=aspnetcore-10.0#deploy-aspnet-core-preview-release-to-azure-app-service).

## OS upgrade removed the 32-bit ASP.NET Core Module

**Application Log:** The Module DLL **C:\WINDOWS\system32\inetsrv\aspnetcore.dll** failed to load. The data is the error.

Troubleshooting:

Non-OS files in the **C:\Windows\SysWOW64\inetsrv** directory aren't preserved during an OS upgrade. If the ASP.NET Core Module is installed prior to an OS upgrade and then any app pool is run in 32-bit mode after an OS upgrade, this issue is encountered. After an OS upgrade, repair the ASP.NET Core Module. See [Install the .NET Core Hosting bundle](iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle). Select **Repair** when the installer is run.

## Missing site extension, 32-bit (x86) and 64-bit (x64) site extensions installed, or wrong process bitness set

*Applies to apps hosted by Azure App Services.*

* **Browser:** HTTP Error 500.0 - ANCM In-Process Handler Load Failure

* **Application Log:** Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. Could not find inprocess request handler. Captured output from invoking hostfxr: It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}-preview-\*' was not found. Failed to start application '/LM/W3SVC/1416782824/ROOT', ErrorCode '0x8000ffff'.

* **ASP.NET Core Module stdout Log:** It was not possible to find any compatible framework version. The specified framework 'Microsoft.AspNetCore.App', version '{VERSION}-preview-\*' was not found.

Troubleshooting:

* If running the app on a preview runtime, install either the 32-bit (x86) **or** 64-bit (x64) site extension that matches the bitness of the app and the app's runtime version. **Don't install both extensions or multiple runtime versions of the extension.**

  * ASP.NET Core {RUNTIME VERSION} (x86) Runtime
  * ASP.NET Core {RUNTIME VERSION} (x64) Runtime

  Restart the app. Wait several seconds for the app to restart.

* If running the app on a preview runtime and both the 32-bit (x86) and 64-bit (x64) [site extensions](azure-apps/?view=aspnetcore-10.0#install-the-preview-site-extension) are installed, uninstall the site extension that doesn't match the bitness of the app. After removing the site extension, restart the app. Wait several seconds for the app to restart.

* If running the app on a preview runtime and the site extension's bitness matches that of the app, confirm that the preview site extension's *runtime version* matches the app's runtime version.

* Confirm that the app's **Platform** in **Application Settings** matches the bitness of the app.

For more information, see [Deploy ASP.NET Core apps to Azure App Service](azure-apps/?view=aspnetcore-10.0#install-the-preview-site-extension).

## An x86 app is deployed but the app pool isn't enabled for 32-bit apps

* **Browser:** HTTP Error 500.30 - ANCM In-Process Start Failure

* **Application Log:** Application '/LM/W3SVC/5/ROOT' with physical root '{PATH}' hit unexpected managed exception, exception code = '0xe0434352'. Please check the stderr logs for more information. Application '/LM/W3SVC/5/ROOT' with physical root '{PATH}' failed to load clr and managed application. CLR worker thread exited prematurely

* **ASP.NET Core Module stdout Log:** The log file is created but empty.

This scenario is trapped by the SDK when publishing a self-contained app. The SDK produces an error if the RID doesn't match the platform target (for example, `win10-x64` RID with `<PlatformTarget>x86</PlatformTarget>` in the project file).

Troubleshooting:

For an x86 framework-dependent deployment (`<PlatformTarget>x86</PlatformTarget>`), enable the IIS app pool for 32-bit apps. In IIS Manager, open the app pool's **Advanced Settings** and set **Enable 32-Bit Applications** to **True**.

## Platform conflicts with RID

* **Browser:** HTTP Error 502.5 - Process Failure

* **Application Log:** Application 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY}' with physical root 'C:{PATH}' failed to start process with commandline '"C:{PATH}{ASSEMBLY}.{exe|dll}" ', ErrorCode = '0x80004005 : ff.

* **ASP.NET Core Module stdout Log:** Unhandled Exception: System.BadImageFormatException: Could not load file or assembly '{ASSEMBLY}.dll'. An attempt was made to load a program with an incorrect format.

Troubleshooting:

* Confirm that the app runs locally on Kestrel. A process failure might be the result of a problem within the app. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

* If this exception occurs for an Azure Apps deployment when upgrading an app and deploying newer assemblies, manually delete all files from the prior deployment. Lingering incompatible assemblies can result in a `System.BadImageFormatException` exception when deploying an upgraded app.

## URI endpoint wrong or stopped website

* **Browser:** ERR\_CONNECTION\_REFUSED **--OR--** Unable to connect

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

Troubleshooting:

* Confirm the correct URI endpoint for the app is in use. Check the bindings.

* Confirm that the IIS website isn't in the *Stopped* state.

## CoreWebEngine or W3SVC server features disabled

**OS Exception:** The IIS 7.0 CoreWebEngine and W3SVC features must be installed to use the ASP.NET Core Module.

Troubleshooting:

Confirm that the proper role and features are enabled. See [IIS Configuration](iis/?view=aspnetcore-10.0#iis-configuration).

## Incorrect website physical path or app missing

* **Browser:** 403 Forbidden - Access is denied **--OR--** 403.14 Forbidden - The Web server is configured to not list the contents of this directory.

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

Troubleshooting:

Check the IIS website **Basic Settings** and the physical app folder. Confirm that the app is in the folder at the IIS website **Physical path**.

## Incorrect role, ASP.NET Core Module not installed, or incorrect permissions

* **Browser:** 500.19 Internal Server Error - The requested page cannot be accessed because the related configuration data for the page is invalid. **--OR--** This page can't be displayed

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

Troubleshooting:

* Confirm that the proper role is enabled. See [IIS Configuration](iis/?view=aspnetcore-10.0#iis-configuration).

* Open **Programs & Features** or **Apps & features** and confirm that **Windows Server Hosting** is installed. If **Windows Server Hosting** isn't present in the list of installed programs, download and install the .NET Core Hosting Bundle.

  [Current .NET Core Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

  For more information, see [Install the .NET Core Hosting Bundle](iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle).

* Make sure that the **Application Pool** > **Process Model** > **Identity** is set to **ApplicationPoolIdentity** or the custom identity has the correct permissions to access the app's deployment folder.

* If you uninstalled the ASP.NET Core Hosting Bundle and installed an earlier version of the hosting bundle, the *applicationHost.config* file doesn't include a section for the ASP.NET Core Module. Open *applicationHost.config* at *%windir%/System32/inetsrv/config* and find the `<configuration><configSections><sectionGroup name="system.webServer">` section group. If the section for the ASP.NET Core Module is missing from the section group, add the section element:

  ```
  <section name="aspNetCore" overrideModeDefault="Allow" />
  ```

  Alternatively, install the latest version of the ASP.NET Core Hosting Bundle. The latest version is backwards-compatible with supported ASP.NET Core apps.

## Incorrect processPath, missing PATH variable, Hosting Bundle not installed, system/IIS not restarted, VC++ Redistributable not installed, or dotnet.exe access violation

* **Browser:** HTTP Error 502.5 - Process Failure

* **Application Log:** Application 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY}' with physical root 'C:{PATH}' failed to start process with commandline '"{...}" ', ErrorCode = '0x80070002 : 0.

* **ASP.NET Core Module stdout Log:** The log file is created but empty.

Troubleshooting:

* Confirm that the app runs locally on Kestrel. A process failure might be the result of a problem within the app. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

* Check the *processPath* attribute on the `<aspNetCore>` element in *web.config* to confirm that it's `dotnet` for a framework-dependent deployment (FDD) or `.\{ASSEMBLY}.exe` for a [self-contained deployment (SCD)](/en-us/dotnet/core/deploying/#self-contained-deployments-scd).

* For an FDD, *dotnet.exe* might not be accessible via the PATH settings. Confirm that *C:\Program Files\dotnet\\* exists in the System PATH settings.

* For an FDD, *dotnet.exe* might not be accessible for the user identity of the app pool. Confirm that the app pool user identity has access to the *C:\Program Files\dotnet* directory. Confirm that there are no deny rules configured for the app pool user identity on the *C:\Program Files\dotnet* and app directories.

* An FDD may have been deployed and .NET Core installed without restarting IIS. Either restart the server or restart IIS by executing **net stop was /y** followed by **net start w3svc** from a command prompt.

* An FDD may have been deployed without installing the .NET Core runtime on the hosting system. If the .NET Core runtime hasn't been installed, run the **.NET Core Hosting Bundle installer** on the system.

  [Current .NET Core Hosting Bundle installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer)

  For more information, see [Install the .NET Core Hosting Bundle](iis/?view=aspnetcore-10.0#install-the-net-core-hosting-bundle).

  If a specific runtime is required, download the runtime from the [.NET Downloads](https://dotnet.microsoft.com/download/dotnet) page and install it on the system. Complete the installation by restarting the system or restarting IIS by executing **net stop was /y** followed by **net start w3svc** from a command prompt.

## Incorrect arguments of \<aspNetCore> element

* **Browser:** HTTP Error 502.5 - Process Failure

* **Application Log:** Application 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY}' with physical root 'C:{PATH}' failed to start process with commandline '"dotnet" .{ASSEMBLY}.dll', ErrorCode = '0x80004005 : 80008081.

* **ASP.NET Core Module stdout Log:** The application to execute does not exist: 'PATH{ASSEMBLY}.dll'

Troubleshooting:

* Confirm that the app runs locally on Kestrel. A process failure might be the result of a problem within the app. For more information, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

* Examine the *arguments* attribute on the `<aspNetCore>` element in *web.config* to confirm that it's either (a) `.\{ASSEMBLY}.dll` for a framework-dependent deployment (FDD); or (b) not present, an empty string (`arguments=""`), or a list of the app's arguments (`arguments="{ARGUMENT_1}, {ARGUMENT_2}, ... {ARGUMENT_X}"`) for a self-contained deployment (SCD).

Troubleshooting:

For a framework-dependent deployment (FDD), confirm that the correct runtime installed on the system.

## Stopped Application Pool

* **Browser:** 503 Service Unavailable

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The log file isn't created.

Troubleshooting:

Confirm that the Application Pool isn't in the *Stopped* state.

## Sub-application includes a \<handlers> section

* **Browser:** HTTP Error 500.19 - Internal Server Error

* **Application Log:** No entry

* **ASP.NET Core Module stdout Log:** The root app's log file is created and shows normal operation. The sub-app's log file isn't created.

Troubleshooting:

Confirm that the sub-app's *web.config* file doesn't include a `<handlers>` section.

## stdout log path incorrect

* **Browser:** The app responds normally.

* **Application Log:** Warning: Could not create stdoutLogFile \\?{PATH}\path\_doesnt\_exist\stdout\_{PROCESS ID}\_{TIMESTAMP}.log, ErrorCode = -2147024893.

* **ASP.NET Core Module stdout Log:** The log file isn't created.

Troubleshooting:

* The `stdoutLogFile` path specified in the `<aspNetCore>` element of *web.config* doesn't exist. For more information, see [ASP.NET Core Module: Log creation and redirection](aspnet-core-module?view=aspnetcore-10.0#log-creation-and-redirection).

* The app pool user doesn't have write access to the stdout log path.

## Application configuration general issue

* **Browser:** HTTP Error 502.5 - Process Failure

* **Application Log:** Application 'MACHINE/WEBROOT/APPHOST/{ASSEMBLY}' with physical root 'C:{PATH}' created process with commandline '"C:{PATH}{ASSEMBLY}.{exe|dll}" ' but either crashed or did not respond or did not listen on the given port '{PORT}', ErrorCode = '{ERROR CODE}'

* **ASP.NET Core Module stdout Log:** The log file is created but empty.

Troubleshooting:

The process failed to start, most likely due to an app configuration or programming issue.

For more information, see the following topics:

* [Troubleshoot ASP.NET Core on Azure App Service and IIS](../test/troubleshoot-azure-iis?view=aspnetcore-10.0)
* [Troubleshoot and debug ASP.NET Core projects](../test/troubleshoot?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/twitter-logins
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/social/twitter-logins.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Twitter external sign-in setup with ASP.NET Core

By [Valeriy Novytskyy](https://github.com/01binary) and [Rick Anderson](https://twitter.com/RickAndMSFT)

This sample shows how to enable users to [sign in with their Twitter account](https://dev.twitter.com/web/sign-in/desktop-browser) using a sample ASP.NET Core project created on the [previous page](./?view=aspnetcore-10.0).

Note

The Microsoft.AspNetCore.Authentication.Twitter package described below uses the OAuth 1.0 APIs provided by Twitter. Twitter has since added OAuth 2.0 APIs with a different set of functionality. The [OpenIddict](https://documentation.openiddict.com/integrations/web-providers) and [AspNet.Security.OAuth.Twitter](https://www.nuget.org/packages/AspNet.Security.OAuth.Twitter/) packages are community implementations that use the new OAuth 2.0 APIs.

## Create the app in Twitter

* Add the [Microsoft.AspNetCore.Authentication.Twitter](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Twitter) NuGet package to the project.

* Navigate to [twitter developer portal Dashboard](https://developer.twitter.com/en/portal/dashboard) and sign in. If you don't already have a Twitter account, use the **[Sign up now](https://twitter.com/signup)** link to create one.

* If you don't have a project, create one.

* Select **+ Add app**. Fill out the **App name** then record the generated API Key, API Key Secret and Bearer Token. These will be needed later.

* In the **App Settings** page, select **Edit** in the **Authentication settings** section, then:

  * Enable 3-legged OAuth
  * Request email address from users
  * Fill out the required fields and select **Save**

  Note

  Microsoft.AspNetCore.Identity requires users to have an email address by default. For Callback URLs during development, use `https://localhost:{PORT}/signin-twitter`, where the `{PORT}` placeholder is the app's port.

  Note

  The URI segment `/signin-twitter` is set as the default callback of the Twitter authentication provider. You can change the default callback URI while configuring the Twitter authentication middleware via the inherited [RemoteAuthenticationOptions.CallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.callbackpath) property of the [TwitterOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.twitter.twitteroptions) class.

## Store the Twitter consumer API key and secret

Store sensitive settings such as the Twitter consumer API key and secret with [Secret Manager](../../app-secrets?view=aspnetcore-10.0). For this sample, use the following steps:

1. Initialize the project for secret storage per the instructions at [Enable secret storage](../../app-secrets?view=aspnetcore-10.0#enable-secret-storage).

2. Store the sensitive settings in the local secret store with the secrets keys `Authentication:Twitter:ConsumerKey` and `Authentication:Twitter:ConsumerSecret`:

   ```
   dotnet user-secrets set "Authentication:Twitter:ConsumerAPIKey" "<consumer-api-key>"
   dotnet user-secrets set "Authentication:Twitter:ConsumerSecret" "<consumer-secret>"
   ```

The colon (`:`) separator doesn't work with environment variable hierarchical keys on all platforms. For example, [Bash](https://linuxhint.com/bash-environment-variables/) doesn't support colon (`:`) as a separator. All platforms support the double underscore (`__`) syntax and automatically replace it with a colon (`:`).

These tokens can be found on the **Keys and Access Tokens** tab after creating a new Twitter application:

## Configure Twitter Authentication

Add the Authentication service to the `Startup.ConfigureServices`:

```
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options =>
        options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();

    services.AddAuthentication().AddTwitter(twitterOptions =>
    {
        twitterOptions.ConsumerKey = Configuration["Authentication:Twitter:ConsumerAPIKey"];
        twitterOptions.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
        twitterOptions.RetrieveUserDetails = true;
    });

}
```

```
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;

services.AddAuthentication().AddTwitter(twitterOptions =>
    {
        twitterOptions.ConsumerKey = configuration["Authentication:Twitter:ConsumerAPIKey"];
        twitterOptions.ConsumerSecret = configuration["Authentication:Twitter:ConsumerSecret"];
    });
```

The [AddAuthentication(IServiceCollection, String)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-string\)) overload sets the [DefaultScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultscheme) property. The [AddAuthentication(IServiceCollection, Action\<AuthenticationOptions>)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection-system-action\(\(microsoft-aspnetcore-authentication-authenticationoptions\)\)\)) overload allows configuring authentication options, which can be used to set up default authentication schemes for different purposes. Subsequent calls to `AddAuthentication` override previously configured [AuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.authenticationoptions) properties.

[AuthenticationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder) extension methods that register an authentication handler may only be called once per authentication scheme. Overloads exist that allow configuring the scheme properties, scheme name, and display name.

For more information on configuration options supported by Twitter authentication, see the [TwitterOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.twitteroptions) API reference. This can be used to request different information about the user.

## Sign in with Twitter

Run the app and select **Log in**. An option to sign in with Twitter appears:

Selecting **Twitter** redirects to Twitter for authentication:

After entering your Twitter credentials, you are redirected back to the web site where you can set your email.

You are now logged in using your Twitter credentials:

## Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (`https`), host, and client IP address. Apps don't automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (`https`) results in the app generating incorrect insecure redirect URLs.

Use forwarded headers middleware to make the original request information available to the app for request processing.

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

## Troubleshooting

* **ASP.NET Core 2.x only:** If Identity isn't configured by calling `services.AddIdentity` in `ConfigureServices`, attempting to authenticate will result in *ArgumentException: The 'SignInScheme' option must be provided*. The project template used in this sample ensures Identity is configured.
* If the site database has not been created by applying the initial migration, you will get *A database operation failed while processing the request* error. Tap **Apply Migrations** to create the database and refresh to continue past the error.

## Next steps

* This article showed how you can authenticate with Twitter. You can follow a similar approach to authenticate with other providers listed on the [previous page](./?view=aspnetcore-10.0).

* Once you publish your web site to Azure web app, you should reset the `ConsumerSecret` in the Twitter developer portal.

* Set the `Authentication:Twitter:ConsumerKey` and `Authentication:Twitter:ConsumerSecret` as application settings in the Azure portal. The configuration system is set up to read keys from environment variables.

## Related content

[Multiple authentication providers](./?view=aspnetcore-10.0#multiple-authentication-providers)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0006
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0006.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0006: Do not use non-literal sequence numbers

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0006      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

An invocation on a method in [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) containing a sequence number that isn't a literal as a parameter.

## Rule description

Blazor's UI diffing algorithm relies on sequence numbers to determine which elements have changed. Computing the sequence number dynamically or using a counter can result in poor diffing performance. Instead, use a literal sequence number that maps to the source code line for the element. For example, the following code produces an error:

```
using Microsoft.AspNetCore.Components.Rendering;
var builder = new RenderTreeBuilder();
var seqNum = 1;
builder.OpenElement(seqNum, "div");
builder.CloseElement();
```

## How to fix violations

To fix a violation of this rule, make sure that calls to methods on the [RenderTreeBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.rendering.rendertreebuilder) class that take a sequence number as a parameter use a literal sequence number.

```
using Microsoft.AspNetCore.Components.Rendering;
var builder = new RenderTreeBuilder();
builder.OpenElement(0, "div");
builder.CloseElement();
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule. Using a non-literal sequence number can result in performance degradation.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0019
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0019.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0019: Suggest using IHeaderDictionary.Append or the indexer

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0019      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

`IDictionary.Add` isn't recommended for setting or appending headers. [`IDictionary.Add`](/en-us/dotnet/api/system.collections.idictionary.add) throws an `ArgumentException` when attempting to add a duplicate key.

## Rule description

`IDictionary.Add` isn't recommended for setting or appending headers.

## How to fix violations

To fix a violation of this rule, use `IHeaderDictionary.Append` or the indexer to append or set headers.

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/config-providers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/config-providers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP Extensibility: Configuration Providers

## Introduction

The [Basic Yarp Sample](https://github.com/microsoft/reverse-proxy/tree/main/samples/BasicYarpSample) show proxy configuration being loaded from appsettings.json. Instead proxy configuration can be loaded programmatically from the source of your choosing. You do this by providing a couple of classes implementing [IProxyConfigProvider](/en-us/dotnet/api/yarp.reverseproxy.configuration.iproxyconfigprovider) and [IProxyConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.iproxyconfig).

See [ReverseProxy.Code.Sample](https://github.com/microsoft/reverse-proxy/tree/main/samples/ReverseProxy.Code.Sample) for an example of a custom configuration provider.

Configuration can be modified during the load sequence using [Configuration Filters](config-filters?view=aspnetcore-10.0).

## Structure

[IProxyConfigProvider](/en-us/dotnet/api/yarp.reverseproxy.configuration.iproxyconfigprovider) has a single method `GetConfig()` that should return an [IProxyConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.iproxyconfig) instance. The IProxyConfig has lists of the current routes and clusters, as well as an `IChangeToken` to notify the proxy when this information is out of date and should be reloaded, which will cause `GetConfig()` to be called again.

### Routes

The routes section is an unordered collection of named routes. A route contains matches and their associated configuration. A route requires at least the following fields:

* RouteId - a unique name
* ClusterId - refers to the name of an entry in the clusters section.
* Match - contains either a Hosts array or a Path pattern string. Path is an ASP.NET Core route template that can be defined as [explained here](/en-us/aspnet/core/fundamentals/routing#route-templates).

[Headers](header-routing?view=aspnetcore-10.0), [Authorization](authn-authz?view=aspnetcore-10.0), [CORS](cors?view=aspnetcore-10.0), and other route based policies can be configured on each route entry. For additional fields see [RouteConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.routeconfig).

The proxy will apply the given matching criteria and policies, and then pass off the request to the specified cluster.

### Clusters

The clusters section is an unordered collection of named clusters. A cluster primarily contains a collection of named destinations and their addresses, any of which is considered capable of handling requests for a given route. The proxy will process the request according to the route and cluster configuration to select a destination.

For additional fields see [ClusterConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.clusterconfig).

## In Memory Config

The `InMemoryConfigProvider` implements `IProxyConfigProvider` and enables specifying routes and clusters directly in code by calling `LoadFromMemory`.

```
services.AddReverseProxy().LoadFromMemory(routes, clusters);
```

To update the config later, resolve the `InMemoryConfigProvider` from the services container and call `Update` with the new lists of routes and clusters.

```
httpContext.RequestServices.GetRequiredService<InMemoryConfigProvider>().Update(routes, clusters);
```

## Lifecycle

### Startup

The `IProxyConfigProvider` should be registered in the DI container as a singleton. At startup, the proxy will resolve this instance and call `GetConfig()`. On this first call the provider may choose to:

* Throw an exception if the provider cannot produce a valid proxy configuration for any reason. This will prevent the application from starting.
* Synchronously block while it loads the configuration. This will block the application from starting until valid route data is available.
* Or, it may choose to return an empty `IProxyConfig` instance while it loads the configuration in the background. The provider will need to trigger the `IChangeToken` when the configuration is available.

The proxy will validate the given configuration and if it's invalid, an exception will be thrown that prevents the application from starting. The provider can avoid this by using the [IConfigValidator](/en-us/dotnet/api/yarp.reverseproxy.configuration.iconfigvalidator) to pre-validate routes and clusters and take whatever action it deems appropriate such as excluding invalid entries.

### Atomicity

The configuration objects and collections supplied to the proxy should be read-only and not modified once they have been handed to the proxy via `GetConfig()`.

### Reload

If the `IChangeToken` supports `ActiveChangeCallbacks`, once the proxy has processed the initial set of configurations it will register a callback with this token. If the provider does not support callbacks then `HasChanged` will be polled every 5 minutes.

When the provider wants to provide a new configuration to the proxy it should:

* Load that configuration in the background.

  * Route and cluster objects are immutable, so new instances have to be created for any new data.
  * Objects for unchanged routes and clusters can be re-used, or new instances can be created - changes will be detected by diffing them.

* Optionally validate the configuration using the [IConfigValidator](/en-us/dotnet/api/yarp.reverseproxy.configuration.iconfigvalidator), and only then signal the `IChangeToken` from the prior `IProxyConfig` instance that new data is available. The proxy will call `GetConfig()` again to retrieve the new data.

There are important differences when reloading configuration vs the first configuration load.

* The new configuration will be diffed against the current one and only modified routes or clusters will be updated. The update will be applied atomically and will only affect new requests, not requests currently in progress.
* Any errors in the reload process will be logged and suppressed. The application will continue using the last known good configuration.
* If `GetConfig()` throws the proxy will be unable to listen for future changes because `IChangeToken`s are single-use.

Once the new configuration has been validated and applied, the proxy will register a callback with the new `IChangeToken`. Note if there are multiple reloads signaled in close succession, the proxy may skip some and load the next available configuration as soon as it's ready. Each `IProxyConfig` contains the full configuration state so nothing will be lost.

## Multiple Configuration Sources

As of 1.1, YARP supports loading the proxy configuration from multiple sources. Multiple `IProxyConfigProvider`'s can be registered as singleton services and all will be resolved and combined. The sources may be the same or different types such as IConfiguration or InMemory. Routes can reference clusters from other sources. Note merging partial config from different sources for a given route or cluster is not supported.

```
    services.AddReverseProxy()
        .LoadFromConfig(Configuration.GetSection("ReverseProxy1"))
        .LoadFromConfig(Configuration.GetSection("ReverseProxy2"));
```

or

```
    services.AddReverseProxy()
        .LoadFromMemory(routes, clusters)
        .LoadFromConfig(Configuration.GetSection("ReverseProxy"));
```

## Example

The [InMemoryConfigProvider](https://github.com/microsoft/reverse-proxy/blob/main/src/ReverseProxy/Configuration/InMemoryConfigProvider.cs) gives an example of an `IProxyConfigProvider` that has routes and clusters manually loaded into it.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/details
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/details.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 10, examine the Details and Delete methods of an ASP.NET Core app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Open the Movie controller and examine the `Details` method:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that invokes the method. In this case it's a GET request with three URL segments, the `Movies` controller, the `Details` method, and an `id` value. Recall these segments are defined in `Program.cs`.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();
```

EF makes it easy to search for data using the `FirstOrDefaultAsync` method. An important security feature built into the method is that the code verifies that the search method has found a movie before it tries to do anything with it. For example, a hacker could introduce errors into the site by changing the URL created by the links from `http://localhost:{PORT}/Movies/Details/1` to something like `http://localhost:{PORT}/Movies/Details/12345` (or some other value that doesn't represent an actual movie). If you didn't check for a null movie, the app would throw an exception.

Examine the `Delete` and `DeleteConfirmed` methods.

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{

    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var movie = await _context.Movie.FindAsync(id);
    if (movie != null)
    {
        _context.Movie.Remove(movie);
    }

    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}
```

Note that the `HTTP GET Delete` method doesn't delete the specified movie, it returns a view of the movie where you can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter, performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.

The `[HttpPost]` method that deletes the data is named `DeleteConfirmed` to give the HTTP POST method a unique signature or name. The two method signatures are shown below:

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
```

```
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
```

The common language runtime (CLR) requires overloaded methods to have a unique parameter signature (same method name but different list of parameters). However, here you need two `Delete` methods -- one for GET and one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a parameter.)

There are two approaches to this problem, one is to give the methods different names. That's what the scaffolding mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps segments of a URL to action methods by name, and if you rename a method, routing normally wouldn't be able to find that method. The solution is what you see in the example, which is to add the `ActionName("Delete")` attribute to the `DeleteConfirmed` method. That attribute performs mapping for the routing system so that a URL that includes /Delete/ for a POST request will find the `DeleteConfirmed` method.

Another common work around for methods that have identical names and signatures is to artificially change the signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post when we added the `notUsed` parameter. You could do the same thing here for the `[HttpPost] Delete` method:

```
// POST: Movies/Delete/6
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)
```

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core and SQL Database app in Azure App Service](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

[Previous](validation?view=aspnetcore-10.0)

Open the Movie controller and examine the `Details` method:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that invokes the method. In this case it's a GET request with three URL segments, the `Movies` controller, the `Details` method, and an `id` value. Recall these segments are defined in `Program.cs`.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

EF makes it easy to search for data using the `FirstOrDefaultAsync` method. An important security feature built into the method is that the code verifies that the search method has found a movie before it tries to do anything with it. For example, a hacker could introduce errors into the site by changing the URL created by the links from `http://localhost:{PORT}/Movies/Details/1` to something like `http://localhost:{PORT}/Movies/Details/12345` (or some other value that doesn't represent an actual movie). If you didn't check for a null movie, the app would throw an exception.

Examine the `Delete` and `DeleteConfirmed` methods.

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{

    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var movie = await _context.Movie.FindAsync(id);
    if (movie != null)
    {
        _context.Movie.Remove(movie);
    }

    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}
```

Note that the `HTTP GET Delete` method doesn't delete the specified movie, it returns a view of the movie where you can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter, performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.

The `[HttpPost]` method that deletes the data is named `DeleteConfirmed` to give the HTTP POST method a unique signature or name. The two method signatures are shown below:

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
```

```
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
```

The common language runtime (CLR) requires overloaded methods to have a unique parameter signature (same method name but different list of parameters). However, here you need two `Delete` methods -- one for GET and one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a parameter.)

There are two approaches to this problem, one is to give the methods different names. That's what the scaffolding mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps segments of a URL to action methods by name, and if you rename a method, routing normally wouldn't be able to find that method. The solution is what you see in the example, which is to add the `ActionName("Delete")` attribute to the `DeleteConfirmed` method. That attribute performs mapping for the routing system so that a URL that includes /Delete/ for a POST request will find the `DeleteConfirmed` method.

Another common work around for methods that have identical names and signatures is to artificially change the signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post when we added the `notUsed` parameter. You could do the same thing here for the `[HttpPost] Delete` method:

```
// POST: Movies/Delete/6
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)
```

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core and SQL Database app in Azure App Service](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

[Previous](validation?view=aspnetcore-10.0)

Open the Movie controller and examine the `Details` method:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that invokes the method. In this case it's a GET request with three URL segments, the `Movies` controller, the `Details` method, and an `id` value. Recall these segments are defined in `Program.cs`.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

EF makes it easy to search for data using the `FirstOrDefaultAsync` method. An important security feature built into the method is that the code verifies that the search method has found a movie before it tries to do anything with it. For example, a hacker could introduce errors into the site by changing the URL created by the links from `http://localhost:{PORT}/Movies/Details/1` to something like `http://localhost:{PORT}/Movies/Details/12345` (or some other value that doesn't represent an actual movie). If you didn't check for a null movie, the app would throw an exception.

Examine the `Delete` and `DeleteConfirmed` methods.

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{

    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var movie = await _context.Movie.FindAsync(id);
    if (movie != null)
    {
        _context.Movie.Remove(movie);
    }

    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}
```

Note that the `HTTP GET Delete` method doesn't delete the specified movie, it returns a view of the movie where you can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter, performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.

The `[HttpPost]` method that deletes the data is named `DeleteConfirmed` to give the HTTP POST method a unique signature or name. The two method signatures are shown below:

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
```

```
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
```

The common language runtime (CLR) requires overloaded methods to have a unique parameter signature (same method name but different list of parameters). However, here you need two `Delete` methods -- one for GET and one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a parameter.)

There are two approaches to this problem, one is to give the methods different names. That's what the scaffolding mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps segments of a URL to action methods by name, and if you rename a method, routing normally wouldn't be able to find that method. The solution is what you see in the example, which is to add the `ActionName("Delete")` attribute to the `DeleteConfirmed` method. That attribute performs mapping for the routing system so that a URL that includes /Delete/ for a POST request will find the `DeleteConfirmed` method.

Another common work around for methods that have identical names and signatures is to artificially change the signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post when we added the `notUsed` parameter. You could do the same thing here for the `[HttpPost] Delete` method:

```
// POST: Movies/Delete/6
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)
```

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core and SQL Database app in Azure App Service](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

[Previous](validation?view=aspnetcore-10.0)

Open the Movie controller and examine the `Details` method:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that invokes the method. In this case it's a GET request with three URL segments, the `Movies` controller, the `Details` method, and an `id` value. Recall these segments are defined in `Program.cs`.

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

EF makes it easy to search for data using the `FirstOrDefaultAsync` method. An important security feature built into the method is that the code verifies that the search method has found a movie before it tries to do anything with it. For example, a hacker could introduce errors into the site by changing the URL created by the links from `http://localhost:{PORT}/Movies/Details/1` to something like `http://localhost:{PORT}/Movies/Details/12345` (or some other value that doesn't represent an actual movie). If you didn't check for a null movie, the app would throw an exception.

Examine the `Delete` and `DeleteConfirmed` methods.

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var movie = await _context.Movie.FindAsync(id);
    _context.Movie.Remove(movie);
    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}
```

Note that the `HTTP GET Delete` method doesn't delete the specified movie, it returns a view of the movie where you can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter, performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.

The `[HttpPost]` method that deletes the data is named `DeleteConfirmed` to give the HTTP POST method a unique signature or name. The two method signatures are shown below:

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
```

```
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
```

The common language runtime (CLR) requires overloaded methods to have a unique parameter signature (same method name but different list of parameters). However, here you need two `Delete` methods -- one for GET and one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a parameter.)

There are two approaches to this problem, one is to give the methods different names. That's what the scaffolding mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps segments of a URL to action methods by name, and if you rename a method, routing normally wouldn't be able to find that method. The solution is what you see in the example, which is to add the `ActionName("Delete")` attribute to the `DeleteConfirmed` method. That attribute performs mapping for the routing system so that a URL that includes /Delete/ for a POST request will find the `DeleteConfirmed` method.

Another common work around for methods that have identical names and signatures is to artificially change the signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post when we added the `notUsed` parameter. You could do the same thing here for the `[HttpPost] Delete` method:

```
// POST: Movies/Delete/6
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)
```

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core and SQL Database app in Azure App Service](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

[Previous](validation?view=aspnetcore-10.0)

Open the Movie controller and examine the `Details` method:

```
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}
```

The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that invokes the method. In this case it's a GET request with three URL segments, the `Movies` controller, the `Details` method, and an `id` value. Recall these segments are defined in `Startup.cs`.

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

EF makes it easy to search for data using the `FirstOrDefaultAsync` method. An important security feature built into the method is that the code verifies that the search method has found a movie before it tries to do anything with it. For example, a hacker could introduce errors into the site by changing the URL created by the links from `http://localhost:{PORT}/Movies/Details/1` to something like `http://localhost:{PORT}/Movies/Details/12345` (or some other value that doesn't represent an actual movie). If you didn't check for a null movie, the app would throw an exception.

Examine the `Delete` and `DeleteConfirmed` methods.

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var movie = await _context.Movie.FindAsync(id);
    _context.Movie.Remove(movie);
    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}
```

Note that the `HTTP GET Delete` method doesn't delete the specified movie, it returns a view of the movie where you can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter, performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.

The `[HttpPost]` method that deletes the data is named `DeleteConfirmed` to give the HTTP POST method a unique signature or name. The two method signatures are shown below:

```
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
```

```
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
```

The common language runtime (CLR) requires overloaded methods to have a unique parameter signature (same method name but different list of parameters). However, here you need two `Delete` methods -- one for GET and one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a parameter.)

There are two approaches to this problem, one is to give the methods different names. That's what the scaffolding mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps segments of a URL to action methods by name, and if you rename a method, routing normally wouldn't be able to find that method. The solution is what you see in the example, which is to add the `ActionName("Delete")` attribute to the `DeleteConfirmed` method. That attribute performs mapping for the routing system so that a URL that includes /Delete/ for a POST request will find the `DeleteConfirmed` method.

Another common work around for methods that have identical names and signatures is to artificially change the signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post when we added the `notUsed` parameter. You could do the same thing here for the `[HttpPost] Delete` method:

```
// POST: Movies/Delete/6
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)
```

### Publish to Azure

For information on deploying to Azure, see [Tutorial: Build an ASP.NET Core and SQL Database app in Azure App Service](/en-us/azure/app-service/tutorial-dotnetcore-sqldb-app).

[Previous](validation?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/apis
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/apis.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# APIs overview

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

ASP.NET Core provides two approaches for building HTTP APIs: **Minimal APIs** and controller-based APIs. **For new projects, we recommend using Minimal APIs** as they provide a simplified, high-performance approach for building APIs with minimal code and configuration.

## Minimal APIs - Recommended for new projects

Minimal APIs are the recommended approach for building fast HTTP APIs with ASP.NET Core. They allow you to build fully functioning REST endpoints with minimal code and configuration. Skip traditional scaffolding and avoid unnecessary controllers by fluently declaring API routes and actions.

Here's a simple example that creates an API at the root of the web app:

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

Most APIs accept parameters as part of the route:

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();
```

Minimal APIs support the configuration and customization needed to scale to multiple APIs, handle complex routes, apply authorization rules, and control the content of API responses.

### Getting started with Minimal APIs

* **Tutorial**: [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0)
* **Quick reference**: [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0)
* **Examples**: For a full list of common scenarios with code examples, see [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0)

## Controller-based APIs - Alternative approach

ASP.NET Core also supports a controller-based approach where controllers are classes that derive from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase). This approach follows traditional object-oriented patterns and may be preferred for:

* Large applications with complex business logic
* Teams familiar with the MVC pattern
* Applications requiring specific MVC features

Here's sample code for an API based on controllers:

```

namespace APIWithControllers;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddControllers();
        var app = builder.Build();

        app.UseHttpsRedirection();

        app.MapControllers();

        app.Run();
    }
}
```

```
using Microsoft.AspNetCore.Mvc;

namespace APIWithControllers.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}
```

The following code provides the same functionality using the recommended Minimal API approach:

```

namespace MinimalAPI;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        var app = builder.Build();

        app.UseHttpsRedirection();

        var summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        app.MapGet("/weatherforecast", (HttpContext httpContext) =>
        {
            var forecast = Enumerable.Range(1, 5).Select(index =>
                new WeatherForecast
                {
                    Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                    TemperatureC = Random.Shared.Next(-20, 55),
                    Summary = summaries[Random.Shared.Next(summaries.Length)]
                })
                .ToArray();
            return forecast;
        });

        app.Run();
    }
}
```

Both API projects refer to the following class:

```
namespace APIWithControllers;

public class WeatherForecast
{
    public DateOnly Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string? Summary { get; set; }
}
```

## Choosing between approaches

**Start with Minimal APIs** for new projects. They offer:

* **Simpler syntax** - Less boilerplate code
* **Better performance** - Reduced overhead compared to controllers
* **Easier testing** - Simplified unit and integration testing
* **Modern approach** - Leverages the latest .NET features

**Consider controller-based APIs** if you need:

* Model binding extensibility ([IModelBinderProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.imodelbinderprovider), [IModelBinder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.imodelbinder))
* Advanced validation features ([IModelValidator](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.imodelvalidator))
* [Application parts](../mvc/advanced/app-parts?view=aspnetcore-10.0) or the [application model](../mvc/controllers/application-model?view=aspnetcore-10.0)
* [OData](https://www.nuget.org/packages/Microsoft.AspNetCore.OData/) support

Most of these features can be implemented in Minimal APIs with custom solutions, but controllers provide them out of the box.

## See also

* [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0) - Minimal API tutorial
* [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0) - Minimal APIs quick reference
* [Create web APIs with ASP.NET Core](../web-api/?view=aspnetcore-10.0) - Controller-based APIs overview
* [Tutorial: Create a controller-based web API with ASP.NET Core](../tutorials/first-web-api?view=aspnetcore-10.0) - Controller-based API tutorial

ASP.NET Core provides two approaches for building HTTP APIs: **Minimal APIs** and controller-based APIs. **For new projects, we recommend using Minimal APIs** as they provide a simplified, high-performance approach for building APIs with minimal code and configuration.

## Minimal APIs - Recommended for new projects

Minimal APIs are the recommended approach for building fast HTTP APIs with ASP.NET Core. They allow you to build fully functioning REST endpoints with minimal code and configuration.

Here's a simple example:

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Getting started with Minimal APIs

* **Tutorial**: [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0)
* **Quick reference**: [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0)

## Controller-based APIs - Alternative approach

Controllers are classes that derive from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase). This approach follows traditional object-oriented patterns.

Here's sample code for an API based on controllers:

```

namespace APIWithControllers;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddControllers();
        var app = builder.Build();

        app.UseHttpsRedirection();

        app.MapControllers();

        app.Run();
    }
}
```

```
using Microsoft.AspNetCore.Mvc;

namespace APIWithControllers.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}
```

The following code provides the same functionality using the recommended Minimal API approach:

```

namespace MinimalAPI;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        var app = builder.Build();

        app.UseHttpsRedirection();

        var summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        app.MapGet("/weatherforecast", (HttpContext httpContext) =>
        {
            var forecast = Enumerable.Range(1, 5).Select(index =>
                new WeatherForecast
                {
                    Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                    TemperatureC = Random.Shared.Next(-20, 55),
                    Summary = summaries[Random.Shared.Next(summaries.Length)]
                })
                .ToArray();
            return forecast;
        });

        app.Run();
    }
}
```

Both API projects refer to the following class:

```
namespace APIWithControllers;

public class WeatherForecast
{
    public DateOnly Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string? Summary { get; set; }
}
```

## Choosing between approaches

**Start with Minimal APIs** for new projects. Consider controller-based APIs if you need:

* Model binding extensibility ([IModelBinderProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.imodelbinderprovider), [IModelBinder](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.imodelbinder))
* Form binding support, including [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile)
* Advanced validation features ([IModelValidator](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.imodelvalidator))
* [Application parts](../mvc/advanced/app-parts?view=aspnetcore-10.0) or the [application model](../mvc/controllers/application-model?view=aspnetcore-10.0)
* [OData](https://www.nuget.org/packages/Microsoft.AspNetCore.OData/) support

## See also

* [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0) - Minimal API tutorial
* [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0) - Minimal APIs quick reference
* [Create web APIs with ASP.NET Core](../web-api/?view=aspnetcore-10.0) - Controller-based APIs overview
* [Tutorial: Create a controller-based web API with ASP.NET Core](../tutorials/first-web-api?view=aspnetcore-10.0) - Controller-based API tutorial

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/host/hosted-services.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Background tasks with hosted services in ASP.NET Core

By [Jeow Li Huan](https://github.com/huan086)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

In ASP.NET Core, background tasks can be implemented as *hosted services*. A hosted service is a class with background task logic that implements the [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) interface. Hosted services can be used in Worker Service apps and in web apps. This article provides three hosted service examples:

* Background task that runs on a timer.
* Hosted service that activates a [scoped service](../dependency-injection?view=aspnetcore-10.0#service-lifetimes). The scoped service can use [dependency injection (DI)](../dependency-injection?view=aspnetcore-10.0).
* Queued background tasks that run sequentially.

## Worker Service template

The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. An app created from the Worker Service template specifies the Worker SDK in its project file:

```
<Project Sdk="Microsoft.NET.Sdk.Worker">
```

To use the template as a basis for a hosted services app:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. Create a new project.
2. Select **Worker Service**. Select **Next**.
3. Provide a project name in the **Project name** field or accept the default project name. Select **Next**.
4. In the **Additional information** dialog, Choose a **Framework**. Select **Create**.

Use the Worker Service (`worker`) template with the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command from a command shell. In the following example, a Worker Service app is created named `ContosoWorker`. A folder for the `ContosoWorker` app is created automatically when the command is executed.

```
dotnet new worker -o ContosoWorker
```

## Package

An app based on the Worker Service template uses the `Microsoft.NET.Sdk.Worker` SDK and has an explicit package reference to the [`Microsoft.Extensions.Hosting` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Hosting). For example, see the Worker Service sample app's project file (`BackgroundTasksSample.csproj`).

For web apps that use the `Microsoft.NET.Sdk.Web` SDK, the [`Microsoft.Extensions.Hosting` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) is referenced implicitly from the shared framework. An explicit package reference in the app's project file isn't required.

## Hosted services in a web app

Hosted services aren't limited to Worker Service apps. A web app that uses the `Microsoft.NET.Sdk.Web` SDK registers hosted services in the `Program` file by calling the same [AddHostedService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionhostedserviceextensions.addhostedservice) extension method on [WebApplicationBuilder.Services](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.services#microsoft-aspnetcore-builder-webapplicationbuilder-services). The host automatically starts and stops registered hosted services, as described in the [`IHostedService` interface](#ihostedservice-interface) section.

The following `Program` file is from the web sample app (`BackgroundTasksWebSample`). It registers the three hosted services that are described in the rest of this article, maps a root endpoint that returns a status message, and adds an endpoint that queues a work item for one of them:

```
using BackgroundTasksWebSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHostedService<TimedHostedService>();

builder.Services.AddHostedService<ConsumeScopedServiceHostedService>();
builder.Services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

builder.Services.AddHostedService<QueuedHostedService>();
builder.Services.AddSingleton<IBackgroundTaskQueue>(_ =>
    new BackgroundTaskQueue(builder.Configuration.GetValue("QueueCapacity", 100)));

var app = builder.Build();

app.MapGet("/", () => "Hosted service is running in the background.");

app.MapPost("/queue", async (
    IBackgroundTaskQueue taskQueue,
    ILogger<Program> logger,
    CancellationToken cancellationToken) =>
{
    await taskQueue.QueueBackgroundWorkItemAsync(async token =>
    {
        var guid = Guid.NewGuid();

        logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        try
        {
            await Task.Delay(TimeSpan.FromSeconds(5), token);
        }
        catch (OperationCanceledException)
        {
            logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);

            return;
        }

        logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
    }, cancellationToken);

    return Results.Accepted();
});

app.Run();
```

In the preceding code:

* `TimedHostedService`, `ConsumeScopedServiceHostedService`, and `QueuedHostedService` are registered with the [AddHostedService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionhostedserviceextensions.addhostedservice) extension method.
* A hosted service is registered as a singleton, so it can't receive [scoped services](../dependency-injection?view=aspnetcore-10.0#service-lifetimes), such as an Entity Framework Core `DbContext`, by constructor injection. `IScopedProcessingService` is registered as a scoped service, and `ConsumeScopedServiceHostedService` creates a scope to resolve it. For more information, see the [Consuming a scoped service in a background task](#consuming-a-scoped-service-in-a-background-task) section.
* The `IBackgroundTaskQueue` singleton is consumed by the `/queue` endpoint and `QueuedHostedService`. A `POST` request to `/queue` returns a `202 Accepted` response once the work item is enqueued without waiting for it to execute. If the bounded queue is full, the request waits until space is available to enqueue the work item. `QueuedHostedService` runs the queued work items in the background. For more information, see the [Queued background tasks](#queued-background-tasks) section.

The following service registration snippets are from the Worker Service sample app (`BackgroundTasksSample`), where `services` is the [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection) parameter passed to `ConfigureServices`. In a web app, use `builder.Services` instead of `services`.

## IHostedService interface

The [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) interface defines two methods for objects that are managed by the host:

* [StartAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.startasync)
* [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.stopasync)

### `StartAsync`

[StartAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.startasync) contains the logic to start the background task. `StartAsync` is called *before*:

* The app's request processing pipeline is configured.
* The server is started and [IApplicationLifetime.ApplicationStarted](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iapplicationlifetime.applicationstarted) is triggered.

`StartAsync` should be limited to short running tasks because hosted services are run sequentially, and no further services are started until `StartAsync` runs to completion.

Hosted service instances start in the order that they're registered in the dependency injection container unless the app opts into concurrent startup by setting [ServicesStartConcurrently](/en-us/dotnet/api/microsoft.extensions.hosting.hostoptions.servicesstartconcurrently#microsoft-extensions-hosting-hostoptions-servicesstartconcurrently) to `true`:

```
builder.Services.Configure<HostOptions>(options =>
{
    options.ServicesStartConcurrently = true;
});
```

### `StopAsync`

* [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.stopasync) is triggered when the host is performing a graceful shutdown. `StopAsync` contains the logic to end the background task. Implement [IDisposable](/en-us/dotnet/api/system.idisposable) and [finalizers (destructors)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/destructors) to dispose of any unmanaged resources.

The cancellation token has a default 30 second timeout to indicate that the shutdown process should no longer be graceful. When cancellation is requested on the token:

* Any remaining background operations that the app is performing should be aborted.
* Any methods called in `StopAsync` should return promptly.

However, tasks aren't abandoned after cancellation is requested—the caller awaits all tasks to complete.

If the app shuts down unexpectedly (for example, the app's process fails), `StopAsync` might not be called. Therefore, any methods called or operations conducted in `StopAsync` might not occur.

To extend the default 30 second shutdown timeout, set:

* [ShutdownTimeout](/en-us/dotnet/api/microsoft.extensions.hosting.hostoptions.shutdowntimeout) when using Generic Host. For more information, see [.NET Generic Host in ASP.NET Core](generic-host?view=aspnetcore-10.0#shutdowntimeout).
* Shutdown timeout host configuration setting when using Web Host. For more information, see [ASP.NET Core Web Host](web-host?view=aspnetcore-10.0#shutdown-timeout).

The hosted service is activated once at app startup and gracefully shut down at app shutdown. If an error is thrown during background task execution, `Dispose` should be called even if `StopAsync` isn't called.

Hosted service instances stop in the reverse order that they're registered in the dependency injection container unless the app opts into concurrent shutdown behavior by setting [ServicesStopConcurrently](/en-us/dotnet/api/microsoft.extensions.hosting.hostoptions.servicesstopconcurrently#microsoft-extensions-hosting-hostoptions-servicesstopconcurrently) to `true`:

```
builder.Services.Configure<HostOptions>(options =>
{
    options.ServicesStopConcurrently = true;
});
```

## BackgroundService base class

[BackgroundService](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) is a base class for implementing a long running [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice).

[ExecuteAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice.executeasync) is called on the thread pool to run the background service. The implementation returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) that represents the entire lifetime of the background service. The host blocks in [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice.stopasync) waiting for `ExecuteAsync` to complete.

[ExecuteAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice.executeasync) is called to run the background service. The implementation returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) that represents the entire lifetime of the background service. No further services are started until [ExecuteAsync becomes asynchronous](https://github.com/dotnet/extensions/issues/2149), such as by calling `await`. Avoid performing long, blocking initialization work in `ExecuteAsync`. The host blocks in [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice.stopasync) waiting for `ExecuteAsync` to complete.

The cancellation token is triggered when [IHostedService.StopAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.stopasync) is called. Your implementation of `ExecuteAsync` should finish promptly when the cancellation token is fired in order to gracefully shut down the service. Otherwise, the service ungracefully shuts down at the shutdown timeout. For more information, see the [IHostedService interface](#ihostedservice-interface) section.

For more information, see the [BackgroundService](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs) source code.

## Timed background tasks

A timed background task makes use of the [System.Threading.Timer](/en-us/dotnet/api/system.threading.timer) class. The timer triggers the task's `DoWork` method. The timer is disabled on `StopAsync` and disposed when the service container is disposed on `Dispose`:

```
public class TimedHostedService(ILogger<TimedHostedService> logger) : IHostedService, IDisposable
{
    private int executionCount = 0;
    private Timer? _timer = null;

    public Task StartAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref executionCount);

        logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}
```

The [Timer](/en-us/dotnet/api/system.threading.timer) doesn't wait for previous executions of `DoWork` to finish, so the approach shown might not be suitable for every scenario. [Interlocked.Increment](/en-us/dotnet/api/system.threading.interlocked.increment) is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update `executionCount` concurrently.

The service is registered in the `Program` file with the [AddHostedService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionhostedserviceextensions.addhostedservice) extension method:

```
services.AddHostedService<TimedHostedService>();
```

## Consuming a scoped service in a background task

To use [scoped services](../dependency-injection?view=aspnetcore-10.0#service-lifetimes) within a [BackgroundService](#backgroundservice-base-class), create a scope. No scope is created for a hosted service by default.

The scoped background task service contains the background task's logic. In the following example:

* The service is asynchronous. The `DoWork` method returns a `Task`. For demonstration purposes, a delay of ten seconds is awaited in the `DoWork` method.
* An [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) is injected into the service.

```
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService(
    ILogger<ScopedProcessingService> logger) : IScopedProcessingService
{
    private int executionCount = 0;

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}
```

The hosted service creates a scope to resolve the scoped background task service to call its `DoWork` method. `DoWork` returns a `Task`, which is awaited in `ExecuteAsync`:

```
public class ConsumeScopedServiceHostedService(
    IServiceProvider services,
    ILogger<ConsumeScopedServiceHostedService> logger) : BackgroundService
{
    public IServiceProvider Services { get; } = services;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService =
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}
```

The services are registered in the `Program` file. The hosted service is registered with the [AddHostedService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionhostedserviceextensions.addhostedservice) extension method:

```
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
```

## Queued background tasks

A background task queue is based on the .NET Framework 4.x [QueueBackgroundWorkItem](/en-us/dotnet/api/system.web.hosting.hostingenvironment.queuebackgroundworkitem):

```
public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue(int capacity) : IBackgroundTaskQueue
{
    // Capacity should be set based on the expected application load and
    // number of concurrent threads accessing the queue.
    // BoundedChannelFullMode.Wait causes calls to WriteAsync() to return a task,
    // which only completes when space becomes available. In the event that many
    // publishers/calls start accumulating, using BoundedChannelFullMode.Wait can
    // lead to backpressure.
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue =
        Channel.CreateBounded<Func<CancellationToken, ValueTask>>(new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        });

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}
```

In the following `QueueHostedService` example:

* The `BackgroundProcessing` method returns a `Task`, which is awaited in `ExecuteAsync`.
* Background tasks in the queue are dequeued and executed in `BackgroundProcessing`.
* Work items are awaited before the service stops in `StopAsync`.

```
public class QueuedHostedService(
    IBackgroundTaskQueue taskQueue,
    ILogger<QueuedHostedService> logger) : BackgroundService
{
    public IBackgroundTaskQueue TaskQueue { get; } = taskQueue;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem =
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                logger.LogError(ex,
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}
```

A `MonitorLoop` service handles enqueuing tasks for the hosted service whenever the `w` key is selected on an input device:

* The `IBackgroundTaskQueue` is injected into the `MonitorLoop` service.

* `IBackgroundTaskQueue.QueueBackgroundWorkItem` is called to enqueue a work item.

* The work item simulates a long-running background task:

  * Three 5-second delays are executed (`Task.Delay`).
  * A `try-catch` statement traps [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception) if the task is cancelled.

```
public class MonitorLoop(
    IBackgroundTaskQueue taskQueue,
    ILogger<MonitorLoop> logger,
    IHostApplicationLifetime applicationLifetime)
{
    private readonly CancellationToken _cancellationToken = applicationLifetime.ApplicationStopping;

    public void StartMonitorLoop()
    {
        logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            logger.LogInformation("Queued Background Task {Guid} is running. "
                                   + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}
```

The services are registered in the `Program` file. The hosted service is registered with the [AddHostedService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionhostedserviceextensions.addhostedservice) extension method:

```
services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});
```

`MonitorLoop` is started in the `Program` file:

```
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
```

In a web app, an endpoint can queue work items instead of a console input loop. For an example, see the [Hosted services in a web app](#hosted-services-in-a-web-app) section.

## Asynchronous timed background task

The following code creates an asynchronous timed background task:

```
namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        // When the timer should have no due-time, then do the work once now.
        await DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                await DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    private async Task DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        // Simulate work
        await Task.Delay(TimeSpan.FromSeconds(2));

        _logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
    }
}
```

## Native AOT

The Worker Service templates support [.NET native ahead-of-time (AOT)](/en-us/dotnet/core/deploying/native-aot/) with the `--aot` flag:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. Create a new project.
2. Select **Worker Service**. Select **Next**.
3. Provide a project name in the **Project name** field or accept the default project name. Select **Next**.
4. In the **Additional information** dialog:
5. Choose a **Framework**.
6. Check the **Enable Native AOT publish** checkbox.
7. Select **Create**.

Use the Worker Service (`worker`) template with the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command from a command shell with the AOT option:

```
dotnet new worker -o WorkerWithAot --aot
```

The AOT option adds `<PublishAot>true</PublishAot>` to the project file:

```

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

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
+   <PublishAot>true</PublishAot>
    <UserSecretsId>dotnet-WorkerWithAot-e94b2</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.4.23259.5" />
  </ItemGroup>
</Project>
```

## Related content

* [Background services unit tests on GitHub](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceTests.cs).
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/hosted-services/samples/) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))
* [Implement background tasks in microservices with IHostedService and the BackgroundService class](/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice)
* [Run background tasks with WebJobs in Azure App Service](/en-us/azure/app-service/webjobs-create)
* [Timer](/en-us/dotnet/api/system.threading.timer)

In ASP.NET Core, background tasks can be implemented as *hosted services*. A hosted service is a class with background task logic that implements the [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) interface. This article provides three hosted service examples:

* Background task that runs on a timer.
* Hosted service that activates a [scoped service](../dependency-injection?view=aspnetcore-10.0#service-lifetimes). The scoped service can use [dependency injection (DI)](../dependency-injection?view=aspnetcore-10.0).
* Queued background tasks that run sequentially.

## Worker Service template

The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. An app created from the Worker Service template specifies the Worker SDK in its project file:

```
<Project Sdk="Microsoft.NET.Sdk.Worker">
```

To use the template as a basis for a hosted services app:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. Create a new project.
2. Select **Worker Service**. Select **Next**.
3. Provide a project name in the **Project name** field or accept the default project name. Select **Next**.
4. In the **Additional information** dialog, Choose a **Framework**. Select **Create**.

Use the Worker Service (`worker`) template with the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command from a command shell. In the following example, a Worker Service app is created named `ContosoWorker`. A folder for the `ContosoWorker` app is created automatically when the command is executed.

```
dotnet new worker -o ContosoWorker
```

## Package

An app based on the Worker Service template uses the `Microsoft.NET.Sdk.Worker` SDK and has an explicit package reference to the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) package. For example, see the sample app's project file (`BackgroundTasksSample.csproj`).

For web apps that use the `Microsoft.NET.Sdk.Web` SDK, the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) package is referenced implicitly from the shared framework. An explicit package reference in the app's project file isn't required.

## IHostedService interface

The [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) interface defines two methods for objects that are managed by the host:

* [StartAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.startasync)
* [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.stopasync)

### `StartAsync`

[StartAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.startasync) contains the logic to start the background task. `StartAsync` is called *before*:

* The app's request processing pipeline is configured.
* The server is started and [IApplicationLifetime.ApplicationStarted](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iapplicationlifetime.applicationstarted) is triggered.

`StartAsync` should be limited to short running tasks because hosted services are run sequentially, and no further services are started until `StartAsync` runs to completion.

### `StopAsync`

* [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.stopasync) is triggered when the host is performing a graceful shutdown. `StopAsync` contains the logic to end the background task. Implement [IDisposable](/en-us/dotnet/api/system.idisposable) and [finalizers (destructors)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/destructors) to dispose of any unmanaged resources.

The cancellation token has a default 30 second timeout to indicate that the shutdown process should no longer be graceful. When cancellation is requested on the token:

* Any remaining background operations that the app is performing should be aborted.
* Any methods called in `StopAsync` should return promptly.

However, tasks aren't abandoned after cancellation is requested—the caller awaits all tasks to complete.

If the app shuts down unexpectedly (for example, the app's process fails), `StopAsync` might not be called. Therefore, any methods called or operations conducted in `StopAsync` might not occur.

To extend the default 30 second shutdown timeout, set:

* [ShutdownTimeout](/en-us/dotnet/api/microsoft.extensions.hosting.hostoptions.shutdowntimeout) when using Generic Host. For more information, see [.NET Generic Host in ASP.NET Core](generic-host?view=aspnetcore-10.0#shutdowntimeout).
* Shutdown timeout host configuration setting when using Web Host. For more information, see [ASP.NET Core Web Host](web-host?view=aspnetcore-10.0#shutdown-timeout).

The hosted service is activated once at app startup and gracefully shut down at app shutdown. If an error is thrown during background task execution, `Dispose` should be called even if `StopAsync` isn't called.

## BackgroundService base class

[BackgroundService](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) is a base class for implementing a long running [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice).

[ExecuteAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice.executeasync) is called to run the background service. The implementation returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) that represents the entire lifetime of the background service. No further services are started until [ExecuteAsync becomes asynchronous](https://github.com/dotnet/extensions/issues/2149), such as by calling `await`. Avoid performing long, blocking initialization work in `ExecuteAsync`. The host blocks in [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice.stopasync) waiting for `ExecuteAsync` to complete.

The cancellation token is triggered when [IHostedService.StopAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.stopasync) is called. Your implementation of `ExecuteAsync` should finish promptly when the cancellation token is fired in order to gracefully shut down the service. Otherwise, the service ungracefully shuts down at the shutdown timeout. For more information, see the [IHostedService interface](#ihostedservice-interface) section.

For more information, see the [BackgroundService](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs) source code.

## Timed background tasks

A timed background task makes use of the [System.Threading.Timer](/en-us/dotnet/api/system.threading.timer) class. The timer triggers the task's `DoWork` method. The timer is disabled on `StopAsync` and disposed when the service container is disposed on `Dispose`:

```
public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer = null;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}
```

The [Timer](/en-us/dotnet/api/system.threading.timer) doesn't wait for previous executions of `DoWork` to finish, so the approach shown might not be suitable for every scenario. [Interlocked.Increment](/en-us/dotnet/api/system.threading.interlocked.increment) is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update `executionCount` concurrently.

The service is registered in `IHostBuilder.ConfigureServices` (`Program.cs`) with the `AddHostedService` extension method:

```
services.AddHostedService<TimedHostedService>();
```

## Consuming a scoped service in a background task

To use [scoped services](../dependency-injection?view=aspnetcore-10.0#service-lifetimes) within a [BackgroundService](#backgroundservice-base-class), create a scope. No scope is created for a hosted service by default.

The scoped background task service contains the background task's logic. In the following example:

* The service is asynchronous. The `DoWork` method returns a `Task`. For demonstration purposes, a delay of ten seconds is awaited in the `DoWork` method.
* An [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) is injected into the service.

```
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}
```

The hosted service creates a scope to resolve the scoped background task service to call its `DoWork` method. `DoWork` returns a `Task`, which is awaited in `ExecuteAsync`:

```
public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}
```

The services are registered in `IHostBuilder.ConfigureServices` (`Program.cs`). The hosted service is registered with the `AddHostedService` extension method:

```
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
```

## Queued background tasks

A background task queue is based on the .NET Framework 4.x [QueueBackgroundWorkItem](/en-us/dotnet/api/system.web.hosting.hostingenvironment.queuebackgroundworkitem):

```
public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait causes calls to WriteAsync() to return a task,
        // which only completes when space becomes available. In the event that many
        // publishers/calls start accumulating, using BoundedChannelFullMode.Wait can
        // lead to backpressure.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}
```

In the following `QueueHostedService` example:

* The `BackgroundProcessing` method returns a `Task`, which is awaited in `ExecuteAsync`.
* Background tasks in the queue are dequeued and executed in `BackgroundProcessing`.
* Work items are awaited before the service stops in `StopAsync`.

```
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}
```

A `MonitorLoop` service handles enqueuing tasks for the hosted service whenever the `w` key is selected on an input device:

* The `IBackgroundTaskQueue` is injected into the `MonitorLoop` service.

* `IBackgroundTaskQueue.QueueBackgroundWorkItem` is called to enqueue a work item.

* The work item simulates a long-running background task:

  * Three 5-second delays are executed (`Task.Delay`).
  * A `try-catch` statement traps [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception) if the task is cancelled.

```
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue,
        ILogger<MonitorLoop> logger,
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " 
                                   + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}
```

The services are registered in `IHostBuilder.ConfigureServices` (`Program.cs`). The hosted service is registered with the `AddHostedService` extension method:

```
services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});
```

`MonitorLoop` is started in `Program.cs`:

```
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
```

## Asynchronous timed background task

The following code creates an asynchronous timed background task:

```
namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        // When the timer should have no due-time, then do the work once now.
        await DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                await DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    private async Task DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        // Simulate work
        await Task.Delay(TimeSpan.FromSeconds(2));

        _logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
    }
}
```

## Related content

* [Background services unit tests on GitHub](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceTests.cs).
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/hosted-services/samples/) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))
* [Implement background tasks in microservices with IHostedService and the BackgroundService class](/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice)
* [Run background tasks with WebJobs in Azure App Service](/en-us/azure/app-service/webjobs-create)
* [Timer](/en-us/dotnet/api/system.threading.timer)

In ASP.NET Core, background tasks can be implemented as *hosted services*. A hosted service is a class with background task logic that implements the [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) interface. This article provides three hosted service examples:

* Background task that runs on a timer.
* Hosted service that activates a [scoped service](../dependency-injection?view=aspnetcore-10.0#service-lifetimes). The scoped service can use [dependency injection (DI)](../dependency-injection?view=aspnetcore-10.0).
* Queued background tasks that run sequentially.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/hosted-services/samples/) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

## Worker Service template

The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. An app created from the Worker Service template specifies the Worker SDK in its project file:

```
<Project Sdk="Microsoft.NET.Sdk.Worker">
```

To use the template as a basis for a hosted services app:

```
dotnet new worker -o ContosoWorker
```

## Package

An app based on the Worker Service template uses the `Microsoft.NET.Sdk.Worker` SDK and has an explicit package reference to the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) package. For example, see the sample app's project file (`BackgroundTasksSample.csproj`).

For web apps that use the `Microsoft.NET.Sdk.Web` SDK, the [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) package is referenced implicitly from the shared framework. An explicit package reference in the app's project file isn't required.

## IHostedService interface

The [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) interface defines two methods for objects that are managed by the host:

* [StartAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.startasync)
* [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.stopasync)

### `StartAsync`

`StartAsync` contains the logic to start the background task. `StartAsync` is called *before*:

* The app's request processing pipeline is configured.
* The server is started and [IApplicationLifetime.ApplicationStarted](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iapplicationlifetime.applicationstarted) is triggered.

The default behavior can be changed so that the hosted service's `StartAsync` runs after the app's pipeline has been configured and `ApplicationStarted` is called. To change the default behavior, add the hosted service (`VideosWatcher` in the following example) after calling `ConfigureWebHostDefaults`:

```
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            })
            .ConfigureServices(services =>
            {
                services.AddHostedService<VideosWatcher>();
            });
}
```

### `StopAsync`

* [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.stopasync) is triggered when the host is performing a graceful shutdown. `StopAsync` contains the logic to end the background task. Implement [IDisposable](/en-us/dotnet/api/system.idisposable) and [finalizers (destructors)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/destructors) to dispose of any unmanaged resources.

The cancellation token has a default five second timeout to indicate that the shutdown process should no longer be graceful. When cancellation is requested on the token:

* Any remaining background operations that the app is performing should be aborted.
* Any methods called in `StopAsync` should return promptly.

However, tasks aren't abandoned after cancellation is requested—the caller awaits all tasks to complete.

If the app shuts down unexpectedly (for example, the app's process fails), `StopAsync` might not be called. Therefore, any methods called or operations conducted in `StopAsync` might not occur.

To extend the default five second shutdown timeout, set:

* [ShutdownTimeout](/en-us/dotnet/api/microsoft.extensions.hosting.hostoptions.shutdowntimeout) when using Generic Host. For more information, see [.NET Generic Host in ASP.NET Core](generic-host?view=aspnetcore-10.0#shutdowntimeout).
* Shutdown timeout host configuration setting when using Web Host. For more information, see [ASP.NET Core Web Host](web-host?view=aspnetcore-10.0#shutdown-timeout).

The hosted service is activated once at app startup and gracefully shut down at app shutdown. If an error is thrown during background task execution, `Dispose` should be called even if `StopAsync` isn't called.

## BackgroundService base class

[BackgroundService](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice) is a base class for implementing a long running [IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice).

[ExecuteAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice.executeasync) is called to run the background service. The implementation returns a [Task](/en-us/dotnet/api/system.threading.tasks.task) that represents the entire lifetime of the background service. No further services are started until [ExecuteAsync becomes asynchronous](https://github.com/dotnet/extensions/issues/2149), such as by calling `await`. Avoid performing long, blocking initialization work in `ExecuteAsync`. The host blocks in [StopAsync(CancellationToken)](/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice.stopasync) waiting for `ExecuteAsync` to complete.

The cancellation token is triggered when [IHostedService.StopAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.stopasync) is called. Your implementation of `ExecuteAsync` should finish promptly when the cancellation token is fired in order to gracefully shut down the service. Otherwise, the service ungracefully shuts down at the shutdown timeout. For more information, see the [IHostedService interface](#ihostedservice-interface) section.

`StartAsync` should be limited to short running tasks because hosted services are run sequentially, and no further services are started until `StartAsync` runs to completion. Long running tasks should be placed in `ExecuteAsync`. For more information, see the source to [BackgroundService](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs).

## Timed background tasks

A timed background task makes use of the [System.Threading.Timer](/en-us/dotnet/api/system.threading.timer) class. The timer triggers the task's `DoWork` method. The timer is disabled on `StopAsync` and disposed when the service container is disposed on `Dispose`:

```
public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}
```

The [Timer](/en-us/dotnet/api/system.threading.timer) doesn't wait for previous executions of `DoWork` to finish, so the approach shown might not be suitable for every scenario. [Interlocked.Increment](/en-us/dotnet/api/system.threading.interlocked.increment) is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update `executionCount` concurrently.

The service is registered in `IHostBuilder.ConfigureServices` (`Program.cs`) with the `AddHostedService` extension method:

```
services.AddHostedService<TimedHostedService>();
```

## Consuming a scoped service in a background task

To use [scoped services](../dependency-injection?view=aspnetcore-10.0#service-lifetimes) within a [BackgroundService](#backgroundservice-base-class), create a scope. No scope is created for a hosted service by default.

The scoped background task service contains the background task's logic. In the following example:

* The service is asynchronous. The `DoWork` method returns a `Task`. For demonstration purposes, a delay of ten seconds is awaited in the `DoWork` method.
* An [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) is injected into the service.

```
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}
```

The hosted service creates a scope to resolve the scoped background task service to call its `DoWork` method. `DoWork` returns a `Task`, which is awaited in `ExecuteAsync`:

```
public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}
```

The services are registered in `IHostBuilder.ConfigureServices` (`Program.cs`). The hosted service is registered with the `AddHostedService` extension method:

```
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
```

## Queued background tasks

A background task queue is based on the .NET Framework 4.x [QueueBackgroundWorkItem](/en-us/dotnet/api/system.web.hosting.hostingenvironment.queuebackgroundworkitem):

```
public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait causes calls to WriteAsync() to return a task,
        // which only completes when space becomes available. In the event that many
        // publishers/calls start accumulating, using BoundedChannelFullMode.Wait can
        // lead to backpressure.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}
```

In the following `QueueHostedService` example:

* The `BackgroundProcessing` method returns a `Task`, which is awaited in `ExecuteAsync`.
* Background tasks in the queue are dequeued and executed in `BackgroundProcessing`.
* Work items are awaited before the service stops in `StopAsync`.

```
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}
```

A `MonitorLoop` service handles enqueuing tasks for the hosted service whenever the `w` key is selected on an input device:

* The `IBackgroundTaskQueue` is injected into the `MonitorLoop` service.

* `IBackgroundTaskQueue.QueueBackgroundWorkItem` is called to enqueue a work item.

* The work item simulates a long-running background task:

  * Three 5-second delays are executed (`Task.Delay`).
  * A `try-catch` statement traps [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception) if the task is cancelled.

```
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue, 
        ILogger<MonitorLoop> logger, 
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}
```

The services are registered in `IHostBuilder.ConfigureServices` (`Program.cs`). The hosted service is registered with the `AddHostedService` extension method:

```
services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx => {
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});
```

`MonitorLoop` is started in `Program.Main`:

```
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
```

## Related content

* [Implement background tasks in microservices with IHostedService and the BackgroundService class](/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/background-tasks-with-ihostedservice)
* [Run background tasks with WebJobs in Azure App Service](/en-us/azure/app-service/webjobs-create)
* [Timer](/en-us/dotnet/api/system.threading.timer)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/call-web-api.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Call a web API from ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes how to call a web API from a Blazor app.

## Server-side scenarios for calling external web APIs

Server-based components call external web APIs using [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances, typically created using [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory). For guidance that applies to server-side apps, see [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0).

A server-side app doesn't include an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service. Provide an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to the app using the [`HttpClient` factory infrastructure](../fundamentals/http-requests?view=aspnetcore-10.0).

In the `Program` file:

```
builder.Services.AddHttpClient();
```

The following Razor component makes a request to a web API for GitHub branches similar to the *Basic Usage* example in the [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0) article.

`CallWebAPI.razor`:

```
@page "/call-web-api"
@using System.Text.Json
@using System.Text.Json.Serialization
@inject IHttpClientFactory ClientFactory

<h1>Call web API from a server-side Razor component</h1>

@if (getBranchesError || branches is null)
{
    <p>Unable to get branches from GitHub. Please try again later.</p>
}
else
{
    <ul>
        @foreach (var branch in branches)
        {
            <li>@branch.Name</li>
        }
    </ul>
}

@code {
    private IEnumerable<GitHubBranch>? branches = [];
    private bool getBranchesError;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    protected override async Task OnInitializedAsync()
    {
        using var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = ClientFactory.CreateClient();

        using var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            getBranchesError = true;
        }

        shouldRender = true;
    }

    public class GitHubBranch
    {
        [JsonPropertyName("name")]
        public string? Name { get; set; }
    }
}
```

In the preceding example for C# 12 or later, an empty array (`[]`) is created for the `branches` variable. For earlier versions of C# compiled with an SDK earlier than .NET 8, create an empty array (`Array.Empty<GitHubBranch>()`).

For an additional working example, see the server-side file upload example that uploads files to a web API controller in the [ASP.NET Core Blazor file uploads](file-uploads?view=aspnetcore-10.0#upload-files-to-a-server-with-server-side-rendering) article.

## Service abstractions for web API calls

*This section applies to Blazor Web Apps that maintain a web API in the server project or transform web API calls to an external web API.*

When using the interactive WebAssembly and Auto render modes, components are prerendered by default. Auto components are also initially rendered interactively from the server before the Blazor bundle downloads to the client and the client-side runtime activates. This means that components using these render modes should be designed so that they run successfully from both the client and the server. If the component must call a server project-based API or transform a request to an external web API (one that's outside of the Blazor Web App) when running on the client, the recommended approach is to abstract that API call behind a service interface and implement client and server versions of the service:

* The client version calls the web API with a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).
* The server version can typically access the server-side resources directly. Injecting an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) on the server that makes calls back to the server isn't recommended, as the network request is typically unnecessary. Alternatively, the API might be external to the server project, but a service abstraction for the server is required to transform the request in some way, for example to add an access token to a proxied request.

When using the WebAssembly render mode, you also have the option of disabling prerendering, so the components only render from the client. For more information, see [Prerender ASP.NET Core Razor components](components/prerender?view=aspnetcore-10.0#disable-prerendering).

Examples ([sample apps](#sample-apps)):

* Movie list web API in the `BlazorWebAppCallWebApi` sample app.
* Streaming rendering weather data web API in the `BlazorWebAppCallWebApi_Weather` sample app.
* Weather data returned to the client in either the `BlazorWebAppOidc` (non-BFF pattern) or `BlazorWebAppOidcBff` (BFF pattern) sample apps. These apps demonstrate secure (web) API calls. For more information, see [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](security/blazor-web-app-with-oidc?view=aspnetcore-10.0).

## Blazor Web App external web APIs

*This section applies to Blazor Web Apps that call a web API maintained by a separate (external) project, possibly hosted on a different server.*

Blazor Web Apps normally prerender client-side WebAssembly components, and Auto components render on the server during static or interactive server-side rendering (SSR). [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) services aren't registered by default in a Blazor Web App's main project. If the app is run with only the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) services registered in the `.Client` project, as described in the [Add the `HttpClient` service](#add-the-httpclient-service) section, executing the app results in a runtime error:

> InvalidOperationException: Cannot provide a value for property 'Http' on type '...{COMPONENT}'. There is no registered service of type 'System.Net.Http.HttpClient'.

Use ***either*** of the following approaches:

* Add the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) services to the server project to make the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) available during SSR. Use the following service registration in the server project's `Program` file:

  ```
  builder.Services.AddHttpClient();
  ```

  [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) services are provided by the shared framework, so a package reference in the app's project file isn't required.

  Example: Todo list web API in the `BlazorWebAppCallWebApi` [sample app](#sample-apps)

* If prerendering isn't required for a WebAssembly component that calls the web API, disable prerendering by following the guidance in [Prerender ASP.NET Core Razor components](components/prerender?view=aspnetcore-10.0#disable-prerendering). If you adopt this approach, you don't need to add [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) services to the main project of the Blazor Web App because the component isn't prerendered on the server.

For more information, see the [Client-side services fail to resolve during prerendering](components/prerender?view=aspnetcore-10.0#client-side-services-fail-to-resolve-during-prerendering) section of the *Prerendering* article.

## Client-side scenarios for calling external web APIs

Client-based components call external web APIs using [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances, typically created with a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) registered in the `Program` file:

```
builder.Services.AddScoped(sp => 
    new HttpClient
    { 
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) 
    });
```

The following Razor component makes a request to a web API for GitHub branches similar to the *Basic Usage* example in the [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0) article.

`CallWebAPI.razor`:

```
@page "/call-web-api"
@using System.Text.Json
@using System.Text.Json.Serialization
@inject HttpClient Client

<h1>Call web API from a Blazor WebAssembly Razor component</h1>

@if (getBranchesError || branches is null)
{
    <p>Unable to get branches from GitHub. Please try again later.</p>
}
else
{
    <ul>
        @foreach (var branch in branches)
        {
            <li>@branch.Name</li>
        }
    </ul>
}

@code {
    private IEnumerable<GitHubBranch>? branches = [];
    private bool getBranchesError;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    protected override async Task OnInitializedAsync()
    {
        using var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        using var response = await Client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            getBranchesError = true;
        }

        shouldRender = true;
    }

    public class GitHubBranch
    {
        [JsonPropertyName("name")]
        public string? Name { get; set; }
    }
}
```

In the preceding example for C# 12 or later, an empty array (`[]`) is created for the `branches` variable. For earlier versions of C# compiled with an SDK earlier than .NET 8, create an empty array (`Array.Empty<GitHubBranch>()`).

To protect .NET/C# code and data, use [ASP.NET Core Data Protection](../security/data-protection/introduction?view=aspnetcore-10.0) features with a server-side ASP.NET Core backend web API. The client-side Blazor WebAssembly app calls the server-side web API for secure app features and data processing.

Blazor WebAssembly apps are often prevented from making direct calls across origins to web APIs due to [Cross-Origin Request Sharing (CORS) security](#cross-origin-resource-sharing-cors). A typical exception looks like the following:

> Access to fetch at '{URL}' from origin 'https\://localhost:{PORT}' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Even if you call [SetBrowserRequestMode](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserrequestmode) with a [BrowserRequestMode](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.browserrequestmode) field of `NoCors` (1) seeking to circumvent the preceding exception, the request often fails due to CORS restrictions on the web API's origin, such as a restriction that only allows calls from specific origins or a restriction that prevents JavaScript [`fetch`](https://developer.mozilla.org/docs/Web/API/Fetch_API/Using_Fetch) requests from a browser. The only way for such calls to succeed is for the web API that you're calling to allow your origin to call its origin with the correct CORS configuration. Most external web APIs don't allow you to configure their CORS policies. To deal with this restriction, adopt either of the following strategies:

* Maintain your own server-side ASP.NET Core backend web API. The client-side Blazor WebAssembly app calls your server-side web API, and your web API makes the request from its server-based C# code (not a browser) to the external web API with the correct CORS headers, returning the result to your client-side Blazor WebAssembly app.

* Use a proxy service to proxy the request from the client-side Blazor WebAssembly app to the external web API. The proxy service uses a server-side app to make the request on the client's behalf and returns the result after the call succeeds. In the following example based on [CloudFlare's CORS PROXY](https://corsproxy.io/), the `{REQUEST URI}` placeholder is the request URI:

  ```
  @using System.Net
  @inject IHttpClientFactory ClientFactory

  ...

  @code {
      public async Task CallApi()
      {
          var client = ClientFactory.CreateClient();

          var urlEncodedRequestUri = WebUtility.UrlEncode("{REQUEST URI}");

          using var request = new HttpRequestMessage(HttpMethod.Get, 
              $"https://corsproxy.io/?{urlEncodedRequestUri}");

          using var response = await client.SendAsync(request);

          ...
      }
  }
  ```

## Add the `HttpClient` service

*The guidance in this section applies to client-side scenarios.*

Client-side components call web APIs using a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service, which is focused on making requests back to the server of origin. Additional [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service configurations for other web APIs can be created in developer code. Requests are composed using Blazor JSON helpers or with [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage). Requests can include [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) option configuration.

The configuration examples in this section are only useful when a single web API is called for a single [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instance in the app. When the app must call multiple web APIs, each with its own base address and configuration, you can adopt the following approaches, which are covered in the next two sections of this article:

* [Named `HttpClient` with `IHttpClientFactory`](#named-httpclient-with-ihttpclientfactory): Each web API is provided a unique name. When app code or a Razor component calls a web API, it uses a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instance to make the call.
* [Typed `HttpClient`](#typed-httpclient): Each web API is typed. When app code or a Razor component calls a web API, it uses a typed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instance to make the call.

In the `Program` file, add an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service if it isn't already present from a Blazor project template used to create the app:

```
builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
```

The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` ([IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress)), which gets the base address for the app and is typically derived from the `<base>` tag's `href` value in the host page.

The most common use cases for using the client's own base address are:

* The client project (`.Client`) of a Blazor Web App (.NET 8 or later) makes web API calls from WebAssembly components or code that runs on the client in WebAssembly to APIs in the server app.
* The client project (**Client**) of a hosted Blazor WebAssembly app makes web API calls to the server project (**Server**). Note that the Hosted Blazor WebAssembly project template is no longer available in .NET 8 or later. However, hosted Blazor WebAssembly apps remain supported for .NET 8.

If you're calling an external web API (not in the same URL space as the client app), set the URI to the web API's base address. The following example sets the base address of the web API to `https://localhost:5001`, where a separate web API app is running and ready to respond to requests from the client app:

```
builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri("https://localhost:5001") });
```

## Named `HttpClient` with `IHttpClientFactory`

*The guidance in this section applies to using a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) in a Blazor WebAssembly app, the `.Client` project of a Blazor Web App, and for prerendered client-side components of a Blazor Web App. For general guidance on HTTP client patterns in ASP.NET Core apps (for example, when you use an HTTP client only in the server project of a Blazor Web App or in a Blazor Server app), see [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0#review-consumption-patterns).*

[IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) services and the configuration of a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) are supported for classes and Razor components in a Blazor WebAssembly app, the `.Client` project of a Blazor Web App, and for prerendered client-side components in a Blazor Web App.

Note

An alternative to using a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) from an [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) is to use a typed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient). For more information, see the [Typed `HttpClient`](#typed-httpclient) section.

Add the [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http) NuGet package to the project.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the `Program` file of the project, register HTTP client services, giving a name to the client. In the following example, the name is `WebAPI`:

```
builder.Services.AddHttpClient("WebAPI", client => 
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
```

If the named client is to be used by prerendered client-side components of a Blazor Web App, the preceding service registration should appear in both the server project and the `.Client` project. On the server, `builder.HostEnvironment.BaseAddress` is replaced by the web API's base address, which is described further below.

The preceding client-side example sets the base address with `builder.HostEnvironment.BaseAddress` ([IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress)), which gets the base address for the client-side app and is typically derived from the `<base>` tag's `href` value in the host page.

The most common use cases for using the client's own base address are:

* The client project (`.Client`) of a Blazor Web App that makes web API calls from WebAssembly/Auto components or code that runs on the client in WebAssembly to APIs in the server app at the same host address.
* The client project (**Client**) of a hosted Blazor WebAssembly app that makes web API calls to the server project (**Server**).

The most common use case for using the client's own base address is in the client project (**Client**) of a hosted Blazor WebAssembly app that makes web API calls to the server project (**Server**).

If you're calling an external web API (not in the same URL space as the client app) or you're configuring the services in a server-side app (for example to deal with prerendering of client-side components on the server), set the URI to the web API's base address. The following example sets the base address of the web API to `https://localhost:5001`, where a separate web API app is running and ready to respond to requests from the client app:

```
builder.Services.AddHttpClient("WebAPI", client => 
    client.BaseAddress = new Uri("https://localhost:5001"));
```

In the following component code:

* An instance of [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) creates a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).
* The named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is used to issue a GET request for JSON weather forecast data from the web API at `/forecast`.

```
@inject IHttpClientFactory ClientFactory

...

@code {
    private Forecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        var client = ClientFactory.CreateClient("WebAPI");

        forecasts = await client.GetFromJsonAsync<Forecast[]>("forecast") ?? [];
    }
}
```

The `BlazorWebAppCallWebApi` [sample app](#sample-apps) demonstrates calling a web API with a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) in its `CallTodoWebApiCsrNamedClient` component. For an additional working demonstration in a client app based on calling Microsoft Graph with a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient), see [Use Graph API with ASP.NET Core Blazor WebAssembly](security/webassembly/graph-api?view=aspnetcore-10.0\&pivots=named-client-graph-api).

For a working demonstration in a client app based on calling Microsoft Graph with a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient), see [Use Graph API with ASP.NET Core Blazor WebAssembly](security/webassembly/graph-api?view=aspnetcore-10.0\&pivots=named-client-graph-api).

## Typed `HttpClient`

*The guidance in this section applies to using a typed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) in a Blazor WebAssembly app, the `.Client` project of a Blazor Web App, and for prerendered client-side components of a Blazor Web App. For general guidance on HTTP client patterns in ASP.NET Core apps (for example, when you use an HTTP client only in the server project of a Blazor Web App or in a Blazor Server app), see [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0#review-consumption-patterns).*

Typed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) uses one or more of the app's [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances, default or named, to return data from one or more web API endpoints. Typed clients are supported for classes and Razor components in a Blazor WebAssembly app, the `.Client` project of a Blazor Web App, and for prerendered client-side components in a Blazor Web App.

Note

An alternative to using a typed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is to use a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) from an [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory). For more information, see the [Named `HttpClient` with `IHttpClientFactory`](#named-httpclient-with-ihttpclientfactory) section.

Add the [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http) NuGet package to the project.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

The following example issues a GET request for JSON weather forecast data from the web API at `/forecast`.

`ForecastHttpClient.cs`:

```
using System.Net.Http.Json;

namespace BlazorSample.Client;

public class ForecastHttpClient(HttpClient http)
{
    public async Task<Forecast[]> GetForecastAsync() => 
        await http.GetFromJsonAsync<Forecast[]>("forecast") ?? [];
}
```

In the `Program` file of the project:

```
builder.Services.AddHttpClient<ForecastHttpClient>(client => 
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
```

If the typed client is to be used by prerendered client-side components of a Blazor Web App, the preceding service registration should appear in both the server project and the `.Client` project. On the server, `builder.HostEnvironment.BaseAddress` is replaced by the web API's base address, which is described further below.

The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` ([IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress)), which gets the base address for the client-side app and is typically derived from the `<base>` tag's `href` value in the host page.

The most common use cases for using the client's own base address are:

* The client project (`.Client`) of a Blazor Web App that makes web API calls from WebAssembly/Auto components or code that runs on the client in WebAssembly to APIs in the server app at the same host address.
* The client project (**Client**) of a hosted Blazor WebAssembly app that makes web API calls to the server project (**Server**).

The most common use case for using the client's own base address is in the client project (**Client**) of a hosted Blazor WebAssembly app that makes web API calls to the server project (**Server**).

If you're calling an external web API (not in the same URL space as the client app) or you're configuring the services in a server-side app (for example to deal with prerendering of client-side components on the server), set the URI to the web API's base address. The following example sets the base address of the web API to `https://localhost:5001`, where a separate web API app is running and ready to respond to requests from the client app:

```
builder.Services.AddHttpClient<ForecastHttpClient>(client => 
    client.BaseAddress = new Uri("https://localhost:5001"));
```

Components inject the typed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to call the web API.

In the following component code:

* An instance of the preceding `ForecastHttpClient` is injected, which creates a typed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).
* The typed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is used to issue a GET request for JSON weather forecast data from the web API.

```
@inject ForecastHttpClient Http

...

@code {
    private Forecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetForecastAsync();
    }
}
```

The `BlazorWebAppCallWebApi` [sample app](#sample-apps) demonstrates calling a web API with a typed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) in its `CallTodoWebApiCsrTypedClient` component. Note that the component adopts and client-side rendering (CSR) (`InteractiveWebAssembly` render mode) ***with prerendering***, so the typed client service registration appears in the `Program` file of both the server project and the `.Client` project.

## Accessing services outside of the `HttpClient`'s scope

[IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) creates [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) instances in a separate dependency injection (DI) scope from the app. If you inject a scoped service into a derived [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) type, the handler doesn't have access to the service from the Blazor circuit.

For an example of how to access a service in outgoing request middleware using either an application scope handler or a circuit activity handler, see [ASP.NET Core Blazor additional server-side security scenarios](security/additional-scenarios?view=aspnetcore-10.0#access-authenticationstateprovider-in-outgoing-request-middleware).

For more information on [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) instances, see [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0#outgoing-request-middleware).

## Disposal of `HttpRequestMessage`, `HttpResponseMessage`, and `HttpClient`

An [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) without a body doesn't require explicit disposal. However, you can dispose of it with either of the following patterns:

* `using` declaration (C# 8 or later):

  ```
  using var request = new HttpRequestMessage(...);
  ```

* [`using` block (all C# releases)](/en-us/dotnet/csharp/language-reference/keywords/using):

  ```
  using (var request = new HttpRequestMessage(...))
  {
      ...
  }
  ```

We recommend disposing of every [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) with every use for the following reasons:

* To gain a performance improvement by avoiding finalizers.
* It hardens the code for the future in case a request body is ever added to an [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) that didn't initially have one.
* To potentially avoid functional issues if a delegating handler expects a call to [Dispose](/en-us/dotnet/api/system.idisposable.dispose)/[DisposeAsync](/en-us/dotnet/api/system.iasyncdisposable.disposeasync).
* It's simpler to apply a general rule everywhere than trying to remember specific cases.

***Always*** dispose of [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage) instances.

***Never*** dispose of [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances created by calling [CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient) because they're managed by the framework.

Example:

```
using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
var client = clientFactory.CreateClient("ExternalApi");
using var response = await client.SendAsync(request);
```

## Prerendered data

When prerendering, components render twice: first statically, then interactively. State doesn't automatically flow from the prerendered component to the interactive one. If a component performs asynchronous initialization operations and renders different content for different states during initialization, such as a "Loading..." progress indicator, you may see a flicker when the component renders twice.

You can address this by flowing prerendered state using the Persistent Component State API, which the `BlazorWebAppCallWebApi` and `BlazorWebAppCallWebApi_Weather` [sample apps](#sample-apps) demonstrate. When the component renders interactively, it can render the same way using the same state. For more information, see the following resources:

* [ASP.NET Core Blazor prerendered state persistence](state-management/prerendered-state-persistence?view=aspnetcore-10.0)
* [ASP.NET Core Blazor navigation](fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling)

Note

The Persistent Component State API only supports enhanced navigation in .NET 10 or later. For apps that target .NET 8 or .NET 9, you can disable enhanced navigation on links to the page with the `data-enhance-nav` attribute set to `false`. For more information, see [ASP.NET Core Blazor navigation](fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling).

## JSON helpers

The [`System.Net.Http.Json` package](https://www.nuget.org/packages/System.Net.Http.Json) provides extension methods for [System.Net.Http.HttpClient](/en-us/dotnet/api/system.net.http.httpclient) and [System.Net.Http.HttpContent](/en-us/dotnet/api/system.net.http.httpcontent) that perform automatic serialization and deserialization using [`System.Text.Json`](https://www.nuget.org/packages/System.Text.Json). The `System.Net.Http.Json` package is provided by the .NET shared framework and doesn't require adding a package reference to the app.

[HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is available as a preconfigured service for making requests back to the origin server. [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) and JSON helpers ([System.Net.Http.Json.HttpClientJsonExtensions](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions)) are also used to call third-party web API endpoints. [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is implemented using the browser's [Fetch API](https://developer.mozilla.org/docs/Web/API/Fetch_API) and is subject to its limitations, including enforcement of the same-origin policy, which is discussed later in this article in the *Cross-Origin Resource Sharing (CORS)* section.

The client's base address is set to the originating server's address. Inject an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instance into a component using the [`@inject`](../mvc/views/razor?view=aspnetcore-10.0#inject) directive:

```
@using System.Net.Http
@inject HttpClient Http
```

Use the [System.Net.Http.Json](/en-us/dotnet/api/system.net.http.json) namespace for access to [HttpClientJsonExtensions](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions), including [GetFromJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.getfromjsonasync), [PutAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.putasjsonasync), and [PostAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.postasjsonasync):

```
@using System.Net.Http.Json
```

The following sections cover JSON helpers:

* [GET](#get-from-json-getfromjsonasync)
* [POST](#post-as-json-postasjsonasync)
* [PUT](#put-as-json-putasjsonasync)
* [PATCH](#patch-as-json-patchasjsonasync)

[System.Net.Http](/en-us/dotnet/api/system.net.http) includes additional methods for sending HTTP requests and receiving HTTP responses, for example to send a DELETE request. For more information, see the [DELETE and additional extension methods](#delete-deleteasync-and-additional-extension-methods) section.

## GET from JSON (`GetFromJsonAsync`)

[GetFromJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.getfromjsonasync) sends an HTTP GET request and parses the JSON response body to create an object.

In the following component code, the `todoItems` are displayed by the component. [GetFromJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.getfromjsonasync) is called when the component is finished initializing ([`OnInitializedAsync`](components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync)).

```
todoItems = await Http.GetFromJsonAsync<TodoItem[]>("todoitems");
```

## POST as JSON (`PostAsJsonAsync`)

[PostAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.postasjsonasync) sends a POST request to the specified URI containing the value serialized as JSON in the request body.

In the following component code, `newItemName` is provided by a bound element of the component. The `AddItem` method is triggered by selecting a `<button>` element.

```
await Http.PostAsJsonAsync("todoitems", addItem);
```

[PostAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.postasjsonasync) returns an [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage). To deserialize the JSON content from the response message, use the [ReadFromJsonAsync](/en-us/dotnet/api/system.net.http.json.httpcontentjsonextensions.readfromjsonasync) extension method. The following example reads JSON weather data as an array:

```
var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ?? 
    Array.Empty<WeatherForecast>();
```

## PUT as JSON (`PutAsJsonAsync`)

[PutAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.putasjsonasync) sends an HTTP PUT request with JSON-encoded content.

In the following component code, `editItem` values for `Name` and `IsCompleted` are provided by bound elements of the component. The item's `Id` is set when the item is selected in another part of the UI (not shown) and `EditItem` is called. The `SaveItem` method is triggered by selecting the `<button>` element. The following example doesn't show loading `todoItems` for brevity. See the [GET from JSON (`GetFromJsonAsync`)](#get-from-json-getfromjsonasync) section for an example of loading items.

```
await Http.PutAsJsonAsync($"todoitems/{editItem.Id}", editItem);
```

[PutAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.putasjsonasync) returns an [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage). To deserialize the JSON content from the response message, use the [ReadFromJsonAsync](/en-us/dotnet/api/system.net.http.json.httpcontentjsonextensions.readfromjsonasync) extension method. The following example reads JSON weather data as an array:

```
var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ?? 
    Array.Empty<WeatherForecast>();
```

## PATCH as JSON (`PatchAsJsonAsync`)

[PatchAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.patchasjsonasync) sends an HTTP PATCH request with JSON-encoded content.

Note

For more information, see [JsonPatch in ASP.NET Core web API](../web-api/jsonpatch?view=aspnetcore-10.0).

In the following example, [PatchAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.patchasjsonasync) receives a JSON PATCH document as a plain text string with escaped quotes:

```
await Http.PatchAsJsonAsync(
    $"todoitems/{id}", 
    "[{\"operationType\":2,\"path\":\"/IsComplete\",\"op\":\"replace\",\"value\":true}]");
```

As of C# 11 (.NET 7), you can compose a JSON string as a [raw string literal](/en-us/dotnet/csharp/language-reference/tokens/raw-string). Specify JSON syntax with the [StringSyntaxAttribute.Json](/en-us/dotnet/api/system.diagnostics.codeanalysis.stringsyntaxattribute.json) field to the [`[StringSyntax]` attribute](/en-us/dotnet/api/system.diagnostics.codeanalysis.stringsyntaxattribute) for code analysis tooling:

```
@using System.Diagnostics.CodeAnalysis

...

@code {
    [StringSyntax(StringSyntaxAttribute.Json)]
    private const string patchOperation =
        """[{"operationType":2,"path":"/IsComplete","op":"replace","value":true}]""";

    ...

    await Http.PatchAsJsonAsync($"todoitems/{id}", patchOperation);
}
```

[PatchAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.patchasjsonasync) returns an [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage). To deserialize the JSON content from the response message, use the [ReadFromJsonAsync](/en-us/dotnet/api/system.net.http.json.httpcontentjsonextensions.readfromjsonasync) extension method. The following example reads JSON todo item data as an array. An empty array is created if no item data is returned by the method, so `content` isn't null after the statement executes:

```
using var response = await Http.PatchAsJsonAsync(...);
var content = await response.Content.ReadFromJsonAsync<TodoItem[]>() ??
    Array.Empty<TodoItem>();
```

Laid out with indentation, spacing, and unescaped quotes, the unencoded PATCH document appears as the following JSON:

```
[
  {
    "operationType": 2,
    "path": "/IsComplete",
    "op": "replace",
    "value": true
  }
]
```

To simplify the creation of PATCH documents in the app issuing PATCH requests, an app can use .NET JSON PATCH support, as the following guidance demonstrates.

Install the [`Microsoft.AspNetCore.JsonPatch.SystemTextJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch.SystemTextJson) NuGet package and use the API features of the package to compose a [JsonPatchDocument](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.jsonpatchdocument) for a PATCH request.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Add `@using` directives for the [System.Text.Json](/en-us/dotnet/api/system.text.json), [System.Text.Json.Serialization](/en-us/dotnet/api/system.text.json.serialization), and `Microsoft.AspNetCore.JsonPatch.SystemTextJson` namespaces to the top of the Razor component:

```
@using System.Text.Json
@using System.Text.Json.Serialization
@using Microsoft.AspNetCore.JsonPatch.SystemTextJson
```

Compose the `JsonPatchDocument` for a `TodoItem` with `IsComplete` set to `true` using the `JsonPatchDocument.Replace` method:

```
var patchDocument = new JsonPatchDocument<TodoItem>()
    .Replace(p => p.IsComplete, true);
```

Install the [`Microsoft.AspNetCore.JsonPatch`](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch) NuGet package and use the API features of the package to compose a [JsonPatchDocument](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.jsonpatchdocument) for a PATCH request.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Add `@using` directives for the [System.Text.Json](/en-us/dotnet/api/system.text.json), [System.Text.Json.Serialization](/en-us/dotnet/api/system.text.json.serialization), and [Microsoft.AspNetCore.JsonPatch](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch) namespaces to the top of the Razor component:

```
@using System.Text.Json
@using System.Text.Json.Serialization
@using Microsoft.AspNetCore.JsonPatch
```

Compose the [JsonPatchDocument](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.jsonpatchdocument) for a `TodoItem` with `IsComplete` set to `true` using the [Replace](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.jsonpatchdocument.replace) method:

```
var patchDocument = new JsonPatchDocument<TodoItem>()
    .Replace(p => p.IsComplete, true);
```

Pass the document's operations (`patchDocument.Operations`) to the [PatchAsJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.patchasjsonasync) call:

```
private async Task UpdateItem(long id)
{
    await Http.PatchAsJsonAsync(
        $"todoitems/{id}", 
        patchDocument.Operations, 
        new JsonSerializerOptions()
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
        });
}
```

[JsonSerializerOptions.DefaultIgnoreCondition](/en-us/dotnet/api/system.text.json.jsonserializeroptions.defaultignorecondition#system-text-json-jsonserializeroptions-defaultignorecondition) is set to [JsonIgnoreCondition.WhenWritingDefault](/en-us/dotnet/api/system.text.json.serialization.jsonignorecondition#system-text-json-serialization-jsonignorecondition-whenwritingdefault) to ignore a property only if it equals the default value for its type.

Add [JsonSerializerOptions.WriteIndented](/en-us/dotnet/api/system.text.json.jsonserializeroptions.writeindented#system-text-json-jsonserializeroptions-writeindented) set to `true` if you want to present the JSON payload in a pleasant format for display. Writing indented JSON has no bearing on processing PATCH requests and isn't typically performed in production apps for web API requests.

Follow the guidance in the [JsonPatch in ASP.NET Core web API](../web-api/jsonpatch?view=aspnetcore-10.0) article to add a PATCH controller action to the web API. Alternatively, PATCH request processing can be implemented as a [Minimal API](../fundamentals/minimal-apis?view=aspnetcore-10.0) with the following steps.

Add a package reference for the [`Microsoft.AspNetCore.JsonPatch.SystemTextJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch.SystemTextJson) NuGet package to the web API app.

In the `Program` file add an `@using` directive for the [Microsoft.AspNetCore.JsonPatch.SystemTextJson](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch.systemtextjson) namespace:

```
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
```

Add a package reference for the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson) NuGet package to the web API app.

Note

There's no need to add a package reference for the [`Microsoft.AspNetCore.JsonPatch` package](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch) to the app because the reference to the `Microsoft.AspNetCore.Mvc.NewtonsoftJson` package automatically transitively adds a package reference for `Microsoft.AspNetCore.JsonPatch`.

In the `Program` file add an `@using` directive for the [Microsoft.AspNetCore.JsonPatch](/en-us/dotnet/api/microsoft.aspnetcore.jsonpatch) namespace:

```
using Microsoft.AspNetCore.JsonPatch;
```

Provide the endpoint to the request processing pipeline of the web API:

```
app.MapPatch("/todoitems/{id}", async (long id, TodoContext db) =>
{
    if (await db.TodoItems.FindAsync(id) is TodoItem todo)
    {
        var patchDocument = 
            new JsonPatchDocument<TodoItem>().Replace(p => p.IsComplete, true);
        patchDocument.ApplyTo(todo);
        await db.SaveChangesAsync();

        return TypedResults.Ok(todo);
    }

    return TypedResults.NotFound();
});
```

Warning

As with the other examples in the [JsonPatch in ASP.NET Core web API](../web-api/jsonpatch?view=aspnetcore-10.0) article, the preceding PATCH API doesn't protect the web API from over-posting attacks. For more information, see [Tutorial: Create a controller-based web API with ASP.NET Core](../tutorials/first-web-api?view=aspnetcore-10.0#prevent-over-posting).

For a fully working PATCH experience, see the `BlazorWebAppCallWebApi` [sample app](#sample-apps).

## DELETE (`DeleteAsync`) and additional extension methods

[System.Net.Http](/en-us/dotnet/api/system.net.http) includes additional extension methods for sending HTTP requests and receiving HTTP responses. [HttpClient.DeleteAsync](/en-us/dotnet/api/system.net.http.httpclient.deleteasync) is used to send an HTTP DELETE request to a web API.

In the following component code, the `<button>` element calls the `DeleteItem` method. The bound `<input>` element supplies the `id` of the item to delete.

```
await Http.DeleteAsync($"todoitems/{id}");
```

## Client-side request streaming

For Chromium-based browsers (for example, Google Chrome and Microsoft Edge) using the HTTP/2 protocol, and HTTPS, client-side Blazor uses [Streams API](https://developer.mozilla.org/docs/Web/API/Streams_API) to permit [request streaming](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests).

To enable request streaming, set [SetBrowserRequestStreamingEnabled](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserrequeststreamingenabled) to `true` on the [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage).

In the following file upload example:

* `content` is the file's [HttpContent](/en-us/dotnet/api/system.net.http.httpcontent).
* `/Filesave` is the web API endpoint.
* `Http` is the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).

```
using var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave");
request.SetBrowserRequestStreamingEnabled(true);
request.Content = content;

using var response = await Http.SendAsync(request);
```

Streaming requests:

* Require HTTPS protocol and don't work on HTTP/1.x.
* Include a body but not a `Content-Length` header. [CORS](../security/cors?view=aspnetcore-10.0) with a preflight request is required for cross-origin streaming requests.

For more information on file uploads with an [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component, see [ASP.NET Core Blazor file uploads](file-uploads?view=aspnetcore-10.0#file-size-read-and-upload-limits) and the example at [Upload files to a server with client-side rendering (CSR)](file-uploads?view=aspnetcore-10.0#upload-files-to-a-server-with-client-side-rendering-csr).

## Cookie-based request credentials

*The guidance in this section applies to client-side scenarios that rely upon an authentication cookie.*

For cookie-based authentication, which is considered more secure than bearer token authentication, cookie credentials can be sent with each web API request by calling [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler) with a [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) on a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient). The handler configures [SetBrowserRequestCredentials](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserrequestcredentials) with [BrowserRequestCredentials.Include](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.browserrequestcredentials#microsoft-aspnetcore-components-webassembly-http-browserrequestcredentials-include), which advises the browser to send credentials with each request, such as cookies or HTTP authentication headers, including for cross-origin requests.

`CookieHandler.cs`:

```
public class CookieHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        request.Headers.Add("X-Requested-With", [ "XMLHttpRequest" ]);

        return base.SendAsync(request, cancellationToken);
    }
}
```

Note

For guidance on how to access an `AuthenticationStateProvider` from a `DelegatingHandler`, see [ASP.NET Core Blazor additional server-side security scenarios](security/additional-scenarios?view=aspnetcore-10.0#access-authenticationstateprovider-in-outgoing-request-middleware).

The `CookieHandler` is registered in the `Program` file:

```
builder.Services.AddTransient<CookieHandler>();
```

The message handler is added to any preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) that requires cookie authentication:

```
builder.Services.AddHttpClient(...)
    .AddHttpMessageHandler<CookieHandler>();
```

For a demonstration, see [Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](security/webassembly/standalone-with-identity/?view=aspnetcore-10.0).

When composing an [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage), set the browser request credentials and header directly:

```
using var request = new HttpRequestMessage() { ... };

request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
request.Headers.Add("X-Requested-With", [ "XMLHttpRequest" ]);
```

## Use a token handler for web API calls

Blazor Web Apps with OIDC authentication can use a token handler approach to make outgoing requests to secure external web API calls. This approach is used by the `BlazorWebAppOidc` and `BlazorWebAppOidcServer` sample apps described in the *Sample apps* section of this article.

For more information, see the following resources:

* [ASP.NET Core Blazor additional server-side security scenarios](security/additional-scenarios?view=aspnetcore-10.0#use-a-token-handler-for-web-api-calls)

* *Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)*

  * [Without YARP and Aspire (Interactive Auto)](security/blazor-web-app-with-oidc?view=aspnetcore-10.0\&pivots=without-yarp-and-aspire)
  * [Without YARP and Aspire (Interactive Server)](security/blazor-web-app-with-oidc?view=aspnetcore-10.0\&pivots=without-yarp-and-aspire-server)

## Microsoft identity platform for web API calls

Blazor Web Apps that use use [Microsoft identity platform](/en-us/entra/identity-platform/) with [Microsoft Identity Web packages](/en-us/entra/msal/dotnet/microsoft-identity-web/) for [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra) can make streamlined web API calls with API provided by the [`Microsoft.Identity.Web.DownstreamApi` NuGet package](https://www.nuget.org/packages/Microsoft.Identity.Web.DownstreamApi).

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the app settings file (`appsettings.json`), provide a base URL and scopes. In the following example, the `{BASE ADDRESS}` placeholder is the base URL of the web API. A single scope is specified with an App ID URI (`{APP ID URI}` placeholder) and scope name (`{SCOPE NAME}` placeholder):

```
"DownstreamApi": {
  "BaseUrl": "{BASE ADDRESS}",
  "Scopes": [ "{APP ID URI}/{SCOPE NAME}" ]
}
```

Example:

```
"DownstreamApi": {
  "BaseUrl": "https://localhost:7277",
  "Scopes": [ "api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get" ]
}
```

In the app's `Program` file, call:

* [EnableTokenAcquisitionToCallDownstreamApi](/en-us/dotnet/api/microsoft.identity.web.microsoftidentitywebapiauthenticationbuilder.enabletokenacquisitiontocalldownstreamapi): Enables token acquisition to call web APIs.

* [AddDownstreamApi](/en-us/dotnet/api/microsoft.identity.web.downstreamapiextensions.adddownstreamapi): Microsoft Identity Web packages provide API to create a named downstream web service for making web API calls. [IDownstreamApi](/en-us/dotnet/api/microsoft.identity.abstractions.idownstreamapi) is injected into a server-side class, which is used to call [CallApiForUserAsync](/en-us/dotnet/api/microsoft.identity.abstractions.idownstreamapi.callapiforuserasync) to obtain weather data from an external web API (`MinimalApiJwt` project).

* [AddDistributedTokenCaches](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.distributedtokencacheadapterextension.adddistributedtokencaches): Adds the .NET distributed token caches to the service collection.

* [AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache): Adds a default implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) that stores cache items in memory.

* Configure the distributed token cache options ([MsalDistributedTokenCacheAdapterOptions](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions)):

  * In development for debugging purposes, you can disable the L1 cache by setting [DisableL1Cache](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.disablel1cache) to `true`. ***Be sure to reset it back to `false` for production.***
  * Set the maximum size of your L1 cache with [`L1CacheOptions.SizeLimit`](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions.sizelimit) to prevent the cache from overrunning the server's memory. The default value is 500 MB.
  * In development for debugging purposes, you can disable token encryption at rest by setting [Encrypt](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.encrypt) to `false`, which is the default value. ***Be sure to reset it back to `true` for production.***
  * Set token eviction from the cache with [SlidingExpiration](/en-us/dotnet/api/microsoft.extensions.caching.distributed.distributedcacheentryoptions.slidingexpiration). The default value is 1 hour.
  * For more information, including guidance on the callback for L2 cache failures ([OnL2CacheFailure](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.onl2cachefailure)) and asynchronous L2 cache writes ([EnableAsyncL2Write](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.enableasyncl2write)), see [MsalDistributedTokenCacheAdapterOptions](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions) and [Token cache serialization: Distributed token caches](/en-us/entra/msal/dotnet/how-to/token-cache-serialization#distributed-token-caches).

You can choose to encrypt the cache and should always do so in production.

```
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", 
        builder.Configuration.GetSection("DownstreamApi"))
    .AddDistributedTokenCaches();

// Requires the 'Microsoft.Extensions.Caching.Memory' NuGet package
builder.Services.AddDistributedMemoryCache();

builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(
    options => 
    {
        // The following lines that are commented out reflect
        // default values. We recommend overriding the default
        // value of Encrypt to encrypt tokens at rest.

        //options.DisableL1Cache = false;
        //options.L1CacheOptions.SizeLimit = 500 * 1024 * 1024;
        options.Encrypt = true;
        //options.SlidingExpiration = TimeSpan.FromHours(1);
    });
```

In-memory distributed token caches are created when calling [AddDistributedTokenCaches](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.distributedtokencacheadapterextension.adddistributedtokencaches) to ensure that there's a base implementation available for distributed token caching.

Production web apps and web APIs should use a production distributed token cache (for example: [Redis](https://redis.io/), [Microsoft SQL Server](https://www.microsoft.com/sql-server), [Microsoft Azure Cosmos DB](https://azure.microsoft.com/products/cosmos-db)).

Note

For local development and testing on a single machine, you can use in-memory token caches instead of distributed token caches:

```
builder.Services.AddInMemoryTokenCaches();
```

Later in the development and testing period, adopt a production distributed token cache provider.

[AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache) adds a default implementation of [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) that stores cache items in memory, which is used by Microsoft Identity Web for token caching.

[AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache) requires a package reference to the [`Microsoft.Extensions.Caching.Memory` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory).

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

To configure a production distributed cache provider, see [Distributed caching in ASP.NET Core](../performance/caching/distributed?view=aspnetcore-10.0).

Warning

Always replace the in-memory distributed token caches with a real token cache provider when deploying the app to a production environment. If you fail to adopt a production distributed token cache provider, the app may suffer significantly degraded performance.

For more information, see [Token cache serialization: Distributed caches](/en-us/entra/msal/dotnet/how-to/token-cache-serialization?tabs=msal#distributed-caches). However, the code examples shown don't apply to ASP.NET Core apps, which configure distributed caches via [AddDistributedMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.adddistributedmemorycache), not [AddDistributedTokenCache](/en-us/dotnet/api/microsoft.identity.web.tokencacheextensions.adddistributedtokencache).

Use a shared Data Protection key ring in production so that instances of the app across servers in a web farm can decrypt tokens when [MsalDistributedTokenCacheAdapterOptions.Encrypt](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.encrypt) is set to `true`.

Note

For early development and local testing on a single machine, you can set [Encrypt](/en-us/dotnet/api/microsoft.identity.web.tokencacheproviders.distributed.msaldistributedtokencacheadapteroptions.encrypt) to `false` and configure a shared Data Protection key ring later:

```
options.Encrypt = false;
```

Later in the development and testing period, enable token encryption and adopt a shared Data Protection key ring.

The following example shows how to use [Azure Blob Storage and Azure Key Vault (`PersistKeysToAzureBlobStorage`/`ProtectKeysWithAzureKeyVault`)](../security/data-protection/configuration/overview?view=aspnetcore-10.0#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault) for the shared key ring. The service configurations are base case scenarios for demonstration purposes. Before deploying production apps, familiarize yourself with the Azure services and adopt best practices using the Azure services' dedicated documentation sets, which are linked at the end of this section.

Add the following packages to the server project of the Blazor Web App:

* [`Azure.Extensions.AspNetCore.DataProtection.Blobs`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Blobs)
* [`Azure.Extensions.AspNetCore.DataProtection.Keys`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Keys)

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Note

Before proceeding with the following steps, confirm that the app is registered with Microsoft Entra.

Configure Azure Blob Storage to maintain data protection keys. Follow the guidance in [Key storage providers in ASP.NET Core](../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0#azure-storage).

Configure Azure Key Vault to encrypt the data protection keys at rest. Follow the guidance in [Configure ASP.NET Core Data Protection](../security/data-protection/configuration/overview?view=aspnetcore-10.0#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault).

Use the following code in the `Program` file where services are registered:

```
TokenCredential? credential;

if (builder.Environment.IsProduction())
{
    credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
    // Local development and testing only
    DefaultAzureCredentialOptions options = new()
    {
        // Specify the tenant ID to use the dev credentials when running the app locally
        // in Visual Studio.
        VisualStudioTenantId = "{TENANT ID}",
        SharedTokenCacheTenantId = "{TENANT ID}"
    };

    credential = new DefaultAzureCredential(options);
}

builder.Services.AddDataProtection()
    .SetApplicationName("{APPLICATION NAME}")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential)
    .ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), credential);
```

`{MANAGED IDENTITY CLIENT ID}`: The Azure Managed Identity Client ID (GUID).

`{TENANT ID}`: Tenant ID.

`{APPLICATION NAME}`: [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the unique name of this app within the data protection system. The value should match across deployments of the app.

`{BLOB URI}`: Full URI to the key file. The URI is generated by Azure Storage when you create the key file. Do not use a SAS.

`{KEY IDENTIFIER}`: Azure Key Vault key identifier used for key encryption. An access policy allows the application to access the key vault with `Get`, `Unwrap Key`, and `Wrap Key` permissions. The key identifier is obtained from the key in the Entra or Azure portal after it's created. If you enable autorotation of the key vault key, make sure that you use a versionless key identifier in the app's key vault configuration, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`).

Note

In non-`Production` environments, the preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as the [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential) shown in the preceding example. For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

Inject [IDownstreamApi](/en-us/dotnet/api/microsoft.identity.abstractions.idownstreamapi) and call [CallApiForUserAsync](/en-us/dotnet/api/microsoft.identity.abstractions.idownstreamapi.callapiforuserasync) when calling on behalf of a user:

```
internal sealed class ServerWeatherForecaster(IDownstreamApi downstreamApi) : IWeatherForecaster
{
    public async Task<IEnumerable<WeatherForecast>> GetWeatherForecastAsync()
    {
        var response = await downstreamApi.CallApiForUserAsync("DownstreamApi",
            options =>
            {
                options.RelativePath = "/weather-forecast";
            });

        return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
            throw new IOException("No weather forecast!");
    }
}
```

This approach is used by the `BlazorWebAppEntra` and `BlazorWebAppEntraBff` sample apps described in the *Sample apps* section of this article.

For more information, see the following resources:

* [Key storage providers in ASP.NET Core](../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0#azure-storage)

* [Configure ASP.NET Core Data Protection](../security/data-protection/configuration/overview?view=aspnetcore-10.0#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault)

* [Use the Azure SDK for .NET in ASP.NET Core apps](/en-us/dotnet/azure/sdk/aspnetcore-guidance?tabs=api)

* [Web API documentation | Microsoft identity platform](/en-us/entra/identity-platform/index-web-api)

* [A web API that calls web APIs: Call an API: Option 2: Call a downstream web API with the helper class](/en-us/entra/identity-platform/scenario-web-api-call-api-call-api?tabs=aspnetcore#option-2-call-a-downstream-web-api-with-the-helper-class)

* [IDownstreamApi](/en-us/dotnet/api/microsoft.identity.abstractions.idownstreamapi)

* *Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID*

  * [With YARP and Aspire (Interactive Auto)](security/blazor-web-app-with-entra?view=aspnetcore-10.0\&pivots=with-yarp-and-aspire)
  * [Without YARP and Aspire (Interactive Auto)](security/blazor-web-app-with-entra?view=aspnetcore-10.0\&pivots=without-yarp-and-aspire)

* [Host ASP.NET Core in a web farm: Data Protection](../host-and-deploy/web-farm?view=aspnetcore-10.0#data-protection)

* [Azure Key Vault documentation](/en-us/azure/key-vault/general/)

* [Azure Storage documentation](/en-us/azure/storage/)

* [Provide access to Key Vault keys, certificates, and secrets with Azure role-based access control](/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli)

## `HttpClient` and `HttpRequestMessage` with Fetch API request options

*The guidance in this section applies to client-side scenarios that rely upon bearer token authentication.*

[`HttpClient`](../fundamentals/http-requests?view=aspnetcore-10.0) ([API documentation](/en-us/dotnet/api/system.net.http.httpclient)) and [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) can be used to customize requests. For example, you can specify the HTTP method and request headers. The following component makes a `POST` request to a web API endpoint and shows the response body.

`TodoRequest.razor`:

```
@page "/todo-request"
@using System.Net.Http.Headers
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject HttpClient Http
@inject IAccessTokenProvider TokenProvider

<h1>ToDo Request</h1>

<h1>ToDo Request Example</h1>

<button @onclick="PostRequest">Submit POST request</button>

<p>Response body returned by the server:</p>

<p>@responseBody</p>

@code {
    private string? responseBody;

    private async Task PostRequest()
    {
        using var request = new HttpRequestMessage()
        {
            Method = new HttpMethod("POST"),
            RequestUri = new Uri("https://localhost:10000/todoitems"),
            Content =
                JsonContent.Create(new TodoItem
                {
                    Name = "My New Todo Item",
                    IsComplete = false
                })
        };

        var tokenResult = await TokenProvider.RequestAccessToken();

        if (tokenResult.TryGetToken(out var token))
        {
            request.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token.Value);

            request.Content.Headers.TryAddWithoutValidation(
                "x-custom-header", "value");

            using var response = await Http.SendAsync(request);
            var responseStatusCode = response.StatusCode;

            responseBody = await response.Content.ReadAsStringAsync();
        }
    }

    public class TodoItem
    {
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
    }
}
```

Blazor's client-side implementation of [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) uses [Fetch API](https://developer.mozilla.org/docs/Web/API/fetch) and configures the underlying [request-specific Fetch API options](https://developer.mozilla.org/docs/Web/API/fetch#Parameters) via [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) extension methods and [WebAssemblyHttpRequestMessageExtensions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions). Set additional options using the generic [SetBrowserRequestOption](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserrequestoption) extension method. Blazor and the underlying Fetch API don't directly add or modify request headers. For more information on how user agents, such as browsers, interact with headers, consult external user agent documentation sets and other web resources.

Response streaming is enabled by default.

Calling [HttpContent.ReadAsStreamAsync](/en-us/dotnet/api/system.net.http.httpcontent.readasstreamasync) for an [HttpResponseMessage.Content](/en-us/dotnet/api/system.net.http.httpresponsemessage.content) (`response.Content.ReadAsStreamAsync()`) returns a [`BrowserHttpReadStream` (reference source)](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs), not a [MemoryStream](/en-us/dotnet/api/system.io.memorystream). `BrowserHttpReadStream` doesn't support synchronous operations, such as `Stream.Read(Span<Byte>)`. If your code uses synchronous operations, you can opt-out of response streaming or copy the [Stream](/en-us/dotnet/api/system.io.stream) into a [MemoryStream](/en-us/dotnet/api/system.io.memorystream) yourself.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

To opt-out of response streaming globally, use either of the following approaches:

* Add the `<WasmEnableStreamingResponse>` property to the project file with a value of `false`:

  ```
  <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
  ```

* Set the `DOTNET_WASM_ENABLE_STREAMING_RESPONSE` environment variable to `false` or `0`.

To opt-out of response streaming for an individual request, set [SetBrowserResponseStreamingEnabled](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserresponsestreamingenabled) to `false` on the [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) (`request` in the following example):

```
request.SetBrowserResponseStreamingEnabled(false);
```

The HTTP response is typically buffered to enable support for synchronous reads on the response content. To enable support for response streaming, set [SetBrowserResponseStreamingEnabled](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserresponsestreamingenabled) to `true` on the [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage):

```
request.SetBrowserResponseStreamingEnabled(true);
```

By default, [`HttpCompletionOption.ResponseContentRead`](/en-us/dotnet/api/system.net.http.httpcompletionoption) is set, which results in the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) completing after reading the entire response, including the content. In order to be able to use the [SetBrowserResponseStreamingEnabled](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserresponsestreamingenabled) option on large files, set [`HttpCompletionOption.ResponseHeadersRead`](/en-us/dotnet/api/system.net.http.httpcompletionoption) to avoid caching the file's content in memory:

```
- using var response = await Http.SendAsync(request);
+ using var response = await Http.SendAsync(request, 
+     HttpCompletionOption.ResponseHeadersRead);
```

To include credentials in a cross-origin request, use the [SetBrowserRequestCredentials](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserrequestcredentials) extension method:

```
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
```

For more information on Fetch API options, see [MDN web docs: WindowOrWorkerGlobalScope.fetch(): Parameters](https://developer.mozilla.org/docs/Web/API/fetch#Parameters).

## Handle errors

Handle web API response errors in developer code when they occur. For example, [GetFromJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.getfromjsonasync) expects a JSON response from the web API with a `Content-Type` of `application/json`. If the response isn't in JSON format, content validation throws a [NotSupportedException](/en-us/dotnet/api/system.notsupportedexception).

In the following example, the URI endpoint for the weather forecast data request is misspelled. The URI should be to `WeatherForecast` but appears in the call as `WeatherForcast`, which is missing the letter `e` in `Forecast`.

The [GetFromJsonAsync](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions.getfromjsonasync) call expects JSON to be returned, but the web API returns HTML for an unhandled exception with a `Content-Type` of `text/html`. The unhandled exception occurs because the path to `/WeatherForcast` isn't found and middleware can't serve a page or view for the request.

In [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) on the client, [NotSupportedException](/en-us/dotnet/api/system.notsupportedexception) is thrown when the response content is validated as non-JSON. The exception is caught in the `catch` block, where custom logic could log the error or present a friendly error message to the user.

`ReturnHTMLOnException.razor`:

```
@page "/return-html-on-exception"
@using {PROJECT NAME}.Shared
@inject HttpClient Http

<h1>Fetch data but receive HTML on unhandled exception</h1>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <h2>Temperatures by Date</h2>

    <ul>
        @foreach (var forecast in forecasts)
        {
            <li>
                @forecast.Date.ToShortDateString():
                @forecast.TemperatureC &#8451;
                @forecast.TemperatureF &#8457;
            </li>
        }
    </ul>
}

<p>
    @exceptionMessage
</p>

@code {
    private WeatherForecast[]? forecasts;
    private string? exceptionMessage;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            // The URI endpoint "WeatherForecast" is misspelled on purpose on the 
            // next line. See the preceding text for more information.
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForcast");
        }
        catch (NotSupportedException exception)
        {
            exceptionMessage = exception.Message;
        }
    }
}
```

Note

The preceding example is for demonstration purposes. A web API can be configured to return JSON even when an endpoint doesn't exist or an unhandled exception occurs on the server.

For more information, see [Handle errors in ASP.NET Core Blazor apps](fundamentals/handle-errors?view=aspnetcore-10.0).

## Cross-Origin Resource Sharing (CORS)

Browser security often restricts a webpage from making requests to a different origin than the one that served the webpage. This restriction is called the *same-origin policy*. The same-origin policy restricts (but doesn't prevent) a malicious site from reading sensitive data from another site. To make requests from the browser to an endpoint with a different origin, the *endpoint* must enable [Cross-Origin Resource Sharing (CORS)](https://www.w3.org/TR/cors/).

For more information on server-side CORS, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0). The article's examples don't pertain directly to Razor component scenarios, but the article is useful for learning general CORS concepts.

For information on client-side CORS requests, see [ASP.NET Core Blazor WebAssembly additional security scenarios](security/webassembly/additional-scenarios?view=aspnetcore-10.0#cross-origin-resource-sharing-cors).

## Antiforgery support

To add antiforgery support to an HTTP request, inject the `AntiforgeryStateProvider` and add a `RequestToken` to the headers collection as a `RequestVerificationToken`:

```
@inject AntiforgeryStateProvider Antiforgery
```

```
private async Task OnSubmit()
{
    var antiforgery = Antiforgery.GetAntiforgeryToken();
    using var request = new HttpRequestMessage(HttpMethod.Post, "action");
    request.Headers.Add("RequestVerificationToken", antiforgery.RequestToken);
    using var response = await client.SendAsync(request);
    ...
}
```

For more information, see [ASP.NET Core Blazor authentication and authorization](security/?view=aspnetcore-10.0#antiforgery-support).

## Blazor framework component examples for testing web API access

Various network tools are publicly available for testing web API backend apps directly, such as [Firefox Browser Developer](https://www.mozilla.org/firefox/developer/). Blazor framework's reference source includes [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) test assets that are useful for testing:

[`HttpClientTest` assets in the `dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore/tree/main/src/Components/test/testassets/BasicTestApp/HttpClientTest)

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Sample apps

For working examples, see the following sample apps in the [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples/) ([how to download](fundamentals/?view=aspnetcore-10.0#sample-apps)).

### `BlazorWebAppCallWebApi`

Call an external (not in the Blazor Web App) todo list web API from a Blazor Web App:

* `Backend`: A web API app for maintaining a todo list, based on [Minimal APIs](../fundamentals/minimal-apis?view=aspnetcore-10.0). The web API app is a separate app from the Blazor Web App, possibly hosted on a different server.
* `BlazorApp`/`BlazorApp.Client`: A Blazor Web App that calls the web API app with an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) for todo list operations, such as creating, reading, updating, and deleting (CRUD) items from the todo list.

For client-side rendering (CSR), which includes Interactive WebAssembly components and Auto components that have adopted CSR, calls are made with a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) registered in the `Program` file of the client project (`BlazorApp.Client`):

```
builder.Services.AddScoped(sp =>
    new HttpClient
    {
        BaseAddress = new Uri(builder.Configuration["FrontendUrl"] ?? 
            "https://localhost:5002")
    });
```

For server-side rendering (SSR), which includes prerendered and interactive Server components, prerendered WebAssembly components, and Auto components that are prerendered or have adopted SSR, calls are made with an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) registered in the `Program` file of the server project (`BlazorApp`):

```
builder.Services.AddHttpClient();
```

Call an internal (inside the Blazor Web App) movie list API, where the API resides in the server project of the Blazor Web App:

* `BlazorApp`: A Blazor Web App that maintains a movie list:

  * When operations are performed on the movie list within the app on the server, ordinary API calls are used.
  * When API calls are made by a web-based client, a web API is used for movie list operations, based on [Minimal APIs](../fundamentals/minimal-apis?view=aspnetcore-10.0).

* `BlazorApp.Client`: The client project of the Blazor Web App, which contains Interactive WebAssembly and Auto components for user management of the movie list.

For CSR, which includes Interactive WebAssembly components and Auto components that have adopted CSR, calls to the API are made via a client-based service (`ClientMovieService`) that uses a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) registered in the `Program` file of the client project (`BlazorApp.Client`). Because these calls are made over a public or private web, the movie list API is a *web API*.

The following example obtains a list of movies from the `/movies` endpoint:

```
public class ClientMovieService(HttpClient http) : IMovieService
{
    public async Task<Movie[]> GetMoviesAsync(bool watchedMovies) => 
        await http.GetFromJsonAsync<Movie[]>("movies") ?? [];
}
```

For SSR, which includes prerendered and interactive Server components, prerendered WebAssembly components, and Auto components that are prerendered or have adopted SSR, calls are made directly via a server-based service (`ServerMovieService`). The API doesn't rely on a network, so it's a standard API for movie list CRUD operations.

The following example obtains a list of movies:

```
public class ServerMovieService(MovieContext db) : IMovieService
{
    public async Task<Movie[]> GetMoviesAsync(bool watchedMovies) => 
        watchedMovies ? 
        await db.Movies.Where(t => t.IsWatched).ToArrayAsync() : 
        await db.Movies.ToArrayAsync();
}
```

For more information on how to secure movie data in this scenario, see the weather data example described by [Secure data in Blazor Web Apps with Interactive Auto rendering](security/?view=aspnetcore-10.0#secure-data-in-blazor-web-apps-with-interactive-auto-rendering).

### `BlazorWebAppCallWebApi_Weather`

A weather data sample app that uses streaming rendering for weather data.

### `BlazorWebAssemblyCallWebApi`

Calls a todo list web API from a Blazor WebAssembly app:

* `Backend`: A web API app for maintaining a todo list, based on [Minimal APIs](../fundamentals/minimal-apis?view=aspnetcore-10.0).
* `BlazorTodo`: A Blazor WebAssembly app that calls the web API with a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) for todo list CRUD operations.

### `BlazorWebAssemblyStandaloneWithIdentity`

A standalone Blazor WebAssembly app secured with ASP.NET Core Identity:

* `Backend`: A backend web API app that maintains a user identity store for ASP.NET Core Identity.
* `BlazorWasmAuth`: A standalone Blazor WebAssembly frontend app with user authentication.

The solution demonstrates calling a secure web API for the following:

* Obtaining an authenticated user's roles.
* Data processing for all authenticated users.
* Data processing for authorized users (the user must be in the `Manager` role) via an [authorization policy](../security/authorization/policies?view=aspnetcore-10.0).

### `BlazorWebAppOidcBffAuto`

A Blazor Web App with global Interactive Auto rendering that uses OIDC authentication with Microsoft Entra without using Entra-specific packages. The sample demonstrates how to [use a token handler for web API calls](security/additional-scenarios?view=aspnetcore-10.0#use-a-token-handler-for-web-api-calls) to call an external secure web API.

### `BlazorWebAppOidcBffServer`

A Blazor Web App with global Interactive Server rendering that uses OIDC authentication with Microsoft Entra without using Entra-specific packages. The sample demonstrates how to [pass an access token](security/additional-scenarios?view=aspnetcore-10.0#use-a-token-handler-for-web-api-calls) to call an external secure web API.

### `BlazorWebAppOidcBffYarpAspire`

A Blazor Web App with global Interactive Auto rendering that uses:

* OIDC authentication (Microsoft Entra without using Entra-specific packages)
* [YARP](https://dotnet.github.io/yarp/)
* [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview)

The solution includes a demonstration of obtaining weather data securely via an external web API when a component that adopts Interactive Auto rendering is rendered on the client.

### `BlazorWebAppEntraBff`

A Blazor Web App with global Auto interactivity that uses [Microsoft identity platform](/en-us/entra/identity-platform/) with [Microsoft Identity Web packages](/en-us/entra/msal/dotnet/microsoft-identity-web/) for [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra). The solution includes a demonstration of obtaining weather data securely via an external web API when a component that adopts Interactive Auto rendering is rendered on the client.

### `BlazorWebAppEntraBffYarpAspire`

A Blazor Web App with global Auto interactivity that uses:

* [Microsoft identity platform](/en-us/entra/identity-platform/) with [Microsoft Identity Web packages](/en-us/entra/msal/dotnet/microsoft-identity-web/) for [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra).
* [YARP](https://dotnet.github.io/yarp/).
* [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview).

The solution includes a demonstration of obtaining weather data securely via an external web API when a component that adopts Interactive Auto rendering is rendered on the client.

## Related content

### General

* [Cross-Origin Resource Sharing (CORS) at W3C](https://www.w3.org/TR/cors/)
* [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0): Although the content applies to ASP.NET Core apps, not Razor components, the article covers general CORS concepts.
* [Secure data in Blazor Web Apps with Interactive Auto rendering](security/?view=aspnetcore-10.0#secure-data-in-blazor-web-apps-with-interactive-auto-rendering)

- [Cross-Origin Resource Sharing (CORS) at W3C](https://www.w3.org/TR/cors/)
- [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../security/cors?view=aspnetcore-10.0): Although the content applies to ASP.NET Core apps, not Razor components, the article covers general CORS concepts.

### Mitigation of overposting attacks

Web APIs can be vulnerable to an *overposting* attack, also known as a *mass assignment* attack. An overposting attack occurs when a malicious user issues an HTML form POST to the server that processes data for properties that aren't part of the rendered form and that the developer doesn't wish to allow users to modify. The term "overposting" literally means that the malicious user has *over*-POSTed with the form.

For guidance on mitigating overposting attacks, see [Tutorial: Create a controller-based web API with ASP.NET Core](../tutorials/first-web-api?view=aspnetcore-10.0#prevent-over-posting).

### Server-side

* [ASP.NET Core Blazor additional server-side security scenarios](security/additional-scenarios?view=aspnetcore-10.0): Includes coverage on using [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to make secure web API requests.
* [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0)
* [Enforce HTTPS in ASP.NET Core](../security/enforcing-ssl?view=aspnetcore-10.0)
* [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0)

### Client-side

* [ASP.NET Core Blazor WebAssembly additional security scenarios](security/webassembly/additional-scenarios?view=aspnetcore-10.0): Includes coverage on using [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) to make secure web API requests.
* [Use Graph API with ASP.NET Core Blazor WebAssembly](security/webassembly/graph-api?view=aspnetcore-10.0)
* [Fetch API](https://developer.mozilla.org/docs/Web/API/fetch)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/app-base-path
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/app-base-path.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor app base path

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains the *app base path* in ASP.NET Core Blazor apps, including configuration guidance.

The *app base path* is the app's root URL path. Successful routing in Blazor apps requires framework configuration for any root URL path that isn't at the default app base path `/`.

Consider the following ASP.NET Core app and Blazor sub-app:

* The ASP.NET Core app is named `MyApp`:

  * The app physically resides at `d:/MyApp`.
  * Requests are received at `https://www.contoso.com/{MYAPP RESOURCE}`.

* A Blazor app named `CoolApp` is a sub-app of `MyApp`:

  * The sub-app physically resides at `d:/MyApp/CoolApp`.
  * Requests are received at `https://www.contoso.com/CoolApp/{COOLAPP RESOURCE}`.

Without specifying additional configuration for `CoolApp`, the sub-app in this scenario has no knowledge of where it resides on the server. For example, the app can't construct correct relative URLs to its resources without knowing that it resides at the relative URL path `/CoolApp/`. This scenario also applies in various hosting and reverse proxy scenarios when an app isn't hosted at a root URL path.

## Background

An anchor tag's destination ([`href`](https://developer.mozilla.org/docs/Web/HTML/Element/a)) can be composed with either of two endpoints:

* Absolute locations that include a scheme (defaults to the page's scheme if omitted), host, port, and path or just a forward slash (`/`) followed by the path.

  Examples: `https://example.com/a/b/c` or `/a/b/c`

* Relative locations that contain just a path and do not start with a forward slash (`/`). These are resolved relative to the current document URL or the `<base>` tag's value, if specified.

  Example: `a/b/c`

The presence of a trailing slash (`/`) in a configured app base path is significant to compute the base path for URLs of the app. For example, `https://example.com/a` has a base path of `https://example.com/`, while `https://example.com/a/` with a trailing slash has a base path of `https://example.com/a`.

For the sources of links that pertain to Blazor in ASP.NET Core apps:

* URLs in Razor components (`.razor`) are typically relative.
* URLs in scripts, such as the Blazor scripts (`blazor.*.js`), are relative to the document.

- URLs manually written in the `_Host.cshtml` file (Blazor Server), which if you are rendering inside different documents should always be absolute.
- URLs in Razor components (`.razor`) are typically relative.
- URLs in scripts, such as the Blazor scripts (`blazor.*.js`), are relative to the document.

If you're rendering a Blazor app from different documents (for example, `/Admin/B/C/` and `/Admin/D/E/`), you must take the app base path into account, or the base path is different when the app renders in each document and the resources are fetched from the wrong URLs.

Choose one of the following strategies to keep relative links resolving correctly:

* Map resources dynamically using the document they were rendered on as the root. This requires custom routing logic (for example, [IDynamicEndpointMetadata](/en-us/dotnet/api/microsoft.aspnetcore.routing.idynamicendpointmetadata) or a [MatcherPolicy](/en-us/dotnet/api/microsoft.aspnetcore.routing.matcherpolicy)) and is typically reserved for highly dynamic sites.

- Establish a consistent base path for the document and map all app resources beneath that path. Blazor Web Apps can rely on the `BasePath` component to render `<base href>` automatically, while standalone WebAssembly apps (or scenarios that always use a fixed value) configure the literal `<base>` tag in the host page.

* Establish a consistent base path for the document and map all app resources beneath that path. Configure the literal `<base>` tag in the host page for both server-side Blazor apps and Blazor WebAssembly apps (or scenarios that always use a fixed value).

The remainder of this article focuses on implementing a consistent base path.

## Server-side Blazor

Map the SignalR hub of a server-side Blazor app by passing the path to [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) in the `Program` file. The default Blazor hub path is `/_blazor`, and the following example sets the base path of the default hub to `base/path`:

```
app.MapBlazorHub("base/path/_blazor");
```

The benefit of using [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) is that you can map patterns, such as `"{tenant}"` and not just concrete paths.

You can also map the SignalR hub when the app is in a virtual folder with a [branched middleware pipeline](../../fundamentals/middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline). In the following example, requests to `/base/path/` are handled by Blazor's SignalR hub:

```
app.Map("/base/path", subapp => {
    subapp.UsePathBase("/base/path");
    subapp.UseRouting();
	  subapp.UseAntiforgery();
    subapp.UseEndpoints(endpoints => {
        endpoints.MapBlazorHub("/base/path/_blazor");
        endpoints.MapStaticAssets();
		    endpoints.MapRazorComponents<App>()
				.AddInteractiveServerRenderMode();
	});
});
```

Note

The default Blazor hub path is `/_blazor`.

Configure the `<base>` tag, per the guidance in the [Configure the app base path](#configure-the-app-base-path) section.

## Hosted Blazor WebAssembly

If the app is a hosted Blazor WebAssembly app:

* In the in the **Server** project (`Program.cs`):

  * Adjust the path of [UseBlazorFrameworkFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentswebassemblyapplicationbuilderextensions.useblazorframeworkfiles) (for example, `app.UseBlazorFrameworkFiles("/base/path");`).
  * Configure calls to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) (for example, `app.UseStaticFiles("/base/path");`).

* In the **Client** project:

  * Configure [`<StaticWebAssetBasePath>`](../fundamentals/static-files?view=aspnetcore-10.0#static-web-asset-base-path) in the project file to match the path for serving static web assets (for example, `<StaticWebAssetBasePath>base/path</StaticWebAssetBasePath> `).
  * Configure the `<base>` tag, per the guidance in the [Configure the app base path](#configure-the-app-base-path) section.

For an example of hosting multiple Blazor WebAssembly apps in a hosted Blazor WebAssembly solution, see [Multiple hosted ASP.NET Core Blazor WebAssembly apps](webassembly/multiple-hosted-webassembly?view=aspnetcore-10.0), where approaches are explained for domain/port hosting and subpath hosting of multiple Blazor WebAssembly client apps.

## Standalone Blazor WebAssembly

In a standalone Blazor WebAssembly app, only the `<base>` tag is configured, per the guidance in the [Configure the app base path](#configure-the-app-base-path) section.

## Configure the app base path

To provide configuration for the Blazor app's base path of `https://www.contoso.com/CoolApp/`, set the [app base path (`<base>`)](https://developer.mozilla.org/docs/Web/HTML/Element/base), which is also called the relative root path.

By configuring the app base path, a component that isn't in the root directory can construct URLs relative to the app's root path. Components at different levels of the directory structure can build links to other resources at locations throughout the app. The app base path is also used to intercept selected hyperlinks where the `href` target of the link is within the app base path URI space. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component handles the internal navigation.

Place the `<base>` tag in `<head>` markup ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)) before any elements with attribute values that are URLs, such as the `href` attributes of `<link>` elements.

In many hosting scenarios, the relative URL path to the app is the root of the app. When the app runs at `/`, the `BasePath` component (`<BasePath />`) renders `<base href="/" />` automatically in [`<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content). For any other deployment path, the component emits a `<base>` element that matches the current request's path base.

In many hosting scenarios, the relative URL path to the app is the root of the app. In these default cases, the app's relative URL base path is `/` configured as `<base href="/" />` in [`<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content).

In many hosting scenarios, the relative URL path to the app is the root of the app. In these default cases, the app's relative URL base path is the following in [`<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content):

* Blazor Server: `~/` configured as `<base href="~/" />`.
* Blazor WebAssembly: `/` configured as `<base href="/" />`.

Note

In some hosting scenarios, such as GitHub Pages and IIS sub-apps, the app base path must be set to the server's relative URL path of the app.

* In a server-side Blazor app, use the following approach:

- Option 1: Use the `BasePath` component to set the app's base path ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)):

  ```
  <BasePath />
  ```

  The component renders `<base href>` automatically based on the current request path base.

* Option 1: Use the `<base>` tag to set the app's base path ([location of `<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content)):

  ```
  <base href="/CoolApp/">
  ```

  **The trailing slash is required.**

- Option 2: Call [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase) ***first*** in the app's request processing pipeline (`Program.cs`) immediately after the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) is built (`builder.Build()`) to configure the base path for any following middleware that interacts with the request path:

  ```
  app.UsePathBase("/CoolApp");
  ```

  Calling [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase) is recommended when you also wish to run the Blazor Server app locally. For example, supply the launch URL in `Properties/launchSettings.json`:

  ```
  "launchUrl": "https://localhost:{PORT}/CoolApp",
  ```

  The `{PORT}` placeholder in the preceding example is the port that matches the secure port in the `applicationUrl` configuration path. The following example shows the full launch profile for an app at port 7279:

  ```
  "BlazorSample": {
    "commandName": "Project",
    "dotnetRunMessages": true,
    "launchBrowser": true,
    "applicationUrl": "https://localhost:7279;http://localhost:5279",
    "launchUrl": "https://localhost:7279/CoolApp",
    "environmentVariables": {
      "ASPNETCORE_ENVIRONMENT": "Development"
  }
  ```

  Note

  When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

  For more information on the `launchSettings.json` file, see [ASP.NET Core runtime environments](../../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson). For additional information on Blazor app base paths and hosting, see [`<base href="/" />` or base-tag alternative for Blazor MVC integration (dotnet/aspnetcore #43191)](https://github.com/dotnet/aspnetcore/issues/43191#issuecomment-1212156106).

- Standalone Blazor WebAssembly (`wwwroot/index.html`):

  ```
  <base href="/CoolApp/">
  ```

  **The trailing slash is required.**

* Hosted Blazor WebAssembly (**Client** project, `wwwroot/index.html`):

  ```
  <base href="/CoolApp/">
  ```

  **The trailing slash is required.**

  In the **Server** project, call [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase) ***first*** in the app's request processing pipeline (`Program.cs`) immediately after the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) is built (`builder.Build()`) to configure the base path for any following middleware that interacts with the request path:

  ```
  app.UsePathBase("/CoolApp");
  ```

Note

When using [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../../migration/50-to-60?view=aspnetcore-10.0#new-hosting-model)), [`app.UseRouting`](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) must be called after [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase) so that the routing middleware can observe the modified path before matching routes. Otherwise, routes are matched before the path is rewritten by [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase) as described in the [ASP.NET Core middleware](../../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order) and [Routing in ASP.NET Core](../../fundamentals/routing?view=aspnetcore-10.0).

Don't prefix links throughout the app with a forward slash. Either avoid the use of a path segment separator or use dot-slash (`./`) relative path notation:

* ❌ Incorrect: `<a href="/account">`
* ✔️ Correct: `<a href="account">`
* ✔️ Correct: `<a href="./account">`

In [Blazor WebAssembly web API requests with the `HttpClient` service](../call-web-api?view=aspnetcore-10.0\&pivots=webassembly), confirm that JSON helpers ([HttpClientJsonExtensions](/en-us/dotnet/api/system.net.http.json.httpclientjsonextensions)) don't prefix URLs with a forward slash (`/`):

* ❌ Incorrect: `var rsp = await client.GetFromJsonAsync("/api/Account");`
* ✔️ Correct: `var rsp = await client.GetFromJsonAsync("api/Account");`

Don't prefix [Navigation Manager](../fundamentals/navigation?view=aspnetcore-10.0#uri-and-navigation-state-helpers) relative links with a forward slash. Either avoid the use of a path segment separator or use dot-slash (`./`) relative path notation (`Navigation` is an injected [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager)):

* ❌ Incorrect: `Navigation.NavigateTo("/other");`
* ✔️ Correct: `Navigation.NavigateTo("other");`
* ✔️ Correct: `Navigation.NavigateTo("./other");`

In typical configurations for Azure/IIS hosting, additional configuration usually isn't required. In some non-IIS hosting and reverse proxy hosting scenarios, additional static file middleware configuration might be required:

* To serve static files correctly (for example, `app.UseStaticFiles("/CoolApp");`).
* To serve the Blazor script (`_framework/blazor.*.js`). For more information, see [ASP.NET Core Blazor static files](../fundamentals/static-files?view=aspnetcore-10.0).

For a Blazor WebAssembly app with a non-root relative URL path (for example, `<base href="/CoolApp/">`), the app fails to find its resources *when run locally*. To overcome this problem during local development and testing, you can supply a *path base* argument that matches the `href` value of the `<base>` tag at runtime. **Don't include a trailing slash.** To pass the path base argument when running the app locally, execute the `dotnet watch` (or `dotnet run`) command from the app's directory with the `--pathbase` option:

```
dotnet watch --pathbase=/{RELATIVE URL PATH (no trailing slash)}
```

For a Blazor WebAssembly app with a relative URL path of `/CoolApp/` (`<base href="/CoolApp/">`), the command is:

```
dotnet watch --pathbase=/CoolApp
```

If you prefer to configure the app's launch profile to specify the `pathbase` automatically instead of manually with `dotnet watch` (or `dotnet run`), set the `commandLineArgs` property in `Properties/launchSettings.json`. The following also configures the launch URL (`launchUrl`):

```
"commandLineArgs": "--pathbase=/{RELATIVE URL PATH (no trailing slash)}",
"launchUrl": "{RELATIVE URL PATH (no trailing slash)}",
```

Using `CoolApp` as the example:

```
"commandLineArgs": "--pathbase=/CoolApp",
"launchUrl": "CoolApp",
```

Using either `dotnet watch` (or `dotnet run`) with the `--pathbase` option or a launch profile configuration that sets the base path, the Blazor WebAssembly app responds locally at `http://localhost:port/CoolApp`.

For more information on the `launchSettings.json` file, see [ASP.NET Core runtime environments](../../fundamentals/environments?view=aspnetcore-10.0#development-and-launchsettingsjson). For additional information on Blazor app base paths and hosting, see [`<base href="/" />` or base-tag alternative for Blazor MVC integration (dotnet/aspnetcore #43191)](https://github.com/dotnet/aspnetcore/issues/43191#issuecomment-1212156106).

## Obtain the app base path from configuration

The following guidance explains how to obtain the path for the `<base>` tag from an app settings file for different [environments](../fundamentals/environments?view=aspnetcore-10.0).

Add the app settings file to the app. The following example is for the `Staging` environment (`appsettings.Staging.json`):

```
{
  "AppBasePath": "staging/"
}
```

In a server-side Blazor app, load the base path from configuration in [`<head>` content](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content):

```
@inject IConfiguration Config

...

<head>
    ...
    <base href="/@(Config.GetValue<string>("AppBasePath"))" />
    ...
</head>
```

Alternatively, a server-side app can obtain the value from configuration for [UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase). Place the following code ***first*** in the app's request processing pipeline (`Program.cs`) immediately after the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) is built (`builder.Build()`). The following example uses the configuration key `AppBasePath`:

```
app.UsePathBase($"/{app.Configuration.GetValue<string>("AppBasePath")}");
```

In a client-side Blazor WebAssembly app:

* Remove the `<base>` tag from `wwwroot/index.html`:

  ```
  - <base href="..." />
  ```

* Supply the app base path via a [`HeadContent` component](../components/control-head-content?view=aspnetcore-10.0#control-head-content-in-a-razor-component) in the `App` component (`App.razor`):

  ```
  @inject IConfiguration Config

  ...

  <HeadContent>
      <base href="/@(Config.GetValue<string>("AppBasePath"))" />
  </HeadContent>
  ```

If there's no configuration value to load, for example in non-staging environments, the preceding `href` resolves to the root path `/`.

The examples in this section focus on supplying the app base path from app settings, but the approach of reading the path from [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) is valid for any configuration provider. For more information, see the following resources:

* [ASP.NET Core Blazor configuration](../fundamentals/configuration?view=aspnetcore-10.0)
* [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/security/authorization/policies
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/security/authorization/policies.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Policy-based authorization in ASP.NET Core MVC

This article provides additional MVC policy-based authorization scenarios following [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0), which should be read before this article when learning about policy-based authorization.

## Apply policies to MVC controllers

Apply policies to controllers using the `[Authorize]` attribute with the policy name:

```
[Authorize(Policy = "AtLeast21")]
public class AtLeast21Controller : Controller
{
    public IActionResult Index() => View();
}
```

If multiple policies are applied at the controller and action levels, ***all*** policies must pass before access is granted:

```
[Authorize(Policy = "AtLeast21")]
public class AtLeast21Controller2 : Controller
{
    [Authorize(Policy = "IdentificationValidated")]
    public IActionResult Index() => View();
}
```

```
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
    public IActionResult Index() => View();
}
```

## Access MVC request context in handlers

The [AuthorizationHandler\<TRequirement>.HandleRequirementAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandler-1.handlerequirementasync) method has two parameters: an [AuthorizationHandlerContext](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext) and the `TRequirement` being handled. Frameworks such as MVC or SignalR are free to add any object to the [AuthorizationHandlerContext.Resource](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.resource) property to pass extra information.

When using endpoint routing, authorization is typically handled by the Authorization Middleware, and the [Resource](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.resource) property is an instance of [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext). The context is used to access the current endpoint, which can be used to probe the underlying resource to which you're routing:

```
if (context.Resource is HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
    var actionDescriptor = 
        endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>();
    ...
}
```

With traditional routing, or when authorization happens as part of MVC's authorization filter, the value of [Resource](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.resource) is an [AuthorizationFilterContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.authorizationfiltercontext) instance. This property provides access to [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext), [RouteData](/en-us/dotnet/api/microsoft.aspnetcore.routing.routedata), and everything else provided by MVC and Razor Pages.

The use of the [Resource](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationhandlercontext.resource) property is framework-specific. Using information in the property limits your authorization policies to particular frameworks. Cast the property using the `is` keyword, and then confirm the cast has succeeded to ensure your code doesn't crash with an [InvalidCastException](/en-us/dotnet/api/system.invalidcastexception) when run on other frameworks. When the cast succeeds, examine MVC-specific data, such as routing data:

```
using Microsoft.AspNetCore.Mvc.Filters;

...

if (context.Resource is AuthorizationFilterContext mvcContext)
{
    var routeValues = mvcContext.RouteData.Values;
    ...
}
```

Note

Endpoint routing passes [RouteEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeendpoint) to authorization handlers, unlike traditional routing in MVC apps, which use an authorization handler context resource of type [AuthorizationFilterContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.authorizationfiltercontext). If the app uses MVC authorization filters along with endpoint routing authorization, it may be necessary to handle both types of resources.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/response
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/caching/response.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Response caching in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Kirk Larkin](https://twitter.com/serpent5)

Response caching reduces the number of requests a client or proxy makes to a web server. Response caching also reduces the amount of work the web server performs to generate a response. Response caching is set in headers.

The [ResponseCache attribute](#responsecache-attribute) sets response caching headers. Clients and intermediate proxies should honor the headers for caching responses under the [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111) specification.

For server-side caching that follows the HTTP 1.1 Caching specification, use [response caching middleware](middleware?view=aspnetcore-10.0). The middleware can use the [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) properties to influence server-side caching behavior.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/response/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## HTTP-based response caching

The [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111) specification describes how internet caches should behave. The primary HTTP header used for caching is [Cache-Control](https://www.rfc-editor.org/rfc/rfc9111#field.cache-control), which is used to specify cache directives. The directives control caching behavior as requests make their way from clients to servers and responses make their way from servers back to clients. Requests and responses move through proxy servers, and proxy servers must also conform to the HTTP 1.1 Caching specification.

Common `Cache-Control` directives are shown in the following table.

| Directive                                                                            | Action                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [public](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.public)     | A cache can store the response.                                                                                                                                                                                                                                                                                           |
| [private](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.private)   | A shared cache must not store the response. A private cache can store and reuse the response.                                                                                                                                                                                                                             |
| [max-age](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.max-age)   | The client doesn't accept a response whose age is greater than the specified number of seconds. Examples: `max-age=60` (60 seconds), `max-age=2592000` (one month)                                                                                                                                                        |
| [no-cache](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.no-cache) | **On requests**: A cache must not use a stored response to satisfy the request. The origin server regenerates the response for the client, and the middleware updates the stored response in its cache. **On responses**: The response must not be used for a subsequent request without validation on the origin server. |
| [no-store](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.no-store) | **On requests**: A cache must not store the request. **On responses**: A cache must not store any part of the response.                                                                                                                                                                                                   |

Other cache headers that play a role in caching are shown in the following table.

| Header                                                          | Function                                                                                                                                                           |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Age](https://www.rfc-editor.org/rfc/rfc9111#field.age)         | Provides an estimate of the amount of time in seconds since the response was generated or successfully validated at the origin server.                             |
| [Expires](https://www.rfc-editor.org/rfc/rfc9111#field.expires) | Specifies the time after which the response is considered stale.                                                                                                   |
| [Pragma](https://www.rfc-editor.org/rfc/rfc9111#field.pragma)   | Exists for backwards compatibility with HTTP 1.0 caches for setting `no-cache` behavior. If the `Cache-Control` header is present, the `Pragma` header is ignored. |
| [Vary](https://www.rfc-editor.org/rfc/rfc9110#field.vary)       | Specifies that a cached response must not be sent unless all of the `Vary` header fields match in both the cached response's original request and the new request. |

## HTTP-based caching respects request Cache-Control directives

The [RFC 9111: HTTP Caching (Section 5.2. Cache-Control)](https://www.rfc-editor.org/rfc/rfc9111#field.cache-control) specification requires a cache to honor a valid `Cache-Control` header sent by the client. A client can make requests with a `no-cache` header value and force the server to generate a new response for every request.

Always honoring client `Cache-Control` request headers makes sense if you consider the goal of HTTP caching. Under the official specification, caching is meant to reduce the latency and network overhead of satisfying requests across a network of clients, proxies, and servers. It isn't necessarily a way to control the load on an origin server.

There's no developer control over this caching behavior when using the [response caching middleware](middleware?view=aspnetcore-10.0) because the middleware adheres to the official caching specification. .NET 7 and later includes support for *output caching* to better control server load. For more information, see [Output caching](overview?view=aspnetcore-10.0#output-caching).

## ResponseCache attribute

The [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) class specifies the parameters necessary for setting appropriate headers in response caching.

Warning

Disable caching for content that contains information for authenticated clients. Caching should only be enabled for content that doesn't change based on a user's identity or whether a user is signed in.

The [VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyquerykeys#microsoft-aspnetcore-mvc-cacheprofile-varybyquerykeys) property varies the stored response by the values of the given list of query keys. When a single value of the wildcard asterisk (`*`) is provided, the middleware varies responses by all request query string parameters.

[Response caching middleware](middleware?view=aspnetcore-10.0) must be enabled to set the [VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyquerykeys#microsoft-aspnetcore-mvc-cacheprofile-varybyquerykeys) property. Otherwise, a runtime exception is thrown. There isn't a corresponding HTTP header for the [VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyquerykeys#microsoft-aspnetcore-mvc-cacheprofile-varybyquerykeys) property. The property is an HTTP feature handled by response caching middleware. For the middleware to serve a cached response, the query string and query string value must match a previous request. For example, consider the sequence of requests and results shown in the following table.

| Request                            | Returned from |
| ---------------------------------- | ------------- |
| `http://example.com?key1=value1`   | Server        |
| `http://example.com?key1=value1`   | Middleware    |
| `http://example.com?key1=NewValue` | Server        |

The server returns the first request and the middleware caches the request. The middleware returns the second request because the query string matches the previous (first) request. The third request isn't in the middleware cache because the query string value doesn't match a previous request.

The [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) class is used to configure and create (via the [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory) interface) a `Microsoft.AspNetCore.Mvc.Internal.ResponseCacheFilter`. The `ResponseCacheFilter` performs the work of updating the appropriate HTTP headers and features of the response. The filter:

* Removes any existing headers for `Vary`, `Cache-Control`, and `Pragma`.
* Writes out the appropriate headers based on the properties set in the [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute).
* Updates the response caching HTTP feature if the [VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyquerykeys#microsoft-aspnetcore-mvc-cacheprofile-varybyquerykeys) property is set.

### Vary header

This header is only written when the [VaryByHeader](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyheader#microsoft-aspnetcore-mvc-cacheprofile-varybyheader) property is set. The property is set to the `Vary` property's value. The following sample uses the [VaryByHeader](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyheader#microsoft-aspnetcore-mvc-cacheprofile-varybyheader) property:

```
[ApiController]
public class TimeController : ControllerBase
{
    [Route("api/[controller]")]
    [HttpGet]
    [ResponseCache(VaryByHeader = "User-Agent", Duration = 30)]
    public ContentResult GetTime() => Content(
                      DateTime.Now.Millisecond.ToString());
```

View the response headers with Fiddler or another tool. The response headers include:

```
Cache-Control: public,max-age=30
Vary: User-Agent
```

The preceding code requires adding the response caching middleware services [AddResponseCaching](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.responsecachingservicesextensions.addresponsecaching) method to the service collection. The code configures the app to use the middleware with the [UseResponseCaching](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecachingextensions.useresponsecaching) extension method.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseHttpsRedirection();

// UseCors must be called before UseResponseCaching
//app.UseCors();

app.UseResponseCaching();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

### NoStore and Location.None properties

The [NoStore](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.nostore#microsoft-aspnetcore-mvc-cacheprofile-nostore) property overrides most of the other properties. When this property is set to `true`, the `Cache-Control` header is set to `no-store`. If the [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) property is set to `None`:

* The `Cache-Control` header is set to `no-store,no-cache`.
* The `Pragma` header is set to `no-cache`.

If the [NoStore](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.nostore#microsoft-aspnetcore-mvc-cacheprofile-nostore) property is set to `false` and the [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) property is set to `None`, the `Cache-Control` and `Pragma` headers are set to `no-cache`.

The [NoStore](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.nostore#microsoft-aspnetcore-mvc-cacheprofile-nostore) property is typically set to `true` for error pages. The following code produces response headers that instruct the client not to store the response.

```
[Route("api/[controller]/ticks")]
[HttpGet]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public ContentResult GetTimeTicks() => Content(
                  DateTime.Now.Ticks.ToString());
```

The preceding code includes the following headers in the response:

```
Cache-Control: no-store,no-cache
Pragma: no-cache
```

To apply the [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) to all of the app's MVC controller or Razor Pages page responses, add the attribute with an [MVC filter](../../mvc/controllers/filters?view=aspnetcore-10.0) or [Razor Pages filter](../../razor-pages/filter?view=aspnetcore-10.0).

In an MVC app, add the following code:

```
builder.Services.AddControllersWithViews().AddMvcOptions(options => 
    options.Filters.Add(
        new ResponseCacheAttribute
        {
            NoStore = true, 
            Location = ResponseCacheLocation.None
        }));
```

For an approach that applies to Razor Pages apps, see [GitHub dotnet/aspnetcore issue #18890)](https://github.com/dotnet/aspnetcore/issues/18890#issuecomment-584290537) - *Adding ResponseCacheAttribute to MVC global filter list does not apply to Razor Pages*. The comment example in the issue applies to ASP.NET Core apps that predate the release of [Minimal APIs](../../fundamentals/minimal-apis?view=aspnetcore-10.0) (ASP.NET Core version 6.0). For apps that target version 6.0 and later, change the service registration used in the example to `builder.Services.AddSingleton...` in the *Program.cs* file.

### Location and Duration properties

To enable caching, the [Duration](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.duration#microsoft-aspnetcore-mvc-cacheprofile-duration) property must be set to a positive value and [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) must be either `Any` (the default) or `Client`. The framework sets the `Cache-Control` header to the location value followed by the `max-age` of the response.

The [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) property options of `Any` and `Client` translate into `Cache-Control` header values of `public` and `private`, respectively. As noted in the [NoStore and Location.None](#nostore-and-locationnone-properties) section, setting the [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) property to `None` sets both the `Cache-Control` and `Pragma` headers to `no-cache`.

The `Location.Any` (`Cache-Control` set to `public`) property setting indicates that the *client or any intermediate proxy* can cache the value, including [response caching middleware](middleware?view=aspnetcore-10.0).

The `Location.Client` (`Cache-Control` set to `private`) property setting indicates that *only the client* can cache the value. No intermediate cache should cache the value, including [response caching middleware](middleware?view=aspnetcore-10.0).

Cache control headers provide guidance to clients and intermediary proxies about when and how to cache responses. There's no guarantee that clients and proxies honor the [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111) specification. [Response caching middleware](middleware?view=aspnetcore-10.0) always follows the caching rules laid out by the specification.

The following example shows the headers produced by setting the [Duration](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.duration#microsoft-aspnetcore-mvc-cacheprofile-duration) property and leaving the default [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) property value:

```
[Route("api/[controller]/ms")]
[HttpGet]
[ResponseCache(Duration = 10, Location = ResponseCacheLocation.Any, NoStore = false)]
public ContentResult GetTimeMS() => Content(
                  DateTime.Now.Millisecond.ToString());
```

The preceding code includes the following headers in the response:

```
Cache-Control: public,max-age=10
```

### Cache profiles

Instead of duplicating response cache settings on many controller action attributes, cache profiles can be configured as options when setting up MVC pages or Razor Pages. Values found in a referenced cache profile are used as the defaults by the [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute). Any properties specified on the attribute override the referenced cache profile values.

The following example shows a 30-second cache profile:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching();
builder.Services.AddControllers(options =>
{
    options.CacheProfiles.Add("Default30",
        new CacheProfile()
        {
            Duration = 30
        });
});

var app = builder.Build();

app.UseHttpsRedirection();

// UseCors must be called before UseResponseCaching
//app.UseCors();

app.UseResponseCaching();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The following code references the `Default30` cache profile:

```
[ApiController]
[ResponseCache(CacheProfileName = "Default30")]
public class Time2Controller : ControllerBase
{
    [Route("api/[controller]")]
    [HttpGet]
    public ContentResult GetTime() => Content(
                      DateTime.Now.Millisecond.ToString());

    [Route("api/[controller]/ticks")]
    [HttpGet]
    public ContentResult GetTimeTicks() => Content(
                      DateTime.Now.Ticks.ToString());
}
```

The resulting header response by the `Default30` cache profile includes:

```
Cache-Control: public,max-age=30
```

You can apply the \[[ResponseCache](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute)] attribute to Razor Pages, MVC controllers, and MVC action methods.

In Razor Pages, you can't apply attributes to handler methods because browsers used with UI apps prevent response caching. In an MVC action method, the method-level attributes override the settings specified in class-level attributes.

The following code applies the `[ResponseCache]` attribute at the controller level and method level:

```
[ApiController]
[ResponseCache(VaryByHeader = "User-Agent", Duration = 30)]
public class Time4Controller : ControllerBase
{
    [Route("api/[controller]")]
    [HttpGet]
    public ContentResult GetTime() => Content(
                      DateTime.Now.Millisecond.ToString());

    [Route("api/[controller]/ticks")]
    [HttpGet]
    public ContentResult GetTimeTicks() => Content(
                  DateTime.Now.Ticks.ToString());

    [Route("api/[controller]/ms")]
    [HttpGet]
    [ResponseCache(Duration = 10, Location = ResponseCacheLocation.Any, NoStore = false)]
    public ContentResult GetTimeMS() => Content(
                      DateTime.Now.Millisecond.ToString());
}
```

## Related content

* [Storing Responses in Caches - RFC 9111: HTTP Caching (Section 3)](https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-in-caches)
* [Cache-Control - RFC 9111: HTTP Caching (Section 5.2)](https://www.rfc-editor.org/rfc/rfc9111.html#field.cache-control)
* [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0)
* [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0)
* [Response caching middleware in ASP.NET Core](middleware?view=aspnetcore-10.0)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/response/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

Response caching reduces the number of requests a client or proxy makes to a web server. Response caching also reduces the amount of work the web server performs to generate a response. Response caching is controlled by headers that specify how you want client, proxy, and middleware to cache responses.

The [`[ResponseCache]`](#responsecache-attribute) participates in setting response caching headers. Clients and intermediate proxies should honor the headers for caching responses under [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111).

For server-side caching that follows the HTTP 1.1 Caching specification, use [response caching middleware](middleware?view=aspnetcore-10.0). The middleware can use the [`[ResponseCache]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) properties to set server-side caching headers.

## HTTP-based response caching

[RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111) describes how Internet caches should behave. The primary HTTP header used for caching is [Cache-Control](https://www.rfc-editor.org/rfc/rfc9111#field.cache-control), which is used to specify cache *directives*. The directives control caching behavior as requests make their way from clients to servers and as responses make their way from servers back to clients. Requests and responses move through proxy servers, and proxy servers must also conform to the HTTP 1.1 Caching specification.

Common `Cache-Control` directives are shown in the following table.

| Directive                                                                            | Action                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [public](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.public)     | A cache may store the response.                                                                                                                                                                                                                                                                                           |
| [private](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.private)   | The response must not be stored by a shared cache. A private cache may store and reuse the response.                                                                                                                                                                                                                      |
| [max-age](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.max-age)   | The client doesn't accept a response whose age is greater than the specified number of seconds. Examples: `max-age=60` (60 seconds), `max-age=2592000` (1 month)                                                                                                                                                          |
| [no-cache](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.no-cache) | **On requests**: A cache must not use a stored response to satisfy the request. The origin server regenerates the response for the client, and the middleware updates the stored response in its cache. **On responses**: The response must not be used for a subsequent request without validation on the origin server. |
| [no-store](https://www.rfc-editor.org/rfc/rfc9111#cache-response-directive.no-store) | **On requests**: A cache must not store the request. **On responses**: A cache must not store any part of the response.                                                                                                                                                                                                   |

Other cache headers that play a role in caching are shown in the following table.

| Header                                                          | Function                                                                                                                                                           |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Age](https://www.rfc-editor.org/rfc/rfc9111#field.age)         | An estimate of the amount of time in seconds since the response was generated or successfully validated at the origin server.                                      |
| [Expires](https://www.rfc-editor.org/rfc/rfc9111#field.expires) | The time after which the response is considered stale.                                                                                                             |
| [Pragma](https://www.rfc-editor.org/rfc/rfc9111#field.pragma)   | Exists for backwards compatibility with HTTP/1.0 caches for setting `no-cache` behavior. If the `Cache-Control` header is present, the `Pragma` header is ignored. |
| [Vary](https://www.rfc-editor.org/rfc/rfc9110#field.vary)       | Specifies that a cached response must not be sent unless all of the `Vary` header fields match in both the cached response's original request and the new request. |

## HTTP-based caching respects request Cache-Control directives

[RFC 9111: HTTP Caching (Section 5.2. Cache-Control)](https://www.rfc-editor.org/rfc/rfc9111#field.cache-control) requires a cache to honor a valid `Cache-Control` header sent by the client. A client can make requests with a `no-cache` header value and force the server to generate a new response for every request.

Always honoring client `Cache-Control` request headers makes sense if you consider the goal of HTTP caching. Under the official specification, caching is meant to reduce the latency and network overhead of satisfying requests across a network of clients, proxies, and servers. It isn't necessarily a way to control the load on an origin server.

There's no developer control over this caching behavior when using the [response caching middleware](middleware?view=aspnetcore-10.0) because the middleware adheres to the official caching specification. Support for *output caching* to better control server load is a design proposal for a future release of ASP.NET Core. For more information, see [Add support for Output Caching (dotnet/aspnetcore #27387)](https://github.com/dotnet/aspnetcore/issues/27387).

## Other caching technology in ASP.NET Core

### In-memory caching

In-memory caching uses server memory to store cached data. This type of caching is suitable for a single server or multiple servers using session affinity. Session affinity is also known as *sticky sessions*. Session affinity means that the requests from a client are always routed to the same server for processing.

For more information, see [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0) and [Troubleshoot Azure Application Gateway session affinity issues](/en-us/azure/application-gateway/how-to-troubleshoot-application-gateway-session-affinity-issues).

### Distributed Cache

Use a distributed cache to store data in memory when the app is hosted in a cloud or server farm. The cache is shared across the servers that process requests. A client can submit a request that's handled by any server in the group if cached data for the client is available. ASP.NET Core works with SQL Server, [Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis), [Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres), and [NCache](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/) distributed caches.

For more information, see [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0).

### Cache Tag Helper

Cache the content from an MVC view or Razor Page with the Cache Tag Helper. The Cache Tag Helper uses in-memory caching to store data.

For more information, see [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0).

### Distributed Cache Tag Helper

Cache the content from an MVC view or Razor Page in distributed cloud or web farm scenarios with the Distributed Cache Tag Helper. The Distributed Cache Tag Helper uses SQL Server, [Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis), or [NCache](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/) to store data.

For more information, see [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0).

## ResponseCache attribute

The [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) specifies the parameters necessary for setting appropriate headers in response caching.

Warning

Disable caching for content that contains information for authenticated clients. Caching should only be enabled for content that doesn't change based on a user's identity or whether a user is signed in.

[VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyquerykeys#microsoft-aspnetcore-mvc-cacheprofile-varybyquerykeys) varies the stored response by the values of the given list of query keys. When a single value of `*` is provided, the middleware varies responses by all request query string parameters.

[Response caching middleware](middleware?view=aspnetcore-10.0) must be enabled to set the [VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyquerykeys#microsoft-aspnetcore-mvc-cacheprofile-varybyquerykeys) property. Otherwise, a runtime exception is thrown. There isn't a corresponding HTTP header for the [VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyquerykeys#microsoft-aspnetcore-mvc-cacheprofile-varybyquerykeys) property. The property is an HTTP feature handled by response caching middleware. For the middleware to serve a cached response, the query string and query string value must match a previous request. For example, consider the sequence of requests and results shown in the following table.

| Request                          | Result                    |
| -------------------------------- | ------------------------- |
| `http://example.com?key1=value1` | Returned from the server. |
| `http://example.com?key1=value1` | Returned from middleware. |
| `http://example.com?key1=value2` | Returned from the server. |

The first request is returned by the server and cached in middleware. The second request is returned by middleware because the query string matches the previous request. The third request isn't in the middleware cache because the query string value doesn't match a previous request.

The [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) is used to configure and create (via [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory)) a `Microsoft.AspNetCore.Mvc.Internal.ResponseCacheFilter`. The `ResponseCacheFilter` performs the work of updating the appropriate HTTP headers and features of the response. The filter:

* Removes any existing headers for `Vary`, `Cache-Control`, and `Pragma`.
* Writes out the appropriate headers based on the properties set in the [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute).
* Updates the response caching HTTP feature if [VaryByQueryKeys](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyquerykeys#microsoft-aspnetcore-mvc-cacheprofile-varybyquerykeys) is set.

### Vary

This header is only written when the [VaryByHeader](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyheader#microsoft-aspnetcore-mvc-cacheprofile-varybyheader) property is set. The property set to the `Vary` property's value. The following sample uses the [VaryByHeader](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.varybyheader#microsoft-aspnetcore-mvc-cacheprofile-varybyheader) property:

```
[ResponseCache(VaryByHeader = "User-Agent", Duration = 30)]
public class Cache1Model : PageModel
{
```

Using the sample app, view the response headers with the browser's network tools. The following response headers are sent with the Cache1 page response:

```
Cache-Control: public,max-age=30
Vary: User-Agent
```

### `NoStore` and `Location.None`

[NoStore](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.nostore#microsoft-aspnetcore-mvc-cacheprofile-nostore) overrides most of the other properties. When this property is set to `true`, the `Cache-Control` header is set to `no-store`. If [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) is set to `None`:

* `Cache-Control` is set to `no-store,no-cache`.
* `Pragma` is set to `no-cache`.

If [NoStore](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.nostore#microsoft-aspnetcore-mvc-cacheprofile-nostore) is `false` and [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) is `None`, `Cache-Control`, and `Pragma` are set to `no-cache`.

[NoStore](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.nostore#microsoft-aspnetcore-mvc-cacheprofile-nostore) is typically set to `true` for error pages. The Cache2 page in the sample app produces response headers that instruct the client not to store the response.

```
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class Cache2Model : PageModel
{
```

The sample app returns the Cache2 page with the following headers:

```
Cache-Control: no-store,no-cache
Pragma: no-cache
```

To apply the [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) to all of the app's MVC controller or Razor Pages page responses, add it with an [MVC filter](../../mvc/controllers/filters?view=aspnetcore-10.0) or [Razor Pages filter](../../razor-pages/filter?view=aspnetcore-10.0).

In an MVC app:

```
services.AddMvc().AddMvcOptions(options => 
    options.Filters.Add(
        new ResponseCacheAttribute
        {
            NoStore = true, 
            Location = ResponseCacheLocation.None
        }));
```

For an approach that applies to Razor Pages apps, see [Adding `ResponseCacheAttribute` to MVC global filter list does not apply to Razor Pages (dotnet/aspnetcore #18890)](https://github.com/dotnet/aspnetcore/issues/18890#issuecomment-584290537).

### Location and Duration

To enable caching, [Duration](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.duration#microsoft-aspnetcore-mvc-cacheprofile-duration) must be set to a positive value and [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) must be either `Any` (the default) or `Client`. The framework sets the `Cache-Control` header to the location value followed by the `max-age` of the response.

[Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location)'s options of `Any` and `Client` translate into `Cache-Control` header values of `public` and `private`, respectively. As noted in the [NoStore and Location.None](#nostore-and-locationnone) section, setting [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) to `None` sets both `Cache-Control` and `Pragma` headers to `no-cache`.

`Location.Any` (`Cache-Control` set to `public`) indicates that the *client or any intermediate proxy* may cache the value, including [response caching middleware](middleware?view=aspnetcore-10.0).

`Location.Client` (`Cache-Control` set to `private`) indicates that *only the client* may cache the value. No intermediate cache should cache the value, including [response caching middleware](middleware?view=aspnetcore-10.0).

Cache control headers merely provide guidance to clients and intermediary proxies when and how to cache responses. There's no guarantee that clients and proxies will honor [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111). [Response caching middleware](middleware?view=aspnetcore-10.0) always follows the caching rules laid out by the specification.

The following example shows the Cache3 page model from the sample app and the headers produced by setting [Duration](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.duration#microsoft-aspnetcore-mvc-cacheprofile-duration) and leaving the default [Location](/en-us/dotnet/api/microsoft.aspnetcore.mvc.cacheprofile.location#microsoft-aspnetcore-mvc-cacheprofile-location) value:

```
[ResponseCache(Duration = 10, Location = ResponseCacheLocation.Any, NoStore = false)]
public class Cache3Model : PageModel
{
```

The sample app returns the Cache3 page with the following header:

```
Cache-Control: public,max-age=10
```

### Cache profiles

Instead of duplicating response cache settings on many controller action attributes, cache profiles can be configured as options when setting up MVC/Razor Pages in `Startup.ConfigureServices`. Values found in a referenced cache profile are used as the defaults by the [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) and are overridden by any properties specified on the attribute.

Set up a cache profile. The following example shows a 30 second cache profile in the sample app's `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddMvc(options =>
    {
        options.CacheProfiles.Add("Default30",
            new CacheProfile()
            {
                Duration = 30
            });
    });
}
```

The sample app's Cache4 page model references the `Default30` cache profile:

```
[ResponseCache(CacheProfileName = "Default30")]
public class Cache4Model : PageModel
{
```

The [ResponseCacheAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.responsecacheattribute) can be applied to:

* Razor Pages: Attributes can't be applied to handler methods.
* MVC controllers.
* MVC action methods: Method-level attributes override the settings specified in class-level attributes.

The resulting header applied to the Cache4 page response by the `Default30` cache profile:

```
Cache-Control: public,max-age=30
```

## Related content

* [Storing Responses in Caches](https://www.rfc-editor.org/rfc/rfc9111#name-storing-responses-in-caches)
* [RFC 9111: HTTP Caching (Section 5.2. Cache-Control)](https://www.rfc-editor.org/rfc/rfc9111#field.cache-control)
* [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0)
* [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0)
* [Detect changes with change tokens in ASP.NET Core](../../fundamentals/change-tokens?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/globalization-localization
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/globalization-localization.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor globalization and localization

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to render globalized and localized content to users in different cultures and languages.

## Globalization and localization

For [globalization](/en-us/dotnet/core/extensions/globalization), Blazor provides number and date formatting. For [localization](/en-us/dotnet/core/extensions/localization), Blazor renders content using the [.NET Resources system](/en-us/dotnet/framework/resources/).

A limited set of ASP.NET Core's localization features are supported:

✔️Supported: [IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer) and [IStringLocalizer\<T>](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer-1) are supported in Blazor apps.

❌Not supported: [IHtmlLocalizer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.localization.ihtmllocalizer) and [IViewLocalizer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.localization.iviewlocalizer) are ASP.NET Core MVC features and *not supported* in Blazor apps.

For Blazor apps, localization of validation messages for [forms validation using data annotations](forms/validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation) is supported if [DisplayAttribute.ResourceType](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute.resourcetype#system-componentmodel-dataannotations-displayattribute-resourcetype) and [ValidationAttribute.ErrorMessageResourceType](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute.errormessageresourcetype#system-componentmodel-dataannotations-validationattribute-errormessageresourcetype) are implemented.

For Blazor apps, localized validation messages for [forms validation using data annotations](forms/validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation) are supported through two paths:

* The static resource path using [DisplayAttribute.ResourceType](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute.resourcetype#system-componentmodel-dataannotations-displayattribute-resourcetype) for display names and [ValidationAttribute.ErrorMessageResourceType](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute.errormessageresourcetype#system-componentmodel-dataannotations-validationattribute-errormessageresourcetype) for localized error messages. This approach is supported in every release.
* [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation), which resolves validation messages and display names through [IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer). Available for Blazor apps that enable the validation pipeline with `AddValidation()`. For details, see [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-10.0#localize-validation-messages).

This article describes how to use Blazor's globalization and localization features based on:

* The [`Accept-Language` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Accept-Language), which is set by the browser based on a user's language preferences in browser settings.
* A culture set by the app not based on the value of the [`Accept-Language` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Accept-Language). The setting can be static for all users or dynamic based on app logic. When the setting is based on the user's preference, the setting is usually saved for reload on future visits.

For additional general information, see the following resources:

* [Globalization and localization in ASP.NET Core](../fundamentals/localization?view=aspnetcore-10.0)
* [.NET Fundamentals: Globalization](/en-us/dotnet/core/extensions/globalization)
* [.NET Fundamentals: Localization](/en-us/dotnet/core/extensions/localization)

Often, the terms *language* and *culture* are used interchangeably when dealing with globalization and localization concepts.

In this article, *language* refers to selections made by a user in their browser's settings. The user's language selections are submitted in browser requests in the [`Accept-Language` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Accept-Language). Browser settings usually use the word "language" in the UI.

*Culture* pertains to members of .NET and Blazor API. For example, a user's request can include the [`Accept-Language` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Accept-Language) specifying a *language* from the user's perspective, but the app ultimately sets the [CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture) ("culture") property from the language that the user requested. API usually uses the word "culture" in its member names.

The guidance in this article doesn't cover setting the page's HTML language attribute ([`<html lang="...">`](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/lang)), which accessibility tools use. You can set the value statically by assigning a language to the `lang` attribute of the `<html>` tag or to `document.documentElement.lang` in JavaScript. You can dynamically set the value of `document.documentElement.lang` with [JS interop](javascript-interoperability/?view=aspnetcore-10.0).

Note

The code examples in this article adopt [nullable reference types (NRTs) and .NET compiler null-state static analysis](../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis), which are supported in ASP.NET Core in .NET 6 or later. When targeting .NET 5 or earlier, remove the null type designation (`?`) from the article's examples.

## Globalization

The [`@bind`](../mvc/views/razor?view=aspnetcore-10.0#bind) attribute directive applies formats and parses values for display based on the user's first [preferred language](https://developer.mozilla.org/docs/Web/API/NavigatorLanguage/languages) that the app supports. [`@bind`](../mvc/views/razor?view=aspnetcore-10.0#bind) supports the [`@bind:culture`](../mvc/views/razor?view=aspnetcore-10.0#bindculture) parameter to provide a [System.Globalization.CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo) for parsing and formatting a value.

The current culture can be accessed from the [System.Globalization.CultureInfo.CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture) property.

[CultureInfo.InvariantCulture](/en-us/dotnet/api/system.globalization.cultureinfo.invariantculture#system-globalization-cultureinfo-invariantculture) is used for the following field types (`<input type="{TYPE}" />`, where the `{TYPE}` placeholder is the type):

* `date`
* `number`

The preceding field types:

* Are displayed using their appropriate browser-based formatting rules.
* Can't contain free-form text.
* Provide user interaction characteristics based on the browser's implementation.

Blazor provides built-in support to render values in the current culture. Therefore, specifying a culture with [`@bind:culture`](../mvc/views/razor?view=aspnetcore-10.0#bindculture) isn't recommended when using the `date` and `number` field types.

The following field types have specific formatting requirements and aren't supported by all of the major browsers, so they aren't supported by Blazor:

* `datetime-local`
* `month`
* `week`

For current browser support of the preceding types, see [Can I use](https://caniuse.com).

By default, Blazor loads a subset of globalization data that contains the app's culture. To load all globalization data, set `<BlazorWebAssemblyLoadAllGlobalizationData>` to `true` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
```

## .NET globalization and International Components for Unicode (ICU) support (Blazor WebAssembly)

Blazor WebAssembly uses a reduced globalization API and set of built-in International Components for Unicode (ICU) locales.

In WebAssembly (Wasm) apps, when globalization invariant mode is disabled, an ICU data file is loaded. There are four basic types of these files:

* `icudt.dat`: Full data
* `icudt_EFIGS.dat`: Data for locales: `en-*`, `fr-FR`, `es-ES`, `it-IT`, and `de-DE`.
* `icudt_CJK.dat`: Data for locales: `en-*`, `ja`, `ko`, and `zh-*`.
* `icudt_no_CJK.dat`: Data for all locales from `icudt.dat`, excluding `ja`, `ko`, and `zh-*`.

Specify one file to load with the `<BlazorIcuDataFileName>` MSBuild property in the app's project file (`.csproj`). The following example loads the `icudt_no_CJK.dat` file:

```
<PropertyGroup>
  <BlazorIcuDataFileName>icudt_no_CJK.dat</BlazorIcuDataFileName>
</PropertyGroup>
```

`<BlazorIcuDataFileName>` only accepts a single file. The file can be a custom file created by the developer. To create a custom ICU file, see [WASM Globalization Icu: Custom ICU](https://github.com/dotnet/runtime/blob/main/docs/design/features/globalization-icu-wasm.md#custom-icu).

If a file isn't specified with `<BlazorIcuDataFileName>`, the app's culture is checked, and the corresponding ICU file is loaded for its culture. For example, the `en-US` culture results in loading the `icudt_EFIGS.dat` file. For `zh-CN`, the `icudt_CJK.dat` file is used.

For more information, see [.NET globalization and ICU: ICU on WebAssembly](/en-us/dotnet/core/extensions/globalization-icu#icu-on-webassembly).

Blazor WebAssembly uses a reduced globalization API and set of built-in International Components for Unicode (ICU) locales. For more information, see [.NET globalization and ICU: ICU on WebAssembly](/en-us/dotnet/core/extensions/globalization-icu#icu-on-webassembly).

Loading a custom subset of locales in a Blazor WebAssembly app is supported in .NET 8 or later. For more information, access this section for a .NET 8 or later version of this article.

## Invariant globalization

*This section only applies to client-side Blazor scenarios.*

If the app doesn't require localization, configure the app to support the invariant culture, which is generally based on United States English (`en-US`). Using invariant globalization reduces the app's download size and results in faster app startup. Set the `InvariantGlobalization` property to `true` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
```

Alternatively, configure invariant globalization with the following approaches:

* In `runtimeconfig.json`:

  ```
  {
    "runtimeOptions": {
      "configProperties": {
        "System.Globalization.Invariant": true
      }
    }
  }
  ```

* With an environment variable:

  * Key: `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT`
  * Value: `true` or `1`

For more information, see [Runtime configuration options for globalization (.NET documentation)](/en-us/dotnet/core/run-time-config/globalization).

## Timezone information

*This section only applies to client-side Blazor scenarios.*

Adopting [invariant globalization](#invariant-globalization) only results in using non-localized timezone names. To trim timezone code and data, which reduces the app's download size and results in faster app startup, apply the `<InvariantTimezone>` MSBuild property with a value of `true` in the app's project file:

```
<PropertyGroup>
  <InvariantTimezone>true</InvariantTimezone>
</PropertyGroup>
```

Note

[`<BlazorEnableTimeZoneSupport>`](performance/app-download-size?view=aspnetcore-10.0#disable-unused-features) overrides an earlier `<InvariantTimezone>` setting. We recommend removing the `<BlazorEnableTimeZoneSupport>` setting.

A data file is included to make timezone information correct. If the app doesn't require this feature, consider disabling it by setting the `<BlazorEnableTimeZoneSupport>` MSBuild property to `false` in the app's project file:

```
<PropertyGroup>
  <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
</PropertyGroup>
```

## Client-side prerendering in a Blazor Web App preserves the server's culture

By default, client-side prerendering on the server (`.Client` project in a Blazor Web App) persists the server's [CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture) and [CurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentuiculture#system-globalization-cultureinfo-currentuiculture) into component state and applies them on the client before satellite assemblies load.

Apps that require the client to choose a culture independently of the server can opt out with `WebAssemblyComponentsOptions.UseCultureFromServer` in the Blazor Web App's `Program` file:

```
builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents(options =>
    {
        options.UseCultureFromServer = false;
    });
```

## Demonstration component

The following `CultureExample1` component can be used to demonstrate Blazor globalization and localization concepts covered by this article.

`CultureExample1.razor`:

```
@page "/culture-example-1"
@using System.Globalization

<h1>Culture Example 1</h1>

<ul>
    <li><b>CurrentCulture</b>: @CultureInfo.CurrentCulture</li>
    <li><b>CurrentUICulture</b>: @CultureInfo.CurrentUICulture</li>
</ul>

<h2>Rendered values</h2>

<ul>
    <li><b>Date</b>: @dt</li>
    <li><b>Number</b>: @number.ToString("N2")</li>
</ul>

<h2><code>&lt;input&gt;</code> elements that don't set a <code>type</code></h2>

<p>
    The following <code>&lt;input&gt;</code> elements use
    <code>CultureInfo.CurrentCulture</code>.
</p>

<ul>
    <li><label><b>Date:</b> <input @bind="dt" /></label></li>
    <li><label><b>Number:</b> <input @bind="number" /></label></li>
</ul>

<h2><code>&lt;input&gt;</code> elements that set a <code>type</code></h2>

<p>
    The following <code>&lt;input&gt;</code> elements use
    <code>CultureInfo.InvariantCulture</code>.
</p>

<ul>
    <li><label><b>Date:</b> <input type="date" @bind="dt" /></label></li>
    <li><label><b>Number:</b> <input type="number" @bind="number" /></label></li>
</ul>

@code {
    private DateTime dt = DateTime.Now;
    private double number = 1999.69;
}
```

The number string format (`N2`) in the preceding example (`.ToString("N2")`) is a [standard .NET numeric format specifier](/en-us/dotnet/standard/base-types/standard-numeric-format-strings#numeric-format-specifier-n). The `N2` format is supported for all numeric types, includes a group separator, and renders up to two decimal places.

Optionally, add a menu item to the navigation in the `NavMenu` component (`NavMenu.razor`) for the `CultureExample1` component.

## Dynamically set the culture from the `Accept-Language` header

*This section applies to server-side and client-side Blazor apps.*

Add the [`Microsoft.Extensions.Localization` package](https://www.nuget.org/packages/Microsoft.Extensions.Localization) to the app.

The [`Accept-Language` header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Accept-Language) is set by the browser and controlled by the user's language preferences in browser settings. In browser settings, a user sets one or more preferred languages in order of preference. The order of preference is used by the browser to set quality values (`q`, 0-1) for each language in the header. The following example specifies United States English, English, and Costa Rican Spanish with a preference for United States English or English:

**Accept-Language**: en-US,en;q=0.9,es-CR;q=0.8

The app's culture is set by matching the first requested language that matches a supported culture of the app.

In ***client-side development***, set the `BlazorWebAssemblyLoadAllGlobalizationData` property to `true` in the client-side app's project file (`.csproj`):

```
<PropertyGroup>
  <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
```

In ***client-side development***, dynamically setting the culture from the `Accept-Language` header isn't supported.

Note

If the app's specification requires limiting the supported cultures to an explicit list, see the [Dynamically set the client-side culture by user preference](#dynamically-set-the-client-side-culture-by-user-preference) section of this article.

Apps are localized using [localization middleware](../fundamentals/localization?view=aspnetcore-10.0#localization-middleware). Add localization services to the app with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization).

Add the following line to the `Program` file where services are registered:

```
builder.Services.AddLocalization();
```

In ***server-side development***, specify the app's supported cultures before any middleware that might check the request culture. Generally, place localization middleware immediately before calling [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents). The following example configures supported cultures for United States English and Costa Rican Spanish:

In ***server-side development***, specify the app's supported cultures immediately after routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) is added to the processing pipeline. The following example configures supported cultures for United States English and Costa Rican Spanish using the following API:

* [AddSupportedCultures](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.addsupportedcultures) adds the set of the supported cultures for *globalization* (date, number, and currency formatting).
* [AddSupportedUICultures](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.addsupporteduicultures) adds the set of the supported UI cultures *for localization* (translated UI strings for rendering content).

```
app.UseRequestLocalization(new RequestLocalizationOptions()
    .AddSupportedCultures(new[] { "en-US", "es-CR" })
    .AddSupportedUICultures(new[] { "en-US", "es-CR" }));
```

In the preceding example, the same supported formatting cultures and UI cultures are specified in a narrow case where the app is only used in the United States and Costa Rica. Alternatively, an app can use a broader set of cultures for date, number, and currency formatting but only provide localized content for the United States and Costa Rica, as the following example demonstrates:

```
var uiCultures = new[] { "en-US", "es-CR" };

var formattingCultures = CultureInfo
    .GetCultures(CultureTypes.SpecificCultures)
    .Select(c => c.Name)
    .ToArray();

var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(uiCultures[0])
    .AddSupportedCultures(formattingCultures)
    .AddSupportedUICultures(uiCultures);

app.UseRequestLocalization(localizationOptions);
```

In the preceding example, [`CultureTypes.SpecificCultures`](/en-us/dotnet/api/system.globalization.culturetypes) returns only cultures that are specific to a country or region—such as `en-US` or `fr-FR`—which come with full, concrete globalization data (for dates, numbers, calendars, and other cultural UI) that .NET can use for accurate formatting and parsing. Neutral cultures, such as `en` or `fr`, may not have complete globalization data, so they aren't included in this list.

For information on ordering the localization middleware in the middleware pipeline of the `Program` file, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).

Use the `CultureExample1` component shown in the [Demonstration component](#demonstration-component) section to study how globalization works. Issue a request with United States English (`en-US`). Switch to Costa Rican Spanish (`es-CR`) in the browser's language settings. Request the webpage again.

When the culture is United States English (`en-US`), the rendered component uses month/day date formatting (`6/7`), 12-hour time (`AM`/`PM`), and comma separators in numbers with a dot for the decimal value (`1,999.69`):

* **Date**: 6/7/2021 6:45:22 AM
* **Number**: 1,999.69

When the culture is Costa Rican Spanish (`es-CR`), the rendered component uses day/month date formatting (`7/6`), 24-hour time, and period separators in numbers with a comma for the decimal value (`1.999,69`):

* **Date**: 7/6/2021 6:49:38
* **Number**: 1.999,69

## Statically set the client-side culture

*This section applies to Blazor WebAssembly apps and Blazor Web App components that adopt the Interactive WebAssembly render mode.*

*This section applies to Blazor WebAssembly apps.*

Set the `BlazorWebAssemblyLoadAllGlobalizationData` property to `true` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
```

The Intermediate Language (IL) Linker configuration for client-side rendering strips out internationalization information except for locales explicitly requested. For more information, see [Configure the Linker for ASP.NET Core Blazor](host-and-deploy/configure-linker?view=aspnetcore-10.0#configure-the-linker-for-internationalization).

The app's culture can be set in JavaScript when Blazor starts with the `applicationCulture` Blazor start option. The following example configures the app to launch using the United States English (`en-US`) culture.

Prevent Blazor autostart by adding `autostart="false"` to the [Blazor `<script>` tag](project-structure?view=aspnetcore-10.0#location-of-the-blazor-script):

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Add the following `<script>` block after the [Blazor `<script>` tag](project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) and before the closing `</body>` tag:

Blazor Web App:

```
<script>
  Blazor.start({
    webAssembly: {
      applicationCulture: 'en-US'
    }
  });
</script>
```

Standalone Blazor WebAssembly:

```
<script>
  Blazor.start({
    applicationCulture: 'en-US'
  });
</script>
```

The value for `applicationCulture` must conform to the [BCP-47 language tag format](https://www.rfc-editor.org/info/bcp47). For more information on Blazor startup, see [ASP.NET Core Blazor startup](fundamentals/startup?view=aspnetcore-10.0).

An alternative to setting the culture Blazor's start option is to set the culture in C# code. Set [CultureInfo.DefaultThreadCurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentculture#system-globalization-cultureinfo-defaultthreadcurrentculture) and [CultureInfo.DefaultThreadCurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentuiculture#system-globalization-cultureinfo-defaultthreadcurrentuiculture) in the `Program` file to the same culture.

Add the [System.Globalization](/en-us/dotnet/api/system.globalization) namespace to the `Program` file:

```
using System.Globalization;
```

Add the culture settings before the line that builds and runs the [WebAssemblyHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder) (`await builder.Build().RunAsync();`):

```
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US");
```

Note

In .NET 9 or earlier, standalone Blazor WebAssembly apps load UI globalization resources based on [CultureInfo.DefaultThreadCurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentculture#system-globalization-cultureinfo-defaultthreadcurrentculture). If you want to additionally load globalization data for your localization culture defined by [CultureInfo.DefaultThreadCurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentuiculture#system-globalization-cultureinfo-defaultthreadcurrentuiculture), [upgrade the app to .NET 10 or later](../migration/?view=aspnetcore-10.0).

Use the `CultureExample1` component shown in the [Demonstration component](#demonstration-component) section to study how globalization works. Issue a request with United States English (`en-US`). Switch to Costa Rican Spanish (`es-CR`) in the browser's language settings. Request the webpage again. When the requested language is Costa Rican Spanish, the app's culture remains United States English (`en-US`).

## Statically set the server-side culture

*This section applies to Blazor Web App components that adopt the Interactive Server render mode and Blazor Server apps.*

*This section applies to Blazor Server apps.*

Server-side apps are localized using [localization middleware](../fundamentals/localization?view=aspnetcore-10.0#localization-middleware). Add localization services to the app with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization).

In the `Program` file:

```
builder.Services.AddLocalization();
```

Specify the static culture in the `Program` file before any middleware that might check the request culture. Generally, place localization middleware immediately before [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents). The following example configures United States English:

Specify the static culture in the `Program` file immediately after routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) is added to the processing pipeline. The following example configures United States English:

```
app.UseRequestLocalization("en-US");
```

The culture value for [UseRequestLocalization](/en-us/dotnet/api/microsoft.aspnetcore.builder.applicationbuilderextensions.userequestlocalization) must conform to the [BCP-47 language tag format](https://www.rfc-editor.org/info/bcp47).

For information on ordering the localization middleware in the middleware pipeline of the `Program` file, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).

Server-side apps are localized using [localization middleware](../fundamentals/localization?view=aspnetcore-10.0#localization-middleware). Add localization services to the app with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization).

In `Startup.ConfigureServices` (`Startup.cs`):

```
services.AddLocalization();
```

Specify the static culture in `Startup.Configure` (`Startup.cs`) immediately after routing middleware is added to the processing pipeline. The following example configures United States English:

```
app.UseRequestLocalization("en-US");
```

The culture value for [UseRequestLocalization](/en-us/dotnet/api/microsoft.aspnetcore.builder.applicationbuilderextensions.userequestlocalization) must conform to the [BCP-47 language tag format](https://www.rfc-editor.org/info/bcp47).

For information on ordering the localization middleware in the middleware pipeline of `Startup.Configure`, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).

Use the `CultureExample1` component shown in the [Demonstration component](#demonstration-component) section to study how globalization works. Issue a request with United States English (`en-US`). Switch to Costa Rican Spanish (`es-CR`) in the browser's language settings. Request the webpage again. When the requested language is Costa Rican Spanish, the app's culture remains United States English (`en-US`).

## Dynamically set the client-side culture by user preference

*This section applies to Blazor WebAssembly apps and Blazor Web App components that adopt the Interactive WebAssembly render mode.*

*This section applies to Blazor WebAssembly apps.*

Examples of locations where an app might store a user's preference include in [browser local storage](https://developer.mozilla.org/docs/Web/API/Window/localStorage) (common for client-side scenarios), in a localization cookie or database (common for server-side scenarios), or in an external service attached to an external database and accessed by a [web API](call-web-api?view=aspnetcore-10.0). The following example demonstrates how to use browser local storage.

Add the [`Microsoft.Extensions.Localization` package](https://www.nuget.org/packages/Microsoft.Extensions.Localization) to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Set the `BlazorWebAssemblyLoadAllGlobalizationData` property to `true` in the project file:

```
<PropertyGroup>
  <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
```

The app's culture for client-side rendering is set using the Blazor framework's API. A user's culture selection can be persisted in browser local storage.

Provide JS functions after the [Blazor `<script>` tag](project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) to get and set the user's culture selection with browser local storage:

```
<script>
  window.blazorCulture = {
    get: () => window.localStorage['BlazorCulture'],
    set: (value) => window.localStorage['BlazorCulture'] = value
  };
</script>
```

Note

The preceding example pollutes the client with global functions. For a better approach in production apps, see [JavaScript isolation in JavaScript modules](javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules).

Add the namespaces for [System.Globalization](/en-us/dotnet/api/system.globalization) and [Microsoft.JSInterop](/en-us/dotnet/api/microsoft.jsinterop) to the top of the `Program` file:

```
using System.Globalization;
using Microsoft.JSInterop;
```

Remove the following line:

```
- await builder.Build().RunAsync();
```

Replace the preceding line with the following code. The code adds Blazor's localization service to the app's service collection with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization) and uses [JS interop](javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0) to call into JS and retrieve the user's culture selection from local storage. If local storage doesn't contain a culture for the user, the code sets a default value of United States English (`en-US`).

```
builder.Services.AddLocalization();

var host = builder.Build();

const string defaultCulture = "en-US";

var js = host.Services.GetRequiredService<IJSRuntime>();
var result = await js.InvokeAsync<string>("blazorCulture.get");
var culture = CultureInfo.GetCultureInfo(result ?? defaultCulture);

if (result == null)
{
    await js.InvokeVoidAsync("blazorCulture.set", defaultCulture);
}

CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;

await host.RunAsync();
```

Note

In .NET 9 or earlier, standalone Blazor WebAssembly apps load UI globalization resources based on [CultureInfo.DefaultThreadCurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentculture#system-globalization-cultureinfo-defaultthreadcurrentculture). If you want to additionally load globalization data for your localization culture defined by [CultureInfo.DefaultThreadCurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentuiculture#system-globalization-cultureinfo-defaultthreadcurrentuiculture), [upgrade the app to .NET 10 or later](../migration/?view=aspnetcore-10.0).

The following `CultureSelector` component shows how to perform the following actions:

* Set the user's culture selection into browser local storage via JS interop.
* Reload the component that they requested (`forceLoad: true`), which uses the updated culture.

`CultureSelector.razor`:

```
@using System.Globalization
@inject IJSRuntime JS
@inject NavigationManager Navigation

<p>
    <label>
        Select your locale:
        <select @bind="selectedCulture" @bind:after="ApplySelectedCultureAsync">
            @foreach (var culture in supportedCultures)
            {
                <option value="@culture">@culture.DisplayName</option>
            }
        </select>
    </label>
</p>

@code
{
    private CultureInfo[] supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("es-CR"),
    };

    private CultureInfo? selectedCulture;

    protected override void OnInitialized()
    {
        selectedCulture = CultureInfo.CurrentCulture;
    }

    private async Task ApplySelectedCultureAsync()
    {
        if (selectedCulture is not null &&
            CultureInfo.CurrentCulture != selectedCulture)
        {
            await JS.InvokeVoidAsync("blazorCulture.set", selectedCulture.Name);

            Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
        }
    }
}
```

```
@using System.Globalization
@inject IJSRuntime JS
@inject NavigationManager Navigation

<p>
    <label>
        Select your locale:
        <select value="@selectedCulture" @onchange="HandleSelectedCultureChanged">
            @foreach (var culture in supportedCultures)
            {
                <option value="@culture">@culture.DisplayName</option>
            }
        </select>
    </label>
</p>

@code
{
    private CultureInfo[] supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("es-CR"),
    };

    private CultureInfo? selectedCulture;

    protected override void OnInitialized()
    {
        selectedCulture = CultureInfo.CurrentCulture;
    }

    private async Task HandleSelectedCultureChanged(ChangeEventArgs args)
    {
        selectedCulture = CultureInfo.GetCultureInfo((string)args.Value!);

        if (selectedCulture is not null &&
            CultureInfo.CurrentCulture != selectedCulture)
        {
            await JS.InvokeVoidAsync("blazorCulture.set", selectedCulture.Name);

            Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
        }
    }
}
```

Note

For more information on [IJSInProcessRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime), see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#invoke-javascript-functions-without-reading-a-returned-value-invokevoidasync).

In the header markup of the `MainLayout` component (`MainLayout.razor`), add the `CultureSelector` component:

```
<CultureSelector />
```

Use the `CultureExample1` component shown in the [Demonstration component](#demonstration-component) section to study how the preceding example works.

## Dynamically set the server-side culture by user preference

*This section applies to Blazor Web Apps adopting the global Interactive Server render mode and Blazor Server apps. For guidance that covers a Blazor Web App adopting per-page/component interactivity, see the [Dynamically set the culture in a Blazor Web App by user preference](#dynamically-set-the-culture-in-a-blazor-web-app-by-user-preference) section.*

*This section applies to Blazor Server apps.*

Examples of locations where an app might store a user's preference include in [browser local storage](https://developer.mozilla.org/docs/Web/API/Window/localStorage) (common for client-side scenarios), in a localization cookie or database (common for server-side scenarios), or in an external service attached to an external database and accessed by a [web API](call-web-api?view=aspnetcore-10.0). The following example demonstrates how to use a localization cookie.

Add the [`Microsoft.Extensions.Localization` package](https://www.nuget.org/packages/Microsoft.Extensions.Localization) to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Server-side apps are localized using [localization middleware](../fundamentals/localization?view=aspnetcore-10.0#localization-middleware). Add localization services to the app with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization).

In the `Program` file:

```
builder.Services.AddLocalization();
```

Set the app's default and supported cultures with [RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions).

Before the call to [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) in the request processing pipeline, place the following code:

After routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) is added to the request processing pipeline, place the following code:

```
var supportedCultures = new[] { "en-US", "es-CR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);
```

For information on ordering the localization middleware in the middleware pipeline, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).

The following example shows how to set the current culture in a cookie that can be read by the localization middleware.

The following namespaces are required for the `App` component:

* [System.Globalization](/en-us/dotnet/api/system.globalization)
* [Microsoft.AspNetCore.Localization](/en-us/dotnet/api/microsoft.aspnetcore.localization)

Add the [System.Globalization](/en-us/dotnet/api/system.globalization) and [Microsoft.AspNetCore.Localization](/en-us/dotnet/api/microsoft.aspnetcore.localization) namespaces to the top of the `App` component file (`Components/App.razor`):

```
@using System.Globalization
@using Microsoft.AspNetCore.Localization
```

Add the following `@code` block to the bottom of the `App` component file:

```
@code {
    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    protected override void OnInitialized()
    {
        HttpContext?.Response.Cookies.Append(
            CookieRequestCultureProvider.DefaultCookieName,
            CookieRequestCultureProvider.MakeCookieValue(
                new RequestCulture(
                    CultureInfo.CurrentCulture,
                    CultureInfo.CurrentUICulture)));
    }
}
```

Modifications to the `Pages/_Host.cshtml` file require the following namespaces:

* [System.Globalization](/en-us/dotnet/api/system.globalization)
* [Microsoft.AspNetCore.Localization](/en-us/dotnet/api/microsoft.aspnetcore.localization)

Add the following Razor markup to the file:

```
@using System.Globalization
@using Microsoft.AspNetCore.Localization
@{
    this.HttpContext.Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(
            new RequestCulture(
                CultureInfo.CurrentCulture,
                CultureInfo.CurrentUICulture)));
}
```

For information on ordering the localization middleware in the middleware pipeline, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).

To provide UI to allow a user to select a culture, use a *redirect-based approach* with a localization cookie. The app persists the user's selected culture via a redirect to a Minimal API endpoint. The endpoint sets the user's selected culture into a cookie and redirects the user back to the original URI. The process is similar to what happens in a web app when a user attempts to access a secure resource, where the user is redirected to a sign-in page and then redirected back to the original resource.

At the top of the `Program` file, add the following `using` statement for the required namespace:

```
using Microsoft.AspNetCore.Localization;
```

In the request processing pipeline of the app's `Program` file after the call to [UseRequestLocalization](/en-us/dotnet/api/microsoft.aspnetcore.builder.applicationbuilderextensions.userequestlocalization):

```
app.MapGet("/Culture/Set", (string? culture, string redirectUri = "/",
    HttpContext context) =>
{
    if (!string.IsNullOrWhiteSpace(culture))
    {
        context.Response.Cookies.Append(
            CookieRequestCultureProvider.DefaultCookieName,
            CookieRequestCultureProvider.MakeCookieValue(
                new RequestCulture(culture, culture)));
    }

    return Results.LocalRedirect(redirectUri);
});
```

Warning

Use the [LocalRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.results.localredirect) result, as shown in the preceding example, to prevent open redirect attacks. For more information, see [Prevent open redirect attacks in ASP.NET Core](../security/preventing-open-redirects?view=aspnetcore-10.0).

If the app isn't configured to process controller actions:

* Add MVC services by calling [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers) on the service collection in the `Program` file:

  ```
  builder.Services.AddControllers();
  ```

* Add controller endpoint routing in the `Program` file by calling [MapControllers](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollers) on the [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder) (`app`):

  ```
  app.MapControllers();
  ```

To provide UI to allow a user to select a culture, use a *redirect-based approach* with a localization cookie. The app persists the user's selected culture via a redirect to a controller. The controller sets the user's selected culture into a cookie and redirects the user back to the original URI. The process is similar to what happens in a web app when a user attempts to access a secure resource, where the user is redirected to a sign-in page and then redirected back to the original resource.

`Controllers/CultureController.cs`:

```
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;

[Route("[controller]/[action]")]
public class CultureController : Controller
{
    public IActionResult Set(string culture, string redirectUri)
    {
        if (culture != null)
        {
            HttpContext.Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName,
                CookieRequestCultureProvider.MakeCookieValue(
                    new RequestCulture(culture, culture)));
        }

        return LocalRedirect(redirectUri);
    }
}
```

Warning

Use the [LocalRedirect](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.localredirect) action result, as shown in the preceding example, to prevent open redirect attacks. For more information, see [Prevent open redirect attacks in ASP.NET Core](../security/preventing-open-redirects?view=aspnetcore-10.0).

The following `CultureSelector` component shows how to call the `Set` endpoint with the new culture. The component is placed in the `Components` folder for use throughout the app.

`Components/CultureSelector.razor`:

The following `CultureSelector` component shows how to call the `Set` endpoint with the new culture. The component is placed in the `Shared` folder for use throughout the app.

`Shared/CultureSelector.razor`:

```
@using System.Globalization
@inject IJSRuntime JS
@inject NavigationManager Navigation

<p>
    <label>
        Select your locale:
        <select @bind="selectedCulture" @bind:after="ApplySelectedCultureAsync">
            @foreach (var culture in supportedCultures)
            {
                <option value="@culture">@culture.DisplayName</option>
            }
        </select>
    </label>
</p>

@code
{
    private CultureInfo[] supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("es-CR"),
    };

    private CultureInfo? selectedCulture;

    protected override void OnInitialized()
    {
        selectedCulture = CultureInfo.CurrentCulture;
    }

    private async Task ApplySelectedCultureAsync()
    {
        if (selectedCulture is not null &&
            CultureInfo.CurrentCulture != selectedCulture)
        {
            var uri = new Uri(Navigation.Uri)
                .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
            var cultureEscaped = Uri.EscapeDataString(selectedCulture.Name);
            var uriEscaped = Uri.EscapeDataString(uri);

            Navigation.NavigateTo(
                $"Culture/Set?culture={cultureEscaped}&redirectUri={uriEscaped}",
                forceLoad: true);
        }
    }
}
```

```
@using System.Globalization
@inject IJSRuntime JS
@inject NavigationManager Navigation

<p>
    <label>
        Select your locale:
        <select value="@selectedCulture" @onchange="HandleSelectedCultureChanged">
            @foreach (var culture in supportedCultures)
            {
                <option value="@culture">@culture.DisplayName</option>
            }
        </select>
    </label>
</p>

@code
{
    private CultureInfo[] supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("es-CR"),
    };

    private CultureInfo? selectedCulture;

    protected override void OnInitialized()
    {
        selectedCulture = CultureInfo.CurrentCulture;
    }

    private async Task HandleSelectedCultureChanged(ChangeEventArgs args)
    {
        selectedCulture = CultureInfo.GetCultureInfo((string)args.Value!);

        if (selectedCulture is not null &&
            CultureInfo.CurrentCulture != selectedCulture)
        {
            var uri = new Uri(Navigation.Uri)
                .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
            var cultureEscaped = Uri.EscapeDataString(selectedCulture.Name);
            var uriEscaped = Uri.EscapeDataString(uri);

            Navigation.NavigateTo(
                $"Culture/Set?culture={cultureEscaped}&redirectUri={uriEscaped}",
                forceLoad: true);
        }
    }
}
```

Add the `CultureSelector` component to the `MainLayout` component.

In the header markup of `Components/Layout/MainLayout.razor`:

In the header markup of `Shared/MainLayout.razor`:

```
<CultureSelector />
```

Use the `CultureExample1` component shown in the [Demonstration component](#demonstration-component) section to study how the preceding example works.

The preceding example assumes that the app adopts ***global*** interactivity by specifying the Interactive Server render mode on the `Routes` component in the `App` component (`Components/App.razor`):

```
<Routes @rendermode="InteractiveServer" />
```

If the app adopts ***per-page/component*** interactivity and only server-side components provide culture selection UI, make the following changes:

* Add the Interactive Server render mode to the top of the `CultureExample1` component file (`Components/Pages/CultureExample1.razor`):

  ```
  @rendermode InteractiveServer
  ```

* In the app's main layout (`Components/Layout/MainLayout.razor`), apply the Interactive Server render mode to the `CultureSelector` component:

  ```
  <CultureSelector @rendermode="InteractiveServer" />
  ```

## Dynamically set the culture in a Blazor Web App by user preference

*This section applies to Blazor Web Apps that adopt per-page/component interactivity.*

Examples of locations where an app might store a user's preference include in [browser local storage](https://developer.mozilla.org/docs/Web/API/Window/localStorage) (common for client-side scenarios), in a localization cookie or database (common for server-side scenarios), both local storage and a localization cookie (Blazor Web Apps with server and WebAssembly components), or in an external service attached to an external database and accessed by a [web API](call-web-api?view=aspnetcore-10.0). The following example demonstrates how to use browser local storage for client-side rendered (CSR) components and a localization cookie for server-side rendered (SSR) components. The guidance in this section also works for components in apps that adopt per-page/component rendering and specify the Interactive Auto render mode (`@rendermode InteractiveAuto`).

### Updates to the `.Client` project

Add the [`Microsoft.Extensions.Localization` package](https://www.nuget.org/packages/Microsoft.Extensions.Localization) to the `.Client` project.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Set the `BlazorWebAssemblyLoadAllGlobalizationData` property to `true` in the `.Client` project file:

```
<PropertyGroup>
  <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
```

Add the namespaces for [System.Globalization](/en-us/dotnet/api/system.globalization) and [Microsoft.JSInterop](/en-us/dotnet/api/microsoft.jsinterop) to the top of the `.Client` project's `Program` file:

```
using System.Globalization;
using Microsoft.JSInterop;
```

Remove the following line:

```
- await builder.Build().RunAsync();
```

Replace the preceding line with the following code. The code adds Blazor's localization service to the app's service collection with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization) and uses [JS interop](javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0) to call into JS and retrieve the user's culture selection from local storage. If local storage doesn't contain a culture for the user, the code sets a default value of United States English (`en-US`).

```
builder.Services.AddLocalization();

var host = builder.Build();

const string defaultCulture = "en-US";

var js = host.Services.GetRequiredService<IJSRuntime>();
var result = await js.InvokeAsync<string>("blazorCulture.get");
var culture = CultureInfo.GetCultureInfo(result ?? defaultCulture);

if (result == null)
{
    await js.InvokeVoidAsync("blazorCulture.set", defaultCulture);
}

CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;

await host.RunAsync();
```

Note

In .NET 9 or earlier, standalone Blazor WebAssembly apps load UI globalization resources based on [CultureInfo.DefaultThreadCurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentculture#system-globalization-cultureinfo-defaultthreadcurrentculture). If you want to additionally load globalization data for your localization culture defined by [CultureInfo.DefaultThreadCurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.defaultthreadcurrentuiculture#system-globalization-cultureinfo-defaultthreadcurrentuiculture), [upgrade the app to .NET 10 or later](../migration/?view=aspnetcore-10.0).

Add the following `CultureSelector` component to the `.Client` project in a `Shared` folder. If a `Shared` folder doesn't exist in the `.Client` project, create one to hold shared components.

When the user changes the culture, JS interop sets the culture in local browser storage and a Minimal API endpoint updates the localization cookie with the culture. The Minimal API endpoint is added to the app later in the [Server project updates](#server-project-updates) section.

`Shared/CultureSelector.razor`:

```
@using System.Globalization
@inject IJSRuntime JS
@inject NavigationManager Navigation

<p>
    <label>
        Select your locale:
        <select @bind="@selectedCulture" @bind:after="ApplySelectedCultureAsync">
            @foreach (var culture in supportedCultures)
            {
                <option value="@culture">@culture.DisplayName</option>
            }
        </select>
    </label>
</p>

@code
{
    private CultureInfo[] supportedCultures = 
        [ 
            new CultureInfo("en-US"), 
            new CultureInfo("es-CR"),
        ];

    private CultureInfo? selectedCulture;

    protected override void OnInitialized()
    {
        selectedCulture = CultureInfo.CurrentCulture;
    }

    private async Task ApplySelectedCultureAsync()
    {
        if (selectedCulture is not null &&
            CultureInfo.CurrentCulture != selectedCulture)
        {
            await JS.InvokeVoidAsync("blazorCulture.set", selectedCulture.Name);

            var uri = new Uri(Navigation.Uri)
                .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
            var cultureEscaped = Uri.EscapeDataString(selectedCulture.Name);
            var uriEscaped = Uri.EscapeDataString(uri);

            Navigation.NavigateTo(
                $"Culture/Set?culture={cultureEscaped}&redirectUri={uriEscaped}",
                forceLoad: true);
        }
    }
}
```

Note

For more information on [IJSInProcessRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime), see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#invoke-javascript-functions-without-reading-a-returned-value-invokevoidasync).

In the server project's imports file (`_Imports.razor`), add the namespace for the `.Client` project's `Shared` folder (update the namespace to match your app):

```
@using BlazorSample.Client.Shared
```

Place the following markup in the header content of the `Components/Layout/MainLayout.razor` file:

```
<CultureSelector @rendermode="InteractiveAuto" />
```

In the `.Client` project, place the following `CultureClient` component to study how globalization works for CSR components.

`Pages/CultureClient.razor`:

```
@page "/culture-client"
@rendermode InteractiveWebAssembly
@using System.Globalization

<PageTitle>Culture Client</PageTitle>

<h1>Culture Client</h1>

<ul>
    <li><b>CurrentCulture</b>: @CultureInfo.CurrentCulture</li>
    <li><b>CurrentUICulture</b>: @CultureInfo.CurrentUICulture</li>
</ul>

<h2>Rendered values</h2>

<ul>
    <li><b>Date</b>: @dt</li>
    <li><b>Number</b>: @number.ToString("N2")</li>
</ul>

<h2><code>&lt;input&gt;</code> elements that don't set a <code>type</code></h2>

<p>
    The following <code>&lt;input&gt;</code> elements use
    <code>CultureInfo.CurrentCulture</code>.
</p>

<ul>
    <li><label><b>Date:</b> <input @bind="dt" /></label></li>
    <li><label><b>Number:</b> <input @bind="number" /></label></li>
</ul>

<h2><code>&lt;input&gt;</code> elements that set a <code>type</code></h2>

<p>
    The following <code>&lt;input&gt;</code> elements use
    <code>CultureInfo.InvariantCulture</code>.
</p>

<ul>
    <li><label><b>Date:</b> <input type="date" @bind="dt" /></label></li>
    <li><label><b>Number:</b> <input type="number" @bind="number" /></label></li>
</ul>

@code {
    private DateTime dt = DateTime.Now;
    private double number = 1999.69;
}
```

In the `.Client` project, place the following `CultureServer` component to study how globalization works for SSR components.

`Pages/CultureServer.razor`:

```
@page "/culture-server"
@rendermode InteractiveServer
@using System.Globalization

<PageTitle>Culture Server</PageTitle>

<h1>Culture Server</h1>

<ul>
    <li><b>CurrentCulture</b>: @CultureInfo.CurrentCulture</li>
    <li><b>CurrentUICulture</b>: @CultureInfo.CurrentUICulture</li>
</ul>

<h2>Rendered values</h2>

<ul>
    <li><b>Date</b>: @dt</li>
    <li><b>Number</b>: @number.ToString("N2")</li>
</ul>

<h2><code>&lt;input&gt;</code> elements that don't set a <code>type</code></h2>

<p>
    The following <code>&lt;input&gt;</code> elements use
    <code>CultureInfo.CurrentCulture</code>.
</p>

<ul>
    <li><label><b>Date:</b> <input @bind="dt" /></label></li>
    <li><label><b>Number:</b> <input @bind="number" /></label></li>
</ul>

<h2><code>&lt;input&gt;</code> elements that set a <code>type</code></h2>

<p>
    The following <code>&lt;input&gt;</code> elements use
    <code>CultureInfo.InvariantCulture</code>.
</p>

<ul>
    <li><label><b>Date:</b> <input type="date" @bind="dt" /></label></li>
    <li><label><b>Number:</b> <input type="number" @bind="number" /></label></li>
</ul>

@code {
    private DateTime dt = DateTime.Now;
    private double number = 1999.69;
}
```

Use the `CultureExample1` component shown in the [Demonstration component](#demonstration-component) section to study how globalization works for a component that inherits the global Auto render mode. Add the `CultureExample1` component to the `.Client` project's `Pages` folder. At the top of the component, specify the Interactive Auto render mode:

```
@rendermode InteractiveAuto
```

Add the `CultureClient`, `CultureServer`, and `CultureExample1` components to the sidebar navigation in `Components/Layout/NavMenu.razor` of the server project:

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="culture-server">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Culture (Server)
    </NavLink>
</div>
<div class="nav-item px-3">
    <NavLink class="nav-link" href="culture-client">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Culture (Client)
    </NavLink>
</div>
<div class="nav-item px-3">
    <NavLink class="nav-link" href="culture-example-1">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Culture (Auto)
    </NavLink>
</div>
```

### Server project updates

Add the [`Microsoft.Extensions.Localization` package](https://www.nuget.org/packages/Microsoft.Extensions.Localization) to the server project.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Server-side apps are localized using [localization middleware](../fundamentals/localization?view=aspnetcore-10.0#localization-middleware). Add localization services to the app with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization).

In the server project's `Program` file where services are registered:

```
builder.Services.AddLocalization();
```

Set the app's default and supported cultures with [RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions).

Before the call to [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) in the request processing pipeline, place the following code:

```
var supportedCultures = new[] { "en-US", "es-CR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);
```

The following example shows how to set the current culture in a cookie that can be read by the localization middleware.

Add the [System.Globalization](/en-us/dotnet/api/system.globalization) and [Microsoft.AspNetCore.Localization](/en-us/dotnet/api/microsoft.aspnetcore.localization) namespaces to the top of the `App` component (`Components/App.razor`):

```
@using System.Globalization
@using Microsoft.AspNetCore.Localization
```

The app's culture for client-side rendering is set using the Blazor framework's API. A user's culture selection can be persisted in browser local storage for CSR components.

After the [Blazor `<script>` tag](project-structure?view=aspnetcore-10.0#location-of-the-blazor-script), provide JS functions to get and set the user's culture selection with browser local storage:

```
<script>
  window.blazorCulture = {
    get: () => window.localStorage['BlazorCulture'],
    set: (value) => window.localStorage['BlazorCulture'] = value
  };
</script>
```

Note

The preceding example pollutes the client with global functions. For a better approach in production apps, see [JavaScript isolation in JavaScript modules](javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules).

Add the following `@code` block to the bottom of the `App` component file:

```
@code {
    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    protected override void OnInitialized()
    {
        HttpContext?.Response.Cookies.Append(
            CookieRequestCultureProvider.DefaultCookieName,
            CookieRequestCultureProvider.MakeCookieValue(
                new RequestCulture(
                    CultureInfo.CurrentCulture,
                    CultureInfo.CurrentUICulture)));
    }
}
```

To provide UI to allow a user to select a culture, use a *redirect-based approach* with a localization cookie. The app persists the user's selected culture via a redirect to a Minimal API endpoint. The endpoint sets the user's selected culture into a cookie and redirects the user back to the original URI. The process is similar to what happens in a web app when a user attempts to access a secure resource, where the user is redirected to a sign-in page and then redirected back to the original resource.

At the top of the `Program` file, add the following `using` statement for the required namespace:

```
using Microsoft.AspNetCore.Localization;
```

In the request processing pipeline of the app's `Program` file after the call to [UseRequestLocalization](/en-us/dotnet/api/microsoft.aspnetcore.builder.applicationbuilderextensions.userequestlocalization):

```
app.MapGet("/Culture/Set", (string? culture, string redirectUri = "/",
    HttpContext context) =>
{
    if (!string.IsNullOrWhiteSpace(culture))
    {
        context.Response.Cookies.Append(
            CookieRequestCultureProvider.DefaultCookieName,
            CookieRequestCultureProvider.MakeCookieValue(
                new RequestCulture(culture, culture)));
    }

    return Results.LocalRedirect(redirectUri);
});
```

Warning

Use the [LocalRedirect](/en-us/dotnet/api/microsoft.aspnetcore.http.results.localredirect) result, as shown in the preceding example, to prevent open redirect attacks. For more information, see [Prevent open redirect attacks in ASP.NET Core](../security/preventing-open-redirects?view=aspnetcore-10.0).

## Localization

If the app doesn't already support dynamic culture selection, add the [`Microsoft.Extensions.Localization` package](https://www.nuget.org/packages/Microsoft.Extensions.Localization) to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

### Client-side localization

Set the `BlazorWebAssemblyLoadAllGlobalizationData` property to `true` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
```

In the `Program` file, add namespace the namespace for [System.Globalization](/en-us/dotnet/api/system.globalization) to the top of the file:

```
using System.Globalization;
```

Add Blazor's localization service to the app's service collection with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization):

```
builder.Services.AddLocalization();
```

### Server-side localization

Use [localization middleware](../fundamentals/localization?view=aspnetcore-10.0#localization-middleware) to set the app's culture.

If the app doesn't already support dynamic culture selection:

* Add localization services to the app with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization).
* Specify the app's default and supported cultures in the `Program` file. The following example configures supported cultures for United States English and Costa Rican Spanish.

```
builder.Services.AddLocalization();
```

Place localization middleware before any middleware that might check the request culture. Generally, place the middleware immediately before calling [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents):

Immediately after routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) is added to the processing pipeline:

```
var supportedCultures = new[] { "en-US", "es-CR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);
```

For information on ordering the localization middleware in the middleware pipeline, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).

* Add localization services to the app with [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization).
* Specify the app's default and supported cultures in `Startup.Configure` (`Startup.cs`). The following example configures supported cultures for United States English and Costa Rican Spanish.

In `Startup.ConfigureServices` (`Startup.cs`):

```
services.AddLocalization();
```

In `Startup.Configure` immediately after routing middleware ([UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting)) is added to the processing pipeline:

```
var supportedCultures = new[] { "en-US", "es-CR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);
```

For information on ordering the localization middleware in the middleware pipeline of `Startup.Configure`, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order).

If the app should localize resources based on storing a user's culture setting, use a localization culture cookie. Use of a cookie ensures that the WebSocket connection can correctly propagate the culture. If localization schemes are based on the URL path or query string, the scheme might not be able to work with [WebSockets](../fundamentals/websockets?view=aspnetcore-10.0), thus fail to persist the culture. Therefore, the recommended approach is to use a localization culture cookie. See the [Dynamically set the server-side culture by user preference](#dynamically-set-the-server-side-culture-by-user-preference) section of this article to see an example Razor expression that persists the user's culture selection.

### Example of localized resources

The example of localized resources in this section works with the prior examples in this article where the app's supported cultures are English (`en`) as a default locale and Spanish (`es`) as a user-selectable or browser-specified alternate locale.

Create a resource file for each locale. In the following example, resources are created for a `Greeting` string in English and Spanish:

* English (`en`): `Hello, World!`
* Spanish (`es`): `¡Hola, Mundo!`

Note

The following resource file can be added in Visual Studio by right-clicking the `Pages` folder and selecting **Add** > **New Item** > **Resources File**. Name the file `CultureExample2.resx`. When the editor appears, provide data for a new entry. Set the **Name** to `Greeting` and **Value** to `Hello, World!`. Save the file.

If using Visual Studio Code, we recommend installing [Tim Heuer's ResX Viewer and Editor](https://marketplace.visualstudio.com/items?itemName=TimHeuer.resx-editor). Add an empty `CultureExample2.resx` file to the `Pages` folder. The extension automatically takes over managing the file in the UI. Select the **Add New Resource** button. Follow the instructions to add an entry for `Greeting` (key), `Hello, World!` (value), and `None` (comment). Save the file. If you close and re-open the file, you can see the `Greeting` resource.

[Tim Heuer's ResX Viewer and Editor](https://marketplace.visualstudio.com/items?itemName=TimHeuer.resx-editor) isn't owned or maintained by Microsoft and isn't covered by any Microsoft Support Agreement or license.

The following demonstrates a typical resource file. You can manually place resource files into the app's `Pages` folder if you prefer not to use built-in tooling with an integrated development environment (IDE), such as Visual Studio's built-in resource file editor or Visual Studio Code with an extension for creating and editing resource files.

`Pages/CultureExample2.resx`:

```
<?xml version="1.0" encoding="utf-8"?>
<root>
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="Greeting" xml:space="preserve">
    <value>Hello, World!</value>
  </data>
</root>
```

Note

The following resource file can be added in Visual Studio by right-clicking the `Pages` folder and selecting **Add** > **New Item** > **Resources File**. Name the file `CultureExample2.es.resx`. When the editor appears, provide data for a new entry. Set the **Name** to `Greeting` and **Value** to `¡Hola, Mundo!`. Save the file.

If using Visual Studio Code, we recommend installing [Tim Heuer's ResX Viewer and Editor](https://marketplace.visualstudio.com/items?itemName=TimHeuer.resx-editor). Add an empty `CultureExample2.resx` file to the `Pages` folder. The extension automatically takes over managing the file in the UI. Select the **Add New Resource** button. Follow the instructions to add an entry for `Greeting` (key), `¡Hola, Mundo!` (value), and `None` (comment). Save the file. If you close and re-open the file, you can see the `Greeting` resource.

The following demonstrates a typical resource file. You can manually place resource files into the app's `Pages` folder if you prefer not to use built-in tooling with an integrated development environment (IDE), such as Visual Studio's built-in resource file editor or Visual Studio Code with an extension for creating and editing resource files.

`Pages/CultureExample2.es.resx`:

```
<?xml version="1.0" encoding="utf-8"?>
<root>
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="Greeting" xml:space="preserve">
    <value>¡Hola, Mundo!</value>
  </data>
</root>
```

The following component demonstrates the use of the localized `Greeting` string with [IStringLocalizer\<T>](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer-1). The Razor markup `@Loc["Greeting"]` in the following example localizes the string keyed to the `Greeting` value, which is set in the preceding resource files.

Add the namespace for [Microsoft.Extensions.Localization](/en-us/dotnet/api/microsoft.extensions.localization) to the app's imports file (`_Imports.razor`):

```
@using Microsoft.Extensions.Localization
```

`CultureExample2.razor`:

```
@page "/culture-example-2"
@using System.Globalization
@inject IStringLocalizer<CultureExample2> Loc

<h1>Culture Example 2</h1>

<ul>
    <li><b>CurrentCulture</b>: @CultureInfo.CurrentCulture</li>
    <li><b>CurrentUICulture</b>: @CultureInfo.CurrentUICulture</li>
</ul>

<h2>Greeting</h2>

<p>
    @Loc["Greeting"]
</p>

<p>
    @greeting
</p>

@code {
    private string? greeting;

    protected override void OnInitialized()
    {
        greeting = Loc["Greeting"];
    }
}
```

Optionally, add a menu item for the `CultureExample2` component to the navigation in the `NavMenu` component (`NavMenu.razor`).

## WebAssembly culture provider reference source

To further understand how the Blazor framework processes localization, see the [`WebAssemblyCultureProvider` class](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyCultureProvider.cs) in the ASP.NET Core reference source.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Shared resources

To create localization shared resources, adopt the following approach.

* Confirm that the [`Microsoft.Extensions.Localization` package](https://www.nuget.org/packages/Microsoft.Extensions.Localization) is referenced by the project.

  Note

  For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

* Confirm that the [Microsoft.Extensions.Localization](/en-us/dotnet/api/microsoft.extensions.localization) namespace is available to the project's Razor components via an entry in the project's imports file (`_Imports.razor`):

  ```
  @using Microsoft.Extensions.Localization
  ```

* Create a dummy class with an arbitrary class name. In the following example:

  * The app uses the `BlazorSample` namespace, and localization assets use the `BlazorSample.Localization` namespace.
  * The dummy class is named `SharedResource`.
  * The class file is placed in a `Localization` folder at the root of the app.

  Note

  Don't use an autogenerated designer file (for example, `SharedResources.Designer.cs`). The dummy class is meant to act as the shared resource class. The presence of a designer file results in a namespace collision.

  `Localization/SharedResource.cs`:

  ```
  namespace BlazorSample.Localization;

  public class SharedResource
  {
  }
  ```

* Create the shared resource files with a **Build Action** of `Embedded resource`. In the following example:

  * The files are placed in the `Localization` folder with the dummy `SharedResource` class (`Localization/SharedResource.cs`).

  * Name the resource files to match the name of the dummy class. The following example files include a default localization file and a file for Spanish (`es`) localization.

  * `Localization/SharedResource.resx`

  * `Localization/SharedResource.es.resx`

  Warning

  When following the approach in this section, you can't simultaneously set [LocalizationOptions.ResourcesPath](/en-us/dotnet/api/microsoft.extensions.localization.localizationoptions.resourcespath) and use [IStringLocalizerFactory.Create](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizerfactory.create) to load resources.

* To reference the dummy class for an injected [IStringLocalizer\<T>](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer-1) in a Razor component, either place an [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directive for the localization namespace or include the localization namespace in the dummy class reference. In the following examples:

  * The first example states the `Localization` namespace for the `SharedResource` dummy class with an [`@using`](../mvc/views/razor?view=aspnetcore-10.0#using) directive.
  * The second example states the `SharedResource` dummy class's namespace explicitly.

  In a Razor component, use ***either*** of the following approaches:

  ```
  @using Localization
  @inject IStringLocalizer<SharedResource> Loc
  ```

  ```
  @inject IStringLocalizer<Localization.SharedResource> Loc
  ```

For additional guidance, see [Globalization and localization in ASP.NET Core](../fundamentals/localization?view=aspnetcore-10.0).

## Location override using "Sensors" pane in developer tools

When using the location override using the **Sensors** pane in Google Chrome or Microsoft Edge developer tools, the fallback language is reset after prerendering. Avoid setting the language using the **Sensors** pane when testing. Set the language using the browser's language settings.

For more information, see [Blazor Localization does not work with InteractiveServer (`dotnet/aspnetcore` #53707)](https://github.com/dotnet/aspnetcore/issues/53707).

## Related content

* [ASP.NET Core Blazor app base path](host-and-deploy/app-base-path?view=aspnetcore-10.0)
* [Globalization and localization in ASP.NET Core](../fundamentals/localization?view=aspnetcore-10.0)
* [DataAnnotations localization in Minimal APIs and Blazor](../fundamentals/localization/make-content-localizable?view=aspnetcore-10.0#dataannotations-localization-in-minimal-apis-and-blazor)
* [Minimal APIs: Localizing validation messages](../fundamentals/minimal-apis?view=aspnetcore-10.0#localizing-validation-messages)
* [Globalizing and localizing .NET applications](/en-us/dotnet/core/extensions/globalization-and-localization)
* [Resources in .resx Files](/en-us/dotnet/framework/resources/working-with-resx-files-programmatically)
* [Localization & Generics](http://hishambinateya.com/localization-and-generics)
* [Calling `InvokeAsync(StateHasChanged)` causes page to fallback to default culture (`dotnet/aspnetcore` #28521)](https://github.com/dotnet/aspnetcore/issues/28521#issuecomment-1112513408)
* [Blazor Localization does not work with InteractiveServer (`dotnet/aspnetcore` #53707)](https://github.com/dotnet/aspnetcore/issues/53707) ([Location override using "Sensors" pane](#location-override-using-sensors-pane-in-developer-tools))

- [ASP.NET Core Blazor app base path](host-and-deploy/app-base-path?view=aspnetcore-10.0)
- [Globalization and localization in ASP.NET Core](../fundamentals/localization?view=aspnetcore-10.0)
- [Globalizing and localizing .NET applications](/en-us/dotnet/core/extensions/globalization-and-localization)
- [Resources in .resx Files](/en-us/dotnet/framework/resources/working-with-resx-files-programmatically)
- [Localization & Generics](http://hishambinateya.com/localization-and-generics)
- [Calling `InvokeAsync(StateHasChanged)` causes page to fallback to default culture (`dotnet/aspnetcore` #28521)](https://github.com/dotnet/aspnetcore/issues/28521#issuecomment-1112513408)
- [Blazor Localization does not work with InteractiveServer (`dotnet/aspnetcore` #53707)](https://github.com/dotnet/aspnetcore/issues/53707) ([Location override using "Sensors" pane](#location-override-using-sensors-pane-in-developer-tools))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization/provide-resources
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/localization/provide-resources.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Provide localized resources for languages and cultures in an ASP.NET Core app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Damien Bowden](https://github.com/damienbod), [Bart Calixto](https://twitter.com/bartmax), [Nadeem Afana](https://afana.me/), and [Hisham Bin Ateya](https://twitter.com/hishambinateya)

One task for localizing an app is to provide localized strings in resource files. This article is about working with resource files.

## `SupportedCultures` and `SupportedUICultures`

ASP.NET Core has two collections of culture values, `SupportedCultures` and `SupportedUICultures`. The [CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo) object for `SupportedCultures` determines the results of culture-dependent functions, such as date, time, number, and currency formatting. `SupportedCultures` also determines the sorting order of text, casing conventions, and string comparisons. See [StringComparer.CurrentCulture](/en-us/dotnet/api/system.stringcomparer.currentculture#system-stringcomparer-currentculture) for more info on how the server gets the culture. The `SupportedUICultures` determines which translated strings (from *.resx* files) are looked up by the [ResourceManager](/en-us/dotnet/api/system.resources.resourcemanager). The `ResourceManager` simply looks up culture-specific strings that are determined by `CurrentUICulture`. Every thread in .NET has `CurrentCulture` and `CurrentUICulture` objects. ASP.NET Core inspects these values when rendering culture-dependent functions. For example, if the current thread's culture is set to "en-US" (English, United States), `DateTime.Now.ToLongDateString()` displays "Thursday, February 18, 2016", but if `CurrentCulture` is set to "es-ES" (Spanish, Spain) the output will be "jueves, 18 de febrero de 2016".

## Resource files

***NOTE:*** [ResX Viewer and Editor](https://marketplace.visualstudio.com/items?itemName=TimHeuer.resx-editor) provides an alternate mechanism to work with resource files using Visual Studio Code.

A resource file is a useful mechanism for separating localizable strings from code. Translated strings for the non-default language are isolated in *.resx* resource files. For example, you might want to create a Spanish resource file named *Welcome.es.resx* containing translated strings. "es" is the language code for Spanish. To create this resource file in Visual Studio:

1. In **Solution Explorer**, right click on the folder that will contain the resource file, and then select **Add** > **New Item**.

2. In the **Search installed templates** box, enter "resource" and name the file.

3. Enter the key value (native string) in the **Name** column and the translated string in the **Value** column.

   Visual Studio shows the *Welcome.es.resx* file.

## Resource file naming

Resources are named for the full type name of their class minus the assembly name. For example, a French resource in a project whose main assembly is `LocalizationWebsite.Web.dll` for the class `LocalizationWebsite.Web.Startup` would be named *Startup.fr.resx*. A resource for the class `LocalizationWebsite.Web.Controllers.HomeController` would be named *Controllers.HomeController.fr.resx*. If your targeted class's namespace isn't the same as the assembly name you will need the full type name. For example, in the sample project a resource for the type `ExtraNamespace.Tools` would be named *ExtraNamespace.Tools.fr.resx*.

In the sample project, the `ConfigureServices` method sets the `ResourcesPath` to "Resources", so the project relative path for the home controller's French resource file is *Resources/Controllers.HomeController.fr.resx*. Alternatively, you can use folders to organize resource files. For the home controller, the path would be *Resources/Controllers/HomeController.fr.resx*. If you don't use the `ResourcesPath` option, the *.resx* file would go in the project base directory. The resource file for `HomeController` would be named *Controllers.HomeController.fr.resx*. The choice of using the dot or path naming convention depends on how you want to organize your resource files.

| Resource name                                | Dot or path naming |
| -------------------------------------------- | ------------------ |
| Resources/Controllers.HomeController.fr.resx | Dot                |
| Resources/Controllers/HomeController.fr.resx | Path               |

Resource files using `@inject IViewLocalizer` in Razor views follow a similar pattern. The resource file for a view can be named using either dot naming or path naming. Razor view resource files mimic the path of their associated view file. Assuming we set the `ResourcesPath` to "Resources", the French resource file associated with the `Views/Home/About.cshtml` view could be either of the following:

* Resources/Views/Home/About.fr.resx

* Resources/Views.Home.About.fr.resx

If you don't use the `ResourcesPath` option, the *.resx* file for a view would be located in the same folder as the view.

## RootNamespaceAttribute

```
using System.Reflection;
using Microsoft.Extensions.Localization;

[assembly: ResourceLocation("Resource Folder Name")]
[assembly: RootNamespace("App Root Namespace")]
```

The preceding code enables the successful resolution of resx files.

## Culture fallback behavior

When searching for a resource, localization engages in "culture fallback". Starting from the requested culture, if not found, it reverts to the parent culture of that culture. As an aside, the [CultureInfo.Parent](/en-us/dotnet/api/system.globalization.cultureinfo.parent) property represents the parent culture. This usually (but not always) means removing the national signifier from the language-and-culture code. For example, the dialect of Spanish spoken in Mexico is "es-MX". It has the parent "es"—Spanish non-specific to any country.

Imagine your site receives a request for a "Welcome" resource using culture "fr-CA". The localization system looks for the following resources, in order, and selects the first match:

* *Welcome.fr-CA.resx*
* *Welcome.fr.resx*
* *Welcome.resx* (if the `NeutralResourcesLanguage` is "fr-CA")

As an example, if you remove the ".fr" culture designator and you have the culture set to French, the default resource file is read and strings are localized. The Resource manager designates a default or fallback resource for when nothing meets your requested culture. If you want to just return the key when missing a resource for the requested culture you must not have a default resource file.

## Generate resource files with Visual Studio

If you create a resource file in Visual Studio without a culture in the file name (for example, *Welcome.resx*), Visual Studio will create a C# class with a property for each string. That's usually not what you want with ASP.NET Core. You typically don't have a default *.resx* resource file (a *.resx* file without the culture name). We suggest you create the *.resx* file with a culture name (for example *Welcome.fr.resx*). When you create a *.resx* file with a culture name, Visual Studio won't generate the class file.

### Add other cultures

Each language and culture combination (other than the default language) requires a unique resource file. You create resource files for different cultures and locales by creating new resource files in which the language codes are part of the file name (for example, **en-us**, **fr-ca**, and **en-gb**). These codes are placed between the file name and the *.resx* file extension, as in *Welcome.es-MX.resx* (Spanish/Mexico).

## Next steps

Localizing an app also involves the following tasks:

* [Make the app's content localizable](make-content-localizable?view=aspnetcore-10.0).
* [Implement a strategy to select the language/culture for each request](select-language-culture?view=aspnetcore-10.0)

## Related content

* [Url culture provider using middleware as filters in ASP.NET Core](https://andrewlock.net/url-culture-provider-using-middleware-as-mvc-filter-in-asp-net-core-1-1-0/)
* [Applying the RouteDataRequest CultureProvider globally with middleware as filters](https://andrewlock.net/applying-the-routedatarequest-cultureprovider-globally-with-middleware-as-filters/)
* [Globalization and localization in ASP.NET Core](../localization?view=aspnetcore-10.0)
* [Make an ASP.NET Core app's content localizable](make-content-localizable?view=aspnetcore-10.0)

One task for localizing an app is to provide localized strings in resource files. This article is about working with resource files.

## `SupportedCultures` and `SupportedUICultures`

ASP.NET Core has two collections of culture values, `SupportedCultures` and `SupportedUICultures`. The [CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo) object for `SupportedCultures` determines the results of culture-dependent functions, such as date, time, number, and currency formatting. `SupportedCultures` also determines the sorting order of text, casing conventions, and string comparisons. See [StringComparer.CurrentCulture](/en-us/dotnet/api/system.stringcomparer.currentculture#system-stringcomparer-currentculture) for more info on how the server gets the culture. The `SupportedUICultures` determines which translated strings (from *.resx* files) are looked up by the [ResourceManager](/en-us/dotnet/api/system.resources.resourcemanager). The `ResourceManager` simply looks up culture-specific strings that are determined by `CurrentUICulture`. Every thread in .NET has `CurrentCulture` and `CurrentUICulture` objects. ASP.NET Core inspects these values when rendering culture-dependent functions. For example, if the current thread's culture is set to "en-US" (English, United States), `DateTime.Now.ToLongDateString()` displays "Thursday, February 18, 2016", but if `CurrentCulture` is set to "es-ES" (Spanish, Spain) the output will be "jueves, 18 de febrero de 2016".

## Resource files

A resource file is a useful mechanism for separating localizable strings from code. Translated strings for the non-default language are isolated in *.resx* resource files. For example, you might want to create a Spanish resource file named *Welcome.es.resx* containing translated strings. "es" is the language code for Spanish. To create this resource file in Visual Studio:

1. In **Solution Explorer**, right click on the folder that will contain the resource file, and then select **Add** > **New Item**.

2. In the **Search installed templates** box, enter "resource" and name the file.

3. Enter the key value (native string) in the **Name** column and the translated string in the **Value** column.

   Visual Studio shows the *Welcome.es.resx* file.

## Resource file naming

Resources are named for the full type name of their class minus the assembly name. For example, a French resource in a project whose main assembly is `LocalizationWebsite.Web.dll` for the class `LocalizationWebsite.Web.Startup` would be named *Startup.fr.resx*. A resource for the class `LocalizationWebsite.Web.Controllers.HomeController` would be named *Controllers.HomeController.fr.resx*. If your targeted class's namespace isn't the same as the assembly name you will need the full type name. For example, in the sample project a resource for the type `ExtraNamespace.Tools` would be named *ExtraNamespace.Tools.fr.resx*.

In the sample project, the `ConfigureServices` method sets the `ResourcesPath` to "Resources", so the project relative path for the home controller's French resource file is *Resources/Controllers.HomeController.fr.resx*. Alternatively, you can use folders to organize resource files. For the home controller, the path would be *Resources/Controllers/HomeController.fr.resx*. If you don't use the `ResourcesPath` option, the *.resx* file would go in the project base directory. The resource file for `HomeController` would be named *Controllers.HomeController.fr.resx*. The choice of using the dot or path naming convention depends on how you want to organize your resource files.

| Resource name                                | Dot or path naming |
| -------------------------------------------- | ------------------ |
| Resources/Controllers.HomeController.fr.resx | Dot                |
| Resources/Controllers/HomeController.fr.resx | Path               |

Resource files using `@inject IViewLocalizer` in Razor views follow a similar pattern. The resource file for a view can be named using either dot naming or path naming. Razor view resource files mimic the path of their associated view file. Assuming we set the `ResourcesPath` to "Resources", the French resource file associated with the `Views/Home/About.cshtml` view could be either of the following:

* Resources/Views/Home/About.fr.resx

* Resources/Views.Home.About.fr.resx

If you don't use the `ResourcesPath` option, the *.resx* file for a view would be located in the same folder as the view.

## RootNamespaceAttribute

```
using System.Reflection;
using Microsoft.Extensions.Localization;

[assembly: ResourceLocation("Resource Folder Name")]
[assembly: RootNamespace("App Root Namespace")]
```

The preceding code enables the successful resolution of resx files.

## Culture fallback behavior

When searching for a resource, localization engages in "culture fallback". Starting from the requested culture, if not found, it reverts to the parent culture of that culture. As an aside, the [CultureInfo.Parent](/en-us/dotnet/api/system.globalization.cultureinfo.parent) property represents the parent culture. This usually (but not always) means removing the national signifier from the language-and-culture code. For example, the dialect of Spanish spoken in Mexico is "es-MX". It has the parent "es"—Spanish non-specific to any country.

Imagine your site receives a request for a "Welcome" resource using culture "fr-CA". The localization system looks for the following resources, in order, and selects the first match:

* *Welcome.fr-CA.resx*
* *Welcome.fr.resx*
* *Welcome.resx* (if the `NeutralResourcesLanguage` is "fr-CA")

As an example, if you remove the ".fr" culture designator and you have the culture set to French, the default resource file is read and strings are localized. The Resource manager designates a default or fallback resource for when nothing meets your requested culture. If you want to just return the key when missing a resource for the requested culture you must not have a default resource file.

## Generate resource files with Visual Studio

If you create a resource file in Visual Studio without a culture in the file name (for example, *Welcome.resx*), Visual Studio will create a C# class with a property for each string. That's usually not what you want with ASP.NET Core. You typically don't have a default *.resx* resource file (a *.resx* file without the culture name). We suggest you create the *.resx* file with a culture name (for example *Welcome.fr.resx*). When you create a *.resx* file with a culture name, Visual Studio won't generate the class file.

### Add other cultures

Each language and culture combination (other than the default language) requires a unique resource file. You create resource files for different cultures and locales by creating new resource files in which the language codes are part of the file name (for example, **en-us**, **fr-ca**, and **en-gb**). These codes are placed between the file name and the *.resx* file extension, as in *Welcome.es-MX.resx* (Spanish/Mexico).

## Next steps

Localizing an app also involves the following tasks:

* [Make the app's content localizable](make-content-localizable?view=aspnetcore-10.0).
* [Implement a strategy to select the language/culture for each request](select-language-culture?view=aspnetcore-10.0)

## Related content

* [Globalization and localization in ASP.NET Core](../localization?view=aspnetcore-10.0)
* [Make an ASP.NET Core app's content localizable](make-content-localizable?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/environments
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/environments.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor environments

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure and read the [environment](../../fundamentals/environments?view=aspnetcore-10.0) in a Blazor app.

When running an app locally, the environment defaults to `Development`. When the app is published, the environment defaults to `Production`.

We recommend the following conventions:

* Always use the "`Development`" environment name for local development. This is because the ASP.NET Core framework expects exactly that name when configuring the app and tooling for local development runs of an app.

* For testing, staging, and production environments, always publish and deploy the app. You can use any environment naming scheme that you wish for published apps, but always use app setting file names with casing of the environment segment that exactly matches the environment name. For staging, use "`Staging`" (capital "S") as the environment name, and name the app settings file to match (`appsettings.Staging.json`). For production, use "`Production`" (capital "P") as the environment name, and name the app settings file to match (`appsettings.Production.json`).

## Set the environment

The environment is set using any of the following approaches:

* Blazor Web App or Blazor Server: Use any of the approaches described in [ASP.NET Core runtime environments](../../fundamentals/environments?view=aspnetcore-10.0) for general ASP.NET Core apps.
* Any Blazor app: [Blazor start configuration](#set-the-client-side-environment-via-blazor-startup-configuration)
* Standalone Blazor WebAssembly: `<WasmApplicationEnvironmentName>` property

On the client for a Blazor Web App, the environment is determined from the server via an HTML comment that developers don't interact with:

```
<!--Blazor-WebAssembly:{"environmentName":"Development", ...}-->
```

For a standalone Blazor WebAssembly app, set the environment with the `<WasmApplicationEnvironmentName>` MSBuild property in the app's project file (`.csproj`). The following example sets the `Staging` environment:

```
<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>
```

The default environments are `Development` for build and `Production` for publish.

There are several approaches for setting the environment in a standalone Blazor WebAssembly app during build/publish operations and one approach for an app starting or running on the client:

* Set the property value when `dotnet build` or `dotnet publish` is executed. The following example sets the environment to `Staging` when an app is published:

  ```
  dotnet publish -p:WasmApplicationEnvironmentName=Staging
  ```

* Set the property during build or publish based on the app's configuration in Visual Studio. The following property groups can be used in the app's project file or any publish configuration file (`.pubxml`). Add additional property groups for other build configurations in use.

  ```
  <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
    <WasmApplicationEnvironmentName>Development</WasmApplicationEnvironmentName>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <WasmApplicationEnvironmentName>Production</WasmApplicationEnvironmentName>
  </PropertyGroup>
  ```

* The environment can be set based on the use of a publish profile. In the following example, the first condition sets the environment to `Development` when no publish profile is used (applies to both build and publish operations without a profile), while the second condition covers setting the environment to `Production` when any publish profile is used:

  ```
  <PropertyGroup Condition="'$(PublishProfile)' == ''">
    <WasmApplicationEnvironmentName>Development</WasmApplicationEnvironmentName>
  </PropertyGroup>

  <PropertyGroup Condition="'$(PublishProfile)' != ''">
    <WasmApplicationEnvironmentName>Production</WasmApplicationEnvironmentName>
  </PropertyGroup>
  ```

* Create a custom server-side web API endpoint. The standalone Blazor WebAssembly app requests its environment from the web API either at app startup or on-demand while it's running. The value should be passed to [`WebAssemblyStartOptions`](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web.JS/src/Platform/WebAssemblyStartOptions.ts#L7) or with [`withApplicationEnvironment`](https://github.com/dotnet/aspnetcore/blob/main/src/Components/dotnet-runtime-js/dotnet.d.ts#L110).

  Note

  Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

- Blazor Web App or Blazor Server: Use any of the approaches described in [ASP.NET Core runtime environments](../../fundamentals/environments?view=aspnetcore-10.0) for general ASP.NET Core apps.

- Any Blazor app:

  * [Blazor start configuration](#set-the-client-side-environment-via-blazor-startup-configuration)
  * [Azure App Service](#set-the-environment-for-azure-app-service)

- Blazor WebAssembly: [`Blazor-Environment` header](#set-the-client-side-environment-via-header)

On the client for a Blazor Web App, the environment is determined from the server via a middleware that communicates the environment to the browser via a header named `Blazor-Environment`. The header sets the environment when the [WebAssemblyHost](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhost) is created in the client-side `Program` file ([WebAssemblyHostBuilder.CreateDefault](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.createdefault)).

For a standalone Blazor WebAssembly app running locally, the development server adds the `Blazor-Environment` header with the environment name obtained from the hosting environment. The hosting environment sets the environment from the `ASPNETCORE_ENVIRONMENT` environment variable established by the project's `Properties/launchSettings.json` file. The default value of the environment variable in a project created from the Blazor WebAssembly project template is `Development`. For more information, see the [Set the client-side environment via header](#set-the-client-side-environment-via-header) section.

* Blazor Server: Use any of the approaches described in [ASP.NET Core runtime environments](../../fundamentals/environments?view=aspnetcore-10.0) for general ASP.NET Core apps.

* Blazor Server or Blazor WebAssembly:

  * [Blazor start configuration](#set-the-client-side-environment-via-blazor-startup-configuration)
  * [Azure App Service](#set-the-environment-for-azure-app-service)

* Blazor WebAssembly: [`Blazor-Environment` header](#set-the-client-side-environment-via-header)

On the client of a hosted Blazor WebAssembly app, the environment is determined from the server via a middleware that communicates the environment to the browser via a header named `Blazor-Environment`. The header sets the environment when the [WebAssemblyHost](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhost) is created in the client-side `Program` file ([WebAssemblyHostBuilder.CreateDefault](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.createdefault)).

For a standalone Blazor WebAssembly app running locally, the development server adds the `Blazor-Environment` header with the environment name obtained from the hosting environment. The hosting environment sets the environment from the `ASPNETCORE_ENVIRONMENT` environment variable established by the project's `Properties/launchSettings.json` file. The default value of the environment variable in a project created from the Blazor WebAssembly project template is `Development`. For more information, see the [Set the client-side environment via header](#set-the-client-side-environment-via-header) section.

Note

When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

For app's running locally in development, the app defaults to the `Development` environment. Publishing the app defaults the environment to `Production`.

For general guidance on ASP.NET Core environments, see [ASP.NET Core runtime environments](../../fundamentals/environments?view=aspnetcore-10.0). For server-side app configuration to serve static files in environments other than the [Development](/en-us/dotnet/api/microsoft.extensions.hosting.environments.development#microsoft-extensions-hosting-environments-development) environment during local development and testing (for example, [Staging](/en-us/dotnet/api/microsoft.extensions.hosting.environments.staging#microsoft-extensions-hosting-environments-staging)), see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0#static-files-in-non-development-environments).

## Set the client-side environment via Blazor startup configuration

The following example starts Blazor in the `Staging` environment if the hostname includes `localhost`. Otherwise, the environment is set to its default value.

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  if (window.location.hostname.includes("localhost")) {
    Blazor.start({
      webAssembly: {
        environment: "Staging"
      }
    });
  } else {
    Blazor.start();
  }
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Note

For Blazor Web Apps that set the `webAssembly` > `environment` property in `Blazor.start` configuration, it's wise to match the server-side environment to the environment set on the `environment` property. Otherwise, prerendering on the server operates under a different environment than rendering on the client, which results in arbitrary effects. For general guidance on setting the environment for a Blazor Web App, see [ASP.NET Core runtime environments](../../fundamentals/environments?view=aspnetcore-10.0).

Standalone Blazor WebAssembly:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  if (window.location.hostname.includes("localhost")) {
    Blazor.start({
      environment: "Staging"
    });
  } else {
    Blazor.start();
  }
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Using the `environment` property overrides the environment set by the [`Blazor-Environment` header](#set-the-client-side-environment-via-header).

The preceding approach sets the client's environment without changing the `Blazor-Environment` header's value, nor does it change the server project's console logging of the startup environment for a Blazor Web App that has adopted global Interactive WebAssembly rendering.

To log the environment to the console in either a standalone Blazor WebAssembly app (all release versions) or the `.Client` project of a Blazor Web App (.NET 8 or later), place the following C# code in the `Program` file after the [WebAssemblyHost](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhost) is created with [WebAssemblyHostBuilder.CreateDefault](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.createdefault) and before the line that builds and runs the project (`await builder.Build().RunAsync();`):

```
Console.WriteLine(
    $"Client Hosting Environment: {builder.HostEnvironment.Environment}");
```

For more information on Blazor startup, see [ASP.NET Core Blazor startup](startup?view=aspnetcore-10.0).

## Set the client-side environment via header

Blazor WebAssembly apps can set the environment with the `Blazor-Environment` header. Specifically, the response header must be set on the `_framework/blazor.boot.json` file, but there's no harm setting the header on file server responses for other Blazor file requests or the entire Blazor deployment.

Although the Blazor framework issues the header name in kebab case with mixed letter case (`Blazor-Environment`), you're welcome to use all-lower or all-upper kebab case (`blazor-environment`, `BLAZOR-ENVIRONMENT`).

For local development runs with Blazor's built-in development server, you can control the value of the `Blazor-Environment` header by setting the value of the `ASPNETCORE_ENVIRONMENT` environment variable in the project's `Properties/launchSettings.json` file. When running locally with the development server, the order of precedence for determining the app's environment is [`Blazor.start` configuration (`environment` key)](#set-the-client-side-environment-via-blazor-startup-configuration) > `Blazor-Environment` response header (`blazor.boot.json` file) > `ASPNETCORE_ENVIRONMENT` environment variable (`launchSettings.json`). You can't use the `ASPNETCORE_ENVIRONMENT` environment variable (`launchSettings.json`) approach for a deployed Blazor WebAssembly app. The technique only works with the development server on local runs of the app.

Note

When an app is run with the .NET CLI, the first launch profile in `launchSettings.json` whose `commandName` is `Project` is used by default. To use a different profile (for example, `https`), pass the [`-lp|--launch-profile` option](/en-us/dotnet/core/tools/dotnet-run#options) to [`dotnet watch`](/en-us/dotnet/core/tools/dotnet-watch) or [`dotnet run`](/en-us/dotnet/core/tools/dotnet-run) or move the preferred profile to the top of the file.

### IIS

In the following example for IIS, the custom header (`Blazor-Environment`) is added to the published `web.config` file. The `web.config` file is located in the `bin/Release/{TARGET FRAMEWORK}/publish` folder, where the `{TARGET FRAMEWORK}` placeholder is the target framework:

```
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    ...
    <httpProtocol>
      <customHeaders>
        <add name="Blazor-Environment" value="Staging" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>
```

Note

To use a custom `web.config` file for IIS that isn't overwritten when the app is published to the `publish` folder, see [Host and deploy ASP.NET Core Blazor WebAssembly with IIS](../host-and-deploy/webassembly/iis?view=aspnetcore-10.0#use-of-a-custom-webconfig).

### Nginx

For Nginx servers, use the `add_header` directive from the `ngx_http_headers_module`:

```
http {
    server {
        ...
        location / {
            ...
            add_header Blazor-Environment "Staging";
        }
    }
}
```

For more information, see the following resources:

* [Nginx documentation](http://nginx.org/docs/http/ngx_http_headers_module.html)
* [Host and deploy ASP.NET Core Blazor WebAssembly with Nginx](../host-and-deploy/webassembly/nginx?view=aspnetcore-10.0)

### Apache

For Apache servers, use the `Header` directive from the `mod_headers` module:

```
<VirtualHost *:80>
    ...
    Header set Blazor-Environment "Staging"
    ...
</VirtualHost>
```

For more information, see the following resources:

* [Apache documentation (search the latest release for "`mod_headers`")](https://httpd.apache.org/docs/)
* [Host and deploy ASP.NET Core Blazor WebAssembly with Apache](../host-and-deploy/webassembly/apache?view=aspnetcore-10.0)

### Set the environment for Azure App Service

For a standalone Blazor WebAssembly app, you can set the environment manually via [start configuration](#set-the-client-side-environment-via-blazor-startup-configuration) or the [`Blazor-Environment` header](#set-the-client-side-environment-via-header).

For a server-side app, set the environment via an `ASPNETCORE_ENVIRONMENT` app setting in Azure:

1. ***Confirm that the casing of environment segments in app settings file names match their environment name casing exactly***. For example, the matching app settings file name for the `Staging` environment is `appsettings.Staging.json`. If the file name is `appsettings.staging.json` (lowercase "`s`"), the file isn't located, and the settings in the file aren't used in the `Staging` environment.

2. For Visual Studio deployment, confirm that the app is deployed to the correct deployment slot. For an app named `BlazorAzureAppSample`, the app is deployed to the `Staging` deployment slot.

3. In the Azure portal for the environment's deployment slot, set the environment with the `ASPNETCORE_ENVIRONMENT` app setting. For an app named `BlazorAzureAppSample`, the staging App Service Slot is named `BlazorAzureAppSample/Staging`. For the `Staging` slot's configuration, create an app setting for `ASPNETCORE_ENVIRONMENT` with a value of `Staging`. **Deployment slot setting** is enabled for the setting.

When requested in a browser, the `BlazorAzureAppSample/Staging` app loads in the `Staging` environment at `https://blazorazureappsample-staging.azurewebsites.net`.

When the app is loaded in the browser, the response header collection for `blazor.boot.json` indicates that the `Blazor-Environment` header value is `Staging`.

App settings from the `appsettings.{ENVIRONMENT}.json` file are loaded by the app, where the `{ENVIRONMENT}` placeholder is the app's environment. In the preceding example, settings from the `appsettings.Staging.json` file are loaded.

## `EnvironmentView` component

Use the `EnvironmentView` component for conditional rendering based on the hosting environment. This component provides a consistent way to render content based on the current environment across both server-side and client-side hosting models.

The `EnvironmentView` component accepts `Include` and `Exclude` parameters for specifying environment names. The component performs case-insensitive matching.

```
@using Microsoft.AspNetCore.Components.Web

<EnvironmentView Include="Development">
    <div class="alert alert-warning">
        Debug mode enabled
    </div>
</EnvironmentView>

<EnvironmentView Include="Development,Staging">
    <p>Pre-production environment</p>
</EnvironmentView>

<EnvironmentView Exclude="Production">
    <p>@DateTime.Now</p>
</EnvironmentView>
```

## Read the environment in a Blazor WebAssembly app

Obtain the app's environment in a component by injecting [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) and reading the [Environment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.environment#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-environment) property.

`ReadEnvironment.razor`:

```
@page "/read-environment"
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@inject IWebAssemblyHostEnvironment Env

<h1>Environment example</h1>

<p>Environment: @Env.Environment</p>
```

## Read the environment client-side in a Blazor Web App

Assuming that prerendering isn't disabled for a component or the app, a component in the `.Client` project is prerendered on the server. Because the server doesn't have a registered [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) service, it isn't possible to inject the service and use the service implementation's host environment extension methods and properties during server prerendering. Injecting the service into an Interactive WebAssembly or Interactive Auto component results in the following runtime error:

> There is no registered service of type 'Microsoft.AspNetCore.Components.WebAssembly.Hosting.IWebAssemblyHostEnvironment'.

To address this, create a custom service implementation for [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) on the server. For more information and an example implementation, see the [Custom service implementation on the server](../components/prerender?view=aspnetcore-10.0#custom-service-implementation-on-the-server) section of the *Prerendering* article, which appears later in the Blazor documentation.

## Read the client-side environment during startup

During startup, the [WebAssemblyHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder) exposes the [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) through the [HostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.hostenvironment#microsoft-aspnetcore-components-webassembly-hosting-webassemblyhostbuilder-hostenvironment) property, which enables environment-specific logic in host builder code.

In the `Program` file:

```
if (builder.HostEnvironment.Environment == "Custom")
{
    ...
};
```

The following convenience extension methods provided through [WebAssemblyHostEnvironmentExtensions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostenvironmentextensions) permit checking the current environment for `Development`, `Production`, `Staging`, and custom environment names:

* [IsDevelopment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostenvironmentextensions.isdevelopment)
* [IsProduction](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostenvironmentextensions.isproduction)
* [IsStaging](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostenvironmentextensions.isstaging)
* [IsEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostenvironmentextensions.isenvironment)

In the `Program` file:

```
if (builder.HostEnvironment.IsStaging())
{
    ...
};

if (builder.HostEnvironment.IsEnvironment("Custom"))
{
    ...
};
```

The [IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-baseaddress) property can be used during startup when the [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) service isn't available.

## Related content

* [ASP.NET Core Blazor startup](startup?view=aspnetcore-10.0)
* [ASP.NET Core runtime environments](../../fundamentals/environments?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](./?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/target-aspnetcore
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/target-aspnetcore.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use ASP.NET Core APIs in a class library

By [Scott Addie](https://github.com/scottaddie)

This document provides guidance for using ASP.NET Core APIs in a class library. For all other library guidance, see [Open-source library guidance](/en-us/dotnet/standard/library-guidance/).

## Determine which ASP.NET Core versions to support

ASP.NET Core adheres to the [.NET and .NET Core support policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). Consult the support policy when determining which ASP.NET Core versions to support in a library. A library should:

* Make an effort to support all ASP.NET Core versions classified as *Long-Term Support* (LTS).
* Not feel obligated to support ASP.NET Core versions classified as *End of Life* (EOL).

As preview releases of ASP.NET Core are made available, breaking changes are posted in the [aspnet/Announcements](https://github.com/aspnet/Announcements/issues) GitHub repository. Compatibility testing of libraries can be conducted as framework features are being developed.

## Use the ASP.NET Core shared framework

With the release of .NET Core 3.0, many ASP.NET Core assemblies are no longer published to NuGet as packages. Instead, the assemblies are included in the `Microsoft.AspNetCore.App` shared framework, which is installed with the .NET SDK and runtime installers. For a list of packages no longer being published, see [Remove obsolete package references](../migration/22-to-30?view=aspnetcore-10.0#remove-obsolete-package-references).

As of .NET Core 3.0, projects using the `Microsoft.NET.Sdk.Web` MSBuild SDK implicitly reference the shared framework. Projects using the `Microsoft.NET.Sdk` or `Microsoft.NET.Sdk.Razor` SDK must reference ASP.NET Core to use ASP.NET Core APIs in the shared framework.

To reference ASP.NET Core, add the following `<FrameworkReference>` element to your project file:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

## Include Blazor extensibility

Blazor supports creating [Razor components](../blazor/components/?view=aspnetcore-10.0) class libraries for server-side and client-side apps. To support Razor components in a class library, the class library must use the [Microsoft.NET.Sdk.Razor SDK](../razor-pages/sdk?view=aspnetcore-10.0).

### Support server-side and client-side apps

To support Razor component consumption by server-side and client-side apps from a single library, use the following instructions for your editor.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / .NET CLI](#tabpanel_1_visual-studio-code+net-cli)

Use the **Razor Class Library** project template.

Note

Do ***not*** select the **Support pages and views** checkbox. Selecting the checkbox results in a class library that only supports server-side apps.

Run the following command in the integrated terminal (VS Code) or a command shell (.NET CLI):

```
dotnet new razorclasslib
```

Note

Do ***not*** add the `-s|--support-pages-and-views` option to the `dotnet new` command. Applying the option results in a class library that only supports server-side apps.

The library generated from the project template:

* Targets the current .NET framework based on the installed SDK.
* Enables browser compatibility checks for platform dependencies by including `browser` as a supported platform with the `SupportedPlatform` MSBuild item.
* Adds a NuGet package reference for [Microsoft.AspNetCore.Components.Web](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Web).

[`RazorClassLibrary-CSharp.csproj` (reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/RazorClassLibrary-CSharp.csproj.in)

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Support multiple framework versions

If the library must support features added to Blazor in the current release while also supporting one or more earlier releases, multi-target the library. Provide a semicolon-separated list of [Target Framework Monikers (TFMs)](/en-us/dotnet/standard/frameworks) in the `TargetFrameworks` MSBuild property:

```
<TargetFrameworks>{TARGET FRAMEWORKS}</TargetFrameworks>
```

In the preceding example, the `{TARGET FRAMEWORKS}` placeholder represents the semicolon-separated TFMs list. For example, `netcoreapp3.1;net5.0`.

### Only support server-side consumption

Class libraries are rarely built to only support server-side apps. If the class library only requires server-side-specific features, such as access to [CircuitHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuithandler) or [Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage](/en-us/dotnet/api/microsoft.aspnetcore.components.server.protectedbrowserstorage), or uses ASP.NET Core-specific features, such as middleware, MVC controllers, or Razor Pages, use **one** of the following approaches:

* Specify that the library supports pages and views when the library is created with the **Support pages and views** checkbox (Visual Studio) or the `-s|--support-pages-and-views` option with the `dotnet new` command:

  ```
  dotnet new razorclasslib -s
  ```

* Only provide a framework reference to ASP.NET Core in the library's project file in addition to any other required MSBuild properties:

  ```
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
  ```

For more information on libraries containing Razor components, see [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../blazor/components/class-libraries?view=aspnetcore-10.0).

## Include MVC extensibility

This section outlines recommendations for libraries that include:

* [Razor views or Razor Pages](#razor-views-or-razor-pages)
* [Tag Helpers](#tag-helpers)
* [View components](#view-components)

This section doesn't discuss multi-targeting to support multiple versions of MVC. For guidance on supporting multiple ASP.NET Core versions, see [Support multiple ASP.NET Core versions](#support-multiple-aspnet-core-versions).

### Razor views or Razor Pages

A project that includes [Razor views](../mvc/views/overview?view=aspnetcore-10.0) or [Razor Pages](../razor-pages/?view=aspnetcore-10.0) must use the [Microsoft.NET.Sdk.Razor SDK](../razor-pages/sdk?view=aspnetcore-10.0).

If the project targets .NET Core 3.x, it requires:

* An `AddRazorSupportForMvc` MSBuild property set to `true`.
* A `<FrameworkReference>` element for the shared framework.

The **Razor Class Library** project template satisfies the preceding requirements for projects targeting .NET Core. Use the following instructions for your editor.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / .NET CLI](#tabpanel_2_visual-studio-code+net-cli)

Use the **Razor Class Library** project template. The template's **Support pages and views** checkbox should be selected.

Run the following command in the integrated terminal (VS Code) or a command shell (.NET CLI):

```
dotnet new razorclasslib -s
```

For example:

```
<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

If the project targets .NET Standard instead, a [Microsoft.AspNetCore.Mvc](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc) package reference is required. The `Microsoft.AspNetCore.Mvc` package moved into the shared framework in ASP.NET Core 3.0 and is therefore no longer published. For example:

```
<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
  </ItemGroup>

</Project>
```

### Tag Helpers

A project that includes [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) should use the `Microsoft.NET.Sdk` SDK. If targeting .NET Core 3.x, add a `<FrameworkReference>` element for the shared framework. For example:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

If targeting .NET Standard (to support versions earlier than ASP.NET Core 3.x), add a package reference to [Microsoft.AspNetCore.Mvc.Razor](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor). The `Microsoft.AspNetCore.Mvc.Razor` package moved into the shared framework and is therefore no longer published. For example:

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
  </ItemGroup>

</Project>
```

### View components

A project that includes [View components](../mvc/views/view-components?view=aspnetcore-10.0) should use the `Microsoft.NET.Sdk` SDK. If targeting .NET Core 3.x, add a `<FrameworkReference>` element for the shared framework. For example:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

If targeting .NET Standard (to support versions earlier than ASP.NET Core 3.x), add a package reference to [Microsoft.AspNetCore.Mvc.ViewFeatures](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.ViewFeatures). The `Microsoft.AspNetCore.Mvc.ViewFeatures` package moved into the shared framework and is therefore no longer published. For example:

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="2.2.0" />
  </ItemGroup>

</Project>
```

## Support multiple ASP.NET Core versions

Multi-targeting is required to author a library that supports multiple variants of ASP.NET Core. Consider a scenario in which a Tag Helpers library must support the following ASP.NET Core variants:

* ASP.NET Core 2.1 targeting .NET Framework 4.6.1
* ASP.NET Core 2.x targeting .NET Core 2.x
* ASP.NET Core 3.x targeting .NET Core 3.x

The following project file supports these variants via the `TargetFrameworks` property:

```
<Project Sdk="Microsoft.NET.Sdk">
  
  <PropertyGroup>
    <TargetFrameworks>netcoreapp2.1;netcoreapp3.1;net461</TargetFrameworks>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="Markdig" Version="0.16.0" />
  </ItemGroup>
  
  <ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1'">
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor" Version="2.1.0" />
  </ItemGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
</Project>
```

With the preceding project file:

* The `Markdig` package is added for all consumers.
* A reference to [Microsoft.AspNetCore.Mvc.Razor](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor) is added for consumers targeting .NET Framework 4.6.1 or later or .NET Core 2.x. Version 2.1.0 of the package works with ASP.NET Core 2.2 because of backwards compatibility.
* The shared framework is referenced for consumers targeting .NET Core 3.x. The `Microsoft.AspNetCore.Mvc.Razor` package is included in the shared framework.

Alternatively, .NET Standard 2.0 could be targeted instead of targeting both .NET Core 2.1 and .NET Framework 4.6.1:

```
<Project Sdk="Microsoft.NET.Sdk">
  
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;netcoreapp3.1</TargetFrameworks>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="Markdig" Version="0.16.0" />
  </ItemGroup>
  
  <ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1'">
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor" Version="2.1.0" />
  </ItemGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
</Project>
```

With the preceding project file, the following caveats exist:

* Since the library only contains Tag Helpers, it's more straightforward to target the specific platforms on which ASP.NET Core runs: .NET Core and .NET Framework. Tag Helpers can't be used by other .NET Standard 2.0-compliant target frameworks such as Unity and UWP.
* Using .NET Standard 2.0 from .NET Framework has some issues that were addressed in .NET Framework 4.7.2. You can improve the experience for consumers using .NET Framework 4.6.1 through 4.7.1 by targeting .NET Framework 4.6.1.

If your library needs to call platform-specific APIs, target specific .NET implementations instead of .NET Standard. For more information, see [Multi-targeting](/en-us/dotnet/standard/library-guidance/cross-platform-targeting#multi-targeting).

## Use an API that hasn't changed

Imagine a scenario in which you're upgrading a middleware library from .NET Core 2.2 to 3.1. The ASP.NET Core middleware APIs being used in the library haven't changed between ASP.NET Core 2.2 and 3.1. To continue supporting the middleware library in .NET Core 3.1, take the following steps:

* Follow the [standard library guidance](/en-us/dotnet/standard/library-guidance/).
* Add a package reference for each API's NuGet package if the corresponding assembly doesn't exist in the shared framework.

## Use an API that changed

Imagine a scenario in which you're upgrading a library from .NET Core 2.2 to .NET Core 3.1. An ASP.NET Core API being used in the library has a [breaking change](/en-us/dotnet/core/compatibility/breaking-changes) in ASP.NET Core 3.1. Consider whether the library can be rewritten to not use the broken API in all versions.

If you can rewrite the library, do so and continue to target an earlier target framework (for example, .NET Standard 2.0 or .NET Framework 4.6.1) with package references.

If you can't rewrite the library, take the following steps:

* Add a target for .NET Core 3.1.
* Add a `<FrameworkReference>` element for the shared framework.
* Use the [#if preprocessor directive](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-if) with the appropriate target framework symbol to conditionally compile code.

For example, synchronous reads and writes on HTTP request and response streams are disabled by default as of ASP.NET Core 3.1. ASP.NET Core 2.2 supports the synchronous behavior by default. Consider a middleware library in which synchronous reads and writes should be enabled where I/O is occurring. The library should enclose the code to enable synchronous features in the appropriate preprocessor directive. For example:

```
public async Task Invoke(HttpContext httpContext)
{
    if (httpContext.Request.Path.StartsWithSegments(_path, StringComparison.Ordinal))
    {
        httpContext.Response.StatusCode = (int) HttpStatusCode.OK;
        httpContext.Response.ContentType = "application/json";
        httpContext.Response.ContentLength = _bufferSize;

#if !NETCOREAPP3_1 && !NETCOREAPP5_0
        var syncIOFeature = httpContext.Features.Get<IHttpBodyControlFeature>();
        if (syncIOFeature != null)
        {
            syncIOFeature.AllowSynchronousIO = true;
        }

        using (var sw = new StreamWriter(
            httpContext.Response.Body, _encoding, bufferSize: _bufferSize))
        {
            _json.Serialize(sw, new JsonMessage { message = "Hello, World!" });
        }
#else
        await JsonSerializer.SerializeAsync<JsonMessage>(
            httpContext.Response.Body, new JsonMessage { message = "Hello, World!" });
#endif
        return;
    }

    await _next(httpContext);
}
```

## Use an API introduced in 3.1

Imagine that you want to use an ASP.NET Core API that was introduced in ASP.NET Core 3.1. Consider the following questions:

1. Does the library functionally require the new API?
2. Can the library implement this feature in a different way?

If the library functionally requires the API and there's no way to implement it down-level:

* Target .NET Core 3.x only.
* Add a `<FrameworkReference>` element for the shared framework.

If the library can implement the feature in a different way:

* Add .NET Core 3.x as a target framework.
* Add a `<FrameworkReference>` element for the shared framework.
* Use the [#if preprocessor directive](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-if) with the appropriate target framework symbol to conditionally compile code.

For example, the following Tag Helper uses the [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) interface introduced in ASP.NET Core 3.1. Consumers targeting .NET Core 3.1 execute the code path defined by the `NETCOREAPP3_1` target framework symbol. The Tag Helper's constructor parameter type changes to [IHostingEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingenvironment) for .NET Core 2.1 and .NET Framework 4.6.1 consumers. This change was necessary because ASP.NET Core 3.1 marked `IHostingEnvironment` as obsolete and recommended `IWebHostEnvironment` as the replacement.

```
[HtmlTargetElement("script", Attributes = "asp-inline")]
public class ScriptInliningTagHelper : TagHelper
{
    private readonly IFileProvider _wwwroot;

#if NETCOREAPP3_1
    public ScriptInliningTagHelper(IWebHostEnvironment env)
#else
    public ScriptInliningTagHelper(IHostingEnvironment env)
#endif
    {
        _wwwroot = env.WebRootFileProvider;
    }

    // code omitted for brevity
}
```

The following multi-targeted project file supports this Tag Helper scenario:

```
<Project Sdk="Microsoft.NET.Sdk">
  
  <PropertyGroup>
    <TargetFrameworks>netcoreapp2.1;netcoreapp3.1;net461</TargetFrameworks>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="Markdig" Version="0.16.0" />
  </ItemGroup>
  
  <ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1'">
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor" Version="2.1.0" />
  </ItemGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>
</Project>
```

## Use an API removed from the shared framework

To use an ASP.NET Core assembly that was removed from the shared framework, add the appropriate package reference. For a list of packages removed from the shared framework in ASP.NET Core 3.1, see [Remove obsolete package references](../migration/22-to-30?view=aspnetcore-10.0#remove-obsolete-package-references).

For example, to add the web API client:

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

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
  </ItemGroup>

</Project>
```

## Related content

* [Reusable Razor UI in class libraries with ASP.NET Core](../razor-pages/ui-class?view=aspnetcore-10.0)
* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../blazor/components/class-libraries?view=aspnetcore-10.0)
* [.NET implementation support](/en-us/dotnet/standard/net-standard#net-implementation-support)
* [.NET support policies](https://dotnet.microsoft.com/platform/support/policy)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/bl0001
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/bl0001.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# BL0001: Component parameter should have public setters

|                                     | Value    |
| ----------------------------------- | -------- |
| **Rule ID**                         | BL0001   |
| **Category**                        | Usage    |
| **Fix is breaking or non-breaking** | Breaking |

## Cause

A property on a type deriving from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) annotated with [`[Parameter]`](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute) has a missing or non-public setters.

## Rule description

Component parameters are required to have publicly accessible setters to allow the framework to assign values. All of the parameter declarations in the following example result in this diagnostic.

```
@code
{
    [Parameter] int Parameter1 { get; set; }

    [Parameter] public int Parameter2 { get; }

    [Parameter] public int Parameter3 { get; private set; }
}
```

## How to fix violations

* Make the property and its setter public.

```
@code
{
    [Parameter] public int Parameter1 { get; set; }

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

    [Parameter] public int Parameter3 { get; set; }
}
```

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-05-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/customize-identity-model.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Identity model customization in ASP.NET Core

By [Arthur Vickers](https://github.com/ajcvickers)

ASP.NET Core Identity provides a framework for managing and storing user accounts in ASP.NET Core apps. Identity is added to your project when **Individual Accounts** is selected as the authentication mechanism. By default, Identity makes use of an Entity Framework (EF) Core data model. This article describes how to customize the Identity model.

## Identity and EF Core Migrations

Before examining the model, it's useful to understand how Identity works with [EF Core Migrations](/en-us/ef/core/managing-schemas/migrations/) to create and update a database. At the top level, the process is:

1. Define or update a [data model in code](/en-us/ef/core/modeling/).
2. Add a Migration to translate this model into changes that can be applied to the database.
3. Check that the Migration correctly represents your intentions.
4. Apply the Migration to update the database to be in sync with the model.
5. Repeat steps 1 through 4 to further refine the model and keep the database in sync.

Use one of the following approaches to add and apply Migrations:

* The **Package Manager Console** (PMC) window if using Visual Studio. For more information, see [EF Core PMC tools](/en-us/ef/core/miscellaneous/cli/powershell).
* The .NET CLI if using the command line. For more information, see [EF Core .NET command line tools](/en-us/ef/core/miscellaneous/cli/dotnet).
* Clicking the **Apply Migrations** button on the error page when the app is run.

ASP.NET Core has a development-time error page handler. The handler can apply migrations when the app is run. Production apps typically generate SQL scripts from the migrations and deploy database changes as part of a controlled app and database deployment.

When a new app using Identity is created, steps 1 and 2 above have already been completed. That is, the initial data model already exists, and the initial migration has been added to the project. The initial migration still needs to be applied to the database. The initial migration can be applied via one of the following approaches:

* Run `Update-Database` in PMC.
* Run `dotnet ef database update` in a command shell.
* Click the **Apply Migrations** button on the error page when the app is run.

Repeat the preceding steps as changes are made to the model.

Important

When Identity options that affect the underlying EF Core model are configured (for example, `options.Stores.MaxLengthForKeys` or `options.Stores.SchemaVersion`), those option values must also be applied at design time for EF Core Migrations to generate the correct model shape. If the EF tooling runs without these options configured, generated migrations may omit the intended changes. For more information, see [efcore#36314](https://github.com/dotnet/efcore/issues/36314). To ensure Identity options are applied consistently during migration generation, use one of the following approaches:

* **Set the startup project:** Run `dotnet ef` commands (or PMC commands) with the application project that calls `AddDefaultIdentity` or `AddIdentityCore` set as the startup project. For example, when running commands from a class library project, specify the startup project with `dotnet ef migrations add {MIGRATION_NAME} --startup-project {PATH_TO_APP_PROJECT}`, where the `{MIGRATION_NAME}` placeholder is the migration name and the `{PATH_TO_APP_PROJECT}` placeholder is the path to the app project.
* **Implement `IDesignTimeDbContextFactory`:** Alternatively, implement an `IDesignTimeDbContextFactory<TContext>` that constructs the context and applies the equivalent Identity option configuration. For an Aspire-friendly solution, see [efcore#35285 (comment)](https://github.com/dotnet/efcore/issues/35285#issuecomment-3161145762).

Example Identity configuration in `Program.cs`:

```
builder.Services
    .AddDefaultIdentity<ApplicationUser>(options =>
    {
        options.Stores.SchemaVersion = IdentitySchemaVersions.Version2;
        options.Stores.MaxLengthForKeys = 256;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>();
```

Example design-time factory:

```
public class DesignTimeApplicationDbContextFactory
    : IDesignTimeDbContextFactory<ApplicationDbContext>
{
    public ApplicationDbContext CreateDbContext(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseSqlServer("{CONNECTION_STRING}");

        return new ApplicationDbContext(optionsBuilder.Options);
    }
}
```

Note

You cannot access `options.Stores.MaxLengthForKeys` directly inside `OnModelCreating` because dependency injection isn't available at design time. Instead, specify the configured value directly (such as `HasMaxLength(256)`), or use a design-time mechanism to pass settings if needed. For more details, see [Configure ASP.NET Core Identity](identity-configuration?view=aspnetcore-10.0).

Tip

Always verify the resulting model snapshot reflects the intended key lengths or schema version after adding a migration.

## The Identity model

### Entity types

The Identity model consists of the following entity types.

| Entity type | Description                                                   |
| ----------- | ------------------------------------------------------------- |
| `User`      | Represents the user.                                          |
| `Role`      | Represents a role.                                            |
| `UserClaim` | Represents a claim that a user possesses.                     |
| `UserToken` | Represents an authentication token for a user.                |
| `UserLogin` | Associates a user with a login.                               |
| `RoleClaim` | Represents a claim that's granted to all users within a role. |
| `UserRole`  | A join entity that associates users and roles.                |

### Entity type relationships

The [entity types](#entity-types) are related to each other in the following ways:

* Each `User` can have many `UserClaims`.
* Each `User` can have many `UserLogins`.
* Each `User` can have many `UserTokens`.
* Each `Role` can have many associated `RoleClaims`.
* Each `User` can have many associated `Roles`, and each `Role` can be associated with many `Users`. This is a many-to-many relationship that requires a join table in the database. The join table is represented by the `UserRole` entity.

### Default model configuration

Identity defines many *context classes* that inherit from [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) to configure and use the model. This configuration is done using the [EF Core Code First Fluent API](/en-us/ef/core/modeling/) in the [OnModelCreating](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.onmodelcreating) method of the context class. The default configuration is:

```
builder.Entity<TUser>(b =>
{
    // Primary key
    b.HasKey(u => u.Id);

    // Indexes for "normalized" username and email, to allow efficient lookups
    b.HasIndex(u => u.NormalizedUserName).HasName("UserNameIndex").IsUnique();
    b.HasIndex(u => u.NormalizedEmail).HasName("EmailIndex");

    // Maps to the AspNetUsers table
    b.ToTable("AspNetUsers");

    // A concurrency token for use with the optimistic concurrency checking
    b.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();

    // Limit the size of columns to use efficient database types
    b.Property(u => u.UserName).HasMaxLength(256);
    b.Property(u => u.NormalizedUserName).HasMaxLength(256);
    b.Property(u => u.Email).HasMaxLength(256);
    b.Property(u => u.NormalizedEmail).HasMaxLength(256);

    // The relationships between User and other entity types
    // Note that these relationships are configured with no navigation properties

    // Each User can have many UserClaims
    b.HasMany<TUserClaim>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();

    // Each User can have many UserLogins
    b.HasMany<TUserLogin>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();

    // Each User can have many UserTokens
    b.HasMany<TUserToken>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();

    // Each User can have many entries in the UserRole join table
    b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
});

builder.Entity<TUserClaim>(b =>
{
    // Primary key
    b.HasKey(uc => uc.Id);

    // Maps to the AspNetUserClaims table
    b.ToTable("AspNetUserClaims");
});

builder.Entity<TUserLogin>(b =>
{
    // Composite primary key consisting of the LoginProvider and the key to use
    // with that provider
    b.HasKey(l => new { l.LoginProvider, l.ProviderKey });

    // Limit the size of the composite key columns due to common DB restrictions
    b.Property(l => l.LoginProvider).HasMaxLength(128);
    b.Property(l => l.ProviderKey).HasMaxLength(128);

    // Maps to the AspNetUserLogins table
    b.ToTable("AspNetUserLogins");
});

builder.Entity<TUserToken>(b =>
{
    // Composite primary key consisting of the UserId, LoginProvider and Name
    b.HasKey(t => new { t.UserId, t.LoginProvider, t.Name });

    // Limit the size of the composite key columns due to common DB restrictions
    b.Property(t => t.LoginProvider).HasMaxLength(maxKeyLength);
    b.Property(t => t.Name).HasMaxLength(maxKeyLength);

    // Maps to the AspNetUserTokens table
    b.ToTable("AspNetUserTokens");
});

builder.Entity<TRole>(b =>
{
    // Primary key
    b.HasKey(r => r.Id);

    // Index for "normalized" role name to allow efficient lookups
    b.HasIndex(r => r.NormalizedName).HasName("RoleNameIndex").IsUnique();

    // Maps to the AspNetRoles table
    b.ToTable("AspNetRoles");

    // A concurrency token for use with the optimistic concurrency checking
    b.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();

    // Limit the size of columns to use efficient database types
    b.Property(u => u.Name).HasMaxLength(256);
    b.Property(u => u.NormalizedName).HasMaxLength(256);

    // The relationships between Role and other entity types
    // Note that these relationships are configured with no navigation properties

    // Each Role can have many entries in the UserRole join table
    b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();

    // Each Role can have many associated RoleClaims
    b.HasMany<TRoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
});

builder.Entity<TRoleClaim>(b =>
{
    // Primary key
    b.HasKey(rc => rc.Id);

    // Maps to the AspNetRoleClaims table
    b.ToTable("AspNetRoleClaims");
});

builder.Entity<TUserRole>(b =>
{
    // Primary key
    b.HasKey(r => new { r.UserId, r.RoleId });

    // Maps to the AspNetUserRoles table
    b.ToTable("AspNetUserRoles");
});
```

### Model generic types

Identity defines default [Common Language Runtime](/en-us/dotnet/standard/glossary#clr) (CLR) types for each of the entity types listed above. These types are all prefixed with *Identity*:

* `IdentityUser`
* `IdentityRole`
* `IdentityUserClaim`
* `IdentityUserToken`
* `IdentityUserLogin`
* `IdentityRoleClaim`
* `IdentityUserRole`

Rather than using these types directly, the types can be used as base classes for the app's own types. The `DbContext` classes defined by Identity are generic, such that different CLR types can be used for one or more of the entity types in the model. These generic types also allow the `User` primary key (PK) data type to be changed.

When using Identity with support for roles, an [IdentityDbContext](/en-us/dotnet/api/microsoft.aspnetcore.identity.entityframeworkcore.identitydbcontext) class should be used. For example:

```
// Uses all the built-in Identity types
// Uses `string` as the key type
public class IdentityDbContext
    : IdentityDbContext<IdentityUser, IdentityRole, string>
{
}

// Uses the built-in Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityDbContext<TUser>
    : IdentityDbContext<TUser, IdentityRole, string>
        where TUser : IdentityUser
{
}

// Uses the built-in Identity types except with custom User and Role types
// The key type is defined by TKey
public class IdentityDbContext<TUser, TRole, TKey> : IdentityDbContext<
    TUser, TRole, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>,
    IdentityUserLogin<TKey>, IdentityRoleClaim<TKey>, IdentityUserToken<TKey>>
        where TUser : IdentityUser<TKey>
        where TRole : IdentityRole<TKey>
        where TKey : IEquatable<TKey>
{
}

// No built-in Identity types are used; all are specified by generic arguments
// The key type is defined by TKey
public abstract class IdentityDbContext<
    TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
    : IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>
         where TUser : IdentityUser<TKey>
         where TRole : IdentityRole<TKey>
         where TKey : IEquatable<TKey>
         where TUserClaim : IdentityUserClaim<TKey>
         where TUserRole : IdentityUserRole<TKey>
         where TUserLogin : IdentityUserLogin<TKey>
         where TRoleClaim : IdentityRoleClaim<TKey>
         where TUserToken : IdentityUserToken<TKey>
```

It's also possible to use Identity without roles (only claims), in which case an [IdentityUserContext\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.entityframeworkcore.identityusercontext-1) class should be used:

```
// Uses the built-in non-role Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityUserContext<TUser>
    : IdentityUserContext<TUser, string>
        where TUser : IdentityUser
{
}

// Uses the built-in non-role Identity types except with a custom User type
// The key type is defined by TKey
public class IdentityUserContext<TUser, TKey> : IdentityUserContext<
    TUser, TKey, IdentityUserClaim<TKey>, IdentityUserLogin<TKey>,
    IdentityUserToken<TKey>>
        where TUser : IdentityUser<TKey>
        where TKey : IEquatable<TKey>
{
}

// No built-in Identity types are used; all are specified by generic arguments, with no roles
// The key type is defined by TKey
public abstract class IdentityUserContext<
    TUser, TKey, TUserClaim, TUserLogin, TUserToken> : DbContext
        where TUser : IdentityUser<TKey>
        where TKey : IEquatable<TKey>
        where TUserClaim : IdentityUserClaim<TKey>
        where TUserLogin : IdentityUserLogin<TKey>
        where TUserToken : IdentityUserToken<TKey>
{
}
```

## Customize the model

The starting point for model customization is to derive from the appropriate context type. See the [Model generic types](#model-generic-types) section. This context type is customarily called `ApplicationDbContext` and is created by the ASP.NET Core templates.

The context is used to configure the model in two ways:

* Supplying entity and key types for the generic type parameters.
* Overriding `OnModelCreating` to modify the mapping of these types.

When overriding `OnModelCreating`, `base.OnModelCreating` should be called first; the overriding configuration should be called next. EF Core generally has a last-one-wins policy for configuration. For example, if the `ToTable` method for an entity type is called first with one table name and then again later with a different table name, the table name in the second call is used.

***NOTE***: If the `DbContext` doesn't derive from `IdentityDbContext`, `AddEntityFrameworkStores` may not infer the correct POCO types for `TUserClaim`, `TUserLogin`, and `TUserToken`. If `AddEntityFrameworkStores` doesn't infer the correct POCO types, a workaround is to directly add the correct types via `services.AddScoped<IUser/RoleStore<TUser>` and `UserStore<...>>`.

### Custom user data

[Custom user data](add-user-data?view=aspnetcore-10.0) is supported by inheriting from `IdentityUser`. It's customary to name this type `ApplicationUser`:

```
public class ApplicationUser : IdentityUser
{
    public string CustomTag { get; set; }
}
```

Use the `ApplicationUser` type as a generic argument for the context:

```
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}
```

There's no need to override `OnModelCreating` in the `ApplicationDbContext` class. EF Core maps the `CustomTag` property by convention. However, the database needs to be updated to create a new `CustomTag` column. To create the column, add a migration, and then update the database as described in [Identity and EF Core Migrations](#identity-and-ef-core-migrations).

Update `Pages/Shared/_LoginPartial.cshtml` and replace `IdentityUser` with `ApplicationUser`:

```
@using Microsoft.AspNetCore.Identity
@using WebApp1.Areas.Identity.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
```

Update `Areas/Identity/IdentityHostingStartup.cs` or `Startup.ConfigureServices` and replace `IdentityUser` with `ApplicationUser`.

```
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();                                    
```

Calling [AddDefaultIdentity](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionuiextensions.adddefaultidentity) is equivalent to the following code:

```
services.AddAuthentication(o =>
{
    o.DefaultScheme = IdentityConstants.ApplicationScheme;
    o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies(o => { });

services.AddIdentityCore<TUser>(o =>
{
    o.Stores.MaxLengthForKeys = 128;
    o.SignIn.RequireConfirmedAccount = true;
})
.AddDefaultUI()
.AddDefaultTokenProviders();
```

Identity is provided as a Razor class library. For more information, see [Scaffold Identity in ASP.NET Core projects](scaffold-identity?view=aspnetcore-10.0). Consequently, the preceding code requires a call to [AddDefaultUI](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderuiextensions.adddefaultui). If the Identity scaffolder was used to add Identity files to the project, remove the call to `AddDefaultUI`. For more information, see:

* [Scaffold Identity](scaffold-identity?view=aspnetcore-10.0)
* [Add, download, and delete custom user data to Identity](add-user-data?view=aspnetcore-10.0)

### Change the primary key type

A change to the PK column's data type after the database has been created is problematic on many database systems. Changing the PK typically involves dropping and re-creating the table. Therefore, key types should be specified in the initial migration when the database is created.

Follow these steps to change the PK type:

1. If the database was created before the PK change, run `Drop-Database` (PMC) or `dotnet ef database drop` (.NET CLI) to delete it.

2. After confirming deletion of the database, remove the initial migration with `Remove-Migration` (PMC) or `dotnet ef migrations remove` (.NET CLI).

3. Update the `ApplicationDbContext` class to derive from [IdentityDbContext\<TUser,TRole,TKey>](/en-us/dotnet/api/microsoft.aspnetcore.identity.entityframeworkcore.identitydbcontext-3). Specify the new key type for `TKey`. For example, to use a `Guid` key type:

   ```
   public class ApplicationDbContext
       : IdentityDbContext<IdentityUser<Guid>, IdentityRole<Guid>, Guid>
   {
       public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
           : base(options)
       {
       }
   }
   ```

   In the preceding code, the generic classes [IdentityUser\<TKey>](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser-1) and [IdentityRole\<TKey>](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityrole-1) must be specified to use the new key type.

   `Startup.ConfigureServices` must be updated to use the generic user:

   ```
   services.AddDefaultIdentity<IdentityUser<Guid>>(options => options.SignIn.RequireConfirmedAccount = true)
           .AddEntityFrameworkStores<ApplicationDbContext>();
   ```

4. If a custom `ApplicationUser` class is being used, update the class to inherit from `IdentityUser`. For example:

   ```
   using System;
   using Microsoft.AspNetCore.Identity;

   public class ApplicationUser : IdentityUser<Guid>
   {
       public string CustomTag { get; set; }
   }
   ```

   Update `ApplicationDbContext` to reference the custom `ApplicationUser` class:

   ```
   public class ApplicationDbContext
       : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
   {
       public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
           : base(options)
       {
       }
   }
   ```

   Register the custom database context class when adding the Identity service in `Startup.ConfigureServices`:

   ```
   services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
           .AddEntityFrameworkStores<ApplicationDbContext>();
   ```

   The primary key's data type is inferred by analyzing the [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) object.

   Identity is provided as a Razor class library. For more information, see [Scaffold Identity in ASP.NET Core projects](scaffold-identity?view=aspnetcore-10.0). Consequently, the preceding code requires a call to [AddDefaultUI](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderuiextensions.adddefaultui). If the Identity scaffolder was used to add Identity files to the project, remove the call to `AddDefaultUI`.

5. If a custom `ApplicationRole` class is being used, update the class to inherit from `IdentityRole<TKey>`. For example:

   ```
   using System;
   using Microsoft.AspNetCore.Identity;

   public class ApplicationRole : IdentityRole<Guid>
   {
       public string Description { get; set; }
   }
   ```

   Update `ApplicationDbContext` to reference the custom `ApplicationRole` class. For example, the following class references a custom `ApplicationUser` and a custom `ApplicationRole`:

   ```
   using System;
   using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
   using Microsoft.EntityFrameworkCore;

   public class ApplicationDbContext :
       IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
   {
       public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
           : base(options)
       {
       }
   }
   ```

   Register the custom database context class when adding the Identity service in `Startup.ConfigureServices`:

   ```
   public void ConfigureServices(IServiceCollection services)
   {
       services.Configure<CookiePolicyOptions>(options =>
       {
           options.CheckConsentNeeded = context => true;
           options.MinimumSameSitePolicy = SameSiteMode.None;
       });

       services.AddDbContext<ApplicationDbContext>(options =>
           options.UseSqlServer(
               Configuration.GetConnectionString("DefaultConnection")));

       services.AddIdentity<ApplicationUser, ApplicationRole>()
               .AddEntityFrameworkStores<ApplicationDbContext>()
               .AddDefaultUI()
               .AddDefaultTokenProviders();

       services.AddMvc()
               .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
   }
   ```

   The primary key's data type is inferred by analyzing the [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) object.

   Identity is provided as a Razor class library. For more information, see [Scaffold Identity in ASP.NET Core projects](scaffold-identity?view=aspnetcore-10.0). Consequently, the preceding code requires a call to [AddDefaultUI](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderuiextensions.adddefaultui). If the Identity scaffolder was used to add Identity files to the project, remove the call to `AddDefaultUI`.

### Add navigation properties

Changing the model configuration for relationships can be more difficult than making other changes. Care must be taken to replace the existing relationships rather than create new, additional relationships. In particular, the changed relationship must specify the same foreign key (FK) property as the existing relationship. For example, the relationship between `Users` and `UserClaims` is, by default, specified as follows:

```
builder.Entity<TUser>(b =>
{
    // Each User can have many UserClaims
    b.HasMany<TUserClaim>()
     .WithOne()
     .HasForeignKey(uc => uc.UserId)
     .IsRequired();
});
```

The FK for this relationship is specified as the `UserClaim.UserId` property. `HasMany` and `WithOne` are called without arguments to create the relationship without navigation properties.

Add a navigation property to `ApplicationUser` that allows associated `UserClaims` to be referenced from the user:

```
public class ApplicationUser : IdentityUser
{
    public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
}
```

The `TKey` for `IdentityUserClaim<TKey>` is the type specified for the PK of users. In this case, `TKey` is `string` because the defaults are being used. It's **not** the PK type for the `UserClaim` entity type.

Now that the navigation property exists, it must be configured in `OnModelCreating`:

```
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne()
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();
        });
    }
}
```

Notice that relationship is configured exactly as it was before, only with a navigation property specified in the call to `HasMany`.

The navigation properties only exist in the EF model, not the database. Because the FK for the relationship hasn't changed, this kind of model change doesn't require the database to be updated. This can be checked by adding a migration after making the change. The `Up` and `Down` methods are empty.

### Add all User navigation properties

Using the section above as guidance, the following example configures unidirectional navigation properties for all relationships on User:

```
public class ApplicationUser : IdentityUser
{
    public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
    public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; }
    public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; }
    public virtual ICollection<IdentityUserRole<string>> UserRoles { get; set; }
}
```

```
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne()
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();

            // Each User can have many UserLogins
            b.HasMany(e => e.Logins)
                .WithOne()
                .HasForeignKey(ul => ul.UserId)
                .IsRequired();

            // Each User can have many UserTokens
            b.HasMany(e => e.Tokens)
                .WithOne()
                .HasForeignKey(ut => ut.UserId)
                .IsRequired();

            // Each User can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne()
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
        });
    }
}
```

### Add User and Role navigation properties

Using the section above as guidance, the following example configures navigation properties for all relationships on User and Role:

```
public class ApplicationUser : IdentityUser
{
    public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
    public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; }
    public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; }
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationRole : IdentityRole
{
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationUserRole : IdentityUserRole<string>
{
    public virtual ApplicationUser User { get; set; }
    public virtual ApplicationRole Role { get; set; }
}
```

```
public class ApplicationDbContext
    : IdentityDbContext<
        ApplicationUser, ApplicationRole, string,
        IdentityUserClaim<string>, ApplicationUserRole, IdentityUserLogin<string>,
        IdentityRoleClaim<string>, IdentityUserToken<string>>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne()
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();

            // Each User can have many UserLogins
            b.HasMany(e => e.Logins)
                .WithOne()
                .HasForeignKey(ul => ul.UserId)
                .IsRequired();

            // Each User can have many UserTokens
            b.HasMany(e => e.Tokens)
                .WithOne()
                .HasForeignKey(ut => ut.UserId)
                .IsRequired();

            // Each User can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.User)
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
        });

        modelBuilder.Entity<ApplicationRole>(b =>
        {
            // Each Role can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.Role)
                .HasForeignKey(ur => ur.RoleId)
                .IsRequired();
        });

    }
}
```

Notes:

* This example also includes the `UserRole` join entity, which is needed to navigate the many-to-many relationship from Users to Roles.
* Remember to change the types of the navigation properties to reflect that `Application{...}` types are now being used instead of `Identity{...}` types.
* Remember to use the `Application{...}` in the generic `ApplicationContext` definition.

### Add all navigation properties

Using the section above as guidance, the following example configures navigation properties for all relationships on all entity types:

```
public class ApplicationUser : IdentityUser
{
    public virtual ICollection<ApplicationUserClaim> Claims { get; set; }
    public virtual ICollection<ApplicationUserLogin> Logins { get; set; }
    public virtual ICollection<ApplicationUserToken> Tokens { get; set; }
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationRole : IdentityRole
{
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
    public virtual ICollection<ApplicationRoleClaim> RoleClaims { get; set; }
}

public class ApplicationUserRole : IdentityUserRole<string>
{
    public virtual ApplicationUser User { get; set; }
    public virtual ApplicationRole Role { get; set; }
}

public class ApplicationUserClaim : IdentityUserClaim<string>
{
    public virtual ApplicationUser User { get; set; }
}

public class ApplicationUserLogin : IdentityUserLogin<string>
{
    public virtual ApplicationUser User { get; set; }
}

public class ApplicationRoleClaim : IdentityRoleClaim<string>
{
    public virtual ApplicationRole Role { get; set; }
}

public class ApplicationUserToken : IdentityUserToken<string>
{
    public virtual ApplicationUser User { get; set; }
}
```

```
public class ApplicationDbContext
    : IdentityDbContext<
        ApplicationUser, ApplicationRole, string,
        ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin,
        ApplicationRoleClaim, ApplicationUserToken>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne(e => e.User)
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();

            // Each User can have many UserLogins
            b.HasMany(e => e.Logins)
                .WithOne(e => e.User)
                .HasForeignKey(ul => ul.UserId)
                .IsRequired();

            // Each User can have many UserTokens
            b.HasMany(e => e.Tokens)
                .WithOne(e => e.User)
                .HasForeignKey(ut => ut.UserId)
                .IsRequired();

            // Each User can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.User)
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
        });

        modelBuilder.Entity<ApplicationRole>(b =>
        {
            // Each Role can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.Role)
                .HasForeignKey(ur => ur.RoleId)
                .IsRequired();

            // Each Role can have many associated RoleClaims
            b.HasMany(e => e.RoleClaims)
                .WithOne(e => e.Role)
                .HasForeignKey(rc => rc.RoleId)
                .IsRequired();
        });
    }
}
```

### Use composite keys

The preceding sections demonstrated changing the type of key used in the Identity model. Changing the Identity key model to use composite keys isn't supported or recommended. Using a composite key with Identity involves changing how the Identity manager code interacts with the model. This customization is beyond the scope of this document.

### Change table/column names and facets

To change the names of tables and columns, call `base.OnModelCreating`. Then, add configuration to override any of the defaults. For example, to change the name of all the Identity tables:

```
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<IdentityUser>(b =>
    {
        b.ToTable("MyUsers");
    });

    modelBuilder.Entity<IdentityUserClaim<string>>(b =>
    {
        b.ToTable("MyUserClaims");
    });

    modelBuilder.Entity<IdentityUserLogin<string>>(b =>
    {
        b.ToTable("MyUserLogins");
    });

    modelBuilder.Entity<IdentityUserToken<string>>(b =>
    {
        b.ToTable("MyUserTokens");
    });

    modelBuilder.Entity<IdentityRole>(b =>
    {
        b.ToTable("MyRoles");
    });

    modelBuilder.Entity<IdentityRoleClaim<string>>(b =>
    {
        b.ToTable("MyRoleClaims");
    });

    modelBuilder.Entity<IdentityUserRole<string>>(b =>
    {
        b.ToTable("MyUserRoles");
    });
}
```

These examples use the default Identity types. If using an app type such as `ApplicationUser`, configure that type instead of the default type.

The following example changes some column names:

```
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<IdentityUser>(b =>
    {
        b.Property(e => e.Email).HasColumnName("EMail");
    });

    modelBuilder.Entity<IdentityUserClaim<string>>(b =>
    {
        b.Property(e => e.ClaimType).HasColumnName("CType");
        b.Property(e => e.ClaimValue).HasColumnName("CValue");
    });
}
```

Some types of database columns can be configured with certain *facets* (for example, the maximum `string` length allowed). The following example sets column maximum lengths for several `string` properties in the model:

```
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<IdentityUser>(b =>
    {
        b.Property(u => u.UserName).HasMaxLength(128);
        b.Property(u => u.NormalizedUserName).HasMaxLength(128);
        b.Property(u => u.Email).HasMaxLength(128);
        b.Property(u => u.NormalizedEmail).HasMaxLength(128);
    });

    modelBuilder.Entity<IdentityUserToken<string>>(b =>
    {
        b.Property(t => t.LoginProvider).HasMaxLength(128);
        b.Property(t => t.Name).HasMaxLength(128);
    });
}
```

### Map to a different schema

Schemas can behave differently across database providers. For SQL Server, the default is to create all tables in the *dbo* schema. The tables can be created in a different schema. For example:

```
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.HasDefaultSchema("notdbo");
}
```

### Lazy loading

In this section, support for lazy-loading proxies in the Identity model is added. Lazy-loading is useful since it allows navigation properties to be used without first ensuring they're loaded.

Entity types can be made suitable for lazy-loading in several ways, as described in the [EF Core documentation](/en-us/ef/core/querying/related-data#lazy-loading). For simplicity, use lazy-loading proxies, which requires:

* Installation of the [Microsoft.EntityFrameworkCore.Proxies](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Proxies/) package.
* A call to [UseLazyLoadingProxies](/en-us/dotnet/api/microsoft.entityframeworkcore.proxiesextensions.uselazyloadingproxies) inside [AddDbContext](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkservicecollectionextensions.adddbcontext).
* Public entity types with `public virtual` navigation properties.

The following example demonstrates calling `UseLazyLoadingProxies` in `Startup.ConfigureServices`:

```
services
    .AddDbContext<ApplicationDbContext>(
        b => b.UseSqlServer(connectionString)
              .UseLazyLoadingProxies())
    .AddDefaultIdentity<ApplicationUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
```

Refer to the preceding examples for guidance on adding navigation properties to the entity types.

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../?view=aspnetcore-10.0#secure-authentication-flows).

## Related content

* [Scaffold Identity in ASP.NET Core projects](scaffold-identity?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-compilation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/view-compilation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Razor file compilation in ASP.NET Core

Razor files with a `.cshtml` extension are compiled at both build and publish time using the [Razor SDK](../../razor-pages/sdk?view=aspnetcore-10.0). Runtime compilation may be optionally enabled by configuring the project.

Note

Runtime compilation:

* Was obsoleted starting in .NET 10.
* Isn't supported for Razor components of Blazor apps.
* Doesn't support [global using directives](/en-us/dotnet/csharp/whats-new/csharp-10#global-using-directives).
* Doesn't support [implicit using directives](/en-us/dotnet/core/tutorials/top-level-templates#implicit-using-directives).
* Disables [.NET Hot Reload](../../test/hot-reload?view=aspnetcore-10.0).
* Isn't recommended for production scenarios.

For production scenarios, use the default build-time compilation. For development scenarios, use Hot Reload. For more information, see [Razor runtime compilation is obsolete](/en-us/dotnet/core/compatibility/aspnet-core/10/razor-runtime-compilation-obsolete).

## Razor compilation

Build-time and publish-time compilation of Razor files is enabled by default by the Razor SDK. When enabled, runtime compilation complements build-time compilation, allowing Razor files to be updated if they're edited while the app is running.

Updating Razor views and Razor Pages during development while the app is running is also supported using [.NET Hot Reload](../../test/hot-reload?view=aspnetcore-10.0).

Note

Razor runtime compilation was obsoleted starting in .NET 10. When enabled, runtime compilation disables [.NET Hot Reload](../../test/hot-reload?view=aspnetcore-10.0). We recommend using Hot Reload instead for development scenarios. For more information, see [Razor runtime compilation is obsolete](/en-us/dotnet/core/compatibility/aspnet-core/10/razor-runtime-compilation-obsolete).

## Enable runtime compilation for all environments

To enable runtime compilation for all environments:

1. Install the [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/) NuGet package.

2. Call [AddRazorRuntimeCompilation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorruntimecompilationmvcbuilderextensions.addrazorruntimecompilation) in `Program.cs`:

   ```
   var builder = WebApplication.CreateBuilder(args);

   builder.Services.AddRazorPages()
       .AddRazorRuntimeCompilation();
   ```

## Enable runtime compilation conditionally

Runtime compilation can be enabled conditionally, which ensures that the published output:

* Uses compiled views.
* Doesn't enable file watchers in production.

To enable runtime compilation only for the `Development` environment:

1. Install the [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/) NuGet package.

2. Call [AddRazorRuntimeCompilation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorruntimecompilationmvcbuilderextensions.addrazorruntimecompilation) in `Program.cs` when the current environment is set to Development:

   ```
   var builder = WebApplication.CreateBuilder(args);

   var mvcBuilder = builder.Services.AddRazorPages();

   if (builder.Environment.IsDevelopment())
   {
       mvcBuilder.AddRazorRuntimeCompilation();
   }
   ```

Runtime compilation can also be enabled with a [hosting startup assembly](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0). To enable runtime compilation in the `Development` environment for specific launch profiles:

1. Install the [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/) NuGet package.

2. Modify the launch profile's `environmentVariables` section in `launchSettings.json`:

   * Verify that `ASPNETCORE_ENVIRONMENT` is set to `"Development"`.

   * Set `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` to `"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"`. For example, the following `launchSettings.json` enables runtime compilation for the `ViewCompilationSample` and `IIS Express` launch profiles:

     ```
     {
       "iisSettings": {
         "windowsAuthentication": false,
         "anonymousAuthentication": true,
         "iisExpress": {
           "applicationUrl": "http://localhost:7098",
           "sslPort": 44332
         }
       },
       "profiles": {
         "ViewCompilationSample": {
           "commandName": "Project",
           "dotnetRunMessages": true,
           "launchBrowser": true,
           "applicationUrl": "https://localhost:7173;http://localhost:5251",
           "environmentVariables": {
             "ASPNETCORE_ENVIRONMENT": "Development",
             "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
           }
         },
         "IIS Express": {
           "commandName": "IISExpress",
           "launchBrowser": true,
           "environmentVariables": {
             "ASPNETCORE_ENVIRONMENT": "Development",
             "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
           }
         }
       }
     }
     ```

With this approach, no code changes are needed in `Program.cs`. At runtime, ASP.NET Core searches for an [assembly-level HostingStartup attribute](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0#hostingstartup-attribute) in `Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation`. The `HostingStartup` attribute specifies the app startup code to execute and that startup code enables runtime compilation.

## Enable runtime compilation for a Razor class library

Consider a scenario in which a Razor Pages project references a [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0) named *MyClassLib*. The RCL contains a `_Layout.cshtml` file consumed by MVC and Razor Pages projects. To enable runtime compilation for the `_Layout.cshtml` file in that RCL, make the following changes in the Razor Pages project:

1. Enable runtime compilation with the instructions at [Enable runtime compilation conditionally](#enable-runtime-compilation-conditionally).

2. Configure [MvcRazorRuntimeCompilationOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.runtimecompilation.mvcrazorruntimecompilationoptions) in `Program.cs`:

   ```
   var builder = WebApplication.CreateBuilder(args);

   builder.Services.AddRazorPages();

   builder.Services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
   {
       var libraryPath = Path.GetFullPath(
           Path.Combine(builder.Environment.ContentRootPath, "..", "MyClassLib"));

       options.FileProviders.Add(new PhysicalFileProvider(libraryPath));
   });
   ```

   The preceding code builds an absolute path to the *MyClassLib* RCL. The [PhysicalFileProvider API](../../fundamentals/file-providers?view=aspnetcore-10.0#physicalfileprovider) is used to locate directories and files at that absolute path. Finally, the `PhysicalFileProvider` instance is added to a file providers collection, which allows access to the RCL's `.cshtml` files.

## Additional resources

* [RazorCompileOnBuild and RazorCompileOnPublish](../../razor-pages/sdk?view=aspnetcore-10.0#properties) properties
* [Razor Pages architecture and concepts in ASP.NET Core](../../razor-pages/?view=aspnetcore-10.0)
* [Views in ASP.NET Core MVC](overview?view=aspnetcore-10.0)
* [ASP.NET Core Razor SDK](../../razor-pages/sdk?view=aspnetcore-10.0)

Razor files with a `.cshtml` extension are compiled at both build and publish time using the [Razor SDK](../../razor-pages/sdk?view=aspnetcore-10.0). Runtime compilation may be optionally enabled by configuring your project.

## Razor compilation

Build-time and publish-time compilation of Razor files is enabled by default by the Razor SDK. When enabled, runtime compilation complements build-time compilation, allowing Razor files to be updated if they're edited.

## Enable runtime compilation at project creation

The Razor Pages and MVC project templates include an option to enable runtime compilation when the project is created. This option is supported in ASP.NET Core 3.1 or later.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

In the **Create a new ASP.NET Core web application** dialog:

1. Select either the **Web Application** or the **Web Application (Model-View-Controller)** project template.
2. Select the **Enable Razor runtime compilation** checkbox.

Use the `-rrc` or `--razor-runtime-compilation` template option. For example, the following command creates a new Razor Pages project with runtime compilation enabled:

```
dotnet new webapp --razor-runtime-compilation
```

## Enable runtime compilation in an existing project

To enable runtime compilation for all environments in an existing project:

1. Install the [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/) NuGet package.

2. Update the project's `Startup.ConfigureServices` method to include a call to [AddRazorRuntimeCompilation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorruntimecompilationmvcbuilderextensions.addrazorruntimecompilation). For example:

   ```
   public void ConfigureServices(IServiceCollection services)
   {
       services.AddRazorPages()
           .AddRazorRuntimeCompilation();

       // code omitted for brevity
   }
   ```

## Conditionally enable runtime compilation in an existing project

Runtime compilation can be enabled such that it's only available for local development. Conditionally enabling in this manner ensures that the published output:

* Uses compiled views.
* Doesn't enable file watchers in production.

To enable runtime compilation only in the `Development` environment:

1. Install the [Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/) NuGet package.

2. Modify the launch profile `environmentVariables` section in `launchSettings.json`:

   * Verify `ASPNETCORE_ENVIRONMENT` is set to `"Development"`.
   * Set `ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` to `"Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"`.

In the following example, runtime compilation is enabled in the `Development` environment for the `IIS Express` and `RazorPagesApp` launch profiles:

```
{
    "iisSettings": {
      "windowsAuthentication": false,
      "anonymousAuthentication": true,
      "iisExpress": {
        "applicationUrl": "http://localhost:57676",
        "sslPort": 44364
      }
    },
    "profiles": {
      "IIS Express": {
        "commandName": "IISExpress",
        "launchBrowser": true,
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development",
          "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
        }
      },
      "RazorPagesApp": {
        "commandName": "Project",
        "launchBrowser": true,
        "applicationUrl": "https://localhost:5001;http://localhost:5000",
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development",
          "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
        }
      }
    }
  }
```

No code changes are needed in the project's `Startup` class. At runtime, ASP.NET Core searches for an [assembly-level HostingStartup attribute](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0#hostingstartup-attribute) in `Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation`. The `HostingStartup` attribute specifies the app startup code to execute. That startup code enables runtime compilation.

## Enable runtime compilation for a Razor class library

Consider a scenario in which a Razor Pages project references a [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0) named *MyClassLib*. The RCL contains a `_Layout.cshtml` file that all of your team's MVC and Razor Pages projects consume. You want to enable runtime compilation for the `_Layout.cshtml` file in that RCL. Make the following changes in the Razor Pages project:

1. Enable runtime compilation with the instructions at [Conditionally enable runtime compilation in an existing project](#conditionally-enable-runtime-compilation-in-an-existing-project).

2. Configure the runtime compilation options in `Startup.ConfigureServices`:

   ```
   public void ConfigureServices(IServiceCollection services)
   {
       services.AddRazorPages();

       services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
       {
           var libraryPath = Path.GetFullPath(
               Path.Combine(HostEnvironment.ContentRootPath, "..", "MyClassLib"));
           options.FileProviders.Add(new PhysicalFileProvider(libraryPath));
       });
   }
   ```

   In the preceding code, an absolute path to the *MyClassLib* RCL is constructed. The [PhysicalFileProvider API](../../fundamentals/file-providers?view=aspnetcore-10.0#physicalfileprovider) is used to locate directories and files at that absolute path. Finally, the `PhysicalFileProvider` instance is added to a file providers collection, which allows access to the RCL's `.cshtml` files.

## Additional resources

* [RazorCompileOnBuild and RazorCompileOnPublish](../../razor-pages/sdk?view=aspnetcore-10.0#properties) properties
* [Razor Pages architecture and concepts in ASP.NET Core](../../razor-pages/?view=aspnetcore-10.0)
* [Views in ASP.NET Core MVC](overview?view=aspnetcore-10.0)
* [ASP.NET Core Razor SDK](../../razor-pages/sdk?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/input-components
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/forms/input-components.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor input components

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes Blazor's built-in input components.

## Input components

The Blazor framework provides built-in input components to receive and validate user input. The built-in input components in the following table are supported in an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) with an [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext).

The components in the table are also supported outside of a form in Razor component markup. Inputs are validated when they're changed and when a form is submitted.

| Input component                                                                                       | Rendered as…                                                                                               |
| ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| [InputCheckbox](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputcheckbox)                | `<input type="checkbox">`                                                                                  |
| [InputDate\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputdate-1)             | `<input type="date">`                                                                                      |
| [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile)                        | `<input type="file">`                                                                                      |
| [InputNumber\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputnumber-1)         | `<input type="number">`                                                                                    |
| [InputRadio\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradio-1)           | `<input type="radio">`                                                                                     |
| [InputRadioGroup\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradiogroup-1) | Group of child [InputRadio\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradio-1) |
| [InputSelect\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputselect-1)         | `<select>`                                                                                                 |
| [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext)                        | `<input>`                                                                                                  |
| [InputTextArea](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtextarea)                | `<textarea>`                                                                                               |
| [`Label<TValue>`](#label-component) (.NET 11 or later)                                                | `<label>`                                                                                                  |

For more information on the [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component, see [ASP.NET Core Blazor file uploads](../file-uploads?view=aspnetcore-10.0).

| Input component                                                                               | Rendered as…              |
| --------------------------------------------------------------------------------------------- | ------------------------- |
| [InputCheckbox](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputcheckbox)        | `<input type="checkbox">` |
| [InputDate\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputdate-1)     | `<input type="date">`     |
| [InputNumber\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputnumber-1) | `<input type="number">`   |
| [InputSelect\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputselect-1) | `<select>`                |
| [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext)                | `<input>`                 |
| [InputTextArea](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtextarea)        | `<textarea>`              |

Note

[InputRadio\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradio-1) and [InputRadioGroup\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputradiogroup-1) components are available in .NET 5 or later. For more information, select a .NET 5 or later version of this article.

All of the input components, including [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform), support arbitrary attributes. Any attribute that doesn't match a component parameter is added to the rendered HTML element.

Input components provide default behavior for validating when a field is changed:

* For input components in a form with an [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext), the default validation behavior includes updating the field CSS class to reflect the field's state as valid or invalid with validation styling of the underlying HTML element.
* For controls that don't have an [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext), the default validation reflects the valid or invalid state but doesn't provide validation styling to the underlying HTML element.

Some components include useful parsing logic. For example, [InputDate\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputdate-1) and [InputNumber\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputnumber-1) handle unparseable values gracefully by registering unparseable values as validation errors. Types that can accept null values also support nullability of the target field (for example, `int?` for a nullable integer).

The [InputNumber\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputnumber-1) component supports the [`type="range"` attribute](https://developer.mozilla.org/docs/Web/HTML/Element/input/range), which creates a range input that supports model binding and form validation, typically rendered as a slider or dial control rather than a text box:

```
<InputNumber @bind-Value="..." max="..." min="..." step="..." type="range" />
```

For more information on the [InputFile](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputfile) component, see [ASP.NET Core Blazor file uploads](../file-uploads?view=aspnetcore-10.0).

## Example form

The following `Starship` type, which is used in several of this article's examples and examples in other *Forms* node articles, defines a diverse set of properties with data annotations:

* `Id` is required because it's annotated with the [RequiredAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute). `Id` requires a value of at least one character but no more than 16 characters using the [StringLengthAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute).
* `Description` is optional because it isn't annotated with the [RequiredAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute).
* `Classification` is required.
* The `MaximumAccommodation` property defaults to zero but requires a value from one to 100,000 per its [RangeAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute).
* `IsValidatedDesign` requires that the property have a `true` value, which matches a selected state when the property is bound to a checkbox in the UI (`<input type="checkbox">`).
* `ProductionDate` is a [DateTime](/en-us/dotnet/api/system.datetime) and required.

`Starship.cs`:

```
using System.ComponentModel.DataAnnotations;

namespace BlazorSample;

public class Starship
{
    [Required]
    [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
    public string? Id { get; set; }

    public string? Description { get; set; }

    [Required]
    public string? Classification { get; set; }

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", ErrorMessage = "Approval required.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }
}
```

```
using System.ComponentModel.DataAnnotations;

namespace BlazorSample;

public class Starship
{
    [Required]
    [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
    public string? Id { get; set; }

    public string? Description { get; set; }

    [Required]
    public string? Classification { get; set; }

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", ErrorMessage = "Approval required.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }
}
```

```
using System.ComponentModel.DataAnnotations;

public class Starship
{
    [Required]
    [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
    public string? Id { get; set; }

    public string? Description { get; set; }

    [Required]
    public string? Classification { get; set; }

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", 
        ErrorMessage = "This form disallows unapproved ships.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }
}
```

```
using System.ComponentModel.DataAnnotations;

public class Starship
{
    [Required]
    [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
    public string? Id { get; set; }

    public string? Description { get; set; }

    [Required]
    public string? Classification { get; set; }

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", 
        ErrorMessage = "This form disallows unapproved ships.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }
}
```

```
using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    [Required]
    [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
    public string Id { get; set; }

    public string Description { get; set; }

    [Required]
    public string Classification { get; set; }

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", 
        ErrorMessage = "This form disallows unapproved ships.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }
}
```

```
using System;
using System.ComponentModel.DataAnnotations;

public class Starship
{
    [Required]
    [StringLength(16, ErrorMessage = "Identifier too long (16 character limit).")]
    public string Id { get; set; }

    public string Description { get; set; }

    [Required]
    public string Classification { get; set; }

    [Range(1, 100000, ErrorMessage = "Accommodation invalid (1-100000).")]
    public int MaximumAccommodation { get; set; }

    [Required]
    [Range(typeof(bool), "true", "true", 
        ErrorMessage = "This form disallows unapproved ships.")]
    public bool IsValidatedDesign { get; set; }

    [Required]
    public DateTime ProductionDate { get; set; }
}
```

The following form accepts and validates user input using:

* The properties and validation defined in the preceding `Starship` model.
* Several of Blazor's built-in input components.

When the model property for the ship's classification (`Classification`) is set, the option matching the model is checked. For example, `checked="@(Model!.Classification == "Exploration")"` for the classification of an exploration ship. The reason for explicitly setting the checked option is that the value of a `<select>` element is only present in the browser. If the form is rendered on the server after it's submitted, any state from the client is overridden with state from the server, which doesn't ordinarily mark an option as checked. By setting the checked option from the model property, the classification always reflects the model's state. This preserves the classification selection across form submissions that result in the form rerendering on the server. In situations where the form isn't rerendered on the server, such as when the Interactive Server render mode is applied directly to the component, explicit assignment of the checked option from the model isn't necessary because Blazor preserves the state for the `<select>` element on the client.

`Starship3.razor`:

```
@page "/starship-3"
@inject ILogger<Starship3> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship3">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <label>
            Description (optional): 
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <label>
            Primary Classification: 
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">
                    Select classification ...
                </option>
                <option checked="@(Model!.Classification == "Exploration")" 
                    value="Exploration">
                    Exploration
                </option>
                <option checked="@(Model!.Classification == "Diplomacy")" 
                    value="Diplomacy">
                    Diplomacy
                </option>
                <option checked="@(Model!.Classification == "Defense")" 
                    value="Defense">
                    Defense
                </option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Maximum Accommodation: 
            <InputNumber @bind-Value="Model!.MaximumAccommodation" />
        </label>
    </div>
    <div>
        <label>
            Engineering Approval: 
            <InputCheckbox @bind-Value="Model!.IsValidatedDesign" />
        </label>
    </div>
    <div>
        <label>
            Production Date: 
            <InputDate @bind-Value="Model!.ProductionDate" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        Logger.LogInformation("Id = {Id} Description = {Description} " +
            "Classification = {Classification} MaximumAccommodation = " +
            "{MaximumAccommodation} IsValidatedDesign = " +
            "{IsValidatedDesign} ProductionDate = {ProductionDate}",
            Model?.Id, Model?.Description, Model?.Classification,
            Model?.MaximumAccommodation, Model?.IsValidatedDesign,
            Model?.ProductionDate);
    }
}
```

```
@page "/starship-3"
@inject ILogger<Starship3> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship3">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <label>
            Description (optional): 
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <label>
            Primary Classification: 
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">
                    Select classification ...
                </option>
                <option checked="@(Model!.Classification == "Exploration")" 
                    value="Exploration">
                    Exploration
                </option>
                <option checked="@(Model!.Classification == "Diplomacy")" 
                    value="Diplomacy">
                    Diplomacy
                </option>
                <option checked="@(Model!.Classification == "Defense")" 
                    value="Defense">
                    Defense
                </option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Maximum Accommodation: 
            <InputNumber @bind-Value="Model!.MaximumAccommodation" />
        </label>
    </div>
    <div>
        <label>
            Engineering Approval: 
            <InputCheckbox @bind-Value="Model!.IsValidatedDesign" />
        </label>
    </div>
    <div>
        <label>
            Production Date: 
            <InputDate @bind-Value="Model!.ProductionDate" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        Logger.LogInformation("Id = {Id} Description = {Description} " +
            "Classification = {Classification} MaximumAccommodation = " +
            "{MaximumAccommodation} IsValidatedDesign = " +
            "{IsValidatedDesign} ProductionDate = {ProductionDate}",
            Model?.Id, Model?.Description, Model?.Classification,
            Model?.MaximumAccommodation, Model?.IsValidatedDesign,
            Model?.ProductionDate);
    }
}
```

```
@page "/starship-3"
@inject ILogger<Starship3> Logger

<h1>Starfleet Starship Database</h1>

<h2>New Ship Entry Form</h2>

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <label>
            Description (optional):
            <InputTextArea @bind-Value="Model!.Description" />
        </label>
    </div>
    <div>
        <label>
            Primary Classification:
            <InputSelect @bind-Value="Model!.Classification">
                <option value="">Select classification ...</option>
                <option value="Exploration">Exploration</option>
                <option value="Diplomacy">Diplomacy</option>
                <option value="Defense">Defense</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <label>
            Maximum Accommodation:
            <InputNumber @bind-Value="Model!.MaximumAccommodation" />
        </label>
    </div>
    <div>
        <label>
            Engineering Approval:
            <InputCheckbox @bind-Value="Model!.IsValidatedDesign" />
        </label>
    </div>
    <div>
        <label>
            Production Date:
            <InputDate @bind-Value="Model!.ProductionDate" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private Starship? Model { get; set; }

    protected override void OnInitialized() =>
        Model ??= new() { ProductionDate = DateTime.UtcNow };

    private void Submit()
    {
        Logger.LogInformation("Id = {Id} Description = {Description} " +
            "Classification = {Classification} MaximumAccommodation = " +
            "{MaximumAccommodation} IsValidatedDesign = " +
            "{IsValidatedDesign} ProductionDate = {ProductionDate}", 
            Model?.Id, Model?.Description, Model?.Classification, 
            Model?.MaximumAccommodation, Model?.IsValidatedDesign, 
            Model?.ProductionDate);
    }
}
```

The [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) in the preceding example creates an [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext) based on the assigned `Starship` instance (`Model="..."`) and handles a valid form. The next example demonstrates how to assign an [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext) to a form and validate when the form is submitted.

In the following example:

* A shortened version of the earlier `Starfleet Starship Database` form (`Starship3` component) is used that only accepts a value for the starship's Id. The other `Starship` properties receive valid default values when an instance of the `Starship` type is created.
* The `Submit` method executes when the **`Submit`** button is selected.
* The form is validated by calling [EditContext.Validate](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.validate) in the `Submit` method.
* Logging is executed depending on the validation result.

`Starship4.razor`:

```
@page "/starship-4"
@inject ILogger<Starship4> Logger

<EditForm EditContext="editContext" OnSubmit="Submit" FormName="Starship4">
    <DataAnnotationsValidator />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

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

    protected override void OnInitialized()
    {
        Model ??=
            new()
                {
                    Id = "NCC-1701",
                    Classification = "Exploration",
                    MaximumAccommodation = 150,
                    IsValidatedDesign = true,
                    ProductionDate = new DateTime(2245, 4, 11)
                };
        editContext = new(Model);
    }

    private void Submit()
    {
        if (editContext != null && editContext.Validate())
        {
            Logger.LogInformation("Submit: Form is valid");
        }
        else
        {
            Logger.LogInformation("Submit: Form is INVALID");
        }
    }
}
```

```
@page "/starship-4"
@inject ILogger<Starship4> Logger

<EditForm EditContext="editContext" OnSubmit="Submit" FormName="Starship4">
    <DataAnnotationsValidator />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

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

    protected override void OnInitialized()
    {
        Model ??=
            new()
                {
                    Id = "NCC-1701",
                    Classification = "Exploration",
                    MaximumAccommodation = 150,
                    IsValidatedDesign = true,
                    ProductionDate = new DateTime(2245, 4, 11)
                };
        editContext = new(Model);
    }

    private void Submit()
    {
        if (editContext != null && editContext.Validate())
        {
            Logger.LogInformation("Submit: Form is valid");
        }
        else
        {
            Logger.LogInformation("Submit: Form is INVALID");
        }
    }
}
```

```
@page "/starship-4"
@inject ILogger<Starship4> Logger

<EditForm EditContext="editContext" OnSubmit="Submit">
    <DataAnnotationsValidator />
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    private EditContext? editContext;

    private Starship Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= 
            new()
            {
                Id = "NCC-1701",
                Classification = "Exploration",
                MaximumAccommodation = 150,
                IsValidatedDesign = true,
                ProductionDate = new DateTime(2245, 4, 11)
            };
        editContext = new(Model);
    }

    private async Task Submit()
    {
        if (editContext != null && editContext.Validate())
        {
            Logger.LogInformation("Submit called: Form is valid");

            // await ...
        }
        else
        {
            Logger.LogInformation("Submit called: Form is INVALID");
        }
    }
}
```

Note

Changing the [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext) after it's assigned is **not** supported.

## Multiple option selection with the `InputSelect` component

Binding supports [`multiple`](https://developer.mozilla.org/docs/Web/HTML/Attributes/multiple) option selection with the [InputSelect\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputselect-1) component. The [`@onchange`](../../mvc/views/razor?view=aspnetcore-10.0#onevent) event provides an array of the selected options via [event arguments (`ChangeEventArgs`)](../components/event-handling?view=aspnetcore-10.0#event-arguments). The value must be bound to an array type, which results in the [InputSelect\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputselect-1) component automatically adding the [`multiple` attribute](https://developer.mozilla.org/docs/Web/HTML/Attributes/multiple) to the `<select>` element when the component is rendered.

In the following example, the user must select at least two starship classifications but no more than three classifications.

`Starship5.razor`:

```
@page "/starship-5"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship5> Logger

<h1>Bind Multiple <code>InputSelect</code> Example</h1>

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship5">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Select classifications (Minimum: 2, Maximum: 3):
            <InputSelect @bind-Value="Model!.SelectedClassification">
                <option value="@Classification.Exploration">Exploration</option>
                <option value="@Classification.Diplomacy">Diplomacy</option>
                <option value="@Classification.Defense">Defense</option>
                <option value="@Classification.Research">Research</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@if (Model?.SelectedClassification?.Length > 0)
{
    <div>@string.Join(", ", Model.SelectedClassification)</div>
}

@code {
    private EditContext? editContext;

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

    protected override void OnInitialized()
    {
        Model = new();
        editContext = new(Model);
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    private class Starship
    {
        [Required]
        [MinLength(2, ErrorMessage = "Select at least two classifications.")]
        [MaxLength(3, ErrorMessage = "Select no more than three classifications.")]
        public Classification[]? SelectedClassification { get; set; } =
            [ Classification.None ];
    }

    private enum Classification { None, Exploration, Diplomacy, Defense, Research }
}
```

```
@page "/starship-5"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship5> Logger

<h1>Bind Multiple <code>InputSelect</code> Example</h1>

<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="Starship5">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Select classifications (Minimum: 2, Maximum: 3):
            <InputSelect @bind-Value="Model!.SelectedClassification">
                <option value="@Classification.Exploration">Exploration</option>
                <option value="@Classification.Diplomacy">Diplomacy</option>
                <option value="@Classification.Defense">Defense</option>
                <option value="@Classification.Research">Research</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@if (Model?.SelectedClassification?.Length > 0)
{
    <div>@string.Join(", ", Model.SelectedClassification)</div>
}

@code {
    private EditContext? editContext;

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

    protected override void OnInitialized()
    {
        Model = new();
        editContext = new(Model);
    }

    private void Submit() => Logger.LogInformation("Submit: Processing form");

    private class Starship
    {
        [Required]
        [MinLength(2, ErrorMessage = "Select at least two classifications.")]
        [MaxLength(3, ErrorMessage = "Select no more than three classifications.")]
        public Classification[]? SelectedClassification { get; set; } =
            [ Classification.None ];
    }

    private enum Classification { None, Exploration, Diplomacy, Defense, Research }
}
```

```
@page "/starship-5"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship5> Logger

<h1>Bind Multiple <code>InputSelect</code> Example</h1>

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label>
            Select classifications (Minimum: 2, Maximum: 3):
            <InputSelect @bind-Value="Model!.SelectedClassification">
                <option value="@Classification.Exploration">Exploration</option>
                <option value="@Classification.Diplomacy">Diplomacy</option>
                <option value="@Classification.Defense">Defense</option>
                <option value="@Classification.Research">Research</option>
            </InputSelect>
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@if (Model?.SelectedClassification?.Length > 0)
{
    <div>@string.Join(", ", Model.SelectedClassification)</div>
}

@code {
    private EditContext? editContext;

    private Starship? Model { get; set; }

    protected override void OnInitialized()
    {
        Model ??= new();
        editContext = new(Model);
    }

    private void Submit()
    {
        Logger.LogInformation("Submit called: Processing the form");
    }

    private class Starship
    {
        [Required]
        [MinLength(2, ErrorMessage = "Select at least two classifications.")]
        [MaxLength(3, ErrorMessage = "Select no more than three classifications.")]
        public Classification[]? SelectedClassification { get; set; } =
            new[] { Classification.None };
    }

    private enum Classification { None, Exploration, Diplomacy, Defense, Research }
}
```

For information on how empty strings and `null` values are handled in data binding, see the [Binding `InputSelect` options to C# object `null` values](#binding-inputselect-options-to-c-object-null-values) section.

## Binding `InputSelect` options to C# object `null` values

For information on how empty strings and `null` values are handled in data binding, see [ASP.NET Core Blazor data binding](../components/data-binding?view=aspnetcore-10.0#binding-select-element-options-to-c-object-null-values).

## Display name support

Several built-in components support display names with the [InputBase\<TValue>.DisplayName](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1.displayname) parameter.

In the `Starfleet Starship Database` form (`Starship3` component) of the [Example form](#example-form) section, the production date of a new starship doesn't specify a display name:

```
<label>
    Production Date:
    <InputDate @bind-Value="Model!.ProductionDate" />
</label>
```

If the field contains an invalid date when the form is submitted, the error message doesn't display a friendly name. The field name, "`ProductionDate`" doesn't have a space between "`Production`" and "`Date`" when it appears in the validation summary:

> The ProductionDate field must be a date.

Set the [DisplayName](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1.displayname) property to a friendly name with a space between the words "`Production`" and "`Date`":

```
<label>
    Production Date:
    <InputDate @bind-Value="Model!.ProductionDate" 
        DisplayName="Production Date" />
</label>
```

The validation summary displays the friendly name when the field's value is invalid:

> The Production Date field must be a date.

The `DisplayName` component can be used to display property names from metadata attributes

```
[Required, DisplayName("Production Date")]
public DateTime ProductionDate { get; set; }
```

The [`[Display]` attribute](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) on the model class property is supported:

```
[Required, Display(Name = "Production Date")]
public DateTime ProductionDate { get; set; }
```

Between the two approaches, the `[Display]` attribute is recommended, which makes additional properties available. The `[Display]` attribute also enables assigning a resource type for localization. When both attributes are present, `[Display]` takes precedence over `[DisplayName]`. If neither attribute is present, the component falls back to the property name.

Use the `DisplayName` component in labels or table headers:

```
<label>
    <DisplayName For="@(() => Model!.ProductionDate)" />
    <InputDate @bind-Value="Model!.ProductionDate" />
</label>
```

## `Label` component

The `Label` component renders a `<label>` element that automatically extracts the display name from a model property using `[Display]` or `[DisplayName]` attributes. This simplifies form creation by eliminating the need to manually specify label text.

### Nested pattern

The nested pattern wraps the input component inside the label:

```
<Label For="() => Model!.ProductionDate">
    <InputDate @bind-Value="Model!.ProductionDate" />
</Label>
```

This renders:

```
<label>Production Date<input type="date" ... /></label>
```

### Non-nested pattern

For accessibility requirements or styling flexibility, use the non-nested pattern where the label's `for` attribute references the input's `id`:

```
<Label For="() => Model!.ProductionDate" />
<InputDate @bind-Value="Model!.ProductionDate" />
```

This renders:

```
<label for="Model_ProductionDate">Production Date</label>
<input id="Model_ProductionDate" type="date" ... />
```

The `id` attribute is automatically sanitized to create a valid HTML id (dots are replaced with underscores to avoid CSS selector conflicts).

Input components automatically generate an `id` attribute based on the bound expression. If an explicit `id` is provided, it takes precedence:

```
<Label For="() => Model!.ProductionDate" for="prod-date" />
<InputDate @bind-Value="Model!.ProductionDate" id="prod-date" />
```

## Error message template support

[InputDate\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputdate-1) and [InputNumber\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputnumber-1) support error message templates:

* [InputDate\<TValue>.ParsingErrorMessage](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputdate-1.parsingerrormessage)
* [InputNumber\<TValue>.ParsingErrorMessage](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputnumber-1.parsingerrormessage)

In the `Starfleet Starship Database` form (`Starship3` component) of the [Example form](#example-form) section with a [friendly display name](#display-name-support) assigned, the `Production Date` field produces an error message using the following default error message template:

```
The {0} field must be a date.
```

The position of the `{0}` placeholder is where the value of the [DisplayName](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1.displayname) property appears when the error is displayed to the user.

```
<label>
    Production Date:
    <InputDate @bind-Value="Model!.ProductionDate" 
        DisplayName="Production Date" />
</label>
```

> The Production Date field must be a date.

Assign a custom template to [ParsingErrorMessage](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputdate-1.parsingerrormessage) to provide a custom message:

```
<label>
    Production Date:
    <InputDate @bind-Value="Model!.ProductionDate" 
        DisplayName="Production Date" 
        ParsingErrorMessage="The {0} field has an incorrect date value." />
</label>
```

> The Production Date field has an incorrect date value.

In the `Starfleet Starship Database` form (`Starship3` component) of the [Example form](#example-form) section uses a default error message template:

```
The {0} field must be a date.
```

The position of the `{0}` placeholder is where the value of the [DisplayName](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1.displayname) property appears when the error is displayed to the user.

```
<label>
    Production Date:
    <InputDate @bind-Value="Model!.ProductionDate" />
</label>
```

> The ProductionDate field must be a date.

Assign a custom template to [ParsingErrorMessage](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputdate-1.parsingerrormessage) to provide a custom message:

```
<label>
    Production Date:
    <InputDate @bind-Value="Model!.ProductionDate" 
        ParsingErrorMessage="The {0} field has an incorrect date value." />
</label>
```

> The ProductionDate field has an incorrect date value.

## `InputHidden` component to handle hidden input fields in forms

The [`InputHidden` component](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputhidden) provides a hidden input field for storing string values.

In the following example, a hidden input field is created for the form's `Parameter` property. When the form is submitted, the value of the hidden field is displayed:

```
<EditForm Model="Parameter" OnValidSubmit="Submit" FormName="InputHidden Example">
    <InputHidden id="hidden" @bind-Value="Parameter" />
    <button type="submit">Submit</button>
</EditForm>

@if (submitted)
{
    <p>Hello @Parameter!</p>
}

@code {
    private bool submitted;

    [SupplyParameterFromForm] 
    public string Parameter { get; set; } = "stranger";

    private void Submit() => submitted = true;
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-25

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/element-component-model-relationships
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/element-component-model-relationships.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Retain element, component, and model relationships in ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to use the `@key` directive attribute to retain element, component, and model relationships when rendering and the elements or components subsequently change.

## Use of the `@key` directive attribute

When rendering a list of elements or components and the elements or components subsequently change, Blazor must decide which of the previous elements or components are retained and how model objects should map to them. Normally, this process is automatic and sufficient for general rendering, but there are often cases where controlling the process using the [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) directive attribute is required.

Consider the following example that demonstrates a collection mapping problem that's solved by using [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key).

For the following components:

* The `Details` component receives data (`Data`) from the parent component, which is displayed in an `<input>` element. Any given displayed `<input>` element can receive the focus of the page from the user when they select one of the `<input>` elements.
* The parent component creates a list of person objects for display using the `Details` component. Every three seconds, a new person is added to the collection.

This demonstration allows you to:

* Select an `<input>` from among several rendered `Details` components.
* Study the behavior of the page's focus as the people collection automatically grows.

`Details.razor`:

```
<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
```

```
<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
```

```
<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
```

```
<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
```

```
<input value="@Data" />

@code {
    [Parameter]
    public string Data { get; set; }
}
```

```
<input value="@Data" />

@code {
    [Parameter]
    public string Data { get; set; }
}
```

In the following parent component, each iteration of adding a person in `OnTimerCallback` results in Blazor rebuilding the entire collection. The page's focus remains on the *same index* position of `<input>` elements, so the focus shifts each time a person is added. *Shifting the focus away from what the user selected isn't desirable behavior.* After demonstrating the poor behavior with the following component, the [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) directive attribute is used to improve the user's experience.

`People.razor`:

```
@page "/people"
@using System.Timers
@implements IDisposable

<PageTitle>People</PageTitle>

<h1>People Example</h1>

@foreach (var person in people)
{
    <Details Data="@person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();

    public class Person
    {
        public string? Data { get; set; }
    }
}
```

`People.razor`:

```
@page "/people"
@using System.Timers
@implements IDisposable

<PageTitle>People</PageTitle>

<h1>People Example</h1>

@foreach (var person in people)
{
    <Details Data="@person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();

    public class Person
    {
        public string? Data { get; set; }
    }
}
```

`PeopleExample.razor`:

```
@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="@person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();

    public class Person
    {
        public string? Data { get; set; }
    }
}
```

`PeopleExample.razor`:

```
@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="@person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();

    public class Person
    {
        public string? Data { get; set; }
    }
}
```

`PeopleExample.razor`:

```
@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="@person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();

    public class Person
    {
        public string Data { get; set; }
    }
}
```

`PeopleExample.razor`:

```
@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="@person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new List<Person>()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();

    public class Person
    {
        public string Data { get; set; }
    }
}
```

The contents of the `people` collection changes with inserted, deleted, or re-ordered entries. Rerendering can lead to visible behavior differences. For example, each time a person is inserted into the `people` collection, the user's focus is lost.

The mapping process of elements or components to a collection can be controlled with the [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) directive attribute. Use of [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) guarantees the preservation of elements or components based on the key's value. If the `Details` component in the preceding example is keyed on the `person` item, Blazor ignores rerendering `Details` components that haven't changed.

To modify the parent component to use the [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) directive attribute with the `people` collection, update the `<Details>` element to the following:

```
<Details @key="person" Data="@person.Data" />
```

When the `people` collection changes, the association between `Details` instances and `person` instances is retained. When a `Person` is inserted at the beginning of the collection, one new `Details` instance is inserted at that corresponding position. Other instances are left unchanged. Therefore, the user's focus isn't lost as people are added to the collection.

Other collection updates exhibit the same behavior when the [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) directive attribute is used:

* If an instance is deleted from the collection, only the corresponding component instance is removed from the UI. Other instances are left unchanged.
* If collection entries are re-ordered, the corresponding component instances are preserved and re-ordered in the UI.

Important

Keys are local to each container element or component. Keys aren't compared globally across the document.

## When to use `@key`

Typically, it makes sense to use [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) whenever a list is rendered (for example, in a [`foreach`](/en-us/dotnet/csharp/language-reference/keywords/foreach-in) block) and a suitable value exists to define the [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key).

You can also use [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) to preserve an element or component subtree when an object doesn't change, as the following examples show.

Example 1:

```
<li @key="person">
    <input value="@person.Data" />
</li>
```

Example 2:

```
<div @key="person">
    @* other HTML elements *@
</div>
```

If an `person` instance changes, the [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) attribute directive forces Blazor to:

* Discard the entire `<li>` or `<div>` and their descendants.
* Rebuild the subtree within the UI with new elements and components.

This is useful to guarantee that no UI state is preserved when the collection changes within a subtree.

## Scope of `@key`

The [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) attribute directive is scoped to its own siblings within its parent.

Consider the following example. The `first` and `second` keys are compared against each other within the same scope of the outer `<div>` element:

```
<div>
    <div @key="first">...</div>
    <div @key="second">...</div>
</div>
```

The following example demonstrates `first` and `second` keys in their own scopes, unrelated to each other and without influence on each other. Each [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) scope only applies to its parent `<div>` element, not across the parent `<div>` elements:

```
<div>
    <div @key="first">...</div>
</div>
<div>
    <div @key="second">...</div>
</div>
```

For the `Details` component shown earlier, the following examples render `person` data within the same [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) scope and demonstrate typical use cases for [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key):

```
<div>
    @foreach (var person in people)
    {
        <Details @key="person" Data="@person.Data" />
    }
</div>
```

```
@foreach (var person in people)
{
    <div @key="person">
        <Details Data="@person.Data" />
    </div>
}
```

```
<ol>
    @foreach (var person in people)
    {
        <li @key="person">
            <Details Data="@person.Data" />
        </li>
    }
</ol>
```

The following examples only scope [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) to the `<div>` or `<li>` element that surrounds each `Details` component instance. Therefore, `person` data for each member of the `people` collection is **not** keyed on each `person` instance across the rendered `Details` components. Avoid the following patterns when using [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key):

```
@foreach (var person in people)
{
    <div>
        <Details @key="person" Data="@person.Data" />
    </div>
}
```

```
<ol>
    @foreach (var person in people)
    {
        <li>
            <Details @key="person" Data="@person.Data" />
        </li>
    }
</ol>
```

## When not to use `@key`

There's a performance cost when rendering with [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key). The performance cost isn't large, but only specify [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) if preserving the element or component benefits the app.

Even if [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) isn't used, Blazor preserves child element and component instances as much as possible. The only advantage to using [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) is control over *how* model instances are mapped to the preserved component instances, instead of Blazor selecting the mapping.

## Values to use for `@key`

Generally, it makes sense to supply one of the following values for [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key):

* Model object instances. For example, the `Person` instance (`person`) was used in the earlier example. This ensures preservation based on object reference equality.
* Unique identifiers. For example, unique identifiers can be based on primary key values of type `int`, `string`, or `Guid`.

Ensure that values used for [`@key`](../../mvc/views/razor?view=aspnetcore-10.0#key) don't clash. If clashing values are detected within the same parent element, Blazor throws an exception because it can't deterministically map old elements or components to new elements or components. Only use distinct values, such as object instances or primary key values.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/proxy-load-balancer.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure ASP.NET Core to work with proxy servers and load balancers

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Chris Ross](https://github.com/Tratcher)

In the recommended configuration for ASP.NET Core, the app is hosted using [ASP.NET Core Module (ANCM) for IIS](aspnet-core-module?view=aspnetcore-10.0), Nginx, or Apache. Proxy servers, load balancers, and other network appliances often obscure information about the request before it reaches the app:

* When HTTPS requests are proxied over HTTP, the original scheme (HTTPS) is lost and must be forwarded in a header.
* Because an app receives a request from the proxy and not its true source on the Internet or corporate network, the originating client IP address must also be forwarded in a header.

This information might be important in request processing, for example in redirects, authentication, link generation, policy evaluation, and client geolocation.

Apps intended to run on web farm should read [Host ASP.NET Core in a web farm](web-farm?view=aspnetcore-10.0).

## Forwarded headers

By convention, proxies forward information in HTTP headers.

| Header                                                                                                       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`X-Forwarded-For`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-For) (XFF)     | Holds information about the client that initiated the request and subsequent proxies in a chain of proxies. This parameter might contain IP addresses and, optionally, port numbers. In a chain of proxy servers, the first parameter indicates the client where the request was first made. Subsequent proxy identifiers follow. The last proxy in the chain isn't in the list of parameters. The last proxy's IP address, and optionally a port number, are available as the remote IP address at the transport layer. |
| [`X-Forwarded-Proto`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto) (XFP) | The value of the originating scheme, HTTP or HTTPS. The value might also be a list of schemes if the request has traversed multiple proxies.                                                                                                                                                                                                                                                                                                                                                                             |
| [`X-Forwarded-Host`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host) (XFH)   | The original value of the Host header field. Usually, proxies don't modify the Host header. See [Microsoft Security Advisory CVE-2018-0787](https://github.com/aspnet/Announcements/issues/295) for information on an elevation-of-privileges vulnerability that affects systems where the proxy doesn't validate or restrict Host headers to known good values.                                                                                                                                                         |
| `X-Forwarded-Prefix`                                                                                         | The original base path requested by the client. This header can be useful for applications to correctly generate URLs, redirects, or links back to the client.                                                                                                                                                                                                                                                                                                                                                           |

The [forwarded headers middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs), [ForwardedHeadersMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersmiddleware), reads these headers and fills in the associated fields on [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext).

The middleware updates:

* [`HttpContext.Connection.RemoteIpAddress`](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.remoteipaddress#microsoft-aspnetcore-http-connectioninfo-remoteipaddress): Set using the `X-Forwarded-For` header value. Additional settings influence how the middleware sets `RemoteIpAddress`. For details, see the [forwarded headers middleware options](#forwarded-headers-middleware-options). The consumed values are removed from `X-Forwarded-For`, and the old value of [`HttpContext.Connection.RemoteIpAddress`](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.remoteipaddress#microsoft-aspnetcore-http-connectioninfo-remoteipaddress) is persisted in `X-Original-For`. Note: This process might be repeated several times if there are multiple values in `X-Forwarded-For/Proto/Host/Prefix`, resulting in several values moved to `X-Original-*`, including the original `RemoteIpAddress/Host/Scheme/PathBase`.
* [`HttpContext.Request.Scheme`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.scheme#microsoft-aspnetcore-http-httprequest-scheme): Set using the [`X-Forwarded-Proto`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto) header value. The consumed value is removed from `X-Forwarded-Proto`, and the old value of [`HttpContext.Request.Scheme`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.scheme#microsoft-aspnetcore-http-httprequest-scheme) is persisted in `X-Original-Proto`.
* [`HttpContext.Request.Host`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.host#microsoft-aspnetcore-http-httprequest-host): Set using the `X-Forwarded-Host` header value. The consumed value is removed from `X-Forwarded-Host`, and the old value of [`HttpContext.Request.Host`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.host#microsoft-aspnetcore-http-httprequest-host) is persisted in `X-Original-Host`.
* [`HttpContext.Request.PathBase`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase#microsoft-aspnetcore-http-httprequest-pathbase): Set using the `X-Forwarded-Prefix` header value. The consumed value is removed from `X-Forwarded-Prefix`, and the old value of [`HttpContext.Request.PathBase`](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase#microsoft-aspnetcore-http-httprequest-pathbase) is persisted in `X-Original-Prefix`.

For more information on the preceding, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/21615).

Forwarded headers middleware [default settings](#forwarded-headers-middleware-options) can be configured. For the default settings:

* There's only ***one proxy*** between the app and the source of the requests.
* Only loopback addresses are configured for known proxies and known networks.
* The forwarded headers are named [`X-Forwarded-For`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-For), [`X-Forwarded-Proto`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto), [`X-Forwarded-Host`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host) and `X-Forwarded-Prefix`.
* The `ForwardedHeaders` value is `ForwardedHeaders.None`. The desired forwarders must be set here to enable the middleware.

Not all network appliances add the `X-Forwarded-For` and `X-Forwarded-Proto` headers without additional configuration. Consult your appliance manufacturer's guidance if proxied requests don't contain these headers when they reach the app. If the appliance uses different header names than `X-Forwarded-For` and `X-Forwarded-Proto`, set the [ForwardedForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedforheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedforheadername) and [ForwardedProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedprotoheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedprotoheadername) options to match the header names used by the appliance. For more information, see [forwarded headers middleware options](#forwarded-headers-middleware-options) and [Configuration for a proxy that uses different header names](#configuration-for-a-proxy-that-uses-different-header-names).

## IIS/IIS Express and ASP.NET Core Module

[IIS integration middleware](iis/?view=aspnetcore-10.0#enable-the-iisintegration-components) enables forwarded headers middleware by default when the app is hosted [out-of-process](iis/?view=aspnetcore-10.0#out-of-process-hosting-model) behind IIS and the [ASP.NET Core Module (ANCM) for IIS](aspnet-core-module?view=aspnetcore-10.0). Forwarded headers middleware is activated to run first in the middleware pipeline with a restricted configuration specific to the ASP.NET Core Module. The restricted configuration is due to trust concerns with forwarded headers, for example, [IP spoofing](https://www.iplocation.net/ip-spoofing). The middleware is configured to forward the [`X-Forwarded-For`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-For) and [`X-Forwarded-Proto`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto) headers and is restricted to a single localhost proxy. If additional configuration is required, see the [forwarded headers middleware options](#forwarded-headers-middleware-options).

## Other proxy server and load balancer scenarios

Outside of using [IIS Integration](iis/?view=aspnetcore-10.0#enable-the-iisintegration-components) when hosting [out-of-process](iis/?view=aspnetcore-10.0#out-of-process-hosting-model), [forwarded headers middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs) isn't enabled by default. Forwarded headers middleware must be enabled for an app to process forwarded headers with [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders). After enabling the middleware if no [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) are specified to the middleware, the default [ForwardedHeadersOptions.ForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedheaders#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedheaders) are [ForwardedHeaders.None](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheaders).

Configure the middleware with [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) to forward the [`X-Forwarded-For`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-For) and [`X-Forwarded-Proto`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto) headers.

### Forwarded headers middleware order

[Forwarded headers middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs) should run before other middleware. This ordering ensures that the middleware relying on forwarded headers information can consume the header values for processing. Forwarded headers middleware can run after diagnostics and error handling, but it must be run before calling [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts):

```
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseForwardedHeaders();
    app.UseHsts();
}
else
{
    app.UseDeveloperExceptionPage();
    app.UseForwardedHeaders();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

Alternatively, call `UseForwardedHeaders` before diagnostics:

```
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

var app = builder.Build();

app.UseForwardedHeaders();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

Note

If no [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) are specified or applied directly to the extension method with [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders), the default headers to forward are [ForwardedHeaders.None](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheaders). The [ForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedheaders#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedheaders) property must be configured with the headers to forward.

## Nginx configuration

To forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers, see [Host ASP.NET Core on Linux with Nginx](linux-nginx?view=aspnetcore-10.0#configure-nginx).

## Apache configuration

`X-Forwarded-For` is added automatically. For more information, see [Apache Module mod\_proxy: Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers).

## Forwarded headers middleware options

[ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) control the behavior of the [forwarded headers middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs). The following example changes the default values:

* Limits the number of entries in the forwarded headers to `2`.
* Adds a known proxy address of `127.0.10.1`.
* Changes the forwarded header name from the default `X-Forwarded-For` to `X-Forwarded-For-My-Custom-Header-Name`.

```
using System.Net;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardLimit = 2;
    options.KnownProxies.Add(IPAddress.Parse("127.0.10.1"));
    options.ForwardedForHeaderName = "X-Forwarded-For-My-Custom-Header-Name";
});

var app = builder.Build();

app.UseForwardedHeaders();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

| Option                                                                                                                                                                                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [AllowedHosts](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.allowedhosts#microsoft-aspnetcore-builder-forwardedheadersoptions-allowedhosts)                                     | Restricts hosts by the `X-Forwarded-Host` header to the values provided.- Values are compared using ordinal-ignore-case.
- Port numbers must be excluded.
- If the list is empty, all hosts are allowed.
- A top-level wildcard `*` allows all non-empty hosts.
- Subdomain wildcards are permitted but don't match the root domain. For example, `*.contoso.com` matches the subdomain `foo.contoso.com` but not the root domain `contoso.com`.
- Unicode host names are allowed but are converted to [Punycode](https://datatracker.ietf.org/doc/html/rfc3492) for matching.
- [IPv6 addresses](https://datatracker.ietf.org/doc/html/rfc4291) must include bounding brackets and be in [conventional form](https://datatracker.ietf.org/doc/html/rfc4291#section-2.2) (for example, `[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]`). IPv6 addresses aren't special-cased to check for logical equality between different formats, and no canonicalization is performed.
- Failure to restrict the allowed hosts might allow a cyberattacker to spoof links generated by the service.The default value is an empty `IList<string>`. |
| [ForwardedForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedforheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedforheadername)       | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XForwardedForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xforwardedforheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xforwardedforheadername). This option is used when the proxy/forwarder doesn't use the `X-Forwarded-For` header but uses some other header to forward the information. The default is `X-Forwarded-For`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| [ForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedheaders#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedheaders)                         | Identifies which forwarders should be processed. See the [ForwardedHeaders Enum](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheaders) for the list of fields that apply. Typical values assigned to this property are `ForwardedHeaders.XForwardedFor \| ForwardedHeaders.XForwardedProto`. The default value is [ForwardedHeaders.None](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheaders).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| [ForwardedHostHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedhostheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedhostheadername)    | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XForwardedHostHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xforwardedhostheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xforwardedhostheadername). This option is used when the proxy/forwarder doesn't use the `X-Forwarded-Host` header but uses some other header to forward the information. The default is `X-Forwarded-Host`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| [ForwardedProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedprotoheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedprotoheadername) | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XForwardedProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xforwardedprotoheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xforwardedprotoheadername). This option is used when the proxy/forwarder doesn't use the `X-Forwarded-Proto` header but uses some other header to forward the information. The default is `X-Forwarded-Proto`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| [ForwardLimit](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardlimit#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardlimit)                                     | Limits the number of entries in the headers that are processed. Set to `null` to disable the limit, but this should only be done if `KnownProxies` or `KnownNetworks` are configured. Setting a non-`null` value is a precaution (but not a guarantee) to protect against misconfigured proxies and malicious requests arriving from side-channels on the network. Forwarded headers middleware processes headers in reverse order from right to left. If the default value (`1`) is used, only the rightmost value from the headers is processed unless the value of `ForwardLimit` is increased. The default is `1`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| [KnownNetworks](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownnetworks#microsoft-aspnetcore-builder-forwardedheadersoptions-knownnetworks)                                  | Address ranges of known networks to accept forwarded headers from. Provide IP ranges using Classless Interdomain Routing (CIDR) notation. If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format (for example, `10.0.0.1` in IPv4 represented in IPv6 as `::ffff:10.0.0.1`). See [IPAddress.MapToIPv6](/en-us/dotnet/api/system.net.ipaddress.maptoipv6). Determine if this format is required by looking at the [HttpContext.Connection.RemoteIpAddress](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.remoteipaddress). The default is an `IList`<[IPNetwork](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.ipnetwork)> containing a single entry for `new IPNetwork(IPAddress.Loopback, 8)`.                                                                                                                                                                                                                                                                                                                                                                               |
| [KnownProxies](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownproxies#microsoft-aspnetcore-builder-forwardedheadersoptions-knownproxies)                                     | Addresses of known proxies to accept forwarded headers from. Use `KnownProxies` to specify exact IP address matches. If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format (for example, `10.0.0.1` in IPv4 represented in IPv6 as `::ffff:10.0.0.1`). See [IPAddress.MapToIPv6](/en-us/dotnet/api/system.net.ipaddress.maptoipv6). Determine if this format is required by looking at the [HttpContext.Connection.RemoteIpAddress](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.remoteipaddress). The default is an `IList`<[IPAddress](/en-us/dotnet/api/system.net.ipaddress)> containing a single entry for `IPAddress.IPv6Loopback`.                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| [OriginalForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.originalforheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-originalforheadername)          | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XOriginalForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xoriginalforheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xoriginalforheadername). The default is `X-Original-For`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| [OriginalHostHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.originalhostheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-originalhostheadername)       | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XOriginalHostHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xoriginalhostheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xoriginalhostheadername). The default is `X-Original-Host`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| [OriginalProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.originalprotoheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-originalprotoheadername)    | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XOriginalProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xoriginalprotoheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xoriginalprotoheadername). The default is `X-Original-Proto`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| [RequireHeaderSymmetry](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.requireheadersymmetry#microsoft-aspnetcore-builder-forwardedheadersoptions-requireheadersymmetry)          | Require the number of header values to be in sync between the [ForwardedHeadersOptions.ForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedheaders#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedheaders) being processed. The default in ASP.NET Core 1.x is `true`. The default in ASP.NET Core 2.0 or later is `false`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

## Scenarios and use cases

### When it isn't possible to add forwarded headers and all requests are secure

In some cases, it might not be possible to add forwarded headers to the requests proxied to the app. If the proxy is enforcing that all public external requests are HTTPS, the scheme can be manually set before using any type of middleware:

```
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

var app = builder.Build();

app.Use((context, next) =>
{
    context.Request.Scheme = "https";
    return next(context);
});

app.UseForwardedHeaders();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

This code can be disabled with an environment variable or other configuration setting in a development or staging environment:

```
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

var app = builder.Build();

if (!app.Environment.IsProduction())
{
    app.Use((context, next) =>
    {
        context.Request.Scheme = "https";
        return next(context);
    });
}

app.UseForwardedHeaders();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

### Work with path base and proxies that change the request path

Some proxies pass the path intact but with an app base path that should be removed so that routing works properly. [UsePathBaseExtensions.UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase) middleware splits the path into [HttpRequest.Path](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.path#microsoft-aspnetcore-http-httprequest-path) and the app base path into [HttpRequest.PathBase](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase#microsoft-aspnetcore-http-httprequest-pathbase).

If `/foo` is the app base path for a proxy path passed as `/foo/api/1`, the middleware sets `Request.PathBase` to `/foo` and `Request.Path` to `/api/1` with the following command:

```
app.UsePathBase("/foo");
// ...
app.UseRouting();
```

Note

When using [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../migration/50-to-60?view=aspnetcore-10.0#new-hosting-model)), [`app.UseRouting`](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) must be called after `UsePathBase` so that the routing middleware can observe the modified path before matching routes. Otherwise, routes are matched before `UsePathBase` rewrites the path as described in [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0#middleware-order) and [Routing in ASP.NET Core](../fundamentals/routing?view=aspnetcore-10.0).

The original path and path base are reapplied when the middleware is called again in reverse. For more information on middleware order processing, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0).

If the proxy trims the path (for example, forwarding `/foo/api/1` to `/api/1`), fix redirects and links by setting the request's [PathBase](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase#microsoft-aspnetcore-http-httprequest-pathbase) property:

```
app.Use((context, next) =>
{
    context.Request.PathBase = new PathString("/foo");
    return next(context);
});
```

If the proxy is adding path data, discard part of the path to fix redirects and links by using [StartsWithSegments](/en-us/dotnet/api/microsoft.aspnetcore.http.pathstring.startswithsegments) and assigning to the [Path](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.path#microsoft-aspnetcore-http-httprequest-path) property:

```
app.Use((context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/foo", out var remainder))
    {
        context.Request.Path = remainder;
    }

    return next(context);
});
```

### Configuration for a proxy that uses different header names

If the proxy doesn't use headers named `X-Forwarded-For` and `X-Forwarded-Proto` to forward the proxy address/port and originating scheme information, set the [ForwardedForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedforheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedforheadername) and [ForwardedProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedprotoheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedprotoheadername) options to match the header names used by the proxy:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedForHeaderName = "HeaderNamUsedByProxy_X-Forwarded-For_Header";
    options.ForwardedProtoHeaderName = "HeaderNamUsedByProxy_X-Forwarded-Proto_Header";
});

var app = builder.Build();

app.UseForwardedHeaders();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

## Forward the scheme for Linux and non-IIS reverse proxies

Apps that call [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) and [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts) put a site into an infinite loop if deployed to an Azure Linux App Service, Azure Linux virtual machine (VM), or behind any other reverse proxy besides IIS. The reverse proxy terminates TLS, and Kestrel isn't made aware of the correct request scheme. OAuth and OIDC also fail in this configuration because they generate incorrect redirects. [UseIISIntegration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiisintegration) adds and configures forwarded headers middleware when running behind IIS, but there's no matching automatic configuration for Linux (Apache or Nginx integration).

To forward the scheme from the proxy in non-IIS scenarios, enable the forwarded headers middleware by setting `ASPNETCORE_FORWARDEDHEADERS_ENABLED` to `true`. Warning: This flag uses settings designed for cloud environments and doesn't enable features such as the [`KnownProxies option`](#forwarded-headers-middleware-options) to restrict which IPs forwarders are accepted from.

## Certificate forwarding

### Azure

To configure Azure App Service for certificate forwarding, see [Configure TLS mutual authentication for Azure App Service](/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth). The following guidance pertains to configuring the ASP.NET Core app.

* Configure [certificate-forwarding middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs) to specify the header name that Azure uses. Add the following code to configure the header from which the middleware builds a certificate.
* Call [UseCertificateForwarding](/en-us/dotnet/api/microsoft.aspnetcore.builder.certificateforwardingbuilderextensions.usecertificateforwarding) before the call to [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication).

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddCertificateForwarding(options =>
    options.CertificateHeader = "X-ARR-ClientCert");

var app = builder.Build();

app.UseCertificateForwarding();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();
app.UseAuthentication();

app.MapRazorPages();

app.Run();
```

### Other web proxies

If a proxy is used that isn't IIS or Azure App Service's Application Request Routing (ARR), configure the proxy to forward the certificate that it received in an HTTP header.

* Configure [certificate-forwarding middleware](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs) to specify the header name. Add the following code to configure the header from which the middleware builds a certificate.
* Call [UseCertificateForwarding](/en-us/dotnet/api/microsoft.aspnetcore.builder.certificateforwardingbuilderextensions.usecertificateforwarding) before the call to [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication).

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddCertificateForwarding(options =>
    options.CertificateHeader = "YOUR_CERTIFICATE_HEADER_NAME");

var app = builder.Build();

app.UseCertificateForwarding();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();
app.UseAuthentication();

app.MapRazorPages();

app.Run();
```

If the proxy isn't base64-encoding the certificate, as is the case with Nginx, set the `HeaderConverter` option. Consider the following example:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME";
    options.HeaderConverter = (headerValue) =>
    {
        // Conversion logic to create an X509Certificate2.
        var clientCertificate = ConversionLogic.CreateAnX509Certificate2();
        return clientCertificate;
    };
});

var app = builder.Build();

app.UseCertificateForwarding();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();
app.UseAuthentication();

app.MapRazorPages();

app.Run();
```

## Troubleshoot

When headers aren't forwarded as expected, enable `debug` level [logging](../fundamentals/logging/?view=aspnetcore-10.0) and HTTP request logging. [UseHttpLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usehttplogging) must be called after [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders):

```
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddHttpLogging(options =>
{
    options.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders;
});

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

var app = builder.Build();

app.UseForwardedHeaders();
app.UseHttpLogging();

app.Use(async (context, next) =>
{
    // Connection: RemoteIp
    app.Logger.LogInformation("Request RemoteIp: {RemoteIpAddress}",
        context.Connection.RemoteIpAddress);

    await next(context);
});

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

If there are multiple values in a given header, forwarded headers middleware processes headers in reverse order from right to left. The default `ForwardLimit` is `1` (one), so only the rightmost value from the headers is processed unless the value of `ForwardLimit` is increased.

The request's original remote IP must match an entry in the [KnownProxies](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownproxies#microsoft-aspnetcore-builder-forwardedheadersoptions-knownproxies) or [KnownNetworks](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownnetworks#microsoft-aspnetcore-builder-forwardedheadersoptions-knownnetworks) lists before forwarded headers are processed. This limits header spoofing by not accepting forwarders from untrusted proxies. When an unknown proxy is detected, logging indicates the address of the proxy:

```
September 20th 2018, 15:49:44.168 Unknown proxy: 10.0.0.100:54321
```

In the preceding example, 10.0.0.100 is a proxy server. If the server is a trusted proxy, add the server's IP address to `KnownProxies`, or add a trusted network to `KnownNetworks`. For more information, see the [forwarded headers middleware options](#forwarded-headers-middleware-options) section.

```
using Microsoft.AspNetCore.HttpOverrides;
using System.Net;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseForwardedHeaders();
    app.UseHsts();
}
else
{
    app.UseDeveloperExceptionPage();
    app.UseForwardedHeaders();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

To display the logs, add `"Microsoft.AspNetCore.HttpLogging": "Information"` to the `appsettings.Development.json` file:

```
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.AspNetCore.HttpLogging": "Information"
    }
  }
}
```

Important

Only allow trusted proxies and networks to forward headers. Otherwise, [IP spoofing](https://www.iplocation.net/ip-spoofing) attacks are possible.

## Additional resources

* [Host ASP.NET Core in a web farm](web-farm?view=aspnetcore-10.0)
* [Microsoft Security Advisory CVE-2018-0787: ASP.NET Core Elevation Of Privilege Vulnerability](https://github.com/aspnet/Announcements/issues/295)
* [YARP: Yet Another Reverse Proxy](https://dotnet.github.io/yarp/)

In the recommended configuration for ASP.NET Core, the app is hosted using IIS/ASP.NET Core Module, Nginx, or Apache. Proxy servers, load balancers, and other network appliances often obscure information about the request before it reaches the app:

* When HTTPS requests are proxied over HTTP, the original scheme (HTTPS) is lost and must be forwarded in a header.
* Because an app receives a request from the proxy and not its true source on the Internet or corporate network, the originating client IP address must also be forwarded in a header.

This information might be important in request processing, for example in redirects, authentication, link generation, policy evaluation, and client geolocation.

## Forwarded headers

By convention, proxies forward information in HTTP headers.

| Header            | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| X-Forwarded-For   | Holds information about the client that initiated the request and subsequent proxies in a chain of proxies. This parameter might contain IP addresses (and, optionally, port numbers). In a chain of proxy servers, the first parameter indicates the client where the request was first made. Subsequent proxy identifiers follow. The last proxy in the chain isn't in the list of parameters. The last proxy's IP address, and optionally a port number, are available as the remote IP address at the transport layer. |
| X-Forwarded-Proto | The value of the originating scheme (HTTP/HTTPS). The value might also be a list of schemes if the request has traversed multiple proxies.                                                                                                                                                                                                                                                                                                                                                                                 |
| X-Forwarded-Host  | The original value of the Host header field. Usually, proxies don't modify the Host header. See [Microsoft Security Advisory CVE-2018-0787](https://github.com/aspnet/Announcements/issues/295) for information on an elevation-of-privileges vulnerability that affects systems where the proxy doesn't validate or restrict Host headers to known good values.                                                                                                                                                           |

The forwarded headers middleware ([ForwardedHeadersMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersmiddleware)), reads these headers and fills in the associated fields on [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext).

The middleware updates:

* [HttpContext.Connection.RemoteIpAddress](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.remoteipaddress#microsoft-aspnetcore-http-connectioninfo-remoteipaddress): Set using the `X-Forwarded-For` header value. Additional settings influence how the middleware sets `RemoteIpAddress`. For details, see the [forwarded headers middleware options](#forwarded-headers-middleware-options). The consumed values are removed from `X-Forwarded-For`, and the old values are persisted in `X-Original-For`. The same pattern is applied to the other headers, `Host` and `Proto`.
* [HttpContext.Request.Scheme](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.scheme#microsoft-aspnetcore-http-httprequest-scheme): Set using the `X-Forwarded-Proto` header value.
* [HttpContext.Request.Host](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.host#microsoft-aspnetcore-http-httprequest-host): Set using the `X-Forwarded-Host` header value.

For more information on the preceding, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/21615).

Forwarded headers middleware [default settings](#forwarded-headers-middleware-options) can be configured. For the default settings:

* There's only *one proxy* between the app and the source of the requests.
* Only loopback addresses are configured for known proxies and known networks.
* The forwarded headers are named `X-Forwarded-For` and `X-Forwarded-Proto`.
* The `ForwardedHeaders` value is `ForwardedHeaders.None`, the desired forwarders must be set here to enable the middleware.

Not all network appliances add the `X-Forwarded-For` and `X-Forwarded-Proto` headers without additional configuration. Consult your appliance manufacturer's guidance if proxied requests don't contain these headers when they reach the app. If the appliance uses different header names than `X-Forwarded-For` and `X-Forwarded-Proto`, set the [ForwardedForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedforheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedforheadername) and [ForwardedProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedprotoheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedprotoheadername) options to match the header names used by the appliance. For more information, see [Forwarded headers middleware options](#forwarded-headers-middleware-options) and [Configuration for a proxy that uses different header names](#configuration-for-a-proxy-that-uses-different-header-names).

## IIS/IIS Express and ASP.NET Core Module

[IIS integration middleware](iis/?view=aspnetcore-10.0#enable-the-iisintegration-components) enables forwarded headers middleware by default when the app is hosted [out-of-process](iis/?view=aspnetcore-10.0#out-of-process-hosting-model) behind IIS and the ASP.NET Core Module. Forwarded headers middleware is activated to run first in the middleware pipeline with a restricted configuration specific to the ASP.NET Core Module due to trust concerns with forwarded headers (for example, [IP spoofing](https://www.iplocation.net/ip-spoofing)). The middleware is configured to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers and is restricted to a single localhost proxy. If additional configuration is required, see the [forwarded headers middleware options](#forwarded-headers-middleware-options).

## Other proxy server and load balancer scenarios

Outside of using [IIS Integration](iis/?view=aspnetcore-10.0#enable-the-iisintegration-components) when hosting [out-of-process](iis/?view=aspnetcore-10.0#out-of-process-hosting-model), forwarded headers middleware isn't enabled by default. Forwarded headers middleware must be enabled for an app to process forwarded headers with [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders). After enabling the middleware if no [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) are specified to the middleware, the default [ForwardedHeadersOptions.ForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedheaders#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedheaders) are [ForwardedHeaders.None](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheaders).

Configure the middleware with [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers in `Startup.ConfigureServices`.

[]()

### Forwarded headers middleware order

Forwarded headers middleware should run before other middleware. This ordering ensures that the middleware relying on forwarded headers information can consume the header values for processing. Forwarded headers middleware can run after diagnostics and error handling, but it must be run before calling `UseHsts`:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.Configure<ForwardedHeadersOptions>(options =>
        {
            options.ForwardedHeaders =
                ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseForwardedHeaders();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseForwardedHeaders();
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

Alternatively, call `UseForwardedHeaders` before diagnostics:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseForwardedHeaders();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

Note

If no [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) are specified in `Startup.ConfigureServices` or directly to the extension method with [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders), the default headers to forward are [ForwardedHeaders.None](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheaders). The [ForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedheaders#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedheaders) property must be configured with the headers to forward.

## Nginx configuration

To forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers, see [Host ASP.NET Core on Linux with Nginx](linux-nginx?view=aspnetcore-10.0#configure-nginx).

## Apache configuration

`X-Forwarded-For` is added automatically (see [Apache Module mod\_proxy: Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)).

## Forwarded headers middleware options

[ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) control the behavior of the forwarded headers middleware. The following example changes the default values:

* Limit the number of entries in the forwarded headers to `2`.
* Add a known proxy address of `127.0.10.1`.
* Change the forwarded header name from the default `X-Forwarded-For` to `X-Forwarded-For-My-Custom-Header-Name`.

```
services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardLimit = 2;
    options.KnownProxies.Add(IPAddress.Parse("127.0.10.1"));
    options.ForwardedForHeaderName = "X-Forwarded-For-My-Custom-Header-Name";
});
```

| Option                                                                                                                                                                                                       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [AllowedHosts](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.allowedhosts#microsoft-aspnetcore-builder-forwardedheadersoptions-allowedhosts)                                        | Restricts hosts by the `X-Forwarded-Host` header to the values provided.- Values are compared using ordinal-ignore-case.
- Port numbers must be excluded.
- If the list is empty, all hosts are allowed.
- A top-level wildcard `*` allows all non-empty hosts.
- Subdomain wildcards are permitted but don't match the root domain. For example, `*.contoso.com` matches the subdomain `foo.contoso.com` but not the root domain `contoso.com`.
- Unicode host names are allowed but are converted to [Punycode](https://datatracker.ietf.org/doc/html/rfc3492) for matching.
- [IPv6 addresses](https://datatracker.ietf.org/doc/html/rfc4291) must include bounding brackets and be in [conventional form](https://datatracker.ietf.org/doc/html/rfc4291#section-2.2) (for example, `[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]`). IPv6 addresses aren't special-cased to check for logical equality between different formats, and no canonicalization is performed.
- Failure to restrict the allowed hosts might allow a cyberattacker to spoof links generated by the service.The default value is an empty `IList<string>`. |
| [ForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedheaders#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedheaders)                            | Identifies which forwarders should be processed. See the [ForwardedHeaders Enum](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheaders) for the list of fields that apply. Typical values assigned to this property are `ForwardedHeaders.XForwardedFor \| ForwardedHeaders.XForwardedProto`. The default value is [ForwardedHeaders.None](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheaders).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| [ForwardedForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedforheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedforheadername)          | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XForwardedForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xforwardedforheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xforwardedforheadername). This option is used when the proxy/forwarder doesn't use the `X-Forwarded-For` header but uses some other header to forward the information. The default is `X-Forwarded-For`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| [ForwardedHostHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedhostheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedhostheadername)       | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XForwardedHostHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xforwardedhostheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xforwardedhostheadername). This option is used when the proxy/forwarder doesn't use the `X-Forwarded-Host` header but uses some other header to forward the information. The default is `X-Forwarded-Host`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| [ForwardedProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedprotoheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedprotoheadername)    | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XForwardedProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xforwardedprotoheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xforwardedprotoheadername). This option is used when the proxy/forwarder doesn't use the `X-Forwarded-Proto` header but uses some other header to forward the information. The default is `X-Forwarded-Proto`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| [ForwardedPrefixHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedprefixheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedprefixheadername) | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XForwardedPrefixHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xforwardedprefixheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xforwardedprefixheadername). This option is used when the proxy/forwarder doesn't use the `X-Forwarded-Prefix` header but uses some other header to forward the information. The default is `X-Forwarded-Prefix`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| [ForwardLimit](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardlimit#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardlimit)                                        | Limits the number of entries in the headers that are processed. Set to `null` to disable the limit, but this should only be done if `KnownProxies` or `KnownNetworks` are configured. Setting a non-`null` value is a precaution (but not a guarantee) to protect against misconfigured proxies and malicious requests arriving from side-channels on the network. Forwarded headers middleware processes headers in reverse order from right to left. If the default value (`1`) is used, only the rightmost value from the headers is processed unless the value of `ForwardLimit` is increased. The default is `1`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| [KnownNetworks](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownnetworks#microsoft-aspnetcore-builder-forwardedheadersoptions-knownnetworks)                                     | Address ranges of known networks to accept forwarded headers from. Provide IP ranges using Classless Interdomain Routing (CIDR) notation. If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format (for example, `10.0.0.1` in IPv4 represented in IPv6 as `::ffff:10.0.0.1`). See [IPAddress.MapToIPv6](/en-us/dotnet/api/system.net.ipaddress.maptoipv6). Determine if this format is required by looking at the [HttpContext.Connection.RemoteIpAddress](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.remoteipaddress). The default is an `IList`<[IPNetwork](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.ipnetwork)> containing a single entry for `new IPNetwork(IPAddress.Loopback, 8)`.                                                                                                                                                                                                                                                                                                                                                                               |
| [KnownProxies](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.knownproxies#microsoft-aspnetcore-builder-forwardedheadersoptions-knownproxies)                                        | Addresses of known proxies to accept forwarded headers from. Use `KnownProxies` to specify exact IP address matches. If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format (for example, `10.0.0.1` in IPv4 represented in IPv6 as `::ffff:10.0.0.1`). See [IPAddress.MapToIPv6](/en-us/dotnet/api/system.net.ipaddress.maptoipv6). Determine if this format is required by looking at the [HttpContext.Connection.RemoteIpAddress](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.remoteipaddress). The default is an `IList`<[IPAddress](/en-us/dotnet/api/system.net.ipaddress)> containing a single entry for `IPAddress.IPv6Loopback`.                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| [OriginalForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.originalforheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-originalforheadername)             | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XOriginalForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xoriginalforheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xoriginalforheadername). The default is `X-Original-For`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| [OriginalHostHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.originalhostheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-originalhostheadername)          | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XOriginalHostHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xoriginalhostheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xoriginalhostheadername). The default is `X-Original-Host`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| [OriginalProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.originalprotoheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-originalprotoheadername)       | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XOriginalProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xoriginalprotoheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xoriginalprotoheadername). The default is `X-Original-Proto`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| [OriginalPrefixHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.originalprefixheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-originalprefixheadername)    | Use the header specified by this property instead of the one specified by [ForwardedHeadersDefaults.XOriginalPrefixHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersdefaults.xoriginalprefixheadername#microsoft-aspnetcore-httpoverrides-forwardedheadersdefaults-xoriginalprefixheadername). The default is `X-Original-Prefix`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| [RequireHeaderSymmetry](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.requireheadersymmetry#microsoft-aspnetcore-builder-forwardedheadersoptions-requireheadersymmetry)             | Require the number of header values to be in sync between the [ForwardedHeadersOptions.ForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedheaders#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedheaders) being processed. The default in ASP.NET Core 1.x is `true`. The default in ASP.NET Core 2.0 or later is `false`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

## Scenarios and use cases

### When it isn't possible to add forwarded headers and all requests are secure

In some cases, it might not be possible to add forwarded headers to the requests proxied to the app. If the proxy is enforcing that all public external requests are HTTPS, the scheme can be manually set in `Startup.Configure` before using any type of middleware:

```
app.Use((context, next) =>
{
    context.Request.Scheme = "https";
    return next();
});
```

This code can be disabled with an environment variable or other configuration setting in a development or staging environment.

### Deal with path base and proxies that change the request path

Some proxies pass the path intact but with an app base path that should be removed so that routing works properly. [UsePathBaseExtensions.UsePathBase](/en-us/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase) middleware splits the path into [HttpRequest.Path](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.path#microsoft-aspnetcore-http-httprequest-path) and the app base path into [HttpRequest.PathBase](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase#microsoft-aspnetcore-http-httprequest-pathbase).

If `/foo` is the app base path for a proxy path passed as `/foo/api/1`, the middleware sets `Request.PathBase` to `/foo` and `Request.Path` to `/api/1` with the following command:

```
app.UsePathBase("/foo");
```

The original path and path base are reapplied when the middleware is called again in reverse. For more information on middleware order processing, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0).

If the proxy trims the path (for example, forwarding `/foo/api/1` to `/api/1`), fix redirects and links by setting the request's [PathBase](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase#microsoft-aspnetcore-http-httprequest-pathbase) property:

```
app.Use((context, next) =>
{
    context.Request.PathBase = new PathString("/foo");
    return next();
});
```

If the proxy is adding path data, discard part of the path to fix redirects and links by using [StartsWithSegments](/en-us/dotnet/api/microsoft.aspnetcore.http.pathstring.startswithsegments) and assigning to the [Path](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.path#microsoft-aspnetcore-http-httprequest-path) property:

```
app.Use((context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/foo", out var remainder))
    {
        context.Request.Path = remainder;
    }

    return next();
});
```

### Configuration for a proxy that uses different header names

If the proxy doesn't use headers named `X-Forwarded-For` and `X-Forwarded-Proto` to forward the proxy address/port and originating scheme information, set the [ForwardedForHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedforheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedforheadername) and [ForwardedProtoHeaderName](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.forwardedprotoheadername#microsoft-aspnetcore-builder-forwardedheadersoptions-forwardedprotoheadername) options to match the header names used by the proxy:

```
services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedForHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-For_Header";
    options.ForwardedProtoHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-Proto_Header";
});
```

## Forward the scheme for Linux and non-IIS reverse proxies

Apps that call [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) and [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts) put a site into an infinite loop if deployed to an Azure Linux App Service, Azure Linux virtual machine (VM), or behind any other reverse proxy besides IIS. The reverse proxy terminates TLS, and Kestrel isn't made aware of the correct request scheme. OAuth and OIDC also fail in this configuration because they generate incorrect redirects. [UseIISIntegration](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderiisextensions.useiisintegration) adds and configures forwarded headers middleware when running behind IIS, but there's no matching automatic configuration for Linux (Apache or Nginx integration).

To forward the scheme from the proxy in non-IIS scenarios, add and configure forwarded headers middleware. In `Startup.ConfigureServices`, use the following code:

```
// using Microsoft.AspNetCore.HttpOverrides;

if (string.Equals(
    Environment.GetEnvironmentVariable("ASPNETCORE_FORWARDEDHEADERS_ENABLED"),
    "true", StringComparison.OrdinalIgnoreCase))
{
    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
            ForwardedHeaders.XForwardedProto;
        // Only loopback proxies are allowed by default.
        // Clear that restriction because forwarders are enabled by explicit
        // configuration.
        options.KnownNetworks.Clear();
        options.KnownProxies.Clear();
    });
}
```

## Certificate forwarding

### Azure

To configure Azure App Service for certificate forwarding, see [Configure TLS mutual authentication for Azure App Service](/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth). The following guidance pertains to configuring the ASP.NET Core app.

In `Startup.Configure`, add the following code before the call to `app.UseAuthentication();`:

```
app.UseCertificateForwarding();
```

Configure certificate-forwarding middleware to specify the header name that Azure uses. In `Startup.ConfigureServices`, add the following code to configure the header from which the middleware builds a certificate:

```
services.AddCertificateForwarding(options =>
    options.CertificateHeader = "X-ARR-ClientCert");
```

### Other web proxies

If a proxy is used that isn't IIS or Azure App Service's Application Request Routing (ARR), configure the proxy to forward the certificate that it received in an HTTP header. In `Startup.Configure`, add the following code before the call to `app.UseAuthentication();`:

```
app.UseCertificateForwarding();
```

Configure the certificate-forwarding middleware to specify the header name. In `Startup.ConfigureServices`, add the following code to configure the header from which the middleware builds a certificate:

```
services.AddCertificateForwarding(options =>
    options.CertificateHeader = "YOUR_CERTIFICATE_HEADER_NAME");
```

If the proxy isn't base64-encoding the certificate (as is the case with Nginx), set the `HeaderConverter` option. Consider the following example in `Startup.ConfigureServices`:

```
services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME";
    options.HeaderConverter = (headerValue) =>
    {
        var clientCertificate =
           /* some conversion logic to create an X509Certificate2 */
        return clientCertificate;
    }
});
```

## Troubleshoot

When headers aren't forwarded as expected, enable [logging](../fundamentals/logging/?view=aspnetcore-10.0). If the logs don't provide sufficient information to troubleshoot the problem, enumerate the request headers received by the server. Use inline middleware to write request headers to an app response or log the headers.

To write the headers to the app's response, place the following terminal inline middleware immediately after the call to [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders) in `Startup.Configure`:

```
app.Run(async (context) =>
{
    context.Response.ContentType = "text/plain";

    // Request method, scheme, and path
    await context.Response.WriteAsync(
        $"Request Method: {context.Request.Method}{Environment.NewLine}");
    await context.Response.WriteAsync(
        $"Request Scheme: {context.Request.Scheme}{Environment.NewLine}");
    await context.Response.WriteAsync(
        $"Request Path: {context.Request.Path}{Environment.NewLine}");

    // Headers
    await context.Response.WriteAsync($"Request Headers:{Environment.NewLine}");

    foreach (var header in context.Request.Headers)
    {
        await context.Response.WriteAsync($"{header.Key}: " +
            $"{header.Value}{Environment.NewLine}");
    }

    await context.Response.WriteAsync(Environment.NewLine);

    // Connection: RemoteIp
    await context.Response.WriteAsync(
        $"Request RemoteIp: {context.Connection.RemoteIpAddress}");
});
```

You can write to logs instead of the response body. Writing to logs allows the site to function normally while debugging.

To write logs rather than to the response body:

* Inject `ILogger<Startup>` into the `Startup` class as described in [Create log messages](../fundamentals/logging/?view=aspnetcore-10.0#create-log-messages).
* Place the following inline middleware immediately after the call to [UseForwardedHeaders](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders) in `Startup.Configure`.

```
app.Use(async (context, next) =>
{
    // Request method, scheme, path, and base path
    _logger.LogDebug("Request Method: {Method}", context.Request.Method);
    _logger.LogDebug("Request Scheme: {Scheme}", context.Request.Scheme);
    _logger.LogDebug("Request Path: {Path}", context.Request.Path);
    _logger.LogDebug("Request Path Base: {PathBase}", context.Request.PathBase);

    // Headers
    foreach (var header in context.Request.Headers)
    {
        _logger.LogDebug("Header: {Key}: {Value}", header.Key, header.Value);
    }

    // Connection: RemoteIp
    _logger.LogDebug("Request RemoteIp: {RemoteIpAddress}",
        context.Connection.RemoteIpAddress);

    await next();
});
```

When processed, `X-Forwarded-{For|Proto|Host|Prefix}` values are moved to `X-Original-{For|Proto|Host|Prefix}`. If there are multiple values in a given header, forwarded headers middleware processes headers in reverse order from right to left. The default `ForwardLimit` is `1` (one), so only the rightmost value from the headers is processed unless the value of `ForwardLimit` is increased.

The request's original remote IP must match an entry in the `KnownProxies` or `KnownNetworks` lists before forwarded headers are processed. This limits header spoofing by not accepting forwarders from untrusted proxies. When an unknown proxy is detected, logging indicates the address of the proxy:

```
September 20th 2018, 15:49:44.168 Unknown proxy: 10.0.0.100:54321
```

In the preceding example, 10.0.0.100 is a proxy server. If the server is a trusted proxy, add the server's IP address to `KnownProxies` (or add a trusted network to `KnownNetworks`) in `Startup.ConfigureServices`. For more information, see the [forwarded headers middleware options](#forwarded-headers-middleware-options) section.

```
services.Configure<ForwardedHeadersOptions>(options =>
{
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});
```

Important

Only allow trusted proxies and networks to forward headers. Otherwise, [IP spoofing](https://www.iplocation.net/ip-spoofing) attacks are possible.

## Additional resources

* [Host ASP.NET Core in a web farm](web-farm?view=aspnetcore-10.0)
* [Microsoft Security Advisory CVE-2018-0787: ASP.NET Core Elevation Of Privilege Vulnerability](https://github.com/aspnet/Announcements/issues/295)
* [YARP: Yet Another Reverse Proxy](https://dotnet.github.io/yarp/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/configuration.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core SignalR configuration

This article describes how to configure client and server options for ASP.NET Core SignalR.

For Blazor SignalR guidance, which adds to or supersedes the guidance in this article, see [ASP.NET Core Blazor SignalR guidance](../blazor/fundamentals/signalr?view=aspnetcore-10.0).

## Configure serialization for encoding messages

ASP.NET Core SignalR supports two protocols for encoding messages: [JSON](https://www.json.org/json-en.html) and [MessagePack](https://msgpack.org/index.html). Each protocol has serialization configuration options.

### JSON serialization options

JSON serialization can be configured on the server by using the [AddJsonProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jsonprotocoldependencyinjectionextensions.addjsonprotocol) extension method. `AddJsonProtocol` can be added after the [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) method in `Startup.ConfigureServices`. The `AddJsonProtocol` method takes a delegate that receives an `options` object. The [PayloadSerializerOptions](/en-us/dotnet/api/microsoft.aspnetcore.signalr.jsonhubprotocoloptions.payloadserializeroptions) property on that object is a `System.Text.Json` [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object that can be used to configure serialization of arguments and return values. For more information, see the [System.Text.Json documentation](/en-us/dotnet/api/system.text.json).

For example, to configure the serializer to not change the casing of property names, rather than the default [camel case](https://wikipedia.org/wiki/Camel_case) names, use the following code in the *Program.cs* file:

```
builder.Services.AddSignalR()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    });
```

In the .NET client, the same `AddJsonProtocol` extension method exists on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). The `Microsoft.Extensions.DependencyInjection` namespace must be imported to resolve the extension method:

```
// At the top of the file:
using Microsoft.Extensions.DependencyInjection;

// When constructing your connection:
var connection = new HubConnectionBuilder()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    })
    .Build();
```

Note

It's not possible to configure JSON serialization in the JavaScript client at this time.

#### Switch to Newtonsoft.Json

If your app requires features in the [Newtonsoft.Json](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.NewtonsoftJson) package that aren't supported in `System.Text.Json`, see [Switch to Newtonsoft.Json](../migration/22-to-30?view=aspnetcore-10.0#switch-to-newtonsoftjson).

### MessagePack serialization options

MessagePack serialization can be configured by providing a delegate to the [AddMessagePackProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msgpackprotocoldependencyinjectionextensions.addmessagepackprotocol) call. For more information, see [MessagePack in SignalR](messagepackhubprotocol?view=aspnetcore-10.0).

Note

It's not possible to configure MessagePack serialization in the JavaScript client at this time.

## Configure server options

The following table describes options for configuring SignalR hubs.

| Option                                  | Default value           | Description                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ClientTimeoutInterval`                 | 30 seconds              | The server considers the client disconnected if it doesn't receive a message (including keep-alive) within this interval. It can take longer than this timeout interval for the client to be marked disconnected due to how this feature is implemented. The recommended value is double the `KeepAliveInterval` value.                                                                                           |
| `HandshakeTimeout`                      | 15 seconds              | If the client doesn't send an initial handshake message within this time interval, the connection is closed. This option is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval`                     | 15 seconds              | The interval at which a ping message is automatically sent to keep the connection open. When you change the `KeepAliveInterval` value, change the `ServerTimeout` or `serverTimeoutInMilliseconds` setting on the client. The recommended `ServerTimeout` or `serverTimeoutInMilliseconds` value is double the `KeepAliveInterval` value.                                                                         |
| `SupportedProtocols`                    | All installed protocols | Protocols supported by this hub. By default, all protocols registered on the server are allowed. Protocols can be removed from this list to disable specific protocols for individual hubs.                                                                                                                                                                                                                       |
| `EnableDetailedErrors`                  | `false`                 | When this option is enabled (`true`), detailed exception messages are returned to clients when an exception is thrown in a hub method. The default is `false` because these exception messages can contain sensitive information.                                                                                                                                                                                 |
| `StreamBufferCapacity`                  | 10                      | The maximum number of items that can be buffered for client upload streams. When this limit is reached, the processing of invocations is blocked until the server processes stream items.                                                                                                                                                                                                                         |
| `MaximumReceiveMessageSize`             | 32 KB                   | The maximum size of a single incoming hub message. Increasing the value might increase the risk of [Denial of service (DoS) attacks](https://developer.mozilla.org/docs/Glossary/Denial_of_Service).                                                                                                                                                                                                              |
| `MaximumParallelInvocationsPerClient`   | 1                       | The maximum number of hub methods that each client can call in parallel before queueing. Note that this limit doesn't apply to streaming hub invocations. For more information, see [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0#limit-per-connection-streaming-invocations).                                                                                                                     |
| `DisableImplicitFromServicesParameters` | `false`                 | Hub method arguments are resolved from dependency injection, if possible.                                                                                                                                                                                                                                                                                                                                         |
| `StatefulReconnectBufferSize`           | 100,000 bytes           | The maximum number of bytes buffered per connection when stateful reconnect is enabled. This option has no effect unless `AllowStatefulReconnects` is enabled on the hub endpoint. For more information, see [Configure stateful reconnect](#configure-stateful-reconnect).                                                                                                                                       |

Options can be configured for all hubs by providing an options delegate to the `AddSignalR` call in the *Program.cs* file.

```
 builder.Services.AddSignalR(hubOptions =>
 {
     hubOptions.EnableDetailedErrors = true;
     hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
 });
```

Options for a single hub override the global options provided in `AddSignalR` and can be configured by using the [AddHubOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addhuboptions) method:

```
builder.Services.AddSignalR().AddHubOptions<ChatHub>(options =>
{
    options.EnableDetailedErrors = true;
});
```

### Configure advanced HTTP options

Use `HttpConnectionDispatcherOptions` to configure advanced settings related to transports and memory buffer management. These options are configured by passing a delegate to the [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.hubendpointroutebuilderextensions.maphub) method in `Program.cs`.

```
using Microsoft.AspNetCore.Http.Connections;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chathub", options =>
{
    options.Transports =
        HttpTransportType.WebSockets |
        HttpTransportType.LongPolling;
}
);
app.Run();
```

The following table describes options for configuring ASP.NET Core SignalR's advanced HTTP options.

| Option                            | Default value                                                                         | Description                                                                                                                                                                                                                                                                                                                                                                                                                      |
| --------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ApplicationMaxBufferSize`        | 64 KB                                                                                 | The maximum number of bytes received from the client that the server buffers before applying backpressure. Increasing this value allows the server to receive larger messages faster without applying backpressure, but can increase memory consumption.                                                                                                                                                                         |
| `TransportMaxBufferSize`          | 64 KB                                                                                 | The maximum number of bytes sent by the app that the server buffers before observing backpressure. Increasing this value allows the server to buffer larger messages faster without awaiting backpressure, but can increase memory consumption.                                                                                                                                                                                  |
| `AuthorizationData`               | Data automatically gathered from the `Authorize` attributes applied to the Hub class. | A list of [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata) objects used to determine if a client is authorized to connect to the hub.                                                                                                                                                                                                                                                       |
| `Transports`                      | All Transports are enabled.                                                           | A bit flags enum of `HttpTransportType` values that can restrict the transports a client can use to connect.                                                                                                                                                                                                                                                                                                                     |
| `LongPolling`                     | See [Configure Long Polling transport](#configure-long-polling-transport)             | Options specific to the Long Polling transport.                                                                                                                                                                                                                                                                                                                                                                                  |
| `WebSockets`                      | See [Configure WebSocket transport](#configure-websocket-transport)                   | Options specific to the WebSockets transport.                                                                                                                                                                                                                                                                                                                                                                                    |
| `MinimumProtocolVersion`          | 0                                                                                     | The minimum version of the negotiation protocol. This value is used to limit clients to newer versions of the protocol.                                                                                                                                                                                                                                                                                                          |
| `TransportSendTimeout`            | 10 seconds                                                                            | The maximum amount of time the transport waits for a single send to a client to complete. If a send exceeds this timeout, the connection is closed. Increase this value for clients on slow or unreliable networks that would otherwise be disconnected while a large message is being sent. Setting the value to `TimeSpan.Zero` throws an [ArgumentOutOfRangeException](/en-us/dotnet/api/system.argumentoutofrangeexception). |
| `CloseOnAuthenticationExpiration` | `false`                                                                               | Controls authentication expiration tracking, which closes connections when a token expires.                                                                                                                                                                                                                                                                                                                                      |
| `AllowStatefulReconnects`         | `false`                                                                               | Allows a client that negotiates stateful reconnect to resume a connection with the same connection ID after a temporary network interruption. For more information, see [Configure stateful reconnect](#configure-stateful-reconnect).                                                                                                                                                                                           |

The following advanced HTTP options configure SignalR *authentication refresh*, introduced in .NET 11. Authentication refresh lets a connected client update its authentication credentials without reconnecting. For more information, see [SignalR authentication and authorization](authn-and-authz?view=aspnetcore-10.0#authentication-refresh).

| Option                            | Default value | Description                                                                                                                                                                                                                                                                                                                            |
| --------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EnableAuthenticationRefresh`     | `false`       | Enables authentication refresh for connections. Enable it together with `CloseOnAuthenticationExpiration` so that connections that aren't refreshed before their token expires are closed.                                                                                                                                             |
| `MaximumAuthenticationExpiration` | `null`        | Caps how far in the future a refreshed token's expiration can be set, relative to the time of the refresh. Setting it also gives tokens that have no expiration a known lifetime, which enables automatic refresh. The value must be greater than zero and doesn't apply to Windows authentication.                                    |
| `OnAuthenticationRefresh`         | `null`        | An optional `Func<AuthenticationRefreshContext, Task<bool>>` callback invoked on each refresh attempt. Return `false` to reject the refresh, which causes the `/refresh` request to fail with an HTTP 403 response. This check runs in addition to the built-in requirement that the refreshed principal map to the same SignalR user. |

#### Configure Long Polling transport

The Long Polling transport has other options that can be configured by using the `LongPolling` property:

| Option        | Default value | Description                                                                                                                                                                                                   |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PollTimeout` | 90 seconds    | The maximum amount of time the server waits for a message to send to the client before terminating a single poll request. Decreasing this value causes the client to issue new poll requests more frequently. |

#### Configure WebSocket transport

The WebSocket transport has other options that can be configured by using the `WebSockets` property:

| Option                | Default value | Description                                                                                                                                                                                              |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CloseTimeout`        | 5 seconds     | After the server closes, if the client fails to close within this time interval, the connection is terminated.                                                                                           |
| `SubProtocolSelector` | `null`        | A delegate that can be used to set the `Sec-WebSocket-Protocol` header to a custom value. The delegate receives the values requested by the client as input and is expected to return the desired value. |

## Configure client options

In the .NET client and JavaScript client, the client options are configured on the `HubConnectionBuilder` type. In the Java client, the options are configured on the `HttpHubConnectionBuilder` subclass, which contains the builder configuration options and the `HubConnection` itself.

### Configure logging

Logging is configured in the .NET client by using the `ConfigureLogging` method. Logging providers and filters can be registered in the same way as they are on the server. For more information, see the [Logging in ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) documentation.

Note

To register logging providers, you must install the necessary packages. For more information, see the full list of [Built-in logging providers](../fundamentals/logging/?view=aspnetcore-10.0#built-in-logging-providers).

For example, to enable Console logging, install the `Microsoft.Extensions.Logging.Console` NuGet package. Call the `AddConsole` extension method:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub")
    .ConfigureLogging(logging => {
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddConsole();
    })
    .Build();
```

In the JavaScript client, a similar `configureLogging` method exists. Provide a `LogLevel` value as the minimum level of log messages to produce. Logs are written to the browser console window.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

Instead of a `LogLevel` value, you can also provide a `string` value that represents a log level name. This approach is useful when configuring SignalR logging in environments where you don't have access to the `LogLevel` constants.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging("warn")
    .build();
```

The following table lists the available log levels. The value you provide to `configureLogging` sets the **minimum** log level for logging. Messages logged at this level, **or the levels listed after it in the table**, are logged.

| String                      | LogLevel               |
| --------------------------- | ---------------------- |
| `trace`                     | `LogLevel.Trace`       |
| `debug`                     | `LogLevel.Debug`       |
| `info` **or** `information` | `LogLevel.Information` |
| `warn` **or** `warning`     | `LogLevel.Warning`     |
| `error`                     | `LogLevel.Error`       |
| `critical`                  | `LogLevel.Critical`    |
| `none`                      | `LogLevel.None`        |

Note

To disable logging entirely, specify `signalR.LogLevel.None` in the `configureLogging` method.

For more information on logging, see [Logging and diagnostics in ASP.NET Core SignalR](diagnostics?view=aspnetcore-10.0).

The SignalR Java client uses the [Simple Logging Facade for Java (SLF4J)](https://www.slf4j.org/) library for logging. It's a high-level logging API that allows users of the library to choose their own specific logging implementation by bringing in a specific logging dependency. The following code snippet shows how to use `java.util.logging` with the SignalR Java client.

```
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
```

If you don't configure logging in your dependencies, SLF4J loads a default no-operation logger with the following warning message:

```
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

You can safely ignore this message.

### Configure allowed transports

The transports used by SignalR can be configured in the `WithUrl` call (`withUrl` in JavaScript). A bitwise-OR of the values of `HttpTransportType` can be used to restrict the client to only use the specified transports. All transports are enabled by default.

For example, to disable the Server-Sent Events transport, but allow WebSockets and Long Polling connections:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
    .Build();
```

In the JavaScript client, transports are configured by setting the `transport` field on the options object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling })
    .build();
```

In this version of the Java client, WebSockets is the only available transport.

In the Java client, the transport is selected by using the `withTransport` method on the `HttpHubConnectionBuilder`. The Java client defaults to using the WebSockets transport.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withTransport(TransportEnum.WEBSOCKETS)
    .build();
```

Note

The SignalR Java client doesn't currently support transport fallback.

### Configure bearer authentication

To provide authentication data along with SignalR requests, use the `AccessTokenProvider` option (`accessTokenFactory` in JavaScript) to specify a function that returns the desired access token. In the .NET client, this access token is passed in as an HTTP "Bearer Authentication" token (using the `Authorization` header with a type of `Bearer`). In the JavaScript client, the access token is used as a Bearer token, **except** in a few cases where browser APIs restrict the ability to apply headers (specifically, in Server-Sent Events and WebSockets requests). In these cases, the access token is provided as a query string value `access_token`.

In the .NET client, the `AccessTokenProvider` option can be specified by using the options delegate in `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.AccessTokenProvider = async () => {
            // Get and return the access token.
        };
    })
    .Build();
```

In the JavaScript client, the access token is configured by setting the `accessTokenFactory` field on the options object in `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        accessTokenFactory: () => {
            // Get and return the access token.
            // This function can return a JavaScript Promise if asynchronous
            // logic is required to retrieve the access token.
        }
    })
    .build();
```

In the SignalR Java client, you can configure a bearer token to use for authentication by supplying an access token provider to the [HttpHubConnectionBuilder](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder?view=aspnet-signalr-java\&preserve-view=true). Use [withAccessTokenProvider](/en-us/java/api/com.microsoft.signalr.HttpHubConnectionBuilder?view=aspnet-signalr-java\&preserve-view=true#com-microsoft-signalr-httphubconnectionbuilder-withaccesstokenprovider\(io-reactivex-single\(java-lang-string\)\)) to provide an [RxJava](https://github.com/ReactiveX/RxJava) [Single\<String>](https://reactivex.io/documentation/single.html). With a call to [Single.defer](https://reactivex.io/RxJava/javadoc/io/reactivex/Single.html#defer-java.util.concurrent.Callable-), you can write logic to produce access tokens for your client.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withAccessTokenProvider(Single.defer(() -> {
        // Your logic here.
        return Single.just("An Access Token");
    })).build();
```

### Configure timeout and keep-alive options

This section describes other options for configuring timeout and keep-alive behavior.

* [.NET](#tabpanel_1_dotnet)
* [JavaScript](#tabpanel_1_javascript)
* [Java](#tabpanel_1_java)

| Option                  | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ----------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `WithServerTimeout`     | 30 seconds (30,000 milliseconds) | The timeout value for server activity, which is set directly on the [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). If the server doesn't send a message within this interval, the client considers the server disconnected and triggers the `Closed` event (`onclose` in JavaScript). This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's keep-alive interval (`WithKeepAliveInterval`) value to allow time for pings to arrive. |
| `HandshakeTimeout`      | 15 seconds                       | The timeout value for the initial server handshake, which is available on the `HubConnection` object itself. If the server doesn't send a handshake response within this interval, the client cancels the handshake and triggers the `Closed` event (`onclose` in JavaScript). This option is an advanced setting that should be modified only if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md).                                                        |
| `WithKeepAliveInterval` | 15 seconds                       | This value determines the interval at which the client sends ping messages and is set directly on the [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). This setting allows the server to detect hard disconnects, such as when a client unplugs their computer from the network. Sending any message from the client resets the timer to the start of the interval. If the client doesn't send a message within the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                            |

In the .NET client, timeout values are specified as `TimeSpan` values.

The following example shows values that are double the default values:

```
var builder = new HubConnectionBuilder()
    .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
    .WithServerTimeout(TimeSpan.FromSeconds(60))
    .WithKeepAliveInterval(TimeSpan.FromSeconds(30))
    .Build();

builder.On<string, string>("ReceiveMessage", (user, message) => ...

await builder.StartAsync();
```

| Option                  | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ----------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withServerTimeout`     | 30 seconds (30,000 milliseconds) | The timeout value for server activity, which is set directly on the [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). If the server doesn't send a message within this interval, the client considers the server disconnected and triggers the `onclose` event. This setting allows the server to detect hard disconnects, such as when a client unplugs their computer from the network. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's keep-alive interval (`withKeepAliveInterval`) value to allow time for pings to arrive.                                                                                                                                                       |
| `withKeepAliveInterval` | 15 seconds (15,000 milliseconds) | This value determines the interval at which the client sends ping messages and is set directly on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). This setting allows the server to detect hard disconnects, such as when a client unplugs their computer from the network. Sending any message from the client resets the timer to the start of the interval. If the client doesn't send a message within the `ClientTimeoutInterval` set on the server, the server considers the client disconnected. The ping occurs at most as often as the server pings. If the server pings every five seconds, assigning a value lower than `5000` (5 seconds) pings every five seconds. The default value is 15 seconds. The keep-alive interval should be less than or equal to half the value assigned to the server timeout (`withServerTimeout`). |

The following example shows values that are double the default values:

```
var connection = new signalR.HubConnectionBuilder()
  .withUrl("/chatHub")
  .withServerTimeout(60000)
  .withKeepAliveInterval(30000)
  .build();
```

| Option                                          | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getServerTimeout` / `setServerTimeout`         | 30 seconds (30,000 milliseconds) | The timeout value for server activity. If the server doesn't send a message within this interval, the client considers the server disconnected and triggers the `onClose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                            |
| `withHandshakeResponseTimeout`                  | 15 seconds                       | The timeout value for the initial server handshake. If the server doesn't send a handshake response within this interval, the client cancels the handshake and triggers the `onClose` event. This option is an advanced setting that should be modified only if handshake timeout errors are occurring due to severe network latency. For more detail on the Handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `getKeepAliveInterval` / `setKeepAliveInterval` | 15 seconds (15,000 milliseconds) | This value determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client doesn't send a message within the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                           |

### Configure stateful reconnect

SignalR *stateful reconnect* reduces the perceived downtime of clients that have a temporary disconnect in their network connection, such as when switching network connections or a short temporary loss in access.

Stateful reconnect achieves the perceived downtime by:

* Temporarily buffering data on the server and client.
* Acknowledging messages received (ACK-ing) by both the server and client.
* Recognizing when a connection is up and replaying messages that might be sent while the connection is down.

Stateful reconnect is available in .NET 8 or later.

#### Enable stateful reconnect

You can opt in to stateful reconnect at both the server hub endpoint and the client.

* On the server hub endpoint, update the configuration to enable the `AllowStatefulReconnects` option:

  ```
  app.MapHub<MyHub>("/hubName", options =>
  {
      options.AllowStatefulReconnects = true;
  });
  ```

  Optionally, the maximum buffer size in bytes allowed by the server can be set globally or for a specific hub with the `StatefulReconnectBufferSize` option.

  * Set the `StatefulReconnectBufferSize` option globally:

    ```
    builder.AddSignalR(o => o.StatefulReconnectBufferSize = 1000);
    ```

  * Set the `StatefulReconnectBufferSize` option for a specific hub:

    ```
    builder.AddSignalR().AddHubOptions<MyHub>(o => o.StatefulReconnectBufferSize = 1000);
    ```

  The `StatefulReconnectBufferSize` setting is optional with a default size of 100,000 bytes.

* On the client, update the JavaScript or TypeScript code to enable the `withStatefulReconnect` option:

  ```
  const builder = new signalR.HubConnectionBuilder()
      .withUrl("/hubname")
      .withStatefulReconnect({ bufferSize: 1000 });  // Optional, defaults to 100,000
  const connection = builder.build();
  ```

  The `bufferSize` setting is optional with a default size of 100,000 bytes.

* On the .NET client, update the code to enable the `WithStatefulReconnect` option:

  ```
  var builder = new HubConnectionBuilder()
      .WithUrl("<hub url>")
      .WithStatefulReconnect();
  builder.Services.Configure<HubConnectionOptions>(o => o.StatefulReconnectBufferSize = 1000);
  var hubConnection = builder.Build();
  ```

  The `StatefulReconnectBufferSize` setting is optional with a default size of 100,000 bytes.

### Configure other options

You can configure other options in the `WithUrl` (`withUrl` in JavaScript) method on `HubConnectionBuilder` or on the various configuration APIs on the `HttpHubConnectionBuilder` in the Java client.

* [.NET](#tabpanel_2_dotnet)
* [JavaScript](#tabpanel_2_javascript)
* [Java](#tabpanel_2_java)

| Option                      | Default value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| --------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AccessTokenProvider`       | `null`        | A function that returns a string, which is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `SkipNegotiation`           | `false`       | Set this option to `true` to skip the negotiation step. **Note**: This option is available only when the WebSockets transport is the only enabled transport. The setting can't be enabled when using the Azure SignalR Service.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `ClientCertificates`        | Empty         | A collection of Transport Layer Security (TLS) certificates to send to authenticate requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `Cookies`                   | Empty         | A collection of HTTP cookies to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `Credentials`               | Empty         | The credentials to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `CloseTimeout`              | 5 seconds     | The maximum amount of time the client waits after closing for the server to acknowledge the close request. If the server doesn't acknowledge the close within this time, the client disconnects. **Note**: This setting is available for WebSockets only.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `Headers`                   | Empty         | A map of other HTTP headers to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `HttpMessageHandlerFactory` | `null`        | A delegate that can be used to configure or replace the `HttpMessageHandler` used to send HTTP requests. This delegate must return a non-null value, and it receives the default value as a parameter. You can either modify settings on that default value and return it, or return a new `HttpMessageHandler` instance. **Note**: - When you replace the handler, be sure to copy the settings you want to keep from the provided handler. Otherwise, the configured options (such as Cookies and Headers) don't apply to the new handler. - This setting isn't used for WebSocket connections.                                                                                                                                                                                            |
| `Proxy`                     | `null`        | An HTTP proxy to use when sending HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `UseDefaultCredentials`     | `false`       | Set this boolean to send the default credentials for HTTP and WebSockets requests. This option enables the use of Windows authentication.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `WebSocketConfiguration`    | `null`        | A delegate that can be used to configure more WebSocket options. Receives an instance of [ClientWebSocketOptions](/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions) that can be used to configure the options.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `WebSocketFactory`          | `null`        | A delegate that wraps or replaces the [WebSocket](/en-us/dotnet/api/system.net.websockets.websocket) used by the WebSockets transport. The delegate receives a `WebSocketConnectionContext`, which exposes the resolved connection `Uri` and the `HttpConnectionOptions`, plus a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken), and it must return a connected `WebSocket`. **Note**: - When this delegate is set, it replaces the client's default socket creation, so the delegate is responsible for connecting the socket and for applying any options it needs, such as `WebSocketConfiguration`, `Headers`, and `Cookies`. - Returning `null` from the delegate throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception). |
| `ApplicationMaxBufferSize`  | 1 MB          | The maximum number of bytes received from the server that the client buffers before applying backpressure. Increasing this value allows the client to receive larger messages faster without applying backpressure, but can increase memory consumption.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `TransportMaxBufferSize`    | 1 MB          | The maximum number of bytes sent by the user application that the client buffers before observing backpressure. Increasing this value allows the client to buffer larger messages faster without awaiting backpressure, but can increase memory consumption.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |

In the .NET client, these options can be modified in the options delegate provided to `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.Headers["Foo"] = "Bar";
        options.SkipNegotiation = true;
        options.Transports = HttpTransportType.WebSockets;
        options.Cookies.Add(new Cookie(/* ... */);
        options.ClientCertificates.Add(/* ... */);
    })
    .Build();
```

| Option               | Default value | Description                                                                                                                                                                                                                                                                                                                      |
| -------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessTokenFactory` | `null`        | A function that returns a string, which is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                           |
| `transport`          | `null`        | An [HttpTransportType](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype) value specifying the transport to use for the connection.                                                                                                                                                                      |
| `headers`            | `null`        | A dictionary of headers sent with every HTTP request. **Note**: Sending headers in the browser doesn't work for WebSockets or the [HttpTransportType](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype#microsoft-aspnetcore-http-connections-httptransporttype-serversenteventsstream) stream.          |
| `logMessageContent`  | `null`        | Set this option to `true` to log the bytes/chars of messages sent and received by the client.                                                                                                                                                                                                                                    |
| `skipNegotiation`    | `false`       | Set this option to `true` to skip the negotiation step. **Note**: This option is available only when the WebSockets transport is the only enabled transport. The setting can't be enabled when using the Azure SignalR Service.                                                                                                  |
| `withCredentials`    | `true`        | Specifies whether credentials are sent with the CORS request. Azure App Service uses cookies for sticky sessions and needs this option enabled to work correctly. For more information on CORS with SignalR, see [Security considerations in ASP.NET Core SignalR](security?view=aspnetcore-10.0#cross-origin-resource-sharing). |
| `timeout`            | `100,000`     | The timeout value in milliseconds to apply to HTTP requests. **Note**: This option doesn't apply to Long Polling poll requests, EventSource, or WebSockets.                                                                                                                                                                      |
| `httpClient`         | `null`        | An `HttpClient` used to make HTTP requests. Provide a custom implementation to inspect or customize the requests made during negotiation and by the Long Polling and Server-Sent Events transports. **Note**: This setting isn't used for WebSocket connections.                                                                 |

In the JavaScript Client, these options can be provided in a JavaScript object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        // "Foo: Bar" will not be sent with WebSockets or Server-Sent Events requests
        headers: { "Foo": "Bar" },
        transport: signalR.HttpTransportType.LongPolling 
    })
    .build();
```

| Option                         | Default value | Description                                                                                                                                                                                                                                                  |
| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `withAccessTokenProvider`      | `null`        | A function that returns a string, which is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                       |
| `shouldSkipNegotiate`          | `false`       | Set this option to `true` to skip the negotiation step. **Note**: This option is available only when the WebSockets transport is the only enabled transport. The setting can't be enabled when using the Azure SignalR Service.                              |
| `withHeader` `withHeaders`     | Empty         | A map of other HTTP headers to send with every HTTP request.                                                                                                                                                                                                 |
| `setHttpClientBuilderCallback` | `null`        | A callback that receives the `OkHttpClient.Builder` used to construct the HTTP client. Use it for customization such as certificate validation, proxies, and cookies. By default, the client is built with a cookie jar and a read timeout for Long Polling. |

In the Java client, these options can be configured with the methods on the `HttpHubConnectionBuilder` returned from the `HubConnectionBuilder.create("HUB URL")`:

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
        .withHeader("Foo", "Bar")
        .shouldSkipNegotiate(true)
        .withHandshakeResponseTimeout(30*1000)
        .build();
```

## Related content

* [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](supported-platforms?view=aspnetcore-10.0)

## JSON/MessagePack serialization options

ASP.NET Core SignalR supports two protocols for encoding messages: [JSON](https://www.json.org/) and [MessagePack](https://msgpack.org/index.html). Each protocol has serialization configuration options.

JSON serialization can be configured on the server using the [AddJsonProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jsonprotocoldependencyinjectionextensions.addjsonprotocol) extension method, which can be added after [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) in your `Startup.ConfigureServices` method. The `AddJsonProtocol` method takes a delegate that receives an `options` object. The [PayloadSerializerSettings](/en-us/dotnet/api/microsoft.aspnetcore.signalr.jsonhubprotocoloptions.payloadserializersettings) property on that object is a Json.NET `JsonSerializerSettings` object that can be used to configure serialization of arguments and return values. For more information, see the [Json.NET documentation](https://www.newtonsoft.com/json/help/html/Introduction.htm).

For example, to configure the serializer to use "PascalCase" property names, rather than the default [camel case](https://wikipedia.org/wiki/Camel_case) names, use the following code in `Startup.ConfigureServices`:

```
services.AddSignalR()
    .AddJsonProtocol(options => {
        options.PayloadSerializerSettings.ContractResolver =
            new DefaultContractResolver();
    });
```

In the .NET client, the same `AddJsonProtocol` extension method exists on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). The `Microsoft.Extensions.DependencyInjection` namespace must be imported to resolve the extension method:

```
// At the top of the file:
using Microsoft.Extensions.DependencyInjection;

// When constructing your connection:
var connection = new HubConnectionBuilder()
    .AddJsonProtocol(options => {
        options.PayloadSerializerSettings.ContractResolver =
            new DefaultContractResolver();
    })
    .Build();
```

Note

It's not possible to configure JSON serialization in the JavaScript client at this time.

### MessagePack serialization options

MessagePack serialization can be configured by providing a delegate to the [AddMessagePackProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msgpackprotocoldependencyinjectionextensions.addmessagepackprotocol) call. See [MessagePack in SignalR](messagepackhubprotocol?view=aspnetcore-10.0) for more details.

Note

It's not possible to configure MessagePack serialization in the JavaScript client at this time.

## Configure server options

The following table describes options for configuring SignalR hubs:

| Option                 | Default Value           | Description                                                                                                                                                                                                                                                                                                                                                                                                |
| ---------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HandshakeTimeout`     | 15 seconds              | If the client doesn't send an initial handshake message within this time interval, the connection is closed. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval`    | 15 seconds              | If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing `KeepAliveInterval`, change the `ServerTimeout` or `serverTimeoutInMilliseconds` setting on the client. The recommended `ServerTimeout` or `serverTimeoutInMilliseconds` value is double the `KeepAliveInterval` value.                                          |
| `SupportedProtocols`   | All installed protocols | Protocols supported by this hub. By default, all protocols registered on the server are allowed. Protocols can be removed from this list to disable specific protocols for individual hubs.                                                                                                                                                                                                                |
| `EnableDetailedErrors` | `false`                 | If `true`, detailed exception messages are returned to clients when an exception is thrown in a Hub method. The default is `false` because these exception messages can contain sensitive information.                                                                                                                                                                                                     |

Options can be configured for all hubs by providing an options delegate to the `AddSignalR` call in `Startup.ConfigureServices`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR(hubOptions =>
    {
        hubOptions.EnableDetailedErrors = true;
        hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
    });
}
```

Options for a single hub override the global options provided in `AddSignalR` and can be configured using [AddHubOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addhuboptions):

```
services.AddSignalR().AddHubOptions<ChatHub>(options =>
{
    options.EnableDetailedErrors = true;
});
```

### Advanced HTTP configuration options

Use `HttpConnectionDispatcherOptions` to configure advanced settings related to transports and memory buffer management. These options are configured by passing a delegate to [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubroutebuilder.maphub) in `Startup.Configure`.

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseSignalR((configure) =>
    {
        var desiredTransports =
            HttpTransportType.WebSockets |
            HttpTransportType.LongPolling;

        configure.MapHub<ChatHub>("/chathub", (options) =>
        {
            options.Transports = desiredTransports;
        });
    });
}
```

The following table describes options for configuring ASP.NET Core SignalR's advanced HTTP options.

| Option                     | Default Value                                                                         | Description                                                                                                                                                                                     |
| -------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ApplicationMaxBufferSize` | 32 KB                                                                                 | The maximum number of bytes received from the client that the server buffers. Increasing this value allows the server to receive larger messages, but can negatively impact memory consumption. |
| `AuthorizationData`        | Data automatically gathered from the `Authorize` attributes applied to the Hub class. | A list of [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata) objects used to determine if a client is authorized to connect to the hub.                      |
| `TransportMaxBufferSize`   | 32 KB                                                                                 | The maximum number of bytes sent by the app that the server buffers. Increasing this value allows the server to send larger messages, but can negatively impact memory consumption.             |
| `Transports`               | All Transports are enabled.                                                           | A bit flags enum of `HttpTransportType` values that can restrict the transports a client can use to connect.                                                                                    |
| `LongPolling`              | See below.                                                                            | Additional options specific to the Long Polling transport.                                                                                                                                      |
| `WebSockets`               | See below.                                                                            | Additional options specific to the WebSockets transport.                                                                                                                                        |

The Long Polling transport has additional options that can be configured using the `LongPolling` property:

| Option        | Default Value | Description                                                                                                                                                                                                   |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PollTimeout` | 90 seconds    | The maximum amount of time the server waits for a message to send to the client before terminating a single poll request. Decreasing this value causes the client to issue new poll requests more frequently. |

The WebSocket transport has additional options that can be configured using the `WebSockets` property:

| Option                | Default Value | Description                                                                                                                                                                                              |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CloseTimeout`        | 5 seconds     | After the server closes, if the client fails to close within this time interval, the connection is terminated.                                                                                           |
| `SubProtocolSelector` | `null`        | A delegate that can be used to set the `Sec-WebSocket-Protocol` header to a custom value. The delegate receives the values requested by the client as input and is expected to return the desired value. |

## Configure client options

Client options can be configured on the `HubConnectionBuilder` type (available in the .NET and JavaScript clients). It's also available in the Java client, but the `HttpHubConnectionBuilder` subclass is what contains the builder configuration options, as well as on the `HubConnection` itself.

### Configure logging

Logging is configured in the .NET Client using the `ConfigureLogging` method. Logging providers and filters can be registered in the same way as they are on the server. See the [Logging in ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) documentation for more information.

Note

In order to register Logging providers, you must install the necessary packages. See the [Built-in logging providers](../fundamentals/logging/?view=aspnetcore-10.0#built-in-logging-providers) section of the docs for a full list.

For example, to enable Console logging, install the `Microsoft.Extensions.Logging.Console` NuGet package. Call the `AddConsole` extension method:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub")
    .ConfigureLogging(logging => {
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddConsole();
    })
    .Build();
```

In the JavaScript client, a similar `configureLogging` method exists. Provide a `LogLevel` value indicating the minimum level of log messages to produce. Logs are written to the browser console window.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

Note

To disable logging entirely, specify `signalR.LogLevel.None` in the `configureLogging` method.

For more information on logging, see the [SignalR Diagnostics documentation](diagnostics?view=aspnetcore-10.0).

The SignalR Java client uses the [SLF4J](https://www.slf4j.org/) library for logging. It's a high-level logging API that allows users of the library to choose their own specific logging implementation by bringing in a specific logging dependency. The following code snippet shows how to use `java.util.logging` with the SignalR Java client.

```
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
```

If you don't configure logging in your dependencies, SLF4J loads a default no-operation logger with the following warning message:

```
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

This can safely be ignored.

### Configure allowed transports

The transports used by SignalR can be configured in the `WithUrl` call (`withUrl` in JavaScript). A bitwise-OR of the values of `HttpTransportType` can be used to restrict the client to only use the specified transports. All transports are enabled by default.

For example, to disable the Server-Sent Events transport, but allow WebSockets and Long Polling connections:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
    .Build();
```

In the JavaScript client, transports are configured by setting the `transport` field on the options object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling })
    .build();
```

### Configure bearer authentication

To provide authentication data along with SignalR requests, use the `AccessTokenProvider` option (`accessTokenFactory` in JavaScript) to specify a function that returns the desired access token. In the .NET Client, this access token is passed in as an HTTP "Bearer Authentication" token (Using the `Authorization` header with a type of `Bearer`). In the JavaScript client, the access token is used as a Bearer token, **except** in a few cases where browser APIs restrict the ability to apply headers (specifically, in Server-Sent Events and WebSockets requests). In these cases, the access token is provided as a query string value `access_token`.

In the .NET client, the `AccessTokenProvider` option can be specified using the options delegate in `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.AccessTokenProvider = async () => {
            // Get and return the access token.
        };
    })
    .Build();
```

In the JavaScript client, the access token is configured by setting the `accessTokenFactory` field on the options object in `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        accessTokenFactory: () => {
            // Get and return the access token.
            // This function can return a JavaScript Promise if asynchronous
            // logic is required to retrieve the access token.
        }
    })
    .build();
```

In the SignalR Java client, you can configure a bearer token to use for authentication by providing an access token factory to the [HttpHubConnectionBuilder](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder?view=aspnet-signalr-java\&preserve-view=true). Use [withAccessTokenFactory](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder.withaccesstokenprovider?view=aspnet-signalr-java\&preserve-view=true) to provide an [RxJava](https://github.com/ReactiveX/RxJava) [Single\<String>](https://reactivex.io/documentation/single.html). With a call to [Single.defer](https://reactivex.io/RxJava/javadoc/io/reactivex/Single.html#defer-java.util.concurrent.Callable-), you can write logic to produce access tokens for your client.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withAccessTokenProvider(Single.defer(() -> {
        // Your logic here.
        return Single.just("An Access Token");
    })).build();
```

### Configure timeout and keep-alive options

Additional options for configuring timeout and keep-alive behavior are available on the `HubConnection` object itself:

* [.NET](#tabpanel_1_dotnet)
* [JavaScript](#tabpanel_1_javascript)
* [Java](#tabpanel_1_java)

| Option             | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ServerTimeout`    | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `Closed` event (`onclose` in JavaScript). This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `HandshakeTimeout` | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `Closed` event (`onclose` in JavaScript). This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |

In the .NET Client, timeout values are specified as `TimeSpan` values.

| Option                        | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ----------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serverTimeoutInMilliseconds` | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onclose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive. |

| Option                                  | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| --------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getServerTimeout` / `setServerTimeout` | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onClose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value, to allow time for pings to arrive.                                 |
| `withHandshakeResponseTimeout`          | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `onClose` event. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |

### Configure additional options

Additional options can be configured in the `WithUrl` (`withUrl` in JavaScript) method on `HubConnectionBuilder` or on the various configuration APIs on the `HttpHubConnectionBuilder` in the Java client:

* [.NET](#tabpanel_2_dotnet)
* [JavaScript](#tabpanel_2_javascript)
* [Java](#tabpanel_2_java)

| .NET Option                 | Default value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AccessTokenProvider`       | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `SkipNegotiation`           | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                                                                                                                                                                                                                                                            |
| `ClientCertificates`        | Empty         | A collection of TLS certificates to send to authenticate requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `Cookies`                   | Empty         | A collection of HTTP cookies to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `Credentials`               | Empty         | Credentials to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `CloseTimeout`              | 5 seconds     | WebSockets only. The maximum amount of time the client waits after closing for the server to acknowledge the close request. If the server doesn't acknowledge the close within this time, the client disconnects.                                                                                                                                                                                                                                                                                                                                               |
| `Headers`                   | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `HttpMessageHandlerFactory` | `null`        | A delegate that can be used to configure or replace the `HttpMessageHandler` used to send HTTP requests. Not used for WebSocket connections. This delegate must return a non-null value, and it receives the default value as a parameter. Either modify settings on that default value and return it, or return a new `HttpMessageHandler` instance. **When replacing the handler make sure to copy the settings you want to keep from the provided handler, otherwise, the configured options (such as Cookies and Headers) won't apply to the new handler.** |
| `Proxy`                     | `null`        | An HTTP proxy to use when sending HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `UseDefaultCredentials`     | `false`       | Set this boolean to send the default credentials for HTTP and WebSockets requests. This enables the use of Windows authentication.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `WebSocketConfiguration`    | `null`        | A delegate that can be used to configure additional WebSocket options. Receives an instance of [ClientWebSocketOptions](/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions) that can be used to configure the options.                                                                                                                                                                                                                                                                                                                              |

| JavaScript Option    | Default Value | Description                                                                                                                                                                                          |
| -------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessTokenFactory` | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `transport`          | `null`        | An [HttpTransportType](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype) value specifying the transport to use for the connection.                                          |
| `logMessageContent`  | `null`        | Set to `true` to log the bytes/chars of messages sent and received by the client.                                                                                                                    |
| `skipNegotiation`    | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |

| Java Option                | Default Value | Description                                                                                                                                                                                          |
| -------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withAccessTokenProvider`  | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `shouldSkipNegotiate`      | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |
| `withHeader` `withHeaders` | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                    |

In the .NET Client, these options can be modified by the options delegate provided to `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.Headers["Foo"] = "Bar";
        options.Cookies.Add(new Cookie(/* ... */);
        options.ClientCertificates.Add(/* ... */);
    })
    .Build();
```

In the JavaScript Client, these options can be provided in a JavaScript object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        skipNegotiation: true,
        transport: signalR.HttpTransportType.WebSockets
    })
    .build();
```

In the Java client, these options can be configured with the methods on the `HttpHubConnectionBuilder` returned from the `HubConnectionBuilder.create("HUB URL")`

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
        .withHeader("Foo", "Bar")
        .shouldSkipNegotiate(true)
        .withHandshakeResponseTimeout(30*1000)
        .build();
```

## Related content

* [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](supported-platforms?view=aspnetcore-10.0)

## JSON/MessagePack serialization options

ASP.NET Core SignalR supports two protocols for encoding messages: [JSON](https://www.json.org/) and [MessagePack](https://msgpack.org/index.html). Each protocol has serialization configuration options.

JSON serialization can be configured on the server using the [AddJsonProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jsonprotocoldependencyinjectionextensions.addjsonprotocol) extension method, which can be added after [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) in your `Startup.ConfigureServices` method. The `AddJsonProtocol` method takes a delegate that receives an `options` object. The [PayloadSerializerSettings](/en-us/dotnet/api/microsoft.aspnetcore.signalr.jsonhubprotocoloptions.payloadserializersettings) property on that object is a Json.NET `JsonSerializerSettings` object that can be used to configure serialization of arguments and return values. For more information, see the [Json.NET documentation](https://www.newtonsoft.com/json/help/html/Introduction.htm).

For example, to configure the serializer to use "PascalCase" property names, rather than the default [camel case](https://wikipedia.org/wiki/Camel_case) names, use the following code in `Startup.ConfigureServices`:

```
services.AddSignalR()
    .AddJsonProtocol(options => {
        options.PayloadSerializerSettings.ContractResolver =
            new DefaultContractResolver();
    });
```

In the .NET client, the same `AddJsonProtocol` extension method exists on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). The `Microsoft.Extensions.DependencyInjection` namespace must be imported to resolve the extension method:

```
// At the top of the file:
using Microsoft.Extensions.DependencyInjection;

// When constructing your connection:
var connection = new HubConnectionBuilder()
    .AddJsonProtocol(options => {
        options.PayloadSerializerSettings.ContractResolver =
            new DefaultContractResolver();
    })
    .Build();
```

Note

It's not possible to configure JSON serialization in the JavaScript client at this time.

### MessagePack serialization options

MessagePack serialization can be configured by providing a delegate to the [AddMessagePackProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msgpackprotocoldependencyinjectionextensions.addmessagepackprotocol) call. See [MessagePack in SignalR](messagepackhubprotocol?view=aspnetcore-10.0) for more details.

Note

It's not possible to configure MessagePack serialization in the JavaScript client at this time.

## Configure server options

The following table describes options for configuring SignalR hubs:

| Option                  | Default Value           | Description                                                                                                                                                                                                                                                                                                                                                                                                |
| ----------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ClientTimeoutInterval` | 30 seconds              | The server considers the client disconnected if it hasn't received a message (including keep-alive) in this interval. It could take longer than this timeout interval for the client to be marked disconnected due to how this is implemented. The recommended value is double the `KeepAliveInterval` value.                                                                                              |
| `HandshakeTimeout`      | 15 seconds              | If the client doesn't send an initial handshake message within this time interval, the connection is closed. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval`     | 15 seconds              | If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing `KeepAliveInterval`, change the `ServerTimeout` or `serverTimeoutInMilliseconds` setting on the client. The recommended `ServerTimeout` or `serverTimeoutInMilliseconds` value is double the `KeepAliveInterval` value.                                          |
| `SupportedProtocols`    | All installed protocols | Protocols supported by this hub. By default, all protocols registered on the server are allowed. Protocols can be removed from this list to disable specific protocols for individual hubs.                                                                                                                                                                                                                |
| `EnableDetailedErrors`  | `false`                 | If `true`, detailed exception messages are returned to clients when an exception is thrown in a Hub method. The default is `false` because these exception messages can contain sensitive information.                                                                                                                                                                                                     |

Options can be configured for all hubs by providing an options delegate to the `AddSignalR` call in `Startup.ConfigureServices`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR(hubOptions =>
    {
        hubOptions.EnableDetailedErrors = true;
        hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
    });
}
```

Options for a single hub override the global options provided in `AddSignalR` and can be configured using [AddHubOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addhuboptions):

```
services.AddSignalR().AddHubOptions<ChatHub>(options =>
{
    options.EnableDetailedErrors = true;
});
```

### Advanced HTTP configuration options

Use `HttpConnectionDispatcherOptions` to configure advanced settings related to transports and memory buffer management. These options are configured by passing a delegate to [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubroutebuilder.maphub) in `Startup.Configure`.

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseSignalR((configure) =>
    {
        var desiredTransports =
            HttpTransportType.WebSockets |
            HttpTransportType.LongPolling;

        configure.MapHub<ChatHub>("/chathub", (options) =>
        {
            options.Transports = desiredTransports;
        });
    });
}
```

The following table describes options for configuring ASP.NET Core SignalR's advanced HTTP options.

| Option                     | Default Value                                                                         | Description                                                                                                                                                                                     |
| -------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ApplicationMaxBufferSize` | 32 KB                                                                                 | The maximum number of bytes received from the client that the server buffers. Increasing this value allows the server to receive larger messages, but can negatively impact memory consumption. |
| `AuthorizationData`        | Data automatically gathered from the `Authorize` attributes applied to the Hub class. | A list of [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata) objects used to determine if a client is authorized to connect to the hub.                      |
| `TransportMaxBufferSize`   | 32 KB                                                                                 | The maximum number of bytes sent by the app that the server buffers. Increasing this value allows the server to send larger messages, but can negatively impact memory consumption.             |
| `Transports`               | All Transports are enabled.                                                           | A bit flags enum of `HttpTransportType` values that can restrict the transports a client can use to connect.                                                                                    |
| `LongPolling`              | See below.                                                                            | Additional options specific to the Long Polling transport.                                                                                                                                      |
| `WebSockets`               | See below.                                                                            | Additional options specific to the WebSockets transport.                                                                                                                                        |

The Long Polling transport has additional options that can be configured using the `LongPolling` property:

| Option        | Default Value | Description                                                                                                                                                                                                   |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PollTimeout` | 90 seconds    | The maximum amount of time the server waits for a message to send to the client before terminating a single poll request. Decreasing this value causes the client to issue new poll requests more frequently. |

The WebSocket transport has additional options that can be configured using the `WebSockets` property:

| Option                | Default Value | Description                                                                                                                                                                                              |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CloseTimeout`        | 5 seconds     | After the server closes, if the client fails to close within this time interval, the connection is terminated.                                                                                           |
| `SubProtocolSelector` | `null`        | A delegate that can be used to set the `Sec-WebSocket-Protocol` header to a custom value. The delegate receives the values requested by the client as input and is expected to return the desired value. |

## Configure client options

Client options can be configured on the `HubConnectionBuilder` type (available in the .NET and JavaScript clients). It's also available in the Java client, but the `HttpHubConnectionBuilder` subclass is what contains the builder configuration options, as well as on the `HubConnection` itself.

### Configure logging

Logging is configured in the .NET Client using the `ConfigureLogging` method. Logging providers and filters can be registered in the same way as they are on the server. See the [Logging in ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) documentation for more information.

Note

In order to register Logging providers, you must install the necessary packages. See the [Built-in logging providers](../fundamentals/logging/?view=aspnetcore-10.0#built-in-logging-providers) section of the docs for a full list.

For example, to enable Console logging, install the `Microsoft.Extensions.Logging.Console` NuGet package. Call the `AddConsole` extension method:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub")
    .ConfigureLogging(logging => {
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddConsole();
    })
    .Build();
```

In the JavaScript client, a similar `configureLogging` method exists. Provide a `LogLevel` value indicating the minimum level of log messages to produce. Logs are written to the browser console window.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

Note

To disable logging entirely, specify `signalR.LogLevel.None` in the `configureLogging` method.

For more information on logging, see the [SignalR Diagnostics documentation](diagnostics?view=aspnetcore-10.0).

The SignalR Java client uses the [SLF4J](https://www.slf4j.org/) library for logging. It's a high-level logging API that allows users of the library to choose their own specific logging implementation by bringing in a specific logging dependency. The following code snippet shows how to use `java.util.logging` with the SignalR Java client.

```
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
```

If you don't configure logging in your dependencies, SLF4J loads a default no-operation logger with the following warning message:

```
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

This can safely be ignored.

### Configure allowed transports

The transports used by SignalR can be configured in the `WithUrl` call (`withUrl` in JavaScript). A bitwise-OR of the values of `HttpTransportType` can be used to restrict the client to only use the specified transports. All transports are enabled by default.

For example, to disable the Server-Sent Events transport, but allow WebSockets and Long Polling connections:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
    .Build();
```

In the JavaScript client, transports are configured by setting the `transport` field on the options object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling })
    .build();
```

In this version of the Java client WebSockets is the only available transport.

### Configure bearer authentication

To provide authentication data along with SignalR requests, use the `AccessTokenProvider` option (`accessTokenFactory` in JavaScript) to specify a function that returns the desired access token. In the .NET Client, this access token is passed in as an HTTP "Bearer Authentication" token (Using the `Authorization` header with a type of `Bearer`). In the JavaScript client, the access token is used as a Bearer token, **except** in a few cases where browser APIs restrict the ability to apply headers (specifically, in Server-Sent Events and WebSockets requests). In these cases, the access token is provided as a query string value `access_token`.

In the .NET client, the `AccessTokenProvider` option can be specified using the options delegate in `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.AccessTokenProvider = async () => {
            // Get and return the access token.
        };
    })
    .Build();
```

In the JavaScript client, the access token is configured by setting the `accessTokenFactory` field on the options object in `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        accessTokenFactory: () => {
            // Get and return the access token.
            // This function can return a JavaScript Promise if asynchronous
            // logic is required to retrieve the access token.
        }
    })
    .build();
```

In the SignalR Java client, you can configure a bearer token to use for authentication by providing an access token factory to the [HttpHubConnectionBuilder](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder?view=aspnet-signalr-java\&preserve-view=true). Use [withAccessTokenFactory](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder.withaccesstokenprovider?view=aspnet-signalr-java\&preserve-view=true#com_microsoft_signalr__http_hub_connection_builder_withAccessTokenProvider_Single_String__) to provide an [RxJava](https://github.com/ReactiveX/RxJava) [Single\<String>](https://reactivex.io/documentation/single.html). With a call to [Single.defer](https://reactivex.io/RxJava/javadoc/io/reactivex/Single.html#defer-java.util.concurrent.Callable-), you can write logic to produce access tokens for your client.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withAccessTokenProvider(Single.defer(() -> {
        // Your logic here.
        return Single.just("An Access Token");
    })).build();
```

### Configure timeout and keep-alive options

Additional options for configuring timeout and keep-alive behavior are available on the `HubConnection` object itself:

* [.NET](#tabpanel_1_dotnet)
* [JavaScript](#tabpanel_1_javascript)
* [Java](#tabpanel_1_java)

| Option              | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ServerTimeout`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `Closed` event (`onclose` in JavaScript). This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `HandshakeTimeout`  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `Closed` event (`onclose` in JavaScript). This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval` | 15 seconds                       | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                                           |

In the .NET Client, timeout values are specified as `TimeSpan` values.

| Option                            | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serverTimeoutInMilliseconds`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onclose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive. |
| `keepAliveIntervalInMilliseconds` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                 |

| Option                                          | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getServerTimeout` / `setServerTimeout`         | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onClose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `withHandshakeResponseTimeout`                  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `onClose` event. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the Handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `getKeepAliveInterval` / `setKeepAliveInterval` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                  |

### Configure additional options

Additional options can be configured in the `WithUrl` (`withUrl` in JavaScript) method on `HubConnectionBuilder` or on the various configuration APIs on the `HttpHubConnectionBuilder` in the Java client:

* [.NET](#tabpanel_2_dotnet)
* [JavaScript](#tabpanel_2_javascript)
* [Java](#tabpanel_2_java)

| .NET Option                 | Default value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AccessTokenProvider`       | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `SkipNegotiation`           | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                                                                                                                                                                                                                                                            |
| `ClientCertificates`        | Empty         | A collection of TLS certificates to send to authenticate requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `Cookies`                   | Empty         | A collection of HTTP cookies to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `Credentials`               | Empty         | Credentials to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `CloseTimeout`              | 5 seconds     | WebSockets only. The maximum amount of time the client waits after closing for the server to acknowledge the close request. If the server doesn't acknowledge the close within this time, the client disconnects.                                                                                                                                                                                                                                                                                                                                               |
| `Headers`                   | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `HttpMessageHandlerFactory` | `null`        | A delegate that can be used to configure or replace the `HttpMessageHandler` used to send HTTP requests. Not used for WebSocket connections. This delegate must return a non-null value, and it receives the default value as a parameter. Either modify settings on that default value and return it, or return a new `HttpMessageHandler` instance. **When replacing the handler make sure to copy the settings you want to keep from the provided handler, otherwise, the configured options (such as Cookies and Headers) won't apply to the new handler.** |
| `Proxy`                     | `null`        | An HTTP proxy to use when sending HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `UseDefaultCredentials`     | `false`       | Set this boolean to send the default credentials for HTTP and WebSockets requests. This enables the use of Windows authentication.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `WebSocketConfiguration`    | `null`        | A delegate that can be used to configure additional WebSocket options. Receives an instance of [ClientWebSocketOptions](/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions) that can be used to configure the options.                                                                                                                                                                                                                                                                                                                              |

| JavaScript Option    | Default Value | Description                                                                                                                                                                                          |
| -------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessTokenFactory` | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `transport`          | `null`        | An [HttpTransportType](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype) value specifying the transport to use for the connection.                                          |
| `logMessageContent`  | `null`        | Set to `true` to log the bytes/chars of messages sent and received by the client.                                                                                                                    |
| `skipNegotiation`    | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |

| Java Option                | Default Value | Description                                                                                                                                                                                          |
| -------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withAccessTokenProvider`  | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `shouldSkipNegotiate`      | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |
| `withHeader` `withHeaders` | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                    |

In the .NET Client, these options can be modified by the options delegate provided to `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.Headers["Foo"] = "Bar";
        options.Cookies.Add(new Cookie(/* ... */);
        options.ClientCertificates.Add(/* ... */);
    })
    .Build();
```

In the JavaScript Client, these options can be provided in a JavaScript object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        skipNegotiation: true,
        transport: signalR.HttpTransportType.WebSockets
    })
    .build();
```

In the Java client, these options can be configured with the methods on the `HttpHubConnectionBuilder` returned from the `HubConnectionBuilder.create("HUB URL")`

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
        .withHeader("Foo", "Bar")
        .shouldSkipNegotiate(true)
        .withHandshakeResponseTimeout(30*1000)
        .build();
```

## Related content

* [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](supported-platforms?view=aspnetcore-10.0)

## JSON/MessagePack serialization options

ASP.NET Core SignalR supports two protocols for encoding messages: [JSON](https://www.json.org/) and [MessagePack](https://msgpack.org/index.html). Each protocol has serialization configuration options.

JSON serialization can be configured on the server using the [AddJsonProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jsonprotocoldependencyinjectionextensions.addjsonprotocol) extension method. `AddJsonProtocol` can be added after [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) in `Startup.ConfigureServices`. The `AddJsonProtocol` method takes a delegate that receives an `options` object. The [PayloadSerializerOptions](/en-us/dotnet/api/microsoft.aspnetcore.signalr.jsonhubprotocoloptions.payloadserializeroptions) property on that object is a `System.Text.Json` [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object that can be used to configure serialization of arguments and return values. For more information, see the [System.Text.Json documentation](/en-us/dotnet/api/system.text.json).

For example, to configure the serializer to not change the casing of property names, rather than the default [camel case](https://wikipedia.org/wiki/Camel_case) names, use the following code in `Startup.ConfigureServices`:

```
services.AddSignalR()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    });
```

In the .NET client, the same `AddJsonProtocol` extension method exists on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). The `Microsoft.Extensions.DependencyInjection` namespace must be imported to resolve the extension method:

```
// At the top of the file:
using Microsoft.Extensions.DependencyInjection;

// When constructing your connection:
var connection = new HubConnectionBuilder()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    })
    .Build();
```

Note

It's not possible to configure JSON serialization in the JavaScript client at this time.

### Switch to Newtonsoft.Json

If you need features of `Newtonsoft.Json` that aren't supported in `System.Text.Json`, see [Switch to `Newtonsoft.Json`](../migration/22-to-30?view=aspnetcore-10.0#switch-to-newtonsoftjson).

### MessagePack serialization options

MessagePack serialization can be configured by providing a delegate to the [AddMessagePackProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msgpackprotocoldependencyinjectionextensions.addmessagepackprotocol) call. See [MessagePack in SignalR](messagepackhubprotocol?view=aspnetcore-10.0) for more details.

Note

It's not possible to configure MessagePack serialization in the JavaScript client at this time.

## Configure server options

The following table describes options for configuring SignalR hubs.

| Option                      | Default Value           | Description                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ClientTimeoutInterval`     | 30 seconds              | The server considers the client disconnected if it hasn't received a message (including keep-alive) in this interval. It could take longer than this timeout interval for the client to be marked disconnected due to how this is implemented. The recommended value is double the `KeepAliveInterval` value.                                                                                              |
| `HandshakeTimeout`          | 15 seconds              | If the client doesn't send an initial handshake message within this time interval, the connection is closed. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval`         | 15 seconds              | If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing `KeepAliveInterval`, change the `ServerTimeout` or `serverTimeoutInMilliseconds` setting on the client. The recommended `ServerTimeout` or `serverTimeoutInMilliseconds` value is double the `KeepAliveInterval` value.                                          |
| `SupportedProtocols`        | All installed protocols | Protocols supported by this hub. By default, all protocols registered on the server are allowed. Protocols can be removed from this list to disable specific protocols for individual hubs.                                                                                                                                                                                                                |
| `EnableDetailedErrors`      | `false`                 | If `true`, detailed exception messages are returned to clients when an exception is thrown in a Hub method. The default is `false` because these exception messages can contain sensitive information.                                                                                                                                                                                                     |
| `StreamBufferCapacity`      | `10`                    | The maximum number of items that can be buffered for client upload streams. If this limit is reached, the processing of invocations is blocked until the server processes stream items.                                                                                                                                                                                                                    |
| `MaximumReceiveMessageSize` | 32 KB                   | Maximum size of a single incoming hub message. Increasing the value may increase the risk of [Denial of service (DoS) attacks](https://developer.mozilla.org/docs/Glossary/DOS_attack).                                                                                                                                                                                                                    |

Options can be configured for all hubs by providing an options delegate to the `AddSignalR` call in `Startup.ConfigureServices`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR(hubOptions =>
    {
        hubOptions.EnableDetailedErrors = true;
        hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
    });
}
```

Options for a single hub override the global options provided in `AddSignalR` and can be configured using [AddHubOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addhuboptions):

```
services.AddSignalR().AddHubOptions<ChatHub>(options =>
{
    options.EnableDetailedErrors = true;
});
```

### Advanced HTTP configuration options

Use `HttpConnectionDispatcherOptions` to configure advanced settings related to transports and memory buffer management. These options are configured by passing a delegate to [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.hubendpointroutebuilderextensions.maphub) in `Startup.Configure`.

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chathub", options =>
        {
            options.Transports =
                HttpTransportType.WebSockets |
                HttpTransportType.LongPolling;
        });
    });
}
```

The following table describes options for configuring ASP.NET Core SignalR's advanced HTTP options.

| Option                     | Default Value                                                                         | Description                                                                                                                                                                                                                                                    |
| -------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ApplicationMaxBufferSize` | 32 KB                                                                                 | The maximum number of bytes received from the client that the server buffers before applying backpressure. Increasing this value allows the server to receive larger messages more quickly without applying backpressure, but can increase memory consumption. |
| `AuthorizationData`        | Data automatically gathered from the `Authorize` attributes applied to the Hub class. | A list of [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata) objects used to determine if a client is authorized to connect to the hub.                                                                                     |
| `TransportMaxBufferSize`   | 32 KB                                                                                 | The maximum number of bytes sent by the app that the server buffers before observing backpressure. Increasing this value allows the server to buffer larger messages more quickly without awaiting backpressure, but can increase memory consumption.          |
| `Transports`               | All Transports are enabled.                                                           | A bit flags enum of `HttpTransportType` values that can restrict the transports a client can use to connect.                                                                                                                                                   |
| `LongPolling`              | See below.                                                                            | Additional options specific to the Long Polling transport.                                                                                                                                                                                                     |
| `WebSockets`               | See below.                                                                            | Additional options specific to the WebSockets transport.                                                                                                                                                                                                       |

The Long Polling transport has additional options that can be configured using the `LongPolling` property:

| Option        | Default Value | Description                                                                                                                                                                                                   |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PollTimeout` | 90 seconds    | The maximum amount of time the server waits for a message to send to the client before terminating a single poll request. Decreasing this value causes the client to issue new poll requests more frequently. |

The WebSocket transport has additional options that can be configured using the `WebSockets` property:

| Option                | Default Value | Description                                                                                                                                                                                              |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CloseTimeout`        | 5 seconds     | After the server closes, if the client fails to close within this time interval, the connection is terminated.                                                                                           |
| `SubProtocolSelector` | `null`        | A delegate that can be used to set the `Sec-WebSocket-Protocol` header to a custom value. The delegate receives the values requested by the client as input and is expected to return the desired value. |

## Configure client options

Client options can be configured on the `HubConnectionBuilder` type (available in the .NET and JavaScript clients). It's also available in the Java client, but the `HttpHubConnectionBuilder` subclass is what contains the builder configuration options, as well as on the `HubConnection` itself.

### Configure logging

Logging is configured in the .NET Client using the `ConfigureLogging` method. Logging providers and filters can be registered in the same way as they are on the server. See the [Logging in ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) documentation for more information.

Note

In order to register Logging providers, you must install the necessary packages. See the [Built-in logging providers](../fundamentals/logging/?view=aspnetcore-10.0#built-in-logging-providers) section of the docs for a full list.

For example, to enable Console logging, install the `Microsoft.Extensions.Logging.Console` NuGet package. Call the `AddConsole` extension method:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub")
    .ConfigureLogging(logging => {
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddConsole();
    })
    .Build();
```

In the JavaScript client, a similar `configureLogging` method exists. Provide a `LogLevel` value indicating the minimum level of log messages to produce. Logs are written to the browser console window.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

Instead of a `LogLevel` value, you can also provide a `string` value representing a log level name. This is useful when configuring SignalR logging in environments where you don't have access to the `LogLevel` constants.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging("warn")
    .build();
```

The following table lists the available log levels. The value you provide to `configureLogging` sets the **minimum** log level that will be logged. Messages logged at this level, **or the levels listed after it in the table**, will be logged.

| String                      | LogLevel               |
| --------------------------- | ---------------------- |
| `trace`                     | `LogLevel.Trace`       |
| `debug`                     | `LogLevel.Debug`       |
| `info` **or** `information` | `LogLevel.Information` |
| `warn` **or** `warning`     | `LogLevel.Warning`     |
| `error`                     | `LogLevel.Error`       |
| `critical`                  | `LogLevel.Critical`    |
| `none`                      | `LogLevel.None`        |

Note

To disable logging entirely, specify `signalR.LogLevel.None` in the `configureLogging` method.

For more information on logging, see the [SignalR Diagnostics documentation](diagnostics?view=aspnetcore-10.0).

The SignalR Java client uses the [SLF4J](https://www.slf4j.org/) library for logging. It's a high-level logging API that allows users of the library to choose their own specific logging implementation by bringing in a specific logging dependency. The following code snippet shows how to use `java.util.logging` with the SignalR Java client.

```
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
```

If you don't configure logging in your dependencies, SLF4J loads a default no-operation logger with the following warning message:

```
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

This can safely be ignored.

### Configure allowed transports

The transports used by SignalR can be configured in the `WithUrl` call (`withUrl` in JavaScript). A bitwise-OR of the values of `HttpTransportType` can be used to restrict the client to only use the specified transports. All transports are enabled by default.

For example, to disable the Server-Sent Events transport, but allow WebSockets and Long Polling connections:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
    .Build();
```

In the JavaScript client, transports are configured by setting the `transport` field on the options object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling })
    .build();
```

In this version of the Java client WebSockets is the only available transport.

In the Java client, the transport is selected with the `withTransport` method on the `HttpHubConnectionBuilder`. The Java client defaults to using the WebSockets transport.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withTransport(TransportEnum.WEBSOCKETS)
    .build();
```

Note

The SignalR Java client doesn't support transport fallback yet.

### Configure bearer authentication

To provide authentication data along with SignalR requests, use the `AccessTokenProvider` option (`accessTokenFactory` in JavaScript) to specify a function that returns the desired access token. In the .NET Client, this access token is passed in as an HTTP "Bearer Authentication" token (Using the `Authorization` header with a type of `Bearer`). In the JavaScript client, the access token is used as a Bearer token, **except** in a few cases where browser APIs restrict the ability to apply headers (specifically, in Server-Sent Events and WebSockets requests). In these cases, the access token is provided as a query string value `access_token`.

In the .NET client, the `AccessTokenProvider` option can be specified using the options delegate in `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.AccessTokenProvider = async () => {
            // Get and return the access token.
        };
    })
    .Build();
```

In the JavaScript client, the access token is configured by setting the `accessTokenFactory` field on the options object in `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        accessTokenFactory: () => {
            // Get and return the access token.
            // This function can return a JavaScript Promise if asynchronous
            // logic is required to retrieve the access token.
        }
    })
    .build();
```

In the SignalR Java client, you can configure a bearer token to use for authentication by providing an access token factory to the [HttpHubConnectionBuilder](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder?view=aspnet-signalr-java\&preserve-view=true). Use [withAccessTokenFactory](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder.withaccesstokenprovider?view=aspnet-signalr-java\&preserve-view=true) to provide an [RxJava](https://github.com/ReactiveX/RxJava) [Single\<String>](https://reactivex.io/documentation/single.html). With a call to [Single.defer](https://reactivex.io/RxJava/javadoc/io/reactivex/Single.html#defer-java.util.concurrent.Callable-), you can write logic to produce access tokens for your client.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withAccessTokenProvider(Single.defer(() -> {
        // Your logic here.
        return Single.just("An Access Token");
    })).build();
```

### Configure timeout and keep-alive options

Additional options for configuring timeout and keep-alive behavior are available on the `HubConnection` object itself:

* [.NET](#tabpanel_1_dotnet)
* [JavaScript](#tabpanel_1_javascript)
* [Java](#tabpanel_1_java)

| Option              | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ServerTimeout`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `Closed` event (`onclose` in JavaScript). This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `HandshakeTimeout`  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `Closed` event (`onclose` in JavaScript). This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval` | 15 seconds                       | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                                           |

In the .NET Client, timeout values are specified as `TimeSpan` values.

| Option                            | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serverTimeoutInMilliseconds`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onclose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive. |
| `keepAliveIntervalInMilliseconds` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                 |

| Option                                          | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getServerTimeout` / `setServerTimeout`         | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onClose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `withHandshakeResponseTimeout`                  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `onClose` event. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the Handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `getKeepAliveInterval` / `setKeepAliveInterval` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                  |

### Configure additional options

Additional options can be configured in the `WithUrl` (`withUrl` in JavaScript) method on `HubConnectionBuilder` or on the various configuration APIs on the `HttpHubConnectionBuilder` in the Java client:

* [.NET](#tabpanel_2_dotnet)
* [JavaScript](#tabpanel_2_javascript)
* [Java](#tabpanel_2_java)

| .NET Option                 | Default value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AccessTokenProvider`       | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `SkipNegotiation`           | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                                                                                                                                                                                                                                                            |
| `ClientCertificates`        | Empty         | A collection of TLS certificates to send to authenticate requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `Cookies`                   | Empty         | A collection of HTTP cookies to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `Credentials`               | Empty         | Credentials to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `CloseTimeout`              | 5 seconds     | WebSockets only. The maximum amount of time the client waits after closing for the server to acknowledge the close request. If the server doesn't acknowledge the close within this time, the client disconnects.                                                                                                                                                                                                                                                                                                                                               |
| `Headers`                   | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `HttpMessageHandlerFactory` | `null`        | A delegate that can be used to configure or replace the `HttpMessageHandler` used to send HTTP requests. Not used for WebSocket connections. This delegate must return a non-null value, and it receives the default value as a parameter. Either modify settings on that default value and return it, or return a new `HttpMessageHandler` instance. **When replacing the handler make sure to copy the settings you want to keep from the provided handler, otherwise, the configured options (such as Cookies and Headers) won't apply to the new handler.** |
| `Proxy`                     | `null`        | An HTTP proxy to use when sending HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `UseDefaultCredentials`     | `false`       | Set this boolean to send the default credentials for HTTP and WebSockets requests. This enables the use of Windows authentication.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `WebSocketConfiguration`    | `null`        | A delegate that can be used to configure additional WebSocket options. Receives an instance of [ClientWebSocketOptions](/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions) that can be used to configure the options.                                                                                                                                                                                                                                                                                                                              |

| JavaScript Option    | Default Value | Description                                                                                                                                                                                          |
| -------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessTokenFactory` | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `transport`          | `null`        | An [HttpTransportType](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype) value specifying the transport to use for the connection.                                          |
| `logMessageContent`  | `null`        | Set to `true` to log the bytes/chars of messages sent and received by the client.                                                                                                                    |
| `skipNegotiation`    | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |

| Java Option                | Default Value | Description                                                                                                                                                                                          |
| -------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withAccessTokenProvider`  | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `shouldSkipNegotiate`      | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |
| `withHeader` `withHeaders` | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                    |

In the .NET Client, these options can be modified by the options delegate provided to `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.Headers["Foo"] = "Bar";
        options.Cookies.Add(new Cookie(/* ... */);
        options.ClientCertificates.Add(/* ... */);
    })
    .Build();
```

In the JavaScript Client, these options can be provided in a JavaScript object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        skipNegotiation: true,
        transport: signalR.HttpTransportType.WebSockets
    })
    .build();
```

In the Java client, these options can be configured with the methods on the `HttpHubConnectionBuilder` returned from the `HubConnectionBuilder.create("HUB URL")`

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
        .withHeader("Foo", "Bar")
        .shouldSkipNegotiate(true)
        .withHandshakeResponseTimeout(30*1000)
        .build();
```

## Related content

* [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](supported-platforms?view=aspnetcore-10.0)

## JSON/MessagePack serialization options

ASP.NET Core SignalR supports two protocols for encoding messages: [JSON](https://www.json.org/) and [MessagePack](https://msgpack.org/index.html). Each protocol has serialization configuration options.

JSON serialization can be configured on the server using the [AddJsonProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jsonprotocoldependencyinjectionextensions.addjsonprotocol) extension method. `AddJsonProtocol` can be added after [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) in `Startup.ConfigureServices`. The `AddJsonProtocol` method takes a delegate that receives an `options` object. The [PayloadSerializerOptions](/en-us/dotnet/api/microsoft.aspnetcore.signalr.jsonhubprotocoloptions.payloadserializeroptions) property on that object is a `System.Text.Json` [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object that can be used to configure serialization of arguments and return values. For more information, see the [System.Text.Json documentation](/en-us/dotnet/api/system.text.json).

For example, to configure the serializer to not change the casing of property names, rather than the default [camel case](https://wikipedia.org/wiki/Camel_case) names, use the following code in `Startup.ConfigureServices`:

```
services.AddSignalR()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null
    });
```

In the .NET client, the same `AddJsonProtocol` extension method exists on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). The `Microsoft.Extensions.DependencyInjection` namespace must be imported to resolve the extension method:

```
// At the top of the file:
using Microsoft.Extensions.DependencyInjection;

// When constructing your connection:
var connection = new HubConnectionBuilder()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    })
    .Build();
```

Note

It's not possible to configure JSON serialization in the JavaScript client at this time.

### Switch to Newtonsoft.Json

If you need features of `Newtonsoft.Json` that aren't supported in `System.Text.Json`, see [Switch to `Newtonsoft.Json`](../migration/22-to-30?view=aspnetcore-10.0#switch-to-newtonsoftjson).

### MessagePack serialization options

MessagePack serialization can be configured by providing a delegate to the [AddMessagePackProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msgpackprotocoldependencyinjectionextensions.addmessagepackprotocol) call. See [MessagePack in SignalR](messagepackhubprotocol?view=aspnetcore-10.0) for more details.

Note

It's not possible to configure MessagePack serialization in the JavaScript client at this time.

## Configure server options

The following table describes options for configuring SignalR hubs:

| Option                      | Default Value           | Description                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ClientTimeoutInterval`     | 30 seconds              | The server considers the client disconnected if it hasn't received a message (including keep-alive) in this interval. It could take longer than this timeout interval for the client to be marked disconnected due to how this is implemented. The recommended value is double the `KeepAliveInterval` value.                                                                                              |
| `HandshakeTimeout`          | 15 seconds              | If the client doesn't send an initial handshake message within this time interval, the connection is closed. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval`         | 15 seconds              | If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing `KeepAliveInterval`, change the `ServerTimeout` or `serverTimeoutInMilliseconds` setting on the client. The recommended `ServerTimeout` or `serverTimeoutInMilliseconds` value is double the `KeepAliveInterval` value.                                          |
| `SupportedProtocols`        | All installed protocols | Protocols supported by this hub. By default, all protocols registered on the server are allowed. Protocols can be removed from this list to disable specific protocols for individual hubs.                                                                                                                                                                                                                |
| `EnableDetailedErrors`      | `false`                 | If `true`, detailed exception messages are returned to clients when an exception is thrown in a Hub method. The default is `false` because these exception messages can contain sensitive information.                                                                                                                                                                                                     |
| `StreamBufferCapacity`      | `10`                    | The maximum number of items that can be buffered for client upload streams. If this limit is reached, the processing of invocations is blocked until the server processes stream items.                                                                                                                                                                                                                    |
| `MaximumReceiveMessageSize` | 32 KB                   | Maximum size of a single incoming hub message. Increasing the value may increase the risk of [Denial of service (DoS) attacks](https://developer.mozilla.org/docs/Glossary/DOS_attack).                                                                                                                                                                                                                    |

Options can be configured for all hubs by providing an options delegate to the `AddSignalR` call in `Startup.ConfigureServices`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR(hubOptions =>
    {
        hubOptions.EnableDetailedErrors = true;
        hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
    });
}
```

Options for a single hub override the global options provided in `AddSignalR` and can be configured using [AddHubOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addhuboptions):

```
services.AddSignalR().AddHubOptions<ChatHub>(options =>
{
    options.EnableDetailedErrors = true;
});
```

### Advanced HTTP configuration options

Use `HttpConnectionDispatcherOptions` to configure advanced settings related to transports and memory buffer management. These options are configured by passing a delegate to [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.hubendpointroutebuilderextensions.maphub) in `Startup.Configure`.

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chathub", options =>
        {
            options.Transports =
                HttpTransportType.WebSockets |
                HttpTransportType.LongPolling;
        });
    });
}
```

The following table describes options for configuring ASP.NET Core SignalR's advanced HTTP options.

| Option                     | Default Value                                                                         | Description                                                                                                                                                                                                                                                    |
| -------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ApplicationMaxBufferSize` | 32 KB                                                                                 | The maximum number of bytes received from the client that the server buffers before applying backpressure. Increasing this value allows the server to receive larger messages more quickly without applying backpressure, but can increase memory consumption. |
| `AuthorizationData`        | Data automatically gathered from the `Authorize` attributes applied to the Hub class. | A list of [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata) objects used to determine if a client is authorized to connect to the hub.                                                                                     |
| `TransportMaxBufferSize`   | 32 KB                                                                                 | The maximum number of bytes sent by the app that the server buffers before observing backpressure. Increasing this value allows the server to buffer larger messages more quickly without awaiting backpressure, but can increase memory consumption.          |
| `Transports`               | All Transports are enabled.                                                           | A bit flags enum of `HttpTransportType` values that can restrict the transports a client can use to connect.                                                                                                                                                   |
| `LongPolling`              | See below.                                                                            | Additional options specific to the Long Polling transport.                                                                                                                                                                                                     |
| `WebSockets`               | See below.                                                                            | Additional options specific to the WebSockets transport.                                                                                                                                                                                                       |
| `MinimumProtocolVersion`   | 0                                                                                     | Specify the minimum version of the negotiate protocol. This is used to limit clients to newer versions.                                                                                                                                                        |

The Long Polling transport has additional options that can be configured using the `LongPolling` property:

| Option        | Default Value | Description                                                                                                                                                                                                   |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PollTimeout` | 90 seconds    | The maximum amount of time the server waits for a message to send to the client before terminating a single poll request. Decreasing this value causes the client to issue new poll requests more frequently. |

The WebSocket transport has additional options that can be configured using the `WebSockets` property:

| Option                | Default Value | Description                                                                                                                                                                                              |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CloseTimeout`        | 5 seconds     | After the server closes, if the client fails to close within this time interval, the connection is terminated.                                                                                           |
| `SubProtocolSelector` | `null`        | A delegate that can be used to set the `Sec-WebSocket-Protocol` header to a custom value. The delegate receives the values requested by the client as input and is expected to return the desired value. |

## Configure client options

Client options can be configured on the `HubConnectionBuilder` type (available in the .NET and JavaScript clients). It's also available in the Java client, but the `HttpHubConnectionBuilder` subclass is what contains the builder configuration options, as well as on the `HubConnection` itself.

### Configure logging

Logging is configured in the .NET Client using the `ConfigureLogging` method. Logging providers and filters can be registered in the same way as they are on the server. See the [Logging in ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) documentation for more information.

Note

In order to register Logging providers, you must install the necessary packages. See the [Built-in logging providers](../fundamentals/logging/?view=aspnetcore-10.0#built-in-logging-providers) section of the docs for a full list.

For example, to enable Console logging, install the `Microsoft.Extensions.Logging.Console` NuGet package. Call the `AddConsole` extension method:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub")
    .ConfigureLogging(logging => {
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddConsole();
    })
    .Build();
```

In the JavaScript client, a similar `configureLogging` method exists. Provide a `LogLevel` value indicating the minimum level of log messages to produce. Logs are written to the browser console window.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

Instead of a `LogLevel` value, you can also provide a `string` value representing a log level name. This is useful when configuring SignalR logging in environments where you don't have access to the `LogLevel` constants.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging("warn")
    .build();
```

The following table lists the available log levels. The value you provide to `configureLogging` sets the **minimum** log level that will be logged. Messages logged at this level, **or the levels listed after it in the table**, will be logged.

| String                      | LogLevel               |
| --------------------------- | ---------------------- |
| `trace`                     | `LogLevel.Trace`       |
| `debug`                     | `LogLevel.Debug`       |
| `info` **or** `information` | `LogLevel.Information` |
| `warn` **or** `warning`     | `LogLevel.Warning`     |
| `error`                     | `LogLevel.Error`       |
| `critical`                  | `LogLevel.Critical`    |
| `none`                      | `LogLevel.None`        |

Note

To disable logging entirely, specify `signalR.LogLevel.None` in the `configureLogging` method.

For more information on logging, see the [SignalR Diagnostics documentation](diagnostics?view=aspnetcore-10.0).

The SignalR Java client uses the [SLF4J](https://www.slf4j.org/) library for logging. It's a high-level logging API that allows users of the library to choose their own specific logging implementation by bringing in a specific logging dependency. The following code snippet shows how to use `java.util.logging` with the SignalR Java client.

```
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
```

If you don't configure logging in your dependencies, SLF4J loads a default no-operation logger with the following warning message:

```
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

This can safely be ignored.

### Configure allowed transports

The transports used by SignalR can be configured in the `WithUrl` call (`withUrl` in JavaScript). A bitwise-OR of the values of `HttpTransportType` can be used to restrict the client to only use the specified transports. All transports are enabled by default.

For example, to disable the Server-Sent Events transport, but allow WebSockets and Long Polling connections:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
    .Build();
```

In the JavaScript client, transports are configured by setting the `transport` field on the options object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling })
    .build();
```

In this version of the Java client WebSockets is the only available transport.

In the Java client, the transport is selected with the `withTransport` method on the `HttpHubConnectionBuilder`. The Java client defaults to using the WebSockets transport.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withTransport(TransportEnum.WEBSOCKETS)
    .build();
```

Note

The SignalR Java client doesn't support transport fallback yet.

### Configure bearer authentication

To provide authentication data along with SignalR requests, use the `AccessTokenProvider` option (`accessTokenFactory` in JavaScript) to specify a function that returns the desired access token. In the .NET Client, this access token is passed in as an HTTP "Bearer Authentication" token (Using the `Authorization` header with a type of `Bearer`). In the JavaScript client, the access token is used as a Bearer token, **except** in a few cases where browser APIs restrict the ability to apply headers (specifically, in Server-Sent Events and WebSockets requests). In these cases, the access token is provided as a query string value `access_token`.

In the .NET client, the `AccessTokenProvider` option can be specified using the options delegate in `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.AccessTokenProvider = async () => {
            // Get and return the access token.
        };
    })
    .Build();
```

In the JavaScript client, the access token is configured by setting the `accessTokenFactory` field on the options object in `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        accessTokenFactory: () => {
            // Get and return the access token.
            // This function can return a JavaScript Promise if asynchronous
            // logic is required to retrieve the access token.
        }
    })
    .build();
```

In the SignalR Java client, you can configure a bearer token to use for authentication by providing an access token factory to the [HttpHubConnectionBuilder](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder?view=aspnet-signalr-java\&preserve-view=true). Use [withAccessTokenFactory](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder.withaccesstokenprovider?view=aspnet-signalr-java\&preserve-view=true) to provide an [RxJava](https://github.com/ReactiveX/RxJava) [Single\<String>](https://reactivex.io/documentation/single.html). With a call to [Single.defer](https://reactivex.io/RxJava/javadoc/io/reactivex/Single.html#defer-java.util.concurrent.Callable-), you can write logic to produce access tokens for your client.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withAccessTokenProvider(Single.defer(() -> {
        // Your logic here.
        return Single.just("An Access Token");
    })).build();
```

### Configure timeout and keep-alive options

Additional options for configuring timeout and keep-alive behavior are available on the `HubConnection` object itself:

* [.NET](#tabpanel_1_dotnet)
* [JavaScript](#tabpanel_1_javascript)
* [Java](#tabpanel_1_java)

| Option              | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ServerTimeout`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `Closed` event (`onclose` in JavaScript). This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `HandshakeTimeout`  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `Closed` event (`onclose` in JavaScript). This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval` | 15 seconds                       | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                                           |

In the .NET Client, timeout values are specified as `TimeSpan` values.

| Option                            | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serverTimeoutInMilliseconds`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onclose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive. |
| `keepAliveIntervalInMilliseconds` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                 |

| Option                                          | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getServerTimeout` / `setServerTimeout`         | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onClose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `withHandshakeResponseTimeout`                  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `onClose` event. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the Handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `getKeepAliveInterval` / `setKeepAliveInterval` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                  |

### Configure additional options

Additional options can be configured in the `WithUrl` (`withUrl` in JavaScript) method on `HubConnectionBuilder` or on the various configuration APIs on the `HttpHubConnectionBuilder` in the Java client:

* [.NET](#tabpanel_2_dotnet)
* [JavaScript](#tabpanel_2_javascript)
* [Java](#tabpanel_2_java)

| .NET Option                 | Default value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AccessTokenProvider`       | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `SkipNegotiation`           | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                                                                                                                                                                                                                                                            |
| `ClientCertificates`        | Empty         | A collection of TLS certificates to send to authenticate requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `Cookies`                   | Empty         | A collection of HTTP cookies to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `Credentials`               | Empty         | Credentials to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `CloseTimeout`              | 5 seconds     | WebSockets only. The maximum amount of time the client waits after closing for the server to acknowledge the close request. If the server doesn't acknowledge the close within this time, the client disconnects.                                                                                                                                                                                                                                                                                                                                               |
| `Headers`                   | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `HttpMessageHandlerFactory` | `null`        | A delegate that can be used to configure or replace the `HttpMessageHandler` used to send HTTP requests. Not used for WebSocket connections. This delegate must return a non-null value, and it receives the default value as a parameter. Either modify settings on that default value and return it, or return a new `HttpMessageHandler` instance. **When replacing the handler make sure to copy the settings you want to keep from the provided handler, otherwise, the configured options (such as Cookies and Headers) won't apply to the new handler.** |
| `Proxy`                     | `null`        | An HTTP proxy to use when sending HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `UseDefaultCredentials`     | `false`       | Set this boolean to send the default credentials for HTTP and WebSockets requests. This enables the use of Windows authentication.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `WebSocketConfiguration`    | `null`        | A delegate that can be used to configure additional WebSocket options. Receives an instance of [ClientWebSocketOptions](/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions) that can be used to configure the options.                                                                                                                                                                                                                                                                                                                              |

| JavaScript Option    | Default Value | Description                                                                                                                                                                                          |
| -------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessTokenFactory` | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `transport`          | `null`        | An [HttpTransportType](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype) value specifying the transport to use for the connection.                                          |
| `logMessageContent`  | `null`        | Set to `true` to log the bytes/chars of messages sent and received by the client.                                                                                                                    |
| `skipNegotiation`    | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |

| Java Option                | Default Value | Description                                                                                                                                                                                          |
| -------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withAccessTokenProvider`  | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `shouldSkipNegotiate`      | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |
| `withHeader` `withHeaders` | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                    |

In the .NET Client, these options can be modified by the options delegate provided to `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.Headers["Foo"] = "Bar";
        options.Cookies.Add(new Cookie(/* ... */);
        options.ClientCertificates.Add(/* ... */);
    })
    .Build();
```

In the JavaScript Client, these options can be provided in a JavaScript object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        skipNegotiation: true,
        transport: signalR.HttpTransportType.WebSockets
    })
    .build();
```

In the Java client, these options can be configured with the methods on the `HttpHubConnectionBuilder` returned from the `HubConnectionBuilder.create("HUB URL")`

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
        .withHeader("Foo", "Bar")
        .shouldSkipNegotiate(true)
        .withHandshakeResponseTimeout(30*1000)
        .build();
```

## Related content

* [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](supported-platforms?view=aspnetcore-10.0)

## JSON/MessagePack serialization options

ASP.NET Core SignalR supports two protocols for encoding messages: [JSON](https://www.json.org/) and [MessagePack](https://msgpack.org/index.html). Each protocol has serialization configuration options.

JSON serialization can be configured on the server using the [AddJsonProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jsonprotocoldependencyinjectionextensions.addjsonprotocol) extension method. `AddJsonProtocol` can be added after [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) in `Startup.ConfigureServices`. The `AddJsonProtocol` method takes a delegate that receives an `options` object. The [PayloadSerializerOptions](/en-us/dotnet/api/microsoft.aspnetcore.signalr.jsonhubprotocoloptions.payloadserializeroptions) property on that object is a `System.Text.Json` [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object that can be used to configure serialization of arguments and return values. For more information, see the [System.Text.Json documentation](/en-us/dotnet/api/system.text.json).

For example, to configure the serializer to not change the casing of property names, rather than the default [camel case](https://wikipedia.org/wiki/Camel_case) names, use the following code in `Startup.ConfigureServices`:

```
services.AddSignalR()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    });
```

In the .NET client, the same `AddJsonProtocol` extension method exists on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). The `Microsoft.Extensions.DependencyInjection` namespace must be imported to resolve the extension method:

```
// At the top of the file:
using Microsoft.Extensions.DependencyInjection;

// When constructing your connection:
var connection = new HubConnectionBuilder()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    })
    .Build();
```

Note

It's not possible to configure JSON serialization in the JavaScript client at this time.

### Switch to Newtonsoft.Json

If you need features of `Newtonsoft.Json` that aren't supported in `System.Text.Json`, see [Switch to `Newtonsoft.Json`](../migration/22-to-30?view=aspnetcore-10.0#switch-to-newtonsoftjson).

### MessagePack serialization options

MessagePack serialization can be configured by providing a delegate to the [AddMessagePackProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msgpackprotocoldependencyinjectionextensions.addmessagepackprotocol) call. See [MessagePack in SignalR](messagepackhubprotocol?view=aspnetcore-10.0) for more details.

Note

It's not possible to configure MessagePack serialization in the JavaScript client at this time.

## Configure server options

The following table describes options for configuring SignalR hubs.

| Option                                | Default Value           | Description                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ClientTimeoutInterval`               | 30 seconds              | The server considers the client disconnected if it hasn't received a message (including keep-alive) in this interval. It could take longer than this timeout interval for the client to be marked disconnected due to how this is implemented. The recommended value is double the `KeepAliveInterval` value.                                                                                              |
| `HandshakeTimeout`                    | 15 seconds              | If the client doesn't send an initial handshake message within this time interval, the connection is closed. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval`                   | 15 seconds              | If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing `KeepAliveInterval`, change the `ServerTimeout` or `serverTimeoutInMilliseconds` setting on the client. The recommended `ServerTimeout` or `serverTimeoutInMilliseconds` value is double the `KeepAliveInterval` value.                                          |
| `SupportedProtocols`                  | All installed protocols | Protocols supported by this hub. By default, all protocols registered on the server are allowed. Protocols can be removed from this list to disable specific protocols for individual hubs.                                                                                                                                                                                                                |
| `EnableDetailedErrors`                | `false`                 | If `true`, detailed exception messages are returned to clients when an exception is thrown in a Hub method. The default is `false` because these exception messages can contain sensitive information.                                                                                                                                                                                                     |
| `StreamBufferCapacity`                | `10`                    | The maximum number of items that can be buffered for client upload streams. If this limit is reached, the processing of invocations is blocked until the server processes stream items.                                                                                                                                                                                                                    |
| `MaximumReceiveMessageSize`           | 32 KB                   | Maximum size of a single incoming hub message. Increasing the value may increase the risk of [Denial of service (DoS) attacks](https://developer.mozilla.org/docs/Glossary/DOS_attack).                                                                                                                                                                                                                    |
| `MaximumParallelInvocationsPerClient` | 1                       | The maximum number of hub methods that each client can call in parallel before queueing.                                                                                                                                                                                                                                                                                                                   |

Note

`MaximumParallelInvocationsPerClient` does not apply to streaming hub invocations. Streaming invocations are expected to be long-running and can run concurrently. Use [hub filters](hub-filters?view=aspnetcore-10.0) to enforce per-connection streaming concurrency limits.

Options can be configured for all hubs by providing an options delegate to the `AddSignalR` call in `Startup.ConfigureServices`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR(hubOptions =>
    {
        hubOptions.EnableDetailedErrors = true;
        hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
    });
}
```

Options for a single hub override the global options provided in `AddSignalR` and can be configured using [AddHubOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addhuboptions):

```
services.AddSignalR().AddHubOptions<ChatHub>(options =>
{
    options.EnableDetailedErrors = true;
});
```

### Advanced HTTP configuration options

Use `HttpConnectionDispatcherOptions` to configure advanced settings related to transports and memory buffer management. These options are configured by passing a delegate to [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.hubendpointroutebuilderextensions.maphub) in `Startup.Configure`.

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chathub", options =>
        {
            options.Transports =
                HttpTransportType.WebSockets |
                HttpTransportType.LongPolling;
        });
    });
}
```

The following table describes options for configuring ASP.NET Core SignalR's advanced HTTP options.

| Option                     | Default Value                                                                         | Description                                                                                                                                                                                                                                                    |
| -------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ApplicationMaxBufferSize` | 32 KB                                                                                 | The maximum number of bytes received from the client that the server buffers before applying backpressure. Increasing this value allows the server to receive larger messages more quickly without applying backpressure, but can increase memory consumption. |
| `AuthorizationData`        | Data automatically gathered from the `Authorize` attributes applied to the Hub class. | A list of [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata) objects used to determine if a client is authorized to connect to the hub.                                                                                     |
| `TransportMaxBufferSize`   | 32 KB                                                                                 | The maximum number of bytes sent by the app that the server buffers before observing backpressure. Increasing this value allows the server to buffer larger messages more quickly without awaiting backpressure, but can increase memory consumption.          |
| `Transports`               | All Transports are enabled.                                                           | A bit flags enum of `HttpTransportType` values that can restrict the transports a client can use to connect.                                                                                                                                                   |
| `LongPolling`              | See below.                                                                            | Additional options specific to the Long Polling transport.                                                                                                                                                                                                     |
| `WebSockets`               | See below.                                                                            | Additional options specific to the WebSockets transport.                                                                                                                                                                                                       |
| `MinimumProtocolVersion`   | 0                                                                                     | Specify the minimum version of the negotiate protocol. This is used to limit clients to newer versions.                                                                                                                                                        |

The Long Polling transport has additional options that can be configured using the `LongPolling` property:

| Option        | Default Value | Description                                                                                                                                                                                                   |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PollTimeout` | 90 seconds    | The maximum amount of time the server waits for a message to send to the client before terminating a single poll request. Decreasing this value causes the client to issue new poll requests more frequently. |

The WebSocket transport has additional options that can be configured using the `WebSockets` property:

| Option                | Default Value | Description                                                                                                                                                                                              |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CloseTimeout`        | 5 seconds     | After the server closes, if the client fails to close within this time interval, the connection is terminated.                                                                                           |
| `SubProtocolSelector` | `null`        | A delegate that can be used to set the `Sec-WebSocket-Protocol` header to a custom value. The delegate receives the values requested by the client as input and is expected to return the desired value. |

## Configure client options

Client options can be configured on the `HubConnectionBuilder` type (available in the .NET and JavaScript clients). It's also available in the Java client, but the `HttpHubConnectionBuilder` subclass is what contains the builder configuration options, as well as on the `HubConnection` itself.

### Configure logging

Logging is configured in the .NET Client using the `ConfigureLogging` method. Logging providers and filters can be registered in the same way as they are on the server. See the [Logging in ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) documentation for more information.

Note

In order to register Logging providers, you must install the necessary packages. See the [Built-in logging providers](../fundamentals/logging/?view=aspnetcore-10.0#built-in-logging-providers) section of the docs for a full list.

For example, to enable Console logging, install the `Microsoft.Extensions.Logging.Console` NuGet package. Call the `AddConsole` extension method:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub")
    .ConfigureLogging(logging => {
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddConsole();
    })
    .Build();
```

In the JavaScript client, a similar `configureLogging` method exists. Provide a `LogLevel` value indicating the minimum level of log messages to produce. Logs are written to the browser console window.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

Instead of a `LogLevel` value, you can also provide a `string` value representing a log level name. This is useful when configuring SignalR logging in environments where you don't have access to the `LogLevel` constants.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging("warn")
    .build();
```

The following table lists the available log levels. The value you provide to `configureLogging` sets the **minimum** log level that will be logged. Messages logged at this level, **or the levels listed after it in the table**, will be logged.

| String                      | LogLevel               |
| --------------------------- | ---------------------- |
| `trace`                     | `LogLevel.Trace`       |
| `debug`                     | `LogLevel.Debug`       |
| `info` **or** `information` | `LogLevel.Information` |
| `warn` **or** `warning`     | `LogLevel.Warning`     |
| `error`                     | `LogLevel.Error`       |
| `critical`                  | `LogLevel.Critical`    |
| `none`                      | `LogLevel.None`        |

Note

To disable logging entirely, specify `signalR.LogLevel.None` in the `configureLogging` method.

For more information on logging, see the [SignalR Diagnostics documentation](diagnostics?view=aspnetcore-10.0).

The SignalR Java client uses the [SLF4J](https://www.slf4j.org/) library for logging. It's a high-level logging API that allows users of the library to choose their own specific logging implementation by bringing in a specific logging dependency. The following code snippet shows how to use `java.util.logging` with the SignalR Java client.

```
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
```

If you don't configure logging in your dependencies, SLF4J loads a default no-operation logger with the following warning message:

```
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

This can safely be ignored.

### Configure allowed transports

The transports used by SignalR can be configured in the `WithUrl` call (`withUrl` in JavaScript). A bitwise-OR of the values of `HttpTransportType` can be used to restrict the client to only use the specified transports. All transports are enabled by default.

For example, to disable the Server-Sent Events transport, but allow WebSockets and Long Polling connections:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
    .Build();
```

In the JavaScript client, transports are configured by setting the `transport` field on the options object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling })
    .build();
```

In this version of the Java client WebSockets is the only available transport.

In the Java client, the transport is selected with the `withTransport` method on the `HttpHubConnectionBuilder`. The Java client defaults to using the WebSockets transport.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withTransport(TransportEnum.WEBSOCKETS)
    .build();
```

Note

The SignalR Java client doesn't support transport fallback yet.

### Configure bearer authentication

To provide authentication data along with SignalR requests, use the `AccessTokenProvider` option (`accessTokenFactory` in JavaScript) to specify a function that returns the desired access token. In the .NET Client, this access token is passed in as an HTTP "Bearer Authentication" token (Using the `Authorization` header with a type of `Bearer`). In the JavaScript client, the access token is used as a Bearer token, **except** in a few cases where browser APIs restrict the ability to apply headers (specifically, in Server-Sent Events and WebSockets requests). In these cases, the access token is provided as a query string value `access_token`.

In the .NET client, the `AccessTokenProvider` option can be specified using the options delegate in `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.AccessTokenProvider = async () => {
            // Get and return the access token.
        };
    })
    .Build();
```

In the JavaScript client, the access token is configured by setting the `accessTokenFactory` field on the options object in `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        accessTokenFactory: () => {
            // Get and return the access token.
            // This function can return a JavaScript Promise if asynchronous
            // logic is required to retrieve the access token.
        }
    })
    .build();
```

In the SignalR Java client, you can configure a bearer token to use for authentication by providing an access token factory to the [HttpHubConnectionBuilder](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder?view=aspnet-signalr-java\&preserve-view=true). Use [withAccessTokenFactory](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder.withaccesstokenprovider?view=aspnet-signalr-java\&preserve-view=true) to provide an [RxJava](https://github.com/ReactiveX/RxJava) [Single\<String>](https://reactivex.io/documentation/single.html). With a call to [Single.defer](https://reactivex.io/RxJava/javadoc/io/reactivex/Single.html#defer-java.util.concurrent.Callable-), you can write logic to produce access tokens for your client.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withAccessTokenProvider(Single.defer(() -> {
        // Your logic here.
        return Single.just("An Access Token");
    })).build();
```

### Configure timeout and keep-alive options

Additional options for configuring timeout and keep-alive behavior are available on the `HubConnection` object itself:

* [.NET](#tabpanel_1_dotnet)
* [JavaScript](#tabpanel_1_javascript)
* [Java](#tabpanel_1_java)

| Option              | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ServerTimeout`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `Closed` event (`onclose` in JavaScript). This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `HandshakeTimeout`  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `Closed` event (`onclose` in JavaScript). This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval` | 15 seconds                       | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                                           |

In the .NET Client, timeout values are specified as `TimeSpan` values.

| Option                            | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serverTimeoutInMilliseconds`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onclose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive. |
| `keepAliveIntervalInMilliseconds` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                 |

| Option                                          | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getServerTimeout` / `setServerTimeout`         | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onClose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `withHandshakeResponseTimeout`                  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `onClose` event. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the Handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `getKeepAliveInterval` / `setKeepAliveInterval` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                  |

### Configure additional options

Additional options can be configured in the `WithUrl` (`withUrl` in JavaScript) method on `HubConnectionBuilder` or on the various configuration APIs on the `HttpHubConnectionBuilder` in the Java client:

* [.NET](#tabpanel_2_dotnet)
* [JavaScript](#tabpanel_2_javascript)
* [Java](#tabpanel_2_java)

| .NET Option                 | Default value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AccessTokenProvider`       | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `SkipNegotiation`           | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                                                                                                                                                                                                                                                            |
| `ClientCertificates`        | Empty         | A collection of TLS certificates to send to authenticate requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `Cookies`                   | Empty         | A collection of HTTP cookies to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `Credentials`               | Empty         | Credentials to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `CloseTimeout`              | 5 seconds     | WebSockets only. The maximum amount of time the client waits after closing for the server to acknowledge the close request. If the server doesn't acknowledge the close within this time, the client disconnects.                                                                                                                                                                                                                                                                                                                                               |
| `Headers`                   | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `HttpMessageHandlerFactory` | `null`        | A delegate that can be used to configure or replace the `HttpMessageHandler` used to send HTTP requests. Not used for WebSocket connections. This delegate must return a non-null value, and it receives the default value as a parameter. Either modify settings on that default value and return it, or return a new `HttpMessageHandler` instance. **When replacing the handler make sure to copy the settings you want to keep from the provided handler, otherwise, the configured options (such as Cookies and Headers) won't apply to the new handler.** |
| `Proxy`                     | `null`        | An HTTP proxy to use when sending HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `UseDefaultCredentials`     | `false`       | Set this boolean to send the default credentials for HTTP and WebSockets requests. This enables the use of Windows authentication.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `WebSocketConfiguration`    | `null`        | A delegate that can be used to configure additional WebSocket options. Receives an instance of [ClientWebSocketOptions](/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions) that can be used to configure the options.                                                                                                                                                                                                                                                                                                                              |

| JavaScript Option    | Default Value | Description                                                                                                                                                                                                                                                                                                                   |
| -------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessTokenFactory` | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                             |
| `transport`          | `null`        | An [HttpTransportType](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype) value specifying the transport to use for the connection.                                                                                                                                                                   |
| `headers`            | `null`        | Dictionary of headers sent with every HTTP request. Sending headers in the browser doesn't work for WebSockets or the [ServerSentEvents](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype#microsoft-aspnetcore-http-connections-httptransporttype-serversentevents) stream.                          |
| `logMessageContent`  | `null`        | Set to `true` to log the bytes/chars of messages sent and received by the client.                                                                                                                                                                                                                                             |
| `skipNegotiation`    | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                          |
| `withCredentials`    | `true`        | Specifies whether credentials will be sent with the CORS request. Azure App Service uses cookies for sticky sessions and needs this option enabled to work correctly. For more info on CORS with SignalR, see [Security considerations in ASP.NET Core SignalR](security?view=aspnetcore-10.0#cross-origin-resource-sharing). |

| Java Option                | Default Value | Description                                                                                                                                                                                          |
| -------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withAccessTokenProvider`  | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `shouldSkipNegotiate`      | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |
| `withHeader` `withHeaders` | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                    |

In the .NET Client, these options can be modified by the options delegate provided to `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.Headers["Foo"] = "Bar";
        options.SkipNegotiation = true;
        options.Transports = HttpTransportType.WebSockets;
        options.Cookies.Add(new Cookie(/* ... */);
        options.ClientCertificates.Add(/* ... */);
    })
    .Build();
```

In the JavaScript Client, these options can be provided in a JavaScript object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        // "Foo: Bar" will not be sent with WebSockets or Server-Sent Events requests
        headers: { "Foo": "Bar" },
        transport: signalR.HttpTransportType.LongPolling 
    })
    .build();
```

In the Java client, these options can be configured with the methods on the `HttpHubConnectionBuilder` returned from the `HubConnectionBuilder.create("HUB URL")`

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
        .withHeader("Foo", "Bar")
        .shouldSkipNegotiate(true)
        .withHandshakeResponseTimeout(30*1000)
        .build();
```

## Related content

* [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](supported-platforms?view=aspnetcore-10.0)

## JSON/MessagePack serialization options

ASP.NET Core SignalR supports two protocols for encoding messages: [JSON](https://www.json.org/) and [MessagePack](https://msgpack.org/index.html). Each protocol has serialization configuration options.

JSON serialization can be configured on the server using the [AddJsonProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jsonprotocoldependencyinjectionextensions.addjsonprotocol) extension method. `AddJsonProtocol` can be added after [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) in `Program.cs`. The `AddJsonProtocol` method takes a delegate that receives an `options` object. The [PayloadSerializerOptions](/en-us/dotnet/api/microsoft.aspnetcore.signalr.jsonhubprotocoloptions.payloadserializeroptions) property on that object is a `System.Text.Json` [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object that can be used to configure serialization of arguments and return values. For more information, see the [System.Text.Json documentation](/en-us/dotnet/api/system.text.json).

For example, to configure the serializer to not change the casing of property names, rather than the default [camel case](https://wikipedia.org/wiki/Camel_case) names, use the following code in `Program.cs`:

```
builder.Services.AddSignalR()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    });
```

In the .NET client, the same `AddJsonProtocol` extension method exists on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). The `Microsoft.Extensions.DependencyInjection` namespace must be imported to resolve the extension method:

```
// At the top of the file:
using Microsoft.Extensions.DependencyInjection;

// When constructing your connection:
var connection = new HubConnectionBuilder()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    })
    .Build();
```

Note

It's not possible to configure JSON serialization in the JavaScript client at this time.

### Switch to Newtonsoft.Json

If you need features of `Newtonsoft.Json` that aren't supported in `System.Text.Json`, see [Switch to `Newtonsoft.Json`](../migration/22-to-30?view=aspnetcore-10.0#switch-to-newtonsoftjson).

### MessagePack serialization options

MessagePack serialization can be configured by providing a delegate to the [AddMessagePackProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msgpackprotocoldependencyinjectionextensions.addmessagepackprotocol) call. See [MessagePack in SignalR](messagepackhubprotocol?view=aspnetcore-10.0) for more details.

Note

It's not possible to configure MessagePack serialization in the JavaScript client at this time.

## Configure server options

The following table describes options for configuring SignalR hubs:

| Option                                | Default Value           | Description                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ClientTimeoutInterval`               | 30 seconds              | The server considers the client disconnected if it hasn't received a message (including keep-alive) in this interval. It could take longer than this timeout interval for the client to be marked disconnected due to how this is implemented. The recommended value is double the `KeepAliveInterval` value.                                                                                              |
| `HandshakeTimeout`                    | 15 seconds              | If the client doesn't send an initial handshake message within this time interval, the connection is closed. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval`                   | 15 seconds              | If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing `KeepAliveInterval`, change the `ServerTimeout` or `serverTimeoutInMilliseconds` setting on the client. The recommended `ServerTimeout` or `serverTimeoutInMilliseconds` value is double the `KeepAliveInterval` value.                                          |
| `SupportedProtocols`                  | All installed protocols | Protocols supported by this hub. By default, all protocols registered on the server are allowed. Protocols can be removed from this list to disable specific protocols for individual hubs.                                                                                                                                                                                                                |
| `EnableDetailedErrors`                | `false`                 | If `true`, detailed exception messages are returned to clients when an exception is thrown in a Hub method. The default is `false` because these exception messages can contain sensitive information.                                                                                                                                                                                                     |
| `StreamBufferCapacity`                | `10`                    | The maximum number of items that can be buffered for client upload streams. If this limit is reached, the processing of invocations is blocked until the server processes stream items.                                                                                                                                                                                                                    |
| `MaximumReceiveMessageSize`           | 32 KB                   | Maximum size of a single incoming hub message. Increasing the value may increase the risk of [Denial of service (DoS) attacks](https://developer.mozilla.org/docs/Glossary/DOS_attack).                                                                                                                                                                                                                    |
| `MaximumParallelInvocationsPerClient` | 1                       | The maximum number of hub methods that each client can call in parallel before queueing.                                                                                                                                                                                                                                                                                                                   |

Note

`MaximumParallelInvocationsPerClient` does not apply to streaming hub invocations. Streaming invocations are expected to be long-running and can run concurrently. Use [hub filters](hub-filters?view=aspnetcore-10.0) to enforce per-connection streaming concurrency limits.

Options can be configured for all hubs by providing an options delegate to the `AddSignalR` call in `Program.cs`.

```
builder.Services.AddSignalR(hubOptions =>
{
    hubOptions.EnableDetailedErrors = true;
    hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
});
```

Options for a single hub override the global options provided in `AddSignalR` and can be configured using [AddHubOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addhuboptions):

```
builder.Services.AddSignalR().AddHubOptions<ChatHub>(options =>
{
    options.EnableDetailedErrors = true;
});
```

### Advanced HTTP configuration options

Use `HttpConnectionDispatcherOptions` to configure advanced settings related to transports and memory buffer management. These options are configured by passing a delegate to [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.hubendpointroutebuilderextensions.maphub) in `Program.cs`.

```
using Microsoft.AspNetCore.Http.Connections;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chathub", options =>
{
    options.Transports =
        HttpTransportType.WebSockets |
        HttpTransportType.LongPolling;
}
);
app.Run();
```

The following table describes options for configuring ASP.NET Core SignalR's advanced HTTP options.

| Option                            | Default Value                                                                         | Description                                                                                                                                                                                                                                              |
| --------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ApplicationMaxBufferSize`        | 64 KB                                                                                 | The maximum number of bytes received from the client that the server buffers before applying backpressure. Increasing this value allows the server to receive larger messages faster without applying backpressure, but can increase memory consumption. |
| `TransportMaxBufferSize`          | 64 KB                                                                                 | The maximum number of bytes sent by the app that the server buffers before observing backpressure. Increasing this value allows the server to buffer larger messages faster without awaiting backpressure, but can increase memory consumption.          |
| `AuthorizationData`               | Data automatically gathered from the `Authorize` attributes applied to the Hub class. | A list of [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata) objects used to determine if a client is authorized to connect to the hub.                                                                               |
| `Transports`                      | All Transports are enabled.                                                           | A bit flags enum of `HttpTransportType` values that can restrict the transports a client can use to connect.                                                                                                                                             |
| `LongPolling`                     | See below.                                                                            | Additional options specific to the Long Polling transport.                                                                                                                                                                                               |
| `WebSockets`                      | See below.                                                                            | Additional options specific to the WebSockets transport.                                                                                                                                                                                                 |
| `MinimumProtocolVersion`          | 0                                                                                     | Specify the minimum version of the negotiate protocol. This is used to limit clients to newer versions.                                                                                                                                                  |
| `TransportSendTimeout`            | 10 seconds                                                                            | The maximum amount of time the transport waits for a single send to a client to complete. If a send exceeds this timeout, the connection is closed.                                                                                                      |
| `CloseOnAuthenticationExpiration` | false                                                                                 | Set this option to enable authentication expiration tracking which will close connections when a token expires.                                                                                                                                          |

The Long Polling transport has additional options that can be configured using the `LongPolling` property:

| Option        | Default Value | Description                                                                                                                                                                                                   |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PollTimeout` | 90 seconds    | The maximum amount of time the server waits for a message to send to the client before terminating a single poll request. Decreasing this value causes the client to issue new poll requests more frequently. |

The WebSocket transport has additional options that can be configured using the `WebSockets` property:

| Option                | Default Value | Description                                                                                                                                                                                              |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CloseTimeout`        | 5 seconds     | After the server closes, if the client fails to close within this time interval, the connection is terminated.                                                                                           |
| `SubProtocolSelector` | `null`        | A delegate that can be used to set the `Sec-WebSocket-Protocol` header to a custom value. The delegate receives the values requested by the client as input and is expected to return the desired value. |

## Configure client options

Client options can be configured on the `HubConnectionBuilder` type (available in the .NET and JavaScript clients). It's also available in the Java client, but the `HttpHubConnectionBuilder` subclass is what contains the builder configuration options, as well as on the `HubConnection` itself.

### Configure logging

Logging is configured in the .NET Client using the `ConfigureLogging` method. Logging providers and filters can be registered in the same way as they are on the server. See the [Logging in ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) documentation for more information.

Note

In order to register Logging providers, you must install the necessary packages. See the [Built-in logging providers](../fundamentals/logging/?view=aspnetcore-10.0#built-in-logging-providers) section of the docs for a full list.

For example, to enable Console logging, install the `Microsoft.Extensions.Logging.Console` NuGet package. Call the `AddConsole` extension method:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub")
    .ConfigureLogging(logging => {
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddConsole();
    })
    .Build();
```

In the JavaScript client, a similar `configureLogging` method exists. Provide a `LogLevel` value indicating the minimum level of log messages to produce. Logs are written to the browser console window.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

Instead of a `LogLevel` value, you can also provide a `string` value representing a log level name. This is useful when configuring SignalR logging in environments where you don't have access to the `LogLevel` constants.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging("warn")
    .build();
```

The following table lists the available log levels. The value you provide to `configureLogging` sets the **minimum** log level that will be logged. Messages logged at this level, **or the levels listed after it in the table**, will be logged.

| String                      | LogLevel               |
| --------------------------- | ---------------------- |
| `trace`                     | `LogLevel.Trace`       |
| `debug`                     | `LogLevel.Debug`       |
| `info` **or** `information` | `LogLevel.Information` |
| `warn` **or** `warning`     | `LogLevel.Warning`     |
| `error`                     | `LogLevel.Error`       |
| `critical`                  | `LogLevel.Critical`    |
| `none`                      | `LogLevel.None`        |

Note

To disable logging entirely, specify `signalR.LogLevel.None` in the `configureLogging` method.

For more information on logging, see the [SignalR Diagnostics documentation](diagnostics?view=aspnetcore-10.0).

The SignalR Java client uses the [SLF4J](https://www.slf4j.org/) library for logging. It's a high-level logging API that allows users of the library to choose their own specific logging implementation by bringing in a specific logging dependency. The following code snippet shows how to use `java.util.logging` with the SignalR Java client.

```
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
```

If you don't configure logging in your dependencies, SLF4J loads a default no-operation logger with the following warning message:

```
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

This can safely be ignored.

### Configure allowed transports

The transports used by SignalR can be configured in the `WithUrl` call (`withUrl` in JavaScript). A bitwise-OR of the values of `HttpTransportType` can be used to restrict the client to only use the specified transports. All transports are enabled by default.

For example, to disable the Server-Sent Events transport, but allow WebSockets and Long Polling connections:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
    .Build();
```

In the JavaScript client, transports are configured by setting the `transport` field on the options object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling })
    .build();
```

In this version of the Java client WebSockets is the only available transport.

In the Java client, the transport is selected with the `withTransport` method on the `HttpHubConnectionBuilder`. The Java client defaults to using the WebSockets transport.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withTransport(TransportEnum.WEBSOCKETS)
    .build();
```

Note

The SignalR Java client doesn't support transport fallback yet.

### Configure bearer authentication

To provide authentication data along with SignalR requests, use the `AccessTokenProvider` option (`accessTokenFactory` in JavaScript) to specify a function that returns the desired access token. In the .NET Client, this access token is passed in as an HTTP "Bearer Authentication" token (Using the `Authorization` header with a type of `Bearer`). In the JavaScript client, the access token is used as a Bearer token, **except** in a few cases where browser APIs restrict the ability to apply headers (specifically, in Server-Sent Events and WebSockets requests). In these cases, the access token is provided as a query string value `access_token`.

In the .NET client, the `AccessTokenProvider` option can be specified using the options delegate in `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.AccessTokenProvider = async () => {
            // Get and return the access token.
        };
    })
    .Build();
```

In the JavaScript client, the access token is configured by setting the `accessTokenFactory` field on the options object in `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        accessTokenFactory: () => {
            // Get and return the access token.
            // This function can return a JavaScript Promise if asynchronous
            // logic is required to retrieve the access token.
        }
    })
    .build();
```

In the SignalR Java client, you can configure a bearer token to use for authentication by providing an access token factory to the [HttpHubConnectionBuilder](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder?view=aspnet-signalr-java\&preserve-view=true). Use [withAccessTokenFactory](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder.withaccesstokenprovider?view=aspnet-signalr-java\&preserve-view=true) to provide an [RxJava](https://github.com/ReactiveX/RxJava) [Single\<String>](https://reactivex.io/documentation/single.html). With a call to [Single.defer](https://reactivex.io/RxJava/javadoc/io/reactivex/Single.html#defer-java.util.concurrent.Callable-), you can write logic to produce access tokens for your client.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withAccessTokenProvider(Single.defer(() -> {
        // Your logic here.
        return Single.just("An Access Token");
    })).build();
```

### Configure timeout and keep-alive options

Additional options for configuring timeout and keep-alive behavior are available on the `HubConnection` object itself:

* [.NET](#tabpanel_1_dotnet)
* [JavaScript](#tabpanel_1_javascript)
* [Java](#tabpanel_1_java)

| Option              | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ServerTimeout`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `Closed` event (`onclose` in JavaScript). This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `HandshakeTimeout`  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `Closed` event (`onclose` in JavaScript). This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval` | 15 seconds                       | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                                           |

In the .NET Client, timeout values are specified as `TimeSpan` values.

| Option                            | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serverTimeoutInMilliseconds`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onclose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive. |
| `keepAliveIntervalInMilliseconds` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                 |

| Option                                          | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getServerTimeout` / `setServerTimeout`         | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onClose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `withHandshakeResponseTimeout`                  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `onClose` event. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the Handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `getKeepAliveInterval` / `setKeepAliveInterval` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                  |

### Configure additional options

Additional options can be configured in the `WithUrl` (`withUrl` in JavaScript) method on `HubConnectionBuilder` or on the various configuration APIs on the `HttpHubConnectionBuilder` in the Java client:

* [.NET](#tabpanel_2_dotnet)
* [JavaScript](#tabpanel_2_javascript)
* [Java](#tabpanel_2_java)

| .NET Option                 | Default value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AccessTokenProvider`       | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `SkipNegotiation`           | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                                                                                                                                                                                                                                                            |
| `ClientCertificates`        | Empty         | A collection of TLS certificates to send to authenticate requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `Cookies`                   | Empty         | A collection of HTTP cookies to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `Credentials`               | Empty         | Credentials to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `CloseTimeout`              | 5 seconds     | WebSockets only. The maximum amount of time the client waits after closing for the server to acknowledge the close request. If the server doesn't acknowledge the close within this time, the client disconnects.                                                                                                                                                                                                                                                                                                                                               |
| `Headers`                   | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `HttpMessageHandlerFactory` | `null`        | A delegate that can be used to configure or replace the `HttpMessageHandler` used to send HTTP requests. Not used for WebSocket connections. This delegate must return a non-null value, and it receives the default value as a parameter. Either modify settings on that default value and return it, or return a new `HttpMessageHandler` instance. **When replacing the handler make sure to copy the settings you want to keep from the provided handler, otherwise, the configured options (such as Cookies and Headers) won't apply to the new handler.** |
| `Proxy`                     | `null`        | An HTTP proxy to use when sending HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `UseDefaultCredentials`     | `false`       | Set this boolean to send the default credentials for HTTP and WebSockets requests. This enables the use of Windows authentication.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `WebSocketConfiguration`    | `null`        | A delegate that can be used to configure additional WebSocket options. Receives an instance of [ClientWebSocketOptions](/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions) that can be used to configure the options.                                                                                                                                                                                                                                                                                                                              |
| `ApplicationMaxBufferSize`  | 1 MB          | The maximum number of bytes received from the server that the client buffers before applying backpressure. Increasing this value allows the client to receive larger messages faster without applying backpressure, but can increase memory consumption.                                                                                                                                                                                                                                                                                                        |
| `TransportMaxBufferSize`    | 1 MB          | The maximum number of bytes sent by the user application that the client buffers before observing backpressure. Increasing this value allows the client to buffer larger messages faster without awaiting backpressure, but can increase memory consumption.                                                                                                                                                                                                                                                                                                    |

| JavaScript Option    | Default Value | Description                                                                                                                                                                                                                                                                                                                   |
| -------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessTokenFactory` | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                             |
| `transport`          | `null`        | An [HttpTransportType](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype) value specifying the transport to use for the connection.                                                                                                                                                                   |
| `headers`            | `null`        | Dictionary of headers sent with every HTTP request. Sending headers in the browser doesn't work for WebSockets or the [ServerSentEvents](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype#microsoft-aspnetcore-http-connections-httptransporttype-serversentevents) stream.                          |
| `logMessageContent`  | `null`        | Set to `true` to log the bytes/chars of messages sent and received by the client.                                                                                                                                                                                                                                             |
| `skipNegotiation`    | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                          |
| `withCredentials`    | `true`        | Specifies whether credentials will be sent with the CORS request. Azure App Service uses cookies for sticky sessions and needs this option enabled to work correctly. For more info on CORS with SignalR, see [Security considerations in ASP.NET Core SignalR](security?view=aspnetcore-10.0#cross-origin-resource-sharing). |
| `timeout`            | `100,000`     | Timeout in milliseconds to apply to HTTP requests. This doesn't apply to Long Polling poll requests, EventSource, or WebSockets.                                                                                                                                                                                              |

| Java Option                | Default Value | Description                                                                                                                                                                                          |
| -------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withAccessTokenProvider`  | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `shouldSkipNegotiate`      | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |
| `withHeader` `withHeaders` | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                    |

In the .NET Client, these options can be modified by the options delegate provided to `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.Headers["Foo"] = "Bar";
        options.SkipNegotiation = true;
        options.Transports = HttpTransportType.WebSockets;
        options.Cookies.Add(new Cookie(/* ... */);
        options.ClientCertificates.Add(/* ... */);
    })
    .Build();
```

In the JavaScript Client, these options can be provided in a JavaScript object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        // "Foo: Bar" will not be sent with WebSockets or Server-Sent Events requests
        headers: { "Foo": "Bar" },
        transport: signalR.HttpTransportType.LongPolling 
    })
    .build();
```

In the Java client, these options can be configured with the methods on the `HttpHubConnectionBuilder` returned from the `HubConnectionBuilder.create("HUB URL")`

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
        .withHeader("Foo", "Bar")
        .shouldSkipNegotiate(true)
        .withHandshakeResponseTimeout(30*1000)
        .build();
```

## Related content

* [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](supported-platforms?view=aspnetcore-10.0)

## JSON/MessagePack serialization options

ASP.NET Core SignalR supports two protocols for encoding messages: [JSON](https://www.json.org/) and [MessagePack](https://msgpack.org/index.html). Each protocol has serialization configuration options.

JSON serialization can be configured on the server using the [AddJsonProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jsonprotocoldependencyinjectionextensions.addjsonprotocol) extension method. `AddJsonProtocol` can be added after [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) in `Startup.ConfigureServices`. The `AddJsonProtocol` method takes a delegate that receives an `options` object. The [PayloadSerializerOptions](/en-us/dotnet/api/microsoft.aspnetcore.signalr.jsonhubprotocoloptions.payloadserializeroptions) property on that object is a `System.Text.Json` [JsonSerializerOptions](/en-us/dotnet/api/system.text.json.jsonserializeroptions) object that can be used to configure serialization of arguments and return values. For more information, see the [System.Text.Json documentation](/en-us/dotnet/api/system.text.json).

For example, to configure the serializer to not change the casing of property names, rather than the default [camel case](https://wikipedia.org/wiki/Camel_case) names, use the following code in `Program.cs`:

```
builder.Services.AddSignalR()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    });
```

In the .NET client, the same `AddJsonProtocol` extension method exists on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). The `Microsoft.Extensions.DependencyInjection` namespace must be imported to resolve the extension method:

```
// At the top of the file:
using Microsoft.Extensions.DependencyInjection;

// When constructing your connection:
var connection = new HubConnectionBuilder()
    .AddJsonProtocol(options => {
        options.PayloadSerializerOptions.PropertyNamingPolicy = null;
    })
    .Build();
```

Note

It's not possible to configure JSON serialization in the JavaScript client at this time.

### Switch to Newtonsoft.Json

If you need features of `Newtonsoft.Json` that aren't supported in `System.Text.Json`, see [Switch to `Newtonsoft.Json`](../migration/22-to-30?view=aspnetcore-10.0#switch-to-newtonsoftjson).

### MessagePack serialization options

MessagePack serialization can be configured by providing a delegate to the [AddMessagePackProtocol](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msgpackprotocoldependencyinjectionextensions.addmessagepackprotocol) call. See [MessagePack in SignalR](messagepackhubprotocol?view=aspnetcore-10.0) for more details.

Note

It's not possible to configure MessagePack serialization in the JavaScript client at this time.

## Configure server options

The following table describes options for configuring SignalR hubs.

| Option                                  | Default Value           | Description                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ClientTimeoutInterval`                 | 30 seconds              | The server considers the client disconnected if it hasn't received a message (including keep-alive) in this interval. It could take longer than this timeout interval for the client to be marked disconnected due to how this is implemented. The recommended value is double the `KeepAliveInterval` value.                                                                                              |
| `HandshakeTimeout`                      | 15 seconds              | If the client doesn't send an initial handshake message within this time interval, the connection is closed. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval`                     | 15 seconds              | If the server hasn't sent a message within this interval, a ping message is sent automatically to keep the connection open. When changing `KeepAliveInterval`, change the `ServerTimeout` or `serverTimeoutInMilliseconds` setting on the client. The recommended `ServerTimeout` or `serverTimeoutInMilliseconds` value is double the `KeepAliveInterval` value.                                          |
| `SupportedProtocols`                    | All installed protocols | Protocols supported by this hub. By default, all protocols registered on the server are allowed. Protocols can be removed from this list to disable specific protocols for individual hubs.                                                                                                                                                                                                                |
| `EnableDetailedErrors`                  | `false`                 | If `true`, detailed exception messages are returned to clients when an exception is thrown in a Hub method. The default is `false` because these exception messages can contain sensitive information.                                                                                                                                                                                                     |
| `StreamBufferCapacity`                  | `10`                    | The maximum number of items that can be buffered for client upload streams. If this limit is reached, the processing of invocations is blocked until the server processes stream items.                                                                                                                                                                                                                    |
| `MaximumReceiveMessageSize`             | 32 KB                   | Maximum size of a single incoming hub message. Increasing the value may increase the risk of [Denial of service (DoS) attacks](https://developer.mozilla.org/docs/Glossary/DOS_attack).                                                                                                                                                                                                                    |
| `MaximumParallelInvocationsPerClient`   | 1                       | The maximum number of hub methods that each client can call in parallel before queueing. This limit does not apply to streaming hub invocations.                                                                                                                                                                                                                                                           |
| `DisableImplicitFromServicesParameters` | `false`                 | Hub method arguments will be resolved from DI if possible.                                                                                                                                                                                                                                                                                                                                                 |

Options can be configured for all hubs by providing an options delegate to the `AddSignalR` call in `Program.cs`.

```
 builder.Services.AddSignalR(hubOptions =>
 {
     hubOptions.EnableDetailedErrors = true;
     hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
 });
```

Options for a single hub override the global options provided in `AddSignalR` and can be configured using [AddHubOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addhuboptions):

```
builder.Services.AddSignalR().AddHubOptions<ChatHub>(options =>
{
    options.EnableDetailedErrors = true;
});
```

### Advanced HTTP configuration options

Use `HttpConnectionDispatcherOptions` to configure advanced settings related to transports and memory buffer management. These options are configured by passing a delegate to [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.hubendpointroutebuilderextensions.maphub) in `Program.cs`.

```
using Microsoft.AspNetCore.Http.Connections;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();
app.MapHub<ChatHub>("/chathub", options =>
{
    options.Transports =
        HttpTransportType.WebSockets |
        HttpTransportType.LongPolling;
}
);
app.Run();
```

The following table describes options for configuring ASP.NET Core SignalR's advanced HTTP options.

| Option                            | Default Value                                                                         | Description                                                                                                                                                                                                                                              |
| --------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ApplicationMaxBufferSize`        | 64 KB                                                                                 | The maximum number of bytes received from the client that the server buffers before applying backpressure. Increasing this value allows the server to receive larger messages faster without applying backpressure, but can increase memory consumption. |
| `TransportMaxBufferSize`          | 64 KB                                                                                 | The maximum number of bytes sent by the app that the server buffers before observing backpressure. Increasing this value allows the server to buffer larger messages faster without awaiting backpressure, but can increase memory consumption.          |
| `AuthorizationData`               | Data automatically gathered from the `Authorize` attributes applied to the Hub class. | A list of [IAuthorizeData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizedata) objects used to determine if a client is authorized to connect to the hub.                                                                               |
| `Transports`                      | All Transports are enabled.                                                           | A bit flags enum of `HttpTransportType` values that can restrict the transports a client can use to connect.                                                                                                                                             |
| `LongPolling`                     | See below.                                                                            | Additional options specific to the Long Polling transport.                                                                                                                                                                                               |
| `WebSockets`                      | See below.                                                                            | Additional options specific to the WebSockets transport.                                                                                                                                                                                                 |
| `MinimumProtocolVersion`          | 0                                                                                     | Specify the minimum version of the negotiate protocol. This is used to limit clients to newer versions.                                                                                                                                                  |
| `TransportSendTimeout`            | 10 seconds                                                                            | The maximum amount of time the transport waits for a single send to a client to complete. If a send exceeds this timeout, the connection is closed.                                                                                                      |
| `CloseOnAuthenticationExpiration` | false                                                                                 | Set this option to enable authentication expiration tracking which will close connections when a token expires.                                                                                                                                          |

The Long Polling transport has additional options that can be configured using the `LongPolling` property:

| Option        | Default Value | Description                                                                                                                                                                                                   |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PollTimeout` | 90 seconds    | The maximum amount of time the server waits for a message to send to the client before terminating a single poll request. Decreasing this value causes the client to issue new poll requests more frequently. |

The WebSocket transport has additional options that can be configured using the `WebSockets` property:

| Option                | Default Value | Description                                                                                                                                                                                              |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CloseTimeout`        | 5 seconds     | After the server closes, if the client fails to close within this time interval, the connection is terminated.                                                                                           |
| `SubProtocolSelector` | `null`        | A delegate that can be used to set the `Sec-WebSocket-Protocol` header to a custom value. The delegate receives the values requested by the client as input and is expected to return the desired value. |

## Configure client options

Client options can be configured on the `HubConnectionBuilder` type (available in the .NET and JavaScript clients). It's also available in the Java client, but the `HttpHubConnectionBuilder` subclass is what contains the builder configuration options, as well as on the `HubConnection` itself.

### Configure logging

Logging is configured in the .NET Client using the `ConfigureLogging` method. Logging providers and filters can be registered in the same way as they are on the server. See the [Logging in ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) documentation for more information.

Note

In order to register Logging providers, you must install the necessary packages. See the [Built-in logging providers](../fundamentals/logging/?view=aspnetcore-10.0#built-in-logging-providers) section of the docs for a full list.

For example, to enable Console logging, install the `Microsoft.Extensions.Logging.Console` NuGet package. Call the `AddConsole` extension method:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub")
    .ConfigureLogging(logging => {
        logging.SetMinimumLevel(LogLevel.Information);
        logging.AddConsole();
    })
    .Build();
```

In the JavaScript client, a similar `configureLogging` method exists. Provide a `LogLevel` value indicating the minimum level of log messages to produce. Logs are written to the browser console window.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
```

Instead of a `LogLevel` value, you can also provide a `string` value representing a log level name. This is useful when configuring SignalR logging in environments where you don't have access to the `LogLevel` constants.

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging("warn")
    .build();
```

The following table lists the available log levels. The value you provide to `configureLogging` sets the **minimum** log level that will be logged. Messages logged at this level, **or the levels listed after it in the table**, will be logged.

| String                      | LogLevel               |
| --------------------------- | ---------------------- |
| `trace`                     | `LogLevel.Trace`       |
| `debug`                     | `LogLevel.Debug`       |
| `info` **or** `information` | `LogLevel.Information` |
| `warn` **or** `warning`     | `LogLevel.Warning`     |
| `error`                     | `LogLevel.Error`       |
| `critical`                  | `LogLevel.Critical`    |
| `none`                      | `LogLevel.None`        |

Note

To disable logging entirely, specify `signalR.LogLevel.None` in the `configureLogging` method.

For more information on logging, see the [SignalR Diagnostics documentation](diagnostics?view=aspnetcore-10.0).

The SignalR Java client uses the [SLF4J](https://www.slf4j.org/) library for logging. It's a high-level logging API that allows users of the library to choose their own specific logging implementation by bringing in a specific logging dependency. The following code snippet shows how to use `java.util.logging` with the SignalR Java client.

```
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
```

If you don't configure logging in your dependencies, SLF4J loads a default no-operation logger with the following warning message:

```
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
```

This can safely be ignored.

### Configure allowed transports

The transports used by SignalR can be configured in the `WithUrl` call (`withUrl` in JavaScript). A bitwise-OR of the values of `HttpTransportType` can be used to restrict the client to only use the specified transports. All transports are enabled by default.

For example, to disable the Server-Sent Events transport, but allow WebSockets and Long Polling connections:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
    .Build();
```

In the JavaScript client, transports are configured by setting the `transport` field on the options object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", { transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling })
    .build();
```

In this version of the Java client WebSockets is the only available transport.

In the Java client, the transport is selected with the `withTransport` method on the `HttpHubConnectionBuilder`. The Java client defaults to using the WebSockets transport.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withTransport(TransportEnum.WEBSOCKETS)
    .build();
```

Note

The SignalR Java client doesn't support transport fallback yet.

### Configure bearer authentication

To provide authentication data along with SignalR requests, use the `AccessTokenProvider` option (`accessTokenFactory` in JavaScript) to specify a function that returns the desired access token. In the .NET Client, this access token is passed in as an HTTP "Bearer Authentication" token (Using the `Authorization` header with a type of `Bearer`). In the JavaScript client, the access token is used as a Bearer token, **except** in a few cases where browser APIs restrict the ability to apply headers (specifically, in Server-Sent Events and WebSockets requests). In these cases, the access token is provided as a query string value `access_token`.

In the .NET client, the `AccessTokenProvider` option can be specified using the options delegate in `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.AccessTokenProvider = async () => {
            // Get and return the access token.
        };
    })
    .Build();
```

In the JavaScript client, the access token is configured by setting the `accessTokenFactory` field on the options object in `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        accessTokenFactory: () => {
            // Get and return the access token.
            // This function can return a JavaScript Promise if asynchronous
            // logic is required to retrieve the access token.
        }
    })
    .build();
```

In the SignalR Java client, you can configure a bearer token to use for authentication by providing an access token factory to the [HttpHubConnectionBuilder](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder?view=aspnet-signalr-java\&preserve-view=true). Use [withAccessTokenFactory](/en-us/java/api/com.microsoft.signalr.httphubconnectionbuilder.withaccesstokenprovider?view=aspnet-signalr-java\&preserve-view=true) to provide an [RxJava](https://github.com/ReactiveX/RxJava) [Single\<String>](https://reactivex.io/documentation/single.html). With a call to [Single.defer](https://reactivex.io/RxJava/javadoc/io/reactivex/Single.html#defer-java.util.concurrent.Callable-), you can write logic to produce access tokens for your client.

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
    .withAccessTokenProvider(Single.defer(() -> {
        // Your logic here.
        return Single.just("An Access Token");
    })).build();
```

### Configure timeout and keep-alive options

Additional options for configuring timeout and keep-alive behavior are available on the `HubConnection` object itself:

* [.NET](#tabpanel_1_dotnet)
* [JavaScript](#tabpanel_1_javascript)
* [Java](#tabpanel_1_java)

| Option              | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ServerTimeout`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `Closed` event (`onclose` in JavaScript). This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `HandshakeTimeout`  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `Closed` event (`onclose` in JavaScript). This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `KeepAliveInterval` | 15 seconds                       | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                                           |

In the .NET Client, timeout values are specified as `TimeSpan` values.

| Option                            | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serverTimeoutInMilliseconds`     | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onclose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive. |
| `keepAliveIntervalInMilliseconds` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                 |

| Option                                          | Default value                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getServerTimeout` / `setServerTimeout`         | 30 seconds (30,000 milliseconds) | Timeout for server activity. If the server hasn't sent a message in this interval, the client considers the server disconnected and triggers the `onClose` event. This value must be large enough for a ping message to be sent from the server **and** received by the client within the timeout interval. The recommended value is a number at least double the server's `KeepAliveInterval` value to allow time for pings to arrive.                                  |
| `withHandshakeResponseTimeout`                  | 15 seconds                       | Timeout for initial server handshake. If the server doesn't send a handshake response in this interval, the client cancels the handshake and triggers the `onClose` event. This is an advanced setting that should only be modified if handshake timeout errors are occurring due to severe network latency. For more detail on the Handshake process, see the [SignalR Hub Protocol Specification](https://github.com/aspnet/SignalR/blob/master/specs/HubProtocol.md). |
| `getKeepAliveInterval` / `setKeepAliveInterval` | 15 seconds (15,000 milliseconds) | Determines the interval at which the client sends ping messages. Sending any message from the client resets the timer to the start of the interval. If the client hasn't sent a message in the `ClientTimeoutInterval` set on the server, the server considers the client disconnected.                                                                                                                                                                                  |

### Configure additional options

Additional options can be configured in the `WithUrl` (`withUrl` in JavaScript) method on `HubConnectionBuilder` or on the various configuration APIs on the `HttpHubConnectionBuilder` in the Java client:

* [.NET](#tabpanel_2_dotnet)
* [JavaScript](#tabpanel_2_javascript)
* [Java](#tabpanel_2_java)

| .NET Option                 | Default value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AccessTokenProvider`       | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `SkipNegotiation`           | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                                                                                                                                                                                                                                                            |
| `ClientCertificates`        | Empty         | A collection of TLS certificates to send to authenticate requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `Cookies`                   | Empty         | A collection of HTTP cookies to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `Credentials`               | Empty         | Credentials to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `CloseTimeout`              | 5 seconds     | WebSockets only. The maximum amount of time the client waits after closing for the server to acknowledge the close request. If the server doesn't acknowledge the close within this time, the client disconnects.                                                                                                                                                                                                                                                                                                                                               |
| `Headers`                   | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `HttpMessageHandlerFactory` | `null`        | A delegate that can be used to configure or replace the `HttpMessageHandler` used to send HTTP requests. Not used for WebSocket connections. This delegate must return a non-null value, and it receives the default value as a parameter. Either modify settings on that default value and return it, or return a new `HttpMessageHandler` instance. **When replacing the handler make sure to copy the settings you want to keep from the provided handler, otherwise, the configured options (such as Cookies and Headers) won't apply to the new handler.** |
| `Proxy`                     | `null`        | An HTTP proxy to use when sending HTTP requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `UseDefaultCredentials`     | `false`       | Set this boolean to send the default credentials for HTTP and WebSockets requests. This enables the use of Windows authentication.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `WebSocketConfiguration`    | `null`        | A delegate that can be used to configure additional WebSocket options. Receives an instance of [ClientWebSocketOptions](/en-us/dotnet/api/system.net.websockets.clientwebsocketoptions) that can be used to configure the options.                                                                                                                                                                                                                                                                                                                              |
| `ApplicationMaxBufferSize`  | 1 MB          | The maximum number of bytes received from the server that the client buffers before applying backpressure. Increasing this value allows the client to receive larger messages faster without applying backpressure, but can increase memory consumption.                                                                                                                                                                                                                                                                                                        |
| `TransportMaxBufferSize`    | 1 MB          | The maximum number of bytes sent by the user application that the client buffers before observing backpressure. Increasing this value allows the client to buffer larger messages faster without awaiting backpressure, but can increase memory consumption.                                                                                                                                                                                                                                                                                                    |

| JavaScript Option    | Default Value | Description                                                                                                                                                                                                                                                                                                                   |
| -------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessTokenFactory` | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                                                                                                                                             |
| `transport`          | `null`        | An [HttpTransportType](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype) value specifying the transport to use for the connection.                                                                                                                                                                   |
| `headers`            | `null`        | Dictionary of headers sent with every HTTP request. Sending headers in the browser doesn't work for WebSockets or the [ServerSentEvents](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httptransporttype#microsoft-aspnetcore-http-connections-httptransporttype-serversentevents) stream.                          |
| `logMessageContent`  | `null`        | Set to `true` to log the bytes/chars of messages sent and received by the client.                                                                                                                                                                                                                                             |
| `skipNegotiation`    | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service.                                                                                                                          |
| `withCredentials`    | `true`        | Specifies whether credentials will be sent with the CORS request. Azure App Service uses cookies for sticky sessions and needs this option enabled to work correctly. For more info on CORS with SignalR, see [Security considerations in ASP.NET Core SignalR](security?view=aspnetcore-10.0#cross-origin-resource-sharing). |
| `timeout`            | `100,000`     | Timeout in milliseconds to apply to HTTP requests. This doesn't apply to Long Polling poll requests, EventSource, or WebSockets.                                                                                                                                                                                              |

| Java Option                | Default Value | Description                                                                                                                                                                                          |
| -------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withAccessTokenProvider`  | `null`        | A function returning a string that is provided as a Bearer authentication token in HTTP requests.                                                                                                    |
| `shouldSkipNegotiate`      | `false`       | Set this to `true` to skip the negotiation step. **Only supported when the WebSockets transport is the only enabled transport**. This setting can't be enabled when using the Azure SignalR Service. |
| `withHeader` `withHeaders` | Empty         | A Map of additional HTTP headers to send with every HTTP request.                                                                                                                                    |

In the .NET Client, these options can be modified by the options delegate provided to `WithUrl`:

```
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chathub", options => {
        options.Headers["Foo"] = "Bar";
        options.SkipNegotiation = true;
        options.Transports = HttpTransportType.WebSockets;
        options.Cookies.Add(new Cookie(/* ... */);
        options.ClientCertificates.Add(/* ... */);
    })
    .Build();
```

In the JavaScript Client, these options can be provided in a JavaScript object provided to `withUrl`:

```
let connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub", {
        // "Foo: Bar" will not be sent with WebSockets or Server-Sent Events requests
        headers: { "Foo": "Bar" },
        transport: signalR.HttpTransportType.LongPolling 
    })
    .build();
```

In the Java client, these options can be configured with the methods on the `HttpHubConnectionBuilder` returned from the `HubConnectionBuilder.create("HUB URL")`

```
HubConnection hubConnection = HubConnectionBuilder.create("https://example.com/chathub")
        .withHeader("Foo", "Bar")
        .shouldSkipNegotiate(true)
        .withHandshakeResponseTimeout(30*1000)
        .build();
```

## Related content

* [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0)
* [Use hubs in ASP.NET Core SignalR](hubs?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [Use MessagePack Hub Protocol in SignalR for ASP.NET Core](messagepackhubprotocol?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](supported-platforms?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-02

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Framework to ASP.NET Core

Updating an app from ASP.NET Framework to ASP.NET Core is non-trivial for the majority of production apps. These apps often incorporate new technologies as they become available and are often composed of many legacy decisions. This guide provides practical approaches and tools for updating ASP.NET Framework apps to ASP.NET Core with as little change as possible.

## Why migration is challenging

Migrating from ASP.NET Framework to ASP.NET Core involves several complex challenges that make a complete rewrite difficult and risky for most production applications:

### Technical debt accumulation

Production applications often accumulate technical debt over years of development:

* **System.Web dependencies** - The pervasive use of [HttpContext](/en-us/dotnet/api/system.web.httpcontext) and associated types throughout a code base.
* **Outdated package dependencies** that might not have modern compatible equivalents.
* **Legacy build tools and project configurations** that aren't compatible with modern .NET.
* **Deprecated API usage** that needs to be replaced with modern alternatives.
* **Compiler warnings and code quality issues** that complicate migration.

### Cross-cutting concerns

Many applications have cross-cutting concerns that span multiple layers and need careful coordination during migration:

* **Session state management** - ASP.NET Framework and ASP.NET Core have fundamentally different session APIs and behaviors.
* **Authentication and authorization** - Different authentication models and APIs between frameworks.
* **Logging and monitoring** - Need to maintain consistent logging across both applications during migration.
* **Caching strategies** - In-memory, distributed, or output caching needs to be maintained consistently.
* **Error handling** - Establishing consistent error handling patterns across both applications.
* **Configuration management** - Managing settings that need to be shared or synchronized between applications.
* **Dependency injection** - Migrating from various DI containers to ASP.NET Core's built-in container.

The generic host pattern can help address several of these concerns by bringing modern .NET infrastructure to ASP.NET Framework applications. For details, see [.NET Generic Host in ASP.NET Framework](areas/hosting?view=aspnetcore-10.0).

### Library dependency chains

Supporting libraries often have complex dependency relationships that require careful upgrade ordering:

* **Dependency tree complexity** - You must upgrade libraries in postorder depth-first search ordering.
* **Multi-targeting requirements** - Libraries need to support all framework versions targeted by the app.
* **API compatibility** - Ensuring libraries work with both framework versions during the migration period.
* **Testing complexity** - Each library upgrade requires thorough testing to ensure compatibility.

### Application architecture differences

The fundamental differences between ASP.NET Framework and ASP.NET Core create extra challenges:

* **Hosting models** - Different approaches to application hosting and lifecycle management
* **Middleware pipeline** - Moving from HTTP modules and handlers to middleware
* **Request processing** - Different request processing models and contexts
* **Performance characteristics** - Different memory usage patterns and performance profiles

These challenges make incremental migration the preferred approach for most production applications. This approach allows teams to address these issues gradually while maintaining a working application in production.

For documentation around important areas that changed, see the associated topics available at [Complex migration scenarios - Deep dive areas](areas/?view=aspnetcore-10.0).

## Start here: Choose your migration path

You can successfully move your ASP.NET Framework application to ASP.NET Core. The key is choosing the right approach for your specific situation.

Tip

Regardless of the path you choose, the [GitHub Copilot app modernization agent](/en-us/dotnet/core/porting/github-copilot-app-modernization/overview) can assess your solution, generate an upgrade plan, and automate many of the migration steps. For more information, see [Learn to upgrade from ASP.NET MVC, Web API, and Web Forms to ASP.NET Core](tooling?view=aspnetcore-10.0).

### Quick decision guide

**Answer these questions to choose your approach:**

1. **What's your timeline and risk tolerance?**

   * Need to stay in production during migration → [Incremental migration](#incremental-migration)
   * Can afford a complete rewrite → [In place migration](#in-place-migration)

2. **How large is your application?**

   * Small to medium apps → [In place migration](#in-place-migration)
   * Large production apps → [Incremental migration](#incremental-migration) is safer

3. **Do you have complex dependencies?**

   * Unknown or out-of-date dependencies → [Incremental migration](#incremental-migration)
   * Heavy use of System.Web → [Incremental migration](#incremental-migration)
   * Minimal dependencies → [In place migration](#in-place-migration)

## Incremental migration

Incremental migration is an implementation of the Strangler Fig pattern. It's best for larger projects or projects that need to continue to stay in production throughout a migration. To get started migrating an application incrementally, see [Get started with incremental ASP.NET to ASP.NET Core migration](start?view=aspnetcore-10.0).

## In place migration

In place migration can work for sufficiently small applications. If possible, this approach allows for a quick replacement of the application. However, small issues can compound if you decide to do an in place migration. For information on migration tooling options, see [Learn to upgrade from ASP.NET MVC, Web API, and Web Forms to ASP.NET Core](tooling?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/direct-forwarding
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/direct-forwarding.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP Direct Forwarding

Some applications only need the ability to take a specific request and forward it to a specific destination. These applications do not need, or have addressed in other ways, the other features of the proxy like configuration discovery, routing, load balancing, etc..

## IHttpForwarder

[IHttpForwarder](/en-us/dotnet/api/yarp.reverseproxy.forwarder.ihttpforwarder) serves as the core proxy adapter between incoming AspNetCore and outgoing System.Net.Http requests. It handles the mechanics of creating a HttpRequestMessage from a HttpContext, sending it, and relaying the response.

IHttpForwarder supports:

* Dynamic destination selection, you specify the destination for each request
* Http client customization, you provide the HttpMessageInvoker
* Request and response customization (except bodies)
* Streaming protocols like gRPC and WebSockets
* Error handling

It does not include:

* Routing
* Load balancing
* Affinity
* Retries

## Example

See [ReverseProxy.Direct.Sample](https://github.com/microsoft/reverse-proxy/tree/release/latest/samples/ReverseProxy.Direct.Sample) as a pre-built sample, or use the steps below.

### Add the YARP Reverse Proxy NuGet package

Add the [`Yarp.ReverseProxy` NuGet package](https://www.nuget.org/packages/Yarp.ReverseProxy) to the project.

### Update `Program.cs`

In this example the IHttpForwarder is registered in DI, injected into the endpoint method, and used to forward requests from a specific route to `https://localhost:10000/prefix/`.

The optional transforms show how to copy all request headers except for the `Host`, it's common that the destination requires its own `Host` from the url.

```
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Yarp.ReverseProxy.Forwarder;
using Yarp.ReverseProxy.Transforms;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpForwarder();

var app = builder.Build();

// Configure our own HttpMessageInvoker for outbound calls for proxy operations
var httpClient = new HttpMessageInvoker(new SocketsHttpHandler
{
    UseProxy = false,
    AllowAutoRedirect = false,
    AutomaticDecompression = DecompressionMethods.None,
    UseCookies = false,
    EnableMultipleHttp2Connections = true,
    ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),
    ConnectTimeout = TimeSpan.FromSeconds(15),
});

// Setup our own request transform class
var transformer = new CustomTransformer(); // or HttpTransformer.Default;
var requestConfig = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(100) };

app.UseRouting();

// When using IHttpForwarder for direct forwarding you are responsible for routing, destination discovery, load balancing, affinity, etc..
// For an alternate example that includes those features see BasicYarpSample.
app.Map("/test/{**catch-all}", async (HttpContext httpContext, IHttpForwarder forwarder) =>
{
    var error = await forwarder.SendAsync(httpContext, "https://localhost:10000/",
        httpClient, requestConfig, transformer);
    // Check if the operation was successful
    if (error != ForwarderError.None)
    {
        var errorFeature = httpContext.GetForwarderErrorFeature();
        var exception = errorFeature.Exception;
    }
});

app.Run();

/// <summary>
/// Custom request transformation
/// </summary>
internal class CustomTransformer : HttpTransformer
{
    /// <summary>
    /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage
    /// fields are initialized except RequestUri, which will be initialized after the
    /// callback if no value is provided. The string parameter represents the destination
    /// URI prefix that should be used when constructing the RequestUri. The headers
    /// are copied by the base implementation, excluding some protocol headers like HTTP/2
    /// pseudo headers (":authority").
    /// </summary>
    /// <param name="httpContext">The incoming request.</param>
    /// <param name="proxyRequest">The outgoing proxy request.</param>
    /// <param name="destinationPrefix">The uri prefix for the selected destination server which can be used to create
    /// the RequestUri.</param>
    public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)
    {
        // Copy all request headers
        await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);

        // Customize the query string:
        var queryContext = new QueryTransformContext(httpContext.Request);
        queryContext.Collection.Remove("param1");
        queryContext.Collection["area"] = "xx2";

        // Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default.
        proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress("https://example.com", httpContext.Request.Path, queryContext.QueryString);

        // Suppress the original request header, use the one from the destination Uri.
        proxyRequest.Headers.Host = null;
    }
}
```

There are also [extension methods](/en-us/dotnet/api/microsoft.aspnetcore.builder.directforwardingiendpointroutebuilderextensions) available that simplify the mapping of IHttpForwarder to endpoints.

```
app.MapForwarder("/{**catch-all}", "https://localhost:10000/", requestConfig, transformer, httpClient);
```

### The HTTP Client

The http client may be customized, but the above example is recommended for common proxy scenarios.

Always use HttpMessageInvoker rather than HttpClient, HttpClient buffers responses by default. Buffering breaks streaming scenarios and increases memory usage and latency.

Re-using a client for requests to the same destination is recommended for performance reasons as it allows you to re-use pooled connections. A client may also be re-used for requests to different destinations if the configuration is the same.

### Transforms

The request and response can be modified by providing a derived [HttpTransformer](/en-us/dotnet/api/yarp.reverseproxy.forwarder.httptransformer) as a parameter to the [SendAsync](/en-us/dotnet/api/yarp.reverseproxy.forwarder.ihttpforwarderextensions.sendasync) method, as shown in the [Update `Program.cs`](#update-programcs) section's example.

To use the [built-in transforms](transforms?view=aspnetcore-10.0) with direct forwarding, resolve `ITransformBuilder` from dependency injection and call its `Create` method. `AddHttpForwarder` registers `ITransformBuilder`, so `AddReverseProxy` isn't required.

Ensure that the `Program` file imports the following namespaces:

```
using Yarp.ReverseProxy.Transforms;
using Yarp.ReverseProxy.Transforms.Builder;
```

Replace the `transformer` initialization in the preceding example with the following code:

```
var transformBuilder = app.Services.GetRequiredService<ITransformBuilder>();
var transformer = transformBuilder.Create(context =>
{
    context.AddQueryRemoveKey("param1");
    context.AddQueryValue("area", "xx2", append: false);
});
```

The callback configures a `TransformBuilderContext` using the same extension methods available to `AddTransforms`. The transforms remove the `param1` query parameter and set `area` to `xx2`. A request for `/test/orders?param1=value&area=old&sort=name` is forwarded as `/test/orders?area=xx2&sort=name`.

Pass the resulting `HttpTransformer` to `SendAsync` or `MapForwarder`, as shown earlier. Create the transformer once and reuse it across requests. By default, the created transformer also suppresses the incoming `Host` header and adds the `X-Forwarded` headers described in [YARP Request and Response Transforms](transforms?view=aspnetcore-10.0#defaults).

### Error handling

IHttpForwarder catches exceptions and timeouts from the HTTP client, logs them, and converts them to 5xx status codes or aborts the response. An error code is returned from `SendAsync`, and the error details can be accessed from the [IForwarderErrorFeature](/en-us/dotnet/api/yarp.reverseproxy.forwarder.iforwardererrorfeature) as shown above.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/portable-object-localization
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/portable-object-localization.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure portable object localization in ASP.NET Core

By [Hisham Bin Ateya](https://github.com/hishamco) and [Sébastien Ros](https://github.com/sebastienros).

This article walks through the steps for using Portable Object (PO) files in an ASP.NET Core application with the [Orchard Core](https://github.com/OrchardCMS/OrchardCore) framework.

**Note:** Orchard Core isn't a Microsoft product. Microsoft provides no support for this feature.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/localization/samples/6.x/POLocalization) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## What is a PO file?

PO files are distributed as text files containing the translated strings for a given language. Some advantages of using PO files instead of *.resx* files include:

* PO files support pluralization; *.resx* files don't support pluralization.
* PO files aren't compiled like *.resx* files. As such, specialized tooling and build steps aren't required.
* PO files work well with collaborative online editing tools.

### Example

The following sample PO file contains the translation for two strings in French, including one with its plural form:

*fr.po*

```
#: Pages/Index.cshtml:13
msgid "Hello world!"
msgstr "Bonjour le monde!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."
```

This example uses the following syntax:

* `#:`: A comment indicating the context of the string to be translated. The same string might be translated differently depending on where it's being used.
* `msgid`: The untranslated string.
* `msgstr`: The translated string.

For pluralization support, more entries can be defined.

* `msgid_plural`: The untranslated plural string.
* `msgstr[0]`: The translated string for the case 0.
* `msgstr[N]`: The translated string for the case N.

The PO file specification can be found [here](https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html).

## Configuring PO file support in ASP.NET Core

This example is based on an ASP.NET Core Web application generated from a Visual Studio 2022 project template.

### Referencing the package

Add a reference to the `OrchardCore.Localization.Core` NuGet package.

The `.csproj` file now contains a line similar to the following (version number may vary):

```
<PackageReference Include="OrchardCore.Localization.Core" Version="1.5.0" />
```

### Registering the service

Add the required services to `Program.cs`:

```
builder.Services.AddPortableObjectLocalization();

builder.Services
    .Configure<RequestLocalizationOptions>(options => options
        .AddSupportedCultures("fr", "cs")
        .AddSupportedUICultures("fr", "cs"));

builder.Services
    .AddRazorPages()
    .AddViewLocalization();
```

Add the following code to your Razor page of choice. `Index.cshtml` is used in this example.

```
@page
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@{
    ViewData["Title"] = "Home";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<p>@Localizer["Hello world!"]</p>
```

An `IViewLocalizer` instance is injected and used to translate the text "Hello world!".

### Creating a PO file

Create a file named *\<culture code>.po* in your application root folder. In this example, the file name is *fr.po* because the French language is used:

```
msgid "Hello world!"
msgstr "Bonjour le monde!"
```

This file stores both the string to translate and the French-translated string. Translations revert to their parent culture, if necessary. In this example, the *fr.po* file is used if the requested culture is `fr-FR` or `fr-CA`.

### Testing the application

Run your application, the text **Hello world!** is displayed.

Navigate to the URL `/Index?culture=fr-FR`. The text **Bonjour le monde!** is displayed.

## Pluralization

PO files support pluralization forms, which is useful when the same string needs to be translated differently based on a cardinality. This task is made complicated by the fact that each language defines custom rules to select which string to use based on the cardinality.

The Orchard Localization package provides an API to invoke these different plural forms automatically.

### Creating pluralization PO files

Add the following content to the previously mentioned *fr.po* file:

```
msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."
```

See [What is a PO file?](#what-is-a-po-file) for an explanation of what each entry in this example represents.

### Add a language using different pluralization forms

English and French strings were used in the previous example. English and French have only two pluralization forms and share the same form rules, which is that a cardinality of one is mapped to the first plural form. Any other cardinality is mapped to the second plural form.

Not all languages share the same rules. This is illustrated with the Czech language, which has three plural forms.

Create the `cs.po` file as follows, and note how the pluralization needs three different translations:

```
msgid "Hello world!"
msgstr "Ahoj světe!!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Existuje jedna položka."
msgstr[1] "Existují {0} položky."
msgstr[2] "Existuje {0} položek."
```

To accept Czech localizations, add `"cs"` to the list of supported cultures in the `Configure` method:

```
builder.Services
    .Configure<RequestLocalizationOptions>(options => options
        .AddSupportedCultures("fr", "cs")
        .AddSupportedUICultures("fr", "cs"));
```

Edit the `Pages/Index.cshtml` file to render localized, plural strings for several cardinalities:

```
<p>@Localizer.Plural(1, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(2, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(5, "There is one item.", "There are {0} items.")</p>
```

**Note:** In a real world scenario, a variable would be used to represent the count. Here, we repeat the same code with three different values to expose a specific case.

Upon switching cultures, you see the following:

For `/Index`:

```
There is one item.
There are 2 items.
There are 5 items.
```

For `/Index?culture=fr`:

```
Il y a un élément.
Il y a 2 éléments.
Il y a 5 éléments.
```

For `/Index?culture=cs`:

```
Existuje jedna položka.
Existují 2 položky.
Existuje 5 položek.
```

For the Czech culture, the three translations are different. The French and English cultures share the same construction for the two last translated strings.

## Advanced tasks

### Using additional arguments

The argument at index zero `{0}` always represents the count value. When invoking the `Plural` method it is possible to add additional arguments and their index will then start at one (`1`).

```
<p>@Localizer.Plural(count, "There is one item with the color {1}.", "There are {0} items. The main color is {1}.", color)</p>
```

### Contextualizing strings

Applications often contain the strings to be translated in several places. The same string may have a different translation in certain locations within an app (Razor views or class files). A PO file supports the notion of a file context, which can be used to categorize the string being represented. Using a file context, a string can be translated differently, depending on the file context (or lack of a file context).

The PO localization services use the name of the full class or the view that's used when translating a string. This is accomplished by setting the value on the `msgctxt` entry.

Consider a minor addition to the previous *fr.po* example. A Razor page located at `Pages/Index.cshtml` can be defined as the file context by setting the reserved `msgctxt` entry's value:

```
msgctxt "Views.Home.About"
msgid "Hello world!"
msgstr "Bonjour le monde!"
```

With the `msgctxt` set as such, text translation occurs when navigating to `/Index?culture=fr-FR`. The translation doesn't occur when navigating to `/Privacy?culture=fr-FR`.

When no specific entry is matched with a given file context, Orchard Core's fallback mechanism looks for an appropriate PO file without a context. Assuming there's no specific file context defined for `Pages/Privacy.cshtml`, navigating to `/Privacy?culture=fr-FR` loads a PO file such as:

```
msgid "Hello world!"
msgstr "Bonjour le monde!"
```

### Changing the location of PO files

The default location of PO files can be changed in `Programs.cs`:

```
services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization");
```

In this example, the PO files are loaded from the *Localization* folder.

### Implementing a custom logic for finding localization files

When more complex logic is needed to locate PO files, the `OrchardCore.Localization.PortableObject.ILocalizationFileLocationProvider` interface can be implemented and registered as a service. This is useful when PO files can be stored in varying locations or when the files have to be found within a hierarchy of folders.

### Using a different default pluralized language

The package includes a `Plural` extension method that's specific to two plural forms. For languages requiring more plural forms, create an extension method. With an extension method, you won't need to provide any localization file for the default language — the original strings are already available directly in the code.

You can use the more generic `Plural(int count, string[] pluralForms, params object[] arguments)` overload which accepts a string array of translations.

By [Sébastien Ros](https://github.com/sebastienros), [Scott Addie](https://twitter.com/Scott_Addie) and [Hisham Bin Ateya](https://github.com/hishamco)

This article walks through the steps for using Portable Object (PO) files in an ASP.NET Core application with the [Orchard Core](https://github.com/OrchardCMS/OrchardCore) framework.

**Note:** Orchard Core isn't a Microsoft product. Consequently, Microsoft provides no support for this feature.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/localization/samples/5.x/POLocalization) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## What is a PO file?

PO files are distributed as text files containing the translated strings for a given language. Some advantages of using PO files instead of *.resx* files include:

* PO files support pluralization; *.resx* files don't support pluralization.
* PO files aren't compiled like *.resx* files. As such, specialized tooling and build steps aren't required.
* PO files work well with collaborative online editing tools.

### Example

Here is a sample PO file containing the translation for two strings in French, including one with its plural form:

*fr.po*

```
#: Pages/Index.cshtml:13
msgid "Hello world!"
msgstr "Bonjour le monde!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."
```

This example uses the following syntax:

* `#:`: A comment indicating the context of the string to be translated. The same string might be translated differently depending on where it's being used.
* `msgid`: The untranslated string.
* `msgstr`: The translated string.

In the case of pluralization support, more entries can be defined.

* `msgid_plural`: The untranslated plural string.
* `msgstr[0]`: The translated string for the case 0.
* `msgstr[N]`: The translated string for the case N.

The PO file specification can be found [here](https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html).

## Configuring PO file support in ASP.NET Core

This example is based on an ASP.NET Core MVC application generated from a Visual Studio 2019 project template.

### Referencing the package

Add a reference to the `OrchardCore.Localization.Core` NuGet package.

The `.csproj` file now contains a line similar to the following (version number may vary):

```
<PackageReference Include="OrchardCore.Localization.Core" Version="1.2.0" />
```

### Registering the service

Add the required services to the `ConfigureServices` method of `Startup.cs`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);

    services.AddPortableObjectLocalization();

    services.Configure<RequestLocalizationOptions>(options => options
        .AddSupportedCultures("fr", "cs")
        .AddSupportedUICultures("fr", "cs")
    );
}
```

Add the required middleware to the `Configure` method of `Startup.cs`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseRouting();
    app.UseStaticFiles();

    app.UseRequestLocalization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

Add the following code to your Razor page of choice. `Index.cshtml` is used in this example.

```
@page
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@{
    ViewData["Title"] = "Home";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<p>@Localizer["Hello world!"]</p>
```

An `IViewLocalizer` instance is injected and used to translate the text "Hello world!".

### Creating a PO file

Create a file named *\<culture code>.po* in your application root folder. In this example, the file name is *fr.po* because the French language is used:

```
msgid "Hello world!"
msgstr "Bonjour le monde!"
```

This file stores both the string to translate and the French-translated string. Translations revert to their parent culture, if necessary. In this example, the *fr.po* file is used if the requested culture is `fr-FR` or `fr-CA`.

### Testing the application

Run your application, and navigate to the URL `/Index`. The text **Hello world!** is displayed.

Navigate to the URL `/Index?culture=fr-FR`. The text **Bonjour le monde!** is displayed.

## Pluralization

PO files support pluralization forms, which is useful when the same string needs to be translated differently based on a cardinality. This task is made complicated by the fact that each language defines custom rules to select which string to use based on the cardinality.

The Orchard Localization package provides an API to invoke these different plural forms automatically.

### Creating pluralization PO files

Add the following content to the previously mentioned *fr.po* file:

```
msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."
```

See [What is a PO file?](#what-is-a-po-file) for an explanation of what each entry in this example represents.

### Add a language using different pluralization forms

English and French strings were used in the previous example. English and French have only two pluralization forms and share the same form rules, which is that a cardinality of one is mapped to the first plural form. Any other cardinality is mapped to the second plural form.

Not all languages share the same rules. This is illustrated with the Czech language, which has three plural forms.

Create the `cs.po` file as follows, and note how the pluralization needs three different translations:

```
msgid "Hello world!"
msgstr "Ahoj světe!!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Existuje jedna položka."
msgstr[1] "Existují {0} položky."
msgstr[2] "Existuje {0} položek."
```

To accept Czech localizations, add `"cs"` to the list of supported cultures in the `ConfigureServices` method:

```
services.Configure<RequestLocalizationOptions>(options => options
                .AddSupportedCultures("fr", "cs")
                .AddSupportedUICultures("fr", "cs")
            );
```

Edit the `Pages/Index.cshtml` file to render localized, plural strings for several cardinalities:

```
<p>@Localizer.Plural(1, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(2, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(5, "There is one item.", "There are {0} items.")</p>
```

**Note:** In a real world scenario, a variable would be used to represent the count. Here, we repeat the same code with three different values to expose a very specific case.

Upon switching cultures, you see the following:

For `/Index`:

```
There is one item.
There are 2 items.
There are 5 items.
```

For `/Index?culture=fr`:

```
Il y a un élément.
Il y a 2 éléments.
Il y a 5 éléments.
```

For `/Index?culture=cs`:

```
Existuje jedna položka.
Existují 2 položky.
Existuje 5 položek.
```

Note that for the Czech culture, the three translations are different. The French and English cultures share the same construction for the two last translated strings.

## Advanced tasks

### Contextualizing strings

Applications often contain the strings to be translated in several places. The same string may have a different translation in certain locations within an app (Razor views or class files). A PO file supports the notion of a file context, which can be used to categorize the string being represented. Using a file context, a string can be translated differently, depending on the file context (or lack of a file context).

The PO localization services use the name of the full class or the view that's used when translating a string. This is accomplished by setting the value on the `msgctxt` entry.

Consider a minor addition to the previous *fr.po* example. A Razor view located at `Pages/Index.cshtml` can be defined as the file context by setting the reserved `msgctxt` entry's value:

```
msgctxt "Pages.Index"
msgid "Hello world!"
msgstr "Bonjour le monde!"
```

With the `msgctxt` set as such, text translation occurs when navigating to `/Index?culture=fr-FR`. The translation won't occur when navigating to `/Privacy?culture=fr-FR`.

When no specific entry is matched with a given file context, Orchard Core's fallback mechanism looks for an appropriate PO file without a context. Assuming there's no specific file context defined for `Pages/Privacy.cshtml`, navigating to `/Privacy?culture=fr-FR` loads a PO file such as:

```
msgid "Hello world!"
msgstr "Bonjour le monde!"
```

### Changing the location of PO files

The default location of PO files can be changed in `ConfigureServices`:

```
services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization");
```

In this example, the PO files are loaded from the *Localization* folder.

### Implementing a custom logic for finding localization files

When more complex logic is needed to locate PO files, the `OrchardCore.Localization.PortableObject.ILocalizationFileLocationProvider` interface can be implemented and registered as a service. This is useful when PO files can be stored in varying locations or when the files have to be found within a hierarchy of folders.

### Using a different default pluralized language

The package includes a `Plural` extension method that's specific to two plural forms. For languages requiring more plural forms, create an extension method. With an extension method, you won't need to provide any localization file for the default language — the original strings are already available directly in the code.

You can use the more generic `Plural(int count, string[] pluralForms, params object[] arguments)` overload which accepts a string array of translations.

By [Sébastien Ros](https://github.com/sebastienros), [Scott Addie](https://twitter.com/Scott_Addie) and [Hisham Bin Ateya](https://github.com/hishamco)

This article walks through the steps for using Portable Object (PO) files in an ASP.NET Core application with the [Orchard Core](https://github.com/OrchardCMS/OrchardCore) framework.

**Note:** Orchard Core isn't a Microsoft product. Consequently, Microsoft provides no support for this feature.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/localization/samples/3.x/POLocalization) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## What is a PO file?

PO files are distributed as text files containing the translated strings for a given language. Some advantages of using PO files instead of *.resx* files include:

* PO files support pluralization; *.resx* files don't support pluralization.
* PO files aren't compiled like *.resx* files. As such, specialized tooling and build steps aren't required.
* PO files work well with collaborative online editing tools.

### Example

Here is a sample PO file containing the translation for two strings in French, including one with its plural form:

*fr.po*

```
#: Services/EmailService.cs:29
msgid "Enter a comma separated list of email addresses."
msgstr "Entrez une liste d'emails séparés par une virgule."

#: Views/Email.cshtml:112
msgid "The email address is \"{0}\"."
msgid_plural "The email addresses are \"{0}\"."
msgstr[0] "L'adresse email est \"{0}\"."
msgstr[1] "Les adresses email sont \"{0}\""
```

This example uses the following syntax:

* `#:`: A comment indicating the context of the string to be translated. The same string might be translated differently depending on where it's being used.
* `msgid`: The untranslated string.
* `msgstr`: The translated string.

In the case of pluralization support, more entries can be defined.

* `msgid_plural`: The untranslated plural string.
* `msgstr[0]`: The translated string for the case 0.
* `msgstr[N]`: The translated string for the case N.

The PO file specification can be found [here](https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html).

## Configuring PO file support in ASP.NET Core

This example is based on an ASP.NET Core MVC application generated from a Visual Studio 2017 project template.

### Referencing the package

Add a reference to the `OrchardCore.Localization.Core` NuGet package.

The `.csproj` file now contains a line similar to the following (version number may vary):

```
<PackageReference Include="OrchardCore.Localization.Core" Version="1.0.0" />
```

### Registering the service

Add the required services to the `ConfigureServices` method of `Startup.cs`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);

    services.AddPortableObjectLocalization();

    services.Configure<RequestLocalizationOptions>(options =>
        {
            var supportedCultures = new List<CultureInfo>
            {
                new CultureInfo("en-US"),
                new CultureInfo("en"),
                new CultureInfo("fr-FR"),
                new CultureInfo("fr")
            };

            options.DefaultRequestCulture = new RequestCulture("en-US");
            options.SupportedCultures = supportedCultures;
            options.SupportedUICultures = supportedCultures;
        });
}
```

Add the required middleware to the `Configure` method of `Startup.cs`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseRouting();
    app.UseStaticFiles();

    app.UseRequestLocalization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

Add the following code to your Razor view of choice. `About.cshtml` is used in this example.

```
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer

<p>@Localizer["Hello world!"]</p>
```

An `IViewLocalizer` instance is injected and used to translate the text "Hello world!".

### Creating a PO file

Create a file named *\<culture code>.po* in your application root folder. In this example, the file name is *fr.po* because the French language is used:

```
msgid "Hello world!"
msgstr "Bonjour le monde!"
```

This file stores both the string to translate and the French-translated string. Translations revert to their parent culture, if necessary. In this example, the *fr.po* file is used if the requested culture is `fr-FR` or `fr-CA`.

### Testing the application

Run your application, and navigate to the URL `/Home/About`. The text **Hello world!** is displayed.

Navigate to the URL `/Home/About?culture=fr-FR`. The text **Bonjour le monde!** is displayed.

## Pluralization

PO files support pluralization forms, which is useful when the same string needs to be translated differently based on a cardinality. This task is made complicated by the fact that each language defines custom rules to select which string to use based on the cardinality.

The Orchard Localization package provides an API to invoke these different plural forms automatically.

### Creating pluralization PO files

Add the following content to the previously mentioned *fr.po* file:

```
msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."
```

See [What is a PO file?](#what-is-a-po-file) for an explanation of what each entry in this example represents.

### Add a language using different pluralization forms

English and French strings were used in the previous example. English and French have only two pluralization forms and share the same form rules, which is that a cardinality of one is mapped to the first plural form. Any other cardinality is mapped to the second plural form.

Not all languages share the same rules. This is illustrated with the Czech language, which has three plural forms.

Create the `cs.po` file as follows, and note how the pluralization needs three different translations:

```
msgid "Hello world!"
msgstr "Ahoj světe!!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Existuje jedna položka."
msgstr[1] "Existují {0} položky."
msgstr[2] "Existuje {0} položek."
```

To accept Czech localizations, add `"cs"` to the list of supported cultures in the `ConfigureServices` method:

```
var supportedCultures = new List<CultureInfo>
{
    new CultureInfo("en-US"),
    new CultureInfo("en"),
    new CultureInfo("fr-FR"),
    new CultureInfo("fr"),
    new CultureInfo("cs")
};
```

Edit the `Views/Home/About.cshtml` file to render localized, plural strings for several cardinalities:

```
<p>@Localizer.Plural(1, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(2, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(5, "There is one item.", "There are {0} items.")</p>
```

**Note:** In a real world scenario, a variable would be used to represent the count. Here, we repeat the same code with three different values to expose a very specific case.

Upon switching cultures, you see the following:

For `/Home/About`:

```
There is one item.
There are 2 items.
There are 5 items.
```

For `/Home/About?culture=fr`:

```
Il y a un élément.
Il y a 2 éléments.
Il y a 5 éléments.
```

For `/Home/About?culture=cs`:

```
Existuje jedna položka.
Existují 2 položky.
Existuje 5 položek.
```

Note that for the Czech culture, the three translations are different. The French and English cultures share the same construction for the two last translated strings.

## Advanced tasks

### Contextualizing strings

Applications often contain the strings to be translated in several places. The same string may have a different translation in certain locations within an app (Razor views or class files). A PO file supports the notion of a file context, which can be used to categorize the string being represented. Using a file context, a string can be translated differently, depending on the file context (or lack of a file context).

The PO localization services use the name of the full class or the view that's used when translating a string. This is accomplished by setting the value on the `msgctxt` entry.

Consider a minor addition to the previous *fr.po* example. A Razor view located at `Views/Home/About.cshtml` can be defined as the file context by setting the reserved `msgctxt` entry's value:

```
msgctxt "Views.Home.About"
msgid "Hello world!"
msgstr "Bonjour le monde!"
```

With the `msgctxt` set as such, text translation occurs when navigating to `/Home/About?culture=fr-FR`. The translation won't occur when navigating to `/Home/Contact?culture=fr-FR`.

When no specific entry is matched with a given file context, Orchard Core's fallback mechanism looks for an appropriate PO file without a context. Assuming there's no specific file context defined for `Views/Home/Contact.cshtml`, navigating to `/Home/Contact?culture=fr-FR` loads a PO file such as:

```
msgid "Hello world!"
msgstr "Bonjour le monde!"
```

### Changing the location of PO files

The default location of PO files can be changed in `ConfigureServices`:

```
services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization");
```

In this example, the PO files are loaded from the *Localization* folder.

### Implementing a custom logic for finding localization files

When more complex logic is needed to locate PO files, the `OrchardCore.Localization.PortableObject.ILocalizationFileLocationProvider` interface can be implemented and registered as a service. This is useful when PO files can be stored in varying locations or when the files have to be found within a hierarchy of folders.

### Using a different default pluralized language

The package includes a `Plural` extension method that's specific to two plural forms. For languages requiring more plural forms, create an extension method. With an extension method, you won't need to provide any localization file for the default language — the original strings are already available directly in the code.

You can use the more generic `Plural(int count, string[] pluralForms, params object[] arguments)` overload which accepts a string array of translations.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/validation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/forms/validation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor forms validation

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to validate user input in Blazor forms.

For most forms, the simplest and recommended approach is to add [data annotations validation attributes](/en-us/dotnet/api/system.componentmodel.dataannotations) to the model and place a [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component in the [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform). Blazor also supports custom validation through the form's [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext), either directly in the form component or in a reusable validator component.

Related articles provide more detail:

* For writing and configuring model-based validation rules, including custom and asynchronous rules, nested object validation, and localization, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0).
* For live browser validation in static server-side rendering (static SSR), see [ASP.NET Core Blazor client-side form validation in static SSR](validation-client-side?view=aspnetcore-10.0).
* For complete validator-component and remote-validation implementations, see [ASP.NET Core Blazor advanced form validation](validation-advanced?view=aspnetcore-10.0).

- For writing and configuring model-based validation rules and nested object validation, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0).
- For complete validator-component and remote-validation implementations, see [ASP.NET Core Blazor advanced form validation](validation-advanced?view=aspnetcore-10.0).

* For complete validator-component and remote-validation implementations, see [ASP.NET Core Blazor advanced form validation](validation-advanced?view=aspnetcore-10.0).

[]()

## Validate with data annotations

The following model uses [RequiredAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute) and [RangeAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute):

`Starship.cs`:

```
using System.ComponentModel.DataAnnotations;

public class Starship
{
    [Required]
    public string? Identifier { get; set; }

    [Range(1, 10, ErrorMessage = "Accommodation must be between 1 and 10.")]
    public int MaximumAccommodation { get; set; }
}
```

Add the model to an `EditForm`, include `DataAnnotationsValidator`, and display errors with [ValidationMessage\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationmessage-1) or [ValidationSummary](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationsummary). The [OnValidSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onvalidsubmit) callback is invoked only when validation succeeds:

```
<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>
            Identifier:
            <InputText @bind-Value="Model.Identifier" />
        </label>
        <ValidationMessage For="() => Model.Identifier" />
    </p>

    <p>
        <label>
            Maximum accommodation:
            <InputNumber @bind-Value="Model.MaximumAccommodation" />
        </label>
        <ValidationMessage For="() => Model.MaximumAccommodation" />
    </p>

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

@code {
    private Starship Model { get; } = new Starship();

    private void Submit()
    {
        // Process the valid form.
    }
}
```

For a static SSR form post, assign a unique `FormName` and receive the posted model with `[SupplyParameterFromForm]`:

```
<EditForm Model="Model" FormName="starship" OnValidSubmit="Submit">
    ...
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();
}
```

For more information about form submission and model binding across render modes, see [ASP.NET Core Blazor forms overview](./?view=aspnetcore-10.0) and [ASP.NET Core Blazor forms binding](binding?view=aspnetcore-10.0).

Without a `DataAnnotationsValidator` component, validation attributes on the model don't participate in the form's validation.

### When validation runs

Blazor performs field validation and full-form validation:

* Field validation runs after a field changes. In an interactive form, this occurs in .NET while the user edits the form.
* Full-form validation normally runs when `EditForm` handles submission through `OnValidSubmit` or `OnInvalidSubmit`. An `OnSubmit` handler takes control of validation, as described in [Control form submission](#control-form-submission).

A static SSR form can provide live browser feedback with [ASP.NET Core Blazor client-side form validation in static SSR](validation-client-side?view=aspnetcore-10.0). The form is validated again authoritatively on the server when posted.

A static SSR form is validated on the server when posted and doesn't provide live field validation between requests.

Validation results that identify a member are associated with that field. Results without a member name are associated with the model and appear in a validation summary rather than a field's `ValidationMessage` component.

### Configure data annotations validation

`DataAnnotationsValidator` always enables DataAnnotations validation for the form. To use the extended validation capabilities provided by the [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation) package, call the `AddValidation` extension method in the `Program` file:

```
builder.Services.AddValidation();
```

The `AddValidation` call registers the package's validation services and activates a source generator that creates validation metadata for discovered model types. The available behavior depends on whether that metadata includes the form's model:

| Configuration                      | Behavior                                                                                                                                                                |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Generated metadata is available    | Validates nested objects and collections and supports message localization.                                                                                             |
| Generated metadata isn't available | Validates top-level properties, but doesn't validate nested objects or collections and doesn't use the `Microsoft.Extensions.Validation` message-localization pipeline. |

| Configuration                      | Behavior                                  |
| ---------------------------------- | ----------------------------------------- |
| Generated metadata is available    | Validates nested objects and collections. |
| Generated metadata isn't available | Validates top-level properties only.      |

The `ValidatableTypeAttribute` and `SkipValidationAttribute` APIs are experimental in .NET 10. For details and available workarounds, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0#experimental-api-in-apps-that-target-net-10).

When using `Microsoft.Extensions.Validation`, declare model types in C# files (`.cs`) rather than Razor component files (`.razor`). The source generator creates validation metadata from C# source and can't include model types declared in Razor components.

For configuration requirements, validation order, custom rules, nested object graphs, and generated metadata, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0).

### Validate nested object graphs

In .NET 9 or earlier, `DataAnnotationsValidator` validates top-level model properties but doesn't recursively validate collection or complex-type properties. For recursive validation, use `ObjectGraphDataAnnotationsValidator` and `[ValidateComplexType]` from the experimental [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation):

```
<EditForm Model="Model" OnValidSubmit="Submit">
    <ObjectGraphDataAnnotationsValidator />
    ...
</EditForm>
```

```
public class Starship
{
    [ValidateComplexType]
    public ShipDescription Description { get; set; } =
        new ShipDescription();
}
```

The package remains experimental in these framework versions.

### `[CompareProperty]` attribute

For .NET 5 or earlier, use the experimental package's `ComparePropertyAttribute` instead of [CompareAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.compareattribute). `ComparePropertyAttribute` associates the validation result with the field consistently during field and full-form validation.

[]()

### Write model-based custom rules

When built-in attributes can't express a rule, use a custom [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) or [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject). For detailed guidance, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0#write-custom-validation-rules).

### Write model-based custom rules

When built-in attributes can't express a rule, use a [custom validation attribute](../../mvc/models/validation?view=aspnetcore-10.0#custom-attributes) or implement [`IValidatableObject`](../../mvc/models/validation?view=aspnetcore-10.0#ivalidatableobject). Both run through `DataAnnotationsValidator`.

When returning a [ValidationResult](/en-us/dotnet/api/system.componentmodel.dataannotations.validationresult) from a custom attribute, include the validated member name so the result can appear in that field's `ValidationMessage` component.

[]()

Custom attributes can resolve registered services through [GetService](/en-us/dotnet/api/system.componentmodel.dataannotations.validationcontext.getservice).

## Add validation through `EditContext`

`EditForm` creates an `EditContext` automatically when its `Model` parameter is assigned. To use validation APIs directly, create the context yourself and assign it to the [EditContext](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.editcontext#microsoft-aspnetcore-components-forms-editform-editcontext) parameter of `EditForm`. Don't assign both `Model` and `EditContext` to the same form.

Custom validation commonly uses:

* [OnValidationRequested](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.onvalidationrequested) for full-form validation.
* [OnFieldChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.onfieldchanged) for field validation.
* [ValidationMessageStore](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationmessagestore) to add and clear messages.
* [NotifyValidationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.notifyvalidationstatechanged) to notify the UI after messages change.

The following interactive-form pattern adds a form-level business rule alongside data annotations validation and rechecks the rule when either relevant field changes:

```
@implements IDisposable

<EditForm EditContext="editContext" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    ...
</EditForm>

@code {
    private Starship Model { get; } = new Starship();
    private EditContext editContext = default!;
    private ValidationMessageStore messages = default!;

    protected override void OnInitialized()
    {
        editContext = new EditContext(Model);
        messages = new ValidationMessageStore(editContext);
        editContext.OnValidationRequested += ValidateBusinessRules;
        editContext.OnFieldChanged += ValidateChangedField;
    }

    private void ValidateBusinessRules(
        object? sender, ValidationRequestedEventArgs e)
    {
        messages.Clear();
        ValidateIdentifier();
        editContext.NotifyValidationStateChanged();
    }

    private void ValidateChangedField(
        object? sender, FieldChangedEventArgs e)
    {
        if (e.FieldIdentifier.FieldName != nameof(Starship.Identifier) &&
            e.FieldIdentifier.FieldName != nameof(Starship.MaximumAccommodation))
        {
            return;
        }

        messages.Clear(
            editContext.Field(nameof(Starship.Identifier)));
        ValidateIdentifier();
        editContext.NotifyValidationStateChanged();
    }

    private void ValidateIdentifier()
    {
        if (Model.MaximumAccommodation == 1 &&
            string.IsNullOrWhiteSpace(Model.Identifier))
        {
            messages.Add(
                editContext.Field(nameof(Starship.Identifier)),
                "An identifier is required for a single-occupant ship.");
        }
    }

    private void Submit()
    {
        // Process the valid form.
    }

    public void Dispose()
    {
        editContext.OnValidationRequested -= ValidateBusinessRules;
        editContext.OnFieldChanged -= ValidateChangedField;
    }
}
```

An `OnFieldChanged` handler receives the changed field in `e.FieldIdentifier`. Clear or replace the affected messages and call `NotifyValidationStateChanged`, as the preceding example demonstrates.

Static SSR doesn't provide live .NET field validation between requests.

For asynchronous full-form validation, call `e.AddAsyncValidator` from an `OnValidationRequested` handler. For asynchronous field validation in an interactive form, call `EditContext.RegisterAsyncFieldValidator` from an `OnFieldChanged` handler. A new asynchronous validation for the same field supersedes and cancels the previous one.

For model-based asynchronous validation attributes, see [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0#asynchronous-validation-support). For a complete reusable validator component, see [ASP.NET Core Blazor advanced form validation](validation-advanced?view=aspnetcore-10.0).

For a reusable implementation that encapsulates event subscriptions and its message store, see [ASP.NET Core Blazor advanced form validation](validation-advanced?view=aspnetcore-10.0#build-a-validator-component).

[]()

## Display validation messages

Use [ValidationMessage\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationmessage-1) to display messages associated with one field:

```
<ValidationMessage For="() => Model.Identifier" />
```

The `For` expression identifies the field by its model instance and property. Blazor represents this identity with a [FieldIdentifier](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.fieldidentifier), so properties with the same name on different model instances are treated as different fields. Using an expression instead of a string also allows refactoring tools to update the property reference.

Use [ValidationSummary](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationsummary) to display messages for the form:

```
<ValidationSummary />
```

Assign the summary's `Model` parameter to restrict it to messages associated with a particular model:

```
<ValidationSummary Model="Model" />
```

To inspect current messages in code, call [GetValidationMessages](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.getvalidationmessages):

```
var allMessages = editContext.GetValidationMessages();
var fieldMessages = editContext.GetValidationMessages(
    editContext.Field(nameof(Starship.Identifier)));
```

These methods read the current validation state. They don't initiate validation.

## Customize validation appearance

Blazor applies CSS classes that represent field and message state:

| Element            | Classes                                                              |
| ------------------ | -------------------------------------------------------------------- |
| Input              | `valid` or `invalid`, plus `modified` after the user edits the field |
| Validation message | `validation-message`                                                 |
| Validation summary | `validation-summary-errors` or `validation-summary-valid`            |

Inputs with asynchronous field validation use `pending` or `faulted`, optionally with `modified`, instead of `valid` or `invalid` while the corresponding state applies.

The Blazor project templates include styles for the common valid and invalid classes. Add styles for other classes as needed. `ValidationMessage` and `ValidationSummary` also accept arbitrary HTML attributes. Supplying a `class` attribute replaces the component's default class.

To change the classes applied to input components, derive from [FieldCssClassProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.fieldcssclassprovider).

```
using Microsoft.AspNetCore.Components.Forms;

public sealed class BootstrapFieldCssClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(
        EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (!editContext.IsModified(fieldIdentifier))
        {
            return string.Empty;
        }

        return editContext.IsValid(fieldIdentifier)
            ? "is-valid"
            : "is-invalid";
    }
}
```

```
using System.Linq;
using Microsoft.AspNetCore.Components.Forms;

public sealed class BootstrapFieldCssClassProvider : FieldCssClassProvider
{
    public override string GetFieldCssClass(
        EditContext editContext,
        in FieldIdentifier fieldIdentifier)
    {
        if (!editContext.IsModified(fieldIdentifier))
        {
            return string.Empty;
        }

        return editContext.GetValidationMessages(fieldIdentifier).Any()
            ? "is-invalid"
            : "is-valid";
    }
}
```

A custom `FieldCssClassProvider` determines the complete class value for each field. If the form uses asynchronous field validation, handle `IsValidationPending(fieldIdentifier)` and `IsValidationFaulted(fieldIdentifier)` in the provider when pending or faulted classes are required.

Assign the provider to the form's `EditContext`:

```
editContext.SetFieldCssClassProvider(
    new BootstrapFieldCssClassProvider());
```

For custom input markup, call [FieldCssClass](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontextfieldclassextensions.fieldcssclass) to obtain the class selected by the current provider.

When an `EditForm` is assigned a model, its child content receives the generated `EditContext`. Capture the context through the `Context` parameter and call `FieldCssClass` to apply the field's classes to surrounding markup:

```
<EditForm Model="Model" Context="editContext">
    <DataAnnotationsValidator />

    <div class="@editContext.FieldCssClass(
        () => Model.Identifier)">
        <InputText @bind-Value="Model.Identifier" />
        <ValidationMessage For="() => Model.Identifier" />
    </div>
</EditForm>
```

[]()

## Respond to validation state

`EditContext` exposes the current validation state without initiating validation.

* Use `IsModified(field)` or `IsModified()` to determine whether a field or any field in the form has changed.
* Use `GetValidationMessages(field)` or `GetValidationMessages()` to inspect current field or form messages.

Use `IsValid(field)` to determine whether a field currently has validation messages.

For a field, the absence of messages can be checked with `!editContext.GetValidationMessages(field).Any()`.

The following example displays custom UI only after a field is modified and invalid:

```
@{
    var identifier = editContext.Field(nameof(Starship.Identifier));
}

@if (editContext.IsModified(identifier) &&
    !editContext.IsValid(identifier))
{
    <p>Correct the identifier before continuing.</p>
}
```

```
@{
    var identifier = editContext.Field(nameof(Starship.Identifier));
}

@if (editContext.IsModified(identifier) &&
    editContext.GetValidationMessages(identifier).Any())
{
    <p>Correct the identifier before continuing.</p>
}
```

Input components, `ValidationMessage`, and `ValidationSummary` update themselves when validation state changes. A component that renders other conditional validation UI should subscribe to [OnValidationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.onvalidationstatechanged) and call `StateHasChanged`:

```
private void HandleValidationStateChanged(
    object? sender, ValidationStateChangedEventArgs e) =>
    _ = InvokeAsync(StateHasChanged);
```

Unsubscribe from `OnValidationStateChanged` when the component is disposed.

Use `IsValidationPending(field)` and `IsValidationFaulted(field)` for asynchronous field validation. The parameterless methods describe form-level `ValidateAsync` passes and don't aggregate the state of every field.

These states also include asynchronous work performed by [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator). An `AsyncValidationAttribute` applied to a property uses field state during field validation, including the default `pending` and `faulted` CSS classes. During `ValidateAsync`, asynchronous attributes and [IAsyncValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.iasyncvalidatableobject) contribute to the form-level state reported by the parameterless methods.

Live pending indicators require an interactive render mode. During a static SSR form post, server-side validation completes before the response is rendered.

## Control form submission

`EditForm` provides three submission callbacks:

| Callback                                                                                            | Behavior                                                |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| [OnValidSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onvalidsubmit)     | Runs after automatic validation succeeds.               |
| [OnInvalidSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.oninvalidsubmit) | Runs after automatic validation fails.                  |
| [OnSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onsubmit)               | Gives the handler control of validation and submission. |

`OnValidSubmit` and `OnInvalidSubmit` can be used together. Don't combine `OnSubmit` with either of them.

`EditForm` uses [ValidateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.validateasync) before invoking `OnValidSubmit` or `OnInvalidSubmit`, so it awaits synchronous and asynchronous validators. When handling `OnSubmit`, call `ValidateAsync` before processing the form:

```
<EditForm EditContext="editContext" OnSubmit="HandleSubmit">
    ...
</EditForm>

@code {
    private async Task HandleSubmit(EditContext editContext)
    {
        if (await editContext.ValidateAsync())
        {
            await SaveAsync();
        }
    }
}
```

The synchronous [Validate](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.validate) method is obsolete in .NET 11. It doesn't await asynchronous validation and throws if a handler attempts to register asynchronous work.

For interactive forms, the form-level pending state can be used to disable submission while `ValidateAsync` is running:

```
<button type="submit" disabled="@editContext.IsValidationPending()">
    Save
</button>
```

When handling `OnSubmit`, call [Validate](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.validate) before processing the form:

```
<EditForm EditContext="editContext" OnSubmit="HandleSubmit">
    ...
</EditForm>

@code {
    private void HandleSubmit(EditContext editContext)
    {
        if (editContext.Validate())
        {
            Save();
        }
    }
}
```

## Related content

* [ASP.NET Core Blazor forms overview](./?view=aspnetcore-10.0)
* [ASP.NET Core Blazor forms binding](binding?view=aspnetcore-10.0)
* [ASP.NET Core Blazor input components](input-components?view=aspnetcore-10.0)
* [ASP.NET Core Blazor advanced form validation](validation-advanced?view=aspnetcore-10.0)

- [Validation in ASP.NET Core](../../fundamentals/validation?view=aspnetcore-10.0)

* [ASP.NET Core Blazor client-side form validation in static SSR](validation-client-side?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-25

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core fundamentals overview

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article provides an overview of the fundamentals for building ASP.NET Core apps, including dependency injection (DI), configuration, and middleware.

For Blazor fundamentals guidance, which adds to or supersedes the guidance in this article, see [ASP.NET Core Blazor fundamentals](../blazor/fundamentals/?view=aspnetcore-10.0).

## The `Program` file

ASP.NET Core apps created from the framework's project templates contain startup code in the `Program` file (`Program.cs`). The `Program` file is where:

* Services required by the app are configured.
* The app's request handling pipeline is defined as a series of [middleware components](middleware/?view=aspnetcore-10.0).

The following app startup code supports two app types:

* [Blazor Web Apps](../blazor/?view=aspnetcore-10.0)
* [Minimal APIs](../tutorials/min-web-api?view=aspnetcore-10.0)

```
// Initialize a new instance of the WebApplicationBuilder class 
// with preconfigured defaults
var builder = WebApplication.CreateBuilder(args);

// Add services for Blazor (Razor components)
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// Build the app
var app = builder.Build();

// Use exception-handling middleware and HSTS middleware
// when in the Development environment
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);

// Use HTTPS redirection middleware to automatically
// redirect requests from HTTP to HTTPS
app.UseHttpsRedirection();

// Map static assets endpoints
app.MapStaticAssets();

// Map a Minimal API endpoint for requests to '/hi'
app.MapGet("/hi", () => "Hello!");

// Add endpoints for Blazor
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

// Run the app
app.Run();
```

Note

With additional configuration in the `Program` file, ASP.NET Core apps can support [Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0), [MVC](../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0), and [web API with controllers](../tutorials/first-web-api?view=aspnetcore-10.0).

The following app startup code supports two app types:

* [Blazor Web Apps](../blazor/?view=aspnetcore-10.0)
* [Minimal APIs](../tutorials/min-web-api?view=aspnetcore-10.0)

```
// Initialize a new instance of the WebApplicationBuilder class 
// with preconfigured defaults
var builder = WebApplication.CreateBuilder(args);

// Add services for Blazor (Razor components)
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// Build the app
var app = builder.Build();

// Use exception-handling middleware and HSTS middleware
// when in the Development environment
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);

// Use HTTPS redirection middleware to automatically
// redirect requests from HTTP to HTTPS
app.UseHttpsRedirection();

// Add antiforgery middleware
app.UseAntiforgery();

// Map static assets endpoints
app.MapStaticAssets();

// Map a Minimal API endpoint for requests to '/hi'
app.MapGet("/hi", () => "Hello!");

// Add endpoints for Blazor
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

// Run the app
app.Run();
```

Note

With additional configuration in the `Program` file, ASP.NET Core apps can support [Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0), [MVC](../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0), and [web API with controllers](../tutorials/first-web-api?view=aspnetcore-10.0).

The following app startup code supports several app types:

* [Blazor Web Apps](../blazor/?view=aspnetcore-10.0)
* [Razor Pages](../tutorials/razor-pages/?view=aspnetcore-10.0)
* [MVC controllers with views](../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0)
* [Minimal APIs](../tutorials/min-web-api?view=aspnetcore-10.0)
* [Web API with controllers](../tutorials/first-web-api?view=aspnetcore-10.0)

```
// Initialize a new instance of the WebApplicationBuilder class 
// with preconfigured defaults
var builder = WebApplication.CreateBuilder(args);

// Add services for Blazor (Razor components), Razor Pages, and MVC
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

// Build the app
var app = builder.Build();

// Configure the HTTP request pipeline

// Use exception-handling middleware and HSTS middleware
// when in the Development environment
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Use HTTPS redirection middleware to automatically
// redirect requests from HTTP to HTTPS
app.UseHttpsRedirection();

// Use static files middleware to serve static assets
app.UseStaticFiles();

// Use authorization middleware
app.UseAuthorization();

// Add antiforgery middleware
app.UseAntiforgery();

// Map a Minimal API endpoint for requests to '/hi'
app.MapGet("/hi", () => "Hello!");

// Configures the standard conventional route for MVC
app.MapDefaultControllerRoute();

// Add endpoints for Razor Pages
app.MapRazorPages();

// Add endpoints for Blazor
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

// Run the app
app.Run();
```

The following app startup code supports:

* [Razor Pages](../tutorials/razor-pages/?view=aspnetcore-10.0)
* [MVC controllers with views](../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0)
* [Minimal APIs](../tutorials/min-web-api?view=aspnetcore-10.0)
* [Web API with controllers](../tutorials/first-web-api?view=aspnetcore-10.0)

```
// Initialize a new instance of the WebApplicationBuilder class 
// with preconfigured defaults
var builder = WebApplication.CreateBuilder(args);

// Add services for Razor Pages and MVC
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

// Build the app
var app = builder.Build();

// Configure the HTTP request pipeline

// Use exception-handling middleware and HSTS middleware
// when in the Development environment
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Use HTTPS redirection middleware to automatically
// redirect requests from HTTP to HTTPS
app.UseHttpsRedirection();

// Use static files middleware to serve static assets
app.UseStaticFiles();

// Use authorization middleware
app.UseAuthorization();

// Map a Minimal API endpoint for requests to '/hi'
app.MapGet("/hi", () => "Hello!");

// Configures the standard conventional route for MVC
app.MapDefaultControllerRoute();

// Add endpoints for Razor Pages
app.MapRazorPages();

// Run the app
app.Run();
```

## The `Startup` class

The `Startup` class (`Startup.cs`) is where:

* Services required by the app are configured in the `ConfigureServices` method.
* The app's request handling pipeline is defined in the `Configure` method as a series of [middleware components](middleware/?view=aspnetcore-10.0).

The following app startup code supports:

* [Razor Pages](../tutorials/razor-pages/?view=aspnetcore-10.0)
* [MVC controllers with views](../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0)
* [Web API with controllers](../tutorials/first-web-api?view=aspnetcore-10.0)

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<RazorPagesMovieContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext")));

        services.AddControllersWithViews();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
            endpoints.MapRazorPages();
        });
    }
}
```

For more information, see [App startup in ASP.NET Core](startup?view=aspnetcore-10.0) and [ASP.NET Core Blazor startup](../blazor/fundamentals/startup?view=aspnetcore-10.0).

## Dependency injection (services)

ASP.NET Core features built-in [dependency injection (DI)](dependency-injection?view=aspnetcore-10.0) that makes configured services available throughout an app for [Inversion of Control (IoC)](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#dependency-inversion).

When the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) is instantiated by calling [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder), [framework-provided services](dependency-injection?view=aspnetcore-10.0#framework-provided-services) are automatically added, such as services for configuration and logging:

```
var builder = WebApplication.CreateBuilder(args);
```

Additional services are added to the DI container with [WebApplicationBuilder.Services](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.services#microsoft-aspnetcore-builder-webapplicationbuilder-services). The following example registers [Blazor](../blazor/?view=aspnetcore-10.0) services:

```
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
```

```
builder.Services.AddServerSideBlazor();
```

The DI framework provides instances of requested services at run time. In Blazor apps, services are often resolved from DI at run time using the [`@inject`](../mvc/views/razor?view=aspnetcore-10.0#inject) directive in a [Razor component](../blazor/components/?view=aspnetcore-10.0) file (`.razor`). In the following example, the component uses the [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager) abstraction to get an instance of the navigation manager, which is used for querying and managing URI navigation, to navigate the user to a page of products at `/products` when the button is selected:

```
@inject NavigationManager Navigation

<button @onclick="NavigateToProductList">
    Products
</button>

@code {
    private void NavigateToProductList()
    {
        Navigation.NavigateTo("/products");
    }
}
```

Another way to resolve a service from DI is using constructor injection. In the following example, the [primary constructor (C# 12 or later)](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) takes parameters of the types `AppDbContext` and `ILogger<OrderProcessor>` and resolves them at run time into the `context` and `logger` variables (the instances of the database and logging abstractions). The database context instance is used to process all of the orders where the `IsProcessed` field is `false` in the database, and each processed order is logged as information with its order ID (`OrderId`) using the logger instance:

```
public class OrderProcessor(AppDbContext context, ILogger<OrderProcessor> logger)
{
    public async Task ProcessPendingOrdersAsync()
    {
        var orders = await context.Orders
            .Where(o => !o.IsProcessed)
            .ToListAsync();

        foreach (var order in orders)
        {
            order.IsProcessed = true;
            logger.LogInformation("Processed order ID {OrderId}.", order.Id);
        }

        await context.SaveChangesAsync();
    }
}
```

You can also inject dependencies directly into the lambda parameters of [Minimal API](../tutorials/min-web-api?view=aspnetcore-10.0) endpoints. In the following example, a list of todo items is returned from the `/todos` endpoint. A logger instance for `ILogger<Program>` logs information, and the database instance for `AppDbContext` is used to obtain the list of todo items from the database to include in the response:

```
app.MapGet("/todos", async (AppDbContext context, ILogger<Program> logger) =>
{
    logger.LogInformation("Fetching todos using inline handler injection.");
    var todos = await context.Todos.ToListAsync();

    return Results.Ok(todos);
});
```

When the [Host.CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder) is called in the `Program` file, a new instance of the [HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder) class is automatically initialized with [framework-provided services](dependency-injection?view=aspnetcore-10.0#framework-provided-services), such as services for configuration and logging:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

Additional services are added to the DI container's service collection ([IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection)) in the `Startup.ConfigureServices` method (`Startup.cs`). The following example registers MVC and Razor Pages services:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
}
```

Services are typically resolved from DI using constructor injection. With constructor injection, a class declares a constructor parameter of either the required type or an interface. The DI framework provides an instance of the service at runtime.

If the built-in DI container doesn't meet your needs, a third-party IoC container can be used instead.

For more information, see [Dependency injection in ASP.NET Core](dependency-injection?view=aspnetcore-10.0) and [ASP.NET Core Blazor dependency injection](../blazor/fundamentals/dependency-injection?view=aspnetcore-10.0).

## Environments

Execution environments are available in ASP.NET Core, such as:

* `Development`: When the app is in local development.
* `Staging`: When the app is staged for deployment.
* `Production`: When the live app is running for users.

Specify the environment an app is running in by setting the `ASPNETCORE_ENVIRONMENT` environment variable on the host where the app is running. ASP.NET Core reads the environment variable at app startup and stores the value to control code execution around the app.

Developer code can check for a given environment. In the following `Program` file example, the code in the execution block only runs when the app isn't running in the `Development` environment:

```
if (!app.Environment.IsDevelopment())
{
    ...
}
```

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (!env.IsDevelopment())
    {
        ...
    }

    ...
}
```

For more information, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0) and [ASP.NET Core Blazor environments](../blazor/fundamentals/environments?view=aspnetcore-10.0).

## Middleware

The request handling pipeline is composed as a series of middleware components. Each component performs operations on an [`HttpContext`](http-context?view=aspnetcore-10.0) and either invokes the next middleware in the pipeline or terminates the request.

By convention, middleware components are added to the pipeline by invoking an extension method that starts with "`Use`." In the following example representing part of a request processing pipeline, middleware for exception handling ([UseExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler)), [HTTP Strict Transport Security (HSTS) protocol](../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) ([UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts)), and HTTPS redirection ([UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection)) are called. Two of the middlewares are only triggered when the app isn't under local development in the `Development` environment, such as when the app is staged for deployment (the `Staging` environment) or in production (the `Production` environment):

```
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
```

```
if (env.IsDevelopment())
{
    ...
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
```

ASP.NET Core includes a rich set of built-in middleware. You can also create custom middleware components to meet an app's special request processing specifications. For more information, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0).

## Host

On startup, an ASP.NET Core app builds a *host*. The host encapsulates all of the app's resources, such as:

* An HTTP server implementation
* Middleware components
* Logging
* Dependency injection (DI) services
* Configuration

There are three different hosts capable of running an ASP.NET Core app:

* [ASP.NET Core WebApplication](minimal-apis/webapplication?view=aspnetcore-10.0) (also known as the [Minimal Host](../migration/50-to-60?view=aspnetcore-10.0#new-hosting-model))
* [.NET Generic Host](host/generic-host?view=aspnetcore-10.0)
* [ASP.NET Core WebHost](host/web-host?view=aspnetcore-10.0)

The ASP.NET Core [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) and [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) types are recommended and are used in all of the ASP.NET Core project templates. `WebApplication` behaves similarly to the .NET Generic Host and exposes many of the same interfaces but requires fewer callbacks to configure. The ASP.NET Core [WebHost](/en-us/dotnet/api/microsoft.aspnetcore.webhost) is only available for backward compatibility.

The following example instantiates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) and assigns it to a variable named `app`:

```
var builder = WebApplication.CreateBuilder(args);

...

var app = builder.Build();
```

The [WebApplicationBuilder.Build](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.build) method configures a host with a set of default options, such as:

* Using [Kestrel](#servers) as the web server and enabling IIS integration.
* Loading [configuration](configuration/?view=aspnetcore-10.0) from app settings files (for example, `appsettings.json`), environment variables, command line arguments, and other configuration sources.
* Setting up logging and directing logging output to the console and debug logging providers.

There are two hosts:

* [.NET Generic Host](host/generic-host?view=aspnetcore-10.0)
* [ASP.NET Core WebHost](host/web-host?view=aspnetcore-10.0)

The .NET Generic Host is recommended. The ASP.NET Core Web Host is only available for backwards compatibility.

The [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder) and [ConfigureWebHostDefaults](/en-us/dotnet/api/microsoft.extensions.hosting.generichostbuilderextensions.configurewebhostdefaults) methods in the following example configure a host with a set of default options, such as:

* Using [Kestrel](#servers) as the web server and enabling IIS integration.
* Loading [configuration](configuration/?view=aspnetcore-10.0) from app settings files (for example, `appsettings.json`), environment variables, command line arguments, and other configuration sources.
* Setting up logging and directing logging output to the console and debug logging providers.

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

For more information, see the following resources:

* [.NET Generic Host in ASP.NET Core](host/generic-host?view=aspnetcore-10.0) (*Recommended*)
* [ASP.NET Core Web Host](host/web-host?view=aspnetcore-10.0) (*For backwards compatibility*)

### Non-web scenarios

The [Generic Host](host/generic-host?view=aspnetcore-10.0) enables other types of apps to use cross-cutting framework extensions, such as logging, dependency injection (DI), configuration, and app lifetime management. For more information, see [.NET Generic Host in ASP.NET Core](host/generic-host?view=aspnetcore-10.0) and [Background tasks with hosted services in ASP.NET Core](host/hosted-services?view=aspnetcore-10.0).

## Servers

An ASP.NET Core app uses an HTTP server implementation to listen for HTTP requests. The server surfaces requests to the app as a set of [request features](request-features?view=aspnetcore-10.0) composed into an [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext).

For more information, see [Web server implementations in ASP.NET Core](servers/?view=aspnetcore-10.0).

### Windows

ASP.NET Core provides the following server implementations:

* *Kestrel* is a cross-platform web server. Kestrel is often run in a reverse proxy configuration using [IIS](https://www.iis.net/). In ASP.NET Core 2.0 or later, Kestrel can be run as a public-facing edge server exposed directly to the Internet.
* *IIS HTTP Server* is a server for Windows that uses IIS. With this server, the ASP.NET Core app and IIS run in the same process.
* *HTTP.sys* is a server for Windows that isn't used with IIS.

### macOS and Linux

ASP.NET Core provides the *Kestrel* cross-platform server implementation. In ASP.NET Core 2.0 or later, Kestrel can run as a public-facing edge server exposed directly to the Internet. Kestrel is often run in a reverse proxy configuration with [Nginx](https://nginx.org) or [Apache](https://httpd.apache.org/).

## Configuration

ASP.NET Core provides a [configuration](configuration/?view=aspnetcore-10.0) framework that obtains settings as name-value pairs from an ordered set of configuration providers. Built-in configuration providers are available for a variety of sources, such as JSON files (`.json`), XML files (`.xml`), environment variables, and command-line arguments. You can create custom configuration providers to support other sources.

By default, ASP.NET Core apps are configured to read from app settings files (for example, `appsettings.json`), environment variables, and the command line.

When the app's configuration is loaded, values from environment variables override values from app settings files. The [Options API](configuration/options?view=aspnetcore-10.0) is available for reading related configuration values.

For managing confidential configuration data such as passwords in the `Development` environment, .NET provides the [Secret Manager](../security/app-secrets?view=aspnetcore-10.0#secret-manager). For production secrets, we recommend using [Azure Key Vault](https://azure.microsoft.com/products/key-vault).

For more information, see the following resources:

* [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor configuration](../blazor/fundamentals/configuration?view=aspnetcore-10.0)
* [Azure Key Vault (ASP.NET Core documentation)](../security/key-vault-configuration?view=aspnetcore-10.0)

## Logging

ASP.NET Core supports a logging API that works with a variety of logging providers:

* Console
* Debug
* Event Tracing on Windows
* Windows Event Log
* TraceSource
* Azure App Service
* Azure Application Insights
* Third-party providers

To create logs, resolve an [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) service from dependency injection (DI) and call logging methods, such as [LogInformation](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.loginformation). A logger object and a console provider for the logger object are stored in the DI container automatically when the [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) method is called.

The following example shows how to obtain a logging instance from DI and use it in a `Weather` component (`Weather.razor`) of a Blazor app that reports weather data:

```
@inject ILogger<Weather> Logger

...

@code {
    protected override async Task OnInitializedAsync()
    {
        Logger.LogInformation("OnInitializedAsync method called!");

        ...
    }
}
```

For more information, including routing guidance for Razor Pages and MVC apps, see [Logging in .NET and ASP.NET Core](logging/?view=aspnetcore-10.0) and [ASP.NET Core Blazor logging](../blazor/fundamentals/logging?view=aspnetcore-10.0).

## Routing

Routing in ASP.NET Core is a mechanism that maps incoming requests to specific endpoints in an app. It enables you to define URL patterns that correspond to different components, such as Razor components, Razor pages, MVC controller actions, or middleware.

The [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) method adds routing middleware to the request pipeline. This middleware processes the routing information and determines the appropriate endpoint for each request. In apps using the [Minimal Host](#host), `UseRouting` isn't explicitly called in developer code unless you want to change middleware processing order.

For more information, see the following resources:

* [Routing in ASP.NET Core](routing?view=aspnetcore-10.0)
* [ASP.NET Core Blazor routing](../blazor/fundamentals/routing?view=aspnetcore-10.0)
* [ASP.NET Core Blazor navigation](../blazor/fundamentals/navigation?view=aspnetcore-10.0)

## Handle errors

ASP.NET Core has built-in features for handling errors, such as:

* A developer exception page
* Custom error pages
* Static status code pages
* Startup exception handling

For more information, see [Handle errors in ASP.NET Core](error-handling?view=aspnetcore-10.0) and [Handle errors in ASP.NET Core Blazor apps](../blazor/fundamentals/handle-errors?view=aspnetcore-10.0).

## Make HTTP requests

An implementation of [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) is available for creating [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances. The factory:

* Provides a central location for naming and configuring logical `HttpClient` instances. For example, rely on a default client for most of the app's data requests with a web API and register a different configured client for accessing GitHub.
* Supports registration and chaining of multiple delegating handlers to build an outgoing request middleware pipeline. This pattern is similar to ASP.NET Core's inbound middleware pipeline. The pattern provides a mechanism to manage cross-cutting concerns for HTTP requests, including caching, error handling, serialization, and logging.
* Integrates with *Polly*, a popular third-party library for transient fault handling.
* Manages the pooling and lifetime of underlying [HttpClientHandler](/en-us/dotnet/api/system.net.http.httpclienthandler) instances to avoid common DNS problems that occur when managing `HttpClient` lifetimes manually.
* Adds a configurable logging experience via [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) for all requests sent through clients created by the factory.

For more information, see [HTTP requests with IHttpClientFactory - ASP.NET Core](http-requests?view=aspnetcore-10.0) and [Call a web API from an ASP.NET Core Blazor app](../blazor/call-web-api?view=aspnetcore-10.0).

## Content root

The content root is the base path for:

* The executable hosting the app (`.exe`).
* Compiled assemblies that make up the app (`.dll`).
* Content files used by the app, such as Razor files (`.cshtml`, `.razor`), configuration files (`.json`, `.xml`), and data files (`.db`).
* The [web root](#web-root), which is typically the `wwwroot` folder.

During development, the content root defaults to the project's root directory. This directory is also the base path for both the app's content files and the [web root](#web-root). Specify a different content root by setting its path when building the [host](#host).

For more information, see [.NET Generic Host in ASP.NET Core](host/generic-host?view=aspnetcore-10.0#contentroot) and [Serve static files in ASP.NET Core apps](static-files?view=aspnetcore-10.0).

## Web root

The web root is the base path for public, static resource files, such as stylesheets, JavaScript files, and images.

By default, static files are only served from the web root directory and its sub-directories. The web root path defaults to `{CONTENT ROOT}/wwwroot`, where the `{CONTENT ROOT}` placeholder is the content root. Specify a different web root by setting its path when [building the host](#host). You can also prevent publishing files in `wwwroot` with the [`<Content>` project item](/en-us/visualstudio/msbuild/common-msbuild-project-items#content) in the app's project file.

In Razor `.cshtml` files, `~/` points to the web root. A path beginning with `~/` is referred to as a *virtual path*.

For more information, see [.NET Generic Host in ASP.NET Core](host/generic-host?view=aspnetcore-10.0#webroot) and [Serve static files in ASP.NET Core apps](static-files?view=aspnetcore-10.0).

## How to download a sample

Many of the articles and tutorials include links to sample code.

1. [Download the ASP.NET repository zip file](https://codeload.github.com/dotnet/AspNetCore.Docs/zip/main).
2. Unzip the `AspNetCore.Docs-main.zip` file.
3. To access an article's sample app in the unzipped repository, use the URL in the article's sample link to help you navigate to the sample's folder. Usually, an article's sample link appears at the top of the article with the link text *View or download sample code*.

To obtain a single sample app and only its last commit, use [`git sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout).

In the following example for the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples), the `git sparse-checkout set` command specifies the path to the sample folder:

* Replace the `{VERSION FOLDER}` placeholder with the version folder.
* Replace the `{SAMPLE FOLDER}` placeholder with the sample folder.

In a command shell, navigate to the folder where you would like to clone the sample. Execute the following commands in the command shell passing the version/sample folder path to the `git sparse-checkout set` command:

```
git clone --depth 1 --filter=blob:none https://github.com/dotnet/blazor-samples.git --sparse
cd blazor-samples
git sparse-checkout init --cone
git sparse-checkout set {VERSION FOLDER}/{SAMPLE FOLDER}
```

The following [PowerShell](/en-us/powershell/) example obtains the 10.0 Blazor Web App sample and places it in the user's documents folder using PowerShell's `~/documents` path for the change directory (`cd`) command:

```
cd "~/documents"
git clone --depth 1 --filter=blob:none https://github.com/dotnet/blazor-samples.git --sparse
cd blazor-samples
git sparse-checkout init --cone
git sparse-checkout set 10.0/BlazorSample_BlazorWebApp
```

## Preprocessor directives in sample code

To demonstrate multiple scenarios, sample apps use the `#define` and `#if-#else/#elif-#endif` preprocessor directives to selectively compile and run different sections of sample code. For those samples that make use of this approach, set the `#define` directive at the top of the C# files to define the symbol associated with the scenario that you want to run. Some samples require defining the symbol at the top of multiple files in order to run a scenario.

For example, the following `#define` symbol list indicates that four scenarios are available (one scenario per symbol). The current sample configuration runs the `TemplateCode` scenario:

```
#define TemplateCode // or LogFromMain or ExpandDefault or FilterInCode
```

To change the sample to run the `ExpandDefault` scenario, define the `ExpandDefault` symbol and leave the remaining symbols commented-out:

```
#define ExpandDefault // TemplateCode or LogFromMain or FilterInCode
```

For more information on using [C# preprocessor directives](/en-us/dotnet/csharp/language-reference/preprocessor-directives/) to selectively compile sections of code, see [#define (C# Reference)](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-define) and [#if (C# Reference)](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-if).

## Regions in sample code

Some sample apps contain sections of code surrounded by [#region](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-region) and [#endregion](/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-endregion) C# directives. The documentation build system injects these regions into the rendered documentation topics.

Region names usually contain the word "snippet." The following example shows a region named `snippet_WebHostDefaults`:

```
#region snippet_WebHostDefaults
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    });
#endregion
```

The preceding C# code snippet is referenced in the topic's markdown file with the following line:

```
[!code-csharp[](sample/SampleApp/Program.cs?name=snippet_WebHostDefaults)]
```

You may safely ignore or remove the `#region` and `#endregion` directives that surround the code. Don't alter the code within these directives if you plan to run the sample scenarios described in the topic.

For more information, see [Contribute to the ASP.NET documentation: Code snippets](https://github.com/dotnet/AspNetCore.Docs/blob/main/CONTRIBUTING.md#code-snippets).

## Document Object Model (DOM)

References to the *Document Object Model* throughout this documentation set use the abbreviation *DOM*.

For more information, see [Introduction to the DOM (MDN documentation)](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction) and [Level 1 Document Object Model Specification (W3C)](https://www.w3.org/TR/WD-DOM/).

## Byte multiples

.NET byte sizes use metric prefixes for non-decimal multiples of bytes based on powers of 1024.

| Name (abbreviation) | Size         | Example                    |
| ------------------- | ------------ | -------------------------- |
| Kilobyte (KB)       | 1,024 bytes  | 1 KB = 1,024 bytes         |
| Megabyte (MB)       | 1,0242 bytes | 1 MB = 1,048,576 bytes     |
| Gigabyte (GB)       | 1,0243 bytes | 1 GB = 1,073,741,824 bytes |

## Support requests

Only documentation-related issues are appropriate for the `dotnet/AspNetCore.Docs` repository. ***For product support, don't open a documentation issue.*** Seek assistance through one or more of the following support channels:

* [Stack Overflow for ASP.NET Core (tagged: `asp.net-core`)](https://stackoverflow.com/questions/tagged/asp.net-core)
* [Stack Overflow for Blazor (tagged: `blazor`)](https://stackoverflow.com/questions/tagged/blazor)
* [General ASP.NET Core Slack Team](https://join.slack.com/t/aspnetcore/shared_invite/zt-1mv5487zb-EOZxJ1iqb0A0ajowEbxByQ)
* [Blazor Gitter](https://gitter.im/aspnet/Blazor)

For a potential bug in the framework or product feedback, open an issue for the ASP.NET Core product unit at [`dotnet/aspnetcore` issues](https://github.com/dotnet/aspnetcore/issues). Bug reports usually require the following:

* **Clear explanation of the problem**: Follow the instructions in the GitHub issue template provided by the product unit when opening the issue.
* **Minimal repro project**: Place a project on GitHub for the product unit engineers to download and run. Cross-link the project into the issue's opening comment.

For a potential problem with an article, open a documentation issue. To open a documentation issue, use the **Open a documentation issue** feedback link at the bottom of the article. Metadata added to your issue provides tracking data and automatically pings the author of the article. If the subject was discussed with the product unit prior to opening the documentation issue, place a cross-link to the engineering issue in the documentation issue's opening comment.

GitHub issues for Blazor documentation are automatically marked for triage on the [`Blazor.Docs` project (`dotnet/AspNetCore.Docs` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs/projects/35). Please wait a short while for a response, especially over weekends and holidays. Usually, documentation authors respond within 24 hours on weekdays.

For problems or feedback on Visual Studio, use the [**Report a Problem**](/en-us/visualstudio/ide/how-to-report-a-problem-with-visual-studio) or [**Suggest a Feature**](/en-us/visualstudio/ide/suggest-a-feature) gestures from within Visual Studio, which open internal issues for Visual Studio. For more information, see [Visual Studio Feedback](https://developercommunity.visualstudio.com/home).

For problems with Visual Studio Code, ask for support on community support forums. For bug reports and product feedback, open an issue on the [`microsoft/vscode` GitHub repo](https://github.com/microsoft/vscode/issues).

## Related content

* Tutorials

  * [Build a Blazor todo list app](../blazor/tutorials/build-a-blazor-app?view=aspnetcore-10.0)
  * [Build a Blazor movie database app (Overview)](../blazor/tutorials/movie-database-app/?view=aspnetcore-10.0)
  * [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0)

* [ASP.NET Core Blazor fundamentals](../blazor/fundamentals/?view=aspnetcore-10.0)

- Tutorials

  * [Build a Blazor todo list app](../blazor/tutorials/build-a-blazor-app?view=aspnetcore-10.0)
  * [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0)

- [ASP.NET Core Blazor fundamentals](../blazor/fundamentals/?view=aspnetcore-10.0)

* Tutorials

  * [Build a Blazor todo list app](../blazor/tutorials/build-a-blazor-app?view=aspnetcore-10.0)
  * [Tutorial: Create a Razor Pages web app with ASP.NET Core](../tutorials/razor-pages/?view=aspnetcore-10.0)
  * [Get started with ASP.NET Core MVC](../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0)
  * [Tutorial: Create a controller-based web API with ASP.NET Core](../tutorials/first-web-api?view=aspnetcore-10.0)

* [ASP.NET Core Blazor fundamentals](../blazor/fundamentals/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-25

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-enable-qrcodes
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/identity-enable-qrcodes.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Enable QR code generation for TOTP authenticator apps in ASP.NET Core

ASP.NET Core includes support for authenticator applications for user authentication. Two-factor authentication (2FA) authenticator apps use a Time-based One-time Password Algorithm (TOTP), the industry-recommended approach for 2FA. (TOTP-based 2FA is preferred over SMS 2FA.) Users typically install the authenticator app on a smartphone. The app provides a 6 to 8 digit code that the user enters after they confirm their username and password.

Warning

Keep the ASP.NET Core TOTP code secret. The user can enter the code multiple times and authenticate successfully before it expires.

The ASP.NET Core web app templates support authenticators, but they don't provide support for QR code generation. QR code generators make it easier to set up 2FA. This article provides guidance for Razor Pages and MVC apps on how to add [QR code](https://wikipedia.org/wiki/QR_code) generation to the 2FA configuration page.

The ASP.NET Core web app templates support authenticators but don't provide support for QR code generation. QR code generators make it easier to set up 2FA. This article guides you through adding [QR code](https://wikipedia.org/wiki/QR_code) generation to the 2FA configuration page.

Two-factor authentication doesn't happen by using an external authentication provider, such as [Google](social/google-logins?view=aspnetcore-10.0) or [Facebook](social/facebook-logins?view=aspnetcore-10.0). External sign ins are protected by whatever mechanism the external authentication provider supports. For example, the [Microsoft](social/microsoft-logins?view=aspnetcore-10.0) authentication provider requires a hardware key or another 2FA approach. When the default templates require 2FA for both the web app and the external authentication provider, users need to satisfy two 2FA approaches. Requiring two 2FA approaches deviates from established security practices, which typically rely on a single, strong 2FA method for authentication.

If you're working with Blazor in ASP.NET Core 8.0 or later, you can find similar guidance in the following articles:

* [Enable QR code generation for TOTP authenticator apps in an ASP.NET Core Blazor Web App](../../blazor/security/qrcodes-for-authenticator-apps?view=aspnetcore-10.0)
* [Enable QR code generation for TOTP authenticator apps in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](../../blazor/security/webassembly/standalone-with-identity/qrcodes-for-authenticator-apps?view=aspnetcore-10.0)

## Add QR codes to the 2FA configuration page

The following instructions use the *qrcode.js* file from the <https://davidshimjs.github.io/qrcodejs/> repo.

1. Download the ['qrcode.js' JavaScript library](https://davidshimjs.github.io/qrcodejs/) to the *wwwroot\lib* folder in your project.

2. Follow the instructions in [Scaffold Identity](scaffold-identity?view=aspnetcore-10.0) to generate the */Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml* file.

3. In the */Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml* file, locate the `Scripts` section at the end of the file:

   ```
   @section Scripts {
      <partial name="_ValidationScriptsPartial" />
   }
   ```

4. Create a new JavaScript file named *qr.js* in the *wwwroot/js* folder, and add the following code that generates the QR code:

   ```
   window.addEventListener("load", () => {
   const uri = document.getElementById("qrCodeData").getAttribute('data-url');
   new QRCode(document.getElementById("qrCode"),
      {
         text: uri,
         width: 150,
         height: 150
      });
   });
   ```

5. Update the `Scripts` section to add a reference to the `qrcode.js` library you previously downloaded.

6. Add the *qr.js* file with the call that generates the QR code:

   ```
   @section Scripts {
      <partial name="_ValidationScriptsPartial" />
      <script type="text/javascript" src="~/lib/qrcode.js"></script>
      <script type="text/javascript" src="~/js/qr.js"></script>
   }
   ```

7. Delete the paragraph that links you to these instructions.

8. Run your app. Confirm you can scan the QR code and validate the code the authenticator provides.

## Change the site name in the QR code

The site name in the QR code comes from the project name you select when you create your project. You can change it by looking for the `GenerateQrCodeUri(string email, string unformattedKey)` method in the */Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs* file.

Here's the default code from the template:

```
private string GenerateQrCodeUri(string email, string unformattedKey)
{
    return string.Format(
        AuthenticatorUriFormat,
        _urlEncoder.Encode("Razor Pages"),
        _urlEncoder.Encode(email),
        unformattedKey);
}
```

The second parameter in the call to `string.Format` is your site name, which is obtained from your solution name. You can change it to any value, but it must always be URL encoded.

## Use a different QR code library

You can replace the QR code library with your preferred library. The HTML contains a `qrCode` element into which you can place a QR code by whatever mechanism your library provides.

You can find the correctly formatted URL for the QR code in the following locations:

* `AuthenticatorUri` property of the model
* `data-url` property in the `qrCodeData` element

## Check TOTP client and server times

TOTP (Time-based One-Time Password) authentication depends on both the server and authenticator device having an accurate time. Tokens only last for 30 seconds. If TOTP 2FA sign-in fails, confirm the server time is accurate, and preferably synchronized to an accurate NTP service.

## Related content

* [Enable QR code generation for TOTP authenticator apps in an ASP.NET Core Blazor Web App](../../blazor/security/qrcodes-for-authenticator-apps?view=aspnetcore-10.0)
* [Enable QR code generation for TOTP authenticator apps in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](../../blazor/security/webassembly/standalone-with-identity/qrcodes-for-authenticator-apps?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/using-openapi-documents
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/openapi/using-openapi-documents.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use openAPI documents

## Use Swagger UI for local ad-hoc testing

By default, the `Microsoft.AspNetCore.OpenApi` package doesn't ship with built-in support for visualizing or interacting with the OpenAPI document. Popular tools for visualizing or interacting with the OpenAPI document include [Swagger UI](https://swagger.io/tools/swaggerhub/) and [ReDoc](https://github.com/Redocly/redoc). Swagger UI and ReDoc can be integrated in an app in several ways. Editors such as Visual Studio and Visual Studio Code offer extensions and built-in experiences for testing against an OpenAPI document.

The `Swashbuckle.AspNetCore.SwaggerUi` package provides a bundle of Swagger UI's web assets for use in apps. This package can be used to render a UI for the generated document. To configure this:

* Install the `Swashbuckle.AspNetCore.SwaggerUi` package.
* Enable the swagger-ui middleware with a reference to the [OpenAPI route registered earlier](aspnetcore-openapi?view=aspnetcore-10.0#customize-the-openapi-endpoint-route).

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();

    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/openapi/v1.json", "v1");
    });

}

app.MapGet("/", () => "Hello world!");

app.Run();
```

As a security best practice on limiting information disclosure, ***OpenAPI user interfaces (Swagger UI, ReDoc, Scalar) should only be enabled in development environments.*** For example, see [Swagger OAuth 2.0 configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/).

Launch the app and navigate to `https://localhost:<port>/swagger` to view the Swagger UI.

To automatically launch the app at the Swagger UI URL using the `https` profile of `Properties/launchSettings.json`:

* Confirm that `launchBrowser` is enabled (`true`).
* Set the `launchUrl` to `swagger`.

```
"launchBrowser": true,
"launchUrl": "swagger",
```

## Use Scalar for interactive API documentation

[Scalar](https://scalar.com/) is an open-source interactive document UI for OpenAPI. Scalar can integrate with the OpenAPI endpoint provided by ASP.NET Core. To configure Scalar, install the `Scalar.AspNetCore` package.

```
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

Launch the app and navigate to `https://localhost:<port>/scalar` to view the Scalar UI.

To automatically launch the app at the Scalar UI URL using the `https` profile of `Properties/launchSettings.json`:

* Confirm that `launchBrowser` is enabled (`true`).
* Set the `launchUrl` to `scalar`.

```
"launchBrowser": true,
"launchUrl": "scalar",
```

## Lint generated OpenAPI documents with Spectral

[Spectral](https://stoplight.io/open-source/spectral) is an open-source OpenAPI document linter. Spectral can be incorporated into an app build to verify the quality of generated OpenAPI documents. Install Spectral according to the [package installation directions](https://github.com/stoplightio/spectral#-installation).

To take advantage of Spectral for linting OpenAPI documents at build time, first install the `Microsoft.Extensions.ApiDescription.Server` package to enable build-time OpenAPI document generation.

Enable document generation at build time by setting the following properties in the app's `.csproj` file":

```
<PropertyGroup>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  <OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
</PropertyGroup>
```

Run `dotnet build` to generate the document.

```
dotnet build
```

Create a `.spectral.yml` file with the following contents.

```
extends: ["spectral:oas"]
```

Run `spectral lint` on the generated file.

````
spectral lint WebMinOpenApi.json
...

The output shows any issues with the OpenAPI document. For example:

```output
1:1  warning  oas3-api-servers       OpenAPI "servers" must be present and non-empty array.
3:10  warning  info-contact           Info object must have "contact" object.                        info
3:10  warning  info-description       Info "description" must be present and non-empty string.       info
9:13  warning  operation-description  Operation "description" must be present and non-empty string.  paths./.get
9:13  warning  operation-operationId  Operation must have "operationId".                             paths./.get

✖ 5 problems (0 errors, 5 warnings, 0 infos, 0 hints)
````

## Support for injecting `IOpenApiDocumentProvider`

Inject [IOpenApiDocumentProvider](/en-us/dotnet/api/microsoft.aspnetcore.openapi.iopenapidocumentprovider) into services to access OpenAPI documents programmatically, even outside HTTP request contexts. The following example customizes version 2 ("`v2`") of the document with title, version, and description information:

```
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

public class CustomDocumentService(
    [FromKeyedServices("v2")] IOpenApiDocumentProvider documentProvider)
{
    public async Task<OpenApiDocument> GetApiDocumentAsync(
            CancellationToken cancellationToken = default)
    {
        var document = 
            await documentProvider.GetOpenApiDocumentAsync(cancellationToken);

        document.Info = new OpenApiInfo
        {
            Title = "Custom API Title",
            Version = "v2",
            Description = "This is a custom API description for version 2."
        };

        return document;
    }
}
```

Register the service in your DI container. Note that the service key should match the document name passed to [AddOpenApi](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openapiservicecollectionextensions.addopenapi):

```
builder.Services.AddOpenApi(); // Adds "v1" by default
builder.Services.AddOpenApi("v2");
builder.Services.AddScoped<CustomDocumentService>();
```

This enables scenarios such as generating client SDKs, validating API contracts in background processes, or exporting documents to external systems.

Support for injecting `IOpenApiDocumentProvider` was introduced in ASP.NET Core in .NET 10. For more information, see [dotnet/aspnetcore #61463](https://github.com/dotnet/aspnetcore/pull/61463).

To learn how to use the generated OpenAPI document in a Minimal API in .NET 6, 7, or 8, see the [Swagger and NSwag overview](../../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0).

## Use Swagger UI for local ad-hoc testing

By default, the `Microsoft.AspNetCore.OpenApi` package doesn't ship with built-in support for visualizing or interacting with the OpenAPI document. Popular tools for visualizing or interacting with the OpenAPI document include [Swagger UI](https://swagger.io/tools/swaggerhub/) and [ReDoc](https://github.com/Redocly/redoc). Swagger UI and ReDoc can be integrated in an app in several ways. Editors such as Visual Studio and VS Code offer extensions and built-in experiences for testing against an OpenAPI document.

The `Swashbuckle.AspNetCore.SwaggerUi` package provides a bundle of Swagger UI's web assets for use in apps. This package can be used to render a UI for the generated document. To configure this:

* Install the `Swashbuckle.AspNetCore.SwaggerUi` package.
* Enable the swagger-ui middleware with a reference to the [OpenAPI route registered earlier](aspnetcore-openapi?view=aspnetcore-10.0#customize-the-openapi-endpoint-route).

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();

    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/openapi/v1.json", "v1");
    });

}

app.MapGet("/", () => "Hello world!");

app.Run();
```

As a security best practice on limiting information disclosure, ***OpenAPI user interfaces (Swagger UI, ReDoc, Scalar) should only be enabled in development environments.*** For example, see [Swagger OAuth 2.0 configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/).

Launch the app and navigate to `https://localhost:<port>/swagger` to view the Swagger UI.

To automatically launch the app at the Swagger UI URL using the `https` profile of `Properties/launchSettings.json`:

* Confirm that `launchBrowser` is enabled (`true`).
* Set the `launchUrl` to `swagger`.

```
"launchBrowser": true,
"launchUrl": "swagger",
```

## Use Scalar for interactive API documentation

[Scalar](https://scalar.com/) is an open-source interactive document UI for OpenAPI. Scalar can integrate with the OpenAPI endpoint provided by ASP.NET Core. To configure Scalar, install the `Scalar.AspNetCore` package.

```
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

app.MapGet("/", () => "Hello world!");

app.Run();
```

Launch the app and navigate to `https://localhost:<port>/scalar` to view the Scalar UI.

To automatically launch the app at the Scalar UI URL using the `https` profile of `Properties/launchSettings.json`:

* Confirm that `launchBrowser` is enabled (`true`).
* Set the `launchUrl` to `scalar`.

```
"launchBrowser": true,
"launchUrl": "scalar",
```

## Lint generated OpenAPI documents with Spectral

[Spectral](https://stoplight.io/open-source/spectral) is an open-source OpenAPI document linter. Spectral can be incorporated into an app build to verify the quality of generated OpenAPI documents. Install Spectral according to the [package installation directions](https://github.com/stoplightio/spectral#-installation).

To take advantage of Spectral, install the `Microsoft.Extensions.ApiDescription.Server` package to enable build-time OpenAPI document generation.

Enable document generation at build time by setting the following properties in the app's `.csproj` file":

```
<PropertyGroup>
    <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
    <OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
</PropertyGroup>
```

Run `dotnet build` to generate the document.

```
dotnet build
```

Create a `.spectral.yml` file with the following contents.

```
extends: ["spectral:oas"]
```

Run `spectral lint` on the generated file.

````
spectral lint WebMinOpenApi.json
...

The output shows any issues with the OpenAPI document. For example:

```output
1:1  warning  oas3-api-servers       OpenAPI "servers" must be present and non-empty array.
3:10  warning  info-contact           Info object must have "contact" object.                        info
3:10  warning  info-description       Info "description" must be present and non-empty string.       info
9:13  warning  operation-description  Operation "description" must be present and non-empty string.  paths./.get
9:13  warning  operation-operationId  Operation must have "operationId".                             paths./.get

✖ 5 problems (0 errors, 5 warnings, 0 infos, 0 hints)
````

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0038
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0038.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0038: `[ValidatableType]` should not be used without a call to `AddValidation`

|              | Value   |
| ------------ | ------- |
| **Rule ID**  | ASP0038 |
| **Category** | Usage   |
| **Severity** | Warning |

## Cause

The [ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute) attribute is used in a project that doesn't call [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation).

## Rule description

`[ValidatableType]` has no effect unless the current project calls `AddValidation`. The validation source generator only discovers types in the project that contains the `AddValidation` call.

## How to fix violations

Call `AddValidation` on the service collection in the project that declares the validatable type:

```
builder.Services.AddValidation();
```

For a class library, provide a public service-registration method that calls `AddValidation` in the library project:

```
public static IServiceCollection AddOrderValidation(
    this IServiceCollection services)
{
    return services.AddValidation();
}
```

For more information, see [Register validation in multi-assembly apps](../fundamentals/validation?view=aspnetcore-12.0#register-validation-in-multi-assembly-apps).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/filters.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Filters in ASP.NET Core

By [Kirk Larkin](https://github.com/serpent5), [Rick Anderson](https://twitter.com/RickAndMSFT), [Tom Dykstra](https://github.com/tdykstra/), and [Steve Smith](https://ardalis.com/)

*Filters* in ASP.NET Core allow code to run before or after specific stages in the request processing pipeline.

Built-in filters handle tasks such as:

* Authorization, preventing access to resources a user isn't authorized for.
* Response caching, short-circuiting the request pipeline to return a cached response.

Custom filters can be created to handle cross-cutting concerns. Examples of cross-cutting concerns include error handling, caching, configuration, authorization, and logging. Filters avoid duplicating code. For example, an error handling exception filter could consolidate error handling.

This document applies to Razor Pages, API controllers, and controllers with views. Filters don't work directly with [Razor components](../../blazor/components/?view=aspnetcore-10.0). A filter can only indirectly affect a component when:

* The component is embedded in a page or view.
* The page or controller and view uses the filter.

## How filters work

Filters run within the *ASP.NET Core action invocation pipeline*, sometimes referred to as the *filter pipeline*. The filter pipeline runs after ASP.NET Core selects the action to execute:

The following diagram shows the filter pipeline execution order for MVC and Razor Pages:

### Filter types

Each filter type is executed at a different stage in the filter pipeline:

* [Authorization filters](#authorization-filters):

  * Run first.
  * Determine whether the user is authorized for the request.
  * Short-circuit the pipeline if the request isn't authorized.

* [Resource filters](#resource-filters):

  * Run after authorization.
  * [OnResourceExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresourcefilter.onresourceexecuting) runs code before the rest of the filter pipeline. For example, `OnResourceExecuting` runs code before model binding.
  * [OnResourceExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresourcefilter.onresourceexecuted) runs code after the rest of the pipeline has completed.

* [Action filters](#action-filters):

  * Run immediately before and after an action method is called.
  * Can change the arguments passed into an action.
  * Can change the result returned from the action.
  * Are **not** supported in Razor Pages.

* [Endpoint filters](/en-us/aspnet/core/fundamentals/minimal-apis/min-api-filters):

  * Run immediately before and after an action method is called.
  * Can change the arguments passed into an action.
  * Can change the result returned from the action.
  * Are **not** supported in Razor Pages.
  * Can be invoked on both actions and route handler-based endpoints.

* [Exception filters](#exception-filters):

  * Apply global policies to unhandled exceptions that occur before the response body has been written to.
  * Run after model binding and action filters, but before the action result is executed.
  * Run only if an unhandled exception occurs during action execution or action result execution.
  * Don't run for exceptions thrown during middleware execution, routing, or model binding.

* [Result filters](#result-filters):

  * Run immediately before and after the execution of action results.
  * Run only when the action method executes successfully.
  * Are useful for logic that must surround view or formatter execution.

Razor Pages also support [Razor Page filters](../../razor-pages/filter?view=aspnetcore-10.0), which run before and after a Razor Page handler.

## Implementation

Filters support both synchronous and asynchronous implementations through different interface definitions.

Synchronous filters run before and after their pipeline stage. For example, [OnActionExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter.onactionexecuting) is called before the action method is called. [OnActionExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter.onactionexecuted) is called after the action method returns:

```
public class SampleActionFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
    }
}
```

Asynchronous filters define an `On-Stage-ExecutionAsync` method. For example, [OnActionExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncactionfilter.onactionexecutionasync):

```
public class SampleAsyncActionFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // Do something before the action executes.
        await next();
        // Do something after the action executes.
    }
}
```

In the preceding code, the `SampleAsyncActionFilter` has an [ActionExecutionDelegate](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutiondelegate), `next`, which executes the action method.

### Multiple filter stages

Interfaces for multiple filter stages can be implemented in a single class. For example, the [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute) class implements:

* Synchronous: [IActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter) and [IResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter)
* Asynchronous: [IAsyncActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncactionfilter) and [IAsyncResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter)
* [IOrderedFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iorderedfilter)

Implement **either** the synchronous or the async version of a filter interface, **not** both. The runtime checks first to see if the filter implements the async interface, and if so, it calls that. If not, it calls the synchronous interface's methods. If both asynchronous and synchronous interfaces are implemented in one class, only the async method is called. When using abstract classes like [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute), override only the synchronous methods or the asynchronous methods for each filter type.

### Built-in filter attributes

ASP.NET Core includes built-in attribute-based filters that can be subclassed and customized. For example, the following result filter adds a header to the response:

[]()

```
public class ResponseHeaderAttribute : ActionFilterAttribute
{
    private readonly string _name;
    private readonly string _value;

    public ResponseHeaderAttribute(string name, string value) =>
        (_name, _value) = (name, value);

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers.Add(_name, _value);

        base.OnResultExecuting(context);
    }
}
```

Attributes allow filters to accept arguments, as shown in the preceding example. Apply the `ResponseHeaderAttribute` to a controller or action method and specify the name and value of the HTTP header:

```
[ResponseHeader("Filter-Header", "Filter Value")]
public class ResponseHeaderController : ControllerBase
{
    public IActionResult Index() =>
        Content("Examine the response headers using the F12 developer tools.");

    // ...
```

Use a tool such as the [browser developer tools](https://developer.mozilla.org/docs/Learn_web_development/Howto/Tools_and_setup/What_are_browser_developer_tools) to examine the headers. Under **Response Headers**, `filter-header: Filter Value` is displayed.

The following code applies `ResponseHeaderAttribute` to both a controller and an action:

```
[ResponseHeader("Filter-Header", "Filter Value")]
public class ResponseHeaderController : ControllerBase
{
    public IActionResult Index() =>
        Content("Examine the response headers using the F12 developer tools.");

    // ...

    [ResponseHeader("Another-Filter-Header", "Another Filter Value")]
    public IActionResult Multiple() =>
        Content("Examine the response headers using the F12 developer tools.");
}
```

Responses from the `Multiple` action include the following headers:

* `filter-header: Filter Value`
* `another-filter-header: Another Filter Value`

Several of the filter interfaces have corresponding attributes that can be used as base classes for custom implementations.

Filter attributes:

* [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute)
* [ExceptionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptionfilterattribute)
* [ResultFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultfilterattribute)
* [FormatFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatfilterattribute)
* [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute)
* [TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute)

Filters can't be applied to Razor Page handler methods. They can be applied either to the Razor Page model or globally.

## Filter scopes and order of execution

A filter can be added to the pipeline at one of three *scopes*:

* Using an attribute on a controller or Razor Page.
* Using an attribute on a controller action. Filter attributes can't be applied to Razor Pages handler methods.
* Globally for all controllers, actions, and Razor Pages as shown in the following code:
  ```
  var builder = WebApplication.CreateBuilder(args);

  // Add services to the container.
  builder.Services.AddControllersWithViews(options =>
  {
      options.Filters.Add<GlobalSampleActionFilter>();
  });
  ```

### Default order of execution

When there are multiple filters for a particular stage of the pipeline, scope determines the default order of filter execution. Global filters surround class filters, which in turn surround method filters.

As a result of filter nesting, the *after* code of filters runs in the reverse order of the *before* code. The filter sequence:

* The *before* code of global filters.

  * The *before* code of controller filters.

    * The *before* code of action method filters.
    * The *after* code of action method filters.

  * The *after* code of controller filters.

* The *after* code of global filters.

The following example illustrates the order in which filter methods run for synchronous action filters:

| Sequence | Filter scope | Filter method       |
| -------- | ------------ | ------------------- |
| 1        | Global       | `OnActionExecuting` |
| 2        | Controller   | `OnActionExecuting` |
| 3        | Action       | `OnActionExecuting` |
| 4        | Action       | `OnActionExecuted`  |
| 5        | Controller   | `OnActionExecuted`  |
| 6        | Global       | `OnActionExecuted`  |

### Controller level filters

Every controller that inherits from [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) includes the [OnActionExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecuting), [OnActionExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecutionasync), and [OnActionExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecuted) methods. These methods wrap the filters that run for a given action:

* `OnActionExecuting` runs before any of the action's filters.
* `OnActionExecuted` runs after all of the action's filters.
* `OnActionExecutionAsync` runs before any of the action's filters. Code after a call to `next` runs after the action's filters.

The following `ControllerFiltersController` class:

* Applies the `SampleActionFilterAttribute` (`[SampleActionFilter]`) to the controller.
* Overrides `OnActionExecuting` and `OnActionExecuted`.

```
[SampleActionFilter]
public class ControllerFiltersController : Controller
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine(
            $"- {nameof(ControllerFiltersController)}.{nameof(OnActionExecuting)}");

        base.OnActionExecuting(context);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine(
            $"- {nameof(ControllerFiltersController)}.{nameof(OnActionExecuted)}");

        base.OnActionExecuted(context);
    }

    public IActionResult Index()
    {
        Console.WriteLine(
            $"- {nameof(ControllerFiltersController)}.{nameof(Index)}");

        return Content("Check the Console.");
    }
}
```

Navigating to `https://localhost:<port>/ControllerFilters` runs the following code:

* `ControllerFiltersController.OnActionExecuting`

  * `GlobalSampleActionFilter.OnActionExecuting`

    * `SampleActionFilterAttribute.OnActionExecuting`
      * `ControllerFiltersController.Index`
    * `SampleActionFilterAttribute.OnActionExecuted`

  * `GlobalSampleActionFilter.OnActionExecuted`

* `ControllerFiltersController.OnActionExecuted`

Controller level filters set the [Order](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/Filters/ControllerActionFilter.cs#L15-L17) property to `int.MinValue`. Controller level filters can **not** be set to run after filters applied to methods. Order is explained in the next section.

For Razor Pages, see [Implement Razor Page filters by overriding filter methods](../../razor-pages/filter?view=aspnetcore-10.0#implement-razor-page-filters-by-overriding-filter-methods).

### Override the default order

The default sequence of execution can be overridden by implementing [IOrderedFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iorderedfilter). `IOrderedFilter` exposes the [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iorderedfilter.order#microsoft-aspnetcore-mvc-filters-iorderedfilter-order) property that takes precedence over scope to determine the order of execution. A filter with a lower `Order` value:

* Runs the *before* code before that of a filter with a higher value of `Order`.
* Runs the *after* code after that of a filter with a higher `Order` value.

In the [Controller level filters](#controller-level-filters) example, `GlobalSampleActionFilter` has global scope so it runs before `SampleActionFilterAttribute`, which has controller scope. To make `SampleActionFilterAttribute` run first, set its order to `int.MinValue`:

```
[SampleActionFilter(Order = int.MinValue)]
public class ControllerFiltersController : Controller
{
    // ...
}
```

To make the global filter `GlobalSampleActionFilter` run first, set its `Order` to `int.MinValue`:

```
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add<GlobalSampleActionFilter>(int.MinValue);
});
```

## Cancellation and short-circuiting

The filter pipeline can be short-circuited by setting the [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resourceexecutingcontext.result#microsoft-aspnetcore-mvc-filters-resourceexecutingcontext-result) property on the [ResourceExecutingContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resourceexecutingcontext) parameter provided to the filter method. For example, the following Resource filter prevents the rest of the pipeline from executing:

[]()

```
public class ShortCircuitingResourceFilterAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        context.Result = new ContentResult
        {
            Content = nameof(ShortCircuitingResourceFilterAttribute)
        };
    }

    public void OnResourceExecuted(ResourceExecutedContext context) { }
}
```

In the following code, both the `[ShortCircuitingResourceFilter]` and the `[ResponseHeader]` filter target the `Index` action method. The `ShortCircuitingResourceFilterAttribute` filter:

* Runs first, because it's a Resource Filter and `ResponseHeaderAttribute` is an Action Filter.
* Short-circuits the rest of the pipeline.

Therefore the `ResponseHeaderAttribute` filter never runs for the `Index` action. This behavior would be the same if both filters were applied at the action method level, provided the `ShortCircuitingResourceFilterAttribute` ran first. The `ShortCircuitingResourceFilterAttribute` runs first because of its filter type:

```
[ResponseHeader("Filter-Header", "Filter Value")]
public class ShortCircuitingController : Controller
{
    [ShortCircuitingResourceFilter]
    public IActionResult Index() =>
        Content($"- {nameof(ShortCircuitingController)}.{nameof(Index)}");
}
```

## Dependency injection

Filters can be added by type or by instance. If an instance is added, that instance is used for every request. If a type is added, it's type-activated. A type-activated filter means:

* An instance is created for each request.
* Any constructor dependencies are populated by [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI).

Filters that are implemented as attributes and added directly to controller classes or action methods can't have constructor dependencies provided by [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI). Constructor dependencies can't be provided by DI because attributes must have their constructor parameters supplied where they're applied.

The following filters support constructor dependencies provided from DI:

* [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute)
* [TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute)
* [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory) implemented on the attribute.

The preceding filters can be applied to a controller or an action.

Loggers are available from DI. However, avoid creating and using filters purely for logging purposes. The [built-in framework logging](../../fundamentals/logging/?view=aspnetcore-10.0) typically provides what's needed for logging. Logging added to filters:

* Should focus on business domain concerns or behavior specific to the filter.
* Should **not** log actions or other framework events. The built-in filters already log actions and framework events.

### ServiceFilterAttribute

Service filter implementation types are registered in `Program.cs`. A [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute) retrieves an instance of the filter from DI.

The following code shows the `LoggingResponseHeaderFilterService` class, which uses DI:

```
public class LoggingResponseHeaderFilterService : IResultFilter
{
    private readonly ILogger _logger;

    public LoggingResponseHeaderFilterService(
            ILogger<LoggingResponseHeaderFilterService> logger) =>
        _logger = logger;

    public void OnResultExecuting(ResultExecutingContext context)
    {
        _logger.LogInformation(
            $"- {nameof(LoggingResponseHeaderFilterService)}.{nameof(OnResultExecuting)}");

        context.HttpContext.Response.Headers.Add(
            nameof(OnResultExecuting), nameof(LoggingResponseHeaderFilterService));
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        _logger.LogInformation(
            $"- {nameof(LoggingResponseHeaderFilterService)}.{nameof(OnResultExecuted)}");
    }
}
```

In the following code, `LoggingResponseHeaderFilterService` is added to the DI container:

```
builder.Services.AddScoped<LoggingResponseHeaderFilterService>();
```

In the following code, the `ServiceFilter` attribute retrieves an instance of the `LoggingResponseHeaderFilterService` filter from DI:

```
[ServiceFilter<LoggingResponseHeaderFilterService>]
public IActionResult WithServiceFilter() =>
    Content($"- {nameof(FilterDependenciesController)}.{nameof(WithServiceFilter)}");
```

When using `ServiceFilterAttribute`, setting [ServiceFilterAttribute.IsReusable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute.isreusable#microsoft-aspnetcore-mvc-servicefilterattribute-isreusable):

* Provides a hint that the filter instance *might* be reused outside of the request scope it was created within. The ASP.NET Core runtime doesn't guarantee:

  * That a single instance of the filter will be created.
  * The filter won't be re-requested from the DI container at some later point.

* Shouldn't be used with a filter that depends on services with a lifetime other than singleton.

[ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute) implements [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory). `IFilterFactory` exposes the [CreateInstance](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.createinstance) method for creating an [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata) instance. `CreateInstance` loads the specified type from DI.

### TypeFilterAttribute

[TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute) is similar to [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute), but its type isn't resolved directly from the DI container. It instantiates the type by using [Microsoft.Extensions.DependencyInjection.ObjectFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.objectfactory).

Because `TypeFilterAttribute` types aren't resolved directly from the DI container:

* Types that are referenced using the `TypeFilterAttribute` don't need to be registered with the DI container. They do have their dependencies fulfilled by the DI container.
* `TypeFilterAttribute` can optionally accept constructor arguments for the type.

When using `TypeFilterAttribute`, setting [TypeFilterAttribute.IsReusable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute.isreusable#microsoft-aspnetcore-mvc-typefilterattribute-isreusable):

* Provides hint that the filter instance *might* be reused outside of the request scope it was created within. The ASP.NET Core runtime provides no guarantees that a single instance of the filter will be created.

* Shouldn't be used with a filter that depends on services with a lifetime other than singleton.

The following example shows how to pass arguments to a type using `TypeFilterAttribute`:

```
[TypeFilter(typeof(LoggingResponseHeaderFilter),
    Arguments = new object[] { "Filter-Header", "Filter Value" })]
public IActionResult WithTypeFilter() =>
    Content($"- {nameof(FilterDependenciesController)}.{nameof(WithTypeFilter)}");
```

## Authorization filters

Authorization filters:

* Are the first filters run in the filter pipeline.
* Control access to action methods.
* Have a before method, but no after method.

Custom authorization filters require a custom authorization framework. Prefer configuring the authorization policies or writing a custom authorization policy over writing a custom filter. The built-in authorization filter:

* Calls the authorization system.
* Doesn't authorize requests.

Do **not** throw exceptions within authorization filters:

* The exception won't be handled.
* Exception filters won't handle the exception.

Consider issuing a challenge when an exception occurs in an authorization filter.

Learn more about [Authorization](../../security/authorization/introduction?view=aspnetcore-10.0).

## Resource filters

Resource filters:

* Implement either the [IResourceFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresourcefilter) or [IAsyncResourceFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresourcefilter) interface.
* Execution wraps most of the filter pipeline.
* Only [Authorization filters](#authorization-filters) run before resource filters.

Resource filters are useful to short-circuit most of the pipeline. For example, a caching filter can avoid the rest of the pipeline on a cache hit.

Resource filter examples:

* [The short-circuiting resource filter](#short-circuiting-resource-filter) shown previously.

* [DisableFormValueModelBindingAttribute](https://github.com/aspnet/Entropy/blob/master/samples/Mvc.FileUpload/Filters/DisableFormValueModelBindingAttribute.cs):

  * Prevents model binding from accessing the form data.
  * Used for large file uploads to prevent the form data from being read into memory.

## Action filters

Action filters do **not** apply to Razor Pages. Razor Pages supports [IPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter) and [IAsyncPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter). For more information, see [Filter methods for Razor Pages](../../razor-pages/filter?view=aspnetcore-10.0).

Action filters:

* Implement either the [IActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter) or [IAsyncActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncactionfilter) interface.
* Their execution surrounds the execution of action methods.

The following code shows a sample action filter:

```
public class SampleActionFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
    }
}
```

The [ActionExecutingContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext) provides the following properties:

* [ActionArguments](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext.actionarguments#microsoft-aspnetcore-mvc-filters-actionexecutingcontext-actionarguments): Enables reading the inputs to an action method.
* [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller): Enables manipulating the controller instance.
* [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext.result): Setting `Result` short-circuits execution of the action method and subsequent action filters.

Throwing an exception in an action method:

* Prevents running of subsequent filters.
* Unlike setting `Result`, is treated as a failure instead of a successful result.

The [ActionExecutedContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext) provides `Controller` and `Result` plus the following properties:

* [Canceled](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.canceled): True if another filter short-circuited the action execution.

* [Exception](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.exception): Non-null if the action or a previously run action filter threw an exception. Setting this property to null:

  * Effectively handles the exception.
  * `Result` is executed as if it was returned from the action method.

For an `IAsyncActionFilter`, a call to the [ActionExecutionDelegate](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutiondelegate):

* Executes any subsequent action filters and the action method.
* Returns `ActionExecutedContext`.

To short-circuit, assign [Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext.Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext.result#microsoft-aspnetcore-mvc-filters-actionexecutingcontext-result) to a result instance and don't call `next` (the `ActionExecutionDelegate`).

The framework provides an abstract [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute) that can be subclassed.

The `OnActionExecuting` action filter can be used to:

* Validate model state.
* Return an error if the state is invalid.

```
public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}
```

Note

Controllers annotated with the `[ApiController]` attribute automatically validate model state and return a 400 response. For more information, see [Automatic HTTP 400 responses](../../web-api/?view=aspnetcore-10.0#automatic-http-400-responses).

The `OnActionExecuted` method runs after the action method:

* And can see and manipulate the results of the action through the [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.result#microsoft-aspnetcore-mvc-filters-actionexecutedcontext-result) property.

* [Canceled](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.canceled#microsoft-aspnetcore-mvc-filters-actionexecutedcontext-canceled) is set to true if another filter short-circuited the action execution.

* [Exception](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.exception#microsoft-aspnetcore-mvc-filters-actionexecutedcontext-exception) is set to a non-null value if the action or a subsequent action filter threw an exception. Setting `Exception` to null:

  * Effectively handles an exception.
  * `ActionExecutedContext.Result` is executed as if it were returned normally from the action method.

## Exception filters

Exception filters:

* Implement [IExceptionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iexceptionfilter) or [IAsyncExceptionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncexceptionfilter).
* Can be used to implement common error handling policies.

The following sample exception filter displays details about exceptions that occur when the app is in development:

```
public class SampleExceptionFilter : IExceptionFilter
{
    private readonly IHostEnvironment _hostEnvironment;

    public SampleExceptionFilter(IHostEnvironment hostEnvironment) =>
        _hostEnvironment = hostEnvironment;

    public void OnException(ExceptionContext context)
    {
        if (!_hostEnvironment.IsDevelopment())
        {
            // Don't display exception details unless running in Development.
            return;
        }

        context.Result = new ContentResult
        {
            Content = context.Exception.ToString()
        };
    }
}
```

The following code tests the exception filter:

```
[TypeFilter<SampleExceptionFilter>]
public class ExceptionController : Controller
{
    public IActionResult Index() =>
        Content($"- {nameof(ExceptionController)}.{nameof(Index)}");
}
```

Exception filters:

* Don't have before and after events.
* Implement [OnException](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iexceptionfilter.onexception) or [OnExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncexceptionfilter.onexceptionasync).
* Handle unhandled exceptions that occur in Razor Page or controller creation, [model binding](../models/model-binding?view=aspnetcore-10.0), action filters, or action methods.
* Do **not** catch exceptions that occur in resource filters, result filters, or MVC result execution.

To handle an exception, set the [ExceptionHandled](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptioncontext.exceptionhandled) property to `true` or assign the [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptioncontext.result) property. This stops propagation of the exception. An exception filter can't turn an exception into a "success". Only an action filter can do that.

Exception filters:

* Are good for trapping exceptions that occur within actions.
* Aren't as flexible as error handling middleware.

Prefer middleware for exception handling. Use exception filters only where error handling *differs* based on which action method is called. For example, an app might have action methods for both API endpoints and for views/HTML. The API endpoints could return error information as JSON, while the view-based actions could return an error page as HTML.

## Result filters

Result filters:

* Implement an interface:

  * [IResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter) or [IAsyncResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter)
  * [IAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ialwaysrunresultfilter) or [IAsyncAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncalwaysrunresultfilter)

* Their execution surrounds the execution of action results.

### IResultFilter and IAsyncResultFilter

The following code shows a sample result filter:

```
public class SampleResultFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        // Do something before the result executes.
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // Do something after the result executes.
    }
}
```

The kind of result being executed depends on the action. An action returning a view includes all razor processing as part of the [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult) being executed. An API method might perform some serialization as part of the execution of the result. Learn more about [action results](actions?view=aspnetcore-10.0).

Result filters are only executed when an action or action filter produces an action result. Result filters aren't executed when:

* An authorization filter or resource filter short-circuits the pipeline.
* An exception filter handles an exception by producing an action result.

The [Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter.onresultexecuting) method can short-circuit execution of the action result and subsequent result filters by setting [Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext.Cancel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultexecutingcontext.cancel#microsoft-aspnetcore-mvc-filters-resultexecutingcontext-cancel) to `true`. Write to the response object when short-circuiting to avoid generating an empty response. Throwing an exception in `IResultFilter.OnResultExecuting`:

* Prevents execution of the action result and subsequent filters.
* Is treated as a failure instead of a successful result.

When the [Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter.onresultexecuted) method runs, the response has probably already been sent to the client. If the response has already been sent to the client, it can't be changed.

`ResultExecutedContext.Canceled` is set to `true` if another filter short-circuited the action result execution.

`ResultExecutedContext.Exception` is set to a non-null value if the action result or a subsequent result filter threw an exception. Setting `Exception` to null effectively handles an exception and prevents the exception from being thrown again later in the pipeline. There's no reliable way to write data to a response when handling an exception in a result filter. If the headers have been flushed to the client when an action result throws an exception, there's no reliable mechanism to send a failure code.

For an [IAsyncResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter), a call to `await next` on the [ResultExecutionDelegate](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultexecutiondelegate) executes any subsequent result filters and the action result. To short-circuit, set [ResultExecutingContext.Cancel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultexecutingcontext.cancel#microsoft-aspnetcore-mvc-filters-resultexecutingcontext-cancel) to `true` and don't call the `ResultExecutionDelegate`:

```
public class SampleAsyncResultFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(
        ResultExecutingContext context, ResultExecutionDelegate next)
    {
        if (context.Result is not EmptyResult)
        {
            await next();
        }
        else
        {
            context.Cancel = true;
        }
    }
}
```

The framework provides an abstract `ResultFilterAttribute` that can be subclassed. The [ResponseHeaderAttribute](#response-header-attribute) class shown previously is an example of a result filter attribute.

### IAlwaysRunResultFilter and IAsyncAlwaysRunResultFilter

The [IAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ialwaysrunresultfilter) and [IAsyncAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncalwaysrunresultfilter) interfaces declare an [IResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter) implementation that runs for all action results. This includes action results produced by:

* Authorization filters and resource filters that short-circuit.
* Exception filters.

For example, the following filter always runs and sets an action result ([ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult)) with a *422 Unprocessable Entity* status code when content negotiation fails:

```
public class UnprocessableResultFilter : IAlwaysRunResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is StatusCodeResult statusCodeResult
            && statusCodeResult.StatusCode == StatusCodes.Status415UnsupportedMediaType)
        {
            context.Result = new ObjectResult("Unprocessable")
            {
                StatusCode = StatusCodes.Status422UnprocessableEntity
            };
        }
    }

    public void OnResultExecuted(ResultExecutedContext context) { }
}
```

## IFilterFactory

[IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory) implements [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata). Therefore, an `IFilterFactory` instance can be used as an `IFilterMetadata` instance anywhere in the filter pipeline. When the runtime prepares to invoke the filter, it attempts to cast it to an `IFilterFactory`. If that cast succeeds, the [CreateInstance](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.createinstance) method is called to create the `IFilterMetadata` instance that is invoked. This provides a flexible design, since the precise filter pipeline doesn't need to be set explicitly when the app starts.

`IFilterFactory.IsReusable`:

* Is a hint by the factory that the filter instance created by the factory might be reused outside of the request scope it was created within.
* Should ***not*** be used with a filter that depends on services with a lifetime other than singleton.

The ASP.NET Core runtime doesn't guarantee:

* That a single instance of the filter will be created.
* The filter won't be re-requested from the DI container at some later point.

Warning

Only configure [IFilterFactory.IsReusable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.isreusable#microsoft-aspnetcore-mvc-filters-ifilterfactory-isreusable) to return `true` if the source of the filters is unambiguous, the filters are stateless, and the filters are safe to use across multiple HTTP requests. For instance, don't return filters from DI that are registered as scoped or transient if `IFilterFactory.IsReusable` returns `true`.

`IFilterFactory` can be implemented using custom attribute implementations as another approach to creating filters:

```
public class ResponseHeaderFilterFactory : Attribute, IFilterFactory
{
    public bool IsReusable => false;

    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) =>
        new InternalResponseHeaderFilter();

    private class InternalResponseHeaderFilter : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context) =>
            context.HttpContext.Response.Headers.Add(
                nameof(OnActionExecuting), nameof(InternalResponseHeaderFilter));

        public void OnActionExecuted(ActionExecutedContext context) { }
    }
```

The filter is applied in the following code:

```
[ResponseHeaderFilterFactory]
public IActionResult Index() =>
    Content($"- {nameof(FilterFactoryController)}.{nameof(Index)}");
```

### IFilterFactory implemented on an attribute

Filters that implement `IFilterFactory` are useful for filters that:

* Don't require passing parameters.
* Have constructor dependencies that need to be filled by DI.

[TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute) implements [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory). `IFilterFactory` exposes the [CreateInstance](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.createinstance) method for creating an [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata) instance. `CreateInstance` loads the specified type from the services container (DI).

```
public class SampleActionTypeFilterAttribute : TypeFilterAttribute
{
    public SampleActionTypeFilterAttribute()
         : base(typeof(InternalSampleActionFilter)) { }

    private class InternalSampleActionFilter : IActionFilter
    {
        private readonly ILogger<InternalSampleActionFilter> _logger;

        public InternalSampleActionFilter(ILogger<InternalSampleActionFilter> logger) =>
            _logger = logger;

        public void OnActionExecuting(ActionExecutingContext context)
        {
            _logger.LogInformation(
                $"- {nameof(InternalSampleActionFilter)}.{nameof(OnActionExecuting)}");
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            _logger.LogInformation(
                $"- {nameof(InternalSampleActionFilter)}.{nameof(OnActionExecuted)}");
        }
    }
}
```

The following code shows three approaches to applying the filter:

```
[SampleActionTypeFilter]
public IActionResult WithDirectAttribute() =>
    Content($"- {nameof(FilterFactoryController)}.{nameof(WithDirectAttribute)}");

[TypeFilter<SampleActionTypeFilterAttribute>]
public IActionResult WithTypeFilterAttribute() =>
    Content($"- {nameof(FilterFactoryController)}.{nameof(WithTypeFilterAttribute)}");

[ServiceFilter<SampleActionTypeFilterAttribute>]
public IActionResult WithServiceFilterAttribute() =>
    Content($"- {nameof(FilterFactoryController)}.{nameof(WithServiceFilterAttribute)}");
```

In the preceding code, the first approach to applying the filter is preferred.

## Use middleware in the filter pipeline

Resource filters work like [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) in that they surround the execution of everything that comes later in the pipeline. But filters differ from middleware in that they're part of the runtime, which means that they have access to context and constructs.

To use middleware as a filter, create a type with a `Configure` method that specifies the middleware to inject into the filter pipeline. The following example uses middleware to set a response header:

```
public class FilterMiddlewarePipeline
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            context.Response.Headers.Add("Pipeline", "Middleware");

            await next();
        });
    }
}
```

Use the [MiddlewareFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.middlewarefilterattribute) to run the middleware:

```
[MiddlewareFilter<FilterMiddlewarePipeline>]
public class FilterMiddlewareController : Controller
{
    public IActionResult Index() =>
        Content($"- {nameof(FilterMiddlewareController)}.{nameof(Index)}");
}
```

Middleware filters run at the same stage of the filter pipeline as Resource filters, before model binding and after the rest of the pipeline.

## Thread safety

When passing an *instance* of a filter into `Add`, instead of its `Type`, the filter is a singleton and is **not** thread-safe.

## Related content

* [View or download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/filters/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).
* [Filter methods for Razor Pages in ASP.NET Core](../../razor-pages/filter?view=aspnetcore-10.0)

By [Kirk Larkin](https://github.com/serpent5), [Rick Anderson](https://twitter.com/RickAndMSFT), [Tom Dykstra](https://github.com/tdykstra/), and [Steve Smith](https://ardalis.com/)

*Filters* in ASP.NET Core allow code to run before or after specific stages in the request processing pipeline.

Built-in filters handle tasks such as:

* Authorization, preventing access to resources a user isn't authorized for.
* Response caching, short-circuiting the request pipeline to return a cached response.

Custom filters can be created to handle cross-cutting concerns. Examples of cross-cutting concerns include error handling, caching, configuration, authorization, and logging. Filters avoid duplicating code. For example, an error handling exception filter could consolidate error handling.

This document applies to Razor Pages, API controllers, and controllers with views. Filters don't work directly with [Razor components](../../blazor/components/?view=aspnetcore-10.0). A filter can only indirectly affect a component when:

* The component is embedded in a page or view.
* The page or controller and view uses the filter.

## How filters work

Filters run within the *ASP.NET Core action invocation pipeline*, sometimes referred to as the *filter pipeline*. The filter pipeline runs after ASP.NET Core selects the action to execute:

### Filter types

Each filter type is executed at a different stage in the filter pipeline:

* [Authorization filters](#authorization-filters):

  * Run first.
  * Determine whether the user is authorized for the request.
  * Short-circuit the pipeline if the request is not authorized.

* [Resource filters](#resource-filters):

  * Run after authorization.
  * [OnResourceExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresourcefilter.onresourceexecuting) runs code before the rest of the filter pipeline. For example, `OnResourceExecuting` runs code before model binding.
  * [OnResourceExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresourcefilter.onresourceexecuted) runs code after the rest of the pipeline has completed.

* [Action filters](#action-filters):

  * Run immediately before and after an action method is called.
  * Can change the arguments passed into an action.
  * Can change the result returned from the action.
  * Are **not** supported in Razor Pages.

- [Endpoint filters](/en-us/aspnet/core/fundamentals/minimal-apis/min-api-filters):

  * Run immediately before and after an action method is called.
  * Can change the arguments passed into an action.
  * Can change the result returned from the action.
  * Are **not** supported in Razor Pages.
  * Can be invoked on both actions and route handler-based endpoints.

* [Exception filters](#exception-filters) apply global policies to unhandled exceptions that occur before the response body has been written to.

* [Result filters](#result-filters):

  * Run immediately before and after the execution of action results.
  * Run only when the action method executes successfully.
  * Are useful for logic that must surround view or formatter execution.

The following diagram shows how filter types interact in the filter pipeline:

Razor Pages also support [Razor Page filters](../../razor-pages/filter?view=aspnetcore-10.0), which run before and after a Razor Page handler.

## Implementation

Filters support both synchronous and asynchronous implementations through different interface definitions.

Synchronous filters run before and after their pipeline stage. For example, [OnActionExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter.onactionexecuting) is called before the action method is called. [OnActionExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter.onactionexecuted) is called after the action method returns:

```
public class SampleActionFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
    }
}
```

Asynchronous filters define an `On-Stage-ExecutionAsync` method. For example, [OnActionExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncactionfilter.onactionexecutionasync):

```
public class SampleAsyncActionFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // Do something before the action executes.
        await next();
        // Do something after the action executes.
    }
}
```

In the preceding code, the `SampleAsyncActionFilter` has an [ActionExecutionDelegate](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutiondelegate), `next`, which executes the action method.

### Multiple filter stages

Interfaces for multiple filter stages can be implemented in a single class. For example, the [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute) class implements:

* Synchronous: [IActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter) and [IResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter)
* Asynchronous: [IAsyncActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncactionfilter) and [IAsyncResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter)
* [IOrderedFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iorderedfilter)

Implement **either** the synchronous or the async version of a filter interface, **not** both. The runtime checks first to see if the filter implements the async interface, and if so, it calls that. If not, it calls the synchronous interface's methods. If both asynchronous and synchronous interfaces are implemented in one class, only the async method is called. When using abstract classes like [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute), override only the synchronous methods or the asynchronous methods for each filter type.

### Built-in filter attributes

ASP.NET Core includes built-in attribute-based filters that can be subclassed and customized. For example, the following result filter adds a header to the response:

[]()

```
public class ResponseHeaderAttribute : ActionFilterAttribute
{
    private readonly string _name;
    private readonly string _value;

    public ResponseHeaderAttribute(string name, string value) =>
        (_name, _value) = (name, value);

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers.Add(_name, _value);

        base.OnResultExecuting(context);
    }
}
```

Attributes allow filters to accept arguments, as shown in the preceding example. Apply the `ResponseHeaderAttribute` to a controller or action method and specify the name and value of the HTTP header:

```
[ResponseHeader("Filter-Header", "Filter Value")]
public class ResponseHeaderController : ControllerBase
{
    public IActionResult Index() =>
        Content("Examine the response headers using the F12 developer tools.");

    // ...
```

Use a tool such as the [browser developer tools](https://developer.mozilla.org/docs/Learn/Common_questions/What_are_browser_developer_tools) to examine the headers. Under **Response Headers**, `filter-header: Filter Value` is displayed.

The following code applies `ResponseHeaderAttribute` to both a controller and an action:

```
[ResponseHeader("Filter-Header", "Filter Value")]
public class ResponseHeaderController : ControllerBase
{
    public IActionResult Index() =>
        Content("Examine the response headers using the F12 developer tools.");

    // ...

    [ResponseHeader("Another-Filter-Header", "Another Filter Value")]
    public IActionResult Multiple() =>
        Content("Examine the response headers using the F12 developer tools.");
}
```

Responses from the `Multiple` action include the following headers:

* `filter-header: Filter Value`
* `another-filter-header: Another Filter Value`

Several of the filter interfaces have corresponding attributes that can be used as base classes for custom implementations.

Filter attributes:

* [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute)
* [ExceptionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptionfilterattribute)
* [ResultFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultfilterattribute)
* [FormatFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatfilterattribute)
* [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute)
* [TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute)

Filters cannot be applied to Razor Page handler methods. They can be applied either to the Razor Page model or globally.

## Filter scopes and order of execution

A filter can be added to the pipeline at one of three *scopes*:

* Using an attribute on a controller or Razor Page.
* Using an attribute on a controller action. Filter attributes cannot be applied to Razor Pages handler methods.
* Globally for all controllers, actions, and Razor Pages as shown in the following code:
  ```
  var builder = WebApplication.CreateBuilder(args);

  // Add services to the container.
  builder.Services.AddControllersWithViews(options =>
  {
      options.Filters.Add<GlobalSampleActionFilter>();
  });
  ```

### Default order of execution

When there are multiple filters for a particular stage of the pipeline, scope determines the default order of filter execution. Global filters surround class filters, which in turn surround method filters.

As a result of filter nesting, the *after* code of filters runs in the reverse order of the *before* code. The filter sequence:

* The *before* code of global filters.

  * The *before* code of controller filters.

    * The *before* code of action method filters.
    * The *after* code of action method filters.

  * The *after* code of controller filters.

* The *after* code of global filters.

The following example illustrates the order in which filter methods run for synchronous action filters:

| Sequence | Filter scope | Filter method       |
| -------- | ------------ | ------------------- |
| 1        | Global       | `OnActionExecuting` |
| 2        | Controller   | `OnActionExecuting` |
| 3        | Action       | `OnActionExecuting` |
| 4        | Action       | `OnActionExecuted`  |
| 5        | Controller   | `OnActionExecuted`  |
| 6        | Global       | `OnActionExecuted`  |

### Controller level filters

Every controller that inherits from [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) includes the [OnActionExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecuting), [OnActionExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecutionasync), and [OnActionExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecuted) methods. These methods wrap the filters that run for a given action:

* `OnActionExecuting` runs before any of the action's filters.
* `OnActionExecuted` runs after all of the action's filters.
* `OnActionExecutionAsync` runs before any of the action's filters. Code after a call to `next` runs after the action's filters.

The following `ControllerFiltersController` class:

* Applies the `SampleActionFilterAttribute` (`[SampleActionFilter]`) to the controller.
* Overrides `OnActionExecuting` and `OnActionExecuted`.

```
[SampleActionFilter]
public class ControllerFiltersController : Controller
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine(
            $"- {nameof(ControllerFiltersController)}.{nameof(OnActionExecuting)}");

        base.OnActionExecuting(context);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine(
            $"- {nameof(ControllerFiltersController)}.{nameof(OnActionExecuted)}");

        base.OnActionExecuted(context);
    }

    public IActionResult Index()
    {
        Console.WriteLine(
            $"- {nameof(ControllerFiltersController)}.{nameof(Index)}");

        return Content("Check the Console.");
    }
}
```

Navigating to `https://localhost:<port>/ControllerFilters` runs the following code:

* `ControllerFiltersController.OnActionExecuting`

  * `GlobalSampleActionFilter.OnActionExecuting`

    * `SampleActionFilterAttribute.OnActionExecuting`
      * `ControllerFiltersController.Index`
    * `SampleActionFilterAttribute.OnActionExecuted`

  * `GlobalSampleActionFilter.OnActionExecuted`

* `ControllerFiltersController.OnActionExecuted`

Controller level filters set the [Order](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/Filters/ControllerActionFilter.cs#L15-L17) property to `int.MinValue`. Controller level filters can **not** be set to run after filters applied to methods. Order is explained in the next section.

For Razor Pages, see [Implement Razor Page filters by overriding filter methods](../../razor-pages/filter?view=aspnetcore-10.0#implement-razor-page-filters-by-overriding-filter-methods).

### Override the default order

The default sequence of execution can be overridden by implementing [IOrderedFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iorderedfilter). `IOrderedFilter` exposes the [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iorderedfilter.order#microsoft-aspnetcore-mvc-filters-iorderedfilter-order) property that takes precedence over scope to determine the order of execution. A filter with a lower `Order` value:

* Runs the *before* code before that of a filter with a higher value of `Order`.
* Runs the *after* code after that of a filter with a higher `Order` value.

In the [Controller level filters](#controller-level-filters) example, `GlobalSampleActionFilter` has global scope so it runs before `SampleActionFilterAttribute`, which has controller scope. To make `SampleActionFilterAttribute` run first, set its order to `int.MinValue`:

```
[SampleActionFilter(Order = int.MinValue)]
public class ControllerFiltersController : Controller
{
    // ...
}
```

To make the global filter `GlobalSampleActionFilter` run first, set its `Order` to `int.MinValue`:

```
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add<GlobalSampleActionFilter>(int.MinValue);
});
```

## Cancellation and short-circuiting

The filter pipeline can be short-circuited by setting the [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resourceexecutingcontext.result#microsoft-aspnetcore-mvc-filters-resourceexecutingcontext-result) property on the [ResourceExecutingContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resourceexecutingcontext) parameter provided to the filter method. For example, the following Resource filter prevents the rest of the pipeline from executing:

[]()

```
public class ShortCircuitingResourceFilterAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        context.Result = new ContentResult
        {
            Content = nameof(ShortCircuitingResourceFilterAttribute)
        };
    }

    public void OnResourceExecuted(ResourceExecutedContext context) { }
}
```

In the following code, both the `[ShortCircuitingResourceFilter]` and the `[ResponseHeader]` filter target the `Index` action method. The `ShortCircuitingResourceFilterAttribute` filter:

* Runs first, because it's a Resource Filter and `ResponseHeaderAttribute` is an Action Filter.
* Short-circuits the rest of the pipeline.

Therefore the `ResponseHeaderAttribute` filter never runs for the `Index` action. This behavior would be the same if both filters were applied at the action method level, provided the `ShortCircuitingResourceFilterAttribute` ran first. The `ShortCircuitingResourceFilterAttribute` runs first because of its filter type:

```
[ResponseHeader("Filter-Header", "Filter Value")]
public class ShortCircuitingController : Controller
{
    [ShortCircuitingResourceFilter]
    public IActionResult Index() =>
        Content($"- {nameof(ShortCircuitingController)}.{nameof(Index)}");
}
```

## Dependency injection

Filters can be added by type or by instance. If an instance is added, that instance is used for every request. If a type is added, it's type-activated. A type-activated filter means:

* An instance is created for each request.
* Any constructor dependencies are populated by [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI).

Filters that are implemented as attributes and added directly to controller classes or action methods cannot have constructor dependencies provided by [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI). Constructor dependencies cannot be provided by DI because attributes must have their constructor parameters supplied where they're applied.

The following filters support constructor dependencies provided from DI:

* [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute)
* [TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute)
* [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory) implemented on the attribute.

The preceding filters can be applied to a controller or an action.

Loggers are available from DI. However, avoid creating and using filters purely for logging purposes. The [built-in framework logging](../../fundamentals/logging/?view=aspnetcore-10.0) typically provides what's needed for logging. Logging added to filters:

* Should focus on business domain concerns or behavior specific to the filter.
* Should **not** log actions or other framework events. The built-in filters already log actions and framework events.

### ServiceFilterAttribute

Service filter implementation types are registered in `Program.cs`. A [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute) retrieves an instance of the filter from DI.

The following code shows the `LoggingResponseHeaderFilterService` class, which uses DI:

```
public class LoggingResponseHeaderFilterService : IResultFilter
{
    private readonly ILogger _logger;

    public LoggingResponseHeaderFilterService(
            ILogger<LoggingResponseHeaderFilterService> logger) =>
        _logger = logger;

    public void OnResultExecuting(ResultExecutingContext context)
    {
        _logger.LogInformation(
            $"- {nameof(LoggingResponseHeaderFilterService)}.{nameof(OnResultExecuting)}");

        context.HttpContext.Response.Headers.Add(
            nameof(OnResultExecuting), nameof(LoggingResponseHeaderFilterService));
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        _logger.LogInformation(
            $"- {nameof(LoggingResponseHeaderFilterService)}.{nameof(OnResultExecuted)}");
    }
}
```

In the following code, `LoggingResponseHeaderFilterService` is added to the DI container:

```
builder.Services.AddScoped<LoggingResponseHeaderFilterService>();
```

In the following code, the `ServiceFilter` attribute retrieves an instance of the `LoggingResponseHeaderFilterService` filter from DI:

```
[ServiceFilter(typeof(LoggingResponseHeaderFilterService))]
public IActionResult WithServiceFilter() =>
    Content($"- {nameof(FilterDependenciesController)}.{nameof(WithServiceFilter)}");
```

When using `ServiceFilterAttribute`, setting [ServiceFilterAttribute.IsReusable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute.isreusable#microsoft-aspnetcore-mvc-servicefilterattribute-isreusable):

* Provides a hint that the filter instance *may* be reused outside of the request scope it was created within. The ASP.NET Core runtime doesn't guarantee:

  * That a single instance of the filter will be created.
  * The filter will not be re-requested from the DI container at some later point.

* Shouldn't be used with a filter that depends on services with a lifetime other than singleton.

[ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute) implements [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory). `IFilterFactory` exposes the [CreateInstance](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.createinstance) method for creating an [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata) instance. `CreateInstance` loads the specified type from DI.

### TypeFilterAttribute

[TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute) is similar to [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute), but its type isn't resolved directly from the DI container. It instantiates the type by using [Microsoft.Extensions.DependencyInjection.ObjectFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.objectfactory).

Because `TypeFilterAttribute` types aren't resolved directly from the DI container:

* Types that are referenced using the `TypeFilterAttribute` don't need to be registered with the DI container. They do have their dependencies fulfilled by the DI container.
* `TypeFilterAttribute` can optionally accept constructor arguments for the type.

When using `TypeFilterAttribute`, setting [TypeFilterAttribute.IsReusable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute.isreusable#microsoft-aspnetcore-mvc-typefilterattribute-isreusable):

* Provides hint that the filter instance *may* be reused outside of the request scope it was created within. The ASP.NET Core runtime provides no guarantees that a single instance of the filter will be created.

* Should not be used with a filter that depends on services with a lifetime other than singleton.

The following example shows how to pass arguments to a type using `TypeFilterAttribute`:

```
[TypeFilter(typeof(LoggingResponseHeaderFilter),
    Arguments = new object[] { "Filter-Header", "Filter Value" })]
public IActionResult WithTypeFilter() =>
    Content($"- {nameof(FilterDependenciesController)}.{nameof(WithTypeFilter)}");
```

## Authorization filters

Authorization filters:

* Are the first filters run in the filter pipeline.
* Control access to action methods.
* Have a before method, but no after method.

Custom authorization filters require a custom authorization framework. Prefer configuring the authorization policies or writing a custom authorization policy over writing a custom filter. The built-in authorization filter:

* Calls the authorization system.
* Does not authorize requests.

Do **not** throw exceptions within authorization filters:

* The exception will not be handled.
* Exception filters will not handle the exception.

Consider issuing a challenge when an exception occurs in an authorization filter.

Learn more about [Authorization](../../security/authorization/introduction?view=aspnetcore-10.0).

## Resource filters

Resource filters:

* Implement either the [IResourceFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresourcefilter) or [IAsyncResourceFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresourcefilter) interface.
* Execution wraps most of the filter pipeline.
* Only [Authorization filters](#authorization-filters) run before resource filters.

Resource filters are useful to short-circuit most of the pipeline. For example, a caching filter can avoid the rest of the pipeline on a cache hit.

Resource filter examples:

* [The short-circuiting resource filter](#short-circuiting-resource-filter) shown previously.

* [DisableFormValueModelBindingAttribute](https://github.com/aspnet/Entropy/blob/master/samples/Mvc.FileUpload/Filters/DisableFormValueModelBindingAttribute.cs):

  * Prevents model binding from accessing the form data.
  * Used for large file uploads to prevent the form data from being read into memory.

## Action filters

Action filters do **not** apply to Razor Pages. Razor Pages supports [IPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter) and [IAsyncPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter). For more information, see [Filter methods for Razor Pages](../../razor-pages/filter?view=aspnetcore-10.0).

Action filters:

* Implement either the [IActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter) or [IAsyncActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncactionfilter) interface.
* Their execution surrounds the execution of action methods.

The following code shows a sample action filter:

```
public class SampleActionFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
    }
}
```

The [ActionExecutingContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext) provides the following properties:

* [ActionArguments](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext.actionarguments#microsoft-aspnetcore-mvc-filters-actionexecutingcontext-actionarguments) - enables reading the inputs to an action method.
* [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) - enables manipulating the controller instance.
* [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext.result) - setting `Result` short-circuits execution of the action method and subsequent action filters.

Throwing an exception in an action method:

* Prevents running of subsequent filters.
* Unlike setting `Result`, is treated as a failure instead of a successful result.

The [ActionExecutedContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext) provides `Controller` and `Result` plus the following properties:

* [Canceled](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.canceled) - True if the action execution was short-circuited by another filter.

* [Exception](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.exception) - Non-null if the action or a previously run action filter threw an exception. Setting this property to null:

  * Effectively handles the exception.
  * `Result` is executed as if it was returned from the action method.

For an `IAsyncActionFilter`, a call to the [ActionExecutionDelegate](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutiondelegate):

* Executes any subsequent action filters and the action method.
* Returns `ActionExecutedContext`.

To short-circuit, assign [Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext.Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext.result#microsoft-aspnetcore-mvc-filters-actionexecutingcontext-result) to a result instance and don't call `next` (the `ActionExecutionDelegate`).

The framework provides an abstract [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute) that can be subclassed.

The `OnActionExecuting` action filter can be used to:

* Validate model state.
* Return an error if the state is invalid.

```
public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}
```

Note

Controllers annotated with the `[ApiController]` attribute automatically validate model state and return a 400 response. For more information, see [Automatic HTTP 400 responses](../../web-api/?view=aspnetcore-10.0#automatic-http-400-responses).

The `OnActionExecuted` method runs after the action method:

* And can see and manipulate the results of the action through the [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.result#microsoft-aspnetcore-mvc-filters-actionexecutedcontext-result) property.

* [Canceled](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.canceled#microsoft-aspnetcore-mvc-filters-actionexecutedcontext-canceled) is set to true if the action execution was short-circuited by another filter.

* [Exception](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.exception#microsoft-aspnetcore-mvc-filters-actionexecutedcontext-exception) is set to a non-null value if the action or a subsequent action filter threw an exception. Setting `Exception` to null:

  * Effectively handles an exception.
  * `ActionExecutedContext.Result` is executed as if it were returned normally from the action method.

## Exception filters

Exception filters:

* Implement [IExceptionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iexceptionfilter) or [IAsyncExceptionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncexceptionfilter).
* Can be used to implement common error handling policies.

The following sample exception filter displays details about exceptions that occur when the app is in development:

```
public class SampleExceptionFilter : IExceptionFilter
{
    private readonly IHostEnvironment _hostEnvironment;

    public SampleExceptionFilter(IHostEnvironment hostEnvironment) =>
        _hostEnvironment = hostEnvironment;

    public void OnException(ExceptionContext context)
    {
        if (!_hostEnvironment.IsDevelopment())
        {
            // Don't display exception details unless running in Development.
            return;
        }

        context.Result = new ContentResult
        {
            Content = context.Exception.ToString()
        };
    }
}
```

The following code tests the exception filter:

```
[TypeFilter(typeof(SampleExceptionFilter))]
public class ExceptionController : Controller
{
    public IActionResult Index() =>
        Content($"- {nameof(ExceptionController)}.{nameof(Index)}");
}
```

Exception filters:

* Don't have before and after events.
* Implement [OnException](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iexceptionfilter.onexception) or [OnExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncexceptionfilter.onexceptionasync).
* Handle unhandled exceptions that occur in Razor Page or controller creation, [model binding](../models/model-binding?view=aspnetcore-10.0), action filters, or action methods.
* Do **not** catch exceptions that occur in resource filters, result filters, or MVC result execution.

To handle an exception, set the [ExceptionHandled](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptioncontext.exceptionhandled) property to `true` or assign the [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptioncontext.result) property. This stops propagation of the exception. An exception filter can't turn an exception into a "success". Only an action filter can do that.

Exception filters:

* Are good for trapping exceptions that occur within actions.
* Are not as flexible as error handling middleware.

Prefer middleware for exception handling. Use exception filters only where error handling *differs* based on which action method is called. For example, an app might have action methods for both API endpoints and for views/HTML. The API endpoints could return error information as JSON, while the view-based actions could return an error page as HTML.

## Result filters

Result filters:

* Implement an interface:

  * [IResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter) or [IAsyncResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter)
  * [IAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ialwaysrunresultfilter) or [IAsyncAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncalwaysrunresultfilter)

* Their execution surrounds the execution of action results.

### IResultFilter and IAsyncResultFilter

The following code shows a sample result filter:

```
public class SampleResultFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        // Do something before the result executes.
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // Do something after the result executes.
    }
}
```

The kind of result being executed depends on the action. An action returning a view includes all razor processing as part of the [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult) being executed. An API method might perform some serialization as part of the execution of the result. Learn more about [action results](actions?view=aspnetcore-10.0).

Result filters are only executed when an action or action filter produces an action result. Result filters are not executed when:

* An authorization filter or resource filter short-circuits the pipeline.
* An exception filter handles an exception by producing an action result.

The [Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter.onresultexecuting) method can short-circuit execution of the action result and subsequent result filters by setting [Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext.Cancel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultexecutingcontext.cancel#microsoft-aspnetcore-mvc-filters-resultexecutingcontext-cancel) to `true`. Write to the response object when short-circuiting to avoid generating an empty response. Throwing an exception in `IResultFilter.OnResultExecuting`:

* Prevents execution of the action result and subsequent filters.
* Is treated as a failure instead of a successful result.

When the [Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter.onresultexecuted) method runs, the response has probably already been sent to the client. If the response has already been sent to the client, it cannot be changed.

`ResultExecutedContext.Canceled` is set to `true` if the action result execution was short-circuited by another filter.

`ResultExecutedContext.Exception` is set to a non-null value if the action result or a subsequent result filter threw an exception. Setting `Exception` to null effectively handles an exception and prevents the exception from being thrown again later in the pipeline. There is no reliable way to write data to a response when handling an exception in a result filter. If the headers have been flushed to the client when an action result throws an exception, there's no reliable mechanism to send a failure code.

For an [IAsyncResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter), a call to `await next` on the [ResultExecutionDelegate](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultexecutiondelegate) executes any subsequent result filters and the action result. To short-circuit, set [ResultExecutingContext.Cancel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultexecutingcontext.cancel#microsoft-aspnetcore-mvc-filters-resultexecutingcontext-cancel) to `true` and don't call the `ResultExecutionDelegate`:

```
public class SampleAsyncResultFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(
        ResultExecutingContext context, ResultExecutionDelegate next)
    {
        if (context.Result is not EmptyResult)
        {
            await next();
        }
        else
        {
            context.Cancel = true;
        }
    }
}
```

The framework provides an abstract `ResultFilterAttribute` that can be subclassed. The [ResponseHeaderAttribute](#response-header-attribute) class shown previously is an example of a result filter attribute.

### IAlwaysRunResultFilter and IAsyncAlwaysRunResultFilter

The [IAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ialwaysrunresultfilter) and [IAsyncAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncalwaysrunresultfilter) interfaces declare an [IResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter) implementation that runs for all action results. This includes action results produced by:

* Authorization filters and resource filters that short-circuit.
* Exception filters.

For example, the following filter always runs and sets an action result ([ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult)) with a *422 Unprocessable Entity* status code when content negotiation fails:

```
public class UnprocessableResultFilter : IAlwaysRunResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is StatusCodeResult statusCodeResult
            && statusCodeResult.StatusCode == StatusCodes.Status415UnsupportedMediaType)
        {
            context.Result = new ObjectResult("Unprocessable")
            {
                StatusCode = StatusCodes.Status422UnprocessableEntity
            };
        }
    }

    public void OnResultExecuted(ResultExecutedContext context) { }
}
```

## IFilterFactory

[IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory) implements [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata). Therefore, an `IFilterFactory` instance can be used as an `IFilterMetadata` instance anywhere in the filter pipeline. When the runtime prepares to invoke the filter, it attempts to cast it to an `IFilterFactory`. If that cast succeeds, the [CreateInstance](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.createinstance) method is called to create the `IFilterMetadata` instance that is invoked. This provides a flexible design, since the precise filter pipeline doesn't need to be set explicitly when the app starts.

`IFilterFactory.IsReusable`:

* Is a hint by the factory that the filter instance created by the factory may be reused outside of the request scope it was created within.
* Should ***not*** be used with a filter that depends on services with a lifetime other than singleton.

The ASP.NET Core runtime doesn't guarantee:

* That a single instance of the filter will be created.
* The filter will not be re-requested from the DI container at some later point.

Warning

Only configure [IFilterFactory.IsReusable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.isreusable#microsoft-aspnetcore-mvc-filters-ifilterfactory-isreusable) to return `true` if the source of the filters is unambiguous, the filters are stateless, and the filters are safe to use across multiple HTTP requests. For instance, don't return filters from DI that are registered as scoped or transient if `IFilterFactory.IsReusable` returns `true`.

`IFilterFactory` can be implemented using custom attribute implementations as another approach to creating filters:

```
public class ResponseHeaderFilterFactory : Attribute, IFilterFactory
{
    public bool IsReusable => false;

    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) =>
        new InternalResponseHeaderFilter();

    private class InternalResponseHeaderFilter : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context) =>
            context.HttpContext.Response.Headers.Add(
                nameof(OnActionExecuting), nameof(InternalResponseHeaderFilter));

        public void OnActionExecuted(ActionExecutedContext context) { }
    }
```

The filter is applied in the following code:

```
[ResponseHeaderFilterFactory]
public IActionResult Index() =>
    Content($"- {nameof(FilterFactoryController)}.{nameof(Index)}");
```

### IFilterFactory implemented on an attribute

Filters that implement `IFilterFactory` are useful for filters that:

* Don't require passing parameters.
* Have constructor dependencies that need to be filled by DI.

[TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute) implements [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory). `IFilterFactory` exposes the [CreateInstance](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.createinstance) method for creating an [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata) instance. `CreateInstance` loads the specified type from the services container (DI).

```
public class SampleActionTypeFilterAttribute : TypeFilterAttribute
{
    public SampleActionTypeFilterAttribute()
         : base(typeof(InternalSampleActionFilter)) { }

    private class InternalSampleActionFilter : IActionFilter
    {
        private readonly ILogger<InternalSampleActionFilter> _logger;

        public InternalSampleActionFilter(ILogger<InternalSampleActionFilter> logger) =>
            _logger = logger;

        public void OnActionExecuting(ActionExecutingContext context)
        {
            _logger.LogInformation(
                $"- {nameof(InternalSampleActionFilter)}.{nameof(OnActionExecuting)}");
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            _logger.LogInformation(
                $"- {nameof(InternalSampleActionFilter)}.{nameof(OnActionExecuted)}");
        }
    }
}
```

The following code shows three approaches to applying the filter:

```
[SampleActionTypeFilter]
public IActionResult WithDirectAttribute() =>
    Content($"- {nameof(FilterFactoryController)}.{nameof(WithDirectAttribute)}");

[TypeFilter(typeof(SampleActionTypeFilterAttribute))]
public IActionResult WithTypeFilterAttribute() =>
    Content($"- {nameof(FilterFactoryController)}.{nameof(WithTypeFilterAttribute)}");

[ServiceFilter(typeof(SampleActionTypeFilterAttribute))]
public IActionResult WithServiceFilterAttribute() =>
    Content($"- {nameof(FilterFactoryController)}.{nameof(WithServiceFilterAttribute)}");
```

In the preceding code, the first approach to applying the filter is preferred.

## Use middleware in the filter pipeline

Resource filters work like [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) in that they surround the execution of everything that comes later in the pipeline. But filters differ from middleware in that they're part of the runtime, which means that they have access to context and constructs.

To use middleware as a filter, create a type with a `Configure` method that specifies the middleware to inject into the filter pipeline. The following example uses middleware to set a response header:

```
public class FilterMiddlewarePipeline
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            context.Response.Headers.Add("Pipeline", "Middleware");

            await next();
        });
    }
}
```

Use the [MiddlewareFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.middlewarefilterattribute) to run the middleware:

```
[MiddlewareFilter(typeof(FilterMiddlewarePipeline))]
public class FilterMiddlewareController : Controller
{
    public IActionResult Index() =>
        Content($"- {nameof(FilterMiddlewareController)}.{nameof(Index)}");
}
```

Middleware filters run at the same stage of the filter pipeline as Resource filters, before model binding and after the rest of the pipeline.

## Thread safety

When passing an *instance* of a filter into `Add`, instead of its `Type`, the filter is a singleton and is **not** thread-safe.

## Related content

* [View or download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/filters/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).
* [Filter methods for Razor Pages in ASP.NET Core](../../razor-pages/filter?view=aspnetcore-10.0)

By [Kirk Larkin](https://github.com/serpent5), [Rick Anderson](https://twitter.com/RickAndMSFT), [Tom Dykstra](https://github.com/tdykstra/), and [Steve Smith](https://ardalis.com/)

*Filters* in ASP.NET Core allow code to be run before or after specific stages in the request processing pipeline.

Built-in filters handle tasks such as:

* Authorization, preventing access to resources a user isn't authorized for.
* Response caching, short-circuiting the request pipeline to return a cached response.

Custom filters can be created to handle cross-cutting concerns. Examples of cross-cutting concerns include error handling, caching, configuration, authorization, and logging. Filters avoid duplicating code. For example, an error handling exception filter could consolidate error handling.

This document applies to Razor Pages, API controllers, and controllers with views. Filters don't work directly with [Razor components](../../blazor/components/?view=aspnetcore-10.0). A filter can only indirectly affect a component when:

* The component is embedded in a page or view.
* The page or controller and view uses the filter.

[View or download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/filters/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## How filters work

Filters run within the *ASP.NET Core action invocation pipeline*, sometimes referred to as the *filter pipeline*. The filter pipeline runs after ASP.NET Core selects the action to execute.

### Filter types

Each filter type is executed at a different stage in the filter pipeline:

* [Authorization filters](#authorization-filters) run first and are used to determine whether the user is authorized for the request. Authorization filters short-circuit the pipeline if the request is not authorized.

* [Resource filters](#resource-filters):

  * Run after authorization.
  * [OnResourceExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresourcefilter.onresourceexecuting) runs code before the rest of the filter pipeline. For example, `OnResourceExecuting` runs code before model binding.
  * [OnResourceExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresourcefilter.onresourceexecuted) runs code after the rest of the pipeline has completed.

* [Action filters](#action-filters):

  * Run code immediately before and after an action method is called.
  * Can change the arguments passed into an action.
  * Can change the result returned from the action.
  * Are **not** supported in Razor Pages.

* [Exception filters](#exception-filters) apply global policies to unhandled exceptions that occur before the response body has been written to.

* [Result filters](#result-filters) run code immediately before and after the execution of action results. They run only when the action method has executed successfully. They are useful for logic that must surround view or formatter execution.

The following diagram shows how filter types interact in the filter pipeline.

## Implementation

Filters support both synchronous and asynchronous implementations through different interface definitions.

Synchronous filters run code before and after their pipeline stage. For example, [OnActionExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter.onactionexecuting) is called before the action method is called. [OnActionExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter.onactionexecuted) is called after the action method returns.

```
public class MySampleActionFilter : IActionFilter 
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), context.HttpContext.Request.Path);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), context.HttpContext.Request.Path);
    }
}
```

In the preceding code, [MyDebug](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/filters/samples/3.x/FiltersSample/Helper/MyDebug.cs) is a utility function in the [sample download](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/filters/samples/3.x/FiltersSample/Helper/MyDebug.cs).

Asynchronous filters define an `On-Stage-ExecutionAsync` method. For example, [OnActionExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncactionfilter.onactionexecutionasync):

```
public class SampleAsyncActionFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // Do something before the action executes.

        // next() calls the action method.
        var resultContext = await next();
        // resultContext.Result is set.
        // Do something after the action executes.
    }
}
```

In the preceding code, the `SampleAsyncActionFilter` has an [ActionExecutionDelegate](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutiondelegate) (`next`) that executes the action method.

### Multiple filter stages

Interfaces for multiple filter stages can be implemented in a single class. For example, the [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute) class implements:

* Synchronous: [IActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter) and [IResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter)
* Asynchronous: [IAsyncActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncactionfilter) and [IAsyncResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter)
* [IOrderedFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iorderedfilter)

Implement **either** the synchronous or the async version of a filter interface, **not** both. The runtime checks first to see if the filter implements the async interface, and if so, it calls that. If not, it calls the synchronous interface's methods. If both asynchronous and synchronous interfaces are implemented in one class, only the async method is called. When using abstract classes like [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute), override only the synchronous methods or the asynchronous methods for each filter type.

### Built-in filter attributes

ASP.NET Core includes built-in attribute-based filters that can be subclassed and customized. For example, the following result filter adds a header to the response:

[]()

```
public class AddHeaderAttribute : ResultFilterAttribute
{
    private readonly string _name;
    private readonly string _value;

    public AddHeaderAttribute(string name, string value)
    {
        _name = name;
        _value = value;
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers.Add( _name, new string[] { _value });
        base.OnResultExecuting(context);
    }
}
```

Attributes allow filters to accept arguments, as shown in the preceding example. Apply the `AddHeaderAttribute` to a controller or action method and specify the name and value of the HTTP header:

```
[AddHeader("Author", "Rick Anderson")]
public class SampleController : Controller
{
    public IActionResult Index()
    {
        return Content("Examine the headers using the F12 developer tools.");
    }
```

Use a tool such as the [browser developer tools](https://developer.mozilla.org/docs/Learn/Common_questions/What_are_browser_developer_tools) to examine the headers. Under **Response Headers**, `author: Rick Anderson` is displayed.

The following code implements an `ActionFilterAttribute` that:

* Reads the title and name from the configuration system. Unlike the previous sample, the following code doesn't require filter parameters to be added to the code.
* Adds the title and name to the response header.

```
public class MyActionFilterAttribute : ActionFilterAttribute
{
    private readonly PositionOptions _settings;

    public MyActionFilterAttribute(IOptions<PositionOptions> options)
    {
        _settings = options.Value;
        Order = 1;
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers.Add(_settings.Title, 
                                                 new string[] { _settings.Name });
        base.OnResultExecuting(context);
    }
}
```

The configuration options are provided from the [configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) using the [options pattern](../../fundamentals/configuration/options?view=aspnetcore-10.0). For example, from the `appsettings.json` file:

```
{
  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

In the `StartUp.ConfigureServices`:

* The `PositionOptions` class is added to the service container with the `"Position"` configuration area.
* The `MyActionFilterAttribute` is added to the service container.

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(
             Configuration.GetSection("Position"));
    services.AddScoped<MyActionFilterAttribute>();

    services.AddControllersWithViews();
}
```

The following code shows the `PositionOptions` class:

```
public class PositionOptions
{
    public string Title { get; set; }
    public string Name { get; set; }
}
```

The following code applies the `MyActionFilterAttribute` to the `Index2` method:

```
[AddHeader("Author", "Rick Anderson")]
public class SampleController : Controller
{
    public IActionResult Index()
    {
        return Content("Examine the headers using the F12 developer tools.");
    }

    [ServiceFilter(typeof(MyActionFilterAttribute))]
    public IActionResult Index2()
    {
        return Content("Header values by configuration.");
    }
```

Under **Response Headers**, `author: Rick Anderson`, and `Editor: Joe Smith` is displayed when the `Sample/Index2` endpoint is called.

The following code applies the `MyActionFilterAttribute` and the `AddHeaderAttribute` to the Razor Page:

```
[AddHeader("Author", "Rick Anderson")]
[ServiceFilter(typeof(MyActionFilterAttribute))]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}
```

Filters cannot be applied to Razor Page handler methods. They can be applied either to the Razor Page model or globally.

Several of the filter interfaces have corresponding attributes that can be used as base classes for custom implementations.

Filter attributes:

* [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute)
* [ExceptionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptionfilterattribute)
* [ResultFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultfilterattribute)
* [FormatFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatfilterattribute)
* [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute)
* [TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute)

## Filter scopes and order of execution

A filter can be added to the pipeline at one of three *scopes*:

* Using an attribute on a controller action. Filter attributes cannot be applied to Razor Pages handler methods.
* Using an attribute on a controller or Razor Page.
* Globally for all controllers, actions, and Razor Pages as shown in the following code:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
   {
        options.Filters.Add(typeof(MySampleActionFilter));
    });
}
```

### Default order of execution

When there are multiple filters for a particular stage of the pipeline, scope determines the default order of filter execution. Global filters surround class filters, which in turn surround method filters.

As a result of filter nesting, the *after* code of filters runs in the reverse order of the *before* code. The filter sequence:

* The *before* code of global filters.

  * The *before* code of controller and Razor Page filters.

    * The *before* code of action method filters.
    * The *after* code of action method filters.

  * The *after* code of controller and Razor Page filters.

* The *after* code of global filters.

The following example that illustrates the order in which filter methods are called for synchronous action filters.

| Sequence | Filter scope             | Filter method       |
| -------- | ------------------------ | ------------------- |
| 1        | Global                   | `OnActionExecuting` |
| 2        | Controller or Razor Page | `OnActionExecuting` |
| 3        | Method                   | `OnActionExecuting` |
| 4        | Method                   | `OnActionExecuted`  |
| 5        | Controller or Razor Page | `OnActionExecuted`  |
| 6        | Global                   | `OnActionExecuted`  |

### Controller level filters

Every controller that inherits from the [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) base class includes [Controller.OnActionExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecuting), [Controller.OnActionExecutionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecutionasync), and [Controller.OnActionExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.onactionexecuted) `OnActionExecuted` methods. These methods:

* Wrap the filters that run for a given action.
* `OnActionExecuting` is called before any of the action's filters.
* `OnActionExecuted` is called after all of the action filters.
* `OnActionExecutionAsync` is called before any of the action's filters. Code in the filter after `next` runs after the action method.

For example, in the download sample, `MySampleActionFilter` is applied globally in startup.

The `TestController`:

* Applies the `SampleActionFilterAttribute` (`[SampleActionFilter]`) to the `FilterTest2` action.
* Overrides `OnActionExecuting` and `OnActionExecuted`.

```
public class TestController : Controller
{
    [SampleActionFilter(Order = int.MinValue)]
    public IActionResult FilterTest2()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), HttpContext.Request.Path);
        base.OnActionExecuting(context);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), HttpContext.Request.Path);
        base.OnActionExecuted(context);
    }
}
```

[MyDisplayRouteInfo](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) is provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package and displays route information.

Navigating to `https://localhost:5001/Test/FilterTest2` runs the following code:

* `TestController.OnActionExecuting`

  * `MySampleActionFilter.OnActionExecuting`

    * `SampleActionFilterAttribute.OnActionExecuting`
      * `TestController.FilterTest2`
    * `SampleActionFilterAttribute.OnActionExecuted`

  * `MySampleActionFilter.OnActionExecuted`

* `TestController.OnActionExecuted`

Controller level filters set the [Order](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/Filters/ControllerActionFilter.cs#L15-L17) property to `int.MinValue`. Controller level filters can **not** be set to run after filters applied to methods. Order is explained in the next section.

For Razor Pages, see [Implement Razor Page filters by overriding filter methods](../../razor-pages/filter?view=aspnetcore-10.0#implement-razor-page-filters-by-overriding-filter-methods).

### Overriding the default order

The default sequence of execution can be overridden by implementing [IOrderedFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iorderedfilter). `IOrderedFilter` exposes the [Order](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iorderedfilter.order#microsoft-aspnetcore-mvc-filters-iorderedfilter-order) property that takes precedence over scope to determine the order of execution. A filter with a lower `Order` value:

* Runs the *before* code before that of a filter with a higher value of `Order`.
* Runs the *after* code after that of a filter with a higher `Order` value.

The `Order` property is set with a constructor parameter:

```
[SampleActionFilter(Order = int.MinValue)]
```

Consider the two action filters in the following controller:

```
[MyAction2Filter]
public class Test2Controller : Controller
{
    public IActionResult FilterTest2()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), HttpContext.Request.Path);
        base.OnActionExecuting(context);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), HttpContext.Request.Path);
        base.OnActionExecuted(context);
    }
}
```

A global filter is added in `StartUp.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
   {
        options.Filters.Add(typeof(MySampleActionFilter));
    });
}
```

The 3 filters run in the following order:

* `Test2Controller.OnActionExecuting`

  * `MySampleActionFilter.OnActionExecuting`

    * `MyAction2FilterAttribute.OnActionExecuting`
      * `Test2Controller.FilterTest2`
    * `MyAction2FilterAttribute.OnResultExecuting`

  * `MySampleActionFilter.OnActionExecuted`

* `Test2Controller.OnActionExecuted`

The `Order` property overrides scope when determining the order in which filters run. Filters are sorted first by order, then scope is used to break ties. All of the built-in filters implement `IOrderedFilter` and set the default `Order` value to 0. As mentioned previously, controller level filters set the [Order](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/Filters/ControllerActionFilter.cs#L15-L17) property to `int.MinValue` For built-in filters, scope determines order unless `Order` is set to a non-zero value.

In the preceding code, `MySampleActionFilter` has global scope so it runs before `MyAction2FilterAttribute`, which has controller scope. To make `MyAction2FilterAttribute` run first, set the order to `int.MinValue`:

```
[MyAction2Filter(int.MinValue)]
public class Test2Controller : Controller
{
    public IActionResult FilterTest2()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), HttpContext.Request.Path);
        base.OnActionExecuting(context);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), HttpContext.Request.Path);
        base.OnActionExecuted(context);
    }
}
```

To make the global filter `MySampleActionFilter` run first, set `Order` to `int.MinValue`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
   {
        options.Filters.Add(typeof(MySampleActionFilter),
                            int.MinValue);
    });
}
```

## Cancellation and short-circuiting

The filter pipeline can be short-circuited by setting the [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resourceexecutingcontext.result#microsoft-aspnetcore-mvc-filters-resourceexecutingcontext-result) property on the [ResourceExecutingContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resourceexecutingcontext) parameter provided to the filter method. For instance, the following Resource filter prevents the rest of the pipeline from executing:

[]()

```
public class ShortCircuitingResourceFilterAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        context.Result = new ContentResult()
        {
            Content = "Resource unavailable - header not set."
        };
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}
```

In the following code, both the `ShortCircuitingResourceFilter` and the `AddHeader` filter target the `SomeResource` action method. The `ShortCircuitingResourceFilter`:

* Runs first, because it's a Resource Filter and `AddHeader` is an Action Filter.
* Short-circuits the rest of the pipeline.

Therefore the `AddHeader` filter never runs for the `SomeResource` action. This behavior would be the same if both filters were applied at the action method level, provided the `ShortCircuitingResourceFilter` ran first. The `ShortCircuitingResourceFilter` runs first because of its filter type, or by explicit use of `Order` property.

```
[AddHeader("Author", "Rick Anderson")]
public class SampleController : Controller
{
    public IActionResult Index()
    {
        return Content("Examine the headers using the F12 developer tools.");
    }

    [ServiceFilter(typeof(MyActionFilterAttribute))]
    public IActionResult Index2()
    {
        return Content("Header values by configuration.");
    }

    [ShortCircuitingResourceFilter]
    public IActionResult SomeResource()
    {
        return Content("Successful access to resource - header is set.");
    }

    [AddHeaderWithFactory]
    public IActionResult HeaderWithFactory()
    {
        return Content("Examine the headers using the F12 developer tools.");
    }
}
```

## Dependency injection

Filters can be added by type or by instance. If an instance is added, that instance is used for every request. If a type is added, it's type-activated. A type-activated filter means:

* An instance is created for each request.
* Any constructor dependencies are populated by [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI).

Filters that are implemented as attributes and added directly to controller classes or action methods cannot have constructor dependencies provided by [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI). Constructor dependencies cannot be provided by DI because:

* Attributes must have their constructor parameters supplied where they're applied.
* This is a limitation of how attributes work.

The following filters support constructor dependencies provided from DI:

* [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute)
* [TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute)
* [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory) implemented on the attribute.

The preceding filters can be applied to a controller or action method:

Loggers are available from DI. However, avoid creating and using filters purely for logging purposes. The [built-in framework logging](../../fundamentals/logging/?view=aspnetcore-10.0) typically provides what's needed for logging. Logging added to filters:

* Should focus on business domain concerns or behavior specific to the filter.
* Should **not** log actions or other framework events. The built-in filters log actions and framework events.

### ServiceFilterAttribute

Service filter implementation types are registered in `ConfigureServices`. A [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute) retrieves an instance of the filter from DI.

The following code shows the `AddHeaderResultServiceFilter`:

```
public class AddHeaderResultServiceFilter : IResultFilter
{
    private ILogger _logger;
    public AddHeaderResultServiceFilter(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<AddHeaderResultServiceFilter>();
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        var headerName = "OnResultExecuting";
        context.HttpContext.Response.Headers.Add(
            headerName, new string[] { "ResultExecutingSuccessfully" });
        _logger.LogInformation("Header added: {HeaderName}", headerName);
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // Can't add to headers here because response has started.
        _logger.LogInformation("AddHeaderResultServiceFilter.OnResultExecuted");
    }
}
```

In the following code, `AddHeaderResultServiceFilter` is added to the DI container:

```
public void ConfigureServices(IServiceCollection services)
{
    // Add service filters.
    services.AddScoped<AddHeaderResultServiceFilter>();
    services.AddScoped<SampleActionFilterAttribute>();

    services.AddControllersWithViews(options =>
   {
       options.Filters.Add(new AddHeaderAttribute("GlobalAddHeader",
           "Result filter added to MvcOptions.Filters"));         // An instance
        options.Filters.Add(typeof(MySampleActionFilter));         // By type
        options.Filters.Add(new SampleGlobalActionFilter());       // An instance
    });
}
```

In the following code, the `ServiceFilter` attribute retrieves an instance of the `AddHeaderResultServiceFilter` filter from DI:

```
[ServiceFilter(typeof(AddHeaderResultServiceFilter))]
public IActionResult Index()
{
    return View();
}
```

When using `ServiceFilterAttribute`, setting [ServiceFilterAttribute.IsReusable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute.isreusable#microsoft-aspnetcore-mvc-servicefilterattribute-isreusable):

* Provides a hint that the filter instance *may* be reused outside of the request scope it was created within. The ASP.NET Core runtime doesn't guarantee:

  * That a single instance of the filter will be created.
  * The filter will not be re-requested from the DI container at some later point.

* Should not be used with a filter that depends on services with a lifetime other than singleton.

[ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute) implements [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory). `IFilterFactory` exposes the [CreateInstance](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.createinstance) method for creating an [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata) instance. `CreateInstance` loads the specified type from DI.

### TypeFilterAttribute

[TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute) is similar to [ServiceFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.servicefilterattribute), but its type isn't resolved directly from the DI container. It instantiates the type by using [Microsoft.Extensions.DependencyInjection.ObjectFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.objectfactory).

Because `TypeFilterAttribute` types aren't resolved directly from the DI container:

* Types that are referenced using the `TypeFilterAttribute` don't need to be registered with the DI container. They do have their dependencies fulfilled by the DI container.
* `TypeFilterAttribute` can optionally accept constructor arguments for the type.

When using `TypeFilterAttribute`, setting [TypeFilterAttribute.IsReusable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute.isreusable#microsoft-aspnetcore-mvc-typefilterattribute-isreusable):

* Provides hint that the filter instance *may* be reused outside of the request scope it was created within. The ASP.NET Core runtime provides no guarantees that a single instance of the filter will be created.

* Should not be used with a filter that depends on services with a lifetime other than singleton.

The following example shows how to pass arguments to a type using `TypeFilterAttribute`:

```
[TypeFilter(typeof(LogConstantFilter),
    Arguments = new object[] { "Method 'Hi' called" })]
public IActionResult Hi(string name)
{
    return Content($"Hi {name}");
}
```

## Authorization filters

Authorization filters:

* Are the first filters run in the filter pipeline.
* Control access to action methods.
* Have a before method, but no after method.

Custom authorization filters require a custom authorization framework. Prefer configuring the authorization policies or writing a custom authorization policy over writing a custom filter. The built-in authorization filter:

* Calls the authorization system.
* Does not authorize requests.

Do **not** throw exceptions within authorization filters:

* The exception will not be handled.
* Exception filters will not handle the exception.

Consider issuing a challenge when an exception occurs in an authorization filter.

Learn more about [Authorization](../../security/authorization/introduction?view=aspnetcore-10.0).

## Resource filters

Resource filters:

* Implement either the [IResourceFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresourcefilter) or [IAsyncResourceFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresourcefilter) interface.
* Execution wraps most of the filter pipeline.
* Only [Authorization filters](#authorization-filters) run before resource filters.

Resource filters are useful to short-circuit most of the pipeline. For example, a caching filter can avoid the rest of the pipeline on a cache hit.

Resource filter examples:

* [The short-circuiting resource filter](#short-circuiting-resource-filter) shown previously.

* [DisableFormValueModelBindingAttribute](https://github.com/aspnet/Entropy/blob/master/samples/Mvc.FileUpload/Filters/DisableFormValueModelBindingAttribute.cs):

  * Prevents model binding from accessing the form data.
  * Used for large file uploads to prevent the form data from being read into memory.

## Action filters

Action filters do **not** apply to Razor Pages. Razor Pages supports [IPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ipagefilter) and [IAsyncPageFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncpagefilter). For more information, see [Filter methods for Razor Pages](../../razor-pages/filter?view=aspnetcore-10.0).

Action filters:

* Implement either the [IActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iactionfilter) or [IAsyncActionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncactionfilter) interface.
* Their execution surrounds the execution of action methods.

The following code shows a sample action filter:

```
public class MySampleActionFilter : IActionFilter 
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), context.HttpContext.Request.Path);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
        MyDebug.Write(MethodBase.GetCurrentMethod(), context.HttpContext.Request.Path);
    }
}
```

The [ActionExecutingContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext) provides the following properties:

* [ActionArguments](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext.actionarguments#microsoft-aspnetcore-mvc-filters-actionexecutingcontext-actionarguments) - enables reading the inputs to an action method.
* [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) - enables manipulating the controller instance.
* [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext.result) - setting `Result` short-circuits execution of the action method and subsequent action filters.

Throwing an exception in an action method:

* Prevents running of subsequent filters.
* Unlike setting `Result`, is treated as a failure instead of a successful result.

The [ActionExecutedContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext) provides `Controller` and `Result` plus the following properties:

* [Canceled](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.canceled) - True if the action execution was short-circuited by another filter.

* [Exception](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.exception) - Non-null if the action or a previously run action filter threw an exception. Setting this property to null:

  * Effectively handles the exception.
  * `Result` is executed as if it was returned from the action method.

For an `IAsyncActionFilter`, a call to the [ActionExecutionDelegate](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutiondelegate):

* Executes any subsequent action filters and the action method.
* Returns `ActionExecutedContext`.

To short-circuit, assign [Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext.Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutingcontext.result#microsoft-aspnetcore-mvc-filters-actionexecutingcontext-result) to a result instance and don't call `next` (the `ActionExecutionDelegate`).

The framework provides an abstract [ActionFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionfilterattribute) that can be subclassed.

The `OnActionExecuting` action filter can be used to:

* Validate model state.
* Return an error if the state is invalid.

```
public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext 
                                           context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(
                                                context.ModelState);
        }
    }
```

Note

Controllers annotated with the `[ApiController]` attribute automatically validate model state and return a 400 response. For more information, see [Automatic HTTP 400 responses](../../web-api/?view=aspnetcore-10.0#automatic-http-400-responses). The `OnActionExecuted` method runs after the action method:

* And can see and manipulate the results of the action through the [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.result#microsoft-aspnetcore-mvc-filters-actionexecutedcontext-result) property.

* [Canceled](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.canceled#microsoft-aspnetcore-mvc-filters-actionexecutedcontext-canceled) is set to true if the action execution was short-circuited by another filter.

* [Exception](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.actionexecutedcontext.exception#microsoft-aspnetcore-mvc-filters-actionexecutedcontext-exception) is set to a non-null value if the action or a subsequent action filter threw an exception. Setting `Exception` to null:

  * Effectively handles an exception.
  * `ActionExecutedContext.Result` is executed as if it were returned normally from the action method.

```
public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext 
                                           context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(
                                                context.ModelState);
        }
    }


    public override void OnActionExecuted(ActionExecutedContext 
                                          context)
    {
        var result = context.Result;
        // Do something with Result.
        if (context.Canceled == true)
        {
            // Action execution was short-circuited by another filter.
        }

        if(context.Exception != null)
        {
            // Exception thrown by action or action filter.
            // Set to null to handle the exception.
            context.Exception = null;
        }
        base.OnActionExecuted(context);
    }
}
```

## Exception filters

Exception filters:

* Implement [IExceptionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iexceptionfilter) or [IAsyncExceptionFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncexceptionfilter).
* Can be used to implement common error handling policies.

The following sample exception filter uses a custom error view to display details about exceptions that occur when the app is in development:

```
public class CustomExceptionFilter : IExceptionFilter
{
    private readonly IWebHostEnvironment _hostingEnvironment;
    private readonly IModelMetadataProvider _modelMetadataProvider;

    public CustomExceptionFilter(
        IWebHostEnvironment hostingEnvironment,
        IModelMetadataProvider modelMetadataProvider)
    {
        _hostingEnvironment = hostingEnvironment;
        _modelMetadataProvider = modelMetadataProvider;
    }

    public void OnException(ExceptionContext context)
    {
        if (!_hostingEnvironment.IsDevelopment())
        {
            return;
        }
        var result = new ViewResult {ViewName = "CustomError"};
        result.ViewData = new ViewDataDictionary(_modelMetadataProvider,
                                                    context.ModelState);
        result.ViewData.Add("Exception", context.Exception);
        // TODO: Pass additional detailed data via ViewData
        context.Result = result;
    }
}
```

The following code tests the exception filter:

```
[TypeFilter(typeof(CustomExceptionFilter))]
public class FailingController : Controller
{
    [AddHeader("Failing Controller", 
               "Won't appear when exception is handled")]
    public IActionResult Index()
    {
        throw new Exception("Testing custom exception filter.");
    }
}
```

Exception filters:

* Don't have before and after events.
* Implement [OnException](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iexceptionfilter.onexception) or [OnExceptionAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncexceptionfilter.onexceptionasync).
* Handle unhandled exceptions that occur in Razor Page or controller creation, [model binding](../models/model-binding?view=aspnetcore-10.0), action filters, or action methods.
* Do **not** catch exceptions that occur in resource filters, result filters, or MVC result execution.

To handle an exception, set the [ExceptionHandled](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptioncontext.exceptionhandled) property to `true` or assign the [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptioncontext.result) property. This stops propagation of the exception. An exception filter can't turn an exception into a "success". Only an action filter can do that.

Exception filters:

* Are good for trapping exceptions that occur within actions.
* Are not as flexible as error handling middleware.

Prefer middleware for exception handling. Use exception filters only where error handling *differs* based on which action method is called. For example, an app might have action methods for both API endpoints and for views/HTML. The API endpoints could return error information as JSON, while the view-based actions could return an error page as HTML.

## Result filters

Result filters:

* Implement an interface:

  * [IResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter) or [IAsyncResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter)
  * [IAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ialwaysrunresultfilter) or [IAsyncAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncalwaysrunresultfilter)

* Their execution surrounds the execution of action results.

### IResultFilter and IAsyncResultFilter

The following code shows a result filter that adds an HTTP header:

```
public class AddHeaderResultServiceFilter : IResultFilter
{
    private ILogger _logger;
    public AddHeaderResultServiceFilter(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<AddHeaderResultServiceFilter>();
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        var headerName = "OnResultExecuting";
        context.HttpContext.Response.Headers.Add(
            headerName, new string[] { "ResultExecutingSuccessfully" });
        _logger.LogInformation("Header added: {HeaderName}", headerName);
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // Can't add to headers here because response has started.
        _logger.LogInformation("AddHeaderResultServiceFilter.OnResultExecuted");
    }
}
```

The kind of result being executed depends on the action. An action returning a view includes all razor processing as part of the [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult) being executed. An API method might perform some serialization as part of the execution of the result. Learn more about [action results](actions?view=aspnetcore-10.0).

Result filters are only executed when an action or action filter produces an action result. Result filters are not executed when:

* An authorization filter or resource filter short-circuits the pipeline.
* An exception filter handles an exception by producing an action result.

The [Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuting](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter.onresultexecuting) method can short-circuit execution of the action result and subsequent result filters by setting [Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext.Cancel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultexecutingcontext.cancel#microsoft-aspnetcore-mvc-filters-resultexecutingcontext-cancel) to `true`. Write to the response object when short-circuiting to avoid generating an empty response. Throwing an exception in `IResultFilter.OnResultExecuting`:

* Prevents execution of the action result and subsequent filters.
* Is treated as a failure instead of a successful result.

When the [Microsoft.AspNetCore.Mvc.Filters.IResultFilter.OnResultExecuted](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter.onresultexecuted) method runs, the response has probably already been sent to the client. If the response has already been sent to the client, it cannot be changed.

`ResultExecutedContext.Canceled` is set to `true` if the action result execution was short-circuited by another filter.

`ResultExecutedContext.Exception` is set to a non-null value if the action result or a subsequent result filter threw an exception. Setting `Exception` to null effectively handles an exception and prevents the exception from being thrown again later in the pipeline. There is no reliable way to write data to a response when handling an exception in a result filter. If the headers have been flushed to the client when an action result throws an exception, there's no reliable mechanism to send a failure code.

For an [IAsyncResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncresultfilter), a call to `await next` on the [ResultExecutionDelegate](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultexecutiondelegate) executes any subsequent result filters and the action result. To short-circuit, set [ResultExecutingContext.Cancel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.resultexecutingcontext.cancel#microsoft-aspnetcore-mvc-filters-resultexecutingcontext-cancel) to `true` and don't call the `ResultExecutionDelegate`:

```
public class MyAsyncResponseFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(ResultExecutingContext context,
                                             ResultExecutionDelegate next)
    {
        if (!(context.Result is EmptyResult))
        {
            await next();
        }
        else
        {
            context.Cancel = true;
        }

    }
}
```

The framework provides an abstract `ResultFilterAttribute` that can be subclassed. The [AddHeaderAttribute](#add-header-attribute) class shown previously is an example of a result filter attribute.

### IAlwaysRunResultFilter and IAsyncAlwaysRunResultFilter

The [IAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ialwaysrunresultfilter) and [IAsyncAlwaysRunResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iasyncalwaysrunresultfilter) interfaces declare an [IResultFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.iresultfilter) implementation that runs for all action results. This includes action results produced by:

* Authorization filters and resource filters that short-circuit.
* Exception filters.

For example, the following filter always runs and sets an action result ([ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult)) with a *422 Unprocessable Entity* status code when content negotiation fails:

```
public class UnprocessableResultFilter : Attribute, IAlwaysRunResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is StatusCodeResult statusCodeResult &&
            statusCodeResult.StatusCode == (int) HttpStatusCode.UnsupportedMediaType)
        {
            context.Result = new ObjectResult("Can't process this!")
            {
                StatusCode = (int) HttpStatusCode.UnsupportedMediaType,
            };
        }
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
    }
}
```

## IFilterFactory

[IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory) implements [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata). Therefore, an `IFilterFactory` instance can be used as an `IFilterMetadata` instance anywhere in the filter pipeline. When the runtime prepares to invoke the filter, it attempts to cast it to an `IFilterFactory`. If that cast succeeds, the [CreateInstance](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.createinstance) method is called to create the `IFilterMetadata` instance that is invoked. This provides a flexible design, since the precise filter pipeline doesn't need to be set explicitly when the app starts.

`IFilterFactory.IsReusable`:

* Is a hint by the factory that the filter instance created by the factory may be reused outside of the request scope it was created within.
* Should ***not*** be used with a filter that depends on services with a lifetime other than singleton.

The ASP.NET Core runtime doesn't guarantee:

* That a single instance of the filter will be created.
* The filter will not be re-requested from the DI container at some later point.

Warning

Only configure [IFilterFactory.IsReusable](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.isreusable#microsoft-aspnetcore-mvc-filters-ifilterfactory-isreusable) to return `true` if the source of the filters is unambiguous, the filters are stateless, and the filters are safe to use across multiple HTTP requests. For instance, don't return filters from DI that are registered as scoped or transient if `IFilterFactory.IsReusable` returns `true`. `IFilterFactory` can be implemented using custom attribute implementations as another approach to creating filters:

```
public class AddHeaderWithFactoryAttribute : Attribute, IFilterFactory
{
    // Implement IFilterFactory
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        return new InternalAddHeaderFilter();
    }

    private class InternalAddHeaderFilter : IResultFilter
    {
        public void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(
                "Internal", new string[] { "My header" });
        }

        public void OnResultExecuted(ResultExecutedContext context)
        {
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}
```

The filter is applied in the following code:

```
[AddHeader("Author", "Rick Anderson")]
public class SampleController : Controller
{
    public IActionResult Index()
    {
        return Content("Examine the headers using the F12 developer tools.");
    }

    [ServiceFilter(typeof(MyActionFilterAttribute))]
    public IActionResult Index2()
    {
        return Content("Header values by configuration.");
    }

    [ShortCircuitingResourceFilter]
    public IActionResult SomeResource()
    {
        return Content("Successful access to resource - header is set.");
    }

    [AddHeaderWithFactory]
    public IActionResult HeaderWithFactory()
    {
        return Content("Examine the headers using the F12 developer tools.");
    }
}
```

Test the preceding code by running the [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/filters/samples):

* Invoke the F12 developer tools.
* Navigate to `https://localhost:5001/Sample/HeaderWithFactory`.

The F12 developer tools display the following response headers added by the sample code:

* **author:** `Rick Anderson`
* **globaladdheader:** `Result filter added to MvcOptions.Filters`
* **internal:** `My header`

The preceding code creates the **internal:** `My header` response header.

### IFilterFactory implemented on an attribute

Filters that implement `IFilterFactory` are useful for filters that:

* Don't require passing parameters.
* Have constructor dependencies that need to be filled by DI.

[TypeFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.typefilterattribute) implements [IFilterFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory). `IFilterFactory` exposes the [CreateInstance](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifilterfactory.createinstance) method for creating an [IFilterMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.ifiltermetadata) instance. `CreateInstance` loads the specified type from the services container (DI).

```
public class SampleActionFilterAttribute : TypeFilterAttribute
{
    public SampleActionFilterAttribute()
                         :base(typeof(SampleActionFilterImpl))
    { 
    }

    private class SampleActionFilterImpl : IActionFilter
    {
        private readonly ILogger _logger;
        public SampleActionFilterImpl(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger<SampleActionFilterAttribute>();
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
           _logger.LogInformation("SampleActionFilterAttribute.OnActionExecuting");
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            _logger.LogInformation("SampleActionFilterAttribute.OnActionExecuted");
        }
    }
}
```

The following code shows three approaches to applying the `[SampleActionFilter]`:

```
[SampleActionFilter]
public IActionResult FilterTest()
{
    return Content("From FilterTest");
}

[TypeFilter(typeof(SampleActionFilterAttribute))]
public IActionResult TypeFilterTest()
{
    return Content("From TypeFilterTest");
}

// ServiceFilter must be registered in ConfigureServices or
// System.InvalidOperationException: No service for type '<filter>'
// has been registered. Is thrown.
[ServiceFilter(typeof(SampleActionFilterAttribute))]
public IActionResult ServiceFilterTest()
{
    return Content("From ServiceFilterTest");
}
```

In the preceding code, decorating the method with `[SampleActionFilter]` is the preferred approach to applying the `SampleActionFilter`.

## Using middleware in the filter pipeline

Resource filters work like [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) in that they surround the execution of everything that comes later in the pipeline. But filters differ from middleware in that they're part of the runtime, which means that they have access to context and constructs.

To use middleware as a filter, create a type with a `Configure` method that specifies the middleware to inject into the filter pipeline. The following example uses the localization middleware to establish the current culture for a request:

```
public class LocalizationPipeline
{
    public void Configure(IApplicationBuilder applicationBuilder)
    {
        var supportedCultures = new[]
        {
            new CultureInfo("en-US"),
            new CultureInfo("fr")
        };

        var options = new RequestLocalizationOptions
        {
            DefaultRequestCulture = new RequestCulture(
                                       culture: "en-US", 
                                       uiCulture: "en-US"),
            SupportedCultures = supportedCultures,
            SupportedUICultures = supportedCultures
        };
        options.RequestCultureProviders = new[] 
            { new RouteDataRequestCultureProvider() {
                Options = options } };

        applicationBuilder.UseRequestLocalization(options);
    }
}
```

Use the [MiddlewareFilterAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.middlewarefilterattribute) to run the middleware:

```
[Route("{culture}/[controller]/[action]")]
[MiddlewareFilter(typeof(LocalizationPipeline))]
public IActionResult CultureFromRouteData()
{
    return Content(
          $"CurrentCulture:{CultureInfo.CurrentCulture.Name},"
        + $"CurrentUICulture:{CultureInfo.CurrentUICulture.Name}");
}
```

Middleware filters run at the same stage of the filter pipeline as Resource filters, before model binding and after the rest of the pipeline.

## Thread safety

When passing an *instance* of a filter into `Add`, instead of its `Type`, the filter is a singleton and is **not** thread-safe.

## Next actions

* See [Filter methods for Razor Pages](../../razor-pages/filter?view=aspnetcore-10.0).
* To experiment with filters, [download, test, and modify the GitHub sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/filters/samples).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/custom-formatters
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/web-api/advanced/custom-formatters.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Custom formatters in ASP.NET Core Web API

ASP.NET Core MVC supports data exchange in Web APIs using input and output formatters. Input formatters are used by [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0). Output formatters are used to [format responses](formatting?view=aspnetcore-10.0).

The framework provides built-in input and output formatters for JSON and XML. It provides a built-in output formatter for plain text, but doesn't provide an input formatter for plain text.

This article shows how to add support for additional formats by creating custom formatters. For an example of a custom plain text input formatter, see [TextPlainInputFormatter](https://github.com/aspnet/Entropy/blob/master/samples/Mvc.Formatters/TextPlainInputFormatter.cs) on GitHub.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## When to use a custom formatter

Use a custom formatter to add support for a content type that isn't handled by the built-in formatters.

## Overview of how to create a custom formatter

To create a custom formatter:

* For serializing data sent to the client, create an output formatter class.
* For deserializing data received from the client, create an input formatter class.
* Add instances of formatter classes to the [InputFormatters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.inputformatters) and [OutputFormatters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.outputformatters) collections in [MvcOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions).

## Create a custom formatter

To create a formatter:

* Derive the class from the appropriate base class. The sample app derives from [TextOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textoutputformatter) and [TextInputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textinputformatter).
* Specify supported media types and encodings in the constructor.
* Override the [CanReadType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter.canreadtype) and [CanWriteType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.canwritetype) methods.
* Override the [ReadRequestBodyAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter.readrequestbodyasync) and [WriteResponseBodyAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.writeresponsebodyasync) methods.

The following code shows the `VcardOutputFormatter` class from the [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples):

```
public class VcardOutputFormatter : TextOutputFormatter
{
    public VcardOutputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanWriteType(Type? type)
        => typeof(Contact).IsAssignableFrom(type)
            || typeof(IEnumerable<Contact>).IsAssignableFrom(type);

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
        var buffer = new StringBuilder();

        if (context.Object is IEnumerable<Contact> contacts)
        {
            foreach (var contact in contacts)
            {
                FormatVcard(buffer, contact, logger);
            }
        }
        else
        {
            FormatVcard(buffer, (Contact)context.Object!, logger);
        }

        await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
    }

    private static void FormatVcard(
        StringBuilder buffer, Contact contact, ILogger logger)
    {
        buffer.AppendLine("BEGIN:VCARD");
        buffer.AppendLine("VERSION:2.1");
        buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
        buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
        buffer.AppendLine($"UID:{contact.Id}");
        buffer.AppendLine("END:VCARD");

        logger.LogInformation("Writing {FirstName} {LastName}",
            contact.FirstName, contact.LastName);
    }
}
```

### Derive from the appropriate base class

For text media types (for example, vCard), derive from the [TextInputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textinputformatter) or [TextOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textoutputformatter) base class:

```
public class VcardOutputFormatter : TextOutputFormatter
```

For binary types, derive from the [InputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter) or [OutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter) base class.

### Specify supported media types and encodings

In the constructor, specify supported media types and encodings by adding to the [SupportedMediaTypes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.supportedmediatypes) and [SupportedEncodings](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textoutputformatter.supportedencodings) collections:

```
public VcardOutputFormatter()
{
    SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

    SupportedEncodings.Add(Encoding.UTF8);
    SupportedEncodings.Add(Encoding.Unicode);
}
```

A formatter class can **not** use constructor injection for its dependencies. For example, `ILogger<VcardOutputFormatter>` can't be added as a parameter to the constructor. To access services, use the context object that gets passed in to the methods. A code example in this article and the [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples) show how to do this.

### Override CanReadType and CanWriteType

Specify the type to deserialize into or serialize from by overriding the [CanReadType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter.canreadtype) or [CanWriteType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.canwritetype) methods. For example, to create vCard text from a `Contact` type and vice versa:

```
protected override bool CanWriteType(Type? type)
    => typeof(Contact).IsAssignableFrom(type)
        || typeof(IEnumerable<Contact>).IsAssignableFrom(type);
```

#### The CanWriteResult method

In some scenarios, [CanWriteResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.canwriteresult) must be overridden rather than [CanWriteType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.canwritetype). Use `CanWriteResult` if the following conditions are true:

* The action method returns a model class.
* There are derived classes that might be returned at runtime.
* The derived class returned by the action must be known at runtime.

For example, suppose the action method:

* Signature returns a `Person` type.
* Can return a `Student` or `Instructor` type that derives from `Person`.

For the formatter to handle only `Student` objects, check the type of [Object](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformattercanwritecontext.object#microsoft-aspnetcore-mvc-formatters-outputformattercanwritecontext-object) in the context object provided to the `CanWriteResult` method. When the action method returns [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult):

* It's not necessary to use `CanWriteResult`.
* The `CanWriteType` method receives the runtime type.

[]()

### Override ReadRequestBodyAsync and WriteResponseBodyAsync

Deserialization or serialization is performed in [ReadRequestBodyAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter.readrequestbodyasync) or [WriteResponseBodyAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.writeresponsebodyasync). The following example shows how to get services from the dependency injection container. Services can't be obtained from constructor parameters:

```
public override async Task WriteResponseBodyAsync(
    OutputFormatterWriteContext context, Encoding selectedEncoding)
{
    var httpContext = context.HttpContext;
    var serviceProvider = httpContext.RequestServices;

    var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
    var buffer = new StringBuilder();

    if (context.Object is IEnumerable<Contact> contacts)
    {
        foreach (var contact in contacts)
        {
            FormatVcard(buffer, contact, logger);
        }
    }
    else
    {
        FormatVcard(buffer, (Contact)context.Object!, logger);
    }

    await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}

private static void FormatVcard(
    StringBuilder buffer, Contact contact, ILogger logger)
{
    buffer.AppendLine("BEGIN:VCARD");
    buffer.AppendLine("VERSION:2.1");
    buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
    buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
    buffer.AppendLine($"UID:{contact.Id}");
    buffer.AppendLine("END:VCARD");

    logger.LogInformation("Writing {FirstName} {LastName}",
        contact.FirstName, contact.LastName);
}
```

## Configure MVC to use a custom formatter

To use a custom formatter, add an instance of the formatter class to the [MvcOptions.InputFormatters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.inputformatters) or [MvcOptions.OutputFormatters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.outputformatters) collection:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, new VcardInputFormatter());
    options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});
```

Formatters are evaluated in the order they're inserted, where the first one takes precedence.

## The complete `VcardInputFormatter` class

The following code shows the `VcardInputFormatter` class from the [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples):

```
public class VcardInputFormatter : TextInputFormatter
{
    public VcardInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanReadType(Type type)
        => type == typeof(Contact);

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context, Encoding effectiveEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardInputFormatter>>();

        using var reader = new StreamReader(httpContext.Request.Body, effectiveEncoding);
        string? nameLine = null;

        try
        {
            await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
            await ReadLineAsync("VERSION:", reader, context, logger);

            nameLine = await ReadLineAsync("N:", reader, context, logger);

            var split = nameLine.Split(";".ToCharArray());
            var contact = new Contact(FirstName: split[1], LastName: split[0].Substring(2));

            await ReadLineAsync("FN:", reader, context, logger);
            await ReadLineAsync("END:VCARD", reader, context, logger);

            logger.LogInformation("nameLine = {nameLine}", nameLine);

            return await InputFormatterResult.SuccessAsync(contact);
        }
        catch
        {
            logger.LogError("Read failed: nameLine = {nameLine}", nameLine);
            return await InputFormatterResult.FailureAsync();
        }
    }

    private static async Task<string> ReadLineAsync(
        string expectedText, StreamReader reader, InputFormatterContext context,
        ILogger logger)
    {
        var line = await reader.ReadLineAsync();

        if (line is null || !line.StartsWith(expectedText))
        {
            var errorMessage = $"Looked for '{expectedText}' and got '{line}'";

            context.ModelState.TryAddModelError(context.ModelName, errorMessage);
            logger.LogError(errorMessage);

            throw new Exception(errorMessage);
        }

        return line;
    }
}
```

## Test the app

[Run the sample app for this article](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples), which implements basic vCard input and output formatters. The app reads and writes vCards similar to the following format:

```
BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD
```

To see vCard output, run the app and send a Get request with Accept header `text/vcard` to `https://localhost:<port>/api/contacts`.

To add a vCard to the in-memory collection of contacts:

* Send a `Post` request to `/api/contacts` with a tool like [http-repl](../http-repl/?view=aspnetcore-10.0).
* Set the `Content-Type` header to `text/vcard`.
* Set `vCard` text in the body, formatted like the preceding example.

## Additional resources

* [Format response data in ASP.NET Core Web API](formatting?view=aspnetcore-10.0)
* [Manage Protobuf references with dotnet-grpc](../../grpc/dotnet-grpc?view=aspnetcore-10.0)

ASP.NET Core MVC supports data exchange in Web APIs using input and output formatters. Input formatters are used by [Model Binding](../../mvc/models/model-binding?view=aspnetcore-10.0). Output formatters are used to [format responses](formatting?view=aspnetcore-10.0).

The framework provides built-in input and output formatters for JSON and XML. It provides a built-in output formatter for plain text, but doesn't provide an input formatter for plain text.

This article shows how to add support for additional formats by creating custom formatters. For an example of a custom plain text input formatter, see [TextPlainInputFormatter](https://github.com/aspnet/Entropy/blob/master/samples/Mvc.Formatters/TextPlainInputFormatter.cs) on GitHub.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## When to use a custom formatter

Use a custom formatter to add support for a content type that isn't handled by the built-in formatters.

## Overview of how to create a custom formatter

To create a custom formatter:

* For serializing data sent to the client, create an output formatter class.
* For deserializing data received from the client, create an input formatter class.
* Add instances of formatter classes to the [InputFormatters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.inputformatters) and [OutputFormatters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.outputformatters) collections in [MvcOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions).

## Create a custom formatter

To create a formatter:

* Derive the class from the appropriate base class. The sample app derives from [TextOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textoutputformatter) and [TextInputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textinputformatter).
* Specify supported media types and encodings in the constructor.
* Override the [CanReadType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter.canreadtype) and [CanWriteType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.canwritetype) methods.
* Override the [ReadRequestBodyAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter.readrequestbodyasync) and [WriteResponseBodyAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.writeresponsebodyasync) methods.

The following code shows the `VcardOutputFormatter` class from the [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples):

```
public class VcardOutputFormatter : TextOutputFormatter
{
    public VcardOutputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanWriteType(Type type)
    {
        return typeof(Contact).IsAssignableFrom(type) ||
            typeof(IEnumerable<Contact>).IsAssignableFrom(type);
    }

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
        var buffer = new StringBuilder();

        if (context.Object is IEnumerable<Contact> contacts)
        {
            foreach (var contact in contacts)
            {
                FormatVcard(buffer, contact, logger);
            }
        }
        else
        {
            FormatVcard(buffer, (Contact)context.Object, logger);
        }

        await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
    }

    private static void FormatVcard(
        StringBuilder buffer, Contact contact, ILogger logger)
    {
        buffer.AppendLine("BEGIN:VCARD");
        buffer.AppendLine("VERSION:2.1");
        buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
        buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
        buffer.AppendLine($"UID:{contact.Id}");
        buffer.AppendLine("END:VCARD");

        logger.LogInformation("Writing {FirstName} {LastName}",
            contact.FirstName, contact.LastName);
    }
}
```

### Derive from the appropriate base class

For text media types (for example, vCard), derive from the [TextInputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textinputformatter) or [TextOutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textoutputformatter) base class:

```
public class VcardOutputFormatter : TextOutputFormatter
```

For binary types, derive from the [InputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter) or [OutputFormatter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter) base class.

### Specify supported media types and encodings

In the constructor, specify supported media types and encodings by adding to the [SupportedMediaTypes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.supportedmediatypes) and [SupportedEncodings](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.textoutputformatter.supportedencodings) collections:

```
public VcardOutputFormatter()
{
    SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

    SupportedEncodings.Add(Encoding.UTF8);
    SupportedEncodings.Add(Encoding.Unicode);
}
```

A formatter class can **not** use constructor injection for its dependencies. For example, `ILogger<VcardOutputFormatter>` can't be added as a parameter to the constructor. To access services, use the context object that gets passed in to the methods. A code example in this article and the [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples) show how to do this.

### Override CanReadType and CanWriteType

Specify the type to deserialize into or serialize from by overriding the [CanReadType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter.canreadtype) or [CanWriteType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.canwritetype) methods. For example, to create vCard text from a `Contact` type and vice versa:

```
protected override bool CanWriteType(Type type)
{
    return typeof(Contact).IsAssignableFrom(type) ||
        typeof(IEnumerable<Contact>).IsAssignableFrom(type);
}
```

#### The CanWriteResult method

In some scenarios, [CanWriteResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.canwriteresult) must be overridden rather than [CanWriteType](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.canwritetype). Use `CanWriteResult` if the following conditions are true:

* The action method returns a model class.
* There are derived classes that might be returned at runtime.
* The derived class returned by the action must be known at runtime.

For example, suppose the action method:

* Signature returns a `Person` type.
* Can return a `Student` or `Instructor` type that derives from `Person`.

For the formatter to handle only `Student` objects, check the type of [Object](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformattercanwritecontext.object#microsoft-aspnetcore-mvc-formatters-outputformattercanwritecontext-object) in the context object provided to the `CanWriteResult` method. When the action method returns [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult):

* It's not necessary to use `CanWriteResult`.
* The `CanWriteType` method receives the runtime type.

[]()

### Override ReadRequestBodyAsync and WriteResponseBodyAsync

Deserialization or serialization is performed in [ReadRequestBodyAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter.readrequestbodyasync) or [WriteResponseBodyAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter.writeresponsebodyasync). The following example shows how to get services from the dependency injection container. Services can't be obtained from constructor parameters:

```
public override async Task WriteResponseBodyAsync(
    OutputFormatterWriteContext context, Encoding selectedEncoding)
{
    var httpContext = context.HttpContext;
    var serviceProvider = httpContext.RequestServices;

    var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
    var buffer = new StringBuilder();

    if (context.Object is IEnumerable<Contact> contacts)
    {
        foreach (var contact in contacts)
        {
            FormatVcard(buffer, contact, logger);
        }
    }
    else
    {
        FormatVcard(buffer, (Contact)context.Object, logger);
    }

    await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}

private static void FormatVcard(
    StringBuilder buffer, Contact contact, ILogger logger)
{
    buffer.AppendLine("BEGIN:VCARD");
    buffer.AppendLine("VERSION:2.1");
    buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
    buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
    buffer.AppendLine($"UID:{contact.Id}");
    buffer.AppendLine("END:VCARD");

    logger.LogInformation("Writing {FirstName} {LastName}",
        contact.FirstName, contact.LastName);
}
```

## Configure MVC to use a custom formatter

To use a custom formatter, add an instance of the formatter class to the [MvcOptions.InputFormatters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.inputformatters) or [MvcOptions.OutputFormatters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.outputformatters) collection:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.InputFormatters.Insert(0, new VcardInputFormatter());
        options.OutputFormatters.Insert(0, new VcardOutputFormatter());
    });
}
```

Formatters are evaluated in the order you insert them. The first one takes precedence.

## The complete `VcardInputFormatter` class

The following code shows the `VcardInputFormatter` class from the [sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples):

```
public class VcardInputFormatter : TextInputFormatter
{
    public VcardInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanReadType(Type type)
    {
        return type == typeof(Contact);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context, Encoding effectiveEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardInputFormatter>>();

        using var reader = new StreamReader(httpContext.Request.Body, effectiveEncoding);
        string nameLine = null;

        try
        {
            await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
            await ReadLineAsync("VERSION:", reader, context, logger);

            nameLine = await ReadLineAsync("N:", reader, context, logger);

            var split = nameLine.Split(";".ToCharArray());
            var contact = new Contact
            {
                LastName = split[0].Substring(2),
                FirstName = split[1]
            };

            await ReadLineAsync("FN:", reader, context, logger);
            await ReadLineAsync("END:VCARD", reader, context, logger);

            logger.LogInformation("nameLine = {nameLine}", nameLine);

            return await InputFormatterResult.SuccessAsync(contact);
        }
        catch
        {
            logger.LogError("Read failed: nameLine = {nameLine}", nameLine);
            return await InputFormatterResult.FailureAsync();
        }
    }

    private static async Task<string> ReadLineAsync(
        string expectedText, StreamReader reader, InputFormatterContext context,
        ILogger logger)
    {
        var line = await reader.ReadLineAsync();

        if (!line.StartsWith(expectedText))
        {
            var errorMessage = $"Looked for '{expectedText}' and got '{line}'";

            context.ModelState.TryAddModelError(context.ModelName, errorMessage);
            logger.LogError(errorMessage);

            throw new Exception(errorMessage);
        }

        return line;
    }
}
```

## Test the app

[Run the sample app for this article](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/advanced/custom-formatters/samples), which implements basic vCard input and output formatters. The app reads and writes vCards similar to the following format:

```
BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD
```

To see vCard output, run the app and send a Get request with Accept header `text/vcard` to `https://localhost:5001/api/contacts`.

To add a vCard to the in-memory collection of contacts:

* Send a `Post` request to `/api/contacts` with a tool like curl.
* Set the `Content-Type` header to `text/vcard`.
* Set `vCard` text in the body, formatted like the preceding example.

## Additional resources

* [Format response data in ASP.NET Core Web API](formatting?view=aspnetcore-10.0)
* [Manage Protobuf references with dotnet-grpc](../../grpc/dotnet-grpc?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/crud
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-rp/crud.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial 2: Razor Pages with EF Core in ASP.NET Core - CRUD

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Tom Dykstra](https://github.com/tdykstra), [Jeremy Likness](https://twitter.com/jeremylikness), and [Jon P Smith](https://twitter.com/thereformedprog)

The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. For information about the tutorial series, see [the first tutorial](intro?view=aspnetcore-10.0).

If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) and compare that code to what you created by following the tutorial.

This article presents the second tutorial in the eight-part series. In this tutorial, you review and customize the CRUD (create, read, update, delete) code that the scaffolding automatically creates for your Razor pages.

In this tutorial, you:

* Customize pages: **Details**, **Create**, **Edit**, and **Delete**
* Explore how to protect against overposting
* Review different methods to read a single entity
* Explore the View model

## Prerequisites

* Review the previous tutorial, [Razor Pages with Entity Framework Core in ASP.NET Core (Tutorial 1 of 8)](intro?view=aspnetcore-10.0).

## Review the tutorial approach (no repository)

Some developers use a service layer or repository pattern to create an abstraction layer between the UI (the Razor Pages) and the data access layer. Rather than that approach, this tutorial adds the EF Core code directly to the page model classes. This method helps to minimize complexity and keep the tutorial focused on EF Core.

## Update the Details page

The scaffolded code for the Students pages doesn't include enrollment data. In this section, enrollments are added to the **Details** page.

### Read enrollments

To display a student's enrollment data on the page, the enrollment data must be read. The scaffolded code in the `Pages/Students/Details.cshtml.cs` file reads only the `Student` data without the `Enrollment` data:

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}
```

Replace the `OnGetAsync` method with the following code that reads enrollment data for the selected student. The changes are highlighted.

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}
```

The [Include](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.include) and [ThenInclude](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.theninclude#microsoft-entityframeworkcore-entityframeworkqueryableextensions-theninclude-3\(microsoft-entityframeworkcore-query-iincludablequeryable\(\(-0-1\)\)-system-linq-expressions-expression\(\(system-func\(\(-1-2\)\)\)\)\))) methods cause the context to load the `Student.Enrollments` navigation property, and within each enrollment, the `Enrollment.Course` navigation property. These methods are examined in detail in [Read related data (Tutorial 6 of 8)](read-related-data?view=aspnetcore-10.0).

The [AsNoTracking](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.asnotracking) method improves performance in scenarios where the entities returned aren't updated in the current context. The `AsNoTracking` setting is discussed later in this tutorial.

### Display enrollments

To display a list of enrollments, replace the code in the *Pages/Students/Details.cshtml* file with the following code. The changes are highlighted.

```
@page
@model ContosoUniversity.Pages.Students.DetailsModel

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>
```

This code loops through the entities in the `Enrollments` navigation property. For each enrollment, the code displays the course title and the grade. The course title is retrieved from the `Course` entity stored in the `Course` navigation property of the `Enrollments` entity.

Run the app, select the **Students** tab, and select the **Details** link for a student. The list of courses and grades for the selected student is displayed.

### Use different methods to read one entity

The generated code uses the [FirstOrDefaultAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.firstordefaultasync#microsoft-entityframeworkcore-entityframeworkqueryableextensions-firstordefaultasync-1\(system-linq-iqueryable\(\(-0\)\)-system-threading-cancellationtoken\)) method to read one entity. This method returns null if nothing is found. Otherwise, it returns the first row found that satisfies the query filter criteria. The `FirstOrDefaultAsync` method is generally a better choice than the following alternatives:

* The [SingleOrDefaultAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.singleordefaultasync#microsoft-entityframeworkcore-entityframeworkqueryableextensions-singleordefaultasync-1\(system-linq-iqueryable\(\(-0\)\)-system-linq-expressions-expression\(\(system-func\(\(-0-system-boolean\)\)\)\)-system-threading-cancellationtoken\)) method throws an exception if there's more than one entity that satisfies the query filter. To determine if the query can return more than one row, the `SingleOrDefaultAsync` method tries to fetch multiple rows. The extra work is unnecessary if the query can only return one entity, as when it searches on a unique key.

* The [FindAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.findasync#microsoft-entityframeworkcore-dbcontext-findasync\(system-type-system-object\(\)\)) method locates an entity with the primary key. If the context tracks an entity with the primary key, the key is returned without a request to the database. This method is optimized to look up a single entity, but you can't call the `Include` method with the `FindAsync` method. If related data is needed, the `FirstOrDefaultAsync` method is the better choice.

### Route data vs. query string

The URL for the **Details** page is `https://localhost:<port>/Students/Details?id=1`. The entity's primary key value is in the query string. Some developers prefer to pass the key value in route data: `https://localhost:<port>/Students/Details/1`. For more information, see [Update the generated pages (Tutorial 5 of 8)](../../tutorials/razor-pages/da1?view=aspnetcore-10.0#update-the-generated-code).

## Update the Create page

The scaffolded `OnPostAsync` code for the **Create** page is vulnerable to [overposting](#prevent-overposting). Replace the `OnPostAsync` method in the *Pages/Students/Create.cshtml.cs* file with the following code.

```
public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}
```

### Use the TryUpdateModelAsync method

The code in the last section creates a `Student` object and then uses posted form fields to update the `Student` object properties. The [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync#microsoft-aspnetcore-mvc-controllerbase-tryupdatemodelasync\(system-object-system-type-system-string\)) method:

* Uses the posted form values from the [PageContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.pagecontext) property in the [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class.

* Updates only the properties listed (`s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate`).

* Looks for form fields with a "student" prefix. For example, `Student.FirstMidName`. It's not case sensitive.

* Uses the [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system to convert form values from strings to the types in the `Student` model. For example, the `EnrollmentDate` value is converted to a `DateTime` type.

Run the app, and create a student entity to test the **Create** page.

## Prevent overposting

Using the `TryUpdateModel` method to update fields with posted values is a security best practice because it prevents overposting. For example, suppose the `Student` entity includes a `Secret` property that this web page shouldn't update or add:

```
public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}
```

Even if the app doesn't have a `Secret` field on the **Create** or **Update** page, a hacker could set the `Secret` value by overposting. A hacker might use a tool such as Fiddler, or write some JavaScript, to post a `Secret` form value. The original code doesn't limit the fields that the model binder uses when it creates a `Student` instance.

Whatever value the hacker specified for the `Secret` form field is updated in the database. The following image shows the Fiddler tool adding the `Secret` field with the value "OverPost" to the posted form values.

The value "OverPost" is successfully added to the `Secret` property of the inserted row. This result occurs even though the app designer never intended to set the `Secret` property with the **Create** page.

### Work with the View model

View models provide an alternative way to prevent overposting.

The application model is often called the *domain model*. The domain model typically contains all the properties required by the corresponding entity in the database. The view model contains only the properties needed for the UI page, for example, the **Create** page.

In addition to the view model, some apps use a binding model or input model to pass data between the Razor Pages page model class and the browser.

Consider the following `StudentVM` view model:

```
public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}
```

The following code uses the `StudentVM` view model to create a new student:

```
[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
```

The [SetValues](/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyvalues.setvalues#microsoft-entityframeworkcore-changetracking-propertyvalues-setvalues\(system-object\)) method sets the values of this object by reading values from another [PropertyValues](/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyvalues) object. The `SetValues` method uses property name matching. The view model type:

* Doesn't need to be related to the model type.
* Requires properties that match.

Using the `StudentVM` view model requires that the **Create** page use the `StudentVM` entity rather than a `Student` entity:

```
@page
@model CreateVMModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Consider the following `Student` view model:

```
using System;

namespace ContosoUniversity.Models
{
    public class StudentVM
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
    }
}
```

The following code uses the `StudentVM` view model to create a new student:

```
[BindProperty]
public StudentVM StudentVM { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
```

The [SetValues](/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyvalues.setvalues#microsoft-entityframeworkcore-changetracking-propertyvalues-setvalues\(system-object\)) method sets the values of this object by reading values from another [PropertyValues](/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyvalues) object. The `SetValues` method uses property name matching. The view model type:

* Doesn't need to be related to the model type.
* Requires properties that match.

Using the `StudentVM` view model requires that the [Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu30snapshots/2-crud/Pages/Students/CreateVM.cshtml) file is updated to use a `StudentVM` entity rather than a `Student` entity.

## Update the Edit page

In the *Pages/Students/Edit.cshtml.cs* file, replace the `OnGetAsync` and `OnPostAsync` methods with the following code.

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Students.FindAsync(id);

    if (Student == null)
    {
        return NotFound();
    }
    return Page();
}

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

    if (studentToUpdate == null)
    {
        return NotFound();
    }

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}
```

The code changes are similar to the **Create** page with a few exceptions:

* The `FirstOrDefaultAsync` method is replaced with the [FindAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1.findasync) method. When you don't have to include related data, the `FindAsync` method is more efficient.
* The `OnPostAsync` method has an `id` parameter.
* The current student is fetched from the database, rather than creating an empty student.

Run the app, and test it by creating and editing a student.

## Use Entity States

The database context keeps track of whether entities in memory are in sync with their corresponding rows in the database. The tracking information determines what happens when the [SaveChangesAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechangesasync#microsoft-entityframeworkcore-dbcontext-savechangesasync\(system-threading-cancellationtoken\)) method is called. For example, when a new entity is passed to the [AddAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.addasync) method, that entity's state is set to [Added](/en-us/dotnet/api/microsoft.entityframeworkcore.entitystate#microsoft-entityframeworkcore-entitystate-added). When the `SaveChangesAsync` method is called, the database context issues a SQL `INSERT` command.

An entity can be in one of the [following states](/en-us/dotnet/api/microsoft.entityframeworkcore.entitystate):

* `Added`: The entity doesn't yet exist in the database. The `SaveChanges` method issues an `INSERT` statement.

* `Unchanged`: No changes need to be saved with this entity. An entity has this status when read from the database.

* `Modified`: Some or all of the entity's property values are modified. The `SaveChanges` method issues an `UPDATE` statement.

* `Deleted`: The entity is marked for deletion. The `SaveChanges` method issues a `DELETE` statement.

* `Detached`: The database context doesn't track the entity.

In a desktop app, state changes are typically set automatically. An entity is read, changes are made, and the entity state is automatically changed to `Modified`. Calling the `SaveChanges` method generates a SQL `UPDATE` statement that updates only the changed properties.

In a web app, the `DbContext` object that reads an entity and displays the data is disposed after a page is rendered. When a page's `OnPostAsync` method is called, a new web request is made with a new instance of the `DbContext` object. Rereading the entity in the new context simulates desktop processing.

## Update the Delete page

In this section, a custom error message is implemented when the call to `SaveChanges` fails.

Replace the code in the *Pages/Students/Delete.cshtml.cs* file with the following code.

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Student == null)
            {
                return NotFound();
            }

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

            if (student == null)
            {
                return NotFound();
            }

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}
```

This code adds the following functionality:

* [Logging](../../fundamentals/logging/?view=aspnetcore-10.0) for .NET and ASP.NET Core.
* The optional parameter `saveChangesError` to the `OnGetAsync` method signature. The `saveChangesError` parameter indicates whether the method call occurs after a failure to delete the `Student` object.

Replace the code in the *Pages/Students/Delete.cshtml.cs* file with the following code. The changes are highlighted (other than cleanup of `using` statements).

```
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Student Student { get; set; }
        public string ErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }

            Student = await _context.Students
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.ID == id);

            if (Student == null)
            {
                return NotFound();
            }

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = "Delete failed. Try again";
            }

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var student = await _context.Students.FindAsync(id);

            if (student == null)
            {
                return NotFound();
            }

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException /* ex */)
            {
                //Log the error (uncomment ex variable name and write a log.)
                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}
```

This code adds the optional parameter `saveChangesError` to the `OnGetAsync` method signature. The `saveChangesError` parameter indicates whether the method call occurs after a failure to delete the `Student` object.

The delete operation might fail because of transient network problems. Transient network errors are more likely when the database is in the cloud. The `saveChangesError` parameter is `false` when the **Delete** page `OnGetAsync` method is called from the UI. When the `OnGetAsync` method is called by the `OnPostAsync` method because the delete operation failed, the `saveChangesError` parameter is true.

The `OnPostAsync` method retrieves the selected entity, then calls the [Remove](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.remove#microsoft-entityframeworkcore-dbcontext-remove\(system-object\)) method to set the entity status to `Deleted`. When `SaveChanges` is called, a SQL `DELETE` command is generated. If call to the `Remove` method fails:

* The database exception is caught.
* The **Delete** page `OnGetAsync` method is called with the `saveChangesError=true` parameter.

Add an error message to the **Delete** page (*Pages/Students/Delete.cshtml*).

```
@page
@model ContosoUniversity.Pages.Students.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>
```

```
@page
@model ContosoUniversity.Pages.Students.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h1>Delete</h1>

<p class="text-danger">@Model.ErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>
```

Run the app and delete a student to test the **Delete** page.

## Next step

[Tutorial 3: Razor Pages with EF Core in ASP.NET Core - Sort, Filter, Paging](sort-filter-page?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/extensibility/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/extensibility/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Data Protection extensibility APIs

* [Core cryptography extensibility](core-crypto?view=aspnetcore-10.0)

* [Key management extensibility](key-management?view=aspnetcore-10.0)

* [Miscellaneous APIs](misc-apis?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-interop/wasm-browser-app
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/dotnet-interop/wasm-browser-app.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# JavaScript `[JSImport]`/`[JSExport]` interop with a WebAssembly Browser App project

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to setup a WebAssembly Browser App project to run .NET from JavaScript (JS) using JS `[JSImport]`/`[JSExport]` interop. For additional information and examples, see [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop in .NET WebAssembly](./?view=aspnetcore-10.0).

For additional guidance, see the [Configuring and hosting .NET WebAssembly applications](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md) guidance in the .NET Runtime (`dotnet/runtime`) GitHub repository.

Existing JS apps can use the expanded client-side WebAssembly support to reuse .NET libraries from JS or to build novel .NET-based apps and frameworks.

Note

This article focuses on running .NET from JS apps without any dependency on [Blazor](../../blazor/?view=aspnetcore-10.0). For guidance on using `[JSImport]`/`[JSExport]` interop in Blazor WebAssembly apps, see [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](../../blazor/javascript-interoperability/import-export-interop?view=aspnetcore-10.0).

These approaches are appropriate when you only expect the Blazor app to run on WebAssembly (WASM). Libraries can make a runtime check to determine if the app is running on WASM by calling [OperatingSystem.IsBrowser](/en-us/dotnet/api/system.operatingsystem.isbrowser).

## Prerequisites

[.NET SDK (latest version)](https://dotnet.microsoft.com/download/dotnet/)

Install the `wasm-tools` workload in an administrative command shell, which brings in the related MSBuild targets:

```
dotnet workload install wasm-tools
```

The tools can also be installed via Visual Studio's installer under the **ASP.NET and web development** workload in the Visual Studio installer. Select the **.NET WebAssembly build tools** option from the list of optional components.

Optionally, install the `wasm-experimental` workload, which adds the following experimental project templates:

* *WebAssembly Browser App* for getting started with .NET on WebAssembly in a browser app.
* *WebAssembly Console App* for getting started in a Node.js-based console app.

After installing the workload, these new templates can be selected when creating a new project. This workload isn't required if you plan to integrate JS `[JSImport]`/`[JSExport]` interop into an existing JS app.

```
dotnet workload install wasm-experimental
```

The templates can also be installed from the [`Microsoft.NET.Runtime.WebAssembly.Templates`](https://www.nuget.org/packages/Microsoft.NET.Runtime.WebAssembly.Templates) NuGet package with the following command:

```
dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates
```

For more information, see the [Experimental workload and project templates](#experimental-workload-and-project-templates) section.

## Namespace

The JS interop API described in this article is controlled by attributes in the [System.Runtime.InteropServices.JavaScript](/en-us/dotnet/api/system.runtime.interopservices.javascript) namespace.

## Project configuration

To configure a project (`.csproj`) to enable JS interop:

* Set the [target framework moniker](/en-us/dotnet/standard/frameworks) (`{TARGET FRAMEWORK}` placeholder):

  ```
  <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
  ```

  .NET 7 (`net7.0`) or later is supported.

* Enable the [AllowUnsafeBlocks](/en-us/dotnet/api/microsoft.build.tasks.csc.allowunsafeblocks#microsoft-build-tasks-csc-allowunsafeblocks) property, which permits the code generator in the Roslyn compiler to use pointers for JS interop:

  ```
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  ```

  Warning

  The JS interop API requires enabling [AllowUnsafeBlocks](/en-us/dotnet/api/microsoft.build.tasks.csc.allowunsafeblocks#microsoft-build-tasks-csc-allowunsafeblocks). Be careful when implementing your own unsafe code in .NET apps, which can introduce security and stability risks. For more information, see [Unsafe code, pointer types, and function pointers](/en-us/dotnet/csharp/language-reference/unsafe-code).

The following is an example project file (`.csproj`) after configuration. The `{TARGET FRAMEWORK}` placeholder is the target framework:

```
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">

  <PropertyGroup>
    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

</Project>
```

Note

In .NET 10 and later, the `WasmEnableHotReload` MSBuild property controls whether [Hot Reload](../../test/hot-reload?view=aspnetcore-10.0) infrastructure is included in the build output. The property is set to `true` by default for the `Debug` configuration. Set the property to `false` to produce bundler-compatible build output, for example for use with webpack or Vite, without Hot Reload dependencies when building the app with `dotnet build`:

```
<PropertyGroup>
  <WasmEnableHotReload>false</WasmEnableHotReload>
</PropertyGroup>
```

* Set the [target framework moniker](/en-us/dotnet/standard/frameworks):

  ```
  <TargetFramework>net7.0</TargetFramework>
  ```

  .NET 7 (`net7.0`) or later is supported.

* Specify `browser-wasm` for the runtime identifier:

  ```
  <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
  ```

* Specify an executable output type:

  ```
  <OutputType>Exe</OutputType>
  ```

* Enable the [AllowUnsafeBlocks](/en-us/dotnet/api/microsoft.build.tasks.csc.allowunsafeblocks#microsoft-build-tasks-csc-allowunsafeblocks) property, which permits the code generator in the Roslyn compiler to use pointers for JS interop:

  ```
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  ```

  Warning

  The JS interop API requires enabling [AllowUnsafeBlocks](/en-us/dotnet/api/microsoft.build.tasks.csc.allowunsafeblocks#microsoft-build-tasks-csc-allowunsafeblocks). Be careful when implementing your own unsafe code in .NET apps, which can introduce security and stability risks. For more information, see [Unsafe code, pointer types, and function pointers](/en-us/dotnet/csharp/language-reference/unsafe-code).

* Specify `WasmMainJSPath` to point to a file on disk. This file is published with the app, but use of the file isn't required if you're integrating .NET into an existing JS app.

  In the following example, the JS file on disk is `main.js`, but any JS filename is permissable:

  ```
  <WasmMainJSPath>main.js</WasmMainJSPath>
  ```

Example project file (`.csproj`) after configuration:

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

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <OutputType>Exe</OutputType>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <WasmMainJSPath>main.js</WasmMainJSPath>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
```

## JavaScript interop on WASM

APIs in the following example are imported from `dotnet.js`. These APIs enable you to set up named modules that can be imported into your C# code and call into methods exposed by your .NET code, including `Program.Main`.

Important

"Import" and "export" throughout this article are defined from the perspective of .NET:

* An app imports JS methods so that they can be called from .NET.
* The app exports .NET methods so that they can be called from JS.

In the following example:

* The `dotnet.js` file is used to create and start the .NET WebAssembly runtime. `dotnet.js` is generated as part of the app's build output.

  Important

  To integrate with an existing app, copy the contents of the publish output folder† to the existing app's deployment assets so that it can be served along with the rest of the app. For production deployments, publish the app with the `dotnet publish -c Release` command in a command shell and deploy the output folder's contents with the app.

  †The publish output folder is the target location of your publish profile. The default for a **Release** profile in .NET 8 or later is `bin/Release/{TARGET FRAMEWORK}/publish`, where the `{TARGET FRAMEWORK}` placeholder is the target framework (for example, `net8.0`).

* `dotnet.create()` sets up the .NET WebAssembly runtime.

- `setModuleImports` associates a name with a module of JS functions for import into .NET. The JS module contains a `dom.setInnerText` function, which accepts and element selector and time to display the current stopwatch time in the UI. The name of the module can be any string (it doesn't need to be a file name), but it must match the name used with the `JSImportAttribute` (explained later in this article). The `dom.setInnerText` function is imported into C# and called by the C# method `SetInnerText`. The `SetInnerText` method is shown later in this section.

- `exports.StopwatchSample.Reset()` calls into .NET (`StopwatchSample.Reset`) from JS. The `Reset` C# method restarts the stopwatch if it's running or resets it if it isn't running. The `Reset` method is shown later in this section.

- `exports.StopwatchSample.Toggle()` calls into .NET (`StopwatchSample.Toggle`) from JS. The `Toggle` C# method starts or stops the stopwatch depending on if it's currently running or not. The `Toggle` method is shown later in this section.

- `runMain()` runs `Program.Main`.

* `setModuleImports` associates a name with a module of JS functions for import into .NET. The JS module contains a `window.location.href` function, which returns the current page address (URL). The name of the module can be any string (it doesn't need to be a file name), but it must match the name used with the `JSImportAttribute` (explained later in this article). The `window.location.href` function is imported into C# and called by the C# method `GetHRef`. The `GetHRef` method is shown later in this section.

* `exports.MyClass.Greeting()` calls into .NET (`MyClass.Greeting`) from JS. The `Greeting` C# method returns a string that includes the result of calling the `window.location.href` function. The `Greeting` method is shown later in this section.

* `dotnet.run()` runs `Program.Main`.

JS module:

```
import { dotnet } from './_framework/dotnet.js'

const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotnet
  .withApplicationArguments("start")
  .create();

setModuleImports('main.js', {
  dom: {
    setInnerText: (selector, time) => 
      document.querySelector(selector).innerText = time
  }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);

document.getElementById('reset').addEventListener('click', e => {
  exports.StopwatchSample.Reset();
  e.preventDefault();
});

const pauseButton = document.getElementById('pause');
pauseButton.addEventListener('click', e => {
  const isRunning = exports.StopwatchSample.Toggle();
  pauseButton.innerText = isRunning ? 'Pause' : 'Start';
  e.preventDefault();
});

await runMain();
```

```
import { dotnet } from './_framework/dotnet.js'

const { setModuleImports, getAssemblyExports, getConfig } = await dotnet
  .withDiagnosticTracing(false)
  .withApplicationArgumentsFromQuery()
  .create();

setModuleImports('main.js', {
  window: {
    location: {
      href: () => globalThis.window.location.href
    }
  }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

document.getElementById('out').innerHTML = text;
await dotnet.run();
```

```
import { dotnet } from './dotnet.js'

const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);

const { setModuleImports, getAssemblyExports, getConfig } = 
  await dotnet.create();

setModuleImports("main.js", {
  window: {
    location: {
      href: () => globalThis.window.location.href
    }
  }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

document.getElementById("out").innerHTML = text;
await dotnet.run();
```

To import a JS function so it can be called from C#, use the new [JSImportAttribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsimportattribute) on a matching method signature. The first parameter to the [JSImportAttribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsimportattribute) is the name of the JS function to import and the second parameter is the name of the module.

In the following example, the `dom.setInnerText` function is called from the `main.js` module when `SetInnerText` method is called:

```
[JSImport("dom.setInnerText", "main.js")]
internal static partial void SetInnerText(string selector, string content);
```

In the following example, the `window.location.href` function is called from the `main.js` module when `GetHRef` method is called:

```
[JSImport("window.location.href", "main.js")]
internal static partial string GetHRef();
```

In the imported method signature, you can use .NET types for parameters and return values, which are marshalled automatically by the runtime. Use [JSMarshalAsAttribute\<T>](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsmarshalasattribute-1) to control how the imported method parameters are marshalled. For example, you might choose to marshal a `long` as [System.Runtime.InteropServices.JavaScript.JSType.Number](/en-us/dotnet/api/system.runtime.interopservices.javascript.jstype.number) or [System.Runtime.InteropServices.JavaScript.JSType.BigInt](/en-us/dotnet/api/system.runtime.interopservices.javascript.jstype.bigint). You can pass [Action](/en-us/dotnet/api/system.action)/[Func\<TResult>](/en-us/dotnet/api/system.func-1) callbacks as parameters, which are marshalled as callable JS functions. You can pass both JS and managed object references, and they are marshaled as proxy objects, keeping the object alive across the boundary until the proxy is garbage collected. You can also import and export asynchronous methods with a [Task](/en-us/dotnet/api/system.threading.tasks.task) result, which are marshaled as [JS promises](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise). Most of the marshalled types work in both directions, as parameters and as return values, on both imported and exported methods.

For additional type mapping information and examples, see [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop in .NET WebAssembly](./?view=aspnetcore-10.0#type-mappings).

Functions accessible on the global namespace can be imported by using the [`globalThis`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/globalThis) prefix in the function name and by using the `[JSImport]` attribute without providing a module name. In the following example, [`console.log`](https://developer.mozilla.org/docs/Web/API/console/log) is prefixed with `globalThis`. The imported function is called by the C# `Log` method, which accepts a C# string message (`message`) and marshalls the C# string to a JS [`String`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) for `console.log`:

```
[JSImport("globalThis.console.log")]
internal static partial void Log([JSMarshalAs<JSType.String>] string message);
```

To export a .NET method so it can be called from JS, use the [JSExportAttribute](/en-us/dotnet/api/system.runtime.interopservices.javascript.jsexportattribute).

In the following example, each method is exported to JS and can be called from JS functions:

* The `Toggle` method starts or stops the stopwatch depending on its running state.
* The `Reset` method restarts the stopwatch if it's running or resets it if it isn't running.
* The `IsRunning` method indicates if the stopwatch is running.

```
[JSExport]
internal static bool Toggle()
{
    if (stopwatch.IsRunning)
    {
        stopwatch.Stop();
        return false;
    }
    else
    {
        stopwatch.Start();
        return true;
    }
}

[JSExport]
internal static void Reset()
{
    if (stopwatch.IsRunning)
        stopwatch.Restart();
    else
        stopwatch.Reset();

    Render();
}

[JSExport]
internal static bool IsRunning() => stopwatch.IsRunning;
```

In the following example, the `Greeting` method returns a string that includes the result of calling the `GetHRef` method. As shown earlier, the `GetHref` C# method calls into JS for the `window.location.href` function from the `main.js` module. `window.location.href` returns the current page address (URL):

```
[JSExport]
internal static string Greeting()
{
    var text = $"Hello, World! Greetings from {GetHRef()}";
    Console.WriteLine(text);
    return text;
}
```

## Experimental workload and project templates

To demonstrate the JS interop functionality and obtain JS interop project templates, install the `wasm-experimental` workload:

```
dotnet workload install wasm-experimental
```

The `wasm-experimental` workload contains two project templates: `wasmbrowser` and `wasmconsole`. These templates are experimental at this time, which means the developer workflow for the templates is evolving. However, the .NET and JS APIs used in the templates are supported in .NET 8 and provide a foundation for using .NET on WASM from JS.

The templates can also be installed from the [`Microsoft.NET.Runtime.WebAssembly.Templates`](https://www.nuget.org/packages/Microsoft.NET.Runtime.WebAssembly.Templates) NuGet package with the following command:

```
dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates
```

### Browser app

You can create a browser app with the `wasmbrowser` template from the command line, which creates a web app that demonstrates using .NET and JS together in a browser:

```
dotnet new wasmbrowser
```

Alternatively in Visual Studio, you can create the app using the **WebAssembly Browser App** project template.

Build the app from Visual Studio or by using the .NET CLI:

```
dotnet build
```

Build and run the app from Visual Studio or by using the .NET CLI:

```
dotnet run
```

Alternatively, install and use the [`dotnet serve` command](https://github.com/natemcmaster/dotnet-serve):

```
dotnet serve -d:bin/$(Configuration)/{TARGET FRAMEWORK}/publish
```

In the preceding example, the `{TARGET FRAMEWORK}` placeholder is the [target framework moniker](/en-us/dotnet/standard/frameworks).

### Node.js console app

You can create a console app with the `wasmconsole` template, which creates an app that runs under WASM as a [Node.js](https://nodejs.org/) or [V8](https://developers.google.com/apps-script/guides/v8-runtime) console app:

```
dotnet new wasmconsole
```

Alternatively in Visual Studio, you can create the app using the **WebAssembly Console App** project template.

Build the app from Visual Studio or by using the .NET CLI:

```
dotnet build
```

Build and run the app from Visual Studio or by using the .NET CLI:

```
dotnet run
```

Alternatively, start any static file server from the publish output directory that contains the `main.mjs` file:

```
node bin/$(Configuration)/{TARGET FRAMEWORK}/{PATH}/main.mjs
```

In the preceding example, the `{TARGET FRAMEWORK}` placeholder is the [target framework moniker](/en-us/dotnet/standard/frameworks), and the `{PATH}` placeholder is the path to the `main.mjs` file.

## Related content

* [JavaScript \`\[JSImport\]\`/\`\[JSExport\]\` interop in .NET WebAssembly](./?view=aspnetcore-10.0)

* [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](../../blazor/javascript-interoperability/import-export-interop?view=aspnetcore-10.0)

* [Use .NET from any JavaScript app in .NET 7](https://devblogs.microsoft.com/dotnet/use-net-7-from-any-javascript-app-in-net-7/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-18

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/test-min-api
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/test-min-api.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Unit and integration tests in Minimal API apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Fiyaz Bin Hasan](https://github.com/fiyazbinhasan), and [Rick Anderson](https://twitter.com/RickAndMSFT)

* Sets the [content root](../?view=aspnetcore-10.0#content-root) to the SUT's project root so that static files and pages/views are found when the tests are executed.
* Provides the [WebApplicationFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) class to streamline bootstrapping the SUT with `TestServer`.

The [unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test) documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.

**Separate unit tests from integration tests into different projects**. Separating the tests:

* Helps ensure that infrastructure testing components aren't accidentally included in the unit tests.
* Allows control over which set of tests are run.

The [sample code on GitHub](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/MinApiTestsSample) provides an example of unit and integration tests on a Minimal API app.

[]()

## Unit test IResult implementation types

The following example shows how to unit test minimal route handlers that return [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) using the [xUnit](https://github.com/xunit/xunit/) testing framework. The external database is replaced with an in-memory database during testing, the implementation of the `MockDb` can be found in the [sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/fundamentals/minimal-apis/samples/MinApiTestsSample/UnitTests/Helpers/MockDb.cs).

Public [IResult](/en-us/dotnet/api/microsoft.aspnetcore.http.iresult) implementation types in the [Microsoft.AspNetCore.Http.HttpResults](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults) namespace can be used to unit test minimal route handlers when using named methods instead of lambdas.

The following code uses the [NotFound\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.notfound-1) class:

```
[Fact]
public async Task GetTodoReturnsNotFoundIfNotExists()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var notFoundResult = (NotFound) result.Result;

    Assert.NotNull(notFoundResult);
}
```

The following code uses the [Ok\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.ok-1) class:

```
[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title",
        Description = "Test description",
        IsDone = false
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var okResult = (Ok<Todo>)result.Result;

    Assert.NotNull(okResult.Value);
    Assert.Equal(1, okResult.Value.Id);
}
```

In the previous examples, the result is cast to a concrete type because the endpoint under test can return multiple types (a [NotFound\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.notfound-1) or [Ok\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.ok-1)) result. However, if the endpoint returns a single [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) type, then the result is automatically inferred to that type and no casting is required.

The following code uses the [Ok](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.ok) class, and the value's type is a collection of `Todo`:

```
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}
```

## Related content

* [Basic authentication tests](https://github.com/blowdart/idunno.Authentication/tree/dev/test/idunno.Authentication.Basic.Test) is not a .NET repository but was written by a member of the .NET team. It provides examples of basic authentication testing.
* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/minimal-apis/samples/MinApiTestsSample)
* [Authentication and authorization in Minimal APIs](security?view=aspnetcore-10.0)
* [Use port tunneling Visual Studio to debug web APIs](/en-us/connectors/custom-connectors/port-tunneling)
* [Test controller logic in ASP.NET Core](../../mvc/controllers/testing?view=aspnetcore-10.0)
* [Razor Pages unit tests in ASP.NET Core](../../test/razor-pages-tests?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-10-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/configuration/options.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Options pattern in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The options pattern uses classes to provide strongly typed access to groups of related settings. When [configuration settings](./?view=aspnetcore-10.0) are isolated by scenario into separate classes, the app adheres to two important software engineering principles:

* [Encapsulation](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#encapsulation): Classes that depend on configuration settings depend only on the configuration settings that they use.
* [Separation of Concerns](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#separation-of-concerns): Settings for different parts of the app aren't dependent or coupled to one another.

Options also provide a mechanism to validate configuration data, which is described in the [Options validation](#options-validation) section.

This article provides information on the options pattern in ASP.NET Core. For information on using the options pattern in console apps, see [Options pattern in .NET](/en-us/dotnet/core/extensions/options).

The examples in this article rely on a general understanding of injecting services into classes. For more information, see [Dependency injection in ASP.NET Core](../dependency-injection?view=aspnetcore-10.0). Examples are based on [Blazor's Razor components](../../blazor/?view=aspnetcore-10.0). To see Razor Pages examples, see the [7.0 version of this article](?view=aspnetcore-7.0\&preserve-view=true). The examples in the .NET 8 or later versions of this article use [primary constructors](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) ([Primary constructors (C# Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors)) and [nullable reference types (NRTs) with .NET compiler null-state static analysis](../../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis).

## How to use the options pattern

Consider the following JSON configuration data from an app settings file (for example, `appsettings.json`), which includes related data for an employee's name and title in an organization's position:

```
"Position": {
  "Name": "Joe Smith",
  "Title": "Editor"
}
```

The following `PositionOptions` options class:

* Is a [POCO](https://wikipedia.org/wiki/Plain_Old_CLR_Object), a simple .NET class with properties. An options class must not be an abstract class.
* Has public read-write properties that match corresponding entries in the configuration data.
* Does ***not*** have its field (`Position`) bound. The `Position` field is used to avoid hardcoding the string `"Position"` in the app when binding the class to a configuration provider.

`PositionOptions.cs`:

```
public class PositionOptions
{
    public const string Position = "Position";

    public string? Name { get; set; }
    public string? Title { get; set; }
}
```

The following example:

* Calls [ConfigurationBinder.Bind](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.bind) to bind the `PositionOptions` class to the `Position` section.
* Displays the `Position` configuration data.

`BasicOptions.razor`:

```
@page "/basic-options"
@inject IConfiguration Config

Name: @positionOptions?.Name<br>
Title: @positionOptions?.Title

@code {
    private PositionOptions? positionOptions;

    protected override void OnInitialized()
    {
        positionOptions = new PositionOptions();
        Config.GetSection(PositionOptions.Position).Bind(positionOptions);
    }
}
```

`BasicOptions.cshtml`:

```
@page
@model RazorPagesSample.Pages.BasicOptionsModel
@{
}
```

`BasicOptions.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesSample.Pages;

public class BasicOptionsModel : PageModel
{
    private readonly IConfiguration _config;

    public BasicOptionsModel(IConfiguration config)
    {
        _config = config;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        _config.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content(
            $"Name: {positionOptions.Name}\n" +
            $"Title: {positionOptions.Title}");
    }
}
```

Output:

> Name: Joe Smith\
> Title: Editor

After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the app.

[Bind](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.bind) allows an abstract class to be instantiated. Consider the following example that uses the abstract class `AbstractClassWithName`.

`NameTitleOptions.cs`:

```
public abstract class AbstractClassWithName
{
    public abstract string? Name { get; set; }
}

public class NameTitleOptions(int age) : AbstractClassWithName
{
    public const string NameTitle = "NameTitle";

    public override string? Name { get; set; }
    public string? Title { get; set; }
    public int Age { get; set; } = age;
}
```

JSON configuration:

```
"NameTitle": {
  "Name": "Sally Jones",
  "Title": "Writer"
}
```

The following example displays the `NameTitleOptions` configuration values.

`AbstractClassOptions.razor`:

```
@page "/abstract-class-options"
@inject IConfiguration Config

Name: @nameTitleOptions?.Name<br>
Title: @nameTitleOptions?.Title<br>
Age: @nameTitleOptions?.Age

@code {
    private NameTitleOptions? nameTitleOptions;

    protected override void OnInitialized()
    {
        nameTitleOptions = new NameTitleOptions(22);
        Config.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);
    }
}
```

`AbstractClassOptions.cshtml`:

```
@page
@model RazorPagesSample.Pages.AbstractClassOptionsModel
@{
}
```

`AbstractClassOptions.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesSample.Pages;

public class AbstractClassOptionsModel : PageModel
{
    private readonly IConfiguration _config;

    public AbstractClassOptionsModel(IConfiguration config)
    {
        _config = config;
    }

    public ContentResult OnGet()
    {
        var nameTitleOptions = new NameTitleOptions(22);
        _config.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);

        return Content(
            $"Name: {nameTitleOptions.Name}\n" +
            $"Title: {nameTitleOptions.Title}\n" +
            $"Age: {nameTitleOptions.Age}");
    }
}
```

Output:

> Name: Sally Jones\
> Title: Writer\
> Age: 22

[ConfigurationBinder.Get](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.get) binds and returns the specified type. The following example shows how to use [Get](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.get) with the `PositionOptions` class.

`GetOptions.razor`:

```
@page "/get-options"
@inject IConfiguration Config

Name: @positionOptions?.Name<br>
Title: @positionOptions?.Title

@code {
    private PositionOptions? positionOptions;

    protected override void OnInitialized() =>
        positionOptions = Config.GetSection(PositionOptions.Position)
            .Get<PositionOptions>();
}
```

`GetOptions.cshtml`:

```
@page
@model RazorPagesSample.Pages.GetOptionsModel
@{
}
```

`GetOptions.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesSample.Pages;

public class GetOptionsModel : PageModel
{
    private readonly IConfiguration _config;

    public GetOptionsModel(IConfiguration config)
    {
        _config = config;
    }

    public ContentResult OnGet()
    {
        var positionOptions = _config.GetSection(PositionOptions.Position)
            .Get<PositionOptions>();

        return Content(
            $"Name: {positionOptions?.Name}\n" +
            $"Title: {positionOptions?.Title}");
    }
}
```

Output:

> Name: Joe Smith\
> Title: Editor

After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the app.

Summary of the differences between [ConfigurationBinder.Bind](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.bind) and [ConfigurationBinder.Get](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.get):

* [Get](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.get) is usually more convenient than using [Bind](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.bind) because [Get](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.get) creates and returns a new instance of the object, while [Bind](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.bind) populates properties of an existing object instance that's typically established by another line of code.
* [Bind](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.bind) allows the concretion of an abstract class, while [Get](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.get) is only able to create a non-abstract instance of the options type.

## Bind options to the dependency injection service container

In the following example, `PositionOptions` is added to the service container with [Configure](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsconfigurationservicecollectionextensions.configure) and bound to configuration.

JSON configuration:

```
"Position": {
  "Name": "Joe Smith",
  "Title": "Editor"
}
```

`PositionOptions.cs`:

```
public class PositionOptions
{
    public const string Position = "Position";

    public string? Name { get; set; }
    public string? Title { get; set; }
}
```

Where services are registered for dependency injection in the app's `Program` file:

```
builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
```

```
services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
```

The following example reads the position options.

`DIOptions.razor`:

```
@page "/di-options"
@using Microsoft.Extensions.Options
@inject IOptions<PositionOptions> Options

Name: @Options.Value.Name<br>
Title: @Options.Value.Title
```

`DIOptions.cshtml`:

```
@page
@model RazorPagesSample.Pages.DIOptionsModel
@{
}
```

`DIOptions.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class DIOptionsModel : PageModel
{
    private readonly IOptions<PositionOptions> _options;

    public DIOptionsModel(IOptions<PositionOptions> options)
    {
        _options = options;
    }

    public ContentResult OnGet()
    {
        return Content(
            $"Name: {_options.Value.Name}\n" +
            $"Title: {_options.Value.Title}");
    }
}
```

Output:

> Name: Joe Smith\
> Title: Editor

For the preceding code, changes to the JSON configuration in the app settings file after the app has started are ***not*** read. To read changes after the app has started, use [`IOptionsSnapshot`](#use-ioptionssnapshot-to-read-updated-data).

## Options interfaces

[IOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptions-1):

* Does ***not*** support:

  * Reading of configuration data after the app has started.
  * [Named options](#specify-a-custom-key-name-for-a-configuration-property-using-configurationkeyname).

* Is registered as a [singleton service](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes#singleton) and can be injected into any [service lifetime](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes).

[IOptionsSnapshot\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionssnapshot-1):

* Covered later in this article in the [Use `IOptionsSnapshot` to read updated data](#use-ioptionssnapshot-to-read-updated-data) section.
* Is useful in scenarios where options should be recomputed on every request.
* Is registered as a [scoped service](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes#scoped), so it can't be injected into a singleton service.
* Supports [named options](#specify-a-custom-key-name-for-a-configuration-property-using-configurationkeyname).

[IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1):

* Covered later in this article in the [Use `IOptionsMonitor` to read updated data](#use-ioptionsmonitor-to-read-updated-data) section.

* Is used to retrieve options and manage options notifications for `TOptions` instances.

* Is registered as a [singleton service](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes#singleton) and can be injected into any [service lifetime](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes).

* Supports:

  * Change notifications.
  * [Named options](#specify-a-custom-key-name-for-a-configuration-property-using-configurationkeyname).
  * Reloadable configuration.
  * Selective options invalidation ([IOptionsMonitorCache\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitorcache-1)).

[Post-configuration scenarios](#options-post-configuration) enable setting or changing options after all [IConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigureoptions-1) configuration occurs.

[IOptionsFactory\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsfactory-1) is responsible for creating new options instances. It has a single [Create](/en-us/dotnet/api/microsoft.extensions.options.ioptionsfactory-1.create) method. The default implementation takes all registered [IConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigureoptions-1) and [IPostConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ipostconfigureoptions-1) and runs all the configurations first, followed by the post-configuration. It distinguishes between [IConfigureNamedOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigurenamedoptions-1) and [IConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigureoptions-1) and only calls the appropriate interface.

## Use `IOptionsSnapshot` to read updated data

Using [IOptionsSnapshot\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionssnapshot-1):

* Options are computed once per request when accessed and cached for the lifetime of the request.
* May incur a significant performance penalty because it's a [scoped service](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes#scoped) and is recomputed per request. For more information, see [`IOptionsSnapshot` is very slow (`dotnet/runtime` #53793)](https://github.com/dotnet/runtime/issues/53793) and [Improve the performance of configuration binding (`dotnet/runtime` #36130)](https://github.com/dotnet/runtime/issues/36130).
* Changes to the configuration are read after the app starts when using configuration providers that support reading updated configuration values.

The difference between [`IOptionsMonitor`](#use-ioptionsmonitor-to-read-updated-data) and [IOptionsSnapshot\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionssnapshot-1) is that:

* [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) is a [singleton service](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes#singleton) that retrieves current option values at any time, which is especially useful in singleton dependencies.
* [IOptionsSnapshot\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionssnapshot-1) is a [scoped service](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes#scoped) and provides a snapshot of the options at the time the `IOptionsSnapshot<T>` object is constructed. Options snapshots are designed for use with transient and scoped dependencies.

The ASP.NET Core runtime uses [OptionsCache\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.optionscache-1) to cache the options instance after it's created.

JSON configuration:

```
"Position": {
  "Name": "Joe Smith",
  "Title": "Editor"
}
```

`PositionOptions.cs`:

```
public class PositionOptions
{
    public const string Position = "Position";

    public string? Name { get; set; }
    public string? Title { get; set; }
}
```

The following example uses [IOptionsSnapshot\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionssnapshot-1).

`SnapshotOptions.razor`:

```
@page "/snapshot-options"
@using Microsoft.Extensions.Options
@inject IOptionsSnapshot<PositionOptions> Options

Name: @Options.Value.Name<br>
Title: @Options.Value.Title
```

`SnapshotOptions.cshtml`:

```
@page
@model RazorPagesSample.Pages.SnapshotOptionsModel
@{
}
```

`SnapshotOptions.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class SnapshotOptionsModel : PageModel
{
    private readonly IOptionsSnapshot<PositionOptions> _options;

    public SnapshotOptionsModel(IOptionsSnapshot<PositionOptions> options)
    {
        _options = options;
    }

    public ContentResult OnGet()
    {
        return Content(
            $"Name: {_options.Value.Name}\n" +
            $"Title: {_options.Value.Title}");
    }
}
```

Where services are registered for dependency injection, the following example registers a configuration instance which `PositionOptions` binds against:

```
builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
```

```
services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
```

Output:

> Name: Joe Smith\
> Title: Editor

After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the app.

## Use `IOptionsMonitor` to read updated data

[IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) is used to retrieve options and manage options notifications for `TOptions` instances.

The difference between [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) and [`IOptionsSnapshot`](#use-ioptionssnapshot-to-read-updated-data) is that:

* [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) is a [singleton service](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes#singleton) that retrieves current option values at any time, which is especially useful in singleton dependencies.
* [IOptionsSnapshot\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionssnapshot-1) is a [scoped service](/en-us/dotnet/core/extensions/dependency-injection/service-lifetimes#scoped) and provides a snapshot of the options at the time the `IOptionsSnapshot<T>` object is constructed. Options snapshots are designed for use with transient and scoped dependencies.

[IOptionsMonitorCache\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitorcache-1) is used by [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) to cache `TOptions` instances. [IOptionsMonitorCache\<TOptions>.TryRemove](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitorcache-1.tryremove) invalidates options instances in the monitor so that the value is recomputed. Values can be manually introduced with [IOptionsMonitorCache\<TOptions>.TryAdd](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitorcache-1.tryadd). The [Clear](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitorcache-1.clear) method is used when all named instances should be recreated on demand.

JSON configuration:

```
"Position": {
  "Name": "Joe Smith",
  "Title": "Editor"
}
```

`PositionOptions.cs`:

```
public class PositionOptions
{
    public const string Position = "Position";

    public string? Name { get; set; }
    public string? Title { get; set; }
}
```

Where services are registered for dependency injection, the following example registers a configuration instance which `PositionOptions` binds against:

```
builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
```

```
services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
```

The following example uses [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1).

`MonitorOptions.razor`:

```
@page "/monitor-options"
@using Microsoft.Extensions.Options
@inject IOptionsMonitor<PositionOptions> Options

Name: @Options.CurrentValue.Name<br>
Title: @Options.CurrentValue.Title
```

`MonitorOptions.cshtml`:

```
@page
@model RazorPagesSample.Pages.MonitorOptionsModel
@{
}
```

`MonitorOptions.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class MonitorOptionsModel : PageModel
{
    private readonly IOptionsMonitor<PositionOptions> _options;

    public MonitorOptionsModel(IOptionsMonitor<PositionOptions> options)
    {
        _options = options;
    }

    public ContentResult OnGet()
    {
        return Content(
            $"Name: {_options.CurrentValue.Name}\n" +
            $"Title: {_options.CurrentValue.Title}");
    }
}
```

Output:

> Name: Joe Smith\
> Title: Editor

After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the app.

## Specify a custom key name for a configuration property using `ConfigurationKeyName`

By default, the property names of the options class are used as the key name in the configuration source. If the property name is `Title`, the key name in the configuration is `Title` as well.

When the names differentiate, you can use the [`[ConfigurationKeyName]` attribute](/en-us/dotnet/api/microsoft.extensions.configuration.configurationkeynameattribute) to specify the key name in the configuration source. Using this technique, you can map a property in the configuration to one in your code with a different name. This is useful when the property name in the configuration source isn't a valid C# identifier or when you want to use a different name in your code.

For example, consider the following options class.

`PositionKeyName.cs`:

```
public class PositionKeyName
{
    public const string Position = "PositionKeyName";

    [ConfigurationKeyName("PositionName")]
    public string? Name { get; set; }

    [ConfigurationKeyName("PositionTitle")]
    public string? Title { get; set; }
}
```

The `Name` and `Title` class properties are bound to the `PositionName` and `PositionTitle` from the following JSON configuration:

```
"PositionKeyName": {
  "PositionName": "Carlos Diego",
  "PositionTitle": "Director"
}
```

`PositionKeyNameOptions.razor`:

```
@page "/position-key-name-options"
@inject IConfiguration Config

Name: @positionOptions?.Name<br>
Title: @positionOptions?.Title

@code {
    private PositionKeyName? positionOptions;

    protected override void OnInitialized() =>
        positionOptions = Config.GetSection(PositionKeyName.Position)
            .Get<PositionKeyName>();
}
```

`PositionKeyName.cshtml`:

```
@page
@model RazorPagesSample.Pages.PositionKeyNameModel
@{
}
```

`PositionKeyName.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesSample.Pages;

public class PositionKeyNameModel : PageModel
{
    private readonly IConfiguration _config;

    public PositionKeyNameModel(IConfiguration config)
    {
        _config = config;
    }

    public ContentResult OnGet()
    {
        var positionOptions = _config.GetSection(PositionKeyName.Position)
            .Get<PositionKeyName>();

        return Content(
            $"Name: {positionOptions?.Name}\n" +
            $"Title: {positionOptions?.Title}");
    }
}
```

Output:

> Name: Carlos Diego\
> Title: Director

After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the app.

## Named options support using `IConfigureNamedOptions`

Named options:

* Are useful when multiple configuration sections bind to the same properties.
* Are case sensitive.

Consider the following JSON configuration:

```
"TopItem": {
  "Month": {
    "Name": "Green Widget",
    "Model": "GW46"
  },
  "Year": {
    "Name": "Orange Gadget",
    "Model": "OG35"
  }
}
```

Rather than creating two classes to bind `TopItem:Month` and `TopItem:Year`, the following class is used for each section.

`TopItemSettings.cs`:

```
public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string? Name { get; set; }
    public string? Model { get; set; }
}
```

Where services are registered for dependency injection, the following example configures the named options:

```
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));
```

```
services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));
```

The following example displays the named options:

`NamedOptions.razor`:

```
@page "/named-options"
@using Microsoft.Extensions.Options
@inject IOptionsSnapshot<TopItemSettings> Options

Month: Name: @monthTopItem?.Name Model: @monthTopItem?.Model<br>
Year: Name: @yearTopItem?.Name Model: @yearTopItem?.Model

@code {
    private TopItemSettings? monthTopItem;
    private TopItemSettings? yearTopItem;

    protected override void OnInitialized()
    {
        monthTopItem = Options.Get(TopItemSettings.Month);
        yearTopItem = Options.Get(TopItemSettings.Year);
    }
}
```

`NamedOptions.cshtml`:

```
@page
@model RazorPagesSample.Pages.NamedOptionsModel
@{
}
```

`NamedOptions.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class NamedOptionsModel : PageModel
{
    private readonly IOptionsSnapshot<TopItemSettings> _options;

    public NamedOptionsModel(IOptionsSnapshot<TopItemSettings> options)
    {
        _options = options;
    }

    public ContentResult OnGet()
    {
        var monthTopItem = _options.Get(TopItemSettings.Month);
        var yearTopItem = _options.Get(TopItemSettings.Year);

        return Content(
            $"Month:Name {monthTopItem.Name}\n" +
            $"Month:Model {monthTopItem.Model}\n" +
            $"Year:Name {yearTopItem.Name}\n" +
            $"Year:Model {yearTopItem.Model}");
    }
}
```

All options are named instances. [IConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigureoptions-1) instances are treated as targeting the `Options.DefaultName` instance, which is `string.Empty`. [IConfigureNamedOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigurenamedoptions-1) also implements [IConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigureoptions-1). The default implementation of the [IOptionsFactory\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsfactory-1) has logic to use each appropriately. The `null` named option is used to target all of the named instances instead of a specific named instance. [ConfigureAll](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsservicecollectionextensions.configureall) and [PostConfigureAll](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsservicecollectionextensions.postconfigureall) use this convention.

Guidance on post-configuring named options is provided in the [Options post-configuration](#options-post-configuration) section.

## `OptionsBuilder` API

[OptionsBuilder\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1) is used to configure `TOptions` instances. `OptionsBuilder` streamlines creating named options as it's only a single parameter to the initial [`AddOptions<TOptions>(string optionsName)`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsservicecollectionextensions.addoptions) call instead of appearing in all of the subsequent calls. Options validation and the [IConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigureoptions-1) overloads that accept service dependencies are only available via `OptionsBuilder` (see [Use DI services to configure options](#use-di-services-to-configure-options)).

[OptionsBuilder\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1) is demonstrated in the [Options validation](#options-validation) section.

For information adding a custom repository, see [Get started with the Data Protection APIs in ASP.NET Core](../../security/data-protection/using-data-protection?view=aspnetcore-10.0#add-opt).

## Use DI services to configure options

Services can be accessed from dependency injection while configuring options in two ways:

* [Configuration delegate](#configuration-delegate-approach)
* [Configuration options service](#configuration-options-service-approach)

### Configuration delegate approach

Where services are registered for dependency injection, pass a configuration delegate to [Configure](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1.configure) on [OptionsBuilder\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1). `OptionsBuilder<TOptions>` provides overloads of [Configure](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1.configure) that allow use of up to five services to configure options:

```
builder.Services.AddOptions<PositionOptions>("optionalName")
    .Configure<Service1, Service2, Service3, Service4, Service5>(
        (o, s, s2, s3, s4, s5) => 
            o.Property = DoSomethingWith(s, s2, s3, s4, s5));
```

```
services.AddOptions<PositionOptions>("optionalName")
    .Configure<Service1, Service2, Service3, Service4, Service5>(
        (o, s, s2, s3, s4, s5) => 
            o.Property = DoSomethingWith(s, s2, s3, s4, s5));
```

### Configuration options service approach

Create a type that implements [IConfigureOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigureoptions-1) or [IConfigureNamedOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigurenamedoptions-1) and register the type as a service.

We recommend passing a configuration delegate to [Configure](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1.configure), since creating a service is more complex. Creating a type is equivalent to what the framework does when calling [Configure](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1.configure). Calling [Configure](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1.configure) registers a transient generic [IConfigureNamedOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iconfigurenamedoptions-1), which has a constructor that accepts the generic service types specified.

## Options validation

Options validation enables the validation of option values.

Consider the following JSON configuration:

```
"KeyOptions": {
  "Key1": "Key One",
  "Key2": 10,
  "Key3": 32
}
```

The following class is used to bind to the `"KeyOptions"` configuration section and applies two data annotations rules, which include a regular expression and range requirement.

`KeyOptions.cs`:

```
public class KeyOptions
{
    public const string Key = "KeyOptions";

    [RegularExpression(@"^[a-zA-Z\s]{1,40}$")]
    public string? Key1 { get; set; }

    [Range(0, 1000, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}
```

Where services are registered for dependency injection, the following example:

* Calls [AddOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsservicecollectionextensions.addoptions) to get an [OptionsBuilder\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1) that binds to the `KeyOptions` class.
* Calls [ValidateDataAnnotations](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsbuilderdataannotationsextensions.validatedataannotations) to enable validation.

```
builder.Services.AddOptions<KeyOptions>()
    .Bind(builder.Configuration.GetSection(KeyOptions.Key))
    .ValidateDataAnnotations();
```

```
services.AddOptions<KeyOptions>()
    .Bind(builder.Configuration.GetSection(KeyOptions.Key))
    .ValidateDataAnnotations();
```

The [ValidateDataAnnotations](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsbuilderdataannotationsextensions.validatedataannotations) extension method is defined in the [`Microsoft.Extensions.Options.DataAnnotations` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Options.DataAnnotations). For web apps that use the `Microsoft.NET.Sdk.Web` SDK, this package is referenced implicitly from the [shared framework](../metapackage-app?view=aspnetcore-10.0).

Where services are registered for dependency injection, the following example applies a more complex validation rule using a delegate:

```
builder.Services.AddOptions<KeyOptions>()
        .Bind(builder.Configuration.GetSection(KeyOptions.Key))
        .ValidateDataAnnotations()
    .Validate(options =>
    {
        return options.Key3 > options.Key2;
    }, "Key3 must be > than Key2");
```

```
services.AddOptions<KeyOptions>()
        .Bind(builder.Configuration.GetSection(KeyOptions.Key))
        .ValidateDataAnnotations()
    .Validate(options =>
    {
        return options.Key3 > options.Key2;
    }, "Key3 must be > than Key2");
```

The following example demonstrates how to log options validation exceptions and display an [OptionsValidationException.Message](/en-us/dotnet/api/microsoft.extensions.options.optionsvalidationexception.message).

Note

For demonstration purposes, the following example uses a [MarkupString](/en-us/dotnet/api/microsoft.aspnetcore.components.markupstring) to format raw HTML. Rendering raw HTML constructed from any untrusted source is a **security risk** and should **always** be avoided. For more information, see [ASP.NET Core Razor components](../../blazor/components/?view=aspnetcore-10.0#raw-html).

`OptionsValidation1.razor`:

```
@page "/options-validation-1"
@inject IOptionsSnapshot<KeyOptions> Options
@inject ILogger<OptionsValidation1> Logger

@if (message is not null)
{
    @((MarkupString)message)
}

@code {
    private string? message;

    protected override void OnInitialized()
    {
        try
        {
            var keyOptions = Options.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                Logger.LogError(failure);
            }
        }

        try
        {
            message = 
                $"Key1: {Options.Value.Key1}<br>" +
                $"Key2: {Options.Value.Key2}<br>" +
                $"Key3: {Options.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            message = optValEx.Message;
        }
    }
}
```

The following example demonstrates how to log options validation exceptions in the page model's constructor and display an [OptionsValidationException.Message](/en-us/dotnet/api/microsoft.extensions.options.optionsvalidationexception.message) in the page's `OnGet` method.

`OptionsValidation1.cshtml`:

```
@page
@model RazorPagesSample.Pages.OptionsValidation1Model
@{
}
```

`OptionsValidation1.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class OptionsValidation1Model : PageModel
{
    private readonly IOptionsSnapshot<KeyOptions>? _options;

    public OptionsValidation1Model(IOptionsSnapshot<KeyOptions> options,
        ILogger<OptionsValidation1Model> logger)
    {
        _options = options;

        try
        {
            var keyOptions = _options?.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                logger?.LogError("Validation: {Failure}", failure);
            }
        }
    }

    public ContentResult OnGet()
    {
        string message;

        try
        {
            message =
                $"Key1: {_options?.Value.Key1}\n" +
                $"Key2: {_options?.Value.Key2}\n" +
                $"Key3: {_options?.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }

        return Content(message);
    }
}
```

The preceding code displays the configuration values or validation errors:

* Use the code with the preceding app settings configuration to demonstrate no validation errors.
* Change the configuration in the app settings file in one or more ways that violate the data annotations rules. Reload the options validation page to see the rule violations.

In the following example, the page content indicates that the value of `Key2` is out of range if the value of `Key2` in the app settings file is changed to a value below zero or over 1,000:

> DataAnnotation validation failed for 'KeyOptions' members: 'Key2' with the error: 'Value for Key2 must be between 0 and 1000.'.

## Validate options in a dedicated class with `IValidateOptions<TOptions>`

Implement [IValidateOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ivalidateoptions-1) to validate options without the need to maintain validation rules with data annotations or in the app's `Program` file.

In the following example, the data annotations rules and options delegate validation of the preceding examples is moved to a validation class. The Options model class (`KeyOptions2`) doesn't contain data annotations.

Consider the following JSON configuration:

```
"KeyOptions": {
  "Key1": "Key One",
  "Key2": 10,
  "Key3": 32
}
```

`KeyOptions2.cs`:

```
public class KeyOptions2
{
    public const string Key = "KeyOptions";

    public string? Key1 { get; set; }
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}
```

```
public class KeyOptionsValidation : IValidateOptions<KeyOptions2>
{
    public ValidateOptionsResult Validate(string? name, KeyOptions2 options)
    {
        if (options == null)
        {
            return ValidateOptionsResult.Fail("KeyOptions not found.");
        }

        StringBuilder? validationResult = new();
        var rx = new Regex(@"^[a-zA-Z\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            validationResult.Append($"{options.Key1} doesn't match RegEx<br>");
        }

        if (options.Key2 < 0 || options.Key2 > 1000)
        {
            validationResult.Append($"{options.Key2} doesn't match Range 0 - 1000<br>");
        }

        if (options.Key3 < options.Key2)
        {
            validationResult.Append("Key3 must be > than Key2<br>");
        }

        if (validationResult.Length > 0)
        {
            return ValidateOptionsResult.Fail(validationResult.ToString());
        }

        return ValidateOptionsResult.Success;
    }
}
```

```
public class KeyOptionsValidation : IValidateOptions<KeyOptions2>
{
    public ValidateOptionsResult Validate(string? name, KeyOptions2 options)
    {
        if (options == null)
        {
            return ValidateOptionsResult.Fail("KeyOptions not found");
        }

        StringBuilder? validationResult = new();
        var rx = new Regex(@"^[a-zA-Z\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            validationResult.Append($"{options.Key1} doesn't match RegEx\n");
        }

        if (options.Key2 < 0 || options.Key2 > 1000)
        {
            validationResult.Append($"{options.Key2} doesn't match Range 0 - 1000\n");
        }

        if (options.Key3 < options.Key2)
        {
            validationResult.Append("Key3 must be > than Key2\n");
        }

        if (validationResult.Length > 0)
        {
            return ValidateOptionsResult.Fail(validationResult.ToString());
        }

        return ValidateOptionsResult.Success;
    }
}
```

Where services are registered for dependency injection and using the preceding code, validation is enabled in `Program.cs` with the following example:

```
builder.Services.Configure<KeyOptions2>(
    builder.Configuration.GetSection(KeyOptions2.Key));

builder.Services.AddSingleton<IValidateOptions<KeyOptions2>, 
    KeyOptionsValidation>();
```

```
services.Configure<KeyOptions>(
    builder.Configuration.GetSection(KeyOptions2.Key));

services.AddSingleton<IValidateOptions<KeyOptions2>, KeyOptionsValidation>();
```

The following example demonstrates how to log options validation exceptions and display an [OptionsValidationException.Message](/en-us/dotnet/api/microsoft.extensions.options.optionsvalidationexception.message).

`OptionsValidation2.razor`:

```
@page "/options-validation-2"
@inject IOptionsSnapshot<KeyOptions2> Options
@inject ILogger<OptionsValidation2> Logger

@if (message is not null)
{
    @((MarkupString)message)
}

@code {
    private string? message;

    protected override void OnInitialized()
    {
        try
        {
            var keyOptions = Options.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                Logger.LogError(failure);
            }
        }

        try
        {
            message = 
                $"Key1: {Options.Value.Key1}<br>" +
                $"Key2: {Options.Value.Key2}<br>" +
                $"Key3: {Options.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            message = optValEx.Message;
        }
    }
}
```

The following example demonstrates how to log options validation exceptions in the page model's constructor and display an [OptionsValidationException.Message](/en-us/dotnet/api/microsoft.extensions.options.optionsvalidationexception.message) in the page's `OnGet` method.

`OptionsValidation2.cshtml`:

```
@page
@model RazorPagesSample.Pages.OptionsValidation2Model
@{
}
```

`OptionsValidation2.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class OptionsValidation2Model : PageModel
{
    private readonly IOptionsSnapshot<KeyOptions2>? _options;

    public OptionsValidation2Model(IOptionsSnapshot<KeyOptions2> options,
        ILogger<OptionsValidation2Model> logger)
    {
        _options = options;

        try
        {
            var keyOptions = _options?.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                logger?.LogError("Validation: {Failure}", failure);
            }
        }
    }

    public ContentResult OnGet()
    {
        string message;

        try
        {
            message =
                $"Key1: {_options?.Value.Key1}\n" +
                $"Key2: {_options?.Value.Key2}\n" +
                $"Key3: {_options?.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }

        return Content(message);
    }
}
```

## Class-level validation with `IValidatableObject`

Options validation supports [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) to perform class-level validation of a class within a class:

* Implement the [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) interface and its [Validate](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject.validate) method within the class.
* Call [ValidateDataAnnotations](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsbuilderdataannotationsextensions.validatedataannotations) in the `Program` file.

### Run options validation when the app starts with `ValidateOnStart`

Options validation runs the first time a `TOption` instance is created, which is when the first access to [`IOptionsSnapshot<TOptions>.Value`](/en-us/dotnet/api/microsoft.extensions.options.ioptionssnapshot-1) occurs in a request pipeline or when [`IOptionsMonitor<TOptions>.Get(string)`](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) is called. Each time options are reloaded, validation runs again.

To run options validation when the app starts, call [ValidateOnStart](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsbuilderextensions.validateonstart) in the `Program` file where services are registered for dependency injection:

```
builder.Services.AddOptions<KeyOptions>()
    .Bind(builder.Configuration.GetSection(KeyOptions.Key))
    .ValidateDataAnnotations()
    .ValidateOnStart();
```

### Asynchronous options validation

In ASP.NET Core 11 or later, options validation supports asynchronous validation during app startup. Use asynchronous validation for rules that require I/O, such as checking whether a configured endpoint or resource is available. Call [ValidateOnStart](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsbuilderextensions.validateonstart) so asynchronous validators run during [StartAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihost.startasync) before the app starts.

Register an asynchronous validation delegate with an overload of [Validate](/en-us/dotnet/api/microsoft.extensions.options.optionsbuilder-1.validate) that accepts a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) and returns `Task<bool>`:

```
builder.Services.AddOptions<RemoteServiceOptions>()
    .Bind(builder.Configuration.GetSection(RemoteServiceOptions.SectionName))
    .Validate<IEndpointHealthCheck>(
        async (options, healthCheck, cancellationToken) =>
            await healthCheck.IsAvailableAsync(options.Endpoint, cancellationToken),
        "The configured endpoint isn't available.")
    .ValidateOnStart();
```

The asynchronous `Validate` overloads support up to five service dependencies from dependency injection, the same as the synchronous overloads.

To move asynchronous validation logic into a dedicated class, implement [IAsyncValidateOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iasyncvalidateoptions-1). `IAsyncValidateOptions<TOptions>` inherits from [IValidateOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ivalidateoptions-1), so the class must implement both the synchronous `Validate` method and the asynchronous `ValidateAsync` method. Because the validation rule requires asynchronous I/O, the synchronous `Validate` method can't perform the check without blocking on the async call (sync-over-async). The following validator handles only the default-named options instance, returns `ValidateOptionsResult.Skip` for other names, and fails explicitly for matching options when synchronous validation is requested:

```
public sealed class RemoteServiceOptionsValidator(
    IEndpointHealthCheck healthCheck) : IAsyncValidateOptions<RemoteServiceOptions>
{
    public ValidateOptionsResult Validate(string? name, RemoteServiceOptions options) =>
        name is not null && name != Options.DefaultName
            ? ValidateOptionsResult.Skip
            : ValidateOptionsResult.Fail(
                $"Asynchronous validation is required for {nameof(RemoteServiceOptions)}.");

    public async Task<ValidateOptionsResult> ValidateAsync(
        string? name, RemoteServiceOptions options, CancellationToken cancellationToken = default)
    {
        if (name is not null && name != Options.DefaultName)
        {
            return ValidateOptionsResult.Skip;
        }
        return await healthCheck.IsAvailableAsync(options.Endpoint, cancellationToken)
            ? ValidateOptionsResult.Success
            : ValidateOptionsResult.Fail("The configured endpoint isn't available.");
    }
}
```

Register the validator as an `IValidateOptions<TOptions>` service, and call `ValidateOnStart` for the options:

```
builder.Services.AddOptions<RemoteServiceOptions>()
    .Bind(builder.Configuration.GetSection(RemoteServiceOptions.SectionName))
    .ValidateOnStart();

builder.Services.AddSingleton<IValidateOptions<RemoteServiceOptions>,
    RemoteServiceOptionsValidator>();
```

#### Asynchronous validation with data annotations

In ASP.NET Core 11 or later, [ValidateDataAnnotations](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsbuilderdataannotationsextensions.validatedataannotations) registers [DataAnnotationValidateOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.dataannotationvalidateoptions-1), which implements [IAsyncValidateOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.iasyncvalidateoptions-1) and [IValidateOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ivalidateoptions-1). When `ValidateDataAnnotations` is combined with `ValidateOnStart`, data annotations validation participates in startup validation.

The asynchronous path uses [TryValidateObjectAsync](/en-us/dotnet/api/system.componentmodel.dataannotations.validator.tryvalidateobjectasync), which evaluates synchronous validation attributes before [AsyncValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.asyncvalidationattribute) derived attributes. It calls [ValidateAsync](/en-us/dotnet/api/system.componentmodel.dataannotations.iasyncvalidatableobject.validateasync) when an options type implements [IAsyncValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.iasyncvalidatableobject). Otherwise, it falls back to the synchronous [class-level validation with `IValidatableObject`](#class-level-validation-with-ivalidatableobject).

`TryValidateObjectAsync` validates one object and doesn't recursively validate its property values. The startup cancellation token is passed through to the asynchronous validation rules.

#### Source-generated asynchronous validation

The [OptionsValidatorAttribute](/en-us/dotnet/api/microsoft.extensions.options.optionsvalidatorattribute) source generator emits an asynchronous `ValidateAsync` method, in addition to the synchronous `Validate` method, when the partial validator type implements `IAsyncValidateOptions<TOptions>`:

```
[OptionsValidator]
public partial class GeneratedRemoteServiceOptionsValidator :
    IAsyncValidateOptions<RemoteServiceOptions>
{
}
```

The generated `ValidateAsync` method evaluates asynchronous data annotations validation rules, including [AsyncValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.asyncvalidationattribute) derived attributes and [ValidateAsync](/en-us/dotnet/api/system.componentmodel.dataannotations.iasyncvalidatableobject.validateasync) for validated types that implement [IAsyncValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.iasyncvalidatableobject). A matching hand-written `Validate` or `ValidateAsync` method produces diagnostic `SYSLIB1205` or `SYSLIB1219`, respectively.

Register the generated validator as an `IValidateOptions<TOptions>` service:

```
builder.Services.AddSingleton<IValidateOptions<RemoteServiceOptions>,
    GeneratedRemoteServiceOptionsValidator>();
```

#### Custom asynchronous startup validators

To run custom asynchronous startup validation that isn't tied to a specific options type, implement [IAsyncStartupValidator](/en-us/dotnet/api/microsoft.extensions.options.iasyncstartupvalidator):

```
public sealed class CustomStartupValidator : IAsyncStartupValidator
{
    public async Task ValidateAsync(CancellationToken cancellationToken = default)
    {
        if (!await IsStartupDependencyAvailableAsync(cancellationToken))
        {
            throw new InvalidOperationException(
                "The startup dependency isn't available.");
        }
    }
    private static Task<bool> IsStartupDependencyAvailableAsync(
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        return Task.FromResult(true);
    }
}
```

Register the validator in dependency injection:

```
builder.Services.AddSingleton<IAsyncStartupValidator, CustomStartupValidator>();
```

The host runs every registered `IAsyncStartupValidator` during [StartAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihost.startasync), alongside the built-in validator that `ValidateOnStart` registers for options validation.

Note

Asynchronous validation runs during host startup through [StartAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihost.startasync). `OptionsFactory.Create()`, `IOptions<TOptions>.Value`, `IOptionsSnapshot<TOptions>`, and configuration reloads observed through `IOptionsMonitor<TOptions>` remain synchronous and don't invoke asynchronous validators. Options that require asynchronous validation can't be validated outside of the `ValidateOnStart` startup path.

## Options post-configuration

Where services are registered for dependency injection, [PostConfigure](/en-us/dotnet/api/microsoft.extensions.options.ipostconfigureoptions-1.postconfigure) is available to initialize a particular [named option](#named-options-support-using-iconfigurenamedoptions). In the following example, only `TopItem:Month:Name` is post-configured:

```
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>(TopItemSettings.Month, options =>
{
    options.Name = "Blue Gizmo";
});
```

```
services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

services.PostConfigure<TopItemSettings>(TopItemSettings.Month, options =>
{
    options.Name = "Blue Gizmo";
});
```

Rendered output from the [named options example](#named-options-support-using-iconfigurenamedoptions), where only the `TopItem:Month:Name` is post-configured:

> Month: Name: Blue Gizmo Model: GW46\
> Year: Name: Orange Gadget Model: OG35

Where services are registered for dependency injection, use [PostConfigureAll](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.optionsservicecollectionextensions.postconfigureall) to initialize all named instances of the specified options type. In the following example, all instances of `TopItem.Name` are set to `Blue Gizmo`:

```
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigureAll<TopItemSettings>(options =>
{
    options.Name = "Blue Gizmo";
});
```

```
services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

services.PostConfigureAll<TopItemSettings>(options =>
{
    options.Name = "Blue Gizmo";
});
```

Rendered output from the [named options example](#named-options-support-using-iconfigurenamedoptions), where all `TopItem:Name` options are post-configured:

> Month: Name: Blue Gizmo Model: GW46\
> Year: Name: Blue Gizmo Model: OG35

## Access options in the request processing pipeline

To access [IOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptions-1) or [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) in the request processing pipeline, call [GetRequiredService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.getrequiredservice) on [WebApplication.Services](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.services):

```
var name = app.Services.GetRequiredService<IOptionsMonitor<PositionOptions>>()
    .CurrentValue.Name;
```

[IOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptions-1) and [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) can be used in `Startup.Configure`, since services are built before the `Configure` method executes.

In the following example, [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) is injected into the `Startup.Configure` method to obtain `PositionOptions` values:

```
public void Configure(IApplicationBuilder app, 
    IOptionsMonitor<PositionOptions> options)
```

Access the options in the request processing pipeline of `Startup.Configure`:

```
var name = options.CurrentValue.Name;
```

Don't use [IOptions\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptions-1) or [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) in `Startup.ConfigureServices`. An inconsistent options state may exist due to the ordering of service registrations.

## Related content

* [ASP.NET Core Blazor configuration: Options configuration](../../blazor/fundamentals/configuration?view=aspnetcore-10.0#options-configuration)
* [View or download sample code (Razor Pages)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/fundamentals/configuration) ([how to download](../?view=aspnetcore-10.0#how-to-download-a-sample))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/passkeys/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/passkeys/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Enable Web Authentication API (WebAuthn) passkeys

Passkeys provide a modern, phishing-resistant authentication method based on the [Web Authentication API (WebAuthn)](https://developer.mozilla.org/docs/Web/API/Web_Authentication_API) and [FIDO2](https://www.microsoft.com/security/business/security-101/what-is-fido2) standards. They are a secure alternative to passwords, using public key cryptography and device-based authentication. This article explains how to configure an ASP.NET Core app to use passkeys to authenticate users.

For guidance specific to new and existing Blazor Web Apps, see [Implement passkeys in ASP.NET Core Blazor Web Apps](blazor?view=aspnetcore-10.0) after reading this article.

## What are passkeys?

Passkeys are a replacement for passwords that use cryptographic key pairs. The private key is stored securely on the user's device, such as in a hardware security module, platform authenticator (examples: Windows Hello, Touch ID, Face ID), or a password manager, while the public key is stored by the web app. During authentication, the user proves possession of the private key without it ever leaving their device.

Key benefits of passkeys include:

* **Phishing resistance**: Passkeys are bound to specific websites and can't be used on fake sites.
* **No shared secrets**: The server only stores public keys, eliminating the risk of password database breaches.
* **User convenience**: Simple biometric or PIN verification replaces complex password requirements.
* **Cross-device synchronization**: Many passkey providers sync credentials across a user's devices.

For more information, see [Web Authentication API (MDN documentation)](https://developer.mozilla.org/docs/Web/API/Web_Authentication_API).

## Passkeys in ASP.NET Core Identity

ASP.NET Core Identity includes built-in support for passkey registration and authentication:

* Seamless integration with Identity infrastructure.
* User authentication support for the most common WebAuthn scenarios.
* Built into the Blazor Web App project template, so only developer configuration is required.

Important

The passkey implementation in ASP.NET Core Identity is deliberately scoped to authentication scenarios. It isn't intended as a general-purpose WebAuthn library. Developers requiring full WebAuthn functionality should consider community libraries that provide comprehensive protocol support.

## Supported scenarios

The ASP.NET Core Identity passkey implementation supports the following primary scenarios:

* **Adding passkeys to existing accounts**: Users with password-based accounts can register passkeys as an additional authentication method.
* **Passwordless account creation**: Users can create accounts without a password by registering a passkey on account creation.
* **Passwordless sign-in**: Users can authenticate using only their passkey without entering a password.

## Limitations

The current implementation has the following limitations:

* **Scoped to ASP.NET Core Identity**: The APIs are designed specifically for Identity authentication scenarios.
* **No default attestation validation**: The implementation doesn't validate attestation statements by default.
* **Template support**: Only the Blazor Web App template includes passkey support.
* **No built-in 2FA support**: Passkeys are treated as a primary authentication factor, not as a second factor.

## Core concepts

Two fundamental processes underpin passkey operations: attestation and assertion.

### Attestation (registration)

*Attestation* is the process of creating and registering a new passkey. During attestation, the server generates a unique challenge that the authenticator must include in the returned credential. The authenticator creates a new key pair and returns the public key along with attestation data proving the key's origin. The server then verifies this attestation and stores the public key for future authentication attempts.

### Assertion (authentication)

*Assertion* is the process of authenticating with an existing passkey. The server generates a unique challenge, which the authenticator signs using the private key. The authenticator returns this signed assertion to the server, which verifies the signature using the previously stored public key. If the signature is valid, the user is authenticated.

## Prerequisites

* [.NET SDK](https://dotnet.microsoft.com/download) (.NET 10 or later)
* A modern web browser that supports WebAuthn.
* A device with a platform authenticator, such as Windows Hello or Apple secure enclave, or a security key.

## Security considerations

When implementing passkeys in ASP.NET Core Identity, ensure the app meets the security requirements described in this section.

### Host header validation

The implementation infers the Relying Party ID from the host header when [ServerDomain](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.serverdomain) isn't explicitly configured. The hosting environment must validate host headers to prevent credential-scoping attacks, which involve using compromised or stolen user credentials (usernames, passwords, tokens) to gain unauthorized access.

**Mitigation**: Either explicitly configure [ServerDomain](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.serverdomain) in [IdentityPasskeyOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions) or ensure that the hosting environment (Kestrel, IIS, reverse proxy) validates host headers. For configuration details, see your hosting platform's documentation. In addition to that, if you use forwarded headers middleware, explicitly configure [AllowedHosts](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions.allowedhosts#microsoft-aspnetcore-builder-forwardedheadersoptions-allowedhosts).

### Subdomain security

ASP.NET Core's passkeys implementation handles subdomain security through the [ServerDomain](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.serverdomain) configuration option. When [ServerDomain](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.serverdomain) isn't explicitly specified, the implementation uses the host header to determine the domain. This means that ***the page on which the passkey was registered controls the domain*** for that credential.

For example:

* If a passkey is registered on `app.contoso.com`, it also works on `*.app.contoso.com`.
* If registered on `contoso.com`, it also works on `*.contoso.com`.
* The browser enforces that passkeys can only be used on the domain (and subdomains) where they were registered.

**Requirement**: Apps requiring strict domain control should explicitly set [ServerDomain](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.serverdomain) rather than relying on the host header. Don't serve untrusted content on any subdomain within the [ServerDomain](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.serverdomain) scope. If you can't guarantee this, implement [custom origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) to restrict passkey usage to specific origins.

### HTTPS requirement

All passkey operations require HTTPS. The implementation stores authentication data in encrypted and signed cookies that could be intercepted over unencrypted connections.

**Requirement**: Always use HTTPS in production. Configure [HTTP Strict Transport Security (HSTS) protocol](../../enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) to prevent protocol downgrade attacks.

### Account recovery

Account recovery is primarily a concern for apps that allow passkeys as the only authentication mechanism. The default Blazor Web App project template already requires users to set up a backup authentication method (password or external provider) when creating an account, so account recovery is handled through these existing mechanisms.

**Recommendations**:

For applications implementing passkey-only authentication, consider:

* Recovery codes generated during account creation.
* Email-based recovery flows.
* Mandatory registration of multiple passkeys.
* Monitoring the [IsBackedUp](/en-us/dotnet/api/microsoft.aspnetcore.identity.userpasskeyinfo.isbackedup) flag on [UserPasskeyInfo](/en-us/dotnet/api/microsoft.aspnetcore.identity.userpasskeyinfo) to prompt users to add additional credentials.

### Administrative controls

When an authenticator model is discovered to have security vulnerabilities, you may need to revoke affected credentials. The implementation stores the complete attestation object with each credential, including the Authenticator Attestation GUID (AAGUID), which is a 128-bit identifier indicating the key type.

**Implementation**: Extract AAGUIDs from stored attestation objects, compare against known-compromised models, and revoke affected credentials. AAGUID reliability depends on whether your app validates attestation statements. To hook in custom attestation statement validation logic, see [Custom attestation statement validation](#custom-attestation-statement-validation). Third-party libraries are available for attestation validation, such as the [Passkeys - FIDO2 .NET Library (WebAuthn) (`passwordless-lib/fido2-net-lib` GitHub repository)](https://github.com/passwordless-lib/fido2-net-lib)†.

Warning

†Third-party libraries, including `passwordless-lib/fido2-net-lib`, aren't owned or maintained by Microsoft and aren't covered by any Microsoft Support Agreement or license. Use caution when adopting a third-party library, especially for security features. Confirm that the library follows official specifications and adopts security best practices. Keep the library's version current to obtain the latest bug fixes.

### Resource limits

To prevent database exhaustion attacks, apps should enforce limits on passkey registration, such as:

* Maximum number of passkeys per user account.
* Maximum length for passkey display names.

The Blazor Web App template enforces these limits by default at the application level. For examples, see the following Razor components in the Blazor Web App project template:

* [`Components/Account/Pages/Manage/Passkeys.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor)
* [`Components/Account/Pages/Manage/RenamePasskey.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor)

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Configure passkey options

ASP.NET Core Identity provides various options to configure passkey behavior through the [IdentityPasskeyOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions) class, which include:

* [AuthenticatorTimeout](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.authenticatortimeout): Gets or sets the time that the browser should wait for the authenticator to provide a passkey as a [TimeSpan](/en-us/dotnet/api/system.timespan). This option applies to both creating a new passkey and requesting an existing passkey. This option is treated as a hint to the browser, and the browser may ignore the option. The default value is 5 minutes.
* [ChallengeSize](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.challengesize): Gets or sets the size of the challenge in bytes sent to the client during attestation and assertion. This option applies to both creating a new passkey and requesting an existing passkey. The default value is 32 bytes.
* [ServerDomain](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.serverdomain): Gets or sets the effective Relying Party ID (domain) of the server. This should be unique and will be used as the identity for the server. This option applies to both creating a new passkey and requesting an existing passkey. If `null`, which is the default value, the server's origin is used. For more information, see [Relying Party Identifier RP ID](https://www.w3.org/TR/webauthn-3/#rp-id).

Example configuration:

```
builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ServerDomain = "contoso.com";
    options.AuthenticatorTimeout = TimeSpan.FromMinutes(3);
    options.ChallengeSize = 64;
});
```

For a complete list of configuration options, see [IdentityPasskeyOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions). For the most up-to-date browser defaults, see the [W3C WebAuthn specification](https://www.w3.org/TR/webauthn-3/).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next preview release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Custom attestation statement validation

By default, ASP.NET Core Identity doesn't validate attestation statements. This is suitable for most consumer authentication scenarios. If your app requires verification of authenticator properties or if you want to disallow specific authenticators from being used, for example, in enterprise environments that require a higher level of security, you can implement custom attestation validation:

```
builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.VerifyAttestationStatement = async (context) =>
    {
        // Custom attestation validation logic
        // Return 'true' if the attestation is valid
        // Return 'false' if the attestation is invalid
        return true;
    };
});
```

Warning

Attestation validation is complex and requires maintaining trust stores for authenticator certificates. Only implement custom validation if your app requires verification of specific authenticator properties.

## Custom origin validation

The default origin validation allows requests from subdomains and disallows cross-origin iframes. To customize this behavior:

```
builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ValidateOrigin = async (context) =>
    {
        // Custom origin validation logic
        //   Access the origin via 'context.Origin'
        //   Access the HTTP context via 'context.HttpContext'
        // Return 'true' if the origin is valid
        // Return 'false' if the origin is invalid
        return true;
    };
});
```

## Customize the passkey handler

[IPasskeyHandler\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1) is the service that generates passkey options and performs attestation and assertion. ASP.NET Core Identity registers [PasskeyHandler\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeyhandler-1) as the default implementation, and [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) calls into it. The interface has four methods:

* [MakeCreationOptionsAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1.makecreationoptionsasync): Generates the creation options and the attestation state for registering a passkey.
* [MakeRequestOptionsAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1.makerequestoptionsasync): Generates the request options and the assertion state for authenticating with an existing passkey.
* [PerformAttestationAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1.performattestationasync): Validates the credential that the browser returns during registration.
* [PerformAssertionAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1.performassertionasync): Validates the credential that the browser returns during authentication.

Most apps should use the [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) methods shown in the [Registration flow](#registration-flow) and [Authentication flow](#authentication-flow) sections instead of calling the handler directly. Those methods manage the temporary state that the two-request registration and authentication ceremonies require, and they protect that state on the app's behalf.

### When to implement `IPasskeyHandler<TUser>`

For most customizations, use the [IdentityPasskeyOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions) callbacks described in the [Custom attestation statement validation](#custom-attestation-statement-validation) and [Custom origin validation](#custom-origin-validation) sections. Implement [IPasskeyHandler\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1) only for scenarios that the options can't express, such as:

* Varying passkey options per request or per tenant.
* Resolving users from a source other than [UserManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1).
* Adding checks that run before or after the built-in WebAuthn verification.

Implementing the interface from scratch means reimplementing the WebAuthn verification steps, which is error prone. Where possible, implement the interface as a wrapper that delegates to an inner [PasskeyHandler\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeyhandler-1) instance and adds only the required behavior.

Warning

An app that calls [IPasskeyHandler\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1) directly, rather than through [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1), becomes responsible for storing the attestation state securely. An app that fails to do so allows an attacker to register a passkey on an account that they don't own.

### Attestation state

Registering a passkey spans two requests. The first request obtains creation options, and the second request submits the new credential. The attestation state is the information that the server remembers between the two requests.

[MakeCreationOptionsAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1.makecreationoptionsasync) returns a [PasskeyCreationOptionsResult](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeycreationoptionsresult) with two parts:

* [CreationOptionsJson](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeycreationoptionsresult.creationoptionsjson): Sent to the browser and passed to `navigator.credentials.create()`.
* [AttestationState](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeycreationoptionsresult.attestationstate): Retained by the app and supplied to [AttestationState](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeyattestationcontext.attestationstate) when the credential is submitted.

The state that the default handler produces holds the challenge and the [PasskeyUserEntity](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeyuserentity) that identifies the account the passkey is registered to. The state is serialized as JSON without a signature or encryption, so it provides no integrity protection of its own.

The state is the only place that records the account. An attestation response from the browser contains a new public key and the signed challenge but nothing that identifies a user, so [PerformAttestationAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1.performattestationasync) reports the user taken from the state in [UserEntity](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeyattestationresult.userentity). If the state is modified before the credential is submitted, the passkey is registered to whichever account the modified state names.

### Validate integrity and ownership

An app that manages the attestation state itself must meet the following three requirements.

**Integrity**: The client must not be able to modify the state. Don't place the state in a hidden form field, a request body, or unprotected browser storage. An attacker who alters the user ID in the state completes registration normally, and their authenticator is stored on the account that the altered state names, which grants them sign-in access to that account. Keep the state on the server or protect it with the [ASP.NET Core Data Protection APIs](../../data-protection/introduction?view=aspnetcore-10.0) before sending it to the browser.

**Ownership**: The state must be bound to the session that requested the creation options, and the binding must be checked when the credential is submitted. Compare [UserEntity](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeyattestationresult.userentity) to the signed-in user before storing the passkey. The handler doesn't perform this check because it has no independent record of the user to compare against.

**Single use and expiration**: Discard the state after reading it and give it a short lifetime. A state that remains valid can be replayed.

For reference, [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) meets these requirements by:

* Storing the state in an authentication cookie protected by the Data Protection APIs, which the browser can't read or modify.
* Signing the state out as soon as it's read, which limits the state to a single use.
* Recording whether the state was created for attestation or assertion and rejecting a mismatch.

### Registration example

The following example calls [IPasskeyHandler\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasskeyhandler-1) directly and keeps the attestation state in a data-protected cookie. Both endpoints require an authenticated user because the passkey is added to the signed-in account:

```
const string AttestationStateCookieName = "PasskeyAttestationState";
const string AttestationStatePurpose = "Contoso.Passkeys.AttestationState.v1";

app.MapPost("/Account/PasskeyCreationOptions", async (
    HttpContext context,
    UserManager<ApplicationUser> userManager,
    IPasskeyHandler<ApplicationUser> passkeyHandler,
    IDataProtectionProvider dataProtectionProvider) =>
{
    var user = await userManager.GetUserAsync(context.User);

    if (user is null)
    {
        return Results.Unauthorized();
    }

    var userId = await userManager.GetUserIdAsync(user);
    var userName = await userManager.GetUserNameAsync(user) ?? "User";

    var options = await passkeyHandler.MakeCreationOptionsAsync(new()
    {
        Id = userId,
        Name = userName,
        DisplayName = userName
    }, context);

    var attestationState = options.AttestationState 
        ?? throw new InvalidOperationException("No attestation state was created.");

    var protector = 
        dataProtectionProvider.CreateProtector(AttestationStatePurpose);

    context.Response.Cookies.Append(
        AttestationStateCookieName,
        protector.Protect(attestationState),
        new CookieOptions
        {
            HttpOnly = true,
            Secure = true,
            SameSite = SameSiteMode.Strict,
            MaxAge = TimeSpan.FromMinutes(5)
        });

    return TypedResults.Content(options.CreationOptionsJson, 
        contentType: "application/json");
}).RequireAuthorization();
```

The preceding code:

* Protects the attestation state with the data protection APIs before the state reaches the browser, so the client can't read or modify it.
* Limits the cookie to a five minute lifetime, which bounds how long a registration can stay open.

When the credential arrives, the app restores the state, verifies that the attestation belongs to the signed-in user, and only then stores the passkey:

```
app.MapPost("/Account/PasskeyRegistration", async (
    PasskeyRegistrationRequest request,
    HttpContext context,
    UserManager<ApplicationUser> userManager,
    IPasskeyHandler<ApplicationUser> passkeyHandler,
    IDataProtectionProvider dataProtectionProvider) =>
{
    var user = await userManager.GetUserAsync(context.User);

    if (user is null)
    {
        return Results.Unauthorized();
    }

    if (!context.Request.Cookies.TryGetValue(AttestationStateCookieName, 
        out var protectedState))
    {
        return Results.BadRequest("No passkey registration is underway");
    }

    context.Response.Cookies.Delete(AttestationStateCookieName);

    var protector = 
        dataProtectionProvider.CreateProtector(AttestationStatePurpose);
    string attestationState;

    try
    {
        attestationState = protector.Unprotect(protectedState);
    }
    catch (CryptographicException)
    {
        return Results.BadRequest("The passkey registration state isn't valid");
    }

    var attestationResult = await passkeyHandler.PerformAttestationAsync(new()
    {
        HttpContext = context,
        CredentialJson = request.CredentialJson,
        AttestationState = attestationState
    });

    if (!attestationResult.Succeeded)
    {
        return Results.BadRequest($"Error: {attestationResult.Failure.Message}");
    }

    var userId = await userManager.GetUserIdAsync(user);

    if (!string.Equals(attestationResult.UserEntity.Id, userId, 
        StringComparison.Ordinal))
    {
        return Results.BadRequest("The passkey isn't registered to the current user");
    }

    var addResult = 
        await userManager.AddOrUpdatePasskeyAsync(user, attestationResult.Passkey);

    if (!addResult.Succeeded)
    {
        return Results.BadRequest("Failed to store passkey");
    }

    return Results.Ok();
}).RequireAuthorization();
```

The preceding code:

* Deletes the state cookie before the state is used, so the state is good for a single registration.
* Rejects the request when the state can't be unprotected, which happens when the value was tampered with or was protected with a different key.
* Confirms that the attestation result names the signed-in user before the passkey is stored. The handler takes the account from the attestation state, so this check is what stops a passkey from being attached to another account.

The `PasskeyRegistrationRequest` type carries the serialized credential from the browser:

```
public record PasskeyRegistrationRequest(string CredentialJson);
```

### Register a custom handler

`AddSignInManager` registers the default handler with `AddScoped` rather than `TryAddScoped`, so a custom handler must be registered ***after*** the Identity services to be the implementation that's resolved:

```
builder.Services.AddIdentityCore<ApplicationUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager();

builder.Services
    .AddScoped<IPasskeyHandler<ApplicationUser>, CustomPasskeyHandler>();
```

## Registration flow

This section walks through each step of the passkey registration process, explaining how ASP.NET Core Identity facilitates the creation and storage of passkey credentials.

```
sequenceDiagram
    participant Authenticator
    participant User
    participant Browser
    participant Server

    User->>Browser: Click "Add passkey"
    Browser->>Server: Request creation options
    Server->>Browser: Return creation options
    Browser->>Authenticator: Request new credential
    Authenticator->>User: Verify identity (biometric/PIN)
    User->>Authenticator: Approve
    Authenticator->>Browser: Return credential
    Browser->>Server: Submit credential
    Server->>Server: Verify and store
    Server->>Browser: Registration complete
    Browser->>User: Success message
```

### Step 1: Initiating registration

The registration process begins when a user decides to add a passkey to their account. This typically happens through a button or link in the app's user interface. When selected, this element triggers JavaScript code to orchestrate the registration flow.

The client-side implementation varies significantly between apps. In the Blazor Web App template, you can find a complete example in `PasskeySubmit.razor.js`, which shows how a custom web component handles the registration initiation and manages the subsequent WebAuthn API calls.

### Step 2: Requesting creation options

After registration is initiated, the browser must obtain creation options from the server. These options tell the browser what kind of credential to create and include important security parameters, such as the challenge that must be signed.

From the browser's perspective, this step involves making an HTTP request to the server:

```
async function createCredential(signal) {
  // Step 2: Request creation options from the server
  const optionsResponse = 
    await fetchWithErrorHandling('/Account/PasskeyCreationOptions', 
    {
      method: 'POST',
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
  return await navigator.credentials.create({ publicKey: options, signal });
}
```

```
async function createCredential(headers, signal) {
  // Step 2: Request creation options from the server
  const optionsResponse = 
    await fetchWithErrorHandling('/Account/PasskeyCreationOptions', 
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
  return await navigator.credentials.create({ publicKey: options, signal });
}
```

The application should define an endpoint that generates these options:

```
app.MapPost("/Account/PasskeyCreationOptions", async (
    HttpContext context,
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager) =>
{
    var user = await userManager.GetUserAsync(context.User);

    if (user is null)
    {
        return Results.NotFound();
    }

    var userId = await userManager.GetUserIdAsync(user);
    var userName = await userManager.GetUserNameAsync(user) ?? "User";
    
    var optionsJson = await signInManager.MakePasskeyCreationOptionsAsync(new()
    {
        Id = userId,
        Name = userName,
        DisplayName = userName
    });
    
    return TypedResults.Content(optionsJson, contentType: "application/json");
});
```

The [MakePasskeyCreationOptionsAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.makepasskeycreationoptionsasync) method is central to this process. The method accepts a `PasskeyUserEntity` that describes the user for whom the passkey is being created. This entity contains the user's ID, username (typically an email address), and a human-readable display name. The method returns a JSON string that conforms to the WebAuthn `PublicKeyCredentialCreationOptions` schema, which the browser uses in the next step. Behind the scenes, this method also stores temporary state in an authentication cookie to ensure that the response from the browser corresponds to these specific options. Apps that manage this state themselves must protect it, as described in the [Customize the passkey handler](#customize-the-passkey-handler) section.

### Step 3: Server generates options

When [MakePasskeyCreationOptionsAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.makepasskeycreationoptionsasync) executes, it uses the app's [IdentityPasskeyOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions) configuration to determine the specific parameters for credential creation. These options control various aspects of the passkey creation process.

You can customize these options during application startup. For example:

```
builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ServerDomain = "contoso.com";
    options.AuthenticatorTimeout = TimeSpan.FromMinutes(3);
    options.UserVerificationRequirement = "required";
    options.ResidentKeyRequirement = "preferred";
});
```

The [UserVerificationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.userverificationrequirement) option determines whether the authenticator must verify the user's identity (through biometric or PIN methods), while [ResidentKeyRequirement](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.residentkeyrequirement) indicates whether the credential should be discoverable, allowing authentication without first providing a username. For more information, see [IdentityPasskeyOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions).

### Step 4: Client requests credential

With the creation options available, the client-side JavaScript passes the options to the WebAuthn API to create a new credential:

```
async function createCredential(signal) {
  // Step 4: Parse the options and request a new credential from the authenticator
  const optionsResponse = 
    await fetchWithErrorHandling('/Account/PasskeyCreationOptions', 
    {
      method: 'POST',
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
  return await navigator.credentials.create({ publicKey: options, signal });
}
```

```
async function createCredential(headers, signal) {
  // Step 4: Parse the options and request a new credential from the authenticator
  const optionsResponse = 
    await fetchWithErrorHandling('/Account/PasskeyCreationOptions', 
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
  return await navigator.credentials.create({ publicKey: options, signal });
}
```

The `parseCreationOptionsFromJSON` function converts the JSON response into the format expected by the WebAuthn API, and `navigator.credentials.create()` initiates the credential creation process with the authenticator.

### Step 5: Authenticator interaction

At this point, the browser communicates with the authenticator to create the credential. The authenticator prompts the user for verification, which might involve scanning a fingerprint, entering a PIN, or using facial recognition. This interaction is handled entirely by the browser and the authenticator, requiring no app code. The user experience varies depending on the type of authenticator and the platform's capabilities.

### Step 6: Credential submission

After the authenticator creates the credential, the browser must send the credential back to the server for verification and storage. The credential must be serialized to JSON before submission:

```
async function createCredentialJson(signal) {
  const credential = await createCredential(signal);
  return JSON.stringify(credential);
}
```

```
async function createCredentialJson(headers, signal) {
  const credential = await createCredential(headers, signal);
  return JSON.stringify(credential);
}
```

In the Blazor Web App template, the returned credential is automatically serialized and submitted through a form, but the exact submission mechanism varies by application.

### Step 7: Server verification and storage

When the server receives the credential, it must verify its validity and store the public key for future authentication. This is where ASP.NET Core Identity's passkey APIs become crucial.

The [PerformPasskeyAttestationAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.performpasskeyattestationasync) method validates the attestation response from the client. This comprehensive validation process:

* Verifies that the credential type matches expectations.
* Validates the client data JSON including origin and challenge.
* Checks authenticator data flags for user presence and verification
* Extracts and validates the public key.

If all checks pass, the method returns a [PasskeyAttestationResult](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeyattestationresult) containing the verified passkey information.

After the attestation is verified, the app uses [AddOrUpdatePasskeyAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1.addorupdatepasskeyasync) to store the passkey in the database:

```
var attestationResult = 
    await signInManager.PerformPasskeyAttestationAsync(credentialJson);

if (!attestationResult.Succeeded)
{
    return Results.BadRequest($"Error: {attestationResult.Failure.Message}");
}

var addResult = 
    await userManager.AddOrUpdatePasskeyAsync(user, attestationResult.Passkey);

if (!addResult.Succeeded)
{
    return Results.BadRequest("Failed to store passkey");
}
```

The stored [UserPasskeyInfo](/en-us/dotnet/api/microsoft.aspnetcore.identity.userpasskeyinfo) contains all of the necessary information for future authentication, including the credential ID, public key, signature counter for replay protection, and flags indicating whether the passkey is backed up or eligible for backup.

### Step 8: Post-registration tasks

After successfully registering a passkey, apps often perform additional tasks to improve the user experience. A common pattern is to prompt users to provide a friendly name for their passkey, making it easier to identify among multiple credentials. The [UserPasskeyInfo.Name](/en-us/dotnet/api/microsoft.aspnetcore.identity.userpasskeyinfo.name) property stores this user-friendly name, which can be updated using the same [AddOrUpdatePasskeyAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1.addorupdatepasskeyasync) method:

```
passkey.Name = "My iPhone";
await userManager.AddOrUpdatePasskeyAsync(user, passkey);
```

## Authentication flow

This section explains how users authenticate with their passkeys, from initiating the sign-in process to establishing an authenticated session.

```
sequenceDiagram
    participant Authenticator
    participant User
    participant Browser
    participant Server

    User->>Browser: Click "Sign in with passkey"
    Browser->>Server: Request authentication options
    Server->>Browser: Return authentication options
    Browser->>Authenticator: Request assertion
    Authenticator->>User: Verify identity
    User->>Authenticator: Approve
    Authenticator->>Browser: Return signed assertion
    Browser->>Server: Submit assertion
    Server->>Server: Verify signature
    Server->>Browser: Authentication complete
    Browser->>User: Redirect to app
```

### Step 1: Initiating authentication

Users typically initiate passkey authentication through a dedicated button or link on the login page. Some apps also support conditional UI, where passkeys appear as autofill suggestions in the username field. The initiation method triggers JavaScript code that manages the authentication flow, similar to the registration process.

### Step 2: Requesting authentication options

The browser requests authentication options from the server to begin the authentication process. These options include a list of acceptable credentials and a new challenge to be signed:

```
async function requestCredential(email, mediation, signal) {
  // Step 2: Request authentication options from the server
  const optionsResponse = 
    await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${encodeURIComponent(email)}`,
    {
      method: 'POST',
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
  return await navigator.credentials.get({ publicKey: options, mediation, signal });
}
```

```
async function requestCredential(email, mediation, headers, signal) {
  // Step 2: Request authentication options from the server
  const optionsResponse = 
    await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${encodeURIComponent(email)}`,
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
  return await navigator.credentials.get({ publicKey: options, mediation, signal });
}
```

The [MakePasskeyRequestOptionsAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.makepasskeyrequestoptionsasync) method generates these options. When you provide a specific user, it includes only that user's credentials in the allow list. When called without a user, it generates options suitable for conditional UI or username-less authentication:

```
app.MapPost("/Account/PasskeyRequestOptions", async (
    SignInManager<ApplicationUser> signInManager,
    string? username) =>
{
    var user = string.IsNullOrEmpty(username) 
        ? null 
        : await userManager.FindByNameAsync(username);

    var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user);

    return TypedResults.Content(optionsJson, contentType: "application/json");
});
```

### Step 3: Server generates options

The server generates authentication options using the same [IdentityPasskeyOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions) configuration used during registration. The [ServerDomain](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.serverdomain) must match the domain where the passkey was originally registered, or authentication fails. The [UserVerificationRequirement](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions.userverificationrequirement) determines whether the authenticator must verify the user's identity during authentication.

### Step 4: Client requests assertion

The client-side JavaScript passes the authentication options to the WebAuthn API to request an assertion from the authenticator:

```
async function requestCredential(email, mediation, signal) {
  // Step 4: Parse the options and request an assertion from the authenticator
  const optionsResponse = 
    await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${encodeURIComponent(email)}`,
    {
      method: 'POST',
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
  return await navigator.credentials.get({ publicKey: options, mediation, signal });
}
```

```
async function requestCredential(email, mediation, headers, signal) {
  // Step 4: Parse the options and request an assertion from the authenticator
  const optionsResponse = 
    await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${encodeURIComponent(email)}`,
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
  return await navigator.credentials.get({ publicKey: options, mediation, signal });
}
```

The `navigator.credentials.get()` call initiates the authentication process with the authenticator, which prompts the user for verification.

### Step 5: Authenticator verification

The authenticator verifies the user's identity and signs the challenge with the private key. This process is handled entirely by the browser and authenticator, similar to the verification step during registration. The user experience depends on the authenticator type and may involve biometric verification or PIN entry.

### Step 6: Assertion submission

After the authenticator creates the signed assertion, the browser serializes it to JSON and submits it to the server:

```
async function requestCredentialJson(email, mediation, signal) {
  const credential = await requestCredential(email, mediation, signal);
  return JSON.stringify(credential);
}
```

```
async function requestCredentialJson(email, mediation, headers, signal) {
  const credential = await requestCredential(email, mediation, headers, signal);
  return JSON.stringify(credential);
}
```

The submission mechanism varies by app but typically involves either a form submission or an API call.

### Step 7: Server verification

The server verifies the assertion to authenticate the user. ASP.NET Core Identity provides the [PasskeySignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.passkeysigninasync) method, which performs the complete authentication flow in a single call:

```
var result = await signInManager.PasskeySignInAsync(credentialJson);

if (result.Succeeded)
{
    return Results.Ok("Authentication successful");
}

return Results.Unauthorized();
```

The [PasskeySignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.passkeysigninasync) method internally calls [PerformPasskeyAssertionAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.performpasskeyassertionasync) to:

* Validate the assertion signature using the stored public key.
* Verify that the challenge matches the one originally sent.
* Check authenticator flags for user presence and verification.
* Update the signature counter to prevent replay attacks.

If all checks pass, the method signs in the user and returns a `SignInResult` indicating success.

The method distinguishes between a recoverable user action and an app error. If the passkey session state is missing or expired, for example because the user took too long to respond to the authenticator, the method returns [SignInResult.Failed](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinresult.failed#microsoft-aspnetcore-identity-signinresult-failed). Calling the method without a preceding call to [MakePasskeyRequestOptionsAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.makepasskeyrequestoptionsasync) throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) because that indicates a problem in the app rather than something the user can retry.

For scenarios requiring more control, you can use [PerformPasskeyAssertionAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.performpasskeyassertionasync) directly to validate the assertion without immediately signing in the user:

* [PerformPasskeyAssertionAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.performpasskeyassertionasync) returns a [PasskeyAssertionResult](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeyassertionresult) containing the authenticated user and updated passkey information.
* Because the passkey's sign-in count and authenticator flags may have changed since the last assertion and the updated passkey isn't automatically stored when calling [PerformPasskeyAssertionAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.performpasskeyassertionasync), call [UserManager\<TUser>.AddOrUpdatePasskeyAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1.addorupdatepasskeyasync) with the returned [PasskeyAssertionResult](/en-us/dotnet/api/microsoft.aspnetcore.identity.passkeyassertionresult).

### Step 8: Session establishment

Upon successful authentication, ASP.NET Core Identity establishes an authenticated session for the user. The [PasskeySignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.passkeysigninasync) method handles this automatically, creating the necessary authentication cookies and claims. The app then redirects the user to protected resources or displays personalized content.

## Mitigate `PublicKeyCredential.toJSON` error (`TypeError: Illegal invocation`)

The [`PublicKeyCredential.toJSON` method](https://developer.mozilla.org/docs/Web/API/PublicKeyCredential/toJSON) returns a JSON representation of a [`PublicKeyCredential`](https://developer.mozilla.org/docs/Web/API/PublicKeyCredential). The method is invoked by the password manager when the app attempts to serialize a `PublicKeyCredential` by calling [`JSON.stringify`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) while registering or authenticating a user.

Some password managers don't implement the `PublicKeyCredential.toJSON` method correctly, which is required for `JSON.stringify` to work when serializing passkey credentials. When registering or authenticating a user with an app based on the Blazor Web App project template, the following error is thrown by some password managers when attempting to add a passkey:

> Error: Could not add a passkey: Illegal invocation

Until your selected password manager is updated to implement the `PublicKeyCredential.toJSON` method correctly, make the following changes to the app. The following code manually JSON serializes the `PublicKeyCredential`.

In the `Components/Account/Shared/PasskeySubmit.razor.js` file, locate the `passkey-submit` custom element definition code block:

```
customElements.define('passkey-submit', class extends HTMLElement {
  ...
});
```

Add the following `convertToBase64` function to the code block:

```
convertToBase64(o) {
  if (!o) {
    return undefined;
  }

  // Normalize Array to Uint8Array
  if (Array.isArray(o)) {
    o = Uint8Array.from(o);
  }

  // Normalize ArrayBuffer to Uint8Array
  if (o instanceof ArrayBuffer) {
    o = new Uint8Array(o);
  }

  // Convert Uint8Array to base64
  if (o instanceof Uint8Array) {
    let str = '';
    for (let i = 0; i < o.byteLength; i++) {
      str += String.fromCharCode(o[i]);
    }
    o = window.btoa(str);
  }

  if (typeof o !== 'string') {
    throw new Error("Could not convert to base64 string");
  }

  // Convert base64 to base64url
  o = o.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");

  return o;
}
```

In the `obtainAndSubmitCredential` function of the code block, locate the line that calls `JSON.stringify` with the user's credential and remove the line:

```
- const credentialJson = JSON.stringify(credential);
```

Replace the preceding line with the following code:

```
const credentialJson = JSON.stringify({
  authenticatorAttachment: credential.authenticatorAttachment,
  clientExtensionResults: credential.getClientExtensionResults(),
  id: credential.id,
  rawId: this.convertToBase64(credential.rawId),
  response: {
    attestationObject: this.convertToBase64(credential.response.attestationObject),
    authenticatorData: this.convertToBase64(credential.response.authenticatorData ?? 
      credential.response.getAuthenticatorData?.() ?? undefined),
    clientDataJSON: this.convertToBase64(credential.response.clientDataJSON),
    publicKey: this.convertToBase64(credential.response.getPublicKey?.() ?? undefined),
    publicKeyAlgorithm: credential.response.getPublicKeyAlgorithm?.() ?? undefined,
    transports: credential.response.getTransports?.() ?? undefined,
    signature: this.convertToBase64(credential.response.signature),
    userHandle: this.convertToBase64(credential.response.userHandle),
  },
  type: credential.type,
});
```

The preceding workaround is only required until the password manager is updated to implement the `PublicKeyCredential.toJSON` method correctly. We recommend tracking your password manager's release notes and reverting the preceding changes after the password manager is updated.

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-29

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/claimsprincipal-current
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/claimsprincipal-current.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from static ClaimsPrincipal access

The current ClaimsPrincipal is a fundamental component of authenticated web applications, providing access to the current user's identity and claims. When migrating from ASP.NET Framework to ASP.NET Core, accessing this presents unique challenges because the two frameworks have different approaches to user context management.

## Why static ClaimsPrincipal migration is complex

ASP.NET Framework and ASP.NET Core take fundamentally different approaches to how the current user is accessed:

* **ASP.NET Framework** uses static properties like [ClaimsPrincipal.Current](/en-us/dotnet/api/system.security.claims.claimsprincipal.current#system-security-claims-claimsprincipal-current) and [Thread.CurrentPrincipal](/en-us/dotnet/api/system.threading.thread.currentprincipal#system-threading-thread-currentprincipal) with automatic context management. These properties are interchangeable and both provide access to the current user's identity.
* **ASP.NET Core** stores the current user in [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user) and avoids static state.

These differences mean you can't simply continue using static principal properties ([ClaimsPrincipal.Current](/en-us/dotnet/api/system.security.claims.claimsprincipal.current#system-security-claims-claimsprincipal-current) or [Thread.CurrentPrincipal](/en-us/dotnet/api/system.threading.thread.currentprincipal#system-threading-thread-currentprincipal)) in ASP.NET Core without changes. By default, the static properties aren't set, and code depending on them needs to be updated to get the current authenticated user's identity through different means.

## Migration strategies overview

You have two main approaches for handling static principal access during migration:

1. **Complete rewrite** - Rewrite all static principal access code to use ASP.NET Core's native patterns
2. **System.Web adapters** - Use adapters to enable static access patterns during incremental migration

For most applications, migrating to ASP.NET Core's native ClaimsPrincipal access provides the best performance and maintainability. However, larger applications or those with extensive static principal usage may benefit from using System.Web adapters during incremental migration.

## Choose your migration approach

You have two main options for migrating static principal access from ASP.NET Framework to ASP.NET Core. Your choice depends on your migration timeline, whether you need to run both applications simultaneously, and how much code you're willing to rewrite.

### Quick decision guide

**Answer these questions to choose your approach:**

1. **Are you doing a complete rewrite or incremental migration?**

   * Complete rewrite → [Complete rewrite to ASP.NET Core patterns](#complete-rewrite-to-aspnet-core-patterns)
   * Incremental migration → Continue to question 2

2. **Do you have extensive static principal usage ([ClaimsPrincipal.Current](/en-us/dotnet/api/system.security.claims.claimsprincipal.current#system-security-claims-claimsprincipal-current) or [Thread.CurrentPrincipal](/en-us/dotnet/api/system.threading.thread.currentprincipal#system-threading-thread-currentprincipal)) across shared libraries?**

   * Yes, lots of shared code → [System.Web adapters](#systemweb-adapters)
   * No, isolated static principal usage → [Complete rewrite to ASP.NET Core patterns](#complete-rewrite-to-aspnet-core-patterns)

### Migration approaches comparison

| Approach                                                          | Code Changes                               | Performance | Shared Libraries         | When to Use                                     |
| ----------------------------------------------------------------- | ------------------------------------------ | ----------- | ------------------------ | ----------------------------------------------- |
| **[Complete rewrite](#complete-rewrite-to-aspnet-core-patterns)** | High - Rewrite all static principal access | Best        | Requires updates         | Complete rewrites, performance-critical apps    |
| **[System.Web adapters](#systemweb-adapters)**                    | Low - Keep existing patterns               | Good        | Works with existing code | Incremental migrations, extensive static access |

## Complete rewrite to ASP.NET Core patterns

Choose this approach when you're performing a complete migration or want the best performance and maintainability.

ASP.NET Core provides several options for retrieving the current authenticated user's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) without relying on static properties. This approach requires rewriting static principal access code but offers the most benefits in the long term.

### Complete rewrite pros and cons

| Pros                                 | Cons                                                |
| ------------------------------------ | --------------------------------------------------- |
| Best performance                     | Requires rewriting all static principal access code |
| More testable (dependency injection) | No automatic migration path                         |
| No static dependencies               | Learning curve for new patterns                     |
| Native ASP.NET Core implementation   | Breaking change from Framework patterns             |
| Thread-safe by design                | Potential refactoring across shared libraries       |

### ASP.NET Core ClaimsPrincipal access patterns

There are several options for retrieving the current authenticated user's [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) in ASP.NET Core in place of [ClaimsPrincipal.Current](/en-us/dotnet/api/system.security.claims.claimsprincipal.current#system-security-claims-claimsprincipal-current):

* **[ControllerBase.User](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.user#microsoft-aspnetcore-mvc-controllerbase-user)**
* **[HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user)**
* **Passed in from caller**. Libraries without access to the current [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) are often called from controllers or middleware components and can have the current user's identity passed as an argument.
* **[IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor)**. The project being migrated to ASP.NET Core may be too large to easily pass the current user's identity to all necessary locations. In such cases, [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) can be used as a workaround. This is not ideal as it uses a static accessor behind the scenes. Prefer a more direct option if possible.

### Code examples

Here are examples of migrating common static principal usage patterns:

**ASP.NET Framework (before):**

```
public class UserService
{
    public string GetCurrentUserId()
    {
        // Both ClaimsPrincipal.Current and Thread.CurrentPrincipal work interchangeably
        return ClaimsPrincipal.Current?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        // or: return Thread.CurrentPrincipal?.Identity?.Name;
    }
}
```

**ASP.NET Core (after) - Pass ClaimsPrincipal as parameter:**

```
public class UserService
{
    public string GetCurrentUserId(ClaimsPrincipal user)
    {
        return user?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    }
}

// Usage in controller
public class HomeController : Controller
{
    private readonly UserService _userService;
    
    public HomeController(UserService userService)
    {
        _userService = userService;
    }
    
    public IActionResult Index()
    {
        var userId = _userService.GetCurrentUserId(User);
        return View();
    }
}
```

**ASP.NET Core (after) - Dependency Injection:**

```
public class UserService
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    
    public UserService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    public string GetCurrentUserId()
    {
        return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    }
}
```

### When to choose this approach

* You can afford to rewrite static principal access code
* Performance is a top priority (in this case, prefer passing the identity as a parameter over DI)
* You want to eliminate static dependencies
* You're not sharing code with legacy applications
* You want the most testable and maintainable solution

## System.Web adapters

Note

This makes use of the [System.Web Adapters](../inc/systemweb-adapters?view=aspnetcore-10.0) to simplify migration.

Choose this approach when you need to maintain existing static principal usage patterns during incremental migration, or when you have extensive shared libraries that would be difficult to update.

The System.Web adapters can enable both [ClaimsPrincipal.Current](/en-us/dotnet/api/system.security.claims.claimsprincipal.current#system-security-claims-claimsprincipal-current) and [Thread.CurrentPrincipal](/en-us/dotnet/api/system.threading.thread.currentprincipal#system-threading-thread-currentprincipal) support in ASP.NET Core, allowing you to keep existing code patterns while migrating incrementally. Both properties work interchangeably once adapters are configured.

### System.Web adapters pros and cons

| Pros                                 | Cons                                    |
| ------------------------------------ | --------------------------------------- |
| Minimal code changes required        | Performance overhead                    |
| Works with existing shared libraries | Not thread-safe in all scenarios        |
| Enables incremental migration        | Requires System.Web adapters dependency |
| Maintains familiar patterns          | Should be temporary solution            |
| Good for large codebases             | Less testable than DI patterns          |

### Setting up static principal support

To enable static principal support ([ClaimsPrincipal.Current](/en-us/dotnet/api/system.security.claims.claimsprincipal.current#system-security-claims-claimsprincipal-current) and [Thread.CurrentPrincipal](/en-us/dotnet/api/system.threading.thread.currentprincipal#system-threading-thread-currentprincipal)) with System.Web adapters, endpoints must be annotated with the `SetThreadCurrentPrincipalAttribute` metadata:

```
// Add to controller or action
[SetThreadCurrentPrincipal]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        // Both ClaimsPrincipal.Current and Thread.CurrentPrincipal are now available
        var user1 = ClaimsPrincipal.Current;
        var user2 = Thread.CurrentPrincipal;
        return View();
    }
}
```

### When to use System.Web adapters

* You have extensive static principal usage across shared libraries
* You're doing an incremental migration
* You can't afford to rewrite all static principal access code immediately
* You need to maintain compatibility with existing ASP.NET Framework code
* You understand the performance and threading implications

## Migration considerations

### Performance implications

* **Native ASP.NET Core patterns** provide the best performance with no overhead
* **System.Web adapters** introduce some performance overhead but enable gradual migration
* **Static variables** should be avoided as they can cause memory leaks and threading issues

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/docker-https
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/docker-https.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Hosting ASP.NET Core images with Docker over HTTPS

By [Rick Anderson](https://twitter.com/RickAndMSFT)

ASP.NET Core uses [HTTPS by default](enforcing-ssl?view=aspnetcore-10.0). [HTTPS](https://en.wikipedia.org/wiki/HTTPS) relies on [certificates](https://en.wikipedia.org/wiki/Public_key_certificate) for trust, identity, and encryption.

This document explains how to run pre-built container images with HTTPS using the [.NET command-line interface (CLI)](/en-us/dotnet/core/tools/). For instructions on how to run Docker in development with Visual Studio, see [Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md).

This sample requires [Docker 17.06](https://docs.docker.com/release-notes/docker-ce) or later of the [Docker client](https://www.docker.com/products/docker).

## Prerequisites

The current [.NET SDK](https://dotnet.microsoft.com/download).

## Certificates

A certificate from a [certificate authority](https://wikipedia.org/wiki/Certificate_authority) is required for [production hosting](https://blogs.msdn.microsoft.com/webdev/2017/11/29/configuring-https-in-asp-net-core-across-different-platforms/) for a domain. [Let's Encrypt](https://letsencrypt.org/) is a certificate authority that offers free certificates.

This document uses [self-signed development certificates](https://en.wikipedia.org/wiki/Self-signed_certificate) for hosting pre-built images over `localhost`. The instructions are similar to using production certificates. The certificate generated by `dotnet dev-certs` is for use with `localhost` only and should ***not*** be used in an environment like Kubernetes. To support HTTPS within a Kubernetes cluster, use the tools provided by [Manage TLS Certificates in a Cluster](https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/) to setup TLS within pods.

Use [`dotnet dev-certs`](/en-us/dotnet/core/tools/dotnet-dev-certs) to create self-signed certificates for development and testing.

For production certs:

* The `dotnet dev-certs` tool is not required.
* Certificates do not need to be stored in the location used in the instructions. Any location should work, although storing certs within your site directory is not recommended.

The instructions contained in the following section volume mount certificates into containers using Docker's `-v` command-line option. You could add certificates into container images with a `COPY` command in a *Dockerfile*, but it's not recommended. Copying certificates into an image isn't recommended for the following reasons:

* It's difficult to use the same image for testing with developer certificates.
* It's difficult to use the same image for Hosting with production certificates.
* There is significant risk of certificate disclosure.

## Running pre-built container images with HTTPS

Use the following instructions for your operating system configuration.

### Windows using Linux containers

Generate a certificate and configure the local machine:

```
dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p <CREDENTIAL_PLACEHOLDER>
dotnet dev-certs https --trust
```

In the preceding commands, replace `<CREDENTIAL_PLACEHOLDER>` with a password.

Run the container image with ASP.NET Core configured for HTTPS in a command shell:

```
docker pull mcr.microsoft.com/dotnet/samples:aspnetapp
docker run --rm -it -p 8000:80 -p 8001:443 -e ASPNETCORE_URLS="https://+;http://+" -e ASPNETCORE_HTTPS_PORTS=443 -e ASPNETCORE_Kestrel__Certificates__Default__Password="<CREDENTIAL_PLACEHOLDER>" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v %USERPROFILE%\.aspnet\https:/https/ mcr.microsoft.com/dotnet/samples:aspnetapp
```

In the preceding code, replace `<CREDENTIAL_PLACEHOLDER>` with the password. The password must match the password used for the certificate.

When using [PowerShell](/en-us/powershell/scripting/overview), replace `%USERPROFILE%` with `$env:USERPROFILE`.

The certificate in this case must be a `.pfx` file. Utilizing a `.crt` or `.key` file with or without the password isn't supported with the sample container. For example, when specifying a `.crt` file, the container may return error messages such as 'The server mode SSL must use a certificate with the associated private key.'. When using [WSL](/en-us/windows/wsl/about), validate the mount path to ensure that the certificate loads correctly.

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### macOS or Linux

Generate certificate and configure local machine:

```
dotnet dev-certs https -ep ${HOME}/.aspnet/https/aspnetapp.pfx -p <CREDENTIAL_PLACEHOLDER>
dotnet dev-certs https --trust
```

On Linux, `dotnet dev-certs https --trust` requires .NET 9 or later SDK. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

In the preceding commands, replace `<CREDENTIAL_PLACEHOLDER>` with a password.

Run the container image with ASP.NET Core configured for HTTPS:

```
docker pull mcr.microsoft.com/dotnet/samples:aspnetapp
docker run --rm -it -p 8000:80 -p 8001:443 -e ASPNETCORE_URLS="https://+;http://+" -e ASPNETCORE_HTTPS_PORTS=443 -e ASPNETCORE_Kestrel__Certificates__Default__Password="<CREDENTIAL_PLACEHOLDER>" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v ${HOME}/.aspnet/https:/https/ mcr.microsoft.com/dotnet/samples:aspnetapp
```

In the preceding code, replace `<CREDENTIAL_PLACEHOLDER>` with the password. The password must match the password used for the certificate.

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### Windows using Windows containers

Generate certificate and configure local machine:

```
dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p <CREDENTIAL_PLACEHOLDER>
dotnet dev-certs https --trust
```

In the preceding commands, replace `<CREDENTIAL_PLACEHOLDER>` with a password. When using [PowerShell](/en-us/powershell/scripting/overview), replace `%USERPROFILE%` with `$env:USERPROFILE`.

Run the container image with ASP.NET Core configured for HTTPS:

```
docker pull mcr.microsoft.com/dotnet/samples:aspnetapp
docker run --rm -it -p 8000:80 -p 8001:443 -e ASPNETCORE_URLS="https://+;http://+" -e ASPNETCORE_HTTPS_PORTS=443 -e ASPNETCORE_Kestrel__Certificates__Default__Password="<CREDENTIAL_PLACEHOLDER>" -e ASPNETCORE_Kestrel__Certificates__Default__Path=c:\https\aspnetapp.pfx -v %USERPROFILE%\.aspnet\https:C:\https\ --user ContainerAdministrator mcr.microsoft.com/dotnet/samples:aspnetapp
```

***NOTE:*** `<CREDENTIAL_PLACEHOLDER>` is a placeholder for the Kestrel certificates default password.

The password must match the password used for the certificate. When using [PowerShell](/en-us/powershell/scripting/overview), replace `%USERPROFILE%` with `$env:USERPROFILE`.

## Developing ASP.NET Core Applications with Docker over HTTPS

See [Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/master/samples/run-aspnetcore-https-development.md) for information and samples on how to develop ASP.NET Core applications with HTTPS in Docker containers.

## See also

* [Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md)
* [`dotnet dev-certs`](/en-us/dotnet/core/tools/dotnet-dev-certs)

ASP.NET Core uses [HTTPS by default](enforcing-ssl?view=aspnetcore-10.0). [HTTPS](https://en.wikipedia.org/wiki/HTTPS) relies on [certificates](https://en.wikipedia.org/wiki/Public_key_certificate) for trust, identity, and encryption.

This document explains how to run pre-built container images with HTTPS using the [.NET command-line interface (CLI)](/en-us/dotnet/core/tools/). For instructions on how to run Docker in development with Visual Studio, see [Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md).

This sample requires [Docker 17.06](https://docs.docker.com/release-notes/docker-ce) or later of the [Docker client](https://www.docker.com/products/docker).

## Prerequisites

The current [.NET SDK](https://dotnet.microsoft.com/download).

## Certificates

A certificate from a [certificate authority](https://wikipedia.org/wiki/Certificate_authority) is required for [production hosting](https://blogs.msdn.microsoft.com/webdev/2017/11/29/configuring-https-in-asp-net-core-across-different-platforms/) for a domain. [Let's Encrypt](https://letsencrypt.org/) is a certificate authority that offers free certificates.

This document uses [self-signed development certificates](https://en.wikipedia.org/wiki/Self-signed_certificate) for hosting pre-built images over `localhost`. The instructions are similar to using production certificates. The certificate generated by `dotnet dev-certs` is for use with `localhost` only and should ***not*** be used in an environment like Kubernetes. To support HTTPS within a Kubernetes cluster, use the tools provided by [Manage TLS Certificates in a Cluster](https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/) to setup TLS within pods.

Use [`dotnet dev-certs`](/en-us/dotnet/core/tools/dotnet-dev-certs) to create self-signed certificates for development and testing.

For production certs:

* The `dotnet dev-certs` tool is not required.
* Certificates do not need to be stored in the location used in the instructions. Any location should work, although storing certs within your site directory is not recommended.

The instructions contained in the following section volume mount certificates into containers using Docker's `-v` command-line option. You could add certificates into container images with a `COPY` command in a *Dockerfile*, but it's not recommended. Copying certificates into an image isn't recommended for the following reasons:

* It's difficult to use the same image for testing with developer certificates.
* It's difficult to use the same image for Hosting with production certificates.
* There is significant risk of certificate disclosure.

## Running pre-built container images with HTTPS

Use the following instructions for your operating system configuration.

### Windows using Linux containers

Generate a certificate and configure the local machine:

```
dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p <CREDENTIAL_PLACEHOLDER>
dotnet dev-certs https --trust
```

In the preceding commands, replace `<CREDENTIAL_PLACEHOLDER>` with a password.

Run the container image with ASP.NET Core configured for HTTPS in a command shell:

```
docker pull mcr.microsoft.com/dotnet/samples:aspnetapp
docker run --rm -it -p 8000:80 -p 8001:443 -e ASPNETCORE_URLS="https://+:443;http://+" -e ASPNETCORE_Kestrel__Certificates__Default__Password="<CREDENTIAL_PLACEHOLDER>" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v %USERPROFILE%\.aspnet\https:/https/ mcr.microsoft.com/dotnet/samples:aspnetapp
```

In the preceding code, replace `<CREDENTIAL_PLACEHOLDER>` with the password. The password must match the password used for the certificate.

When using [PowerShell](/en-us/powershell/scripting/overview), replace `%USERPROFILE%` with `$env:USERPROFILE`.

Note: The certificate in this case must be a `.pfx` file. Utilizing a `.crt` or `.key` file with or without the password isn't supported with the sample container. For example, when specifying a `.crt` file, the container may return error messages such as 'The server mode SSL must use a certificate with the associated private key.'. When using [WSL](/en-us/windows/wsl/about), validate the mount path to ensure that the certificate loads correctly.

### macOS or Linux

Generate certificate and configure local machine:

```
dotnet dev-certs https -ep ${HOME}/.aspnet/https/aspnetapp.pfx -p <CREDENTIAL_PLACEHOLDER>
dotnet dev-certs https --trust
```

`dotnet dev-certs https --trust` is only supported on macOS and Windows. You need to trust certs on Linux in the way that is supported by your distribution. It is likely that you need to trust the certificate in your browser.

In the preceding commands, replace `<CREDENTIAL_PLACEHOLDER>` with a password.

Run the container image with ASP.NET Core configured for HTTPS:

```
docker pull mcr.microsoft.com/dotnet/samples:aspnetapp
docker run --rm -it -p 8000:80 -p 8001:443 -e ASPNETCORE_URLS="https://+:443;http://+" -e ASPNETCORE_Kestrel__Certificates__Default__Password="<CREDENTIAL_PLACEHOLDER>" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v ${HOME}/.aspnet/https:/https/ mcr.microsoft.com/dotnet/samples:aspnetapp
```

In the preceding code, replace `<CREDENTIAL_PLACEHOLDER>` with the password. The password must match the password used for the certificate.

### Windows using Windows containers

Generate certificate and configure local machine:

```
dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p <CREDENTIAL_PLACEHOLDER>
dotnet dev-certs https --trust
```

In the preceding commands, replace `<CREDENTIAL_PLACEHOLDER>` with a password. When using [PowerShell](/en-us/powershell/scripting/overview), replace `%USERPROFILE%` with `$env:USERPROFILE`.

Run the container image with ASP.NET Core configured for HTTPS:

```
docker pull mcr.microsoft.com/dotnet/samples:aspnetapp
docker run --rm -it -p 8000:80 -p 8001:443 -e ASPNETCORE_URLS="https://+:443;http://+" -e ASPNETCORE_Kestrel__Certificates__Default__Password="<CREDENTIAL_PLACEHOLDER>" -e ASPNETCORE_Kestrel__Certificates__Default__Path=c:\https\aspnetapp.pfx -v %USERPROFILE%\.aspnet\https:C:\https\ --user ContainerAdministrator mcr.microsoft.com/dotnet/samples:aspnetapp
```

***NOTE:*** `<CREDENTIAL_PLACEHOLDER>` is a placeholder for the Kestrel certificates default password.

The password must match the password used for the certificate. When using [PowerShell](/en-us/powershell/scripting/overview), replace `%USERPROFILE%` with `$env:USERPROFILE`.

## Developing ASP.NET Core Applications with Docker over HTTPS

See [Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/master/samples/run-aspnetcore-https-development.md) for information and samples on how to develop ASP.NET Core applications with HTTPS in Docker containers.

## See also

* [Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md)
* [`dotnet dev-certs`](/en-us/dotnet/core/tools/dotnet-dev-certs)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/options
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel/options.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure options for the ASP.NET Core Kestrel web server

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The Kestrel web server has constraint configuration options that are especially useful in Internet-facing deployments. To configure Kestrel configuration options, call [ConfigureKestrel](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.configurekestrel) in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    // ...
});
```

Set constraints on the [KestrelServerOptions.Limits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.limits) property. This property holds an instance of the [KestrelServerLimits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits) class.

## General limits

### Keep-alive timeout

[KeepAliveTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.keepalivetimeout#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-keepalivetimeout) gets or sets the [keep-alive timeout](https://www.rfc-editor.org/rfc/rfc9112.html#name-keep-alive-connections):

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
});
```

This timeout is not enforced when a debugger is attached to the Kestrel process.

### Maximum client connections

[MaxConcurrentConnections](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentconnections#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxconcurrentconnections) gets or sets the maximum number of open connections:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
});
```

[MaxConcurrentUpgradedConnections](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentupgradedconnections#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxconcurrentupgradedconnections) gets or sets the maximum number of open, upgraded connections:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
});
```

An upgraded connection is one that has been switched from HTTP to another protocol, such as WebSockets. After a connection is upgraded, it isn't counted against the `MaxConcurrentConnections` limit.

### Maximum request body size

[MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxrequestbodysize#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxrequestbodysize) gets or sets the maximum allowed size of any request body in bytes.

The recommended approach to override the limit in an ASP.NET Core MVC app is to use the [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) attribute on an action method:

```
[RequestSizeLimit(100_000_000)]
public IActionResult Get()
```

The following example configures `MaxRequestBodySize` for all requests:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxRequestBodySize = 100_000_000;
});
```

The following example configures `MaxRequestBodySize` for a specific request using [IHttpMaxRequestBodySizeFeature](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature) in a custom middleware:

```
app.Use(async (context, next) =>
{
    var httpMaxRequestBodySizeFeature = context.Features.Get<IHttpMaxRequestBodySizeFeature>();

    if (httpMaxRequestBodySizeFeature is not null)
        httpMaxRequestBodySizeFeature.MaxRequestBodySize = 10 * 1024;

    // ...

    await next(context);
});
```

If the app attempts to configure the limit on a request after it starts to read the request, an exception is thrown. Use the [IHttpMaxRequestBodySizeFeature.IsReadOnly](/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpmaxrequestbodysizefeature.isreadonly) property to check if it's safe to set the `MaxRequestBodySize` property.

When an app runs [out-of-process](../../../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model) behind the [ASP.NET Core Module](../../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0), IIS sets the limit and Kestrel's request body size limit is disabled.

### Minimum request body data rate

Kestrel checks every second if data is arriving at the specified rate in bytes/second. If the rate drops below the minimum, the connection is timed out. The grace period is the amount of time Kestrel allows the client to increase its send rate up to the minimum. The rate isn't checked during that time. The grace period helps avoid dropping connections that are initially sending data at a slow rate because of TCP slow-start. A minimum rate also applies to the response.

[MinRequestBodyDataRate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.minrequestbodydatarate#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-minrequestbodydatarate) gets or sets the request body minimum data rate in bytes/second. [MinResponseDataRate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.minresponsedatarate#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-minresponsedatarate) gets or sets the response minimum data rate in bytes/second.

The following example configures `MinRequestBodyDataRate` and `MinResponseDataRate` for all requests:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MinRequestBodyDataRate = new MinDataRate(
        bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate = new MinDataRate(
        bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
});
```

The following example configures `MinRequestBodyDataRate` and `MinResponseDataRate` for a specific request using [IHttpMinRequestBodyDataRateFeature](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminrequestbodydataratefeature) and [IHttpMinResponseDataRateFeature](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminresponsedataratefeature) in a custom middleware:

```
app.Use(async (context, next) =>
{
    var httpMinRequestBodyDataRateFeature = context.Features
        .Get<IHttpMinRequestBodyDataRateFeature>();

    if (httpMinRequestBodyDataRateFeature is not null)
    {
        httpMinRequestBodyDataRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }

    var httpMinResponseDataRateFeature = context.Features
        .Get<IHttpMinResponseDataRateFeature>();

    if (httpMinResponseDataRateFeature is not null)
    {
        httpMinResponseDataRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }

    // ...

    await next(context);
});
```

`IHttpMinResponseDataRateFeature` isn't present in [HttpContext.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features#microsoft-aspnetcore-http-httpcontext-features) for HTTP/2 requests. Modifying rate limits on a per-request basis isn't generally supported for HTTP/2 because of the protocol's support for request multiplexing. However, `IHttpMinRequestBodyDataRateFeature` is still present in `HttpContext.Features` for HTTP/2 requests, because the read rate limit can still be *disabled entirely* on a per-request basis by setting [IHttpMinResponseDataRateFeature.MinDataRate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminresponsedataratefeature.mindatarate#microsoft-aspnetcore-server-kestrel-core-features-ihttpminresponsedataratefeature-mindatarate) to `null`, even for an HTTP/2 request. Attempts to read `IHttpMinRequestBodyDataRateFeature.MinDataRate` or attempts to set it to a value other than `null` result in a [NotSupportedException](/en-us/dotnet/api/system.notsupportedexception) for HTTP/2 requests.

Server-wide rate limits configured via [KestrelServerOptions.Limits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelserveroptions.limits#microsoft-aspnetcore-server-kestrel-kestrelserveroptions-limits) still apply to both HTTP/1.x and HTTP/2 connections.

### Request headers timeout

[RequestHeadersTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.requestheaderstimeout#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-requestheaderstimeout) gets or sets the maximum amount of time the server spends receiving request headers:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
});
```

This timeout is not enforced when a debugger is attached to the Kestrel process.

## HTTP/2 limits

The limits in this section are set on [KestrelServerLimits.Http2](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.http2#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-http2).

### Maximum streams per connection

[MaxStreamsPerConnection](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.maxstreamsperconnection#microsoft-aspnetcore-server-kestrel-core-http2limits-maxstreamsperconnection) limits the number of concurrent request streams per HTTP/2 connection. Excess streams are refused:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.MaxStreamsPerConnection = 100;
});
```

### Header table size

[HeaderTableSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.headertablesize#microsoft-aspnetcore-server-kestrel-core-http2limits-headertablesize) limits the size of the header compression tables, in octets, the HPACK encoder and decoder on the server can use. The HPACK decoder decompresses HTTP headers for HTTP/2 connections:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.HeaderTableSize = 4096;
});
```

### Maximum frame size

[MaxFrameSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.maxframesize#microsoft-aspnetcore-server-kestrel-core-http2limits-maxframesize) indicates the size of the largest frame payload that is allowed to be received, in octets:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.MaxFrameSize = 16_384;
});
```

### Maximum request header size

[MaxRequestHeaderFieldSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.maxrequestheaderfieldsize#microsoft-aspnetcore-server-kestrel-core-http2limits-maxrequestheaderfieldsize) indicates the size of the maximum allowed size of a request header field sequence. This limit applies to both name and value sequences in their compressed and uncompressed representations:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.MaxRequestHeaderFieldSize = 8192;
});
```

### Initial connection window size

[InitialConnectionWindowSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialconnectionwindowsize#microsoft-aspnetcore-server-kestrel-core-http2limits-initialconnectionwindowsize) indicates how much request body data the server is willing to receive and buffer at a time aggregated across all requests (streams) per connection:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.InitialConnectionWindowSize = 131_072;
});
```

Requests are also limited by [`InitialStreamWindowSize`](#initial-stream-window-size).

### Initial stream window size

[InitialStreamWindowSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialstreamwindowsize#microsoft-aspnetcore-server-kestrel-core-http2limits-initialstreamwindowsize) indicates how much request body data the server is willing to receive and buffer at a time per stream:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.InitialStreamWindowSize = 98_304;
});
```

Requests are also limited by [`InitialConnectionWindowSize`](#initial-connection-window-size).

### HTTP/2 keep alive ping configuration

Kestrel can be configured to send HTTP/2 pings to connected clients. HTTP/2 pings serve multiple purposes:

* Keep idle connections alive. Some clients and proxy servers close connections that are idle. HTTP/2 pings are considered as activity on a connection and prevent the connection from being closed as idle.
* Close unhealthy connections. Connections where the client doesn't respond to the keep alive ping in the configured time are closed by the server.

There are two configuration options related to HTTP/2 keep alive pings:

* [KeepAlivePingDelay](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.keepalivepingdelay#microsoft-aspnetcore-server-kestrel-core-http2limits-keepalivepingdelay) is a [TimeSpan](/en-us/dotnet/api/system.timespan) that configures the ping interval. The server sends a keep alive ping to the client if it doesn't receive any frames for this period of time. Keep alive pings are disabled when this option is set to [TimeSpan.MaxValue](/en-us/dotnet/api/system.timespan.maxvalue#system-timespan-maxvalue).
* [KeepAlivePingTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.keepalivepingtimeout#microsoft-aspnetcore-server-kestrel-core-http2limits-keepalivepingtimeout) is a [TimeSpan](/en-us/dotnet/api/system.timespan) that configures the ping timeout. If the server doesn't receive any frames, such as a response ping, during this timeout then the connection is closed. Keep alive timeout is disabled when this option is set to [TimeSpan.MaxValue](/en-us/dotnet/api/system.timespan.maxvalue#system-timespan-maxvalue).

The following example sets `KeepAlivePingDelay` and `KeepAlivePingTimeout`:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.KeepAlivePingDelay = TimeSpan.FromSeconds(30);
    serverOptions.Limits.Http2.KeepAlivePingTimeout = TimeSpan.FromMinutes(1);
});
```

## Other options

### Synchronous I/O

[AllowSynchronousIO](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.allowsynchronousio#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-allowsynchronousio) controls whether synchronous I/O is allowed for the request and response.

Warning

A large number of blocking synchronous I/O operations can lead to thread pool starvation, which makes the app unresponsive. Only enable `AllowSynchronousIO` when using a library that doesn't support asynchronous I/O.

The following example enables synchronous I/O:

```
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.AllowSynchronousIO = true;
});
```

For information about other Kestrel options and limits, see:

* [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions)
* [KestrelServerLimits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits)
* [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions)

## Behavior with debugger attached

The following timeout and rate limit options aren't enforced when a debugger is attached to a Kestrel process:

The Kestrel web server has constraint configuration options that are especially useful in Internet-facing deployments.

To provide more configuration after calling [ConfigureWebHostDefaults](/en-us/dotnet/api/microsoft.extensions.hosting.generichostbuilderextensions.configurewebhostdefaults), use [ConfigureKestrel](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.configurekestrel):

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(serverOptions =>
            {
                // Set properties and call methods on options
            })
            .UseStartup<Startup>();
        });
```

Set constraints on the [Limits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.limits#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-limits) property of the [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions) class. The `Limits` property holds an instance of the [KestrelServerLimits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits) class.

The following examples use the [Microsoft.AspNetCore.Server.Kestrel.Core](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core) namespace:

```
using Microsoft.AspNetCore.Server.Kestrel.Core;
```

Note

[KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions) and [endpoint configuration](endpoints?view=aspnetcore-10.0) are configurable from configuration providers. Remaining Kestrel configuration must be configured in C# code.

## General limits

### Keep-alive timeout

[KeepAliveTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.keepalivetimeout#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-keepalivetimeout)

Gets or sets the [keep-alive timeout](https://www.rfc-editor.org/rfc/rfc9112.html#name-keep-alive-connections). Defaults to 2 minutes.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

### Maximum client connections

[MaxConcurrentConnections](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentconnections#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxconcurrentconnections)\
[MaxConcurrentUpgradedConnections](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentupgradedconnections#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxconcurrentupgradedconnections)

The maximum number of concurrent open TCP connections can be set for the entire app with the following code:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

There's a separate limit for connections that have been upgraded from HTTP or HTTPS to another protocol (for example, on a WebSockets request). After a connection is upgraded, it isn't counted against the `MaxConcurrentConnections` limit.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

The maximum number of connections is unlimited (null) by default.

### Maximum request body size

[MaxRequestBodySize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxrequestbodysize#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-maxrequestbodysize)

The default maximum request body size is 30,000,000 bytes, which is approximately 28.6 MB.

The recommended approach to override the limit in an ASP.NET Core MVC app is to use the [RequestSizeLimitAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.requestsizelimitattribute) attribute on an action method:

```
[RequestSizeLimit(100000000)]
public IActionResult MyActionMethod()
```

The following example shows how to configure the constraint for the app on every request:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

Override the setting on a specific request in middleware:

```
app.Run(async (context) =>
{
    context.Features.Get<IHttpMaxRequestBodySizeFeature>()
        .MaxRequestBodySize = 10 * 1024;

    var minRequestRateFeature =
        context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
    var minResponseRateFeature =
        context.Features.Get<IHttpMinResponseDataRateFeature>();

    if (minRequestRateFeature != null)
    {
        minRequestRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }

    if (minResponseRateFeature != null)
    {
        minResponseRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }
```

An exception is thrown if the app configures the limit on a request after the app has started to read the request. There's an `IsReadOnly` property that indicates if the `MaxRequestBodySize` property is in read-only state, meaning it's too late to configure the limit.

When an app runs [out-of-process](../../../host-and-deploy/iis/?view=aspnetcore-10.0#out-of-process-hosting-model) behind the [ASP.NET Core Module](../../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0), Kestrel's request body size limit is disabled. IIS already sets the limit.

### Minimum request body data rate

[MinRequestBodyDataRate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.minrequestbodydatarate#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-minrequestbodydatarate)\
[MinResponseDataRate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.minresponsedatarate#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-minresponsedatarate)

Kestrel checks every second if data is arriving at the specified rate in bytes/second. If the rate drops below the minimum, the connection is timed out. The grace period is the amount of time Kestrel allows the client to increase its send rate up to the minimum. The rate isn't checked during that time. The grace period helps avoid dropping connections that are initially sending data at a slow rate because of TCP slow-start.

The default minimum rate is 240 bytes/second with a 5-second grace period.

A minimum rate also applies to the response. The code to set the request limit and the response limit is the same except for having `RequestBody` or `Response` in the property and interface names.

Here's an example that shows how to configure the minimum data rates in `Program.cs`:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

Override the minimum rate limits per request in middleware:

```
app.Run(async (context) =>
{
    context.Features.Get<IHttpMaxRequestBodySizeFeature>()
        .MaxRequestBodySize = 10 * 1024;

    var minRequestRateFeature =
        context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
    var minResponseRateFeature =
        context.Features.Get<IHttpMinResponseDataRateFeature>();

    if (minRequestRateFeature != null)
    {
        minRequestRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }

    if (minResponseRateFeature != null)
    {
        minResponseRateFeature.MinDataRate = new MinDataRate(
            bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
    }
```

The [IHttpMinResponseDataRateFeature](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminresponsedataratefeature) referenced in the prior sample isn't present in [HttpContext.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features#microsoft-aspnetcore-http-httpcontext-features) for HTTP/2 requests. Modifying rate limits on a per-request basis isn't generally supported for HTTP/2 because of the protocol's support for request multiplexing. However, the [IHttpMinRequestBodyDataRateFeature](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminrequestbodydataratefeature) is still present `HttpContext.Features` for HTTP/2 requests, because the read rate limit can still be *disabled entirely* on a per-request basis by setting [IHttpMinResponseDataRateFeature.MinDataRate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.features.ihttpminresponsedataratefeature.mindatarate#microsoft-aspnetcore-server-kestrel-core-features-ihttpminresponsedataratefeature-mindatarate) to `null` even for an HTTP/2 request. Attempting to read `IHttpMinRequestBodyDataRateFeature.MinDataRate` or attempting to set it to a value other than `null` will result in a [NotSupportedException](/en-us/dotnet/api/system.notsupportedexception) being thrown given an HTTP/2 request.

Server-wide rate limits configured via [KestrelServerOptions.Limits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelserveroptions.limits#microsoft-aspnetcore-server-kestrel-kestrelserveroptions-limits) still apply to both HTTP/1.x and HTTP/2 connections.

### Request headers timeout

[RequestHeadersTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.requestheaderstimeout#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-requestheaderstimeout)

Gets or sets the maximum amount of time the server spends receiving request headers. Defaults to 30 seconds.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.MaxConcurrentConnections = 100;
    serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
    serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
    serverOptions.Limits.MinRequestBodyDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Limits.MinResponseDataRate =
        new MinDataRate(bytesPerSecond: 100, 
            gracePeriod: TimeSpan.FromSeconds(10));
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testPassword");
        });
    serverOptions.Limits.KeepAliveTimeout = 
        TimeSpan.FromMinutes(2);
    serverOptions.Limits.RequestHeadersTimeout = 
        TimeSpan.FromMinutes(1);
})
```

## HTTP/2 limits

The limits in this section are set on [KestrelServerLimits.Http2](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.http2#microsoft-aspnetcore-server-kestrel-core-kestrelserverlimits-http2).

### Maximum streams per connection

[MaxStreamsPerConnection](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.maxstreamsperconnection#microsoft-aspnetcore-server-kestrel-core-http2limits-maxstreamsperconnection)

Limits the number of concurrent request streams per HTTP/2 connection. Excess streams are refused.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.MaxStreamsPerConnection = 100;
});
```

The default value is 100.

### Header table size

[HeaderTableSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.headertablesize#microsoft-aspnetcore-server-kestrel-core-http2limits-headertablesize)

The HPACK decoder decompresses HTTP headers for HTTP/2 connections. `HeaderTableSize` limits the size of the header compression table that the HPACK decoder uses. The value is provided in octets and must be greater than zero (0).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.HeaderTableSize = 4096;
});
```

The default value is 4096.

### Maximum frame size

[MaxFrameSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.maxframesize#microsoft-aspnetcore-server-kestrel-core-http2limits-maxframesize)

Indicates the maximum allowed size of an HTTP/2 connection frame payload received or sent by the server. The value is provided in octets and must be between 2^14 (16,384) and 2^24-1 (16,777,215).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.MaxFrameSize = 16384;
});
```

The default value is 2^14 (16,384).

### Maximum request header size

[MaxRequestHeaderFieldSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.maxrequestheaderfieldsize#microsoft-aspnetcore-server-kestrel-core-http2limits-maxrequestheaderfieldsize)

Indicates the maximum allowed size in octets of request header values. This limit applies to both name and value in their compressed and uncompressed representations. The value must be greater than zero (0).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.MaxRequestHeaderFieldSize = 8192;
});
```

The default value is 8,192.

### Initial connection window size

[InitialConnectionWindowSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialconnectionwindowsize#microsoft-aspnetcore-server-kestrel-core-http2limits-initialconnectionwindowsize)

Indicates the maximum request body data in bytes the server buffers at one time, aggregated across all requests (streams) per connection. Requests are also limited by `Http2.InitialStreamWindowSize`. The value must be greater than or equal to 65,535 and less than 2^31 (2,147,483,648).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.InitialConnectionWindowSize = 131072;
});
```

The default value is 128 KB (131,072).

### Initial stream window size

[InitialStreamWindowSize](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialstreamwindowsize#microsoft-aspnetcore-server-kestrel-core-http2limits-initialstreamwindowsize)

Indicates the maximum request body data in bytes the server buffers at one time per request (stream). Requests are also limited by [`InitialConnectionWindowSize`](#initial-connection-window-size). The value must be greater than or equal to 65,535 and less than 2^31 (2,147,483,648).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.InitialStreamWindowSize = 98304;
});
```

The default value is 96 KB (98,304).

### HTTP/2 keep alive ping configuration

Kestrel can be configured to send HTTP/2 pings to connected clients. HTTP/2 pings serve multiple purposes:

* Keep idle connections alive. Some clients and proxy servers close connections that are idle. HTTP/2 pings are considered as activity on a connection and prevent the connection from being closed as idle.
* Close unhealthy connections. Connections where the client doesn't respond to the keep alive ping in the configured time are closed by the server.

There are two configuration options related to HTTP/2 keep alive pings:

* [KeepAlivePingDelay](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.keepalivepingdelay#microsoft-aspnetcore-server-kestrel-core-http2limits-keepalivepingdelay) is a [TimeSpan](/en-us/dotnet/api/system.timespan) that configures the ping interval. The server sends a keep alive ping to the client if it doesn't receive any frames for this period of time. Keep alive pings are disabled when this option is set to [TimeSpan.MaxValue](/en-us/dotnet/api/system.timespan.maxvalue#system-timespan-maxvalue). The default value is [TimeSpan.MaxValue](/en-us/dotnet/api/system.timespan.maxvalue#system-timespan-maxvalue).
* [KeepAlivePingTimeout](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.keepalivepingtimeout#microsoft-aspnetcore-server-kestrel-core-http2limits-keepalivepingtimeout) is a [TimeSpan](/en-us/dotnet/api/system.timespan) that configures the ping timeout. If the server doesn't receive any frames, such as a response ping, during this timeout then the connection is closed. Keep alive timeout is disabled when this option is set to [TimeSpan.MaxValue](/en-us/dotnet/api/system.timespan.maxvalue#system-timespan-maxvalue). The default value is 20 seconds.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Limits.Http2.KeepAlivePingDelay = TimeSpan.FromSeconds(30);
    serverOptions.Limits.Http2.KeepAlivePingTimeout = TimeSpan.FromSeconds(60);
});
```

## Other options

### Synchronous I/O

[AllowSynchronousIO](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.allowsynchronousio#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-allowsynchronousio) controls whether synchronous I/O is allowed for the request and response. The default value is `false`.

Warning

A large number of blocking synchronous I/O operations can lead to thread pool starvation, which makes the app unresponsive. Only enable `AllowSynchronousIO` when using a library that doesn't support asynchronous I/O.

The following example enables synchronous I/O:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.AllowSynchronousIO = true;
})
```

For information about other Kestrel options and limits, see:

* [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions)
* [KestrelServerLimits](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits)
* [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-3.0
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-3.0.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# What's new in ASP.NET Core 3.0

This article highlights the most significant changes in ASP.NET Core 3.0 with links to relevant documentation.

## Blazor

Blazor is a new framework in ASP.NET Core for building interactive client-side web UI with .NET:

* Create rich interactive UIs using C#.
* Share server-side and client-side app logic written in .NET.
* Render the UI as HTML and CSS for wide browser support, including mobile browsers.

Blazor framework supported scenarios:

* Reusable UI components (Razor components)
* Client-side routing
* Component layouts
* Support for dependency injection
* Forms and validation
* Supply Razor components in Razor class libraries
* JavaScript interop

For more information, see [ASP.NET Core Blazor](../blazor/?view=aspnetcore-10.0).

### Blazor Server

Blazor decouples component rendering logic from how UI updates are applied. Blazor Server provides support for hosting Razor components on the server in an ASP.NET Core app. UI updates are handled over a SignalR connection. Blazor Server is supported in ASP.NET Core 3.0.

### Blazor WebAssembly (Preview)

Blazor apps can also be run directly in the browser using a WebAssembly-based .NET runtime. Blazor WebAssembly is in preview and *not* supported in ASP.NET Core 3.0. Blazor WebAssembly will be supported in a future release of ASP.NET Core.

### Razor components

Blazor apps are built from components. Components are self-contained chunks of user interface (UI), such as a page, dialog, or form. Components are normal .NET classes that define UI rendering logic and client-side event handlers. You can create rich interactive web apps without JavaScript.

Components in Blazor are typically authored using Razor syntax, a natural blend of HTML and C#. Razor components are similar to Razor Pages and MVC views in that they both use Razor. Unlike pages and views, which are based on a request-response model, components are used specifically for handling UI composition.

## gRPC

[gRPC](https://grpc.io/):

* Is a popular, high-performance RPC (remote procedure call) framework.

* Offers an opinionated contract-first approach to API development.

* Uses modern technologies such as:

  * HTTP/2 for transport.
  * Protocol Buffers as the interface description language.
  * Binary serialization format.

* Provides features such as:

  * Authentication
  * Bidirectional streaming and flow control.
  * Cancellation and timeouts.

gRPC functionality in ASP.NET Core 3.0 includes:

* [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore): An ASP.NET Core framework for hosting gRPC services. gRPC on ASP.NET Core integrates with standard ASP.NET Core features like logging, dependency injection (DI), authentication, and authorization.
* [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client): A gRPC client for .NET Core that builds upon the familiar `HttpClient`.
* [Grpc.Net.ClientFactory](https://www.nuget.org/packages/Grpc.Net.ClientFactory): gRPC client integration with `HttpClientFactory`.

For more information, see [Overview for gRPC on .NET](../grpc/?view=aspnetcore-10.0).

## SignalR

See [Update SignalR code](../migration/22-to-30?view=aspnetcore-10.0#signalr) for migration instructions. SignalR now uses `System.Text.Json` to serialize/deserialize JSON messages. See [Switch to Newtonsoft.Json](../migration/22-to-30?view=aspnetcore-10.0#switch-to-newtonsoftjson) for instructions to restore the `Newtonsoft.Json`-based serializer.

In the JavaScript and .NET Clients for SignalR, support was added for automatic reconnection. By default, the client tries to reconnect immediately and retry after 2, 10, and 30 seconds if necessary. If the client successfully reconnects, it receives a new connection ID. Automatic reconnect is opt-in:

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect()
    .build();
```

The reconnection intervals can be specified by passing an array of millisecond-based durations:

```
.withAutomaticReconnect([0, 3000, 5000, 10000, 15000, 30000])
//.withAutomaticReconnect([0, 2000, 10000, 30000]) The default intervals.
```

A custom implementation can be passed in for full control of the reconnection intervals.

If the reconnection fails after the last reconnect interval:

* The client considers the connection is offline.
* The client stops trying to reconnect.

During reconnection attempts, update the app UI to notify the user that the reconnection is being attempted.

To provide UI feedback when the connection is interrupted, the SignalR client API has been expanded to include the following event handlers:

* `onreconnecting`: Gives developers an opportunity to disable UI or to let users know the app is offline.
* `onreconnected`: Gives developers an opportunity to update the UI once the connection is reestablished.

The following code uses `onreconnecting` to update the UI while trying to connect:

```
connection.onreconnecting((error) => {
    const status = `Connection lost due to error "${error}". Reconnecting.`;
    document.getElementById("messageInput").disabled = true;
    document.getElementById("sendButton").disabled = true;
    document.getElementById("connectionStatus").innerText = status;
});
```

The following code uses `onreconnected` to update the UI on connection:

```
connection.onreconnected((connectionId) => {
    const status = `Connection reestablished. Connected.`;
    document.getElementById("messageInput").disabled = false;
    document.getElementById("sendButton").disabled = false;
    document.getElementById("connectionStatus").innerText = status;
});
```

SignalR 3.0 or later provides a custom resource to authorization handlers when a hub method requires authorization. The resource is an instance of `HubInvocationContext`. The `HubInvocationContext` includes the:

* `HubCallerContext`
* Name of the hub method being invoked.
* Arguments to the hub method.

Consider the following example of a chat room app allowing multiple organization sign-in via Azure Active Directory. Anyone with a Microsoft account can sign in to chat, but only members of the owning organization can ban users or view users' chat histories. The app could restrict certain functionality from specific users.

```
public class DomainRestrictedRequirement :
    AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>,
    IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
        DomainRestrictedRequirement requirement,
        HubInvocationContext resource)
    {
        if (context.User?.Identity?.Name == null)
        {
            return Task.CompletedTask;
        }

        if (IsUserAllowedToDoThis(resource.HubMethodName, context.User.Identity.Name))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }

    private bool IsUserAllowedToDoThis(string hubMethodName, string currentUsername)
    {
        if (hubMethodName.Equals("banUser", StringComparison.OrdinalIgnoreCase))
        {
            return currentUsername.Equals("bob42@jabbr.net", StringComparison.OrdinalIgnoreCase);
        }

        return currentUsername.EndsWith("@jabbr.net", StringComparison.OrdinalIgnoreCase);
    }
}
```

In the preceding code, `DomainRestrictedRequirement` serves as a custom `IAuthorizationRequirement`. Because the `HubInvocationContext` resource parameter is being passed in, the internal logic can:

* Inspect the context in which the Hub is being called.
* Make decisions on allowing the user to execute individual Hub methods.

Individual Hub methods can be marked with the name of the policy the code checks at run-time. As clients attempt to call individual Hub methods, the `DomainRestrictedRequirement` handler runs and controls access to the methods. Based on the way the `DomainRestrictedRequirement` controls access:

* All logged-in users can call the `SendMessage` method.
* Only users who have logged in with a `@jabbr.net` email address can view users' histories.
* Only `bob42@jabbr.net` can ban users from the chat room.

```
[Authorize]
public class ChatHub : Hub
{
    public void SendMessage(string message)
    {
    }

    [Authorize("DomainRestricted")]
    public void BanUser(string username)
    {
    }

    [Authorize("DomainRestricted")]
    public void ViewUserHistory(string username)
    {
    }
}
```

Creating the `DomainRestricted` policy might involve:

* In `Startup.cs`, adding the new policy.
* Provide the custom `DomainRestrictedRequirement` requirement as a parameter.
* Registering `DomainRestricted` with the authorization middleware.

```
services
    .AddAuthorization(options =>
    {
        options.AddPolicy("DomainRestricted", policy =>
        {
            policy.Requirements.Add(new DomainRestrictedRequirement());
        });
    });
```

SignalR hubs use [Endpoint Routing](../fundamentals/routing?view=aspnetcore-10.0). SignalR hub connection was previously done explicitly:

```
app.UseSignalR(routes =>
{
    routes.MapHub<ChatHub>("hubs/chat");
});
```

In the previous version, developers needed to wire up controllers, Razor pages, and hubs in a variety of places. Explicit connection results in a series of nearly-identical routing segments:

```
app.UseSignalR(routes =>
{
    routes.MapHub<ChatHub>("hubs/chat");
});

app.UseRouting(routes =>
{
    routes.MapRazorPages();
});
```

SignalR 3.0 hubs can be routed via endpoint routing. With endpoint routing, typically all routing can be configured in `UseRouting`:

```
app.UseRouting(routes =>
{
    routes.MapRazorPages();
    routes.MapHub<ChatHub>("hubs/chat");
});
```

ASP.NET Core 3.0 SignalR added:

Client-to-server streaming. With client-to-server streaming, server-side methods can take instances of either an `IAsyncEnumerable<T>` or `ChannelReader<T>`. In the following C# sample, the `UploadStream` method on the Hub will receive a stream of strings from the client:

```
public async Task UploadStream(IAsyncEnumerable<string> stream)
{
    await foreach (var item in stream)
    {
        // process content
    }
}
```

.NET client apps can pass either an `IAsyncEnumerable<T>` or `ChannelReader<T>` instance as the `stream` argument of the `UploadStream` Hub method above.

After the `for` loop has completed and the local function exits, the stream completion is sent:

```
async IAsyncEnumerable<string> clientStreamData()
{
    for (var i = 0; i < 5; i++)
    {
        var data = await FetchSomeData();
        yield return data;
    }
}

await connection.SendAsync("UploadStream", clientStreamData());
```

JavaScript client apps use the SignalR `Subject` (or an [RxJS Subject](https://rxjs.dev/api/index/class/Subject)) for the `stream` argument of the `UploadStream` Hub method above.

```
let subject = new signalR.Subject();
await connection.send("StartStream", "MyAsciiArtStream", subject);
```

The JavaScript code could use the `subject.next` method to handle strings as they are captured and ready to be sent to the server.

```
subject.next("example");
subject.complete();
```

Using code like the two preceding snippets, real-time streaming experiences can be created.

## New JSON serialization

ASP.NET Core 3.0 now uses [System.Text.Json](/en-us/dotnet/api/system.text.json) by default for JSON serialization:

* Reads and writes JSON asynchronously.
* Is optimized for UTF-8 text.
* Typically higher performance than `Newtonsoft.Json`.

To add Json.NET to ASP.NET Core 3.0, see [Add Newtonsoft.Json-based JSON format support](../web-api/advanced/formatting?view=aspnetcore-10.0#add-newtonsoftjson-based-json-format-support).

## New Razor directives

The following list contains new Razor directives:

* [`@attribute`](../mvc/views/razor?view=aspnetcore-10.0#attribute): The `@attribute` directive applies the given attribute to the class of the generated page or view. For example, `@attribute [Authorize]`.
* [`@implements`](../mvc/views/razor?view=aspnetcore-10.0#implements): The `@implements` directive implements an interface for the generated class. For example, `@implements IDisposable`.

## IdentityServer4 supports authentication and authorization for web APIs and SPAs

ASP.NET Core 3.0 offers authentication in Single Page Apps (SPAs) using the support for web API authorization. ASP.NET Core Identity for authenticating and storing users is combined with [IdentityServer4](https://identityserver.io/) for implementing OpenID Connect.

IdentityServer4 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core 3.0. It enables the following security features:

* Authentication as a Service (AaaS)
* Single sign-on/off (SSO) over multiple application types
* Access control for APIs
* Federation Gateway

For more information, see [the IdentityServer4 documentation](https://identityserver4.readthedocs.io/en/latest/) or [Authentication and authorization for SPAs](../security/authentication/identity-api-authorization?view=aspnetcore-10.0).

## Certificate and Kerberos authentication

Certificate authentication requires:

* Configuring the server to accept certificates.
* Adding the authentication middleware in `Startup.Configure`.
* Adding the certificate authentication service in `Startup.ConfigureServices`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
            .AddCertificate();
    // Other service configuration removed.
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();
    // Other app configuration removed.
}
```

Options for certificate authentication include the ability to:

* Accept self-signed certificates.
* Check for certificate revocation.
* Check that the proffered certificate has the right usage flags in it.

A default user principal is constructed from the certificate properties. The user principal contains an event that enables supplementing or replacing the principal. For more information, see [Configure certificate authentication in ASP.NET Core](../security/authentication/certauth?view=aspnetcore-10.0).

[Windows Authentication](/en-us/windows-server/security/windows-authentication/windows-authentication-overview) has been extended onto Linux and macOS. In previous versions, Windows Authentication was limited to [IIS](../host-and-deploy/iis/?view=aspnetcore-10.0) and [HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0). In ASP.NET Core 3.0, [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) has the ability to use Negotiate, [Kerberos](/en-us/windows-server/security/kerberos/kerberos-authentication-overview), and [NTLM on Windows](/en-us/windows-server/security/kerberos/ntlm-overview), Linux, and macOS for Windows domain-joined hosts. Kestrel support of these authentication schemes is provided by the [Microsoft.AspNetCore.Authentication.Negotiate NuGet](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Negotiate) package. As with the other authentication services, configure authentication app wide, then configure the service:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
        .AddNegotiate();
    // Other service configuration removed.
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();
    // Other app configuration removed.
}
```

Host requirements:

* Windows hosts must have [Service Principal Names](/en-us/windows/win32/ad/service-principal-names) (SPNs) added to the user account hosting the app.

* Linux and macOS machines must be joined to the domain.

  * SPNs must be created for the web process.
  * [Keytab files](/en-us/archive/blogs/pie/all-you-need-to-know-about-keytab-files) must be generated and configured on the host machine.

For more information, see [Configure Windows Authentication in ASP.NET Core](../security/authentication/windowsauth?view=aspnetcore-10.0).

## Template changes

The web UI templates (Razor Pages, MVC with controller and views) have the following removed:

* The cookie consent UI is no longer included. To enable the cookie consent feature in an ASP.NET Core 3.0 template-generated app, see [General Data Protection Regulation (GDPR) support in ASP.NET Core](../security/gdpr?view=aspnetcore-10.0).
* Scripts and related static assets are now referenced as local files instead of using CDNs. For more information, see [Scripts and related static assets are now referenced as local files instead of using CDNs based on the current environment (dotnet/AspNetCore.Docs #14350)](https://github.com/dotnet/AspNetCore.Docs/issues/14350).

The Angular template updated to use Angular 8.

The Razor class library (RCL) template defaults to Razor component development by default. A new template option in Visual Studio provides template support for pages and views. When creating an RCL from the template in a command shell, pass the `--support-pages-and-views` option (`dotnet new razorclasslib --support-pages-and-views`).

## Generic Host

The ASP.NET Core 3.0 templates use [.NET Generic Host in ASP.NET Core](../fundamentals/host/generic-host?view=aspnetcore-10.0). Previous versions used [WebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder). Using the .NET Core Generic Host ([HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder)) provides better integration of ASP.NET Core apps with other server scenarios that aren't web-specific. For more information, see [HostBuilder replaces WebHostBuilder](../migration/22-to-30?view=aspnetcore-2.2#hostbuilder-replaces-webhostbuilder).

### Host configuration

Prior to the release of ASP.NET Core 3.0, environment variables prefixed with `ASPNETCORE_` were loaded for host configuration of the Web Host. In 3.0, `AddEnvironmentVariables` is used to load environment variables prefixed with `DOTNET_` for host configuration with `CreateDefaultBuilder`.

### Changes to Startup constructor injection

The Generic Host only supports the following types for `Startup` constructor injection:

* [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment)
* `IWebHostEnvironment`
* [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration)

All services can still be injected directly as arguments to the `Startup.Configure` method. For more information, see [Generic Host restricts Startup constructor injection (aspnet/Announcements #353)](https://github.com/aspnet/Announcements/issues/353).

## Kestrel

* Kestrel configuration has been updated for the migration to the Generic Host. In 3.0, Kestrel is configured on the web host builder provided by `ConfigureWebHostDefaults`.
* Connection Adapters have been removed from Kestrel and replaced with connection middleware, which is similar to HTTP middleware in the ASP.NET Core pipeline but for lower-level connections.
* The Kestrel transport layer has been exposed as a public interface in `Connections.Abstractions`.
* Ambiguity between headers and trailers has been resolved by moving trailing headers to a new collection.
* Synchronous I/O APIs, such as `HttpRequest.Body.Read`, are a common source of thread starvation leading to app crashes. In 3.0, `AllowSynchronousIO` is disabled by default.

For more information, see [Migrate from ASP.NET Core 2.2 to 3.0](../migration/22-to-30?view=aspnetcore-10.0#kestrel).

## HTTP/2 enabled by default

HTTP/2 is enabled by default in Kestrel for HTTPS endpoints. HTTP/2 support for IIS or HTTP.sys is enabled when supported by the operating system.

## EventCounters on request

The Hosting EventSource, `Microsoft.AspNetCore.Hosting`, emits the following new [EventCounter](/en-us/dotnet/api/system.diagnostics.tracing.eventcounter) types related to incoming requests:

* `requests-per-second`
* `total-requests`
* `current-requests`
* `failed-requests`

## Endpoint routing

Endpoint Routing, which allows frameworks (for example, MVC) to work well with middleware, is enhanced:

* The order of middleware and endpoints is configurable in the request processing pipeline of `Startup.Configure`.
* Endpoints and middleware compose well with other ASP.NET Core-based technologies, such as Health Checks.
* Endpoints can implement a policy, such as CORS or authorization, in both middleware and MVC.
* Filters and attributes can be placed on methods in controllers.

For more information, see [Routing in ASP.NET Core](../fundamentals/routing?view=aspnetcore-10.0#routing-basics).

## Health Checks

Health Checks use endpoint routing with the Generic Host. In `Startup.Configure`, call `MapHealthChecks` on the endpoint builder with the endpoint URL or relative path:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
});
```

Health Checks endpoints can:

* Specify one or more permitted hosts/ports.
* Require authorization.
* Require CORS.

For more information, see the following articles:

* [Migrate from ASP.NET Core 2.2 to 3.0](../migration/22-to-30?view=aspnetcore-10.0#health-checks)
* [Health checks in ASP.NET Core](../host-and-deploy/health-checks?view=aspnetcore-10.0)

## Pipes on HttpContext

It's now possible to read the request body and write the response body using the [System.IO.Pipelines](/en-us/dotnet/api/system.io.pipelines) API. The `HttpRequest.BodyReader` property provides a [PipeReader](/en-us/dotnet/api/system.io.pipelines.pipereader) that can be used to read the request body. The `HttpResponse.BodyWriter` property provides a [PipeWriter](/en-us/dotnet/api/system.io.pipelines.pipewriter) that can be used to write the response body. `HttpRequest.BodyReader` is an analogue of the `HttpRequest.Body` stream. `HttpResponse.BodyWriter` is an analogue of the `HttpResponse.Body` stream.

## Improved error reporting in IIS

Startup errors when hosting ASP.NET Core apps in IIS now produce richer diagnostic data. These errors are reported to the Windows Event Log with stack traces wherever applicable. In addition, all warnings, errors, and unhandled exceptions are logged to the Windows Event Log.

## Worker Service and Worker SDK

.NET Core 3.0 introduces the new Worker Service app template. This template provides a starting point for writing long running services in .NET Core.

For more information, see:

* [.NET Core Workers as Windows Services](https://devblogs.microsoft.com/aspnet/net-core-workers-as-windows-services/)
* [Background tasks with hosted services in ASP.NET Core](../fundamentals/host/hosted-services?view=aspnetcore-10.0)
* [Host ASP.NET Core in a Windows Service](../host-and-deploy/windows-service?view=aspnetcore-10.0)

## Forwarded headers middleware improvements

In previous versions of ASP.NET Core, calling [UseHsts](/en-us/dotnet/api/microsoft.aspnetcore.builder.hstsbuilderextensions.usehsts) and [UseHttpsRedirection](/en-us/dotnet/api/microsoft.aspnetcore.builder.httpspolicybuilderextensions.usehttpsredirection) were problematic when deployed to an Azure Linux or behind any reverse proxy other than IIS. The fix for previous versions is documented in [Forward the scheme for Linux and non-IIS reverse proxies](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0#forward-the-scheme-for-linux-and-non-iis-reverse-proxies).

This scenario is fixed in ASP.NET Core 3.0. The host enables the [forwarded headers middleware](../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0#forwarded-headers-middleware-options) when the `ASPNETCORE_FORWARDEDHEADERS_ENABLED` environment variable is set to `true`. `ASPNETCORE_FORWARDEDHEADERS_ENABLED` is set to `true` in our container images.

## Performance improvements

ASP.NET Core 3.0 includes many improvements that reduce memory usage and improve throughput:

* Reduction in memory usage when using the built-in dependency injection container for scoped services.
* Reduction in allocations across the framework, including middleware scenarios and routing.
* Reduction in memory usage for WebSocket connections.
* Memory reduction and throughput improvements for HTTPS connections.
* New optimized and fully asynchronous JSON serializer.
* Reduction in memory usage and throughput improvements in form parsing.

## ASP.NET Core 3.0 only runs on .NET Core 3.0

As of ASP.NET Core 3.0, .NET Framework is no longer a supported target framework. Projects targeting .NET Framework can continue in a fully supported fashion using the [.NET Core 2.1 LTS release](https://dotnet.microsoft.com/download/dotnet-core/2.1). Most ASP.NET Core 2.1.x related packages will be supported indefinitely, beyond the three-year LTS period for .NET Core 2.1.

For migration information, see [Port your code from .NET Framework to .NET Core](/en-us/dotnet/core/porting/).

## Use the ASP.NET Core shared framework

The ASP.NET Core 3.0 shared framework, contained in the [Microsoft.AspNetCore.App metapackage](../fundamentals/metapackage-app?view=aspnetcore-10.0), no longer requires an explicit `<PackageReference />` element in the project file. The shared framework is automatically referenced when using the `Microsoft.NET.Sdk.Web` SDK in the project file:

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

## Assemblies removed from the ASP.NET Core shared framework

The most notable assemblies removed from the ASP.NET Core 3.0 shared framework are:

* [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json/) (Json.NET). To add Json.NET to ASP.NET Core 3.0, see [Add Newtonsoft.Json-based JSON format support](../web-api/advanced/formatting?view=aspnetcore-10.0#add-newtonsoftjson-based-json-format-support). ASP.NET Core 3.0 introduces `System.Text.Json` for reading and writing JSON. For more information, see [New JSON serialization](#new-json-serialization) in this document.
* [Entity Framework Core](/en-us/ef/core/)

For a complete list of assemblies removed from the shared framework, see [Assemblies being removed from Microsoft.AspNetCore.App 3.0](https://github.com/dotnet/AspNetCore/issues/3755). For more information on the motivation for this change, see [Breaking changes to Microsoft.AspNetCore.App in 3.0](https://github.com/aspnet/Announcements/issues/325) and [A first look at changes coming in ASP.NET Core 3.0](https://devblogs.microsoft.com/aspnet/a-first-look-at-changes-coming-in-asp-net-core-3-0/).

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/using-browserlink
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/using-browserlink.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Browser link in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Nicolò Carandini](https://github.com/ncarandini) and [Tom Dykstra](https://github.com/tdykstra)

Browser link is a Visual Studio feature. It creates a communication channel between the `Development` environment and one or more web browsers. Use browser link to:

* Refresh your web app in several browsers at once.
* Test across multiple browsers with specific settings such as screen sizes.
* Select UI elements in browsers in real-time, see what markup and source it's correlated to in Visual Studio.
* Conduct real-time browser test automation.

## Runtime compilation vs. hot reload

Use browser Link with [runtime compilation](../mvc/views/view-compilation?view=aspnetcore-10.0) or [hot reload](../test/hot-reload?view=aspnetcore-10.0) to see the effect of run-time changes in Razor (`.cshtml`) files. We recommend hot reload.

## How to use browser link

When you have an ASP.NET Core project open, Visual Studio shows the browser link toolbar control next to the **Debug Type** toolbar control:

From the browser link toolbar control, you can:

* Refresh the web app in several browsers at once.
* Open the **Browser Link Dashboard**.
* Enable or disable **Browser Link**.
* Enable or disable **CSS Hot Reload**.

## Refresh the web app in several browsers at once

To choose a single web browser to launch when starting the project, use the drop-down menu in the **Debug Target** toolbar control:

To open multiple browsers at once, choose **Browse with...** from the same drop-down. Hold down the `Ctrl` key to select the browsers you want, and then click **Browse**:

The following screenshot shows Visual Studio with the Index view open and two open browsers:

Hover over the browser link toolbar control to see the browsers that are connected to the project:

Change the Index view, and all connected browsers are updated when you click the browser link refresh button:

browser link also works with browsers that you launch from outside Visual Studio and navigate to the app URL.

## The browser link dashboard

Open the **browser link dashboard** window from the browser link drop down menu to manage the connection with open browsers:

The connected browsers are shown with the path to the page that each browser is showing:

You can also click on an individual browser name to refresh only that browser.

## Enable or disable browser link

When you re-enable browser link after disabling it, you must refresh the browsers to reconnect them.

## Enable or disable CSS hot reload

When CSS hot reload is enabled, connected browsers are automatically refreshed when you make any change to CSS files.

## How it works

browser link uses [SignalR](../signalr/introduction?view=aspnetcore-10.0) to create a communication channel between Visual Studio and the browser. When browser link is enabled, Visual Studio acts as a SignalR server that multiple clients (browsers) can connect to. browser link also registers a middleware component in the ASP.NET Core request pipeline. This component injects special `<script>` references into every page request from the server. You can see the script references by selecting **View source** in the browser and scrolling to the end of the `<body>` tag content:

```
    <!-- Visual Studio browser link -->
    <script type="application/json" id="__browserLink_initializationData">
        {"requestId":"a717d5a07c1741949a7cefd6fa2bad08","requestMappingFromServer":false}
    </script>
    <script type="text/javascript" src="http://localhost:54139/b6e36e429d034f578ebccd6a79bf19bf/browserLink" async="async"></script>
    <!-- End browser link -->
</body>
```

Your source files aren't modified. The middleware component injects the script references dynamically.

Because the browser-side code is all JavaScript, it works on all browsers that SignalR supports without requiring a browser plug-in.

By [Nicolò Carandini](https://github.com/ncarandini) and [Tom Dykstra](https://github.com/tdykstra)

Browser Link is a Visual Studio feature. It creates a communication channel between the `Development` environment and one or more web browsers. Use Browser Link to:

* Refresh your web app in several browsers at once.
* Test across multiple browsers with specific settings such as screen sizes.
* Select UI elements in browsers in real-time, see what markup and source it's correlated to in Visual Studio.
* Conduct real-time browser test automation. Browser Link is also extensible.

## Browser Link setup

Add the [Microsoft.VisualStudio.Web.BrowserLink](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.BrowserLink/) package to your project. For ASP.NET Core Razor Pages or MVC projects, also enable runtime compilation of Razor (`.cshtml`) files as described in [Razor file compilation in ASP.NET Core](../mvc/views/view-compilation?view=aspnetcore-10.0). Razor syntax changes are applied only when runtime compilation has been enabled.

When converting an ASP.NET Core 2.0 project to ASP.NET Core 2.1 and transitioning to the [Microsoft.AspNetCore.App metapackage](../fundamentals/metapackage-app?view=aspnetcore-10.0), install the [Microsoft.VisualStudio.Web.BrowserLink](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.BrowserLink/) package for Browser Link functionality. The ASP.NET Core 2.1 project templates use the `Microsoft.AspNetCore.App` metapackage by default.

The ASP.NET Core 2.0 **Web Application**, **Empty**, and **Web API** project templates use the [Microsoft.AspNetCore.All metapackage](../fundamentals/metapackage?view=aspnetcore-10.0), which contains a package reference for [Microsoft.VisualStudio.Web.BrowserLink](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.BrowserLink/). Therefore, using the `Microsoft.AspNetCore.All` metapackage requires no further action to make Browser Link available for use.

The ASP.NET Core 1.x **Web Application** project template has a package reference for the [Microsoft.VisualStudio.Web.BrowserLink](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.BrowserLink/) package. Other project types require you to add a package reference to `Microsoft.VisualStudio.Web.BrowserLink`.

### Configuration

Call `UseBrowserLink` in the `Startup.Configure` method:

```
app.UseBrowserLink();
```

The `UseBrowserLink` call is typically placed inside an `if` block that only enables Browser Link in the `Development` environment. For example:

```
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseBrowserLink();
}
```

For more information, see [ASP.NET Core runtime environments](../fundamentals/environments?view=aspnetcore-10.0).

## How to use Browser Link

When you have an ASP.NET Core project open, Visual Studio shows the Browser Link toolbar control next to the **Debug Target** toolbar control:

From the Browser Link toolbar control, you can:

* Refresh the web app in several browsers at once.
* Open the **Browser Link Dashboard**.
* Enable or disable **Browser Link**. Note: Browser Link is disabled by default in Visual Studio.
* Enable or disable [CSS Auto-Sync](#enable-or-disable-css-auto-sync).

## Refresh the web app in several browsers at once

To choose a single web browser to launch when starting the project, use the drop-down menu in the **Debug Target** toolbar control:

To open multiple browsers at once, choose **Browse with...** from the same drop-down. Hold down the `Ctrl` key to select the browsers you want, and then click **Browse**:

The following screenshot shows Visual Studio with the Index view open and two open browsers:

Hover over the Browser Link toolbar control to see the browsers that are connected to the project:

Change the Index view, and all connected browsers are updated when you click the Browser Link refresh button:

Browser Link also works with browsers that you launch from outside Visual Studio and navigate to the app URL.

### The Browser Link Dashboard

Open the **Browser Link Dashboard** window from the Browser Link drop down menu to manage the connection with open browsers:

If no browser is connected, you can start a non-debugging session by selecting the **View in Browser** link:

Otherwise, the connected browsers are shown with the path to the page that each browser is showing:

You can also click on an individual browser name to refresh only that browser.

### Enable or disable Browser Link

When you re-enable Browser Link after disabling it, you must refresh the browsers to reconnect them.

### Enable or disable CSS Auto-Sync

When CSS Auto-Sync is enabled, connected browsers are automatically refreshed when you make any change to CSS files.

## How it works

Browser Link uses [SignalR](../signalr/introduction?view=aspnetcore-10.0) to create a communication channel between Visual Studio and the browser. When Browser Link is enabled, Visual Studio acts as a SignalR server that multiple clients (browsers) can connect to. Browser Link also registers a middleware component in the ASP.NET Core request pipeline. This component injects special `<script>` references into every page request from the server. You can see the script references by selecting **View source** in the browser and scrolling to the end of the `<body>` tag content:

```
    <!-- Visual Studio Browser Link -->
    <script type="application/json" id="__browserLink_initializationData">
        {"requestId":"a717d5a07c1741949a7cefd6fa2bad08","requestMappingFromServer":false}
    </script>
    <script type="text/javascript" src="http://localhost:54139/b6e36e429d034f578ebccd6a79bf19bf/browserLink" async="async"></script>
    <!-- End Browser Link -->
</body>
```

Your source files aren't modified. The middleware component injects the script references dynamically.

Because the browser-side code is all JavaScript, it works on all browsers that SignalR supports without requiring a browser plug-in.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-16

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/examples/overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/examples/overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Upgrade from ASP.NET MVC and Web API to ASP.NET Core MVC

This article shows how to start migrating an ASP.NET MVC project to [ASP.NET Core MVC](../../../mvc/overview?view=aspnetcore-10.0). In the process, it highlights related changes from ASP.NET MVC.

Migrating from ASP.NET MVC is a multi-step process. This article covers:

* Initial setup.
* Basic controllers and views.
* Static content.
* Client-side dependencies.

For migrating configuration and Identity code, see [Migrate configuration to ASP.NET Core](configuration?view=aspnetcore-10.0) and [Migrate Authentication and Identity to ASP.NET Core](identity?view=aspnetcore-10.0).

## Prerequisites

* [Visual Studio 2019 16.4 or later](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the **ASP.NET and web development** workload
* [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)

## Create the starter ASP.NET MVC project

Create an example ASP.NET MVC project in Visual Studio to migrate:

1. From the **File** menu, select **New** > **Project**.
2. Select **ASP.NET Web Application (.NET Framework)** and then select **Next**.
3. Name the project *WebApp1* so the namespace matches the ASP.NET Core project created in the next step. Select **Create**.
4. Select **MVC**, and then select **Create**.

## Create the ASP.NET Core project

Create a new solution with a new ASP.NET Core project to migrate to:

1. Launch a second instance of Visual Studio.
2. From the **File** menu, select **New** > **Project**.
3. Select **ASP.NET Core Web Application** and then select **Next**.
4. In the **Configure your new project** dialog, Name the project *WebApp1*.
5. Set the location to a different directory than the previous project to use the same project name. Using the same namespace makes it easier to copy code between the two projects. Select **Create**.
6. In the **Create a new ASP.NET Core Web Application** dialog, confirm that **.NET Core** and **ASP.NET Core 3.1** are selected. Select the **Web Application (Model-View-Controller)** project template, and select **Create**.

## Configure the ASP.NET Core site to use MVC

In ASP.NET Core 3.0 or later projects, .NET Framework is no longer a supported target framework. Your project must target .NET Core. The ASP.NET Core shared framework, which includes MVC, is part of the .NET Core runtime installation. The shared framework is automatically referenced when using the `Microsoft.NET.Sdk.Web` SDK in the project file:

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

For more information, see [Framework reference](../../22-to-30?view=aspnetcore-10.0#framework-reference).

In ASP.NET Core, the `Startup` class:

* Replaces `Global.asax`.
* Handles all app startup tasks.

For more information, see [App startup in ASP.NET Core](../../../fundamentals/startup?view=aspnetcore-10.0).

In the ASP.NET Core project, open the `Startup.cs` file:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            // 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.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

ASP.NET Core apps must opt in to framework features with middleware. The previous template-generated code adds the following services and middleware:

* The [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews) extension method registers MVC service support for controllers, API-related features, and views. For more information on MVC service registration options, see [MVC service registration](../../22-to-30?view=aspnetcore-10.0#mvc-service-registration)
* The [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) extension method adds the static file handler `Microsoft.AspNetCore.StaticFiles`. The `UseStaticFiles` extension method must be called before `UseRouting`. For more information, see [Serve static files in ASP.NET Core apps](../../../fundamentals/static-files?view=aspnetcore-10.0).
* The [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) extension method adds routing. For more information, see [Routing in ASP.NET Core](../../../fundamentals/routing?view=aspnetcore-10.0).

This existing configuration includes what is needed to migrate the example ASP.NET MVC project. For more information on ASP.NET Core middleware options, see [App startup in ASP.NET Core](../../../fundamentals/startup?view=aspnetcore-10.0).

## Migrate controllers and views

In the ASP.NET Core project, a new empty controller class and view class would be added to serve as placeholders using the same names as the controller and view classes in any ASP.NET MVC project to migrate from.

The ASP.NET Core *WebApp1* project already includes a minimal example controller and view by the same name as the ASP.NET MVC project. So those will serve as placeholders for the ASP.NET MVC controller and views to be migrated from the ASP.NET MVC *WebApp1* project.

1. Copy the methods from the ASP.NET MVC `HomeController` to replace the new ASP.NET Core `HomeController` methods. There's no need to change the return type of the action methods. The ASP.NET MVC built-in template's controller action method return type is [ActionResult](/en-us/dotnet/api/system.web.mvc.actionresult); in ASP.NET Core MVC, the action methods return `IActionResult` instead. `ActionResult` implements `IActionResult`.
2. In the ASP.NET Core project, right-click the *Views/Home* directory, select **Add** > **Existing Item**.
3. In the **Add Existing Item** dialog, navigate to the ASP.NET MVC *WebApp1* project's *Views/Home* directory.
4. Select the `About.cshtml`, `Contact.cshtml`, and `Index.cshtml` Razor view files, then select **Add**, replacing the existing files.

For more information, see [Handle requests with controllers in ASP.NET Core MVC](../../../mvc/controllers/actions?view=aspnetcore-10.0) and [Views in ASP.NET Core MVC](../../../mvc/views/overview?view=aspnetcore-10.0).

## Test each method

Each controller endpoint can be tested, however, layout and styles are covered later in the document.

1. Run the ASP.NET Core app.
2. Invoke the rendered views from the browser on the running ASP.NET Core app by replacing the current port number with the port number used in the ASP.NET Core project. For example, `https://localhost:44375/home/about`.

## Migrate static content

In ASP.NET MVC 5 or earlier, static content was hosted from the web project's root directory and was intermixed with server-side files. In ASP.NET Core, static files are stored within the project's [web root](../../../fundamentals/?view=aspnetcore-10.0#web-root) directory. The default directory is `{content root}/wwwroot`, but it can be changed. For more information, see [Static files in ASP.NET Core](../../../fundamentals/static-files?view=aspnetcore-10.0).

Copy the static content from the ASP.NET MVC *WebApp1* project to the `wwwroot` directory in the ASP.NET Core *WebApp1* project:

1. In the ASP.NET Core project, right-click the `wwwroot` directory, select **Add** > **Existing Item**.
2. In the **Add Existing Item** dialog, navigate to the ASP.NET MVC *WebApp1* project.
3. Select the `favicon.ico` file, then select **Add**, replacing the existing file.

## Migrate the layout files

Copy the ASP.NET MVC project layout files to the ASP.NET Core project:

1. In the ASP.NET Core project, right-click the `Views` directory, select **Add** > **Existing Item**.
2. In the **Add Existing Item** dialog, navigate to the ASP.NET MVC *WebApp1* project's `Views` directory.
3. Select the `_ViewStart.cshtml` file then select **Add**.

Copy the ASP.NET MVC project shared layout files to the ASP.NET Core project:

1. In the ASP.NET Core project, right-click the `Views/Shared` directory, select **Add** > **Existing Item**.
2. In the **Add Existing Item** dialog, navigate to the ASP.NET MVC *WebApp1* project's `Views/Shared` directory.
3. Select the `_Layout.cshtml` file, then select **Add**, replacing the existing file.

In the ASP.NET Core project, open the `_Layout.cshtml` file. Make the following changes to match the completed code shown below:

Update the Bootstrap CSS inclusion to match the completed code below:

1. Replace `@Styles.Render("~/Content/css")` with a `<link>` element to load `bootstrap.css` (see below).
2. Remove `@Scripts.Render("~/bundles/modernizr")`.

The completed replacement markup for Bootstrap CSS inclusion:

```
<link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css"
    integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">
```

Update the jQuery and Bootstrap JavaScript inclusion to match the completed code below:

1. Replace `@Scripts.Render("~/bundles/jquery")` with a `<script>` element (see below).
2. Replace `@Scripts.Render("~/bundles/bootstrap")` with a `<script>` element (see below).

The completed replacement markup for jQuery and Bootstrap JavaScript inclusion:

```
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.js"
    integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
```

The updated `_Layout.cshtml` file is shown below:

```
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
          crossorigin="anonymous">
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    <li>@Html.ActionLink("About", "About", "Home")</li>
                    <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                </ul>
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    <script src="https://code.jquery.com/jquery-3.3.1.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.js"
            integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
    @RenderSection("scripts", required: false)
</body>
</html>
```

View the site in the browser. It should render with the expected styles in place.

## Configure bundling and minification

ASP.NET Core is compatible with several open-source bundling and minification solutions such as [WebOptimizer](https://github.com/ligershark/WebOptimizer) and other similar libraries. ASP.NET Core doesn't provide a native bundling and minification solution. For information on configuring bundling and minification, see [Bundling and Minification](../../../client-side/bundling-and-minification?view=aspnetcore-10.0).

## Solve HTTP 500 errors

There are many problems that can cause an HTTP 500 error message that contains no information on the source of the problem. For example, if the `Views/_ViewImports.cshtml` file contains a namespace that doesn't exist in the project, an HTTP 500 error is generated. By default in ASP.NET Core apps, the `UseDeveloperExceptionPage` extension is added to the `IApplicationBuilder` and executed when the environment is *Development*. This is detailed in the following code:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            // 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.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

ASP.NET Core converts unhandled exceptions into HTTP 500 error responses. Normally, error details aren't included in these responses to prevent disclosure of potentially sensitive information about the server. For more information, see [Developer Exception Page](../../../fundamentals/error-handling?view=aspnetcore-10.0#developer-exception-page).

## Next steps

* \<identity.md>

## Additional resources

* [ASP.NET Core Blazor](../../../blazor/?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)

This article shows how to start migrating an ASP.NET MVC project to [ASP.NET Core MVC](../../../mvc/overview?view=aspnetcore-10.0) 2.2. In the process, it highlights many of the things that have changed from ASP.NET MVC. Migrating from ASP.NET MVC is a multi-step process. This article covers:

* Initial setup
* Basic controllers and views
* Static content
* Client-side dependencies.

For migrating configuration and Identity code, see \<configuration.md> and \<identity.md>.

Note

The version numbers in the samples might not be current, update the projects accordingly.

## Create the starter ASP.NET MVC project

To demonstrate the upgrade, we'll start by creating an ASP.NET MVC app. Create it with the name *WebApp1* so the namespace matches the ASP.NET Core project created in the next step.

*Optional:* Change the name of the Solution from *WebApp1* to *Mvc5*. Visual Studio displays the new solution name (*Mvc5*), which makes it easier to tell this project from the next project.

## Create the ASP.NET Core project

Create a new *empty* ASP.NET Core web app with the same name as the previous project (*WebApp1*) so the namespaces in the two projects match. Having the same namespace makes it easier to copy code between the two projects. Create this project in a different directory than the previous project to use the same name.

* *Optional:* Create a new ASP.NET Core app using the *Web Application* project template. Name the project *WebApp1*, and select an authentication option of **Individual User Accounts**. Rename this app to *FullAspNetCore*. Creating this project saves time in the conversion. The end result can be viewed in the template-generated code, code can be copied to the conversion project, or compared with the template-generated project.

## Configure the site to use MVC

* When targeting .NET Core, the [Microsoft.AspNetCore.App metapackage](../../../fundamentals/metapackage-app?view=aspnetcore-10.0) is referenced by default. This package contains packages commonly used by MVC apps. If targeting .NET Framework, package references must be listed individually in the project file.

`Microsoft.AspNetCore.Mvc` is the ASP.NET Core MVC framework. `Microsoft.AspNetCore.StaticFiles` is the static file handler. ASP.NET Core apps explicitly opt in for middleware, such as for serving static files. For more information, see [Static files](../../../fundamentals/static-files?view=aspnetcore-10.0).

* Open the `Startup.cs` file and change the code to match the following:

```
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseStaticFiles();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

The [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) extension method adds the static file handler. For more information, see [Application Startup](../../../fundamentals/startup?view=aspnetcore-10.0) and [Routing](../../../fundamentals/routing?view=aspnetcore-10.0).

## Add a controller and view

In this section, a minimal controller and view are added to serve as placeholders for the ASP.NET MVC controller and views migrated in the next section.

* Add a `Controllers` directory.

* Add a controller class named `HomeController.cs` to the `Controllers` directory.

- Add a `Views` directory.

- Add a `Views/Home` directory.

- Add a **Razor View** named `Index.cshtml` to the `Views/Home` directory.

The project structure is shown below:

Replace the contents of the `Views/Home/Index.cshtml` file with the following markup:

```
<h1>Hello world!</h1>
```

Run the app.

For more information, see [Controllers](../../../mvc/controllers/actions?view=aspnetcore-10.0) and [Views](../../../mvc/views/overview?view=aspnetcore-10.0).

The following functionality requires migration from the example ASP.NET MVC project to the ASP.NET Core project:

* client-side content (CSS, fonts, and scripts)

* controllers

* views

* models

* bundling

* filters

* Log in/out, Identity (This is done in the next tutorial.)

## Controllers and views

* Copy each of the methods from the ASP.NET MVC `HomeController` to the new `HomeController`. In ASP.NET MVC, the built-in template's controller action method return type is [ActionResult](/en-us/dotnet/api/system.web.mvc.actionresult); in ASP.NET Core MVC, the action methods return `IActionResult` instead. `ActionResult` implements `IActionResult`, so there's no need to change the return type of the action methods.

* Copy the `About.cshtml`, `Contact.cshtml`, and `Index.cshtml` Razor view files from the ASP.NET MVC project to the ASP.NET Core project.

## Test each method

The layout file and styles have not been migrated yet, so the rendered views only contain the content in the view files. The layout file generated links for the `About` and `Contact` views will not be available yet.

Invoke the rendered views from the browser on the running ASP.NET core app by replacing the current port number with the port number used in the ASP.NET core project. For example: `https://localhost:44375/home/about`.

Note the lack of styling and menu items. The styling will be fixed in the next section.

## Static content

In ASP.NET MVC 5 or earlier, static content was hosted from the root of the web project and was intermixed with server-side files. In ASP.NET Core, static content is hosted in the `wwwroot` directory. Copy the static content from the ASP.NET MVC app to the `wwwroot` directory in the ASP.NET Core project. In this sample conversion:

* Copy the `favicon.ico` file from the ASP.NET MVC project to the `wwwroot` directory in the ASP.NET Core project.

The ASP.NET MVC project uses [Bootstrap](https://getbootstrap.com/) for its styling and stores the Bootstrap files in the `Content` and `Scripts` directories. The template, which generated the ASP.NET MVC project, references Bootstrap in the layout file (`Views/Shared/_Layout.cshtml`). The `bootstrap.js` and `bootstrap.css` files could be copied from the ASP.NET MVC project to the `wwwroot` directory in the new project. Instead, this document adds support for Bootstrap (and other client-side libraries) using CDNs, in the next section.

## Migrate the layout file

* Copy the `_ViewStart.cshtml` file from the ASP.NET MVC project's `Views` directory into the ASP.NET Core project's `Views` directory. The `_ViewStart.cshtml` file has not changed in ASP.NET Core MVC.

* Create a `Views/Shared` directory.

* *Optional:* Copy `_ViewImports.cshtml` from the *FullAspNetCore* MVC project's `Views` directory into the ASP.NET Core project's `Views` directory. Remove any namespace declaration in the `_ViewImports.cshtml` file. The `_ViewImports.cshtml` file provides namespaces for all the view files and brings in [Tag Helpers](../../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). Tag Helpers are used in the new layout file. The `_ViewImports.cshtml` file is new for ASP.NET Core.

* Copy the `_Layout.cshtml` file from the ASP.NET MVC project's `Views/Shared` directory into the ASP.NET Core project's `Views/Shared` directory.

Open `_Layout.cshtml` file and make the following changes (the completed code is shown below):

* Replace `@Styles.Render("~/Content/css")` with a `<link>` element to load `bootstrap.css` (see below).

* Remove `@Scripts.Render("~/bundles/modernizr")`.

* Comment out the `@Html.Partial("_LoginPartial")` line (surround the line with `@*...*@`). For more information, see [Migrate Authentication and Identity to ASP.NET Core](identity?view=aspnetcore-10.0)

* Replace `@Scripts.Render("~/bundles/jquery")` with a `<script>` element (see below).

* Replace `@Scripts.Render("~/bundles/bootstrap")` with a `<script>` element (see below).

The replacement markup for Bootstrap CSS inclusion:

```
<link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css"
    integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">
```

The replacement markup for jQuery and Bootstrap JavaScript inclusion:

```
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.js"
    integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
```

The updated `_Layout.cshtml` file is shown below:

```
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
          crossorigin="anonymous">
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    <li>@Html.ActionLink("About", "About", "Home")</li>
                    <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                </ul>
                @*@Html.Partial("_LoginPartial")*@
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    <script src="https://code.jquery.com/jquery-3.3.1.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.js"
            integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
            crossorigin="anonymous"></script>
    @RenderSection("scripts", required: false)
</body>
</html>
```

View the site in the browser. It should now load correctly, with the expected styles in place.

* *Optional:* Try using the new layout file. Copy the layout file from the *FullAspNetCore* project. The new layout file uses [Tag Helpers](../../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) and has other improvements.

## Configure bundling and minification

For information about how to configure bundling and minification, see [Bundling and Minification](../../../client-side/bundling-and-minification?view=aspnetcore-10.0).

## Solve HTTP 500 errors

There are many problems that can cause an HTTP 500 error messages that contain no information on the source of the problem. For example, if the `Views/_ViewImports.cshtml` file contains a namespace that doesn't exist in the project, a HTTP 500 error is generated. By default in ASP.NET Core apps, the `UseDeveloperExceptionPage` extension is added to the `IApplicationBuilder` and executed when the configuration is *Development*. See an example in the following code:

```
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseStaticFiles();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

ASP.NET Core converts unhandled exceptions into HTTP 500 error responses. Normally, error details aren't included in these responses to prevent disclosure of potentially sensitive information about the server. For more information, see [Developer Exception Page](../../../fundamentals/error-handling?view=aspnetcore-10.0#developer-exception-page).

## Additional resources

* [ASP.NET Core Blazor](../../../blazor/?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)

This article shows how to start migrating an ASP.NET MVC project to [ASP.NET Core MVC](../../../mvc/overview?view=aspnetcore-10.0) 2.1. In the process, it highlights many of the things that have changed from ASP.NET MVC. Migrating from ASP.NET MVC is a multi-step process. This article covers:

* Initial setup
* Basic controllers and views
* Static content
* Client-side dependencies.

For migrating configuration and Identity code, see [Migrate configuration to ASP.NET Core](configuration?view=aspnetcore-10.0) and [Migrate Authentication and Identity to ASP.NET Core](identity?view=aspnetcore-10.0).

Note

The version numbers in the samples might not be current, update the projects accordingly.

## Create the starter ASP.NET MVC project

To demonstrate the upgrade, we'll start by creating an ASP.NET MVC app. Create it with the name *WebApp1* so the namespace matches the ASP.NET Core project created in the next step.

*Optional:* Change the name of the Solution from *WebApp1* to *Mvc5*. Visual Studio displays the new solution name (*Mvc5*), which makes it easier to tell this project from the next project.

## Create the ASP.NET Core project

Create a new *empty* ASP.NET Core web app with the same name as the previous project (*WebApp1*) so the namespaces in the two projects match. Having the same namespace makes it easier to copy code between the two projects. Create this project in a different directory than the previous project to use the same name.

* *Optional:* Create a new ASP.NET Core app using the *Web Application* project template. Name the project *WebApp1*, and select an authentication option of **Individual User Accounts**. Rename this app to *FullAspNetCore*. Creating this project saves time in the conversion. The end result can be viewed in the template-generated code, code can be copied to the conversion project, or compared with the template-generated project.

## Configure the site to use MVC

* When targeting .NET Core, the [Microsoft.AspNetCore.App metapackage](../../../fundamentals/metapackage-app?view=aspnetcore-10.0) is referenced by default. This package contains packages commonly used by MVC apps. If targeting .NET Framework, package references must be listed individually in the project file.

`Microsoft.AspNetCore.Mvc` is the ASP.NET Core MVC framework. `Microsoft.AspNetCore.StaticFiles` is the static file handler. ASP.NET Core apps explicitly opt in for middleware, such as for serving static files. For more information, see [Static files](../../../fundamentals/static-files?view=aspnetcore-10.0).

* Open the `Startup.cs` file and change the code to match the following:

```
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseStaticFiles();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

The [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) extension method adds the static file handler. The `UseMvc` extension method adds routing. For more information, see [Application Startup](../../../fundamentals/startup?view=aspnetcore-10.0) and [Routing](../../../fundamentals/routing?view=aspnetcore-10.0).

## Add a controller and view

In this section, a minimal controller and view are added to serve as placeholders for the ASP.NET MVC controller and views migrated in the next section.

* Add a `Controllers` directory.

* Add a **Controller Class** named `HomeController.cs` to the `Controllers` directory.

- Add a `Views` directory.

- Add a `Views/Home` directory.

- Add a **Razor View** named `Index.cshtml` to the `Views/Home` directory.

The project structure is shown below:

Replace the contents of the `Views/Home/Index.cshtml` file with the following markup:

```
<h1>Hello world!</h1>
```

Run the app.

For more information, see [Controllers](../../../mvc/controllers/actions?view=aspnetcore-10.0) and [Views](../../../mvc/views/overview?view=aspnetcore-10.0).

The following functionality requires migration from the example ASP.NET MVC project to the ASP.NET Core project:

* client-side content (CSS, fonts, and scripts)

* controllers

* views

* models

* bundling

* filters

* Log in/out, Identity (This is done in the next tutorial.)

## Controllers and views

* Copy each of the methods from the ASP.NET MVC `HomeController` to the new `HomeController`. In ASP.NET MVC, the built-in template's controller action method return type is [ActionResult](/en-us/dotnet/api/system.web.mvc.actionresult); in ASP.NET Core MVC, the action methods return `IActionResult` instead. `ActionResult` implements `IActionResult`, so there's no need to change the return type of the action methods.

* Copy the `About.cshtml`, `Contact.cshtml`, and `Index.cshtml` Razor view files from the ASP.NET MVC project to the ASP.NET Core project.

## Test each method

The layout file and styles have not been migrated yet, so the rendered views only contain the content in the view files. The layout file generated links for the `About` and `Contact` views will not be available yet.

* Invoke the rendered views from the browser on the running ASP.NET core app by replacing the current port number with the port number used in the ASP.NET core project. For example: `https://localhost:44375/home/about`.

Note the lack of styling and menu items. The styling will be fixed in the next section.

## Static content

In ASP.NET MVC 5 or earlier, static content was hosted from the root of the web project and was intermixed with server-side files. In ASP.NET Core, static content is hosted in the `wwwroot` directory. Copy the static content from the ASP.NET MVC app to the `wwwroot` directory in the ASP.NET Core project. In this sample conversion:

* Copy the `favicon.ico` file from the ASP.NET MVC project to the `wwwroot` directory in the ASP.NET Core project.

The ASP.NET MVC project uses [Bootstrap](https://getbootstrap.com/) for its styling and stores the Bootstrap files in the `Content` and `Scripts` directories. The template, which generated the ASP.NET MVC project, references Bootstrap in the layout file (`Views/Shared/_Layout.cshtml`). The `bootstrap.js` and `bootstrap.css` files could be copied from the ASP.NET MVC project to the `wwwroot` directory in the new project. Instead, this document adds support for Bootstrap (and other client-side libraries) using CDNs, in the next section.

## Migrate the layout file

* Copy the `_ViewStart.cshtml` file from the ASP.NET MVC project's `Views` directory into the ASP.NET Core project's `Views` directory. The `_ViewStart.cshtml` file has not changed in ASP.NET Core MVC.

* Create a `Views/Shared` directory.

* *Optional:* Copy `_ViewImports.cshtml` from the *FullAspNetCore* MVC project's `Views` directory into the ASP.NET Core project's `Views` directory. Remove any namespace declaration in the `_ViewImports.cshtml` file. The `_ViewImports.cshtml` file provides namespaces for all the view files and brings in [Tag Helpers](../../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). Tag Helpers are used in the new layout file. The `_ViewImports.cshtml` file is new for ASP.NET Core.

* Copy the `_Layout.cshtml` file from the ASP.NET MVC project's `Views/Shared` directory into the ASP.NET Core project's `Views/Shared` directory.

Open `_Layout.cshtml` file and make the following changes (the completed code is shown below):

* Replace `@Styles.Render("~/Content/css")` with a `<link>` element to load `bootstrap.css` (see below).

* Remove `@Scripts.Render("~/bundles/modernizr")`.

* Comment out the `@Html.Partial("_LoginPartial")` line (surround the line with `@*...*@`). For more information, see [Migrate Authentication and Identity to ASP.NET Core](identity?view=aspnetcore-10.0)

* Replace `@Scripts.Render("~/bundles/jquery")` with a `<script>` element (see below).

* Replace `@Scripts.Render("~/bundles/bootstrap")` with a `<script>` element (see below).

The replacement markup for Bootstrap CSS inclusion:

```
<link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css"
    integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">
```

The replacement markup for jQuery and Bootstrap JavaScript inclusion:

```
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.js"
    integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
```

The updated `_Layout.cshtml` file is shown below:

```
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
          crossorigin="anonymous">
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    <li>@Html.ActionLink("About", "About", "Home")</li>
                    <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                </ul>
                @*@Html.Partial("_LoginPartial")*@
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    <script src="https://code.jquery.com/jquery-3.3.1.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.js"
            integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
            crossorigin="anonymous"></script>
    @RenderSection("scripts", required: false)
</body>
</html>
```

View the site in the browser. It should now load correctly, with the expected styles in place.

* *Optional:* Try using the new layout file. Copy the layout file from the *FullAspNetCore* project. The new layout file uses [Tag Helpers](../../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) and has other improvements.

## Configure bundling and minification

For information about how to configure bundling and minification, see [Bundling and Minification](../../../client-side/bundling-and-minification?view=aspnetcore-10.0).

## Solve HTTP 500 errors

There are many problems that can cause an HTTP 500 error messages that contain no information on the source of the problem. For example, if the `Views/_ViewImports.cshtml` file contains a namespace that doesn't exist in the project, a HTTP 500 error is generated. By default in ASP.NET Core apps, the `UseDeveloperExceptionPage` extension is added to the `IApplicationBuilder` and executed when the configuration is *Development*. See an example in the following code:

```
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseStaticFiles();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}
```

ASP.NET Core converts unhandled exceptions into HTTP 500 error responses. Normally, error details aren't included in these responses to prevent disclosure of potentially sensitive information about the server. For more information, see [Developer Exception Page](../../../fundamentals/error-handling?view=aspnetcore-10.0#developer-exception-page).

## Additional resources

* [ASP.NET Core Blazor](../../../blazor/?view=aspnetcore-10.0)
* [Tag Helpers in ASP.NET Core](../../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-23

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/new-field
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/new-field.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 8, add a new field to an ASP.NET Core MVC app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

In this section [Entity Framework Core migrations](/en-us/ef/core/managing-schemas/migrations/) is used to:

* Add a new field to the model.
* Migrate the new field to the database.

When Entity Framework (EF) is used to automatically create a database from model classes:

* A table is added to the database to track the schema of the database.
* The database is verified to be in sync with the model classes it was generated from. If they aren't in sync, EF throws an exception. This makes it easier to find inconsistent database/code issues.

## Add a Rating Property to the Movie Model

Add a `Rating` property to `Models/Movie.cs`:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
    public string? Rating {  get; set; }
}
```

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Press `Ctrl`+`Shift`+`B`

From the *View* menu, select *Terminal* and enter the following command:

```
dotnet build
```

Because you've added a new field to the `Movie` class, you need to update the property binding list so this new property will be included. In `MoviesController.cs`, update the `[Bind]` attribute for both the `Create` and `Edit` action methods to include the `Rating` property:

```
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]
```

Update the view templates in order to display, create, and edit the new `Rating` property in the browser view.

Edit the `/Views/Movies/Index.cshtml` file and add a `Rating` field:

```
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Rating)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Rating)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Update the `/Views/Movies/Create.cshtml` with a `Rating` field.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

You can copy/paste the previous "form group" and let intelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

You can copy/paste the previous "form group" and let IntelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). The [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) extension provides IntelliSense support in Visual Studio Code.

Add the `Rating` property to the remaining `Create.cshtml`, `Delete.cshtml`, `Details.cshtml`, and `Edit.cshtml` view templates.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown in the following code snippet, but you'll want to make this change for each `new Movie`.

```
new Movie
{
    Title = "When Harry Met Sally",
    ReleaseDate = DateTime.Parse("1989-1-11"),
    Genre = "Romantic Comedy",
    Rating = "R",
    Price = 7.99M
},
```

The app won't work until the DB is updated to include the new field. If it's run now, the following `SqlException` is thrown:

`SqlException: Invalid column name 'Rating'.`

This error occurs because the updated Movie model class is different than the schema of the Movie table of the existing database. (There's no `Rating` column in the database table.)

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database based on the new model class schema. This approach is very convenient early in the development cycle when you're doing active development on a test database; it allows you to quickly evolve the model and database schema together. The downside, though, is that you lose existing data in the database — so you don't want to use this approach on a production database! Using an initializer to automatically seed a database with test data is often a productive way to develop an application. This is a good approach for early development and when using SQLite.

2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is that you keep your data. You can make this change either manually or by creating a database change script.

3. Use Entity Framework Migrations to update the database schema.

For this tutorial, Entity Framework Migrations is used.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the Package Manager Console, enter the following command:

```
Add-Migration Rating
```

The `Add-Migration` command tells the migration framework to examine the current `Movie` model with the current `Movie` DB schema and create the necessary code to migrate the DB to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

If all the records in the DB are deleted, the initialize method will seed the DB and include the `Rating` field.

In the Package Manager Console, enter the following command:

```
Update-Database
```

The Update-Database command runs the Up method in migrations that have not been applied.

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

For more information, see [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations).

Run the app and verify you can create, edit, and display movies with a `Rating` field.

[Previous](search?view=aspnetcore-10.0) [Next](validation?view=aspnetcore-10.0)

In this section [Entity Framework](/en-us/ef/core/get-started/aspnetcore/new-db) Migrations is used to:

* Add a new field to the model.
* Migrate the new field to the database.

When Entity Framework (EF) is used to automatically create a database from model classes:

* A table is added to the database to track the schema of the database.
* The database is verified to be in sync with the model classes it was generated from. If they aren't in sync, EF throws an exception. This makes it easier to find inconsistent database/code issues.

## Add a Rating Property to the Movie Model

Add a `Rating` property to `Models/Movie.cs`:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
    public string? Rating {  get; set; }
}
```

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Press `Ctrl`+`Shift`+`B`

From the *View* menu, select *Terminal* and enter the following command:

```
dotnet build
```

Because you've added a new field to the `Movie` class, you need to update the property binding list so this new property will be included. In `MoviesController.cs`, update the `[Bind]` attribute for both the `Create` and `Edit` action methods to include the `Rating` property:

```
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]
```

Update the view templates in order to display, create, and edit the new `Rating` property in the browser view.

Edit the `/Views/Movies/Index.cshtml` file and add a `Rating` field:

```
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Rating)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Rating)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Update the `/Views/Movies/Create.cshtml` with a `Rating` field.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

You can copy/paste the previous "form group" and let IntelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

You can copy/paste the previous "form group" and let IntelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). The [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) extension provides IntelliSense support in Visual Studio Code.

Add the `Rating` property to the remaining `Create.cshtml`, `Delete.cshtml`, `Details.cshtml`, and `Edit.cshtml` view templates.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but you'll want to make this change for each `new Movie`.

```
new Movie
{
    Title = "When Harry Met Sally",
    ReleaseDate = DateTime.Parse("1989-1-11"),
    Genre = "Romantic Comedy",
    Rating = "R",
    Price = 7.99M
},
```

The app won't work until the DB is updated to include the new field. If it's run now, the following `SqlException` is thrown:

`SqlException: Invalid column name 'Rating'.`

This error occurs because the updated Movie model class is different than the schema of the Movie table of the existing database. (There's no `Rating` column in the database table.)

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database based on the new model class schema. This approach is very convenient early in the development cycle when you're doing active development on a test database; it allows you to quickly evolve the model and database schema together. The downside, though, is that you lose existing data in the database — so you don't want to use this approach on a production database! Using an initializer to automatically seed a database with test data is often a productive way to develop an application. This is a good approach for early development and when using SQLite.

2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is that you keep your data. You can make this change either manually or by creating a database change script.

3. Use Entity Framework Migrations to update the database schema.

For this tutorial, Entity Framework Migrations is used.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the Package Manager Console, enter the following command:

```
Add-Migration Rating
```

The `Add-Migration` command tells the migration framework to examine the current `Movie` model with the current `Movie` DB schema and create the necessary code to migrate the DB to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

If all the records in the DB are deleted, the initialize method will seed the DB and include the `Rating` field.

In the Package Manager Console, enter the following command:

```
Update-Database
```

The Update-Database command runs the Up method in migrations that have not been applied.

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

For more information, see [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations).

Run the app and verify you can create, edit, and display movies with a `Rating` field.

[Previous](search?view=aspnetcore-10.0) [Next](validation?view=aspnetcore-10.0)

In this section [Entity Framework Core migrations](/en-us/ef/core/managing-schemas/migrations/) is used to:

* Add a new field to the model.
* Migrate the new field to the database.

When EF Code First is used to automatically create a database, Code First:

* Adds a table to the database to track the schema of the database.
* Verifies the database is in sync with the model classes it was generated from. If they aren't in sync, EF throws an exception. This makes it easier to find inconsistent database/code issues.

## Add a Rating Property to the Movie Model

Add a `Rating` property to `Models/Movie.cs`:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
    public string? Rating {  get; set; }
}
```

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

Press `Ctrl`+`Shift`+`B`

From the *View* menu, select *Terminal* and enter the following command:

```
dotnet build
```

Because you've added a new field to the `Movie` class, you need to update the property binding list so this new property will be included. In `MoviesController.cs`, update the `[Bind]` attribute for both the `Create` and `Edit` action methods to include the `Rating` property:

```
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]
```

Update the view templates in order to display, create, and edit the new `Rating` property in the browser view.

Edit the `/Views/Movies/Index.cshtml` file and add a `Rating` field:

```
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Rating)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Rating)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Update the `/Views/Movies/Create.cshtml` with a `Rating` field.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

You can copy/paste the previous "form group" and let intelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

You can copy/paste the previous "form group" and let IntelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0). The [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) extension provides IntelliSense support in Visual Studio Code.

Update the remaining templates.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but you'll want to make this change for each `new Movie`.

```
new Movie
{
    Title = "When Harry Met Sally",
    ReleaseDate = DateTime.Parse("1989-1-11"),
    Genre = "Romantic Comedy",
    Rating = "R",
    Price = 7.99M
},
```

The app won't work until the DB is updated to include the new field. If it's run now, the following `SqlException` is thrown:

`SqlException: Invalid column name 'Rating'.`

This error occurs because the updated Movie model class is different than the schema of the Movie table of the existing database. (There's no `Rating` column in the database table.)

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database based on the new model class schema. This approach is very convenient early in the development cycle when you're doing active development on a test database; it allows you to quickly evolve the model and database schema together. The downside, though, is that you lose existing data in the database — so you don't want to use this approach on a production database! Using an initializer to automatically seed a database with test data is often a productive way to develop an application. This is a good approach for early development and when using SQLite.

2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is that you keep your data. You can make this change either manually or by creating a database change script.

3. Use Code First Migrations to update the database schema.

For this tutorial, Code First Migrations is used.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration Rating
Update-Database
```

The `Add-Migration` command tells the migration framework to examine the current `Movie` model with the current `Movie` DB schema and create the necessary code to migrate the DB to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

If all the records in the DB are deleted, the initialize method will seed the DB and include the `Rating` field.

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

For more information, see [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations).

Run the app and verify you can create, edit, and display movies with a `Rating` field.

[Previous](search?view=aspnetcore-10.0) [Next](validation?view=aspnetcore-10.0)

In this section [Entity Framework Core migrations](/en-us/ef/core/managing-schemas/migrations/) is used to:

* Add a new field to the model.
* Migrate the new field to the database.

When EF Code First is used to automatically create a database, Code First:

* Adds a table to the database to track the schema of the database.
* Verifies the database is in sync with the model classes it was generated from. If they aren't in sync, EF throws an exception. This makes it easier to find inconsistent database/code issues.

## Add a Rating Property to the Movie Model

Add a `Rating` property to `Models/Movie.cs`:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
    public string? Rating {  get; set; }
}
```

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Press `Ctrl`+`Shift`+`B`

From the *View* menu, select *Terminal* and enter the following command:

```
dotnet build
```

Press `⌘`+`B`

Because you've added a new field to the `Movie` class, you need to update the property binding list so this new property will be included. In `MoviesController.cs`, update the `[Bind]` attribute for both the `Create` and `Edit` action methods to include the `Rating` property:

```
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]
```

Update the view templates in order to display, create, and edit the new `Rating` property in the browser view.

Edit the `/Views/Movies/Index.cshtml` file and add a `Rating` field:

```
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Rating)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Rating)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Update the `/Views/Movies/Create.cshtml` with a `Rating` field.

* [Visual Studio / Visual Studio for Mac](#tabpanel_2_visual-studio+visual-studio-mac)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

You can copy/paste the previous "form group" and let intelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

Update the remaining templates.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but you'll want to make this change for each `new Movie`.

```
new Movie
{
    Title = "When Harry Met Sally",
    ReleaseDate = DateTime.Parse("1989-1-11"),
    Genre = "Romantic Comedy",
    Rating = "R",
    Price = 7.99M
},
```

The app won't work until the DB is updated to include the new field. If it's run now, the following `SqlException` is thrown:

`SqlException: Invalid column name 'Rating'.`

This error occurs because the updated Movie model class is different than the schema of the Movie table of the existing database. (There's no `Rating` column in the database table.)

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database based on the new model class schema. This approach is very convenient early in the development cycle when you're doing active development on a test database; it allows you to quickly evolve the model and database schema together. The downside, though, is that you lose existing data in the database — so you don't want to use this approach on a production database! Using an initializer to automatically seed a database with test data is often a productive way to develop an application. This is a good approach for early development and when using SQLite.

2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is that you keep your data. You can make this change either manually or by creating a database change script.

3. Use Code First Migrations to update the database schema.

For this tutorial, Code First Migrations is used.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration Rating
Update-Database
```

The `Add-Migration` command tells the migration framework to examine the current `Movie` model with the current `Movie` DB schema and create the necessary code to migrate the DB to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

If all the records in the DB are deleted, the initialize method will seed the DB and include the `Rating` field.

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

For more information, see [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations).

Run the app and verify you can create, edit, and display movies with a `Rating` field.

[Previous](search?view=aspnetcore-10.0) [Next](validation?view=aspnetcore-10.0)

In this section [Entity Framework Core migrations](/en-us/ef/core/managing-schemas/migrations/) is used to:

* Add a new field to the model.
* Migrate the new field to the database.

When EF Code First is used to automatically create a database, Code First:

* Adds a table to the database to track the schema of the database.
* Verifies the database is in sync with the model classes it was generated from. If they aren't in sync, EF throws an exception. This makes it easier to find inconsistent database/code issues.

## Add a Rating Property to the Movie Model

Add a `Rating` property to `Models/Movie.cs`:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string? Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string? Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
        public string? Rating {  get; set; }
    }
}
```

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Ctrl+Shift+B

```
dotnet build
```

Command ⌘ + B

Because you've added a new field to the `Movie` class, you need to update the property binding list so this new property will be included. In `MoviesController.cs`, update the `[Bind]` attribute for both the `Create` and `Edit` action methods to include the `Rating` property:

```
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]
```

Update the view templates in order to display, create, and edit the new `Rating` property in the browser view.

Edit the `/Views/Movies/Index.cshtml` file and add a `Rating` field:

```
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Price)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Rating)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Rating)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
```

Update the `/Views/Movies/Create.cshtml` with a `Rating` field.

* [Visual Studio / Visual Studio for Mac](#tabpanel_2_visual-studio+visual-studio-mac)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

You can copy/paste the previous "form group" and let intelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

Update the remaining templates.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but you'll want to make this change for each `new Movie`.

```
new Movie
{
    Title = "When Harry Met Sally",
    ReleaseDate = DateTime.Parse("1989-1-11"),
    Genre = "Romantic Comedy",
    Rating = "R",
    Price = 7.99M
},
```

The app won't work until the DB is updated to include the new field. If it's run now, the following `SqlException` is thrown:

`SqlException: Invalid column name 'Rating'.`

This error occurs because the updated Movie model class is different than the schema of the Movie table of the existing database. (There's no `Rating` column in the database table.)

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database based on the new model class schema. This approach is very convenient early in the development cycle when you're doing active development on a test database; it allows you to quickly evolve the model and database schema together. The downside, though, is that you lose existing data in the database — so you don't want to use this approach on a production database! Using an initializer to automatically seed a database with test data is often a productive way to develop an application. This is a good approach for early development and when using SQLite.

2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is that you keep your data. You can make this change either manually or by creating a database change script.

3. Use Code First Migrations to update the database schema.

For this tutorial, Code First Migrations is used.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration Rating
Update-Database
```

The `Add-Migration` command tells the migration framework to examine the current `Movie` model with the current `Movie` DB schema and create the necessary code to migrate the DB to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

If all the records in the DB are deleted, the initialize method will seed the DB and include the `Rating` field.

See [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations) to remove all migrations and start over.

Run the app and verify you can create, edit, and display movies with a `Rating` field.

[Previous](search?view=aspnetcore-10.0) [Next](validation?view=aspnetcore-10.0)

In this section [Entity Framework](/en-us/ef/core/get-started/aspnetcore/new-db) Code First Migrations is used to:

* Add a new field to the model.
* Migrate the new field to the database.

When EF Code First is used to automatically create a database, Code First:

* Adds a table to the database to track the schema of the database.
* Verifies the database is in sync with the model classes it was generated from. If they aren't in sync, EF throws an exception. This makes it easier to find inconsistent database/code issues.

## Add a Rating Property to the Movie Model

Add a `Rating` property to `Models/Movie.cs`:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
        public string Rating { get; set; }
    }
}
```

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Ctrl+Shift+B

```
dotnet build
```

Command ⌘ + B

Because you've added a new field to the `Movie` class, you need to update the property binding list so this new property will be included. In `MoviesController.cs`, update the `[Bind]` attribute for both the `Create` and `Edit` action methods to include the `Rating` property:

```
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]
```

Update the view templates in order to display, create, and edit the new `Rating` property in the browser view.

Edit the `/Views/Movies/Index.cshtml` file and add a `Rating` field:

```
<thead>
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].Genre)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].Price)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].Rating)
        </th>
        <th></th>
    </tr>
</thead>
<tbody>
    @foreach (var item in Model.Movies)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Rating)
            </td>
            <td>
```

Update the `/Views/Movies/Create.cshtml` with a `Rating` field.

* [Visual Studio / Visual Studio for Mac](#tabpanel_2_visual-studio+visual-studio-mac)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

You can copy/paste the previous "form group" and let intelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

Update the remaining templates.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but you'll want to make this change for each `new Movie`.

```
new Movie
{
    Title = "When Harry Met Sally",
    ReleaseDate = DateTime.Parse("1989-1-11"),
    Genre = "Romantic Comedy",
    Rating = "R",
    Price = 7.99M
},
```

The app won't work until the DB is updated to include the new field. If it's run now, the following `SqlException` is thrown:

`SqlException: Invalid column name 'Rating'.`

This error occurs because the updated Movie model class is different than the schema of the Movie table of the existing database. (There's no `Rating` column in the database table.)

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database based on the new model class schema. This approach is very convenient early in the development cycle when you're doing active development on a test database; it allows you to quickly evolve the model and database schema together. The downside, though, is that you lose existing data in the database — so you don't want to use this approach on a production database! Using an initializer to automatically seed a database with test data is often a productive way to develop an application. This is a good approach for early development and when using SQLite.

2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is that you keep your data. You can make this change either manually or by creating a database change script.

3. Use Code First Migrations to update the database schema.

For this tutorial, Code First Migrations is used.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration Rating
Update-Database
```

The `Add-Migration` command tells the migration framework to examine the current `Movie` model with the current `Movie` DB schema and create the necessary code to migrate the DB to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

If all the records in the DB are deleted, the initialize method will seed the DB and include the `Rating` field.

See [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations) to remove all migrations and start over.

Run the app and verify you can create, edit, and display movies with a `Rating` field.

[Previous](search?view=aspnetcore-10.0) [Next](validation?view=aspnetcore-10.0)

In this section [Entity Framework](/en-us/ef/core/get-started/aspnetcore/new-db) Code First Migrations is used to:

* Add a new field to the model.
* Migrate the new field to the database.

When EF Code First is used to automatically create a database, Code First:

* Adds a table to the database to track the schema of the database.
* Verifies the database is in sync with the model classes it was generated from. If they aren't in sync, EF throws an exception. This makes it easier to find inconsistent database/code issues.

## Add a Rating Property to the Movie Model

Add a `Rating` property to `Models/Movie.cs`:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
        public string Rating { get; set; }
    }
}
```

Build the app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Ctrl+Shift+B

```
dotnet build
```

Command ⌘ + B

Because you've added a new field to the `Movie` class, you need to update the property binding list so this new property will be included. In `MoviesController.cs`, update the `[Bind]` attribute for both the `Create` and `Edit` action methods to include the `Rating` property:

```
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]
```

Update the view templates in order to display, create, and edit the new `Rating` property in the browser view.

Edit the `/Views/Movies/Index.cshtml` file and add a `Rating` field:

```
<thead>
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].Genre)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].Price)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Movies[0].Rating)
        </th>
        <th></th>
    </tr>
</thead>
<tbody>
    @foreach (var item in Model.Movies)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Rating)
            </td>
            <td>
```

Update the `/Views/Movies/Create.cshtml` with a `Rating` field.

* [Visual Studio / Visual Studio for Mac](#tabpanel_2_visual-studio+visual-studio-mac)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

You can copy/paste the previous "form group" and let intelliSense help you update the fields. IntelliSense works with [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

Update the remaining templates.

Update the `SeedData` class so that it provides a value for the new column. A sample change is shown below, but you'll want to make this change for each `new Movie`.

```
new Movie
{
    Title = "When Harry Met Sally",
    ReleaseDate = DateTime.Parse("1989-1-11"),
    Genre = "Romantic Comedy",
    Rating = "R",
    Price = 7.99M
},
```

The app won't work until the DB is updated to include the new field. If it's run now, the following `SqlException` is thrown:

`SqlException: Invalid column name 'Rating'.`

This error occurs because the updated Movie model class is different than the schema of the Movie table of the existing database. (There's no `Rating` column in the database table.)

There are a few approaches to resolving the error:

1. Have the Entity Framework automatically drop and re-create the database based on the new model class schema. This approach is very convenient early in the development cycle when you're doing active development on a test database; it allows you to quickly evolve the model and database schema together. The downside, though, is that you lose existing data in the database — so you don't want to use this approach on a production database! Using an initializer to automatically seed a database with test data is often a productive way to develop an application. This is a good approach for early development and when using SQLite.

2. Explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is that you keep your data. You can make this change either manually or by creating a database change script.

3. Use Code First Migrations to update the database schema.

For this tutorial, Code First Migrations is used.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration Rating
Update-Database
```

The `Add-Migration` command tells the migration framework to examine the current `Movie` model with the current `Movie` DB schema and create the necessary code to migrate the DB to the new model.

The name "Rating" is arbitrary and is used to name the migration file. It's helpful to use a meaningful name for the migration file.

If all the records in the DB are deleted, the initialize method will seed the DB and include the `Rating` field.

See [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations) to remove all migrations and start over.

Run the app and verify you can create, edit, and display movies with a `Rating` field.

[Previous](search?view=aspnetcore-10.0) [Next](validation?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-03-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/client
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/client.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Call gRPC services with the .NET client

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

A .NET gRPC client library is available in the [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client) NuGet package. This document explains how to:

* Configure a gRPC client to call gRPC services.
* Make gRPC calls to unary, server streaming, client streaming, and bi-directional streaming methods.

## Configure gRPC client

gRPC clients are concrete client types that are [generated from `.proto` files](basics?view=aspnetcore-10.0#generated-c-assets). The concrete gRPC client has methods that translate to the gRPC service in the `.proto` file. For example, a service called `Greeter` generates a `GreeterClient` type with methods to call the service.

A gRPC client is created from a channel. Start by using `GrpcChannel.ForAddress` to create a channel, and then use the channel to create a gRPC client:

```
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greet.GreeterClient(channel);
```

A channel represents a long-lived connection to a gRPC service. When a channel is created, it's configured with options related to calling a service. For example, the `HttpClient` used to make calls, the maximum send and receive message size, and logging can be specified on `GrpcChannelOptions` and used with `GrpcChannel.ForAddress`. For a complete list of options, see [client configuration options](configuration?view=aspnetcore-10.0#configure-client-options).

```
var channel = GrpcChannel.ForAddress("https://localhost:5001");

var greeterClient = new Greet.GreeterClient(channel);
var counterClient = new Count.CounterClient(channel);

// Use clients to call gRPC services
```

### Configure TLS

A gRPC client must use the same connection-level security as the called service. gRPC client Transport Layer Security (TLS) is configured when the gRPC channel is created. A gRPC client throws an error when it calls a service and the connection-level security of the channel and service don't match.

To configure a gRPC channel to use TLS, ensure the server address starts with `https`. For example, `GrpcChannel.ForAddress("https://localhost:5001")` uses HTTPS protocol. The gRPC channel automatically negotiates a connection secured by TLS and uses a secure connection to make gRPC calls.

Tip

gRPC supports client certificate authentication over TLS. For information on configuring client certificates with a gRPC channel, see [Authentication and authorization in gRPC for ASP.NET Core](authn-and-authz?view=aspnetcore-10.0#client-certificate-authentication).

To call unsecured gRPC services, ensure the server address starts with `http`. For example, `GrpcChannel.ForAddress("http://localhost:5000")` uses HTTP protocol. In .NET Core 3.1, additional configuration is required to [call insecure gRPC services with the .NET client](troubleshoot?view=aspnetcore-10.0#call-insecure-grpc-services-with-net-client).

### Client performance

Channel and client performance and usage:

* Creating a channel can be an expensive operation. Reusing a channel for gRPC calls provides performance benefits.
* A channel manages connections to the server. If the connection is closed or lost, the channel automatically reconnects the next time a gRPC call is made.
* gRPC clients are created with channels. gRPC clients are lightweight objects and don't need to be cached or reused.
* Multiple gRPC clients can be created from a channel, including different types of clients.
* A channel and clients created from the channel can safely be used by multiple threads.
* Clients created from the channel can make multiple simultaneous calls.

`GrpcChannel.ForAddress` isn't the only option for creating a gRPC client. If calling gRPC services from an ASP.NET Core app, consider [gRPC client factory integration](clientfactory?view=aspnetcore-10.0). gRPC integration with `HttpClientFactory` offers a centralized alternative to creating gRPC clients.

When calling gRPC methods, prefer using [asynchronous programming with async and await](/en-us/dotnet/csharp/asynchronous-programming/). Making gRPC calls with blocking, such as using `Task.Result` or `Task.Wait()`, prevents other tasks from using a thread. This can lead to thread pool starvation and poor performance. It could cause the app to hang with a deadlock. For more information, see [Asynchronous calls in client apps](performance?view=aspnetcore-10.0#asynchronous-calls-in-client-apps).

## Make gRPC calls

A gRPC call is initiated by calling a method on the client. The gRPC client will handle message serialization and addressing the gRPC call to the correct service.

gRPC has different types of methods. How the client is used to make a gRPC call depends on the type of method called. The gRPC method types are:

* Unary
* Server streaming
* Client streaming
* Bi-directional streaming

### Unary call

A unary call starts with the client sending a request message. A response message is returned when the service finishes.

```
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = "World" });

Console.WriteLine("Greeting: " + response.Message);
// Greeting: Hello World
```

Each unary service method in the `.proto` file will result in two .NET methods on the concrete gRPC client type for calling the method: an asynchronous method and a blocking method. For example, on `GreeterClient` there are two ways of calling `SayHello`:

* `GreeterClient.SayHelloAsync` - calls the `Greeter.SayHello` service asynchronously. Can be awaited.
* `GreeterClient.SayHello` - calls the `Greeter.SayHello` service and blocks until complete. Don't use in asynchronous code. Can cause performance and reliability issues.

For more information, see [Asynchronous calls in client apps](performance?view=aspnetcore-10.0#asynchronous-calls-in-client-apps).

### Server streaming call

A server streaming call starts with the client sending a request message. `ResponseStream.MoveNext()` reads messages streamed from the service. The server streaming call is complete when `ResponseStream.MoveNext()` returns `false`.

```
var client = new Greet.GreeterClient(channel);
using var call = client.SayHellos(new HelloRequest { Name = "World" });

while (await call.ResponseStream.MoveNext())
{
    Console.WriteLine("Greeting: " + call.ResponseStream.Current.Message);
    // "Greeting: Hello World" is written multiple times
}
```

When using C# 8 or later, the `await foreach` syntax can be used to read messages. The `IAsyncStreamReader<T>.ReadAllAsync()` extension method reads all messages from the response stream:

```
var client = new Greet.GreeterClient(channel);
using var call = client.SayHellos(new HelloRequest { Name = "World" });

await foreach (var response in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine("Greeting: " + response.Message);
    // "Greeting: Hello World" is written multiple times
}
```

The type returned from starting a server streaming call implements `IDisposable`. Always dispose a streaming call to ensure it's stopped and all resources are cleaned up.

### Client streaming call

A client streaming call starts *without* the client sending a message. The client can choose to send messages with `RequestStream.WriteAsync`. When the client has finished sending messages, `RequestStream.CompleteAsync()` should be called to notify the service. The call is finished when the service returns a response message.

```
var client = new Counter.CounterClient(channel);
using var call = client.AccumulateCount();

for (var i = 0; i < 3; i++)
{
    await call.RequestStream.WriteAsync(new CounterRequest { Count = 1 });
}
await call.RequestStream.CompleteAsync();

var response = await call;
Console.WriteLine($"Count: {response.Count}");
// Count: 3
```

The type returned from starting a client streaming call implements `IDisposable`. Always dispose a streaming call to ensure it's stopped and all resources are cleaned up.

### Bi-directional streaming call

A bi-directional streaming call starts *without* the client sending a message. The client can choose to send messages with `RequestStream.WriteAsync`. Messages streamed from the service are accessible with `ResponseStream.MoveNext()` or `ResponseStream.ReadAllAsync()`. The bi-directional streaming call is complete when the `ResponseStream` has no more messages.

```
var client = new Echo.EchoClient(channel);
using var call = client.Echo();

Console.WriteLine("Starting background task to receive messages");
var readTask = Task.Run(async () =>
{
    await foreach (var response in call.ResponseStream.ReadAllAsync())
    {
        Console.WriteLine(response.Message);
        // Echo messages sent to the service
    }
});

Console.WriteLine("Starting to send messages");
Console.WriteLine("Type a message to echo then press enter.");
while (true)
{
    var result = Console.ReadLine();
    if (string.IsNullOrEmpty(result))
    {
        break;
    }

    await call.RequestStream.WriteAsync(new EchoMessage { Message = result });
}

Console.WriteLine("Disconnecting");
await call.RequestStream.CompleteAsync();
await readTask;
```

For best performance, and to avoid unnecessary errors in the client and service, try to complete bi-directional streaming calls gracefully. A bi-directional call completes gracefully when the server has finished reading the request stream and the client has finished reading the response stream. The preceding sample call is one example of a bi-directional call that ends gracefully. In the call, the client:

1. Starts a new bi-directional streaming call by calling `EchoClient.Echo`.
2. Creates a background task to read messages from the service using `ResponseStream.ReadAllAsync()`.
3. Sends messages to the server with `RequestStream.WriteAsync`.
4. Notifies the server it has finished sending messages with `RequestStream.CompleteAsync()`.
5. Waits until the background task has read all incoming messages.

During a bi-directional streaming call, the client and service can send messages to each other at any time. The best client logic for interacting with a bi-directional call varies depending upon the service logic.

The type returned from starting a bi-directional streaming call implements `IDisposable`. Always dispose a streaming call to ensure it's stopped and all resources are cleaned up.

## Access gRPC headers

gRPC calls return response headers. HTTP response headers pass name/value metadata about a call that isn't related the returned message.

Headers are accessible using `ResponseHeadersAsync`, which returns a collection of metadata. Headers are typically returned with the response message; therefore, you must await them.

```
var client = new Greet.GreeterClient(channel);
using var call = client.SayHelloAsync(new HelloRequest { Name = "World" });

var headers = await call.ResponseHeadersAsync;
var myValue = headers.GetValue("my-trailer-name");

var response = await call.ResponseAsync;
```

`ResponseHeadersAsync` usage:

* Must await the result of `ResponseHeadersAsync` to get the headers collection.
* Doesn't have to be accessed before `ResponseAsync` (or the response stream when streaming). If a response has been returned, then `ResponseHeadersAsync` returns headers instantly.
* Will throw an exception if there was a connection or server error and headers weren't returned for the gRPC call.

## Access gRPC trailers

gRPC calls may return response trailers. Trailers are used to provide name/value metadata about a call. Trailers provide similar functionality to HTTP headers, but are received at the end of the call.

Trailers are accessible using `GetTrailers()`, which returns a collection of metadata. Trailers are returned after the response is complete. Therefore, you must await all response messages before accessing the trailers.

Unary and client streaming calls must await `ResponseAsync` before calling `GetTrailers()`:

```
var client = new Greet.GreeterClient(channel);
using var call = client.SayHelloAsync(new HelloRequest { Name = "World" });
var response = await call.ResponseAsync;

Console.WriteLine("Greeting: " + response.Message);
// Greeting: Hello World

var trailers = call.GetTrailers();
var myValue = trailers.GetValue("my-trailer-name");
```

Server and bidirectional streaming calls must finish awaiting the response stream before calling `GetTrailers()`:

```
var client = new Greet.GreeterClient(channel);
using var call = client.SayHellos(new HelloRequest { Name = "World" });

await foreach (var response in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine("Greeting: " + response.Message);
    // "Greeting: Hello World" is written multiple times
}

var trailers = call.GetTrailers();
var myValue = trailers.GetValue("my-trailer-name");
```

Trailers are also accessible from `RpcException`. A service may return trailers together with a non-OK gRPC status. In this situation, the trailers are retrieved from the exception thrown by the gRPC client:

```
var client = new Greet.GreeterClient(channel);
string myValue = null;

try
{
    using var call = client.SayHelloAsync(new HelloRequest { Name = "World" });
    var response = await call.ResponseAsync;

    Console.WriteLine("Greeting: " + response.Message);
    // Greeting: Hello World

    var trailers = call.GetTrailers();
    myValue = trailers.GetValue("my-trailer-name");
}
catch (RpcException ex)
{
    var trailers = ex.Trailers;
    myValue = trailers.GetValue("my-trailer-name");
}
```

## Configure deadline

Configuring a gRPC call deadline is recommended because it provides an upper limit on how long a call can run for. It stops misbehaving services from running forever and exhausting server resources. Deadlines are a useful tool for building reliable apps.

Configure `CallOptions.Deadline` to set a deadline for a gRPC call:

```
var client = new Greet.GreeterClient(channel);

try
{
    var response = await client.SayHelloAsync(
        new HelloRequest { Name = "World" },
        deadline: DateTime.UtcNow.AddSeconds(5));
    
    // Greeting: Hello World
    Console.WriteLine("Greeting: " + response.Message);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
    Console.WriteLine("Greeting timeout.");
}
```

For more information, see [Reliable gRPC services with deadlines and cancellation](deadlines-cancellation?view=aspnetcore-10.0#deadlines).

## Related content

* [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0)
* [Reliable gRPC services with deadlines and cancellation](deadlines-cancellation?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/health-checks
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/health-checks.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC health checks in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

The [gRPC health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) is a standard for reporting the health of gRPC server apps.

Health checks are exposed by an app as a gRPC service. They're typically used with an external monitoring service to check the status of an app. The service can be configured for various real-time monitoring scenarios:

* Health probes can be used by container orchestrators and load balancers to check an app's status. For example, Kubernetes supports [gRPC liveness, readiness and startup probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-grpc-liveness-probe). Kubernetes can be configured to reroute traffic or restart unhealthy containers based on gRPC health check results.
* Use of memory, disk, and other physical server resources can be monitored for healthy status.
* Health checks can test an app's dependencies, such as databases and external service endpoints, to confirm availability and normal functioning.

## Set up gRPC health checks

gRPC ASP.NET Core has built-in support for gRPC health checks with the [`Grpc.AspNetCore.HealthChecks`](https://www.nuget.org/packages/Grpc.AspNetCore.HealthChecks) package. Results from [.NET health checks](../host-and-deploy/health-checks?view=aspnetcore-10.0) are reported to callers.

To set up gRPC health checks in an app:

* Add a `Grpc.AspNetCore.HealthChecks` package reference.

* Register gRPC health checks service:

  * `AddGrpcHealthChecks` to register services that enable health checks.
  * `MapGrpcHealthChecksService` to add a health checks service endpoint.

* Add health checks by implementing [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) or using the [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) method.

```
using GrpcServiceHC.Services;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddGrpcHealthChecks()
                .AddCheck("Sample", () => HealthCheckResult.Healthy());

var app = builder.Build();

app.MapGrpcService<GreeterService>();
app.MapGrpcHealthChecksService();

// Code removed for brevity.
```

When health checks is set up:

* The health checks service is added to the server app.

* .NET health checks registered with the app are periodically executed for health results. By default, there's a 5-second delay after app startup, and then health checks are executed every 30 seconds. Health check execution interval [can be customized with `HealthCheckPublisherOptions`](#configure-health-checks-execution-interval).

* Health results determine what the gRPC service reports:

  * `Unknown` is reported when there are no health results.
  * `NotServing` is reported when there are any health results of [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy).
  * Otherwise, `Serving` is reported.

### Health checks service security

gRPC health checks returns health status about an app, which could be sensitive information. Care should be taken to limit access to the gRPC health checks service.

Access to the service can be controlled through standard ASP.NET Core authorization extension methods, such as [`AllowAnonymous`](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.allowanonymous) and [`RequireAuthorization`](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization).

For example, if an app has been configured to require authorization by default, configure the gRPC health checks endpoint with `AllowAnonymous` to skip authentication and authorization.

```
app.MapGrpcHealthChecksService().AllowAnonymous();
```

### Configure `Grpc.AspNetCore.HealthChecks`

By default, the gRPC health checks service uses all registered health checks to determine health status. gRPC health checks can be customized when registered to use a subset of health checks. The `MapService` method is used to map health results to service names, along with a predicate for filtering health results:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddGrpcHealthChecks(o =>
{
    o.Services.MapService("", r => r.Tags.Contains("public"));
});

var app = builder.Build();
```

The preceding code overrides the default service (`""`) to only use health results with the "public" tag.

gRPC health checks supports the client specifying a service name argument when checking health. Multiple services are supported by providing a service name to `MapService`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddGrpcHealthChecks(o =>
{
    o.Services.MapService("greet.Greeter", r => r.Tags.Contains("greeter"));
    o.Services.MapService("count.Counter", r => r.Tags.Contains("counter"));
});

var app = builder.Build();
```

The service name specified by the client is usually the default (`""`) or a package-qualified name of a service in your app. However, nothing prevents the client using arbitrary values to check app health.

### Configure health checks execution interval

Health checks are run immediately when `Check` is called. `Watch` is a streaming method and has a different behavior than `Check`: The long running stream reports health checks results over time by periodically executing [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) to gather health results. By default, the publisher:

* Waits 5 seconds after app startup before running health checks.
* Runs health checks every 30 seconds.

Publisher intervals can be configured using [HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions) at startup:

```
builder.Services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.Zero;
    options.Period = TimeSpan.FromSeconds(10);
});
```

## Call gRPC health checks service

The [`Grpc.HealthCheck`](https://www.nuget.org/packages/Grpc.HealthCheck) package includes a client for gRPC health checks:

```
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Health.HealthClient(channel);

var response = await client.CheckAsync(new HealthCheckRequest());
var status = response.Status;
```

There are two methods on the `Health` service:

* `Check` is a unary method for getting the current health status. Health checks are executed immediately when `Check` is called. The server returns a `NOT_FOUND` error response if the client requests an unknown service name. This can happen at app startup if health results haven't been published yet.
* `Watch` is a streaming method that reports changes in health status over time. [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) is periodically executed to gather health results. The server returns an `Unknown` status if the client requests an unknown service name.

The `Grpc.HealthCheck` client can be used in a client factory approach:

```
builder.Services
    .AddGrpcClient<Health.HealthClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    });
```

In the previous example, a client factory for `Health.HealthClient` instances is registered with the dependency injection system. Then, these instances are injected into services for executing health check calls.

For more information, see [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0).

## Additional resources

* [Health checks in ASP.NET Core](../host-and-deploy/health-checks?view=aspnetcore-10.0)
* [gRPC health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md)
* [`Grpc.AspNetCore.HealthChecks`](https://www.nuget.org/packages/Grpc.AspNetCore.HealthChecks)
* [`Grpc.HealthCheck`](https://www.nuget.org/packages/Grpc.HealthCheck)

By [James Newton-King](https://twitter.com/jamesnk)

The [gRPC health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) is a standard for reporting the health of gRPC server apps.

Health checks are exposed by an app as a gRPC service. They are typically used with an external monitoring service to check the status of an app. The service can be configured for various real-time monitoring scenarios:

* Health probes can be used by container orchestrators and load balancers to check an app's status. For example, Kubernetes supports [gRPC liveness, readiness and startup probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-grpc-liveness-probe). Kubernetes can be configured to reroute traffic or restart unhealthy containers based on gRPC health check results.
* Use of memory, disk, and other physical server resources can be monitored for healthy status.
* Health checks can test an app's dependencies, such as databases and external service endpoints, to confirm availability and normal functioning.

## Set up gRPC health checks

gRPC ASP.NET Core has built-in support for gRPC health checks with the [`Grpc.AspNetCore.HealthChecks`](https://www.nuget.org/packages/Grpc.AspNetCore.HealthChecks) package. Results from [.NET health checks](../host-and-deploy/health-checks?view=aspnetcore-10.0) are reported to callers.

To set up gRPC health checks in an app:

* Add a `Grpc.AspNetCore.HealthChecks` package reference.

* Register gRPC health checks service in `Startup.cs`:

  * `AddGrpcHealthChecks` to register services that enable health checks.
  * `MapGrpcHealthChecksService` to add a health checks service endpoint.

* Add health checks by implementing [IHealthCheck](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheck) or using the [AddCheck](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.healthchecksbuilderaddcheckextensions.addcheck) method.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();
    services
        .AddGrpcHealthChecks()
        .AddCheck("Sample", () => HealthCheckResult.Healthy());
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();
    
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<GreeterService>();
        endpoints.MapGrpcHealthChecksService();
    });
}
```

When health checks is set up:

* The health checks service is added to the server app.

* .NET health checks registered with the app are periodically executed for health results. By default, there is a 5 second delay after app startup, and then health checks are executed every 30 seconds. Health check execution interval [can be customized with `HealthCheckPublisherOptions`](#configure-health-checks-execution-interval).

* Health results determine what the gRPC service reports:

  * `Unknown` is reported when there are no health results.
  * `NotServing` is reported when there are any health results of [HealthStatus.Unhealthy](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthstatus#microsoft-extensions-diagnostics-healthchecks-healthstatus-unhealthy).
  * Otherwise, `Serving` is reported.

### Configure `Grpc.AspNetCore.HealthChecks`

By default, the gRPC health checks service uses all registered health checks to determine health status. gRPC health checks can be customized when registered to use a subset of health checks. The `MapService` method is used to map health results to service names, along with a predicate for filtering health results:

```
services.AddGrpcHealthChecks(o =>
{
    o.Services.MapService("", r => r.Tags.Contains("public"));
});
```

The preceding code overrides the default service (`""`) to only use health results with the "public" tag.

gRPC health checks supports the client specifying a service name argument when checking health. Multiple services are supported by providing a service name to `MapService`:

```
services.AddGrpcHealthChecks(o =>
{
    o.Services.MapService("greet.Greeter", r => r.Tags.Contains("greeter"));
    o.Services.MapService("count.Counter", r => r.Tags.Contains("counter"));
});
```

The service name specified by the client is usually the default (`""`) or a package-qualified name of a service in your app. However, nothing prevents the client using arbitrary values to check app health.

### Configure health checks execution interval

Health checks are run immediately when `Check` is called. `Watch` is a streaming method and has a different behavior than `Check`: The long running stream reports health checks results over time by periodically executing [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) to gather health results. By default, the publisher:

* Waits 5 seconds after app startup before running health checks.
* Runs health checks every 30 seconds.

Publisher intervals can be configured using [HealthCheckPublisherOptions](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.healthcheckpublisheroptions) at startup:

```
services.Configure<HealthCheckPublisherOptions>(options =>
{
    options.Delay = TimeSpan.Zero;
    options.Period = TimeSpan.FromSeconds(10);
});
```

## Call gRPC health checks service

The [`Grpc.HealthCheck`](https://www.nuget.org/packages/Grpc.HealthCheck) package includes a client for gRPC health checks:

```
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Health.HealthClient(channel);

var response = client.CheckAsync(new HealthCheckRequest());
var status = response.Status;
```

There are two methods on the `Health` service:

* `Check` is a unary method for getting the current health status. Health checks are executed immediately when `Check` is called. The server returns a `NOT_FOUND` error response if the client requests an unknown service name. This can happen at app startup if health results haven't been published yet.
* `Watch` is a streaming method that reports changes in health status over time. [IHealthCheckPublisher](/en-us/dotnet/api/microsoft.extensions.diagnostics.healthchecks.ihealthcheckpublisher) is periodically executed to gather health results. The server returns an `Unknown` status if the client requests an unknown service name.

## Additional resources

* [Health checks in ASP.NET Core](../host-and-deploy/health-checks?view=aspnetcore-10.0)
* [gRPC health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md)
* [`Grpc.AspNetCore.HealthChecks`](https://www.nuget.org/packages/Grpc.AspNetCore.HealthChecks)
* [`Grpc.HealthCheck`](https://www.nuget.org/packages/Grpc.HealthCheck)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-08-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/community
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/community.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Community OSS authentication options for ASP.NET Core

This page shows community-provided [open-source software (OSS)](https://en.wikipedia.org/wiki/Open-source_software) authentication options for ASP.NET Core. This page is periodically updated as new providers become available.

## OSS authentication providers \[.NET]

| Name                                                                              | Description                                                                   |
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [Duende IdentityServer](https://duendesoftware.com/products/identityserver)       | IdentityServer is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core. |
| [OpenIddict](https://github.com/openiddict/openiddict-core)                       | OAuth 2.0/OpenID Connect server for ASP.NET Core and ASP.NET 4.x.             |
| [FIDO2 .NET Library, WebAuthn](https://github.com/passwordless-lib/fido2-net-lib) | FIDO2 .NET library for FIDO2 / WebAuthn Attestation and Assertion using .NET  |

## OSS authentication provider clients \[.NET]

| Name                                                                                                   | Description                                                                                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [OpenIddict](https://github.com/openiddict/openiddict-core)                                            | OAuth 2.0/OpenID Connect client for ASP.NET Core, ASP.NET 4.x and Windows/Linux desktop apps with built-in integrations for 80+ services such as Auth0, Microsoft Entra ID, GitHub, Google, Twitter or Yahoo. |
| [AspNet.Security.OAuth.Providers](https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers)   | A collection of security middleware for ASP.NET Core apps to support social authentication.                                                                                                                   |
| [AspNet.Security.OpenId.Providers](https://github.com/aspnet-contrib/AspNet.Security.OpenId.Providers) | A collection of security middleware for ASP.NET Core apps to support OpenID 2.0 authentication providers like [Steam](https://steampowered.com/).                                                             |

## Other OSS authentication providers

| Name                                                              | Description                                                                                                                                                                                 |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Gluu Server](https://gluu.org/)                                  | Enterprise ready, open source software for identity, access management (IAM), and single sign-on (SSO). For more information, see the [Gluu Product Documentation](https://gluu.org/docs/). |
| [Keycloak](https://github.com/keycloak/keycloak/)                 | Open Source Identity and Access Management For Modern Applications and Services.                                                                                                            |
| [node-oidc-provider](https://github.com/panva/node-oidc-provider) | OpenID Certified™ OAuth 2.0 Authorization Server implementation for Node.js.                                                                                                                |
| [Authentik](https://goauthentik.io)                               | Authentik is an open-source Identity Provider focused on flexibility and versatility.                                                                                                       |

To add a provider, [edit this page](https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fdotnet%2FAspNetCore.Docs%2Fedit%2Fmain%2Faspnetcore%2Fsecurity%2Fauthentication%2Fcommunity.md).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/microsoft-entra-id-groups-and-roles
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/microsoft-entra-id-groups-and-roles.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Microsoft Entra (ME-ID) groups, Administrator Roles, and App Roles

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure Blazor WebAssembly to use Microsoft Entra ID (ME-ID) groups and roles.

ME-ID provides several authorization approaches that can be combined with ASP.NET Core Identity:

* Groups

  * Security
  * Microsoft 365
  * Distribution

* Roles

  * ME-ID built-in Administrator Roles
  * App Roles

The guidance in this article applies to the Blazor WebAssembly ME-ID deployment scenarios described in the following articles:

* [Standalone with Microsoft Accounts](standalone-with-microsoft-accounts?view=aspnetcore-10.0)
* [Standalone with ME-ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0)

The examples in this article take advantage of new .NET/C# features. When using the examples with .NET 7 or earlier, minor modifications are required. However, the text and code examples that pertain to interacting with ME-ID and Microsoft Graph are the same for all versions of ASP.NET Core.

## Sample app

Access the sample app, named `BlazorWebAssemblyEntraGroupsAndRoles`, through the latest version folder from the repository's root with the following link. The sample is provided for .NET 8 or later. See the sample app's `README` file for steps on how to run the app.

The sample app includes a `UserClaims` component for displaying a user's claims. The `UserData` component displays the user's basic account properties.

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#sample-apps))

## Prerequisite

The guidance in this article implements the Microsoft Graph API per the *Graph SDK* guidance in [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0\&pivots=graph-sdk). Follow the *Graph SDK* implementation guidance to configure the app and test it to confirm that the app can obtain Graph API data for a test user account. Additionally, see the [Graph API article's security article cross-links](graph-api?view=aspnetcore-10.0#security-guidance) to review Microsoft Graph security concepts.

When testing with the Graph SDK locally, we recommend using a new in-private/incognito browser session for each test to prevent lingering cookies from interfering with tests. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0#troubleshoot).

## ME-ID app registration online tools

This article refers to the [Azure portal](/en-us/azure/azure-portal/azure-portal-overview) throughout when prompting you to configure the app's ME-ID app registration, but the [Microsoft Entra Admin Center](/en-us/entra/fundamentals/entra-admin-center) is also a viable option for managing ME-ID app registrations. Either interface can be used, but the guidance in this article specifically covers gestures for the Azure portal.

## Scopes

*Permissions* and *scopes* mean the same thing and are used interchangeably in security documentation and the Azure portal. Unless the text is referring to the Azure portal, this article uses *scope*/*scopes* when referring to Graph permissions.

Scopes are case insensitive, so `User.Read` is the same as `user.read`. Feel free to use either format, but we recommend a consistent choice across application code.

To permit [Microsoft Graph API](/en-us/graph/use-the-api) calls for user profile, role assignment, and group membership data, the app is configured with the ***delegated*** `User.Read` scope (`https://graph.microsoft.com/User.Read`) in the Azure portal because access to read user data is determined by the scopes granted (delegated) to individual users. This scope is required in addition to the scopes required in ME-ID deployment scenarios described by the articles listed earlier (*Standalone with Microsoft Accounts* or *Standalone with ME-ID*).

Additional required scopes include:

* ***Delegated*** [`RoleManagement.Read.Directory`](/en-us/graph/permissions-reference#rolemanagementreaddirectory) scope (`https://graph.microsoft.com/RoleManagement.Read.Directory`): Allows the app to read the role-based access control (RBAC) settings for your company's directory, on behalf of the signed-in user. This includes reading directory role templates, directory roles, and memberships. Directory role memberships are used to create `directoryRole` claims in the app for ME-ID built-in Administrator Roles. Admin consent is required.
* ***Delegated*** [`AdministrativeUnit.Read.All`](/en-us/graph/permissions-reference#administrativeunitreadall) scope (`https://graph.microsoft.com/AdministrativeUnit.Read.All`): Allows the app to read administrative units and administrative unit membership on behalf of the signed-in user. These memberships are used to create `administrativeUnit` claims in the app. Admin consent is required.

For more information, see [Overview of permissions and consent in the Microsoft identity platform](/en-us/entra/identity-platform/permissions-consent-overview) and [Overview of Microsoft Graph permissions](/en-us/graph/permissions-overview).

## Custom user account

Assign users to ME-ID security groups and ME-ID Administrator Roles in the Azure portal.

The examples in this article:

* Assume that a user is assigned to the ME-ID *Billing Administrator* role in the Azure portal ME-ID tenant for authorization to access server API data.
* Use [authorization policies](../../../security/authorization/policies?view=aspnetcore-10.0) to control access within the app.

Extend [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) to include properties for:

* `Roles`: ME-ID App Roles array (covered in the [App Roles](#app-roles) section)
* `Oid`: Immutable [object identifier claim (`oid`)](/en-us/entra/identity-platform/id-tokens#payload-claims) (uniquely identifies a user within and across tenants)

`CustomUserAccount.cs`:

```
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

namespace BlazorWebAssemblyEntraGroupsAndRoles;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("roles")]
    public List<string>? Roles { get; set; }

    [JsonPropertyName("oid")]
    public string? Oid { get; set; }
}
```

Add a package reference to app for [`Microsoft.Graph`](https://www.nuget.org/packages/Microsoft.Graph).

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Add the Graph SDK utility classes and configuration in the *Graph SDK* guidance of the [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0\&pivots=graph-sdk) article. Specify the `User.Read`, `RoleManagement.Read.Directory`, and `AdministrativeUnit.Read.All` scopes for the access token as the article shows in its example `wwwroot/appsettings.json` file.

Add the following custom user account factory to the app. The custom user factory is used to establish:

* App Role claims (`role`) (covered in the [App Roles](#app-roles) section).

* Example user profile data claims for the user's mobile phone number (`mobilePhone`) and office location (`officeLocation`).

* ME-ID Administrator Role claims (`directoryRole`).

* ME-ID Administrative Unit claims (`administrativeUnit`).

* ME-ID Group claims (`directoryGroup`).

* An [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) (`logger`) for convenience in case you wish to log information or errors.

`CustomAccountFactory.cs`:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;

namespace BlazorWebAssemblyEntraGroupsAndRoles;

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor,
        IServiceProvider serviceProvider, ILogger<CustomAccountFactory> logger,
        IConfiguration config)
    : AccountClaimsPrincipalFactory<CustomUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IServiceProvider serviceProvider = serviceProvider;
    private readonly string? baseUrl = string.Join("/",
        config.GetSection("MicrosoftGraph")["BaseUrl"],
        config.GetSection("MicrosoftGraph")["Version"]);

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity is not null &&
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null && !string.IsNullOrEmpty(baseUrl) &&
                account.Oid is not null)
            {
                account?.Roles?.ForEach((role) =>
                {
                    userIdentity.AddClaim(new Claim("role", role));
                });

                try
                {
                    var client = new GraphServiceClient(
                        new HttpClient(),
                        serviceProvider
                            .GetRequiredService<IAuthenticationProvider>(),
                        baseUrl);

                    var user = await client.Me.GetAsync();

                    if (user is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            user.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            user.OfficeLocation ?? "Not set"));
                    }

                    var memberOf = client.Users[account?.Oid].MemberOf;

                    var graphDirectoryRoles = await memberOf.GraphDirectoryRole.GetAsync();

                    if (graphDirectoryRoles?.Value is not null)
                    {
                        foreach (var entry in graphDirectoryRoles.Value)
                        {
                            if (entry.RoleTemplateId is not null)
                            {
                                userIdentity.AddClaim(
                                    new Claim("directoryRole", entry.RoleTemplateId));
                            }
                        }
                    }

                    var graphAdministrativeUnits = await memberOf.GraphAdministrativeUnit.GetAsync();

                    if (graphAdministrativeUnits?.Value is not null)
                    {
                        foreach (var entry in graphAdministrativeUnits.Value)
                        {
                            if (entry.Id is not null)
                            {
                                userIdentity.AddClaim(
                                    new Claim("administrativeUnit", entry.Id));
                            }
                        }
                    }

                    var graphGroups = await memberOf.GraphGroup.GetAsync();

                    if (graphGroups?.Value is not null)
                    {
                        foreach (var entry in graphGroups.Value)
                        {
                            if (entry.Id is not null)
                            {
                                userIdentity.AddClaim(
                                    new Claim("directoryGroup", entry.Id));
                            }
                        }
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}
```

The preceding code:

* Doesn't include transitive memberships. If the app requires direct and transitive group membership claims, replace the `MemberOf` property (`IUserMemberOfCollectionWithReferencesRequestBuilder`) with `TransitiveMemberOf` (`IUserTransitiveMemberOfCollectionWithReferencesRequestBuilder`).
* Sets GUID values in `directoryRole` claims are ME-ID Administrator Role [*Template IDs*](/en-us/entra/identity/role-based-access-control/permissions-reference) (`Microsoft.Graph.Models.DirectoryRole.RoleTemplateId`). Template IDs are stable identifiers for creating user authorization policies in apps, which is covered later in this article. Don't use `entry.Id` for directory role claim values, as they aren't stable across tenants.

Next, configure the MSAL authentication to use the custom user account factory.

Confirm that the `Program` file uses the [Microsoft.AspNetCore.Components.WebAssembly.Authentication](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication) namespace:

```
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
```

Update the [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication) call to the following. Note that the Blazor framework's [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) is replaced by the app's `CustomUserAccount` for the MSAL authentication and account claims principal factory:

```
builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
    CustomUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd",
            options.ProviderOptions.Authentication);
        options.UserOptions.RoleClaim = "role";
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, CustomUserAccount,
        CustomAccountFactory>();
```

Confirm the presence of the *Graph SDK* code in the `Program` file described by the [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0\&pivots=graph-sdk) article:

```
var baseUrl =
    string.Join("/",
        builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
            "https://graph.microsoft.com",
        builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
            "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);
```

Important

Confirm in the app's registration in the Azure portal that the following permissions are granted:

* `User.Read`
* `RoleManagement.Read.Directory` (Requires admin consent)
* `AdministrativeUnit.Read.All` (Requires admin consent)

Confirm that the `wwwroot/appsettings.json` configuration is correct per the *Graph SDK* guidance.

`wwwroot/appsettings.json`:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "User.Read",
      "RoleManagement.Read.Directory",
      "AdministrativeUnit.Read.All"
    ]
  }
}
```

Provide values for the following placeholders from the app's ME-ID registration in the Azure portal:

* `{TENANT ID}`: The Directory (Tenant) Id GUID value.
* `{CLIENT ID}`: The Application (Client) Id GUID value.

## Authorization configuration

Create a [policy](../../../security/authorization/policies?view=aspnetcore-10.0) for each [App Role](#app-roles) (by role name), ME-ID built-in Administrator Role (by Role Template Id/GUID), or security group (by Object Id/GUID) in the `Program` file. The following example creates a policy for the ME-ID built-in *Billing Administrator* role:

```
builder.Services.AddAuthorizationCore(options =>
{
    options.AddPolicy("BillingAdministrator", policy => 
        policy.RequireClaim("directoryRole", 
            "b0f54661-2d74-4c50-afa3-1ec803f12efe"));
});
```

For the complete list of IDs (GUIDs) for ME-ID Administrator Roles, see [Role template IDs](/en-us/entra/identity/role-based-access-control/permissions-reference) in the ME-ID documentation. For an Azure security or O365 group ID (GUID), see the **Object Id** for the group in the Azure portal **Groups** pane of the app's registration. For more information on authorization policies, see [Policy-based authorization in ASP.NET Core](../../../security/authorization/policies?view=aspnetcore-10.0).

In the following examples, the app uses the preceding policy to authorize the user.

The [`AuthorizeView` component](../?view=aspnetcore-10.0#authorizeview-component) works with the policy:

```
<AuthorizeView Policy="BillingAdministrator">
    <Authorized>
        <p>
            The user is in the 'Billing Administrator' ME-ID Administrator Role
            and can see this content.
        </p>
    </Authorized>
    <NotAuthorized>
        <p>
            The user is NOT in the 'Billing Administrator' role and sees this
            content.
        </p>
    </NotAuthorized>
</AuthorizeView>
```

Access to an entire component can be based on the policy using an [`[Authorize]` attribute directive](../?view=aspnetcore-10.0#authorize-attribute) ([AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute)):

```
@page "/"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "BillingAdministrator")]
```

If the user isn't authorized, they're redirected to the ME-ID sign-in page.

A policy check can also be [performed in code with procedural logic](../?view=aspnetcore-10.0#procedural-logic).

`CheckPolicy.razor`:

```
@page "/checkpolicy"
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

<h1>Check Policy</h1>

<p>This component checks a policy in code.</p>

<button @onclick="CheckPolicy">Check 'BillingAdministrator' policy</button>

<p>Policy Message: @policyMessage</p>

@code {
    private string policyMessage = "Check hasn't been made yet.";

    [CascadingParameter]
    private Task<AuthenticationState> authenticationStateTask { get; set; }

    private async Task CheckPolicy()
    {
        var user = (await authenticationStateTask).User;

        if ((await AuthorizationService.AuthorizeAsync(user, 
            "BillingAdministrator")).Succeeded)
        {
            policyMessage = "Yes! The 'BillingAdministrator' policy is met.";
        }
        else
        {
            policyMessage = "No! 'BillingAdministrator' policy is NOT met.";
        }
    }
}
```

Using the preceding approaches, you can also create policy-based access for security groups, where the GUID used for the policy matches the

## App Roles

To configure the app in the Azure portal to provide App Roles membership claims, see [Add app roles to your application and receive them in the token](/en-us/entra/identity-platform/howto-add-app-roles-in-apps) in the ME-ID documentation.

The following example assumes that the app is configured with two roles, and the roles are assigned to a test user:

* `Admin`
* `Developer`

Although you can't [assign roles to groups](/en-us/entra/identity/role-based-access-control/groups-concept) without an ME-ID Premium account, you can assign roles to users and receive role claims for users with a standard Azure account. The guidance in this section doesn't require an ME-ID Premium account.

Take either of the following approaches add app roles in ME-ID:

* When working with the ***default directory***, follow the guidance in [Add app roles to your application and receive them in the token](/en-us/entra/identity-platform/howto-add-app-roles-in-apps) to create ME-ID roles.

* If you ***aren't working with the default directory***, edit the app's manifest in the Azure portal to establish the app's roles manually in the `appRoles` entry of the manifest file. The following is an example `appRoles` entry that creates `Admin` and `Developer` roles. These example roles are used later at the component level to implement access restrictions:

  Important

  The following approach is only recommended for apps that aren't registered in the Azure account's default directory. For apps registered in the default directory, see the preceding bullet of this list.

  ```
  "appRoles": [
    {
      "allowedMemberTypes": [
        "User"
      ],
      "description": "Administrators manage developers.",
      "displayName": "Admin",
      "id": "{ADMIN GUID}",
      "isEnabled": true,
      "lang": null,
      "origin": "Application",
      "value": "Admin"
    },
    {
      "allowedMemberTypes": [
        "User"
      ],
      "description": "Developers write code.",
      "displayName": "Developer",
      "id": "{DEVELOPER GUID}",
      "isEnabled": true,
      "lang": null,
      "origin": "Application",
      "value": "Developer"
    }
  ],
  ```

  For the `{ADMIN GUID}` and `{DEVELOPER GUID}` placeholders in the preceding example, you can generate GUIDs with an [online GUID generator (Google search result for "guid generator")](https://www.google.com/search?q=guid+generator).

To assign a role to a user (or group if you have a Premium tier Azure account):

1. Navigate to **Enterprise applications** in the ME-ID area of the [Azure portal](https://portal.azure.com/).
2. Select the app. Select **Manage** > **Users and groups** from the sidebar.
3. Select the checkbox for one or more user accounts.
4. From the menu above the list of users, select **Edit assignment**.
5. For the **Select a role** entry, select **None selected**.
6. Choose a role from the list and use the **Select** button to select it.
7. Use the **Assign** button at the bottom of the screen to assign the role.

Multiple roles are assigned in the Azure portal by ***re-adding a user*** for each additional role assignment. Use the **Add user/group** button at the top of the list of users to re-add a user. Use the preceding steps to assign another role to the user. You can repeat this process as many times as needed to add additional roles to a user (or group).

The `CustomAccountFactory` shown in the [Custom user account](#custom-user-account) section is set up to act on a `role` claim with a JSON array value. Add and register the `CustomAccountFactory` in the app as shown in the [Custom user account](#custom-user-account) section. There's no need to provide code to remove the original `role` claim because it's automatically removed by the framework.

In the `Program` file, add or confirm the claim named "`role`" as the role claim for [ClaimsPrincipal.IsInRole](/en-us/dotnet/api/system.security.claims.claimsprincipal.isinrole) checks:

```
builder.Services.AddMsalAuthentication(options =>
{
    ...

    options.UserOptions.RoleClaim = "role";
});
```

Note

If you prefer to use the `directoryRoles` claim (ME-ID Administrator Roles), assign "`directoryRoles`" to the [RemoteAuthenticationUserOptions.RoleClaim](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticationuseroptions.roleclaim#microsoft-aspnetcore-components-webassembly-authentication-remoteauthenticationuseroptions-roleclaim).

After you've completed the preceding steps to create and assign roles to users (or groups if you have a Premium tier Azure account) and implemented the `CustomAccountFactory` with the Graph SDK, as explained earlier in this article and in [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0\&pivots=graph-sdk), you should see an `role` claim for each assigned role that a signed-in user is assigned (or roles assigned to groups that they are members of). Run the app with a test user to confirm the claims are present as expected. When testing with the Graph SDK locally, we recommend using a new in-private/incognito browser session for each test to prevent lingering cookies from interfering with tests. For more information, see [Secure an ASP.NET Core Blazor WebAssembly standalone app with Microsoft Entra ID](standalone-with-microsoft-entra-id?view=aspnetcore-10.0#troubleshoot).

Component authorization approaches are functional at this point. Any of the authorization mechanisms in components of the app can use the `Admin` role to authorize the user:

* [`AuthorizeView` component](../?view=aspnetcore-10.0#authorizeview-component)

  ```
  <AuthorizeView Roles="Admin">
  ```

* [`[Authorize]` attribute directive](../?view=aspnetcore-10.0#authorize-attribute) ([AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute))

  ```
  @attribute [Authorize(Roles = "Admin")]
  ```

* [Procedural logic](../?view=aspnetcore-10.0#procedural-logic)

  ```
  var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
  var user = authState.User;

  if (user.IsInRole("Admin")) { ... }
  ```

Multiple role tests are supported:

* Require that the user be in **either** the `Admin` **or** `Developer` role with the `AuthorizeView` component:

  ```
  <AuthorizeView Roles="Admin, Developer">
      ...
  </AuthorizeView>
  ```

* Require that the user be in **both** the `Admin` **and** `Developer` roles with the `AuthorizeView` component:

  ```
  <AuthorizeView Roles="Admin">
      <AuthorizeView Roles="Developer" Context="innerContext">
          ...
      </AuthorizeView>
  </AuthorizeView>
  ```

  For more information on the `Context` for the inner [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview), see [ASP.NET Core Blazor authentication and authorization](../?view=aspnetcore-10.0#role-based-and-policy-based-authorization).

* Require that the user be in **either** the `Admin` **or** `Developer` role with the `[Authorize]` attribute:

  ```
  @attribute [Authorize(Roles = "Admin, Developer")]
  ```

* Require that the user be in **both** the `Admin` **and** `Developer` roles with the `[Authorize]` attribute:

  ```
  @attribute [Authorize(Roles = "Admin")]
  @attribute [Authorize(Roles = "Developer")]
  ```

* Require that the user be in **either** the `Admin` **or** `Developer` role with procedural code:

  ```
  @code {
      private async Task DoSomething()
      {
          var authState = await AuthenticationStateProvider
              .GetAuthenticationStateAsync();
          var user = authState.User;

          if (user.IsInRole("Admin") || user.IsInRole("Developer"))
          {
              ...
          }
          else
          {
              ...
          }
      }
  }
  ```

* Require that the user be in **both** the `Admin` **and** `Developer` roles with procedural code by changing the [conditional OR (`||`)](/en-us/dotnet/csharp/language-reference/operators/boolean-logical-operators) to a [conditional AND (`&&`)](/en-us/dotnet/csharp/language-reference/operators/boolean-logical-operators) in the preceding example:

  ```
  if (user.IsInRole("Admin") && user.IsInRole("Developer"))
  ```

Multiple role tests are supported:

* Require that the user be in **either** the `Admin` **or** `Developer` role with the `[Authorize]` attribute:

  ```
  [Authorize(Roles = "Admin, Developer")]
  ```

* Require that the user be in **both** the `Admin` **and** `Developer` roles with the `[Authorize]` attribute:

  ```
  [Authorize(Roles = "Admin")]
  [Authorize(Roles = "Developer")]
  ```

* Require that the user be in **either** the `Admin` **or** `Developer` role with procedural code:

  ```
  static readonly string[] scopeRequiredByApi = new string[] { "API.Access" };

  ...

  [HttpGet]
  public IEnumerable<ReturnType> Get()
  {
      HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

      if (User.IsInRole("Admin") || User.IsInRole("Developer"))
      {
          ...
      }
      else
      {
          ...
      }

      return ...
  }
  ```

* Require that the user be in **both** the `Admin` **and** `Developer` roles with procedural code by changing the [conditional OR (`||`)](/en-us/dotnet/csharp/language-reference/operators/boolean-logical-operators) to a [conditional AND (`&&`)](/en-us/dotnet/csharp/language-reference/operators/boolean-logical-operators) in the preceding example:

  ```
  if (User.IsInRole("Admin") && User.IsInRole("Developer"))
  ```

Role matching is typically case-sensitive because role names are stored and compared using .NET string comparisons. For example, `Admin` (uppercase `A`) isn't treated as the same role as `admin` (lowercase `a`). For more information, see [Claim-based authorization in ASP.NET Core](../../../security/authorization/claims?view=aspnetcore-10.0#claim-case-sensitivity). By contrast, ASP.NET Core policy name lookup is typically case-insensitive, so `RequireAdministratorRole` and `requireadministratorrole` refer to the same policy.

## Related content

* [Role template IDs (ME-ID documentation)](/en-us/entra/identity/role-based-access-control/permissions-reference)
* [`groupMembershipClaims` attribute (ME-ID documentation)](/en-us/entra/identity-platform/reference-app-manifest#groupmembershipclaims-attribute)
* [Add app roles to your application and receive them in the token (ME-ID documentation)](/en-us/entra/identity-platform/howto-add-app-roles-in-apps)
* [Application roles (Azure documentation)](/en-us/azure/architecture/guide/multitenant/considerations/identity)
* [Claim-based authorization in ASP.NET Core](../../../security/authorization/claims?view=aspnetcore-10.0)
* [Role-based authorization in ASP.NET Core](../../../security/authorization/roles?view=aspnetcore-10.0)
* [ASP.NET Core Blazor authentication and authorization](../?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/http-handlers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/http-handlers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate HTTP handlers to ASP.NET Core middleware

This article shows how to migrate existing ASP.NET [HTTP handlers from system.webserver](/en-us/iis/configuration/system.webserver/) to ASP.NET Core [middleware](../../../fundamentals/middleware/?view=aspnetcore-10.0).

## Handlers revisited

Before proceeding to ASP.NET Core middleware, let's first recap how HTTP handlers work:

**Handlers are:**

* Classes that implement [IHttpHandler](/en-us/dotnet/api/system.web.ihttphandler)

* Used to handle requests with a given file name or extension, such as *.report*

* [Configured](/en-us/iis/configuration/system.webserver/handlers/) in *Web.config*

## From handlers to middleware

**Middleware are simpler than HTTP handlers:**

* Handlers, *Web.config* (except for IIS configuration) and the application life cycle are gone

* The roles of handlers have been taken over by middleware

* Middleware are configured using code rather than in *Web.config*

- [Pipeline branching](../../../fundamentals/middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline) lets you send requests to specific middleware, based on not only the URL but also on request headers, query strings, etc.

* [Pipeline branching](../../../fundamentals/middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline) lets you send requests to specific middleware, based on not only the URL but also on request headers, query strings, etc.

**Middleware are very similar to handlers:**

* Able to create their own HTTP response

## Migrating handler code to middleware

An HTTP handler looks something like this:

```
// ASP.NET 4 handler

using System.Web;

namespace MyApp.HttpHandlers
{
    public class MyHandler : IHttpHandler
    {
        public bool IsReusable { get { return true; } }

        public void ProcessRequest(HttpContext context)
        {
            string response = GenerateResponse(context);

            context.Response.ContentType = GetContentType();
            context.Response.Output.Write(response);
        }

        // ...

        private string GenerateResponse(HttpContext context)
        {
            string title = context.Request.QueryString["title"];
            return string.Format("Title of the report: {0}", title);
        }

        private string GetContentType()
        {
            return "text/plain";
        }
    }
}
```

In your ASP.NET Core project, you would translate this to a middleware similar to this:

```
// ASP.NET Core middleware migrated from a handler

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace MyApp.Middleware
{
    public class MyHandlerMiddleware
    {

        // Must have constructor with this signature, otherwise exception at run time
        public MyHandlerMiddleware(RequestDelegate next)
        {
            // This is an HTTP Handler, so no need to store next
        }

        public async Task Invoke(HttpContext context)
        {
            string response = GenerateResponse(context);

            context.Response.ContentType = GetContentType();
            await context.Response.WriteAsync(response);
        }

        // ...

        private string GenerateResponse(HttpContext context)
        {
            string title = context.Request.Query["title"];
            return string.Format("Title of the report: {0}", title);
        }

        private string GetContentType()
        {
            return "text/plain";
        }
    }

    public static class MyHandlerExtensions
    {
        public static IApplicationBuilder UseMyHandler(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<MyHandlerMiddleware>();
        }
    }
}
```

This middleware is very similar to the middleware corresponding to modules. The only real difference is that here there's no call to `_next.Invoke(context)`. That makes sense, because the handler is at the end of the request pipeline, so there will be no next middleware to invoke.

## Migrating handler insertion into the request pipeline

Configuring an HTTP handler is done in *Web.config* and looks something like this:

```
<?xml version="1.0" encoding="utf-8"?>
<!--ASP.NET 4 web.config-->
<configuration>
  <system.webServer>
    <handlers>
      <add name="MyHandler" verb="*" path="*.report" type="MyApp.HttpHandlers.MyHandler" resourceType="Unspecified" preCondition="integratedMode"/>
    </handlers>
  </system.webServer>
</configuration>
```

You could convert this by adding your new handler middleware to the request pipeline in your `Startup` class, similar to middleware converted from modules. The problem with that approach is that it would send all requests to your new handler middleware. However, you only want requests with a given extension to reach your middleware. That would give you the same functionality you had with your HTTP handler.

One solution is to branch the pipeline for requests with a given extension, using the `MapWhen` extension method. You do this in the same `Configure` method where you add the other middleware:

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseMyMiddleware();

    app.UseMyMiddlewareWithParams();

    var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>();
    var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
    app.UseMyMiddlewareWithParams(myMiddlewareOptions);
    app.UseMyMiddlewareWithParams(myMiddlewareOptions2);

    app.UseMyTerminatingMiddleware();

    // Create branch to the MyHandlerMiddleware. 
    // All requests ending in .report will follow this branch.
    app.MapWhen(
        context => context.Request.Path.ToString().EndsWith(".report"),
        appBranch => {
            // ... optionally add more middleware to this branch
            appBranch.UseMyHandler();
        });

    app.MapWhen(
        context => context.Request.Path.ToString().EndsWith(".context"),
        appBranch => {
            appBranch.UseHttpContextDemoMiddleware();
        });

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

`MapWhen` takes these parameters:

1. A lambda that takes the `HttpContext` and returns `true` if the request should go down the branch. This means you can branch requests not just based on their extension, but also on request headers, query string parameters, etc.

2. A lambda that takes an `IApplicationBuilder` and adds all the middleware for the branch. This means you can add additional middleware to the branch in front of your handler middleware.

Middleware added to the pipeline before the branch will be invoked on all requests; the branch will have no impact on them.

For additional details, see the [middleware documentation](../../../fundamentals/middleware/?view=aspnetcore-10.0) for additional ways to use middleware to replace your usage of handlers.

## Migrating to the new HttpContext

The `Invoke` method in your middleware takes a parameter of type `HttpContext`:

```
public async Task Invoke(HttpContext context)
```

`HttpContext` has significantly changed in ASP.NET Core. For detailed information on how to translate the most commonly used properties of `System.Web.HttpContext` to the new `Microsoft.AspNetCore.Http.HttpContext`, see [Migrate from ASP.NET Framework HttpContext to ASP.NET Core](http-context?view=aspnetcore-10.0).

## Related content

* [HTTP Handlers and HTTP Modules Overview](/en-us/iis/configuration/system.webserver/)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/configuration
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/configuration.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor configuration

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure Blazor apps, including app settings, authentication, and logging configuration.

This guidance applies to client-side project configuration in a Blazor Web App or a standalone Blazor WebAssembly app.

Default behavior in Blazor Web Apps:

* For server-side configuration:

  * See [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0) for guidance.
  * Only configuration in the project's root app settings files is loaded.
  * The remainder of this article only applies to client-side configuration in the `.Client` project.

* For client-side configuration (`.Client` project), configuration is loaded from the following app settings files:

  * `wwwroot/appsettings.json`.
  * `wwwroot/appsettings.{ENVIRONMENT}.json`, where the `{ENVIRONMENT}` placeholder is the app's [runtime environment](../../fundamentals/environments?view=aspnetcore-10.0).

In standalone Blazor WebAssembly apps, configuration is loaded from the following app settings files:

* `wwwroot/appsettings.json`.
* `wwwroot/appsettings.{ENVIRONMENT}.json`, where the `{ENVIRONMENT}` placeholder is the app's [runtime environment](../../fundamentals/environments?view=aspnetcore-10.0).

This guidance applies to the **`Client`** project of a hosted Blazor WebAssembly solution or a Blazor WebAssembly app.

For server-side ASP.NET Core app configuration in the **`Server`** project of a hosted Blazor WebAssembly solution, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

On the client, configuration is loaded from the following app settings files:

* `wwwroot/appsettings.json`.
* `wwwroot/appsettings.{ENVIRONMENT}.json`, where the `{ENVIRONMENT}` placeholder is the app's [runtime environment](../../fundamentals/environments?view=aspnetcore-10.0).

Note

Logging configuration placed into an app settings file in `wwwroot` isn't loaded by default. For more information, see the [Logging configuration](#logging-configuration) section later in this article.

In some scenarios, such as with Azure services, it's important to use an environment file name segment that exactly matches the environment name. For example, use the file name `appsettings.Staging.json` with a capital "S" for the `Staging` environment. For recommended conventions, see the opening remarks of [ASP.NET Core Blazor environments](environments?view=aspnetcore-10.0).

Other configuration providers registered by the app can also provide configuration, but not all providers or provider features are appropriate:

* [Azure Key Vault configuration provider](../../security/key-vault-configuration?view=aspnetcore-10.0): The provider isn't supported for managed identity and application ID (client ID) with client secret scenarios. Application ID with a client secret isn't recommended for any ASP.NET Core app, especially client-side apps because the client secret can't be secured client-side to access the Azure Key Vault service.
* [Azure App configuration provider](/en-us/azure/azure-app-configuration/quickstart-aspnet-core-app): The provider isn't appropriate for a client-side app because the app doesn't run on a server in Azure.

For more information on configuration providers, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

Warning

Configuration and settings files in the web root (`wwwroot` folder) are visible to users on the client, and users can tamper with the data. **Don't store app secrets, credentials, or any other sensitive data in any web root file.**

## App settings configuration

Configuration in app settings files are loaded by default. In the following example, a UI configuration value is stored in an app settings file and loaded by the Blazor framework automatically. The value is read by a component.

`wwwroot/appsettings.json`:

```
{
    "h1FontSize": "50px"
}
```

Inject an [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) instance into a component to access the configuration data.

`ConfigExample.razor`:

```
@page "/config-example"
@inject IConfiguration Configuration

<PageTitle>Configuration</PageTitle>

<h1 style="font-size:@Configuration["h1FontSize"]">
    Configuration example (50px)
</h1>
```

```
@page "/config-example"
@inject IConfiguration Configuration

<PageTitle>Configuration</PageTitle>

<h1 style="font-size:@Configuration["h1FontSize"]">
    Configuration example (50px)
</h1>
```

```
@page "/config-example"
@inject IConfiguration Configuration

<h1 style="font-size:@Configuration["h1FontSize"]">
    Configuration example
</h1>
```

```
@page "/config-example"
@inject IConfiguration Configuration

<h1 style="font-size:@Configuration["h1FontSize"]">
    Configuration example
</h1>
```

```
@page "/config-example"
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<h1 style="font-size:@Configuration["h1FontSize"]">
    Configuration example
</h1>
```

```
@page "/config-example"
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<h1 style="font-size:@Configuration["h1FontSize"]">
    Configuration example
</h1>
```

Client security restrictions prevent direct access to files via user code, including settings files for app configuration. To read configuration files in addition to `appsettings.json`/`appsettings.{ENVIRONMENT}.json` from the `wwwroot` folder into configuration, use an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).

Warning

Configuration and settings files in the web root (`wwwroot` folder) are visible to users on the client, and users can tamper with the data. **Don't store app secrets, credentials, or any other sensitive data in any web root file.**

The following example reads a configuration file (`cars.json`) into the app's configuration.

`wwwroot/cars.json`:

```
{
    "size": "tiny"
}
```

Add the namespace for [Microsoft.Extensions.Configuration](/en-us/dotnet/api/microsoft.extensions.configuration) to the `Program` file:

```
using Microsoft.Extensions.Configuration;
```

Modify the existing [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service registration to use the client to read the file:

```
var http = new HttpClient()
{
    BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
};

builder.Services.AddScoped(sp => http);

using var response = await http.GetAsync("cars.json");
using var stream = await response.Content.ReadAsStreamAsync();

builder.Configuration.AddJsonStream(stream);
```

The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` ([IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress)), which gets the base address for the app and is typically derived from the `<base>` tag's `href` value in the host page.

## Memory Configuration Source

The following example uses a [MemoryConfigurationSource](/en-us/dotnet/api/microsoft.extensions.configuration.memory.memoryconfigurationsource) in the `Program` file to supply additional configuration.

Add the namespace for [Microsoft.Extensions.Configuration.Memory](/en-us/dotnet/api/microsoft.extensions.configuration.memory) to the `Program` file:

```
using Microsoft.Extensions.Configuration.Memory;
```

In the `Program` file:

```
var vehicleData = new Dictionary<string, string?>()
{
    { "color", "blue" },
    { "type", "car" },
    { "wheels:count", "3" },
    { "wheels:brand", "Blazin" },
    { "wheels:brand:type", "rally" },
    { "wheels:year", "2008" },
};

var memoryConfig = new MemoryConfigurationSource { InitialData = vehicleData };

builder.Configuration.Add(memoryConfig);
```

Inject an [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) instance into a component to access the configuration data.

`MemoryConfig.razor`:

```
@page "/memory-config"
@inject IConfiguration Configuration

<PageTitle>Memory Configuration</PageTitle>

<h1>Memory Configuration Example</h1>

<h2>General specifications</h2>

<ul>
    <li>Color: @Configuration["color"]</li>
    <li>Type: @Configuration["type"]</li>
</ul>

<h2>Wheels</h2>

<ul>
    <li>Count: @Configuration["wheels:count"]</li>
    <li>Brand: @Configuration["wheels:brand"]</li>
    <li>Type: @Configuration["wheels:brand:type"]</li>
    <li>Year: @Configuration["wheels:year"]</li>
</ul>
```

```
@page "/memory-config"
@inject IConfiguration Configuration

<PageTitle>Memory Configuration</PageTitle>

<h1>Memory Configuration Example</h1>

<h2>General specifications</h2>

<ul>
    <li>Color: @Configuration["color"]</li>
    <li>Type: @Configuration["type"]</li>
</ul>

<h2>Wheels</h2>

<ul>
    <li>Count: @Configuration["wheels:count"]</li>
    <li>Brand: @Configuration["wheels:brand"]</li>
    <li>Type: @Configuration["wheels:brand:type"]</li>
    <li>Year: @Configuration["wheels:year"]</li>
</ul>
```

```
@page "/memory-config"
@inject IConfiguration Configuration

<h1>Memory configuration example</h1>

<h2>General specifications</h2>

<ul>
    <li>Color: @Configuration["color"]</li>
    <li>Type: @Configuration["type"]</li>
</ul>

<h2>Wheels</h2>

<ul>
    <li>Count: @Configuration["wheels:count"]</li>
    <li>Brand: @Configuration["wheels:brand"]</li>
    <li>Type: @Configuration["wheels:brand:type"]</li>
    <li>Year: @Configuration["wheels:year"]</li>
</ul>
```

```
@page "/memory-config"
@inject IConfiguration Configuration

<h1>Memory configuration example</h1>

<h2>General specifications</h2>

<ul>
    <li>Color: @Configuration["color"]</li>
    <li>Type: @Configuration["type"]</li>
</ul>

<h2>Wheels</h2>

<ul>
    <li>Count: @Configuration["wheels:count"]</li>
    <li>Brand: @Configuration["wheels:brand"]</li>
    <li>Type: @Configuration["wheels:brand:type"]</li>
    <li>Year: @Configuration["wheels:year"]</li>
</ul>
```

```
@page "/memory-config"
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<h1>Memory configuration example</h1>

<h2>General specifications</h2>

<ul>
    <li>Color: @Configuration["color"]</li>
    <li>Type: @Configuration["type"]</li>
</ul>

<h2>Wheels</h2>

<ul>
    <li>Count: @Configuration["wheels:count"]</li>
    <li>Brand: @Configuration["wheels:brand"]</li>
    <li>Type: @Configuration["wheels:brand:type"]</li>
    <li>Year: @Configuration["wheels:year"]</li>
</ul>
```

```
@page "/memory-config"
@using Microsoft.Extensions.Configuration
@inject IConfiguration Configuration

<h1>Memory configuration example</h1>

<h2>General specifications</h2>

<ul>
    <li>Color: @Configuration["color"]</li>
    <li>Type: @Configuration["type"]</li>
</ul>

<h2>Wheels</h2>

<ul>
    <li>Count: @Configuration["wheels:count"]</li>
    <li>Brand: @Configuration["wheels:brand"]</li>
    <li>Type: @Configuration["wheels:brand:type"]</li>
    <li>Year: @Configuration["wheels:year"]</li>
</ul>
```

Obtain a section of the configuration in C# code with [IConfiguration.GetSection](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getsection). The following example obtains the `wheels` section for the configuration in the preceding example:

```
@code {
    protected override void OnInitialized()
    {
        var wheelsSection = Configuration.GetSection("wheels");

        ...
    }
}
```

## Authentication configuration

Provide ***public*** authentication configuration in an app settings file.

`wwwroot/appsettings.json`:

```
{
  "Local": {
    "Authority": "{AUTHORITY}",
    "ClientId": "{CLIENT ID}"
  }
}
```

Load the configuration for an Identity provider with [ConfigurationBinder.Bind](/en-us/dotnet/api/microsoft.extensions.configuration.configurationbinder.bind) in the `Program` file. The following example loads configuration for an [OIDC provider](../security/webassembly/standalone-with-authentication-library?view=aspnetcore-10.0):

```
builder.Services.AddOidcAuthentication(options =>
    builder.Configuration.Bind("Local", options.ProviderOptions));
```

Warning

Configuration and settings files in the web root (`wwwroot` folder) are visible to users on the client, and users can tamper with the data. **Don't store app secrets, credentials, or any other sensitive data in any web root file.**

## Logging configuration

*This section applies to apps that configure logging via an app settings file in the `wwwroot` folder.*

Add the [`Microsoft.Extensions.Logging.Configuration` package](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Configuration) to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the app settings file, provide logging configuration. The logging configuration is loaded in the `Program` file.

`wwwroot/appsettings.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
```

In the `Program` file:

```
builder.Logging.AddConfiguration(
    builder.Configuration.GetSection("Logging"));
```

## Host builder configuration

Read host builder configuration from [WebAssemblyHostBuilder.Configuration](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.configuration#microsoft-aspnetcore-components-webassembly-hosting-webassemblyhostbuilder-configuration) in the `Program` file:

```
var hostname = builder.Configuration["HostName"];
```

## Cached configuration

Configuration files are cached for offline use. With [Progressive Web Applications (PWAs)](../progressive-web-app/?view=aspnetcore-10.0), you can only update configuration files when creating a new deployment. Editing configuration files between deployments has no effect because:

* Users have cached versions of the files that they continue to use.
* The PWA's `service-worker.js` and `service-worker-assets.js` files must be rebuilt on compilation, which signal to the app on the user's next online visit that the app has been redeployed.

For more information on how background updates are handled by PWAs, see [ASP.NET Core Blazor Progressive Web Application (PWA)](../progressive-web-app/?view=aspnetcore-10.0#background-updates).

## Options configuration

[Options configuration](../../fundamentals/configuration/options?view=aspnetcore-10.0) uses API in the [`Microsoft.Extensions.Options.ConfigurationExtensions`](https://www.nuget.org/packages/Microsoft.Extensions.Options.ConfigurationExtensions) NuGet package.

Example:

`OptionsExample.cs`:

```
public class OptionsExample
{
    public string? Option1 { get; set; }
    public string? Option2 { get; set; }
}
```

In `appsettings.json`:

```
"OptionsExample": {
  "Option1": "Option1 Value",
  "Option2": "Option2 Value"
}
```

```
builder.Services.Configure<OptionsExample>(
    builder.Configuration.GetSection("OptionsExample"));
```

The following Razor component retrieves the settings with the [`@inject`](../../mvc/views/razor?view=aspnetcore-10.0#inject) directive or [`[Inject]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.injectattribute).

`Options.razor`:

```
@page "/options"
@using Microsoft.Extensions.Options
@inject IOptions<OptionsExample>? OptionsExample1

<h1>Options</h1>

<h2>
    &commat;inject approach
</h2>

<ul>
    <li>@OptionsExample1?.Value.Option1</li>
    <li>@OptionsExample1?.Value.Option2</li>
</ul>

<h2>
    [Inject] approach
</h2>

<ul>
    <li>@OptionsExample2?.Value.Option1</li>
    <li>@OptionsExample2?.Value.Option2</li>
</ul>

@code {
    [Inject]
    public IOptions<OptionsExample>? OptionsExample2 { get; set; }
}
```

Not all of the ASP.NET Core Options features are supported in Razor components. For example, [IOptionsSnapshot\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionssnapshot-1) and [IOptionsMonitor\<TOptions>](/en-us/dotnet/api/microsoft.extensions.options.ioptionsmonitor-1) configuration is supported, but recomputing option values for these interfaces isn't supported outside of reloading the app by either requesting the app in a new browser tab or selecting the browser's reload button. Merely calling [`StateHasChanged`](../components/lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged) doesn't update snapshot or monitored option values when the underlying configuration changes.

### Environment variables in Blazor WebAssembly configuration

Blazor WebAssembly applications access environment variables through [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration). This enables runtime configuration without rebuilding the app, making it easier to deploy the same build to different environments.

In the following example, the `API_ENDPOINT` and `ENABLE_FEATURE_X` environment variables are automatically included in configuration:

```
var builder = WebAssemblyHostBuilder.CreateDefault(args);

var apiEndpoint = builder.Configuration["API_ENDPOINT"];
var featureFlag = builder.Configuration["ENABLE_FEATURE_X"];
```

Environment variables are loaded into the configuration system alongside other configuration sources, such as app settings (`appsettings.json`), providing a unified way to access configuration values regardless of their source.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/working-with-sql
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/working-with-sql.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 5, work with a database in an ASP.NET Core MVC app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

## Introduction

This part of the tutorial series focuses on working with a SQL database in your ASP.NET Core MVC application.

You'll learn how to:

* Register and configure the Entity Framework Core database context for your ASP.NET Core MVC app.
* Work with database connection strings for local development.
* Use SQL Server Express LocalDB for development and examine your database and data using SQL Server Object Explorer.
* Seed your database with initial sample data.

## Prerequisite

This tutorial uses a database you set up in the previous step: [Part 4, add a model to an ASP.NET Core MVC app](adding-model?view=aspnetcore-10.0).

## Working with the database context

The `MvcMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in the `Program.cs` file:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext") ?? throw new InvalidOperationException("Connection string 'MvcMovieContext' not found.")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-4ebefa10-de29-4dea-b2ad-8a8dc6bcf374;Trusted_Connection=True;MultipleActiveResultSets=true"
}
```

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString`. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Data Source=MvcMovie.db"
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

## SQL Server Express LocalDB

LocalDB:

* Is a lightweight version of the SQL Server Express Database Engine, installed by default with Visual Studio.
* Starts on demand by using a connection string.
* Is targeted for program development. It runs in user mode, so there's no complex configuration.
* By default creates *.mdf* files in the *C:/Users/{user}* directory.

### Examine the database

From the **View** menu, open **SQL Server Object Explorer** (SSOX).

Right-click on the `Movie` table (`dbo.Movie`) **> View Designer**

Note the key icon next to `ID`. By default, EF makes a property named `ID` the primary key.

Right-click on the `Movie` table **> View Data**

##

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

## Seed the database

Create a new class named `SeedData` in the *Models* folder. Replace the generated code with the following:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;

namespace MvcMovie.Models;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using (var context = new MvcMovieContext(
            serviceProvider.GetRequiredService<
                DbContextOptions<MvcMovieContext>>()))
        {
            // Look for any movies.
            if (context.Movie.Any())
            {
                return;   // DB has been seeded
            }
            context.Movie.AddRange(
                new Movie
                {
                    Title = "When Harry Met Sally",
                    ReleaseDate = DateTime.Parse("1989-2-12"),
                    Genre = "Romantic Comedy",
                    Price = 7.99M
                },
                new Movie
                {
                    Title = "Ghostbusters ",
                    ReleaseDate = DateTime.Parse("1984-3-13"),
                    Genre = "Comedy",
                    Price = 8.99M
                },
                new Movie
                {
                    Title = "Ghostbusters 2",
                    ReleaseDate = DateTime.Parse("1986-2-23"),
                    Genre = "Comedy",
                    Price = 9.99M
                },
                new Movie
                {
                    Title = "Rio Bravo",
                    ReleaseDate = DateTime.Parse("1959-4-15"),
                    Genre = "Western",
                    Price = 3.99M
                }
            );
            context.SaveChanges();
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;  // DB has been seeded.
}
```

[]()

### Add the seed initializer

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Replace the contents of `Program.cs` with the following code. The new code is highlighted.

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext") ?? throw new InvalidOperationException("Connection string 'MvcMovieContext' not found.")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();

app.Run();
```

Test the app. Force the app to initialize, calling the code in the `Program.cs` file, so the seed method runs. To force initialization, close the command prompt window that Visual Studio opened, and restart by pressing Ctrl+F5.

Update `Program.cs` with the following highlighted code:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();

app.Run();
```

Test the app. Stop it and restart it so the `SeedData.Initialize` method runs and seeds the database.

The app shows the seeded data.

And a refresh of the `Movie` table shows the same data.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

[Previous: Adding a model](adding-model?view=aspnetcore-10.0) [Next: Adding controller methods and views](controller-methods-views?view=aspnetcore-10.0)

## Introduction

This part of the tutorial series focuses on working with a SQL database in your ASP.NET Core MVC application.

You'll learn how to:

* Register and configure the Entity Framework Core database context for your ASP.NET Core MVC app.
* Work with database connection strings for local development.
* Use SQL Server Express LocalDB for development and examine your database and data using SQL Server Object Explorer.
* Seed your database with initial sample data.

## Prerequisite

This tutorial uses a database you set up in the previous step: [Part 4, add a model to an ASP.NET Core MVC app](adding-model?view=aspnetcore-10.0).

## Working with the database context

The `MvcMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in the `Program.cs` file:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext") ?? throw new InvalidOperationException("Connection string 'MvcMovieContext' not found.")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-4ebefa10-de29-4dea-b2ad-8a8dc6bcf374;Trusted_Connection=True;MultipleActiveResultSets=true"
}
```

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString`. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Data Source=MvcMovie.db"
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

## SQL Server Express LocalDB

LocalDB:

* Is a lightweight version of the SQL Server Express Database Engine, installed by default with Visual Studio.
* Starts on demand by using a connection string.
* Is targeted for program development. It runs in user mode, so there's no complex configuration.
* By default creates *.mdf* files in the *C:/Users/{user}* directory.

### Examine the database

From the **View** menu, open **SQL Server Object Explorer** (SSOX).

Right-click on the `Movie` table (`dbo.Movie`) **> View Designer**

Note the key icon next to `ID`. By default, EF makes a property named `ID` the primary key.

Right-click on the `Movie` table **> View Data**

##

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

## Seed the database

Create a new class named `SeedData` in the *Models* folder. Replace the generated code with the following:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;

namespace MvcMovie.Models;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using (var context = new MvcMovieContext(
            serviceProvider.GetRequiredService<
                DbContextOptions<MvcMovieContext>>()))
        {
            // Look for any movies.
            if (context.Movie.Any())
            {
                return;   // DB has been seeded
            }
            context.Movie.AddRange(
                new Movie
                {
                    Title = "When Harry Met Sally",
                    ReleaseDate = DateTime.Parse("1989-2-12"),
                    Genre = "Romantic Comedy",
                    Price = 7.99M
                },
                new Movie
                {
                    Title = "Ghostbusters ",
                    ReleaseDate = DateTime.Parse("1984-3-13"),
                    Genre = "Comedy",
                    Price = 8.99M
                },
                new Movie
                {
                    Title = "Ghostbusters 2",
                    ReleaseDate = DateTime.Parse("1986-2-23"),
                    Genre = "Comedy",
                    Price = 9.99M
                },
                new Movie
                {
                    Title = "Rio Bravo",
                    ReleaseDate = DateTime.Parse("1959-4-15"),
                    Genre = "Western",
                    Price = 3.99M
                }
            );
            context.SaveChanges();
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;  // DB has been seeded.
}
```

[]()

### Add the seed initializer

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Replace the contents of `Program.cs` with the following code. The new code is highlighted.

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext") ?? throw new InvalidOperationException("Connection string 'MvcMovieContext' not found.")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

Delete all the records in the database. You can do this with the delete links in the browser or from SSOX.

Test the app. Force the app to initialize, calling the code in the `Program.cs` file, so the seed method runs. To force initialization, close the command prompt window that Visual Studio opened, and restart by pressing Ctrl+F5.

Update `Program.cs` with the following highlighted code:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

Delete all the records in the database.

Test the app. Stop it and restart it so the `SeedData.Initialize` method runs and seeds the database.

The app shows the seeded data.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

[Previous: Adding a model](adding-model?view=aspnetcore-10.0) [Next: Adding controller methods and views](controller-methods-views?view=aspnetcore-10.0)

## Introduction

This part of the tutorial series focuses on working with a SQL database in your ASP.NET Core MVC application.

You'll learn how to:

* Register and configure the Entity Framework Core database context for your ASP.NET Core MVC app.
* Work with database connection strings for local development.
* Use SQL Server Express LocalDB for development and examine your database and data using SQL Server Object Explorer.
* Seed your database with initial sample data.

## Prerequisite

This tutorial uses a database you set up in the previous step: [Part 4, add a model to an ASP.NET Core MVC app](adding-model?view=aspnetcore-10.0).

## Working with the database context

The `MvcMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in the `Program.cs` file:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Data Source=MvcMovieContext-ea7a4069-f366-4742-bd1c-3f753a804ce1.db"
}
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString`. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Data Source=MvcMovie.db"
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

## SQL Server Express LocalDB

LocalDB:

* Is a lightweight version of the SQL Server Express Database Engine, installed by default with Visual Studio.
* Starts on demand by using a connection string.
* Is targeted for program development. It runs in user mode, so there's no complex configuration.
* By default creates *.mdf* files in the *C:/Users/{user}* directory.

### Examine the database

From the **View** menu, open **SQL Server Object Explorer** (SSOX).

Right-click on the `Movie` table (`dbo.Movie`) **> View Designer**

Note the key icon next to `ID`. By default, EF makes a property named `ID` the primary key.

Right-click on the `Movie` table **> View Data**

##

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

## Seed the database

Create a new class named `SeedData` in the *Models* folder. Replace the generated code with the following:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;

namespace MvcMovie.Models;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using (var context = new MvcMovieContext(
            serviceProvider.GetRequiredService<
                DbContextOptions<MvcMovieContext>>()))
        {
            // Look for any movies.
            if (context.Movie.Any())
            {
                return;   // DB has been seeded
            }
            context.Movie.AddRange(
                new Movie
                {
                    Title = "When Harry Met Sally",
                    ReleaseDate = DateTime.Parse("1989-2-12"),
                    Genre = "Romantic Comedy",
                    Price = 7.99M
                },
                new Movie
                {
                    Title = "Ghostbusters ",
                    ReleaseDate = DateTime.Parse("1984-3-13"),
                    Genre = "Comedy",
                    Price = 8.99M
                },
                new Movie
                {
                    Title = "Ghostbusters 2",
                    ReleaseDate = DateTime.Parse("1986-2-23"),
                    Genre = "Comedy",
                    Price = 9.99M
                },
                new Movie
                {
                    Title = "Rio Bravo",
                    ReleaseDate = DateTime.Parse("1959-4-15"),
                    Genre = "Western",
                    Price = 3.99M
                }
            );
            context.SaveChanges();
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;  // DB has been seeded.
}
```

[]()

### Add the seed initializer

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Replace the contents of `Program.cs` with the following code. The new code is highlighted.

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

Delete all the records in the database. You can do this with the delete links in the browser or from SSOX.

Test the app. Force the app to initialize, calling the code in the `Program.cs` file, so the seed method runs. To force initialization, close the command prompt window that Visual Studio opened, and restart by pressing Ctrl+F5.

Update `Program.cs` with the following highlighted code:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

Delete all the records in the database.

Test the app. Stop it and restart it so the `SeedData.Initialize` method runs and seeds the database.

The app shows the seeded data.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

[Previous: Adding a model](adding-model?view=aspnetcore-10.0) [Next: Adding controller methods and views](controller-methods-views?view=aspnetcore-10.0)

## Introduction

This part of the tutorial series focuses on working with a SQL database in your ASP.NET Core MVC application.

You'll learn how to:

* Register and configure the Entity Framework Core database context for your ASP.NET Core MVC app.
* Work with database connection strings for local development.
* Use SQL Server Express LocalDB for development and examine your database and data using SQL Server Object Explorer.
* Seed your database with initial sample data.

## Prerequisite

This tutorial uses a database you set up in the previous step: [Part 4, add a model to an ASP.NET Core MVC app](adding-model?view=aspnetcore-10.0).

## Working with the database context

The `MvcMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in the `Program.cs` file:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Data Source=MvcMovieContext-ea7a4069-f366-4742-bd1c-3f753a804ce1.db"
}
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString`. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Data Source=MvcMovie.db"
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

## SQL Server Express LocalDB

LocalDB:

* Is a lightweight version of the SQL Server Express Database Engine, installed by default with Visual Studio.
* Starts on demand by using a connection string.
* Is targeted for program development. It runs in user mode, so there's no complex configuration.
* By default creates *.mdf* files in the *C:/Users/{user}* directory.

### Examine the database

From the **View** menu, open **SQL Server Object Explorer** (SSOX).

Right-click on the `Movie` table (`dbo.Movie`) **> View Designer**

Note the key icon next to `ID`. By default, EF makes a property named `ID` the primary key.

Right-click on the `Movie` table **> View Data**

##

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

## Seed the database

Create a new class named `SeedData` in the *Models* folder. Replace the generated code with the following:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;

namespace MvcMovie.Models;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using (var context = new MvcMovieContext(
            serviceProvider.GetRequiredService<
                DbContextOptions<MvcMovieContext>>()))
        {
            // Look for any movies.
            if (context.Movie.Any())
            {
                return;   // DB has been seeded
            }
            context.Movie.AddRange(
                new Movie
                {
                    Title = "When Harry Met Sally",
                    ReleaseDate = DateTime.Parse("1989-2-12"),
                    Genre = "Romantic Comedy",
                    Price = 7.99M
                },
                new Movie
                {
                    Title = "Ghostbusters ",
                    ReleaseDate = DateTime.Parse("1984-3-13"),
                    Genre = "Comedy",
                    Price = 8.99M
                },
                new Movie
                {
                    Title = "Ghostbusters 2",
                    ReleaseDate = DateTime.Parse("1986-2-23"),
                    Genre = "Comedy",
                    Price = 9.99M
                },
                new Movie
                {
                    Title = "Rio Bravo",
                    ReleaseDate = DateTime.Parse("1959-4-15"),
                    Genre = "Western",
                    Price = 3.99M
                }
            );
            context.SaveChanges();
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;  // DB has been seeded.
}
```

\<a name=snippet\_"si">

### Add the seed initializer

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

Replace the contents of `Program.cs` with the following code. The new code is highlighted.

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

Delete all the records in the database. You can do this with the delete links in the browser or from SSOX.

Test the app. Force the app to initialize, calling the code in the `Program.cs` file, so the seed method runs. To force initialization, close the command prompt window that Visual Studio opened, and restart by pressing Ctrl+F5.

Update `Program.cs` with the following highlighted code:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

Delete all the records in the database.

Test the app. Stop it and restart it so the `SeedData.Initialize` method runs and seeds the database.

The app shows the seeded data.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

[Previous: Adding a model](adding-model?view=aspnetcore-10.0) [Next: Adding controller methods and views](controller-methods-views?view=aspnetcore-10.0)

## Introduction

This part of the tutorial series focuses on working with a SQL database in your ASP.NET Core MVC application.

You'll learn how to:

* Register and configure the Entity Framework Core database context for your ASP.NET Core MVC app.
* Work with database connection strings for local development.
* Use SQL Server Express LocalDB for development and examine your database and data using SQL Server Object Explorer.
* Seed your database with initial sample data.

## Prerequisite

This tutorial uses a database you set up in the previous step: [Part 4, add a model to an ASP.NET Core MVC app](adding-model?view=aspnetcore-10.0).

## Working with the database context

The `MvcMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in the `Program.cs` file:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-7dc5;Trusted_Connection=True;MultipleActiveResultSets=true"
}
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString`. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Data Source=MvcMovie.db"
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

## SQL Server Express LocalDB

LocalDB:

* Is a lightweight version of the SQL Server Express Database Engine, installed by default with Visual Studio.
* Starts on demand by using a connection string.
* Is targeted for program development. It runs in user mode, so there's no complex configuration.
* By default creates *.mdf* files in the *C:/Users/{user}* directory.

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

## Seed the database

Create a new class named `SeedData` in the *Models* folder. Replace the generated code with the following:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;

namespace MvcMovie.Models
{
    public static class SeedData
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new MvcMovieContext(
                serviceProvider.GetRequiredService<
                    DbContextOptions<MvcMovieContext>>()))
            {
                // Look for any movies.
                if (context.Movie.Any())
                {
                    return;   // DB has been seeded
                }

                context.Movie.AddRange(
                    new Movie
                    {
                        Title = "When Harry Met Sally",
                        ReleaseDate = DateTime.Parse("1989-2-12"),
                        Genre = "Romantic Comedy",
                        Price = 7.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters ",
                        ReleaseDate = DateTime.Parse("1984-3-13"),
                        Genre = "Comedy",
                        Price = 8.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters 2",
                        ReleaseDate = DateTime.Parse("1986-2-23"),
                        Genre = "Comedy",
                        Price = 9.99M
                    },

                    new Movie
                    {
                        Title = "Rio Bravo",
                        ReleaseDate = DateTime.Parse("1959-4-15"),
                        Genre = "Western",
                        Price = 3.99M
                    }
                );
                context.SaveChanges();
            }
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;  // DB has been seeded.
}
```

[]()

### Add the seed initializer

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

Replace the contents of `Program.cs` with the following code. The new code is highlighted.

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

Delete all the records in the database. You can do this with the delete links in the browser or from SSOX.

Test the app. Force the app to initialize, calling the code in the `Program.cs` file, so the seed method runs. To force initialization, close the command prompt window that Visual Studio opened, and restart by pressing Ctrl+F5.

Update `Program.cs` with the following highlighted code:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using MvcMovie.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("MvcMovieContext")));

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

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    SeedData.Initialize(services);
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

Delete all the records in the database.

Test the app. Stop it and restart it so the `SeedData.Initialize` method runs and seeds the database.

The app shows the seeded data.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

[Previous: Adding a model](adding-model?view=aspnetcore-10.0) [Next: Adding controller methods and views](controller-methods-views?view=aspnetcore-10.0)

## Introduction

This part of the tutorial series focuses on working with a SQL database in your ASP.NET Core MVC application.

You'll learn how to:

* Register and configure the Entity Framework Core database context for your ASP.NET Core MVC app.
* Work with database connection strings for local development.
* Use SQL Server Express LocalDB for development and examine your database and data using SQL Server Object Explorer.
* Seed your database with initial sample data.

## Prerequisite

This tutorial uses a database you set up in the previous step: [Part 4, add a model to an ASP.NET Core MVC app](adding-model?view=aspnetcore-10.0).

## Working with the database context

The `MvcMovieContext` object handles the task of connecting to the database and mapping `Movie` objects to database records. The database context is registered with the [Dependency Injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in the `ConfigureServices` method in the `Startup.cs` file:

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_1_visual-studio-code+visual-studio-mac)

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString` key. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-2;Trusted_Connection=True;MultipleActiveResultSets=true"
}
```

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    
    services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlite(Configuration.GetConnectionString("MvcMovieContext")));
}
```

The ASP.NET Core [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) system reads the `ConnectionString`. For local development, it gets the connection string from the `appsettings.json` file:

```
"ConnectionStrings": {
  "MvcMovieContext": "Data Source=MvcMovie.db"
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

## SQL Server Express LocalDB

LocalDB:

* Is a lightweight version of the SQL Server Express Database Engine, installed by default with Visual Studio.
* Starts on demand by using a connection string.
* Is targeted for program development. It runs in user mode, so there's no complex configuration.
* By default creates *.mdf* files in the *C:/Users/{user}* directory.

### Examine the database

From the **View** menu, open **SQL Server Object Explorer** (SSOX).

Right-click on the `Movie` table **> View Designer**

Note the key icon next to `ID`. By default, EF makes a property named `ID` the primary key.

Right-click on the `Movie` table **> View Data**

## SQLite

The [SQLite](https://www.sqlite.org/) website states:

> SQLite is a self-contained, high-reliability, embedded, full-featured, public-domain, SQL database engine. SQLite is the most used database engine in the world.

There are many third party tools you can download to manage and view a SQLite database. The image below is from [DB Browser for SQLite](https://sqlitebrowser.org/). If you have a favorite SQLite tool, leave a comment on what you like about it.

## Seed the database

Create a new class named `SeedData` in the *Models* folder. Replace the generated code with the following:

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;

namespace MvcMovie.Models
{
    public static class SeedData
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new MvcMovieContext(
                serviceProvider.GetRequiredService<
                    DbContextOptions<MvcMovieContext>>()))
            {
                // Look for any movies.
                if (context.Movie.Any())
                {
                    return;   // DB has been seeded
                }

                context.Movie.AddRange(
                    new Movie
                    {
                        Title = "When Harry Met Sally",
                        ReleaseDate = DateTime.Parse("1989-2-12"),
                        Genre = "Romantic Comedy",
                        Price = 7.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters ",
                        ReleaseDate = DateTime.Parse("1984-3-13"),
                        Genre = "Comedy",
                        Price = 8.99M
                    },

                    new Movie
                    {
                        Title = "Ghostbusters 2",
                        ReleaseDate = DateTime.Parse("1986-2-23"),
                        Genre = "Comedy",
                        Price = 9.99M
                    },

                    new Movie
                    {
                        Title = "Rio Bravo",
                        ReleaseDate = DateTime.Parse("1959-4-15"),
                        Genre = "Western",
                        Price = 3.99M
                    }
                );
                context.SaveChanges();
            }
        }
    }
}
```

If there are any movies in the database, the seed initializer returns and no movies are added.

```
if (context.Movie.Any())
{
    return;  // DB has been seeded.
}
```

[]()

### Add the seed initializer

Replace the contents of `Program.cs` with the following code:

```
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MvcMovie.Data;
using MvcMovie.Models;
using System;

namespace MvcMovie
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    SeedData.Initialize(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();

        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

Test the app.

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

Delete all the records in the database. You can do this with the delete links in the browser or from SSOX.

Force the app to initialize, calling the methods in the `Startup` class, so the seed method runs. To force initialization, IIS Express must be stopped and restarted. You can do this with any of the following approaches:

* Right-click the IIS Express system tray icon in the notification area and tap **Exit** or **Stop Site**:

  * If you were running VS in non-debug mode, press F5 to run in debug mode
  * If you were running VS in debug mode, stop the debugger and press F5

Delete all the records in the database. Stop and start the app so the `SeedData.Initialize` method runs and seeds the database.

The app shows the seeded data.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

[Previous: Adding a model](adding-model?view=aspnetcore-10.0) [Next: Adding controller methods and views](controller-methods-views?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/w3c-logger/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/w3c-logger/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# W3CLogger in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

W3CLogger is a middleware that writes log files in the [W3C standard format](https://www.w3.org/TR/WD-logfile.html). The logs contain information about HTTP requests and HTTP responses. W3CLogger provides logs of:

* HTTP request information
* Common properties
* Headers
* HTTP response information
* Metadata about the request/response pair (date/time started, time taken)

W3CLogger is valuable in several scenarios to:

* Record information about incoming requests and responses.
* Filter which parts of the request and response are logged.
* Filter which headers to log.

W3CLogger ***can reduce the performance of an app***. Consider the performance impact when selecting fields to log - the performance reduction will increase as you log more properties. Test the performance impact of the selected logging properties.

Warning

W3CLogger can potentially log personally identifiable information (PII). Consider the risk and avoid logging sensitive information. By default, fields that could contain PII aren't logged.

## Enable W3CLogger

Call [AddW3CLogging](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addw3clogging) to add the W3CLogger services to the dependency injection container:

```
builder.Services.AddW3CLogging(logging =>
{
   // Set logging properties here
});
```

Next, call [UseW3CLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usew3clogging) to add the W3CLogger middleware to the request processing pipeline:

```
app.UseW3CLogging();
```

By default, log files are written to a `logs` directory in the app's content root directory (`./logs/`) with the file name prefix `w3clog-` (for example, `./logs/w3clog-20210929.0000.txt`).

By default, W3CLogger logs common properties such as path, status-code, date, time, and protocol. All information about a single request/response pair is written to the same line.

```
#Version: 1.0
#Start-Date: 2021-09-29 22:18:28
#Fields: date time c-ip s-computername s-ip s-port cs-method cs-uri-stem cs-uri-query sc-status time-taken cs-version cs-host cs(User-Agent) cs(Referer)
2021-09-29 22:18:28 ::1 DESKTOP-LH3TLTA ::1 5000 GET / - 200 59.9171 HTTP/1.1 localhost:5000 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/93.0.4577.82+Safari/537.36 -
2021-09-29 22:18:28 ::1 DESKTOP-LH3TLTA ::1 5000 GET / - 200 0.1802 HTTP/1.1 localhost:5000 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/93.0.4577.82+Safari/537.36 -
2021-09-29 22:18:30 ::1 DESKTOP-LH3TLTA ::1 5000 GET / - 200 0.0966 HTTP/1.1 localhost:5000 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/93.0.4577.82+Safari/537.36 -
```

## W3CLogger options

To configure the W3CLogger middleware, call [AddW3CLogging](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addw3clogging) and configure [W3CLoggerOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions) in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddW3CLogging(logging =>
{
    // Log all W3C fields
    logging.LoggingFields = W3CLoggingFields.All;

    logging.FileSizeLimit = 5 * 1024 * 1024;
    logging.RetainedFileCountLimit = 2;
    logging.FileName = "MyLogFile";
    logging.LogDirectory = @"C:\logs";
    logging.FlushInterval = TimeSpan.FromSeconds(2);
});
```

[W3CLoggerOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions) supports the following properties:

* [FileName](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.filename#microsoft-aspnetcore-httplogging-w3cloggeroptions-filename): The log file name prefix. Defaults to `w3clog-`.
* [FileSizeLimit](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.filesizelimit#microsoft-aspnetcore-httplogging-w3cloggeroptions-filesizelimit): Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit. Once the log is full, no more messages will be appended. Defaults to 10MB (10 \* 1024 \* 1024).
* [FlushInterval](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.flushinterval#microsoft-aspnetcore-httplogging-w3cloggeroptions-flushinterval): The period after which logs are flushed to disk. Defaults to 1 second.
* [LogDirectory](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.logdirectory#microsoft-aspnetcore-httplogging-w3cloggeroptions-logdirectory): The directory where log files are written. Defaults to `./logs/` relative to the app's content root directory.
* [RetainedFileCountLimit](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.retainedfilecountlimit#microsoft-aspnetcore-httplogging-w3cloggeroptions-retainedfilecountlimit): The maximum number of retained log files. Defaults to 4.
* [LoggingFields](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.loggingfields#microsoft-aspnetcore-httplogging-w3cloggeroptions-loggingfields): A bit flag enumeration that configures specific parts of the request and response to log and other information about the connection. Defaults to include all possible fields except `UserName` and `Cookie`. For a complete list of available fields, see [W3CLoggingFields](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggingfields).

W3CLogger is a middleware that writes log files in the [W3C standard format](https://www.w3.org/TR/WD-logfile.html). The logs contain information about HTTP requests and HTTP responses. W3CLogger provides logs of:

* HTTP request information
* Common properties
* Headers
* HTTP response information
* Metadata about the request/response pair (date/time started, time taken)

W3CLogger is valuable in several scenarios to:

* Record information about incoming requests and responses.
* Filter which parts of the request and response are logged.
* Filter which headers to log.

W3CLogger ***can reduce the performance of an app***. Consider the performance impact when selecting fields to log - the performance reduction will increase as you log more properties. Test the performance impact of the selected logging properties.

Warning

W3CLogger can potentially log personally identifiable information (PII). Consider the risk and avoid logging sensitive information. By default, fields that could contain PII aren't logged.

## Enable W3CLogger

Call [AddW3CLogging](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addw3clogging) to add the W3CLogger services to the dependency injection container:

```
builder.Services.AddW3CLogging(logging =>
{
   // Set logging properties here
});
```

Next, call [UseW3CLogging](/en-us/dotnet/api/microsoft.aspnetcore.builder.httploggingbuilderextensions.usew3clogging) to add the W3CLogger middleware to the request processing pipeline:

```
app.UseW3CLogging();
```

By default, log files are written to a `logs` directory in the app's content root directory (`./logs/`) with the file name prefix `w3clog-` (for example, `./logs/w3clog-20210929.0000.txt`).

By default, W3CLogger logs common properties such as path, status-code, date, time, and protocol. All information about a single request/response pair is written to the same line.

```
#Version: 1.0
#Start-Date: 2021-09-29 22:18:28
#Fields: date time c-ip s-computername s-ip s-port cs-method cs-uri-stem cs-uri-query sc-status time-taken cs-version cs-host cs(User-Agent) cs(Referer)
2021-09-29 22:18:28 ::1 DESKTOP-LH3TLTA ::1 5000 GET / - 200 59.9171 HTTP/1.1 localhost:5000 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/93.0.4577.82+Safari/537.36 -
2021-09-29 22:18:28 ::1 DESKTOP-LH3TLTA ::1 5000 GET / - 200 0.1802 HTTP/1.1 localhost:5000 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/93.0.4577.82+Safari/537.36 -
2021-09-29 22:18:30 ::1 DESKTOP-LH3TLTA ::1 5000 GET / - 200 0.0966 HTTP/1.1 localhost:5000 Mozilla/5.0+(Windows+NT+10.0;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/93.0.4577.82+Safari/537.36 -
```

## W3CLogger options

To configure the W3CLogger middleware, call [AddW3CLogging](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addw3clogging) and configure [W3CLoggerOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions) in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddW3CLogging(logging =>
{
    // Log all W3C fields
    logging.LoggingFields = W3CLoggingFields.All;

    logging.AdditionalRequestHeaders.Add("x-forwarded-for");
    logging.AdditionalRequestHeaders.Add("x-client-ssl-protocol");
    logging.FileSizeLimit = 5 * 1024 * 1024;
    logging.RetainedFileCountLimit = 2;
    logging.FileName = "MyLogFile";
    logging.LogDirectory = @"C:\logs";
    logging.FlushInterval = TimeSpan.FromSeconds(2);
});
```

[W3CLoggerOptions](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions) supports the following properties:

* [AdditionalRequestHeaders](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.additionalrequestheaders#microsoft-aspnetcore-httplogging-w3cloggeroptions-additionalrequestheaders): Additional request headers to include in the log file.
* [FileName](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.filename#microsoft-aspnetcore-httplogging-w3cloggeroptions-filename): The log file name prefix. Defaults to `w3clog-`.
* [FileSizeLimit](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.filesizelimit#microsoft-aspnetcore-httplogging-w3cloggeroptions-filesizelimit): Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit. Once the log is full, no more messages will be appended. Defaults to 10MB (10 \* 1024 \* 1024).
* [FlushInterval](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.flushinterval#microsoft-aspnetcore-httplogging-w3cloggeroptions-flushinterval): The period after which logs are flushed to disk. Defaults to 1 second.
* [LogDirectory](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.logdirectory#microsoft-aspnetcore-httplogging-w3cloggeroptions-logdirectory): The directory where log files are written. Defaults to `./logs/` relative to the app's content root directory.
* [RetainedFileCountLimit](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.retainedfilecountlimit#microsoft-aspnetcore-httplogging-w3cloggeroptions-retainedfilecountlimit): The maximum number of retained log files. Defaults to 4.
* [LoggingFields](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggeroptions.loggingfields#microsoft-aspnetcore-httplogging-w3cloggeroptions-loggingfields): A bit flag enumeration that configures specific parts of the request and response to log and other information about the connection. Defaults to include all possible fields except `UserName` and `Cookie`. For a complete list of available fields, see [W3CLoggingFields](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.w3cloggingfields).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/code-analysis
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/code-analysis.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Diagnostic code analysis in ASP.NET Core apps

.NET compiler platform analyzers inspect application code for code quality and style issues.

This article provides links to specific diagnostic messages for ASP.NET Core. For more information on .NET diagnostics, see [Overview of .NET source code analysis](/en-us/dotnet/fundamentals/code-analysis/overview).

## Diagnostic codes

The following table lists the diagnostics available for ASP.NET Core applications.

Note

Not all diagnostics are available in older versions of ASP.NET Core.

| Diagnostic ID                               | Message                                                                                                   |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| **[ASP0000](asp0000?view=aspnetcore-10.0)** | Don't call `IServiceCollection.BuildServiceProvider` in `ConfigureServices`                               |
| **[ASP0001](asp0001?view=aspnetcore-10.0)** | Authorization middleware is incorrectly configured                                                        |
| **[ASP0003](asp0003?view=aspnetcore-10.0)** | Don't use model binding attributes with route handlers                                                    |
| **[ASP0004](asp0004?view=aspnetcore-10.0)** | Don't use action results with route handlers                                                              |
| **[ASP0005](asp0005?view=aspnetcore-10.0)** | Don't place attribute on method called by route handler lambda                                            |
| **[ASP0006](asp0006?view=aspnetcore-10.0)** | Don't use nonliteral sequence numbers                                                                     |
| **[ASP0007](asp0007?view=aspnetcore-10.0)** | Route parameter and argument optionality is mismatched                                                    |
| **[ASP0008](asp0008?view=aspnetcore-10.0)** | Don't use `ConfigureWebHost` with `WebApplicationBuilder.Host`                                            |
| **[ASP0009](asp0009?view=aspnetcore-10.0)** | Don't use `Configure` with `WebApplicationBuilder.WebHost`                                                |
| **[ASP0010](asp0010?view=aspnetcore-10.0)** | Don't use `UseStartup` with `WebApplicationBuilder.WebHost`                                               |
| **[ASP0011](asp0011?view=aspnetcore-10.0)** | Suggest using `builder.Logging` over `Host.ConfigureLogging` or `WebHost.ConfigureLogging`                |
| **[ASP0012](asp0012?view=aspnetcore-10.0)** | Suggest using `builder.Services` over `Host.ConfigureServices` or `WebHost.ConfigureServices`             |
| **[ASP0013](asp0013?view=aspnetcore-10.0)** | Suggest switching from using `Configure` methods to `WebApplicationBuilder.Configuration`                 |
| **[ASP0014](asp0014?view=aspnetcore-10.0)** | Suggest using top level route registrations                                                               |
| **[ASP0015](asp0015?view=aspnetcore-10.0)** | Suggest using `IHeaderDictionary` properties                                                              |
| **[ASP0016](asp0016?view=aspnetcore-10.0)** | Don't return a value from `RequestDelegate`                                                               |
| **[ASP0017](asp0017?view=aspnetcore-10.0)** | Invalid route pattern                                                                                     |
| **[ASP0018](asp0018?view=aspnetcore-10.0)** | Unused route parameter                                                                                    |
| **[ASP0019](asp0019?view=aspnetcore-10.0)** | Suggest using `IHeaderDictionary.Append` or the indexer                                                   |
| **[ASP0020](asp0020?view=aspnetcore-10.0)** | Complex types referenced by route parameters must be parsable                                             |
| **[ASP0021](asp0021?view=aspnetcore-10.0)** | Return type of the `BindAsync` method must be `ValueTask<T>`                                              |
| **[ASP0022](asp0022?view=aspnetcore-10.0)** | Route conflict detected between route handlers ([Minimal API apps](/en-us/aspnet/core/fundamentals/apis)) |
| **[ASP0023](asp0023?view=aspnetcore-10.0)** | Route conflict detected between route handlers                                                            |
| **[ASP0024](asp0024?view=aspnetcore-10.0)** | Route handler has multiple parameters with the `[FromBody]` attribute                                     |
| **[ASP0025](asp0025?view=aspnetcore-10.0)** | Use `AddAuthorizationBuilder` to register authorization services and construct policies                   |
| **[ASP0026](asp0026?view=aspnetcore-10.0)** | `[Authorize]` is overridden by `[AllowAnonymous]` from "farther away"                                     |
| **[ASP0027](asp0027?view=aspnetcore-10.0)** | Unnecessary `public Program` class declaration                                                            |
| **[ASP0028](asp0028?view=aspnetcore-10.0)** | Consider using `IPAddress.IPv6Any` instead of `IPAddress.Any`                                             |
| **[ASP0033](asp0033?view=aspnetcore-10.0)** | `[ValidatableType]` is applied to an inaccessible type                                                    |
| **[ASP0034](asp0034?view=aspnetcore-10.0)** | Endpoint parameter type is inaccessible from generated code                                               |
| **[ASP0035](asp0035?view=aspnetcore-10.0)** | Validatable property or its type on a `[ValidatableType]` is not accessible                               |
| **[ASP0036](asp0036?view=aspnetcore-10.0)** | Validatable property or its type on an endpoint parameter type is not accessible                          |
| **[ASP0037](asp0037?view=aspnetcore-10.0)** | `[ValidatableType]` cannot be used in generated code                                                      |
| **[ASP0038](asp0038?view=aspnetcore-10.0)** | `[ValidatableType]` should not be used without a call to `AddValidation`                                  |
| **[BL0001](bl0001?view=aspnetcore-10.0)**   | Component parameter should have public setters                                                            |
| **[BL0002](bl0002?view=aspnetcore-10.0)**   | Component has multiple `CaptureUnmatchedValues` parameters                                                |
| **[BL0003](bl0003?view=aspnetcore-10.0)**   | Component parameter with `CaptureUnmatchedValues` has the wrong type                                      |
| **[BL0004](bl0004?view=aspnetcore-10.0)**   | Component parameter should be public                                                                      |
| **[BL0005](bl0005?view=aspnetcore-10.0)**   | Component parameter shouldn't be set outside of its component                                             |
| **[BL0006](bl0006?view=aspnetcore-10.0)**   | Don't use RenderTree types                                                                                |
| **[BL0007](bl0007?view=aspnetcore-10.0)**   | Component parameter `{0}` should be auto property                                                         |
| **[BL0008](bl0008?view=aspnetcore-10.0)**   | Component parameters should be auto properties                                                            |
| **[MVC1000](mvc1000?view=aspnetcore-10.0)** | Use of `IHtmlHelper.Partial` should be avoided                                                            |
| **[MVC1001](mvc1001?view=aspnetcore-10.0)** | Filters can't be applied to page handler methods                                                          |
| **[MVC1002](mvc1002?view=aspnetcore-10.0)** | Route attribute can't be applied to page handler methods                                                  |
| **[MVC1003](mvc1003?view=aspnetcore-10.0)** | Route attributes can't be applied to page models                                                          |
| **[MVC1004](mvc1004?view=aspnetcore-10.0)** | Rename model bound parameter                                                                              |
| **[MVC1005](mvc1005?view=aspnetcore-10.0)** | Can't use `UseMvc` with Endpoint Routing                                                                  |
| **[MVC1006](mvc1006?view=aspnetcore-10.0)** | Methods containing Tag Helpers (`TagHelpers`) must be async and return `Task`                             |

## Related content

* [Overview of .NET source code analysis](/en-us/dotnet/fundamentals/code-analysis/overview)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/31-to-60
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/31-to-60.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core 3.1 to .NET 6

This article explains how to update an existing ASP.NET Core 3.1 project to ASP.NET Core in .NET 6. To upgrade from ASP.NET Core in .NET 5 to .NET 6, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](50-to-60?view=aspnetcore-10.0).

## Update .NET SDK version in `global.json`

If you rely upon a [`global.json`](/en-us/dotnet/core/tools/global-json) file to target a specific .NET SDK version, update the `version` property to the .NET 6 SDK version that's installed. For example:

```
{
  "sdk": {
-    "version": "3.1.200"
+    "version": "6.0.100"
  }
}
```

[]()

## Update the target framework

Update the project file's [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks) to `net6.0`:

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

  <PropertyGroup>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

</Project>
```

## Update package references

In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.org/packages?q=Microsoft.AspNetCore.%2A), [`Microsoft.EntityFrameworkCore.*`](https://www.nuget.org/packages?q=Microsoft.EntityFrameworkCore.%2A), [`Microsoft.Extensions.*`](https://www.nuget.org/packages?q=Microsoft.Extensions.%2A), and [`System.Net.Http.Json`](https://www.nuget.org/packages/System.Net.Http.Json) package reference's `Version` attribute to 6.0.0 or later. For example:

```
<ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="3.1.6" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" />
-    <PackageReference Include="System.Net.Http.Json" Version="3.2.1" />
+    <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="6.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
+    <PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
</ItemGroup>
```

## Delete `bin` and `obj` folders

You may need to delete the `bin` and `obj` folders. Run `dotnet nuget locals --clear all` to clear the NuGet package cache.

## Minimal hosting model

The ASP.NET Core templates generate code using the new [minimal hosting model](50-to-60?view=aspnetcore-10.0#new-hosting-model). The minimal hosting model unifies `Startup.cs` and `Program.cs` into a single `Program.cs` file. `ConfigureServices` and `Configure` are no longer used. Apps migrating from ASP.NET Core 3.1 to .NET 6 don't need to use the minimal hosting model, using `Startup` and the [Generic Host](../fundamentals/host/generic-host?view=aspnetcore-10.0) used by the ASP.NET Core 3.1 templates is fully supported.

To use `Startup` with the new minimal hosting model, see [Use Startup with the new minimal hosting model](50-to-60?view=aspnetcore-10.0#use-a-startup-class-with-the-new-minimal-hosting-model).

To migrate to the new minimal hosting model using the following pattern used by the ASP.NET Core in .NET 6 templates, see [Code samples migrated to the new minimal hosting model in ASP.NET Core in .NET 6](50-to-60-samples?view=aspnetcore-10.0) and [Migrate from ASP.NET Core in .NET 5 to .NET 6](50-to-60?view=aspnetcore-10.0)

## Update Razor class libraries (RCLs)

Migrate Razor class libraries (RCLs) to take advantage of new APIs or features that are introduced as part of ASP.NET Core in .NET 6.

To update a RCL that targets components:

1. Update the following properties in the project file:

   ```
   <Project Sdk="Microsoft.NET.Sdk.Razor">
     <PropertyGroup>
   -     <TargetFramework>netstandard2.0</TargetFramework>
   -     <RazorLangVersion>3.0</RazorLangVersion>
   +     <TargetFramework>net6.0</TargetFramework>
     </PropertyGroup>
   ```

2. Update other packages to their latest versions. The latest versions can be found at [NuGet.org](https://www.nuget.org).

To update an RCL targeting MVC, update the following properties in the project file:

```
<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
    <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
  </PropertyGroup>
```

## Blazor

To adopt all of the [5.0 features](../release-notes/aspnetcore-5.0?view=aspnetcore-10.0) and [6.0 features](../release-notes/aspnetcore-6.0?view=aspnetcore-10.0#blazor) for Blazor apps, we recommend the following process:

* Create a new 6.0 Blazor project from one of the Blazor project templates. For more information, see [Tooling for ASP.NET Core Blazor](../blazor/tooling?view=aspnetcore-10.0).
* Move the app's components and code to the 6.0 app making modifications to adopt the new 5.0 and 6.0 features.

## Update Docker images

For apps using Docker, update your *Dockerfile* `FROM` statements and scripts. Use a base image that includes the ASP.NET Core in .NET 6 runtime. Consider the following `docker pull` command difference between ASP.NET Core 3.1 and .NET 6:

```
- docker pull mcr.microsoft.com/dotnet/core/aspnet:3.1
+ docker pull mcr.microsoft.com/dotnet/aspnet:6.0
```

As part of the move to ".NET" as the product name, the Docker images moved from the `mcr.microsoft.com/dotnet/core` repositories to `mcr.microsoft.com/dotnet`. For more information, see [.NET 5.0 - Docker Repo Name Change (dotnet/dotnet-docker #1939)](https://github.com/dotnet/dotnet-docker/issues/1939).

## Model binding changes in ASP.NET Core MVC and Razor Pages

### `DateTime` values are model bound as UTC times

In ASP.NET Core 3.1 or earlier, `DateTime` values were model-bound as local time, where the timezone was determined by the server. `DateTime` values bound from input formatting (JSON) and `DateTimeOffset` values were bound as UTC timezones.

In .NET 5 or later, model binding consistently binds `DateTime` values with the UTC timezone.

To retain the previous behavior, remove the `DateTimeModelBinderProvider` in `Startup.ConfigureServices`:

```
services.AddControllersWithViews(options =>
    options.ModelBinderProviders.RemoveType<DateTimeModelBinderProvider>());
```

### `ComplexObjectModelBinderProvider` \ `ComplexObjectModelBinder` replace `ComplexTypeModelBinderProvider` \ `ComplexTypeModelBinder`

To add support for model binding [C# 9 record types](/en-us/dotnet/csharp/whats-new/csharp-9#record-types), the [ComplexTypeModelBinderProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.binders.complextypemodelbinderprovider) is:

* Annotated as obsolete.
* No longer registered by default.

Apps that rely on the presence of the `ComplexTypeModelBinderProvider` in the `ModelBinderProviders` collection need to reference the new binder provider:

```
- var complexModelBinderProvider = options.ModelBinderProviders.OfType<ComplexTypeModelBinderProvider>();
+ var complexModelBinderProvider = options.ModelBinderProviders.OfType<ComplexObjectModelBinderProvider>();
```

## `UseDatabaseErrorPage` obsolete

The ASP.NET Core 3.1 templates that include an option for individual user accounts generate a call to [UseDatabaseErrorPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.databaseerrorpageextensions.usedatabaseerrorpage). `UseDatabaseErrorPage` is now obsolete and should be replaced with a combination of `AddDatabaseDeveloperPageExceptionFilter` and `UseMigrationsEndPoint`, as shown in the following code:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
+   services.AddDatabaseDeveloperPageExceptionFilter();
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
+       app.UseMigrationsEndPoint();
-       app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }
```

For more information, see [Obsoleting DatabaseErrorPage middleware (dotnet/aspnetcore #24987)](https://github.com/dotnet/aspnetcore/issues/24987).

## ASP.NET Core Module (ANCM)

If the [ASP.NET Core Module (ANCM)](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) wasn't a selected component when Visual Studio was installed or if a prior version of the ANCM was installed on the system, download the latest [.NET Core Hosting Bundle Installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer) and run the installer. For more information, see [Hosting Bundle](../host-and-deploy/iis/hosting-bundle?view=aspnetcore-10.0).

## Application name change

In .NET 6, [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) normalizes the content root path to end with a [DirectorySeparatorChar](/en-us/dotnet/api/system.io.path.directoryseparatorchar#system-io-path-directoryseparatorchar). Most apps migrating from [HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder) or [WebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder) won't have the same app name because they aren't normalized. For more information, see [SetApplicationName](../security/data-protection/configuration/overview?view=aspnetcore-10.0#set-the-application-name-setapplicationname)

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

For more information, see the following resources:

* [Announcements GitHub repository (`aspnet/Announcements`, `6.0.0` label)](https://github.com/aspnet/Announcements/issues?q=is%3Aissue+label%3A6.0.0+is%3Aopen): Includes breaking and non-breaking information.
* [Announcements GitHub repository (`aspnet/Announcements`, `5.0.0` label)](https://github.com/aspnet/Announcements/issues?q=is%3Aissue+label%3A5.0.0+is%3Aopen): Includes breaking and non-breaking information.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/quickgrid
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/quickgrid.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor `QuickGrid` component

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The [`QuickGrid` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid) is a Razor component for quickly and efficiently displaying data in tabular form. QuickGrid provides a simple and convenient data grid component for common grid rendering scenarios and serves as a reference architecture and performance baseline for building data grid components. QuickGrid is highly optimized and uses advanced techniques to achieve optimal rendering performance.

## Package

Add a package reference for the [`Microsoft.AspNetCore.Components.QuickGrid` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.QuickGrid).

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

## Sample app

For various QuickGrid demonstrations, see the [**QuickGrid for Blazor** sample app](https://aspnet.github.io/quickgridsamples/). The demo site is hosted on GitHub Pages. The site loads fast thanks to static prerendering using the community-maintained [`BlazorWasmPrerendering.Build` GitHub project](https://github.com/jsakamoto/BlazorWasmPreRendering.Build).

## QuickGrid implementation

To implement a `QuickGrid` component:

* Specify tags for the `QuickGrid` component in Razor markup (`<QuickGrid>...</QuickGrid>`).

* Name a queryable source of data for the grid. Use ***either*** of the following data sources:

  * [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.items): A nullable `IQueryable<TGridItem>`, where `TGridItem` is the type of data represented by each row in the grid.
  * [ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsprovider): A callback that supplies data for the grid.

* `QueryParameterNameOptions`: Controls the names of the query string parameters that persist the grid's sort column, sort direction, and page number in the URL. The default value is an instance that results in query parameters named "`sort`", "`direction`", and "`page`". Assigning unique names to these query string parameters allows the use of multiple `QuickGrid` components on the same page without their URL parameters conflicting with each other. For more information, see the [Pagination modes](#pagination-modes), [Query parameter names](#query-parameter-names), and [Multiple grids on the same page](#multiple-grids-on-the-same-page) sections.

* [Class](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.class): An optional CSS class name. If provided, the class name is included in the `class` attribute of the rendered table.

* [Theme](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.theme): A theme name (default value: `default`). This affects which styling rules match the table.

* [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize): If true, the grid is rendered with virtualization. This is normally used in conjunction with scrolling and causes the grid to fetch and render only the data around the current scroll viewport. This can greatly improve the performance when scrolling through large data sets. If you use [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize), you should supply a value for [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsize) and must ensure that every row renders with a constant height. Generally, it's preferable not to use [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize) if the amount of data rendered is small or if you're using pagination.

* [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsize): Only applicable when using [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize). [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsize) defines an expected height in pixels for each row, allowing the virtualization mechanism to fetch the correct number of items to match the display size and to ensure accurate scrolling.

* [ItemKey](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemkey): Optionally defines a value for `@key` on each rendered row. Typically, this is used to specify a unique identifier, such as a primary key value, for each data item. This allows the grid to preserve the association between row elements and data items based on their unique identifiers, even when the `TGridItem` instances are replaced by new copies (for example, after a new query against the underlying data store). If not set, the `@key` is the `TGridItem` instance.

* [OverscanCount](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.overscancount): Defines how many additional items to render before and after the visible region to reduce rendering frequency during scrolling. While higher values can improve scroll smoothness by rendering more items off-screen, a higher value can also result in an increase in initial load times. Finding a balance based on your data set size and user experience requirements is recommended. The default value is `3`. Only available when using [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize).

* `InitialItemIndex`: Only applicable when using [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize). Scrolls the grid to the given zero-based row index on the first interactive render. The value is applied once and clamped to the valid range. This forwards to the inner `Virtualize` component. For more information, see [ASP.NET Core Razor component virtualization](virtualization?view=aspnetcore-10.0#scroll-to-a-specific-item).

* `ScrollToItemAsync`: Only applicable when using [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize). Programmatically scrolls the grid to the given zero-based row index, aligning it to the top. The last call wins, and the method throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) when virtualization is disabled or the grid isn't rendered yet. This forwards to the inner `Virtualize` component. For more information, see [ASP.NET Core Razor component virtualization](virtualization?view=aspnetcore-10.0#scroll-to-a-specific-item).

* `AnchorMode`: Only applicable when using [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize). Controls how the viewport behaves at list edges when items are dynamically added (default: `Start`). This is an experimental API that requires opting in to the `ASP0030` diagnostic, and it forwards to the inner `Virtualize` component. For more information, see [ASP.NET Core Razor component virtualization](virtualization?view=aspnetcore-10.0#control-viewport-scroll-position-behavior-when-items-are-dynamically-added).

* `ItemComparer`: Only applicable when using [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize). A comparer used to detect whether items were prepended or appended between data loads, which is useful for class-typed items supplied by an [ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsprovider). This is an experimental API that requires opting in to the `ASP0030` diagnostic, and it forwards to the inner `Virtualize` component. For more information, see [ASP.NET Core Razor component virtualization](virtualization?view=aspnetcore-10.0#control-viewport-scroll-position-behavior-when-items-are-dynamically-added).

* [Pagination](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.pagination): Optionally links this `TGridItem` instance with a [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) model, causing the grid to fetch and render only the current page of data. This is normally used in conjunction with a [Paginator](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginator) component or some other UI logic that displays and updates the supplied [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) instance. [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) includes API for interacting with the current zero-based page index, the number of items on each page, the zero-based index of the last page, and the total number of items across all pages.

* In the QuickGrid child content ([RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment)), specify [PropertyColumn\<TGridItem,TProp>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2)s, which represent `TGridItem` columns whose cells display values:

  * [Property](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2.property): Defines the value to be displayed in this column's cells.
  * [Format](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2.format): Optionally specifies a format string for the value. Using [Format](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2.format) requires the `TProp` type to implement [IFormattable](/en-us/dotnet/api/system.iformattable).
  * [Sortable](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.sortable): Indicates whether the data should be sortable by this column. The default value may vary according to the column type. For example, a [TemplateColumn\<TGridItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.templatecolumn-1) is sorted if any [SortBy](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.templatecolumn-1.sortby) parameter is specified.
  * [InitialSortDirection](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.initialsortdirection): Indicates the sort direction if [IsDefaultSortColumn](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.isdefaultsortcolumn) is `true`.
  * [IsDefaultSortColumn](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.isdefaultsortcolumn): Indicates whether this column should be sorted by default.
  * [PlaceholderTemplate](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.placeholdertemplate): If specified, virtualized grids use this template to render cells whose data hasn't been loaded.
  * [HeaderTemplate](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.headertemplate#microsoft-aspnetcore-components-quickgrid-columnbase-1-headertemplate): An optional template for this column's header cell. If not specified, the default header template includes the [Title](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.title#microsoft-aspnetcore-components-quickgrid-columnbase-1-title), along with any applicable sort indicators and options buttons.
  * [Title](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.title#microsoft-aspnetcore-components-quickgrid-columnbase-1-title): Title text for the column. The title is rendered automatically if [HeaderTemplate](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.headertemplate#microsoft-aspnetcore-components-quickgrid-columnbase-1-headertemplate) isn't used.

- Specify tags for the `QuickGrid` component in Razor markup (`<QuickGrid>...</QuickGrid>`).

- Name a queryable source of data for the grid. Use ***either*** of the following data sources:

  * [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.items): A nullable `IQueryable<TGridItem>`, where `TGridItem` is the type of data represented by each row in the grid.
  * [ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsprovider): A callback that supplies data for the grid.

- [Class](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.class): An optional CSS class name. If provided, the class name is included in the `class` attribute of the rendered table.

- [Theme](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.theme): A theme name (default value: `default`). This affects which styling rules match the table.

- [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize): If true, the grid is rendered with virtualization. This is normally used in conjunction with scrolling and causes the grid to fetch and render only the data around the current scroll viewport. This can greatly improve the performance when scrolling through large data sets. If you use [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize), you should supply a value for [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsize) and must ensure that every row renders with a constant height. Generally, it's preferable not to use [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize) if the amount of data rendered is small or if you're using pagination.

- [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsize): Only applicable when using [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize). [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsize) defines an expected height in pixels for each row, allowing the virtualization mechanism to fetch the correct number of items to match the display size and to ensure accurate scrolling.

- [ItemKey](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemkey): Optionally defines a value for `@key` on each rendered row. Typically, this is used to specify a unique identifier, such as a primary key value, for each data item. This allows the grid to preserve the association between row elements and data items based on their unique identifiers, even when the `TGridItem` instances are replaced by new copies (for example, after a new query against the underlying data store). If not set, the `@key` is the `TGridItem` instance.

- [OverscanCount](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.overscancount): Defines how many additional items to render before and after the visible region to reduce rendering frequency during scrolling. While higher values can improve scroll smoothness by rendering more items off-screen, a higher value can also result in an increase in initial load times. Finding a balance based on your data set size and user experience requirements is recommended. The default value is `3`. Only available when using [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize).

- [Pagination](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.pagination): Optionally links this `TGridItem` instance with a [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) model, causing the grid to fetch and render only the current page of data. This is normally used in conjunction with a [Paginator](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginator) component or some other UI logic that displays and updates the supplied [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) instance. [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) includes API for interacting with the current zero-based page index, the number of items on each page, the zero-based index of the last page, and the total number of items across all pages.

- In the QuickGrid child content ([RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment)), specify [PropertyColumn\<TGridItem,TProp>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2)s, which represent `TGridItem` columns whose cells display values:

  * [Property](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2.property): Defines the value to be displayed in this column's cells.
  * [Format](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2.format): Optionally specifies a format string for the value. Using [Format](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2.format) requires the `TProp` type to implement [IFormattable](/en-us/dotnet/api/system.iformattable).
  * [Sortable](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.sortable): Indicates whether the data should be sortable by this column. The default value may vary according to the column type. For example, a [TemplateColumn\<TGridItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.templatecolumn-1) is sorted if any [SortBy](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.templatecolumn-1.sortby) parameter is specified.
  * [InitialSortDirection](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.initialsortdirection): Indicates the sort direction if [IsDefaultSortColumn](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.isdefaultsortcolumn) is `true`.
  * [IsDefaultSortColumn](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.isdefaultsortcolumn): Indicates whether this column should be sorted by default.
  * [PlaceholderTemplate](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.placeholdertemplate): If specified, virtualized grids use this template to render cells whose data hasn't been loaded.
  * [HeaderTemplate](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.headertemplate#microsoft-aspnetcore-components-quickgrid-columnbase-1-headertemplate): An optional template for this column's header cell. If not specified, the default header template includes the [Title](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.title#microsoft-aspnetcore-components-quickgrid-columnbase-1-title), along with any applicable sort indicators and options buttons.
  * [Title](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.title#microsoft-aspnetcore-components-quickgrid-columnbase-1-title): Title text for the column. The title is rendered automatically if [HeaderTemplate](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.headertemplate#microsoft-aspnetcore-components-quickgrid-columnbase-1-headertemplate) isn't used.

* Specify tags for the `QuickGrid` component in Razor markup (`<QuickGrid>...</QuickGrid>`).

* Name a queryable source of data for the grid. Use ***either*** of the following data sources:

  * [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.items): A nullable `IQueryable<TGridItem>`, where `TGridItem` is the type of data represented by each row in the grid.
  * [ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsprovider): A callback that supplies data for the grid.

* [Class](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.class): An optional CSS class name. If provided, the class name is included in the `class` attribute of the rendered table.

* [Theme](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.theme): A theme name (default value: `default`). This affects which styling rules match the table.

* [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize): If true, the grid is rendered with virtualization. This is normally used in conjunction with scrolling and causes the grid to fetch and render only the data around the current scroll viewport. This can greatly improve the performance when scrolling through large data sets. If you use [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize), you should supply a value for [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsize) and must ensure that every row renders with a constant height. Generally, it's preferable not to use [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize) if the amount of data rendered is small or if you're using pagination.

* [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsize): Only applicable when using [Virtualize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.virtualize). [ItemSize](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsize) defines an expected height in pixels for each row, allowing the virtualization mechanism to fetch the correct number of items to match the display size and to ensure accurate scrolling.

* [ItemKey](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemkey): Optionally defines a value for `@key` on each rendered row. Typically, this is used to specify a unique identifier, such as a primary key value, for each data item. This allows the grid to preserve the association between row elements and data items based on their unique identifiers, even when the `TGridItem` instances are replaced by new copies (for example, after a new query against the underlying data store). If not set, the `@key` is the `TGridItem` instance.

* [Pagination](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.pagination): Optionally links this `TGridItem` instance with a [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) model, causing the grid to fetch and render only the current page of data. This is normally used in conjunction with a [Paginator](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginator) component or some other UI logic that displays and updates the supplied [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) instance. [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) includes API for interacting with the current zero-based page index, the number of items on each page, the zero-based index of the last page, and the total number of items across all pages.

* In the QuickGrid child content ([RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment)), specify [PropertyColumn\<TGridItem,TProp>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2)s, which represent `TGridItem` columns whose cells display values:

  * [Property](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2.property): Defines the value to be displayed in this column's cells.
  * [Format](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2.format): Optionally specifies a format string for the value. Using [Format](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2.format) requires the `TProp` type to implement [IFormattable](/en-us/dotnet/api/system.iformattable).
  * [Sortable](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.sortable): Indicates whether the data should be sortable by this column. The default value may vary according to the column type. For example, a [TemplateColumn\<TGridItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.templatecolumn-1) is sorted if any [SortBy](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.templatecolumn-1.sortby) parameter is specified.
  * [InitialSortDirection](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.initialsortdirection): Indicates the sort direction if [IsDefaultSortColumn](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.isdefaultsortcolumn) is `true`.
  * [IsDefaultSortColumn](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.isdefaultsortcolumn): Indicates whether this column should be sorted by default.
  * [PlaceholderTemplate](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.placeholdertemplate): If specified, virtualized grids use this template to render cells whose data hasn't been loaded.
  * [HeaderTemplate](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.headertemplate#microsoft-aspnetcore-components-quickgrid-columnbase-1-headertemplate): An optional template for this column's header cell. If not specified, the default header template includes the [Title](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.title#microsoft-aspnetcore-components-quickgrid-columnbase-1-title), along with any applicable sort indicators and options buttons.
  * [Title](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.title#microsoft-aspnetcore-components-quickgrid-columnbase-1-title): Title text for the column. The title is rendered automatically if [HeaderTemplate](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.headertemplate#microsoft-aspnetcore-components-quickgrid-columnbase-1-headertemplate) isn't used.

For example, add the following component to render a grid.

For Blazor Web Apps, the `QuickGrid` component must adopt an [interactive render mode](render-modes?view=aspnetcore-10.0#render-modes) to enable interactive features, such as paging and sorting.

`PromotionGrid.razor`:

```
@page "/promotion-grid"
@using Microsoft.AspNetCore.Components.QuickGrid

<PageTitle>Promotion Grid</PageTitle>

<h1>Promotion Grid Example</h1>

<QuickGrid Items="people">
    <PropertyColumn Property="@(p => p.PersonId)" Sortable="true" />
    <PropertyColumn Property="@(p => p.Name)" Sortable="true" />
    <PropertyColumn Property="@(p => p.PromotionDate)" Format="yyyy-MM-dd" Sortable="true" />
</QuickGrid>

@code {
    private record Person(int PersonId, string Name, DateOnly PromotionDate);

    private IQueryable<Person> people = new[]
    {
        new Person(10895, "Jean Martin", new DateOnly(1985, 3, 16)),
        new Person(10944, "António Langa", new DateOnly(1991, 12, 1)),
        new Person(11203, "Julie Smith", new DateOnly(1958, 10, 10)),
        new Person(11205, "Nur Sari", new DateOnly(1922, 4, 27)),
        new Person(11898, "Jose Hernandez", new DateOnly(2011, 5, 3)),
        new Person(12130, "Kenji Sato", new DateOnly(2004, 1, 9)),
    }.AsQueryable();
}
```

```
@page "/promotion-grid"
@using Microsoft.AspNetCore.Components.QuickGrid

<PageTitle>Promotion Grid</PageTitle>

<h1>Promotion Grid Example</h1>

<QuickGrid Items="people">
    <PropertyColumn Property="@(p => p.PersonId)" Sortable="true" />
    <PropertyColumn Property="@(p => p.Name)" Sortable="true" />
    <PropertyColumn Property="@(p => p.PromotionDate)" Format="yyyy-MM-dd" Sortable="true" />
</QuickGrid>

@code {
    private record Person(int PersonId, string Name, DateOnly PromotionDate);

    private IQueryable<Person> people = new[]
    {
        new Person(10895, "Jean Martin", new DateOnly(1985, 3, 16)),
        new Person(10944, "António Langa", new DateOnly(1991, 12, 1)),
        new Person(11203, "Julie Smith", new DateOnly(1958, 10, 10)),
        new Person(11205, "Nur Sari", new DateOnly(1922, 4, 27)),
        new Person(11898, "Jose Hernandez", new DateOnly(2011, 5, 3)),
        new Person(12130, "Kenji Sato", new DateOnly(2004, 1, 9)),
    }.AsQueryable();
}
```

Access the component in a browser at the relative path `/promotion-grid`.

There aren't current plans to extend QuickGrid with features that full-blown commercial grids tend to offer, for example, hierarchical rows, drag-to-reorder columns, or Excel-like range selections. If you require advanced features that you don't wish to develop on your own, continue using third-party grids.

## Page items with a `Paginator` component

The `QuickGrid` component can page data from the data source.

The `QuickGrid` component can page data from the data source. In Blazor Web Apps, paging requires the component to adopt an [interactive render mode](render-modes?view=aspnetcore-10.0#render-modes).

Add a [PaginationState](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate) instance to the component's `@code` block. Set the [ItemsPerPage](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate.itemsperpage) to the number of items to display per page. In the following example, the instance is named `pagination`, and ten items per page is set:

```
PaginationState pagination = new PaginationState { ItemsPerPage = 10 };
```

Set the `QuickGrid` component's [Pagination](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.pagination#microsoft-aspnetcore-components-quickgrid-quickgrid-1-pagination) property to `pagination`:

```
<QuickGrid Items="..." Pagination="pagination">
```

To provide a UI for pagination, add a [`Paginator` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginator) above, below, or both above and below the `QuickGrid` component. Set the [Paginator.State](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginator.state) to `pagination`:

To provide a UI for pagination, add a [`Paginator` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginator) above or below the `QuickGrid` component. Set the [Paginator.State](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginator.state) to `pagination`:

```
<Paginator State="pagination" />
```

In the running app, page through the items using a rendered `Paginator` component.

QuickGrid renders additional empty rows to fill in the final page of data when used with a `Paginator` component. In .NET 9 or later, empty data cells (`<td></td>`) are added to the empty rows. The empty rows are intended to facilitate rendering the QuickGrid with stable row height and styling across all pages.

`GetPageUrl` returns a URL with the one-based page number. Page index 0 (page 1) omits the query parameter entirely.

## Pagination modes

`QuickGrid` supports *URL-based navigation* with pagination and sort state persisted by the URL's query string. When users paginate or sort, the URL updates (example: `?page=2&sort=Name&direction=asc`). This enables link sharing, browser back/forward, and static SSR without interactivity.

Sortable column headers and [paginator controls](#page-items-with-a-paginator-component) render as `<a>` elements with `href` attributes. The links support standard browser behavior, such as opening a page in a new tab or window. The `StaticHtmlRenderer` renders these anchors. On each request, the server reads the query string to determine current page and sort state—no JavaScript runtime required.

Query string parameters:

* `page`: One-based page number. The first page omits the parameter for clean URLs.
* `sort`: Column title for sorting the grid.
* `direction`: Ascending (`asc`) or descending (`desc`).

If the `page` value is malformed, zero, or negative, `QuickGrid` displays the first page. If the value is greater than the number of available pages, `QuickGrid` displays the last page. This behavior applies when the grid receives data from either `Items` or `ItemsProvider`. An `ItemsProvider` must return an accurate total item count for `QuickGrid` to resolve the last page correctly.

Rename the preceding query string parameters with the `QueryParameterNameOptions` parameter. For more information, see the [Query parameter names](#query-parameter-names) section.

The `sort` column is identified by the column's `Title` property. Columns without a `Title` render a non-clickable `<div>` header.

`QuickGrid` reads the URL on initialization and subscribes to `NavigationManager.LocationChanged`, so browser back/forward and direct URL entry work. When sort parameters are removed from the URL, it falls back to the default sort column/direction.

Note

Disabled [paginator links](#page-items-with-a-paginator-component) are marked with `aria-disabled="true"`, removed from the tab order with `tabindex="-1"`, and made non-interactive with `pointer-events: none`.

Pagination and sort state is managed in memory inside the `QuickGrid` component without changing the URL, called *inner-state navigation*. An interactive render mode is required.

To disable URL-based navigation, set the `AppContext` switch for the feature to `false`:

```
AppContext.SetSwitch(
    "Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting",
    false);
```

This restores `<button>` elements with `@onclick` handlers. An interactive render mode is required.

The switch only controls the rendered HTML element (`<a>` versus `<button>`). Even when disabled, `QuickGrid` still reads and writes state to the URL query string internally. `SortByColumnAsync` and `Paginator.GoToPageAsync` navigate via `NavigationManager.NavigateTo` regardless of the flag.

## Query parameter names

The `QueryParameterNameOptions` parameter of the `QuickGrid` component controls the names of the query string parameters that persist grid state in the URL. The `QueryParameterNameOptions` class has three settable properties:

* `Sort`: Name of the query string parameter that holds the sort column. The default value is `sort`.
* `Direction`: Name of the query string parameter that holds the sort direction. The default value is `direction`.
* `Page`: Name of the query string parameter that holds the page number. The default value is `page`.

The parameter is never `null`. If the parameter isn't set, the `QuickGrid` component uses an instance created by the parameterless constructor, which results in the default names of the preceding list.

The constructor takes an optional prefix argument that's prepended to all three default names. The prefix must include any separator character that you want to appear between the prefix and the name. In the following example, the query string parameters are named `products_sort`, `products_direction`, and `products_page`:

```
@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid ... 
    QueryParameterNameOptions="@(new QueryParameterNameOptions("products_"))">
    ...
</QuickGrid>
```

To control the names individually, set the properties of the class. Properties set explicitly take precedence over a prefix passed to the constructor, so the two approaches can be combined. The following example names the query string parameters `orderBy`, `orderDir`, and `p`:

```
@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid ... QueryParameterNameOptions="@queryParameterNames">
    ...
</QuickGrid>

@code {
    private QueryParameterNameOptions queryParameterNames = new()
    {
        Sort = "orderBy",
        Direction = "orderDir",
        Page = "p"
    };
}
```

The page query parameter name is applied to the `PaginationState` instance assigned to the grid's [Pagination](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.pagination) parameter, so a linked `Paginator` component reads and writes the same parameter name automatically.

## Multiple grids on the same page

Multiple `QuickGrid` components on the same page require unique query parameter names to avoid query string conflicts. Assign a `QueryParameterNameOptions` parameter to all but one of the grids. For more information, see the [Query parameter names](#query-parameter-names) section.

Each `QuickGrid` must have its own `PaginationState` instance. Multiple grids must not share a `PaginationState` if they use different query parameter names—the last grid to render overwrites the query parameter name on the shared state, causing the `Paginator` to read from the wrong parameter.

In the following example, the first `QuickGrid` uses the default query parameter names, while the second one uses a `cities_` prefix:

```
<QuickGrid ... Pagination="@pagination1">
    ...
</QuickGrid>

<Paginator State="pagination1" />

<QuickGrid ... Pagination="@pagination2" 
    QueryParameterNameOptions="@(new QueryParameterNameOptions("cities_"))">
    ...
</QuickGrid>

<Paginator State="pagination2" />
```

In the following query string:

* The `page=2&sort=Name&direction=asc` portion applies to the first `QuickGrid` component.
* The `cities_page=3&cities_sort=Population&cities_direction=desc` portion applies to the second `QuickGrid` component.

```
?page=2&sort=Name&direction=asc&cities_page=3&cities_sort=Population&cities_direction=desc
```

#### Row click event (`OnRowClick`)

The `QuickGrid` component supports row click events through the [OnRowClick](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.onrowclick) parameter. When set, the grid automatically applies appropriate styling (cursor pointer) and invokes the callback with the clicked item:

```
@using Microsoft.AspNetCore.Components.QuickGrid
@inject NavigationManager NavigationManager

<QuickGrid Items="@people.AsQueryable()" 
    OnRowClick="@((Person args) => HandleRowClick(args))">
    <PropertyColumn Property="@(p => p.Name)" />
    <PropertyColumn Property="@(p => p.Email)" />
</QuickGrid>

@code {
    private List<Person> people = new()
    {
        new(1, "Alice Smith", "alice@example.com", "Engineering"),
        new(2, "Bob Johnson", "bob@example.com", "Marketing"),
        new(3, "Carol Williams", "carol@example.com", "Engineering"),
    };

    private void HandleRowClick(Person person)
    {
        NavigationManager.NavigateTo($"/person/{person.Id}");
    }

    private record Person(int Id, string Name, string Email, string Department);
}
```

The feature includes built-in CSS styling that applies a pointer cursor to clickable rows through the `row-clickable` CSS class, providing clear visual feedback to users. The class is applied to the `tr` element of every data row when [OnRowClick](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.onrowclick) is set, and it's combined with any class returned by the [RowClass](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.rowclass) parameter.

##### Event propagation from interactive cell content

The row click handler is registered on the row's `tr` element, so a click on an interactive control inside a cell, such as a button, a checkbox, or a link, bubbles up and also invokes [OnRowClick](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.onrowclick). To run only the control's own handler, stop propagation on the control with the [`@onclick:stopPropagation` directive attribute](event-handling?view=aspnetcore-10.0#stop-event-propagation):

```
<QuickGrid Items="@people.AsQueryable()" 
    OnRowClick="@((Person args) => HandleRowClick(args))">
    <PropertyColumn Property="@(p => p.Name)" />
    <TemplateColumn Title="Actions">
        <button @onclick="@(() => Delete(context))" 
            @onclick:stopPropagation="true">
            Delete
        </button>
    </TemplateColumn>
</QuickGrid>
```

##### Accessibility

[OnRowClick](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.onrowclick) is a pointer-based convenience. By design, it doesn't change the grid's table semantics, so rows don't receive keyboard focus, don't respond to `Enter` or `Space`, and aren't announced as interactive by assistive technologies. Making a row focusable with `tabindex` and `role="button"` isn't a supported workaround, as it breaks the table semantics that assistive technologies rely on to navigate the grid.

Treat a row click as a shortcut for pointer users rather than the only path to an action. For any action that a row click performs, provide a keyboard-accessible control in a cell, such as a button or a link:

```
<TemplateColumn Title="Details">
    <a href="@($"/person/{context.Id}")">View</a>
</TemplateColumn>
```

## Sort by column

The `QuickGrid` component can sort items by columns. Selecting a header navigates to a URL with updated `sort` and `direction` parameters. `SortByColumnAsync` navigates via `NavigationManager.NavigateTo(GetSortQueryStringUrl(...))`, so the URL always reflects the sort state.

The `QuickGrid` component can sort items by columns. In Blazor Web Apps, sorting requires the component to adopt an [interactive render mode](render-modes?view=aspnetcore-10.0#render-modes).

Add `Sortable="true"` ([Sortable](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.sortable)) to the [PropertyColumn\<TGridItem,TProp>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2) tag:

```
<PropertyColumn Property="..." Sortable="true" />
```

In the running app, sort the QuickGrid column by selecting the rendered column title.

Sort state in the URL uses the column's `Title` property as the identifier. The `sort` query parameter is set to `column.Title` (example for column title `Name`: `?sort=Name&direction=asc`). On a URL change, `QuickGrid` matches the `sort` value back to a column by executing `_columns.FirstOrDefault(c => c.Title == sort.ColumnTitle)`. If no column title matches, the sort is ignored and the grid falls back to its default sort.

Renaming a column's `Title` is a URL-breaking change. Any bookmarked or shared URLs containing the old title in the `sort` parameter stop matching, and the grid silently falls back to the default sort instead of sorting by the intended column. For `PropertyColumn`, the `Title` defaults to the property name (example: `Property="@(p => p.FirstName)"` produces `Title="First Name"`), so renaming the property or explicitly changing the `Title` parameter both break existing URLs.

Note

If you want to share stylesheet classes between URL-based and inner-state based pagination, selectors targeting `button.col-title` must also target `a.col-title`, and `nav button`/`nav button:disabled` require `nav a`/`nav a[aria-disabled="true"]`. The built-in QuickGrid stylesheet provides both by default.

## Open and return from a details page with a paged QuickGrid component

A paged QuickGrid component can open a details page for a record and return to the correct page of results using the approach in this section. URL-based navigation is used to save the page number and return the user to the same page of items from a details page.

A paged QuickGrid component can open a details page for a record and return to the correct page of results using the approach in this section.

Note

The approach described in this section is simplified by URL-based navigation in .NET 11 or later. For more information, see this section in a .NET 11 or later version of this article.

The following API is used:

* [PaginationState.CurrentPageIndex](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate.currentpageindex): Gets the current zero-based page index.
* [PaginationState.SetCurrentPageIndexAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate.setcurrentpageindexasync): Sets the current page index and notifies any associated `QuickGrid` components to fetch and render updated data.

The `Details` component receives the page number from the query string in the `Page` property and uses it to form a link back to the `QuickGrid` component at `/scifi-characters`.

`Details.razor`:

```
@page "/details"

<ul>
    <li>Character ID for this detail record: @Id</li>
    <li>QuickGrid page number: @Page</li>
</ul>
<div>
    @if (Page.HasValue)
    {
        <a href="@($"/scifi-characters?page={Page}")">Back to List</a>
    }
    else
    {
        <a href="/scifi-characters">Back to List</a>
    }
</div>

@code {
    [SupplyParameterFromQuery]
    private int? Id { get; set; }

    [SupplyParameterFromQuery]
    private int? Page { get; set; }
}
```

The `SciFiCharacters` component:

* Automatically pages the `QuickGrid` component on component initialization using [URL-based navigation](#pagination-modes), which sets the page index from the value of a `page` query string value, if it exists.
* Opens the preceding `Details` component with the current page number, which is the current page index ([CurrentPageIndex](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate.currentpageindex)) incremented by one to make the value a one-based index in the query string. A one-based index for the `page` query string parameter matches the rendered `Paginator` component's rendered one-based page number in the UI.

`ScifiCharacters.razor`:

```
@page "/scifi-characters"
@using Microsoft.AspNetCore.Components.QuickGrid
@inject NavigationManager Navigation

<QuickGrid Items="characters" Pagination="pagination" 
    OnRowClick="@((Character args) => HandleRowClick(args))">
    <PropertyColumn Property="@(c => c.Id)" />
    <PropertyColumn Property="@(c => c.Name)" />
</QuickGrid>

<Paginator State="pagination" />

@code {
    PaginationState pagination = new PaginationState { ItemsPerPage = 3 };

    private record Character(int Id, string Name);

    private IQueryable<Character> characters = new[]
    {
        new Character(0, "Ellen Ripley"),
        new Character(1, "Darth Vader"),
        new Character(2, "Rick Deckard"),
        new Character(3, "Sarah Connor"),
        new Character(4, "Malcolm Reynolds"),
        new Character(5, "Kara Thrace"),
        new Character(6, "James Kirk"),
        new Character(7, "Flash Gordon"),
        new Character(8, "Max Rockatansky"),
        new Character(9, "Katniss Everdeen"),
        new Character(10, "Ellie Sattler"),
        new Character(11, "Leela")
    }.AsQueryable();

    private void HandleRowClick(Character character)
    {
        Navigation.NavigateTo(
            $"/details?id={character.Id}&page={pagination.CurrentPageIndex + 1}");
    }
}
```

If the `QuickGrid` component sets `QueryParameterNameOptions`, set the query string parameter key for the results page in the `Details` component to match the `Page` property of the options. In the following example, the `QuickGrid` component is assigned a `QueryParameterNameOptions` instance with a `scifi-characters-quickgrid_` prefix, which results in a page query string parameter named `scifi-characters-quickgrid_page`.

In `Characters.razor`:

```
<QuickGrid ... 
    QueryParameterNameOptions="@(new QueryParameterNameOptions("scifi-characters-quickgrid_"))">
```

In `Details.razor`:

```
<a href="@($"/scifi-characters?scifi-characters-quickgrid_page={Page}")">Back to List</a>
```

Note

To disable URL-based navigation, set the following feature flag in the app's `Program` file:

```
AppContext.SetSwitch(
    "Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting", 
    false);
```

* Pages the `QuickGrid` component by calling [PaginationState.SetCurrentPageIndexAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate.setcurrentpageindexasync) on component initialization, setting the page index with the value of `Page` (page number) minus one (`-1`). The `page` query string parameter is removed after setting the page index using [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) and [`GetUriWithQueryParameter`](../fundamentals/navigation?view=aspnetcore-10.0#query-strings).
* Opens the preceding `Details` component with the current page number, the page index incremented by one (`+1`), to make the value a one-based index ([CurrentPageIndex](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.paginationstate.currentpageindex)) in the query string. A one-based index for the `page` query string parameter matches the rendered `Paginator` component's rendered one-based page number in the UI.

`ScifiCharacters.razor`:

```
@page "/scifi-characters"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.QuickGrid
@inject NavigationManager Navigation

<QuickGrid Items="characters" Pagination="pagination">
    <PropertyColumn Property="@(c => c.Id)" />
    <PropertyColumn Property="@(c => c.Name)" />
    <TemplateColumn Context="c">
        <a href="@($"/details?id={c.Id}&page={pagination.CurrentPageIndex + 1}")">
            Details
        </a>
    </TemplateColumn>
</QuickGrid>

<Paginator State="pagination" />

@code {
    PaginationState pagination = new PaginationState { ItemsPerPage = 3 };

    private record Character(int Id, string Name);

    private IQueryable<Character> characters = new[]
    {
        new Character(0, "Ellen Ripley"),
        new Character(1, "Darth Vader"),
        new Character(2, "Rick Deckard"),
        new Character(3, "Sarah Connor"),
        new Character(4, "Malcolm Reynolds"),
        new Character(5, "Kara Thrace"),
        new Character(6, "James Kirk"),
        new Character(7, "Flash Gordon"),
        new Character(8, "Max Rockatansky"),
        new Character(9, "Katniss Everdeen"),
        new Character(10, "Ellie Sattler"),
        new Character(11, "Leela")
    }.AsQueryable();

    [SupplyParameterFromQuery]
    private int? Page { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (Page.HasValue && Page > 0)
        {
            await pagination.SetCurrentPageIndexAsync(Page.Value - 1);
            Navigation.NavigateTo(
                Navigation.GetUriWithQueryParameter("page", (int?)null));
        }
    }
}
```

## Apply row styles

Apply styles to rows using [CSS isolation](css-isolation?view=aspnetcore-10.0), which can include styling empty rows for `QuickGrid` components that [page data with a `Paginator` component](#page-items-with-a-paginator-component).

Wrap the `QuickGrid` component in a wrapper block element, for example a `<div>`:

```
+ <div>
    <QuickGrid ...>
        ...
    </QuickGrid>
+ </div>
```

Apply a row style with the `::deep` [pseudo-element](https://developer.mozilla.org/docs/Web/CSS/Pseudo-elements). In the following example, row height is set to `2em`, including for empty data rows.

`{COMPONENT}.razor.css`:

```
::deep tr {
    height: 2em;
}
```

Alternatively, use the following CSS styling approach:

* Display row cells populated with data.
* Don't display empty row cells, which avoids empty row cell borders from rendering per Bootstrap styling.

`{COMPONENT}.razor.css`:

```
::deep tr:has(> td:not(:empty)) > td {
    display: table-cell;
}

::deep td:empty {
    display: none;
}
```

For more information on using `::deep` [pseudo-elements](https://developer.mozilla.org/docs/Web/CSS/Pseudo-elements) with CSS isolation, see [ASP.NET Core Blazor CSS isolation](css-isolation?view=aspnetcore-10.0#child-component-support).

## Custom attributes and styles

QuickGrid also supports passing custom attributes and style classes ([Class](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.class)) to the rendered table element:

```
<QuickGrid Items="..." custom-attribute="value" Class="custom-class">
```

## Style a table row based on the row item

Apply a stylesheet class to a row of the grid based on the row item using the [RowClass](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.rowclass) parameter.

In the following example:

* A row item is represented by the `Person` [record](/en-us/dotnet/csharp/language-reference/builtin-types/record). The `Person` record includes a `FirstName` property.
* The `GetRowCssClass` method applies the `highlight-row` class styles to any row where the person's first name is "`Julie`."

```
<QuickGrid ... RowClass="GetRowCssClass">
    ...
</QuickGrid>

@code {
    private record Person(int PersonId, string FirstName, string LastName);

    private string GetRowCssClass(Person person) =>
        person.FirstName == "Julie" ? "highlight-row" : null;
}
```

### Close `QuickGrid` column options

Close the `QuickGrid` column options UI with the [HideColumnOptionsAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.hidecolumnoptionsasync) method.

The following example closes the column options UI as soon as the title filter is applied:

```
<QuickGrid @ref="movieGrid" Items="movies">
    <PropertyColumn Property="@(m => m.Title)" Title="Title">
        <ColumnOptions>
            <input type="search" @bind="titleFilter" placeholder="Filter by title" 
                @bind:after="@(() => movieGrid.HideColumnOptionsAsync())" />
        </ColumnOptions>
    </PropertyColumn>
    <PropertyColumn Property="@(m => m.Genre)" Title="Genre" />
    <PropertyColumn Property="@(m => m.ReleaseYear)" Title="Release Year" />
</QuickGrid>

@code {
    private QuickGrid<Movie>? movieGrid;
    private string titleFilter = string.Empty;
    private IQueryable<Movie> movies = new List<Movie> { ... }.AsQueryable();
    private IQueryable<Movie> filteredMovies => 
        movies.Where(m => m.Title!.Contains(titleFilter));
}
```

## Entity Framework Core (EF Core) data source

Use the factory pattern to resolve an EF Core database context that provides data to a `QuickGrid` component. For more information on why the factory pattern is recommended, see [ASP.NET Core Blazor with Entity Framework Core (EF Core)](../blazor-ef-core?view=aspnetcore-10.0).

A database context factory ([IDbContextFactory\<TContext>](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1)) is injected into the component with the `@inject` directive. The factory approach requires disposal of the database context, so the component implements the [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable) interface with the `@implements` directive. The item provider for the `QuickGrid` component is a `DbSet<T>` obtained from the created database context ([CreateDbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1.createdbcontext)) of the injected database context factory.

QuickGrid recognizes EF-supplied [IQueryable](/en-us/dotnet/api/system.linq.iqueryable) instances and knows how to resolve queries asynchronously for efficiency.

Add a package reference for the [`Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter).

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Call [AddQuickGridEntityFrameworkAdapter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkadapterservicecollectionextensions.addquickgridentityframeworkadapter) on the service collection in the `Program` file to register an EF-aware [IAsyncQueryExecutor](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.iasyncqueryexecutor) implementation:

```
builder.Services.AddQuickGridEntityFrameworkAdapter();
```

The following example uses an `ExampleTable` [DbSet\<TEntity>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) (table) from a `AppDbContext` database context (`context`) as the data source for a `QuickGrid` component:

```
@using Microsoft.AspNetCore.Components.QuickGrid
@using Microsoft.EntityFrameworkCore
@implements IAsyncDisposable
@inject IDbContextFactory<AppDbContext> DbFactory

...

<QuickGrid ... Items="context.ExampleTable" ...>
    ...
</QuickGrid>

@code {
    private AppDbContext context = default!;

    protected override void OnInitialized()
    {
        context = DbFactory.CreateDbContext();
    }

    public async ValueTask DisposeAsync() => await context.DisposeAsync();
}
```

In the code block (`@code`) of the preceding example:

* The `context` field holds the database context, typed as an `AppDbContext`.
* The `OnInitialized` lifecycle method assigns a new database context ([CreateDbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1.createdbcontext)) to the `context` field from the injected factory (`DbFactory`).
* The asynchronous `DisposeAsync` method disposes of the database context when the component is disposed.

You may also use any EF-supported LINQ operator to filter the data before passing it to the [Items](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.items) parameter.

The following example filters movies by a movie title entered in a search box. The database context is `BlazorWebAppMoviesContext`, and the model is `Movie`. The movie's `Title` property is used for the filter operation.

```
@using Microsoft.AspNetCore.Components.QuickGrid
@using Microsoft.EntityFrameworkCore
@implements IAsyncDisposable
@inject IDbContextFactory<BlazorWebAppMoviesContext> DbFactory

...

<p>
    <input type="search" @bind="titleFilter" @bind:event="oninput" />
</p>

<QuickGrid ... Items="FilteredMovies" ...>
    ...
</QuickGrid>

@code {
    private string titleFilter = string.Empty;
    private BlazorWebAppMoviesContext context = default!;

    protected override void OnInitialized()
    {
        context = DbFactory.CreateDbContext();
    }

    private IQueryable<Movie> FilteredMovies => 
        context.Movie.Where(m => m.Title!.Contains(titleFilter));

    public async ValueTask DisposeAsync() => await context.DisposeAsync();
}
```

For a working example, see the following resources:

* [Build a Blazor movie database app tutorial](../tutorials/movie-database-app/?view=aspnetcore-10.0)
* [Blazor movie database sample app](https://github.com/dotnet/blazor-samples): Select the latest version folder in the repository. The sample folder for the tutorial's project is named `BlazorWebAppMovies`.

## Display name support

A column title can be assigned using [ColumnBase\<TGridItem>.Title](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.title#microsoft-aspnetcore-components-quickgrid-columnbase-1-title) in the [PropertyColumn\<TGridItem,TProp>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2)'s tag. In the following movie example, the column is given the name "`Release Date`" for the column's movie release date data:

```
<PropertyColumn Property="movie => movie.ReleaseDate" Title="Release Date" />
```

However, managing column titles (names) from bound model properties is usually a better choice for maintaining an app. A model can control the display name of a property with the [`[Display]` attribute](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute). In the following example, the model specifies a movie release date display name of "`Release Date`" for its `ReleaseDate` property:

```
[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; set; }
```

To enable the `QuickGrid` component to use the [DisplayAttribute.Name](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute.name#system-componentmodel-dataannotations-displayattribute-name) property, subclass [PropertyColumn\<TGridItem,TProp>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.propertycolumn-2), either in the component or in a separate class. Call the [GetName](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute.getname) method to return the localized [DisplayAttribute.Name](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute.name#system-componentmodel-dataannotations-displayattribute-name) value if an unlocalized [DisplayName](/en-us/dotnet/api/system.componentmodel.displaynameattribute.displayname#system-componentmodel-displaynameattribute-displayname) ([`[DisplayName]` attribute](/en-us/dotnet/api/system.componentmodel.displaynameattribute)) doesn't hold the value:

```
public class DisplayNameColumn<TGridItem, TProp> : PropertyColumn<TGridItem, TProp>
{
    protected override void OnParametersSet()
    {
        if (Title is null && Property.Body is MemberExpression memberExpression)
        {
            var memberInfo = memberExpression.Member;
            Title = 
                memberInfo.GetCustomAttribute<DisplayNameAttribute>().DisplayName ??
                memberInfo.GetCustomAttribute<DisplayAttribute>().GetName() ??
                memberInfo.Name;
        }

        base.OnParametersSet();
    }
}
```

Use the subclass in the `QuickGrid` component. In the following example, the preceding `DisplayNameColumn` is used. The name "`Release Date`" is provided by the [`[Display]` attribute](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) in the model, so there's no need to specify a [Title](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.columnbase-1.title#microsoft-aspnetcore-components-quickgrid-columnbase-1-title):

```
<DisplayNameColumn Property="movie => movie.ReleaseDate" />
```

The [`[DisplayName]` attribute](/en-us/dotnet/api/system.componentmodel.displaynameattribute) is also supported:

```
[DisplayName("Release Date")]
public DateTime ReleaseDate { get; set; }
```

However, the `[Display]` attribute is recommended because it makes additional properties available. For example, the `[Display]` attribute offers the ability to assign a resource type for localization.

## Remote data

In Blazor WebAssembly apps, fetching data from a JSON-based web API on a server is a common requirement. To fetch only the data that's required for the current page/viewport of data and apply sorting or filtering rules on the server, use the [ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsprovider) parameter.

[ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsprovider) can also be used in a server-side Blazor app if the app is required to query an external endpoint or in other cases where the requirements aren't covered by an [IQueryable](/en-us/dotnet/api/system.linq.iqueryable).

Supply a callback matching the [GridItemsProvider\<TGridItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.griditemsprovider-1) delegate type, where `TGridItem` is the type of data displayed in the grid. The callback is given a parameter of type [GridItemsProviderRequest\<TGridItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.griditemsproviderrequest-1), which specifies the start index, maximum row count, and sort order of data to return. In addition to returning the matching items, a total item count (`totalItemCount`) is also required for paging and virtualization to function correctly.

The following example obtains data from the public [OpenFDA Food Enforcement database](https://open.fda.gov/apis/food/enforcement/).

The [GridItemsProvider\<TGridItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.griditemsprovider-1) converts the [GridItemsProviderRequest\<TGridItem>](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.griditemsproviderrequest-1) into a query against the OpenFDA database. Query parameters are translated into the particular URL format supported by the external JSON API. It's only possible to perform sorting and filtering via sorting and filtering that's supported by the external API. The OpenFDA endpoint doesn't support sorting, so none of the columns are marked as sortable. However, it does support skipping records (`skip` parameter) and limiting the return of records (`limit` parameter), so the component can enable virtualization and scroll quickly through tens of thousands of records.

`FoodRecalls.razor`:

```
@page "/food-recalls"
@inject HttpClient Http
@inject NavigationManager Navigation

<PageTitle>Food Recalls</PageTitle>

<h1>OpenFDA Food Recalls</h1>

<div class="grid" tabindex="-1">
    <QuickGrid ItemsProvider="@foodRecallProvider" Virtualize="true">
        <PropertyColumn Title="ID" Property="@(c => c.Event_Id)" />
        <PropertyColumn Property="@(c => c.State)" />
        <PropertyColumn Property="@(c => c.City)" />
        <PropertyColumn Title="Company" Property="@(c => c.Recalling_Firm)" />
        <PropertyColumn Property="@(c => c.Status)" />
    </QuickGrid>
</div>

<p>Total: <strong>@numResults results found</strong></p>

@code {
    private GridItemsProvider<FoodRecall>? foodRecallProvider;
    private int numResults;

    protected override async Task OnInitializedAsync()
    {
        foodRecallProvider = async req =>
        {
            var url = Navigation.GetUriWithQueryParameters(
                "https://api.fda.gov/food/enforcement.json", 
                new Dictionary<string, object?>
            {
                { "skip", req.StartIndex },
                { "limit", req.Count },
            });

            using var response = await Http.GetFromJsonAsync<FoodRecallQueryResult>(
                url, req.CancellationToken);

            return GridItemsProviderResult.From(
                items: response!.Results,
                totalItemCount: response!.Meta.Results.Total);
        };

        numResults = (await Http.GetFromJsonAsync<FoodRecallQueryResult>(
            "https://api.fda.gov/food/enforcement.json"))!.Meta.Results.Total;
    }
}
```

For more information on calling web APIs, see [Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0).

## QuickGrid scaffolder

The QuickGrid scaffolder scaffolds Razor components with QuickGrid to display data from a database.

The scaffolder generates basic Create, Read, Update, and Delete (CRUD) pages based on an Entity Framework Core data model. You can scaffold individual pages or all of the CRUD pages. You select the model class and the `DbContext`, optionally creating a new `DbContext` if needed.

The scaffolded Razor components are added to the project's in a generated folder named after the model class. The generated `Index` component uses a `QuickGrid` component to display the data. Customize the generated components as needed and enable interactivity to take advantage of interactive features, such as [paging](#page-items-with-a-paginator-component), [sorting](#sort-by-column) and filtering.

The components produced by the scaffolder require server-side rendering (SSR), so they aren't supported when running on WebAssembly.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [.NET CLI](#tabpanel_1_net-cli)

Right-click on the `Components/Pages` folder and select **Add** > **New Scaffolded Item**.

With the **Add New Scaffold Item** dialog open to **Installed** > **Common** > **Blazor** > **Razor Component**, select **Razor Components using Entity Framework (CRUD)**. Select the **Add** button.

*CRUD* is an acronym for Create, Read, Update, and Delete. The scaffolder produces create, edit, delete, details, and index components for the app.

Complete the **Add Razor Components using Entity Framework (CRUD)** dialog:

* The **Template** dropdown list includes other templates for specifically creating create, edit, delete, details, and list components. This dropdown list comes in handy when you only need to create a specific type of component scaffolded to a model class. Leave the **Template** dropdown list set to **CRUD** to scaffold a full set of components.

* In the **Model class** dropdown list, select the model class. A folder is created for the generated components from the model name (if the model class is named `Movie`, the folder is automatically named `MoviePages`).

* For **DbContext class**, take either of the following approaches:

  * Select an existing [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.
  * Select the **+** (plus sign) button and use the **Add Data Context** modal dialog to supply a new [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class name. The scaffolder registers a new context with a factory provider instead of using the context type directly as a service registration, and it updates an existing `ApplicationDbContext` registration to use a factory provider when needed.

* After the model dialog closes, the **Database provider** dropdown list defaults to **SQL Server**. You can select the appropriate provider for the database that you're using. The options include SQL Server, SQLite, PostgreSQL, and Azure Cosmos DB.

* Select **Add**.

Paste all of the following commands at the prompt (`>`) of the **Terminal** (**Terminal** menu > **New Terminal**) opened to the project's root directory. When you paste multiple commands, a warning appears stating that multiple commands will execute. Dismiss the warning and proceed with the paste operation.

When you paste multiple commands, all of the commands execute except the last one. The last command doesn't execute until you press `Enter` on the keyboard.

```
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.AspNetCore.Components.QuickGrid
dotnet add package Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter
```

Important

After the first eight commands execute, make sure that you press `Enter` on the keyboard to execute the last command.

The preceding commands add:

* [Command-line interface (CLI) tools for EF Core](/en-us/ef/core/miscellaneous/cli/dotnet)
* [`aspnet-codegenerator` scaffolding tool](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)
* Design time tools for EF Core
* The SQLite and SQL Server providers with the EF Core package as a dependency
* [`Microsoft.VisualStudio.Web.CodeGeneration.Design`](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) for scaffolding

In the **Terminal**, execute the following command to scaffold a full set of components with the `CRUD` template:

```
dotnet aspnet-codegenerator blazor CRUD -dbProvider {PROVIDER} -dc {DB CONTEXT CLASS} -m {MODEL} -outDir {PATH}
```

Note

The preceding command is a .NET CLI command, and .NET CLI commands are executed when entered at a [PowerShell](/en-us/powershell/) prompt, which is the default command shell of the VS Code **Terminal**.

The following table explains the ASP.NET Core code generator options in the preceding command.

| Option        | Placeholder          | Description                                                                                                                                                                                                                                                                                                                                               |
| ------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-dbProvider` | `{PROVIDER}`         | Database provider to use. Options include `sqlserver` (default), `sqlite`, `cosmos`, `postgres`.                                                                                                                                                                                                                                                          |
| `-dc`         | `{DB CONTEXT CLASS}` | The [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class to use, including the namespace.                                                                                                                                                                                                                                         |
| `-m`          | `{MODEL}`            | The name of the model class.                                                                                                                                                                                                                                                                                                                              |
| `-outDir`     | `{PATH}`             | The output directory for the generated components. A folder is created from the model name in the output directory to hold the components (if the model class is named `Movie`, the folder is automatically named `MoviePages`). The path is typically either `Components/Pages` for a Blazor Web App or `Pages` for a standalone Blazor WebAssembly app. |

For the additional Blazor provider options, use the .NET CLI help option (`-h`|`--help`):

```
dotnet aspnet-codegenerator blazor -h
```

Paste all of the following commands at the prompt (`>`) of a command shell opened to the project's root directory. When you paste multiple commands, a warning appears stating that multiple commands will execute. Dismiss the warning and proceed with the paste operation.

When you paste multiple commands, all of the commands execute except the last one. The last command doesn't execute until you press `Enter` on the keyboard.

```
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.AspNetCore.Components.QuickGrid
dotnet add package Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter
```

Important

After the first eight commands execute, make sure that you press `Enter` on the keyboard to execute the last command.

The preceding commands add:

* [Command-line interface (CLI) tools for EF Core](/en-us/ef/core/miscellaneous/cli/dotnet)
* [`aspnet-codegenerator` scaffolding tool](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0)
* Design time tools for EF Core
* The SQLite and SQL Server providers with the EF Core package as a dependency
* [`Microsoft.VisualStudio.Web.CodeGeneration.Design`](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) for scaffolding.

In a command shell, execute the following command to scaffold a full set of components with the `CRUD` template:

```
dotnet aspnet-codegenerator blazor CRUD -dbProvider {PROVIDER} -dc {DB CONTEXT CLASS} -m {MODEL} -outDir {PATH}
```

The following table explains the ASP.NET Core code generator options in the preceding command.

| Option        | Placeholder          | Description                                                                                                                                                                                                                                                                                                                                               |
| ------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-dbProvider` | `{PROVIDER}`         | Database provider to use. Options include `sqlserver` (default), `sqlite`, `cosmos`, `postgres`.                                                                                                                                                                                                                                                          |
| `-dc`         | `{DB CONTEXT CLASS}` | The [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class to use, including the namespace.                                                                                                                                                                                                                                         |
| `-m`          | `{MODEL}`            | The name of the model class.                                                                                                                                                                                                                                                                                                                              |
| `-outDir`     | `{PATH}`             | The output directory for the generated components. A folder is created from the model name in the output directory to hold the components (if the model class is named `Movie`, the folder is automatically named `MoviePages`). The path is typically either `Components/Pages` for a Blazor Web App or `Pages` for a standalone Blazor WebAssembly app. |

For the additional Blazor provider options, use the .NET CLI help option (`-h`|`--help`):

```
dotnet aspnet-codegenerator blazor -h
```

For an example use of the QuickGrid scaffolder, see [Build a Blazor movie database app (Overview)](../tutorials/movie-database-app/?view=aspnetcore-10.0).

## Multiple concurrent EF Core queries trigger `System.InvalidOperationException`

Multiple concurrent EF Core queries can trigger the following [System.InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception):

> System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https\://go.microsoft.com/fwlink/?linkid=2097913.

This scenario is scheduled for improvement in an upcoming release of ASP.NET Core. For more information, see [\[Blazor\] Improve the experience with QuickGrid and EF Core (`dotnet/aspnetcore` #58716)](https://github.com/dotnet/aspnetcore/issues/58716).

In the meantime, you can address the problem using an [ItemsProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.quickgrid-1.itemsprovider) with a cancellation token. The cancellation token prevents concurrent queries by cancelling the previous request when a new request is issued.

Consider the following example, which is based on the movie database `Index` component for the [Build a Blazor movie database app (Overview)](../tutorials/movie-database-app/?view=aspnetcore-10.0) tutorial. The simpler version scaffolded into the app can be seen in the article's [sample app](../tutorials/movie-database-app/?view=aspnetcore-10.0#sample-app). The `Index` component scaffolded into the app is replaced by the following component.

`Components/Pages/MoviePages/Index.razor`:

```
@page "/movies"
@rendermode InteractiveServer
@using Microsoft.EntityFrameworkCore
@using Microsoft.AspNetCore.Components.QuickGrid
@using BlazorWebAppMovies.Models
@using BlazorWebAppMovies.Data
@inject IDbContextFactory<BlazorWebAppMovies.Data.BlazorWebAppMoviesContext> DbFactory

<PageTitle>Index</PageTitle>

<h1>Index</h1>

<div>
    <input type="search" @bind="titleFilter" @bind:event="oninput" />
</div>

<p>
    <a href="movies/create">Create New</a>
</p>

<div>
    <QuickGrid Class="table" TGridItem="Movie" ItemsProvider="GetMovies"
            ItemKey="(x => x.Id)" Pagination="pagination">
        <PropertyColumn Property="movie => movie.Title" Sortable="true" />
        <PropertyColumn Property="movie => movie.ReleaseDate" Title="Release Date" />
        <PropertyColumn Property="movie => movie.Genre" />
        <PropertyColumn Property="movie => movie.Price" />
        <PropertyColumn Property="movie => movie.Rating" />

        <TemplateColumn Context="movie">
            <a href="@($"movies/edit?id={movie.Id}")">Edit</a> |
            <a href="@($"movies/details?id={movie.Id}")">Details</a> |
            <a href="@($"movies/delete?id={movie.Id}")">Delete</a>
        </TemplateColumn>
    </QuickGrid>
</div>

<Paginator State="pagination" />

@code {
    private BlazorWebAppMoviesContext context = default!;
    private PaginationState pagination = new PaginationState { ItemsPerPage = 5 };
    private string titleFilter = string.Empty;

    public async ValueTask<GridItemsProviderResult<Movie>> GetMovies(GridItemsProviderRequest<Movie> request)
    {
        using var context = DbFactory.CreateDbContext();
        var totalCount = await context.Movie.CountAsync(request.CancellationToken);
        IQueryable<Movie> query = context.Movie.OrderBy(x => x.Id);
        query = request.ApplySorting(query).Skip(request.StartIndex);

        if (request.Count.HasValue)
        {
            query = query.Take(request.Count.Value);
        }

        var items = await query.ToArrayAsync(request.CancellationToken);

        var result = new GridItemsProviderResult<Movie>
        {
            Items = items,
            TotalItemCount = totalCount
        };

        return result;
    }
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/scaling-aspnet-apps/scaling-aspnet-apps
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/scaling-aspnet-apps/scaling-aspnet-apps.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Deploying and scaling an ASP.NET Core app on Azure Container Apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Apps deployed to Azure that experience intermittent high demand benefit from scalability to meet demand. Scalable apps can scale out to ensure capacity during workload peaks and then scale down automatically when the peak drops, which can lower costs. Horizontal scaling (scaling out) adds new instances of a resource, such as VMs or database replicas. This article demonstrates how to deploy a horizontally scalable ASP.NET Core app to [Azure container apps](/en-us/azure/container-apps/overview) by completing the following tasks:

1. [Set up the sample project](#set-up-the-sample-project)
2. [Deploy the app to Azure Container Apps](#deploy-the-app-to-azure-container-apps)
3. [Scale and troubleshoot the app](#scale-and-troubleshoot-the-app)
4. [Create the Azure Services](#create-the-azure-services)
5. [Connect the Azure services](#connect-the-azure-services)
6. [Configure and redeploy the app](#configure-and-redeploy-the-app)

This article uses Razor Pages, but most of it applies to other ASP.NET Core apps.

In some cases, basic ASP.NET Core apps are able to scale without special considerations. However, apps that utilize certain framework features or architectural patterns require extra configurations, including the following:

* **Secure form submissions**: Razor Pages, MVC and Web API apps often rely on form submissions. By default these apps use [cross site forgery tokens](../../security/anti-request-forgery?view=aspnetcore-10.0) and internal data protection services to secure requests. When deployed to the cloud, these apps must be configured to manage data protection service concerns in a secure, centralized location.

* **SignalR circuits**: Blazor Server apps require the use of a centralized [Azure SignalR service](/en-us/azure/azure-signalr/signalr-overview) in order to securely scale. These services also utilize the data protection services mentioned previously.

* **Centralized caching or state management services**: Scalable apps may use [Azure Cache for Redis](/en-us/azure/azure-cache-for-redis/cache-overview) to provide distributed caching. [Azure storage](/en-us/azure/storage/common/storage-introduction) may be needed to store state for frameworks such as [Microsoft Orleans](/en-us/dotnet/orleans/overview), which can help write apps that manage state across many different app instances.

The steps in this article demonstrate how to properly address the preceding concerns by deploying a scalable app to Azure Container Apps. Most of the concepts in this tutorial also apply when scaling [Azure App Service](/en-us/azure/app-service/overview) instances.

## Set up the sample project

Use the GitHub Explorer sample app to follow along with this tutorial. Clone the app from GitHub using the following command:

```
git clone "https://github.com/dotnet/AspNetCore.Docs.Samples.git"
```

Navigate to the `/tutorials/scalable-razor-apps/start` folder and open the `ScalableRazor.csproj`.

The sample app uses a search form to browse GitHub repositories by name. The form relies on the built-in ASP.NET Core data protection services to handle antiforgery concerns. By default, when the app scales horizontally on Container Apps, the data protection service throws an exception.

#### Test the app

1. Launch the app in Visual Studio. The project includes a Docker file, which means that the arrow next to the run button can be selected to start the app using either a Docker Desktop setup or the standard ASP.NET Core local web server.

Use the search form to browse for GitHub repositories by name.

## Deploy the app to Azure Container Apps

Visual Studio is used to deploy the app to Azure Container Apps. Container apps provide a managed service designed to simplify hosting containerized apps and microservices.

Note

Many of the resources created for the app require a location. For this app, location isn't important. A real app should select a location closest to the clients. You may want to select a location near you.

1. In Visual Studio solution explorer, right click on the top level project node and select **Publish**.

2. In the publishing dialog, select **Azure** as the deployment target, and then select **Next**.

3. For the specific target, select **Azure Container Apps (Linux)**, and then select **Next**.

4. Create a new container app to deploy to. Select the green **+** icon to open a new dialog and enter the following values:

   [](media/scaling-deploy-visual-studio.png?view=aspnetcore-10.0#lightbox)

   * **Container app name**: Leave the default value or enter a name.

   * **Subscription name**: Select the subscription to deploy to.

   * **Resource group**: Select **New** and create a new resource group called *msdocs-scalable-razor*.

   * **Container apps environment**: Select **New** to open the container apps environment dialog and enter the following values:

     * **Environment name**: Keep the default value.

     * **Location**: Select a location near you.

     * **Azure Log Analytics Workspace**: Select **New** to open the log analytics workspace dialog.

       * **Name**: Leave the default value.
       * **Location**: Select a location near you and then select **Ok** to close the dialog.

     * Select **Ok** to close the container apps environment dialog.

   * Select **Create** to close the original container apps dialog. Visual Studio creates the container app resource in Azure.

5. Once the resource is created, make sure it's selected in the list of container apps, and then select **Next**.

6. You'll need to create an Azure Container Registry to store the published image artifact for your app. Select the green **+** icon on the container registry screen.

7. Leave the default values, and then select **Create**.

   [](media/scaling-new-registry.png?view=aspnetcore-10.0#lightbox)

8. After the container registry is created, make sure it's selected, and then select finish to close the dialog workflow and display a summary of the publishing profile.

   If Visual Studio prompts you to enable the Admin user to access the published docker container, select **Yes**.

9. Select **Publish** in the upper right of the publishing profile summary to deploy the app to Azure.

When the deployment finishes, Visual Studio launches the browser to display the hosted app. Search for `Microsoft` in the form field, and a list of repositories is displayed.

## Scale and troubleshoot the app

The app is currently working without any issues, but we'd like to scale the app across more instances in anticipation of high traffic volumes.

1. In the Azure portal, search for the `razorscaling-app-****` container app in the top level search bar and select it from the results.
2. On the overview page, select **Scale** from the left navigation, and then select **+ Edit and deploy**.
3. On the revisions page, switch to the **Scale** tab.
4. Set both the min and max instances to **4** and then select **Create**. This configuration change guarantees your app is scaled horizontally across four instances.

Navigate back to the app. When the page loads, at first it appears everything is working correctly. However, when a search term is entered and submitted, an error may occur. If an error is not displayed, submit the form several more times.

#### Troubleshooting the error

It's not immediately apparent why the search requests are failing. The browser tools indicate a 400 Bad Request response was sent back. However, you can use the logging features of container apps to diagnose errors occurring in your environment.

1. On the overview page of the container app, select **Logs** from the left navigation.

2. On the **Logs** page, close the pop-up that opens and navigate to the **Tables** tab.

3. Expand the **Custom Logs** item to reveal the **ContainerAppConsoleLogs\_CL** node. This table holds various logs for the container app that can be queried to troubleshoot problems.

   [](media/scaling-logs.png?view=aspnetcore-10.0#lightbox)

4. In the query editor, compose a basic query to search the **ContainerAppConsoleLogs\_CL Logs** table for recent exceptions, such as the following script:

   ```
   ContainerAppConsoleLogs_CL
   | where Log_s contains "exception"
   | sort by TimeGenerated desc
   | limit 500
   | project ContainerAppName_s, Log_s
   ```

   The preceding query searches the **ContainerAppConsoleLogs\_CL** table for any rows that contain the word exception. The results are ordered by the time generated, limited to 500 results, and only include the **ContainerAppName\_s** and **Log\_s** columns to make the results easier to read.

5. Select **Run**, a list of results is displayed. Read through the logs and note that most of them are related to antiforgery tokens and cryptography.

   [](media/scaling-troubleshoot.png?view=aspnetcore-10.0#lightbox)

   Important

   The errors in the app are caused by the .NET data protection services. When multiple instances of the app are running, there is no guarantee that the HTTP POST request to submit the form is routed to the same container that initially loaded the page from the HTTP GET request. If the requests are handled by different instances, the antiforgery tokens aren't handled correctly and an exception occurs.

   In the steps ahead, this issue is resolved by centralizing the data protection keys in an Azure storage service and protecting them with Key Vault.

## Create the Azure Services

To resolve the preceding errors, the following services are created and connected to the app:

* [Azure Storage Account](/en-us/azure/storage/common/storage-account-overview): Handles storing data for the Data Protection Services. Provides a centralized location to store key data as the app scales. Storage accounts can also be used to hold documents, queue data, file shares, and almost any type of blob data.
* [Azure KeyVault](/en-us/azure/key-vault/general/basic-concepts): This service stores secrets for an app, and is used to help manage encryption concerns for the Data Protection Services.

#### Create the storage account service

1. In the Azure portal search bar, enter `Storage accounts` and select the matching result.

2. On the storage accounts listing page, select **+ Create**.

3. On the **Basics** tab, enter the following values:

   * **Subscription**: Select the same subscription that you chose for the container app.
   * **Resource Group**: Select the *msdocs-scalable-razor* resource group you created previously.
   * **Storage account name**: Name the account *scalablerazorstorageXXXX* where the X's are random numbers of your choosing. This name must be unique across all of Azure.
   * **Region**: Select the same region you previously selected.

4. Leave the rest of the values at their default and select **Review**. After Azure validates the inputs, select **Create**.

Azure provisions the new storage account. When the task completes, choose **Go to resource** to view the new service.

#### Create the storage container

Create a Container to store the app's data protection keys.

1. On the overview page for the new storage account, select **Storage browser** on the left navigation.
2. Select **Blob containers**.
3. Select **+ Add container** to open the **New container** flyout menu.
4. Enter a name of *scalablerazorkeys*, leave the rest of the settings at their defaults, and then select **Create**.

The new containers appear on the page list.

#### Create the key vault service

Create a key vault to hold the keys that protect the data in the blob storage container.

1. In the Azure portal search bar, enter `Key Vault` and select the matching result.

2. On the key vault listing page, select **+ Create**.

3. On the **Basics** tab, enter the following values:

   * **Subscription**: Select the same subscription that was previously selected.
   * **Resource Group**: Select the *msdocs-scalable-razor* resource group previously created.
   * **Key Vault name**: Enter the name *scalablerazorvaultXXXX*.
   * **Region**: Select a region near your location.

4. Leave the rest of the settings at their default, and then select **Review + create**. Wait for Azure to validate your settings, and then select **Create**.

Azure provisions the new key vault. When the task completes, select **Go to resource** to view the new service.

#### Create the key

Create a secret key to protect the data in the blob storage account.

1. On the main overview page of the key vault, select **Keys** from the left navigation.
2. On the **Create a key** page, select **+ Generate/Import** to open the **Create a key** flyout menu.
3. Enter *razorkey* in the **Name** field. Leave the rest of the settings at their default values and then select **Create**. A new key appears on the key list page.

## Connect the Azure Services

The Container App requires a secure connection to the storage account and the key vault services in order to resolve the data protection errors and scale correctly. The new services are connected together using the following steps:

Important

Security role assignments through Service Connector and other tools typically take a minute or two to propagate, and in some rare cases can take up to eight minutes.

#### Connect the storage account

1. In the Azure portal, navigate to the Container App overview page.

2. On the left navigation, select **Service connector**

3. On the Service Connector page, choose **+ Create** to open the **Creation Connection** flyout panel and enter the following values:

   * **Container**: Select the Container App created previously.
   * **Service type**: Choose **Storage - blob**.
   * **Subscription**: Select the subscription previously used.
   * **Connection name**: Leave the default value.
   * **Storage account**: Select the storage account created previously.
   * **Client type**: Select **.NET**.

4. Select **Next: Authentication** to progress to the next step.

5. Select **System assigned managed identity** and choose **Next: Networking**.

6. Leave the default networking options selected, and then select **Review + Create**.

7. After Azure validates the settings, select **Create**.

The service connector enables a system-assigned managed identity on the container app. It also assigns a role of **Storage Blob Data Contributor** to the identity so it can perform data operations on the storage containers.

#### Connect the key vault

1. In the Azure portal, navigate to your Container App overview page.

2. On the left navigation, select **Service connector**.

3. On the Service Connector page, choose **+ Create** to open the **Creation Connection** flyout panel and enter the following values:

   * **Container**: Select the container app created previously.
   * **Service type**: Choose **Key Vault**.
   * **Subscription**: Select the subscription previously used.
   * **Connection name**: Leave the default value.
   * **Key vault**: Select the key vault created previously.
   * **Client type**: Select **.NET**.

4. Select **Next: Authentication** to progress to the next step.

5. Select **System assigned managed identity** and choose **Next: Networking**.

6. Leave the default networking options selected, and then select **Review + Create**.

7. After Azure validates the settings, select **Create**.

The service connector assigns a role to the identity so it can perform data operations on the key vault keys.

## Configure and redeploy the app

The necessary Azure resources have been created. In this section the app code is configured to use the new resources.

1. Install the following NuGet packages:

   * **Azure.Identity**: Provides classes to work with the Azure identity and access management services.
   * **Microsoft.Extensions.Azure**: Provides helpful extension methods to perform core Azure configurations.
   * **Azure.Extensions.AspNetCore.DataProtection.Blobs**: Allows storing ASP.NET Core DataProtection keys in Azure Blob Storage so that keys can be shared across several instances of a web app.
   * **Azure.Extensions.AspNetCore.DataProtection.Keys**: Enables protecting keys at rest using the Azure Key Vault Key Encryption/Wrapping feature.

   ```
   dotnet add package Azure.Identity
   dotnet add package Microsoft.Extensions.Azure
   dotnet add package Azure.Extensions.AspNetCore.DataProtection.Blobs
   dotnet add package Azure.Extensions.AspNetCore.DataProtection.Keys
   ```

2. Update `Program.cs` with the following highlighted code:

   ```
   using Azure.Identity;
   using Microsoft.AspNetCore.DataProtection;
   using Microsoft.Extensions.Azure;

   var builder = WebApplication.CreateBuilder(args);
   var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"];
   var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"];

   builder.Services.AddRazorPages();
   builder.Services.AddHttpClient();
   builder.Services.AddServerSideBlazor();

   builder.Services.AddAzureClientsCore();

   builder.Services.AddDataProtection()
                   .PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri),
                                                   new DefaultAzureCredential())
                   .ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI),
                                                   new DefaultAzureCredential());
   var app = builder.Build();

   if (!app.Environment.IsDevelopment())
   {
       app.UseExceptionHandler("/Error");
       app.UseHsts();
   }

   app.UseHttpsRedirection();
   app.UseStaticFiles();

   app.UseRouting();

   app.UseAuthorization();

   app.MapRazorPages();

   app.Run();
   ```

The preceding changes allow the app to manage data protection using a centralized, scalable architecture. `DefaultAzureCredential` discovers the managed identity configurations enabled earlier when the app is redeployed.

Note

The preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative such as [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential) may be more appropriate. For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

Update the placeholders in `AzureURIs` section of the `appsettings.json` file to include the following:

1. Replace the `<storage-account-name>` placeholder with the name of the `scalablerazorstorageXXXX` storage account.

2. Replace the `<container-name>` placeholder with the name of the `scalablerazorkeys` storage container.

3. Replace the `<key-vault-name>` placeholder with the name of the `scalablerazorvaultXXXX` key vault.

4. Replace the `<key-name>` placeholder in the key vault URI with the `razorkey` name created previously.

   ```
   {
     "GitHubURL": "https://api.github.com",
     "Logging": {
       "LogLevel": {
         "Default": "Information",
         "Microsoft.AspNetCore": "Warning"
       }
     },
     "AllowedHosts": "*",
     "AzureURIs": {
       "BlobStorage": "https://<storage-account-name>.blob.core.windows.net/<container-name>/keys.xml",
       "KeyVault": "https://<key-vault-name>.vault.azure.net/keys/<key-name>/"
     }
   }
   ```

#### Redeploy the app

The app is now configured correctly to use the Azure services created previously. Redeploy the app for the code changes to be applied.

1. Right click on the project node in the solution explorer and select **Publish**.
2. On the publishing profile summary view, select the **Publish** button in the upper right corner.

Visual Studio redeploys the app to the container apps environment created previously. When the processes finished, the browser launches to the app home page.

Test the app again by searching for *Microsoft* in the search field. The page should now reload with the correct results every time you submit.

## Configure roles for local development

The existing code and configuration of the app can also work while running locally during development. The `DefaultAzureCredential` class configured previously is able to pick up local environment credentials to authenticate to Azure Services. You'll need to assign the same roles to your own account that were assigned to your app's managed identity in order for the authentication to work. This should be the same account you use to log into Visual Studio or the Azure CLI.

Note

[DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) simplifies authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential). For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

#### Sign-in to your local development environment

You'll need to be signed in to the Azure CLI, Visual Studio, or Azure PowerShell for your credentials to be picked up by `DefaultAzureCredential`.

* [Azure CLI](#tabpanel_1_login-azure-cli)
* [Visual Studio](#tabpanel_1_login-visual-studio)
* [PowerShell](#tabpanel_1_login-powershell)

```
az login
```

Sign in to Azure using the account settings dialog, which can be accessed in the upper right of the Visual Studio interface.

```
Connect-AzAccount
```

#### Assign roles to your developer account

1. In the Azure portal, navigate to the `scalablerazor****` storage account created previously.
2. Select **Access Control (IAM)** from the left navigation.
3. Choose **+ Add** and then **Add role assignment** from the drop-down menu.
4. On the **Add role assignment** page, search for `Storage blob data contributor`, select the matching result, and then select **Next**.
5. Make sure **User, group, or service principal** is selected, and then select **+ Select members**.
6. In the **Select members** flyout, search for your own *user\@domain* account and select it from the results.
7. Choose **Next** and then select **Review + assign**. After Azure validates the settings, select **Review + assign** again.

As previously stated, role assignment permissions might take a minute or two to propagate, or in rare cases up to eight minutes.

Repeat the previous steps to assign a role to your account so that it can access the key vault service and secret.

1. In the Azure portal, navigate to the `razorscalingkeys` key vault created previously.
2. Select **Access Control (IAM)** from the left navigation.
3. Choose **+ Add** and then **Add role assignment** from the drop-down menu.
4. On the **Add role assignment** page, search for `Key Vault Crypto Service Encryption User`, select the matching result, and then select **Next**.
5. Make sure **User, group, or service principal** is selected, and then select **+ Select members**.
6. In the **Select members** flyout, search for your own *user\@domain* account and select it from the results.
7. Choose **Next** and then select **Review + assign**. After Azure validates your settings, select **Review + assign** again.

You may need to wait again for this role assignment to propagate.

You can then return to Visual Studio and run the app locally. The code should continue to function as expected. `DefaultAzureCredential` uses your existing credentials from Visual Studio or the Azure CLI.

Note

[DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) simplifies authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential). For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

## Enterprise web app patterns

For guidance on creating a reliable, secure, performant, testable, and scalable ASP.NET Core app, see [Enterprise web app patterns](/en-us/azure/architecture/web-apps/guides/enterprise-app-patterns/overview). A complete production-quality sample web app that implements the patterns is available.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/signalr
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/signalr.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor SignalR guidance

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure and manage SignalR connections in Blazor apps.

For general guidance on ASP.NET Core SignalR configuration, see the topics in the [Overview of ASP.NET Core SignalR](../../signalr/introduction?view=aspnetcore-10.0) area of the documentation, especially [ASP.NET Core SignalR configuration](../../signalr/configuration?view=aspnetcore-10.0#configure-server-options).

Server-side apps use ASP.NET Core SignalR to communicate with the browser. [SignalR's hosting and scaling conditions](../../signalr/publish-to-azure-web-app?view=aspnetcore-10.0) apply to server-side apps.

Blazor works best when using WebSockets as the SignalR transport due to lower latency, reliability, and [security](../../signalr/security?view=aspnetcore-10.0). Long Polling is used by SignalR when WebSockets isn't available or when the app is explicitly configured to use Long Polling.

## Azure SignalR Service with stateful reconnect

The Azure SignalR Service with SDK [v1.26.1](https://github.com/Azure/azure-signalr/releases/tag/v1.26.1) or later supports [SignalR stateful reconnect](../../signalr/configuration?view=aspnetcore-10.0#configure-stateful-reconnect) ([WithStatefulReconnect](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilderhttpextensions.withstatefulreconnect)).

## WebSocket compression for Interactive Server components

By default, Interactive Server components:

* Enable compression for [WebSocket connections](../../fundamentals/websockets?view=aspnetcore-10.0). [DisableWebSocketCompression](/en-us/dotnet/api/microsoft.aspnetcore.components.server.servercomponentsendpointoptions.disablewebsocketcompression#microsoft-aspnetcore-components-server-servercomponentsendpointoptions-disablewebsocketcompression) (default: `false`) controls WebSocket compression.

* Adopt a [`frame-ancestors`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) Content Security Policy (CSP) directive set to `'self'`, which is the default and only permits embedding the app in an `<iframe>` of the origin from which the app is served when compression is enabled or when a configuration for the WebSocket context is provided.

The default `frame-ancestors` CSP can be changed by setting the value of [ContentSecurityFrameAncestorsPolicy](/en-us/dotnet/api/microsoft.aspnetcore.components.server.servercomponentsendpointoptions.contentsecurityframeancestorspolicy) to `null` if you want to [configure the CSP in a centralized way](../security/content-security-policy?view=aspnetcore-10.0) or `'none'` for an even stricter policy. When the `frame-ancestors` CSP is managed in a centralized fashion, care must be taken to apply a policy whenever the first document is rendered. We don't recommend removing the policy completely, as it will make the app vulnerable to attack. For more information, see [Enforce a Content Security Policy for ASP.NET Core Blazor](../security/content-security-policy?view=aspnetcore-10.0#the-frame-ancestors-directive) and the [MDN CSP Guide](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP).

Use [ConfigureWebSocketAcceptContext](/en-us/dotnet/api/microsoft.aspnetcore.components.server.servercomponentsendpointoptions.configurewebsocketacceptcontext#microsoft-aspnetcore-components-server-servercomponentsendpointoptions-configurewebsocketacceptcontext) to configure the [WebSocketAcceptContext](/en-us/dotnet/api/microsoft.aspnetcore.http.websocketacceptcontext) for the WebSocket connections used by the server components. By default, a policy that enables compression and sets a CSP for the frame ancestors defined in [ContentSecurityFrameAncestorsPolicy](/en-us/dotnet/api/microsoft.aspnetcore.components.server.servercomponentsendpointoptions.contentsecurityframeancestorspolicy#microsoft-aspnetcore-components-server-servercomponentsendpointoptions-contentsecurityframeancestorspolicy) is applied.

Usage examples:

Disable compression by setting [DisableWebSocketCompression](/en-us/dotnet/api/microsoft.aspnetcore.components.server.servercomponentsendpointoptions.disablewebsocketcompression#microsoft-aspnetcore-components-server-servercomponentsendpointoptions-disablewebsocketcompression) to `true`, which reduces the [vulnerability of the app to attack](../security/interactive-server-side-rendering?view=aspnetcore-10.0#interactive-server-components-with-websocket-compression-enabled) but may result in reduced performance:

```
builder.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(o => o.DisableWebSocketCompression = true)
```

When compression is enabled, configure a stricter `frame-ancestors` CSP with a value of `'none'` (single quotes required), which allows WebSocket compression but prevents browsers from embedding the app into an `<iframe>`:

```
builder.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")
```

When compression is enabled, remove the `frame-ancestors` CSP by setting [ContentSecurityFrameAncestorsPolicy](/en-us/dotnet/api/microsoft.aspnetcore.components.server.servercomponentsendpointoptions.contentsecurityframeancestorspolicy#microsoft-aspnetcore-components-server-servercomponentsendpointoptions-contentsecurityframeancestorspolicy) to `null`. This scenario is only recommended for apps that [set the CSP in a centralized way](../security/content-security-policy?view=aspnetcore-10.0):

```
builder.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = null)
```

Important

Browsers apply CSP directives from multiple CSP headers using the strictest policy directive value. Therefore, a developer can't add a weaker `frame-ancestors` policy than `'self'` on purpose or by mistake.

Single quotes are required on the string value passed to [ContentSecurityFrameAncestorsPolicy](/en-us/dotnet/api/microsoft.aspnetcore.components.server.servercomponentsendpointoptions.contentsecurityframeancestorspolicy#microsoft-aspnetcore-components-server-servercomponentsendpointoptions-contentsecurityframeancestorspolicy):

❌Unsupported values: `none`, `self`

✔️Supported values: `'none'`, `'self'`

Additional options include specifying one or more host sources and scheme sources.

For security implications, see [Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering](../security/interactive-server-side-rendering?view=aspnetcore-10.0#interactive-server-components-with-websocket-compression-enabled). For more information, see [Enforce a Content Security Policy for ASP.NET Core Blazor](../security/content-security-policy?view=aspnetcore-10.0) and [CSP: `frame-ancestors` (MDN documentation)](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors).

### SignalR `ConfigureConnection` for Interactive Server components

Blazor provides access to configure the underlying SignalR connection options when using Interactive Server components through the `ConfigureConnection` property on `ServerComponentsEndpointOptions`. This enables configuration of `HttpConnectionDispatcherOptions` properties.

```
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(options =>
    {
        options.ConfigureConnection = dispatcherOptions =>
        {
            dispatcherOptions.CloseOnAuthenticationExpiration = true;
            dispatcherOptions.AllowStatefulReconnects = true;
            dispatcherOptions.ApplicationMaxBufferSize = 1024 * 1024;
        };
    });
```

This provides a clean, type-safe API for configuring SignalR connection settings without needing to inspect endpoint metadata.

## Disable response compression for Hot Reload

When using [Hot Reload](../../test/hot-reload?view=aspnetcore-10.0), disable response compression middleware in the `Development` environment. Whether or not the default code from a project template is used, always call [UseResponseCompression](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecompressionbuilderextensions.useresponsecompression) first in the request processing pipeline.

In the `Program` file:

```
if (!app.Environment.IsDevelopment())
{
    app.UseResponseCompression();
}
```

## Client-side SignalR cross-origin negotiation for authentication

This section explains how to configure SignalR's underlying client to send credentials, such as cookies or HTTP authentication headers.

Use [SetBrowserRequestCredentials](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.webassemblyhttprequestmessageextensions.setbrowserrequestcredentials) to set [Include](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.http.browserrequestcredentials#microsoft-aspnetcore-components-webassembly-http-browserrequestcredentials-include) on cross-origin [`fetch`](https://developer.mozilla.org/docs/Web/API/Fetch_API/Using_Fetch) requests.

`IncludeRequestCredentialsMessageHandler.cs`:

```
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Http;

public class IncludeRequestCredentialsMessageHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        return base.SendAsync(request, cancellationToken);
    }
}
```

Where a hub connection is built, assign the [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler) to the [HttpMessageHandlerFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.client.httpconnectionoptions.httpmessagehandlerfactory#microsoft-aspnetcore-http-connections-client-httpconnectionoptions-httpmessagehandlerfactory) option:

```
private HubConnectionBuilder? hubConnection;

...

hubConnection = new HubConnectionBuilder()
    .WithUrl(new Uri(Navigation.ToAbsoluteUri("/chathub")), options =>
    {
        options.HttpMessageHandlerFactory = innerHandler => 
            new IncludeRequestCredentialsMessageHandler { InnerHandler = innerHandler };
    }).Build();
```

The preceding example configures the hub connection URL to the absolute URI address at `/chathub`. The URI can also be set via a string, for example `https://signalr.example.com`, or via [configuration](configuration?view=aspnetcore-10.0). `Navigation` is an injected [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager).

For more information, see [ASP.NET Core SignalR configuration](../../signalr/configuration?view=aspnetcore-10.0#configure-other-options).

## Client-side rendering

If prerendering is configured, prerendering occurs before the client connection to the server is established. For more information, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0).

If prerendering is configured, prerendering occurs before the client connection to the server is established. For more information, see the following articles:

* [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0)
* [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../components/integration?view=aspnetcore-10.0)

## Prerendered state size and SignalR message size limit

A large prerendered state size may exceed Blazor's SignalR circuit message size limit, which results in the following:

* The SignalR circuit fails to initialize with an error on the client: Circuit host not initialized.
* The reconnection UI on the client appears when the circuit fails. Recovery isn't possible.

To resolve the problem, use ***either*** of the following approaches:

* Reduce the amount of data that you are putting into the prerendered state.
* Increase the [SignalR message size limit](signalr?view=aspnetcore-10.0#server-side-circuit-handler-options). ***WARNING***: Increasing the limit may increase the risk of Denial of Service (DoS) attacks.

## Additional client-side resources

* [Secure a SignalR hub](../security/webassembly/?view=aspnetcore-10.0#secure-a-signalr-hub)
* [Overview of ASP.NET Core SignalR](../../signalr/introduction?view=aspnetcore-10.0)
* [ASP.NET Core SignalR configuration](../../signalr/configuration?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](./?view=aspnetcore-10.0#sample-apps))

## Use session affinity (sticky sessions) for server-side web farm hosting

When more than one backend server is in use, the app must implement session affinity, also called *sticky sessions*. Session affinity ensures that a client's circuit reconnects to the same server if the connection is dropped, which is important because client state is only held in the memory of the server that first established the client's circuit.

The following error is thrown by an app that hasn't enabled session affinity in a web farm:

> Uncaught (in promise) Error: Invocation canceled due to the underlying connection being closed.

For more information on session affinity with Azure App Service hosting, see [Host and deploy ASP.NET Core server-side Blazor apps](../host-and-deploy/server/?view=aspnetcore-10.0#azure-app-service).

## Azure SignalR Service

The optional [Azure SignalR Service](../../signalr/scale?view=aspnetcore-10.0#azure-signalr-service) works in conjunction with the app's SignalR hub for scaling up a server-side app to a large number of concurrent connections. In addition, the service's global reach and high-performance data centers significantly aid in reducing latency due to geography.

The service isn't required for Blazor apps hosted in Azure App Service or Azure Container Apps but can be helpful in other hosting environments:

* To facilitate connection scale out.
* Handle global distribution.

For more information, see [Host and deploy ASP.NET Core server-side Blazor apps](../host-and-deploy/server/?view=aspnetcore-10.0#azure-signalr-service).

## Server-side circuit handler options

Configure the circuit with [CircuitOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions). View default values in the [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Server/src/CircuitOptions.cs).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Read or set the options in the `Program` file with an options delegate to [AddInteractiveServerComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serverrazorcomponentsbuilderextensions.addinteractiveservercomponents). The `{OPTION}` placeholder represents the option, and the `{VALUE}` placeholder is the value.

In the `Program` file:

```
builder.Services.AddRazorComponents().AddInteractiveServerComponents(options =>
{
    options.{OPTION} = {VALUE};
});
```

Read or set the options in the `Program` file with an options delegate to [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor). The `{OPTION}` placeholder represents the option, and the `{VALUE}` placeholder is the value.

In the `Program` file:

```
builder.Services.AddServerSideBlazor(options =>
{
    options.{OPTION} = {VALUE};
});
```

Read or set the options in `Startup.ConfigureServices` with an options delegate to [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor). The `{OPTION}` placeholder represents the option, and the `{VALUE}` placeholder is the value.

In `Startup.ConfigureServices` of `Startup.cs`:

```
services.AddServerSideBlazor(options =>
{
    options.{OPTION} = {VALUE};
});
```

To configure the [HubConnectionContext](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubconnectioncontext), use [HubConnectionContextOptions](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubconnectioncontextoptions) with [AddHubOptions](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serversideblazorbuilderextensions.addhuboptions). View the defaults for the hub connection context options in [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/server/Core/src/HubConnectionContextOptions.cs). For option descriptions in the SignalR documentation, see [ASP.NET Core SignalR configuration](../../signalr/configuration?view=aspnetcore-10.0#configure-server-options). The `{OPTION}` placeholder represents the option, and the `{VALUE}` placeholder is the value.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

In the `Program` file:

```
builder.Services.AddRazorComponents().AddInteractiveServerComponents().AddHubOptions(options =>
{
    options.{OPTION} = {VALUE};
});
```

In the `Program` file:

```
builder.Services.AddServerSideBlazor().AddHubOptions(options =>
{
    options.{OPTION} = {VALUE};
});
```

In `Startup.ConfigureServices` of `Startup.cs`:

```
services.AddServerSideBlazor().AddHubOptions(options =>
{
    options.{OPTION} = {VALUE};
});
```

Warning

The default value of [MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize) is 32 KB. Increasing the value may increase the risk of [Denial of Service (DoS) attacks](../security/interactive-server-side-rendering?view=aspnetcore-10.0#denial-of-service-dos-attacks).

Blazor relies on [MaximumParallelInvocationsPerClient](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumparallelinvocationsperclient) set to 1, which is the default value. For more information, see [MaximumParallelInvocationsPerClient > 1 breaks file upload in Blazor Server mode (`dotnet/aspnetcore` #53951)](https://github.com/dotnet/aspnetcore/issues/53951).

For information on memory management, see [Manage memory in deployed ASP.NET Core server-side Blazor apps](../host-and-deploy/server/memory-management?view=aspnetcore-10.0).

## Blazor hub options

Configure [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) options to control [HttpConnectionDispatcherOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.httpconnectiondispatcheroptions) of the Blazor hub. View the defaults for the hub connection dispatcher options in [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/common/Http.Connections/src/HttpConnectionDispatcherOptions.cs). The `{OPTION}` placeholder represents the option, and the `{VALUE}` placeholder is the value.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Place the call to `app.MapBlazorHub` after the call to `app.MapRazorComponents` in the app's `Program` file:

```
app.MapBlazorHub(options =>
{
    options.{OPTION} = {VALUE};
});
```

Configuring the hub used by [AddInteractiveServerRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.serverrazorcomponentsendpointconventionbuilderextensions.addinteractiveserverrendermode) with [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) fails with an [AmbiguousMatchException](/en-us/dotnet/api/system.reflection.ambiguousmatchexception):

> Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.

To workaround the problem for apps targeting .NET 8/9, take the following approach.

At the top of the `Program` file, add a `using` statement for [Microsoft.AspNetCore.Http.Connections](/en-us/dotnet/api/microsoft.aspnetcore.http.connections):

```
using Microsoft.AspNetCore.Http.Connections;
```

Where [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) is called, chain the following endpoint convention to the [RazorComponentsEndpointConventionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointconventionbuilder):

```
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .Add(e =>
    {
        var metadata = e.Metadata;
        var dispatcherOptions = metadata.OfType<HttpConnectionDispatcherOptions>().FirstOrDefault();

        if (dispatcherOptions != null)
        {
            dispatcherOptions.CloseOnAuthenticationExpiration = true;
        }
    });
```

For more information, see the following resources:

* [MapBlazorHub configuration in NET8 throws a The request matched multiple endpoints exception (`dotnet/aspnetcore` #51698)](https://github.com/dotnet/aspnetcore/issues/51698#issuecomment-1984340954)
* [Attempts to map multiple blazor entry points with MapBlazorHub causes Ambiguous Route Error. This worked with net7 (`dotnet/aspnetcore` #52156)](https://github.com/dotnet/aspnetcore/issues/52156#issuecomment-1984503178)
* [\[Blazor\] Provide access to the underlying SignalR HttpConnectionDispatcherOptions in AddInteractiveServerRenderMode (`dotnet/aspnetcore` #63520)](https://github.com/dotnet/aspnetcore/issues/63520)

Supply the options to `app.MapBlazorHub` in the app's `Program` file:

```
app.MapBlazorHub(options =>
{
    options.{OPTION} = {VALUE};
});
```

Supply the options to `app.MapBlazorHub` in endpoint routing configuration:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapBlazorHub(options =>
    {
        options.{OPTION} = {VALUE};
    });
    ...
});
```

## Maximum receive message size

*This section only applies to projects that implement SignalR.*

The maximum incoming SignalR message size permitted for hub methods is limited by the [HubOptions.MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize) (default: 32 KB). SignalR messages larger than [MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize) throw an error. The framework doesn't impose a limit on the size of a SignalR message from the hub to a client.

When SignalR logging isn't set to [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel) or [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel), a message size error only appears in the browser's developer tools console:

> Error: Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'.

When [SignalR server-side logging](../../signalr/diagnostics?view=aspnetcore-10.0#server-side-logging) is set to [Debug](/en-us/dotnet/api/microsoft.extensions.logging.loglevel) or [Trace](/en-us/dotnet/api/microsoft.extensions.logging.loglevel), server-side logging surfaces an [InvalidDataException](/en-us/dotnet/api/system.io.invaliddataexception) for a message size error.

`appsettings.Development.json`:

```
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      ...
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}
```

Error:

> System.IO.InvalidDataException: The maximum message size of 32768B was exceeded. The message size can be configured in AddHubOptions.

One approach involves increasing the limit by setting [MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize) in the `Program` file. The following example sets the maximum receive message size to 64 KB:

```
builder.Services.AddRazorComponents().AddInteractiveServerComponents()
    .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);
```

Increasing the SignalR incoming message size limit comes at the cost of requiring more server resources, and it increases the risk of [Denial of Service (DoS) attacks](../security/interactive-server-side-rendering?view=aspnetcore-10.0#denial-of-service-dos-attacks). Additionally, reading a large amount of content in to memory as strings or byte arrays can also result in allocations that work poorly with the garbage collector, resulting in additional performance penalties.

A better option for reading large payloads is to send the content in smaller chunks and process the payload as a [Stream](/en-us/dotnet/api/system.io.stream). This can be used when reading large JavaScript (JS) interop JSON payloads or if JS interop data is available as raw bytes. For an example that demonstrates sending large binary payloads in server-side apps that uses techniques similar to the [`InputFile` component](../file-uploads?view=aspnetcore-10.0), see the [Binary Submit sample app](https://github.com/aspnet/samples/tree/main/samples/aspnetcore/blazor/BinarySubmit) and the [Blazor `InputLargeTextArea` Component Sample](https://github.com/aspnet/samples/tree/main/samples/aspnetcore/blazor/InputLargeTextArea).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Forms that process large payloads over SignalR can also use streaming JS interop directly. For more information, see [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0#stream-from-javascript-to-net). For a forms example that streams `<textarea>` data to the server, see [Troubleshoot ASP.NET Core Blazor forms](../forms/troubleshoot?view=aspnetcore-10.0#large-form-payloads-and-the-signalr-message-size-limit).

One approach involves increasing the limit by setting [MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize) in the `Program` file. The following example sets the maximum receive message size to 64 KB:

```
builder.Services.AddServerSideBlazor()
    .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);
```

Increasing the SignalR incoming message size limit comes at the cost of requiring more server resources, and it increases the risk of [Denial of Service (DoS) attacks](../security/interactive-server-side-rendering?view=aspnetcore-10.0#denial-of-service-dos-attacks). Additionally, reading a large amount of content in to memory as strings or byte arrays can also result in allocations that work poorly with the garbage collector, resulting in additional performance penalties.

A better option for reading large payloads is to send the content in smaller chunks and process the payload as a [Stream](/en-us/dotnet/api/system.io.stream). This can be used when reading large JavaScript (JS) interop JSON payloads or if JS interop data is available as raw bytes. For an example that demonstrates sending large binary payloads in Blazor Server that uses techniques similar to the [`InputFile` component](../file-uploads?view=aspnetcore-10.0), see the [Binary Submit sample app](https://github.com/aspnet/samples/tree/main/samples/aspnetcore/blazor/BinarySubmit) and the [Blazor `InputLargeTextArea` Component Sample](https://github.com/aspnet/samples/tree/main/samples/aspnetcore/blazor/InputLargeTextArea).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Forms that process large payloads over SignalR can also use streaming JS interop directly. For more information, see [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0#stream-from-javascript-to-net). For a forms example that streams `<textarea>` data in a Blazor Server app, see [Troubleshoot ASP.NET Core Blazor forms](../forms/troubleshoot?view=aspnetcore-10.0#large-form-payloads-and-the-signalr-message-size-limit).

Increase the limit by setting [MaximumReceiveMessageSize](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumreceivemessagesize#microsoft-aspnetcore-signalr-huboptions-maximumreceivemessagesize) in `Startup.ConfigureServices`:

```
services.AddServerSideBlazor()
    .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);
```

Increasing the SignalR incoming message size limit comes at the cost of requiring more server resources, and it increases the risk of [Denial of Service (DoS) attacks](../security/interactive-server-side-rendering?view=aspnetcore-10.0#denial-of-service-dos-attacks). Additionally, reading a large amount of content in to memory as strings or byte arrays can also result in allocations that work poorly with the garbage collector, resulting in additional performance penalties.

Consider the following guidance when developing code that transfers a large amount of data:

* Leverage the native streaming JS interop support to transfer data larger than the SignalR incoming message size limit:

  * [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#stream-from-net-to-javascript)
  * [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](../javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0#stream-from-javascript-to-net)
  * Form payload example: [Troubleshoot ASP.NET Core Blazor forms](../forms/troubleshoot?view=aspnetcore-10.0#large-form-payloads-and-the-signalr-message-size-limit)

* General tips:

  * Don't allocate large objects in JS and C# code.

  * Free consumed memory when the process is completed or cancelled.

  * Enforce the following additional requirements for security purposes:

    * Declare the maximum file or data size that can be passed.
    * Declare the minimum upload rate from the client to the server.

  * After the data is received by the server, the data can be:

    * Temporarily stored in a memory buffer until all of the segments are collected.
    * Consumed immediately. For example, the data can be stored immediately in a database or written to disk as each segment is received.

- Slice the data into smaller pieces, and send the data segments sequentially until all of the data is received by the server.

- Don't allocate large objects in JS and C# code.

- Don't block the main UI thread for long periods when sending or receiving data.

- Free consumed memory when the process is completed or cancelled.

- Enforce the following additional requirements for security purposes:

  * Declare the maximum file or data size that can be passed.
  * Declare the minimum upload rate from the client to the server.

- After the data is received by the server, the data can be:

  * Temporarily stored in a memory buffer until all of the segments are collected.
  * Consumed immediately. For example, the data can be stored immediately in a database or written to disk as each segment is received.

## Blazor server-side Hub endpoint route configuration

In the `Program` file, call [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) to map the Blazor [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub) to the app's default path. The Blazor script (`blazor.*.js`) automatically points to the endpoint created by [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub).

## Reflect the server-side connection state in the UI

If the client detects a lost connection (circuit) to the server, a default UI is displayed to the user while the client attempts to reconnect:

If reconnection fails, the user is instructed to retry or reload the page:

If reconnection succeeds, user state is often lost. Custom code can be added to any component to save and reload user state across connection failures. For more information, see [ASP.NET Core Blazor state management overview](../state-management/?view=aspnetcore-10.0) and [ASP.NET Core Blazor server-side state management](../state-management/server?view=aspnetcore-10.0).

To create UI elements that track reconnection state, the following table describes:

* A set of `components-reconnect-*` CSS classes (**CSS class** column) that are set or unset by Blazor on an element with an `id` of `components-reconnect-modal`.
* A `components-reconnect-state-changed` event (**Event** column) that indicates a reconnection status change.

| CSS class                       | Event      | Indicates…                                                                                                                                                  |
| ------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `components-reconnect-show`     | `show`     | A lost connection. The client is attempting to reconnect. The reconnection modal is shown.                                                                  |
| `components-reconnect-paused`   | `paused`   | The connection is paused. For more information, see [Pause and resume circuits](../state-management/server?view=aspnetcore-10.0#pause-and-resume-circuits). |
| `components-reconnect-hide`     | `hide`     | An active connection is re-established to the server. The reconnection model is closed.                                                                     |
| `components-reconnect-retrying` | `retrying` | The client is attempting to reconnect.                                                                                                                      |
| `components-reconnect-failed`   | `failed`   | Reconnection failed, probably due to a network failure.                                                                                                     |
| `components-reconnect-rejected` | `rejected` | Reconnection rejected.                                                                                                                                      |

When the reconnection state change in `components-reconnect-state-changed` is `failed`, call `Blazor.reconnect()` in JavaScript to attempt reconnection.

When the reconnection state change is `rejected`, the server was reached but refused the connection, and the user's state on the server is lost. To reload the app, call `location.reload()` in JavaScript. This connection state may result when:

* A crash in the server-side circuit occurs.
* The client is disconnected long enough for the server to drop the user's state. Instances of the user's components are disposed.
* The server is restarted, or the app's worker process is recycled.

The developer adds an event listener on the reconnect modal element to monitor and react to reconnection state changes, as seen in the following example:

```
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", 
  handleReconnectStateChanged);

function handleReconnectStateChanged(event) {
  if (event.detail.state === "show") {
    reconnectModal.showModal();
  } else if (event.detail.state === "hide") {
    reconnectModal.close();
  } else if (event.detail.state === "failed") {
    Blazor.reconnect();
  } else if (event.detail.state === "rejected") {
    location.reload();
  }
}
```

An element with an `id` of `components-reconnect-max-retries` displays the maximum number of reconnect retries:

```
<span id="components-reconnect-max-retries"></span>
```

An element with an `id` of `components-reconnect-current-attempt` displays the current reconnect attempt:

```
<span id="components-reconnect-current-attempt"></span>
```

An element with an `id` of `components-seconds-to-next-attempt` displays the number of seconds to the next reconnection attempt:

```
<span id="components-seconds-to-next-attempt"></span>
```

The [Blazor Web App project template](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp) includes a `ReconnectModal` component (`Components/Layout/ReconnectModal.razor`) with collocated stylesheet and JavaScript files (`ReconnectModal.razor.css`, `ReconnectModal.razor.js`) that can be customized as needed. These files can be examined in the ASP.NET Core reference source or by inspecting an app created from the Blazor Web App project template. The component is added to the project when the project is created in Visual Studio with **Interactive render mode** set to **Server** or **Auto** or created with the .NET CLI with the option `--interactivity server` (default) or `--interactivity auto`.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

To customize the UI, define a single element with an `id` of `components-reconnect-modal` in the `<body>` element content. The following example places the element in the `App` component.

`App.razor`:

To customize the UI, define a single element with an `id` of `components-reconnect-modal` in the `<body>` element content. The following example places the element in the host page.

`Pages/_Host.cshtml`:

To customize the UI, define a single element with an `id` of `components-reconnect-modal` in the `<body>` element content. The following example places the element in the layout page.

`Pages/_Layout.cshtml`:

To customize the UI, define a single element with an `id` of `components-reconnect-modal` in the `<body>` element content. The following example places the element in the host page.

`Pages/_Host.cshtml`:

```
<div id="components-reconnect-modal">
    Connection lost.<br>Attempting to reconnect...
</div>
```

Note

If more than one element with an `id` of `components-reconnect-modal` are rendered by the app, only the first rendered element receives CSS class changes to display or hide the element.

Add the following CSS styles to the site's stylesheet.

`wwwroot/app.css`:

`wwwroot/css/site.css`:

```
#components-reconnect-modal {
    display: none;
}

#components-reconnect-modal.components-reconnect-show, 
#components-reconnect-modal.components-reconnect-failed, 
#components-reconnect-modal.components-reconnect-rejected {
    display: block;
    background-color: white;
    padding: 2rem;
    border-radius: 0.5rem;
    text-align: center;
    box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
    margin: 50px 50px;
    position: fixed;
    top: 0;
    z-index: 10001;
}
```

The following table describes the CSS classes applied to the `components-reconnect-modal` element by the Blazor framework.

| CSS class                       | Indicates…                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `components-reconnect-show`     | A lost connection. The client is attempting to reconnect. Show the modal.                                                                                                                                                                                                                                                                                                                                                                                                  |
| `components-reconnect-hide`     | An active connection is re-established to the server. Hide the modal.                                                                                                                                                                                                                                                                                                                                                                                                      |
| `components-reconnect-failed`   | Reconnection failed, probably due to a network failure. To attempt reconnection, call `window.Blazor.reconnect()` in JavaScript.                                                                                                                                                                                                                                                                                                                                           |
| `components-reconnect-rejected` | Reconnection rejected. The server was reached but refused the connection, and the user's state on the server is lost. To reload the app, call `location.reload()` in JavaScript. This connection state may result when:- A crash in the server-side circuit occurs.
- The client is disconnected long enough for the server to drop the user's state. Instances of the user's components are disposed.
- The server is restarted, or the app's worker process is recycled. |

Customize the delay before the reconnection UI appears by setting the `transition-delay` property in the site's CSS for the modal element. The following example sets the transition delay from 500 ms (default) to 1,000 ms (1 second).

`wwwroot/app.css`:

`wwwroot/css/site.css`:

```
#components-reconnect-modal {
    transition: visibility 0s linear 1000ms;
}
```

To display the current reconnect attempt, define an element with an `id` of `components-reconnect-current-attempt`. To display the maximum number of reconnect retries, define an element with an `id` of `components-reconnect-max-retries`. The following example places these elements inside a reconnect attempt modal element following the previous example.

```
<div id="components-reconnect-modal">
    There was a problem with the connection!
    (Current reconnect attempt: 
    <span id="components-reconnect-current-attempt"></span> /
    <span id="components-reconnect-max-retries"></span>)
</div>
```

When the custom reconnect modal appears, it renders the following content with a reconnection attempt counter:

> There was a problem with the connection! (Current reconnect attempt: 1 / 8)

## Server-side rendering

By default, components are prerendered on the server before the client connection to the server is established. For more information, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0).

By default, components are prerendered on the server before the client connection to the server is established. For more information, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

## Monitor server-side circuit activity

Monitor inbound circuit activity using the [CreateInboundActivityHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuithandler.createinboundactivityhandler) method on [CircuitHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuithandler). Inbound circuit activity is any activity sent from the browser to the server, such as UI events or JavaScript-to-.NET interop calls.

For example, you can use a circuit activity handler to detect if the client is idle and log its circuit ID ([Circuit.Id](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuit.id#microsoft-aspnetcore-components-server-circuits-circuit-id)):

```
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.Options;
using Timer = System.Timers.Timer;

public sealed class IdleCircuitHandler : CircuitHandler, IDisposable
{
    private Circuit? currentCircuit;
    private readonly ILogger logger;
    private readonly Timer timer;

    public IdleCircuitHandler(ILogger<IdleCircuitHandler> logger, 
        IOptions<IdleCircuitOptions> options)
    {
        timer = new Timer
        {
            Interval = options.Value.IdleTimeout.TotalMilliseconds,
            AutoReset = false
        };

        timer.Elapsed += CircuitIdle;
        this.logger = logger;
    }

    private void CircuitIdle(object? sender, System.Timers.ElapsedEventArgs e)
    {
        logger.LogInformation("{CircuitId} is idle", currentCircuit?.Id);
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        currentCircuit = circuit;

        return Task.CompletedTask;
    }

    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next)
    {
        return context =>
        {
            timer.Stop();
            timer.Start();

            return next(context);
        };
    }

    public void Dispose() => timer.Dispose();
}

public class IdleCircuitOptions
{
    public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(5);
}

public static class IdleCircuitHandlerServiceCollectionExtensions
{
    public static IServiceCollection AddIdleCircuitHandler(
        this IServiceCollection services, 
        Action<IdleCircuitOptions> configureOptions)
    {
        services.Configure(configureOptions);
        services.AddIdleCircuitHandler();

        return services;
    }

    public static IServiceCollection AddIdleCircuitHandler(
        this IServiceCollection services)
    {
        services.AddScoped<CircuitHandler, IdleCircuitHandler>();

        return services;
    }
}
```

Register the service in the `Program` file. The following example configures the default idle timeout of five minutes to five seconds in order to test the preceding `IdleCircuitHandler` implementation:

```
builder.Services.AddIdleCircuitHandler(options => 
    options.IdleTimeout = TimeSpan.FromSeconds(5));
```

Circuit activity handlers also provide an approach for accessing scoped Blazor services from other non-Blazor dependency injection (DI) scopes. For more information and examples, see:

* [ASP.NET Core Blazor dependency injection](dependency-injection?view=aspnetcore-10.0#access-server-side-blazor-services-from-a-different-di-scope)
* [ASP.NET Core Blazor additional server-side security scenarios](../security/additional-scenarios?view=aspnetcore-10.0#access-authenticationstateprovider-in-outgoing-request-middleware)

## Blazor startup

Configure the manual start of Blazor's SignalR circuit in the `App.razor` file of a Blazor Web App:

Configure the manual start of Blazor's SignalR circuit in the `Pages/_Host.cshtml` file (Blazor Server):

Configure the manual start of Blazor's SignalR circuit in the `Pages/_Layout.cshtml` file (Blazor Server):

Configure the manual start of Blazor's SignalR circuit in the `Pages/_Host.cshtml` file (Blazor Server):

* Add an `autostart="false"` attribute to the `<script>` tag for the `blazor.*.js` script.
* Place a script that calls `Blazor.start()` after the Blazor script is loaded and inside the closing `</body>` tag.

When `autostart` is disabled, any aspect of the app that doesn't depend on the circuit works normally. For example, client-side routing is operational. However, any aspect that depends on the circuit isn't operational until `Blazor.start()` is called. App behavior is unpredictable without an established circuit. For example, component methods fail to execute while the circuit is disconnected.

For more information, including how to initialize Blazor when the document is ready and how to chain to a [JS `Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise), see [ASP.NET Core Blazor startup](startup?view=aspnetcore-10.0).

## Configure SignalR timeouts and Keep-Alive on the client

Configure the following values for the client:

* `withServerTimeout`: Configures the server timeout in milliseconds. If this timeout elapses without receiving any messages from the server, the connection is terminated with an error. The default timeout value is 30 seconds. The server timeout should be at least double the value assigned to the Keep-Alive interval (`withKeepAliveInterval`).
* `withKeepAliveInterval`: Configures the Keep-Alive interval in milliseconds (default interval at which to ping the server). This setting allows the server to detect hard disconnects, such as when a client unplugs their computer from the network. The ping occurs at most as often as the server pings. If the server pings every five seconds, assigning a value lower than `5000` (5 seconds) pings every five seconds. The default value is 15 seconds. The Keep-Alive interval should be less than or equal to half the value assigned to the server timeout (`withServerTimeout`).

The following example for the `App.razor` file (Blazor Web App) shows the assignment of default values.

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      configureSignalR: function (builder) {
        builder.withServerTimeout(30000).withKeepAliveInterval(15000);
      }
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

The following example for the `Pages/_Host.cshtml` file (Blazor Server, all versions except ASP.NET Core in .NET 6) or `Pages/_Layout.cshtml` file (Blazor Server, ASP.NET Core in .NET 6).

Blazor Server:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      builder.withServerTimeout(30000).withKeepAliveInterval(15000);
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

When creating a hub connection in a component, set the [ServerTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.servertimeout#microsoft-aspnetcore-signalr-client-hubconnection-servertimeout) (default: 30 seconds) and [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.keepaliveinterval#microsoft-aspnetcore-signalr-client-hubconnection-keepaliveinterval) (default: 15 seconds) on the [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). Set the [HandshakeTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.handshaketimeout#microsoft-aspnetcore-signalr-client-hubconnection-handshaketimeout) (default: 15 seconds) on the built [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection). The following example shows the assignment of default values:

```
protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .WithServerTimeout(TimeSpan.FromSeconds(30))
        .WithKeepAliveInterval(TimeSpan.FromSeconds(15))
        .Build();

    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(15);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}
```

Configure the following values for the client:

* `serverTimeoutInMilliseconds`: The server timeout in milliseconds. If this timeout elapses without receiving any messages from the server, the connection is terminated with an error. The default timeout value is 30 seconds. The server timeout should be at least double the value assigned to the Keep-Alive interval (`keepAliveIntervalInMilliseconds`).
* `keepAliveIntervalInMilliseconds`: Default interval at which to ping the server. This setting allows the server to detect hard disconnects, such as when a client unplugs their computer from the network. The ping occurs at most as often as the server pings. If the server pings every five seconds, assigning a value lower than `5000` (5 seconds) pings every five seconds. The default value is 15 seconds. The Keep-Alive interval should be less than or equal to half the value assigned to the server timeout (`serverTimeoutInMilliseconds`).

The following example for the `Pages/_Host.cshtml` file (Blazor Server, all versions except ASP.NET Core in .NET 6) or `Pages/_Layout.cshtml` file (Blazor Server, ASP.NET Core in .NET 6):

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      let c = builder.build();
      c.serverTimeoutInMilliseconds = 30000;
      c.keepAliveIntervalInMilliseconds = 15000;
      builder.build = () => {
        return c;
      };
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

When creating a hub connection in a component, set the [ServerTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.servertimeout#microsoft-aspnetcore-signalr-client-hubconnection-servertimeout) (default: 30 seconds), [HandshakeTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.handshaketimeout#microsoft-aspnetcore-signalr-client-hubconnection-handshaketimeout) (default: 15 seconds), and [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.keepaliveinterval#microsoft-aspnetcore-signalr-client-hubconnection-keepaliveinterval) (default: 15 seconds) on the built [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection). The following example shows the assignment of default values:

```
protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .Build();

    hubConnection.ServerTimeout = TimeSpan.FromSeconds(30);
    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(15);
    hubConnection.KeepAliveInterval = TimeSpan.FromSeconds(15);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}
```

When changing the values of the server timeout ([ServerTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.servertimeout#microsoft-aspnetcore-signalr-client-hubconnection-servertimeout)) or the Keep-Alive interval ([KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.keepaliveinterval#microsoft-aspnetcore-signalr-client-hubconnection-keepaliveinterval)):

* The server timeout should be at least double the value assigned to the Keep-Alive interval.
* The Keep-Alive interval should be less than or equal to half the value assigned to the server timeout.

For more information, see the *Global deployment and connection failures* sections of the following articles:

* [Host and deploy ASP.NET Core server-side Blazor apps](../host-and-deploy/server/?view=aspnetcore-10.0#global-deployment-and-connection-failures)
* [Host and deploy ASP.NET Core Blazor WebAssembly](../host-and-deploy/webassembly/?view=aspnetcore-10.0#global-deployment-and-connection-failures)

## Adjust the server-side reconnection retry count and interval

To adjust the reconnection retry count and interval, set the number of retries (`maxRetries`) and period in milliseconds permitted for each retry attempt (`retryIntervalMilliseconds`).

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      reconnectionOptions: {
        maxRetries: 3,
        retryIntervalMilliseconds: 2000
      }
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Blazor Server:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    reconnectionOptions: {
      maxRetries: 3,
      retryIntervalMilliseconds: 2000
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

When the user navigates back to an app with a disconnected circuit, reconnection is attempted immediately rather than waiting for the duration of the next reconnect interval. This behavior seeks to resume the connection as quickly as possible for the user.

The default reconnect timing uses a computed backoff strategy. The first several reconnection attempts occur in rapid succession before computed delays are introduced between attempts. The default logic for computing the retry interval is an implementation detail subject to change without notice, but you can find the default logic that the Blazor framework uses [in the `computeDefaultRetryInterval` function (reference source)](https://github.com/search?q=repo%3Adotnet%2Faspnetcore%20computeDefaultRetryInterval\&type=code).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Customize the retry interval behavior by specifying a function to compute the retry interval. In the following exponential backoff example, the number of previous reconnection attempts is multiplied by 1,000 ms to calculate the retry interval. When the count of previous attempts to reconnect (`previousAttempts`) is greater than the maximum retry limit (`maxRetries`), `null` is assigned to the retry interval (`retryIntervalMilliseconds`) to cease further reconnection attempts:

Blazor Web App:

```
Blazor.start({
  circuit: {
    reconnectionOptions: {
      retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
        previousAttempts >= maxRetries ? null : previousAttempts * 1000
    },
  },
});
```

Blazor Server:

```
Blazor.start({
  reconnectionOptions: {
    retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
      previousAttempts >= maxRetries ? null : previousAttempts * 1000
  },
});
```

An alternative is to specify the exact sequence of retry intervals. After the last specified retry interval, retries stop because the `retryIntervalMilliseconds` function returns `undefined`:

Blazor Web App:

```
Blazor.start({
  circuit: {
    reconnectionOptions: {
      retryIntervalMilliseconds: 
        Array.prototype.at.bind([0, 1000, 2000, 5000, 10000, 15000, 30000]),
    },
  },
});
```

Blazor Server:

```
Blazor.start({
  reconnectionOptions: {
    retryIntervalMilliseconds: 
      Array.prototype.at.bind([0, 1000, 2000, 5000, 10000, 15000, 30000]),
  },
});
```

For more information on Blazor startup, see [ASP.NET Core Blazor startup](startup?view=aspnetcore-10.0).

## Control when the reconnection UI appears

Controlling when the reconnection UI appears can be useful in the following situations:

* A deployed app frequently displays the reconnection UI due to ping timeouts caused by internal network or Internet latency, and you would like to increase the delay.
* An app should report to users that the connection has dropped sooner, and you would like to shorten the delay.

The timing of the appearance of the reconnection UI is influenced by adjusting the Keep-Alive interval and timeouts on the client. The reconnection UI appears when the server timeout is reached on the client (`withServerTimeout`, [Client configuration](#client-configuration) section). However, changing the value of `withServerTimeout` requires changes to other Keep-Alive, timeout, and handshake settings described in the following guidance.

As general recommendations for the guidance that follows:

* The Keep-Alive interval should match between client and server configurations.
* Timeouts should be at least double the value assigned to the Keep-Alive interval.

### Server configuration

Set the following:

* [ClientTimeoutInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.clienttimeoutinterval#microsoft-aspnetcore-signalr-huboptions-clienttimeoutinterval) (default: 30 seconds): The time window clients have to send a message before the server closes the connection.
* [HandshakeTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.handshaketimeout#microsoft-aspnetcore-signalr-huboptions-handshaketimeout) (default: 15 seconds): The interval used by the server to timeout incoming handshake requests by clients.
* [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.keepaliveinterval#microsoft-aspnetcore-signalr-huboptions-keepaliveinterval) (default: 15 seconds): The interval used by the server to send keep alive pings to connected clients. Note that there is also a Keep-Alive interval setting on the client, which should match the server's value.

The [ClientTimeoutInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.clienttimeoutinterval#microsoft-aspnetcore-signalr-huboptions-clienttimeoutinterval) and [HandshakeTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.handshaketimeout#microsoft-aspnetcore-signalr-huboptions-handshaketimeout) can be increased, and the [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.keepaliveinterval#microsoft-aspnetcore-signalr-huboptions-keepaliveinterval) can remain the same. The important consideration is that if you change the values, make sure that the timeouts are at least double the value of the Keep-Alive interval and that the Keep-Alive interval matches between server and client. For more information, see the [Configure SignalR timeouts and Keep-Alive on the client](#configure-signalr-timeouts-and-keep-alive-on-the-client) section.

In the following example:

* The [ClientTimeoutInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.clienttimeoutinterval#microsoft-aspnetcore-signalr-huboptions-clienttimeoutinterval) is increased to 60 seconds (default value: 30 seconds).
* The [HandshakeTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.handshaketimeout#microsoft-aspnetcore-signalr-huboptions-handshaketimeout) is increased to 30 seconds (default value: 15 seconds).
* The [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.keepaliveinterval#microsoft-aspnetcore-signalr-huboptions-keepaliveinterval) isn't set in developer code and uses its default value of 15 seconds. Decreasing the value of the Keep-Alive interval increases the frequency of communication pings, which increases the load on the app, server, and network. Care must be taken to avoid introducing poor performance when lowering the Keep-Alive interval.

**Blazor Web App** (.NET 8 or later) in the server project's `Program` file:

```
builder.Services.AddRazorComponents().AddInteractiveServerComponents()
    .AddHubOptions(options =>
{
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
    options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});
```

**Blazor Server** in the `Program` file:

```
builder.Services.AddServerSideBlazor()
    .AddHubOptions(options =>
    {
        options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
        options.HandshakeTimeout = TimeSpan.FromSeconds(30);
    });
```

For more information, see the [Server-side circuit handler options](#server-side-circuit-handler-options) section.

### Client configuration

Set the following:

* `withServerTimeout` (default: 30 seconds): Configures the server timeout, specified in milliseconds, for the circuit's hub connection.
* `withKeepAliveInterval` (default: 15 seconds): The interval, specified in milliseconds, at which the connection sends Keep-Alive messages.

The server timeout can be increased, and the Keep-Alive interval can remain the same. The important consideration is that if you change the values, make sure that the server timeout is at least double the value of the Keep-Alive interval and that the Keep-Alive interval values match between server and client. For more information, see the [Configure SignalR timeouts and Keep-Alive on the client](#configure-signalr-timeouts-and-keep-alive-on-the-client) section.

In the following [startup configuration](startup?view=aspnetcore-10.0) example ([location of the Blazor script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script)), a custom value of 60 seconds is used for the server timeout. The Keep-Alive interval (`withKeepAliveInterval`) isn't set and uses its default value of 15 seconds.

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      configureSignalR: function (builder) {
        builder.withServerTimeout(60000);
      }
    }
  });
</script>
```

Blazor Server:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      builder.withServerTimeout(60000);
    }
  });
</script>
```

When creating a hub connection in a component, set the server timeout ([WithServerTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilderextensions.withservertimeout), default: 30 seconds) on the [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder). Set the [HandshakeTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.handshaketimeout#microsoft-aspnetcore-signalr-client-hubconnection-handshaketimeout) (default: 15 seconds) on the built [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection). Confirm that the timeouts are at least double the Keep-Alive interval ([WithKeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilderextensions.withkeepaliveinterval)/[KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.keepaliveinterval#microsoft-aspnetcore-signalr-client-hubconnection-keepaliveinterval)) and that the Keep-Alive value matches between server and client.

The following example is based on the `Index` component in the [SignalR with Blazor tutorial](../tutorials/signalr-blazor?view=aspnetcore-10.0). The server timeout is increased to 60 seconds, and the handshake timeout is increased to 30 seconds. The Keep-Alive interval isn't set and uses its default value of 15 seconds.

```
protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .WithServerTimeout(TimeSpan.FromSeconds(60))
        .Build();

    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}
```

Set the following:

* `serverTimeoutInMilliseconds` (default: 30 seconds): Configures the server timeout, specified in milliseconds, for the circuit's hub connection.
* `keepAliveIntervalInMilliseconds` (default: 15 seconds): The interval, specified in milliseconds, at which the connection sends Keep-Alive messages.

The server timeout can be increased, and the Keep-Alive interval can remain the same. The important consideration is that if you change the values, make sure that the server timeout is at least double the value of the Keep-Alive interval and that the Keep-Alive interval values match between server and client. For more information, see the [Configure SignalR timeouts and Keep-Alive on the client](#configure-signalr-timeouts-and-keep-alive-on-the-client) section.

In the following [startup configuration](startup?view=aspnetcore-10.0) example ([location of the Blazor script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script)), a custom value of 60 seconds is used for the server timeout. The Keep-Alive interval (`keepAliveIntervalInMilliseconds`) isn't set and uses its default value of 15 seconds.

In `Pages/_Host.cshtml`:

```
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      let c = builder.build();
      c.serverTimeoutInMilliseconds = 60000;
      builder.build = () => {
        return c;
      };
    }
  });
</script>
```

When creating a hub connection in a component, set the [ServerTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.servertimeout#microsoft-aspnetcore-signalr-client-hubconnection-servertimeout) (default: 30 seconds) and [HandshakeTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.handshaketimeout#microsoft-aspnetcore-signalr-client-hubconnection-handshaketimeout) (default: 15 seconds) on the built [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection). Confirm that the timeouts are at least double the Keep-Alive interval. Confirm that the Keep-Alive interval matches between server and client.

The following example is based on the `Index` component in the [SignalR with Blazor tutorial](../tutorials/signalr-blazor?view=aspnetcore-10.0). The [ServerTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.servertimeout#microsoft-aspnetcore-signalr-client-hubconnection-servertimeout) is increased to 60 seconds, and the [HandshakeTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.handshaketimeout#microsoft-aspnetcore-signalr-client-hubconnection-handshaketimeout) is increased to 30 seconds. The Keep-Alive interval ([KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.keepaliveinterval#microsoft-aspnetcore-signalr-client-hubconnection-keepaliveinterval)) isn't set and uses its default value of 15 seconds.

```
protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .Build();

    hubConnection.ServerTimeout = TimeSpan.FromSeconds(60);
    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}
```

## Modify the server-side reconnection handler

The reconnection handler's circuit connection events can be modified for custom behaviors, such as:

* To notify the user if the connection is dropped.
* To perform logging (from the client) when a circuit is connected.

To modify the connection events, register callbacks for the following connection changes:

* Dropped connections use `onConnectionDown`.
* Established/re-established connections use `onConnectionUp`.

**Both `onConnectionDown` and `onConnectionUp` must be specified.**

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      reconnectionHandler: {
        onConnectionDown: (options, error) => console.error(error),
        onConnectionUp: () => console.log("Up, up, and away!")
      }
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Blazor Server:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    reconnectionHandler: {
      onConnectionDown: (options, error) => console.error(error),
      onConnectionUp: () => console.log("Up, up, and away!")
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

## Programmatic control of reconnection and reload behavior

Blazor automatically attempts reconnection and refreshes the browser when reconnection fails. For more information, see the [Adjust the server-side reconnection retry count and interval](#adjust-the-server-side-reconnection-retry-count-and-interval) section. However, developer code can implement a custom reconnection handler to take full control of reconnection behavior.

The default reconnection behavior requires the user to take manual action to refresh the page after reconnection fails. However, developer code can implement a custom reconnection handler to take full control of reconnection behavior, including implementing automatic page refresh after reconnection attempts fail.

`App.razor`:

`Pages/_Host.cshtml`:

```
<div id="reconnect-modal" style="display: none;"></div>
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script src="boot.js"></script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Create the following `wwwroot/boot.js` file.

Blazor Web App:

```
(() => {
  const maximumRetryCount = 3;
  const retryIntervalMilliseconds = 5000;
  const reconnectModal = document.getElementById('reconnect-modal');

  const startReconnectionProcess = () => {
    reconnectModal.style.display = 'block';

    let isCanceled = false;

    (async () => {
      for (let i = 0; i < maximumRetryCount; i++) {
        reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`;

        await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));

        if (isCanceled) {
          return;
        }

        try {
          const result = await Blazor.reconnect();
          if (!result) {
            // The server was reached, but the connection was rejected; reload the page.
            location.reload();
            return;
          }

          // Successfully reconnected to the server.
          return;
        } catch {
          // Didn't reach the server; try again.
        }
      }

      // Retried too many times; reload the page.
      location.reload();
    })();

    return {
      cancel: () => {
        isCanceled = true;
        reconnectModal.style.display = 'none';
      },
    };
  };

  let currentReconnectionProcess = null;

  Blazor.start({
    circuit: {
      reconnectionHandler: {
        onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(),
        onConnectionUp: () => {
          currentReconnectionProcess?.cancel();
          currentReconnectionProcess = null;
        }
      }
    }
  });
})();
```

Blazor Server:

```
(() => {
  const maximumRetryCount = 3;
  const retryIntervalMilliseconds = 5000;
  const reconnectModal = document.getElementById('reconnect-modal');

  const startReconnectionProcess = () => {
    reconnectModal.style.display = 'block';

    let isCanceled = false;

    (async () => {
      for (let i = 0; i < maximumRetryCount; i++) {
        reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`;

        await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));

        if (isCanceled) {
          return;
        }

        try {
          const result = await Blazor.reconnect();
          if (!result) {
            // The server was reached, but the connection was rejected; reload the page.
            location.reload();
            return;
          }

          // Successfully reconnected to the server.
          return;
        } catch {
          // Didn't reach the server; try again.
        }
      }

      // Retried too many times; reload the page.
      location.reload();
    })();

    return {
      cancel: () => {
        isCanceled = true;
        reconnectModal.style.display = 'none';
      },
    };
  };

  let currentReconnectionProcess = null;

  Blazor.start({
    reconnectionHandler: {
      onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(),
      onConnectionUp: () => {
        currentReconnectionProcess?.cancel();
        currentReconnectionProcess = null;
      }
    }
  });
})();
```

For more information on Blazor startup, see [ASP.NET Core Blazor startup](startup?view=aspnetcore-10.0).

## Disconnect Blazor's SignalR circuit from the client

Blazor's SignalR circuit is disconnected when the [`unload` page event](https://developer.mozilla.org/docs/Web/API/Window/unload_event) is triggered. To disconnect the circuit for other scenarios on the client, invoke `Blazor.disconnect` in the appropriate event handler. In the following example, the circuit is disconnected when the page is hidden ([`pagehide` event](https://developer.mozilla.org/docs/Web/API/Window/pagehide_event)):

```
window.addEventListener('pagehide', () => {
  Blazor.disconnect();
});
```

For more information on Blazor startup, see [ASP.NET Core Blazor startup](startup?view=aspnetcore-10.0).

## Server-side circuit handler

You can define a *circuit handler*, which allows running code on changes to the state of a user's circuit. A circuit handler is implemented by deriving from [CircuitHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuithandler) and registering the class in the app's service container. The following example of a circuit handler tracks open SignalR connections.

`TrackingCircuitHandler.cs`:

```
using Microsoft.AspNetCore.Components.Server.Circuits;

public class TrackingCircuitHandler : CircuitHandler
{
    private HashSet<Circuit> circuits = new();

    public override Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        circuits.Add(circuit);

        return Task.CompletedTask;
    }

    public override Task OnConnectionDownAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        circuits.Remove(circuit);

        return Task.CompletedTask;
    }

    public int ConnectedCircuits => circuits.Count;
}
```

Circuit handlers are registered using DI. Scoped instances are created per instance of a circuit. Using the `TrackingCircuitHandler` in the preceding example, a singleton service is created because the state of all circuits must be tracked.

In the `Program` file:

```
builder.Services.AddSingleton<CircuitHandler, TrackingCircuitHandler>();
```

In `Startup.ConfigureServices` of `Startup.cs`:

```
services.AddSingleton<CircuitHandler, TrackingCircuitHandler>();
```

If a custom circuit handler's methods throw an unhandled exception, the exception is fatal to the circuit. To tolerate exceptions in a handler's code or called methods, wrap the code in one or more [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statements with error handling and logging.

When a circuit ends because a user has disconnected and the framework is cleaning up the circuit state, the framework disposes of the circuit's DI scope. Disposing the scope disposes any circuit-scoped DI services that implement [System.IDisposable](/en-us/dotnet/api/system.idisposable). If any DI service throws an unhandled exception during disposal, the framework logs the exception. For more information, see [ASP.NET Core Blazor dependency injection](dependency-injection?view=aspnetcore-10.0#service-lifetime).

## Server-side circuit handler to capture users for custom services

Use a [CircuitHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuithandler) to capture a user from the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) and set that user in a service. For more information and example code, see [ASP.NET Core Blazor additional server-side security scenarios](../security/additional-scenarios?view=aspnetcore-10.0#circuit-handler-to-capture-users-for-custom-services).

## Closure of circuits when there are no remaining Interactive Server components

Interactive Server components handle web UI events using a real-time connection with the browser called a circuit. A circuit and its associated state are created when a root Interactive Server component is rendered. The circuit is closed when there are no remaining Interactive Server components on the page, which frees up server resources.

## Start the SignalR circuit at a different URL

Prevent automatically starting the app by adding `autostart="false"` to the Blazor `<script>` tag ([location of the Blazor start script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script)). Manually establish the circuit URL using `Blazor.start`. The following examples use the path `/signalr`.

Blazor Web Apps:

```
- <script src="_framework/blazor.web.js"></script>
+ <script src="_framework/blazor.web.js" autostart="false"></script>
+ <script>
+   Blazor.start({
+     circuit: {
+       configureSignalR: builder => builder.withUrl("/signalr")
+     },
+   });
+ </script>
```

Blazor Server:

```
- <script src="_framework/blazor.server.js"></script>
+ <script src="_framework/blazor.server.js" autostart="false"></script>
+ <script>
+   Blazor.start({
+     configureSignalR: builder => builder.withUrl("/signalr")
+   });
+ </script>
```

Add the following [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) call with the hub path to the middleware processing pipeline in the server app's `Program` file.

Blazor Web Apps:

```
app.MapBlazorHub("/signalr");
```

Blazor Server:

Leave the existing call to [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) in the file and add a new call to [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub) with the path:

```
app.MapBlazorHub();
+ app.MapBlazorHub("/signalr");
```

## Impersonation for Windows Authentication

Authenticated hub connections ([HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection)) are created with [UseDefaultCredentials](/en-us/dotnet/api/microsoft.aspnetcore.http.connections.client.httpconnectionoptions.usedefaultcredentials) to indicate the use of default credentials for HTTP requests. For more information, see [SignalR authentication and authorization](../../signalr/authn-and-authz?view=aspnetcore-10.0#windows-authentication).

When the app is running in IIS Express as the signed-in user under Windows Authentication, which is likely the user's personal or work account, the default credentials are those of the signed-in user.

When the app is published to IIS, the app runs under the *Application Pool Identity*. The [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection) connects as the IIS "user" account hosting the app, not the user accessing the page.

Implement *impersonation* with the [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection) to use the identity of the browsing user.

In the following example:

* The user from the authentication state provider is cast to a [WindowsIdentity](/en-us/dotnet/api/system.security.principal.windowsidentity).
* The identity's access token is passed to [WindowsIdentity.RunImpersonatedAsync](/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonatedasync) with the code that builds and starts the [HubConnection](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection).

```
protected override async Task OnInitializedAsync()
{
    var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();

    if (authState?.User.Identity is not null)
    {
        var user = authState.User.Identity as WindowsIdentity;

        if (user is not null)
        {
            await WindowsIdentity.RunImpersonatedAsync(user.AccessToken, 
                async () =>
                {
                    hubConnection = new HubConnectionBuilder()
                        .WithUrl(NavManager.ToAbsoluteUri("/hub"), config =>
                        {
                            config.UseDefaultCredentials = true;
                        })
                        .WithAutomaticReconnect()
                        .Build();

                        hubConnection.On<string>("name", userName =>
                        {
                            name = userName;
                            InvokeAsync(StateHasChanged);
                        });

                        await hubConnection.StartAsync();
                });
        }
    }
}
```

In the preceding code, `NavManager` is a [NavigationManager](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager), and `AuthenticationStateProvider` is an [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) service instance ([`AuthenticationStateProvider` documentation](../security/authentication-state?view=aspnetcore-10.0)).

## Configure the hub URL endpoint for a loopback connection

*This section only applies to server-side Blazor apps.*

If HTTP requests in a server-side Blazor app are failing to connect to itself when using [NavigationManager.ToAbsoluteUri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.toabsoluteuri), you might have a load balancer or proxy that isn't expecting requests from the backend server. In this scenario, you can try to change the hub URL that the client is using to connect directly to the backend server.

The following example:

* Configures the hub URL using [UriBuilder](/en-us/dotnet/api/system.uribuilder) and passes it to [WithUrl](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilderhttpextensions.withurl).
* Sets the URI based on the server's address ([IServerAddressesFeature](/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.features.iserveraddressesfeature)) and machine name ([MachineName](/en-us/dotnet/api/system.environment.machinename)).

```
@using System.Net
@using System.Net.Sockets
@using Microsoft.AspNetCore.Hosting.Server
@using Microsoft.AspNetCore.Hosting.Server.Features
@using Microsoft.AspNetCore.SignalR.Client
@inject IHostEnvironment Environment
@inject IServer Server

...

@code {
    private HubConnection? hubConnection;

    protected override async Task OnInitializedAsync()
    {
        var serverAddress = Server.Features
           .Get<IServerAddressesFeature>()?
           .Addresses
           .FirstOrDefault(a => a.StartsWith("http://") || a.StartsWith("https://"));

        if (serverAddress is null)
        {
            throw new InvalidOperationException("No server address available.");
        }

        var uri = new UriBuilder(serverAddress + "/chathub");

        // If Kestrel is bound to a wildcard, substitute a real IP
        if (uri.Host is "0.0.0.0" or "[::]" or "+" or "*")
        {
            var addresses = await Dns.GetHostAddressesAsync(
                System.Environment.MachineName);
            var ip = addresses.FirstOrDefault(a =>
                a.AddressFamily == AddressFamily.InterNetwork
                && !IPAddress.IsLoopback(a));

            if (ip is null)
            {
                throw new InvalidOperationException("No suitable IP address.");
            }

            uri.Host = ip.ToString();
        }

        hubConnection = new HubConnectionBuilder()
            .WithUrl(uri.Uri)
            .Build();

        hubConnection.On<ChatMessage>("ReceiveMessage", (message) =>
        {
            ...
        });

        await hubConnection.StartAsync();
    }
}
```

## Additional server-side resources

* [Server-side host and deployment guidance: SignalR configuration](../host-and-deploy/server/?view=aspnetcore-10.0#signalr-configuration)

* [Overview of ASP.NET Core SignalR](../../signalr/introduction?view=aspnetcore-10.0)

* [ASP.NET Core SignalR configuration](../../signalr/configuration?view=aspnetcore-10.0)

* Server-side security documentation

  * [ASP.NET Core Blazor authentication and authorization](../security/?view=aspnetcore-10.0)
  * [ASP.NET Core Blazor authentication and authorization](../security/?view=aspnetcore-10.0)
  * [Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering](../security/interactive-server-side-rendering?view=aspnetcore-10.0)
  * [ASP.NET Core Blazor additional server-side security scenarios](../security/additional-scenarios?view=aspnetcore-10.0)

* [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../components/httpcontext?view=aspnetcore-10.0)

* [Server-side reconnection events and component lifecycle events](../components/lifecycle?view=aspnetcore-10.0#blazor-server-reconnection-events)

* [What is Azure SignalR Service?](/en-us/azure/azure-signalr/signalr-overview)

* [Performance guide for Azure SignalR Service](/en-us/azure/azure-signalr/signalr-concept-performance)

* [Publish an ASP.NET Core SignalR app to Azure App Service](../../signalr/publish-to-azure-web-app?view=aspnetcore-10.0)

* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](./?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/tutorials/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/tutorials/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor Hybrid tutorials

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The following tutorials provide a basic working experience for building a Blazor Hybrid app:

* [Build your first app](/en-us/dotnet/maui/get-started/first-app)

- [Build a .NET MAUI Blazor Hybrid app with a Blazor Web App](maui-blazor-web-app?view=aspnetcore-10.0)

* [Build a Windows Forms Blazor app](windows-forms?view=aspnetcore-10.0)

* [Build a Windows Presentation Foundation (WPF) Blazor app](wpf?view=aspnetcore-10.0)

For an overview of Blazor and reference articles, see [ASP.NET Core Blazor](../../?view=aspnetcore-10.0) and the articles that follow it in the table of contents.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/mvc1003
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/mvc1003.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# MVC1003: Route attributes cannot be applied to page models

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | MVC1003      |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

An attribute implementing [IRouteTemplateProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.iroutetemplateprovider) was applied to a Razor Page model.

### Rule description

Razor Page models are identified after routing is completed, and consequently cannot contribute a route. Applying a route attribute such as `Route` to a Razor Page model is not supported.

```
[Route("/my-page-route")]
public class IndexModel : PageModel
{
    public IActionResult OnGet() => Page();
}
```

## How to fix violations

Remove the route attribute from the page model. Routes can be specified for a Razor Page using an `@page` directive or by using conventions. For more information, see [custom routes in Razor Pages](../razor-pages/?view=aspnetcore-10.0#custom-routes).

## When to suppress warnings

Don't suppress warnings from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/prerender
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/prerender.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Prerender ASP.NET Core Razor components

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains Razor component prerendering scenarios for server-rendered components in Blazor Web Apps and Blazor Server apps.

*Prerendering* is the process of statically rendering page content from the server to deliver HTML to the browser as quickly as possible. After the prerendered content is quickly displayed to the user, interactive content with active event handlers are rendered, replacing any content that was rendered previously. Prerendering can also improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines use to calculate page rank.

Prerendering is enabled by default for interactive components.

Internal navigation with interactive routing doesn't use prerendering because the page is already interactive. For more information, see [Static versus interactive routing](../fundamentals/routing?view=aspnetcore-10.0#static-versus-interactive-routing) and [Interactive routing and prerendering](../state-management/prerendered-state-persistence?view=aspnetcore-10.0#interactive-routing-and-prerendering).

[`OnAfterRender{Async}` component lifecycle events](lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync) aren't called when prerendering, only after the component renders interactively.

## Disable prerendering

Prerendering can complicate an app because the app's Razor components must render twice: once for prerendering and once for setting up interactivity. If the components are set up to run on WebAssembly, then you also must design your components so that they can run from both the server and the client.

To disable prerendering for a *component instance*, pass the `prerender` flag with a value of `false` to the render mode:

* `<... @rendermode="new InteractiveServerRenderMode(prerender: false)" />`
* `<... @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />`
* `<... @rendermode="new InteractiveAutoRenderMode(prerender: false)" />`

To disable prerendering in a *component definition*:

* `@rendermode @(new InteractiveServerRenderMode(prerender: false))`
* `@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))`
* `@rendermode @(new InteractiveAutoRenderMode(prerender: false))`

To disable prerendering for the entire app, indicate the render mode at the highest-level interactive component in the app's component hierarchy that isn't a root component.

For apps based on the Blazor Web App project template, a render mode assigned to the entire app is specified where the `Routes` component is used in the `App` component (`Components/App.razor`). The following example sets the app's render mode to Interactive Server with prerendering disabled:

```
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
```

Also, disable prerendering for the [`HeadOutlet` component](control-head-content?view=aspnetcore-10.0#headoutlet-component) in the `App` component:

```
<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
```

Making a root component, such as the `App` component, interactive with the `@rendermode` directive at the top of the root component's definition file (`.razor`) isn't supported. Therefore, prerendering can't be disabled directly by the `App` component.

Disabling prerendering using the preceding techniques only takes effect for top-level render modes. If a parent component specifies a render mode, the prerendering settings of its children are ignored.

## Persist prerendered state

Without persisting prerendered state, state used during prerendering is lost and must be recreated when the app is fully loaded. If any state is created asynchronously, the UI may flicker as the prerendered UI is replaced when the component is rerendered. For guidance on how to persist state during prerendering, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0).

## Client-side services fail to resolve during prerendering

Assuming that prerendering isn't disabled for a component or for the app, a component in the `.Client` project is prerendered on the server. Because the server doesn't have access to registered client-side Blazor services, it isn't possible to inject these services into a component without receiving an error that the service can't be found during prerendering.

For example, consider the following `Home` component in the `.Client` project in a Blazor Web App with [global Interactive WebAssembly or Interactive Auto rendering](render-modes?view=aspnetcore-10.0#apply-a-render-mode-to-the-entire-app). The component attempts to inject [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) to obtain the environment's name.

```
@page "/"
@inject IWebAssemblyHostEnvironment Environment

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<p>
    Environment: @Environment.Environment
</p>
```

No compile time error occurs, but a runtime error occurs during prerendering:

> Cannot provide a value for property 'Environment' on type 'BlazorSample.Client.Pages.Home'. There is no registered service of type 'Microsoft.AspNetCore.Components.WebAssembly.Hosting.IWebAssemblyHostEnvironment'.

This error occurs because the component must compile and execute on the server during prerendering, but [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) isn't a registered service on the server.

Consider any of the following approaches to address this scenario:

* [Register the service on the server in addition to the client](#register-the-service-on-the-server-in-addition-to-the-client)
* [Inject a service that the app can use during prerendering](#inject-a-service-that-the-app-can-use-during-prerendering)
* [Make the service optional](#make-the-service-optional)
* [Create a service abstraction](#create-a-service-abstraction)
* [Disable prerendering for the component](#disable-prerendering-for-the-component)

### Register the service on the server in addition to the client

If the service supports server execution, register the service on the server in addition to the client so that it's available during prerendering. For an example of this scenario, see the guidance for [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) services in the [Blazor Web App external web APIs](../call-web-api?view=aspnetcore-10.0#blazor-web-app-external-web-apis) section of the *Call web API* article.

### Inject a service that the app can use during prerendering

In some cases, the app can use a service on the server during prerendering and a different service on the client.

For example, the following code obtains the app's environment whether the code is running on the server or on the client by injecting [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment) from the [`Microsoft.Extensions.Hosting.Abstractions` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Hosting.Abstractions):

```
private string? environmentName;

public Home(IHostEnvironment? serverEnvironment = null, 
    IWebAssemblyHostEnvironment? wasmEnvironment = null)
{
    environmentName = serverEnvironment?.EnvironmentName;
    environmentName ??= wasmEnvironment?.Environment;
}
```

However, this approach adds an additional dependency to the client project that isn't needed.

### Make the service optional

Make the service optional if it isn't required during prerendering using either of the following approaches.

The following example uses constructor injection of [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment):

```
private string? environmentName;

public Home(IWebAssemblyHostEnvironment? env = null)
{
    environmentName = env?.Environment;
}
```

Alternatively, inject [IServiceProvider](/en-us/dotnet/api/system.iserviceprovider) to optionally obtain the service if it's available:

```
@page "/"
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@inject IServiceProvider Services

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<p>
    <b>Environment:</b> @environmentName
</p>

@code {
    private string? environmentName;

    protected override void OnInitialized()
    {
        if (Services.GetService<IWebAssemblyHostEnvironment>() is { } env)
        {
            environmentName = env.Environment;
        }
    }
}
```

### Create a service abstraction

If a different service implementation is needed on the server, create a service abstraction and create implementations for the service in the server and client projects. Register the services in each project. Inject the custom service abstraction into components where needed. The component then depends solely on the custom service abstraction.

In the case of [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment), we can reuse the existing interface instead of creating a new one:

`ServerHostEnvironment.cs`:

```
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components;

public class ServerHostEnvironment(IWebHostEnvironment env, NavigationManager nav) : 
    IWebAssemblyHostEnvironment
{
    public string Environment => env.EnvironmentName;
    public string BaseAddress => nav.BaseUri;
}
```

In the server project's `Program` file, register the service:

```
builder.Services.TryAddScoped<IWebAssemblyHostEnvironment, ServerHostEnvironment>();
```

At this point, the [IWebAssemblyHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) service can be [injected into an interactive WebAssembly or Auto component that is also prerendered from the server](../fundamentals/environments?view=aspnetcore-10.0#read-the-environment-in-a-blazor-webassembly-app).

### Disable prerendering for the component

Disable prerendering for the component or for the entire app. For more information, see the [Disable prerendering](#disable-prerendering) section.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/azure-active-directory/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/azure-active-directory/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Microsoft Entra ID with ASP.NET Core

These tutorials and samples demonstrate authentication in ASP.NET Core using Microsoft identity platform and Microsoft Entra ID. For additional tutorials and samples using ASP.NET Core with Microsoft Entra ID, see [Microsoft identity platform](/en-us/azure/active-directory/develop/).

## Application Scenarios

* [Quickstart: Add sign-in with Microsoft to an ASP.NET Core web app](/en-us/azure/active-directory/develop/web-app-quickstart?pivots=devlang-aspnet-core\&tabs=windows)
* [Web app that signs in users](/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=aspnetcore)
* [Web app that calls web APIs](/en-us/azure/active-directory/develop/scenario-web-app-call-api-overview)
* [Protected web API](/en-us/azure/active-directory/develop/scenario-protected-web-api-overview)
* [Web API that calls other web APIs](/en-us/azure/active-directory/develop/scenario-web-api-call-api-overview)

## Samples

* [Enable your ASP.NET Core app to sign-in users and call web APIs using Microsoft identity platform for developers](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/)
* [Microsoft identity platform code samples](/en-us/entra/identity-platform/sample-v2-code): Microsoft Entra documentation's full code samples page, which covers common scenarios, including web apps and web APIs.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-03-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/deployment-layout
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/deployment-layout.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Deployment layout for ASP.NET Core hosted Blazor WebAssembly apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to enable hosted Blazor WebAssembly deployments in environments that block the download and execution of dynamic-link library (DLL) files.

Note

This guidance addresses environments that block clients from downloading and executing DLLs. In .NET 8 or later, Blazor uses the Webcil file format to address this problem. For more information, see [Host and deploy ASP.NET Core Blazor WebAssembly](./?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#webcil-packaging-format-for-net-assemblies). Multipart bundling using the experimental NuGet package described by this article isn't supported for Blazor apps in .NET 8 or later. You can use the guidance in this article to create your own multipart bundling NuGet package for .NET 8 or later.

Blazor WebAssembly apps require [dynamic-link libraries (DLLs)](/en-us/windows/win32/dlls/dynamic-link-libraries) to function, but some environments block clients from downloading and executing DLLs. Security products are often able to scan the content of files traversing the network and block or quarantine DLL files. This article describes one approach for enabling Blazor WebAssembly apps in these environments, where a multipart bundle file is created from the app's DLLs so that the DLLs can be downloaded together bypassing security restrictions.

Blazor WebAssembly apps require [dynamic-link libraries (DLLs)](/en-us/windows/win32/dlls/dynamic-link-libraries) to function, but some environments block clients from downloading and executing DLLs. In a subset of these environments, [changing the file name extension of DLL files (`.dll`)](./?view=aspnetcore-10.0#change-the-file-name-extension-of-dll-files) is sufficient to bypass security restrictions, but security products are often able to scan the content of files traversing the network and block or quarantine DLL files. This article describes one approach for enabling Blazor WebAssembly apps in these environments, where a multipart bundle file is created from the app's DLLs so that the DLLs can be downloaded together bypassing security restrictions.

A hosted Blazor WebAssembly app can customize its published files and packaging of app DLLs using the following features:

* [JavaScript initializers](../../javascript-interoperability/?view=aspnetcore-10.0#javascript-initializers) that allow customizing the Blazor boot process.
* MSBuild extensibility to transform the list of published files and define *Blazor Publish Extensions*. Blazor Publish Extensions are files defined during the publish process that provide an alternative representation for the set of files required to run a published Blazor WebAssembly app. In this article, a Blazor Publish Extension is created that produces a multipart bundle with all of the app's DLLs packed into a single file so that the DLLs can be downloaded together.

The approach demonstrated in this article serves as a starting point for developers to devise their own strategies and custom loading processes.

Warning

Any approach taken to circumvent a security restriction must be carefully considered for its security implications. We recommend exploring the subject further with your organization's network security professionals before adopting the approach in this article. Alternatives to consider include:

* Enable security appliances and security software to permit network clients to download and use the exact files required by a Blazor WebAssembly app.
* Switch from the Blazor WebAssembly hosting model to the [Blazor Server hosting model](../../hosting-models?view=aspnetcore-10.0#blazor-server), which maintains all of the app's C# code on the server and doesn't require downloading DLLs to clients. Blazor Server also offers the advantage of keeping C# code private without requiring the use of web API apps for C# code privacy with Blazor WebAssembly apps.

## Experimental NuGet package and sample app

The approach described in this article is used by the *experimental* [`Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle` package (NuGet.org)](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle) for apps targeting .NET 6 or later. The package contains MSBuild targets to customize the Blazor publish output and a [JavaScript initializer](../../javascript-interoperability/?view=aspnetcore-10.0#javascript-initializers) to use a custom [boot resource loader](../../fundamentals/startup?view=aspnetcore-10.0#load-boot-resources), each of which are described in detail later in this article.

Warning

Experimental and preview features are provided for the purpose of collecting feedback and aren't supported for production use.

Later in this article, the [Customize the Blazor WebAssembly loading process via a NuGet package](#customize-the-blazor-webassembly-loading-process-via-a-nuget-package) section with its three subsections provide detailed explanations on the configuration and code in the `Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle` package. The detailed explanations are important to understand when you create your own strategy and custom loading process for Blazor WebAssembly apps. To use the published, experimental, unsupported NuGet package without customization as a **local demonstration**, perform the following steps:

1. Use an existing hosted Blazor WebAssembly [solution](../../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) or create a new solution from the Blazor WebAssembly project template using Visual Studio or by passing the [`-ho|--hosted` option](/en-us/dotnet/core/tools/dotnet-new-sdk-templates#blazorwasm) to the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) command (`dotnet new blazorwasm -ho`). For more information, see [Tooling for ASP.NET Core Blazor](../../tooling?view=aspnetcore-10.0).

2. In the **Client** project, add the experimental `Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle` package.

   Note

   For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

3. In the **Server** project, add an endpoint for serving the bundle file (`app.bundle`). Example code can be found in the [Serve the bundle from the host server app](#serve-the-bundle-from-the-host-server-app) section of this article.

4. Publish the app in Release configuration.

## Customize the Blazor WebAssembly loading process via a NuGet package

Warning

The guidance in this section with its three subsections pertains to building a NuGet package from scratch to implement your own strategy and custom loading process. The *experimental* [`Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle` package (NuGet.org)](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle) for .NET 6 and 7 is based on the guidance in this section. When using the provided package in a **local demonstration** of the multipart bundle download approach, you don't need to follow the guidance in this section. For guidance on how to use the provided package, see the [Experimental NuGet package and sample app](#experimental-nuget-package-and-sample-app) section.

Blazor app resources are packed into a multipart bundle file and loaded by the browser via a custom [JavaScript (JS) initializer](../../javascript-interoperability/?view=aspnetcore-10.0#javascript-initializers). For an app consuming the package with the JS initializer, the app only requires that the bundle file is served when requested. All of the other aspects of this approach are handled transparently.

Four customizations are required to how a default published Blazor app loads:

* An MSBuild task to transform the publish files.
* A NuGet package with MSBuild targets that hooks into the Blazor publishing process, transforms the output, and defines one or more Blazor Publish Extension files (in this case, a single bundle).
* A JS initializer to update the Blazor WebAssembly resource loader callback so that it loads the bundle and provides the app with the individual files.
* A helper on the host **Server** app to ensure that the bundle is served to clients on request.

### Create an MSBuild task to customize the list of published files and define new extensions

Create an MSBuild task as a public C# class that can be imported as part of an MSBuild compilation and that can interact with the build.

The following are required for the C# class:

* A new class library project.

* A project target framework of `netstandard2.0`.

* References to MSBuild packages:

  * [`Microsoft.Build.Framework`](https://www.nuget.org/packages/Microsoft.Build.Framework)
  * [`Microsoft.Build.Utilities.Core`](https://www.nuget.org/packages/Microsoft.Build.Utilities.Core)

Note

The NuGet package for the examples in this article are named after the package provided by Microsoft, `Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle`. For guidance on naming and producing your own NuGet package, see the following NuGet articles:

* [Package authoring best practices](/en-us/nuget/create-packages/package-authoring-best-practices)
* [Package ID prefix reservation](/en-us/nuget/nuget-org/id-prefix-reservation)

`Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj`:

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Build.Framework" Version="{VERSION}" />
    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="{VERSION}" />
  </ItemGroup>

</Project>
```

Determine the latest package versions for the `{VERSION}` placeholders at NuGet.org:

* [`Microsoft.Build.Framework`](https://www.nuget.org/packages/Microsoft.Build.Framework)
* [`Microsoft.Build.Utilities.Core`](https://www.nuget.org/packages/Microsoft.Build.Utilities.Core)

To create the MSBuild task, create a public C# class extending [Microsoft.Build.Utilities.Task](/en-us/dotnet/api/microsoft.build.utilities.task) (not [System.Threading.Tasks.Task](/en-us/dotnet/api/system.threading.tasks.task)) and declare three properties:

* `PublishBlazorBootStaticWebAsset`: The list of files to publish for the Blazor app.
* `BundlePath`: The path where the bundle is written.
* `Extension`: The new Publish Extensions to include in the build.

The following example `BundleBlazorAssets` class is a starting point for further customization:

* In the `Execute` method, the bundle is created from the following three file types:

  * JavaScript files (`dotnet.js`)
  * WebAssembly (Wasm) files (`dotnet.wasm`)
  * App DLLs (`.dll`)

* A `multipart/form-data` bundle is created. Each file is added to the bundle with its respective descriptions via the [Content-Disposition header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Disposition) and the [Content-Type header](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Type).

* After the bundle is created, the bundle is written to a file.

* The build is configured for the extension. The following code creates an extension item and adds it to the `Extension` property. Each extension item contains three pieces of data:

  * The path to the extension file.
  * The URL path relative to the root of the Blazor WebAssembly app.
  * The name of the extension, which groups the files produced by a given extension.

After accomplishing the preceding goals, the MSBuild task is created for customizing the Blazor publish output. Blazor takes care of gathering the extensions and making sure that the extensions are copied to the correct location in the publish output folder (for example, `bin\Release\net6.0\publish`). The same optimizations (for example, compression) are applied to the JavaScript, Wasm, and DLL files as Blazor applies to other files.

`Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/BundleBlazorAssets.cs`:

```
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks
{
    public class BundleBlazorAssets : Task
    {
        [Required]
        public ITaskItem[]? PublishBlazorBootStaticWebAsset { get; set; }

        [Required]
        public string? BundlePath { get; set; }

        [Output]
        public ITaskItem[]? Extension { get; set; }

        public override bool Execute()
        {
            var bundle = new MultipartFormDataContent(
                "--0a7e8441d64b4bf89086b85e59523b7d");

            foreach (var asset in PublishBlazorBootStaticWebAsset)
            {
                var name = Path.GetFileName(asset.GetMetadata("RelativePath"));
                var fileContents = File.OpenRead(asset.ItemSpec);
                var content = new StreamContent(fileContents);
                var disposition = new ContentDispositionHeaderValue("form-data");
                disposition.Name = name;
                disposition.FileName = name;
                content.Headers.ContentDisposition = disposition;
                var contentType = Path.GetExtension(name) switch
                {
                    ".js" => "text/javascript",
                    ".wasm" => "application/wasm",
                    _ => "application/octet-stream"
                };
                content.Headers.ContentType = 
                    MediaTypeHeaderValue.Parse(contentType);
                bundle.Add(content);
            }

            using (var output = File.Open(BundlePath, FileMode.OpenOrCreate))
            {
                output.SetLength(0);
                bundle.CopyToAsync(output).ConfigureAwait(false).GetAwaiter()
                    .GetResult();
                output.Flush(true);
            }

            var bundleItem = new TaskItem(BundlePath);
            bundleItem.SetMetadata("RelativePath", "app.bundle");
            bundleItem.SetMetadata("ExtensionName", "multipart");

            Extension = new ITaskItem[] { bundleItem };

            return true;
        }
    }
}
```

### Author a NuGet package to automatically transform the publish output

Generate a NuGet package with MSBuild targets that are automatically included when the package is referenced:

* Create a new [Razor class library (RCL) project](../../components/class-libraries?view=aspnetcore-10.0).
* Create a targets file following NuGet conventions to automatically import the package in consuming projects. For example, create `build\net6.0\{PACKAGE ID}.targets`, where `{PACKAGE ID}` is the package identifier of the package.
* Collect the output from the class library containing the MSBuild task and confirm the output is packed in the right location.
* Add the necessary MSBuild code to attach to the Blazor pipeline and invoke the MSBuild task to generate the bundle.

The approach described in this section only uses the package to deliver targets and content, which is different from most packages where the package includes a library DLL.

Warning

The sample package described in this section demonstrates how to customize the Blazor publish process. **The sample NuGet package is for use as a local demonstration only. Using this package in production is not supported.**

Note

The NuGet package for the examples in this article are named after the package provided by Microsoft, `Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle`. For guidance on naming and producing your own NuGet package, see the following NuGet articles:

* [Package authoring best practices](/en-us/nuget/create-packages/package-authoring-best-practices)
* [Package ID prefix reservation](/en-us/nuget/nuget-org/id-prefix-reservation)

`Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.csproj`:

```
<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <NoWarn>NU5100</NoWarn>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <Description>
      Sample demonstration package showing how to customize the Blazor publish 
      process. Using this package in production is not supported!
    </Description>
    <IsPackable>true</IsPackable>
    <IsShipping>true</IsShipping>
    <IncludeBuildOutput>false</IncludeBuildOutput>
  </PropertyGroup>

  <ItemGroup>
    <None Update="build\**" 
          Pack="true" 
          PackagePath="%(Identity)" />
    <Content Include="_._" 
             Pack="true" 
             PackagePath="lib\net6.0\_._" />
  </ItemGroup>

  <Target Name="GetTasksOutputDlls" 
          BeforeTargets="CoreCompile">
    <MSBuild Projects="..\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj" 
             Targets="Publish;PublishItemsOutputGroup" 
             Properties="Configuration=Release">
      <Output TaskParameter="TargetOutputs" 
              ItemName="_TasksProjectOutputs" />
    </MSBuild>
    <ItemGroup>
      <Content Include="@(_TasksProjectOutputs)" 
               Condition="'%(_TasksProjectOutputs.Extension)' == '.dll'" 
               Pack="true" 
               PackagePath="tasks\%(_TasksProjectOutputs.TargetPath)" 
               KeepMetadata="Pack;PackagePath" />
    </ItemGroup>
  </Target>

</Project>
```

Note

The `<NoWarn>NU5100</NoWarn>` property in the preceding example suppresses the warning about the assemblies placed in the `tasks` folder. For more information, see [NuGet Warning NU5100](/en-us/nuget/reference/errors-and-warnings/nu5100).

Add a `.targets` file to wire up the MSBuild task to the build pipeline. In this file, the following goals are accomplished:

* Import the task into the build process. Note that the path to the DLL is relative to the ultimate location of the file in the package.
* The `ComputeBlazorExtensionsDependsOn` property attaches the custom target to the Blazor WebAssembly pipeline.
* Capture the `Extension` property on the task output and add it to `BlazorPublishExtension` to tell Blazor about the extension. Invoking the task in the target produces the bundle. The list of published files is provided by the Blazor WebAssembly pipeline in the `PublishBlazorBootStaticWebAsset` item group. The bundle path is defined using the `IntermediateOutputPath` (typically inside the `obj` folder). Ultimately, the bundle is copied automatically to the correct location in the publish output folder (for example, `bin\Release\net6.0\publish`).

When the package is referenced, it generates a bundle of the Blazor files during publish.

`Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/build/net6.0/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.targets`:

```
<Project>
  <UsingTask 
    TaskName="Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.BundleBlazorAssets" 
    AssemblyFile="$(MSBuildThisProjectFileDirectory)..\..\tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.dll" />

  <PropertyGroup>
    <ComputeBlazorExtensionsDependsOn>
      $(ComputeBlazorExtensionsDependsOn);_BundleBlazorDlls
    </ComputeBlazorExtensionsDependsOn>
  </PropertyGroup>

  <Target Name="_BundleBlazorDlls">
    <BundleBlazorAssets
      PublishBlazorBootStaticWebAsset="@(PublishBlazorBootStaticWebAsset)"
      BundlePath="$(IntermediateOutputPath)bundle.multipart">
      <Output TaskParameter="Extension" 
              ItemName="BlazorPublishExtension"/>
    </BundleBlazorAssets>
  </Target>

</Project>
```

### Automatically bootstrap Blazor from the bundle

The NuGet package leverages [JavaScript (JS) initializers](../../javascript-interoperability/?view=aspnetcore-10.0#javascript-initializers) to automatically bootstrap a Blazor WebAssembly app from the bundle instead of using individual DLL files. JS initializers are used to change the Blazor [boot resource loader](../../fundamentals/startup?view=aspnetcore-10.0#load-boot-resources) and use the bundle.

To create a JS initializer, add a JS file with the name `{NAME}.lib.module.js` to the `wwwroot` folder of the package project, where the `{NAME}` placeholder is the package identifier. For example, the file for the Microsoft package is named `Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js`. The exported functions `beforeWebAssemblyStart` and `afterWebAssemblyStarted` handle loading.

The JS initializers:

* Detect if the Publish Extension is available by checking for `extensions.multipart`, which is the extension name (`ExtensionName`) provided in the [Create an MSBuild task to customize the list of published files and define new extensions](#create-an-msbuild-task-to-customize-the-list-of-published-files-and-define-new-extensions) section.
* Download the bundle and parse the contents into a resources map using generated object URLs.
* Update the [boot resource loader (`options.loadBootResource`)](../../fundamentals/startup?view=aspnetcore-10.0#load-boot-resources) with a custom function that resolves the resources using the object URLs.
* After the app has started, revoke the object URLs to release memory in the `afterWebAssemblyStarted` function.

`Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js`:

```
const resources = new Map();

export async function beforeWebAssemblyStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterWebAssemblyStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}
```

To create a JS initializer, add a JS file with the name `{NAME}.lib.module.js` to the `wwwroot` folder of the package project, where the `{NAME}` placeholder is the package identifier. For example, the file for the Microsoft package is named `Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js`. The exported functions `beforeStart` and `afterStarted` handle loading.

The JS initializers:

* Detect if the Publish Extension is available by checking for `extensions.multipart`, which is the extension name (`ExtensionName`) provided in the [Create an MSBuild task to customize the list of published files and define new extensions](#create-an-msbuild-task-to-customize-the-list-of-published-files-and-define-new-extensions) section.
* Download the bundle and parse the contents into a resources map using generated object URLs.
* Update the [boot resource loader (`options.loadBootResource`)](../../fundamentals/startup?view=aspnetcore-10.0#load-boot-resources) with a custom function that resolves the resources using the object URLs.
* After the app has started, revoke the object URLs to release memory in the `afterStarted` function.

`Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js`:

```
const resources = new Map();

export async function beforeStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}
```

## Serve the bundle from the host server app

Due to security restrictions, ASP.NET Core doesn't serve the `app.bundle` file. A request processing helper is required to serve the file when it's requested by clients.

Note

Since the same optimizations are transparently applied to the Publish Extensions that are applied to the app's files, the `app.bundle.gz` and `app.bundle.br` compressed asset files are produced automatically on publish.

Place C# code in `Program.cs` of the **Server** project immediately before the line that sets the fallback file to `index.html` (`app.MapFallbackToFile("index.html");`) to respond to a request for the bundle file (for example, `app.bundle`):

```
app.MapGet("app.bundle", (HttpContext context) =>
{
    string? contentEncoding = null;
    var contentType = 
        "multipart/form-data; boundary=\"--0a7e8441d64b4bf89086b85e59523b7d\"";
    var fileName = "app.bundle";

    var acceptEncodings = context.Request.Headers.AcceptEncoding;

    if (Microsoft.Net.Http.Headers.StringWithQualityHeaderValue
        .StringWithQualityHeaderValue
        .TryParseList(acceptEncodings, out var encodings))
    {
        if (encodings.Any(e => e.Value == "br"))
        {
            contentEncoding = "br";
            fileName += ".br";
        }
        else if (encodings.Any(e => e.Value == "gzip"))
        {
            contentEncoding = "gzip";
            fileName += ".gz";
        }
    }

    if (contentEncoding != null)
    {
        context.Response.Headers.ContentEncoding = contentEncoding;
    }

    return Results.File(
        app.Environment.WebRootFileProvider.GetFileInfo(fileName)
            .CreateReadStream(), contentType);
});
```

The content type matches the type defined earlier in the build task. The endpoint checks for the content encodings accepted by the browser and serves the optimal file, Brotli (`.br`) or Gzip (`.gz`).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/timeouts
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/timeouts.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP Request Timeouts

## Introduction

.NET 8 introduced the [request timeouts middleware](/en-us/aspnet/core/performance/timeouts) to enable configuring request timeouts globally as well as per endpoint. This functionality is also available in YARP 2.1 when running on .NET 8 or newer.

## Defaults

Requests do not have any timeouts by default, other than the [ActivityTimeout](http-client-config?view=aspnetcore-10.0#HttpRequest) used to clean up idle requests. A default policy specified in [RequestTimeoutOptions](/en-us/dotnet/api/microsoft.aspnetcore.http.timeouts.requesttimeoutoptions) will apply to proxied requests as well.

## Interaction with ActivityTimeout

Both route-level `Timeout` and cluster-level `ActivityTimeout` apply to proxied requests. Understanding how they interact is important for configuring appropriate timeouts:

* **ActivityTimeout** is configured per cluster in the `HttpRequest` section and has a default value of **100 seconds**. This timeout resets whenever there's activity on the request. Activity includes receiving response headers or successfully reading or writing request, response, or streaming data (like gRPC or WebSockets). TCP keep-alives and HTTP/2 protocol pings do not reset the timeout, but WebSocket pings do.

* **Timeout** is configured per route and specifies the total time allowed for a request to complete.

### Which timeout takes precedence?

Both timeouts apply simultaneously. If a request is **idle** (no activity), the `ActivityTimeout` will cancel it. If a request is **active** but taking too long overall, the route-level `Timeout` will cancel it. In practice, for an idle request, whichever timeout is lower will take effect first.

For example:

* With a route `Timeout` of **300 seconds** and the default `ActivityTimeout` of **100 seconds**: if the request becomes idle, it will be canceled after 100 seconds due to `ActivityTimeout`, even though the route allows up to 300 seconds.
* With a route `Timeout` of **60 seconds** and the default `ActivityTimeout` of **100 seconds**: the request will be canceled after 60 seconds if it hasn't completed, regardless of activity.

### Debugging considerations

An important difference between these timeouts:

* **Route-level Timeout** does not apply when a debugger is attached to the process, making it easier to debug long-running requests.
* **ActivityTimeout** always applies, even when a debugger is attached.

To avoid `ActivityTimeout` interfering with debugging or legitimate long-running requests, configure it explicitly in the cluster's `HttpRequest` section:

```
{
  "Clusters": {
    "cluster1": {
      "HttpRequest": {
        "ActivityTimeout": "00:10:00"
      },
      "Destinations": {
        "cluster1/destination1": {
          "Address": "https://localhost:10001/"
        }
      }
    }
  }
}
```

## Configuration

Timeouts and Timeout Policies can be specified per route via [RouteConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.routeconfig) and can be bound from the `Routes` sections of the config file. As with other route properties, this can be modified and reloaded without restarting the proxy. Policy names are case insensitive.

Timeouts are specified in a TimeSpan format (HH:MM:SS). Specifying both a Timeout and a TimeoutPolicy on the same route is invalid and will cause the configuration to be rejected.

Note that request timeouts do not apply when a debugger is attached to the process.

Example:

```
{
  "ReverseProxy": {
    "Routes": {
      "route1" : {
        "ClusterId": "cluster1",
        "TimeoutPolicy": "customPolicy",
        "Match": {
          "Hosts": [ "localhost" ]
        }
      }
      "route2" : {
        "ClusterId": "cluster1",
        "Timeout": "00:01:00",
        "Match": {
          "Hosts": [ "localhost2" ]
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "cluster1/destination1": {
            "Address": "https://localhost:10001/"
          }
        }
      }
    }
  }
}
```

Timeout policies and the default policy can be configured in the service collection and the middleware can be added as follows:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

builder.Services.AddRequestTimeouts(options =>
{
    options.AddPolicy("customPolicy", TimeSpan.FromSeconds(20));
});

var app = builder.Build();

app.UseRequestTimeouts();

app.MapReverseProxy();

app.Run();
```

### Disable timeouts

Specifying the value `disable` in a route's `TimeoutPolicy` parameter means the request timeout middleware will not apply timeouts to this route.

### WebSockets

Request timeouts are disabled after the initial WebSocket handshake. However, `ActivityTimeout` does apply to WebSocket requests. WebSocket keep-alives can be enabled by either the client or server talking to the proxy to keep the connection from becoming idle and triggering the `ActivityTimeout`.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/configuration/overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure ASP.NET Core Data Protection

When the Data Protection system is initialized, it applies [default settings](default-settings?view=aspnetcore-10.0) based on the operational environment. These settings are appropriate for apps running on a single machine. However, there are cases where a developer may want to change the default settings:

* The app is spread across multiple machines.
* For compliance reasons.

For these scenarios, the Data Protection system offers a rich configuration API.

Warning

Similar to configuration files, the data protection key ring should be protected using appropriate permissions. You can choose to encrypt keys at rest, but this doesn't prevent cyberattackers from creating new keys. Consequently, your app's security is impacted. The storage location configured with Data Protection should have its access limited to the app itself, similar to the way you would protect configuration files. For example, if you choose to store your key ring on disk, use file system permissions. Ensure only the identity under which your web app runs has read, write, and create access to that directory. If you use Azure Blob Storage, only the web app should have the ability to read, write, or create new entries in the blob store, etc.

The extension method [AddDataProtection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.dataprotectionservicecollectionextensions.adddataprotection) returns an [IDataProtectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotectionbuilder). `IDataProtectionBuilder` exposes extension methods that you can chain together to configure Data Protection options.

Note

This article was written for an app that runs within a docker container. In a docker container the app always has the same path and, therefore, the same application discriminator. Apps that need to run in multiple environments (for example local and deployed), must set the default application discriminator for the environment. Running an app in multiple environments is beyond the scope of this article.

The following NuGet packages are required for the Data Protection extensions used in this article:

* [`Azure.Extensions.AspNetCore.DataProtection.Blobs`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Blobs)
* [`Azure.Extensions.AspNetCore.DataProtection.Keys`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Keys)

## Protect keys with Azure Key Vault (`ProtectKeysWithAzureKeyVault`)

To interact with [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) locally using developer credentials, either sign into your storage account in Visual Studio or sign in with the [Azure CLI](/en-us/cli/azure/). If you haven't already installed the Azure CLI, see [How to install the Azure CLI](/en-us/cli/azure/install-azure-cli). You can execute the following command in the Developer PowerShell panel in Visual Studio or from a command shell when not using Visual Studio:

```
az login
```

For more information, see [Sign-in to Azure using developer tooling](/en-us/dotnet/azure/sdk/authentication/local-development-dev-accounts#sign-in-to-azure-using-developer-tooling).

When [establishing the key vault in the Entra or Azure portal](/en-us/azure/key-vault/general/quick-create-portal):

* Configure the key vault to use Azure role-based access control (RABC). If you aren't operating on an [Azure Virtual Network](/en-us/azure/virtual-network/virtual-networks-overview), including for local development and testing, confirm that public access on the **Networking** step is **enabled** (checked). Enabling public access only exposes the key vault endpoint. Authenticated accounts are still required for access.

* Create an Azure Managed Identity (or add a role to the existing Managed Identity that you plan to use) with the **Key Vault Crypto User** role. Assign the Managed Identity to the Azure App Service that's hosting the deployment: **Settings** > **Identity** > **User assigned** > **Add**.

  Note

  If you also plan to run an app locally with an authorized user for blob access using the [Azure CLI](/en-us/cli/azure/) or Visual Studio's Azure Service Authentication, add your developer Azure user account in **Access Control (IAM)** with the **Key Vault Crypto User** role. If you want to use the Azure CLI through Visual Studio, execute the `az login` command from the Developer PowerShell panel and follow the prompts to authenticate with the tenant.

* When key encryption is active, keys in the key file include the comment, "This key is encrypted with Azure Key Vault." After starting the app, select the **View/edit** command from the context menu at the end of the key row to confirm that a key is present with key vault security applied.

* Optionally, you can enable automatic key vault key rotation without concern about decrypting payloads with data protection keys based on expired/rotated key vault keys. Each generated data protection key includes a reference to the key vault key used to encrypted it. Just make sure that you retain expired key vault keys, don't delete them in the key vault. Also, use a versionless key identifier in the app's key vault configuration, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`). Use a similar rotation period for both keys with the key vault key rotating more frequently than the data protection key to ensure that a new key vault key is used at the time of data protection key rotation.

Protecting keys with Azure Key Vault implements an [IXmlEncryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.xmlencryption.ixmlencryptor) that disables automatic data protection settings, including the key ring storage location. To configure the Azure Blob Storage provider to store the keys in blob storage, follow the guidance in [Key storage providers in ASP.NET Core](../implementation/key-storage-providers?view=aspnetcore-10.0#azure-storage) and call one of the [PersistKeysToAzureBlobStorage](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azuredataprotectionbuilderextensions.persistkeystoazureblobstorage) overloads in the app. The following example uses the overload that accepts a blob URI and token credential ([TokenCredential](/en-us/dotnet/api/azure.core.tokencredential)), relying on an Azure Managed Identity for role-based access control (RBAC).

To configure the Azure Key Vault provider, call one of the [ProtectKeysWithAzureKeyVault](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azuredataprotectionkeyvaultkeybuilderextensions.protectkeyswithazurekeyvault) overloads. The following example uses the overload that accepts key identifier and token credential ([TokenCredential](/en-us/dotnet/api/azure.core.tokencredential)), relying on a Managed Identity for RBAC in production ([ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential)) or a [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) during development and testing. Other overloads accept either a key vault client or an app client ID with client secret. For more information, see [Key storage providers in ASP.NET Core](../implementation/key-storage-providers?view=aspnetcore-10.0#azure-storage).

For more information on the Azure SDK's API and authentication, see [Authenticate .NET apps to Azure services using the Azure Identity library](/en-us/dotnet/azure/sdk/authentication/) and [Provide access to Key Vault keys, certificates, and secrets with Azure role-based access control](/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli). For logging guidance, see [Logging with the Azure SDK for .NET: Logging without client registration](/en-us/dotnet/azure/sdk/logging#logging-without-client-registration). For apps using dependency injection, an app can call [AddAzureClientsCore](/en-us/dotnet/api/microsoft.extensions.azure.azureclientservicecollectionextensions.addazureclientscore), passing `true` for `enableLogForwarding`, to create and wire up the logging infrastructure.

To create a key in the Azure portal, see [Quickstart: Set and retrieve a key from Azure Key Vault using the Azure portal](/en-us/azure/key-vault/keys/quick-create-portal). Give the key at least `Get`, `Unwrap Key`, and `Wrap Key` permissions. Record the key identifier for use with the app's configuration. If you plan to enable automatic rotation of the key vault key, record the *versionless* key identifier, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`).

In the `Program` file where services are registered:

```
TokenCredential? credential;

if (builder.Environment.IsProduction())
{
    credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
    // Local development and testing only
    DefaultAzureCredentialOptions options = new()
    {
        // Specify the tenant ID to use the dev credentials when running the app locally
        // in Visual Studio.
        VisualStudioTenantId = "{TENANT ID}",
        SharedTokenCacheTenantId = "{TENANT ID}"
    };

    credential = new DefaultAzureCredential(options);
}

builder.Services.AddDataProtection()
    .SetApplicationName("{APPLICATION NAME}")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential)
    .ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), credential);
```

`{MANAGED IDENTITY CLIENT ID}`: The Azure Managed Identity Client ID (GUID).

`{TENANT ID}`: Tenant ID.

`{APPLICATION NAME}`: [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the unique name of this app within the data protection system. The value should match across deployments of the app.

`{BLOB URI}`: Full URI to the key file. The URI is generated by Azure Storage when you create the key file. Do not use a SAS.

`{KEY IDENTIFIER}`: Azure Key Vault key identifier used for key encryption. An access policy allows the application to access the key vault with `Get`, `Unwrap Key`, and `Wrap Key` permissions. The version of the key is obtained from the key in the Entra or Azure portal after it's created. If you enable automatic rotation of the key vault key, make sure that you use a *versionless* key identifier in the app's key vault configuration, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`).

For an app to communicate and authorize itself with Azure Key Vault, the [`Azure.Identity` NuGet package](https://www.nuget.org/packages/Azure.Identity/) must be referenced by the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Note

In non-`Production` environments, the preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

If the app uses the older Azure packages (`Microsoft.AspNetCore.DataProtection.AzureStorage` and `Microsoft.AspNetCore.DataProtection.AzureKeyVault`), we recommend ***removing*** these references and upgrading to the [`Azure.Extensions.AspNetCore.DataProtection.Blobs`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Blobs) and [`Azure.Extensions.AspNetCore.DataProtection.Keys`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Keys) packages. The newer packages address key security and stability issues.

**Alternative shared-access signature (SAS) approach**: As an alternative to using a Managed Identity for access to the key blob in Azure Blob Storage, you can call the [PersistKeysToAzureBlobStorage](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azuredataprotectionbuilderextensions.persistkeystoazureblobstorage) overload that accepts a blob URI with a SAS token. The following example continues to use either a [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential) (production) or [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) (development and testing) for its [TokenCredential](/en-us/dotnet/api/azure.core.tokencredential), as seen in the preceding example:

```
builder.Services.AddDataProtection()
    .SetApplicationName("{APPLICATION NAME}")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI WITH SAS}"))
    .ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), credential);
```

`{APPLICATION NAME}`: [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the unique name of this app within the data protection system. The value should match across deployments of the app.

`{BLOB URI WITH SAS}`: The full URI where the key file should be stored with the SAS token as a query string parameter. The URI is generated by Azure Storage when you request a SAS for the uploaded key file.

`{KEY IDENTIFIER}`: Azure Key Vault key identifier used for key encryption. An access policy allows the application to access the key vault with `Get`, `Unwrap Key`, and `Wrap Key` permissions. The version of the key is obtained from the key in the Entra or Azure portal after it's created. If you enable automatic rotation of the key vault key, make sure that you use a versionless key identifier in the app's key vault configuration, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`).

## Persist keys to the file system (`PersistKeysToFileSystem`)

To store keys on a UNC share instead of at the *%LOCALAPPDATA%* default location, configure the system with [PersistKeysToFileSystem](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.persistkeystofilesystem):

```
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"));
```

Warning

If you change the key persistence location, the system no longer automatically encrypts keys at rest, since it doesn't know whether DPAPI is an appropriate encryption mechanism.

## Persist keys in a database (`PersistKeysToDbContext`)

To store keys in a database using EntityFramework, configure the system with the [Microsoft.AspNetCore.DataProtection.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/) package:

```
builder.Services.AddDataProtection()
    .PersistKeysToDbContext<SampleDbContext>();
```

The preceding code stores the keys in the configured database. The database context being used must implement `IDataProtectionKeyContext`. `IDataProtectionKeyContext` exposes the property `DataProtectionKeys`

```
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } = null!;
```

This property represents the table in which the keys are stored. Create the table manually or with `DbContext` Migrations. For more information, see [DataProtectionKey](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.entityframeworkcore.dataprotectionkey).

## Protect keys configuration API (`ProtectKeysWith\*`)

You can configure the system to protect keys at rest by calling any of the [`ProtectKeysWith\*`](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions) configuration APIs. Consider the example below, which stores keys on a UNC share and encrypts those keys at rest with a specific X.509 certificate.

You can provide an [X509Certificate2](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2) to [ProtectKeysWithCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate) from a file by calling [X509CertificateLoader.LoadCertificateFromFile](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificateloader.loadcertificatefromfile):

```
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
    .ProtectKeysWithCertificate(
        new X509Certificate2("certificate.pfx", builder.Configuration["CertificatePassword"]));
```

The following code example demonstrates how to load a certificate using a thumbprint:

```
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
    .ProtectKeysWithCertificate(builder.Configuration["CertificateThumbprint"]);
```

You can provide an [X509Certificate2](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2) to [ProtectKeysWithCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate), such as a certificate loaded from a file:

```
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
    .ProtectKeysWithCertificate(
        new X509Certificate2("certificate.pfx", builder.Configuration["CertificatePassword"]));
```

The following code example demonstrates how to load a certificate using a thumbprint:

```
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
    .ProtectKeysWithCertificate(builder.Configuration["CertificateThumbprint"]);
```

For examples and discussion on the built-in key encryption mechanisms, see [Key encryption at rest in Windows and Azure using ASP.NET Core](../implementation/key-encryption-at-rest?view=aspnetcore-10.0).

## Unprotect keys with any certificate (`UnprotectKeysWithAnyCertificate`)

You can rotate certificates and decrypt keys at rest using an array of [X509Certificate2](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2) certificates with [UnprotectKeysWithAnyCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.unprotectkeyswithanycertificate):

```
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
    .ProtectKeysWithCertificate(
        new X509Certificate2("certificate.pfx", builder.Configuration["CertificatePassword"]))
    .UnprotectKeysWithAnyCertificate(
        new X509Certificate2("certificate_1.pfx", builder.Configuration["CertificatePassword_1"]),
        new X509Certificate2("certificate_2.pfx", builder.Configuration["CertificatePassword_2"]));
```

## Set the default key lifetime (`SetDefaultKeyLifetime`)

To configure the system to use a key lifetime of 14 days instead of the default 90 days, use [SetDefaultKeyLifetime](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setdefaultkeylifetime):

```
builder.Services.AddDataProtection()
    .SetDefaultKeyLifetime(TimeSpan.FromDays(14));
```

## Set the application name (`SetApplicationName`)

By default, the Data Protection system isolates apps from one another based on their [content root](../../../fundamentals/?view=aspnetcore-10.0#content-root) paths, even if they share the same physical key repository. This isolation prevents the apps from understanding each other's protected payloads.

To share protected payloads among apps:

* Configure [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) in each app with the same value.

* Use the same version of the Data Protection API stack across the apps. Perform **either** of the following in the apps' project files:

  * Reference the same shared framework version via the [Microsoft.AspNetCore.App metapackage](../../../fundamentals/metapackage-app?view=aspnetcore-10.0).
  * Reference the same [Data Protection package](../introduction?view=aspnetcore-10.0#package-layout) version.

```
builder.Services.AddDataProtection()
    .SetApplicationName("<sharedApplicationName>");
```

[SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) internally sets [DataProtectionOptions.ApplicationDiscriminator](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionoptions.applicationdiscriminator#microsoft-aspnetcore-dataprotection-dataprotectionoptions-applicationdiscriminator). For troubleshooting purposes, the value assigned to the discriminator by the framework can be logged with the following code placed after the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) is built in `Program.cs`:

```
var discriminator = app.Services.GetRequiredService<IOptions<DataProtectionOptions>>()
    .Value.ApplicationDiscriminator;
app.Logger.LogInformation("ApplicationDiscriminator: {ApplicationDiscriminator}", discriminator);
```

For more information on how the discriminator is used, see the following sections later in this article:

* [Per-application isolation](#per-application-isolation)
* [Data Protection and app isolation](#data-protection-and-app-isolation)

Warning

In .NET 6, [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) normalizes the content root path to end with a [DirectorySeparatorChar](/en-us/dotnet/api/system.io.path.directoryseparatorchar#system-io-path-directoryseparatorchar). For example, on Windows the content root path ends in `\` and on Linux `/`. Other hosts don't normalize the path. Most apps migrating from [HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder) or [WebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder) won't share the same app name because they won't have the terminating `DirectorySeparatorChar`. To work around this issue, remove the directory separator character and set the app name manually, as shown in the following code:

```
using System.Reflection;
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

var trimmedContentRootPath = 
    builder.Environment.ContentRootPath.TrimEnd(Path.DirectorySeparatorChar);

builder.Services.AddDataProtection().SetApplicationName(trimmedContentRootPath);

var app = builder.Build();

app.MapGet("/", () => Assembly.GetEntryAssembly()!.GetName().Name);

app.Run();
```

## Disable automatic key generation (`DisableAutomaticKeyGeneration`)

You may have a scenario where you don't want an app to automatically roll keys (create new keys) as they approach expiration. One example of this scenario might be apps set up in a primary/secondary relationship, where only the primary app is responsible for key management concerns and secondary apps simply have a read-only view of the key ring. The secondary apps can be configured to treat the key ring as read-only by configuring the system with [DisableAutomaticKeyGeneration](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.disableautomatickeygeneration):

```
builder.Services.AddDataProtection()
    .DisableAutomaticKeyGeneration();
```

## Per-application isolation

When the Data Protection system is provided by an ASP.NET Core host, it automatically isolates apps from one another, even if those apps are running under the same worker process account and are using the same master keying material. This is similar to the IsolateApps modifier from System.Web's `<machineKey>` element.

The isolation mechanism works by considering each app on the local machine as a unique tenant, thus the [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector) rooted for any given app automatically includes the app ID as a discriminator ([ApplicationDiscriminator](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionoptions.applicationdiscriminator#microsoft-aspnetcore-dataprotection-dataprotectionoptions-applicationdiscriminator)). The app's unique ID is the app's physical path:

* For apps hosted in IIS, the unique ID is the IIS physical path of the app. If an app is deployed in a web farm environment, this value is stable assuming that the IIS environments are configured similarly across all machines in the web farm.
* For self-hosted apps running on the [Kestrel server](../../../fundamentals/servers/?view=aspnetcore-10.0#kestrel), the unique ID is the physical path to the app on disk.

The unique identifier is designed to survive resets—both of the individual app and of the machine itself.

This isolation mechanism assumes that the apps aren't malicious. A malicious app can always impact any other app running under the same worker process account. In a shared hosting environment where apps are mutually untrusted, the hosting provider should take steps to ensure OS-level isolation between apps, including separating the apps' underlying key repositories.

If the Data Protection system isn't provided by an ASP.NET Core host (for example, if you instantiate it via the `DataProtectionProvider` concrete type) app isolation is disabled by default. When app isolation is disabled, all apps backed by the same keying material can share payloads as long as they provide the appropriate [purposes](../consumer-apis/purpose-strings?view=aspnetcore-10.0). To provide app isolation in this environment, call the [`SetApplicationName`](#set-the-application-name-setapplicationname) method on the configuration object and provide a unique name for each app.

### Data Protection and app isolation

Consider the following points for app isolation:

* When multiple apps are pointed at the same key repository, the intention is that the apps share the same master key material. Data Protection is developed with the assumption that all apps sharing a key ring can access all items in that key ring. The application unique identifier is used to isolate application specific keys derived from the key ring provided keys. It doesn't expect item level permissions, such as those provided by Azure KeyVault to be used to enforce extra isolation. Attempting item level permissions generates application errors. If you don't want to rely on the built-in application isolation, separate key store locations should be used and not shared between applications.

* The application discriminator ([ApplicationDiscriminator](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionoptions.applicationdiscriminator#microsoft-aspnetcore-dataprotection-dataprotectionoptions-applicationdiscriminator)) is used to allow different apps to share the same master key material but to keep their cryptographic payloads distinct from one another. For the apps to be able to read each other's cryptographic payloads, they must have the same application discriminator, which can be set by calling [`SetApplicationName`](#set-the-application-name-setapplicationname).

* If an app is compromised (for example, by an RCE attack), all master key material accessible to that app must also be considered compromised, regardless of its protection-at-rest state. This implies that if two apps are pointed at the same repository, even if they use different app discriminators, a compromise of one is functionally equivalent to a compromise of both.

  This "functionally equivalent to a compromise of both" clause holds even if the two apps use different mechanisms for key protection at rest. Typically, this isn't an expected configuration. The protection-at-rest mechanism is intended to provide protection in the event a cyberattacker gains read access to the repository. A cyberattacker who gains write access to the repository (perhaps because they attained code execution permission within an app) can insert malicious keys into storage. The Data Protection system intentionally doesn't provide protection against a cyberattacker who gains write access to the key repository.

* If apps need to remain truly isolated from one another, they should use different key repositories. This naturally falls out of the definition of "isolated". Apps are ***not*** isolated if they all have Read and Write access to each other's data stores.

## Changing algorithms with `UseCryptographicAlgorithms`

The Data Protection stack allows you to change the default algorithm used by newly generated keys. The simplest way to do this is to call [UseCryptographicAlgorithms](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.usecryptographicalgorithms) from the configuration callback:

```
builder.Services.AddDataProtection()
    .UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
    {
        EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
        ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
    });
```

The default EncryptionAlgorithm is AES-256-CBC, and the default ValidationAlgorithm is HMACSHA256. The default policy can be set by a system administrator via a [machine-wide policy](machine-wide-policy?view=aspnetcore-10.0), but an explicit call to `UseCryptographicAlgorithms` overrides the default policy.

Calling `UseCryptographicAlgorithms` allows you to specify the desired algorithm from a predefined built-in list. You don't need to worry about the implementation of the algorithm. In the scenario above, the Data Protection system attempts to use the CNG implementation of AES if running on Windows. Otherwise, it falls back to the managed [System.Security.Cryptography.Aes](/en-us/dotnet/api/system.security.cryptography.aes) class.

You can manually specify an implementation via a call to [UseCustomCryptographicAlgorithms](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.usecustomcryptographicalgorithms).

Tip

Changing algorithms doesn't affect existing keys in the key ring. It only affects newly-generated keys.

### Specifying custom managed algorithms

To specify custom managed algorithms, create a [ManagedAuthenticatedEncryptorConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.configurationmodel.managedauthenticatedencryptorconfiguration) instance that points to the implementation types:

```
builder.Services.AddDataProtection()
    .UseCustomCryptographicAlgorithms(new ManagedAuthenticatedEncryptorConfiguration
    {
        // A type that subclasses SymmetricAlgorithm
        EncryptionAlgorithmType = typeof(Aes),

        // Specified in bits
        EncryptionAlgorithmKeySize = 256,

        // A type that subclasses KeyedHashAlgorithm
        ValidationAlgorithmType = typeof(HMACSHA256)
    });
```

Generally the \*Type properties must point to concrete, instantiable (via a public parameterless ctor) implementations of [SymmetricAlgorithm](/en-us/dotnet/api/system.security.cryptography.symmetricalgorithm) and [KeyedHashAlgorithm](/en-us/dotnet/api/system.security.cryptography.keyedhashalgorithm), though the system special-cases some values like `typeof(Aes)` for convenience.

Note

The SymmetricAlgorithm must have a key length of ≥ 128 bits and a block size of ≥ 64 bits, and it must support CBC-mode encryption with PKCS #7 padding. The KeyedHashAlgorithm must have a digest size of >= 128 bits, and it must support keys of length equal to the hash algorithm's digest length. The KeyedHashAlgorithm isn't strictly required to be HMAC.

### Specifying custom Windows CNG algorithms

To specify a custom Windows CNG algorithm using CBC-mode encryption with HMAC validation, create a [CngCbcAuthenticatedEncryptorConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.configurationmodel.cngcbcauthenticatedencryptorconfiguration) instance that contains the algorithmic information:

```
builder.Services.AddDataProtection()
    .UseCustomCryptographicAlgorithms(new CngCbcAuthenticatedEncryptorConfiguration
    {
        // Passed to BCryptOpenAlgorithmProvider
        EncryptionAlgorithm = "AES",
        EncryptionAlgorithmProvider = null,

        // Specified in bits
        EncryptionAlgorithmKeySize = 256,

        // Passed to BCryptOpenAlgorithmProvider
        HashAlgorithm = "SHA256",
        HashAlgorithmProvider = null
    });
```

Note

The symmetric block cipher algorithm must have a key length of >= 128 bits, a block size of >= 64 bits, and it must support CBC-mode encryption with PKCS #7 padding. The hash algorithm must have a digest size of >= 128 bits and must support being opened with the BCRYPT\_ALG\_HANDLE\_HMAC\_FLAG flag. The \*Provider properties can be set to null to use the default provider for the specified algorithm. For more information, see the [BCryptOpenAlgorithmProvider](/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptopenalgorithmprovider) documentation.

To specify a custom Windows CNG algorithm using Galois/Counter Mode encryption with validation, create a [CngGcmAuthenticatedEncryptorConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.configurationmodel.cnggcmauthenticatedencryptorconfiguration) instance that contains the algorithmic information:

```
builder.Services.AddDataProtection()
    .UseCustomCryptographicAlgorithms(new CngGcmAuthenticatedEncryptorConfiguration
    {
        // Passed to BCryptOpenAlgorithmProvider
        EncryptionAlgorithm = "AES",
        EncryptionAlgorithmProvider = null,

        // Specified in bits
        EncryptionAlgorithmKeySize = 256
    });
```

Note

The symmetric block cipher algorithm must have a key length of >= 128 bits, a block size of exactly 128 bits, and it must support GCM encryption. You can set the [EncryptionAlgorithmProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.configurationmodel.cngcbcauthenticatedencryptorconfiguration.encryptionalgorithmprovider#microsoft-aspnetcore-dataprotection-authenticatedencryption-configurationmodel-cngcbcauthenticatedencryptorconfiguration-encryptionalgorithmprovider) property to null to use the default provider for the specified algorithm. For more information, see the [BCryptOpenAlgorithmProvider](/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptopenalgorithmprovider) documentation.

### Specifying other custom algorithms

Though not exposed as a first-class API, the Data Protection system is extensible enough to allow specifying almost any kind of algorithm. For example, it's possible to keep all keys contained within a Hardware Security Module (HSM) and to provide a custom implementation of the core encryption and decryption routines. For more information, see [IAuthenticatedEncryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.iauthenticatedencryptor) in [Core cryptography extensibility](../extensibility/core-crypto?view=aspnetcore-10.0).

## Persisting keys when hosting in a Docker container

When hosting in a [Docker](/en-us/dotnet/standard/microservices-architecture/container-docker-introduction/) container, keys should be maintained in either:

* A folder that's a Docker volume that persists beyond the container's lifetime, such as a shared volume or a host-mounted volume.
* An external provider, such as [Azure Blob Storage](/en-us/azure/storage/blobs/storage-blobs-introduction) (shown in the [`ProtectKeysWithAzureKeyVault`](#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault) section) or [Redis](https://redis.io).

## Persisting keys with Redis

Only Redis versions supporting [Redis Data Persistence](/en-us/azure/azure-cache-for-redis/cache-how-to-premium-persistence) should be used to store keys. [Azure Blob storage](/en-us/azure/storage/blobs/storage-blobs-introduction) is persistent and can be used to store keys. For more information, see [this GitHub issue](https://github.com/dotnet/AspNetCore/issues/13476).

## Logging

Enable the `Information` or lower level of logging to diagnose problems. The following `appsettings.json` file enables information logging of the Data Protection API:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.AspNetCore.DataProtection": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

For more information on logging, see [Logging in .NET and ASP.NET Core](../../../fundamentals/logging/?view=aspnetcore-10.0).

## Additional resources

* [Non-DI aware scenarios for Data Protection in ASP.NET Core](non-di-scenarios?view=aspnetcore-10.0)
* [Data Protection machine-wide policy support in ASP.NET Core](machine-wide-policy?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../../host-and-deploy/web-farm?view=aspnetcore-10.0)
* [Key storage providers in ASP.NET Core](../implementation/key-storage-providers?view=aspnetcore-10.0)

When the Data Protection system is initialized, it applies [default settings](default-settings?view=aspnetcore-10.0) based on the operational environment. These settings are appropriate for apps running on a single machine. However, there are cases where a developer may want to change the default settings:

* The app is spread across multiple machines.
* For compliance reasons.

For these scenarios, the Data Protection system offers a rich configuration API.

Warning

Similar to configuration files, the data protection key ring should be protected using appropriate permissions. You can choose to encrypt keys at rest, but this doesn't prevent cyberattackers from creating new keys. Consequently, your app's security is impacted. The storage location configured with Data Protection should have its access limited to the app itself, similar to the way you would protect configuration files. For example, if you choose to store your key ring on disk, use file system permissions. Ensure only the identity under which your web app runs has read, write, and create access to that directory. If you use Azure Blob Storage, only the web app should have the ability to read, write, or create new entries in the blob store.

The extension method [AddDataProtection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.dataprotectionservicecollectionextensions.adddataprotection) returns an [IDataProtectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotectionbuilder), which exposes extension methods that you can chain together to configure Data Protection options.

The following NuGet packages are required for the Data Protection extensions used in this article:

* [`Azure.Extensions.AspNetCore.DataProtection.Blobs`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Blobs)
* [`Azure.Extensions.AspNetCore.DataProtection.Keys`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Keys)

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

## Protect keys with Azure Key Vault (`ProtectKeysWithAzureKeyVault`)

To interact with [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) locally using developer credentials, either sign into your storage account in Visual Studio or sign in with the [Azure CLI](/en-us/cli/azure/). If you haven't already installed the Azure CLI, see [How to install the Azure CLI](/en-us/cli/azure/install-azure-cli). You can execute the following command in the Developer PowerShell panel in Visual Studio or from a command shell when not using Visual Studio:

```
az login
```

For more information, see [Sign-in to Azure using developer tooling](/en-us/dotnet/azure/sdk/authentication/local-development-dev-accounts#sign-in-to-azure-using-developer-tooling).

When [establishing the key vault in the Entra or Azure portal](/en-us/azure/key-vault/general/quick-create-portal):

* Configure the key vault to use Azure role-based access control (RABC). If you aren't operating on an [Azure Virtual Network](/en-us/azure/virtual-network/virtual-networks-overview), including for local development and testing, confirm that public access on the **Networking** step is **enabled** (checked). Enabling public access only exposes the key vault endpoint. Authenticated accounts are still required for access.

* Create an Azure Managed Identity (or add a role to the existing Managed Identity that you plan to use) with the **Key Vault Crypto User** role. Assign the Managed Identity to the Azure App Service that's hosting the deployment: **Settings** > **Identity** > **User assigned** > **Add**.

  Note

  If you also plan to run an app locally with an authorized user for blob access using the [Azure CLI](/en-us/cli/azure/) or Visual Studio's Azure Service Authentication, add your developer Azure user account in **Access Control (IAM)** with the **Key Vault Crypto User** role. If you want to use the Azure CLI through Visual Studio, execute the `az login` command from the Developer PowerShell panel and follow the prompts to authenticate with the tenant.

* When key encryption is active, keys in the key file include the comment, "This key is encrypted with Azure Key Vault." After starting the app, select the **View/edit** command from the context menu at the end of the key row to confirm that a key is present with key vault security applied.

* Optionally, you can enable automatic key vault key rotation without concern about decrypting payloads with data protection keys based on expired/rotated key vault keys. Each generated data protection key includes a reference to the key vault key used to encrypted it. Just make sure that you retain expired key vault keys, don't delete them in the key vault. Also, use a versionless key identifier in the app's key vault configuration, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`). Use a similar rotation period for both keys with the key vault key rotating more frequently than the data protection key to ensure that a new key vault key is used at the time of data protection key rotation.

Protecting keys with Azure Key Vault implements an [IXmlEncryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.xmlencryption.ixmlencryptor) that disables automatic data protection settings, including the key ring storage location. To configure the Azure Blob Storage provider to store the keys in blob storage, follow the guidance in [Key storage providers in ASP.NET Core](../implementation/key-storage-providers?view=aspnetcore-10.0#azure-storage) and call one of the [PersistKeysToAzureBlobStorage](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azuredataprotectionbuilderextensions.persistkeystoazureblobstorage) overloads in the app. The following example uses the overload that accepts a blob URI and token credential ([TokenCredential](/en-us/dotnet/api/azure.core.tokencredential)), relying on an Azure Managed Identity for role-based access control (RBAC).

To configure the Azure Key Vault provider, call one of the [ProtectKeysWithAzureKeyVault](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azuredataprotectionkeyvaultkeybuilderextensions.protectkeyswithazurekeyvault) overloads. The following example uses the overload that accepts key identifier and token credential ([TokenCredential](/en-us/dotnet/api/azure.core.tokencredential)), relying on a Managed Identity for RBAC in production ([ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential)) or a [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) during development and testing. Other overloads accept either a key vault client or an app client ID with client secret. For more information, see [Key storage providers in ASP.NET Core](../implementation/key-storage-providers?view=aspnetcore-10.0#azure-storage).

For more information on the Azure SDK's API and authentication, see [Authenticate .NET apps to Azure services using the Azure Identity library](/en-us/dotnet/azure/sdk/authentication/) and [Provide access to Key Vault keys, certificates, and secrets with Azure role-based access control](/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli). For logging guidance, see [Logging with the Azure SDK for .NET: Logging without client registration](/en-us/dotnet/azure/sdk/logging#logging-without-client-registration). For apps using dependency injection, an app can call [AddAzureClientsCore](/en-us/dotnet/api/microsoft.extensions.azure.azureclientservicecollectionextensions.addazureclientscore), passing `true` for `enableLogForwarding`, to create and wire up the logging infrastructure.

To create a key in the Azure portal, see [Quickstart: Set and retrieve a key from Azure Key Vault using the Azure portal](/en-us/azure/key-vault/keys/quick-create-portal). Give the key at least `Get`, `Unwrap Key`, and `Wrap Key` permissions. Record the key identifier for use with the app's configuration. If you plan to enable automatic rotation of the key vault key, record the *versionless* key identifier, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`).

In the `Program` file where services are registered:

```
TokenCredential? credential;

if (builder.Environment.IsProduction())
{
    credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
    // Local development and testing only
    DefaultAzureCredentialOptions options = new()
    {
        // Specify the tenant ID to use the dev credentials when running the app locally
        // in Visual Studio.
        VisualStudioTenantId = "{TENANT ID}",
        SharedTokenCacheTenantId = "{TENANT ID}"
    };

    credential = new DefaultAzureCredential(options);
}

services.AddDataProtection()
    .SetApplicationName("{APPLICATION NAME}")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential)
    .ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), credential);
```

`{MANAGED IDENTITY CLIENT ID}`: The Azure Managed Identity Client ID (GUID).

`{TENANT ID}`: Tenant ID.

`{APPLICATION NAME}`: [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the unique name of this app within the data protection system. The value should match across deployments of the app.

`{BLOB URI}`: Full URI to the key file. The URI is generated by Azure Storage when you create the key file. Do not use a SAS.

`{KEY IDENTIFIER}`: Azure Key Vault key identifier used for key encryption. An access policy allows the application to access the key vault with `Get`, `Unwrap Key`, and `Wrap Key` permissions. The version of the key is obtained from the key in the Entra or Azure portal after it's created. If you enable automatic rotation of the key vault key, make sure that you use a *versionless* key identifier in the app's key vault configuration, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`).

For an app to communicate and authorize itself with Azure Key Vault, the [`Azure.Identity` NuGet package](https://www.nuget.org/packages/Azure.Identity/) must be referenced by the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Note

In non-`Production` environments, the preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

If the app uses the older Azure packages (`Microsoft.AspNetCore.DataProtection.AzureStorage` and `Microsoft.AspNetCore.DataProtection.AzureKeyVault`), we recommend ***removing*** these references and upgrading to the [`Azure.Extensions.AspNetCore.DataProtection.Blobs`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Blobs) and [`Azure.Extensions.AspNetCore.DataProtection.Keys`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Keys) packages. The newer packages address key security and stability issues.

**Alternative shared-access signature (SAS) approach**: As an alternative to using a Managed Identity for access to the key blob in Azure Blob Storage, you can call the [PersistKeysToAzureBlobStorage](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.azuredataprotectionbuilderextensions.persistkeystoazureblobstorage) overload that accepts a blob URI with a SAS token. The following example continues to use either a [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential) (production) or [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) (development and testing) for its [TokenCredential](/en-us/dotnet/api/azure.core.tokencredential), as seen in the preceding example:

```
services.AddDataProtection()
    .SetApplicationName("{APPLICATION NAME}")
    .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI WITH SAS}"))
    .ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), credential);
```

`{APPLICATION NAME}`: [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the unique name of this app within the data protection system. The value should match across deployments of the app.

`{BLOB URI WITH SAS}`: The full URI where the key file should be stored with the SAS token as a query string parameter. The URI is generated by Azure Storage when you request a SAS for the uploaded key file.

`{KEY IDENTIFIER}`: Azure Key Vault key identifier used for key encryption. An access policy allows the application to access the key vault with `Get`, `Unwrap Key`, and `Wrap Key` permissions. The version of the key is obtained from the key in the Entra or Azure portal after it's created. If you enable automatic rotation of the key vault key, make sure that you use a versionless key identifier in the app's key vault configuration, where no key GUID is placed at the end of the identifier (example: `https://contoso.vault.azure.net/keys/data-protection`).

## Persist keys to the file system (`PersistKeysToFileSystem`)

To store keys on a UNC share instead of at the *%LOCALAPPDATA%* default location, configure the system with [PersistKeysToFileSystem](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.persistkeystofilesystem):

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"));
}
```

Warning

If you change the key persistence location, the system no longer automatically encrypts keys at rest, since it doesn't know whether DPAPI is an appropriate encryption mechanism.

## Persist keys in a database (`PersistKeysToDbContext`)

To store keys in a database using EntityFramework, configure the system with the [Microsoft.AspNetCore.DataProtection.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.EntityFrameworkCore/) package:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToDbContext<DbContext>()
}
```

The preceding code stores the keys in the configured database. The database context being used must implement `IDataProtectionKeyContext`. `IDataProtectionKeyContext` exposes the property `DataProtectionKeys`

```
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
```

This property represents the table in which the keys are stored. Create the table manually or with `DbContext` Migrations. For more information, see [DataProtectionKey](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.entityframeworkcore.dataprotectionkey).

## Protect keys configuration API (`ProtectKeysWith\*`)

You can configure the system to protect keys at rest by calling any of the [`ProtectKeysWith\*`](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions) configuration APIs. Consider the example below, which stores keys on a UNC share and encrypts those keys at rest with a specific X.509 certificate:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
        .ProtectKeysWithCertificate(Configuration["Thumbprint"]);
}
```

You can provide an [X509Certificate2](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2) to [ProtectKeysWithCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate), such as a certificate loaded from a file:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
        .ProtectKeysWithCertificate(
            new X509Certificate2("certificate.pfx", Configuration["Thumbprint"]));
}
```

For more examples and discussion on the built-in key encryption mechanisms, see [Key encryption at rest in Windows and Azure using ASP.NET Core](../implementation/key-encryption-at-rest?view=aspnetcore-10.0).

## Unprotect keys with any certificate (`UnprotectKeysWithAnyCertificate`)

You can rotate certificates and decrypt keys at rest using an array of [X509Certificate2](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2) certificates with [UnprotectKeysWithAnyCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.unprotectkeyswithanycertificate):

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"))
        .ProtectKeysWithCertificate(
            new X509Certificate2("certificate.pfx", Configuration["MyPasswordKey"));
        .UnprotectKeysWithAnyCertificate(
            new X509Certificate2("certificate_old_1.pfx", Configuration["MyPasswordKey_1"]),
            new X509Certificate2("certificate_old_2.pfx", Configuration["MyPasswordKey_2"]));
}
```

## Set the default key lifetime (`SetDefaultKeyLifetime`)

To configure the system to use a key lifetime of 14 days instead of the default 90 days, use [SetDefaultKeyLifetime](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setdefaultkeylifetime):

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .SetDefaultKeyLifetime(TimeSpan.FromDays(14));
}
```

## Set the application name (`SetApplicationName`)

By default, the Data Protection system isolates apps from one another based on their [content root](../../../fundamentals/?view=aspnetcore-10.0#content-root) paths, even if they share the same physical key repository. This isolation prevents the apps from understanding each other's protected payloads.

To share protected payloads among apps:

* Configure [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) in each app with the same value.

* Use the same version of the Data Protection API stack across the apps. Perform **either** of the following in the apps' project files:

  * Reference the same shared framework version via the [Microsoft.AspNetCore.App metapackage](../../../fundamentals/metapackage-app?view=aspnetcore-10.0).
  * Reference the same [Data Protection package](../introduction?view=aspnetcore-10.0#package-layout) version.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .SetApplicationName("{APPLICATION NAME}");
}
```

[SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) internally sets [DataProtectionOptions.ApplicationDiscriminator](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionoptions.applicationdiscriminator#microsoft-aspnetcore-dataprotection-dataprotectionoptions-applicationdiscriminator). For more information on how the discriminator is used, see the following sections later in this article:

* [Per-application isolation](#per-application-isolation)
* [Data Protection and app isolation](#data-protection-and-app-isolation)

## Disable automatic key generation (`DisableAutomaticKeyGeneration`)

You may have a scenario where you don't want an app to automatically roll keys (create new keys) as they approach expiration. One example of this scenario might be apps set up in a primary/secondary relationship, where only the primary app is responsible for key management concerns and secondary apps simply have a read-only view of the key ring. The secondary apps can be configured to treat the key ring as read-only by configuring the system with [DisableAutomaticKeyGeneration](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.disableautomatickeygeneration):

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .DisableAutomaticKeyGeneration();
}
```

## Per-application isolation

When the Data Protection system is provided by an ASP.NET Core host, it automatically isolates apps from one another, even if those apps are running under the same worker process account and are using the same master keying material. This is similar to the IsolateApps modifier from System.Web's `<machineKey>` element.

The isolation mechanism works by considering each app on the local machine as a unique tenant, thus the [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector) rooted for any given app automatically includes the app ID as a discriminator ([ApplicationDiscriminator](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionoptions.applicationdiscriminator#microsoft-aspnetcore-dataprotection-dataprotectionoptions-applicationdiscriminator)). The app's unique ID is the app's physical path:

* For apps hosted in IIS, the unique ID is the IIS physical path of the app. If an app is deployed in a web farm environment, this value is stable assuming that the IIS environments are configured similarly across all machines in the web farm.
* For self-hosted apps running on the [Kestrel server](../../../fundamentals/servers/?view=aspnetcore-10.0#kestrel), the unique ID is the physical path to the app on disk.

The unique identifier is designed to survive resets—both of the individual app and of the machine itself.

This isolation mechanism assumes that the apps aren't malicious. A malicious app can always impact any other app running under the same worker process account. In a shared hosting environment where apps are mutually untrusted, the hosting provider should take steps to ensure OS-level isolation between apps, including separating the apps' underlying key repositories.

If the Data Protection system isn't provided by an ASP.NET Core host (for example, if you instantiate it via the `DataProtectionProvider` concrete type) app isolation is disabled by default. When app isolation is disabled, all apps backed by the same keying material can share payloads as long as they provide the appropriate [purposes](../consumer-apis/purpose-strings?view=aspnetcore-10.0). To provide app isolation in this environment, call the [SetApplicationName](#set-the-application-name-setapplicationname) method on the configuration object and provide a unique name for each app.

### Data Protection and app isolation

Consider the following points for app isolation:

* When multiple apps are pointed at the same key repository, the intention is that the apps share the same master key material. Data Protection is developed with the assumption that all apps sharing a key ring can access all items in that key ring. The application unique identifier is used to isolate application specific keys derived from the key ring provided keys. It doesn't expect item level permissions, such as those provided by Azure KeyVault to be used to enforce extra isolation. Attempting item level permissions generates application errors. If you don't want to rely on the built-in application isolation, separate key store locations should be used and not shared between applications.

* The application discriminator ([ApplicationDiscriminator](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionoptions.applicationdiscriminator#microsoft-aspnetcore-dataprotection-dataprotectionoptions-applicationdiscriminator)) is used to allow different apps to share the same master key material but to keep their cryptographic payloads distinct from one another. For the apps to be able to read each other's cryptographic payloads, they must have the same application discriminator, which can be set by calling [`SetApplicationName`](#set-the-application-name-setapplicationname).

* If an app is compromised (for example, by an RCE attack), all master key material accessible to that app must also be considered compromised, regardless of its protection-at-rest state. This implies that if two apps are pointed at the same repository, even if they use different app discriminators, a compromise of one is functionally equivalent to a compromise of both.

  This "functionally equivalent to a compromise of both" clause holds even if the two apps use different mechanisms for key protection at rest. Typically, this isn't an expected configuration. The protection-at-rest mechanism is intended to provide protection in the event a cyberattacker gains read access to the repository. A cyberattacker who gains write access to the repository (perhaps because they attained code execution permission within an app) can insert malicious keys into storage. The Data Protection system intentionally doesn't provide protection against a cyberattacker who gains write access to the key repository.

* If apps need to remain truly isolated from one another, they should use different key repositories. This naturally falls out of the definition of "isolated". Apps are ***not*** isolated if they all have Read and Write access to each other's data stores.

## Changing algorithms with `UseCryptographicAlgorithms`

The Data Protection stack allows you to change the default algorithm used by newly generated keys. The simplest way to do this is to call [UseCryptographicAlgorithms](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.usecryptographicalgorithms) from the configuration callback:

```
services.AddDataProtection()
    .UseCryptographicAlgorithms(
        new AuthenticatedEncryptorConfiguration()
    {
        EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
        ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
    });
```

The default EncryptionAlgorithm is AES-256-CBC, and the default ValidationAlgorithm is HMACSHA256. The default policy can be set by a system administrator via a [machine-wide policy](machine-wide-policy?view=aspnetcore-10.0), but an explicit call to `UseCryptographicAlgorithms` overrides the default policy.

Calling `UseCryptographicAlgorithms` allows you to specify the desired algorithm from a predefined built-in list. You don't need to worry about the implementation of the algorithm. In the scenario above, the Data Protection system attempts to use the CNG implementation of AES if running on Windows. Otherwise, it falls back to the managed [System.Security.Cryptography.Aes](/en-us/dotnet/api/system.security.cryptography.aes) class.

You can manually specify an implementation via a call to [UseCustomCryptographicAlgorithms](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.usecustomcryptographicalgorithms).

Tip

Changing algorithms doesn't affect existing keys in the key ring. It only affects newly-generated keys.

### Specifying custom managed algorithms

To specify custom managed algorithms, create a [ManagedAuthenticatedEncryptorConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.configurationmodel.managedauthenticatedencryptorconfiguration) instance that points to the implementation types:

```
serviceCollection.AddDataProtection()
    .UseCustomCryptographicAlgorithms(
        new ManagedAuthenticatedEncryptorConfiguration()
    {
        // A type that subclasses SymmetricAlgorithm
        EncryptionAlgorithmType = typeof(Aes),

        // Specified in bits
        EncryptionAlgorithmKeySize = 256,

        // A type that subclasses KeyedHashAlgorithm
        ValidationAlgorithmType = typeof(HMACSHA256)
    });
```

Generally the \*Type properties must point to concrete, instantiable (via a public parameterless ctor) implementations of [SymmetricAlgorithm](/en-us/dotnet/api/system.security.cryptography.symmetricalgorithm) and [KeyedHashAlgorithm](/en-us/dotnet/api/system.security.cryptography.keyedhashalgorithm), though the system special-cases some values like `typeof(Aes)` for convenience.

Note

The SymmetricAlgorithm must have a key length of ≥ 128 bits and a block size of ≥ 64 bits, and it must support CBC-mode encryption with PKCS #7 padding. The KeyedHashAlgorithm must have a digest size of >= 128 bits, and it must support keys of length equal to the hash algorithm's digest length. The KeyedHashAlgorithm isn't strictly required to be HMAC.

### Specifying custom Windows CNG algorithms

To specify a custom Windows CNG algorithm using CBC-mode encryption with HMAC validation, create a [CngCbcAuthenticatedEncryptorConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.configurationmodel.cngcbcauthenticatedencryptorconfiguration) instance that contains the algorithmic information:

```
services.AddDataProtection()
    .UseCustomCryptographicAlgorithms(
        new CngCbcAuthenticatedEncryptorConfiguration()
    {
        // Passed to BCryptOpenAlgorithmProvider
        EncryptionAlgorithm = "AES",
        EncryptionAlgorithmProvider = null,

        // Specified in bits
        EncryptionAlgorithmKeySize = 256,

        // Passed to BCryptOpenAlgorithmProvider
        HashAlgorithm = "SHA256",
        HashAlgorithmProvider = null
    });
```

Note

The symmetric block cipher algorithm must have a key length of >= 128 bits, a block size of >= 64 bits, and it must support CBC-mode encryption with PKCS #7 padding. The hash algorithm must have a digest size of >= 128 bits and must support being opened with the BCRYPT\_ALG\_HANDLE\_HMAC\_FLAG flag. The \*Provider properties can be set to null to use the default provider for the specified algorithm. For more information, see the [BCryptOpenAlgorithmProvider](/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptopenalgorithmprovider) documentation.

To specify a custom Windows CNG algorithm using Galois/Counter Mode encryption with validation, create a [CngGcmAuthenticatedEncryptorConfiguration](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.configurationmodel.cnggcmauthenticatedencryptorconfiguration) instance that contains the algorithmic information:

```
services.AddDataProtection()
    .UseCustomCryptographicAlgorithms(
        new CngGcmAuthenticatedEncryptorConfiguration()
    {
        // Passed to BCryptOpenAlgorithmProvider
        EncryptionAlgorithm = "AES",
        EncryptionAlgorithmProvider = null,

        // Specified in bits
        EncryptionAlgorithmKeySize = 256
    });
```

Note

The symmetric block cipher algorithm must have a key length of >= 128 bits, a block size of exactly 128 bits, and it must support GCM encryption. You can set the [EncryptionAlgorithmProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.configurationmodel.cngcbcauthenticatedencryptorconfiguration.encryptionalgorithmprovider#microsoft-aspnetcore-dataprotection-authenticatedencryption-configurationmodel-cngcbcauthenticatedencryptorconfiguration-encryptionalgorithmprovider) property to null to use the default provider for the specified algorithm. For more information, see the [BCryptOpenAlgorithmProvider](/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptopenalgorithmprovider) documentation.

### Specifying other custom algorithms

Though not exposed as a first-class API, the Data Protection system is extensible enough to allow specifying almost any kind of algorithm. For example, it's possible to keep all keys contained within a Hardware Security Module (HSM) and to provide a custom implementation of the core encryption and decryption routines. For more information, see [IAuthenticatedEncryptor](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.authenticatedencryption.iauthenticatedencryptor) in [Core cryptography extensibility](../extensibility/core-crypto?view=aspnetcore-10.0).

## Persisting keys when hosting in a Docker container

When hosting in a [Docker](/en-us/dotnet/standard/microservices-architecture/container-docker-introduction/) container, keys should be maintained in either:

* A folder that's a Docker volume that persists beyond the container's lifetime, such as a shared volume or a host-mounted volume.
* An external provider, such as [Azure Blob Storage](/en-us/azure/storage/blobs/storage-blobs-introduction) (shown in the [Protect keys with Azure Key Vault (`ProtectKeysWithAzureKeyVault`)](#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault) section) or [Redis](https://redis.io).

## Persisting keys with Redis

Only Redis versions supporting [Redis Data Persistence](/en-us/azure/azure-cache-for-redis/cache-how-to-premium-persistence) should be used to store keys. [Azure Blob storage](/en-us/azure/storage/blobs/storage-blobs-introduction) is persistent and can be used to store keys. For more information, see [this GitHub issue](https://github.com/dotnet/AspNetCore/issues/13476).

## Logging

Enable the `Information` or lower level of logging to diagnose problems. The following `appsettings.json` file enables information logging of the Data Protection API:

```
{
  "Logging": {
    "LogLevel": {
      "Microsoft.AspNetCore.DataProtection": "Information"
    }
  }
}
```

For more information on logging, see [Logging in .NET and ASP.NET Core](../../../fundamentals/logging/?view=aspnetcore-10.0).

## Additional resources

* [Non-DI aware scenarios for Data Protection in ASP.NET Core](non-di-scenarios?view=aspnetcore-10.0)
* [Data Protection machine-wide policy support in ASP.NET Core](machine-wide-policy?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../../host-and-deploy/web-farm?view=aspnetcore-10.0)
* [Key storage providers in ASP.NET Core](../implementation/key-storage-providers?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Create a Razor Pages web app with ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This series of tutorials explains the basics of building a Razor Pages web app.

For a more advanced introduction aimed at developers who are familiar with controllers and views, see [Razor Pages architecture and concepts in ASP.NET Core](../../razor-pages/?view=aspnetcore-10.0).

If you're new to ASP.NET Core development and are unsure of which ASP.NET Core web UI solution will best fit your needs, see [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0).

This series includes the following tutorials:

1. [Create a Razor Pages web app](razor-pages-start?view=aspnetcore-10.0)
2. [Add a model to a Razor Pages app](model?view=aspnetcore-10.0)
3. [Scaffold (generate) Razor pages](page?view=aspnetcore-10.0)
4. [Work with a database](sql?view=aspnetcore-10.0)
5. [Update Razor pages](da1?view=aspnetcore-10.0)
6. [Add search](search?view=aspnetcore-10.0)
7. [Add a new field](new-field?view=aspnetcore-10.0)
8. [Add validation](validation?view=aspnetcore-10.0)

At the end, you'll have an app that can display and manage a database of movies.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/built-in-components
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/built-in-components.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core built-in Razor components

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The following built-in Razor components are provided by the Blazor framework. For information on non-security-related project template components, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0). For information on security-related project template components, see the [Security node articles](../security/?view=aspnetcore-10.0).

* [`AntiforgeryToken`](../forms/?view=aspnetcore-10.0#antiforgery-support)
* [`AuthorizeView`](../security/?view=aspnetcore-10.0#authorizeview-component)
* [`CacheView`](../state-management/cacheview-component?view=aspnetcore-10.0)
* [`CascadingValue`](cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component)
* [`DataAnnotationsValidator`](../forms/validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation)
* [`DynamicComponent`](dynamiccomponent?view=aspnetcore-10.0)
* [`Editor<T>`](../forms/binding?view=aspnetcore-10.0#nest-and-bind-forms)
* [`EditForm`](../forms/binding?view=aspnetcore-10.0#editformeditcontext-model)
* [`ErrorBoundary`](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries)
* [`FocusOnNavigate`](../fundamentals/routing?view=aspnetcore-10.0#focus-an-element-on-navigation)
* [`HeadContent`](control-head-content?view=aspnetcore-10.0)
* [`HeadOutlet`](control-head-content?view=aspnetcore-10.0)
* [`ImportMap`](../fundamentals/static-files?view=aspnetcore-10.0#importmap-component)
* [`InputCheckbox`](../forms/input-components?view=aspnetcore-10.0)
* [`InputDate`](../forms/input-components?view=aspnetcore-10.0)
* [`InputFile`](../file-uploads?view=aspnetcore-10.0)
* [`InputNumber`](../forms/input-components?view=aspnetcore-10.0)
* [`InputRadio`](../forms/input-components?view=aspnetcore-10.0)
* [`InputRadioGroup`](../forms/input-components?view=aspnetcore-10.0)
* [`InputSelect`](../forms/input-components?view=aspnetcore-10.0)
* [`InputText`](../forms/input-components?view=aspnetcore-10.0)
* [`InputTextArea`](../forms/input-components?view=aspnetcore-10.0)
* [`LayoutComponentBase`](layouts?view=aspnetcore-10.0#create-a-layout-component)
* [`LayoutView`](layouts?view=aspnetcore-10.0#apply-a-layout-to-arbitrary-content-layoutview-component)
* [`NavigationLock`](../fundamentals/navigation?view=aspnetcore-10.0#handleprevent-location-changes)
* [`NavLink`](../fundamentals/navigation?view=aspnetcore-10.0#navlink-component)
* [`PageTitle`](control-head-content?view=aspnetcore-10.0)
* [`OwningComponentBase`](../../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope)
* [`Paginator`](quickgrid?view=aspnetcore-10.0#page-items-with-a-paginator-component)
* [`QuickGrid`](quickgrid?view=aspnetcore-10.0)
* [`Router`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [`RouteView`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [`SectionContent`](sections?view=aspnetcore-10.0)
* [`SectionOutlet`](sections?view=aspnetcore-10.0)
* [`ValidationMessage`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
* [`ValidationSummary`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
* [`Virtualize`](virtualization?view=aspnetcore-10.0)

- [`AntiforgeryToken`](../forms/?view=aspnetcore-10.0#antiforgery-support)
- [`AuthorizeView`](../security/?view=aspnetcore-10.0#authorizeview-component)
- [`CascadingValue`](cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component)
- [`DataAnnotationsValidator`](../forms/validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation)
- [`DynamicComponent`](dynamiccomponent?view=aspnetcore-10.0)
- [`Editor<T>`](../forms/binding?view=aspnetcore-10.0#nest-and-bind-forms)
- [`EditForm`](../forms/binding?view=aspnetcore-10.0#editformeditcontext-model)
- [`ErrorBoundary`](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries)
- [`FocusOnNavigate`](../fundamentals/routing?view=aspnetcore-10.0#focus-an-element-on-navigation)
- [`HeadContent`](control-head-content?view=aspnetcore-10.0)
- [`HeadOutlet`](control-head-content?view=aspnetcore-10.0)
- [`ImportMap`](../fundamentals/static-files?view=aspnetcore-10.0#importmap-component)
- [`InputCheckbox`](../forms/input-components?view=aspnetcore-10.0)
- [`InputDate`](../forms/input-components?view=aspnetcore-10.0)
- [`InputFile`](../file-uploads?view=aspnetcore-10.0)
- [`InputNumber`](../forms/input-components?view=aspnetcore-10.0)
- [`InputRadio`](../forms/input-components?view=aspnetcore-10.0)
- [`InputRadioGroup`](../forms/input-components?view=aspnetcore-10.0)
- [`InputSelect`](../forms/input-components?view=aspnetcore-10.0)
- [`InputText`](../forms/input-components?view=aspnetcore-10.0)
- [`InputTextArea`](../forms/input-components?view=aspnetcore-10.0)
- [`LayoutComponentBase`](layouts?view=aspnetcore-10.0#create-a-layout-component)
- [`LayoutView`](layouts?view=aspnetcore-10.0#apply-a-layout-to-arbitrary-content-layoutview-component)
- [`NavigationLock`](../fundamentals/navigation?view=aspnetcore-10.0#handleprevent-location-changes)
- [`NavLink`](../fundamentals/navigation?view=aspnetcore-10.0#navlink-component)
- [`PageTitle`](control-head-content?view=aspnetcore-10.0)
- [`OwningComponentBase`](../../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope)
- [`Paginator`](quickgrid?view=aspnetcore-10.0#page-items-with-a-paginator-component)
- [`QuickGrid`](quickgrid?view=aspnetcore-10.0)
- [`Router`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
- [`RouteView`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
- [`SectionContent`](sections?view=aspnetcore-10.0)
- [`SectionOutlet`](sections?view=aspnetcore-10.0)
- [`ValidationMessage`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
- [`ValidationSummary`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
- [`Virtualize`](virtualization?view=aspnetcore-10.0)

* [`AntiforgeryToken`](../forms/?view=aspnetcore-10.0#antiforgery-support)
* [`AuthorizeView`](../security/?view=aspnetcore-10.0#authorizeview-component)
* [`CascadingValue`](cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component)
* [`DataAnnotationsValidator`](../forms/validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation)
* [`DynamicComponent`](dynamiccomponent?view=aspnetcore-10.0)
* [`Editor<T>`](../forms/binding?view=aspnetcore-10.0#nest-and-bind-forms)
* [`EditForm`](../forms/binding?view=aspnetcore-10.0#editformeditcontext-model)
* [`ErrorBoundary`](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries)
* [`FocusOnNavigate`](../fundamentals/routing?view=aspnetcore-10.0#focus-an-element-on-navigation)
* [`HeadContent`](control-head-content?view=aspnetcore-10.0)
* [`HeadOutlet`](control-head-content?view=aspnetcore-10.0)
* [`InputCheckbox`](../forms/input-components?view=aspnetcore-10.0)
* [`InputDate`](../forms/input-components?view=aspnetcore-10.0)
* [`InputFile`](../file-uploads?view=aspnetcore-10.0)
* [`InputNumber`](../forms/input-components?view=aspnetcore-10.0)
* [`InputRadio`](../forms/input-components?view=aspnetcore-10.0)
* [`InputRadioGroup`](../forms/input-components?view=aspnetcore-10.0)
* [`InputSelect`](../forms/input-components?view=aspnetcore-10.0)
* [`InputText`](../forms/input-components?view=aspnetcore-10.0)
* [`InputTextArea`](../forms/input-components?view=aspnetcore-10.0)
* [`LayoutComponentBase`](layouts?view=aspnetcore-10.0#create-a-layout-component)
* [`LayoutView`](layouts?view=aspnetcore-10.0#apply-a-layout-to-arbitrary-content-layoutview-component)
* [`NavigationLock`](../fundamentals/navigation?view=aspnetcore-10.0#handleprevent-location-changes)
* [`NavLink`](../fundamentals/navigation?view=aspnetcore-10.0#navlink-component)
* [`PageTitle`](control-head-content?view=aspnetcore-10.0)
* [`OwningComponentBase`](../../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope)
* [`Paginator`](quickgrid?view=aspnetcore-10.0#page-items-with-a-paginator-component)
* [`QuickGrid`](quickgrid?view=aspnetcore-10.0)
* [`Router`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [`RouteView`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [`SectionContent`](sections?view=aspnetcore-10.0)
* [`SectionOutlet`](sections?view=aspnetcore-10.0)
* [`ValidationMessage`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
* [`ValidationSummary`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
* [`Virtualize`](virtualization?view=aspnetcore-10.0)

- [`AuthorizeView`](../security/?view=aspnetcore-10.0#authorizeview-component)
- [`CascadingValue`](cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component)
- [`DataAnnotationsValidator`](../forms/validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation)
- [`DynamicComponent`](dynamiccomponent?view=aspnetcore-10.0)
- [`EditForm`](../forms/binding?view=aspnetcore-10.0#editformeditcontext-model)
- [`ErrorBoundary`](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries)
- [`FocusOnNavigate`](../fundamentals/routing?view=aspnetcore-10.0#focus-an-element-on-navigation)
- [`HeadContent`](control-head-content?view=aspnetcore-10.0)
- [`HeadOutlet`](control-head-content?view=aspnetcore-10.0)
- [`InputCheckbox`](../forms/input-components?view=aspnetcore-10.0)
- [`InputDate`](../forms/input-components?view=aspnetcore-10.0)
- [`InputFile`](../file-uploads?view=aspnetcore-10.0)
- [`InputNumber`](../forms/input-components?view=aspnetcore-10.0)
- [`InputRadio`](../forms/input-components?view=aspnetcore-10.0)
- [`InputRadioGroup`](../forms/input-components?view=aspnetcore-10.0)
- [`InputSelect`](../forms/input-components?view=aspnetcore-10.0)
- [`InputText`](../forms/input-components?view=aspnetcore-10.0)
- [`InputTextArea`](../forms/input-components?view=aspnetcore-10.0)
- [`LayoutComponentBase`](layouts?view=aspnetcore-10.0#create-a-layout-component)
- [`LayoutView`](layouts?view=aspnetcore-10.0#apply-a-layout-to-arbitrary-content-layoutview-component)
- [`NavigationLock`](../fundamentals/navigation?view=aspnetcore-10.0#handleprevent-location-changes)
- [`NavLink`](../fundamentals/navigation?view=aspnetcore-10.0#navlink-component)
- [`OwningComponentBase`](../../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope)
- [`PageTitle`](control-head-content?view=aspnetcore-10.0)
- [`Router`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
- [`RouteView`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
- [`ValidationMessage`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
- [`ValidationSummary`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
- [`Virtualize`](virtualization?view=aspnetcore-10.0)

* [`AuthorizeView`](../security/?view=aspnetcore-10.0#authorizeview-component)
* [`CascadingValue`](cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component)
* [`DataAnnotationsValidator`](../forms/validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation)
* [`DynamicComponent`](dynamiccomponent?view=aspnetcore-10.0)
* [`EditForm`](../forms/binding?view=aspnetcore-10.0#editformeditcontext-model)
* [`ErrorBoundary`](../fundamentals/handle-errors?view=aspnetcore-10.0#error-boundaries)
* [`FocusOnNavigate`](../fundamentals/routing?view=aspnetcore-10.0#focus-an-element-on-navigation)
* [`HeadContent`](control-head-content?view=aspnetcore-10.0)
* [`HeadOutlet`](control-head-content?view=aspnetcore-10.0)
* [`InputCheckbox`](../forms/input-components?view=aspnetcore-10.0)
* [`InputDate`](../forms/input-components?view=aspnetcore-10.0)
* [`InputFile`](../file-uploads?view=aspnetcore-10.0)
* [`InputNumber`](../forms/input-components?view=aspnetcore-10.0)
* [`InputRadio`](../forms/input-components?view=aspnetcore-10.0)
* [`InputRadioGroup`](../forms/input-components?view=aspnetcore-10.0)
* [`InputSelect`](../forms/input-components?view=aspnetcore-10.0)
* [`InputText`](../forms/input-components?view=aspnetcore-10.0)
* [`InputTextArea`](../forms/input-components?view=aspnetcore-10.0)
* [`LayoutComponentBase`](layouts?view=aspnetcore-10.0#create-a-layout-component)
* [`LayoutView`](layouts?view=aspnetcore-10.0#apply-a-layout-to-arbitrary-content-layoutview-component)
* [`NavLink`](../fundamentals/navigation?view=aspnetcore-10.0#navlink-component)
* [`OwningComponentBase`](../../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope)
* [`PageTitle`](control-head-content?view=aspnetcore-10.0)
* [`Router`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [`RouteView`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [`ValidationMessage`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
* [`ValidationSummary`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
* [`Virtualize`](virtualization?view=aspnetcore-10.0)

- [`AuthorizeView`](../security/?view=aspnetcore-10.0#authorizeview-component)
- [`CascadingValue`](cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component)
- [`DataAnnotationsValidator`](../forms/validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation)
- [`EditForm`](../forms/binding?view=aspnetcore-10.0#editformeditcontext-model)
- [`InputCheckbox`](../forms/input-components?view=aspnetcore-10.0)
- [`InputDate`](../forms/input-components?view=aspnetcore-10.0)
- [`InputFile`](../file-uploads?view=aspnetcore-10.0)
- [`InputNumber`](../forms/input-components?view=aspnetcore-10.0)
- [`InputRadio`](../forms/input-components?view=aspnetcore-10.0)
- [`InputRadioGroup`](../forms/input-components?view=aspnetcore-10.0)
- [`InputSelect`](../forms/input-components?view=aspnetcore-10.0)
- [`InputText`](../forms/input-components?view=aspnetcore-10.0)
- [`InputTextArea`](../forms/input-components?view=aspnetcore-10.0)
- [`LayoutComponentBase`](layouts?view=aspnetcore-10.0#create-a-layout-component)
- [`LayoutView`](layouts?view=aspnetcore-10.0#apply-a-layout-to-arbitrary-content-layoutview-component)
- [`NavLink`](../fundamentals/navigation?view=aspnetcore-10.0#navlink-component)
- [`OwningComponentBase`](../../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope)
- [`Router`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
- [`RouteView`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
- [`ValidationMessage`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
- [`ValidationSummary`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
- [`Virtualize`](virtualization?view=aspnetcore-10.0)

* [`AuthorizeView`](../security/?view=aspnetcore-10.0#authorizeview-component)
* [`CascadingValue`](cascading-values-and-parameters?view=aspnetcore-10.0#cascadingvalue-component)
* [`DataAnnotationsValidator`](../forms/validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation)
* [`EditForm`](../forms/binding?view=aspnetcore-10.0#editformeditcontext-model)
* [`InputCheckbox`](../forms/input-components?view=aspnetcore-10.0)
* [`InputDate`](../forms/input-components?view=aspnetcore-10.0)
* [`InputNumber`](../forms/input-components?view=aspnetcore-10.0)
* [`InputRadio`](../forms/input-components?view=aspnetcore-10.0)
* [`InputRadioGroup`](../forms/input-components?view=aspnetcore-10.0)
* [`InputSelect`](../forms/input-components?view=aspnetcore-10.0)
* [`InputText`](../forms/input-components?view=aspnetcore-10.0)
* [`InputTextArea`](../forms/input-components?view=aspnetcore-10.0)
* [`LayoutComponentBase`](layouts?view=aspnetcore-10.0#create-a-layout-component)
* [`LayoutView`](layouts?view=aspnetcore-10.0#apply-a-layout-to-arbitrary-content-layoutview-component)
* [`NavLink`](../fundamentals/navigation?view=aspnetcore-10.0#navlink-component)
* [`OwningComponentBase`](../../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope)
* [`Router`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [`RouteView`](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [`ValidationMessage`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)
* [`ValidationSummary`](../forms/validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-14

----
url: https://learn.microsoft.com/en-us/aspnet/core/metrics/http
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/metrics/http.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core built-in HTTP metrics

This article describes the built-in HTTP-related metrics for ASP.NET Core produced using the [System.Diagnostics.Metrics](/en-us/dotnet/api/system.diagnostics.metrics) API. These metrics cover hosting, routing, rate limiting, header parsing, the Kestrel web server, and SignalR.

For an overview of all built-in metrics reference pages and how to read this reference, see [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0). For information on how to collect, report, enrich, and test with ASP.NET Core metrics, see [ASP.NET Core metrics](overview?view=aspnetcore-10.0).

## `Microsoft.AspNetCore.Hosting`

The `Microsoft.AspNetCore.Hosting` metrics report high-level information about HTTP requests received by ASP.NET Core:

* [`http.server.request.duration`](#metric-httpserverrequestduration)
* [`http.server.active_requests`](#metric-httpserveractive_requests)

Note

In ASP.NET Core 11 and later, the instruments under `Microsoft.AspNetCore.Hosting` are the framework's primary implementation of the [OpenTelemetry HTTP server semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/). Register this meter directly with the OpenTelemetry SDK (for example, call `AddMeter("Microsoft.AspNetCore.Hosting")` in your OpenTelemetry metrics pipeline) without adding the [`OpenTelemetry.Instrumentation.AspNetCore`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNetCore) package. For details, see [ASP.NET Core metrics](overview?view=aspnetcore-10.0).

### Metric: `http.server.request.duration`

| Name                                                                                                                                       | Instrument Type | Unit (UCUM) | Description                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------ | --------------- | ----------- | ----------------------------------------------- |
| [`http.server.request.duration`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-http-metrics/#metric-httpserverrequestduration) | Histogram       | `s`         | Measures the duration of inbound HTTP requests. |

| Attribute                         | Type    | Description                                                                                                   | Examples                                  | Presence                            |
| --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------- |
| `http.route`                      | string  | The matched route.                                                                                            | `{controller}/{action}/` `{id?}`          | If it's available.                  |
| `error.type`                      | string  | Describes a class of error the operation ended with.                                                          | `timeout`; `name_resolution_error`; `500` | If request has ended with an error. |
| `http.request.method`             | string  | HTTP request method.                                                                                          | `GET`; `POST`; `HEAD`                     | Always                              |
| `http.response.status_code`       | int     | [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).                                   | `200`                                     | If one was sent.                    |
| `network.protocol.version`        | string  | Version of the protocol specified in `network.protocol.name`.                                                 | `3.1.1`                                   | Always                              |
| `url.scheme`                      | string  | The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. | `http`; `https`                           | Always                              |
| `aspnetcore.request.is_unhandled` | boolean | True when the request wasn't handled by the application pipeline.                                             | `true`                                    | If the request was unhandled.       |

The time used to handle an inbound HTTP request as measured at the hosting layer of ASP.NET Core. The time measurement starts once the underlying web host has:

* Sufficiently parsed the HTTP request headers on the inbound network stream to identify the new request.
* Initialized the context data structures such as the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext).

The time ends when:

* The ASP.NET Core handler pipeline is finished executing.
* All response data has been sent.
* The context data structures for the request are being disposed.

When using OpenTelemetry, the default buckets for this metric are set to \[ 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 ].

### Metric: `http.server.active_requests`

| Name                                                                                                                                      | Instrument Type | Unit (UCUM) | Description                                                                   |
| ----------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | ----------------------------------------------------------------------------- |
| [`http.server.active_requests`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-http-metrics/#metric-httpserveractive_requests) | UpDownCounter   | `{request}` | Measures the number of concurrent HTTP requests that are currently in flight. |

| Attribute             | Type   | Description                                                                                                   | Examples              | Presence |
| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------- | --------------------- | -------- |
| `http.request.method` | string | HTTP request method. \[1]                                                                                     | `GET`; `POST`; `HEAD` | Always   |
| `url.scheme`          | string | The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. | `http`; `https`       | Always   |

## `Microsoft.AspNetCore.Routing`

The `Microsoft.AspNetCore.Routing` metrics report information about [routing HTTP requests](/en-us/aspnet/core/fundamentals/routing) to ASP.NET Core endpoints:

* [`aspnetcore.routing.match_attempts`](#metric-aspnetcoreroutingmatch_attempts)

### Metric: `aspnetcore.routing.match_attempts`

| Name                                                                                                                                                        | Instrument Type | Unit (UCUM)       | Description                                                          |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------------- | -------------------------------------------------------------------- |
| [`aspnetcore.routing.match_attempts`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-aspnetcore-metrics/#metric-aspnetcoreroutingmatch_attempts) | Counter         | `{match_attempt}` | Number of requests that were attempted to be matched to an endpoint. |

| Attribute                              | Type    | Description                                                           | Examples                         | Presence                             |
| -------------------------------------- | ------- | --------------------------------------------------------------------- | -------------------------------- | ------------------------------------ |
| `aspnetcore.routing.match_status`      | string  | Match result.                                                         | `success`; `failure`             | Always                               |
| `aspnetcore.routing.is_fallback_route` | boolean | A value that indicates whether the matched route is a fallback route. | `true`                           | If a route was successfully matched. |
| `http.route`                           | string  | The matched route.                                                    | `{controller}/{action}/` `{id?}` | If a route was successfully matched. |

## `Microsoft.AspNetCore.RateLimiting`

The `Microsoft.AspNetCore.RateLimiting` metrics report rate limiting information from [ASP.NET Core rate-limiting middleware](/en-us/aspnet/core/performance/rate-limit):

* [`aspnetcore.rate_limiting.active_request_leases`](#metric-aspnetcorerate_limitingactive_request_leases)
* [`aspnetcore.rate_limiting.request_lease.duration`](#metric-aspnetcorerate_limitingrequest_leaseduration)
* [`aspnetcore.rate_limiting.queued_requests`](#metric-aspnetcorerate_limitingqueued_requests)
* [`aspnetcore.rate_limiting.request.time_in_queue`](#metric-aspnetcorerate_limitingrequesttime_in_queue)
* [`aspnetcore.rate_limiting.requests`](#metric-aspnetcorerate_limitingrequests)

### Metric: `aspnetcore.rate_limiting.active_request_leases`

| Name                                                                                                                                                                                  | Instrument Type | Unit (UCUM) | Description                                                                                 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | ------------------------------------------------------------------------------------------- |
| [`aspnetcore.rate_limiting.active_request_leases`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-aspnetcore-metrics/#metric-aspnetcorerate_limitingactive_request_leases) | UpDownCounter   | `{request}` | Number of requests that are currently active on the server that hold a rate limiting lease. |

| Attribute                         | Type   | Description                | Examples                    | Presence                                                            |
| --------------------------------- | ------ | -------------------------- | --------------------------- | ------------------------------------------------------------------- |
| `aspnetcore.rate_limiting.policy` | string | Rate limiting policy name. | `fixed`; `sliding`; `token` | If the matched endpoint for the request had a rate-limiting policy. |

### Metric: `aspnetcore.rate_limiting.request_lease.duration`

| Name                                                                                                                                                                                   | Instrument Type | Unit (UCUM) | Description                                                             |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | ----------------------------------------------------------------------- |
| [`aspnetcore.rate_limiting.request_lease.duration`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-aspnetcore-metrics/#metric-aspnetcorerate_limitingrequest_leaseduration) | Histogram       | `s`         | The duration of the rate limiting lease held by requests on the server. |

| Attribute                         | Type   | Description                | Examples                    | Presence                                                            |
| --------------------------------- | ------ | -------------------------- | --------------------------- | ------------------------------------------------------------------- |
| `aspnetcore.rate_limiting.policy` | string | Rate limiting policy name. | `fixed`; `sliding`; `token` | If the matched endpoint for the request had a rate-limiting policy. |

### Metric: `aspnetcore.rate_limiting.queued_requests`

| Name                                                                                                                                                                      | Instrument Type | Unit (UCUM) | Description                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | -------------------------------------------------------------------------------------- |
| [`aspnetcore.rate_limiting.queued_requests`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-aspnetcore-metrics/#metric-aspnetcorerate_limitingqueued_requests) | UpDownCounter   | `{request}` | Number of requests that are currently queued waiting to acquire a rate limiting lease. |

| Attribute                         | Type   | Description                | Examples                    | Presence                                                            |
| --------------------------------- | ------ | -------------------------- | --------------------------- | ------------------------------------------------------------------- |
| `aspnetcore.rate_limiting.policy` | string | Rate limiting policy name. | `fixed`; `sliding`; `token` | If the matched endpoint for the request had a rate-limiting policy. |

### Metric: `aspnetcore.rate_limiting.request.time_in_queue`

| Name                                                                                                                                                                                 | Instrument Type | Unit (UCUM) | Description                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------- | ----------- | ----------------------------------------------------------------------------- |
| [`aspnetcore.rate_limiting.request.time_in_queue`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-aspnetcore-metrics/#metric-aspnetcorerate_limitingrequesttime_in_queue) | Histogram       | `s`         | The time a request spent in a queue waiting to acquire a rate limiting lease. |

| Attribute                         | Type   | Description                                                                               | Examples                       | Presence                                                            |
| --------------------------------- | ------ | ----------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------- |
| `aspnetcore.rate_limiting.policy` | string | Rate limiting policy name.                                                                | `fixed`; `sliding`; `token`    | If the matched endpoint for the request had a rate-limiting policy. |
| `aspnetcore.rate_limiting.result` | string | The rate limiting result shows whether lease was acquired or contains a rejection reason. | `acquired`; `request_canceled` | Always                                                              |

### Metric: `aspnetcore.rate_limiting.requests`

| Name                                                                                                                                                        | Instrument Type | Unit (UCUM) | Description                                                     |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | --------------------------------------------------------------- |
| [`aspnetcore.rate_limiting.requests`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-aspnetcore-metrics/#metric-aspnetcorerate_limitingrequests) | Counter         | `{request}` | Number of requests that tried to acquire a rate limiting lease. |

| Attribute                         | Type   | Description                                                                               | Examples                       | Presence                                                            |
| --------------------------------- | ------ | ----------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------- |
| `aspnetcore.rate_limiting.policy` | string | Rate limiting policy name.                                                                | `fixed`; `sliding`; `token`    | If the matched endpoint for the request had a rate-limiting policy. |
| `aspnetcore.rate_limiting.result` | string | The rate limiting result shows whether lease was acquired or contains a rejection reason. | `acquired`; `request_canceled` | Always                                                              |

## `Microsoft.AspNetCore.HeaderParsing`

The `Microsoft.AspNetCore.HeaderParsing` metrics report information about [ASP.NET Core header parsing](https://www.nuget.org/packages/Microsoft.AspNetCore.HeaderParsing):

* [`aspnetcore.header_parsing.parse_errors`](#metric-aspnetcoreheader_parsingparse_errors)
* [`aspnetcore.header_parsing.cache_accesses`](#metric-aspnetcoreheader_parsingcache_accesses)

### Metric: `aspnetcore.header_parsing.parse_errors`

| Name                                     | Instrument Type | Unit (UCUM)     | Description                                                       |
| ---------------------------------------- | --------------- | --------------- | ----------------------------------------------------------------- |
| `aspnetcore.header_parsing.parse_errors` | Counter         | `{parse_error}` | Number of errors that occurred when parsing HTTP request headers. |

| Attribute                               | Type   | Description        | Examples                            | Presence |
| --------------------------------------- | ------ | ------------------ | ----------------------------------- | -------- |
| `aspnetcore.header_parsing.header.name` | string | The header name.   | `Content-Type`                      | Always   |
| `error.type`                            | string | The error message. | `Unable to parse media type value.` | Always   |

### Metric: `aspnetcore.header_parsing.cache_accesses`

The metric is emitted only for HTTP request header parsers that support caching.

| Name                                       | Instrument Type | Unit (UCUM)      | Description                                                        |
| ------------------------------------------ | --------------- | ---------------- | ------------------------------------------------------------------ |
| `aspnetcore.header_parsing.cache_accesses` | Counter         | `{cache_access}` | Number of times a cache storing parsed header values was accessed. |

| Attribute                                     | Type   | Description                                                                  | Examples       | Presence |
| --------------------------------------------- | ------ | ---------------------------------------------------------------------------- | -------------- | -------- |
| `aspnetcore.header_parsing.header.name`       | string | The header name.                                                             | `Content-Type` | Always   |
| `aspnetcore.header_parsing.cache_access.type` | string | A value indicating whether the header's value was found in the cache or not. | `Hit`; `Miss`  | Always   |

## `Microsoft.AspNetCore.Server.Kestrel`

The `Microsoft.AspNetCore.Server.Kestrel` metrics report HTTP connection information from [ASP.NET Core Kestrel web server](/en-us/aspnet/core/fundamentals/servers/kestrel):

* [`kestrel.active_connections`](#metric-kestrelactive_connections)
* [`kestrel.connection.duration`](#metric-kestrelconnectionduration)
* [`kestrel.rejected_connections`](#metric-kestrelrejected_connections)
* [`kestrel.queued_connections`](#metric-kestrelqueued_connections)
* [`kestrel.queued_requests`](#metric-kestrelqueued_requests)
* [`kestrel.upgraded_connections`](#metric-kestrelupgraded_connections)
* [`kestrel.tls_handshake.duration`](#metric-kestreltls_handshakeduration)
* [`kestrel.active_tls_handshakes`](#metric-kestrelactive_tls_handshakes)

Note

In ASP.NET Core 11 and later, the Kestrel meter conforms to the [OpenTelemetry semantic conventions for Kestrel web server metrics](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/). Register it directly with the OpenTelemetry SDK (for example, call `AddMeter("Microsoft.AspNetCore.Server.Kestrel")` in your OpenTelemetry metrics pipeline) without adding the [`OpenTelemetry.Instrumentation.AspNetCore`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNetCore) package. For details, see [ASP.NET Core metrics](overview?view=aspnetcore-10.0).

### Metric: `kestrel.active_connections`

| Name                                                                                                                                        | Instrument Type | Unit (UCUM)    | Description                                                    |
| ------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------------- | -------------------------------------------------------------- |
| [`kestrel.active_connections`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/#metric-kestrelactive_connections) | UpDownCounter   | `{connection}` | Number of connections that are currently active on the server. |

| Attribute           | Type   | Description                                                                                                                                                                            | Examples            | Presence                            |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------- |
| `network.transport` | string | [OSI transport layer](https://www.geeksforgeeks.org/transport-layer-in-osi-model/) or [inter-process communication method](https://en.wikipedia.org/wiki/Inter-process_communication). | `tcp`; `unix`       | Always                              |
| `network.type`      | string | [OSI network layer](https://www.geeksforgeeks.org/network-layer-in-osi-model/) or non-OSI equivalent.                                                                                  | `ipv4`; `ipv6`      | If the transport is `tcp` or `udp`. |
| `server.address`    | string | Server address domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.                                                                  | `example.com`       | Always                              |
| `server.port`       | int    | Server port number                                                                                                                                                                     | `80`; `8080`; `443` | If the transport is `tcp` or `udp`. |

### Metric: `kestrel.connection.duration`

| Name                                                                                                                                         | Instrument Type | Unit (UCUM) | Description                                |
| -------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | ------------------------------------------ |
| [`kestrel.connection.duration`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/#metric-kestrelconnectionduration) | Histogram       | `s`         | The duration of connections on the server. |

| Attribute                  | Type   | Description                                                                                                                                                                                                                                                                                            | Examples                                                                           | Presence                                                               |
| -------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `error.type`               | string | Describes a type of error the connection ended with, or the unhandled exception type thrown during the connection pipeline. Known connection errors can be found at [Semantic Conventions for Kestrel web server metrics](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/). | `connection_reset`; `invalid_request_headers`; `System.OperationCanceledException` | If the connection ended with a known error or an exception was thrown. |
| `network.protocol.name`    | string | [OSI application layer](https://www.geeksforgeeks.org/application-layer-in-osi-model/) or non-OSI equivalent.                                                                                                                                                                                          | `http`; `web_sockets`                                                              | Always                                                                 |
| `network.protocol.version` | string | Version of the protocol specified in `network.protocol.name`.                                                                                                                                                                                                                                          | `1.1`; `2`                                                                         | Always                                                                 |
| `network.transport`        | string | [OSI transport layer](https://www.geeksforgeeks.org/transport-layer-in-osi-model/) or [inter-process communication method](https://en.wikipedia.org/wiki/Inter-process_communication).                                                                                                                 | `tcp`; `unix`                                                                      | Always                                                                 |
| `network.type`             | string | [OSI network layer](https://www.geeksforgeeks.org/network-layer-in-osi-model/) or non-OSI equivalent.                                                                                                                                                                                                  | `ipv4`; `ipv6`                                                                     | If the transport is `tcp` or `udp`.                                    |
| `server.address`           | string | Server address domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.                                                                                                                                                                                  | `example.com`                                                                      | Always                                                                 |
| `server.port`              | int    | Server port number                                                                                                                                                                                                                                                                                     | `80`; `8080`; `443`                                                                | If the transport is `tcp` or `udp`.                                    |
| `tls.protocol.version`     | string | TLS protocol version.                                                                                                                                                                                                                                                                                  | `1.2`; `1.3`                                                                       | If the connection is secured with TLS.                                 |

As this metric is tracking the connection duration, and ideally HTTP connections are used for multiple requests, the buckets should be longer than those used for request durations. For example, using \[ 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300] provides an upper bucket of 5 mins.

When a connection ends with a known error, the `error.type` attribute value is set to the known error type. Known connection errors can be found at [Semantic Conventions for Kestrel web server metrics](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/).

### Metric: `kestrel.rejected_connections`

| Name                                                                                                                                            | Instrument Type | Unit (UCUM)    | Description                                   |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------------- | --------------------------------------------- |
| [`kestrel.rejected_connections`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/#metric-kestrelrejected_connections) | Counter         | `{connection}` | Number of connections rejected by the server. |

| Attribute           | Type   | Description                                                                                                                                                                            | Examples            | Presence                            |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------- |
| `network.transport` | string | [OSI transport layer](https://www.geeksforgeeks.org/transport-layer-in-osi-model/) or [inter-process communication method](https://en.wikipedia.org/wiki/Inter-process_communication). | `tcp`; `unix`       | Always                              |
| `network.type`      | string | [OSI network layer](https://www.geeksforgeeks.org/network-layer-in-osi-model/) or non-OSI equivalent.                                                                                  | `ipv4`; `ipv6`      | If the transport is `tcp` or `udp`. |
| `server.address`    | string | Server address domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.                                                                  | `example.com`       | Always                              |
| `server.port`       | int    | Server port number                                                                                                                                                                     | `80`; `8080`; `443` | If the transport is `tcp` or `udp`. |

Connections are rejected when the currently active count exceeds the value configured with `MaxConcurrentConnections`.

### Metric: `kestrel.queued_connections`

| Name                                                                                                                                        | Instrument Type | Unit (UCUM)    | Description                                                               |
| ------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------------- | ------------------------------------------------------------------------- |
| [`kestrel.queued_connections`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/#metric-kestrelqueued_connections) | UpDownCounter   | `{connection}` | Number of connections that are currently queued and are waiting to start. |

| Attribute           | Type   | Description                                                                                                                                                                            | Examples            | Presence                            |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------- |
| `network.transport` | string | [OSI transport layer](https://www.geeksforgeeks.org/transport-layer-in-osi-model/) or [inter-process communication method](https://en.wikipedia.org/wiki/Inter-process_communication). | `tcp`; `unix`       | Always                              |
| `network.type`      | string | [OSI network layer](https://www.geeksforgeeks.org/network-layer-in-osi-model/) or non-OSI equivalent.                                                                                  | `ipv4`; `ipv6`      | If the transport is `tcp` or `udp`. |
| `server.address`    | string | Server address domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.                                                                  | `example.com`       | Always                              |
| `server.port`       | int    | Server port number                                                                                                                                                                     | `80`; `8080`; `443` | If the transport is `tcp` or `udp`. |

### Metric: `kestrel.queued_requests`

| Name                                                                                                                                  | Instrument Type | Unit (UCUM) | Description                                                                                                                |
| ------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------- |
| [`kestrel.queued_requests`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/#metric-kestrelqueued_requests) | UpDownCounter   | `{request}` | Number of HTTP requests on multiplexed connections (HTTP/2 and HTTP/3) that are currently queued and are waiting to start. |

| Attribute                  | Type   | Description                                                                                                                                                                            | Examples              | Presence                            |
| -------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ----------------------------------- |
| `network.protocol.name`    | string | [OSI application layer](https://www.geeksforgeeks.org/application-layer-in-osi-model/) or non-OSI equivalent.                                                                          | `http`; `web_sockets` | Always                              |
| `network.protocol.version` | string | Version of the protocol specified in `network.protocol.name`.                                                                                                                          | `1.1`; `2`            | Always                              |
| `network.transport`        | string | [OSI transport layer](https://www.geeksforgeeks.org/transport-layer-in-osi-model/) or [inter-process communication method](https://en.wikipedia.org/wiki/Inter-process_communication). | `tcp`; `unix`         | Always                              |
| `network.type`             | string | [OSI network layer](https://www.geeksforgeeks.org/network-layer-in-osi-model/) or non-OSI equivalent.                                                                                  | `ipv4`; `ipv6`        | If the transport is `tcp` or `udp`. |
| `server.address`           | string | Server address domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.                                                                  | `example.com`         | Always                              |
| `server.port`              | int    | Server port number                                                                                                                                                                     | `80`; `8080`; `443`   | If the transport is `tcp` or `udp`. |

### Metric: `kestrel.upgraded_connections`

| Name                                                                                                                                            | Instrument Type | Unit (UCUM)    | Description                                                     |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------------- | --------------------------------------------------------------- |
| [`kestrel.upgraded_connections`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/#metric-kestrelupgraded_connections) | UpDownCounter   | `{connection}` | Number of connections that are currently upgraded (WebSockets). |

| Attribute           | Type   | Description                                                                                                                                                                            | Examples            | Presence                            |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------- |
| `network.transport` | string | [OSI transport layer](https://www.geeksforgeeks.org/transport-layer-in-osi-model/) or [inter-process communication method](https://en.wikipedia.org/wiki/Inter-process_communication). | `tcp`; `unix`       | Always                              |
| `network.type`      | string | [OSI network layer](https://www.geeksforgeeks.org/network-layer-in-osi-model/) or non-OSI equivalent.                                                                                  | `ipv4`; `ipv6`      | If the transport is `tcp` or `udp`. |
| `server.address`    | string | Server address domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.                                                                  | `example.com`       | Always                              |
| `server.port`       | int    | Server port number                                                                                                                                                                     | `80`; `8080`; `443` | If the transport is `tcp` or `udp`. |

The counter only tracks HTTP/1.1 connections.

### Metric: `kestrel.tls_handshake.duration`

| Name                                                                                                                                               | Instrument Type | Unit (UCUM) | Description                                   |
| -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | --------------------------------------------- |
| [`kestrel.tls_handshake.duration`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/#metric-kestreltls_handshakeduration) | Histogram       | `s`         | The duration of TLS handshakes on the server. |

| Attribute              | Type   | Description                                                                                                                                                                            | Examples                                                   | Presence                               |
| ---------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------- |
| `error.type`           | string | The full name of exception type.                                                                                                                                                       | `System.OperationCanceledException`; `Contoso.MyException` | If an exception was thrown.            |
| `network.transport`    | string | [OSI transport layer](https://www.geeksforgeeks.org/transport-layer-in-osi-model/) or [inter-process communication method](https://en.wikipedia.org/wiki/Inter-process_communication). | `tcp`; `unix`                                              | Always                                 |
| `network.type`         | string | [OSI network layer](https://www.geeksforgeeks.org/network-layer-in-osi-model/) or non-OSI equivalent.                                                                                  | `ipv4`; `ipv6`                                             | If the transport is `tcp` or `udp`.    |
| `server.address`       | string | Server address domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.                                                                  | `example.com`                                              | Always                                 |
| `server.port`          | int    | Server port number                                                                                                                                                                     | `80`; `8080`; `443`                                        | If the transport is `tcp` or `udp`.    |
| `tls.protocol.version` | string | TLS protocol version.                                                                                                                                                                  | `1.2`; `1.3`                                               | If the connection is secured with TLS. |

When using OpenTelemetry, the default buckets for this metric are set to \[ 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 ].

### Metric: `kestrel.active_tls_handshakes`

| Name                                                                                                                                              | Instrument Type | Unit (UCUM)   | Description                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------- | ---------------------------------------------------------------------- |
| [`kestrel.active_tls_handshakes`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-kestrel-metrics/#metric-kestrelactive_tls_handshakes) | UpDownCounter   | `{handshake}` | Number of TLS handshakes that are currently in progress on the server. |

| Attribute           | Type   | Description                                                                                                                                                                            | Examples            | Presence                            |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------- |
| `network.transport` | string | [OSI transport layer](https://www.geeksforgeeks.org/transport-layer-in-osi-model/) or [inter-process communication method](https://en.wikipedia.org/wiki/Inter-process_communication). | `tcp`; `unix`       | Always                              |
| `network.type`      | string | [OSI network layer](https://www.geeksforgeeks.org/network-layer-in-osi-model/) or non-OSI equivalent.                                                                                  | `ipv4`; `ipv6`      | If the transport is `tcp` or `udp`. |
| `server.address`    | string | Server address domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.                                                                  | `example.com`       | Always                              |
| `server.port`       | int    | Server port number                                                                                                                                                                     | `80`; `8080`; `443` | If the transport is `tcp` or `udp`. |

## `Microsoft.AspNetCore.Http.Connections`

The `Microsoft.AspNetCore.Http.Connections` metrics report connection information from [ASP.NET Core SignalR](/en-us/aspnet/core/signalr/introduction):

* [`signalr.server.connection.duration`](#metric-signalrserverconnectionduration)
* [`signalr.server.active_connections`](#metric-signalrserveractive_connections)

### Metric: `signalr.server.connection.duration`

| Name                                                                                                                                                      | Instrument Type | Unit (UCUM) | Description                                |
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- | ------------------------------------------ |
| [`signalr.server.connection.duration`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-signalr-metrics/#metric-signalrserverconnectionduration) | Histogram       | `s`         | The duration of connections on the server. |

| Attribute                   | Type   | Description                                                                                                           | Examples                      | Presence |
| --------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------- | ----------------------------- | -------- |
| `signalr.connection.status` | string | SignalR HTTP connection closure status.                                                                               | `app_shutdown`; `timeout`     | Always   |
| `signalr.transport`         | string | [SignalR transport type](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/TransportProtocols.md) | `web_sockets`; `long_polling` | Always   |

| Value            | Description                                                 |
| ---------------- | ----------------------------------------------------------- |
| `normal_closure` | The connection was closed normally.                         |
| `timeout`        | The connection was closed due to a timeout.                 |
| `app_shutdown`   | The connection was closed because the app is shutting down. |

`signalr.transport` is one of the following:

| Value                | Description                                                                                                  |
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
| `server_sent_events` | [server-sent events](https://developer.mozilla.org/docs/Web/API/Server-sent_events/Using_server-sent_events) |
| `long_polling`       | [Long Polling](/en-us/archive/msdn-magazine/2012/april/cutting-edge-long-polling-and-signalr)                |
| `web_sockets`        | [WebSocket](https://datatracker.ietf.org/doc/html/rfc6455)                                                   |

As this metric is tracking the connection duration, and ideally SignalR connections are durable, the buckets should be longer than those used for request durations. For example, using \[0, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300] provides an upper bucket of 5 mins.

### Metric: `signalr.server.active_connections`

| Name                                                                                                                                                     | Instrument Type | Unit (UCUM)    | Description                                                    |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------------- | -------------------------------------------------------------- |
| [`signalr.server.active_connections`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-signalr-metrics/#metric-signalrserveractive_connections) | UpDownCounter   | `{connection}` | Number of connections that are currently active on the server. |

| Attribute                   | Type   | Description                                                                                                           | Examples                      | Presence |
| --------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------- | ----------------------------- | -------- |
| `signalr.connection.status` | string | SignalR HTTP connection closure status.                                                                               | `app_shutdown`; `timeout`     | Always   |
| `signalr.transport`         | string | [SignalR transport type](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/TransportProtocols.md) | `web_sockets`; `long_polling` | Always   |

## Related content

* [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0)
* [ASP.NET Core built-in diagnostics metrics](diagnostics?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-02

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/cookie-sharing.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Share authentication cookies among ASP.NET apps

By [Rick Anderson](https://twitter.com/RickAndMSFT)

Websites often consist of individual web apps working together. To provide a single sign-on (SSO) experience, web apps within a site must share authentication cookies. To support this scenario, the data protection stack allows sharing Katana cookie authentication and ASP.NET Core cookie authentication tickets.

In the examples that follow:

* The authentication cookie name is set to a common value of `.AspNet.SharedCookie`.

* The `AuthenticationType` is set to `Identity.Application` either explicitly or by default.

* A common app name, `SharedCookieApp`, is used to enable the data protection system to share data protection keys.

* `Identity.Application` is used as the authentication scheme. Whatever scheme is used, it must be used consistently *within and across* the shared cookie apps either as the default scheme or by explicitly setting it. The scheme is used when encrypting and decrypting cookies, so a consistent scheme must be used across apps.

* A common [data protection key](data-protection/implementation/key-management?view=aspnetcore-10.0) storage location is used.

  * In ASP.NET Core apps, [PersistKeysToFileSystem](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.persistkeystofilesystem) is used to set the key storage location.
  * In .NET Framework apps, cookie authentication middleware uses an implementation of [DataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionprovider). `DataProtectionProvider` provides data protection services for the encryption and decryption of authentication cookie payload data. The `DataProtectionProvider` instance is isolated from the data protection system used by other parts of the app. [DataProtectionProvider.Create(System.IO.DirectoryInfo, Action\<IDataProtectionBuilder>)](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionprovider.create) accepts a [DirectoryInfo](/en-us/dotnet/api/system.io.directoryinfo) to specify the location for data protection key storage.

* `DataProtectionProvider` requires the [Microsoft.AspNetCore.DataProtection.Extensions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Extensions/) NuGet package:
  * In .NET Framework apps, add a package reference to [Microsoft.AspNetCore.DataProtection.Extensions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Extensions/).

* [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the common app name.

## Share authentication cookies with ASP.NET Core Identity

When using ASP.NET Core Identity:

* Data protection keys and the app name must be shared among apps. A common key storage location is provided to the [PersistKeysToFileSystem](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.persistkeystofilesystem) method in the following examples. Use [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) to configure a common shared app name (`SharedCookieApp` in the following examples). For more information, see [Configure ASP.NET Core Data Protection](data-protection/configuration/overview?view=aspnetcore-10.0).
* Use the [ConfigureApplicationCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.configureapplicationcookie) extension method to set up the data protection service for cookies.
* The default authentication type is `Identity.Application`.

In `Program.cs`:

```
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"c:\PATH TO COMMON KEY RING FOLDER"))
    .SetApplicationName("SharedCookieApp");

builder.Services.ConfigureApplicationCookie(options => {
    options.Cookie.Name = ".AspNet.SharedCookie";
});

var app = builder.Build();
```

**Note:** The preceding instructions don't work with `ITicketStore` (`CookieAuthenticationOptions.SessionStore`). For more information, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/21163).

For security reasons, authentication cookies are not compressed in ASP.NET Core. When using authentication cookies, developers should minimize the number of claim information included to just that necessary for their needs.

## Share authentication cookies without ASP.NET Core Identity

When using cookies directly without ASP.NET Core Identity, configure data protection and authentication. In the following example, the authentication type is set to `Identity.Application`:

```
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"c:\PATH TO COMMON KEY RING FOLDER"))
    .SetApplicationName("SharedCookieApp");

builder.Services.AddAuthentication("Identity.Application")
    .AddCookie("Identity.Application", options =>
    {
        options.Cookie.Name = ".AspNet.SharedCookie";
    });

var app = builder.Build();
```

For security reasons, authentication cookies are not compressed in ASP.NET Core. When using authentication cookies, developers should minimize the number of claim information included to just that necessary for their needs.

## Share cookies across different base paths

An authentication cookie uses the [HttpRequest.PathBase](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase#microsoft-aspnetcore-http-httprequest-pathbase) as its default [Cookie.Path](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.path#microsoft-aspnetcore-http-cookiebuilder-path). If the app's cookie must be shared across different base paths, `Path` must be overridden:

```
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"c:\PATH TO COMMON KEY RING FOLDER"))
    .SetApplicationName("SharedCookieApp");

builder.Services.ConfigureApplicationCookie(options => {
    options.Cookie.Name = ".AspNet.SharedCookie";
    options.Cookie.Path = "/";
});

var app = builder.Build();
```

## Share cookies across subdomains

When hosting apps that share cookies across subdomains, specify a common domain in the [Cookie.Domain](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.domain#microsoft-aspnetcore-http-cookiebuilder-domain) property. To share cookies across apps at `contoso.com`, such as `first_subdomain.contoso.com` and `second_subdomain.contoso.com`, specify the `Cookie.Domain` as `.contoso.com`:

```
options.Cookie.Domain = ".contoso.com";
```

## Encrypt data protection keys at rest

For production deployments, configure the `DataProtectionProvider` to encrypt keys at rest with DPAPI or an X509Certificate. For more information, see [Key encryption at rest in Windows and Azure using ASP.NET Core](data-protection/implementation/key-encryption-at-rest?view=aspnetcore-10.0). In the following example, a certificate thumbprint is provided to [ProtectKeysWithCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate):

```
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDataProtection()
    .ProtectKeysWithCertificate("{CERTIFICATE THUMBPRINT}");
```

## Use a common user database

When apps use the same Identity schema (same version of Identity), confirm that the Identity system for each app is pointed at the same user database. Otherwise, the identity system produces failures at runtime when it attempts to match the information in the authentication cookie against the information in its database.

When the Identity schema is different among apps, usually because apps are using different Identity versions, sharing a common database based on the latest version of Identity isn't possible without remapping and adding columns in other app's Identity schemas. It's often more efficient to upgrade the other apps to use the latest Identity version so that a common database can be shared by the apps.

## Application name change

In .NET 6, [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) normalizes the content root path to end with a [DirectorySeparatorChar](/en-us/dotnet/api/system.io.path.directoryseparatorchar#system-io-path-directoryseparatorchar). Most apps migrating from [HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder) or [WebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder) won't have the same app name because they aren't normalized. For more information, see [SetApplicationName](data-protection/configuration/overview?view=aspnetcore-10.0#set-the-application-name-setapplicationname)

## Share authentication cookies between ASP.NET 4.x and ASP.NET Core apps

ASP.NET 4.x apps that use Microsoft.Owin cookie authentication middleware can be configured to generate authentication cookies that are compatible with the ASP.NET Core cookie authentication middleware. This can be useful if a web application consists of both ASP.NET 4.x apps and ASP.NET Core apps that must share a single sign-on experience. A specific example of such a scenario is [incrementally migrating](../migration/fx-to-core/?view=aspnetcore-10.0) a web app from ASP.NET to ASP.NET Core. In such scenarios, it's common for some parts of an app to be served by the original ASP.NET app while others are served by the new ASP.NET Core app. Users should only have to sign in once, though. This can be accomplished by either of the following approaches:

* Using the System.Web adapters' [remote authentication](../migration/fx-to-core/areas/authentication?view=aspnetcore-10.0#remote-authentication) feature, which uses the ASP.NET app to sign users in.
* Configuring the ASP.NET app to use Microsoft.Owin cookie authentication middleware so that authentication cookies are shared with the ASP.NET Core app.

To configure ASP.NET Microsoft.Owin cookie authentication middleware to share cookies with an ASP.NET Core app, follow the preceding instructions to configure the ASP.NET Core app to use a specific cookie name, app name, and to persist data protection keys to a well-known location. See [Configure ASP.NET Core Data Protection](data-protection/configuration/overview?view=aspnetcore-10.0) for more information on persisting data protection keys.

In the ASP.NET app, install the [`Microsoft.Owin.Security.Interop`](https://www.nuget.org/packages/Microsoft.Owin.Security.Interop/) package.

Update the `UseCookieAuthentication` call in Startup.Auth.cs to configure an AspNetTicketDataFormat to match the ASP.NET Core app's settings:

```
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    { 
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(30),
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    },

    // Settings to configure shared cookie with ASP.NET Core app
    CookieName = ".AspNet.ApplicationCookie",
    AuthenticationType = "Identity.Application",                
    TicketDataFormat = new AspNetTicketDataFormat(
        new DataProtectorShim(
            DataProtectionProvider.Create(new DirectoryInfo(@"c:\PATH TO COMMON KEY RING FOLDER"),
            builder => builder.SetApplicationName("SharedCookieApp"))
            .CreateProtector(
                "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
                // Must match the Scheme name used in the ASP.NET Core app, i.e. IdentityConstants.ApplicationScheme
                "Identity.Application",
                "v2"))),
    CookieManager = new ChunkingCookieManager()
});
```

Important items configured here include:

* The cookie name is set to the same name as in the ASP.NET Core app.
* A data protection provider is created using the same key ring path. Note that in these examples, data protection keys are stored on disk but other data protection providers can be used. For example, Redis or Azure Blob Storage can be used for data protection providers as long as the configuration matches between the apps. See [Configure ASP.NET Core Data Protection](data-protection/configuration/overview?view=aspnetcore-10.0) for more information on persisting data protection keys.
* The app name is set to be the same as the app name used in the ASP.NET Core app.
* The authentication type is set to the name of the authentication scheme in the ASP.NET Core app.
* `System.Web.Helpers.AntiForgeryConfig.UniqueClaimTypeIdentifier` is set to a claim from the ASP.NET Core identity that will be unique to a user.

Because the authentication type was changed to match the authentication scheme of the ASP.NET Core app, it's also necessary to update how the ASP.NET app generates new identities to use that same name. This is typically done in `Models/IdentityModels.cs`:

```
public class ApplicationUser : IdentityUser
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, "Identity.Application");
        
        // Add custom user claims here
        return userIdentity;
    }
}
```

With these changes, the ASP.NET and ASP.NET Core apps are able to use the same authentication cookies so that users signing in or out of one app are reflected in the other app.

Note that because there are differences between ASP.NET Identity and ASP.NET Core Identity's database schemas, it is recommended that users only *sign-in* using one of the apps - either the ASP.NET or ASP.NET Core app. Once users are signed in, the steps documented in this section will allow the authentication cookie to be *used* by either app and both apps should be able to log users out.

## Additional resources

* [Host ASP.NET Core in a web farm](../host-and-deploy/web-farm?view=aspnetcore-10.0)
* [A primer on OWIN cookie authentication middleware for the ASP.NET developer](https://brockallen.com/2013/10/24/a-primer-on-owin-cookie-authentication-middleware-for-the-asp-net-developer/) by [Brock Allen](https://brockallen.com/)
* [OWIN authentication middleware Architecture](https://brockallen.com/2013/08/07/owin-authentication-middleware-architecture/) by [Brock Allen](https://brockallen.com/)
* [Posts from the 'OWIN / Katana' Category](https://brockallen.com/category/owin-katana/) by [Brock Allen](https://brockallen.com/)

In the examples that follow:

* The authentication cookie name is set to a common value of `.AspNet.SharedCookie`.

* The `AuthenticationType` is set to `Identity.Application` either explicitly or by default.

* A common app name is used to enable the data protection system to share data protection keys (`SharedCookieApp`).

* `Identity.Application` is used as the authentication scheme. Whatever scheme is used, it must be used consistently *within and across* the shared cookie apps either as the default scheme or by explicitly setting it. The scheme is used when encrypting and decrypting cookies, so a consistent scheme must be used across apps.

* A common [data protection key](data-protection/implementation/key-management?view=aspnetcore-10.0) storage location is used.

  * In ASP.NET Core apps, [PersistKeysToFileSystem](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.persistkeystofilesystem) is used to set the key storage location.
  * In .NET Framework apps, cookie authentication middleware uses an implementation of [DataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionprovider). `DataProtectionProvider` provides data protection services for the encryption and decryption of authentication cookie payload data. The `DataProtectionProvider` instance is isolated from the data protection system used by other parts of the app. [DataProtectionProvider.Create(System.IO.DirectoryInfo, Action\<IDataProtectionBuilder>)](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionprovider.create) accepts a [DirectoryInfo](/en-us/dotnet/api/system.io.directoryinfo) to specify the location for data protection key storage.

* `DataProtectionProvider` requires the [Microsoft.AspNetCore.DataProtection.Extensions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Extensions/) NuGet package:

  * In ASP.NET Core 2.x apps, reference the [Microsoft.AspNetCore.App metapackage](../fundamentals/metapackage-app?view=aspnetcore-10.0).
  * In .NET Framework apps, add a package reference to [Microsoft.AspNetCore.DataProtection.Extensions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Extensions/).

* [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) sets the common app name.

## Share authentication cookies with ASP.NET Core Identity

When using ASP.NET Core Identity:

* Data protection keys and the app name must be shared among apps. A common key storage location is provided to the [PersistKeysToFileSystem](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.persistkeystofilesystem) method in the following examples. Use [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname) to configure a common shared app name (`SharedCookieApp` in the following examples). For more information, see [Configure ASP.NET Core Data Protection](data-protection/configuration/overview?view=aspnetcore-10.0).
* Use the [ConfigureApplicationCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.configureapplicationcookie) extension method to set up the data protection service for cookies.
* The default authentication type is `Identity.Application`.

In `Startup.ConfigureServices`:

```
services.AddDataProtection()
    .PersistKeysToFileSystem("{PATH TO COMMON KEY RING FOLDER}")
    .SetApplicationName("SharedCookieApp");

services.ConfigureApplicationCookie(options => {
    options.Cookie.Name = ".AspNet.SharedCookie";
});
```

**Note:** The preceding instructions don't work with `ITicketStore` (`CookieAuthenticationOptions.SessionStore`). For more information, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/21163).

For security reasons, authentication cookies are not compressed in ASP.NET Core. When using authentication cookies, developers should minimize the number of claim information included to just that necessary for their needs.

## Share authentication cookies without ASP.NET Core Identity

When using cookies directly without ASP.NET Core Identity, configure data protection and authentication in `Startup.ConfigureServices`. In the following example, the authentication type is set to `Identity.Application`:

```
services.AddDataProtection()
    .PersistKeysToFileSystem("{PATH TO COMMON KEY RING FOLDER}")
    .SetApplicationName("SharedCookieApp");

services.AddAuthentication("Identity.Application")
    .AddCookie("Identity.Application", options =>
    {
        options.Cookie.Name = ".AspNet.SharedCookie";
    });
```

For security reasons, authentication cookies are not compressed in ASP.NET Core. When using authentication cookies, developers should minimize the number of claim information included to just that necessary for their needs.

## Share cookies across different base paths

An authentication cookie uses the [HttpRequest.PathBase](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase#microsoft-aspnetcore-http-httprequest-pathbase) as its default [Cookie.Path](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.path#microsoft-aspnetcore-http-cookiebuilder-path). If the app's cookie must be shared across different base paths, `Path` must be overridden:

```
services.AddDataProtection()
    .PersistKeysToFileSystem("{PATH TO COMMON KEY RING FOLDER}")
    .SetApplicationName("SharedCookieApp");

services.ConfigureApplicationCookie(options => {
    options.Cookie.Name = ".AspNet.SharedCookie";
    options.Cookie.Path = "/";
});
```

## Share cookies across subdomains

When hosting apps that share cookies across subdomains, specify a common domain in the [Cookie.Domain](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.domain#microsoft-aspnetcore-http-cookiebuilder-domain) property. To share cookies across apps at `contoso.com`, such as `first_subdomain.contoso.com` and `second_subdomain.contoso.com`, specify the `Cookie.Domain` as `.contoso.com`:

```
options.Cookie.Domain = ".contoso.com";
```

## Encrypt data protection keys at rest

For production deployments, configure the `DataProtectionProvider` to encrypt keys at rest with DPAPI or an X509Certificate. For more information, see [Key encryption at rest in Windows and Azure using ASP.NET Core](data-protection/implementation/key-encryption-at-rest?view=aspnetcore-10.0). In the following example, a certificate thumbprint is provided to [ProtectKeysWithCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate):

```
services.AddDataProtection()
    .ProtectKeysWithCertificate("{CERTIFICATE THUMBPRINT}");
```

## Share authentication cookies between ASP.NET 4.x and ASP.NET Core apps

ASP.NET 4.x apps that use Katana cookie authentication middleware can be configured to generate authentication cookies that are compatible with the ASP.NET Core cookie authentication middleware. For more information, see [Share authentication cookies between ASP.NET 4.x and ASP.NET Core apps (dotnet/AspNetCore.Docs #21987)](https://github.com/dotnet/AspNetCore.Docs/issues/21987).

## Use a common user database

When apps use the same Identity schema (same version of Identity), confirm that the Identity system for each app is pointed at the same user database. Otherwise, the identity system produces failures at runtime when it attempts to match the information in the authentication cookie against the information in its database.

When the Identity schema is different among apps, usually because apps are using different Identity versions, sharing a common database based on the latest version of Identity isn't possible without remapping and adding columns in other app's Identity schemas. It's often more efficient to upgrade the other apps to use the latest Identity version so that a common database can be shared by the apps.

## Additional resources

* [Host ASP.NET Core in a web farm](../host-and-deploy/web-farm?view=aspnetcore-10.0)
* [A primer on OWIN cookie authentication middleware for the ASP.NET developer](https://brockallen.com/2013/10/24/a-primer-on-owin-cookie-authentication-middleware-for-the-asp-net-developer/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/extensibility-transforms
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/extensibility-transforms.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Request and Response Transform Extensibility

## Introduction

When proxying a request it's common to modify parts of the request or response to adapt to the destination server's requirements or to flow additional data such as the client's original IP address. This process is implemented via Transforms. Types of transforms are defined globally for the application and then individual routes supply the parameters to enable and configure those transforms. The original request objects are not modified by these transforms, only the proxy requests.

YARP includes a set of built-in request and response transforms that can be used. For more information, see [YARP Request and Response Transforms](transforms?view=aspnetcore-10.0). If those transforms are not sufficient, then custom transforms can be added.

## `RequestTransform`

All request transforms must derive from the abstract base class [`RequestTransform`](transforms?view=aspnetcore-10.0). These can freely modify the proxy `HttpRequestMessage`. Avoid reading or modifying the request body as this may disrupt the proxying flow. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and ease of use.

A request transform may conditionally produce an immediate response such as for error conditions. This prevents any remaining transforms from running and the request from being proxied. This is indicated by setting the `HttpResponse.StatusCode` to a value other than 200, or calling `HttpResponse.StartAsync()`, or writing to the `HttpResponse.Body` or `BodyWriter`.

[AddRequestTransform](/en-us/dotnet/api/yarp.reverseproxy.transforms.transformbuildercontextfuncextensions.addrequesttransform) is a `TransformBuilderContext` extension method that defines a request transform as a `Func<RequestTransformContext, ValueTask>`. This allows creating a custom request transform without implementing a `RequestTransform` derived class.

## `ResponseTransform`

All response transforms must derive from the abstract base class [ResponseTransform](/en-us/dotnet/api/yarp.reverseproxy.transforms.responsetransform). These can freely modify the client `HttpResponse`. Avoid reading or modifying the response body as this may disrupt the proxying flow. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use.

[AddResponseTransform](/en-us/dotnet/api/yarp.reverseproxy.transforms.transformbuildercontextfuncextensions.addresponsetransform) is a `TransformBuilderContext` extension method that defines a response transform as a `Func<ResponseTransformContext, ValueTask>`. This allows creating a custom response transform without implementing a `ResponseTransform` derived class.

## `ResponseTrailersTransform`

All response trailers transforms must derive from the abstract base class [ResponseTrailersTransform](/en-us/dotnet/api/yarp.reverseproxy.transforms.responsetrailerstransform). These can freely modify the client HttpResponse trailers. These run after the response body and should not attempt to modify the response headers or body. Consider also adding a parametrized extension method on `TransformBuilderContext` for discoverability and easy of use.

[AddResponseTrailersTransform](/en-us/dotnet/api/yarp.reverseproxy.transforms.transformbuildercontextfuncextensions.addresponsetrailerstransform) is a `TransformBuilderContext` extension method that defines a response trailers transform as a `Func<ResponseTrailersTransformContext, ValueTask>`. This allows creating a custom response trailers transform without implementing a `ResponseTrailersTransform` derived class.

## Request body transforms

YARP does not provide any built in transforms for modifying the request body. However, the body can be modified by custom transforms.

Be careful about which kinds of requests are modified, how much data gets buffered, enforcing timeouts, parsing untrusted input, and updating the body-related headers like `Content-Length`.

The below example uses simple, inefficient buffering to transform requests. A more efficient implementation would wrap and replace `HttpContext.Request.Body` with a stream that performed the needed modifications as data was proxied from client to server. That would also require removing the Content-Length header since the final length would not be known in advance.

This sample requires YARP 1.1, see <https://github.com/microsoft/reverse-proxy/pull/1569>.

```
.AddTransforms(context =>
{
    context.AddRequestTransform(async requestContext =>
    {
        using var reader =
            new StreamReader(requestContext.HttpContext.Request.Body);
        // TODO: size limits, timeouts
        var body = await reader.ReadToEndAsync();
        if (!string.IsNullOrEmpty(body))
        {
            body = body.Replace("Alpha", "Charlie");
            var bytes = Encoding.UTF8.GetBytes(body);
            // Change Content-Length to match the modified body, or remove it
            requestContext.HttpContext.Request.Body = new MemoryStream(bytes);
            // Request headers are copied before transforms are invoked, update any
            // needed headers on the ProxyRequest
            requestContext.ProxyRequest.Content.Headers.ContentLength =
                bytes.Length;
        }
    });
});
```

Custom transforms can only modify a request body if one is already present. They can't add a new body to a request that doesn't have one (for example, a POST request without a body or a GET request). If you need to add a body for a specific HTTP method and route, you must do so in [middleware](../../middleware/?view=aspnetcore-10.0) that runs before YARP, not in a transform.

The following middleware demonstrates how to add a body to a request that doesn't have one:

```
public class AddRequestBodyMiddleware
{
    private readonly RequestDelegate _next;

    public AddRequestBodyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Only modify specific route and method
        if (context.Request.Method == HttpMethods.Get &&
            context.Request.Path == "/special-route")
        {
            var bodyContent = "key=value";
            var bodyBytes = Encoding.UTF8.GetBytes(bodyContent);

            // Create a new request body
            context.Request.Body = new MemoryStream(bodyBytes);
            context.Request.ContentLength = bodyBytes.Length;

            // Replace IHttpRequestBodyDetectionFeature so YARP knows
            // a body is present
            context.Features.Set<IHttpRequestBodyDetectionFeature>(
                new CustomBodyDetectionFeature());
        }

        await _next(context);
    }

    // Helper class to indicate the request can have a body
    private class CustomBodyDetectionFeature : IHttpRequestBodyDetectionFeature
    {
        public bool CanHaveBody => true;
    }
}
```

Note

You can use `context.GetRouteModel().Config.RouteId` in middleware to conditionally apply this logic for specific YARP routes.

## Response body transforms

YARP does not provide any built in transforms for modifying the response body. However, the body can be modified by custom transforms.

Be careful about which kinds of responses are modified, how much data gets buffered, enforcing timeouts, parsing untrusted input, and updating the body-related headers like `Content-Length`. You may need to decompress content before modifying it, as indicated by the Content-Encoding header, and afterwards re-compress it or remove the header.

The below example uses simple, inefficient buffering to transform responses. A more efficient implementation would wrap the stream returned by `ReadAsStreamAsync()` with a stream that performed the needed modifications as data was proxied from client to server. That would also require removing the Content-Length header since the final length would not be known in advance.

```
.AddTransforms(context =>
{
    context.AddResponseTransform(async responseContext =>
    {
        var stream =
            await responseContext.ProxyResponse.Content.ReadAsStreamAsync();
        using var reader = new StreamReader(stream);
        // TODO: size limits, timeouts
        var body = await reader.ReadToEndAsync();

        if (!string.IsNullOrEmpty(body))
        {
            responseContext.SuppressResponseBody = true;

            body = body.Replace("Bravo", "Charlie");
            var bytes = Encoding.UTF8.GetBytes(body);
            // Change Content-Length to match the modified body, or remove it
            responseContext.HttpContext.Response.ContentLength = bytes.Length;
            // Response headers are copied before transforms are invoked, update
            // any needed headers on the HttpContext.Response
            await responseContext.HttpContext.Response.Body.WriteAsync(bytes);
        }
    });
});
```

## `ITransformProvider`

[ITransformProvider](/en-us/dotnet/api/yarp.reverseproxy.transforms.builder.itransformprovider) provides the functionality of `AddTransforms` described above as well as DI integration and validation support.

`ITransformProvider`'s can be registered in DI by calling [AddTransforms](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.reverseproxyservicecollectionextensions.addtransforms). Multiple `ITransformProvider` implementations can be registered and all will be run.

`ITransformProvider` has two methods, `Validate` and `Apply`. `Validate` gives you the opportunity to inspect the route for any parameters that are needed to configure a transform, such as custom metadata, and to return validation errors on the context if any needed values are missing or invalid. The `Apply` method provides the same functionality as AddTransform as discussed above, adding and configuring transforms per route.

```
services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddTransforms<MyTransformProvider>();
```

```
internal class MyTransformProvider : ITransformProvider
{
    public void ValidateRoute(TransformRouteValidationContext context)
    {
        // Check all routes for a custom property and validate the associated
        // transform data
        if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ??
            false)
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomMetadata value is required"));
            }
        }
    }

    public void ValidateCluster(TransformClusterValidationContext context)
    {
        // Check all clusters for a custom property and validate the associated
        // transform data.
        if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var value)
            ?? false)
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomMetadata value is required"));
            }
        }
    }

    public void Apply(TransformBuilderContext transformBuildContext)
    {
        // Check all routes for a custom property and add the associated transform.
        if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata",
            out var value) ?? false)
            || (transformBuildContext.Cluster?.Metadata?.TryGetValue(
            "CustomMetadata", out value) ?? false))
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(
                    "A non-empty CustomMetadata value is required");
            }

            transformBuildContext.AddRequestTransform(transformContext =>
            {
                transformContext.ProxyRequest.Options.Set(
                    new HttpRequestOptionsKey<string>("CustomMetadata"), value);

                return default;
            });
        }
    }
}
```

## `ITransformFactory`

Developers that want to integrate their custom transforms with the `Transforms` section of configuration can implement an [ITransformFactory](/en-us/dotnet/api/yarp.reverseproxy.transforms.builder.itransformfactory). This should be registered in DI using the `AddTransformFactory<T>()` method. Multiple factories can be registered and all will be used.

`ITransformFactory` provides two methods, `Validate` and `Build`. These process one set of transform values at a time, represented by a `IReadOnlyDictionary<string, string>`.

The `Validate` method is called when loading a configuration to verify the contents and report all errors. Any reported errors will prevent the configuration from being applied.

The `Build` method takes the given configuration and produces the associated transform instances for the route.

```
services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddTransformFactory<MyTransformFactory>();
```

```
internal class MyTransformFactory : ITransformFactory
{
    public bool Validate(TransformRouteValidationContext context,
        IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("CustomTransform", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomTransform value is required"));
            }

            return true; // Matched
        }

        return false;
    }

    public bool Build(TransformBuilderContext context,
        IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("CustomTransform", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(
                    "A non-empty CustomTransform value is required");
            }

            context.AddRequestTransform(transformContext =>
            {
                transformContext.ProxyRequest.Options.Set(
                    new HttpRequestOptionsKey<string>("CustomTransform"), value);
                return default;
            });

            return true;
        }

        return false;
    }
}
```

`Validate` and `Build` return `true` if they've identified the given transform configuration as one that they own. A `ITransformFactory` may implement multiple transforms. Any `RouteConfig.Transforms` entries not handled by any `ITransformFactory` will be considered configuration errors and prevent the configuration from being applied.

Consider also adding parametrized extension methods on `RouteConfig` like `WithTransformQueryValue` to facilitate programmatic route construction.

```
public static RouteConfig WithTransformQueryValue(this RouteConfig routeConfig,
    string queryKey, string value, bool append = true)
{
    var type = append ? QueryTransformFactory.AppendKey :
        QueryTransformFactory.SetKey;
    return routeConfig.WithTransform(transform =>
    {
        transform[QueryTransformFactory.QueryValueParameterKey] = queryKey;
        transform[type] = value;
    });
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-23

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/movie-database-app/part-2
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/movie-database-app/part-2.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor movie database app (Part 2 - Add and scaffold a model)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article is the second part of the Blazor movie database app tutorial that teaches you the basics of building an ASP.NET Core Blazor Web App with features to manage a movie database.

In this part of the tutorial series:

* A class is added to the app representing a movie in a database.
* [Entity Framework Core (EF Core)](/en-us/ef/core) services and tooling create a database context and database. EF Core is an object-relational mapper (O/RM) that simplifies data access. You write model classes first, and EF Core creates the database from the model classes, which is called *scaffolding*.
* Additional tooling scaffolds a Razor component-based UI for interacting with the movie records in the database via the database context.

## Add a data model

In **Solution Explorer**, right-click the `BlazorWebAppMovies` project and select **Add** > **New Folder**. Name the folder `Models`.

Right-click the `Models` folder. Select **Add** > **Class**. Name the file `Movie.cs`. Use the following contents for the file.

Add a folder to the project named `Models`.

Add a class file to the `Models` folder named `Movie.cs`. Use the following contents for the file.

Add a folder to the project named `Models`.

Add a class file to the `Models` folder named `Movie.cs`. Use the following contents for the file.

`Models/Movie.cs`:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorWebAppMovies.Models;

public class Movie
{
    public int Id { get; set; }

    public string? Title { get; set; }

    public DateOnly ReleaseDate { get; set; }

    public string? Genre { get; set; }

    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

The `Movie` class contains:

* A record identifier property (`Id`), which is required by EF Core and the database to track records. In the database, the `Id` property is the database record's primary key.

* Other properties that describe aspects of a movie:

  * Title (`Title`)
  * Release date (`ReleaseDate`)
  * Genre (`Genre`)
  * Price (`Price`)

The question mark on a `string` type indicates that the property is nullable (it can hold a `null` value).

The EF Core database provider selects data types based on the .NET types of the model's properties. The provider also takes into account other metadata provided by [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations), which are a set of attribute classes placed above a model's property with the following format, where the `{ANNOTATION}` placeholder is the annotation name. You can place multiple annotations on the same line separated by commas inside the brackets, or you can place multiple annotations on separate lines, which is the approach adopted by this tutorial series.

```
[{ANNOTATION}]
```

The `Price` property in the `Movie` class file has two data annotations:

```
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
```

These annotations specify:

* That the property holds a currency data type.
* The database column is a decimal of 18 digits with two decimal places.

More information on data annotations, including adding data annotations for validation, is covered in a later part of the tutorial series.

Select **Build** > **Build Solution** from the menu bar or press `F6` on the keyboard. Confirm in the **Output** panel that the build succeeded.

## Add Nuget packages and tools

To add the `dotnet scaffold` tool, execute the following .NET CLI command in the **Terminal** (**Terminal** menu > **New Terminal**) opened to the project's root folder:

```
dotnet tool install --global Microsoft.dotnet-scaffold
```

Save any open files.

In the **Command Palette** (`Ctrl`+`Shift`+`P`), use the `.NET: Build` command to build the app.

Confirm that the app built successfully.

## Add Nuget packages and tools

To add the `dotnet scaffold` tool, execute the following .NET CLI command in a command shell opened to the project's root folder:

```
dotnet tool install --global Microsoft.dotnet-scaffold
```

Save any open files.

In a command shell opened to the project's root folder, execute the [`dotnet build`](/en-us/dotnet/core/tools/dotnet-build) command:

```
dotnet build
```

Confirm that the app built successfully.

## Scaffold the model

In this section, the `Movie` model is used to *scaffold* a database context and a UI for managing movies in the database. .NET scaffolding is a code generation framework for .NET applications. You use scaffolding to quickly add database and UI code that interacts with data models.

Right-click on the `Components/Pages` folder and select **Add** > **New Scaffolded Item**:

With the **Add New Scaffold Item** dialog open to **Installed** > **Common** > **Blazor** > **Razor Component**, select **Razor Components using Entity Framework (CRUD)**. Select the **Add** button.

*CRUD* is an acronym for Create, Read, Update, and Delete. The scaffolder produces create, edit, delete, details, and index components for the app.

Complete the **Add Razor Components using Entity Framework (CRUD)** dialog:

* The **Template** dropdown list includes other templates for specifically creating create, edit, delete, details, and list components. This dropdown list comes in handy when you only need to create a specific type of component scaffolded to a model class. Leave the **Template** dropdown list set to **CRUD** to scaffold a full set of components.
* In the **Model class** dropdown list, select **Movie (BlazorWebAppMovies.Models)**.
* For **DbContext class**, select the **+** (plus sign) button.
* In the **Add Data Context** modal dialog, the class name `BlazorWebAppMovies.Data.BlazorWebAppMoviesContext` is generated. Use the default generated value. Select the **Add** button.
* After the model dialog closes, the **Database provider** dropdown list defaults to **SQL Server**. When you're building apps in the future, you can select the appropriate provider for the database that you're using. The options include SQLite, PostgreSQL, and Azure Cosmos DB. Leave the **Database provider** dropdown list set to **SQL Server**.
* Select **Add**.

In the **Terminal** (**Terminal** menu > **New Terminal**) opened to the project's root directory, execute the following command. SQLite is used as the database for users adopting VS Code tooling for this tutorial series.

```
dotnet scaffold
```

When the initializing prompt appears, press `Enter` on the keyboard.

For the **Scaffolding Category**, use the arrow keys to select **Blazor**. Press `Enter`.

For the **Command Name**, use the arrow keys to select **Razor Components with EntityFrameworkCore (CRUD)**. Press `Enter`.

*CRUD* is an acronym for Create, Read, Update, and Delete. The scaffolder produces create, edit, delete, details, and index components for the app.

For the **.NET project file**, press `Enter` to accept the app's project file (`BlazorWebAppMovies.csproj`).

For the **Model Name**, confirm that **Movie** (`Movie` class) is selected. Press `Enter`.

For the **Data Context Class**, type `BlazorWebAppMoviesContext`. Press `Enter`.

For the **Database Provider**, use the arrow keys to select **sqlite-efcore**. Press `Enter`.

For **Page**, confirm that **CRUD** is selected. Press `Enter`.

When prompted to include prerelease packages, use the arrow keys to select **No**. Press `Enter`.

Executing the `dotnet scaffold` command adds the following tools and packages to the app:

* [Command-line interface (CLI) tools for EF Core](/en-us/ef/core/miscellaneous/cli/dotnet).
* [`dotnet scaffold` tooling](https://devblogs.microsoft.com/dotnet/introducing-dotnet-scaffold/).
* Design time tools for EF Core.
* The SQLite provider with the EF Core package as a dependency.
* [`Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore) to use the [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) extension method in the `Program` file, which captures database-related exceptions.

In a command shell opened to the project's root folder, execute the following command. SQLite is used as the database for users adopting .NET CLI tooling for this tutorial series.

```
dotnet scaffold
```

When the initializing prompt appears, press `Enter` on the keyboard.

For the **Scaffolding Category**, use the arrow keys to select **Blazor**. Press `Enter`.

For the **Command Name**, use the arrow keys to select **Razor Components with EntityFrameworkCore (CRUD)**. Press `Enter`.

*CRUD* is an acronym for Create, Read, Update, and Delete. The scaffolder produces create, edit, delete, details, and index components for the app.

For the **.NET project file**, press `Enter` to accept the app's project file (`BlazorWebAppMovies.csproj`).

For the **Model Name**, confirm that **Movie** (`Movie` class) is selected. Press `Enter`.

For the **Data Context Class**, type `BlazorWebAppMoviesContext`. Press `Enter`.

For the **Database Provider**, use the arrow keys to select **sqlite-efcore**. Press `Enter`.

For **Page**, confirm that **CRUD** is selected. Press `Enter`.

When prompted to include prerelease packages, use the arrow keys to select **No**. Press `Enter`.

Executing the `dotnet scaffold` command adds the following tools and packages to the app:

* [Command-line interface (CLI) tools for EF Core](/en-us/ef/core/miscellaneous/cli/dotnet).
* [`dotnet scaffold` tooling](https://devblogs.microsoft.com/dotnet/introducing-dotnet-scaffold/).
* Design time tools for EF Core.
* The SQLite provider with the EF Core package as a dependency.
* [`Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore) to use the [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) extension method in the `Program` file, which captures database-related exceptions.

The `appsettings.json` file is updated with the connection string used to connect to a local database. In the following example, the `{CONNECTION STRING}` is the connection string automatically generated by the scaffolder:

```
"ConnectionStrings": {
  "BlazorWebAppMoviesContext": "{CONNECTION STRING}"
}
```

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](../../security/?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

## Files created and updated by scaffolding

The scaffolding process creates the following component files and movie database context class file:

* `Components/Pages/MoviePages`

  * `Create.razor`: Creates new movie entities.
  * `Delete.razor`: Deletes a movie entity.
  * `Details.razor`: Shows movie entity details.
  * `Edit.razor`: Updates a movie entity.
  * `Index.razor`: Lists movie entities (records) in the database.

* `Data/BlazorWebAppMoviesContext.cs`: Database context file ([DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext)).

The component files in the `MoviePages` folder are described in greater detail in the next part of this tutorial. The database context is described later in this article.

If the `MoviePages` folder and assets aren't present after scaffolding, return to the [Scaffold the model](#scaffold-the-model) section and rescaffold the `Movie` model, making sure that you select the **Razor Components using Entity Framework (CRUD)** scaffolder under **Installed** > **Common** > **Blazor** > **Razor Component** in the **Add New Scaffold Item** dialog.

ASP.NET Core is built with dependency injection (DI), which is a software design pattern for achieving [Inversion of Control (IoC)](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#dependency-inversion) between classes and their dependencies. Services, such as the EF Core database context, are registered with DI during application startup. These services are injected into Razor components for use by the components.

The [`QuickGrid` component](/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid) is a Razor component for efficiently displaying data in tabular form. The scaffolder places a `QuickGrid` component in the `Index` component (`Components/Pages/Index.razor`) to display movie entities. Calling [AddQuickGridEntityFrameworkAdapter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkadapterservicecollectionextensions.addquickgridentityframeworkadapter) on the service collection adds an EF Core adapter for QuickGrid to recognize EF Core-supplied [IQueryable\<T>](/en-us/dotnet/api/system.linq.iqueryable-1) instances and to resolve database queries asynchronously for efficiency.

In combination with [UseDeveloperExceptionPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage), [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) captures database-related exceptions that can be resolved by using Entity Framework migrations. When these exceptions occur, an HTML response is generated with details about possible actions to resolve the issue.

The following code is added to the `Program` file by the scaffolder if the database provider is SQL Server. The first statement of the following code registers a database context factory in the DI container to create database context instances on demand. A database context is used to perform database operations, such as reading and updating database records.

```
builder.Services.AddDbContextFactory<BlazorWebAppMoviesContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("BlazorWebAppMoviesContext") ?? 
        throw new InvalidOperationException(
            "Connection string 'BlazorWebAppMoviesContext' not found.")));

builder.Services.AddQuickGridEntityFrameworkAdapter();

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
```

The first statement reads and validates the connection string, and the second registers a database context factory in the DI container. A database context is used to perform database operations, such as reading and updating database records.

```
var connectionString = 
    builder.Configuration.GetConnectionString("BlazorWebAppMoviesContext") ?? 
    throw new InvalidOperationException(
        "Connection string 'BlazorWebAppMoviesContext' not found.");

builder.Services.AddDbContextFactory<BlazorWebAppMoviesContext>(options => options.UseSqlite(connectionString));

builder.Services.AddQuickGridEntityFrameworkAdapter();

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
```

The following code is added to the `Program` file by the scaffolder if the database provider is SQLite. The first statement reads and validates the connection string, and the second statement registers a database context factory in the DI container to create database context instances on demand. A database context is used to perform database operations, such as reading and updating database records.

```
var connectionString = 
    builder.Configuration.GetConnectionString("BlazorWebAppMoviesContext") ?? 
    throw new InvalidOperationException(
        "Connection string 'BlazorWebAppMoviesContext' not found.");

builder.Services.AddDbContextFactory<BlazorWebAppMoviesContext>(options => options.UseSqlite(connectionString));

builder.Services.AddQuickGridEntityFrameworkAdapter();

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
```

## Create the initial database schema using EF Core's migration feature

The migrations feature in EF Core:

* Creates the initial database schema.
* Incrementally updates the database schema to keep it synchronized with the app's data model. Existing data in the database is preserved.

EF Core adopts the *code-first* approach for database design and maintenance:

* Entity classes are created and updated first in the app.
* The database is created and updated from the app's entity classes.

This is the reverse procedure of *database-first* approaches, where the database is designed, built, and updated first. Adopting EF Core's code-first approach aims to speed up the process of app development because most of the difficult and time-consuming database creation and management procedures are handled transparently by the EF Core tooling, so you can focus on app development.

In this section, [Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to issue EF Core commands that:

* Add an initial migration.
* Update the database using the initial migration.

In **Solution Explorer**, double-click **Connected Services**. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select the ellipsis (`...`) followed by **Add migration**:

Give the migration a **Migration name** of `InitialCreate`, which is a name that describes the migration. Wait for the database context to load in the **DbContext class names** field, which may take a few seconds. Select **Finish** to create the migration:

Select the **Close** button after the operation finishes.

Adding a migration generates code to create the initial database schema. The schema is based on the model specified in [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext).

Select the ellipsis (`...`) again followed by the **Update database** command:

The **Update database with the latest migration** dialog opens. Wait for the **DbContext class names** field to update and for prior migrations to load, which may take a few seconds. Select the **Finish** button:

The update database command executes the `Up` method migrations that haven't been applied in a migration code file created by the scaffolder. In this case, the command executes the `Up` method in the `Migrations/{TIME STAMP}_InitialCreate.cs` file, which creates the database. The `{TIME STAMP}` placeholder is a time stamp.

Select the **Close** button after the operation finishes.

Right-click the `BlazorWebAppMovies` project file (`BlazorWebAppMovies.csproj`) and select **Open in Integrated Terminal**.

The **Terminal** window opens with a command prompt at the project directory, which contains the `Program` file and the app's project file (`.csproj`).

Execute the following .NET CLI command to add an initial migration. The `migrations` command generates code to create the initial database schema. The schema is based on the model specified in [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext). The `InitialCreate` argument is used to name the migration. Any name can be used, but the convention is to use a name that describes the migration.

```
dotnet ef migrations add InitialCreate
```

After the preceding command completes, update the database with the `update` command. The `update` command executes the `Up` method on migrations that haven't been applied in a migration code file created by the scaffolder. In this case, the command executes the `Up` method migrations in the `Migrations/{TIME STAMP}_InitialCreate.cs` file, which creates the database. The `{TIME STAMP}` placeholder is a time stamp.

```
dotnet ef database update
```

From the project's root folder, execute the following .NET CLI command to add an initial migration. The `migrations` command generates code to create the initial database schema. The schema is based on the model specified in [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext). The `InitialCreate` argument is used to name the migration. Any name can be used, but the convention is to use a name that describes the migration.

```
dotnet ef migrations add InitialCreate
```

After the preceding command completes, update the database with the `update` command. The `update` command executes the `Up` method on migrations that haven't been applied in a migration code file created by the scaffolder. In this case, the command executes the `Up` method migrations in the `Migrations/{TIME STAMP}_InitialCreate.cs` file, which creates the database. The `{TIME STAMP}` placeholder is a time stamp.

```
dotnet ef database update
```

The database context `BlazorWebAppMoviesContext` (`Data/BlazorWebAppMoviesContext.cs`):

* Derives from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext).
* Specifies which entities are included in the data model.
* Coordinates EF Core functionality, such as CRUD operations, for the `Movie` model.
* Contains a [DbSet\<TEntity>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for the `Movie` entity set. In EF terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the connection string is read from the `appsettings.json` file.

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](../../security/?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

## Test the app

Run the app.

Add `/movies` to the URL in the browser's address bar to navigate to the movies `Index` page.

After the `Index` page loads, select the **Create New** link.

Add a movie to the database. In the following example, the classic sci-fi movie [*The Matrix*](https://www.warnerbros.com/movies/matrix) (©1999 [Warner Bros. Entertainment Inc.](https://www.warnerbros.com/)) is added as the first movie entry. Selecting the **Create** button adds the movie to the database:

When you select the **Create** button, the movie data is posted to the server and saved in the database.

In .NET 9, the Visual Studio debugger may break with a [NavigationException](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationexception) on the line that navigates back to the `Index` page:

To resolve this problem:

1. Deselect the checkbox for **Break when this exception type is user-handled**.
2. Select the **Continue** button in the menu bar to continue execution.

The exception isn't thrown when a [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) method is executed throughout the rest of the tutorial series.

In .NET 10 or later:

* Updates to the way that [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) manages navigation never result in a [NavigationException](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationexception).
* The Visual Studio debugger doesn't break when processing [NavigateTo](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto) calls with a valid endpoint.

When the app returns to the `Index` page, the added entity appears:

Open the `Edit` page. Edit the movie's record and save the changes.

Examine the `Delete` page, but don't delete *The Matrix* movie from the database. The presence of this movie record is valuable in the next step of the tutorial where rendered HTML is studied and some enhancements are made to the data displayed. If you already deleted the movie, add the movie to the database again using the `Create` page before proceeding to the next part of the tutorial series.

## Stop the app

Stop the app using either of the following approaches:

* Close the browser window.

* In Visual Studio, either:

  * Use the Stop button in Visual Studio's menu bar:

  * Press `Shift`+`F5` on the keyboard.

Stop the app using the following approach:

1. Close the browser window.

2. In VS Code, either:

   * From the **Run** menu, select **Stop Debugging**.
   * Press `Shift`+`F5` on the keyboard.

Stop the app using the following approach:

1. Close the browser window.
2. From the command shell, press `Ctrl`+`C`.

## Troubleshoot with the completed sample

If you run into a problem while following the tutorial that you can't resolve from the text, compare your code to the completed project in the Blazor samples repository:

[Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)

Select the latest version folder. The sample folder for this tutorial's project is named `BlazorWebAppMovies`.

## Related content

EF Core documentation:

* [Entity Framework Core](/en-us/ef/core/)
* [Entity Framework Core tools reference - .NET CLI](/en-us/ef/core/cli/dotnet)
* [Data Types](/en-us/ef/core/modeling/relational/data-types)
* [SaveChangesAsync](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechangesasync): The API document includes basic information on how entities are saved and change detection.
* [Environment-based `Startup` class and methods](../../../fundamentals/environments?view=aspnetcore-10.0#environment-based-startup-class-and-methods)
* [Dependency injection in ASP.NET Core](../../../fundamentals/dependency-injection?view=aspnetcore-10.0)
* [ASP.NET Core Blazor \`QuickGrid\` component](../../components/quickgrid?view=aspnetcore-10.0)

## Legal

[*The Matrix*](https://www.warnerbros.com/movies/matrix) is a copyright of [Warner Bros. Entertainment Inc.](https://www.warnerbros.com/).

## Next steps

[Previous: Create a Blazor Web App](part-1?view=aspnetcore-10.0) [Next: Learn about Razor components](part-3?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-19

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/70-80
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/70-to-80.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core in .NET 7 to .NET 8

This article explains how to update an existing ASP.NET Core in .NET 7 project to .NET 8.

## Update the .NET SDK version in `global.json`

If you rely on a [`global.json`](/en-us/dotnet/core/tools/global-json) file to target a specific .NET SDK version, update the `version` property to the .NET 8 SDK version that's installed. For example:

```
{
  "sdk": {
-    "version": "7.0.100"
+    "version": "8.0.100"
  }
}
```

## Update the target framework

Update the project file's [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks) to `net8.0`:

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

  <PropertyGroup>
-    <TargetFramework>net7.0</TargetFramework>
+    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

</Project>
```

## Update package references

In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.org/packages?q=Microsoft.AspNetCore.*), [`Microsoft.EntityFrameworkCore.*`](https://www.nuget.org/packages?q=Microsoft.EntityFrameworkCore.*), [`Microsoft.Extensions.*`](https://www.nuget.org/packages?q=Microsoft.Extensions.*), and [`System.Net.Http.Json`](https://www.nuget.org/packages/System.Net.Http.Json) package reference's `Version` attribute to 8.0.0 or later. For example:

```
<ItemGroup>
-   <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="7.0.12" />
-   <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" />
-   <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
-   <PackageReference Include="System.Net.Http.Json" Version="7.0.1" />
+   <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="8.0.0" />
+   <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
+   <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
+   <PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
</ItemGroup>
```

## Blazor

The following migration scenarios are covered:

* [Update a Blazor Server app](#update-a-blazor-server-app)
* [Adopt all Blazor Web App conventions](#adopt-all-blazor-web-app-conventions)
* [Convert a Blazor Server app into a Blazor Web App](#convert-a-blazor-server-app-into-a-blazor-web-app)
* [Update a Blazor WebAssembly app](#update-a-blazor-webassembly-app)
* [Convert a hosted Blazor WebAssembly app into a Blazor Web App](#convert-a-hosted-blazor-webassembly-app-into-a-blazor-web-app)
* [Update service and endpoint option configuration](#update-service-and-endpoint-option-configuration)
* [Drop Blazor Server with Yarp routing workaround](#drop-blazor-server-with-yarp-routing-workaround)
* [Migrate `CascadingValue` components in layout components](#migrate-cascadingvalue-components-in-layout-components)
* [Migrate the `BlazorEnableCompression` MSBuild property](#migrate-the-blazorenablecompression-msbuild-property)
* [Migrate the `<CascadingAuthenticationState>` component to cascading authentication state services](#migrate-the-cascadingauthenticationstate-component-to-cascading-authentication-state-services)
* [*New article*: HTTP caching issues during migration](#new-article-on-http-caching-issues)
* [*New article*: New article on class libraries with static server-side rendering (static SSR)](#new-article-on-class-libraries-with-static-server-side-rendering-static-ssr)
* [Discover components from additional assemblies](#discover-components-from-additional-assemblies)
* [Drop `[Parameter]` attribute when the parameter is supplied from a query string](#drop-parameter-attribute-when-the-parameter-is-supplied-from-a-query-string)
* [Blazor Server script fallback policy authorization](#blazor-server-script-fallback-policy-authorization)

For guidance on adding Blazor support to an ASP.NET Core app, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../blazor/components/integration?view=aspnetcore-10.0#add-blazor-support-to-an-aspnet-core-app).

### Update a Blazor Server app

We recommend using Blazor Web Apps in .NET 8, but Blazor Server is supported. To continue using Blazor Server with .NET 8, follow the guidance in the first three sections of this article:

* [Update the .NET SDK version in `global.json`](#update-the-net-sdk-version-in-globaljson)
* [Update the target framework](#update-the-target-framework)
* [Update package references](#update-package-references)

New Blazor features introduced for Blazor Web Apps aren't available to a Blazor Server app updated to run under .NET 8. If you wish to adopt the new .NET 8 Blazor features, follow the guidance in either of the following sections:

* [Adopt all Blazor Web App conventions](#adopt-all-blazor-web-app-conventions)
* [Convert a Blazor Server app into a Blazor Web App](#convert-a-blazor-server-app-into-a-blazor-web-app)

### Adopt all Blazor Web App conventions

To optionally adopt all of the new Blazor Web App conventions, we recommend the following process:

* Create a new app from the Blazor Web App project template. For more information, see [Tooling for ASP.NET Core Blazor](../blazor/tooling?view=aspnetcore-10.0).
* Move the your app's components and code to the new Blazor Web App, making modifications to adopt new features.
* Update the layout and styles of the Blazor Web App.

New .NET 8 features are covered in [What's new in ASP.NET Core in .NET 8](../release-notes/aspnetcore-8.0?view=aspnetcore-10.0#blazor). When updating an app from .NET 6 or earlier, see the migration and release notes (*What's new* articles) for intervening releases.

### Convert a Blazor Server app into a Blazor Web App

Blazor Server apps are supported in .NET 8 without any code changes. Use the following guidance to convert a Blazor Server app into an equivalent .NET 8 Blazor Web App, which makes all of the [new .NET 8 features](../release-notes/aspnetcore-8.0?view=aspnetcore-10.0#blazor) available.

Important

This section focuses on the minimal changes required to convert a .NET 7 Blazor Server app into a .NET 8 Blazor Web App. To adopt all of the new Blazor Web App conventions, follow the guidance in the [Adopt all Blazor Web App conventions](#adopt-all-blazor-web-app-conventions) section.

1. Follow the guidance in the first three sections of this article:

   * [Update the .NET SDK version in `global.json`](#update-the-net-sdk-version-in-globaljson)
   * [Update the target framework](#update-the-target-framework)
   * [Update package references](#update-package-references)

2. Move the contents of the `App` component (`App.razor`) to a new `Routes` component file (`Routes.razor`) added to the project's root folder. Leave the empty `App.razor` file in the app in the project's root folder.

3. Add an entry to the imports file (`_Imports.razor`) to make shorthand render modes available to the app:

   ```
   @using static Microsoft.AspNetCore.Components.Web.RenderMode
   ```

4. Move the content in the `_Host` page (`Pages/_Host.cshtml`) to the empty `App.razor` file. Proceed to make the following changes to the `App` component.

   Note

   In the following example, the project's namespace is `BlazorServerApp`. Adjust the namespace to match your project.

   Remove the following lines from the top of the file:

   ```
   - @page "/"
   - @using Microsoft.AspNetCore.Components.Web
   - @namespace BlazorServerApp.Pages
   - @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
   ```

   Replace the preceding lines with a line that injects an [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment) instance:

   ```
   @inject IHostEnvironment Env
   ```

   Remove the tilde (`~`) from the `href` of the `<base>` tag and replace with the base path for your app:

   ```
   - <base href="~/" />
   + <base href="/" />
   ```

   Remove the Component Tag Helper for the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component and replace it with the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component.

   Remove the following line:

   ```
   - <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
   ```

   Replace the preceding line with the following:

   ```
   <HeadOutlet @rendermode="InteractiveServer" />
   ```

   Remove the Component Tag Helper for the `App` component and replace it with the `Routes` component.

   Remove the following line:

   ```
   - <component type="typeof(App)" render-mode="ServerPrerendered" />
   ```

   Replace the preceding line with the following:

   ```
   <Routes @rendermode="InteractiveServer" />
   ```

   Note

   The preceding configuration assumes that the app's components adopt interactive server rendering. For more information, including how to adopt static server-side rendering (SSR), see [ASP.NET Core Blazor render modes](../blazor/components/render-modes?view=aspnetcore-10.0).

   Remove the Environment Tag Helpers for error UI and replace them with the following Razor markup.

   Remove the following lines:

   ```
   - <environment include="Staging,Production">
   -     An error has occurred. This application may no longer respond until reloaded.
   - </environment>
   - <environment include="Development">
   -     An unhandled exception has occurred. See browser dev tools for details.
   - </environment>
   ```

   Replace the preceding lines with the following:

   ```
   @if (Env.IsDevelopment())
   {
       <text>
           An unhandled exception has occurred. See browser dev tools for details.
       </text>
   }
   else
   {
       <text>
           An error has occurred. This app may no longer respond until reloaded.
       </text>
   }
   ```

   Change the Blazor script from `blazor.server.js` to `blazor.web.js`:

   ```
   - <script src="_framework/blazor.server.js"></script>
   + <script src="_framework/blazor.web.js"></script>
   ```

5. Delete the `Pages/_Host.cshtml` file.

6. Update `Program.cs`:

   Note

   In the following example, the project's namespace is `BlazorServerApp`. Adjust the namespace to match your project.

   Add a `using` statement to the top of the file for the project's namespace:

   ```
   using BlazorServerApp;
   ```

   Replace [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor) with [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) and a chained call to [AddInteractiveServerComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serverrazorcomponentsbuilderextensions.addinteractiveservercomponents).

   Remove the following line:

   ```
   - builder.Services.AddServerSideBlazor();
   ```

   Replace the preceding line with Razor component and interactive server component services. Calling [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) adds antiforgery services ([AddAntiforgery](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.antiforgeryservicecollectionextensions.addantiforgery)) by default.

   ```
   builder.Services.AddRazorComponents()
       .AddInteractiveServerComponents();
   ```

   Remove the following line:

   ```
   - app.MapBlazorHub();
   ```

   Replace the preceding line with a call to [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents), supplying the `App` component as the root component type, and add a chained call to [AddInteractiveServerRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.serverrazorcomponentsendpointconventionbuilderextensions.addinteractiveserverrendermode):

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

   Remove the following line:

   ```
   - app.MapFallbackToPage("/_Host");
   ```

   Remove routing middleware:

   ```
   - app.UseRouting();
   ```

   Add [antiforgery middleware](../blazor/security/?view=aspnetcore-10.0#antiforgery-support) to the request processing pipeline after the line that adds HTTPS redirection middleware (`app.UseHttpsRedirection`):

   ```
   app.UseAntiforgery();
   ```

   The preceding call to `app.UseAntiforgery` must be placed after calls, if present, to `app.UseAuthentication` and `app.UseAuthorization`. There's no need to explicitly add antiforgery services (`builder.Services.AddAntiforgery`), as they're added automatically by [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents), which was covered earlier.

7. If the Blazor Server app was configured to disable prerendering, you can continue to disable prerendering for the updated app. In the `App` component, change the value assigned to the `@rendermode` Razor directive attributes for the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) and `Routes` components.

   Change the value of the `@rendermode` directive attribute for both the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) and `Routes` components to disable prerendering:

   ```
   - @rendermode="InteractiveServer"
   + @rendermode="new InteractiveServerRenderMode(prerender: false)"
   ```

   For more information, see [ASP.NET Core Blazor render modes](../blazor/components/render-modes?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#prerendering).

### Update a Blazor WebAssembly app

Follow the guidance in the first three sections of this article:

* [Update the .NET SDK version in `global.json`](#update-the-net-sdk-version-in-globaljson)
* [Update the target framework](#update-the-target-framework)
* [Update package references](#update-package-references)

For apps that adopt [lazy assembly loading](../blazor/webassembly-lazy-load-assemblies?view=aspnetcore-10.0), change the file extension from `.dll` to `.wasm` in the app's implementation to reflect Blazor WebAssembly's adoption of [Webcil assembly packaging](../release-notes/aspnetcore-8.0?view=aspnetcore-10.0#web-friendly-webcil-packaging).

Prior to the release of .NET 8, guidance in [Deployment layout for ASP.NET Core hosted Blazor WebAssembly apps](../blazor/host-and-deploy/webassembly/deployment-layout?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0) addresses environments that block clients from downloading and executing DLLs with a multipart bundling approach. In .NET 8 or later, Blazor uses the Webcil file format to address this problem. Multipart bundling using the experimental NuGet package described by the *WebAssembly deployment layout* article isn't supported for Blazor apps in .NET 8 or later. If you desire to continue using the multipart bundle package in .NET 8 or later apps, you can use the guidance in the article to create your own multipart bundling NuGet package, but it won't be supported by Microsoft.

### Convert a hosted Blazor WebAssembly app into a Blazor Web App

Blazor WebAssembly apps are supported in .NET 8 without any code changes. Use the following guidance to convert an ASP.NET Core hosted Blazor WebAssembly app into an equivalent .NET 8 Blazor Web App, which makes all of the [new .NET 8 features](../release-notes/aspnetcore-8.0?view=aspnetcore-10.0#blazor) available.

Important

This section focuses on the minimal changes required to convert a .NET 7 ASP.NET Core hosted Blazor WebAssembly app into a .NET 8 Blazor Web App. To adopt all of the new Blazor Web App conventions, follow the guidance in the [Adopt all Blazor Web App conventions](#adopt-all-blazor-web-app-conventions) section.

1. Follow the guidance in the first three sections of this article:

   * [Update the .NET SDK version in `global.json`](#update-the-net-sdk-version-in-globaljson)
   * [Update the target framework](#update-the-target-framework)
   * [Update package references](#update-package-references)

   Important

   Using the preceding guidance, update the `.Client`, `.Server`, and `.Shared` projects of the solution.

2. In the `.Client` project file (`.csproj`), add the following MSBuild properties:

   ```
   <NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
   <StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
   ```

   Also in the `.Client` project file, remove the [`Microsoft.AspNetCore.Components.WebAssembly.DevServer`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.DevServer) package reference:

   ```
   - <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer"... />
   ```

3. Move the file content from the `.Client/wwwroot/index.html` file to a new `App` component file (`App.razor`) created at the root of the `.Server` project. After you move the file's contents, delete the `index.html` file.

   Rename `App.razor` in the `.Client` project to `Routes.razor`.

   In `Routes.razor`, update the value of the `AppAssembly` attribute to `typeof(Program).Assembly`.

4. In the `.Client` project, add an entry to the imports file (`_Imports.razor`) to make shorthand render modes available to the app:

   ```
   @using static Microsoft.AspNetCore.Components.Web.RenderMode
   ```

   Make a copy of the `.Client` project's imports file (`_Imports.razor`) and add it to the `.Server` project.

5. Make the following changes to the `App.razor` file:

   Replace the website's default website title (`<title>...</title>`) with a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component. Note the website title for use later and remove the title tags and title:

   ```
   - <title>...</title>
   ```

   Where you removed the title, place a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component assigning the Interactive WebAssembly render mode (prerendering disabled):

   ```
   <HeadOutlet @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
   ```

   Change the [CSS style bundle](../blazor/components/css-isolation?view=aspnetcore-10.0#css-isolation-bundling):

   ```
   - <link href="{CLIENT PROJECT ASSEMBLY NAME}.styles.css" rel="stylesheet">
   + <link href="{SERVER PROJECT ASSEMBLY NAME}.styles.css" rel="stylesheet">
   ```

   Placeholders in the preceding code:

   * `{CLIENT PROJECT ASSEMBLY NAME}`: Client project assembly name. Example: `BlazorSample.Client`
   * `{SERVER PROJECT ASSEMBLY NAME}`: Server project assembly name. Example: `BlazorSample.Server`

   Locate following `<div>...</div>` HTML markup:

   ```
   - <div id="app">
   -     ...
   - </div>
   ```

   Replace the preceding `<div>...</div>` HTML markup with the `Routes` component using the Interactive WebAssembly render mode (prerendering disabled):

   ```
   <Routes @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
   ```

   Update the `blazor.webassembly.js` script to `blazor.web.js`:

   ```
   - <script src="_framework/blazor.webassembly.js"></script>
   + <script src="_framework/blazor.web.js"></script>
   ```

6. Open the `.Client` project's layout file (`.Client/Shared/MainLayout.razor`) and add a [PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component with the website's default title (`{TITLE}` placeholder):

   ```
   <PageTitle>{TITLE}</PageTitle>
   ```

   Note

   Other layout files should also receive a [PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component with the default website title.

   For more information, see [Control head content in ASP.NET Core Blazor apps](../blazor/components/control-head-content?view=aspnetcore-10.0#set-a-page-title-for-components-via-a-layout).

7. Remove the following lines from `.Client/Program.cs`:

   ```
   - builder.RootComponents.Add<App>("#app");
   - builder.RootComponents.Add<HeadOutlet>("head::after");
   ```

8. Update `.Server/Program.cs`:

   Add Razor component and interactive WebAssembly component services to the project. Call [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) with a chained call to [AddInteractiveWebAssemblyComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addinteractivewebassemblycomponents). Calling [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) adds antiforgery services ([AddAntiforgery](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.antiforgeryservicecollectionextensions.addantiforgery)) by default.

   ```
   builder.Services.AddRazorComponents()
       .AddInteractiveWebAssemblyComponents();
   ```

   Add [antiforgery middleware](../blazor/security/?view=aspnetcore-10.0#antiforgery-support) to the request processing pipeline.

   Place the following line after the call to `app.UseHttpsRedirection`. The call to `app.UseAntiforgery` must be placed after calls, if present, to `app.UseAuthentication` and `app.UseAuthorization`. There's no need to explicitly add antiforgery services (`builder.Services.AddAntiforgery`), as they're added automatically by [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents), which was covered earlier.

   ```
   app.UseAntiforgery();
   ```

   Remove the following line:

   ```
   - app.UseBlazorFrameworkFiles();
   ```

   Remove the following line:

   ```
   - app.MapFallbackToFile("index.html");
   ```

   Replace the preceding line with a call to [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents), supplying the `App` component as the root component type, and add chained calls to [AddInteractiveWebAssemblyRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.builder.webassemblyrazorcomponentsendpointconventionbuilderextensions.addinteractivewebassemblyrendermode) and [AddAdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointconventionbuilderextensions.addadditionalassemblies):

   ```
   app.MapRazorComponents<App>()
       .AddInteractiveWebAssemblyRenderMode()
       .AddAdditionalAssemblies(typeof({CLIENT APP NAMESPACE}._Imports).Assembly);
   ```

   In the preceding example, the `{CLIENT APP NAMESPACE}` placeholder is the namespace of the `.Client` project (for example, `HostedBlazorApp.Client`).

9. Run the solution from the `.Server` project:

   For Visual Studio, confirm that the `.Server` project is selected in **Solution Explorer** when running the app.

   If using the .NET CLI, run the project from the `.Server` project's folder.

### Update service and endpoint option configuration

With the release of Blazor Web Apps in .NET 8, Blazor service and endpoint option configuration is updated with the introduction of new API for interactive component services and component endpoint configuration.

Updated configuration guidance appears in the following locations:

* [Setting and reading the app's environment](../blazor/fundamentals/environments?view=aspnetcore-10.0): Contains updated guidance, especially in the section titled *Read the environment client-side in a Blazor Web App*.
* [Server-side circuit handler options](../blazor/fundamentals/signalr?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#server-side-circuit-handler-options): Covers new Blazor-SignalR circuit and hub options configuration.
* [Render Razor components from JavaScript](../blazor/components/js-spa-frameworks?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#render-razor-components-from-javascript): Covers dynamic component registration with [RegisterForJavaScript](/en-us/dotnet/api/microsoft.aspnetcore.components.web.jscomponentconfigurationextensions.registerforjavascript).
* [Blazor custom elements: Blazor Web App registration](../blazor/components/js-spa-frameworks?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#blazor-web-app-registration): Covers root component custom element registration with `RegisterCustomElement`.
* [Prefix for Blazor WebAssembly assets](../blazor/fundamentals/static-files?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#prefix-for-blazor-webassembly-assets): Covers control of the path string that indicates the prefix for Blazor WebAssembly assets.
* [Temporary redirection URL validity duration](../blazor/security/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#temporary-redirection-url-validity-duration): Covers control of the lifetime of data protection validity for temporary redirection URLs emitted by Blazor server-side rendering.
* [Detailed errors](../blazor/fundamentals/handle-errors?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#detailed-errors-for-razor-component-server-side-rendering): Covers enabling detailed errors for Razor component server-side rendering.
* [Prerendering configuration](../blazor/components/render-modes?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#prerendering): Prerendering is enabled by default for Blazor Web Apps. Follow this link for guidance on how to disable prerendering if you have special circumstances that require an app to disable prerendering.
* [Form binding options](../blazor/forms/binding?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#additional-binding-options): Covers form binding options configuration.

### Drop Blazor Server with Yarp routing workaround

If you previously followed the guidance in [Enable ASP.NET Core Blazor Server support with Yarp in incremental migration](fx-to-core/inc/blazor?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0) for migrating a Blazor Server app with Yarp to .NET 6 or .NET 7, you can reverse the workaround steps that you took when following the article's guidance. Routing and deep linking for Blazor Server with Yarp work correctly in .NET 8.

### Migrate `CascadingValue` components in layout components

Cascading parameters don't pass data across render mode boundaries, and layouts are statically rendered in otherwise interactive apps. Therefore, apps that seek to use cascading parameters in interactively rendered components won't be able to cascade the values from a layout.

The two approaches for migration are:

* (*Recommended*) Pass the state as a root-level cascading value. For more information, see the guidance on [root-level cascading values](../blazor/components/cascading-values-and-parameters?view=aspnetcore-10.0#root-level-cascading-values) and [root-level cascading values with notifications](../blazor/components/cascading-values-and-parameters?view=aspnetcore-10.0#root-level-cascading-values-with-notifications).
* Wrap the router in the `Routes` component with the [`CascadingValue`](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvalue-1) component and make the `Routes` component interactively rendered. For an example, see [`CascadingValue` component](../blazor/components/cascading-values-and-parameters?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#cascadingvalue-component).

For more information, see [Cascading values/parameters and render mode boundaries](../blazor/components/cascading-values-and-parameters?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#cascading-valuesparameters-and-render-mode-boundaries).

### Migrate the `BlazorEnableCompression` MSBuild property

For Blazor WebAssembly apps that disable compression and target .NET 7 or earlier but are built with the .NET 8 SDK, the `BlazorEnableCompression` MSBuild property has changed to `CompressionEnabled`:

```
<PropertyGroup>
-   <BlazorEnableCompression>false</BlazorEnableCompression>
+   <CompressionEnabled>false</CompressionEnabled>
</PropertyGroup>
```

When using the .NET CLI publish command, use the new property:

```
dotnet publish -p:CompressionEnabled=false
```

For more information, see the following resources:

* [Static Web Assets Compression Flag Breaking Change (dotnet/announcements #283)](https://github.com/dotnet/announcements/issues/283)
* [Host and deploy ASP.NET Core Blazor WebAssembly](../blazor/host-and-deploy/webassembly/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#compression)

### Migrate the `<CascadingAuthenticationState>` component to cascading authentication state services

In .NET 7 or earlier, the [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) component is wrapped around some part of the UI tree, for example around the Blazor router, to provide cascading authentication state:

```
<CascadingAuthenticationState>
    <Router ...>
        ...
    </Router>
</CascadingAuthenticationState>
```

In .NET 8, don't use the [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) component:

```
- <CascadingAuthenticationState>
      <Router ...>
          ...
      </Router>
- </CascadingAuthenticationState>
```

Instead, add cascading authentication state services to the service collection by calling [AddCascadingAuthenticationState](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cascadingauthenticationstateservicecollectionextensions.addcascadingauthenticationstate) in the `Program` file:

```
builder.Services.AddCascadingAuthenticationState();
```

For more information, see the following resources:

* *ASP.NET Core Blazor authentication and authorization* article

  * [`AuthenticationStateProvider` service](../blazor/security/?view=aspnetcore-10.0#authenticationstateprovider-service)
  * [Expose the authentication state as a cascading parameter](../blazor/security/?view=aspnetcore-10.0#expose-the-authentication-state-as-a-cascading-parameter)
  * [Customize unauthorized content with the Router component](../blazor/security/?view=aspnetcore-10.0#customize-unauthorized-content-with-the-router-component)

* [ASP.NET Core Blazor authentication and authorization](../blazor/security/?view=aspnetcore-10.0#implement-a-custom-authenticationstateprovider)

### New article on HTTP caching issues

We've added a new article that discusses some of the common HTTP caching issues that can occur when upgrading Blazor apps across major versions and how to address HTTP caching issues.

For more information, see [Avoid HTTP caching issues when upgrading ASP.NET Core Blazor apps](../blazor/host-and-deploy/webassembly/http-caching-issues?view=aspnetcore-10.0).

### New article on class libraries with static server-side rendering (static SSR)

We've added a new article that discusses component library authorship in Razor class libraries (RCLs) with static server-side rendering (static SSR).

For more information, see [ASP.NET Core Razor class libraries (RCLs) with static server-side rendering (static SSR)](../blazor/components/class-libraries-and-static-server-side-rendering?view=aspnetcore-10.0).

### Discover components from additional assemblies

When migrating from a Blazor Server app to a Blazor Web App, access the guidance in [ASP.NET Core Blazor routing](../blazor/fundamentals/routing?view=aspnetcore-10.0#route-to-components-from-multiple-assemblies) if the app uses routable components from additional assemblies, such as component class libraries.

### Drop `[Parameter]` attribute when the parameter is supplied from a query string

The `[Parameter]` attribute is no longer required when supplying a parameter from the query string:

```
- [Parameter]
  [SupplyParameterFromQuery]
```

### Blazor Server script fallback policy authorization

In .NET 7, the Blazor Server script (`blazor.server.js`) is [served by static file middleware](https://github.com/dotnet/aspnetcore/blob/v7.0.16/src/Components/Server/src/DependencyInjection/ConfigureStaticFilesOptions.cs). Placing the call for static file middleware ([UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles)) in the request processing pipeline before the call to authorization middleware ([UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization)) is sufficient in .NET 7 apps to serve the Blazor script to anonymous users.

In .NET 8, the Blazor Server script is served [by its own endpoint](https://github.com/search?q=repo%3Adotnet%2Faspnetcore%20GetBlazorEndpoint\&type=code), using endpoint routing. This change is introduced by [Fixed bug - Passing options to UseStaticFiles breaks Blazor Server (`dotnet/aspnetcore` #45897)](https://github.com/dotnet/aspnetcore/pull/45897).

Consider a multi-tenant scenario where:

* Both the default and fallback policies are set identically.
* The tenant is resolved using the first segment in the request path (for example, `tld.com/tenant-name/...`).
* The requests to tenant endpoints are authenticated by an additional authentication scheme, which adds an additional identity to the request principal.
* The fallback authorization policy has requirements that check claims via the additional identity.

Requests for the Blazor script file (`blazor.server.js`) are served at `/_framework/blazor.server.js`, which is hardcoded in the framework. Requests for the file aren't authenticated by the additional authentication scheme for tenants ***but are still challenged by the fallback policy***, which results in returning an unauthorized result.

This problem is under evaluation for a new framework feature in [MapRazorComponents broken with FallbackPolicy RequireAuthenticatedUser (`dotnet/aspnetcore` 51836)](https://github.com/dotnet/aspnetcore/issues/51836), which is currently scheduled for .NET 9's release in November, 2024. Until then, you can work around this problem using any of the following three approaches:

* Don't use a fallback policy. Apply the `[Authorize]` attribute in the imports file (`_Imports.razor`) to apply it to all of the components of the app. For non-blazor endpoints, explicitly use `[Authorize]` or `RequireAuthorization`.

* Add `[AllowAnonymous]` to the `/_framework/blazor.server.js` endpoint in the `Program` file:

  ```
  app.MapBlazorHub().Add(endpointBuilder =>
  {
      if (endpointBuilder is 
          RouteEndpointBuilder
          { 
              RoutePattern: { RawText: "/_framework/blazor.server.js" }
          })
      {
          endpointBuilder.Metadata.Add(new AllowAnonymousAttribute());
      }
  });
  ```

* Register a custom `AuthorizationHandler` that [checks the `HttpContext`](../mvc/security/authorization/policies?view=aspnetcore-10.0#access-mvc-request-context-in-handlers) to allow the `/_framework/blazor.server.js` file through.

## Docker

### Update Docker images

For apps using Docker, update the *Dockerfile* `FROM` statements and scripts. Use a base image that includes the .NET 8 runtime. Consider the following `docker pull` command difference between ASP.NET Core in .NET 7 and .NET 8:

```
- docker pull mcr.microsoft.com/dotnet/aspnet:7.0
+ docker pull mcr.microsoft.com/dotnet/aspnet:8.0
```

### Update Docker port

The default ASP.NET Core port configured in .NET container images has been updated from port 80 to 8080.

The new `ASPNETCORE_HTTP_PORTS` environment variable was added as a simpler alternative to `ASPNETCORE_URLS`.

For more information, see:

* [Default ASP.NET Core port changed from 80 to 8080](/en-us/dotnet/core/compatibility/containers/8.0/aspnet-port).
* [Specify ports only with `ASPNETCORE_HTTP_PORTS`](/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints#specify-ports-only)

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor fundamentals

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

*Fundamentals* articles provide guidance on foundational Blazor concepts. Some of the concepts are connected to a basic understanding of *Razor components*, which are described further in the next section of this article and covered in detail in the *Components* articles.

## Static and interactive rendering concepts

Razor components are either *statically* rendered or *interactively* rendered.

*Static* or *static rendering* is a server-side scenario that means the component is rendered without the capacity for interplay between the user and .NET/C# code. JavaScript and HTML DOM events remain unaffected, but no user events on the client can be processed with .NET running on the server.

*Interactive*/*interactivity* or *interactive rendering* means that the component has the capacity to process .NET events and binding via C# code. The .NET events and binding are either processed on the server by the ASP.NET Core runtime or in the browser on the client by the WebAssembly-based Blazor runtime.

Important

When using a Blazor Web App, most of the Blazor documentation example components ***require*** interactivity to function and demonstrate the concepts covered by the articles. When you test an example component provided by an article in a Blazor Web App, make sure that either the app adopts global interactivity or the component adopts an interactive render mode. More information on this subject is provided by [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0) in the *Components* section, which appears later in the Blazor documentation set.

More information on these concepts and how to control static and interactive rendering is found in the [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0) article later in the Blazor documentation.

## Client and server rendering concepts

Throughout the Blazor documentation, activity that takes place on the user's system is said to occur *on the client* or *client-side*. Activity that takes place on a server is said to occur *on the server* or *server-side*.

The term *rendering* means to produce the HTML markup that browsers display.

* **Client-side rendering (CSR)** means that the final HTML markup is generated by the .NET WebAssembly runtime on the client. No HTML for the app's client-generated UI is sent from a server to the client for this type of rendering. User interactivity with the page is assumed. There's no such concept as *static* client-side rendering. CSR is assumed to be interactive, so "*interactive* client-side rendering" and "*interactive* CSR" aren't used by the industry or in the Blazor documentation.

* **Server-side rendering (SSR)** means that the final HTML markup is generated by the ASP.NET Core runtime on the server. The HTML is sent to the client over a network for display by the client's browser. No HTML for the app's server-generated UI is created by the client for this type of rendering. SSR can be of two varieties:

  * **Static SSR**: The server produces static HTML that doesn't provide for user interactivity or maintaining Razor component state.
  * **Interactive SSR**: Blazor events permit user interactivity and Razor component state is maintained by the Blazor framework.

* **Prerendering** is the process of initially rendering page content on the server without enabling event handlers for rendered controls. The server outputs the HTML UI of the page as soon as possible in response to the initial request, which makes the app feel more responsive to users. Prerendering can also improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines use to calculate page rank. Prerendering is always followed by final rendering, either on the server or the client.

## Razor components

Blazor apps are based on *Razor components*, often referred to as just *components*. A *component* is an element of UI, such as a page, dialog, or data entry form. Components are .NET C# classes built into [.NET assemblies](/en-us/dotnet/standard/assembly/).

*Razor* refers to how components are usually written in the form of a [Razor](../../mvc/views/razor?view=aspnetcore-10.0) markup page for client-side UI logic and composition. Razor is a syntax for combining HTML markup with C# code designed for developer productivity. Razor files use the `.razor` file extension.

Although some Blazor developers and online resources use the term "Blazor components," the documentation avoids that term and universally uses "Razor components" or "components."

Blazor documentation adopts several conventions for showing and discussing components:

* Generally, examples adhere to ASP.NET Core/C# coding conventions and engineering guidelines. For more information see the following resources:

  * [ASP.NET Core framework engineering guidelines (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines)
  * [C# Coding Conventions (C# guide)](/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)

* Project code, file paths and names, project template names, and other specialized terms are in United States English and usually code-fenced.

* Components are usually referred to by their C# class name (Pascal case) followed by the word "component." For example, a typical file upload component is referred to as the "`FileUpload` component."

* Usually, a component's C# class name is the same as its file name.

* Routable components usually set their relative URLs to the component's class name in kebab-case. For example, a `FileUpload` component includes routing configuration to reach the rendered component at the relative URL `/file-upload`. Routing and navigation is covered in [ASP.NET Core Blazor routing](routing?view=aspnetcore-10.0).

* When multiple versions of a component are used, they're numbered sequentially. For example, the `FileUpload3` component is reached at `/file-upload-3`.

* [Razor directives](../../mvc/views/razor?view=aspnetcore-10.0#directives) at the top of a component definition (`.razor file`) are placed in the following order: `@page`, `@rendermode` (.NET 8 or later), `@using` statements, other directives in alphabetical order.

* Although not required for `private` members, access modifiers are used in article examples and sample apps. For example, `private` is stated for declaring a field named `maxAllowedFiles` as `private int maxAllowedFiles = 3;`.

* [Component parameter](../components/?view=aspnetcore-10.0#component-parameters) values lead with a [Razor reserved `@` symbol](../../mvc/views/razor?view=aspnetcore-10.0#razor-syntax), but it isn't required. Literals (for example, boolean values), keywords (for example, `this`), and `null` as component parameter values aren't prefixed with `@`, but this is also merely a documentation convention. Your own code can prefix literals with `@` if you wish.

* C# classes use the [`this` keyword](/en-us/dotnet/csharp/language-reference/keywords/this) and avoid prefixing fields with an underscore (`_`) that are assigned to in constructors, which differs from the [ASP.NET Core framework engineering guidelines](https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines).

* In examples that use [primary constructors (C# 12 or later)](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors), primary constructor parameters are typically used directly by class members. In article examples, code lines are split to reduce horizontal scrolling. These breaks don't affect execution but can be removed when pasting into your project.

Additional information on Razor component syntax is provided in the *Razor syntax* section of [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#razor-syntax).

The following is an example counter component and part of an app created from a Blazor project template. Detailed components coverage is found in the *Components* articles later in the documentation. The following example demonstrates component concepts seen in the *Fundamentals* articles before reaching the *Components* articles later in the documentation.

`Counter.razor`:

The component assumes that an interactive render mode is inherited from a parent component or applied globally to the app.

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount() => currentCount++;
}
```

The component assumes that an interactive render mode is inherited from a parent component or applied globally to the app.

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount() => currentCount++;
}
```

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

```
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

The preceding `Counter` component:

* Sets its route with the `@page` directive in the first line.
* Sets its page title and heading.
* Renders the current count with `@currentCount`. `currentCount` is an integer variable defined in the C# code of the `@code` block.
* Displays a button to trigger the `IncrementCount` method, which is also found in the `@code` block and increases the value of the `currentCount` variable.

## Render modes

Articles in the *Fundamentals* node make reference to the concept of *render modes*. This subject is covered in detail in the [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0) article in the *Components* node, which appears after the *Fundamentals* node of articles.

For the early references in this node of articles to render mode concepts, merely note the following at this time:

Every component in a Blazor Web App adopts a *render mode* to determine the hosting model that it uses, where it's rendered, and whether or not it's rendered statically on the server, rendered for user interactivity on the server, or rendered for user interactivity on the client (usually with prerendering on the server).

Blazor Server and Blazor WebAssembly apps for ASP.NET Core releases prior to .NET 8 remain fixated on *hosting model* concepts, not render modes. Render modes are conceptually applied to Blazor Web Apps in .NET 8 or later.

The following table shows the available render modes for rendering Razor components in a Blazor Web App. Render modes are applied to components with the `@rendermode` directive on the component instance or on the component definition. It's also possible to set a render mode for the entire app.

| Name                    | Description                                                                                                           | Render location     | Interactive |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------- |
| Static Server           | Static server-side rendering (static SSR)                                                                             | Server              | ❌No         |
| Interactive Server      | Interactive server-side rendering (interactive SSR) using Blazor Server                                               | Server              | ✔️Yes       |
| Interactive WebAssembly | Client-side rendering (CSR) using Blazor WebAssembly†                                                                 | Client              | ✔️Yes       |
| Interactive Auto        | Interactive SSR using Blazor Server initially and then CSR on subsequent visits after the Blazor bundle is downloaded | Server, then client | ✔️Yes       |

†Client-side rendering (CSR) is assumed to be interactive. "*Interactive* client-side rendering" and "*interactive* CSR" aren't used by the industry or in the Blazor documentation.

The preceding information on render modes is all that you need to know to understand the *Fundamentals* node articles. If you're new to Blazor and reading Blazor articles in order down the table of contents, you can delay consuming in-depth information on render modes until you reach the [ASP.NET Core Blazor render modes](../components/render-modes?view=aspnetcore-10.0) article in the *Components* node.

## Subset of .NET APIs for Blazor WebAssembly apps

A curated list of specific .NET APIs that are supported on the browser for Blazor WebAssembly isn't available. However, you can manually [search for a list of .NET APIs annotated with `[UnsupportedOSPlatform("browser")]`](https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Runtime/Versioning/PlatformAttributes.cs,34041602e232c616,references) to discover .NET APIs that aren't supported in WebAssembly.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

For more information, see the following resources:

* [Class libraries: Client-side browser compatibility analyzer](../components/class-libraries?view=aspnetcore-10.0#client-side-browser-compatibility-analyzer)
* [Annotating APIs as unsupported on specific platforms (`dotnet/designs` GitHub repository](https://github.com/dotnet/designs/blob/main/accepted/2020/platform-exclusion/platform-exclusion.md#build-configuration-for-platforms)

## Sample apps

Documentation sample apps are available for inspection and download:

[Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)

Locate a sample app by first selecting the version folder that matches the version of .NET that you're working with.

Sample apps only contain a subset of article examples for Blazor feature demonstrations. When a code example shown by an article isn't in one or more of the sample apps, you can usually place the example code into a local test app for a demonstration.

Samples apps in the repository:

* Blazor Web App
* Blazor WebAssembly
* Blazor Web App Movies tutorial sample ([Build a Blazor movie database app (Overview)](../tutorials/movie-database-app/?view=aspnetcore-10.0))
* Blazor Web App with SignalR ([Use ASP.NET Core SignalR with Blazor](../tutorials/signalr-blazor?view=aspnetcore-10.0))
* Two Blazor Web Apps and a Blazor WebAssembly app for calling web (server) APIs ([Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0))
* Blazor Web App with OIDC ([Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](../security/blazor-web-app-with-oidc?view=aspnetcore-10.0))
* Blazor Web App with Entra ([Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](../security/blazor-web-app-with-entra?view=aspnetcore-10.0))
* Blazor WebAssembly scopes-enabled logging ([ASP.NET Core Blazor logging](logging?view=aspnetcore-10.0#client-side-log-scopes))
* Blazor WebAssembly with ASP.NET Core Identity ([Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](../security/webassembly/standalone-with-identity/?view=aspnetcore-10.0))
* .NET MAUI Blazor Hybrid app with a Blazor Web App and a shared UI provided by a Razor class library (RCL) ([Build a .NET MAUI Blazor Hybrid app with a Blazor Web App](../hybrid/tutorials/maui-blazor-web-app?view=aspnetcore-10.0))
* Additional samples (see the [Blazor samples GitHub repository README file](https://github.com/dotnet/blazor-samples))

Samples apps in the repository:

* Blazor Web App
* Blazor WebAssembly
* Blazor Web App Movies tutorial sample ([Build a Blazor movie database app (Overview)](../tutorials/movie-database-app/?view=aspnetcore-10.0))
* Blazor Web App with SignalR ([Use ASP.NET Core SignalR with Blazor](../tutorials/signalr-blazor?view=aspnetcore-10.0))
* Two Blazor Web Apps and a Blazor WebAssembly app for calling web (server) APIs ([Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0))
* Blazor Web App with OIDC (BFF and non-BFF patterns) ([Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](../security/blazor-web-app-with-oidc?view=aspnetcore-10.0))
* Blazor WebAssembly scopes-enabled logging ([ASP.NET Core Blazor logging](logging?view=aspnetcore-10.0#client-side-log-scopes))
* Blazor WebAssembly with ASP.NET Core Identity ([Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](../security/webassembly/standalone-with-identity/?view=aspnetcore-10.0))
* .NET MAUI Blazor Hybrid app with a Blazor Web App and a shared UI provided by a Razor class library (RCL) ([Build a .NET MAUI Blazor Hybrid app with a Blazor Web App](../hybrid/tutorials/maui-blazor-web-app?view=aspnetcore-10.0))
* Additional samples (see the [Blazor samples GitHub repository README file](https://github.com/dotnet/blazor-samples))

The sample repository contains two types of samples:

* Snippet sample apps provide the code examples that appear in articles. These apps compile but aren't necessarily runnable apps. These apps are useful for merely obtaining example code that appears in articles.

* Samples apps to accompany Blazor articles compile and run for the following scenarios:

  * Blazor Server and Blazor WebAssembly with SignalR
  * Blazor WebAssembly scopes-enabled logging

For more information, a list of the samples in the repository, and download instructions, see the [Blazor samples GitHub repository README file](https://github.com/dotnet/blazor-samples).

The ASP.NET Core repository's Basic Test App is also a helpful set of samples for various Blazor scenarios:

[`BasicTestApp` in ASP.NET Core reference source (`dotnet/aspnetcore`)](https://github.com/dotnet/aspnetcore/tree/main/src/Components/test/testassets/BasicTestApp)

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Community links to Blazor resources

For a collection of links to Blazor resources maintained by the community, visit [Awesome Blazor](https://github.com/AdrienTorris/awesome-blazor).

Note

Microsoft doesn't own, maintain, or support *Awesome Blazor* and most of the community products and services described and linked there.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/transform-webconfig
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/iis/transform-webconfig.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Transform web.config

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Vijay Ramakrishnan](https://github.com/vijayrkn)

Transformations to the *web.config* file can be applied automatically when an app is published based on:

* [Build configuration](#build-configuration)
* [Profile](#profile)
* [Environment](#environment)
* [Custom](#custom)

These transformations occur for either of the following *web.config* generation scenarios:

* Generated automatically by the `Microsoft.NET.Sdk.Web` SDK.
* Provided by the developer in the [content root](../../fundamentals/?view=aspnetcore-10.0#content-root) of the app.

## Build configuration

Build configuration transforms are run first.

Include a *web.{CONFIGURATION}.config* file for each [build configuration (Debug|Release)](/en-us/dotnet/core/tools/dotnet-publish#options) requiring a *web.config* transformation.

In the following example, a configuration-specific environment variable is set in *web.Release.config*:

```
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <location>
    <system.webServer>
      <aspNetCore>
        <environmentVariables xdt:Transform="InsertIfMissing">
          <environmentVariable name="Configuration_Specific" 
                               value="Configuration_Specific_Value" 
                               xdt:Locator="Match(name)" 
                               xdt:Transform="InsertIfMissing" />
        </environmentVariables>
      </aspNetCore>
    </system.webServer>
  </location>
</configuration>
```

The transform is applied when the configuration is set to *Release*:

```
dotnet publish --configuration Release
```

The MSBuild property for the configuration is `$(Configuration)`.

## Profile

Profile transformations are run second, after [Build configuration](#build-configuration) transforms.

Include a *web.{PROFILE}.config* file for each profile configuration requiring a *web.config* transformation.

In the following example, a profile-specific environment variable is set in *web.FolderProfile.config* for a folder publish profile:

```
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <location>
    <system.webServer>
      <aspNetCore>
        <environmentVariables xdt:Transform="InsertIfMissing">
          <environmentVariable name="Profile_Specific" 
                               value="Profile_Specific_Value" 
                               xdt:Locator="Match(name)" 
                               xdt:Transform="InsertIfMissing" />
        </environmentVariables>
      </aspNetCore>
    </system.webServer>
  </location>
</configuration>
```

The transform is applied when the profile is *FolderProfile*:

```
dotnet publish --configuration Release /p:PublishProfile=FolderProfile
```

The MSBuild property for the profile name is `$(PublishProfile)`.

If no profile is passed, the default profile name is **FileSystem** and *web.FileSystem.config* is applied if the file is present in the app's content root.

## Environment

Environment transformations are run third, after [Build configuration](#build-configuration) and [Profile](#profile) transforms.

Include a *web.{ENVIRONMENT}.config* file for each [environment](../../fundamentals/environments?view=aspnetcore-10.0) requiring a *web.config* transformation.

In the following example, an environment-specific environment variable is set in *web.Production.config* for the `Production` environment:

```
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <location>
    <system.webServer>
      <aspNetCore>
        <environmentVariables xdt:Transform="InsertIfMissing">
          <environmentVariable name="Environment_Specific" 
                               value="Environment_Specific_Value" 
                               xdt:Locator="Match(name)" 
                               xdt:Transform="InsertIfMissing" />
        </environmentVariables>
      </aspNetCore>
    </system.webServer>
  </location>
</configuration>
```

The transform is applied when the environment is *Production*:

```
dotnet publish --configuration Release /p:EnvironmentName=Production
```

The MSBuild property for the environment is `$(EnvironmentName)`.

When publishing from Visual Studio and using a publish profile, see [Visual Studio publish profiles (.pubxml) for ASP.NET Core app deployment](../visual-studio-publish-profiles?view=aspnetcore-10.0#set-the-environment).

The `ASPNETCORE_ENVIRONMENT` environment variable is automatically added to the *web.config* file when the environment name is specified.

## Custom

Custom transformations are run last, after [Build configuration](#build-configuration), [Profile](#profile), and [Environment](#environment) transforms.

Include a *{CUSTOM\_NAME}.transform* file for each custom configuration requiring a *web.config* transformation.

In the following example, a custom transform environment variable is set in *custom.transform*:

```
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <location>
    <system.webServer>
      <aspNetCore>
        <environmentVariables xdt:Transform="InsertIfMissing">
          <environmentVariable name="Custom_Specific" 
                               value="Custom_Specific_Value" 
                               xdt:Locator="Match(name)" 
                               xdt:Transform="InsertIfMissing" />
        </environmentVariables>
      </aspNetCore>
    </system.webServer>
  </location>
</configuration>
```

The transform is applied when the `CustomTransformFileName` property is passed to the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command:

```
dotnet publish --configuration Release /p:CustomTransformFileName=custom.transform
```

The MSBuild property for the profile name is `$(CustomTransformFileName)`.

## Prevent web.config transformation

To prevent transformations of the *web.config* file, set the MSBuild property `$(IsWebConfigTransformDisabled)`:

```
dotnet publish /p:IsWebConfigTransformDisabled=true
```

## Related content

* [Web.config Transformation Syntax for Web Application Project Deployment](/en-us/previous-versions/dd465326\(v=vs.100\))
* [Web.config Transformation Syntax for Web Project Deployment Using Visual Studio](/en-us/previous-versions/aspnet/dd465326\(v=vs.110\))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/location-of-javascript
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/javascript-interoperability/location-of-javascript.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# JavaScript location in ASP.NET Core Blazor apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Load JavaScript (JS) code using any of the following approaches:

* [Load a script in `<head>` markup](#load-a-script-in-head-markup) (*Not generally recommended*)
* [Load a script in `<body>` markup](#load-a-script-in-body-markup)
* [Load a script from an external JavaScript file (`.js`) collocated with a component](#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component)
* [Load a script from an external JavaScript file (`.js`)](#load-a-script-from-an-external-javascript-file-js)
* [Inject a script before or after Blazor starts](#inject-a-script-before-or-after-blazor-starts)

- [Load a script in `<head>` markup](#load-a-script-in-head-markup) (*Not generally recommended*)
- [Load a script in `<body>` markup](#load-a-script-in-body-markup)
- [Load a script from an external JavaScript file (`.js`)](#load-a-script-from-an-external-javascript-file-js)
- [Inject a script after Blazor starts](#inject-a-script-after-blazor-starts)

Inline JavaScript isn't recommended for Blazor apps. We recommend using [JS collocation](#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) combined with [JS modules](#javascript-isolation-in-javascript-modules).

## Location of `<script>` tags

Only place a `<script>` tag in a component file (`.razor`) if the component is guaranteed to adopt [static server-side rendering (static SSR)](../fundamentals/?view=aspnetcore-10.0#client-and-server-rendering-concepts) without [enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling). Placing a `<script>` tag in a component file doesn't produce a compile-time warning or error, but script loading behavior might not match your expectations in components that adopt an interactive render mode or static SSR with enhanced navigation.

Don't place a `<script>` tag in a component file (`.razor`) because the `<script>` tag can't be updated dynamically. Placing a `<script>` tag in a component file produces a compile-time error.

Note

Documentation examples usually place scripts in a `<script>` tag or load global scripts from external files. These approaches pollute the client with global functions. For production apps, we recommend placing JS into separate [JS modules](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules) that can be imported when needed. For more information, see the [JavaScript isolation in JavaScript modules](#javascript-isolation-in-javascript-modules) section.

Note

Documentation examples place scripts into a `<script>` tag or load global scripts from external files. These approaches pollute the client with global functions. Placing JS into separate [JS modules](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules) that can be imported when needed is **not** supported in Blazor earlier than .NET 5. If the app requires the use of JS modules for JS isolation, we recommend using .NET 5 or later to build the app. For more information, use the **Version** dropdown list to select a .NET 5 or later version of this article and see the *JavaScript isolation in JavaScript modules* section.

## Load a script in `<head>` markup

*The approach in this section isn't generally recommended.*

Place the JavaScript (JS) tags (`<script>...</script>`) in the [`<head>` element markup](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content):

```
<head>
    ...

    <script>
      window.jsMethod = (methodParameter) => {
        ...
      };
    </script>
</head>
```

Loading JS from the `<head>` isn't the best approach for the following reasons:

* JS interop may fail if the script depends on Blazor. We recommend loading scripts using one of the other approaches, not via the `<head>` markup.
* The page may become interactive slower due to the time it takes to parse the JS in the script.

In component markup, scripts can be loaded via a [`HeadContent` component](../components/control-head-content?view=aspnetcore-10.0) with the usual caveat that the approach slows down page load on the client, which we recommend avoiding. When a script is loaded with a `HeadContent` component in a Blazor Server app, Blazor WebAssembly app, or a Blazor Web App using either an interactive render mode (interactive SSR, CSR) or static SSR with enhanced navigation, navigating away from the component's page removes the `<script>` tag from the rendered `<head>` content but doesn't unload the script's JavaScript code, including event handlers that the script registers, exposed variables, and methods that the script provides. Only Blazor Web Apps using static SSR without enhanced navigation unload JavaScript code when the user navigates away from the page. Generally, you're better off adding `<script>` tags to the physical `<head>` content, unless you explicitly desire to keep such script references in the components that use them and don't mind that the code isn't unloaded on navigation events.

In component markup, scripts can be loaded via a [`HeadContent` component](../components/control-head-content?view=aspnetcore-10.0) with the usual caveat that the approach slows down page load on the client, which we recommend avoiding. When a script is loaded with a `HeadContent` component, navigating away from the component's page removes the `<script>` tag from the rendered `<head>` content but doesn't unload the script's JavaScript code, including event handlers that the script registers, exposed variables, and methods that the script provides. Generally, you're better off adding `<script>` tags to the physical `<head>` content, unless you explicitly desire to keep such script references in the components that use them and don't mind that the code isn't unloaded on navigation events.

## Load a script in `<body>` markup

Place the JavaScript tags (`<script>...</script>`) inside the [closing `</body>` element](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content) after the Blazor script reference:

```
<body>
    ...

    <script src="{BLAZOR SCRIPT}"></script>
    <script>
      window.jsMethod = (methodParameter) => {
        ...
      };
    </script>
</body>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

## Load a script from an external JavaScript file (`.js`) collocated with a component

This section and the following examples are primarily focused on explaining JS file collocation. The first example demonstrates a collocated JS file with an ordinary JS function. The second example demonstrates the use of a module to load a function, which is the recommended approach for most production apps. Calling JS from .NET is fully covered in [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](call-javascript-from-dotnet?view=aspnetcore-10.0), where there are further explanations of the Blazor JS API with additional examples. Component disposal, which is present in the second example, is covered in [ASP.NET Core Razor component disposal](../components/component-disposal?view=aspnetcore-10.0).

The following `JsCollocation1` component loads a script via a [`HeadContent` component](../components/control-head-content?view=aspnetcore-10.0) and calls a JS function with [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync). The `{PATH}` placeholder is the path to the component.

Important

If you use the following code for a demonstration in a test app, change the `{PATH}` placeholder to the path of the component (example: `Components/Pages` in .NET 8 or later or `Pages` in .NET 7 or earlier). In a Blazor Web App (.NET 8 or later), the component requires an interactive render mode applied either globally to the app or to the component definition.

Add the following script after the Blazor script ([location of the Blazor start script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script)):

```
<script src="{PATH}/JsCollocation1.razor.js"></script>
```

`JsCollocation1` component (`{PATH}/JsCollocation1.razor`):

```
@page "/js-collocation-1"
@inject IJSRuntime JS

<PageTitle>JS Collocation 1</PageTitle>

<h1>JS Collocation Example 1</h1>

<button @onclick="ShowPrompt">Call showPrompt1</button>

@if (!string.IsNullOrEmpty(result))
{
    <p>
        Hello @result!
    </p>
}

@code {
    private string? result;

    public async Task ShowPrompt()
    {
        result = await JS.InvokeAsync<string>(
            "showPrompt1", "What's your name?");
        StateHasChanged();
    }
}
```

The collocated JS file is placed next to the `JsCollocation1` component file with the file name `JsCollocation1.razor.js`. In the `JsCollocation1` component, the script is referenced at the path of the collocated file. In the following example, the `showPrompt1` function accepts the user's name from a [`Window prompt()`](https://developer.mozilla.org/docs/Web/API/Window/prompt) and returns it to the `JsCollocation1` component for display.

`{PATH}/JsCollocation1.razor.js`:

```
function showPrompt1(message) {
  return prompt(message, 'Type your name here');
}
```

The preceding approach isn't recommended for general use in production apps because the approach pollutes the client with global functions. A better approach for production apps is to use JS modules. The same general principles apply to loading a JS module from a collocated JS file, as the next example demonstrates.

The following `JsCollocation2` component's `OnAfterRenderAsync` method loads a JS module into `module`, which is an [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) of the component class. `module` is used to call the `showPrompt2` function. The `{PATH}` placeholder is the path to the component.

Important

If you use the following code for a demonstration in a test app, change the `{PATH}` placeholder to the path of the component. In a Blazor Web App (.NET 8 or later), the component requires an interactive render mode applied either globally to the app or to the component definition.

`JsCollocation2` component (`{PATH}/JsCollocation2.razor`):

```
@page "/js-collocation-2"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>JS Collocation 2</PageTitle>

<h1>JS Collocation Example 2</h1>

<button @onclick="ShowPrompt">Call showPrompt2</button>

@if (!string.IsNullOrEmpty(result))
{
    <p>
        Hello @result!
    </p>
}

@code {
    private IJSObjectReference? module;
    private string? result;

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            /*
                Change the {PATH} placeholder in the next line to the path of
                the collocated JS file in the app. Examples:

                ./Components/Pages/JsCollocation2.razor.js (.NET 8 or later)
                ./Pages/JsCollocation2.razor.js (.NET 7 or earlier)
            */
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./{PATH}/JsCollocation2.razor.js");
        }
    }

    public async Task ShowPrompt()
    {
        if (module is not null)
        {
            result = await module.InvokeAsync<string>(
                "showPrompt2", "What's your name?");
            StateHasChanged();
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            try
            {
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}
```

In the preceding example, [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) is trapped during module disposal in case Blazor's SignalR circuit is lost. If the preceding code is used in a Blazor WebAssembly app, there's no SignalR connection to lose, so you can remove the `try`-`catch` block and leave the line that disposes the module (`await module.DisposeAsync();`). For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](./?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit).

`{PATH}/JsCollocation2.razor.js`:

```
export function showPrompt2(message) {
  return prompt(message, 'Type your name here');
}
```

Important

Don't place a `<script>` tag for `JsCollocation2.razor.js` after the [Blazor script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) because the module is loaded and cached automatically when the [dynamic `import()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/import) is invoked.

Use of scripts and modules for collocated JS in a Razor class library (RCL) is only supported for Blazor's JS interop mechanism based on the [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) interface. If you're implementing [JavaScript `[JSImport]`/`[JSExport]` interop](import-export-interop?view=aspnetcore-10.0), see [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](import-export-interop?view=aspnetcore-10.0#razor-class-library-rcl-collocated-js-is-unsupported).

```
module = await JS.InvokeAsync<IJSObjectReference>("import", 
    "./_content/AppJS/Components/Pages/JsCollocation3.razor.js");
```

For more information on RCLs, see [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../components/class-libraries?view=aspnetcore-10.0).

### Load a script from an external JavaScript file (`.js`)

Place the JavaScript (JS) tags (`<script>...</script>`) with a script source (`src`) path inside the [closing `</body>` element](../project-structure?view=aspnetcore-10.0#location-of-head-and-body-content) after the Blazor script reference:

```
<body>
    ...

    <script src="{BLAZOR SCRIPT}"></script>
    <script src="{SCRIPT PATH AND FILE NAME (.js)}"></script>
</body>
```

For the placeholders in the preceding example:

* The `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name. For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).
* The `{SCRIPT PATH AND FILE NAME (.js)}` placeholder is the path and script file name under `wwwroot`.

In the following example of the preceding `<script>` tag, the `scripts.js` file is in the `wwwroot/js` folder of the app:

```
<script src="js/scripts.js"></script>
```

You can also serve scripts directly from the `wwwroot` folder if you prefer not to keep all of your scripts in a separate folder under `wwwroot`:

```
<script src="scripts.js"></script>
```

When the external JS file is supplied by a [Razor class library](../components/class-libraries?view=aspnetcore-10.0), specify the JS file using its stable static web asset path: `_content/{PACKAGE ID}/{SCRIPT PATH AND FILE NAME (.js)}`:

* The `{PACKAGE ID}` placeholder is the library's [package ID](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties). The package ID defaults to the project's assembly name if `<PackageId>` isn't specified in the project file.
* The `{SCRIPT PATH AND FILE NAME (.js)}` placeholder is the path and file name under `wwwroot`.

```
<body>
    ...

    <script src="{BLAZOR SCRIPT}"></script>
    <script src="_content/{PACKAGE ID}/{SCRIPT PATH AND FILE NAME (.js)}"></script>
</body>
```

In the following example of the preceding `<script>` tag:

* The Razor class library has an assembly name of `ComponentLibrary`, and a `<PackageId>` isn't specified in the library's project file.
* The `scripts.js` file is in the class library's `wwwroot` folder.

```
<script src="_content/ComponentLibrary/scripts.js"></script>
```

For more information, see [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../components/class-libraries?view=aspnetcore-10.0).

## Inject a script before or after Blazor starts

To ensure scripts load before or after Blazor starts, use a JavaScript initializer. For more information and examples, see [ASP.NET Core Blazor startup](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers).

## Inject a script after Blazor starts

To inject a script after Blazor starts, chain to the `Promise` that results from a manual start of Blazor. For more information and an example, see [ASP.NET Core Blazor startup](../fundamentals/startup?view=aspnetcore-10.0#inject-a-script-after-blazor-starts).

## JavaScript isolation in JavaScript modules

Blazor enables JavaScript (JS) isolation in standard [JS modules](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules) ([ECMAScript specification](https://tc39.es/ecma262/#sec-modules)).

JS isolation provides the following benefits:

* Imported JS no longer pollutes the global namespace.
* Consumers of a library and components aren't required to import the related JS.

In server-side scenarios, always trap [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) in case loss of Blazor's SignalR circuit prevents a JS interop call from disposing a module, which results in an unhandled exception. Blazor WebAssembly apps don't use a SignalR connection during JS interop, so there's no need to trap [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) in Blazor WebAssembly apps for module disposal.

For more information, see the following resources:

* [JavaScript isolation in JavaScript modules](call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules)
* [JavaScript interop calls without a circuit](./?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/metrics/blazor
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/metrics/blazor.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core built-in Blazor (Razor components) metrics

This article describes the built-in Blazor (Razor components) metrics for ASP.NET Core produced using the [System.Diagnostics.Metrics](/en-us/dotnet/api/system.diagnostics.metrics) API. These metrics cover Razor component route changes and browser events, component lifecycle events, and server-side Blazor circuits. They're available in ASP.NET Core 10.0 or later.

For an overview of all built-in metrics reference pages and how to read this reference, see [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0). For information on how to collect, report, enrich, and test with ASP.NET Core metrics, see [ASP.NET Core metrics](overview?view=aspnetcore-10.0).

## `Microsoft.AspNetCore.Components`

The `Microsoft.AspNetCore.Components` metrics report information on Razor component route changes and browser events:

* [`aspnetcore.components.navigation`](#metric-aspnetcorecomponentsnavigation)
* [`aspnetcore.components.event_handler`](#metric-aspnetcorecomponentsevent_handler)

### Metric: `aspnetcore.components.navigation`

| Name                               | Instrument Type | Unit (UCUM) | Description                                          |
| ---------------------------------- | --------------- | ----------- | ---------------------------------------------------- |
| `aspnetcore.components.navigation` | Counter         | `{route}`   | Tracks the total number of route changes in the app. |

| Attribute                     | Type   | Description                      | Examples                                                  | Presence                   |
| ----------------------------- | ------ | -------------------------------- | --------------------------------------------------------- | -------------------------- |
| `aspnetcore.components.type`  | string | Component navigated to.          | `TestComponent`                                           | Always                     |
| `aspnetcore.components.route` | string | The component's route.           | `/test-route`                                             | Always                     |
| `error.type`                  | string | The full name of exception type. | `System.InvalidOperationException`; `Contoso.MyException` | If an exception is thrown. |

Usage: How many different Blazor pages did users visit?

### Metric: `aspnetcore.components.event_handler`

| Name                                  | Instrument Type | Unit (UCUM) | Description                                                                                                                                              |
| ------------------------------------- | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aspnetcore.components.event_handler` | Histogram       | `s`         | Measures the duration of processing browser events, including business logic of the component, excluding the duration of child component event handling. |

| Attribute                              | Type   | Description                                  | Examples                                                  | Presence                   |
| -------------------------------------- | ------ | -------------------------------------------- | --------------------------------------------------------- | -------------------------- |
| `aspnetcore.components.type`           | string | Component type handling the event.           | `TestComponent`                                           | Always                     |
| `aspnetcore.components.method`         | string | C# method handling the event.                | `OnClick`                                                 | Always                     |
| `aspnetcore.components.attribute.name` | string | Component attribute name handling the event. | `onclick`                                                 | Always                     |
| `error.type`                           | string | The full name of exception type.             | `System.InvalidOperationException`; `Contoso.MyException` | If an exception is thrown. |

Usage:

* Which component's click event handler is slow?
* Which buttons are selected often?

## `Microsoft.AspNetCore.Components.Lifecycle`

The `Microsoft.AspNetCore.Components.Lifecycle` metrics report information on Razor component lifecycle events:

* [`aspnetcore.components.update_parameters`](#metric-aspnetcorecomponentsupdate_parameters)
* [`aspnetcore.components.render_diff`](#metric-aspnetcorecomponentsrender_diff)

### Metric: `aspnetcore.components.update_parameters`

| Name                                      | Instrument Type | Unit (UCUM) | Description                                                                         |
| ----------------------------------------- | --------------- | ----------- | ----------------------------------------------------------------------------------- |
| `aspnetcore.components.update_parameters` | Histogram       | `s`         | Measures the duration of processing component parameters, including business logic. |

| Attribute                    | Type   | Description                        | Examples                                                  | Presence                   |
| ---------------------------- | ------ | ---------------------------------- | --------------------------------------------------------- | -------------------------- |
| `aspnetcore.components.type` | string | Component type handling the event. | `TestComponent`                                           | Always                     |
| `error.type`                 | string | The full name of exception type.   | `System.InvalidOperationException`; `Contoso.MyException` | If an exception is thrown. |

Usage:

* Which components are slow to update?
* Which components are updated often?

### Metric: `aspnetcore.components.render_diff`

| Name                                | Instrument Type | Unit (UCUM) | Description                               |
| ----------------------------------- | --------------- | ----------- | ----------------------------------------- |
| `aspnetcore.components.render_diff` | Histogram       | `s`         | Tracks the duration of rendering batches. |

| Attribute                           | Type   | Description                                                 | Examples                                                  | Presence                   |
| ----------------------------------- | ------ | ----------------------------------------------------------- | --------------------------------------------------------- | -------------------------- |
| `aspnetcore.components.diff.length` | int    | The length of the render diff/size of the batch (bucketed). | 50                                                        | Always                     |
| `error.type`                        | string | The full name of exception type.                            | `System.InvalidOperationException`; `Contoso.MyException` | If an exception is thrown. |

Usage:

* Is server rendering slow?
* Do I render diffs that are too large? (network bandwidth, DOM update)

## `Microsoft.AspNetCore.Components.Server.Circuits`

The `Microsoft.AspNetCore.Components.Server.Circuits` metrics report information on server-side Blazor circuits in Blazor Server and Blazor Web Apps:

* [`aspnetcore.components.circuit.active`](#metric-aspnetcorecomponentscircuitactive)
* [`aspnetcore.components.circuit.connected`](#metric-aspnetcorecomponentscircuitconnected)
* [`aspnetcore.components.circuit.duration`](#metric-aspnetcorecomponentscircuitduration)

### Metric: `aspnetcore.components.circuit.active`

| Name                                   | Instrument Type | Unit (UCUM) | Description                                              |
| -------------------------------------- | --------------- | ----------- | -------------------------------------------------------- |
| `aspnetcore.components.circuit.active` | UpDownCounter   | `{circuit}` | Shows the number of active circuits currently in memory. |

Usage: How much memory does the session state hold?

### Metric: `aspnetcore.components.circuit.connected`

| Name                                      | Instrument Type | Unit (UCUM) | Description                                         |
| ----------------------------------------- | --------------- | ----------- | --------------------------------------------------- |
| `aspnetcore.components.circuit.connected` | UpDownCounter   | `{circuit}` | Tracks the number of circuits connected to clients. |

Usage: How many SignalR connections are open?

### Metric: `aspnetcore.components.circuit.duration`

| Name                                     | Instrument Type | Unit (UCUM) | Description                                                          |
| ---------------------------------------- | --------------- | ----------- | -------------------------------------------------------------------- |
| `aspnetcore.components.circuit.duration` | Histogram       | `s`         | Measures circuit lifetime duration and provides total circuit count. |

Usage:

* How many sessions processed?
* How long do users keep the session/tab open?

## Related content

* [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0)
* [ASP.NET Core built-in HTTP metrics](http?view=aspnetcore-10.0)
* [ASP.NET Core built-in diagnostics metrics](diagnostics?view=aspnetcore-10.0)
* [ASP.NET Core built-in authentication and authorization metrics](security?view=aspnetcore-10.0)
* [ASP.NET Core metrics](overview?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/anti-request-forgery.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core

By [Fiyaz Hasan](https://twitter.com/FiyazBinHasan) and [Rick Anderson](https://twitter.com/RickAndMSFT)

Cross-site request forgery is an attack against web-hosted apps whereby a malicious web app can influence the interaction between a client browser and a web app that trusts that browser. These attacks are possible because web browsers send some types of authentication tokens automatically with every request to a website. This form of exploit is also known as a *one-click attack* or *session riding* because the attack takes advantage of the user's previously authenticated session. Cross-site request forgery is also known as XSRF or CSRF.

An example of a CSRF attack:

1. A user signs into `www.good-banking-site.example.com` using forms authentication. The server authenticates the user and issues a response that includes an authentication cookie. The site is vulnerable to attack because it trusts any request that it receives with a valid authentication cookie.

2. The user visits a malicious site, `www.bad-crook-site.example.com`.

   The malicious site, `www.bad-crook-site.example.com`, contains an HTML form similar to the following example:

   ```
   <h1>Congratulations! You're a Winner!</h1>
   <form action="https://www.good-banking-site.example.com/api/account" method="post">
       <input type="hidden" name="Transaction" value="withdraw" />
       <input type="hidden" name="Amount" value="1000000" />
       <input type="submit" value="Click to collect your prize!" />
   </form>
   ```

   Notice that the form's `action` posts to the vulnerable site, not to the malicious site. This is the "cross-site" part of CSRF.

3. The user selects the submit button. The browser makes the request and automatically includes the authentication cookie for the requested domain, `www.good-banking-site.example.com`.

4. The request runs on the `www.good-banking-site.example.com` server with the user's authentication context and can perform any action that an authenticated user is allowed to perform.

In addition to the scenario where the user selects the button to submit the form, the malicious site could:

* Run a script that automatically submits the form.
* Send the form submission as an AJAX request.
* Hide the form using CSS.

These alternative scenarios don't require any action or input from the user other than initially visiting the malicious site.

Using HTTPS doesn't prevent a CSRF attack. The malicious site can send `https://www.good-banking-site.example.com/` a request just as easily as it can send an insecure request.

Some attacks target endpoints that respond to GET requests, in which case an image tag can be used to perform the action. This form of attack is common on forum sites that permit images but block JavaScript. Apps that change state on GET requests, where variables or resources are altered, are vulnerable to malicious attacks. **GET requests that change state are insecure. A best practice is to never change state on a GET request.**

CSRF attacks are possible against web apps that use cookies for authentication because:

* Browsers store cookies issued by a web app.
* Stored cookies include session cookies for authenticated users.
* Browsers send all of the cookies associated with a domain to the web app every request regardless of how the request to app was generated within the browser.

However, CSRF attacks aren't limited to exploiting cookies. For example, Basic and Digest authentication are also vulnerable. After a user signs in with Basic or Digest authentication, the browser automatically sends the credentials until the session ends.

In this context, *session* refers to the client-side session during which the user is authenticated. It's unrelated to server-side sessions or [ASP.NET Core session middleware](../fundamentals/app-state?view=aspnetcore-10.0).

Users can protect against CSRF vulnerabilities by taking precautions:

* Sign out of web apps when finished using them.
* Clear browser cookies periodically.

However, CSRF vulnerabilities are fundamentally a problem with the web app, not the end user.

## Authentication fundamentals

Cookie-based authentication is a popular form of authentication. Token-based authentication systems are growing in popularity, especially for Single Page Applications (SPAs).

### Cookie-based authentication

When a user authenticates using their username and password they're issued a token containing an authentication ticket. The token can be used for authentication and authorization. The token is stored as a cookie that's sent with every request the client makes. Generating and validating this cookie is performed with the cookie authentication middleware. The [middleware](../fundamentals/middleware/?view=aspnetcore-10.0) serializes a user principal into an encrypted cookie. On subsequent requests, the middleware validates the cookie, recreates the principal, and assigns the principal to the [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user) property.

### Token-based authentication

When a user is authenticated, they're issued a token (not an antiforgery token). The token contains user information in the form of [claims](/en-us/dotnet/framework/security/claims-based-identity-model) or a reference token that points the app to user state maintained in the app. When a user attempts to access a resource that requires authentication, the token is sent to the app with an extra authorization header in the form of a Bearer token. This approach makes the app stateless. In each subsequent request, the token is passed in the request for server-side validation. This token isn't *encrypted*; it's *encoded*. On the server, the token is decoded to access its information. To send the token on subsequent requests, store the token in the browser's local storage. Placing a token in the browser local storage and retrieving it and using it as a bearer token provides protection against CSRF attacks. However, should the app be vulnerable to script injection via XSS or a compromised external JavaScript file, a cyberattacker could retrieve any value from local storage and send it to themselves. ASP.NET Core encodes all server side output from variables by default, reducing the risk of XSS. If you override this behavior by using [Html.Raw](/en-us/dotnet/api/system.web.mvc.htmlhelper.raw) or custom code with untrusted input then you may increase the risk of XSS.

Don't be concerned about CSRF vulnerability if the token is stored in the browser's local storage. CSRF is a concern when the token is stored in a cookie. For more information, see the GitHub issue [SPA code sample adds two cookies](https://github.com/dotnet/AspNetCore.Docs/issues/13369).

### Multiple apps hosted at one domain

Shared hosting environments are vulnerable to session hijacking, sign-in CSRF, and other attacks.

Although `example1.contoso.net` and `example2.contoso.net` are different hosts, there's an implicit trust relationship between hosts under the `*.contoso.net` domain. This implicit trust relationship allows potentially untrusted hosts to affect each other's cookies (the same-origin policies that govern AJAX requests don't necessarily apply to HTTP cookies).

Attacks that exploit trusted cookies between apps hosted on the same domain can be prevented by not sharing domains. When each app is hosted on its own domain, there's no implicit cookie trust relationship to exploit.

### Fetch metadata headers

Modern browsers attach [Fetch Metadata](https://developer.mozilla.org/docs/Web/HTTP/Headers/Sec-Fetch-Site) request headers—most importantly `Sec-Fetch-Site`—to every request. `Sec-Fetch-Site` describes the relationship between the origin that initiated the request and the origin being requested: `same-origin` identifies a request the site made to itself, while `same-site` and `cross-site` identify requests initiated by another origin. The [`Origin`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Origin) header carries the initiating origin and serves as a fallback for browsers that predate Fetch Metadata.

`Sec-Fetch-Site` and `Origin` are [forbidden request headers](https://developer.mozilla.org/docs/Glossary/Forbidden_request_header): the browser sets them, and JavaScript running in a page can't override or forge them. That makes them a trustworthy signal for telling a site's own requests apart from cross-site requests without a server-issued token. The [automatic CSRF protection](#automatic-csrf-protection-in-aspnet-core) built into ASP.NET Core uses this signal to reject cross-site form posts that aren't explicitly trusted.

## Automatic CSRF protection in ASP.NET Core

ASP.NET Core ships an automatic CSRF protection middleware that's enabled by default in apps built with `WebApplication.CreateBuilder`. Unlike the [token-based antiforgery system](#antiforgery-in-aspnet-core), this middleware doesn't issue or validate tokens. Instead, it inspects the `Sec-Fetch-Site` and `Origin` [Fetch metadata headers](#fetch-metadata-headers) and records a validation verdict on the request. Components that process submitted form data enforce that verdict, rejecting cross-origin form posts that aren't explicitly trusted.

For most apps, no code changes are required: same-origin browser requests, safe HTTP methods, and non-browser clients (`curl`, server-to-server, mobile apps) all pass through unaffected. The middleware primarily affects apps that accept cross-origin form posts from a browser, such as a site that posts a form to an API on a different origin. Those scenarios need to either configure [CORS](cors?view=aspnetcore-10.0) to declare the trusted origin or opt the endpoint out.

This middleware is *additive* to the token-based antiforgery system. The two protections coexist and can both be active on the same endpoint. For a comparison of when each one applies, see [Interaction with token-based antiforgery](#interaction-with-token-based-antiforgery).

### How it works

For every request, the middleware evaluates a short chain of rules to reach a verdict—*allowed* or *denied*. The checks run in order, and the first match wins:

1. **Safe HTTP methods are always allowed.** `GET`, `HEAD`, `OPTIONS`, and `TRACE` requests pass through. This follows [RFC 9110 §9.2.1](https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.1) and is consistent with the long-standing rule that endpoints shouldn't change state on `GET`.
2. **`Sec-Fetch-Site: same-origin` or `Sec-Fetch-Site: none` is allowed.** Modern browsers send `Sec-Fetch-Site` on every request. `same-origin` covers normal in-app navigation and fetch, and `none` covers requests initiated directly by the user (typing a URL, using a bookmark). This is the most common code path—most legitimate browser traffic exits here.
3. **A trusted, credentialed origin from CORS is allowed.** If the request carries an `Origin` header and the endpoint's resolved CORS policy trusts that origin and the policy has `.AllowCredentials()` configured, the request is allowed. The middleware resolves the policy the same way the CORS middleware does: per-endpoint policy from `[EnableCors("name")]` first, then the default policy registered with `AddDefaultPolicy`. See [Allowing cross-origin clients](#allowing-cross-origin-clients) for important limits on this rule.
4. **Any other `Sec-Fetch-Site` value is denied.** When `Sec-Fetch-Site` is `cross-site` or `same-site` and the origin isn't trusted via CORS, the request is denied.
5. **No `Sec-Fetch-Site`, but `Origin` is present:** the middleware compares the `Origin` to `scheme://host[:port]` built from the request. If they match, the request is allowed; otherwise it's denied. This is the fallback path for browsers older than the Fetch Metadata spec (released \~2020).
6. **No `Sec-Fetch-Site` and no `Origin`: the request is allowed.** Browsers always send at least one of these on a write request, so a request missing both is almost certainly a non-browser client such as `curl`, Postman, a mobile app, or a server-to-server caller. CSRF is a browser-only attack vector, so these requests pass through.

The middleware records this verdict on the request rather than ending the request itself. For how and when a denied verdict turns into an HTTP `400 Bad Request` response, see [Deferred validation](#deferred-validation).

### Deferred validation

The middleware doesn't reject a request on its own. Instead, it records its verdict on the request's [IAntiforgeryValidationFeature](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryvalidationfeature)—the same feature the token-based antiforgery system uses—where a denied verdict is recorded as *invalid*. The request continues down the pipeline. An invalid verdict becomes an HTTP `400 Bad Request` only when a component that processes form data observes it. This deferral matches how the token-based system already behaves: the verdict is produced early but enforced at the point where a form is consumed.

The following components read [IAntiforgeryValidationFeature](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryvalidationfeature) and reject a request with `400 - Bad Request` when the recorded verdict is invalid:

* MVC actions protected by antiforgery.
* Minimal API endpoints that bind a form parameter.
* Blazor SSR endpoints.
* Any code that reads the request form directly, which acts as a backstop.

Each consumer first confirms that an antiforgery or CSRF middleware actually ran before it trusts the verdict, so a pipeline without either middleware doesn't produce false rejections.

A consequence of this model is that an endpoint that never reads form data runs even when the verdict is invalid. For example, a JSON API endpoint that binds its body from JSON, or a handler that ignores the request body, isn't rejected automatically on a cross-origin request. The verdict is still recorded on [IAntiforgeryValidationFeature](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryvalidationfeature) for code that wants to inspect it, but nothing enforces it. CSRF is a form-and-cookie attack vector, so endpoints that don't consume a browser-submitted form generally don't need this rejection. Endpoints that do process forms—Razor Pages, MVC views, Blazor SSR, and Minimal API form binding—get the protection automatically.

### Default behavior

The middleware is registered automatically by `WebApplication.CreateBuilder` and runs after authentication and authorization. It validates each request using the registered `ICsrfProtection` implementation, which by default applies the rules described in [How it works](#how-it-works). The default implementation can be replaced; see [Customizing: implement `ICsrfProtection`](#customizing-implement-icsrfprotection). To turn the middleware off entirely, see [Disabling globally](#disabling-globally).

The result is that a minimal app with a form-handling endpoint like the following is already protected:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/widgets", ([FromForm] Widget w) =>
    Results.Created($"/widgets/{w.Id}", w));

app.Run();
```

A browser making a same-origin `POST /widgets` request reaches the endpoint normally. A browser at `https://attacker.example.com` posting the same form is rejected with `400 - Bad Request` when the endpoint binds the form, before the handler body runs. A `curl` request without `Sec-Fetch-Site` or `Origin` is allowed.

Because rejection is [deferred to form consumers](#deferred-validation), an endpoint that doesn't read form data—such as a JSON API that binds its body from JSON—isn't rejected automatically, even on a cross-origin request. The verdict is still recorded on the request for code that wants to inspect it.

The middleware integrates with the existing antiforgery model:

* **Minimal APIs:** Calling `.DisableAntiforgery()` on an endpoint opts that endpoint out of *both* the token-based middleware and CSRF protection middleware. The same metadata (`IAntiforgeryMetadata { RequiresValidation = false }`) is checked by both.
* **MVC controllers and actions:** `[IgnoreAntiforgeryToken]` also opts the endpoint out of both protections.

### Allowing cross-origin clients

The most common scenario that requires action is a browser-based client that submits a cross-origin form—for example, a site at `https://app.contoso.com` posting a form to an API at `https://api.contoso.com`. Such form posts are denied by default because `Sec-Fetch-Site` is `same-site` or `cross-site` rather than `same-origin`, and the form consumer enforces that verdict with a `400 - Bad Request`.

The CSRF middleware doesn't introduce its own trust list. It reuses the same CORS policy that the [CORS middleware](cors?view=aspnetcore-10.0) resolves for the endpoint: if that policy allows the request's `Origin` *and* the policy has `.AllowCredentials()` configured, the CSRF middleware records an allowed verdict for the request.

Allowing an origin with `WithOrigins` does not, by itself, permit browser JavaScript at that origin to read responses to credentialed requests. `.AllowCredentials()` grants that permission. Omitting it does not guarantee that requests arrive without credentials, so CORS alone is not a defense against CSRF. The CSRF middleware requires both an allowed origin and `.AllowCredentials()` as an explicit trust signal. Otherwise, the request falls through to the remaining `Sec-Fetch-Site` and Origin-vs-Host checks.

The policy is picked per-endpoint in this order:

1. `[EnableCors("api")]` (MVC) or `.RequireCors("api")` (Minimal API) → the named policy `"api"`.
2. No CORS metadata on the endpoint → the default policy registered with `AddDefaultPolicy`.
3. No matching policy (named policy not registered, no default policy, or `services.AddCors()` never called) → no CORS-derived trust. The middleware falls through to the `Sec-Fetch-Site` and Origin-vs-Host rules.

Note

The CSRF middleware trusts whatever `IsOriginAllowed` returns for the resolved CORS policy, so this includes custom predicates configured with `SetIsOriginAllowed` and wildcard-subdomain matching configured with `SetIsOriginAllowedToAllowWildcardSubdomains`. When such a policy also has `.AllowCredentials()` configured, every origin the predicate accepts is treated as CSRF-trusted. Use custom or wildcard origin matching only when every origin the predicate can match is controlled and trusted for cookie-authenticated operations, and avoid broad wildcard credentialed policies when subdomains are user-controlled, third-party-hosted, or otherwise susceptible to takeover.

A minimal example using a default policy and a Minimal API endpoint:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
        policy.WithOrigins("https://app.contoso.com")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials());
});

var app = builder.Build();

app.UseCors();

app.MapPost("/widgets", ([FromForm] Widget w) =>
    Results.Created($"/widgets/{w.Id}", w));

app.Run();
```

For a named policy on a single endpoint:

```
app.MapPost("/widgets", ([FromForm] Widget w) => Results.Created($"/widgets/{w.Id}", w))
   .RequireCors("api");
```

Warning

`AllowAnyOrigin` is intentionally **not** honored as a CSRF trust signal. (In ASP.NET Core CORS, combining `AllowAnyOrigin` with `.AllowCredentials()` is invalid and insecure.) `AllowAnyOrigin` means "any browser can read this resource," which is a different concern than "any origin may mutate state on the user's behalf." Treating `AllowAnyOrigin` as trusted would turn this middleware into a no-op for cross-origin writes. Apps that need a public-read CORS policy combined with CSRF-protected writes should list trusted write origins explicitly with `WithOrigins` or [opt write endpoints out](#opting-an-endpoint-out) if they don't rely on cookie-based authentication.

A policy that lists specific origins with `WithOrigins` but omits `.AllowCredentials()` isn't honored for CSRF trust either. Add `.AllowCredentials()` to any policy that's meant to mark cross-origin, cookie-authenticated form posts as CSRF-trusted.

`[DisableCors]` on an endpoint isn't a CSRF opt-out. It skips the CORS-derived trust step, and the request still has to satisfy the `Sec-Fetch-Site` and Origin-vs-Host rules. To opt out of CSRF protection, see [Opting an endpoint out](#opting-an-endpoint-out).

For details on configuring CORS itself—`AddCors`, `AddDefaultPolicy`, `AddPolicy`, `WithOrigins`, and the rest of the policy builder API—see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](cors?view=aspnetcore-10.0).

### Opting an endpoint out

If an endpoint isn't browser-reachable or is secured by a non-cookie mechanism, such as a bearer token or API key, opt it out individually rather than disabling the middleware globally.

**Minimal APIs** — call `DisableAntiforgery` on the endpoint or group:

```
app.MapPost("/api/webhook", (WebhookPayload p) => Results.Accepted())
   .DisableAntiforgery();
```

**MVC controllers** — apply `[IgnoreAntiforgeryToken]` to the action or controller:

```
[ApiController]
[Route("api/[controller]")]
[IgnoreAntiforgeryToken]
public class WebhookController : ControllerBase
{
    [HttpPost]
    public IActionResult Post([FromBody] WebhookPayload payload) => Accepted();
}
```

Either approach adds `IAntiforgeryMetadata { RequiresValidation = false }` to the endpoint, which the CSRF middleware honors by skipping validation.

Warning

Disabling CSRF protection on an endpoint should only be done when the endpoint isn't vulnerable to CSRF attacks—for example, endpoints that aren't callable from a browser or that are secured with non-cookie authentication such as bearer tokens or API keys. Don't disable CSRF protection on browser-accessible endpoints that rely on cookies for authentication.

### Disabling globally

The middleware can be disabled across the entire app using the `DisableCsrfProtection` configuration key. This is an escape hatch—prefer per-endpoint opt-outs.

In `appsettings.json`:

```
{
  "DisableCsrfProtection": true
}
```

Or as an environment variable:

```
ASPNETCORE_DisableCsrfProtection=true
```

When this key is set to `true`, `WebApplication` skips registering the middleware in the pipeline. The `ICsrfProtection` service remains registered, so anything that resolves it directly continues to work.

Warning

The automatic CSRF middleware also satisfies the antiforgery requirement for endpoints that require validation, even when an app doesn't call `app.UseAntiforgery()`. If an app relies on antiforgery but doesn't call `app.UseAntiforgery()`, disabling the CSRF middleware globally or running on a host that isn't built with `WebApplication`, where the middleware isn't injected, leaves those endpoints with no antiforgery middleware. A request to such an endpoint then throws an exception. Call `app.UseAntiforgery()` in that configuration.

### Browser support

`Sec-Fetch-Site` is supported by all current versions of Chromium-based browsers, Firefox, and Safari. For an authoritative compatibility table, see the [MDN reference for `Sec-Fetch-Site`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Sec-Fetch-Site).

Older browsers that predate Fetch Metadata don't send `Sec-Fetch-Site`. For those clients, the middleware falls back to comparing the `Origin` header against the request's scheme and host. Browsers have sent `Origin` on cross-origin write requests for many years, so this fallback covers essentially all legacy browser traffic.

Non-browser clients—`curl`, Postman, mobile apps, server-to-server callers—typically send neither `Sec-Fetch-Site` nor `Origin`. Those requests are allowed because CSRF is a browser-only attack vector that depends on the browser automatically attaching ambient credentials such as cookies. A non-browser client that wants to attack the API has no need for CSRF; it can simply call the API directly with whatever credentials it possesses.

### Customizing: implement `ICsrfProtection`

The decision logic lives behind a one-method interface:

```
namespace Microsoft.AspNetCore.Antiforgery;

public interface ICsrfProtection
{
    ValueTask<CsrfProtectionResult> ValidateAsync(HttpContext context);
}
```

To replace the default implementation, register a singleton in DI. Because the framework uses `TryAddSingleton`, an explicit `AddSingleton` call overrides the default:

```
builder.Services.AddSingleton<ICsrfProtection, AllowlistCsrfProtection>();
```

A custom implementation is useful when the trust model doesn't fit CORS—for example, when a fixed allowlist of partner origins is preferred, or when stricter rules are needed:

```
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;

public sealed class AllowlistCsrfProtection : ICsrfProtection
{
    private static readonly HashSet<string> SafeMethods =
        new(StringComparer.OrdinalIgnoreCase) { "GET", "HEAD", "OPTIONS", "TRACE" };

    private static readonly HashSet<string> TrustedOrigins =
        new(StringComparer.OrdinalIgnoreCase)
        {
            "https://app.contoso.com",
            "https://admin.contoso.com",
        };

    public ValueTask<CsrfProtectionResult> ValidateAsync(HttpContext context)
    {
        if (SafeMethods.Contains(context.Request.Method))
        {
            return ValueTask.FromResult(CsrfProtectionResult.Allowed());
        }

        var origin = context.Request.Headers.Origin.ToString();
        var allowed = !string.IsNullOrEmpty(origin) && TrustedOrigins.Contains(origin);

        return ValueTask.FromResult(
            allowed ? CsrfProtectionResult.Allowed() : CsrfProtectionResult.Denied());
    }
}
```

The middleware still honors `.DisableAntiforgery()` / `[IgnoreAntiforgeryToken]` regardless of which implementation is registered—opt-out is handled by the middleware itself before `ValidateAsync` is called.

### Interaction with token-based antiforgery

The two CSRF defenses target different layers and are designed to coexist. They also share the same request feature: both record their result on [IAntiforgeryValidationFeature](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryvalidationfeature), and form consumers enforce whatever verdict is present.

| Aspect               | Token-based `AntiforgeryMiddleware`                                                                             | Automatic CSRF protection middleware              |
| -------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| Introduced           | ASP.NET Core 2.0+                                                                                               | .NET 11                                           |
| Activation           | Opt-in via `app.UseAntiforgery()` (or implicitly by `AddMvc` / `MapRazorPages` / `AddRazorComponents`)          | Auto-injected by `WebApplication.CreateBuilder`   |
| Validates            | Synchronized token (form field + cookie pair)                                                                   | `Sec-Fetch-Site` / `Origin` headers               |
| Requires             | [ASP.NET Core Data Protection Overview](data-protection/introduction?view=aspnetcore-10.0) for token encryption | No tokens, no state                               |
| Browser scope        | All browsers that send cookies                                                                                  | All modern browsers; `Origin` fallback for legacy |
| Per-endpoint opt-out | `.DisableAntiforgery()` / `[IgnoreAntiforgeryToken]`                                                            | Same — both honor the same metadata               |

The token-based middleware specifically protects against the classic CSRF attack pattern in which a malicious site triggers a form `POST` to a vulnerable site using the user's ambient cookies. The automatic CSRF middleware addresses the same threat at the HTTP layer using browser-supplied metadata. Both can be active on the same endpoint, and many apps will benefit from defense in depth:

* **Razor Pages, MVC, and Blazor SSR apps** that already use the token system gain a header-based check that runs before token validation, without changing the token flow.
* **Minimal API apps** that bind forms get a useful default without needing to call `app.UseAntiforgery()` or thread `IAntiforgery` through endpoints.
* **APIs called from cross-origin SPAs** can rely on this middleware combined with a CORS allowlist, and skip the token system entirely if the API never serves HTML forms.

The automatic CSRF middleware replaces the token-based system in many scenarios, because both protect the same form-handling endpoints. Keep the token-based system when:

* The app must support browsers that don't send `Sec-Fetch-Site`. See [Browser support](#browser-support).
* The app uses [IAntiforgeryAdditionalDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider) to round-trip extra data inside the token.
* A security review or compliance requirement specifies the token defense as an independent layer.

For details on the token-based system, including form integration, AJAX flows, configuration via `AntiforgeryOptions`, and `IAntiforgery` APIs, see [Antiforgery in ASP.NET Core](#antiforgery-in-aspnet-core).

#### Token validation takes precedence

When an app calls `app.UseAntiforgery()`, the token-based middleware runs after the automatic CSRF middleware. The token middleware clears any verdict the CSRF middleware recorded and replaces it with the result of token validation. The token result is authoritative:

* A request that the CSRF middleware marked invalid becomes valid if it carries a valid token.
* A request that the CSRF middleware allowed is marked invalid if its token is missing or invalid.

This ordering means apps that use the token system see the same end-to-end behavior they had before the automatic middleware existed, while apps that don't use tokens fall back to the CSRF middleware's verdict.

### Blazor static server-side rendering

Blazor static server-side rendering (SSR) endpoints participate in the same deferred model. The Razor Components endpoint trusts the verdict recorded on [IAntiforgeryValidationFeature](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryvalidationfeature) by the upstream middleware and returns `400 - Bad Request` for a form post only when that verdict is invalid. The endpoint no longer validates the request itself.

The behavior depends on which middleware ran:

* Apps that call `app.UseAntiforgery()` are unchanged. The token-based middleware validates each request, and antiforgery tokens are generated for rendered forms as before.
* Apps that don't call `app.UseAntiforgery()` are protected by the automatic CSRF middleware instead. In that configuration, the endpoint skips antiforgery token generation because no token middleware is present to validate a token on a later request.

This is a behavior change for static SSR that previously removed `app.UseAntiforgery()`: they're now protected by the CSRF middleware rather than left unprotected, and they stop emitting antiforgery tokens. For migration guidance, see [Migrate from ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11](../migration/100-to-110?view=aspnetcore-10.0). For the formal breaking-change notice, see [Blazor server-side rendering defers antiforgery validation to middleware](/en-us/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection).

### Troubleshooting

**Symptom:** Same-origin requests from a browser succeed, but cross-origin form posts return `400 - Bad Request` with no body.

**Cause:** The CSRF middleware recorded an invalid verdict for the cross-origin request, and a form-processing component—such as an MVC action, a Minimal API form binding, or a Blazor SSR form post—enforced that verdict with a `400 - Bad Request`. This is the expected default behavior for endpoints that process forms.

**Resolution:** Choose one of the following, depending on the scenario:

* If the calling origin is known and trusted, [allow it via CORS](#allowing-cross-origin-clients).
* If the endpoint isn't browser-reachable or uses non-cookie authentication, [opt it out](#opting-an-endpoint-out) with `.DisableAntiforgery()` or `[IgnoreAntiforgeryToken]`.
* If the entire app needs to opt out (for example, during a migration window), [disable globally](#disabling-globally).

**Diagnosing:** The middleware logs every invalid verdict at `Debug` level under the category `Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware` with event name `CsrfValidationFailed`. Enable `Debug` logging for that category in `appsettings.Development.json`:

```
{
  "Logging": {
    "LogLevel": {
      "Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware": "Debug"
    }
  }
}
```

A recorded verdict then appears in the log as:

```
dbug: Microsoft.AspNetCore.Antiforgery.CsrfProtectionMiddleware[1]
      Cross-origin CSRF protection marked request POST /widgets from origin 'https://attacker.example.com' as invalid.
```

**Reproducing locally:** Use `curl` with an explicit `Origin` header to simulate a cross-origin browser request against a form endpoint:

```
curl -i -X POST https://localhost:{PORT}/widgets \
     -H "Origin: https://attacker.example.com" \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "name=test"
```

Replace `{PORT}` with the app's local HTTPS port. The `400 - Bad Request` is observed because the endpoint binds the form, which enforces the recorded verdict. A non-form endpoint returns its normal response because nothing reads the verdict. Without the `Origin` header, the same request is allowed because `curl` doesn't send `Sec-Fetch-Site` either and a request with neither header is treated as a non-browser client.

The token-based antiforgery system described in the rest of this article predates this middleware and remains available. For most apps, the automatic protection is enough on its own. For guidance on when to keep the token-based system and how to migrate, see [Migrate from ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11](../migration/100-to-110?view=aspnetcore-10.0).

[]()

## Antiforgery in ASP.NET Core

Warning

ASP.NET Core implements antiforgery using [ASP.NET Core Data Protection](data-protection/introduction?view=aspnetcore-10.0). The data protection stack must be configured to work in a server farm. For more information, see [Configuring data protection](data-protection/configuration/overview?view=aspnetcore-10.0).

Antiforgery *services* are registered in the [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) container when one of the following APIs is called in `Program.cs`:

* [AddAntiforgery](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.antiforgeryservicecollectionextensions.addantiforgery)
* [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc)
* [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)
* [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute)
* [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents)

Note

Registering the services doesn't add the Antiforgery middleware to the request processing pipeline. Calling `AddRazorComponents` or `AddAntiforgery` only registers the antiforgery services in DI; it doesn't add the middleware. Blazor and Minimal APIs require an explicit call to [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) in `Program.cs` to add the middleware to the request processing pipeline. MVC and Razor Pages validate tokens with built-in filters and don't require the middleware. For more information, see [ASP.NET Core Blazor authentication and authorization](../blazor/security/?view=aspnetcore-10.0#antiforgery-support) and [Antiforgery with Minimal APIs](#antiforgery-with-minimal-apis).

Note

Registering the services doesn't add the Antiforgery middleware to the request processing pipeline. Calling `AddRazorComponents` or `AddAntiforgery` only registers the antiforgery services in DI; it doesn't add the middleware. Blazor and Minimal APIs require an explicit call to [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) in `Program.cs` to add the middleware to the request processing pipeline (present by default in the Blazor Web App project template). MVC and Razor Pages validate tokens with built-in filters and don't require the middleware. For more information, see [ASP.NET Core Blazor authentication and authorization](../blazor/security/?view=aspnetcore-10.0#antiforgery-support) and [Antiforgery with Minimal APIs](#antiforgery-with-minimal-apis).

The [FormTagHelper](../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper) injects antiforgery tokens into HTML form elements. The following markup in a Razor file automatically generates antiforgery tokens:

```
<form method="post">
    <!-- ... -->
</form>
```

Similarly, [IHtmlHelper.BeginForm](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper.beginform) generates antiforgery tokens by default if the form's method isn't GET.

The automatic generation of antiforgery tokens for HTML form elements happens when the `<form>` tag contains the `method="post"` attribute and either of the following are true:

* The action attribute is empty (`action=""`).
* The action attribute isn't supplied (`<form method="post">`).

Automatic generation of antiforgery tokens for HTML form elements can be disabled:

* Explicitly disable antiforgery tokens with the `asp-antiforgery` attribute:

  ```
  <form method="post" asp-antiforgery="false">
      <!-- ... -->
  </form>
  ```

* The form element is opted-out of Tag Helpers by using the Tag Helper [! opt-out symbol](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0#opt-out):

  ```
  <!form method="post">
      <!-- ... -->
  </!form>
  ```

* Remove the `FormTagHelper` from the view. The `FormTagHelper` can be removed from a view by adding the following directive to the Razor view:

  ```
  @removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
  ```

Note

[Razor Pages](../razor-pages/?view=aspnetcore-10.0) are automatically protected from XSRF/CSRF. For more information, see [XSRF/CSRF and Razor Pages](../razor-pages/?view=aspnetcore-10.0#xsrfcsrf-and-razor-pages-1).

The most common approach to protecting against CSRF attacks is to use the *Synchronizer Token Pattern* (STP). STP is used when the user requests a page with form data:

1. The server sends a token associated with the current user's identity to the client.
2. The client sends back the token to the server for verification.
3. If the server receives a token that doesn't match the authenticated user's identity, the request is rejected.

The token is unique and unpredictable. The token can also be used to ensure proper sequencing of a series of requests (for example, ensuring the request sequence of: page 1 > page 2 > page 3). All of the forms in ASP.NET Core MVC and Razor Pages templates generate antiforgery tokens. The following pair of view examples generates antiforgery tokens:

```
<form asp-action="Index" asp-controller="Home" method="post">
    <!-- ... -->
</form>

@using (Html.BeginForm("Index", "Home"))
{
    <!-- ... -->
}
```

Explicitly add an antiforgery token to a `<form>` element without using Tag Helpers with the HTML helper [`@Html.AntiForgeryToken`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.htmlhelper.antiforgerytoken):

```
<form asp-action="Index" asp-controller="Home" method="post">
    @Html.AntiForgeryToken()

    <!-- ... -->
</form>
```

In each of the preceding cases, ASP.NET Core adds a hidden form field similar to the following example:

```
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">
```

ASP.NET Core includes three [filters](../mvc/controllers/filters?view=aspnetcore-10.0) for working with antiforgery tokens:

* [ValidateAntiForgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validateantiforgerytokenattribute)
* [AutoValidateAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.autovalidateantiforgerytokenattribute)
* [IgnoreAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.ignoreantiforgerytokenattribute)

## Antiforgery with `AddControllers`

Calling [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers) does ***not*** enable antiforgery tokens. [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews) must be called to have built-in antiforgery token support.

## Multiple browser tabs and the Synchronizer Token Pattern

Multiple tabs logged in as different users, or one logged in as anonymous, are not supported.

## Configure antiforgery with `AntiforgeryOptions`

Customize [AntiforgeryOptions](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions) in the app's `Program` file:

```
builder.Services.AddAntiforgery(options =>
{
    // Set Cookie properties using CookieBuilder properties†.
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});
```

Set the antiforgery cookie properties using the properties of the [CookieBuilder](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder) class, as shown in the following table.

| Option                                                                                                                           | Description                                                                                                                                                      |
| -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie)                                           | Determines the settings used to create the antiforgery cookies.                                                                                                  |
| [FormFieldName](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.formfieldname)                             | The name of the hidden form field used by the antiforgery system to render antiforgery tokens in views.                                                          |
| [HeaderName](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.headername)                                   | The name of the header used by the antiforgery system. If `null`, the system considers only form data.                                                           |
| [SuppressXFrameOptionsHeader](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.suppressxframeoptionsheader) | Specifies whether to suppress generation of the `X-Frame-Options` header. By default, the header is generated with a value of "SAMEORIGIN". Defaults to `false`. |

Some browsers don't allow insecure endpoints to set cookies with a 'secure' flag or overwrite cookies whose 'secure' flag is set (for more information, see [Deprecate modification of 'secure' cookies from non-secure origins](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-alone-01)). Since mixing secure and insecure endpoints is a common scenario in apps, ASP.NET Core relaxes the restriction on the secure policy on some cookies, such as the antiforgery cookie, by setting the cookie's [SecurePolicy](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.securepolicy) to [`CookieSecurePolicy.None`](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy). Even if a malicious user steals an antiforgery cookie, they also must steal the antiforgery token that's typically sent via a form field (more common) or a separate request header (less common) plus the authentication cookie. Cookies related to authentication or authorization use a stronger policy than [`CookieSecurePolicy.None`](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy).

Optionally, you can secure the antiforgery cookie in non-`Development` environments using Secure Sockets Layer (SSL), over HTTPS only, with the following [AntiforgeryOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie) property setting in the app's `Program` file:

```
if (!builder.Environment.IsDevelopment())
{
    builder.Services.AddAntiforgery(o =>
    {
        o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    });
}
```

For more information, see [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieauthenticationoptions).

## Generate antiforgery tokens with `IAntiforgery`

[IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) provides the API to configure antiforgery features. `IAntiforgery` can be requested in `Program.cs` using [WebApplication.Services](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.services). The following example uses middleware from the app's home page to generate an antiforgery token and send it in the response as a cookie:

```
app.UseRouting();

app.UseAuthorization();

var antiforgery = app.Services.GetRequiredService<IAntiforgery>();

app.Use((context, next) =>
{
    var requestPath = context.Request.Path.Value;

    if (string.Equals(requestPath, "/", StringComparison.OrdinalIgnoreCase)
        || string.Equals(requestPath, "/index.html", StringComparison.OrdinalIgnoreCase))
    {
        var tokenSet = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken!,
            new CookieOptions { HttpOnly = false });
    }

    return next(context);
});
```

The preceding example sets a cookie named `XSRF-TOKEN`. The client can read this cookie and provide its value as a header attached to AJAX requests. For example, Angular includes [built-in XSRF protection](https://angular.dev/best-practices/security#httpclient-xsrf-csrf-security) that reads a cookie named `XSRF-TOKEN` by default.

### Require antiforgery validation

The [ValidateAntiForgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validateantiforgerytokenattribute) action filter can be applied to an individual action, a controller, or globally. Requests made to actions that have this filter applied are blocked unless the request includes a valid antiforgery token:

```
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index()
{
    // ...

    return RedirectToAction();
}
```

The `ValidateAntiForgeryToken` attribute requires a token for requests to the action methods it marks, including HTTP GET requests. If the `ValidateAntiForgeryToken` attribute is applied across the app's controllers, it can be overridden with the `IgnoreAntiforgeryToken` attribute.

### Automatically validate antiforgery tokens for unsafe HTTP methods only

Instead of broadly applying the `ValidateAntiForgeryToken` attribute and then overriding it with `IgnoreAntiforgeryToken` attributes, the [AutoValidateAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.autovalidateantiforgerytokenattribute) attribute can be used. This attribute works identically to the `ValidateAntiForgeryToken` attribute, except that it doesn't require tokens for requests made using the following HTTP methods:

* GET
* HEAD
* OPTIONS
* TRACE

We recommend use of `AutoValidateAntiforgeryToken` broadly for non-API scenarios. This attribute ensures POST actions are protected by default. The alternative is to ignore antiforgery tokens by default, unless `ValidateAntiForgeryToken` is applied to individual action methods. It's more likely in this scenario for a POST action method to be left unprotected by mistake, leaving the app vulnerable to CSRF attacks. All POSTs should send the antiforgery token.

APIs don't have an automatic mechanism for sending the non-cookie part of the token. The implementation probably depends on the client code implementation. Some examples are shown below:

Class-level example:

```
[AutoValidateAntiforgeryToken]
public class HomeController : Controller
```

Global example:

```
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
```

### Override global or controller antiforgery attributes

The [IgnoreAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.ignoreantiforgerytokenattribute) filter is used to eliminate the need for an antiforgery token for a given action (or controller). When applied, this filter overrides `ValidateAntiForgeryToken` and `AutoValidateAntiforgeryToken` filters specified at a higher level (globally or on a controller).

```
[IgnoreAntiforgeryToken]
public IActionResult IndexOverride()
{
    // ...

    return RedirectToAction();
}
```

## Refresh tokens after authentication

Tokens should be refreshed after the user is authenticated by redirecting the user to a view or Razor Pages page.

## JavaScript, AJAX, and SPAs

In traditional HTML-based apps, antiforgery tokens are passed to the server using hidden form fields. In modern JavaScript-based apps and SPAs, many requests are made programmatically. These AJAX requests may use other techniques, such as request headers or cookies, to send the token.

If cookies are used to store authentication tokens and to authenticate API requests on the server, CSRF is a potential problem. If local storage is used to store the token, CSRF vulnerability might be mitigated because values from local storage aren't sent automatically to the server with every request. Using local storage to store the antiforgery token on the client and sending the token as a request header is a recommended approach.

### Blazor

For more information, see [ASP.NET Core Blazor authentication and authorization](../blazor/security/?view=aspnetcore-10.0#antiforgery-support).

### JavaScript

Using JavaScript with views, the token can be created using a service from within the view. Inject the [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) service into the view and call [GetAndStoreTokens](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery.getandstoretokens):

```
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery

@{
    ViewData["Title"] = "JavaScript";

    var requestToken = Antiforgery.GetAndStoreTokens(Context).RequestToken;
}

<input id="RequestVerificationToken" type="hidden" value="@requestToken" />

<button id="button" class="btn btn-primary">Submit with Token</button>
<div id="result" class="mt-2"></div>

@section Scripts {
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const resultElement = document.getElementById("result");

        document.getElementById("button").addEventListener("click", async () => {

            const response = await fetch("@Url.Action("FetchEndpoint")", {
                method: "POST",
                headers: {
                    RequestVerificationToken:
                        document.getElementById("RequestVerificationToken").value
                }
            });

            if (response.ok) {
                resultElement.innerText = await response.text();
            } else {
                resultElement.innerText = `Request Failed: ${response.status}`
            }
        });
    });
</script>
}
```

The preceding example uses JavaScript to read the hidden field value for the AJAX POST header.

This approach eliminates the need to deal directly with setting cookies from the server or reading them from the client. However, when injecting the [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) service isn't possible, use JavaScript to access tokens in cookies:

* Access tokens in an additional request to the server, typically usually `same-origin`.
* Use the cookie's contents to create a header with the token's value.

Assuming the script sends the token in a request header called `X-XSRF-TOKEN`, configure the antiforgery service to look for the `X-XSRF-TOKEN` header:

```
builder.Services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
```

The following example adds a protected endpoint that writes the request token to a JavaScript-readable cookie:

```
app.UseAuthorization();
app.MapGet("antiforgery/token", (IAntiforgery forgeryService, HttpContext context) =>
{
    var tokens = forgeryService.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
            new CookieOptions { HttpOnly = false });

    return Results.Ok();
}).RequireAuthorization();
```

The following example uses JavaScript to make an AJAX request to obtain the token and make another request with the appropriate header:

```
var response = await fetch("/antiforgery/token", {
    method: "GET",
    headers: { "Authorization": authorizationToken }
});

if (response.ok) {
    // https://developer.mozilla.org/docs/web/api/document/cookie
    const xsrfToken = document.cookie
        .split("; ")
        .find(row => row.startsWith("XSRF-TOKEN="))
        .split("=")[1];

    response = await fetch("/JavaScript/FetchEndpoint", {
        method: "POST",
        headers: { "X-XSRF-TOKEN": xsrfToken, "Authorization": authorizationToken }
    });

    if (response.ok) {
        resultElement.innerText = await response.text();
    } else {
        resultElement.innerText = `Request Failed: ${response.status}`
    }
} else {    
    resultElement.innerText = `Request Failed: ${response.status}`
}
```

[]()

Note

When the antiforgery token is provided in both the request header and in the form payload, only the token in the header is validated.

## Antiforgery with Minimal APIs

Call [AddAntiforgery](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.antiforgeryservicecollectionextensions.addantiforgery) to register antiforgery services in DI, and [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) to add the Antiforgery Middleware to the request processing pipeline. Antiforgery tokens are used to mitigate [cross-site request forgery attacks](anti-request-forgery?view=aspnetcore-10.0).

```
var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The antiforgery middleware:

* Does ***not*** short-circuit the execution of the rest of the request pipeline.
* Sets the [IAntiforgeryValidationFeature](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryvalidationfeature) in the [HttpContext.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features) of the current request.

The antiforgery token is only validated if:

* The endpoint contains metadata implementing [IAntiforgeryMetadata](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgerymetadata) where `RequiresValidation=true`.
* The HTTP method associated with the endpoint is a relevant [HTTP method](https://developer.mozilla.org/docs/Web/HTTP/Methods) of type POST, PUT, or PATCH.
* The request is associated with a valid endpoint.

Antiforgery middleware doesn't short-circuit the request pipeline. Endpoint code always runs, even if token validation fails. To observe the outcome of the token validation, resolve the [IAntiforgeryValidationFeature](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryvalidationfeature) from [HttpContext.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features) and inspect its [IsValid](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryvalidationfeature.isvalid) property or the [Error](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryvalidationfeature.error) property for failure details. This approach is useful when endpoints require custom handling for failed antiforgery validation.

***Note:*** When enabled manually, the antiforgery middleware must run after the authentication and authorization middleware to prevent reading form data when the user is unauthenticated.

By default, Minimal APIs that accept form data require antiforgery token validation and fail before running application code if antiforgery validation isn't successful.

Consider the following `GenerateForm` method:

```
public static string GenerateForm(string action, 
    AntiforgeryTokenSet token, bool UseToken=true)
{
    string tokenInput = "";
    if (UseToken)
    {
        tokenInput = $@"<input name=""{token.FormFieldName}""
                         type=""hidden"" value=""{token.RequestToken}"" />";
    }

    return $@"
    <html><body>
        <form action=""{action}"" method=""POST"" enctype=""multipart/form-data"">
            {tokenInput}
            <input type=""text"" name=""name"" />
            <input type=""date"" name=""dueDate"" />
            <input type=""checkbox"" name=""isCompleted"" />
            <input type=""submit"" />
        </form>
    </body></html>
";
}
```

The preceding code has three arguments, the action, the antiforgery token, and a `bool` indicating whether the token should be used.

Consider the following sample:

```
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

// Pass token
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    return Results.Content(MyHtml.GenerateForm("/todo", token), "text/html");
});

// Don't pass a token, fails
app.MapGet("/SkipToken", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    return Results.Content(MyHtml.GenerateForm("/todo",token, false ), "text/html");
});

// Post to /todo2. DisableAntiforgery on that endpoint so no token needed.
app.MapGet("/DisableAntiforgery", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    return Results.Content(MyHtml.GenerateForm("/todo2", token, false), "text/html");
});

app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo));

app.MapPost("/todo2", ([FromForm] Todo todo) => Results.Ok(todo))
                                                .DisableAntiforgery();

app.Run();

class Todo
{
    public required string Name { get; set; }
    public bool IsCompleted { get; set; }
    public DateTime DueDate { get; set; }
}

public static class MyHtml
{
    public static string GenerateForm(string action, 
        AntiforgeryTokenSet token, bool UseToken=true)
    {
        string tokenInput = "";
        if (UseToken)
        {
            tokenInput = $@"<input name=""{token.FormFieldName}""
                             type=""hidden"" value=""{token.RequestToken}"" />";
        }

        return $@"
        <html><body>
            <form action=""{action}"" method=""POST"" enctype=""multipart/form-data"">
                {tokenInput}
                <input type=""text"" name=""name"" />
                <input type=""date"" name=""dueDate"" />
                <input type=""checkbox"" name=""isCompleted"" />
                <input type=""submit"" />
            </form>
        </body></html>
    ";
    }
}
```

In the preceding code, posts to:

* `/todo` require a valid antiforgery token.
* `/todo2` do ***not*** require a valid antiforgery token because [DisableAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.routingendpointconventionbuilderextensions.disableantiforgery) is called.

```
app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo));

app.MapPost("/todo2", ([FromForm] Todo todo) => Results.Ok(todo))
                                                .DisableAntiforgery();
```

Warning

Calling `.DisableAntiforgery()` disables cross-site request forgery (CSRF) protection for the endpoint. This should only be used when an endpoint is not vulnerable to CSRF attacks, such as:

* Endpoints that are not callable from a browser (for example, internal APIs)
* Endpoints secured with non-cookie-based authentication (for example, bearer tokens or API keys)
* Internal or infrastructure endpoints that do not rely on user cookies

Do **not** disable antiforgery validation for browser-accessible endpoints that rely on cookies for authentication or that process user-submitted form data, as this exposes your application to CSRF attacks.

A POST to:

* `/todo` from the form generated by the `/` endpoint succeeds because the antiforgery token is valid.
* `/todo` from the form generated by the `/SkipToken` fails because the antiforgery is not included.
* `/todo2` from the form generated by the `/DisableAntiforgery` endpoint succeeds because the antiforgery is not required.

```
app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo));

app.MapPost("/todo2", ([FromForm] Todo todo) => Results.Ok(todo))
                                                .DisableAntiforgery();
```

When a form is submitted without a valid antiforgery token:

* In the `Development` environment, an exception is thrown.
* In the `Production` environment, a message is logged.

### HTTP method limitations and `HttpMethodOverrideMiddleware` interaction

For the middleware-based antiforgery path, `AntiforgeryMiddleware` and `UseAntiforgery()` validate antiforgery tokens only for HTTP **POST**, **PUT**, and **PATCH** requests. Other HTTP methods, such as **DELETE**, aren't validated automatically.

To validate antiforgery tokens for other HTTP methods, resolve [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) from DI and call [ValidateRequestAsync](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery.validaterequestasync) or [IsRequestValidAsync](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery.isrequestvalidasync) explicitly:

```
app.MapDelete("/item/{id}", async (int id, IAntiforgery antiforgery, HttpContext context) =>
{
    await antiforgery.ValidateRequestAsync(context);
    // Process the DELETE request
});
```

Warning

When `HttpMethodOverrideMiddleware` is configured with `FormFieldName` (form-field mode) and placed before `AntiforgeryMiddleware`, a POST request can be overridden to DELETE (or another non-validated method). Because `AntiforgeryMiddleware` validates only POST, PUT, and PATCH, the overridden request bypasses antiforgery validation.

To protect these endpoints:

* Prefer placing `HttpMethodOverrideMiddleware` after antiforgery validation when your pipeline allows it.
* Avoid form-field overrides for endpoints that rely on antiforgery validation.
* If form-field override must run first, validate the antiforgery token explicitly using `IAntiforgery.ValidateRequestAsync`.

For configuration details, see [ASP.NET Core middleware](../fundamentals/middleware/?view=aspnetcore-10.0).

## Windows authentication and antiforgery cookies

When using Windows Authentication, application endpoints must be protected against CSRF attacks in the same way as done for cookies. The browser implicitly sends the authentication context to the server and endpoints need to be protected against CSRF attacks.

## Extend antiforgery

The [IAntiforgeryAdditionalDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider) type allows developers to extend the behavior of the anti-CSRF system by round-tripping additional data in each token. The [GetAdditionalData](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider.getadditionaldata) method is called each time a field token is generated, and the return value is embedded within the generated token. An implementer could return a timestamp, a nonce, or any other value and then call [ValidateAdditionalData](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider.validateadditionaldata) to validate this data when the token is validated. The client's username is already embedded in the generated tokens, so there's no need to include this information. If a token includes supplemental data but no `IAntiForgeryAdditionalDataProvider` is configured, the supplemental data isn't validated.

## Additional resources

* [Host ASP.NET Core in a web farm](../host-and-deploy/web-farm?view=aspnetcore-10.0)

Cross-site request forgery (also known as XSRF or CSRF) is an attack against web-hosted apps whereby a malicious web app can influence the interaction between a client browser and a web app that trusts that browser. These attacks are possible because web browsers send some types of authentication tokens automatically with every request to a website. This form of exploit is also known as a *one-click attack* or *session riding* because the attack takes advantage of the user's previously authenticated session.

An example of a CSRF attack:

1. A user signs into `www.good-banking-site.example.com` using forms authentication. The server authenticates the user and issues a response that includes an authentication cookie. The site is vulnerable to attack because it trusts any request that it receives with a valid authentication cookie.

2. The user visits a malicious site, `www.bad-crook-site.example.com`.

   The malicious site, `www.bad-crook-site.example.com`, contains an HTML form similar to the following example:

   ```
   <h1>Congratulations! You're a Winner!</h1>
   <form action="https://www.good-banking-site.example.com/api/account" method="post">
       <input type="hidden" name="Transaction" value="withdraw" />
       <input type="hidden" name="Amount" value="1000000" />
       <input type="submit" value="Click to collect your prize!" />
   </form>
   ```

   Notice that the form's `action` posts to the vulnerable site, not to the malicious site. This is the "cross-site" part of CSRF.

3. The user selects the submit button. The browser makes the request and automatically includes the authentication cookie for the requested domain, `www.good-banking-site.example.com`.

4. The request runs on the `www.good-banking-site.example.com` server with the user's authentication context and can perform any action that an authenticated user is allowed to perform.

In addition to the scenario where the user selects the button to submit the form, the malicious site could:

* Run a script that automatically submits the form.
* Send the form submission as an AJAX request.
* Hide the form using CSS.

These alternative scenarios don't require any action or input from the user other than initially visiting the malicious site.

Using HTTPS doesn't prevent a CSRF attack. The malicious site can send `https://www.good-banking-site.example.com/` a request just as easily as it can send an insecure request.

Some attacks target endpoints that respond to GET requests, in which case an image tag can be used to perform the action. This form of attack is common on forum sites that permit images but block JavaScript. Apps that change state on GET requests, where variables or resources are altered, are vulnerable to malicious attacks. **GET requests that change state are insecure. A best practice is to never change state on a GET request.**

CSRF attacks are possible against web apps that use cookies for authentication because:

* Browsers store cookies issued by a web app.
* Stored cookies include session cookies for authenticated users.
* Browsers send all of the cookies associated with a domain to the web app every request regardless of how the request to app was generated within the browser.

However, CSRF attacks aren't limited to exploiting cookies. For example, Basic and Digest authentication are also vulnerable. After a user signs in with Basic or Digest authentication, the browser automatically sends the credentials until the session ends.

In this context, *session* refers to the client-side session during which the user is authenticated. It's unrelated to server-side sessions or [ASP.NET Core session middleware](../fundamentals/app-state?view=aspnetcore-10.0).

Users can protect against CSRF vulnerabilities by taking precautions:

* Sign out of web apps when finished using them.
* Clear browser cookies periodically.

However, CSRF vulnerabilities are fundamentally a problem with the web app, not the end user.

## Authentication fundamentals

Cookie-based authentication is a popular form of authentication. Token-based authentication systems are growing in popularity, especially for Single Page Applications (SPAs).

### Cookie-based authentication

When a user authenticates using their username and password, they're issued a token, containing an authentication ticket that can be used for authentication and authorization. The token is stored as a cookie that's sent with every request the client makes. Generating and validating this cookie is performed by the cookie authentication middleware. The [middleware](../fundamentals/middleware/?view=aspnetcore-10.0) serializes a user principal into an encrypted cookie. On subsequent requests, the middleware validates the cookie, recreates the principal, and assigns the principal to the [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user) property.

### Token-based authentication

When a user is authenticated, they're issued a token (not an antiforgery token). The token contains user information in the form of [claims](/en-us/dotnet/framework/security/claims-based-identity-model) or a reference token that points the app to user state maintained in the app. When a user attempts to access a resource that requires authentication, the token is sent to the app with an extra authorization header in the form of a Bearer token. This approach makes the app stateless. In each subsequent request, the token is passed in the request for server-side validation. This token isn't *encrypted*; it's *encoded*. On the server, the token is decoded to access its information. To send the token on subsequent requests, store the token in the browser's local storage. Placing a token in the browser local storage and retrieving it and using it as a bearer token provides protection against CSRF attacks. However, should the app be vulnerable to script injection via XSS or a compromised external javascript file, a cyberattacker could retrieve any value from local storage and send it to themselves. ASP.NET Core encodes all server side output from variables by default, reducing the risk of XSS. If you override this behavior by using [Html.Raw](/en-us/dotnet/api/system.web.mvc.htmlhelper.raw) or custom code with untrusted input then you may increase the risk of XSS.

Don't be concerned about CSRF vulnerability if the token is stored in the browser's local storage. CSRF is a concern when the token is stored in a cookie. For more information, see the GitHub issue [SPA code sample adds two cookies](https://github.com/dotnet/AspNetCore.Docs/issues/13369).

### Multiple apps hosted at one domain

Shared hosting environments are vulnerable to session hijacking, login CSRF, and other attacks.

Although `example1.contoso.net` and `example2.contoso.net` are different hosts, there's an implicit trust relationship between hosts under the `*.contoso.net` domain. This implicit trust relationship allows potentially untrusted hosts to affect each other's cookies (the same-origin policies that govern AJAX requests don't necessarily apply to HTTP cookies).

Attacks that exploit trusted cookies between apps hosted on the same domain can be prevented by not sharing domains. When each app is hosted on its own domain, there's no implicit cookie trust relationship to exploit.

[]()

## Antiforgery in ASP.NET Core

Warning

ASP.NET Core implements antiforgery using [ASP.NET Core Data Protection](data-protection/introduction?view=aspnetcore-10.0). The data protection stack must be configured to work in a server farm. For more information, see [Configuring data protection](data-protection/configuration/overview?view=aspnetcore-10.0).

Antiforgery *services* are registered in the [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) container when one of the following APIs is called in `Program.cs`:

* [AddAntiforgery](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.antiforgeryservicecollectionextensions.addantiforgery)
* [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc)
* [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)
* [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute)
* [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub)

The [FormTagHelper](../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper) injects antiforgery tokens into HTML form elements. The following markup in a Razor file automatically generates antiforgery tokens:

```
<form method="post">
    <!-- ... -->
</form>
```

Similarly, [IHtmlHelper.BeginForm](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper.beginform) generates antiforgery tokens by default if the form's method isn't GET.

The automatic generation of antiforgery tokens for HTML form elements happens when the `<form>` tag contains the `method="post"` attribute and either of the following are true:

* The action attribute is empty (`action=""`).
* The action attribute isn't supplied (`<form method="post">`).

Automatic generation of antiforgery tokens for HTML form elements can be disabled:

* Explicitly disable antiforgery tokens with the `asp-antiforgery` attribute:

  ```
  <form method="post" asp-antiforgery="false">
      <!-- ... -->
  </form>
  ```

* The form element is opted-out of Tag Helpers by using the Tag Helper [! opt-out symbol](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0#opt-out):

  ```
  <!form method="post">
      <!-- ... -->
  </!form>
  ```

* Remove the `FormTagHelper` from the view. The `FormTagHelper` can be removed from a view by adding the following directive to the Razor view:

  ```
  @removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
  ```

Note

[Razor Pages](../razor-pages/?view=aspnetcore-10.0) are automatically protected from XSRF/CSRF. For more information, see [XSRF/CSRF and Razor Pages](../razor-pages/?view=aspnetcore-10.0#xsrfcsrf-and-razor-pages-1).

The most common approach to defending against CSRF attacks is to use the *Synchronizer Token Pattern* (STP). STP is used when the user requests a page with form data:

1. The server sends a token associated with the current user's identity to the client.
2. The client sends back the token to the server for verification.
3. If the server receives a token that doesn't match the authenticated user's identity, the request is rejected.

The token is unique and unpredictable. The token can also be used to ensure proper sequencing of a series of requests (for example, ensuring the request sequence of: page 1 > page 2 > page 3). All of the forms in ASP.NET Core MVC and Razor Pages templates generate antiforgery tokens. The following pair of view examples generates antiforgery tokens:

```
<form asp-action="Index" asp-controller="Home" method="post">
    <!-- ... -->
</form>

@using (Html.BeginForm("Index", "Home"))
{
    <!-- ... -->
}
```

Explicitly add an antiforgery token to a `<form>` element without using Tag Helpers with the HTML helper [`@Html.AntiForgeryToken`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.htmlhelper.antiforgerytoken):

```
<form asp-action="Index" asp-controller="Home" method="post">
    @Html.AntiForgeryToken()

    <!-- ... -->
</form>
```

In each of the preceding cases, ASP.NET Core adds a hidden form field similar to the following example:

```
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">
```

ASP.NET Core includes three [filters](../mvc/controllers/filters?view=aspnetcore-10.0) for working with antiforgery tokens:

* [ValidateAntiForgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validateantiforgerytokenattribute)
* [AutoValidateAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.autovalidateantiforgerytokenattribute)
* [IgnoreAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.ignoreantiforgerytokenattribute)

## Antiforgery with `AddControllers`

Calling [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers) does ***not*** enable antiforgery tokens. [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews) must be called to have built-in antiforgery token support.

## Multiple browser tabs and the Synchronizer Token Pattern

With the Synchronizer Token Pattern, only the most recently loaded page contains a valid antiforgery token. Using multiple tabs can be problematic. For example, if a user opens multiple tabs:

* Only the most recently loaded tab contains a valid antiforgery token.
* Requests made from previously loaded tabs fail with an error: `Antiforgery token validation failed. The antiforgery cookie token and request token do not match`

Consider alternative CSRF protection patterns if this poses an issue.

## Configure antiforgery with `AntiforgeryOptions`

Customize [AntiforgeryOptions](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions) in the app's `Program` file:

```
builder.Services.AddAntiforgery(options =>
{
    // Set Cookie properties using CookieBuilder properties†.
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});
```

Set the antiforgery cookie properties using the properties of the [CookieBuilder](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder) class, as shown in the following table.

| Option                                                                                                                           | Description                                                                                                                                                      |
| -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie)                                           | Determines the settings used to create the antiforgery cookies.                                                                                                  |
| [FormFieldName](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.formfieldname)                             | The name of the hidden form field used by the antiforgery system to render antiforgery tokens in views.                                                          |
| [HeaderName](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.headername)                                   | The name of the header used by the antiforgery system. If `null`, the system considers only form data.                                                           |
| [SuppressXFrameOptionsHeader](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.suppressxframeoptionsheader) | Specifies whether to suppress generation of the `X-Frame-Options` header. By default, the header is generated with a value of "SAMEORIGIN". Defaults to `false`. |

Some browsers don't allow insecure endpoints to set cookies with a 'secure' flag or overwrite cookies whose 'secure' flag is set (for more information, see [Deprecate modification of 'secure' cookies from non-secure origins](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-alone-01)). Since mixing secure and insecure endpoints is a common scenario in apps, ASP.NET Core relaxes the restriction on the secure policy on some cookies, such as the antiforgery cookie, by setting the cookie's [SecurePolicy](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.securepolicy) to [`CookieSecurePolicy.None`](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy). Even if a malicious user steals an antiforgery cookie, they also must steal the antiforgery token that's typically sent via a form field (more common) or a separate request header (less common) plus the authentication cookie. Cookies related to authentication or authorization use a stronger policy than [`CookieSecurePolicy.None`](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy).

Optionally, you can secure the antiforgery cookie in non-`Development` environments using Secure Sockets Layer (SSL), over HTTPS only, with the following [AntiforgeryOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie) property setting in the app's `Program` file:

```
if (!builder.Environment.IsDevelopment())
{
    builder.Services.AddAntiforgery(o =>
    {
        o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    });
}
```

For more information, see [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieauthenticationoptions).

## Generate antiforgery tokens with `IAntiforgery`

[IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) provides the API to configure antiforgery features. `IAntiforgery` can be requested in `Program.cs` using [WebApplication.Services](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.services). The following example uses middleware from the app's home page to generate an antiforgery token and send it in the response as a cookie:

```
app.UseRouting();

app.UseAuthorization();

var antiforgery = app.Services.GetRequiredService<IAntiforgery>();

app.Use((context, next) =>
{
    var requestPath = context.Request.Path.Value;

    if (string.Equals(requestPath, "/", StringComparison.OrdinalIgnoreCase)
        || string.Equals(requestPath, "/index.html", StringComparison.OrdinalIgnoreCase))
    {
        var tokenSet = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken!,
            new CookieOptions { HttpOnly = false });
    }

    return next(context);
});
```

The preceding example sets a cookie named `XSRF-TOKEN`. The client can read this cookie and provide its value as a header attached to AJAX requests. For example, Angular includes [built-in XSRF protection](https://angular.dev/best-practices/security#httpclient-xsrf-csrf-security) that reads a cookie named `XSRF-TOKEN` by default.

### Require antiforgery validation

The [ValidateAntiForgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validateantiforgerytokenattribute) action filter can be applied to an individual action, a controller, or globally. Requests made to actions that have this filter applied are blocked unless the request includes a valid antiforgery token:

```
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index()
{
    // ...

    return RedirectToAction();
}
```

The `ValidateAntiForgeryToken` attribute requires a token for requests to the action methods it marks, including HTTP GET requests. If the `ValidateAntiForgeryToken` attribute is applied across the app's controllers, it can be overridden with the `IgnoreAntiforgeryToken` attribute.

### Automatically validate antiforgery tokens for unsafe HTTP methods only

Instead of broadly applying the `ValidateAntiForgeryToken` attribute and then overriding it with `IgnoreAntiforgeryToken` attributes, the [AutoValidateAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.autovalidateantiforgerytokenattribute) attribute can be used. This attribute works identically to the `ValidateAntiForgeryToken` attribute, except that it doesn't require tokens for requests made using the following HTTP methods:

* GET
* HEAD
* OPTIONS
* TRACE

We recommend use of `AutoValidateAntiforgeryToken` broadly for non-API scenarios. This attribute ensures POST actions are protected by default. The alternative is to ignore antiforgery tokens by default, unless `ValidateAntiForgeryToken` is applied to individual action methods. It's more likely in this scenario for a POST action method to be left unprotected by mistake, leaving the app vulnerable to CSRF attacks. All POSTs should send the antiforgery token.

APIs don't have an automatic mechanism for sending the non-cookie part of the token. The implementation probably depends on the client code implementation. Some examples are shown below:

Class-level example:

```
[AutoValidateAntiforgeryToken]
public class HomeController : Controller
```

Global example:

```
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
```

### Override global or controller antiforgery attributes

The [IgnoreAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.ignoreantiforgerytokenattribute) filter is used to eliminate the need for an antiforgery token for a given action (or controller). When applied, this filter overrides `ValidateAntiForgeryToken` and `AutoValidateAntiforgeryToken` filters specified at a higher level (globally or on a controller).

```
[IgnoreAntiforgeryToken]
public IActionResult IndexOverride()
{
    // ...

    return RedirectToAction();
}
```

## Refresh tokens after authentication

Tokens should be refreshed after the user is authenticated by redirecting the user to a view or Razor Pages page.

## JavaScript, AJAX, and SPAs

In traditional HTML-based apps, antiforgery tokens are passed to the server using hidden form fields. In modern JavaScript-based apps and SPAs, many requests are made programmatically. These AJAX requests may use other techniques (such as request headers or cookies) to send the token.

If cookies are used to store authentication tokens and to authenticate API requests on the server, CSRF is a potential problem. If local storage is used to store the token, CSRF vulnerability might be mitigated because values from local storage aren't sent automatically to the server with every request. Using local storage to store the antiforgery token on the client and sending the token as a request header is a recommended approach.

### JavaScript

Using JavaScript with views, the token can be created using a service from within the view. Inject the [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) service into the view and call [GetAndStoreTokens](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery.getandstoretokens):

```
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery

@{
    ViewData["Title"] = "JavaScript";

    var requestToken = Antiforgery.GetAndStoreTokens(Context).RequestToken;
}

<input id="RequestVerificationToken" type="hidden" value="@requestToken" />

<button id="button" class="btn btn-primary">Submit with Token</button>
<div id="result" class="mt-2"></div>

@section Scripts {
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const resultElement = document.getElementById("result");

        document.getElementById("button").addEventListener("click", async () => {

            const response = await fetch("@Url.Action("FetchEndpoint")", {
                method: "POST",
                headers: {
                    RequestVerificationToken:
                        document.getElementById("RequestVerificationToken").value
                }
            });

            if (response.ok) {
                resultElement.innerText = await response.text();
            } else {
                resultElement.innerText = `Request Failed: ${response.status}`
            }
        });
    });
</script>
}
```

The preceding example uses JavaScript to read the hidden field value for the AJAX POST header.

This approach eliminates the need to deal directly with setting cookies from the server or reading them from the client. However, when injecting the [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) service isn't possible, use JavaScript to access tokens in cookies:

* Access tokens in an additional request to the server, typically usually `same-origin`.
* Use the cookie's contents to create a header with the token's value.

Assuming the script sends the token in a request header called `X-XSRF-TOKEN`, configure the antiforgery service to look for the `X-XSRF-TOKEN` header:

```
builder.Services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
```

The following example adds a protected endpoint that writes the request token to a JavaScript-readable cookie:

```
app.UseAuthorization();
app.MapGet("antiforgery/token", (IAntiforgery forgeryService, HttpContext context) =>
{
    var tokens = forgeryService.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
            new CookieOptions { HttpOnly = false });

    return Results.Ok();
}).RequireAuthorization();
```

The following example uses JavaScript to make an AJAX request to obtain the token and make another request with the appropriate header:

```
var response = await fetch("/antiforgery/token", {
    method: "GET",
    headers: { "Authorization": authorizationToken }
});

if (response.ok) {
    // https://developer.mozilla.org/docs/web/api/document/cookie
    const xsrfToken = document.cookie
        .split("; ")
        .find(row => row.startsWith("XSRF-TOKEN="))
        .split("=")[1];

    response = await fetch("/JavaScript/FetchEndpoint", {
        method: "POST",
        headers: { "X-XSRF-TOKEN": xsrfToken, "Authorization": authorizationToken }
    });

    if (response.ok) {
        resultElement.innerText = await response.text();
    } else {
        resultElement.innerText = `Request Failed: ${response.status}`
    }
} else {    
    resultElement.innerText = `Request Failed: ${response.status}`
}
```

[]()

Note

When the antiforgery token is provided in both the request header and in the form payload, only the token in the header is validated.

### Antiforgery with Minimal APIs

`Minimal APIs` do not support the usage of the included filters (`ValidateAntiForgeryToken`, `AutoValidateAntiforgeryToken`, `IgnoreAntiforgeryToken`), however, [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) provides the required APIs to validate a request.

The following example creates a filter that validates the antiforgery token:

```
internal static class AntiForgeryExtensions
{
    public static TBuilder ValidateAntiforgery<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
    {
        return builder.AddEndpointFilter(routeHandlerFilter: async (context, next) =>
        {
            try
            {
                var antiForgeryService = context.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
                await antiForgeryService.ValidateRequestAsync(context.HttpContext);
            }
            catch (AntiforgeryValidationException)
            {
                return Results.BadRequest("Antiforgery token validation failed.");
            }

            return await next(context);

        });
    }
}
```

The filter can then be applied to an endpoint:

```
app.MapPost("api/upload", (IFormFile name) => Results.Accepted())
    .RequireAuthorization()
    .ValidateAntiforgery();
```

## Windows authentication and antiforgery cookies

When using Windows Authentication, application endpoints must be protected against CSRF attacks in the same way as done for cookies. The browser implicitly sends the authentication context to the server and endpoints need to be protected against CSRF attacks.

## Extend antiforgery

The [IAntiforgeryAdditionalDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider) type allows developers to extend the behavior of the anti-CSRF system by round-tripping additional data in each token. The [GetAdditionalData](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider.getadditionaldata) method is called each time a field token is generated, and the return value is embedded within the generated token. An implementer could return a timestamp, a nonce, or any other value and then call [ValidateAdditionalData](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider.validateadditionaldata) to validate this data when the token is validated. The client's username is already embedded in the generated tokens, so there's no need to include this information. If a token includes supplemental data but no `IAntiForgeryAdditionalDataProvider` is configured, the supplemental data isn't validated.

## Additional resources

* [Host ASP.NET Core in a web farm](../host-and-deploy/web-farm?view=aspnetcore-10.0)

Cross-site request forgery (also known as XSRF or CSRF) is an attack against web-hosted apps whereby a malicious web app can influence the interaction between a client browser and a web app that trusts that browser. These attacks are possible because web browsers send some types of authentication tokens automatically with every request to a website. This form of exploit is also known as a *one-click attack* or *session riding* because the attack takes advantage of the user's previously authenticated session.

An example of a CSRF attack:

1. A user signs into `www.good-banking-site.example.com` using forms authentication. The server authenticates the user and issues a response that includes an authentication cookie. The site is vulnerable to attack because it trusts any request that it receives with a valid authentication cookie.

2. The user visits a malicious site, `www.bad-crook-site.example.com`.

   The malicious site, `www.bad-crook-site.example.com`, contains an HTML form similar to the following example:

   ```
   <h1>Congratulations! You're a Winner!</h1>
   <form action="https://www.good-banking-site.example.com/api/account" method="post">
       <input type="hidden" name="Transaction" value="withdraw" />
       <input type="hidden" name="Amount" value="1000000" />
       <input type="submit" value="Click to collect your prize!" />
   </form>
   ```

   Notice that the form's `action` posts to the vulnerable site, not to the malicious site. This is the "cross-site" part of CSRF.

3. The user selects the submit button. The browser makes the request and automatically includes the authentication cookie for the requested domain, `www.good-banking-site.example.com`.

4. The request runs on the `www.good-banking-site.example.com` server with the user's authentication context and can perform any action that an authenticated user is allowed to perform.

In addition to the scenario where the user selects the button to submit the form, the malicious site could:

* Run a script that automatically submits the form.
* Send the form submission as an AJAX request.
* Hide the form using CSS.

These alternative scenarios don't require any action or input from the user other than initially visiting the malicious site.

Using HTTPS doesn't prevent a CSRF attack. The malicious site can send `https://www.good-banking-site.example.com/` a request just as easily as it can send an insecure request.

Some attacks target endpoints that respond to GET requests, in which case an image tag can be used to perform the action. This form of attack is common on forum sites that permit images but block JavaScript. Apps that change state on GET requests, where variables or resources are altered, are vulnerable to malicious attacks. **GET requests that change state are insecure. A best practice is to never change state on a GET request.**

CSRF attacks are possible against web apps that use cookies for authentication because:

* Browsers store cookies issued by a web app.
* Stored cookies include session cookies for authenticated users.
* Browsers send all of the cookies associated with a domain to the web app every request regardless of how the request to app was generated within the browser.

However, CSRF attacks aren't limited to exploiting cookies. For example, Basic and Digest authentication are also vulnerable. After a user signs in with Basic or Digest authentication, the browser automatically sends the credentials until the session ends.

In this context, *session* refers to the client-side session during which the user is authenticated. It's unrelated to server-side sessions or [ASP.NET Core session middleware](../fundamentals/app-state?view=aspnetcore-10.0).

Users can protect against CSRF vulnerabilities by taking precautions:

* Sign out of web apps when finished using them.
* Clear browser cookies periodically.

However, CSRF vulnerabilities are fundamentally a problem with the web app, not the end user.

## Authentication fundamentals

Cookie-based authentication is a popular form of authentication. Token-based authentication systems are growing in popularity, especially for Single Page Applications (SPAs).

### Cookie-based authentication

When a user authenticates using their username and password, they're issued a token, containing an authentication ticket that can be used for authentication and authorization. The token is stored as a cookie that's sent with every request the client makes. Generating and validating this cookie is performed by the cookie authentication middleware. The [middleware](../fundamentals/middleware/?view=aspnetcore-10.0) serializes a user principal into an encrypted cookie. On subsequent requests, the middleware validates the cookie, recreates the principal, and assigns the principal to the [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user) property.

### Token-based authentication

When a user is authenticated, they're issued a token (not an antiforgery token). The token contains user information in the form of [claims](/en-us/dotnet/framework/security/claims-based-identity-model) or a reference token that points the app to user state maintained in the app. When a user attempts to access a resource that requires authentication, the token is sent to the app with an extra authorization header in the form of a Bearer token. This approach makes the app stateless. In each subsequent request, the token is passed in the request for server-side validation. This token isn't *encrypted*; it's *encoded*. On the server, the token is decoded to access its information. To send the token on subsequent requests, store the token in the browser's local storage. Don't be concerned about CSRF vulnerability if the token is stored in the browser's local storage. CSRF is a concern when the token is stored in a cookie. For more information, see the GitHub issue [SPA code sample adds two cookies](https://github.com/dotnet/AspNetCore.Docs/issues/13369).

### Multiple apps hosted at one domain

Shared hosting environments are vulnerable to session hijacking, login CSRF, and other attacks.

Although `example1.contoso.net` and `example2.contoso.net` are different hosts, there's an implicit trust relationship between hosts under the `*.contoso.net` domain. This implicit trust relationship allows potentially untrusted hosts to affect each other's cookies (the same-origin policies that govern AJAX requests don't necessarily apply to HTTP cookies).

Attacks that exploit trusted cookies between apps hosted on the same domain can be prevented by not sharing domains. When each app is hosted on its own domain, there's no implicit cookie trust relationship to exploit.

## Antiforgery in ASP.NET Core

Warning

ASP.NET Core implements antiforgery using [ASP.NET Core Data Protection](data-protection/introduction?view=aspnetcore-10.0). The data protection stack must be configured to work in a server farm. For more information, see [Configuring data protection](data-protection/configuration/overview?view=aspnetcore-10.0).

Antiforgery *services* are registered in the [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) container when one of the following APIs is called in `Program.cs`:

* [AddAntiforgery](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.antiforgeryservicecollectionextensions.addantiforgery)
* [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc)
* [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)
* [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute)
* [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub)

The [FormTagHelper](../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper) injects antiforgery tokens into HTML form elements. The following markup in a Razor file automatically generates antiforgery tokens:

```
<form method="post">
    <!-- ... -->
</form>
```

Similarly, [IHtmlHelper.BeginForm](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper.beginform) generates antiforgery tokens by default if the form's method isn't GET.

The automatic generation of antiforgery tokens for HTML form elements happens when the `<form>` tag contains the `method="post"` attribute and either of the following are true:

* The action attribute is empty (`action=""`).
* The action attribute isn't supplied (`<form method="post">`).

Automatic generation of antiforgery tokens for HTML form elements can be disabled:

* Explicitly disable antiforgery tokens with the `asp-antiforgery` attribute:

  ```
  <form method="post" asp-antiforgery="false">
      <!-- ... -->
  </form>
  ```

* The form element is opted-out of Tag Helpers by using the Tag Helper [! opt-out symbol](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0#opt-out):

  ```
  <!form method="post">
      <!-- ... -->
  </!form>
  ```

* Remove the `FormTagHelper` from the view. The `FormTagHelper` can be removed from a view by adding the following directive to the Razor view:

  ```
  @removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
  ```

Note

[Razor Pages](../razor-pages/?view=aspnetcore-10.0) are automatically protected from XSRF/CSRF. For more information, see [XSRF/CSRF and Razor Pages](../razor-pages/?view=aspnetcore-10.0#xsrfcsrf-and-razor-pages-1).

The most common approach to defending against CSRF attacks is to use the *Synchronizer Token Pattern* (STP). STP is used when the user requests a page with form data:

1. The server sends a token associated with the current user's identity to the client.
2. The client sends back the token to the server for verification.
3. If the server receives a token that doesn't match the authenticated user's identity, the request is rejected.

The token is unique and unpredictable. The token can also be used to ensure proper sequencing of a series of requests (for example, ensuring the request sequence of: page 1 > page 2 > page 3). All of the forms in ASP.NET Core MVC and Razor Pages templates generate antiforgery tokens. The following pair of view examples generates antiforgery tokens:

```
<form asp-action="Index" asp-controller="Home" method="post">
    <!-- ... -->
</form>

@using (Html.BeginForm("Index", "Home"))
{
    <!-- ... -->
}
```

Explicitly add an antiforgery token to a `<form>` element without using Tag Helpers with the HTML helper [`@Html.AntiForgeryToken`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.htmlhelper.antiforgerytoken):

```
<form asp-action="Index" asp-controller="Home" method="post">
    @Html.AntiForgeryToken()

    <!-- ... -->
</form>
```

In each of the preceding cases, ASP.NET Core adds a hidden form field similar to the following example:

```
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">
```

ASP.NET Core includes three [filters](../mvc/controllers/filters?view=aspnetcore-10.0) for working with antiforgery tokens:

* [ValidateAntiForgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validateantiforgerytokenattribute)
* [AutoValidateAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.autovalidateantiforgerytokenattribute)
* [IgnoreAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.ignoreantiforgerytokenattribute)

## Antiforgery with `AddControllers`

Calling [AddControllers](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollers) does ***not*** enable antiforgery tokens. [AddControllersWithViews](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addcontrollerswithviews) must be called to have built-in antiforgery token support.

## Multiple browser tabs and the Synchronizer Token Pattern

With the Synchronizer Token Pattern, only the most recently loaded page contains a valid antiforgery token. Using multiple tabs can be problematic. For example, if a user opens multiple tabs:

* Only the most recently loaded tab contains a valid antiforgery token.
* Requests made from previously loaded tabs fail with an error: `Antiforgery token validation failed. The antiforgery cookie token and request token do not match`

Consider alternative CSRF protection patterns if this poses an issue.

## Configure antiforgery with `AntiforgeryOptions`

Customize [AntiforgeryOptions](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions) in the app's `Program` file:

```
builder.Services.AddAntiforgery(options =>
{
    // Set Cookie properties using CookieBuilder properties†.
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});
```

Set the antiforgery cookie properties using the properties of the [CookieBuilder](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder) class, as shown in the following table.

| Option                                                                                                                           | Description                                                                                                                                                      |
| -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie)                                           | Determines the settings used to create the antiforgery cookies.                                                                                                  |
| [FormFieldName](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.formfieldname)                             | The name of the hidden form field used by the antiforgery system to render antiforgery tokens in views.                                                          |
| [HeaderName](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.headername)                                   | The name of the header used by the antiforgery system. If `null`, the system considers only form data.                                                           |
| [SuppressXFrameOptionsHeader](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.suppressxframeoptionsheader) | Specifies whether to suppress generation of the `X-Frame-Options` header. By default, the header is generated with a value of "SAMEORIGIN". Defaults to `false`. |

Some browsers don't allow insecure endpoints to set cookies with a 'secure' flag or overwrite cookies whose 'secure' flag is set (for more information, see [Deprecate modification of 'secure' cookies from non-secure origins](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-alone-01)). Since mixing secure and insecure endpoints is a common scenario in apps, ASP.NET Core relaxes the restriction on the secure policy on some cookies, such as the antiforgery cookie, by setting the cookie's [SecurePolicy](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.securepolicy) to [`CookieSecurePolicy.None`](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy). Even if a malicious user steals an antiforgery cookie, they also must steal the antiforgery token that's typically sent via a form field (more common) or a separate request header (less common) plus the authentication cookie. Cookies related to authentication or authorization use a stronger policy than [`CookieSecurePolicy.None`](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy).

Optionally, you can secure the antiforgery cookie in non-`Development` environments using Secure Sockets Layer (SSL), over HTTPS only, with the following [AntiforgeryOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie) property setting in the app's `Program` file:

```
if (!builder.Environment.IsDevelopment())
{
    builder.Services.AddAntiforgery(o =>
    {
        o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    });
}
```

For more information, see [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieauthenticationoptions).

## Generate antiforgery tokens with `IAntiforgery`

[IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) provides the API to configure antiforgery features. `IAntiforgery` can be requested in `Program.cs` using [WebApplication.Services](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.services). The following example uses middleware from the app's home page to generate an antiforgery token and send it in the response as a cookie:

```
app.UseRouting();

app.UseAuthorization();

var antiforgery = app.Services.GetRequiredService<IAntiforgery>();

app.Use((context, next) =>
{
    var requestPath = context.Request.Path.Value;

    if (string.Equals(requestPath, "/", StringComparison.OrdinalIgnoreCase)
        || string.Equals(requestPath, "/index.html", StringComparison.OrdinalIgnoreCase))
    {
        var tokenSet = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken!,
            new CookieOptions { HttpOnly = false });
    }

    return next(context);
});
```

The preceding example sets a cookie named `XSRF-TOKEN`. The client can read this cookie and provide its value as a header attached to AJAX requests. For example, Angular includes [built-in XSRF protection](https://angular.dev/best-practices/security#httpclient-xsrf-csrf-security) that reads a cookie named `XSRF-TOKEN` by default.

### Require antiforgery validation

The [ValidateAntiForgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validateantiforgerytokenattribute) action filter can be applied to an individual action, a controller, or globally. Requests made to actions that have this filter applied are blocked unless the request includes a valid antiforgery token:

```
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index()
{
    // ...

    return RedirectToAction();
}
```

The `ValidateAntiForgeryToken` attribute requires a token for requests to the action methods it marks, including HTTP GET requests. If the `ValidateAntiForgeryToken` attribute is applied across the app's controllers, it can be overridden with the `IgnoreAntiforgeryToken` attribute.

### Automatically validate antiforgery tokens for unsafe HTTP methods only

Instead of broadly applying the `ValidateAntiForgeryToken` attribute and then overriding it with `IgnoreAntiforgeryToken` attributes, the [AutoValidateAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.autovalidateantiforgerytokenattribute) attribute can be used. This attribute works identically to the `ValidateAntiForgeryToken` attribute, except that it doesn't require tokens for requests made using the following HTTP methods:

* GET
* HEAD
* OPTIONS
* TRACE

We recommend use of `AutoValidateAntiforgeryToken` broadly for non-API scenarios. This attribute ensures POST actions are protected by default. The alternative is to ignore antiforgery tokens by default, unless `ValidateAntiForgeryToken` is applied to individual action methods. It's more likely in this scenario for a POST action method to be left unprotected by mistake, leaving the app vulnerable to CSRF attacks. All POSTs should send the antiforgery token.

APIs don't have an automatic mechanism for sending the non-cookie part of the token. The implementation probably depends on the client code implementation. Some examples are shown below:

Class-level example:

```
[AutoValidateAntiforgeryToken]
public class HomeController : Controller
```

Global example:

```
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
```

### Override global or controller antiforgery attributes

The [IgnoreAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.ignoreantiforgerytokenattribute) filter is used to eliminate the need for an antiforgery token for a given action (or controller). When applied, this filter overrides `ValidateAntiForgeryToken` and `AutoValidateAntiforgeryToken` filters specified at a higher level (globally or on a controller).

```
[IgnoreAntiforgeryToken]
public IActionResult IndexOverride()
{
    // ...

    return RedirectToAction();
}
```

## Refresh tokens after authentication

Tokens should be refreshed after the user is authenticated by redirecting the user to a view or Razor Pages page.

## JavaScript, AJAX, and SPAs

In traditional HTML-based apps, antiforgery tokens are passed to the server using hidden form fields. In modern JavaScript-based apps and SPAs, many requests are made programmatically. These AJAX requests may use other techniques (such as request headers or cookies) to send the token.

If cookies are used to store authentication tokens and to authenticate API requests on the server, CSRF is a potential problem. If local storage is used to store the token, CSRF vulnerability might be mitigated because values from local storage aren't sent automatically to the server with every request. Using local storage to store the antiforgery token on the client and sending the token as a request header is a recommended approach.

### JavaScript

Using JavaScript with views, the token can be created using a service from within the view. Inject the [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) service into the view and call [GetAndStoreTokens](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery.getandstoretokens):

```
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery

@{
    ViewData["Title"] = "JavaScript";

    var requestToken = Antiforgery.GetAndStoreTokens(Context).RequestToken;
}

<input id="RequestVerificationToken" type="hidden" value="@requestToken" />

<button id="button" class="btn btn-primary">Submit with Token</button>
<div id="result" class="mt-2"></div>

@section Scripts {
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const resultElement = document.getElementById("result");

        document.getElementById("button").addEventListener("click", async () => {

            const response = await fetch("@Url.Action("FetchEndpoint")", {
                method: "POST",
                headers: {
                    RequestVerificationToken:
                        document.getElementById("RequestVerificationToken").value
                }
            });

            if (response.ok) {
                resultElement.innerText = await response.text();
            } else {
                resultElement.innerText = `Request Failed: ${response.status}`
            }
        });
    });
</script>
}
```

The preceding example uses JavaScript to read the hidden field value for the AJAX POST header.

This approach eliminates the need to deal directly with setting cookies from the server or reading them from the client. However, when injecting the [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) service is not possible, JavaScript can also access token in cookies, obtained from an additional request to the server (usually `same-origin`), and use the cookie's contents to create a header with the token's value.

Assuming the script sends the token in a request header called `X-XSRF-TOKEN`, configure the antiforgery service to look for the `X-XSRF-TOKEN` header:

```
builder.Services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
```

The following example adds a protected endpoint that will write the request token to a JavaScript-readable cookie:

```
app.UseAuthorization();
app.MapGet("antiforgery/token", (IAntiforgery forgeryService, HttpContext context) =>
{
    var tokens = forgeryService.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
            new CookieOptions { HttpOnly = false });

    return Results.Ok();
}).RequireAuthorization();
```

The following example uses JavaScript to make an AJAX request to obtain the token and make another request with the appropriate header:

```
var response = await fetch("/antiforgery/token", {
    method: "GET",
    headers: { "Authorization": authorizationToken }
});

if (response.ok) {
    // https://developer.mozilla.org/docs/web/api/document/cookie
    const xsrfToken = document.cookie
        .split("; ")
        .find(row => row.startsWith("XSRF-TOKEN="))
        .split("=")[1];

    response = await fetch("/JavaScript/FetchEndpoint", {
        method: "POST",
        headers: { "X-XSRF-TOKEN": xsrfToken, "Authorization": authorizationToken }
    });

    if (response.ok) {
        resultElement.innerText = await response.text();
    } else {
        resultElement.innerText = `Request Failed: ${response.status}`
    }
} else {    
    resultElement.innerText = `Request Failed: ${response.status}`
}
```

## Windows authentication and antiforgery cookies

When using Windows Authentication, application endpoints must be protected against CSRF attacks in the same way as done for cookies. The browser implicitly sends the authentication context to the server and so endpoints need to be protected against CSRF attacks.

## Extend antiforgery

The [IAntiforgeryAdditionalDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider) type allows developers to extend the behavior of the anti-CSRF system by round-tripping additional data in each token. The [GetAdditionalData](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider.getadditionaldata) method is called each time a field token is generated, and the return value is embedded within the generated token. An implementer could return a timestamp, a nonce, or any other value and then call [ValidateAdditionalData](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider.validateadditionaldata) to validate this data when the token is validated. The client's username is already embedded in the generated tokens, so there's no need to include this information. If a token includes supplemental data but no `IAntiForgeryAdditionalDataProvider` is configured, the supplemental data isn't validated.

## Additional resources

* [Host ASP.NET Core in a web farm](../host-and-deploy/web-farm?view=aspnetcore-10.0)

Cross-site request forgery (also known as XSRF or CSRF) is an attack against web-hosted apps whereby a malicious web app can influence the interaction between a client browser and a web app that trusts that browser. These attacks are possible because web browsers send some types of authentication tokens automatically with every request to a website. This form of exploit is also known as a *one-click attack* or *session riding* because the attack takes advantage of the user's previously authenticated session.

An example of a CSRF attack:

1. A user signs into `www.good-banking-site.example.com` using forms authentication. The server authenticates the user and issues a response that includes an authentication cookie. The site is vulnerable to attack because it trusts any request that it receives with a valid authentication cookie.

2. The user visits a malicious site, `www.bad-crook-site.example.com`.

   The malicious site, `www.bad-crook-site.example.com`, contains an HTML form similar to the following example:

   ```
   <h1>Congratulations! You're a Winner!</h1>
   <form action="https://www.good-banking-site.example.com/api/account" method="post">
       <input type="hidden" name="Transaction" value="withdraw" />
       <input type="hidden" name="Amount" value="1000000" />
       <input type="submit" value="Click to collect your prize!" />
   </form>
   ```

   Notice that the form's `action` posts to the vulnerable site, not to the malicious site. This is the "cross-site" part of CSRF.

3. The user selects the submit button. The browser makes the request and automatically includes the authentication cookie for the requested domain, `www.good-banking-site.example.com`.

4. The request runs on the `www.good-banking-site.example.com` server with the user's authentication context and can perform any action that an authenticated user is allowed to perform.

In addition to the scenario where the user selects the button to submit the form, the malicious site could:

* Run a script that automatically submits the form.
* Send the form submission as an AJAX request.
* Hide the form using CSS.

These alternative scenarios don't require any action or input from the user other than initially visiting the malicious site.

Using HTTPS doesn't prevent a CSRF attack. The malicious site can send `https://www.good-banking-site.example.com/` a request just as easily as it can send an insecure request.

Some attacks target endpoints that respond to GET requests, in which case an image tag can be used to perform the action. This form of attack is common on forum sites that permit images but block JavaScript. Apps that change state on GET requests, where variables or resources are altered, are vulnerable to malicious attacks. **GET requests that change state are insecure. A best practice is to never change state on a GET request.**

CSRF attacks are possible against web apps that use cookies for authentication because:

* Browsers store cookies issued by a web app.
* Stored cookies include session cookies for authenticated users.
* Browsers send all of the cookies associated with a domain to the web app every request regardless of how the request to app was generated within the browser.

However, CSRF attacks aren't limited to exploiting cookies. For example, Basic and Digest authentication are also vulnerable. After a user signs in with Basic or Digest authentication, the browser automatically sends the credentials until the session ends.

In this context, *session* refers to the client-side session during which the user is authenticated. It's unrelated to server-side sessions or [ASP.NET Core session middleware](../fundamentals/app-state?view=aspnetcore-10.0).

Users can protect against CSRF vulnerabilities by taking precautions:

* Sign out of web apps when finished using them.
* Clear browser cookies periodically.

However, CSRF vulnerabilities are fundamentally a problem with the web app, not the end user.

## Authentication fundamentals

Cookie-based authentication is a popular form of authentication. Token-based authentication systems are growing in popularity, especially for Single Page Applications (SPAs).

### Cookie-based authentication

When a user authenticates using their username and password, they're issued a token, containing an authentication ticket that can be used for authentication and authorization. The token is stored as a cookie that's sent with every request the client makes. Generating and validating this cookie is performed by the cookie authentication middleware. The [middleware](../fundamentals/middleware/?view=aspnetcore-10.0) serializes a user principal into an encrypted cookie. On subsequent requests, the middleware validates the cookie, recreates the principal, and assigns the principal to the [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user) property.

### Token-based authentication

When a user is authenticated, they're issued a token (not an antiforgery token). The token contains user information in the form of [claims](/en-us/dotnet/framework/security/claims-based-identity-model) or a reference token that points the app to user state maintained in the app. When a user attempts to access a resource that requires authentication, the token is sent to the app with an extra authorization header in the form of a Bearer token. This approach makes the app stateless. In each subsequent request, the token is passed in the request for server-side validation. This token isn't *encrypted*; it's *encoded*. On the server, the token is decoded to access its information. To send the token on subsequent requests, store the token in the browser's local storage. Don't be concerned about CSRF vulnerability if the token is stored in the browser's local storage. CSRF is a concern when the token is stored in a cookie. For more information, see the GitHub issue [SPA code sample adds two cookies](https://github.com/dotnet/AspNetCore.Docs/issues/13369).

### Multiple apps hosted at one domain

Shared hosting environments are vulnerable to session hijacking, login CSRF, and other attacks.

Although `example1.contoso.net` and `example2.contoso.net` are different hosts, there's an implicit trust relationship between hosts under the `*.contoso.net` domain. This implicit trust relationship allows potentially untrusted hosts to affect each other's cookies (the same-origin policies that govern AJAX requests don't necessarily apply to HTTP cookies).

Attacks that exploit trusted cookies between apps hosted on the same domain can be prevented by not sharing domains. When each app is hosted on its own domain, there's no implicit cookie trust relationship to exploit.

## ASP.NET Core antiforgery configuration

Warning

ASP.NET Core implements antiforgery using [ASP.NET Core Data Protection](data-protection/introduction?view=aspnetcore-10.0). The data protection stack must be configured to work in a server farm. For more information, see [Configuring data protection](data-protection/configuration/overview?view=aspnetcore-10.0).

Antiforgery *services* are registered in the [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) container when one of the following APIs is called in `Startup.ConfigureServices`:

* [AddAntiforgery](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.antiforgeryservicecollectionextensions.addantiforgery)
* [AddMvc](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addmvc)
* [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages)
* [MapControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapcontrollerroute)
* [MapBlazorHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.componentendpointroutebuilderextensions.mapblazorhub)

In ASP.NET Core 2.0 or later, the [FormTagHelper](../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper) injects antiforgery tokens into HTML form elements. The following markup in a Razor file automatically generates antiforgery tokens:

```
<form method="post">
    ...
</form>
```

Similarly, [IHtmlHelper.BeginForm](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.ihtmlhelper.beginform) generates antiforgery tokens by default if the form's method isn't GET.

The automatic generation of antiforgery tokens for HTML form elements happens when the `<form>` tag contains the `method="post"` attribute and either of the following are true:

* The action attribute is empty (`action=""`).
* The action attribute isn't supplied (`<form method="post">`).

Automatic generation of antiforgery tokens for HTML form elements can be disabled:

* Explicitly disable antiforgery tokens with the `asp-antiforgery` attribute:

  ```
  <form method="post" asp-antiforgery="false">
      ...
  </form>
  ```

* The form element is opted-out of Tag Helpers by using the Tag Helper [! opt-out symbol](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0#opt-out):

  ```
  <!form method="post">
      ...
  </!form>
  ```

* Remove the `FormTagHelper` from the view. The `FormTagHelper` can be removed from a view by adding the following directive to the Razor view:

  ```
  @removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
  ```

Note

[Razor Pages](../razor-pages/?view=aspnetcore-10.0) are automatically protected from XSRF/CSRF. For more information, see [XSRF/CSRF and Razor Pages](../razor-pages/?view=aspnetcore-10.0#xsrf).

The most common approach to defending against CSRF attacks is to use the *Synchronizer Token Pattern* (STP). STP is used when the user requests a page with form data:

1. The server sends a token associated with the current user's identity to the client.
2. The client sends back the token to the server for verification.
3. If the server receives a token that doesn't match the authenticated user's identity, the request is rejected.

The token is unique and unpredictable. The token can also be used to ensure proper sequencing of a series of requests (for example, ensuring the request sequence of: page 1 > page 2 > page 3). All of the forms in ASP.NET Core MVC and Razor Pages templates generate antiforgery tokens. The following pair of view examples generates antiforgery tokens:

```
<form asp-controller="Todo" asp-action="Create" method="post">
    ...
</form>

@using (Html.BeginForm("Create", "Todo"))
{
    ...
}
```

Explicitly add an antiforgery token to a `<form>` element without using Tag Helpers with the HTML helper [`@Html.AntiForgeryToken`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.htmlhelper.antiforgerytoken):

```
<form action="/" method="post">
    @Html.AntiForgeryToken()
</form>
```

In each of the preceding cases, ASP.NET Core adds a hidden form field similar to the following example:

```
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">
```

ASP.NET Core includes three [filters](../mvc/controllers/filters?view=aspnetcore-10.0) for working with antiforgery tokens:

* [ValidateAntiForgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validateantiforgerytokenattribute)
* [AutoValidateAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.autovalidateantiforgerytokenattribute)
* [IgnoreAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.ignoreantiforgerytokenattribute)

## Antiforgery options

Customize [AntiforgeryOptions](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions) in `Startup.ConfigureServices`:

```
services.AddAntiforgery(options => 
{
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});
```

Set the antiforgery cookie properties using the properties of the [CookieBuilder](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder) class, as shown in the following table.

| Option                                                                                                                           | Description                                                                                                                                                      |
| -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie)                                           | Determines the settings used to create the antiforgery cookies.                                                                                                  |
| [FormFieldName](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.formfieldname)                             | The name of the hidden form field used by the antiforgery system to render antiforgery tokens in views.                                                          |
| [HeaderName](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.headername)                                   | The name of the header used by the antiforgery system. If `null`, the system considers only form data.                                                           |
| [SuppressXFrameOptionsHeader](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.suppressxframeoptionsheader) | Specifies whether to suppress generation of the `X-Frame-Options` header. By default, the header is generated with a value of "SAMEORIGIN". Defaults to `false`. |

Some browsers don't allow insecure endpoints to set cookies with a 'secure' flag or overwrite cookies whose 'secure' flag is set (for more information, see [Deprecate modification of 'secure' cookies from non-secure origins](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-alone-01)). Since mixing secure and insecure endpoints is a common scenario in apps, ASP.NET Core relaxes the restriction on the secure policy on some cookies, such as the antiforgery cookie, by setting the cookie's [SecurePolicy](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.securepolicy) to [`CookieSecurePolicy.None`](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy). Even if a malicious user steals an antiforgery cookie, they also must steal the antiforgery token that's typically sent via a form field (more common) or a separate request header (less common) plus the authentication cookie. Cookies related to authentication or authorization use a stronger policy than [`CookieSecurePolicy.None`](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy).

Optionally, you can secure the antiforgery cookie in non-`Development` environments using Secure Sockets Layer (SSL), over HTTPS only, with the following [AntiforgeryOptions.Cookie](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.antiforgeryoptions.cookie) property setting in the app's `Startup` class:

```
public class Startup
{
    public Startup(IConfiguration configuration, IHostEnvironment environment)
    {
        Configuration = configuration;
        Environment = environment;
    }

    public IConfiguration Configuration { get; }
    public IHostEnvironment Environment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Other services are registered here

        if (!Environment.IsDevelopment())
        {
            services.AddAntiforgery(o =>
            {
                o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            });
        }
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // Request processing pipeline
    }
}
```

For more information, see [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieauthenticationoptions).

## Configure antiforgery features with IAntiforgery

[IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) provides the API to configure antiforgery features. `IAntiforgery` can be requested in the `Configure` method of the `Startup` class.

In the following example:

* Middleware from the app's home page is used to generate an antiforgery token and send it in the response as a cookie.
* The request token is sent as a JavaScript-readable cookie with the default Angular naming convention described in the [AngularJS](#angularjs) section.

```
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
    app.Use(next => context =>
    {
        string path = context.Request.Path.Value;

        if (string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
        {
            var tokens = antiforgery.GetAndStoreTokens(context);
            context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, 
                new CookieOptions() { HttpOnly = false });
        }

        return next(context);
    });
}
```

### Require antiforgery validation

[ValidateAntiForgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validateantiforgerytokenattribute) is an action filter that can be applied to an individual action, a controller, or globally. Requests made to actions that have this filter applied are blocked unless the request includes a valid antiforgery token.

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel account)
{
    ManageMessageId? message = ManageMessageId.Error;
    var user = await GetCurrentUserAsync();

    if (user != null)
    {
        var result = 
            await _userManager.RemoveLoginAsync(
                user, account.LoginProvider, account.ProviderKey);

        if (result.Succeeded)
        {
            await _signInManager.SignInAsync(user, isPersistent: false);
            message = ManageMessageId.RemoveLoginSuccess;
        }
    }

    return RedirectToAction(nameof(ManageLogins), new { Message = message });
}
```

The `ValidateAntiForgeryToken` attribute requires a token for requests to the action methods it marks, including HTTP GET requests. If the `ValidateAntiForgeryToken` attribute is applied across the app's controllers, it can be overridden with the `IgnoreAntiforgeryToken` attribute.

Note

ASP.NET Core doesn't support adding antiforgery tokens to GET requests automatically.

### Automatically validate antiforgery tokens for unsafe HTTP methods only

ASP.NET Core apps don't generate antiforgery tokens for safe HTTP methods (GET, HEAD, OPTIONS, and TRACE). Instead of broadly applying the `ValidateAntiForgeryToken` attribute and then overriding it with `IgnoreAntiforgeryToken` attributes, the [AutoValidateAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.autovalidateantiforgerytokenattribute) attribute can be used. This attribute works identically to the `ValidateAntiForgeryToken` attribute, except that it doesn't require tokens for requests made using the following HTTP methods:

* GET
* HEAD
* OPTIONS
* TRACE

We recommend use of `AutoValidateAntiforgeryToken` broadly for non-API scenarios. This attribute ensures POST actions are protected by default. The alternative is to ignore antiforgery tokens by default, unless `ValidateAntiForgeryToken` is applied to individual action methods. It's more likely in this scenario for a POST action method to be left unprotected by mistake, leaving the app vulnerable to CSRF attacks. All POSTs should send the antiforgery token.

APIs don't have an automatic mechanism for sending the non-cookie part of the token. The implementation probably depends on the client code implementation. Some examples are shown below:

Class-level example:

```
[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{
```

Global example:

```
services.AddControllersWithViews(options =>
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));
```

### Override global or controller antiforgery attributes

The [IgnoreAntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.mvc.ignoreantiforgerytokenattribute) filter is used to eliminate the need for an antiforgery token for a given action (or controller). When applied, this filter overrides `ValidateAntiForgeryToken` and `AutoValidateAntiforgeryToken` filters specified at a higher level (globally or on a controller).

```
[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{
    [HttpPost]
    [IgnoreAntiforgeryToken]
    public async Task<IActionResult> DoSomethingSafe(SomeViewModel model)
    {
        // no antiforgery token required
    }
}
```

## Refresh tokens after authentication

Tokens should be refreshed after the user is authenticated by redirecting the user to a view or Razor Pages page.

## JavaScript, AJAX, and SPAs

In traditional HTML-based apps, antiforgery tokens are passed to the server using hidden form fields. In modern JavaScript-based apps and SPAs, many requests are made programmatically. These AJAX requests may use other techniques (such as request headers or cookies) to send the token.

If cookies are used to store authentication tokens and to authenticate API requests on the server, CSRF is a potential problem. If local storage is used to store the token, CSRF vulnerability might be mitigated because values from local storage aren't sent automatically to the server with every request. Using local storage to store the antiforgery token on the client and sending the token as a request header is a recommended approach.

### JavaScript

Using JavaScript with views, the token can be created using a service from within the view. Inject the [IAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery) service into the view and call [GetAndStoreTokens](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgery.getandstoretokens):

```
@{
    ViewData["Title"] = "AJAX Demo";
}
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
    public string GetAntiXsrfRequestToken()
    {
        return Xsrf.GetAndStoreTokens(Context).RequestToken;
    }
}

<input type="hidden" id="RequestVerificationToken" 
       name="RequestVerificationToken" value="@GetAntiXsrfRequestToken()">

<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<div class="row">
    <p><input type="button" id="antiforgery" value="Antiforgery"></p>
    <script>
        var xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function() {
            if (xhttp.readyState == XMLHttpRequest.DONE) {
                if (xhttp.status == 200) {
                    alert(xhttp.responseText);
                } else {
                    alert('There was an error processing the AJAX request.');
                }
            }
        };

        document.addEventListener('DOMContentLoaded', function() {
            document.getElementById("antiforgery").onclick = function () {
                xhttp.open('POST', '@Url.Action("Antiforgery", "Home")', true);
                xhttp.setRequestHeader("RequestVerificationToken", 
                    document.getElementById('RequestVerificationToken').value);
                xhttp.send();
            }
        });
    </script>
</div>
```

This approach eliminates the need to deal directly with setting cookies from the server or reading them from the client.

The preceding example uses JavaScript to read the hidden field value for the AJAX POST header.

JavaScript can also access tokens in cookies and use the cookie's contents to create a header with the token's value.

```
context.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken, 
    new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });
```

Assuming the script requests to send the token in a header called `X-CSRF-TOKEN`, configure the antiforgery service to look for the `X-CSRF-TOKEN` header:

```
services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
```

The following example uses JavaScript to make an AJAX request with the appropriate header:

```
function getCookie(cname) {
    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) === ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) === 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

var csrfToken = getCookie("CSRF-TOKEN");

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
    if (xhttp.readyState === XMLHttpRequest.DONE) {
        if (xhttp.status === 204) {
            alert('Todo item is created successfully.');
        } else {
            alert('There was an error processing the AJAX request.');
        }
    }
};
xhttp.open('POST', '/api/items', true);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.setRequestHeader("X-CSRF-TOKEN", csrfToken);
xhttp.send(JSON.stringify({ "name": "Learn C#" }));
```

### AngularJS

AngularJS uses a convention to address CSRF. If the server sends a cookie with the name `XSRF-TOKEN`, the AngularJS `$http` service adds the cookie value to a header when it sends a request to the server. This process is automatic. The client doesn't need to set the header explicitly. The header name is `X-XSRF-TOKEN`. The server should detect this header and validate its contents.

For ASP.NET Core API to work with this convention in your application startup:

* Configure your app to provide a token in a cookie called `XSRF-TOKEN`.
* Configure the antiforgery service to look for a header named `X-XSRF-TOKEN`, which is Angular's default header name for sending the XSRF token.

```
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
    app.Use(next => context =>
    {
        string path = context.Request.Path.Value;

        if (
            string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
        {
            var tokens = antiforgery.GetAndStoreTokens(context);
            context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, 
                new CookieOptions() { HttpOnly = false });
        }

        return next(context);
    });
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
}
```

Note

When the antiforgery token is provided in both the request header and in the form payload, only the token in the header is validated.

## Windows authentication and antiforgery cookies

When using Windows Authentication, application endpoints must be protected against CSRF attacks in the same way as done for cookies. The browser implicitly sends the authentication context to the server and so endpoints need to be protected against CSRF attacks.

## Extend antiforgery

The [IAntiforgeryAdditionalDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider) type allows developers to extend the behavior of the anti-CSRF system by round-tripping additional data in each token. The [GetAdditionalData](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider.getadditionaldata) method is called each time a field token is generated, and the return value is embedded within the generated token. An implementer could return a timestamp, a nonce, or any other value and then call [ValidateAdditionalData](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider.validateadditionaldata) to validate this data when the token is validated. The client's username is already embedded in the generated tokens, so there's no need to include this information. If a token includes supplemental data but no `IAntiForgeryAdditionalDataProvider` is configured, the supplemental data isn't validated.

## Additional resources

* [Host ASP.NET Core in a web farm](../host-and-deploy/web-farm?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/mvc1006
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/mvc1006.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# MVC1006: Methods containing TagHelpers must be async and return Task

|                                     | Value    |
| ----------------------------------- | -------- |
| **Rule ID**                         | MVC1006  |
| **Fix is breaking or non-breaking** | Breaking |

## Cause

A tag helper was defined inside a Razor function that executes synchronously

### Rule description

Tag Helper execution is asynchronous. When used inside a method or a lambda within a Razor Page, the containing function must also be declared to be async.

Consider the following cshtml file:

```
void Helper(string controller)
{
    <a asp-controller="@controller">Home</a>
}
```

`asp-controller` is a tag helper and will trigger this rule.

## How to fix violations

Declare the function to be async and Task returning:

```
async Task Helper(string controller)
{
    <a asp-controller="@controller">Home</a>
}
```

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/static-server-side-rendering
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/static-server-side-rendering.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Threat mitigation guidance for ASP.NET Core Blazor static server-side rendering

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains the security considerations that developers should take into account when developing Blazor Web Apps with static server-side rendering.

Blazor combines three different models in one for writing interactive web apps. Traditional server-side rendering, which is a request/response model based on HTTP. Interactive server-side rendering, which is a rendering model based on SignalR. Finally, client-side rendering, which is a rendering model based on WebAssembly.

All of the general security considerations defined for the interactive rendering modes apply to Blazor Web Apps when there are interactive components rendering in one of the supported render modes. The following sections explain the security considerations specific to non-interactive server-side rendering in Blazor Web Apps and the specific aspects that apply when render modes interact with each other.

## General considerations for server-side rendering

The server-side rendering (SSR) model is based on the traditional request/response model of HTTP. As such, there are common areas of concern between SSR and request/response HTTP. General security considerations and specific threats must be successfully mitigated. The framework provides built-in mechanisms for managing some of these threats, but other threats are specific to app code and must be handled by the app. These threats can be categorized as follows:

* Authentication and authorization: The app must ensure that the user is authenticated and authorized to access the app and the resources it exposes. The framework provides built-in mechanisms for authentication and authorization, but the app must ensure that the mechanisms are properly configured and used. The built-in mechanisms for authentication and authorization are covered in the [Blazor documentation's *Server* security node](./?view=aspnetcore-10.0) and in the [ASP.NET Core documentation's *Security and Identity* node](../../security/?view=aspnetcore-10.0), so they won't be covered here.

* Input validation and sanitization: All input arriving from a client must be validated and sanitized before use. Otherwise, the app might be exposed to attacks, such as SQL injection, cross-site scripting, cross-site request forgery, open redirection, and other forms of attacks. The input might come from anywhere in the request.

* Session management: Properly managing user sessions is critical to ensure that the app isn't exposed to attacks, such as session fixation, session hijacking, and other attacks. Information stored in the session must be properly protected and encrypted, and the app's code must prevent a malicious user from guessing or manipulating sessions.

* Error handling and logging: The app must ensure that errors are properly handled and logged. Otherwise, the app might be exposed to attacks, such as information disclosure. This can happen when the app returns sensitive information in the response or when the app returns detailed error messages with data that can be used to attack the app.

* Data protection: Sensitive data must be properly protected, which includes app logic when running on WebAssembly, since it can be easily reverse-engineered.

* Denial of service: The app must ensure that it isn't exposed to attacks, such as denial of service. This happens for example, when the app isn't properly protected against brute force attacks or when an action can cause the app to consume too many resources.

## Input validation and sanitization

All input arriving from the client must be considered untrusted unless its information was generated and protected on the server, such as a CSRF token, an authentication cookie, a session identifier, or any other payload that's protected with authenticated encryption.

Input is normally available to the app through a binding process, for example via the [`[SupplyParameterFromQuery]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromqueryattribute) or [`[SupplyParameterFromForm]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.supplyparameterfromformattribute). Before processing this input, the app must make sure that the data is valid. For example, the app must confirm that there were no binding errors when mapping the form data to a component property. Otherwise, the app might process invalid data.

If the input is used to perform a redirect, the app must make sure that the input is valid and that it isn't pointing to a domain considered invalid or to an invalid subpath within the app base path. Otherwise, the app may be exposed to open redirection attacks, where a cyberattacker can craft a link that redirects the user to a malicious site.

If the input is used to perform a database query, app must confirm that the input is valid and that it isn't exposing the app to SQL injection attacks. Otherwise, a cyberattacker might be able to craft a malicious query that can be used to extract information from the database or to modify the database.

Data that might have come from user input also must be sanitized before included in a response. For example, the input might contain HTML or JavaScript that can be used to perform cross-site scripting attacks, which can be used to extract information from the user or to perform actions on behalf of the user.

The framework provides the following mechanisms to help with input validation and sanitization:

* All bound form data is validated for basic correctness. If an input can't be parsed, the binding process reports an error that the app can discover before taking any action with the data. The built-in [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component takes this into account before invoking the [OnValidSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onvalidsubmit#microsoft-aspnetcore-components-forms-editform-onvalidsubmit) form callback. Blazor avoids executing the callback if there are one or more binding errors.
* The framework uses an antiforgery token to protect against cross-site request forgery attacks. For more information, see [ASP.NET Core Blazor authentication and authorization](./?view=aspnetcore-10.0#antiforgery-support) and [ASP.NET Core Blazor forms overview](../forms/?view=aspnetcore-10.0#antiforgery-support).

All input and permissions must be validated on the server at the time of performing a given action to ensure that the data is valid and accurate at that time and that the user is allowed to perform the action. This approach is consistent with the [security guidance provided for interactive server-side rendering](interactive-server-side-rendering?view=aspnetcore-10.0).

## Session management

Session management is handled by the framework. The framework uses a session cookie to identify the user session. The session cookie is protected using the ASP.NET Core Data Protection APIs. The session cookie isn't accessible to JavaScript code running on the browser and it can't be easily guessed or manipulated by a user.

With regard to other session data, such as data stored within services, the session data should be stored within scoped services, as scoped services are unique per a given user session, as opposed to singleton services which are shared across all user sessions in a given process instance.

When it comes to SSR, there's not much difference between scoped and transient services in most cases, as the lifetime of the service is limited to a single request. There's a difference in two scenarios:

* If the service is injected in more than one location or at different times during the request.
* If the service might be used in an interactive server context, where it survives multiple renders and its fundamental that the service is scoped to the user session.

## Error handling and logging

The framework provides built-in logging for the app at the framework level. The framework logs important events, such as when the antiforgery token for a form fails to validate, when a root component starts to render, and when an action is dispatched. The app is responsible for logging any other events that might be important to record.

The framework provides built-in error handling for the app at the framework level. The framework handles errors that happen during the rendering of a component and either uses the error boundary mechanism to display a friendly error message or allows the error to bubble up to the exception handling middleware, which is configured to render the error page.

Errors that occur during streaming rendering after the response has started to be sent to the client are displayed in the final response as a generic error message. Details about the cause of the error are only included during development.

## ASP.NET Core Data Protection

The framework offers mechanisms for protecting sensitive information for a given user session and ensures that the built-in components use these mechanisms to protect sensitive information, such as protecting user identity when using cookie authentication. Outside of scenarios handled by the framework, developer code is responsible for protecting other app-specific information. The most common way of doing this is via the ASP.NET Core Data Protection (DP) APIs or any other form of encryption. As a general rule, the app is responsible for:

* Making sure that a user can't inspect or modify the private information of another user.
* Making sure that a user can't modify user data of another user, such as an internal identifier.

With regard to DP, you must clearly understand where the code is executing. For the static server-side rendering (static SSR) and interactive server-side rendering (interactive SSR), code is stored on the server and never reaches the client. For the Interactive WebAssembly render mode, the app code *always reaches the client*, which means that any sensitive information stored in the app code is available to anyone with access to the app. Obfuscation and other similar technique to "protect" the code isn't effective. Once the code reaches the client, it can be reverse-engineered to extract the sensitive information.

## Denial of service

At the server level, the framework provides limits on request/response parameters, such as the maximum size of the request and the header size. In regard to app code, Blazor's form mapping system defines limits similar to those defined by the MVC model binding system:

* Limit on the maximum number of errors.
* Limit on the maximum recursion depth for the binder.
* Limit on the maximum number of elements bound in a collection.

In addition, there are limits defined for the form, such as the maximum form key size and value size and the maximum number of entries.

In general, the app must evaluate when there's a chance that a request triggers an asymmetric amount of work by the server. Examples of this include when the user sends a request parameterized by N and the server performs an operation in response that is N times as expensive, where N is a parameter that a user controls and can grow indefinitely. Normally, the app must either impose a limit on the maximum N that it's willing to process or ensure that any operation is either less, equal, or more expensive than the request by a constant factor.

This aspect has more to do with the difference in growth between the work the client performs and the work the server performs than with a specific 1→N comparison. For example, a client might submit a work item (inserting elements into a list) that takes N units of time to perform, but the server needs N2 to process (because it might be doing something very naive). It's the difference between N and N2 that matters.

As such, there's a limit on how much work the server must be willing to do, which is specific to the app. This aspect applies to server-side workloads, since the resources are on the server, but doesn't necessarily apply to WebAssembly workloads on the client in most cases.

The other important aspect is that this isn't only reserved to CPU time. It also applies to any resources, such as memory, network, and space on disk.

For WebAssembly workloads, there's usually little concern over the amount of work the client performs, since the client is normally limited by the resources available on the client. However, there are some scenarios where the client might be impacted, if for example, an app displays data from other users and one user is capable of adding data to the system that forces the clients that display the data to perform an amount of work that isn't proportional to the amount of data added by the user.

## Recommended (non-exhaustive) check list

* Ensure that the user is authenticated and authorized to access the app and the resources it exposes.
* Validate and sanitize all input coming from a client before using it.
* Properly manage user sessions to ensure that state isn't mistakenly shared across users.
* Handle and log errors properly to avoid exposing sensitive information.
* Log important events in the app to identify potential issues and audit actions performed by users.
* Protect sensitive information using the ASP.NET Core Data Protection APIs or one of the available components ([Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage](/en-us/dotnet/api/microsoft.aspnetcore.components.server.protectedbrowserstorage), [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate)).
* Ensure that the app understands the resources that can be consumed by a given request and has limits in place to avoid denial of service attacks.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0033
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0033.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0033: `[ValidatableType]` is applied to an inaccessible type

|              | Value   |
| ------------ | ------- |
| **Rule ID**  | ASP0033 |
| **Category** | Usage   |
| **Severity** | Warning |

## Cause

The [ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute) attribute is applied to a type that the validation source generator can't access. The type, or one of its containing types, is private or file-local.

## Rule description

The validation source generator emits code in a separate file. A validatable type and all of its containing types must be public or internal and can't be file-local. Otherwise, the generator silently skips validation for the type.

The following code produces this diagnostic:

```
[ValidatableType]
file sealed class Asp0033FileLocalValidatableType
{
    [Required]
    public string Name { get; set; } = string.Empty;
}
```

## How to fix violations

Make the attributed type and each of its containing types public or internal. For example, change `Asp0033FileLocalValidatableType` to an internal type:

```
[ValidatableType]
internal sealed class Asp0033FileLocalValidatableType
{
    [Required]
    public string Name { get; set; } = string.Empty;
}
```

For more information, see [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-12.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-on-webworkers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/dotnet-on-webworkers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# .NET on Web Workers

Modern web apps often require intensive computational tasks that can block the main UI thread, leading to poor user experience. [Web Workers](https://developer.mozilla.org/docs/Web/API/Web_Workers_API) provide a solution to this problem by enabling JavaScript (JS) to run on separate threads. With .NET WebAssembly (Wasm), you can run C# code in Web Workers, combining the performance benefits of compiled code with the non-blocking execution model of background threads.

This approach is particularly valuable when you need to perform complex calculations, data processing, or business logic without requiring direct DOM manipulation. Instead of rewriting algorithms in JS, you can maintain your existing .NET codebase and execute it efficiently in the background while your React.js frontend remains responsive.

Tip

In .NET 11 or later, `dotnet new blazorwebworker` generates the worker scripts and starter `[JSExport]` code used by the Blazor integration. For the Blazor-specific walkthrough, see [ASP.NET Core Blazor with .NET on Web Workers](../blazor/blazor-with-dotnet-on-web-workers?view=aspnetcore-10.0).

Tip

For earlier versions, follow the manual `wasmbrowser` steps in this article. For the Blazor-specific walkthrough, see [ASP.NET Core Blazor with .NET on Web Workers](../blazor/blazor-with-dotnet-on-web-workers?view=aspnetcore-10.0).

This article demonstrates the React approach using a standalone .NET WebAssembly project.

## Sample app

Explore a complete working implementation in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples). The sample is available for .NET 10 or later and named `DotNetOnWebWorkersReact`.

## Prerequisites and setup

Before diving into the implementation, ensure the necessary tools are installed.

The [.NET 11 SDK or later](https://dotnet.microsoft.com/download) is required for the `blazorwebworker` template approach.

The [.NET SDK 8.0 or later](https://dotnet.microsoft.com/download) is required. If the WebAssembly build tools aren't already installed, run:

```
dotnet workload install wasm-tools
dotnet workload install wasm-experimental
```

For the React.js frontend, [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com) must be installed.

Create a new React app with Vite:

```
npm create vite@latest react-app
cd react-app
npm install
```

When prompted, select `React` and `JavaScript`.

Create a new React app:

```
npx create-react-app react-app
cd react-app
```

## Create the .NET WebAssembly project

In .NET 11 or later, you can start from the Blazor Web Worker template and adapt it for a React host:

```
dotnet new blazorwebworker -o WebWorkersOnReact
cd WebWorkersOnReact
dotnet add package QRCoder
```

Update `WebWorkersOnReact.csproj` to use the WebAssembly SDK and build as a library. The template uses the Razor SDK by default because, in the Blazor scenario, the host app already provides the .NET WebAssembly runtime assets. In a React app, there isn't a Blazor host to provide that runtime bundle, so the worker project must produce its own `_framework` output.

Setting `<OutputType>Library</OutputType>` enables WebAssembly library mode. In this mode, the project produces the runtime files needed by the worker scripts but doesn't require a standalone app entry point:

```
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
  <PropertyGroup>
    <TargetFramework>net11.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <OutputType>Library</OutputType>
  </PropertyGroup>
</Project>
```

Delete `WebWorkerClient.cs` because it's specific to the Blazor integration.

Update `WorkerMethods.cs` with the methods that the React app should call:

```
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using QRCoder;

namespace WebWorkersOnReact;

[SupportedOSPlatform("browser")]
public static partial class WorkerMethods
{
    private static readonly int MaxQrSize = 20;

    [JSExport]
    public static byte[] Generate(string text, int qrSize)
    {
        if (qrSize >= MaxQrSize)
        {
            throw new ArgumentOutOfRangeException(
                nameof(qrSize),
                $"QR code size must be less than {MaxQrSize}.");
        }

        using var qrGenerator = new QRCodeGenerator();
        using var qrCodeData = qrGenerator.CreateQrCode(text, QRCodeGenerator.ECCLevel.Q);
        var qrCode = new BitmapByteQRCode(qrCodeData);
        return qrCode.GetGraphic(qrSize);
    }
}
```

Create a new WebAssembly browser project to serve as the Web Worker:

```
dotnet new wasmbrowser -o WebWorkersOnReact
cd WebWorkersOnReact
dotnet add package QRCoder
```

Modify the `Program.cs` file to set up the Web Worker entry point and message handling:

```
using System;
using System.Runtime.InteropServices.JavaScript;
using QRCoder;
using System.Linq;

public partial class QRGenerator
{
    private static readonly int MAX_QR_SIZE = 20;

    [JSExport]
    internal static byte[] Generate(string text, int qrSize)
    {
        if (qrSize >= MAX_QR_SIZE)
        {
            throw new Exception(
                $"QR code size must be less than {MAX_QR_SIZE}. Try again.");
        }
        QRCodeGenerator qrGenerator = new QRCodeGenerator();
        QRCodeData qrCodeData = qrGenerator.CreateQrCode(
            text, QRCodeGenerator.ECCLevel.Q);
        BitmapByteQRCode qrCode = new BitmapByteQRCode(qrCodeData);
        return qrCode.GetGraphic(qrSize);
    }
}
```

Add a `wwwroot/worker.js` file with code that interops between C# and JS:

```
import { dotnet } from './_framework/dotnet.js'

let assemblyExports = null;
let startupError = undefined;

try {
  const { getAssemblyExports, getConfig } = await dotnet.create();
  const config = getConfig();
  assemblyExports = await getAssemblyExports(config.mainAssemblyName);
}
catch (err) {
  startupError = err.message;
}

self.addEventListener('message', async function(e) {
  try {
    if (!assemblyExports) {
      throw new Error(startupError || "worker exports not loaded");
    }
    let result = null;
    switch (e.data.command) {
      case "generateQR":
        const size = Number(e.data.size);
        const text = e.data.text;
        if (size === undefined || text === undefined)
          new Error("Inner error, got empty QR generation data from React");
        result = assemblyExports.QRGenerator.Generate(text, size);
        break;
      default:
          throw new Error("Unknown command: " + e.data.command);
    }
    self.postMessage({
      command: "response",
      requestId: e.data.requestId,
      result,
    });
  }
  catch (err) {
    self.postMessage({
      command: "response",
      requestId: e.data.requestId,
      error: err.message,
    });
  }
}, false);
```

Build the worker project:

```
dotnet build
```

## Set up the React app

For a quick test, copy the worker output into the React app's static files manually. For a real app, automate these copies with an npm script or another build step.

Use the generated JavaScript client to host the .NET WebAssembly runtime in a Web Worker. For example:

* Copy `WebWorkersOnReact/bin/Debug/net11.0/wwwroot/_framework` to `react-app/public/_framework`.
* Copy `WebWorkersOnReact/wwwroot/dotnet-web-worker.js` to `react-app/public/_content/WebWorkersOnReact/dotnet-web-worker.js`.
* Copy `WebWorkersOnReact/wwwroot/dotnet-web-worker-client.js` to `react-app/public/_content/WebWorkersOnReact/dotnet-web-worker-client.js`.

Then create a client helper, such as `src/client.js`, to load the generated client and call the worker:

```
let worker;

async function getWorker() {
  if (!worker) {
    const { create } = await import(
      /* @vite-ignore */ '/_content/WebWorkersOnReact/dotnet-web-worker-client.js');
    worker = await create(60000, { assemblyName: 'WebWorkersOnReact' });
  }

  return worker;
}

export async function generateQR(text, size) {
  const worker = await getWorker();
  const response = await worker.invoke(
    'WebWorkersOnReact.WorkerMethods.Generate',
    [text, size],
    60000);
  const blob = new Blob([response], { type: 'image/png' });
  return URL.createObjectURL(blob);
}
```

Replace the starter content in `src/App.jsx` with a button that calls `generateQR`:

```
import { useState } from 'react';
import './App.css';
import { generateQR } from './client';

function App() {
  const [qrUrl, setQrUrl] = useState('');
  const [status, setStatus] = useState('Ready');

  async function handleGenerate() {
    try {
      setStatus('Generating QR code...');
      const url = await generateQR('Hello from docs', 10);
      setQrUrl(url);
      setStatus('Done');
    } catch (error) {
      setStatus(error.message);
    }
  }

  return (
    <main>
      <h1>.NET on Web Workers</h1>
      <p>{status}</p>
      <button onClick={handleGenerate}>Generate QR</button>
      {qrUrl ? <img src={qrUrl} alt="Generated QR code" /> : null}
    </main>
  );
}

export default App;
```

Create a Web Worker to host the .NET WebAssembly runtime. The sample app copies the entire output folder into `public/qr`, which preserves both `wwwroot/worker.js` and the `_framework` assets required by the runtime. See the [sample app](#sample-app) for reference.

Create a Web Worker file `client.js` to receive messages from dotnet:

```
const dotnetWorker = new Worker('../../qr/wwwroot/worker.js', { type: "module" } );

dotnetWorker.addEventListener('message', async function (e) {
  switch (e.data.command) {
    case "response":
      if (!e.data.requestId) {
        console.error("No requestId in response from worker");
      }
      const request = pendingRequests[e.data.requestId];
      delete pendingRequests[e.data.requestId];
      if (e.data.error) {
        request.reject(new Error(e.data.error));
      }
      request.resolve(e.data.result);
      break;
    default:
      console.log('Worker said: ', e.data);
      break;
  }
}, false);
```

Connect this functionality with UI and add a button that triggers `generateQR`:

```
export async function generateQR(text, size) {
  const response = await sendRequestToWorker({
    command: "generateQR",
    text: text,
    size: size
  });
  const blob = new Blob([response], { type: 'image/png' });
  return URL.createObjectURL(blob);
}

function sendRequestToWorker(request) {
  pendingRequestId++;
  const promise = new Promise((resolve, reject) => {
    pendingRequests[pendingRequestId] = { resolve, reject };
  });
  dotnetWorker.postMessage({
    ...request,
    requestId: pendingRequestId
  });
  return promise;
}
```

Run the app:

```
npm run dev
```

Open the local URL shown by the development server and select **Generate QR**. If everything is set up correctly, the page displays the generated image.

## Performance considerations and optimization

When working with .NET on Web Workers, consider these key optimization strategies:

* **Minimize data transfer**: Serialize only essential data between the main thread and worker to reduce communication overhead.
* **Batch operations**: Group multiple calculations together rather than sending individual requests.
* **Memory management**: Be mindful of memory usage in the WebAssembly environment, especially for long-running workers.
* **Startup cost**: WebAssembly initialization has overhead, so prefer persistent workers over frequent creation/destruction.

See the [sample app](#sample-app) for a demonstration of the preceding concepts.

## Related content

[ASP.NET Core Blazor with .NET on Web Workers](../blazor/blazor-with-dotnet-on-web-workers?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/testing
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/testing.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Unit test controller logic in ASP.NET Core

By [Steve Smith](https://ardalis.com/)

[Unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test) involve testing a part of an app in isolation from its infrastructure and dependencies. When unit testing controller logic, only the contents of a single action are tested, not the behavior of its dependencies or of the framework itself.

## Unit testing controllers

Set up unit tests of controller actions to focus on the controller's behavior. A controller unit test avoids scenarios such as [filters](filters?view=aspnetcore-10.0), [routing](../../fundamentals/routing?view=aspnetcore-10.0), and [model binding](../models/model-binding?view=aspnetcore-10.0). Tests that cover the interactions among components that collectively respond to a request are handled by *integration tests*. For more information on integration tests, see [Integration tests in ASP.NET Core](../../test/integration-tests?view=aspnetcore-10.0).

If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.

To demonstrate controller unit tests, review the following controller in the sample app.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/testing/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The Home controller displays a list of brainstorming sessions and allows the creation of new brainstorming sessions with a POST request:

```
public class HomeController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public HomeController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index()
    {
        var sessionList = await _sessionRepository.ListAsync();

        var model = sessionList.Select(session => new StormSessionViewModel()
        {
            Id = session.Id,
            DateCreated = session.DateCreated,
            Name = session.Name,
            IdeaCount = session.Ideas.Count
        });

        return View(model);
    }

    public class NewSessionModel
    {
        [Required]
        public string SessionName { get; set; }
    }

    [HttpPost]
    public async Task<IActionResult> Index(NewSessionModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        else
        {
            await _sessionRepository.AddAsync(new BrainstormSession()
            {
                DateCreated = DateTimeOffset.Now,
                Name = model.SessionName
            });
        }

        return RedirectToAction(actionName: nameof(Index));
    }
}
```

The preceding controller:

* Follows the [Explicit Dependencies Principle](/en-us/dotnet/architecture/modern-web-apps-azure/architectural-principles#explicit-dependencies).
* Expects [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to provide an instance of `IBrainstormSessionRepository`.
* Can be tested with a mocked `IBrainstormSessionRepository` service using a mock object framework, such as [Moq](https://www.nuget.org/packages/Moq/). A *mocked object* is a fabricated object with a predetermined set of property and method behaviors used for testing. For more information, see [Introduction to integration tests](../../test/integration-tests?view=aspnetcore-10.0#introduction-to-integration-tests).

The `HTTP GET Index` method has no looping or branching and only calls one method. The unit test for this action:

* Mocks the `IBrainstormSessionRepository` service using the `GetTestSessions` method. `GetTestSessions` creates two mock brainstorm sessions with dates and session names.

* Executes the `Index` method.

* Makes assertions on the result returned by the method:

  * A [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult) is returned.
  * The [ViewDataDictionary.Model](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.viewdatadictionary.model) is a `StormSessionViewModel`.
  * There are two brainstorming sessions stored in the `ViewDataDictionary.Model`.

```
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

    // Act
    var result = await controller.Index();

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
        viewResult.ViewData.Model);
    Assert.Equal(2, model.Count());
}
```

```
private List<BrainstormSession> GetTestSessions()
{
    var sessions = new List<BrainstormSession>();
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 2),
        Id = 1,
        Name = "Test One"
    });
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 1),
        Id = 2,
        Name = "Test Two"
    });
    return sessions;
}
```

The Home controller's `HTTP POST Index` method tests verifies that:

* When [ModelState.IsValid](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.isvalid) is `false`, the action method returns a *400 Bad Request* [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult) with the appropriate data.

* When `ModelState.IsValid` is `true`:

  * The `Add` method on the repository is called.
  * A [RedirectToActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.redirecttoactionresult) is returned with the correct arguments.

An invalid model state is tested by adding errors using [AddModelError](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.addmodelerror) as shown in the first test below:

```
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);
    controller.ModelState.AddModelError("SessionName", "Required");
    var newSession = new HomeController.NewSessionModel();

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
    Assert.IsType<SerializableError>(badRequestResult.Value);
}

[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
        .Returns(Task.CompletedTask)
        .Verifiable();
    var controller = new HomeController(mockRepo.Object);
    var newSession = new HomeController.NewSessionModel()
    {
        SessionName = "Test Name"
    };

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
    Assert.Null(redirectToActionResult.ControllerName);
    Assert.Equal("Index", redirectToActionResult.ActionName);
    mockRepo.Verify();
}
```

When [ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary) isn't valid, the same `ViewResult` is returned as for a GET request. The test doesn't attempt to pass in an invalid model. Passing an invalid model isn't a valid approach, since model binding isn't running (although an [integration test](../../test/integration-tests?view=aspnetcore-10.0) does use model binding). In this case, model binding isn't tested. These unit tests are only testing the code in the action method.

The second test verifies that when the `ModelState` is valid:

* A new `BrainstormSession` is added (via the repository).
* The method returns a `RedirectToActionResult` with the expected properties.

Mocked calls that aren't called are normally ignored, but calling `Verifiable` at the end of the setup call allows mock validation in the test. This is performed with the call to `mockRepo.Verify`, which fails the test if the expected method wasn't called.

Note

The Moq library used in this sample makes it possible to mix verifiable, or "strict", mocks with non-verifiable mocks (also called "loose" mocks or stubs). Learn more about [customizing Mock behavior with Moq](https://github.com/Moq/moq4/wiki/Quickstart#customizing-mock-behavior).

[SessionController](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/testing/samples/3.x/TestingControllersSample/src/TestingControllersSample/Controllers/SessionController.cs) in the sample app displays information related to a particular brainstorming session. The controller includes logic to deal with invalid `id` values (there are two `return` scenarios in the following example to cover these scenarios). The final `return` statement returns a new `StormSessionViewModel` to the view (`Controllers/SessionController.cs`):

```
public class SessionController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public SessionController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index(int? id)
    {
        if (!id.HasValue)
        {
            return RedirectToAction(actionName: nameof(Index), 
                controllerName: "Home");
        }

        var session = await _sessionRepository.GetByIdAsync(id.Value);
        if (session == null)
        {
            return Content("Session not found.");
        }

        var viewModel = new StormSessionViewModel()
        {
            DateCreated = session.DateCreated,
            Name = session.Name,
            Id = session.Id
        };

        return View(viewModel);
    }
}
```

The unit tests include one test for each `return` scenario in the Session controller `Index` action:

```
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
    // Arrange
    var controller = new SessionController(sessionRepository: null);

    // Act
    var result = await controller.Index(id: null);

    // Assert
    var redirectToActionResult = 
        Assert.IsType<RedirectToActionResult>(result);
    Assert.Equal("Home", redirectToActionResult.ControllerName);
    Assert.Equal("Index", redirectToActionResult.ActionName);
}

[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
    // Arrange
    int testSessionId = 1;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new SessionController(mockRepo.Object);

    // Act
    var result = await controller.Index(testSessionId);

    // Assert
    var contentResult = Assert.IsType<ContentResult>(result);
    Assert.Equal("Session not found.", contentResult.Content);
}

[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
    // Arrange
    int testSessionId = 1;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSessions().FirstOrDefault(
            s => s.Id == testSessionId));
    var controller = new SessionController(mockRepo.Object);

    // Act
    var result = await controller.Index(testSessionId);

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsType<StormSessionViewModel>(
        viewResult.ViewData.Model);
    Assert.Equal("Test One", model.Name);
    Assert.Equal(2, model.DateCreated.Day);
    Assert.Equal(testSessionId, model.Id);
}
```

Moving to the Ideas controller, the app exposes functionality as a web API on the `api/ideas` route:

* A list of ideas (`IdeaDTO`) associated with a brainstorming session is returned by the `ForSession` method.
* The `Create` method adds new ideas to a session.

```
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);
    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return Ok(result);
}

[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var session = await _sessionRepository.GetByIdAsync(model.SessionId);
    if (session == null)
    {
        return NotFound(model.SessionId);
    }

    var idea = new Idea()
    {
        DateCreated = DateTimeOffset.Now,
        Description = model.Description,
        Name = model.Name
    };
    session.AddIdea(idea);

    await _sessionRepository.UpdateAsync(session);

    return Ok(session);
}
```

Avoid returning business domain entities directly via API calls. Domain entities:

* Often include more data than the client requires.
* Unnecessarily couple the app's internal domain model with the publicly exposed API.

Mapping between domain entities and the types returned to the client can be performed:

* Manually with a LINQ `Select`, as the sample app uses. For more information, see [LINQ (Language Integrated Query)](/en-us/dotnet/standard/using-linq).
* Automatically with a library, such as [AutoMapper](https://github.com/AutoMapper/AutoMapper).

Next, the sample app demonstrates unit tests for the `Create` and `ForSession` API methods of the Ideas controller.

The sample app contains two `ForSession` tests. The first test determines if `ForSession` returns a [NotFoundObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.notfoundobjectresult) (HTTP Not Found) for an invalid session:

```
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSession(testSessionId);

    // Assert
    var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
    Assert.Equal(testSessionId, notFoundObjectResult.Value);
}
```

The second `ForSession` test determines if `ForSession` returns a list of session ideas (`<List<IdeaDTO>>`) for a valid session. The checks also examine the first idea to confirm its `Name` property is correct:

```
[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSession(testSessionId);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}
```

To test the behavior of the `Create` method when the `ModelState` is invalid, the sample app adds a model error to the controller as part of the test. Don't try to test model validation or model binding in unit tests—just test the action method's behavior when confronted with an invalid `ModelState`:

```
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.Create(model: null);

    // Assert
    Assert.IsType<BadRequestObjectResult>(result);
}
```

The second test of `Create` depends on the repository returning `null`, so the mock repository is configured to return `null`. There's no need to create a test database (in memory or otherwise) and construct a query that returns this result. The test can be accomplished in a single statement, as the sample code illustrates:

```
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.Create(new NewIdeaModel());

    // Assert
    Assert.IsType<NotFoundObjectResult>(result);
}
```

The third `Create` test, `Create_ReturnsNewlyCreatedIdeaForSession`, verifies that the repository's `UpdateAsync` method is called. The mock is called with `Verifiable`, and the mocked repository's `Verify` method is called to confirm the verifiable method is executed. It's not the unit test's responsibility to ensure that the `UpdateAsync` method saved the data—that can be performed with an integration test.

```
[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.Create(newIdea);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnSession.Ideas.Count());
    Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
```

## Test ActionResult\<T>

[ActionResult\<T>](../../web-api/action-return-types?view=aspnetcore-10.0#actionresultt-type) ([ActionResult\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1)) can return a type deriving from `ActionResult` or return a specific type.

The sample app includes a method that returns a `List<IdeaDTO>` for a given session `id`. If the session `id` doesn't exist, the controller returns [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound):

```
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);

    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return result;
}
```

Two tests of the `ForSessionActionResult` controller are included in the `ApiIdeasControllerTests`.

The first test confirms that the controller returns an `ActionResult` but not a nonexistent list of ideas for a nonexistent session `id`:

* The `ActionResult` type is `ActionResult<List<IdeaDTO>>`.
* The [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1.result) is a [NotFoundObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.notfoundobjectresult).

```
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    var nonExistentSessionId = 999;

    // Act
    var result = await controller.ForSessionActionResult(nonExistentSessionId);

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
```

For a valid session `id`, the second test confirms that the method returns:

* An `ActionResult` with a `List<IdeaDTO>` type.
* The [ActionResult\<T>.Value](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1.value) is a `List<IdeaDTO>` type.
* The first item in the list is a valid idea matching the idea stored in the mock session (obtained by calling `GetTestSession`).

```
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSessionActionResult(testSessionId);

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}
```

The sample app also includes a method to create a new `Idea` for a given session. The controller returns:

* [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest) for an invalid model.
* [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) if the session doesn't exist.
* [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) when the session is updated with the new idea.

```
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var session = await _sessionRepository.GetByIdAsync(model.SessionId);

    if (session == null)
    {
        return NotFound(model.SessionId);
    }

    var idea = new Idea()
    {
        DateCreated = DateTimeOffset.Now,
        Description = model.Description,
        Name = model.Name
    };
    session.AddIdea(idea);

    await _sessionRepository.UpdateAsync(session);

    return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);
}
```

Three tests of `CreateActionResult` are included in the `ApiIdeasControllerTests`.

The first text confirms that a [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest) is returned for an invalid model.

```
[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.CreateActionResult(model: null);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}
```

The second test checks that a [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) is returned if the session doesn't exist.

```
[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var nonExistentSessionId = 999;
    string testName = "test name";
    string testDescription = "test description";
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = nonExistentSessionId
    };

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
```

For a valid session `id`, the final test confirms that:

* The method returns an `ActionResult` with a `BrainstormSession` type.
* The [ActionResult\<T>.Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1.result) is a [CreatedAtActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.createdatactionresult). `CreatedAtActionResult` is analogous to a *201 Created* response with a `Location` header.
* The [ActionResult\<T>.Value](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1.value) is a `BrainstormSession` type.
* The mock call to update the session, `UpdateAsync(testSession)`, was invoked. The `Verifiable` method call is checked by executing `mockRepo.Verify()` in the assertions.
* Two `Idea` objects are returned for the session.
* The last item (the `Idea` added by the mock call to `UpdateAsync`) matches the `newIdea` added to the session in the test.

```
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
    var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnValue.Ideas.Count());
    Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
```

[Controllers](actions?view=aspnetcore-10.0) play a central role in any ASP.NET Core MVC app. As such, you should have confidence that controllers behave as intended. Automated tests can detect errors before the app is deployed to a production environment.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/testing/samples/) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Unit tests of controller logic

[Unit tests](/en-us/dotnet/articles/core/testing/unit-testing-with-dotnet-test) involve testing a part of an app in isolation from its infrastructure and dependencies. When unit testing controller logic, only the contents of a single action are tested, not the behavior of its dependencies or of the framework itself.

Set up unit tests of controller actions to focus on the controller's behavior. A controller unit test avoids scenarios such as [filters](filters?view=aspnetcore-10.0), [routing](../../fundamentals/routing?view=aspnetcore-10.0), and [model binding](../models/model-binding?view=aspnetcore-10.0). Tests that cover the interactions among components that collectively respond to a request are handled by *integration tests*. For more information on integration tests, see [Integration tests in ASP.NET Core](../../test/integration-tests?view=aspnetcore-10.0).

If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.

To demonstrate controller unit tests, review the following controller in the sample app. The Home controller displays a list of brainstorming sessions and allows the creation of new brainstorming sessions with a POST request:

```
public class HomeController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public HomeController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index()
    {
        var sessionList = await _sessionRepository.ListAsync();

        var model = sessionList.Select(session => new StormSessionViewModel()
        {
            Id = session.Id,
            DateCreated = session.DateCreated,
            Name = session.Name,
            IdeaCount = session.Ideas.Count
        });

        return View(model);
    }

    public class NewSessionModel
    {
        [Required]
        public string SessionName { get; set; }
    }

    [HttpPost]
    public async Task<IActionResult> Index(NewSessionModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        else
        {
            await _sessionRepository.AddAsync(new BrainstormSession()
            {
                DateCreated = DateTimeOffset.Now,
                Name = model.SessionName
            });
        }

        return RedirectToAction(actionName: nameof(Index));
    }
}
```

The preceding controller:

* Follows the [Explicit Dependencies Principle](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#explicit-dependencies).
* Expects [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to provide an instance of `IBrainstormSessionRepository`.
* Can be tested with a mocked `IBrainstormSessionRepository` service using a mock object framework, such as [Moq](https://www.nuget.org/packages/Moq/). A *mocked object* is a fabricated object with a predetermined set of property and method behaviors used for testing. For more information, see [Introduction to integration tests](../../test/integration-tests?view=aspnetcore-10.0#introduction-to-integration-tests).

The `HTTP GET Index` method has no looping or branching and only calls one method. The unit test for this action:

* Mocks the `IBrainstormSessionRepository` service using the `GetTestSessions` method. `GetTestSessions` creates two mock brainstorm sessions with dates and session names.

* Executes the `Index` method.

* Makes assertions on the result returned by the method:

  * A [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult) is returned.
  * The [ViewDataDictionary.Model](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.viewdatadictionary.model) is a `StormSessionViewModel`.
  * There are two brainstorming sessions stored in the `ViewDataDictionary.Model`.

```
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

    // Act
    var result = await controller.Index();

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
        viewResult.ViewData.Model);
    Assert.Equal(2, model.Count());
}
```

```
private List<BrainstormSession> GetTestSessions()
{
    var sessions = new List<BrainstormSession>();
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 2),
        Id = 1,
        Name = "Test One"
    });
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 1),
        Id = 2,
        Name = "Test Two"
    });
    return sessions;
}
```

The Home controller's `HTTP POST Index` method tests verifies that:

* When [ModelState.IsValid](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.isvalid) is `false`, the action method returns a *400 Bad Request* [ViewResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewresult) with the appropriate data.

* When `ModelState.IsValid` is `true`:

  * The `Add` method on the repository is called.
  * A [RedirectToActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.redirecttoactionresult) is returned with the correct arguments.

An invalid model state is tested by adding errors using [AddModelError](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.addmodelerror) as shown in the first test below:

```
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);
    controller.ModelState.AddModelError("SessionName", "Required");
    var newSession = new HomeController.NewSessionModel();

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
    Assert.IsType<SerializableError>(badRequestResult.Value);
}

[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
        .Returns(Task.CompletedTask)
        .Verifiable();
    var controller = new HomeController(mockRepo.Object);
    var newSession = new HomeController.NewSessionModel()
    {
        SessionName = "Test Name"
    };

    // Act
    var result = await controller.Index(newSession);

    // Assert
    var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
    Assert.Null(redirectToActionResult.ControllerName);
    Assert.Equal("Index", redirectToActionResult.ActionName);
    mockRepo.Verify();
}
```

When [ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary) isn't valid, the same `ViewResult` is returned as for a GET request. The test doesn't attempt to pass in an invalid model. Passing an invalid model isn't a valid approach, since model binding isn't running (although an [integration test](../../test/integration-tests?view=aspnetcore-10.0) does use model binding). In this case, model binding isn't tested. These unit tests are only testing the code in the action method.

The second test verifies that when the `ModelState` is valid:

* A new `BrainstormSession` is added (via the repository).
* The method returns a `RedirectToActionResult` with the expected properties.

Mocked calls that aren't called are normally ignored, but calling `Verifiable` at the end of the setup call allows mock validation in the test. This is performed with the call to `mockRepo.Verify`, which fails the test if the expected method wasn't called.

Note

The Moq library used in this sample makes it possible to mix verifiable, or "strict", mocks with non-verifiable mocks (also called "loose" mocks or stubs). Learn more about [customizing Mock behavior with Moq](https://github.com/Moq/moq4/wiki/Quickstart#customizing-mock-behavior).

[SessionController](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/testing/samples/2.x/TestingControllersSample/src/TestingControllersSample/Controllers/SessionController.cs) in the sample app displays information related to a particular brainstorming session. The controller includes logic to deal with invalid `id` values (there are two `return` scenarios in the following example to cover these scenarios). The final `return` statement returns a new `StormSessionViewModel` to the view (`Controllers/SessionController.cs`):

```
public class SessionController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public SessionController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index(int? id)
    {
        if (!id.HasValue)
        {
            return RedirectToAction(actionName: nameof(Index), 
                controllerName: "Home");
        }

        var session = await _sessionRepository.GetByIdAsync(id.Value);
        if (session == null)
        {
            return Content("Session not found.");
        }

        var viewModel = new StormSessionViewModel()
        {
            DateCreated = session.DateCreated,
            Name = session.Name,
            Id = session.Id
        };

        return View(viewModel);
    }
}
```

The unit tests include one test for each `return` scenario in the Session controller `Index` action:

```
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
    // Arrange
    var controller = new SessionController(sessionRepository: null);

    // Act
    var result = await controller.Index(id: null);

    // Assert
    var redirectToActionResult = 
        Assert.IsType<RedirectToActionResult>(result);
    Assert.Equal("Home", redirectToActionResult.ControllerName);
    Assert.Equal("Index", redirectToActionResult.ActionName);
}

[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
    // Arrange
    int testSessionId = 1;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new SessionController(mockRepo.Object);

    // Act
    var result = await controller.Index(testSessionId);

    // Assert
    var contentResult = Assert.IsType<ContentResult>(result);
    Assert.Equal("Session not found.", contentResult.Content);
}

[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
    // Arrange
    int testSessionId = 1;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSessions().FirstOrDefault(
            s => s.Id == testSessionId));
    var controller = new SessionController(mockRepo.Object);

    // Act
    var result = await controller.Index(testSessionId);

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsType<StormSessionViewModel>(
        viewResult.ViewData.Model);
    Assert.Equal("Test One", model.Name);
    Assert.Equal(2, model.DateCreated.Day);
    Assert.Equal(testSessionId, model.Id);
}
```

Moving to the Ideas controller, the app exposes functionality as a web API on the `api/ideas` route:

* A list of ideas (`IdeaDTO`) associated with a brainstorming session is returned by the `ForSession` method.
* The `Create` method adds new ideas to a session.

```
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);
    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return Ok(result);
}

[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var session = await _sessionRepository.GetByIdAsync(model.SessionId);
    if (session == null)
    {
        return NotFound(model.SessionId);
    }

    var idea = new Idea()
    {
        DateCreated = DateTimeOffset.Now,
        Description = model.Description,
        Name = model.Name
    };
    session.AddIdea(idea);

    await _sessionRepository.UpdateAsync(session);

    return Ok(session);
}
```

Avoid returning business domain entities directly via API calls. Domain entities:

* Often include more data than the client requires.
* Unnecessarily couple the app's internal domain model with the publicly exposed API.

Mapping between domain entities and the types returned to the client can be performed:

* Manually with a LINQ `Select`, as the sample app uses. For more information, see [LINQ (Language Integrated Query)](/en-us/dotnet/standard/using-linq).
* Automatically with a library, such as [AutoMapper](https://github.com/AutoMapper/AutoMapper).

Next, the sample app demonstrates unit tests for the `Create` and `ForSession` API methods of the Ideas controller.

The sample app contains two `ForSession` tests. The first test determines if `ForSession` returns a [NotFoundObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.notfoundobjectresult) (HTTP Not Found) for an invalid session:

```
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSession(testSessionId);

    // Assert
    var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
    Assert.Equal(testSessionId, notFoundObjectResult.Value);
}
```

The second `ForSession` test determines if `ForSession` returns a list of session ideas (`<List<IdeaDTO>>`) for a valid session. The checks also examine the first idea to confirm its `Name` property is correct:

```
[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSession(testSessionId);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}
```

To test the behavior of the `Create` method when the `ModelState` is invalid, the sample app adds a model error to the controller as part of the test. Don't try to test model validation or model binding in unit tests—just test the action method's behavior when confronted with an invalid `ModelState`:

```
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.Create(model: null);

    // Assert
    Assert.IsType<BadRequestObjectResult>(result);
}
```

The second test of `Create` depends on the repository returning `null`, so the mock repository is configured to return `null`. There's no need to create a test database (in memory or otherwise) and construct a query that returns this result. The test can be accomplished in a single statement, as the sample code illustrates:

```
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.Create(new NewIdeaModel());

    // Assert
    Assert.IsType<NotFoundObjectResult>(result);
}
```

The third `Create` test, `Create_ReturnsNewlyCreatedIdeaForSession`, verifies that the repository's `UpdateAsync` method is called. The mock is called with `Verifiable`, and the mocked repository's `Verify` method is called to confirm the verifiable method is executed. It's not the unit test's responsibility to ensure that the `UpdateAsync` method saved the data—that can be performed with an integration test.

```
[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.Create(newIdea);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnSession.Ideas.Count());
    Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
```

## Test ActionResult\<T>

In ASP.NET Core 2.1 or later, [ActionResult\<T>](../../web-api/action-return-types?view=aspnetcore-10.0#actionresultt-type) ([ActionResult\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1)) enables you to return a type deriving from `ActionResult` or return a specific type.

The sample app includes a method that returns a `List<IdeaDTO>` for a given session `id`. If the session `id` doesn't exist, the controller returns [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound):

```
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);

    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return result;
}
```

Two tests of the `ForSessionActionResult` controller are included in the `ApiIdeasControllerTests`.

The first test confirms that the controller returns an `ActionResult` but not a nonexistent list of ideas for a nonexistent session `id`:

* The `ActionResult` type is `ActionResult<List<IdeaDTO>>`.
* The [Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1.result) is a [NotFoundObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.notfoundobjectresult).

```
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    var nonExistentSessionId = 999;

    // Act
    var result = await controller.ForSessionActionResult(nonExistentSessionId);

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
```

For a valid session `id`, the second test confirms that the method returns:

* An `ActionResult` with a `List<IdeaDTO>` type.
* The [ActionResult\<T>.Value](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1.value) is a `List<IdeaDTO>` type.
* The first item in the list is a valid idea matching the idea stored in the mock session (obtained by calling `GetTestSession`).

```
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(GetTestSession());
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.ForSessionActionResult(testSessionId);

    // Assert
    var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
    var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
    var idea = returnValue.FirstOrDefault();
    Assert.Equal("One", idea.Name);
}
```

The sample app also includes a method to create a new `Idea` for a given session. The controller returns:

* [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest) for an invalid model.
* [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) if the session doesn't exist.
* [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) when the session is updated with the new idea.

```
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var session = await _sessionRepository.GetByIdAsync(model.SessionId);

    if (session == null)
    {
        return NotFound(model.SessionId);
    }

    var idea = new Idea()
    {
        DateCreated = DateTimeOffset.Now,
        Description = model.Description,
        Name = model.Name
    };
    session.AddIdea(idea);

    await _sessionRepository.UpdateAsync(session);

    return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);
}
```

Three tests of `CreateActionResult` are included in the `ApiIdeasControllerTests`.

The first text confirms that a [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest) is returned for an invalid model.

```
[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.CreateActionResult(model: null);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}
```

The second test checks that a [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound) is returned if the session doesn't exist.

```
[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
    // Arrange
    var nonExistentSessionId = 999;
    string testName = "test name";
    string testDescription = "test description";
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = nonExistentSessionId
    };

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
```

For a valid session `id`, the final test confirms that:

* The method returns an `ActionResult` with a `BrainstormSession` type.
* The [ActionResult\<T>.Result](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1.result) is a [CreatedAtActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.createdatactionresult). `CreatedAtActionResult` is analogous to a *201 Created* response with a `Location` header.
* The [ActionResult\<T>.Value](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1.value) is a `BrainstormSession` type.
* The mock call to update the session, `UpdateAsync(testSession)`, was invoked. The `Verifiable` method call is checked by executing `mockRepo.Verify()` in the assertions.
* Two `Idea` objects are returned for the session.
* The last item (the `Idea` added by the mock call to `UpdateAsync`) matches the `newIdea` added to the session in the test.

```
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.CreateActionResult(newIdea);

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
    var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnValue.Ideas.Count());
    Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
```

## Related content

* [Integration tests in ASP.NET Core](../../test/integration-tests?view=aspnetcore-10.0)
* [Create and run unit tests with Visual Studio](/en-us/visualstudio/test/unit-test-your-code)
* [MyTested.AspNetCore.Mvc - Fluent Testing Library for ASP.NET Core MVC](https://github.com/ivaylokenov/MyTested.AspNetCore.Mvc): Strongly-typed unit testing library, providing a fluent interface for testing MVC and web API apps. (*Not maintained or supported by Microsoft.*)
* [JustMockLite](https://github.com/telerik/JustMockLite): A mocking framework for .NET developers. (*Not maintained or supported by Microsoft.*)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/actions
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/actions.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Handle requests with controllers in ASP.NET Core MVC

By [Steve Smith](https://ardalis.com/) and [Scott Addie](https://github.com/scottaddie)

Controllers, actions, and action results are a fundamental part of how developers build apps using ASP.NET Core MVC.

## What is a Controller?

A controller is used to define and group a set of actions. An action (or *action method*) is a method on a controller which handles requests. Controllers logically group similar actions together. This aggregation of actions allows common sets of rules, such as routing, caching, and authorization, to be applied collectively. Requests are mapped to actions through [routing](routing?view=aspnetcore-10.0). Controllers are activated and disposed on a per request basis.

By convention, controller classes:

* Reside in the project's root-level *Controllers* folder.
* Inherit from `Microsoft.AspNetCore.Mvc.Controller`.

A controller is an instantiable class, usually [public](/en-us/dotnet/csharp/language-reference/keywords/public), in which at least one of the following conditions is true:

* The class name is suffixed with `Controller`.
* The class inherits from a class whose name is suffixed with `Controller`.
* The `[Controller]` attribute is applied to the class.

A controller class must not have an associated `[NonController]` attribute.

Controllers should follow the [Explicit Dependencies Principle](/en-us/dotnet/architecture/modern-web-apps-azure/architectural-principles#explicit-dependencies). There are a couple of approaches to implementing this principle. If multiple controller actions require the same service, consider using [constructor injection](dependency-injection?view=aspnetcore-10.0#constructor-injection) to request those dependencies. If the service is needed by only a single action method, consider using [Action Injection](dependency-injection?view=aspnetcore-10.0#action-injection-with-fromservices) to request the dependency.

Within the **M**odel-**V**iew-**C**ontroller pattern, a controller is responsible for the initial processing of the request and instantiation of the model. Generally, business decisions should be performed within the model.

The controller takes the result of the model's processing (if any) and returns either the proper view and its associated view data or the result of the API call. Learn more at [Overview of ASP.NET Core MVC](../overview?view=aspnetcore-10.0) and [Get started with ASP.NET Core MVC and Visual Studio](../../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0).

The controller is a *UI-level* abstraction. Its responsibilities are to ensure request data is valid and to choose which view (or result for an API) should be returned. In well-factored apps, it doesn't directly include data access or business logic. Instead, the controller delegates to services handling these responsibilities.

## Defining Actions

Public methods on a controller, except those with the `[NonAction]` attribute, are actions. Parameters on actions are bound to request data and are validated using [model binding](../models/model-binding?view=aspnetcore-10.0). Model validation occurs for everything that's model-bound. The `ModelState.IsValid` property value indicates whether model binding and validation succeeded.

Action methods should contain logic for mapping a request to a business concern. Business concerns should typically be represented as services that the controller accesses through [dependency injection](dependency-injection?view=aspnetcore-10.0). Actions then map the result of the business action to an application state.

Actions can return anything, but they frequently return an instance of `IActionResult` (or `Task<IActionResult>` for async methods) that produces a response. The action method is responsible for choosing *what kind of response*. The action result *does the responding*.

### Controller Helper Methods

Controllers usually inherit from [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller), although this isn't required. Deriving from `Controller` provides access to three categories of helper methods:

#### 1. Methods resulting in an empty response body

No `Content-Type` HTTP response header is included, since the response body lacks content to describe.

There are two result types within this category: Redirect and HTTP Status Code.

* **HTTP Status Code**

  This type returns an HTTP status code. A couple of helper methods of this type are `BadRequest`, `NotFound`, and `Ok`. For example, `return BadRequest();` produces a 400 status code when executed. When methods such as `BadRequest`, `NotFound`, and `Ok` are overloaded, they no longer qualify as HTTP Status Code responders, since content negotiation is taking place.

* **Redirect**

  This type returns a redirect to an action or destination (using `Redirect`, `LocalRedirect`, `RedirectToAction`, or `RedirectToRoute`). For example, `return RedirectToAction("Complete", new {id = 123});` redirects to `Complete`, passing an anonymous object.

  The Redirect result type differs from the HTTP Status Code type primarily in the addition of a `Location` HTTP response header.

#### 2. Methods resulting in a non-empty response body with a predefined content type

Most helper methods in this category include a `ContentType` property, allowing you to set the `Content-Type` response header to describe the response body.

There are two result types within this category: [View](../views/overview?view=aspnetcore-10.0) and [Formatted Response](../../web-api/advanced/formatting?view=aspnetcore-10.0).

* **View**

  This type returns a view, which uses a model to render HTML. For example, `return View(customer);` passes a model to the view for data-binding.

* **Formatted Response**

  This type returns JSON or a similar data exchange format to represent an object in a specific manner. For example, `return Json(customer);` serializes the provided object into JSON format.

  Other common methods of this type include `File` and `PhysicalFile`. For example, `return PhysicalFile(customerFilePath, "text/xml");` returns [PhysicalFileResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.physicalfileresult).

#### 3. Methods resulting in a non-empty response body formatted in a content type negotiated with the client

This category is better known as **Content Negotiation**. [Content negotiation](../../web-api/advanced/formatting?view=aspnetcore-10.0#content-negotiation) applies whenever an action returns an [ObjectResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult) type or something other than an [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult) implementation. An action that returns a non-`IActionResult` implementation (for example, `object`) also returns a Formatted Response.

Some helper methods of this type include `BadRequest`, `CreatedAtRoute`, and `Ok`. Examples of these methods include `return BadRequest(modelState);`, `return CreatedAtRoute("routename", values, newobject);`, and `return Ok(value);`, respectively. Note that `BadRequest` and `Ok` perform content negotiation only when passed a value; without being passed a value, they instead serve as HTTP Status Code result types. The `CreatedAtRoute` method, on the other hand, always performs content negotiation since its overloads all require that a value be passed.

### Cross-Cutting Concerns

Applications typically share parts of their workflow. Examples include an app that requires authentication to access the shopping cart, or an app that caches data on some pages. To perform logic before or after an action method, use a *filter*. Using [Filters](filters?view=aspnetcore-10.0) on cross-cutting concerns can reduce duplication.

Most filter attributes, such as `[Authorize]`, can be applied at the controller or action level depending upon the desired level of granularity.

Error handling and response caching are often cross-cutting concerns:

* [Handle errors](filters?view=aspnetcore-10.0#exception-filters)
* [Response Caching](../../performance/caching/response?view=aspnetcore-10.0)

Many cross-cutting concerns can be handled using filters or custom [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-29

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/owin
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/owin.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Open Web Interface for .NET (OWIN) with ASP.NET Core

By [Steve Smith](https://ardalis.com/) and [Rick Anderson](https://twitter.com/RickAndMSFT)

ASP.NET Core:

* Supports the Open Web Interface for .NET (OWIN).
* Has compatible replacements for the `Microsoft.Owin.*` ([Katana](/en-us/aspnet/aspnet/overview/owin-and-katana/)) libraries.

OWIN allows web apps to be decoupled from web servers. It defines a standard way for middleware to be used in a pipeline to handle requests and associated responses. ASP.NET Core applications and middleware can interoperate with OWIN-based applications, servers, and middleware.

OWIN provides a decoupling layer that allows two frameworks with disparate object models to be used together. The `Microsoft.AspNetCore.Owin` package provides two adapter implementations:

* ASP.NET Core to OWIN
* OWIN to ASP.NET Core

This allows ASP.NET Core to be hosted on top of an OWIN compatible server/host or for other OWIN compatible components to be run on top of ASP.NET Core.

Note

Using these adapters comes with a performance cost. Apps using only ASP.NET Core components shouldn't use the `Microsoft.AspNetCore.Owin` package or adapters.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/owin/sample) ([how to download](./?view=aspnetcore-10.0#how-to-download-a-sample))

## Running OWIN middleware in the ASP.NET Core pipeline

ASP.NET Core's OWIN support is deployed as part of the `Microsoft.AspNetCore.Owin` package. You can import OWIN support into your project by installing this package.

OWIN middleware conforms to the [OWIN specification](http://owin.org/spec/spec/owin-1.0.0.html), which requires a `Func<IDictionary<string, object>, Task>` interface, and specific keys be set (such as `owin.ResponseBody`). The following simple OWIN middleware displays "Hello World":

```
public Task OwinHello(IDictionary<string, object> environment)
{
    string responseText = "Hello World via OWIN";
    byte[] responseBytes = Encoding.UTF8.GetBytes(responseText);

    // OWIN Environment Keys: http://owin.org/spec/spec/owin-1.0.0.html
    var responseStream = (Stream)environment["owin.ResponseBody"];
    var responseHeaders = (IDictionary<string, string[]>)environment["owin.ResponseHeaders"];

    responseHeaders["Content-Length"] = new string[] { responseBytes.Length.ToString(CultureInfo.InvariantCulture) };
    responseHeaders["Content-Type"] = new string[] { "text/plain" };

    return responseStream.WriteAsync(responseBytes, 0, responseBytes.Length);
}
```

The sample signature returns a `Task` and accepts an `IDictionary<string, object>` as required by OWIN.

The following code shows how to add the `OwinHello` middleware (shown above) to the ASP.NET Core pipeline with the `UseOwin` extension method.

```
public void Configure(IApplicationBuilder app)
{
    app.UseOwin(pipeline =>
    {
        pipeline(next => OwinHello);
    });
}
```

You can configure other actions to take place within the OWIN pipeline.

Note

Response headers should only be modified prior to the first write to the response stream.

Note

Multiple calls to `UseOwin` is discouraged for performance reasons. OWIN components will operate best if grouped together.

```
app.UseOwin(pipeline =>
{
    pipeline(next =>
    {
        return async environment =>
        {
            // Do something before.
            await next(environment);
            // Do something after.
        };
    });
});
```

## OWIN environment

You can construct an OWIN environment using the `HttpContext`.

```

   var environment = new OwinEnvironment(HttpContext);
   var features = new OwinFeatureCollection(environment);
```

## OWIN keys

OWIN depends on an `IDictionary<string,object>` object to communicate information throughout an HTTP Request/Response exchange. ASP.NET Core implements the keys listed below. See the [primary specification, extensions](http://owin.org/#spec), and [OWIN Key Guidelines and Common Keys](http://owin.org/spec/spec/CommonKeys.html).

### Request data (OWIN v1.0.0)

| Key                     | Value (type)                   | Description |
| ----------------------- | ------------------------------ | ----------- |
| owin.RequestScheme      | `String`                       |             |
| owin.RequestMethod      | `String`                       |             |
| owin.RequestPathBase    | `String`                       |             |
| owin.RequestPath        | `String`                       |             |
| owin.RequestQueryString | `String`                       |             |
| owin.RequestProtocol    | `String`                       |             |
| owin.RequestHeaders     | `IDictionary<string,string[]>` |             |
| owin.RequestBody        | `Stream`                       |             |

### Request data (OWIN v1.1.0)

| Key            | Value (type) | Description |
| -------------- | ------------ | ----------- |
| owin.RequestId | `String`     | Optional    |

### Response data (OWIN v1.0.0)

| Key                       | Value (type)                   | Description |
| ------------------------- | ------------------------------ | ----------- |
| owin.ResponseStatusCode   | `int`                          | Optional    |
| owin.ResponseReasonPhrase | `String`                       | Optional    |
| owin.ResponseHeaders      | `IDictionary<string,string[]>` |             |
| owin.ResponseBody         | `Stream`                       |             |

### Other data (OWIN v1.0.0)

| Key                | Value (type)        | Description |
| ------------------ | ------------------- | ----------- |
| owin.CallCancelled | `CancellationToken` |             |
| owin.Version       | `String`            |             |

### Common keys

| Key                     | Value (type)                    | Description |
| ----------------------- | ------------------------------- | ----------- |
| ssl.ClientCertificate   | `X509Certificate`               |             |
| ssl.LoadClientCertAsync | `Func<Task>`                    |             |
| server.RemoteIpAddress  | `String`                        |             |
| server.RemotePort       | `String`                        |             |
| server.LocalIpAddress   | `String`                        |             |
| server.LocalPort        | `String`                        |             |
| server.OnSendingHeaders | `Action<Action<object>,object>` |             |

### SendFiles v0.3.0

| Key                | Value (type)                                                                                 | Description |
| ------------------ | -------------------------------------------------------------------------------------------- | ----------- |
| sendfile.SendAsync | See [delegate signature](http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm) | Per Request |

### Opaque v0.3.0

| Key                  | Value (type)        | Description                                                                                  |
| -------------------- | ------------------- | -------------------------------------------------------------------------------------------- |
| opaque.Version       | `String`            |                                                                                              |
| opaque.Upgrade       | `OpaqueUpgrade`     | See [delegate signature](http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm) |
| opaque.Stream        | `Stream`            |                                                                                              |
| opaque.CallCancelled | `CancellationToken` |                                                                                              |

### WebSocket v0.3.0

| Key                              | Value (type)            | Description                                                                                  |
| -------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------- |
| websocket.Version                | `String`                |                                                                                              |
| websocket.Accept                 | `WebSocketAccept`       | See [delegate signature](http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm) |
| websocket.AcceptAlt              |                         | Non-spec                                                                                     |
| websocket.SubProtocol            | `String`                | See [RFC6455 Section 4.2.2](https://tools.ietf.org/html/rfc6455#section-4.2.2) Step 5.5      |
| websocket.SendAsync              | `WebSocketSendAsync`    | See [delegate signature](http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm) |
| websocket.ReceiveAsync           | `WebSocketReceiveAsync` | See [delegate signature](http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm) |
| websocket.CloseAsync             | `WebSocketCloseAsync`   | See [delegate signature](http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm) |
| websocket.CallCancelled          | `CancellationToken`     |                                                                                              |
| websocket.ClientCloseStatus      | `int`                   | Optional                                                                                     |
| websocket.ClientCloseDescription | `String`                | Optional                                                                                     |

## Related content

* See the [source on GitHub](https://github.com/aspnet/HttpAbstractions/blob/524a0227e5b7945b9520855d37881dabf7e5da32/src/Microsoft.AspNetCore.Owin/OwinFeatureCollection.cs) for OWIN keys supported in the translation layer.
* [Middleware](middleware/?view=aspnetcore-10.0)
* [Servers](servers/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/webassembly-browser-developer-tools-diagnostics
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/performance/webassembly-browser-developer-tools-diagnostics.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor WebAssembly browser developer tools diagnostics

This article describes browser developer tools diagnostic tools in Blazor WebAssembly apps.

## Prerequisite

Install the [.NET WebAssembly build tools](../webassembly-build-tools-and-aot?view=aspnetcore-10.0#net-webassembly-build-tools):

```
dotnet workload install wasm-tools
```

## Browser developer tools

App code can be manually profiled using the diagnostic profiler in a browser's developer tools console.

Built-in diagnostic features are available to track:

* [Ahead-of-time (AOT) compilation](../webassembly-build-tools-and-aot?view=aspnetcore-10.0#ahead-of-time-aot-compilation)
* Call specification ("callspec", sequence and timing of function calls) and instrumentation

The MSBuild properties in the following table enable profiler integration.

| Property                   | Default  | Set value to… | Description                                                                                                                                                                                                     |
| -------------------------- | -------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<WasmProfilers>`          | No value | `browser`     | Mono profilers to use. Potential values are "`browser`" and "`log`". To use both, separate the values with a semicolon. The `browser` profiler enables integration with the browser's developer tools profiler. |
| `<WasmNativeStrip>`        | `true`   | `false`       | Enables stripping the native executable.                                                                                                                                                                        |
| `<WasmNativeDebugSymbols>` | `true`   | `true`        | Enables building with native debug symbols.                                                                                                                                                                     |

Setting the [`Timing-Allow-Origin` HTTP header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Timing-Allow-Origin) allows for more precise time measurements.

Enabling profilers has negative size and performance impacts, so don't publish an app for production with profilers enabled. In the following example, a condition is set on a property group section that only enables profiling when the app is built with `/p:BlazorSampleProfilingEnabled=true` (.NET CLI) or `<BlazorSampleProfilingEnabled>true</BlazorSampleProfilingEnabled>` in a Visual Studio publish profile, where "`BlazorSampleProfilingEnabled`" is a custom symbol name that you choose and doesn't conflict with other symbol names.

In the app's project file (`.csproj`):

```
<PropertyGroup Condition="'$(BlazorSampleProfilingEnabled)' == 'true'">
  <WasmProfilers>browser;</WasmProfilers>
  <WasmNativeStrip>false</WasmNativeStrip>
  <WasmNativeDebugSymbols>true</WasmNativeDebugSymbols>
  <!-- disable debugger -->
  <WasmDebugLevel>0</WasmDebugLevel>
</PropertyGroup>
```

Alternatively, enable features when the app is built with the .NET CLI. The following options passed to the `dotnet build` command mirror the preceding MSBuild property configuration:

```
/p:WasmProfilers=browser /p:WasmNativeStrip=false /p:WasmNativeDebugSymbols=true
```

Setting WebAssembly profilers with `<WasmProfilers>` doesn't require [ahead-of-time (AOT) compilation](../webassembly-build-tools-and-aot?view=aspnetcore-10.0).

The browser developer tools profiler can be used with AOT (`<RunAOTCompilation>`/`<RunAOTCompilationAfterBuild>` set to `true`) and without WebAssembly profilers (`<WasmProfilers>` removed).

To see AOT method names in the developer tools console, install [DWARF chrome extension](https://chromewebstore.google.com/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb).

## Set the sample interval

Configure the sample interval in the app's project file. In the following example, the `{INTERVAL}` placeholder represents the time in milliseconds. The default setting if `sampleIntervalMs` isn't specified is 10 ms.

```
<PropertyGroup>
  <WasmProfilers>browser:interval={INTERVAL};</WasmProfilers>
</PropertyGroup>
```

## Call specification (callspec)

If you want to filter profiled methods, use call specification (callspec).

Add `callspec` with a filter to the `browser` WebAssembly profiler in the `<WasmProfilers>` element:

```
<WasmProfilers>browser:callspec={FILTER};</WasmProfilers>
```

Permissiable `{FILTER}` placeholder values are shown in the following table.

| Filter            | Description                           |
| ----------------- | ------------------------------------- |
| `all`             | All assemblies                        |
| `program`         | Entry point assembly                  |
| `{ASSEMBLY}`      | Specifies an assembly (`{ASSEMBLY}`)  |
| `M:Type:{METHOD}` | Specifies a method (`{METHOD}`)       |
| `N:{NAMESPACE}`   | Specifies a namespace (`{NAMESPACE}`) |
| `T:{TYPE}`        | Specifies a type (`{TYPE}`)           |
| `+EXPR`           | Includes expression                   |
| `-EXPR`           | Excludes expression                   |

In the following example, profiled methods are filtered to the app's namespace `SampleApp` and sampling interval is 50ms.

```
<WasmProfilers>browser:callspec=N:SampleApp,interval=50</WasmProfilers>
```

## Related content

* [What diagnostic tools are available in .NET Core?](/en-us/dotnet/core/diagnostics/)
* [.NET diagnostic tools](/en-us/dotnet/core/diagnostics/tools-overview)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/test/middleware
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/middleware.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Test ASP.NET Core middleware

By [Chris Ross](https://github.com/Tratcher)

Middleware can be tested in isolation with [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver). It allows you to:

* Instantiate an app pipeline containing only the components that you need to test.
* Send custom requests to verify middleware behavior.

Advantages:

* Requests are sent in-memory rather than being serialized over the network.
* This avoids additional concerns, such as port management and HTTPS certificates.
* Exceptions in the middleware can flow directly back to the calling test.
* It's possible to customize server data structures, such as [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext), directly in the test.

## Set up the TestServer

In the test project, create a test:

* Build and start a host that uses [TestServer](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver).

* Add any required services that the middleware uses.

* Add a package reference to the project for the [`Microsoft.AspNetCore.TestHost`](https://www.nuget.org/packages/Microsoft.AspNetCore.TestHost/) NuGet package.

* Configure the processing pipeline to use the middleware for the test.

  ```
  [Fact]
  public async Task MiddlewareTest_ReturnsNotFoundForRequest()
  {
      using var host = await new HostBuilder()
          .ConfigureWebHost(webBuilder =>
          {
              webBuilder
                  .UseTestServer()
                  .ConfigureServices(services =>
                  {
                      services.AddMyServices();
                  })
                  .Configure(app =>
                  {
                      app.UseMiddleware<MyMiddleware>();
                  });
          })
          .StartAsync();

      ...
  }
  ```

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

## Send requests with HttpClient

Send a request using [HttpClient](/en-us/dotnet/api/system.net.http.httpclient):

```
[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var response = await host.GetTestClient().GetAsync("/");

    ...
}
```

Assert the result. First, make an assertion the opposite of the result that you expect. An initial run with a false positive assertion confirms that the test fails when the middleware is performing correctly. Run the test and confirm that the test fails.

In the following example, the middleware should return a 404 status code (*Not Found*) when the root endpoint is requested. Make the first test run with `Assert.NotEqual( ... );`, which should fail:

```
[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var response = await host.GetTestClient().GetAsync("/");

    Assert.NotEqual(HttpStatusCode.NotFound, response.StatusCode);
}
```

Change the assertion to test the middleware under normal operating conditions. The final test uses `Assert.Equal( ... );`. Run the test again to confirm that it passes.

```
[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var response = await host.GetTestClient().GetAsync("/");

    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
```

## Send requests with HttpContext

A test app can also send a request using [SendAsync(Action\<HttpContext>, CancellationToken)](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver.sendasync). In the following example, several checks are made when `https://example.com/A/Path/?and=query` is processed by the middleware:

```
[Fact]
public async Task TestMiddleware_ExpectedResponse()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var server = host.GetTestServer();
    server.BaseAddress = new Uri("https://example.com/A/Path/");

    var context = await server.SendAsync(c =>
    {
        c.Request.Method = HttpMethods.Post;
        c.Request.Path = "/and/file.txt";
        c.Request.QueryString = new QueryString("?and=query");
    });

    Assert.True(context.RequestAborted.CanBeCanceled);
    Assert.Equal(HttpProtocol.Http11, context.Request.Protocol);
    Assert.Equal("POST", context.Request.Method);
    Assert.Equal("https", context.Request.Scheme);
    Assert.Equal("example.com", context.Request.Host.Value);
    Assert.Equal("/A/Path", context.Request.PathBase.Value);
    Assert.Equal("/and/file.txt", context.Request.Path.Value);
    Assert.Equal("?and=query", context.Request.QueryString.Value);
    Assert.NotNull(context.Request.Body);
    Assert.NotNull(context.Request.Headers);
    Assert.NotNull(context.Response.Headers);
    Assert.NotNull(context.Response.Body);
    Assert.Equal(404, context.Response.StatusCode);
    Assert.Null(context.Features.Get<IHttpResponseFeature>().ReasonPhrase);
}
```

[SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver.sendasync) permits direct configuration of an [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) object rather than using the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) abstractions. Use [SendAsync](/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver.sendasync) to manipulate structures only available on the server, such as [HttpContext.Items](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.items#microsoft-aspnetcore-http-httpcontext-items) or [HttpContext.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features#microsoft-aspnetcore-http-httpcontext-features).

As with the earlier example that tested for a *404 - Not Found* response, check the opposite for each `Assert` statement in the preceding test. The check confirms that the test fails correctly when the middleware is operating normally. After you've confirmed that the false positive test works, set the final `Assert` statements for the expected conditions and values of the test. Run it again to confirm that the test passes.

## Add request routes

Additional routes can be added by configuration using the test `HttpClient`:

```
	[Fact]
	public async Task TestWithEndpoint_ExpectedResponse ()
	{
		using var host = await new HostBuilder()
			.ConfigureWebHost(webBuilder =>
			{
				webBuilder
					.UseTestServer()
					.ConfigureServices(services =>
					{
						services.AddRouting();
					})
					.Configure(app =>
					{
						app.UseRouting();
						app.UseMiddleware<MyMiddleware>();
						app.UseEndpoints(endpoints =>
						{
							endpoints.MapGet("/hello", () =>
								TypedResults.Text("Hello Tests"));
						});
					});
			})
			.StartAsync();

		var client = host.GetTestClient();

		var response = await client.GetAsync("/hello");

		Assert.True(response.IsSuccessStatusCode);
		var responseBody = await response.Content.ReadAsStringAsync();
		Assert.Equal("Hello Tests", responseBody);
```

Additional routes can also be added using the approach `server.SendAsync`.

## TestServer limitations

TestServer:

* Was created to replicate server behaviors to test middleware.
* Does ***not*** try to replicate all [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) behaviors.
* Attempts to give the client access to as much control over the server as possible, and with as much visibility into what's happening on the server as possible. For example it may throw exceptions not normally thrown by `HttpClient` in order to directly communicate server state.
* Doesn't set some transport specific headers by default as those aren't usually relevant to middleware. For more information, see the next section.
* Ignores the `Stream` position passed through [StreamContent](/en-us/dotnet/api/system.net.http.streamcontent). [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) sends the entire stream from the start position, even when positioning is set. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/33780).

### Content-Length and Transfer-Encoding headers

TestServer does ***not*** set transport related request or response headers such as [Content-Length](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Length) or [Transfer-Encoding](https://developer.mozilla.org/docs/Web/HTTP/Headers/Transfer-Encoding). Applications should avoid depending on these headers because their usage varies by client, scenario, and protocol. If `Content-Length` and `Transfer-Encoding` are necessary to test a specific scenario, they can be specified in the test when composing the [HttpRequestMessage](/en-us/dotnet/api/system.net.http.httprequestmessage) or [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext). For more information, see the following GitHub issues:

* [dotnet/aspnetcore#21677](https://github.com/dotnet/aspnetcore/issues/21677)
* [dotnet/aspnetcore#18463](https://github.com/dotnet/aspnetcore/issues/18463)
* [dotnet/aspnetcore#13273](https://github.com/dotnet/aspnetcore/issues/13273)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/link-tag-helper
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/link-tag-helper.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Link Tag Helper in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

The [Link Tag Helper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.linktaghelper) generates a link to a primary or fall back CSS file. Typically the primary CSS file is on a [Content Delivery Network](/en-us/office365/enterprise/content-delivery-networks#what-exactly-is-a-cdn) (CDN).

A CDN:

* Provides several [performance advantages](/en-us/office365/enterprise/content-delivery-networks#how-do-cdns-make-services-work-faster) vs hosting the asset with the web app.
* Should not be relied on as the only source for the asset. CDNs are not always available, therefore a reliable fallback should be used. Typically the fallback is the site hosting the web app.

The Link Tag Helper allows you to specify a CDN for the CSS file and a fallback when the CDN is not available. The Link Tag Helper provides the performance advantage of a CDN with the robustness of local hosting.

The following Razor markup shows the `head` element of a layout file created with the ASP.NET Core web app template:

```
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebLinkTH</title>

    <environment include="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" 
              href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" 
              asp-fallback-test-value="absolute"
              crossorigin="anonymous"
              integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" />
    </environment>
    <link rel="stylesheet" href="~/css/site.css" />
</head>
```

The following is rendered HTML from the preceding code (in a non-`Development` environment):

```
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Home page - WebLinkTH</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.css"
          crossorigin="anonymous" integrity="sha256-eS<snip>BE=" />
    <meta name="x-stylesheet-fallback-test" content="" class="sr-only" />
    <script>
        !function (a, b, c, d) {
            var e, f = document,
                g = f.getElementsByTagName("SCRIPT"),
                h = g[g.length - 1].previousElementSibling,
                i = f.defaultView && f.defaultView.getComputedStyle ? f.defaultView.getComputedStyle(h) : h.currentStyle;
            if (i && i[a] !== b) for (e = 0; e < c.length; e++)
                f.write('<link href="' + c[e] + '" ' + d + "/>")
        }
            ("position", "absolute", ["\/lib\/bootstrap\/dist\/css\/bootstrap.css"],
                "rel=\u0022stylesheet\u0022 crossorigin=\u0022anonymous\u0022 integrity=\abc<snip>BE=\u0022 ");
    </script>

    <link rel="stylesheet" href="/css/site.css" />
</head>
```

In the preceding code, the Link Tag Helper generated the `<meta name="x-stylesheet-fallback-test" content="" class="sr-only" />` element and the following JavaScript which is used to verify the requested `bootstrap.css` file is available on the CDN. In this case, the CSS file was available so the Tag Helper generated the `<link />` element with the CDN CSS file.

## Commonly used Link Tag Helper attributes

See [Link Tag Helper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.linktaghelper) for all the Link Tag Helper attributes, properties, and methods.

### href

Preferred address of the linked resource. The address is passed thought to the generated HTML in all cases.

### asp-fallback-href

The URL of a CSS stylesheet to fallback to in the case the primary URL fails.

### asp-fallback-test-class

The class name defined in the stylesheet to use for the fallback test. For more information, see [FallbackTestClass](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.linktaghelper.fallbacktestclass#microsoft-aspnetcore-mvc-taghelpers-linktaghelper-fallbacktestclass).

### asp-fallback-test-property

The CSS property name to use for the fallback test. For more information, see [FallbackTestProperty](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.linktaghelper.fallbacktestproperty#microsoft-aspnetcore-mvc-taghelpers-linktaghelper-fallbacktestproperty).

### asp-fallback-test-value

The CSS property value to use for the fallback test. For more information, see [FallbackTestValue](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.linktaghelper.fallbacktestvalue#microsoft-aspnetcore-mvc-taghelpers-linktaghelper-fallbacktestvalue).

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/extensibility/misc-apis
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/extensibility/misc-apis.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Miscellaneous ASP.NET Core Data Protection APIs

[]()

Warning

Types that implement any of the following interfaces should be thread-safe for multiple callers.

## ISecret

The `ISecret` interface represents a secret value, such as cryptographic key material. It contains the following API surface:

* `Length`: `int`

* `Dispose()`: `void`

* `WriteSecretIntoBuffer(ArraySegment<byte> buffer)`: `void`

The `WriteSecretIntoBuffer` method populates the supplied buffer with the raw secret value. The reason this API takes the buffer as a parameter rather than returning a `byte[]` directly is that this gives the caller the opportunity to pin the buffer object, limiting secret exposure to the managed garbage collector.

The `Secret` type is a concrete implementation of `ISecret` where the secret value is stored in in-process memory. On Windows platforms, the secret value is encrypted via [CryptProtectMemory](/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectmemory).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/test/razor-pages-tests
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/razor-pages-tests.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Razor Pages unit tests in ASP.NET Core

ASP.NET Core supports unit tests of Razor Pages apps. Tests of the data access layer (DAL) and page models help ensure:

* Parts of a Razor Pages app work independently and together as a unit during app construction.
* Classes and methods have limited scopes of responsibility.
* Additional documentation exists on how the app should behave.
* Regressions, which are errors brought about by updates to the code, are found during automated building and deployment.

This topic assumes that you have a basic understanding of Razor Pages apps and unit tests. If you're unfamiliar with Razor Pages apps or test concepts, see the following topics:

* [Razor Pages architecture and concepts in ASP.NET Core](../razor-pages/?view=aspnetcore-10.0)
* [Tutorial: Get started with Razor Pages in ASP.NET Core](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Testing with `dotnet test`](/en-us/dotnet/core/testing/unit-testing-with-dotnet-test)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/test/razor-pages-tests/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample project is composed of two apps:

| App         | Project folder                     | Description                                                                                                                                   |
| ----------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Message app | *src/RazorPagesTestSample*         | Allows a user to add a message, delete one message, delete all messages, and analyze messages (find the average number of words per message). |
| Test app    | *tests/RazorPagesTestSample.Tests* | Used to unit test the DAL and Index page model of the message app.                                                                            |

The tests can be run using the built-in test features of an IDE, such as [Visual Studio](/en-us/visualstudio/test/unit-test-your-code). If using [Visual Studio Code](https://code.visualstudio.com/) or the command line, execute the following command at a command prompt in the *tests/RazorPagesTestSample.Tests* folder:

```
dotnet test
```

## Message app organization

The message app is a Razor Pages message system with the following characteristics:

* The Index page of the app (`Pages/Index.cshtml` and `Pages/Index.cshtml.cs`) provides a UI and page model methods to control the addition, deletion, and analysis of messages (find the average number of words per message).
* A message is described by the `Message` class (`Data/Message.cs`) with two properties: `Id` (key) and `Text` (message). The `Text` property is required and limited to 200 characters.
* Messages are stored using [Entity Framework's in-memory database](/en-us/ef/core/providers/in-memory/)†.
* The app contains a DAL in its database context class, `AppDbContext` (`Data/AppDbContext.cs`). The DAL methods are marked `virtual`, which allows mocking the methods for use in the tests.
* If the database is empty on app startup, the message store is initialized with three messages. These *seeded messages* are also used in tests.

†The EF topic, [Test with InMemory](/en-us/ef/core/miscellaneous/testing/in-memory), explains how to use an in-memory database for tests with MSTest. This topic uses the [xUnit](https://github.com/xunit/xunit) test framework. Test concepts and test implementations across different test frameworks are similar but not identical.

Although the sample app doesn't use the repository pattern and isn't an effective example of the [Unit of Work (UoW) pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html), Razor Pages supports these patterns of development. For more information, see [Designing the infrastructure persistence layer](/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) and [Test controller logic in ASP.NET Core](../mvc/controllers/testing?view=aspnetcore-10.0) (the sample implements the repository pattern).

## Test app organization

The test app is a console app inside the *tests/RazorPagesTestSample.Tests* folder.

| Test app folder | Description                                                                                                                                                                          |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| *UnitTests*     | * `DataAccessLayerTest.cs` contains the unit tests for the DAL.
* `IndexPageTests.cs` contains the unit tests for the Index page model.                                              |
| *Utilities*     | Contains the `TestDbContextOptions` method used to create new database context options for each DAL unit test so that the database is reset to its baseline condition for each test. |

The test framework is [xUnit](https://github.com/xunit/xunit). The object mocking framework is [Moq](https://github.com/moq/moq4).

## Unit tests of the data access layer (DAL)

The message app has a DAL with four methods contained in the `AppDbContext` class (`src/RazorPagesTestSample/Data/AppDbContext.cs`). Each method has one or two unit tests in the test app.

| DAL method               | Function                                                                   |
| ------------------------ | -------------------------------------------------------------------------- |
| `GetMessagesAsync`       | Obtains a `List<Message>` from the database sorted by the `Text` property. |
| `AddMessageAsync`        | Adds a `Message` to the database.                                          |
| `DeleteAllMessagesAsync` | Deletes all `Message` entries from the database.                           |
| `DeleteMessageAsync`     | Deletes a single `Message` from the database by `Id`.                      |

Unit tests of the DAL require [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) when creating a new `AppDbContext` for each test. One approach to creating the `DbContextOptions` for each test is to use a [DbContextOptionsBuilder](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder):

```
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}
```

The problem with this approach is that each test receives the database in whatever state the previous test left it. This can be problematic when trying to write atomic unit tests that don't interfere with each other. To force the `AppDbContext` to use a new database context for each test, supply a `DbContextOptions` instance that's based on a new service provider. The test app shows how to do this using its `Utilities` class method `TestDbContextOptions` (`tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs`):

```
public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}
```

Using the `DbContextOptions` in the DAL unit tests allows each test to run atomically with a fresh database instance:

```
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}
```

Each test method in the `DataAccessLayerTest` class (`UnitTests/DataAccessLayerTest.cs`) follows a similar Arrange-Act-Assert pattern:

1. Arrange: The database is configured for the test and/or the expected outcome is defined.
2. Act: The test is executed.
3. Assert: Assertions are made to determine if the test result is a success.

For example, the `DeleteMessageAsync` method is responsible for removing a single message identified by its `Id` (`src/RazorPagesTestSample/Data/AppDbContext.cs`):

```
public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}
```

There are two tests for this method. One test checks that the method deletes a message when the message is present in the database. The other method tests that the database doesn't change if the message `Id` for deletion doesn't exist. The `DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound` method is shown below:

```
[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}
```

First, the method performs the Arrange step, where preparation for the Act step takes place. The seeding messages are obtained and held in `seedMessages`. The seeding messages are saved into the database. The message with an `Id` of `1` is set for deletion. When the `DeleteMessageAsync` method is executed, the expected messages should have all of the messages except for the one with an `Id` of `1`. The `expectedMessages` variable represents this expected outcome.

```
// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();
```

The method acts: The `DeleteMessageAsync` method is executed passing in the `recId` of `1`:

```
// Act
await db.DeleteMessageAsync(recId);
```

Finally, the method obtains the `Messages` from the context and compares it to the `expectedMessages` asserting that the two are equal:

```
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
```

In order to compare that the two `List<Message>` are the same:

* The messages are ordered by `Id`.
* Message pairs are compared on the `Text` property.

A similar test method, `DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound` checks the result of attempting to delete a message that doesn't exist. In this case, the expected messages in the database should be equal to the actual messages after the `DeleteMessageAsync` method is executed. There should be no change to the database's content:

```
[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        try
        {
            await db.DeleteMessageAsync(recId);
        }
        catch
        {
            // recId doesn't exist
        }

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}
```

## Unit tests of the page model methods

Another set of unit tests is responsible for tests of page model methods. In the message app, the Index page models are found in the `IndexModel` class in `src/RazorPagesTestSample/Pages/Index.cshtml.cs`.

| Page model method              | Function                                                                                                                                                              |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OnGetAsync`                   | Obtains the messages from the DAL for the UI using the `GetMessagesAsync` method.                                                                                     |
| `OnPostAddMessageAsync`        | If the [ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary) is valid, calls `AddMessageAsync` to add a message to the database. |
| `OnPostDeleteAllMessagesAsync` | Calls `DeleteAllMessagesAsync` to delete all of the messages in the database.                                                                                         |
| `OnPostDeleteMessageAsync`     | Executes `DeleteMessageAsync` to delete a message with the `Id` specified.                                                                                            |
| `OnPostAnalyzeMessagesAsync`   | If one or more messages are in the database, calculates the average number of words per message.                                                                      |

The page model methods are tested using seven tests in the `IndexPageTests` class (`tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs`). The tests use the familiar Arrange-Act-Assert pattern. These tests focus on:

* Determining if the methods follow the correct behavior when the [ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary) is invalid.
* Confirming the methods produce the correct [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult).
* Checking that property value assignments are made correctly.

This group of tests often mock the methods of the DAL to produce expected data for the Act step where a page model method is executed. For example, the `GetMessagesAsync` method of the `AppDbContext` is mocked to produce output. When a page model method executes this method, the mock returns the result. The data doesn't come from the database. This creates predictable, reliable test conditions for using the DAL in the page model tests.

The `OnGetAsync_PopulatesThePageModel_WithAListOfMessages` test shows how the `GetMessagesAsync` method is mocked for the page model:

```
var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);
```

When the `OnGetAsync` method is executed in the Act step, it calls the page model's `GetMessagesAsync` method.

Unit test Act step (`tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs`):

```
// Act
await pageModel.OnGetAsync();
```

`IndexPage` page model's `OnGetAsync` method (`src/RazorPagesTestSample/Pages/Index.cshtml.cs`):

```
public async Task OnGetAsync()
{
    Messages = await _db.GetMessagesAsync();
}
```

The `GetMessagesAsync` method in the DAL doesn't return the result for this method call. The mocked version of the method returns the result.

In the `Assert` step, the actual messages (`actualMessages`) are assigned from the `Messages` property of the page model. A type check is also performed when the messages are assigned. The expected and actual messages are compared by their `Text` properties. The test asserts that the two `List<Message>` instances contain the same messages.

```
// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
```

Other tests in this group create page model objects that include the [DefaultHttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.defaulthttpcontext), the [ModelStateDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary), an [ActionContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actioncontext) to establish the `PageContext`, a `ViewDataDictionary`, and a `PageContext`. These are useful in conducting tests. For example, the message app establishes a `ModelState` error with [AddModelError](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.addmodelerror) to check that a valid [PageResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pageresult) is returned when `OnPostAddMessageAsync` is executed:

```
[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}
```

## Additional resources

* [Testing with `dotnet test`](/en-us/dotnet/core/testing/unit-testing-with-dotnet-test)
* [Test controller logic in ASP.NET Core](../mvc/controllers/testing?view=aspnetcore-10.0)
* [Unit Test Your Code](/en-us/visualstudio/test/unit-test-your-code) (Visual Studio)
* [Integration tests in ASP.NET Core](integration-tests?view=aspnetcore-10.0)
* [xUnit.net](https://github.com/xunit/xunit)
* [Getting started with xUnit.net](https://xunit.net/docs/getting-started/netcore/cmdline)
* [Moq](https://github.com/moq/moq4)
* [Moq Quickstart](https://github.com/Moq/moq4/wiki/Quickstart)

ASP.NET Core supports unit tests of Razor Pages apps. Tests of the data access layer (DAL) and page models help ensure:

* Parts of a Razor Pages app work independently and together as a unit during app construction.
* Classes and methods have limited scopes of responsibility.
* Additional documentation exists on how the app should behave.
* Regressions, which are errors brought about by updates to the code, are found during automated building and deployment.

This topic assumes that you have a basic understanding of Razor Pages apps and unit tests. If you're unfamiliar with Razor Pages apps or test concepts, see the following topics:

* [Razor Pages architecture and concepts in ASP.NET Core](../razor-pages/?view=aspnetcore-10.0)
* [Tutorial: Get started with Razor Pages in ASP.NET Core](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Testing with `dotnet test`](/en-us/dotnet/core/testing/unit-testing-with-dotnet-test)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/test/razor-pages-tests/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

The sample project is composed of two apps:

| App         | Project folder                     | Description                                                                                                                                   |
| ----------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Message app | *src/RazorPagesTestSample*         | Allows a user to add a message, delete one message, delete all messages, and analyze messages (find the average number of words per message). |
| Test app    | *tests/RazorPagesTestSample.Tests* | Used to unit test the DAL and Index page model of the message app.                                                                            |

The tests can be run using the built-in test features of an IDE, such as [Visual Studio](/en-us/visualstudio/test/unit-test-your-code). If using [Visual Studio Code](https://code.visualstudio.com/) or the command line, execute the following command at a command prompt in the *tests/RazorPagesTestSample.Tests* folder:

```
dotnet test
```

## Message app organization

The message app is a Razor Pages message system with the following characteristics:

* The Index page of the app (`Pages/Index.cshtml` and `Pages/Index.cshtml.cs`) provides a UI and page model methods to control the addition, deletion, and analysis of messages (find the average number of words per message).
* A message is described by the `Message` class (`Data/Message.cs`) with two properties: `Id` (key) and `Text` (message). The `Text` property is required and limited to 200 characters.
* Messages are stored using [Entity Framework's in-memory database](/en-us/ef/core/providers/in-memory/)†.
* The app contains a DAL in its database context class, `AppDbContext` (`Data/AppDbContext.cs`). The DAL methods are marked `virtual`, which allows mocking the methods for use in the tests.
* If the database is empty on app startup, the message store is initialized with three messages. These *seeded messages* are also used in tests.

†The EF topic, [Test with InMemory](/en-us/ef/core/miscellaneous/testing/in-memory), explains how to use an in-memory database for tests with MSTest. This topic uses the [xUnit](https://github.com/xunit/xunit) test framework. Test concepts and test implementations across different test frameworks are similar but not identical.

Although the sample app doesn't use the repository pattern and isn't an effective example of the [Unit of Work (UoW) pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html), Razor Pages supports these patterns of development. For more information, see [Designing the infrastructure persistence layer](/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design) and [Test controller logic in ASP.NET Core](../mvc/controllers/testing?view=aspnetcore-10.0) (the sample implements the repository pattern).

## Test app organization

The test app is a console app inside the *tests/RazorPagesTestSample.Tests* folder.

| Test app folder | Description                                                                                                                                                                          |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| *UnitTests*     | * `DataAccessLayerTest.cs` contains the unit tests for the DAL.
* `IndexPageTests.cs` contains the unit tests for the Index page model.                                              |
| *Utilities*     | Contains the `TestDbContextOptions` method used to create new database context options for each DAL unit test so that the database is reset to its baseline condition for each test. |

The test framework is [xUnit](https://github.com/xunit/xunit). The object mocking framework is [Moq](https://github.com/moq/moq4).

## Unit tests of the data access layer (DAL)

The message app has a DAL with four methods contained in the `AppDbContext` class (`src/RazorPagesTestSample/Data/AppDbContext.cs`). Each method has one or two unit tests in the test app.

| DAL method               | Function                                                                   |
| ------------------------ | -------------------------------------------------------------------------- |
| `GetMessagesAsync`       | Obtains a `List<Message>` from the database sorted by the `Text` property. |
| `AddMessageAsync`        | Adds a `Message` to the database.                                          |
| `DeleteAllMessagesAsync` | Deletes all `Message` entries from the database.                           |
| `DeleteMessageAsync`     | Deletes a single `Message` from the database by `Id`.                      |

Unit tests of the DAL require [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) when creating a new `AppDbContext` for each test. One approach to creating the `DbContextOptions` for each test is to use a [DbContextOptionsBuilder](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder):

```
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}
```

The problem with this approach is that each test receives the database in whatever state the previous test left it. This can be problematic when trying to write atomic unit tests that don't interfere with each other. To force the `AppDbContext` to use a new database context for each test, supply a `DbContextOptions` instance that's based on a new service provider. The test app shows how to do this using its `Utilities` class method `TestDbContextOptions` (`tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs`):

```
public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}
```

Using the `DbContextOptions` in the DAL unit tests allows each test to run atomically with a fresh database instance:

```
using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}
```

Each test method in the `DataAccessLayerTest` class (`UnitTests/DataAccessLayerTest.cs`) follows a similar Arrange-Act-Assert pattern:

1. Arrange: The database is configured for the test and/or the expected outcome is defined.
2. Act: The test is executed.
3. Assert: Assertions are made to determine if the test result is a success.

For example, the `DeleteMessageAsync` method is responsible for removing a single message identified by its `Id` (`src/RazorPagesTestSample/Data/AppDbContext.cs`):

```
public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}
```

There are two tests for this method. One test checks that the method deletes a message when the message is present in the database. The other method tests that the database doesn't change if the message `Id` for deletion doesn't exist. The `DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound` method is shown below:

```
[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}
```

First, the method performs the Arrange step, where preparation for the Act step takes place. The seeding messages are obtained and held in `seedMessages`. The seeding messages are saved into the database. The message with an `Id` of `1` is set for deletion. When the `DeleteMessageAsync` method is executed, the expected messages should have all of the messages except for the one with an `Id` of `1`. The `expectedMessages` variable represents this expected outcome.

```
// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();
```

The method acts: The `DeleteMessageAsync` method is executed passing in the `recId` of `1`:

```
// Act
await db.DeleteMessageAsync(recId);
```

Finally, the method obtains the `Messages` from the context and compares it to the `expectedMessages` asserting that the two are equal:

```
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
```

In order to compare that the two `List<Message>` are the same:

* The messages are ordered by `Id`.
* Message pairs are compared on the `Text` property.

A similar test method, `DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound` checks the result of attempting to delete a message that doesn't exist. In this case, the expected messages in the database should be equal to the actual messages after the `DeleteMessageAsync` method is executed. There should be no change to the database's content:

```
[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}
```

## Unit tests of the page model methods

Another set of unit tests is responsible for tests of page model methods. In the message app, the Index page models are found in the `IndexModel` class in `src/RazorPagesTestSample/Pages/Index.cshtml.cs`.

| Page model method              | Function                                                                                                                                                              |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OnGetAsync`                   | Obtains the messages from the DAL for the UI using the `GetMessagesAsync` method.                                                                                     |
| `OnPostAddMessageAsync`        | If the [ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary) is valid, calls `AddMessageAsync` to add a message to the database. |
| `OnPostDeleteAllMessagesAsync` | Calls `DeleteAllMessagesAsync` to delete all of the messages in the database.                                                                                         |
| `OnPostDeleteMessageAsync`     | Executes `DeleteMessageAsync` to delete a message with the `Id` specified.                                                                                            |
| `OnPostAnalyzeMessagesAsync`   | If one or more messages are in the database, calculates the average number of words per message.                                                                      |

The page model methods are tested using seven tests in the `IndexPageTests` class (`tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs`). The tests use the familiar Arrange-Act-Assert pattern. These tests focus on:

* Determining if the methods follow the correct behavior when the [ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary) is invalid.
* Confirming the methods produce the correct [IActionResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iactionresult).
* Checking that property value assignments are made correctly.

This group of tests often mock the methods of the DAL to produce expected data for the Act step where a page model method is executed. For example, the `GetMessagesAsync` method of the `AppDbContext` is mocked to produce output. When a page model method executes this method, the mock returns the result. The data doesn't come from the database. This creates predictable, reliable test conditions for using the DAL in the page model tests.

The `OnGetAsync_PopulatesThePageModel_WithAListOfMessages` test shows how the `GetMessagesAsync` method is mocked for the page model:

```
var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);
```

When the `OnGetAsync` method is executed in the Act step, it calls the page model's `GetMessagesAsync` method.

Unit test Act step (`tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs`):

```
// Act
await pageModel.OnGetAsync();
```

`IndexPage` page model's `OnGetAsync` method (`src/RazorPagesTestSample/Pages/Index.cshtml.cs`):

```
public async Task OnGetAsync()
{
    Messages = await _db.GetMessagesAsync();
}
```

The `GetMessagesAsync` method in the DAL doesn't return the result for this method call. The mocked version of the method returns the result.

In the `Assert` step, the actual messages (`actualMessages`) are assigned from the `Messages` property of the page model. A type check is also performed when the messages are assigned. The expected and actual messages are compared by their `Text` properties. The test asserts that the two `List<Message>` instances contain the same messages.

```
// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
```

Other tests in this group create page model objects that include the [DefaultHttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.defaulthttpcontext), the [ModelStateDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary), an [ActionContext](/en-us/dotnet/api/microsoft.aspnetcore.mvc.actioncontext) to establish the `PageContext`, a `ViewDataDictionary`, and a `PageContext`. These are useful in conducting tests. For example, the message app establishes a `ModelState` error with [AddModelError](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.addmodelerror) to check that a valid [PageResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pageresult) is returned when `OnPostAddMessageAsync` is executed:

```
[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}
```

## Additional resources

* [Testing with `dotnet test`](/en-us/dotnet/core/testing/unit-testing-with-dotnet-test)
* [Test controller logic in ASP.NET Core](../mvc/controllers/testing?view=aspnetcore-10.0)
* [Unit Test Your Code](/en-us/visualstudio/test/unit-test-your-code) (Visual Studio)
* [Integration tests in ASP.NET Core](integration-tests?view=aspnetcore-10.0)
* [xUnit.net](https://github.com/xunit/xunit)
* [Getting started with xUnit.net](https://xunit.net/docs/getting-started/netcore/cmdline)
* [Moq](https://github.com/moq/moq4)
* [Moq Quickstart](https://github.com/Moq/moq4/wiki/Quickstart)
* [JustMockLite](https://github.com/telerik/JustMockLite): A mocking framework for .NET developers. (*Not maintained or supported by Microsoft.*)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/http-modules
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/http-modules.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate HTTP modules to ASP.NET Core middleware

This article shows how to migrate existing ASP.NET [HTTP modules from system.webserver](/en-us/iis/configuration/system.webserver/) to ASP.NET Core [middleware](../../../fundamentals/middleware/?view=aspnetcore-10.0).

## Modules revisited

Before proceeding to ASP.NET Core middleware, let's first recap how HTTP modules work:

**Modules are:**

* Classes that implement [IHttpModule](/en-us/dotnet/api/system.web.ihttpmodule)

* Invoked for every request

* Able to short-circuit (stop further processing of a request)

* Able to add to the HTTP response, or create their own

* [Configured](/en-us/iis/configuration/system.webserver/modules/) in *Web.config*

**The order in which modules process incoming requests is determined by:**

1. A series events fired by ASP.NET, such as [BeginRequest](/en-us/dotnet/api/system.web.httpapplication.beginrequest#system-web-httpapplication-beginrequest) and [AuthenticateRequest](/en-us/dotnet/api/system.web.httpapplication.authenticaterequest#system-web-httpapplication-authenticaterequest). For a complete list, see [System.Web.HttpApplication](/en-us/dotnet/api/system.web.httpapplication). Each module can create a handler for one or more events.

2. For the same event, the order in which they're configured in *Web.config*.

In addition to modules, you can add handlers for the life cycle events to your `Global.asax.cs` file. These handlers run after the handlers in the configured modules.

## From modules to middleware

**Middleware are simpler than HTTP modules:**

* Modules, `Global.asax.cs`, *Web.config* (except for IIS configuration) and the application life cycle are gone

* The roles of modules have been taken over by middleware

* Middleware are configured using code rather than in *Web.config*

- [Pipeline branching](../../../fundamentals/middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline) lets you send requests to specific middleware, based on not only the URL but also on request headers, query strings, etc.

* [Pipeline branching](../../../fundamentals/middleware/?view=aspnetcore-10.0#branch-the-middleware-pipeline) lets you send requests to specific middleware, based on not only the URL but also on request headers, query strings, etc.

**Middleware are very similar to modules:**

* Invoked in principle for every request

* Able to short-circuit a request, by [not passing the request to the next middleware](#http-modules-shortcircuiting-middleware)

* Able to create their own HTTP response

**Middleware and modules are processed in a different order:**

* Order of middleware is based on the order in which they're inserted into the request pipeline, while order of modules is mainly based on [System.Web.HttpApplication](/en-us/dotnet/api/system.web.httpapplication) events.

* Order of middleware for responses is the reverse from that for requests, while order of modules is the same for requests and responses

* See [Create a middleware pipeline with IApplicationBuilder](../../../fundamentals/middleware/?view=aspnetcore-10.0#create-a-middleware-pipeline-with-iapplicationbuilder)

Note how in the image above, the authentication middleware short-circuited the request.

## Migrating module code to middleware

An existing HTTP module will look similar to this:

```
// ASP.NET 4 module

using System;
using System.Web;

namespace MyApp.Modules
{
    public class MyModule : IHttpModule
    {
        public void Dispose()
        {
        }

        public void Init(HttpApplication application)
        {
            application.BeginRequest += (new EventHandler(this.Application_BeginRequest));
            application.EndRequest += (new EventHandler(this.Application_EndRequest));
        }

        private void Application_BeginRequest(Object source, EventArgs e)
        {
            HttpContext context = ((HttpApplication)source).Context;

            // Do something with context near the beginning of request processing.
        }

        private void Application_EndRequest(Object source, EventArgs e)
        {
            HttpContext context = ((HttpApplication)source).Context;

            // Do something with context near the end of request processing.
        }
    }
}
```

As shown in the [Middleware](../../../fundamentals/middleware/?view=aspnetcore-10.0) page, an ASP.NET Core middleware is a class that exposes an `Invoke` method taking an `HttpContext` and returning a `Task`. Your new middleware will look like this:

[]()

```
// ASP.NET Core middleware

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace MyApp.Middleware
{
    public class MyMiddleware
    {
        private readonly RequestDelegate _next;

        public MyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            // Do something with context near the beginning of request processing.

            await _next.Invoke(context);

            // Clean up.
        }
    }

    public static class MyMiddlewareExtensions
    {
        public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<MyMiddleware>();
        }
    }
}
```

The preceding middleware template was taken from the section on [writing middleware](../../../fundamentals/middleware/write?view=aspnetcore-10.0).

The *MyMiddlewareExtensions* helper class makes it easier to configure your middleware in your `Startup` class. The `UseMyMiddleware` method adds your middleware class to the request pipeline. Services required by the middleware get injected in the middleware's constructor.

[]()

Your module might terminate a request, for example if the user isn't authorized:

```
// ASP.NET 4 module that may terminate the request

private void Application_BeginRequest(Object source, EventArgs e)
{
    HttpContext context = ((HttpApplication)source).Context;

    // Do something with context near the beginning of request processing.

    if (TerminateRequest())
    {
        context.Response.End();
        return;
    }
}
```

A middleware handles this by not calling `Invoke` on the next middleware in the pipeline. Keep in mind that this doesn't fully terminate the request, because previous middlewares will still be invoked when the response makes its way back through the pipeline.

```
// ASP.NET Core middleware that may terminate the request

public async Task Invoke(HttpContext context)
{
    // Do something with context near the beginning of request processing.

    if (!TerminateRequest())
        await _next.Invoke(context);

    // Clean up.
}
```

When you migrate your module's functionality to your new middleware, you may find that your code doesn't compile because the `HttpContext` class has significantly changed in ASP.NET Core. See [Migrate from ASP.NET Framework HttpContext to ASP.NET Core](http-context?view=aspnetcore-10.0) to learn how to migrate to the new ASP.NET Core HttpContext.

## Migrating module insertion into the request pipeline

HTTP modules are typically added to the request pipeline using *Web.config*:

```
<?xml version="1.0" encoding="utf-8"?>
<!--ASP.NET 4 web.config-->
<configuration>
  <system.webServer>
    <modules>
      <add name="MyModule" type="MyApp.Modules.MyModule"/>
    </modules>
  </system.webServer>
</configuration>
```

Convert this by [adding your new middleware](../../../fundamentals/middleware/?view=aspnetcore-10.0#create-a-middleware-pipeline-with-iapplicationbuilder) to the request pipeline in your `Startup` class:

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseMyMiddleware();

    app.UseMyMiddlewareWithParams();

    var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>();
    var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
    app.UseMyMiddlewareWithParams(myMiddlewareOptions);
    app.UseMyMiddlewareWithParams(myMiddlewareOptions2);

    app.UseMyTerminatingMiddleware();

    // Create branch to the MyHandlerMiddleware. 
    // All requests ending in .report will follow this branch.
    app.MapWhen(
        context => context.Request.Path.ToString().EndsWith(".report"),
        appBranch => {
            // ... optionally add more middleware to this branch
            appBranch.UseMyHandler();
        });

    app.MapWhen(
        context => context.Request.Path.ToString().EndsWith(".context"),
        appBranch => {
            appBranch.UseHttpContextDemoMiddleware();
        });

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

The exact spot in the pipeline where you insert your new middleware depends on the event that it handled as a module (`BeginRequest`, `EndRequest`, etc.) and its order in your list of modules in *Web.config*.

As previously stated, there's no application life cycle in ASP.NET Core and the order in which responses are processed by middleware differs from the order used by modules. This could make your ordering decision more challenging.

If ordering becomes a problem, you could split your module into multiple middleware components that can be ordered independently.

## Loading middleware options using the options pattern

Some modules have configuration options that are stored in *Web.config*. However, in ASP.NET Core a new configuration model is used in place of *Web.config*.

The new [configuration system](../../../fundamentals/configuration/?view=aspnetcore-10.0) gives you these options to solve this:

* Directly inject the options into the middleware, as shown in the [next section](#loading-middleware-options-through-direct-injection).

* Use the [options pattern](../../../fundamentals/configuration/options?view=aspnetcore-10.0):

1. Create a class to hold your middleware options, for example:

   ```
   public class MyMiddlewareOptions
   {
       public string Param1 { get; set; }
       public string Param2 { get; set; }
   }
   ```

2. Store the option values

   The configuration system allows you to store option values anywhere you want. However, most sites use `appsettings.json`, so we'll take that approach:

   ```
   {
     "MyMiddlewareOptionsSection": {
       "Param1": "Param1Value",
       "Param2": "Param2Value"
     }
   }
   ```

   *MyMiddlewareOptionsSection* here is a section name. It doesn't have to be the same as the name of your options class.

3. Associate the option values with the options class

   The options pattern uses ASP.NET Core's dependency injection framework to associate the options type (such as `MyMiddlewareOptions`) with a `MyMiddlewareOptions` object that has the actual options.

   Update your `Startup` class:

   1. If you're using `appsettings.json`, add it to the configuration builder in the `Startup` constructor:

      ```
      public Startup(IHostingEnvironment env)
      {
          var builder = new ConfigurationBuilder()
              .SetBasePath(env.ContentRootPath)
              .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
              .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
              .AddEnvironmentVariables();
          Configuration = builder.Build();
      }
      ```

   2. Configure the options service:

      ```
      public void ConfigureServices(IServiceCollection services)
      {
          // Setup options service
          services.AddOptions();

          // Load options from section "MyMiddlewareOptionsSection"
          services.Configure<MyMiddlewareOptions>(
              Configuration.GetSection("MyMiddlewareOptionsSection"));

          // Add framework services.
          services.AddMvc();
      }
      ```

   3. Associate your options with your options class:

      ```
      public void ConfigureServices(IServiceCollection services)
      {
          // Setup options service
          services.AddOptions();

          // Load options from section "MyMiddlewareOptionsSection"
          services.Configure<MyMiddlewareOptions>(
              Configuration.GetSection("MyMiddlewareOptionsSection"));

          // Add framework services.
          services.AddMvc();
      }
      ```

4. Inject the options into your middleware constructor. This is similar to injecting options into a controller.

   ```
   public class MyMiddlewareWithParams
   {
       private readonly RequestDelegate _next;
       private readonly MyMiddlewareOptions _myMiddlewareOptions;

       public MyMiddlewareWithParams(RequestDelegate next,
           IOptions<MyMiddlewareOptions> optionsAccessor)
       {
           _next = next;
           _myMiddlewareOptions = optionsAccessor.Value;
       }

       public async Task Invoke(HttpContext context)
       {
           // Do something with context near the beginning of request processing
           // using configuration in _myMiddlewareOptions

           await _next.Invoke(context);

           // Do something with context near the end of request processing
           // using configuration in _myMiddlewareOptions
       }
   }
   ```

   The [UseMiddleware](#http-modules-usemiddleware) extension method that adds your middleware to the `IApplicationBuilder` takes care of dependency injection.

   This isn't limited to `IOptions` objects. Any other object that your middleware requires can be injected this way.

## Loading middleware options through direct injection

The options pattern has the advantage that it creates loose coupling between options values and their consumers. Once you've associated an options class with the actual options values, any other class can get access to the options through the dependency injection framework. There's no need to pass around options values.

This breaks down though if you want to use the same middleware twice, with different options. For example an authorization middleware used in different branches allowing different roles. You can't associate two different options objects with the one options class.

The solution is to get the options objects with the actual options values in your `Startup` class and pass those directly to each instance of your middleware.

1. Add a second key to `appsettings.json`

   To add a second set of options to the `appsettings.json` file, use a new key to uniquely identify it:

   ```
   {
     "MyMiddlewareOptionsSection2": {
       "Param1": "Param1Value2",
       "Param2": "Param2Value2"
     },
     "MyMiddlewareOptionsSection": {
       "Param1": "Param1Value",
       "Param2": "Param2Value"
     }
   }
   ```

2. Retrieve options values and pass them to middleware. The `Use...` extension method (which adds your middleware to the pipeline) is a logical place to pass in the option values:

   ```
   public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
   {
       loggerFactory.AddConsole(Configuration.GetSection("Logging"));
       loggerFactory.AddDebug();

       if (env.IsDevelopment())
       {
           app.UseDeveloperExceptionPage();
           app.UseBrowserLink();
       }
       else
       {
           app.UseExceptionHandler("/Home/Error");
       }

       app.UseMyMiddleware();

       app.UseMyMiddlewareWithParams();

       var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>();
       var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
       app.UseMyMiddlewareWithParams(myMiddlewareOptions);
       app.UseMyMiddlewareWithParams(myMiddlewareOptions2);

       app.UseMyTerminatingMiddleware();

       // Create branch to the MyHandlerMiddleware. 
       // All requests ending in .report will follow this branch.
       app.MapWhen(
           context => context.Request.Path.ToString().EndsWith(".report"),
           appBranch => {
               // ... optionally add more middleware to this branch
               appBranch.UseMyHandler();
           });

       app.MapWhen(
           context => context.Request.Path.ToString().EndsWith(".context"),
           appBranch => {
               appBranch.UseHttpContextDemoMiddleware();
           });

       app.UseStaticFiles();

       app.UseMvc(routes =>
       {
           routes.MapRoute(
               name: "default",
               template: "{controller=Home}/{action=Index}/{id?}");
       });
   }
   ```

3. Enable middleware to take an options parameter. Provide an overload of the `Use...` extension method (that takes the options parameter and passes it to `UseMiddleware`). When `UseMiddleware` is called with parameters, it passes the parameters to your middleware constructor when it instantiates the middleware object.

   ```
   public static class MyMiddlewareWithParamsExtensions
   {
       public static IApplicationBuilder UseMyMiddlewareWithParams(
           this IApplicationBuilder builder)
       {
           return builder.UseMiddleware<MyMiddlewareWithParams>();
       }

       public static IApplicationBuilder UseMyMiddlewareWithParams(
           this IApplicationBuilder builder, MyMiddlewareOptions myMiddlewareOptions)
       {
           return builder.UseMiddleware<MyMiddlewareWithParams>(
               new OptionsWrapper<MyMiddlewareOptions>(myMiddlewareOptions));
       }
   }
   ```

   Note how this wraps the options object in an `OptionsWrapper` object. This implements `IOptions`, as expected by the middleware constructor.

## Incremental IHttpModule migration

There are times when converting modules to middleware cannot easily be done. In order to support migration scenarios in which modules are required and cannot be moved to middleware, System.Web adapters support adding them to ASP.NET Core.

### IHttpModule Example

In order to support modules, an instance of [HttpApplication](/en-us/dotnet/api/system.web.httpapplication) must be available. If no custom [HttpApplication](/en-us/dotnet/api/system.web.httpapplication) is used, a default one will be used to add the modules to. Events declared in a custom application (including `Application_Start`) will be registered and run accordingly.

```
using System.Web;
using Microsoft.AspNetCore.OutputCaching;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSystemWebAdapters()
    .AddHttpApplication<MyApp>(options =>
    {
        // Size of pool for HttpApplication instances. Should be what the expected concurrent requests will be
        options.PoolSize = 10;

        // Register a module (optionally) by name
        options.RegisterModule<MyModule>("MyModule");
    });

// Only available in .NET 7+
builder.Services.AddOutputCache(options =>
{
    options.AddHttpApplicationBasePolicy(_ => new[] { "browser" });
});

builder.Services.AddAuthentication();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthenticationEvents();

app.UseAuthorization();
app.UseAuthorizationEvents();

app.UseSystemWebAdapters();
app.UseOutputCache();

app.MapGet("/", () => "Hello World!")
    .CacheOutput();

app.Run();

class MyApp : HttpApplication
{
    protected void Application_Start()
    {
    }

    public override string? GetVaryByCustomString(System.Web.HttpContext context, string custom)
    {
        // Any custom vary-by string needed

        return base.GetVaryByCustomString(context, custom);
    }
}

class MyModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.BeginRequest += (s, e) =>
        {
            // Handle events at the beginning of a request
        };

        application.AuthorizeRequest += (s, e) =>
        {
            // Handle events that need to be authorized
        };
    }

    public void Dispose()
    {
    }
}
```

### Global.asax migration

This infrastructure can be used to migrate usage of `Global.asax` if needed. The source from `Global.asax` is a custom [HttpApplication](/en-us/dotnet/api/system.web.httpapplication) and the file can be included in an ASP.NET Core application. Since it is named `Global`, the following code can be used to register it:

```
builder.Services.AddSystemWebAdapters()
    .AddHttpApplication<Global>();
```

As long as the logic within it is available in ASP.NET Core, this approach can be used to incrementally migrate reliance on `Global.asax` to ASP.NET Core.

### Authentication/Authorization events

In order for the authentication and authorization events to run at the desired time, the following pattern should be used:

```
app.UseAuthentication();
app.UseAuthenticationEvents();

app.UseAuthorization();
app.UseAuthorizationEvents();
```

If this is not done, the events will still run. However, it will be during the call of `.UseSystemWebAdapters()`.

### HTTP Module pooling

Because modules and applications in ASP.NET Framework were assigned to a request, a new instance is needed for each request. However, since they can be expensive to create, they are pooled using [ObjectPool\<T>](/en-us/dotnet/api/microsoft.extensions.objectpool.objectpool-1). In order to customize the actual lifetime of the [HttpApplication](/en-us/dotnet/api/system.web.httpapplication) instances, a custom pool can be used:

```
builder.Services.TryAddSingleton<ObjectPool<HttpApplication>>(sp =>
{
    // Recommended to use the in-built policy as that will ensure everything is initialized correctly and is not intended to be replaced
    var policy = sp.GetRequiredService<IPooledObjectPolicy<HttpApplication>>();

    // Can use any provider needed
    var provider = new DefaultObjectPoolProvider();

    // Use the provider to create a custom pool that will then be used for the application.
    return provider.Create(policy);
});
```

## Related content

* [HTTP Handlers and HTTP Modules Overview](/en-us/iis/configuration/system.webserver/)
* [Configuration](../../../fundamentals/configuration/?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/javascript-interoperability
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/performance/javascript-interoperability.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor JavaScript interoperability (JS interop) performance best practices

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Calls between .NET and JavaScript require additional overhead because:

* Calls are asynchronous.
* Parameters and return values are JSON-serialized to provide an easy-to-understand conversion mechanism between .NET and JavaScript types.

Additionally for server-side Blazor apps, these calls are passed across the network.

## Avoid excessively fine-grained calls

Since each call involves some overhead, it can be valuable to reduce the number of calls. Consider the following code, which stores a collection of items in the browser's [`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage):

```
private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
    foreach (var item in items)
    {
        await JS.InvokeVoidAsync("localStorage.setItem", item.Id, 
            JsonSerializer.Serialize(item));
    }
}
```

The preceding example makes a separate JS interop call for each item. Instead, the following approach reduces the JS interop to a single call:

```
private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
    await JS.InvokeVoidAsync("storeAllInLocalStorage", items);
}
```

The corresponding JavaScript function stores the whole collection of items on the client:

```
function storeAllInLocalStorage(items) {
  items.forEach(item => {
    localStorage.setItem(item.id, JSON.stringify(item));
  });
}
```

For Blazor WebAssembly apps, rolling individual JS interop calls into a single call usually only improves performance significantly if the component makes a large number of JS interop calls.

## Consider the use of synchronous calls

### Call JavaScript from .NET

*This section only applies to client-side components.*

JS interop calls are asynchronous, regardless of whether the called code is synchronous or asynchronous. Calls are asynchronous to ensure that components are compatible across server-side and client-side render modes. On the server, all JS interop calls must be asynchronous because they're sent over a network connection.

If you know for certain that your component only runs on WebAssembly, you can choose to make synchronous JS interop calls. This has slightly less overhead than making asynchronous calls and can result in fewer render cycles because there's no intermediate state while awaiting results.

To make a synchronous call from .NET to JavaScript in a client-side component, cast [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) to [IJSInProcessRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime) to make the JS interop call:

```
@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}
```

When working with [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) in .NET 5 or later client-side components, you can use [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) synchronously instead. [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) implements [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable)/[IDisposable](/en-us/dotnet/api/system.idisposable) and should be disposed for garbage collection to prevent a memory leak, as the following example demonstrates:

```
@inject IJSRuntime JS
@implements IDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var jsInProcess = (IJSInProcessRuntime)JS;
            module = await jsInProcess.Invoke<IJSInProcessObjectReference>("import", 
                "./scripts.js");
            var value = module.Invoke<string>("javascriptFunctionIdentifier");
        }
    }

    ...

    void IDisposable.Dispose()
    {
        if (module is not null)
        {
            await module.Dispose();
        }
    }
}
```

In the preceding example, a [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) isn't trapped during module disposal because there's no Blazor-SignalR circuit in a Blazor WebAssembly app to lose. For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](../javascript-interoperability/?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit).

### Call .NET from JavaScript

*This section only applies to client-side components.*

JS interop calls are asynchronous, regardless of whether the called code is synchronous or asynchronous. Calls are asynchronous to ensure that components are compatible across server-side and client-side render modes. On the server, all JS interop calls must be asynchronous because they're sent over a network connection.

If you know for certain that your component only runs on WebAssembly, you can choose to make synchronous JS interop calls. This has slightly less overhead than making asynchronous calls and can result in fewer render cycles because there's no intermediate state while awaiting results.

To make a synchronous call from .NET to JavaScript in a client-side component, cast [IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) to [IJSInProcessRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessruntime) to make the JS interop call:

```
@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}
```

When working with [IJSObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsobjectreference) in .NET 5 or later client-side components, you can use [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) synchronously instead. [IJSInProcessObjectReference](/en-us/dotnet/api/microsoft.jsinterop.ijsinprocessobjectreference) implements [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable)/[IDisposable](/en-us/dotnet/api/system.idisposable) and should be disposed for garbage collection to prevent a memory leak, as the following example demonstrates:

```
@inject IJSRuntime JS
@implements IDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var jsInProcess = (IJSInProcessRuntime)JS;
            module = await jsInProcess.Invoke<IJSInProcessObjectReference>("import", 
                "./scripts.js");
            var value = module.Invoke<string>("javascriptFunctionIdentifier");
        }
    }

    ...

    void IDisposable.Dispose()
    {
        if (module is not null)
        {
            await module.Dispose();
        }
    }
}
```

In the preceding example, a [JSDisconnectedException](/en-us/dotnet/api/microsoft.jsinterop.jsdisconnectedexception) isn't trapped during module disposal because there's no Blazor-SignalR circuit in a Blazor WebAssembly app to lose. For more information, see [ASP.NET Core Blazor JavaScript interoperability (JS interop)](../javascript-interoperability/?view=aspnetcore-10.0#javascript-interop-calls-without-a-circuit).

## Consider the use of unmarshalled calls

*This section only applies to Blazor WebAssembly apps.*

When running on Blazor WebAssembly, it's possible to make unmarshalled calls from .NET to JavaScript. These are synchronous calls that don't perform JSON serialization of arguments or return values. All aspects of memory management and translations between .NET and JavaScript representations are left up to the developer.

Warning

While using [IJSUnmarshalledRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsunmarshalledruntime) has the least overhead of the JS interop approaches, the JavaScript APIs required to interact with these APIs are currently undocumented and subject to breaking changes in future releases.

```
function jsInteropCall() {
  return BINDING.js_to_mono_obj("Hello world");
}
```

```
@inject IJSRuntime JS

@code {
    protected override void OnInitialized()
    {
        var unmarshalledJs = (IJSUnmarshalledRuntime)JS;
        var value = unmarshalledJs.InvokeUnmarshalled<string>("jsInteropCall");
    }
}
```

## Use JavaScript `[JSImport]`/`[JSExport]` interop

JavaScript `[JSImport]`/`[JSExport]` interop for Blazor WebAssembly apps offers improved performance and stability over the JS interop API in framework releases prior to ASP.NET Core in .NET 7.

For more information, see [JavaScript JSImport/JSExport interop with ASP.NET Core Blazor](../javascript-interoperability/import-export-interop?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/integration
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/integration.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Integrate ASP.NET Core Razor components with MVC or Razor Pages

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Razor components can be integrated into Razor Pages or MVC apps. When the page or view is rendered, components can be prerendered at the same time.

Important

Framework changes across ASP.NET Core releases led to different sets of instructions in this article. Before using this article's guidance, confirm that the document version selector at the top of this article matches the version of ASP.NET Core that you intend to use for your app.

Prerendering can improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines can use to calculate page rank.

After [configuring the project](#configuration), use the guidance in the following sections depending on the project's requirements:

* For components that are directly routable from user requests. Follow this guidance when visitors should be able to make an HTTP request in their browser for a component with an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive.

  * [Use routable components in a Razor Pages app](#use-routable-components-in-a-razor-pages-app)
  * [Use routable components in an MVC app](#use-routable-components-in-an-mvc-app)

* For components that aren't directly routable from user requests, see the [Render components from a page or view](#render-components-from-a-page-or-view) section. Follow this guidance when the app embeds components into existing pages or views with the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

## Configuration

Use the following guidance to integrate Razor components into pages or views of an existing Razor Pages or MVC app.

1. Add an imports file to the root folder of the project with the following content. Change the `{APP NAMESPACE}` placeholder to the namespace of the project.

   `_Imports.razor`:

   ```
   @using System.Net.Http
   @using Microsoft.AspNetCore.Authorization
   @using Microsoft.AspNetCore.Components.Authorization
   @using Microsoft.AspNetCore.Components.Forms
   @using Microsoft.AspNetCore.Components.Routing
   @using Microsoft.AspNetCore.Components.Web
   @using Microsoft.AspNetCore.Components.Web.Virtualization
   @using Microsoft.JSInterop
   @using {APP NAMESPACE}
   ```

2. In the project's layout file (`Pages/Shared/_Layout.cshtml` in Razor Pages apps or `Views/Shared/_Layout.cshtml` in MVC apps):

   * Add the following `<base>` tag and [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component Tag Helper to the `<head>` element:

     ```
     <base href="~/" />
     <component type="typeof(Microsoft.AspNetCore.Components.Web.HeadOutlet)" 
         render-mode="ServerPrerendered" />
     ```

     The `href` value (the *app base path*) in the preceding example assumes that the app resides at the root URL path (`/`). If the app is a sub-application, see [ASP.NET Core Blazor app base path](../host-and-deploy/app-base-path?view=aspnetcore-10.0).

     The [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is used to render head (`<head>`) content for page titles ([PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component) and other head elements ([HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component) set by Razor components. For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-10.0).

   * Add a `<script>` tag for the `blazor.server.js` script immediately before the `Scripts` render section (`@await RenderSectionAsync(...)`):

     ```
     <script src="_framework/blazor.server.js"></script>
     ```

     The framework adds the `blazor.server.js` script to the app. There's no need to manually add a `blazor.server.js` script file to the app.

   Note

   Typically, the layout loads via a `_ViewStart.cshtml` file.

3. Register the Blazor Server services in `Program.cs` where services are registered:

   ```
   builder.Services.AddServerSideBlazor();
   ```

4. Add the Blazor Hub endpoint to the endpoints of `Program.cs` where routes are mapped. Place the following line after the call to `MapRazorPages` (Razor Pages) or `MapControllerRoute` (MVC):

   ```
   app.MapBlazorHub();
   ```

5. Integrate components into any page or view. For example, add a `Counter` component to the project's `Shared` folder.

   `Pages/Shared/Counter.razor` (Razor Pages) or `Views/Shared/Counter.razor` (MVC):

   ```
   <h1>Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

   **Razor Pages**:

   In the project's `Index` page of a Razor Pages app, add the `Counter` component's namespace and embed the component into the page. When the `Index` page loads, the `Counter` component is prerendered in the page. In the following example, replace the `{APP NAMESPACE}` placeholder with the project's namespace.

   `Pages/Index.cshtml`:

   ```
   @page
   @using {APP NAMESPACE}.Pages.Shared
   @model IndexModel
   @{
       ViewData["Title"] = "Home page";
   }

   <component type="typeof(Counter)" render-mode="ServerPrerendered" />
   ```

   **MVC**:

   In the project's `Index` view of an MVC app, add the `Counter` component's namespace and embed the component into the view. When the `Index` view loads, the `Counter` component is prerendered in the page. In the following example, replace the `{APP NAMESPACE}` placeholder with the project's namespace.

   `Views/Home/Index.cshtml`:

   ```
   @using {APP NAMESPACE}.Views.Shared
   @{
       ViewData["Title"] = "Home Page";
   }

   <component type="typeof(Counter)" render-mode="ServerPrerendered" />
   ```

For more information, see the [Render components from a page or view](#render-components-from-a-page-or-view) section.

## Use routable components in a Razor Pages app

*This section pertains to adding components that are directly routable from user requests.*

To support routable Razor components in Razor Pages apps:

1. Follow the guidance in the [Configuration](#configuration) section.

2. Add an `App` component to the project root with the following content.

   `App.razor`:

   ```
   @using Microsoft.AspNetCore.Components.Routing

   <Router AppAssembly="typeof(App).Assembly">
       <Found Context="routeData">
           <RouteView RouteData="routeData" />
       </Found>
       <NotFound>
           <PageTitle>Not found</PageTitle>
           <p role="alert">Sorry, there's nothing at this address.</p>
       </NotFound>
   </Router>
   ```

3. Add a `_Host` page to the project with the following content. Replace the `{APP NAMESPACE}` placeholder with the app's namespace.

   `Pages/_Host.cshtml`:

   ```
   @page
   @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

   <component type="typeof(App)" render-mode="ServerPrerendered" />
   ```

   Note

   The preceding example assumes that the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component and Blazor script (`_framework/blazor.server.js`) are rendered by the app's layout. For more information, see the [Configuration](#configuration) section.

   In .NET 10 or later, the Blazor script is included by the framework if the project contains at least one Razor component file (`.razor`). If your app requires the Blazor script but doesn't contain at least one component, add the following MSBuild property to the app's project file to force unconditional script inclusion:

   ```
   <RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>
   ```

   [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the `App` component:

   * Is prerendered into the page.
   * Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

   For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

4. In the `Program.cs` endpoints, add a low-priority route for the `_Host` page as the last endpoint:

   ```
   app.MapFallbackToPage("/_Host");
   ```

5. Add routable components to the project. The following example is a `RoutableCounter` component based on the `Counter` component in the Blazor project templates.

   `Pages/RoutableCounter.razor`:

   ```
   @page "/routable-counter"

   <PageTitle>Routable Counter</PageTitle>

   <h1>Routable Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

6. Run the project and navigate to the routable `RoutableCounter` component at `/routable-counter`.

For more information on namespaces, see the [Component namespaces](#component-namespaces) section.

## Use routable components in an MVC app

*This section pertains to adding components that are directly routable from user requests.*

To support routable Razor components in MVC apps:

1. Follow the guidance in the [Configuration](#configuration) section.

2. Add an `App` component to the project root with the following content.

   `App.razor`:

   ```
   @using Microsoft.AspNetCore.Components.Routing

   <Router AppAssembly="typeof(App).Assembly">
       <Found Context="routeData">
           <RouteView RouteData="routeData" />
       </Found>
       <NotFound>
           <PageTitle>Not found</PageTitle>
           <p role="alert">Sorry, there's nothing at this address.</p>
       </NotFound>
   </Router>
   ```

3. Add a `_Host` view to the project with the following content. Replace the `{APP NAMESPACE}` placeholder with the app's namespace.

   `Views/Home/_Host.cshtml`:

   ```
   @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

   <component type="typeof(App)" render-mode="ServerPrerendered" />
   ```

   Note

   The preceding example assumes that the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component and Blazor script (`_framework/blazor.server.js`) are rendered by the app's layout. For more information, see the [Configuration](#configuration) section.

   In .NET 10 or later, the Blazor script is included by the framework if the project contains at least one Razor component file (`.razor`). If your app requires the Blazor script but doesn't contain at least one component, add the following MSBuild property to the app's project file to force unconditional script inclusion:

   ```
   <RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>
   ```

   [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the `App` component:

   * Is prerendered into the page.
   * Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

   For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

4. Add an action to the Home controller.

   `Controllers/HomeController.cs`:

   ```
   public IActionResult Blazor()
   {
      return View("_Host");
   }
   ```

5. In the `Program.cs` endpoints, add a low-priority route for the controller action that returns the `_Host` view:

   ```
   app.MapFallbackToController("Blazor", "Home");
   ```

6. Create a `Pages` folder in the MVC app and add routable components. The following example is a `RoutableCounter` component based on the `Counter` component in the Blazor project templates.

   `Pages/RoutableCounter.razor`:

   ```
   @page "/routable-counter"

   <PageTitle>Routable Counter</PageTitle>

   <h1>Routable Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

7. Run the project and navigate to the routable `RoutableCounter` component at `/routable-counter`.

For more information on namespaces, see the [Component namespaces](#component-namespaces) section.

## Render components from a page or view

*This section pertains to adding components to pages or views, where the components aren't directly routable from user requests.*

To render a component from a page or view, use the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

### Render stateful interactive components

Stateful interactive components can be added to a Razor page or view.

When the page or view renders:

* The component is prerendered with the page or view.
* The initial component state used for prerendering is lost.
* New component state is created when the SignalR connection is established.

The following Razor page renders a `Counter` component:

```
<h1>Razor Page</h1>

<component type="typeof(Counter)" render-mode="ServerPrerendered" 
    param-InitialValue="InitialValue" />

@functions {
    [BindProperty(SupportsGet=true)]
    public int InitialValue { get; set; }
}
```

For more information, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

### Render noninteractive components

In the following Razor page, the `Counter` component is statically rendered with an initial value that's specified using a form. Since the component is statically rendered, the component isn't interactive:

```
<h1>Razor Page</h1>

<form>
    <input type="number" asp-for="InitialValue" />
    <button type="submit">Set initial value</button>
</form>

<component type="typeof(Counter)" render-mode="Static" 
    param-InitialValue="InitialValue" />

@functions {
    [BindProperty(SupportsGet=true)]
    public int InitialValue { get; set; }
}
```

For more information, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

## Component namespaces

When using a custom folder to hold the project's Razor components, add the namespace representing the folder to either the page/view or to the `_ViewImports.cshtml` file. In the following example:

* Components are stored in the `Components` folder of the project.
* The `{APP NAMESPACE}` placeholder is the project's namespace. `Components` represents the name of the folder.

```
@using {APP NAMESPACE}.Components
```

The `_ViewImports.cshtml` file is located in the `Pages` folder of a Razor Pages app or the `Views` folder of an MVC app.

For more information, see [ASP.NET Core Razor components](./?view=aspnetcore-10.0#class-name-and-namespace).

## Persist prerendered state

Without persisting prerendered state, state used during prerendering is lost and must be recreated when the app is fully loaded. If any state is setup asynchronously, the UI may flicker as the prerendered UI is replaced with temporary placeholders and then fully rendered again.

To persist state for prerendered components, use the [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-10.0) ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs)). Add the Tag Helper's tag, `<persist-component-state />`, inside the closing `</body>` tag of the `_Host` page in an app that prerenders components.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

In `Pages/_Host.cshtml` of Blazor apps that are `ServerPrerendered` in a Blazor Server app:

```
<body>
    ...

    <persist-component-state />
</body>
```

Decide what state to persist using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service. The [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute) applied to a `public` property registers a callback to persist the state during prerendering and loads it when the component renders interactively or the service is instantiated.

In the following example, the `{TYPE}` placeholder represents the type of data to persist (for example, `WeatherForecast[]`).

```
@code {
    [PersistentState]
    public {TYPE} Data { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Data ??= await ...;
    }
}
```

In the following example, the `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-10.0) persists the component state after all component invocations.

`WeatherForecastPreserveState.razor`:

```
@page "/weather-forecast-preserve-state"
@using BlazorSample.Shared
@inject IWeatherForecastService WeatherForecastService

<PageTitle>Weather Forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (Forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in Forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    [PersistentState]
    public WeatherForecast[]? Forecasts { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Forecasts ??= await WeatherForecastService.GetForecastAsync(
            DateOnly.FromDateTime(DateTime.Now));
    }
}
```

Decide what state to persist using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service. [PersistentComponentState.RegisterOnPersisting](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) registers a callback to persist the component state before the app is paused. The state is retrieved when the application resumes. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown.

In the following example:

* The `{TYPE}` placeholder represents the type of data to persist (for example, `WeatherForecast[]`).
* The `{TOKEN}` placeholder is a state identifier string (for example, `fetchdata`).

```
@implements IDisposable
@inject PersistentComponentState ApplicationState

...

@code {
    private {TYPE} data;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<{TYPE}>(
            "{TOKEN}", out var restored))
        {
            data = await ...;
        }
        else
        {
            data = restored!;
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson("{TOKEN}", data);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}
```

In the following example, the `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-10.0) persists the component state after all component invocations.

`Pages/WeatherForecastPreserveState.razor`:

```
@page "/weather-forecast-preserve-state"
@using BlazorSample.Shared
@implements IDisposable
@inject IWeatherForecastService WeatherForecastService
@inject PersistentComponentState ApplicationState

<PageTitle>Weather Forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts = Array.Empty<WeatherForecast>();
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<WeatherForecast[]>(
            nameof(forecasts), out var restored))
        {
            forecasts = 
                await WeatherForecastService.GetForecastAsync(
                    DateOnly.FromDateTime(DateTime.Now));
        }
        else
        {
            forecasts = restored!;
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson(nameof(forecasts), forecasts);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}
```

By initializing components with the same state used during prerendering, any expensive initialization steps are only executed once. The rendered UI also matches the prerendered UI, so no flicker occurs in the browser.

The persisted prerendered state is transferred to the client, where it's used to restore the component state. [ASP.NET Core Data Protection](../../security/data-protection/introduction?view=aspnetcore-10.0) ensures that the data is transferred securely in Blazor Server apps.

## Prerendered state size and SignalR message size limit

A large prerendered state size may exceed Blazor's SignalR circuit message size limit, which results in the following:

* The SignalR circuit fails to initialize with an error on the client: Circuit host not initialized.
* The reconnection UI on the client appears when the circuit fails. Recovery isn't possible.

To resolve the problem, use ***either*** of the following approaches:

* Reduce the amount of data that you are putting into the prerendered state.
* Increase the [SignalR message size limit](../fundamentals/signalr?view=aspnetcore-10.0#server-side-circuit-handler-options). ***WARNING***: Increasing the limit may increase the risk of Denial of Service (DoS) attacks.

## Additional Blazor Server resources

* [State management: Protected browser storage: Handle prerendering](../state-management/protected-browser-storage?view=aspnetcore-10.0#handle-prerendering)

* Razor component lifecycle subjects that pertain to prerendering

  * [Component initialization (`OnInitialized{Async}`)](lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync)
  * [After component render (`OnAfterRender{Async}`)](lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync)
  * [Stateful reconnection after prerendering](lifecycle?view=aspnetcore-10.0#stateful-reconnection-after-prerendering)
  * [Prerendering with JavaScript interop](lifecycle?view=aspnetcore-10.0#prerendering-with-javascript-interop)

* [Authentication and authorization: General aspects](../security/?view=aspnetcore-10.0#aspnet-core-blazor-authentication-and-authorization)

* [Handle Errors: Prerendering](../fundamentals/handle-errors?view=aspnetcore-10.0#prerendering)

* [Host and deploy ASP.NET Core server-side Blazor apps](../host-and-deploy/server/?view=aspnetcore-10.0)

* [Threat mitigation: Cross-site scripting (XSS)](../security/interactive-server-side-rendering?view=aspnetcore-10.0#cross-site-scripting-xss)

* [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) is executed *twice* when prerendering: [Handle asynchronous navigation events with `OnNavigateAsync`](../fundamentals/routing?view=aspnetcore-10.0#handle-asynchronous-navigation-events-with-onnavigateasync)

Prerendering can improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines can use to calculate page rank.

After [configuring the project](#configuration), use the guidance in the following sections depending on the project's requirements:

* Routable components: For components that are directly routable from user requests. Follow this guidance when visitors should be able to make an HTTP request in their browser for a component with an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive.

  * [Use routable components in a Razor Pages app](#use-routable-components-in-a-razor-pages-app)
  * [Use routable components in an MVC app](#use-routable-components-in-an-mvc-app)

* [Render components from a page or view](#render-components-from-a-page-or-view): For components that aren't directly routable from user requests. Follow this guidance when the app embeds components into existing pages or views with the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

## Configuration

Use the following guidance to integrate Razor components into pages or views of an existing Razor Pages or MVC app.

Important

The use of a layout page (`_Layout.cshtml`) with a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) for a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is required to control `<head>` content, such as the page's title ([PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component) and other head elements ([HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component). For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-10.0#control-head-content-during-prerendering).

1. In the project's layout file:

   * Add the following `<base>` tag and [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component Tag Helper to the `<head>` element in `Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC):

     ```
     <base href="~/" />
     <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
     ```

     The `href` value (the *app base path*) in the preceding example assumes that the app resides at the root URL path (`/`). If the app is a sub-application, see [ASP.NET Core Blazor app base path](../host-and-deploy/app-base-path?view=aspnetcore-10.0).

     The [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is used to render head (`<head>`) content for page titles ([PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component) and other head elements ([HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component) set by Razor components. For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-10.0).

   * Add a `<script>` tag for the `blazor.server.js` script immediately before the `Scripts` render section (`@await RenderSectionAsync(...)`) in the app's layout.

     `Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC):

     ```
     <script src="_framework/blazor.server.js"></script>
     ```

     The framework adds the `blazor.server.js` script to the app. There's no need to manually add a `blazor.server.js` script file to the app.

2. Add an imports file to the root folder of the project with the following content. Change the `{APP NAMESPACE}` placeholder to the namespace of the project.

   `_Imports.razor`:

   ```
   @using System.Net.Http
   @using Microsoft.AspNetCore.Authorization
   @using Microsoft.AspNetCore.Components.Authorization
   @using Microsoft.AspNetCore.Components.Forms
   @using Microsoft.AspNetCore.Components.Routing
   @using Microsoft.AspNetCore.Components.Web
   @using Microsoft.AspNetCore.Components.Web.Virtualization
   @using Microsoft.JSInterop
   @using {APP NAMESPACE}
   ```

3. Register the Blazor Server services in `Program.cs` where services are registered:

   ```
   builder.Services.AddServerSideBlazor();
   ```

4. Add the Blazor Hub endpoint to the endpoints of `Program.cs` where routes are mapped.

   Place the following line after the call to `MapRazorPages` (Razor Pages) or `MapControllerRoute` (MVC):

   ```
   app.MapBlazorHub();
   ```

5. Integrate components into any page or view. For example, add a `Counter` component to the project's `Shared` folder.

   `Pages/Shared/Counter.razor` (Razor Pages) or `Views/Shared/Counter.razor` (MVC):

   ```
   <h1>Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

   **Razor Pages**:

   In the project's `Index` page of a Razor Pages app, add the `Counter` component's namespace and embed the component into the page. When the `Index` page loads, the `Counter` component is prerendered in the page. In the following example, replace the `{APP NAMESPACE}` placeholder with the project's namespace.

   `Pages/Index.cshtml`:

   ```
   @page
   @using {APP NAMESPACE}.Pages.Shared
   @model IndexModel
   @{
       ViewData["Title"] = "Home page";
   }

   <component type="typeof(Counter)" render-mode="ServerPrerendered" />
   ```

   **MVC**:

   In the project's `Index` view of an MVC app, add the `Counter` component's namespace and embed the component into the view. When the `Index` view loads, the `Counter` component is prerendered in the page. In the following example, replace the `{APP NAMESPACE}` placeholder with the project's namespace.

   `Views/Home/Index.cshtml`:

   ```
   @using {APP NAMESPACE}.Views.Shared
   @{
       ViewData["Title"] = "Home Page";
   }

   <component type="typeof(Counter)" render-mode="ServerPrerendered" />
   ```

For more information, see the [Render components from a page or view](#render-components-from-a-page-or-view) section.

## Use routable components in a Razor Pages app

*This section pertains to adding components that are directly routable from user requests.*

To support routable Razor components in Razor Pages apps:

1. Follow the guidance in the [Configuration](#configuration) section.

2. Add an `App` component to the project root with the following content.

   `App.razor`:

   ```
   @using Microsoft.AspNetCore.Components.Routing

   <Router AppAssembly="typeof(App).Assembly">
       <Found Context="routeData">
           <RouteView RouteData="routeData" />
       </Found>
       <NotFound>
           <PageTitle>Not found</PageTitle>
           <p role="alert">Sorry, there's nothing at this address.</p>
       </NotFound>
   </Router>
   ```

3. Add a `_Host` page to the project with the following content.

   `Pages/_Host.cshtml`:

   ```
   @page "/blazor"
   @namespace {APP NAMESPACE}.Pages.Shared
   @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
   @{
       Layout = "_Layout";
   }

   <component type="typeof(App)" render-mode="ServerPrerendered" />
   ```

   In this scenario, components use the shared `_Layout.cshtml` file for their layout.

   Important

   The use of a layout page (`_Layout.cshtml`) with a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) for a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is required to control `<head>` content, such as the page's title ([PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component) and other head elements ([HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component). For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-10.0#control-head-content-during-prerendering).

   [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the `App` component:

   * Is prerendered into the page.
   * Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

   For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

4. In the `Program.cs` endpoints, add a low-priority route for the `_Host` page as the last endpoint:

   ```
   app.MapFallbackToPage("/_Host");
   ```

5. Add routable components to the project. The following example is a `RoutableCounter` component based on the `Counter` component in the Blazor project templates.

   `Pages/RoutableCounter.razor`:

   ```
   @page "/routable-counter"

   <PageTitle>Routable Counter</PageTitle>

   <h1>Routable Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

6. Run the project and navigate to the routable `RoutableCounter` component at `/routable-counter`.

For more information on namespaces, see the [Component namespaces](#component-namespaces) section.

## Use routable components in an MVC app

*This section pertains to adding components that are directly routable from user requests.*

To support routable Razor components in MVC apps:

1. Follow the guidance in the [Configuration](#configuration) section.

2. Add an `App` component to the project root with the following content.

   `App.razor`:

   ```
   @using Microsoft.AspNetCore.Components.Routing

   <Router AppAssembly="typeof(App).Assembly">
       <Found Context="routeData">
           <RouteView RouteData="routeData" />
       </Found>
       <NotFound>
           <PageTitle>Not found</PageTitle>
           <p role="alert">Sorry, there's nothing at this address.</p>
       </NotFound>
   </Router>
   ```

3. Add a `_Host` view to the project with the following content.

   `Views/Home/_Host.cshtml`:

   ```
   @namespace {APP NAMESPACE}.Views.Shared
   @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
   @{
       Layout = "_Layout";
   }

   <component type="typeof(App)" render-mode="ServerPrerendered" />
   ```

   Components use the shared `_Layout.cshtml` file for their layout.

   Important

   The use of a layout page (`_Layout.cshtml`) with a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) for a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is required to control `<head>` content, such as the page's title ([PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component) and other head elements ([HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component). For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-10.0#control-head-content-during-prerendering).

   [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the `App` component:

   * Is prerendered into the page.
   * Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

   For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

4. Add an action to the Home controller.

   `Controllers/HomeController.cs`:

   ```
   public IActionResult Blazor()
   {
      return View("_Host");
   }
   ```

5. In the `Program.cs` endpoints, add a low-priority route for the controller action that returns the `_Host` view:

   ```
   app.MapFallbackToController("Blazor", "Home");
   ```

6. Create a `Pages` folder in the MVC app and add routable components. The following example is a `RoutableCounter` component based on the `Counter` component in the Blazor project templates.

   `Pages/RoutableCounter.razor`:

   ```
   @page "/routable-counter"

   <PageTitle>Routable Counter</PageTitle>

   <h1>Routable Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

7. Run the project and navigate to the routable `RoutableCounter` component at `/routable-counter`.

For more information on namespaces, see the [Component namespaces](#component-namespaces) section.

## Render components from a page or view

*This section pertains to adding components to pages or views, where the components aren't directly routable from user requests.*

To render a component from a page or view, use the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

### Render stateful interactive components

Stateful interactive components can be added to a Razor page or view.

When the page or view renders:

* The component is prerendered with the page or view.
* The initial component state used for prerendering is lost.
* New component state is created when the SignalR connection is established.

The following Razor page renders a `Counter` component:

```
<h1>Razor Page</h1>

<component type="typeof(Counter)" render-mode="ServerPrerendered" 
    param-InitialValue="InitialValue" />

@functions {
    [BindProperty(SupportsGet=true)]
    public int InitialValue { get; set; }
}
```

For more information, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

Important

The use of a layout page (`_Layout.cshtml`) with a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) for a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is required to control `<head>` content, such as the page's title ([PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component) and other head elements ([HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component). For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-10.0#control-head-content-during-prerendering).

### Render noninteractive components

In the following Razor page, the `Counter` component is statically rendered with an initial value that's specified using a form. Since the component is statically rendered, the component isn't interactive:

```
<h1>Razor Page</h1>

<form>
    <input type="number" asp-for="InitialValue" />
    <button type="submit">Set initial value</button>
</form>

<component type="typeof(Counter)" render-mode="Static" 
    param-InitialValue="InitialValue" />

@functions {
    [BindProperty(SupportsGet=true)]
    public int InitialValue { get; set; }
}
```

For more information, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

Important

The use of a layout page (`_Layout.cshtml`) with a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) for a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is required to control `<head>` content, such as the page's title ([PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component) and other head elements ([HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component). For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-10.0#control-head-content-during-prerendering).

## Component namespaces

When using a custom folder to hold the project's Razor components, add the namespace representing the folder to either the page/view or to the `_ViewImports.cshtml` file. In the following example:

* Components are stored in the `Components` folder of the project.
* The `{APP NAMESPACE}` placeholder is the project's namespace. `Components` represents the name of the folder.

```
@using {APP NAMESPACE}.Components
```

The `_ViewImports.cshtml` file is located in the `Pages` folder of a Razor Pages app or the `Views` folder of an MVC app.

For more information, see [ASP.NET Core Razor components](./?view=aspnetcore-10.0#class-name-and-namespace).

## Persist prerendered state

Without persisting prerendered state, state used during prerendering is lost and must be recreated when the app is fully loaded. If any state is setup asynchronously, the UI may flicker as the prerendered UI is replaced with temporary placeholders and then fully rendered again.

To solve these problems, Blazor supports persisting state in a prerendered page using the [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-10.0). Add the Tag Helper's tag, `<persist-component-state />`, inside the closing `</body>` tag.

`Pages/_Layout.cshtml`:

```
<body>
    ...

    <persist-component-state />
</body>
```

Decide what state to persist using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service. [PersistentComponentState.RegisterOnPersisting](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate.registeronpersisting) registers a callback to persist the component state before the app is paused. The state is retrieved when the application resumes. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown.

In the following example, the `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](../../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-10.0) persists the component state after all component invocations.

`Pages/WeatherForecastPreserveState.razor`:

```
@page "/weather-forecast-preserve-state"
@implements IDisposable
@using BlazorSample.Shared
@inject IWeatherForecastService WeatherForecastService
@inject PersistentComponentState ApplicationState

<PageTitle>Weather Forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts = Array.Empty<WeatherForecast>();
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<WeatherForecast[]>(
            nameof(forecasts), out var restored))
        {
            forecasts = 
                await WeatherForecastService.GetForecastAsync(DateTime.Now);
        }
        else
        {
            forecasts = restored!;
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson(nameof(forecasts), forecasts);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}
```

By initializing components with the same state used during prerendering, any expensive initialization steps are only executed once. The rendered UI also matches the prerendered UI, so no flicker occurs in the browser.

The persisted prerendered state is transferred to the client, where it's used to restore the component state. [ASP.NET Core Data Protection](../../security/data-protection/introduction?view=aspnetcore-10.0) ensures that the data is transferred securely in Blazor Server apps.

## Prerendered state size and SignalR message size limit

A large prerendered state size may exceed Blazor's SignalR circuit message size limit, which results in the following:

* The SignalR circuit fails to initialize with an error on the client: Circuit host not initialized.
* The reconnection UI on the client appears when the circuit fails. Recovery isn't possible.

To resolve the problem, use ***either*** of the following approaches:

* Reduce the amount of data that you are putting into the prerendered state.
* Increase the [SignalR message size limit](../fundamentals/signalr?view=aspnetcore-10.0#server-side-circuit-handler-options). ***WARNING***: Increasing the limit may increase the risk of Denial of Service (DoS) attacks.

## Additional Blazor Server resources

* [State management: Protected browser storage: Handle prerendering](../state-management/protected-browser-storage?view=aspnetcore-10.0#handle-prerendering)

* Razor component lifecycle subjects that pertain to prerendering

  * [Component initialization (`OnInitialized{Async}`)](lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync)
  * [After component render (`OnAfterRender{Async}`)](lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync)
  * [Stateful reconnection after prerendering](lifecycle?view=aspnetcore-10.0#stateful-reconnection-after-prerendering)
  * [Prerendering with JavaScript interop](lifecycle?view=aspnetcore-10.0#prerendering-with-javascript-interop)

* [Authentication and authorization: General aspects](../security/?view=aspnetcore-10.0#aspnet-core-blazor-authentication-and-authorization)

* [Handle Errors: Prerendering](../fundamentals/handle-errors?view=aspnetcore-10.0#prerendering)

* [Host and deploy ASP.NET Core server-side Blazor apps](../host-and-deploy/server/?view=aspnetcore-10.0)

* [Threat mitigation: Cross-site scripting (XSS)](../security/interactive-server-side-rendering?view=aspnetcore-10.0#cross-site-scripting-xss)

Prerendering can improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines can use to calculate page rank.

After [configuring the project](#configuration), use the guidance in the following sections depending on the project's requirements:

* Routable components: For components that are directly routable from user requests. Follow this guidance when visitors should be able to make an HTTP request in their browser for a component with an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive.

  * [Use routable components in a Razor Pages app](#use-routable-components-in-a-razor-pages-app)
  * [Use routable components in an MVC app](#use-routable-components-in-an-mvc-app)

* [Render components from a page or view](#render-components-from-a-page-or-view): For components that aren't directly routable from user requests. Follow this guidance when the app embeds components into existing pages or views with the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

## Configuration

An existing Razor Pages or MVC app can integrate Razor components into pages or views:

1. In the project's layout file:

   * Add the following `<base>` tag to the `<head>` element in `Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC):

     ```
     <base href="~/" />
     ```

     The `href` value (the *app base path*) in the preceding example assumes that the app resides at the root URL path (`/`). If the app is a sub-application, see [ASP.NET Core Blazor app base path](../host-and-deploy/app-base-path?view=aspnetcore-10.0).

   * Add a `<script>` tag for the `blazor.server.js` script immediately before the `Scripts` render section.

     `Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC):

     ```
         ...
         <script src="_framework/blazor.server.js"></script>

         @await RenderSectionAsync("Scripts", required: false)
     </body>
     ```

     The framework adds the `blazor.server.js` script to the app. There's no need to manually add a `blazor.server.js` script file to the app.

2. Add an imports file to the root folder of the project with the following content. Change the `{APP NAMESPACE}` placeholder to the namespace of the project.

   `_Imports.razor`:

   ```
   @using System.Net.Http
   @using Microsoft.AspNetCore.Authorization
   @using Microsoft.AspNetCore.Components.Authorization
   @using Microsoft.AspNetCore.Components.Forms
   @using Microsoft.AspNetCore.Components.Routing
   @using Microsoft.AspNetCore.Components.Web
   @using Microsoft.JSInterop
   @using {APP NAMESPACE}
   ```

3. Register the Blazor Server service in `Startup.ConfigureServices`.

   In `Startup.cs`:

   ```
   services.AddServerSideBlazor();
   ```

4. Add the Blazor Hub endpoint to the endpoints (`app.UseEndpoints`) of `Startup.Configure`.

   `Startup.cs`:

   ```
   endpoints.MapBlazorHub();
   ```

5. Integrate components into any page or view. For example, add a `Counter` component to the project's `Shared` folder.

   `Pages/Shared/Counter.razor` (Razor Pages) or `Views/Shared/Counter.razor` (MVC):

   ```
   <h1>Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

   **Razor Pages**:

   In the project's `Index` page of a Razor Pages app, add the `Counter` component's namespace and embed the component into the page. When the `Index` page loads, the `Counter` component is prerendered in the page. In the following example, replace the `{APP NAMESPACE}` placeholder with the project's namespace.

   `Pages/Index.cshtml`:

   ```
   @page
   @using {APP NAMESPACE}.Pages.Shared
   @model IndexModel
   @{
       ViewData["Title"] = "Home page";
   }

   <div>
       <component type="typeof(Counter)" render-mode="ServerPrerendered" />
   </div>
   ```

   In the preceding example, replace the `{APP NAMESPACE}` placeholder with the app's namespace.

   **MVC**:

   In the project's `Index` view of an MVC app, add the `Counter` component's namespace and embed the component into the view. When the `Index` view loads, the `Counter` component is prerendered in the page. In the following example, replace the `{APP NAMESPACE}` placeholder with the project's namespace.

   `Views/Home/Index.cshtml`:

   ```
   @using {APP NAMESPACE}.Views.Shared
   @{
       ViewData["Title"] = "Home Page";
   }

   <div>
       <component type="typeof(Counter)" render-mode="ServerPrerendered" />
   </div>
   ```

For more information, see the [Render components from a page or view](#render-components-from-a-page-or-view) section.

## Use routable components in a Razor Pages app

*This section pertains to adding components that are directly routable from user requests.*

To support routable Razor components in Razor Pages apps:

1. Follow the guidance in the [Configuration](#configuration) section.

2. Add an `App` component to the project root with the following content.

   `App.razor`:

   ```
   @using Microsoft.AspNetCore.Components.Routing

   <Router AppAssembly="typeof(Program).Assembly">
       <Found Context="routeData">
           <RouteView RouteData="routeData" />
       </Found>
       <NotFound>
           <h1>Page not found</h1>
           <p>Sorry, but there's nothing here!</p>
       </NotFound>
   </Router>
   ```

   Note

   With the release of .NET 5.0.1 and for any additional 5.x releases, the `Router` component includes the `PreferExactMatches` parameter set to `@true`. For more information, see [Migrate from ASP.NET Core 3.1 to .NET 5](../../migration/31-to-50?view=aspnetcore-10.0#changes-to-blazor-app-routing-logic-in-501-and-further-5x-releases-up-to-60).

3. Add a `_Host` page to the project with the following content.

   `Pages/_Host.cshtml`:

   ```
   @page "/blazor"
   @{
       Layout = "_Layout";
   }

   <app>
       <component type="typeof(App)" render-mode="ServerPrerendered" />
   </app>
   ```

   Components use the shared `_Layout.cshtml` file for their layout.

   [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the `App` component:

   * Is prerendered into the page.
   * Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

   For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

4. In the `Startup.Configure` endpoints of `Startup.cs`, add a low-priority route for the `_Host` page as the last endpoint:

   ```
   endpoints.MapFallbackToPage("/_Host");
   ```

   The following example shows the added line in a typical app's endpoint configuration:

   ```
   app.UseEndpoints(endpoints =>
   {
       endpoints.MapRazorPages();
       endpoints.MapBlazorHub();
       endpoints.MapFallbackToPage("/_Host");
   });
   ```

5. Add routable components to the project.

   `Pages/RoutableCounter.razor`:

   ```
   @page "/routable-counter"

   <h1>Routable Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

6. Run the project and navigate to the routable `RoutableCounter` component at `/routable-counter`.

For more information on namespaces, see the [Component namespaces](#component-namespaces) section.

## Use routable components in an MVC app

*This section pertains to adding components that are directly routable from user requests.*

To support routable Razor components in MVC apps:

1. Follow the guidance in the [Configuration](#configuration) section.

2. Add an `App` component to the project root with the following content.

   `App.razor`:

   ```
   @using Microsoft.AspNetCore.Components.Routing

   <Router AppAssembly="typeof(Program).Assembly">
       <Found Context="routeData">
           <RouteView RouteData="routeData" />
       </Found>
       <NotFound>
           <h1>Page not found</h1>
           <p>Sorry, but there's nothing here!</p>
       </NotFound>
   </Router>
   ```

   Note

   With the release of .NET 5.0.1 and for any additional 5.x releases, the `Router` component includes the `PreferExactMatches` parameter set to `@true`. For more information, see [Migrate from ASP.NET Core 3.1 to .NET 5](../../migration/31-to-50?view=aspnetcore-10.0#changes-to-blazor-app-routing-logic-in-501-and-further-5x-releases-up-to-60).

3. Add a `_Host` view to the project with the following content.

   `Views/Home/_Host.cshtml`:

   ```
   @{
       Layout = "_Layout";
   }

   <app>
       <component type="typeof(App)" render-mode="ServerPrerendered" />
   </app>
   ```

   Components use the shared `_Layout.cshtml` file for their layout.

   [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the `App` component:

   * Is prerendered into the page.
   * Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

   For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

4. Add an action to the Home controller.

   `Controllers/HomeController.cs`:

   ```
   public IActionResult Blazor()
   {
      return View("_Host");
   }
   ```

5. In the `Startup.Configure` endpoints of `Startup.cs`, add a low-priority route for the controller action that returns the `_Host` view:

   ```
   endpoints.MapFallbackToController("Blazor", "Home");
   ```

   The following example shows the added line in a typical app's endpoint configuration:

   ```
   app.UseEndpoints(endpoints =>
   {
       endpoints.MapControllerRoute(
           name: "default",
           pattern: "{controller=Home}/{action=Index}/{id?}");
       endpoints.MapBlazorHub();
       endpoints.MapFallbackToController("Blazor", "Home");
   });
   ```

6. Add routable components to the project.

   `Pages/RoutableCounter.razor`:

   ```
   @page "/routable-counter"

   <h1>Routable Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

7. Run the project and navigate to the routable `RoutableCounter` component at `/routable-counter`.

For more information on namespaces, see the [Component namespaces](#component-namespaces) section.

## Render components from a page or view

*This section pertains to adding components to pages or views, where the components aren't directly routable from user requests.*

To render a component from a page or view, use the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

### Render stateful interactive components

Stateful interactive components can be added to a Razor page or view.

When the page or view renders:

* The component is prerendered with the page or view.
* The initial component state used for prerendering is lost.
* New component state is created when the SignalR connection is established.

The following Razor page renders a `Counter` component:

```
<h1>My Razor Page</h1>

<component type="typeof(Counter)" render-mode="ServerPrerendered" 
    param-InitialValue="InitialValue" />

@functions {
    [BindProperty(SupportsGet=true)]
    public int InitialValue { get; set; }
}
```

For more information, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

### Render noninteractive components

In the following Razor page, the `Counter` component is statically rendered with an initial value that's specified using a form. Since the component is statically rendered, the component isn't interactive:

```
<h1>My Razor Page</h1>

<form>
    <input type="number" asp-for="InitialValue" />
    <button type="submit">Set initial value</button>
</form>

<component type="typeof(Counter)" render-mode="Static" 
    param-InitialValue="InitialValue" />

@functions {
    [BindProperty(SupportsGet=true)]
    public int InitialValue { get; set; }
}
```

For more information, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

## Component namespaces

When using a custom folder to hold the project's Razor components, add the namespace representing the folder to either the page/view or to the `_ViewImports.cshtml` file. In the following example:

* Components are stored in the `Components` folder of the project.
* The `{APP NAMESPACE}` placeholder is the project's namespace. `Components` represents the name of the folder.

```
@using {APP NAMESPACE}.Components
```

The `_ViewImports.cshtml` file is located in the `Pages` folder of a Razor Pages app or the `Views` folder of an MVC app.

For more information, see [ASP.NET Core Razor components](./?view=aspnetcore-10.0#class-name-and-namespace).

## Prerendered state size and SignalR message size limit

A large prerendered state size may exceed Blazor's SignalR circuit message size limit, which results in the following:

* The SignalR circuit fails to initialize with an error on the client: Circuit host not initialized.
* The reconnection UI on the client appears when the circuit fails. Recovery isn't possible.

To resolve the problem, use ***either*** of the following approaches:

* Reduce the amount of data that you are putting into the prerendered state.
* Increase the [SignalR message size limit](../fundamentals/signalr?view=aspnetcore-10.0#server-side-circuit-handler-options). ***WARNING***: Increasing the limit may increase the risk of Denial of Service (DoS) attacks.

## Additional Blazor Server resources

* [State management: Protected browser storage: Handle prerendering](../state-management/protected-browser-storage?view=aspnetcore-10.0#handle-prerendering)

* Razor component lifecycle subjects that pertain to prerendering

  * [Component initialization (`OnInitialized{Async}`)](lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync)
  * [After component render (`OnAfterRender{Async}`)](lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync)
  * [Stateful reconnection after prerendering](lifecycle?view=aspnetcore-10.0#stateful-reconnection-after-prerendering)
  * [Prerendering with JavaScript interop](lifecycle?view=aspnetcore-10.0#prerendering-with-javascript-interop)

* [Authentication and authorization: General aspects](../security/?view=aspnetcore-10.0#aspnet-core-blazor-authentication-and-authorization)

* [Handle Errors: Prerendering](../fundamentals/handle-errors?view=aspnetcore-10.0#prerendering)

* [Host and deploy ASP.NET Core server-side Blazor apps](../host-and-deploy/server/?view=aspnetcore-10.0)

* [Threat mitigation: Cross-site scripting (XSS)](../security/interactive-server-side-rendering?view=aspnetcore-10.0#cross-site-scripting-xss)

Razor components can be integrated into Razor Pages or MVC apps. When the page or view is rendered, components can be prerendered at the same time.

Prerendering can improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines can use to calculate page rank.

After [configuring the project](#configuration), use the guidance in the following sections depending on the project's requirements:

* Routable components: For components that are directly routable from user requests. Follow this guidance when visitors should be able to make an HTTP request in their browser for a component with an [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive.

  * [Use routable components in a Razor Pages app](#use-routable-components-in-a-razor-pages-app)
  * [Use routable components in an MVC app](#use-routable-components-in-an-mvc-app)

* [Render components from a page or view](#render-components-from-a-page-or-view): For components that aren't directly routable from user requests. Follow this guidance when the app embeds components into existing pages or views with the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

## Configuration

An existing Razor Pages or MVC app can integrate Razor components into pages or views:

1. In the project's layout file:

   * Add the following `<base>` tag to the `<head>` element in `Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC):

     ```
     + <base href="~/" />
     ```

     The `href` value (the *app base path*) in the preceding example assumes that the app resides at the root URL path (`/`). If the app is a sub-application, see [ASP.NET Core Blazor app base path](../host-and-deploy/app-base-path?view=aspnetcore-10.0).

   * Add a `<script>` tag for the `blazor.server.js` script immediately before the `Scripts` render section.

     `Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC):

     ```
         ...
         <script src="_framework/blazor.server.js"></script>

         @await RenderSectionAsync("Scripts", required: false)
     </body>
     ```

     The framework adds the `blazor.server.js` script to the app. There's no need to manually add a `blazor.server.js` script file to the app.

2. Add an imports file to the root folder of the project with the following content. Change the `{APP NAMESPACE}` placeholder to the namespace of the project.

   `_Imports.razor`:

   ```
   @using System.Net.Http
   @using Microsoft.AspNetCore.Authorization
   @using Microsoft.AspNetCore.Components.Authorization
   @using Microsoft.AspNetCore.Components.Forms
   @using Microsoft.AspNetCore.Components.Routing
   @using Microsoft.AspNetCore.Components.Web
   @using Microsoft.JSInterop
   @using {APP NAMESPACE}
   ```

3. Register the Blazor Server service in `Startup.ConfigureServices`.

   `Startup.cs`:

   ```
   services.AddServerSideBlazor();
   ```

4. Add the Blazor Hub endpoint to the endpoints (`app.UseEndpoints`) of `Startup.Configure`.

   `Startup.cs`:

   ```
   endpoints.MapBlazorHub();
   ```

5. Integrate components into any page or view. For example, add a `Counter` component to the project's `Shared` folder.

   `Pages/Shared/Counter.razor` (Razor Pages) or `Views/Shared/Counter.razor` (MVC):

   ```
   <h1>Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

   **Razor Pages**:

   In the project's `Index` page of a Razor Pages app, add the `Counter` component's namespace and embed the component into the page. When the `Index` page loads, the `Counter` component is prerendered in the page. In the following example, replace the `{APP NAMESPACE}` placeholder with the project's namespace.

   `Pages/Index.cshtml`:

   ```
   @page
   @using {APP NAMESPACE}.Pages.Shared
   @model IndexModel
   @{
       ViewData["Title"] = "Home page";
   }

   <div>
       <component type="typeof(Counter)" render-mode="ServerPrerendered" />
   </div>
   ```

   In the preceding example, replace the `{APP NAMESPACE}` placeholder with the app's namespace.

   **MVC**:

   In the project's `Index` view of an MVC app, add the `Counter` component's namespace and embed the component into the view. When the `Index` view loads, the `Counter` component is prerendered in the page. In the following example, replace the `{APP NAMESPACE}` placeholder with the project's namespace.

   `Views/Home/Index.cshtml`:

   ```
   @using {APP NAMESPACE}.Views.Shared
   @{
       ViewData["Title"] = "Home Page";
   }

   <div>
       <component type="typeof(Counter)" render-mode="ServerPrerendered" />
   </div>
   ```

For more information, see the [Render components from a page or view](#render-components-from-a-page-or-view) section.

## Use routable components in a Razor Pages app

*This section pertains to adding components that are directly routable from user requests.*

To support routable Razor components in Razor Pages apps:

1. Follow the guidance in the [Configuration](#configuration) section.

2. Add an `App` component to the project root with the following content.

   `App.razor`:

   ```
   @using Microsoft.AspNetCore.Components.Routing

   <Router AppAssembly="typeof(Program).Assembly">
       <Found Context="routeData">
           <RouteView RouteData="routeData" />
       </Found>
       <NotFound>
           <h1>Page not found</h1>
           <p>Sorry, but there's nothing here!</p>
       </NotFound>
   </Router>
   ```

3. Add a `_Host` page to the project with the following content.

   `Pages/_Host.cshtml`:

   ```
   @page "/blazor"
   @{
       Layout = "_Layout";
   }

   <app>
       <component type="typeof(App)" render-mode="ServerPrerendered" />
   </app>
   ```

   Components use the shared `_Layout.cshtml` file for their layout.

   [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the `App` component:

   * Is prerendered into the page.
   * Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

   For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

4. In the `Startup.Configure` endpoints of `Startup.cs`, add a low-priority route for the `_Host` page as the last endpoint:

   ```
   endpoints.MapFallbackToPage("/_Host");
   ```

   The following example shows the added line in a typical app's endpoint configuration:

   ```
   app.UseEndpoints(endpoints =>
   {
       endpoints.MapRazorPages();
       endpoints.MapBlazorHub();
       endpoints.MapFallbackToPage("/_Host");
   });
   ```

5. Add routable components to the project.

   `Pages/RoutableCounter.razor`:

   ```
   @page "/routable-counter"

   <h1>Routable Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

6. Run the project and navigate to the routable `RoutableCounter` component at `/routable-counter`.

For more information on namespaces, see the [Component namespaces](#component-namespaces) section.

## Use routable components in an MVC app

*This section pertains to adding components that are directly routable from user requests.*

To support routable Razor components in MVC apps:

1. Follow the guidance in the [Configuration](#configuration) section.

2. Add an `App` component to the project root with the following content.

   `App.razor`:

   ```
   @using Microsoft.AspNetCore.Components.Routing

   <Router AppAssembly="typeof(Program).Assembly">
       <Found Context="routeData">
           <RouteView RouteData="routeData" />
       </Found>
       <NotFound>
           <h1>Page not found</h1>
           <p>Sorry, but there's nothing here!</p>
       </NotFound>
   </Router>
   ```

3. Add a `_Host` view to the project with the following content.

   `Views/Home/_Host.cshtml`:

   ```
   @{
       Layout = "_Layout";
   }

   <app>
       <component type="typeof(App)" render-mode="ServerPrerendered" />
   </app>
   ```

   Components use the shared `_Layout.cshtml` file for their layout.

   [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the `App` component:

   * Is prerendered into the page.
   * Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

   For more information on the Component Tag Helper, including passing parameters and [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configuration, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

4. Add an action to the Home controller.

   `Controllers/HomeController.cs`:

   ```
   public IActionResult Blazor()
   {
      return View("_Host");
   }
   ```

5. In the `Startup.Configure` endpoints of `Startup.cs`, add a low-priority route for the controller action that returns the `_Host` view:

   ```
   endpoints.MapFallbackToController("Blazor", "Home");
   ```

   The following example shows the added line in a typical app's endpoint configuration:

   ```
   app.UseEndpoints(endpoints =>
   {
       endpoints.MapControllerRoute(
           name: "default",
           pattern: "{controller=Home}/{action=Index}/{id?}");
       endpoints.MapBlazorHub();
       endpoints.MapFallbackToController("Blazor", "Home");
   });
   ```

6. Add routable components to the project.

   `Pages/RoutableCounter.razor`:

   ```
   @page "/routable-counter"

   <h1>Routable Counter</h1>

   <p>Current count: @currentCount</p>

   <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

   @code {
       private int currentCount = 0;

       private void IncrementCount()
       {
           currentCount++;
       }
   }
   ```

7. Run the project and navigate to the routable `RoutableCounter` component at `/routable-counter`.

For more information on namespaces, see the [Component namespaces](#component-namespaces) section.

## Render components from a page or view

*This section pertains to adding components to pages or views, where the components aren't directly routable from user requests.*

To render a component from a page or view, use the [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

### Render stateful interactive components

Stateful interactive components can be added to a Razor page or view.

When the page or view renders:

* The component is prerendered with the page or view.
* The initial component state used for prerendering is lost.
* New component state is created when the SignalR connection is established.

The following Razor page renders a `Counter` component:

```
<h1>My Razor Page</h1>

<component type="typeof(Counter)" render-mode="ServerPrerendered" 
    param-InitialValue="InitialValue" />

@functions {
    [BindProperty(SupportsGet=true)]
    public int InitialValue { get; set; }
}
```

For more information, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

### Render noninteractive components

In the following Razor page, the `Counter` component is statically rendered with an initial value that's specified using a form. Since the component is statically rendered, the component isn't interactive:

```
<h1>My Razor Page</h1>

<form>
    <input type="number" asp-for="InitialValue" />
    <button type="submit">Set initial value</button>
</form>

<component type="typeof(Counter)" render-mode="Static" 
    param-InitialValue="InitialValue" />

@functions {
    [BindProperty(SupportsGet=true)]
    public int InitialValue { get; set; }
}
```

For more information, see [Component Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0).

## Component namespaces

When using a custom folder to hold the project's Razor components, add the namespace representing the folder to either the page/view or to the `_ViewImports.cshtml` file. In the following example:

* Components are stored in the `Components` folder of the project.
* The `{APP NAMESPACE}` placeholder is the project's namespace. `Components` represents the name of the folder.

```
@using {APP NAMESPACE}.Components
```

The `_ViewImports.cshtml` file is located in the `Pages` folder of a Razor Pages app or the `Views` folder of an MVC app.

For more information, see [ASP.NET Core Razor components](./?view=aspnetcore-10.0#class-name-and-namespace).

## Prerendered state size and SignalR message size limit

A large prerendered state size may exceed Blazor's SignalR circuit message size limit, which results in the following:

* The SignalR circuit fails to initialize with an error on the client: Circuit host not initialized.
* The reconnection UI on the client appears when the circuit fails. Recovery isn't possible.

To resolve the problem, use ***either*** of the following approaches:

* Reduce the amount of data that you are putting into the prerendered state.
* Increase the [SignalR message size limit](../fundamentals/signalr?view=aspnetcore-10.0#server-side-circuit-handler-options). ***WARNING***: Increasing the limit may increase the risk of Denial of Service (DoS) attacks.

## Additional Blazor Server resources

* [State management: Protected browser storage: Handle prerendering](../state-management/protected-browser-storage?view=aspnetcore-10.0#handle-prerendering)

* Razor component lifecycle subjects that pertain to prerendering

  * [Component initialization (`OnInitialized{Async}`)](lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync)
  * [After component render (`OnAfterRender{Async}`)](lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync)
  * [Stateful reconnection after prerendering](lifecycle?view=aspnetcore-10.0#stateful-reconnection-after-prerendering)
  * [Prerendering with JavaScript interop](lifecycle?view=aspnetcore-10.0#prerendering-with-javascript-interop)

* [Authentication and authorization: General aspects](../security/?view=aspnetcore-10.0#aspnet-core-blazor-authentication-and-authorization)

* [Handle Errors: Prerendering](../fundamentals/handle-errors?view=aspnetcore-10.0#prerendering)

* [Host and deploy ASP.NET Core server-side Blazor apps](../host-and-deploy/server/?view=aspnetcore-10.0)

* [Threat mitigation: Cross-site scripting (XSS)](../security/interactive-server-side-rendering?view=aspnetcore-10.0#cross-site-scripting-xss)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/building-net-docker-images
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/docker/building-net-docker-images.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Run an ASP.NET Core app in Docker containers

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article shows how to run an ASP.NET Core app in Docker containers.

Windows Home Edition doesn't support Hyper-V, and Hyper-V is needed for Docker.

See [Containerize a .NET app with dotnet publish](/en-us/dotnet/core/docker/publish-as-container) for information on containerized a .NET app with `dotnet publish`.

## ASP.NET Core Docker images

For this tutorial, you download an ASP.NET Core sample app and run it in Docker containers. The sample works with both Linux and Windows containers.

The sample Dockerfile uses the [Docker multi-stage build feature](https://docs.docker.com/engine/userguide/eng-image/multistage-build/) to build and run in different containers. The build and run containers are created from images that are provided in Docker Hub by Microsoft:

* `dotnet/sdk`

  The sample uses this image for building the app. The image contains the .NET SDK, which includes the Command Line Tools (CLI). The image is optimized for local development, debugging, and unit testing. The tools installed for development and compilation make the image relatively large.

* `dotnet/aspnet`

  The sample uses this image for running the app. The image contains the ASP.NET Core runtime and libraries and is optimized for running apps in production. Designed for speed of deployment and app startup, the image is relatively small, so network performance from Docker Registry to Docker host is optimized. Only the binaries and content needed to run an app are copied to the container. The contents are ready to run, enabling the fastest time from `docker run` to app startup. Dynamic code compilation isn't needed in the Docker model.

## Prerequisites

* [.NET SDK 10.0](https://dotnet.microsoft.com/download)

* Docker client 18.03 or later

  * Linux distributions

    * [Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
    * [Fedora](https://docs.docker.com/install/linux/docker-ce/fedora/)
    * [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/)

  * [macOS](https://docs.docker.com/desktop/mac/install/)

  * [Windows](https://docs.docker.com/desktop/windows/install/)

* [Git](https://git-scm.com/download)

## Download the sample app

* Download the sample by cloning the [.NET Docker repository](https://github.com/dotnet/dotnet-docker):

  ```
  git clone https://github.com/dotnet/dotnet-docker
  ```

## Run the app locally

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the following command to build and run the app locally:

  ```
  dotnet run
  ```

* Go to `http://localhost:<port>` in a browser to test the app.

* Press Ctrl+C at the command prompt to stop the app.

## Run in a Linux container or Windows container

* To run in a Linux container, right-click the System Tray's Docker client icon and select [switch to Linux containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* To run in a Windows container, right-click the System Tray's Docker client icon and select [switch to Windows containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* Navigate to the Dockerfile folder at *dotnet-docker/samples/aspnetapp*.

* Run the following commands to build and run the sample in Docker:

  ```
  docker build -t aspnetapp .
  docker run -it --rm -p <port>:8080 --name aspnetcore_sample aspnetapp
  ```

  The `build` command arguments:

  * Name the image aspnetapp.
  * Look for the Dockerfile in the current folder (the period at the end).

  The run command arguments:

  * Allocate a pseudo-TTY and keep it open even if not attached. (Same effect as `--interactive --tty`.)
  * Automatically remove the container when it exits.
  * Map `<port>` on the local machine to port 8080 in the container.
  * Name the container aspnetcore\_sample.
  * Specify the aspnetapp image.

* Go to `http://localhost:<port>` in a browser to test the app.

## Build and deploy manually

In some scenarios, you might want to deploy an app to a container by copying its assets that are needed at run time. This section shows how to deploy manually.

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command:

  ```
  dotnet publish -c Release -o published
  ```

  The command arguments:

  * Build the app in release mode (the default is debug mode).
  * Create the assets in the *published* folder.

* Run the app.

  * Windows:

    ```
    dotnet published\aspnetapp.dll
    ```

  * Linux:

    ```
    dotnet published/aspnetapp.dll
    ```

* Browse to `http://localhost:<port>` to see the home page.

To use the manually published app within a Docker container, create a new *Dockerfile* and use the `docker build .` command to build an image.

```
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY published/ ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

To see the new image use the `docker images` command.

### The Dockerfile

Here's the *Dockerfile* used by the `docker build` command you ran earlier. It uses `dotnet publish` the same way you did in this section to build and deploy.

```
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app --no-restore

# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

In the preceding *Dockerfile*, the `*.csproj` files are copied and restored as distinct *layers*. When the `docker build` command builds an image, it uses a built-in cache. If the `*.csproj` files did not change since the `docker build` command last ran, the `dotnet restore` command doesn't need to run again. Instead, the built-in cache for the corresponding `dotnet restore` layer is reused. For more information, see [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache).

## Related content

* [Containerize a .NET app with dotnet publish](/en-us/dotnet/core/docker/publish-as-container)
* [Docker build command](https://docs.docker.com/engine/reference/commandline/build)
* [Docker run command](https://docs.docker.com/engine/reference/commandline/run)
* [ASP.NET Core Docker sample](https://github.com/dotnet/dotnet-docker) (The one used in this tutorial.)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0)
* [Working with Visual Studio Docker Tools](/en-us/visualstudio/containers/add-container-support)
* [Debugging with Visual Studio Code](https://code.visualstudio.com/docs/nodejs/debugging-recipes#_debug-nodejs-in-docker-containers)
* [GC using Docker and small containers](../../performance/memory?view=aspnetcore-10.0#gc-using-docker-and-small-containers)
* [System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached](./?view=aspnetcore-10.0#d128)
* [Updates to Docker images](https://andrewlock.net/exploring-the-dotnet-8-preview-updates-to-docker-images-in-dotnet-8/)

## Next steps

The Git repository that contains the sample app also includes documentation. For an overview of the resources available in the repository, see [the README file](https://github.com/dotnet/dotnet-docker/blob/main/samples/aspnetapp/README.md). In particular, learn how to implement HTTPS:

[Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md)

## ASP.NET Core Docker images

For this tutorial, you download an ASP.NET Core sample app and run it in Docker containers. The sample works with both Linux and Windows containers.

The sample Dockerfile uses the [Docker multi-stage build feature](https://docs.docker.com/engine/userguide/eng-image/multistage-build/) to build and run in different containers. The build and run containers are created from images that are provided in Docker Hub by Microsoft:

* `dotnet/sdk`

  The sample uses this image for building the app. The image contains the .NET SDK, which includes the Command Line Tools (CLI). The image is optimized for local development, debugging, and unit testing. The tools installed for development and compilation make the image relatively large.

* `dotnet/aspnet`

  The sample uses this image for running the app. The image contains the ASP.NET Core runtime and libraries and is optimized for running apps in production. Designed for speed of deployment and app startup, the image is relatively small, so network performance from Docker Registry to Docker host is optimized. Only the binaries and content needed to run an app are copied to the container. The contents are ready to run, enabling the fastest time from `docker run` to app startup. Dynamic code compilation isn't needed in the Docker model.

## Prerequisites

* [.NET SDK 9.0](https://dotnet.microsoft.com/download)

* Docker client 18.03 or later

  * Linux distributions

    * [Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
    * [Fedora](https://docs.docker.com/install/linux/docker-ce/fedora/)
    * [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/)

  * [macOS](https://docs.docker.com/desktop/mac/install/)

  * [Windows](https://docs.docker.com/desktop/windows/install/)

* [Git](https://git-scm.com/download)

## Download the sample app

* Download the sample by cloning the [.NET Docker repository](https://github.com/dotnet/dotnet-docker):

  ```
  git clone https://github.com/dotnet/dotnet-docker
  ```

## Run the app locally

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the following command to build and run the app locally:

  ```
  dotnet run
  ```

* Go to `http://localhost:<port>` in a browser to test the app.

* Press Ctrl+C at the command prompt to stop the app.

## Run in a Linux container or Windows container

* To run in a Linux container, right-click the System Tray's Docker client icon and select [switch to Linux containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* To run in a Windows container, right-click the System Tray's Docker client icon and select [switch to Windows containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* Navigate to the Dockerfile folder at *dotnet-docker/samples/aspnetapp*.

* Run the following commands to build and run the sample in Docker:

  ```
  docker build -t aspnetapp .
  docker run -it --rm -p <port>:8080 --name aspnetcore_sample aspnetapp
  ```

  The `build` command arguments:

  * Name the image aspnetapp.
  * Look for the Dockerfile in the current folder (the period at the end).

  The run command arguments:

  * Allocate a pseudo-TTY and keep it open even if not attached. (Same effect as `--interactive --tty`.)
  * Automatically remove the container when it exits.
  * Map `<port>` on the local machine to port 8080 in the container.
  * Name the container aspnetcore\_sample.
  * Specify the aspnetapp image.

* Go to `http://localhost:<port>` in a browser to test the app.

## Build and deploy manually

In some scenarios, you might want to deploy an app to a container by copying its assets that are needed at run time. This section shows how to deploy manually.

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command:

  ```
  dotnet publish -c Release -o published
  ```

  The command arguments:

  * Build the app in release mode (the default is debug mode).
  * Create the assets in the *published* folder.

* Run the app.

  * Windows:

    ```
    dotnet published\aspnetapp.dll
    ```

  * Linux:

    ```
    dotnet published/aspnetapp.dll
    ```

* Browse to `http://localhost:<port>` to see the home page.

To use the manually published app within a Docker container, create a new *Dockerfile* and use the `docker build .` command to build an image.

```
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY published/ ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

To see the new image use the `docker images` command.

### The Dockerfile

Here's the *Dockerfile* used by the `docker build` command you ran earlier. It uses `dotnet publish` the same way you did in this section to build and deploy.

```
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app --no-restore

# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

In the preceding *Dockerfile*, the `*.csproj` files are copied and restored as distinct *layers*. When the `docker build` command builds an image, it uses a built-in cache. If the `*.csproj` files haven't changed since the `docker build` command last ran, the `dotnet restore` command doesn't need to run again. Instead, the built-in cache for the corresponding `dotnet restore` layer is reused. For more information, see [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache).

## Related content

* [Containerize a .NET app with dotnet publish](/en-us/dotnet/core/docker/publish-as-container)
* [Docker build command](https://docs.docker.com/engine/reference/commandline/build)
* [Docker run command](https://docs.docker.com/engine/reference/commandline/run)
* [ASP.NET Core Docker sample](https://github.com/dotnet/dotnet-docker) (The one used in this tutorial.)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0)
* [Working with Visual Studio Docker Tools](/en-us/visualstudio/containers/add-container-support)
* [Debugging with Visual Studio Code](https://code.visualstudio.com/docs/nodejs/debugging-recipes#_debug-nodejs-in-docker-containers)
* [GC using Docker and small containers](../../performance/memory?view=aspnetcore-10.0#gc-using-docker-and-small-containers)
* [System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached](./?view=aspnetcore-10.0#d128)
* [Updates to Docker images](https://andrewlock.net/exploring-the-dotnet-8-preview-updates-to-docker-images-in-dotnet-8/)

## Next steps

The Git repository that contains the sample app also includes documentation. For an overview of the resources available in the repository, see [the README file](https://github.com/dotnet/dotnet-docker/blob/main/samples/aspnetapp/README.md). In particular, learn how to implement HTTPS:

[Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md)

## ASP.NET Core Docker images

For this tutorial, you download an ASP.NET Core sample app and run it in Docker containers. The sample works with both Linux and Windows containers.

The sample Dockerfile uses the [Docker multi-stage build feature](https://docs.docker.com/engine/userguide/eng-image/multistage-build/) to build and run in different containers. The build and run containers are created from images that are provided in Docker Hub by Microsoft:

* `dotnet/sdk`

  The sample uses this image for building the app. The image contains the .NET SDK, which includes the Command Line Tools (CLI). The image is optimized for local development, debugging, and unit testing. The tools installed for development and compilation make the image relatively large.

* `dotnet/aspnet`

  The sample uses this image for running the app. The image contains the ASP.NET Core runtime and libraries and is optimized for running apps in production. Designed for speed of deployment and app startup, the image is relatively small, so network performance from Docker Registry to Docker host is optimized. Only the binaries and content needed to run an app are copied to the container. The contents are ready to run, enabling the fastest time from `docker run` to app startup. Dynamic code compilation isn't needed in the Docker model.

## Prerequisites

* [.NET SDK 8.0](https://dotnet.microsoft.com/download)

* Docker client 18.03 or later

  * Linux distributions

    * [Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
    * [Fedora](https://docs.docker.com/install/linux/docker-ce/fedora/)
    * [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/)

  * [macOS](https://docs.docker.com/desktop/mac/install/)

  * [Windows](https://docs.docker.com/desktop/windows/install/)

* [Git](https://git-scm.com/download)

## Download the sample app

* Download the sample by cloning the [.NET Docker repository](https://github.com/dotnet/dotnet-docker):

  ```
  git clone https://github.com/dotnet/dotnet-docker
  ```

## Run the app locally

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the following command to build and run the app locally:

  ```
  dotnet run
  ```

* Go to `http://localhost:<port>` in a browser to test the app.

* Press Ctrl+C at the command prompt to stop the app.

## Run in a Linux container or Windows container

* To run in a Linux container, right-click the System Tray's Docker client icon and select [switch to Linux containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* To run in a Windows container, right-click the System Tray's Docker client icon and select [switch to Windows containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* Navigate to the Dockerfile folder at *dotnet-docker/samples/aspnetapp*.

* Run the following commands to build and run the sample in Docker:

  ```
  docker build -t aspnetapp .
  docker run -it --rm -p <port>:8080 --name aspnetcore_sample aspnetapp
  ```

  The `build` command arguments:

  * Name the image aspnetapp.
  * Look for the Dockerfile in the current folder (the period at the end).

  The run command arguments:

  * Allocate a pseudo-TTY and keep it open even if not attached. (Same effect as `--interactive --tty`.)
  * Automatically remove the container when it exits.
  * Map `<port>` on the local machine to port 8080 in the container.
  * Name the container aspnetcore\_sample.
  * Specify the aspnetapp image.

* Go to `http://localhost:<port>` in a browser to test the app.

## Build and deploy manually

In some scenarios, you might want to deploy an app to a container by copying its assets that are needed at run time. This section shows how to deploy manually.

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command:

  ```
  dotnet publish -c Release -o published
  ```

  The command arguments:

  * Build the app in release mode (the default is debug mode).
  * Create the assets in the *published* folder.

* Run the app.

  * Windows:

    ```
    dotnet published\aspnetapp.dll
    ```

  * Linux:

    ```
    dotnet published/aspnetapp.dll
    ```

* Browse to `http://localhost:<port>` to see the home page.

To use the manually published app within a Docker container, create a new *Dockerfile* and use the `docker build .` command to build an image.

```
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY published/ ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

To see the new image use the `docker images` command.

### The Dockerfile

Here's the *Dockerfile* used by the `docker build` command you ran earlier. It uses `dotnet publish` the same way you did in this section to build and deploy.

```
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app --no-restore

# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

In the preceding *Dockerfile*, the `*.csproj` files are copied and restored as distinct *layers*. When the `docker build` command builds an image, it uses a built-in cache. If the `*.csproj` files haven't changed since the `docker build` command last ran, the `dotnet restore` command doesn't need to run again. Instead, the built-in cache for the corresponding `dotnet restore` layer is reused. For more information, see [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache).

## Related content

* [Containerize a .NET app with dotnet publish](/en-us/dotnet/core/docker/publish-as-container)
* [Docker build command](https://docs.docker.com/engine/reference/commandline/build)
* [Docker run command](https://docs.docker.com/engine/reference/commandline/run)
* [ASP.NET Core Docker sample](https://github.com/dotnet/dotnet-docker) (The one used in this tutorial.)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0)
* [Working with Visual Studio Docker Tools](/en-us/visualstudio/containers/add-container-support)
* [Debugging with Visual Studio Code](https://code.visualstudio.com/docs/nodejs/debugging-recipes#_debug-nodejs-in-docker-containers)
* [GC using Docker and small containers](../../performance/memory?view=aspnetcore-10.0#gc-using-docker-and-small-containers)
* [System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached](./?view=aspnetcore-10.0#d128)
* [Updates to Docker images](https://andrewlock.net/exploring-the-dotnet-8-preview-updates-to-docker-images-in-dotnet-8/)

## Next steps

The Git repository that contains the sample app also includes documentation. For an overview of the resources available in the repository, see [the README file](https://github.com/dotnet/dotnet-docker/blob/main/samples/aspnetapp/README.md). In particular, learn how to implement HTTPS:

[Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md)

## ASP.NET Core Docker images

For this tutorial, you download an ASP.NET Core sample app and run it in Docker containers. The sample works with both Linux and Windows containers.

The sample Dockerfile uses the [Docker multi-stage build feature](https://docs.docker.com/engine/userguide/eng-image/multistage-build/) to build and run in different containers. The build and run containers are created from images that are provided in Docker Hub by Microsoft:

* `dotnet/sdk`

  The sample uses this image for building the app. The image contains the .NET SDK, which includes the Command Line Tools (CLI). The image is optimized for local development, debugging, and unit testing. The tools installed for development and compilation make the image relatively large.

* `dotnet/aspnet`

  The sample uses this image for running the app. The image contains the ASP.NET Core runtime and libraries and is optimized for running apps in production. Designed for speed of deployment and app startup, the image is relatively small, so network performance from Docker Registry to Docker host is optimized. Only the binaries and content needed to run an app are copied to the container. The contents are ready to run, enabling the fastest time from `docker run` to app startup. Dynamic code compilation isn't needed in the Docker model.

## Prerequisites

* [.NET SDK 7.0](https://dotnet.microsoft.com/download)

* Docker client 18.03 or later

  * Linux distributions

    * [Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
    * [Fedora](https://docs.docker.com/install/linux/docker-ce/fedora/)
    * [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/)

  * [macOS](https://docs.docker.com/desktop/mac/install/)

  * [Windows](https://docs.docker.com/desktop/windows/install/)

* [Git](https://git-scm.com/download)

## Download the sample app

* Download the sample by cloning the [.NET Docker repository](https://github.com/dotnet/dotnet-docker):

  ```
  git clone https://github.com/dotnet/dotnet-docker
  ```

## Run the app locally

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the following command to build and run the app locally:

  ```
  dotnet run
  ```

* Go to `http://localhost:5000` in a browser to test the app.

* Press Ctrl+C at the command prompt to stop the app.

## Run in a Linux container or Windows container

* To run in a Linux container, right-click the System Tray's Docker client icon and select [switch to Linux containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* To run in a Windows container, right-click the System Tray's Docker client icon and select [switch to Windows containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* Navigate to the Dockerfile folder at *dotnet-docker/samples/aspnetapp*.

* Run the following commands to build and run the sample in Docker:

  ```
  docker build -t aspnetapp .
  docker run -it --rm -p 5000:80 --name aspnetcore_sample aspnetapp
  ```

  The `build` command arguments:

  * Name the image aspnetapp.
  * Look for the Dockerfile in the current folder (the period at the end).

  The run command arguments:

  * Allocate a pseudo-TTY and keep it open even if not attached. (Same effect as `--interactive --tty`.)
  * Automatically remove the container when it exits.
  * Map port 5000 on the local machine to port 80 in the container.
  * Name the container aspnetcore\_sample.
  * Specify the aspnetapp image.

* Go to `http://localhost:5000` in a browser to test the app.

## Build and deploy manually

In some scenarios, you might want to deploy an app to a container by copying its assets that are needed at run time. This section shows how to deploy manually.

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command:

  ```
  dotnet publish -c Release -o published
  ```

  The command arguments:

  * Build the app in release mode (the default is debug mode).
  * Create the assets in the *published* folder.

* Run the app.

  * Windows:

    ```
    dotnet published\aspnetapp.dll
    ```

  * Linux:

    ```
    dotnet published/aspnetapp.dll
    ```

* Browse to `http://localhost:5000` to see the home page.

To use the manually published app within a Docker container, create a new *Dockerfile* and use the `docker build .` command to build an image.

```
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS runtime
WORKDIR /app
COPY published/ ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

To see the new image use the `docker images` command.

### The Dockerfile

Here's the *Dockerfile* used by the `docker build` command you ran earlier. It uses `dotnet publish` the same way you did in this section to build and deploy.

```
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app --no-restore

# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

In the preceding *Dockerfile*, the `*.csproj` files are copied and restored as distinct *layers*. When the `docker build` command builds an image, it uses a built-in cache. If the `*.csproj` files haven't changed since the `docker build` command last ran, the `dotnet restore` command doesn't need to run again. Instead, the built-in cache for the corresponding `dotnet restore` layer is reused. For more information, see [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache).

## Related content

* [Containerize a .NET app with dotnet publish](/en-us/dotnet/core/docker/publish-as-container)
* [Docker build command](https://docs.docker.com/engine/reference/commandline/build)
* [Docker run command](https://docs.docker.com/engine/reference/commandline/run)
* [ASP.NET Core Docker sample](https://github.com/dotnet/dotnet-docker) (The one used in this tutorial.)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0)
* [Working with Visual Studio Docker Tools](/en-us/visualstudio/containers/add-container-support)
* [Debugging with Visual Studio Code](https://code.visualstudio.com/docs/nodejs/debugging-recipes#_debug-nodejs-in-docker-containers)
* [GC using Docker and small containers](../../performance/memory?view=aspnetcore-10.0#gc-using-docker-and-small-containers)
* [System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached](./?view=aspnetcore-10.0#d128)
* [Updates to Docker images](https://andrewlock.net/exploring-the-dotnet-8-preview-updates-to-docker-images-in-dotnet-8/)

## Next steps

The Git repository that contains the sample app also includes documentation. For an overview of the resources available in the repository, see [the README file](https://github.com/dotnet/dotnet-docker/blob/main/samples/aspnetapp/README.md). In particular, learn how to implement HTTPS:

[Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md)

## ASP.NET Core Docker images

For this tutorial, you download an ASP.NET Core sample app and run it in Docker containers. The sample works with both Linux and Windows containers.

The sample Dockerfile uses the [Docker multi-stage build feature](https://docs.docker.com/engine/userguide/eng-image/multistage-build/) to build and run in different containers. The build and run containers are created from images that are provided in Docker Hub by Microsoft:

* `dotnet/sdk`

  The sample uses this image for building the app. The image contains the .NET SDK, which includes the Command Line Tools (CLI). The image is optimized for local development, debugging, and unit testing. The tools installed for development and compilation make the image relatively large.

- `dotnet/core/sdk`

  The sample uses this image for building the app. The image contains the .NET Core SDK, which includes the Command Line Tools (CLI). The image is optimized for local development, debugging, and unit testing. The tools installed for development and compilation make the image relatively large.

* `dotnet/aspnet`

  The sample uses this image for running the app. The image contains the ASP.NET Core runtime and libraries and is optimized for running apps in production. Designed for speed of deployment and app startup, the image is relatively small, so network performance from Docker Registry to Docker host is optimized. Only the binaries and content needed to run an app are copied to the container. The contents are ready to run, enabling the fastest time from `docker run` to app startup. Dynamic code compilation isn't needed in the Docker model.

- `dotnet/core/aspnet`

  The sample uses this image for running the app. The image contains the ASP.NET Core runtime and libraries and is optimized for running apps in production. Designed for speed of deployment and app startup, the image is relatively small, so network performance from Docker Registry to Docker host is optimized. Only the binaries and content needed to run an app are copied to the container. The contents are ready to run, enabling the fastest time from `docker run` to app startup. Dynamic code compilation isn't needed in the Docker model.

## Prerequisites

* [.NET SDK 6.0](https://dotnet.microsoft.com/download)

- [.NET SDK 5.0](https://dotnet.microsoft.com/download)

* [.NET Core SDK 3.1](https://dotnet.microsoft.com/download)

- [.NET Core 2.2 SDK](https://dotnet.microsoft.com/download/dotnet-core)

* Docker client 18.03 or later

  * Linux distributions

    * [Debian](https://docs.docker.com/install/linux/docker-ce/debian/)
    * [Fedora](https://docs.docker.com/install/linux/docker-ce/fedora/)
    * [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/)

  * [macOS](https://docs.docker.com/desktop/mac/install/)

  * [Windows](https://docs.docker.com/desktop/windows/install/)

* [Git](https://git-scm.com/download)

## Download the sample app

* Download the sample by cloning the [.NET Docker repository](https://github.com/dotnet/dotnet-docker):

  ```
  git clone https://github.com/dotnet/dotnet-docker
  ```

## Run the app locally

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the following command to build and run the app locally:

  ```
  dotnet run
  ```

* Go to `http://localhost:5000` in a browser to test the app.

* Press Ctrl+C at the command prompt to stop the app.

## Run in a Linux container or Windows container

* To run in a Linux container, right-click the System Tray's Docker client icon and select [switch to Linux containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* To run in a Windows container, right-click the System Tray's Docker client icon and select [switch to Windows containers](https://docs.docker.com/desktop/windows/#switch-between-windows-and-linux-containers).

* Navigate to the Dockerfile folder at *dotnet-docker/samples/aspnetapp*.

* Run the following commands to build and run the sample in Docker:

  ```
  docker build -t aspnetapp .
  docker run -it --rm -p 5000:80 --name aspnetcore_sample aspnetapp
  ```

  The `build` command arguments:

  * Name the image aspnetapp.
  * Look for the Dockerfile in the current folder (the period at the end).

  The run command arguments:

  * Allocate a pseudo-TTY and keep it open even if not attached. (Same effect as `--interactive --tty`.)
  * Automatically remove the container when it exits.
  * Map port 5000 on the local machine to port 80 in the container.
  * Name the container aspnetcore\_sample.
  * Specify the aspnetapp image.

* Go to `http://localhost:5000` in a browser to test the app.

## Build and deploy manually

In some scenarios, you might want to deploy an app to a container by copying its assets that are needed at run time. This section shows how to deploy manually.

* Navigate to the project folder at *dotnet-docker/samples/aspnetapp/aspnetapp*.

* Run the [dotnet publish](/en-us/dotnet/core/tools/dotnet-publish) command:

  ```
  dotnet publish -c Release -o published
  ```

  The command arguments:

  * Build the app in release mode (the default is debug mode).
  * Create the assets in the *published* folder.

* Run the app.

  * Windows:

    ```
    dotnet published\aspnetapp.dll
    ```

  * Linux:

    ```
    dotnet published/aspnetapp.dll
    ```

* Browse to `http://localhost:5000` to see the home page.

To use the manually published app within a Docker container, create a new *Dockerfile* and use the `docker build .` command to build an image.

```
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime
WORKDIR /app
COPY published/ ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

To see the new image use the `docker images` command.

### The Dockerfile

Here's the *Dockerfile* used by the `docker build` command you ran earlier. It uses `dotnet publish` the same way you did in this section to build and deploy.

```
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app --no-restore

# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

```
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
WORKDIR /app
COPY published/ ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

To see the new image use the `docker images` command.

### The Dockerfile

Here's the *Dockerfile* used by the `docker build` command you ran earlier. It uses `dotnet publish` the same way you did in this section to build and deploy.

```
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /source/aspnetapp
RUN dotnet publish -c release -o /app --no-restore

# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

In the preceding *Dockerfile*, the `*.csproj` files are copied and restored as distinct *layers*. When the `docker build` command builds an image, it uses a built-in cache. If the `*.csproj` files haven't changed since the `docker build` command last ran, the `dotnet restore` command doesn't need to run again. Instead, the built-in cache for the corresponding `dotnet restore` layer is reused. For more information, see [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache).

```
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS runtime
WORKDIR /app
COPY published/ ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

### The Dockerfile

Here's the *Dockerfile* used by the `docker build` command you ran earlier. It uses `dotnet publish` the same way you did in this section to build and deploy.

```
FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS build
WORKDIR /app

# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /app/aspnetapp
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS runtime
WORKDIR /app
COPY --from=build /app/aspnetapp/out ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

As noted in the preceding Dockerfile, the `*.csproj` files are copied and restored as distinct *layers*. When the `docker build` command builds an image, it uses a built-in cache. If the `*.csproj` files haven't changed since the `docker build` command last ran, the `dotnet restore` command doesn't need to run again. Instead, the built-in cache for the corresponding `dotnet restore` layer is reused. For more information, see [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache).

```
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS runtime
WORKDIR /app
COPY published/ ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

### The Dockerfile

Here's the *Dockerfile* used by the `docker build` command you ran earlier. It uses `dotnet publish` the same way you did in this section to build and deploy.

```
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build
WORKDIR /app

# copy csproj and restore as distinct layers
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore

# copy everything else and build app
COPY aspnetapp/. ./aspnetapp/
WORKDIR /app/aspnetapp
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS runtime
WORKDIR /app
COPY --from=build /app/aspnetapp/out ./
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
```

In the preceding *Dockerfile*, the `*.csproj` files are copied and restored as distinct *layers*. When the `docker build` command builds an image, it uses a built-in cache. If the `*.csproj` files haven't changed since the `docker build` command last ran, the `dotnet restore` command doesn't need to run again. Instead, the built-in cache for the corresponding `dotnet restore` layer is reused. For more information, see [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache).

## Related content

* [Containerize a .NET app with dotnet publish](/en-us/dotnet/core/docker/publish-as-container)
* [Docker build command](https://docs.docker.com/engine/reference/commandline/build)
* [Docker run command](https://docs.docker.com/engine/reference/commandline/run)
* [ASP.NET Core Docker sample](https://github.com/dotnet/dotnet-docker) (The one used in this tutorial.)
* [Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0)
* [Working with Visual Studio Docker Tools](/en-us/visualstudio/containers/add-container-support)
* [Debugging with Visual Studio Code](https://code.visualstudio.com/docs/nodejs/debugging-recipes#_debug-nodejs-in-docker-containers)
* [GC using Docker and small containers](../../performance/memory?view=aspnetcore-10.0#gc-using-docker-and-small-containers)
* [System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached](./?view=aspnetcore-10.0#d128)

## Next steps

The Git repository that contains the sample app also includes documentation. For an overview of the resources available in the repository, see [the README file](https://github.com/dotnet/dotnet-docker/blob/main/samples/aspnetapp/README.md). In particular, learn how to implement HTTPS:

[Developing ASP.NET Core Applications with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/examples/configuration
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/examples/configuration.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate configuration to ASP.NET Core

In the previous article, we began to [migrate an ASP.NET MVC project to ASP.NET Core MVC](overview?view=aspnetcore-10.0). In this article, we migrate configuration.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/migration/fx-to-core/examples/samples/configuration) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Setup configuration

ASP.NET Core no longer uses the *Global.asax* and *web.config* files that previous versions of ASP.NET utilized. In the earlier versions of ASP.NET, application startup logic was placed in an `Application_StartUp` method within *Global.asax*. Later, in ASP.NET MVC, a `Startup.cs` file was included in the root of the project; and, it was called when the application started. ASP.NET Core has adopted this approach completely by placing all startup logic in the `Startup.cs` file.

The *web.config* file has also been replaced in ASP.NET Core. Configuration itself can now be configured, as part of the application startup procedure described in `Startup.cs`. Configuration can still utilize XML files, but typically ASP.NET Core projects will place configuration values in a JSON-formatted file, such as `appsettings.json`. ASP.NET Core's configuration system can also easily access environment variables, which can provide a [more secure and robust location](../../../security/app-secrets?view=aspnetcore-10.0) for environment-specific values. This is especially true for secrets like connection strings and API keys that shouldn't be checked into source control. See [Configuration](../../../fundamentals/configuration/?view=aspnetcore-10.0) to learn more about configuration in ASP.NET Core.

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../../security/?view=aspnetcore-10.0#secure-authentication-flows).

To setup configuration, add the following constructor and property to the `Startup.cs` file located in the root of the project:

```
public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public IConfiguration Configuration { get; }
```

Note that at this point, the `Startup.cs` file won't compile, as we still need to add the following `using` statement:

```
using Microsoft.Extensions.Configuration;
```

Add an `appsettings.json` file to the root of the project using the appropriate item template:

## Migrate configuration settings from web.config

Our ASP.NET MVC project included the required database connection string in *web.config*, in the `<connectionStrings>` element. In our ASP.NET Core project, we are going to store this information in the `appsettings.json` file. Open `appsettings.json`, and note that it already includes the following:

```
{
    "Data": {
        "DefaultConnection": {
            "ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=_CHANGE_ME;Trusted_Connection=True;"
        }
    }
}
```

In the highlighted line depicted above, change the name of the database from **\_CHANGE\_ME** to the name of your database.

## Summary

ASP.NET Core places all startup logic for the application in a single file, in which the necessary services and dependencies can be defined and configured. It replaces the *web.config* file with a flexible configuration feature that can leverage a variety of file formats, such as JSON, as well as environment variables.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/versioning
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/versioning.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Versioning gRPC services

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

New features added to an app can require gRPC services provided to clients to change, sometimes in unexpected and breaking ways. When gRPC services change:

* Consideration should be given on how changes impact clients.
* A versioning strategy to support changes should be implemented.

## Backwards compatibility

The gRPC protocol is designed to support services that change over time. Generally, additions to gRPC services and methods are non-breaking. Non-breaking changes allow existing clients to continue working without changes. Changing or deleting gRPC services are breaking changes. When gRPC services have breaking changes, clients using that service have to be updated and redeployed.

Making non-breaking changes to a service has a number of benefits:

* Existing clients continue to run.
* Avoids work involved with notifying clients of breaking changes, and updating them.
* Only one version of the service needs to be documented and maintained.

### Non-breaking changes

These changes are non-breaking at a gRPC protocol level and .NET binary level.

* **Adding a new service**
* **Adding a new method to a service**
* **Adding a field to a request message** - Fields added to a request message are deserialized with the [default value](https://developers.google.com/protocol-buffers/docs/proto3#default) on the server when not set. To be a non-breaking change, the service must succeed when the new field isn't set by older clients.
* **Adding a field to a response message** - If an older client hasn't been updated with the new field, the value is deserialized into the response message's [unknown fields](https://developers.google.com/protocol-buffers/docs/proto3#unknowns) collection.
* **Adding a value to an enum** - Enums are serialized as a numeric value. New enum values are deserialized on the client to the enum value without an enum name. To be a non-breaking change, older clients must run correctly when receiving the new enum value.

### Binary breaking changes

The following changes are non-breaking at a gRPC protocol level, but the client needs to be updated if it upgrades to the latest `.proto` contract or client .NET assembly. Binary compatibility is important if you plan to publish a gRPC library to NuGet.

* **Removing a field** - Values from a removed field are deserialized to a message's [unknown fields](https://developers.google.com/protocol-buffers/docs/proto3#unknowns). This isn't a gRPC protocol breaking change, but the client needs to be updated if it upgrades to the latest contract. It's important that a removed field number isn't accidentally reused in the future. To ensure this doesn't happen, specify deleted field numbers and names on the message using Protobuf's [reserved](https://developers.google.com/protocol-buffers/docs/proto3#reserved) keyword.
* **Renaming a message** - Message names aren't typically sent on the network, so this isn't a gRPC protocol breaking change. The client will need to be updated if it upgrades to the latest contract. One situation where message names **are** sent on the network is with [Any](https://developers.google.com/protocol-buffers/docs/proto3#any) fields, when the message name is used to identify the message type.
* **Nesting or unnesting a message** - Message types can be [nested](https://developers.google.com/protocol-buffers/docs/proto3#nested). Nesting or unnesting a message changes its message name. Changing how a message type is nested has the same impact on compatibility as renaming.
* **Changing csharp\_namespace** - Changing `csharp_namespace` will change the namespace of generated .NET types. This isn't a gRPC protocol breaking change, but the client needs to be updated if it upgrades to the latest contract.

### Protocol breaking changes

The following items are protocol and binary breaking changes:

* **Renaming a field** - With Protobuf content, the field names are only used in generated code. The field number is used to identify fields on the network. Renaming a field isn't a protocol breaking change for Protobuf. However, if a server is using JSON content then renaming a field is a breaking change.
* **Changing a field data type** - Changing a field's data type to an [incompatible type](https://developers.google.com/protocol-buffers/docs/proto3#updating) will cause errors when deserializing the message. Even if the new data type is compatible, it's likely the client needs to be updated to support the new type if it upgrades to the latest contract.
* **Changing a field number** - With Protobuf payloads, the field number is used to identify fields on the network.
* **Renaming a package, service or method** - gRPC uses the package name, service name, and method name to build the URL. The client gets an *UNIMPLEMENTED* status from the server.
* **Removing a service or method** - The client gets an *UNIMPLEMENTED* status from the server when calling the removed method.

### Behavior breaking changes

When making non-breaking changes, you must also consider whether older clients can continue working with the new service behavior. For example, adding a new field to a request message:

* Isn't a protocol breaking change.
* Returning an error status on the server if the new field isn't set makes it a breaking change for old clients.

Behavior compatibility is determined by your app-specific code.

## Version number services

Services should strive to remain backwards compatible with old clients. Eventually changes to your app may require breaking changes. Breaking old clients and forcing them to be updated along with your service isn't a good user experience. A way to maintain backwards compatibility while making breaking changes is to publish multiple versions of a service.

gRPC supports an optional [package](https://developers.google.com/protocol-buffers/docs/proto3#packages) specifier, which functions much like a .NET namespace. In fact, the `package` will be used as the .NET namespace for generated .NET types if `option csharp_namespace` is not set in the `.proto` file. The package can be used to specify a version number for your service and its messages:

```
syntax = "proto3";

package greet.v1;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
```

The package name is combined with the service name to identify a service address. A service address allows multiple versions of a service to be hosted side-by-side:

* `greet.v1.Greeter`
* `greet.v2.Greeter`

Implementations of the versioned service are registered in `Startup.cs`:

```
app.UseEndpoints(endpoints =>
{
    // Implements greet.v1.Greeter
    endpoints.MapGrpcService<GreeterServiceV1>();

    // Implements greet.v2.Greeter
    endpoints.MapGrpcService<GreeterServiceV2>();
});
```

Including a version number in the package name gives you the opportunity to publish a *v2* version of your service with breaking changes, while continuing to support older clients who call the *v1* version. Once clients have updated to use the *v2* service, you can choose to remove the old version. When planning to publish multiple versions of a service:

* Avoid breaking changes if reasonable.
* Don't update the version number unless making breaking changes.
* Do update the version number when you make breaking changes.

Publishing multiple versions of a service duplicates it. To reduce duplication, consider moving business logic from the service implementations to a centralized location that can be reused by the old and new implementations:

```
using Greet.V1;
using Grpc.Core;
using System.Threading.Tasks;

namespace Services
{
    public class GreeterServiceV1 : Greeter.GreeterBase
    {
        private readonly IGreeter _greeter;
        public GreeterServiceV1(IGreeter greeter)
        {
            _greeter = greeter;
        }

        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloReply
            {
                Message = _greeter.GetHelloMessage(request.Name)
            });
        }
    }
}
```

Services and messages generated with different package names are **different .NET types**. Moving business logic to a centralized location requires mapping messages to common types.

## Related content

* [Create Protobuf messages for .NET apps](protobuf?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-09-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/iis
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/iis.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host and deploy ASP.NET Core Blazor WebAssembly with IIS

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to host and deploy Blazor WebAssembly using [Internet Information Services (IIS)](https://www.iis.net/).

IIS is a capable static file server for Blazor apps. To configure IIS to host Blazor, see [Build a Static Website on IIS](/en-us/iis/manage/creating-websites/scenario-build-a-static-website-on-iis).

Published assets are created in the `/bin/Release/{TARGET FRAMEWORK}/publish` or `bin/Release/{TARGET FRAMEWORK}/browser-wasm/publish` folder, where the `{TARGET FRAMEWORK}` placeholder is the target framework. Host the contents of the `publish` folder on the web server or hosting service.

## `web.config` file

When a Blazor project is published, a `web.config` file is created with the following IIS configuration:

* MIME types

* HTTP compression is enabled for the following MIME types:

  * `application/octet-stream`
  * `application/wasm`

* URL Rewrite Module rules are established:

  * Serve the sub-directory where the app's static assets reside (`wwwroot/{PATH REQUESTED}`).
  * Create SPA fallback routing so that requests for non-file assets are redirected to the app's default document in its static assets folder (`wwwroot/index.html`).

## Use of a custom `web.config`

To use a custom `web.config` file:

1. Place the custom `web.config` file in the project's root folder.
2. Publish the project. For more information, see [Host and deploy ASP.NET Core Blazor](../?view=aspnetcore-10.0).

1) Place the custom `web.config` file in the project's root folder. For a hosted Blazor WebAssembly [solution](../../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln), place the file in the **Server** project's folder.
2) Publish the project. For a hosted Blazor WebAssembly solution, publish the solution from the **Server** project. For more information, see [Host and deploy ASP.NET Core Blazor](../?view=aspnetcore-10.0).

If the SDK's `web.config` generation or transformation during publish either doesn't move the file to published assets in the `publish` folder or modifies the custom configuration in your custom `web.config` file, use any of the following approaches as needed to take full control of the process:

* If the SDK doesn't generate the file, for example, in a standalone Blazor WebAssembly app at `/bin/Release/{TARGET FRAMEWORK}/publish/wwwroot` or `bin/Release/{TARGET FRAMEWORK}/browser-wasm/publish`, where the `{TARGET FRAMEWORK}` placeholder is the target framework, set the `<PublishIISAssets>` property to `true` in the project file (`.csproj`). Usually for standalone WebAssembly apps, this is the only required setting to move a custom `web.config` file and prevent transformation of the file by the SDK.

  ```
  <PropertyGroup>
    <PublishIISAssets>true</PublishIISAssets>
  </PropertyGroup>
  ```

* Disable the SDK's `web.config` transformation in the project file (`.csproj`):

  ```
  <PropertyGroup>
    <IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
  </PropertyGroup>
  ```

* Add a custom target to the project file (`.csproj`) to move a custom `web.config` file. In the following example, the custom `web.config` file is placed by the developer at the root of the project. If the `web.config` file resides elsewhere, specify the path to the file in `SourceFiles`. The following example specifies the `publish` folder with `$(PublishDir)`, but provide a path to `DestinationFolder` for a custom output location.

  ```
  <Target Name="CopyWebConfig" AfterTargets="Publish">
    <Copy SourceFiles="web.config" DestinationFolder="$(PublishDir)" />
  </Target>
  ```

## Install the URL Rewrite Module

The [URL Rewrite Module](https://www.iis.net/downloads/microsoft/url-rewrite) is required to rewrite URLs. The module isn't installed by default, and it isn't available for install as a Web Server (IIS) role service feature. The module must be downloaded from the IIS website. Use the Web Platform Installer to install the module:

1. Locally, navigate to the [URL Rewrite Module downloads page](https://www.iis.net/downloads/microsoft/url-rewrite#additionalDownloads). For the English version, select **WebPI** to download the WebPI installer. For other languages, select the appropriate architecture for the server (x86/x64) to download the installer.
2. Copy the installer to the server. Run the installer. Select the **Install** button and accept the license terms. A server restart isn't required after the install completes.

## Configure the website

Set the website's **Physical path** to the app's folder. The folder contains:

* The `web.config` file that IIS uses to configure the website, including the required redirect rules and file content types.
* The app's static asset folder.

## Host as an IIS sub-app

If a standalone app is hosted as an IIS sub-app, perform either of the following:

* Disable the inherited ASP.NET Core Module handler.

  Remove the handler in the Blazor app's published `web.config` file by adding a `<handlers>` section to the `<system.webServer>` section of the file:

  ```
  <handlers>
    <remove name="aspNetCore" />
  </handlers>
  ```

* Disable inheritance of the root (parent) app's `<system.webServer>` section using a `<location>` element with `inheritInChildApplications` set to `false`:

  ```
  <?xml version="1.0" encoding="utf-8"?>
  <configuration>
    <location path="." inheritInChildApplications="false">
      <system.webServer>
        <handlers>
          <add name="aspNetCore" ... />
        </handlers>
        <aspNetCore ... />
      </system.webServer>
    </location>
  </configuration>
  ```

  Note

  Disabling inheritance of the root (parent) app's `<system.webServer>` section is the default configuration for published apps using the .NET SDK.

Removing the handler or disabling inheritance is performed in addition to [configuring the app's base path](../app-base-path?view=aspnetcore-10.0). Set the app base path in the app's `index.html` file to the IIS alias used when configuring the sub-app in IIS.

Configure the app's base path by following the guidance in [ASP.NET Core Blazor app base path](../app-base-path?view=aspnetcore-10.0).

## Brotli and Gzip compression

*This section only applies to standalone Blazor WebAssembly apps.*

*This section only applies to standalone Blazor WebAssembly apps. Hosted Blazor apps use a default ASP.NET Core app `web.config` file, not the file linked in this section.*

IIS can be configured via `web.config` to serve Brotli or Gzip compressed Blazor assets for standalone Blazor WebAssembly apps. For an example configuration file, see [`web.config`](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/samples/web.config?raw=true).

Additional configuration of the example `web.config` file might be required in the following scenarios:

* The app's specification calls for either of the following:

  * Serving compressed files that aren't configured by the example `web.config` file.
  * Serving compressed files configured by the example `web.config` file in an uncompressed format.

* The server's IIS configuration (for example, `applicationHost.config`) provides server-level IIS defaults. Depending on the server-level configuration, the app might require a different IIS configuration than what the example `web.config` file contains.

For more information on custom `web.config` files, see the [Use of a custom `web.config`](#use-of-a-custom-webconfig) section.

## Troubleshooting

If a *500 - Internal Server Error* is received and IIS Manager throws errors when attempting to access the website's configuration, confirm that the URL Rewrite Module is installed. When the module isn't installed, the `web.config` file can't be parsed by IIS. This prevents the IIS Manager from loading the website's configuration and the website from serving Blazor's static files.

For more information on troubleshooting deployments to IIS, see [Troubleshoot ASP.NET Core on Azure App Service and IIS](../../../test/troubleshoot-azure-iis?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

*Welcome to Blazor!*

Blazor is a [.NET](/en-us/dotnet/standard/tour) frontend web framework that supports both server-side rendering and client interactivity in a single programming model:

Blazor is a framework for building interactive client-side web UI with [.NET](/en-us/dotnet/standard/tour):

* Create rich interactive UIs using [C#](/en-us/dotnet/csharp/).
* Share server-side and client-side app logic written in .NET.
* Render the UI as HTML and CSS for wide browser support, including mobile browsers.
* Build hybrid desktop and mobile apps with .NET and Blazor.

- Create rich interactive UIs using [C#](/en-us/dotnet/csharp/).
- Share server-side and client-side app logic written in .NET.
- Render the UI as HTML and CSS for wide browser support, including mobile browsers.

Using .NET for client-side web development offers the following advantages:

* Write code in C#, which can improve productivity in app development and maintenance.
* Leverage the existing .NET ecosystem of [.NET libraries](/en-us/dotnet/standard/class-libraries).
* Benefit from .NET's performance, reliability, and security.
* Stay productive on Windows, Linux, or macOS with a development environment, such as [Visual Studio](https://visualstudio.microsoft.com/) or [Visual Studio Code](https://code.visualstudio.com/). Integrate with modern hosting platforms, such as [Docker](/en-us/dotnet/standard/microservices-architecture/container-docker-introduction/index).
* Build on a common set of languages, frameworks, and tools that are stable, feature-rich, and easy to use.

Note

For a Blazor quick start tutorial, see [Build your first Blazor app](https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/intro).

## Components

Blazor apps are based on *components*. A component in Blazor is an element of UI, such as a page, dialog, or data entry form.

Components are .NET C# classes built into [.NET assemblies](/en-us/dotnet/standard/assembly/) that:

* Define flexible UI rendering logic.
* Handle user events.
* Can be nested and reused.
* Can be shared and distributed as [Razor class libraries](../razor-pages/ui-class?view=aspnetcore-10.0) or [NuGet packages](/en-us/nuget/what-is-nuget).

The component class is usually written in the form of a [Razor](../mvc/views/razor?view=aspnetcore-10.0) markup page with a `.razor` file extension. Components in Blazor are formally referred to as *Razor components*, informally as *Blazor components*. Razor is a syntax for combining HTML markup with C# code designed for developer productivity. Razor allows you to switch between HTML markup and C# in the same file with [IntelliSense](/en-us/visualstudio/ide/using-intellisense) programming support in Visual Studio.

Blazor uses natural HTML tags for UI composition. The following Razor markup demonstrates a component that increments a counter when the user selects a button.

```
<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

Components render into an in-memory representation of the browser's [Document Object Model (DOM)](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction) called a *render tree*, which is used to update the UI in a flexible and efficient way.

## Build a full-stack web app with Blazor

Blazor Web Apps provide a component-based architecture with server-side rendering and full client-side interactivity in a single solution, where you can switch between server-side and client-side rendering modes and even mix them in the same page.

Blazor Web Apps can quickly deliver UI to the browser by statically rendering HTML content from the server in response to requests. The page loads fast because UI rendering is performed quickly on the server without the need to download a large JavaScript bundle. Blazor can also further improve the user experience with various progressive enhancements to server rendering, such as enhanced navigation with form posts and streaming rendering of asynchronously-generated content.

Blazor supports *interactive* server-side rendering (interactive SSR), where UI interactions are handled from the server over a real-time connection with the browser. Interactive SSR enables a rich user experience like one would expect from a client app but without the need to create API endpoints to access server resources. Page content for interactive pages is prerendered, where content on the server is initially generated and sent to the client without enabling event handlers for rendered controls. The server outputs the HTML UI of the page as soon as possible in response to the initial request, which makes the app feel more responsive to users.

Blazor Web Apps support interactivity with client-side rendering (CSR) that relies on a .NET runtime built with [WebAssembly](https://webassembly.org) that you can download with your app. When running Blazor on WebAssembly, your .NET code can access the full functionality of the browser and interop with JavaScript. Your .NET code runs in the browser's security sandbox with the protections that the sandbox provides against malicious actions on the client machine.

Blazor apps can entirely target running on WebAssembly in the browser without the involvement of a server. For a *standalone Blazor WebAssembly app*, assets are deployed as static files to a web server or service capable of serving static content to clients. Once downloaded, standalone Blazor WebAssembly apps can be cached and executed offline as a Progressive Web App (PWA).

## Build a native client app with Blazor Hybrid

*Blazor Hybrid* enables using Razor components in a native client app with a blend of native and web technologies for web, mobile, and desktop platforms. Code runs natively in the .NET process and renders web UI to an embedded Web View control using a local interop channel. WebAssembly isn't used in Hybrid apps. Hybrid apps are built with [.NET Multi-platform App UI (.NET MAUI)](/en-us/dotnet/maui/what-is-maui), which is a cross-platform framework for creating native mobile and desktop apps with C# and XAML.

The Blazor Hybrid supports [Windows Presentation Foundation (WPF)](/en-us/dotnet/desktop/wpf/overview/) and [Windows Forms](/en-us/dotnet/desktop/winforms/overview/) to transition apps from earlier technology to .NET MAUI.

## Blazor Server

Blazor Server provides support for hosting Razor components on the server in an ASP.NET Core app. UI updates are handled over a [SignalR](../signalr/introduction?view=aspnetcore-10.0) connection.

The runtime stays on the server and handles:

* Executing the app's C# code.
* Sending UI events from the browser to the server.
* Applying UI updates to a rendered component that are sent back by the server.

The connection used by Blazor Server to communicate with the browser is also used to handle JavaScript interop calls.

Blazor Server apps render content differently than traditional models for rendering UI in ASP.NET Core apps using Razor views or Razor Pages. Both models use the [Razor language](../mvc/views/razor?view=aspnetcore-10.0) to describe HTML content for rendering, but they significantly differ in *how* markup is rendered.

When a Razor Page or view is rendered, every line of Razor code emits HTML in text form. After rendering, the server disposes of the page or view instance, including any state that was produced. When another request for the page occurs, the entire page is rerendered to HTML again and sent to the client.

Blazor Server produces a graph of components to display similar to an HTML or XML DOM. The component graph includes state held in properties and fields. Blazor evaluates the component graph to produce a binary representation of the markup, which is sent to the client for rendering. After the connection is made between the client and the server, the component's static prerendered elements are replaced with interactive elements. Prerendering content on the server in order to load HTML content on the client quickly makes the app feel more responsive to the client.

After the components are interactive on the client, UI updates are triggered by user interaction and app events. When an update occurs, the component graph is rerendered, and a UI *diff* (difference) is calculated. This diff is the smallest set of DOM edits required to update the UI on the client. The diff is sent to the client in a binary format and applied by the browser.

A component is disposed after the user navigates away from the component.

## Blazor WebAssembly

Blazor WebAssembly is a [single-page app (SPA) framework](/en-us/dotnet/architecture/modern-web-apps-azure/choose-between-traditional-web-and-single-page-apps) for building interactive client-side web apps with .NET.

Running .NET code inside web browsers is made possible by [WebAssembly](https://webassembly.org) (abbreviated `wasm`). WebAssembly is a compact bytecode format optimized for fast download and maximum execution speed. WebAssembly is an open web standard and supported in web browsers without plugins. WebAssembly works in all modern web browsers, including mobile browsers.

WebAssembly code can access the full functionality of the browser via JavaScript, called *JavaScript interoperability*, often shortened to *JavaScript interop* or *JS interop*. .NET code executed via WebAssembly in the browser runs in the browser's JavaScript sandbox with the protections that the sandbox provides against malicious actions on the client machine.

When a Blazor WebAssembly app is built and run:

* C# code files and Razor files are compiled into .NET assemblies.
* The assemblies and the [.NET runtime](/en-us/dotnet/framework/get-started/overview) are downloaded to the browser.
* Blazor WebAssembly bootstraps the .NET WebAssembly runtime and configures the runtime to load the assemblies for the app. The runtime uses JavaScript interop to handle DOM manipulation and browser API calls.

The size of the published app, its *payload size*, is a critical performance factor for an app's usability. A large app takes a relatively long time to download to a browser, which diminishes the user experience. Blazor WebAssembly optimizes payload size to reduce download times:

* Unused code is stripped out of the app when it's published by the [Intermediate Language (IL) Trimmer](host-and-deploy/configure-trimmer?view=aspnetcore-10.0).
* HTTP responses are compressed.
* The .NET runtime and assemblies are cached in the browser.

## Blazor Hybrid

Hybrid apps use a blend of native and web technologies. A *Blazor Hybrid* app uses Blazor in a native client app. Razor components run natively in the .NET process and render web UI to an embedded Web View control using a local interop channel. WebAssembly isn't used in Hybrid apps. Hybrid apps encompass the following technologies:

* [.NET Multi-platform App UI (.NET MAUI)](/en-us/dotnet/maui/what-is-maui): A cross-platform framework for creating native mobile and desktop apps with C# and XAML.
* [Windows Presentation Foundation (WPF)](/en-us/dotnet/desktop/wpf/overview/): A UI framework that is resolution-independent and uses a vector-based rendering engine, built to take advantage of modern graphics hardware.
* [Windows Forms](/en-us/dotnet/desktop/winforms/overview/): A UI framework that creates rich desktop client apps for Windows. The Windows Forms development platform supports a broad set of app development features, including controls, graphics, data binding, and user input.

## JavaScript interop

For apps that require third-party JavaScript libraries and access to browser APIs, components interoperate with JavaScript. Components are capable of using any library or API that JavaScript is able to use. C# code can [call into JavaScript code](javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0), and JavaScript code can [call into C# code](javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0).

## Code sharing and .NET Standard

Blazor implements the [.NET Standard](/en-us/dotnet/standard/net-standard), which enables Blazor projects to reference libraries that conform to .NET Standard specifications. .NET Standard is a formal specification of .NET APIs that are common across .NET implementations. .NET Standard class libraries can be shared across different .NET platforms, such as Blazor, .NET Framework, .NET Core, .NET Multi-platform App UI (.NET MAUI), Mono, and Unity.

APIs that aren't applicable inside of a web browser (for example, accessing the file system, opening a socket, and threading) throw a [PlatformNotSupportedException](/en-us/dotnet/api/system.platformnotsupportedexception).

## Next steps

[Blazor Tutorial - Build your first Blazor app](https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/intro) [ASP.NET Core Blazor supported platforms](supported-platforms?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/generic-type-support
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/generic-type-support.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Razor component generic type support

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes generic type support in Razor components.

If you're new to generic types, see [Generic classes and methods (C# Guide)](/en-us/dotnet/csharp/fundamentals/types/generics) for general guidance on the use of generics before reading this article.

The example code in this article is only available for the latest .NET release in the [Blazor sample apps](../fundamentals/?view=aspnetcore-10.0#sample-apps).

## Generic type parameter support

The [`@typeparam`](../../mvc/views/razor?view=aspnetcore-10.0#typeparam) directive declares a [generic type parameter](/en-us/dotnet/csharp/programming-guide/generics/generic-type-parameters) for the generated component class:

```
@typeparam TItem
```

C# syntax with [`where`](/en-us/dotnet/csharp/language-reference/keywords/where-generic-type-constraint) type constraints is supported:

```
@typeparam TEntity where TEntity : IEntity
```

A [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) generic type is supported, but components aren't supported as the generic type. To render components by type, consider using a [DynamicComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent). For more information, see [Dynamically-rendered ASP.NET Core Razor components](dynamiccomponent?view=aspnetcore-10.0).

In the following example, the `ListItems1` component is generically typed as `TExample`, which represents the type of the `ExampleList` collection.

`ListItems1.razor`:

```
@typeparam TExample

<h2>List Items 1</h2>

@if (ExampleList is not null)
{
    <ul style="color:@Color">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>

    <p>
        Type of <code>TExample</code>: @typeof(TExample)
    </p>
}

@code {
    [Parameter]
    public string? Color { get; set; }

    [Parameter]
    public IEnumerable<TExample>? ExampleList { get; set; }
}
```

```
@typeparam TExample

<h2>List Items 1</h2>

@if (ExampleList is not null)
{
    <ul style="color:@Color">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>

    <p>
        Type of <code>TExample</code>: @typeof(TExample)
    </p>
}

@code {
    [Parameter]
    public string? Color { get; set; }

    [Parameter]
    public IEnumerable<TExample>? ExampleList { get; set; }
}
```

The following component renders two `ListItems1` components:

* String or integer data is assigned to the `ExampleList` parameter of each component.
* Type `string` or `int` that matches the type of the assigned data is set for the type parameter (`TExample`) of each component.

`Generics1.razor`:

```
@page "/generics-1"

<PageTitle>Generics 1</PageTitle>

<h1>Generic Type Example 1</h1>

<ListItems1 Color="blue"
            ExampleList="@(new List<string> { "Item 1", "Item 2" })"
            TExample="string" />

<ListItems1 Color="red"
            ExampleList="@(new List<int> { 1, 2 })"
            TExample="int" />
```

```
@page "/generics-1"

<PageTitle>Generics 1</PageTitle>

<h1>Generic Type Example 1</h1>

<ListItems1 Color="blue"
            ExampleList="@(new List<string> { "Item 1", "Item 2" })"
            TExample="string" />

<ListItems1 Color="red"
            ExampleList="@(new List<int> { 1, 2 })"
            TExample="int" />
```

For more information, see [Razor syntax reference for ASP.NET Core](../../mvc/views/razor?view=aspnetcore-10.0#typeparam). For an example of generic typing with templated components, see [ASP.NET Core Blazor templated components](templated-components?view=aspnetcore-10.0).

## Cascaded generic type support

An ancestor component can cascade a type parameter by name to descendants using the [`[CascadingTypeParameter]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingtypeparameterattribute). This attribute allows a generic type inference to use the specified type parameter automatically with descendants that have a type parameter with the same name.

By adding `@attribute [CascadingTypeParameter(...)]` to a component, the specified generic type argument is automatically used by descendants that:

* Are nested as child content for the component in the same `.razor` document.
* Also declare a [`@typeparam`](../../mvc/views/razor?view=aspnetcore-10.0#typeparam) with the exact same name.
* Don't have another value explicitly supplied or implicitly inferred for the type parameter. If another value is supplied or inferred, it takes precedence over the cascaded generic type.

When receiving a cascaded type parameter, components obtain the parameter value from the closest ancestor that has a [`[CascadingTypeParameter]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingtypeparameterattribute) with a matching name. Cascaded generic type parameters are overridden within a particular subtree.

Matching is only performed by name. Therefore, we recommend avoiding a cascaded generic type parameter with a generic name, for example `T` or `TItem`. If a developer opts into cascading a type parameter, they're implicitly promising that its name is unique enough not to clash with other cascaded type parameters from unrelated components.

Generic types can be cascaded to child components with either of the following approaches for ancestor (parent) components, which are demonstrated in the following two sub-sections:

* Explicitly set the cascaded generic type.
* Infer the cascaded generic type.

The following subsections provide examples of the preceding approaches using the following `ListDisplay1` component. The component receives and renders list data generically typed as `TExample`. To make each instance of `ListDisplay1` stand out, an additional component parameter controls the color of the list.

`ListDisplay1.razor`:

```
@typeparam TExample

@if (ExampleList is not null)
{
    <ul style="color:@Color">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>
}

@code {
    [Parameter]
    public string? Color { get; set; }

    [Parameter]
    public IEnumerable<TExample>? ExampleList { get; set; }
}
```

```
@typeparam TExample

@if (ExampleList is not null)
{
    <ul style="color:@Color">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>
}

@code {
    [Parameter]
    public string? Color { get; set; }

    [Parameter]
    public IEnumerable<TExample>? ExampleList { get; set; }
}
```

### Explicit generic types based on ancestor components

The demonstration in this section cascades a type explicitly for `TExample`.

Note

This section uses the preceding `ListDisplay1` component in the [Cascaded generic type support](#cascaded-generic-type-support) section.

The following `ListItems2` component receives data and cascades a generic type parameter named `TExample` to its descendent components. In the upcoming parent component, the `ListItems2` component is used to display list data with the preceding `ListDisplay1` component.

`ListItems2.razor`:

```
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample

<h2>List Items 2</h2>

@ChildContent

<p>
    Type of <code>TExample</code>: @typeof(TExample)
</p>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

```
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample

<h2>List Items 2</h2>

@ChildContent

<p>
    Type of <code>TExample</code>: @typeof(TExample)
</p>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

The following parent component sets the child content ([RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment)) of two `ListItems2` components specifying the `ListItems2` types (`TExample`), which are cascaded to child components. `ListDisplay1` components are rendered with the list item data shown in the example. String data is used with the first `ListItems2` component, and integer data is used with the second `ListItems2` component.

`Generics2.razor`:

```
@page "/generics-2"

<PageTitle>Generics 2</PageTitle>

<h1>Generic Type Example 2</h1>

<ListItems2 TExample="string">
    <ListDisplay1 Color="blue" 
                  ExampleList="@(new List<string> { "Item 1", "Item 2" })" />
    <ListDisplay1 Color="red" 
                  ExampleList="@(new List<string> { "Item 3", "Item 4" })" />
</ListItems2>

<ListItems2 TExample="int">
    <ListDisplay1 Color="blue" 
                  ExampleList="@(new List<int> { 1, 2 })" />
    <ListDisplay1 Color="red" 
                  ExampleList="@(new List<int> { 3, 4 })" />
</ListItems2>
```

```
@page "/generics-2"

<PageTitle>Generics 2</PageTitle>

<h1>Generic Type Example 2</h1>

<ListItems2 TExample="string">
    <ListDisplay1 Color="blue" 
                  ExampleList="@(new List<string> { "Item 1", "Item 2" })" />
    <ListDisplay1 Color="red" 
                  ExampleList="@(new List<string> { "Item 3", "Item 4" })" />
</ListItems2>

<ListItems2 TExample="int">
    <ListDisplay1 Color="blue" 
                  ExampleList="@(new List<int> { 1, 2 })" />
    <ListDisplay1 Color="red" 
                  ExampleList="@(new List<int> { 3, 4 })" />
</ListItems2>
```

Specifying the type explicitly also allows the use of [cascading values and parameters](cascading-values-and-parameters?view=aspnetcore-10.0) to provide data to child components, as the following demonstration shows.

`ListDisplay2.razor`:

```
@typeparam TExample

@if (ExampleList is not null)
{
    <ul style="color:@Color">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>
}

@code {
    [Parameter]
    public string? Color { get; set; }

    [CascadingParameter]
    private IEnumerable<TExample>? ExampleList { get; set; }
}
```

```
@typeparam TExample

@if (ExampleList is not null)
{
    <ul style="color:@Color">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>
}

@code {
    [Parameter]
    public string? Color { get; set; }

    [CascadingParameter]
    private IEnumerable<TExample>? ExampleList { get; set; }
}
```

`ListItems3.razor`:

```
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample

<h2>List Items 3</h2>

@ChildContent

@if (ExampleList is not null)
{
    <ul style="color:green">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>

    <p>
        Type of <code>TExample</code>: @typeof(TExample)
    </p>
}

@code {
    [CascadingParameter]
    private IEnumerable<TExample>? ExampleList { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

```
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample

<h2>List Items 3</h2>

@ChildContent

@if (ExampleList is not null)
{
    <ul style="color:green">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>

    <p>
        Type of <code>TExample</code>: @typeof(TExample)
    </p>
}

@code {
    [CascadingParameter]
    private IEnumerable<TExample>? ExampleList { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

When cascading the data in the following example, the type must be provided to the component.

`Generics3.razor`:

```
@page "/generics-3"

<PageTitle>Generics 3</PageTitle>

<h1>Generic Type Example 3</h1>

<CascadingValue Value="stringData">
    <ListItems3 TExample="string">
        <ListDisplay2 Color="blue" />
        <ListDisplay2 Color="red" />
    </ListItems3>
</CascadingValue>

<CascadingValue Value="integerData">
    <ListItems3 TExample="int">
        <ListDisplay2 Color="blue" />
        <ListDisplay2 Color="red" />
    </ListItems3>
</CascadingValue>

@code {
    private List<string> stringData = new() { "Item 1", "Item 2" };
    private List<int> integerData = new() { 1, 2 };
}
```

```
@page "/generics-3"

<PageTitle>Generics 3</PageTitle>

<h1>Generic Type Example 3</h1>

<CascadingValue Value="stringData">
    <ListItems3 TExample="string">
        <ListDisplay2 Color="blue" />
        <ListDisplay2 Color="red" />
    </ListItems3>
</CascadingValue>

<CascadingValue Value="integerData">
    <ListItems3 TExample="int">
        <ListDisplay2 Color="blue" />
        <ListDisplay2 Color="red" />
    </ListItems3>
</CascadingValue>

@code {
    private List<string> stringData = new() { "Item 1", "Item 2" };
    private List<int> integerData = new() { 1, 2 };
}
```

When multiple generic types are cascaded, values for all generic types in the set must be passed. In the following example, `TItem`, `TValue`, and `TEdit` are `GridColumn` generic types, but the parent component that places `GridColumn` doesn't specify the `TItem` type:

```
<GridColumn TValue="string" TEdit="TextEdit" />
```

The preceding example generates a compile-time error that the `GridColumn` component is missing the `TItem` type parameter. Valid code specifies all of the types:

```
<GridColumn TValue="string" TEdit="TextEdit" TItem="User" />
```

### Infer generic types based on ancestor components

The demonstration in this section cascades a type inferred for `TExample`.

Note

This section uses the `ListDisplay` component in the [Cascaded generic type support](#cascaded-generic-type-support) section.

`ListItems4.razor`:

```
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample

<h2>List Items 4</h2>

@ChildContent

@if (ExampleList is not null)
{
    <ul style="color:green">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>

    <p>
        Type of <code>TExample</code>: @typeof(TExample)
    </p>
}

@code {
    [Parameter]
    public IEnumerable<TExample>? ExampleList { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

```
@attribute [CascadingTypeParameter(nameof(TExample))]
@typeparam TExample

<h2>List Items 4</h2>

@ChildContent

@if (ExampleList is not null)
{
    <ul style="color:green">
        @foreach (var item in ExampleList)
        {
            <li>@item</li>
        }
    </ul>

    <p>
        Type of <code>TExample</code>: @typeof(TExample)
    </p>
}

@code {
    [Parameter]
    public IEnumerable<TExample>? ExampleList { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

The following component with inferred cascaded types provides different data for display.

`Generics4.razor`:

```
@page "/generics-4"

<PageTitle>Generics 4</PageTitle>

<h1>Generic Type Example 4</h1>

<ListItems4 ExampleList="@(new List<string> { "Item 5", "Item 6" })">
    <ListDisplay1 Color="blue" 
                  ExampleList="@(new List<string> { "Item 1", "Item 2" })" />
    <ListDisplay1 Color="red" 
                  ExampleList="@(new List<string> { "Item 3", "Item 4" })" />
</ListItems4>

<ListItems4 ExampleList="@(new List<int> { 5, 6 })">
    <ListDisplay1 Color="blue" 
                  ExampleList="@(new List<int> { 1, 2 })" />
    <ListDisplay1 Color="red" 
                  ExampleList="@(new List<int> { 3, 4 })" />
</ListItems4>
```

```
@page "/generics-4"

<PageTitle>Generics 4</PageTitle>

<h1>Generic Type Example 4</h1>

<ListItems4 ExampleList="@(new List<string> { "Item 5", "Item 6" })">
    <ListDisplay1 Color="blue" 
                  ExampleList="@(new List<string> { "Item 1", "Item 2" })" />
    <ListDisplay1 Color="red" 
                  ExampleList="@(new List<string> { "Item 3", "Item 4" })" />
</ListItems4>

<ListItems4 ExampleList="@(new List<int> { 5, 6 })">
    <ListDisplay1 Color="blue" 
                  ExampleList="@(new List<int> { 1, 2 })" />
    <ListDisplay1 Color="red" 
                  ExampleList="@(new List<int> { 3, 4 })" />
</ListItems4>
```

The following component with inferred cascaded types provides the same data for display. The following example directly assigns the data to the components.

`Generics5.razor`:

```
@page "/generics-5"

<PageTitle>Generics 5</PageTitle>

<h1>Generic Type Example 5</h1>

<ListItems4 ExampleList="stringData">
    <ListDisplay1 Color="blue" ExampleList="stringData" />
    <ListDisplay1 Color="red" ExampleList="stringData" />
</ListItems4>

<ListItems4 ExampleList="integerData">
    <ListDisplay1 Color="blue" ExampleList="integerData" />
    <ListDisplay1 Color="red" ExampleList="integerData" />
</ListItems4>

@code {
    private List<string> stringData = new() { "Item 1", "Item 2" };
    private List<int> integerData = new() { 1, 2 };
}
```

```
@page "/generics-5"

<PageTitle>Generics 5</PageTitle>

<h1>Generic Type Example 5</h1>

<ListItems4 ExampleList="stringData">
    <ListDisplay1 Color="blue" ExampleList="stringData" />
    <ListDisplay1 Color="red" ExampleList="stringData" />
</ListItems4>

<ListItems4 ExampleList="integerData">
    <ListDisplay1 Color="blue" ExampleList="integerData" />
    <ListDisplay1 Color="red" ExampleList="integerData" />
</ListItems4>

@code {
    private List<string> stringData = new() { "Item 1", "Item 2" };
    private List<int> integerData = new() { 1, 2 };
}
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host and deploy ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to host and deploy Blazor apps.

## Publish the app

Apps are published for deployment in Release configuration.

Note

Publish a hosted Blazor WebAssembly [solution](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) from the **Server** project.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code and .NET CLI](#tabpanel_1_visual-studio-code-dotnet-cli)

1. Select the **Publish {APPLICATION}** command from the **Build** menu, where the `{APPLICATION}` placeholder the app's name.
2. Select the *publish target*. To publish locally, select **Folder**. Select **Next**.
3. When publishing locally, accept the default folder location or specify a different location. Select **Finish** to save the profile. Select **Close**.
4. To clean the target's publish folder prior to publishing the app, select **Show all settings**. Select **Settings** > **File Publish Options** > **Delete all existing files prior to publish**. Select **Save**.
5. Select the **Publish** button.

Open a command shell to the project's root directory.

Use the [`dotnet publish`](/en-us/dotnet/core/tools/dotnet-publish) command to publish the app:

```
dotnet publish
```

Prior to .NET 8, the default publish configuration is `Debug`. When publishing an app that targets .NET 7 or earlier, pass the `-c|--configuration` option to the command with "`Release`" to publish in `Release` configuration:

```
dotnet publish -c Release
```

Publishing the app triggers a [restore](/en-us/dotnet/core/tools/dotnet-restore) of the project's dependencies and [builds](/en-us/dotnet/core/tools/dotnet-build) the project before creating the assets for deployment. As part of the build process, unused methods and assemblies are removed to reduce app download size and load times.

## Empty the target publish folder

When using the [`dotnet publish`](/en-us/dotnet/core/tools/dotnet-publish) command in a command shell to publish an app, the command generates the necessary files for deployment based on the current state of the project and places the files into the specified output folder. The command doesn't automatically clean the target folder before publishing the app.

To empty the target folder automatically before the app is published, add the following MSBuild target to the app's project file (`.csproj`) under the root `<Project>` element:

```
<Target Name="_RemovePublishDirBeforePublishing" BeforeTargets="BeforePublish">
  <RemoveDir Directories="$(PublishDir)" Condition="'$(PublishDir)' != ''" />
</Target>
```

## Default publish locations

* Blazor Web App: The app is published into the `/bin/Release/{TARGET FRAMEWORK}/publish` folder, where the `{TARGET FRAMEWORK}` placeholder is the target framework. Deploy the contents of the `publish` folder to the host.
* Standalone Blazor WebAssembly: The app is published into the `bin/Release/{TARGET FRAMEWORK}/publish` or `bin/Release/{TARGET FRAMEWORK}/browser-wasm/publish` folder. To deploy the app as a static site, copy the contents of the `wwwroot` folder to the static site host.

- Blazor Server: The app is published into the `/bin/Release/{TARGET FRAMEWORK}/publish` folder, where the `{TARGET FRAMEWORK}` placeholder is the target framework.. Deploy the contents of the `publish` folder to the host.

- Blazor WebAssembly

  * Standalone: The app is published into the `/bin/Release/{TARGET FRAMEWORK}/publish` or `bin/Release/{TARGET FRAMEWORK}/browser-wasm/publish` folder. To deploy the app as a static site, copy the contents of the `wwwroot` folder to the static site host.
  * Hosted: The server ASP.NET Core app and client Blazor WebAssembly app are published into the `/bin/Release/{TARGET FRAMEWORK}/publish` folder of the server app, along with any static web assets of the client app. Deploy the contents of the `publish` folder to the host.

## IIS

To host a Blazor app in IIS, see the following resources:

* IIS hosting

  * [Publish an ASP.NET Core app to IIS](../../tutorials/publish-to-iis?view=aspnetcore-10.0)
  * [Host ASP.NET Core on Windows with IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0)

* [Host and deploy ASP.NET Core server-side Blazor apps](server/?view=aspnetcore-10.0): Blazor Web Apps (.NET 8 or later) and Blazor Server apps (.NET 7 or earlier) running on IIS, including IIS with Azure Virtual Machines (VMs) running Windows OS and Azure App Service.

* [Host and deploy ASP.NET Core Blazor WebAssembly with IIS](webassembly/iis?view=aspnetcore-10.0): Standalone Blazor WebAssembly apps (all .NET releases) and hosted Blazor WebAssembly apps (.NET 7 or earlier).

* IIS sub-application hosting

  * Follow the [app base path guidance](app-base-path?view=aspnetcore-10.0) prior to publishing the app. The examples use an app base path of `/CoolApp` and show how to [obtain the base path from app settings or other configuration providers](app-base-path?view=aspnetcore-10.0#obtain-the-app-base-path-from-configuration).
  * Follow the sub-application configuration guidance in [Advanced configuration](../../host-and-deploy/iis/advanced?view=aspnetcore-10.0#sub-applications). The sub-app's folder path under the root site becomes the virtual path of the sub-app. For an app base path of `/CoolApp`, the Blazor app is placed in a folder named `CoolApp` under the root site and the sub-app takes on a virtual path of `/CoolApp`.

Sharing an app pool among ASP.NET Core apps isn't supported, including for Blazor apps. Use one app pool per app when hosting with IIS, and avoid the use of IIS's [virtual directories](/en-us/iis/get-started/planning-your-iis-architecture/understanding-sites-applications-and-virtual-directories-on-iis#virtual-directories) for hosting multiple apps.

One or more Blazor WebAssembly apps hosted by an ASP.NET Core app, known as a [hosted Blazor WebAssembly solution](../hosting-models?view=aspnetcore-10.0#blazor-webassembly), are supported for ***one*** app pool. However, we don't recommend or support assigning a single app pool to multiple hosted Blazor WebAssembly solutions or in sub-app hosting scenarios.

For more information on *solutions*, see [Tooling for ASP.NET Core Blazor](../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln).

## JavaScript bundler support

The Blazor runtime relies on JavaScript (JS) files, the .NET runtime compiled into WebAssembly code, and managed assemblies packed as WebAssembly files. When a Blazor app is built, the Blazor runtime depends on these files from different build locations. Due to this constraint, Blazor's build output isn't compatible with JS bundlers, such as [Gulp](https://gulpjs.com), [Webpack](https://webpack.js.org), and [Rollup](https://rollupjs.org/).

To produce build output compatible with JS bundlers *during publish*, set the `WasmBundlerFriendlyBootConfig` MSBuild property to `true` in the app's project file:

```
<WasmBundlerFriendlyBootConfig>true</WasmBundlerFriendlyBootConfig>
```

Important

This feature only produces the bundler-friendly output when publishing the app.

The output isn't directly runnable in the browser, but it can be consumed by JS tools to bundle JS files with the rest of the developer-supplied scripts.

When `WasmBundlerFriendlyBootConfig` is enabled, the produced JS contains `import` directives for all of the assets in the app, which makes the dependencies visible for the bundler. Many of the assets aren't loadable by the browser, but bundlers usually can be configured to recognize the assets by their file type to handle loading. For details on how to configure your bundler, refer to the bundler's documentation.

Note

Bundling build output should be possible by mapping imports to individual file locations using a JS bundler custom plugin. We don't provide such a plugin at the moment.

Note

Replacing the `files` plugin with `url`, all of the app's JS files, including the Blazor-WebAssembly runtime (base64 encoded in the JS), are bundled into the output. The size of the file is significantly larger (for example, 300% larger) than when the files are curated with the `files` plugin, so we don't recommend using the `url` plugin as a general practice when producing bundler-friendly output for JS bundler processing.

The following sample apps are based on [Rollup](https://rollupjs.org/). Similar concepts apply when using other JS bundlers.

Demonstration sample apps for Blazor WebAssembly in a React app (`BlazorWebAssemblyReact`) and .NET on WebAssembly in a React app (`DotNetWebAssemblyReact`) for .NET 10 or later are available in the [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples).

## Aspects of Blazor WebAssembly caching apply to Blazor Web Apps

Blazor bundle caching and HTTP caching guidance in the *Blazor WebAssembly* node focus on standalone Blazor WebAssembly apps, but several aspects of client-side caching in these articles also apply to Blazor Web Apps that adopt Interactive WebAssembly or Interactive Auto render modes. If a Blazor Web App that renders content client-side encounters a static asset or bundle caching problem, see the guidance in these articles to troubleshoot the problem:

* [ASP.NET Core Blazor WebAssembly caching and integrity check failures](webassembly/bundle-caching-and-integrity-check-failures?view=aspnetcore-10.0)
* [Avoid HTTP caching issues when upgrading ASP.NET Core Blazor apps](webassembly/http-caching-issues?view=aspnetcore-10.0)

## Blazor Server `MapFallbackToPage` configuration

*This section only applies to Blazor Server apps. [MapFallbackToPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.mapfallbacktopage) isn't supported in Blazor Web Apps and Blazor WebAssembly apps.*

In scenarios where an app requires a separate area with custom resources and Razor components:

* Create a folder within the app's `Pages` folder to hold the resources. For example, an administrator section of an app is created in a new folder named `Admin` (`Pages/Admin`).

* Create a root page (`_Host.cshtml`) for the area. For example, create a `Pages/Admin/_Host.cshtml` file from the app's main root page (`Pages/_Host.cshtml`). Don't provide an `@page` directive in the Admin `_Host` page.

* Add a layout to the area's folder (for example, `Pages/Admin/_Layout.razor`). In the layout for the separate area, set the `<base>` tag `href` to match the area's folder (for example, `<base href="/Admin/" />`). For demonstration purposes, add `~/` to the static resources in the page. For example:

  * `~/css/bootstrap/bootstrap.min.css`
  * `~/css/site.css`
  * `~/BlazorSample.styles.css` (the example app's namespace is `BlazorSample`)
  * `~/_framework/blazor.server.js` (Blazor script)

* If the area should have its own static asset folder, add the folder and specify its location to static file middleware in `Program.cs` (for example, `app.UseStaticFiles("/Admin/wwwroot")`).

* Razor components are added to the area's folder. At a minimum, add an `Index` component to the area folder with the correct `@page` directive for the area. For example, add a `Pages/Admin/Index.razor` file based on the app's default `Pages/Index.razor` file. Indicate the Admin area as the route template at the top of the file (`@page "/admin"`). Add additional components as needed. For example, `Pages/Admin/Component1.razor` with an `@page` directive and route template of `@page "/admin/component1`.

* In `Program.cs`, call [MapFallbackToPage](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.mapfallbacktopage) for the area's request path immediately before the fallback root page path to the `_Host` page:

  ```
  ...
  app.UseRouting();

  app.MapBlazorHub();
  app.MapFallbackToPage("~/Admin/{*clientroutes:nonfile}", "/Admin/_Host");
  app.MapFallbackToPage("/_Host");

  app.Run();
  ```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/server/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/server/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host and deploy server-side Blazor apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to host and deploy server-side Blazor apps (Blazor Web Apps and Blazor Server apps) using ASP.NET Core.

## Host configuration values

Server-side Blazor apps can accept [Generic Host configuration values](../../../fundamentals/host/generic-host?view=aspnetcore-10.0#host-configuration).

## Deployment

Using a server-side hosting model, Blazor is executed on the server from within an ASP.NET Core app. UI updates, event handling, and JavaScript calls are handled over a [SignalR](../../../signalr/introduction?view=aspnetcore-10.0) connection.

A web server capable of hosting an ASP.NET Core app is required. Visual Studio includes a server-side app project template. For more information on Blazor project templates, see [ASP.NET Core Blazor project structure](../../project-structure?view=aspnetcore-10.0).

Publish an app in Release configuration and deploy the contents of the `bin/Release/{TARGET FRAMEWORK}/publish` folder, where the `{TARGET FRAMEWORK}` placeholder is the target framework.

## Scalability

When considering the scalability of a single server (scale up), the memory available to an app is likely the first resource that the app exhausts as user demands increase. The available memory on the server affects the:

* Number of active circuits that a server can support.
* UI latency on the client.

For guidance on building secure and scalable server-side Blazor apps, see the following resources:

* [Threat mitigation guidance for ASP.NET Core Blazor static server-side rendering](../../security/static-server-side-rendering?view=aspnetcore-10.0)
* [Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering](../../security/interactive-server-side-rendering?view=aspnetcore-10.0)

Each circuit uses approximately 250 KB of memory for a minimal *Hello World*-style app. The size of a circuit depends on the app's code and the state maintenance requirements associated with each component. We recommend that you measure resource demands during development for your app and infrastructure, but the following baseline can be a starting point in planning your deployment target: If you expect your app to support 5,000 concurrent users, consider budgeting at least 1.3 GB of server memory to the app (or \~273 KB per user).

## Blazor WebAssembly static asset preloading

The `ResourcePreloader` component in the `App` component's head content (`App.razor`) is used to reference Blazor static assets. The component is placed after the base URL tag (`<base>`):

```
<ResourcePreloader />
```

A Razor component is used instead of `<link>` elements because:

* The component permits the base URL (`<base>` tag's `href` attribute value) to correctly identify the root of the Blazor app within an ASP.NET Core app.
* The feature can be removed by removing the `ResourcePreloader` component tag from the `App` component. This is helpful in cases where the app is using a [`loadBootResource` callback](../../fundamentals/startup?view=aspnetcore-10.0#load-client-side-boot-resources) to modify URLs.

## SignalR configuration

[SignalR's hosting and scaling conditions](../../../signalr/publish-to-azure-web-app?view=aspnetcore-10.0) apply to Blazor apps that use SignalR.

For more information on SignalR in Blazor apps, including configuration guidance, see [ASP.NET Core Blazor SignalR guidance](../../fundamentals/signalr?view=aspnetcore-10.0).

### Transports

Blazor works best when using [WebSockets](../../../fundamentals/websockets?view=aspnetcore-10.0) as the SignalR transport due to lower latency, better reliability, and improved [security](../../../signalr/security?view=aspnetcore-10.0). [Long Polling](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/TransportProtocols.md#long-polling-server-to-client-only) is used by SignalR when WebSockets isn't available or when the app is explicitly configured to use Long Polling.

A console warning appears if Long Polling is utilized:

> Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection.

### Global deployment and connection failures

Recommendations for global deployments to geographical data centers:

* Deploy the app to the regions where most of the users reside.
* Take into consideration the increased latency for traffic across continents. To control the appearance of the reconnection UI, see [ASP.NET Core Blazor SignalR guidance](../../fundamentals/signalr?view=aspnetcore-10.0#control-when-the-reconnection-ui-appears).
* Consider using the [Azure SignalR Service](#azure-signalr-service).

## Azure App Service

Hosting on Azure App Service requires configuration for WebSockets and session affinity, also called Application Request Routing (ARR) affinity.

Note

A Blazor app on Azure App Service doesn't require [Azure SignalR Service](#azure-signalr-service).

Enable the following for the app's registration in Azure App Service:

* [WebSockets](../../../fundamentals/websockets?view=aspnetcore-10.0) to allow the WebSockets transport to function. The default setting is **Off**.
* Session affinity to route requests from a user back to the same App Service instance. The default setting is **On**.

1. In the Azure portal, navigate to the web app in **App Services**.
2. Open **Settings** > **Configuration**.
3. Set **Web sockets** to **On**.
4. Verify that **Session affinity** is set to **On**.

## Azure SignalR Service

The optional [Azure SignalR Service](../../../signalr/scale?view=aspnetcore-10.0#azure-signalr-service) works in conjunction with the app's SignalR hub for scaling up a server-side app to a large number of concurrent connections. In addition, the service's global reach and high-performance data centers significantly aid in reducing latency due to geography.

The service isn't required for Blazor apps hosted in Azure App Service or Azure Container Apps but can be helpful in other hosting environments:

* To facilitate connection scale out.
* Handle global distribution.

The Azure SignalR Service with SDK [v1.26.1](https://github.com/Azure/azure-signalr/releases/tag/v1.26.1) or later supports [SignalR stateful reconnect](../../../signalr/configuration?view=aspnetcore-10.0#configure-stateful-reconnect) ([WithStatefulReconnect](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilderhttpextensions.withstatefulreconnect)).

In the event that the app uses Long Polling or falls back to Long Polling instead of [WebSockets](../../../fundamentals/websockets?view=aspnetcore-10.0), you may need to configure the maximum poll interval (`MaxPollIntervalInSeconds`, default: 5 seconds, limit: 1-300 seconds), which defines the maximum poll interval allowed for Long Polling connections in the Azure SignalR Service. If the next poll request doesn't arrive within the maximum poll interval, the service closes the client connection.

For guidance on how to add the service as a dependency to a production deployment, see [Publish an ASP.NET Core SignalR app to Azure App Service](../../../signalr/publish-to-azure-web-app?view=aspnetcore-10.0).

For more information, see:

* [Azure SignalR Service](https://azure.microsoft.com/products/signalr-service/)
* [What is Azure SignalR Service?](/en-us/azure/azure-signalr/signalr-overview)
* [ASP.NET Core SignalR production hosting and scaling](../../../signalr/scale?view=aspnetcore-10.0#azure-signalr-service)
* [Publish an ASP.NET Core SignalR app to Azure App Service](../../../signalr/publish-to-azure-web-app?view=aspnetcore-10.0)

## Azure Container Apps

For a deeper exploration of scaling server-side Blazor apps on the Azure Container Apps service, see [Scaling ASP.NET Core Apps on Azure](../../../host-and-deploy/scaling-aspnet-apps/scaling-aspnet-apps?view=aspnetcore-10.0). The tutorial explains how to create and integrate the services required to host apps on Azure Container Apps. Basic steps are also provided in this section.

1. Configure Azure Container Apps service for session affinity by following the guidance in [Session Affinity in Azure Container Apps (Azure documentation)](/en-us/azure/container-apps/sticky-sessions).

2. The ASP.NET Core Data Protection (DP) service must be configured to persist keys in a centralized location that all container instances can access. The keys can be stored in Azure Blob Storage and protected with Azure Key Vault. The DP service uses the keys to deserialize Razor components. To configure the DP service to use Azure Blob Storage and Azure Key Vault, reference the following NuGet packages:

   * [`Azure.Identity`](https://www.nuget.org/packages/Azure.Identity): Provides classes to work with the Azure identity and access management services.
   * [`Microsoft.Extensions.Azure`](https://www.nuget.org/packages/Microsoft.Extensions.Azure): Provides helpful extension methods to perform core Azure configurations.
   * [`Azure.Extensions.AspNetCore.DataProtection.Blobs`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Blobs): Allows storing ASP.NET Core Data Protection keys in Azure Blob Storage so that keys can be shared across several instances of a web app.
   * [`Azure.Extensions.AspNetCore.DataProtection.Keys`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Keys): Enables protecting keys at rest using the Azure Key Vault Key Encryption/Wrapping feature.

   Note

   For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

3. Update `Program.cs` with the following highlighted code:

   ```
   using Azure.Identity;
   using Microsoft.AspNetCore.DataProtection;
   using Microsoft.Extensions.Azure;

   var builder = WebApplication.CreateBuilder(args);
   var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"];
   var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"];

   builder.Services.AddRazorPages();
   builder.Services.AddHttpClient();
   builder.Services.AddServerSideBlazor();

   builder.Services.AddAzureClientsCore();

   builder.Services.AddDataProtection()
                   .PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri),
                                                   new DefaultAzureCredential())
                   .ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI),
                                                   new DefaultAzureCredential());
   var app = builder.Build();

   if (!app.Environment.IsDevelopment())
   {
       app.UseExceptionHandler("/Error");
       app.UseHsts();
   }

   app.UseHttpsRedirection();
   app.UseStaticFiles();

   app.UseRouting();

   app.UseAuthorization();

   app.MapRazorPages();

   app.Run();
   ```

   The preceding changes allow the app to manage the DP service using a centralized, scalable architecture. [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) discovers the container app managed identity after the code is deployed to Azure and uses it to connect to blob storage and the app's key vault.

4. To create the container app managed identity and grant it access to blob storage and a key vault, complete the following steps:

   1. In the Azure portal, navigate to the overview page of the container app.

   2. Select **Service Connector** from the left navigation.

   3. Select **+ Create** from the top navigation.

   4. In the **Create connection** flyout menu, enter the following values:

      * **Container**: Select the container app you created to host your app.
      * **Service type**: Select **Blob Storage**.
      * **Subscription**: Select the subscription that owns the container app.
      * **Connection name**: Enter a name of `scalablerazorstorage`.
      * **Client type**: Select **.NET** and then select **Next**.

   5. Select **System assigned managed identity** and select **Next**.

   6. Use the default network settings and select **Next**.

   7. After Azure validates the settings, select **Create**.

   Repeat the preceding settings for the key vault. Select the appropriate key vault service and key in the **Basics** tab.

Note

The preceding example uses [DefaultAzureCredential](/en-us/dotnet/api/azure.identity.defaultazurecredential) to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as [ManagedIdentityCredential](/en-us/dotnet/api/azure.identity.managedidentitycredential). For more information, see [Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity](/en-us/dotnet/azure/sdk/authentication/system-assigned-managed-identity).

## IIS

When using IIS, enable:

* [WebSockets on IIS](../../../fundamentals/websockets?view=aspnetcore-10.0#enabling-websockets-on-iis).
* [Session affinity with Application Request Routing](/en-us/iis/extensions/configuring-application-request-routing-arr/http-load-balancing-using-application-request-routing).

For more information, see the guidance and external IIS resource cross-links in [Publish an ASP.NET Core app to IIS](../../../tutorials/publish-to-iis?view=aspnetcore-10.0).

## Kubernetes

Create an ingress definition with the following [Kubernetes annotations for session affinity](https://kubernetes.github.io/ingress-nginx/examples/affinity/cookie/):

```
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: <ingress-name>
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
    nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"
```

## Linux with Nginx

Follow the guidance for an [ASP.NET Core SignalR app](../../../signalr/scale?view=aspnetcore-10.0#linux-with-nginx) with the following changes:

* Change the `location` path from `/hubroute` (`location /hubroute { ... }`) to the root path `/` (`location / { ... }`).
* Remove the configuration for proxy buffering (`proxy_buffering off;`) because the setting only applies to [Server-Sent Events (SSE)](https://developer.mozilla.org/docs/Web/API/Server-sent_events), which aren't relevant to Blazor app client-server interactions.

For more information and configuration guidance, consult the following resources:

* [ASP.NET Core SignalR production hosting and scaling](../../../signalr/scale?view=aspnetcore-10.0)

* [Host ASP.NET Core on Linux with Nginx](../../../host-and-deploy/linux-nginx?view=aspnetcore-10.0)

* [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)

* [NGINX as a WebSocket Proxy](https://www.nginx.com/blog/websocket-nginx/)

* [WebSocket proxying](http://nginx.org/docs/http/websocket.html)

* Consult developers on non-Microsoft support forums:

  * [Stack Overflow (tag: `blazor`)](https://stackoverflow.com/questions/tagged/blazor)
  * [ASP.NET Core Slack Team](https://join.slack.com/t/aspnetcore/shared_invite/zt-1mv5487zb-EOZxJ1iqb0A0ajowEbxByQ)
  * [Blazor Gitter](https://gitter.im/aspnet/Blazor)

## Linux with Apache

To host a Blazor app behind Apache on Linux, configure `ProxyPass` for HTTP and WebSockets traffic.

In the following example:

* Kestrel server is running on the host machine.
* The app listens for traffic on port 5000.

```
ProxyPreserveHost   On
ProxyPassMatch      ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass           /_blazor ws://localhost:5000/_blazor
ProxyPass           / http://localhost:5000/
ProxyPassReverse    / http://localhost:5000/
```

Enable the following modules:

```
a2enmod   proxy
a2enmod   proxy_wstunnel
```

Check the browser console for WebSockets errors. Example errors:

* Firefox can't establish a connection to the server at ws\://the-domain-name.tld/\_blazor?id=XXX
* Error: Failed to start the transport 'WebSockets': Error: There was an error with the transport.
* Error: Failed to start the transport 'LongPolling': TypeError: this.transport is undefined
* Error: Unable to connect to the server with any of the available transports. WebSockets failed
* Error: Cannot send data if the connection is not in the 'Connected' State.

For more information and configuration guidance, consult the following resources:

* [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)

* [Apache documentation](https://httpd.apache.org/docs/current/mod/mod_proxy.html)

* Consult developers on non-Microsoft support forums:

  * [Stack Overflow (tag: `blazor`)](https://stackoverflow.com/questions/tagged/blazor)
  * [ASP.NET Core Slack Team](https://join.slack.com/t/aspnetcore/shared_invite/zt-1mv5487zb-EOZxJ1iqb0A0ajowEbxByQ)
  * [Blazor Gitter](https://gitter.im/aspnet/Blazor)

## Measure network latency

[JS interop](../../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0) can be used to measure network latency, as the following example demonstrates.

`MeasureLatency.razor`:

```
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
```

```
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
```

```
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
```

```
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
```

```
@inject IJSRuntime JS

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
```

For a reasonable UI experience, we recommend a sustained UI latency of 250 ms or less.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/startup
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/fundamentals/startup.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor startup

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains Blazor app startup configuration.

For general guidance on ASP.NET Core app configuration for server-side development, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

## Startup process and configuration

The Blazor startup process is automatic and asynchronous via the Blazor script (`blazor.*.js`), where the `*` placeholder is:

* `web` for a Blazor Web App.
* `server` for a Blazor Server app.
* `webassembly` for a Blazor WebAssembly app.

The Blazor startup process is automatic and asynchronous via the Blazor script (`blazor.*.js`), where the `*` placeholder is:

* `server` for a Blazor Server app.
* `webassembly` for a Blazor WebAssembly app.

For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

To manually start Blazor:

Blazor Web App:

* Add an `autostart="false"` attribute and value to the Blazor `<script>` tag.
* Place a script that calls `Blazor.start()` after the Blazor `<script>` tag and inside the closing `</body>` tag.
* Place static server-side rendering (static SSR) options in the `ssr` property.
* Place server-side Blazor-SignalR circuit options in the `circuit` property.
* Place client-side WebAssembly options in the `webAssembly` property.

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  ...
  Blazor.start({
    ssr: {
      ...
    },
    circuit: {
      ...
    },
    webAssembly: {
      ...
    }
  });
  ...
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Standalone Blazor WebAssembly and Blazor Server:

* Add an `autostart="false"` attribute and value to the Blazor `<script>` tag.
* Place a script that calls `Blazor.start()` after the Blazor `<script>` tag and inside the closing `</body>` tag.
* You can provide additional options in the `Blazor.start()` parameter.

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  ...
  Blazor.start({
    ...
  });
  ...
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

## JavaScript initializers

JavaScript (JS) initializers execute logic before and after a Blazor app loads. JS initializers are useful in the following scenarios:

* Customizing how a Blazor app loads.
* Initializing libraries before Blazor starts up.
* Configuring Blazor settings.

JS initializers are detected as part of the build process and imported automatically. Use of JS initializers often removes the need to [manually trigger script functions from the app](startup?view=aspnetcore-10.0#chain-to-the-promise-that-results-from-a-manual-start) when using [Razor class libraries (RCLs)](../components/class-libraries?view=aspnetcore-10.0).

To define a JS initializer, add a JS module to the project named `{NAME}.lib.module.js`, where the `{NAME}` placeholder is the assembly name, library name, or package identifier. Place the file in the project's web root, which is typically the `wwwroot` folder.

For Blazor Web Apps:

* `beforeWebStart(options)`: Called before the Blazor Web App starts. For example, `beforeWebStart` is used to customize the loading process, logging level, and other options. Receives the Blazor Web options (`options`).
* `afterWebStarted(blazor)`: Called after all `beforeWebStart` promises resolve. For example, `afterWebStarted` can be used to register Blazor event listeners and custom event types. The Blazor instance is passed to `afterWebStarted` as an argument (`blazor`).
* `beforeServerStart(options, extensions)`: Called before the first Server runtime is started. Receives SignalR circuit start options (`options`) and any extensions (`extensions`) added during publishing.
* `afterServerStarted(blazor)`: Called after the first Interactive Server runtime is started.
* `beforeWebAssemblyStart(options, extensions)`: Called before the Interactive WebAssembly runtime is started. Receives the Blazor options (`options`) and any extensions (`extensions`) added during publishing. For example, options can specify the use of a custom [boot resource loader](startup?view=aspnetcore-10.0#load-client-side-boot-resources).
* `afterWebAssemblyStarted(blazor)`: Called after the Interactive WebAssembly runtime is started.

Note

Legacy JS initializers (`beforeStart`, `afterStarted`) aren't invoked by default in a Blazor Web App. You can enable the legacy initializers to run with the `enableClassicInitializers` option. However, legacy initializer execution is unpredictable.

```
<script>
  Blazor.start({ enableClassicInitializers: true });
</script>
```

Due to a [framework bug in .NET 8 and 9 (`dotnet/aspnetcore` #54049)](https://github.com/dotnet/aspnetcore/issues/54049), the Blazor script must be manually started when `beforeWebAssemblyStart(options, extensions)` or `afterWebAssemblyStarted(blazor)` are called. If the server app doesn't already start Blazor manually with a WebAssembly (`webAssembly: {...}`) configuration, update the `App` component in the server project with the following.

In `Components/App.razor`, remove the existing Blazor `<script>` tag:

```
- <script src="_framework/blazor.web.js"></script>
```

Replace the `<script>` tag with the following markup that starts Blazor manually with a WebAssembly (`webAssembly: {...}`) configuration:

```
<script src="_framework/blazor.web.js" autostart="false"></script>
<script>
    Blazor.start({
        webAssembly: {}
    });
</script>
```

For Blazor Server, Blazor WebAssembly, and Blazor Hybrid apps:

* `beforeStart(options, extensions)`: Called before Blazor starts. For example, `beforeStart` is used to customize the loading process, logging level, and other options specific to the hosting model.

  * Client-side, `beforeStart` receives the Blazor options (`options`) and any extensions (`extensions`) added during publishing. For example, options can specify the use of a custom [boot resource loader](startup?view=aspnetcore-10.0#load-client-side-boot-resources).
  * Server-side, `beforeStart` receives SignalR circuit start options (`options`).
  * In a [`BlazorWebView`](/en-us/mobile-blazor-bindings/walkthroughs/hybrid-hello-world#mainrazor-native-ui-page), no options are passed.

* `afterStarted(blazor)`: Called after Blazor is ready to receive calls from JS. For example, `afterStarted` is used to initialize libraries by making JS interop calls and registering custom elements. The Blazor instance is passed to `afterStarted` as an argument (`blazor`).

Additional .NET WebAssembly runtime callbacks:

* `onRuntimeConfigLoaded(config)`: Called when the boot configuration is downloaded. Allows the app to modify parameters (config) before the runtime starts (the parameter is `MonoConfig` from [`dotnet.d.ts`](https://github.com/dotnet/runtime/blob/main/src/mono/browser/runtime/dotnet.d.ts)):

  ```
  export function onRuntimeConfigLoaded(config) {
    // Sample: Enable startup diagnostic logging when the URL contains 
    // parameter debug=1
    const params = new URLSearchParams(location.search);
    if (params.get("debug") == "1") {
      config.diagnosticTracing = true;
    }
  }
  ```

* `onRuntimeReady({ getAssemblyExports, getConfig })`: Called after the .NET WebAssembly runtime has started (the parameter is `RuntimeAPI` from [`dotnet.d.ts`](https://github.com/dotnet/runtime/blob/main/src/mono/browser/runtime/dotnet.d.ts)):

  ```
  export function onRuntimeReady({ getAssemblyExports, getConfig }) {
    // Sample: After the runtime starts, but before Main method is called, 
    // call [JSExport]ed method.
    const config = getConfig();
    const exports = await getAssemblyExports(config.mainAssemblyName);
    exports.Sample.Greet();
  }
  ```

Both callbacks can return a `Promise`, and the promise is awaited before the startup continues.

For the file name:

* If the JS initializers are consumed as a static asset in the project, use the format `{ASSEMBLY NAME}.lib.module.js`, where the `{ASSEMBLY NAME}` placeholder is the app's assembly name. For example, name the file `BlazorSample.lib.module.js` for a project with an assembly name of `BlazorSample`. Place the file in the app's `wwwroot` folder.
* If the JS initializers are consumed from an RCL, use the format `{LIBRARY NAME/PACKAGE ID}.lib.module.js`, where the `{LIBRARY NAME/PACKAGE ID}` placeholder is the project's library name or package identifier (`<PackageId>` value in the library's project file). For example, name the file `RazorClassLibrary1.lib.module.js` for an RCL with a package identifier of `RazorClassLibrary1`. Place the file in the library's `wwwroot` folder.

For Blazor Web Apps:

The following example demonstrates JS initializers that load custom scripts before and after the Blazor Web App has started by appending them to the `<head>` in `beforeWebStart` and `afterWebStarted`:

```
export function beforeWebStart() {
  var customScript = document.createElement('script');
  customScript.setAttribute('src', 'beforeStartScripts.js');
  document.head.appendChild(customScript);
}

export function afterWebStarted() {
  var customScript = document.createElement('script');
  customScript.setAttribute('src', 'afterStartedScripts.js');
  document.head.appendChild(customScript);
}
```

The preceding `beforeWebStart` example only guarantees that the custom script loads before Blazor starts. It doesn't guarantee that awaited promises in the script complete their execution before Blazor starts.

For Blazor Server, Blazor WebAssembly, and Blazor Hybrid apps:

The following example demonstrates JS initializers that load custom scripts before and after Blazor has started by appending them to the `<head>` in `beforeStart` and `afterStarted`:

```
export function beforeStart(options, extensions) {
  var customScript = document.createElement('script');
  customScript.setAttribute('src', 'beforeStartScripts.js');
  document.head.appendChild(customScript);
}

export function afterStarted(blazor) {
  var customScript = document.createElement('script');
  customScript.setAttribute('src', 'afterStartedScripts.js');
  document.head.appendChild(customScript);
}
```

The preceding `beforeStart` example only guarantees that the custom script loads before Blazor starts. It doesn't guarantee that awaited promises in the script complete their execution before Blazor starts.

Note

MVC and Razor Pages apps don't automatically load JS initializers. However, developer code can include a script to fetch the app's manifest and trigger the load of the JS initializers.

For examples of JS initializers, see the following resources:

* [Blazor Web App loading indicator](#global-interactive-webassembly-rendering-without-prerendering) (*Global Interactive WebAssembly rendering without prerendering example*)
* [ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](../javascript-interoperability/static-server-rendering?view=aspnetcore-10.0)
* [Use Razor components in JavaScript apps and SPA frameworks](../components/js-spa-frameworks?view=aspnetcore-10.0#render-razor-components-from-javascript) (*`quoteContainer2` example*)
* [ASP.NET Core Blazor event handling](../components/event-handling?view=aspnetcore-10.0#custom-event-arguments) (*Custom clipboard paste event example*)
* [Enable QR code generation for TOTP authenticator apps in an ASP.NET Core Blazor Web App](../security/qrcodes-for-authenticator-apps?view=aspnetcore-10.0)
* [Basic Test App in the ASP.NET Core GitHub repository (`BasicTestApp.lib.module.js`)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/wwwroot/BasicTestApp.lib.module.js)

- [Use Razor components in JavaScript apps and SPA frameworks](../components/js-spa-frameworks?view=aspnetcore-10.0#render-razor-components-from-javascript) (*`quoteContainer2` example*)
- [ASP.NET Core Blazor event handling](../components/event-handling?view=aspnetcore-10.0#custom-event-arguments) (*Custom clipboard paste event example*)
- [Deployment layout for ASP.NET Core hosted Blazor WebAssembly apps](../host-and-deploy/webassembly/deployment-layout?view=aspnetcore-10.0)
- [Basic Test App in the ASP.NET Core GitHub repository (`BasicTestApp.lib.module.js`)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/wwwroot/BasicTestApp.lib.module.js)

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Ensure libraries are loaded in a specific order

Append custom scripts to the `<head>` in `beforeStart` and `afterStarted` in the order that they should load.

The following example loads `script1.js` before `script2.js` and `script3.js` before `script4.js`:

```
export function beforeStart(options, extensions) {
    var customScript1 = document.createElement('script');
    customScript1.setAttribute('src', 'script1.js');
    document.head.appendChild(customScript1);

    var customScript2 = document.createElement('script');
    customScript2.setAttribute('src', 'script2.js');
    document.head.appendChild(customScript2);
}

export function afterStarted(blazor) {
    var customScript1 = document.createElement('script');
    customScript1.setAttribute('src', 'script3.js');
    document.head.appendChild(customScript1);

    var customScript2 = document.createElement('script');
    customScript2.setAttribute('src', 'script4.js');
    document.head.appendChild(customScript2);
}
```

### Import additional modules

Use top-level [`import`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/import) statements in the JS initializers file to import additional modules.

`additionalModule.js`:

```
export function logMessage() {
  console.log('logMessage is logging');
}
```

In the JS initializers file (`.lib.module.js`):

```
import { logMessage } from "/additionalModule.js";

export function beforeStart(options, extensions) {
  ...

  logMessage();
}
```

### Import map

[Import maps](https://developer.mozilla.org/docs/Web/HTML/Element/script/type/importmap) are supported by ASP.NET Core and Blazor.

## Initialize Blazor when the document is ready

The following example starts Blazor when the document is ready:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  document.addEventListener("DOMContentLoaded", function() {
    Blazor.start();
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

## Chain to the `Promise` that results from a manual start

To perform additional tasks, such as JS interop initialization, use [`then`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) to chain to the [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) that results from a manual Blazor app start:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start().then(function () {
    ...
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Note

For a library to automatically execute additional tasks after Blazor has started, use a [JavaScript initializer](#javascript-initializers). Use of a JS initializer doesn't require the consumer of the library to chain JS calls to Blazor's manual start.

## Load client-side boot resources

When an app loads in the browser, the app downloads boot resources from the server:

* JavaScript code to bootstrap the app
* .NET runtime and assemblies
* Locale specific data

Customize how these boot resources are loaded using the `loadBootResource` API. The `loadBootResource` function overrides the built-in boot resource loading mechanism. Use `loadBootResource` for the following scenarios:

* Load static resources, such as timezone data or `dotnet.wasm`, from a CDN.
* Load compressed assemblies using an HTTP request and decompress them on the client for hosts that don't support fetching compressed contents from the server.
* Alias resources to a different name by redirecting each `fetch` request to a new name.

Note

External sources must return the required [Cross-Origin Resource Sharing (CORS)](https://www.w3.org/TR/cors/) headers for browsers to allow cross-origin resource loading. CDNs usually provide the required headers.

`loadBootResource` parameters appear in the following table.

| Parameter    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`       | The type of the resource. Permissible types include: `assembly`, `pdb`, `dotnetjs`, `dotnetwasm`, and `timezonedata`. You only need to specify types for custom behaviors. Types not specified to `loadBootResource` are loaded by the framework per their default loading behaviors. The `dotnetjs` boot resource (`dotnet.*.js`) must either return `null` for default loading behavior or a URI for the source of the `dotnetjs` boot resource. |
| `name`       | The name of the resource.                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `defaultUri` | The relative or absolute URI of the resource.                                                                                                                                                                                                                                                                                                                                                                                                      |
| `integrity`  | The integrity string representing the expected content in the response.                                                                                                                                                                                                                                                                                                                                                                            |

The `loadBootResource` function can return a URI string to override the loading process. In the following example, the following files from `bin/Release/{TARGET FRAMEWORK}/wwwroot/_framework` are served from a CDN at `https://cdn.example.com/blazorwebassembly/{VERSION}/`:

* `dotnet.*.js`
* `dotnet.wasm`
* Timezone data

The `{TARGET FRAMEWORK}` placeholder is the target framework moniker (for example, `net7.0`). The `{VERSION}` placeholder is the shared framework version (for example, `7.0.0`).

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    webAssembly: {
      loadBootResource: function (type, name, defaultUri, integrity) {
        console.log(`Loading: '${type}', '${name}', '${defaultUri}', '${integrity}'`);
        switch (type) {
          case 'dotnetjs':
          case 'dotnetwasm':
          case 'timezonedata':
            return `https://cdn.example.com/blazorwebassembly/{VERSION}/${name}`;
        }
      }
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Standalone Blazor WebAssembly:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    loadBootResource: function (type, name, defaultUri, integrity) {
      console.log(`Loading: '${type}', '${name}', '${defaultUri}', '${integrity}'`);
      switch (type) {
        case 'dotnetjs':
        case 'dotnetwasm':
        case 'timezonedata':
          return `https://cdn.example.com/blazorwebassembly/{VERSION}/${name}`;
      }
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

To customize more than just the URLs for boot resources, the `loadBootResource` function can call `fetch` directly and return the result. The following example adds a custom HTTP header to the outbound requests. To retain the default integrity checking behavior, pass through the `integrity` parameter.

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    webAssembly: {
      loadBootResource: function (type, name, defaultUri, integrity) {
        if (type == 'dotnetjs') {
          return null;
        } else {
          return fetch(defaultUri, {
            cache: 'no-cache',
            integrity: integrity,
            headers: { 'Custom-Header': 'Custom Value' }
          });
        }
      }
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Standalone Blazor WebAssembly:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    loadBootResource: function (type, name, defaultUri, integrity) {
      if (type == 'dotnetjs') {
        return null;
      } else {
        return fetch(defaultUri, {
          cache: 'no-cache',
          integrity: integrity,
          headers: { 'Custom-Header': 'Custom Value' }
        });
      }
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

When the `loadBootResource` function returns `null`, Blazor uses the default loading behavior for the resource. For example, the preceding code returns `null` for the `dotnetjs` boot resource (`dotnet.*.js`) because the `dotnetjs` boot resource must either return `null` for default loading behavior or a URI for the source of the `dotnetjs` boot resource.

The `loadBootResource` function can also return a [`Response` promise](https://developer.mozilla.org/docs/Web/API/Response). For an example, see [Host and deploy ASP.NET Core Blazor WebAssembly](../host-and-deploy/webassembly/?view=aspnetcore-10.0#compression).

For more information, see [ASP.NET Core Blazor WebAssembly caching and integrity check failures](../host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures?view=aspnetcore-10.0).

## Control headers in C# code

Control headers at startup in C# code using the following approaches.

In the following examples, a [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/Guides/CSP) is applied to the app via a CSP header. The `{POLICY STRING}` placeholder is the CSP policy string. For more information on CSPs, see [Enforce a Content Security Policy for ASP.NET Core Blazor](../security/content-security-policy?view=aspnetcore-10.0).

Note

Headers can't be set after the response starts. The approaches in this section only set headers before the response starts, so the approaches described here are safe. For more information, see [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../components/httpcontext?view=aspnetcore-10.0#dont-set-or-modify-headers-after-the-response-starts).

### Server-side and prerendered client-side scenarios

Use [ASP.NET Core middleware](../../fundamentals/middleware/?view=aspnetcore-10.0) to control the headers collection.

In the `Program` file:

In `Startup.Configure` of `Startup.cs`:

```
app.Use(async (context, next) =>
{
    context.Response.Headers.Append("Content-Security-Policy", "{POLICY STRING}");
    await next();
});
```

The preceding example uses inline middleware, but you can also create a custom middleware class and call the middleware with an extension method in the `Program` file. For more information, see [Write custom ASP.NET Core middleware](../../fundamentals/middleware/write?view=aspnetcore-10.0).

### Client-side development without prerendering

Pass [StaticFileOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileoptions) to [MapFallbackToFile](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfilesendpointroutebuilderextensions.mapfallbacktofile) that specifies response headers at the [OnPrepareResponse](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileoptions.onprepareresponse#microsoft-aspnetcore-builder-staticfileoptions-onprepareresponse) stage.

In the server-side `Program` file:

In `Startup.Configure` of `Startup.cs`:

```
var staticFileOptions = new StaticFileOptions
{
    OnPrepareResponse = context =>
    {
        context.Context.Response.Headers.Append("Content-Security-Policy", 
            "{POLICY STRING}");
    }
};

...

app.MapFallbackToFile("index.html", staticFileOptions);
```

## Client-side loading indicators

A loading indicator shows that the app is loading normally and that the user should wait until loading is finished.

### Blazor Web App loading indicator

The loading indicator used in Blazor WebAssembly apps isn't present in an app created from the Blazor Web App project template. Usually, a loading indicator isn't desirable for interactive WebAssembly components because Blazor Web Apps prerender client-side components on the server for fast initial load times. For mixed-render-mode situations, the framework or developer code must also be careful to avoid the following problems:

* Showing multiple loading indicators on the same rendered page.
* Inadvertently discarding prerendered content while the .NET WebAssembly runtime is loading.

A future release of .NET might provide a framework-based loading indicator. In the meantime, you can add a custom loading indicator to a Blazor Web App.

#### Per-component Interactive WebAssembly rendering with prerendering

*This scenario applies to per-component Interactive WebAssembly rendering (`@rendermode InteractiveWebAssembly` applied to individual components).*

Create a `ContentLoading` component in the `Layout` folder of the `.Client` app that calls [OperatingSystem.IsBrowser](/en-us/dotnet/api/system.operatingsystem.isbrowser):

* When `false`, display a loading indicator.
* When `true`, render the requested component's content.

To load CSS styles for the indicator, add the styles to `<head>` content with the [HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component. For more information, see [Control head content in ASP.NET Core Blazor apps](../components/control-head-content?view=aspnetcore-10.0).

`Layout/ContentLoading.razor`:

```
@if (!RendererInfo.IsInteractive)
{
    <!-- OPTIONAL ...
    <HeadContent>
        <style>
            ...
        </style>
    </HeadContent>
    -->
    <progress id="loadingIndicator" aria-label="Content loading…"></progress>
}
else
{
    @ChildContent
}

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

```
@if (!OperatingSystem.IsBrowser())
{
    <!-- OPTIONAL ...
    <HeadContent>
        <style>
            ...
        </style>
    </HeadContent>
    -->
    <progress id="loadingIndicator" aria-label="Content loading…"></progress>
}
else
{
    @ChildContent
}

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

If you didn't already have a `Layout` folder in the `.Client` project, add the namespace for the `Layout` folder to the imports file (`_Imports.razor`). In the following example, the project's namespace is `BlazorSample.Client`:

```
@using BlazorSample.Client.Layout
```

In a component that adopts Interactive WebAssembly rendering, wrap the component's Razor markup with the `ContentLoading` component. The following example demonstrates the approach with the `Counter` component of an app created from the Blazor Web App project template.

`Pages/Counter.razor`:

```
@page "/counter"
@rendermode InteractiveWebAssembly

<PageTitle>Counter</PageTitle>

<ContentLoading>
    <h1>Counter</h1>

    <p role="status">Current count: @currentCount</p>

    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</ContentLoading>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

#### Global Interactive WebAssembly rendering with prerendering

*This scenario applies to global Interactive WebAssembly rendering with prerendering (`@rendermode="InteractiveWebAssembly"` on the `HeadOutlet` and `Routes` components in the `App` component).*

Create a `ContentLoading` component in the `Layout` folder of the `.Client` app that calls [RendererInfo.IsInteractive](/en-us/dotnet/api/microsoft.aspnetcore.components.rendererinfo.isinteractive#microsoft-aspnetcore-components-rendererinfo-isinteractive):

* When `false`, display a loading indicator.
* When `true`, render the requested component's content.

To load CSS styles for the indicator, add the styles to `<head>` content with the [HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component. For more information, see [Control head content in ASP.NET Core Blazor apps](../components/control-head-content?view=aspnetcore-10.0).

`Layout/ContentLoading.razor`:

```
@if (!RendererInfo.IsInteractive)
{
    <!-- OPTIONAL ...
    <HeadContent>
        <style>
            ...
        </style>
    </HeadContent>
    -->
    <progress id="loadingIndicator" aria-label="Content loading…"></progress>
}
else
{
    @ChildContent
}

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

```
@if (!OperatingSystem.IsBrowser())
{
    <!-- OPTIONAL ...
    <HeadContent>
        <style>
            ...
        </style>
    </HeadContent>
    -->
    <progress id="loadingIndicator" aria-label="Content loading…"></progress>
}
else
{
    @ChildContent
}

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
}
```

If you didn't already have a `Layout` folder in the `.Client` project, add the namespace for the `Layout` folder to the imports file (`_Imports.razor`). In the following example, the project's namespace is `BlazorSample.Client`:

```
@using BlazorSample.Client.Layout
```

In the `MainLayout` component (`Layout/MainLayout.razor`) of the `.Client` project, wrap the [Body](/en-us/dotnet/api/microsoft.aspnetcore.components.layoutcomponentbase.body) property (`@Body`) with the `ContentLoading` component:

In `Layout/MainLayout.razor`:

```
+ <ContentLoading>
    @Body
+ </ContentLoading>
```

#### Global Interactive WebAssembly rendering without prerendering

*This scenario applies to global Interactive WebAssembly rendering without prerendering (`@rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)"` on the `HeadOutlet` and `Routes` components in the `App` component).*

Add a [JavaScript initializer](#javascript-initializers) to the app. In the following JavaScript module file name example, the `{ASSEMBLY NAME}` placeholder is the assembly name of the server project (for example, `BlazorSample`). The `wwwroot` folder where the module is placed is the `wwwroot` folder in the server-side project, not the `.Client` project.

The following example uses a [`progress`](https://developer.mozilla.org/docs/Web/HTML/Element/progress) indicator that doesn't indicate the actual progress of [delivering client-side boot resources to the client](#load-client-side-boot-resources), but it serves as a general approach for further development if you want the progress indicator to show the actual progress of loading the app's boot resources.

`wwwroot/{ASSEMBLY NAME}.lib.module.js`:

```
export function beforeWebStart(options) {
  var progress = document.createElement("progress");
  progress.id = 'loadingIndicator';
  progress.ariaLabel = 'Blazor loading…';
  progress.style = 'position:absolute;top:50%;left:50%;margin-right:-50%;' +
    'transform:translate(-50%,-50%);';
  document.body.appendChild(progress);
}

export function afterWebAssemblyStarted(blazor) {
  var progress = document.getElementById('loadingIndicator');
  progress.remove();
}
```

Due to a [framework bug in .NET 8 and 9 (`dotnet/aspnetcore` #54049)](https://github.com/dotnet/aspnetcore/issues/54049), the Blazor script must be manually started. If the server app doesn't already start Blazor manually with a WebAssembly (`webAssembly: {...}`) configuration, update the `App` component in the server project with the following.

In `Components/App.razor`, remove the existing Blazor `<script>` tag:

```
- <script src="_framework/blazor.web.js"></script>
```

Replace the `<script>` tag with the following markup that starts Blazor manually with a WebAssembly (`webAssembly: {...}`) configuration:

```
<script src="_framework/blazor.web.js" autostart="false"></script>
<script>
    Blazor.start({
        webAssembly: {}
    });
</script>
```

If you notice a short delay between the loading indicator removal and the first page render, you can guarantee removal of the indicator after rendering by calling for indicator removal in the [`OnAfterRenderAsync` lifecycle method](../components/lifecycle?view=aspnetcore-10.0#after-component-render-onafterrenderasync) of either the `MainLayout` or `Routes` components. For more information and a code example, see [Document an approach for a loading indicator that works with global Interactive WebAssembly without prerendering (`dotnet/AspNetCore.Docs` #35111)](https://github.com/dotnet/AspNetCore.Docs/issues/35111#issuecomment-2778796998).

### Blazor WebAssembly app loading progress

The project template contains Scalable Vector Graphics (SVG) and text indicators that show the loading progress of the app.

The progress indicators are implemented with HTML and CSS using two CSS custom properties (variables) provided by Blazor:

* `--blazor-load-percentage`: The percentage of app files loaded.
* `--blazor-load-percentage-text`: The percentage of app files loaded, rounded to the nearest whole number.

Using the preceding CSS variables, you can create custom progress indicators that match the styling of your app.

In the following example:

* `resourcesLoaded` is an instantaneous count of the resources loaded during app startup.
* `totalResources` is the total number of resources to load.

```
const percentage = resourcesLoaded / totalResources * 100;
document.documentElement.style.setProperty(
  '--blazor-load-percentage', `${percentage}%`);
document.documentElement.style.setProperty(
  '--blazor-load-percentage-text', `"${Math.floor(percentage)}%"`);
```

The default round progress indicator is implemented in HTML in the `wwwroot/index.html` file:

```
<div id="app">
    <svg class="loading-progress">
        <circle r="40%" cx="50%" cy="50%" />
        <circle r="40%" cx="50%" cy="50%" />
    </svg>
    <div class="loading-progress-text"></div>
</div>
```

To review the project template markup and styling for the default progress indicators, see the ASP.NET Core reference source:

* [`wwwroot/index.html`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/wwwroot/index.html)
* [`app.css`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/wwwroot/css/app.css)

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Instead of using the default round progress indicator, the following example shows how to implement a linear progress indicator.

Add the following styles to `wwwroot/css/app.css`:

```
.linear-progress {
    background: silver;
    width: 50vw;
    margin: 20% auto;
    height: 1rem;
    border-radius: 10rem;
    overflow: hidden;
    position: relative;
}

.linear-progress:after {
    content: '';
    position: absolute;
    inset: 0;
    background: blue;
    scale: var(--blazor-load-percentage, 0%) 100%;
    transform-origin: left top;
    transition: scale ease-out 0.5s;
}
```

A CSS variable (`var(...)`) is used to pass the value of `--blazor-load-percentage` to the `scale` property of a blue pseudo-element that indicates the loading progress of the app's files. As the app loads, `--blazor-load-percentage` is updated automatically, which dynamically changes the progress indicator's visual representation.

In `wwwroot/index.html`, remove the default SVG round indicator in `<div id="app">...</div>` and replace it with the following markup:

```
<div class="linear-progress"></div>
```

## Configure the .NET WebAssembly runtime

In advanced programming scenarios, the `configureRuntime` function with the `dotnet` runtime host builder is used to configure the .NET WebAssembly runtime. For example, `dotnet.withEnvironmentVariable` sets an environment variable that:

* Configures the .NET WebAssembly runtime.
* Changes the behavior of a C library.

Note

A documentation request is pending in the `dotnet/runtime` GitHub repository for more information on environment variables that configure the .NET WebAssembly runtime or affect the behavior of C libraries. Although the documentation request is pending, more information and cross-links to additional resources are available in the request, [Question/request for documentation on .NET WASM runtime env vars (`dotnet/runtime` #98225)](https://github.com/dotnet/runtime/issues/98225).

The `configureRuntime` function can also be used to [enable integration with a browser profiler](https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md#profiling).

For the placeholders in the following examples that set an environment variable:

* The `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name. For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).
* The `{NAME}` placeholder is the environment variable's name.
* The `{VALUE}` placeholder is the environment variable's value.

Blazor Web App:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    webAssembly: {
      configureRuntime: dotnet => {
        dotnet.withEnvironmentVariable("{NAME}", "{VALUE}");
      }
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Standalone Blazor WebAssembly:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureRuntime: dotnet => {
      dotnet.withEnvironmentVariable("{NAME}", "{VALUE}");
    }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

Note

The .NET runtime instance can be accessed using the .NET WebAssembly Runtime API (`Blazor.runtime`). For example, the app's build configuration can be obtained using `Blazor.runtime.runtimeBuildInfo.buildConfiguration`.

For more information on the .NET WebAssembly runtime configuration, see the [runtime's TypeScript definition file (`dotnet.d.ts`) in the `dotnet/runtime` GitHub repository](https://github.com/dotnet/runtime/blob/main/src/mono/browser/runtime/dotnet.d.ts).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Disable enhanced navigation and form handling

*This section applies to Blazor Web Apps.*

To disable [enhanced navigation and form handling](navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling), set `disableDomPreservation` to `true` for `Blazor.start`:

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    ssr: { disableDomPreservation: true }
  });
</script>
```

**In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script path and file name.** For the location of the script, see [ASP.NET Core Blazor project structure](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script).

## Related content

* [Environments: Set the app's environment](environments?view=aspnetcore-10.0)
* [SignalR (includes sections on SignalR startup configuration)](signalr?view=aspnetcore-10.0)
* [Globalization and localization: Statically set the culture with `Blazor.start()` (*client-side only*)](../globalization-localization?view=aspnetcore-10.0#statically-set-the-client-side-culture)
* [Blazor WebAssembly compression](../host-and-deploy/webassembly/?view=aspnetcore-10.0#compression)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/configuration
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/configuration.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC for .NET configuration

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

## Configure services options

gRPC services are configured with `AddGrpc` in `Startup.cs`. Configuration options are in the [`Grpc.AspNetCore.Server`](https://www.nuget.org/packages/Grpc.AspNetCore.Server) package.

The following table describes options for configuring gRPC services.

| Option                         | Default Value | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ------------------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MaxSendMessageSize`           | `null`        | The maximum message size in bytes that can be sent from the server. Attempting to send a message that exceeds the configured maximum message size results in an exception. When set to `null`, the message size is unlimited.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `MaxReceiveMessageSize`        | 4 MB          | The maximum message size in bytes that can be received by the server. If the server receives a message that exceeds this limit, it throws an exception. Increasing this value allows the server to receive larger messages, but can negatively impact memory consumption. When set to `null`, the message size is unlimited.                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `EnableDetailedErrors`         | `false`       | If `true`, detailed exception messages are returned to clients when an exception is thrown in a service method. The default is `false`. Setting `EnableDetailedErrors` to `true` can leak sensitive information.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `CompressionProviders`         | gzip          | A collection of compression providers used to compress and decompress messages. Custom compression providers can be created and added to the collection. The default configured providers support **gzip** compression.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `ResponseCompressionAlgorithm` | `null`        | The compression algorithm used to compress messages sent from the server. The algorithm must match a compression provider in `CompressionProviders`. For the algorithm to compress a response, the client must indicate it supports the algorithm by sending it in the **grpc-accept-encoding** header.                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `ResponseCompressionLevel`     | `null`        | The compress level used to compress messages sent from the server.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `Interceptors`                 | None          | A collection of interceptors that are run with each gRPC call. Interceptors are run in the order they are registered. Globally configured interceptors are run before interceptors configured for a single service. Interceptors have a per-request lifetime by default. The interceptor constructor is called and parameters are resolved from [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0). An interceptor type can also be registered with DI to override how it is created and its lifetime. Interceptors offer similar functionalities compared to ASP.NET Core middleware. For more information, see [gRPC Interceptors vs. middleware](migration?view=aspnetcore-10.0#grpc-interceptors-vs-middleware). |
| `IgnoreUnknownServices`        | `false`       | If `true`, calls to unknown services and methods don't return an **UNIMPLEMENTED** status, and the request passes to the next registered middleware in ASP.NET Core.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |

Options can be configured for all services by providing an options delegate to the `AddGrpc` call in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.EnableDetailedErrors = true;
        options.MaxReceiveMessageSize = 2 * 1024 * 1024; // 2 MB
        options.MaxSendMessageSize = 5 * 1024 * 1024; // 5 MB
    });
}
```

Options for a single service override the global options provided in `AddGrpc` and can be configured using `AddServiceOptions<TService>`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc().AddServiceOptions<MyService>(options =>
    {
        options.MaxReceiveMessageSize = 2 * 1024 * 1024; // 2 MB
        options.MaxSendMessageSize = 5 * 1024 * 1024; // 5 MB
    });
}
```

Service interceptors have a per-request lifetime by default. Registering the interceptor type with DI overrides how an interceptor is created and its lifetime.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.Interceptors.Add<LoggingInterceptor>();
    });
    services.AddSingleton<LoggingInterceptor>();
}
```

### ASP.NET Core server options

`Grpc.AspNetCore.Server` is hosted by an ASP.NET Core web server. There are a number of options for ASP.NET Core servers, including Kestrel, IIS and HTTP.sys. Each server offers additional options for how HTTP requests are served.

The server used by an ASP.NET Core app is configured in app startup code. The default server is Kestrel.

For more information about the different servers and their configuration options, see:

* [Kestrel web server in ASP.NET Core](../fundamentals/servers/kestrel?view=aspnetcore-10.0)
* [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0)
* [Host ASP.NET Core on Windows with IIS](../host-and-deploy/iis/?view=aspnetcore-10.0)

## Configure client options

gRPC client configuration is set on `GrpcChannelOptions`. Configuration options are in the [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client) package.

The following table describes options for configuring gRPC channels.

| Option                                    | Default Value | Description                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HttpHandler`                             | New instance  | The `HttpMessageHandler` used to make gRPC calls. A client can be set to configure a custom `HttpClientHandler` or add additional handlers to the HTTP pipeline for gRPC calls. If no `HttpMessageHandler` is specified, a new `HttpClientHandler` instance is created for the channel with automatic disposal.                                                                                                                       |
| `HttpClient`                              | `null`        | The `HttpClient` used to make gRPC calls. This setting is an alternative to `HttpHandler`.                                                                                                                                                                                                                                                                                                                                            |
| `DisposeHttpClient`                       | `false`       | If set to `true` and an `HttpMessageHandler` or `HttpClient` is specified, then either the `HttpHandler` or `HttpClient`, respectively, is disposed when the `GrpcChannel` is disposed.                                                                                                                                                                                                                                               |
| `LoggerFactory`                           | `null`        | The `LoggerFactory` used by the client to log information about gRPC calls. A `LoggerFactory` instance can be resolved from dependency injection or created using `LoggerFactory.Create`. For examples of configuring logging, see [Logging and diagnostics in gRPC on .NET](diagnostics?view=aspnetcore-10.0#grpc-client-logging).                                                                                                   |
| `MaxSendMessageSize`                      | `null`        | The maximum message size in bytes that can be sent from the client. Attempting to send a message that exceeds the configured maximum message size results in an exception. When set to `null`, the message size is unlimited.                                                                                                                                                                                                         |
| `MaxReceiveMessageSize`                   | 4 MB          | The maximum message size in bytes that can be received by the client. If the client receives a message that exceeds this limit, it throws an exception. Increasing this value allows the client to receive larger messages, but can negatively impact memory consumption. When set to `null`, the message size is unlimited.                                                                                                          |
| `Credentials`                             | `null`        | A `ChannelCredentials` instance. Credentials are used to add authentication metadata to gRPC calls.                                                                                                                                                                                                                                                                                                                                   |
| `CompressionProviders`                    | gzip          | A collection of compression providers used to compress and decompress messages. Custom compression providers can be created and added to the collection. The default configured providers support **gzip** compression.                                                                                                                                                                                                               |
| `ThrowOperationCanceledOnCancellation`    | `false`       | If set to `true`, clients throw [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception) when a call is canceled or its deadline is exceeded.                                                                                                                                                                                                                                                                |
| `UnsafeUseInsecureChannelCallCredentials` | `false`       | If set to `true`, `CallCredentials` are applied to gRPC calls made by an insecure channel. Sending authentication headers over an insecure connection has security implications and shouldn't be done in production environments.                                                                                                                                                                                                     |
| `MaxRetryAttempts`                        | 5             | The maximum retry attempts. This value limits any retry and hedging attempt values specified in the service config. Setting this value alone doesn't enable retries. Retries are enabled in the service config, which can be done using `ServiceConfig`. A `null` value removes the maximum retry attempts limit. For more information about retries, see [Transient fault handling with gRPC retries](retries?view=aspnetcore-10.0). |
| `MaxRetryBufferSize`                      | 16 MB         | The maximum buffer size in bytes that can be used to store sent messages when retrying or hedging calls. If the buffer limit is exceeded, then no more retry attempts are made and all hedging calls but one will be canceled. This limit is applied across all calls made using the channel. A `null` value removes the maximum retry buffer size limit.                                                                             |
| `MaxRetryBufferPerCallSize`               | 1 MB          | The maximum buffer size in bytes that can be used to store sent messages when retrying or hedging calls. If the buffer limit is exceeded, then no more retry attempts are made and all hedging calls but one will be canceled. This limit is applied to one call. A `null` value removes the maximum retry buffer size limit per call.                                                                                                |
| `ServiceConfig`                           | `null`        | The service config for a gRPC channel. A service config can be used to configure [gRPC retries](retries?view=aspnetcore-10.0).                                                                                                                                                                                                                                                                                                        |

The following code:

* Sets the maximum send and receive message size on the channel.
* Creates a client.

```
static async Task Main(string[] args)
{
    var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
    {
        MaxReceiveMessageSize = 5 * 1024 * 1024, // 5 MB
        MaxSendMessageSize = 2 * 1024 * 1024 // 2 MB
    });
    var client = new Greeter.GreeterClient(channel);

    var reply = await client.SayHelloAsync(
                      new HelloRequest { Name = "GreeterClient" });
    Console.WriteLine("Greeting: " + reply.Message);
}
```

Note that client interceptors aren't configured with `GrpcChannelOptions`. Instead, client interceptors are configured using the `Intercept` extension method with a channel. This extension method is in the `Grpc.Core.Interceptors` namespace.

```
static async Task Main(string[] args)
{
    var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var callInvoker = channel.Intercept(new LoggingInterceptor());
    var client = new Greeter.GreeterClient(callInvoker);

    var reply = await client.SayHelloAsync(
                      new HelloRequest { Name = "GreeterClient" });
    Console.WriteLine("Greeting: " + reply.Message);
}
```

### System.Net handler options

`Grpc.Net.Client` uses a HTTP transport derived from `HttpMessageHandler` to make HTTP requests. Each handler offers additional options for how HTTP requests are made.

The handler is configured on a channel and can be overridden by setting `GrpcChannelOptions.HttpHandler`. .NET Core 3 and .NET 5 or later uses [SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler) by default. gRPC client apps on .NET Framework [should configure WinHttpHandler](netstandard?view=aspnetcore-10.0#net-framework).

For more information about the different handlers and their configuration options, see:

* [System.Net.Http.SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler)
* [System.Net.Http.WinHttpHandler](/en-us/dotnet/api/system.net.http.winhttphandler)

## Related content

* [gRPC services with ASP.NET Core](aspnetcore?view=aspnetcore-10.0)
* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)
* [Logging and diagnostics in gRPC on .NET](diagnostics?view=aspnetcore-10.0)
* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/search
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/search.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 6, add search to ASP.NET Core Razor Pages

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT)

In the following sections, you add the ability to search movies by *genre* or *name*.

Add the following highlighted code to `Pages/MoviePages/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    public IList<Movie> Movie { get;set; }  = default!;

    [BindProperty(SupportsGet = true)]
    public string? SearchString { get; set; }

    public SelectList? Genres { get; set; }

    [BindProperty(SupportsGet = true)]
    public string? MovieGenre { get; set; }
```

In the previous code:

* `SearchString`: Contains the text users enter in the search text box. `SearchString` has the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute. `[BindProperty]` binds form values and query strings with the same name as the property. `[BindProperty(SupportsGet = true)]` is required for binding on HTTP GET requests.
* `Genres`: Contains the list of genres. `Genres` allows the user to select a genre from the list. `SelectList` requires `using Microsoft.AspNetCore.Mvc.Rendering;`
* `MovieGenre`: Contains the specific genre the user selects. For example, "Western".
* `Genres` and `MovieGenre` are used later in this tutorial.

Warning

For security reasons, you must opt in to binding `GET` request data to page model properties. Verify user input before mapping it to properties. Opting into `GET` binding is useful when addressing scenarios that rely on query string or route values.

To bind a property on `GET` requests, set the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute's `SupportsGet` property to `true`:

```
[BindProperty(SupportsGet = true)]
```

For more information, see [ASP.NET Core Community Standup: Bind on GET discussion (YouTube)](https://www.youtube.com/watch?v=p7iHB9V-KVU\&feature=youtu.be\&t=54m27s).

Update the `MoviePages/Index` page's `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    var movies = from m in _context.Movie
                 select m;
    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    Movie = await movies.ToListAsync();
}
```

The first line of the `OnGetAsync` method creates a [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) query to select the movies:

```
var movies = from m in _context.Movie
             select m;
```

The query is only ***defined*** at this point. It isn't run against the database.

If the `SearchString` property isn't `null` or empty, the movies query is modified to filter on the search string:

```
if (!string.IsNullOrEmpty(SearchString))
{
    movies = movies.Where(s => s.Title.Contains(SearchString));
}
```

The `s => s.Title.Contains()` code is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/csharp/programming-guide/concepts/linq/query-syntax-and-method-syntax-in-linq) method or `Contains`. LINQ queries aren't executed when you define them or when you modify them by calling a method, such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. The evaluation of an expression is delayed until its realized value is iterated over or the `ToListAsync` method is called. For more information, see [Query Execution](/en-us/dotnet/csharp/linq/get-started/introduction-to-linq-queries#deferred).

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method runs on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [How to use case-insensitive query with Sqlite provider? (`dotnet/efcore` #11414)](https://github.com/dotnet/efcore/issues/11414)
* [How to make a SQLite column case insensitive (`dotnet/AspNetCore.Docs` #22314)](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to the MoviePages page and append a query string such as `?searchString=Ghost` to the URL. For example, `https://localhost:7247/MoviePages?searchString=Ghost`. The filtered movies are displayed.

If you add the following route template to the Index page, you can pass the search string as a URL segment. For example, `https://localhost:7247/MoviePages/Ghost`.

```
@page "{searchString?}"
```

The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query string value. The `?` in `"{searchString?}"` means this is an optional route parameter.

The ASP.NET Core runtime uses [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) to set the value of the `SearchString` property from the query string (`?searchString=Ghost`) or route data (`https://localhost:7247/MoviePages/Ghost`). Model binding isn't case sensitive.

However, users can't be expected to modify the URL to search for a movie. In this step, you add UI to filter movies. If you added the route constraint `"{searchString?}"`, remove it.

Open the `Pages/MoviePages/Index.cshtml` file, and add the markup highlighted in the following code:

```
@page
@model RazorPagesMovie.Pages.MoviePages.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
```

The HTML `<form>` tag uses the following [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0):

* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). When you submit the form, it sends the filter string to the *Pages/MoviePages/Index* page through the query string.
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper)

Save your changes and test the filter.

## Search by genre

Update the `MoviePages/Index.cshtml.cs` page `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    // <snippet_search_linqQuery>
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    // </snippet_search_linqQuery>

    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    if (!string.IsNullOrEmpty(MovieGenre))
    {
        movies = movies.Where(x => x.Genre == MovieGenre);
    }

    // <snippet_search_selectList>
    Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
    // </snippet_search_selectList>
    Movie = await movies.ToListAsync();
}
```

The following code is a LINQ query that retrieves all the genres from the database.

```
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres:

```
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
```

### Add search by genre to the Razor Page

Update the `Index.cshtml` [`<form>` element](https://developer.mozilla.org/docs/Web/HTML/Element/form) as highlighted in the following markup:

```
@page
@model RazorPagesMovie.Pages.MoviePages.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
```

Test the app by searching by genre, by movie title, and by both:

## Next steps

[Previous: Update the pages](da1?view=aspnetcore-10.0) [Next: Add a new field](new-field?view=aspnetcore-10.0)

In the following sections, searching movies by *genre* or *name* is added.

Add the following highlighted code to `Pages/Movies/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    public IList<Movie> Movie { get;set; }  = default!;

    [BindProperty(SupportsGet = true)]
    public string? SearchString { get; set; }

    public SelectList? Genres { get; set; }

    [BindProperty(SupportsGet = true)]
    public string? MovieGenre { get; set; }
```

In the previous code:

* `SearchString`: Contains the text users enter in the search text box. `SearchString` has the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute. `[BindProperty]` binds form values and query strings with the same name as the property. `[BindProperty(SupportsGet = true)]` is required for binding on HTTP GET requests.
* `Genres`: Contains the list of genres. `Genres` allows the user to select a genre from the list. `SelectList` requires `using Microsoft.AspNetCore.Mvc.Rendering;`
* `MovieGenre`: Contains the specific genre the user selects. For example, "Western".
* `Genres` and `MovieGenre` are used later in this tutorial.

Warning

For security reasons, you must opt in to binding `GET` request data to page model properties. Verify user input before mapping it to properties. Opting into `GET` binding is useful when addressing scenarios that rely on query string or route values.

To bind a property on `GET` requests, set the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute's `SupportsGet` property to `true`:

```
[BindProperty(SupportsGet = true)]
```

For more information, see [ASP.NET Core Community Standup: Bind on GET discussion (YouTube)](https://www.youtube.com/watch?v=p7iHB9V-KVU\&feature=youtu.be\&t=54m27s).

Update the `Movies/Index` page's `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    var movies = from m in _context.Movie
                 select m;
    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    Movie = await movies.ToListAsync();
}
```

The first line of the `OnGetAsync` method creates a [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) query to select the movies:

```
var movies = from m in _context.Movie
             select m;
```

The query is only ***defined*** at this point, it has ***not*** been run against the database.

If the `SearchString` property is not `null` or empty, the movies query is modified to filter on the search string:

```
if (!string.IsNullOrEmpty(SearchString))
{
    movies = movies.Where(s => s.Title.Contains(SearchString));
}
```

The `s => s.Title.Contains()` code is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/csharp/programming-guide/concepts/linq/query-syntax-and-method-syntax-in-linq) method or `Contains`. LINQ queries are not executed when they're defined or when they're modified by calling a method, such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. The evaluation of an expression is delayed until its realized value is iterated over or the `ToListAsync` method is called. See [Query Execution](/en-us/dotnet/csharp/linq/get-started/introduction-to-linq-queries#deferred) for more information.

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [How to use case-insensitive query with Sqlite provider? (`dotnet/efcore` #11414)](https://github.com/dotnet/efcore/issues/11414)
* [How to make a SQLite column case insensitive (`dotnet/AspNetCore.Docs` #22314)](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to the Movies page and append a query string such as `?searchString=Ghost` to the URL. For example, `https://localhost:5001/Movies?searchString=Ghost`. The filtered movies are displayed.

If the following route template is added to the Index page, the search string can be passed as a URL segment. For example, `https://localhost:5001/Movies/Ghost`.

```
@page "{searchString?}"
```

The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query string value. The `?` in `"{searchString?}"` means this is an optional route parameter.

The ASP.NET Core runtime uses [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) to set the value of the `SearchString` property from the query string (`?searchString=Ghost`) or route data (`https://localhost:5001/Movies/Ghost`). Model binding is ***not*** case sensitive.

However, users cannot be expected to modify the URL to search for a movie. In this step, UI is added to filter movies. If you added the route constraint `"{searchString?}"`, remove it.

Open the `Pages/Movies/Index.cshtml` file, and add the markup highlighted in the following code:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
```

The HTML `<form>` tag uses the following [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0):

* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). When the form is submitted, the filter string is sent to the *Pages/Movies/Index* page via query string.
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper)

Save the changes and test the filter.

## Search by genre

Update the `Movies/Index.cshtml.cs` page `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    // <snippet_search_linqQuery>
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    // </snippet_search_linqQuery>

    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    if (!string.IsNullOrEmpty(MovieGenre))
    {
        movies = movies.Where(x => x.Genre == MovieGenre);
    }

    // <snippet_search_selectList>
    Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
    // </snippet_search_selectList>
    Movie = await movies.ToListAsync();
}
```

The following code is a LINQ query that retrieves all the genres from the database.

```
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres:

```
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
```

### Add search by genre to the Razor Page

Update the `Index.cshtml` [`<form>` element](https://developer.mozilla.org/docs/Web/HTML/Element/form) as highlighted in the following markup:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
```

Test the app by searching by genre, by movie title, and by both:

## Next steps

[Previous: Update the pages](da1?view=aspnetcore-10.0) [Next: Add a new field](new-field?view=aspnetcore-10.0)

In the following sections, searching movies by *genre* or *name* is added.

Add the following highlighted code to `Pages/Movies/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    public IList<Movie> Movie { get;set; }  = default!;

    [BindProperty(SupportsGet = true)]
    public string? SearchString { get; set; }

    public SelectList? Genres { get; set; }

    [BindProperty(SupportsGet = true)]
    public string? MovieGenre { get; set; }
```

In the previous code:

* `SearchString`: Contains the text users enter in the search text box. `SearchString` has the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute. `[BindProperty]` binds form values and query strings with the same name as the property. `[BindProperty(SupportsGet = true)]` is required for binding on HTTP GET requests.
* `Genres`: Contains the list of genres. `Genres` allows the user to select a genre from the list. `SelectList` requires `using Microsoft.AspNetCore.Mvc.Rendering;`
* `MovieGenre`: Contains the specific genre the user selects. For example, "Western".
* `Genres` and `MovieGenre` are used later in this tutorial.

Warning

For security reasons, you must opt in to binding `GET` request data to page model properties. Verify user input before mapping it to properties. Opting into `GET` binding is useful when addressing scenarios that rely on query string or route values.

To bind a property on `GET` requests, set the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute's `SupportsGet` property to `true`:

```
[BindProperty(SupportsGet = true)]
```

For more information, see [ASP.NET Core Community Standup: Bind on GET discussion (YouTube)](https://www.youtube.com/watch?v=p7iHB9V-KVU\&feature=youtu.be\&t=54m27s).

Update the Index page's `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    var movies = from m in _context.Movie
                 select m;
    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    Movie = await movies.ToListAsync();
}
```

The first line of the `OnGetAsync` method creates a [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) query to select the movies:

```
// using System.Linq;
var movies = from m in _context.Movie
             select m;
```

The query is only ***defined*** at this point, it has ***not*** been run against the database.

If the `SearchString` property is not `null` or empty, the movies query is modified to filter on the search string:

```
if (!string.IsNullOrEmpty(SearchString))
{
    movies = movies.Where(s => s.Title.Contains(SearchString));
}
```

The `s => s.Title.Contains()` code is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/csharp/programming-guide/concepts/linq/query-syntax-and-method-syntax-in-linq) method or `Contains`. LINQ queries are not executed when they're defined or when they're modified by calling a method, such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. The evaluation of an expression is delayed until its realized value is iterated over or the `ToListAsync` method is called. See [Query Execution](/en-us/dotnet/csharp/linq/get-started/introduction-to-linq-queries#deferred) for more information.

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [How to use case-insensitive query with Sqlite provider? (`dotnet/efcore` #11414)](https://github.com/dotnet/efcore/issues/11414)
* [How to make a SQLite column case insensitive (`dotnet/AspNetCore.Docs` #22314)](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to the Movies page and append a query string such as `?searchString=Ghost` to the URL. For example, `https://localhost:5001/Movies?searchString=Ghost`. The filtered movies are displayed.

If the following route template is added to the Index page, the search string can be passed as a URL segment. For example, `https://localhost:5001/Movies/Ghost`.

```
@page "{searchString?}"
```

The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query string value. The `?` in `"{searchString?}"` means this is an optional route parameter.

The ASP.NET Core runtime uses [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) to set the value of the `SearchString` property from the query string (`?searchString=Ghost`) or route data (`https://localhost:5001/Movies/Ghost`). Model binding is ***not*** case sensitive.

However, users cannot be expected to modify the URL to search for a movie. In this step, UI is added to filter movies. If you added the route constraint `"{searchString?}"`, remove it.

Open the `Pages/Movies/Index.cshtml` file, and add the markup highlighted in the following code:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    @*Markup removed for brevity.*@
```

The HTML `<form>` tag uses the following [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0):

* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). When the form is submitted, the filter string is sent to the *Pages/Movies/Index* page via query string.
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper)

Save the changes and test the filter.

## Search by genre

Update the `Movies/Index.cshtml.cs` page `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;

    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    if (!string.IsNullOrEmpty(MovieGenre))
    {
        movies = movies.Where(x => x.Genre == MovieGenre);
    }
    Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
    Movie = await movies.ToListAsync();
}
```

The following code is a LINQ query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres.

```
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
```

### Add search by genre to the Razor Page

Update the `Index.cshtml` [`<form>` element](https://developer.mozilla.org/docs/Web/HTML/Element/form) as highlighted in the following markup:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
```

Test the app by searching by genre, by movie title, and by both.

## Next steps

[Previous: Update the pages](da1?view=aspnetcore-10.0) [Next: Add a new field](new-field?view=aspnetcore-10.0)

In the following sections, searching movies by *genre* or *name* is added.

Add the following highlighted code to `Pages/Movies/Index.cshtml.cs`:

```
public class IndexModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    public IList<Movie> Movie { get;set; }  = default!;

    [BindProperty(SupportsGet = true)]
    public string? SearchString { get; set; }

    public SelectList? Genres { get; set; }

    [BindProperty(SupportsGet = true)]
    public string? MovieGenre { get; set; }
```

In the previous code:

* `SearchString`: Contains the text users enter in the search text box. `SearchString` has the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute. `[BindProperty]` binds form values and query strings with the same name as the property. `[BindProperty(SupportsGet = true)]` is required for binding on HTTP GET requests.
* `Genres`: Contains the list of genres. `Genres` allows the user to select a genre from the list. `SelectList` requires `using Microsoft.AspNetCore.Mvc.Rendering;`
* `MovieGenre`: Contains the specific genre the user selects. For example, "Western".
* `Genres` and `MovieGenre` are used later in this tutorial.

Warning

For security reasons, you must opt in to binding `GET` request data to page model properties. Verify user input before mapping it to properties. Opting into `GET` binding is useful when addressing scenarios that rely on query string or route values.

To bind a property on `GET` requests, set the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute's `SupportsGet` property to `true`:

```
[BindProperty(SupportsGet = true)]
```

For more information, see [ASP.NET Core Community Standup: Bind on GET discussion (YouTube)](https://www.youtube.com/watch?v=p7iHB9V-KVU\&feature=youtu.be\&t=54m27s).

Update the Index page's `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    var movies = from m in _context.Movie
                 select m;
    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    Movie = await movies.ToListAsync();
}
```

The first line of the `OnGetAsync` method creates a [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) query to select the movies:

```
// using System.Linq;
var movies = from m in _context.Movie
             select m;
```

The query is only ***defined*** at this point, it has ***not*** been run against the database.

If the `SearchString` property is not `null` or empty, the movies query is modified to filter on the search string:

```
if (!string.IsNullOrEmpty(SearchString))
{
    movies = movies.Where(s => s.Title.Contains(SearchString));
}
```

The `s => s.Title.Contains()` code is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/csharp/programming-guide/concepts/linq/query-syntax-and-method-syntax-in-linq) method or `Contains`. LINQ queries are not executed when they're defined or when they're modified by calling a method, such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. The evaluation of an expression is delayed until its realized value is iterated over or the `ToListAsync` method is called. See [Query Execution](/en-us/dotnet/csharp/linq/get-started/introduction-to-linq-queries#deferred) for more information.

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [This GitHub issue](https://github.com/dotnet/efcore/issues/11414)
* [This GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to the Movies page and append a query string such as `?searchString=Ghost` to the URL. For example, `https://localhost:5001/Movies?searchString=Ghost`. The filtered movies are displayed.

If the following route template is added to the Index page, the search string can be passed as a URL segment. For example, `https://localhost:5001/Movies/Ghost`.

```
@page "{searchString?}"
```

The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query string value. The `?` in `"{searchString?}"` means this is an optional route parameter.

The ASP.NET Core runtime uses [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) to set the value of the `SearchString` property from the query string (`?searchString=Ghost`) or route data (`https://localhost:5001/Movies/Ghost`). Model binding is ***not*** case sensitive.

However, users cannot be expected to modify the URL to search for a movie. In this step, UI is added to filter movies. If you added the route constraint `"{searchString?}"`, remove it.

Open the `Pages/Movies/Index.cshtml` file, and add the markup highlighted in the following code:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    @*Markup removed for brevity.*@
```

The HTML `<form>` tag uses the following [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0):

* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). When the form is submitted, the filter string is sent to the *Pages/Movies/Index* page via query string.
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper)

Save the changes and test the filter.

## Search by genre

Update the Index page's `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;

    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    if (!string.IsNullOrEmpty(MovieGenre))
    {
        movies = movies.Where(x => x.Genre == MovieGenre);
    }
    Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
    Movie = await movies.ToListAsync();
}
```

The following code is a LINQ query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres.

```
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
```

### Add search by genre to the Razor Page

Update the `Index.cshtml` [`<form>` element](https://developer.mozilla.org/docs/Web/HTML/Element/form) as highlighted in the following markup:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
```

Test the app by searching by genre, by movie title, and by both.

## Next steps

[Previous: Update the pages](da1?view=aspnetcore-10.0) [Next: Add a new field](new-field?view=aspnetcore-10.0)

In the following sections, searching movies by *genre* or *name* is added.

Add the following highlighted code to `Pages/Movies/Index.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{
    public class IndexModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IList<Movie> Movie { get;set; } = default!;
        [BindProperty(SupportsGet = true)]
        public string ? SearchString { get; set; }
        public SelectList ? Genres { get; set; }
        [BindProperty(SupportsGet = true)]
        public string ? MovieGenre { get; set; }
```

In the previous code:

* `SearchString`: Contains the text users enter in the search text box. `SearchString` has the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute. `[BindProperty]` binds form values and query strings with the same name as the property. `[BindProperty(SupportsGet = true)]` is required for binding on HTTP GET requests.
* `Genres`: Contains the list of genres. `Genres` allows the user to select a genre from the list. `SelectList` requires `using Microsoft.AspNetCore.Mvc.Rendering;`
* `MovieGenre`: Contains the specific genre the user selects. For example, "Western".
* `Genres` and `MovieGenre` are used later in this tutorial.

Warning

For security reasons, you must opt in to binding `GET` request data to page model properties. Verify user input before mapping it to properties. Opting into `GET` binding is useful when addressing scenarios that rely on query string or route values.

To bind a property on `GET` requests, set the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute's `SupportsGet` property to `true`:

```
[BindProperty(SupportsGet = true)]
```

For more information, see [ASP.NET Core Community Standup: Bind on GET discussion (YouTube)](https://www.youtube.com/watch?v=p7iHB9V-KVU\&feature=youtu.be\&t=54m27s).

Update the Index page's `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    var movies = from m in _context.Movie
                 select m;
    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    Movie = await movies.ToListAsync();
}
```

The first line of the `OnGetAsync` method creates a [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) query to select the movies:

```
// using System.Linq;
var movies = from m in _context.Movie
             select m;
```

The query is only ***defined*** at this point, it has ***not*** been run against the database.

If the `SearchString` property is not null or empty, the movies query is modified to filter on the search string:

```
if (!string.IsNullOrEmpty(SearchString))
{
    movies = movies.Where(s => s.Title.Contains(SearchString));
}
```

The `s => s.Title.Contains()` code is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/csharp/programming-guide/concepts/linq/query-syntax-and-method-syntax-in-linq) method or `Contains`. LINQ queries are not executed when they're defined or when they're modified by calling a method, such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. The evaluation of an expression is delayed until its realized value is iterated over or the `ToListAsync` method is called. See [Query Execution](/en-us/dotnet/csharp/linq/get-started/introduction-to-linq-queries#deferred) for more information.

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [This GitHub issue](https://github.com/dotnet/efcore/issues/11414)
* [This GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to the Movies page and append a query string such as `?searchString=Ghost` to the URL. For example, `https://localhost:5001/Movies?searchString=Ghost`. The filtered movies are displayed.

If the following route template is added to the Index page, the search string can be passed as a URL segment. For example, `https://localhost:5001/Movies/Ghost`.

```
@page "{searchString?}"
```

The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query string value. The `?` in `"{searchString?}"` means this is an optional route parameter.

The ASP.NET Core runtime uses [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) to set the value of the `SearchString` property from the query string (`?searchString=Ghost`) or route data (`https://localhost:5001/Movies/Ghost`). Model binding is ***not*** case sensitive.

However, users cannot be expected to modify the URL to search for a movie. In this step, UI is added to filter movies. If you added the route constraint `"{searchString?}"`, remove it.

Open the `Pages/Movies/Index.cshtml` file, and add the markup highlighted in the following code:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    @*Markup removed for brevity.*@
```

The HTML `<form>` tag uses the following [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0):

* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). When the form is submitted, the filter string is sent to the *Pages/Movies/Index* page via query string.
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper)

Save the changes and test the filter.

## Search by genre

Update the Index page's `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;

    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    if (!string.IsNullOrEmpty(MovieGenre))
    {
        movies = movies.Where(x => x.Genre == MovieGenre);
    }
    Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
    Movie = await movies.ToListAsync();
}
```

The following code is a LINQ query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres.

```
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
```

### Add search by genre to the Razor Page

Update the `Index.cshtml` [`<form>` element](https://developer.mozilla.org/docs/Web/HTML/Element/form) as highlighted in the following markup:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>
```

Test the app by searching by genre, by movie title, and by both.

## Next steps

[Previous: Update the pages](da1?view=aspnetcore-10.0) [Next: Add a new field](new-field?view=aspnetcore-10.0)

In the following sections, searching movies by *genre* or *name* is added.

Add the following highlighted using statement and properties to `Pages/Movies/Index.cshtml.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace RazorPagesMovie.Pages.Movies
{

    public class IndexModel : PageModel
    {
        private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

        public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IList<Movie> Movie { get; set; }
        [BindProperty(SupportsGet = true)]
        public string SearchString { get; set; }
        public SelectList Genres { get; set; }
        [BindProperty(SupportsGet = true)]
        public string MovieGenre { get; set; }
```

In the previous code:

* `SearchString`: Contains the text users enter in the search text box. `SearchString` has the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute. `[BindProperty]` binds form values and query strings with the same name as the property. `[BindProperty(SupportsGet = true)]` is required for binding on HTTP GET requests.
* `Genres`: Contains the list of genres. `Genres` allows the user to select a genre from the list. `SelectList` requires `using Microsoft.AspNetCore.Mvc.Rendering;`
* `MovieGenre`: Contains the specific genre the user selects. For example, "Western".
* `Genres` and `MovieGenre` are used later in this tutorial.

Warning

For security reasons, you must opt in to binding `GET` request data to page model properties. Verify user input before mapping it to properties. Opting into `GET` binding is useful when addressing scenarios that rely on query string or route values.

To bind a property on `GET` requests, set the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute's `SupportsGet` property to `true`:

```
[BindProperty(SupportsGet = true)]
```

For more information, see [ASP.NET Core Community Standup: Bind on GET discussion (YouTube)](https://www.youtube.com/watch?v=p7iHB9V-KVU\&feature=youtu.be\&t=54m27s).

Update the Index page's `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    var movies = from m in _context.Movie
                 select m;
    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    Movie = await movies.ToListAsync();
}
```

The first line of the `OnGetAsync` method creates a [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) query to select the movies:

```
// using System.Linq;
var movies = from m in _context.Movie
             select m;
```

The query is only ***defined*** at this point, it has ***not*** been run against the database.

If the `SearchString` property is not null or empty, the movies query is modified to filter on the search string:

```
if (!string.IsNullOrEmpty(SearchString))
{
    movies = movies.Where(s => s.Title.Contains(SearchString));
}
```

The `s => s.Title.Contains()` code is a [Lambda Expression](/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). Lambdas are used in method-based [LINQ](/en-us/dotnet/csharp/programming-guide/concepts/linq/) queries as arguments to standard query operator methods such as the [Where](/en-us/dotnet/csharp/programming-guide/concepts/linq/query-syntax-and-method-syntax-in-linq) method or `Contains`. LINQ queries are not executed when they're defined or when they're modified by calling a method, such as `Where`, `Contains`, or `OrderBy`. Rather, query execution is deferred. The evaluation of an expression is delayed until its realized value is iterated over or the `ToListAsync` method is called. See [Query Execution](/en-us/dotnet/csharp/linq/get-started/introduction-to-linq-queries#deferred) for more information.

Note

The [Contains](/en-us/dotnet/api/system.data.objects.dataclasses.entitycollection-1.contains) method is run on the database, not in the C# code. The case sensitivity on the query depends on the database and the collation. On SQL Server, `Contains` maps to [SQL LIKE](/en-us/sql/t-sql/language-elements/like-transact-sql), which is case insensitive. SQLite with the default collation is a mixture of case sensitive and case ***IN***&#x73;ensitive, depending on the query. For information on making case insensitive SQLite queries, see the following:

* [This GitHub issue](https://github.com/dotnet/efcore/issues/11414).
* [This GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/22314)
* [Collations and Case Sensitivity](/en-us/ef/core/miscellaneous/collations-and-case-sensitivity)

Navigate to the Movies page and append a query string such as `?searchString=Ghost` to the URL. For example, `https://localhost:5001/Movies?searchString=Ghost`. The filtered movies are displayed.

If the following route template is added to the Index page, the search string can be passed as a URL segment. For example, `https://localhost:5001/Movies/Ghost`.

```
@page "{searchString?}"
```

The preceding route constraint allows searching the title as route data (a URL segment) instead of as a query string value. The `?` in `"{searchString?}"` means this is an optional route parameter.

The ASP.NET Core runtime uses [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) to set the value of the `SearchString` property from the query string (`?searchString=Ghost`) or route data (`https://localhost:5001/Movies/Ghost`). Model binding is ***not*** case sensitive.

However, users cannot be expected to modify the URL to search for a movie. In this step, UI is added to filter movies. If you added the route constraint `"{searchString?}"`, remove it.

Open the `Pages/Movies/Index.cshtml` file, and add the markup highlighted in the following code:

```
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form>
    <p>
        <label>Title: <input type="text" asp-for="SearchString" /></label>
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    @*Markup removed for brevity.*@
```

The HTML `<form>` tag uses the following [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0):

* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper). When the form is submitted, the filter string is sent to the *Pages/Movies/Index* page via query string.
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper)

Save the changes and test the filter.

## Search by genre

Update the Index page's `OnGetAsync` method with the following code:

```
public async Task OnGetAsync()
{
    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;

    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(SearchString))
    {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    if (!string.IsNullOrEmpty(MovieGenre))
    {
        movies = movies.Where(x => x.Genre == MovieGenre);
    }
    Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
    Movie = await movies.ToListAsync();
}
```

The following code is a LINQ query that retrieves all the genres from the database.

```
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;
```

The `SelectList` of genres is created by projecting the distinct genres.

```
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
```

### Add search by genre to the Razor Page

1. Update the `Index.cshtml` [`<form>` element](https://developer.mozilla.org/docs/Web/HTML/Element/form) as highlighted in the following markup:

   ```
   @page
   @model RazorPagesMovie.Pages.Movies.IndexModel

   @{
       ViewData["Title"] = "Index";
   }

   <h1>Index</h1>

   <p>
       <a asp-page="Create">Create New</a>
   </p>

   <form>
       <p>
           <select asp-for="MovieGenre" asp-items="Model.Genres">
               <option value="">All</option>
           </select>
           <label>Title: <input type="text" asp-for="SearchString" /></label>
           <input type="submit" value="Filter" />
       </p>
   </form>

   <table class="table">
       @*Markup removed for brevity.*@
   ```

2. Test the app by searching by genre, by movie title, and by both.

## Next steps

[Previous: Update the pages](da1?view=aspnetcore-10.0) [Next: Add a new field](new-field?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/streaming
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/streaming.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use streaming in ASP.NET Core SignalR

By [Brennan Conroy](https://github.com/BrennanConroy)

ASP.NET Core SignalR supports streaming from client to server and from server to client. This is useful for scenarios where fragments of data arrive over time. When streaming, each fragment is sent to the client or server as soon as it becomes available, rather than waiting for all of the data to become available.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/signalr/streaming/samples/) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Set up a hub for streaming

A hub method automatically becomes a streaming hub method when it returns [IAsyncEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1), [ChannelReader\<T>](/en-us/dotnet/api/system.threading.channels.channelreader-1), `Task<IAsyncEnumerable<T>>`, or `Task<ChannelReader<T>>`.

### Server-to-client streaming

Streaming hub methods can return `IAsyncEnumerable<T>` in addition to `ChannelReader<T>`. The simplest way to return `IAsyncEnumerable<T>` is by making the hub method an async iterator method as the following sample demonstrates. Hub async iterator methods can accept a `CancellationToken` parameter that's triggered when the client unsubscribes from the stream. Async iterator methods avoid problems common with Channels, such as not returning the `ChannelReader` early enough or exiting the method without completing the [ChannelWriter\<T>](/en-us/dotnet/api/system.threading.channels.channelwriter-1).

Note

The following sample requires C# 8.0 or later.

```
public class AsyncEnumerableHub : Hub
{
    public async IAsyncEnumerable<int> Counter(
        int count,
        int delay,
        [EnumeratorCancellation]
        CancellationToken cancellationToken)
    {
        for (var i = 0; i < count; i++)
        {
            // Check the cancellation token regularly so that the server will stop
            // producing items if the client disconnects.
            cancellationToken.ThrowIfCancellationRequested();

            yield return i;

            // Use the cancellationToken in other APIs that accept cancellation
            // tokens so the cancellation can flow down to them.
            await Task.Delay(delay, cancellationToken);
        }
    }
}
```

The following sample shows the basics of streaming data to the client using Channels. Whenever an object is written to the [ChannelWriter\<T>](/en-us/dotnet/api/system.threading.channels.channelwriter-1), the object is immediately sent to the client. At the end, the `ChannelWriter` is completed to tell the client the stream is closed.

Note

Write to the `ChannelWriter<T>` on a background thread and return the `ChannelReader` as soon as possible. Other hub invocations are blocked until a `ChannelReader` is returned.

Wrap logic in a [`try ... catch` statement](/en-us/dotnet/csharp/language-reference/keywords/try-catch). Complete the `Channel` in a [`finally` block](/en-us/dotnet/csharp/language-reference/keywords/try-catch-finally). If you want to flow an error, capture it inside the `catch` block and write it in the `finally` block.

```
public ChannelReader<int> Counter(
    int count,
    int delay,
    CancellationToken cancellationToken)
{
    var channel = Channel.CreateUnbounded<int>();

    // We don't want to await WriteItemsAsync, otherwise we'd end up waiting 
    // for all the items to be written before returning the channel back to
    // the client.
    _ = WriteItemsAsync(channel.Writer, count, delay, cancellationToken);

    return channel.Reader;
}

private async Task WriteItemsAsync(
    ChannelWriter<int> writer,
    int count,
    int delay,
    CancellationToken cancellationToken)
{
    Exception localException = null;
    try
    {
        for (var i = 0; i < count; i++)
        {
            await writer.WriteAsync(i, cancellationToken);

            // Use the cancellationToken in other APIs that accept cancellation
            // tokens so the cancellation can flow down to them.
            await Task.Delay(delay, cancellationToken);
        }
    }
    catch (Exception ex)
    {
        localException = ex;
    }
    finally
    {
        writer.Complete(localException);
    }
}
```

Server-to-client streaming hub methods can accept a `CancellationToken` parameter that's triggered when the client unsubscribes from the stream. Use this token to stop the server operation and release any resources if the client disconnects before the end of the stream.

### Client-to-server streaming

A hub method automatically becomes a client-to-server streaming hub method when it accepts one or more objects of type [ChannelReader\<T>](/en-us/dotnet/api/system.threading.channels.channelreader-1) or [IAsyncEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1). The following sample shows the basics of reading streaming data sent from the client. Whenever the client writes to the [ChannelWriter\<T>](/en-us/dotnet/api/system.threading.channels.channelwriter-1), the data is written into the `ChannelReader` on the server from which the hub method is reading.

```
public async Task UploadStream(ChannelReader<string> stream)
{
    while (await stream.WaitToReadAsync())
    {
        while (stream.TryRead(out var item))
        {
            // do something with the stream item
            Console.WriteLine(item);
        }
    }
}
```

An [IAsyncEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1) version of the method follows.

Note

The following sample requires C# 8.0 or later.

```
public async Task UploadStream(IAsyncEnumerable<string> stream)
{
    await foreach (var item in stream)
    {
        Console.WriteLine(item);
    }
}
```

## .NET client

### Server-to-client streaming

The `StreamAsync` and `StreamAsChannelAsync` methods on `HubConnection` are used to invoke server-to-client streaming methods. Pass the hub method name and arguments defined in the hub method to `StreamAsync` or `StreamAsChannelAsync`. The generic parameter on `StreamAsync<T>` and `StreamAsChannelAsync<T>` specifies the type of objects returned by the streaming method. An object of type `IAsyncEnumerable<T>` or `ChannelReader<T>` is returned from the stream invocation and represents the stream on the client.

A `StreamAsync` example that returns `IAsyncEnumerable<int>`:

```
// Call "Cancel" on this CancellationTokenSource to send a cancellation message to
// the server, which will trigger the corresponding token in the hub method.
var cancellationTokenSource = new CancellationTokenSource();
var stream = hubConnection.StreamAsync<int>(
    "Counter", 10, 500, cancellationTokenSource.Token);

await foreach (var count in stream)
{
    Console.WriteLine($"{count}");
}

Console.WriteLine("Streaming completed");
```

A corresponding `StreamAsChannelAsync` example that returns `ChannelReader<int>`:

```
// Call "Cancel" on this CancellationTokenSource to send a cancellation message to
// the server, which will trigger the corresponding token in the hub method.
var cancellationTokenSource = new CancellationTokenSource();
var channel = await hubConnection.StreamAsChannelAsync<int>(
    "Counter", 10, 500, cancellationTokenSource.Token);

// Wait asynchronously for data to become available
while (await channel.WaitToReadAsync())
{
    // Read all currently available data synchronously, before waiting for more data
    while (channel.TryRead(out var count))
    {
        Console.WriteLine($"{count}");
    }
}

Console.WriteLine("Streaming completed");
```

In the previous code:

* The `StreamAsChannelAsync` method on `HubConnection` is used to invoke a server-to-client streaming method. Pass the hub method name and arguments defined in the hub method to `StreamAsChannelAsync`.
* The generic parameter on `StreamAsChannelAsync<T>` specifies the type of objects returned by the streaming method.
* A `ChannelReader<T>` is returned from the stream invocation and represents the stream on the client.

### Client-to-server streaming

There are two ways to invoke a client-to-server streaming hub method from the .NET client. You can either pass in an `IAsyncEnumerable<T>` or a `ChannelReader` as an argument to `SendAsync`, `InvokeAsync`, or `StreamAsChannelAsync`, depending on the hub method invoked.

Whenever data is written to the `IAsyncEnumerable` or `ChannelWriter` object, the hub method on the server receives a new item with the data from the client.

If using an `IAsyncEnumerable` object, the stream ends after the method returning stream items exits.

Note

The following sample requires C# 8.0 or later.

```
async IAsyncEnumerable<string> clientStreamData()
{
    for (var i = 0; i < 5; i++)
    {
        var data = await FetchSomeData();
        yield return data;
    }
    //After the for loop has completed and the local function exits the stream completion will be sent.
}

await connection.SendAsync("UploadStream", clientStreamData());
```

Or if you're using a `ChannelWriter`, you complete the channel with `channel.Writer.Complete()`:

```
var channel = Channel.CreateBounded<string>(10);
await connection.SendAsync("UploadStream", channel.Reader);
await channel.Writer.WriteAsync("some data");
await channel.Writer.WriteAsync("some more data");
channel.Writer.Complete();
```

## JavaScript client

### Server-to-client streaming

JavaScript clients call server-to-client streaming methods on hubs with `connection.stream`. The `stream` method accepts two arguments:

* The name of the hub method. In the following example, the hub method name is `Counter`.
* Arguments defined in the hub method. In the following example, the arguments are a count for the number of stream items to receive and the delay between stream items.

`connection.stream` returns an `IStreamResult`, which contains a `subscribe` method. Pass an `IStreamSubscriber` to `subscribe` and set the `next`, `error`, and `complete` callbacks to receive notifications from the `stream` invocation.

```
connection.stream("Counter", 10, 500)
    .subscribe({
        next: (item) => {
            var li = document.createElement("li");
            li.textContent = item;
            document.getElementById("messagesList").appendChild(li);
        },
        complete: () => {
            var li = document.createElement("li");
            li.textContent = "Stream completed";
            document.getElementById("messagesList").appendChild(li);
        },
        error: (err) => {
            var li = document.createElement("li");
            li.textContent = err;
            document.getElementById("messagesList").appendChild(li);
        },
});
```

To end the stream from the client, call the `dispose` method on the `ISubscription` that's returned from the `subscribe` method. Calling this method causes cancellation of the `CancellationToken` parameter of the Hub method, if you provided one.

### Client-to-server streaming

JavaScript clients call client-to-server streaming methods on hubs by passing in a `Subject` as an argument to `send`, `invoke`, or `stream`, depending on the hub method invoked. The `Subject` is a class that looks like a `Subject`. For example in RxJS, you can use the [Subject](https://rxjs-dev.firebaseapp.com/api/index/class/Subject) class from that library.

```
const subject = new signalR.Subject();
yield connection.send("UploadStream", subject);
var iteration = 0;
const intervalHandle = setInterval(() => {
    iteration++;
    subject.next(iteration.toString());
    if (iteration === 10) {
        clearInterval(intervalHandle);
        subject.complete();
    }
}, 500);
```

Calling `subject.next(item)` with an item writes the item to the stream, and the hub method receives the item on the server.

To end the stream, call `subject.complete()`.

## Java client

### Server-to-client streaming

The SignalR Java client uses the `stream` method to invoke streaming methods. `stream` accepts three or more arguments:

* The expected type of the stream items.
* The name of the hub method.
* Arguments defined in the hub method.

```
hubConnection.stream(String.class, "ExampleStreamingHubMethod", "Arg1")
    .subscribe(
        (item) -> {/* Define your onNext handler here. */ },
        (error) -> {/* Define your onError handler here. */},
        () -> {/* Define your onCompleted handler here. */});
```

The `stream` method on `HubConnection` returns an Observable of the stream item type. The Observable type's `subscribe` method is where `onNext`, `onError` and `onCompleted` handlers are defined.

### Client-to-server streaming

The SignalR Java client can call client-to-server streaming methods on hubs by passing in an [Observable](https://rxjs-dev.firebaseapp.com/api/index/class/Observable) as an argument to `send`, `invoke`, or `stream`, depending on the hub method invoked.

```
ReplaySubject<String> stream = ReplaySubject.create();
hubConnection.send("UploadStream", stream);
stream.onNext("FirstItem");
stream.onNext("SecondItem");
stream.onComplete();
```

Calling `stream.onNext(item)` with an item writes the item to the stream, and the hub method receives the item on the server.

To end the stream, call `stream.onComplete()`.

## Related content

* [Hubs](hubs?view=aspnetcore-10.0)
* [.NET client](dotnet-client?view=aspnetcore-10.0)
* [JavaScript client](javascript-client?view=aspnetcore-10.0)
* [Publish to Azure](publish-to-azure-web-app?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/tutorials/build-a-blazor-app
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/tutorials/build-a-blazor-app.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Blazor todo list app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This tutorial provides a basic working experience for building and modifying a Blazor app. For detailed Blazor guidance, see the [Blazor reference documentation](../?view=aspnetcore-10.0).

Learn how to:

* Create a todo list Blazor app project
* Modify Razor components
* Use event handling and data binding in components
* Use routing in a Blazor app

At the end of this tutorial, you'll have a working todo list app.

## Prerequisites

[Download and install .NET](https://dotnet.microsoft.com/download/dotnet) if it isn't already installed on the system or if the system doesn't have the latest version installed.

## Create a Blazor app

Create a new Blazor Web App named `TodoList` in a command shell:

```
dotnet new blazor -o TodoList
```

The `-o|--output` option creates a folder for the project. If you've created a folder for the project and the command shell is open in that folder, omit the `-o|--output` option and value to create the project.

Use either of the following hosting models to create a new Blazor app named `TodoList` in a command shell:

* For an experience with Blazor Server, create the app with the following command:

  ```
  dotnet new blazorserver -o TodoList
  ```

* For an experience with Blazor WebAssembly, create the app with the following command:

  ```
  dotnet new blazorwasm -o TodoList
  ```

The preceding command creates a folder named `TodoList` with the `-o|--output` option to hold the app. The `TodoList` folder is the *root folder* of the project. Change directories to the `TodoList` folder with the following command:

```
cd TodoList
```

## Build a todo list Blazor app

Add a new `Todo` Razor component to the app using the following command:

```
dotnet new razorcomponent -n Todo -o Components/Pages
```

The `-n|--name` option in the preceding command specifies the name of the new Razor component. The new component is created in the project's `Components/Pages` folder with the `-o|--output` option.

```
dotnet new razorcomponent -n Todo -o Pages
```

The `-n|--name` option in the preceding command specifies the name of the new Razor component. The new component is created in the project's `Pages` folder with the `-o|--output` option.

Important

Razor component file names require a capitalized first letter. Open the `Pages` folder and confirm that the `Todo` component file name starts with a capital letter `T`. The file name should be `Todo.razor`.

Open the `Todo` component in any file editor and make the following changes at the top of the file:

* Add an `@page` Razor directive with a relative URL of `/todo`.
* Enable interactivity on the page so that it isn't just statically rendered. The Interactive Server render mode enables the component to handle UI events from the server.
* Add a page title with the `PageTitle` component, which enables adding an HTML `<title>` element to the page.

Open the `Todo` component in any file editor and make the following changes at the top of the file:

* Add an `@page` Razor directive with a relative URL of `/todo`.
* Add a page title with the `PageTitle` component, which enables adding an HTML `<title>` element to the page.

Open the `Todo` component in any file editor and add an `@page` Razor directive with a relative URL of `/todo`.

`Todo.razor`:

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

@code {

}
```

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

@code {

}
```

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

@code {

}
```

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

@code {

}
```

```
@page "/todo"

<h3>Todo</h3>

@code {

}
```

```
@page "/todo"

<h3>Todo</h3>

@code {

}
```

Save the `Todo.razor` file.

Add the `Todo` component to the navigation bar.

The `NavMenu` component is used in the app's layout. Layouts are components that allow you to avoid duplication of content in an app. The `NavLink` component provides a cue in the app's UI when the component URL is loaded by the app.

In the navigation element (`<nav>`) content of the `NavMenu` component, add the following `<div>` element for the `Todo` component.

In `Components/Layout/NavMenu.razor`:

In `Shared/NavMenu.razor`:

```
<div class="nav-item px-3">
    <NavLink class="nav-link" href="todo">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Todo
    </NavLink>
</div>
```

Save the `NavMenu.razor` file.

Build and run the app by executing the [`dotnet watch run`](../../tutorials/dotnet-watch?view=aspnetcore-10.0) command in the command shell from the `TodoList` folder. After the app is running, visit the new Todo page by selecting the **`Todo`** link in the app's navigation bar, which loads the page at `/todo`.

Leave the app running the command shell. Each time a file is saved, the app is automatically rebuilt, and the page in the browser is automatically reloaded.

Add a `TodoItem.cs` file to the root of the project (the `TodoList` folder) to hold a class that represents a todo item. Use the following C# code for the `TodoItem` class.

`TodoItem.cs`:

```
public class TodoItem
{
    public string? Title { get; set; }
    public bool IsDone { get; set; }
}
```

```
public class TodoItem
{
    public string? Title { get; set; }
    public bool IsDone { get; set; }
}
```

```
public class TodoItem
{
    public string? Title { get; set; }
    public bool IsDone { get; set; }
}
```

```
public class TodoItem
{
    public string? Title { get; set; }
    public bool IsDone { get; set; }
}
```

```
public class TodoItem
{
    public string Title { get; set; }
    public bool IsDone { get; set; }
}
```

```
public class TodoItem
{
    public string Title { get; set; }
    public bool IsDone { get; set; }
}
```

Note

If using Visual Studio to create the `TodoItem.cs` file and `TodoItem` class, use ***either*** of the following approaches:

* Remove the namespace that Visual Studio generates for the class.
* Use the **Copy** button in the preceding code block and replace the entire contents of the file that Visual Studio generates.

Return to the `Todo` component and perform the following tasks:

* Add a field for the todo items in the `@code` block. The `Todo` component uses this field to maintain the state of the todo list.
* Add unordered list markup and a `foreach` loop to render each todo item as a list item (`<li>`).

`Components/Pages/Todo.razor`:

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

@code {
    private List<TodoItem> todos = [];
}
```

`Components/Pages/Todo.razor`:

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

@code {
    private List<TodoItem> todos = [];
}
```

`Pages/Todo.razor`:

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

@code {
    private List<TodoItem> todos = new();
}
```

`Pages/Todo.razor`:

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

@code {
    private List<TodoItem> todos = new();
}
```

`Pages/Todo.razor`:

```
@page "/todo"

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

@code {
    private List<TodoItem> todos = new();
}
```

`Pages/Todo.razor`:

```
@page "/todo"

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

@code {
    private IList<TodoItem> todos = new List<TodoItem>();
}
```

The app requires UI elements for adding todo items to the list. Add a text input (`<input>`) and a button (`<button>`) below the unordered list (`<ul>...</ul>`):

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" />
<button>Add todo</button>

@code {
    private List<TodoItem> todos = [];
}
```

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" />
<button>Add todo</button>

@code {
    private List<TodoItem> todos = [];
}
```

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" />
<button>Add todo</button>

@code {
    private List<TodoItem> todos = new();
}
```

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" />
<button>Add todo</button>

@code {
    private List<TodoItem> todos = new();
}
```

```
@page "/todo"

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" />
<button>Add todo</button>

@code {
    private List<TodoItem> todos = new();
}
```

```
@page "/todo"

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" />
<button>Add todo</button>

@code {
    private IList<TodoItem> todos = new List<TodoItem>();
}
```

Save the `TodoItem.cs` file and the updated `Todo.razor` file. In the command shell, the app is automatically rebuilt when the files are saved. The browser reloads the page.

When the **`Add todo`** button is selected, nothing happens because an event handler isn't attached to the button.

Add an `AddTodo` method to the `Todo` component and register the method for the button using the `@onclick` attribute. The `AddTodo` C# method is called when the button is selected:

```
<input placeholder="Something todo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = [];

    private void AddTodo()
    {
        // Todo: Add the todo
    }
}
```

```
<input placeholder="Something todo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = [];

    private void AddTodo()
    {
        // Todo: Add the todo
    }
}
```

```
<input placeholder="Something todo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();

    private void AddTodo()
    {
        // Todo: Add the todo
    }
}
```

```
<input placeholder="Something todo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();

    private void AddTodo()
    {
        // Todo: Add the todo
    }
}
```

```
<input placeholder="Something todo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();

    private void AddTodo()
    {
        // Todo: Add the todo
    }
}
```

```
<input placeholder="Something todo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private IList<TodoItem> todos = new List<TodoItem>();

    private void AddTodo()
    {
        // Todo: Add the todo
    }
}
```

To get the title of the new todo item, add a `newTodo` string field at the top of the `@code` block:

```
private string? newTodo;
```

```
private string newTodo;
```

Modify the text `<input>` element to bind `newTodo` with the `@bind` attribute:

```
<input placeholder="Something todo" @bind="newTodo" />
```

Update the `AddTodo` method to add the `TodoItem` with the specified title to the list. Clear the value of the text input by setting `newTodo` to an empty string:

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = [];
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = [];
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();
    private string newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"

<h3>Todo</h3>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private IList<TodoItem> todos = new List<TodoItem>();
    private string newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

Save the `Todo.razor` file. The app is automatically rebuilt in the command shell, and the page reloads in the browser.

The title text for each todo item can be made editable, and a checkbox can help the user keep track of completed items. Add a checkbox input for each todo item and bind its value to the `IsDone` property. Change `@todo.Title` to an `<input>` element bound to `todo.Title` with `@bind`:

```
<ul>
      @foreach (var todo in todos)
      {
         <li>
            <input type="checkbox" @bind="todo.IsDone" />
            <input @bind="todo.Title" />
         </li>
      }
</ul>
```

Update the `<h3>` header to show a count of the number of todo items that aren't complete (`IsDone` is `false`). The Razor expression in the following header evaluates each time Blazor rerenders the component.

```
<h3>Todo (@todos.Count(todo => !todo.IsDone))</h3>
```

The completed `Todo` component:

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

<ul>
    @foreach (var todo in todos)
    {
        <li>
            <input type="checkbox" @bind="todo.IsDone" />
            <input @bind="todo.Title" />
        </li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = [];
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"
@rendermode InteractiveServer

<PageTitle>Todo</PageTitle>

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

<ul>
    @foreach (var todo in todos)
    {
        <li>
            <input type="checkbox" @bind="todo.IsDone" />
            <input @bind="todo.Title" />
        </li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = [];
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

<ul>
    @foreach (var todo in todos)
    {
        <li>
            <input type="checkbox" @bind="todo.IsDone" />
            <input @bind="todo.Title" />
        </li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"

<PageTitle>Todo</PageTitle>

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

<ul>
    @foreach (var todo in todos)
    {
        <li>
            <input type="checkbox" @bind="todo.IsDone" />
            <input @bind="todo.Title" />
        </li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

<ul>
    @foreach (var todo in todos)
    {
        <li>
            <input type="checkbox" @bind="todo.IsDone" />
            <input @bind="todo.Title" />
        </li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

```
@page "/todo"

<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>

<ul>
    @foreach (var todo in todos)
    {
        <li>
            <input type="checkbox" @bind="todo.IsDone" />
            <input @bind="todo.Title" />
        </li>
    }
</ul>

<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

@code {
    private List<TodoItem> todos = new();
    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}
```

Save the `Todo.razor` file. The app is automatically rebuilt in the command shell, and the page reloads in the browser.

Add items, edit items, and mark todo items done to test the component.

When finished, shut down the app in the command shell. Many command shells accept the keyboard command `Ctrl`+`C` to stop an app.

## Publish to Azure

For information on deploying to Azure, see [Quickstart: Deploy an ASP.NET web app](/en-us/azure/app-service/quickstart-dotnetcore).

## Next steps

In this tutorial, you learned how to:

* Create a todo list Blazor app project
* Modify Razor components
* Use event handling and data binding in components
* Use routing in a Blazor app

Learn about tooling for ASP.NET Core Blazor:

[ASP.NET Core Blazor](../?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/controller-methods-views
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/controller-methods-views.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 6, controller methods and views in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

We have a good start to the movie app, but the presentation isn't ideal, for example, **ReleaseDate** should be two words.

Open the `Models/Movie.cs` file and add the highlighted lines shown below:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    
    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

`DataAnnotations` are explained in the next tutorial. The [Display](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies what to display for the name of a field (in this case "Release Date" instead of "ReleaseDate"). The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (Date).

Using the data annotations above, run the application and refresh the `/Movies` page. Because the view markup uses the `@Html.DisplayNameFor` and `@Html.DisplayFor` methods to render the property name and value, the updated `Index` view shows all the fields properly formatted. For example, **Release Date** is now two words.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

Browse to the `Movies` controller and hold the mouse pointer over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the Core MVC Anchor Tag Helper in the `Views/Movies/Index.cshtml` file.

```
        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files. In the code above, the `AnchorTagHelper` dynamically generates the HTML `href` attribute value from the controller action method and route id. You use **View Source** from your favorite browser or use the developer tools to examine the generated markup. A portion of the generated HTML is shown below:

```
 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>
```

Recall the format for [routing](../../mvc/controllers/routing?view=aspnetcore-10.0) set in the `Program.cs` file:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}")
    .WithStaticAssets();
```

ASP.NET Core translates `https://localhost:5001/Movies/Edit/4` into a request to the `Edit` action method of the `Movies` controller with the parameter `Id` of 4. (Controller methods are also known as action methods.)

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) are one of the most popular new features in ASP.NET Core. For more information, see [Related content](#related-content).

[]()

Open the `Movies` controller and examine the two `Edit` action methods. The following code shows the `HTTP GET Edit` method, which fetches the movie and populates the edit form generated by the `Edit.cshtml` Razor file.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

The following code shows the `HTTP POST Edit` method, which processes the posted movie values:

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[Bind]` attribute is one way to protect against [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application#overpost). You should only include properties in the `[Bind]` attribute that you want to change. For more information, see [Protect your controller from over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application). [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/) provide an alternative approach to prevent over-posting.

Notice the second `Edit` action method is preceded by the `[HttpPost]` attribute.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `HttpPost` attribute specifies that this `Edit` method can be invoked *only* for `POST` requests. You could apply the `[HttpGet]` attribute to the first edit method, but that's not necessary because `[HttpGet]` is the default.

The `ValidateAntiForgeryToken` attribute is used to [prevent forgery of a request](../../security/anti-request-forgery?view=aspnetcore-10.0) and is paired up with an antiforgery token generated in the edit view file (`Views/Movies/Edit.cshtml`). The edit view file generates the antiforgery token with the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

```
<form asp-action="Edit">
```

The [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates a hidden antiforgery token that must match the `[ValidateAntiForgeryToken]` generated antiforgery token in the `Edit` method of the Movies controller. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0).

The `HttpGet Edit` method takes the movie `ID` parameter, looks up the movie using the Entity Framework `FindAsync` method, and returns the selected movie to the Edit view. If a movie cannot be found, `NotFound` (HTTP 404) is returned.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

When the scaffolding system created the Edit view, it examined the `Movie` class and created code to render `<label>` and `<input>` elements for each property of the class. The following example shows the Edit view that was generated by the Visual Studio scaffolding system:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Notice how the view template has a `@model MvcMovie.Models.Movie` statement at the top of the file. `@model MvcMovie.Models.Movie` specifies that the view expects the model for the view template to be of type `Movie`.

The scaffolded code uses several Tag Helper methods to streamline the HTML markup. The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays the name of the field ("Title", "ReleaseDate", "Genre", or "Price"). The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) renders an HTML `<input>` element. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays any validation messages associated with that property.

Run the application and navigate to the `/Movies` URL. Click an **Edit** link. In the browser, view the source for the page. The generated HTML for the `<form>` element is shown below.

```
<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>
```

The `<input>` elements are in an `HTML <form>` element whose `action` attribute is set to post to the `/Movies/Edit/id` URL. The form data will be posted to the server when the `Save` button is clicked. The last line before the closing `</form>` element shows the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

## Processing the POST Request

The following listing shows the `[HttpPost]` version of the `Edit` action method.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[ValidateAntiForgeryToken]` attribute validates the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the antiforgery token generator in the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

The [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system takes the posted form values and creates a `Movie` object that's passed as the `movie` parameter. The `ModelState.IsValid` property verifies that the data submitted in the form can be used to modify (edit or update) a `Movie` object. If the data is valid, it's saved. The updated (edited) movie data is saved to the database by calling the `SaveChangesAsync` method of database context. After saving the data, the code redirects the user to the `Index` action method of the `MoviesController` class, which displays the movie collection, including the changes just made.

Before the form is posted to the server, client-side validation checks any validation rules on the fields. If there are any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't have client-side validation but the server will detect the posted values that are not valid, and the form values will be redisplayed with error messages. Later in the tutorial we examine [Model Validation](../../mvc/models/validation?view=aspnetcore-10.0) in more detail. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) in the `Views/Movies/Edit.cshtml` view template takes care of displaying appropriate error messages.

All the `HttpGet` methods in the movie controller follow a similar pattern. They get a movie object (or list of objects, in the case of `Index`), and pass the object (model) to the view. The `Create` method passes an empty movie object to the `Create` view. All the methods that create, edit, delete, or otherwise modify data do so in the `[HttpPost]` overload of the method. Modifying data in an `HTTP GET` method is a security risk. Modifying data in an `HTTP GET` method also violates HTTP best practices and the architectural [REST](http://rest.elkstein.org/) pattern, which specifies that GET requests shouldn't change the state of your application. In other words, performing a GET operation should be a safe operation that has no side effects and doesn't modify your persisted data.

## Related content

* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)
* [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0)
* Protect your controller from [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application)
* [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/)
* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Select Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

[Previous](working-with-sql?view=aspnetcore-10.0) [Next](search?view=aspnetcore-10.0)

We have a good start to the movie app, but the presentation isn't ideal, for example, **ReleaseDate** should be two words.

Open the `Models/Movie.cs` file and add the highlighted lines shown below:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    
    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

`DataAnnotations` are explained in the next tutorial. The [Display](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies what to display for the name of a field (in this case "Release Date" instead of "ReleaseDate"). The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (Date), so the time information stored in the field isn't displayed.

Using the data annotations above, run the application and refresh the `/Movies` page. Because the view markup uses the `@Html.DisplayNameFor` and `@Html.DisplayFor` methods to render the property name and value, the updated `Index` view shows all the fields properly formatted. For example, **Release Date** is now two words and the time information is no longer shown.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

Browse to the `Movies` controller and hold the mouse pointer over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the Core MVC Anchor Tag Helper in the `Views/Movies/Index.cshtml` file.

```
        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files. In the code above, the `AnchorTagHelper` dynamically generates the HTML `href` attribute value from the controller action method and route id. You use **View Source** from your favorite browser or use the developer tools to examine the generated markup. A portion of the generated HTML is shown below:

```
 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>
```

Recall the format for [routing](../../mvc/controllers/routing?view=aspnetcore-10.0) set in the `Program.cs` file:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

ASP.NET Core translates `https://localhost:5001/Movies/Edit/4` into a request to the `Edit` action method of the `Movies` controller with the parameter `Id` of 4. (Controller methods are also known as action methods.)

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) are one of the most popular new features in ASP.NET Core. For more information, see [Related content](#related-content).

[]()

Open the `Movies` controller and examine the two `Edit` action methods. The following code shows the `HTTP GET Edit` method, which fetches the movie and populates the edit form generated by the `Edit.cshtml` Razor file.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

The following code shows the `HTTP POST Edit` method, which processes the posted movie values:

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[Bind]` attribute is one way to protect against [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application#overpost). You should only include properties in the `[Bind]` attribute that you want to change. For more information, see [Protect your controller from over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application). [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/) provide an alternative approach to prevent over-posting.

Notice the second `Edit` action method is preceded by the `[HttpPost]` attribute.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `HttpPost` attribute specifies that this `Edit` method can be invoked *only* for `POST` requests. You could apply the `[HttpGet]` attribute to the first edit method, but that's not necessary because `[HttpGet]` is the default.

The `ValidateAntiForgeryToken` attribute is used to [prevent forgery of a request](../../security/anti-request-forgery?view=aspnetcore-10.0) and is paired up with an antiforgery token generated in the edit view file (`Views/Movies/Edit.cshtml`). The edit view file generates the antiforgery token with the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

```
<form asp-action="Edit">
```

The [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates a hidden antiforgery token that must match the `[ValidateAntiForgeryToken]` generated antiforgery token in the `Edit` method of the Movies controller. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0).

The `HttpGet Edit` method takes the movie `ID` parameter, looks up the movie using the Entity Framework `FindAsync` method, and returns the selected movie to the Edit view. If a movie cannot be found, `NotFound` (HTTP 404) is returned.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

When the scaffolding system created the Edit view, it examined the `Movie` class and created code to render `<label>` and `<input>` elements for each property of the class. The following example shows the Edit view that was generated by the Visual Studio scaffolding system:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Notice how the view template has a `@model MvcMovie.Models.Movie` statement at the top of the file. `@model MvcMovie.Models.Movie` specifies that the view expects the model for the view template to be of type `Movie`.

The scaffolded code uses several Tag Helper methods to streamline the HTML markup. The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays the name of the field ("Title", "ReleaseDate", "Genre", or "Price"). The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) renders an HTML `<input>` element. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays any validation messages associated with that property.

Run the application and navigate to the `/Movies` URL. Click an **Edit** link. In the browser, view the source for the page. The generated HTML for the `<form>` element is shown below.

```
<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>
```

The `<input>` elements are in an `HTML <form>` element whose `action` attribute is set to post to the `/Movies/Edit/id` URL. The form data will be posted to the server when the `Save` button is clicked. The last line before the closing `</form>` element shows the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

## Processing the POST Request

The following listing shows the `[HttpPost]` version of the `Edit` action method.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[ValidateAntiForgeryToken]` attribute validates the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the antiforgery token generator in the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

The [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system takes the posted form values and creates a `Movie` object that's passed as the `movie` parameter. The `ModelState.IsValid` property verifies that the data submitted in the form can be used to modify (edit or update) a `Movie` object. If the data is valid, it's saved. The updated (edited) movie data is saved to the database by calling the `SaveChangesAsync` method of database context. After saving the data, the code redirects the user to the `Index` action method of the `MoviesController` class, which displays the movie collection, including the changes just made.

Before the form is posted to the server, client-side validation checks any validation rules on the fields. If there are any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't have client-side validation but the server will detect the posted values that are not valid, and the form values will be redisplayed with error messages. Later in the tutorial we examine [Model Validation](../../mvc/models/validation?view=aspnetcore-10.0) in more detail. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) in the `Views/Movies/Edit.cshtml` view template takes care of displaying appropriate error messages.

All the `HttpGet` methods in the movie controller follow a similar pattern. They get a movie object (or list of objects, in the case of `Index`), and pass the object (model) to the view. The `Create` method passes an empty movie object to the `Create` view. All the methods that create, edit, delete, or otherwise modify data do so in the `[HttpPost]` overload of the method. Modifying data in an `HTTP GET` method is a security risk. Modifying data in an `HTTP GET` method also violates HTTP best practices and the architectural [REST](http://rest.elkstein.org/) pattern, which specifies that GET requests shouldn't change the state of your application. In other words, performing a GET operation should be a safe operation that has no side effects and doesn't modify your persisted data.

## Related content

* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)
* [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0)
* Protect your controller from [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application)
* [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/)
* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Select Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

[Previous](working-with-sql?view=aspnetcore-10.0) [Next](search?view=aspnetcore-10.0)

We have a good start to the movie app, but the presentation isn't ideal, for example, **ReleaseDate** should be two words.

Open the `Models/Movie.cs` file and add the highlighted lines shown below:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    
    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

`DataAnnotations` are explained in the next tutorial. The [Display](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies what to display for the name of a field (in this case "Release Date" instead of "ReleaseDate"). The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (Date), so the time information stored in the field isn't displayed.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

Browse to the `Movies` controller and hold the mouse pointer over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the Core MVC Anchor Tag Helper in the `Views/Movies/Index.cshtml` file.

```
        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files. In the code above, the `AnchorTagHelper` dynamically generates the HTML `href` attribute value from the controller action method and route id. You use **View Source** from your favorite browser or use the developer tools to examine the generated markup. A portion of the generated HTML is shown below:

```
 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>
```

Recall the format for [routing](../../mvc/controllers/routing?view=aspnetcore-10.0) set in the `Program.cs` file:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

ASP.NET Core translates `https://localhost:5001/Movies/Edit/4` into a request to the `Edit` action method of the `Movies` controller with the parameter `Id` of 4. (Controller methods are also known as action methods.)

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) are one of the most popular new features in ASP.NET Core. For more information, see [Related content](#related-content).

[]()

Open the `Movies` controller and examine the two `Edit` action methods. The following code shows the `HTTP GET Edit` method, which fetches the movie and populates the edit form generated by the `Edit.cshtml` Razor file.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

The following code shows the `HTTP POST Edit` method, which processes the posted movie values:

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[Bind]` attribute is one way to protect against [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application#overpost). You should only include properties in the `[Bind]` attribute that you want to change. For more information, see [Protect your controller from over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application). [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/) provide an alternative approach to prevent over-posting.

Notice the second `Edit` action method is preceded by the `[HttpPost]` attribute.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `HttpPost` attribute specifies that this `Edit` method can be invoked *only* for `POST` requests. You could apply the `[HttpGet]` attribute to the first edit method, but that's not necessary because `[HttpGet]` is the default.

The `ValidateAntiForgeryToken` attribute is used to [prevent forgery of a request](../../security/anti-request-forgery?view=aspnetcore-10.0) and is paired up with an antiforgery token generated in the edit view file (`Views/Movies/Edit.cshtml`). The edit view file generates the antiforgery token with the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

```
<form asp-action="Edit">
```

The [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates a hidden antiforgery token that must match the `[ValidateAntiForgeryToken]` generated antiforgery token in the `Edit` method of the Movies controller. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0).

The `HttpGet Edit` method takes the movie `ID` parameter, looks up the movie using the Entity Framework `FindAsync` method, and returns the selected movie to the Edit view. If a movie cannot be found, `NotFound` (HTTP 404) is returned.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

When the scaffolding system created the Edit view, it examined the `Movie` class and created code to render `<label>` and `<input>` elements for each property of the class. The following example shows the Edit view that was generated by the Visual Studio scaffolding system:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Notice how the view template has a `@model MvcMovie.Models.Movie` statement at the top of the file. `@model MvcMovie.Models.Movie` specifies that the view expects the model for the view template to be of type `Movie`.

The scaffolded code uses several Tag Helper methods to streamline the HTML markup. The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays the name of the field ("Title", "ReleaseDate", "Genre", or "Price"). The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) renders an HTML `<input>` element. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays any validation messages associated with that property.

Run the application and navigate to the `/Movies` URL. Click an **Edit** link. In the browser, view the source for the page. The generated HTML for the `<form>` element is shown below.

```
<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>
```

The `<input>` elements are in an `HTML <form>` element whose `action` attribute is set to post to the `/Movies/Edit/id` URL. The form data will be posted to the server when the `Save` button is clicked. The last line before the closing `</form>` element shows the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

## Processing the POST Request

The following listing shows the `[HttpPost]` version of the `Edit` action method.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[ValidateAntiForgeryToken]` attribute validates the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the antiforgery token generator in the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

The [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system takes the posted form values and creates a `Movie` object that's passed as the `movie` parameter. The `ModelState.IsValid` property verifies that the data submitted in the form can be used to modify (edit or update) a `Movie` object. If the data is valid, it's saved. The updated (edited) movie data is saved to the database by calling the `SaveChangesAsync` method of database context. After saving the data, the code redirects the user to the `Index` action method of the `MoviesController` class, which displays the movie collection, including the changes just made.

Before the form is posted to the server, client-side validation checks any validation rules on the fields. If there are any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't have client-side validation but the server will detect the posted values that are not valid, and the form values will be redisplayed with error messages. Later in the tutorial we examine [Model Validation](../../mvc/models/validation?view=aspnetcore-10.0) in more detail. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) in the `Views/Movies/Edit.cshtml` view template takes care of displaying appropriate error messages.

All the `HttpGet` methods in the movie controller follow a similar pattern. They get a movie object (or list of objects, in the case of `Index`), and pass the object (model) to the view. The `Create` method passes an empty movie object to the `Create` view. All the methods that create, edit, delete, or otherwise modify data do so in the `[HttpPost]` overload of the method. Modifying data in an `HTTP GET` method is a security risk. Modifying data in an `HTTP GET` method also violates HTTP best practices and the architectural [REST](http://rest.elkstein.org/) pattern, which specifies that GET requests shouldn't change the state of your application. In other words, performing a GET operation should be a safe operation that has no side effects and doesn't modify your persisted data.

## Related content

* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)
* [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0)
* Protect your controller from [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application)
* [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/)
* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Select Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

[Previous](working-with-sql?view=aspnetcore-10.0) [Next](search?view=aspnetcore-10.0)

We have a good start to the movie app, but the presentation isn't ideal, for example, **ReleaseDate** should be two words.

Open the `Models/Movie.cs` file and add the highlighted lines shown below:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    
    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

`DataAnnotations` are explained in the next tutorial. The [Display](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies what to display for the name of a field (in this case "Release Date" instead of "ReleaseDate"). The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (Date), so the time information stored in the field isn't displayed.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

Browse to the `Movies` controller and hold the mouse pointer over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the Core MVC Anchor Tag Helper in the `Views/Movies/Index.cshtml` file.

```
        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files. In the code above, the `AnchorTagHelper` dynamically generates the HTML `href` attribute value from the controller action method and route id. You use **View Source** from your favorite browser or use the developer tools to examine the generated markup. A portion of the generated HTML is shown below:

```
 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>
```

Recall the format for [routing](../../mvc/controllers/routing?view=aspnetcore-10.0) set in the `Program.cs` file:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

ASP.NET Core translates `https://localhost:5001/Movies/Edit/4` into a request to the `Edit` action method of the `Movies` controller with the parameter `Id` of 4. (Controller methods are also known as action methods.)

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) are one of the most popular new features in ASP.NET Core. For more information, see [Related content](#related-content).

[]()

Open the `Movies` controller and examine the two `Edit` action methods. The following code shows the `HTTP GET Edit` method, which fetches the movie and populates the edit form generated by the `Edit.cshtml` Razor file.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

The following code shows the `HTTP POST Edit` method, which processes the posted movie values:

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[Bind]` attribute is one way to protect against [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application#overpost). You should only include properties in the `[Bind]` attribute that you want to change. For more information, see [Protect your controller from over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application). [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/) provide an alternative approach to prevent over-posting.

Notice the second `Edit` action method is preceded by the `[HttpPost]` attribute.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `HttpPost` attribute specifies that this `Edit` method can be invoked *only* for `POST` requests. You could apply the `[HttpGet]` attribute to the first edit method, but that's not necessary because `[HttpGet]` is the default.

The `ValidateAntiForgeryToken` attribute is used to [prevent forgery of a request](../../security/anti-request-forgery?view=aspnetcore-10.0) and is paired up with an antiforgery token generated in the edit view file (`Views/Movies/Edit.cshtml`). The edit view file generates the antiforgery token with the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

```
<form asp-action="Edit">
```

The [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates a hidden antiforgery token that must match the `[ValidateAntiForgeryToken]` generated antiforgery token in the `Edit` method of the Movies controller. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0).

The `HttpGet Edit` method takes the movie `ID` parameter, looks up the movie using the Entity Framework `FindAsync` method, and returns the selected movie to the Edit view. If a movie cannot be found, `NotFound` (HTTP 404) is returned.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

When the scaffolding system created the Edit view, it examined the `Movie` class and created code to render `<label>` and `<input>` elements for each property of the class. The following example shows the Edit view that was generated by the Visual Studio scaffolding system:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Notice how the view template has a `@model MvcMovie.Models.Movie` statement at the top of the file. `@model MvcMovie.Models.Movie` specifies that the view expects the model for the view template to be of type `Movie`.

The scaffolded code uses several Tag Helper methods to streamline the HTML markup. The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays the name of the field ("Title", "ReleaseDate", "Genre", or "Price"). The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) renders an HTML `<input>` element. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays any validation messages associated with that property.

Run the application and navigate to the `/Movies` URL. Click an **Edit** link. In the browser, view the source for the page. The generated HTML for the `<form>` element is shown below.

```
<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>
```

The `<input>` elements are in an `HTML <form>` element whose `action` attribute is set to post to the `/Movies/Edit/id` URL. The form data will be posted to the server when the `Save` button is clicked. The last line before the closing `</form>` element shows the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

## Processing the POST Request

The following listing shows the `[HttpPost]` version of the `Edit` action method.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[ValidateAntiForgeryToken]` attribute validates the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the antiforgery token generator in the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

The [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system takes the posted form values and creates a `Movie` object that's passed as the `movie` parameter. The `ModelState.IsValid` property verifies that the data submitted in the form can be used to modify (edit or update) a `Movie` object. If the data is valid, it's saved. The updated (edited) movie data is saved to the database by calling the `SaveChangesAsync` method of database context. After saving the data, the code redirects the user to the `Index` action method of the `MoviesController` class, which displays the movie collection, including the changes just made.

Before the form is posted to the server, client-side validation checks any validation rules on the fields. If there are any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't have client-side validation but the server will detect the posted values that are not valid, and the form values will be redisplayed with error messages. Later in the tutorial we examine [Model Validation](../../mvc/models/validation?view=aspnetcore-10.0) in more detail. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) in the `Views/Movies/Edit.cshtml` view template takes care of displaying appropriate error messages.

All the `HttpGet` methods in the movie controller follow a similar pattern. They get a movie object (or list of objects, in the case of `Index`), and pass the object (model) to the view. The `Create` method passes an empty movie object to the `Create` view. All the methods that create, edit, delete, or otherwise modify data do so in the `[HttpPost]` overload of the method. Modifying data in an `HTTP GET` method is a security risk. Modifying data in an `HTTP GET` method also violates HTTP best practices and the architectural [REST](http://rest.elkstein.org/) pattern, which specifies that GET requests shouldn't change the state of your application. In other words, performing a GET operation should be a safe operation that has no side effects and doesn't modify your persisted data.

## Related content

* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)
* [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0)
* Protect your controller from [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application)
* [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/)
* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Select Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

[Previous](working-with-sql?view=aspnetcore-10.0) [Next](search?view=aspnetcore-10.0)

We have a good start to the movie app, but the presentation isn't ideal, for example, **ReleaseDate** should be two words.

Open the `Models/Movie.cs` file and add the highlighted lines shown below:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string? Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string? Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}
```

`DataAnnotations` are explained in the next tutorial. The [Display](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies what to display for the name of a field (in this case "Release Date" instead of "ReleaseDate"). The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (Date), so the time information stored in the field isn't displayed.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

Browse to the `Movies` controller and hold the mouse pointer over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the Core MVC Anchor Tag Helper in the `Views/Movies/Index.cshtml` file.

```
        <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
    </td>
</tr>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files. In the code above, the `AnchorTagHelper` dynamically generates the HTML `href` attribute value from the controller action method and route id. You use **View Source** from your favorite browser or use the developer tools to examine the generated markup. A portion of the generated HTML is shown below:

```
 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>
```

Recall the format for [routing](../../mvc/controllers/routing?view=aspnetcore-10.0) set in the `Program.cs` file:

```
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
```

ASP.NET Core translates `https://localhost:5001/Movies/Edit/4` into a request to the `Edit` action method of the `Movies` controller with the parameter `Id` of 4. (Controller methods are also known as action methods.)

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) are a popular feature in ASP.NET Core. For more information about them, see [Related content](#related-content).

[]()

Open the `Movies` controller and examine the two `Edit` action methods. The following code shows the `HTTP GET Edit` method, which fetches the movie and populates the edit form generated by the `Edit.cshtml` Razor file.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

The following code shows the `HTTP POST Edit` method, which processes the posted movie values:

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[Bind]` attribute is one way to protect against [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application#overpost). You should only include properties in the `[Bind]` attribute that you want to change. For more information, see [Protect your controller from over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application). [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/) provide an alternative approach to prevent over-posting.

Notice the second `Edit` action method is preceded by the `[HttpPost]` attribute.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `HttpPost` attribute specifies that this `Edit` method can be invoked *only* for `POST` requests. You could apply the `[HttpGet]` attribute to the first edit method, but that's not necessary because `[HttpGet]` is the default.

The `ValidateAntiForgeryToken` attribute is used to [prevent forgery of a request](../../security/anti-request-forgery?view=aspnetcore-10.0) and is paired up with an antiforgery token generated in the edit view file (`Views/Movies/Edit.cshtml`). The edit view file generates the antiforgery token with the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

```
<form asp-action="Edit">
```

The [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates a hidden antiforgery token that must match the `[ValidateAntiForgeryToken]` generated antiforgery token in the `Edit` method of the Movies controller. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0).

The `HttpGet Edit` method takes the movie `ID` parameter, looks up the movie using the Entity Framework `FindAsync` method, and returns the selected movie to the Edit view. If a movie cannot be found, `NotFound` (HTTP 404) is returned.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

When the scaffolding system created the Edit view, it examined the `Movie` class and created code to render `<label>` and `<input>` elements for each property of the class. The following example shows the Edit view that was generated by the Visual Studio scaffolding system:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Notice how the view template has a `@model MvcMovie.Models.Movie` statement at the top of the file. `@model MvcMovie.Models.Movie` specifies that the view expects the model for the view template to be of type `Movie`.

The scaffolded code uses several Tag Helper methods to streamline the HTML markup. The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays the name of the field ("Title", "ReleaseDate", "Genre", or "Price"). The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) renders an HTML `<input>` element. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays any validation messages associated with that property.

Run the application and navigate to the `/Movies` URL. Click an **Edit** link. In the browser, view the source for the page. The generated HTML for the `<form>` element is shown below.

```
<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>
```

The `<input>` elements are in an `HTML <form>` element whose `action` attribute is set to post to the `/Movies/Edit/id` URL. The form data will be posted to the server when the `Save` button is clicked. The last line before the closing `</form>` element shows the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

## Processing the POST Request

The following listing shows the `[HttpPost]` version of the `Edit` action method.

```
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[ValidateAntiForgeryToken]` attribute validates the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the antiforgery token generator in the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

The [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system takes the posted form values and creates a `Movie` object that's passed as the `movie` parameter. The `ModelState.IsValid` property verifies that the data submitted in the form can be used to modify (edit or update) a `Movie` object. If the data is valid, it's saved. The updated (edited) movie data is saved to the database by calling the `SaveChangesAsync` method of database context. After saving the data, the code redirects the user to the `Index` action method of the `MoviesController` class, which displays the movie collection, including the changes just made.

Before the form is posted to the server, client-side validation checks any validation rules on the fields. If there are any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't have client-side validation but the server will detect the posted values that are not valid, and the form values will be redisplayed with error messages. Later in the tutorial we examine [Model Validation](../../mvc/models/validation?view=aspnetcore-10.0) in more detail. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) in the `Views/Movies/Edit.cshtml` view template takes care of displaying appropriate error messages.

All the `HttpGet` methods in the movie controller follow a similar pattern. They get a movie object (or list of objects, in the case of `Index`), and pass the object (model) to the view. The `Create` method passes an empty movie object to the `Create` view. All the methods that create, edit, delete, or otherwise modify data do so in the `[HttpPost]` overload of the method. Modifying data in an `HTTP GET` method is a security risk. Modifying data in an `HTTP GET` method also violates HTTP best practices and the architectural [REST](http://rest.elkstein.org/) pattern, which specifies that GET requests shouldn't change the state of your application. In other words, performing a GET operation should be a safe operation that has no side effects and doesn't modify your persisted data.

## Related content

* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)
* [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0)
* Protect your controller from [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application)
* [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/)
* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Select Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

[Previous](working-with-sql?view=aspnetcore-10.0) [Next](search?view=aspnetcore-10.0)

We have a good start to the movie app, but the presentation isn't ideal, for example, **ReleaseDate** should be two words.

Open the `Models/Movie.cs` file and add the highlighted lines shown below:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}
```

We cover [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) in the next tutorial. The [Display](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies what to display for the name of a field (in this case "Release Date" instead of "ReleaseDate"). The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (Date), so the time information stored in the field isn't displayed.

The `[Column(TypeName = "decimal(18, 2)")]` data annotation is required so Entity Framework Core can correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).

Browse to the `Movies` controller and hold the mouse pointer over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the Core MVC Anchor Tag Helper in the `Views/Movies/Index.cshtml` file.

```
        <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
        <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
        <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
    </td>
</tr>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files. In the code above, the `AnchorTagHelper` dynamically generates the HTML `href` attribute value from the controller action method and route id. You use **View Source** from your favorite browser or use the developer tools to examine the generated markup. A portion of the generated HTML is shown below:

```
 <td>
    <a href="/Movies/Edit/4"> Edit </a> |
    <a href="/Movies/Details/4"> Details </a> |
    <a href="/Movies/Delete/4"> Delete </a>
</td>
```

Recall the format for [routing](../../mvc/controllers/routing?view=aspnetcore-10.0) set in the `Startup.cs` file:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});
```

ASP.NET Core translates `https://localhost:5001/Movies/Edit/4` into a request to the `Edit` action method of the `Movies` controller with the parameter `Id` of 4. (Controller methods are also known as action methods.)

For more information about [Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0), see [Related content](#related-content).

[]()

Open the `Movies` controller and examine the two `Edit` action methods. The following code shows the `HTTP GET Edit` method, which fetches the movie and populates the edit form generated by the `Edit.cshtml` Razor file.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

The following code shows the `HTTP POST Edit` method, which processes the posted movie values:

```
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for 
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction("Index");
    }
    return View(movie);
}
```

The `[Bind]` attribute is one way to protect against [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application#overpost). You should only include properties in the `[Bind]` attribute that you want to change. For more information, see [Protect your controller from over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application). [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/) provide an alternative approach to prevent over-posting.

Notice the second `Edit` action method is preceded by the `[HttpPost]` attribute.

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `HttpPost` attribute specifies that this `Edit` method can be invoked *only* for `POST` requests. You could apply the `[HttpGet]` attribute to the first edit method, but that's not necessary because `[HttpGet]` is the default.

The `ValidateAntiForgeryToken` attribute is used to [prevent forgery of a request](../../security/anti-request-forgery?view=aspnetcore-10.0) and is paired up with an antiforgery token generated in the edit view file (`Views/Movies/Edit.cshtml`). The edit view file generates the antiforgery token with the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

```
<form asp-action="Edit">
```

The [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) generates a hidden antiforgery token that must match the `[ValidateAntiForgeryToken]` generated antiforgery token in the `Edit` method of the Movies controller. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0).

The `HttpGet Edit` method takes the movie `ID` parameter, looks up the movie using the Entity Framework `FindAsync` method, and returns the selected movie to the Edit view. If a movie cannot be found, `NotFound` (HTTP 404) is returned.

```
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
```

When the scaffolding system created the Edit view, it examined the `Movie` class and created code to render `<label>` and `<input>` elements for each property of the class. The following example shows the Edit view that was generated by the Visual Studio scaffolding system:

```
@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

Notice how the view template has a `@model MvcMovie.Models.Movie` statement at the top of the file. `@model MvcMovie.Models.Movie` specifies that the view expects the model for the view template to be of type `Movie`.

The scaffolded code uses several Tag Helper methods to streamline the HTML markup. The [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays the name of the field ("Title", "ReleaseDate", "Genre", or "Price"). The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) renders an HTML `<input>` element. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) displays any validation messages associated with that property.

Run the application and navigate to the `/Movies` URL. Click an **Edit** link. In the browser, view the source for the page. The generated HTML for the `<form>` element is shown below.

```
<form action="/Movies/Edit/7" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div class="text-danger" />
        <input type="hidden" data-val="true" data-val-required="The ID field is required." id="ID" name="ID" value="7" />
        <div class="form-group">
            <label class="control-label col-md-2" for="Genre" />
            <div class="col-md-10">
                <input class="form-control" type="text" id="Genre" name="Genre" value="Western" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-2" for="Price" />
            <div class="col-md-10">
                <input class="form-control" type="text" data-val="true" data-val-number="The field Price must be a number." data-val-required="The Price field is required." id="Price" name="Price" value="3.99" />
                <span class="text-danger field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
            </div>
        </div>
        <!-- Markup removed for brevity -->
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmqUyXnJBXhmrjcUVDJyDUMm7-MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>
```

The `<input>` elements are in an `HTML <form>` element whose `action` attribute is set to post to the `/Movies/Edit/id` URL. The form data will be posted to the server when the `Save` button is clicked. The last line before the closing `</form>` element shows the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0).

## Processing the POST Request

The following listing shows the `[HttpPost]` version of the `Edit` action method.

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
    if (id != movie.ID)
    {
        return NotFound();
    }

    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The `[ValidateAntiForgeryToken]` attribute validates the hidden [XSRF](../../security/anti-request-forgery?view=aspnetcore-10.0) token generated by the antiforgery token generator in the [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

The [model binding](../../mvc/models/model-binding?view=aspnetcore-10.0) system takes the posted form values and creates a `Movie` object that's passed as the `movie` parameter. The `ModelState.IsValid` property verifies that the data submitted in the form can be used to modify (edit or update) a `Movie` object. If the data is valid, it's saved. The updated (edited) movie data is saved to the database by calling the `SaveChangesAsync` method of database context. After saving the data, the code redirects the user to the `Index` action method of the `MoviesController` class, which displays the movie collection, including the changes just made.

Before the form is posted to the server, client-side validation checks any validation rules on the fields. If there are any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't have client-side validation but the server will detect the posted values that are not valid, and the form values will be redisplayed with error messages. Later in the tutorial we examine [Model Validation](../../mvc/models/validation?view=aspnetcore-10.0) in more detail. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) in the `Views/Movies/Edit.cshtml` view template takes care of displaying appropriate error messages.

All the `HttpGet` methods in the movie controller follow a similar pattern. They get a movie object (or list of objects, in the case of `Index`), and pass the object (model) to the view. The `Create` method passes an empty movie object to the `Create` view. All the methods that create, edit, delete, or otherwise modify data do so in the `[HttpPost]` overload of the method. Modifying data in an `HTTP GET` method is a security risk. Modifying data in an `HTTP GET` method also violates HTTP best practices and the architectural [REST](http://rest.elkstein.org/) pattern, which specifies that GET requests shouldn't change the state of your application. In other words, performing a GET operation should be a safe operation that has no side effects and doesn't modify your persisted data.

## Related content

* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)
* [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0)
* Protect your controller from [over-posting](/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application)
* [ViewModels](https://rachelappel.com/use-viewmodels-to-manage-data-amp-organize-code-in-asp-net-mvc-applications/)
* [Form Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Label Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Select Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0)

[Previous](working-with-sql?view=aspnetcore-10.0) [Next](search?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-27

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/diagnostics
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/diagnostics.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Logging and diagnostics in gRPC on .NET

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

This article provides guidance for gathering diagnostics from a gRPC app to help troubleshoot issues. Topics covered include:

* **Logging** - Structured logs written to [.NET logging](../fundamentals/logging/?view=aspnetcore-10.0). [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) is used by app frameworks to write logs, and by users for their own logging in an app.
* **Tracing** - Events related to an operation written using `DiaganosticSource` and `Activity`. Traces from diagnostic source are commonly used to collect app telemetry by libraries such as [Application Insights](/en-us/azure/azure-monitor/app/asp-net-core) and [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-dotnet).
* **Metrics** - Representation of data measures over intervals of time, for example, requests per second. Metrics are emitted using `EventCounter` and can be observed using [dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters) command-line tool or with [Application Insights](/en-us/azure/azure-monitor/app/eventcounters).

## Logging

gRPC services and the gRPC client write logs using [.NET logging](../fundamentals/logging/?view=aspnetcore-10.0). Logs are a good place to start when debugging unexpected behavior in service and client apps.

### gRPC services logging

Warning

Server-side logs may contain sensitive information from your app. **Never** post raw logs from production apps to public forums like GitHub.

Since gRPC services are hosted on ASP.NET Core, it uses the ASP.NET Core logging system. In the default configuration, gRPC logs minimal information, but logging can be configured. See the documentation on [ASP.NET Core logging](../fundamentals/logging/?view=aspnetcore-10.0) for details on configuring ASP.NET Core logging.

gRPC adds logs under the `Grpc` category. To enable detailed logs from gRPC, configure the `Grpc` prefixes to the `Debug` level in the `appsettings.json` file by adding the following items to the `LogLevel` subsection in `Logging`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information",
      "Grpc": "Debug"
    }
  }
}
```

Logging can also be configured in `Program.cs` with `ConfigureLogging`:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.AddFilter("Grpc", LogLevel.Debug);
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

When not using JSON-based configuration, set the following configuration value in the configuration system:

* `Logging:LogLevel:Grpc` = `Debug`

Check the documentation for your configuration system to determine how to specify nested configuration values. For example, when using environment variables, two `_` characters are used instead of the `:` (for example, `Logging__LogLevel__Grpc`).

We recommend using the `Debug` level when gathering detailed diagnostics for an app. The `Trace` level produces low-level diagnostics and is rarely needed to diagnose issues.

#### Sample logging output

Here is an example of console output at the `Debug` level of a gRPC service:

```
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:5001/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
dbug: Grpc.AspNetCore.Server.ServerCallHandler[1]
      Reading message.
info: GrpcService.GreeterService[0]
      Hello World
dbug: Grpc.AspNetCore.Server.ServerCallHandler[6]
      Sending message.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 1.4113ms 200 application/grpc
```

### Access server-side logs

How server-side logs are accessed depends on the app's environment.

#### As a console app

If you're running in a console app, the [Console logger](../fundamentals/logging/?view=aspnetcore-10.0#console) should be enabled by default. gRPC logs will appear in the console.

#### Other environments

If the app is deployed to another environment (for example, Docker, Kubernetes, or Windows Service), see [Logging in .NET and ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) for more information on how to configure logging providers suitable for the environment.

### gRPC client logging

Warning

Client-side logs may contain sensitive information from your app. **Never** post raw logs from production apps to public forums like GitHub.

To get logs from the .NET client, set the `GrpcChannelOptions.LoggerFactory` property when the client's channel is created. When calling a gRPC service from an ASP.NET Core app, the logger factory can be resolved from dependency injection (DI):

```
[ApiController]
[Route("[controller]")]
public class GreetingController : ControllerBase
{
    private ILoggerFactory _loggerFactory;

    public GreetingController(ILoggerFactory loggerFactory)
    {
        _loggerFactory = loggerFactory;
    }

    [HttpGet]
    public async Task<ActionResult<string>> Get(string name)
    {
        var channel = GrpcChannel.ForAddress("https://localhost:5001",
            new GrpcChannelOptions { LoggerFactory = _loggerFactory });
        var client = new Greeter.GreeterClient(channel);

        var reply = await client.SayHelloAsync(new HelloRequest { Name = name });
        return Ok(reply.Message);
    }
}
```

An alternative way to enable client logging is to use the [gRPC client factory](clientfactory?view=aspnetcore-10.0) to create the client. A gRPC client registered with the client factory and resolved from DI will automatically use the app's configured logging.

If the app isn't using DI, then create a new `ILoggerFactory` instance with [LoggerFactory.Create](/en-us/dotnet/api/microsoft.extensions.logging.loggerfactory.create). To access this method, add the [Microsoft.Extensions.Logging](https://www.nuget.org/packages/microsoft.extensions.logging/) package to your app.

```
var loggerFactory = LoggerFactory.Create(logging =>
{
    logging.AddConsole();
    logging.SetMinimumLevel(LogLevel.Debug);
});

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { LoggerFactory = loggerFactory });

var client = Greeter.GreeterClient(channel);
```

#### gRPC client log scopes

The gRPC client adds a [logging scope](../fundamentals/logging/?view=aspnetcore-10.0#log-scopes) to logs made during a gRPC call. The scope has metadata related to the gRPC call:

* **GrpcMethodType** - The gRPC method type. Possible values are names from `Grpc.Core.MethodType` enum. For example, `Unary`.
* **GrpcUri** - The relative URI of the gRPC method. For example, */greet.Greeter/SayHellos*.

#### Sample logging output

Here is an example of console output at the `Debug` level of a gRPC client:

```
dbug: Grpc.Net.Client.Internal.GrpcCall[1]
      Starting gRPC call. Method type: 'Unary', URI: 'https://localhost:5001/Greet.Greeter/SayHello'.
dbug: Grpc.Net.Client.Internal.GrpcCall[6]
      Sending message.
dbug: Grpc.Net.Client.Internal.GrpcCall[1]
      Reading message.
dbug: Grpc.Net.Client.Internal.GrpcCall[4]
      Finished gRPC call.
```

## Tracing

gRPC services and the gRPC client provide information about gRPC calls using [DiagnosticSource](/en-us/dotnet/api/system.diagnostics.diagnosticsource) and [Activity](/en-us/dotnet/api/system.diagnostics.activity).

* .NET gRPC uses an activity to represent a gRPC call.
* Tracing events are written to the diagnostic source at the start and stop of the gRPC call activity.
* Tracing doesn't capture information about when messages are sent over the lifetime of gRPC streaming calls.

### gRPC service tracing

gRPC services are hosted on ASP.NET Core, which reports events about incoming HTTP requests. gRPC specific metadata is added to the existing HTTP request diagnostics that ASP.NET Core provides.

* Diagnostic source name is `Microsoft.AspNetCore`.

* Activity name is `Microsoft.AspNetCore.Hosting.HttpRequestIn`.

  * Name of the gRPC method invoked by the gRPC call is added as a tag with the name `grpc.method`.
  * Status code of the gRPC call when it is complete is added as a tag with the name `grpc.status_code`.

### gRPC client tracing

The .NET gRPC client uses `HttpClient` to make gRPC calls. Although `HttpClient` writes diagnostic events, the .NET gRPC client provides a custom diagnostic source, activity, and events so that complete information about a gRPC call can be collected.

* Diagnostic source name is `Grpc.Net.Client`.

* Activity name is `Grpc.Net.Client.GrpcOut`.

  * Name of the gRPC method invoked by the gRPC call is added as a tag with the name `grpc.method`.
  * Status code of the gRPC call when it is complete is added as a tag with the name `grpc.status_code`.

### Collecting tracing

The easiest way to use `DiagnosticSource` is to configure a telemetry library such as [Application Insights](/en-us/azure/azure-monitor/app/asp-net-core) or [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-dotnet) in your app. The library will process information about gRPC calls along-side other app telemetry.

Tracing can be viewed in a managed service like Application Insights, or run as your own distributed tracing system. OpenTelemetry supports exporting tracing data to [Jaeger](https://www.jaegertracing.io/) and [Zipkin](https://zipkin.io/).

`DiagnosticSource` can consume tracing events in code using `DiagnosticListener`. For information about listening to a diagnostic source with code, see the [DiagnosticSource user's guide](https://github.com/dotnet/corefx/blob/d3942d4671919edb0cca6ddc1840190f524a809d/src/System.Diagnostics.DiagnosticSource/src/DiagnosticSourceUsersGuide.md#consuming-data-with-diagnosticlistener).

Note

Telemetry libraries do not capture gRPC specific `Grpc.Net.Client.GrpcOut` telemetry currently. Work to improve telemetry libraries capturing this tracing is ongoing.

## Metrics

Metrics is a representation of data measures over intervals of time, for example, requests per second. Metrics data allows observation of the state of an app at a high level. .NET gRPC metrics are emitted using `EventCounter`.

### gRPC service metrics

gRPC server metrics are reported on `Grpc.AspNetCore.Server` event source.

| Name                      | Description                   |
| ------------------------- | ----------------------------- |
| `total-calls`             | Total Calls                   |
| `current-calls`           | Current Calls                 |
| `calls-failed`            | Total Calls Failed            |
| `calls-deadline-exceeded` | Total Calls Deadline Exceeded |
| `messages-sent`           | Total Messages Sent           |
| `messages-received`       | Total Messages Received       |
| `calls-unimplemented`     | Total Calls Unimplemented     |

ASP.NET Core also provides its own metrics on `Microsoft.AspNetCore.Hosting` event source.

### gRPC client metrics

gRPC client metrics are reported on `Grpc.Net.Client` event source.

| Name                      | Description                   |
| ------------------------- | ----------------------------- |
| `total-calls`             | Total Calls                   |
| `current-calls`           | Current Calls                 |
| `calls-failed`            | Total Calls Failed            |
| `calls-deadline-exceeded` | Total Calls Deadline Exceeded |
| `messages-sent`           | Total Messages Sent           |
| `messages-received`       | Total Messages Received       |

### Observe metrics

[dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters) is a performance monitoring tool for ad-hoc health monitoring and first-level performance investigation. Monitor a .NET app with either `Grpc.AspNetCore.Server` or `Grpc.Net.Client` as the provider name.

```
> dotnet-counters monitor --process-id 1902 --counters Grpc.AspNetCore.Server

Press p to pause, r to resume, q to quit.
    Status: Running
[Grpc.AspNetCore.Server]
    Total Calls                                 300
    Current Calls                               5
    Total Calls Failed                          0
    Total Calls Deadline Exceeded               0
    Total Messages Sent                         295
    Total Messages Received                     300
    Total Calls Unimplemented                   0
```

Another way to observe gRPC metrics is to capture counter data using Application Insights's [Microsoft.ApplicationInsights.EventCounterCollector package](/en-us/azure/azure-monitor/app/eventcounters). Once setup, Application Insights collects common .NET counters at runtime. gRPC's counters are not collected by default, but App Insights can be [customized to include additional counters](/en-us/azure/azure-monitor/app/eventcounters#customizing-counters-to-be-collected).

Specify the gRPC counters for Application Insight to collect in `Startup.cs`:

```
    using Microsoft.ApplicationInsights.Extensibility.EventCounterCollector;

    public void ConfigureServices(IServiceCollection services)
    {
        //... other code...

        services.ConfigureTelemetryModule<EventCounterCollectionModule>(
            (module, o) =>
            {
                // Configure App Insights to collect gRPC counters gRPC services hosted in an ASP.NET Core app
                module.Counters.Add(new EventCounterCollectionRequest("Grpc.AspNetCore.Server", "current-calls"));
                module.Counters.Add(new EventCounterCollectionRequest("Grpc.AspNetCore.Server", "total-calls"));
                module.Counters.Add(new EventCounterCollectionRequest("Grpc.AspNetCore.Server", "calls-failed"));
            }
        );
    }
```

## Additional resources

* [Logging in .NET and ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0)
* [gRPC for .NET configuration](configuration?view=aspnetcore-10.0)
* [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0)

This article provides guidance for gathering diagnostics from a gRPC app to help troubleshoot issues. Topics covered include:

* **Logging** - Structured logs written to [.NET logging](../fundamentals/logging/?view=aspnetcore-10.0). [ILogger](/en-us/dotnet/api/microsoft.extensions.logging.ilogger) is used by app frameworks to write logs, and by users for their own logging in an app.
* **Tracing** - Events related to an operation written using `DiaganosticSource` and `Activity`. Traces from diagnostic source are commonly used to collect app telemetry by libraries such as [Application Insights](/en-us/azure/azure-monitor/app/asp-net-core) and [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-dotnet).
* **Metrics** - Representation of data measures over intervals of time, for example, requests per second. Metrics are emitted using `EventCounter` and can be observed using [dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters) command line tool or with [Application Insights](/en-us/azure/azure-monitor/app/eventcounters).

## Logging

gRPC services and the gRPC client write logs using [.NET logging](../fundamentals/logging/?view=aspnetcore-10.0). Logs are a good place to start when debugging unexpected behavior in service and client apps.

### gRPC services logging

Warning

Server-side logs may contain sensitive information from your app. **Never** post raw logs from production apps to public forums like GitHub.

Since gRPC services are hosted on ASP.NET Core, it uses the ASP.NET Core logging system. In the default configuration, gRPC logs minimal information, but logging can be configured. See the documentation on [ASP.NET Core logging](../fundamentals/logging/?view=aspnetcore-10.0) for details on configuring ASP.NET Core logging.

gRPC adds logs under the `Grpc` category. To enable detailed logs from gRPC, configure the `Grpc` prefixes to the `Debug` level in your `appsettings.json` file by adding the following items to the `LogLevel` subsection in `Logging`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information",
      "Grpc": "Debug"
    }
  }
}
```

You can also configure this in `Startup.cs` with `ConfigureLogging`:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.AddFilter("Grpc", LogLevel.Debug);
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
```

If you aren't using JSON-based configuration, set the following configuration value in your configuration system:

* `Logging:LogLevel:Grpc` = `Debug`

Check the documentation for your configuration system to determine how to specify nested configuration values. For example, when using environment variables, two `_` characters are used instead of the `:` (for example, `Logging__LogLevel__Grpc`).

We recommend using the `Debug` level when gathering detailed diagnostics for an app. The `Trace` level produces low-level diagnostics and is rarely needed to diagnose issues.

#### Sample logging output

Here is an example of console output at the `Debug` level of a gRPC service:

```
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2 POST https://localhost:5001/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
dbug: Grpc.AspNetCore.Server.ServerCallHandler[1]
      Reading message.
info: GrpcService.GreeterService[0]
      Hello World
dbug: Grpc.AspNetCore.Server.ServerCallHandler[6]
      Sending message.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 1.4113ms 200 application/grpc
```

### Access server-side logs

How you access server-side logs depends on the environment in which you're running.

#### As a console app

If you're running in a console app, the [Console logger](../fundamentals/logging/?view=aspnetcore-10.0#console) should be enabled by default. gRPC logs will appear in the console.

#### Other environments

If the app is deployed to another environment (for example, Docker, Kubernetes, or Windows Service), see [Logging in .NET and ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0) for more information on how to configure logging providers suitable for the environment.

### gRPC client logging

Warning

Client-side logs may contain sensitive information from your app. **Never** post raw logs from production apps to public forums like GitHub.

To get logs from the .NET client, set the `GrpcChannelOptions.LoggerFactory` property when the client's channel is created. When calling a gRPC service from an ASP.NET Core app, the logger factory can be resolved from dependency injection (DI):

```
[ApiController]
[Route("[controller]")]
public class GreetingController : ControllerBase
{
    private ILoggerFactory _loggerFactory;

    public GreetingController(ILoggerFactory loggerFactory)
    {
        _loggerFactory = loggerFactory;
    }

    [HttpGet]
    public async Task<ActionResult<string>> Get(string name)
    {
        var channel = GrpcChannel.ForAddress("https://localhost:5001",
            new GrpcChannelOptions { LoggerFactory = _loggerFactory });
        var client = new Greeter.GreeterClient(channel);

        var reply = await client.SayHelloAsync(new HelloRequest { Name = name });
        return Ok(reply.Message);
    }
}
```

An alternative way to enable client logging is to use the [gRPC client factory](clientfactory?view=aspnetcore-10.0) to create the client. A gRPC client registered with the client factory and resolved from DI will automatically use the app's configured logging.

If your app isn't using DI, then you can create a new `ILoggerFactory` instance with [LoggerFactory.Create](/en-us/dotnet/api/microsoft.extensions.logging.loggerfactory.create). To access this method, add the [Microsoft.Extensions.Logging](https://www.nuget.org/packages/microsoft.extensions.logging/) package to your app.

```
var loggerFactory = LoggerFactory.Create(logging =>
{
    logging.AddConsole();
    logging.SetMinimumLevel(LogLevel.Debug);
});

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { LoggerFactory = loggerFactory });

var client = Greeter.GreeterClient(channel);
```

#### gRPC client log scopes

The gRPC client adds a [logging scope](../fundamentals/logging/?view=aspnetcore-10.0#log-scopes) to logs made during a gRPC call. The scope has metadata related to the gRPC call:

* **GrpcMethodType** - The gRPC method type. Possible values are names from `Grpc.Core.MethodType` enum. For example, `Unary`.
* **GrpcUri** - The relative URI of the gRPC method. For example, */greet.Greeter/SayHellos*.

#### Sample logging output

Here is an example of console output at the `Debug` level of a gRPC client:

```
dbug: Grpc.Net.Client.Internal.GrpcCall[1]
      Starting gRPC call. Method type: 'Unary', URI: 'https://localhost:5001/Greet.Greeter/SayHello'.
dbug: Grpc.Net.Client.Internal.GrpcCall[6]
      Sending message.
dbug: Grpc.Net.Client.Internal.GrpcCall[1]
      Reading message.
dbug: Grpc.Net.Client.Internal.GrpcCall[4]
      Finished gRPC call.
```

## Tracing

gRPC services and the gRPC client provide information about gRPC calls using [DiagnosticSource](/en-us/dotnet/api/system.diagnostics.diagnosticsource) and [Activity](/en-us/dotnet/api/system.diagnostics.activity).

* .NET gRPC uses an activity to represent a gRPC call.
* Tracing events are written to the diagnostic source at the start and stop of the gRPC call activity.
* Tracing doesn't capture information about when messages are sent over the lifetime of gRPC streaming calls.

### gRPC service tracing

gRPC services are hosted on ASP.NET Core, which reports events about incoming HTTP requests. gRPC specific metadata is added to the existing HTTP request diagnostics that ASP.NET Core provides.

* Diagnostic source name is `Microsoft.AspNetCore`.

* Activity name is `Microsoft.AspNetCore.Hosting.HttpRequestIn`.

  * Name of the gRPC method invoked by the gRPC call is added as a tag with the name `grpc.method`.
  * Status code of the gRPC call when it is complete is added as a tag with the name `grpc.status_code`.

### gRPC client tracing

The .NET gRPC client uses `HttpClient` to make gRPC calls. Although `HttpClient` writes diagnostic events, the .NET gRPC client provides a custom diagnostic source, activity, and events so that complete information about a gRPC call can be collected.

* Diagnostic source name is `Grpc.Net.Client`.

* Activity name is `Grpc.Net.Client.GrpcOut`.

  * Name of the gRPC method invoked by the gRPC call is added as a tag with the name `grpc.method`.
  * Status code of the gRPC call when it is complete is added as a tag with the name `grpc.status_code`.

### Collecting tracing

The easiest way to use `DiagnosticSource` is to configure a telemetry library such as [Application Insights](/en-us/azure/azure-monitor/app/asp-net-core) or [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-dotnet) in your app. The library will process information about gRPC calls along-side other app telemetry.

Tracing can be viewed in a managed service like Application Insights, or you can choose to run your own distributed tracing system. OpenTelemetry supports exporting tracing data to [Jaeger](https://www.jaegertracing.io/) and [Zipkin](https://zipkin.io/).

`DiagnosticSource` can consume tracing events in code using `DiagnosticListener`. For information about listening to a diagnostic source with code, see the [DiagnosticSource user's guide](https://github.com/dotnet/corefx/blob/d3942d4671919edb0cca6ddc1840190f524a809d/src/System.Diagnostics.DiagnosticSource/src/DiagnosticSourceUsersGuide.md#consuming-data-with-diagnosticlistener).

Note

Telemetry libraries do not capture gRPC specific `Grpc.Net.Client.GrpcOut` telemetry currently. Work to improve telemetry libraries capturing this tracing is ongoing.

## Metrics

Metrics is a representation of data measures over intervals of time, for example, requests per second. Metrics data allows observation of the state of an app at a high level. .NET gRPC metrics are emitted using `EventCounter`.

### gRPC service metrics

gRPC server metrics are reported on `Grpc.AspNetCore.Server` event source.

| Name                      | Description                   |
| ------------------------- | ----------------------------- |
| `total-calls`             | Total Calls                   |
| `current-calls`           | Current Calls                 |
| `calls-failed`            | Total Calls Failed            |
| `calls-deadline-exceeded` | Total Calls Deadline Exceeded |
| `messages-sent`           | Total Messages Sent           |
| `messages-received`       | Total Messages Received       |
| `calls-unimplemented`     | Total Calls Unimplemented     |

ASP.NET Core also provides its own metrics on `Microsoft.AspNetCore.Hosting` event source.

### gRPC client metrics

gRPC client metrics are reported on `Grpc.Net.Client` event source.

| Name                      | Description                   |
| ------------------------- | ----------------------------- |
| `total-calls`             | Total Calls                   |
| `current-calls`           | Current Calls                 |
| `calls-failed`            | Total Calls Failed            |
| `calls-deadline-exceeded` | Total Calls Deadline Exceeded |
| `messages-sent`           | Total Messages Sent           |
| `messages-received`       | Total Messages Received       |

### Observe metrics

[dotnet-counters](/en-us/dotnet/core/diagnostics/dotnet-counters) is a performance monitoring tool for ad-hoc health monitoring and first-level performance investigation. Monitor a .NET app with either `Grpc.AspNetCore.Server` or `Grpc.Net.Client` as the provider name.

```
> dotnet-counters monitor --process-id 1902 --counters Grpc.AspNetCore.Server

Press p to pause, r to resume, q to quit.
    Status: Running
[Grpc.AspNetCore.Server]
    Total Calls                                 300
    Current Calls                               5
    Total Calls Failed                          0
    Total Calls Deadline Exceeded               0
    Total Messages Sent                         295
    Total Messages Received                     300
    Total Calls Unimplemented                   0
```

Another way to observe gRPC metrics is to capture counter data using Application Insights's [Microsoft.ApplicationInsights.EventCounterCollector package](/en-us/azure/azure-monitor/app/eventcounters). Once setup, Application Insights collects common .NET counters at runtime. gRPC's counters are not collected by default, but App Insights can be [customized to include additional counters](/en-us/azure/azure-monitor/app/eventcounters#customizing-counters-to-be-collected).

Specify the gRPC counters for Application Insight to collect in `Startup.cs`:

```
    using Microsoft.ApplicationInsights.Extensibility.EventCounterCollector;

    public void ConfigureServices(IServiceCollection services)
    {
        //... other code...

        services.ConfigureTelemetryModule<EventCounterCollectionModule>(
            (module, o) =>
            {
                // Configure App Insights to collect gRPC counters gRPC services hosted in an ASP.NET Core app
                module.Counters.Add(new EventCounterCollectionRequest("Grpc.AspNetCore.Server", "current-calls"));
                module.Counters.Add(new EventCounterCollectionRequest("Grpc.AspNetCore.Server", "total-calls"));
                module.Counters.Add(new EventCounterCollectionRequest("Grpc.AspNetCore.Server", "calls-failed"));
            }
        );
    }
```

## Additional resources

* [Logging in .NET and ASP.NET Core](../fundamentals/logging/?view=aspnetcore-10.0)
* [gRPC for .NET configuration](configuration?view=aspnetcore-10.0)
* [gRPC client factory integration in .NET](clientfactory?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-5.0
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-5.0.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# What's new in ASP.NET Core in .NET 5

This article highlights the most significant changes in ASP.NET Core in .NET 5 with links to relevant documentation.

## ASP.NET Core MVC and Razor improvements

### Model binding DateTime as UTC

Model binding now supports binding UTC time strings to `DateTime`. If the request contains a UTC time string, model binding binds it to a UTC `DateTime`. For example, the following time string is bound the UTC `DateTime`: `https://example.com/mycontroller/myaction?time=2019-06-14T02%3A30%3A04.0576719Z`

### Model binding and validation with C# 9 record types

[C# 9 record types](/en-us/dotnet/csharp/whats-new/csharp-9#record-types) can be used with model binding in an MVC controller or a Razor Page. Record types are a good way to model data being transmitted over the network.

For example, the following `PersonController` uses the `Person` record type with model binding and form validation:

```
public record Person([Required] string Name, [Range(0, 150)] int Age);

public class PersonController
{
   public IActionResult Index() => View();

   [HttpPost]
   public IActionResult Index(Person person)
   {
          // ...
   }
}
```

The `Person/Index.cshtml` file:

```
@model Person

<label>Name: <input asp-for="Model.Name" /></label>
<span asp-validation-for="Model.Name" />

<label>Age: <input asp-for="Model.Age" /></label>
<span asp-validation-for="Model.Age" />
```

### Improvements to DynamicRouteValueTransformer

ASP.NET Core 3.1 introduced [DynamicRouteValueTransformer](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.dynamicroutevaluetransformer) as a way to use custom endpoint to dynamically select an MVC controller action or a Razor page. .NET 5 apps can pass state to a `DynamicRouteValueTransformer` and filter the set of endpoints chosen.

### Miscellaneous

* The [\[Compare\]](/en-us/dotnet/api/system.componentmodel.dataannotations.compareattribute) attribute can be applied to properties on a Razor Page model.
* Parameters and properties bound from the body are considered required by default.

## Web API

### OpenAPI Specification on by default

[OpenAPI Specification](http://spec.openapis.org/oas/v3.0.3) is an industry standard for describing HTTP APIs and integrating them into complex business processes or with third parties. OpenAPI is widely supported by all cloud providers and many API registries. Apps that emit OpenAPI documents from web APIs have a variety of new opportunities in which those APIs can be used. In partnership with the maintainers of the open-source project [Swashbuckle.AspNetCore](https://www.nuget.org/packages/Swashbuckle.AspNetCore/), the ASP.NET Core API template contains a NuGet dependency on [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore). Swashbuckle is a popular open-source NuGet package that emits OpenAPI documents dynamically. Swashbuckle does this by introspecting over the API controllers and generating the OpenAPI document at run-time, or at build time using the Swashbuckle CLI.

In .NET 5, the web API templates enable the OpenAPI support by default. To disable OpenAPI:

* From the command line:

  ```
    dotnet new webapi --no-openapi true
  ```

* From Visual Studio: Uncheck **Enable OpenAPI support**.

All `.csproj` files created for web API projects contain the [Swashbuckle.AspNetCore](https://www.nuget.org/packages/Swashbuckle.AspNetCore/) NuGet package reference.

```
<ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
</ItemGroup>
```

The template generated code contains code in `Startup.ConfigureServices` that activates OpenAPI document generation:

```
public void ConfigureServices(IServiceCollection services)
{

    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApp1", Version = "v1" });
    });
}
```

The `Startup.Configure` method adds the Swashbuckle middleware, which enables the:

* Document generation process.
* Swagger UI page by default in development mode.

The template generated code won't accidentally expose the API's description when publishing to production.

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();  // UseSwaggerUI Protected by if (env.IsDevelopment())
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
                         "WebApp1 v1"));
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

#### Azure API Management Import

When ASP.NET Core API projects enable OpenAPI, the Visual Studio 2019 version 16.8 or later publishing automatically offer an additional step in the publishing flow. Developers who use [Azure API Management](../tutorials/publish-to-azure-api-management-using-vs?view=aspnetcore-10.0) have an opportunity to automatically import the APIs into Azure API Management during the publish flow:

### Better launch experience for web API projects

With OpenAPI enabled by default, the app launching experience (F5) for web API developers significantly improves. With .NET 5, the web API template comes pre-configured to load up the Swagger UI page. The Swagger UI page provides both the documentation added for the published API, and enables testing the APIs with a single click.

## Blazor

### Performance improvements

For .NET 5, we made significant improvements to .NET WebAssembly runtime performance with a specific focus on complex UI rendering and JSON serialization. In our performance tests, Blazor WebAssembly in .NET 5 is two to three times faster for most scenarios. For more information, see [ASP.NET Blog: ASP.NET Core updates in .NET 5 Release Candidate 1](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-5-release-candidate-1/#blazor-webassembly-performance-improvements).

### CSS isolation

Blazor now supports defining CSS styles that are scoped to a given component. Component-specific CSS styles make it easier to reason about the styles in an app and to avoid unintentional side effects of global styles. For more information, see [ASP.NET Core Blazor CSS isolation](../blazor/components/css-isolation?view=aspnetcore-10.0).

### New `InputFile` component

The `InputFile` component allows reading one or more files selected by a user for upload. For more information, see [ASP.NET Core Blazor file uploads](../blazor/file-uploads?view=aspnetcore-10.0).

### New `InputRadio` and `InputRadioGroup` components

Blazor has built-in `InputRadio` and `InputRadioGroup` components that simplify data binding to radio button groups with integrated validation. For more information, see [ASP.NET Core Blazor input components](../blazor/forms/input-components?view=aspnetcore-10.0).

### Component virtualization

Improve the perceived performance of component rendering using the Blazor framework's built-in virtualization support. For more information, see [ASP.NET Core Razor component virtualization](../blazor/components/virtualization?view=aspnetcore-10.0).

### `ontoggle` event support

Blazor events now support the `ontoggle` DOM event. For more information, see [ASP.NET Core Blazor event handling](../blazor/components/event-handling?view=aspnetcore-10.0#event-arguments).

### Set UI focus in Blazor apps

Use the `FocusAsync` convenience method on element references to set the UI focus to that element. For more information, see [ASP.NET Core Blazor event handling](../blazor/components/event-handling?view=aspnetcore-10.0#focus-an-element).

### Custom validation CSS class attributes

Custom validation CSS class attributes are useful when integrating with CSS frameworks, such as Bootstrap. For more information, see [ASP.NET Core Blazor forms validation](../blazor/forms/validation?view=aspnetcore-10.0#customize-validation-appearance).

### IAsyncDisposable support

Razor components now support the [IAsyncDisposable](/en-us/dotnet/api/system.iasyncdisposable) interface for the asynchronous release of allocated resources.

### JavaScript isolation and object references

Blazor enables JavaScript isolation in standard [JavaScript modules](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules). For more information, see [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](../blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules).

### Form components support display name

The following built-in components support display names with the `DisplayName` parameter:

* `InputDate`
* `InputNumber`
* `InputSelect`

For more information, see [ASP.NET Core Blazor forms overview](../blazor/forms/?view=aspnetcore-10.0#display-name-support).

### Catch-all route parameters

Catch-all route parameters, which capture paths across multiple folder boundaries, are supported in components. For more information, see [ASP.NET Core Blazor routing](../blazor/fundamentals/routing?view=aspnetcore-10.0#catch-all-route-parameters).

### Debugging improvements

Debugging Blazor WebAssembly apps is improved in .NET 5. Additionally, debugging is now supported on Visual Studio for Mac. For more information, see [Debug ASP.NET Core Blazor apps](../blazor/debug?view=aspnetcore-10.0).

### Microsoft Identity v2.0 and MSAL v2.0

Blazor security now uses Microsoft Identity v2.0 ([`Microsoft.Identity.Web`](https://www.nuget.org/packages/Microsoft.Identity.Web) and [`Microsoft.Identity.Web.UI`](https://www.nuget.org/packages/Microsoft.Identity.Web.UI)) and MSAL v2.0. For more information, see the topics in the [Blazor Security and Identity node](../blazor/security/?view=aspnetcore-10.0).

### Protected Browser Storage for Blazor Server

Blazor Server apps can now use built-in support for storing app state in the browser that has been protected from tampering using ASP.NET Core data protection. Data can be stored in either local browser storage or session storage. For more information, see [ASP.NET Core Blazor state management overview](../blazor/state-management/?view=aspnetcore-10.0) and [ASP.NET Core Blazor server-side state management](../blazor/state-management/server?view=aspnetcore-10.0).

### Blazor WebAssembly prerendering

Component integration is improved across hosting models, and Blazor WebAssembly apps can now prerender output on the server.

### Trimming/linking improvements

Blazor WebAssembly performs Intermediate Language (IL) trimming/linking during a build to trim unnecessary IL from the app's output assemblies. With the release of .NET 5, Blazor WebAssembly performs improved trimming with additional configuration options. For more information, see [Configure the Trimmer for ASP.NET Core Blazor](../blazor/host-and-deploy/configure-trimmer?view=aspnetcore-10.0) and [Trimming options](/en-us/dotnet/core/deploying/trimming-options).

### Browser compatibility analyzer

Blazor WebAssembly apps target the full .NET API surface area, but not all .NET APIs are supported on WebAssembly due to browser sandbox constraints. Unsupported APIs throw [PlatformNotSupportedException](/en-us/dotnet/api/system.platformnotsupportedexception) when running on WebAssembly. A platform compatibility analyzer warns the developer when the app uses APIs that aren't supported by the app's target platforms. For more information, see [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../blazor/components/class-libraries?view=aspnetcore-10.0#browser-compatibility-analyzer-for-blazor-webassembly).

### Lazy load assemblies

Blazor WebAssembly app startup performance can be improved by deferring the loading of some application assemblies until they're required. For more information, see [Lazy load assemblies in ASP.NET Core Blazor WebAssembly](../blazor/webassembly-lazy-load-assemblies?view=aspnetcore-10.0).

### Updated globalization support

Globalization support is available for Blazor WebAssembly based on International Components for Unicode (ICU). For more information, see [ASP.NET Core Blazor globalization and localization](../blazor/globalization-localization?view=aspnetcore-10.0).

## gRPC

Many performance improvements have been made in [gRPC](https://grpc.io/). For more information, see [gRPC performance improvements in .NET 5](https://devblogs.microsoft.com/aspnet/grpc-performance-improvements-in-net-5/).

For more gRPC information, see [Overview for gRPC on .NET](../grpc/?view=aspnetcore-10.0).

## SignalR

### SignalR Hub filters

SignalR Hub filters, called Hub pipelines in ASP.NET SignalR, is a feature that allows code to run before and after Hub methods are called. Running code before and after Hub methods are called is similar to how middleware has the ability to run code before and after an HTTP request. Common uses include logging, error handling, and argument validation.

For more information, see [Use hub filters in ASP.NET Core SignalR](../signalr/hub-filters?view=aspnetcore-10.0).

### SignalR parallel hub invocations

ASP.NET Core SignalR is now capable of handling parallel hub invocations. The default behavior can be changed to allow clients to invoke more than one hub method at a time:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR(options =>
    {
        options.MaximumParallelInvocationsPerClient = 5;
    });
}
```

### Added Messagepack support in SignalR Java client

A new package, [com.microsoft.signalr.messagepack](https://mvnrepository.com/artifact/com.microsoft.signalr.messagepack), adds MessagePack support to the SignalR Java client. To use the MessagePack hub protocol, add `.withHubProtocol(new MessagePackHubProtocol())` to the connection builder:

```
HubConnection hubConnection = HubConnectionBuilder.create(
                           "http://localhost:53353/MyHub")
               .withHubProtocol(new MessagePackHubProtocol())
               .build();
```

## Kestrel

* Reloadable endpoints via configuration: Kestrel can detect changes to configuration passed to [KestrelServerOptions.Configure](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configure) and unbind from existing endpoints and bind to new endpoints without requiring an application restart when the `reloadOnChange` parameter is `true`. By default when using [ConfigureWebHostDefaults](/en-us/dotnet/api/microsoft.extensions.hosting.generichostbuilderextensions.configurewebhostdefaults) or [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder), Kestrel binds to the ["Kestrel"](https://github.com/dotnet/aspnetcore/blob/7e9e03b70124784b1de5564c573bd65cdaccbfcc/src/DefaultBuilder/src/WebHost.cs#L226) configuration subsection with `reloadOnChange` enabled. Apps must pass `reloadOnChange: true` when calling `KestrelServerOptions.Configure` manually to get reloadable endpoints.

* HTTP/2 response headers improvements. For more information, see [Performance improvements](#performance-improvements) in the next section.

* Support for additional endpoints types in the sockets transport: Adding to the new API introduced in [System.Net.Sockets](/en-us/dotnet/api/system.net.sockets), the sockets default transport in [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) allows binding to both existing file handles and Unix domain sockets. Support for binding to existing file handles enables using the existing `Systemd` integration without requiring the `libuv` transport.

* Custom header decoding in Kestrel: Apps can specify which [Encoding](/en-us/dotnet/api/system.text.encoding) to use to interpret incoming headers based on the header name instead of defaulting to UTF-8. Set the `Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions.RequestHeaderEncodingSelector` property to specify which encoding to use:

  ```
  public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(options =>
            {
                options.RequestHeaderEncodingSelector = encoding =>
                {
                      return encoding switch
                      {
                          "Host" => System.Text.Encoding.Latin1,
                          _ => System.Text.Encoding.UTF8,
                      };
                };
            });
            webBuilder.UseStartup<Startup>();
        });
  ```

### Kestrel endpoint-specific options via configuration

Support has been added for configuring Kestrel's endpoint-specific options via [configuration](../fundamentals/configuration/?view=aspnetcore-10.0). The endpoint-specific configurations includes the:

* HTTP protocols used
* TLS protocols used
* Certificate selected
* Client certificate mode

Configuration allows specifying which certificate is selected based on the specified server name. The server name is part of the Server Name Indication (SNI) extension to the TLS protocol as indicated by the client. Kestrel's configuration also supports a wildcard prefix in the host name.

The following example shows how to specify endpoint-specific using a configuration file:

```
{
  "Kestrel": {
    "Endpoints": {
      "EndpointName": {
        "Url": "https://*",
        "Sni": {
          "a.example.org": {
            "Protocols": "Http1AndHttp2",
            "SslProtocols": [ "Tls11", "Tls12"],
            "Certificate": {
              "Path": "testCert.pfx",
              "Password": "testPassword"
            },
            "ClientCertificateMode" : "NoCertificate"
          },
          "*.example.org": {
            "Certificate": {
              "Path": "testCert2.pfx",
              "Password": "testPassword"
            }
          },
          "*": {
            // At least one sub-property needs to exist per
            // SNI section or it cannot be discovered via
            // IConfiguration
            "Protocols": "Http1",
          }
        }
      }
    }
  }
}
```

Server Name Indication (SNI) is a TLS extension to include a virtual domain as a part of SSL negotiation. What this effectively means is that the virtual domain name, or a hostname, can be used to identify the network end point.

## Performance improvements

### HTTP/2

* Significant reductions in allocations in the HTTP/2 code path.

* Support for [HPack dynamic compression](https://tools.ietf.org/html/rfc7541) of HTTP/2 response headers in [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0). For more information, see [Header table size](../fundamentals/servers/kestrel/options?view=aspnetcore-10.0#header-table-size) and [HPACK: the silent killer (feature) of HTTP/2](https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/).

* Sending HTTP/2 PING frames: HTTP/2 has a mechanism for sending PING frames to ensure an idle connection is still functional. Ensuring a viable connection is especially useful when working with long-lived streams that are often idle but only intermittently see activity, for example, gRPC streams. Apps can send periodic PING frames in [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) by setting limits on [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelserveroptions):

  ```
  public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(options =>
            {
                options.Limits.Http2.KeepAlivePingInterval =
                                               TimeSpan.FromSeconds(10);
                options.Limits.Http2.KeepAlivePingTimeout =
                                               TimeSpan.FromSeconds(1);
            });
            webBuilder.UseStartup<Startup>();
        });
  ```

### Containers

Prior to .NET 5, building and publishing a *Dockerfile* for an ASP.NET Core app required pulling the entire .NET Core SDK and the ASP.NET Core image. With this release, pulling the SDK images bytes is reduced and the bytes pulled for the ASP.NET Core image is largely eliminated. For more information, see [this GitHub issue comment](https://github.com/dotnet/dotnet-docker/issues/1814#issuecomment-625294750).

## Authentication and authorization

### Microsoft Entra ID authentication with Microsoft.Identity.Web

The ASP.NET Core project templates now integrate with [Microsoft.Identity.Web](/en-us/dotnet/api/microsoft.identity.web) to handle authentication with [Microsoft Entra ID](/en-us/entra/fundamentals/what-is-entra). The [Microsoft.Identity.Web package](https://www.nuget.org/packages/Microsoft.Identity.Web/) provides:

* A better experience for authentication through Microsoft Entra ID.
* An easier way to access Azure resources on behalf of your users, including [Microsoft Graph](/en-us/graph/overview). See the [Microsoft.Identity.Web sample](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2), which starts with a basic login and advances through multi-tenancy, using Azure APIs, using Microsoft Graph, and protecting your own APIs. `Microsoft.Identity.Web` is available alongside .NET 5.

### Allow anonymous access to an endpoint

The `AllowAnonymous` extension method allows anonymous access to an endpoint:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        })
        .AllowAnonymous();
    });
}
```

### Custom handling of authorization failures

Custom handling of authorization failures is now easier with the new [IAuthorizationMiddlewareResultHandler](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationmiddlewareresulthandler) interface that is invoked by the [authorization middleware (`UseAuthorization`)](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The default implementation remains the same, but a custom handler can be registered with dependency injection, which allows custom HTTP responses based on an authorization failure. See [`SampleAuthorizationMiddlewareResultHandler.cs` (reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/Security/samples/CustomAuthorizationFailureResponse/Authorization/SampleAuthorizationMiddlewareResultHandler.cs) for a demonstration of `IAuthorizationMiddlewareResultHandler`.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Authorization when using endpoint routing

Authorization when using endpoint routing now receives the `HttpContext` rather than the endpoint instance. This allows the authorization middleware to access the `RouteData` and other properties of the `HttpContext` that were not accessible though the [Endpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpoint) class. The endpoint can be fetched from the context using [context.GetEndpoint](/en-us/dotnet/api/microsoft.aspnetcore.http.endpointhttpcontextextensions.getendpoint).

### Role-based access control with Kerberos authentication and LDAP on Linux

See [Kerberos authentication and role-based access control (RBAC)](../security/authentication/windowsauth?view=aspnetcore-10.0#rbac)

## API improvements

### JSON extension methods for HttpRequest and HttpResponse

JSON data can be read and written to from an `HttpRequest` and `HttpResponse` using the new [ReadFromJsonAsync](/en-us/dotnet/api/system.net.http.json.httpcontentjsonextensions.readfromjsonasync) and `WriteAsJsonAsync` extension methods. These extension methods use the [System.Text.Json](/en-us/dotnet/api/system.text.json) serializer to handle the JSON data. The new `HasJsonContentType` extension method can also check if a request has a JSON content type.

The JSON extension methods can be combined with [endpoint routing](../fundamentals/routing?view=aspnetcore-10.0) to create JSON APIs in a style of programming we call ***route to code***. It is a new option for developers who want to create basic JSON APIs in a lightweight way. For example, a web app that has only a handful of endpoints might choose to use route to code rather than the full functionality of ASP.NET Core MVC:

```
endpoints.MapGet("/weather/{city:alpha}", async context =>
{
    var city = (string)context.Request.RouteValues["city"];
    var weather = GetFromDatabase(city);

    await context.Response.WriteAsJsonAsync(weather);
});
```

### System.Diagnostics.Activity

The default format for [System.Diagnostics.Activity](/en-us/dotnet/api/system.diagnostics.activity) now defaults to the W3C format. This makes distributed tracing support in ASP.NET Core interoperable with more frameworks by default.

### FromBodyAttribute

[FromBodyAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) now supports configuring an option that allows these parameters or properties to be considered optional:

```
public IActionResult Post([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]
                          MyModel model)
{
    ...
}
```

## Miscellaneous improvements

We've started applying [nullable annotations](/en-us/dotnet/csharp/nullable-references#attributes-describe-apis) to ASP.NET Core assemblies. We plan to annotate most of the common public API surface of the .NET 5 framework.

### Control Startup class activation

An additional [UseStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usestartup) overload has been added that lets an app provide a factory method for controlling `Startup` class activation. Controlling `Startup` class activation is useful to pass additional parameters to `Startup` that are initialized along with the host:

```
public class Program
{
    public static async Task Main(string[] args)
    {
        var logger = CreateLogger();
        var host = Host.CreateDefaultBuilder()
            .ConfigureWebHost(builder =>
            {
                builder.UseStartup(context => new Startup(logger));
            })
            .Build();

        await host.RunAsync();
    }
}
```

### Auto refresh with dotnet watch

In .NET 5, running [dotnet watch](../tutorials/dotnet-watch?view=aspnetcore-10.0) on an ASP.NET Core project both launches the default browser and auto refreshes the browser as changes are made to the code. This means you can:

* Open an ASP.NET Core project in a text editor.
* Run `dotnet watch`.
* Focus on the code changes while the tooling handles rebuilding, restarting, and reloading the app.

### Console Logger Formatter

Improvements have been made to the console log provider in the `Microsoft.Extensions.Logging` library. Developers can now implement a custom `ConsoleFormatter` to exercise complete control over formatting and colorization of the console output. The formatter APIs allow for rich formatting by implementing a subset of the VT-100 escape sequences. VT-100 is supported by most modern terminals. The console logger can parse out escape sequences on unsupported terminals allowing developers to author a single formatter for all terminals.

### JSON Console Logger

In addition to support for custom formatters, we've also added a built-in JSON formatter that emits structured JSON logs to the console. The following code shows how to switch from the default logger to JSON:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
           Host.CreateDefaultBuilder(args)
  .ConfigureLogging(logging =>
  {
     logging.AddJsonConsole(options =>
     {
         options.JsonWriterOptions = new JsonWriterOptions()
         { Indented = true };
     });
  })
  .ConfigureWebHostDefaults(webBuilder =>
  {
    webBuilder.UseStartup<Startup>();
  });
```

Log messages emitted to the console are JSON formatted:

```
{
  "EventId": 0,
  "LogLevel": "Information",
  "Category": "Microsoft.Hosting.Lifetime",
  "Message": "Now listening on: https://localhost:5001",
  "State": {
    "Message": "Now listening on: https://localhost:5001",
    "address": "https://localhost:5001",
    "{OriginalFormat}": "Now listening on: {address}"
  }
}
```

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/libman/libman-vs
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/libman/libman-vs.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use LibMan with ASP.NET Core in Visual Studio

By [Scott Addie](https://twitter.com/Scott_Addie)

Visual Studio has built-in support for [LibMan](./?view=aspnetcore-10.0) in ASP.NET Core projects, including:

* Support for configuring and running LibMan restore operations on build.
* Menu items for triggering LibMan restore and clean operations.
* Search dialog for finding libraries and adding the files to a project.
* Editing support for `libman.json`—the LibMan manifest file.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/client-side/libman/samples/) [(how to download)](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)

## Prerequisites

* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the **ASP.NET and web development** workload

## Add library files

Library files can be added to an ASP.NET Core project in two different ways:

1. [Use the Add Client-Side Library dialog](#use-the-add-client-side-library-dialog)
2. [Manually configure LibMan manifest file entries](#manually-configure-libman-manifest-file-entries)

### Use the Add Client-Side Library dialog

Follow these steps to install a client-side library:

* In **Solution Explorer**, right-click the project folder in which the files should be added. Choose **Add** > **Client-Side Library**. The **Add Client-Side Library** dialog appears:

* Select the library provider from the **Provider** drop down. CDNJS is the default provider.

* Type the library name to fetch in the **Library** text box. IntelliSense provides a list of libraries beginning with the provided text.

* Select the library from the IntelliSense list. Notice the library name is suffixed with the `@` symbol and the latest stable version known to the selected provider.

* Decide which files to include:

  * Select the **Include all library files** radio button to include all of the library's files.
  * Select the **Choose specific files** radio button to include a subset of the library's files. When the radio button is selected, the file selector tree is enabled. Check the boxes to the left of the file names to download.

* Specify the project folder for storing the files in the **Target Location** text box. As a recommendation, store each library in a separate folder.

  The suggested **Target Location** folder is based on the location from which the dialog launched:

  * If launched from the project root:

    * *wwwroot/lib* is used if *wwwroot* exists.
    * *lib* is used if *wwwroot* doesn't exist.

  * If launched from a project folder, the corresponding folder name is used.

  The folder suggestion is suffixed with the library name. The following table illustrates folder suggestions when installing jQuery in a Razor Pages project.

  | Launch location                           | Suggested folder      |
  | ----------------------------------------- | --------------------- |
  | project root (if *wwwroot* exists)        | *wwwroot/lib/jquery/* |
  | project root (if *wwwroot* doesn't exist) | *lib/jquery/*         |
  | *Pages* folder in project                 | *Pages/jquery/*       |

* Click the **Install** button to download the files, per the configuration in `libman.json`.

* Review the **Library Manager** feed of the **Output** window for installation details. For example:

  ```
  Restore operation started...
  Restoring libraries for project LibManSample
  Restoring library jquery@3.3.1... (LibManSample)
  wwwroot/lib/jquery/jquery.min.js written to destination (LibManSample)
  wwwroot/lib/jquery/jquery.js written to destination (LibManSample)
  wwwroot/lib/jquery/jquery.min.map written to destination (LibManSample)
  Restore operation completed
  1 libraries restored in 2.32 seconds
  ```

### Manually configure LibMan manifest file entries

All LibMan operations in Visual Studio are based on the content of the project root's LibMan manifest (`libman.json`). You can manually edit `libman.json` to configure library files for the project. Visual Studio restores all library files once `libman.json` is saved.

To open `libman.json` for editing, the following options exist:

* Double-click the `libman.json` file in **Solution Explorer**.
* Right-click the project in **Solution Explorer** and select **Manage Client-Side Libraries**. **†**
* Select **Manage Client-Side Libraries** from the Visual Studio **Project** menu. **†**

**†** If the `libman.json` file doesn't already exist in the project root, it will be created with the default item template content.

Visual Studio offers rich JSON editing support such as colorization, formatting, IntelliSense, and schema validation. The LibMan manifest's JSON schema is found at <https://json.schemastore.org/libman>.

With the following manifest file, LibMan retrieves files per the configuration defined in the `libraries` property. An explanation of the object literals defined within `libraries` follows:

* A subset of [jQuery](https://jquery.com/) version 3.3.1 is retrieved from the CDNJS provider. The subset is defined in the `files` property—`jquery.min.js`, `jquery.js`, and *jquery.min.map*. The files are placed in the project's *wwwroot/lib/jquery* folder.
* The entirety of [Bootstrap](https://getbootstrap.com/) version 4.1.3 is retrieved and placed in a *wwwroot/lib/bootstrap* folder. The object literal's `provider` property overrides the `defaultProvider` property value. LibMan retrieves the Bootstrap files from the unpkg provider.

```
{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "jquery@3.7.1",
      "files": [
        "jquery.min.js",
        "jquery.js",
        "jquery.min.map"
      ],
      "destination": "wwwroot/lib/jquery/"
    },
    {
      "provider": "unpkg",
      "library": "bootstrap@5.3.6",
      "destination": "wwwroot/lib/bootstrap/"
    }
  ]
}
```

Note

LibMan only supports one version of each library from each provider. The `libman.json` file fails schema validation if it contains two libraries with the same library name for a given provider.

## Restore library files

To restore library files from within Visual Studio, there must be a valid `libman.json` file in the project root. Restored files are placed in the project at the location specified for each library.

Library files can be restored in an ASP.NET Core project in two ways:

1. [Restore files during build](#restore-files-during-build)
2. [Restore files manually](#restore-files-manually)

### Restore files during build

LibMan can restore the defined library files as part of the build process. By default, the *restore-on-build* behavior is disabled.

To enable and test the restore-on-build behavior:

* Right-click `libman.json` in **Solution Explorer** and select **Enable Restore Client-Side Libraries on Build** from the context menu.

* Click the **Yes** button when prompted to install a NuGet package. The [Microsoft.Web.LibraryManager.Build](https://www.nuget.org/packages/Microsoft.Web.LibraryManager.Build/) NuGet package is added to the project:

  ```
  <PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="3.0.71" />
  ```

* Build the project to confirm LibMan file restoration occurs. The `Microsoft.Web.LibraryManager.Build` package injects an MSBuild target that runs LibMan during the project's build operation.

* Review the **Build** feed of the **Output** window for a LibMan activity log:

  ```
  1>------ Build started: Project: LibManSample, Configuration: Debug Any CPU ------
  1>
  1>Restore operation started...
  1>Restoring library jquery@3.3.1...
  1>Restoring library bootstrap@4.1.3...
  1>
  1>2 libraries restored in 10.66 seconds
  1>LibManSample -> C:\LibManSample\bin\Debug\netcoreapp2.1\LibManSample.dll
  ========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
  ```

When the restore-on-build behavior is enabled, the `libman.json` context menu displays a **Disable Restore Client-Side Libraries on Build** option. Selecting this option removes the `Microsoft.Web.LibraryManager.Build` package reference from the project file. Consequently, the client-side libraries are no longer restored on each build.

Regardless of the restore-on-build setting, you can manually restore at any time from the `libman.json` context menu. For more information, see [Restore files manually](#restore-files-manually).

### Restore files manually

To manually restore library files:

* For all projects in the solution:

  * Right-click the solution name in **Solution Explorer**.
  * Select the **Restore Client-Side Libraries** option.

* For a specific project:

  * Right-click the `libman.json` file in **Solution Explorer**.
  * Select the **Restore Client-Side Libraries** option.

While the restore operation is running:

* The Task Status Center (TSC) icon on the Visual Studio status bar will be animated and will read *Restore operation started*. Clicking the icon opens a tooltip listing the known background tasks.

* Messages will be sent to the status bar and the **Library Manager** feed of the **Output** window. For example:

  ```
  Restore operation started...
  Restoring libraries for project LibManSample
  Restoring library jquery@3.3.1... (LibManSample)
  wwwroot/lib/jquery/jquery.min.js written to destination (LibManSample)
  wwwroot/lib/jquery/jquery.js written to destination (LibManSample)
  wwwroot/lib/jquery/jquery.min.map written to destination (LibManSample)
  Restore operation completed
  1 libraries restored in 2.32 seconds
  ```

## Delete library files

To perform the *clean* operation, which deletes library files previously restored in Visual Studio:

* Right-click the `libman.json` file in **Solution Explorer**.
* Select the **Clean Client-Side Libraries** option.

To prevent unintentional removal of non-library files, the clean operation doesn't delete whole directories. It only removes files that were included in the previous restore.

While the clean operation is running:

* The TSC icon on the Visual Studio status bar will be animated and will read *Client libraries operation started*. Clicking the icon opens a tooltip listing the known background tasks.
* Messages are sent to the status bar and the **Library Manager** feed of the **Output** window. For example:

```
Clean libraries operation started...
Clean libraries operation completed
2 libraries were successfully deleted in 1.91 secs
```

The clean operation only deletes files from the project. Library files stay in the cache for faster retrieval on future restore operations. To manage library files stored in the local machine's cache, use the [LibMan CLI](libman-cli?view=aspnetcore-10.0).

## Uninstall library files

To uninstall library files:

* Open `libman.json`.

* Position the caret inside the corresponding `libraries` object literal.

* Click the light bulb icon that appears in the left margin, and select **Uninstall \<library\_name>@\<library\_version>**:

Alternatively, you can manually edit and save the LibMan manifest (`libman.json`). The [restore operation](#restore-library-files) runs when the file is saved. Library files that are no longer defined in `libman.json` are removed from the project.

## Update library version

To check for an updated library version:

* Open `libman.json`.
* Position the caret inside the corresponding `libraries` object literal.
* Click the light bulb icon that appears in the left margin. Hover over **Check for updates**.

LibMan checks for a library version newer than the version installed. The following outcomes can occur:

* A **No updates found** message is displayed if the latest version is already installed.

* The latest stable version is displayed if not already installed.

* If a pre-release newer than the installed version is available, the pre-release is displayed.

To downgrade to an older library version, manually edit the `libman.json` file. When the file is saved, the LibMan [restore operation](#restore-library-files):

* Removes redundant files from the previous version.
* Adds new and updated files from the new version.

## Related content

* [Use the LibMan CLI with ASP.NET Core](libman-cli?view=aspnetcore-10.0)
* [LibMan GitHub repository](https://github.com/aspnet/LibraryManager)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/test/load-tests
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/load-tests.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core load/stress testing

Load testing and stress testing are important to ensure a web app is performant and scalable. Load and stress testing have different goals even though they often share similar tests.

**Load tests**: Test whether the app can handle a specified load of users for a certain scenario while still satisfying the response goal. The app is run under normal conditions.

**Stress tests**: Test app stability when running under extreme conditions, often for a long period of time. The tests place high user load, either spikes or gradually increasing load on the app, or they limit the app's computing resources.

Stress tests determine if an app under stress can recover from failure and gracefully return to expected behavior. Under stress, the app is run at abnormally high stress.

[Azure Load Testing](/en-us/azure/load-testing/overview-what-is-azure-load-testing) is a fully managed load-testing service that enables you to generate high-scale load. The service simulates traffic for apps, regardless of where they're hosted. Azure Load Testing Preview enables you to use existing Apache JMeter scripts to generate high-scale load.

[Visual Studio 2019 load testing](https://devblogs.microsoft.com/devops/cloud-based-load-testing-service-eol/) has been deprecated. The corresponding Azure DevOps cloud-based load testing service has been closed.

## Third-party tools

The following list contains third-party web performance tools with various feature sets:

* [Apache JMeter](https://jmeter.apache.org/)
* [ApacheBench (ab)](https://httpd.apache.org/docs/2.4/programs/ab.html)
* [Gatling](https://gatling.io/)
* [jmeter-dotnet-dsl](https://abstracta.github.io/jmeter-dotnet-dsl/)
* [k6](https://k6.io)
* [Locust](https://locust.io/)
* [West Wind WebSurge](https://websurge.west-wind.com/)
* [Netling](https://github.com/hallatore/Netling)
* [Vegeta](https://github.com/tsenart/vegeta)
* [NBomber C#/F#](https://nbomber.com/)

## Load and stress test with release builds

Load and stress tests should be done in release and [production](../fundamentals/environments?view=aspnetcore-10.0) mode and not in debug and development mode. [Release configurations](/en-us/visualstudio/debugger/how-to-set-debug-and-release-configurations) are fully optimized with minimal logging. Debug configuration is not optimized. [Development](../fundamentals/environments?view=aspnetcore-10.0) mode enables more information logging that can impact performance.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/additional-scenarios
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/additional-scenarios.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor additional server-side security scenarios

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure server-side Blazor for additional security scenarios, including how to pass tokens to a Blazor app.

Note

The code examples in this article adopt [nullable reference types (NRTs) and .NET compiler null-state static analysis](../../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis), which are supported in ASP.NET Core in .NET 6 or later. When targeting .NET 5 or earlier, remove the null type designation (`?`) from the `string?`, `TodoItem[]?`, `WeatherForecast[]?`, and `IEnumerable<GitHubBranch>?` types in the article's examples.

## Pass tokens to a server-side Blazor app

*This section applies to Blazor Web Apps. For Blazor Server, view the [.NET 7 version of this article section](additional-scenarios?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-7.0#pass-tokens-to-a-server-side-blazor-app).*

If you merely want to use access tokens to make web API calls from a Blazor Web App with a [named HTTP client](../call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory), see the [Use a token handler for web API calls](#use-a-token-handler-for-web-api-calls) section, which explains how to use a [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) implementation to attach a user's access token to outgoing requests. The following guidance in this section is for developers who need access tokens, refresh tokens, and other authentication properties server-side for other purposes.

Note

For more information on [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) instances, see [HTTP requests with IHttpClientFactory - ASP.NET Core](../../fundamentals/http-requests?view=aspnetcore-10.0#outgoing-request-middleware).

To save tokens and other authentication properties for server-side use in Blazor Web Apps, we recommend using [`IHttpContextAccessor`/`HttpContext`](../components/httpcontext?view=aspnetcore-10.0) ([IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor), [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext)). Reading tokens from [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext), including as a [cascading parameter](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingparameterattribute), using [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) is supported for obtaining tokens for use during interactive server rendering if the tokens are obtained during static server-side rendering (static SSR) or prerendering. However, tokens aren't updated if the user authenticates after the circuit is established, since the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is captured at the start of the SignalR connection. Also, the use of [AsyncLocal\<T>](/en-us/dotnet/api/system.threading.asynclocal-1) by [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) means that you must be careful not to lose the execution context before reading the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext). For more information, see [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../components/httpcontext?view=aspnetcore-10.0).

In a service class, obtain access to the members of the namespace [Microsoft.AspNetCore.Authentication](/en-us/dotnet/api/microsoft.aspnetcore.authentication) to surface the [GetTokenAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.gettokenasync) method on [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext). An alternative approach, which is commented out in the following example, is to call [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.authenticateasync) on [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext). For the returned [AuthenticateResult.Properties](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticateresult.properties), call [GetTokenValue](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationtokenextensions.gettokenvalue).

```
using Microsoft.AspNetCore.Authentication;

public class AuthenticationProcessor(IHttpContextAccessor httpContextAccessor)
{
    public async Task<string?> GetAccessToken()
    {
        if (httpContextAccessor.HttpContext is null)
        {
            throw new Exception("HttpContext not available");
        }

        // Approach 1: Call 'GetTokenAsync'
        var accessToken = await httpContextAccessor.HttpContext
            .GetTokenAsync("access_token");

        // Approach 2: Authenticate the user and call 'GetTokenValue'
        /*
        var authResult = await httpContextAccessor.HttpContext.AuthenticateAsync();
        var accessToken = authResult?.Properties?.GetTokenValue("access_token");
        */

        return accessToken;
    }
}
```

The service is registered in the server project's `Program` file:

```
builder.Services.AddScoped<AuthenticationProcessor>();
```

`AuthenticationProcessor` can be injected into server-side services, for example in a [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) for a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient). The following example is only for demonstration purposes or in case you need to perform special processing in the `AuthenticationProcessor` service because you can just inject [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) and obtain the token directly for calling external web APIs (for more information on using [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) directly to call web APIs, see the [Use a token handler for web API calls](#use-a-token-handler-for-web-api-calls) section).

```
using System.Net.Http.Headers;

public class TokenHandler(AuthenticationProcessor authProcessor) : 
    DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var accessToken = authProcessor.GetAccessToken();

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);

        return await base.SendAsync(request, cancellationToken);
    }
}
```

The token handler is registered and acts as the delegating handler for a named HTTP client in the `Program` file:

```
builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("ExternalApi",
      client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ?? 
          throw new Exception("Missing base address!")))
      .AddHttpMessageHandler<TokenHandler>();
```

Caution

Ensure that tokens are never transmitted and handled by the client (the `.Client` project), for example, in a component that adopts Interactive Auto rendering and is rendered on the client or by a client-side service. Always have the client call the server (project) to process requests with tokens. **Tokens and other authentication data should never leave the server.**

For Interactive Auto components, see [ASP.NET Core Blazor authentication and authorization](./?view=aspnetcore-10.0#secure-data-in-blazor-web-apps-with-interactive-auto-rendering), which demonstrates how to leave access tokens and other authentication properties on the server. Also, consider adopting the Backend-for-Frontend (BFF) pattern, which adopts a similar call structure and is described in [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](blazor-web-app-with-oidc?view=aspnetcore-10.0) for OIDC providers and [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0) for Microsoft Identity Web with Entra.

## Use a token handler for web API calls

The following approach is aimed at attaching a user's access token to outgoing requests, specifically to make web API calls to external web API apps. The approach is shown for a Blazor Web App that adopts global Interactive Server rendering, but the same general approach applies to Blazor Web Apps that adopt the global Interactive Auto render mode. The important concept to keep in mind is that accessing the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) using [IHttpContextAccessor](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) is only performed on the server.

For a demonstration of the guidance in this section, see the `BlazorWebAppOidc` and `BlazorWebAppOidcServer` sample apps (.NET 8 or later) in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples). The samples adopt a global interactive render mode and OIDC authentication with Microsoft Entra without using Entra-specific packages. The samples demonstrate how to pass a JWT access token to call a secure web API.

[Microsoft identity platform](/en-us/entra/identity-platform/) with [Microsoft Identity Web packages](/en-us/entra/msal/dotnet/microsoft-identity-web/) for [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra) provides a API to call web APIs from Blazor Web Apps with automatic token management and renewal. For more information, see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0) and the `BlazorWebAppEntra` and `BlazorWebAppEntraBff` sample apps (.NET 9 or later) in the [Blazor samples GitHub repository](https://github.com/dotnet/blazor-samples).

Subclass [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) to attach a user's access token to outgoing requests. The token handler only executes on the server, so using [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is safe.

`TokenHandler.cs`:

```
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Authentication;

public class TokenHandler(IHttpContextAccessor httpContextAccessor) : 
    DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (httpContextAccessor.HttpContext is null)
        {
            throw new Exception("HttpContext not available");
        }

        var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");

        if (accessToken is null)
        {
            throw new Exception("No access token");
        }

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);

        return await base.SendAsync(request, cancellationToken);
    }
}
```

Note

For guidance on how to access an `AuthenticationStateProvider` from a `DelegatingHandler`, see the [Access `AuthenticationStateProvider` in outgoing request middleware](#access-authenticationstateprovider-in-outgoing-request-middleware) section.

In the project's `Program` file, the token handler (`TokenHandler`) is registered as a scoped service and specified as a [named HTTP client's](../call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory) message handler with [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler).

In the following example, the `{HTTP CLIENT NAME}` placeholder is the name of the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient), and the `{BASE ADDRESS}` placeholder is the web API's base address URI. For more information on [AddHttpContextAccessor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpservicecollectionextensions.addhttpcontextaccessor), see [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../components/httpcontext?view=aspnetcore-10.0).

In `Program.cs`:

```
builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("{HTTP CLIENT NAME}",
      client => client.BaseAddress = new Uri("{BASE ADDRESS}"))
      .AddHttpMessageHandler<TokenHandler>();
```

Example:

```
builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("ExternalApi",
      client => client.BaseAddress = new Uri("https://localhost:7277"))
      .AddHttpMessageHandler<TokenHandler>();
```

You can supply the HTTP client base address from [configuration](../fundamentals/configuration?view=aspnetcore-10.0) with `builder.Configuration["{CONFIGURATION KEY}"]`, where the `{CONFIGURATION KEY}` placeholder is the configuration key:

```
new Uri(builder.Configuration["ExternalApiUri"] ?? throw new IOException("No URI!"))
```

In `appsettings.json`, specify the `ExternalApiUri`. The following example sets the value to the localhost address of the external web API to `https://localhost:7277`:

```
"ExternalApiUri": "https://localhost:7277"
```

At this point, an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) created by a component can make secure web API requests. In the following example, the `{REQUEST URI}` is the relative request URI, and the `{HTTP CLIENT NAME}` placeholder is the name of the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient):

```
using var request = new HttpRequestMessage(HttpMethod.Get, "{REQUEST URI}");
var client = ClientFactory.CreateClient("{HTTP CLIENT NAME}");
using var response = await client.SendAsync(request);
```

Example:

```
using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
var client = ClientFactory.CreateClient("ExternalApi");
using var response = await client.SendAsync(request);
```

Additional features are planned for Blazor, which are tracked by [Access `AuthenticationStateProvider` in outgoing request middleware (`dotnet/aspnetcore` #52379)](https://github.com/dotnet/aspnetcore/issues/52379). [Problem providing Access Token to HttpClient in Interactive Server mode (`dotnet/aspnetcore` #52390)](https://github.com/dotnet/aspnetcore/issues/52390) is a closed issue that contains helpful discussion and potential workaround strategies for advanced use cases.

Tokens available outside of the Razor components in a server-side Blazor app can be passed to components with the approach described in this section. The example in this section focuses on passing access, refresh, and [anti-request forgery (XSRF) token](../../security/anti-request-forgery?view=aspnetcore-10.0) tokens to the Blazor app, but the approach is valid for other HTTP context state.

Note

Passing the XSRF token to Razor components is useful in scenarios where components POST to Identity or other endpoints that require validation. If your app only requires access and refresh tokens, you can remove the XSRF token code from the following example.

Authenticate the app as you would with a regular Razor Pages or MVC app. Provision and save the tokens to the authentication cookie.

In the `Program` file:

```
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
```

In `Startup.cs`:

```
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
```

In `Startup.cs`:

```
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

...

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.SaveTokens = true;
    options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
```

Optionally, additional scopes are added with `options.Scope.Add("{SCOPE}");`, where the `{SCOPE}` placeholder is the additional scope to add.

Define a **scoped** token provider service that can be used within the Blazor app to resolve the tokens from [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0).

`TokenProvider.cs`:

```
public class TokenProvider
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}
```

In the `Program` file, add services for:

* [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory): Used in a `WeatherForecastService` class that obtains weather data from a server API with an access token.
* `TokenProvider`: Holds the access and refresh tokens.

```
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();
```

In `Startup.ConfigureServices` of `Startup.cs`, add services for:

* [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory): Used in a `WeatherForecastService` class that obtains weather data from a server API with an access token.
* `TokenProvider`: Holds the access and refresh tokens.

```
services.AddHttpClient();
services.AddScoped<TokenProvider>();
```

Define a class to pass in the initial app state with the access and refresh tokens.

`InitialApplicationState.cs`:

```
public class InitialApplicationState
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
    public string? XsrfToken { get; set; }
}
```

In the `Pages/_Host.cshtml` file, create and instance of `InitialApplicationState` and pass it as a parameter to the app:

In the `Pages/_Layout.cshtml` file, create and instance of `InitialApplicationState` and pass it as a parameter to the app:

In the `Pages/_Host.cshtml` file, create and instance of `InitialApplicationState` and pass it as a parameter to the app:

```
@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf

...

@{
    var tokens = new InitialApplicationState
    {
        AccessToken = await HttpContext.GetTokenAsync("access_token"),
        RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
        XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
    };
}

<component ... param-InitialState="tokens" ... />
```

In the `App` component (`App.razor`), resolve the service and initialize it with the data from the parameter:

```
@inject TokenProvider TokenProvider

...

@code {
    [Parameter]
    public InitialApplicationState? InitialState { get; set; }

    protected override Task OnInitializedAsync()
    {
        TokenProvider.AccessToken = InitialState?.AccessToken;
        TokenProvider.RefreshToken = InitialState?.RefreshToken;
        TokenProvider.XsrfToken = InitialState?.XsrfToken;

        return base.OnInitializedAsync();
    }
}
```

Note

An alternative to assigning the initial state to the `TokenProvider` in the preceding example is to copy the data into a scoped service within [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) for use across the app.

Add a package reference to the app for the [`Microsoft.AspNet.WebApi.Client`](https://www.nuget.org/packages/Microsoft.AspNet.WebApi.Client) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the service that makes a secure API request, inject the token provider and retrieve the token for the API request:

`WeatherForecastService.cs`:

```
using System;
using System.Net.Http;
using System.Threading.Tasks;

public class WeatherForecastService
{
    private readonly HttpClient http;
    private readonly TokenProvider tokenProvider;

    public WeatherForecastService(IHttpClientFactory clientFactory, 
        TokenProvider tokenProvider)
    {
        http = clientFactory.CreateClient();
        this.tokenProvider = tokenProvider;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        var token = tokenProvider.AccessToken;
        using var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://localhost:5003/WeatherForecast");
        request.Headers.Add("Authorization", $"Bearer {token}");
        using var response = await http.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ?? 
            Array.Empty<WeatherForecast>();
    }
}
```

For an XSRF token passed to a component, inject the `TokenProvider` and add the XSRF token to the POST request. The following example adds the token to a logout endpoint POST. The scenario for the following example is that the logout endpoint (`Areas/Identity/Pages/Account/Logout.cshtml`, [scaffolded into the app](../../security/authentication/scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-server-side-blazor-app)) doesn't specify an [IgnoreAntiforgeryTokenAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.ignoreantiforgerytokenattribute) (`@attribute [IgnoreAntiforgeryToken]`) because it performs some action in addition to a normal logout operation that must be protected. The endpoint requires a valid XSRF token to successfully process the request.

In a component that presents a **Logout** button to authorized users:

```
@inject TokenProvider TokenProvider

...

<AuthorizeView>
    <Authorized>
        <form action="/Identity/Account/Logout?returnUrl=%2F" method="post">
            <button class="nav-link btn btn-link" type="submit">Logout</button>
            <input name="__RequestVerificationToken" type="hidden" 
                value="@TokenProvider.XsrfToken">
        </form>
    </Authorized>
    <NotAuthorized>
        ...
    </NotAuthorized>
</AuthorizeView>
```

## Set the authentication scheme

For an app that uses more than one authentication middleware and thus has more than one authentication scheme, the scheme that Blazor uses can be explicitly set in the endpoint configuration of the `Program` file. The following example sets the OpenID Connect (OIDC) scheme:

For an app that uses more than one authentication middleware and thus has more than one authentication scheme, the scheme that Blazor uses can be explicitly set in the endpoint configuration of `Startup.cs`. The following example sets the OpenID Connect (OIDC) scheme:

```
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapRazorComponents<App>().RequireAuthorization(
    new AuthorizeAttribute
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    })
    .AddInteractiveServerRenderMode();
```

```
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

...

app.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
    });
```

For an app that uses more than one authentication middleware and thus has more than one authentication scheme, the scheme that Blazor uses can be explicitly set in the endpoint configuration of `Startup.Configure`. The following example sets the Microsoft Entra ID scheme:

```
endpoints.MapBlazorHub().RequireAuthorization(
    new AuthorizeAttribute 
    {
        AuthenticationSchemes = AzureADDefaults.AuthenticationScheme
    });
```

## Use OpenID Connect (OIDC) v2.0 endpoints

In versions of ASP.NET Core prior to .NET 5, the authentication library and Blazor templates use OpenID Connect (OIDC) v1.0 endpoints. To use a v2.0 endpoint with versions of ASP.NET Core prior to .NET 5, configure the [OpenIdConnectOptions.Authority](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions.authority#microsoft-aspnetcore-builder-openidconnectoptions-authority) option in the [OpenIdConnectOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions):

```
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    }
```

Alternatively, the setting can be made in the app settings (`appsettings.json`) file:

```
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/common/oauth2/v2.0",
    ...
  }
}
```

If tacking on a segment to the authority isn't appropriate for the app's OIDC provider, such as with non-ME-ID providers, set the [Authority](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions.authority#microsoft-aspnetcore-builder-openidconnectoptions-authority) property directly. Either set the property in [OpenIdConnectOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions) or in the app settings file with the [Authority](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions.authority#microsoft-aspnetcore-builder-openidconnectoptions-authority) key.

### Code changes

* The list of claims in the ID token changes for v2.0 endpoints. Microsoft documentation on the changes has been retired, but guidance on the claims in an ID token is available in the [ID token claims reference](/en-us/entra/identity-platform/id-token-claims-reference).

* Since resources are specified in scope URIs for v2.0 endpoints, remove the [OpenIdConnectOptions.Resource](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions.resource#microsoft-aspnetcore-builder-openidconnectoptions-resource) property setting in [OpenIdConnectOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions):

  ```
  services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => 
      {
          ...
          options.Resource = "...";    // REMOVE THIS LINE
          ...
      }
  ```

### App ID URI

* When using v2.0 endpoints, APIs define an *`App ID URI`*, which is meant to represent a unique identifier for the API.
* All scopes include the App ID URI as a prefix, and v2.0 endpoints emit access tokens with the App ID URI as the audience.
* When using V2.0 endpoints, the client ID configured in the Server API changes from the API Application ID (Client ID) to the App ID URI.

`appsettings.json`:

```
{
  "AzureAd": {
    ...
    "ClientId": "https://{TENANT}.onmicrosoft.com/{PROJECT NAME}"
    ...
  }
}
```

You can find the App ID URI to use in the OIDC provider app registration description.

## Circuit handler to capture users for custom services

Use a [CircuitHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuithandler) to capture a user from the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) and set the user in a service. If you want to update the user, register a callback to [AuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.authenticationstatechanged#microsoft-aspnetcore-components-authorization-authenticationstateprovider-authenticationstatechanged) and enqueue a [Task](/en-us/dotnet/api/system.threading.tasks.task) to obtain the new user and update the service. The following example demonstrates the approach.

In the following example:

* [OnConnectionUpAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuithandler.onconnectionupasync) is called every time the circuit reconnects, setting the user for the lifetime of the connection. Only the [OnConnectionUpAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuithandler.onconnectionupasync) method is required unless you implement updates via a handler for authentication changes (`AuthenticationChanged` in the following example).
* [OnCircuitOpenedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuits.circuithandler.oncircuitopenedasync) is called to attach the authentication changed handler, `AuthenticationChanged`, to update the user.
* The `catch` block of the `UpdateAuthentication` task takes no action on exceptions because there's no way to report the exceptions at this point in code execution. If an exception is thrown from the task, the exception is reported elsewhere in app.

`UserService.cs`:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new(new ClaimsIdentity());

    public ClaimsPrincipal GetUser() => currentUser;

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService) 
        : CircuitHandler, IDisposable
{
    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}
```

```
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;

public class UserService
{
    private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());

    public ClaimsPrincipal GetUser()
    {
        return currentUser;
    }

    internal void SetUser(ClaimsPrincipal user)
    {
        if (currentUser != user)
        {
            currentUser = user;
        }
    }
}

internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
    private readonly AuthenticationStateProvider authenticationStateProvider;
    private readonly UserService userService;

    public UserCircuitHandler(
        AuthenticationStateProvider authenticationStateProvider,
        UserService userService)
    {
        this.authenticationStateProvider = authenticationStateProvider;
        this.userService = userService;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        authenticationStateProvider.AuthenticationStateChanged += 
            AuthenticationChanged;

        return base.OnCircuitOpenedAsync(circuit, cancellationToken);
    }

    private void AuthenticationChanged(Task<AuthenticationState> task)
    {
        _ = UpdateAuthentication(task);

        async Task UpdateAuthentication(Task<AuthenticationState> task)
        {
            try
            {
                var state = await task;
                userService.SetUser(state.User);
            }
            catch
            {
            }
        }
    }

    public override async Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        var state = await authenticationStateProvider.GetAuthenticationStateAsync();
        userService.SetUser(state.User);
    }

    public void Dispose()
    {
        authenticationStateProvider.AuthenticationStateChanged -= 
            AuthenticationChanged;
    }
}
```

In the `Program` file:

```
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
```

In `Startup.ConfigureServices` of `Startup.cs`:

```
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

...

services.AddScoped<UserService>();
services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
```

Use the service in a component to obtain the user:

```
@inject UserService UserService

<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>
```

To set the user in middleware for MVC, Razor Pages, and in other ASP.NET Core scenarios, call `SetUser` on the `UserService` in custom middleware after the authentication middleware runs, or set the user with an [IClaimsTransformation](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation) implementation. The following example adopts the middleware approach.

`UserServiceMiddleware.cs`:

```
public class UserServiceMiddleware
{
    private readonly RequestDelegate next;

    public UserServiceMiddleware(RequestDelegate next)
    {
        this.next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext context, UserService service)
    {
        service.SetUser(context.User);
        await next(context);
    }
}
```

Immediately before the call to `app.MapRazorComponents<App>()` in the `Program` file, call the middleware:

Immediately before the call to `app.MapBlazorHub()` in the `Program` file, call the middleware:

Immediately before the call to `app.MapBlazorHub()` in `Startup.Configure` of `Startup.cs`, call the middleware:

```
app.UseMiddleware<UserServiceMiddleware>();
```

## Access `AuthenticationStateProvider` in outgoing request middleware

[IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) creates [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) instances in a separate dependency injection (DI) scope from the app. If you inject [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) into a derived [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) type, the handler doesn't have access to the current user's authentication state from the Blazor circuit.

Use either of the following approaches to address this scenario:

* [Application scope handler](#application-scope-handler-recommended) (*Recommended*)
* [Circuit activity handler](#circuit-activity-handler)

Note

For general guidance on defining delegating handlers for HTTP requests by [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances created using [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory), see the following sections of [HTTP requests with IHttpClientFactory - ASP.NET Core](../../fundamentals/http-requests?view=aspnetcore-10.0):

* [Outgoing request middleware](../../fundamentals/http-requests?view=aspnetcore-10.0#outgoing-request-middleware)
* [Use DI in outgoing request middleware](../../fundamentals/http-requests?view=aspnetcore-10.0#use-di-in-outgoing-request-middleware)

The examples in the following subsections attach a custom user name header for authenticated users to outgoing requests.

### Application scope handler (*Recommended*)

The approach in this section uses a [keyed service](../../fundamentals/dependency-injection?view=aspnetcore-10.0#keyed-services) to register a custom [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) that wraps the base client with an application scope handler resolved from the current application scope to access [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider).

Overview of the approach:

* Base client configuration: [AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) is called to register a [named client](../call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory) with [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory).
* Keyed registration: A custom `AddApplicationScopeHandler` extension method registers a keyed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) with the same client name.
* Scope-aware handler: The application scope handler is resolved from the current scope, giving it access to [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider).
* Handler caching: The application scope handler uses [IHttpMessageHandlerFactory](/en-us/dotnet/api/system.net.http.ihttpmessagehandlerfactory) to get the cached [HttpMessageHandler](/en-us/dotnet/api/system.net.http.httpmessagehandler), which preserves connection pooling.
* Configuration reuse: The application scope handler applies the same [HttpClientFactoryOptions](/en-us/dotnet/api/microsoft.extensions.http.httpclientfactoryoptions) configuration to its [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) as the base client.

Create the following methods and classes:

* `AddApplicationScopeHandler`: An extension method to add the application scope handler and a keyed [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service to the DI container.
* `ApplicationScopeHandler`: The application scope handler class.
* `AuthenticationStateHandler`: A [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) that attaches a custom user name header for authenticated users to outgoing requests.

`Services/ApplicationScopeHttpClientExtensions.cs`:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;

namespace BlazorSample.Services;

public static class ApplicationScopeHttpClientExtensions
{
    public static readonly HttpRequestOptionsKey<IServiceProvider> ScopeKey = 
        new("ApplicationScope");

    public static IHttpClientBuilder AddApplicationScopeHandler(
        this IHttpClientBuilder builder)
    {
        var name = builder.Name;

        builder.Services.AddTransient<ApplicationScopeHandler>();

        builder.Services.AddKeyedScoped<HttpClient>(name, (sp, key) =>
        {
            var handler = sp.GetRequiredService<ApplicationScopeHandler>();
            handler.InnerHandler = 
                sp.GetRequiredService<IHttpMessageHandlerFactory>()
                .CreateHandler(name);

            var client = new HttpClient(handler, disposeHandler: false);

            var options = 
                sp.GetRequiredService<IOptionsMonitor<HttpClientFactoryOptions>>()
                .Get(name);

            foreach (var action in options.HttpClientActions)
            {
                action(client);
            }

            return client;
        });

        return builder;
    }
}

public class ApplicationScopeHandler(IServiceProvider serviceProvider) 
    : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        request.Options.Set(ApplicationScopeHttpClientExtensions.ScopeKey, 
            serviceProvider);
        return base.SendAsync(request, cancellationToken);
    }
}

public class AuthenticationStateHandler : DelegatingHandler
{
    private ClaimsPrincipal? user;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (user is null)
        {
            if (request.Options.TryGetValue(
                ApplicationScopeHttpClientExtensions.ScopeKey, out var sp))
            {
                var authStateProvider = sp.GetService<AuthenticationStateProvider>();

                if (authStateProvider is not null)
                {
                    user = (await authStateProvider.GetAuthenticationStateAsync())
                        .User;
                }
            }
        }

        if (user?.Identity?.IsAuthenticated)
        {
            request.Headers.TryAddWithoutValidation("X-USER-IDENTITY-NAME", 
                user.Identity.Name);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}
```

The `AuthenticationStateHandler` in the preceding example caches the user for the lifetime of the [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler). To fetch the user's current authentication state for each request, remove the `null` conditional check on the user.

Register the named client in the `Program` file, calling `AddApplicationScopeHandler` to add the application scope handler:

```
builder.Services.AddHttpClient("ExternalApi", client =>
{
    client.BaseAddress = new Uri("{REQUEST URI}");
})
.AddApplicationScopeHandler()
.AddHttpMessageHandler<AuthenticationStateHandler>();
```

The `{REQUEST URI}` placeholder in the preceding example is the request URI (localhost example: `http://localhost:5209`).

Inject the client into components using the keyed service:

```
@using Microsoft.Extensions.DependencyInjection

@code {
    [Inject(Key = "ExternalApi")]
    public HttpClient Http { get; set; } = default!;

    private async Task CallApiAsync()
    {
        var response = await Http.GetAsync("/api/endpoint");
    }
}
```

### Circuit activity handler

The approach in this section uses a [circuit activity handler](../fundamentals/signalr?view=aspnetcore-10.0#monitor-circuit-activity-blazor-server) to access the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider), which is an alternative to the recommended [application scope handler approach](#application-scope-handler-recommended) in the preceding section.

First, implement the `CircuitServicesAccessor` class in the following section of the Blazor dependency injection (DI) article:

[Access server-side Blazor services from a different DI scope](../fundamentals/dependency-injection?view=aspnetcore-10.0#access-server-side-blazor-services-from-a-different-di-scope)

Use the `CircuitServicesAccessor` to access the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) in the [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) implementation.

`AuthenticationStateHandler.cs`:

```
using Microsoft.AspNetCore.Components.Authorization;

public class AuthenticationStateHandler(
    CircuitServicesAccessor circuitServicesAccessor) 
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var authStateProvider = circuitServicesAccessor.Services?
            .GetRequiredService<AuthenticationStateProvider>();

        if (authStateProvider is null)
        {
            throw new Exception("AuthenticationStateProvider not available");
        }

        var authState = await authStateProvider.GetAuthenticationStateAsync();

        var user = authState?.User;

        if (user?.Identity is not null && user.Identity.IsAuthenticated)
        {
            request.Headers.Add("X-USER-IDENTITY-NAME", user.Identity.Name);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}
```

In the `Program` file, register the `AuthenticationStateHandler` and add the handler to the [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) that creates [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances:

```
builder.Services.AddTransient<AuthenticationStateHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<AuthenticationStateHandler>();
```

## Opaque (reference) access token support

The guidance in this section explains how to implement opaque (reference) access token support, which offers the following advantages over JSON Web Tokens (JWTs):

* Strict revocation: Invalidate access tokens at any time before they naturally expire.
* Token size limits: Store a large number of user claims in the token to avoid a prohibitively large JWT.
* Security: Prevent API consumers or third parties from reading access token claims.

Note

The following guidance requires an authentication server that supports opaque (reference) access tokens. Currently, Microsoft Entra doesn't support opaque access token validation. [Keycloak](https://www.keycloak.org/) and [Okta](https://developer.okta.com/code/) issue JWT access tokens by default. The opaque token handler in this section still works against Keycloak and Okta because it relies only on RFC 7662 introspection. "Opaque" in this section describes how the client treats the token rather than how the server mints it. Alternatively, [Duende IdentityServer](https://duendesoftware.com/products/identityserver) can be configured to only issue opaque tokens.

When testing this pattern against Keycloak, the API's introspection client must be a different OIDC client than the one that issued the user's access token. Introspecting a token using the client that minted it returns `{"active": false}` with "`Access token JWT check failed`" in the server's log. This doesn't happen naturally for the following scenario because the Blazor Web App and the Minimal API (`MinimalApiJwt`) are separate clients.

[AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect) supports opaque tokens because it doesn't perform access token validation when configured for Proof Key for Code Exchange (PKCE) authorization code flow. It relies on the ASP.NET Core server's HTTPS backchannel to the OIDC authentication service to obtain the ID token using the authorization code received when the user redirects back to the ASP.NET Core app after signing in. If the app is only required to log a user in with OIDC to get a valid authentication cookie, opaque access tokens are supported without modifying the app.

A failure occurs only when the opaque token acquired by [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect) is passed to another service that attempts to validate it with [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer). Unlike self-contained JWTs, opaque tokens require a request to an authorization server to validate the status and to retrieve the claims. To work around this limitation, either use a third-party API, such as the [Duende Introspection Authentication Handler](https://docs.duendesoftware.com/introspection/), or create a [custom `AuthenticationHandler`](../../security/authentication/?view=aspnetcore-10.0#authentication-handler) to validate the token.

Important

[Duende Software](https://duendesoftware.com/) and [Okta](https://www.okta.com) aren't owned or controlled by Microsoft and might require you to pay a license fee for production use of their services and libraries.

The following [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) and associated configuration and helper code is provided as a general approach, which might require further development to suit a specific authorization server's requirements. The following handler extracts the opaque token from the `Authorization` header for an HTTP call to an authorization server's introspection endpoint and creates an [AuthenticationTicket](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationticket) containing the user's claims.

Calling an authorization server's introspection endpoint requires authentication. The following example relies on setting the client secret for authentication in the request's Authorization header (base64 encoded credentials) using the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) for local development and testing.

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](./?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

In the following handler, the authorization server's introspection endpoint client secret uses the configuration key `Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret`. For production apps, consider using *client assertions*. For more information, see [Confidential client assertions (Microsoft Entra documentation)](/en-us/entra/msal/dotnet/acquiring-tokens/web-apps-apis/confidential-client-assertions).

If the Blazor server project hasn't been initialized for the Secret Manager tool, use a command shell, such as the Developer PowerShell command shell in Visual Studio, to execute the following command. Before executing the command, change the directory with the `cd` command to the server project's directory. The command establishes a user secrets identifier (`<UserSecretsId>` in the server app's project file):

```
dotnet user-secrets init
```

Execute the following command to set the client secret for the authorization server. The `{SECRET}` placeholder is the client secret:

```
dotnet user-secrets set "Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret" "{SECRET}"
```

If using Visual Studio, you can confirm the secret is set by right-clicking the server project in **Solution Explorer** and selecting **Manage User Secrets**.

`Extensions/HttpRequestExtensions.cs`:

```
namespace MinimalApiJwt.Extensions;

public static class HttpRequestExtensions
{
    public static string? ExtractBearerToken(this HttpRequest request)
    {
        var authorizationHeader = request.Headers.Authorization.ToString();

        if (!string.IsNullOrEmpty(authorizationHeader) && 
            authorizationHeader.StartsWith("Bearer ", 
            StringComparison.OrdinalIgnoreCase))
        {
            var token = authorizationHeader["Bearer ".Length..].Trim();

            if (!string.IsNullOrEmpty(token))
            {
                return token;
            }
        }

        return null;
    }
}
```

`Authentication/OpaqueTokenAuthenticationOptions.cs`:

```
using Microsoft.AspNetCore.Authentication;

namespace MinimalApiJwt.Authentication;

public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions
{
    public const string DefaultScheme = "OpaqueTokenAuthentication";
    public string? IntrospectionEndpoint { get; set; }
    public string? ClientId { get; set; }
    public string? ClientSecret { get; set; }
}
```

The following handler attempts to validate an opaque (reference) access token. An HTTP call is made to the authorization server's introspection endpoint with the token and the API's credentials. The response is processed to determine if the token is valid:

* If the token is valid, an [AuthenticationTicket](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationticket) is created containing the user's claims.
* If the token is invalid, a failed authorization result is returned.

The handler's options (`Options`) is an instance of `OpaqueTokenAuthenticationOptions` provided by the [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1) base type, which is configured in the app's `Program` file with the authorization server's introspection endpoint and the API's client ID. The API's client secret is provided by the Secret Manager tool during development.

`IOptionsMonitor<OpaqueTokenAuthenticationOptions>` (`optionsMonitor`) isn't used directly by the handler, but it could be used to support dynamic configuration changes at runtime.

For the request's content in [FormUrlEncodedContent](/en-us/dotnet/api/system.net.http.formurlencodedcontent), some servers require a token type hint (`token_type_hint`). For example, the required value might be `access_token`. See your authentication server's documentation for details.

`Authentication/OpaqueTokenAuthenticationHandler.cs`:

```
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using MinimalApiJwt.Extensions;

namespace MinimalApiJwt.Authentication;

public class OpaqueTokenAuthenticationHandler(
    IOptionsMonitor<OpaqueTokenAuthenticationOptions> optionsMonitor,
    ILoggerFactory logger,
    UrlEncoder encoder,
    IHttpClientFactory httpClientFactory)
    : AuthenticationHandler<OpaqueTokenAuthenticationOptions>(optionsMonitor, 
        logger, encoder)
{
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var opaqueToken = Request.ExtractBearerToken();

        if (opaqueToken is null)
        {
            var failedResult = AuthenticateResult.Fail(
                "Bearer token not found in Authorization header.");
            return failedResult;
        }

        var introspectionUri = Options.IntrospectionEndpoint;
        var clientId = Options.ClientId;
        var clientSecret = Options.ClientSecret;

        if (string.IsNullOrWhiteSpace(introspectionUri) ||
            string.IsNullOrWhiteSpace(clientId) ||
            string.IsNullOrWhiteSpace(clientSecret))
        {
            var failedResult = AuthenticateResult.Fail(
                "Opaque token authentication isn't fully configured.");
            return failedResult;
        }

        using var client = httpClientFactory.CreateClient();

        // Set the Authorization header (base64 encoded credentials)
        var authString = Convert.ToBase64String(
            System.Text.Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"));
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Basic", authString);

        // Prepare the form-encoded body containing the token
        var content = new FormUrlEncodedContent(
        [
            new KeyValuePair<string, string>("token", opaqueToken)
        ]);

        // Post to the introspection endpoint
        var response = await client.PostAsync(introspectionUri, content);

        if (!response.IsSuccessStatusCode)
        {
            var failedResult = AuthenticateResult.Fail(
                "Introspection endpoint failure.");

            return failedResult;
        }

        // Parse the JSON response
        var responseString = await response.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(responseString);

        // The 'active' property determines if the token is valid and not expired
        var tokenIsValid = 
            doc.RootElement.TryGetProperty("active", out var activeProperty) &&
            activeProperty.ValueKind == JsonValueKind.True;

        if (tokenIsValid)
        {
            // Map standard introspection response fields onto claims.
            // Field names below match what Keycloak, Duende IdentityServer,
            // Auth0, and Okta return; adjust the role source for your provider.
            var claims = new List<Claim>();

            string? Get(string name) =>
                doc.RootElement.TryGetProperty(name, out var v) &&
                v.ValueKind == JsonValueKind.String ? v.GetString() : null;

            var sub = Get("sub");
            var username = Get("preferred_username") ?? Get("username") ?? sub;

            if (sub is not null) claims.Add(new Claim(ClaimTypes.NameIdentifier, sub));
            if (username is not null) claims.Add(new Claim(ClaimTypes.Name, username));
            if (Get("email") is { } email) claims.Add(new Claim(ClaimTypes.Email, email));
            if ((Get("client_id") ?? Get("azp")) is { } cid)
                claims.Add(new Claim("client_id", cid));
            if (Get("scope") is { } scope)
                foreach (var s in scope.Split(' ', StringSplitOptions.RemoveEmptyEntries))
                    claims.Add(new Claim("scope", s));

            // Keycloak surfaces realm roles under realm_access.roles.
            // Duende/IdentityServer uses a flat "role" claim; Auth0 uses a
            // configurable custom claim. Adjust for your authorization server.
            if (doc.RootElement.TryGetProperty("realm_access", out var ra) &&
                ra.ValueKind == JsonValueKind.Object &&
                ra.TryGetProperty("roles", out var roles) &&
                roles.ValueKind == JsonValueKind.Array)
            {
                foreach (var r in roles.EnumerateArray())
                    if (r.ValueKind == JsonValueKind.String)
                        claims.Add(new Claim(ClaimTypes.Role, r.GetString()!));
            }

            var identity = new ClaimsIdentity(claims,
                OpaqueTokenAuthenticationOptions.DefaultScheme,
                nameType: ClaimTypes.Name,
                roleType: ClaimTypes.Role);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal,
                OpaqueTokenAuthenticationOptions.DefaultScheme);

            var result = AuthenticateResult.Success(ticket);

            return result;
        }
        else
        {
            var failedResult = AuthenticateResult.Fail("Bearer token invalid.");

            return failedResult;
        }
    }
}
```

Note

The preceding approach can be further improved by using the OpenID Connect discovery endpoint and adding a cache for the client's [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) introspection requests.

In the `Program` file:

```
using MinimalApiJwt.Authentication;

...

builder.Services.AddHttpClient();
builder.Services.AddAuthentication()
    .AddScheme<OpaqueTokenAuthenticationOptions, OpaqueTokenAuthenticationHandler>(
        OpaqueTokenAuthenticationOptions.DefaultScheme,
        options =>
        {
            options.IntrospectionEndpoint = "{AUTH SERVER INTROSPECTION URI}";
            options.ClientId = "{API CLIENT ID}";
            options.ClientSecret = 
                builder.Configuration[
                    "Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret"];
        });
```

The preceding example's placeholders:

* `{AUTH SERVER INTROSPECTION URI}`: Authentication server's introspection URI
* `{API CLIENT ID}`: API client ID

Values for the authentication server introspection URI (`{AUTH SERVER INTROSPECTION URI}`) and the API client ID (`{API CLIENT ID}`) can be supplied from app settings or any other configuration source.

Tokens are typically invalidated on a logout event using the revocation endpoint. The following example is a starting point for further development:

```
app.MapPost("/logout", 
    async ([FromForm] string? returnUrl, HttpContext context, 
    IHttpClientFactory httpClientFactory) =>
{
    var accessToken = await context.GetTokenAsync("access_token");

    if (!string.IsNullOrEmpty(accessToken))
    {
        // Prepare the revocation request (RFC 7009)
        var content = 
            new FormUrlEncodedContent(new Dictionary<string, string>
            {
                { "token", accessToken },
                { "token_type_hint", "access_token" },
                { "client_id", "{API CLIENT ID}" },
                { "client_secret", "{CLIENT SECRET}" }
            });

        // POST to the revocation endpoint
        using var client = httpClientFactory.CreateClient();
        await client.PostAsync("{AUTH SERVER TOKEN REVOCATION URI}", content);
    }

    return TypedResults.SignOut(new AuthenticationProperties { RedirectUri = "{REDIRECT URI}" }, 
        [CookieAuthenticationDefaults.AuthenticationScheme]);
});
```

The preceding example's placeholders:

* `{AUTH SERVER TOKEN REVOCATION URI}`: The authentication server's token revocation URI.
* `{API CLIENT ID}`: The API client ID.
* `{CLIENT SECRET}`: The client secret obtained securely.
* `{REDIRECT URI}`: The redirect URI.

In [Duende IdentityServer](https://duendesoftware.com/products/identityserver), tokens are revoked automatically by setting the `CoordinateLifetimeWithUserSession` client configuration property to `true`, which automatically cleans up associated tokens when a session ends. For more information, see [Session Cleanup and Logout (Duende documentation)](https://docs.duendesoftware.com/identityserver/ui/logout/session-cleanup/).

Built-in opaque access token support is under consideration for a future release of .NET. For more information, see [Opaque - reference token validation (`dotnet/aspnetcore` #46026)](https://github.com/dotnet/aspnetcore/issues/46026).

## Server-side Blazor app authorization patterns

*For patterns that apply to Blazor WebAssembly apps, see [Secure ASP.NET Core Blazor WebAssembly](webassembly/?view=aspnetcore-10.0#blazor-webassembly-authorization-patterns).*

Server-side Blazor apps (Blazor Web Apps, Blazor Server apps) usually adopt **either** of the following approaches to require authorization:

* The app sets an authorization fallback policy that requires authorization globally across the app and applies the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) to resources (for example, Razor components, static assets) that don't require an authenticated user. For more information, see the [Global authorization via a fallback authorization policy](#global-authorization-via-a-fallback-authorization-policy) section.
* Instead of requiring global authorization for resources, the app applies the [`[Authorize]` attribute](./?view=aspnetcore-10.0#authorize-attribute) to resources that require an authorized user. For more information, see the [Local authorization via `[Authorize]` attributes](#local-authorization-via-authorize-attributes) section.

### Global authorization via a fallback authorization policy

The following demonstration code can be used with the [`BlazorWebAppAuthorization` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/BlazorWebAppAuthorization) ([how to download](../../overview?view=aspnetcore-10.0#how-to-download-a-sample)).

Set the [AuthorizationOptions.FallbackPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.fallbackpolicy#microsoft-aspnetcore-authorization-authorizationoptions-fallbackpolicy) to a policy with [RequireAuthenticatedUser](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireauthenticateduser). The fallback policy is selected when no policy is produced from authorization metadata. For complete policy selection rules, see [Policy-based authorization in ASP.NET Core](../../security/authorization/policies?view=aspnetcore-10.0#default-and-fallback-policies).

```
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
```

```
services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
});
```

The framework's [AuthorizationOptions.DefaultPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.defaultpolicy) requires an authenticated user. Unless the app uses a [custom policy provider](../../security/authorization/custom-authorization-policy-providers?view=aspnetcore-10.0) with a custom default policy, assigning the framework's default policy (`options.DefaultPolicy`), as shown in the preceding example, is equivalent to using the following code:

```
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});
```

```
services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});
```

The app requires an authenticated user for any resource where no specific policy is set.

If the app's security specification doesn't call for protecting static assets, call [AuthorizationEndpointConventionBuilderExtensions.AllowAnonymous](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.allowanonymous) on [MapStaticAssets](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticassetsendpointroutebuilderextensions.mapstaticassets):

```
app.MapStaticAssets().AllowAnonymous();
```

To alternatively allow anonymous access for specific paths, apply the [AllowAnonymousAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) to the route pattern inside the endpoint convention lambda of [StaticAssetsEndpointConventionBuilder.Add](/en-us/dotnet/api/microsoft.aspnetcore.staticassets.staticassetsendpointconventionbuilder.add).

Important

When only authorizing specific endpoints for anonymous access, the [Blazor script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) and other Blazor static assets, such as stylesheets, scripts, and modules, must be taken into consideration. If public Razor component pages require the assets to render and function correctly, the assets must be made available anonymously as well because they're requested separately via Map Static Assets routing endpoint conventions or static files middleware.

Place static assets for anonymous access into a single folder. In the following example, endpoint routes with the `/public/` path segment are served anonymously:

```
app.MapStaticAssets()
    .Add(endpointBuilder => 
    {
        if (endpointBuilder is RouteEndpointBuilder routeBuilder &&
            routeBuilder.RoutePattern.RawText?.Contains(
            "/public/", StringComparison.OrdinalIgnoreCase) == true)
        {
            routeBuilder.Metadata.Add(new AllowAnonymousAttribute());
        }
    });
```

The next example demonstrates anonymously serving the uncompressed Blazor script (`_framework/blazor.web.{FINGERPRINT}.js`, where the `{FINGERPRINT}` placeholder is the file's fingerprint):

```
// using System.Text.RegularExpressions;

var regex = new Regex(
    @"^_framework/blazor\.web\.[a-z0-9]{10}\.js$", RegexOptions.Compiled);

app.MapStaticAssets()
    .Add(endpointBuilder =>
    {
        if (endpointBuilder is RouteEndpointBuilder routeBuilder &&
            regex.IsMatch(routeBuilder.RoutePattern.RawText ?? string.Empty))
        {
            routeBuilder.Metadata.Add(new AllowAnonymousAttribute());
        }
    });
```

If the app's security specification doesn't call for protecting static assets, place the call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) ***before*** [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization):

```
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();
```

To alternatively allow anonymous access for specific paths, register a separate static files middleware before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) are called. A second call to [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) after authorization pipeline processing only serves other static assets if the user is authorized.

Important

When only authorizing specific endpoints for anonymous access, the [Blazor script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) and other Blazor static assets, such as stylesheets, scripts, and modules, must be taken into consideration. If public Razor component pages require the assets to render and function correctly, the assets must be made available anonymously as well because they're requested separately via static files middleware.

In the following example, static assets in the app's `wwwroot/public` folder are served anonymously:

```
app.UseStaticFiles(new StaticFileOptions {
    FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(
        System.IO.Path.Combine(builder.Environment.WebRootPath, "public")),
    RequestPath = "/public"
});

app.UseAuthentication();
app.UseAuthorization();

app.UseStaticFiles();
```

Use an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive for the [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization) namespace with an [`@attribute`](../../mvc/views/razor?view=aspnetcore-10.0#attribute) directive for the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute) to permit anonymous access to individual components. In the following example, the `Home` component sets the attribute.

At the top of `Components/Pages/Home.razor`:

```
@page "/"
@using Microsoft.AspNetCore.Authorization
@attribute [AllowAnonymous]
```

Often, it's convenient to apply authorization to an entire folder of components. In the following example, a user account pages' imports file sets the [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute), so users can anonymously reach the app's sign-in, sign-out, access denied, and invalid user pages in the `Components/Account/Pages` folder.

In `Components/Account/Pages/_Imports.razor`:

```
@using Microsoft.AspNetCore.Authorization
@attribute [AllowAnonymous]
```

If the app uses one or more endpoint convention builder instances to provide additional endpoints, such as for Identity components, the endpoint builder's method call chains a call to [AuthorizationEndpointConventionBuilderExtensions.AllowAnonymous](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.allowanonymous). The following example maps additional Identity endpoints by calling `MapAdditionalIdentityEndpoints`, which returns an [IEndpointConventionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iendpointconventionbuilder):

```
app.MapAdditionalIdentityEndpoints().AllowAnonymous();
```

Note

For an example of the preceding `MapAdditionalIdentityEndpoints` method, see [`IdentityComponentsEndpointRouteBuilderExtensions`](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/security/authorization/BlazorWebAppAuthorization/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs) in the [`BlazorWebAppAuthorization` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/BlazorWebAppAuthorization).

### Local authorization via `[Authorize]` attributes

Apply [`[Authorize]` attributes](./?view=aspnetcore-10.0#authorize-attribute) ([API documentation](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute)) to Razor components using ***either*** of the following approaches:

* In the app's imports file, add an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive for the [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization) namespace with an [`@attribute`](../../mvc/views/razor?view=aspnetcore-10.0#attribute) directive for the [`[Authorize]` attribute](./?view=aspnetcore-10.0#authorize-attribute).

  `_Imports.razor`:

  ```
  @using Microsoft.AspNetCore.Authorization
  @attribute [Authorize]
  ```

  Imports files can be applied at any level of a folder hierarchy to apply an [`[Authorize]` attribute](./?view=aspnetcore-10.0#authorize-attribute) for that folder's components and its subfolders.

* Add the [`[Authorize]` attribute](./?view=aspnetcore-10.0#authorize-attribute) to each Razor component that requires authorization under the [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive with an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive for the [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization) namespace:

  ```
  @using Microsoft.AspNetCore.Authorization
  @attribute [Authorize]
  ```

  The [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) directive for the [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization) namespace in the preceding example can be applied broadly to the app's components by placing it into the app's imports file (`_Imports.razor`) instead of in individual components.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/scaffold-identity.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Scaffold Identity in ASP.NET Core projects

By [Rick Anderson](https://twitter.com/RickAndMSFT)

This article shows how to migrate a database and complete an Identity scaffolding update.

## Blazor Identity scaffolding

ASP.NET Core Identity scaffolding adds [ASP.NET Core Identity](identity?view=aspnetcore-10.0) to Blazor Web Apps and Blazor Server apps. After the scaffolder adds the Identity Razor components to the app, you can customize the components to suit your application requirements.

Although the scaffolder generates the necessary C# code to scaffold Identity into the app, you must update the project database with an Entity Framework Core database migration to complete the process. This article describes the steps for migrating a database.

After running the Identity scaffolder, inspect the changes. Use GitHub or another source control system that shows file changes with a revert changes feature.

Services are required when using [two-factor authentication (2FA)](../../blazor/security/qrcodes-for-authenticator-apps?view=aspnetcore-10.0), [account confirmation and password recovery](../../blazor/security/account-confirmation-and-password-recovery?view=aspnetcore-10.0), and other security features with Identity. Services or service stubs aren't generated when scaffolding Identity. Services to enable these features must be added manually.

## Razor Pages and MVC Identity scaffolding

ASP.NET Core provides [ASP.NET Core Identity](identity?view=aspnetcore-10.0) as a [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0). Applications that include Identity can apply the scaffolder to selectively add the source code contained in the Identity RCL. You might want to generate source code so you can modify the code and change the behavior. For example, you could instruct the scaffolder to generate the code used in registration. Customized Identity code overrides the default implementation provided by the Identity RCL. To gain full control of the UI and not use the default RCL, see the [Create full Identity UI source](#create-full-identity-ui-source) section.

Applications that do **not** include authentication can apply the scaffolder to add the RCL Identity package. You have the option of selecting Identity code to generate.

Although the scaffolder generates most of the necessary code, you need to update your project to complete the process. This article shows the steps needed to complete an Identity scaffolding update. After running the Identity scaffolder, inspect the changes. Use a source control system that shows file differences and allows you to back out of changes.

Services are required when using [2FA](identity-enable-qrcodes?view=aspnetcore-10.0), [account confirmation and password recovery](accconfirm?view=aspnetcore-10.0), and other security features with Identity. Services or service stubs aren't generated when scaffolding Identity. Services to enable these features must be added manually. For example, see [Configure an email provider](accconfirm?view=aspnetcore-10.0#configure-an-email-provider) so the app can require email confirmation.

Typically, apps created with individual accounts should ***not*** create a new data context.

## Scaffold Identity into a Blazor project

The scaffolding procedures described in this section apply to Blazor Web Apps and Blazor Server apps.

Run the Identity scaffolder:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Blazor Identity**, and then select **Add**.

4. In the **Add Blazor Identity** dialog:

   1. Select the plus icon (**+**) and add the database context class, **DbContext class**.

   2. Select the database provider (**Database provider**), which defaults to SQL Server.

   3. Select the plus icon (**+**) and add the user class, **User class**.

   4. Select **Add**.

To add the required NuGet packages and tools, run the following .NET CLI commands in a command shell opened to the project's root folder.

* Paste the following commands at the prompt (`>`) of the command shell.
* If you paste multiple commands, a warning indicates execution of multiple commands. Select the **Paste anyway** option.
* When you paste multiple commands, all of the commands run, except the last one. To run the last command, select `Enter`.

The following commands assume you're using a SQL Server database and the SQL Server Entity Framework Core database provider package ([Microsoft.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer)). If you're using a different database, swap the Entity Framework Core database provider package. The scaffolding tool supports the following providers †:

* SQLServer: [Microsoft.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer)
* SQLite: [Microsoft.EntityFrameworkCore.Sqlite](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite)
* Cosmos: [Microsoft.EntityFrameworkCore.Cosmos](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Cosmos)
* Postgres: [Npgsql.EntityFrameworkCore.PostgreSQL](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL)

† Microsoft owns, maintains, and supports the SQL Server, SQLite, and Azure Cosmos DB database provider packages. [The Npgsql Development Team](https://www.npgsql.org/) owns, maintains, and supports the PostgreSQL database provider package ([npgsql/efcore.pg GitHub repository](https://github.com/npgsql/efcore.pg)).

1. Run the following commands:

   ```
   dotnet tool install --global dotnet-aspnet-codegenerator
   dotnet tool install --global dotnet-ef
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

   Important

   After the first five commands execute, be sure to run the last command by selecting `Enter`.

   These commands add:

   * [Command-line interface (CLI) tools](/en-us/ef/core/cli/dotnet) for Entity Framework Core
   * The [aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0) scaffolding tool
   * Design time tools for Entity Framework Core
   * The SQLite and SQL Server providers with the Entity Framework Core package as a dependency
   * [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) for scaffolding

2. Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

3. In the project folder, run the Identity scaffolder with the options you want. To scaffold Identity with the default Identity UI and the minimum number of files, run the following command:

   ```
   dotnet aspnet-codegenerator identity --useDefaultUI
   ```

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). The following steps explain how to create and apply a migration to the database.

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an Entity Framework Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More action** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

From the project's root folder, run the following .NET CLI command to add a migration. The `{MIGRATION NAME}` placeholder is used to name the migration, such as `CreateIdentitySchema`. Any name can be used, but the convention is to use a name that describes the migration.

1. In a command shell, run the following command to add the migration:

   ```
   dotnet ef migrations add {MIGRATION NAME}
   ```

   Example:

   ```
   dotnet ef migrations add CreateIdentitySchema
   ```

2. After the migration command completes, run the following command to update the database:

   ```
   dotnet ef database update
   ```

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

## Client-side Blazor apps (Standalone Blazor WebAssembly)

Client-side Blazor apps (Standalone Blazor WebAssembly) use their own Identity UI approaches and can't use ASP.NET Core Identity scaffolding.

For more information, see the [Blazor Security and Identity articles](../../blazor/security/?view=aspnetcore-10.0).

## Scaffold Identity into a Razor project without existing authorization

Install the [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

***

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a new class for Identity.

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command:

   ```
   dotnet aspnet-codegenerator identity --useDefaultUI
   ```

### Migrations, UseAuthentication, and layout

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). If a migration to generate the Identity schema isn't already created and applied to the database, use the following procedures to create a migration and update the database.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an EF Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

In a command shell, run the following commands:

```
dotnet ef migrations add CreateIdentitySchema
dotnet ef database update
```

If the Identity schema is created but not applied to the database, you only need to run the command to update the database:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

3. Select **Finish** to complete the update. After the operation finishes, select **Close**.

In a command shell, run the [dotnet ef database update](/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#command-line-tools) command:

```
dotnet ef database update
```

You can confirm the application of an Identity schema with the following command. The output of the command includes an "`applied`" column to show which migrations are applied to the database.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

In the Visual Studio **Package Manager Console**, run the [Get-Migration](/en-us/ef/core/managing-schemas/migrations/managing?tabs=vs#listing-migrations) command:

```
Get-Migration
```

If more than one database context exists, specify the context with the `-Context` parameter.

In a command shell, run the [dotnet ef migrations list](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#listing-migrations) command:

```
dotnet ef migrations list
```

If more than one database context exists, specify the context with the `--context` parameter.

### Layout changes

As an option, you can add the login partial (`_LoginPartial`) to the layout file:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebRPnoAuth2Auth</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/WebRPnoAuth2Auth.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">WebRPnoAuth2Auth</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
                    </ul>
                    <partial name="_LoginPartial" />
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2021 - WebRPnoAuth2Auth - <a asp-area="" asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

## Scaffold Identity into a Razor project with authorization

Install the [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

***

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a class (for example, `Contoso.Data.ApplicationDbContext` for a company named "Contoso").

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command. Use the correct fully qualified name for your DB context.

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login"
   ```

   PowerShell uses the semicolon (`;`) as a command separator. When using PowerShell, escape any semicolons in the file list or put the file list in double quotes (`""`). For example:

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout"
   ```

   If you run the Identity scaffolder without specifying the `--files` flag or the `--useDefaultUI` flag, all the available Identity UI pages are created in the project.

## Scaffold Identity into an MVC project without existing authorization

Install the [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

***

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a new class for Identity.

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command:

   ```
   dotnet aspnet-codegenerator identity --useDefaultUI
   ```

Optional: Add the login partial (`_LoginPartial`) to the `Views/Shared/_Layout.cshtml` file:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebRPnoAuth2Auth</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/WebRPnoAuth2Auth.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">WebRPnoAuth2Auth</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
                    </ul>
                    <partial name="_LoginPartial" />
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2021 - WebRPnoAuth2Auth - <a asp-area="" asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). If a migration to generate the Identity schema isn't already created and applied to the database, use the following procedures to create a migration and update the database.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an EF Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

In a command shell, run the following commands:

```
dotnet ef migrations add CreateIdentitySchema
dotnet ef database update
```

If the Identity schema is created but not applied to the database, you only need to run the command to update the database:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

3. Select **Finish** to complete the update. After the operation finishes, select **Close**.

In a command shell, run the [dotnet ef database update](/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#command-line-tools) command:

```
dotnet ef database update
```

You can confirm the application of an Identity schema with the following command. The output of the command includes an "`applied`" column to show which migrations are applied to the database.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

In the Visual Studio **Package Manager Console**, run the [Get-Migration](/en-us/ef/core/managing-schemas/migrations/managing?tabs=vs#listing-migrations) command:

```
Get-Migration
```

If more than one database context exists, specify the context with the `-Context` parameter.

In a command shell, run the [dotnet ef migrations list](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#listing-migrations) command:

```
dotnet ef migrations list
```

If more than one database context exists, specify the context with the `--context` parameter.

Add `MapRazorPages` to the *Program.cs* file as shown in the following highlighted code:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMVCauth.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();
```

## Scaffold Identity into an MVC project with authorization

Install the [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

***

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a class (for example, `Contoso.Data.ApplicationDbContext` for a company named "Contoso").

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command. Use the correct fully qualified name for your DB context.

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login"
   ```

   PowerShell uses the semicolon (`;`) as a command separator. When using PowerShell, escape any semicolons in the file list or put the file list in double quotes (`""`). For example:

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout"
   ```

   If you run the Identity scaffolder without specifying the `--files` flag or the `--useDefaultUI` flag, all the available Identity UI pages are created in the project.

## Create full Identity UI source

To maintain full control of the Identity UI, run the Identity scaffolder and select **Override all files**.

## Password configuration

If [PasswordOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions) are configured in `Startup.ConfigureServices`, the [StringLengthAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute) configuration might be required for the `Password` property in scaffolded Identity pages. The `InputModel` and its `Password` properties are located in the following files:

* *Areas/Identity/Pages/Account/Register.cshtml.cs*
* *Areas/Identity/Pages/Account/ResetPassword.cshtml.cs*

## Disable a page

This section describes how to disable the register page, but you can use the same approach to disable any page.

To disable user registration:

1. Scaffold Identity. Include `Account.Register`, `Account.Login`, and `Account.RegisterConfirmation`. For example:

   ```
   dotnet aspnet-codegenerator identity -dc RPauth.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.RegisterConfirmation"
   ```

2. Update the *Areas/Identity/Pages/Account/Register.cshtml.cs* file so users can't register from this endpoint:

   ```
   public class RegisterModel : PageModel
   {
       public IActionResult OnGet()
       {
           return RedirectToPage("Login");
       }

       public IActionResult OnPost()
       {
           return RedirectToPage("Login");
       }
   }
   ```

3. Update the *Areas/Identity/Pages/Account/Register.cshtml* file to be consistent with the preceding changes:

   ```
   @page
   @model RegisterModel
   @{
       ViewData["Title"] = "Go to Login";
   }

   <h1>@ViewData["Title"]</h1>

   <li class="nav-item">
       <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
   </li>
   ```

4. Comment out or remove the registration link from the *Areas/Identity/Pages/Account/Login.cshtml* file:

   ```
   @*
   <p>
       <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
   </p>
   *@
   ```

5. Update the `Areas/Identity/Pages/Account/RegisterConfirmation` page.

   * Remove the code and links from the *.cshtml* file.

   * Remove the confirmation code from the `PageModel` implementation:

   ```
   [AllowAnonymous]
   public class RegisterConfirmationModel : PageModel
   {
        public IActionResult OnGet()
        {  
            return Page();
        }
   }
   ```

### Use another app to add users

Provide a mechanism to add users outside the web app. Options to add users include:

* A dedicated admin web app
* A console app

The following code outlines one approach for adding users:

* Read the list of users into memory.
* Generate a strong unique password for each user.
* Add the user to the Identity database.
* Notify the user about changing their password.

```
public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                var context = services.GetRequiredService<AppDbCntx>();
                context.Database.Migrate();

                var config = host.Services.GetRequiredService<IConfiguration>();
                var userList = config.GetSection("userList").Get<List<string>>();

                SeedData.Initialize(services, userList).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred adding users.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

The following code outlines adding a user:

```

public static async Task Initialize(IServiceProvider serviceProvider,
                                    List<string> userList)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    foreach (var userName in userList)
    {
        var userPassword = GenerateSecurePassword();
        var userId = await EnsureUser(userManager, userName, userPassword);

        NotifyUser(userName, userPassword);
    }
}

private static async Task<string> EnsureUser(UserManager<IdentityUser> userManager,
                                             string userName, string userPassword)
{
    var user = await userManager.FindByNameAsync(userName);

    if (user == null)
    {
        user = new IdentityUser(userName)
        {
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, userPassword);
    }

    return user.Id;
}
```

A similar approach can be followed for production scenarios.

## Prevent publish of static Identity assets

To prevent publishing static Identity assets to the web root, see [Introduction to Identity on ASP.NET Core](identity?view=aspnetcore-10.0#prevent-publish-of-static-identity-assets).

ASP.NET Core provides [ASP.NET Core Identity](identity?view=aspnetcore-10.0) as a [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0). Applications that include Identity can apply the scaffolder to selectively add the source code contained in the Identity RCL. You might want to generate source code so you can modify the code and change the behavior. For example, you could instruct the scaffolder to generate the code used in registration. Generated code takes precedence over the same code in the Identity RCL. To gain full control of the UI and not use the default RCL, see the section [Create full Identity UI source](#create-full-identity-ui-source).

Applications that do **not** include authentication can apply the scaffolder to add the RCL Identity package. You have the option of selecting Identity code to be generated.

Although the scaffolder generates most of the necessary code, you need to update your project to complete the process. This document explains the steps needed to complete an Identity scaffolding update.

We recommend using a source control system that shows file differences and allows you to back out of changes. Inspect the changes after running the Identity scaffolder.

Services are required when using [Two Factor Authentication](identity-enable-qrcodes?view=aspnetcore-10.0), [Account confirmation and password recovery](accconfirm?view=aspnetcore-10.0), and other security features with Identity. Services or service stubs aren't generated when scaffolding Identity. Services to enable these features must be added manually. For example, see [Require Email Confirmation](accconfirm?view=aspnetcore-10.0#require-email-confirmation).

Typically, apps created with individual accounts should ***not*** create a new data context.

[]()

## Scaffold Identity into a Razor project without existing authorization

Install the [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

***

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a new class for Identity.

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command:

   ```
   dotnet aspnet-codegenerator identity --useDefaultUI
   ```

### Migrations, UseAuthentication, and layout

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). If a migration to generate the Identity schema isn't already created and applied to the database, use the following procedures to create a migration and update the database.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an EF Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

In a command shell, run the following commands:

```
dotnet ef migrations add CreateIdentitySchema
dotnet ef database update
```

If the Identity schema is created but not applied to the database, you only need to run the command to update the database:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

3. Select **Finish** to complete the update. After the operation finishes, select **Close**.

In a command shell, run the [dotnet ef database update](/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#command-line-tools) command:

```
dotnet ef database update
```

You can confirm the application of an Identity schema with the following command. The output of the command includes an "`applied`" column to show which migrations are applied to the database.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

In the Visual Studio **Package Manager Console**, run the [Get-Migration](/en-us/ef/core/managing-schemas/migrations/managing?tabs=vs#listing-migrations) command:

```
Get-Migration
```

If more than one database context exists, specify the context with the `-Context` parameter.

In a command shell, run the [dotnet ef migrations list](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#listing-migrations) command:

```
dotnet ef migrations list
```

If more than one database context exists, specify the context with the `--context` parameter.

### Layout changes

Optional: Add the login partial (`_LoginPartial`) to the layout file:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebRPnoAuth2Auth</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/WebRPnoAuth2Auth.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">WebRPnoAuth2Auth</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
                    </ul>
                    <partial name="_LoginPartial" />
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2021 - WebRPnoAuth2Auth - <a asp-area="" asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

## Scaffold Identity into a Razor project with authorization

Install the [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

***

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a class (for example, `Contoso.Data.ApplicationDbContext` for a company named "Contoso").

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command. Use the correct fully qualified name for your DB context.

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login"
   ```

   PowerShell uses the semicolon (`;`) as a command separator. When using PowerShell, escape any semicolons in the file list or put the file list in double quotes (`""`). For example:

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout"
   ```

   If you run the Identity scaffolder without specifying the `--files` flag or the `--useDefaultUI` flag, all the available Identity UI pages are created in the project.

## Scaffold Identity into an MVC project without existing authorization

Install the [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

***

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a new class for Identity.

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command:

   ```
   dotnet aspnet-codegenerator identity --useDefaultUI
   ```

Optional: Add the login partial (`_LoginPartial`) to the `Views/Shared/_Layout.cshtml` file:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebRPnoAuth2Auth</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/WebRPnoAuth2Auth.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">WebRPnoAuth2Auth</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
                    </ul>
                    <partial name="_LoginPartial" />
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2021 - WebRPnoAuth2Auth - <a asp-area="" asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). If a migration to generate the Identity schema isn't already created and applied to the database, use the following procedures to create a migration and update the database.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an EF Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

In a command shell, run the following commands:

```
dotnet ef migrations add CreateIdentitySchema
dotnet ef database update
```

If the Identity schema is created but not applied to the database, you only need to run the command to update the database:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

3. Select **Finish** to complete the update. After the operation finishes, select **Close**.

In a command shell, run the [dotnet ef database update](/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#command-line-tools) command:

```
dotnet ef database update
```

You can confirm the application of an Identity schema with the following command. The output of the command includes an "`applied`" column to show which migrations are applied to the database.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

In the Visual Studio **Package Manager Console**, run the [Get-Migration](/en-us/ef/core/managing-schemas/migrations/managing?tabs=vs#listing-migrations) command:

```
Get-Migration
```

If more than one database context exists, specify the context with the `-Context` parameter.

In a command shell, run the [dotnet ef migrations list](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#listing-migrations) command:

```
dotnet ef migrations list
```

If more than one database context exists, specify the context with the `--context` parameter.

Add `MapRazorPages` to `Program.cs` as shown in the following highlighted code:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMVCauth.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();
```

## Scaffold Identity into an MVC project with authorization

Install the [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

***

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a class (for example, `Contoso.Data.ApplicationDbContext` for a company named "Contoso").

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command. Use the correct fully qualified name for your DB context.

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login"
   ```

   PowerShell uses the semicolon (`;`) as a command separator. When using PowerShell, escape any semicolons in the file list or put the file list in double quotes (`""`). For example:

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout"
   ```

   If you run the Identity scaffolder without specifying the `--files` flag or the `--useDefaultUI` flag, all the available Identity UI pages are created in the project.

## Scaffold Identity into a server-side Blazor app with authorization

Install the [Microsoft.VisualStudio.Web.CodeGeneration.Design](https://www.nuget.org/packages/Microsoft.VisualStudio.Web.CodeGeneration.Design) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

***

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a class (for example, `Contoso.Data.ApplicationDbContext` for a company named "Contoso").

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command. Use the correct fully qualified name for your DB context.

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login"
   ```

   PowerShell uses the semicolon (`;`) as a command separator. When using PowerShell, escape any semicolons in the file list or put the file list in double quotes (`""`). For example:

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout"
   ```

   If you run the Identity scaffolder without specifying the `--files` flag or the `--useDefaultUI` flag, all the available Identity UI pages are created in the project.

### Migrations

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). If a migration to generate the Identity schema isn't already created and applied to the database, use the following procedures to create a migration and update the database.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an EF Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

In a command shell, run the following commands:

```
dotnet ef migrations add CreateIdentitySchema
dotnet ef database update
```

If the Identity schema is created but not applied to the database, you only need to run the command to update the database:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

3. Select **Finish** to complete the update. After the operation finishes, select **Close**.

In a command shell, run the [dotnet ef database update](/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#command-line-tools) command:

```
dotnet ef database update
```

You can confirm the application of an Identity schema with the following command. The output of the command includes an "`applied`" column to show which migrations are applied to the database.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

In the Visual Studio **Package Manager Console**, run the [Get-Migration](/en-us/ef/core/managing-schemas/migrations/managing?tabs=vs#listing-migrations) command:

```
Get-Migration
```

If more than one database context exists, specify the context with the `-Context` parameter.

In a command shell, run the [dotnet ef migrations list](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#listing-migrations) command:

```
dotnet ef migrations list
```

If more than one database context exists, specify the context with the `--context` parameter.

### Style authentication endpoints

Because server-side Blazor apps use Razor Pages Identity pages, the styling of the UI changes when a visitor navigates between Identity pages and components. You have two options to address the incongruous styles:

* [Custom Identity components](#custom-identity-components)
* [Use a custom layout with Blazor app styles](#use-a-custom-layout-with-blazor-app-styles)

#### Custom Identity components

ASP.NET Core Identity is designed to work in the context of HTTP request and response communication, which isn't the primary client-server communication model in Blazor apps. ASP.NET Core apps that use ASP.NET Core Identity for user management should use Razor Pages instead of Razor components for Identity-related UI, such as user registration, login, logout, and other user management tasks.

Because [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) and [UserManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1) aren't supported in Razor components, we recommend using web API to manage Identity actions from Razor components via a server-side Identity-enabled ASP.NET Core app. For guidance on creating web APIs for Blazor apps, see [Call a web API from an ASP.NET Core Blazor app](../../blazor/call-web-api?view=aspnetcore-10.0).

An approach to using Razor components for Identity instead of Razor pages is to build your own custom Identity Razor components, but Microsoft doesn't recommend or support the approach. For additional context, explore the following discussions. In the following discussions, code examples in issue comments and code examples cross-linked in non-Microsoft GitHub repositories aren't supported by Microsoft but might be helpful to some developers:

* [Support Custom Login Component when using Identity (dotnet/aspnetcore #13601)](https://github.com/dotnet/aspnetcore/issues/13601)
* [Reiteration on the `SigninManager<T>` not being supported in Razor Components (dotnet/aspnetcore #34095)](https://github.com/dotnet/aspnetcore/issues/34095)
* [There is no info on how to actually implement custom login form for server-side blazor (dotnet/AspNetCore.Docs #16813)](https://github.com/dotnet/AspNetCore.Docs/issues/16813)

For additional assistance when seeking to build custom Identity Razor components or searching for third-party Razor components, we recommend the following resources:

* [Stack Overflow (tag: `blazor`)](https://stackoverflow.com/questions/tagged/blazor) (Public support forum)
* [ASP.NET Core Slack Team](https://join.slack.com/t/aspnetcore/shared_invite/zt-1mv5487zb-EOZxJ1iqb0A0ajowEbxByQ) (Public support chat)
* [Blazor Gitter](https://gitter.im/aspnet/Blazor) (Public support chat)
* [Awesome Blazor](https://github.com/AdrienTorris/awesome-blazor) (Links to community-maintained Blazor resources)

#### Use a custom layout with Blazor app styles

The Identity pages layout and styles can be modified to produce pages that use styles similar to the default Blazor theme. This approach isn't covered by the documentation.

## Client-side Blazor apps

Client-side Blazor apps use their own Identity UI approaches and can't use ASP.NET Core Identity scaffolding. Server-side ASP.NET Core apps of hosted Blazor solutions can follow the Razor Pages/MVC guidance in this article and are configured just like any other type of ASP.NET Core app that supports Identity.

The Blazor framework doesn't include Razor component versions of Identity UI pages. Identity UI Razor components can be custom built or obtained from unsupported third-party sources.

For more information, see the [Blazor Security and Identity articles](../../blazor/security/?view=aspnetcore-10.0).

## Create full Identity UI source

To maintain full control of the Identity UI, run the Identity scaffolder and select **Override all files**.

## Password configuration

If [PasswordOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions) are configured in `Startup.ConfigureServices`, [`[StringLength]` attribute](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute) configuration might be required for the `Password` property in scaffolded Identity pages. `InputModel` `Password` properties are found in the following files:

* `Areas/Identity/Pages/Account/Register.cshtml.cs`
* `Areas/Identity/Pages/Account/ResetPassword.cshtml.cs`

## Disable a page

This section shows how to disable the register page but the approach can be used to disable any page.

To disable user registration:

* Scaffold Identity. Include Account.Register, Account.Login, and Account.RegisterConfirmation. For example:

  ```
  dotnet aspnet-codegenerator identity -dc RPauth.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.RegisterConfirmation"
  ```

* Update `Areas/Identity/Pages/Account/Register.cshtml.cs` so users can't register from this endpoint:

  ```
  public class RegisterModel : PageModel
  {
      public IActionResult OnGet()
      {
          return RedirectToPage("Login");
      }

      public IActionResult OnPost()
      {
          return RedirectToPage("Login");
      }
  }
  ```

* Update `Areas/Identity/Pages/Account/Register.cshtml` to be consistent with the preceding changes:

  ```
  @page
  @model RegisterModel
  @{
      ViewData["Title"] = "Go to Login";
  }

  <h1>@ViewData["Title"]</h1>

  <li class="nav-item">
      <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
  </li>
  ```

* Comment out or remove the registration link from `Areas/Identity/Pages/Account/Login.cshtml`

  ```
  @*
  <p>
      <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
  </p>
  *@
  ```

* Update the `Areas/Identity/Pages/Account/RegisterConfirmation` page.

  * Remove the code and links from the cshtml file.
  * Remove the confirmation code from the `PageModel`:

  ```
  [AllowAnonymous]
    public class RegisterConfirmationModel : PageModel
    {
        public IActionResult OnGet()
        {  
            return Page();
        }
    }
  ```

### Use another app to add users

Provide a mechanism to add users outside the web app. Options to add users include:

* A dedicated admin web app.
* A console app.

The following code outlines one approach to adding users:

* A list of users is read into memory.
* A strong unique password is generated for each user.
* The user is added to the Identity database.
* The user is notified and told to change the password.

```
public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                var context = services.GetRequiredService<AppDbCntx>();
                context.Database.Migrate();

                var config = host.Services.GetRequiredService<IConfiguration>();
                var userList = config.GetSection("userList").Get<List<string>>();

                SeedData.Initialize(services, userList).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred adding users.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

The following code outlines adding a user:

```

public static async Task Initialize(IServiceProvider serviceProvider,
                                    List<string> userList)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    foreach (var userName in userList)
    {
        var userPassword = GenerateSecurePassword();
        var userId = await EnsureUser(userManager, userName, userPassword);

        NotifyUser(userName, userPassword);
    }
}

private static async Task<string> EnsureUser(UserManager<IdentityUser> userManager,
                                             string userName, string userPassword)
{
    var user = await userManager.FindByNameAsync(userName);

    if (user == null)
    {
        user = new IdentityUser(userName)
        {
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, userPassword);
    }

    return user.Id;
}
```

A similar approach can be followed for production scenarios.

## Prevent publish of static Identity assets

To prevent publishing static Identity assets to the web root, see [Introduction to Identity on ASP.NET Core](identity?view=aspnetcore-10.0#prevent-publish-of-static-identity-assets).

ASP.NET Core provides [ASP.NET Core Identity](identity?view=aspnetcore-10.0) as a [Razor class library (RCL)](../../razor-pages/ui-class?view=aspnetcore-10.0). Applications that include Identity can apply the scaffolder to selectively add the source code contained in the Identity RCL. You might want to generate source code so you can modify the code and change the behavior. For example, you could instruct the scaffolder to generate the code used in registration. Generated code takes precedence over the same code in the Identity RCL. To gain full control of the UI and not use the default RCL, see the section [Create full Identity UI source](#create-full-identity-ui-source).

Applications that do **not** include authentication can apply the scaffolder to add the RCL Identity package. You have the option of selecting Identity code to be generated.

Although the scaffolder generates most of the necessary code, you need to update your project to complete the process. This document explains the steps needed to complete an Identity scaffolding update.

We recommend using a source control system that shows file differences and allows you to back out of changes. Inspect the changes after running the Identity scaffolder.

Services are required when using [Two Factor Authentication](identity-enable-qrcodes?view=aspnetcore-10.0), [Account confirmation and password recovery](accconfirm?view=aspnetcore-10.0), and other security features with Identity. Services or service stubs aren't generated when scaffolding Identity. Services to enable these features must be added manually. For example, see [Require Email Confirmation](accconfirm?view=aspnetcore-10.0#require-email-confirmation).

When scaffolding Identity with a new data context into a project with existing individual accounts, open `Startup.ConfigureServices` and remove the calls to:

* `AddDbContext`
* `AddDefaultIdentity`

For example, `AddDbContext` and `AddDefaultIdentity` are commented out in the following code:

```
public void ConfigureServices(IServiceCollection services)
{
    //services.AddDbContext<ApplicationDbContext>(options =>
    //    options.UseSqlServer(
    //        Configuration.GetConnectionString("DefaultConnection")));
    //services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    //    .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddControllersWithViews();
    services.AddRazorPages();
}
```

The preceding code comments out the code that is duplicated in `Areas/Identity/IdentityHostingStartup.cs`

Typically, apps created with individual accounts should ***not*** create a new data context.

## Scaffold Identity into an empty project

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a new class for Identity.

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command:

   ```
   dotnet aspnet-codegenerator identity --useDefaultUI
   ```

Update the `Startup` class with code similar to the following:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
            endpoints.MapRazorPages();
        });
    }
}
```

`UseHsts` is recommended but not required. For more information, see [HTTP Strict Transport Security (HSTS) protocol](../enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol).

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). If a migration to generate the Identity schema isn't already created and applied to the database, use the following procedures to create a migration and update the database.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an EF Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

In a command shell, run the following commands:

```
dotnet ef migrations add CreateIdentitySchema
dotnet ef database update
```

If the Identity schema is created but not applied to the database, you only need to run the command to update the database:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

3. Select **Finish** to complete the update. After the operation finishes, select **Close**.

In a command shell, run the [dotnet ef database update](/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#command-line-tools) command:

```
dotnet ef database update
```

You can confirm the application of an Identity schema with the following command. The output of the command includes an "`applied`" column to show which migrations are applied to the database.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

In the Visual Studio **Package Manager Console**, run the [Get-Migration](/en-us/ef/core/managing-schemas/migrations/managing?tabs=vs#listing-migrations) command:

```
Get-Migration
```

If more than one database context exists, specify the context with the `-Context` parameter.

In a command shell, run the [dotnet ef migrations list](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#listing-migrations) command:

```
dotnet ef migrations list
```

If more than one database context exists, specify the context with the `--context` parameter.

## Scaffold Identity into a Razor project without existing authorization

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a new class for Identity.

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command:

   ```
   dotnet aspnet-codegenerator identity --useDefaultUI
   ```

Identity is configured in `Areas/Identity/IdentityHostingStartup.cs`. For more information, see [`IHostingStartup`](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0).

### Migrations, UseAuthentication, and layout

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). If a migration to generate the Identity schema isn't already created and applied to the database, use the following procedures to create a migration and update the database.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an EF Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

In a command shell, run the following commands:

```
dotnet ef migrations add CreateIdentitySchema
dotnet ef database update
```

If the Identity schema is created but not applied to the database, you only need to run the command to update the database:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

3. Select **Finish** to complete the update. After the operation finishes, select **Close**.

In a command shell, run the [dotnet ef database update](/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#command-line-tools) command:

```
dotnet ef database update
```

You can confirm the application of an Identity schema with the following command. The output of the command includes an "`applied`" column to show which migrations are applied to the database.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

In the Visual Studio **Package Manager Console**, run the [Get-Migration](/en-us/ef/core/managing-schemas/migrations/managing?tabs=vs#listing-migrations) command:

```
Get-Migration
```

If more than one database context exists, specify the context with the `-Context` parameter.

In a command shell, run the [dotnet ef migrations list](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#listing-migrations) command:

```
dotnet ef migrations list
```

If more than one database context exists, specify the context with the `--context` parameter.

### Enable authentication

Update the `Startup` class with code similar to the following:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

`UseHsts` is recommended but not required. For more information, see [HTTP Strict Transport Security (HSTS) protocol](../enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol).

### Layout changes

Optional: Add the login partial (`_LoginPartial`) to the layout file:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebRP</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">WebRP</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <partial name="_LoginPartial" />
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2019 - WebRP - <a asp-area="" asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>
```

## Scaffold Identity into a Razor project with authorization

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a class (for example, `Contoso.Data.ApplicationDbContext` for a company named "Contoso").

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command. Use the correct fully qualified name for your DB context.

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login"
   ```

   PowerShell uses the semicolon (`;`) as a command separator. When using PowerShell, escape any semicolons in the file list or put the file list in double quotes (`""`). For example:

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout"
   ```

   If you run the Identity scaffolder without specifying the `--files` flag or the `--useDefaultUI` flag, all the available Identity UI pages are created in the project.

Some Identity options are configured in `Areas/Identity/IdentityHostingStartup.cs`. For more information, see [`IHostingStartup`](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0).

## Scaffold Identity into an MVC project without existing authorization

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a new class for Identity.

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command:

   ```
   dotnet aspnet-codegenerator identity --useDefaultUI
   ```

Optional: Add the login partial (`_LoginPartial`) to the `Views/Shared/_Layout.cshtml` file:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebRP</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">WebRP</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <partial name="_LoginPartial" />
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2019 - WebRP - <a asp-area="" asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>
```

Move the `Pages/Shared/_LoginPartial.cshtml` file to `Views/Shared/_LoginPartial.cshtml`.

Identity is configured in `Areas/Identity/IdentityHostingStartup.cs`. For more information, see [`IHostingStartup`](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0).

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). If a migration to generate the Identity schema isn't already created and applied to the database, use the following procedures to create a migration and update the database.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an EF Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

In a command shell, run the following commands:

```
dotnet ef migrations add CreateIdentitySchema
dotnet ef database update
```

If the Identity schema is created but not applied to the database, you only need to run the command to update the database:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

3. Select **Finish** to complete the update. After the operation finishes, select **Close**.

In a command shell, run the [dotnet ef database update](/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#command-line-tools) command:

```
dotnet ef database update
```

You can confirm the application of an Identity schema with the following command. The output of the command includes an "`applied`" column to show which migrations are applied to the database.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

In the Visual Studio **Package Manager Console**, run the [Get-Migration](/en-us/ef/core/managing-schemas/migrations/managing?tabs=vs#listing-migrations) command:

```
Get-Migration
```

If more than one database context exists, specify the context with the `-Context` parameter.

In a command shell, run the [dotnet ef migrations list](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#listing-migrations) command:

```
dotnet ef migrations list
```

If more than one database context exists, specify the context with the `--context` parameter.

Update the `Startup` class with code similar to the following:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
            endpoints.MapRazorPages();
        });
    }
}
```

`UseHsts` is recommended but not required. For more information, see [HTTP Strict Transport Security (HSTS) protocol](../enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol).

## Scaffold Identity into an MVC project with authorization

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a class (for example, `Contoso.Data.ApplicationDbContext` for a company named "Contoso").

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command. Use the correct fully qualified name for your DB context.

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login"
   ```

   PowerShell uses the semicolon (`;`) as a command separator. When using PowerShell, escape any semicolons in the file list or put the file list in double quotes (`""`). For example:

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout"
   ```

   If you run the Identity scaffolder without specifying the `--files` flag or the `--useDefaultUI` flag, all the available Identity UI pages are created in the project.

## Scaffold Identity into a server-side Blazor app without existing authorization

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a new class for Identity.

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command:

   ```
   dotnet aspnet-codegenerator identity --useDefaultUI
   ```

Identity is configured in `Areas/Identity/IdentityHostingStartup.cs`. For more information, see [`IHostingStartup`](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0).

### Migrations

The generated Identity database code requires [Entity Framework Core Migrations](/en-us/ef/core/managing-schemas/migrations/). If a migration to generate the Identity schema isn't already created and applied to the database, use the following procedures to create a migration and update the database.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

[Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services) are used to add an EF Core migration and update the database.

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Add migration**.

3. Give the migration a **Migration name**, such as `CreateIdentitySchema`, which is a name that describes the migration.

   Wait for the database context to load in the **DbContext class names** field. The action can take a few seconds.

4. Select **Finish** to complete the update. After the operation finishes, select **Close**.

5. Select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

6. Select **Finish** to complete the update. After the operation finishes, select **Close**.

The update database command runs the `Up` method migrations that aren't applied in a migration code file created by the scaffolder. In this case, the command runs the `Up` method in the *Migrations/{TIME STAMP}\_{MIGRATION NAME}.cs* file, which creates the Identity tables, constraints, and indexes. The `{TIME STAMP}` placeholder is a time stamp, and the `{MIGRATION NAME}` placeholder is the migration name.

In a command shell, run the following commands:

```
dotnet ef migrations add CreateIdentitySchema
dotnet ef database update
```

If the Identity schema is created but not applied to the database, you only need to run the command to update the database:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

1. In **Solution Explorer**, double-click **Connected Services**.

2. In the **SQL Server Express LocalDB** area of **Service Dependencies**, select **More actions** (**...**) > **Update database**.

   The **Update database with the latest migration** dialog opens.

   Wait for the **DbContext class names** field to update and for prior migrations to load. The action can take a few seconds.

3. Select **Finish** to complete the update. After the operation finishes, select **Close**.

In a command shell, run the [dotnet ef database update](/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#command-line-tools) command:

```
dotnet ef database update
```

You can confirm the application of an Identity schema with the following command. The output of the command includes an "`applied`" column to show which migrations are applied to the database.

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

In the Visual Studio **Package Manager Console**, run the [Get-Migration](/en-us/ef/core/managing-schemas/migrations/managing?tabs=vs#listing-migrations) command:

```
Get-Migration
```

If more than one database context exists, specify the context with the `-Context` parameter.

In a command shell, run the [dotnet ef migrations list](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#listing-migrations) command:

```
dotnet ef migrations list
```

If more than one database context exists, specify the context with the `--context` parameter.

### Style authentication endpoints

Because server-side Blazor apps use Razor Pages Identity pages, the styling of the UI changes when a visitor navigates between Identity pages and components. You have two options to address the incongruous styles:

* [Custom Identity components](#custom-identity-components)
* [Use a custom layout with Blazor app styles](#use-a-custom-layout-with-blazor-app-styles)

#### Custom Identity components

An approach to using components for Identity instead of pages is to build Identity components. Because `SignInManager` and `UserManager` aren't supported in Razor components, use web API endpoints in the Blazor app to process user account actions.

#### Use a custom layout with Blazor app styles

The Identity pages layout and styles can be modified to produce pages that use styles similar to the default Blazor theme. This approach isn't covered by the documentation.

## Scaffold Identity into a server-side Blazor app with authorization

Run the Identity scaffolder with the following procedures.

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

1. In **Solution Explorer**, right-click the project, and select **Add** > **New Scaffolded Item**.

2. In the left pane of the **Add New Scaffolded Item** dialog, select **Identity**.

3. In the center pane, select **Identity**, and then select **Add**.

4. In the **Add Identity** dialog, select your desired options:

   1. If you have an existing customized layout page for Identity (*\_Layout.cshtml*), select the existing page to avoid overwriting your layout with incorrect markup by the scaffolder. For example, select either:

      * *Pages/Shared/\_Layout.cshtml* for Razor Pages or Blazor Server projects with existing Razor Pages infrastructure.
      * *Views/Shared/\_Layout.cshtml* for MVC projects or Blazor Server projects with existing MVC infrastructure.

   2. Select the plus icon (**+**) and add the database context class, **DbContext class**. You must select at least one file to add your data context.

      * To create a data context, select the plus icon (**+**). Accept the default value or specify a class (for example, `Contoso.Data.ApplicationDbContext` for a company named "Contoso").

      * To create a new user class, select the plus icon (**+**) for **User class** and specify the class (for example, `ContosoUser` for a company named "Contoso").

   3. Select **Add** to run the scaffolder.

1) If the ASP.NET Core scaffolder isn't already installed, run the following command to install it now:

   ```
   dotnet tool install -g dotnet-aspnet-codegenerator
   ```

   Note

   By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

2) Add required NuGet package references to the project file (*.csproj*). Run the following commands in the project directory:

   ```
   dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
   dotnet add package Microsoft.EntityFrameworkCore.Design
   dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
   dotnet add package Microsoft.AspNetCore.Identity.UI
   dotnet add package Microsoft.EntityFrameworkCore.SqlServer
   dotnet add package Microsoft.EntityFrameworkCore.Tools
   ```

3) Run the following command to list the Identity scaffolder options:

   ```
   dotnet aspnet-codegenerator identity -h
   ```

   If you get a scaffolding error, verify the Target Framework Moniker (TFM) matches the NuGet package version in the project file. For example, the following project file contains the version 3.1 for .NET Core and the listed NuGet packages:

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

     <PropertyGroup>
       <TargetFramework>netcoreapp3.1</TargetFramework>
     </PropertyGroup>

     <ItemGroup>
       <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
       <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
       <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
       <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
     </ItemGroup>

   </Project>
   ```

4) In the project folder, run the Identity scaffolder with the options you want.

   For example, to set up identity with the default UI and the minimum number of files, run the following command. Use the correct fully qualified name for your DB context.

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login"
   ```

   PowerShell uses the semicolon (`;`) as a command separator. When using PowerShell, escape any semicolons in the file list or put the file list in double quotes (`""`). For example:

   ```
   dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.Logout"
   ```

   If you run the Identity scaffolder without specifying the `--files` flag or the `--useDefaultUI` flag, all the available Identity UI pages are created in the project.

Some Identity options are configured in `Areas/Identity/IdentityHostingStartup.cs`. For more information, see [`IHostingStartup`](../../fundamentals/host/platform-specific-configuration?view=aspnetcore-10.0).

## Client-side Blazor apps

Client-side Blazor apps use their own Identity UI approaches and can't use ASP.NET Core Identity scaffolding. Server-side ASP.NET Core apps of hosted Blazor solutions can follow the Razor Pages/MVC guidance in this article and are configured just like any other type of ASP.NET Core app that supports Identity.

The Blazor framework doesn't include Razor component versions of Identity UI pages. Identity UI Razor components can be custom built or obtained from unsupported third-party sources.

For more information, see the [Blazor Security and Identity articles](../../blazor/security/?view=aspnetcore-10.0).

## Create full Identity UI source

To maintain full control of the Identity UI, run the Identity scaffolder and select **Override all files**.

The following highlighted code shows the changes to replace the default Identity UI with Identity in an ASP.NET Core 2.1 web app. You might want to do this to have full control of the Identity UI.

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<IdentityUser, IdentityRole>()
        // services.AddDefaultIdentity<IdentityUser>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc()
        .AddRazorPagesOptions(options =>
        {
            options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
            options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
        });

    services.ConfigureApplicationCookie(options =>
    {
        options.LoginPath = $"/Identity/Account/Login";
        options.LogoutPath = $"/Identity/Account/Logout";
        options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
    });

    // using Microsoft.AspNetCore.Identity.UI.Services;
    services.AddSingleton<IEmailSender, EmailSender>();
}
```

The default Identity is replaced in the following code:

```
services.AddIdentity<IdentityUser, IdentityRole>()
    // services.AddDefaultIdentity<IdentityUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();
```

The following code sets the [LoginPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions.loginpath), [LogoutPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions.logoutpath), and [AccessDeniedPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions.accessdeniedpath)):

```
services.ConfigureApplicationCookie(options =>
{
    options.LoginPath = $"/Identity/Account/Login";
    options.LogoutPath = $"/Identity/Account/Logout";
    options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
});
```

Register an `IEmailSender` implementation, for example:

```
// using Microsoft.AspNetCore.Identity.UI.Services;
services.AddSingleton<IEmailSender, EmailSender>();
```

```
public class EmailSender : IEmailSender
{
    public Task SendEmailAsync(string email, string subject, string message)
    {
        return Task.CompletedTask;
    }
}
```

## Password configuration

If [PasswordOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordoptions) are configured in `Startup.ConfigureServices`, [`[StringLength]` attribute](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute) configuration might be required for the `Password` property in scaffolded Identity pages. `InputModel` `Password` properties are found in the following files:

* `Areas/Identity/Pages/Account/Register.cshtml.cs`
* `Areas/Identity/Pages/Account/ResetPassword.cshtml.cs`

## Disable a page

This section shows how to disable the register page but the approach can be used to disable any page.

To disable user registration:

* Scaffold Identity. Include Account.Register, Account.Login, and Account.RegisterConfirmation. For example:

  ```
  dotnet aspnet-codegenerator identity -dc RPauth.Data.ApplicationDbContext --files "Account.Register;Account.Login;Account.RegisterConfirmation"
  ```

* Update `Areas/Identity/Pages/Account/Register.cshtml.cs` so users can't register from this endpoint:

  ```
  public class RegisterModel : PageModel
  {
      public IActionResult OnGet()
      {
          return RedirectToPage("Login");
      }

      public IActionResult OnPost()
      {
          return RedirectToPage("Login");
      }
  }
  ```

* Update `Areas/Identity/Pages/Account/Register.cshtml` to be consistent with the preceding changes:

  ```
  @page
  @model RegisterModel
  @{
      ViewData["Title"] = "Go to Login";
  }

  <h1>@ViewData["Title"]</h1>

  <li class="nav-item">
      <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
  </li>
  ```

* Comment out or remove the registration link from `Areas/Identity/Pages/Account/Login.cshtml`

  ```
  @*
  <p>
      <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
  </p>
  *@
  ```

* Update the `Areas/Identity/Pages/Account/RegisterConfirmation` page.

  * Remove the code and links from the cshtml file.
  * Remove the confirmation code from the `PageModel`:

  ```
  [AllowAnonymous]
    public class RegisterConfirmationModel : PageModel
    {
        public IActionResult OnGet()
        {  
            return Page();
        }
    }
  ```

### Use another app to add users

Provide a mechanism to add users outside the web app. Options to add users include:

* A dedicated admin web app.
* A console app.

The following code outlines one approach to adding users:

* A list of users is read into memory.
* A strong unique password is generated for each user.
* The user is added to the Identity database.
* The user is notified and told to change the password.

```
public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                var context = services.GetRequiredService<AppDbCntx>();
                context.Database.Migrate();

                var config = host.Services.GetRequiredService<IConfiguration>();
                var userList = config.GetSection("userList").Get<List<string>>();

                SeedData.Initialize(services, userList).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred adding users.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

The following code outlines adding a user:

```

public static async Task Initialize(IServiceProvider serviceProvider,
                                    List<string> userList)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    foreach (var userName in userList)
    {
        var userPassword = GenerateSecurePassword();
        var userId = await EnsureUser(userManager, userName, userPassword);

        NotifyUser(userName, userPassword);
    }
}

private static async Task<string> EnsureUser(UserManager<IdentityUser> userManager,
                                             string userName, string userPassword)
{
    var user = await userManager.FindByNameAsync(userName);

    if (user == null)
    {
        user = new IdentityUser(userName)
        {
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, userPassword);
    }

    return user.Id;
}
```

A similar approach can be followed for production scenarios.

## Prevent publish of static Identity assets

To prevent publishing static Identity assets to the web root, see [Introduction to Identity on ASP.NET Core](identity?view=aspnetcore-10.0#prevent-publish-of-static-identity-assets).

## Related content

* [ASP.NET Core Identity](identity?view=aspnetcore-10.0)
* [Visual Studio Connected Services](/en-us/visualstudio/azure/overview-connected-services)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-identity-server
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/hosted-with-identity-server.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure a hosted ASP.NET Core Blazor WebAssembly app with Identity Server

Important

The Hosted Blazor WebAssembly project template was removed from the framework with the release of .NET 8 (November, 2023). The guidance in this article is only supported for .NET 7 or earlier. Hosted Blazor WebAssembly apps that are upgraded each release continue to receive product support. Alternatively, refactor the app into either a standalone Blazor WebAssembly app or a Blazor Web App.

This article explains how to create a [hosted Blazor WebAssembly solution](../../hosting-models?view=aspnetcore-7.0#blazor-webassembly) that uses [Duende Identity Server](https://docs.duendesoftware.com) to authenticate users and API calls.

Important

[Duende Software](https://duendesoftware.com/) might require you to pay a license fee for production use of Duende Identity Server. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../../../migration/50-to-60?view=aspnetcore-7.0#project-templates-use-duende-identity-server).

Note

To configure a standalone or hosted Blazor WebAssembly app to use an existing, external Identity Server instance, follow the guidance in [Secure an ASP.NET Core Blazor WebAssembly standalone app with the Authentication library](standalone-with-authentication-library?view=aspnetcore-7.0).

For additional security scenario coverage after reading this article, see [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-7.0).

## Walkthrough

The subsections of the walkthrough explain how to:

* Create the Blazor app
* Run the app

### Create a Blazor app

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / .NET CLI](#tabpanel_1_visual-studio-code+net-cli)

To create a new Blazor WebAssembly project with an authentication mechanism:

1. Create a new project.

2. Choose the **Blazor WebAssembly App** template. Select **Next**.

3. Provide a **Project name** without using dashes. Confirm that the **Location** is correct. Select **Next**.

   Avoid using dashes (`-`) in the project name that break the formation of the OIDC app identifier. Logic in the Blazor WebAssembly project template uses the project name for an OIDC app identifier in the solution's configuration, and dashes aren't permitted in an OIDC app identifier. Pascal case (`BlazorSample`) or underscores (`Blazor_Sample`) are acceptable alternatives.

4. In the **Additional information** dialog, select **Individual Accounts** as the **Authentication type** to store users within the app using ASP.NET Core's [Identity](../../../security/authentication/identity?view=aspnetcore-7.0) system.

5. Select the **ASP.NET Core Hosted** checkbox.

6. Select the **Create** button to create the app.

To create a new Blazor WebAssembly project with an authentication mechanism in an empty folder, specify the `Individual` authentication mechanism with the `-au|--auth` option to store users within the app using ASP.NET Core's [Identity](../../../security/authentication/identity?view=aspnetcore-7.0) system:

```
dotnet new blazorwasm -au Individual -ho -o {PROJECT NAME}
```

| Placeholder      | Example        |
| ---------------- | -------------- |
| `{PROJECT NAME}` | `BlazorSample` |

The output location specified with the optional `-o|--output` option creates a project folder if it doesn't exist and becomes part of the project's name.

Avoid using dashes (`-`) in the project name that break the formation of the OIDC app identifier. Logic in the Blazor WebAssembly project template uses the project name for an OIDC app identifier in the solution's configuration, and dashes aren't permitted in an OIDC app identifier. Pascal case (`BlazorSample`) or underscores (`Blazor_Sample`) are acceptable alternatives.

For more information, see the [`dotnet new`](/en-us/dotnet/core/tools/dotnet-new) command in the .NET Guide.

### Run the app

Run the app from the **`Server`** project. When using Visual Studio, either:

* Select the dropdown arrow next to the **Run** button. Open **Configure Startup Projects** from the dropdown list. Select the **Single startup project** option. Confirm or change the project for the startup project to the **`Server`** project.

* Confirm that the **`Server`** project is highlighted in **Solution Explorer** before you start the app with any of the following approaches:

  * Select the **Run** button.
  * Use **Debug** > **Start Debugging** from the menu.
  * Press `F5`.

* In a command shell, navigate to the **`Server`** project folder of the solution. Execute the `dotnet watch` (or `dotnet run`) command.

  ```
  @using Microsoft.Extensions.Options
  @inject IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>> RemoteOptions
  ```

  Modify the component's redirect in the `OnInitialized` method:

  ```
  - Navigation.NavigateToLogin("authentication/login");
  + Navigation.NavigateToLogin(RemoteOptions.Get(Options.DefaultName)
  +     .AuthenticationPaths.LogInPath);
  ```

  Note

  If other paths differ from the project template's paths or [framework's default paths](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationDefaults.cs), manage them in the same fashion.

## Parts of the solution

This section describes the parts of a solution generated from the Blazor WebAssembly project template and describes how the solution's **Client** and **Server** projects are configured for reference. There's no specific guidance to follow in this section for a basic working application if you created the app using the guidance in the [Walkthrough](#walkthrough) section. The guidance in this section is helpful for updating an app to authenticate and authorize users. However, an alternative approach to updating an app is to create a new app from the guidance in the [Walkthrough](#walkthrough) section and moving the app's components, classes, and resources to the new app.

### **Server** app services

*This section pertains to the solution's **Server** app.*

The following services are registered.

* In the `Program` file:

  * Entity Framework Core and ASP.NET Core Identity:

    ```
    builder.Services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlite( ... ));
    builder.Services.AddDatabaseDeveloperPageExceptionFilter();

    builder.Services.AddDefaultIdentity<ApplicationUser>(options => 
            options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    ```

  * Identity Server with an additional [AddApiAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityserverbuilderconfigurationextensions.addapiauthorization) helper method that sets up default ASP.NET Core conventions on top of Identity Server:

    ```
    builder.Services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
    ```

  * Authentication with an additional [AddIdentityServerJwt](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilderextensions.addidentityserverjwt) helper method that configures the app to validate JWT tokens produced by Identity Server:

    ```
    builder.Services.AddAuthentication()
        .AddIdentityServerJwt();
    ```

- In `Startup.ConfigureServices` of `Startup.cs`:

  * Entity Framework Core and ASP.NET Core Identity:

    ```
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlite(
            Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<ApplicationUser>(options => 
            options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    ```

    Warning

    Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../../security/app-secrets?view=aspnetcore-7.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](../?view=aspnetcore-7.0#securely-maintain-sensitive-data-and-credentials).

  * Identity Server with an additional [AddApiAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityserverbuilderconfigurationextensions.addapiauthorization) helper method that sets up default ASP.NET Core conventions on top of Identity Server:

    ```
    services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
    ```

  * Authentication with an additional [AddIdentityServerJwt](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilderextensions.addidentityserverjwt) helper method that configures the app to validate JWT tokens produced by Identity Server:

    ```
    services.AddAuthentication()
        .AddIdentityServerJwt();
    ```

Note

When a single authentication scheme is registered, the authentication scheme is automatically used as the app's default scheme, and it isn't necessary to state the scheme to [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) or via [AuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions). For more information, see [Overview of ASP.NET Core Authentication](../../../security/authentication/?view=aspnetcore-7.0#defaultscheme) and the [ASP.NET Core announcement (aspnet/Announcements #490)](https://github.com/aspnet/Announcements/issues/490).

* In the `Program` file:

- In `Startup.Configure` of `Startup.cs`:

* The Identity Server middleware exposes the OpenID Connect (OIDC) endpoints:

  ```
  app.UseIdentityServer();
  ```

- The authentication middleware is responsible for validating request credentials and setting the user on the request context:

  ```
  app.UseAuthentication();
  ```

* Authorization middleware enables authorization capabilities:

  ```
  app.UseAuthorization();
  ```

### API authorization

*This section pertains to the solution's **Server** app.*

The [AddApiAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityserverbuilderconfigurationextensions.addapiauthorization) helper method configures [Identity Server](https://docs.duendesoftware.com) for ASP.NET Core scenarios. Identity Server is a powerful and extensible framework for handling app security concerns. Identity Server exposes unnecessary complexity for the most common scenarios. Consequently, a set of conventions and configuration options is provided that we consider a good starting point. Once your authentication needs change, the full power of Identity Server is available to customize authentication to suit an app's requirements.

### Add an authentication handler for an API that coexists with Identity Server

*This section pertains to the solution's **Server** app.*

The [AddIdentityServerJwt](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilderextensions.addidentityserverjwt) helper method configures a policy scheme for the app as the default authentication handler. The policy is configured to allow Identity to handle all requests routed to any subpath in the Identity URL space under `/Identity`. The [JwtBearerHandler](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerhandler) handles all other requests. Additionally, this method:

* Registers an API resource with Identity Server with a default scope of `{PROJECT NAME}API`, where the `{PROJECT NAME}` placeholder is the project's name at app creation.
* Configures the JWT bearer token middleware to validate tokens issued by Identity Server for the app.

### Weather forecast controller

*This section pertains to the solution's **Server** app.*

In the `WeatherForecastController` (`Controllers/WeatherForecastController.cs`), the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) is applied to the class. The attribute indicates that the user must be authorized based on the default policy to access the resource. The default authorization policy is configured to use the default authentication scheme, which is set up by [AddIdentityServerJwt](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilderextensions.addidentityserverjwt). The helper method configures [JwtBearerHandler](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerhandler) as the default handler for requests to the app.

### Application database context

*This section pertains to the solution's **Server** app.*

In the `ApplicationDbContext` (`Data/ApplicationDbContext.cs`), [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) extends [ApiAuthorizationDbContext\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.apiauthorization.identityserver.apiauthorizationdbcontext-1) to include the schema for Identity Server. [ApiAuthorizationDbContext\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.apiauthorization.identityserver.apiauthorizationdbcontext-1) is derived from [IdentityDbContext](/en-us/dotnet/api/microsoft.aspnetcore.identity.entityframeworkcore.identitydbcontext).

To gain full control of the database schema, inherit from one of the available Identity [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) classes and configure the context to include the Identity schema by calling `builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value)` in the [OnModelCreating](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.onmodelcreating) method.

### OIDC configuration controller

*This section pertains to the solution's **Server** app.*

In the `OidcConfigurationController` (`Controllers/OidcConfigurationController.cs`), the client endpoint is provisioned to serve OIDC parameters.

### App settings

*This section pertains to the solution's **Server** app.*

In the app settings file (`appsettings.json`) at the project root, the `IdentityServer` section describes the list of configured clients. In the following example, there's a single client. The client name corresponds to the **Client** app's assembly name and is mapped by convention to the OAuth `ClientId` parameter. The profile indicates the app type being configured. The profile is used internally to drive conventions that simplify the configuration process for the server.

```
"IdentityServer": {
  "Clients": {
    "{ASSEMBLY NAME}": {
      "Profile": "IdentityServerSPA"
    }
  }
}
```

The `{ASSEMBLY NAME}` placeholder is the **Client** app's assembly name (for example, `BlazorSample.Client`).

### Authentication package

*This section pertains to the solution's **Client** app.*

When an app is created to use Individual Accounts (`Individual`), the app automatically receives a package reference for the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication). The package provides a set of primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) to the app.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

### `HttpClient` configuration

*This section pertains to the solution's **Client** app.*

In the `Program` file, a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is configured to supply [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances that include access tokens when making requests to the server API. At solution creation, the named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is `{PROJECT NAME}.ServerAPI`, where the `{PROJECT NAME}` placeholder is the project's name.

```
builder.Services.AddHttpClient("{PROJECT NAME}.ServerAPI", 
        client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("{PROJECT NAME}.ServerAPI"));
```

The `{PROJECT NAME}` placeholder is the project name at solution creation. For example, providing a project name of `BlazorSample` produces a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) of `BlazorSample.ServerAPI`.

Note

If you're configuring a Blazor WebAssembly app to use an existing Identity Server instance that isn't part of a hosted Blazor solution, change the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) base address registration from [IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-baseaddress) (`builder.HostEnvironment.BaseAddress`) to the server app's API authorization endpoint URL.

### API authorization support

*This section pertains to the solution's **Client** app.*

The support for authenticating users is plugged into the service container by the extension method provided inside the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication). This method sets up the services required by the app to interact with the existing authorization system.

```
builder.Services.AddApiAuthorization();
```

Configuration for the app is loaded by convention from `_configuration/{client-id}`. By convention, the client ID is set to the app's assembly name. This URL can be changed to point to a separate endpoint by calling the overload with options.

### `Imports` file

*This section pertains to the solution's **Client** app.*

The [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespace is made available throughout the app via the imports file (`_Imports.razor`):

```
...
@using Microsoft.AspNetCore.Components.Authorization
...
```

### `Index` page

*This section pertains to the solution's **Client** app.*

The Index page (`wwwroot/index.html`) page includes a script that defines the `AuthenticationService` in JavaScript. `AuthenticationService` handles the low-level details of the OIDC protocol. The app internally calls methods defined in the script to perform the authentication operations.

```
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
```

### `App` component

*This section pertains to the solution's **Client** app.*

The `App` component (`App.razor`) is similar to the `App` component found in Blazor Server apps:

* The [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) component manages exposing the [AuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstate) to the rest of the app.
* The [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) component makes sure that the current user is authorized to access a given page or otherwise renders the `RedirectToLogin` component.
* The `RedirectToLogin` component manages redirecting unauthorized users to the login page.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the `App` component (`App.razor`) isn't shown in this section. To inspect the markup of the component for a given release, use ***either*** of the following approaches:

* Create an app provisioned for authentication from the default Blazor WebAssembly project template for the version of ASP.NET Core that you intend to use. Inspect the `App` component (`App.razor`) in the generated app.

* Inspect the `App` component (`App.razor`) in [reference source](https://github.com/dotnet/aspnetcore). Select the version from the branch selector, and search for the component in the `ProjectTemplates` folder of the repository because the `App` component's location has changed over the years.

  Note

  Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### `RedirectToLogin` component

*This section pertains to the solution's **Client** app.*

The `RedirectToLogin` component (`RedirectToLogin.razor`):

* Manages redirecting unauthorized users to the login page.

* The current URL that the user is attempting to access is maintained so that they can be returned to that page if authentication is successful using:

  * [Navigation history state](../../fundamentals/navigation?view=aspnetcore-7.0#navigation-history-state) in ASP.NET Core in .NET 7 or later.
  * A query string in ASP.NET Core in .NET 6 or earlier.

Inspect the `RedirectToLogin` component in [reference source](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp). The location of the component changed over time, so use GitHub search tools to locate the component.

### `LoginDisplay` component

*This section pertains to the solution's **Client** app.*

The `LoginDisplay` component (`LoginDisplay.razor`) is rendered in the `MainLayout` component (`MainLayout.razor`) and manages the following behaviors:

* For authenticated users:

  * Displays the current user name.
  * Offers a link to the user profile page in ASP.NET Core Identity.
  * Offers a button to log out of the app.

* For anonymous users:

  * Offers the option to register.
  * Offers the option to log in.

Due to changes in the framework across releases of ASP.NET Core, Razor markup for the `LoginDisplay` component isn't shown in this section. To inspect the markup of the component for a given release, use ***either*** of the following approaches:

* Create an app provisioned for authentication from the default Blazor WebAssembly project template for the version of ASP.NET Core that you intend to use. Inspect the `LoginDisplay` component in the generated app.

* Inspect the `LoginDisplay` component in [reference source](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp). The location of the component changed over time, so use GitHub search tools to locate the component. The templated content for `Hosted` equal to `true` is used.

  Note

  Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### `Authentication` component

*This section pertains to the solution's **Client** app.*

The page produced by the `Authentication` component (`Pages/Authentication.razor`) defines the routes required for handling different authentication stages.

The [RemoteAuthenticatorView](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticatorview) component:

* Is provided by the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication/).
* Manages performing the appropriate actions at each stage of authentication.

```
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string? Action { get; set; }
}
```

Note

[Nullable reference types (NRTs) and .NET compiler null-state static analysis](../../../migration/50-to-60?view=aspnetcore-7.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis) is supported in ASP.NET Core in .NET 6 or later. Prior to the release of ASP.NET Core in .NET 6, the `string` type appears without the null type designation (`?`).

### `FetchData` component

*This section pertains to the solution's **Client** app.*

The `FetchData` component shows how to:

* Provision an access token.
* Use the access token to call a protected resource API in the *Server* app.

The [`@attribute [Authorize]`](../../../mvc/views/razor?view=aspnetcore-7.0#attribute) directive indicates to the Blazor WebAssembly authorization system that the user must be authorized in order to visit this component. The presence of the attribute in the **`Client`** app doesn't prevent the API on the server from being called without proper credentials. The **`Server`** app also must use `[Authorize]` on the appropriate endpoints to correctly protect them.

[IAccessTokenProvider.RequestAccessToken](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.iaccesstokenprovider.requestaccesstoken) takes care of requesting an access token that can be added to the request to call the API. If the token is cached or the service is able to provision a new access token without user interaction, the token request succeeds. Otherwise, the token request fails with an [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception), which is caught in a [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) statement.

In order to obtain the actual token to include in the request, the app must check that the request succeeded by calling [`tokenResult.TryGetToken(out var token)`](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenresult.trygettoken).

If the request was successful, the token variable is populated with the access token. The [AccessToken.Value](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstoken.value#microsoft-aspnetcore-components-webassembly-authentication-accesstoken-value) property of the token exposes the literal string to include in the `Authorization` request header.

If the token couldn't be provisioned without user interaction resulting in a failed request:

* ASP.NET Core in .NET 7 or later: The app navigates to `AccessTokenResult.InteractiveRequestUrl` using the given `AccessTokenResult.InteractionOptions` to allow refreshing the access token.
* ASP.NET Core in .NET 6 or earlier: The token result contains a redirect URL. Navigating to this URL takes the user to the login page and back to the current page after a successful authentication.

```
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using {APP NAMESPACE}.Shared
@attribute [Authorize]
@inject HttpClient Http

...

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}
```

## Azure App Service on Linux

Specify the issuer explicitly when deploying to Azure App Service on Linux. For more information, see [Use Identity to secure a Web API backend for SPAs](../../../security/authentication/identity-api-authorization?view=aspnetcore-7.0#azure-app-service-on-linux).

## Name and role claim with API authorization

### Custom user factory

In the **Client** app, create a custom user factory. Identity Server sends multiple roles as a JSON array in a single `role` claim. A single role is sent as a string value in the claim. The factory creates an individual `role` claim for each of the user's roles.

`CustomUserFactory.cs`:

```
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomUserFactory(IAccessTokenProviderAccessor accessor)
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var user = await base.CreateUserAsync(account, options);

        if (user.Identity is not null && user.Identity.IsAuthenticated)
        {
            var identity = (ClaimsIdentity)user.Identity;
            var roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();

            if (roleClaims.Any())
            {
                foreach (var existingClaim in roleClaims)
                {
                    identity.RemoveClaim(existingClaim);
                }

                var rolesElem = 
                    account.AdditionalProperties[identity.RoleClaimType];

                if (options.RoleClaim is not null && rolesElem is JsonElement roles)
                {
                    if (roles.ValueKind == JsonValueKind.Array)
                    {
                        foreach (var role in roles.EnumerateArray())
                        {
                            var roleValue = role.GetString();

                            if (!string.IsNullOrEmpty(roleValue))
                            {
                                identity.AddClaim(
                                  new Claim(options.RoleClaim, roleValue));
                            }
        
                        }
                    }
                    else
                    {
                        var roleValue = roles.GetString();

                        if (!string.IsNullOrEmpty(roleValue))
                        {
                            identity.AddClaim(
                              new Claim(options.RoleClaim, roleValue));
                        }
                    }
                }
            }
        }

        return user;
    }
}
```

In the **Client** app, register the factory in the `Program` file:

```
builder.Services.AddApiAuthorization()
    .AddAccountClaimsPrincipalFactory<CustomUserFactory>();
```

In the **Server** app, call [AddRoles](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addroles) on the Identity builder, which adds role-related services.

In the `Program` file:

```
using Microsoft.AspNetCore.Identity;

...

builder.Services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
```

In `Startup.cs`:

```
using Microsoft.AspNetCore.Identity;

...

services.AddDefaultIdentity<ApplicationUser>(options => 
    options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
```

### Configure Identity Server

Use **one** of the following approaches:

* [API authorization options](#api-authorization-options)
* [Profile Service](#profile-service)

#### API authorization options

In the **Server** app:

* Configure Identity Server to put the `name` and `role` claims into the ID token and access token.
* Prevent the default mapping for roles in the JWT token handler.

In the `Program` file:

```
using System.IdentityModel.Tokens.Jwt;

...

builder.Services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
        options.IdentityResources["openid"].UserClaims.Add("name");
        options.ApiResources.Single().UserClaims.Add("name");
        options.IdentityResources["openid"].UserClaims.Add("role");
        options.ApiResources.Single().UserClaims.Add("role");
    });

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
```

In `Startup.cs`:

```
using System.IdentityModel.Tokens.Jwt;
using System.Linq;

...

services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
        options.IdentityResources["openid"].UserClaims.Add("name");
        options.ApiResources.Single().UserClaims.Add("name");
        options.IdentityResources["openid"].UserClaims.Add("role");
        options.ApiResources.Single().UserClaims.Add("role");
    });

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
```

#### Profile Service

In the **Server** app, create a `ProfileService` implementation.

`ProfileService.cs`:

```
using IdentityModel;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;

public class ProfileService : IProfileService
{
    public ProfileService()
    {
    }

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var nameClaim = context.Subject.FindAll(JwtClaimTypes.Name);
        context.IssuedClaims.AddRange(nameClaim);

        var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
        context.IssuedClaims.AddRange(roleClaims);

        await Task.CompletedTask;
    }

    public async Task IsActiveAsync(IsActiveContext context)
    {
        await Task.CompletedTask;
    }
}
```

In the **Server** app, register the Profile Service in the `Program` file:

```
using Duende.IdentityServer.Services;

...

builder.Services.AddTransient<IProfileService, ProfileService>();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
```

In the **Server** app, register the Profile Service in `Startup.ConfigureServices` of `Startup.cs`:

```
using IdentityServer4.Services;

...

services.AddTransient<IProfileService, ProfileService>();

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
```

### Use authorization mechanisms

In the **Client** app, component authorization approaches are functional at this point. Any of the authorization mechanisms in components can use a role to authorize the user:

* [`AuthorizeView` component](../?view=aspnetcore-7.0#authorizeview-component) (Example: `<AuthorizeView Roles="Admin">`)

* [`[Authorize]` attribute directive](../?view=aspnetcore-7.0#authorize-attribute) ([AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute)) (Example: `@attribute [Authorize(Roles = "Admin")]`)

* [Procedural logic](../?view=aspnetcore-7.0#procedural-logic) (Example: `if (user.IsInRole("Admin")) { ... }`)

  Multiple role tests are supported:

  ```
  if (user.IsInRole("Admin") && user.IsInRole("Developer"))
  {
      ...
  }
  ```

`User.Identity.Name` is populated in the **Client** app with the user's user name, which is usually their sign-in email address.

## `UserManager` and `SignInManager`

Set the user identifier claim type when a Server app requires:

* [UserManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1) or [SignInManager\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1) in an API endpoint.
* [IdentityUser](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser) details, such as the user's name, email address, or lockout end time.

In `Program.cs` for ASP.NET Core in .NET 6 or later:

```
using System.Security.Claims;

...

builder.Services.Configure<IdentityOptions>(options => 
    options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
```

In `Startup.ConfigureServices` for .NET 5 or earlier:

```
using System.Security.Claims;

...

services.Configure<IdentityOptions>(options => 
    options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
```

The following `WeatherForecastController` logs the [UserName](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser-1.username#microsoft-aspnetcore-identity-identityuser-1-username) when the `Get` method is called.

Note

The following example uses:

* A [file-scoped namespace](/en-us/dotnet/csharp/language-reference/keywords/namespace), which is a C# 10 or later (.NET 6 or later) feature.
* A [primary constructor](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors), which is a C# 12 or later (.NET 8 or later) feature.

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using BlazorSample.Server.Models;
using BlazorSample.Shared;

namespace BlazorSample.Server.Controllers;

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController(ILogger<WeatherForecastController> logger, 
        UserManager<ApplicationUser> userManager) : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", 
        "Balmy", "Hot", "Sweltering", "Scorching"
    };

    [HttpGet]
    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        var rng = new Random();

        var user = await userManager.GetUserAsync(User);

        if (user != null)
        {
            logger.LogInformation("User.Identity.Name: {UserIdentityName}", user.UserName);
        }

        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}
```

In the preceding example:

* The **`Server`** project's namespace is `BlazorSample.Server`.
* The **`Shared`** project's namespace is `BlazorSample.Shared`.

## Host in Azure App Service with a custom domain and certificate

The following guidance explains:

* How to deploy a hosted Blazor WebAssembly app with Identity Server to [Azure App Service](https://azure.microsoft.com/services/app-service/) with a custom domain.
* How to create and use a TLS certificate for HTTPS protocol communication with browsers. Although the guidance focuses on using the certificate with a custom domain, the guidance is equally applicable to using a default Azure Apps domain, for example `contoso.azurewebsites.net`.

For this hosting scenario, do **not** use the same certificate for [Duende Identity Server's](https://docs.duendesoftware.com) token signing key and the site's HTTPS secure communication with browsers:

* Using different certificates for these two requirements is a good security practice because it isolates private keys for each purpose.
* TLS certificates for communication with browsers is managed independently without affecting Identity Server's token signing.
* When [Azure Key Vault](https://azure.microsoft.com/services/key-vault/) supplies a certificate to an App Service app for custom domain binding, Identity Server can't obtain the same certificate from Azure Key Vault for token signing. Although configuring Identity Server to use the same TLS certificate from a physical path is possible, placing security certificates into source control is a **poor practice and should be avoided in most scenarios**.

In the following guidance, a self-signed certificate is created in Azure Key Vault solely for Identity Server token signing. The Identity Server configuration uses the key vault certificate via the app's `CurrentUser` > `My` certificate store. Other certificates used for HTTPS traffic with custom domains are created and configured separately from the Identity Server signing certificate.

To configure an app, Azure App Service, and Azure Key Vault to host with a custom domain and HTTPS:

1. Create an [App Service plan](/en-us/azure/app-service/overview-hosting-plans) with an plan level of `Basic B1` or higher. App Service requires a `Basic B1` or higher service tier to use custom domains.

2. Create a PFX certificate for the site's secure browser communication (HTTPS protocol) with a common name of the site's fully qualified domain name (FQDN) that your organization controls (for example, `www.contoso.com`). Create the certificate with:

   * Key uses

     * Digital signature validation (`digitalSignature`)
     * Key encipherment (`keyEncipherment`)

   * Enhanced/extended key uses

     * Client Authentication (1.3.6.1.5.5.7.3.2)
     * Server Authentication (1.3.6.1.5.5.7.3.1)

   To create the certificate, use one of the following approaches or any other suitable tool or online service:

   * [Azure Key Vault](/en-us/azure/key-vault/certificates/quick-create-portal#add-a-certificate-to-key-vault)
   * [MakeCert on Windows](/en-us/windows/desktop/seccrypto/makecert)
   * [OpenSSL](https://www.openssl.org)

   Make note of the password, which is used later to import the certificate into Azure Key Vault.

   For more information on Azure Key Vault certificates, see [Azure Key Vault: Certificates](/en-us/azure/key-vault/certificates/).

3. Create a new Azure Key Vault or use an existing key vault in your Azure subscription.

4. In the key vault's **Certificates** area, import the PFX site certificate. Record the certificate's thumbprint, which is used in the app's configuration later.

5. In Azure Key Vault, generate a new self-signed certificate for Identity Server token signing. Give the certificate a **Certificate Name** and **Subject**. The **Subject** is specified as `CN={COMMON NAME}`, where the `{COMMON NAME}` placeholder is the certificate's common name. The common name can be any alphanumeric string. For example, `CN=IdentityServerSigning` is a valid certificate **Subject**. In **Issuance Policy** > **Advanced Policy Configuration**, use the default settings. Record the certificate's thumbprint, which is used in the app's configuration later.

6. Navigate to Azure App Service in the Azure portal and create a new App Service with the following configuration:

   * **Publish** set to `Code`.
   * **Runtime stack** set to the app's runtime.
   * For **Sku and size**, confirm that the App Service tier is `Basic B1` or higher. App Service requires a `Basic B1` or higher service tier to use custom domains.

7. After Azure creates the App Service, open the app's **Configuration** and add a new application setting specifying the certificate thumbprints recorded earlier. The app setting key is `WEBSITE_LOAD_CERTIFICATES`. Separate the certificate thumbprints in the app setting value with a comma, as the following example shows:

   * Key: `WEBSITE_LOAD_CERTIFICATES`
   * Value: `57443A552A46DB...D55E28D412B943565,29F43A772CB6AF...1D04F0C67F85FB0B1`

   In the Azure portal, saving app settings is a two-step process: Save the `WEBSITE_LOAD_CERTIFICATES` key-value setting, then select the **Save** button at the top of the blade.

8. Select the app's **TLS/SSL settings**. Select **Private Key Certificates (.pfx)**. Use the **Import Key Vault Certificate** process. **Use the process *twice* to import both the site's certificate for HTTPS communication and the site's self-signed Identity Server token signing certificate.**

9. Navigate to the **Custom domains** blade. At your domain registrar's website, use the **IP address** and **Custom Domain Verification ID** to configure the domain. A typical domain configuration includes:

   * An **A Record** with a **Host** of `@` and a value of the IP address from the Azure portal.
   * A **TXT Record** with a **Host** of `asuid` and the value of the verification ID generated by Azure and provided by the Azure portal.

   Make sure that you save the changes at your domain registrar's website correctly. Some registrar websites require a two-step process to save domain records: One or more records are saved individually followed by updating the domain's registration with a separate button.

10. Return to the **Custom domains** blade in the Azure portal. Select **Add custom domain**. Select the **A Record** option. Provide the domain and select **Validate**. If the domain records are correct and propagated across the Internet, the portal allows you to select the **Add custom domain** button.

    It can take a few days for domain registration changes to propagate across Internet domain name servers (DNS) after they're processed by your domain registrar. If domain records aren't updated within three business days, confirm the records are correctly set with the domain registrar and contact their customer support.

11. In the **Custom domains** blade, the **SSL STATE** for the domain is marked `Not Secure`. Select the **Add binding** link. Select the site HTTPS certificate from the key vault for the custom domain binding.

12. In Visual Studio, open the *Server* project's app settings file (`appsettings.json` or `appsettings.Production.json`). In the Identity Server configuration, add the following `Key` section. Specify the self-signed certificate **Subject** for the `Name` key. In the following example, the certificate's common name assigned in the key vault is `IdentityServerSigning`, which yields a **Subject** of `CN=IdentityServerSigning`:

    ```
    "IdentityServer": {

      ...

      "Key": {
        "Type": "Store",
        "StoreName": "My",
        "StoreLocation": "CurrentUser",
        "Name": "CN=IdentityServerSigning"
      }
    },
    ```

13. In Visual Studio, create an Azure App Service [publish profile](../../../host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-7.0#publish-profiles) for the *Server* project. From the menu bar, select: **Build** > **Publish** > **New** > **Azure** > **Azure App Service** (Windows or Linux). When Visual Studio is connected to an Azure subscription, you can set the **View** of Azure resources by **Resource type**. Navigate within the **Web App** list to find the App Service for the app and select it. Select **Finish**.

14. When Visual Studio returns to the **Publish** window, the key vault and SQL Server database service dependencies are automatically detected.

    No configuration changes to the default settings are required for the key vault service.

    For testing purposes, an app's local [SQLite](https://www.sqlite.org/index.html) database, which is configured by the Blazor template, can be deployed with the app without additional configuration. Configuring a different database for Identity Server in production is beyond the scope of this article. For more information, see the database resources in the following documentation sets:

    * [App Service](/en-us/azure/app-service/)
    * [Duende Identity Server](https://docs.duendesoftware.com)

15. Select the **Edit** link under the deployment profile name at the top of the window. Change the destination URL to the site's custom domain URL (for example, `https://www.contoso.com`). Save the settings.

16. Publish the app. Visual Studio opens a browser window and requests the site at its custom domain.

The Azure documentation contains additional detail on using Azure services and custom domains with TLS binding in App Service, including information on using CNAME records instead of A records. For more information, see the following resources:

* [App Service documentation](/en-us/azure/app-service/)
* [Tutorial: Map an existing custom DNS name to Azure App Service](/en-us/azure/app-service/app-service-web-tutorial-custom-domain)
* [Secure a custom DNS name with a TLS/SSL binding in Azure App Service](/en-us/azure/app-service/configure-ssl-bindings)
* [Azure Key Vault](/en-us/azure/key-vault/)

We recommend using a new private mode browser window (for example, Microsoft Edge InPrivate mode or Google Chrome Incognito mode) for each app test run after a change to the app, app configuration, or Azure services in the Azure portal. Lingering cookies from a previous test run can result in failed authentication or authorization when testing the site even when the site's configuration is correct. For more information on how to configure Visual Studio to open a new private browser window for each test run, see the [Cookies and site data](#cookies-and-site-data) section.

When App Service configuration is changed in the Azure portal, the updates generally take effect quickly but aren't instant. Sometimes, you must wait a short period for an App Service to restart in order for a configuration change to take effect.

If troubleshooting an Identity Server key-signing certificate loading problem, execute the following command in an Azure portal [Kudu](https://github.com/projectkudu/kudu/wiki/Accessing-the-kudu-service) PowerShell command shell. The command provides a list of certificates that the app can access from the `CurrentUser` > `My` certificate store. The output includes certificate subjects and thumbprints useful when debugging an app:

```
Get-ChildItem -path Cert:\CurrentUser\My -Recurse | Format-List DnsNameList, Subject, Thumbprint, EnhancedKeyUsageList
```

    * [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)
    * [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)
    * [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

  > info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

```
@page "/user"
@attribute [Authorize]
@using System.Text.Json
@using System.Security.Claims
@inject IAccessTokenProvider AuthorizationService

<h1>@AuthenticatedUser?.Identity?.Name</h1>

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

<h2>Access token</h2>

<p id="access-token">@AccessToken?.Value</p>

<h2>Access token claims</h2>

@foreach (var claim in GetAccessTokenClaims())
{
    <p>@(claim.Key): @claim.Value.ToString()</p>
}

@if (AccessToken != null)
{
    <h2>Access token expires</h2>

    <p>Current time: <span id="current-time">@DateTimeOffset.Now</span></p>
    <p id="access-token-expires">@AccessToken.Expires</p>

    <h2>Access token granted scopes (as reported by the API)</h2>

    @foreach (var scope in AccessToken.GrantedScopes)
    {
        <p>Scope: @scope</p>
    }
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState> AuthenticationState { get; set; }

    public ClaimsPrincipal AuthenticatedUser { get; set; }
    public AccessToken AccessToken { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        var state = await AuthenticationState;
        var accessTokenResult = await AuthorizationService.RequestAccessToken();

        if (!accessTokenResult.TryGetToken(out var token))
        {
            throw new InvalidOperationException(
                "Failed to provision the access token.");
        }

        AccessToken = token;

        AuthenticatedUser = state.User;
    }

    protected IDictionary<string, object> GetAccessTokenClaims()
    {
        if (AccessToken == null)
        {
            return new Dictionary<string, object>();
        }

        // header.payload.signature
        var payload = AccessToken.Value.Split(".")[1];
        var base64Payload = payload.Replace('-', '+').Replace('_', '/')
            .PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');

        return JsonSerializer.Deserialize<IDictionary<string, object>>(
            Convert.FromBase64String(base64Payload));
    }
}
```

### Inspect the content of a JSON Web Token (JWT)

To decode a JSON Web Token (JWT), use Microsoft's [jwt.ms](https://jwt.ms/) tool. Values in the UI never leave your browser.

Example encoded JWT (shortened for display):

> eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1j ... bQdHBHGcQQRbW7Wmo6SWYG4V\_bU55Ug\_PW4pLPr20tTS8Ct7\_uwy9DWrzCMzpD-EiwT5IjXwlGX3IXVjHIlX50IVIydBoPQtadvT7saKo1G5Jmutgq41o-dmz6-yBMKV2\_nXA25Q

Example JWT decoded by the tool for an app that authenticates against Azure AAD B2C:

```
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
  "exp": 1610059429,
  "nbf": 1610055829,
  "ver": "1.0",
  "iss": "https://mysiteb2c.b2clogin.com/11112222-bbbb-3333-cccc-4444dddd5555/v2.0/",
  "sub": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
  "aud": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "nonce": "bbbb0000-cccc-1111-dddd-2222eeee3333",
  "iat": 1610055829,
  "auth_time": 1610055822,
  "idp": "idp.com",
  "tfp": "B2C_1_signupsignin"
}.[Signature]
```

## Related content

* [Deployment to Azure App Service](../../../security/authentication/identity-api-authorization?view=aspnetcore-7.0#deploy-to-production)

* [Import a certificate from Key Vault (Azure documentation)](/en-us/azure/app-service/configure-ssl-certificate#import-a-certificate-from-key-vault)

* [ASP.NET Core Blazor WebAssembly additional security scenarios](additional-scenarios?view=aspnetcore-7.0)

* [Unauthenticated or unauthorized web API requests in an app with a secure default client](additional-scenarios?view=aspnetcore-7.0#unauthenticated-or-unauthorized-web-api-requests-in-an-app-with-a-secure-default-client)

* [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-7.0): Includes guidance on:

  * Using forwarded headers middleware to preserve HTTPS scheme information across proxy servers and internal networks.
  * Additional scenarios and use cases, including manual scheme configuration, request path changes for correct request routing, and forwarding the request scheme for Linux and non-IIS reverse proxies.

* [Duende Identity Server](https://docs.duendesoftware.com)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-11-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0022
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0022.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0022: Route conflict detected between route handlers

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0022      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

[ASP0023](asp0023?view=aspnetcore-10.0) warns on route conflicts in ASP.NET Core MVC apps, this analyzer warns on route conflicts in [Minimal API](../fundamentals/apis?view=aspnetcore-10.0) apps.

This analyzer is intentionally conservative about duplicate routes it reports to avoid false positives:

* The analyzer only reports duplicate routes declared in the same code block in a method. Duplicate routes in different branches of an `if` statement aren't reported because the analyzer can't statically determine which will be used at runtime.
* The analyzer only reports duplicate routes with known metadata. Unknown methods called on a route handler might add new metadata that is then used to customize how the route is matched.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-12-07

----
url: https://learn.microsoft.com/en-us/aspnet/core/client-side/spa-services
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/client-side/spa-services.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use JavaScript Services to Create Single Page Applications in ASP.NET Core

By [Fiyaz Hasan](https://github.com/fiyazbinhasan)

Warning

The features described in this article are obsolete as of ASP.NET Core 3.0. A simpler SPA frameworks integration mechanism is available in the [Microsoft.AspNetCore.SpaServices.Extensions](https://www.nuget.org/packages/Microsoft.AspNetCore.SpaServices.Extensions) NuGet package. For more information, see [\[Announcement\] Obsoleting Microsoft.AspNetCore.SpaServices and Microsoft.AspNetCore.NodeServices](https://github.com/dotnet/AspNetCore/issues/12890).

A Single Page Application (SPA) is a popular type of web application due to its inherent rich user experience. Integrating client-side SPA frameworks or libraries, such as [Angular](https://angular.dev/) or [React](https://react.dev/), with server-side frameworks such as ASP.NET Core can be difficult. JavaScript Services was developed to reduce friction in the integration process. It enables seamless operation between the different client and server technology stacks.

## What is JavaScript Services

JavaScript Services is a collection of client-side technologies for ASP.NET Core. Its goal is to position ASP.NET Core as developers' preferred server-side platform for building SPAs.

JavaScript Services consists of two distinct NuGet packages:

* [Microsoft.AspNetCore.NodeServices](https://www.nuget.org/packages/Microsoft.AspNetCore.NodeServices/) (NodeServices)
* [Microsoft.AspNetCore.SpaServices](https://www.nuget.org/packages/Microsoft.AspNetCore.SpaServices/) (SpaServices)

These packages are useful in the following scenarios:

* Run JavaScript on the server
* Use a SPA framework or library
* Build client-side assets with Webpack

Much of the focus in this article is placed on using the SpaServices package.

## What is SpaServices

SpaServices was created to position ASP.NET Core as developers' preferred server-side platform for building SPAs. SpaServices isn't required to develop SPAs with ASP.NET Core, and it doesn't lock developers into a particular client framework.

SpaServices provides useful infrastructure such as:

* [Server-side prerendering](#server-side-prerendering)
* [Webpack dev middleware](#webpack-dev-middleware)
* [Hot Module Replacement](#hot-module-replacement)
* [Routing helpers](#routing-helpers)

Collectively, these infrastructure components enhance both the development workflow and the runtime experience. The components can be adopted individually.

## Prerequisites for using SpaServices

To work with SpaServices, install the following:

* [Node.js](https://nodejs.org/) (version 6 or later) with npm

  * To verify these components are installed and can be found, run the following from the command line:

    ```
    node -v && npm -v
    ```

  * If deploying to an Azure web site, no action is required—Node.js is installed and available in the server environments.

* [.NET Core SDK 2.0 or later](https://dotnet.microsoft.com/download)

  * On Windows using Visual Studio 2017, the SDK is installed by selecting the **.NET Core cross-platform development** workload.

* [Microsoft.AspNetCore.SpaServices](https://www.nuget.org/packages/Microsoft.AspNetCore.SpaServices/) NuGet package

## Server-side prerendering

A universal (also known as isomorphic) application is a JavaScript application capable of running both on the server and the client. Angular, React, and other popular frameworks provide a universal platform for this application development style. The idea is to first render the framework components on the server via Node.js, and then delegate further execution to the client.

ASP.NET Core [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-3.0) provided by SpaServices simplify the implementation of server-side prerendering by invoking the JavaScript functions on the server.

### Server-side prerendering prerequisites

Install the [aspnet-prerendering](https://www.npmjs.com/package/aspnet-prerendering) npm package:

```
npm i -S aspnet-prerendering
```

### Server-side prerendering configuration

The Tag Helpers are made discoverable via namespace registration in the project's `_ViewImports.cshtml` file:

```
@using SpaServicesSampleApp
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@addTagHelper "*, Microsoft.AspNetCore.SpaServices"
```

These Tag Helpers abstract away the intricacies of communicating directly with low-level APIs by leveraging an HTML-like syntax inside the Razor view:

```
<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
```

### asp-prerender-module Tag Helper

The `asp-prerender-module` Tag Helper, used in the preceding code example, executes `ClientApp/dist/main-server.js` on the server via Node.js. For clarity's sake, `main-server.js` file is an artifact of the TypeScript-to-JavaScript transpilation task in the [Webpack](https://webpack.github.io/) build process. Webpack defines an entry point alias of `main-server`; and, traversal of the dependency graph for this alias begins at the `ClientApp/boot-server.ts` file:

```
entry: { 'main-server': './ClientApp/boot-server.ts' },
```

In the following Angular example, the `ClientApp/boot-server.ts` file utilizes the `createServerRenderer` function and `RenderResult` type of the `aspnet-prerendering` npm package to configure server rendering via Node.js. The HTML markup destined for server-side rendering is passed to a resolve function call, which is wrapped in a strongly-typed JavaScript `Promise` object. The `Promise` object's significance is that it asynchronously supplies the HTML markup to the page for injection in the DOM's placeholder element.

```
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {
    const providers = [
        { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
        { provide: 'ORIGIN_URL', useValue: params.origin }
    ];

    return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
        const appRef = moduleRef.injector.get(ApplicationRef);
        const state = moduleRef.injector.get(PlatformState);
        const zone = moduleRef.injector.get(NgZone);
        
        return new Promise<RenderResult>((resolve, reject) => {
            zone.onError.subscribe(errorInfo => reject(errorInfo));
            appRef.isStable.first(isStable => isStable).subscribe(() => {
                // Because 'onStable' fires before 'onError', we have to delay slightly before
                // completing the request in case there's an error to report
                setImmediate(() => {
                    resolve({
                        html: state.renderToString()
                    });
                    moduleRef.destroy();
                });
            });
        });
    });
});
```

### asp-prerender-data Tag Helper

When coupled with the `asp-prerender-module` Tag Helper, the `asp-prerender-data` Tag Helper can be used to pass contextual information from the Razor view to the server-side JavaScript. For example, the following markup passes user data to the `main-server` module:

```
<app asp-prerender-module="ClientApp/dist/main-server"
        asp-prerender-data='new {
            UserName = "John Doe"
        }'>Loading...</app>
```

The received `UserName` argument is serialized using the built-in JSON serializer and is stored in the `params.data` object. In the following Angular example, the data is used to construct a personalized greeting within an `h1` element:

```
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {
    const providers = [
        { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
        { provide: 'ORIGIN_URL', useValue: params.origin }
    ];

    return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
        const appRef = moduleRef.injector.get(ApplicationRef);
        const state = moduleRef.injector.get(PlatformState);
        const zone = moduleRef.injector.get(NgZone);
        
        return new Promise<RenderResult>((resolve, reject) => {
            const result = `<h1>Hello, ${params.data.userName}</h1>`;

            zone.onError.subscribe(errorInfo => reject(errorInfo));
            appRef.isStable.first(isStable => isStable).subscribe(() => {
                // Because 'onStable' fires before 'onError', we have to delay slightly before
                // completing the request in case there's an error to report
                setImmediate(() => {
                    resolve({
                        html: result
                    });
                    moduleRef.destroy();
                });
            });
        });
    });
});
```

Property names passed in Tag Helpers are represented with **PascalCase** notation. Contrast that to JavaScript, where the same property names are represented with **camelCase**. The default JSON serialization configuration is responsible for this difference.

To expand upon the preceding code example, data can be passed from the server to the view by hydrating the `globals` property provided to the `resolve` function:

```
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';

export default createServerRenderer(params => {
    const providers = [
        { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
        { provide: 'ORIGIN_URL', useValue: params.origin }
    ];

    return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
        const appRef = moduleRef.injector.get(ApplicationRef);
        const state = moduleRef.injector.get(PlatformState);
        const zone = moduleRef.injector.get(NgZone);
        
        return new Promise<RenderResult>((resolve, reject) => {
            const result = `<h1>Hello, ${params.data.userName}</h1>`;

            zone.onError.subscribe(errorInfo => reject(errorInfo));
            appRef.isStable.first(isStable => isStable).subscribe(() => {
                // Because 'onStable' fires before 'onError', we have to delay slightly before
                // completing the request in case there's an error to report
                setImmediate(() => {
                    resolve({
                        html: result,
                        globals: {
                            postList: [
                                'Introduction to ASP.NET Core',
                                'Making apps with Angular and ASP.NET Core'
                            ]
                        }
                    });
                    moduleRef.destroy();
                });
            });
        });
    });
});
```

The `postList` array defined inside the `globals` object is attached to the browser's global `window` object. This variable hoisting to global scope eliminates duplication of effort, particularly as it pertains to loading the same data once on the server and again on the client.

## Webpack dev middleware

[Webpack dev middleware](https://webpack.js.org/guides/development/#using-webpack-dev-middleware) introduces a streamlined development workflow whereby Webpack builds resources on demand. The middleware automatically compiles and serves client-side resources when a page is reloaded in the browser. The alternate approach is to manually invoke Webpack via the project's npm build script when a third-party dependency or the custom code changes. An npm build script in the `package.json` file is shown in the following example:

```
"build": "npm run build:vendor && npm run build:custom",
```

### Webpack dev middleware prerequisites

Install the [aspnet-webpack](https://www.npmjs.com/package/aspnet-webpack) npm package:

```
npm i -D aspnet-webpack
```

### Webpack dev middleware configuration

Webpack dev middleware is registered into the HTTP request pipeline via the following code in the `Startup.cs` file's `Configure` method:

```
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseWebpackDevMiddleware();
}
else
{
    app.UseExceptionHandler("/Home/Error");
}

// Call UseWebpackDevMiddleware before UseStaticFiles
app.UseStaticFiles();
```

The `UseWebpackDevMiddleware` extension method must be called before [registering static file hosting](../fundamentals/static-files?view=aspnetcore-3.0) via the `UseStaticFiles` extension method. For security reasons, register the middleware only when the app runs in development mode.

The `webpack.config.js` file's `output.publicPath` property tells the middleware to watch the `dist` folder for changes:

```
module.exports = (env) => {
        output: {
            filename: '[name].js',
            publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
        },
```

## Hot Module Replacement

Think of Webpack's [Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/) (HMR) feature as an evolution of [webpack dev middleware](#webpack-dev-middleware). HMR introduces all the same benefits, but it further streamlines the development workflow by automatically updating page content after compiling the changes. Don't confuse this with a refresh of the browser, which would interfere with the current in-memory state and debugging session of the SPA. There's a live link between the webpack dev middleware service and the browser, which means changes are pushed to the browser.

### Hot Module Replacement prerequisites

Install the [webpack-hot-middleware](https://www.npmjs.com/package/webpack-hot-middleware) npm package:

```
npm i -D webpack-hot-middleware
```

### Hot Module Replacement configuration

The HMR component must be registered into MVC's HTTP request pipeline in the `Configure` method:

```
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
    HotModuleReplacement = true
});
```

As was true with [webpack dev middleware](#webpack-dev-middleware), the `UseWebpackDevMiddleware` extension method must be called before the `UseStaticFiles` extension method. For security reasons, register the middleware only when the app runs in development mode.

The `webpack.config.js` file must define a `plugins` array, even if it's left empty:

```
module.exports = (env) => {
        plugins: [new CheckerPlugin()]
```

After loading the app in the browser, the developer tools' Console tab provides confirmation of HMR activation:

## Routing helpers

In most ASP.NET Core-based SPAs, client-side routing is often desired in addition to server-side routing. The SPA and MVC routing systems can work independently without interference. There's, however, one edge case posing challenges: identifying 404 HTTP responses.

Consider the scenario in which an extensionless route of `/some/page` is used. Assume the request doesn't pattern-match a server-side route, but its pattern does match a client-side route. Now consider an incoming request for `/images/user-512.png`, which generally expects to find an image file on the server. If that requested resource path doesn't match any server-side route or static file, it's unlikely that the client-side application would handle it—generally returning a 404 HTTP status code is desired.

### Routing helpers prerequisites

Install the client-side routing npm package. Using Angular as an example:

```
npm i -S @angular/router
```

### Routing helpers configuration

An extension method named `MapSpaFallbackRoute` is used in the `Configure` method:

```
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");

    routes.MapSpaFallbackRoute(
        name: "spa-fallback",
        defaults: new { controller = "Home", action = "Index" });
});
```

Routes are evaluated in the order in which they're configured. Consequently, the `default` route in the preceding code example is used first for pattern matching.

## Create a new project

JavaScript Services provide pre-configured application templates. SpaServices is used in these templates in conjunction with different frameworks and libraries such as Angular, React, and Redux.

These templates can be installed via the .NET CLI by running the following command:

```
dotnet new --install Microsoft.AspNetCore.SpaTemplates::*
```

A list of available SPA templates is displayed:

| Templates                                | Short Name | Language | Tags        |
| ---------------------------------------- | ---------- | -------- | ----------- |
| MVC ASP.NET Core with Angular            | angular    | \[C#]    | Web/MVC/SPA |
| MVC ASP.NET Core with React.js           | react      | \[C#]    | Web/MVC/SPA |
| MVC ASP.NET Core with React.js and Redux | reactredux | \[C#]    | Web/MVC/SPA |

To create a new project using one of the SPA templates, include the **Short Name** of the template in the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command. The following command creates an Angular application with ASP.NET Core MVC configured for the server side:

```
dotnet new angular
```

### Set the runtime configuration mode

Two primary runtime configuration modes exist:

* **Development**:

  * Includes source maps to ease debugging.
  * Doesn't optimize the client-side code for performance.

* **Production**:

  * Excludes source maps.
  * Optimizes the client-side code via bundling and minification.

ASP.NET Core uses an environment variable named `ASPNETCORE_ENVIRONMENT` to store the configuration mode. For more information, see [Set the environment](../fundamentals/environments?view=aspnetcore-3.0#set-the-environment).

### Run with .NET CLI

Restore the required NuGet and npm packages by running the following command at the project root:

```
dotnet restore && npm i
```

Build and run the application:

```
dotnet run
```

The application starts on localhost according to the [runtime configuration mode](#set-the-runtime-configuration-mode). Navigating to `http://localhost:5000` in the browser displays the landing page.

### Run with Visual Studio 2017

Open the `.csproj` file generated by the [dotnet new](/en-us/dotnet/core/tools/dotnet-new) command. The required NuGet and npm packages are restored automatically upon project open. This restoration process may take up to a few minutes, and the application is ready to run when it completes. Click the green run button or press `Ctrl + F5`, and the browser opens to the application's landing page. The application runs on localhost according to the [runtime configuration mode](#set-the-runtime-configuration-mode).

## Test the app

SpaServices templates are pre-configured to run client-side tests using [Karma](https://karma-runner.github.io/1.0/index.html) and [Jasmine](https://jasmine.github.io/). Jasmine is a popular unit testing framework for JavaScript, whereas Karma is a test runner for those tests. Karma is configured to work with the [webpack dev middleware](#webpack-dev-middleware) such that the developer isn't required to stop and run the test every time changes are made. Whether it's the code running against the test case or the test case itself, the test runs automatically.

Using the Angular application as an example, two Jasmine test cases are already provided for the `CounterComponent` in the `counter.component.spec.ts` file:

```
it('should display a title', async(() => {
    const titleText = fixture.nativeElement.querySelector('h1').textContent;
    expect(titleText).toEqual('Counter');
}));

it('should start with count 0, then increments by 1 when clicked', async(() => {
    const countElement = fixture.nativeElement.querySelector('strong');
    expect(countElement.textContent).toEqual('0');

    const incrementButton = fixture.nativeElement.querySelector('button');
    incrementButton.click();
    fixture.detectChanges();
    expect(countElement.textContent).toEqual('1');
}));
```

Open the command prompt in the *ClientApp* directory. Run the following command:

```
npm test
```

The script launches the Karma test runner, which reads the settings defined in the `karma.conf.js` file. Among other settings, the `karma.conf.js` identifies the test files to be executed via its `files` array:

```
module.exports = function (config) {
    config.set({
        files: [
            '../../wwwroot/dist/vendor.js',
            './boot-tests.ts'
        ],
```

## Publish the app

See [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/12474) for more information on publishing to Azure.

Combining the generated client-side assets and the published ASP.NET Core artifacts into a ready-to-deploy package can be cumbersome. Thankfully, SpaServices orchestrates that entire publication process with a custom MSBuild target named `RunWebpack`:

```
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
  <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
  <Exec Command="npm install" />
  <Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
  <Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />

  <!-- Include the newly-built files in the publish output -->
  <ItemGroup>
    <DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
    <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
      <RelativePath>%(DistFiles.Identity)</RelativePath>
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </ResolvedFileToPublish>
  </ItemGroup>
</Target>
```

The MSBuild target has the following responsibilities:

1. Restore the npm packages.
2. Create a production-grade build of the third-party, client-side assets.
3. Create a production-grade build of the custom client-side assets.
4. Copy the Webpack-generated assets to the publish folder.

The MSBuild target is invoked when running:

```
dotnet publish -c Release
```

## Related content

* [Angular Docs](https://angular.dev/docs)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-06

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/tutorials/wpf
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/tutorials/wpf.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Build a Windows Presentation Foundation (WPF) Blazor app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This tutorial shows you how to build and run a WPF Blazor app. You learn how to:

* Create a WPF Blazor app project
* Add a Razor component to the project
* Run the app on Windows

## Prerequisites

* [Supported platforms (WPF documentation)](/en-us/dotnet/desktop/wpf/overview/)
* [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) with the **.NET desktop development** workload

## Visual Studio workload

If the **.NET desktop development** workload isn't installed, use the Visual Studio installer to install the workload. For more information, see [Modify Visual Studio workloads, components, and language packs](/en-us/visualstudio/install/modify-visual-studio).

## Create a WPF Blazor project

Launch Visual Studio. In the **Start Window**, select **Create a new project**:

In the **Create a new project** dialog, filter the **Project type** dropdown to **Desktop**. Select the C# project template for **WPF Application** and select the **Next** button:

In the **Configure your new project** dialog:

* Set the **Project name** to **WpfBlazor**.
* Choose a suitable location for the project.
* Select the **Next** button.

In the **Additional information** dialog, select the framework version with the **Framework** dropdown list. Select the **Create** button:

Use [NuGet Package Manager](/en-us/nuget/consume-packages/install-use-packages-visual-studio) to install the [`Microsoft.AspNetCore.Components.WebView.Wpf`](https://nuget.org/packages/Microsoft.AspNetCore.Components.WebView.Wpf) NuGet package:

In **Solution Explorer**, right-click the project's name, **WpfBlazor**, and select **Edit Project File** to open the project file (`WpfBlazor.csproj`).

At the top of the project file, change the SDK to `Microsoft.NET.Sdk.Razor`:

```
<Project Sdk="Microsoft.NET.Sdk.Razor">
```

In the project file's existing `<TargetFramework>` property, add a Windows 10 or later version number:

```
<TargetFramework>net10.0-windows10.0.17763.0</TargetFramework>
```

Note

The preceding guidance on setting the project's target framework version to target Windows 10 or later is a result of switching to the `WebView2CompositionControl` with the release of .NET 10.

In the project file's existing `<PropertyGroup>` add the following markup to set the app's root namespace, which is `WpfBlazor` in this tutorial:

```
<RootNamespace>WpfBlazor</RootNamespace>
```

Note

The preceding guidance on setting the project's root namespace is a temporary workaround. For more information, see \[[Blazor\]\[Wpf\] Root namespace related issue (dotnet/maui #5861)](https://github.com/dotnet/maui/issues/5861).

Save the changes to the project file (`WpfBlazor.csproj`).

Add an `_Imports.razor` file to the root of the project with an [`@using`](../../../mvc/views/razor?view=aspnetcore-10.0#using) directive for [Microsoft.AspNetCore.Components.Web](/en-us/dotnet/api/microsoft.aspnetcore.components.web).

`_Imports.razor`:

```
@using Microsoft.AspNetCore.Components.Web
```

Save the `_Imports.razor` file.

Add a `wwwroot` folder to the project.

Add an `index.html` file to the `wwwroot` folder with the following markup.

`wwwroot/index.html`:

```
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WpfBlazor</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="WpfBlazor.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui" data-nosnippet>
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webview.js"></script>
</body>

</html>
```

Inside the `wwwroot` folder, create a `css` folder.

Add an `app.css` stylesheet to the `wwwroot/css` folder with the following content.

`wwwroot/css/app.css`:

```
html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1:focus {
    outline: none;
}

a, .btn-link {
    color: #0071c1;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }
```

Inside the `wwwroot/css` folder, create a `bootstrap` folder. Inside the `bootstrap` folder, place a copy of `bootstrap.min.css`. You can obtain the latest version of `bootstrap.min.css` from the [Bootstrap website](https://getbootstrap.com/). Follow navigation bar links to **Docs** > **Download**. A direct link can't be provided here because all of the content at the site is versioned in the URL.

Add the following `Counter` component to the root of the project, which is the default `Counter` component found in Blazor project templates.

`Counter.razor`:

```
<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
```

Save the `Counter` component (`Counter.razor`).

If the `MainWindow` designer isn't open, open it by double-clicking the `MainWindow.xaml` file in **Solution Explorer**. In the `MainWindow` designer, replace the XAML code with the following:

```
<Window x:Class="WpfBlazor.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
        xmlns:local="clr-namespace:WpfBlazor"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">
            <blazor:BlazorWebView.RootComponents>
                <blazor:RootComponent Selector="#app" ComponentType="{x:Type local:Counter}" />
            </blazor:BlazorWebView.RootComponents>
        </blazor:BlazorWebView>
    </Grid>
</Window>
```

In **Solution Explorer**, right-click `MainWindow.xaml` and select **View Code**:

Add the namespace [Microsoft.Extensions.DependencyInjection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection) to the top of the `MainWindow.xaml.cs` file:

```
using Microsoft.Extensions.DependencyInjection;
```

Inside the `MainWindow` constructor, after the `InitializeComponent` method call, add the following code:

```
var serviceCollection = new ServiceCollection();
serviceCollection.AddWpfBlazorWebView();
Resources.Add("services", serviceCollection.BuildServiceProvider());
```

Note

The `InitializeComponent` method is automatically generated at app build time and added to the compilation object for the calling class.

The final, complete C# code of `MainWindow.xaml.cs` with a [file-scoped namespace](/en-us/dotnet/csharp/language-reference/keywords/namespace) and comments removed:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Extensions.DependencyInjection;

namespace WpfBlazor;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        var serviceCollection = new ServiceCollection();
        serviceCollection.AddWpfBlazorWebView();
        Resources.Add("services", serviceCollection.BuildServiceProvider());
    }
}
```

## Run the app

Select the start button in the Visual Studio toolbar:

The app running on Windows:

## Next steps

In this tutorial, you learned how to:

* Create a WPF Blazor app project
* Add a Razor component to the project

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/dynamiccomponent
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/dynamiccomponent.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Dynamically-rendered ASP.NET Core Razor components

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Dave Brock](https://twitter.com/daveabrock)

Use the built-in [DynamicComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent) component to render components by type.

## Dynamic components

A [DynamicComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent) is useful for rendering components without iterating through possible types or using conditional logic. For example, [DynamicComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent) can render a component based on a user selection from a dropdown list.

In the following example:

* `componentType` specifies the type.
* `parameters` specifies component parameters to pass to the `componentType` component.

```
<DynamicComponent Type="componentType" Parameters="parameters" />

@code {
    private Type componentType = ...;
    private IDictionary<string, object> parameters = ...;
}
```

For more information on passing parameter values, see the [Pass parameters](#pass-parameters) section later in this article.

## Example

In the following example, a Razor component renders a component based on the user's selection from a dropdown list of four possible values.

| User spaceflight carrier selection | Shared Razor component to render |
| ---------------------------------- | -------------------------------- |
| Rocket Lab®                        | `RocketLab.razor`                |
| SpaceX®                            | `SpaceX.razor`                   |
| ULA®                               | `UnitedLaunchAlliance.razor`     |
| Virgin Galactic®                   | `VirginGalactic.razor`           |

`RocketLab.razor`:

```
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>
```

`SpaceX.razor`:

```
<h2>SpaceX®</h2>

<p>
    SpaceX is a registered trademark of 
    <a href="https://www.spacex.com/">Space Exploration Technologies Corp.</a>
</p>
```

`UnitedLaunchAlliance.razor`:

```
<h2>United Launch Alliance®</h2>

<p>
    United Launch Alliance and ULA are registered trademarks of
    <a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>
```

`VirginGalactic.razor`:

```
<h2>Virgin Galactic®</h2>

<p>
    Virgin Galactic is a registered trademark of 
    <a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>
```

`DynamicComponent1.razor`:

```
@page "/dynamic-component-1"

<PageTitle>Dynamic Component 1</PageTitle>

<h1>Dynamic Component Example 1</h1>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var entry in components.Keys)
            {
                <option value="@entry">@entry</option>
            }
        </select>
    </label>
</p>

@if (selectedType is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedType" />
    </div>
}

@code {
    private readonly Dictionary<string, Type> components = new()
    {
        ["Rocket Lab"] = typeof(RocketLab),
        ["SpaceX"] = typeof(SpaceX),
        ["ULA"] = typeof(UnitedLaunchAlliance),
        ["Virgin Galactic"] = typeof(VirginGalactic)
    };
    private Type? selectedType;

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && 
            !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedType = components[dropdownValue];
        }
        else
        {
            selectedType = null;
        }
    }
}
```

`RocketLab.razor`:

```
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>
```

`SpaceX.razor`:

```
<h2>SpaceX®</h2>

<p>
    SpaceX is a registered trademark of 
    <a href="https://www.spacex.com/">Space Exploration Technologies Corp.</a>
</p>
```

`UnitedLaunchAlliance.razor`:

```
<h2>United Launch Alliance®</h2>

<p>
    United Launch Alliance and ULA are registered trademarks of
    <a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>
```

`VirginGalactic.razor`:

```
<h2>Virgin Galactic®</h2>

<p>
    Virgin Galactic is a registered trademark of 
    <a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>
```

`DynamicComponent1.razor`:

```
@page "/dynamic-component-1"

<PageTitle>Dynamic Component 1</PageTitle>

<h1>Dynamic Component Example 1</h1>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var entry in components.Keys)
            {
                <option value="@entry">@entry</option>
            }
        </select>
    </label>
</p>

@if (selectedType is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedType" />
    </div>
}

@code {
    private readonly Dictionary<string, Type> components = new()
    {
        ["Rocket Lab"] = typeof(RocketLab),
        ["SpaceX"] = typeof(SpaceX),
        ["ULA"] = typeof(UnitedLaunchAlliance),
        ["Virgin Galactic"] = typeof(VirginGalactic)
    };
    private Type? selectedType;

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && 
            !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedType = components[dropdownValue];
        }
        else
        {
            selectedType = null;
        }
    }
}
```

`RocketLab.razor`:

```
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>
```

`SpaceX.razor`:

```
<h2>SpaceX®</h2>

<p>
    SpaceX is a registered trademark of 
    <a href="https://www.spacex.com/">Space Exploration Technologies Corp.</a>
</p>
```

`UnitedLaunchAlliance.razor`:

```
<h2>United Launch Alliance®</h2>

<p>
    United Launch Alliance and ULA are registered trademarks of
    <a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>
```

`VirginGalactic.razor`:

```
<h2>Virgin Galactic®</h2>

<p>
    Virgin Galactic is a registered trademark of 
    <a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>
```

`DynamicComponentExample1.razor`:

```
@page "/dynamiccomponent-example-1"

<h1><code>DynamicComponent</code> Component Example 1</h1>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var entry in components.Keys)
            {
                <option value="@entry">@entry</option>
            }
        </select>
    </label>
</p>

@if (selectedType is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedType" />
    </div>
}

@code {
    private readonly Dictionary<string, Type> components = new()
    {
        ["Rocket Lab"] = typeof(RocketLab),
        ["SpaceX"] = typeof(SpaceX),
        ["ULA"] = typeof(UnitedLaunchAlliance),
        ["Virgin Galactic"] = typeof(VirginGalactic)
    };
    private Type? selectedType;

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedType = components[dropdownValue];
        }
        else
        {
            selectedType = null;
        }
    }
}
```

`RocketLab.razor`:

```
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>
```

`SpaceX.razor`:

```
<h2>SpaceX®</h2>

<p>
    SpaceX is a registered trademark of 
    <a href="https://www.spacex.com/">Space Exploration Technologies Corp.</a>
</p>
```

`UnitedLaunchAlliance.razor`:

```
<h2>United Launch Alliance®</h2>

<p>
    United Launch Alliance and ULA are registered trademarks of
    <a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>
```

`VirginGalactic.razor`:

```
<h2>Virgin Galactic®</h2>

<p>
    Virgin Galactic is a registered trademark of 
    <a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>
```

`DynamicComponentExample1.razor`:

```
@page "/dynamiccomponent-example-1"

<h1><code>DynamicComponent</code> Component Example 1</h1>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var entry in components.Keys)
            {
                <option value="@entry">@entry</option>
            }
        </select>
    </label>
</p>

@if (selectedType is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedType" />
    </div>
}

@code {
    private readonly Dictionary<string, Type> components = new()
    {
        ["Rocket Lab"] = typeof(RocketLab),
        ["SpaceX"] = typeof(SpaceX),
        ["ULA"] = typeof(UnitedLaunchAlliance),
        ["Virgin Galactic"] = typeof(VirginGalactic)
    };
    private Type? selectedType;

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedType = components[dropdownValue];
        }
        else
        {
            selectedType = null;
        }
    }
}
```

In the preceding example:

* An [Dictionary\<TKey,TValue>](/en-us/dotnet/api/system.collections.generic.dictionary-2) is used to manage components to be displayed.
* Names serve as the dictionary keys and are provided as selection options.
* Component types are stored as dictionary values using the [`typeof` operator](/en-us/dotnet/csharp/language-reference/operators/type-testing-and-cast#typeof-operator).

## Pass parameters

If dynamically-rendered components have [component parameters](./?view=aspnetcore-10.0#component-parameters), pass them into the [DynamicComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent) as an `IDictionary<string, object>`. The `string` is the name of the parameter, and the `object` is the parameter's value.

The following example configures a component metadata object (`ComponentMetadata`) to supply parameter values to dynamically-rendered components based on the type name. The example is just one of several approaches that you can adopt. Parameter data can also be provided from a web API, a database, or a method. The only requirement is that the approach returns an `IDictionary<string, object>`.

`ComponentMetadata.cs`:

```
namespace BlazorSample;

public class ComponentMetadata
{
    public required Type Type { get; init; }
    public required string Name { get; init; }
    public Dictionary<string, object> Parameters { get; } = [];
}
```

```
namespace BlazorSample;

public class ComponentMetadata
{
    public required Type Type { get; init; }
    public required string Name { get; init; }
    public Dictionary<string, object> Parameters { get; } = [];
}
```

```
public class ComponentMetadata
{
    public required Type Type { get; init; }
    public required string Name { get; init; }
    public Dictionary<string, object> Parameters { get; } = new();
}
```

```
public class ComponentMetadata
{
    public Type? Type { get; init; }
    public string? Name { get; init; }
    public Dictionary<string, object> Parameters { get; } = new();
}
```

The following `RocketLabWithWindowSeat` component (`RocketLabWithWindowSeat.razor`) has been updated from the preceding example to include a component parameter named `WindowSeat` to specify if the passenger prefers a window seat on their flight:

`RocketLabWithWindowSeat.razor`:

```
<h2>Rocket Lab®</h2>

<p>
    User selected a window seat: @WindowSeat
</p>

<p>
    Rocket Lab is a trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

@code {
    [Parameter]
    public bool WindowSeat { get; set; }
}
```

```
<h2>Rocket Lab®</h2>

<p>
    User selected a window seat: @WindowSeat
</p>

<p>
    Rocket Lab is a trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

@code {
    [Parameter]
    public bool WindowSeat { get; set; }
}
```

```
<h2>Rocket Lab®</h2>

<p>
    User selected a window seat: @WindowSeat
</p>

<p>
    Rocket Lab is a trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

@code {
    [Parameter]
    public bool WindowSeat { get; set; }
}
```

```
<h2>Rocket Lab®</h2>

<p>
    User selected a window seat: @WindowSeat
</p>

<p>
    Rocket Lab is a trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

@code {
    [Parameter]
    public bool WindowSeat { get; set; }
}
```

In the following example:

* Only the `RocketLabWithWindowSeat` component's parameter for a window seat (`WindowSeat`) receives the value of the **`Window Seat`** checkbox.

* Component names are used as dictionary keys using the [`nameof` operator](/en-us/dotnet/csharp/language-reference/operators/nameof), which returns component names as constant strings.

* The dynamically-rendered components are shared components:

  * Shown in this article section: `RocketLabWithWindowSeat` (`RocketLabWithWindowSeat.razor`)

  * Components shown in the [Example](#example) section earlier in this article:

    * `SpaceX` (`SpaceX.razor`)
    * `UnitedLaunchAlliance` (`UnitedLaunchAlliance.razor`)
    * `VirginGalactic` (`VirginGalactic.razor`)

`DynamicComponent2.razor`:

```
@page "/dynamic-component-2"

<PageTitle>Dynamic Component 2</PageTitle>

<h1>Dynamic Component Example 2</h1>

<p>
    <label>
        <input type="checkbox" @bind="windowSeat" 
            @bind:after="HandleWindowSeatChanged" />
        Window Seat (Rocket Lab only)
    </label>
</p>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var c in components)
            {
                <option value="@c.Key">@c.Value.Name</option>
            }
        </select>
    </label>
</p>

@if (selectedComponent is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedComponent.Type"
            Parameters="selectedComponent.Parameters" />
    </div>
}

@code {
    private Dictionary<string, ComponentMetadata> components =
        new()
        {
            [nameof(RocketLabWithWindowSeat)] = new ComponentMetadata()
            {
                Type = typeof(RocketLabWithWindowSeat),
                Name = "Rocket Lab with Window Seat",
                Parameters = { [nameof(RocketLabWithWindowSeat.WindowSeat)] = false }
            },
            [nameof(VirginGalactic)] = new ComponentMetadata()
            {
                Type = typeof(VirginGalactic),
                Name = "Virgin Galactic"
            },
            [nameof(UnitedLaunchAlliance)] = new ComponentMetadata()
            {
                Type = typeof(UnitedLaunchAlliance),
                Name = "ULA"
            },
            [nameof(SpaceX)] = new ComponentMetadata()
            {
                Type = typeof(SpaceX),
                Name = "SpaceX"
            }
        };
    private ComponentMetadata? selectedComponent;
    private bool windowSeat;

    private void HandleWindowSeatChanged()
    {
        components[nameof(RocketLabWithWindowSeat)]
            .Parameters[nameof(RocketLabWithWindowSeat.WindowSeat)] = windowSeat;
    }

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedComponent = components[dropdownValue];
        }
        else
        {
            selectedComponent = null;
        }
    }
}
```

`DynamicComponent2.razor`:

```
@page "/dynamic-component-2"

<PageTitle>Dynamic Component 2</PageTitle>

<h1>Dynamic Component Example 2</h1>

<p>
    <label>
        <input type="checkbox" @bind="windowSeat" 
            @bind:after="HandleWindowSeatChanged" />
        Window Seat (Rocket Lab only)
    </label>
</p>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var c in components)
            {
                <option value="@c.Key">@c.Value.Name</option>
            }
        </select>
    </label>
</p>

@if (selectedComponent is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedComponent.Type"
            Parameters="selectedComponent.Parameters" />
    </div>
}

@code {
    private Dictionary<string, ComponentMetadata> components =
        new()
        {
            [nameof(RocketLabWithWindowSeat)] = new ComponentMetadata()
            {
                Type = typeof(RocketLabWithWindowSeat),
                Name = "Rocket Lab with Window Seat",
                Parameters = { [nameof(RocketLabWithWindowSeat.WindowSeat)] = false }
            },
            [nameof(VirginGalactic)] = new ComponentMetadata()
            {
                Type = typeof(VirginGalactic),
                Name = "Virgin Galactic"
            },
            [nameof(UnitedLaunchAlliance)] = new ComponentMetadata()
            {
                Type = typeof(UnitedLaunchAlliance),
                Name = "ULA"
            },
            [nameof(SpaceX)] = new ComponentMetadata()
            {
                Type = typeof(SpaceX),
                Name = "SpaceX"
            }
        };
    private ComponentMetadata? selectedComponent;
    private bool windowSeat;

    private void HandleWindowSeatChanged()
    {
        components[nameof(RocketLabWithWindowSeat)]
            .Parameters[nameof(RocketLabWithWindowSeat.WindowSeat)] = windowSeat;
    }

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedComponent = components[dropdownValue];
        }
        else
        {
            selectedComponent = null;
        }
    }
}
```

`DynamicComponentExample2.razor`:

```
@page "/dynamiccomponent-example-2"

<h1><code>DynamicComponent</code> Component Example 2</h1>

<p>
    <label>
        <input type="checkbox" @bind="windowSeat" @bind:after="HandleWindowSeatChanged" />
        Window Seat (Rocket Lab only)
    </label>
</p>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var c in components)
            {
                <option value="@c.Key">@c.Value.Name</option>
            }
        </select>
    </label>
</p>

@if (selectedComponent is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedComponent.Type"
            Parameters="selectedComponent.Parameters" />
    </div>
}

@code {
    private Dictionary<string, ComponentMetadata> components =
        new()
        {
            [nameof(RocketLabWithWindowSeat)] = new ComponentMetadata()
            {
                Type = typeof(RocketLabWithWindowSeat),
                Name = "Rocket Lab with Window Seat",
                Parameters = { [nameof(RocketLabWithWindowSeat.WindowSeat)] = false }
            },
            [nameof(VirginGalactic)] = new ComponentMetadata()
            {
                Type = typeof(VirginGalactic),
                Name = "Virgin Galactic"
            },
            [nameof(UnitedLaunchAlliance)] = new ComponentMetadata()
            {
                Type = typeof(UnitedLaunchAlliance),
                Name = "ULA"
            },
            [nameof(SpaceX)] = new ComponentMetadata()
            {
                Type = typeof(SpaceX),
                Name = "SpaceX"
            }
        };
    private ComponentMetadata? selectedComponent;
    private bool windowSeat;

    private void HandleWindowSeatChanged()
    {
        components[nameof(RocketLabWithWindowSeat)]
            .Parameters[nameof(RocketLabWithWindowSeat.WindowSeat)] = windowSeat;
    }

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedComponent = components[dropdownValue];
        }
        else
        {
            selectedComponent = null;
        }
    }
}
```

`DynamicComponentExample2.razor`:

```
@page "/dynamiccomponent-example-2"

<h1><code>DynamicComponent</code> Component Example 2</h1>

<p>
    <label>
        <input type="checkbox" @bind="WindowSeat" />
        Window Seat (Rocket Lab only)
    </label>
</p>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var c in components)
            {
                <option value="@c.Key">@c.Value.Name</option>
            }
        </select>
    </label>
</p>

@if (selectedComponent is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedComponent.Type"
            Parameters="selectedComponent.Parameters" />
    </div>
}

@code {
    private Dictionary<string, ComponentMetadata> components =
        new()
        {
            [nameof(RocketLabWithWindowSeat)] = new ComponentMetadata()
            {
                Type = typeof(RocketLabWithWindowSeat),
                Name = "Rocket Lab with Window Seat",
                Parameters = { [nameof(RocketLabWithWindowSeat.WindowSeat)] = false }
            },
            [nameof(VirginGalactic)] = new ComponentMetadata()
            {
                Type = typeof(VirginGalactic),
                Name = "Virgin Galactic"
            },
            [nameof(UnitedLaunchAlliance)] = new ComponentMetadata()
            {
                Type = typeof(UnitedLaunchAlliance),
                Name = "ULA"
            },
            [nameof(SpaceX)] = new ComponentMetadata()
            {
                Type = typeof(SpaceX),
                Name = "SpaceX"
            }
        };
    private ComponentMetadata? selectedComponent;
    private bool windowSeat;

    private bool WindowSeat
    {
        get { return windowSeat; }
        set
        {
            windowSeat = value;
            components[nameof(RocketLabWithWindowSeat)]
                .Parameters[nameof(RocketLabWithWindowSeat.WindowSeat)] = windowSeat;
        }
    }

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedComponent = components[dropdownValue];
        }
        else
        {
            selectedComponent = null;
        }
    }
}
```

Dictionary values for [C# union-typed parameters](/en-us/dotnet/csharp/language-reference/builtin-types/union) must be boxed as the union, not the raw case value, because an implicit conversion is only valid at compile-time.

Note

During the preview of .NET 11, the following example requires the app's project file to specify the "`preview`" C# language version:

```
<LangVersion>preview</LangVersion>
```

Consider the following C# union type, `SlotContent`, that accepts text, a [MarkupString](/en-us/dotnet/api/microsoft.aspnetcore.components.markupstring), or a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment):

```
using Microsoft.AspNetCore.Components;

public union SlotContent(string, MarkupString, RenderFragment);
```

The following `Slot` component exposes a component parameter typed to the `SlotContent` union and renders the union's content using two implementations of the [C# `switch` expression](/en-us/dotnet/csharp/language-reference/operators/switch-expression), one through an assignment to a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment) (`ContentSwitch`) and one via an inline `switch` expression. In the following example, the [`[EditorRequired]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.editorrequiredattribute) specifies that `Content` is a required component parameter at design-time or build-time.

`Slot.razor`:

```
<h2><code>ContentSwitch</code></h2>

<div>
    @ContentSwitch()
</div>

<h2>Inline <code>switch</code> expression</h2>

<div>
    @switch (Content)
    {
        case string text:
            @text
            break;
        case MarkupString html:
            @html
            break;
        case RenderFragment fragment:
            @fragment
            break;
        default:
            <em>Empty slot!</em>
            break;
    }
</div>

@code {
    [Parameter, EditorRequired] 
    public SlotContent Content { get; set; }

    private RenderFragment ContentSwitch() => Content switch
    {
        string text => @<span>@text</span>,
        MarkupString html => @<span>@html</span>,
        RenderFragment fragment => fragment,
        _ => @<em>Empty slot!</em>
    };
}
```

To box the union, wrap the raw value explicitly into the union wrapper type (`new SlotContent(...)`), as the following example demonstrates.

`SlotTest2.razor`:

```
@page "/slot-test-2"

<DynamicComponent Type="@TargetComponent" Parameters="@ValidParameters" />

@code {
    [Parameter]
    public Type TargetComponent { get; set; } = typeof(Slot);

    [Parameter]
    public string RawTextPayload { get; set; } = "Hello from dynamic data!";

    private Dictionary<string, object> ValidParameters => new()
    {
        { "Content", new SlotContent(RawTextPayload) }
    };
}
```

## Event callbacks (`EventCallback`)

Event callbacks ([EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback)) can be passed to a [DynamicComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent) in its parameter dictionary.

`ComponentMetadata.cs`:

```
namespace BlazorSample;

public class ComponentMetadata
{
    public required Type Type { get; init; }
    public required string Name { get; init; }
    public Dictionary<string, object> Parameters { get; } = [];
}
```

```
namespace BlazorSample;

public class ComponentMetadata
{
    public required Type Type { get; init; }
    public required string Name { get; init; }
    public Dictionary<string, object> Parameters { get; } = [];
}
```

```
public class ComponentMetadata
{
    public required Type Type { get; init; }
    public required string Name { get; init; }
    public Dictionary<string, object> Parameters { get; } = new();
}
```

```
public class ComponentMetadata
{
    public Type? Type { get; init; }
    public string? Name { get; init; }
    public Dictionary<string, object> Parameters { get; } = new();
}
```

Implement an event callback parameter ([EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback)) within each dynamically-rendered component.

`RocketLab2.razor`:

```
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

`SpaceX2.razor`:

```
<h2>SpaceX®</h2>

<p>
    SpaceX is a registered trademark of
    <a href="https://www.spacex.com/">Space Exploration Technologies Corp.</a>
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>SpaceX®</h2>

<p>
    SpaceX is a registered trademark of
    <a href="https://www.spacex.com/">Space Exploration Technologies Corp.</a>
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>SpaceX®</h2>

<p>
    SpaceX is a registered trademark of
    <a href="https://www.spacex.com/">Space Exploration Technologies Corp.</a>
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>SpaceX®</h2>

<p>
    SpaceX is a registered trademark of
    <a href="https://www.spacex.com/">Space Exploration Technologies Corp.</a>
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

`UnitedLaunchAlliance2.razor`:

```
<h2>United Launch Alliance®</h2>

<p>
    United Launch Alliance and ULA are registered trademarks of
    <a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>United Launch Alliance®</h2>

<p>
    United Launch Alliance and ULA are registered trademarks of
    <a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>United Launch Alliance®</h2>

<p>
    United Launch Alliance and ULA are registered trademarks of
    <a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>United Launch Alliance®</h2>

<p>
    United Launch Alliance and ULA are registered trademarks of
    <a href="https://www.ulalaunch.com/">United Launch Alliance, LLC</a>.
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

`VirginGalactic2.razor`:

```
<h2>Virgin Galactic®</h2>

<p>
    Virgin Galactic is a registered trademark of
    <a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>Virgin Galactic®</h2>

<p>
    Virgin Galactic is a registered trademark of
    <a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>Virgin Galactic®</h2>

<p>
    Virgin Galactic is a registered trademark of
    <a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

```
<h2>Virgin Galactic®</h2>

<p>
    Virgin Galactic is a registered trademark of
    <a href="https://www.virgingalactic.com/">Galactic Enterprises, LLC</a>.
</p>

<button @onclick="OnClickCallback">
    Trigger a Parent component method
</button>

@code {
    [Parameter]
    public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
```

In the following parent component example, the `ShowDTMessage` method assigns a string with the current time to `message`, and the value of `message` is rendered.

The parent component passes the callback method, `ShowDTMessage` in the parameter dictionary:

* The `string` key is the callback method's name, `OnClickCallback`.
* The `object` value is created by [EventCallbackFactory.Create](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallbackfactory.create) for the parent callback method, `ShowDTMessage`. Note that the [`this` keyword](/en-us/dotnet/csharp/language-reference/keywords/this) isn't supported in C# field initialization, so a C# property is used for the parameter dictionary.

`DynamicComponent3.razor`:

```
@page "/dynamic-component-3"

<PageTitle>Dynamic Component 3</PageTitle>

<h1>Dynamic Component Example 3</h1>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var c in Components)
            {
                <option value="@c.Key">@c.Value.Name</option>
            }
        </select>
    </label>
</p>

@if (selectedComponent is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedComponent.Type"
            Parameters="selectedComponent.Parameters" />
    </div>
}

<p>
    @message
</p>

@code {
    private ComponentMetadata? selectedComponent;
    private string? message;

    private Dictionary<string, ComponentMetadata> Components =>
        new()
        {
            [nameof(RocketLab2)] = new ComponentMetadata()
            {
                Type = typeof(RocketLab2),
                Name = "Rocket Lab",
                Parameters = { [nameof(RocketLab2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(VirginGalactic2)] = new ComponentMetadata()
            {
                Type = typeof(VirginGalactic2),
                Name = "Virgin Galactic",
                Parameters = { [nameof(VirginGalactic2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(UnitedLaunchAlliance2)] = new ComponentMetadata()
            {
                Type = typeof(UnitedLaunchAlliance2),
                Name = "ULA",
                Parameters = { [nameof(UnitedLaunchAlliance2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(SpaceX2)] = new ComponentMetadata()
            {
                Type = typeof(SpaceX2),
                Name = "SpaceX",
                Parameters = { [nameof(SpaceX2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            }
        };

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedComponent = Components[dropdownValue];
        }
        else
        {
            selectedComponent = null;
        }
    }

    private void ShowDTMessage(MouseEventArgs e) =>
        message = $"The current DT is: {DateTime.Now}.";
}
```

`DynamicComponent3.razor`:

```
@page "/dynamic-component-3"

<PageTitle>Dynamic Component 3</PageTitle>

<h1>Dynamic Component Example 3</h1>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var c in Components)
            {
                <option value="@c.Key">@c.Value.Name</option>
            }
        </select>
    </label>
</p>

@if (selectedComponent is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedComponent.Type"
            Parameters="selectedComponent.Parameters" />
    </div>
}

<p>
    @message
</p>

@code {
    private ComponentMetadata? selectedComponent;
    private string? message;

    private Dictionary<string, ComponentMetadata> Components =>
        new()
        {
            [nameof(RocketLab2)] = new ComponentMetadata()
            {
                Type = typeof(RocketLab2),
                Name = "Rocket Lab",
                Parameters = { [nameof(RocketLab2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(VirginGalactic2)] = new ComponentMetadata()
            {
                Type = typeof(VirginGalactic2),
                Name = "Virgin Galactic",
                Parameters = { [nameof(VirginGalactic2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(UnitedLaunchAlliance2)] = new ComponentMetadata()
            {
                Type = typeof(UnitedLaunchAlliance2),
                Name = "ULA",
                Parameters = { [nameof(UnitedLaunchAlliance2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(SpaceX2)] = new ComponentMetadata()
            {
                Type = typeof(SpaceX2),
                Name = "SpaceX",
                Parameters = { [nameof(SpaceX2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            }
        };

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedComponent = Components[dropdownValue];
        }
        else
        {
            selectedComponent = null;
        }
    }

    private void ShowDTMessage(MouseEventArgs e) =>
        message = $"The current DT is: {DateTime.Now}.";
}
```

`DynamicComponentExample3.razor`:

```
@page "/dynamiccomponent-example-3"

<h1><code>DynamicComponent</code> Component Example 3</h1>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var c in Components)
            {
                <option value="@c.Key">@c.Value.Name</option>
            }
        </select>
    </label>
</p>

@if (selectedComponent is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedComponent.Type"
            Parameters="selectedComponent.Parameters" />
    </div>
}

<p>
    @message
</p>

@code {
    private ComponentMetadata? selectedComponent;
    private string? message;

    private Dictionary<string, ComponentMetadata> Components =>
        new()
        {
            [nameof(RocketLab2)] = new ComponentMetadata()
            {
                Type = typeof(RocketLab2),
                Name = "Rocket Lab",
                Parameters = { [nameof(RocketLab2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(VirginGalactic2)] = new ComponentMetadata()
            {
                Type = typeof(VirginGalactic2),
                Name = "Virgin Galactic",
                Parameters = { [nameof(VirginGalactic2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(UnitedLaunchAlliance2)] = new ComponentMetadata()
            {
                Type = typeof(UnitedLaunchAlliance2),
                Name = "ULA",
                Parameters = { [nameof(UnitedLaunchAlliance2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(SpaceX2)] = new ComponentMetadata()
            {
                Type = typeof(SpaceX2),
                Name = "SpaceX",
                Parameters = { [nameof(SpaceX2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            }
        };

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedComponent = Components[dropdownValue];
        }
        else
        {
            selectedComponent = null;
        }
    }

    private void ShowDTMessage(MouseEventArgs e) =>
        message = $"The current DT is: {DateTime.Now}.";
}
```

`DynamicComponentExample3.razor`:

```
@page "/dynamiccomponent-example-3"

<h1><code>DynamicComponent</code> Component Example 3</h1>

<p>
    <label>
        Select your transport:
        <select @onchange="OnDropdownChange">
            <option value="">Select a value</option>
            @foreach (var c in Components)
            {
                <option value="@c.Key">@c.Value.Name</option>
            }
        </select>
    </label>
</p>

@if (selectedComponent is not null)
{
    <div class="border border-primary my-1 p-1">
        <DynamicComponent Type="selectedComponent.Type"
            Parameters="selectedComponent.Parameters" />
    </div>
}

<p>
    @message
</p>

@code {
    private ComponentMetadata? selectedComponent;
    private string? message;

    private Dictionary<string, ComponentMetadata> Components =>
        new()
        {
            [nameof(RocketLab2)] = new ComponentMetadata()
            {
                Type = typeof(RocketLab2),
                Name = "Rocket Lab",
                Parameters = { [nameof(RocketLab2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(VirginGalactic2)] = new ComponentMetadata()
            {
                Type = typeof(VirginGalactic2),
                Name = "Virgin Galactic",
                Parameters = { [nameof(VirginGalactic2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(UnitedLaunchAlliance2)] = new ComponentMetadata()
            {
                Type = typeof(UnitedLaunchAlliance2),
                Name = "ULA",
                Parameters = { [nameof(UnitedLaunchAlliance2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            },
            [nameof(SpaceX2)] = new ComponentMetadata()
            {
                Type = typeof(SpaceX2),
                Name = "SpaceX",
                Parameters = { [nameof(SpaceX2.OnClickCallback)] =
                    EventCallback.Factory.Create<MouseEventArgs>(this, ShowDTMessage) }
            }
        };

    private void OnDropdownChange(ChangeEventArgs e)
    {
        if ((e.Value is string dropdownValue) && !String.IsNullOrWhiteSpace(dropdownValue))
        {
            selectedComponent = Components[dropdownValue];
        }
        else
        {
            selectedComponent = null;
        }
    }

    private void ShowDTMessage(MouseEventArgs e) =>
        message = $"The current DT is: {DateTime.Now}.";
}
```

## Avoid catch-all parameters

Avoid the use of [catch-all parameters](../fundamentals/routing?view=aspnetcore-10.0#catch-all-route-parameters). If catch-all parameters are used, every explicit parameter on [DynamicComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent) effectively is a reserved word that you can't pass to a dynamic child. Any new parameters passed to [DynamicComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent) are a breaking change, as they start shadowing child component parameters that happen to have the same name. It's unlikely that the caller always knows a fixed set of parameter names to pass to all possible dynamic children.

## Access the dynamically-created component instance

Use the [Instance](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent.instance#microsoft-aspnetcore-components-dynamiccomponent-instance) property to access the dynamically-created component instance.

Create an interface to describe the dynamically-created component instance with any methods and properties that you need to access from the parent component when the component is dynamically loaded. The following example specifies a `Log` method for implementation in components.

`Interfaces/ILoggable.cs`:

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Interfaces;

public interface ILoggable : IComponent
{
    public void Log();
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Interfaces;

public interface ILoggable : IComponent
{
    public void Log();
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Interfaces;

public interface ILoggable : IComponent
{
    public void Log();
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Interfaces;

public interface ILoggable : IComponent
{
    public void Log();
}
```

Each component definition implements the interface. The following example is a modified Rocket Lab® component from the [Example](#example) section that logs a string via its `Log` method.

`RocketLab3.razor`:

```
@using BlazorSample.Interfaces
@implements ILoggable
@inject ILogger<RocketLab3> Logger
    
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

@code {
    public void Log() => Logger.LogInformation("Woot! I logged this call!");
}
```

```
@using BlazorSample.Interfaces
@implements ILoggable
@inject ILogger<RocketLab3> Logger
    
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

@code {
    public void Log() => Logger.LogInformation("Woot! I logged this call!");
}
```

```
@using BlazorSample.Interfaces
@implements ILoggable
@inject ILogger<RocketLab3> Logger
    
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

@code {
    public void Log()
    {
        Logger.LogInformation("Woot! I logged this call!");
    }
}
```

```
@using BlazorSample.Interfaces
@implements ILoggable
@inject ILogger<RocketLab3> Logger
    
<h2>Rocket Lab®</h2>

<p>
    Rocket Lab is a registered trademark of 
    <a href="https://www.rocketlabusa.com/">Rocket Lab USA Inc.</a>
</p>

@code {
    public void Log()
    {
        Logger.LogInformation("Woot! I logged this call!");
    }
}
```

The remaining three shared components (`VirginGalactic3`, `UnitedLaunchAlliance3`, `SpaceX3`) receive similar treatment:

* The following directives are added to the components, where the `{COMPONENT TYPE}` placeholder is the component type:

  ```
  @using BlazorSample.Interfaces
  @implements ILoggable
  @inject ILogger<{COMPONENT TYPE}> Logger
  ```

* Each component implements a `Log` method. The log category written by the logger includes the fully-qualified name of the component type when [LogInformation](/en-us/dotnet/api/microsoft.extensions.logging.loggerextensions.loginformation) is called:

  ```
  @code {
      public void Log()
      {
          Logger.LogInformation("Woot! I logged this call!");
      }
  }
  ```

The parent component casts the dynamically-loaded component instance as an `ILoggable` to access members of the interface. In the following example, the loaded component's `Log` method is called when a button is selected in the UI:

```
...
@using BlazorSample.Interfaces

...

<DynamicComponent Type="..." @ref="dc" />

...

<button @onclick="LogFromLoadedComponent">Log from loaded component</button>

@code {
    private DynamicComponent? dc;

    ...

    private void LogFromLoadedComponent() => (dc?.Instance as ILoggable)?.Log();
}
```

For a working demonstration of the preceding example, see the [`DynamicComponent4` component in the Blazor sample app](https://github.com/dotnet/blazor-samples/blob/main/9.0/BlazorSample_BlazorWebApp/Components/Pages/DynamicComponent4.razor).

For a working demonstration of the preceding example, see the [`DynamicComponent4` component in the Blazor sample app](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorSample_BlazorWebApp/Components/Pages/DynamicComponent4.razor).

For a working demonstration of the preceding example, see the [`DynamicComponentExample4` component in the Blazor sample app](https://github.com/dotnet/blazor-samples/blob/main/7.0/BlazorSample_Server/Pages/dynamiccomponent/DynamicComponentExample4.razor).

For a working demonstration of the preceding example, see the [`DynamicComponentExample4` component in the Blazor sample app](https://github.com/dotnet/blazor-samples/blob/main/6.0/BlazorSample_Server/Pages/dynamiccomponent/DynamicComponentExample4.razor).

## Trademarks

Rocket Lab is a registered trademark of [Rocket Lab USA Inc.](https://www.rocketlabusa.com/) SpaceX is a registered trademark of [Space Exploration Technologies Corp.](https://www.spacex.com/) United Launch Alliance and ULA are registered trademarks of [United Launch Alliance, LLC](https://www.ulalaunch.com/). Virgin Galactic is a registered trademark of [Galactic Enterprises, LLC](https://www.virgingalactic.com/).

## Related content

* [ASP.NET Core Blazor event handling](event-handling?view=aspnetcore-10.0#eventcallback)
* [DynamicComponent](/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0020
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0020.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0020: Complex types referenced by route parameters must be parsable

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0020      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A route parameter is a complex type that isn't parsable.

## Rule description

This diagnostic is emitted when a route parameter is a complex type that isn't parsable.

## How to fix violations

To fix a violation of this rule, define a `bool TryParse(string, IFormatProvider, out T)` method, where `T` is the complex type identified in the error message. As an alternative, implement [IParsable\<TSelf>](/en-us/dotnet/api/system.iparsable-1).

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/component-tag-helper
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/component-tag-helper.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Component Tag Helper in ASP.NET Core

The Component Tag Helper renders a Razor component in a Razor Pages page or MVC view.

## Prerequisites

Follow the guidance in the *Use non-routable components in pages or views* section of the [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../../../blazor/components/integration?view=aspnetcore-10.0#use-non-routable-components-in-pages-or-views) article.

Follow the guidance in the *Configuration* section for either:

* [Blazor Server](../../../../blazor/components/integration?view=aspnetcore-10.0\&pivots=server): Integrate routable and non-routable Razor components into Razor Pages and MVC apps.
* [Blazor WebAssembly](../../../../blazor/components/integration?view=aspnetcore-10.0\&pivots=webassembly): Integrate Razor components from a hosted Blazor WebAssembly solution into Razor Pages and MVC apps.

Follow the guidance in the *Configuration* section of the [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../../../blazor/components/integration?view=aspnetcore-10.0\&pivots=server) article.

## Component Tag Helper

To render a component from a page or view, use the [Component Tag Helper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper) (`<component>` tag).

Note

Integrating Razor components into Razor Pages and MVC apps in a *hosted Blazor WebAssembly app* is supported in ASP.NET Core in .NET 5 or later.

[RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode) configures whether the component:

* Is prerendered into the page.
* Is rendered as static HTML on the page or if it includes the necessary information to bootstrap a Blazor app from the user agent.

Blazor WebAssembly app render modes are shown in the following table.

| Render Mode              | Description                                                                                                                                                                                                                                              |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WebAssembly`            | Renders a marker for a Blazor WebAssembly app for use to include an interactive component when loaded in the browser. The component isn't prerendered. This option makes it easier to render different Blazor WebAssembly components on different pages. |
| `WebAssemblyPrerendered` | Prerenders the component into static HTML and includes a marker for a Blazor WebAssembly app for later use to make the component interactive when loaded in the browser.                                                                                 |

Render modes are shown in the following table.

| Render Mode                                                                                                                                          | Description                                                                                                                                                           |
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ServerPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-serverprerendered) | Renders the component into static HTML and includes a marker for a server-side Blazor app. When the user-agent starts, this marker is used to bootstrap a Blazor app. |
| [Server](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-server)                       | Renders a marker for a server-side Blazor app. Output from the component isn't included. When the user-agent starts, this marker is used to bootstrap a Blazor app.   |
| [Static](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-static)                       | Renders the component into static HTML.                                                                                                                               |

App render modes are shown in the following table.

| Render Mode                                                                                                                                          | Description                                                                                                                                                           |
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ServerPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-serverprerendered) | Renders the component into static HTML and includes a marker for a server-side Blazor app. When the user-agent starts, this marker is used to bootstrap a Blazor app. |
| [Server](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-server)                       | Renders a marker for a server-side Blazor app. Output from the component isn't included. When the user-agent starts, this marker is used to bootstrap a Blazor app.   |
| [Static](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-static)                       | Renders the component into static HTML.                                                                                                                               |

Additional characteristics include:

* Multiple Component Tag Helpers rendering multiple Razor components is allowed.
* Components can't be dynamically rendered after the app has started.
* While pages and views can use components, the converse isn't true. Components can't use view- and page-specific features, such as partial views and sections. To use logic from a partial view in a component, factor out the partial view logic into a component.
* Rendering server components from a static HTML page isn't supported.

The following Component Tag Helper renders the `EmbeddedCounter` component in a page or view in a server-side Blazor app with `ServerPrerendered`:

```
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using {APP ASSEMBLY}.Components

...

<component type="typeof(EmbeddedCounter)" render-mode="ServerPrerendered" />
```

The preceding example assumes that the `EmbeddedCounter` component is in the app's `Components` folder. The placeholder `{APP ASSEMBLY}` is the app's assembly name (for example, `@using BlazorSample.Components`).

The Component Tag Helper can also pass parameters to components. Consider the following `ColorfulCheckbox` component that sets the checkbox label's color and size.

`Components/ColorfulCheckbox.razor`:

```
<label style="font-size:@(Size)px;color:@Color">
    <input @bind="Value"
           id="survey" 
           name="blazor" 
           type="checkbox" />
    Enjoying Blazor?
</label>

@code {
    [Parameter]
    public bool Value { get; set; }

    [Parameter]
    public int Size { get; set; } = 8;

    [Parameter]
    public string? Color { get; set; }

    protected override void OnInitialized()
    {
        Size += 10;
    }
}
```

The `Size` (`int`) and `Color` (`string`) [component parameters](../../../../blazor/components/?view=aspnetcore-10.0#component-parameters) can be set by the Component Tag Helper:

```
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using {APP ASSEMBLY}.Components

...

<component type="typeof(ColorfulCheckbox)" render-mode="ServerPrerendered" 
    param-Size="14" param-Color="@("blue")" />
```

The preceding example assumes that the `ColorfulCheckbox` component is in the `Components` folder. The placeholder `{APP ASSEMBLY}` is the app's assembly name (for example, `@using BlazorSample.Components`).

```
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using {APP ASSEMBLY}.Shared

...

<component type="typeof(EmbeddedCounter)" render-mode="ServerPrerendered" />
```

The preceding example assumes that the `EmbeddedCounter` component is in the app's `Shared` folder. The placeholder `{APP ASSEMBLY}` is the app's assembly name (for example, `@using BlazorSample.Shared` or `@using BlazorSample.Client.Shared` in a hosted Blazor solution).

The Component Tag Helper can also pass parameters to components. Consider the following `ColorfulCheckbox` component that sets the checkbox label's color and size.

`Shared/ColorfulCheckbox.razor`:

```
<label style="font-size:@(Size)px;color:@Color">
    <input @bind="Value"
           id="survey" 
           name="blazor" 
           type="checkbox" />
    Enjoying Blazor?
</label>

@code {
    [Parameter]
    public bool Value { get; set; }

    [Parameter]
    public int Size { get; set; } = 8;

    [Parameter]
    public string? Color { get; set; }

    protected override void OnInitialized()
    {
        Size += 10;
    }
}
```

The `Size` (`int`) and `Color` (`string`) [component parameters](../../../../blazor/components/?view=aspnetcore-10.0#component-parameters) can be set by the Component Tag Helper:

```
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using {APP ASSEMBLY}.Shared

...

<component type="typeof(ColorfulCheckbox)" render-mode="ServerPrerendered" 
    param-Size="14" param-Color="@("blue")" />
```

The preceding example assumes that the `ColorfulCheckbox` component is in the `Shared` folder. The placeholder `{APP ASSEMBLY}` is the app's assembly name (for example, `@using BlazorSample.Shared`).

The following HTML is rendered in the page or view:

```
<label style="font-size:24px;color:blue">
    <input id="survey" name="blazor" type="checkbox">
    Enjoying Blazor?
</label>
```

Passing a quoted string requires an [explicit Razor expression](../../razor?view=aspnetcore-10.0#explicit-razor-expressions), as shown for `param-Color` in the preceding example. The Razor parsing behavior for a `string` type value doesn't apply to a `param-*` attribute because the attribute is an `object` type.

All types of parameters are supported, except:

* Generic parameters.
* Non-serializable parameters.
* Inheritance in collection parameters.
* Parameters whose type is defined outside of the Blazor WebAssembly app or within a lazily-loaded assembly.
* For receiving a [`RenderFragment` delegate for child content](../../../../blazor/components/?view=aspnetcore-10.0#child-content) (for example, `param-ChildContent="..."`). For this scenario, we recommend creating a Razor component (`.razor`) that references the component you want to render with the child content you want to pass and then invoke the Razor component from the page or view with the Component Tag Helper.

The parameter type must be JSON serializable, which typically means that the type must have a default constructor and settable properties. For example, you can specify a value for `Size` and `Color` in the preceding example because the types of `Size` and `Color` are primitive types (`int` and `string`), which are supported by the JSON serializer.

In the following example, a class object is passed to the component:

`MyClass.cs`:

```
public class MyClass
{
    public MyClass()
    {
    }

    public int MyInt { get; set; } = 999;
    public string MyString { get; set; } = "Initial value";
}
```

**The class must have a public parameterless constructor.**

`Components/ParameterComponent.razor`:

```
<h2>ParameterComponent</h2>

<p>Int: @MyObject?.MyInt</p>
<p>String: @MyObject?.MyString</p>

@code
{
    [Parameter]
    public MyClass? MyObject { get; set; }
}
```

`Pages/MyPage.cshtml`:

```
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using {APP ASSEMBLY}
@using {APP ASSEMBLY}.Components

...

@{
    var myObject = new MyClass();
    myObject.MyInt = 7;
    myObject.MyString = "Set by MyPage";
}

<component type="typeof(ParameterComponent)" render-mode="ServerPrerendered" 
    param-MyObject="@myObject" />
```

The preceding example assumes that the `ParameterComponent` component is in the app's `Components` folder. The placeholder `{APP ASSEMBLY}` is the app's assembly name (for example, `@using BlazorSample` and `@using BlazorSample.Components`). `MyClass` is in the app's namespace.

`Shared/ParameterComponent.razor`:

```
<h2>ParameterComponent</h2>

<p>Int: @MyObject?.MyInt</p>
<p>String: @MyObject?.MyString</p>

@code
{
    [Parameter]
    public MyClass? MyObject { get; set; }
}
```

`Pages/MyPage.cshtml`:

```
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using {APP ASSEMBLY}
@using {APP ASSEMBLY}.Shared

...

@{
    var myObject = new MyClass();
    myObject.MyInt = 7;
    myObject.MyString = "Set by MyPage";
}

<component type="typeof(ParameterComponent)" render-mode="ServerPrerendered" 
    param-MyObject="@myObject" />
```

The preceding example assumes that the `ParameterComponent` component is in the app's `Shared` folder. The placeholder `{APP ASSEMBLY}` is the app's assembly name (for example, `@using BlazorSample` and `@using BlazorSample.Shared`). `MyClass` is in the app's namespace.

## Related content

* [Persist Component State Tag Helper in ASP.NET Core](persist-component-state?view=aspnetcore-10.0)
* [Prerender ASP.NET Core Razor components](../../../../blazor/components/prerender?view=aspnetcore-10.0)
* [ComponentTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper)
* [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0)
* [ASP.NET Core Razor components](../../../../blazor/components/?view=aspnetcore-10.0)

- [Persist Component State Tag Helper in ASP.NET Core](persist-component-state?view=aspnetcore-10.0)
- [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../../../blazor/components/integration?view=aspnetcore-10.0)
- [ComponentTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper)
- [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0)
- [ASP.NET Core Razor components](../../../../blazor/components/?view=aspnetcore-10.0)

* [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../../../blazor/components/integration?view=aspnetcore-10.0)
* [ComponentTagHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper)
* [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0)
* [ASP.NET Core Razor components](../../../../blazor/components/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/validation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/validation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Validation in ASP.NET Core

[Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation) provides model validation for Blazor and Minimal API projects.

Validation rules are declared the same way in both frameworks, using [data annotations attributes](/en-us/dotnet/api/system.componentmodel.dataannotations) and [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject). This article describes the validation behavior that both frameworks share:

Asynchronous validation attributes and [IAsyncValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.iasyncvalidatableobject) are also supported.

* Minimal APIs use the service to validate a request before the endpoint handler runs. For guidance on how validation is surfaced in an endpoint, see [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0#validation-support-in-minimal-apis).
* Blazor uses the service through the [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component. For guidance on how validation is surfaced in a form, see [ASP.NET Core Blazor forms validation](../blazor/forms/validation?view=aspnetcore-10.0).

While the API in the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) can be used in scenarios outside ASP.NET Core, this article focuses on ASP.NET Core. The API isn't supported for MVC or Razor Pages. For validation guidance that applies to MVC and Razor Pages, see [Model validation in ASP.NET Core MVC](../mvc/models/validation?view=aspnetcore-10.0).

## Register validation services

Call [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation) on [WebApplicationBuilder.Services](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.services) in the app's `Program` file:

```
builder.Services.AddValidation();
```

For Minimal APIs, this enables automatic validation of supported parameters before the endpoint handler runs.

Blazor forms can perform basic top-level DataAnnotations validation without calling `AddValidation`. Registering the service enables nested object and collection validation when the form model is discovered by the validation source generator.

Generated validation metadata also enables message localization.

Validation uses a source generator that creates metadata for validatable types in the assembly where `AddValidation` is called. For types declared in another assembly, see [Register validation across assemblies](#register-validation-across-assemblies).

### Behavior without generated validation metadata

The consequence of omitting [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation), or of calling it without the required type being discovered by the source generator, differs by framework, as shown in the following table.

| Framework    | Behavior without generated validation metadata                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes.                                                                                                                                                                                                                                                                                                                                                             |
| Blazor       | The [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component falls back to [System.ComponentModel.DataAnnotations.Validator](/en-us/dotnet/api/system.componentmodel.dataannotations.validator), which validates top-level properties only. Nested objects, collection items, and the [`Microsoft.Extensions.Validation` message-localization pipeline](#localize-validation-messages) aren't supported on the fallback path. |

| Framework    | Behavior without generated validation metadata                                                                                                                                                                                                                                                                                                                                       |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Minimal APIs | No automatic validation runs. Invalid input reaches the endpoint handler instead of being rejected before the handler executes.                                                                                                                                                                                                                                                      |
| Blazor       | The [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component falls back to [System.ComponentModel.DataAnnotations.Validator](/en-us/dotnet/api/system.componentmodel.dataannotations.validator), which validates top-level properties only. Nested objects and collection items aren't validated on the fallback path. |

Missing metadata doesn't produce a runtime exception or log entry. Build analyzers report many unsupported configurations, but other missing-metadata cases might not produce a diagnostic. For checks to perform when expected validation is missing, see [Troubleshoot generated validation metadata](#troubleshoot-generated-validation-metadata).

[]()

## How validation runs

Minimal APIs begin with endpoint parameter validation. Blazor begins with validation of the form's model. Both frameworks then use the same model validation order and object-graph traversal.

[]()

### Minimal API parameter validation

For each supported endpoint parameter:

1. Validate [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) instances applied directly to the parameter.
2. If the parameter value is an `IEnumerable`, validate each non-`null` element. Otherwise, validate the parameter value itself.

Note

Prior to the release of .NET 11, there's a known limitation where nullable value types declared as Minimal API parameters aren't validated. For more information, see [Validation attributes are ignored for nullable value types when passing a null value (`dotnet/aspnetcore` #67033)](https://github.com/dotnet/aspnetcore/issues/67033).

[]()[]()

### Model validation order

When validating a model:

1. Validate the attributes on each property, then validate the property's value. If the value is an `IEnumerable`, validate each non-`null` element. If property validation produces an error, the remaining steps are skipped.
2. Validate attributes applied to the model type. If type-level validation produces an error, the remaining step is skipped.
3. Run [IValidatableObject.Validate](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject.validate) if the model implements [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject).

### Nested objects and collections

Validation recurses into nested objects and collection items, so a rule declared on a nested property is enforced when the root model is validated. Without generated validation metadata, Blazor only validates the top-level properties of a form model.

To validate a nested object graph:

1. Call [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation) in the `Program` file where services are registered.
2. Declare model types in C# files (`.cs`), not in Razor component files (`.razor`).
3. Annotate the root model type with [ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute) (`[ValidatableType]`). Types reachable from the root are discovered automatically.

In the following example, only the root `Order` type is annotated. The other model types are reachable from `Order` and are included in its validation graph.

`Order.cs`:

```
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Validation;

[ValidatableType]
public class Order
{
    public Customer Customer { get; set; } = new();
    public List<OrderItem> OrderItems { get; set; } = [];
}

public class Customer
{
    [Required(ErrorMessage = "Name is required.")]
    public string? FullName { get; set; }

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

public class ShippingAddress
{
    [Required(ErrorMessage = "Street is required.")]
    public string? Street { get; set; }
}

public class OrderItem
{
    [Required(ErrorMessage = "Description is required.")]
    public string? Description { get; set; }

    [Range(1, 1000)]
    public int Quantity { get; set; }
}
```

Errors from nested members use paths such as `Customer.ShippingAddress.Street` or `OrderItems[0].Description`.

For model types defined in another assembly or in a Blazor Web App's `.Client` project, see [Register validation across assemblies](#register-validation-across-assemblies).

[]()

### Skip validation

Apply [SkipValidationAttribute](/en-us/dotnet/api/microsoft.extensions.validation.skipvalidationattribute) to a property or parameter to skip validation for that property or parameter. Apply it to a type to skip validation for all properties and parameters of that type, including nested properties when the type is complex.

## Write custom validation rules

When the [built-in validation attributes](../mvc/models/validation?view=aspnetcore-10.0#built-in-attributes) don't express a rule, write a custom [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) or implement [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) on the model. Both are discovered and executed by [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation) in Blazor and Minimal API apps.

### Custom validation attributes

Derive from [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) and override [IsValid](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute.isvalid) to validate a single value.

Pass the validation context's [MemberName](/en-us/dotnet/api/system.componentmodel.dataannotations.validationcontext.membername#system-componentmodel-dataannotations-validationcontext-membername) when creating the [ValidationResult](/en-us/dotnet/api/system.componentmodel.dataannotations.validationresult). Without a member name, the result isn't associated with a field, which prevents the error from being displayed next to the corresponding input in a Blazor form:

```
using System.ComponentModel.DataAnnotations;

public class EvenNumberAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value,
        ValidationContext validationContext)
    {
        if (value is int number && number % 2 != 0)
        {
            return new ValidationResult(
                "The value must be an even number.",
                [ validationContext.MemberName! ]);
        }

        return ValidationResult.Success;
    }
}
```

Apply the attribute to a property in the same way as a built-in attribute:

```
public class Order
{
    [EvenNumber]
    public int Quantity { get; set; }
}
```

### Resolve services in a validation attribute

A validation attribute obtains services from dependency injection (DI) through the validation context, which makes rules that require a database lookup or a configured option possible:

```
protected override ValidationResult? IsValid(object? value,
    ValidationContext validationContext)
{
    var catalog = validationContext.GetService<IProductCatalog>();

    // ...
}
```

For a service that must be resolved, use [GetRequiredService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.getrequiredservice). Services resolved this way must be registered in the app's service container.

### Class-level validation with `IValidatableObject`

Implement [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) for a rule that spans several properties because an attribute applied to one property can't reliably observe the others. Class-level validation runs after property validation and only if property validation succeeds:

```
using System.ComponentModel.DataAnnotations;

public class DateRange : IValidatableObject
{
    public DateOnly Start { get; set; }
    public DateOnly End { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (End < Start)
        {
            yield return new ValidationResult(
                "End date must fall on or after the start date.",
                [ nameof(End) ]);
        }
    }
}
```

For rules that require I/O, such as a database or web API call, see the [Asynchronous validation support](#asynchronous-validation-support) section instead.

Note

In a Blazor form that uses static server-side rendering (static SSR), custom attributes aren't enforced by the browser unless the attribute also supplies a client-side rule. For more information, see [ASP.NET Core Blazor client-side form validation in static SSR](../blazor/forms/validation-client-side?view=aspnetcore-10.0).

## Asynchronous validation support

[Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation) supports asynchronous validation. Apply custom implementations of `AsyncValidationAttribute` to parameters, types, or properties, and they're called asynchronously. In addition, types can implement `IAsyncValidatableObject` as well.

Asynchronous validation operations can run in parallel, and their execution and completion order isn't guaranteed. Validation rules must not depend on a particular order.

`IAsyncValidatableObject` and `AsyncValidationAttribute` require synchronous **and** asynchronous validation logic. For example, the `Validate` and `ValidateAsync` methods of `IAsyncValidatableObject` must be implemented for objects that use the interface. However, validation never calls both methods. If validation is called through an asynchronous code path, only `ValidateAsync` is called. If validation is called through a synchronous code path, only `Validate` is called.

For Minimal API validation, [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation) always calls the asynchronous path and never the synchronous path.

Blazor form validation calls the asynchronous path for per-field validation and when the form is validated with [EditContext.ValidateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.validateasync), which is what [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) uses on submit. The synchronous path is only reached through the [EditContext.Validate](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.validate) method, which is obsolete in .NET 11 or later. Asynchronous rules therefore work in Blazor forms without additional configuration.

If your implementation can't support the synchronous path, throw [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception).

The following example performs property-level asynchronous validation with a custom `AsyncValidationAttribute`. The attribute uses a hypothetical `IUserService` to check whether an email address is already registered. Empty values are left to [RequiredAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute), and the cancellation token is passed to the asynchronous operation:

```
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;

public sealed class UniqueEmailAttribute : AsyncValidationAttribute
{
    public UniqueEmailAttribute()
    {
        ErrorMessage = "Email is already registered.";
    }

    protected override async Task<ValidationResult?> IsValidAsync(
        object? value,
        ValidationContext validationContext,
        CancellationToken cancellationToken)
    {
        if (value is not string email || string.IsNullOrWhiteSpace(email))
        {
            return ValidationResult.Success;
        }

        var userService =
            validationContext.GetRequiredService<IUserService>();

        return await userService.IsEmailExistsAsync(
            email, cancellationToken)
                ? new ValidationResult(
                    FormatErrorMessage(validationContext.DisplayName),
                    [validationContext.MemberName!])
                : ValidationResult.Success;
    }

    protected override ValidationResult? IsValid(
        object? value, ValidationContext validationContext)
    {
        throw new InvalidOperationException(
            "Synchronous validation isn't supported.");
    }
}

public class RegistrationModel
{
    [Required, EmailAddress, UniqueEmail]
    public string Email { get; set; } = string.Empty;
}
```

The following example shows object-level asynchronous validation with `IAsyncValidatableObject`. It uses a hypothetical `IUserService` to check a database for an existing email address. Because the rule requires asynchronous I/O, the required synchronous `Validate` implementation throws [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception).

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

public class ValidateUser : IAsyncValidatableObject
{
    [Required, EmailAddress]
    public string Email { get; set; } = string.Empty;

    // Asynchronous validation path
    public async IAsyncEnumerable<ValidationResult> ValidateAsync(
        ValidationContext validationContext, 
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        var userService = validationContext.GetService<IUserService>();

        if (userService is not null)
        {
            // Asynchronous call that checks a database via a service
            if (await userService.IsEmailExistsAsync(Email, cancellationToken))
            {
                yield return new ValidationResult(
                    "Email is already registered.", new[] { nameof(Email) });
            }
        }
    }

    // Synchronous validation path that throws InvalidOperationException
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        throw new InvalidOperationException("Synchronous validation isn't supported.");
    }
}
```

## Localize validation messages

Validation error messages and the display names of validated members are localized by [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation). The same rules apply wherever the model is validated, so a message localizes identically in a Minimal API endpoint and in a Blazor form.

### Activate localization

Localization activates automatically when an [IStringLocalizerFactory](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizerfactory) is available in the service container. Call [AddLocalization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.localizationservicecollectionextensions.addlocalization) to register the standard localization services, then call [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation):

```
builder.Services.AddLocalization();
builder.Services.AddValidation();
```

There's no separate package or additional opt-in call.

Important

`AddLocalization` registers localization services but doesn't select the culture for a request or circuit. Validation resource lookup uses [CurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentuiculture#system-globalization-cultureinfo-currentuiculture). For request culture providers, including query string, cookie, and `Accept-Language` header selection, see [Strategies for selecting language and culture in a localized ASP.NET Core app](localization/select-language-culture?view=aspnetcore-10.0). For configuring culture selection across Blazor render modes, see [ASP.NET Core Blazor globalization and localization](../blazor/globalization-localization?view=aspnetcore-10.0).

```
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Validation;

[ValidatableType]
public class CustomerModel
{
    // "CustomerName" is looked up as the resource key for the display name.
    [Display(Name = "CustomerName")]
    // "NameRequired" is looked up as the resource key for the error message.
    [Required(ErrorMessage = "NameRequired")]
    public string? Name { get; set; }
}
```

By default, keys are resolved against the resources of the type that declares the member. If a key doesn't resolve, the attribute's built-in error message is used, so a missing resource degrades to the non-localized message rather than surfacing the key to the user.

### Message lookup keys

When [ErrorMessage](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute.errormessage#system-componentmodel-dataannotations-validationattribute-errormessage) is set, its value is the lookup key and it takes precedence.

When `ErrorMessage` isn't set, conventional keys are tried in order from most to least specific:

1. `{DeclaringType}_{MemberName}_{AttributeType}_Error`
2. `{DeclaringType}_{AttributeType}_Error`
3. `{AttributeType}_Error`

For example, a [RequiredAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute) on the `Name` property of `CustomerModel` is looked up in this order: `CustomerModel_Name_RequiredAttribute_Error`, `CustomerModel_RequiredAttribute_Error`, and `RequiredAttribute_Error`. If none resolve, the attribute's built-in message is used.

This makes it possible to translate or override the default message of an attribute across an entire app without setting `ErrorMessage` on every attribute instance:

```
[ValidatableType]
public class CustomerModel
{
    // Resolves the localized string for 'RequiredAttribute_Error'.
    [Required]
    public string? Name { get; set; }
}
```

Two details affect key construction:

* The member segment is skipped for type-level attributes that report no member names.
* A nullable value type contributes its underlying type name.

### Where resource files are located

Keys are resolved from `.resx` files through ASP.NET Core's standard [IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer) infrastructure, so the usual naming and placement conventions apply. Per-type resolution is the default and needs no configuration beyond the resources path:

```
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddValidation();
```

Under the configured `ResourcesPath`, the type's full name minus the project's root namespace is used as a dotted path. For a project root namespace of `Contoso`, French messages for `Contoso.Models.Customer` are read from `Resources/Models/Customer.fr.resx` (equivalently `Resources/Models.Customer.fr.resx`). For a full description of the conventions, see [Provide localized resources for languages and cultures in an ASP.NET Core app](localization/provide-resources?view=aspnetcore-10.0).

For per-type lookup, place the resource files in the project that declares the validated type. For example, if a Blazor Web App's form models are declared in its `.Client` project, place their per-type resources in that project. The model assembly's root namespace and the configured `ResourcesPath` determine the resource name.

### Use a shared resource file

To resolve keys from one resource file for every validated type instead of per-type resources, set `ValidationOptions.LocalizerProvider`:

```
builder.Services.AddValidation(options =>
{
    options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationMessages));
});
```

The delegate also receives the validated type, so an app can select different resources for different model types.

The marker type passed to `factory.Create` identifies both the resource name and the assembly containing the resource. This makes a shared resource in the host project an alternative to placing per-type resources in a referenced model assembly.

### Localize from a source other than resource files

To read localized strings from a database, JSON files, or another source, register a custom [IStringLocalizerFactory](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizerfactory). A user-registered factory takes precedence over the default resource file implementation:

```
builder.Services.AddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();
builder.Services.AddValidation();
```

### Attributes that localize themselves

Attributes configured with [ErrorMessageResourceType](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute.errormessageresourcetype#system-componentmodel-dataannotations-validationattribute-errormessageresourcetype) or [DisplayAttribute.ResourceType](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute.resourcetype) perform their own resource lookup and aren't processed by the `Microsoft.Extensions.Validation` localizer.

### Format a custom attribute's message

A custom attribute that substitutes its own values into a message template implements `IValidationMessageFormatter`. The framework calls `FormatMessage` with the culture, the localized template, and the resolved display name:

```
using System.Globalization;
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Validation;

public sealed class DivisibleByAttribute : ValidationAttribute, IValidationMessageFormatter
{
    public int Divisor { get; init; }

    // Fills {0} with the display name and {1} with the divisor.
    public string FormatMessage(CultureInfo culture, string template, string displayName) =>
        string.Format(culture, template, displayName, Divisor);
}
```

Note

This localization pipeline requires [Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation). A Blazor form whose model isn't discovered by the validation source generator falls back to [System.ComponentModel.DataAnnotations.Validator](/en-us/dotnet/api/system.componentmodel.dataannotations.validator). Attributes configured to localize themselves with `ErrorMessageResourceType` continue to do so, but the generated lookup conventions and `ValidationOptions.LocalizerProvider` aren't available. For more information, see [Behavior without generated validation metadata](#behavior-without-generated-validation-metadata).

## Configure generated validation metadata

[Microsoft.Extensions.Validation](/en-us/dotnet/api/microsoft.extensions.validation) uses a Roslyn source generator to create validation metadata at build time. Minimal API parameter types are discovered from endpoint handler signatures. Blazor form model types are included by applying [ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute).

[]()

### Include root model types

Apply `[ValidatableType]` to a Blazor form's root model type and to any other root type that the source generator can't discover from a Minimal API endpoint signature. Types reachable from the root are included automatically.

### Model types can't be declared in Razor component files

The Razor compiler and the validation feature both use source generators. A source generator can't inspect another generator's output, so the validation generator can't include model types declared in Razor component files (`.razor`). Declare model types in regular C# files (`.cs`) instead.

Applying `[ValidatableType]` to a type in generated code produces warning ASP0037.

[]()

### Register validation across assemblies

The source generator creates metadata only for the assembly where [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation) is called. Calling `AddValidation` only from the host app doesn't generate metadata for types declared in a referenced assembly, such as a class library or the `.Client` project of a Blazor Web App.

To validate types from separate assemblies:

* If the assembly is a plain class library (it isn't based on the `Microsoft.NET.Sdk.Web` or `Microsoft.NET.Sdk.Razor` SDKs), add a package reference to the project for the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation).

* Create an extension method in each assembly that declares validatable types. The method calls [AddValidation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.validationservicecollectionextensions.addvalidation) so that the source generator runs in that assembly:

  ```
  namespace ValidatableTypesAssembly.Extensions;

  public static class ServiceCollectionExtensions
  {
      public static IServiceCollection AddValidationForLibraryTypes(
          this IServiceCollection services)
      {
          return services.AddValidation();
      }
  }
  ```

* Call each of those extension methods from the host app, along with `AddValidation` for the host app's own types:

  ```
  using ValidatableTypesAssembly.Extensions;

  ...

  builder.Services.AddValidationForLibraryTypes();
  builder.Services.AddValidation();
  ```

The preceding approach validates the types from both assemblies.

For a Blazor Web App whose form models are declared in the `.Client` project, create the validation-registration extension method in that project and call it from the server project's `Program` file.

For Minimal API endpoints and models defined in referenced assemblies, see [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0#validation-support-in-minimal-apis).

### Troubleshoot generated validation metadata

Missing generated validation metadata doesn't produce a runtime exception or log entry. If expected validation behavior is missing, confirm all of the following:

* `AddValidation` is called from the assembly that declares the validatable types.
* Blazor root form models and other roots not discovered from Minimal API signatures have `[ValidatableType]`.
* Model types are declared in `.cs` files.
* Validated types and properties are accessible to generated code.

Build analyzers report common unsupported configurations:

* [ASP0033: ValidatableType is applied to an inaccessible type](../diagnostics/asp0033?view=aspnetcore-10.0) and [ASP0034: Endpoint parameter type is inaccessible from generated code](../diagnostics/asp0034?view=aspnetcore-10.0) report inaccessible validatable types and endpoint parameter types.
* [ASP0035: Validatable property or its type on a ValidatableType is not accessible](../diagnostics/asp0035?view=aspnetcore-10.0) and [ASP0036: Validatable property or its type on an endpoint parameter type is not accessible](../diagnostics/asp0036?view=aspnetcore-10.0) report inaccessible validated properties or property types.
* [ASP0037: ValidatableType cannot be used in generated code](../diagnostics/asp0037?view=aspnetcore-10.0) reports `[ValidatableType]` applied to generated code.
* [ASP0038: ValidatableType should not be used without a call to AddValidation](../diagnostics/asp0038?view=aspnetcore-10.0) reports `[ValidatableType]` used without a matching `AddValidation` call.

Other missing-metadata cases might not produce a diagnostic.

For the runtime behavior when metadata isn't available, see [Behavior without generated validation metadata](#behavior-without-generated-validation-metadata).

## Experimental API in apps that target .NET 10

Attributes from the [`Microsoft.Extensions.Validation` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Validation) ([ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute) and [SkipValidationAttribute](/en-us/dotnet/api/microsoft.extensions.validation.skipvalidationattribute)) are published as *experimental* in .NET 10. The package is intended to provide a new shared infrastructure for validation features across frameworks, and publishing experimental types provides greater flexibility for the final design of the public API for better support in consuming frameworks. As of .NET 11, the attributes are no longer experimental, so the guidance in this section doesn't apply to apps that target .NET 11 or later.

In Blazor apps, types are made available via a generated embedded attribute. If a web app project that uses the `Microsoft.NET.Sdk.Web` SDK (`<Project Sdk="Microsoft.NET.Sdk.Web">`) or an RCL that uses the `Microsoft.NET.Sdk.Razor` SDK (`<Project Sdk="Microsoft.NET.Sdk.Razor">`) contains Razor components (`.razor`), the framework automatically generates an internal attribute inside the project (`Microsoft.Extensions.Validation.Embedded.ValidatableType`, `Microsoft.Extensions.Validation.Embedded.SkipValidation`). These types are interchangeable with the actual attributes and not marked experimental. In the majority of cases, developers use the `[ValidatableType]`/`[SkipValidation]` attributes on their classes without concern over their source.

However, the preceding approach isn't viable in plain class libraries that use the `Microsoft.NET.Sdk` SDK (`<Project Sdk="Microsoft.NET.Sdk">`). Using the types in a plain class library results in a code analysis warning:

> ASP0029: 'Microsoft.Extensions.Validation.ValidatableTypeAttribute' is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

The warning can be suppressed using any of the following approaches:

* A `<NoWarn>` property in the project file:

  ```
  <PropertyGroup>
    <NoWarn>$(NoWarn);ASP0029</NoWarn>
  </PropertyGroup>
  ```

* A [`pragma` directive](/en-us/cpp/preprocessor/pragma-directives-and-the-pragma-keyword) where the attribute is used:

  ```
  #pragma warning disable ASP0029
  [Microsoft.Extensions.Validation.ValidatableType]
  #pragma warning restore ASP0029
  ```

* An [EditorConfig file (`.editorconfig`)](/en-us/visualstudio/ide/create-portable-custom-editor-options) rule:

  ```
  dotnet_diagnostic.ASP0029.severity = none
  ```

If suppressing the warning isn't acceptable, manually create the embedded attribute in the library that the Web and Razor SDKs generate automatically.

`ValidatableTypeAttribute.cs`:

```
namespace Microsoft.Extensions.Validation.Embedded
{
    [AttributeUsage(AttributeTargets.Class)]
    internal sealed class ValidatableTypeAttribute : Attribute
    {
    }
}
```

Use the exact namespace (`Microsoft.Extensions.Validation.Embedded`) and class name ([ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute)) in order for the validation source generator to detect and use the type. You can declare a global `using` statement for the namespace, either with a `global using Microsoft.Extensions.Validation.Embedded;` statement or with a `<Using Include="Microsoft.Extensions.Validation.Embedded" />` item in the library's project file.

Whichever approach is adopted, denote the presence of the workaround for a future update to your code when the app can target .NET 11 or later. At that time, you can remove your workarounds from the app.

## Related content

* [ASP.NET Core Blazor forms validation](../blazor/forms/validation?view=aspnetcore-10.0)

  * [ASP.NET Core Blazor client-side form validation in static SSR](../blazor/forms/validation-client-side?view=aspnetcore-10.0)
  * [ASP.NET Core Blazor advanced form validation](../blazor/forms/validation-advanced?view=aspnetcore-10.0)

* [Upgrade from the experimental `ValidatableTypeAttribute`/`SkipValidationAttribute` in .NET 10](../migration/100-to-110?view=aspnetcore-10.0#upgrade-from-the-experimental-validatabletypeattribute-and-skipvalidationattribute-in-net-10)

* [Minimal APIs quick reference](minimal-apis?view=aspnetcore-10.0)
  * [Validation support in Minimal APIs](minimal-apis?view=aspnetcore-10.0#validation-support-in-minimal-apis)

* [Make an ASP.NET Core app's content localizable](localization/make-content-localizable?view=aspnetcore-10.0)

* [Model validation in ASP.NET Core MVC](../mvc/models/validation?view=aspnetcore-10.0)

- [ASP.NET Core Blazor forms validation](../blazor/forms/validation?view=aspnetcore-10.0)
- [Validation support in Minimal APIs](minimal-apis?view=aspnetcore-10.0#validation-support-in-minimal-apis)
- [Model validation in ASP.NET Core MVC](../mvc/models/validation?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-16

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/100-to-110
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/100-to-110.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11

This article explains how to update an ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11.

## Prerequisites

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

- [Visual Studio](https://visualstudio.microsoft.com/downloads/) with the **ASP.NET and web development** workload.

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# Dev Kit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit)
* [Latest released .NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core)

You can follow the Visual Studio Code instructions on macOS, Linux, or Windows. Changes may be required if you use an integrated development environment (IDE) other than Visual Studio Code.

## Update the .NET SDK version in `global.json`

If you rely on a [`global.json`](/en-us/dotnet/core/tools/global-json) file to target a specific .NET SDK version, update the `version` property to the .NET 11 SDK version that's installed. For example:

```
{
  "sdk": {
-    "version": "10.0.102"
+    "version": "11.0.100"
  }
}
```

## Update the target framework

Update the project file's [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks) to `net11.0`:

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

  <PropertyGroup>
-    <TargetFramework>net10.0</TargetFramework>
+    <TargetFramework>net11.0</TargetFramework>
  </PropertyGroup>

</Project>
```

## Update package references

In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.org/packages?q=Microsoft.AspNetCore.*), [`Microsoft.EntityFrameworkCore.*`](https://www.nuget.org/packages?q=Microsoft.EntityFrameworkCore.*), [`Microsoft.Extensions.*`](https://www.nuget.org/packages?q=Microsoft.Extensions.*), and [`System.Net.Http.Json`](https://www.nuget.org/packages/System.Net.Http.Json) package reference's `Version` attribute to 11.0.0 or later. For example:

```
<ItemGroup>
-   <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="10.0.0" />
-   <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0" />
-   <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
-   <PackageReference Include="System.Net.Http.Json" Version="10.0.0" />
+   <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="11.0.0" />
+   <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="11.0.0" />
+   <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="11.0.0" />
+   <PackageReference Include="System.Net.Http.Json" Version="11.0.0" />
</ItemGroup>
```

## Blazor

### Blazor release notes

For new feature coverage, see [What's new in ASP.NET Core in .NET 11](../release-notes/aspnetcore-11?view=aspnetcore-10.0).

### Adopt Inline JS event handler removed from the `NavMenu` component

*This section only applies to Blazor Web Apps.*

The inline JS event handler for the navigation bar toggler isn't present in the `NavMenu` component of the Blazor Web App project template in .NET 11 or later. Apps generated from the project template use a [collocated JS module](../blazor/javascript-interoperability/location-of-javascript?view=aspnetcore-10.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) approach to show or hide the navigation links on the rendered page. The approach improves [Content Security Policy (CSP) compliance](../blazor/security/content-security-policy?view=aspnetcore-10.0) because it doesn't require the CSP to include an unsafe hash for the inline JS.

Use the following instructions to adopt the new JS module approach for the navigation links toggler in an existing app.

Add a [collocated JS module](../blazor/javascript-interoperability/location-of-javascript?view=aspnetcore-10.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) next to the app's `NavMenu` component.

`NavMenu.razor.js`:

```
// Handle navigation menu toggle
const navScrollable = document.getElementById("nav-scrollable");
const navToggler = document.querySelector(".navbar-toggler");

if (navScrollable && navToggler) {
    navScrollable.addEventListener("click", function() {
        navToggler.click();
    });
}
```

At the top of the app's `NavMenu` component (`NavMenu.razor`), add a `<script>` tag for the collocated JS module:

* If the app adopts client-side rendering (has a `.Client` project) with global interactivity (the render mode is set globally for the app by the app's `App` component), use the following tag, which indicates the path to the module in the `Layout` folder:

```
<script type="module" src="@Assets["Layout/NavMenu.razor.js"]"></script>
```

* Otherwise, use the following tag, which indicates the path to the module in the `Components/Layout` folder:

```
<script type="module" src="@Assets["Components/Layout/NavMenu.razor.js"]"></script>
```

Also in the app's `NavMenu` component, change the line that has the inline JS to toggle the navigation links:

```
- <div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
+ <div id="nav-scrollable" class="nav-scrollable">
```

If the app has a [Content Security Policy (CSP)](../blazor/security/content-security-policy?view=aspnetcore-10.0#server-side-blazor-apps) with an unsafe hash for the inline JS removed by the preceding step, remove the unsafe hash:

```
- 'unsafe-hashes' 'sha256-qnHnQs7NjQNHHNYv/I9cW+I62HzDJjbnyS/OFzqlix0='
```

### QuickGrid adopts URL-based pagination and sorting

The `QuickGrid` component persists pagination and sort state in the URL query string (for example, `?page=2&sort=Name&direction=asc`), which enables link sharing, browser back/forward navigation, and operation under static server-side rendering (static SSR). This behavior is enabled by default.

To work without a JavaScript runtime, sortable column headers and paginator controls now render as `<a>` (link) elements instead of `<button>` elements. Update any custom CSS that targets the previous markup:

```
- button.col-title { ... }
+ button.col-title, a.col-title { ... }
- nav button:disabled { ... }
+ nav button:disabled, nav a[aria-disabled="true"] { ... }
```

Disabled paginator links use `aria-disabled="true"` instead of the HTML `disabled` attribute, which isn't valid on `<a>` elements. The built-in QuickGrid CSS already covers both markup styles.

When more than one `QuickGrid` is rendered on the same page, set unique query parameter names with `QueryParameterNameOptions` on all but one grid (and give each its own `PaginationState`) to prevent the grids from sharing the page, sort, and direction query parameters:

```
- <QuickGrid Items="@cities" Pagination="@pagination2">...</QuickGrid>
+ <QuickGrid Items="@cities" Pagination="@pagination2" QueryParameterNameOptions="@(new QueryParameterNameOptions("cities_"))">...</QuickGrid>
```

To revert to the previous `<button>`-based markup, which requires an interactive render mode, set the following AppContext switch to false:

```
AppContext.SetSwitch(
    "Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting",
    false);
```

The switch only controls the rendered HTML element; sort and page state is read from and written to the URL query string regardless of the setting.

For more information, see [QuickGrid sorting and pagination controls render as links](/en-us/aspnet/core/breaking-changes/11/quickgrid-controls-render-as-links).

### Upgrade from the experimental `ValidatableTypeAttribute` and `SkipValidationAttribute` in .NET 10

[ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute) and [SkipValidationAttribute](/en-us/dotnet/api/microsoft.extensions.validation.skipvalidationattribute) were published as *experimental* in .NET 10. In .NET 11 or later, they aren't experimental. Referencing them from any project, including a plain class library that uses the `Microsoft.NET.Sdk` SDK, no longer produces the `ASP0029` code analysis warning.

Apps upgrading from .NET 10 can remove the workarounds that warning required:

* Delete `#pragma warning disable ASP0029` and `#pragma warning restore ASP0029` directives around uses of the attributes.
* Remove `ASP0029` from any `<NoWarn>` property in the project file.
* Replace any locally declared `Microsoft.Extensions.Validation.Embedded.ValidatableType` or `Microsoft.Extensions.Validation.Embedded.SkipValidation` attribute with the real [ValidatableTypeAttribute](/en-us/dotnet/api/microsoft.extensions.validation.validatabletypeattribute) and [SkipValidationAttribute](/en-us/dotnet/api/microsoft.extensions.validation.skipvalidationattribute), and remove the `global using Microsoft.Extensions.Validation.Embedded;` statement or the `<Using Include="Microsoft.Extensions.Validation.Embedded" />` item that made it visible.

Leaving the suppressions in place is harmless, so an upgraded app doesn't fail because of them. Removing them keeps the project free of suppressions that no longer correspond to a diagnostic.

## Security

### Automatic CSRF protection

.NET 11 adds automatic Cross-Site Request Forgery (CSRF) protection. When an app is built with `WebApplication.CreateBuilder` and has endpoints, a middleware is wired up by default that inspects the `Sec-Fetch-Site` and `Origin` headers and records a validation verdict on the request.

The middleware validates endpoints that opt in to antiforgery validation—that is, endpoints with metadata implementing `IAntiforgeryMetadata` where `RequiresValidation` is `true`. The framework sets this automatically for:

* All Blazor server-side rendering (SSR) endpoints. Each is protected by default; a page can opt out with `@attribute [RequireAntiforgeryToken(false)]`.
* Minimal API endpoints that bind form data.
* MVC actions that use antiforgery validation, such as those annotated with `[ValidateAntiForgeryToken]` or `[AutoValidateAntiforgeryToken]`.

Endpoints that bind JSON, such as a plain `MapPost` or a Web API `[HttpPost]` action, have no behavioral change.

Going forward, the automatic CSRF protection is the recommended defense, and most apps no longer need the token-based antiforgery system. Keep the token-based system when the app must support browsers that don't send `Sec-Fetch-Site`, uses [IAntiforgeryAdditionalDataProvider](/en-us/dotnet/api/microsoft.aspnetcore.antiforgery.iantiforgeryadditionaldataprovider), or must keep the token defense as an independent layer for a compliance requirement. Both protections can coexist.

To simplify an app that configures antiforgery explicitly, drop the [AddAntiforgery](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.antiforgeryservicecollectionextensions.addantiforgery) and [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) calls and rely on the automatic protection. For most apps, this is a one-line change with no other code updates. For Blazor static SSR, removing `app.UseAntiforgery()` also stops antiforgery token generation for rendered forms; see [Blazor server-side rendering defers antiforgery validation to middleware](/en-us/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection).

A `400 - Bad Request` on a cross-origin form post is the CSRF protection working as intended. When the request comes from a legitimate origin, allow that origin rather than suppressing the check:

* Configure [CORS](../security/cors?view=aspnetcore-10.0) so the endpoint's resolved policy includes the caller's origin. The CSRF middleware honors that policy and allows the request.
* Only opt an endpoint out with `.DisableAntiforgery()` (Minimal APIs) or `[IgnoreAntiforgeryToken]` (MVC) when it isn't vulnerable to CSRF, such as an endpoint that isn't reachable from a browser or that authenticates with a non-cookie mechanism (for example, bearer authentication).

For a full description of the middleware, the validation rules, and how it interacts with the token-based antiforgery system, see [Automatic CSRF protection in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-10.0#automatic-csrf-protection-in-aspnet-core).

## ASP.NET Core MVC

### Upgrading F# MVC applications from .NET 10 to .NET 11

Newly generated .NET 11 F# MVC projects already include build- and publish-time Razor compilation and view assembly registration. The following steps apply when migrating an existing F# MVC application from .NET 10 or earlier.

F# MVC projects generated by earlier templates use the [`Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation) and call [AddRazorRuntimeCompilation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorruntimecompilationmvcbuilderextensions.addrazorruntimecompilation). Razor runtime compilation is obsolete in .NET 10 or later ([breaking change announcement](/en-us/aspnet/core/breaking-changes/10/razor-runtime-compilation-obsolete)). Unlike C# MVC projects, F# projects can't use the Razor source generator directly. Removing runtime compilation without configuring compilation causes the app to build successfully but fail at runtime when a view is requested with an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception).

To upgrade an existing F# MVC project:

1. Remove the `Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation` package reference from the project file (`.fsproj`).

2. In `Program.fs`, remove the call to `AddRazorRuntimeCompilation`.

3. Configure Razor compilation in the project file:

   ```
   <PropertyGroup>
     <UseRazorSourceGenerator>false</UseRazorSourceGenerator>
     <RazorCompileOnBuild>true</RazorCompileOnBuild>
     <RazorCompileOnPublish>true</RazorCompileOnPublish>
   </PropertyGroup>
   ```

4. Add `MvcBuilderExtensions.fs` to the project file before `Program.fs`, as F# compilation order is significant:

   ```
   <ItemGroup>
     <!-- Other F# source files -->
     <Compile Include="MvcBuilderExtensions.fs" />
     <Compile Include="Program.fs" />
   </ItemGroup>
   ```

5. Add `MvcBuilderExtensions.fs`. The namespace must match the application's root namespace (for example, `Company.WebApplication1`):

   ```
   namespace Company.WebApplication1

   open System
   open System.IO
   open System.Reflection
   open System.Runtime.CompilerServices
   open System.Runtime.Loader
   open Microsoft.AspNetCore.Mvc.ApplicationParts
   open Microsoft.Extensions.DependencyInjection

   [<Extension>]
   type MvcBuilderExtensions =
       [<Extension>]
       static member AddCompiledRazorViews(builder: IMvcBuilder) =
           let applicationAssembly = Assembly.GetExecutingAssembly()
           let viewsAssemblyName = $"{applicationAssembly.GetName().Name}.Views"
           let viewsAssemblyPath =
               Path.Combine(AppContext.BaseDirectory, $"{viewsAssemblyName}.dll")
           let loadContext =
               AssemblyLoadContext.GetLoadContext(applicationAssembly)

           let viewsAssembly =
               if File.Exists(viewsAssemblyPath) then
                   loadContext.LoadFromAssemblyPath(viewsAssemblyPath)
               else
                   loadContext.LoadFromAssemblyName(AssemblyName(viewsAssemblyName))

           builder.PartManager.ApplicationParts.Add(
               CompiledRazorAssemblyPart(viewsAssembly))
           builder
   ```

6. Register the compiled views in `Program.fs`:

   ```
       .AddControllersWithViews()
       .AddCompiledRazorViews()
   ```

This configuration compiles Razor views during build and publish and registers the resulting views assembly with MVC. It avoids the obsolete runtime compilation dependency and allows view discovery at runtime.

## OpenAPI

### Microsoft.OpenApi upgraded to 3.x

In .NET 11, `Microsoft.AspNetCore.OpenApi` upgrades its dependency on `Microsoft.OpenApi` from 2.x to 3.x. The transformer interfaces are unchanged, but the object model for `OpenApiDocument`, `OpenApiOperation`, and `OpenApiSchema` has changed shape. Custom document, operation, and schema transformers that inspect or mutate OpenAPI objects must be updated to compile against the 3.x API.

For detailed migration guidance and breaking change details, see [Microsoft.OpenApi upgraded to 3.x (ASP.NET Core 11.0 breaking change announcement)](/en-us/aspnet/core/breaking-changes/11/microsoft-openapi-3x) and the [Microsoft.OpenApi 3.x upgrade guide](https://github.com/microsoft/OpenAPI.NET/blob/main/docs/upgrade-guide-3.md).

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/metapackage
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/metapackage.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Microsoft.AspNetCore.All metapackage for ASP.NET Core 2.0

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Note

The `Microsoft.AspNetCore.All` metapackage isn't used in ASP.NET Core 3.0 or later. For more information, see [this GitHub issue](https://github.com/aspnet/Announcements/issues/314).

Note

We recommend applications targeting ASP.NET Core 2.1 or later use the [Microsoft.AspNetCore.App metapackage](metapackage-app?view=aspnetcore-10.0) rather than this package. See [Migrating from Microsoft.AspNetCore.All to Microsoft.AspNetCore.App](#migrate) in this article.

This feature requires ASP.NET Core 2.x targeting .NET Core 2.x.

[`Microsoft.AspNetCore.All`](https://www.nuget.org/packages/Microsoft.AspNetCore.All) is a metapackage that refers to a shared framework. A *shared framework* is a set of assemblies (*.dll* files) that are not in the app's folders. The shared framework must be installed on the machine to run the app.

The shared framework that `Microsoft.AspNetCore.All` refers to includes:

* All supported packages by the ASP.NET Core team.
* All supported packages by the Entity Framework Core.
* Internal and 3rd-party dependencies used by ASP.NET Core and Entity Framework Core.

All the features of ASP.NET Core 2.x and Entity Framework Core 2.x are included in the `Microsoft.AspNetCore.All` package. The default project templates targeting ASP.NET Core 2.0 use this package.

The version number of the `Microsoft.AspNetCore.All` metapackage represents the minimum ASP.NET Core version and Entity Framework Core version.

The following `.csproj` file references the `Microsoft.AspNetCore.All` metapackage for ASP.NET Core:

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

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.9" />
  </ItemGroup>

</Project>
```

## Implicit versioning

In ASP.NET Core 2.1 or later, you can specify the `Microsoft.AspNetCore.All` package reference without a version. When the version isn't specified, an implicit version is specified by the SDK (`Microsoft.NET.Sdk.Web`). We recommend relying on the implicit version specified by the SDK and not explicitly setting the version number on the package reference. If you have questions about this approach, leave a GitHub comment at the [Discussion for the Microsoft.AspNetCore.App implicit version](https://github.com/dotnet/AspNetCore.Docs/issues/6430).

The implicit version is set to `major.minor.0` for portable apps. The shared framework roll-forward mechanism runs the app on the latest compatible version among the installed shared frameworks. To guarantee the same version is used in development, test, and production, ensure the same version of the shared framework is installed in all environments. For self-contained apps, the implicit version number is set to the `major.minor.patch` of the shared framework bundled in the installed SDK.

Specifying a version number on the `Microsoft.AspNetCore.All` package reference does **not** guarantee that version of the shared framework is chosen. For example, suppose version "2.1.1" is specified, but "2.1.3" is installed. In that case, the app will use "2.1.3". Although not recommended, you can disable roll forward (patch and/or minor). For more information regarding dotnet host roll-forward and how to configure its behavior, see [dotnet host roll forward](https://github.com/dotnet/core-setup/blob/master/Documentation/design-docs/roll-forward-on-no-candidate-fx.md).

The project's SDK must be set to `Microsoft.NET.Sdk.Web` in the project file to use the implicit version of `Microsoft.AspNetCore.All`. When the `Microsoft.NET.Sdk` SDK is specified (`<Project Sdk="Microsoft.NET.Sdk">` at the top of the project file), the following warning is generated:

*Warning NU1604: Project dependency Microsoft.AspNetCore.All does not contain an inclusive lower bound. Include a lower bound in the dependency version to ensure consistent restore results.*

This is a known issue with the .NET Core 2.1 SDK and will be fixed in the .NET Core 2.2 SDK.

[]()

## Migrating from Microsoft.AspNetCore.All to Microsoft.AspNetCore.App

The following packages are included in `Microsoft.AspNetCore.All` but not the `Microsoft.AspNetCore.App` package.

* `Microsoft.AspNetCore.ApplicationInsights.HostingStartup`
* `Microsoft.AspNetCore.AzureAppServices.HostingStartup`
* `Microsoft.AspNetCore.AzureAppServicesIntegration`
* `Microsoft.AspNetCore.DataProtection.AzureKeyVault`
* `Microsoft.AspNetCore.DataProtection.AzureStorage`
* `Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv`
* `Microsoft.AspNetCore.SignalR.Redis`
* `Microsoft.Data.Sqlite`
* `Microsoft.Data.Sqlite.Core`
* `Microsoft.EntityFrameworkCore.Sqlite`
* `Microsoft.EntityFrameworkCore.Sqlite.Core`
* `Microsoft.Extensions.Caching.Redis`
* `Microsoft.Extensions.Configuration.AzureKeyVault`
* `Microsoft.Extensions.Logging.AzureAppServices`
* `Microsoft.VisualStudio.Web.BrowserLink`

To move from `Microsoft.AspNetCore.All` to `Microsoft.AspNetCore.App`, if your app uses any APIs from the above packages, or packages brought in by those packages, add references to those packages in your project.

Any dependencies of the preceding packages that otherwise aren't dependencies of `Microsoft.AspNetCore.App` are not included implicitly. For example:

* `StackExchange.Redis` as a dependency of `Microsoft.Extensions.Caching.Redis`
* `Microsoft.ApplicationInsights` as a dependency of `Microsoft.AspNetCore.ApplicationInsights.HostingStartup`

## Update ASP.NET Core 2.1

We recommend migrating to the `Microsoft.AspNetCore.App` metapackage for 2.1 or later. To keep using the `Microsoft.AspNetCore.All` metapackage and ensure the latest patch version is deployed:

* On development machines and build servers: Install the latest [.NET Core SDK](https://dotnet.microsoft.com/download).
* On deployment servers: Install the latest [.NET Core runtime](https://dotnet.microsoft.com/download). Your app will roll forward to the latest installed version on an application restart.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/
----

# ASP.NET documentation

Learn to use ASP.NET Core to create web apps and services that are fast, secure, cross-platform, and cloud-based. Browse tutorials, sample code, fundamentals, API reference and more.

Get started

[Create an ASP.NET Core app on any platform in 5 minutes](get-started?view=aspnetcore-10.0)

Overview

[ASP.NET Core overview](introduction-to-aspnet-core?view=aspnetcore-10.0)

Download

[Download .NET](https://dotnet.microsoft.com/download)

Get started

[Create your first web API](tutorials/min-web-api?view=aspnetcore-10.0)

Get started

[Create your first real-time web app](tutorials/signalr?view=aspnetcore-10.0)

## Develop ASP.NET Core apps

Choose interactive web apps, web API, MVC-patterned apps, real-time apps, and more

### HTTP API apps

Develop HTTP services with ASP.NET Core

* [Choose between controller-based APIs and Minimal APIs](fundamentals/apis?view=aspnetcore-10.0)
* [Create a minimal web API with ASP.NET Core](tutorials/min-web-api?view=aspnetcore-10.0)
* [Create a web API with ASP.NET Core Controllers](/en-us/training/modules/build-web-api-net-core/)
* [Generate web API help pages with Swagger / OpenAPI](tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)

### Interactive client-side Blazor apps

Develop with reusable UI components that can take advantage of WebAssembly for near-native performance

* [Overview](blazor/?view=aspnetcore-10.0)
* [Blazor hosting models](blazor/hosting-models?view=aspnetcore-10.0)
* [Build your first Blazor app](https://dotnet.microsoft.com/learn/aspnet/blazor-tutorial/intro)
* [Build your first Blazor app with reusable components](blazor/tutorials/build-a-blazor-app?view=aspnetcore-10.0)

### Page-focused web UI with Razor Pages

Develop page-focused web apps with a clean separation of concerns

* [Create your first Razor Pages web app](tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Create a page-focused web UI that consumes a web API](/en-us/training/modules/create-razor-pages-aspnet-core/)
* [Razor syntax](mvc/views/razor?view=aspnetcore-10.0)
* [See more](razor-pages/?view=aspnetcore-10.0)

### Page-focused web UI with MVC

Develop web apps using the Model-View-Controller design pattern

* [Overview](mvc/overview?view=aspnetcore-10.0)
* [Create your first ASP.NET Core MVC app](tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0)
* [Views](mvc/views/overview?view=aspnetcore-10.0)
* [Controllers](mvc/controllers/actions?view=aspnetcore-10.0)
* [Routing to controller actions](mvc/controllers/routing?view=aspnetcore-10.0)

### Data-driven web apps

Create data-driven web apps in ASP.NET Core

* [Data binding in ASP.NET Core Blazor](blazor/components/data-binding?view=aspnetcore-10.0)
* [SQL Server Express and Razor Pages](tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Entity Framework Core with Razor Pages](data/ef-rp/intro?view=aspnetcore-10.0)
* [Entity Framework Core with ASP.NET Core MVC](data/ef-mvc/intro?view=aspnetcore-10.0)
* [Get started with ASP.NET Core MVC (SQL Server Express and SQLite)](tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0)

### Real-time web apps with SignalR

Add real-time functionality to your web app, enable server-side code to push content instantly

* [Overview](signalr/introduction?view=aspnetcore-10.0)
* [Create your first SignalR app](signalr/introduction?view=aspnetcore-10.0)
* [SignalR with Blazor WebAssembly](blazor/tutorials/signalr-blazor?view=aspnetcore-10.0)
* [SignalR with TypeScript](tutorials/signalr-typescript-webpack?view=aspnetcore-10.0)
* [Samples](https://github.com/aspnet/SignalR-samples)

### Remote Procedure Call (RPC) apps - gRPC services

Develop contract-first, high-performance services with gRPC in ASP.NET Core

* [Overview](grpc/?view=aspnetcore-10.0)
* [Create a gRPC client and server](tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [gRPC services concepts in C#](grpc/basics?view=aspnetcore-10.0)
* [Samples](https://github.com/grpc/grpc-dotnet/tree/master/examples)
* [Compare gRPC services with HTTP APIs](grpc/comparison?view=aspnetcore-10.0)

### Previous ASP.NET framework versions

Explore overviews, tutorials, fundamental concepts, architecture and API reference for previous ASP.NET framework versions

* [ASP.NET 4.x](/en-us/aspnet/overview)

### ASP.NET Core video tutorials

* [ASP.NET Core 101 video series](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oW8nviYduHq7bmKode-p8Wy)
* [Entity Framework Core 101 video series with .NET Core and ASP.NET Core](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oX7uTkjYwvCJDG2qhcSzwZ6)
* [Microservice architecture with ASP.NET Core](https://www.youtube.com/watch?v=RyHDWlIq6vI)
* [Focus on Blazor video series](https://www.youtube.com/watch?v=KlngrOF6RPw\&list=PLdo4fOcmZ0oWlP1Qpzg7Dwzxr298ewdUQ)
* [.NET Channel](https://www.youtube.com/channel/UCvtT19MZW8dq5Wwfu6B0oxw/)

## Concepts and features

### API reference for ASP.NET Core

* [.NET API browser](/en-us/dotnet/api/?view=aspnetcore-9.0)

### Servers

* [Overview](fundamentals/servers/overview?view=aspnetcore-10.0)
* [Kestrel](fundamentals/servers/kestrel?view=aspnetcore-10.0)
* [IIS](host-and-deploy/iis/?view=aspnetcore-10.0)
* [HTTP.sys](fundamentals/servers/httpsys?view=aspnetcore-10.0)

### Host and deploy

* [Overview](host-and-deploy/?view=aspnetcore-10.0)
* [Deploy to Azure App Service](host-and-deploy/azure-apps/?view=aspnetcore-10.0)
* [DevOps for ASP.NET Core Developers](/en-us/dotnet/architecture/devops-for-aspnet-developers)
* [Linux with Apache](host-and-deploy/linux-apache?view=aspnetcore-10.0)
* [Linux with Nginx](host-and-deploy/linux-nginx?view=aspnetcore-10.0)
* [Kestrel](fundamentals/servers/kestrel?view=aspnetcore-10.0)
* [IIS](host-and-deploy/iis/?view=aspnetcore-10.0)
* [HTTP.sys](fundamentals/servers/httpsys?view=aspnetcore-10.0)
* [Docker](host-and-deploy/docker/?view=aspnetcore-10.0)

### Security and identity

* [Overview](security/?view=aspnetcore-10.0)
* [Choose an identity solution](security/how-to-choose-identity-solution?view=aspnetcore-10.0)
* [Authentication](security/authentication/?view=aspnetcore-10.0)
* [Authorization](security/authorization/introduction?view=aspnetcore-10.0)
* [Course: Secure an ASP.NET Core web app with the Identity framework](/en-us/training/modules/secure-aspnet-core-identity/)
* [Data protection](security/data-protection/introduction?view=aspnetcore-10.0)
* [Secrets management](security/app-secrets?view=aspnetcore-10.0)
* [Enforce HTTPS](security/enforcing-ssl?view=aspnetcore-10.0)
* [Host Docker with HTTPS](security/docker-https?view=aspnetcore-10.0)

### Globalization and localization

* [Overview](fundamentals/localization?view=aspnetcore-10.0)
* [Portable object localization](fundamentals/portable-object-localization?view=aspnetcore-10.0)
* [Localization extensibility](fundamentals/localization-extensibility?view=aspnetcore-10.0)
* [Troubleshoot](fundamentals/troubleshoot-aspnet-core-localization?view=aspnetcore-10.0)

### Test, debug and troubleshoot

* [Razor Pages unit tests](test/razor-pages-tests?view=aspnetcore-10.0)
* [Remote debugging](/en-us/visualstudio/debugger/remote-debugging-azure?view=vs-2022)
* [Snapshot debugging](/en-us/azure/azure-monitor/app/snapshot-debugger?bc=%252faspnet%252fcore%252fbreadcrumb%252ftoc.json\&toc=%252faspnet%252fcore%252ftoc.json\&view=aspnetcore-3.1)
* [Integration tests](test/integration-tests?view=aspnetcore-10.0)
* [Load and stress testing](test/load-tests?view=aspnetcore-10.0)
* [Troubleshoot and debug](test/troubleshoot?view=aspnetcore-10.0)
* [Logging](fundamentals/logging/?view=aspnetcore-10.0)
* [Load test Azure web apps by using Azure DevOps](/en-us/training/modules/load-test-web-app-azure-devops/)

### Azure and ASP.NET Core

* [Deploy an ASP.NET Core web app](/en-us/azure/app-service/app-service-web-get-started-dotnet)
* [ASP.NET Core and Docker](/en-us/visualstudio/containers/container-tools?view=vs-2022)
* [Host a web application with Azure App Service](/en-us/training/modules/host-a-web-app-with-azure-app-service/)
* [App Service and Azure SQL Database](/en-us/azure/app-service/app-service-web-tutorial-dotnet-sqldatabase)
* [Managed identity with ASP.NET Core and Azure SQL Database](/en-us/azure/app-service/app-service-web-tutorial-connect-msi)
* [Web API with CORS in Azure App Service](/en-us/azure/app-service/app-service-web-tutorial-rest-api)
* [Capture Web Application Logs with App Service Diagnostics Logging](/en-us/training/modules/capture-application-logs-app-service/)

### Performance

* [Overview](performance/performance-best-practices?view=aspnetcore-10.0)
* [Memory and garbage collection](performance/memory?view=aspnetcore-10.0)
* [Response caching](performance/caching/response?view=aspnetcore-10.0)
* [Response compression](performance/response-compression?view=aspnetcore-10.0)
* [Diagnostic tools](performance/diagnostic-tools?view=aspnetcore-10.0)
* [Load and stress testing](test/load-tests?view=aspnetcore-10.0)

### Advanced features

* [Model binding](mvc/models/model-binding?view=aspnetcore-10.0)
* [Model validation](mvc/models/validation?view=aspnetcore-10.0)
* [Write middleware](fundamentals/middleware/?view=aspnetcore-10.0)
* [Request and response operations](fundamentals/middleware/request-response?view=aspnetcore-10.0)
* [URL rewriting](fundamentals/url-rewriting?view=aspnetcore-10.0)

### Migration

* [ASP.NET Core 8.0 to 9.0](migration/80-90?view=aspnetcore-10.0)
* [ASP.NET Core 7.0 to 8.0](migration/70-80?view=aspnetcore-10.0)
* [ASP.NET Core 6.0 to 7.0](migration/60-70?view=aspnetcore-10.0)
* [ASP.NET Core 5.0 to 6.0](migration/50-to-60?view=aspnetcore-10.0)
* [ASP.NET Core 5.0 code samples to 6.0 minimal hosting model](migration/50-to-60-samples?view=aspnetcore-10.0)
* [Earlier versions](/en-us/aspnet/core/migration/)

### Architecture

* [Choose between traditional web apps and Single Page Apps (SPAs)](/en-us/dotnet/architecture/modern-web-apps-azure/choose-between-traditional-web-and-single-page-apps)
* [Architectural principles](/en-us/dotnet/architecture/modern-web-apps-azure/architectural-principles)
* [Common web application architectures](/en-us/dotnet/architecture/modern-web-apps-azure/common-web-application-architectures)
* [Common client-side web technologies](/en-us/dotnet/architecture/modern-web-apps-azure/common-client-side-web-technologies)
* [Development process for Azure](/en-us/dotnet/architecture/modern-web-apps-azure/development-process-for-azure)

Contribute to ASP.NET Core docs. Read our [contributor guide](https://github.com/dotnet/AspNetCore.Docs/blob/main/CONTRIBUTING.md).

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/splat-attributes-and-arbitrary-parameters
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/splat-attributes-and-arbitrary-parameters.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor attribute splatting and arbitrary parameters

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Components can capture and render additional attributes in addition to the component's declared parameters and fields. Additional attributes can be captured in a dictionary and then applied to an element, called *splatting*, when the component is rendered using the [`@attributes`](../../mvc/views/razor?view=aspnetcore-10.0#attributes) Razor directive attribute. This scenario is useful for defining a component that produces a markup element that supports a variety of customizations. For example, it can be tedious to define attributes separately for an `<input>` that supports many parameters or fields.

## Attribute splatting

In the following `Splat` component:

* The first `<input>` element (`id="useIndividualParams"`) uses individual component fields.
* The second `<input>` element (`id="useAttributesDict"`) uses attribute splatting.

`Splat.razor`:

```
@page "/splat"

<PageTitle>SPLAT!</PageTitle>

<h1>Splat Parameters Example</h1>

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
```

```
@page "/splat"

<PageTitle>SPLAT!</PageTitle>

<h1>Splat Parameters Example</h1>

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
```

```
@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
```

```
@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
```

```
@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
```

```
@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new Dictionary<string, object>()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
```

Except for `id`, the rendered `<input>` elements in the webpage have identical attributes:

```
<input id="useIndividualParams"
       maxlength="10"
       placeholder="Input placeholder text"
       required="required"
       size="50">

<input id="useAttributesDict"
       maxlength="10"
       placeholder="Input placeholder text"
       required="required"
       size="50">
```

Although the preceding example uses fields for the first `<input>` element (`id="useIndividualParams"`), the same behavior applies when component parameters are used.

## Arbitrary attributes

To accept arbitrary attributes, define a [component parameter](./?view=aspnetcore-10.0#component-parameters) with the [CaptureUnmatchedValues](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute.captureunmatchedvalues#microsoft-aspnetcore-components-parameterattribute-captureunmatchedvalues) property set to `true`:

```
@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object>? InputAttributes { get; set; }
}
```

The [CaptureUnmatchedValues](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute.captureunmatchedvalues#microsoft-aspnetcore-components-parameterattribute-captureunmatchedvalues) property on [`[Parameter]`](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute) allows the parameter to match all attributes that don't match any other parameter. A component can only define a single parameter with [CaptureUnmatchedValues](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute.captureunmatchedvalues#microsoft-aspnetcore-components-parameterattribute-captureunmatchedvalues). The property type used with [CaptureUnmatchedValues](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute.captureunmatchedvalues#microsoft-aspnetcore-components-parameterattribute-captureunmatchedvalues) must be assignable from [`Dictionary<string, object>`](/en-us/dotnet/api/system.collections.generic.dictionary-2) with string keys. Use of [`IEnumerable<KeyValuePair<string, object>>`](/en-us/dotnet/api/system.collections.generic.ienumerable-1) or [`IReadOnlyDictionary<string, object>`](/en-us/dotnet/api/system.collections.generic.ireadonlydictionary-2) are also options in this scenario.

The position of [`@attributes`](../../mvc/views/razor?view=aspnetcore-10.0#attributes) relative to the position of element attributes is important. When [`@attributes`](../../mvc/views/razor?view=aspnetcore-10.0#attributes) are splatted on the rendered element, the attributes are processed from right to left (last to first) with the first attribute winning for any common attributes. Consider the following example of a parent component that consumes a child component, where the child sets an "`extra`" attribute and the parent component splats an "`extra`" attribute on the child component.

`AttributeOrderChild1.razor`:

```
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
```

```
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
```

```
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
```

```
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
```

```
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}
```

```
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}
```

`AttributeOrder1.razor`:

```
@page "/attribute-order-1"

<PageTitle>Attribute Order 1</PageTitle>

<h1>Attribute Order Example 1</h1>

<AttributeOrderChild1 extra="10" />

<p>
    View the HTML markup in your browser to inspect the attributes on
    the AttributeOrderChild1 component.
</p>
```

`AttributeOrder1.razor`:

```
@page "/attribute-order-1"

<PageTitle>Attribute Order 1</PageTitle>

<h1>Attribute Order Example 1</h1>

<AttributeOrderChild1 extra="10" />

<p>
    View the HTML markup in your browser to inspect the attributes on
    the AttributeOrderChild1 component.
</p>
```

`AttributeOrderParent1.razor`:

```
@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />
```

`AttributeOrderParent1.razor`:

```
@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />
```

`AttributeOrderParent1.razor`:

```
@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />
```

`AttributeOrderParent1.razor`:

```
@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />
```

The `AttributeOrderChild1` component's `extra` attribute is set to the right of [`@attributes`](../../mvc/views/razor?view=aspnetcore-10.0#attributes). The `AttributeOrderParent1` component's rendered `<div>` contains `extra="5"` when passed through the additional attribute because the attributes are processed right to left (last to first) with the first "`extra`" attribute winning, which is the hard-coded `extra` HTML attribute of the `AttributeOrderParent1` component:

```
<div extra="5" />
```

In the following example, the order of `extra` and [`@attributes`](../../mvc/views/razor?view=aspnetcore-10.0#attributes) is reversed in the child component's `<div>`. In this scenario, the `AttributeOrderParent2` component's rendered `<div>` contains `extra="10"` when passed through the additional attribute because the first "`extra`" attribute processed is the splatted `extra` HTML attribute from the parent component.

`AttributeOrderChild2.razor`:

```
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
```

```
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
```

```
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
```

```
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
```

```
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}
```

```
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}
```

`AttributeOrder2.razor`:

```
@page "/attribute-order-2"

<PageTitle>Attribute Order 2</PageTitle>

<h1>Attribute Order Example 2</h1>

<AttributeOrderChild2 extra="10" />

<p>
    View the HTML markup in your browser to inspect the attributes on
    the AttributeOrderChild2 component.
</p>
```

`AttributeOrder2.razor`:

```
@page "/attribute-order-2"

<PageTitle>Attribute Order 2</PageTitle>

<h1>Attribute Order Example 2</h1>

<AttributeOrderChild2 extra="10" />

<p>
    View the HTML markup in your browser to inspect the attributes on
    the AttributeOrderChild2 component.
</p>
```

`AttributeOrderParent2.razor`:

```
@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />
```

`AttributeOrderParent2.razor`:

```
@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />
```

`AttributeOrderParent2.razor`:

```
@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />
```

`AttributeOrderParent2.razor`:

```
@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />
```

The `<div>` in the parent component's rendered webpage contains `extra="10"`:

```
<div extra="10" />
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel/endpoints.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure endpoints for the ASP.NET Core Kestrel web server

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Kestrel endpoints provide the infrastructure for listening to incoming requests and routing them to the appropriate middleware. The combination of an address and a protocol defines an endpoint.

* The address specifies the network interface that the server listens on for incoming requests, such as a TCP port.
* The protocol specifies the communication between the client and server, such as HTTP/1.1, HTTP/2, or HTTP/3.
* An endpoint can be secured by using the `https` URL scheme or `UseHttps` method.

Endpoints can be configured with URLs, JSON in the *appsettings.json* file, and code. This article describes how to use each option to configure endpoints, HTTPS, and HTTP protocols.

## Identify the default endpoint

The configuration for new ASP.NET Core projects binds each project to a default endpoint. The configuration selects a random HTTP port between 5000-5300 and a random HTTPS port between 7000-7300. The selected ports are stored in the generated *Properties/launchSettings.json* file and are modifiable by the developer. The *launchSetting.json* file is used for local development only.

If there's no endpoint configuration, Kestrel binds to the `http://localhost:5000` URL.

## Configure endpoints

Kestrel endpoints listen for incoming connections. When an endpoint is created, it must be configured with the address to use for listening. The address is usually a TCP address and port number.

You have several options for configuring endpoints. You can specify the URLs or ports directly, define the addresses in your code, or create the endpoints with JSON in the *appsettings.json* file.

### Specify endpoints with URLs

The following sections explain how to configure endpoints by using the following resources:

* `ASPNETCORE_URLS` environment variable
* `--urls` command-line argument
* `urls` host configuration key
* [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls) extension method
* [WebApplication.Urls](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.urls#microsoft-aspnetcore-builder-webapplication-urls) property

#### URL formats

The URLs indicate the IP or host addresses with ports and protocols that the server listens on. You can omit the port if it's the default for the protocol (typically 80 and 443). URLs can be in any of the following formats.

* IPv4 address with port number:

  ```
  http://65.55.39.10:80/
  ```

  `0.0.0.0` is a special case that binds to all IPv4 addresses.

* IPv6 address with port number:

  ```
  http://[0:0:0:0:0:ffff:4137:270a]:80/
  ```

  `[::]` is the IPv6 equivalent of IPv4 `0.0.0.0`.

* Wildcard (`*`) host with port number:

  ```
  http://contoso.com:80/
  http://*:80/
  ```

  Anything not recognized as a valid IP address or `localhost` is treated as a wildcard that binds to all IPv4 and IPv6 addresses. Some developers prefer to use the asterisk `*` or plus symbol `+` to be more explicit. To bind different host names to different ASP.NET Core apps on the same port, use [HTTP.sys](../httpsys?view=aspnetcore-10.0) or a reverse proxy server. Reverse proxy server examples include IIS, YARP, Nginx, and Apache.

* Host name `localhost` with port number or loopback IP with port number:

  ```
  http://localhost:5000/
  http://127.0.0.1:5000/
  http://[::1]:5000/
  ```

  When `localhost` is specified, Kestrel attempts to bind to both IPv4 and IPv6 loopback interfaces. If the requested port is in use by another service on either loopback interface, Kestrel fails to start. If either loopback interface is unavailable for any other reason (most commonly because IPv6 isn't supported), Kestrel logs a warning.

* Specify multiple URL prefixes by using a semicolon (`;`) delimiter, for example:

  ```
  http://*:5000;http://localhost:5001;https://hostname:5002
  ```

For more information, see [Override configuration](../../host/web-host?view=aspnetcore-10.0#override-configuration).

#### HTTPS URL prefixes

You can define endpoints by using HTTPS URL prefixes only if a default certificate is provided in the HTTPS endpoint configuration. For example, use the [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelserveroptions) configuration or a configuration file. This approach is described in the [Configure HTTPS in appsettings.json](#configure-https-in-appsettingsjson) section later in this article. For more information, see the [Configure HTTPS](#configure-https) section.

### Specify ports only

Most configurations for apps and containers define only a port for listening, like port 80, without specifying other constraints like the host or path. `HTTP_PORTS` and `HTTPS_PORTS` are config keys that specify the listening ports for the Kestrel and HTTP.sys servers. You can specify the keys as environment variables defined with the `DOTNET_` or `ASPNETCORE_` prefixes, or set them directly through any other config input, such as the `appsettings.json` file. Each configuration is a semicolon-delimited list of port values, as shown in the following example:

```
ASPNETCORE_HTTP_PORTS=80;8080
ASPNETCORE_HTTPS_PORTS=443;8081
```

The configuration in the example is shorthand for the following specification, which defines the scheme (HTTP or HTTPS) and any host or IP:

```
ASPNETCORE_URLS=http://*:80/;http://*:8080/;https://*:443/;https://*:8081/
```

The `HTTP_PORTS` and `HTTPS_PORTS` configuration keys are lower priority. If other URLs or values are set directly in code, they can override the configuration keys. Configure certificates separately using server-specific mechanics for HTTPS.

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../../../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### Create endpoints in `appsettings.json`

Kestrel can load endpoints from an [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) instance. By default, Kestrel configuration is loaded from the `Kestrel` section and endpoints are configured in `Kestrel:Endpoints`:

```
{
  "Kestrel": {
    "Endpoints": {
      "MyHttpEndpoint": {
        "Url": "http://localhost:8080"
      }
    }
  }
}
```

The example code:

* Uses the *appsettings.json* file as the configuration source. However, any `IConfiguration` source can be used.
* Adds an endpoint named `MyHttpEndpoint` on port 8080.

For more information about configuring endpoints with JSON, see the [Configure HTTPS in appsettings.json](#configure-https-in-appsettingsjson) and [Configure HTTP protocols in appsettings.json](#configure-http-protocols-in-appsettingsjson) sections later in this article.

#### Reload endpoints from configuration

By default, the endpoint configuration can be reloaded when the configuration source changes. It can be disabled by using the [Configure(IConfiguration, Boolean)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configure#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configure\(microsoft-extensions-configuration-iconfiguration-system-boolean\)) method.

When a change is signaled:

* The new configuration is compared to the old version. Any endpoint without configuration changes isn't modified.
* Removed or modified endpoints are allowed 5 seconds to complete processing requests and shut down.
* New or modified endpoints are started.

Clients connecting to a modified endpoint might be disconnected or refused while the endpoint is restarted.

#### ConfigurationLoader

The [Configure](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configure) returns a [KestrelConfigurationLoader](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelconfigurationloader). The loader's [Endpoint(String, Action\<EndpointConfiguration>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelconfigurationloader.endpoint#microsoft-aspnetcore-server-kestrel-kestrelconfigurationloader-endpoint\(system-string-system-action\(\(microsoft-aspnetcore-server-kestrel-endpointconfiguration\)\)\)) method can be used to supplement a configured endpoint's settings:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    var kestrelSection = context.Configuration.GetSection("Kestrel");

    serverOptions.Configure(kestrelSection)
        .Endpoint("HTTPS", listenOptions =>
        {
            // ...
        });
});
```

`KestrelServerOptions.ConfigurationLoader` can be directly accessed to continue iterating on the existing loader, such as the one provided by the [WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost).

* The configuration section for each endpoint is available on the options in the [Endpoint](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelconfigurationloader.endpoint) method so custom settings can be read.
* [KestrelServerOptions.Configure(IConfiguration)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configure#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configure\(microsoft-extensions-configuration-iconfiguration\)) can be called multiple times, but only the last configuration is used unless `Load` is explicitly called on prior instances. The default host doesn't call `Load` so its default configuration section might be replaced.
* `KestrelConfigurationLoader` mirrors the `Listen` family of APIs from `KestrelServerOptions` as `Endpoint` overloads, so code and config endpoints can be configured in the same place. These overloads don't use names and only consume default settings from configuration.

### Define endpoints in the code

[KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions) provides methods for configuring endpoints in code:

* [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen)
* [ListenLocalhost](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenlocalhost)
* [ListenAnyIP](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenanyip)
* [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket)
* [ListenNamedPipe](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listennamedpipe)

When both the `Listen` and [UseUrls](#specify-endpoints-with-urls) APIs are used simultaneously, the `Listen` endpoints override the `UseUrls` endpoints.

#### Bind to a TCP socket

The [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen), [ListenLocalhost](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenlocalhost), and [ListenAnyIP](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenanyip) methods bind to a TCP socket:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
    });
});
```

The example code:

* Configures endpoints that listen on port 5000 and 5001.
* Configures HTTPS for an endpoint with the [UseHttps](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionshttpsextensions.usehttps) extension method on a [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions) object. For more information, see [Configure HTTPS in code](#configure-https-in-code).

On Windows, self-signed certificates can be created by using the [New-SelfSignedCertificate PowerShell cmdlet](/en-us/powershell/module/pki/new-selfsignedcertificate). For an unsupported example, see the [UpdateIISExpressSSLForChrome.ps1](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/includes/make-x509-cert/UpdateIISExpressSSLForChrome.ps1) certificate file on GitHub.

On macOS, Linux, and Windows, create certificates can be created by using [OpenSSL](https://www.openssl.org/).

#### Bind to a Unix socket

Listen on a Unix socket with [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) for improved performance with Nginx, as shown in this example:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ListenUnixSocket("/tmp/kestrel-test.sock");
});
```

* In the Nginx configuration file, set the `server` > `location` > `proxy_pass` entry to `http://unix:/tmp/{KESTREL SOCKET}:/;`, where `{KESTREL SOCKET}` is the name of the socket provided to [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket). In the code example, the name is `kestrel-test.sock`.
* Ensure the socket is writeable by Nginx. (You can set the write permissions on the socket with the `chmod go+w /tmp/kestrel-test.sock` command).

#### Configure endpoint defaults

[ConfigureEndpointDefaults(Action\<ListenOptions>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configureendpointdefaults#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configureendpointdefaults\(system-action\(\(microsoft-aspnetcore-server-kestrel-core-listenoptions\)\)\)) specifies configuration that runs for each specified endpoint. Multiple calls to `ConfigureEndpointDefaults` replace the previous configuration.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // ...
    });
});
```

Note

Endpoints created by calling the [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling the [ConfigureEndpointDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configureendpointdefaults) don't have the defaults applied.

### Use dynamic port binding

When port number `0` is specified, Kestrel dynamically binds to an available port. The following example shows how to determine the port bound by Kestrel at runtime:

```
app.Run(async (context) =>
{
    var serverAddressFeature = context.Features.Get<IServerAddressesFeature>();

    if (serverAddressFeature is not null)
    {
        var listenAddresses = string.Join(", ", serverAddressFeature.Addresses);

        // ...
    }
});
```

Dynamically binding a port isn't available in some scenarios:

* The code calls the [ListenLocalhost](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenlocalhost).
* The code binds together TCP-based HTTP/1.1 or HTTP/2 with QUIC-based HTTP/3.

## Configure HTTPS

Kestrel supports securing endpoints with HTTPS. Data sent over HTTPS is encrypted by using [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246) to increase the security of data transferred between the client and server.

HTTPS requires a TLS certificate. The TLS certificate is stored on the server, and Kestrel is configured to use it. An app can use the [ASP.NET Core HTTPS development certificate](../../../security/enforcing-ssl?view=aspnetcore-10.0) in a local development environment. The development certificate isn't installed in nondevelopment environments. In production, a TLS certificate must be explicitly configured. At a minimum, a default certificate must be provided.

How HTTPS and the TLS certificate is configured depends on how endpoints are configured. If [URL prefixes](#specify-endpoints-with-urls) or [only specified ports](#specify-ports-only) are used to define endpoints, HTTPS can be used only if a default certificate is provided in HTTPS endpoint configuration. A default certificate can be configured with the following options:

* [Configure HTTPS in appsettings.json](#configure-https-in-appsettingsjson)
* [Configure HTTPS in code](#configure-https-in-code)

### Configure HTTPS in appsettings.json

A default HTTPS app settings configuration schema is available for Kestrel. Configure multiple endpoints, including the URLs and the certificates to use, either from a file on disk or from a certificate store.

Any HTTPS endpoint that doesn't specify a certificate (`HttpsDefaultCert` in the following example code) falls back to the certificate defined under `Certificates:Default` or the development certificate.

The following example is for the *appsettings.json* file, but any configuration source can be used:

```
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertAndKeyFile": {
        "Url": "https://localhost:5002",
        "Certificate": {
          "Path": "<path to .pem/.crt file>",
          "KeyPath": "<path to .key file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertStore": {
        "Url": "https://localhost:5003",
        "Certificate": {
          "Subject": "<subject; required>",
          "Store": "<certificate store; required>",
          "Location": "<location; defaults to CurrentUser>",
          "AllowInvalid": "<true or false; defaults to false>"
        }
      },
      "HttpsDefaultCert": {
        "Url": "https://localhost:5004"
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}
```

Warning

In the example code, the certificate password is stored as plain text in the *appsettings.json* file. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). It's a best practice to not use development secrets for production or testing.

#### Schema notes

* Endpoint names are [case-insensitive](../../configuration/?view=aspnetcore-10.0#configuration-keys-and-values). For example, `HTTPS` and `Https` are equivalent.
* The `Url` parameter is required for each endpoint. The format for this parameter is the same as the top-level `Urls` configuration parameter, but it can have only a single value. For more information, see the [URL formats](#url-formats) section in this article.
* These endpoints replace the values defined in the top-level `Urls` configuration rather than adding to them. Endpoints defined in code with the `Listen` API are cumulative with the endpoints defined in the configuration section.
* The `Certificate` section is optional. If the `Certificate` section isn't specified, the defaults defined in `Certificates:Default` are used. If no defaults are available, the development certificate is used. If there are no defaults and the development certificate isn't present, the server throws an exception and fails to start.
* The `Certificate` section supports multiple certificate sources.
* Any number of endpoints can be defined in `Configuration`, as long as they don't cause port conflicts.

#### Certificate sources

Certificate nodes can be configured to load certificates from various sources:

* `Path` and `Password`: Load *.pfx* files.
* `Path`, `KeyPath`, and `Password`: Load *.pem*/*.crt* and *.key* files.
* `Subject` and `Store`: Load from the certificate store.

For example, the `Certificates:Default` certificate can be specified with the following JSON:

```
"Default": {
  "Subject": "<subject; required>",
  "Store": "<cert store; required>",
  "Location": "<location; defaults to CurrentUser>",
  "AllowInvalid": "<true or false; defaults to false>"
}
```

#### Configure client certificates in appsettings.json

The [ClientCertificateMode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode) is used to configure client certificate behavior.

```
{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "ClientCertificateMode": "AllowCertificate",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}
```

Warning

In the example code, the certificate password is stored as plain text in the *appsettings.json* file. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). It's a best practice to not use development secrets for production or testing.

The default value is `ClientCertificateMode.NoCertificate`, where Kestrel doesn't request or require a certificate from the client.

For more information, see [Configure certificate authentication in ASP.NET Core](../../../security/authentication/certauth?view=aspnetcore-10.0).

#### Configure SSL/TLS protocols in appsettings.json

SSL Protocols are protocols used for encrypting and decrypting traffic between two peers, which traditionally are a client and a server.

```
{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "SslProtocols": ["Tls12", "Tls13"],
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}
```

Warning

In the example code, the certificate password is stored as plain text in the *appsettings.json* file. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). It's a best practice to not use development secrets for production or testing.

The default value, `SslProtocols.None`, causes Kestrel to use the operating system defaults to choose the best protocol. Unless you have a specific reason to select a protocol, use the default.

### Configure HTTPS in code

When you use the `Listen` API, the [UseHttps](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionshttpsextensions.usehttps) extension method on [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions) is available to configure HTTPS.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
    });
});
```

The `ListenOptions.UseHttps` parameters include:

* `filename`: The path and file name of a certificate file, relative to the directory that contains the app's content files.
* `password`: The password required to access the X.509 certificate data.
* `configureOptions`: An `Action` to configure the `HttpsConnectionAdapterOptions`. Returns the `ListenOptions`.
* `storeName`: The certificate store from which to load the certificate.
* `subject`: The subject name for the certificate.
* `allowInvalid`: Indicates whether to consider invalid certificates, such as self-signed certificates.
* `location`: The store location to load the certificate from.
* `serverCertificate`: The X.509 certificate.

For a complete list of `UseHttps` overloads, see [UseHttps](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionshttpsextensions.usehttps).

#### Configure client certificates in code

The [ClientCertificateMode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode) configures the client certificate requirements.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
    });
});
```

The default value is [NoCertificate](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode#microsoft-aspnetcore-server-kestrel-https-clientcertificatemode-nocertificate) (0), where Kestrel doesn't request or require a certificate from the client.

For more information, see [Configure certificate authentication in ASP.NET Core](../../../security/authentication/certauth?view=aspnetcore-10.0).

#### Configure HTTPS defaults in code

The [ConfigureHttpsDefaults(Action\<HttpsConnectionAdapterOptions>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configurehttpsdefaults\(system-action\(\(microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions\)\)\)) specifies a configuration `Action` to run for each HTTPS endpoint. Multiple calls to `ConfigureHttpsDefaults` replace prior `Action` instances with the last `Action` specified.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        // ...
    });
});
```

Note

Endpoints created by calling the [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling the [ConfigureHttpsDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults) don't have the defaults applied.

#### Configure SSL/TLS protocols in code

SSL protocols are protocols used for encrypting and decrypting traffic between two peers, which traditionally are a client and a server.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.SslProtocols = SslProtocols.Tls13;
    });
});
```

#### Configure TLS cipher suites filter in code

On Linux, a [CipherSuitesPolicy](/en-us/dotnet/api/system.net.security.ciphersuitespolicy) object can be used to filter TLS handshakes on a per-connection basis:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.OnAuthenticate = (context, sslOptions) =>
        {
            sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(
                new[]
                {
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
                    // ...
                });
        };
    });
});
```

## Configure Server Name Indication

[Server Name Indication (SNI)](https://tools.ietf.org/html/rfc6066#section-3) can be used to host multiple domains on the same IP address and port. SNI can be used to conserve resources by serving multiple sites from one server.

For SNI to function, the client sends the host name for the secure session to the server during the TLS handshake so the server can provide the correct certificate. The client uses the furnished certificate for encrypted communication with the server during the secure session that follows the TLS handshake.

All websites must run on the same Kestrel instance. Kestrel doesn't support sharing an IP address and port across multiple instances without a reverse proxy.

SNI can be configured in two ways:

* Configure a mapping between host names and HTTPS options in [Configuration](../../configuration/?view=aspnetcore-10.0). For example, specify JSON in the *appsettings.json* file.
* Create an endpoint in code and select a certificate by using the host name with the [ServerCertificateSelector](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.servercertificateselector) callback property.

### Configure SNI in appsettings.json

Kestrel supports SNI defined in configuration. An endpoint can be configured with an `Sni` object that contains a mapping between host names and HTTPS options. The connection host name is matched to the options and they're used for that connection.

The following configuration adds an endpoint named `MySniEndpoint` that uses SNI to select HTTPS options based on the host name:

```
{
  "Kestrel": {
    "Endpoints": {
      "MySniEndpoint": {
        "Url": "https://*",
        "SslProtocols": ["Tls11", "Tls12"],
        "Sni": {
          "a.example.org": {
            "Protocols": "Http1AndHttp2",
            "SslProtocols": ["Tls11", "Tls12", "Tls13"],
            "Certificate": {
              "Subject": "<subject; required>",
              "Store": "<certificate store; required>",
            },
            "ClientCertificateMode" : "NoCertificate"
          },
          "*.example.org": {
            "Certificate": {
              "Path": "<path to .pfx file>",
              "Password": "$CREDENTIAL_PLACEHOLDER$"
            }
          },
          "*": {
            // At least one subproperty needs to exist per SNI section or it
            // cannot be discovered via IConfiguration
            "Protocols": "Http1",
          }
        }
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}
```

Warning

In the example code, the certificate password is stored as plain text in the *appsettings.json* file. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). It's a best practice to not use development secrets for production or testing.

SNI can override the following HTTPS options:

* `Certificate` configures the [certificate source](#certificate-sources).
* `Protocols` configures the allowed [HTTP protocols](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.httpprotocols).
* `SslProtocols` configures the allowed [SSL protocols](/en-us/dotnet/api/system.security.authentication.sslprotocols).
* `ClientCertificateMode` configures the [client certificate requirements](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode).

The host name supports the following wildcard matching:

* **Exact match**: For example, `a.example.org` matches `a.example.org`.
* **Wildcard prefix**: If there are multiple wildcard matches, the longest pattern is selected. For example, `*.example.org` matches `b.example.org` and `c.example.org`.
* **Full wildcard**: The wildcard `*` matches everything else, including clients that don't use SNI and don't send a host name.

The matched SNI configuration is applied to the endpoint for the connection, overriding values on the endpoint. If a connection doesn't match a configured SNI host name, the connection is refused.

### Configure SNI with code

Kestrel supports SNI with several callback APIs:

* `ServerCertificateSelector`
* `ServerOptionsSelectionCallback`
* `TlsHandshakeCallbackOptions`

#### SNI with `ServerCertificateSelector`

Kestrel supports SNI via the `ServerCertificateSelector` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var localhostCert = CertificateLoader.LoadFromStoreCert(
                "localhost", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var exampleCert = CertificateLoader.LoadFromStoreCert(
                "example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var subExampleCert = CertificateLoader.LoadFromStoreCert(
                "sub.example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var certs = new Dictionary<string, X509Certificate2>(
                StringComparer.OrdinalIgnoreCase)
            {
                ["localhost"] = localhostCert,
                ["example.com"] = exampleCert,
                ["sub.example.com"] = subExampleCert
            };

            httpsOptions.ServerCertificateSelector = (connectionContext, name) =>
            {
                if (name is not null && certs.TryGetValue(name, out var cert))
                {
                    return cert;
                }

                return exampleCert;
            };
        });
    });
});
```

#### SNI with `ServerOptionsSelectionCallback`

Kestrel supports more dynamic TLS configuration via the `ServerOptionsSelectionCallback` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate and TLS configuration. Default certificates and `ConfigureHttpsDefaults` aren't used with this callback.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        var localhostCert = CertificateLoader.LoadFromStoreCert(
            "localhost", "My", StoreLocation.CurrentUser,
            allowInvalid: true);
        var exampleCert = CertificateLoader.LoadFromStoreCert(
            "example.com", "My", StoreLocation.CurrentUser,
            allowInvalid: true);

        listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
        {
            if (string.Equals(clientHelloInfo.ServerName, "localhost",
                StringComparison.OrdinalIgnoreCase))
            {
                return new ValueTask<SslServerAuthenticationOptions>(
                    new SslServerAuthenticationOptions
                    {
                        ServerCertificate = localhostCert,
                        // Different TLS requirements for this host
                        ClientCertificateRequired = true
                    });
            }

            return new ValueTask<SslServerAuthenticationOptions>(
                new SslServerAuthenticationOptions
                {
                    ServerCertificate = exampleCert
                });
        }, state: null!);
    });
});
```

#### SNI with `TlsHandshakeCallbackOptions`

Kestrel supports more dynamic TLS configuration via the `TlsHandshakeCallbackOptions.OnConnection` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate, TLS configuration, and other server options. Default certificates and `ConfigureHttpsDefaults` aren't used with this callback.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        var localhostCert = CertificateLoader.LoadFromStoreCert(
            "localhost", "My", StoreLocation.CurrentUser,
            allowInvalid: true);
        var exampleCert = CertificateLoader.LoadFromStoreCert(
            "example.com", "My", StoreLocation.CurrentUser,
            allowInvalid: true);

        listenOptions.UseHttps(new TlsHandshakeCallbackOptions
        {
            OnConnection = context =>
            {
                if (string.Equals(context.ClientHelloInfo.ServerName, "localhost",
                    StringComparison.OrdinalIgnoreCase))
                {
                    // Different TLS requirements for this host
                    context.AllowDelayedClientCertificateNegotation = true;

                    return new ValueTask<SslServerAuthenticationOptions>(
                        new SslServerAuthenticationOptions
                        {
                            ServerCertificate = localhostCert
                        });
                }

                return new ValueTask<SslServerAuthenticationOptions>(
                    new SslServerAuthenticationOptions
                    {
                        ServerCertificate = exampleCert
                    });
            }
        });
    });
});
```

## Configure HTTP protocols

Kestrel supports all commonly used HTTP versions. Endpoints can be configured to support different HTTP versions by using the [HttpProtocols](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.httpprotocols) enum, which specifies available HTTP version options.

TLS is required to support more than one HTTP version. The TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake is used to negotiate the connection protocol between the client and the server when an endpoint supports multiple protocols.

| HttpProtocols value     | Allowed protocol       | Notes                                                                                                                                                                                                                                                   |
| ----------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Http1`                 | HTTP/1.1               | Can be used with or without TLS.                                                                                                                                                                                                                        |
| `Http2`                 | HTTP/2                 | Can be used without TLS, only if the client supports a [Prior Knowledge mode](https://tools.ietf.org/html/rfc7540#section-3.4).                                                                                                                         |
| `Http3`                 | HTTP/3                 | **Requires TLS**. The client might need to be configured to use HTTP/3 only.                                                                                                                                                                            |
| `Http1AndHttp2`         | HTTP/1.1 HTTP/2        | HTTP/2 requires the client to select HTTP/2 in the TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake. Otherwise, the connection defaults to HTTP/1.1.                                        |
| `Http1AndHttp2AndHttp3` | HTTP/1.1 HTTP/2 HTTP/3 | The first client request normally uses HTTP/1.1 or HTTP/2. The ['alt-svc' response header](http3?view=aspnetcore-10.0#alt-svc) prompts the client to upgrade to HTTP/3. HTTP/2 and HTTP/3 requires TLS. Otherwise, the connection defaults to HTTP/1.1. |

The default protocol value for an endpoint is `HttpProtocols.Http1AndHttp2`.

### TLS restrictions for HTTP/2

When you use the HTTP/2 protocol for the connection, the following TLS restrictions apply:

* Requires TLS version 1.2 or later

* Renegotiation is disabled

* Compression is disabled

* Minimum ephemeral key exchange sizes:

  * Elliptic curve Diffie-Hellman (ECDHE) (see \[[RFC 4492](https://www.ietf.org/rfc/rfc4492.txt)]): 224 bits minimum
  * Finite field Diffie-Hellman (DHE) (see TLS12 in \[[RFC 5246](https://www.rfc-editor.org/rfc/rfc5246)]): 2,048 bits minimum

* The Cipher suite isn't prohibited.

The `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` format (see TLS-ECDHE in \[[RFC 8422](https://www.rfc-editor.org/rfc/rfc8422)]) with the P-256 elliptic curve (see \[[FIPS186](https://csrc.nist.gov/pubs/fips/186-5/final)]) is supported by default.

### Configure HTTP protocols in appsettings.json

The following the *appsettings.json* file example establishes the HTTP/1.1 connection protocol for a specific endpoint:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsDefaultCert": {
        "Url": "https://localhost:5001",
        "Protocols": "Http1"
      }
    }
  }
}
```

A default protocol can be configured in the `Kestrel:EndpointDefaults` section. The following *appsettings.json* file example establishes HTTP/1.1 as the default connection protocol for all endpoints:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1"
    }
  }
}
```

Protocols specified in code override values set by configuration.

### Configure HTTP protocols in code

The [ListenOptions.Protocols](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions.protocols#microsoft-aspnetcore-server-kestrel-core-listenoptions-protocols) is used to specify protocols with the [HttpProtocols](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.httpprotocols) enum.

The following example configures an endpoint for HTTP/1.1, HTTP/2, and HTTP/3 connections on port 8000. Connections are made secure with TLS and a supplied certificate:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
    });
});
```

## Customize Kestrel named pipe endpoints

Kestrel's named pipe support includes advanced customization options. The [CreateNamedPipeServerStream](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.namedpipes.namedpipetransportoptions.createnamedpipeserverstream) property on the named pipe options allows pipes to be customized per-endpoint.

This approach is useful in a Kestrel app that requires two pipe endpoints with different [access security](/en-us/windows/win32/ipc/named-pipe-security-and-access-rights). The `CreateNamedPipeServerStream` option can be used to create pipes with custom security settings, depending on the pipe name.

```
using System.IO.Pipes;
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes;

var builder = WebApplication.CreateBuilder();

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenNamedPipe("defaultPipe");
    options.ListenNamedPipe("securedPipe");
});

builder.WebHost.UseNamedPipes(options =>
{
    options.CreateNamedPipeServerStream = (context) =>
    {
        var pipeName = context.NamedPipeEndPoint.PipeName;

        switch (pipeName)
        {
            case "defaultPipe":
                return NamedPipeTransportOptions.CreateDefaultNamedPipeServerStream(context);
            case "securedPipe":
                // A custom PipeSecurity can't be combined with
                // PipeOptions.CurrentUserOnly, so clear the flag for this
                // endpoint only. defaultPipe keeps its default protection.
                var pipeOptions = context.PipeOptions & ~PipeOptions.CurrentUserOnly;

                return NamedPipeServerStreamAcl.Create(pipeName, PipeDirection.InOut,
                    NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte,
                    pipeOptions, inBufferSize: 0, outBufferSize: 0, CreateSecuredPipeSecurity());
            default:
                throw new InvalidOperationException($"Unexpected pipe name: {pipeName}");
        }
    };
});

static PipeSecurity CreateSecuredPipeSecurity()
{
    using var serverIdentity = WindowsIdentity.GetCurrent();
    var pipeSecurity = new PipeSecurity();

    // Only the account the server runs as creates pipe instances, so it's the
    // only principal that needs CreateNewInstance.
    pipeSecurity.AddAccessRule(new PipeAccessRule(
        serverIdentity.User!,
        PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance,
        AccessControlType.Allow));

    // Callers only need to read and write. Replace this SID with the narrowest
    // group that must reach the endpoint.
    pipeSecurity.AddAccessRule(new PipeAccessRule(
        new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null),
        PipeAccessRights.ReadWrite,
        AccessControlType.Allow));

    return pipeSecurity;
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

In the preceding example, only the account the server runs as is granted `CreateNewInstance` access. Kestrel creates several instances of each pipe to accept connections in parallel, and Windows requires that right to create each instance after the first. Callers are granted read/write access only.

Warning

Don't grant `PipeAccessRights.CreateNewInstance` to callers, and avoid `PipeAccessRights.FullControl`, which includes it along with `ChangePermissions` and `TakeOwnership`. `CreateNewInstance` corresponds to the Windows `FILE_CREATE_PIPE_INSTANCE` right, which authorizes a grantee to create additional server instances under the same pipe name. Windows distributes incoming connections across all instances of a pipe, so a process holding that right can accept genuine client connections and impersonate the server.

Note

A pipe's security descriptor controls which accounts can connect to that pipe. It doesn't control which endpoints a connected caller can reach because every endpoint mapped in the app is served on every named pipe endpoint. Use authentication and authorization to restrict individual endpoints.

## Related content

* [Kestrel web server in ASP.NET Core](../kestrel?view=aspnetcore-10.0)
* [Configure options for the ASP.NET Core Kestrel web server](options?view=aspnetcore-10.0)

ASP.NET Core projects are configured to bind to a random HTTP port between 5000-5300 and a random HTTPS port between 7000-7300. This default configuration is specified in the generated `Properties/launchSettings.json` file and can be overridden. If no ports are specified, Kestrel binds to `http://localhost:5000`.

Specify URLs using the:

* `ASPNETCORE_URLS` environment variable.
* `--urls` command-line argument.
* `urls` host configuration key.
* [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls) extension method.

The value provided using these approaches can be one or more HTTP and HTTPS endpoints (HTTPS if a default cert is available). Configure the value as a semicolon-separated list (for example, `"Urls": "http://localhost:8000;http://localhost:8001"`).

For more information on these approaches, see [Server URLs](../../host/web-host?view=aspnetcore-10.0#server-urls) and [Override configuration](../../host/web-host?view=aspnetcore-10.0#override-configuration).

A development certificate is created:

* When the [.NET SDK](/en-us/dotnet/core/sdk) is installed.
* The [dev-certs tool](/en-us/dotnet/core/tools/dotnet-dev-certs) is used to create a certificate.

The development certificate is available only for the user that generates the certificate. Some browsers require granting explicit permission to trust the local development certificate.

Project templates configure apps to run on HTTPS by default and include [HTTPS redirection and HSTS support](../../../security/enforcing-ssl?view=aspnetcore-10.0).

Call [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) or [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) methods on [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions) to configure URL prefixes and ports for Kestrel.

`UseUrls`, the `--urls` command-line argument, `urls` host configuration key, and the `ASPNETCORE_URLS` environment variable also work but have the limitations noted later in this section (a default certificate must be available for HTTPS endpoint configuration).

`KestrelServerOptions` configuration:

## ConfigureEndpointDefaults

[ConfigureEndpointDefaults(Action\<ListenOptions>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configureendpointdefaults#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configureendpointdefaults\(system-action\(\(microsoft-aspnetcore-server-kestrel-core-listenoptions\)\)\)) specifies a configuration `Action` to run for each specified endpoint. Calling `ConfigureEndpointDefaults` multiple times replaces prior `Action`s with the last `Action` specified:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // ...
    });
});
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureEndpointDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configureendpointdefaults) won't have the defaults applied.

## Configure(IConfiguration)

Enables Kestrel to load endpoints from an [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration). The configuration must be scoped to the configuration section for Kestrel. The `Configure(IConfiguration, bool)` overload can be used to enable reloading endpoints when the configuration source changes.

By default, Kestrel configuration is loaded from the `Kestrel` section and reloading changes is enabled:

```
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "Https": {
        "Url": "https://localhost:5001"
      }
    }
  }
}
```

If reloading configuration is enabled and a change is signaled then the following steps are taken:

* The new configuration is compared to the old one, any endpoint without configuration changes are not modified.
* Removed or modified endpoints are given 5 seconds to complete processing requests and shut down.
* New or modified endpoints are started.

Clients connecting to a modified endpoint may be disconnected or refused while the endpoint is restarted.

## ConfigureHttpsDefaults

[ConfigureHttpsDefaults(Action\<HttpsConnectionAdapterOptions>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configurehttpsdefaults\(system-action\(\(microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions\)\)\)) specifies a configuration `Action` to run for each HTTPS endpoint. Calling `ConfigureHttpsDefaults` multiple times replaces prior `Action`s with the last `Action` specified.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        // ...
    });
});
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureHttpsDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults) won't have the defaults applied.

## ListenOptions.UseHttps

Configure Kestrel to use HTTPS.

`ListenOptions.UseHttps` extensions:

* `UseHttps`: Configure Kestrel to use HTTPS with the default certificate. Throws an exception if no default certificate is configured.
* `UseHttps(string fileName)`
* `UseHttps(string fileName, string password)`
* `UseHttps(string fileName, string password, Action<HttpsConnectionAdapterOptions> configureOptions)`
* `UseHttps(StoreName storeName, string subject)`
* `UseHttps(StoreName storeName, string subject, bool allowInvalid)`
* `UseHttps(StoreName storeName, string subject, bool allowInvalid, StoreLocation location)`
* `UseHttps(StoreName storeName, string subject, bool allowInvalid, StoreLocation location, Action<HttpsConnectionAdapterOptions> configureOptions)`
* `UseHttps(X509Certificate2 serverCertificate)`
* `UseHttps(X509Certificate2 serverCertificate, Action<HttpsConnectionAdapterOptions> configureOptions)`
* `UseHttps(Action<HttpsConnectionAdapterOptions> configureOptions)`

`ListenOptions.UseHttps` parameters:

* `filename` is the path and file name of a certificate file, relative to the directory that contains the app's content files.
* `password` is the password required to access the X.509 certificate data.
* `configureOptions` is an `Action` to configure the `HttpsConnectionAdapterOptions`. Returns the `ListenOptions`.
* `storeName` is the certificate store from which to load the certificate.
* `subject` is the subject name for the certificate.
* `allowInvalid` indicates if invalid certificates should be considered, such as self-signed certificates.
* `location` is the store location to load the certificate from.
* `serverCertificate` is the X.509 certificate.

In production, HTTPS must be explicitly configured. At a minimum, a default certificate must be provided.

If certificates are being read from disk, as opposed to a [Windows Certificate Store](/en-us/windows-hardware/drivers/install/certificate-stores), the containing directory must have appropriate permissions to prevent unauthorized access.

Supported configurations described next:

* No configuration
* Replace the default certificate from configuration
* Change the defaults in code

### No configuration

Kestrel listens on `http://localhost:5000`.

[]()

### Replace the default certificate from configuration

A default HTTPS app settings configuration schema is available for Kestrel. Configure multiple endpoints, including the URLs and the certificates to use, either from a file on disk or from a certificate store.

In the following `appsettings.json` example:

* Set `AllowInvalid` to `true` to permit the use of invalid certificates (for example, self-signed certificates).
* Any HTTPS endpoint that doesn't specify a certificate (`HttpsDefaultCert` in the example that follows) falls back to the cert defined under `Certificates:Default` or the development certificate.

```
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertAndKeyFile": {
        "Url": "https://localhost:5002",
        "Certificate": {
          "Path": "<path to .pem/.crt file>",
          "KeyPath": "<path to .key file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertStore": {
        "Url": "https://localhost:5003",
        "Certificate": {
          "Subject": "<subject; required>",
          "Store": "<certificate store; required>",
          "Location": "<location; defaults to CurrentUser>",
          "AllowInvalid": "<true or false; defaults to false>"
        }
      },
      "HttpsDefaultCert": {
        "Url": "https://localhost:5004"
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}
```

Warning

In the preceding example, certificate passwords are stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for each certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

Schema notes:

* Endpoints names are [case-insensitive](../../configuration/?view=aspnetcore-10.0#configuration-keys-and-values). For example, `HTTPS` and `Https` are equivalent.
* The `Url` parameter is required for each endpoint. The format for this parameter is the same as the top-level `Urls` configuration parameter except that it's limited to a single value.
* These endpoints replace those defined in the top-level `Urls` configuration rather than adding to them. Endpoints defined in code via `Listen` are cumulative with the endpoints defined in the configuration section.
* The `Certificate` section is optional. If the `Certificate` section isn't specified, the defaults defined in `Certificates:Default` are used. If no defaults are available, the development certificate is used. If there are no defaults and the development certificate isn't present, the server throws an exception and fails to start.
* The `Certificate` section supports multiple [certificate sources](#certificate-sources).
* Any number of endpoints may be defined in [Configuration](../../configuration/?view=aspnetcore-10.0) as long as they don't cause port conflicts.

#### Certificate sources

Certificate nodes can be configured to load certificates from a number of sources:

* `Path` and `Password` to load *.pfx* files.
* `Path`, `KeyPath` and `Password` to load *.pem*/*.crt* and *.key* files.
* `Subject` and `Store` to load from the certificate store.

For example, the `Certificates:Default` certificate can be specified as:

```
"Default": {
  "Subject": "<subject; required>",
  "Store": "<cert store; required>",
  "Location": "<location; defaults to CurrentUser>",
  "AllowInvalid": "<true or false; defaults to false>"
}
```

#### ConfigurationLoader

[Configure(IConfiguration)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configure#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configure\(microsoft-extensions-configuration-iconfiguration\)) returns a [KestrelConfigurationLoader](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelconfigurationloader) with an [Endpoint(String, Action\<EndpointConfiguration>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelconfigurationloader.endpoint#microsoft-aspnetcore-server-kestrel-kestrelconfigurationloader-endpoint\(system-string-system-action\(\(microsoft-aspnetcore-server-kestrel-endpointconfiguration\)\)\)) method that can be used to supplement a configured endpoint's settings:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    var kestrelSection = context.Configuration.GetSection("Kestrel");

    serverOptions.Configure(kestrelSection)
        .Endpoint("HTTPS", listenOptions =>
        {
            // ...
        });
});
```

`KestrelServerOptions.ConfigurationLoader` can be directly accessed to continue iterating on the existing loader, such as the one provided by [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost).

* The configuration section for each endpoint is available on the options in the `Endpoint` method so that custom settings may be read.
* Multiple configurations may be loaded by calling [Configure(IConfiguration)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configure#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configure\(microsoft-extensions-configuration-iconfiguration\)) again with another section. Only the last configuration is used, unless `Load` is explicitly called on prior instances. The metapackage doesn't call `Load` so that its default configuration section may be replaced.
* `KestrelConfigurationLoader` mirrors the `Listen` family of APIs from `KestrelServerOptions` as `Endpoint` overloads, so code and config endpoints may be configured in the same place. These overloads don't use names and only consume default settings from configuration.

### Change the defaults in code

`ConfigureEndpointDefaults` and `ConfigureHttpsDefaults` can be used to change default settings for `ListenOptions` and `HttpsConnectionAdapterOptions`, including overriding the default certificate specified in the prior scenario. `ConfigureEndpointDefaults` and `ConfigureHttpsDefaults` should be called before any endpoints are configured.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // ...
    });

    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        // ...
    });
});
```

## Configure endpoints using Server Name Indication

[Server Name Indication (SNI)](https://tools.ietf.org/html/rfc6066#section-3) can be used to host multiple domains on the same IP address and port. For SNI to function, the client sends the host name for the secure session to the server during the TLS handshake so that the server can provide the correct certificate. The client uses the furnished certificate for encrypted communication with the server during the secure session that follows the TLS handshake.

SNI can be configured in two ways:

* Create an endpoint in code and select a certificate using the host name with the [ServerCertificateSelector](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.servercertificateselector) callback.
* Configure a mapping between host names and HTTPS options in [Configuration](../../configuration/?view=aspnetcore-10.0). For example, JSON in the `appsettings.json` file.

### SNI with `ServerCertificateSelector`

Kestrel supports SNI via the `ServerCertificateSelector` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var localhostCert = CertificateLoader.LoadFromStoreCert(
                "localhost", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var exampleCert = CertificateLoader.LoadFromStoreCert(
                "example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var subExampleCert = CertificateLoader.LoadFromStoreCert(
                "sub.example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var certs = new Dictionary<string, X509Certificate2>(
                StringComparer.OrdinalIgnoreCase)
            {
                ["localhost"] = localhostCert,
                ["example.com"] = exampleCert,
                ["sub.example.com"] = subExampleCert
            };

            httpsOptions.ServerCertificateSelector = (connectionContext, name) =>
            {
                if (name is not null && certs.TryGetValue(name, out var cert))
                {
                    return cert;
                }

                return exampleCert;
            };
        });
    });
});
```

### SNI with `ServerOptionsSelectionCallback`

Kestrel supports additional dynamic TLS configuration via the `ServerOptionsSelectionCallback` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate and TLS configuration. Default certificates and `ConfigureHttpsDefaults` are not used with this callback.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        var localhostCert = CertificateLoader.LoadFromStoreCert(
            "localhost", "My", StoreLocation.CurrentUser,
            allowInvalid: true);
        var exampleCert = CertificateLoader.LoadFromStoreCert(
            "example.com", "My", StoreLocation.CurrentUser,
            allowInvalid: true);

        listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
        {
            if (string.Equals(clientHelloInfo.ServerName, "localhost",
                StringComparison.OrdinalIgnoreCase))
            {
                return new ValueTask<SslServerAuthenticationOptions>(
                    new SslServerAuthenticationOptions
                    {
                        ServerCertificate = localhostCert,
                        // Different TLS requirements for this host
                        ClientCertificateRequired = true
                    });
            }

            return new ValueTask<SslServerAuthenticationOptions>(
                new SslServerAuthenticationOptions
                {
                    ServerCertificate = exampleCert
                });
        }, state: null!);
    });
});
```

### SNI with `TlsHandshakeCallbackOptions`

Kestrel supports additional dynamic TLS configuration via the `TlsHandshakeCallbackOptions.OnConnection` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate, TLS configuration, and other server options. Default certificates and `ConfigureHttpsDefaults` are not used with this callback.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        var localhostCert = CertificateLoader.LoadFromStoreCert(
            "localhost", "My", StoreLocation.CurrentUser,
            allowInvalid: true);
        var exampleCert = CertificateLoader.LoadFromStoreCert(
            "example.com", "My", StoreLocation.CurrentUser,
            allowInvalid: true);

        listenOptions.UseHttps(new TlsHandshakeCallbackOptions
        {
            OnConnection = context =>
            {
                if (string.Equals(context.ClientHelloInfo.ServerName, "localhost",
                    StringComparison.OrdinalIgnoreCase))
                {
                    // Different TLS requirements for this host
                    context.AllowDelayedClientCertificateNegotation = true;

                    return new ValueTask<SslServerAuthenticationOptions>(
                        new SslServerAuthenticationOptions
                        {
                            ServerCertificate = localhostCert
                        });
                }

                return new ValueTask<SslServerAuthenticationOptions>(
                    new SslServerAuthenticationOptions
                    {
                        ServerCertificate = exampleCert
                    });
            }
        });
    });
});
```

### SNI in configuration

Kestrel supports SNI defined in configuration. An endpoint can be configured with an `Sni` object that contains a mapping between host names and HTTPS options. The connection host name is matched to the options and they are used for that connection.

The following configuration adds an endpoint named `MySniEndpoint` that uses SNI to select HTTPS options based on the host name:

```
{
  "Kestrel": {
    "Endpoints": {
      "MySniEndpoint": {
        "Url": "https://*",
        "SslProtocols": ["Tls11", "Tls12"],
        "Sni": {
          "a.example.org": {
            "Protocols": "Http1AndHttp2",
            "SslProtocols": ["Tls11", "Tls12", "Tls13"],
            "Certificate": {
              "Subject": "<subject; required>",
              "Store": "<certificate store; required>",
            },
            "ClientCertificateMode" : "NoCertificate"
          },
          "*.example.org": {
            "Certificate": {
              "Path": "<path to .pfx file>",
              "Password": "$CREDENTIAL_PLACEHOLDER$"
            }
          },
          "*": {
            // At least one subproperty needs to exist per SNI section or it
            // cannot be discovered via IConfiguration
            "Protocols": "Http1",
          }
        }
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}
```

Warning

In the preceding example, certificate passwords are stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for each certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

HTTPS options that can be overridden by SNI:

* `Certificate` configures the [certificate source](#certificate-sources).
* `Protocols` configures the allowed [HTTP protocols](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.httpprotocols).
* `SslProtocols` configures the allowed [SSL protocols](/en-us/dotnet/api/system.security.authentication.sslprotocols).
* `ClientCertificateMode` configures the [client certificate requirements](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode).

The host name supports wildcard matching:

* Exact match. For example, `a.example.org` matches `a.example.org`.
* Wildcard prefix. If there are multiple wildcard matches then the longest pattern is chosen. For example, `*.example.org` matches `b.example.org` and `c.example.org`.
* Full wildcard. `*` matches everything else, including clients that aren't using SNI and don't send a host name.

The matched SNI configuration is applied to the endpoint for the connection, overriding values on the endpoint. If a connection doesn't match a configured SNI host name then the connection is refused.

### SNI requirements

All websites must run on the same Kestrel instance. Kestrel doesn't support sharing an IP address and port across multiple instances without a reverse proxy.

## SSL/TLS Protocols

SSL Protocols are protocols used for encrypting and decrypting traffic between two peers, traditionally a client and a server.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.SslProtocols = SslProtocols.Tls13;
    });
});
```

```
{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "SslProtocols": ["Tls12", "Tls13"],
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}
```

Warning

In the preceding example, the certificate password is stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

The default value, `SslProtocols.None`, causes Kestrel to use the operating system defaults to choose the best protocol. Unless you have a specific reason to select a protocol, use the default.

## Client Certificates

`ClientCertificateMode` configures the [client certificate requirements](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode).

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
    });
});
```

```
{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "ClientCertificateMode": "AllowCertificate",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}
```

Warning

In the preceding example, the certificate password is stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0).

The default value is `ClientCertificateMode.NoCertificate` where Kestrel will not request or require a certificate from the client.

For more information, see [Configure certificate authentication in ASP.NET Core](../../../security/authentication/certauth?view=aspnetcore-10.0).

## Connection logging

Call [UseConnectionLogging](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionsconnectionloggingextensions.useconnectionlogging) to emit Debug level logs for byte-level communication on a connection. Connection logging is helpful for troubleshooting problems in low-level communication, such as during TLS encryption and behind proxies. If `UseConnectionLogging` is placed before `UseHttps`, encrypted traffic is logged. If `UseConnectionLogging` is placed after `UseHttps`, decrypted traffic is logged. This is built-in [connection middleware](#connection-middleware).

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseConnectionLogging();
    });
});
```

## Bind to a TCP socket

The [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) method binds to a TCP socket, and an options lambda permits X.509 certificate configuration:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
    });
});
```

The example configures HTTPS for an endpoint with [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions). Use the same API to configure other Kestrel settings for specific endpoints.

On Windows, self-signed certificates can be created by using the [New-SelfSignedCertificate PowerShell cmdlet](/en-us/powershell/module/pki/new-selfsignedcertificate). For an unsupported example, see the [UpdateIISExpressSSLForChrome.ps1](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/includes/make-x509-cert/UpdateIISExpressSSLForChrome.ps1) certificate file on GitHub.

On macOS, Linux, and Windows, create certificates can be created by using [OpenSSL](https://www.openssl.org/).

## Bind to a Unix socket

Listen on a Unix socket with [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) for improved performance with Nginx, as shown in this example:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ListenUnixSocket("/tmp/kestrel-test.sock");
});
```

* In the Nginx configuration file, set the `server` > `location` > `proxy_pass` entry to `http://unix:/tmp/{KESTREL SOCKET}:/;`. `{KESTREL SOCKET}` is the name of the socket provided to [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) (for example, `kestrel-test.sock` in the preceding example).
* Ensure that the socket is writeable by Nginx (for example, `chmod go+w /tmp/kestrel-test.sock`).

## Port 0

When the port number `0` is specified, Kestrel dynamically binds to an available port. The following example shows how to determine which port Kestrel bound at runtime:

```
app.Run(async (context) =>
{
    var serverAddressFeature = context.Features.Get<IServerAddressesFeature>();

    if (serverAddressFeature is not null)
    {
        var listenAddresses = string.Join(", ", serverAddressFeature.Addresses);

        // ...
    }
});
```

Dynamically binding a port isn't available in some situations:

* `ListenLocalhost`
* Binding TCP-based HTTP/1.1 or HTTP/2, and QUIC-based HTTP/3 together.

## Limitations

Configure endpoints with the following approaches:

* [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls)
* `--urls` command-line argument
* `urls` host configuration key
* `ASPNETCORE_URLS` environment variable

These methods are useful for making code work with servers other than Kestrel. However, be aware of the following limitations:

* HTTPS can't be used with these approaches unless a default certificate is provided in the HTTPS endpoint configuration (for example, using `KestrelServerOptions` configuration or a configuration file as shown earlier in this article).
* When both the `Listen` and `UseUrls` approaches are used simultaneously, the `Listen` endpoints override the `UseUrls` endpoints.

## IIS endpoint configuration

When using IIS, the URL bindings for IIS override bindings are set by either `Listen` or `UseUrls`. For more information, see [ASP.NET Core Module](../../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0).

## ListenOptions.Protocols

The `Protocols` property establishes the HTTP protocols (`HttpProtocols`) enabled on a connection endpoint or for the server. Assign a value to the `Protocols` property from the `HttpProtocols` enum.

| `HttpProtocols` enum value | Connection protocol permitted                                                                                                                                                                                                                                                            |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Http1`                    | HTTP/1.1 only. Can be used with or without TLS.                                                                                                                                                                                                                                          |
| `Http2`                    | HTTP/2 only. May be used without TLS only if the client supports a [Prior Knowledge mode](https://tools.ietf.org/html/rfc7540#section-3.4).                                                                                                                                              |
| `Http3`                    | HTTP/3 only. Requires TLS. The client may need to be configured to use HTTP/3 only.                                                                                                                                                                                                      |
| `Http1AndHttp2`            | HTTP/1.1 and HTTP/2. HTTP/2 requires the client to select HTTP/2 in the TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake; otherwise, the connection defaults to HTTP/1.1.                                                    |
| `Http1AndHttp2AndHttp3`    | HTTP/1.1, HTTP/2 and HTTP/3. The first client request normally uses HTTP/1.1 or HTTP/2, and the [`alt-svc` response header](http3?view=aspnetcore-10.0#alt-svc) prompts the client to upgrade to HTTP/3. HTTP/2 and HTTP/3 requires TLS; otherwise, the connection defaults to HTTP/1.1. |

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
    });
});
```

On Linux, [CipherSuitesPolicy](/en-us/dotnet/api/system.net.security.ciphersuitespolicy) can be used to filter TLS handshakes on a per-connection basis:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.OnAuthenticate = (context, sslOptions) =>
        {
            sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(
                new[]
                {
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
                    // ...
                });
        };
    });
});
```

## Connection middleware

Custom connection middleware can filter TLS handshakes on a per-connection basis for specific ciphers if necessary.

The following example throws [NotSupportedException](/en-us/dotnet/api/system.notsupportedexception) for any cipher algorithm that the app doesn't support. Alternatively, define and compare [ITlsHandshakeFeature.CipherAlgorithm](/en-us/dotnet/api/microsoft.aspnetcore.connections.features.itlshandshakefeature.cipheralgorithm) to a list of acceptable cipher suites.

No encryption is used with a [CipherAlgorithmType.Null](/en-us/dotnet/api/system.security.authentication.cipheralgorithmtype#system-security-authentication-cipheralgorithmtype-null) cipher algorithm.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");

        listenOptions.Use((context, next) =>
        {
            var tlsFeature = context.Features.Get<ITlsHandshakeFeature>()!;

            if (tlsFeature.CipherAlgorithm == CipherAlgorithmType.Null)
            {
                throw new NotSupportedException(
                    $"Prohibited cipher: {tlsFeature.CipherAlgorithm}");
            }

            return next();
        });
    });
});
```

## Set the HTTP protocol from configuration

By default, Kestrel configuration is loaded from the `Kestrel` section. The following `appsettings.json` example establishes HTTP/1.1 as the default connection protocol for all endpoints:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1"
    }
  }
}
```

The following `appsettings.json` example establishes the HTTP/1.1 connection protocol for a specific endpoint:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsDefaultCert": {
        "Url": "https://localhost:5001",
        "Protocols": "Http1"
      }
    }
  }
}
```

Protocols specified in code override values set by configuration.

## URL prefixes

When using `UseUrls`, `--urls` command-line argument, `urls` host configuration key, or `ASPNETCORE_URLS` environment variable, the URL prefixes can be in any of the following formats.

Only HTTP URL prefixes are valid. Kestrel doesn't support HTTPS when configuring URL bindings using `UseUrls`.

* IPv4 address with port number

  ```
  http://65.55.39.10:80/
  ```

  `0.0.0.0` is a special case that binds to all IPv4 addresses.

* IPv6 address with port number

  ```
  http://[0:0:0:0:0:ffff:4137:270a]:80/
  ```

  `[::]` is the IPv6 equivalent of IPv4 `0.0.0.0`.

* Host name with port number

  ```
  http://contoso.com:80/
  http://*:80/
  ```

  Host names, `*`, and `+`, aren't special. Anything not recognized as a valid IP address or `localhost` binds to all IPv4 and IPv6 IPs. To bind different host names to different ASP.NET Core apps on the same port, use [HTTP.sys](../httpsys?view=aspnetcore-10.0) or a reverse proxy server. Reverse proxy server examples include IIS, Nginx, or Apache.

  Warning

  Hosting in a reverse proxy configuration requires [host filtering](host-filtering?view=aspnetcore-10.0).

* Host `localhost` name with port number or loopback IP with port number

  ```
  http://localhost:5000/
  http://127.0.0.1:5000/
  http://[::1]:5000/
  ```

  When `localhost` is specified, Kestrel attempts to bind to both IPv4 and IPv6 loopback interfaces. If the requested port is in use by another service on either loopback interface, Kestrel fails to start. If either loopback interface is unavailable for any other reason (most commonly because IPv6 isn't supported), Kestrel logs a warning.

ASP.NET Core projects are configured to bind to a random HTTP port between 5000-5300 and a random HTTPS port between 7000-7300. This default configuration is specified in the generated `Properties/launchSettings.json` file and can be overridden. If no ports are specified, Kestrel binds to:

* `http://localhost:5000`
* `https://localhost:5001` (when a local development certificate is present)

Specify URLs using the:

* `ASPNETCORE_URLS` environment variable.
* `--urls` command-line argument.
* `urls` host configuration key.
* [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls) extension method.

The value provided using these approaches can be one or more HTTP and HTTPS endpoints (HTTPS if a default cert is available). Configure the value as a semicolon-separated list (for example, `"Urls": "http://localhost:8000;http://localhost:8001"`).

For more information on these approaches, see [Server URLs](../../host/web-host?view=aspnetcore-10.0#server-urls) and [Override configuration](../../host/web-host?view=aspnetcore-10.0#override-configuration).

A development certificate is created:

* When the [.NET SDK](/en-us/dotnet/core/sdk) is installed.
* The [dev-certs tool](/en-us/dotnet/core/tools/dotnet-dev-certs) is used to create a certificate.

The development certificate is available only for the user that generates the certificate. Some browsers require granting explicit permission to trust the local development certificate.

Project templates configure apps to run on HTTPS by default and include [HTTPS redirection and HSTS support](../../../security/enforcing-ssl?view=aspnetcore-10.0).

Call [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) or [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) methods on [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions) to configure URL prefixes and ports for Kestrel.

`UseUrls`, the `--urls` command-line argument, `urls` host configuration key, and the `ASPNETCORE_URLS` environment variable also work but have the limitations noted later in this section (a default certificate must be available for HTTPS endpoint configuration).

`KestrelServerOptions` configuration:

## ConfigureEndpointDefaults

[ConfigureEndpointDefaults(Action\<ListenOptions>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configureendpointdefaults#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configureendpointdefaults\(system-action\(\(microsoft-aspnetcore-server-kestrel-core-listenoptions\)\)\)) specifies a configuration `Action` to run for each specified endpoint. Calling `ConfigureEndpointDefaults` multiple times replaces prior `Action`s with the last `Action` specified:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // ...
    });
});
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureEndpointDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configureendpointdefaults) won't have the defaults applied.

## Configure(IConfiguration)

Enables Kestrel to load endpoints from an [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration). The configuration must be scoped to the configuration section for Kestrel. The `Configure(IConfiguration, bool)` overload can be used to enable reloading endpoints when the configuration source changes.

By default, Kestrel configuration is loaded from the `Kestrel` section and reloading changes is enabled:

```
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "Https": {
        "Url": "https://localhost:5001"
      }
    }
  }
}
```

If reloading configuration is enabled and a change is signaled then the following steps are taken:

* The new configuration is compared to the old one, any endpoint without configuration changes are not modified.
* Removed or modified endpoints are given 5 seconds to complete processing requests and shut down.
* New or modified endpoints are started.

Clients connecting to a modified endpoint may be disconnected or refused while the endpoint is restarted.

## ConfigureHttpsDefaults

[ConfigureHttpsDefaults(Action\<HttpsConnectionAdapterOptions>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configurehttpsdefaults\(system-action\(\(microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions\)\)\)) specifies a configuration `Action` to run for each HTTPS endpoint. Calling `ConfigureHttpsDefaults` multiple times replaces prior `Action`s with the last `Action` specified.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        // ...
    });
});
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureHttpsDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults) won't have the defaults applied.

## ListenOptions.UseHttps

### No configuration

Kestrel listens on `http://localhost:5000` and `https://localhost:5001` (if a default cert is available).

[]()

### Replace the default certificate from configuration

A default HTTPS app settings configuration schema is available for Kestrel. Configure multiple endpoints, including the URLs and the certificates to use, either from a file on disk or from a certificate store.

In the following `appsettings.json` example:

* Set `AllowInvalid` to `true` to permit the use of invalid certificates (for example, self-signed certificates).
* Any HTTPS endpoint that doesn't specify a certificate (`HttpsDefaultCert` in the example that follows) falls back to the cert defined under `Certificates:Default` or the development certificate.

```
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertAndKeyFile": {
        "Url": "https://localhost:5002",
        "Certificate": {
          "Path": "<path to .pem/.crt file>",
          "KeyPath": "<path to .key file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertStore": {
        "Url": "https://localhost:5003",
        "Certificate": {
          "Subject": "<subject; required>",
          "Store": "<certificate store; required>",
          "Location": "<location; defaults to CurrentUser>",
          "AllowInvalid": "<true or false; defaults to false>"
        }
      },
      "HttpsDefaultCert": {
        "Url": "https://localhost:5004"
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}
```

Warning

In the preceding example, certificate passwords are stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for each certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

Schema notes:

* Endpoints names are [case-insensitive](../../configuration/?view=aspnetcore-10.0#configuration-keys-and-values). For example, `HTTPS` and `Https` are equivalent.
* The `Url` parameter is required for each endpoint. The format for this parameter is the same as the top-level `Urls` configuration parameter except that it's limited to a single value.
* These endpoints replace those defined in the top-level `Urls` configuration rather than adding to them. Endpoints defined in code via `Listen` are cumulative with the endpoints defined in the configuration section.
* The `Certificate` section is optional. If the `Certificate` section isn't specified, the defaults defined in `Certificates:Default` are used. If no defaults are available, the development certificate is used. If there are no defaults and the development certificate isn't present, the server throws an exception and fails to start.
* The `Certificate` section supports multiple [certificate sources](#certificate-sources).
* Any number of endpoints may be defined in [Configuration](../../configuration/?view=aspnetcore-10.0) as long as they don't cause port conflicts.

#### Certificate sources

Certificate nodes can be configured to load certificates from a number of sources:

* `Path` and `Password` to load *.pfx* files.
* `Path`, `KeyPath` and `Password` to load *.pem*/*.crt* and *.key* files.
* `Subject` and `Store` to load from the certificate store.

For example, the `Certificates:Default` certificate can be specified as:

```
"Default": {
  "Subject": "<subject; required>",
  "Store": "<cert store; required>",
  "Location": "<location; defaults to CurrentUser>",
  "AllowInvalid": "<true or false; defaults to false>"
}
```

#### ConfigurationLoader

[Configure(IConfiguration)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configure#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configure\(microsoft-extensions-configuration-iconfiguration\)) returns a [KestrelConfigurationLoader](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelconfigurationloader) with an [Endpoint(String, Action\<EndpointConfiguration>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelconfigurationloader.endpoint#microsoft-aspnetcore-server-kestrel-kestrelconfigurationloader-endpoint\(system-string-system-action\(\(microsoft-aspnetcore-server-kestrel-endpointconfiguration\)\)\)) method that can be used to supplement a configured endpoint's settings:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    var kestrelSection = context.Configuration.GetSection("Kestrel");

    serverOptions.Configure(kestrelSection)
        .Endpoint("HTTPS", listenOptions =>
        {
            // ...
        });
});
```

`KestrelServerOptions.ConfigurationLoader` can be directly accessed to continue iterating on the existing loader, such as the one provided by [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost).

* The configuration section for each endpoint is available on the options in the `Endpoint` method so that custom settings may be read.
* Multiple configurations may be loaded by calling [Configure(IConfiguration)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configure#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configure\(microsoft-extensions-configuration-iconfiguration\)) again with another section. Only the last configuration is used, unless `Load` is explicitly called on prior instances. The metapackage doesn't call `Load` so that its default configuration section may be replaced.
* `KestrelConfigurationLoader` mirrors the `Listen` family of APIs from `KestrelServerOptions` as `Endpoint` overloads, so code and config endpoints may be configured in the same place. These overloads don't use names and only consume default settings from configuration.

### Change the defaults in code

`ConfigureEndpointDefaults` and `ConfigureHttpsDefaults` can be used to change default settings for `ListenOptions` and `HttpsConnectionAdapterOptions`, including overriding the default certificate specified in the prior scenario. `ConfigureEndpointDefaults` and `ConfigureHttpsDefaults` should be called before any endpoints are configured.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // ...
    });

    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        // ...
    });
});
```

## Configure endpoints using Server Name Indication

[Server Name Indication (SNI)](https://tools.ietf.org/html/rfc6066#section-3) can be used to host multiple domains on the same IP address and port. For SNI to function, the client sends the host name for the secure session to the server during the TLS handshake so that the server can provide the correct certificate. The client uses the furnished certificate for encrypted communication with the server during the secure session that follows the TLS handshake.

SNI can be configured in two ways:

* Create an endpoint in code and select a certificate using the host name with the [ServerCertificateSelector](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.servercertificateselector) callback.
* Configure a mapping between host names and HTTPS options in [Configuration](../../configuration/?view=aspnetcore-10.0). For example, JSON in the `appsettings.json` file.

### SNI with `ServerCertificateSelector`

Kestrel supports SNI via the `ServerCertificateSelector` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var localhostCert = CertificateLoader.LoadFromStoreCert(
                "localhost", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var exampleCert = CertificateLoader.LoadFromStoreCert(
                "example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var subExampleCert = CertificateLoader.LoadFromStoreCert(
                "sub.example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var certs = new Dictionary<string, X509Certificate2>(
                StringComparer.OrdinalIgnoreCase)
            {
                ["localhost"] = localhostCert,
                ["example.com"] = exampleCert,
                ["sub.example.com"] = subExampleCert
            };

            httpsOptions.ServerCertificateSelector = (connectionContext, name) =>
            {
                if (name is not null && certs.TryGetValue(name, out var cert))
                {
                    return cert;
                }

                return exampleCert;
            };
        });
    });
});
```

### SNI with `ServerOptionsSelectionCallback`

Kestrel supports additional dynamic TLS configuration via the `ServerOptionsSelectionCallback` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate and TLS configuration. Default certificates and `ConfigureHttpsDefaults` are not used with this callback.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        var localhostCert = CertificateLoader.LoadFromStoreCert(
            "localhost", "My", StoreLocation.CurrentUser,
            allowInvalid: true);
        var exampleCert = CertificateLoader.LoadFromStoreCert(
            "example.com", "My", StoreLocation.CurrentUser,
            allowInvalid: true);

        listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
        {
            if (string.Equals(clientHelloInfo.ServerName, "localhost",
                StringComparison.OrdinalIgnoreCase))
            {
                return new ValueTask<SslServerAuthenticationOptions>(
                    new SslServerAuthenticationOptions
                    {
                        ServerCertificate = localhostCert,
                        // Different TLS requirements for this host
                        ClientCertificateRequired = true
                    });
            }

            return new ValueTask<SslServerAuthenticationOptions>(
                new SslServerAuthenticationOptions
                {
                    ServerCertificate = exampleCert
                });
        }, state: null!);
    });
});
```

### SNI with `TlsHandshakeCallbackOptions`

Kestrel supports additional dynamic TLS configuration via the `TlsHandshakeCallbackOptions.OnConnection` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate, TLS configuration, and other server options. Default certificates and `ConfigureHttpsDefaults` are not used with this callback.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        var localhostCert = CertificateLoader.LoadFromStoreCert(
            "localhost", "My", StoreLocation.CurrentUser,
            allowInvalid: true);
        var exampleCert = CertificateLoader.LoadFromStoreCert(
            "example.com", "My", StoreLocation.CurrentUser,
            allowInvalid: true);

        listenOptions.UseHttps(new TlsHandshakeCallbackOptions
        {
            OnConnection = context =>
            {
                if (string.Equals(context.ClientHelloInfo.ServerName, "localhost",
                    StringComparison.OrdinalIgnoreCase))
                {
                    // Different TLS requirements for this host
                    context.AllowDelayedClientCertificateNegotation = true;

                    return new ValueTask<SslServerAuthenticationOptions>(
                        new SslServerAuthenticationOptions
                        {
                            ServerCertificate = localhostCert
                        });
                }

                return new ValueTask<SslServerAuthenticationOptions>(
                    new SslServerAuthenticationOptions
                    {
                        ServerCertificate = exampleCert
                    });
            }
        });
    });
});
```

### SNI in configuration

Kestrel supports SNI defined in configuration. An endpoint can be configured with an `Sni` object that contains a mapping between host names and HTTPS options. The connection host name is matched to the options and they are used for that connection.

The following configuration adds an endpoint named `MySniEndpoint` that uses SNI to select HTTPS options based on the host name:

```
{
  "Kestrel": {
    "Endpoints": {
      "MySniEndpoint": {
        "Url": "https://*",
        "SslProtocols": ["Tls11", "Tls12"],
        "Sni": {
          "a.example.org": {
            "Protocols": "Http1AndHttp2",
            "SslProtocols": ["Tls11", "Tls12", "Tls13"],
            "Certificate": {
              "Subject": "<subject; required>",
              "Store": "<certificate store; required>",
            },
            "ClientCertificateMode" : "NoCertificate"
          },
          "*.example.org": {
            "Certificate": {
              "Path": "<path to .pfx file>",
              "Password": "$CREDENTIAL_PLACEHOLDER$"
            }
          },
          "*": {
            // At least one subproperty needs to exist per SNI section or it
            // cannot be discovered via IConfiguration
            "Protocols": "Http1",
          }
        }
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}
```

Warning

In the preceding example, certificate passwords are stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for each certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

HTTPS options that can be overridden by SNI:

* `Certificate` configures the [certificate source](#certificate-sources).
* `Protocols` configures the allowed [HTTP protocols](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.httpprotocols).
* `SslProtocols` configures the allowed [SSL protocols](/en-us/dotnet/api/system.security.authentication.sslprotocols).
* `ClientCertificateMode` configures the [client certificate requirements](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode).

The host name supports wildcard matching:

* Exact match. For example, `a.example.org` matches `a.example.org`.
* Wildcard prefix. If there are multiple wildcard matches then the longest pattern is chosen. For example, `*.example.org` matches `b.example.org` and `c.example.org`.
* Full wildcard. `*` matches everything else, including clients that aren't using SNI and don't send a host name.

The matched SNI configuration is applied to the endpoint for the connection, overriding values on the endpoint. If a connection doesn't match a configured SNI host name then the connection is refused.

### SNI requirements

All websites must run on the same Kestrel instance. Kestrel doesn't support sharing an IP address and port across multiple instances without a reverse proxy.

## SSL/TLS Protocols

SSL Protocols are protocols used for encrypting and decrypting traffic between two peers, traditionally a client and a server.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.SslProtocols = SslProtocols.Tls13;
    });
});
```

```
{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "SslProtocols": ["Tls12", "Tls13"],
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}
```

Warning

In the preceding example, the certificate password is stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

The default value, `SslProtocols.None`, causes Kestrel to use the operating system defaults to choose the best protocol. Unless you have a specific reason to select a protocol, use the default.

## Client Certificates

`ClientCertificateMode` configures the [client certificate requirements](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode).

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
    });
});
```

```
{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "ClientCertificateMode": "AllowCertificate",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}
```

Warning

In the preceding example, the certificate password is stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0).

The default value is `ClientCertificateMode.NoCertificate` where Kestrel will not request or require a certificate from the client.

For more information, see [Configure certificate authentication in ASP.NET Core](../../../security/authentication/certauth?view=aspnetcore-10.0).

## Connection logging

Call [UseConnectionLogging](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionsconnectionloggingextensions.useconnectionlogging) to emit Debug level logs for byte-level communication on a connection. Connection logging is helpful for troubleshooting problems in low-level communication, such as during TLS encryption and behind proxies. If `UseConnectionLogging` is placed before `UseHttps`, encrypted traffic is logged. If `UseConnectionLogging` is placed after `UseHttps`, decrypted traffic is logged. This is built-in [connection middleware](#connection-middleware).

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseConnectionLogging();
    });
});
```

## Bind to a TCP socket

The [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) method binds to a TCP socket, and an options lambda permits X.509 certificate configuration:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Loopback, 5000);
    serverOptions.Listen(IPAddress.Loopback, 5001, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
    });
});
```

The example configures HTTPS for an endpoint with [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions). Use the same API to configure other Kestrel settings for specific endpoints.

On Windows, self-signed certificates can be created by using the [New-SelfSignedCertificate PowerShell cmdlet](/en-us/powershell/module/pki/new-selfsignedcertificate). For an unsupported example, see the [UpdateIISExpressSSLForChrome.ps1](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/includes/make-x509-cert/UpdateIISExpressSSLForChrome.ps1) certificate file on GitHub.

On macOS, Linux, and Windows, create certificates can be created by using [OpenSSL](https://www.openssl.org/).

## Bind to a Unix socket

Listen on a Unix socket with [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) for improved performance with Nginx, as shown in this example:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ListenUnixSocket("/tmp/kestrel-test.sock");
});
```

* In the Nginx configuration file, set the `server` > `location` > `proxy_pass` entry to `http://unix:/tmp/{KESTREL SOCKET}:/;`. `{KESTREL SOCKET}` is the name of the socket provided to [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) (for example, `kestrel-test.sock` in the preceding example).
* Ensure that the socket is writeable by Nginx (for example, `chmod go+w /tmp/kestrel-test.sock`).

## Port 0

When the port number `0` is specified, Kestrel dynamically binds to an available port. The following example shows how to determine which port Kestrel bound at runtime:

```
app.Run(async (context) =>
{
    var serverAddressFeature = context.Features.Get<IServerAddressesFeature>();

    if (serverAddressFeature is not null)
    {
        var listenAddresses = string.Join(", ", serverAddressFeature.Addresses);

        // ...
    }
});
```

## Limitations

Configure endpoints with the following approaches:

* [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls)
* `--urls` command-line argument
* `urls` host configuration key
* `ASPNETCORE_URLS` environment variable

These methods are useful for making code work with servers other than Kestrel. However, be aware of the following limitations:

* HTTPS can't be used with these approaches unless a default certificate is provided in the HTTPS endpoint configuration (for example, using `KestrelServerOptions` configuration or a configuration file as shown earlier in this article).
* When both the `Listen` and `UseUrls` approaches are used simultaneously, the `Listen` endpoints override the `UseUrls` endpoints.

## IIS endpoint configuration

When using IIS, the URL bindings for IIS override bindings are set by either `Listen` or `UseUrls`. For more information, see [ASP.NET Core Module](../../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0).

## ListenOptions.Protocols

The `Protocols` property establishes the HTTP protocols (`HttpProtocols`) enabled on a connection endpoint or for the server. Assign a value to the `Protocols` property from the `HttpProtocols` enum.

| `HttpProtocols` enum value | Connection protocol permitted                                                                                                                                                                                                         |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Http1`                    | HTTP/1.1 only. Can be used with or without TLS.                                                                                                                                                                                       |
| `Http2`                    | HTTP/2 only. May be used without TLS only if the client supports a [Prior Knowledge mode](https://tools.ietf.org/html/rfc7540#section-3.4).                                                                                           |
| `Http1AndHttp2`            | HTTP/1.1 and HTTP/2. HTTP/2 requires the client to select HTTP/2 in the TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake; otherwise, the connection defaults to HTTP/1.1. |

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
    });
});
```

On Linux, [CipherSuitesPolicy](/en-us/dotnet/api/system.net.security.ciphersuitespolicy) can be used to filter TLS handshakes on a per-connection basis:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.OnAuthenticate = (context, sslOptions) =>
        {
            sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(
                new[]
                {
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
                    // ...
                });
        };
    });
});
```

## Connection middleware

Custom connection middleware can filter TLS handshakes on a per-connection basis for specific ciphers if necessary.

The following example throws [NotSupportedException](/en-us/dotnet/api/system.notsupportedexception) for any cipher algorithm that the app doesn't support. Alternatively, define and compare [ITlsHandshakeFeature.CipherAlgorithm](/en-us/dotnet/api/microsoft.aspnetcore.connections.features.itlshandshakefeature.cipheralgorithm) to a list of acceptable cipher suites.

No encryption is used with a [CipherAlgorithmType.Null](/en-us/dotnet/api/system.security.authentication.cipheralgorithmtype#system-security-authentication-cipheralgorithmtype-null) cipher algorithm.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel((context, serverOptions) =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");

        listenOptions.Use((context, next) =>
        {
            var tlsFeature = context.Features.Get<ITlsHandshakeFeature>()!;

            if (tlsFeature.CipherAlgorithm == CipherAlgorithmType.Null)
            {
                throw new NotSupportedException(
                    $"Prohibited cipher: {tlsFeature.CipherAlgorithm}");
            }

            return next();
        });
    });
});
```

## Set the HTTP protocol from configuration

By default, Kestrel configuration is loaded from the `Kestrel` section. The following `appsettings.json` example establishes HTTP/1.1 as the default connection protocol for all endpoints:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1"
    }
  }
}
```

The following `appsettings.json` example establishes the HTTP/1.1 connection protocol for a specific endpoint:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsDefaultCert": {
        "Url": "https://localhost:5001",
        "Protocols": "Http1"
      }
    }
  }
}
```

Protocols specified in code override values set by configuration.

## URL prefixes

When using `UseUrls`, `--urls` command-line argument, `urls` host configuration key, or `ASPNETCORE_URLS` environment variable, the URL prefixes can be in any of the following formats.

Only HTTP URL prefixes are valid. Kestrel doesn't support HTTPS when configuring URL bindings using `UseUrls`.

* IPv4 address with port number

  ```
  http://65.55.39.10:80/
  ```

  `0.0.0.0` is a special case that binds to all IPv4 addresses.

* IPv6 address with port number

  ```
  http://[0:0:0:0:0:ffff:4137:270a]:80/
  ```

  `[::]` is the IPv6 equivalent of IPv4 `0.0.0.0`.

* Host name with port number

  ```
  http://contoso.com:80/
  http://*:80/
  ```

  Host names, `*`, and `+`, aren't special. Anything not recognized as a valid IP address or `localhost` binds to all IPv4 and IPv6 IPs. To bind different host names to different ASP.NET Core apps on the same port, use [HTTP.sys](../httpsys?view=aspnetcore-10.0) or a reverse proxy server. Reverse proxy server examples include IIS, Nginx, or Apache.

  Warning

  Hosting in a reverse proxy configuration requires [host filtering](host-filtering?view=aspnetcore-10.0).

* Host `localhost` name with port number or loopback IP with port number

  ```
  http://localhost:5000/
  http://127.0.0.1:5000/
  http://[::1]:5000/
  ```

  When `localhost` is specified, Kestrel attempts to bind to both IPv4 and IPv6 loopback interfaces. If the requested port is in use by another service on either loopback interface, Kestrel fails to start. If either loopback interface is unavailable for any other reason (most commonly because IPv6 isn't supported), Kestrel logs a warning.

* [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls) extension method.

The value provided using these approaches can be one or more HTTP and HTTPS endpoints (HTTPS if a default cert is available). Configure the value as a semicolon-separated list (for example, `"Urls": "http://localhost:8000;http://localhost:8001"`).

For more information on these approaches, see [Server URLs](../../host/web-host?view=aspnetcore-10.0#server-urls) and [Override configuration](../../host/web-host?view=aspnetcore-10.0#override-configuration).

A development certificate is created:

* When the [.NET SDK](/en-us/dotnet/core/sdk) is installed.
* The [dev-certs tool](/en-us/dotnet/core/tools/dotnet-dev-certs) is used to create a certificate.

Some browsers require granting explicit permission to trust the local development certificate.

Project templates configure apps to run on HTTPS by default and include [HTTPS redirection and HSTS support](../../../security/enforcing-ssl?view=aspnetcore-10.0).

Call [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) or [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) methods on [KestrelServerOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions) to configure URL prefixes and ports for Kestrel.

`UseUrls`, the `--urls` command-line argument, `urls` host configuration key, and the `ASPNETCORE_URLS` environment variable also work but have the limitations noted later in this section (a default certificate must be available for HTTPS endpoint configuration).

`KestrelServerOptions` configuration:

## ConfigureEndpointDefaults

[ConfigureEndpointDefaults(Action\<ListenOptions>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configureendpointdefaults#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configureendpointdefaults\(system-action\(\(microsoft-aspnetcore-server-kestrel-core-listenoptions\)\)\)) specifies a configuration `Action` to run for each specified endpoint. Calling `ConfigureEndpointDefaults` multiple times replaces prior `Action`s with the last `Action` specified.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // Configure endpoint defaults
    });
});
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureEndpointDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configureendpointdefaults) won't have the defaults applied.

## Configure(IConfiguration)

Enables Kestrel to load endpoints from an [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration). The configuration must be scoped to the configuration section for Kestrel.

The `Configure(IConfiguration, bool)` overload can be used to enable reloading endpoints when the configuration source changes.

`IHostBuilder.ConfigureWebHostDefaults` calls `Configure(context.Configuration.GetSection("Kestrel"), reloadOnChange: true)` by default to load Kestrel configuration and enable reloading.

```
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "Https": {
        "Url": "https://localhost:5001"
      }
    }
  }
}
```

If reloading configuration is enabled and a change is signaled then the following steps are taken:

* The new configuration is compared to the old one, any endpoint without configuration changes are not modified.
* Removed or modified endpoints are given 5 seconds to complete processing requests and shut down.
* New or modified endpoints are started.

Clients connecting to a modified endpoint may be disconnected or refused while the endpoint is restarted.

## ConfigureHttpsDefaults

[ConfigureHttpsDefaults(Action\<HttpsConnectionAdapterOptions>)](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configurehttpsdefaults\(system-action\(\(microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions\)\)\)) specifies a configuration `Action` to run for each HTTPS endpoint. Calling `ConfigureHttpsDefaults` multiple times replaces prior `Action`s with the last `Action` specified.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        // certificate is an X509Certificate2
        listenOptions.ServerCertificate = certificate;
    });
});
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureHttpsDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults) won't have the defaults applied.

## ListenOptions.UseHttps

### No configuration

Kestrel listens on `http://localhost:5000` and `https://localhost:5001` (if a default cert is available).

[]()

### Replace the default certificate from configuration

A default HTTPS app settings configuration schema is available for Kestrel. Configure multiple endpoints, including the URLs and the certificates to use, either from a file on disk or from a certificate store.

In the following `appsettings.json` example:

* Set `AllowInvalid` to `true` to permit the use of invalid certificates (for example, self-signed certificates).
* Any HTTPS endpoint that doesn't specify a certificate (`HttpsDefaultCert` in the example that follows) falls back to the cert defined under `Certificates:Default` or the development certificate.

```
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertAndKeyFile": {
        "Url": "https://localhost:5002",
        "Certificate": {
          "Path": "<path to .pem/.crt file>",
          "KeyPath": "<path to .key file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      },
      "HttpsInlineCertStore": {
        "Url": "https://localhost:5003",
        "Certificate": {
          "Subject": "<subject; required>",
          "Store": "<certificate store; required>",
          "Location": "<location; defaults to CurrentUser>",
          "AllowInvalid": "<true or false; defaults to false>"
        }
      },
      "HttpsDefaultCert": {
        "Url": "https://localhost:5004"
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}
```

Warning

In the preceding example, certificate passwords are stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for each certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

Schema notes:

* Endpoints names are [case-insensitive](../../configuration/?view=aspnetcore-10.0#configuration-keys-and-values). For example, `HTTPS` and `Https` are equivalent.
* The `Url` parameter is required for each endpoint. The format for this parameter is the same as the top-level `Urls` configuration parameter except that it's limited to a single value.
* These endpoints replace those defined in the top-level `Urls` configuration rather than adding to them. Endpoints defined in code via `Listen` are cumulative with the endpoints defined in the configuration section.
* The `Certificate` section is optional. If the `Certificate` section isn't specified, the defaults defined in `Certificates:Default` are used. If no defaults are available, the development certificate is used. If there are no defaults and the development certificate isn't present, the server throws an exception and fails to start.
* The `Certificate` section supports multiple [certificate sources](#certificate-sources).
* Any number of endpoints may be defined in [Configuration](../../configuration/?view=aspnetcore-10.0) as long as they don't cause port conflicts.

#### Certificate sources

Certificate nodes can be configured to load certificates from a number of sources:

* `Path` and `Password` to load *.pfx* files.
* `Path`, `KeyPath` and `Password` to load *.pem*/*.crt* and *.key* files.
* `Subject` and `Store` to load from the certificate store.

For example, the `Certificates:Default` certificate can be specified as:

```
"Default": {
  "Subject": "<subject; required>",
  "Store": "<cert store; required>",
  "Location": "<location; defaults to CurrentUser>",
  "AllowInvalid": "<true or false; defaults to false>"
}
```

#### ConfigurationLoader

`options.Configure(context.Configuration.GetSection("{SECTION}"))` returns a [KestrelConfigurationLoader](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.kestrelconfigurationloader) with an `.Endpoint(string name, listenOptions => { })` method that can be used to supplement a configured endpoint's settings:

```
webBuilder.UseKestrel((context, serverOptions) =>
{
    serverOptions.Configure(context.Configuration.GetSection("Kestrel"))
        .Endpoint("HTTPS", listenOptions =>
        {
            listenOptions.HttpsOptions.SslProtocols = SslProtocols.Tls12;
        });
});
```

`KestrelServerOptions.ConfigurationLoader` can be directly accessed to continue iterating on the existing loader, such as the one provided by [CreateDefaultBuilder](/en-us/dotnet/api/microsoft.aspnetcore.webhost.createdefaultbuilder).

* The configuration section for each endpoint is available on the options in the `Endpoint` method so that custom settings may be read.
* Multiple configurations may be loaded by calling `options.Configure(context.Configuration.GetSection("{SECTION}"))` again with another section. Only the last configuration is used, unless `Load` is explicitly called on prior instances. The metapackage doesn't call `Load` so that its default configuration section may be replaced.
* `KestrelConfigurationLoader` mirrors the `Listen` family of APIs from `KestrelServerOptions` as `Endpoint` overloads, so code and config endpoints may be configured in the same place. These overloads don't use names and only consume default settings from configuration.

### Change the defaults in code

`ConfigureEndpointDefaults` and `ConfigureHttpsDefaults` can be used to change default settings for `ListenOptions` and `HttpsConnectionAdapterOptions`, including overriding the default certificate specified in the prior scenario. `ConfigureEndpointDefaults` and `ConfigureHttpsDefaults` should be called before any endpoints are configured.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        // Configure endpoint defaults
    });

    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.SslProtocols = SslProtocols.Tls12;
    });
});
```

## Configure endpoints using Server Name Indication

[Server Name Indication (SNI)](https://tools.ietf.org/html/rfc6066#section-3) can be used to host multiple domains on the same IP address and port. For SNI to function, the client sends the host name for the secure session to the server during the TLS handshake so that the server can provide the correct certificate. The client uses the furnished certificate for encrypted communication with the server during the secure session that follows the TLS handshake.

SNI can be configured in two ways:

* Create an endpoint in code and select a certificate using the host name with the [ServerCertificateSelector](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.servercertificateselector) callback.
* Configure a mapping between host names and HTTPS options in [Configuration](../../configuration/?view=aspnetcore-10.0). For example, JSON in the `appsettings.json` file.

### SNI with `ServerCertificateSelector`

Kestrel supports SNI via the `ServerCertificateSelector` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate. The following callback code can be used in the `ConfigureWebHostDefaults` method call of a project's `Program.cs` file:

```
// using System.Security.Cryptography.X509Certificates;
// using Microsoft.AspNetCore.Server.Kestrel.Https;

webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var localhostCert = CertificateLoader.LoadFromStoreCert(
                "localhost", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var exampleCert = CertificateLoader.LoadFromStoreCert(
                "example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var subExampleCert = CertificateLoader.LoadFromStoreCert(
                "sub.example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var certs = new Dictionary<string, X509Certificate2>(StringComparer.OrdinalIgnoreCase)
            {
                { "localhost", localhostCert },
                { "example.com", exampleCert },
                { "sub.example.com", subExampleCert },
            };            

            httpsOptions.ServerCertificateSelector = (connectionContext, name) =>
            {
                if (name != null && certs.TryGetValue(name, out var cert))
                {
                    return cert;
                }

                return exampleCert;
            };
        });
    });
});
```

### SNI with `ServerOptionsSelectionCallback`

Kestrel supports additional dynamic TLS configuration via the `ServerOptionsSelectionCallback` callback. The callback is invoked once per connection to allow the app to inspect the host name and select the appropriate certificate and TLS configuration. Default certificates and `ConfigureHttpsDefaults` are not used with this callback.

```
// using System.Security.Cryptography.X509Certificates;
// using Microsoft.AspNetCore.Server.Kestrel.Https;

webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenAnyIP(5005, listenOptions =>
    {
        listenOptions.UseHttps(httpsOptions =>
        {
            var localhostCert = CertificateLoader.LoadFromStoreCert(
                "localhost", "My", StoreLocation.CurrentUser,
                allowInvalid: true);
            var exampleCert = CertificateLoader.LoadFromStoreCert(
                "example.com", "My", StoreLocation.CurrentUser,
                allowInvalid: true);

            listenOptions.UseHttps((stream, clientHelloInfo, state, cancellationToken) =>
            {
                if (string.Equals(clientHelloInfo.ServerName, "localhost", StringComparison.OrdinalIgnoreCase))
                {
                    return new ValueTask<SslServerAuthenticationOptions>(new SslServerAuthenticationOptions
                    {
                        ServerCertificate = localhostCert,
                        // Different TLS requirements for this host
                        ClientCertificateRequired = true,
                    });
                }

                return new ValueTask<SslServerAuthenticationOptions>(new SslServerAuthenticationOptions
                {
                    ServerCertificate = exampleCert,
                });
            }, state: null);
        });
    });
});
```

### SNI in configuration

Kestrel supports SNI defined in configuration. An endpoint can be configured with an `Sni` object that contains a mapping between host names and HTTPS options. The connection host name is matched to the options and they are used for that connection.

The following configuration adds an endpoint named `MySniEndpoint` that uses SNI to select HTTPS options based on the host name:

```
{
  "Kestrel": {
    "Endpoints": {
      "MySniEndpoint": {
        "Url": "https://*",
        "SslProtocols": ["Tls11", "Tls12"],
        "Sni": {
          "a.example.org": {
            "Protocols": "Http1AndHttp2",
            "SslProtocols": ["Tls11", "Tls12", "Tls13"],
            "Certificate": {
              "Subject": "<subject; required>",
              "Store": "<certificate store; required>",
            },
            "ClientCertificateMode" : "NoCertificate"
          },
          "*.example.org": {
            "Certificate": {
              "Path": "<path to .pfx file>",
              "Password": "$CREDENTIAL_PLACEHOLDER$"
            }
          },
          "*": {
            // At least one subproperty needs to exist per SNI section or it
            // cannot be discovered via IConfiguration
            "Protocols": "Http1",
          }
        }
      }
    },
    "Certificates": {
      "Default": {
        "Path": "<path to .pfx file>",
        "Password": "$CREDENTIAL_PLACEHOLDER$"
      }
    }
  }
}
```

Warning

In the preceding example, certificate passwords are stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for each certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

HTTPS options that can be overridden by SNI:

* `Certificate` configures the [certificate source](#certificate-sources).
* `Protocols` configures the allowed [HTTP protocols](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.httpprotocols).
* `SslProtocols` configures the allowed [SSL protocols](/en-us/dotnet/api/system.security.authentication.sslprotocols).
* `ClientCertificateMode` configures the [client certificate requirements](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode).

The host name supports wildcard matching:

* Exact match. For example, `a.example.org` matches `a.example.org`.
* Wildcard prefix. If there are multiple wildcard matches then the longest pattern is chosen. For example, `*.example.org` matches `b.example.org` and `c.example.org`.
* Full wildcard. `*` matches everything else, including clients that aren't using SNI and don't send a host name.

The matched SNI configuration is applied to the endpoint for the connection, overriding values on the endpoint. If a connection doesn't match a configured SNI host name then the connection is refused.

### SNI requirements

* Running on target framework `netcoreapp2.1` or later. On `net461` or later, the callback is invoked but the `name` is always `null`. The `name` is also `null` if the client doesn't provide the host name parameter in the TLS handshake.
* All websites run on the same Kestrel instance. Kestrel doesn't support sharing an IP address and port across multiple instances without a reverse proxy.

## SSL/TLS Protocols

SSL Protocols are protocols used for encrypting and decrypting traffic between two peers, traditionally a client and a server.

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.SslProtocols = SslProtocols.Tls13;
    });
});
```

```
{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "SslProtocols": ["Tls12", "Tls13"],
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}
```

Warning

In the preceding example, the certificate password is stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

The default value, `SslProtocols.None`, causes Kestrel to use the operating system defaults to choose the best protocol. Unless you have a specific reason to select a protocol, use the default.

## Client Certificates

`ClientCertificateMode` configures the [client certificate requirements](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
    });
});
```

```
{
  "Kestrel": {
    "Endpoints": {
      "MyHttpsEndpoint": {
        "Url": "https://localhost:5001",
        "ClientCertificateMode": "AllowCertificate",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "$CREDENTIAL_PLACEHOLDER$"
        }
      }
    }
  }
}
```

Warning

In the preceding example, the certificate password is stored in plain-text in `appsettings.json`. The `$CREDENTIAL_PLACEHOLDER$` token is used as a placeholder for the certificate's password. To store certificate passwords securely in development environments, see [Protect secrets in development](../../../security/app-secrets?view=aspnetcore-10.0). To store certificate passwords securely in production environments, see [Azure Key Vault configuration provider](../../../security/key-vault-configuration?view=aspnetcore-10.0). Development secrets shouldn't be used for production or test.

The default value is `ClientCertificateMode.NoCertificate` where Kestrel will not request or require a certificate from the client.

For more information, see [Configure certificate authentication in ASP.NET Core](../../../security/authentication/certauth?view=aspnetcore-10.0).

## Connection logging

Call [UseConnectionLogging](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionsconnectionloggingextensions.useconnectionlogging) to emit Debug level logs for byte-level communication on a connection. Connection logging is helpful for troubleshooting problems in low-level communication, such as during TLS encryption and behind proxies. If `UseConnectionLogging` is placed before `UseHttps`, encrypted traffic is logged. If `UseConnectionLogging` is placed after `UseHttps`, decrypted traffic is logged. This is built-in [connection middleware](#connection-middleware).

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseConnectionLogging();
    });
});
```

## Bind to a TCP socket

The [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) method binds to a TCP socket, and an options lambda permits X.509 certificate configuration:

```
public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(serverOptions =>
            {
                serverOptions.Listen(IPAddress.Loopback, 5000);
                serverOptions.Listen(IPAddress.Loopback, 5001, 
                    listenOptions =>
                    {
                        listenOptions.UseHttps("testCert.pfx", 
                            "testPassword");
                    });
            })
            .UseStartup<Startup>();
        });
```

The example configures HTTPS for an endpoint with [ListenOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.listenoptions). Use the same API to configure other Kestrel settings for specific endpoints.

On Windows, self-signed certificates can be created by using the [New-SelfSignedCertificate PowerShell cmdlet](/en-us/powershell/module/pki/new-selfsignedcertificate). For an unsupported example, see the [UpdateIISExpressSSLForChrome.ps1](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/includes/make-x509-cert/UpdateIISExpressSSLForChrome.ps1) certificate file on GitHub.

On macOS, Linux, and Windows, create certificates can be created by using [OpenSSL](https://www.openssl.org/).

## Bind to a Unix socket

Listen on a Unix socket with [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) for improved performance with Nginx, as shown in this example:

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenUnixSocket("/tmp/kestrel-test.sock");
    serverOptions.ListenUnixSocket("/tmp/kestrel-test.sock", 
        listenOptions =>
        {
            listenOptions.UseHttps("testCert.pfx", 
                "testpassword");
        });
})
```

* In the Nginx configuration file, set the `server` > `location` > `proxy_pass` entry to `http://unix:/tmp/{KESTREL SOCKET}:/;`. `{KESTREL SOCKET}` is the name of the socket provided to [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) (for example, `kestrel-test.sock` in the preceding example).
* Ensure that the socket is writeable by Nginx (for example, `chmod go+w /tmp/kestrel-test.sock`).

## Port 0

When the port number `0` is specified, Kestrel dynamically binds to an available port. The following example shows how to determine which port Kestrel bound at runtime:

```
public void Configure(IApplicationBuilder app)
{
    var serverAddressesFeature =
        app.ServerFeatures.Get<IServerAddressesFeature>();

    app.UseStaticFiles();

    app.Run(async (context) =>
    {
        context.Response.ContentType = "text/html";
        await context.Response
            .WriteAsync("<!DOCTYPE html><html lang=\"en\"><head>" +
                "<title></title></head><body><p>Hosted by Kestrel</p>");

        if (serverAddressesFeature != null)
        {
            await context.Response
                .WriteAsync("<p>Listening on the following addresses: " +
                    string.Join(", ", serverAddressesFeature.Addresses) +
                    "</p>");
        }

        await context.Response.WriteAsync("<p>Request URL: " +
            $"{context.Request.GetDisplayUrl()}<p>");
    });
}
```

When the app is run, the console window output indicates the dynamic port where the app can be reached:

```
Listening on the following addresses: http://127.0.0.1:48508
```

## Limitations

Configure endpoints with the following approaches:

* [UseUrls](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.useurls)
* `--urls` command-line argument
* `urls` host configuration key
* `ASPNETCORE_URLS` environment variable

These methods are useful for making code work with servers other than Kestrel. However, be aware of the following limitations:

* HTTPS can't be used with these approaches unless a default certificate is provided in the HTTPS endpoint configuration (for example, using `KestrelServerOptions` configuration or a configuration file as shown earlier in this article).
* When both the `Listen` and `UseUrls` approaches are used simultaneously, the `Listen` endpoints override the `UseUrls` endpoints.

## IIS endpoint configuration

When using IIS, the URL bindings for IIS override bindings are set by either `Listen` or `UseUrls`. For more information, see [ASP.NET Core Module](../../../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0).

## ListenOptions.Protocols

The `Protocols` property establishes the HTTP protocols (`HttpProtocols`) enabled on a connection endpoint or for the server. Assign a value to the `Protocols` property from the `HttpProtocols` enum.

| `HttpProtocols` enum value | Connection protocol permitted                                                                                                                                                                                                         |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Http1`                    | HTTP/1.1 only. Can be used with or without TLS.                                                                                                                                                                                       |
| `Http2`                    | HTTP/2 only. May be used without TLS only if the client supports a [Prior Knowledge mode](https://tools.ietf.org/html/rfc7540#section-3.4).                                                                                           |
| `Http1AndHttp2`            | HTTP/1.1 and HTTP/2. HTTP/2 requires the client to select HTTP/2 in the TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake; otherwise, the connection defaults to HTTP/1.1. |

```
webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
    });
});
```

On Linux, [CipherSuitesPolicy](/en-us/dotnet/api/system.net.security.ciphersuitespolicy) can be used to filter TLS handshakes on a per-connection basis:

```
// using System.Net.Security;
// using Microsoft.AspNetCore.Hosting;
// using Microsoft.AspNetCore.Server.Kestrel.Core;
// using Microsoft.Extensions.DependencyInjection;
// using Microsoft.Extensions.Hosting;

webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(listenOptions =>
    {
        listenOptions.OnAuthenticate = (context, sslOptions) =>
        {
            sslOptions.CipherSuitesPolicy = new CipherSuitesPolicy(
                new[]
                {
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
                    TlsCipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
                    // ...
                });
        };
    });
});
```

## Connection middleware

Custom connection middleware can filter TLS handshakes on a per-connection basis for specific ciphers if necessary.

The following example throws [NotSupportedException](/en-us/dotnet/api/system.notsupportedexception) for any cipher algorithm that the app doesn't support. Alternatively, define and compare [ITlsHandshakeFeature.CipherAlgorithm](/en-us/dotnet/api/microsoft.aspnetcore.connections.features.itlshandshakefeature.cipheralgorithm#microsoft-aspnetcore-connections-features-itlshandshakefeature-cipheralgorithm) to a list of acceptable cipher suites.

No encryption is used with a [CipherAlgorithmType.Null](/en-us/dotnet/api/system.security.authentication.cipheralgorithmtype) cipher algorithm.

```
// using System.Net;
// using Microsoft.AspNetCore.Connections;

webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
        listenOptions.UseTlsFilter();
    });
});
```

```
using System;
using System.Security.Authentication;
using Microsoft.AspNetCore.Connections.Features;

namespace Microsoft.AspNetCore.Connections
{
    public static class TlsFilterConnectionMiddlewareExtensions
    {
        public static IConnectionBuilder UseTlsFilter(
            this IConnectionBuilder builder)
        {
            return builder.Use((connection, next) =>
            {
                var tlsFeature = connection.Features.Get<ITlsHandshakeFeature>();

                if (tlsFeature.CipherAlgorithm == CipherAlgorithmType.Null)
                {
                    throw new NotSupportedException("Prohibited cipher: " +
                        tlsFeature.CipherAlgorithm);
                }

                return next();
            });
        }
    }
}
```

Connection filtering can also be configured via an [IConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.connections.iconnectionbuilder) lambda:

```
// using System;
// using System.Net;
// using System.Security.Authentication;
// using Microsoft.AspNetCore.Connections;
// using Microsoft.AspNetCore.Connections.Features;

webBuilder.ConfigureKestrel(serverOptions =>
{
    serverOptions.Listen(IPAddress.Any, 8000, listenOptions =>
    {
        listenOptions.UseHttps("testCert.pfx", "testPassword");
        listenOptions.Use((context, next) =>
        {
            var tlsFeature = context.Features.Get<ITlsHandshakeFeature>();

            if (tlsFeature.CipherAlgorithm == CipherAlgorithmType.Null)
            {
                throw new NotSupportedException(
                    $"Prohibited cipher: {tlsFeature.CipherAlgorithm}");
            }

            return next();
        });
    });
});
```

## Set the HTTP protocol from configuration

`CreateDefaultBuilder` calls `serverOptions.Configure(context.Configuration.GetSection("Kestrel"))` by default to load Kestrel configuration.

The following `appsettings.json` example establishes HTTP/1.1 as the default connection protocol for all endpoints:

```
{
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1"
    }
  }
}
```

The following `appsettings.json` example establishes the HTTP/1.1 connection protocol for a specific endpoint:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsDefaultCert": {
        "Url": "https://localhost:5001",
        "Protocols": "Http1"
      }
    }
  }
}
```

Protocols specified in code override values set by configuration.

## URL prefixes

When using `UseUrls`, `--urls` command-line argument, `urls` host configuration key, or `ASPNETCORE_URLS` environment variable, the URL prefixes can be in any of the following formats.

Only HTTP URL prefixes are valid. Kestrel doesn't support HTTPS when configuring URL bindings using `UseUrls`.

* IPv4 address with port number

  ```
  http://65.55.39.10:80/
  ```

  `0.0.0.0` is a special case that binds to all IPv4 addresses.

* IPv6 address with port number

  ```
  http://[0:0:0:0:0:ffff:4137:270a]:80/
  ```

  `[::]` is the IPv6 equivalent of IPv4 `0.0.0.0`.

* Host name with port number

  ```
  http://contoso.com:80/
  http://*:80/
  ```

  Host names, `*`, and `+`, aren't special. Anything not recognized as a valid IP address or `localhost` binds to all IPv4 and IPv6 IPs. To bind different host names to different ASP.NET Core apps on the same port, use [HTTP.sys](../httpsys?view=aspnetcore-10.0) or a reverse proxy server. Reverse proxy server examples include IIS, Nginx, or Apache.

  Warning

  Hosting in a reverse proxy configuration requires [host filtering](host-filtering?view=aspnetcore-10.0).

* Host `localhost` name with port number or loopback IP with port number

  ```
  http://localhost:5000/
  http://127.0.0.1:5000/
  http://[::1]:5000/
  ```

  When `localhost` is specified, Kestrel attempts to bind to both IPv4 and IPv6 loopback interfaces. If the requested port is in use by another service on either loopback interface, Kestrel fails to start. If either loopback interface is unavailable for any other reason (most commonly because IPv6 isn't supported), Kestrel logs a warning.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/caching/overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Overview of caching in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Tom Dykstra](https://twitter.com/tdykstra)

This article provides an overview of caching in ASP.NET Core with an introduction to in-memory, distributed, hybrid, response, and output caching.

## In-memory caching

In-memory caching uses server memory to store cached data. This type of caching is suitable for a single server or multiple servers that use session affinity. Session affinity is also known as *sticky sessions*. Session affinity means that the requests from a client are always routed to the same server for processing.

For more information, see [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0) and [Troubleshoot Azure Application Gateway session affinity issues](/en-us/azure/application-gateway/how-to-troubleshoot-application-gateway-session-affinity-issues).

## Distributed cache

Use a distributed cache to store data when the app is hosted in a cloud or server farm. The cache is shared across the servers that process requests. A client can submit a request that gets handled by any server in the group when cached data for the client is available. ASP.NET Core works with SQL Server, [Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis), [Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres), and [NCache](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/) distributed caches.

For more information, see [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0).

## HybridCache

The [HybridCache](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache) API bridges gaps in the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) and [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) APIs. `HybridCache` is an abstract class with a default implementation that handles most aspects of saving to cache and retrieving from cache.

### Features of HybridCache

`HybridCache` provides the following features that aren't available with other APIs:

* A unified API for both in-process and out-of-process caching.

  `HybridCache` is designed to be a drop-in replacement for existing `IDistributedCache` and `IMemoryCache` usage, and it provides a simple API for adding new caching code. If the app has an `IDistributedCache` implementation, the `HybridCache` service uses it for secondary caching. This two-level caching strategy allows `HybridCache` to provide the speed of an in-memory cache and the durability of a distributed or persistent cache.

* Stampede protection.

  *Cache stampede* happens when a frequently used cache entry is revoked, and too many requests try to repopulate the same cache entry at the same time. `HybridCache` combines concurrent operations that use the same `HybridCache` instance, which ensures that all requests for a given response wait for the first request to populate the cache. This coordination doesn't extend to other `HybridCache` instances.

* Configurable serialization.

  Serialization is configured as part of registering the service, with support for type-specific and generalized serializers via the `WithSerializer` and `WithSerializerFactory` methods, chained from the `AddHybridCache` call. By default, the service handles `string` and `byte[]` types internally, and uses the `System.Text.Json` namespace for everything else. `HybridCache` can be configured for other types of serializers, such as protobuf or XML.

To see the relative simplicity of the `HybridCache` API, compare code that uses it to code that uses `IDistributedCache`. Here's an example of what using `IDistributedCache` looks like:

```
public class SomeService(IDistributedCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
        var bytes = await cache.GetAsync(key, token); // Try to get from cache.
        SomeInformation info;
        if (bytes is null)
        {
            // Cache miss; get the data from the real source.
            info = await SomeExpensiveOperationAsync(name, id, token);

            // Serialize and cache it.
            bytes = SomeSerializer.Serialize(info);
            await cache.SetAsync(key, bytes, token);
        }
        else
        {
            // Cache hit; deserialize it.
            info = SomeSerializer.Deserialize<SomeInformation>(bytes);
        }
        return info;
    }

    // This is the work we're trying to cache.
    private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
        CancellationToken token = default)
    { /* ... */ }
}
```

The code demonstrates a significant amount of work to get right each time, including things like serialization. Also in the "cache miss" scenario, you might end up with multiple concurrent threads. These threads might all receive a cache miss, all fetch the underlying data, all serialize it, and all send the data to the cache.

Here's equivalent code that uses the `HybridCache` API:

```
public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // Unique key for this entry.
            async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
            token: token
        );
    }
}
```

The code is simpler, and the library provides stampede protection and other features not available with the `IDistributedCache` interface.

### Compatibility

The `HybridCache` library supports older .NET runtimes, including .NET Framework 4.7.2 and .NET Standard 2.0.

### More information

For more information, see the following resources:

* [HybridCache library in ASP.NET Core](hybrid?view=aspnetcore-10.0)
* [Hybrid Cache API proposal (GitHub dotnet/aspnetcore issue #54647)](https://github.com/dotnet/aspnetcore/issues/54647)
* [HybridCache source code](https://source.dot.net/#Microsoft.Extensions.Caching.Abstractions/Hybrid/HybridCache.cs,8c0fe94693d1ac8d)

## Response caching

For more information, see [Response caching in ASP.NET Core](response?view=aspnetcore-10.0).

## Output caching

The output caching middleware enables caching of HTTP responses. Output caching differs from [response caching](#response-caching) in the following ways:

* Caching behavior is configurable on the server.

  Response caching behavior is defined with HTTP headers. For example, when you browse a website with Chrome or Microsoft Edge, the browser automatically sends a `Cache-control: max-age=0` header. This header effectively disables response caching because the server follows the directions provided by the client. A new response is returned for every request, even if the server has a fresh cached response. With output caching, the client doesn't override the caching behavior that you configure on the server.

* The cache storage medium is extensible.

  Memory is used by default. Response caching is limited to memory.

* You can programmatically invalidate selected cache entries.

  Response caching is dependent on HTTP headers, which leaves you with few options for invalidating cache entries.

* Resource locking mitigates the risk of cache stampede and thundering herd.

  *Cache stampede* happens when a frequently used cache entry is revoked, and too many requests try to repopulate the same cache entry at the same time. *Thundering herd* is similar: a burst of requests for the same response that isn't already in a cache entry. Resource locking ensures that all requests for a given response wait for the first request to populate the cache. Response caching doesn't have a resource locking feature.

* Cache revalidation minimizes bandwidth usage.

  *Cache revalidation* means the server can return a *304 Not Modified* HTTP status code instead of a cached response body. This status code informs the client that the response to the request is unchanged from what was previously received. Response caching doesn't do cache revalidation.

For more information, see [Output caching middleware in ASP.NET Core](output?view=aspnetcore-10.0).

## Cache Tag Helper

Cache the content from an MVC view or Razor Page with the Cache Tag Helper. The Cache Tag Helper uses in-memory caching to store data.

For more information, see [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0).

## Distributed Cache Tag Helper

Cache the content from an MVC view or Razor Page in distributed cloud or web farm scenarios with the Distributed Cache Tag Helper. The Distributed Cache Tag Helper uses SQL Server, [Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis), or [NCache](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/) to store data.

For more information, see [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0).

## In-memory caching

In-memory caching uses server memory to store cached data. This type of caching is suitable for a single server or multiple servers using session affinity. Session affinity is also known as *sticky sessions*. Session affinity means that the requests from a client are always routed to the same server for processing.

For more information, see [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0) and [Troubleshoot Azure Application Gateway session affinity issues](/en-us/azure/application-gateway/how-to-troubleshoot-application-gateway-session-affinity-issues).

## Distributed Cache

Use a distributed cache to store data when the app is hosted in a cloud or server farm. The cache is shared across the servers that process requests. A client can submit a request that's handled by any server in the group if cached data for the client is available. ASP.NET Core works with SQL Server, [Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis), [Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres), and [NCache](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/) distributed caches.

For more information, see [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0).

## HybridCache

The [HybridCache](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache) API bridges gaps in the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) and [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) APIs. `HybridCache` is an abstract class with a default implementation that handles most aspects of saving to cache and retrieving from cache.

### Features of HybridCache

`HybridCache` provides the following features that aren't available with other APIs:

* A unified API for both in-process and out-of-process caching.

  `HybridCache` is designed to be a drop-in replacement for existing `IDistributedCache` and `IMemoryCache` usage, and it provides a simple API for adding new caching code. If the app has an `IDistributedCache` implementation, the `HybridCache` service uses it for secondary caching. This two-level caching strategy allows `HybridCache` to provide the speed of an in-memory cache and the durability of a distributed or persistent cache.

* Stampede protection.

  *Cache stampede* happens when a frequently used cache entry is revoked, and too many requests try to repopulate the same cache entry at the same time. `HybridCache` combines concurrent operations that use the same `HybridCache` instance, which ensures that all requests for a given response wait for the first request to populate the cache. This coordination doesn't extend to other `HybridCache` instances.

* Configurable serialization.

  Serialization is configured as part of registering the service, with support for type-specific and generalized serializers via the `WithSerializer` and `WithSerializerFactory` methods, chained from the `AddHybridCache` call. By default, the service handles `string` and `byte[]` types internally, and uses the `System.Text.Json` namespace for everything else. `HybridCache` can be configured for other types of serializers, such as protobuf or XML.

To see the relative simplicity of the `HybridCache` API, compare code that uses it to code that uses `IDistributedCache`. Here's an example of what using `IDistributedCache` looks like:

```
public class SomeService(IDistributedCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
        var bytes = await cache.GetAsync(key, token); // Try to get from cache.
        SomeInformation info;
        if (bytes is null)
        {
            // Cache miss; get the data from the real source.
            info = await SomeExpensiveOperationAsync(name, id, token);

            // Serialize and cache it.
            bytes = SomeSerializer.Serialize(info);
            await cache.SetAsync(key, bytes, token);
        }
        else
        {
            // Cache hit; deserialize it.
            info = SomeSerializer.Deserialize<SomeInformation>(bytes);
        }
        return info;
    }

    // This is the work we're trying to cache.
    private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
        CancellationToken token = default)
    { /* ... */ }
}
```

The code demonstrates a significant amount of work to get right each time, including things like serialization. Also in the "cache miss" scenario, you might end up with multiple concurrent threads. These threads might all receive a cache miss, all fetch the underlying data, all serialize it, and all send the data to the cache.

Here's equivalent code that uses the `HybridCache` API:

```
public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // Unique key for this entry.
            async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
            token: token
        );
    }
}
```

The code is simpler, and the library provides stampede protection and other features not available with the `IDistributedCache` interface.

### Compatibility

The `HybridCache` library supports older .NET runtimes, including .NET Framework 4.7.2 and .NET Standard 2.0.

### More information

For more information, see the following resources:

* [HybridCache library in ASP.NET Core](hybrid?view=aspnetcore-10.0)
* [Hybrid Cache API proposal (GitHub dotnet/aspnetcore issue #54647)](https://github.com/dotnet/aspnetcore/issues/54647)
* [HybridCache source code](https://source.dot.net/#Microsoft.Extensions.Caching.Abstractions/Hybrid/HybridCache.cs,8c0fe94693d1ac8d)

## Cache Tag Helper

Cache the content from an MVC view or Razor Page with the Cache Tag Helper. The Cache Tag Helper uses in-memory caching to store data.

For more information, see [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0).

## Distributed Cache Tag Helper

Cache the content from an MVC view or Razor Page in distributed cloud or web farm scenarios with the Distributed Cache Tag Helper. The Distributed Cache Tag Helper uses SQL Server, [Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis), or [NCache](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/) to store data.

For more information, see [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0).

## Response caching

[]()

## Output caching

The output caching middleware enables caching of HTTP responses. Output caching differs from [response caching](#response-caching) in the following ways:

* The caching behavior is configurable on the server.

  Response caching behavior is defined by HTTP headers. For example, when you visit a website with Chrome or Edge, the browser automatically sends a `Cache-control: max-age=0` header. This header effectively disables response caching, since the server follows the directions provided by the client. A new response is returned for every request, even if the server has a fresh cached response. With output caching the client doesn't override the caching behavior that you configure on the server.

* The cache storage medium is extensible.

  Memory is used by default. Response caching is limited to memory.

* You can programmatically invalidate selected cache entries.

  Response caching's dependence on HTTP headers leaves you with few options for invalidating cache entries.

* Resource locking mitigates the risk of cache stampede and thundering herd.

  *Cache stampede* happens when a frequently used cache entry is revoked, and too many requests try to repopulate the same cache entry at the same time. *Thundering herd* is similar: a burst of requests for the same response that isn't already in a cache entry. Resource locking ensures that all requests for a given response wait for the first request to populate the cache. Response caching doesn't have a resource locking feature.

* Cache revalidation minimizes bandwidth usage.

  *Cache revalidation* means the server can return a `304 Not Modified` HTTP status code instead of a cached response body. This status code informs the client that the response to the request is unchanged from what was previously received. Response caching doesn't do cache revalidation.

## In-memory caching

In-memory caching uses server memory to store cached data. This type of caching is suitable for a single server or multiple servers using session affinity. Session affinity is also known as *sticky sessions*. Session affinity means that the requests from a client are always routed to the same server for processing.

For more information, see [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0) and [Troubleshoot Azure Application Gateway session affinity issues](/en-us/azure/application-gateway/how-to-troubleshoot-application-gateway-session-affinity-issues).

## Distributed Cache

Use a distributed cache to store data when the app is hosted in a cloud or server farm. The cache is shared across the servers that process requests. A client can submit a request that's handled by any server in the group if cached data for the client is available. ASP.NET Core works with SQL Server, [Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis), [Postgres](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Postgres), and [NCache](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/) distributed caches.

For more information, see [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0).

## HybridCache

The [HybridCache](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache) API bridges gaps in the [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) and [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) APIs. `HybridCache` is an abstract class with a default implementation that handles most aspects of saving to cache and retrieving from cache.

### Features of HybridCache

`HybridCache` provides the following features that aren't available with other APIs:

* A unified API for both in-process and out-of-process caching.

  `HybridCache` is designed to be a drop-in replacement for existing `IDistributedCache` and `IMemoryCache` usage, and it provides a simple API for adding new caching code. If the app has an `IDistributedCache` implementation, the `HybridCache` service uses it for secondary caching. This two-level caching strategy allows `HybridCache` to provide the speed of an in-memory cache and the durability of a distributed or persistent cache.

* Stampede protection.

  *Cache stampede* happens when a frequently used cache entry is revoked, and too many requests try to repopulate the same cache entry at the same time. `HybridCache` combines concurrent operations that use the same `HybridCache` instance, which ensures that all requests for a given response wait for the first request to populate the cache. This coordination doesn't extend to other `HybridCache` instances.

* Configurable serialization.

  Serialization is configured as part of registering the service, with support for type-specific and generalized serializers via the `WithSerializer` and `WithSerializerFactory` methods, chained from the `AddHybridCache` call. By default, the service handles `string` and `byte[]` types internally, and uses the `System.Text.Json` namespace for everything else. `HybridCache` can be configured for other types of serializers, such as protobuf or XML.

To see the relative simplicity of the `HybridCache` API, compare code that uses it to code that uses `IDistributedCache`. Here's an example of what using `IDistributedCache` looks like:

```
public class SomeService(IDistributedCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
        var bytes = await cache.GetAsync(key, token); // Try to get from cache.
        SomeInformation info;
        if (bytes is null)
        {
            // Cache miss; get the data from the real source.
            info = await SomeExpensiveOperationAsync(name, id, token);

            // Serialize and cache it.
            bytes = SomeSerializer.Serialize(info);
            await cache.SetAsync(key, bytes, token);
        }
        else
        {
            // Cache hit; deserialize it.
            info = SomeSerializer.Deserialize<SomeInformation>(bytes);
        }
        return info;
    }

    // This is the work we're trying to cache.
    private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
        CancellationToken token = default)
    { /* ... */ }
}
```

The code demonstrates a significant amount of work to get right each time, including things like serialization. Also in the "cache miss" scenario, you might end up with multiple concurrent threads. These threads might all receive a cache miss, all fetch the underlying data, all serialize it, and all send the data to the cache.

Here's equivalent code that uses the `HybridCache` API:

```
public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // Unique key for this entry.
            async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
            token: token
        );
    }
}
```

The code is simpler, and the library provides stampede protection and other features not available with the `IDistributedCache` interface.

### Compatibility

The `HybridCache` library supports older .NET runtimes, including .NET Framework 4.7.2 and .NET Standard 2.0.

### More information

For more information, see the following resources:

* [HybridCache library in ASP.NET Core](hybrid?view=aspnetcore-10.0)
* [Hybrid Cache API proposal (GitHub dotnet/aspnetcore issue #54647)](https://github.com/dotnet/aspnetcore/issues/54647)
* [HybridCache source code](https://source.dot.net/#Microsoft.Extensions.Caching.Abstractions/Hybrid/HybridCache.cs,8c0fe94693d1ac8d)

## Cache Tag Helper

Cache the content from an MVC view or Razor Page with the Cache Tag Helper. The Cache Tag Helper uses in-memory caching to store data.

For more information, see [Cache Tag Helper in ASP.NET Core MVC](../../mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-10.0).

## Distributed Cache Tag Helper

Cache the content from an MVC view or Razor Page in distributed cloud or web farm scenarios with the Distributed Cache Tag Helper. The Distributed Cache Tag Helper uses SQL Server, [Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis), or [NCache](https://www.nuget.org/packages/Alachisoft.NCache.OpenSource.SDK/) to store data.

For more information, see [Distributed Cache Tag Helper in ASP.NET Core](../../mvc/views/tag-helpers/built-in/distributed-cache-tag-helper?view=aspnetcore-10.0).

## Response caching

## Output caching

[Output caching](overview?view=aspnetcore-7.0#oc7) is available in .NET 7 or later.

## Related content

* [Cache in-memory in ASP.NET Core](memory?view=aspnetcore-10.0)
* [Distributed caching in ASP.NET Core](distributed?view=aspnetcore-10.0)
* [HybridCache library in ASP.NET Core](hybrid?view=aspnetcore-10.0)
* [Response caching middleware in ASP.NET Core](middleware?view=aspnetcore-10.0)
* [Output caching middleware in ASP.NET Core](output?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-06

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/lifecycle.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Razor component lifecycle

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains the ASP.NET Core Razor component lifecycle and how to use lifecycle events.

## Lifecycle events

The Razor component processes Razor component lifecycle events in a set of synchronous and asynchronous lifecycle methods. The lifecycle methods can be overridden to perform additional operations in components during component initialization and rendering.

This article simplifies component lifecycle event processing in order to clarify complex framework logic and doesn't cover every change that was made over the years. You may need to access the [`ComponentBase` reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ComponentBase.cs) to integrate custom event processing with Blazor's lifecycle event processing. Code comments in the reference source include additional remarks on lifecycle event processing that don't appear in this article or in the [API documentation](/en-us/dotnet/api/).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

The following simplified diagrams illustrate Razor component lifecycle event processing. The C# methods associated with the lifecycle events are defined with examples in the following sections of this article.

Component lifecycle events:

1. If the component is rendering for the first time on a request:

   * Create the component's instance.
   * Perform property injection.
   * Call [`OnInitialized{Async}`](#component-initialization-oninitializedasync). If an incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task) is returned, the [Task](/en-us/dotnet/api/system.threading.tasks.task) is awaited and then the component is rerendered. The synchronous method is called prior to the asynchronous method.

2. Call [`OnParametersSet{Async}`](#after-parameters-are-set-onparameterssetasync). If an incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task) is returned, the [Task](/en-us/dotnet/api/system.threading.tasks.task) is awaited and then the component is rerendered. The synchronous method is called prior to the asynchronous method.

3. Render for all synchronous work and complete [Task](/en-us/dotnet/api/system.threading.tasks.task)s.

Note

Asynchronous actions performed in lifecycle events might delay component rendering or displaying data. For more information, see the [Handle incomplete asynchronous actions at render](#handle-incomplete-asynchronous-actions-at-render) section later in this article.

A parent component renders before its children components because rendering is what determines which children are present. If synchronous parent component initialization is used, the parent initialization is guaranteed to complete first. If asynchronous parent component initialization is used, the completion order of parent and child component initialization can't be determined because it depends on the initialization code running.

DOM event processing:

1. The event handler is run.
2. If an incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task) is returned, the [Task](/en-us/dotnet/api/system.threading.tasks.task) is awaited and then the component is rerendered.
3. Render for all synchronous work and complete [Task](/en-us/dotnet/api/system.threading.tasks.task)s.

The `Render` lifecycle:

1. Avoid further rendering operations on the component when both of the following conditions are met:

   * It is not the first render.
   * [`ShouldRender`](rendering?view=aspnetcore-10.0#suppress-ui-refreshing-shouldrender) returns `false`.

2. Build the render tree diff (difference) and render the component.

3. Await the DOM to update.

4. Call [`OnAfterRender{Async}`](#after-component-render-onafterrenderasync). The synchronous method is called prior to the asynchronous method.

Developer calls to [`StateHasChanged`](#state-changes-statehaschanged) result in a rerender. For more information, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0#statehaschanged).

## Quiescence during prerendering

In server-side Blazor apps, prerendering waits for *quiescence*, which means that a component doesn't render until all of the components in the render tree have finished rendering. Quiescence can lead to noticeable delays in rendering when a component performs long-running tasks during initialization and other lifecycle methods, leading to a poor user experience. For more information, see the [Handle incomplete asynchronous actions at render](#handle-incomplete-asynchronous-actions-at-render) section later in this article.

## When parameters are set (`SetParametersAsync`)

[SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync) sets parameters supplied by the component's parent in the render tree or from route parameters.

The method's [ParameterView](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview) parameter contains the set of [component parameter](./?view=aspnetcore-10.0#component-parameters) values for the component each time [SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync) is called. By overriding the [SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync) method, developer code can interact directly with [ParameterView](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview)'s parameters.

The default implementation of [SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync) sets the value of each property with the [`[Parameter]`](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute) or [`[CascadingParameter]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingparameterattribute) that has a corresponding value in the [ParameterView](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview). Parameters that don't have a corresponding value in [ParameterView](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview) are left unchanged.

Generally, your code should call the base class method (`await base.SetParametersAsync(parameters);`) when overriding [SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync). In advanced scenarios, developer code can interpret the incoming parameters' values in any way required by not invoking the base class method. For example, there's no requirement to assign the incoming parameters to the properties of the class. However, you must refer to the [`ComponentBase` reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ComponentBase.cs) when structuring your code without calling the base class method because it calls other lifecycle methods and triggers rendering in a complex fashion.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

If you want to rely on the initialization and rendering logic of [ComponentBase.SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync) but not process incoming parameters, you have the option of passing an empty [ParameterView](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview) to the base class method:

```
await base.SetParametersAsync(ParameterView.Empty);
```

If event handlers are provided in developer code, unhook them on disposal. For more information, see [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

In the following example, [ParameterView.TryGetValue](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview.trygetvalue) assigns the `Param` parameter's value to `value` if parsing a route parameter for `Param` is successful. When `value` isn't `null`, the value is displayed by the component.

Although [route parameter matching is case insensitive](../fundamentals/routing?view=aspnetcore-10.0#route-parameters), [TryGetValue](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview.trygetvalue) only matches case-sensitive parameter names in the route template. The following example requires the use of `/{Param?}` in the route template in order to get the value with [TryGetValue](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview.trygetvalue), not `/{param?}`. If `/{param?}` is used in this scenario, [TryGetValue](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterview.trygetvalue) returns `false` and `message` isn't set to either `message` string.

`SetParamsAsync.razor`:

```
@page "/set-params-async/{Param?}"

<PageTitle>Set Parameters Async</PageTitle>

<h1>Set Parameters Async Example</h1>

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string? Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
```

```
@page "/set-params-async/{Param?}"

<PageTitle>Set Parameters Async</PageTitle>

<h1>Set Parameters Async Example</h1>

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string? Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
```

```
@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string? Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
```

```
@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

    [Parameter]
    public string? Param { get; set; }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
```

```
@page "/set-params-async/{Param?}"

<p>@message</p>

@code {
    private string message = "Not set";

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

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
```

```
@page "/set-params-async"
@page "/set-params-async/{Param}"

<p>@message</p>

@code {
    private string message = "Not set";

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

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (parameters.TryGetValue<string>(nameof(Param), out var value))
        {
            if (value is null)
            {
                message = "The value of 'Param' is null.";
            }
            else
            {
                message = $"The value of 'Param' is {value}.";
            }
        }

        await base.SetParametersAsync(parameters);
    }
}
```

## Component initialization (`OnInitialized{Async}`)

[OnInitialized](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitialized) and [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) are used exclusively to initialize a component for the entire lifetime of the component instance. Parameter values and parameter value changes shouldn't affect the initialization performed in these methods. For example, loading static options into a dropdown list that doesn't change for the lifetime of the component and that isn't dependent on parameter values is performed in one of these lifecycle methods. If parameter values or changes in parameter values affect component state, use [`OnParametersSet{Async}`](#when-parameters-are-set-setparametersasync) instead.

These methods are invoked when the component is initialized after having received its initial parameters in [SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync). The synchronous method is called prior to the asynchronous method.

If synchronous parent component initialization is used, the parent initialization is guaranteed to complete before child component initialization. If asynchronous parent component initialization is used, the completion order of parent and child component initialization can't be determined because it depends on the initialization code running.

For a synchronous operation, override [OnInitialized](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitialized):

`OnInit.razor`:

```
@page "/on-init"

<PageTitle>On Initialized</PageTitle>

<h1>On Initialized Example</h1>

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized() => 
        message = $"Initialized at {DateTime.Now}";
}
```

```
@page "/on-init"

<PageTitle>On Initialized</PageTitle>

<h1>On Initialized Example</h1>

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized() => 
        message = $"Initialized at {DateTime.Now}";
}
```

```
@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
```

```
@page "/on-init"

<p>@message</p>

@code {
    private string? message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
```

```
@page "/on-init"

<p>@message</p>

@code {
    private string message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
```

```
@page "/on-init"

<p>@message</p>

@code {
    private string message;

    protected override void OnInitialized()
    {
        message = $"Initialized at {DateTime.Now}";
    }
}
```

To perform an asynchronous operation, override [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) and use the [`await`](/en-us/dotnet/csharp/language-reference/operators/await) operator:

```
protected override async Task OnInitializedAsync()
{
    await ...
}
```

If a custom [base class](./?view=aspnetcore-10.0#specify-a-base-class) is used with custom initialization logic, call [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) on the base class:

```
protected override async Task OnInitializedAsync()
{
    await ...

    await base.OnInitializedAsync();
}
```

It isn't necessary to call [ComponentBase.OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) unless a custom base class is used with custom logic. For more information, see the [Base class lifecycle methods](#base-class-lifecycle-methods) section.

A component must ensure that it's in a valid state for rendering when [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) awaits a potentially incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task). If the method returns an incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task), the part of the method that completes synchronously must leave the component in a valid state for rendering. For more information, see the introductory remarks of [ASP.NET Core Blazor synchronization context](synchronization-context?view=aspnetcore-10.0) and [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

Blazor apps that prerender their content on the server call [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) *twice*:

* Once when the component is initially rendered statically as part of the page.
* A second time when the browser renders the component.

To prevent developer code in [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) from running twice when prerendering, see the [Stateful reconnection after prerendering](#stateful-reconnection-after-prerendering) section. The content in the section focuses on Blazor Web Apps and stateful SignalR *reconnection*. To preserve state during the execution of initialization code while prerendering, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0).

To prevent developer code in [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) from running twice when prerendering, see the [Stateful reconnection after prerendering](#stateful-reconnection-after-prerendering) section. Although the content in the section focuses on Blazor Server and stateful SignalR *reconnection*, the scenario for prerendering in hosted Blazor WebAssembly solutions ([WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered)) involves similar conditions and approaches to prevent executing developer code twice. To preserve state during the execution of initialization code while prerendering, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](integration?view=aspnetcore-10.0#persist-prerendered-state).

While a Blazor app is prerendering, certain actions, such as calling into JavaScript (JS interop), aren't possible. Components may need to render differently when prerendered. For more information, see the [Prerendering with JavaScript interop](#prerendering-with-javascript-interop) section.

If event handlers are provided in developer code, unhook them on disposal. For more information, see [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

Use *streaming rendering* with static server-side rendering (static SSR) or prerendering to improve the user experience for components that perform long-running asynchronous tasks in [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) to fully render. For more information, see the following resources:

* [Handle incomplete asynchronous actions at render](#handle-incomplete-asynchronous-actions-at-render) (this article)
* [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0#streaming-rendering)

## After parameters are set (`OnParametersSet{Async}`)

[OnParametersSet](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparametersset) or [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) are called:

* After the component is initialized in [OnInitialized](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitialized) or [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync).

* When the parent component rerenders and supplies:

  * Known or primitive immutable types when at least one parameter has changed.
  * Complex-typed parameters. The framework can't know whether the values of a complex-typed parameter have mutated internally, so the framework always treats the parameter set as changed when one or more complex-typed parameters are present.

  For more information on rendering conventions, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0#rendering-conventions-for-componentbase).

The synchronous method is called prior to the asynchronous method.

The methods can be invoked even if the parameter values haven't changed. This behavior underscores the need for developers to implement additional logic within the methods to check whether parameter values have indeed changed before re-initializing data or state dependent on those parameters.

For the following example component, navigate to the component's page at a URL:

* With a start date that's received by `StartDate`: `/on-parameters-set/2021-03-19`
* Without a start date, where `StartDate` is assigned a value of the current local time: `/on-parameters-set`

Note

In a component route, it isn't possible to both constrain a [DateTime](/en-us/dotnet/api/system.datetime) parameter with the [route constraint `datetime`](../fundamentals/routing?view=aspnetcore-10.0#route-constraints) and [make the parameter optional](../fundamentals/routing?view=aspnetcore-10.0#route-parameters). Therefore, the following `OnParamsSet` component uses two [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directives to handle routing with and without a supplied date segment in the URL.

`OnParamsSet.razor`:

```
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<PageTitle>On Parameters Set</PageTitle>

<h1>On Parameters Set Example</h1>

<p>
    Pass a datetime in the URI of the browser's address bar. 
    For example, add <code>/1-1-2024</code> to the address.
</p>

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied " +
                $"(StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used " +
                $"(StartDate: {StartDate}).";
        }
    }
}
```

```
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<PageTitle>On Parameters Set</PageTitle>

<h1>On Parameters Set Example</h1>

<p>
    Pass a datetime in the URI of the browser's address bar. 
    For example, add <code>/1-1-2024</code> to the address.
</p>

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied " +
                $"(StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used " +
                $"(StartDate: {StartDate}).";
        }
    }
}
```

```
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
```

```
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string? message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
```

```
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
```

```
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"

<p>@message</p>

@code {
    private string message;

    [Parameter]
    public DateTime StartDate { get; set; }

    protected override void OnParametersSet()
    {
        if (StartDate == default)
        {
            StartDate = DateTime.Now;

            message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
        }
        else
        {
            message = $"The start date in the URL was used (StartDate: {StartDate}).";
        }
    }
}
```

Asynchronous work when applying parameters and property values must occur during the [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) lifecycle event:

```
protected override async Task OnParametersSetAsync()
{
    await ...
}
```

If a custom [base class](./?view=aspnetcore-10.0#specify-a-base-class) is used with custom initialization logic, call [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) on the base class:

```
protected override async Task OnParametersSetAsync()
{
    await ...

    await base.OnParametersSetAsync();
}
```

It isn't necessary to call [ComponentBase.OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) unless a custom base class is used with custom logic. For more information, see the [Base class lifecycle methods](#base-class-lifecycle-methods) section.

A component must ensure that it's in a valid state for rendering when [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) awaits a potentially incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task). If the method returns an incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task), the part of the method that completes synchronously must leave the component in a valid state for rendering. For more information, see the introductory remarks of [ASP.NET Core Blazor synchronization context](synchronization-context?view=aspnetcore-10.0) and [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

If event handlers are provided in developer code, unhook them on disposal. For more information, see [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

If a disposable component doesn't use a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken), [OnParametersSet](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparametersset) and [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) should check if the component is disposed. If [OnParametersSetAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onparameterssetasync) returns an incomplete [Task](/en-us/dotnet/api/system.threading.tasks.task), the component must ensure that the part of the method that completes synchronously leaves the component in a valid state for rendering. For more information, see the introductory remarks of [ASP.NET Core Blazor synchronization context](synchronization-context?view=aspnetcore-10.0) and [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

For more information on route parameters and constraints, see [ASP.NET Core Blazor routing](../fundamentals/routing?view=aspnetcore-10.0).

For an example of implementing `SetParametersAsync` manually to improve performance in some scenarios, see [ASP.NET Core Blazor rendering performance best practices](../performance/rendering?view=aspnetcore-10.0#implement-setparametersasync-manually).

## After component render (`OnAfterRender{Async}`)

[OnAfterRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrender) and [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) are invoked after a component has rendered interactively and the UI has finished updating (for example, after elements are added to the browser DOM). Element and component references are populated at this point. Use this stage to perform additional initialization steps with the rendered content, such as JS interop calls that interact with the rendered DOM elements. The synchronous method is called prior to the asynchronous method.

These methods aren't invoked during prerendering or static server-side rendering (static SSR) on the server because those processes aren't attached to a live browser DOM and are already complete before the DOM is updated.

For [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync), the component doesn't automatically rerender after the completion of any returned `Task` to avoid an infinite render loop.

[OnAfterRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrender) and [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) are called after a component has finished rendering. Element and component references are populated at this point. Use this stage to perform additional initialization steps with the rendered content, such as JS interop calls that interact with the rendered DOM elements. The synchronous method is called prior to the asynchronous method.

These methods aren't invoked during prerendering because prerendering isn't attached to a live browser DOM and is already complete before the DOM is updated.

For [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync), the component doesn't automatically rerender after the completion of any returned `Task` to avoid an infinite render loop.

The `firstRender` parameter for [OnAfterRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrender) and [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync):

* Is set to `true` the first time that the component instance is rendered.
* Can be used to ensure that initialization work is only performed once.

`AfterRender.razor`:

```
@page "/after-render"
@inject ILogger<AfterRender> Logger 

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender) =>
        Logger.LogInformation("firstRender = {FirstRender}", firstRender);

    private void HandleClick() => Logger.LogInformation("HandleClick called");
}
```

```
@page "/after-render"
@inject ILogger<AfterRender> Logger 

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender) =>
        Logger.LogInformation("firstRender = {FirstRender}", firstRender);

    private void HandleClick() => Logger.LogInformation("HandleClick called");
}
```

```
@page "/after-render"
@inject ILogger<AfterRender> Logger

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
```

```
@page "/after-render"
@inject ILogger<AfterRender> Logger 

<PageTitle>After Render</PageTitle>

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
```

```
@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger 

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
```

```
@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger

<h1>After Render Example</h1>

<p>
    <button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>

<p>Study logged messages in the console.</p>

@code {
    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }

    private void HandleClick()
    {
        Logger.LogInformation("HandleClick called");
    }
}
```

The `AfterRender.razor` sample produces following output to console when the page is loaded and the button is selected:

> OnAfterRender: firstRender = True\
> HandleClick called\
> OnAfterRender: firstRender = False

Asynchronous work immediately after rendering must occur during the [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) lifecycle event:

```
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    ...
}
```

If a custom [base class](./?view=aspnetcore-10.0#specify-a-base-class) is used with custom initialization logic, call [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) on the base class:

```
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    ...

    await base.OnAfterRenderAsync(firstRender);
}
```

It isn't necessary to call [ComponentBase.OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) unless a custom base class is used with custom logic. For more information, see the [Base class lifecycle methods](#base-class-lifecycle-methods) section.

Even if you return a [Task](/en-us/dotnet/api/system.threading.tasks.task) from [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync), the framework doesn't schedule a further render cycle for your component once that task completes. This is to avoid an infinite render loop. This is different from the other lifecycle methods, which schedule a further render cycle once a returned [Task](/en-us/dotnet/api/system.threading.tasks.task) completes.

[OnAfterRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrender) and [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) *aren't called during the prerendering process on the server*. The methods are called when the component is rendered interactively after prerendering. When the app prerenders:

1. The component executes on the server to produce some static HTML markup in the HTTP response. During this phase, [OnAfterRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrender) and [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) aren't called.
2. When the Blazor script (`blazor.{server|webassembly|web}.js`) starts in the browser, the component is restarted in an interactive rendering mode. After a component is restarted, [OnAfterRender](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrender) and [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) **are** called because the app isn't in the prerendering phase any longer.

If event handlers are provided in developer code, unhook them on disposal. For more information, see [ASP.NET Core Razor component disposal](component-disposal?view=aspnetcore-10.0).

## Base class lifecycle methods

When overriding Blazor's lifecycle methods, it isn't necessary to call base class lifecycle methods for [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase). However, a component should call an overridden base class lifecycle method in the following situations:

* When overriding [ComponentBase.SetParametersAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.setparametersasync), `await base.SetParametersAsync(parameters);` is usually invoked because the base class method calls other lifecycle methods and triggers rendering in a complex fashion. For more information, see the [When parameters are set (`SetParametersAsync`)](#when-parameters-are-set-setparametersasync) section.
* If the base class method contains logic that must be executed. Library consumers usually call base class lifecycle methods when inheriting a base class because library base classes often have custom lifecycle logic to execute. If the app uses a base class from a library, consult the library's documentation for guidance.

In the following example, `base.OnInitialized();` is called to ensure that the base class's `OnInitialized` method is executed. Without the call, `BlazorRocksBase2.OnInitialized` doesn't execute.

`BlazorRocks2.razor`:

```
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
```

```
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
```

```
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
```

```
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<PageTitle>Blazor Rocks!</PageTitle>

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
```

```
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
```

```
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger

<h1>Blazor Rocks! Example 2</h1>

<p>
    @BlazorRocksText
</p>

@code {
    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocks2 executed!");

        base.OnInitialized();
    }
}
```

`BlazorRocksBase2.cs`:

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";

    protected override void OnInitialized() =>
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";

    protected override void OnInitialized() =>
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
```

```
using Microsoft.AspNetCore.Components;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
```

```
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
```

```
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorSample;

public class BlazorRocksBase2 : ComponentBase
{
    [Inject]
    private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;

    public string BlazorRocksText { get; set; } =
        "Blazor rocks the browser!";

    protected override void OnInitialized()
    {
        Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
    }
}
```

## State changes (`StateHasChanged`)

[StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) notifies the component that its state has changed. When applicable, calling [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) enqueues a rerender that occurs when the app's main thread is free.

[StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is called automatically for [EventCallback](/en-us/dotnet/api/microsoft.aspnetcore.components.eventcallback) methods. For more information on event callbacks, see [ASP.NET Core Blazor event handling](event-handling?view=aspnetcore-10.0#eventcallback).

For more information on component rendering and when to call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged), including when to invoke it with [ComponentBase.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync), see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0).

## Handle incomplete asynchronous actions at render

Asynchronous actions performed in lifecycle events might not have completed before the component is rendered. Objects might be `null` or incompletely populated with data while the lifecycle method is executing. Provide rendering logic to confirm that objects are initialized. Render placeholder UI elements (for example, a loading message) while objects are `null`.

In the following `Slow` component, [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) is overridden to asynchronously execute a long-running task. While `isLoading` is `true`, a loading message is displayed to the user. After the `Task` returned by [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) completes, the component is rerendered with the updated state, showing the "`Finished!`" message.

`Slow.razor`:

```
@page "/slow"

<h2>Slow Component</h2>

@if (isLoading)
{
    <div><em>Loading...</em></div>
}
else
{
    <div>Finished!</div>
}

@code {
    private bool isLoading = true;

    protected override async Task OnInitializedAsync()
    {
        await LoadDataAsync();
        isLoading = false;
    }

    private Task LoadDataAsync()
    {
        return Task.Delay(10000);
    }
}
```

The preceding component uses an `isLoading` variable to display the loading message. A similar approach is used for a component that loads data into a collection and checks if the collection is `null` to present the loading message. The following example checks the `movies` collection for `null` to either display the loading message or display the collection of movies:

```
@if (movies == null)
{
    <p><em>Loading...</em></p>
}
else
{
    @* display movies *@
}

@code {
    private Movies[]? movies;

    protected override async Task OnInitializedAsync()
    {
        movies = await GetMovies();
    }
}
```

Prerendering waits for *quiescence*, which means that a component doesn't render until all of the components in the render tree have finished rendering. This means that a loading message doesn't display while a child component's [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) method is executing a long-running task during prerendering. To demonstrate this behavior, place the preceding `Slow` component into a test app's `Home` component:

```
@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<Slow />
```

Note

Although the examples in this section discuss the [OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync) lifecycle method, other lifecycle methods that execute during prerendering may delay final rendering of a component. Only [`OnAfterRender{Async}`](#after-component-render-onafterrenderasync) isn't executed during prerendering and is immune to delays due to quiescence.

During prerendering, the `Home` component doesn't render until the `Slow` component is rendered, which takes ten seconds. The UI is blank during this ten-second period, and there's no loading message. After prerendering, the `Home` component renders, and the `Slow` component's loading message is displayed. After ten more seconds, the `Slow` component finally displays the finished message.

As the preceding demonstration illustrates, quiescence during prerendering results in a poor user experience. To improve the user experience, begin by implementing [streaming rendering](rendering?view=aspnetcore-10.0#streaming-rendering) to avoid waiting for the asynchronous task to complete while prerendering.

Add the [`[StreamRendering]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.streamrenderingattribute) to the `Slow` component (use `[StreamRendering(true)]` in .NET 8):

```
@attribute [StreamRendering]
```

When the `Home` component is prerendering, the `Slow` component is quickly rendered with its loading message. The `Home` component doesn't wait for ten seconds for the `Slow` component to finish rendering. However, the finished message displayed at the end of prerendering is replaced by the loading message while the component finally renders, which is another ten-second delay. This occurs because the `Slow` component is rendering twice, along with `LoadDataAsync` executing twice. When a component is accessing resources, such as services and databases, double execution of service calls and database queries creates undesirable load on the app's resources.

To address the double rendering of the loading message and the re-execution of service and database calls, persist prerendered state with [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) for final rendering of the component, as seen in the following updates to the `Slow` component:

```
@page "/slow"
@attribute [StreamRendering]

<h2>Slow Component</h2>

@if (Data is null)
{
    <div><em>Loading...</em></div>
}
else
{
    <div>@Data</div>
}

@code {
    [PersistentState]
    public string? Data { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Data ??= await LoadDataAsync();
    }

    private async Task<string> LoadDataAsync()
    {
        await Task.Delay(5000);
        return "Finished!";
    }
}
```

```
@page "/slow"
@attribute [StreamRendering]
@implements IDisposable
@inject PersistentComponentState ApplicationState

<h2>Slow Component</h2>

@if (data is null)
{
    <div><em>Loading...</em></div>
}
else
{
    <div>@data</div>
}

@code {
    private string? data;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<string>(nameof(data), out var restored))
        {
            data = await LoadDataAsync();
        }
        else
        {
            data = restored!;
        }

        // Call at the end to avoid a potential race condition at app shutdown
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson(nameof(data), data);

        return Task.CompletedTask;
    }

    private async Task<string> LoadDataAsync()
    {
        await Task.Delay(5000);
        return "Finished!";
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}
```

By combining streaming rendering with persistent component state:

* Services and databases only require a single call for component initialization.
* Components render their UIs quickly with loading messages during long-running tasks for the best user experience.

For more information, see the following resources:

* [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0#streaming-rendering)
* [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0).

:::moniker-end

Quiescence during prerendering results in a poor user experience. The delay can be addressed in apps that target .NET 8 or later with a feature called *streaming rendering*, usually combined with [persisting component state during prerendering](integration?view=aspnetcore-10.0#persist-prerendered-state) to avoid waiting for the asynchronous task to complete. In versions of .NET earlier than .NET 8, executing a [long-running background task](#cancelable-background-work) that loads the data after final rendering can address a long rendering delay due to quiescence.

## Handle errors

For information on handling errors during lifecycle method execution, see [Handle errors in ASP.NET Core Blazor apps](../fundamentals/handle-errors?view=aspnetcore-10.0).

## Stateful reconnection after prerendering

When prerendering on the server, a component is initially rendered statically as part of the page. Once the browser establishes a SignalR connection back to the server, the component is rendered *again* and interactive. If the [`OnInitialized{Async}`](#component-initialization-oninitializedasync) lifecycle method for initializing the component is present, the method is executed *twice*:

* When the component is prerendered statically.
* After the server connection has been established.

This can result in a noticeable change in the data displayed in the UI when the component is finally rendered. To avoid this behavior, pass in an identifier to cache the state during prerendering and to retrieve the state after prerendering.

The following code demonstrates a `WeatherForecastService` that avoids the change in data display due to prerendering. The awaited [Delay](/en-us/dotnet/api/system.threading.tasks.task.delay) (`await Task.Delay(...)`) simulates a short delay before returning data from the `GetForecastAsync` method.

Add [IMemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) services with [AddMemoryCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.memorycacheservicecollectionextensions.addmemorycache) on the service collection in the app's `Program` file:

```
builder.Services.AddMemoryCache();
```

`WeatherForecastService.cs`:

```
using Microsoft.Extensions.Caching.Memory;

namespace BlazorSample;

public class WeatherForecastService(IMemoryCache memoryCache)
{
    private static readonly string[] summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    public IMemoryCache MemoryCache { get; } = memoryCache;

    public Task<WeatherForecast[]?> GetForecastAsync(DateOnly startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
```

```
using Microsoft.Extensions.Caching.Memory;

namespace BlazorSample;

public class WeatherForecastService(IMemoryCache memoryCache)
{
    private static readonly string[] summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    public IMemoryCache MemoryCache { get; } = memoryCache;

    public Task<WeatherForecast[]?> GetForecastAsync(DateOnly startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
```

```
using Microsoft.Extensions.Caching.Memory;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
```

```
using Microsoft.Extensions.Caching.Memory;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
```

```
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
```

```
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;

public class WeatherForecastService
{
    private static readonly string[] summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        MemoryCache = memoryCache;
    }

    public IMemoryCache MemoryCache { get; }

    public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
    {
        return MemoryCache.GetOrCreateAsync(startDate, async e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow =
                    TimeSpan.FromSeconds(30)
            });

            var rng = new Random();

            await Task.Delay(TimeSpan.FromSeconds(10));

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = summaries[rng.Next(summaries.Length)]
            }).ToArray();
        });
    }
}
```

For more information on the [RenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.componenttaghelper.rendermode#microsoft-aspnetcore-mvc-taghelpers-componenttaghelper-rendermode), see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#server-side-render-mode).

The content in this section focuses on Blazor Web Apps and stateful SignalR *reconnection*. To preserve state during the execution of initialization code while prerendering, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0).

Although the content in this section focuses on Blazor Server and stateful SignalR *reconnection*, the scenario for prerendering in hosted Blazor WebAssembly solutions ([WebAssemblyPrerendered](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.rendermode#microsoft-aspnetcore-mvc-rendering-rendermode-webassemblyprerendered)) involves similar conditions and approaches to prevent executing developer code twice. To preserve state during the execution of initialization code while prerendering, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](integration?view=aspnetcore-10.0#persist-prerendered-state).

## Prerendering with JavaScript interop

*This section applies to server-side apps that prerender Razor components. Prerendering is covered in [Prerender ASP.NET Core Razor components](prerender?view=aspnetcore-10.0).*

*This section applies to server-side apps that prerender Razor components. Prerendering is covered in [Prerender ASP.NET Core Razor components](prerender?view=aspnetcore-10.0).*

Note

Internal navigation for [interactive routing](../fundamentals/routing?view=aspnetcore-10.0#static-versus-interactive-routing) in Blazor Web Apps doesn't involve requesting new page content from the server. Therefore, prerendering doesn't occur for internal page requests. If the app adopts interactive routing, perform a full page reload for component examples that demonstrate prerendering behavior. For more information, see [ASP.NET Core Blazor prerendered state persistence](../state-management/prerendered-state-persistence?view=aspnetcore-10.0#interactive-routing-and-prerendering).

*This section applies to server-side apps and hosted Blazor WebAssembly apps that prerender Razor components. Prerendering is covered in [Integrate ASP.NET Core Razor components with MVC or Razor Pages](integration?view=aspnetcore-10.0).*

During prerendering, calling into JavaScript (JS) isn't possible. The following example demonstrates how to use JS interop as part of a component's initialization logic in a way that's compatible with prerendering.

The following `scrollElementIntoView` function:

* Scrolls to the passed element with [`scrollIntoView`](https://developer.mozilla.org/docs/Web/API/Element/scrollIntoView).
* Returns the element's `top` property value from the [`getBoundingClientRect`](https://developer.mozilla.org/docs/Web/API/Element/getBoundingClientRect) method.

```
window.scrollElementIntoView = (element) => {
  element.scrollIntoView();
  return element.getBoundingClientRect().top;
}
```

Where [IJSRuntime.InvokeAsync](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime.invokeasync) calls the JS function in component code, the [ElementReference](/en-us/dotnet/api/microsoft.aspnetcore.components.elementreference) is only used in [OnAfterRenderAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.onafterrenderasync) and not in any earlier lifecycle method because there's no HTML DOM element until after the component is rendered.

[`StateHasChanged`](lifecycle?view=aspnetcore-10.0#state-changes-statehaschanged) ([reference source](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged)) is called to enqueue rerendering of the component with the new state obtained from the JS interop call (for more information, see [ASP.NET Core Razor component rendering](rendering?view=aspnetcore-10.0)). An infinite loop isn't created because [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is only called when `scrollPosition` is `null`.

`PrerenderedInterop.razor`:

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<PageTitle>Prerendered Interop</PageTitle>

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

```
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Prerendered Interop Example</h1>

<div @ref="divElement" style="margin-top:2000px">
    Set value via JS interop call: <strong>@scrollPosition</strong>
</div>

@code {
    private ElementReference divElement;
    private double? scrollPosition;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && scrollPosition is null)
        {
            scrollPosition = await JS.InvokeAsync<double>(
                "scrollElementIntoView", divElement);

            StateHasChanged();
        }
    }
}
```

The preceding example pollutes the client with a global function. For a better approach in production apps, see [JavaScript isolation in JavaScript modules](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#javascript-isolation-in-javascript-modules).

## Cancelable background work

Components often perform long-running background work, such as making network calls ([HttpClient](/en-us/dotnet/api/system.net.http.httpclient)) and interacting with databases. It's desirable to stop the background work to conserve system resources in several situations. For example, background asynchronous operations don't automatically stop when a user navigates away from a component.

Other reasons why background work items might require cancellation include:

* An executing background task was started with faulty input data or processing parameters.
* The current set of executing background work items must be replaced with a new set of work items.
* The priority of currently executing tasks must be changed.
* The app must be shut down for server redeployment.
* Server resources become limited, necessitating the rescheduling of background work items.

To implement a cancelable background work pattern in a component:

* Use a [CancellationTokenSource](/en-us/dotnet/api/system.threading.cancellationtokensource) and [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken).
* Upon [disposal of the component](component-disposal?view=aspnetcore-10.0) and at any point cancellation is desired by manually canceling the token, call [`CancellationTokenSource.Cancel`](/en-us/dotnet/api/system.threading.cancellationtokensource.cancel) to signal that the background work should be cancelled.
* After the asynchronous call returns, call [ThrowIfCancellationRequested](/en-us/dotnet/api/system.threading.cancellationtoken.throwifcancellationrequested) on the token.

In the following example:

* `await Task.Delay(10000, cts.Token);` represents long-running asynchronous background work.
* `BackgroundResourceMethod` represents a long-running background method that shouldn't start if the `Resource` is disposed before the method is called.

`BackgroundWork.razor`:

```
@page "/background-work"
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<PageTitle>Background Work</PageTitle>

<h1>Background Work Example</h1>

<p>
    <button @onclick="LongRunningWork">Trigger long running work</button>
    <button @onclick="Dispose">Trigger Disposal</button>
</p>
<p>Study logged messages in the console.</p>
<p>
    If you trigger disposal within 10 seconds of page load, the 
    <code>BackgroundResourceMethod</code> isn't executed.
</p>
<p>
    If disposal occurs after <code>BackgroundResourceMethod</code> is called but
    before action is taken on the resource, an <code>ObjectDisposedException</code>
    is thrown by <code>BackgroundResourceMethod</code>, and the resource isn't
    processed.
</p>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();
    private IList<string> messages = [];

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(10000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");

        if (!cts.IsCancellationRequested)
        {
            cts.Cancel();
        }
        
        cts?.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose() => disposed = true;
    }
}
```

```
@page "/background-work"
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<PageTitle>Background Work</PageTitle>

<h1>Background Work Example</h1>

<p>
    <button @onclick="LongRunningWork">Trigger long running work</button>
    <button @onclick="Dispose">Trigger Disposal</button>
</p>
<p>Study logged messages in the console.</p>
<p>
    If you trigger disposal within 10 seconds of page load, the 
    <code>BackgroundResourceMethod</code> isn't executed.
</p>
<p>
    If disposal occurs after <code>BackgroundResourceMethod</code> is called but
    before action is taken on the resource, an <code>ObjectDisposedException</code>
    is thrown by <code>BackgroundResourceMethod</code>, and the resource isn't
    processed.
</p>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();
    private IList<string> messages = [];

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(10000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");

        if (!cts.IsCancellationRequested)
        {
            cts.Cancel();
        }
        
        cts?.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose() => disposed = true;
    }
}
```

```
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
```

```
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
```

```
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new();
    private CancellationTokenSource cts = new();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
```

```
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger

<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>

@code {
    private Resource resource = new Resource();
    private CancellationTokenSource cts = new CancellationTokenSource();

    protected async Task LongRunningWork()
    {
        Logger.LogInformation("Long running work started");

        await Task.Delay(5000, cts.Token);

        cts.Token.ThrowIfCancellationRequested();
        resource.BackgroundResourceMethod(Logger);
    }

    public void Dispose()
    {
        Logger.LogInformation("Executing Dispose");
        cts.Cancel();
        cts.Dispose();
        resource?.Dispose();
    }

    private class Resource : IDisposable
    {
        private bool disposed;

        public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
        {
            logger.LogInformation("BackgroundResourceMethod: Start method");

            if (disposed)
            {
                logger.LogInformation("BackgroundResourceMethod: Disposed");
                throw new ObjectDisposedException(nameof(Resource));
            }

            // Take action on the Resource

            logger.LogInformation("BackgroundResourceMethod: Action on Resource");
        }

        public void Dispose()
        {
            disposed = true;
        }
    }
}
```

To display a loading indicator while the background work is taking place, use the following approach.

Create a loading indicator component with a `Loading` parameter that can display child content in a [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment). For the `Loading` parameter:

* When `true`, display a loading indicator.
* When `false`, render the component's content (`ChildContent`). For more information, see [Child content render fragments](./?view=aspnetcore-10.0#child-content-render-fragments).

`ContentLoading.razor`:

```
@if (Loading)
{
    <progress id="loadingIndicator" aria-label="Content loading…"></progress>
}
else
{
    @ChildContent
}

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    [Parameter]
    public bool Loading { get; set; }
}
```

To load CSS styles for the indicator, add the styles to `<head>` content with the [HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component. For more information, see [Control head content in ASP.NET Core Blazor apps](control-head-content?view=aspnetcore-10.0).

```
@if (Loading)
{
    <!-- OPTIONAL ...
    <HeadContent>
        <style>
            ...
        </style>
    </HeadContent>
    -->
    <progress id="loadingIndicator" aria-label="Content loading…"></progress>
}
else
{
    @ChildContent
}

...
```

Wrap the component's Razor markup with the `ContentLoading` component and pass a value in a C# field to the `Loading` parameter when initialization work is performed by the component:

```
<ContentLoading Loading="@loading">
    ...
</ContentLoading>

@code {
    private bool loading = true;
    ...

    protected override async Task OnInitializedAsync()
    {
        await LongRunningWork().ContinueWith(_ => loading = false);
    }

    ...
}
```

## Blazor Server reconnection events

The component lifecycle events covered in this article operate separately from [server-side reconnection event handlers](../fundamentals/signalr?view=aspnetcore-10.0#reflect-the-server-side-connection-state-in-the-ui). When the SignalR connection to the client is lost, only UI updates are interrupted. UI updates are resumed when the connection is re-established. For more information on circuit handler events and configuration, see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0).

## Related content

[Handle caught exceptions outside of a Razor component's lifecycle](synchronization-context?view=aspnetcore-10.0#handle-caught-exceptions-outside-of-a-razor-components-lifecycle)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0010
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0010.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0010: Do not use UseStartup with WebApplicationBuilder.WebHost

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0010      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

`UseStartup` can't be used with `WebApplicationBuilder.WebHost`.

## Rule description

The `WebApplicationBuilder` doesn't support configuration via a `Startup` class.

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseStartup<Startup>();

var app = builder.Build();

app.Run();
```

## How to fix violations

To fix a violation of this rule, leverage the `Configuration` and `Services` properties on the `WebApplicationBuilder` to modify configuration and DI directly, without the need for a startup class.

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication();

var app = builder.Build();

app.Run();
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule. A misconfigured application can result in unexpected behavior at runtime.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/state-management/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor state management overview

This article and the other articles in this node describe common approaches for maintaining a user's data (state) while they use an app and across browser sessions, including during server prerendering.

A typical requirement during Blazor app development is sharing state across components:

* Parent to child: A parent component passes state to a child component using parameters.
* Child to parent: A child component enables data binding to its state or provides state through callbacks.
* Parent to descendants: A parent shares state with all of its descendants using cascading values.
* App-wide: State is shared across the entire app using configured app state services.
* Per circuit: State is shared for a specific circuit using scoped app state services.

Persisted state may need to survive pages refreshes, resumed circuits, and prerendering. State often requires central management, tracking, and testing. The locations and techniques for persisting state are highly variable.

Blazor doesn't provide comprehensive, opinionated state management. Third-party state container products and services that work seamlessly with Blazor, such as Flux, Redux, and MobX, satisfy virtually any app requirement.

The remainder of this article discusses general state management strategies for any type of Blazor app.

## State management using the URL

For transient data representing navigation state, model the data as a part of the URL. Examples of user state modeled in the URL include:

* The ID of a viewed entity.
* The current page number in a paged grid.

The contents of the browser's address bar are retained:

* If the user manually reloads the page.
* *Server-side scenarios only*: If the web server becomes unavailable, and the user is forced to reload the page in order to connect to a different server.

For information on defining URL patterns with the [`@page`](../../mvc/views/razor?view=aspnetcore-10.0#page) directive, see [ASP.NET Core Blazor routing](../fundamentals/routing?view=aspnetcore-10.0).

## In-memory state container service

Nested components typically bind data using *chained bind* as described in [ASP.NET Core Blazor data binding](../components/data-binding?view=aspnetcore-10.0). Nested and unnested components can share access to data using a registered in-memory state container. A custom state container class can use an assignable [Action](/en-us/dotnet/api/system.action) to notify components in different parts of the app of state changes. In the following example:

* A pair of components uses a state container to track a property.
* One component in the following example is nested in the other component, but nesting isn't required for this approach to work.

Important

The example in this section demonstrates how to create an in-memory state container service, register the service, and use the service in components. The example doesn't persist data without further development. For persistent storage of data, the state container must adopt an underlying storage mechanism that survives when browser memory is cleared. This can be accomplished with `localStorage`/`sessionStorage` or some other technology.

`StateContainer.cs`:

```
public class StateContainer
{
    private string? savedString;

    public string Property
    {
        get => savedString ?? string.Empty;
        set
        {
            savedString = value;
            NotifyStateChanged();
        }
    }

    public event Action? OnChange;

    private void NotifyStateChanged() => OnChange?.Invoke();
}
```

Client-side apps (`Program` file):

```
builder.Services.AddSingleton<StateContainer>();
```

Server-side apps (`Program` file, ASP.NET Core in .NET 6 or later):

```
builder.Services.AddScoped<StateContainer>();
```

Server-side apps (`Startup.ConfigureServices` of `Startup.cs`, typically in .NET 6 or earlier):

```
services.AddScoped<StateContainer>();
```

`Shared/Nested.razor`:

```
@implements IDisposable
@inject StateContainer StateContainer

<h2>Nested component</h2>

<p>Nested component Property: <b>@StateContainer.Property</b></p>

<p>
    <button @onclick="ChangePropertyValue">
        Change the Property from the Nested component
    </button>
</p>

@code {
    protected override void OnInitialized()
    {
        StateContainer.OnChange += StateHasChanged;
    }

    private void ChangePropertyValue()
    {
        StateContainer.Property = 
            $"New value set in the Nested component: {DateTime.Now}";
    }

    public void Dispose()
    {
        StateContainer.OnChange -= StateHasChanged;
    }
}
```

`StateContainerExample.razor`:

```
@page "/state-container-example"
@implements IDisposable
@inject StateContainer StateContainer

<h1>State Container Example component</h1>

<p>State Container component Property: <b>@StateContainer.Property</b></p>

<p>
    <button @onclick="ChangePropertyValue">
        Change the Property from the State Container Example component
    </button>
</p>

<Nested />

@code {
    protected override void OnInitialized()
    {
        StateContainer.OnChange += StateHasChanged;
    }

    private void ChangePropertyValue()
    {
        StateContainer.Property = "New value set in the State " +
            $"Container Example component: {DateTime.Now}";
    }

    public void Dispose()
    {
        StateContainer.OnChange -= StateHasChanged;
    }
}
```

The preceding components implement [IDisposable](/en-us/dotnet/api/system.idisposable), and the `OnChange` delegates are unsubscribed in the `Dispose` methods, which are called by the framework when the components are disposed. For more information, see [ASP.NET Core Razor component disposal](../components/component-disposal?view=aspnetcore-10.0).

## Cascading values and parameters

Use [cascading values and parameters](../components/cascading-values-and-parameters?view=aspnetcore-10.0) to manage state by flowing data from an ancestor Razor component to descendent components:

* To consume state across many components.
* If there's just one top-level state object to persist.

Root-level cascading values with a [CascadingValueSource\<TValue>](/en-us/dotnet/api/microsoft.aspnetcore.components.cascadingvaluesource-1) permit Razor component subscriber notifications of changed cascading values. For more information and a working example, see the `NotifyingDalek` example in [ASP.NET Core Blazor cascading values and parameters](../components/cascading-values-and-parameters?view=aspnetcore-10.0#root-level-cascading-values-with-notifications).

## Support state modifications from outside Blazor's synchronization context

When using a custom state management service where you want to support state modifications from outside Blazor's synchronization context (for example from a timer or a background service), all consuming components must wrap the [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) call in [ComponentBase.InvokeAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.invokeasync). This ensures the change notification is handled on the renderer's synchronization context.

When the state management service doesn't call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) on Blazor's synchronization context, the following error is thrown:

> System.InvalidOperationException: 'The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.'

For more information and an example of how to address this error, see [ASP.NET Core Razor component rendering](../components/rendering?view=aspnetcore-10.0#receiving-a-call-from-something-external-to-the-blazor-rendering-and-event-handling-system).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/authorize-with-a-specific-scheme
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/authorize-with-a-specific-scheme.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Authorize with a specific scheme in ASP.NET Core

For an introduction to authentication schemes, see [Overview of ASP.NET Core Authentication: Authentication scheme](../authentication/?view=aspnetcore-10.0#authentication-scheme).

In some scenarios, such as Single Page Applications (SPAs), it's common to use multiple authentication methods. For example, the app may use cookie-based authentication to sign a user into an app and establish their identity and Bearer authentication (often relying on JWTs) for JavaScript-based requests to web API endpoints. In some cases, the app may have multiple instances of an authentication handler. For example, an app has two cookie handlers, where one contains a basic identity and one is created when a multi-factor authentication (MFA) is triggered. MFA may be triggered because the user requested an operation that requires extra security.

For the following [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) call without a default authentication scheme specified, two authentication handlers are added to the app using their default authentication scheme names:

* Cookie (scheme name: "Cookies"): [AddCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie)
* JWT bearer (scheme name: "Bearer"): [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer)

```
builder.Services.AddAuthentication()
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Unauthorized/";
        options.AccessDeniedPath = "/Account/Forbidden/";
    })
    .AddJwtBearer(options =>
    {
        options.Audience = "http://localhost:5001/";
        options.Authority = "http://localhost:5000/";
    });
```

```
services.AddAuthentication()
    .AddCookie(options => {
        options.LoginPath = "/Account/Unauthorized/";
        options.AccessDeniedPath = "/Account/Forbidden/";
    })
    .AddJwtBearer(options => {
        options.Audience = "http://localhost:5001/";
        options.Authority = "http://localhost:5000/";
    });
```

Specifying the default scheme when calling [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) results in setting the [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user) property to a [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) that relies on that identity. If this behavior isn't desired, invoke the parameterless form of [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), as shown in the preceding example.

## JWT bearer NuGet package

Several examples in this article rely on API in the [`Microsoft.AspNetCore.Authentication.JwtBearer` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer). The package provides middleware that facilitates JSON Web Token (JWT) authentication, enabling secure authentication for APIs and web services.

## Select a scheme with an `[Authorize]` attribute

An app can specify an authentication handler for Razor components, Minimal API endpoints, controllers, action methods, Razor Pages, and [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel)s by passing a comma-delimited list of authentication schemes to the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute). The attribute specifies the authentication schemes regardless of whether or not a default scheme is configured. In the following example, the Cookies ([CookieAuthenticationDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme)) and Bearer ([JwtBearerDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerdefaults.authenticationscheme)) authentication schemes are set.

Note

The following examples require the following namespaces: [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization), [Microsoft.AspNetCore.Authentication.Cookies](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies), and [Microsoft.AspNetCore.Authentication.JwtBearer](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer).

For a Razor component:

```
@attribute [Authorize(AuthenticationSchemes = 
    CookieAuthenticationDefaults.AuthenticationScheme + "," + 
    JwtBearerDefaults.AuthenticationScheme)]
```

For a Minimal API endpoint, decorate the constructor with an [AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to set the schemes:

```
app.MapGet("/api/data", [Authorize(AuthenticationSchemes = 
    CookieAuthenticationDefaults.AuthenticationScheme + "," + 
    JwtBearerDefaults.AuthenticationScheme)] () =>
{
    ...
});
```

Alternatively, you can pass the schemes via a custom policy:

```
app.MapGet("/api/data", () =>
{
    ...
})
.RequireAuthorization(policy => 
    policy.AddAuthenticationSchemes(
        CookieAuthenticationDefaults.AuthenticationScheme + "," + 
        JwtBearerDefaults.AuthenticationScheme));
```

For an MVC controller:

```
[Authorize(AuthenticationSchemes = AuthSchemes)]
public class MixedAuthSchemesController : Controller
{
    private const string AuthSchemes = 
    CookieAuthenticationDefaults.AuthenticationScheme + "," + 
    JwtBearerDefaults.AuthenticationScheme;

    ...
}
```

For a [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class:

```
[Authorize(AuthenticationSchemes = 
    CookieAuthenticationDefaults.AuthenticationScheme + "," + 
    JwtBearerDefaults.AuthenticationScheme)]
public class MixedAuthSchemesModel : PageModel
{
    ...
}
```

Authorization middleware approves access with any of the specified schemes in the order listed. If both schemes authenticate the user (a valid cookie and a valid bearer token are present), authorization middleware merges the identities into a single [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) context.

By specifying a single scheme, the corresponding handler runs. In the following example, only the handler with the Bearer scheme runs, and any cookie-based identities are ignored for the endpoint:

```
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Authentication.JwtBearer
@attribute [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
```

## Select the scheme with an authorization policy

If you prefer to specify the desired schemes in a [policy](policies?view=aspnetcore-10.0), set the [AuthenticationSchemes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.authenticationschemes) collection when adding the policy.

In the following example, the `Over18` policy only runs against the identity created by the JWT bearer handler ([JwtBearerDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerdefaults.authenticationscheme)). For an example of the `MinimumAgeRequirement` class used in the following example, see [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0). The [RequireAuthenticatedUser](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireauthenticateduser) method enforces user authentication to endpoints where the policy is applied.

Note

The following example requires the [Microsoft.AspNetCore.Authentication.JwtBearer](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer) namespace.

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("Over18", policy =>
    {
        policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
        policy.RequireAuthenticatedUser();
        policy.Requirements.Add(new MinimumAgeRequirement(18));
    });
```

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Over18", policy =>
    {
        policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
        policy.RequireAuthenticatedUser();
        policy.Requirements.Add(new MinimumAgeRequirement(18));
    });
});
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("Over18", policy =>
    {
        policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
        policy.RequireAuthenticatedUser();
        policy.Requirements.Add(new MinimumAgeRequirement());
    });
});
```

Use the policy by setting [AuthorizeAttribute.Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy).

For a Razor component:

```
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "Over18")]
```

For a Minimal API endpoint, call [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) with the policy name:

```
app.MapGet("/api/data", () => 
{
    ...
})
.RequireAuthorization("Over18");
```

For a MVC controller:

```
[Authorize(Policy = "Over18")]
public class RegistrationController : Controller
```

For a [PageModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class:

```
[Authorize(Policy = "Over18")]
public class MixedAuthSchemesModel : PageModel
{
    ...
}
```

## `[Authorize]` attribute scheme and policy scheme interaction

The authorization schemes for an endpoint with one or more [`Authorize` attributes](#select-a-scheme-with-an-authorize-attribute) and one or more [policy-based schemes](#select-the-scheme-with-an-authorization-policy) are *combined* to set the final set of permitted schemes for the endpoint. This forms a union, and any listed scheme may authenticate the request. An attribute adding cookies to a policy restricted to Bearer authentication allows a cookie-only request, assuming the cookie creates a [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) meeting the policy requirements.

## Use multiple authentication schemes

Some apps require support for multiple methods of authentication. A typical scenario involves accepting bearer JWTs issued by several identity providers.

Only one JWT bearer handler is registered with the default authentication scheme [JwtBearerDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerdefaults.authenticationscheme). Register additional JWT bearer schemes for additional identity providers with unique authentication scheme names. The following example names the second scheme "`MEID`" for the ME-ID issuer.

Note

The following examples require the [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization) and [Microsoft.AspNetCore.Authentication.JwtBearer](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer) namespaces.

```
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Audience = "https://localhost:5000/";
        options.Authority = "https://localhost:5000/identity/";
    })
    .AddJwtBearer("MEID", options =>
    {
        options.Audience = "https://localhost:5000/";
        options.Authority = 
            "https://sts.windows.net/00001111-aaaa-2222-bbbb-3333cccc4444/";
    });
```

```
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Audience = "https://localhost:5000/";
        options.Authority = "https://localhost:5000/identity/";
    })
    .AddJwtBearer("MEID", options =>
    {
        options.Audience = "https://localhost:5000/";
        options.Authority = 
            "https://sts.windows.net/00001111-aaaa-2222-bbbb-3333cccc4444/";
    });
```

Update the default authorization policy to accept both authentication schemes:

```
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
    JwtBearerDefaults.AuthenticationScheme, "MEID");

defaultAuthorizationPolicyBuilder =
    defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();

builder.Services.AddAuthorizationBuilder()
    .SetDefaultPolicy(defaultAuthorizationPolicyBuilder.Build());
```

```
builder.Services.AddAuthorization(options =>
{
    var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
        JwtBearerDefaults.AuthenticationScheme, "MEID");

    defaultAuthorizationPolicyBuilder =
        defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();

    options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
```

```
services.AddAuthorization(options =>
{
    var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
        JwtBearerDefaults.AuthenticationScheme, "MEID");

    defaultAuthorizationPolicyBuilder = 
        defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();

    options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
```

The preceding code configures default authorization with support for multiple authentication schemes:

1. A new [AuthorizationPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder) initializes a policy builder that accepts authentication from two schemes:

   * [JwtBearerDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerdefaults.authenticationscheme) (JWT bearer tokens)
   * "MEID" (the custom authentication scheme for ME-ID, defined earlier)

   This means users can authenticate using either JWT tokens or the MEID scheme

2. [RequireAuthenticatedUser](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireauthenticateduser) is called to require authenticated users for access to protected endpoints.

3. [SetDefaultPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder.setdefaultpolicy) chained to [AddAuthorizationBuilder](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.policyservicecollectionextensions.addauthorizationbuilder):

   * Registers authorization services.
   * Sets this policy as the default for all `[Authorize]` attributes that don't specify a custom policy. Any endpoint marked with `[Authorize]` automatically uses this policy

The result of using the preceding API is that protected endpoints in the app require authentication via either JWT bearer tokens or the MEID scheme, providing flexibility in how users authenticate.

## Select a policy scheme based on the `Authorization` header

For guidance on how to use the [AddPolicyScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder.addpolicyscheme) method with the [ForwardDefaultSelector](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationschemeoptions.forwarddefaultselector#microsoft-aspnetcore-authentication-authenticationschemeoptions-forwarddefaultselector) property to dynamically select an authentication scheme for each request, see [Policy schemes in ASP.NET Core](../authentication/policyschemes?view=aspnetcore-10.0).

## Related content

* [Multi-factor authentication in ASP.NET Core](../authentication/mfa?view=aspnetcore-10.0)
* [Protect section with MFA (`dotnet/AspNetCore.Docs` #15791)](https://github.com/dotnet/AspNetCore.Docs/issues/15791#issuecomment-580464195)
* [Multiple jwt authentication schemes can't validate signature key (`dotnet/aspnetcore` #26002)](https://github.com/dotnet/aspnetcore/issues/26002)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Technology specific guidance

* [Authentication](authentication?view=aspnetcore-10.0)
* [ClaimsPrincipal.Current](claimsprincipal-current?view=aspnetcore-10.0)
* [HttpApplicationHost](hosting?view=aspnetcore-10.0)
* [HttpContext](http-context?view=aspnetcore-10.0)
* [HTTP Handlers](http-handlers?view=aspnetcore-10.0)
* [HTTP Modules](http-modules?view=aspnetcore-10.0)
* [Machine Key](machine-key?view=aspnetcore-10.0)
* [Membership](membership?view=aspnetcore-10.0)
* [Miscellaneous](misc?view=aspnetcore-10.0)
* [OWIN](owin?view=aspnetcore-10.0)
* [Session State](session?view=aspnetcore-10.0)
* [WebAPI](webapi?view=aspnetcore-10.0)

## Getting help

For complex migration scenarios not covered in these guides:

* Review the [ASP.NET Core migration overview](../?view=aspnetcore-10.0)
* Consult the [ASP.NET Core fundamentals documentation](../../../fundamentals/?view=aspnetcore-10.0)
* Engage with the community through [GitHub discussions](https://github.com/dotnet/aspnetcore/discussions)
* Consider professional migration services for mission-critical applications

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/forms/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor forms overview

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to use forms in Blazor.

## Input components and forms

The Blazor framework supports forms and provides built-in input components:

* Bound to an object or model that can use [data annotations](../../mvc/models/validation?view=aspnetcore-10.0).

  * HTML forms with the `<form>` element.
  * [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) components.

* [Built-in input components](input-components?view=aspnetcore-10.0).

- An [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component bound to an object or model that can use [data annotations](../../mvc/models/validation?view=aspnetcore-10.0).
- [Built-in input components](input-components?view=aspnetcore-10.0).

In Blazor Web Apps that use static server-side rendering (static SSR), input components automatically participate in client-side validation when the form contains a [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component. For details, see [ASP.NET Core Blazor client-side form validation in static SSR](validation-client-side?view=aspnetcore-10.0).

Note

Unsupported ASP.NET Core validation features are covered in the [Unsupported validation features](#unsupported-validation-features) section.

The [Microsoft.AspNetCore.Components.Forms](/en-us/dotnet/api/microsoft.aspnetcore.components.forms) namespace provides:

* Classes for managing form elements, state, and validation.
* Access to built-in Input\* components.

A project created from the Blazor project template includes the namespace in the app's imports file (`_Imports.razor`), which makes the namespace available to the app's Razor components.

Standard HTML forms are supported. Create a form using the normal HTML `<form>` tag and specify an `@onsubmit` handler for handling the submitted form request.

`StarshipPlainForm.razor`:

```
@page "/starship-plain-form"
@inject ILogger<StarshipPlainForm> Logger

<form method="post" @onsubmit="Submit" @formname="starship-plain-form">
    <AntiforgeryToken />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</form>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Id = {Id}", Model?.Id);

    public class Starship
    {
        public string? Id { get; set; }
    }
}
```

```
@page "/starship-plain-form"
@inject ILogger<StarshipPlainForm> Logger

<form method="post" @onsubmit="Submit" @formname="starship-plain-form">
    <AntiforgeryToken />
    <div>
        <label>
            Identifier: 
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</form>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Id = {Id}", Model?.Id);

    public class Starship
    {
        public string? Id { get; set; }
    }
}
```

In the preceding `StarshipPlainForm` component:

* The form is rendered where the `<form>` element appears. The form is named with the [`@formname`](../../mvc/views/razor?view=aspnetcore-10.0#formname) directive attribute, which uniquely identifies the form to the Blazor framework.
* The model is created in the component's `@code` block and held in a `public` property (`Model`). The `[SupplyParameterFromForm]` attribute indicates that the value of the associated property should be supplied from the form data. Data in the request that matches the property's name is bound to the property.
* The [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext) component is an input component for editing string values. The `@bind-Value` directive attribute binds the `Model.Id` model property to the [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext) component's [Value](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1.value) property.
* The `Submit` method is registered as a handler for the `@onsubmit` callback. The handler is called when the form is submitted by the user.

Important

Always use the [`@formname`](../../mvc/views/razor?view=aspnetcore-10.0#formname) directive attribute with a unique form name.

Blazor enhances page navigation and form handling by intercepting the request in order to apply the response to the existing DOM, preserving as much of the rendered form as possible. The enhancement avoids the need to fully load the page and provides a much smoother user experience, similar to a single-page app (SPA), although the component is rendered on the server. For more information, see [ASP.NET Core Blazor navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling).

[Streaming rendering](../components/rendering?view=aspnetcore-10.0#streaming-rendering) is supported for plain HTML forms. Note that when `POST`ing a form, only DOM updates inside the form's handlers are streamed (for example, `@onsubmit`). Updates inside `OnInitializedAsync` are only streamed for `GET` requests. For more information, see [Allow streaming the loading phase of POST responses (`dotnet/aspnetcore` #50994)](https://github.com/dotnet/aspnetcore/issues/50994).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

The preceding example includes antiforgery support by including an [AntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerytoken) component in the form. Antiforgery support is explained further in the [Antiforgery support](#antiforgery-support) section of this article.

To submit a form based on another element's DOM events, for example `oninput` or `onblur`, use JavaScript to submit the form ([`submit`](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/submit)).

Instead of using plain forms in Blazor apps, a form is typically defined with Blazor's built-in form support using the framework's [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component. The following Razor component demonstrates typical elements, components, and Razor code to render a webform using an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component.

A form is defined using the Blazor framework's [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component. The following Razor component demonstrates typical elements, components, and Razor code to render a webform using an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component.

`Starship1.razor`:

```
@page "/starship-1"
@inject ILogger<Starship1> Logger

<EditForm Model="Model" OnSubmit="Submit" FormName="Starship1">
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Id = {Id}", Model?.Id);

    public class Starship
    {
        public string? Id { get; set; }
    }
}
```

```
@page "/starship-1"
@inject ILogger<Starship1> Logger

<EditForm Model="Model" OnSubmit="Submit" FormName="Starship1">
    <div>
        <label>
            Identifier:
            <InputText @bind-Value="Model!.Id" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Id = {Id}", Model?.Id);

    public class Starship
    {
        public string? Id { get; set; }
    }
}
```

In the preceding `Starship1` component:

* The [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component is rendered where the `<EditForm>` element appears. The form is named with the [FormName](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.formname#microsoft-aspnetcore-components-forms-editform-formname) property, which uniquely identifies the form to the Blazor framework.
* The model is created in the component's `@code` block and held in a `public` property (`Model`). The property is assigned to the [EditForm.Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model#microsoft-aspnetcore-components-forms-editform-model) parameter. The `[SupplyParameterFromForm]` attribute indicates that the value of the associated property should be supplied from the form data. Data in the request that matches the property's name is bound to the property.
* The [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext) component is an [input component](input-components?view=aspnetcore-10.0) for editing string values. The `@bind-Value` directive attribute binds the `Model.Id` model property to the [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext) component's [Value](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1.value) property.
* The `Submit` method is registered as a handler for the [OnSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onsubmit#microsoft-aspnetcore-components-forms-editform-onsubmit) callback. The handler is called when the form is submitted by the user.

Important

Always use the [FormName](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.formname#microsoft-aspnetcore-components-forms-editform-formname) property with a unique form name.

Blazor enhances page navigation and form handling for [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) components. For more information, see [ASP.NET Core Blazor navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling).

[Streaming rendering](../components/rendering?view=aspnetcore-10.0#streaming-rendering) is supported for [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform). Note that when `POST`ing a form, only DOM updates inside the form's handlers are streamed (for example, `OnValidSubmit`). Updates inside `OnInitializedAsync` are only streamed for `GET` requests. For more information, see [Allow streaming the loading phase of POST responses (`dotnet/aspnetcore` #50994)](https://github.com/dotnet/aspnetcore/issues/50994).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

```
@page "/starship-1"
@inject ILogger<Starship1> Logger

<EditForm Model="Model" OnSubmit="Submit">
    <InputText @bind-Value="Model!.Id" />
    <button type="submit">Submit</button>
</EditForm>

@code {
    public Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit()
    {
        Logger.LogInformation("Model.Id = {Id}", Model?.Id);
    }

    public class Starship
    {
        public string? Id { get; set; }
    }
}
```

In the preceding `Starship1` component:

* The [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) component is rendered where the `<EditForm>` element appears.
* The model is created in the component's `@code` block and held in a private field (`model`). The field is assigned to the [EditForm.Model](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model#microsoft-aspnetcore-components-forms-editform-model) parameter.
* The [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext) component is an input component for editing string values. The `@bind-Value` directive attribute binds the `Model.Id` model property to the [InputText](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputtext) component's [Value](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.inputbase-1.value) property†.
* The `Submit` method is registered as a handler for the [OnSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onsubmit#microsoft-aspnetcore-components-forms-editform-onsubmit) callback. The handler is called when the form is submitted by the user.

†For more information on property binding, see [ASP.NET Core Blazor data binding](../components/data-binding?view=aspnetcore-10.0#binding-with-component-parameters).

In the next example, the preceding component is modified to create the form in the `Starship2` component:

* [OnSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onsubmit#microsoft-aspnetcore-components-forms-editform-onsubmit) is replaced with [OnValidSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onvalidsubmit#microsoft-aspnetcore-components-forms-editform-onvalidsubmit), which processes assigned event handler if the form is valid when submitted by the user.

* A [ValidationSummary](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationsummary) component is added to display validation messages when the form is invalid on form submission.

* The data annotations validator ([DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component†) attaches validation support using data annotations:

  * If the `<input>` form field is left blank when the **`Submit`** button is selected, an error appears in the validation summary ([ValidationSummary](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationsummary) component‡) ("`The Id field is required.`") and `Submit` is **not** called.
  * If the `<input>` form field contains more than ten characters when the **`Submit`** button is selected, an error appears in the validation summary ("`Id is too long.`"). `Submit` is **not** called.
  * If the `<input>` form field contains a valid value when the **`Submit`** button is selected, `Submit` is called.

†The [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component is covered in the [Data Annotations Validator component and custom validation](validation?view=aspnetcore-10.0#data-annotations-validator-component-and-custom-validation) section. ‡The [ValidationSummary](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.validationsummary) component is covered in the [Validation Summary and Validation Message components](validation?view=aspnetcore-10.0#validation-summary-and-validation-message-components) section.

`Starship2.razor`:

```
@page "/starship-2"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship2> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship2">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <label>
        Identifier: 
        <InputText @bind-Value="Model!.Id" />
    </label>
    <button type="submit">Submit</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Id = {Id}", Model?.Id);

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
```

```
@page "/starship-2"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship2> Logger

<EditForm Model="Model" OnValidSubmit="Submit" FormName="Starship2">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <label>
        Identifier: 
        <InputText @bind-Value="Model!.Id" />
    </label>
    <button type="submit">Submit</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() => Logger.LogInformation("Id = {Id}", Model?.Id);

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
```

```
@page "/starship-2"
@using System.ComponentModel.DataAnnotations
@inject ILogger<Starship2> Logger

<EditForm Model="Model" OnValidSubmit="Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <InputText @bind-Value="Model!.Id" />
    <button type="submit">Submit</button>
</EditForm>

@code {
    public Starship? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit()
    {
        Logger.LogInformation("Id = {Id}", Model?.Id);
    }

    public class Starship
    {
        [Required]
        [StringLength(10, ErrorMessage = "Id is too long.")]
        public string? Id { get; set; }
    }
}
```

## Handle form submission

The [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) provides the following callbacks for handling form submission:

* Use [OnValidSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onvalidsubmit#microsoft-aspnetcore-components-forms-editform-onvalidsubmit) to assign an event handler to run when a form with valid fields is submitted.
* Use [OnInvalidSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.oninvalidsubmit#microsoft-aspnetcore-components-forms-editform-oninvalidsubmit) to assign an event handler to run when a form with invalid fields is submitted.
* Use [OnSubmit](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.onsubmit#microsoft-aspnetcore-components-forms-editform-onsubmit) to assign an event handler to run regardless of the form fields' validation status. The form is validated by calling [EditContext.Validate](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.validate) in the event handler method. If [Validate](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editcontext.validate) returns `true`, the form is valid.

During static server-side rendering (static SSR), when a `<form>` or [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) doesn't specify an explicit `action` attribute, Blazor automatically renders an `action` attribute set to the current request's root-relative path and query string, which is derived from [NavigationManager.Uri](/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.uri#microsoft-aspnetcore-components-navigationmanager-uri).

## Clear a form or field

Reset a form by clearing its model back to its default state, which can be performed inside or outside of an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform)'s markup:

```
<button @onclick="ClearForm">Clear form</button>

...

private void ClearForm() => Model = new();
```

Alternatively, use an explicit Razor expression:

```
<button @onclick="@(() => Model = new())">Clear form</button>
```

Reset a field by clearing its model value back to its default state:

```
<button @onclick="ResetId">Reset Identifier</button>

...

private void ResetId() => Model!.Id = string.Empty;
```

Alternatively, use an explicit Razor expression:

```
<button @onclick="@(() => Model!.Id = string.Empty)">Reset Identifier</button>
```

There's no need to call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) in the preceding examples because [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) is automatically called by the Blazor framework to rerender the component after an event handler is invoked. If an event handler isn't used to invoke the methods that clear a form or field, then developer code should call [StateHasChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.statehaschanged) to rerender the component.

## Antiforgery support

Antiforgery services are automatically added to Blazor apps when [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) is called in the `Program` file.

Automatic Cross-Site Request Forgery (CSRF) protection middleware is enabled by default in apps built with `WebApplication.CreateBuilder`. The middleware inspects the `Sec-Fetch-Site` and `Origin` headers on unsafe HTTP methods and records a validation verdict on the request. Blazor server-side rendering (SSR) form posts enforce that verdict and return `400 Bad Request` for cross-origin form posts that aren't trusted.

Token-based antiforgery services are added to the app when [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) is called in the `Program` file. However, token validation only runs when antiforgery middleware is explicitly added to the request processing pipeline by calling [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery).

Adding token-based antiforgery middleware doesn't replace the automatic header-based CSRF protection middleware. When an app calls [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery), both defense mechanisms run for a form post:

* The header-based CSRF protection middleware runs first and records its verdict.
* Antiforgery middleware performs token-based validation.

The token-based result from antiforgery middleware is authoritative and overrides the earlier header-based CSRF middleware verdict.

To explicitly add antiforgery middleware, call [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) after the call to [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting). If there are calls to [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints), the call to [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) must go between them. A call to [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) must be placed after calls to [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization).

To disable the automatic header-based CSRF protection middleware, set the `DisableCsrfProtection` configuration key to true. For example, use the app settings file (`appsettings.json`) to disable the middleware:

```
{
  "DisableCsrfProtection": true
}
```

The `DisableCsrfProtection` configuration setting can be supplied by any configuration source, including via an environment variable (`DisableCsrfProtection=true`).

Warning

Disabling the automatic CSRF protection middleware removes the default header-based (`Sec-Fetch-Site`/`Origin`) protection for the entire app. Only disable it if you provide an alternative CSRF defense, such as explicitly adopting token-based antiforgery middleware by calling [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery).

For more information, see [Automatic CSRF protection](../../security/anti-request-forgery?view=aspnetcore-10.0#automatic-csrf-protection-in-aspnet-core).

Important

The following guidance on the [AntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerytoken) component and the [AntiforgeryStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerystateprovider) service only apply to an app that explicitly adopts token-based antiforgery middleware by calling [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) in its request processing pipeline.

The app uses antiforgery middleware by calling [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) in its request processing pipeline in the `Program` file. [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) is called after the call to [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting). If there are calls to [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) and [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints), the call to [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) must go between them. A call to [UseAntiforgery](/en-us/dotnet/api/microsoft.aspnetcore.builder.antiforgeryapplicationbuilderextensions.useantiforgery) must be placed after calls to [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization).

The [AntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerytoken) component renders an antiforgery token as a hidden field, and the `[RequireAntiforgeryToken]` attribute enables antiforgery protection. If an antiforgery check fails, a [`400 - Bad Request`](https://developer.mozilla.org/docs/Web/HTTP/Status/400) response is thrown and the form isn't processed.

For forms based on [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform), the [AntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerytoken) component and `[RequireAntiforgeryToken]` attribute are automatically added to provide antiforgery protection.

For forms based on the HTML `<form>` element, manually add the [AntiforgeryToken](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.antiforgerytoken) component to the form:

```
<form method="post" @onsubmit="Submit" @formname="starshipForm">
    <AntiforgeryToken />
    <input id="send" type="submit" value="Send" />
</form>

@if (submitted)
{
    <p>Form submitted!</p>
}

@code{
    private bool submitted = false;

    private void Submit() => submitted = true;
}
```

Warning

For forms based on either [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) or the HTML `<form>` element, antiforgery protection can be disabled by passing `required: false` to the `[RequireAntiforgeryToken]` attribute. The following example disables antiforgery and is ***not recommended*** for public apps:

```
@using Microsoft.AspNetCore.Antiforgery
@attribute [RequireAntiforgeryToken(required: false)]
```

For more information, see [ASP.NET Core Blazor authentication and authorization](../security/?view=aspnetcore-10.0#antiforgery-support).

## Mitigate overposting attacks

Statically-rendered server-side forms, such as those typically used in components that create and edit records in a database with a form model, can be vulnerable to an *overposting* attack, also known as a *mass assignment* attack. An overposting attack occurs when a malicious user issues an HTML form POST to the server that processes data for properties that aren't part of the rendered form and that the developer doesn't wish to allow users to modify. The term "overposting" literally means that the malicious user has *over*-POSTed with the form.

Overposting isn't a concern when the model doesn't include restricted properties for create and update operations. However, it's important to keep overposting in mind when working with static SSR-based Blazor forms that you maintain.

To mitigate overposting, we recommend using a separate view model/data transfer object (DTO) for the form and database with create (insert) and update operations. When the form is submitted, only properties of the view model/DTO are used by the component and C# code to modify the database. Any extra data included by a malicious user is discarded, so the malicious user is prevented from conducting an overposting attack.

## Enhanced form handling

[Enhance navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling) for form POST requests with the [Enhance](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.enhance) parameter for [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform) forms or the `data-enhance` attribute for HTML forms (`<form>`):

```
<EditForm ... Enhance ...>
    ...
</EditForm>
```

```
<form ... data-enhance ...>
    ...
</form>
```

❌Unsupported: You can't set enhanced navigation on a form's ancestor element to enable enhanced form handling.

```
<div ... data-enhance ...>
    <form ...>
        <!-- NOT enhanced -->
    </form>
</div>
```

Enhanced form posts only work with Blazor endpoints. Posting an enhanced form to non-Blazor endpoint results in an error.

To disable enhanced form handling:

* For an [EditForm](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform), remove the [Enhance](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.enhance) parameter from the form element (or set it to `false`: `Enhance="false"`).
* For an HTML `<form>`, remove the `data-enhance` attribute from form element (or set it to `false`: `data-enhance="false"`).

Blazor's enhanced navigation and form handling may undo dynamic changes to the DOM if the updated content isn't part of the server rendering. To preserve the content of an element, use the `data-permanent` attribute.

In the following example, the content of the `<div>` element is updated dynamically by a script when the page loads:

```
<div data-permanent>
    ...
</div>
```

To disable enhanced navigation and form handling globally, see [ASP.NET Core Blazor startup](../fundamentals/startup?view=aspnetcore-10.0#enhanced-navigation-and-form-handling).

For guidance on using the `enhancedload` event to listen for enhanced page updates, see [ASP.NET Core Blazor navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling).

## Examples

Examples don't adopt enhanced form handling for form POST requests, but all of the examples can be updated to adopt the enhanced features by following the guidance in the [Enhanced form handling](#enhanced-form-handling) section.

Examples use the [target-typed `new` operator](/en-us/dotnet/csharp/language-reference/operators/new-operator#target-typed-new), which was introduced with C# 9 and .NET 5. In the following example, the type isn't explicitly stated for the `new` operator:

```
public ShipDescription ShipDescription { get; set; } = new();
```

If using C# 8 or earlier (ASP.NET Core 3.1), modify the example code to state the type to the `new` operator:

```
public ShipDescription ShipDescription { get; set; } = new ShipDescription();
```

Components use nullable reference types (NRTs), and the .NET compiler performs null-state static analysis, both of which are supported in .NET 6 or later. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../../migration/50-to-60?view=aspnetcore-10.0#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis).

If using C# 9 or earlier (.NET 5 or earlier), remove the NRTs from the examples. Usually, this merely involves removing the question marks (`?`) and exclamation points (`!`) from the types in the example code.

The .NET SDK applies implicit global `using` directives to projects when targeting .NET 6 or later. The examples use a logger to log information about form processing, but it isn't necessary to specify an `@using` directive for the [Microsoft.Extensions.Logging](/en-us/dotnet/api/microsoft.extensions.logging) namespace in the component examples. For more information, see [.NET project SDKs: Implicit using directives](/en-us/dotnet/core/project-sdk/overview#implicit-using-directives).

If using C# 9 or earlier (.NET 5 or earlier), add `@using` directives to the top of the component after the `@page` directive for any API required by the example. Find API namespaces through Visual Studio (right-click the object and select **Peek Definition**) or the [.NET API browser](/en-us/dotnet/api/).

To demonstrate how forms work with [data annotations](../../mvc/models/validation?view=aspnetcore-10.0) validation, example components rely on [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) API. If you wish to avoid an extra line of code in components that use data annotations, make the namespace available throughout the app's components with the imports file (`_Imports.razor`):

```
@using System.ComponentModel.DataAnnotations
```

Form examples reference aspects of the [Star Trek](http://www.startrek.com/) universe. Star Trek is a copyright ©1966-2023 of [CBS Studios](https://www.paramount.com/brand/cbs-studios) and [Paramount](https://www.paramount.com).

## Client-side validation requires a circuit

In Blazor Web Apps, client-side validation requires an active Blazor SignalR circuit. Client-side validation isn't available to forms in components that have adopted static server-side rendering (static SSR). Forms that adopt static SSR are validated on the server after the form is submitted.

## Client-side validation in static SSR forms

In Blazor Web Apps, forms in components that adopt static server-side rendering (static SSR) gain client-side validation automatically when a [DataAnnotationsValidator](/en-us/dotnet/api/microsoft.aspnetcore.components.forms.dataannotationsvalidator) component is present in the form. For details, see [ASP.NET Core Blazor client-side form validation in static SSR](validation-client-side?view=aspnetcore-10.0).

## Unsupported validation features

All of the [data annotation built-in validators](../../mvc/models/validation?view=aspnetcore-10.0#built-in-attributes) are supported in Blazor except for the [`[Remote]` validation attribute](../../mvc/models/validation?view=aspnetcore-10.0#remote-attribute).

jQuery validation isn't supported in Razor components. We recommend any of the following approaches:

* Follow the guidance in [ASP.NET Core Blazor forms validation](validation?view=aspnetcore-10.0) for any of the following scenarios:

  * Server-side validation in a Blazor Web App that adopts an interactive render mode.
  * Client-side validation in [static SSR forms](validation-client-side?view=aspnetcore-10.0).
  * Client-side validation in a standalone Blazor WebAssembly app.

* Use native HTML validation attributes (see [Client-side form validation](https://developer.mozilla.org/docs/Learn/Forms/Form_validation)).

* Adopt a third-party validation JavaScript library.

- Follow the guidance in [ASP.NET Core Blazor forms validation](validation?view=aspnetcore-10.0) for either:

  * Server-side validation in a Blazor Web App that adopts an interactive render mode.
  * Client-side validation in a standalone Blazor WebAssembly app.

- Use native HTML validation attributes (see [Client-side form validation](https://developer.mozilla.org/docs/Learn/Forms/Form_validation)).

- Adopt a third-party validation JavaScript library.

## Related content

* [ASP.NET Core Blazor file uploads](../file-uploads?view=aspnetcore-10.0)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))
* [ASP.NET Core GitHub repository (`dotnet/aspnetcore`) forms test assets](https://github.com/dotnet/aspnetcore/tree/main/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms)

- [ASP.NET Core Blazor file uploads](../file-uploads?view=aspnetcore-10.0)
- [Secure a hosted ASP.NET Core Blazor WebAssembly app with Microsoft Entra ID](../security/webassembly/hosted-with-microsoft-entra-id?view=aspnetcore-10.0)
- [Secure a hosted ASP.NET Core Blazor WebAssembly app with Azure Active Directory B2C](../security/webassembly/hosted-with-azure-active-directory-b2c?view=aspnetcore-10.0)
- [Secure a hosted ASP.NET Core Blazor WebAssembly app with Identity Server](../security/webassembly/hosted-with-identity-server?view=aspnetcore-10.0)
- [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))
- [ASP.NET Core GitHub repository (`dotnet/aspnetcore`) forms test assets](https://github.com/dotnet/aspnetcore/tree/main/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-8.0.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# What's new in ASP.NET Core in .NET 8

This article highlights the most significant changes in ASP.NET Core in .NET 8 with links to relevant documentation.

## Blazor

### Full-stack web UI

With the release of .NET 8, Blazor is a full-stack web UI framework for developing apps that render content at either the component or page level with:

* Static Server rendering (also called *static server-side rendering*, static SSR) to generate static HTML on the server.
* Interactive Server rendering (also called *interactive server-side rendering*, interactive SSR) to generate interactive components with prerendering on the server.
* Interactive WebAssembly rendering (also called *client-side rendering*, CSR, which is always assumed to be interactive) to generate interactive components on the client with prerendering on the server.
* Interactive Auto (automatic) rendering to initially use the server-side ASP.NET Core runtime for content rendering and interactivity. The .NET WebAssembly runtime on the client is used for subsequent rendering and interactivity after the Blazor bundle is downloaded and the WebAssembly runtime activates. Interactive Auto rendering usually provides the fastest app startup experience.

Interactive render modes also prerender content by default.

For more information, see the following articles:

* [ASP.NET Core Blazor fundamentals](../blazor/fundamentals/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0): New sections on rendering and static/interactive concepts appear at the top of the article.
* [ASP.NET Core Blazor render modes](../blazor/components/render-modes?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0)
* **Migration coverage**: [Migrate from ASP.NET Core in .NET 7 to .NET 8](../migration/70-to-80?view=aspnetcore-10.0#blazor)

Examples throughout the Blazor documentation have been updated for use in Blazor Web Apps. Blazor Server examples remain in content versioned for .NET 7 or earlier.

### New article on class libraries with static server-side rendering (static SSR)

We've added a new article that discusses component library authorship in Razor class libraries (RCLs) with static server-side rendering (static SSR).

For more information, see [ASP.NET Core Razor class libraries (RCLs) with static server-side rendering (static SSR)](../blazor/components/class-libraries-and-static-server-side-rendering?view=aspnetcore-10.0).

### New article on HTTP caching issues

We've added a new article that discusses some of the common HTTP caching issues that can occur when upgrading Blazor apps across major versions and how to address HTTP caching issues.

For more information, see [Avoid HTTP caching issues when upgrading ASP.NET Core Blazor apps](../blazor/host-and-deploy/webassembly/http-caching-issues?view=aspnetcore-10.0).

### New Blazor Web App template

We've introduced a new Blazor project template: the *Blazor Web App* template. The new template provides a single starting point for using Blazor components to build any style of web UI. The template combines the strengths of the existing Blazor Server and Blazor WebAssembly hosting models with the new Blazor capabilities added in .NET 8: static server-side rendering (static SSR), streaming rendering, enhanced navigation and form handling, and the ability to add interactivity using either Blazor Server or Blazor WebAssembly on a per-component basis.

As part of unifying the various Blazor hosting models into a single model in .NET 8, we're also consolidating the number of Blazor project templates. We removed the Blazor Server template, and the ASP.NET Core Hosted option has been removed from the Blazor WebAssembly template. Both of these scenarios are represented by options when using the Blazor Web App template.

Note

Existing Blazor Server and Blazor WebAssembly apps remain supported in .NET 8. Optionally, these apps can be updated to use the new full-stack web UI Blazor features.

For more information on the new Blazor Web App template, see the following articles:

* [Tooling for ASP.NET Core Blazor](../blazor/tooling?view=aspnetcore-10.0\&pivots=windows\&preserve-view=true\&view=aspnetcore-8.0)
* [ASP.NET Core Blazor project structure](../blazor/project-structure?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0)

### New JS initializers for Blazor Web Apps

For Blazor Server, Blazor WebAssembly, and Blazor Hybrid apps:

* `beforeStart` is used for tasks such as customizing the loading process, logging level, and other options.
* `afterStarted` is used for tasks such as registering Blazor event listeners and custom event types.

The preceding legacy JS initializers aren't invoked by default in a Blazor Web App. For Blazor Web Apps, a new set of JS initializers are used: `beforeWebStart`, `afterWebStarted`, `beforeServerStart`, `afterServerStarted`, `beforeWebAssemblyStart`, and `afterWebAssemblyStarted`.

For more information, see [ASP.NET Core Blazor startup](../blazor/fundamentals/startup?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#javascript-initializers).

### Split of prerendering and integration guidance

For prior releases of .NET, we covered prerendering and integration in a single article. To simplify and focus our coverage, we've split the subjects into the following new articles, which have been updated for .NET 8:

* [Prerender ASP.NET Core Razor components](../blazor/components/prerender?view=aspnetcore-10.0\&pivots=windows\&preserve-view=true\&view=aspnetcore-8.0)
* [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../blazor/components/integration?view=aspnetcore-10.0\&pivots=windows\&preserve-view=true\&view=aspnetcore-8.0)

### Persist component state in a Blazor Web App

You can persist and read component state in a Blazor Web App using the existing [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service. This is useful for [persisting component state during prerendering](../blazor/state-management/prerendered-state-persistence?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#persist-prerendered-state).

Blazor Web Apps automatically persist any registered app-level state created during prerendering, removing the need for the [Persist Component State Tag Helper](../mvc/views/tag-helpers/built-in/persist-component-state?view=aspnetcore-10.0).

### Form handling and model binding

Blazor components can now handle submitted form requests, including model binding and validating the request data. Components can implement forms with separate form handlers using the standard HTML `<form>` tag or using the existing `EditForm` component.

Form model binding in Blazor honors the data contract attributes (for example, `[DataMember]` and `[IgnoreDataMember]`) for customizing how the form data is bound to the model.

New antiforgery support is included in .NET 8. A new `AntiforgeryToken` component renders an antiforgery token as a hidden field, and the new `[RequireAntiforgeryToken]` attribute enables antiforgery protection. If an antiforgery check fails, a 400 (Bad Request) response is returned without form processing. The new antiforgery features are enabled by default for forms based on `Editform` and can be applied manually to standard HTML forms.

For more information, see [ASP.NET Core Blazor forms overview](../blazor/forms/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0).

### Enhanced navigation and form handling

Static server-side rendering (static SSR) typically performs a full page refresh whenever the user navigates to a new page or submits a form. In .NET 8, Blazor can enhance page navigation and form handling by intercepting the request and performing a fetch request instead. Blazor then handles the rendered response content by patching it into the browser DOM. Enhanced navigation and form handling avoids the need for a full page refresh and preserves more of the page state, so pages load faster and more smoothly. Enhanced navigation is enabled by default when the Blazor script (`blazor.web.js`) is loaded. Enhanced form handling can be optionally enabled for specific forms.

New enhanced navigation API allows you to refresh the current page by calling `NavigationManager.Refresh(bool forceLoad = false)`.

For more information, see the following guidance:

* [Enhanced navigation and form handling](../blazor/fundamentals/routing?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#enhanced-navigation-and-form-handling)
* [Location changes](../blazor/fundamentals/navigation?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#location-changes)

### New article on static rendering with enhanced navigation for JS interop

Some apps depend on JS interop to perform initialization tasks that are specific to each page. When using Blazor's enhanced navigation feature with statically-rendered pages that perform JS interop initialization tasks, page-specific JS may not be executed again as expected each time an enhanced page navigation occurs. A new article explains how to address this scenario in Blazor Web Apps:

[ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)](../blazor/javascript-interoperability/static-server-rendering?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0)

### Streaming rendering

You can now stream content updates on the response stream when using static server-side rendering (static SSR) with Blazor. Streaming rendering can improve the user experience for pages that perform long-running asynchronous tasks in order to fully render by rendering content as soon as it's available.

For example, to render a page you might need to make a long running database query or an API call. Normally, asynchronous tasks executed as part of rendering a page must complete before the rendered response is sent, which can delay loading the page. Streaming rendering initially renders the entire page with placeholder content while asynchronous operations execute. After the asynchronous operations are complete, the updated content is sent to the client on the same response connection and patched by into the DOM. The benefit of this approach is that the main layout of the app renders as quickly as possible and the page is updated as soon as the content is ready.

For more information, see [ASP.NET Core Razor component rendering](../blazor/components/rendering?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#streaming-rendering).

### Inject keyed services into components

Blazor now supports injecting keyed services using the `[Inject]` attribute. Keys allow for scoping of registration and consumption of services when using dependency injection. Use the new `InjectAttribute.Key` property to specify the key for the service to inject:

```
[Inject(Key = "my-service")]
public IMyService MyService { get; set; }
```

The `@inject` Razor directive doesn't support keyed services for this release, but work is tracked by [Update `@inject` to support keyed services (dotnet/razor #9286)](https://github.com/dotnet/razor/issues/9286) for a future .NET release.

For more information, see [ASP.NET Core Blazor dependency injection](../blazor/fundamentals/dependency-injection?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#inject-keyed-services-into-components).

### Access `HttpContext` as a cascading parameter

You can now access the current [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) as a cascading parameter from a static server component:

```
[CascadingParameter]
public HttpContext? HttpContext { get; set; }
```

Accessing the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) from a static server component might be useful for inspecting and modifying headers or other properties.

For an example that passes [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) state, access and refresh tokens, to components, see [ASP.NET Core Blazor additional server-side security scenarios](../blazor/security/additional-scenarios?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#pass-tokens-to-a-server-side-blazor-app).

### Render Razor components outside of ASP.NET Core

You can now render Razor components outside the context of an HTTP request. You can render Razor components as HTML directly to a string or stream independently of the ASP.NET Core hosting environment. This is convenient for scenarios where you want to generate HTML fragments, such as for a generating email or static site content.

For more information, see [Render Razor components outside of ASP.NET Core](../blazor/components/render-components-outside-of-aspnetcore?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0).

### Sections support

The new `SectionOutlet` and `SectionContent` components in Blazor add support for specifying outlets for content that can be filled in later. Sections are often used to define placeholders in layouts that are then filled in by specific pages. Sections are referenced either by a unique name or using a unique object ID.

For more information, see [ASP.NET Core Blazor sections](../blazor/components/sections?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0).

### Error page support

Blazor Web Apps can define a custom error page for use with the [ASP.NET Core exception handling middleware](../fundamentals/error-handling?view=aspnetcore-10.0#exception-handler-page). The Blazor Web App project template includes a default error page with similar content to the one used in MVC and Razor Pages apps. When the error page is rendered in response to a request from exception handling middleware, the error page always renders as a static server component, even if interactivity is otherwise enabled.

See the `Error` component (`Components/Pages/Error.razor`) of the server project in the [Blazor Web App project template (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### QuickGrid

The Blazor QuickGrid component is no longer experimental and is now part of the Blazor framework in .NET 8.

QuickGrid is a high performance grid component for displaying data in tabular form. QuickGrid is built to be a simple and convenient way to display your data, while still providing powerful features, such as sorting, filtering, paging, and virtualization.

For more information, see [ASP.NET Core Blazor \`QuickGrid\` component](../blazor/components/quickgrid?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0).

### Route to named elements

Blazor now supports using client-side routing to navigate to a specific HTML element on a page using standard URL fragments. If you specify an identifier for an HTML element using the standard `id` attribute, Blazor correctly scrolls to that element when the URL fragment matches the element identifier.

For more information, see [ASP.NET Core Blazor navigation](../blazor/fundamentals/navigation?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#hashed-routing-to-named-elements).

### Root-level cascading values

Root-level cascading values can be registered for the entire component hierarchy. Named cascading values and subscriptions for update notifications are supported.

For more information, see the following resources:

* [Root-level cascading values](../blazor/components/cascading-values-and-parameters?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#root-level-cascading-values)
* [Root-level cascading values with notifications](../blazor/components/cascading-values-and-parameters?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#root-level-cascading-values-with-notifications)

### Virtualize empty content

Use the new `EmptyContent` parameter on the `Virtualize` component to supply content when the component has loaded and either `Items` is empty or `ItemsProviderResult<T>.TotalItemCount` is zero.

For more information, see [ASP.NET Core Razor component virtualization](../blazor/components/virtualization?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#empty-content).

### Close circuits when there are no remaining interactive server components

Interactive server components handle web UI events using a real-time connection with the browser called a circuit. A circuit and its associated state are set up when a root interactive server component is rendered. The circuit is closed when there are no remaining interactive server components on the page, which frees up server resources.

### Monitor SignalR circuit activity

You can now monitor inbound circuit activity in server-side apps using the new `CreateInboundActivityHandler` method on `CircuitHandler`. Inbound circuit activity is any activity sent from the browser to the server, such as UI events or JavaScript-to-.NET interop calls.

For more information, see [ASP.NET Core Blazor SignalR guidance](../blazor/fundamentals/signalr?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#monitor-server-side-circuit-activity).

### Faster runtime performance with the Jiterpreter

The *Jiterpreter* is a new runtime feature in .NET 8 that enables partial Just-in-Time (JIT) compilation support when running on WebAssembly to achieve improved runtime performance.

For more information, see [ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation](../blazor/webassembly-build-tools-and-aot?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#ahead-of-time-aot-compilation).

### Ahead-of-time (AOT) SIMD and exception handling

Blazor WebAssembly ahead-of-time (AOT) compilation now uses [WebAssembly Fixed-width SIMD](https://wikipedia.org/wiki/Single_instruction,_multiple_data) and [WebAssembly Exception handling](https://github.com/WebAssembly/exception-handling/blob/master/proposals/exception-handling/Exceptions.md) by default to improve runtime performance.

For more information, see the following articles:

* [AOT: Single Instruction, Multiple Data (SIMD)](../blazor/tooling?view=aspnetcore-10.0\&pivots=windows\&preserve-view=true\&view=aspnetcore-8.0#single-instruction-multiple-data-simd)
* [AOT: Exception handling](../blazor/tooling?view=aspnetcore-10.0\&pivots=windows\&preserve-view=true\&view=aspnetcore-8.0#exception-handling)

### Web-friendly Webcil packaging

Webcil is web-friendly packaging of .NET assemblies that removes content specific to native Windows execution to avoid issues when deploying to environments that block the download or use of `.dll` files. Webcil is enabled by default for Blazor WebAssembly apps.

For more information, see [Host and deploy ASP.NET Core Blazor WebAssembly](../blazor/host-and-deploy/webassembly/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#webcil-packaging-format-for-net-assemblies).

Note

Prior to the release of .NET 8, guidance in [Deployment layout for ASP.NET Core hosted Blazor WebAssembly apps](../blazor/host-and-deploy/webassembly/deployment-layout?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0) addresses environments that block clients from downloading and executing DLLs with a multipart bundling approach. In .NET 8 or later, Blazor uses the Webcil file format to address this problem. Multipart bundling using the experimental NuGet package described by the *WebAssembly deployment layout* article isn't supported for Blazor apps in .NET 8 or later. For more information, see [Enhance `Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle` package to define a custom bundle format (dotnet/aspnetcore #36978)](https://github.com/dotnet/aspnetcore/issues/36978#issuecomment-1439283893). If you desire to continue using the multipart bundle package in .NET 8 or later apps, you can use the guidance in the article to create your own multipart bundling NuGet package, but it won't be supported by Microsoft.

### Blazor WebAssembly debugging improvements

When debugging .NET on WebAssembly, the debugger now downloads symbol data from symbol locations that are configured in Visual Studio preferences. This improves the debugging experience for apps that use NuGet packages.

You can now debug Blazor WebAssembly apps using Firefox. Debugging Blazor WebAssembly apps requires configuring the browser for remote debugging and then connecting to the browser using the browser developer tools through the .NET WebAssembly debugging proxy. Debugging Firefox from Visual Studio isn't supported at this time.

For more information, see [Debug ASP.NET Core Blazor apps](../blazor/debug?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#debug-with-firefox).

### Content Security Policy (CSP) compatibility

Blazor WebAssembly no longer requires enabling the `unsafe-eval` script source when specifying a Content Security Policy (CSP).

For more information, see [Enforce a Content Security Policy for ASP.NET Core Blazor](../blazor/security/content-security-policy?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0).

### Handle caught exceptions outside of a Razor component's lifecycle

Use `ComponentBase.DispatchExceptionAsync` in a Razor component to process exceptions thrown outside of the component's lifecycle call stack. This permits the component's code to treat exceptions as though they're lifecycle method exceptions. Thereafter, Blazor's error handling mechanisms, such as error boundaries, can process exceptions.

For more information, see [Handle errors in ASP.NET Core Blazor apps](../blazor/fundamentals/handle-errors?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#handle-caught-exceptions-outside-of-a-razor-components-lifecycle).

### Configure the .NET WebAssembly runtime

The .NET WebAssembly runtime can now be configured for Blazor startup.

For more information, see [ASP.NET Core Blazor startup](../blazor/fundamentals/startup?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#configure-the-net-webAssembly-runtime).

### Configuration of connection timeouts in `HubConnectionBuilder`

Prior workarounds for configuring hub connection timeouts can be replaced with formal SignalR hub connection builder timeout configuration.

For more information, see the following:

* [ASP.NET Core Blazor SignalR guidance](../blazor/fundamentals/signalr?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#configure-signalr-timeouts-and-keep-alive-on-the-client)
* [Host and deploy ASP.NET Core Blazor WebAssembly](../blazor/host-and-deploy/webassembly/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#global-deployment-and-connection-failures)
* [Host and deploy ASP.NET Core server-side Blazor apps](../blazor/host-and-deploy/server/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#global-deployment-and-connection-failures)

### Project templates shed Open Iconic

The Blazor project templates no longer depend on [Open Iconic](https://github.com/iconic/open-iconic) for icons.

### Support for dialog cancel and close events

Blazor now supports the [`cancel`](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/cancel_event) and [`close`](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/close_event) events on the `dialog` HTML element.

In the following example:

* `OnClose` is called when the `my-dialog` dialog is closed with the **Close** button.
* `OnCancel` is called when the dialog is canceled with the `Esc` key. When an HTML dialog is dismissed with the `Esc` key, both the `cancel` and `close` events are triggered.

```
<div>
    <p>Output: @message</p>

    <button onclick="document.getElementById('my-dialog').showModal()">
        Show modal dialog
    </button>

    <dialog id="my-dialog" @onclose="OnClose" @oncancel="OnCancel">
        <p>Hi there!</p>

        <form method="dialog">
            <button>Close</button>
        </form>
    </dialog>
</div>

@code {
    private string? message;

    private void OnClose(EventArgs e) => message += "onclose, ";

    private void OnCancel(EventArgs e) => message += "oncancel, ";
}
```

### Blazor Identity UI

Blazor supports generating a full Blazor-based Identity UI when you choose the authentication option for *Individual Accounts*. You can either select the option for Individual Accounts in the new project dialog for Blazor Web Apps from Visual Studio or pass the `-au|--auth` option set to `Individual` from the command line when you create a new project.

For more information, see the following resources:

* [ASP.NET Core Blazor authentication and authorization](../blazor/security/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#blazor-identity-ui-individual-accounts)
* [What's new with identity in .NET 8 (blog post)](https://devblogs.microsoft.com/dotnet/whats-new-with-identity-in-dotnet-8/#the-blazor-identity-ui)

### Secure Blazor WebAssembly with ASP.NET Core Identity

The Blazor documentation hosts a new article and sample app to cover securing a standalone Blazor WebAssembly app with ASP.NET Core Identity.

For more information, see the following resources:

* [Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](../blazor/security/webassembly/standalone-with-identity/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0)
* [What's new with identity in .NET 8 (blog post)](https://devblogs.microsoft.com/dotnet/whats-new-with-identity-in-dotnet-8/#the-blazor-identity-ui)

### Blazor Server with Yarp routing

Routing and deep linking for Blazor Server with Yarp work correctly in .NET 8.

For more information, see [Migrate from ASP.NET Core in .NET 7 to .NET 8](../migration/70-to-80?view=aspnetcore-10.0#drop-blazor-server-with-yarp-routing-workaround).

### Multiple Blazor Web Apps per server project

Support for multiple Blazor Web Apps per server project is under consideration for a future .NET release.

For more information, see [Support for multiple Blazor Web apps per server project (`dotnet/aspnetcore` #52216)](https://github.com/dotnet/aspnetcore/issues/52216).

### Blazor Hybrid

The following articles document changes for Blazor Hybrid in .NET 8:

* [Troubleshoot ASP.NET Core Blazor Hybrid](../blazor/hybrid/troubleshoot?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0): A new article explains how to use [BlazorWebView](/en-us/dotnet/api/microsoft.aspnetcore.components.webview.maui.blazorwebview) logging.
* [Build your first app](/en-us/dotnet/maui/get-started/first-app) tutorial: The project template name **.NET MAUI Blazor** has changed to **.NET MAUI Blazor Hybrid**.
* [ASP.NET Core Blazor Hybrid](../blazor/hybrid/?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#access-scoped-services-from-native-ui): `BlazorWebView` gains a `TryDispatchAsync` method that calls a specified `Action<ServiceProvider>` asynchronously and passes in the scoped services available in Razor components. This enables code from the native UI to access scoped services such as `NavigationManager`.
* [ASP.NET Core Blazor Hybrid routing and navigation](../blazor/hybrid/routing?view=aspnetcore-10.0\&preserve-view=true\&pivots=maui\&view=aspnetcore-8.0#get-or-set-a-path-for-initial-navigation): Use the `BlazorWebView.StartPath` property to get or set the path for initial navigation within the Blazor navigation context when the Razor component is finished loading.

### `[Parameter]` attribute is no longer required when supplied from the query string

The `[Parameter]` attribute is no longer required when supplying a parameter from the query string:

```
- [Parameter]
  [SupplyParameterFromQuery]
```

## SignalR

### New approach to set the server timeout and Keep-Alive interval

[ServerTimeout](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.servertimeout#microsoft-aspnetcore-signalr-client-hubconnection-servertimeout) (default: 30 seconds) and [KeepAliveInterval](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnection.keepaliveinterval#microsoft-aspnetcore-signalr-client-hubconnection-keepaliveinterval) (default: 15 seconds) can be set directly on [HubConnectionBuilder](/en-us/dotnet/api/microsoft.aspnetcore.signalr.client.hubconnectionbuilder).

#### Prior approach for JavaScript clients

The following example shows the assignment of values that are double the default values in .NET 7 or earlier:

```
var connection = new signalR.HubConnectionBuilder()
  .withUrl("/chatHub")
  .build();

connection.serverTimeoutInMilliseconds = 60000;
connection.keepAliveIntervalInMilliseconds = 30000;
```

#### New approach for JavaScript clients

The following example shows the ***new approach*** for assigning values that are double the default values in .NET 8 or later:

```
var connection = new signalR.HubConnectionBuilder()
  .withUrl("/chatHub")
  .withServerTimeout(60000)
  .withKeepAlive(30000)
  .build();
```

#### Prior approach for the JavaScript client of a Blazor Server app

The following example shows the assignment of values that are double the default values in .NET 7 or earlier:

```
Blazor.start({
  configureSignalR: function (builder) {
    let c = builder.build();
    c.serverTimeoutInMilliseconds = 60000;
    c.keepAliveIntervalInMilliseconds = 30000;
    builder.build = () => {
      return c;
    };
  }
});
```

#### New approach for the JavaScript client of server-side Blazor app

The following example shows the ***new approach*** for assigning values that are double the default values in .NET 8 or later for Blazor Web Apps and Blazor Server.

Blazor Web App:

```
Blazor.start({
  circuit: {
    configureSignalR: function (builder) {
      builder.withServerTimeout(60000).withKeepAliveInterval(30000);
    }
  }
});
```

Blazor Server:

```
Blazor.start({
  configureSignalR: function (builder) {
    builder.withServerTimeout(60000).withKeepAliveInterval(30000);
  }
});
```

#### Prior approach for .NET clients

The following example shows the assignment of values that are double the default values in .NET 7 or earlier:

```
var builder = new HubConnectionBuilder()
    .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
    .Build();

builder.ServerTimeout = TimeSpan.FromSeconds(60);
builder.KeepAliveInterval = TimeSpan.FromSeconds(30);

builder.On<string, string>("ReceiveMessage", (user, message) => ...

await builder.StartAsync();
```

#### New approach for .NET clients

The following example shows the ***new approach*** for assigning values that are double the default values in .NET 8 or later:

```
var builder = new HubConnectionBuilder()
    .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
    .WithServerTimeout(TimeSpan.FromSeconds(60))
    .WithKeepAliveInterval(TimeSpan.FromSeconds(30))
    .Build();

builder.On<string, string>("ReceiveMessage", (user, message) => ...

await builder.StartAsync();
```

### SignalR stateful reconnect

SignalR stateful reconnect reduces the perceived downtime of clients that have a temporary disconnect in their network connection, such as when switching network connections or a short temporary loss in access.

Stateful reconnect achieves this by:

* Temporarily buffering data on the server and client.
* Acknowledging messages received (ACK-ing) by both the server and client.
* Recognizing when a connection is returning and replaying messages that might have been sent while the connection was down.

Stateful reconnect is available in .NET 8 or later.

Opt in to stateful reconnect at both the server hub endpoint and the client:

* Update the server hub endpoint configuration to enable the `AllowStatefulReconnects` option:

  ```
  app.MapHub<MyHub>("/hubName", options =>
  {
      options.AllowStatefulReconnects = true;
  });
  ```

  Optionally, the maximum buffer size in bytes allowed by the server can be set globally or for a specific hub with the `StatefulReconnectBufferSize` option:

  The `StatefulReconnectBufferSize` option set globally:

  ```
  builder.AddSignalR(o => o.StatefulReconnectBufferSize = 1000);
  ```

  The `StatefulReconnectBufferSize` option set for a specific hub:

  ```
  builder.AddSignalR().AddHubOptions<MyHub>(o => o.StatefulReconnectBufferSize = 1000);
  ```

  The `StatefulReconnectBufferSize` option is optional with a default of 100,000 bytes.

* Update JavaScript or TypeScript client code to enable the `withStatefulReconnect` option:

  ```
  const builder = new signalR.HubConnectionBuilder()
    .withUrl("/hubname")
    .withStatefulReconnect({ bufferSize: 1000 });  // Optional, defaults to 100,000
  const connection = builder.build();
  ```

  The `bufferSize` option is optional with a default of 100,000 bytes.

* Update .NET client code to enable the `WithStatefulReconnect` option:

  ```
    var builder = new HubConnectionBuilder()
        .WithUrl("<hub url>")
        .WithStatefulReconnect();
    builder.Services.Configure<HubConnectionOptions>(o => o.StatefulReconnectBufferSize = 1000);
    var hubConnection = builder.Build();
  ```

  The `StatefulReconnectBufferSize` option is optional with a default of 100,000 bytes.

For more information, see [Configure stateful reconnect](../signalr/configuration?view=aspnetcore-10.0#configure-stateful-reconnect).

## Minimal APIs

This section describes new features for Minimal APIs. See also [the section on Native AOT](#native-aot) for more information relevant to Minimal APIs.

### User override culture

Starting in .NET 8, the [RequestLocalizationOptions.CultureInfoUseUserOverride](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.cultureinfouseuseroverride#microsoft-aspnetcore-builder-requestlocalizationoptions-cultureinfouseuseroverride) property allows the application to decide whether or not to use nondefault Windows settings for the [CultureInfo](/en-us/dotnet/api/system.globalization.cultureinfo) [DateTimeFormat](/en-us/dotnet/api/system.globalization.cultureinfo.datetimeformat#system-globalization-cultureinfo-datetimeformat) and [NumberFormat](/en-us/dotnet/api/system.globalization.cultureinfo.numberformat#system-globalization-cultureinfo-numberformat) properties. This has no impact on Linux. This directly corresponds to [UseUserOverride](/en-us/dotnet/api/system.globalization.cultureinfo.useuseroverride#system-globalization-cultureinfo-useuseroverride).

```
    app.UseRequestLocalization(options =>
    {
        options.CultureInfoUseUserOverride = false;
    });
```

### Binding to forms

Explicit binding to form values using the [\[FromForm\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) attribute is now supported. Parameters bound to the request with `[FromForm]` include an [antiforgery token](../security/anti-request-forgery?view=aspnetcore-10.0). The antiforgery token is validated when the request is processed.

Inferred binding to forms using the [IFormCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection), [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile), and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection) types is also supported. [OpenAPI](../fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0) metadata is inferred for form parameters to support integration with [Swagger UI](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0).

For more information, see:

* [Explicit binding from form values](../fundamentals/minimal-apis/parameter-binding?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#explicit-binding-from-form-values).
* [Binding to forms with IFormCollection, IFormFile, and IFormFileCollection](../fundamentals/minimal-apis/parameter-binding?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#binding-to-forms-with-iformcollection-iformfile-and-iformfilecollection).
* [Form binding in Minimal APIs](https://andrewlock.net/exploring-the-dotnet-8-preview-form-binding-in-minimal-apis/)

Binding from forms is now supported for:

* Collections, for example [List](/en-us/dotnet/api/system.collections.generic.list-1) and [Dictionary](/en-us/dotnet/api/system.collections.generic.dictionary-2)
* Complex types, for example, `Todo` or `Project`

For more information, see [Bind to collections and complex types from forms](../fundamentals/minimal-apis/parameter-binding?view=aspnetcore-10.0#bindcc).

### Antiforgery with Minimal APIs

This release adds a middleware for validating antiforgery tokens, which are used to mitigate cross-site request forgery attacks. Call [AddAntiforgery](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.antiforgeryservicecollectionextensions.addantiforgery) to register antiforgery services in DI. `WebApplicationBuilder` automatically adds the middleware when the antiforgery services have been registered in the DI container. Antiforgery tokens are used to mitigate [cross-site request forgery attacks](../security/anti-request-forgery?view=aspnetcore-10.0).

```
var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The antiforgery middleware:

* Does ***not*** short-circuit the execution of the rest of the request pipeline.
* Sets the [IAntiforgeryValidationFeature](https://source.dot.net/#Microsoft.AspNetCore.Http.Features/IAntiforgeryValidationFeature.cs,33a7a0e106f11c6f) in the [HttpContext.Features](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.features#microsoft-aspnetcore-http-httpcontext-features) of the current request.

The antiforgery token is only validated if:

* The endpoint contains metadata implementing [IAntiforgeryMetadata](https://source.dot.net/#Microsoft.AspNetCore.Http.Abstractions/Metadata/IAntiforgeryMetadata.cs,5f49d4d07fc58320) where `RequiresValidation=true`.
* The HTTP method associated with the endpoint is a relevant [HTTP method](https://developer.mozilla.org/docs/Web/HTTP/Methods). The relevant methods are all [HTTP methods](https://developer.mozilla.org/docs/Web/HTTP/Methods) except for TRACE, OPTIONS, HEAD, and GET.
* The request is associated with a valid endpoint.

For more information, see [Antiforgery with Minimal APIs](../security/anti-request-forgery?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#afwma).

### New `IResettable` interface in `ObjectPool`

[Microsoft.Extensions.ObjectPool](/en-us/dotnet/api/microsoft.extensions.objectpool) provides support for pooling object instances in memory. Apps can use an object pool if the values are expensive to allocate or initialize.

In this release, we've made the object pool easier to use by adding the [IResettable](/en-us/dotnet/api/microsoft.extensions.objectpool.iresettable) interface. Reusable types often need to be reset back to a default state between uses. `IResettable` types are automatically reset when returned to an object pool.

For more information, see the [ObjectPool sample](../performance/objectpool?view=aspnetcore-10.0##objectpool-sample).

## Native AOT

Support for [.NET native ahead-of-time (AOT)](/en-us/dotnet/core/deploying/native-aot/) has been added. Apps that are published using AOT can have substantially better performance: smaller app size, less memory usage, and faster startup time. Native AOT is currently supported by gRPC, Minimal API, and worker service apps. For more information, see [ASP.NET Core support for Native AOT](../fundamentals/native-aot?view=aspnetcore-10.0) and [Tutorial: Publish an ASP.NET Core app using Native AOT](../fundamentals/aot/native-aot-tutorial?view=aspnetcore-10.0). For information about known issues with ASP.NET Core and Native AOT compatibility, see GitHub issue [dotnet/core #8288](https://github.com/dotnet/core/issues/8288).

### New project template

The new **ASP.NET Core Web API (Native AOT)** project template (short name `webapiaot`) creates a project with AOT publish enabled. For more information, see [The Web API (Native AOT) template](../fundamentals/native-aot?view=aspnetcore-10.0#the-web-api-native-aot-template).

### New `CreateSlimBuilder` method

The [CreateSlimBuilder()](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createslimbuilder#microsoft-aspnetcore-builder-webapplication-createslimbuilder) method used in the Web API (Native AOT) template initializes the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) with the minimum ASP.NET Core features necessary to run an app. The `CreateSlimBuilder` method includes the following features that are typically needed for an efficient development experience:

* JSON file configuration for `appsettings.json` and `appsettings.{EnvironmentName}.json`.
* User secrets configuration.
* Console logging.
* Logging configuration.

For more information, see [The `CreateSlimBuilder` method](../fundamentals/native-aot?view=aspnetcore-10.0#the-createslimbuilder-method).

### New `CreateEmptyBuilder` method

There's another new [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) factory method for building small apps that only contain necessary features: `WebApplication.CreateEmptyBuilder(WebApplicationOptions options)`. This `WebApplicationBuilder` is created with no built-in behavior. The app it builds contains only the services and middleware that are explicitly configured.

Here's an example of using this API to create a small web application:

```
var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions());
builder.WebHost.UseKestrelCore();

var app = builder.Build();

app.Use(async (context, next) =>
{
    await context.Response.WriteAsync("Hello, World!");
    await next(context);
});

Console.WriteLine("Running...");
app.Run();
```

Publishing this code with Native AOT using .NET 8 Preview 7 on a linux-x64 machine results in a self-contained native executable of about 8.5 MB.

### Reduced app size with configurable HTTPS support

We've further reduced Native AOT binary size for apps that don't need HTTPS or HTTP/3 support. Not using HTTPS or HTTP/3 is common for apps that run behind a TLS termination proxy (for example, hosted on Azure). The new `WebApplication.CreateSlimBuilder` method omits this functionality by default. It can be added by calling `builder.WebHost.UseKestrelHttpsConfiguration()` for HTTPS or `builder.WebHost.UseQuic()` for HTTP/3. For more information, see [The `CreateSlimBuilder` method](../fundamentals/native-aot?view=aspnetcore-10.0#the-createslimbuilder-method).

### JSON serialization of compiler-generated `IAsyncEnumerable<T>` types

New features were added to [System.Text.Json](/en-us/dotnet/api/system.text.json) to better support Native AOT. These new features add capabilities for the source generation mode of `System.Text.Json`, because reflection isn't supported by AOT.

One of the new features is support for JSON serialization of [IAsyncEnumerable\<T>](/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1) implementations implemented by the C# compiler. This support opens up their use in ASP.NET Core projects configured to publish Native AOT.

This API is useful in scenarios where a route handler uses `yield return` to asynchronously return an enumeration. For example, to materialize rows from a database query. For more information, see [Unspeakable type support](https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-4/#unspeakable-type-support) in the .NET 8 Preview 4 announcement.

For information abut other improvements in `System.Text.Json` source generation, see [Serialization improvements in .NET 8](/en-us/dotnet/core/whats-new/dotnet-8#serialization).

### Top-level APIs annotated for trim warnings

The main entry points to subsystems that don't work reliably with Native AOT are now annotated. When these methods are called from an application with Native AOT enabled, a warning is provided. For example, the following code produces a warning at the invocation of `AddControllers` because this API isn't trim-safe and isn't supported by Native AOT.

### Request delegate generator

In order to make Minimal APIs compatible with Native AOT, we're introducing the Request Delegate Generator (RDG). The RDG is a source generator that does what the [RequestDelegateFactory](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegatefactory) (RDF) does. That is, it turns the various `MapGet()`, `MapPost()`, and calls like them into [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate) instances associated with the specified routes. But rather than doing it in-memory in an application when it starts, the RDG does it at compile time and generates C# code directly into the project. The RDG:

* Removes the runtime generation of this code.
* Ensures that the types used in APIs are statically analyzable by the Native AOT tool-chain.
* Ensures that required code isn't trimmed away.

We're working to ensure that as many as possible of the Minimal API features are supported by the RDG and thus compatible with Native AOT.

The RDG is enabled automatically in a project when publishing with Native AOT is enabled. RDG can be manually enabled even when not using Native AOT by setting `<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>` in the project file. This can be useful when initially evaluating a project's readiness for Native AOT, or to reduce the startup time of an app.

### Improved performance using Interceptors

The Request Delegate Generator uses the new [C# 12 interceptors compiler feature](/en-us/dotnet/csharp/whats-new/csharp-12) to support intercepting calls to Minimal API [Map](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions) methods with statically generated variants at runtime. The use of interceptors results in increased startup performance for apps compiled with `PublishAot`.

### Logging and exception handling in compile-time generated Minimal APIs

Minimal APIs generated at run time support automatically logging (or throwing exceptions in `Development` environments) when parameter binding fails. .NET 8 introduces the same support for APIs generated at compile time via the [Request Delegate Generator](#request-delegate-generator) (RDG). For more information, see [Logging and exception handling in compile-time generated Minimal APIs](https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-4/#logging-and-exception-handling-in-compile-time-generated-minimal-apis).

### AOT and System.Text.Json

Minimal APIs are optimized for receiving and returning JSON payloads using `System.Text.Json`, so the compatibility requirements for JSON and Native AOT apply too. Native AOT compatibility requires the use of the `System.Text.Json` source generator. All types accepted as parameters to or returned from request delegates in Minimal APIs must be configured on a `JsonSerializerContext` that is registered via ASP.NET Core's dependency injection, for example:

```
// Register the JSON serializer context with DI
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

...

// Add types used in the Minimal API app to source generated JSON serializer content
[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{

}
```

For more information about the [TypeInfoResolverChain](/en-us/dotnet/api/system.text.json.jsonserializeroptions.typeinforesolverchain#system-text-json-jsonserializeroptions-typeinforesolverchain) API, see the following resources:

* [JsonSerializerOptions.TypeInfoResolverChain](https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-4/#jsonserializeroptions-typeinforesolverchain)
* [Chain source generators](/en-us/dotnet/core/whats-new/dotnet-8#chain-source-generators)
* [Changes to support source generation](../fundamentals/native-aot?view=aspnetcore-10.0#changes-to-support-source-generation)

### Libraries and Native AOT

Many of the common libraries available for ASP.NET Core projects today have some compatibility issues if used in a project targeting Native AOT. Popular libraries often rely on the dynamic capabilities of .NET reflection to inspect and discover types, conditionally load libraries at runtime, and generate code on the fly to implement their functionality. These libraries need to be updated in order to work with Native AOT by using tools like [Roslyn source generators](/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview).

Library authors wishing to learn more about preparing their libraries for Native AOT are encouraged to start by [preparing their library for trimming](/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming) and learning more about the [Native AOT compatibility requirements](/en-us/dotnet/core/deploying/native-aot/).

## Kestrel and HTTP.sys servers

There are several new features for Kestrel and HTTP.sys.

### Support for named pipes in Kestrel

Named pipes is a popular technology for building inter-process communication (IPC) between Windows apps. You can now build an IPC server using .NET, Kestrel, and named pipes.

```
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenNamedPipe("MyPipeName");
});
```

For more information about this feature and how to use .NET and gRPC to create an IPC server and client, see [Inter-process communication with gRPC](../grpc/interprocess?view=aspnetcore-10.0).

### Performance improvements to named pipes transport

We've improved named pipe connection performance. Kestrel's named pipe transport now accepts connections in parallel, and reuses [NamedPipeServerStream](/en-us/dotnet/api/system.io.pipes.namedpipeserverstream) instances.

Time to create 100,000 connections:

* Before : 5.916 seconds
* After   : 2.374 seconds

### HTTP/2 over TLS (HTTPS) support on macOS in Kestrel

.NET 8 adds support for Application-Layer Protocol Negotiation (ALPN) to macOS. ALPN is a TLS feature used to negotiate which HTTP protocol a connection will use. For example, ALPN allows browsers and other HTTP clients to request an HTTP/2 connection. This feature is especially useful for gRPC apps, which require HTTP/2. For more information, see [Use HTTP/2 with the ASP.NET Core Kestrel web server](../fundamentals/servers/kestrel/http2?view=aspnetcore-10.0).

### Certificate file watching in Kestrel

TLS certificates [configured](../fundamentals/configuration/?view=aspnetcore-10.0) by path are now monitored for changes when `reloadOnChange` is passed to [KestrelServerOptions.Configure()](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configure#microsoft-aspnetcore-server-kestrel-core-kestrelserveroptions-configure). A change to the certificate file is treated the same way as a change to the configured path (that is, endpoints are reloaded).

Note that file deletions are specifically not tracked since they arise transiently and would crash the server if non-transient.

### Warning when specified HTTP protocols won't be used

If TLS is disabled and HTTP/1.x is available, HTTP/2 and HTTP/3 will be disabled, even if they've been specified. This can cause some nasty surprises, so we've added warning output to let you know when it happens.

### `HTTP_PORTS` and `HTTPS_PORTS` config keys

Most configurations for apps and containers define only a port for listening, like port 80, without specifying other constraints like the host or path. `HTTP_PORTS` and `HTTPS_PORTS` are config keys that specify the listening ports for the Kestrel and HTTP.sys servers. You can specify the keys as environment variables defined with the `DOTNET_` or `ASPNETCORE_` prefixes, or set them directly through any other config input, such as the `appsettings.json` file. Each configuration is a semicolon-delimited list of port values, as shown in the following example:

```
ASPNETCORE_HTTP_PORTS=80;8080
ASPNETCORE_HTTPS_PORTS=443;8081
```

The configuration in the example is shorthand for the following specification, which defines the scheme (HTTP or HTTPS) and any host or IP:

```
ASPNETCORE_URLS=http://*:80/;http://*:8080/;https://*:443/;https://*:8081/
```

The `HTTP_PORTS` and `HTTPS_PORTS` configuration keys are lower priority. If other URLs or values are set directly in code, they can override the configuration keys. Configure certificates separately using server-specific mechanics for HTTPS.

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### SNI host name in ITlsHandshakeFeature

The Server Name Indication (SNI) host name is now exposed in the [HostName](https://source.dot.net/#Microsoft.AspNetCore.Connections.Abstractions/Features/ITlsHandshakeFeature.cs,29) property of the [ITlsHandshakeFeature](/en-us/dotnet/api/microsoft.aspnetcore.connections.features.itlshandshakefeature) interface.

SNI is part of the [TLS handshake](https://auth0.com/blog/the-tls-handshake-explained/) process. It allows clients to specify the host name they're attempting to connect to when the server hosts multiple virtual hosts or domains. To present the correct security certificate during the handshake process, the server needs to know the host name selected for each request.

Normally the host name is only handled within the TLS stack and is used to select the matching certificate. But by exposing it, other components in an app can use that information for purposes such as diagnostics, rate limiting, routing, and billing.

Exposing the host name is useful for large-scale services managing thousands of SNI bindings. This feature can significantly improve debugging efficiency during customer escalations. The increased transparency allows for faster problem resolution and enhanced service reliability.

For more information, see [ITlsHandshakeFeature.HostName](https://source.dot.net/#Microsoft.AspNetCore.Connections.Abstractions/Features/ITlsHandshakeFeature.cs,30).

### IHttpSysRequestTimingFeature

\[IHttpSysRequestTimingFeature]\(https\://source.dot.net/#Microsoft.AspNetCore.Server.HttpSys/IHttpSysRequestTimingFeature.cs,3c5dc86dc837b1f4) provides detailed timing information for requests when using the \[HTTP.sys server]\(xref:fundamentals/servers/httpsys) and \[In-process hosting with IIS]\(xref:host-and-deploy/iis/in-process-hosting?view=aspnetcore-8.0\&preserve-view=true#ihsrtf8):

* Timestamps are obtained using [QueryPerformanceCounter](/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter).
* The timestamp frequency can be obtained via [QueryPerformanceFrequency](/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency).
* The index of the timing can be cast to [HttpSysRequestTimingType](https://source.dot.net/#Microsoft.AspNetCore.Server.HttpSys/HttpSysRequestTimingType.cs,e62e7bcd02f8589e) to know what the timing represents.
* The value might be 0 if the timing isn't available for the current request.

\[IHttpSysRequestTimingFeature.TryGetTimestamp]\(https\://source.dot.net/#Microsoft.AspNetCore.Server.HttpSys/IHttpSysRequestTimingFeature.cs,3c5dc86dc837b1f4) retrieves the timestamp for the provided timing type:

```
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys();

var app = builder.Build();

app.Use((context, next) =>
{
    var feature = context.Features.GetRequiredFeature<IHttpSysRequestTimingFeature>();

    var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("Sample");

    var timingType = HttpSysRequestTimingType.RequestRoutingEnd;

    if (feature.TryGetTimestamp(timingType, out var timestamp))
    {
        logger.LogInformation("Timestamp {timingType}: {timestamp}",
                                          timingType, timestamp);
    }
    else
    {
        logger.LogInformation("Timestamp {timingType}: not available for the "
                                           + "current request",    timingType);
    }

    return next(context);
});

app.MapGet("/", () => Results.Ok());

app.Run();
```

For more information, see [Get detailed timing information with IHttpSysRequestTimingFeature](../fundamentals/servers/httpsys?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#ihsrtf8) and [Timing information and In-process hosting with IIS](../host-and-deploy/iis/in-process-hosting?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#ihsrtf8).

### HTTP.sys: opt-in support for kernel-mode response buffering

In some scenarios, high volumes of small writes with high latency can cause significant performance impact to `HTTP.sys`. This impact is due to the lack of a [Pipe](/en-us/dotnet/api/system.io.pipelines.pipe) buffer in the `HTTP.sys` implementation. To improve performance in these scenarios, support for response buffering has been added to `HTTP.sys`. Enable buffering by setting [HttpSysOptions.EnableKernelResponseBuffering](https://github.com/dotnet/aspnetcore/blob/main/src/Servers/HttpSys/src/HttpSysOptions.cs#L120) to `true`.

Response buffering should be enabled by an app that does synchronous I/O, or asynchronous I/O with no more than one outstanding write at a time. In these scenarios, response buffering can significantly improve throughput over high-latency connections.

Apps that use asynchronous I/O and that can have more than one write outstanding at a time should ***not*** use this flag. Enabling this flag can result in higher CPU and memory usage by HTTP.Sys.

## Authentication and authorization

.NET 8 adds new features to authentication and authorization.

### Identity API endpoints

[`MapIdentityApi<TUser>`](/en-us/dotnet/api/microsoft.aspnetcore.routing.identityapiendpointroutebuilderextensions.mapidentityapi) is a new extension method that adds two API endpoints (`/register` and `/login`). The main goal of the `MapIdentityApi` is to make it easy for developers to use ASP.NET Core Identity for authentication in JavaScript-based single page apps (SPA) or Blazor apps. Instead of using the default UI provided by ASP.NET Core Identity, which is based on Razor Pages, MapIdentityApi adds JSON API endpoints that are more suitable for SPA apps and nonbrowser apps. For more information, see [Identity API endpoints](https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-4/#identity-api-endpoints).

### `IAuthorizationRequirementData`

Prior to the release of .NET 8, adding a parameterized authorization policy to an endpoint required implementing:

* `AuthorizeAttribute` for each policy.
* `AuthorizationPolicyProvider` to process a custom policy from a string-based contract.
* `AuthorizationRequirement` for the policy.
* `AuthorizationHandler` for each requirement.

For example, consider the following code written for .NET 7:

```
using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
builder.Services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>();

var app = builder.Build();

app.MapControllers();

app.Run();
```

```
using Microsoft.AspNetCore.Mvc;

namespace AuthRequirementsData.Controllers;

[ApiController]
[Route("api/[controller]")]
public class GreetingsController : Controller
{
    [MinimumAgeAuthorize(16)]
    [HttpGet("hello")]
    public string Hello() => $"Hello {(HttpContext.User.Identity?.Name ?? "world")}!";
}
```

```
using Microsoft.AspNetCore.Authorization;
using System.Globalization;
using System.Security.Claims;

namespace AuthRequirementsData.Authorization;

class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    private readonly ILogger<MinimumAgeAuthorizationHandler> _logger;

    public MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler> logger)
    {
        _logger = logger;
    }

    // Check whether a given MinimumAgeRequirement is satisfied or not for a particular
    // context.
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                               MinimumAgeRequirement requirement)
    {
        // Log as a warning so that it's very clear in sample output which authorization
        // policies(and requirements/handlers) are in use.
        _logger.LogWarning("Evaluating authorization requirement for age >= {age}",
                                                                    requirement.Age);

        // Check the user's age
        var dateOfBirthClaim = context.User.FindFirst(c => c.Type ==
                                                                 ClaimTypes.DateOfBirth);
        if (dateOfBirthClaim != null)
        {
            // If the user has a date of birth claim, check their age
            var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value, CultureInfo.InvariantCulture);
            var age = DateTime.Now.Year - dateOfBirth.Year;
            if (dateOfBirth > DateTime.Now.AddYears(-age))
            {
                // Adjust age if the user hasn't had a birthday yet this year.
                age--;
            }

            // If the user meets the age criterion, mark the authorization requirement
            // succeeded.
            if (age >= requirement.Age)
            {
                _logger.LogInformation("Minimum age authorization requirement {age} satisfied",
                                         requirement.Age);
                context.Succeed(requirement);
            }
            else
            {
                _logger.LogInformation("Current user's DateOfBirth claim ({dateOfBirth})" +
                    " does not satisfy the minimum age authorization requirement {age}",
                    dateOfBirthClaim.Value,
                    requirement.Age);
            }
        }
        else
        {
            _logger.LogInformation("No DateOfBirth claim present");
        }

        return Task.CompletedTask;
    }
}
```

The complete sample for .NET 7 or earlier is the [OldStyleAuthRequirements sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/OldStyleAuthRequirements) ([how to download](../blazor/fundamentals/?view=aspnetcore-10.0#sample-apps)).

.NET 8 introduces the [IAuthorizationRequirementData](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationrequirementdata) interface. The `IAuthorizationRequirementData` interface allows the attribute definition to specify the requirements associated with the authorization policy. Using `IAuthorizationRequirementData`, the preceding custom authorization policy code can be written with fewer lines of code. The updated `Program.cs` file:

```
using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;
  
var builder = WebApplication.CreateBuilder();
  
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
-builder.Services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>();
  
var app = builder.Build();
  
app.MapControllers();
  
app.Run();
```

The updated `MinimumAgeAuthorizationHandler`:

```
using Microsoft.AspNetCore.Authorization;
using System.Globalization;
using System.Security.Claims;

namespace AuthRequirementsData.Authorization;

class MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler> logger) 
-    : AuthorizationHandler<MinimumAgeRequirement>
+    : AuthorizationHandler<MinimumAgeAuthorizeAttribute>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
-       MinimumAgeRequirement requirement)
+       MinimumAgeAuthorizeAttribute requirement)
    {
        ...
    }
}
```

The updated sample is the [AuthRequirementsData sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/AuthRequirementsData) ([how to download](../blazor/fundamentals/?view=aspnetcore-10.0#sample-apps)).

For more information, see [Custom authorization policies with \`IAuthorizationRequirementData\`](../security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata?view=aspnetcore-10.0).

### Securing Swagger UI endpoints

Swagger UI endpoints can now be secured in production environments by calling [`MapSwagger().RequireAuthorization`](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization). For more information, see [Securing Swagger UI endpoints](/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?view=aspnetcore-8.0\&preserve-view=true#securing-swagger-ui-endpoints)

## Miscellaneous

The following sections describe miscellaneous new features in ASP.NET Core in .NET 8.

### Keyed services support in Dependency Injection

*Keyed services* refers to a mechanism for registering and retrieving Dependency Injection (DI) services using keys. A service is associated with a key by calling [AddKeyedSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addkeyedsingleton) (or `AddKeyedScoped` or `AddKeyedTransient`) to register it. Access a registered service by specifying the key with the [`[FromKeyedServices]`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute) attribute. The following code shows how to use keyed services:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
                                                               smallCache.Get("date"));

app.MapControllers();

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
    [HttpGet("big-cache")]
    public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
    {
        return cache.Get("data-mvc");
    }
}

public class MyHub : Hub
{
    public void Method([FromKeyedServices("small")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }
}
```

### Visual Studio project templates for SPA apps with ASP.NET Core backend

Visual Studio project templates are now the recommended way to create single-page apps (SPAs) that have an ASP.NET Core backend. Templates are provided that create apps based on the JavaScript technologies, such as [Angular](https://angular.dev/), [React](https://react.dev/), and [Vue](https://vuejs.org/). These templates:

* Create a Visual Studio solution with a frontend project and a backend project.
* Use the Visual Studio project type for JavaScript and TypeScript (*.esproj*) for the frontend.
* Use an ASP.NET Core project for the backend.

For more information about the Visual Studio templates and how to access the legacy templates, see [Overview of Single Page Apps (SPAs) - ASP.NET Core](../client-side/spa/intro?view=aspnetcore-10.0)

### Support for generic attributes

Attributes that previously required a [Type](/en-us/dotnet/api/system.type) parameter are now available in cleaner generic variants. This is made possible by support for [generic attributes](/en-us/dotnet/csharp/whats-new/csharp-11) in C# 11. For example, the syntax for annotating the response type of an action can be modified as follows:

```
[ApiController]
[Route("api/[controller]")]
public class TodosController : Controller
{
  [HttpGet("/")]
- [ProducesResponseType(typeof(Todo), StatusCodes.Status200OK)]
+ [ProducesResponseType<Todo>(StatusCodes.Status200OK)]
  public Todo Get() => new Todo(1, "Write a sample", DateTime.Now, false);
}
```

Generic variants are supported for the following attributes:

* `[ProducesResponseType<T>]`
* `[Produces<T>]`
* `[MiddlewareFilter<T>]`
* `[ModelBinder<T>]`
* `[ModelMetadataType<T>]`
* `[ServiceFilter<T>]`
* `[TypeFilter<T>]`

### Code analysis in ASP.NET Core apps

The new analyzers shown in the following table are available in .NET 8.

| Diagnostic ID                                          | Breaking or nonbreaking | Description                                                           |
| ------------------------------------------------------ | ----------------------- | --------------------------------------------------------------------- |
| [ASP0016](../diagnostics/asp0016?view=aspnetcore-10.0) | Nonbreaking             | Don't return a value from RequestDelegate                             |
| [ASP0019](../diagnostics/asp0019?view=aspnetcore-10.0) | Nonbreaking             | Suggest using IHeaderDictionary.Append or the indexer                 |
| [ASP0020](../diagnostics/asp0020?view=aspnetcore-10.0) | Nonbreaking             | Complex types referenced by route parameters must be parsable         |
| [ASP0021](../diagnostics/asp0021?view=aspnetcore-10.0) | Nonbreaking             | The return type of the BindAsync method must be `ValueTask<T>`        |
| [ASP0022](../diagnostics/asp0022?view=aspnetcore-10.0) | Nonbreaking             | Route conflict detected between route handlers                        |
| [ASP0023](../diagnostics/asp0023?view=aspnetcore-10.0) | Nonbreaking             | MVC: Route conflict detected between route handlers                   |
| [ASP0024](../diagnostics/asp0024?view=aspnetcore-10.0) | Nonbreaking             | Route handler has multiple parameters with the `[FromBody]` attribute |
| [ASP0025](../diagnostics/asp0025?view=aspnetcore-10.0) | Nonbreaking             | Use AddAuthorizationBuilder                                           |

### Route tooling

ASP.NET Core is built on routing. Minimal APIs, Web APIs, Razor Pages, and Blazor all use routes to customize how HTTP requests map to code.

In .NET 8 we've invested in a suite of new features to make routing easier to learn and use. These new features include:

* [Route syntax highlighting](https://devblogs.microsoft.com/dotnet/aspnet-core-route-tooling-dotnet-8/#route-syntax-highlighting)

* [Autocomplete of parameter and route names](https://devblogs.microsoft.com/dotnet/aspnet-core-route-tooling-dotnet-8/#autocomplete-of-parameter-and-route-names)

* [Autocomplete of route constraints](https://devblogs.microsoft.com/dotnet/aspnet-core-route-tooling-dotnet-8/#autocomplete-of-route-constraints)

* [Route analyzers and fixers](https://devblogs.microsoft.com/dotnet/aspnet-core-route-tooling-dotnet-8/#route-analyzers-and-fixers)

  * [Route syntax analyzer](https://devblogs.microsoft.com/dotnet/aspnet-core-route-tooling-dotnet-8/#route-syntax-analyzer)
  * [Mismatched parameter optionality analyzer and fixer](https://devblogs.microsoft.com/dotnet/aspnet-core-route-tooling-dotnet-8/#mismatched-parameter-optionality-analyzer-and-fixer)
  * [Ambiguous Minimal API and Web API route analyzer](https://devblogs.microsoft.com/dotnet/aspnet-core-route-tooling-dotnet-8/#ambiguous-minimal-api-and-web-api-route-analyzer)

* [Support for Minimal APIs, Web APIs, and Blazor](https://devblogs.microsoft.com/dotnet/aspnet-core-route-tooling-dotnet-8/#supports-minimal-apis-web-apis-and-blazor)

For more information, see [Route tooling in .NET 8](https://devblogs.microsoft.com/dotnet/aspnet-core-route-tooling-dotnet-8/).

### ASP.NET Core metrics

Metrics are measurements reported over time and are most often used to monitor the health of an app and to generate alerts. For example, a counter that reports failed HTTP requests could be displayed in dashboards or generate alerts when failures pass a threshold.

This preview adds new metrics throughout ASP.NET Core using [System.Diagnostics.Metrics](/en-us/dotnet/api/system.diagnostics.metrics). `Metrics` is a modern API for reporting and collecting information about apps.

Metrics offers many improvements compared to existing event counters:

* New kinds of measurements with counters, gauges and histograms.
* Powerful reporting with multi-dimensional values.
* Integration into the wider cloud native ecosystem by aligning with OpenTelemetry standards.

Metrics have been added for ASP.NET Core hosting, Kestrel, and SignalR. For more information, see [System.Diagnostics.Metrics](/en-us/dotnet/core/diagnostics/compare-metric-apis#systemdiagnosticsmetrics).

### IExceptionHandler

[IExceptionHandler](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) is a new interface that gives the developer a callback for handling known exceptions in a central location.

`IExceptionHandler` implementations are registered by calling [`IServiceCollection.AddExceptionHandler<T>`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.exceptionhandlerservicecollectionextensions.addexceptionhandler). Multiple implementations can be added, and they're called in the order registered. If an exception handler handles a request, it can return `true` to stop processing. If an exception isn't handled by any exception handler, then control falls back to the default behavior and options from the middleware.

For more information, see [IExceptionHandler](../fundamentals/error-handling?view=aspnetcore-10.0#iexceptionhandler).

### Improved debugging experience

[Debug customization attributes](/en-us/visualstudio/debugger/using-the-debuggerdisplay-attribute) have been added to types like `HttpContext`, `HttpRequest`, `HttpResponse`, `ClaimsPrincipal`, and `WebApplication`. The enhanced debugger displays for these types make finding important information easier in an IDE's debugger. The following screenshots show the difference that these attributes make in the debugger's display of `HttpContext`.

.NET 7:

.NET 8:

The debugger display for `WebApplication` highlights important information such as configured endpoints, middleware, and `IConfiguration` values.

.NET 7:

.NET 8:

For more information about debugging improvements in .NET 8, see:

* [Debugging Enhancements in .NET 8](https://devblogs.microsoft.com/dotnet/debugging-enhancements-in-dotnet-8/)
* GitHub issue [dotnet/aspnetcore 48205](https://github.com/dotnet/aspnetcore/issues/48205)

### `IPNetwork.Parse` and `TryParse`

The new [Parse](/en-us/dotnet/api/system.net.ipnetwork.parse) and [TryParse](/en-us/dotnet/api/system.net.ipnetwork.tryparse) methods on [IPNetwork](/en-us/dotnet/api/system.net.ipnetwork) add support for creating an `IPNetwork` by using an input string in [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) or "slash notation".

Here are IPv4 examples:

```
// Using Parse
var network = IPNetwork.Parse("192.168.0.1/32");
```

```
// Using TryParse
bool success = IPNetwork.TryParse("192.168.0.1/32", out var network);
```

```
// Constructor equivalent
var network = new IPNetwork(IPAddress.Parse("192.168.0.1"), 32);
```

And here are examples for IPv6:

```
// Using Parse
var network = IPNetwork.Parse("2001:db8:3c4d::1/128");
```

```
// Using TryParse
bool success = IPNetwork.TryParse("2001:db8:3c4d::1/128", out var network);
```

```
// Constructor equivalent
var network = new IPNetwork(IPAddress.Parse("2001:db8:3c4d::1"), 128);
```

### Redis-based output caching

ASP.NET Core in .NET 8 adds support for using Redis as a distributed cache for output caching. Output caching is a feature that enables an app to cache the output of a Minimal API endpoint, controller action, or Razor Page. For more information, see [Output caching](../performance/caching/output?view=aspnetcore-10.0#cache-storage).

### Short-circuit middleware after routing

When routing matches an endpoint, it typically lets the rest of the middleware pipeline run before invoking the endpoint logic. Services can reduce resource usage by filtering out known requests early in the pipeline. Use the [ShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.builder.routeshortcircuitendpointconventionbuilderextensions.shortcircuit) extension method to cause routing to invoke the endpoint logic immediately and then end the request. For example, a given route might not need to go through authentication or CORS middleware. The following example short-circuits requests that match the `/short-circuit` route:

```
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
```

Use the [MapShortCircuit](/en-us/dotnet/api/microsoft.aspnetcore.routing.routeshortcircuitendpointroutebuilderextensions.mapshortcircuit) method to set up short-circuiting for multiple routes at once, by passing to it a params array of URL prefixes. For example, browsers and bots often probe servers for well known paths like `robots.txt` and `favicon.ico`. If the app doesn't have those files, one line of code can configure both routes:

```
app.MapShortCircuit(404, "robots.txt", "favicon.ico");
```

For more information, see [Short-circuit middleware after routing](../fundamentals/routing?view=aspnetcore-10.0#short-circuit-middleware-after-routing).

### HTTP logging middleware extensibility

The HTTP logging middleware has several new capabilities:

* [HttpLoggingFields.Duration](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingfields#microsoft-aspnetcore-httplogging-httploggingfields-duration): When enabled, the middleware emits a new log at the end of the request and response that measures the total time taken for processing. This new field has been added to the [HttpLoggingFields.All](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingfields#microsoft-aspnetcore-httplogging-httploggingfields-all) set.

* [HttpLoggingOptions.CombineLogs](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingoptions.combinelogs#microsoft-aspnetcore-httplogging-httploggingoptions-combinelogs): When enabled, the middleware consolidates all of its enabled logs for a request and response into one log at the end. A single log message includes the request, request body, response, response body, and duration.

* [IHttpLoggingInterceptor](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.ihttplogginginterceptor): A new interface for a service that can be implemented and registered (using [AddHttpLoggingInterceptor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httploggingservicesextensions.addhttplogginginterceptor)) to receive per-request and per-response callbacks for customizing what details get logged. Any endpoint-specific log settings are applied first and can then be overridden in these callbacks. An implementation can:

  * Inspect a request and response.
  * Enable or disable any [HttpLoggingFields](/en-us/dotnet/api/microsoft.aspnetcore.httplogging.httploggingfields).
  * Adjust how much of the request or response body is logged.
  * Add custom fields to the logs.

For more information, see [HTTP logging in .NET and ASP.NET Core](../fundamentals/http-logging/?view=aspnetcore-10.0).

### New APIs in ProblemDetails to support more resilient integrations

In .NET 7, the [ProblemDetails service](../fundamentals/error-handling?view=aspnetcore-10.0#problem-details) was introduced to improve the experience for generating error responses that comply with the [ProblemDetails specification](https://datatracker.ietf.org/doc/html/rfc7807). In .NET 8, a new API was added to make it easier to implement fallback behavior if [IProblemDetailsService](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice) isn't able to generate [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails). The following example illustrates use of the new [TryWriteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.iproblemdetailsservice.trywriteasync) API:

```
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async httpContext =>
    {
        var pds = httpContext.RequestServices.GetService<IProblemDetailsService>();
        if (pds == null
            || !await pds.TryWriteAsync(new() { HttpContext = httpContext }))
        {
            // Fallback behavior
            await httpContext.Response.WriteAsync("Fallback: An error occurred.");
        }
    });
});

app.MapGet("/exception", () =>
{
    throw new InvalidOperationException("Sample Exception");
});

app.MapGet("/", () => "Test by calling /exception");

app.Run();
```

For more information, see [IProblemDetailsService fallback](../fundamentals/error-handling-api?view=aspnetcore-10.0#iproblemdetailsservice-fallback)

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

## Related content

* [Announcing ASP.NET Core in .NET 8 (blog post)](https://devblogs.microsoft.com/dotnet/announcing-asp-net-core-in-dotnet-8/)
* [ASP.NET Core announcements and breaking changes (`aspnet/Announcements` GitHub repository)](https://github.com/aspnet/Announcements/issues)
* [.NET announcements and breaking changes (`dotnet/Announcements` GitHub repository)](https://github.com/dotnet/announcements/issues)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0001
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0001.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0001: Authorization middleware is incorrectly configured

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0001      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

An out of order call to [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) was detected in the application start up code.

## Rule description

For authorization to be effective for endpoint routes, the call to `UseAuthorization` should appear between the calls to `UseRouting` and `UseEndpoints`. In the absence of this, the fallback policy, if configured, will be used to authorize all requests.

Consider the following code:

```
app.UseStaticFiles();
app.UseAuthorization();

app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapDefaultControllerRoute();
});
```

The call to `UseAuthorization` appears before `UseRouting` and consequently is not endpoint aware.

## How to fix violations

Change the order in which the call to `UseAuthorization` and `UseRouting` are performed.

```
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapDefaultControllerRoute();
});
```

## When to suppress warnings

It is safe to suppress this rule if the call to `UseAuthorization` is intended to authorize the fallback policy on all outgoing requests, or is meant to authorize resources not routed using endpoint routing.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/claims
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/claims.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Claim-based authorization in ASP.NET Core

When an identity is created for an app user upon signing into an app, the identity provider may assign one or more [claims](/en-us/dotnet/api/system.security.claims.claim#remarks) to the user's identity. A claim is a name value pair that represents what the subject (a user, an app or service, or a device/computer) is, not what the subject can do. A claim can be evaluated by the app to determine access rights to data and other secured resources during the process of authorization and can also be used to make or express authentication decisions about a subject. An identity can contain multiple claims with multiple values and can contain multiple claims of the same type. This article explains how to add claims checks for authorization in an ASP.NET Core app.

This article uses Razor component examples and focuses on Blazor authorization scenarios. For additional Blazor guidance, see the [Related content](#related-content) section. For Razor Pages and MVC guidance, see the following resources:

* [Claim-based authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/claims?view=aspnetcore-10.0)
* [Claim-based authorization in ASP.NET Core MVC](../../mvc/security/authorization/claims?view=aspnetcore-10.0)

## Sample app

The Blazor Web App sample for this article is the [`BlazorWebAppAuthorization` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/BlazorWebAppAuthorization) ([how to download](../../overview?view=aspnetcore-10.0#how-to-download-a-sample)). The sample app uses seeded accounts with preconfigured claims to demonstrate most of the examples in this article. For more information, see the sample's README file (`README.md`).

Caution

This sample app uses an in-memory database to store user information, which isn't suitable for production scenarios. The sample app is intended for demonstration purposes only and shouldn't be used as a starting point for production apps.

## Add claim checks

Claim-based authorization checks:

* Are declarative and specify claims via policies that the current user must present to access the requested resource.
* Are applied to Razor components (examples in this article), [Razor Pages](../../razor-pages/security/authorization/claims?view=aspnetcore-10.0), or [MVC controllers or actions within a controller](../../mvc/security/authorization/claims?view=aspnetcore-10.0).

The [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component ([`AuthorizeView` component in Blazor documentation](../../blazor/security/?view=aspnetcore-10.0#authorizeview-component)) supports *policy-based* authorization, where the policy requires one or more claims. Alternatively, a claims-based authorization via one or more policy checks can be set up using [`[Authorize]` attributes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) in Razor components. The developer must build and register a policy expressing the claims requirements. This section covers basic concepts. For complete coverage, see [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0).

The simplest type of claim policy looks for the presence of a claim and doesn't check the value.

Registering the policy takes place as part of the Authorization service configuration in the app's `Program` file:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
```

Note

`WebApplicationBuilder.ConfigureApplication` ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/DefaultBuilder/src/WebApplicationBuilder.cs)) automatically adds a call for [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) when [IAuthorizationHandlerProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationhandlerprovider) is registered, which has been the behavior for ASP.NET Core since the release of .NET 8. Therefore, calling [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) explicitly for server-side Blazor apps in .NET 8 or later is technically redundant, but the call isn't harmful. Calling it in developer code after it has already been called by the framework merely no-ops.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

Registering the policy takes place as part of the Authorization service configuration in the app's `Program` file:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
```

In Blazor Server apps, call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) after the line that calls [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) (if present):

```
app.UseAuthentication(); // Only present if not called internally
app.UseAuthorization();
```

Registering the policy takes place as part of the Authorization service configuration in `Startup.ConfigureServices` (`Startup.cs`):

```
services.AddAuthorization(options =>
{
    options.AddPolicy("EmployeeOnly", policy =>
        policy.RequireClaim("EmployeeNumber"));
});
```

In Blazor Server apps, call [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) in `Startup.Configure` after the line that calls [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) (if present):

```
app.UseAuthentication(); // Only present if not called internally
app.UseAuthorization();
```

Blazor WebAssembly apps call [AddAuthorizationCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authorizationservicecollectionextensions.addauthorizationcore) in the `Program` file to add authorization services:

```
builder.Services.AddAuthorizationCore();
```

Apply the policy using the `Policy` property on the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to specify the policy name. In the following example, the `EmployeeOnly` policy checks for the presence of an `EmployeeNumber` claim on the current identity:

For policy-based authorization using an [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component, use the [AuthorizeView.Policy](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview.policy#microsoft-aspnetcore-components-authorization-authorizeview-policy) parameter with a single policy name.

`Pages/PassEmployeeOnlyPolicyWithAuthorizeView.razor`:

```
@page "/pass-employeeonly-policy-with-authorizeview"

<h1>Pass 'EmployeeOnly' policy with AuthorizeView</h1>

<AuthorizeView Policy="EmployeeOnly">
    <Authorized>
        <p>You satisfy the 'EmployeeOnly' policy.</p>
    </Authorized>
    <NotAuthorized>
        <p>You <b>don't</b> satisfy the 'EmployeeOnly' policy.</p>
    </NotAuthorized>
</AuthorizeView>
```

Alternatively, apply the policy using the `Policy` property on the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to specify the policy name. In the following example, the `EmployeeOnly` policy checks for the presence of an `EmployeeNumber` claim on the current identity:

`Pages/PassEmployeeOnlyPolicyWithAuthorizeAttribute.razor`:

```
@page "/pass-employeeonly-policy-with-authorize-attribute"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "EmployeeOnly")]

<h1>Pass 'EmployeeOnly' policy with [Authorize] attribute</h1>

<p>You satisfy the 'EmployeeOnly' policy.</p>
```

You can specify a list of allowed values when creating a policy. The following policy only passes for employees whose employee number is 1, 2, 3, 4, or 5:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("Founders", policy =>
        policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Founders", policy =>
                      policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("Founder", policy =>
        policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});
```

`Pages/PassFounderPolicyWithAuthorizeView.razor`:

```
@page "/pass-founder-policy-with-authorizeview"

<h1>Pass 'Founder' policy with AuthorizeView</h1>

<AuthorizeView Policy="Founder">
    <Authorized>
        <p>You satisfy the 'Founder' policy.</p>
    </Authorized>
    <NotAuthorized>
        <p>You <b>don't</b> satisfy the 'Founder' policy.</p>
    </NotAuthorized>
</AuthorizeView>
```

### Add a generic claim check

If the claim value isn't a single value or you need more flexible claim evaluation logic, such as pattern matching, checking the claim issuer, or parsing complex claim values, use [RequireAssertion](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireassertion) with [HasClaim](/en-us/dotnet/api/system.security.claims.claimsprincipal.hasclaim). For example, the following policy requires that the user's `email` claim ends with a specific domain:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ContosoOnly", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                c.Type == "email" &&
                c.Value.EndsWith("@contoso.com", StringComparison.OrdinalIgnoreCase))));
```

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ContosoOnly", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                c.Type == "email" &&
                c.Value.EndsWith("@contoso.com", StringComparison.OrdinalIgnoreCase))));
});
```

```
services.AddAuthorization(options =>
{
    options.AddPolicy("ContosoOnly", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                c.Type == "email" &&
                c.Value.EndsWith("@contoso.com", StringComparison.OrdinalIgnoreCase))));
});
```

For more information, see [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0#use-a-func-to-fulfill-a-policy).

## Evaluate multiple policies

Multiple policies are applied via multiple [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) components. The inner component requires the user to pass its policy and every policy of parent [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) components.

The following example:

* Requires a `CustomerServiceMember` policy, which indicates that the user is in the organization's customer service department because they have a `Department` claim with a value of `Customer Service`.
* Also requires a `HumanResourcesMember` policy, which indicates that the user is in the organization's human resources department because they have a `Department` claim with a value of `Human Resources`.

In the app's `Program` file:

```
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("CustomerServiceMember", policy =>
        policy.RequireClaim("Department", "Customer Service"))
    .AddPolicy("HumanResourcesMember", policy =>
        policy.RequireClaim("Department", "Human Resources"));
```

In the app's `Program` file:

```
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CustomerServiceMember", policy =>
        policy.RequireClaim("Department", "Customer Service"));
    options.AddPolicy("HumanResourcesMember", policy =>
        policy.RequireClaim("Department", "Human Resources"));
});
```

In `Startup.ConfigureServices` (`Startup.cs`):

```
services.AddAuthorization(options =>
{
    options.AddPolicy("CustomerServiceMember", policy =>
        policy.RequireClaim("Department", "Customer Service"));
    options.AddPolicy("HumanResourcesMember", policy =>
        policy.RequireClaim("Department", "Human Resources"));
});
```

The following example uses [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) components.

`Pages/PassCustomerServiceMemberAndHumanResourcesMemberPoliciesWithAuthorizeViews.razor`:

```
@page "/pass-customerservicemember-and-humanresourcesmember-policies-with-authorizeviews"

<h1>Pass 'CustomerServiceMember' and 'HumanResourcesMember' policies with AuthorizeViews</h1>

<AuthorizeView Policy="CustomerServiceMember">
    <Authorized>
        <p>User: @context.User.Identity?.Name</p>
        <AuthorizeView Policy="HumanResourcesMember" Context="innerContext">
            <Authorized>
                <p>
                    You satisfy the 'CustomerServiceMember' and 'HumanResourcesMember' policies.
                </p>
            </Authorized>
            <NotAuthorized>
                <p>
                    You satisfy the 'CustomerServiceMember' policy, but you <b>don't</b> satisfy 
                    the 'HumanResourcesMember' policy.
                </p>
            </NotAuthorized>
        </AuthorizeView>
    </Authorized>
    <NotAuthorized>
        <p>
            You <b>don't</b> satisfy the 'CustomerServiceMember' policy.
        </p>
    </NotAuthorized>
</AuthorizeView>
```

The following example uses [`[Authorize]` attributes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute).

`Pages/PassCustomerServiceMemberAndHumanResourcesMemberPoliciesWithAuthorizeAttributes.razor`:

```
@page "/pass-customerservicemember-and-humanresourcesmember-policies-with-authorize-attributes"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "CustomerServiceMember")]
@attribute [Authorize(Policy = "HumanResourcesMember")]

<h1>
    Pass 'CustomerServiceMember' and 'HumanResourcesMember' policies with [Authorize] attributes
</h1>

<p>
    You satisfy the 'CustomerServiceMember' and 'HumanResourcesMember' policies.
</p>
```

For more complicated policies, such as taking a date of birth claim, calculating an age from it, then checking that the age is 21 or older, use [RequireAssertion](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireassertion) or write [custom policy handlers](policies?view=aspnetcore-10.0). Custom policy handlers are useful when you need access to dependency-injected services or want a reusable, testable authorization component.

## Claim case sensitivity

Claim *values* are compared using [`StringComparison.Ordinal`](/en-us/dotnet/api/system.stringcomparison). This means `Admin` (uppercase `A`) and `admin` (lowercase `a`) are always treated as different roles, regardless of which authentication handler created the identity.

Separately, the claim *type* comparison (used to locate role claims by their claim type, such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/role`) may be case-sensitive or case-insensitive depending on the [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) implementation. With `Microsoft.IdentityModel` in ASP.NET Core 8.0 or later (used by [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer), [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect), [AddWsFederation](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.wsfederationextensions.addwsfederation), and [AddMicrosoftIdentityWebApp](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapp)/[AddMicrosoftIdentityWebApi](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapi)), [CaseSensitiveClaimsIdentity](/en-us/dotnet/api/microsoft.identitymodel.tokens.casesensitiveclaimsidentity) is produced during token validation, which uses case-sensitive claim type matching.

The default [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) provided by the .NET runtime (used in most cases, including all cookie-based flows) still uses case-insensitive claim type matching.

In practice, this distinction rarely matters for role authorization because the role claim type is set once during identity creation and matched consistently. Always use consistent casing for role names and claim types to avoid subtle issues.

## Related content

* [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor authentication state](../../blazor/security/authentication-state?view=aspnetcore-10.0)
* [ASP.NET Core Blazor WebAssembly additional security scenarios](../../blazor/security/webassembly/additional-scenarios?view=aspnetcore-10.0)
* [Use Graph API with ASP.NET Core Blazor WebAssembly](../../blazor/security/webassembly/graph-api?view=aspnetcore-10.0#customize-user-claims-using-the-graph-sdk)
* [Secure ASP.NET Core Blazor WebAssembly](../../blazor/security/webassembly/?view=aspnetcore-10.0#establish-claims-for-users)
* [Claim-based authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/claims?view=aspnetcore-10.0)
* [Claim-based authorization in ASP.NET Core MVC](../../mvc/security/authorization/claims?view=aspnetcore-10.0)
* [Extend or add custom claims, including role claims, using `IClaimsTransformation`](../authentication/claims?view=aspnetcore-10.0#extend-or-add-custom-claims-using-iclaimstransformation)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/bl0003
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/bl0003.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# BL0003: Component parameter with CaptureUnmatchedValues has the wrong type

|                                     | Value    |
| ----------------------------------- | -------- |
| **Rule ID**                         | BL0003   |
| **Category**                        | Usage    |
| **Fix is breaking or non-breaking** | Breaking |

## Cause

A parameter on a type deriving from [ComponentBase](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase) annotated with [CaptureUnmatchedValues](/en-us/dotnet/api/microsoft.aspnetcore.components.parameterattribute.captureunmatchedvalues#microsoft-aspnetcore-components-parameterattribute-captureunmatchedvalues)`= true` is not assignable from `Dictionary<string, object>`

## Rule description

Parameters annotated with `CaptureUnmatchedValues = true` must be able to receive a `Dictionary<string, object>` value.

```
@code
{
    [Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, string> Attributes { get; set; }
}
```

## How to fix violations

Change the type of the parameter to either `IDictionary<string, object>` or `Dictionary<string, object>`

```
@code
{
    [Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> Attributes { get; set; }
}
```

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-05-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/request-draining
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel/request-draining.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Request draining with ASP.NET Core Kestrel web server

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Opening HTTP connections is time consuming. For HTTPS, it's also resource intensive. Therefore, Kestrel tries to reuse connections per the HTTP/1.1 protocol. A request body must be fully consumed to allow the connection to be reused. The app doesn't always consume the request body, such as HTTP POST requests where the server returns a redirect or 404 response. In the HTTP POST redirect case:

* The client may already have sent part of the POST data.
* The server writes the 301 response.
* The connection can't be used for a new request until the POST data from the previous request body has been fully read.
* Kestrel tries to drain the request body. Draining the request body means reading and discarding the data without processing it.

The draining process makes a tradeoff between allowing the connection to be reused and the time it takes to drain any remaining data:

* Draining has a timeout of five seconds, which isn't configurable.
* If all of the data specified by the `Content-Length` or `Transfer-Encoding` header hasn't been read before the timeout, the connection is closed.

Sometimes you may want to terminate the request immediately, before or after writing the response. For example, clients may have restrictive data caps. Limiting uploaded data might be a priority. In such cases to terminate a request, call [HttpContext.Abort](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.abort) from a controller, Razor Page, or middleware.

There are caveats to calling `Abort`:

* Creating new connections can be slow and expensive.

* There's no guarantee that the client has read the response before the connection closes.

* Calling `Abort` should be rare and reserved for severe error cases, not common errors.

  * Only call `Abort` when a specific problem needs to be solved. For example, call `Abort` if malicious clients are trying to POST data or when there's a bug in client code that causes large or several requests.
  * Don't call `Abort` for common error situations, such as HTTP 404 (Not Found).

Calling [HttpResponse.CompleteAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.completeasync) before calling `Abort` ensures that the server has completed writing the response. However, client behavior isn't predictable and they may not read the response before the connection is aborted.

This process is different for HTTP/2 because the protocol supports aborting individual request streams without closing the connection. The five-second drain timeout doesn't apply. If there's any unread request body data after completing a response, then the server sends an HTTP/2 RST frame. Additional request body data frames are ignored.

If possible, it's better for clients to use the [Expect: 100-continue](https://developer.mozilla.org/docs/Web/HTTP/Status/100) request header and wait for the server to respond before starting to send the request body. That gives the client an opportunity to examine the response and abort before sending unneeded data.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/web-api/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/web-api/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Create web APIs with ASP.NET Core

ASP.NET Core supports creating web APIs using controllers or using Minimal APIs. *Controllers* in a web API are classes that derive from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase). Controllers are activated and disposed on a per request basis.

This article shows how to use controllers for handling web API requests. For information on creating web APIs without controllers, see [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0).

Important

Starting with ASP.NET Core 10, known API endpoints no longer redirect to login pages when using cookie authentication. Instead, they return 401/403 status codes. For details, see [API endpoint authentication behavior in ASP.NET Core](../security/authentication/api-endpoint-auth?view=aspnetcore-10.0).

## ControllerBase class

A controller-based web API consists of one or more controller classes that derive from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase). The web API project template provides a starter controller:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
```

Web API controllers should typically derive from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase) rather from [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller). `Controller` derives from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase) and adds support for views, so it's for handling web pages, not web API requests. If the same controller must support views and web APIs, derive from `Controller`.

The `ControllerBase` class provides many properties and methods that are useful for handling HTTP requests. For example, [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) returns a 201 status code:

```
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
    pet.Id = _petsInMemoryStore.Any() ? 
             _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
    _petsInMemoryStore.Add(pet);

    return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);
}
```

The following table contains examples of methods in `ControllerBase`.

| Method                                                                                               | Notes                                                                      |
| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest)                   | Returns 400 status code.                                                   |
| [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound)                       | Returns 404 status code.                                                   |
| [PhysicalFile](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.physicalfile)               | Returns a file.                                                            |
| [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) | Invokes [model binding](../mvc/models/model-binding?view=aspnetcore-10.0). |
| [TryValidateModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryvalidatemodel)       | Invokes [model validation](../mvc/models/validation?view=aspnetcore-10.0). |

For a list of all available methods and properties, see [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase).

## Attributes

The [Microsoft.AspNetCore.Mvc](/en-us/dotnet/api/microsoft.aspnetcore.mvc) namespace provides attributes that can be used to configure the behavior of web API controllers and action methods. The following example uses attributes to specify the supported HTTP action verb and any known HTTP status codes that could be returned:

```
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
    pet.Id = _petsInMemoryStore.Any() ? 
             _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
    _petsInMemoryStore.Add(pet);

    return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);
}
```

Here are some more examples of attributes that are available.

| Attribute                                                                    | Notes                                                         |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [`[Route]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute)       | Specifies URL pattern for a controller or action.             |
| [`[Bind]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindattribute)         | Specifies prefix and properties to include for model binding. |
| [`[HttpGet]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute)   | Identifies an action that supports the HTTP GET action verb.  |
| [`[Consumes]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) | Specifies data types that an action accepts.                  |
| [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) | Specifies data types that an action returns.                  |

For a list that includes the available attributes, see the [Microsoft.AspNetCore.Mvc](/en-us/dotnet/api/microsoft.aspnetcore.mvc) namespace.

## ApiController attribute

The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute can be applied to a controller class to enable the following opinionated, API-specific behaviors:

* [Attribute routing requirement](#attribute-routing-requirement)
* [Automatic HTTP 400 responses](#automatic-http-400-responses)
* [Binding source parameter inference](#binding-source-parameter-inference)
* [Multipart/form-data request inference](#multipartform-data-request-inference)
* [Problem details for error status codes](#problem-details-for-error-status-codes)

### Attribute on specific controllers

The `[ApiController]` attribute can be applied to specific controllers, as in the following example from the project template:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
```

### Attribute on multiple controllers

One approach to using the attribute on more than one controller is to create a custom base controller class annotated with the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute. The following example shows a custom base class and a controller that derives from it:

```
[ApiController]
public class MyControllerBase : ControllerBase
{
}
```

```
[Produces(MediaTypeNames.Application.Json)]
[Route("[controller]")]
public class PetsController : MyControllerBase
```

### Attribute on an assembly

The `[ApiController]` attribute can be applied to an assembly. When the `[ApiController]` attribute is applied to an assembly, all controllers in the assembly have the `[ApiController]` attribute applied. There's no way to opt out for individual controllers. Apply the assembly-level attribute to the `Program.cs` file:

```
using Microsoft.AspNetCore.Mvc;
[assembly: ApiController]

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## Attribute routing requirement

The `[ApiController]` attribute makes attribute routing a requirement. For example:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
```

Actions are inaccessible via [conventional routes](../mvc/controllers/routing?view=aspnetcore-10.0#conventional-routing) defined by `UseEndpoints`, [UseMvc](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvc), or [UseMvcWithDefaultRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvcwithdefaultroute).

## Automatic HTTP 400 responses

The `[ApiController]` attribute makes model validation errors automatically trigger an HTTP 400 response. Consequently, the following code is unnecessary in an action method:

```
if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}
```

ASP.NET Core MVC uses the [ModelStateInvalidFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.modelstateinvalidfilter) action filter to do the preceding check.

### Default BadRequest response

The default response type for an HTTP 400 response is [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails). The following response body is an example of the serialized type:

```
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "|7fb5e16a-4c8f23bbfc974667.",
  "errors": {
    "": [
      "A non-empty request body is required."
    ]
  }
}
```

The `ValidationProblemDetails` type:

* Provides a machine-readable format for specifying errors in web API responses.
* Complies with the [RFC 7807 specification](https://datatracker.ietf.org/doc/html/rfc7807).

To make automatic and custom responses consistent, call the [ValidationProblem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.validationproblem) method instead of [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest). `ValidationProblem` returns a [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails) object as well as the automatic response.

### Log automatic 400 responses

To log automatic 400 responses, set the [InvalidModelStateResponseFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.invalidmodelstateresponsefactory) delegate property to perform custom processing. By default, `InvalidModelStateResponseFactory` uses [ProblemDetailsFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.problemdetailsfactory) to create an instance of [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails).

The following example shows how to retrieve an instance of [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) to log information about an automatic 400 response:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
      // To preserve the default behavior, capture the original delegate to call later.
        var builtInFactory = options.InvalidModelStateResponseFactory;

        options.InvalidModelStateResponseFactory = context =>
        {
            var logger = context.HttpContext.RequestServices
                                .GetRequiredService<ILogger<Program>>();

            // Perform logging here.
            // ...

            // Invoke the default behavior, which produces a ValidationProblemDetails
            // response.
            // To produce a custom response, return a different implementation of 
            // IActionResult instead.
            return builtInFactory(context);
        };
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

### Disable automatic 400 response

To disable the automatic 400 behavior, set the [SuppressModelStateInvalidFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmodelstateinvalidfilter#microsoft-aspnetcore-mvc-apibehavioroptions-suppressmodelstateinvalidfilter) property to `true`. Add the following highlighted code:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## Binding source parameter inference

A binding source attribute defines the location at which an action parameter's value is found. The following binding source attributes exist:

| Attribute                                                                                                           | Binding source                                                                  |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute)                                        | Request body                                                                    |
| [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute)                                        | Form data in the request body                                                   |
| [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute)                                    | Request header                                                                  |
| [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute)                                      | Request query string parameter                                                  |
| [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute)                                      | Route data from the current request                                             |
| [`[FromServices]`](../mvc/controllers/dependency-injection?view=aspnetcore-10.0#action-injection-with-fromservices) | The request service injected as an action parameter                             |
| [`[AsParameters]`](/en-us/dotnet/api/microsoft.aspnetcore.http.asparametersattribute)                               | [Method parameters](../fundamentals/minimal-apis?view=aspnetcore-10.0#asparam7) |

Warning

Don't use `[FromRoute]` when values might contain `%2f` (that is `/`). `%2f` won't be unescaped to `/`. Use `[FromQuery]` if the value might contain `%2f`.

Without the `[ApiController]` attribute or binding source attributes like `[FromQuery]`, the ASP.NET Core runtime attempts to use the complex object model binder. The complex object model binder pulls data from value providers in a defined order.

In the following example, the `[FromQuery]` attribute indicates that the `discontinuedOnly` parameter value is provided in the request URL's query string:

```
[HttpGet]
public ActionResult<List<Product>> Get(
    [FromQuery] bool discontinuedOnly = false)
{
    List<Product> products = null;

    if (discontinuedOnly)
    {
        products = _productsInMemoryStore.Where(p => p.IsDiscontinued).ToList();
    }
    else
    {
        products = _productsInMemoryStore;
    }

    return products;
}
```

The `[ApiController]` attribute applies inference rules for the default data sources of action parameters. These rules save you from having to identify binding sources manually by applying attributes to the action parameters. The binding source inference rules behave as follows:

* `[FromServices]` is inferred for complex type parameters registered in the DI Container.
* `[FromBody]` is inferred for complex type parameters not registered in the DI Container. An exception to the `[FromBody]` inference rule is any complex, built-in type with a special meaning, such as [IFormCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection) and [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken). The binding source inference code ignores those special types.
* `[FromForm]` is inferred for action parameters of type [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection). It's not inferred for any simple or user-defined types.
* `[FromRoute]` is inferred for any action parameter name matching a parameter in the route template. When more than one route matches an action parameter, any route value is considered `[FromRoute]`.
* `[FromQuery]` is inferred for any other action parameters.

### FromBody inference notes

`[FromBody]` isn't inferred for simple types such as `string` or `int`. Therefore, the `[FromBody]` attribute should be used for simple types when that functionality is needed.

When an action has more than one parameter bound from the request body, an exception is thrown. For example, all of the following action method signatures cause an exception:

* `[FromBody]` inferred on both because they're complex types.

  ```
  [HttpPost]
  public IActionResult Action1(Product product, Order order)
  ```

* `[FromBody]` attribute on one, inferred on the other because it's a complex type.

  ```
  [HttpPost]
  public IActionResult Action2(Product product, [FromBody] Order order)
  ```

* `[FromBody]` attribute on both.

  ```
  [HttpPost]
  public IActionResult Action3([FromBody] Product product, [FromBody] Order order)
  ```

[]()

### FromServices inference notes

Parameter binding binds parameters through [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) when the type is configured as a service. This means it's not required to explicitly apply the [`[FromServices]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromservicesattribute) attribute to a parameter. In the following code, both actions return the time:

```
[Route("[controller]")]
[ApiController]
public class MyController : ControllerBase
{
    public ActionResult GetWithAttribute([FromServices] IDateTime dateTime) 
                                                        => Ok(dateTime.Now);

    [Route("noAttribute")]
    public ActionResult Get(IDateTime dateTime) => Ok(dateTime.Now);
}
```

In rare cases, automatic DI can break apps that have a type in DI that is also accepted in an API controller's action methods. It's not common to have a type in DI and as an argument in an API controller action.

To disable `[FromServices]` inference for a single action parameter, apply the desired binding source attribute to the parameter. For example, apply the `[FromBody]` attribute to an action parameter that should be bound from the body of the request.

To disable `[FromServices]` inference globally, set [DisableImplicitFromServicesParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.disableimplicitfromservicesparameters) to `true`:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.DisableImplicitFromServicesParameters = true;
});

var app = builder.Build();

app.MapControllers();

app.Run();
```

Types are checked at app startup with [IServiceProviderIsService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iserviceproviderisservice) to determine if an argument in an API controller action comes from DI or from the other sources.

The mechanism to infer binding source of API Controller action parameters uses the following rules:

* A previously specified [`BindingInfo.BindingSource`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindinginfo.bindingsource#microsoft-aspnetcore-mvc-modelbinding-bindinginfo-bindingsource) is never overwritten.
* A complex type parameter, registered in the DI container, is assigned [`BindingSource.Services`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindingsource.services#microsoft-aspnetcore-mvc-modelbinding-bindingsource-services).
* A complex type parameter, not registered in the DI container, is assigned [`BindingSource.Body`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindingsource.body#microsoft-aspnetcore-mvc-modelbinding-bindingsource-body).
* A parameter with a name that appears as a route value in ***any*** route template is assigned [`BindingSource.Path`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindingsource.path#microsoft-aspnetcore-mvc-modelbinding-bindingsource-path).
* All other parameters are [`BindingSource.Query`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindingsource.query#microsoft-aspnetcore-mvc-modelbinding-bindingsource-query).

### Disable inference rules

To disable binding source inference, set [SuppressInferBindingSourcesForParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressinferbindingsourcesforparameters#microsoft-aspnetcore-mvc-apibehavioroptions-suppressinferbindingsourcesforparameters) to `true`:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
        options.DisableImplicitFromServicesParameters = true;
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## Multipart/form-data request inference

The `[ApiController]` attribute applies an inference rule for action parameters of type [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection). The `multipart/form-data` request content type is inferred for these types.

To disable the default behavior, set the [SuppressConsumesConstraintForFormFileParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressconsumesconstraintforformfileparameters#microsoft-aspnetcore-mvc-apibehavioroptions-suppressconsumesconstraintforformfileparameters) property to `true`:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## Problem details for error status codes

MVC transforms an error result (a result with status code 400 or higher) to a result with [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails). The `ProblemDetails` type is based on the [RFC 7807 specification](https://datatracker.ietf.org/doc/html/rfc7807) for providing machine-readable error details in an HTTP response.

Consider the following code in a controller action:

```
if (pet == null)
{
    return NotFound();
}
```

The `NotFound` method produces an HTTP 404 status code with a `ProblemDetails` body. For example:

```
{
  type: "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  title: "Not Found",
  status: 404,
  traceId: "0HLHLV31KRN83:00000001"
}
```

### Disable ProblemDetails response

The automatic creation of a `ProblemDetails` for error status codes is disabled when the [SuppressMapClientErrors](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmapclienterrors) property is set to `true`. Add the following code:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

[]()

## Define supported request content types with the \[Consumes] attribute

By default, an action supports all available request content types. For example, if an app is configured to support both JSON and XML [input formatters](../mvc/models/model-binding?view=aspnetcore-10.0#input-formatters), an action supports multiple content types, including `application/json` and `application/xml`.

The [\[Consumes\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute allows an action to limit the supported request content types. Apply the `[Consumes]` attribute to an action or controller, specifying one or more content types:

```
[HttpPost]
[Consumes("application/xml")]
public IActionResult CreateProduct(Product product)
```

In the preceding code, the `CreateProduct` action specifies the content type `application/xml`. Requests routed to this action must specify a `Content-Type` header of `application/xml`. Requests that don't specify a `Content-Type` header of `application/xml` result in a [415 Unsupported Media Type](https://developer.mozilla.org/docs/Web/HTTP/Reference/Status/415) response.

The `[Consumes]` attribute also allows an action to influence its selection based on an incoming request's content type by applying a type constraint. Consider the following example:

```
[ApiController]
[Route("api/[controller]")]
public class ConsumesController : ControllerBase
{
    [HttpPost]
    [Consumes("application/json")]
    public IActionResult PostJson(IEnumerable<int> values) =>
        Ok(new { Consumes = "application/json", Values = values });

    [HttpPost]
    [Consumes("application/x-www-form-urlencoded")]
    public IActionResult PostForm([FromForm] IEnumerable<int> values) =>
        Ok(new { Consumes = "application/x-www-form-urlencoded", Values = values });
}
```

In the preceding code, `ConsumesController` is configured to handle requests sent to the `https://localhost:5001/api/Consumes` URL. Both of the controller's actions, `PostJson` and `PostForm`, handle POST requests with the same URL. Without the `[Consumes]` attribute applying a type constraint, an ambiguous match exception is thrown.

The `[Consumes]` attribute is applied to both actions. The `PostJson` action handles requests sent with a `Content-Type` header of `application/json`. The `PostForm` action handles requests sent with a `Content-Type` header of `application/x-www-form-urlencoded`.

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/index/samples). ([How to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).
* [Controller action return types in ASP.NET Core web API](action-return-types?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core APIs](../fundamentals/error-handling-api?view=aspnetcore-10.0)
* [Custom formatters in ASP.NET Core Web API](advanced/custom-formatters?view=aspnetcore-10.0)
* [Format response data in ASP.NET Core Web API](advanced/formatting?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)
* [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0)
* [Use port tunneling Visual Studio to debug web APIs](/en-us/connectors/custom-connectors/port-tunneling)
* [Create a web API with ASP.NET Core](/en-us/training/modules/build-web-api-aspnet-core/)

ASP.NET Core supports creating web APIs using controllers or using Minimal APIs. *Controllers* in a web API are classes that derive from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase). This article shows how to use controllers for handling web API requests. For information on creating web APIs without controllers, see [Tutorial: Create a Minimal API with ASP.NET Core](../tutorials/min-web-api?view=aspnetcore-10.0).

## ControllerBase class

A controller-based web API consists of one or more controller classes that derive from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase). The web API project template provides a starter controller:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
```

Web API controllers should typically derive from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase) rather from [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller). `Controller` derives from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase) and adds support for views, so it's for handling web pages, not web API requests. If the same controller must support views and web APIs, derive from `Controller`.

The `ControllerBase` class provides many properties and methods that are useful for handling HTTP requests. For example, [CreatedAtAction](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.createdataction) returns a 201 status code:

```
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
    pet.Id = _petsInMemoryStore.Any() ? 
             _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
    _petsInMemoryStore.Add(pet);

    return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);
}
```

The following table contains examples of methods in `ControllerBase`.

| Method                                                                                               | Notes                                                                      |
| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest)                   | Returns 400 status code.                                                   |
| [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound)                       | Returns 404 status code.                                                   |
| [PhysicalFile](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.physicalfile)               | Returns a file.                                                            |
| [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) | Invokes [model binding](../mvc/models/model-binding?view=aspnetcore-10.0). |
| [TryValidateModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryvalidatemodel)       | Invokes [model validation](../mvc/models/validation?view=aspnetcore-10.0). |

For a list of all available methods and properties, see [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase).

## Attributes

The [Microsoft.AspNetCore.Mvc](/en-us/dotnet/api/microsoft.aspnetcore.mvc) namespace provides attributes that can be used to configure the behavior of web API controllers and action methods. The following example uses attributes to specify the supported HTTP action verb and any known HTTP status codes that could be returned:

```
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
    pet.Id = _petsInMemoryStore.Any() ? 
             _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
    _petsInMemoryStore.Add(pet);

    return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);
}
```

Here are some more examples of attributes that are available.

| Attribute                                                                    | Notes                                                         |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [`[Route]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute)       | Specifies URL pattern for a controller or action.             |
| [`[Bind]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindattribute)         | Specifies prefix and properties to include for model binding. |
| [`[HttpGet]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute)   | Identifies an action that supports the HTTP GET action verb.  |
| [`[Consumes]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) | Specifies data types that an action accepts.                  |
| [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) | Specifies data types that an action returns.                  |

For a list that includes the available attributes, see the [Microsoft.AspNetCore.Mvc](/en-us/dotnet/api/microsoft.aspnetcore.mvc) namespace.

## ApiController attribute

The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute can be applied to a controller class to enable the following opinionated, API-specific behaviors:

* [Attribute routing requirement](#attribute-routing-requirement)
* [Automatic HTTP 400 responses](#automatic-http-400-responses)
* [Binding source parameter inference](#binding-source-parameter-inference)
* [Multipart/form-data request inference](#multipartform-data-request-inference)
* [Problem details for error status codes](#problem-details-for-error-status-codes)

### Attribute on specific controllers

The `[ApiController]` attribute can be applied to specific controllers, as in the following example from the project template:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
```

### Attribute on multiple controllers

One approach to using the attribute on more than one controller is to create a custom base controller class annotated with the [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute. The following example shows a custom base class and a controller that derives from it:

```
[ApiController]
public class MyControllerBase : ControllerBase
{
}
```

```
[Produces(MediaTypeNames.Application.Json)]
[Route("[controller]")]
public class PetsController : MyControllerBase
```

### Attribute on an assembly

The `[ApiController]` attribute can be applied to an assembly. When the `[ApiController]` attribute is applied to an assembly, all controllers in the assembly have the `[ApiController]` attribute applied. There's no way to opt out for individual controllers. Apply the assembly-level attribute to the `Program.cs` file:

```
using Microsoft.AspNetCore.Mvc;
[assembly: ApiController]

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## Attribute routing requirement

The `[ApiController]` attribute makes attribute routing a requirement. For example:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
```

Actions are inaccessible via [conventional routes](../mvc/controllers/routing?view=aspnetcore-10.0#conventional-routing) defined by `UseEndpoints`, [UseMvc](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvc), or [UseMvcWithDefaultRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvcwithdefaultroute).

## Automatic HTTP 400 responses

The `[ApiController]` attribute makes model validation errors automatically trigger an HTTP 400 response. Consequently, the following code is unnecessary in an action method:

```
if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}
```

ASP.NET Core MVC uses the [ModelStateInvalidFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.modelstateinvalidfilter) action filter to do the preceding check.

### Default BadRequest response

The following response body is an example of the serialized type:

```
{
  "": [
    "A non-empty request body is required."
  ]
}
```

The default response type for an HTTP 400 response is [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails). The following response body is an example of the serialized type:

```
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "|7fb5e16a-4c8f23bbfc974667.",
  "errors": {
    "": [
      "A non-empty request body is required."
    ]
  }
}
```

The `ValidationProblemDetails` type:

* Provides a machine-readable format for specifying errors in web API responses.
* Complies with the [RFC 7807 specification](https://tools.ietf.org/html/rfc7807).

To make automatic and custom responses consistent, call the [ValidationProblem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.validationproblem) method instead of [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest). `ValidationProblem` returns a [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails) object as well as the automatic response.

### Log automatic 400 responses

To log automatic 400 responses, set the [InvalidModelStateResponseFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.invalidmodelstateresponsefactory) delegate property to perform custom processing. By default, `InvalidModelStateResponseFactory` uses [ProblemDetailsFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.problemdetailsfactory) to create an instance of [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails).

The following example shows how to retrieve an instance of [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) to log information about an automatic 400 response:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
      // To preserve the default behavior, capture the original delegate to call later.
        var builtInFactory = options.InvalidModelStateResponseFactory;

        options.InvalidModelStateResponseFactory = context =>
        {
            var logger = context.HttpContext.RequestServices
                                .GetRequiredService<ILogger<Program>>();

            // Perform logging here.
            // ...

            // Invoke the default behavior, which produces a ValidationProblemDetails
            // response.
            // To produce a custom response, return a different implementation of 
            // IActionResult instead.
            return builtInFactory(context);
        };
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

### Disable automatic 400 response

To disable the automatic 400 behavior, set the [SuppressModelStateInvalidFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmodelstateinvalidfilter#microsoft-aspnetcore-mvc-apibehavioroptions-suppressmodelstateinvalidfilter) property to `true`. Add the following highlighted code:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## Binding source parameter inference

A binding source attribute defines the location at which an action parameter's value is found. The following binding source attributes exist:

| Attribute                                                                                                           | Binding source                                      |
| ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute)                                        | Request body                                        |
| [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute)                                        | Form data in the request body                       |
| [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute)                                    | Request header                                      |
| [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute)                                      | Request query string parameter                      |
| [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute)                                      | Route data from the current request                 |
| [`[FromServices]`](../mvc/controllers/dependency-injection?view=aspnetcore-10.0#action-injection-with-fromservices) | The request service injected as an action parameter |

Warning

Don't use `[FromRoute]` when values might contain `%2f` (that is `/`). `%2f` won't be unescaped to `/`. Use `[FromQuery]` if the value might contain `%2f`.

Without the `[ApiController]` attribute or binding source attributes like `[FromQuery]`, the ASP.NET Core runtime attempts to use the complex object model binder. The complex object model binder pulls data from value providers in a defined order.

In the following example, the `[FromQuery]` attribute indicates that the `discontinuedOnly` parameter value is provided in the request URL's query string:

```
[HttpGet]
public ActionResult<List<Product>> Get(
    [FromQuery] bool discontinuedOnly = false)
{
    List<Product> products = null;

    if (discontinuedOnly)
    {
        products = _productsInMemoryStore.Where(p => p.IsDiscontinued).ToList();
    }
    else
    {
        products = _productsInMemoryStore;
    }

    return products;
}
```

The `[ApiController]` attribute applies inference rules for the default data sources of action parameters. These rules save you from having to identify binding sources manually by applying attributes to the action parameters. The binding source inference rules behave as follows:

* `[FromBody]` is inferred for complex type parameters not registered in the DI Container. An exception to the `[FromBody]` inference rule is any complex, built-in type with a special meaning, such as [IFormCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection) and [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken). The binding source inference code ignores those special types.
* `[FromForm]` is inferred for action parameters of type [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection). It's not inferred for any simple or user-defined types.
* `[FromRoute]` is inferred for any action parameter name matching a parameter in the route template. When more than one route matches an action parameter, any route value is considered `[FromRoute]`.
* `[FromQuery]` is inferred for any other action parameters.

### FromBody inference notes

`[FromBody]` isn't inferred for simple types such as `string` or `int`. Therefore, the `[FromBody]` attribute should be used for simple types when that functionality is needed.

When an action has more than one parameter bound from the request body, an exception is thrown. For example, all of the following action method signatures cause an exception:

* `[FromBody]` inferred on both because they're complex types.

  ```
  [HttpPost]
  public IActionResult Action1(Product product, Order order)
  ```

* `[FromBody]` attribute on one, inferred on the other because it's a complex type.

  ```
  [HttpPost]
  public IActionResult Action2(Product product, [FromBody] Order order)
  ```

* `[FromBody]` attribute on both.

  ```
  [HttpPost]
  public IActionResult Action3([FromBody] Product product, [FromBody] Order order)
  ```

### Disable inference rules

To disable binding source inference, set [SuppressInferBindingSourcesForParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressinferbindingsourcesforparameters#microsoft-aspnetcore-mvc-apibehavioroptions-suppressinferbindingsourcesforparameters) to `true`:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## Multipart/form-data request inference

The `[ApiController]` attribute applies an inference rule for action parameters of type [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection). The `multipart/form-data` request content type is inferred for these types.

To disable the default behavior, set the [SuppressConsumesConstraintForFormFileParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressconsumesconstraintforformfileparameters#microsoft-aspnetcore-mvc-apibehavioroptions-suppressconsumesconstraintforformfileparameters) property to `true`:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## Problem details for error status codes

MVC transforms an error result (a result with status code 400 or higher) to a result with [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails). The `ProblemDetails` type is based on the [RFC 7807 specification](https://tools.ietf.org/html/rfc7807) for providing machine-readable error details in an HTTP response.

Consider the following code in a controller action:

```
if (pet == null)
{
    return NotFound();
}
```

The `NotFound` method produces an HTTP 404 status code with a `ProblemDetails` body. For example:

```
{
  type: "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  title: "Not Found",
  status: 404,
  traceId: "0HLHLV31KRN83:00000001"
}
```

### Disable ProblemDetails response

The automatic creation of a `ProblemDetails` for error status codes is disabled when the [SuppressMapClientErrors](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmapclienterrors) property is set to `true`:

```
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

[]()

## Define supported request content types with the \[Consumes] attribute

By default, an action supports all available request content types. For example, if an app is configured to support both JSON and XML [input formatters](../mvc/models/model-binding?view=aspnetcore-10.0#input-formatters), an action supports multiple content types, including `application/json` and `application/xml`.

The [\[Consumes\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute allows an action to limit the supported request content types. Apply the `[Consumes]` attribute to an action or controller, specifying one or more content types:

```
[HttpPost]
[Consumes("application/xml")]
public IActionResult CreateProduct(Product product)
```

In the preceding code, the `CreateProduct` action specifies the content type `application/xml`. Requests routed to this action must specify a `Content-Type` header of `application/xml`. Requests that don't specify a `Content-Type` header of `application/xml` result in a [415 Unsupported Media Type](https://developer.mozilla.org/docs/Web/HTTP/Status/415) response.

The `[Consumes]` attribute also allows an action to influence its selection based on an incoming request's content type by applying a type constraint. Consider the following example:

```
[ApiController]
[Route("api/[controller]")]
public class ConsumesController : ControllerBase
{
    [HttpPost]
    [Consumes("application/json")]
    public IActionResult PostJson(IEnumerable<int> values) =>
        Ok(new { Consumes = "application/json", Values = values });

    [HttpPost]
    [Consumes("application/x-www-form-urlencoded")]
    public IActionResult PostForm([FromForm] IEnumerable<int> values) =>
        Ok(new { Consumes = "application/x-www-form-urlencoded", Values = values });
}
```

In the preceding code, `ConsumesController` is configured to handle requests sent to the `https://localhost:5001/api/Consumes` URL. Both of the controller's actions, `PostJson` and `PostForm`, handle POST requests with the same URL. Without the `[Consumes]` attribute applying a type constraint, an ambiguous match exception is thrown.

The `[Consumes]` attribute is applied to both actions. The `PostJson` action handles requests sent with a `Content-Type` header of `application/json`. The `PostForm` action handles requests sent with a `Content-Type` header of `application/x-www-form-urlencoded`.

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/index/samples). ([How to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).
* [Controller action return types in ASP.NET Core web API](action-return-types?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core APIs](../fundamentals/error-handling-api?view=aspnetcore-10.0)
* [Custom formatters in ASP.NET Core Web API](advanced/custom-formatters?view=aspnetcore-10.0)
* [Format response data in ASP.NET Core Web API](advanced/formatting?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)
* [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0)
* [Use port tunneling Visual Studio to debug web APIs](/en-us/connectors/custom-connectors/port-tunneling)
* [Create a web API with ASP.NET Core](/en-us/training/modules/build-web-api-aspnet-core/)

ASP.NET Core supports creating RESTful services, also known as web APIs, using C#. To handle requests, a web API uses controllers. *Controllers* in a web API are classes that derive from `ControllerBase`. This article shows how to use controllers for handling web API requests.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/index/samples). ([How to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## ControllerBase class

A web API consists of one or more controller classes that derive from [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase). The web API project template provides a starter controller:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
```

Don't create a web API controller by deriving from the [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) class. `Controller` derives from `ControllerBase` and adds support for views, so it's for handling web pages, not web API requests. There's an exception to this rule: if you plan to use the same controller for both views and web APIs, derive it from `Controller`.

The `ControllerBase` class provides many properties and methods that are useful for handling HTTP requests. For example, `ControllerBase.CreatedAtAction` returns a 201 status code:

```
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
    pet.Id = _petsInMemoryStore.Any() ? 
             _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
    _petsInMemoryStore.Add(pet);

    return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);
}
```

Here are some more examples of methods that `ControllerBase` provides.

| Method                                                                                               | Notes                                                                      |
| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest)                   | Returns 400 status code.                                                   |
| [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound)                       | Returns 404 status code.                                                   |
| [PhysicalFile](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.physicalfile)               | Returns a file.                                                            |
| [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) | Invokes [model binding](../mvc/models/model-binding?view=aspnetcore-10.0). |
| [TryValidateModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryvalidatemodel)       | Invokes [model validation](../mvc/models/validation?view=aspnetcore-10.0). |

For a list of all available methods and properties, see [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase).

## Attributes

The [Microsoft.AspNetCore.Mvc](/en-us/dotnet/api/microsoft.aspnetcore.mvc) namespace provides attributes that can be used to configure the behavior of web API controllers and action methods. The following example uses attributes to specify the supported HTTP action verb and any known HTTP status codes that could be returned:

```
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
    pet.Id = _petsInMemoryStore.Any() ? 
             _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
    _petsInMemoryStore.Add(pet);

    return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);
}
```

Here are some more examples of attributes that are available.

| Attribute                                                                    | Notes                                                         |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [`[Route]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute)       | Specifies URL pattern for a controller or action.             |
| [`[Bind]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindattribute)         | Specifies prefix and properties to include for model binding. |
| [`[HttpGet]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute)   | Identifies an action that supports the HTTP GET action verb.  |
| [`[Consumes]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) | Specifies data types that an action accepts.                  |
| [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) | Specifies data types that an action returns.                  |

For a list that includes the available attributes, see the [Microsoft.AspNetCore.Mvc](/en-us/dotnet/api/microsoft.aspnetcore.mvc) namespace.

## ApiController attribute

The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute can be applied to a controller class to enable the following opinionated, API-specific behaviors:

* [Attribute routing requirement](#attribute-routing-requirement)
* [Automatic HTTP 400 responses](#automatic-http-400-responses)
* [Binding source parameter inference](#binding-source-parameter-inference)
* [Multipart/form-data request inference](#multipartform-data-request-inference)
* [Problem details for error status codes](#problem-details-for-error-status-codes)

### Attribute on specific controllers

The `[ApiController]` attribute can be applied to specific controllers, as in the following example from the project template:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
```

### Attribute on multiple controllers

One approach to using the attribute on more than one controller is to create a custom base controller class annotated with the `[ApiController]` attribute. The following example shows a custom base class and a controller that derives from it:

```
[ApiController]
public class MyControllerBase : ControllerBase
{
}
```

```
[Produces(MediaTypeNames.Application.Json)]
[Route("[controller]")]
public class PetsController : MyControllerBase
```

### Attribute on an assembly

The `[ApiController]` attribute can be applied to an assembly. Annotation in this manner applies web API behavior to all controllers in the assembly. There's no way to opt out for individual controllers. Apply the assembly-level attribute to the namespace declaration surrounding the `Startup` class:

```
[assembly: ApiController]
namespace WebApiSample
{
    public class Startup
    {
        ...
    }
}
```

## Attribute routing requirement

The `[ApiController]` attribute makes attribute routing a requirement. For example:

```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
```

Actions are inaccessible via [conventional routes](../mvc/controllers/routing?view=aspnetcore-10.0#conventional-routing) defined by `UseEndpoints`, [UseMvc](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvc), or [UseMvcWithDefaultRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvcwithdefaultroute) in `Startup.Configure`.

## Automatic HTTP 400 responses

The `[ApiController]` attribute makes model validation errors automatically trigger an HTTP 400 response. Consequently, the following code is unnecessary in an action method:

```
if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}
```

ASP.NET Core MVC uses the [ModelStateInvalidFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.modelstateinvalidfilter) action filter to do the preceding check.

### Default BadRequest response

The following request body is an example of the serialized type:

```
{
  "": [
    "A non-empty request body is required."
  ]
}
```

The default response type for an HTTP 400 response is [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails). The following request body is an example of the serialized type:

```
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "|7fb5e16a-4c8f23bbfc974667.",
  "errors": {
    "": [
      "A non-empty request body is required."
    ]
  }
}
```

The `ValidationProblemDetails` type:

* Provides a machine-readable format for specifying errors in web API responses.
* Complies with the [RFC 7807 specification](https://tools.ietf.org/html/rfc7807).

To make automatic and custom responses consistent, call the [ValidationProblem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.validationproblem) method instead of [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest). `ValidationProblem` returns a [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails) object as well as the automatic response.

### Log automatic 400 responses

To log automatic 400 responses, set the [InvalidModelStateResponseFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.invalidmodelstateresponsefactory) delegate property to perform custom processing in `Startup.ConfigureServices`. By default, `InvalidModelStateResponseFactory` uses [ProblemDetailsFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.problemdetailsfactory) to create an instance of [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails).

The following example shows how to retrieve an instance of [ILogger\<TCategoryName>](/en-us/dotnet/api/microsoft.extensions.logging.ilogger-1) to log information about an automatic 400 response:

```
services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        // To preserve the default behavior, capture the original delegate to call later.
        var builtInFactory = options.InvalidModelStateResponseFactory;

        options.InvalidModelStateResponseFactory = context =>
        {
            var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Startup>>();

            // Perform logging here.
            // ...

            // Invoke the default behavior, which produces a ValidationProblemDetails response.
            // To produce a custom response, return a different implementation of IActionResult instead.
            return builtInFactory(context);
        };
    });
```

### Disable automatic 400 response

To disable the automatic 400 behavior, set the [SuppressModelStateInvalidFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmodelstateinvalidfilter#microsoft-aspnetcore-mvc-apibehavioroptions-suppressmodelstateinvalidfilter) property to `true`. Add the following highlighted code in `Startup.ConfigureServices`:

```
services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
        options.DisableImplicitFromServicesParameters = true;
    });
```

## Binding source parameter inference

A binding source attribute defines the location at which an action parameter's value is found. The following binding source attributes exist:

| Attribute                                                                                                           | Binding source                                      |
| ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute)                                        | Request body                                        |
| [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute)                                        | Form data in the request body                       |
| [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute)                                    | Request header                                      |
| [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute)                                      | Request query string parameter                      |
| [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute)                                      | Route data from the current request                 |
| [`[FromServices]`](../mvc/controllers/dependency-injection?view=aspnetcore-10.0#action-injection-with-fromservices) | The request service injected as an action parameter |

Warning

Don't use `[FromRoute]` when values might contain `%2f` (that is `/`). `%2f` won't be unescaped to `/`. Use `[FromQuery]` if the value might contain `%2f`.

Without the `[ApiController]` attribute or binding source attributes like `[FromQuery]`, the ASP.NET Core runtime attempts to use the complex object model binder. The complex object model binder pulls data from value providers in a defined order.

In the following example, the `[FromQuery]` attribute indicates that the `discontinuedOnly` parameter value is provided in the request URL's query string:

```
[HttpGet]
public ActionResult<List<Product>> Get(
    [FromQuery] bool discontinuedOnly = false)
{
    List<Product> products = null;

    if (discontinuedOnly)
    {
        products = _productsInMemoryStore.Where(p => p.IsDiscontinued).ToList();
    }
    else
    {
        products = _productsInMemoryStore;
    }

    return products;
}
```

The `[ApiController]` attribute applies inference rules for the default data sources of action parameters. These rules save you from having to identify binding sources manually by applying attributes to the action parameters. The binding source inference rules behave as follows:

* `[FromBody]` is inferred for complex type parameters. An exception to the `[FromBody]` inference rule is any complex, built-in type with a special meaning, such as [IFormCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection) and [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken). The binding source inference code ignores those special types.
* `[FromForm]` is inferred for action parameters of type [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection). It's not inferred for any simple or user-defined types.
* `[FromRoute]` is inferred for any action parameter name matching a parameter in the route template. When more than one route matches an action parameter, any route value is considered `[FromRoute]`.
* `[FromQuery]` is inferred for any other action parameters.

### FromBody inference notes

`[FromBody]` isn't inferred for simple types such as `string` or `int`. Therefore, the `[FromBody]` attribute should be used for simple types when that functionality is needed.

When an action has more than one parameter bound from the request body, an exception is thrown. For example, all of the following action method signatures cause an exception:

* `[FromBody]` inferred on both because they're complex types.

  ```
  [HttpPost]
  public IActionResult Action1(Product product, Order order)
  ```

* `[FromBody]` attribute on one, inferred on the other because it's a complex type.

  ```
  [HttpPost]
  public IActionResult Action2(Product product, [FromBody] Order order)
  ```

* `[FromBody]` attribute on both.

  ```
  [HttpPost]
  public IActionResult Action3([FromBody] Product product, [FromBody] Order order)
  ```

### Disable inference rules

To disable binding source inference, set [SuppressInferBindingSourcesForParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressinferbindingsourcesforparameters#microsoft-aspnetcore-mvc-apibehavioroptions-suppressinferbindingsourcesforparameters) to `true`. Add the following code in `Startup.ConfigureServices`:

```
services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
        options.DisableImplicitFromServicesParameters = true;
    });
```

## Multipart/form-data request inference

The `[ApiController]` attribute applies an inference rule for action parameters of type [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection). The `multipart/form-data` request content type is inferred for these types.

To disable the default behavior, set the [SuppressConsumesConstraintForFormFileParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressconsumesconstraintforformfileparameters#microsoft-aspnetcore-mvc-apibehavioroptions-suppressconsumesconstraintforformfileparameters) property to `true` in `Startup.ConfigureServices`:

```
services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
        options.DisableImplicitFromServicesParameters = true;
    });
```

## Problem details for error status codes

MVC transforms an error result (a result with status code 400 or higher) to a result with [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails). The `ProblemDetails` type is based on the [RFC 7807 specification](https://tools.ietf.org/html/rfc7807) for providing machine-readable error details in an HTTP response.

Consider the following code in a controller action:

```
if (pet == null)
{
    return NotFound();
}
```

The `NotFound` method produces an HTTP 404 status code with a `ProblemDetails` body. For example:

```
{
  type: "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  title: "Not Found",
  status: 404,
  traceId: "0HLHLV31KRN83:00000001"
}
```

### Disable ProblemDetails response

The automatic creation of a `ProblemDetails` for error status codes is disabled when the [SuppressMapClientErrors](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmapclienterrors) property is set to `true`. Add the following code in `Startup.ConfigureServices`:

```
services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
        options.DisableImplicitFromServicesParameters = true;
    });
```

[]()

## Define supported request content types with the \[Consumes] attribute

By default, an action supports all available request content types. For example, if an app is configured to support both JSON and XML [input formatters](../mvc/models/model-binding?view=aspnetcore-10.0#input-formatters), an action supports multiple content types, including `application/json` and `application/xml`.

The [\[Consumes\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute allows an action to limit the supported request content types. Apply the `[Consumes]` attribute to an action or controller, specifying one or more content types:

```
[HttpPost]
[Consumes("application/xml")]
public IActionResult CreateProduct(Product product)
```

In the preceding code, the `CreateProduct` action specifies the content type `application/xml`. Requests routed to this action must specify a `Content-Type` header of `application/xml`. Requests that don't specify a `Content-Type` header of `application/xml` result in a [415 Unsupported Media Type](https://developer.mozilla.org/docs/Web/HTTP/Status/415) response.

The `[Consumes]` attribute also allows an action to influence its selection based on an incoming request's content type by applying a type constraint. Consider the following example:

```
[ApiController]
[Route("api/[controller]")]
public class ConsumesController : ControllerBase
{
    [HttpPost]
    [Consumes("application/json")]
    public IActionResult PostJson(IEnumerable<int> values) =>
        Ok(new { Consumes = "application/json", Values = values });

    [HttpPost]
    [Consumes("application/x-www-form-urlencoded")]
    public IActionResult PostForm([FromForm] IEnumerable<int> values) =>
        Ok(new { Consumes = "application/x-www-form-urlencoded", Values = values });
}
```

In the preceding code, `ConsumesController` is configured to handle requests sent to the `https://localhost:5001/api/Consumes` URL. Both of the controller's actions, `PostJson` and `PostForm`, handle POST requests with the same URL. Without the `[Consumes]` attribute applying a type constraint, an ambiguous match exception is thrown.

The `[Consumes]` attribute is applied to both actions. The `PostJson` action handles requests sent with a `Content-Type` header of `application/json`. The `PostForm` action handles requests sent with a `Content-Type` header of `application/x-www-form-urlencoded`.

## Additional resources

* [Controller action return types in ASP.NET Core web API](action-return-types?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core APIs](../fundamentals/error-handling-api?view=aspnetcore-10.0)
* [Custom formatters in ASP.NET Core Web API](advanced/custom-formatters?view=aspnetcore-10.0)
* [Format response data in ASP.NET Core Web API](advanced/formatting?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)
* [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0)
* [Create a web API with ASP.NET Core](/en-us/training/modules/build-web-api-aspnet-core/)

```
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
```

Don't create a web API controller by deriving from the [Controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller) class. `Controller` derives from `ControllerBase` and adds support for views, so it's for handling web pages, not web API requests. There's an exception to this rule: if you plan to use the same controller for both views and web APIs, derive it from `Controller`. The `ControllerBase` class provides many properties and methods that are useful for handling HTTP requests. For example, `ControllerBase.CreatedAtAction` returns a 201 status code:

```
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
    pet.Id = _petsInMemoryStore.Any() ? 
             _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
    _petsInMemoryStore.Add(pet);

    return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);
}
```

Here are some more examples of methods that `ControllerBase` provides:

| Method                                                                                               | Notes                                                                      |
| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| [BadRequest](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.badrequest)                   | Returns 400 status code.                                                   |
| [NotFound](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.notfound)                       | Returns 404 status code.                                                   |
| [PhysicalFile](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.physicalfile)               | Returns a file.                                                            |
| [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) | Invokes [model binding](../mvc/models/model-binding?view=aspnetcore-10.0). |
| [TryValidateModel](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryvalidatemodel)       | Invokes [model validation](../mvc/models/validation?view=aspnetcore-10.0). |

For a list of all available methods and properties, see [ControllerBase](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase).

## Attributes

The [Microsoft.AspNetCore.Mvc](/en-us/dotnet/api/microsoft.aspnetcore.mvc) namespace provides attributes that can be used to configure the behavior of web API controllers and action methods. The following example uses attributes to specify the supported HTTP action verb and any known HTTP status codes that could be returned:

```
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<Pet> Create(Pet pet)
{
    pet.Id = _petsInMemoryStore.Any() ? 
             _petsInMemoryStore.Max(p => p.Id) + 1 : 1;
    _petsInMemoryStore.Add(pet);

    return CreatedAtAction(nameof(GetById), new { id = pet.Id }, pet);
}
```

Here are some more examples of attributes that are available:

| Attribute                                                                    | Notes                                                         |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [`[Route]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute)       | Specifies URL pattern for a controller or action.             |
| [`[Bind]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindattribute)         | Specifies prefix and properties to include for model binding. |
| [`[HttpGet]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute)   | Identifies an action that supports the HTTP GET action verb.  |
| [`[Consumes]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) | Specifies data types that an action accepts.                  |
| [`[Produces]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute) | Specifies data types that an action returns.                  |

For a list that includes the available attributes, see the [Microsoft.AspNetCore.Mvc](/en-us/dotnet/api/microsoft.aspnetcore.mvc) namespace.

## ApiController attribute

The [`[ApiController]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute can be applied to a controller class to enable the following opinionated, API-specific behaviors:

* [Attribute routing requirement](#attribute-routing-requirement)
* [Automatic HTTP 400 responses](#automatic-http-400-responses)
* [Binding source parameter inference](#binding-source-parameter-inference)
* [Multipart/form-data request inference](#multipartform-data-request-inference)
* [Problem details for error status codes](#problem-details-for-error-status-codes) The *Problem details for error status codes* feature requires a [compatibility version](../mvc/compatibility-version?view=aspnetcore-10.0) of 2.2 or later. The other features require a compatibility version of 2.1 or later.
* [Attribute routing requirement](#attribute-routing-requirement)
* [Automatic HTTP 400 responses](#automatic-http-400-responses)
* [Binding source parameter inference](#binding-source-parameter-inference)
* [Multipart/form-data request inference](#multipartform-data-request-inference) These features require a [compatibility version](../mvc/compatibility-version?view=aspnetcore-10.0) of 2.1 or later.

### Attribute on specific controllers

The `[ApiController]` attribute can be applied to specific controllers, as in the following example from the project template:

```
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
```

### Attribute on multiple controllers

One approach to using the attribute on more than one controller is to create a custom base controller class annotated with the `[ApiController]` attribute. The following example shows a custom base class and a controller that derives from it:

```
[ApiController]
public class MyControllerBase : ControllerBase
{
}
```

```
[Produces(MediaTypeNames.Application.Json)]
[Route("api/[controller]")]
public class PetsController : MyControllerBase
```

### Attribute on an assembly

If [compatibility version](../mvc/compatibility-version?view=aspnetcore-10.0) is set to 2.2 or later, the `[ApiController]` attribute can be applied to an assembly. Annotation in this manner applies web API behavior to all controllers in the assembly. There's no way to opt out for individual controllers. Apply the assembly-level attribute to the namespace declaration surrounding the `Startup` class:

```
[assembly: ApiController]
namespace WebApiSample
{
    public class Startup
    {
        ...
    }
}
```

## Attribute routing requirement

The `[ApiController]` attribute makes attribute routing a requirement. For example:

```
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
```

Actions are inaccessible via [conventional routes](../mvc/controllers/routing?view=aspnetcore-10.0#conventional-routing) defined by [UseMvc](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvc) or [UseMvcWithDefaultRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcapplicationbuilderextensions.usemvcwithdefaultroute) in `Startup.Configure`.

## Automatic HTTP 400 responses

The `[ApiController]` attribute makes model validation errors automatically trigger an HTTP 400 response. Consequently, the following code is unnecessary in an action method:

```
if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}
```

ASP.NET Core MVC uses the [ModelStateInvalidFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.modelstateinvalidfilter) action filter to do the preceding check.

### Default BadRequest response

With a compatibility version of 2.1, the default response type for an HTTP 400 response is [SerializableError](/en-us/dotnet/api/microsoft.aspnetcore.mvc.serializableerror). The following request body is an example of the serialized type:

```
{
  "": [
    "A non-empty request body is required."
  ]
}
```

With a compatibility version of 2.2 or later, the default response type for an HTTP 400 response is [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails). The following request body is an example of the serialized type:

```
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "|7fb5e16a-4c8f23bbfc974667.",
  "errors": {
    "": [
      "A non-empty request body is required."
    ]
  }
}
```

The `ValidationProblemDetails` type:

* Provides a machine-readable format for specifying errors in web API responses.
* Complies with the [RFC 7807 specification](https://tools.ietf.org/html/rfc7807).

To make automatic and custom responses consistent, call the [ValidationProblem](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.validationproblem) method instead of [BadRequest](/en-us/dotnet/api/system.web.http.apicontroller.badrequest). `ValidationProblem` returns a [ValidationProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails) object as well as the automatic response.

### Log automatic 400 responses

See [How to log automatic 400 responses on model validation errors (dotnet/AspNetCore.Docs#12157)](https://github.com/dotnet/AspNetCore.Docs/issues/12157).

### Disable automatic 400 response

To disable the automatic 400 behavior, set the [SuppressModelStateInvalidFilter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmodelstateinvalidfilter#microsoft-aspnetcore-mvc-apibehavioroptions-suppressmodelstateinvalidfilter) property to `true`. Add the following highlighted code in `Startup.ConfigureServices`:

```
services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressConsumesConstraintForFormFileParameters = true;
    options.SuppressInferBindingSourcesForParameters = true;
    options.SuppressModelStateInvalidFilter = true;
});
```

## Binding source parameter inference

A binding source attribute defines the location at which an action parameter's value is found. The following binding source attributes exist:

| Attribute                                                                                                           | Binding source                                      |
| ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute)                                        | Request body                                        |
| [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute)                                        | Form data in the request body                       |
| [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute)                                    | Request header                                      |
| [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute)                                      | Request query string parameter                      |
| [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute)                                      | Route data from the current request                 |
| [`[FromServices]`](../mvc/controllers/dependency-injection?view=aspnetcore-10.0#action-injection-with-fromservices) | The request service injected as an action parameter |

Warning

Don't use `[FromRoute]` when values might contain `%2f` (that is `/`). `%2f` won't be unescaped to `/`. Use `[FromQuery]` if the value might contain `%2f`. Without the `[ApiController]` attribute or binding source attributes like `[FromQuery]`, the ASP.NET Core runtime attempts to use the complex object model binder. The complex object model binder pulls data from value providers in a defined order.

In the following example, the `[FromQuery]` attribute indicates that the `discontinuedOnly` parameter value is provided in the request URL's query string:

```
[HttpGet]
public ActionResult<List<Product>> Get(
    [FromQuery] bool discontinuedOnly = false)
{
    List<Product> products = null;

    if (discontinuedOnly)
    {
        products = _productsInMemoryStore.Where(p => p.IsDiscontinued).ToList();
    }
    else
    {
        products = _productsInMemoryStore;
    }

    return products;
}
```

The `[ApiController]` attribute applies inference rules for the default data sources of action parameters. These rules save you from having to identify binding sources manually by applying attributes to the action parameters. The binding source inference rules behave as follows:

* `[FromBody]` is inferred for complex type parameters. An exception to the `[FromBody]` inference rule is any complex, built-in type with a special meaning, such as [IFormCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformcollection) and [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken). The binding source inference code ignores those special types.
* `[FromForm]` is inferred for action parameters of type [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection). It's not inferred for any simple or user-defined types.
* `[FromRoute]` is inferred for any action parameter name matching a parameter in the route template. When more than one route matches an action parameter, any route value is considered `[FromRoute]`.
* `[FromQuery]` is inferred for any other action parameters.

### FromBody inference notes

`[FromBody]` isn't inferred for simple types such as `string` or `int`. Therefore, the `[FromBody]` attribute should be used for simple types when that functionality is needed.

When an action has more than one parameter bound from the request body, an exception is thrown. For example, all of the following action method signatures cause an exception:

* `[FromBody]` inferred on both because they're complex types.

  ```
  [HttpPost]
  public IActionResult Action1(Product product, Order order)
  ```

* `[FromBody]` attribute on one, inferred on the other because it's a complex type.

  ```
  [HttpPost]
  public IActionResult Action2(Product product, [FromBody] Order order)
  ```

* `[FromBody]` attribute on both.

  ```
  [HttpPost]
  public IActionResult Action3([FromBody] Product product, [FromBody] Order order)
  ```

Note

In ASP.NET Core 2.1, collection type parameters such as lists and arrays are incorrectly inferred as `[FromQuery]`. The `[FromBody]` attribute should be used for these parameters if they are to be bound from the request body. This behavior is corrected in ASP.NET Core 2.2 or later, where collection type parameters are inferred to be bound from the body by default.

### Disable inference rules

To disable binding source inference, set [SuppressInferBindingSourcesForParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressinferbindingsourcesforparameters#microsoft-aspnetcore-mvc-apibehavioroptions-suppressinferbindingsourcesforparameters) to `true`. Add the following code in `Startup.ConfigureServices`:

```
services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressConsumesConstraintForFormFileParameters = true;
    options.SuppressInferBindingSourcesForParameters = true;
    options.SuppressModelStateInvalidFilter = true;
});
```

## Multipart/form-data request inference

The `[ApiController]` attribute applies an inference rule for action parameters of type [IFormFile](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfile) and [IFormFileCollection](/en-us/dotnet/api/microsoft.aspnetcore.http.iformfilecollection). The `multipart/form-data` request content type is inferred for these types. To disable the default behavior, set the [SuppressConsumesConstraintForFormFileParameters](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressconsumesconstraintforformfileparameters#microsoft-aspnetcore-mvc-apibehavioroptions-suppressconsumesconstraintforformfileparameters) property to `true` in `Startup.ConfigureServices`:

```
services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressConsumesConstraintForFormFileParameters = true;
    options.SuppressInferBindingSourcesForParameters = true;
    options.SuppressModelStateInvalidFilter = true;
});
```

## Problem details for error status codes

When the compatibility version is 2.2 or later, MVC transforms an error result (a result with status code 400 or higher) to a result with [ProblemDetails](/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails). The `ProblemDetails` type is based on the [RFC 7807 specification](https://tools.ietf.org/html/rfc7807) for providing machine-readable error details in an HTTP response. Consider the following code in a controller action:

```
if (pet == null)
{
    return NotFound();
}
```

The `NotFound` method produces an HTTP 404 status code with a `ProblemDetails` body. For example:

```
{
  type: "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  title: "Not Found",
  status: 404,
  traceId: "0HLHLV31KRN83:00000001"
}
```

### Disable ProblemDetails response

The automatic creation of a `ProblemDetails` for error status codes is disabled when the [SuppressMapClientErrors](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmapclienterrors) property is set to `true`. Add the following code in `Startup.ConfigureServices`:

[]()

## Define supported request content types with the \[Consumes] attribute

By default, an action supports all available request content types. For example, if an app is configured to support both JSON and XML [input formatters](../mvc/models/model-binding?view=aspnetcore-10.0#input-formatters), an action supports multiple content types, including `application/json` and `application/xml`.

The [\[Consumes\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute allows an action to limit the supported request content types. Apply the `[Consumes]` attribute to an action or controller, specifying one or more content types:

```
[HttpPost]
[Consumes("application/xml")]
public IActionResult CreateProduct(Product product)
```

In the preceding code, the `CreateProduct` action specifies the content type `application/xml`. Requests routed to this action must specify a `Content-Type` header of `application/xml`. Requests that don't specify a `Content-Type` header of `application/xml` result in a [415 Unsupported Media Type](https://developer.mozilla.org/docs/Web/HTTP/Status/415) response. The `[Consumes]` attribute also allows an action to influence its selection based on an incoming request's content type by applying a type constraint. Consider the following example:

```
[ApiController]
[Route("api/[controller]")]
public class ConsumesController : ControllerBase
{
    [HttpPost]
    [Consumes("application/json")]
    public IActionResult PostJson(IEnumerable<int> values) =>
        Ok(new { Consumes = "application/json", Values = values });

    [HttpPost]
    [Consumes("application/x-www-form-urlencoded")]
    public IActionResult PostForm([FromForm] IEnumerable<int> values) =>
        Ok(new { Consumes = "application/x-www-form-urlencoded", Values = values });
}
```

In the preceding code, `ConsumesController` is configured to handle requests sent to the `https://localhost:5001/api/Consumes` URL. Both of the controller's actions, `PostJson` and `PostForm`, handle POST requests with the same URL. Without the `[Consumes]` attribute applying a type constraint, an ambiguous match exception is thrown. The `[Consumes]` attribute is applied to both actions. The `PostJson` action handles requests sent with a `Content-Type` header of `application/json`. The `PostForm` action handles requests sent with a `Content-Type` header of `application/x-www-form-urlencoded`.

## Additional resources

* [Controller action return types in ASP.NET Core web API](action-return-types?view=aspnetcore-10.0)
* [Handle errors in ASP.NET Core APIs](../fundamentals/error-handling-api?view=aspnetcore-10.0)
* [Custom formatters in ASP.NET Core Web API](advanced/custom-formatters?view=aspnetcore-10.0)
* [Format response data in ASP.NET Core Web API](advanced/formatting?view=aspnetcore-10.0)
* [ASP.NET Core web API documentation with Swagger / OpenAPI](../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0)
* [Routing to controller actions in ASP.NET Core](../mvc/controllers/routing?view=aspnetcore-10.0)
* [Create a web API with ASP.NET Core](/en-us/training/modules/build-web-api-aspnet-core/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/app-parts
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/advanced/app-parts.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Share controllers, views, Razor Pages and more with Application Parts

By [Rick Anderson](https://twitter.com/RickAndMSFT)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/advanced/app-parts) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

An *Application Part* is an abstraction over the resources of an app. Application Parts allow ASP.NET Core to discover controllers, view components, tag helpers, Razor Pages, razor compilation sources, and more. [AssemblyPart](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.assemblypart) is an Application part. `AssemblyPart` encapsulates an assembly reference and exposes types and compilation references.

[Feature providers](#fp) work with application parts to populate the features of an ASP.NET Core app. The main use case for application parts is to configure an app to discover (or avoid loading) ASP.NET Core features from an assembly. For example, you might want to share common functionality between multiple apps. Using Application Parts, you can share an assembly (DLL) containing controllers, views, Razor Pages, razor compilation sources, Tag Helpers, and more with multiple apps. Sharing an assembly is preferred to duplicating code in multiple projects.

ASP.NET Core apps load features from [ApplicationPart](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.applicationpart). The [AssemblyPart](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.assemblypart) class represents an application part that's backed by an assembly.

## Load ASP.NET Core features

Use the [Microsoft.AspNetCore.Mvc.ApplicationParts](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts) and [AssemblyPart](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.assemblypart) classes to discover and load ASP.NET Core features (controllers, view components, etc.). The [ApplicationPartManager](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.applicationpartmanager) tracks the application parts and feature providers available. `ApplicationPartManager` is configured in `Startup.ConfigureServices`:

```
// Requires using System.Reflection;
public void ConfigureServices(IServiceCollection services)
{
    var assembly = typeof(MySharedController).Assembly;
    services.AddControllersWithViews()
        .AddApplicationPart(assembly)
        .AddRazorRuntimeCompilation();

    services.Configure<MvcRazorRuntimeCompilationOptions>(options => 
    { options.FileProviders.Add(new EmbeddedFileProvider(assembly)); });
}
```

The following code provides an alternative approach to configuring `ApplicationPartManager` using `AssemblyPart`:

```
// Requires using System.Reflection;
// Requires using Microsoft.AspNetCore.Mvc.ApplicationParts;
public void ConfigureServices(IServiceCollection services)
{
    var assembly = typeof(MySharedController).Assembly;
    // This creates an AssemblyPart, but does not create any related parts for items such as views.
    var part = new AssemblyPart(assembly);
    services.AddControllersWithViews()
        .ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
}
```

The preceding two code samples load the `SharedController` from an assembly. The `SharedController` is not in the app's project. See the [WebAppParts solution](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/advanced/app-parts/samples/3.0-1/WebAppParts) sample download.

### Include views

Use a [Razor class library](../../razor-pages/ui-class?view=aspnetcore-10.0) to include views in the assembly.

### Prevent loading resources

Application parts can be used to *avoid* loading resources in a particular assembly or location. Add or remove members of the [Microsoft.AspNetCore.Mvc.ApplicationParts](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts) collection to hide or make available resources. The order of the entries in the `ApplicationParts` collection isn't important. Configure the `ApplicationPartManager` before using it to configure services in the container. For example, configure the `ApplicationPartManager` before invoking `AddControllersAsServices`. Call `Remove` on the `ApplicationParts` collection to remove a resource.

The `ApplicationPartManager` includes parts for:

* The app's assembly and dependent assemblies.
* `Microsoft.AspNetCore.Mvc.ApplicationParts.CompiledRazorAssemblyPart`
* `Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation`
* `Microsoft.AspNetCore.Mvc.TagHelpers`.
* `Microsoft.AspNetCore.Mvc.Razor`.

[]()

## Feature providers

Application feature providers examine application parts and provide features for those parts. There are built-in feature providers for the following ASP.NET Core features:

* [ControllerFeatureProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllers.controllerfeatureprovider)
* [TagHelperFeatureProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.taghelpers.taghelperfeatureprovider)
* [MetadataReferenceFeatureProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.compilation.metadatareferencefeatureprovider)
* [ViewsFeatureProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.compilation.viewsfeatureprovider)
* `internal class` [RazorCompiledItemFeatureProvider](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Razor/src/ApplicationParts/RazorCompiledItemFeatureProvider.cs#L14)

Feature providers inherit from [IApplicationFeatureProvider\<TFeature>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.iapplicationfeatureprovider-1), where `T` is the type of the feature. Feature providers can be implemented for any of the previously listed feature types. The order of feature providers in the `ApplicationPartManager.FeatureProviders` can impact run time behavior. Later added providers can react to actions taken by earlier added providers.

### Display available features

The features available to an app can be enumerated by requesting an `ApplicationPartManager` through [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0):

```
using AppPartsSample.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewComponents;

namespace AppPartsSample.Controllers
{
    public class FeaturesController : Controller
    {
        private readonly ApplicationPartManager _partManager;

        public FeaturesController(ApplicationPartManager partManager)
        {
            _partManager = partManager;
        }

        public IActionResult Index()
        {
            var viewModel = new FeaturesViewModel();

            var controllerFeature = new ControllerFeature();
            _partManager.PopulateFeature(controllerFeature);
            viewModel.Controllers = controllerFeature.Controllers.ToList();

            var tagHelperFeature = new TagHelperFeature();
            _partManager.PopulateFeature(tagHelperFeature);
            viewModel.TagHelpers = tagHelperFeature.TagHelpers.ToList();

            var viewComponentFeature = new ViewComponentFeature();
            _partManager.PopulateFeature(viewComponentFeature);
            viewModel.ViewComponents = viewComponentFeature.ViewComponents.ToList();

            return View(viewModel);
        }
    }
}
```

The [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/advanced/app-parts/samples/2) uses the preceding code to display the app features:

```
Controllers:
    - FeaturesController
    - HomeController
    - HelloController
    - GenericController`1
    - GenericController`1
Tag Helpers:
    - PrerenderTagHelper
    - AnchorTagHelper
    - CacheTagHelper
    - DistributedCacheTagHelper
    - EnvironmentTagHelper
    - Additional Tag Helpers omitted for brevity.
View Components:
    - SampleViewComponent
```

## Discovery in application parts

HTTP 404 errors are not uncommon when developing with application parts. These errors are typically caused by missing an essential requirement for how applications parts are discovered. If your app returns an HTTP 404 error, verify the following requirements have been met:

* The `applicationName` setting needs to be set to the root assembly used for discovery. The root assembly used for discovery is normally the entry point assembly.
* The root assembly needs to have a reference to the parts used for discovery. The reference can be direct or transitive.
* The root assembly needs to reference the Web SDK. The framework has logic that stamps attributes into the root assembly that are used for discovery.

By [Rick Anderson](https://twitter.com/RickAndMSFT)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/advanced/app-parts) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

An *Application Part* is an abstraction over the resources of an app. Application Parts allow ASP.NET Core to discover controllers, view components, tag helpers, Razor Pages, razor compilation sources, and more. [AssemblyPart](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.assemblypart) is an Application part. `AssemblyPart` encapsulates an assembly reference and exposes types and compilation references.

*Feature providers* work with application parts to populate the features of an ASP.NET Core app. The main use case for application parts is to configure an app to discover (or avoid loading) ASP.NET Core features from an assembly. For example, you might want to share common functionality between multiple apps. Using Application Parts, you can share an assembly (DLL) containing controllers, views, Razor Pages, razor compilation sources, Tag Helpers, and more with multiple apps. Sharing an assembly is preferred to duplicating code in multiple projects.

ASP.NET Core apps load features from [ApplicationPart](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.applicationpart). The [AssemblyPart](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.assemblypart) class represents an application part that's backed by an assembly.

## Load ASP.NET Core features

Use the `ApplicationPart` and `AssemblyPart` classes to discover and load ASP.NET Core features (controllers, view components, etc.). The [ApplicationPartManager](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.applicationpartmanager) tracks the application parts and feature providers available. `ApplicationPartManager` is configured in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    // Requires using System.Reflection;
    var assembly = typeof(MySharedController).GetTypeInfo().Assembly;
    services.AddMvc()
        .AddApplicationPart(assembly)
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
```

The following code provides an alternative approach to configuring `ApplicationPartManager` using `AssemblyPart`:

```
public void ConfigureServices(IServiceCollection services)
{
    // Requires using System.Reflection;
    // Requires using Microsoft.AspNetCore.Mvc.ApplicationParts;
    var assembly = typeof(MySharedController).GetTypeInfo().Assembly;
    var part = new AssemblyPart(assembly);
    services.AddMvc()
        .ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part))
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
```

The preceding two code samples load the `SharedController` from an assembly. The `SharedController` is not in the application's project. See the [WebAppParts solution](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/advanced/app-parts/samples/1/WebAppParts) sample download.

### Include views

Use a [Razor class library](../../razor-pages/ui-class?view=aspnetcore-10.0) to include views in the assembly.

### Prevent loading resources

Application parts can be used to *avoid* loading resources in a particular assembly or location. Add or remove members of the [Microsoft.AspNetCore.Mvc.ApplicationParts](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts) collection to hide or make available resources. The order of the entries in the `ApplicationParts` collection isn't important. Configure the `ApplicationPartManager` before using it to configure services in the container. For example, configure the `ApplicationPartManager` before invoking `AddControllersAsServices`. Call `Remove` on the `ApplicationParts` collection to remove a resource.

The following code uses [Microsoft.AspNetCore.Mvc.ApplicationParts](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts) to remove `MyDependentLibrary` from the app:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
            .ConfigureApplicationPartManager(apm =>
            {
                var dependentLibrary = apm.ApplicationParts
                    .FirstOrDefault(part => part.Name == "MyDependentLibrary");

                if (dependentLibrary != null)
                {
                    apm.ApplicationParts.Remove(dependentLibrary);
                }
            });
}
```

The `ApplicationPartManager` includes parts for:

* The app's assembly and dependent assemblies.
* `Microsoft.AspNetCore.Mvc.TagHelpers`.
* `Microsoft.AspNetCore.Mvc.Razor`.

## Application feature providers

Application feature providers examine application parts and provide features for those parts. There are built-in feature providers for the following ASP.NET Core features:

* [Controllers](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllers.controllerfeatureprovider)
* [Tag Helpers](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.taghelpers.taghelperfeatureprovider)
* [View Components](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponents.viewcomponentfeatureprovider)

Feature providers inherit from [IApplicationFeatureProvider\<TFeature>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationparts.iapplicationfeatureprovider-1), where `T` is the type of the feature. Feature providers can be implemented for any of the previously listed feature types. The order of feature providers in the `ApplicationPartManager.FeatureProviders` can impact run time behavior. Later added providers can react to actions taken by earlier added providers.

### Display available features

The features available to an app can be enumerated by requesting an `ApplicationPartManager` through [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0):

```
using AppPartsSample.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewComponents;

namespace AppPartsSample.Controllers
{
    public class FeaturesController : Controller
    {
        private readonly ApplicationPartManager _partManager;

        public FeaturesController(ApplicationPartManager partManager)
        {
            _partManager = partManager;
        }

        public IActionResult Index()
        {
            var viewModel = new FeaturesViewModel();

            var controllerFeature = new ControllerFeature();
            _partManager.PopulateFeature(controllerFeature);
            viewModel.Controllers = controllerFeature.Controllers.ToList();

            var tagHelperFeature = new TagHelperFeature();
            _partManager.PopulateFeature(tagHelperFeature);
            viewModel.TagHelpers = tagHelperFeature.TagHelpers.ToList();

            var viewComponentFeature = new ViewComponentFeature();
            _partManager.PopulateFeature(viewComponentFeature);
            viewModel.ViewComponents = viewComponentFeature.ViewComponents.ToList();

            return View(viewModel);
        }
    }
}
```

The [download sample](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/advanced/app-parts/samples/2) uses the preceding code to display the app features:

```
Controllers:
    - FeaturesController
    - HomeController
    - HelloController
    - GenericController`1
    - GenericController`1
Tag Helpers:
    - PrerenderTagHelper
    - AnchorTagHelper
    - CacheTagHelper
    - DistributedCacheTagHelper
    - EnvironmentTagHelper
    - Additional Tag Helpers omitted for brevity.
View Components:
    - SampleViewComponent
```

## Discovery in application parts

HTTP 404 errors are not uncommon when developing with application parts. These errors are typically caused by missing an essential requirement for how applications parts are discovered. If your app returns an HTTP 404 error, verify the following requirements have been met:

* The `applicationName` setting needs to be set to the root assembly used for discovery. The root assembly used for discovery is normally the entry point assembly.
* The root assembly needs to have a reference to the parts used for discovery. The reference can be direct or transitive.
* The root assembly needs to reference the Web SDK.
  * The ASP.NET Core framework has custom build logic that stamps attributes into the root assembly that are used for discovery.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-components
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/view-components.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# View components in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

## View components

View components are similar to partial views, but they're much more powerful. View components don't use model binding. They depend on the data passed when calling the view component. This article was written using controllers and views, but view components work with [Razor Pages](https://www.learnrazorpages.com/razor-pages/view-components).

A view component:

* Renders a chunk rather than a whole response.
* Includes the same separation-of-concerns and testability benefits found between a controller and view.
* Can have parameters and business logic.
* Is typically invoked from a layout page.

View components are intended anywhere reusable rendering logic that's too complex for a partial view, such as:

* Dynamic navigation menus.
* Tag cloud, where it queries the database.
* Sign in panel.
* Shopping cart.
* Recently published articles.
* Sidebar content on a blog.
* A sign in panel that would be rendered on every page and show either the links to sign out or sign in, depending on the sign in state of the user.

A view component consists of two parts:

* The class, typically derived from [ViewComponent](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponent).
* The result it returns, typically a view.

Like controllers, a view component can be a POCO, but most developers take advantage of the methods and properties available by deriving from [ViewComponent](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponent).

When considering if view components meet an app's specifications, consider using Razor components instead. Razor components also combine markup with C# code to produce reusable UI units. Razor components are designed for developer productivity when providing client-side UI logic and composition. For more information, see [ASP.NET Core Razor components](../../blazor/components/?view=aspnetcore-10.0). For information on how to incorporate Razor components into an MVC or Razor Pages app, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../blazor/components/integration?view=aspnetcore-10.0).

## Create a view component

This section contains the high-level requirements to create a view component. Later in the article, we'll examine each step in detail and create a view component.

### The view component class

Any of the following can create a view component class:

* Deriving from [ViewComponent](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponent).
* Decorating a class with the [`[ViewComponent]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponentattribute) attribute, or deriving from a class with the `[ViewComponent]` attribute.
* Creating a class where the name ends with the suffix [`ViewComponent`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponents).

Like controllers, view components must be public, non-nested, and non-abstract classes. The view component name is the class name with the `ViewComponent` suffix removed. It can also be explicitly specified using the [Name](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponentattribute.name) property.

A view component class:

* Supports constructor [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0).
* Doesn't take part in the controller lifecycle, therefore [filters](../controllers/filters?view=aspnetcore-10.0) can't be used in a view component.

To prevent a class that has a case-insensitive `ViewComponent` suffix from being treated as a view component, decorate the class with the [`[NonViewComponent]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.nonviewcomponentattribute) attribute:

```
using Microsoft.AspNetCore.Mvc;

[NonViewComponent]
public class ReviewComponent
{
    public string Status(string name) => JobStatus.GetCurrentStatus(name);
}
```

### View component methods

A view component defines its logic in an:

* `InvokeAsync` method that returns `Task<IViewComponentResult>`.
* `Invoke` synchronous method that returns an [IViewComponentResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iviewcomponentresult).

Parameters come directly from invocation of the view component, not from model binding. A view component never directly handles a request. Typically, a view component initializes a model and passes it to a view by calling the `View` method. In summary, view component methods:

* Define an `InvokeAsync` method that returns a `Task<IViewComponentResult>` or a synchronous `Invoke` method that returns an `IViewComponentResult`.
* Typically initializes a model and passes it to a view by calling the [ViewComponent.View](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponent.view#microsoft-aspnetcore-mvc-viewcomponent-view) method.
* Parameters come from the calling method, not HTTP. There's no model binding.
* Aren't reachable directly as an HTTP endpoint. They're typically invoked in a view. A view component never handles a request.
* Are overloaded on the signature rather than any details from the current HTTP request.

### View search path

The runtime searches for the view in the following paths:

* /Views/{Controller Name}/Components/{View Component Name}/{View Name}
* /Views/Shared/Components/{View Component Name}/{View Name}
* /Pages/Shared/Components/{View Component Name}/{View Name}
* /Areas/{Area Name}/Views/Shared/Components/{View Component Name}/{View Name}

The search path applies to projects using controllers + views and Razor Pages.

The default view name for a view component is `Default`, which means view files will typically be named `Default.cshtml`. A different view name can be specified when creating the view component result or when calling the `View` method.

We recommend naming the view file `Default.cshtml` and using the *Views/Shared/Components/{View Component Name}/{View Name}* path. The `PriorityList` view component used in this sample uses `Views/Shared/Components/PriorityList/Default.cshtml` for the view component view.

### Customize the view search path

To customize the view search path, modify Razor's [ViewLocationFormats](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.razorviewengineoptions.viewlocationformats#microsoft-aspnetcore-mvc-razor-razorviewengineoptions-viewlocationformats) collection. For example, to search for views within the path `/Components/{View Component Name}/{View Name}`, add a new item to the collection:

```
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews()
    .AddRazorOptions(options =>
    {
        options.ViewLocationFormats.Add("/{0}.cshtml");
    });

builder.Services.AddDbContext<ToDoContext>(options =>
        options.UseInMemoryDatabase("db"));

var app = builder.Build();

// Remaining code removed for brevity.
```

In the preceding code, the placeholder `{0}` represents the path `Components/{View Component Name}/{View Name}`.

## Invoke a view component

To use the view component, call the following inside a view:

```
@await Component.InvokeAsync("Name of view component",
                             {Anonymous Type Containing Parameters})
```

The parameters are passed to the `InvokeAsync` method. The `PriorityList` view component developed in the article is invoked from the `Views/ToDo/Index.cshtml` view file. In the following code, the `InvokeAsync` method is called with two parameters:

```
</table>

<div>
    Maximum Priority: @ViewData["maxPriority"] <br />
    Is Complete:  @ViewData["isDone"]
    @await Component.InvokeAsync("PriorityList",
                     new { 
                         maxPriority =  ViewData["maxPriority"],
                         isDone = ViewData["isDone"]  }
                     )
</div>
```

## Invoke a view component as a Tag Helper

A View Component can be invoked as a [Tag Helper](tag-helpers/intro?view=aspnetcore-10.0):

```
<div>
       Maxium Priority: @ViewData["maxPriority"] <br />
       Is Complete:  @ViewData["isDone"]
    @{
        int maxPriority = Convert.ToInt32(ViewData["maxPriority"]);
        bool isDone = Convert.ToBoolean(ViewData["isDone"]);
    }
    <vc:priority-list max-priority=maxPriority is-done=isDone>
    </vc:priority-list>
</div>
```

Pascal-cased class and method parameters for Tag Helpers are translated into their [kebab case](https://stackoverflow.com/questions/11273282/whats-the-name-for-hyphen-separated-case/12273101#12273101). The Tag Helper to invoke a view component uses the `<vc></vc>` element. The view component is specified as follows:

```
<vc:[view-component-name]
  parameter1="parameter1 value"
  parameter2="parameter2 value">
</vc:[view-component-name]>
```

To use a view component as a Tag Helper, register the assembly containing the view component using the `@addTagHelper` directive. If the view component is in an assembly called `MyWebApp`, add the following directive to the `_ViewImports.cshtml` file:

```
@addTagHelper *, MyWebApp
```

A view component can be registered as a Tag Helper to any file that references the view component. See [Managing Tag Helper Scope](tag-helpers/intro?view=aspnetcore-10.0#managing-tag-helper-scope) for more information on how to register Tag Helpers.

The `InvokeAsync` method used in this tutorial:

```
@await Component.InvokeAsync("PriorityList",
                 new { 
                     maxPriority =  ViewData["maxPriority"],
                     isDone = ViewData["isDone"]  }
                 )
```

In the preceding markup, the `PriorityList` view component becomes `priority-list`. The parameters to the view component are passed as attributes in kebab case.

### Invoke a view component directly from a controller

View components are typically invoked from a view, but they can be invoked directly from a controller method. While view components don't define endpoints like controllers, a controller action that returns the content of a `ViewComponentResult` can be implemented.

In the following example, the view component is called directly from the controller:

```
public IActionResult IndexVC(int maxPriority = 2, bool isDone = false)
{
    return ViewComponent("PriorityList",
        new { 
           maxPriority = maxPriority,
           isDone = isDone
        });
}
```

## Create a basic view component

[Download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/view-components/samples/6.x), build, and test the starter code. It's a basic project with a `ToDo` controller that displays a list of *ToDo* items.

### Update the controller to pass in priority and completion status

Update the `Index` method to use priority and completion status parameters:

```
using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;

namespace ViewComponentSample.Controllers;
public class ToDoController : Controller
{
    private readonly ToDoContext _ToDoContext;

    public ToDoController(ToDoContext context)
    {
        _ToDoContext = context;
        _ToDoContext.Database.EnsureCreated();
    }

    public IActionResult Index(int maxPriority = 2, bool isDone = false)
    {
        var model = _ToDoContext!.ToDo!.ToList();
        ViewData["maxPriority"] = maxPriority;
        ViewData["isDone"] = isDone;
        return View(model);
    }
```

### Add a ViewComponent class

Add a ViewComponent class to `ViewComponents/PriorityListViewComponent.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents;

public class PriorityListViewComponent : ViewComponent
{
    private readonly ToDoContext db;

    public PriorityListViewComponent(ToDoContext context) => db = context;

    public async Task<IViewComponentResult> InvokeAsync(
                                            int maxPriority, bool isDone)
    {
        var items = await GetItemsAsync(maxPriority, isDone);
        return View(items);
    }

    private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
    {
        return db!.ToDo!.Where(x => x.IsDone == isDone &&
                             x.Priority <= maxPriority).ToListAsync();
    }
}
```

Notes on the code:

* View component classes can be contained in **any** folder in the project.

* Because the class name PriorityList**ViewComponent** ends with the suffix **ViewComponent**, the runtime uses the string `PriorityList` when referencing the class component from a view.

* The [`[ViewComponent]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponentattribute) attribute can change the name used to reference a view component. For example, the class could have been named `XYZ` with the following `[ViewComponent]` attribute:

  ```
  [ViewComponent(Name = "PriorityList")]
     public class XYZ : ViewComponent
  ```

* The `[ViewComponent]` attribute in the preceding code tells the view component selector to use:

  * The name `PriorityList` when looking for the views associated with the component.
  * The string "PriorityList" when referencing the class component from a view.

* The component uses [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to make the data context available.

* `InvokeAsync` exposes a method that can be called from a view, and it can take an arbitrary number of arguments.

* The `InvokeAsync` method returns the set of `ToDo` items that satisfy the `isDone` and `maxPriority` parameters.

### Create the view component Razor view

* Create the *Views/Shared/Components* folder. This folder **must** be named *Components*.

* Create the *Views/Shared/Components/PriorityList* folder. This folder name must match the name of the view component class, or the name of the class minus the suffix. If the `ViewComponent` attribute is used, the class name would need to match the attribute designation.

* Create a `Views/Shared/Components/PriorityList/Default.cshtml` Razor view:

  ```
  @model IEnumerable<ViewComponentSample.Models.TodoItem>

  <h3>Priority Items</h3>
  <ul>
      @foreach (var todo in Model)
      {
          <li>@todo.Name</li>
      }
  </ul>
  ```

  The Razor view takes a list of `TodoItem` and displays them. If the view component `InvokeAsync` method doesn't pass the name of the view, *Default* is used for the view name by convention. To override the default styling for a specific controller, add a view to the controller-specific view folder (for example *Views/ToDo/Components/PriorityList/Default.cshtml)*.

  If the view component is controller-specific, it can be added to the controller-specific folder. For example, `Views/ToDo/Components/PriorityList/Default.cshtml` is controller-specific.

* Add a `div` containing a call to the priority list component to the bottom of the `Views/ToDo/index.cshtml` file:

  ```
  </table>

  <div>
      Maximum Priority: @ViewData["maxPriority"] <br />
      Is Complete:  @ViewData["isDone"]
      @await Component.InvokeAsync("PriorityList",
                       new { 
                           maxPriority =  ViewData["maxPriority"],
                           isDone = ViewData["isDone"]  }
                       )
  </div>
  ```

The markup `@await Component.InvokeAsync` shows the syntax for calling view components. The first argument is the name of the component we want to invoke or call. Subsequent parameters are passed to the component. `InvokeAsync` can take an arbitrary number of arguments.

Test the app. The following image shows the ToDo list and the priority items:

The view component can be called directly from the controller:

```
public IActionResult IndexVC(int maxPriority = 2, bool isDone = false)
{
    return ViewComponent("PriorityList",
        new { 
           maxPriority = maxPriority,
           isDone = isDone
        });
}
```

### Specify a view component name

A complex view component might need to specify a non-default view under some conditions. The following code shows how to specify the "PVC" view from the `InvokeAsync` method. Update the `InvokeAsync` method in the `PriorityListViewComponent` class.

```
public async Task<IViewComponentResult> InvokeAsync(
                                           int maxPriority, bool isDone)
{
    string MyView = "Default";
    // If asking for all completed tasks, render with the "PVC" view.
    if (maxPriority > 3 && isDone == true)
    {
        MyView = "PVC";
    }
    var items = await GetItemsAsync(maxPriority, isDone);
    return View(MyView, items);
}
```

Copy the `Views/Shared/Components/PriorityList/Default.cshtml` file to a view named `Views/Shared/Components/PriorityList/PVC.cshtml`. Add a heading to indicate the PVC view is being used.

```
@model IEnumerable<ViewComponentSample.Models.TodoItem>

<h2> PVC Named Priority Component View</h2>
<h4>@ViewBag.PriorityMessage</h4>
<ul>
    @foreach (var todo in Model)
    {
        <li>@todo.Name</li>
    }
</ul>
```

Run the app and verify PVC view.

If the PVC view isn't rendered, verify the view component with a priority of 4 or higher is called.

### Examine the view path

* Change the priority parameter to three or less so the priority view isn't returned.

* Temporarily rename the `Views/ToDo/Components/PriorityList/Default.cshtml` to `1Default.cshtml`.

* Test the app, the following error occurs:

  ```
  An unhandled exception occurred while processing the request.
  InvalidOperationException: The view 'Components/PriorityList/Default' wasn't found. The following locations were searched:
  /Views/ToDo/Components/PriorityList/Default.cshtml
  /Views/Shared/Components/PriorityList/Default.cshtml
  ```

* Copy `Views/ToDo/Components/PriorityList/1Default.cshtml` to `Views/Shared/Components/PriorityList/Default.cshtml`.

* Add some markup to the *Shared* ToDo view component view to indicate the view is from the *Shared* folder.

* Test the **Shared** component view.

### Avoid hard-coded strings

For compile time safety, replace the hard-coded view component name with the class name. Update the *PriorityListViewComponent.cs* file to not use the "ViewComponent" suffix:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents;

public class PriorityList : ViewComponent
{
    private readonly ToDoContext db;

    public PriorityList(ToDoContext context)
    {
        db = context;
    }

    public async Task<IViewComponentResult> InvokeAsync(
                                               int maxPriority, bool isDone)
    {
        var items = await GetItemsAsync(maxPriority, isDone);
        return View(items);
    }

    private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
    {
        return db!.ToDo!.Where(x => x.IsDone == isDone &&
                             x.Priority <= maxPriority).ToListAsync();
    }
}
```

The view file:

```
</table>

<div>
    Testing nameof(PriorityList) <br />

    Maxium Priority: @ViewData["maxPriority"] <br />
    Is Complete:  @ViewData["isDone"]
    @await Component.InvokeAsync(nameof(PriorityList),
                     new { 
                         maxPriority =  ViewData["maxPriority"],
                         isDone = ViewData["isDone"]  }
                     )
</div>
```

An overload of `Component.InvokeAsync` method that takes a CLR type uses the `typeof` operator:

```
</table>

<div>
    Testing typeof(PriorityList) <br />

    Maxium Priority: @ViewData["maxPriority"] <br />
    Is Complete:  @ViewData["isDone"]
    @await Component.InvokeAsync(typeof(PriorityList),
                     new { 
                         maxPriority =  ViewData["maxPriority"],
                         isDone = ViewData["isDone"]  }
                     )
</div>
```

## Perform synchronous work

The framework handles invoking a synchronous `Invoke` method if asynchronous work isn't required. The following method creates a synchronous `Invoke` view component:

```
using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents
{
    public class PriorityListSync : ViewComponent
    {
        private readonly ToDoContext db;

        public PriorityListSync(ToDoContext context)
        {
            db = context;
        }

        public IViewComponentResult Invoke(int maxPriority, bool isDone)
        {
 
            var x = db!.ToDo!.Where(x => x.IsDone == isDone &&
                                  x.Priority <= maxPriority).ToList();
            return View(x);
        }
    }
}
```

The view component's Razor file:

```
<div>
    Testing nameof(PriorityList) <br />

    Maxium Priority: @ViewData["maxPriority"] <br />
    Is Complete:  @ViewData["isDone"]
    @await Component.InvokeAsync(nameof(PriorityListSync),
                     new { 
                         maxPriority =  ViewData["maxPriority"],
                         isDone = ViewData["isDone"]  }
                     )
</div>
```

The view component is invoked in a Razor file (for example, `Views/Home/Index.cshtml`) using one of the following approaches:

* [IViewComponentHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iviewcomponenthelper)
* [Tag Helper](tag-helpers/intro?view=aspnetcore-10.0)

To use the [IViewComponentHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iviewcomponenthelper) approach, call `Component.InvokeAsync`:

```
@await Component.InvokeAsync(nameof(PriorityList),
                             new { maxPriority = 4, isDone = true })
```

To use the Tag Helper, register the assembly containing the View Component using the `@addTagHelper` directive (the view component is in an assembly called `MyWebApp`):

```
@addTagHelper *, MyWebApp
```

Use the view component Tag Helper in the Razor markup file:

```
<vc:priority-list max-priority="999" is-done="false">
</vc:priority-list>
```

The method signature of `PriorityList.Invoke` is synchronous, but Razor finds and calls the method with `Component.InvokeAsync` in the markup file.

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/view-components/samples/6.x) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Dependency injection into views](dependency-injection?view=aspnetcore-10.0)
* [View Components in Razor Pages](https://www.learnrazorpages.com/razor-pages/view-components)
* [Why You Should Use View Components, not Partial Views, in ASP.NET Core](https://www.telerik.com/blogs/why-you-should-use-view-components-not-partial-views-aspnet-core)

## View components

View components are similar to partial views, but they're much more powerful. View components don't use model binding, they depend on the data passed when calling the view component. This article was written using controllers and views, but view components work with [Razor Pages](https://www.learnrazorpages.com/razor-pages/view-components).

A view component:

* Renders a chunk rather than a whole response.
* Includes the same separation-of-concerns and testability benefits found between a controller and view.
* Can have parameters and business logic.
* Is typically invoked from a layout page.

View components are intended anywhere reusable rendering logic that's too complex for a partial view, such as:

* Dynamic navigation menus
* Tag cloud, where it queries the database
* Sign in panel
* Shopping cart
* Recently published articles
* Sidebar content on a blog
* A sign in panel that would be rendered on every page and show either the links to sign out or sign in, depending on the sign in state of the user

A view component consists of two parts:

* The class, typically derived from [ViewComponent](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponent)
* The result it returns, typically a view.

Like controllers, a view component can be a POCO, but most developers take advantage of the methods and properties available by deriving from [ViewComponent](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponent).

When considering if view components meet an app's specifications, consider using Razor components instead. Razor components also combine markup with C# code to produce reusable UI units. Razor components are designed for developer productivity when providing client-side UI logic and composition. For more information, see [ASP.NET Core Razor components](../../blazor/components/?view=aspnetcore-10.0). For information on how to incorporate Razor components into an MVC or Razor Pages app, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../blazor/components/integration?view=aspnetcore-10.0\&pivots=server).

## Create a view component

This section contains the high-level requirements to create a view component. Later in the article, we'll examine each step in detail and create a view component.

### The view component class

A view component class can be created by any of the following:

* Deriving from [ViewComponent](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponent)
* Decorating a class with the [`[ViewComponent]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponentattribute) attribute, or deriving from a class with the `[ViewComponent]` attribute
* Creating a class where the name ends with the suffix [`ViewComponent`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponents)

Like controllers, view components must be public, non-nested, and non-abstract classes. The view component name is the class name with the `ViewComponent` suffix removed. It can also be explicitly specified using the [Name](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponentattribute.name) property.

A view component class:

* Supports constructor [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0)
* Doesn't take part in the controller lifecycle, therefore [filters](../controllers/filters?view=aspnetcore-10.0) can't be used in a view component

To prevent a class that has a case-insensitive `ViewComponent` suffix from being treated as a view component, decorate the class with the [`[NonViewComponent]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.nonviewcomponentattribute) attribute:

```
using Microsoft.AspNetCore.Mvc;

[NonViewComponent]
public class ReviewComponent
{
    public string Status(string name) => JobStatus.GetCurrentStatus(name);
}
```

### View component methods

A view component defines its logic in an:

* `InvokeAsync` method that returns `Task<IViewComponentResult>`.
* `Invoke` synchronous method that returns an [IViewComponentResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iviewcomponentresult).

Parameters come directly from invocation of the view component, not from model binding. A view component never directly handles a request. Typically, a view component initializes a model and passes it to a view by calling the `View` method. In summary, view component methods:

* Define an `InvokeAsync` method that returns a `Task<IViewComponentResult>` or a synchronous `Invoke` method that returns an `IViewComponentResult`.
* Typically initializes a model and passes it to a view by calling the [ViewComponent.View](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponent.view#microsoft-aspnetcore-mvc-viewcomponent-view) method.
* Parameters come from the calling method, not HTTP. There's no model binding.
* Aren't reachable directly as an HTTP endpoint. They're typically invoked in a view. A view component never handles a request.
* Are overloaded on the signature rather than any details from the current HTTP request.

### View search path

The runtime searches for the view in the following paths:

* /Views/{Controller Name}/Components/{View Component Name}/{View Name}
* /Views/Shared/Components/{View Component Name}/{View Name}
* /Pages/Shared/Components/{View Component Name}/{View Name}
* /Areas/{Area Name}/Views/Shared/Components/{View Component Name}/{View Name}

The search path applies to projects using controllers + views and Razor Pages.

The default view name for a view component is `Default`, which means view files will typically be named `Default.cshtml`. A different view name can be specified when creating the view component result or when calling the `View` method.

We recommend naming the view file `Default.cshtml` and using the *Views/Shared/Components/{View Component Name}/{View Name}* path. The `PriorityList` view component used in this sample uses `Views/Shared/Components/PriorityList/Default.cshtml` for the view component view.

### Customize the view search path

To customize the view search path, modify Razor's [ViewLocationFormats](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.razorviewengineoptions.viewlocationformats#microsoft-aspnetcore-mvc-razor-razorviewengineoptions-viewlocationformats) collection. For example, to search for views within the path `/Components/{View Component Name}/{View Name}`, add a new item to the collection:

```
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews()
    .AddRazorOptions(options =>
    {
        options.ViewLocationFormats.Add("/{0}.cshtml");
    });

builder.Services.AddDbContext<ToDoContext>(options =>
        options.UseInMemoryDatabase("db"));

var app = builder.Build();

// Remaining code removed for brevity.
```

In the preceding code, the placeholder `{0}` represents the path `Components/{View Component Name}/{View Name}`.

## Invoke a view component

To use the view component, call the following inside a view:

```
@await Component.InvokeAsync("Name of view component",
                             {Anonymous Type Containing Parameters})
```

The parameters are passed to the `InvokeAsync` method. The `PriorityList` view component developed in the article is invoked from the `Views/ToDo/Index.cshtml` view file. In the following code, the `InvokeAsync` method is called with two parameters:

```
</table>

<div>
    Maximum Priority: @ViewData["maxPriority"] <br />
    Is Complete:  @ViewData["isDone"]
    @await Component.InvokeAsync("PriorityList",
                     new { 
                         maxPriority =  ViewData["maxPriority"],
                         isDone = ViewData["isDone"]  }
                     )
</div>
```

## Invoke a view component as a Tag Helper

A View Component can be invoked as a [Tag Helper](tag-helpers/intro?view=aspnetcore-10.0):

```
<div>
       Maxium Priority: @ViewData["maxPriority"] <br />
       Is Complete:  @ViewData["isDone"]
    @{
        int maxPriority = Convert.ToInt32(ViewData["maxPriority"]);
        bool isDone = Convert.ToBoolean(ViewData["isDone"]);
    }
    <vc:priority-list max-priority=maxPriority is-done=isDone>
    </vc:priority-list>
</div>
```

Pascal-cased class and method parameters for Tag Helpers are translated into their [kebab case](https://stackoverflow.com/questions/11273282/whats-the-name-for-dash-separated-case/12273101). The Tag Helper to invoke a view component uses the `<vc></vc>` element. The view component is specified as follows:

```
<vc:[view-component-name]
  parameter1="parameter1 value"
  parameter2="parameter2 value">
</vc:[view-component-name]>
```

To use a view component as a Tag Helper, register the assembly containing the view component using the `@addTagHelper` directive. If the view component is in an assembly called `MyWebApp`, add the following directive to the `_ViewImports.cshtml` file:

```
@addTagHelper *, MyWebApp
```

A view component can be registered as a Tag Helper to any file that references the view component. See [Managing Tag Helper Scope](tag-helpers/intro?view=aspnetcore-10.0#managing-tag-helper-scope) for more information on how to register Tag Helpers.

The `InvokeAsync` method used in this tutorial:

```
@await Component.InvokeAsync("PriorityList",
                 new { 
                     maxPriority =  ViewData["maxPriority"],
                     isDone = ViewData["isDone"]  }
                 )
```

In the preceding markup, the `PriorityList` view component becomes `priority-list`. The parameters to the view component are passed as attributes in kebab case.

### Invoke a view component directly from a controller

View components are typically invoked from a view, but they can be invoked directly from a controller method. While view components don't define endpoints like controllers, a controller action that returns the content of a `ViewComponentResult` can be implemented.

In the following example, the view component is called directly from the controller:

```
public IActionResult IndexVC(int maxPriority = 2, bool isDone = false)
{
    return ViewComponent("PriorityList",
        new { 
           maxPriority = maxPriority,
           isDone = isDone
        });
}
```

## Create a basic view component

[Download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/view-components/samples/6.x), build and test the starter code. It's a basic project with a `ToDo` controller that displays a list of *ToDo* items.

### Update the controller to pass in priority and completion status

Update the `Index` method to use priority and completion status parameters:

```
using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;

namespace ViewComponentSample.Controllers;
public class ToDoController : Controller
{
    private readonly ToDoContext _ToDoContext;

    public ToDoController(ToDoContext context)
    {
        _ToDoContext = context;
        _ToDoContext.Database.EnsureCreated();
    }

    public IActionResult Index(int maxPriority = 2, bool isDone = false)
    {
        var model = _ToDoContext!.ToDo!.ToList();
        ViewData["maxPriority"] = maxPriority;
        ViewData["isDone"] = isDone;
        return View(model);
    }
```

### Add a ViewComponent class

Add a ViewComponent class to `ViewComponents/PriorityListViewComponent.cs`:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents;

public class PriorityListViewComponent : ViewComponent
{
    private readonly ToDoContext db;

    public PriorityListViewComponent(ToDoContext context) => db = context;

    public async Task<IViewComponentResult> InvokeAsync(
                                            int maxPriority, bool isDone)
    {
        var items = await GetItemsAsync(maxPriority, isDone);
        return View(items);
    }

    private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
    {
        return db!.ToDo!.Where(x => x.IsDone == isDone &&
                             x.Priority <= maxPriority).ToListAsync();
    }
}
```

Notes on the code:

* View component classes can be contained in **any** folder in the project.

* Because the class name PriorityList**ViewComponent** ends with the suffix **ViewComponent**, the runtime uses the string `PriorityList` when referencing the class component from a view.

* The [`[ViewComponent]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponentattribute) attribute can change the name used to reference a view component. For example, the class could have been named `XYZ` with the following `[ViewComponent]` attribute:

  ```
  [ViewComponent(Name = "PriorityList")]
     public class XYZ : ViewComponent
  ```

* The `[ViewComponent]` attribute in the preceding code tells the view component selector to use:

  * The name `PriorityList` when looking for the views associated with the component
  * The string "PriorityList" when referencing the class component from a view.

* The component uses [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to make the data context available.

* `InvokeAsync` exposes a method that can be called from a view, and it can take an arbitrary number of arguments.

* The `InvokeAsync` method returns the set of `ToDo` items that satisfy the `isDone` and `maxPriority` parameters.

### Create the view component Razor view

* Create the *Views/Shared/Components* folder. This folder **must** be named *Components*.

* Create the *Views/Shared/Components/PriorityList* folder. This folder name must match the name of the view component class, or the name of the class minus the suffix. If the `ViewComponent` attribute is used, the class name would need to match the attribute designation.

* Create a `Views/Shared/Components/PriorityList/Default.cshtml` Razor view:

  ```
  @model IEnumerable<ViewComponentSample.Models.TodoItem>

  <h3>Priority Items</h3>
  <ul>
      @foreach (var todo in Model)
      {
          <li>@todo.Name</li>
      }
  </ul>
  ```

  The Razor view takes a list of `TodoItem` and displays them. If the view component `InvokeAsync` method doesn't pass the name of the view, *Default* is used for the view name by convention. To override the default styling for a specific controller, add a view to the controller-specific view folder (for example *Views/ToDo/Components/PriorityList/Default.cshtml)*.

  If the view component is controller-specific, it can be added to the controller-specific folder. For example, `Views/ToDo/Components/PriorityList/Default.cshtml` is controller-specific.

* Add a `div` containing a call to the priority list component to the bottom of the `Views/ToDo/index.cshtml` file:

  ```
  </table>

  <div>
      Maximum Priority: @ViewData["maxPriority"] <br />
      Is Complete:  @ViewData["isDone"]
      @await Component.InvokeAsync("PriorityList",
                       new { 
                           maxPriority =  ViewData["maxPriority"],
                           isDone = ViewData["isDone"]  }
                       )
  </div>
  ```

The markup `@await Component.InvokeAsync` shows the syntax for calling view components. The first argument is the name of the component we want to invoke or call. Subsequent parameters are passed to the component. `InvokeAsync` can take an arbitrary number of arguments.

Test the app. The following image shows the ToDo list and the priority items:

The view component can be called directly from the controller:

```
public IActionResult IndexVC(int maxPriority = 2, bool isDone = false)
{
    return ViewComponent("PriorityList",
        new { 
           maxPriority = maxPriority,
           isDone = isDone
        });
}
```

### Specify a view component name

A complex view component might need to specify a non-default view under some conditions. The following code shows how to specify the "PVC" view from the `InvokeAsync` method. Update the `InvokeAsync` method in the `PriorityListViewComponent` class.

```
public async Task<IViewComponentResult> InvokeAsync(
                                           int maxPriority, bool isDone)
{
    string MyView = "Default";
    // If asking for all completed tasks, render with the "PVC" view.
    if (maxPriority > 3 && isDone == true)
    {
        MyView = "PVC";
    }
    var items = await GetItemsAsync(maxPriority, isDone);
    return View(MyView, items);
}
```

Copy the `Views/Shared/Components/PriorityList/Default.cshtml` file to a view named `Views/Shared/Components/PriorityList/PVC.cshtml`. Add a heading to indicate the PVC view is being used.

```
@model IEnumerable<ViewComponentSample.Models.TodoItem>

<h2> PVC Named Priority Component View</h2>
<h4>@ViewBag.PriorityMessage</h4>
<ul>
    @foreach (var todo in Model)
    {
        <li>@todo.Name</li>
    }
</ul>
```

Run the app and verify PVC view.

If the PVC view isn't rendered, verify the view component with a priority of 4 or higher is called.

### Examine the view path

* Change the priority parameter to three or less so the priority view isn't returned.

* Temporarily rename the `Views/ToDo/Components/PriorityList/Default.cshtml` to `1Default.cshtml`.

* Test the app, the following error occurs:

  ```
  An unhandled exception occurred while processing the request.
  InvalidOperationException: The view 'Components/PriorityList/Default' wasn't found. The following locations were searched:
  /Views/ToDo/Components/PriorityList/Default.cshtml
  /Views/Shared/Components/PriorityList/Default.cshtml
  ```

* Copy `Views/ToDo/Components/PriorityList/1Default.cshtml` to `Views/Shared/Components/PriorityList/Default.cshtml`.

* Add some markup to the *Shared* ToDo view component view to indicate the view is from the *Shared* folder.

* Test the **Shared** component view.

### Avoid hard-coded strings

For compile time safety, replace the hard-coded view component name with the class name. Update the *PriorityListViewComponent.cs* file to not use the "ViewComponent" suffix:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents;

public class PriorityList : ViewComponent
{
    private readonly ToDoContext db;

    public PriorityList(ToDoContext context)
    {
        db = context;
    }

    public async Task<IViewComponentResult> InvokeAsync(
                                               int maxPriority, bool isDone)
    {
        var items = await GetItemsAsync(maxPriority, isDone);
        return View(items);
    }

    private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
    {
        return db!.ToDo!.Where(x => x.IsDone == isDone &&
                             x.Priority <= maxPriority).ToListAsync();
    }
}
```

The view file:

```
</table>

<div>
    Testing nameof(PriorityList) <br />

    Maxium Priority: @ViewData["maxPriority"] <br />
    Is Complete:  @ViewData["isDone"]
    @await Component.InvokeAsync(nameof(PriorityList),
                     new { 
                         maxPriority =  ViewData["maxPriority"],
                         isDone = ViewData["isDone"]  }
                     )
</div>
```

An overload of `Component.InvokeAsync` method that takes a CLR type uses the `typeof` operator:

```
</table>

<div>
    Testing typeof(PriorityList) <br />

    Maxium Priority: @ViewData["maxPriority"] <br />
    Is Complete:  @ViewData["isDone"]
    @await Component.InvokeAsync(typeof(PriorityList),
                     new { 
                         maxPriority =  ViewData["maxPriority"],
                         isDone = ViewData["isDone"]  }
                     )
</div>
```

## Perform synchronous work

The framework handles invoking a synchronous `Invoke` method if asynchronous work isn't required. The following method creates a synchronous `Invoke` view component:

```
using Microsoft.AspNetCore.Mvc;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents
{
    public class PriorityListSync : ViewComponent
    {
        private readonly ToDoContext db;

        public PriorityListSync(ToDoContext context)
        {
            db = context;
        }

        public IViewComponentResult Invoke(int maxPriority, bool isDone)
        {
 
            var x = db!.ToDo!.Where(x => x.IsDone == isDone &&
                                  x.Priority <= maxPriority).ToList();
            return View(x);
        }
    }
}
```

The view component's Razor file:

```
<div>
    Testing nameof(PriorityList) <br />

    Maxium Priority: @ViewData["maxPriority"] <br />
    Is Complete:  @ViewData["isDone"]
    @await Component.InvokeAsync(nameof(PriorityListSync),
                     new { 
                         maxPriority =  ViewData["maxPriority"],
                         isDone = ViewData["isDone"]  }
                     )
</div>
```

The view component is invoked in a Razor file (for example, `Views/Home/Index.cshtml`) using one of the following approaches:

* [IViewComponentHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iviewcomponenthelper)
* [Tag Helper](tag-helpers/intro?view=aspnetcore-10.0)

To use the [IViewComponentHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iviewcomponenthelper) approach, call `Component.InvokeAsync`:

```
@await Component.InvokeAsync(nameof(PriorityList),
                             new { maxPriority = 4, isDone = true })
```

To use the Tag Helper, register the assembly containing the View Component using the `@addTagHelper` directive (the view component is in an assembly called `MyWebApp`):

```
@addTagHelper *, MyWebApp
```

Use the view component Tag Helper in the Razor markup file:

```
<vc:priority-list max-priority="999" is-done="false">
</vc:priority-list>
```

The method signature of `PriorityList.Invoke` is synchronous, but Razor finds and calls the method with `Component.InvokeAsync` in the markup file.

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/view-components/samples/6.x) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Dependency injection into views](dependency-injection?view=aspnetcore-10.0)
* [View Components in Razor Pages](https://www.learnrazorpages.com/razor-pages/view-components)
* [Why You Should Use View Components, not Partial Views, in ASP.NET Core](https://www.telerik.com/blogs/why-you-should-use-view-components-not-partial-views-aspnet-core)

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/view-components/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## View components

View components are similar to partial views, but they're much more powerful. View components don't use model binding, and only depend on the data provided when calling into it. This article was written using controllers and views, but view components also work with Razor Pages.

A view component:

* Renders a chunk rather than a whole response.
* Includes the same separation-of-concerns and testability benefits found between a controller and view.
* Can have parameters and business logic.
* Is typically invoked from a layout page.

View components are intended anywhere you have reusable rendering logic that's too complex for a partial view, such as:

* Dynamic navigation menus
* Tag cloud (where it queries the database)
* Login panel
* Shopping cart
* Recently published articles
* Sidebar content on a typical blog
* A login panel that would be rendered on every page and show either the links to log out or log in, depending on the log in state of the user

A view component consists of two parts: the class (typically derived from [ViewComponent](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewcomponent)) and the result it returns (typically a view). Like controllers, a view component can be a [POCO](https://stackoverflow.com/questions/250001/poco-definition), but most developers take advantage of the methods and properties available by deriving from `ViewComponent`.

When considering if view components meet an app's specifications, consider using Razor components instead. Razor components also combine markup with C# code to produce reusable UI units. Razor components are designed for developer productivity when providing client-side UI logic and composition. For more information, see [ASP.NET Core Razor components](../../blazor/components/?view=aspnetcore-10.0). For information on how to incorporate Razor components into an MVC or Razor Pages app, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages](../../blazor/components/integration?view=aspnetcore-10.0\&pivots=server).

## Creating a view component

This section contains the high-level requirements to create a view component. Later in the article, we'll examine each step in detail and create a view component.

### The view component class

A view component class can be created by any of the following:

* Deriving from *ViewComponent*
* Decorating a class with the `[ViewComponent]` attribute, or deriving from a class with the `[ViewComponent]` attribute
* Creating a class where the name ends with the suffix *ViewComponent*

Like controllers, view components must be public, non-nested, and non-abstract classes. The view component name is the class name with the "ViewComponent" suffix removed. It can also be explicitly specified using the `ViewComponentAttribute.Name` property.

A view component class:

* Fully supports constructor [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0)
* Doesn't take part in the controller lifecycle, which means you can't use [filters](../controllers/filters?view=aspnetcore-10.0) in a view component

To stop a class that has a case-insensitive *ViewComponent* suffix from being treated as a view component, decorate the class with the [\[NonViewComponent\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.nonviewcomponentattribute) attribute:

```
[NonViewComponent]
public class ReviewComponent
{
    // ...
```

### View component methods

A view component defines its logic in an `InvokeAsync` method that returns a `Task<IViewComponentResult>` or in a synchronous `Invoke` method that returns an `IViewComponentResult`. Parameters come directly from invocation of the view component, not from model binding. A view component never directly handles a request. Typically, a view component initializes a model and passes it to a view by calling the `View` method. In summary, view component methods:

* Define an `InvokeAsync` method that returns a `Task<IViewComponentResult>` or a synchronous `Invoke` method that returns an `IViewComponentResult`.
* Typically initializes a model and passes it to a view by calling the `ViewComponent` `View` method.
* Parameters come from the calling method, not HTTP. There's no model binding.
* Are not reachable directly as an HTTP endpoint. They're invoked from your code (usually in a view). A view component never handles a request.
* Are overloaded on the signature rather than any details from the current HTTP request.

### View search path

The runtime searches for the view in the following paths:

* /Views/{Controller Name}/Components/{View Component Name}/{View Name}
* /Views/Shared/Components/{View Component Name}/{View Name}
* /Pages/Shared/Components/{View Component Name}/{View Name}
* /Areas/{Area Name}/Views/Shared/Components/{View Component Name}/{View Name}

The search path applies to projects using controllers + views and Razor Pages.

The default view name for a view component is *Default*, which means your view file will typically be named `Default.cshtml`. You can specify a different view name when creating the view component result or when calling the `View` method.

We recommend you name the view file `Default.cshtml` and use the *Views/Shared/Components/{View Component Name}/{View Name}* path. The `PriorityList` view component used in this sample uses `Views/Shared/Components/PriorityList/Default.cshtml` for the view component view.

### Customize the view search path

To customize the view search path, modify Razor's [ViewLocationFormats](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razor.razorviewengineoptions.viewlocationformats#microsoft-aspnetcore-mvc-razor-razorviewengineoptions-viewlocationformats) collection. For example, to search for views within the path "/Components/{View Component Name}/{View Name}", add a new item to the collection:

```
services.AddMvc()
    .AddRazorOptions(options =>
    {
        options.ViewLocationFormats.Add("/{0}.cshtml");
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
```

In the preceding code, the placeholder "{0}" represents the path "Components/{View Component Name}/{View Name}".

## Invoking a view component

To use the view component, call the following inside a view:

```
@await Component.InvokeAsync("Name of view component", {Anonymous Type Containing Parameters})
```

The parameters will be passed to the `InvokeAsync` method. The `PriorityList` view component developed in the article is invoked from the `Views/ToDo/Index.cshtml` view file. In the following, the `InvokeAsync` method is called with two parameters:

```
@await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })
```

## Invoking a view component as a Tag Helper

For ASP.NET Core 1.1 and higher, you can invoke a view component as a [Tag Helper](tag-helpers/intro?view=aspnetcore-10.0):

```
<vc:priority-list max-priority="2" is-done="false">
</vc:priority-list>
```

Pascal-cased class and method parameters for Tag Helpers are translated into their [kebab case](https://stackoverflow.com/questions/11273282/whats-the-name-for-dash-separated-case/12273101). The Tag Helper to invoke a view component uses the `<vc></vc>` element. The view component is specified as follows:

```
<vc:[view-component-name]
  parameter1="parameter1 value"
  parameter2="parameter2 value">
</vc:[view-component-name]>
```

To use a view component as a Tag Helper, register the assembly containing the view component using the `@addTagHelper` directive. If your view component is in an assembly called `MyWebApp`, add the following directive to the `_ViewImports.cshtml` file:

```
@addTagHelper *, MyWebApp
```

You can register a view component as a Tag Helper to any file that references the view component. See [Managing Tag Helper Scope](tag-helpers/intro?view=aspnetcore-10.0#managing-tag-helper-scope) for more information on how to register Tag Helpers.

The `InvokeAsync` method used in this tutorial:

```
@await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })
```

In Tag Helper markup:

```
<vc:priority-list max-priority="2" is-done="false">
</vc:priority-list>
```

In the sample above, the `PriorityList` view component becomes `priority-list`. The parameters to the view component are passed as attributes in kebab case.

### Invoking a view component directly from a controller

View components are typically invoked from a view, but you can invoke them directly from a controller method. While view components don't define endpoints like controllers, you can easily implement a controller action that returns the content of a `ViewComponentResult`.

In this example, the view component is called directly from the controller:

```
public IActionResult IndexVC()
{
    return ViewComponent("PriorityList", new { maxPriority = 3, isDone = false });
}
```

## Walkthrough: Creating a simple view component

[Download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/view-components/samples/6.x), build and test the starter code. It's a simple project with a `ToDo` controller that displays a list of *ToDo* items.

### Add a ViewComponent class

Create a *ViewComponents* folder and add the following `PriorityListViewComponent` class:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents
{
    public class PriorityListViewComponent : ViewComponent
    {
        private readonly ToDoContext db;

        public PriorityListViewComponent(ToDoContext context)
        {
            db = context;
        }

        public async Task<IViewComponentResult> InvokeAsync(
        int maxPriority, bool isDone)
        {
            var items = await GetItemsAsync(maxPriority, isDone);
            return View(items);
        }
        private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
        {
            return db.ToDo.Where(x => x.IsDone == isDone &&
                                 x.Priority <= maxPriority).ToListAsync();
        }
    }
}
```

Notes on the code:

* View component classes can be contained in **any** folder in the project.

* Because the class name PriorityList**ViewComponent** ends with the suffix **ViewComponent**, the runtime uses the string `PriorityList` when referencing the class component from a view.

* The `[ViewComponent]` attribute can change the name used to reference a view component. For example, the class could have been named `XYZ` with the `ViewComponent` attribute:

  ```
  [ViewComponent(Name = "PriorityList")]
     public class XYZ : ViewComponent
  ```

* The `[ViewComponent]` attribute in the preceding code tells the view component selector to use:

  * The name `PriorityList` when looking for the views associated with the component
  * The string "PriorityList" when referencing the class component from a view.

* The component uses [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) to make the data context available.

* `InvokeAsync` exposes a method which can be called from a view, and it can take an arbitrary number of arguments.

* The `InvokeAsync` method returns the set of `ToDo` items that satisfy the `isDone` and `maxPriority` parameters.

### Create the view component Razor view

* Create the *Views/Shared/Components* folder. This folder ***must*** be named `Components`.

* Create the *Views/Shared/Components/PriorityList* folder. This folder name must match the name of the view component class, or the name of the class minus the suffix (if we followed convention and used the *ViewComponent* suffix in the class name). If you used the `ViewComponent` attribute, the class name would need to match the attribute designation.

* Create a `Views/Shared/Components/PriorityList/Default.cshtml` Razor view:

  ```
  @model IEnumerable<ViewComponentSample.Models.TodoItem>

  <h3>Priority Items</h3>
  <ul>
      @foreach (var todo in Model)
      {
          <li>@todo.Name</li>
      }
  </ul>
  ```

  The Razor view takes a list of `TodoItem` and displays them. If the view component `InvokeAsync` method doesn't pass the name of the view (as in our sample), *Default* is used for the view name by convention. Later in the tutorial, I'll show you how to pass the name of the view. To override the default styling for a specific controller, add a view to the controller-specific view folder (for example *Views/ToDo/Components/PriorityList/Default.cshtml)*.

  If the view component is controller-specific, you can add it to the controller-specific folder (`Views/ToDo/Components/PriorityList/Default.cshtml`).

* Add a `div` containing a call to the priority list component to the bottom of the `Views/ToDo/index.cshtml` file:

  ```
  </table>
  <div>
      @await Component.InvokeAsync("PriorityList", new { maxPriority = 2, isDone = false })
  </div>
  ```

The markup `@await Component.InvokeAsync` shows the syntax for calling view components. The first argument is the name of the component we want to invoke or call. Subsequent parameters are passed to the component. `InvokeAsync` can take an arbitrary number of arguments.

Test the app. The following image shows the ToDo list and the priority items:

You can also call the view component directly from the controller:

```
public IActionResult IndexVC()
{
    return ViewComponent("PriorityList", new { maxPriority = 3, isDone = false });
}
```

### Specifying a view name

A complex view component might need to specify a non-default view under some conditions. The following code shows how to specify the "PVC" view from the `InvokeAsync` method. Update the `InvokeAsync` method in the `PriorityListViewComponent` class.

```
public async Task<IViewComponentResult> InvokeAsync(
    int maxPriority, bool isDone)
{
    string MyView = "Default";
    // If asking for all completed tasks, render with the "PVC" view.
    if (maxPriority > 3 && isDone == true)
    {
        MyView = "PVC";
    }
    var items = await GetItemsAsync(maxPriority, isDone);
    return View(MyView, items);
}
```

Copy the `Views/Shared/Components/PriorityList/Default.cshtml` file to a view named `Views/Shared/Components/PriorityList/PVC.cshtml`. Add a heading to indicate the PVC view is being used.

```
@model IEnumerable<ViewComponentSample.Models.TodoItem>

<h2> PVC Named Priority Component View</h2>
<h4>@ViewBag.PriorityMessage</h4>
<ul>
    @foreach (var todo in Model)
    {
        <li>@todo.Name</li>
    }
</ul>
```

Update `Views/ToDo/Index.cshtml`:

```
@await Component.InvokeAsync("PriorityList", new { maxPriority = 4, isDone = true })
```

Run the app and verify PVC view.

If the PVC view isn't rendered, verify you are calling the view component with a priority of 4 or higher.

### Examine the view path

* Change the priority parameter to three or less so the priority view isn't returned.

* Temporarily rename the `Views/ToDo/Components/PriorityList/Default.cshtml` to `1Default.cshtml`.

* Test the app, you'll get the following error:

  ```
  An unhandled exception occurred while processing the request.
  InvalidOperationException: The view 'Components/PriorityList/Default' wasn't found. The following locations were searched:
  /Views/ToDo/Components/PriorityList/Default.cshtml
  /Views/Shared/Components/PriorityList/Default.cshtml
  EnsureSuccessful
  ```

* Copy `Views/ToDo/Components/PriorityList/1Default.cshtml` to `Views/Shared/Components/PriorityList/Default.cshtml`.

* Add some markup to the *Shared* ToDo view component view to indicate the view is from the *Shared* folder.

* Test the **Shared** component view.

### Avoiding hard-coded strings

If you want compile time safety, you can replace the hard-coded view component name with the class name. Create the view component without the "ViewComponent" suffix:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ViewComponentSample.Models;

namespace ViewComponentSample.ViewComponents
{
    public class PriorityList : ViewComponent
    {
        private readonly ToDoContext db;

        public PriorityList(ToDoContext context)
        {
            db = context;
        }

        public async Task<IViewComponentResult> InvokeAsync(
        int maxPriority, bool isDone)
        {
            var items = await GetItemsAsync(maxPriority, isDone);
            return View(items);
        }
        private Task<List<TodoItem>> GetItemsAsync(int maxPriority, bool isDone)
        {
            return db.ToDo.Where(x => x.IsDone == isDone &&
                                 x.Priority <= maxPriority).ToListAsync();
        }
    }
}
```

Add a `using` statement to your Razor view file, and use the `nameof` operator:

```
@using ViewComponentSample.Models
@using ViewComponentSample.ViewComponents
@model IEnumerable<TodoItem>

    <h2>ToDo nameof</h2>
    <!-- Markup removed for brevity.  -->

    <div>

        @*
            Note: 
            To use the below line, you need to #define no_suffix in ViewComponents/PriorityList.cs or it won't compile.
            By doing so it will cause a problem to index as there will be multiple viewcomponents 
            with the same name after the compiler removes the suffix "ViewComponent"
        *@

        @*@await Component.InvokeAsync(nameof(PriorityList), new { maxPriority = 4, isDone = true })*@
    </div>
```

You can use an overload of `Component.InvokeAsync` method that takes a CLR type. Remember to use the `typeof` operator in this case:

```
@using ViewComponentSample.Models
@using ViewComponentSample.ViewComponents
@model IEnumerable<TodoItem>

<h2>ToDo typeof</h2>
<!-- Markup removed for brevity.  -->

<div>
    @await Component.InvokeAsync(typeof(PriorityListViewComponent), new { maxPriority = 4, isDone = true })
</div>
```

## Perform synchronous work

The framework handles invoking a synchronous `Invoke` method if you don't need to perform asynchronous work. The following method creates a synchronous `Invoke` view component:

```
public class PriorityList : ViewComponent
{
    public IViewComponentResult Invoke(int maxPriority, bool isDone)
    {
        var items = new List<string> { $"maxPriority: {maxPriority}", $"isDone: {isDone}" };
        return View(items);
    }
}
```

The view component's Razor file lists the strings passed to the `Invoke` method (`Views/Home/Components/PriorityList/Default.cshtml`):

```
@model List<string>

<h3>Priority Items</h3>
<ul>
    @foreach (var item in Model)
    {
        <li>@item</li>
    }
</ul>
```

The view component is invoked in a Razor file (for example, `Views/Home/Index.cshtml`) using one of the following approaches:

* [IViewComponentHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iviewcomponenthelper)
* [Tag Helper](tag-helpers/intro?view=aspnetcore-10.0)

To use the [IViewComponentHelper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.iviewcomponenthelper) approach, call `Component.InvokeAsync`:

```
@await Component.InvokeAsync(nameof(PriorityList), new { maxPriority = 4, isDone = true })
```

To use the Tag Helper, register the assembly containing the View Component using the `@addTagHelper` directive (the view component is in an assembly called `MyWebApp`):

```
@addTagHelper *, MyWebApp
```

Use the view component Tag Helper in the Razor markup file:

```
<vc:priority-list max-priority="999" is-done="false">
</vc:priority-list>
```

The method signature of `PriorityList.Invoke` is synchronous, but Razor finds and calls the method with `Component.InvokeAsync` in the markup file.

## All view component parameters are required

Each parameter in a view component is a required attribute. See [this GitHub issue](https://github.com/dotnet/AspNetCore/issues/5011). If any parameter is omitted:

* The `InvokeAsync` method signature won't match, therefore the method won't execute.
* The ViewComponent won't render any markup.
* No errors will be thrown.

## Additional resources

* [Dependency injection into views](dependency-injection?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/supported-platforms
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/supported-platforms.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core SignalR supported platforms

## Server system requirements

SignalR for ASP.NET Core supports any server platform that ASP.NET Core supports.

## JavaScript client

The [JavaScript client](javascript-client?view=aspnetcore-10.0) runs on the current [Node.js long-term support (LTS) release](https://nodejs.org/en/download) and the following browsers:

| Browser                          | Version  |
| -------------------------------- | -------- |
| Apple Safari, including iOS      | Current† |
| Google Chrome, including Android | Current† |
| Microsoft Edge                   | Current† |
| Mozilla Firefox                  | Current† |

†*Current* refers to the latest version of the browser.

The JavaScript client doesn't support Internet Explorer and other older browsers. The client might have unexpected behavior and errors on unsupported browsers.

## .NET client

The [.NET client](dotnet-client?view=aspnetcore-10.0) runs on any platform supported by ASP.NET Core. For example, [.NET Multi-platform App UI (.NET MAUI) developers can use SignalR](https://github.com/aspnet/Announcements/issues/305) for building Android and iOS apps.

If the server runs IIS, the WebSockets transport requires IIS 8.0 or later on Windows Server 2012 or later. Other transports are supported on all platforms.

## Java client

The [Java client](java-client?view=aspnetcore-10.0) supports Java 8 or later versions.

## Swift client

The [Swift client](swift-client?view=aspnetcore-10.0) supports Swift >= 5.10

## Unsupported clients

The following client is available for experimentation only, isn't currently supported, and may never be supported:

* [C++ client](https://github.com/aspnet/SignalR-Client-Cpp)

[]()

## Browsers that don't support ECMAScript 6 (ES6)

SignalR targets ES6. For browsers that don't support ES6, transpile the library to ES5. For more information, see [Getting Started with ES6 – Transpiling ES6 to ES5 with Traceur and Babel](https://blog.codewithdan.com/getting-started-with-es6-transpiling-es6-to-es5-with-traceur-and-babel/).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/js-spa-frameworks
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/js-spa-frameworks.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use Razor components in JavaScript apps and SPA frameworks

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article covers how to render Razor components from JavaScript, use Blazor custom elements, and generate Angular and React components.

Note

We recommend using the `blazor.server.js` (Blazor Server) and `blazor.webassembly.js` (Blazor WebAssembly) scripts when integrating Razor components into an existing JavaScript app until better support for the `blazor.web.js` (Blazor Web App) script is added in the future. For more information, see [RegisterCustomElement stopped working in Blazor 8 (`dotnet/aspnetcore` #53920)](https://github.com/dotnet/aspnetcore/issues/53920#issuecomment-2261507850).

## Angular sample apps

The following sample apps demonstrate rendering a Razor component as a [Blazor custom element](#blazor-custom-elements) in an Angular app:

* [CustomElementsBlazorSample (Blazor Server) (`javiercn/CustomElementsBlazorSample` GitHub repository, branch: `blazor-server`)](https://github.com/javiercn/CustomElementsBlazorSample/tree/blazor-server)
* [CustomElementsBlazorSample (Blazor WebAssembly) (`javiercn/CustomElementsBlazorSample` GitHub repository, branch: `blazor-wasm`)](https://github.com/javiercn/CustomElementsBlazorSample/tree/blazor-wasm)

To migrate either of these .NET 7 samples, see the following resources:

* [Migrate from ASP.NET Core in .NET 7 to .NET 8](../../migration/70-to-80?view=aspnetcore-10.0)
* [Migrate from ASP.NET Core in .NET 8 to ASP.NET Core in .NET 9](../../migration/80-to-90?view=aspnetcore-10.0)
* [Migrate from ASP.NET Core in .NET 9 to ASP.NET Core in .NET 10](../../migration/90-to-100?view=aspnetcore-10.0)

The principal updates to make are:

* Update the [target framework monikers (TFMs)](/en-us/dotnet/standard/frameworks) to the latest version.

* Update the .NET package references and Angular dependencies to their latest versions.

  Note

  For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

## Render Razor components from JavaScript

Razor components can be dynamically-rendered from JavaScript (JS) for existing JS apps.

The example in this section renders the following Razor component into a page via JS.

`Quote.razor`:

```
<div class="m-5 p-5">
    <h2>Quote</h2>
    <p>@Text</p>
</div>

@code {
    [Parameter]
    public string? Text { get; set; }
}
```

In the `Program` file, add the [namespace for the location of the component](./?view=aspnetcore-10.0#component-name-class-name-and-namespace).

Call [RegisterForJavaScript](/en-us/dotnet/api/microsoft.aspnetcore.components.web.jscomponentconfigurationextensions.registerforjavascript) on the app's root component collection to register a Razor component as a root component for JS rendering.

[RegisterForJavaScript](/en-us/dotnet/api/microsoft.aspnetcore.components.web.jscomponentconfigurationextensions.registerforjavascript) includes an overload that accepts the name of a JS function that executes initialization logic (`javaScriptInitializer`). The JS function is called once per component registration immediately after the Blazor app starts and before any components are rendered. This function can be used for integration with JS technologies, such as HTML custom elements or a JS-based SPA framework.

One or more initializer functions can be created and called by different component registrations. The typical use case is to reuse the same initializer function for multiple components, which is expected if the initializer function is configuring integration with custom elements or another JS-based SPA framework.

Important

Don't confuse the `javaScriptInitializer` parameter of [RegisterForJavaScript](/en-us/dotnet/api/microsoft.aspnetcore.components.web.jscomponentconfigurationextensions.registerforjavascript) with [JavaScript initializers](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers). The name of the parameter and the JS initializers feature is coincidental.

The following example demonstrates the dynamic registration of the preceding `Quote` component with "`quote`" as the identifier.

* In a Blazor Server app, modify the call to [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor) in the `Program` file:

  ```
  builder.Services.AddServerSideBlazor(options =>
  {
      options.RootComponents.RegisterForJavaScript<Quote>(identifier: "quote", 
          javaScriptInitializer: "initializeComponent");
  });
  ```

* In a Blazor WebAssembly app, call [RegisterForJavaScript](/en-us/dotnet/api/microsoft.aspnetcore.components.web.jscomponentconfigurationextensions.registerforjavascript) on [RootComponents](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.rootcomponents#microsoft-aspnetcore-components-webassembly-hosting-webassemblyhostbuilder-rootcomponents) in the client-side `Program` file:

  ```
  builder.RootComponents.RegisterForJavaScript<Quote>(identifier: "quote", 
      javaScriptInitializer: "initializeComponent");
  ```

Attach the initializer function with `name` and `parameters` function parameters to the `window` object. For demonstration purposes, the following `initializeComponent` function logs the name and parameters of the registered component.

`wwwroot/jsComponentInitializers.js`:

```
window.initializeComponent = (name, parameters) => {
  console.log({ name: name, parameters: parameters });
}
```

Render the component from JS into a container element using the registered identifier, passing component parameters as needed.

In the following example:

* The `Quote` component (`quote` identifier) is rendered into the `quoteContainer` element when the `showQuote` function is called.
* A quote string is passed to the component's `Text` parameter.

`wwwroot/scripts.js`:

```
window.showQuote = async () => {
  let targetElement = document.getElementById('quoteContainer');
  await Blazor.rootComponents.add(targetElement, 'quote', 
  {
    text: "Crow: I have my doubts that this movie is actually 'starring' " +
      "anybody. More like, 'camera is generally pointed at.'"
  });
}

const btn = document.querySelector("#showQuoteBtn");
btn.addEventListener("click", showQuote);
```

After the [Blazor script](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script) is loaded, load the preceding scripts into the JS app:

```
<script src="_framework/{BLAZOR SCRIPT}"></script>
<script src="jsComponentInitializers.js"></script>
<script src="scripts.js"></script>
```

In the preceding example, the `{BLAZOR SCRIPT}` placeholder is the Blazor script.

In HTML, place the target container element (`quoteContainer`). For the demonstration in this section, a button triggers rendering the `Quote` component by calling the `showQuote` JS function:

```
<button id="showQuoteBtn">Show Quote</button>

<div id="quoteContainer"></div>
```

On initialization before any components are rendered, the browser's developer tools console logs the `Quote` component's identifier (`name`) and parameters (`parameters`) when `initializeComponent` is called:

```
Object { name: "quote", parameters: (1) […] }
  name: "quote"
  parameters: Array [ {…} ]
    0: Object { name: "Text", type: "string" }
    length: 1
```

When the **Show Quote** button is selected, the `Quote` component is rendered with the quote stored in `Text` displayed:

Quote ©1988-1999 Satellite of Love LLC: [*Mystery Science Theater 3000*](https://mst3k.com/) ([Trace Beaulieu (Crow)](https://www.imdb.com/name/nm0064546/))

Note

`rootComponents.add` returns an instance of the component. Call `dispose` on the instance to release it:

```
const rootComponent = await window.Blazor.rootComponents.add(...);

...

rootComponent.dispose();
```

The preceding example dynamically renders the root component when the `showQuote()` JS function is called. To render a root component into a container element when Blazor starts, use a [JavaScript initializer](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers) to render the component, as the following example demonstrates.

The following example builds on the preceding example, using the `Quote` component, the root component registration in the `Program` file, and the initialization of `jsComponentInitializers.js`. The `showQuote()` function (and the `script.js` file) aren't used.

In HTML, place the target container element, `quoteContainer2` for this example:

```
<div id="quoteContainer2"></div>
```

Using a [JavaScript initializer](../fundamentals/startup?view=aspnetcore-10.0#javascript-initializers), add the root component to the target container element.

`wwwroot/{PACKAGE ID/ASSEMBLY NAME}.lib.module.js`:

```
export function afterStarted(blazor) {
  let targetElement = document.getElementById('quoteContainer2');
  blazor.rootComponents.add(targetElement, 'quote',
    {
      text: "Crow: I have my doubts that this movie is actually 'starring' " +
          "anybody. More like, 'camera is generally pointed at.'"
    });
}
```

Note

For the call to `rootComponents.add`, use the `blazor` parameter (lowercase `b`) provided by the Blazor start event. Although the registration is valid when using the `Blazor` object (uppercase `B`), the preferred approach is to use the parameter.

For an advanced example with additional features, see the example in the `BasicTestApp` of the ASP.NET Core reference source (`dotnet/aspnetcore` GitHub repository):

* [`JavaScriptRootComponents.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/JavaScriptRootComponents.razor)
* [`wwwroot/js/jsRootComponentInitializers.js`](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/wwwroot/js/jsRootComponentInitializers.js)
* [`wwwroot/index.html`](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/wwwroot/index.html)

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Blazor custom elements

Use Blazor custom elements to dynamically render Razor components from different JavaScript technologies, such as [Angular](https://angular.dev/), [React](https://react.dev/), and [Vue](https://vuejs.org/).

Blazor custom elements:

* Use standard HTML interfaces to implement custom HTML elements.
* Eliminate the need to manually manage the state and lifecycle of root Razor components using JavaScript APIs.
* Are useful for gradually introducing Razor components into existing projects written in other technologies.

Custom elements don't support [child content](./?view=aspnetcore-10.0#child-content-render-fragments) or [templated components](templated-components?view=aspnetcore-10.0).

### Element name

Per the [HTML specification](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-core-concepts), custom element tag names must adopt kebab case:

❌Invalid: `mycounter`\
❌Invalid: `MY-COUNTER`\
❌Invalid: `MyCounter`\
✔️Valid: `my-counter`\
✔️Valid: `my-cool-counter`

### Package

Add a package reference for [`Microsoft.AspNetCore.Components.CustomElements`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.CustomElements) to the app's project file.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

### Example component

The following examples are based on the `Counter` component from the Blazor project template.

`Counter.razor`:

```
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount() => currentCount++;
}
```

### Blazor Server registration

Take the following steps to register a root component as a custom element in a Blazor Server app.

Add the [Microsoft.AspNetCore.Components.Web](/en-us/dotnet/api/microsoft.aspnetcore.components.web) namespace to the top of the `Program` file:

```
using Microsoft.AspNetCore.Components.Web;
```

Add a namespace for the app's components. In the following example, the app's namespace is `BlazorSample` and the components are located in the `Pages` folder:

```
using BlazorSample.Pages;
```

Modify the call to [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor). Specify the custom element with [RegisterCustomElement](/en-us/dotnet/api/microsoft.aspnetcore.components.web.customelementsjscomponentconfigurationextensions.registercustomelement) on the [RootComponents](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.rootcomponents#microsoft-aspnetcore-components-server-circuitoptions-rootcomponents) circuit option. The following example registers the `Counter` component with the custom HTML element `my-counter`:

```
builder.Services.AddServerSideBlazor(options =>
{
    options.RootComponents.RegisterCustomElement<Counter>("my-counter");
});
```

### Blazor WebAssembly registration

Register a root component as a custom element in a Blazor WebAssembly app. In the following example, the code:

* Adds a namespace for the app's components. In the example, the app's namespace is `BlazorSample`, and the components are located in the `Pages` folder.
* Provides access to the API in the [Microsoft.AspNetCore.Components.Web](/en-us/dotnet/api/microsoft.aspnetcore.components.web) namespace.
* Calls [RegisterCustomElement](/en-us/dotnet/api/microsoft.aspnetcore.components.web.customelementsjscomponentconfigurationextensions.registercustomelement) on [RootComponents](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.rootcomponents#microsoft-aspnetcore-components-webassembly-hosting-webassemblyhostbuilder-rootcomponents) to register the `Counter` component with the custom HTML element `my-counter`.

```
using BlazorSample.Pages;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.RootComponents.RegisterCustomElement<Counter>("my-counter");

await builder.Build().RunAsync();
```

### Use the registered custom element

Use the custom element with any web framework. For example, the preceding `my-counter` custom HTML element that renders the app's `Counter` component is used in a React app with the following markup:

```
<my-counter></my-counter>
```

For a complete example of how to create custom elements with Blazor, see the [`CustomElementsComponent` component](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/CustomElementsComponent.razor) in the reference source.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Pass parameters

Pass parameters to your Razor component either as HTML attributes or as JavaScript properties on the DOM element.

The following `Counter` component uses an `IncrementAmount` parameter to set the increment amount of the **Click me** button.

`Counter.razor`:

```
<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    [Parameter]
    public int IncrementAmount { get; set; } = 1;

    private void IncrementCount()
    {
        currentCount += IncrementAmount;
    }
}
```

Render the `Counter` component with the custom element and pass a value to the `IncrementAmount` parameter as an HTML attribute. The attribute name adopts kebab-case syntax (`increment-amount`, not `IncrementAmount`):

```
<my-counter increment-amount="10"></my-counter>
```

Alternatively, you can set the parameter's value as a JavaScript property on the element object. The property name adopts camel case syntax (`incrementAmount`, not `IncrementAmount`):

```
const elem = document.querySelector("my-counter");
elem.incrementAmount = 10;
```

You can update parameter values at any time using either attribute or property syntax.

Supported parameter types:

* Using JavaScript property syntax, you can pass objects of any JSON-serializable type.
* Using HTML attributes, you are limited to passing objects of string, boolean, or numerical types.

*Experimental* support is available for building custom elements using the [`Microsoft.AspNetCore.Components.CustomElements` NuGet package](https://www.nuget.org/packages/microsoft.aspnetcore.components.customelements). Custom elements use standard HTML interfaces to implement custom HTML elements.

Warning

Experimental features are provided for the purpose of exploring feature viability and may not ship in a stable version.

Register a root component as a custom element:

* In a Blazor Server app, modify the call to [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor) in the `Program` file to call [RegisterCustomElement](/en-us/dotnet/api/microsoft.aspnetcore.components.web.customelementsjscomponentconfigurationextensions.registercustomelement) on [CircuitOptions.RootComponents](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.rootcomponents):

  ```
  builder.Services.AddServerSideBlazor(options =>
  {
      options.RootComponents.RegisterCustomElement<Counter>("my-counter");
  });
  ```

  Note

  The preceding code example requires a namespace for the app's components (for example, `using BlazorSample.Components.Pages;`) in the `Program` file.

* In a Blazor WebAssembly app, call [RegisterCustomElement](/en-us/dotnet/api/microsoft.aspnetcore.components.web.customelementsjscomponentconfigurationextensions.registercustomelement) on [WebAssemblyHostBuilder.RootComponents](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.rootcomponents#microsoft-aspnetcore-components-webassembly-hosting-webassemblyhostbuilder-rootcomponents) in the `Program` file:

  ```
  builder.RootComponents.RegisterCustomElement<Counter>("my-counter");
  ```

  Note

  The preceding code example requires a namespace for the app's components (for example, `using BlazorSample.Components.Pages;`) in the `Program` file.

Include the following `<script>` tag in the app's HTML ***before*** the Blazor script tag:

```
<script src="/_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js"></script>
```

Use the custom element with any web framework. For example, the preceding counter custom element is used in a React app with the following markup:

```
<my-counter increment-amount={incrementAmount}></my-counter>
```

Warning

The custom elements feature is currently **experimental, unsupported, and subject to change or be removed at any time**. We welcome your feedback on how well this particular approach meets your requirements.

## Generate Angular and React components

Generate JavaScript (JS) components from Razor components for JavaScript technologies, such as Angular or React. This capability isn't included with .NET, but is enabled by the support for rendering Razor components from JS. The [JS component generation sample on GitHub](https://github.com/aspnet/samples/tree/main/samples/aspnetcore/blazor/JSComponentGeneration) demonstrates how to generate Angular and React components from Razor components. See the GitHub sample app's `README.md` file for additional information.

Warning

The Angular and React component features are currently **experimental, unsupported, and subject to change or be removed at any time**. We welcome your feedback on how well this particular approach meets your requirements.

## Related content

[Host and deploy ASP.NET Core Blazor](../host-and-deploy/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/images-and-documents
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/images-and-documents.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Display images and documents in ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes approaches for displaying images and documents in Blazor apps.

The examples in this article are available for inspection and use in the [Blazor sample apps](fundamentals/?view=aspnetcore-10.0#sample-apps):

[`dotnet/blazor-samples` GitHub repository](https://github.com/dotnet/blazor-samples): Navigate to the app named `BlazorSample_BlazorWebApp` (8.0 or later), `BlazorSample_Server` (7.0 or earlier), or `BlazorSample_WebAssembly`.

## Dynamically set an image source

The following example demonstrates how to dynamically set an image's source with a C# field.

The example in this section uses three image files, named `image1.png`, `image2.png`, and `image3.png`. The images are placed in a folder named `images` in the app's web root (`wwwroot`). The use of the `images` folder is only for demonstration purposes. You can organize static assets in any folder layout that you prefer, including serving assets directly from the `wwwroot` folder.

In the following `ShowImage1` component:

* The image's source (`src`) is dynamically set to the value of `imageSource` in C#.
* The `ShowImage` method updates the `imageSource` field based on an image `id` argument passed to the method.
* Rendered buttons call the `ShowImage` method with an image argument for each of the three available images in the `images` folder. The file name is composed using the argument passed to the method and matches one of the three images in the `images` folder.

`ShowImage1.razor`:

```
@page "/show-image-1"

<PageTitle>Show Image 1</PageTitle>

<h1>Show Image Example 1</h1>

@if (imageSource is not null)
{
    <p>
        <img src="@imageSource" />
    </p>
}

@for (var i = 1; i <= 3; i++)
{
    var imageId = i;
    <button @onclick="() => ShowImage(imageId)">
        Image @imageId
    </button>
}

@code {
    private string? imageSource;

    private void ShowImage(int id) => imageSource = $"images/image{id}.png";
}
```

```
@page "/show-image-1"

<PageTitle>Show Image 1</PageTitle>

<h1>Show Image Example 1</h1>

@if (imageSource is not null)
{
    <p>
        <img src="@imageSource" />
    </p>
}

@for (var i = 1; i <= 3; i++)
{
    var imageId = i;
    <button @onclick="() => ShowImage(imageId)">
        Image @imageId
    </button>
}

@code {
    private string? imageSource;

    private void ShowImage(int id) => imageSource = $"images/image{id}.png";
}
```

```
@page "/show-image-1"

<h1>Dynamic Image Source Example</h1>

@if (imageSource is not null)
{
    <p>
        <img src="@imageSource" />
    </p>
}

@for (var i = 1; i <= 3; i++)
{
    var imageId = i;
    <button @onclick="() => ShowImage(imageId)">
        Image @imageId
    </button>
}

@code {
    private string? imageSource;

    private void ShowImage(int id)
    {
        imageSource = $"images/image{id}.png";
    }
}
```

```
@page "/show-image-1"

<h1>Dynamic Image Source Example</h1>

@if (imageSource is not null)
{
    <p>
        <img src="@imageSource" />
    </p>
}

@for (var i = 1; i <= 3; i++)
{
    var imageId = i;
    <button @onclick="() => ShowImage(imageId)">
        Image @imageId
    </button>
}

@code {
    private string? imageSource;

    private void ShowImage(int id)
    {
        imageSource = $"images/image{id}.png";
    }
}
```

The preceding example uses a C# field to hold the image's source data, but you can also use a C# property to hold the data.

Avoid using a loop variable directly in a lambda expression, such as `i` in the preceding `for` loop example. Otherwise, the same variable is used by all lambda expressions, which results in use of the same value in all lambdas. Capture the variable's value in a local variable. In the preceding example:

* The loop variable `i` is assigned to `imageId`.
* `imageId` is used in the lambda expression.

Alternatively, use a `foreach` loop with [Enumerable.Range](/en-us/dotnet/api/system.linq.enumerable.range), which doesn't suffer from the preceding problem:

```
@foreach (var imageId in Enumerable.Range(1, 3))
{
    <button @onclick="() => ShowImage(imageId)">
        Image @imageId
    </button>
}
```

For more information on lambda expressions with event handling, see [ASP.NET Core Blazor event handling](components/event-handling?view=aspnetcore-10.0#lambda-expressions).

## Stream image or document data

An image or other document type, such as a PDF, can be directly transmitted to the client using Blazor's streaming interop features instead of hosting the file at a public URL.

The example in this section streams source data using [JavaScript (JS) interop](javascript-interoperability/?view=aspnetcore-10.0). The following `setSource` JS function:

* Can be used to stream content for the following elements: `<body>`, `<embed>`, `<iframe>`, `<img>`, `<link>`, `<object>`, `<script>`, `<style>`, and `<track>`.
* Accepts an element `id` to display the file's contents, a data stream for the document, the content type, and a title for the display element.

The function:

* Reads the provided stream into an [`ArrayBuffer`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer).
* Creates a [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob) to wrap the `ArrayBuffer`, setting the blob's content type.
* Creates an object URL to serve as the address for the document to be shown.
* Set's the element's title (`title`) from the `title` parameter and sets the element's source (`src`) from the created object URL.
* To prevent memory leaks, the function calls [`revokeObjectURL`](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL) to dispose of the object URL after the element loads the resource ([`load` event](https://developer.mozilla.org/docs/Web/API/HTMLElement/load_event)).

```
<script>
  window.setSource = async (elementId, stream, contentType, title) => {
    const arrayBuffer = await stream.arrayBuffer();
    let blobOptions = {};
    if (contentType) {
      blobOptions['type'] = contentType;
    }
    const blob = new Blob([arrayBuffer], blobOptions);
    const url = URL.createObjectURL(blob);
    const element = document.getElementById(elementId);
    element.title = title;
    element.onload = () => {
      URL.revokeObjectURL(url);
    }
    element.src = url;
  }
</script>
```

Note

For general guidance on JS location and our recommendations for production apps, see [JavaScript location in ASP.NET Core Blazor apps](javascript-interoperability/location-of-javascript?view=aspnetcore-10.0).

The following `ShowImage2` component:

* Injects services for an [System.Net.Http.HttpClient](/en-us/dotnet/api/system.net.http.httpclient) and [Microsoft.JSInterop.IJSRuntime](/en-us/dotnet/api/microsoft.jsinterop.ijsruntime).

* Includes an `<img>` tag to display an image.

* Has a `GetImageStreamAsync` C# method to retrieve a [Stream](/en-us/dotnet/api/system.io.stream) for an image. A production app may dynamically generate an image based on the specific user or retrieve an image from storage. The following example retrieves the .NET avatar for the `dotnet` GitHub repository.

* Has a `SetImageAsync` method that's triggered on the button's selection by the user. `SetImageAsync` performs the following steps:

  * Retrieves the [Stream](/en-us/dotnet/api/system.io.stream) from `GetImageStreamAsync`.
  * Wraps the [Stream](/en-us/dotnet/api/system.io.stream) in a [DotNetStreamReference](/en-us/dotnet/api/microsoft.jsinterop.dotnetstreamreference), which allows streaming the image data to the client.
  * Invokes the `setSource` JavaScript function, which accepts the data on the client.

Note

Server-side apps use a dedicated [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service to make requests, so no action is required by the developer of a server-side Blazor app to register an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service. Client-side apps have a default [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service registration when the app is created from a Blazor project template. If an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) service registration isn't present in the `Program` file of a client-side app, provide one by adding `builder.Services.AddHttpClient();`. For more information, see [HTTP requests with IHttpClientFactory - ASP.NET Core](../fundamentals/http-requests?view=aspnetcore-10.0).

`ShowImage2.razor`:

```
@page "/show-image-2"
@inject HttpClient Http
@inject IJSRuntime JS

<PageTitle>Show Image 2</PageTitle>

<h1>Show Image Example 2</h1>

<button @onclick="SetImageAsync">
    Set Image
</button>

<div class="p-3">
    <img id="avatar" />
</div>

@code {
    private async Task<Stream> GetImageStreamAsync() => 
        await Http.GetStreamAsync("https://avatars.githubusercontent.com/u/9141961");

    private async Task SetImageAsync()
    {
        var imageStream = await GetImageStreamAsync();
        var strRef = new DotNetStreamReference(imageStream);
        await JS.InvokeVoidAsync("setSource", "avatar", strRef, "image/png", 
            ".NET GitHub avatar");
    }
}
```

```
@page "/show-image-2"
@inject HttpClient Http
@inject IJSRuntime JS

<PageTitle>Show Image 2</PageTitle>

<h1>Show Image Example 2</h1>

<button @onclick="SetImageAsync">
    Set Image
</button>

<div class="p-3">
    <img id="avatar" />
</div>

@code {
    private async Task<Stream> GetImageStreamAsync() => 
        await Http.GetStreamAsync("https://avatars.githubusercontent.com/u/9141961");

    private async Task SetImageAsync()
    {
        var imageStream = await GetImageStreamAsync();
        var strRef = new DotNetStreamReference(imageStream);
        await JS.InvokeVoidAsync("setSource", "avatar", strRef, "image/png", 
            ".NET GitHub avatar");
    }
}
```

```
@page "/show-image-2"
@inject HttpClient Http
@inject IJSRuntime JS

<h1>Show Image Example 2</h1>

<button @onclick="SetImageAsync">
    Set Image
</button>

<div class="p-3">
    <img id="avatar" />
</div>

@code {
    private async Task<Stream> GetImageStreamAsync()
    {
        return await Http.GetStreamAsync(
            "https://avatars.githubusercontent.com/u/9141961");
    }

    private async Task SetImageAsync()
    {
        var imageStream = await GetImageStreamAsync();
        var strRef = new DotNetStreamReference(imageStream);
        await JS.InvokeVoidAsync("setSource", "avatar", strRef, "image/png", 
            ".NET GitHub avatar");
    }
}
```

```
@page "/show-image-2"
@inject HttpClient Http
@inject IJSRuntime JS

<h1>Show Image Example 2</h1>

<button @onclick="SetImageAsync">
    Set Image
</button>

<div class="p-3">
    <img id="avatar" />
</div>

@code {
    private async Task<Stream> GetImageStreamAsync()
    {
        return await Http.GetStreamAsync(
            "https://avatars.githubusercontent.com/u/9141961");
    }

    private async Task SetImageAsync()
    {
        var imageStream = await GetImageStreamAsync();
        var strRef = new DotNetStreamReference(imageStream);
        await JS.InvokeVoidAsync("setSource", "avatar", strRef, "image/png", 
            ".NET GitHub avatar");
    }
}
```

The following `ShowFile` component loads either a text file (`files/quote.txt`) or a PDF file (`files/quote.pdf`) into an [`<iframe>` element](https://developer.mozilla.org/docs/Web/HTML/Element/iframe).

Warning

Use of the `<iframe>` element in the following example is safe and doesn't require [sandboxing](https://developer.mozilla.org/docs/Web/HTML/Element/iframe#sandbox) because content is loaded from the app, which is a trusted source.

When loading content from an untrusted source or user input, an improperly implemented `<iframe>` element risks creating security vulnerabilities.

`ShowFile.razor`:

```
@page "/show-file"
@inject NavigationManager Navigation
@inject HttpClient Http
@inject IJSRuntime JS

<PageTitle>Show File</PageTitle>

<div class="d-flex flex-column">
    <h1>Show File Example</h1>
    <div class="mb-4">
        <button @onclick="@(() => ShowFileAsync("files/quote.txt", 
                "General Ravon quote (text file)"))">
            Show text ('quote.txt')
        </button>
        <button @onclick="@(() => ShowFileAsync("files/quote.pdf", 
                "General Ravon quote (PDF file)"))">
            Show PDF ('quote.pdf')
        </button>
    </div>
    <iframe id="iframe" style="height: calc(100vh - 200px)" />
</div>

@code
{
    private async Task ShowFileAsync(string url, string title)
    {
        var absoluteUrl = Navigation.ToAbsoluteUri(url);
        using var response = await Http.GetAsync(absoluteUrl);
        string? contentType = null;

        if (response.Content.Headers.TryGetValues("Content-Type", out var values))
        {
            contentType = values.FirstOrDefault();
        }

        var fileStream = await response.Content.ReadAsStreamAsync();
        var strRef = new DotNetStreamReference(fileStream);

        await JS.InvokeVoidAsync("setSource", "iframe", strRef, contentType, title);
    }
}
```

```
@page "/show-file"
@inject NavigationManager Navigation
@inject HttpClient Http
@inject IJSRuntime JS

<PageTitle>Show File</PageTitle>

<div class="d-flex flex-column">
    <h1>Show File Example</h1>
    <div class="mb-4">
        <button @onclick="@(() => ShowFileAsync("files/quote.txt", 
                "General Ravon quote (text file)"))">
            Show text ('quote.txt')
        </button>
        <button @onclick="@(() => ShowFileAsync("files/quote.pdf", 
                "General Ravon quote (PDF file)"))">
            Show PDF ('quote.pdf')
        </button>
    </div>
    <iframe id="iframe" style="height: calc(100vh - 200px)" />
</div>

@code
{
    private async Task ShowFileAsync(string url, string title)
    {
        var absoluteUrl = Navigation.ToAbsoluteUri(url);
        using var response = await Http.GetAsync(absoluteUrl);
        string? contentType = null;

        if (response.Content.Headers.TryGetValues("Content-Type", out var values))
        {
            contentType = values.FirstOrDefault();
        }

        var fileStream = await response.Content.ReadAsStreamAsync();
        var strRef = new DotNetStreamReference(fileStream);

        await JS.InvokeVoidAsync("setSource", "iframe", strRef, contentType, title);
    }
}
```

```
@page "/show-file"
@inject NavigationManager NavigationManager
@inject HttpClient Http
@inject IJSRuntime JS

<div class="d-flex flex-column">
    <h1>Show File Example</h1>
    <div class="mb-4">
        <button @onclick="@(() => ShowFileAsync("files/quote.txt", 
                "General Ravon quote (text file)"))">
            Show text ('quote.txt')
        </button>
        <button @onclick="@(() => ShowFileAsync("files/quote.pdf", 
                "General Ravon quote (PDF file)"))">
            Show PDF ('quote.pdf')
        </button>
    </div>
    <iframe id="iframe" style="height: calc(100vh - 200px)" />
</div>

@code
{
    private async Task ShowFileAsync(string url, string title)
    {
        var absoluteUrl = NavigationManager.ToAbsoluteUri(url);
        using var response = await Http.GetAsync(absoluteUrl);
        string? contentType = null;

        if (response.Content.Headers.TryGetValues("Content-Type", out var values))
        {
            contentType = values.FirstOrDefault();
        }

        var fileStream = await response.Content.ReadAsStreamAsync();
        var strRef = new DotNetStreamReference(fileStream);

        await JS.InvokeVoidAsync("setSource", "iframe", strRef, contentType, title);
    }
}
```

```
@page "/show-file"
@inject NavigationManager NavigationManager
@inject HttpClient Http
@inject IJSRuntime JS

<div class="d-flex flex-column">
    <h1>Show File Example</h1>
    <div class="mb-4">
        <button @onclick="@(() => ShowFileAsync("files/quote.txt", 
                "General Ravon quote (text file)"))">
            Show text ('quote.txt')
        </button>
        <button @onclick="@(() => ShowFileAsync("files/quote.pdf", 
                "General Ravon quote (PDF file)"))">
            Show PDF ('quote.pdf')
        </button>
    </div>
    <iframe id="iframe" style="height: calc(100vh - 200px)" />
</div>

@code
{
    private async Task ShowFileAsync(string url, string title)
    {
        var absoluteUrl = NavigationManager.ToAbsoluteUri(url);
        using var response = await Http.GetAsync(absoluteUrl);
        string? contentType = null;

        if (response.Content.Headers.TryGetValues("Content-Type", out var values))
        {
            contentType = values.FirstOrDefault();
        }

        var fileStream = await response.Content.ReadAsStreamAsync();
        var strRef = new DotNetStreamReference(fileStream);

        await JS.InvokeVoidAsync("setSource", "iframe", strRef, contentType, title);
    }
}
```

In the preceding example, the `using` statement for the `response` variable doesn't dispose of the [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage) instance until the scope of `ShowFileAsync` ends. The open stream is maintained long enough to transfer the file data to the `setSource` function via JavaScript interop. For general guidance on the importance of disposing of [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage) instances, see [Call a web API from an ASP.NET Core Blazor app](call-web-api?view=aspnetcore-10.0#disposal-of-httprequestmessage-httpresponsemessage-and-httpclient).

## Related content

* [ASP.NET Core Blazor file uploads](file-uploads?view=aspnetcore-10.0)
* [File uploads: Upload image preview](file-uploads?view=aspnetcore-10.0#upload-image-preview)
* [ASP.NET Core Blazor file downloads](file-downloads?view=aspnetcore-10.0)
* [Call .NET methods from JavaScript functions in ASP.NET Core Blazor](javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0#stream-from-javascript-to-net)
* [Call JavaScript functions from .NET methods in ASP.NET Core Blazor](javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0#stream-from-net-to-javascript)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](fundamentals/?view=aspnetcore-10.0#sample-apps))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/1x-to-2x/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core 1.x to 2.0

By [Scott Addie](https://github.com/scottaddie)

In this article, we walk you through updating an existing ASP.NET Core 1.x project to ASP.NET Core 2.0. Migrating your application to ASP.NET Core 2.0 enables you to take advantage of [many new features and performance improvements](../../release-notes/aspnetcore-2.0?view=aspnetcore-10.0).

Existing ASP.NET Core 1.x applications are based off of version-specific project templates. As the ASP.NET Core framework evolves, so do the project templates and the starter code contained within them. In addition to updating the ASP.NET Core framework, you need to update the code for your application.

[]()

## Prerequisites

Follow the [Get started with ASP.NET Core](../../get-started?view=aspnetcore-10.0) tutorial.

[]()

## Update Target Framework Moniker (TFM)

Projects targeting .NET Core should use the [TFM](/en-us/dotnet/standard/frameworks) of a version greater than or equal to .NET Core 2.0. Search for the `<TargetFramework>` node in the `.csproj` file, and replace its inner text with `netcoreapp2.0`:

```
<TargetFramework>netcoreapp2.0</TargetFramework>
```

Projects targeting .NET Framework should use the TFM of a version greater than or equal to .NET Framework 4.6.1. Search for the `<TargetFramework>` node in the `.csproj` file, and replace its inner text with `net461`:

```
<TargetFramework>net461</TargetFramework>
```

Note

.NET Core 2.0 offers a much larger surface area than .NET Core 1.x. If you're targeting .NET Framework solely because of missing APIs in .NET Core 1.x, targeting .NET Core 2.0 is likely to work.

If the project file contains `<RuntimeFrameworkVersion>1.{sub-version}</RuntimeFrameworkVersion>`, see [this GitHub issue](https://github.com/dotnet/AspNetCore/issues/3221#issuecomment-413094268).

[]()

## Update .NET Core SDK version in global.json

If your solution relies upon a [global.json](/en-us/dotnet/core/tools/global-json) file to target a specific .NET Core SDK version, update its `version` property to use the 2.0 version installed on your machine:

```
{
  "sdk": {
    "version": "2.0.0"
  }
}
```

[]()

## Update package references

The `.csproj` file in a 1.x project lists each NuGet package used by the project.

In an ASP.NET Core 2.0 project targeting .NET Core 2.0, a single [metapackage](../../fundamentals/metapackage?view=aspnetcore-10.0) reference in the `.csproj` file replaces the collection of packages:

```
<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.9" />
</ItemGroup>
```

All the features of ASP.NET Core 2.0 and Entity Framework Core 2.0 are included in the metapackage.

ASP.NET Core 2.0 projects targeting .NET Framework should continue to reference individual NuGet packages. Update the `Version` attribute of each `<PackageReference />` node to 2.0.0.

For example, here's the list of `<PackageReference />` nodes used in a typical ASP.NET Core 2.0 project targeting .NET Framework:

```
<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
  <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.0.0" />
  <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="2.0.0" />
  <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.0" />
  <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
  <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.0.0" PrivateAssets="All" />
  <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.0" PrivateAssets="All" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
  <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.0.0" />
  <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
</ItemGroup>
```

The package `Microsoft.Extensions.CommandLineUtils` has been [retired](https://github.com/dotnet/extensions/blob/release/2.1/README.md). It is still available but unsupported.

[]()

## Update .NET CLI tools

In the `.csproj` file, update the `Version` attribute of each `<DotNetCliToolReference />` node to 2.0.0.

For example, here's the list of CLI tools used in a typical ASP.NET Core 2.0 project targeting .NET Core 2.0:

```
<ItemGroup>
  <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
  <DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
  <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
```

[]()

## Rename Package Target Fallback property

The `.csproj` file of a 1.x project used a `PackageTargetFallback` node and variable:

```
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
```

Rename both the node and variable to `AssetTargetFallback`:

```
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
```

[]()

## Update Main method in Program.cs

In 1.x projects, the `Main` method of `Program.cs` looked like this:

```
using System.IO;
using Microsoft.AspNetCore.Hosting;

namespace AspNetCoreDotNetCore1App
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .UseApplicationInsights()
                .Build();

            host.Run();
        }
    }
}
```

In 2.0 projects, the `Main` method of `Program.cs` has been simplified:

```
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace AspNetCoreDotNetCore2App
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }
}
```

The adoption of this new 2.0 pattern is highly recommended and is required for product features like [Entity Framework (EF) Core Migrations](../../data/ef-mvc/migrations?view=aspnetcore-10.0) to work. For example, running `Update-Database` from the Package Manager Console window or `dotnet ef database update` from the command line (on projects converted to ASP.NET Core 2.0) generates the following error:

```
Unable to create an object of type '<Context>'. Add an implementation of 'IDesignTimeDbContextFactory<Context>' to the project, or see https://go.microsoft.com/fwlink/?linkid=851728 for additional patterns supported at design time.
```

[]()

## Add configuration providers

In 1.x projects, adding configuration providers to an app was accomplished via the `Startup` constructor. The steps involved creating an instance of `ConfigurationBuilder`, loading applicable providers (environment variables, app settings, etc.), and initializing a member of `IConfigurationRoot`.

```
public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

    if (env.IsDevelopment())
    {
        builder.AddUserSecrets<Startup>();
    }

    builder.AddEnvironmentVariables();
    Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; }
```

The preceding example loads the `Configuration` member with configuration settings from `appsettings.json` as well as any `appsettings.{Environment}.json` file matching the `IHostingEnvironment.EnvironmentName` property. The location of these files is at the same path as `Startup.cs`.

In 2.0 projects, the boilerplate configuration code inherent to 1.x projects runs behind-the-scenes. For example, environment variables and app settings are loaded at startup. The equivalent `Startup.cs` code is reduced to `IConfiguration` initialization with the injected instance:

```
public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public IConfiguration Configuration { get; }
```

To remove the default providers added by `WebHostBuilder.CreateDefaultBuilder`, invoke the `Clear` method on the `IConfigurationBuilder.Sources` property inside of `ConfigureAppConfiguration`. To add providers back, utilize the `ConfigureAppConfiguration` method in `Program.cs`:

```
public static void Main(string[] args)
{
    BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureAppConfiguration((hostContext, config) =>
        {
            // delete all default configuration providers
            config.Sources.Clear();
            config.AddJsonFile("myconfig.json", optional: true);
        })
        .Build();
```

The configuration used by the `CreateDefaultBuilder` method in the preceding code snippet can be seen [here](https://github.com/aspnet/MetaPackages/blob/rel/2.0.0/src/Microsoft.AspNetCore/WebHost.cs#L152).

For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

[]()

## Move database initialization code

In 1.x projects using EF Core 1.x, a command such as `dotnet ef migrations add` does the following:

1. Instantiates a `Startup` instance
2. Invokes the `ConfigureServices` method to register all services with dependency injection (including `DbContext` types)
3. Performs its requisite tasks

In 2.0 projects using EF Core 2.0, `Program.BuildWebHost` is invoked to obtain the application services. Unlike 1.x, this has the additional side effect of invoking `Startup.Configure`. If your 1.x app invoked database initialization code in its `Configure` method, unexpected problems can occur. For example, if the database doesn't yet exist, the seeding code runs before the EF Core Migrations command execution. This problem causes a `dotnet ef migrations list` command to fail if the database doesn't yet exist.

Consider the following 1.x seed initialization code in the `Configure` method of `Startup.cs`:

```
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

SeedData.Initialize(app.ApplicationServices);
```

In 2.0 projects, move the `SeedData.Initialize` call to the `Main` method of `Program.cs`:

```
var host = BuildWebHost(args);

using (var scope = host.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    try
    {
        // Requires using RazorPagesMovie.Models;
        SeedData.Initialize(services);
    }
    catch (Exception ex)
    {
        var logger = services.GetRequiredService<ILogger<Program>>();
        logger.LogError(ex, "An error occurred seeding the DB.");
    }
}

host.Run();
```

As of 2.0, it's bad practice to do anything in `BuildWebHost` except build and configure the web host. Anything that's about running the application should be handled outside of `BuildWebHost` — typically in the `Main` method of `Program.cs`.

[]()

## Review Razor view compilation setting

Faster application startup time and smaller published bundles are of utmost importance to you. For these reasons, [Razor view compilation](../../mvc/views/view-compilation?view=aspnetcore-10.0) is enabled by default in ASP.NET Core 2.0.

Setting the `MvcRazorCompileOnPublish` property to true is no longer required. Unless you're disabling view compilation, the property may be removed from the `.csproj` file.

When targeting .NET Framework, you still need to explicitly reference the [Microsoft.AspNetCore.Mvc.Razor.ViewCompilation](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Razor.ViewCompilation) NuGet package in your `.csproj` file:

```
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.0.0" PrivateAssets="All" />
```

[]()

## Rely on Application Insights "light-up" features

Effortless setup of application performance instrumentation is important. You can now rely on the new [Application Insights](/en-us/azure/application-insights/app-insights-overview) "light-up" features available in the Visual Studio 2017 tooling.

ASP.NET Core 1.1 projects created in Visual Studio 2017 added Application Insights by default. If you're not using the Application Insights SDK directly, outside of `Program.cs` and `Startup.cs`, follow these steps:

1. If targeting .NET Core, remove the following `<PackageReference />` node from the `.csproj` file:

   ```
   <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" />
   ```

2. If targeting .NET Core, remove the `UseApplicationInsights` extension method invocation from `Program.cs`:

   ```
   public static void Main(string[] args)
   {
       var host = new WebHostBuilder()
           .UseKestrel()
           .UseContentRoot(Directory.GetCurrentDirectory())
           .UseIISIntegration()
           .UseStartup<Startup>()
           .UseApplicationInsights()
           .Build();

       host.Run();
   }
   ```

3. Remove the Application Insights client-side API call from `_Layout.cshtml`. It comprises the following two lines of code:

   ```
   @inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet
   @Html.Raw(JavaScriptSnippet.FullScript)
   ```

If you are using the Application Insights SDK directly, continue to do so. The 2.0 [metapackage](../../fundamentals/metapackage?view=aspnetcore-10.0) includes the latest version of Application Insights, so a package downgrade error appears if you're referencing an older version.

[]()

## Adopt authentication/Identity improvements

ASP.NET Core 2.0 has a new authentication model and a number of significant changes to ASP.NET Core Identity. If you created your project with Individual User Accounts enabled, or if you have manually added authentication or Identity, see [Migrate Authentication and Identity to ASP.NET Core 2.0](identity-2x?view=aspnetcore-10.0).

## Related content

* [Breaking Changes in ASP.NET Core 2.0](https://github.com/aspnet/announcements/issues?page=1\&q=is%3Aissue+is%3Aopen+label%3A2.0.0+label%3A%22Breaking+change%22\&utf8=%E2%9C%93)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/diagnostic-tools
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/diagnostic-tools.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Performance Diagnostic Tools

By [Mike Rousos](https://github.com/mjrousos)

This article lists tools for diagnosing performance issues in ASP.NET Core.

For information on Blazor WebAssembly performance diagnostic tools and counters, see [ASP.NET Core Blazor WebAssembly browser developer tools diagnostics](../blazor/performance/webassembly-browser-developer-tools-diagnostics?view=aspnetcore-10.0) and [ASP.NET Core Blazor WebAssembly Event Pipe diagnostics](../blazor/performance/webassembly-event-pipe-diagnostics?view=aspnetcore-10.0).

## Visual Studio Diagnostic Tools

The [profiling and diagnostic tools](/en-us/visualstudio/profiling) built into Visual Studio are a good place to start investigating performance issues. These tools are powerful and convenient to use from the Visual Studio development environment. The tooling allows analysis of CPU usage, memory usage, and performance events in ASP.NET Core apps. Being built-in makes profiling easy at development time.

More information is available in [Visual Studio documentation](/en-us/visualstudio/profiling/profiling-overview).

## Application Insights

[Application Insights](/en-us/azure/application-insights/app-insights-overview) provides in-depth performance data for your app. Application Insights automatically collects data on response rates, failure rates, dependency response times, and more. Application Insights supports logging custom events and metrics specific to your app.

Azure Application Insights provides multiple ways to give insights on monitored apps:

* [Application Map](/en-us/azure/application-insights/app-insights-app-map) – helps spot performance bottlenecks or failure hot-spots across all components of distributed apps.

* [Azure Metrics Explorer](/en-us/azure/azure-monitor/platform/metrics-getting-started) is a component of the Microsoft Azure portal that allows plotting charts, visually correlating trends, and investigating spikes and dips in metrics' values.

* [Performance blade in Application Insights portal](/en-us/azure/application-insights/app-insights-tutorial-performance):

  * Shows performance details for different operations in the monitored app.
  * Allows drilling into a single operation to check all parts/dependencies that contribute to a long duration.
  * Profiler can be invoked from here to collect performance traces on-demand.

* [Azure Application Insights Profiler](/en-us/azure/azure-monitor/app/profiler) allows regular and on-demand profiling of .NET apps. Azure portal shows captured performance traces with call stacks and hot paths. The trace files can also be downloaded for deeper analysis using PerfView.

Application Insights can be used in a variety of environments:

* Optimized to work in Azure.
* Works in production, development, and staging.
* Works locally from [Visual Studio](/en-us/azure/application-insights/app-insights-visual-studio) or in other hosting environments.

For more information on code-based monitoring, see [Application Insights for ASP.NET Core](/en-us/azure/application-insights/app-insights-asp-net-core). For more information on codeless monitoring, see [Monitor Azure App Service performance](/en-us/azure/azure-monitor/app/azure-web-apps?tabs=netcore).

## PerfView

[PerfView](https://github.com/Microsoft/perfview) is a performance analysis tool created by the .NET team specifically for diagnosing .NET performance issues. PerfView allows analysis of CPU usage, memory and GC behavior, performance events, and wall clock time.

For more about PerfView, see the user's guide available in the tool or [on GitHub](https://github.com/Microsoft/perfview).

## Windows Performance Toolkit

[Windows Performance Toolkit](/en-us/windows-hardware/test/wpt/) (WPT) consists of two components: Windows Performance Recorder (WPR) and Windows Performance Analyzer (WPA). The tools produce in-depth performance profiles of Windows operating systems and apps. WPT has richer ways of visualizing data, but its data collecting is less powerful than PerfView's.

## PerfCollect

While PerfView is a useful performance analysis tool for .NET scenarios, it only runs on Windows, so you can't use it to collect traces from ASP.NET Core apps running in Linux environments.

[PerfCollect](https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md) is a bash script that uses native Linux profiling tools ([Perf](https://perf.wiki.kernel.org/index.php/Main_Page) and [LTTng](https://lttng.org/)) to collect traces on Linux that can be analyzed by PerfView. PerfCollect is useful when performance problems show up in Linux environments where PerfView can't be used directly. Instead, PerfCollect can collect traces from .NET apps that are then analyzed on a Windows computer using PerfView.

More information about how to install and get started with PerfCollect is available [on GitHub](https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md).

## Other Third-party Performance Tools

The following lists some third-party performance tools that are useful in performance investigation of .NET applications.

* [MiniProfiler](https://miniprofiler.com/)
* [dotTrace](https://www.jetbrains.com/profiler/) and [dotMemory](https://www.jetbrains.com/dotmemory/) from [JetBrains](https://www.jetbrains.com/)
* [VTune](https://software.intel.com/content/www/us/en/develop/tools/vtune-profiler.html) from Intel

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-30

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/startup
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/startup.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# App startup in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes how ASP.NET Core apps start up and how to configure services and the app's request pipeline.

For Blazor startup guidance, which adds to or supersedes the guidance in this article, see [ASP.NET Core Blazor startup](../blazor/fundamentals/startup?view=aspnetcore-10.0).

## The `Program` file

ASP.NET Core apps initialize and configure startup in the app's `Program` file (`Program.cs`).

The first part of the `Program` file focuses on building the app. This phase utilizes [WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) to initialize a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults before the app is started. The ASP.NET Core project templates assign the web application builder to a variable named `builder`:

```
var builder = WebApplication.CreateBuilder(args);
```

Properties of the web application builder include:

* [`builder.Configuration`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.configuration#microsoft-aspnetcore-builder-webapplicationbuilder-configuration) is a [IConfigurationBuilder](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationbuilder) and [IConfigurationRoot](/en-us/dotnet/api/microsoft.extensions.configuration.iconfigurationroot) for managing configuration sources and providers. For more information, see [Configuration in ASP.NET Core](configuration/?view=aspnetcore-10.0).
* [`builder.Environment`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.environment#microsoft-aspnetcore-builder-webapplicationbuilder-environment) is an [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) that provides information about the app's web hosting environment. For more information, see [ASP.NET Core runtime environments](environments?view=aspnetcore-10.0).
* [`builder.Host`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.host#microsoft-aspnetcore-builder-webapplicationbuilder-host) is an [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) for configuring host-specific properties. For more information, see [.NET Generic Host in ASP.NET Core](host/generic-host?view=aspnetcore-10.0).
* [`builder.Logging`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.logging#microsoft-aspnetcore-builder-webapplicationbuilder-logging) is an [ILoggingBuilder](/en-us/dotnet/api/microsoft.extensions.logging.iloggingbuilder) with extension methods that add and manage logging providers. For more information, see [Logging in .NET and ASP.NET Core](logging/?view=aspnetcore-10.0).
* [`builder.Metrics`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.metrics#microsoft-aspnetcore-builder-webapplicationbuilder-metrics) (.NET 8 or later) allows enabling metrics and directing their output. For more information, see [ASP.NET Core metrics](../metrics/overview?view=aspnetcore-10.0).
* [`builder.Services`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.services#microsoft-aspnetcore-builder-webapplicationbuilder-services) is a collection of dependency injection (DI) services ([IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection)) for the app to compose for [Inversion of Control (IoC)](/en-us/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#dependency-inversion). For more information, see [Dependency injection in ASP.NET Core](dependency-injection?view=aspnetcore-10.0).
* [`builder.WebHost`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost#microsoft-aspnetcore-builder-webapplicationbuilder-webhost) is an [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) for configuring server specific properties.

The app is built by calling [WebApplicationBuilder.Build](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.build), which returns the built [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication). The ASP.NET Core project templates assign the built web application to a variable named `app`:

```
var app = builder.Build();
```

The next part of the `Program` file focuses on establishing the HTTP request handling pipeline as a series of [middleware components](middleware/?view=aspnetcore-10.0). Each middleware performs operations on an [`HttpContext`](http-context?view=aspnetcore-10.0) and either invokes the next middleware in the pipeline or terminates the request. By convention, middleware components are added to the pipeline by invoking an extension method that starts with "`Use`." For more information, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0).

The [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.run) method starts the app and blocks the calling thread until the host is shut down:

```
app.Run();
```

When [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.run) executes, the app transitions to an active, running process:

1. Hosted services start.

   The host loops through all registered [hosted services](host/hosted-services?view=aspnetcore-10.0) ([IHostedService](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) instances) and calls their [StartAsync](/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice.startasync) methods. Unless the app opts into concurrent hosted service startup (.NET 8 or later), hosted services start sequentially in the order of their DI container registrations. For more information, see [Background tasks with hosted services in ASP.NET Core](host/hosted-services?view=aspnetcore-10.0).

2. The middleware pipeline is built.

   When [`builder.Build`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.build) is called, dependencies are resolved, but the actual processing pipeline isn't completely set. When [`app.Run`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.run) executes, the framework finalizes the HTTP middleware pipeline. The declared middleware methods and endpoint mappings are compiled into a single, high-performance execution delegate sequence. For more information, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0).

3. The web server ([Kestrel](servers/kestrel?view=aspnetcore-10.0) by default) is started.

   The host looks inside its dependency container, locates the registered server implementation (usually Kestrel), and triggers its startup cycle. Kestrel then:

   * Looks up the defined hosting URLs and ports from configuration, environment variables, or command-line arguments.
   * Opens and allocates physical network sockets.
   * Binds ports and begins listening for incoming traffic.

   For more information, see [.NET Generic Host in ASP.NET Core](host/generic-host?view=aspnetcore-10.0), [Web server implementations in ASP.NET Core](servers/?view=aspnetcore-10.0), and [Kestrel web server in ASP.NET Core](servers/kestrel?view=aspnetcore-10.0).

4. Application started lifetime events are triggered.

   The [IHostApplicationLifetime](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime) service fires its [ApplicationStarted](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime.applicationstarted#microsoft-extensions-hosting-ihostapplicationlifetime-applicationstarted) token, which invokes callbacks registered on the token. Any callbacks, database seeders, or other custom event listeners that are wired up to wait for the token to fire are triggered to start processing.

5. The main execution thread is blocked while the app runs.

   [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.run) (`app.Run()`) synchronously waits until shutdown.

6. The app is ready to process requests.

   At this point, the command shell logs hosting diagnostics:

   ```
   info: Microsoft.Hosting.Lifetime[14]
         Now listening on: https://localhost:7123
   info: Microsoft.Hosting.Lifetime[14]
         Now listening on: http://localhost:5123
   info: Microsoft.Hosting.Lifetime[0]
         Application started. Press Ctrl+C to shut down.
   ```

The app remains in this state indefinitely, passing incoming web traffic down the middleware pipeline and sending responses.

When shutdown is signaled, for example when `Ctrl`+`c` is detected in the command shell running the app or a container orchestration tool sends a SIGTERM event, [Run](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.run) unblocks and the following actions take place:

1. [ApplicationStopping](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime.applicationstopping#microsoft-extensions-hosting-ihostapplicationlifetime-applicationstopping) tokens are triggered, which allows the app to run logic before the shutdown process begins.

2. The Kestrel server is shut down, which disables new connections. The server waits for requests on existing connections to complete for as long as the shutdown timeout allows. The server sends the connection close header for further requests on existing connections.

3. The host shuts down registered hosted services. Unless the app opts into stopping hosted services concurrently (.NET 8 or later), hosted services stop sequentially in the reverse order of their DI container registrations. For more information, see [Background tasks with hosted services in ASP.NET Core](host/hosted-services?view=aspnetcore-10.0).

4. [ApplicationStopped](/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime.applicationstopped) event handlers are triggered, which allows the app to run logic after the app has shut down.

5. Console execution gracefully exits with an exit code of 0.

The `Startup` class configures services and the app's request pipeline.

## The `Startup` class

ASP.NET Core apps use a startup class, which is named `Startup` by convention. The `Startup` class:

* Optionally includes a [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configureservices) method to configure the app's *services*. A service is a reusable component that provides app functionality. Services are *registered* in `ConfigureServices` and consumed across the app via [dependency injection (DI)](dependency-injection?view=aspnetcore-10.0) or [ApplicationServices](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder.applicationservices).
* Includes a [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configure) method to create the app's request processing pipeline.

`ConfigureServices` and `Configure` are called by the ASP.NET Core runtime when the app starts:

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        ...
    }
}
```

The `Startup` class is specified when the app's [host](./?view=aspnetcore-10.0#host) is built. The `Startup` class is typically specified by calling [WebHostBuilderExtensions.UseStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestartup) on the host builder:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

The host provides services that are available to the `Startup` class constructor. The app adds additional services via `ConfigureServices`. Both the host and app services are available in `Configure` and throughout the app.

Only the following service types can be injected into the `Startup` constructor when using the [Generic Host](host/generic-host?view=aspnetcore-10.0) ([IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder)):

* [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment)
* [IHostEnvironment](/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment)
* [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration)

```
public class Startup
{
    private readonly IWebHostEnvironment _env;

    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Configuration = configuration;
        _env = env;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        if (_env.IsDevelopment())
        {
        }
        else
        {
        }
    }
}
```

Most services aren't available until the `Configure` method is called.

Note

The private field in the preceding example for [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) is named with an underscore (`_env`). It's also acceptable to adopt a coding convention that uses the same name as the injected [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) (`env`) when the private field uses the [`this` keyword](/en-us/dotnet/csharp/language-reference/keywords/this) (`this.env = env` in the constructor and `env.IsDevelopment()` in the `ConfigureServices` method).

The ASP.NET Core project templates prior to .NET 8 and C# 12 don't adopt *primary constructors*, but the preceding code can be refactored to adopt a primary constructor if your organization uses a .NET 8 or later SDK and the app targets C# 12 or later (for example, `<LangVersion>12.0</LangVersion>`). For more information, see [Declare primary constructors for classes and structs (C# documentation tutorial)](/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) and [Primary constructors (C# Guide)](/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors).

## Multiple `Startup` classes

When the app defines separate `Startup` classes for different environments (for example, `StartupDevelopment`), the appropriate `Startup` class is selected at runtime. The class whose name suffix matches the current environment is prioritized. If the app is run in the `Development` environment and includes both a `Startup` class and a `StartupDevelopment` class, the `StartupDevelopment` class is used. For more information, see [Use multiple environments](environments?view=aspnetcore-10.0#environment-based-startup-class-and-methods).

## The `ConfigureServices` method

The optional [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configureservices) method is:

* Called by the host before the `Configure` method to configure the app's services.
* Where [configuration options](configuration/?view=aspnetcore-10.0) are set by convention.

The host may configure some services before `Startup` methods are called. For more information, see [ASP.NET Core fundamentals overview](./?view=aspnetcore-10.0#host).

For features that require substantial setup, there are `Add{Service}` extension methods on [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection), such as:

* **Add**DbContext
* **Add**DefaultIdentity
* **Add**EntityFrameworkStores
* **Add**RazorPages

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
        services.AddDefaultIdentity<IdentityUser>(
            options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();

        services.AddRazorPages();
    }
}
```

Adding services to the service container makes them available within the app and in the `Configure` method. The services are resolved via [dependency injection](dependency-injection?view=aspnetcore-10.0) or from [ApplicationServices](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder.applicationservices).

## The `Configure` method

The [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configure) method is used to specify how the app responds to HTTP requests. The request pipeline is configured by adding [middleware](middleware/?view=aspnetcore-10.0) components to an [IApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder) instance. `IApplicationBuilder` is available to the `Configure` method, but it isn't registered in the service container. Hosting creates an `IApplicationBuilder` and passes it directly to `Configure`.

The [ASP.NET Core templates](/en-us/dotnet/core/tools/dotnet-new) configure the pipeline with support for:

* [Developer Exception Page](error-handling?view=aspnetcore-10.0#developer-exception-page)
* [Exception handler](error-handling?view=aspnetcore-10.0#exception-handler-page)
* [HTTP Strict Transport Security (HSTS)](../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol)
* [HTTPS redirection](../security/enforcing-ssl?view=aspnetcore-10.0)
* [Static files](static-files?view=aspnetcore-10.0)
* ASP.NET Core [MVC](../mvc/overview?view=aspnetcore-10.0) and [Razor Pages](../razor-pages/?view=aspnetcore-10.0)

The following example demonstrates middleware for a typical Razor Pages app:

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

The preceding sample is for [Razor Pages](../razor-pages/?view=aspnetcore-10.0); the MVC version is similar.

Each `Use` extension method adds one or more middleware components to the request pipeline. For instance, [UseStaticFiles](/en-us/dotnet/api/microsoft.aspnetcore.builder.staticfileextensions.usestaticfiles) configures [middleware](middleware/?view=aspnetcore-10.0) to serve [static files](static-files?view=aspnetcore-10.0).

Each middleware component in the request pipeline is responsible for invoking the next component in the pipeline or short-circuiting the chain, if appropriate.

Additional services, such as `IWebHostEnvironment`, `ILoggerFactory`, or anything defined in `ConfigureServices`, can be specified in the `Configure` method signature. These services are injected if they're available.

For more information on how to use `IApplicationBuilder` and the order of middleware processing, see [ASP.NET Core middleware](middleware/?view=aspnetcore-10.0).

## Configure services without a `Startup` class

To configure services and the request processing pipeline without using a `Startup` class, call `ConfigureServices` and `Configure` convenience methods on the host builder. Multiple calls to `ConfigureServices` append to one another. If multiple `Configure` method calls exist, the last `Configure` call is used.

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.ConfigureServices(services =>
                {
                    ...
                })
                .Configure(app =>
                {
                    ...
                });
        });
}
```

## Startup filters

While an app typically creates an explicit middleware execution pipeline, a startup filter ([IStartupFilter](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter)) is useful for:

* Creating a shared library/NuGet package that automatically loads custom middleware without requiring the app to explicitly call the middleware's "`Use`" method. For example, the library's consumer isn't required to make an `app.UseImageProcessingMiddleware` call for an image-processing middleware in the app's request processing pipeline.
* Guaranteeing a piece of middleware executes before or after other middleware, regardless of how a developer modifies the app's request processing pipeline.

A startup filter implementation provides an [IStartupFilter.Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter.configure) method that receives and returns an `Action<IApplicationBuilder>`. The [IApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder) interface is used to configure the app's request pipeline. For more information, see [Create a middleware pipeline with `IApplicationBuilder`](middleware/?view=aspnetcore-10.0#create-a-middleware-pipeline-with-iapplicationbuilder).

Each startup filter implementation can add one or more middlewares to the request pipeline. The filters are invoked in the order they're added to the service container. Filters can add middleware before or after passing control to the next filter, thus they append to the beginning or end of the pipeline.

The following example demonstrates how to register a middleware with [IStartupFilter](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter). The `CustomResponseHeaderFilter` startup filter uses middleware to append a custom header (`X-Custom-Header`) to all of the app's responses before other middlewares execute.

`CustomResponseHeaderFilter.cs`:

```
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

public class CustomResponseHeaderFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            // 1. Add middleware that runs BEFORE subsequent middlewares
            builder.Use(async (context, nextMiddleware) =>
            {
                context.Response.Headers.Append("X-Custom-Header", "VALUE");
                await nextMiddleware();
            });

            // 2. Call the rest of the application's configuration pipeline
            next(builder);

            // 3. (Optional) Add middleware that runs AFTER the rest of the pipeline
        };
    }
}
```

The startup filter implementation is registered in the `Program` file:

```
builder.Services.AddTransient<IStartupFilter, CustomResponseHeaderFilter>();
```

The startup filter implementation is registered in `Startup.ConfigureServices`:

```
services.AddTransient<IStartupFilter, CustomResponseHeaderFilter>();
```

Middleware execution order is set by the order of startup filter registrations:

* Multiple implementations might interact with the same objects. If ordering is important, order their service registrations to match the order that their middlewares should run.

* Libraries can add middleware with one or more implementations that run before or after other app middleware registered with [IStartupFilter](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter). To invoke a startup filter middleware before a middleware added by a library's startup filter:

  * Position the startup filter service registration before the library is added to the service container.
  * To invoke afterward, position the service registration after the library is added.

Note

You can't extend the ASP.NET Core app with startup filters when you override the `Configure` delegate. For more information, see [WebApplicationFactory Client returns NotFound for all requests with Overriding Configure method (`dotnet/aspnetcore` #45372)](https://github.com/dotnet/aspnetcore/issues/45372).

## Add configuration at startup from an external assembly

An [IHostingStartup](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingstartup) implementation allows adding enhancements to an app at startup from an external assembly outside of the app's `Program` file or `Startup` class. For more information, see [Use hosting startup assemblies in ASP.NET Core](host/platform-specific-configuration?view=aspnetcore-10.0).

## The `Startup` class (`ConfigureServices` and `Configure` methods)

*Although supported in ASP.NET Core apps that target .NET 6 or later, using a `Startup` class isn't recommended. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../migration/50-to-60?view=aspnetcore-10.0#new-hosting-model).*

For information on using the [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configureservices) and [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configure) methods with the minimal hosting model, see the following:

* [Use a `Startup` class with the minimal hosting model](../migration/50-to-60?view=aspnetcore-10.0#use-a-startup-class-with-the-new-minimal-hosting-model)
* [The `Startup` class (.NET 5 version of this article)](?view=aspnetcore-5.0\&preserve-view=true#the-startup-class)

## Measure startup performance

The ASP.NET Core hosting [EventSource](/en-us/dotnet/api/system.diagnostics.tracing.eventsource) emits the `ServerReady` event, which represents the point where the server is ready to respond to requests and can be used to measure startup time. For more information, see [Logging in .NET and ASP.NET Core](logging/?view=aspnetcore-10.0#eventsource).

## Related content

* [Host guidance](./?view=aspnetcore-10.0#host)
* [Startup exception handling](error-handling?view=aspnetcore-10.0#startup-exception-handling)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/test/hot-reload
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/test/hot-reload.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# .NET Hot Reload support for ASP.NET Core

.NET Hot Reload applies code changes, including changes to stylesheets, to a running app without restarting the app and without losing app state. Hot Reload is supported for all ASP.NET Core in .NET 6 or later projects.

Generally, updated code is rerun to take effect with the following conditions:

* Some startup logic is only run once:

  * Middleware, unless the code update is to an inline middleware delegate.
  * Configured services.
  * Route creation and configuration, unless the code update is to a route handler delegate (for example, `OnInitialized`).

* In [Blazor apps](../blazor/?view=aspnetcore-10.0), the framework triggers a [Razor component](../blazor/components/?view=aspnetcore-10.0) render automatically.

* In MVC and Razor Pages apps, Hot Reload triggers a browser refresh automatically.

* Removing a Razor [component parameter](../blazor/components/?view=aspnetcore-10.0#component-parameters) attribute doesn't cause the component to rerender. The app must be restarted.

For more information on supported scenarios, see [Supported code changes (C# and Visual Basic)](/en-us/visualstudio/debugger/supported-code-changes-csharp).

## Blazor WebAssembly

Blazor WebAssembly Hot Reload supports the following code changes:

* New types.
* Nested classes.
* Most changes to method bodies, such as adding, removing, and editing variables, expressions, and statements.
* Changes to the bodies of [lambda expressions](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions) and [local functions](/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions).
* Adding static and instance methods to existing types.
* Adding static and instance fields, events, and properties to existing types.
* Adding static lambdas to existing methods.
* Adding lambdas that capture `this` to existing methods that already captured `this` previously.

Note that when an attribute is removed that previously set the value of a component parameter, the component is disposed and re-initialized to set the removed parameter back to its default value.

The following code changes aren't supported for Blazor WebAssembly apps:

* Adding a new [`await` operator](/en-us/dotnet/csharp/language-reference/operators/await) or [`yield` keyword](/en-us/dotnet/csharp/language-reference/keywords/yield) expression.
* Changing the names of method parameters.

Blazor WebAssembly Hot Reload supports the following code changes:

* New types.
* Nested classes.
* Most changes to method bodies, such as adding, removing, and editing variables, expressions, and statements.
* Changes to the bodies of [lambda expressions](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions) and [local functions](/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions).
* Adding static and instance methods to existing types.
* Adding static fields to existing types.
* Adding static lambdas to existing methods.
* Adding lambdas that capture `this` to existing methods that already captured `this` previously.

Note that when an attribute is removed that previously set the value of a component parameter, the component is disposed and re-initialized to set the removed parameter back to its default value.

The following code changes aren't supported for Blazor WebAssembly apps:

* Adding a new [`await` operator](/en-us/dotnet/csharp/language-reference/operators/await) or [`yield` keyword](/en-us/dotnet/csharp/language-reference/keywords/yield) expression.
* Changing the names of method parameters.
* Adding instance (non-`static`) fields, events, or properties.

Blazor WebAssembly Hot Reload supports the following code changes:

* Most changes to method bodies, such as adding, removing, and editing variables, expressions, and statements.
* Changes to the bodies of [lambda expressions](/en-us/dotnet/csharp/language-reference/operators/lambda-expressions) and [local functions](/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions).

The following code changes aren't supported for Blazor WebAssembly apps:

* Adding new lambdas or local functions.
* Adding a new [`await` operator](/en-us/dotnet/csharp/language-reference/operators/await) or [`yield` keyword](/en-us/dotnet/csharp/language-reference/keywords/yield) expression.
* Changing the names of method parameters.
* Changes outside of method bodies.
* Adding instance (non-`static`) fields, events, or properties.

## .NET CLI

Hot Reload is activated using the [`dotnet watch`](../tutorials/dotnet-watch?view=aspnetcore-10.0) command:

```
dotnet watch
```

To force the app to rebuild and restart, use the keyboard combination `Ctrl`+`R` in the command shell.

When an unsupported code edit is made, called a *rude edit*, `dotnet watch` asks you if you want to restart the app:

* **Yes**: Restarts the app.
* **No**: Doesn't restart the app and leaves the app running without the changes applied.
* **Always**: Restarts the app as needed when rude edits occur.
* **Never**: Doesn't restart the app and avoids future prompts.

To disable support for Hot Reload, pass the `--no-hot-reload` option to the `dotnet watch` command:

```
dotnet watch --no-hot-reload
```

## Disable Hot Reload

The following setting in `Properties/launchSettings.json` disables Hot Reload:

```
"hotReloadEnabled" : false
```

## WebAssembly Hot Reload (`WasmEnableHotReload`)

In .NET 10 and later, the `WasmEnableHotReload` MSBuild property controls whether Hot Reload infrastructure is included in the build output for .NET WebAssembly apps, including both Blazor WebAssembly apps and non-Blazor WebAssembly Browser App (`wasmbrowser` template) projects. The property is set to `true` by default for the `Debug` configuration.

Setting the property to `false` removes Hot Reload dependencies from the `dotnet build` output, which is useful when producing bundler-compatible artifacts, for example, for use with webpack or Vite.

To disable Hot Reload for WebAssembly apps, set the property to `false` in the app's project file (`.csproj`):

```
<PropertyGroup>
  <WasmEnableHotReload>false</WasmEnableHotReload>
</PropertyGroup>
```

To enable Hot Reload for a custom (non-`Debug`) configuration, set the property to `true`:

```
<PropertyGroup>
  <WasmEnableHotReload>true</WasmEnableHotReload>
</PropertyGroup>
```

For more information on controlling Hot Reload in Blazor WebAssembly apps, see [Debug ASP.NET Core Blazor apps](../blazor/debug?view=aspnetcore-10.0#control-hot-reload).

## Related content

For more information, see the following resources in the Visual Studio documentation:

* YouTube video [.NET 6 Hot Reload in Visual Studio 2022, VS Code, and NOTEPAD?!?](https://www.youtube.com/watch?v=4S3vPzawnoQ)
* [Introducing the .NET Hot Reload experience for editing code at runtime](https://devblogs.microsoft.com/dotnet/introducing-net-hot-reload/)
* [Write and debug running code with Hot Reload in Visual Studio](/en-us/visualstudio/debugger/hot-reload)
* [Updates for Blazor & Razor editors + Hot Reload for ASP.NET](/en-us/visualstudio/ide/whats-new-visual-studio-2022#updates-for-blazor--razor-editors--hot-reload-for-aspnet)
* [Test Execution with Hot Reload](/en-us/visualstudio/test/test-execution-with-hot-reload)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-18

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/caching/output.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Output caching middleware in ASP.NET Core

By [Tom Dykstra](https://github.com/tdykstra)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure output caching middleware in an ASP.NET Core app. For an introduction to output caching, see [Output caching](overview?view=aspnetcore-10.0#output-caching).

The output caching middleware can be used in all types of ASP.NET Core apps: [Minimal APIs](../../fundamentals/minimal-apis?view=aspnetcore-10.0), [Web API with controllers](../../web-api/?view=aspnetcore-10.0), [MVC](../../mvc/overview?view=aspnetcore-10.0), and [Razor Pages](../../razor-pages/?view=aspnetcore-10.0). Code examples are provided for Minimal APIs and controller-based APIs. The controller-based API examples show how to use attributes to configure caching. These attributes can also be used in MVC and Razor Pages apps.

The code examples refer to a [Gravatar class](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/caching/output/samples/7.x/Gravatar.cs) that generates an image and provides a "generated at" date and time. The class is defined and used only in [the sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/performance/caching/output/samples/7.x). Its purpose is to make it easy to see when cached output is being used. For more information, see [How to download a sample](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample) and [Preprocessor directives](/en-us/dotnet/csharp/fundamentals/program-structure/preprocessor-directives).

## Add the middleware to the app

Add the output caching middleware to the service collection by calling the [AddOutputCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.outputcacheservicecollectionextensions.addoutputcache) method. For example:

```
builder.Services.AddOutputCache();
```

Add the middleware to the request processing pipeline by calling the [UseOutputCache](/en-us/dotnet/api/microsoft.aspnetcore.builder.outputcacheapplicationbuilderextensions.useoutputcache) method. For example:

```
var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseOutputCache();
```

Calling the `AddOutputCache`and `UseOutputCache` methods doesn't start caching behavior, it makes caching available. To make the app cache responses, caching must be configured as described in the following sections.

Note

* In apps that use [Cross-Origin Requests (CORS) middleware](../../security/cors?view=aspnetcore-10.0), the `UseOutputCache` method must be called after the [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) method.
* In Razor Pages apps and apps with controllers, the `UseOutputCache` method must be called after the `UseRouting` method.
* In apps that use [authentication](../../security/authentication/?view=aspnetcore-10.0) or [authorization](../../security/authorization/introduction?view=aspnetcore-10.0), the `UseOutputCache` method must be called after the [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) methods. Otherwise, the output caching middleware can serve content cached for unauthorized users instead of content for authorized users.

## Configure one endpoint or page

For Minimal API apps, configure an endpoint to do caching by calling the [CacheOutput](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.outputcacheconventionbuilderextensions.cacheoutput) method, or by applying the \[[OutputCache](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheattribute)] attribute, as shown in the following examples:

```
app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));
```

For apps with controllers, apply the `[OutputCache]` attribute to the action method as shown in the following code:

```
[ApiController]
[Route("/[controller]")]
[OutputCache]
public class CachedController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}
```

For Razor Pages apps, apply the attribute to the Razor page class.

## Configure multiple endpoints or pages

Create *policies* when calling the `AddOutputCache` method to specify caching configuration that applies to multiple endpoints. A policy can be selected for specific endpoints, while a base policy provides default caching configuration for a collection of endpoints.

The following highlighted code configures caching for all of the app's endpoints, with an expiration time of 10 seconds. If an expiration time isn't specified, it defaults to one minute.

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});
```

The following highlighted code creates two policies, each specifying a different expiration time. Selected endpoints can use the 20-second expiration, and others can use the 30-second expiration.

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});
```

You can select a policy for an endpoint when calling the `CacheOutput` method or by using the `[OutputCache]` attribute.

In a Minimal API app, the following code configures one endpoint with a 20-second expiration and one with a 30-second expiration:

```
app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));
```

For apps with controllers, apply the `[OutputCache]` attribute to the action method to select a policy:

```
[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Expire20")]
public class Expire20Controller : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}
```

For Razor Pages apps, apply the attribute to the Razor page class.

## Work with the default output caching policy

By default, output caching follows these rules:

* Only HTTP 200 responses are cached.
* Only HTTP GET or HEAD requests are cached.
* Responses that set cookies aren't cached.
* Responses to authenticated requests aren't cached.

The following code applies all of the default caching rules to all of an app's endpoints:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});
```

### Override the default policy

The following code shows how to override the default policy rules. The highlighted lines in the following custom policy code enable caching for HTTP POST methods and HTTP 301 responses:

```
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeResponseAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        var response = context.HttpContext.Response;

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}
```

To use this custom policy, create a named policy:

```
builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});
```

And, select the named policy for an endpoint. The following code selects the custom policy for an endpoint in a Minimal API app:

```
app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");
```

The following code does the same for a controller action:

```
[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "CachePost")]
public class PostController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}
```

### Use an alternative default policy override

Alternatively, use Dependency Injection (DI) to initialize an instance with the following changes to the custom policy class:

* Use a public constructor instead of a private constructor.
* Eliminate the `Instance` property in the custom policy class.

For example:

```
public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }
```

The remainder of the class is the same as shown previously. Add the custom policy as shown in the following example:

```
builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});
```

The preceding code uses DI to create the instance of the custom policy class. Any public arguments in the constructor are resolved.

When using a custom policy as a base policy, don't call the `OutputCache()` method (with no arguments) or use the `[OutputCache]` attribute on any endpoint that the base policy should apply to. Calling the `OutputCache()` method or using the attribute adds the default policy to the endpoint.

## Specify the cache key

By default, every part of the URL is included as the key to a cache entry, that is, the scheme, host, port, path, and query string. However, you might want to explicitly control the cache key. For example, suppose you have an endpoint that returns a unique response only for each unique value of the `culture` query string. Variation in other parts of the URL, such as other query strings, shouldn't result in different cache entries. You can specify such rules in a policy, as shown in the following highlighted code:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
```

You can then select the `VaryByQuery` policy for an endpoint. In a Minimal API app, the following code selects the `VaryByQuery` policy for an endpoint that returns a unique response only for each unique value of the `culture` query string:

```
app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");
```

The following code does the same for a controller action:

```
[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Query")]
public class QueryController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}
```

Here are some of the options for controlling the cache key:

* The [SetVaryByQuery](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder.setvarybyquery) method specifies one or more query string names to add to the cache key.

* The [SetVaryByHeader](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder.setvarybyheader) method specifies one or more HTTP headers to add to the cache key.

* The [VaryByValue](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder.varybyvalue) method provides a value to add to the cache key. The following example uses a value that indicates whether the current server time in seconds is odd or even. A new response is generated only when the number of seconds changes from an odd to even value, or vice versa.

  ```
  builder.Services.AddOutputCache(options =>
  {
      options.AddBasePolicy(builder => builder
          .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
          .Tag("tag-blog"));
      options.AddBasePolicy(builder => builder.Tag("tag-all"));
      options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
      options.AddPolicy("NoCache", builder => builder.NoCache());
      options.AddPolicy("NoLock", builder => builder.SetLocking(false));
      options.AddPolicy("VaryByValue", builder => 
          builder.VaryByValue((context) =>
              new KeyValuePair<string, string>(
              "time", (DateTime.Now.Second % 2)
                  .ToString(CultureInfo.InvariantCulture))));
  });
  ```

Use the [OutputCacheOptions.UseCaseSensitivePaths](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions.usecasesensitivepaths#microsoft-aspnetcore-outputcaching-outputcacheoptions-usecasesensitivepaths) property to specify that the path part of the key is case sensitive. The default is case insensitive.

For more options, see the [OutputCachePolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder) class.

## Enable cache revalidation

Cache revalidation means the server can return a *304 Not Modified* HTTP status code instead of the full response body. This status code informs the client that the response to the request is unchanged from what the client previously received.

The following code illustrates the use of an [ETag](https://developer.mozilla.org/docs/Web/HTTP/Headers/ETag) header to enable cache revalidation. If the client sends an [If-None-Match](https://developer.mozilla.org/docs/Web/HTTP/Headers/If-None-Match) header with the value for the `ETag` from an earlier response, and the cache entry is fresh, the server returns the [304 Not Modified](https://developer.mozilla.org/docs/Web/HTTP/Status/304) code instead of the full response.

The following code sets the `ETag` value in a policy in a Minimal API app:

```
app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();
```

The following code does the same for a controller-based API:

```
[ApiController]
[Route("/[controller]")]
[OutputCache]
public class EtagController : ControllerBase
{
    public async Task GetAsync()
    {
        var etag = $"\"{Guid.NewGuid():n}\"";
        HttpContext.Response.Headers.ETag = etag;
        await Gravatar.WriteGravatar(HttpContext);
    }
}
```

Another way to do cache revalidation is to check the date of the cache entry creation compared to the date requested by the client. When the request header `If-Modified-Since` is provided, output caching returns the 304 code if the cached entry is older and isn't expired.

Cache revalidation is automatic in response to these headers sent from the client. No special configuration is required on the server to enable this behavior, aside from enabling output caching.

## Use tags to evict cache entries

You can use tags to identify a group of endpoints and evict all cache entries for the group. For example, the following Minimal API code creates a pair of endpoints whose URLs begin with the text `blog` and applies the `tag-blog` tag:

```
app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
```

The following code shows how to assign tags to an endpoint in a controller-based API:

```
[ApiController]
[Route("/[controller]")]
[OutputCache(Tags = new[] { "tag-blog", "tag-all" })]
public class TagEndpointController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}
```

An alternative way to assign tags for endpoints with routes that begin with `blog` is to define a base policy that applies to all endpoints with that route. The following code demonstrates this approach:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
```

Another alternative for Minimal API apps is to call the [MapGroup](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutebuilderextensions.mapgroup) method:

```
var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);
```

In the preceding tag assignment examples, both endpoints are identified by the `tag-blog` tag. You can then evict the cache entries for those endpoints with a single statement that references that tag:

```
app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});
```

With this code, an HTTP POST request sent to the `https://localhost:<port>/purge/tag-blog` URL evicts cache entries for these endpoints.

You might want a way to evict all cache entries for all endpoints. You can create a base policy for all endpoints as demonstrated in the following code:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
```

This base policy enables you to use the `tag-all` tag to evict everything in cache.

## Disable resource locking

By default, resource locking is enabled to mitigate the risk of [cache stampede and thundering herd](https://wikipedia.org/wiki/Thundering_herd_problem). For more information, see [Output Caching](overview?view=aspnetcore-10.0#output-caching).

To disable resource locking, call the [SetLocking(false)](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder.setlocking) method while creating a policy, as shown in the following example:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
```

The next example selects the no-locking policy for an endpoint in a Minimal API app:

```
app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");
```

In a controller-based API, use the attribute to select the policy:

```
[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "NoLock")]
public class NoLockController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}
```

## Configure limits

The following properties of the [OutputCacheOptions](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions) class let you configure limits that apply to all endpoints:

* The [SizeLimit](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions.sizelimit#microsoft-aspnetcore-outputcaching-outputcacheoptions-sizelimit) property sets the maximum size for the cache storage. When the limit is reached, no new responses are cached until older entries are evicted. The default value is 100 MB.
* The [MaximumBodySize](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions.maximumbodysize#microsoft-aspnetcore-outputcaching-outputcacheoptions-maximumbodysize) property sets the maximum size for the response body. If the response body exceeds the limit, it isn't cached. The default value is 64 MB.
* The [DefaultExpirationTimeSpan](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions.defaultexpirationtimespan#microsoft-aspnetcore-outputcaching-outputcacheoptions-defaultexpirationtimespan) property sets the maximum duration a response is cached, when a time isn't specified in a policy. The default value is 60 seconds.

## Explore cache storage options

The [IOutputCacheStore](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.ioutputcachestore) interface is used for storage. By default, it's used with the [MemoryCache](/en-us/dotnet/api/system.runtime.caching.memorycache) class. Cached responses are stored in-process, so each server has a separate cache that is lost whenever the server process restarts.

### Alternative: Redis cache

An alternative is to use [Redis](https://redis.io/) cache. Redis cache provides consistency between server nodes via a shared cache that outlives individual server processes. To use Redis for output caching:

* Install the [Microsoft.AspNetCore.OutputCaching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.AspNetCore.OutputCaching.StackExchangeRedis) NuGet package.

* Call the `builder.Services.AddStackExchangeRedisOutputCache` method (not the `AddStackExchangeRedisCache` method), and provide a connection string that points to a Redis server.

  For example:

  ```
  builder.Services.AddStackExchangeRedisOutputCache(options =>
  {
      options.Configuration = 
          builder.Configuration.GetConnectionString("MyRedisConStr");
      options.InstanceName = "SampleInstance";
  });

  builder.Services.AddOutputCache(options =>
  {
      options.AddBasePolicy(builder => 
          builder.Expire(TimeSpan.FromSeconds(10)));
  });
  ```

  * The [options.Configuration](/en-us/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscacheoptions.configuration#microsoft-extensions-caching-stackexchangeredis-rediscacheoptions-configuration) property is a connection string to an on-premises Redis server or to a hosted offering such as [Azure Cache for Redis](/en-us/azure/azure-cache-for-redis/). For example, `<instance_name>.redis.cache.windows.net:6380,password=,pw,ssl=True,abortConnect=False` for Azure Cache for Redis.

  * (Optional) The [options.InstanceName](/en-us/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscacheoptions.instancename#microsoft-extensions-caching-stackexchangeredis-rediscacheoptions-instancename) property specifies a logical partition for the cache.

  The configuration options are identical to the [Redis-based distributed caching options](distributed?view=aspnetcore-10.0#distributed-redis-cache).

### Not recommended: IDistributedCache

The [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) interface isn't recommended for use with output caching. This interface doesn't provide atomic features, which are required for tagging.

The recommended approach is to use the built-in support for Redis or create a custom [IOutputCacheStore](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.ioutputcachestore) implementation by using direct dependencies on the underlying storage mechanism.

## Related content

* [Overview of caching in ASP.NET Core](overview?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../../fundamentals/middleware/?view=aspnetcore-10.0)
* [OutputCacheOptions](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions)
* [OutputCachePolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder)

This article explains how to configure output caching middleware in an ASP.NET Core app. For an introduction to output caching, see [Output caching](overview?view=aspnetcore-10.0#output-caching).

The output caching middleware can be used in all types of ASP.NET Core apps: Minimal API, Web API with controllers, MVC, and Razor Pages. The sample app is a Minimal API, but every caching feature it illustrates is also supported in the other app types.

## Add the middleware to the app

Add the output caching middleware to the service collection by calling [AddOutputCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.outputcacheservicecollectionextensions.addoutputcache).

Add the middleware to the request processing pipeline by calling [UseOutputCache](/en-us/dotnet/api/microsoft.aspnetcore.builder.outputcacheapplicationbuilderextensions.useoutputcache).

Note

* In apps that use [CORS middleware](../../security/cors?view=aspnetcore-10.0), `UseOutputCache` must be called after [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors).
* In Razor Pages apps and apps with controllers, `UseOutputCache` must be called after `UseRouting`.
* In apps that use [authentication](../../security/authentication/?view=aspnetcore-10.0) or [authorization](../../security/authorization/introduction?view=aspnetcore-10.0), `UseOutputCache` must be called after [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). Otherwise, the output caching middleware can serve content cached for unauthorized users instead of content for authorized users.
* Calling `AddOutputCache`and `UseOutputCache` doesn't start caching behavior, it makes caching available. Caching response data must be configured as shown in the following sections.

## Configure one endpoint or page

For Minimal API apps, configure an endpoint to do caching by calling [`CacheOutput`](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.outputcacheconventionbuilderextensions.cacheoutput), or by applying the [`[OutputCache]`](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheattribute) attribute, as shown in the following examples:

```
app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));
```

For apps with controllers, apply the `[OutputCache]` attribute to the action method. For Razor Pages apps, apply the attribute to the Razor page class.

## Configure multiple endpoints or pages

Create *policies* when calling `AddOutputCache` to specify caching configuration that applies to multiple endpoints. A policy can be selected for specific endpoints, while a base policy provides default caching configuration for a collection of endpoints.

The following highlighted code configures caching for all of the app's endpoints, with expiration time of 10 seconds. If an expiration time isn't specified, it defaults to one minute.

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});
```

The following highlighted code creates two policies, each specifying a different expiration time. Selected endpoints can use the 20 second expiration, and others can use the 30 second expiration.

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});
```

You can select a policy for an endpoint when calling the `CacheOutput` method or using the `[OutputCache]` attribute:

```
app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));
```

For apps with controllers, apply the `[OutputCache]` attribute to the action method. For Razor Pages apps, apply the attribute to the Razor page class.

## Default output caching policy

By default, output caching follows these rules:

* Only HTTP 200 responses are cached.
* Only HTTP GET or HEAD requests are cached.
* Responses that set cookies aren't cached.
* Responses to authenticated requests aren't cached.

The following code applies all of the default caching rules to all of an app's endpoints:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});
```

### Override the default policy

The following code shows how to override the default rules. The highlighted lines in the following custom policy code enable caching for HTTP POST methods and HTTP 301 responses:

```
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeResponseAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        var response = context.HttpContext.Response;

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}
```

To use this custom policy, create a named policy:

```
builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});
```

And select the named policy for an endpoint:

```
app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");
```

### Alternative default policy override

Alternatively, use Dependency Injection (DI) to initialize an instance, with the following changes to the custom policy class:

* A public constructor instead of a private constructor.
* Eliminate the `Instance` property in the custom policy class.

For example:

```
public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }
```

The remainder of the class is the same as shown previously. Add the custom policy as shown in the following example:

```
builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});
```

The preceding code uses DI to create the instance of the custom policy class. Any public arguments in the constructor are resolved.

When using a custom policy as a base policy, don't call `OutputCache()` (with no arguments) on any endpoint that the base policy should apply to. Calling `OutputCache()` adds the default policy to the endpoint.

## Specify the cache key

By default, every part of the URL is included as the key to a cache entry, that is, the scheme, host, port, path, and query string. However, you might want to explicitly control the cache key. For example, suppose you have an endpoint that returns a unique response only for each unique value of the `culture` query string. Variation in other parts of the URL, such as other query strings, shouldn't result in different cache entries. You can specify such rules in a policy, as shown in the following highlighted code:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
```

You can then select the `VaryByQuery` policy for an endpoint:

```
app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");
```

Here are some of the options for controlling the cache key:

* [SetVaryByQuery](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder.setvarybyquery) - Specify one or more query string names to add to the cache key.

* [SetVaryByHeader](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder.setvarybyheader) - Specify one or more HTTP headers to add to the cache key.

* [VaryByValue](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder.varybyvalue)- Specify a value to add to the cache key. The following example uses a value that indicates whether the current server time in seconds is odd or even. A new response is generated only when the number of seconds goes from odd to even or even to odd.

  ```
  app.MapGet("/varybyvalue", Gravatar.WriteGravatar)
      .CacheOutput(c => c.VaryByValue((context) => 
          new KeyValuePair<string, string>(
              "time", (DateTime.Now.Second % 2)
                  .ToString(CultureInfo.InvariantCulture))));
  ```

Use [OutputCacheOptions.UseCaseSensitivePaths](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions.usecasesensitivepaths#microsoft-aspnetcore-outputcaching-outputcacheoptions-usecasesensitivepaths) to specify that the path part of the key is case sensitive. The default is case insensitive.

For more options, see the [OutputCachePolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder) class.

## Cache revalidation

Cache revalidation means the server can return a `304 Not Modified` HTTP status code instead of the full response body. This status code informs the client that the response to the request is unchanged from what the client previously received.

The following code illustrates the use of an [`Etag`](https://developer.mozilla.org/docs/Web/HTTP/Headers/ETag) header to enable cache revalidation. If the client sends an [`If-None-Match`](https://developer.mozilla.org/docs/Web/HTTP/Headers/If-None-Match) header with the etag value of an earlier response, and the cache entry is fresh, the server returns [304 Not Modified](https://developer.mozilla.org/docs/Web/HTTP/Status/304) instead of the full response:

```
app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();
```

Another way to do cache revalidation is to check the date of the cache entry creation compared to the date requested by the client. When the request header `If-Modified-Since` is provided, output caching returns 304 if the cached entry is older and isn't expired.

Cache revalidation is automatic in response to these headers sent from the client. No special configuration is required on the server to enable this behavior, aside from enabling output caching.

## Use tags to evict cache entries

You can use tags to identify a group of endpoints and evict all cache entries for the group. For example, the following code creates a pair of endpoints whose URLs begin with "blog", and tags them "tag-blog":

```
app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
```

An alternative way to assign tags for the same pair of endpoints is to define a base policy that applies to endpoints that begin with `blog`:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
```

Another alternative is to call `MapGroup`:

```
var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);
```

In the preceding tag assignment examples, both endpoints are identified by the `tag-blog` tag. You can then evict the cache entries for those endpoints with a single statement that references that tag:

```
app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});
```

With this code, an HTTP POST request sent to `https://localhost:<port>/purge/tag-blog` will evict cache entries for these endpoints.

You might want a way to evict all cache entries for all endpoints. To do that, create a base policy for all endpoints as the following code does:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
```

This base policy enables you to use the "tag-all" tag to evict everything in cache.

## Disable resource locking

By default, resource locking is enabled to mitigate the risk of [cache stampede and thundering herd](https://en.wikipedia.org/wiki/Thundering_herd_problem). For more information, see [Output Caching](overview?view=aspnetcore-10.0#output-caching).

To disable resource locking, call [SetLocking(false)](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder.setlocking) while creating a policy, as shown in the following example:

```
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});
```

The following example selects the no-locking policy for an endpoint:

```
app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");
```

## Limits

The following properties of [OutputCacheOptions](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions) let you configure limits that apply to all endpoints:

* [SizeLimit](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions.sizelimit#microsoft-aspnetcore-outputcaching-outputcacheoptions-sizelimit) - Maximum size of cache storage. When this limit is reached, no new responses will be cached until older entries are evicted. Default value is 100 MB.
* [MaximumBodySize](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions.maximumbodysize#microsoft-aspnetcore-outputcaching-outputcacheoptions-maximumbodysize) - If the response body exceeds this limit, it will not be cached. Default value is 64 MB.
* [DefaultExpirationTimeSpan](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions.defaultexpirationtimespan#microsoft-aspnetcore-outputcaching-outputcacheoptions-defaultexpirationtimespan) - The expiration time duration that applies when not specified by a policy. Default value is 60 seconds.

## Cache storage

[IOutputCacheStore](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.ioutputcachestore) is used for storage. By default it's used with [MemoryCache](/en-us/dotnet/api/system.runtime.caching.memorycache). We don't recommend [IDistributedCache](/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache) for use with output caching. `IDistributedCache` doesn't have atomic features, which are required for tagging. We recommend that you create custom [IOutputCacheStore](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.ioutputcachestore) implementations by using direct dependencies on the underlying storage mechanism, such as Redis. Or use the [built-in support for Redis cache in .NET 8.](output?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-8.0#cache-storage).

## See also

* [Overview of caching in ASP.NET Core](overview?view=aspnetcore-10.0)
* [ASP.NET Core middleware](../../fundamentals/middleware/?view=aspnetcore-10.0)
* [OutputCacheOptions](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcacheoptions)
* [OutputCachePolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.outputcaching.outputcachepolicybuilder)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/ui-class
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/ui-class.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Create reusable UI using the Razor class library project in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

Razor views, pages, controllers, page models, [Razor components](../blazor/components/class-libraries?view=aspnetcore-10.0), [View components](../mvc/views/view-components?view=aspnetcore-10.0), and data models can be built into a Razor class library (RCL). The RCL can be packaged and reused. Applications can include the RCL and override the views and pages it contains. When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence.

For information on how to integrate npm and webpack into the build process for a RCL, see [Build client web assets for your Razor Class Library](https://devblogs.microsoft.com/dotnet/build-client-web-assets-for-your-razor-class-library/).

## Create a class library containing Razor UI

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

- From Visual Studio's **Start Window**, select **Create a new project**. Alternatively, select the **File** > **New** > **Project/Solution**.
- Select **Razor Class Library** > **Next**.
- Name the library (for example, "RazorClassLib"), > **Create**. To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.
- Select **Support pages and views** if you need the library to contain pages and/or views. By default, only Razor components are supported. Select **Create**.

The Razor Class Library template defaults to Razor component development by default. The **Support pages and views** option supports pages and views. For more information on RCL support in Blazor, see [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../blazor/components/class-libraries?view=aspnetcore-10.0).

From the command line, run `dotnet new razorclasslib`. For example:

```
dotnet new razorclasslib -o RazorUIClassLib
```

The Razor Class Library template defaults to Razor component development by default. Pass the `--support-pages-and-views` option (`dotnet new razorclasslib --support-pages-and-views`) to provide support for pages and views.

For more information, see [dotnet new](/en-us/dotnet/core/tools/dotnet-new). To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.

Add Razor files to the RCL.

The ASP.NET Core templates assume the RCL content is in the `Areas` folder. See [RCL Pages layout](#rcl-lay) below to create an RCL that exposes content in `~/Pages` rather than `~/Areas/Pages`.

## Reference RCL content

The RCL can be referenced by:

* NuGet package. See [Creating NuGet packages](/en-us/nuget/create-packages/creating-a-package) and [dotnet add package](/en-us/dotnet/core/tools/dotnet-add-package) and [Create and publish a NuGet package](/en-us/nuget/quickstart/create-and-publish-a-package-using-visual-studio).
* `{ProjectName}.csproj`. See [dotnet-add reference](/en-us/dotnet/core/tools/dotnet-add-reference).

## Override views, partial views, and pages

When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence. For example, add `WebApp1/Areas/MyFeature/Pages/Page1.cshtml` to WebApp1, and Page1 in the WebApp1 will take precedence over Page1 in the RCL.

In the sample download, rename `WebApp1/Areas/MyFeature2` to `WebApp1/Areas/MyFeature` to test precedence.

Copy the `RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml` partial view to `WebApp1/Areas/MyFeature/Pages/Shared/_Message.cshtml`. Update the markup to indicate the new location. Build and run the app to verify the app's version of the partial is being used.

If the RCL uses Razor Pages, enable the Razor Pages services and endpoints in the hosting app:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();
app.Run();
```

[]()

### RCL Pages layout

To reference RCL content as though it is part of the web app's `Pages` folder, create the RCL project with the following file structure:

* `RazorUIClassLib/Pages`
* `RazorUIClassLib/Pages/Shared`

Suppose `RazorUIClassLib/Pages/Shared` contains two partial files: `_Header.cshtml` and `_Footer.cshtml`. The `<partial>` tags could be added to `_Layout.cshtml` file:

```
<body>
  <partial name="_Header">
  @RenderBody()
  <partial name="_Footer">
</body>
```

Add the `_ViewStart.cshtml` file to the RCL project's `Pages` folder to use the `_Layout.cshtml` file from the host web app:

```
@{
    Layout = "_Layout";
}
```

## Create an RCL with static assets

An RCL may require companion static assets that can be referenced by either the RCL or the consuming app of the RCL. ASP.NET Core allows creating RCLs that include static assets that are available to a consuming app.

To include companion assets as part of an RCL, create a `wwwroot` folder in the class library and include any required files in that folder.

When packing an RCL, all companion assets in the `wwwroot` folder are automatically included in the package.

Use the `dotnet pack` command rather than the NuGet.exe version `nuget pack`.

### Add client web assets to the build process

Integrating client web assets into the build pipeline is nontrivial. See [Build client web assets for your Razor Class Library](https://devblogs.microsoft.com/dotnet/build-client-web-assets-for-your-razor-class-library/) for more information.

### Exclude static assets

To exclude static assets, add the desired exclusion path to the `$(DefaultItemExcludes)` property group in the project file. Separate entries with a semicolon (`;`).

In the following example, the `lib.css` stylesheet in the `wwwroot` folder isn't considered a static asset and isn't included in the published RCL:

```
<PropertyGroup>
  <DefaultItemExcludes>$(DefaultItemExcludes);wwwroot\lib.css</DefaultItemExcludes>
</PropertyGroup>
```

### Typescript integration

To include TypeScript files in an RCL:

* Reference the [`Microsoft.TypeScript.MSBuild`](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild) NuGet package in the project.

  Note

  For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

* Place the TypeScript files (`.ts`) outside of the `wwwroot` folder. For example, place the files in a `Client` folder.

* Add the following markup to the project file:

  * Configure the TypeScript build output for the `wwwroot` folder with the `TypescriptOutDir` property.
  * Include the TypeScript target as a dependency of the `PrepareForBuildDependsOn` target.
  * Remove the output in the `wwwroot folder`.

```
<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    // Markup removed for brevity.
    <TypescriptOutDir>wwwroot</TypescriptOutDir>
    <PrepareForBuildDependsOn>
      CompileTypeScriptWithTSConfig;
      GetTypeScriptOutputForPublishing;$(PrepareForBuildDependsOn)
    </PrepareForBuildDependsOn>
  </PropertyGroup>

  <ItemGroup>
    <Content Remove="wwwroot\{path-to-typescript-outputs}" />
  </ItemGroup>

</Project>
```

### Consume content from a referenced RCL

The files included in the `wwwroot` folder of the RCL are exposed to either the RCL or the consuming app under the prefix `_content/{PACKAGE ID}/`. For example, a library with an assembly name of `Razor.Class.Lib` and without a `<PackageId>` specified in its project file results in a path to static content at `_content/Razor.Class.Lib/`. When producing a NuGet package and the assembly name isn't the same as the package ID ([`<PackageId>`](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties) in the library's project file), use the package ID as specified in the project file for `{PACKAGE ID}`.

The consuming app references static assets provided by the library with `<script>`, `<style>`, `<img>`, and other HTML tags. The consuming app must have [static file support](../fundamentals/static-files?view=aspnetcore-10.0) enabled in:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();
app.Run();
```

When running the consuming app from build output (`dotnet run`), static web assets are enabled by default in the `Development` environment. To support assets in other environments when running from build output, call [UseStaticWebAssets](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestaticwebassets) on the host builder in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

Calling `UseStaticWebAssets` isn't required when running an app from published output (`dotnet publish`).

### Multi-project development flow

When the consuming app runs:

* The assets in the RCL stay in their original folders. The assets aren't moved to the consuming app.
* Any change within the RCL's `wwwroot` folder is reflected in the consuming app after the RCL is rebuilt and without rebuilding the consuming app.

When the RCL is built, a manifest is produced that describes the static web asset locations. The consuming app reads the manifest at runtime to consume the assets from referenced projects and packages. When a new asset is added to an RCL, the RCL must be rebuilt to update its manifest before a consuming app can access the new asset.

### Publish

When the app is published, the companion assets from all referenced projects and packages are copied into the `wwwroot` folder of the published app under `_content/{PACKAGE ID}/`. When producing a NuGet package and the assembly name isn't the same as the package ID ([`<PackageId>`](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties) in the library's project file), use the package ID as specified in the project file for `{PACKAGE ID}` when examining the `wwwroot` folder for the published assets.

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/ui-class/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../blazor/components/class-libraries?view=aspnetcore-10.0)

* [ASP.NET Core Blazor CSS isolation](../blazor/components/css-isolation?view=aspnetcore-10.0#razor-class-library-rcl-support)

Razor views, pages, controllers, page models, [Razor components](../blazor/components/class-libraries?view=aspnetcore-10.0), [View components](../mvc/views/view-components?view=aspnetcore-10.0), and data models can be built into a Razor class library (RCL). The RCL can be packaged and reused. Applications can include the RCL and override the views and pages it contains. When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence.

For information on how to integrate npm and webpack into the build process for a Razor Class Library, see [Build client web assets for your Razor Class Library](https://devblogs.microsoft.com/dotnet/build-client-web-assets-for-your-razor-class-library/).

## Create a class library containing Razor UI

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

- From Visual Studio's **Start Window**, select **Create a new project**. Alternatively, select **File** > **New** > **Project/Solution**.
- Select **Razor Class Library** > **Next**.
- Name the library (for example, "RazorClassLib"), > **Create**. To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.
- Select **Support pages and views** if you need to support views. By default, only Razor Pages are supported. Select **Create**.

The Razor Class Library (RCL) template defaults to Razor component development by default. The **Support pages and views** option supports pages and views.

From the command line, run `dotnet new razorclasslib`. For example:

```
dotnet new razorclasslib -o RazorUIClassLib
```

The Razor Class Library (RCL) template defaults to Razor component development by default. Pass the `--support-pages-and-views` option (`dotnet new razorclasslib --support-pages-and-views`) to provide support for pages and views.

For more information, see [dotnet new](/en-us/dotnet/core/tools/dotnet-new). To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.

Add Razor files to the RCL.

The ASP.NET Core templates assume the RCL content is in the `Areas` folder. See [RCL Pages layout](#rcl-lay) below to create an RCL that exposes content in `~/Pages` rather than `~/Areas/Pages`.

## Reference RCL content

The RCL can be referenced by:

* NuGet package. See [Creating NuGet packages](/en-us/nuget/create-packages/creating-a-package) and [dotnet add package](/en-us/dotnet/core/tools/dotnet-add-package) and [Create and publish a NuGet package](/en-us/nuget/quickstart/create-and-publish-a-package-using-visual-studio).
* `{ProjectName}.csproj`. See [dotnet-add reference](/en-us/dotnet/core/tools/dotnet-add-reference).

## Override views, partial views, and pages

When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence. For example, add `WebApp1/Areas/MyFeature/Pages/Page1.cshtml` to WebApp1, and Page1 in the WebApp1 will take precedence over Page1 in the RCL.

In the sample download, rename `WebApp1/Areas/MyFeature2` to `WebApp1/Areas/MyFeature` to test precedence.

Copy the `RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml` partial view to `WebApp1/Areas/MyFeature/Pages/Shared/_Message.cshtml`. Update the markup to indicate the new location. Build and run the app to verify the app's version of the partial is being used.

If the RCL uses Razor Pages, enable the Razor Pages services and endpoints in the hosting app:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();
app.Run();
```

[]()

### RCL Pages layout

To reference RCL content as though it is part of the web app's `Pages` folder, create the RCL project with the following file structure:

* `RazorUIClassLib/Pages`
* `RazorUIClassLib/Pages/Shared`

Suppose `RazorUIClassLib/Pages/Shared` contains two partial files: `_Header.cshtml` and `_Footer.cshtml`. The `<partial>` tags could be added to `_Layout.cshtml` file:

```
<body>
  <partial name="_Header">
  @RenderBody()
  <partial name="_Footer">
</body>
```

Add the `_ViewStart.cshtml` file to the RCL project's `Pages` folder to use the `_Layout.cshtml` file from the host web app:

```
@{
    Layout = "_Layout";
}
```

## Create an RCL with static assets

An RCL may require companion static assets that can be referenced by either the RCL or the consuming app of the RCL. ASP.NET Core allows creating RCLs that include static assets that are available to a consuming app.

To include companion assets as part of an RCL, create a `wwwroot` folder in the class library and include any required files in that folder.

When packing an RCL, all companion assets in the `wwwroot` folder are automatically included in the package.

Use the `dotnet pack` command rather than the NuGet.exe version `nuget pack`.

### Exclude static assets

To exclude static assets, add the desired exclusion path to the `$(DefaultItemExcludes)` property group in the project file. Separate entries with a semicolon (`;`).

In the following example, the `lib.css` stylesheet in the `wwwroot` folder isn't considered a static asset and isn't included in the published RCL:

```
<PropertyGroup>
  <DefaultItemExcludes>$(DefaultItemExcludes);wwwroot\lib.css</DefaultItemExcludes>
</PropertyGroup>
```

### Typescript integration

To include TypeScript files in an RCL:

1. Reference the [`Microsoft.TypeScript.MSBuild`](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild) NuGet package in the project.

   Note

   For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

2. Place the TypeScript files (`.ts`) outside of the `wwwroot` folder. For example, place the files in a `Client` folder.

3. Configure the TypeScript build output for the `wwwroot` folder. Set the `TypescriptOutDir` property inside of a `PropertyGroup` in the project file:

   ```
   <TypescriptOutDir>wwwroot</TypescriptOutDir>
   ```

4. Include the TypeScript target as a dependency of the `PrepareForBuildDependsOn` target by adding the following target inside of a `PropertyGroup` in the project file:

   ```
   <PrepareForBuildDependsOn>
     CompileTypeScript;
     GetTypeScriptOutputForPublishing;$(PrepareForBuildDependsOn)
   </PrepareForBuildDependsOn>
   ```

### Consume content from a referenced RCL

The files included in the `wwwroot` folder of the RCL are exposed to either the RCL or the consuming app under the prefix `_content/{PACKAGE ID}/`. For example, a library with an assembly name of `Razor.Class.Lib` and without a `<PackageId>` specified in its project file results in a path to static content at `_content/Razor.Class.Lib/`. When producing a NuGet package and the assembly name isn't the same as the package ID ([`<PackageId>`](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties) in the library's project file), use the package ID as specified in the project file for `{PACKAGE ID}`.

The consuming app references static assets provided by the library with `<script>`, `<style>`, `<img>`, and other HTML tags. The consuming app must have [static file support](../fundamentals/static-files?view=aspnetcore-10.0) enabled in:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();
app.Run();
```

When running the consuming app from build output (`dotnet run`), static web assets are enabled by default in the `Development` environment. To support assets in other environments when running from build output, call [UseStaticWebAssets](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.usestaticwebassets) on the host builder in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseWebRoot("wwwroot").UseStaticWebAssets();

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

***Note:*** .NET 6 only requires calling `builder.WebHost.UseWebRoot("wwwroot").UseStaticWebAssets`. For more information, see [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/38212).

Calling `UseStaticWebAssets` isn't required when running an app from published output (`dotnet publish`).

### Multi-project development flow

When the consuming app runs:

* The assets in the RCL stay in their original folders. The assets aren't moved to the consuming app.
* Any change within the RCL's `wwwroot` folder is reflected in the consuming app after the RCL is rebuilt and without rebuilding the consuming app.

When the RCL is built, a manifest is produced that describes the static web asset locations. The consuming app reads the manifest at runtime to consume the assets from referenced projects and packages. When a new asset is added to an RCL, the RCL must be rebuilt to update its manifest before a consuming app can access the new asset.

### Publish

When the app is published, the companion assets from all referenced projects and packages are copied into the `wwwroot` folder of the published app under `_content/{PACKAGE ID}/`. When producing a NuGet package and the assembly name isn't the same as the package ID ([`<PackageId>`](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties) in the library's project file), use the package ID as specified in the project file for `{PACKAGE ID}` when examining the `wwwroot` folder for the published assets.

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/ui-class/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../blazor/components/class-libraries?view=aspnetcore-10.0)

* [ASP.NET Core Blazor CSS isolation](../blazor/components/css-isolation?view=aspnetcore-10.0#razor-class-library-rcl-support)

Razor views, pages, controllers, page models, [Razor components](../blazor/components/class-libraries?view=aspnetcore-10.0), [View components](../mvc/views/view-components?view=aspnetcore-10.0), and data models can be built into a Razor class library (RCL). The RCL can be packaged and reused. Applications can include the RCL and override the views and pages it contains. When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/ui-class/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Create a class library containing Razor UI

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

- From Visual Studio's **Start Window**, select **Create a new project**. Alternatively, select the **File** > **New** > **Project/Solution**.
- Select **Razor Class Library** > **Next**.
- Name the library (for example, "RazorClassLib"), > **Create** > **Next**. To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.
- Select the **Target Framework**. Check **☑ Support pages and views** to support views. By default, only Razor components are supported. Select **Create**.

The Razor class library (RCL) template defaults to Razor component development by default. The **Support pages and views** option supports pages and views.

From the command line, run `dotnet new razorclasslib`. For example:

```
dotnet new razorclasslib -o RazorUIClassLib
```

The Razor class library (RCL) template defaults to Razor component development by default. Pass the `--support-pages-and-views` option (`dotnet new razorclasslib --support-pages-and-views`) to provide support for pages and views.

For more information, see [dotnet new](/en-us/dotnet/core/tools/dotnet-new). To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.

Add Razor files to the RCL.

The ASP.NET Core templates assume the RCL content is in the `Areas` folder. See [RCL Pages layout](#rcl-pages-layout) to create an RCL that exposes content in `~/Pages` rather than `~/Areas/Pages`.

## Reference RCL content

The RCL can be referenced by:

* NuGet package. See [Creating NuGet packages](/en-us/nuget/create-packages/creating-a-package) and [dotnet add package](/en-us/dotnet/core/tools/dotnet-add-package) and [Create and publish a NuGet package](/en-us/nuget/quickstart/create-and-publish-a-package-using-visual-studio).
* `{ProjectName}.csproj`. See [dotnet-add reference](/en-us/dotnet/core/tools/dotnet-add-reference).

## Override views, partial views, and pages

When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence. For example, add `WebApp1/Areas/MyFeature/Pages/Page1.cshtml` to WebApp1, and Page1 in the WebApp1 will take precedence over Page1 in the RCL.

In the sample download, rename `WebApp1/Areas/MyFeature2` to `WebApp1/Areas/MyFeature` to test precedence.

Copy the `RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml` partial view to `WebApp1/Areas/MyFeature/Pages/Shared/_Message.cshtml`. Update the markup to indicate the new location. Build and run the app to verify the app's version of the partial is being used.

### RCL Pages layout

To reference RCL content as though it is part of the web app's `Pages` folder, create the RCL project with the following file structure:

* `RazorUIClassLib/Pages`
* `RazorUIClassLib/Pages/Shared`

Suppose `RazorUIClassLib/Pages/Shared` contains two partial files: `_Header.cshtml` and `_Footer.cshtml`. The `<partial>` tags could be added to `_Layout.cshtml` file:

```
<body>
  <partial name="_Header">
  @RenderBody()
  <partial name="_Footer">
</body>
```

Add the `_ViewStart.cshtml` file to the RCL project's `Pages` folder to use the `_Layout.cshtml` file from the host web app:

```
@{
    Layout = "_Layout";
}
```

## Create an RCL with static assets

An RCL may require companion static assets that can be referenced by either the RCL or the consuming app of the RCL. ASP.NET Core allows creating RCLs that include static assets that are available to a consuming app.

To include companion assets as part of an RCL, create a `wwwroot` folder in the class library and include any required files in that folder.

When packing an RCL, all companion assets in the `wwwroot` folder are automatically included in the package.

Use the `dotnet pack` command rather than the NuGet.exe version `nuget pack`.

### Exclude static assets

To exclude static assets, add the desired exclusion path to the `$(DefaultItemExcludes)` property group in the project file. Separate entries with a semicolon (`;`).

In the following example, the `lib.css` stylesheet in the `wwwroot` folder isn't considered a static asset and isn't included in the published RCL:

```
<PropertyGroup>
  <DefaultItemExcludes>$(DefaultItemExcludes);wwwroot\lib.css</DefaultItemExcludes>
</PropertyGroup>
```

### Typescript integration

To include TypeScript files in an RCL:

1. Reference the [`Microsoft.TypeScript.MSBuild`](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild) NuGet package in the project.

   Note

   For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

2. Place the TypeScript files (`.ts`) outside of the `wwwroot` folder. For example, place the files in a `Client` folder.

3. Configure the TypeScript build output for the `wwwroot` folder. Set the `TypescriptOutDir` property inside of a `PropertyGroup` in the project file:

   ```
   <TypescriptOutDir>wwwroot</TypescriptOutDir>
   ```

4. Include the TypeScript target as a dependency of the `ResolveCurrentProjectStaticWebAssets` target by adding the following target inside of a `PropertyGroup` in the project file:

   ```
   <ResolveCurrentProjectStaticWebAssetsInputsDependsOn>
     CompileTypeScript;
     $(ResolveCurrentProjectStaticWebAssetsInputs)
   </ResolveCurrentProjectStaticWebAssetsInputsDependsOn>
   ```

### Consume content from a referenced RCL

The files included in the `wwwroot` folder of the RCL are exposed to either the RCL or the consuming app under the prefix `_content/{PACKAGE ID}/`. For example, a library with an assembly name of `Razor.Class.Lib` and without a `<PackageId>` specified in its project file results in a path to static content at `_content/Razor.Class.Lib/`. When producing a NuGet package and the assembly name isn't the same as the package ID ([`<PackageId>`](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties) in the library's project file), use the package ID as specified in the project file for `{PACKAGE ID}`.

The consuming app references static assets provided by the library with `<script>`, `<style>`, `<img>`, and other HTML tags. The consuming app must have [static file support](../fundamentals/static-files?view=aspnetcore-10.0) enabled in `Startup.Configure`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseStaticFiles();

    ...
}
```

When running the consuming app from build output (`dotnet run`), static web assets are enabled by default in the `Development` environment. To support assets in other environments when running from build output, call `UseStaticWebAssets` on the host builder in `Program.cs`:

```
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStaticWebAssets();
                webBuilder.UseStartup<Startup>();
            });
}
```

Calling `UseStaticWebAssets` isn't required when running an app from published output (`dotnet publish`).

### Multi-project development flow

When the consuming app runs:

* The assets in the RCL stay in their original folders. The assets aren't moved to the consuming app.
* Any change within the RCL's `wwwroot` folder is reflected in the consuming app after the RCL is rebuilt and without rebuilding the consuming app.

When the RCL is built, a manifest is produced that describes the static web asset locations. The consuming app reads the manifest at runtime to consume the assets from referenced projects and packages. When a new asset is added to an RCL, the RCL must be rebuilt to update its manifest before a consuming app can access the new asset.

### Publish

When the app is published, the companion assets from all referenced projects and packages are copied into the `wwwroot` folder of the published app under `_content/{PACKAGE ID}/`. When producing a NuGet package and the assembly name isn't the same as the package ID ([`<PackageId>`](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties) in the library's project file), use the package ID as specified in the project file for `{PACKAGE ID}` when examining the `wwwroot` folder for the published assets.

## Additional resources

* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../blazor/components/class-libraries?view=aspnetcore-10.0)

Razor views, pages, controllers, page models, [Razor components](../blazor/components/class-libraries?view=aspnetcore-10.0), [View components](../mvc/views/view-components?view=aspnetcore-10.0), and data models can be built into a Razor class library (RCL). The RCL can be packaged and reused. Applications can include the RCL and override the views and pages it contains. When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/ui-class/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Create a class library containing Razor UI

* [Visual Studio](#tabpanel_3_visual-studio)
* [.NET CLI](#tabpanel_3_net-cli)

- From the Visual Studio **File** menu, select **New** > **Project**.
- Select **ASP.NET Core Web Application**.
- Name the library (for example, "RazorClassLib") > **OK**. To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.
- Verify **ASP.NET Core 2.1** or later is selected.
- Select **Razor Class Library** > **OK**.

An RCL has the following project file:

```
<Project Sdk="Microsoft.NET.Sdk.Razor">

    <PropertyGroup>
        <TargetFramework>netcoreapp3.1</TargetFramework>
        <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
    </PropertyGroup>

    <ItemGroup>
        <FrameworkReference Include="Microsoft.AspNetCore.App" />
    </ItemGroup>


</Project>
```

From the command line, run `dotnet new razorclasslib`. For example:

```
dotnet new razorclasslib -o RazorUIClassLib
```

For more information, see [dotnet new](/en-us/dotnet/core/tools/dotnet-new). To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.

Add Razor files to the RCL.

The ASP.NET Core templates assume the RCL content is in the `Areas` folder. See [RCL Pages layout](#rcl-pages-layout) to create an RCL that exposes content in `~/Pages` rather than `~/Areas/Pages`.

## Reference RCL content

The RCL can be referenced by:

* NuGet package. See [Creating NuGet packages](/en-us/nuget/create-packages/creating-a-package) and [dotnet add package](/en-us/dotnet/core/tools/dotnet-add-package) and [Create and publish a NuGet package](/en-us/nuget/quickstart/create-and-publish-a-package-using-visual-studio).
* `{ProjectName}.csproj`. See [dotnet-add reference](/en-us/dotnet/core/tools/dotnet-add-reference).

## Walkthrough: Create an RCL project and use from a Razor Pages project

You can download the [complete project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/ui-class/samples) and test it rather than creating it. The sample download contains additional code and links that make the project easy to test. You can leave feedback in [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/6098) with your comments on download samples versus step-by-step instructions.

### Test the download app

If you haven't downloaded the completed app and would rather create the walkthrough project, skip to the [next section](#create-an-rcl).

* [Visual Studio](#tabpanel_4_visual-studio)
* [.NET CLI](#tabpanel_4_net-cli)

Open the `.sln` file in Visual Studio. Run the app.

From a command prompt in the `cli` directory, build the RCL and web app.

```
dotnet build
```

Move to the `WebApp1` directory and run the app:

```
dotnet run
```

Follow the instructions in [Test WebApp1](#test-webapp1)

## Create an RCL

In this section, an RCL is created. Razor files are added to the RCL.

* [Visual Studio](#tabpanel_5_visual-studio)
* [.NET CLI](#tabpanel_5_net-cli)

Create the RCL project:

* From the Visual Studio **File** menu, select **New** > **Project**.
* Select **ASP.NET Core Web Application**.
* Name the app **RazorUIClassLib** > **OK**.
* Verify **ASP.NET Core 2.1** or later is selected.
* Select **Razor Class Library** > **OK**.
* Add a Razor partial view file named `RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml`.

From the command line, run the following:

```
dotnet new razorclasslib -o RazorUIClassLib
dotnet new page -n _Message -np -o RazorUIClassLib/Areas/MyFeature/Pages/Shared
dotnet new viewstart -o RazorUIClassLib/Areas/MyFeature/Pages
```

The preceding commands:

* Creates the `RazorUIClassLib` RCL.
* Creates a Razor \_Message page, and adds it to the RCL. The `-np` parameter creates the page without a `PageModel`.
* Creates a [`_ViewStart.cshtml`](../mvc/views/layout?view=aspnetcore-10.0#running-code-before-each-view) file and adds it to the RCL.

The `_ViewStart.cshtml` file is required to use the layout of the Razor Pages project (which is added in the next section).

### Add Razor files and folders to the project

* Replace the markup in `RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml` with the following code:

  ```
  <h3>_Message.cshtml partial view.</h3>

  <p>RazorUIClassLib\Areas\MyFeature\Pages\Shared\_Message.cshtml</p>
  ```

* Replace the markup in `RazorUIClassLib/Areas/MyFeature/Pages/Page1.cshtml` with the following code:

  ```
  @page
  @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

  <h2>Hello from a Razor UI class library!</h2>
  <p> From  RazorUIClassLib\Areas\MyFeature\Pages\Page1.cshtml</p>

  <partial name="_Message" />
  ```

  `@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers` is required to use the partial view (`<partial name="_Message" />`). Rather than including the `@addTagHelper` directive, you can add a `_ViewImports.cshtml` file. For example:

  ```
  dotnet new viewimports -o RazorUIClassLib/Areas/MyFeature/Pages
  ```

  For more information on `_ViewImports.cshtml`, see [Importing Shared Directives](../mvc/views/layout?view=aspnetcore-10.0#importing-shared-directives)

* Build the class library to verify there are no compiler errors:

  ```
  dotnet build RazorUIClassLib
  ```

The build output contains `RazorUIClassLib.dll` and `RazorUIClassLib.Views.dll`. `RazorUIClassLib.Views.dll` contains the compiled Razor content.

### Use the Razor UI library from a Razor Pages project

* [Visual Studio](#tabpanel_6_visual-studio)
* [.NET CLI](#tabpanel_6_net-cli)

Create the Razor Pages web app:

* From **Solution Explorer**, right-click the solution > **Add** > **New Project**.

* Select **ASP.NET Core Web Application**.

* Name the app **WebApp1**.

* Verify **ASP.NET Core 2.1** or later is selected.

* Select **Web Application** > **OK**.

* From **Solution Explorer**, right-click on **WebApp1** and select **Set as StartUp Project**.

* From **Solution Explorer**, right-click on **WebApp1** and select **Build Dependencies** > **Project Dependencies**.

* Check **RazorUIClassLib** as a dependency of **WebApp1**.

* From **Solution Explorer**, right-click on **WebApp1** and select **Add** > **Reference**.

* In the **Reference Manager** dialog, check **RazorUIClassLib** > **OK**.

Run the app.

Create a Razor Pages web app and a solution file containing the Razor Pages app and the RCL:

```
dotnet new webapp -o WebApp1
dotnet new sln
dotnet sln add WebApp1
dotnet sln add RazorUIClassLib
dotnet add WebApp1 reference RazorUIClassLib
```

Build and run the web app:

```
cd WebApp1
dotnet run
```

### Test WebApp1

Browse to `/MyFeature/Page1` to verify that the Razor UI class library is in use.

## Override views, partial views, and pages

When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence. For example, add `WebApp1/Areas/MyFeature/Pages/Page1.cshtml` to WebApp1, and Page1 in the WebApp1 will take precedence over Page1 in the RCL.

In the sample download, rename `WebApp1/Areas/MyFeature2` to `WebApp1/Areas/MyFeature` to test precedence.

Copy the `RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml` partial view to `WebApp1/Areas/MyFeature/Pages/Shared/_Message.cshtml`. Update the markup to indicate the new location. Build and run the app to verify the app's version of the partial is being used.

### RCL Pages layout

To reference RCL content as though it is part of the web app's `Pages` folder, create the RCL project with the following file structure:

* `RazorUIClassLib/Pages`
* `RazorUIClassLib/Pages/Shared`

Suppose `RazorUIClassLib/Pages/Shared` contains two partial files: `_Header.cshtml` and `_Footer.cshtml`. The `<partial>` tags could be added to `_Layout.cshtml` file:

```
<body>
  <partial name="_Header">
  @RenderBody()
  <partial name="_Footer">
</body>
```

Razor views, pages, controllers, page models, [Razor components](../blazor/components/class-libraries?view=aspnetcore-10.0), [View components](../mvc/views/view-components?view=aspnetcore-10.0), and data models can be built into a Razor class library (RCL). The RCL can be packaged and reused. Applications can include the RCL and override the views and pages it contains. When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/ui-class/samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## Create a class library containing Razor UI

* [Visual Studio](#tabpanel_7_visual-studio)
* [.NET CLI](#tabpanel_7_net-cli)

- From Visual Studio's **Start Window**, select **Create a new project**. Alternatively, select the **File** > **New** > **Project/Solution**.
- Select **Razor Class Library** > **Next**.
- Name the library (for example, "RazorClassLib"), > **Create**. To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.
- Select **Support pages and views** if you need to support views. By default, only Razor Pages are supported. Select **Create**.

The Razor class library (RCL) template defaults to Razor component development by default. The **Support pages and views** option supports pages and views.

From the command line, run `dotnet new razorclasslib`. For example:

```
dotnet new razorclasslib -o RazorUIClassLib
```

The Razor class library (RCL) template defaults to Razor component development by default. Pass the `--support-pages-and-views` option (`dotnet new razorclasslib --support-pages-and-views`) to provide support for pages and views.

For more information, see [dotnet new](/en-us/dotnet/core/tools/dotnet-new). To avoid a file name collision with the generated view library, ensure the library name doesn't end in `.Views`.

Add Razor files to the RCL.

The ASP.NET Core templates assume the RCL content is in the `Areas` folder. See [RCL Pages layout](#rcl-lay) below to create an RCL that exposes content in `~/Pages` rather than `~/Areas/Pages`.

## Reference RCL content

The RCL can be referenced by:

* NuGet package. See [Creating NuGet packages](/en-us/nuget/create-packages/creating-a-package) and [dotnet add package](/en-us/dotnet/core/tools/dotnet-add-package) and [Create and publish a NuGet package](/en-us/nuget/quickstart/create-and-publish-a-package-using-visual-studio).
* `{ProjectName}.csproj`. See [dotnet-add reference](/en-us/dotnet/core/tools/dotnet-add-reference).

## Override views, partial views, and pages

When a view, partial view, or Razor Page is found in both the web app and the RCL, the Razor markup (`.cshtml` file) in the web app takes precedence. For example, add `WebApp1/Areas/MyFeature/Pages/Page1.cshtml` to WebApp1, and Page1 in the WebApp1 will take precedence over Page1 in the RCL.

In the sample download, rename `WebApp1/Areas/MyFeature2` to `WebApp1/Areas/MyFeature` to test precedence.

Copy the `RazorUIClassLib/Areas/MyFeature/Pages/Shared/_Message.cshtml` partial view to `WebApp1/Areas/MyFeature/Pages/Shared/_Message.cshtml`. Update the markup to indicate the new location. Build and run the app to verify the app's version of the partial is being used.

If the RCL uses Razor Pages, enable the Razor Pages services and endpoints in the hosting app:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
}

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");

        endpoints.MapRazorPages();
    });
}
```

[]()

### RCL Pages layout

To reference RCL content as though it is part of the web app's `Pages` folder, create the RCL project with the following file structure:

* `RazorUIClassLib/Pages`
* `RazorUIClassLib/Pages/Shared`

Suppose `RazorUIClassLib/Pages/Shared` contains two partial files: `_Header.cshtml` and `_Footer.cshtml`. The `<partial>` tags could be added to `_Layout.cshtml` file:

```
<body>
  <partial name="_Header">
  @RenderBody()
  <partial name="_Footer">
</body>
```

Add the `_ViewStart.cshtml` file to the RCL project's `Pages` folder to use the `_Layout.cshtml` file from the host web app:

```
@{
    Layout = "_Layout";
}
```

## Create an RCL with static assets

An RCL may require companion static assets that can be referenced by either the RCL or the consuming app of the RCL. ASP.NET Core allows creating RCLs that include static assets that are available to a consuming app.

To include companion assets as part of an RCL, create a `wwwroot` folder in the class library and include any required files in that folder.

When packing an RCL, all companion assets in the `wwwroot` folder are automatically included in the package.

Use the `dotnet pack` command rather than the NuGet.exe version `nuget pack`.

### Exclude static assets

To exclude static assets, add the desired exclusion path to the `$(DefaultItemExcludes)` property group in the project file. Separate entries with a semicolon (`;`).

In the following example, the `lib.css` stylesheet in the `wwwroot` folder isn't considered a static asset and isn't included in the published RCL:

```
<PropertyGroup>
  <DefaultItemExcludes>$(DefaultItemExcludes);wwwroot\lib.css</DefaultItemExcludes>
</PropertyGroup>
```

### Typescript integration

To include TypeScript files in an RCL:

1. Reference the [`Microsoft.TypeScript.MSBuild`](https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild) NuGet package in the project.

   Note

   For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

2. Place the TypeScript files (`.ts`) outside of the `wwwroot` folder. For example, place the files in a `Client` folder.

3. Configure the TypeScript build output for the `wwwroot` folder. Set the `TypescriptOutDir` property inside of a `PropertyGroup` in the project file:

   ```
   <TypescriptOutDir>wwwroot</TypescriptOutDir>
   ```

4. Include the TypeScript target as a dependency of the `ResolveCurrentProjectStaticWebAssets` target by adding the following target inside of a `PropertyGroup` in the project file:

   ```
   <ResolveCurrentProjectStaticWebAssetsInputsDependsOn>
     CompileTypeScript;
     $(ResolveCurrentProjectStaticWebAssetsInputs)
   </ResolveCurrentProjectStaticWebAssetsInputsDependsOn>
   ```

### Consume content from a referenced RCL

The files included in the `wwwroot` folder of the RCL are exposed to either the RCL or the consuming app under the prefix `_content/{PACKAGE ID}/`. For example, a library with an assembly name of `Razor.Class.Lib` and without a `<PackageId>` specified in its project file results in a path to static content at `_content/Razor.Class.Lib/`. When producing a NuGet package and the assembly name isn't the same as the package ID ([`<PackageId>`](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties) in the library's project file), use the package ID as specified in the project file for `{PACKAGE ID}`.

The consuming app references static assets provided by the library with `<script>`, `<style>`, `<img>`, and other HTML tags. The consuming app must have [static file support](../fundamentals/static-files?view=aspnetcore-10.0) enabled in `Startup.Configure`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseStaticFiles();

    ...
}
```

When running the consuming app from build output (`dotnet run`), static web assets are enabled by default in the `Development` environment. To support assets in other environments when running from build output, call `UseStaticWebAssets` on the host builder in `Program.cs`:

```
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStaticWebAssets();
                webBuilder.UseStartup<Startup>();
            });
}
```

Calling `UseStaticWebAssets` isn't required when running an app from published output (`dotnet publish`).

### Multi-project development flow

When the consuming app runs:

* The assets in the RCL stay in their original folders. The assets aren't moved to the consuming app.
* Any change within the RCL's `wwwroot` folder is reflected in the consuming app after the RCL is rebuilt and without rebuilding the consuming app.

When the RCL is built, a manifest is produced that describes the static web asset locations. The consuming app reads the manifest at runtime to consume the assets from referenced projects and packages. When a new asset is added to an RCL, the RCL must be rebuilt to update its manifest before a consuming app can access the new asset.

### Publish

When the app is published, the companion assets from all referenced projects and packages are copied into the `wwwroot` folder of the published app under `_content/{PACKAGE ID}/`. When producing a NuGet package and the assembly name isn't the same as the package ID ([`<PackageId>`](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties) in the library's project file), use the package ID as specified in the project file for `{PACKAGE ID}` when examining the `wwwroot` folder for the published assets.

## Additional resources

* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../blazor/components/class-libraries?view=aspnetcore-10.0)
* [ASP.NET Core Blazor CSS isolation](../blazor/components/css-isolation?view=aspnetcore-10.0#razor-class-library-rcl-support)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-ef-core
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/blazor-ef-core.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor with Entity Framework Core (EF Core)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to use [Entity Framework Core (EF Core)](/en-us/ef/core/) in server-side Blazor apps.

Server-side Blazor is a stateful app framework. The app maintains an ongoing connection to the server, and the user's state is held in the server's memory in a *circuit*. One example of user state is data held in [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0) service instances that are scoped to the circuit. The unique application model that Blazor provides requires a special approach to use Entity Framework Core.

Note

This article addresses EF Core in server-side Blazor apps. Blazor WebAssembly apps run in a WebAssembly sandbox that prevents most direct database connections. Running EF Core in Blazor WebAssembly is beyond the scope of this article.

This guidance applies to components that adopt interactive server-side rendering (interactive SSR) in a Blazor Web App.

This guidance applies to the **`Server`** project of a hosted Blazor WebAssembly solution or a Blazor Server app.

## Secure authentication flow required for production apps

This article pertains to the use of a local database that doesn't require user authentication. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production Blazor apps, see the articles in the [Blazor *Security and Identity* node](security/?view=aspnetcore-10.0).

For Microsoft Azure services, we recommend using *managed identities*. Managed identities securely authenticate to Azure services without storing credentials in app code. For more information, see the following resources:

* [What are managed identities for Azure resources? (Microsoft Entra documentation)](/en-us/entra/identity/managed-identities-azure-resources/overview)

* Azure services documentation

  * [Managed identities in Microsoft Entra for Azure SQL](/en-us/azure/azure-sql/database/authentication-azure-ad-user-assigned-managed-identity)
  * [How to use managed identities for App Service and Azure Functions](/en-us/azure/app-service/overview-managed-identity)

## Build a Blazor movie database app tutorial

For a tutorial experience building an app that uses EF Core for database operations, see [Build a Blazor movie database app (Overview)](tutorials/movie-database-app/?view=aspnetcore-10.0). The tutorial shows you how to create a Blazor Web App that can display and manage movies in a movie database.

## Database access

EF Core relies on a [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) as the means to [configure database access](/en-us/ef/core/miscellaneous/configuring-dbcontext) and act as a [*unit of work*](https://martinfowler.com/eaaCatalog/unitOfWork.html). EF Core provides the [AddDbContext](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkservicecollectionextensions.adddbcontext) extension for ASP.NET Core apps that registers the context as a *scoped* service. In server-side Blazor apps, scoped service registrations can be problematic because the instance is shared across components within the user's circuit. [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) isn't thread safe and isn't designed for concurrent use. The existing lifetimes are inappropriate for these reasons:

* **Singleton** shares state across all users of the app and leads to inappropriate concurrent use.
* **Scoped** (the default) poses a similar issue between components for the same user.
* **Transient** results in a new instance per request; but as components can be long-lived, this results in a longer-lived context than may be intended.

The following recommendations are designed to provide a consistent approach to using EF Core in server-side Blazor apps.

* Consider using one context per operation. The context is designed for fast, low overhead instantiation:

  ```
  using var context = new ProductsDatabaseContext();

  return await context.Products.ToListAsync();
  ```

* Use a flag to prevent multiple concurrent operations:

  ```
  if (Loading)
  {
      return;
  }

  try
  {
      Loading = true;

      ...
  }
  finally
  {
      Loading = false;
  }
  ```

  Place database operations after the `Loading = true;` line in the `try` block.

  Thread safety isn't a concern, so loading logic doesn't require locking database records. The loading logic is used to disable UI controls so that users don't inadvertently select buttons or update fields while data is fetched.

* If there's any chance that multiple threads may access the same code block, [inject a factory](#scope-a-database-context-to-the-lifetime-of-the-component) and make a new instance per operation. Otherwise, injecting and using the context is usually sufficient.

* For longer-lived operations that take advantage of EF Core's [change tracking](/en-us/ef/core/querying/tracking) or [concurrency control](/en-us/ef/core/saving/concurrency), [scope the context to the lifetime of the component](#scope-a-database-context-to-the-lifetime-of-the-component).

## New `DbContext` instances

The fastest way to create a new [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) instance is by using `new` to create a new instance. However, there are scenarios that require resolving additional dependencies:

* Using [`DbContextOptions`](/en-us/ef/core/miscellaneous/configuring-dbcontext#configuring-dbcontextoptions) to configure the context.
* Using a connection string per [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext), such as when you use [ASP.NET Core's Identity model](../security/authentication/customize-identity-model?view=aspnetcore-10.0). For more information, see [Multi-tenancy (EF Core documentation)](/en-us/ef/core/miscellaneous/multitenancy).

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](security/?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

The recommended approach to create a new [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) with dependencies is to use a factory. EF Core in .NET 5 or later provides a built-in factory for creating new contexts.

In versions of .NET prior to .NET 5, use the following `DbContextFactory`:

```
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorServerDbContextExample.Data
{
    public class DbContextFactory<TContext> 
        : IDbContextFactory<TContext> where TContext : DbContext
    {
        private readonly IServiceProvider provider;

        public DbContextFactory(IServiceProvider provider)
        {
            this.provider = provider ?? throw new ArgumentNullException(
                $"{nameof(provider)}: You must configure an instance of " +
                "IServiceProvider");
        }

        public TContext CreateDbContext() => 
            ActivatorUtilities.CreateInstance<TContext>(provider);
    }
}
```

In the preceding factory:

* [ActivatorUtilities.CreateInstance](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.activatorutilities.createinstance) satisfies any dependencies via the service provider.
* [IDbContextFactory\<TContext>](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1) is available in EF Core in .NET 5 or later, so the preceding interface is only required for ASP.NET Core 3.x.

The following example configures [SQLite](https://www.sqlite.org/index.html) and enables data logging in an app that manages contacts. The code uses an extension method ([AddDbContextFactory](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkservicecollectionextensions.adddbcontextfactory)) to configure the database factory for DI and provide default options:

```
builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
```

```
services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
```

The factory is injected into components to create new [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) instances.

## Scope a database context to a component method

The factory is injected into the component:

```
@inject IDbContextFactory<ContactContext> DbFactory
```

Create a [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) for a method using the factory (`DbFactory`):

```
private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    if (context.Contacts is not null)
    {
        var contact = await context.Contacts.FirstAsync(...);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }
}
```

## Scope a database context to the lifetime of the component

You may wish to create a [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) that exists for the lifetime of a component. This allows you to use it as a [unit of work](https://martinfowler.com/eaaCatalog/unitOfWork.html) and take advantage of built-in features, such as change tracking and concurrency resolution.

Implement [IDisposable](/en-us/dotnet/api/system.idisposable) and inject the factory into the component:

```
@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory
```

Establish a property for the [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext):

```
private ContactContext? Context { get; set; }
```

[`OnInitializedAsync`](components/lifecycle?view=aspnetcore-10.0) is overridden to create the [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext):

```
protected override async Task OnInitializedAsync()
{
    Context = DbFactory.CreateDbContext();
}
```

The [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) is disposed when the component is disposed:

```
public void Dispose() => Context?.Dispose();
```

## Enable sensitive data logging

[EnableSensitiveDataLogging](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.enablesensitivedatalogging) includes application data in exception messages and framework logging. The logged data can include the values assigned to properties of entity instances and parameter values for commands sent to the database. Logging data with [EnableSensitiveDataLogging](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.enablesensitivedatalogging) is a **security risk**, as it may expose passwords and other [Personally Identifiable Information (PII)](security/?view=aspnetcore-10.0#personally-identifiable-information-pii) when it logs SQL statements executed against the database.

We recommend only enabling [EnableSensitiveDataLogging](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.enablesensitivedatalogging) for local development and testing:

```
if (builder.Environment.IsDevelopment())
{
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
        .EnableSensitiveDataLogging());
}
else
{
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
}
```

## Related content

* [EF Core documentation](/en-us/ef/)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples)
* [Blazor *Security and Identity* node articles](security/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/da1
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/da1.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 5, update the generated pages in an ASP.NET Core app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The scaffolded movie app is a good start, but the presentation isn't ideal. **ReleaseDate** should be two words, **Release Date**.

## Update the model

Update `Models/Movie.cs` with the following highlighted code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; } = string.Empty;

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

In the previous code:

* The `[Column(TypeName = "decimal(18, 2)")]` data annotation enables Entity Framework Core to correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).
* The [\[Display\]](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies the display name of a field. In the preceding code, `Release Date` instead of `ReleaseDate`.
* The [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). The time information stored in the field isn't displayed.

[DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) is covered in the next tutorial.

Browse to *Pages/MoviePages* and hover over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) in the `Pages/MoviePages/Index.cshtml` file.

```
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files.

In the preceding code, the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) dynamically generates the HTML `href` attribute value from the Razor Page (the route is relative), the `asp-page`, and the route identifier (`asp-route-id`). For more information, see [URL generation for Pages](../../razor-pages/?view=aspnetcore-10.0#url-generation-for-pages).

Use **View Source** from a browser to examine the generated markup. A portion of the generated HTML is shown below:

```
<td>
  <a href="/MoviePages/Edit?id=1">Edit</a> |
  <a href="/MoviePages/Details?id=1">Details</a> |
  <a href="/MoviePages/Delete?id=1">Delete</a>
</td>
```

The dynamically generated links pass the movie ID with a [query string](https://launchschool.com/books/http/read/what_is_a_url). For example, the `?id=1` in `https://localhost:7247/MoviePages/Details?id=1`.

### Add route template

Update the Edit, Details, and Delete Razor Pages to use the `{id:int}` route template. Change the page directive for each of these pages from `@page` to `@page "{id:int}"`. Run the app and then view source.

The generated HTML adds the ID to the path portion of the URL:

```
<td>
  <a href="/MoviePages/Edit/1">Edit</a> |
  <a href="/MoviePages/Details/1">Details</a> |
  <a href="/MoviePages/Delete/1">Delete</a>
</td>
```

A request to the page with the `{id:int}` route template that doesn't include the integer returns an HTTP 404 (not found) error. For example, `https://localhost:7247/MoviePages/Details` returns a 404 error. To make the ID optional, append `?` to the route constraint:

```
@page "{id:int?}"
```

Test the behavior of `@page "{id:int?}"`:

1. Set the page directive in `Pages/MoviePages/Details.cshtml` to `@page "{id:int?}"`.
2. Set a break point in `public async Task<IActionResult> OnGetAsync(int? id)`, in `Pages/MoviePages/Details.cshtml.cs`.
3. Navigate to `https://localhost:7247/MoviePages/Details/`.

With the `@page "{id:int}"` directive, the break point is never hit. The routing engine returns HTTP 404. Using `@page "{id:int?}"`, the `OnGetAsync` method returns `NotFound` (HTTP 404):

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }
    else
    {
        Movie = movie;
    }
    return Page();
}
```

### Review concurrency exception handling

Review the `OnPostAsync` method in the `Pages/MoviePages/Edit.cshtml.cs` file:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.Id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
    return _context.Movie.Any(e => e.Id == id);
}
```

The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.

To test the `catch` block:

1. Set a breakpoint on `catch (DbUpdateConcurrencyException)`.
2. Select **Edit** for a movie, make changes, but don't select **Save**.
3. In another browser window, select the **Delete** link for the same movie, and then delete the movie.
4. In the previous browser window, post changes to the movie.

Production code might want to detect concurrency conflicts. For more information, see [Handle concurrency conflicts](../../data/ef-rp/concurrency?view=aspnetcore-10.0).

### Posting and binding review

Examine the `Pages/MoviePages/Edit.cshtml.cs` file:

```
public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
        return _context.Movie.Any(e => e.Id == id);
    }
```

When you make an HTTP GET request to the `MoviePages/Edit` page, for example, `https://localhost:7247/MoviePages/Edit/3`:

* The `OnGetAsync` method fetches the movie from the database and returns the `Page` method.
* The `Page` method renders the `Pages/MoviePages/Edit.cshtml` Razor Page. The `Pages/MoviePages/Edit.cshtml` file contains the model directive `@model RazorPagesMovie.Pages.MoviePages.EditModel`, which makes the movie model available on the page.
* The Edit form displays the values from the movie.

When you post the `MoviePages/Edit` page:

* The form values on the page bind to the `Movie` property. The `[BindProperty]` attribute enables [Model binding](../../mvc/models/model-binding?view=aspnetcore-10.0).

  ```
  [BindProperty]
  public Movie Movie { get; set; }
  ```

* If there are errors in the model state, for example, `ReleaseDate` can't convert to a date, the form redisplays with the submitted values.

* If there are no model errors, the movie is saved.

The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST `OnPostAsync` method in the Create Razor Page follows a similar pattern to the `OnPostAsync` method in the Edit Razor Page.

## Next steps

[Previous: Work with a database](sql?view=aspnetcore-10.0) [Next: Add search](search?view=aspnetcore-10.0)

The scaffolded movie app has a good start, but the presentation isn't ideal. **ReleaseDate** should be two words, **Release Date**.

## Update the model

Update `Models/Movie.cs` with the following highlighted code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; } = string.Empty;

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

In the previous code:

* The `[Column(TypeName = "decimal(18, 2)")]` data annotation enables Entity Framework Core to correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).
* The [\[Display\]](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies the display name of a field. In the preceding code, `Release Date` instead of `ReleaseDate`.
* The [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). The time information stored in the field isn't displayed.

[DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) is covered in the next tutorial.

Browse to *Pages/Movies* and hover over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) in the `Pages/Movies/Index.cshtml` file.

```
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files.

In the preceding code, the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) dynamically generates the HTML `href` attribute value from the Razor Page (the route is relative), the `asp-page`, and the route identifier (`asp-route-id`). For more information, see [URL generation for Pages](../../razor-pages/?view=aspnetcore-10.0#url-generation-for-pages).

Use **View Source** from a browser to examine the generated markup. A portion of the generated HTML is shown below:

```
<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>
```

The dynamically generated links pass the movie ID with a [query string](https://launchschool.com/books/http/read/what_is_a_url). For example, the `?id=1` in `https://localhost:5001/Movies/Details?id=1`.

### Add route template

Update the Edit, Details, and Delete Razor Pages to use the `{id:int}` route template. Change the page directive for each of these pages from `@page` to `@page "{id:int}"`. Run the app and then view source.

The generated HTML adds the ID to the path portion of the URL:

```
<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>
```

A request to the page with the `{id:int}` route template that does **not** include the integer returns an HTTP 404 (not found) error. For example, `https://localhost:5001/Movies/Details` returns a 404 error. To make the ID optional, append `?` to the route constraint:

```
@page "{id:int?}"
```

Test the behavior of `@page "{id:int?}"`:

1. Set the page directive in `Pages/Movies/Details.cshtml` to `@page "{id:int?}"`.
2. Set a break point in `public async Task<IActionResult> OnGetAsync(int? id)`, in `Pages/Movies/Details.cshtml.cs`.
3. Navigate to `https://localhost:5001/Movies/Details/`.

With the `@page "{id:int}"` directive, the break point is never hit. The routing engine returns HTTP 404. Using `@page "{id:int?}"`, the `OnGetAsync` method returns `NotFound` (HTTP 404):

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }
    else
    {
        Movie = movie;
    }
    return Page();
}
```

### Review concurrency exception handling

Review the `OnPostAsync` method in the `Pages/Movies/Edit.cshtml.cs` file:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.Id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
    return _context.Movie.Any(e => e.Id == id);
}
```

The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.

To test the `catch` block:

1. Set a breakpoint on `catch (DbUpdateConcurrencyException)`.
2. Select **Edit** for a movie, make changes, but don't enter **Save**.
3. In another browser window, select the **Delete** link for the same movie, and then delete the movie.
4. In the previous browser window, post changes to the movie.

Production code may want to detect concurrency conflicts. See [Handle concurrency conflicts](../../data/ef-rp/concurrency?view=aspnetcore-10.0) for more information.

### Posting and binding review

Examine the `Pages/Movies/Edit.cshtml.cs` file:

```
public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
        return _context.Movie.Any(e => e.Id == id);
    }
```

When an HTTP GET request is made to the Movies/Edit page, for example, `https://localhost:5001/Movies/Edit/3`:

* The `OnGetAsync` method fetches the movie from the database and returns the `Page` method.
* The `Page` method renders the `Pages/Movies/Edit.cshtml` Razor Page. The `Pages/Movies/Edit.cshtml` file contains the model directive `@model RazorPagesMovie.Pages.Movies.EditModel`, which makes the movie model available on the page.
* The Edit form is displayed with the values from the movie.

When the Movies/Edit page is posted:

* The form values on the page are bound to the `Movie` property. The `[BindProperty]` attribute enables [Model binding](../../mvc/models/model-binding?view=aspnetcore-10.0).

  ```
  [BindProperty]
  public Movie Movie { get; set; }
  ```

* If there are errors in the model state, for example, `ReleaseDate` cannot be converted to a date, the form is redisplayed with the submitted values.

* If there are no model errors, the movie is saved.

The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST `OnPostAsync` method in the Create Razor Page follows a similar pattern to the `OnPostAsync` method in the Edit Razor Page.

## Next steps

[Previous: Work with a database](sql?view=aspnetcore-10.0) [Next: Add search](search?view=aspnetcore-10.0)

The scaffolded movie app has a good start, but the presentation isn't ideal. **ReleaseDate** should be two words, **Release Date**.

## Update the model

Update `Models/Movie.cs` with the following highlighted code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; } = string.Empty;

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

In the previous code:

* The `[Column(TypeName = "decimal(18, 2)")]` data annotation enables Entity Framework Core to correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).
* The [\[Display\]](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies the display name of a field. In the preceding code, `Release Date` instead of `ReleaseDate`.
* The [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). The time information stored in the field isn't displayed.

[DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) is covered in the next tutorial.

Browse to *Pages/Movies* and hover over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) in the `Pages/Movies/Index.cshtml` file.

```
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files.

In the preceding code, the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) dynamically generates the HTML `href` attribute value from the Razor Page (the route is relative), the `asp-page`, and the route identifier (`asp-route-id`). For more information, see [URL generation for Pages](../../razor-pages/?view=aspnetcore-10.0#url-generation-for-pages).

Use **View Source** from a browser to examine the generated markup. A portion of the generated HTML is shown below:

```
<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>
```

The dynamically generated links pass the movie ID with a [query string](https://launchschool.com/books/http/read/what_is_a_url). For example, the `?id=1` in `https://localhost:5001/Movies/Details?id=1`.

### Add route template

Update the Edit, Details, and Delete Razor Pages to use the `{id:int}` route template. Change the page directive for each of these pages from `@page` to `@page "{id:int}"`. Run the app and then view source.

The generated HTML adds the ID to the path portion of the URL:

```
<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>
```

A request to the page with the `{id:int}` route template that does **not** include the integer returns an HTTP 404 (not found) error. For example, `https://localhost:5001/Movies/Details` returns a 404 error. To make the ID optional, append `?` to the route constraint:

```
@page "{id:int?}"
```

Test the behavior of `@page "{id:int?}"`:

1. Set the page directive in `Pages/Movies/Details.cshtml` to `@page "{id:int?}"`.
2. Set a break point in `public async Task<IActionResult> OnGetAsync(int? id)`, in `Pages/Movies/Details.cshtml.cs`.
3. Navigate to `https://localhost:5001/Movies/Details/`.

With the `@page "{id:int}"` directive, the break point is never hit. The routing engine returns HTTP 404. Using `@page "{id:int?}"`, the `OnGetAsync` method returns `NotFound` (HTTP 404):

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}
```

### Review concurrency exception handling

Review the `OnPostAsync` method in the `Pages/Movies/Edit.cshtml.cs` file:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.Id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
  return _context.Movie.Any(e => e.Id == id);
}
```

The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.

To test the `catch` block:

1. Set a breakpoint on `catch (DbUpdateConcurrencyException)`.
2. Select **Edit** for a movie, make changes, but don't enter **Save**.
3. In another browser window, select the **Delete** link for the same movie, and then delete the movie.
4. In the previous browser window, post changes to the movie.

Production code may want to detect concurrency conflicts. See [Handle concurrency conflicts](../../data/ef-rp/concurrency?view=aspnetcore-10.0) for more information.

### Posting and binding review

Examine the `Pages/Movies/Edit.cshtml.cs` file:

```
public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null || _context.Movie == null)
        {
            return NotFound();
        }

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
      return _context.Movie.Any(e => e.Id == id);
    }
```

When an HTTP GET request is made to the Movies/Edit page, for example, `https://localhost:5001/Movies/Edit/3`:

* The `OnGetAsync` method fetches the movie from the database and returns the `Page` method.
* The `Page` method renders the `Pages/Movies/Edit.cshtml` Razor Page. The `Pages/Movies/Edit.cshtml` file contains the model directive `@model RazorPagesMovie.Pages.Movies.EditModel`, which makes the movie model available on the page.
* The Edit form is displayed with the values from the movie.

When the Movies/Edit page is posted:

* The form values on the page are bound to the `Movie` property. The `[BindProperty]` attribute enables [Model binding](../../mvc/models/model-binding?view=aspnetcore-10.0).

  ```
  [BindProperty]
  public Movie Movie { get; set; }
  ```

* If there are errors in the model state, for example, `ReleaseDate` cannot be converted to a date, the form is redisplayed with the submitted values.

* If there are no model errors, the movie is saved.

The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST `OnPostAsync` method in the Create Razor Page follows a similar pattern to the `OnPostAsync` method in the Edit Razor Page.

## Next steps

[Previous: Work with a database](sql?view=aspnetcore-10.0) [Next: Add search](search?view=aspnetcore-10.0)

The scaffolded movie app has a good start, but the presentation isn't ideal. **ReleaseDate** should be two words, **Release Date**.

## Update the model

Update `Models/Movie.cs` with the following highlighted code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; } = string.Empty;

    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
}
```

In the previous code:

* The `[Column(TypeName = "decimal(18, 2)")]` data annotation enables Entity Framework Core to correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).
* The [\[Display\]](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies the display name of a field. In the preceding code, `Release Date` instead of `ReleaseDate`.
* The [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). The time information stored in the field isn't displayed.

[DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) is covered in the next tutorial.

Browse to *Pages/Movies* and hover over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) in the `Pages/Movies/Index.cshtml` file.

```
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files.

In the preceding code, the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) dynamically generates the HTML `href` attribute value from the Razor Page (the route is relative), the `asp-page`, and the route identifier (`asp-route-id`). For more information, see [URL generation for Pages](../../razor-pages/?view=aspnetcore-10.0#url-generation-for-pages).

Use **View Source** from a browser to examine the generated markup. A portion of the generated HTML is shown below:

```
<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>
```

The dynamically generated links pass the movie ID with a [query string](https://launchschool.com/books/http/read/what_is_a_url). For example, the `?id=1` in `https://localhost:5001/Movies/Details?id=1`.

### Add route template

Update the Edit, Details, and Delete Razor Pages to use the `{id:int}` route template. Change the page directive for each of these pages from `@page` to `@page "{id:int}"`. Run the app and then view source.

The generated HTML adds the ID to the path portion of the URL:

```
<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>
```

A request to the page with the `{id:int}` route template that does **not** include the integer returns an HTTP 404 (not found) error. For example, `https://localhost:5001/Movies/Details` returns a 404 error. To make the ID optional, append `?` to the route constraint:

```
@page "{id:int?}"
```

Test the behavior of `@page "{id:int?}"`:

1. Set the page directive in `Pages/Movies/Details.cshtml` to `@page "{id:int?}"`.
2. Set a break point in `public async Task<IActionResult> OnGetAsync(int? id)`, in `Pages/Movies/Details.cshtml.cs`.
3. Navigate to `https://localhost:5001/Movies/Details/`.

With the `@page "{id:int}"` directive, the break point is never hit. The routing engine returns HTTP 404. Using `@page "{id:int?}"`, the `OnGetAsync` method returns `NotFound` (HTTP 404):

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}
```

### Review concurrency exception handling

Review the `OnPostAsync` method in the `Pages/Movies/Edit.cshtml.cs` file:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.Id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
  return _context.Movie.Any(e => e.Id == id);
}
```

The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.

To test the `catch` block:

1. Set a breakpoint on `catch (DbUpdateConcurrencyException)`.
2. Select **Edit** for a movie, make changes, but don't enter **Save**.
3. In another browser window, select the **Delete** link for the same movie, and then delete the movie.
4. In the previous browser window, post changes to the movie.

Production code may want to detect concurrency conflicts. See [Handle concurrency conflicts](../../data/ef-rp/concurrency?view=aspnetcore-10.0) for more information.

### Posting and binding review

Examine the `Pages/Movies/Edit.cshtml.cs` file:

```
public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null || _context.Movie == null)
        {
            return NotFound();
        }

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.Id == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
      return _context.Movie.Any(e => e.Id == id);
    }
```

When an HTTP GET request is made to the Movies/Edit page, for example, `https://localhost:5001/Movies/Edit/3`:

* The `OnGetAsync` method fetches the movie from the database and returns the `Page` method.
* The `Page` method renders the `Pages/Movies/Edit.cshtml` Razor Page. The `Pages/Movies/Edit.cshtml` file contains the model directive `@model RazorPagesMovie.Pages.Movies.EditModel`, which makes the movie model available on the page.
* The Edit form is displayed with the values from the movie.

When the Movies/Edit page is posted:

* The form values on the page are bound to the `Movie` property. The `[BindProperty]` attribute enables [Model binding](../../mvc/models/model-binding?view=aspnetcore-10.0).

  ```
  [BindProperty]
  public Movie Movie { get; set; }
  ```

* If there are errors in the model state, for example, `ReleaseDate` cannot be converted to a date, the form is redisplayed with the submitted values.

* If there are no model errors, the movie is saved.

The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST `OnPostAsync` method in the Create Razor Page follows a similar pattern to the `OnPostAsync` method in the Edit Razor Page.

## Next steps

[Previous: Work with a database](sql?view=aspnetcore-10.0) [Next: Add search](search?view=aspnetcore-10.0)

The scaffolded movie app has a good start, but the presentation isn't ideal. **ReleaseDate** should be two words, **Release Date**.

## Update the generated code

Update `Models/Movie.cs` with the following highlighted code:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; } = string.Empty;

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; } = string.Empty;

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}
```

In the previous code:

* The `[Column(TypeName = "decimal(18, 2)")]` data annotation enables Entity Framework Core to correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).
* The [\[Display\]](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies the display name of a field. In the preceding code, "Release Date" instead of "ReleaseDate".
* The [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). The time information stored in the field isn't displayed.

[DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) is covered in the next tutorial.

Browse to *Pages/Movies* and hover over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) in the `Pages/Movies/Index.cshtml` file.

```
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files.

In the preceding code, the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) dynamically generates the HTML `href` attribute value from the Razor Page (the route is relative), the `asp-page`, and the route identifier (`asp-route-id`). For more information, see [URL generation for Pages](../../razor-pages/?view=aspnetcore-10.0#url-generation-for-pages).

Use **View Source** from a browser to examine the generated markup. A portion of the generated HTML is shown below:

```
<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>
```

The dynamically generated links pass the movie ID with a query string. For example, the `?id=1` in `https://localhost:5001/Movies/Details?id=1`.

### Add route template

Update the Edit, Details, and Delete Razor Pages to use the `{id:int}` route template. Change the page directive for each of these pages from `@page` to `@page "{id:int}"`. Run the app and then view source.

The generated HTML adds the ID to the path portion of the URL:

```
<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>
```

A request to the page with the `{id:int}` route template that does **not** include the integer will return an HTTP 404 (not found) error. For example, `https://localhost:5001/Movies/Details` will return a 404 error. To make the ID optional, append `?` to the route constraint:

```
@page "{id:int?}"
```

Test the behavior of `@page "{id:int?}"`:

1. Set the page directive in `Pages/Movies/Details.cshtml` to `@page "{id:int?}"`.
2. Set a break point in `public async Task<IActionResult> OnGetAsync(int? id)`, in `Pages/Movies/Details.cshtml.cs`.
3. Navigate to `https://localhost:5001/Movies/Details/`.

With the `@page "{id:int}"` directive, the break point is never hit. The routing engine returns HTTP 404. Using `@page "{id:int?}"`, the `OnGetAsync` method returns `NotFound` (HTTP 404):

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}
```

### Review concurrency exception handling

Review the `OnPostAsync` method in the `Pages/Movies/Edit.cshtml.cs` file:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.ID))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
  return (_context.Movie?.Any(e => e.ID == id)).GetValueOrDefault();
}
```

The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie. The previous code does **not** detect conflicts that occur because of two or more clients editing the same movie concurrently. In this case edits by multiple clients are applied in the order that `SaveChanges` is called and edits that are applied later may overwrite earlier edits with stale values.

To test the `catch` block:

1. Set a breakpoint on `catch (DbUpdateConcurrencyException)`.
2. Select **Edit** for a movie, make changes, but don't enter **Save**.
3. In another browser window, select the **Delete** link for the same movie, and then delete the movie.
4. In the previous browser window, post changes to the movie.

Production code may want to detect additional concurrency conflicts such as multiple clients editing an entity at the same time. See [Handle concurrency conflicts](../../data/ef-rp/concurrency?view=aspnetcore-10.0) for more information.

### Posting and binding review

Examine the `Pages/Movies/Edit.cshtml.cs` file:

```
public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null || _context.Movie == null)
        {
            return NotFound();
        }

        var movie =  await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);
        if (movie == null)
        {
            return NotFound();
        }
        Movie = movie;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
      return (_context.Movie?.Any(e => e.ID == id)).GetValueOrDefault();
    }
```

When an HTTP GET request is made to the Movies/Edit page, for example, `https://localhost:5001/Movies/Edit/3`:

* The `OnGetAsync` method fetches the movie from the database and returns the `Page` method.
* The `Page` method renders the `Pages/Movies/Edit.cshtml` Razor Page. The `Pages/Movies/Edit.cshtml` file contains the model directive `@model RazorPagesMovie.Pages.Movies.EditModel`, which makes the movie model available on the page.
* The Edit form is displayed with the values from the movie.

When the Movies/Edit page is posted:

* The form values on the page are bound to the `Movie` property. The `[BindProperty]` attribute enables [Model binding](../../mvc/models/model-binding?view=aspnetcore-10.0).

  ```
  [BindProperty]
  public Movie Movie { get; set; }
  ```

* If there are errors in the model state, for example, `ReleaseDate` cannot be converted to a date, the form is redisplayed with the submitted values.

* If there are no model errors, the movie is saved.

The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST `OnPostAsync` method in the Create Razor Page follows a similar pattern to the `OnPostAsync` method in the Edit Razor Page.

## Next steps

[Previous: Work with a database](sql?view=aspnetcore-10.0) [Next: Add search](search?view=aspnetcore-10.0)

The scaffolded movie app has a good start, but the presentation isn't ideal. **ReleaseDate** should be two words, **Release Date**.

## Update the generated code

Open the `Models/Movie.cs` file and add the highlighted lines shown in the following code:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }

        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
    }
}
```

In the previous code:

* The `[Column(TypeName = "decimal(18, 2)")]` data annotation enables Entity Framework Core to correctly map `Price` to currency in the database. For more information, see [Data Types](/en-us/ef/core/modeling/relational/data-types).
* The [\[Display\]](/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) attribute specifies the display name of a field. In the preceding code, "Release Date" instead of "ReleaseDate".
* The [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). The time information stored in the field isn't displayed.

[DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) is covered in the next tutorial.

Browse to *Pages/Movies* and hover over an **Edit** link to see the target URL.

The **Edit**, **Details**, and **Delete** links are generated by the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) in the `Pages/Movies/Index.cshtml` file.

```
@foreach (var item in Model.Movie) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>
```

[Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files.

In the preceding code, the [Anchor Tag Helper](../../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) dynamically generates the HTML `href` attribute value from the Razor Page (the route is relative), the `asp-page`, and the route identifier (`asp-route-id`). For more information, see [URL generation for Pages](../../razor-pages/?view=aspnetcore-10.0#url-generation-for-pages).

Use **View Source** from a browser to examine the generated markup. A portion of the generated HTML is shown below:

```
<td>
  <a href="/Movies/Edit?id=1">Edit</a> |
  <a href="/Movies/Details?id=1">Details</a> |
  <a href="/Movies/Delete?id=1">Delete</a>
</td>
```

The dynamically generated links pass the movie ID with a query string. For example, the `?id=1` in `https://localhost:5001/Movies/Details?id=1`.

### Add route template

Update the Edit, Details, and Delete Razor Pages to use the `{id:int}` route template. Change the page directive for each of these pages from `@page` to `@page "{id:int}"`. Run the app and then view source.

The generated HTML adds the ID to the path portion of the URL:

```
<td>
  <a href="/Movies/Edit/1">Edit</a> |
  <a href="/Movies/Details/1">Details</a> |
  <a href="/Movies/Delete/1">Delete</a>
</td>
```

A request to the page with the `{id:int}` route template that does **not** include the integer will return an HTTP 404 (not found) error. For example, `https://localhost:5001/Movies/Details` will return a 404 error. To make the ID optional, append `?` to the route constraint:

```
@page "{id:int?}"
```

Test the behavior of `@page "{id:int?}"`:

1. Set the page directive in `Pages/Movies/Details.cshtml` to `@page "{id:int?}"`.
2. Set a break point in `public async Task<IActionResult> OnGetAsync(int? id)`, in `Pages/Movies/Details.cshtml.cs`.
3. Navigate to `https://localhost:5001/Movies/Details/`.

With the `@page "{id:int}"` directive, the break point is never hit. The routing engine returns HTTP 404. Using `@page "{id:int?}"`, the `OnGetAsync` method returns `NotFound` (HTTP 404):

```
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}
```

### Review concurrency exception handling

Review the `OnPostAsync` method in the `Pages/Movies/Edit.cshtml.cs` file:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Movie).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!MovieExists(Movie.ID))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

private bool MovieExists(int id)
{
    return _context.Movie.Any(e => e.ID == id);
}
```

The previous code detects concurrency exceptions when one client deletes the movie and the other client posts changes to the movie.

To test the `catch` block:

1. Set a breakpoint on `catch (DbUpdateConcurrencyException)`.
2. Select **Edit** for a movie, make changes, but don't enter **Save**.
3. In another browser window, select the **Delete** link for the same movie, and then delete the movie.
4. In the previous browser window, post changes to the movie.

Production code may want to detect concurrency conflicts. See [Handle concurrency conflicts](../../data/ef-rp/concurrency?view=aspnetcore-10.0) for more information.

### Posting and binding review

Examine the `Pages/Movies/Edit.cshtml.cs` file:

```
public class EditModel : PageModel
{
    private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;

    public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Movie Movie { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);

        if (Movie == null)
        {
            return NotFound();
        }
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool MovieExists(int id)
    {
        return _context.Movie.Any(e => e.ID == id);
    }
```

When an HTTP GET request is made to the Movies/Edit page, for example, `https://localhost:5001/Movies/Edit/3`:

* The `OnGetAsync` method fetches the movie from the database and returns the `Page` method.
* The `Page` method renders the `Pages/Movies/Edit.cshtml` Razor Page. The `Pages/Movies/Edit.cshtml` file contains the model directive `@model RazorPagesMovie.Pages.Movies.EditModel`, which makes the movie model available on the page.
* The Edit form is displayed with the values from the movie.

When the Movies/Edit page is posted:

* The form values on the page are bound to the `Movie` property. The `[BindProperty]` attribute enables [Model binding](../../mvc/models/model-binding?view=aspnetcore-10.0).

  ```
  [BindProperty]
  public Movie Movie { get; set; }
  ```

* If there are errors in the model state, for example, `ReleaseDate` cannot be converted to a date, the form is redisplayed with the submitted values.

* If there are no model errors, the movie is saved.

The HTTP GET methods in the Index, Create, and Delete Razor pages follow a similar pattern. The HTTP POST `OnPostAsync` method in the Create Razor Page follows a similar pattern to the `OnPostAsync` method in the Edit Razor Page.

## Next steps

[Previous: Work with a database](sql?view=aspnetcore-10.0) [Next: Add search](search?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/additional-scenarios.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor WebAssembly additional security scenarios

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes additional security scenarios for Blazor WebAssembly apps.

## Attach tokens to outgoing requests

[AuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler) is a [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) used to process access tokens. Tokens are acquired using the [IAccessTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.iaccesstokenprovider) service, which is registered by the framework. If a token can't be acquired, an [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) is thrown. [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) has a [Redirect](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception.redirect) method that navigates to [AccessTokenResult.InteractiveRequestUrl](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenresult.interactiverequesturl) using the given [AccessTokenResult.InteractionOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenresult.interactionoptions) to allow refreshing the access token.

For convenience, the framework provides the [BaseAddressAuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.baseaddressauthorizationmessagehandler) preconfigured with the app's base address as an authorized URL. **Access tokens are only added when the request URI is within the app's base URI.** When outgoing request URIs aren't within the app's base URI, use a [custom `AuthorizationMessageHandler` class (*recommended*)](#custom-authorizationmessagehandler-class) or [configure the `AuthorizationMessageHandler`](#configure-authorizationmessagehandler).

Note

In addition to the client app configuration for server API access, the server API must also allow cross-origin requests (CORS) when the client and the server don't reside at the same base address. For more information on server-side CORS configuration, see the [Cross-Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) section later in this article.

In the following example:

* [AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) adds [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) and related services to the service collection and configures a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) (`WebAPI`). [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress) is the base address of the resource URI when sending requests. [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) is provided by the [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http) NuGet package.

* [BaseAddressAuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.baseaddressauthorizationmessagehandler) is the [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) used to process access tokens. Access tokens are only added when the request URI is within the app's base URI. For more information on [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) instances, see [HTTP requests with IHttpClientFactory - ASP.NET Core](../../../fundamentals/http-requests?view=aspnetcore-10.0#outgoing-request-middleware).

* [IHttpClientFactory.CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient) creates and configures an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instance for outgoing requests using the configuration that corresponds to the named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) (`WebAPI`).

In the following example, [HttpClientFactoryServiceCollectionExtensions.AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) is an extension in [Microsoft.Extensions.Http](/en-us/dotnet/api/microsoft.extensions.http). Add the package to an app that doesn't already reference it.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

```
using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient("WebAPI", 
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("WebAPI"));
```

For a hosted Blazor [solution](../../tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) based on the [Blazor WebAssembly project template](../../project-structure?view=aspnetcore-10.0), request URIs are within the app's base URI. Therefore, [IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-baseaddress) (`new Uri(builder.HostEnvironment.BaseAddress)`) is assigned to the [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress) in an app generated from the project template.

The configured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is used to make authorized requests using the [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) pattern:

```
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject HttpClient Http

...

protected override async Task OnInitializedAsync()
{
    try
    {
        var examples = 
            await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

        ...
    }
    catch (AccessTokenNotAvailableException exception)
    {
        exception.Redirect();
    }
}
```

## Custom authentication request scenarios

The following scenarios demonstrate how to customize authentication requests and how to obtain the login path from authentication options.

### Customize the login process

Manage additional parameters to a login request with the following methods one or more times on a new instance of [InteractiveRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions):

* [TryAddAdditionalParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions.tryaddadditionalparameter)
* [TryRemoveAdditionalParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions.tryremoveadditionalparameter)
* [TryGetAdditionalParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions.trygetadditionalparameter)

In the following `LoginDisplay` component example, additional parameters are added to the login request:

* `prompt` is set to `login`: Forces the user to enter their credentials on that request, negating single sign on.
* `loginHint` is set to `peter@contoso.com`: Pre-fills the username/email address field of the sign-in page for the user to `peter@contoso.com`. Apps often use this parameter during re-authentication, having already extracted the username from a previous sign in using the `preferred_username` claim.

`Shared/LoginDisplay.razor`:

```
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        <button @onclick="BeginLogOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <button @onclick="BeginLogIn">Log in</button>
    </NotAuthorized>
</AuthorizeView>

@code{
    public void BeginLogOut()
    {
        Navigation.NavigateToLogout("authentication/logout");
    }

    public void BeginLogIn()
    {
        InteractiveRequestOptions requestOptions =
            new()
            {
                Interaction = InteractionType.SignIn,
                ReturnUrl = Navigation.Uri,
            };

        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");

        Navigation.NavigateToLogin("authentication/login", requestOptions);
    }
}
```

For more information, see the following resources:

* [InteractiveRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions)
* [Popup request parameter list](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_browser.html#popuprequest)

### Customize options before obtaining a token interactively

If an [AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) occurs, manage additional parameters for a new identity provider access token request with the following methods one or more times on a new instance of [InteractiveRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions):

* [TryAddAdditionalParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions.tryaddadditionalparameter)
* [TryRemoveAdditionalParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions.tryremoveadditionalparameter)
* [TryGetAdditionalParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions.trygetadditionalparameter)

In the following example that obtains JSON data via web API, additional parameters are added to the redirect request if an access token isn't available ([AccessTokenNotAvailableException](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokennotavailableexception) is thrown):

* `prompt` is set to `login`: Forces the user to enter their credentials on that request, negating single sign on.
* `loginHint` is set to `peter@contoso.com`: Pre-fills the username/email address field of the sign-in page for the user to `peter@contoso.com`. Apps often use this parameter during re-authentication, having already extracted the username from a previous sign in using the `preferred_username` claim.

```
try
{
    var examples = await Http.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

    ...
}
catch (AccessTokenNotAvailableException ex)
{
    ex.Redirect(requestOptions => {
        requestOptions.TryAddAdditionalParameter("prompt", "login");
        requestOptions.TryAddAdditionalParameter("loginHint", "peter@contoso.com");
    });
}
```

The preceding example assumes that:

* The presence of an `@using`/`using` statement for API in the [Microsoft.AspNetCore.Components.WebAssembly.Authentication](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication) namespace.
* `HttpClient` injected as `Http`.

For more information, see the following resources:

* [InteractiveRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions)
* [Redirect request parameter list](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_browser.html#redirectrequest)

### Customize options when using an `IAccessTokenProvider`

If obtaining a token fails when using an [IAccessTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.iaccesstokenprovider), manage additional parameters for a new identity provider access token request with the following methods one or more times on a new instance of [InteractiveRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions):

* [TryAddAdditionalParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions.tryaddadditionalparameter)
* [TryRemoveAdditionalParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions.tryremoveadditionalparameter)
* [TryGetAdditionalParameter](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions.trygetadditionalparameter)

In the following example that attempts to obtain an access token for the user, additional parameters are added to the login request if the attempt to obtain a token fails when [TryGetToken](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenresult.trygettoken) is called:

* `prompt` is set to `login`: Forces the user to enter their credentials on that request, negating single sign on.
* `loginHint` is set to `peter@contoso.com`: Pre-fills the username/email address field of the sign-in page for the user to `peter@contoso.com`. Apps often use this parameter during re-authentication, having already extracted the username from a previous sign in using the `preferred_username` claim.

```
var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = [ ... ]
    });

if (!tokenResult.TryGetToken(out var token))
{
    tokenResult.InteractionOptions.TryAddAdditionalParameter("prompt", "login");
    tokenResult.InteractionOptions.TryAddAdditionalParameter("loginHint", 
        "peter@contoso.com");

    Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl, 
        accessTokenResult.InteractionOptions);
}
```

```
var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { ... }
    });

if (!tokenResult.TryGetToken(out var token))
{
    tokenResult.InteractionOptions.TryAddAdditionalParameter("prompt", "login");
    tokenResult.InteractionOptions.TryAddAdditionalParameter("loginHint", 
        "peter@contoso.com");

    Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl, 
        accessTokenResult.InteractionOptions);
}
```

The preceding example assumes:

* The presence of an `@using`/`using` statement for API in the [Microsoft.AspNetCore.Components.WebAssembly.Authentication](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication) namespace.
* [IAccessTokenProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.iaccesstokenprovider) injected as `TokenProvider`.

For more information, see the following resources:

* [InteractiveRequestOptions](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.interactiverequestoptions)
* [Popup request parameter list](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_browser.html#popuprequest)

### Logout with a custom return URL

The following example logs out the user and returns the user to the `/goodbye` endpoint:

```
Navigation.NavigateToLogout("authentication/logout", "goodbye");
```

### Obtain the login path from authentication options

Obtain the configured login path from [RemoteAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.remoteauthenticationoptions):

```
var loginPath = 
    RemoteAuthOptions.Get(Options.DefaultName).AuthenticationPaths.LogInPath;
```

The preceding example assumes:

* The presence of an `@using`/`using` statement for API in the following namespaces:

  * [Microsoft.AspNetCore.Components.WebAssembly.Authentication](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication)
  * [Microsoft.Extensions.Options](/en-us/dotnet/api/microsoft.extensions.options)

* `IOptionsSnapshot<RemoteAuthenticationOptions<ApiAuthorizationProviderOptions>>` injected as `RemoteAuthOptions`.

### Custom `AuthorizationMessageHandler` class

*This guidance in this section is recommended for client apps that make outgoing requests to URIs that aren't within the app's base URI.*

In the following example, a custom class extends [AuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler) for use as the [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) for an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient). [ConfigureHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler.configurehandler) configures this handler to authorize outbound HTTP requests using an access token. The access token is only attached if at least one of the authorized URLs is a base of the request URI ([HttpRequestMessage.RequestUri](/en-us/dotnet/api/system.net.http.httprequestmessage.requesturi#system-net-http-httprequestmessage-requesturi)).

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, 
        NavigationManager navigation)
        : base(provider, navigation)
    {
        ConfigureHandler(
            authorizedUrls: [ "https://api.contoso.com/v1.0" ],
            scopes: [ "example.read", "example.write" ]);
    }
}
```

Note

In this section, the preceding message handler is used when a configured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is created from an injected [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory). If you don't use an [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory), you must create an [HttpClientHandler](/en-us/dotnet/api/system.net.http.httpclienthandler) instance and assign it to the [AuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler)'s [DelegatingHandler.InnerHandler](/en-us/dotnet/api/system.net.http.delegatinghandler.innerhandler):

```
InnerHandler = new HttpClientHandler();
```

You don't need to make the preceding [InnerHandler](/en-us/dotnet/api/system.net.http.delegatinghandler.innerhandler#system-net-http-delegatinghandler-innerhandler) assignment if you use [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory), as the `ExampleAPIMethod` call later in this section demonstrates.

In the preceding code, the scopes `example.read` and `example.write` are generic examples not meant to reflect valid scopes for any particular provider.

In the `Program` file, `CustomAuthorizationMessageHandler` is registered as a transient service and is configured as the [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) for outgoing [HttpResponseMessage](/en-us/dotnet/api/system.net.http.httpresponsemessage) instances made by a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).

In the following example, [HttpClientFactoryServiceCollectionExtensions.AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) is an extension in [Microsoft.Extensions.Http](/en-us/dotnet/api/microsoft.extensions.http). Add the package to an app that doesn't already reference it.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

```
builder.Services.AddTransient<CustomAuthorizationMessageHandler>();

builder.Services.AddHttpClient("WebAPI",
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
```

Note

In the preceding example, the `CustomAuthorizationMessageHandler` [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) is registered as a transient service for [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler). Transient registration is recommended for [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory), which manages its own DI scopes. For more information, see the following resources:

* [Utility base component classes to manage a DI scope](../../fundamentals/dependency-injection?view=aspnetcore-10.0#utility-base-component-classes-to-manage-a-di-scope)
* [Detect client-side transient disposables](../../fundamentals/dependency-injection?view=aspnetcore-10.0#detect-client-side-transient-disposables)

For a hosted Blazor solution based on the [Blazor WebAssembly project template](../../project-structure?view=aspnetcore-10.0), [IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-baseaddress) (`new Uri(builder.HostEnvironment.BaseAddress)`) is assigned to the [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress).

The configured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is used to make authorized requests using the [`try-catch`](/en-us/dotnet/csharp/language-reference/keywords/try-catch) pattern. Where the client is created with [CreateClient](/en-us/dotnet/api/system.net.http.ihttpclientfactory.createclient) ([`Microsoft.Extensions.Http` package](https://www.nuget.org/packages/Microsoft.Extensions.Http)), the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is supplied instances that include access tokens when making requests to the server API. If the request URI is a relative URI, as it is in the following example (`ExampleAPIMethod`), it's combined with the [BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress) when the client app makes the request:

```
@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        try
        {
            var client = ClientFactory.CreateClient("WebAPI");

            var examples = 
                await client.GetFromJsonAsync<ExampleType[]>("ExampleAPIMethod");

            ...
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}
```

### Configure `AuthorizationMessageHandler`

[AuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler) can be configured with authorized URLs, scopes, and a return URL using the [ConfigureHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler.configurehandler) method. [ConfigureHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler.configurehandler) configures the handler to authorize outbound HTTP requests using an access token. The access token is only attached if at least one of the authorized URLs is a base of the request URI ([HttpRequestMessage.RequestUri](/en-us/dotnet/api/system.net.http.httprequestmessage.requesturi#system-net-http-httprequestmessage-requesturi)). If the request URI is a relative URI, it's combined with the [BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress).

In the following example, [AuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler) configures an [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) in the `Program` file:

```
using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddScoped(sp => new HttpClient(
    sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: [ "https://api.contoso.com/v1.0" ],
        scopes: [ "example.read", "example.write" ])
    .InnerHandler = new HttpClientHandler())
{
    BaseAddress = new Uri("https://api.contoso.com/v1.0")
});
```

In the preceding code:

* The scopes `example.read` and `example.write` are generic examples not meant to reflect valid scopes for any particular provider.
* [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) isn't used to create [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instances, so an [HttpClientHandler](/en-us/dotnet/api/system.net.http.httpclienthandler) instance is manually created and assigned to the [AuthorizationMessageHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler)'s [DelegatingHandler.InnerHandler](/en-us/dotnet/api/system.net.http.delegatinghandler.innerhandler).

For a hosted Blazor solution based on the [Blazor WebAssembly project template](../../project-structure?view=aspnetcore-10.0), [IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-baseaddress) is assigned to the following:

* The [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress) (`new Uri(builder.HostEnvironment.BaseAddress)`).
* A URL of the `authorizedUrls` array.

## Typed `HttpClient`

A typed client can be defined that handles all of the HTTP and token acquisition concerns within a single class.

`WeatherForecastClient.cs`:

```
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using static {PACKAGE ID/ASSEMBLY NAME}.Data;

public class WeatherForecastClient(HttpClient http)
{
    private WeatherForecast[]? forecasts;

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        try
        {
            forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
                "WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}
```

In the preceding example, the `WeatherForecast` type is a static class that holds weather forecast data. The `{PACKAGE ID/ASSEMBLY NAME}` placeholder is the project's package ID (`<PackageId>` in the project file) for a library or assembly name for an app (for example, `using static BlazorSample.Data;`).

In the following example, [HttpClientFactoryServiceCollectionExtensions.AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) is an extension in [Microsoft.Extensions.Http](/en-us/dotnet/api/microsoft.extensions.http). Add the package to an app that doesn't already reference it.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the `Program` file:

```
using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
```

For a hosted Blazor solution based on the [Blazor WebAssembly project template](../../project-structure?view=aspnetcore-10.0), [IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-baseaddress) (`new Uri(builder.HostEnvironment.BaseAddress)`) is assigned to the [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress).

In a component that fetches weather data:

```
@inject WeatherForecastClient Client

...

protected override async Task OnInitializedAsync()
{
    forecasts = await Client.GetForecastAsync();
}
```

## Configure the `HttpClient` handler

The handler can be further configured with [ConfigureHandler](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.authorizationmessagehandler.configurehandler) for outbound HTTP requests.

In the following example, [HttpClientFactoryServiceCollectionExtensions.AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) is an extension in [Microsoft.Extensions.Http](/en-us/dotnet/api/microsoft.extensions.http). Add the package to an app that doesn't already reference it.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the `Program` file:

```
builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: [ "https://api.contoso.com/v1.0" ],
        scopes: [ "example.read", "example.write" ]));
```

```
builder.Services.AddHttpClient<WeatherForecastClient>(
        client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"))
    .AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
    .ConfigureHandler(
        authorizedUrls: new[] { "https://api.contoso.com/v1.0" },
        scopes: new[] { "example.read", "example.write" }));
```

In the preceding code, the scopes `example.read` and `example.write` are generic examples not meant to reflect valid scopes for any particular provider.

For a hosted Blazor solution based on the [Blazor WebAssembly project template](../../project-structure?view=aspnetcore-10.0), [IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-baseaddress) is assigned to the following:

* The [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress) (`new Uri(builder.HostEnvironment.BaseAddress)`).
* A URL of the `authorizedUrls` array.

## Unauthenticated or unauthorized web API requests in an app with a secure default client

An app that ordinarily uses a secure default [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) can also make unauthenticated or unauthorized web API requests by configuring a named [HttpClient](/en-us/dotnet/api/system.net.http.httpclient).

In the following example, [HttpClientFactoryServiceCollectionExtensions.AddHttpClient](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientfactoryservicecollectionextensions.addhttpclient) is an extension in [Microsoft.Extensions.Http](/en-us/dotnet/api/microsoft.extensions.http). Add the package to an app that doesn't already reference it.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

In the `Program` file:

```
builder.Services.AddHttpClient("WebAPI.NoAuthenticationClient", 
    client => client.BaseAddress = new Uri("https://api.contoso.com/v1.0"));
```

For a hosted Blazor solution based on the [Blazor WebAssembly project template](../../project-structure?view=aspnetcore-10.0), [IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-baseaddress) (`new Uri(builder.HostEnvironment.BaseAddress)`) is assigned to the [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress).

The preceding registration is in addition to the existing secure default [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) registration.

A component creates the [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) from the [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) ([`Microsoft.Extensions.Http` package](https://www.nuget.org/packages/Microsoft.Extensions.Http)) to make unauthenticated or unauthorized requests:

```
@inject IHttpClientFactory ClientFactory

...

@code {
    protected override async Task OnInitializedAsync()
    {
        var client = ClientFactory.CreateClient("WebAPI.NoAuthenticationClient");

        var examples = await client.GetFromJsonAsync<ExampleType[]>(
            "ExampleNoAuthentication");

        ...
    }
}
```

Note

The controller in the server API, `ExampleNoAuthenticationController` for the preceding example, isn't marked with the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute).

The decision whether to use a secure client or an insecure client as the default [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instance is up to the developer. One way to make this decision is to consider the number of authenticated versus unauthenticated endpoints that the app contacts. If the majority of the app's requests are to secure API endpoints, use the authenticated [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instance as the default. Otherwise, register the unauthenticated [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) instance as the default.

An alternative approach to using the [IHttpClientFactory](/en-us/dotnet/api/system.net.http.ihttpclientfactory) is to create a [typed client](#typed-httpclient) for unauthenticated access to anonymous endpoints.

## Request additional access tokens

Access tokens can be manually obtained by calling [IAccessTokenProvider.RequestAccessToken](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.iaccesstokenprovider.requestaccesstoken). In the following example, an additional scope is required by an app for the default [HttpClient](/en-us/dotnet/api/system.net.http.httpclient). The Microsoft Authentication Library (MSAL) example configures the scope with `MsalProviderOptions`:

In the `Program` file:

```
builder.Services.AddMsalAuthentication(options =>
{
    ...

    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 1}");
    options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 2}");
}
```

The `{CUSTOM SCOPE 1}` and `{CUSTOM SCOPE 2}` placeholders in the preceding example are custom scopes.

Note

[AdditionalScopesToConsent](/en-us/dotnet/api/microsoft.authentication.webassembly.msal.models.msalprovideroptions.additionalscopestoconsent) isn't able to provision delegated user permissions for Microsoft Graph via the Microsoft Entra ID consent UI when a user first uses an app registered in Microsoft Azure. For more information, see [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0\&pivots=graph-sdk-5#defaultaccesstokenscopes-versus-additionalscopestoconsent).

The [IAccessTokenProvider.RequestAccessToken](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.iaccesstokenprovider.requestaccesstoken) method provides an overload that allows an app to provision an access token with a given set of scopes.

In a Razor component:

```
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider

...

var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = [ "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" ]
    });

if (tokenResult.TryGetToken(out var token))
{
    ...
}
```

```
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider

...

var tokenResult = await TokenProvider.RequestAccessToken(
    new AccessTokenRequestOptions
    {
        Scopes = new[] { "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" }
    });

if (tokenResult.TryGetToken(out var token))
{
    ...
}
```

The `{CUSTOM SCOPE 1}` and `{CUSTOM SCOPE 2}` placeholders in the preceding example are custom scopes.

[AccessTokenResult.TryGetToken](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accesstokenresult.trygettoken) returns:

* `true` with the `token` for use.
* `false` if the token isn't retrieved.

## Cross-Origin Resource Sharing (CORS)

When sending credentials (authorization cookies/headers) on CORS requests, the `Authorization` header must be allowed by the CORS policy.

The following policy includes configuration for:

* Request origins (`http://localhost:5000`, `https://localhost:5001`).
* Any method (verb).
* `Content-Type` and `Authorization` headers. To allow a custom header (for example, `x-custom-header`), list the header when calling [WithHeaders](/en-us/dotnet/api/microsoft.aspnetcore.cors.infrastructure.corspolicybuilder.withheaders).
* Credentials set by client-side JavaScript code (`credentials` property set to `include`).

```
app.UseCors(policy => 
    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
        .AllowAnyMethod()
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, 
            "x-custom-header")
        .AllowCredentials());
```

A hosted Blazor solution based on the [Blazor WebAssembly project template](../../project-structure?view=aspnetcore-10.0) uses the same base address for the client and server apps. The client app's [HttpClient.BaseAddress](/en-us/dotnet/api/system.net.http.httpclient.baseaddress#system-net-http-httpclient-baseaddress) is set to a URI of `builder.HostEnvironment.BaseAddress`. CORS configuration isn't required in the default configuration of a hosted Blazor solution. Additional client apps that aren't hosted by the server project and don't share the server app's base address ***do*** require CORS configuration in the server project.

For more information, see [Enable Cross-Origin Requests (CORS) in ASP.NET Core](../../../security/cors?view=aspnetcore-10.0) and the sample app's HTTP Request Tester component (`Components/HTTPRequestTester.razor`).

## Handle token request errors

When a single-page application (SPA) authenticates a user using OpenID Connect (OIDC), the authentication state is maintained locally within the SPA and in the Identity Provider (IP) in the form of a session cookie that's set as a result of the user providing their credentials.

The tokens that the IP emits for the user typically are valid for short periods of time, about one hour normally, so the client app must regularly fetch new tokens. Otherwise, the user would be logged-out after the granted tokens expire. In most cases, OIDC clients are able to provision new tokens without requiring the user to authenticate again thanks to the authentication state or "session" that is kept within the IP.

There are some cases in which the client can't get a token without user interaction, for example, when for some reason the user explicitly logs out from the IP. This scenario occurs if a user visits `https://login.microsoftonline.com` and logs out. In these scenarios, the app doesn't know immediately that the user has logged out. Any token that the client holds might no longer be valid. Also, the client isn't able to provision a new token without user interaction after the current token expires.

These scenarios aren't specific to token-based authentication. They are part of the nature of SPAs. An SPA using cookies also fails to call a server API if the authentication cookie is removed.

When an app performs API calls to protected resources, you must be aware of the following:

* To provision a new access token to call the API, the user might be required to authenticate again.
* Even if the client has a token that seems to be valid, the call to the server might fail because the token was revoked by the user.

When the app requests a token, there are two possible outcomes:

* The request succeeds, and the app has a valid token.
* The request fails, and the app must authenticate the user again to obtain a new token.

When a token request fails, you need to decide whether you want to save any current state before you perform a redirection. Several approaches exist to store state with increasing levels of complexity:

* Store the current page state in session storage. During the [`OnInitializedAsync` lifecycle method](../../components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) ([OnInitializedAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.componentbase.oninitializedasync)), check if state can be restored before continuing.
* Add a query string parameter and use that as a way to signal the app that it needs to re-hydrate the previously saved state.
* Add a query string parameter with a unique identifier to store data in session storage without risking collisions with other items.

## Save app state before an authentication operation with session storage

The following example shows how to:

* Preserve state before redirecting to the login page.
* Recover the previous state after authentication using a query string parameter.

```
...
@using System.Text.Json
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IAccessTokenProvider TokenProvider
@inject IJSRuntime JS
@inject NavigationManager Navigation

<EditForm Model="User" OnSubmit="OnSaveAsync">
    <label>
        First Name: 
        <InputText @bind-Value="User!.Name" />
    </label>
    <label>
        Last Name: 
        <InputText @bind-Value="User!.LastName" />
    </label>
    <button type="submit">Save User</button>
</EditForm>

@code {
    public Profile User { get; set; } = new Profile();

    protected override async Task OnInitializedAsync()
    {
        var currentQuery = new Uri(Navigation.Uri).Query;

        if (currentQuery.Contains("state=resumeSavingProfile"))
        {
            var user = await JS.InvokeAsync<string>("sessionStorage.getItem",
                "resumeSavingProfile");

            if (!string.IsNullOrEmpty(user))
            {
                User = JsonSerializer.Deserialize<Profile>(user);
            }
        }
    }

    public async Task OnSaveAsync()
    {
        var http = new HttpClient();
        http.BaseAddress = new Uri(Navigation.BaseUri);

        var resumeUri = Navigation.Uri + $"?state=resumeSavingProfile";

        var tokenResult = await TokenProvider.RequestAccessToken(
            new AccessTokenRequestOptions
            {
                ReturnUrl = resumeUri
            });

        if (tokenResult.TryGetToken(out var token))
        {
            http.DefaultRequestHeaders.Add("Authorization", 
                $"Bearer {token.Value}");
            await http.PostAsJsonAsync("Save", User);
        }
        else
        {
            await JS.InvokeVoidAsync("sessionStorage.setItem", 
                "resumeSavingProfile", JsonSerializer.Serialize(User));
            Navigation.NavigateTo(tokenResult.InteractiveRequestUrl);
        }
    }

    public class Profile
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }
}
```

## Save app state before an authentication operation with session storage and a state container

During an authentication operation, there are cases where you want to save the app state before the browser is redirected to the IP. This can be the case when you're using a state container and want to restore the state after the authentication succeeds. You can use a custom authentication state object to preserve app-specific state or a reference to it and restore that state after the authentication operation successfully completes. The following example demonstrates the approach.

A state container class is created in the app with properties to hold the app's state values. In the following example, the container is used to maintain the counter value of the default [Blazor project template's](../../project-structure?view=aspnetcore-10.0) `Counter` component (`Counter.razor`). Methods for serializing and deserializing the container are based on [System.Text.Json](/en-us/dotnet/api/system.text.json).

```
using System.Text.Json;

public class StateContainer
{
    public int CounterValue { get; set; }

    public string GetStateForLocalStorage() => JsonSerializer.Serialize(this);

    public void SetStateFromLocalStorage(string locallyStoredState)
    {
        var deserializedState = 
            JsonSerializer.Deserialize<StateContainer>(locallyStoredState);

        CounterValue = deserializedState.CounterValue;
    }
}
```

The `Counter` component uses the state container to maintain the `currentCount` value outside of the component:

```
@page "/counter"
@inject StateContainer State

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    protected override void OnInitialized()
    {
        if (State.CounterValue > 0)
        {
            currentCount = State.CounterValue;
        }
    }

    private void IncrementCount()
    {
        currentCount++;
        State.CounterValue = currentCount;
    }
}
```

Create an `ApplicationAuthenticationState` from [RemoteAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticationstate). Provide an `Id` property, which serves as an identifier for the locally-stored state.

`ApplicationAuthenticationState.cs`:

```
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class ApplicationAuthenticationState : RemoteAuthenticationState
{
    public string? Id { get; set; }
}
```

The `Authentication` component (`Authentication.razor`) saves and restores the app's state using local session storage with the `StateContainer` serialization and deserialization methods, `GetStateForLocalStorage` and `SetStateFromLocalStorage`:

```
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IJSRuntime JS
@inject StateContainer State

<RemoteAuthenticatorViewCore Action="Action"
                             TAuthenticationState="ApplicationAuthenticationState"
                             AuthenticationState="AuthenticationState"
                             OnLogInSucceeded="RestoreState"
                             OnLogOutSucceeded="RestoreState" />

@code {
    [Parameter]
    public string? Action { get; set; }

    public ApplicationAuthenticationState AuthenticationState { get; set; } =
        new ApplicationAuthenticationState();

    protected override async Task OnInitializedAsync()
    {
        if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn,
            Action) ||
            RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut,
            Action))
        {
            AuthenticationState.Id = Guid.NewGuid().ToString();

            await JS.InvokeVoidAsync("sessionStorage.setItem",
                AuthenticationState.Id, State.GetStateForLocalStorage());
        }
    }

    private async Task RestoreState(ApplicationAuthenticationState state)
    {
        if (state.Id != null)
        {
            var locallyStoredState = await JS.InvokeAsync<string>(
                "sessionStorage.getItem", state.Id);

            if (locallyStoredState != null)
            {
                State.SetStateFromLocalStorage(locallyStoredState);
                await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id);
            }
        }
    }
}
```

This example uses Microsoft Entra (ME-ID) for authentication. In the `Program` file:

* The `ApplicationAuthenticationState` is configured as the Microsoft Authentication Library (MSAL) `RemoteAuthenticationState` type.
* The state container is registered in the service container.

```
builder.Services.AddMsalAuthentication<ApplicationAuthenticationState>(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});

builder.Services.AddSingleton<StateContainer>();
```

## Customize app routes

The [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) library uses the routes shown in the following table for representing different authentication states.

| Route                            | Purpose                                                                    |
| -------------------------------- | -------------------------------------------------------------------------- |
| `authentication/login`           | Triggers a sign-in operation.                                              |
| `authentication/login-callback`  | Handles the result of any sign-in operation.                               |
| `authentication/login-failed`    | Displays error messages when the sign-in operation fails for some reason.  |
| `authentication/logout`          | Triggers a sign-out operation.                                             |
| `authentication/logout-callback` | Handles the result of a sign-out operation.                                |
| `authentication/logout-failed`   | Displays error messages when the sign-out operation fails for some reason. |
| `authentication/logged-out`      | Indicates that the user has successfully logout.                           |
| `authentication/profile`         | Triggers an operation to edit the user profile.                            |
| `authentication/register`        | Triggers an operation to register a new user.                              |

The routes shown in the preceding table are configurable via [RemoteAuthenticationOptions\<TRemoteAuthenticationProviderOptions>.AuthenticationPaths](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticationoptions-1.authenticationpaths). When setting options to provide custom routes, confirm that the app has a route that handles each path.

In the following example, all of the paths are prefixed with `/security`.

`Authentication` component (`Authentication.razor`):

```
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter]
    public string? Action { get; set; }
}
```

```
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter]
    public string Action { get; set; }
}
```

In the `Program` file:

```
builder.Services.AddApiAuthorization(options => { 
    options.AuthenticationPaths.LogInPath = "security/login";
    options.AuthenticationPaths.LogInCallbackPath = "security/login-callback";
    options.AuthenticationPaths.LogInFailedPath = "security/login-failed";
    options.AuthenticationPaths.LogOutPath = "security/logout";
    options.AuthenticationPaths.LogOutCallbackPath = "security/logout-callback";
    options.AuthenticationPaths.LogOutFailedPath = "security/logout-failed";
    options.AuthenticationPaths.LogOutSucceededPath = "security/logged-out";
    options.AuthenticationPaths.ProfilePath = "security/profile";
    options.AuthenticationPaths.RegisterPath = "security/register";
});
```

If the requirement calls for completely different paths, set the routes as described previously and render the [RemoteAuthenticatorView](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticatorview) with an explicit action parameter:

```
@page "/register"

<RemoteAuthenticatorView Action="RemoteAuthenticationActions.Register" />
```

You're allowed to break the UI into different pages if you choose to do so.

## Customize the authentication user interface

[RemoteAuthenticatorView](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticatorview) includes a default set of UI fragments for each authentication state. Each state can be customized by passing in a custom [RenderFragment](/en-us/dotnet/api/microsoft.aspnetcore.components.renderfragment). To customize the displayed text during the initial login process, can change the [RemoteAuthenticatorView](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticatorview) as follows.

`Authentication` component (`Authentication.razor`):

```
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string? Action { get; set; }
}
```

```
@page "/security/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action">
    <LoggingIn>
        You are about to be redirected to https://login.microsoftonline.com.
    </LoggingIn>
</RemoteAuthenticatorView>

@code{
    [Parameter]
    public string Action { get; set; }
}
```

The [RemoteAuthenticatorView](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticatorview) has one fragment that can be used per authentication route shown in the following table.

| Route                            | Fragment                |
| -------------------------------- | ----------------------- |
| `authentication/login`           | `<LoggingIn>`           |
| `authentication/login-callback`  | `<CompletingLoggingIn>` |
| `authentication/login-failed`    | `<LogInFailed>`         |
| `authentication/logout`          | `<LogOut>`              |
| `authentication/logout-callback` | `<CompletingLogOut>`    |
| `authentication/logout-failed`   | `<LogOutFailed>`        |
| `authentication/logged-out`      | `<LogOutSucceeded>`     |
| `authentication/profile`         | `<UserProfile>`         |
| `authentication/register`        | `<Registering>`         |

## Customize the user

Users bound to the app can be customized.

### Customize the user with a payload claim

In the following example, the app's authenticated users receive an `amr` claim for each of the user's authentication methods. The `amr` claim identifies how the subject of the token was authenticated in Microsoft identity platform v1.0 [payload claims](/en-us/entra/identity-platform/access-tokens#amr-claim). The example uses a custom user account class based on [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount).

Create a class that extends the [RemoteUserAccount](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteuseraccount) class. The following example sets the `AuthenticationMethod` property to the user's array of `amr` JSON property values. `AuthenticationMethod` is populated automatically by the framework when the user is authenticated.

```
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomUserAccount : RemoteUserAccount
{
    [JsonPropertyName("amr")]
    public string[]? AuthenticationMethod { get; set; }
}
```

Create a factory that extends [AccountClaimsPrincipalFactory\<TAccount>](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.accountclaimsprincipalfactory-1) to create claims from the user's authentication methods stored in `CustomUserAccount.AuthenticationMethod`:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory(NavigationManager navigation,
    IAccessTokenProviderAccessor accessor)
    : AccountClaimsPrincipalFactory<CustomUserAccount>(accessor)
{
    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        CustomUserAccount account, RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity != null && initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = (ClaimsIdentity)initialUser.Identity;

            if (account.AuthenticationMethod is not null)
            {
                foreach (var value in account.AuthenticationMethod)
                {
                    userIdentity.AddClaim(new Claim("amr", value));
                }
            }
        }

        return initialUser;
    }
}
```

Register the `CustomAccountFactory` for the authentication provider in use. Any of the following registrations are valid:

* [AddOidcAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addoidcauthentication):

  ```
  using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

  ...

  builder.Services.AddOidcAuthentication<RemoteAuthenticationState, 
      CustomUserAccount>(options =>
      {
          ...
      })
      .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
          CustomUserAccount, CustomAccountFactory>();
  ```

* [AddMsalAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.msalwebassemblyservicecollectionextensions.addmsalauthentication):

  ```
  using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

  ...

  builder.Services.AddMsalAuthentication<RemoteAuthenticationState, 
      CustomUserAccount>(options =>
      {
          ...
      })
      .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
          CustomUserAccount, CustomAccountFactory>();
  ```

* [AddApiAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addapiauthorization):

  ```
  using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

  ...

  builder.Services.AddApiAuthorization<RemoteAuthenticationState, 
      CustomUserAccount>(options =>
      {
          ...
      })
      .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, 
          CustomUserAccount, CustomAccountFactory>();
  ```

### ME-ID security groups and roles with a custom user account class

For an additional example that works with ME-ID security groups and ME-ID Administrator Roles and a custom user account class, see [ASP.NET Core Blazor WebAssembly with Microsoft Entra ID groups and roles](microsoft-entra-id-groups-and-roles?view=aspnetcore-10.0).

## Prerendering with authentication

Prerendering content that requires authentication and authorization isn't currently supported. After following the guidance in one of the Blazor WebAssembly security app topics, use the following instructions to create an app that:

* Prerenders paths for which authorization isn't required.
* Doesn't prerender paths for which authorization is required.

For the **Client** project's the `Program` file, factor common service registrations into a separate method (for example, create a `ConfigureCommonServices` method in the **Client** project). Common services are those that the developer registers for use by both the client and server projects.

```
public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}
```

In the `Program` file:

```
var builder = WebAssemblyHostBuilder.CreateDefault(args);
...

builder.Services.AddScoped( ... );

ConfigureCommonServices(builder.Services);

await builder.Build().RunAsync();
```

In the **Server** project's the `Program` file, register the following additional services and call `ConfigureCommonServices`:

```
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

...

builder.Services.AddRazorPages();
builder.Services.TryAddScoped<AuthenticationStateProvider, 
    ServerAuthenticationStateProvider>();

Client.Program.ConfigureCommonServices(services);
```

In the **Server** project's `Startup.ConfigureServices` method, register the following additional services and call `ConfigureCommonServices`:

```
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddRazorPages();
    services.AddScoped<AuthenticationStateProvider, 
        ServerAuthenticationStateProvider>();
    services.AddScoped<SignOutSessionStateManager>();

    Client.Program.ConfigureCommonServices(services);
}
```

For more information on the Blazor framework server authentication provider (`ServerAuthenticationStateProvider`), see [ASP.NET Core Blazor authentication and authorization](../?view=aspnetcore-10.0#authenticationstateprovider).

In the **Server** project's `Pages/_Host.cshtml` file, replace the `Component` Tag Helper (`<component ... />`) with the following:

```
<div id="app">
    @if (HttpContext.Request.Path.StartsWithSegments("/authentication"))
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssembly" />
    }
    else
    {
        <component type="typeof({CLIENT APP ASSEMBLY NAME}.App)" 
            render-mode="WebAssemblyPrerendered" />
    }
</div>
```

In the preceding example:

* The `{CLIENT APP ASSEMBLY NAME}` placeholder is the client app's assembly name (for example `BlazorSample.Client`).

* The conditional check for the `/authentication` path segment:

  * Avoids prerendering (`render-mode="WebAssembly"`) for authentication paths.
  * Prerenders (`render-mode="WebAssemblyPrerendered"`) for non-authentication paths.

## Options for hosted apps and third-party login providers

When authenticating and authorizing a hosted Blazor WebAssembly app with a third-party provider, there are several options available for authenticating the user. Which one you choose depends on your scenario.

For more information, see [Persist additional claims and tokens from external providers in ASP.NET Core](../../../security/authentication/social/additional-claims?view=aspnetcore-10.0).

### Authenticate users to only call protected third party APIs

Authenticate the user with a client-side OAuth flow against the third-party API provider:

```
builder.services.AddOidcAuthentication(options => { ... });
```

In this scenario:

* The server hosting the app doesn't play a role.
* APIs on the server can't be protected.
* The app can only call protected third-party APIs.

### Authenticate users with a third-party provider and call protected APIs on the host server and the third party

Configure Identity with a third-party login provider. Obtain the tokens required for third-party API access and store them.

When a user logs in, Identity collects access and refresh tokens as part of the authentication process. At that point, there are a couple of approaches available for making API calls to third-party APIs.

#### Use a server access token to retrieve the third-party access token

Use the access token generated on the server to retrieve the third-party access token from a server API endpoint. From there, use the third-party access token to call third-party API resources directly from Identity on the client.

***We don't recommend this approach.*** This approach requires treating the third-party access token as if it were generated for a public client. In OAuth terms, the public app doesn't have a client secret because it can't be trusted to store secrets safely, and the access token is produced for a confidential client. A confidential client is a client that has a client secret and is assumed to be able to safely store secrets.

* The third-party access token might be granted additional scopes to perform sensitive operations based on the fact that the third-party emitted the token for a more trusted client.
* Similarly, refresh tokens shouldn't be issued to a client that isn't trusted, as doing so gives the client unlimited access unless other restrictions are put into place.

#### Make API calls from the client to the server API in order to call third-party APIs

Make an API call from the client to the server API. From the server, retrieve the access token for the third-party API resource and issue whatever call is necessary.

***We recommend this approach.*** While this approach requires an extra network hop through the server to call a third-party API, it ultimately results in a safer experience:

* The server can store refresh tokens and ensure that the app doesn't lose access to third-party resources.
* The app can't leak access tokens from the server that might contain more sensitive permissions.

## Use OpenID Connect (OIDC) v2.0 endpoints

The authentication library and [Blazor project templates](../../project-structure?view=aspnetcore-10.0) use OpenID Connect (OIDC) v1.0 endpoints. To use a v2.0 endpoint, configure the JWT bearer [JwtBearerOptions.Authority](/en-us/dotnet/api/microsoft.aspnetcore.builder.jwtbeareroptions.authority#microsoft-aspnetcore-builder-jwtbeareroptions-authority) option. In the following example, ME-ID is configured for v2.0 by appending a `v2.0` segment to the [Authority](/en-us/dotnet/api/microsoft.aspnetcore.builder.jwtbeareroptions.authority#microsoft-aspnetcore-builder-jwtbeareroptions-authority) property:

```
using Microsoft.AspNetCore.Authentication.JwtBearer;

...

builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme, 
    options =>
    {
        options.Authority += "/v2.0";
    });
```

Alternatively, the setting can be made in the app settings (`appsettings.json`) file:

```
{
  "Local": {
    "Authority": "https://login.microsoftonline.com/common/oauth2/v2.0",
    ...
  }
}
```

If tacking on a segment to the authority isn't appropriate for the app's OIDC provider, such as with non-ME-ID providers, set the [Authority](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions.authority#microsoft-aspnetcore-builder-openidconnectoptions-authority) property directly. Either set the property in [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.jwtbeareroptions) or in the app settings file (`appsettings.json`) with the `Authority` key.

The list of claims in the ID token changes for v2.0 endpoints. Microsoft documentation on the changes has been retired, but guidance on the claims in an ID token is available in the [ID token claims reference](/en-us/entra/identity-platform/id-token-claims-reference).

## Configure and use gRPC in components

To configure a Blazor WebAssembly app to use the [ASP.NET Core gRPC framework](../../../grpc/?view=aspnetcore-10.0):

* Enable gRPC-Web on the server. For more information, see [gRPC-Web in ASP.NET Core gRPC apps](../../../grpc/grpcweb?view=aspnetcore-10.0).
* Register gRPC services for the app's message handler. The following example configures the app's authorization message handler to use the [`GreeterClient` service from the gRPC tutorial](../../../tutorials/grpc/grpc-start?view=aspnetcore-10.0#create-a-grpc-service) (the `Program` file).

Note

Prerendering is enabled by default in Blazor Web Apps, so you must account for the component rendering first from the server and then from the client. Any prerendered state should flow to the client so that it can be reused. For more information, see [ASP.NET Core Blazor prerendered state persistence](../../state-management/prerendered-state-persistence?view=aspnetcore-10.0).

Note

Prerendering is enabled by default in hosted Blazor WebAssembly apps, so you must account for the component rendering first from the server and then from the client. Any prerendered state should flow to the client so that it can be reused. For more information, see [Integrate ASP.NET Core Razor components with MVC or Razor Pages in hosted Blazor WebAssembly solutions](../../components/integration-hosted-webassembly?view=aspnetcore-10.0#persist-prerendered-state).

```
using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;

...

builder.Services.AddScoped(sp =>
{
    var baseAddressMessageHandler = 
        sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
    baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
    var grpcWebHandler = 
        new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler);
    var channel = GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress, 
        new GrpcChannelOptions { HttpHandler = grpcWebHandler });

    return new Greeter.GreeterClient(channel);
});
```

A component in the client app can make gRPC calls using the gRPC client (`Grpc.razor`):

```
@page "/grpc"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject Greeter.GreeterClient GreeterClient

<h1>Invoke gRPC service</h1>

<p>
    <input @bind="name" placeholder="Type your name" />
    <button @onclick="GetGreeting" class="btn btn-primary">Call gRPC service</button>
</p>

Server response: <strong>@serverResponse</strong>

@code {
    private string name = "Bert";
    private string? serverResponse;

    private async Task GetGreeting()
    {
        try
        {
            var request = new HelloRequest { Name = name };
            var reply = await GreeterClient.SayHelloAsync(request);
            serverResponse = reply.Message;
        }
        catch (Grpc.Core.RpcException ex)
            when (ex.Status.DebugException is 
                AccessTokenNotAvailableException tokenEx)
        {
            tokenEx.Redirect();
        }
    }
}
```

To use the `Status.DebugException` property, use [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client) version 2.30.0 or later.

For more information, see [gRPC-Web in ASP.NET Core gRPC apps](../../../grpc/grpcweb?view=aspnetcore-10.0).

## Replace the `AuthenticationService` implementation

The following subsections explain how to replace:

* Any JavaScript `AuthenticationService` implementation.
* The Microsoft Authentication Library for JavaScript (`MSAL.js`).

### Replace any JavaScript `AuthenticationService` implementation

Create a JavaScript library to handle your custom authentication details.

Warning

The guidance in this section is an implementation detail of the default [RemoteAuthenticationService\<TRemoteAuthenticationState,TAccount,TProviderOptions>](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.authentication.remoteauthenticationservice-3). The TypeScript code in this section applies specifically to ASP.NET Core in .NET 7 and is subject to change without notice in upcoming releases of ASP.NET Core.

```
// .NET makes calls to an AuthenticationService object in the Window.
declare global {
  interface Window { AuthenticationService: AuthenticationService }
}

export interface AuthenticationService {
  // Init is called to initialize the AuthenticationService.
  public static init(settings: UserManagerSettings & AuthorizeServiceSettings, logger: any) : Promise<void>;

  // Gets the currently authenticated user.
  public static getUser() : Promise<{[key: string] : string }>;

  // Tries to get an access token silently.
  public static getAccessToken(options: AccessTokenRequestOptions) : Promise<AccessTokenResult>;

  // Tries to sign in the user or get an access token interactively.
  public static signIn(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the sign-in process when a redirect is used.
  public static async completeSignIn(url: string) : Promise<AuthenticationResult>;

  // Signs the user out.
  public static signOut(context: AuthenticationContext) : Promise<AuthenticationResult>;

  // Handles the signout callback when a redirect is used.
  public static async completeSignOut(url: string) : Promise<AuthenticationResult>;
}

// The rest of these interfaces match their C# definitions.

export interface AccessTokenRequestOptions {
  scopes: string[];
  returnUrl: string;
}

export interface AccessTokenResult {
  status: AccessTokenResultStatus;
  token?: AccessToken;
}

export interface AccessToken {
  value: string;
  expires: Date;
  grantedScopes: string[];
}

export enum AccessTokenResultStatus {
  Success = 'Success',
  RequiresRedirect = 'RequiresRedirect'
}

export enum AuthenticationResultStatus {
  Redirect = 'Redirect',
  Success = 'Success',
  Failure = 'Failure',
  OperationCompleted = 'OperationCompleted'
};

export interface AuthenticationResult {
  status: AuthenticationResultStatus;
  state?: unknown;
  message?: string;
}

export interface AuthenticationContext {
  state?: unknown;
  interactiveRequest: InteractiveAuthenticationRequest;
}

export interface InteractiveAuthenticationRequest {
  scopes?: string[];
  additionalRequestParameters?: { [key: string]: any };
};
```

You can import the library by removing the original `<script>` tag and adding a `<script>` tag that loads the custom library. The following example demonstrates replacing the default `<script>` tag with one that loads a library named `CustomAuthenticationService.js` from the `wwwroot/js` folder.

In `wwwroot/index.html` before the Blazor script (`_framework/blazor.webassembly.js`) inside the closing `</body>` tag:

```
- <script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
+ <script src="js/CustomAuthenticationService.js"></script>
```

For more information, see [`AuthenticationService.ts` in the `dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/Interop/AuthenticationService.ts).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Replace the Microsoft Authentication Library for JavaScript (`MSAL.js`)

If an app requires a custom version of the [Microsoft Authentication Library for JavaScript (`MSAL.js`)](https://www.npmjs.com/package/@azure/msal-browser), perform the following steps:

1. Confirm the system has the latest developer .NET SDK or obtain and install the latest developer SDK from [.NET SDK: Installers and Binaries](https://github.com/dotnet/sdk#installing-the-sdk). Configuration of internal NuGet feeds isn't required for this scenario.
2. Set up the `dotnet/aspnetcore` GitHub repository for development following the documentation at [Build ASP.NET Core from Source](https://github.com/dotnet/aspnetcore/blob/main/docs/BuildFromSource.md). Fork and clone or download a ZIP archive of the [`dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore).
3. Open the `src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json` file and set the desired version of `@azure/msal-browser`. For a list of released versions, visit the [`@azure/msal-browser` npm website](https://www.npmjs.com/package/@azure/msal-browser) and select the **Versions** tab.
4. Build the `Authentication.Msal` project in the `src/Components/WebAssembly/Authentication.Msal/src` folder with the `yarn build` command in a command shell.
5. If the app uses [compressed assets (Brotli/Gzip)](../../host-and-deploy/webassembly/?view=aspnetcore-10.0#compression), compress the `Interop/dist/Release/AuthenticationService.js` file.
6. Copy the `AuthenticationService.js` file and compressed versions (`.br`/`.gz`) of the file, if produced, from the `Interop/dist/Release` folder into the app's `publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal` folder in the app's published assets.

## Pass custom provider options

Define a class for passing the data to the underlying JavaScript library.

Important

The class's structure must match what the library expects when the JSON is serialized with [System.Text.Json](/en-us/dotnet/api/system.text.json).

The following example demonstrates a `ProviderOptions` class with [`JsonPropertyName` attributes](/en-us/dotnet/api/system.text.json.serialization.jsonpropertynameattribute) matching a hypothetical custom provider library's expectations:

```
public class ProviderOptions
{
    public string? Authority { get; set; }
    public string? MetadataUrl { get; set; }
    
    [JsonPropertyName("client_id")]
    public string? ClientId { get; set; }
    
    public IList<string> DefaultScopes { get; set; } = [ "openid", "profile" ];
        
    [JsonPropertyName("redirect_uri")]
    public string? RedirectUri { get; set; }
    
    [JsonPropertyName("post_logout_redirect_uri")]
    public string? PostLogoutRedirectUri { get; set; }
    
    [JsonPropertyName("response_type")]
    public string? ResponseType { get; set; }
    
    [JsonPropertyName("response_mode")]
    public string? ResponseMode { get; set; }
}
```

Register the provider options within the DI system and configure the appropriate values:

```
builder.Services.AddRemoteAuthentication<RemoteAuthenticationState, RemoteUserAccount,
    ProviderOptions>(options => {
        options.ProviderOptions.Authority = "...";
        options.ProviderOptions.MetadataUrl = "...";
        options.ProviderOptions.ClientId = "...";
        options.ProviderOptions.DefaultScopes = [ "openid", "profile", "myApi" ];
        options.ProviderOptions.RedirectUri = "https://localhost:5001/authentication/login-callback";
        options.ProviderOptions.PostLogoutRedirectUri = "https://localhost:5001/authentication/logout-callback";
        options.ProviderOptions.ResponseType = "...";
        options.ProviderOptions.ResponseMode = "...";
    });
```

The preceding example sets redirect URIs with regular string literals. The following alternatives are available:

* [TryCreate](/en-us/dotnet/api/system.uri.trycreate) using [IWebAssemblyHostEnvironment.BaseAddress](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment.baseaddress#microsoft-aspnetcore-components-webassembly-hosting-iwebassemblyhostenvironment-baseaddress):

  ```
  Uri.TryCreate(
      $"{builder.HostEnvironment.BaseAddress}authentication/login-callback", 
      UriKind.Absolute, out var redirectUri);
  options.RedirectUri = redirectUri;
  ```

* [Host builder configuration](../../fundamentals/configuration?view=aspnetcore-10.0#host-builder-configuration):

  ```
  options.RedirectUri = builder.Configuration["RedirectUri"];
  ```

  `wwwroot/appsettings.json`:

  ```
  {
    "RedirectUri": "https://localhost:5001/authentication/login-callback"
  }
  ```

## Related content

* [Use Graph API with ASP.NET Core Blazor WebAssembly](graph-api?view=aspnetcore-10.0)
* [Cookie-based request credentials (*Call web API* article)](../../call-web-api?view=aspnetcore-10.0#cookie-based-request-credentials)
* [`HttpClient` and `HttpRequestMessage` with Fetch API request options (*Call web API* article)](../../call-web-api?view=aspnetcore-10.0#httpclient-and-httprequestmessage-with-fetch-api-request-options)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/netstandard
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/netstandard.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use gRPC client with .NET Standard 2.0

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

This article discusses how to use the .NET gRPC client with .NET implementations that support [.NET Standard 2.0](/en-us/dotnet/standard/net-standard).

## .NET implementations

The following .NET implementations (or later) support [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client/) but don't have full support for HTTP/2:

* .NET Core 2.1
* .NET Framework 4.6.1
* Mono 5.4
* Universal Windows Platform 10.0.16299
* Unity 2018.1

The .NET gRPC client can call services from these .NET implementations with some additional configuration.

## HttpHandler configuration

An HTTP provider must be configured using `GrpcChannelOptions.HttpHandler`. If a handler isn't configured, an error is thrown:

> `System.PlatformNotSupportedException`: gRPC requires extra configuration to successfully make RPC calls on .NET implementations that don't have support for gRPC over HTTP/2. An HTTP provider must be specified using `GrpcChannelOptions.HttpHandler`. The configured HTTP provider must either support HTTP/2 or be configured to use gRPC-Web.

.NET implementations that don't support HTTP/2, such as UWP and Unity, can use gRPC-Web as an alternative.

```
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
    {
        HttpHandler = new GrpcWebHandler(new HttpClientHandler())
    });

var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = ".NET" });
```

Clients can also be created using the [gRPC client factory](clientfactory?view=aspnetcore-10.0). An HTTP provider is configured using the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method.

```
builder.Services
    .AddGrpcClient<Greet.GreeterClient>(options =>
    {
        options.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(
        () => new GrpcWebHandler(new HttpClientHandler()));
```

For more information, see [Configure gRPC-Web with the .NET gRPC client](grpcweb?view=aspnetcore-10.0#configure-grpc-web-with-the-net-grpc-client).

Important

gRPC-Web requires the client ***and*** server to support it. gRPC-Web can be [quickly configured by an ASP.NET Core gRPC server](grpcweb?view=aspnetcore-10.0#configure-grpc-web-in-aspnet-core). Other gRPC server implementations require a proxy to support gRPC-Web.

## .NET Framework

.NET Framework has limited support for gRPC over HTTP/2. To enable gRPC over HTTP/2 on .NET Framework, configure the channel to use [WinHttpHandler](/en-us/dotnet/api/system.net.http.winhttphandler).

Requirements and restrictions to using `WinHttpHandler`:

* Windows 11 or later, Windows Server 2019 or later.

  * gRPC client is fully supported on Windows 11 or later.
  * gRPC client is partially supported on Windows Server 2019 and Windows Server 2022. Unary and server streaming methods are supported. Client and bidirectional streaming methods are ***not*** supported.

* A reference to [`System.Net.Http.WinHttpHandler`](https://www.nuget.org/packages/System.Net.Http.WinHttpHandler/) version 6.0.1 or later.

* Configure `WinHttpHandler` on the channel using `GrpcChannelOptions.HttpHandler`.

* .NET Framework 4.6.1 or later.

* Only gRPC calls over TLS are supported.

```
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
    {
        HttpHandler = new WinHttpHandler()
    });

var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = ".NET" });
```

Note

The gRPC client on .NET Framework is slower than on modern .NET versions. For improved gRPC performance, apps should be updated to use modern .NET.

## gRPC C# core-library

An alternative option for .NET Framework has been to use [gRPC C# core-library](https://grpc.io/docs/languages/csharp/quickstart/) to make gRPC calls. gRPC C# core-library is:

* A third party library that supports making gRPC calls over HTTP/2 on .NET Framework.
* Not supported by Microsoft.
* In maintenance mode and will be [deprecated in favour of gRPC for .NET](https://grpc.io/blog/grpc-csharp-future/).
* Not recommended for new apps.

## Related content

* [Call gRPC services with the .NET client](client?view=aspnetcore-10.0)
* [Use gRPC in browser apps](browser?view=aspnetcore-10.0)
* [gRPC C# core-library](https://grpc.io/docs/languages/csharp/quickstart/)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-16

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/90-to-100
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/90-to-100.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core in .NET 9 to ASP.NET Core in .NET 10

This article explains how to update an ASP.NET Core in .NET 9 to ASP.NET Core in .NET 10.

## Prerequisites

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

- [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) with the **ASP.NET and web development** workload.

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# Dev Kit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit)
* [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)

You can follow the Visual Studio Code instructions on macOS, Linux, or Windows. Changes may be required if you use an integrated development environment (IDE) other than Visual Studio Code.

## Update the .NET SDK version in `global.json`

If you rely on a [`global.json`](/en-us/dotnet/core/tools/global-json) file to target a specific .NET SDK version, update the `version` property to the .NET 10 SDK version that's installed. For example:

```
{
  "sdk": {
-    "version": "9.0.304"
+    "version": "10.0.100"
  }
}
```

## Update the target framework

Update the project file's [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks) to `net10.0`:

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

  <PropertyGroup>
-    <TargetFramework>net9.0</TargetFramework>
+    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>

</Project>
```

## Update package references

In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.org/packages?q=Microsoft.AspNetCore.*), [`Microsoft.EntityFrameworkCore.*`](https://www.nuget.org/packages?q=Microsoft.EntityFrameworkCore.*), [`Microsoft.Extensions.*`](https://www.nuget.org/packages?q=Microsoft.Extensions.*), and [`System.Net.Http.Json`](https://www.nuget.org/packages/System.Net.Http.Json) package reference's `Version` attribute to 10.0.0 or later. For example:

```
<ItemGroup>
-   <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="9.0.0" />
-   <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
-   <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.0" />
-   <PackageReference Include="System.Net.Http.Json" Version="9.0.0" />
+   <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="10.0.0" />
+   <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0" />
+   <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
+   <PackageReference Include="System.Net.Http.Json" Version="10.0.0" />
</ItemGroup>
```

## Blazor

### Blazor release notes

For new feature coverage, see [What's new in ASP.NET Core in .NET 10](../release-notes/aspnetcore-10.0?view=aspnetcore-10.0).

### Set the Blazor WebAssembly environment with the `WasmApplicationEnvironmentName` MSBuild property

*This section only applies to standalone Blazor WebAssembly apps.*

The `Blazor-Environment` header and `Properties/launchSettings.json` file (`ASPNETCORE_ENVIRONMENT` environment variable) are no longer used to control the environment in standalone Blazor WebAssembly apps.

Set the environment with the `<WasmApplicationEnvironmentName>` property in the app's project file (`.csproj`).

The following example sets the app's environment to `Staging`:

```
<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>
```

The default environments are:

* `Development` for build.
* `Production` for publish.

### Boot configuration file inlined

Blazor's boot configuration, which prior to the release of .NET 10 existed in a file named `blazor.boot.json`, has been inlined into the `dotnet.js` script. This only affects developers who are interacting directly with the `blazor.boot.json` file, such as when developers are:

* Checking file integrity for published assets with the troubleshoot integrity PowerShell script per the guidance in [ASP.NET Core Blazor WebAssembly caching and integrity check failures](../blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures?view=aspnetcore-9.0#troubleshoot-integrity-powershell-script).
* Changing the file name extension of DLL files when not using the default Webcil file format per the guidance in [Host and deploy ASP.NET Core Blazor WebAssembly](../blazor/host-and-deploy/webassembly/?view=aspnetcore-9.0#customize-how-boot-resources-are-loaded).

Currently, there's no documented replacement strategy for the preceding approaches. If you require either of the preceding strategies, open a new documentation issue describing your scenario using the **Open a documentation issue** link at the bottom of either article.

### Declarative model for persisting state from components and services

In prior Blazor releases, persisting component state during prerendering using the [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) service involved a significant amount of code. Starting with .NET 10, you can declaratively specify state to persist from components and services using the `[PersistentState]` attribute. For more information, see [What's new in ASP.NET Core in .NET 10](../release-notes/aspnetcore-10.0?view=aspnetcore-10.0#declarative-model-for-persisting-state-from-components-and-services).

### Custom Blazor cache and `BlazorCacheBootResources` MSBuild property removed

Now that all Blazor client-side files are fingerprinted and cached by the browser, Blazor's custom caching mechanism and the `BlazorCacheBootResources` MSBuild property are no longer available. If the client-side project's project file contains the MSBuild property, remove the property, as it no longer has any effect:

```
- <BlazorCacheBootResources>...</BlazorCacheBootResources>
```

For more information, see [ASP.NET Core Blazor WebAssembly caching and integrity check failures](../blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures?view=aspnetcore-10.0).

### Adopt passkey user authentication in an existing Blazor Web App

For guidance, see [Implement passkeys in ASP.NET Core Blazor Web Apps](../security/authentication/passkeys/blazor?view=aspnetcore-10.0\&pivots=existing-app).

### When navigation errors are disabled in a Blazor Web App with Individual Accounts

*This section applies to Blazor Web Apps that set the `<BlazorDisableThrowNavigationException>` MSBuild property to `true` in order to avoid throwing an navigation exception during static server-side rendering (static SSR).*

The `IdentityRedirectManager` threw an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) in the `RedirectTo` method to ensure the method wasn't called from an interactive render mode and all the redirection methods were marked with the [`[DoesNotReturn]` attribute](/en-us/dotnet/api/system.diagnostics.codeanalysis.doesnotreturnattribute). The .NET 10 or later Blazor Web App project template sets the `<BlazorDisableThrowNavigationException>` MSBuild property to `true` in the app's project file in order to avoid throwing the exception during static SSR. If an app based on the project template from a prior release of .NET is updated to .NET 10 or later and includes the `<BlazorDisableThrowNavigationException>` MSBuild property set to `true`, make the following changes. For more information, see [What's new in ASP.NET Core in .NET 10](../release-notes/aspnetcore-10.0?view=aspnetcore-10.0#opt-in-to-avoiding-a-navigationexception-during-static-server-side-rendering-with-navigationmanagernavigateto).

In `Components/Account/IdentityRedirectManager.cs`:

* Remove the [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception) from the `RedirectTo` method:

  ```
  - throw new InvalidOperationException(
  -     $"{nameof(IdentityRedirectManager)} can only be used during static rendering.");
  ```

* Remove five instances of the `[DoesNotReturn]` attribute from the file:

  ```
  - [DoesNotReturn]
  ```

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-04-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/class-libraries-best-practices
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/hybrid/class-libraries-best-practices.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Combine Blazor Hybrid with .NET MAUI and Razor class libraries (RCLs)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains the recommended pattern for creating [Razor class libraries (RCLs)](../components/class-libraries?view=aspnetcore-10.0) that combine .NET MAUI and Razor functionality. The architecture explained in this article is adopted by the [.NET MAUI Blazor Hybrid and Web solution template](tutorials/maui-blazor-web-app?view=aspnetcore-10.0).

RCLs should adopt *host-agnostic design*, where libraries are reusable UI component packages that work across different platforms, app types, Blazor hosting models (Blazor Server, Blazor WebAssembly, Blazor Hybrid/MAUI), and static server-side rendering. To maintain this flexibility, RCLs shouldn't depend on specific hosting infrastructure or platform APIs:

* **Universal component reuse**: Components work in web apps, desktop apps, and mobile apps.
* **Clean architecture**: UI concerns are separated from platform-specific implementation details.
* **Testability**: Components are tested without platform dependencies.
* **Future-proof**: Components adapt to new hosting models without modification.

These goals are accomplished by:

* Keeping Razor components platform-agnostic in the RCL.
* Providing platform-specific implementations through dependency injection (DI).

## Key advantages

*Host-agnostic design* has the following advantages:

* Host independence

  * The RCL works across Blazor Server, Blazor WebAssembly, .NET MAUI, and other hosting models.
  * The components are the same but have different platform implementations.
  * There's no coupling to specific hosting infrastructure.

* Clean dependency flow

  * The RCL defines abstractions (interfaces).
  * The MAUI library implements abstractions using MAUI APIs.
  * The app activates implementations via DI.
  * There are no circular dependencies.

* Testability

  * The RCL can be tested independently without MAUI dependencies.
  * Implementations of `IDeviceInfoService` are mocked for unit testing.
  * MAUI library tests focus on platform-specific logic.

* Maximum reusability

  * The RCL works seamlessly in:

    * MAUI Blazor apps (`Lib.Maui` provides native device APIs).
    * Blazor Web Apps (web-based `IDeviceInfoService` uses browser APIs).
    * Blazor Server (server-side implementations).
    * Blazor WebAssembly (client-side implementations).

  * A single codebase serves all platforms.

## Best practices

* Keep the RCL platform-agnostic.

  * No MAUI dependencies are in the RCL.
  * Define abstractions for all platform-specific functionality.
  * Use dependency injection (DI) to provide implementations.

* The MAUI library references RCL. The RCL doesn't reference the MAUI library:

  * Dependency flow: App → MAUI Library → RCL.
  * Never reference the MAUI library from RCL.

* Use interfaces for abstractions.

  * Prefer interfaces over abstract classes for flexibility.
  * Document expected behavior in XML comments.
  * Consider asynchronous methods for I/O operations.

* Register services appropriately.

  * Use singleton services for stateless services, such as device info and connectivity.
  * Use scoped services for user-specific services.
  * Use transient services for stateful operations.

* Test independently

  * Unit test RCL components with mocked interfaces.
  * Integration test MAUI implementations on devices/emulators.
  * Use DI to swap implementations for testing.

## Architecture

The following sections demonstrate how to adopt the following example app architecture:

* `Lib` (RCL)

  * Razor components
  * Static web assets
  * Abstractions (interfaces) for platform functionality

* `Lib.Maui` (MAUI Class Library)

  * References `Lib`
  * MAUI-specific implementations
  * Uses MAUI APIs (examples: `DeviceInfo`, `Permissions`)

* `MauiApp` (MAUI Blazor Application)

  * References both `Lib` and `Lib.Maui`
  * Registers MAUI implementations in the DI container

Project structure summary:

```
MauiClassLibrarySample.sln
├── Lib/                          # RCL
│   ├── Lib.csproj                # Microsoft.NET.Sdk.Razor, net10.0
│   ├── IDeviceInfoService.cs       # Platform abstraction
│   ├── Component1.razor            # Razor component using abstraction
│   └── wwwroot/
│       └── lib.js                # JS initializer
│
├── Lib.Maui/                     # MAUI Class Library
│   ├── Lib.Maui.csproj           # Microsoft.NET.Sdk, multi-targeted, UseMaui=true
│   └── MauiDeviceInfoService.cs    # MAUI implementation of IDeviceInfoService
│
└── MauiApp/                      # MAUI Blazor Application
    ├── MauiApp.csproj            # References both Lib and Lib.Maui
    ├── MauiProgram.cs              # Registers services
    └── Components/Pages/
        └── Home.razor              # Uses Component1 from RCL
```

## Step 1: Create the RCL

Create a RCL *without* `<UseMaui>`:

```
dotnet new razorclasslib -o Lib
```

Key characteristics:

* Uses the `Microsoft.NET.Sdk.Razor` SDK.
* Targets `net10.0` (or your target framework).
* No MAUI dependencies.
* Contains only Razor components and web assets.

### Define platform abstractions

In `Lib/IDeviceInfoService.cs`:

```
namespace Lib;

/// <summary>
/// Abstraction for retrieving device-specific information.
/// This interface should be implemented by platform-specific libraries (e.g., MAUI).
/// </summary>
public interface IDeviceInfoService
{
    /// <summary>
    /// Gets the name of the platform (e.g., "Android", "iOS", "Windows").
    /// </summary>
    string Platform { get; }

    /// <summary>
    /// Gets the device model (e.g., "iPhone 14 Pro", "Samsung Galaxy S23").
    /// </summary>
    string DeviceModel { get; }

    /// <summary>
    /// Gets the operating system version.
    /// </summary>
    string OSVersion { get; }
}
```

### Create components using abstractions

In `Lib/Component1.razor`:

```
@inject IDeviceInfoService DeviceInfo

<div class="component">
    <h3>Device Information</h3>
    <p><strong>Platform:</strong> @DeviceInfo.Platform</p>
    <p><strong>Device Model:</strong> @DeviceInfo.DeviceModel</p>
    <p><strong>OS Version:</strong> @DeviceInfo.OSVersion</p>
</div>
```

## Step 2: Create the MAUI class library

Create a MAUI class library with `<UseMaui>true</UseMaui>`:

```
dotnet new classlib -o Lib.Maui
```

### Configure as a MAUI class library

Edit `Lib.Maui/Lib.Maui.csproj`:

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

  <PropertyGroup>
    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
    <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
    <UseMaui>true</UseMaui>
    <SingleProject>true</SingleProject>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
    <TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Lib\Lib.csproj" />
  </ItemGroup>

</Project>
```

Key properties:

* `<UseMaui>true</UseMaui>` enables MAUI functionality.
* `<SingleProject>true</SingleProject>` uses single-project MAUI structure.
* Multi-targets MAUI platforms (Android, iOS, macOS, Windows).
* References the `Lib` RCL.

### Implement platform-specific functionality

In `Lib.Maui/MauiDeviceInfoService.cs`:

```
using Lib;

namespace Lib.Maui;

/// <summary>
/// MAUI-specific implementation of IDeviceInfoService.
/// Uses Microsoft.Maui.Devices.DeviceInfo to retrieve platform information.
/// </summary>
public class MauiDeviceInfoService : IDeviceInfoService
{
    public string Platform => Microsoft.Maui.Devices.DeviceInfo.Platform.ToString();

    public string DeviceModel => Microsoft.Maui.Devices.DeviceInfo.Model;

    public string OSVersion => Microsoft.Maui.Devices.DeviceInfo.VersionString;
}
```

## Step 3: Create the MAUI Blazor app

Create a MAUI Blazor app to consume both libraries:

```
dotnet new maui-blazor -o MauiApp
```

### Add library references

Add `Lib` and `Lib.Maui` project references to the `MauiApp` project:

```
<ItemGroup>
  <ProjectReference Include="..\Lib\Lib.csproj" />
  <ProjectReference Include="..\Lib.Maui\Lib.Maui.csproj" />
</ItemGroup>
```

### Register services

In `MauiApp/MauiProgram.cs`, register the MAUI implementation:

```
using Microsoft.Extensions.Logging;
using Lib;
using Lib.Maui;

namespace MauiApp;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        builder.Services.AddMauiBlazorWebView();

        // Register the MAUI-specific implementation of IDeviceInfoService
        builder.Services.AddSingleton<IDeviceInfoService, MauiDeviceInfoService>();

#if DEBUG
        builder.Services.AddBlazorWebViewDeveloperTools();
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}
```

### Use RCL components

In `MauiApp/Components/Pages/Home.razor`:

```
@page "/"
@using Lib

<h1>Hello, world!</h1>

<p>Welcome to your new app demonstrating the RCL + MAUI library pattern.</p>

<h2>Device Information Component</h2>

<p>
    This component is defined in the Lib RCL and uses the IDeviceInfoService 
    abstraction, which is implemented by Lib.Maui.
</p>

<Component1 />
```

## Step 4: Build and run

Create a solution file and build:

```
dotnet new sln -n MauiClassLibrarySample
dotnet sln add Lib\Lib.csproj Lib.Maui\Lib.Maui.csproj MauiApp\MauiApp.csproj
dotnet build
```

## When to Use MAUI APIs

Use abstractions in the RCL for any functionality that requires MAUI APIs.

### Common Scenarios

| Functionality      | Pattern                                                                           |
| ------------------ | --------------------------------------------------------------------------------- |
| Device information | Define `IDeviceInfoService` in the RCL, implement in MAUI library.                |
| Permissions        | Define `IPermissionsService` in the RCL, use `Permissions` API in MAUI library.   |
| File system access | Define `IFileService` in the RCL, use `FileSystem` API in MAUI library.           |
| Connectivity       | Define `IConnectivityService` in the RCL, use `Connectivity` API in MAUI library. |
| Geolocation        | Define `ILocationService` in the RCL, use `Geolocation` API in MAUI library.      |

### Example: Adding permissions service

In the RCL (`Lib`):

```
public interface IPermissionsService
{
    Task<bool> CheckCameraPermissionAsync();
    Task<bool> RequestCameraPermissionAsync();
}
```

In the MAUI Library (`Lib.Maui`):

```
public class MauiPermissionsService : IPermissionsService
{
    public async Task<bool> CheckCameraPermissionAsync()
    {
        var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
        return status == PermissionStatus.Granted;
    }

    public async Task<bool> RequestCameraPermissionAsync()
    {
        var status = await Permissions.RequestAsync<Permissions.Camera>();
        return status == PermissionStatus.Granted;
    }
}
```

In the app (`MauiApp`):

```
builder.Services.AddSingleton<IPermissionsService, MauiPermissionsService>();
```

## Troubleshoot

### MA002 warnings about implicit package references

**Symptom:** Build warnings suggesting to add explicit `<PackageReference>` for MAUI packages.

**Solution:** These warnings are informational. The packages are added implicitly when `<UseMaui>true</UseMaui>` is set.

### Dependency injection fails

**Symptom:** `InvalidOperationException: Unable to resolve service for type 'Lib.IDeviceInfoService'`

**Solution:** Register the service in `MauiProgram.cs`:

```
builder.Services.AddSingleton<IDeviceInfoService, MauiDeviceInfoService>();
```

## Related content

* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../components/class-libraries?view=aspnetcore-10.0)
* [.NET MAUI Class Libraries](/en-us/dotnet/maui/platform-integration/)
* [.NET MAUI Dependency Injection](/en-us/dotnet/maui/fundamentals/dependency-injection)
* [Reuse Razor components in ASP.NET Core Blazor Hybrid apps](reuse-razor-components?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/model-binding.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Model Binding in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains what model binding is, how it works, and how to customize its behavior.

## What is Model binding

Controllers and Razor pages work with data that comes from HTTP requests. For example, route data may provide a record key, and posted form fields may provide values for the properties of the model. Writing code to retrieve each of these values and convert them from strings to .NET types would be tedious and error-prone. Model binding automates this process. The model binding system:

* Retrieves data from various sources such as route data, form fields, and query strings.
* Provides the data to controllers and Razor pages in method parameters and `public` properties.
* Converts string data to .NET types.
* Updates properties of complex types.

## Example

Suppose you have the following action method:

```
[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)
```

And the app receives a request with this URL:

```
https://contoso.com/api/pets/2?DogsOnly=true
```

Model binding goes through the following steps after the routing system selects the action method:

* Finds the first parameter of `GetById`, an integer named `id`.
* Looks through the available sources in the HTTP request and finds `id` = "2" in route data.
* Converts the string "2" into integer 2.
* Finds the next parameter of `GetById`, a boolean named `dogsOnly`.
* Looks through the sources and finds "DogsOnly=true" in the query string. Name matching is not case-sensitive.
* Converts the string "true" into boolean `true`.

The framework then calls the `GetById` method, passing in 2 for the `id` parameter, and `true` for the `dogsOnly` parameter.

In the preceding example, the model binding targets are method parameters that are [simple](#simp-comp7) types. Targets may also be the properties of a complex type. After each property is successfully bound, [model validation](validation?view=aspnetcore-10.0) occurs for that property. The record of what data is bound to the model, and any binding or validation errors, is stored in [ControllerBase.ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.modelstate#microsoft-aspnetcore-mvc-controllerbase-modelstate) or [PageModel.ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.modelstate#microsoft-aspnetcore-mvc-controllerbase-modelstate). To find out if this process was successful, the app checks the [ModelState.IsValid](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.isvalid#microsoft-aspnetcore-mvc-modelbinding-modelstatedictionary-isvalid) flag.

## Targets

Model binding tries to find values for the following kinds of targets:

* Parameters of the controller action method that a request is routed to.
* Parameters of the Razor Pages handler method that a request is routed to.
* Public (`public`) properties of a controller or `PageModel` class, if specified by attributes.

### \[BindProperty] attribute

Can be applied to a `public` property of a controller or `PageModel` class to cause model binding to target that property:

```
public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}
```

### \[BindProperties] attribute

Can be applied to a controller or `PageModel` class to tell model binding to target all `public` properties of the class:

```
[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}
```

### Model binding for HTTP GET requests

By default, properties are not bound for HTTP GET requests. Typically, all you need for a GET request is a record ID parameter. The record ID is used to look up the item in the database. Therefore, there is no need to bind a property that holds an instance of the model. In scenarios where you do want properties bound to data from GET requests, set the `SupportsGet` property to `true`:

```
[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }
```

[]()

## Model binding simple and complex types

Model binding uses specific definitions for the types it operates on. A *simple type* is converted from a single string using [TypeConverter](/en-us/dotnet/api/system.componentmodel.typeconverter) or a `TryParse` method. A *complex type* is converted from multiple input values. The framework determines the difference based on the existence of a `TypeConverter` or `TryParse`. We recommend creating a type converter or using `TryParse` for a `string` to `SomeType` conversion that doesn't require external resources or multiple inputs.

## Sources

By default, model binding gets data in the form of key-value pairs from the following sources in an HTTP request:

1. Form fields
2. The request body (For [controllers that have the \[ApiController\] attribute](../../web-api/?view=aspnetcore-10.0#binding-source-parameter-inference).)
3. Route data
4. Query string parameters
5. Uploaded files

For each target parameter or property, the sources are scanned in the order indicated in the preceding list. There are a few exceptions:

* Route data and query string values are used only for [simple](#simp-comp7) types.
* Uploaded files are bound only to target types that implement `IFormFile` or `IEnumerable<IFormFile>`.

If the default source is not correct, use one of the following attributes to specify the source:

* [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute) - Gets values from the query string.
* [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute) - Gets values from route data.
* [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) - Gets values from posted form fields.
* [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) - Gets values from the request body.
* [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute) - Gets values from HTTP headers.

Note

[C# union types](/en-us/dotnet/csharp/language-reference/builtin-types/union) are supported only with `[FromBody]`. The other binding sources—`[FromQuery]`, `[FromRoute]`, `[FromForm]`, and `[FromHeader]`—bind string values without JSON parsing, so they can't dispatch to a union case.

These attributes:

* Are added to model properties individually and not to the model class, as in the following example:

  ```
  public class Instructor
  {
      public int Id { get; set; }

      [FromQuery(Name = "Note")]
      public string? NoteFromQueryString { get; set; }

      // ...
  }
  ```

* Optionally accept a model name value in the constructor. This option is provided in case the property name doesn't match the value in the request. For instance, the value in the request might be a header with a hyphen in its name, as in the following example:

  ```
  public void OnGet([FromHeader(Name = "Accept-Language")] string language)
  ```

### \[FromBody] attribute

Apply the `[FromBody]` attribute to a parameter to populate its properties from the body of an HTTP request. The ASP.NET Core runtime delegates the responsibility of reading the body to an input formatter. Input formatters are explained [later in this article](#input-formatters).

When `[FromBody]` is applied to a complex type parameter, any binding source attributes applied to its properties are ignored. For example, the following `Create` action specifies that its `pet` parameter is populated from the body:

```
public ActionResult<Pet> Create([FromBody] Pet pet)
```

The `Pet` class specifies that its `Breed` property is populated from a query string parameter:

```
public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}
```

In the preceding example:

* The `[FromQuery]` attribute is ignored.
* The `Breed` property is not populated from a query string parameter.

Input formatters read only the body and don't understand binding source attributes. If a suitable value is found in the body, that value is used to populate the `Breed` property.

Don't apply `[FromBody]` to more than one parameter per action method. Once the request stream is read by an input formatter, it's no longer available to be read again for binding other `[FromBody]` parameters.

### Additional sources

Source data is provided to the model binding system by *value providers*. You can write and register custom value providers that get data for model binding from other sources. For example, you might want data from cookies or session state. To get data from a new source:

* Create a class that implements `IValueProvider`.
* Create a class that implements `IValueProviderFactory`.
* Register the factory class in `Program.cs`.

The sample includes a [value provider](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/model-binding/samples/6.x/ModelBindingSample/CookieValueProvider.cs) and [factory](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/model-binding/samples/6.x/ModelBindingSample/CookieValueProviderFactory.cs) example that gets values from cookies. Register custom value provider factories in `Program.cs`:

```
builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});
```

The preceding code puts the custom value provider after all built-in value providers. To make it the first in the list, call `Insert(0, new CookieValueProviderFactory())` instead of `Add`.

## No source for a model property

By default, a model state error isn't created if no value is found for a model property. The property is set to null or a default value:

* Nullable [simple](#simp-comp7) types are set to `null`.
* Non-nullable value types are set to `default(T)`. For example, a parameter `int id` is set to 0.
* For complex Types, model binding creates an instance by using the default constructor, without setting properties.
* Arrays are set to `Array.Empty<T>()`, except that `byte[]` arrays are set to `null`.

If model state should be invalidated when nothing is found in form fields for a model property, use the [`[BindRequired]`](#bindrequired-attribute) attribute.

Note that this `[BindRequired]` behavior applies to model binding from posted form data, not from JSON or XML data in a request body. Request body data is handled by [input formatters](#input-formatters).

## Type conversion errors

If a source is found but can't be converted into the target type, model state is flagged as invalid. The target parameter or property is set to null or a default value, as noted in the previous section.

In an API controller that has the `[ApiController]` attribute, invalid model state results in an automatic HTTP 400 response.

In a Razor page, redisplay the page with an error message:

```
public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}
```

When the page is redisplayed by the preceding code, the invalid input isn't shown in the form field. This is because the model property has been set to null or a default value. The invalid input does appear in an error message. If you want to redisplay the bad data in the form field, consider making the model property a string and doing the data conversion manually.

The same strategy is recommended if you don't want type conversion errors to result in model state errors. In that case, make the model property a string.

[]()

## Simple types

See [Model binding simple and complex types](#simp-comp7) for explanation of simple and complex types.

The simple types that the model binder can convert source strings into include the following:

* [Boolean](/en-us/dotnet/api/system.componentmodel.booleanconverter)
* [Byte](/en-us/dotnet/api/system.componentmodel.byteconverter), [SByte](/en-us/dotnet/api/system.componentmodel.sbyteconverter)
* [Char](/en-us/dotnet/api/system.componentmodel.charconverter)
* [DateOnly](/en-us/dotnet/api/system.componentmodel.dateonlyconverter)
* [DateTime](/en-us/dotnet/api/system.componentmodel.datetimeconverter)
* [DateTimeOffset](/en-us/dotnet/api/system.componentmodel.datetimeoffsetconverter)
* [Decimal](/en-us/dotnet/api/system.componentmodel.decimalconverter)
* [Double](/en-us/dotnet/api/system.componentmodel.doubleconverter)
* [Enum](/en-us/dotnet/api/system.componentmodel.enumconverter)
* [Guid](/en-us/dotnet/api/system.componentmodel.guidconverter)
* [Int16](/en-us/dotnet/api/system.componentmodel.int16converter), [Int32](/en-us/dotnet/api/system.componentmodel.int32converter), [Int64](/en-us/dotnet/api/system.componentmodel.int64converter)
* [Single](/en-us/dotnet/api/system.componentmodel.singleconverter)
* [TimeOnly](/en-us/dotnet/api/system.componentmodel.timeonlyconverter)
* [TimeSpan](/en-us/dotnet/api/system.componentmodel.timespanconverter)
* [UInt16](/en-us/dotnet/api/system.componentmodel.uint16converter), [UInt32](/en-us/dotnet/api/system.componentmodel.uint32converter), [UInt64](/en-us/dotnet/api/system.componentmodel.uint64converter)
* [Uri](/en-us/dotnet/api/system.uritypeconverter)
* [Version](/en-us/dotnet/api/system.componentmodel.versionconverter)

[]()

## Bind with `IParsable<T>.TryParse`

The [`IParsable<TSelf>.TryParse`](/en-us/dotnet/api/system.iparsable-1.tryparse#system-iparsable-1-tryparse\(system-string-system-iformatprovider-0@\)) API supports binding controller action parameter values:

```
public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);
```

The following `DateRange` class implements [`IParsable<TSelf>`](/en-us/dotnet/api/system.iparsable-1) to support binding a date range:

```
public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}
```

The preceding code:

* Converts a string representing two dates to a `DateRange` object
* The model binder uses the [`IParsable<TSelf>.TryParse`](/en-us/dotnet/api/system.iparsable-1.tryparse#system-iparsable-1-tryparse\(system-string-system-iformatprovider-0@\)) method to bind the `DateRange`.

The following controller action uses the `DateRange` class to bind a date range:

```
// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}
```

The following `Locale` class implements [`IParsable<TSelf>`](/en-us/dotnet/api/system.iparsable-1) to support binding to `CultureInfo`:

```
public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}
```

The following controller action uses the `Locale` class to bind a `CultureInfo` string:

```
// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}
```

The following controller action uses the `DateRange` and `Locale` classes to bind a date range with `CultureInfo`:

```
// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}
```

The [API sample app on GitHub](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/bind-tryparse/samples/7.0/BindUsingTryParse) shows the preceding sample for an API controller.

### Bind with `TryParse`

The `TryParse` API supports binding controller action parameter values:

```
public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);
```

[`IParsable<T>.TryParse`](#itp7) is the recommended approach for parameter binding because unlike `TryParse`, it doesn't depend on reflection.

The following `DateRangeTP` class implements `TryParse`:

```
public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}
```

The following controller action uses the `DateRangeTP` class to bind a date range:

```
// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}
```

## Complex types

A complex type must have a public default constructor and public writable properties to bind. When model binding occurs, the class is instantiated using the public default constructor.

Structs and other value types, including `record struct` types, aren't supported as complex type binding targets. Reflection doesn't report an implicit parameterless constructor for a struct, so model binding can't create the model instance. Value types also have copy-by-value semantics, which prevents the binder from setting properties on an instance after it's created. Binding a struct as a complex type throws an [InvalidOperationException](/en-us/dotnet/api/system.invalidoperationexception). Use a class or a `record` class instead. A struct can still be bound as a [simple type](#simple-types) if it has a type converter or implements [`IParsable<TSelf>`](/en-us/dotnet/api/system.iparsable-1), which is how types such as [Guid](/en-us/dotnet/api/system.guid) and [DateTime](/en-us/dotnet/api/system.datetime) are bound.

For each property of the complex type, [model binding looks through the sources for the name pattern](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/ParameterBinder.cs#L115-L130) *prefix.property\_name*. If nothing is found, it looks for just *property\_name* without the prefix. The decision to use the prefix isn't made per property. For example, with a query containing `?Instructor.Id=100&Name=foo`, bound to method `OnGet(Instructor instructor)`, the resulting object of type `Instructor` contains:

* `Id` set to `100`.
* `Name` set to `null`. Model binding expects `Instructor.Name` because `Instructor.Id` was used in the preceding query parameter.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

For binding to a parameter, the prefix is the parameter name. For binding to a `PageModel` `public` property, the prefix is the property name. Some attributes have a `Prefix` property that lets you override the default usage of parameter or property name.

For example, suppose the complex type is the following `Instructor` class:

```
public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}
```

### Prefix = parameter name

If the model to be bound is a parameter named `instructorToUpdate`:

```
public IActionResult OnPost(int? id, Instructor instructorToUpdate)
```

Model binding starts by looking through the sources for the key `instructorToUpdate.ID`. If that isn't found, it looks for `ID` without a prefix.

### Prefix = property name

If the model to be bound is a property named `Instructor` of the controller or `PageModel` class:

```
[BindProperty]
public Instructor Instructor { get; set; }
```

Model binding starts by looking through the sources for the key `Instructor.ID`. If that isn't found, it looks for `ID` without a prefix.

### Custom prefix

If the model to be bound is a parameter named `instructorToUpdate` and a `Bind` attribute specifies `Instructor` as the prefix:

```
public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)
```

Model binding starts by looking through the sources for the key `Instructor.ID`. If that isn't found, it looks for `ID` without a prefix.

### Attributes for complex type targets

Several built-in attributes are available for controlling model binding of complex types:

* [`[Bind]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindattribute)
* [`[BindRequired]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindrequiredattribute)
* [`[BindNever]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindneverattribute)

Warning

These attributes affect model binding when posted form data is the source of values. They do ***not*** affect input formatters, which process posted JSON and XML request bodies. Input formatters are explained [later in this article](#input-formatters).

### \[Bind] attribute

Can be applied to a class or a method parameter. Specifies which properties of a model should be included in model binding. `[Bind]` does ***not*** affect input formatters.

In the following example, only the specified properties of the `Instructor` model are bound when any handler or action method is called:

```
[Bind("LastName,FirstMidName,HireDate")]
public class Instructor
```

In the following example, only the specified properties of the `Instructor` model are bound when the `OnPost` method is called:

```
[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)
```

The `[Bind]` attribute can be used to protect against overposting in *create* scenarios. It doesn't work well in edit scenarios because excluded properties are set to null or a default value instead of being left unchanged. For protection against overposting, view models are recommended rather than the `[Bind]` attribute. For more information, see [Security note about overposting](../../data/ef-mvc/crud?view=aspnetcore-10.0#security-note-about-overposting).

### \[ModelBinder] attribute

[ModelBinderAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinderattribute) can be applied to types, properties, or parameters. It allows specifying the type of model binder used to bind the specific instance or type. For example:

```
[HttpPost]
public IActionResult OnPost(
    [ModelBinder<MyInstructorModelBinder>] Instructor instructor)
```

The `[ModelBinder]` attribute can also be used to change the name of a property or parameter when it's being model bound:

```
public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}
```

### \[BindRequired] attribute

Causes model binding to add a model state error if binding cannot occur for a model's property. Here's an example:

```
public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}
```

See also the discussion of the `[Required]` attribute in [Model validation](validation?view=aspnetcore-10.0#required-attribute).

### \[BindNever] attribute

Can be applied to a property or a type. Prevents model binding from setting a model's property. When applied to a type, the model binding system excludes all properties the type defines. Here's an example:

```
public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}
```

## Collections

For targets that are collections of simple types, model binding looks for matches to *parameter\_name* or *property\_name*. If no match is found, it looks for one of the supported formats without the prefix. For example:

* Suppose the parameter to be bound is an array named `selectedCourses`:

  ```
  public IActionResult OnPost(int? id, int[] selectedCourses)
  ```

* Form or query string data can be in one of the following formats:

  ```
  selectedCourses=1050&selectedCourses=2000 
  ```

  ```
  selectedCourses[0]=1050&selectedCourses[1]=2000
  ```

  ```
  [0]=1050&[1]=2000
  ```

  ```
  selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
  ```

  ```
  [a]=1050&[b]=2000&index=a&index=b
  ```

  Avoid binding a parameter or a property named `index` or `Index` if it is adjacent to a collection value. Model binding attempts to use `index` as the index for the collection which might result in incorrect binding. For example, consider the following action:

  ```
  public IActionResult Post(string index, List<Product> products)
  ```

  In the preceding code, the `index` query string parameter binds to the `index` method parameter and also is used to bind the product collection. Renaming the `index` parameter or using a model binding attribute to configure binding avoids this issue:

  ```
  public IActionResult Post(string productIndex, List<Product> products)
  ```

* The following format is supported only in form data:

  ```
  selectedCourses[]=1050&selectedCourses[]=2000
  ```

* For all of the preceding example formats, model binding passes an array of two items to the `selectedCourses` parameter:

  * selectedCourses\[0]=1050
  * selectedCourses\[1]=2000

  Data formats that use subscript numbers (... \[0] ... \[1] ...) must ensure that they are numbered sequentially starting at zero. If there are any gaps in subscript numbering, all items after the gap are ignored. For example, if the subscripts are 0 and 2 instead of 0 and 1, the second item is ignored.

## Dictionaries

For `Dictionary` targets, model binding looks for matches to *parameter\_name* or *property\_name*. If no match is found, it looks for one of the supported formats without the prefix. For example:

* Suppose the target parameter is a `Dictionary<int, string>` named `selectedCourses`:

  ```
  public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
  ```

* The posted form or query string data can look like one of the following examples:

  ```
  selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
  ```

  ```
  [1050]=Chemistry&selectedCourses[2000]=Economics
  ```

  ```
  selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
  selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
  ```

  ```
  [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
  ```

* For all of the preceding example formats, model binding passes a dictionary of two items to the `selectedCourses` parameter:

  * selectedCourses\["1050"]="Chemistry"
  * selectedCourses\["2000"]="Economics"

## Constructor binding and record types

Model binding requires that complex types have a parameterless constructor. Both `System.Text.Json` and `Newtonsoft.Json` based input formatters support deserialization of classes that don't have a parameterless constructor.

Record types are a great way to succinctly represent data over the network. ASP.NET Core supports model binding and validating record types with a single constructor:

```
public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}
```

`Person/Index.cshtml`:

```
@model Person

<label>Name: <input asp-for="Name" /></label>
<br />
<label>Age: <input asp-for="Age" /></label>
```

When validating record types, the runtime searches for binding and validation metadata specifically on parameters rather than on properties.

The framework allows binding to and validating record types:

```
public record Person([Required] string Name, [Range(0, 100)] int Age);
```

For the preceding to work, the type must:

* Be a record type.
* Have exactly one public constructor.
* Contain parameters that have a property with the same name and type. The names must not differ by case.

### POCOs without parameterless constructors

POCOs that do not have parameterless constructors can't be bound.

The following code results in an exception saying that the type must have a parameterless constructor:

```
public class Person {
    public Person(string Name) { }
}
public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0)
    {
    }
}
```

### Record types with manually authored constructors

Record types with manually authored constructors that look like primary constructors work

```
public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}
```

### Record types, validation and binding metadata

For record types, validation and binding metadata on parameters is used. Any metadata on properties is ignored

```
public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}
```

### Validation and metadata

Validation uses metadata on the parameter but uses the property to read the value. In the ordinary case with primary constructors, the two would be identical. However, there are ways to defeat it:

```
public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}
```

### TryUpdateModel does not update parameters on a record type

```
public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);
```

In this case, MVC will not attempt to bind `Name` again. However, `Age` is allowed to be updated

[]()

## Globalization behavior of model binding route data and query strings

The ASP.NET Core route value provider and query string value provider:

* Treat values as invariant culture.
* Expect that URLs are culture-invariant.

In contrast, values coming from form data undergo a culture-sensitive conversion. This is by design so that URLs are shareable across locales.

To make the ASP.NET Core route value provider and query string value provider undergo a culture-sensitive conversion:

* Inherit from [IValueProviderFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.ivalueproviderfactory)
* Copy the code from [QueryStringValueProviderFactory](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/QueryStringValueProviderFactory.cs) or [RouteValueValueProviderFactory](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/RouteValueProviderFactory.cs)
* Replace the [culture value](https://github.com/dotnet/AspNetCore/blob/e625fe29b049c60242e8048b4ea743cca65aa7b5/src/Mvc/Mvc.Core/src/ModelBinding/QueryStringValueProviderFactory.cs#L30) passed to the value provider constructor with [CultureInfo.CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture)
* Replace the default value provider factory in MVC options with your new one:

```
public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
```

```
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});
```

## Special data types

There are some special data types that model binding can handle.

### IFormFile and IFormFileCollection

An uploaded file included in the HTTP request. Also supported is `IEnumerable<IFormFile>` for multiple files.

### CancellationToken

Actions can optionally bind a `CancellationToken` as a parameter. This binds [RequestAborted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted) that signals when the connection underlying the HTTP request is aborted. Actions can use this parameter to cancel long running async operations that are executed as part of the controller actions.

### FormCollection

Used to retrieve all the values from posted form data.

## Input formatters

Data in the request body can be in JSON, XML, or some other format. To parse this data, model binding uses an *input formatter* that is configured to handle a particular content type. By default, ASP.NET Core includes JSON based input formatters for handling JSON data using [`System.Text.Json`](/en-us/dotnet/standard/serialization/system-text-json-overview). You can add other formatters for other content types.

The default JSON input formatter can be configured using the `AddJsonOptions` method:

```
builder.Services.AddControllers().AddJsonOptions(options =>
{
    // Configure property naming policy (camelCase)
    options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;

    // Add enum converter to serialize enums as strings
    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());

    // Configure other JSON options
    options.JsonSerializerOptions.WriteIndented = true;
    options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
```

Common configuration options include:

* **Property naming policy** - Configure camelCase or other naming conventions
* **Enum converters** - Handle enum serialization as strings
* **Custom converters** - Add type-specific serialization logic

ASP.NET Core selects input formatters based on the [Consumes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute. If no attribute is present, it uses the [Content-Type header](https://www.w3.org/Protocols/rfc1341/4_Content-Type.html).

To use the built-in XML input formatters:

* In `Program.cs`, call [AddXmlSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvccorebuilderextensions.addxmlserializerformatters) or [AddXmlDataContractSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvccorebuilderextensions.addxmldatacontractserializerformatters).

  ```
  builder.Services.AddControllers()
      .AddXmlSerializerFormatters();
  ```

* Apply the `Consumes` attribute to controller classes or action methods that should expect XML in the request body.

  ```
  [HttpPost]
  [Consumes("application/xml")]
  public ActionResult<Pet> Create(Pet pet)
  ```

  For more information, see [Introducing XML Serialization](/en-us/dotnet/standard/serialization/introducing-xml-serialization).

### Customize model binding with input formatters

An input formatter takes full responsibility for reading data from the request body. To customize this process, configure the APIs used by the input formatter. This section describes how to customize the `System.Text.Json`-based input formatter to understand a custom type named `ObjectId`.

Consider the following model, which contains a custom `ObjectId` property:

```
public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}
```

To customize the model binding process when using `System.Text.Json`, create a class derived from [JsonConverter\<T>](/en-us/dotnet/api/system.text.json.serialization.jsonconverter-1):

```
internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}
```

To use a custom converter, apply the [JsonConverterAttribute](/en-us/dotnet/api/system.text.json.serialization.jsonconverterattribute) attribute to the type. In the following example, the `ObjectId` type is configured with `ObjectIdConverter` as its custom converter:

```
[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);
```

For more information, see [How to write custom converters](/en-us/dotnet/standard/serialization/system-text-json-converters-how-to).

## Exclude specified types from model binding

The model binding and validation systems' behavior is driven by [ModelMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelmetadata). You can customize `ModelMetadata` by adding a details provider to [MvcOptions.ModelMetadataDetailsProviders](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.modelmetadatadetailsproviders#microsoft-aspnetcore-mvc-mvcoptions-modelmetadatadetailsproviders). Built-in details providers are available for disabling model binding or validation for specified types.

To disable model binding on all models of a specified type, add an [ExcludeBindingMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.excludebindingmetadataprovider) in `Program.cs`. For example, to disable model binding on all models of type `System.Version`:

```
builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });
```

To disable validation on properties of a specified type, add a [SuppressChildValidationMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.suppresschildvalidationmetadataprovider) in `Program.cs`. For example, to disable validation on properties of type `System.Guid`:

```
builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });
```

## Custom model binders

You can extend model binding by writing a custom model binder and using the `[ModelBinder]` attribute to select it for a given target. Learn more about [custom model binding](../advanced/custom-model-binding?view=aspnetcore-10.0).

## Manual model binding

Model binding can be invoked manually by using the [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) method. The method is defined on both `ControllerBase` and `PageModel` classes. Method overloads let you specify the prefix and value provider to use. The method returns `false` if model binding fails. Here's an example:

```
if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();
```

[TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) uses value providers to get data from the form body, query string, and route data. `TryUpdateModelAsync` is typically:

* Used with Razor Pages and MVC apps using controllers and views to prevent over-posting.
* Not used with a web API unless consumed from form data, query strings, and route data. Web API endpoints that consume JSON use [Input formatters](#input-formatters) to deserialize the request body into an object.

For more information, see [TryUpdateModelAsync](../../data/ef-rp/crud?view=aspnetcore-10.0#TryUpdateModelAsync).

## \[FromServices] attribute

This attribute's name follows the pattern of model binding attributes that specify a data source. But it's not about binding data from a value provider. It gets an instance of a type from the [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container. Its purpose is to provide an alternative to constructor injection for when you need a service only if a particular method is called.

If an instance of the type isn't registered in the dependency injection container, the app throws an exception when attempting to bind the parameter. To make the parameter optional, use one of the following approaches:

* Make the parameter nullable.
* Set a default value for the parameter.

For nullable parameters, ensure that the parameter isn't `null` before accessing it.

## Json+PipeReader deserialization in MVC

```
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
    // previous code
}
```

### Option 3: A more complicated but better performing fix

This fix involves setting up a separate code path for the `ReadOnlySequence` handling:

```
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    if (reader.HasValueSequence)
    {
        reader.ValueSequence;
        // ReadOnlySequence optimized path
    }
    else
    {
        reader.ValueSpan;
        // ReadOnlySpan optimized path
    }
}
```

For more information, see

* [System.Text.Json.Serialization.JsonConverter](/en-us/dotnet/api/system.text.json.serialization.jsonconverter)
* [github.com/dotnet/aspnetcore/pull/62895](https://github.com/dotnet/aspnetcore/pull/62895)

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/model-binding/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Model validation in ASP.NET Core MVC](validation?view=aspnetcore-10.0)
* [Custom Model Binding in ASP.NET Core](../advanced/custom-model-binding?view=aspnetcore-10.0)

This article explains what model binding is, how it works, and how to customize its behavior.

## What is Model binding

Controllers and Razor pages work with data that comes from HTTP requests. For example, route data may provide a record key, and posted form fields may provide values for the properties of the model. Writing code to retrieve each of these values and convert them from strings to .NET types would be tedious and error-prone. Model binding automates this process. The model binding system:

* Retrieves data from various sources such as route data, form fields, and query strings.
* Provides the data to controllers and Razor pages in method parameters and `public` properties.
* Converts string data to .NET types.
* Updates properties of complex types.

## Example

Suppose you have the following action method:

```
[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)
```

And the app receives a request with this URL:

```
https://contoso.com/api/pets/2?DogsOnly=true
```

Model binding goes through the following steps after the routing system selects the action method:

* Finds the first parameter of `GetById`, an integer named `id`.
* Looks through the available sources in the HTTP request and finds `id` = "2" in route data.
* Converts the string "2" into integer 2.
* Finds the next parameter of `GetById`, a boolean named `dogsOnly`.
* Looks through the sources and finds "DogsOnly=true" in the query string. Name matching is not case-sensitive.
* Converts the string "true" into boolean `true`.

The framework then calls the `GetById` method, passing in 2 for the `id` parameter, and `true` for the `dogsOnly` parameter.

In the preceding example, the model binding targets are method parameters that are [simple](#simp-comp7) types. Targets may also be the properties of a complex type. After each property is successfully bound, [model validation](validation?view=aspnetcore-10.0) occurs for that property. The record of what data is bound to the model, and any binding or validation errors, is stored in [ControllerBase.ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.modelstate#microsoft-aspnetcore-mvc-controllerbase-modelstate) or [PageModel.ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.modelstate#microsoft-aspnetcore-mvc-controllerbase-modelstate). To find out if this process was successful, the app checks the [ModelState.IsValid](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.isvalid#microsoft-aspnetcore-mvc-modelbinding-modelstatedictionary-isvalid) flag.

## Targets

Model binding tries to find values for the following kinds of targets:

* Parameters of the controller action method that a request is routed to.
* Parameters of the Razor Pages handler method that a request is routed to.
* Public (`public`) properties of a controller or `PageModel` class, if specified by attributes.

### \[BindProperty] attribute

Can be applied to a `public` property of a controller or `PageModel` class to cause model binding to target that property:

```
public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}
```

### \[BindProperties] attribute

Can be applied to a controller or `PageModel` class to tell model binding to target all `public` properties of the class:

```
[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}
```

### Model binding for HTTP GET requests

By default, properties are not bound for HTTP GET requests. Typically, all you need for a GET request is a record ID parameter. The record ID is used to look up the item in the database. Therefore, there is no need to bind a property that holds an instance of the model. In scenarios where you do want properties bound to data from GET requests, set the `SupportsGet` property to `true`:

```
[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }
```

[]()

## Model binding simple and complex types

Model binding uses specific definitions for the types it operates on. A *simple type* is converted from a single string using [TypeConverter](/en-us/dotnet/api/system.componentmodel.typeconverter) or a `TryParse` method. A *complex type* is converted from multiple input values. The framework determines the difference based on the existence of a `TypeConverter` or `TryParse`. We recommend creating a type converter or using `TryParse` for a `string` to `SomeType` conversion that doesn't require external resources or multiple inputs.

## Sources

By default, model binding gets data in the form of key-value pairs from the following sources in an HTTP request:

1. Form fields
2. The request body (For [controllers that have the \[ApiController\] attribute](../../web-api/?view=aspnetcore-10.0#binding-source-parameter-inference).)
3. Route data
4. Query string parameters
5. Uploaded files

For each target parameter or property, the sources are scanned in the order indicated in the preceding list. There are a few exceptions:

* Route data and query string values are used only for [simple](#simp-comp7) types.
* Uploaded files are bound only to target types that implement `IFormFile` or `IEnumerable<IFormFile>`.

If the default source is not correct, use one of the following attributes to specify the source:

* [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute) - Gets values from the query string.
* [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute) - Gets values from route data.
* [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) - Gets values from posted form fields.
* [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) - Gets values from the request body.
* [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute) - Gets values from HTTP headers.

These attributes:

* Are added to model properties individually and not to the model class, as in the following example:

  ```
  public class Instructor
  {
      public int Id { get; set; }

      [FromQuery(Name = "Note")]
      public string? NoteFromQueryString { get; set; }

      // ...
  }
  ```

* Optionally accept a model name value in the constructor. This option is provided in case the property name doesn't match the value in the request. For instance, the value in the request might be a header with a hyphen in its name, as in the following example:

  ```
  public void OnGet([FromHeader(Name = "Accept-Language")] string language)
  ```

### \[FromBody] attribute

Apply the `[FromBody]` attribute to a parameter to populate its properties from the body of an HTTP request. The ASP.NET Core runtime delegates the responsibility of reading the body to an input formatter. Input formatters are explained [later in this article](#input-formatters).

When `[FromBody]` is applied to a complex type parameter, any binding source attributes applied to its properties are ignored. For example, the following `Create` action specifies that its `pet` parameter is populated from the body:

```
public ActionResult<Pet> Create([FromBody] Pet pet)
```

The `Pet` class specifies that its `Breed` property is populated from a query string parameter:

```
public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}
```

In the preceding example:

* The `[FromQuery]` attribute is ignored.
* The `Breed` property is not populated from a query string parameter.

Input formatters read only the body and don't understand binding source attributes. If a suitable value is found in the body, that value is used to populate the `Breed` property.

Don't apply `[FromBody]` to more than one parameter per action method. Once the request stream is read by an input formatter, it's no longer available to be read again for binding other `[FromBody]` parameters.

### Additional sources

Source data is provided to the model binding system by *value providers*. You can write and register custom value providers that get data for model binding from other sources. For example, you might want data from cookies or session state. To get data from a new source:

* Create a class that implements `IValueProvider`.
* Create a class that implements `IValueProviderFactory`.
* Register the factory class in `Program.cs`.

The sample includes a [value provider](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/model-binding/samples/6.x/ModelBindingSample/CookieValueProvider.cs) and [factory](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/model-binding/samples/6.x/ModelBindingSample/CookieValueProviderFactory.cs) example that gets values from cookies. Register custom value provider factories in `Program.cs`:

```
builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});
```

The preceding code puts the custom value provider after all built-in value providers. To make it the first in the list, call `Insert(0, new CookieValueProviderFactory())` instead of `Add`.

## No source for a model property

By default, a model state error isn't created if no value is found for a model property. The property is set to null or a default value:

* Nullable [simple](#simp-comp7) types are set to `null`.
* Non-nullable value types are set to `default(T)`. For example, a parameter `int id` is set to 0.
* For complex Types, model binding creates an instance by using the default constructor, without setting properties.
* Arrays are set to `Array.Empty<T>()`, except that `byte[]` arrays are set to `null`.

If model state should be invalidated when nothing is found in form fields for a model property, use the [`[BindRequired]`](#bindrequired-attribute) attribute.

Note that this `[BindRequired]` behavior applies to model binding from posted form data, not to JSON or XML data in a request body. Request body data is handled by [input formatters](#input-formatters).

## Type conversion errors

If a source is found but can't be converted into the target type, model state is flagged as invalid. The target parameter or property is set to null or a default value, as noted in the previous section.

In an API controller that has the `[ApiController]` attribute, invalid model state results in an automatic HTTP 400 response.

In a Razor page, redisplay the page with an error message:

```
public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}
```

When the page is redisplayed by the preceding code, the invalid input isn't shown in the form field. This is because the model property has been set to null or a default value. The invalid input does appear in an error message. If you want to redisplay the bad data in the form field, consider making the model property a string and doing the data conversion manually.

The same strategy is recommended if you don't want type conversion errors to result in model state errors. In that case, make the model property a string.

[]()

## Simple types

See [Model binding simple and complex types](#simp-comp7) for explanation of simple and complex types.

The simple types that the model binder can convert source strings into include the following:

* [Boolean](/en-us/dotnet/api/system.componentmodel.booleanconverter)
* [Byte](/en-us/dotnet/api/system.componentmodel.byteconverter), [SByte](/en-us/dotnet/api/system.componentmodel.sbyteconverter)
* [Char](/en-us/dotnet/api/system.componentmodel.charconverter)
* [DateOnly](/en-us/dotnet/api/system.componentmodel.dateonlyconverter)
* [DateTime](/en-us/dotnet/api/system.componentmodel.datetimeconverter)
* [DateTimeOffset](/en-us/dotnet/api/system.componentmodel.datetimeoffsetconverter)
* [Decimal](/en-us/dotnet/api/system.componentmodel.decimalconverter)
* [Double](/en-us/dotnet/api/system.componentmodel.doubleconverter)
* [Enum](/en-us/dotnet/api/system.componentmodel.enumconverter)
* [Guid](/en-us/dotnet/api/system.componentmodel.guidconverter)
* [Int16](/en-us/dotnet/api/system.componentmodel.int16converter), [Int32](/en-us/dotnet/api/system.componentmodel.int32converter), [Int64](/en-us/dotnet/api/system.componentmodel.int64converter)
* [Single](/en-us/dotnet/api/system.componentmodel.singleconverter)
* [TimeOnly](/en-us/dotnet/api/system.componentmodel.timeonlyconverter)
* [TimeSpan](/en-us/dotnet/api/system.componentmodel.timespanconverter)
* [UInt16](/en-us/dotnet/api/system.componentmodel.uint16converter), [UInt32](/en-us/dotnet/api/system.componentmodel.uint32converter), [UInt64](/en-us/dotnet/api/system.componentmodel.uint64converter)
* [Uri](/en-us/dotnet/api/system.uritypeconverter)
* [Version](/en-us/dotnet/api/system.componentmodel.versionconverter)

[]()

## Bind with `IParsable<T>.TryParse`

The [`IParsable<TSelf>.TryParse`](/en-us/dotnet/api/system.iparsable-1.tryparse#system-iparsable-1-tryparse\(system-string-system-iformatprovider-0@\)) API supports binding controller action parameter values:

```
public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);
```

The following `DateRange` class implements [`IParsable<TSelf>`](/en-us/dotnet/api/system.iparsable-1) to support binding a date range:

```
public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}
```

The preceding code:

* Converts a string representing two dates to a `DateRange` object
* The model binder uses the [`IParsable<TSelf>.TryParse`](/en-us/dotnet/api/system.iparsable-1.tryparse#system-iparsable-1-tryparse\(system-string-system-iformatprovider-0@\)) method to bind the `DateRange`.

The following controller action uses the `DateRange` class to bind a date range:

```
// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}
```

The following `Locale` class implements [`IParsable<TSelf>`](/en-us/dotnet/api/system.iparsable-1) to support binding to `CultureInfo`:

```
public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}
```

The following controller action uses the `Locale` class to bind a `CultureInfo` string:

```
// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}
```

The following controller action uses the `DateRange` and `Locale` classes to bind a date range with `CultureInfo`:

```
// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}
```

The [API sample app on GitHub](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/bind-tryparse/samples/7.0/BindUsingTryParse) shows the preceding sample for an API controller.

### Bind with `TryParse`

The `TryParse` API supports binding controller action parameter values:

```
public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);
```

[`IParsable<T>.TryParse`](#itp7) is the recommended approach for parameter binding because unlike `TryParse`, it doesn't depend on reflection.

The following `DateRangeTP` class implements `TryParse`:

```
public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}
```

The following controller action uses the `DateRangeTP` class to bind a date range:

```
// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}
```

## Complex types

A complex type must have a public default constructor and public writable properties to bind. When model binding occurs, the class is instantiated using the public default constructor.

For each property of the complex type, [model binding looks through the sources for the name pattern](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/ParameterBinder.cs#L115-L130) *prefix.property\_name*. If nothing is found, it looks for just *property\_name* without the prefix. The decision to use the prefix isn't made per property. For example, with a query containing `?Instructor.Id=100&Name=foo`, bound to method `OnGet(Instructor instructor)`, the resulting object of type `Instructor` contains:

* `Id` set to `100`.
* `Name` set to `null`. Model binding expects `Instructor.Name` because `Instructor.Id` was used in the preceding query parameter.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

For binding to a parameter, the prefix is the parameter name. For binding to a `PageModel` `public` property, the prefix is the property name. Some attributes have a `Prefix` property that lets you override the default usage of parameter or property name.

For example, suppose the complex type is the following `Instructor` class:

```
public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}
```

### Prefix = parameter name

If the model to be bound is a parameter named `instructorToUpdate`:

```
public IActionResult OnPost(int? id, Instructor instructorToUpdate)
```

Model binding starts by looking through the sources for the key `instructorToUpdate.ID`. If that isn't found, it looks for `ID` without a prefix.

### Prefix = property name

If the model to be bound is a property named `Instructor` of the controller or `PageModel` class:

```
[BindProperty]
public Instructor Instructor { get; set; }
```

Model binding starts by looking through the sources for the key `Instructor.ID`. If that isn't found, it looks for `ID` without a prefix.

### Custom prefix

If the model to be bound is a parameter named `instructorToUpdate` and a `Bind` attribute specifies `Instructor` as the prefix:

```
public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)
```

Model binding starts by looking through the sources for the key `Instructor.ID`. If that isn't found, it looks for `ID` without a prefix.

### Attributes for complex type targets

Several built-in attributes are available for controlling model binding of complex types:

* [`[Bind]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindattribute)
* [`[BindRequired]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindrequiredattribute)
* [`[BindNever]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindneverattribute)

Warning

These attributes affect model binding when posted form data is the source of values. They do ***not*** affect input formatters, which process posted JSON and XML request bodies. Input formatters are explained [later in this article](#input-formatters).

### \[Bind] attribute

Can be applied to a class or a method parameter. Specifies which properties of a model should be included in model binding. `[Bind]` does ***not*** affect input formatters.

In the following example, only the specified properties of the `Instructor` model are bound when any handler or action method is called:

```
[Bind("LastName,FirstMidName,HireDate")]
public class Instructor
```

In the following example, only the specified properties of the `Instructor` model are bound when the `OnPost` method is called:

```
[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)
```

The `[Bind]` attribute can be used to protect against overposting in *create* scenarios. It doesn't work well in edit scenarios because excluded properties are set to null or a default value instead of being left unchanged. For protection against overposting, view models are recommended rather than the `[Bind]` attribute. For more information, see [Security note about overposting](../../data/ef-mvc/crud?view=aspnetcore-10.0#security-note-about-overposting).

### \[ModelBinder] attribute

[ModelBinderAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinderattribute) can be applied to types, properties, or parameters. It allows specifying the type of model binder used to bind the specific instance or type. For example:

```
[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)
```

The `[ModelBinder]` attribute can also be used to change the name of a property or parameter when it's being model bound:

```
public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}
```

### \[BindRequired] attribute

Causes model binding to add a model state error if binding cannot occur for a model's property. Here's an example:

```
public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}
```

See also the discussion of the `[Required]` attribute in [Model validation](validation?view=aspnetcore-10.0#required-attribute).

### \[BindNever] attribute

Can be applied to a property or a type. Prevents model binding from setting a model's property. When applied to a type, the model binding system excludes all properties the type defines. Here's an example:

```
public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}
```

## Collections

For targets that are collections of simple types, model binding looks for matches to *parameter\_name* or *property\_name*. If no match is found, it looks for one of the supported formats without the prefix. For example:

* Suppose the parameter to be bound is an array named `selectedCourses`:

  ```
  public IActionResult OnPost(int? id, int[] selectedCourses)
  ```

* Form or query string data can be in one of the following formats:

  ```
  selectedCourses=1050&selectedCourses=2000 
  ```

  ```
  selectedCourses[0]=1050&selectedCourses[1]=2000
  ```

  ```
  [0]=1050&[1]=2000
  ```

  ```
  selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
  ```

  ```
  [a]=1050&[b]=2000&index=a&index=b
  ```

  Avoid binding a parameter or a property named `index` or `Index` if it is adjacent to a collection value. Model binding attempts to use `index` as the index for the collection which might result in incorrect binding. For example, consider the following action:

  ```
  public IActionResult Post(string index, List<Product> products)
  ```

  In the preceding code, the `index` query string parameter binds to the `index` method parameter and also is used to bind the product collection. Renaming the `index` parameter or using a model binding attribute to configure binding avoids this issue:

  ```
  public IActionResult Post(string productIndex, List<Product> products)
  ```

* The following format is supported only in form data:

  ```
  selectedCourses[]=1050&selectedCourses[]=2000
  ```

* For all of the preceding example formats, model binding passes an array of two items to the `selectedCourses` parameter:

  * selectedCourses\[0]=1050
  * selectedCourses\[1]=2000

  Data formats that use subscript numbers (... \[0] ... \[1] ...) must ensure that they are numbered sequentially starting at zero. If there are any gaps in subscript numbering, all items after the gap are ignored. For example, if the subscripts are 0 and 2 instead of 0 and 1, the second item is ignored.

## Dictionaries

For `Dictionary` targets, model binding looks for matches to *parameter\_name* or *property\_name*. If no match is found, it looks for one of the supported formats without the prefix. For example:

* Suppose the target parameter is a `Dictionary<int, string>` named `selectedCourses`:

  ```
  public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
  ```

* The posted form or query string data can look like one of the following examples:

  ```
  selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
  ```

  ```
  [1050]=Chemistry&selectedCourses[2000]=Economics
  ```

  ```
  selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
  selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
  ```

  ```
  [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
  ```

* For all of the preceding example formats, model binding passes a dictionary of two items to the `selectedCourses` parameter:

  * selectedCourses\["1050"]="Chemistry"
  * selectedCourses\["2000"]="Economics"

## Constructor binding and record types

Model binding requires that complex types have a parameterless constructor. Both `System.Text.Json` and `Newtonsoft.Json` based input formatters support deserialization of classes that don't have a parameterless constructor.

Record types are a great way to succinctly represent data over the network. ASP.NET Core supports model binding and validating record types with a single constructor:

```
public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}
```

`Person/Index.cshtml`:

```
@model Person

<label>Name: <input asp-for="Name" /></label>
<br />
<label>Age: <input asp-for="Age" /></label>
```

When validating record types, the runtime searches for binding and validation metadata specifically on parameters rather than on properties.

The framework allows binding to and validating record types:

```
public record Person([Required] string Name, [Range(0, 100)] int Age);
```

For the preceding to work, the type must:

* Be a record type.
* Have exactly one public constructor.
* Contain parameters that have a property with the same name and type. The names must not differ by case.

### POCOs without parameterless constructors

POCOs that do not have parameterless constructors can't be bound.

The following code results in an exception saying that the type must have a parameterless constructor:

```
public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}
```

### Record types with manually authored constructors

Record types with manually authored constructors that look like primary constructors work

```
public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}
```

### Record types, validation and binding metadata

For record types, validation and binding metadata on parameters is used. Any metadata on properties is ignored

```
public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}
```

### Validation and metadata

Validation uses metadata on the parameter but uses the property to read the value. In the ordinary case with primary constructors, the two would be identical. However, there are ways to defeat it:

```
public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}
```

### TryUpdateModel does not update parameters on a record type

```
public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);
```

In this case, MVC will not attempt to bind `Name` again. However, `Age` is allowed to be updated

[]()

## Globalization behavior of model binding route data and query strings

The ASP.NET Core route value provider and query string value provider:

* Treat values as invariant culture.
* Expect that URLs are culture-invariant.

In contrast, values coming from form data undergo a culture-sensitive conversion. This is by design so that URLs are shareable across locales.

To make the ASP.NET Core route value provider and query string value provider undergo a culture-sensitive conversion:

* Inherit from [IValueProviderFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.ivalueproviderfactory)
* Copy the code from [QueryStringValueProviderFactory](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/QueryStringValueProviderFactory.cs) or [RouteValueValueProviderFactory](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/RouteValueProviderFactory.cs)
* Replace the [culture value](https://github.com/dotnet/AspNetCore/blob/e625fe29b049c60242e8048b4ea743cca65aa7b5/src/Mvc/Mvc.Core/src/ModelBinding/QueryStringValueProviderFactory.cs#L30) passed to the value provider constructor with [CultureInfo.CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture)
* Replace the default value provider factory in MVC options with your new one:

```
public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
```

```
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});
```

## Special data types

There are some special data types that model binding can handle.

### IFormFile and IFormFileCollection

An uploaded file included in the HTTP request. Also supported is `IEnumerable<IFormFile>` for multiple files.

### CancellationToken

Actions can optionally bind a `CancellationToken` as a parameter. This binds [RequestAborted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted) that signals when the connection underlying the HTTP request is aborted. Actions can use this parameter to cancel long running async operations that are executed as part of the controller actions.

### FormCollection

Used to retrieve all the values from posted form data.

## Input formatters

Data in the request body can be in JSON, XML, or some other format. To parse this data, model binding uses an *input formatter* that is configured to handle a particular content type. By default, ASP.NET Core includes JSON based input formatters for handling JSON data. You can add other formatters for other content types.

ASP.NET Core selects input formatters based on the [Consumes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute. If no attribute is present, it uses the [Content-Type header](https://www.w3.org/Protocols/rfc1341/4_Content-Type.html).

To use the built-in XML input formatters:

* In `Program.cs`, call [AddXmlSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvccorebuilderextensions.addxmlserializerformatters) or [AddXmlDataContractSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvccorebuilderextensions.addxmldatacontractserializerformatters).

  ```
  builder.Services.AddControllers()
      .AddXmlSerializerFormatters();
  ```

* Apply the `Consumes` attribute to controller classes or action methods that should expect XML in the request body.

  ```
  [HttpPost]
  [Consumes("application/xml")]
  public ActionResult<Pet> Create(Pet pet)
  ```

  For more information, see [Introducing XML Serialization](/en-us/dotnet/standard/serialization/introducing-xml-serialization).

### Customize model binding with input formatters

An input formatter takes full responsibility for reading data from the request body. To customize this process, configure the APIs used by the input formatter. This section describes how to customize the `System.Text.Json`-based input formatter to understand a custom type named `ObjectId`.

Consider the following model, which contains a custom `ObjectId` property:

```
public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}
```

To customize the model binding process when using `System.Text.Json`, create a class derived from [JsonConverter\<T>](/en-us/dotnet/api/system.text.json.serialization.jsonconverter-1):

```
internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}
```

To use a custom converter, apply the [JsonConverterAttribute](/en-us/dotnet/api/system.text.json.serialization.jsonconverterattribute) attribute to the type. In the following example, the `ObjectId` type is configured with `ObjectIdConverter` as its custom converter:

```
[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);
```

For more information, see [How to write custom converters](/en-us/dotnet/standard/serialization/system-text-json-converters-how-to).

## Exclude specified types from model binding

The model binding and validation systems' behavior is driven by [ModelMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelmetadata). You can customize `ModelMetadata` by adding a details provider to [MvcOptions.ModelMetadataDetailsProviders](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.modelmetadatadetailsproviders#microsoft-aspnetcore-mvc-mvcoptions-modelmetadatadetailsproviders). Built-in details providers are available for disabling model binding or validation for specified types.

To disable model binding on all models of a specified type, add an [ExcludeBindingMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.excludebindingmetadataprovider) in `Program.cs`. For example, to disable model binding on all models of type `System.Version`:

```
builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });
```

To disable validation on properties of a specified type, add a [SuppressChildValidationMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.suppresschildvalidationmetadataprovider) in `Program.cs`. For example, to disable validation on properties of type `System.Guid`:

```
builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });
```

## Custom model binders

You can extend model binding by writing a custom model binder and using the `[ModelBinder]` attribute to select it for a given target. Learn more about [custom model binding](../advanced/custom-model-binding?view=aspnetcore-10.0).

## Manual model binding

Model binding can be invoked manually by using the [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) method. The method is defined on both `ControllerBase` and `PageModel` classes. Method overloads let you specify the prefix and value provider to use. The method returns `false` if model binding fails. Here's an example:

```
if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();
```

[TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) uses value providers to get data from the form body, query string, and route data. `TryUpdateModelAsync` is typically:

* Used with Razor Pages and MVC apps using controllers and views to prevent over-posting.
* Not used with a web API unless consumed from form data, query strings, and route data. Web API endpoints that consume JSON use [Input formatters](#input-formatters) to deserialize the request body into an object.

For more information, see [TryUpdateModelAsync](../../data/ef-rp/crud?view=aspnetcore-10.0#TryUpdateModelAsync).

## \[FromServices] attribute

This attribute's name follows the pattern of model binding attributes that specify a data source. But it's not about binding data from a value provider. It gets an instance of a type from the [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container. Its purpose is to provide an alternative to constructor injection for when you need a service only if a particular method is called.

If an instance of the type isn't registered in the dependency injection container, the app throws an exception when attempting to bind the parameter. To make the parameter optional, use one of the following approaches:

* Make the parameter nullable.
* Set a default value for the parameter.

For nullable parameters, ensure that the parameter isn't `null` before accessing it.

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/model-binding/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Model validation in ASP.NET Core MVC](validation?view=aspnetcore-10.0)
* [Custom Model Binding in ASP.NET Core](../advanced/custom-model-binding?view=aspnetcore-10.0)

This article explains what model binding is, how it works, and how to customize its behavior.

## What is Model binding

Controllers and Razor pages work with data that comes from HTTP requests. For example, route data may provide a record key, and posted form fields may provide values for the properties of the model. Writing code to retrieve each of these values and convert them from strings to .NET types would be tedious and error-prone. Model binding automates this process. The model binding system:

* Retrieves data from various sources such as route data, form fields, and query strings.
* Provides the data to controllers and Razor pages in method parameters and `public` properties.
* Converts string data to .NET types.
* Updates properties of complex types.

## Example

Suppose you have the following action method:

```
[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)
```

And the app receives a request with this URL:

```
https://contoso.com/api/pets/2?DogsOnly=true
```

Model binding goes through the following steps after the routing system selects the action method:

* Finds the first parameter of `GetById`, an integer named `id`.
* Looks through the available sources in the HTTP request and finds `id` = "2" in route data.
* Converts the string "2" into integer 2.
* Finds the next parameter of `GetById`, a boolean named `dogsOnly`.
* Looks through the sources and finds "DogsOnly=true" in the query string. Name matching is not case-sensitive.
* Converts the string "true" into boolean `true`.

The framework then calls the `GetById` method, passing in 2 for the `id` parameter, and `true` for the `dogsOnly` parameter.

In the preceding example, the model binding targets are method parameters that are simple types. Targets may also be the properties of a complex type. After each property is successfully bound, [model validation](validation?view=aspnetcore-10.0) occurs for that property. The record of what data is bound to the model, and any binding or validation errors, is stored in [ControllerBase.ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.modelstate#microsoft-aspnetcore-mvc-controllerbase-modelstate) or [PageModel.ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.modelstate#microsoft-aspnetcore-mvc-controllerbase-modelstate). To find out if this process was successful, the app checks the [ModelState.IsValid](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.isvalid#microsoft-aspnetcore-mvc-modelbinding-modelstatedictionary-isvalid) flag.

## Targets

Model binding tries to find values for the following kinds of targets:

* Parameters of the controller action method that a request is routed to.
* Parameters of the Razor Pages handler method that a request is routed to.
* Public (`public`) properties of a controller or `PageModel` class, if specified by attributes.

### \[BindProperty] attribute

Can be applied to a `public` property of a controller or `PageModel` class to cause model binding to target that property:

```
public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}
```

### \[BindProperties] attribute

Can be applied to a controller or `PageModel` class to tell model binding to target all `public` properties of the class:

```
[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}
```

### Model binding for HTTP GET requests

By default, properties are not bound for HTTP GET requests. Typically, all you need for a GET request is a record ID parameter. The record ID is used to look up the item in the database. Therefore, there is no need to bind a property that holds an instance of the model. In scenarios where you do want properties bound to data from GET requests, set the `SupportsGet` property to `true`:

```
[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }
```

## Sources

By default, model binding gets data in the form of key-value pairs from the following sources in an HTTP request:

1. Form fields
2. The request body (For [controllers that have the \[ApiController\] attribute](../../web-api/?view=aspnetcore-10.0#binding-source-parameter-inference).)
3. Route data
4. Query string parameters
5. Uploaded files

For each target parameter or property, the sources are scanned in the order indicated in the preceding list. There are a few exceptions:

* Route data and query string values are used only for simple types.
* Uploaded files are bound only to target types that implement `IFormFile` or `IEnumerable<IFormFile>`.

If the default source is not correct, use one of the following attributes to specify the source:

* [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute) - Gets values from the query string.
* [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute) - Gets values from route data.
* [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) - Gets values from posted form fields.
* [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) - Gets values from the request body.
* [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute) - Gets values from HTTP headers.

These attributes:

* Are added to model properties individually and not to the model class, as in the following example:

  ```
  public class Instructor
  {
      public int Id { get; set; }

      [FromQuery(Name = "Note")]
      public string? NoteFromQueryString { get; set; }

      // ...
  }
  ```

* Optionally accept a model name value in the constructor. This option is provided in case the property name doesn't match the value in the request. For instance, the value in the request might be a header with a hyphen in its name, as in the following example:

  ```
  public void OnGet([FromHeader(Name = "Accept-Language")] string language)
  ```

### \[FromBody] attribute

Apply the `[FromBody]` attribute to a parameter to populate its properties from the body of an HTTP request. The ASP.NET Core runtime delegates the responsibility of reading the body to an input formatter. Input formatters are explained [later in this article](#input-formatters).

When `[FromBody]` is applied to a complex type parameter, any binding source attributes applied to its properties are ignored. For example, the following `Create` action specifies that its `pet` parameter is populated from the body:

```
public ActionResult<Pet> Create([FromBody] Pet pet)
```

The `Pet` class specifies that its `Breed` property is populated from a query string parameter:

```
public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}
```

In the preceding example:

* The `[FromQuery]` attribute is ignored.
* The `Breed` property is not populated from a query string parameter.

Input formatters read only the body and don't understand binding source attributes. If a suitable value is found in the body, that value is used to populate the `Breed` property.

Don't apply `[FromBody]` to more than one parameter per action method. Once the request stream is read by an input formatter, it's no longer available to be read again for binding other `[FromBody]` parameters.

### Additional sources

Source data is provided to the model binding system by *value providers*. You can write and register custom value providers that get data for model binding from other sources. For example, you might want data from cookies or session state. To get data from a new source:

* Create a class that implements `IValueProvider`.
* Create a class that implements `IValueProviderFactory`.
* Register the factory class in `Program.cs`.

The sample includes a [value provider](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/model-binding/samples/6.x/ModelBindingSample/CookieValueProvider.cs) and [factory](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/model-binding/samples/6.x/ModelBindingSample/CookieValueProviderFactory.cs) example that gets values from cookies. Register custom value provider factories in `Program.cs`:

```
builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});
```

The preceding code puts the custom value provider after all built-in value providers. To make it the first in the list, call `Insert(0, new CookieValueProviderFactory())` instead of `Add`.

## No source for a model property

By default, a model state error isn't created if no value is found for a model property. The property is set to null or a default value:

* Nullable simple types are set to `null`.
* Non-nullable value types are set to `default(T)`. For example, a parameter `int id` is set to 0.
* For complex Types, model binding creates an instance by using the default constructor, without setting properties.
* Arrays are set to `Array.Empty<T>()`, except that `byte[]` arrays are set to `null`.

If model state should be invalidated when nothing is found in form fields for a model property, use the [`[BindRequired]`](#bindrequired-attribute) attribute.

Note that this `[BindRequired]` behavior applies to model binding from posted form data, not to JSON or XML data in a request body. Request body data is handled by [input formatters](#input-formatters).

## Type conversion errors

If a source is found but can't be converted into the target type, model state is flagged as invalid. The target parameter or property is set to null or a default value, as noted in the previous section.

In an API controller that has the `[ApiController]` attribute, invalid model state results in an automatic HTTP 400 response.

In a Razor page, redisplay the page with an error message:

```
public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}
```

When the page is redisplayed by the preceding code, the invalid input isn't shown in the form field. This is because the model property has been set to null or a default value. The invalid input does appear in an error message. If you want to redisplay the bad data in the form field, consider making the model property a string and doing the data conversion manually.

The same strategy is recommended if you don't want type conversion errors to result in model state errors. In that case, make the model property a string.

## Simple types

The simple types that the model binder can convert source strings into include the following:

* [Boolean](/en-us/dotnet/api/system.componentmodel.booleanconverter)
* [Byte](/en-us/dotnet/api/system.componentmodel.byteconverter), [SByte](/en-us/dotnet/api/system.componentmodel.sbyteconverter)
* [Char](/en-us/dotnet/api/system.componentmodel.charconverter)
* [DateTime](/en-us/dotnet/api/system.componentmodel.datetimeconverter)
* [DateTimeOffset](/en-us/dotnet/api/system.componentmodel.datetimeoffsetconverter)
* [Decimal](/en-us/dotnet/api/system.componentmodel.decimalconverter)
* [Double](/en-us/dotnet/api/system.componentmodel.doubleconverter)
* [Enum](/en-us/dotnet/api/system.componentmodel.enumconverter)
* [Guid](/en-us/dotnet/api/system.componentmodel.guidconverter)
* [Int16](/en-us/dotnet/api/system.componentmodel.int16converter), [Int32](/en-us/dotnet/api/system.componentmodel.int32converter), [Int64](/en-us/dotnet/api/system.componentmodel.int64converter)
* [Single](/en-us/dotnet/api/system.componentmodel.singleconverter)
* [TimeSpan](/en-us/dotnet/api/system.componentmodel.timespanconverter)
* [UInt16](/en-us/dotnet/api/system.componentmodel.uint16converter), [UInt32](/en-us/dotnet/api/system.componentmodel.uint32converter), [UInt64](/en-us/dotnet/api/system.componentmodel.uint64converter)
* [Uri](/en-us/dotnet/api/system.uritypeconverter)
* [Version](/en-us/dotnet/api/system.componentmodel.versionconverter)

## Complex types

A complex type must have a public default constructor and public writable properties to bind. When model binding occurs, the class is instantiated using the public default constructor.

For each property of the complex type, [model binding looks through the sources for the name pattern](https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/ParameterBinder.cs#L115-L130) *prefix.property\_name*. If nothing is found, it looks for just *property\_name* without the prefix. The decision to use the prefix isn't made per property. For example, with a query containing `?Instructor.Id=100&Name=foo`, bound to method `OnGet(Instructor instructor)`, the resulting object of type `Instructor` contains:

* `Id` set to `100`.
* `Name` set to `null`. Model binding expects `Instructor.Name` because `Instructor.Id` was used in the preceding query parameter.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

For binding to a parameter, the prefix is the parameter name. For binding to a `PageModel` `public` property, the prefix is the property name. Some attributes have a `Prefix` property that lets you override the default usage of parameter or property name.

For example, suppose the complex type is the following `Instructor` class:

```
public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}
```

### Prefix = parameter name

If the model to be bound is a parameter named `instructorToUpdate`:

```
public IActionResult OnPost(int? id, Instructor instructorToUpdate)
```

Model binding starts by looking through the sources for the key `instructorToUpdate.ID`. If that isn't found, it looks for `ID` without a prefix.

### Prefix = property name

If the model to be bound is a property named `Instructor` of the controller or `PageModel` class:

```
[BindProperty]
public Instructor Instructor { get; set; }
```

Model binding starts by looking through the sources for the key `Instructor.ID`. If that isn't found, it looks for `ID` without a prefix.

### Custom prefix

If the model to be bound is a parameter named `instructorToUpdate` and a `Bind` attribute specifies `Instructor` as the prefix:

```
public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)
```

Model binding starts by looking through the sources for the key `Instructor.ID`. If that isn't found, it looks for `ID` without a prefix.

### Attributes for complex type targets

Several built-in attributes are available for controlling model binding of complex types:

* [`[Bind]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindattribute)
* [`[BindRequired]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindrequiredattribute)
* [`[BindNever]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindneverattribute)

Warning

These attributes affect model binding when posted form data is the source of values. They do ***not*** affect input formatters, which process posted JSON and XML request bodies. Input formatters are explained [later in this article](#input-formatters).

### \[Bind] attribute

Can be applied to a class or a method parameter. Specifies which properties of a model should be included in model binding. `[Bind]` does ***not*** affect input formatters.

In the following example, only the specified properties of the `Instructor` model are bound when any handler or action method is called:

```
[Bind("LastName,FirstMidName,HireDate")]
public class Instructor
```

In the following example, only the specified properties of the `Instructor` model are bound when the `OnPost` method is called:

```
[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)
```

The `[Bind]` attribute can be used to protect against overposting in *create* scenarios. It doesn't work well in edit scenarios because excluded properties are set to null or a default value instead of being left unchanged. For protection against overposting, view models are recommended rather than the `[Bind]` attribute. For more information, see [Security note about overposting](../../data/ef-mvc/crud?view=aspnetcore-10.0#security-note-about-overposting).

### \[ModelBinder] attribute

[ModelBinderAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinderattribute) can be applied to types, properties, or parameters. It allows specifying the type of model binder used to bind the specific instance or type. For example:

```
[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)
```

The `[ModelBinder]` attribute can also be used to change the name of a property or parameter when it's being model bound:

```
public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}
```

### \[BindRequired] attribute

Causes model binding to add a model state error if binding cannot occur for a model's property. Here's an example:

```
public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}
```

See also the discussion of the `[Required]` attribute in [Model validation](validation?view=aspnetcore-10.0#required-attribute).

### \[BindNever] attribute

Can be applied to a property or a type. Prevents model binding from setting a model's property. When applied to a type, the model binding system excludes all properties the type defines. Here's an example:

```
public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}
```

## Collections

For targets that are collections of simple types, model binding looks for matches to *parameter\_name* or *property\_name*. If no match is found, it looks for one of the supported formats without the prefix. For example:

* Suppose the parameter to be bound is an array named `selectedCourses`:

  ```
  public IActionResult OnPost(int? id, int[] selectedCourses)
  ```

* Form or query string data can be in one of the following formats:

  ```
  selectedCourses=1050&selectedCourses=2000 
  ```

  ```
  selectedCourses[0]=1050&selectedCourses[1]=2000
  ```

  ```
  [0]=1050&[1]=2000
  ```

  ```
  selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
  ```

  ```
  [a]=1050&[b]=2000&index=a&index=b
  ```

  Avoid binding a parameter or a property named `index` or `Index` if it is adjacent to a collection value. Model binding attempts to use `index` as the index for the collection which might result in incorrect binding. For example, consider the following action:

  ```
  public IActionResult Post(string index, List<Product> products)
  ```

  In the preceding code, the `index` query string parameter binds to the `index` method parameter and also is used to bind the product collection. Renaming the `index` parameter or using a model binding attribute to configure binding avoids this issue:

  ```
  public IActionResult Post(string productIndex, List<Product> products)
  ```

* The following format is supported only in form data:

  ```
  selectedCourses[]=1050&selectedCourses[]=2000
  ```

* For all of the preceding example formats, model binding passes an array of two items to the `selectedCourses` parameter:

  * selectedCourses\[0]=1050
  * selectedCourses\[1]=2000

  Data formats that use subscript numbers (... \[0] ... \[1] ...) must ensure that they are numbered sequentially starting at zero. If there are any gaps in subscript numbering, all items after the gap are ignored. For example, if the subscripts are 0 and 2 instead of 0 and 1, the second item is ignored.

## Dictionaries

For `Dictionary` targets, model binding looks for matches to *parameter\_name* or *property\_name*. If no match is found, it looks for one of the supported formats without the prefix. For example:

* Suppose the target parameter is a `Dictionary<int, string>` named `selectedCourses`:

  ```
  public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
  ```

* The posted form or query string data can look like one of the following examples:

  ```
  selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
  ```

  ```
  [1050]=Chemistry&selectedCourses[2000]=Economics
  ```

  ```
  selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
  selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
  ```

  ```
  [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
  ```

* For all of the preceding example formats, model binding passes a dictionary of two items to the `selectedCourses` parameter:

  * selectedCourses\["1050"]="Chemistry"
  * selectedCourses\["2000"]="Economics"

## Constructor binding and record types

Model binding requires that complex types have a parameterless constructor. Both `System.Text.Json` and `Newtonsoft.Json` based input formatters support deserialization of classes that don't have a parameterless constructor.

Record types are a great way to succinctly represent data over the network. ASP.NET Core supports model binding and validating record types with a single constructor:

```
public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}
```

`Person/Index.cshtml`:

```
@model Person

<label>Name: <input asp-for="Name" /></label>
<br />
<label>Age: <input asp-for="Age" /></label>
```

When validating record types, the runtime searches for binding and validation metadata specifically on parameters rather than on properties.

The framework allows binding to and validating record types:

```
public record Person([Required] string Name, [Range(0, 100)] int Age);
```

For the preceding to work, the type must:

* Be a record type.
* Have exactly one public constructor.
* Contain parameters that have a property with the same name and type. The names must not differ by case.

### POCOs without parameterless constructors

POCOs that do not have parameterless constructors can't be bound.

The following code results in an exception saying that the type must have a parameterless constructor:

```
public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}
```

### Record types with manually authored constructors

Record types with manually authored constructors that look like primary constructors work

```
public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}
```

### Record types, validation and binding metadata

For record types, validation and binding metadata on parameters is used. Any metadata on properties is ignored

```
public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}
```

### Validation and metadata

Validation uses metadata on the parameter but uses the property to read the value. In the ordinary case with primary constructors, the two would be identical. However, there are ways to defeat it:

```
public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}
```

### TryUpdateModel does not update parameters on a record type

```
public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);
```

In this case, MVC will not attempt to bind `Name` again. However, `Age` is allowed to be updated

[]()

## Globalization behavior of model binding route data and query strings

The ASP.NET Core route value provider and query string value provider:

* Treat values as invariant culture.
* Expect that URLs are culture-invariant.

In contrast, values coming from form data undergo a culture-sensitive conversion. This is by design so that URLs are shareable across locales.

To make the ASP.NET Core route value provider and query string value provider undergo a culture-sensitive conversion:

* Inherit from [IValueProviderFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.ivalueproviderfactory)
* Copy the code from [QueryStringValueProviderFactory](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/QueryStringValueProviderFactory.cs) or [RouteValueValueProviderFactory](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/RouteValueProviderFactory.cs)
* Replace the [culture value](https://github.com/dotnet/AspNetCore/blob/e625fe29b049c60242e8048b4ea743cca65aa7b5/src/Mvc/Mvc.Core/src/ModelBinding/QueryStringValueProviderFactory.cs#L30) passed to the value provider constructor with [CultureInfo.CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture)
* Replace the default value provider factory in MVC options with your new one:

```
public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
```

```
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});
```

## Special data types

There are some special data types that model binding can handle.

### IFormFile and IFormFileCollection

An uploaded file included in the HTTP request. Also supported is `IEnumerable<IFormFile>` for multiple files.

### CancellationToken

Actions can optionally bind a `CancellationToken` as a parameter. This binds [RequestAborted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted) that signals when the connection underlying the HTTP request is aborted. Actions can use this parameter to cancel long running async operations that are executed as part of the controller actions.

### FormCollection

Used to retrieve all the values from posted form data.

## Input formatters

Data in the request body can be in JSON, XML, or some other format. To parse this data, model binding uses an *input formatter* that is configured to handle a particular content type. By default, ASP.NET Core includes JSON based input formatters for handling JSON data. You can add other formatters for other content types.

ASP.NET Core selects input formatters based on the [Consumes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute. If no attribute is present, it uses the [Content-Type header](https://www.w3.org/Protocols/rfc1341/4_Content-Type.html).

To use the built-in XML input formatters:

* In `Program.cs`, call [AddXmlSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvccorebuilderextensions.addxmlserializerformatters) or [AddXmlDataContractSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvccorebuilderextensions.addxmldatacontractserializerformatters).

  ```
  builder.Services.AddControllers()
      .AddXmlSerializerFormatters();
  ```

* Apply the `Consumes` attribute to controller classes or action methods that should expect XML in the request body.

  ```
  [HttpPost]
  [Consumes("application/xml")]
  public ActionResult<Pet> Create(Pet pet)
  ```

  For more information, see [Introducing XML Serialization](/en-us/dotnet/standard/serialization/introducing-xml-serialization).

### Customize model binding with input formatters

An input formatter takes full responsibility for reading data from the request body. To customize this process, configure the APIs used by the input formatter. This section describes how to customize the `System.Text.Json`-based input formatter to understand a custom type named `ObjectId`.

Consider the following model, which contains a custom `ObjectId` property:

```
public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}
```

To customize the model binding process when using `System.Text.Json`, create a class derived from [JsonConverter\<T>](/en-us/dotnet/api/system.text.json.serialization.jsonconverter-1):

```
internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}
```

To use a custom converter, apply the [JsonConverterAttribute](/en-us/dotnet/api/system.text.json.serialization.jsonconverterattribute) attribute to the type. In the following example, the `ObjectId` type is configured with `ObjectIdConverter` as its custom converter:

```
[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);
```

For more information, see [How to write custom converters](/en-us/dotnet/standard/serialization/system-text-json-converters-how-to).

## Exclude specified types from model binding

The model binding and validation systems' behavior is driven by [ModelMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelmetadata). You can customize `ModelMetadata` by adding a details provider to [MvcOptions.ModelMetadataDetailsProviders](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.modelmetadatadetailsproviders#microsoft-aspnetcore-mvc-mvcoptions-modelmetadatadetailsproviders). Built-in details providers are available for disabling model binding or validation for specified types.

To disable model binding on all models of a specified type, add an [ExcludeBindingMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.excludebindingmetadataprovider) in `Program.cs`. For example, to disable model binding on all models of type `System.Version`:

```
builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });
```

To disable validation on properties of a specified type, add a [SuppressChildValidationMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.suppresschildvalidationmetadataprovider) in `Program.cs`. For example, to disable validation on properties of type `System.Guid`:

```
builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });
```

## Custom model binders

You can extend model binding by writing a custom model binder and using the `[ModelBinder]` attribute to select it for a given target. Learn more about [custom model binding](../advanced/custom-model-binding?view=aspnetcore-10.0).

## Manual model binding

Model binding can be invoked manually by using the [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) method. The method is defined on both `ControllerBase` and `PageModel` classes. Method overloads let you specify the prefix and value provider to use. The method returns `false` if model binding fails. Here's an example:

```
if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();
```

[TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) uses value providers to get data from the form body, query string, and route data. `TryUpdateModelAsync` is typically:

* Used with Razor Pages and MVC apps using controllers and views to prevent over-posting.
* Not used with a web API unless consumed from form data, query strings, and route data. Web API endpoints that consume JSON use [Input formatters](#input-formatters) to deserialize the request body into an object.

For more information, see [TryUpdateModelAsync](../../data/ef-rp/crud?view=aspnetcore-10.0#TryUpdateModelAsync).

## \[FromServices] attribute

This attribute's name follows the pattern of model binding attributes that specify a data source. But it's not about binding data from a value provider. It gets an instance of a type from the [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container. Its purpose is to provide an alternative to constructor injection for when you need a service only if a particular method is called.

If an instance of the type isn't registered in the dependency injection container, the app throws an exception when attempting to bind the parameter. To make the parameter optional, use one of the following approaches:

* Make the parameter nullable.
* Set a default value for the parameter.

For nullable parameters, ensure that the parameter isn't `null` before accessing it.

## Additional resources

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/model-binding/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))
* [Model validation in ASP.NET Core MVC](validation?view=aspnetcore-10.0)
* [Custom Model Binding in ASP.NET Core](../advanced/custom-model-binding?view=aspnetcore-10.0)

This article explains what model binding is, how it works, and how to customize its behavior.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/model-binding/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## What is Model binding

Controllers and Razor pages work with data that comes from HTTP requests. For example, route data may provide a record key, and posted form fields may provide values for the properties of the model. Writing code to retrieve each of these values and convert them from strings to .NET types would be tedious and error-prone. Model binding automates this process. The model binding system:

* Retrieves data from various sources such as route data, form fields, and query strings.
* Provides the data to controllers and Razor pages in method parameters and `public` properties.
* Converts string data to .NET types.
* Updates properties of complex types.

## Example

Suppose you have the following action method:

```
[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)
```

And the app receives a request with this URL:

```
http://contoso.com/api/pets/2?DogsOnly=true
```

Model binding goes through the following steps after the routing system selects the action method:

* Finds the first parameter of `GetById`, an integer named `id`.
* Looks through the available sources in the HTTP request and finds `id` = "2" in route data.
* Converts the string "2" into integer 2.
* Finds the next parameter of `GetById`, a boolean named `dogsOnly`.
* Looks through the sources and finds "DogsOnly=true" in the query string. Name matching is not case-sensitive.
* Converts the string "true" into boolean `true`.

The framework then calls the `GetById` method, passing in 2 for the `id` parameter, and `true` for the `dogsOnly` parameter.

In the preceding example, the model binding targets are method parameters that are simple types. Targets may also be the properties of a complex type. After each property is successfully bound, [model validation](validation?view=aspnetcore-10.0) occurs for that property. The record of what data is bound to the model, and any binding or validation errors, is stored in [ControllerBase.ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.modelstate#microsoft-aspnetcore-mvc-controllerbase-modelstate) or [PageModel.ModelState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.modelstate#microsoft-aspnetcore-mvc-controllerbase-modelstate). To find out if this process was successful, the app checks the [ModelState.IsValid](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.isvalid#microsoft-aspnetcore-mvc-modelbinding-modelstatedictionary-isvalid) flag.

## Targets

Model binding tries to find values for the following kinds of targets:

* Parameters of the controller action method that a request is routed to.
* Parameters of the Razor Pages handler method that a request is routed to.
* Public (`public`) properties of a controller or `PageModel` class, if specified by attributes.

### \[BindProperty] attribute

Can be applied to a `public` property of a controller or `PageModel` class to cause model binding to target that property:

```
public class EditModel : InstructorsPageModel
{
    [BindProperty]
    public Instructor Instructor { get; set; }
```

### \[BindProperties] attribute

Available in ASP.NET Core 2.1 or later. Can be applied to a controller or `PageModel` class to tell model binding to target all `public` properties of the class:

```
[BindProperties(SupportsGet = true)]
public class CreateModel : InstructorsPageModel
{
    public Instructor Instructor { get; set; }
```

### Model binding for HTTP GET requests

By default, properties are not bound for HTTP GET requests. Typically, all you need for a GET request is a record ID parameter. The record ID is used to look up the item in the database. Therefore, there is no need to bind a property that holds an instance of the model. In scenarios where you do want properties bound to data from GET requests, set the `SupportsGet` property to `true`:

```
[BindProperty(Name = "ai_user", SupportsGet = true)]
public string ApplicationInsightsCookie { get; set; }
```

## Sources

By default, model binding gets data in the form of key-value pairs from the following sources in an HTTP request:

1. Form fields
2. The request body (For [controllers that have the \[ApiController\] attribute](../../web-api/?view=aspnetcore-10.0#binding-source-parameter-inference).)
3. Route data
4. Query string parameters
5. Uploaded files

For each target parameter or property, the sources are scanned in the order indicated in the preceding list. There are a few exceptions:

* Route data and query string values are used only for simple types.
* Uploaded files are bound only to target types that implement `IFormFile` or `IEnumerable<IFormFile>`.

If the default source is not correct, use one of the following attributes to specify the source:

* [`[FromQuery]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromqueryattribute) - Gets values from the query string.
* [`[FromRoute]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromrouteattribute) - Gets values from route data.
* [`[FromForm]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromformattribute) - Gets values from posted form fields.
* [`[FromBody]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.frombodyattribute) - Gets values from the request body.
* [`[FromHeader]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.fromheaderattribute) - Gets values from HTTP headers.

These attributes:

* Are added to model properties individually (not to the model class), as in the following example:

  ```
  public class Instructor
  {
      public int ID { get; set; }

      [FromQuery(Name = "Note")]
      public string NoteFromQueryString { get; set; }
  ```

* Optionally accept a model name value in the constructor. This option is provided in case the property name doesn't match the value in the request. For instance, the value in the request might be a header with a hyphen in its name, as in the following example:

  ```
  public void OnGet([FromHeader(Name = "Accept-Language")] string language)
  ```

### \[FromBody] attribute

Apply the `[FromBody]` attribute to a parameter to populate its properties from the body of an HTTP request. The ASP.NET Core runtime delegates the responsibility of reading the body to an input formatter. Input formatters are explained [later in this article](#input-formatters-1).

When `[FromBody]` is applied to a complex type parameter, any binding source attributes applied to its properties are ignored. For example, the following `Create` action specifies that its `pet` parameter is populated from the body:

```
public ActionResult<Pet> Create([FromBody] Pet pet)
```

The `Pet` class specifies that its `Breed` property is populated from a query string parameter:

```
public class Pet
{
    public string Name { get; set; }

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; }
}
```

In the preceding example:

* The `[FromQuery]` attribute is ignored.
* The `Breed` property is not populated from a query string parameter.

Input formatters read only the body and don't understand binding source attributes. If a suitable value is found in the body, that value is used to populate the `Breed` property.

Don't apply `[FromBody]` to more than one parameter per action method. Once the request stream is read by an input formatter, it's no longer available to be read again for binding other `[FromBody]` parameters.

### Additional sources

Source data is provided to the model binding system by *value providers*. You can write and register custom value providers that get data for model binding from other sources. For example, you might want data from cookies or session state. To get data from a new source:

* Create a class that implements `IValueProvider`.
* Create a class that implements `IValueProviderFactory`.
* Register the factory class in `Startup.ConfigureServices`.

The sample app includes a [value provider](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/model-binding/samples/3.x/ModelBindingSample/CookieValueProvider.cs) and [factory](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/model-binding/samples/3.x/ModelBindingSample/CookieValueProviderFactory.cs) example that gets values from cookies. Here's the registration code in `Startup.ConfigureServices`:

```
services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();
```

The code shown puts the custom value provider after all the built-in value providers. To make it the first in the list, call `Insert(0, new CookieValueProviderFactory())` instead of `Add`.

## No source for a model property

By default, a model state error isn't created if no value is found for a model property. The property is set to null or a default value:

* Nullable simple types are set to `null`.
* Non-nullable value types are set to `default(T)`. For example, a parameter `int id` is set to 0.
* For complex Types, model binding creates an instance by using the default constructor, without setting properties.
* Arrays are set to `Array.Empty<T>()`, except that `byte[]` arrays are set to `null`.

If model state should be invalidated when nothing is found in form fields for a model property, use the [`[BindRequired]`](#bindrequired-attribute) attribute.

Note that this `[BindRequired]` behavior applies to model binding from posted form data, not to JSON or XML data in a request body. Request body data is handled by [input formatters](#input-formatters).

## Type conversion errors

If a source is found but can't be converted into the target type, model state is flagged as invalid. The target parameter or property is set to null or a default value, as noted in the previous section.

In an API controller that has the `[ApiController]` attribute, invalid model state results in an automatic HTTP 400 response.

In a Razor page, redisplay the page with an error message:

```
public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _instructorsInMemoryStore.Add(Instructor);
    return RedirectToPage("./Index");
}
```

Client-side validation catches most bad data that would otherwise be submitted to a Razor Pages form. This validation makes it hard to trigger the preceding highlighted code. The sample app includes a **Submit with Invalid Date** button that puts bad data in the **Hire Date** field and submits the form. This button shows how the code for redisplaying the page works when data conversion errors occur.

When the page is redisplayed by the preceding code, the invalid input is not shown in the form field. This is because the model property has been set to null or a default value. The invalid input does appear in an error message. But if you want to redisplay the bad data in the form field, consider making the model property a string and doing the data conversion manually.

The same strategy is recommended if you don't want type conversion errors to result in model state errors. In that case, make the model property a string.

## Simple types

The simple types that the model binder can convert source strings into include the following:

* [Boolean](/en-us/dotnet/api/system.componentmodel.booleanconverter)
* [Byte](/en-us/dotnet/api/system.componentmodel.byteconverter), [SByte](/en-us/dotnet/api/system.componentmodel.sbyteconverter)
* [Char](/en-us/dotnet/api/system.componentmodel.charconverter)
* [DateTime](/en-us/dotnet/api/system.componentmodel.datetimeconverter)
* [DateTimeOffset](/en-us/dotnet/api/system.componentmodel.datetimeoffsetconverter)
* [Decimal](/en-us/dotnet/api/system.componentmodel.decimalconverter)
* [Double](/en-us/dotnet/api/system.componentmodel.doubleconverter)
* [Enum](/en-us/dotnet/api/system.componentmodel.enumconverter)
* [Guid](/en-us/dotnet/api/system.componentmodel.guidconverter)
* [Int16](/en-us/dotnet/api/system.componentmodel.int16converter), [Int32](/en-us/dotnet/api/system.componentmodel.int32converter), [Int64](/en-us/dotnet/api/system.componentmodel.int64converter)
* [Single](/en-us/dotnet/api/system.componentmodel.singleconverter)
* [TimeSpan](/en-us/dotnet/api/system.componentmodel.timespanconverter)
* [UInt16](/en-us/dotnet/api/system.componentmodel.uint16converter), [UInt32](/en-us/dotnet/api/system.componentmodel.uint32converter), [UInt64](/en-us/dotnet/api/system.componentmodel.uint64converter)
* [Uri](/en-us/dotnet/api/system.uritypeconverter)
* [Version](/en-us/dotnet/api/system.componentmodel.versionconverter)

## Complex types

A complex type must have a public default constructor and public writable properties to bind. When model binding occurs, the class is instantiated using the public default constructor.

For each property of the complex type, model binding looks through the sources for the name pattern *prefix.property\_name*. If nothing is found, it looks for just *property\_name* without the prefix.

For binding to a parameter, the prefix is the parameter name. For binding to a `PageModel` `public` property, the prefix is the property name. Some attributes have a `Prefix` property that lets you override the default usage of parameter or property name.

For example, suppose the complex type is the following `Instructor` class:

```
public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}
```

### Prefix = parameter name

If the model to be bound is a parameter named `instructorToUpdate`:

```
public IActionResult OnPost(int? id, Instructor instructorToUpdate)
```

Model binding starts by looking through the sources for the key `instructorToUpdate.ID`. If that isn't found, it looks for `ID` without a prefix.

### Prefix = property name

If the model to be bound is a property named `Instructor` of the controller or `PageModel` class:

```
[BindProperty]
public Instructor Instructor { get; set; }
```

Model binding starts by looking through the sources for the key `Instructor.ID`. If that isn't found, it looks for `ID` without a prefix.

### Custom prefix

If the model to be bound is a parameter named `instructorToUpdate` and a `Bind` attribute specifies `Instructor` as the prefix:

```
public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)
```

Model binding starts by looking through the sources for the key `Instructor.ID`. If that isn't found, it looks for `ID` without a prefix.

### Attributes for complex type targets

Several built-in attributes are available for controlling model binding of complex types:

* `[Bind]`
* `[BindRequired]`
* `[BindNever]`

Warning

These attributes affect model binding when posted form data is the source of values. They do ***not*** affect input formatters, which process posted JSON and XML request bodies. Input formatters are explained [later in this article](#input-formatters-1).

### \[Bind] attribute

Can be applied to a class or a method parameter. Specifies which properties of a model should be included in model binding. `[Bind]` does ***not*** affect input formatters.

In the following example, only the specified properties of the `Instructor` model are bound when any handler or action method is called:

```
[Bind("LastName,FirstMidName,HireDate")]
public class Instructor
```

In the following example, only the specified properties of the `Instructor` model are bound when the `OnPost` method is called:

```
[HttpPost]
public IActionResult OnPost([Bind("LastName,FirstMidName,HireDate")] Instructor instructor)
```

The `[Bind]` attribute can be used to protect against overposting in *create* scenarios. It doesn't work well in edit scenarios because excluded properties are set to null or a default value instead of being left unchanged. For protection against overposting, view models are recommended rather than the `[Bind]` attribute. For more information, see [Security note about overposting](../../data/ef-mvc/crud?view=aspnetcore-10.0#security-note-about-overposting).

### \[ModelBinder] attribute

[ModelBinderAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinderattribute) can be applied to types, properties, or parameters. It allows specifying the type of model binder used to bind the specific instance or type. For example:

```
[HttpPost]
public IActionResult OnPost([ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)
```

The `[ModelBinder]` attribute can also be used to change the name of a property or parameter when it's being model bound:

```
public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    public string Name { get; set; }
}
```

### \[BindRequired] attribute

Can only be applied to model properties, not to method parameters. Causes model binding to add a model state error if binding cannot occur for a model's property. Here's an example:

```
public class InstructorWithCollection
{
    public int ID { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Hire Date")]
    [BindRequired]
    public DateTime HireDate { get; set; }
```

See also the discussion of the `[Required]` attribute in [Model validation](validation?view=aspnetcore-10.0#required-attribute).

### \[BindNever] attribute

Can only be applied to model properties, not to method parameters. Prevents model binding from setting a model's property. Here's an example:

```
public class InstructorWithDictionary
{
    [BindNever]
    public int ID { get; set; }
```

## Collections

For targets that are collections of simple types, model binding looks for matches to *parameter\_name* or *property\_name*. If no match is found, it looks for one of the supported formats without the prefix. For example:

* Suppose the parameter to be bound is an array named `selectedCourses`:

  ```
  public IActionResult OnPost(int? id, int[] selectedCourses)
  ```

* Form or query string data can be in one of the following formats:

  ```
  selectedCourses=1050&selectedCourses=2000 
  ```

  ```
  selectedCourses[0]=1050&selectedCourses[1]=2000
  ```

  ```
  [0]=1050&[1]=2000
  ```

  ```
  selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
  ```

  ```
  [a]=1050&[b]=2000&index=a&index=b
  ```

  Avoid binding a parameter or a property named `index` or `Index` if it is adjacent to a collection value. Model binding attempts to use `index` as the index for the collection which might result in incorrect binding. For example, consider the following action:

  ```
  public IActionResult Post(string index, List<Product> products)
  ```

  In the preceding code, the `index` query string parameter binds to the `index` method parameter and also is used to bind the product collection. Renaming the `index` parameter or using a model binding attribute to configure binding avoids this issue:

  ```
  public IActionResult Post(string productIndex, List<Product> products)
  ```

* The following format is supported only in form data:

  ```
  selectedCourses[]=1050&selectedCourses[]=2000
  ```

* For all of the preceding example formats, model binding passes an array of two items to the `selectedCourses` parameter:

  * selectedCourses\[0]=1050
  * selectedCourses\[1]=2000

  Data formats that use subscript numbers (... \[0] ... \[1] ...) must ensure that they are numbered sequentially starting at zero. If there are any gaps in subscript numbering, all items after the gap are ignored. For example, if the subscripts are 0 and 2 instead of 0 and 1, the second item is ignored.

## Dictionaries

For `Dictionary` targets, model binding looks for matches to *parameter\_name* or *property\_name*. If no match is found, it looks for one of the supported formats without the prefix. For example:

* Suppose the target parameter is a `Dictionary<int, string>` named `selectedCourses`:

  ```
  public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
  ```

* The posted form or query string data can look like one of the following examples:

  ```
  selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
  ```

  ```
  [1050]=Chemistry&selectedCourses[2000]=Economics
  ```

  ```
  selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
  selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
  ```

  ```
  [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
  ```

* For all of the preceding example formats, model binding passes a dictionary of two items to the `selectedCourses` parameter:

  * selectedCourses\["1050"]="Chemistry"
  * selectedCourses\["2000"]="Economics"

## Constructor binding and record types

Model binding requires that complex types have a parameterless constructor. Both `System.Text.Json` and `Newtonsoft.Json` based input formatters support deserialization of classes that don't have a parameterless constructor.

C# 9 introduces record types, which are a great way to succinctly represent data over the network. ASP.NET Core adds support for model binding and validating record types with a single constructor:

```
public record Person([Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
   public IActionResult Index() => View();

   [HttpPost]
   public IActionResult Index(Person person)
   {
       ...
   }
}
```

`Person/Index.cshtml`:

```
@model Person

<label>Name: <input asp-for="Name" /></label>
...
<label>Age: <input asp-for="Age" /></label>
```

When validating record types, the runtime searches for binding and validation metadata specifically on parameters rather than on properties.

The framework allows binding to and validating record types:

```
public record Person([Required] string Name, [Range(0, 100)] int Age);
```

For the preceding to work, the type must:

* Be a record type.
* Have exactly one public constructor.
* Contain parameters that have a property with the same name and type. The names must not differ by case.

### POCOs without parameterless constructors

POCOs that do not have parameterless constructors can't be bound.

The following code results in an exception saying that the type must have a parameterless constructor:

```
public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
   public Person(string Name) : this (Name, 0);
}
```

### Record types with manually authored constructors

Record types with manually authored constructors that look like primary constructors work

```
public record Person
{
   public Person([Required] string Name, [Range(0, 100)] int Age) => (this.Name, this.Age) = (Name, Age);

   public string Name { get; set; }
   public int Age { get; set; }
}
```

### Record types, validation and binding metadata

For record types, validation and binding metadata on parameters is used. Any metadata on properties is ignored

```
public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}
```

### Validation and metadata

Validation uses metadata on the parameter but uses the property to read the value. In the ordinary case with primary constructors, the two would be identical. However, there are ways to defeat it:

```
public record Person([Required] string Name)
{
   private readonly string _name;
   public Name { get; init => _name = value ?? string.Empty; } // Now this property is never null. However this object could have been constructed as `new Person(null);`
}
```

### TryUpdateModel does not update parameters on a record type

```
public record Person(string Name)
{
   public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);
```

In this case, MVC will not attempt to bind `Name` again. However, `Age` is allowed to be updated

[]()

## Globalization behavior of model binding route data and query strings

The ASP.NET Core route value provider and query string value provider:

* Treat values as invariant culture.
* Expect that URLs are culture-invariant.

In contrast, values coming from form data undergo a culture-sensitive conversion. This is by design so that URLs are shareable across locales.

To make the ASP.NET Core route value provider and query string value provider undergo a culture-sensitive conversion:

* Inherit from [IValueProviderFactory](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.ivalueproviderfactory)
* Copy the code from [QueryStringValueProviderFactory](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/QueryStringValueProviderFactory.cs) or [RouteValueValueProviderFactory](https://github.com/dotnet/AspNetCore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/RouteValueProviderFactory.cs)
* Replace the [culture value](https://github.com/dotnet/AspNetCore/blob/e625fe29b049c60242e8048b4ea743cca65aa7b5/src/Mvc/Mvc.Core/src/ModelBinding/QueryStringValueProviderFactory.cs#L30) passed to the value provider constructor with [CultureInfo.CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture#system-globalization-cultureinfo-currentculture)
* Replace the default value provider factory in MVC options with your new one:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        var index = options.ValueProviderFactories.IndexOf(
            options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>().Single());
        options.ValueProviderFactories[index] = new CulturedQueryStringValueProviderFactory();
    });
}
```

```
public class CulturedQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query != null && query.Count > 0)
        {
            var valueProvider = new QueryStringValueProvider(
                BindingSource.Query,
                query,
                CultureInfo.CurrentCulture);

            context.ValueProviders.Add(valueProvider);
        }

        return Task.CompletedTask;
    }
}
```

## Special data types

There are some special data types that model binding can handle.

### IFormFile and IFormFileCollection

An uploaded file included in the HTTP request. Also supported is `IEnumerable<IFormFile>` for multiple files.

### CancellationToken

Actions can optionally bind a `CancellationToken` as a parameter. This binds [RequestAborted](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted#microsoft-aspnetcore-http-httpcontext-requestaborted) that signals when the connection underlying the HTTP request is aborted. Actions can use this parameter to cancel long running async operations that are executed as part of the controller actions.

### FormCollection

Used to retrieve all the values from posted form data.

## Input formatters

Data in the request body can be in JSON, XML, or some other format. To parse this data, model binding uses an *input formatter* that is configured to handle a particular content type. By default, ASP.NET Core includes JSON based input formatters for handling JSON data. You can add other formatters for other content types.

ASP.NET Core selects input formatters based on the [Consumes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute) attribute. If no attribute is present, it uses the [Content-Type header](https://www.w3.org/Protocols/rfc1341/4_Content-Type.html).

To use the built-in XML input formatters:

* Install the `Microsoft.AspNetCore.Mvc.Formatters.Xml` NuGet package.

* In `Startup.ConfigureServices`, call [AddXmlSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvccorebuilderextensions.addxmlserializerformatters) or [AddXmlDataContractSerializerFormatters](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcxmlmvccorebuilderextensions.addxmldatacontractserializerformatters).

  ```
  services.AddRazorPages()
      .AddMvcOptions(options =>
  {
      options.ValueProviderFactories.Add(new CookieValueProviderFactory());
      options.ModelMetadataDetailsProviders.Add(
          new ExcludeBindingMetadataProvider(typeof(System.Version)));
      options.ModelMetadataDetailsProviders.Add(
          new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
  })
  .AddXmlSerializerFormatters();
  ```

* Apply the `Consumes` attribute to controller classes or action methods that should expect XML in the request body.

  ```
  [HttpPost]
  [Consumes("application/xml")]
  public ActionResult<Pet> Create(Pet pet)
  ```

  For more information, see [Introducing XML Serialization](/en-us/dotnet/standard/serialization/introducing-xml-serialization).

### Customize model binding with input formatters

An input formatter takes full responsibility for reading data from the request body. To customize this process, configure the APIs used by the input formatter. This section describes how to customize the `System.Text.Json`-based input formatter to understand a custom type named `ObjectId`.

Consider the following model, which contains a custom `ObjectId` property named `Id`:

```
public class ModelWithObjectId
{
    public ObjectId Id { get; set; }
}
```

To customize the model binding process when using `System.Text.Json`, create a class derived from [JsonConverter\<T>](/en-us/dotnet/api/system.text.json.serialization.jsonconverter-1):

```
internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return new ObjectId(JsonSerializer.Deserialize<int>(ref reader, options));
    }

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
    {
        writer.WriteNumberValue(value.Id);
    }
}
```

To use a custom converter, apply the [JsonConverterAttribute](/en-us/dotnet/api/system.text.json.serialization.jsonconverterattribute) attribute to the type. In the following example, the `ObjectId` type is configured with `ObjectIdConverter` as its custom converter:

```
[JsonConverter(typeof(ObjectIdConverter))]
public struct ObjectId
{
    public ObjectId(int id) =>
        Id = id;

    public int Id { get; }
}
```

For more information, see [How to write custom converters](/en-us/dotnet/standard/serialization/system-text-json-converters-how-to).

## Exclude specified types from model binding

The model binding and validation systems' behavior is driven by [ModelMetadata](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelmetadata). You can customize `ModelMetadata` by adding a details provider to [MvcOptions.ModelMetadataDetailsProviders](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.modelmetadatadetailsproviders#microsoft-aspnetcore-mvc-mvcoptions-modelmetadatadetailsproviders). Built-in details providers are available for disabling model binding or validation for specified types.

To disable model binding on all models of a specified type, add an [ExcludeBindingMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.excludebindingmetadataprovider) in `Startup.ConfigureServices`. For example, to disable model binding on all models of type `System.Version`:

```
services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();
```

To disable validation on properties of a specified type, add a [SuppressChildValidationMetadataProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.suppresschildvalidationmetadataprovider) in `Startup.ConfigureServices`. For example, to disable validation on properties of type `System.Guid`:

```
services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();
```

## Custom model binders

You can extend model binding by writing a custom model binder and using the `[ModelBinder]` attribute to select it for a given target. Learn more about [custom model binding](../advanced/custom-model-binding?view=aspnetcore-10.0).

## Manual model binding

Model binding can be invoked manually by using the [TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) method. The method is defined on both `ControllerBase` and `PageModel` classes. Method overloads let you specify the prefix and value provider to use. The method returns `false` if model binding fails. Here's an example:

```
if (await TryUpdateModelAsync<InstructorWithCollection>(
    newInstructor,
    "Instructor",
    i => i.FirstMidName, i => i.LastName, i => i.HireDate))
{
    _instructorsInMemoryStore.Add(newInstructor);
    return RedirectToPage("./Index");
}
PopulateAssignedCourseData(newInstructor);
return Page();
```

[TryUpdateModelAsync](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync) uses value providers to get data from the form body, query string, and route data. `TryUpdateModelAsync` is typically:

* Used with Razor Pages and MVC apps using controllers and views to prevent over-posting.
* Not used with a web API unless consumed from form data, query strings, and route data. Web API endpoints that consume JSON use [Input formatters](#input-formatters) to deserialize the request body into an object.

For more information, see [TryUpdateModelAsync](../../data/ef-rp/crud?view=aspnetcore-10.0#TryUpdateModelAsync).

## \[FromServices] attribute

This attribute's name follows the pattern of model binding attributes that specify a data source. But it's not about binding data from a value provider. It gets an instance of a type from the [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container. Its purpose is to provide an alternative to constructor injection for when you need a service only if a particular method is called.

## Additional resources

* [Model validation in ASP.NET Core MVC](validation?view=aspnetcore-10.0)
* [Custom Model Binding in ASP.NET Core](../advanced/custom-model-binding?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/interprocess-uds
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/interprocess-uds.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Inter-process communication with gRPC and Unix domain sockets

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

.NET supports inter-process communication (IPC) using gRPC. For more information about getting started with using gRPC to communicate between processes, see [Inter-process communication with gRPC](interprocess?view=aspnetcore-10.0).

[Unix domain sockets (UDS)](https://wikipedia.org/wiki/Unix_domain_socket) is a widely supported IPC transport that's more efficient than TCP when the client and server are on the same machine. This article discusses how to configure gRPC communication over UDS.

## Prerequisites

* .NET 5 or later
* Linux, macOS, or [Windows 10/Windows Server 2019 or later](https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/)

## Server configuration

Unix domain sockets are supported by [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0), which is configured in `Program.cs`:

```
var socketPath = Path.Combine(Path.GetTempPath(), "socket.tmp");

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenUnixSocket(socketPath, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
    });
});
```

The preceding example:

* Configures Kestrel's endpoints in [ConfigureKestrel](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.configurekestrel).
* Calls [ListenUnixSocket](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listenunixsocket) to listen to a UDS with the specified path.
* Creates a UDS endpoint that isn't configured to use HTTPS. For information about enabling HTTPS, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsusehttps).

## Client configuration

`GrpcChannel` supports making gRPC calls over custom transports. When a channel is created, it can be configured with a [SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler) that has a custom [ConnectCallback](/en-us/dotnet/api/system.net.http.socketshttphandler.connectcallback#system-net-http-socketshttphandler-connectcallback). The callback allows the client to make connections over custom transports and then send HTTP requests over that transport.

Note

Some connectivity features of `GrpcChannel`, such as client side load balancing and channel status, can't be used together with Unix domain sockets.

Unix domain sockets connection factory example:

```
public class UnixDomainSocketsConnectionFactory
{
    private readonly EndPoint endPoint;

    public UnixDomainSocketsConnectionFactory(EndPoint endPoint)
    {
        this.endPoint = endPoint;
    }

    public async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext _,
        CancellationToken cancellationToken = default)
    {
        var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);

        try
        {
            await socket.ConnectAsync(this.endPoint, cancellationToken).ConfigureAwait(false);
            return new NetworkStream(socket, true);
        }
        catch
        {
            socket.Dispose();
            throw;
        }
    }
}
```

Using the custom connection factory to create a channel:

```
public static readonly string SocketPath = Path.Combine(Path.GetTempPath(), "socket.tmp");

public static GrpcChannel CreateChannel()
{
    var udsEndPoint = new UnixDomainSocketEndPoint(SocketPath);
    var connectionFactory = new UnixDomainSocketsConnectionFactory(udsEndPoint);
    var socketsHttpHandler = new SocketsHttpHandler
    {
        ConnectCallback = connectionFactory.ConnectAsync
    };

    return GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
    {
        HttpHandler = socketsHttpHandler
    });
}
```

Channels created using the preceding code send gRPC calls over Unix domain sockets.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/fx-to-core/areas/machine-key
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/fx-to-core/areas/machine-key.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate ASP.NET machineKey by using System.Web adapters

Note

This makes use of the [System.Web Adapters](../inc/systemweb-adapters?view=aspnetcore-10.0) to simplify migration.

In the ASP.NET Framework app, configure `<machineKey>` and the System.Web adapters host so that both apps can share a compatible data protection configuration. For full background on replacing `<machineKey>`, see [Replace the ASP.NET machineKey in ASP.NET Core](../../../security/data-protection/compatibility/replacing-machinekey?view=aspnetcore-10.0).

This guidance builds on the `System.Web` adapters hosting model so that data protection services are registered in the host dependency injection (DI) container and made available throughout the ASP.NET Framework app. By integrating with the host DI provided by the adapters, existing ASP.NET Framework components can resolve `IDataProtectionProvider`, `IDataProtector`, and related types.

Both the ASP.NET Framework app and the ASP.NET Core app must use a shared application name and key repository for data protection so that protected payloads can round-trip between apps.

* Call `SetApplicationName` with the same logical application name in both apps (for example, `"my-app"`).
* Configure `PersistKeysToFileSystem` to point to the same key repository location that both apps can read and write.

Note

The directory used with `PersistKeysToFileSystem` is the backing store for the shared data protection keys. In production, use a durable, shared store (such as a UNC share, Redis, or Azure Blob Storage) and follow the key management guidance in [Configure ASP.NET Core Data Protection](../../../security/data-protection/configuration/overview?view=aspnetcore-10.0) and [ASP.NET Core Data Protection Overview](../../../security/data-protection/introduction?view=aspnetcore-10.0).

## Configure the ASP.NET Framework app

To implement this configuration in the ASP.NET Framework app, ensure the `Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices` package is installed in the ASP.NET Framework app.

When you install the `Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices` package in the ASP.NET Framework app, it normally configures `<machineKey>` automatically. If `<machineKey>` isn't present or you need to verify the settings, configure \`\` in *Web.config* to use the compatibility data protector as shown:

```
<configuration>
  <system.web>
    <httpRuntime targetFramework="4.8.1" />
    <machineKey
      compatibilityMode="Framework45"
      dataProtectorType="Microsoft.AspNetCore.DataProtection.SystemWeb.CompatibilityDataProtector,
      Microsoft.AspNetCore.DataProtection.SystemWeb" />
  </system.web>
</configuration>
```

Next, in `Global.asax.cs`, register the System.Web adapters host and configure data protection using the same application name and key repository that the ASP.NET Core app will use. The following example is adapted from the MachineKey Framework sample:

```
using System.IO;
using System.Web;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.SystemWebAdapters.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DataProtectionDemo
{
    public class MvcApplication : HttpApplication
    {
        protected void Application_Start()
        {
            HttpApplicationHost.RegisterHost(builder =>
            {
                builder.AddServiceDefaults();

                builder.AddDataProtection()
                    .SetApplicationName("my-app")
                    .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\myapp-keys\"));
            });
        }
    }
}
```

This configuration:

* Sets a shared application name (`my-app`) that the ASP.NET Core app must also use.
* Configures a shared key repository (for example, a UNC share) that both apps can access.
* Ensures `<machineKey>` operations (forms auth, view state, `MachineKey.Protect`, and related APIs) are routed through ASP.NET Core data protection.
* Runs as part of the ASP.NET Framework host so that existing `<machineKey>`-based features use the same data protection system as ASP.NET Core.

## Configure the ASP.NET Core app

No additional configuration is required for data protection in the ASP.NET Core app. Just configure the same application name and key storage location that the ASP.NET Framework app uses.

```
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDataProtection()
    .SetApplicationName(MachineKeyExampleHandler.AppName)
    .PersistKeysToFileSystem(
        new DirectoryInfo(Path.Combine(Path.GetTempPath(), "sharedkeys", MachineKeyExampleHandler.AppName)));

var app = builder.Build();

// Configure application

app.Run();
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Razor Pages architecture and concepts in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Dave Brock](https://twitter.com/daveabrock), and [Kirk Larkin](https://twitter.com/serpent5)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Razor Pages can make coding page-focused scenarios easier and more productive than using controllers and views.

If you're looking for a tutorial that uses the Model-View-Controller approach, see [Get started with ASP.NET Core MVC](../tutorials/first-mvc-app/start-mvc?view=aspnetcore-10.0).

This article covers the architecture, concepts, and patterns that make Razor Pages effective for building page-focused web applications. It explains how Razor Pages work, their key components, and best practices for implementation. If you prefer hands-on learning with step-by-step instructions, see [Tutorial: Create a Razor Pages web app with ASP.NET Core](../tutorials/razor-pages/?view=aspnetcore-10.0). For an overview of ASP.NET Core, see the [Introduction to ASP.NET Core](../overview?view=aspnetcore-10.0).

[]()

## Create a Razor Pages project

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

See [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0) for detailed instructions on how to create a Razor Pages project.

Run `dotnet new webapp` from the command line.

## Razor Pages

Razor Pages is enabled in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

In the preceding code:

* [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages) adds services for Razor Pages to the app.
* [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages) adds endpoints for Razor Pages to the [IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder).

Consider a basic page:[]()

```
@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>
```

The preceding code looks a lot like a [Razor view file](../tutorials/first-mvc-app/adding-view?view=aspnetcore-10.0) used in an ASP.NET Core app with controllers and views. What makes it different is the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. `@page` makes the file into an MVC action, which means that it handles requests directly, without going through a controller. `@page` must be the first Razor directive on a page. `@page` affects the behavior of other [Razor](../mvc/views/razor?view=aspnetcore-10.0) constructs. Razor Pages file names have a `.cshtml` suffix.

A similar page, using a `PageModel` class, is shown in the following two files. The `Pages/Index2.cshtml` file:

```
@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>
```

The `Pages/Index2.cshtml.cs` page model:

```
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}
```

By convention, the `PageModel` class file has the same name as the Razor Page file with `.cs` appended. For example, the previous Razor Page is `Pages/Index2.cshtml`. The file containing the `PageModel` class is named `Pages/Index2.cshtml.cs`.

The associations of URL paths to pages are determined by the page's location in the file system. The following table shows a Razor Page path and the matching URL.

| File name and path            | matching URL               |
| ----------------------------- | -------------------------- |
| `/Pages/Index.cshtml`         | `/` or `/Index`            |
| `/Pages/Contact.cshtml`       | `/Contact`                 |
| `/Pages/Store/Contact.cshtml` | `/Store/Contact`           |
| `/Pages/Store/Index.cshtml`   | `/Store` or `/Store/Index` |

Notes:

* The runtime looks for Razor Pages files in the *Pages* folder by default.
* `Index` is the default page when a URL doesn't include a page.

## Write a basic form

Razor Pages is designed to make common patterns used with web browsers easy to implement when building an app. [Model binding](../mvc/models/model-binding?view=aspnetcore-10.0), [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0), and HTML helpers work with the properties defined in a Razor Page class. Consider a page that implements a basic "contact us" form for the `Contact` model:

For the samples in this document, the `DbContext` is initialized in the [Program.cs](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/index/samples/6.0/RazorPagesContacts/Program.cs#L11-L12) file.

The in memory database requires the `Microsoft.EntityFrameworkCore.InMemory` NuGet package.

```
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The data model:

```
using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string? Name { get; set; }
    }
}
```

The db context:

```
using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext (DbContextOptions<CustomerDbContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesContacts.Models.Customer> Customer => Set<RazorPagesContacts.Models.Customer>();
    }
}
```

The `Pages/Customers/Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>
```

The `Pages/Customers/Create.cshtml.cs` page model:

```
public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

By convention, the `PageModel` class is called `<PageName>Model` and is in the same namespace as the page.

The `PageModel` class allows separation of the logic of a page from its presentation. It defines page handlers for requests sent to the page and the data used to render the page. This separation allows:

* Managing of page dependencies through [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0).
* [Unit testing](../test/razor-pages-tests?view=aspnetcore-10.0)

The page has an `OnPostAsync` *handler method*, which runs on `POST` requests (when a user posts the form). Handler methods for any HTTP verb can be added. The most common handlers are:

* `OnGet` to initialize state needed for the page. In the preceding code, the `OnGet` method displays the `Create.cshtml` Razor Page.
* `OnPost` to handle form submissions.

The `Async` naming suffix is optional but is often used by convention for asynchronous functions. The preceding code is typical for Razor Pages.

If you're familiar with ASP.NET apps using controllers and views:

* The `OnPostAsync` code in the preceding example looks similar to typical controller code.
* Most of the MVC primitives like [model binding](../mvc/models/model-binding?view=aspnetcore-10.0), [validation](../mvc/models/validation?view=aspnetcore-10.0), and action results work the same with Controllers and Razor Pages.

The previous `OnPostAsync` method:

```
[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

The basic flow of `OnPostAsync`:

Check for validation errors.

* If there are no errors, save the data and redirect.
* If there are errors, show the page again with validation messages. In many cases, validation errors would be detected on the client, and never submitted to the server.

The `Pages/Customers/Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>
```

The rendered HTML from `Pages/Customers/Create.cshtml`:

```
<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>
```

In the previous code, posting the form:

* With valid data:

  * The `OnPostAsync` handler method calls the [RedirectToPage](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.redirecttopage) helper method. `RedirectToPage` returns an instance of [RedirectToPageResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.redirecttopageresult). `RedirectToPage`:

    * Is an action result.
    * Is similar to `RedirectToAction` or `RedirectToRoute` (used in controllers and views).
    * Is customized for pages. In the preceding sample, it redirects to the root Index page (`/Index`). `RedirectToPage` is detailed in the [URL generation for Pages](#url_gen) section.

* With validation errors that are passed to the server:

  * The `OnPostAsync` handler method calls the [Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagebase.page) helper method. `Page` returns an instance of [PageResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pageresult). Returning `Page` is similar to how actions in controllers return `View`. `PageResult` is the default return type for a handler method. A handler method that returns `void` renders the page.
  * In the preceding example, posting the form with no value results in [ModelState.IsValid](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.isvalid#microsoft-aspnetcore-mvc-modelbinding-modelstatedictionary-isvalid) returning false. In this sample, no validation errors are displayed on the client. Validation error handling is covered later in this document.

  ```
  [BindProperty]
  public Customer? Customer { get; set; }

  public async Task<IActionResult> OnPostAsync()
  {
      if (!ModelState.IsValid)
      {
          return Page();
      }

      if (Customer != null) _context.Customer.Add(Customer);
      await _context.SaveChangesAsync();

      return RedirectToPage("./Index");
  }
  ```

* With validation errors detected by client side validation:

  * Data is **not** posted to the server.
  * Client-side validation is explained later in this document.

The `Customer` property uses [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute to opt in to model binding:

```
[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

`[BindProperty]` should **not** be used on models containing properties that should not be changed by the client. For more information, see [Overposting](../data/ef-rp/crud?view=aspnetcore-10.0#overposting).

Razor Pages, by default, bind properties only with non-`GET` verbs. Binding to properties removes the need to writing code to convert HTTP data to the model type. Binding reduces code by using the same property to render form fields (`<input asp-for="Customer.Name">`) and accept the input.

Warning

For security reasons, you must opt in to binding `GET` request data to page model properties. Verify user input before mapping it to properties. Opting into `GET` binding is useful when addressing scenarios that rely on query string or route values.

To bind a property on `GET` requests, set the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute's `SupportsGet` property to `true`:

```
[BindProperty(SupportsGet = true)]
```

For more information, see [ASP.NET Core Community Standup: Bind on GET discussion (YouTube)](https://www.youtube.com/watch?v=p7iHB9V-KVU\&feature=youtu.be\&t=54m27s).

Reviewing the `Pages/Customers/Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>
```

* In the preceding code, the [input tag helper](../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper) `<input asp-for="Customer.Name" />` binds the HTML `<input>` element to the `Customer.Name` model expression.
* [`@addTagHelper`](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0#addtaghelper-makes-tag-helpers-available) makes Tag Helpers available.

### The home page

`Index.cshtml` is the home page:

```
@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
        @if (Model.Customers != null)
        {
            foreach (var contact in Model.Customers)
            {
                <tr>
                    <td> @contact.Id </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>
```

The associated `PageModel` class (`Index.cshtml.cs`):

```
public class IndexModel : PageModel
{
    private readonly Data.CustomerDbContext _context;
    public IndexModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer>? Customers { get; set; }

    public async Task OnGetAsync()
    {
        Customers = await _context.Customer.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customer.FindAsync(id);

        if (contact != null)
        {
            _context.Customer.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}
```

The `Index.cshtml` file contains the following markup:

```
<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
```

The `<a /a>` [Anchor Tag Helper](../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) used the `asp-route-{value}` attribute to generate a link to the Edit page. The link contains route data with the contact ID. For example, `https://localhost:5001/Edit/1`. [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files.

The `Index.cshtml` file contains markup to create a delete button for each customer contact:

```
<button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
```

The rendered HTML:

```
<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>
```

When the delete button is rendered in HTML, its [formaction](https://developer.mozilla.org/docs/Web/HTML/Element/button#attr-formaction) includes parameters for:

* The customer contact ID, specified by the `asp-route-id` attribute.
* The `handler`, specified by the `asp-page-handler` attribute.

When the button is selected, a form `POST` request is sent to the server. By convention, the name of the handler method is selected based on the value of the `handler` parameter according to the scheme `OnPost[handler]Async`.

Because the `handler` is `delete` in this example, the `OnPostDeleteAsync` handler method is used to process the `POST` request. If the `asp-page-handler` is set to a different value, such as `remove`, a handler method with the name `OnPostRemoveAsync` is selected.

```
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customer.FindAsync(id);

    if (contact != null)
    {
        _context.Customer.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}
```

The `OnPostDeleteAsync` method:

* Gets the `id` from the query string.
* Queries the database for the customer contact with `FindAsync`.
* If the customer contact is found, it's removed and the database is updated.
* Calls [RedirectToPage](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.redirecttopage) to redirect to the root Index page (`/Index`).

### The Edit.cshtml file

```
@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel

@{
    ViewData["Title"] = "Edit";
}

<h1>Edit</h1>

<h4>Customer</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Customer!.Id" />
            <div class="form-group">
                <label asp-for="Customer!.Name" class="control-label"></label>
                <input asp-for="Customer!.Name" class="form-control" />
                <span asp-validation-for="Customer!.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="./Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

The first line contains the `@page "{id:int}"` directive. The routing constraint `"{id:int}"` tells the page to accept requests to the page that contain `int` route data. If a request to the page doesn't contain route data that can be converted to an `int`, the runtime returns an HTTP 404 (not found) error. To make the ID optional, append `?` to the route constraint:

```
@page "{id:int?}"
```

The `Edit.cshtml.cs` file:

```
public class EditModel : PageModel
{
    private readonly RazorPagesContacts.Data.CustomerDbContext _context;

    public EditModel(RazorPagesContacts.Data.CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        Customer = await _context.Customer.FirstOrDefaultAsync(m => m.Id == id);
        
        if (Customer == null)
        {
            return NotFound();
        }
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null)
        {
            _context.Attach(Customer).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!CustomerExists(Customer.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
        }

        return RedirectToPage("./Index");
    }

    private bool CustomerExists(int id)
    {
        return _context.Customer.Any(e => e.Id == id);
    }
}
```

## Validation

Validation rules:

* Are declaratively specified in the model class.
* Are enforced everywhere in the app.

The [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like [`[DataType]`](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) that help with formatting and don't provide any validation.

Consider the `Customer` model:

```
using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string? Name { get; set; }
    }
}
```

Using the following `Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer!.Name"></span>
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
```

The preceding code:

* Includes jQuery and jQuery validation scripts.

* Uses the `<div />` and `<span />` [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) to enable:

  * Client-side validation.
  * Validation error rendering.

* Generates the following HTML:

  ```
  <p>Enter a customer name:</p>

  <form method="post">
      Name:
      <input type="text" data-val="true"
             data-val-length="The field Name must be a string with a maximum length of 10."
             data-val-length-max="10" data-val-required="The Name field is required."
             id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
      <input type="submit" />
      <input name="__RequestVerificationToken" type="hidden"
             value="<Antiforgery token here>" />
  </form>

  <script src="/lib/jquery/dist/jquery.js"></script>
  <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
  <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
  ```

Posting the Create form without a name value displays the error message "The Name field is required." on the form. If JavaScript is enabled on the client, the browser displays the error without posting to the server.

The `[StringLength(10)]` attribute generates `data-val-length-max="10"` on the rendered HTML. `data-val-length-max` prevents browsers from entering more than the maximum length specified. If a tool such as [Fiddler](https://www.telerik.com/fiddler) is used to edit and replay the post:

* With the name longer than 10.
* The error message "The field Name must be a string with a maximum length of 10." is returned.

Consider the following `Movie` model:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}
```

The validation attributes specify behavior to enforce on the model properties they're applied to:

* The `Required` and `MinimumLength` attributes indicate that a property must have a value, but nothing prevents a user from entering white space to satisfy this validation.

* The `RegularExpression` attribute is used to limit what characters can be input. In the preceding code, "Genre":

  * Must only use letters.
  * The first letter is required to be uppercase. White space, numbers, and special characters are not allowed.

* The `RegularExpression` "Rating":

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a "Genre".

* The `Range` attribute constrains a value to within a specified range.

* The `StringLength` attribute sets the maximum length of a string property, and optionally its minimum length.

* Value types (such as `decimal`, `int`, `float`, `DateTime`) are inherently required and don't need the `[Required]` attribute.

The Create page for the `Movie` model shows displays errors with invalid values:

For more information, see:

* [Add validation to the Movie app](../tutorials/razor-pages/validation?view=aspnetcore-10.0)
* [Model validation in ASP.NET Core](../mvc/models/validation?view=aspnetcore-10.0).

## CSS isolation

Isolate CSS styles to individual pages, views, and components to reduce or avoid:

* Dependencies on global styles that can be challenging to maintain.
* Style conflicts in nested content.

To add a *scoped CSS file* for a page or view, place the CSS styles in a companion `.cshtml.css` file matching the name of the `.cshtml` file. In the following example, an `Index.cshtml.css` file supplies CSS styles that are only applied to the `Index.cshtml` page or view.

`Pages/Index.cshtml.css` (Razor Pages) or `Views/Index.cshtml.css` (MVC):

```
h1 {
    color: red;
}
```

CSS isolation occurs at build time. The framework rewrites CSS selectors to match markup rendered by the app's pages or views. The rewritten CSS styles are bundled and produced as a static asset, `{APP ASSEMBLY}.styles.css`. The placeholder `{APP ASSEMBLY}` is the assembly name of the project. A link to the bundled CSS styles is placed in the app's layout.

In the `<head>` content of the app's `Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC), add or confirm the presence of the link to the bundled CSS styles:

```
<link rel="stylesheet" href="~/{APP ASSEMBLY}.styles.css" />
```

In the following example, the app's assembly name is `WebApp`:

```
<link rel="stylesheet" href="WebApp.styles.css" />
```

The styles defined in a scoped CSS file are only applied to the rendered output of the matching file. In the preceding example, any `h1` CSS declarations defined elsewhere in the app don't conflict with the `Index`'s heading style. CSS style cascading and inheritance rules remain in effect for scoped CSS files. For example, styles applied directly to an `<h1>` element in the `Index.cshtml` file override the scoped CSS file's styles in `Index.cshtml.css`.

Note

In order to guarantee CSS style isolation when bundling occurs, importing CSS in Razor code blocks isn't supported.

CSS isolation only applies to HTML elements. CSS isolation isn't supported for [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0).

Within the bundled CSS file, each page, view, or Razor component is associated with a scope identifier in the format `b-{STRING}`, where the `{STRING}` placeholder is a ten-character string generated by the framework. The following example provides the style for the preceding `<h1>` element in the `Index` page of a Razor Pages app:

```
/* /Pages/Index.cshtml.rz.scp.css */
h1[b-3xxtam6d07] {
    color: red;
}
```

In the `Index` page where the CSS style is applied from the bundled file, the scope identifier is appended as an HTML attribute:

```
<h1 b-3xxtam6d07>
```

The identifier is unique to an app. At build time, a project bundle is created with the convention `{STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css`, where the placeholder `{STATIC WEB ASSETS BASE PATH}` is the static web assets base path.

If other projects are utilized, such as NuGet packages or [Razor class libraries](ui-class?view=aspnetcore-10.0), the bundled file:

```
<ItemGroup>
  <None Update="{Pages|Views}/Index.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

In the preceding example, the CSS generated for `Index.cshtml.css` changes its scope identifier from `b-{STRING}` to `custom-scope-identifier`.

Use scope identifiers to achieve inheritance with scoped CSS files. In the following project file example, a `BaseView.cshtml.css` file contains common styles across views. A `DerivedView.cshtml.css` file inherits these styles.

```
<ItemGroup>
  <None Update="{Pages|Views}/BaseView.cshtml.css" CssScope="custom-scope-identifier" />
  <None Update="{Pages|Views}/DerivedView.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

Use the wildcard (`*`) operator to share scope identifiers across multiple files:

```
<ItemGroup>
  <None Update="{Pages|Views}/*.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>
```

### Change base path for static web assets

The scoped CSS file is generated at the root of the app. In the project file, use the `StaticWebAssetBasePath` property to change the default path. The following example places the scoped CSS file, and the rest of the app's assets, at the `_content` path:

```
<PropertyGroup>
  <StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>
```

### Disable automatic bundling

To opt out of how framework publishes and loads scoped files at runtime, use the `DisableScopedCssBundling` property. When using this property, other tools or processes are responsible for taking the isolated CSS files from the `obj` directory and publishing and loading them at runtime:

```
<PropertyGroup>
  <DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>
```

## Razor class library (RCL) support

When a [Razor class library (RCL)](ui-class?view=aspnetcore-10.0) provides isolated styles, the `<link>` tag's `href` attribute points to `{STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css`, where the placeholders are:

* `{STATIC WEB ASSET BASE PATH}`: The static web asset base path.
* `{PACKAGE ID}`: The library's [package identifier](/en-us/nuget/create-packages/creating-a-package-msbuild#set-properties). The package identifier defaults to the project's assembly name if the package identifier isn't specified in the project file.

In the following example:

* The static web asset base path is `_content/ClassLib`.
* The class library's assembly name is `ClassLib`.

`Pages/Shared/_Layout.cshtml` (Razor Pages) or `Views/Shared/_Layout.cshtml` (MVC):

```
<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">
```

For more information on RCLs, see the following articles:

* [Reusable Razor UI in class libraries with ASP.NET Core](ui-class?view=aspnetcore-10.0)
* [Consume ASP.NET Core Razor components from a Razor class library (RCL)](../blazor/components/class-libraries?view=aspnetcore-10.0)

For information on Blazor CSS isolation, see [ASP.NET Core Blazor CSS isolation](../blazor/components/css-isolation?view=aspnetcore-10.0).

## Handle HEAD requests with an OnGet handler fallback

`HEAD` requests allow retrieving the headers for a specific resource. Unlike `GET` requests, `HEAD` requests don't return a response body.

Ordinarily, an `OnHead` handler is created and called for `HEAD` requests:

```
public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}
```

Razor Pages falls back to calling the `OnGet` handler if no `OnHead` handler is defined.

[]()

## XSRF/CSRF and Razor Pages

Razor Pages are protected by [Antiforgery validation](../security/anti-request-forgery?view=aspnetcore-10.0). The [FormTagHelper](../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper) injects antiforgery tokens into HTML form elements.

[]()

## Using Layouts, partials, templates, and Tag Helpers with Razor Pages

Pages work with all the capabilities of the Razor view engine. Layouts, partials, templates, Tag Helpers, `_ViewStart.cshtml`, and `_ViewImports.cshtml` work in the same way they do for conventional Razor views.

Let's declutter this page by taking advantage of some of those capabilities.

Add a [layout page](../mvc/views/layout?view=aspnetcore-10.0) to `Pages/Shared/_Layout.cshtml`:

```
<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>
```

The [Layout](../mvc/views/layout?view=aspnetcore-10.0):

* Controls the layout of each page (unless the page opts out of layout).
* Imports HTML structures such as JavaScript and stylesheets.
* The contents of the Razor page are rendered where `@RenderBody()` is called.

For more information, see [layout page](../mvc/views/layout?view=aspnetcore-10.0).

The [Layout](../mvc/views/layout?view=aspnetcore-10.0#specifying-a-layout) property is set in `Pages/_ViewStart.cshtml`:

```
@{
    Layout = "_Layout";
}
```

The layout is in the *Pages/Shared* folder. Pages look for other views (layouts, templates, partials) hierarchically, starting in the same folder as the current page. A layout in the *Pages/Shared* folder can be used from any Razor page under the *Pages* folder.

The layout file should go in the *Pages/Shared* folder.

We recommend you **not** put the layout file in the *Views/Shared* folder. *Views/Shared* is an MVC views pattern. Razor Pages are meant to rely on folder hierarchy, not path conventions.

View search from a Razor Page includes the *Pages* folder. The layouts, templates, and partials used with MVC controllers and conventional Razor views *just work*.

Add a `Pages/_ViewImports.cshtml` file:

```
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

`@namespace` is explained later in the tutorial. The `@addTagHelper` directive brings in the [built-in Tag Helpers](../mvc/views/tag-helpers/built-in/?view=aspnetcore-10.0) to all the pages in the *Pages* folder.

[]()

The `@namespace` directive set on a page:

```
@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>
```

The `@namespace` directive sets the namespace for the page. The `@model` directive doesn't need to include the namespace.

When the `@namespace` directive is contained in `_ViewImports.cshtml`, the specified namespace supplies the prefix for the generated namespace in the Page that imports the `@namespace` directive. The rest of the generated namespace (the suffix portion) is the dot-separated relative path between the folder containing `_ViewImports.cshtml` and the folder containing the page.

For example, the `PageModel` class `Pages/Customers/Edit.cshtml.cs` explicitly sets the namespace:

```
namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.
```

The `Pages/_ViewImports.cshtml` file sets the following namespace:

```
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

The generated namespace for the `Pages/Customers/Edit.cshtml` Razor Page is the same as the `PageModel` class.

`@namespace` *also works with conventional Razor views.*

Consider the `Pages/Customers/Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer!.Name"></span>
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
```

The updated `Pages/Customers/Create.cshtml` view file with `_ViewImports.cshtml` and the preceding layout file:

```
@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>
```

In the preceding code, the `_ViewImports.cshtml` imported the namespace and Tag Helpers. The layout file imported the JavaScript files.

The [Razor Pages starter project](#rpvs17) contains the `Pages/_ValidationScriptsPartial.cshtml`, which hooks up client-side validation.

For more information on partial views, see [Partial views in ASP.NET Core](../mvc/views/partial?view=aspnetcore-10.0).

[]()

## URL generation for Pages

The `Create` page, shown previously, uses `RedirectToPage`:

```
public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

The app has the following file/folder structure:

* */Pages*

  * `Index.cshtml`

  * `Privacy.cshtml`

  * */Customers*

    * `Create.cshtml`
    * `Edit.cshtml`
    * `Index.cshtml`

The `Pages/Customers/Create.cshtml` and `Pages/Customers/Edit.cshtml` pages redirect to `Pages/Customers/Index.cshtml` after success. The string `./Index` is a relative page name used to access the preceding page. It is used to generate URLs to the `Pages/Customers/Index.cshtml` page. For example:

* `Url.Page("./Index", ...)`
* `<a asp-page="./Index">Customers Index Page</a>`
* `RedirectToPage("./Index")`

The absolute page name `/Index` is used to generate URLs to the `Pages/Index.cshtml` page. For example:

* `Url.Page("/Index", ...)`
* `<a asp-page="/Index">Home Index Page</a>`
* `RedirectToPage("/Index")`

The page name is the path to the page from the root */Pages* folder including a leading `/` (for example, `/Index`). The preceding URL generation samples offer enhanced options and functional capabilities over hard-coding a URL. URL generation uses [routing](../mvc/controllers/routing?view=aspnetcore-10.0) and can generate and encode parameters according to how the route is defined in the destination path.

URL generation for pages supports relative names. The following table shows which Index page is selected using different `RedirectToPage` parameters in `Pages/Customers/Create.cshtml`.

| RedirectToPage(x)          | Page                    |
| -------------------------- | ----------------------- |
| RedirectToPage("/Index")   | *Pages/Index*           |
| RedirectToPage("./Index"); | *Pages/Customers/Index* |
| RedirectToPage("../Index") | *Pages/Index*           |
| RedirectToPage("Index")    | *Pages/Customers/Index* |

`RedirectToPage("Index")`, `RedirectToPage("./Index")`, and `RedirectToPage("../Index")` are *relative names*. The `RedirectToPage` parameter is *combined* with the path of the current page to compute the name of the destination page.

Relative name linking is useful when building sites with a complex structure. When relative names are used to link between pages in a folder:

* Renaming a folder doesn't break the relative links.
* Links are not broken because they don't include the folder name.

To redirect to a page in a different [Area](../mvc/controllers/areas?view=aspnetcore-10.0), specify the area:

```
RedirectToPage("/Index", new { area = "Services" });
```

For more information, see [Areas in ASP.NET Core](../mvc/controllers/areas?view=aspnetcore-10.0) and [Razor Pages route and app conventions in ASP.NET Core](razor-pages-conventions?view=aspnetcore-10.0).

## ViewData attribute

Data can be passed to a page with [ViewDataAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewdataattribute). Properties with the `[ViewData]` attribute have their values stored and loaded from the [ViewDataDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.viewdatadictionary).

In the following example, the `AboutModel` applies the `[ViewData]` attribute to the `Title` property:

```
public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}
```

In the About page, access the `Title` property as a model property:

```
<h1>@Model.Title</h1>
```

In the layout, the title is read from the ViewData dictionary:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...
```

## TempData

ASP.NET Core exposes the [TempData](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.tempdata#microsoft-aspnetcore-mvc-controller-tempdata). This property stores data until it's read. The [Keep](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.tempdatadictionary.keep) and [Peek](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.tempdatadictionary.peek) methods can be used to examine the data without deletion. `TempData` is useful for redirection, when data is needed for more than a single request.

The following code sets the value of `Message` using `TempData`:

```
public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}
```

The following markup in the `Pages/Customers/Index.cshtml` file displays the value of `Message` using `TempData`.

```
<h3>Msg: @Model.Message</h3>
```

The `Pages/Customers/Index.cshtml.cs` page model applies the `[TempData]` attribute to the `Message` property.

```
[TempData]
public string Message { get; set; }
```

For more information, see [TempData](../fundamentals/app-state?view=aspnetcore-10.0#tempdata).

[]()

## Multiple handlers per page

The following page generates markup for two handlers using the `asp-page-handler` Tag Helper:

```
@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div><label>Name: <input asp-for="Customer.Name" /></label></div>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </form>
</body>
</html>
```

The form in the preceding example has two submit buttons, each using the `FormActionTagHelper` to submit to a different URL. The `asp-page-handler` attribute is a companion to `asp-page`. `asp-page-handler` generates URLs that submit to each of the handler methods defined by a page. `asp-page` isn't specified because the sample is linking to the current page.

The page model:

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}
```

The preceding code uses *named handler methods*. Named handler methods are created by taking the text in the name after `On<HTTP Verb>` and before `Async` (if present). In the preceding example, the page methods are OnPost**JoinList**Async and OnPost**JoinListUC**Async. With *OnPost* and *Async* removed, the handler names are `JoinList` and `JoinListUC`.

```
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
```

Using the preceding code, the URL path that submits to `OnPostJoinListAsync` is `https://localhost:5001/Customers/CreateFATH?handler=JoinList`. The URL path that submits to `OnPostJoinListUCAsync` is `https://localhost:5001/Customers/CreateFATH?handler=JoinListUC`.

## Custom routes

Use the `@page` directive to:

* Specify a custom route to a page. For example, the route to the About page can be set to `/Some/Other/Path` with `@page "/Some/Other/Path"`.
* Append segments to a page's default route. For example, an "item" segment can be added to a page's default route with `@page "item"`.
* Append parameters to a page's default route. For example, an ID parameter, `id`, can be required for a page with `@page "{id}"`.

A root-relative path designated by a tilde (`~`) at the beginning of the path is supported. For example, `@page "~/Some/Other/Path"` is the same as `@page "/Some/Other/Path"`.

If you don't like the query string `?handler=JoinList` in the URL, change the route to put the handler name in the path portion of the URL. The route can be customized by adding a route template enclosed in double quotes after the `@page` directive.

```
@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div><label>Name: <input asp-for="Customer.Name" /></label></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>
```

Using the preceding code, the URL path that submits to `OnPostJoinListAsync` is `https://localhost:5001/Customers/CreateFATH/JoinList`. The URL path that submits to `OnPostJoinListUCAsync` is `https://localhost:5001/Customers/CreateFATH/JoinListUC`.

The `?` following `handler` means the route parameter is optional.

## Collocation of JavaScript (JS) files

Collocation of JavaScript (JS) files for pages and views is a convenient way to organize scripts in an app.

Collocate JS files using the following filename extension conventions:

* Pages of Razor Pages apps and views of MVC apps: `.cshtml.js`. Examples:

  * `Pages/Index.cshtml.js` for the `Index` page of a Razor Pages app at `Pages/Index.cshtml`.
  * `Views/Home/Index.cshtml.js` for the `Index` view of an MVC app at `Views/Home/Index.cshtml`.

Collocated JS files are publicly addressable using the ***path to the file in the project***:

* Pages and views from a collocated scripts file in the app:

  `{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js`

  * The `{PATH}` placeholder is the path to the page, view, or component.
  * The `{PAGE, VIEW, OR COMPONENT}` placeholder is the page, view, or component.
  * The `{EXTENSION}` placeholder matches the extension of the page, view, or component, either `razor` or `cshtml`.

  Razor Pages example:

  A JS file for the `Index` page is placed in the `Pages` folder (`Pages/Index.cshtml.js`) next to the `Index` page (`Pages/Index.cshtml`). In the `Index` page, the script is referenced at the path in the `Pages` folder:

  ```
  @section Scripts {
    <script src="~/Pages/Index.cshtml.js"></script>
  }
  ```

The default layout `Pages/Shared/_Layout.cshtml` can be configured to include collocated JS files, eliminating the need to configure each page individually:

```
<script asp-src-include="@(ViewContext.View.Path).js"></script>
```

The [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/tag-helpers/built-in/samples/ScriptTagHelper) uses the preceding code snippet to include collocated JS files in the default layout.

When the app is published, the framework automatically moves the script to the web root. In the preceding example, the script is moved to `bin\Release\{TARGET FRAMEWORK MONIKER}\publish\wwwroot\Pages\Index.cshtml.js`, where the `{TARGET FRAMEWORK MONIKER}` placeholder is the [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks). No change is required to the script's relative URL in the `Index` page.

When the app is published, the framework automatically moves the script to the web root. In the preceding example, the script is moved to `bin\Release\{TARGET FRAMEWORK MONIKER}\publish\wwwroot\Components\Pages\Index.razor.js`, where the `{TARGET FRAMEWORK MONIKER}` placeholder is the [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks). No change is required to the script's relative URL in the `Index` component.

* For scripts provided by a Razor class library (RCL):

  `_content/{PACKAGE ID}/{PATH}/{PAGE, VIEW, OR COMPONENT}.{EXTENSION}.js`

  * The `{PACKAGE ID}` placeholder is the RCL's package identifier (or library name for a class library referenced by the app).
  * The `{PATH}` placeholder is the path to the page, view, or component. If a Razor component is located at the root of the RCL, the path segment isn't included.
  * The `{PAGE, VIEW, OR COMPONENT}` placeholder is the page, view, or component.
  * The `{EXTENSION}` placeholder matches the extension of page, view, or component, either `razor` or `cshtml`.

## Advanced configuration and settings

The configuration and settings in following sections is not required by most apps.

To configure advanced options, use the [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages) overload that configures [RazorPagesOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions):

```
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.RootDirectory = "/MyPages";
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
});

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

Use the [RazorPagesOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions) to set the root directory for pages, or add application model conventions for pages. For more information on conventions, see [Razor Pages authorization conventions](security/authorization/conventions?view=aspnetcore-10.0).

To precompile views, see [Razor view compilation](../mvc/views/view-compilation?view=aspnetcore-10.0).

### Specify that Razor Pages are at the content root

By default, Razor Pages are rooted in the */Pages* directory. Add [WithRazorPagesAtContentRoot](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcrazorpagesmvcbuilderextensions.withrazorpagesatcontentroot) to specify that your Razor Pages are at the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) ([ContentRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingenvironment.contentrootpath#microsoft-aspnetcore-hosting-ihostingenvironment-contentrootpath)) of the app:

```
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesAtContentRoot();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

### Specify that Razor Pages are at a custom root directory

Add [WithRazorPagesRoot](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcrazorpagesmvccorebuilderextensions.withrazorpagesroot) to specify that Razor Pages are at a custom root directory in the app (provide a relative path):

```
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/MyPages/Admin");
})
  .WithRazorPagesRoot("/path/to/razor/pages");

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

## Related content

* See [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0), which builds on this introduction.
* [`[Authorize]` attribute in Razor Pages apps](security/authorization/simple?view=aspnetcore-10.0#authorize-attribute-in-razor-pages-apps)
* [Download or view sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/index/samples/6.0)
* [Overview of ASP.NET Core](../overview?view=aspnetcore-10.0)
* [Razor syntax reference for ASP.NET Core](../mvc/views/razor?view=aspnetcore-10.0)
* [Areas in ASP.NET Core](../mvc/controllers/areas?view=aspnetcore-10.0)
* [Tutorial: Get started with Razor Pages in ASP.NET Core](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Razor Pages authorization conventions in ASP.NET Core](security/authorization/conventions?view=aspnetcore-10.0)
* [Razor Pages route and app conventions in ASP.NET Core](razor-pages-conventions?view=aspnetcore-10.0)
* [Razor Pages unit tests in ASP.NET Core](../test/razor-pages-tests?view=aspnetcore-10.0)
* [Partial views in ASP.NET Core](../mvc/views/partial?view=aspnetcore-10.0)

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

* [Visual Studio 2019 16.4 or later](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the **ASP.NET and web development** workload
* [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)

- [Visual Studio Code](https://code.visualstudio.com/download)
- [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
- [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on any platform (macOS, Linux, or Windows) and with any code editor. Minor changes may be required if you use something other than Visual Studio Code. For more information on installing Visual Studio Code on macOS, see [Visual Studio Code on macOS](https://code.visualstudio.com/docs/setup/mac).

* [Visual Studio for Mac version 8.4 or later](/en-us/lifecycle/announcements/visual-studio-mac-end-of-servicing)
* [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)

- [Visual Studio](#tabpanel_2_visual-studio)
- [Visual Studio Code](#tabpanel_2_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

* [Visual Studio 2019 16.8 or later](https://visualstudio.microsoft.com/downloads/?utm_medium=microsoft\&utm_source=learn.microsoft.com\&utm_campaign=inline+link\&utm_content=download+vs2019) with the **ASP.NET and web development** workload
* [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)

- [Visual Studio Code](https://code.visualstudio.com/download)
- [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
- [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use something other than Visual Studio Code.

* [Visual Studio for Mac](/en-us/lifecycle/announcements/visual-studio-mac-end-of-servicing)
* [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)

[]()

## Create a Razor Pages project

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

See [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0) for detailed instructions on how to create a Razor Pages project.

Run `dotnet new webapp` from the command line.

See [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0) for detailed instructions on how to create a Razor Pages project.

## Razor Pages

Razor Pages is enabled in `Startup.cs`:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

Consider a basic page:[]()

```
@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>
```

The preceding code looks a lot like a [Razor view file](../tutorials/first-mvc-app/adding-view?view=aspnetcore-10.0) used in an ASP.NET Core app with controllers and views. What makes it different is the [`@page`](../mvc/views/razor?view=aspnetcore-10.0#page) directive. `@page` makes the file into an MVC action - which means that it handles requests directly, without going through a controller. `@page` must be the first Razor directive on a page. `@page` affects the behavior of other [Razor](../mvc/views/razor?view=aspnetcore-10.0) constructs. Razor Pages file names have a `.cshtml` suffix.

A similar page, using a `PageModel` class, is shown in the following two files. The `Pages/Index2.cshtml` file:

```
@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>
```

The `Pages/Index2.cshtml.cs` page model:

```
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : PageModel
    {
        public string Message { get; private set; } = "PageModel in C#";

        public void OnGet()
        {
            Message += $" Server time is { DateTime.Now }";
        }
    }
}
```

By convention, the `PageModel` class file has the same name as the Razor Page file with `.cs` appended. For example, the previous Razor Page is `Pages/Index2.cshtml`. The file containing the `PageModel` class is named `Pages/Index2.cshtml.cs`.

The associations of URL paths to pages are determined by the page's location in the file system. The following table shows a Razor Page path and the matching URL.

| File name and path            | matching URL               |
| ----------------------------- | -------------------------- |
| `/Pages/Index.cshtml`         | `/` or `/Index`            |
| `/Pages/Contact.cshtml`       | `/Contact`                 |
| `/Pages/Store/Contact.cshtml` | `/Store/Contact`           |
| `/Pages/Store/Index.cshtml`   | `/Store` or `/Store/Index` |

Notes:

* The runtime looks for Razor Pages files in the *Pages* folder by default.
* `Index` is the default page when a URL doesn't include a page.

## Write a basic form

Razor Pages is designed to make common patterns used with web browsers easy to implement when building an app. [Model binding](../mvc/models/model-binding?view=aspnetcore-10.0), [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0), and HTML helpers all *just work* with the properties defined in a Razor Page class. Consider a page that implements a basic "contact us" form for the `Contact` model:

For the samples in this document, the `DbContext` is initialized in the [Startup.cs](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/razor-pages/index/samples/3.0/RazorPagesContacts/Startup.cs#L23-L24) file.

The in memory database requires the `Microsoft.EntityFrameworkCore.InMemory` NuGet package.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<CustomerDbContext>(options =>
                      options.UseInMemoryDatabase("name"));
    services.AddRazorPages();
}
```

The data model:

```
using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}
```

The db context:

```
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Models;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }
    }
}
```

The `Pages/Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>
```

The `Pages/Create.cshtml.cs` page model:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using RazorPagesContacts.Models;
using System.Threading.Tasks;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateModel : PageModel
    {
        private readonly CustomerDbContext _context;

        public CreateModel(CustomerDbContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Customers.Add(Customer);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}
```

By convention, the `PageModel` class is called `<PageName>Model` and is in the same namespace as the page.

The `PageModel` class allows separation of the logic of a page from its presentation. It defines page handlers for requests sent to the page and the data used to render the page. This separation allows:

* Managing of page dependencies through [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0).
* [Unit testing](../test/razor-pages-tests?view=aspnetcore-10.0)

The page has an `OnPostAsync` *handler method*, which runs on `POST` requests (when a user posts the form). Handler methods for any HTTP verb can be added. The most common handlers are:

* `OnGet` to initialize state needed for the page. In the preceding code, the `OnGet` method displays the `CreateModel.cshtml` Razor Page.
* `OnPost` to handle form submissions.

The `Async` naming suffix is optional but is often used by convention for asynchronous functions. The preceding code is typical for Razor Pages.

If you're familiar with ASP.NET apps using controllers and views:

* The `OnPostAsync` code in the preceding example looks similar to typical controller code.
* Most of the MVC primitives like [model binding](../mvc/models/model-binding?view=aspnetcore-10.0), [validation](../mvc/models/validation?view=aspnetcore-10.0), and action results work the same with Controllers and Razor Pages.

The previous `OnPostAsync` method:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Customers.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

The basic flow of `OnPostAsync`:

Check for validation errors.

* If there are no errors, save the data and redirect.
* If there are errors, show the page again with validation messages. In many cases, validation errors would be detected on the client, and never submitted to the server.

The `Pages/Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>
```

The rendered HTML from `Pages/Create.cshtml`:

```
<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>
```

In the previous code, posting the form:

* With valid data:

  * The `OnPostAsync` handler method calls the [RedirectToPage](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.redirecttopage) helper method. `RedirectToPage` returns an instance of [RedirectToPageResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.redirecttopageresult). `RedirectToPage`:

    * Is an action result.
    * Is similar to `RedirectToAction` or `RedirectToRoute` (used in controllers and views).
    * Is customized for pages. In the preceding sample, it redirects to the root Index page (`/Index`). `RedirectToPage` is detailed in the [URL generation for Pages](#url_gen) section.

* With validation errors that are passed to the server:

  * The `OnPostAsync` handler method calls the [Page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagebase.page) helper method. `Page` returns an instance of [PageResult](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pageresult). Returning `Page` is similar to how actions in controllers return `View`. `PageResult` is the default return type for a handler method. A handler method that returns `void` renders the page.
  * In the preceding example, posting the form with no value results in [ModelState.IsValid](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.isvalid#microsoft-aspnetcore-mvc-modelbinding-modelstatedictionary-isvalid) returning false. In this sample, no validation errors are displayed on the client. Validation error handling is covered later in this document.

  ```
  public async Task<IActionResult> OnPostAsync()
  {
      if (!ModelState.IsValid)
      {
          return Page();
      }

      _context.Customers.Add(Customer);
      await _context.SaveChangesAsync();

      return RedirectToPage("./Index");
  }
  ```

* With validation errors detected by client side validation:

  * Data is **not** posted to the server.
  * Client-side validation is explained later in this document.

The `Customer` property uses [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute to opt in to model binding:

```
public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

`[BindProperty]` should **not** be used on models containing properties that should not be changed by the client. For more information, see [Overposting](../data/ef-rp/crud?view=aspnetcore-10.0#overposting).

Razor Pages, by default, bind properties only with non-`GET` verbs. Binding to properties removes the need to writing code to convert HTTP data to the model type. Binding reduces code by using the same property to render form fields (`<input asp-for="Customer.Name">`) and accept the input.

Warning

For security reasons, you must opt in to binding `GET` request data to page model properties. Verify user input before mapping it to properties. Opting into `GET` binding is useful when addressing scenarios that rely on query string or route values.

To bind a property on `GET` requests, set the [`[BindProperty]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.bindpropertyattribute) attribute's `SupportsGet` property to `true`:

```
[BindProperty(SupportsGet = true)]
```

For more information, see [ASP.NET Core Community Standup: Bind on GET discussion (YouTube)](https://www.youtube.com/watch?v=p7iHB9V-KVU\&feature=youtu.be\&t=54m27s).

Reviewing the `Pages/Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>
```

* In the preceding code, the [input tag helper](../mvc/views/working-with-forms?view=aspnetcore-10.0#the-input-tag-helper) `<input asp-for="Customer.Name" />` binds the HTML `<input>` element to the `Customer.Name` model expression.
* [`@addTagHelper`](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0#addtaghelper-makes-tag-helpers-available) makes Tag Helpers available.

### The home page

`Index.cshtml` is the home page:

```
@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var contact in Model.Customer)
            {
                <tr>
                    <td> @contact.Id  </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>
```

The associated `PageModel` class (`Index.cshtml.cs`):

```
public class IndexModel : PageModel
{
    private readonly CustomerDbContext _context;

    public IndexModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer> Customer { get; set; }

    public async Task OnGetAsync()
    {
        Customer = await _context.Customers.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customers.FindAsync(id);

        if (contact != null)
        {
            _context.Customers.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}
```

The `Index.cshtml` file contains the following markup:

```
<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
```

The `<a /a>` [Anchor Tag Helper](../mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) used the `asp-route-{value}` attribute to generate a link to the Edit page. The link contains route data with the contact ID. For example, `https://localhost:5001/Edit/1`. [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) enable server-side code to participate in creating and rendering HTML elements in Razor files.

The `Index.cshtml` file contains markup to create a delete button for each customer contact:

```
<button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
```

The rendered HTML:

```
<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>
```

When the delete button is rendered in HTML, its [formaction](https://developer.mozilla.org/docs/Web/HTML/Element/button#attr-formaction) includes parameters for:

* The customer contact ID, specified by the `asp-route-id` attribute.
* The `handler`, specified by the `asp-page-handler` attribute.

When the button is selected, a form `POST` request is sent to the server. By convention, the name of the handler method is selected based on the value of the `handler` parameter according to the scheme `OnPost[handler]Async`.

Because the `handler` is `delete` in this example, the `OnPostDeleteAsync` handler method is used to process the `POST` request. If the `asp-page-handler` is set to a different value, such as `remove`, a handler method with the name `OnPostRemoveAsync` is selected.

```
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customers.FindAsync(id);

    if (contact != null)
    {
        _context.Customers.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}
```

The `OnPostDeleteAsync` method:

* Gets the `id` from the query string.
* Queries the database for the customer contact with `FindAsync`.
* If the customer contact is found, it's removed and the database is updated.
* Calls [RedirectToPage](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.redirecttopage) to redirect to the root Index page (`/Index`).

### The Edit.cshtml file

```
@page "{id:int}"
@model RazorPagesContacts.Pages.Customers.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
    <div asp-validation-summary="All"></div>
    <input asp-for="Customer.Id" type="hidden" />
    <div>
        <label asp-for="Customer.Name"></label>
        <div>
            <input asp-for="Customer.Name" />
            <span asp-validation-for="Customer.Name"></span>
        </div>
    </div>

    <div>
        <button type="submit">Save</button>
    </div>
</form>
```

The first line contains the `@page "{id:int}"` directive. The routing constraint `"{id:int}"` tells the page to accept requests to the page that contain `int` route data. If a request to the page doesn't contain route data that can be converted to an `int`, the runtime returns an HTTP 404 (not found) error. To make the ID optional, append `?` to the route constraint:

```
@page "{id:int?}"
```

The `Edit.cshtml.cs` file:

```
public class EditModel : PageModel
{
    private readonly CustomerDbContext _context;

    public EditModel(CustomerDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Customer = await _context.Customers.FindAsync(id);

        if (Customer == null)
        {
            return RedirectToPage("./Index");
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Customer).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            throw new Exception($"Customer {Customer.Id} not found!");
        }

        return RedirectToPage("./Index");
    }

}
```

## Validation

Validation rules:

* Are declaratively specified in the model class.
* Are enforced everywhere in the app.

The [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like [`[DataType]`](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) that help with formatting and don't provide any validation.

Consider the `Customer` model:

```
using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string Name { get; set; }
    }
}
```

Using the following `Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
```

The preceding code:

* Includes jQuery and jQuery validation scripts.

* Uses the `<div />` and `<span />` [Tag Helpers](../mvc/views/tag-helpers/intro?view=aspnetcore-10.0) to enable:

  * Client-side validation.
  * Validation error rendering.

* Generates the following HTML:

  ```
  <p>Enter a customer name:</p>

  <form method="post">
      Name:
      <input type="text" data-val="true"
             data-val-length="The field Name must be a string with a maximum length of 10."
             data-val-length-max="10" data-val-required="The Name field is required."
             id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
      <input type="submit" />
      <input name="__RequestVerificationToken" type="hidden"
             value="<Antiforgery token here>" />
  </form>

  <script src="/lib/jquery/dist/jquery.js"></script>
  <script src="/lib/jquery-validation/dist/jquery.validate.js"></script>
  <script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
  ```

Posting the Create form without a name value displays the error message "The Name field is required." on the form. If JavaScript is enabled on the client, the browser displays the error without posting to the server.

The `[StringLength(10)]` attribute generates `data-val-length-max="10"` on the rendered HTML. `data-val-length-max` prevents browsers from entering more than the maximum length specified. If a tool such as [Fiddler](https://www.telerik.com/fiddler) is used to edit and replay the post:

* With the name longer than 10.
* The error message "The field Name must be a string with a maximum length of 10." is returned.

Consider the following `Movie` model:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string Rating { get; set; }
    }
}
```

The validation attributes specify behavior to enforce on the model properties they're applied to:

* The `Required` and `MinimumLength` attributes indicate that a property must have a value, but nothing prevents a user from entering white space to satisfy this validation.

* The `RegularExpression` attribute is used to limit what characters can be input. In the preceding code, "Genre":

  * Must only use letters.
  * The first letter is required to be uppercase. White space, numbers, and special characters are not allowed.

* The `RegularExpression` "Rating":

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a "Genre".

* The `Range` attribute constrains a value to within a specified range.

* The `StringLength` attribute sets the maximum length of a string property, and optionally its minimum length.

* Value types (such as `decimal`, `int`, `float`, `DateTime`) are inherently required and don't need the `[Required]` attribute.

The Create page for the `Movie` model shows displays errors with invalid values:

For more information, see:

* [Add validation to the Movie app](../tutorials/razor-pages/validation?view=aspnetcore-10.0)
* [Model validation in ASP.NET Core](../mvc/models/validation?view=aspnetcore-10.0).

## Handle HEAD requests with an OnGet handler fallback

`HEAD` requests allow retrieving the headers for a specific resource. Unlike `GET` requests, `HEAD` requests don't return a response body.

Ordinarily, an `OnHead` handler is created and called for `HEAD` requests:

```
public void OnHead()
{
    HttpContext.Response.Headers.Add("Head Test", "Handled by OnHead!");
}
```

Razor Pages falls back to calling the `OnGet` handler if no `OnHead` handler is defined.

[]()

## XSRF/CSRF and Razor Pages

Razor Pages are protected by [Antiforgery validation](../security/anti-request-forgery?view=aspnetcore-10.0). The [FormTagHelper](../mvc/views/working-with-forms?view=aspnetcore-10.0#the-form-tag-helper) injects antiforgery tokens into HTML form elements.

[]()

## Using Layouts, partials, templates, and Tag Helpers with Razor Pages

Pages work with all the capabilities of the Razor view engine. Layouts, partials, templates, Tag Helpers, `_ViewStart.cshtml`, and `_ViewImports.cshtml` work in the same way they do for conventional Razor views.

Let's declutter this page by taking advantage of some of those capabilities.

Add a [layout page](../mvc/views/layout?view=aspnetcore-10.0) to `Pages/Shared/_Layout.cshtml`:

```
<!DOCTYPE html>
<html>
<head>
    <title>RP Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <a asp-page="/Index">Home</a>
    <a asp-page="/Customers/Create">Create</a>
    <a asp-page="/Customers/Index">Customers</a> <br />

    @RenderBody()
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</body>
</html>
```

The [Layout](../mvc/views/layout?view=aspnetcore-10.0):

* Controls the layout of each page (unless the page opts out of layout).
* Imports HTML structures such as JavaScript and stylesheets.
* The contents of the Razor page are rendered where `@RenderBody()` is called.

For more information, see [layout page](../mvc/views/layout?view=aspnetcore-10.0).

The [Layout](../mvc/views/layout?view=aspnetcore-10.0#specifying-a-layout) property is set in `Pages/_ViewStart.cshtml`:

```
@{
    Layout = "_Layout";
}
```

The layout is in the *Pages/Shared* folder. Pages look for other views (layouts, templates, partials) hierarchically, starting in the same folder as the current page. A layout in the *Pages/Shared* folder can be used from any Razor page under the *Pages* folder.

The layout file should go in the *Pages/Shared* folder.

We recommend you **not** put the layout file in the *Views/Shared* folder. *Views/Shared* is an MVC views pattern. Razor Pages are meant to rely on folder hierarchy, not path conventions.

View search from a Razor Page includes the *Pages* folder. The layouts, templates, and partials used with MVC controllers and conventional Razor views *just work*.

Add a `Pages/_ViewImports.cshtml` file:

```
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

`@namespace` is explained later in the tutorial. The `@addTagHelper` directive brings in the [built-in Tag Helpers](../mvc/views/tag-helpers/built-in/?view=aspnetcore-10.0) to all the pages in the *Pages* folder.

[]()

The `@namespace` directive set on a page:

```
@page
@namespace RazorPagesIntro.Pages.Customers

@model NameSpaceModel

<h2>Name space</h2>
<p>
    @Model.Message
</p>
```

The `@namespace` directive sets the namespace for the page. The `@model` directive doesn't need to include the namespace.

When the `@namespace` directive is contained in `_ViewImports.cshtml`, the specified namespace supplies the prefix for the generated namespace in the Page that imports the `@namespace` directive. The rest of the generated namespace (the suffix portion) is the dot-separated relative path between the folder containing `_ViewImports.cshtml` and the folder containing the page.

For example, the `PageModel` class `Pages/Customers/Edit.cshtml.cs` explicitly sets the namespace:

```
namespace RazorPagesContacts.Pages
{
    public class EditModel : PageModel
    {
        private readonly AppDbContext _db;

        public EditModel(AppDbContext db)
        {
            _db = db;
        }

        // Code removed for brevity.
```

The `Pages/_ViewImports.cshtml` file sets the following namespace:

```
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

The generated namespace for the `Pages/Customers/Edit.cshtml` Razor Page is the same as the `PageModel` class.

`@namespace` *also works with conventional Razor views.*

Consider the `Pages/Create.cshtml` view file:

```
@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Validation: customer name:</p>

<form method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <span asp-validation-for="Customer.Name"></span>
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>

<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
```

The updated `Pages/Create.cshtml` view file with `_ViewImports.cshtml` and the preceding layout file:

```
@page
@model CreateModel

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer.Name" />
    <input type="submit" />
</form>
```

In the preceding code, the `_ViewImports.cshtml` imported the namespace and Tag Helpers. The layout file imported the JavaScript files.

The [Razor Pages starter project](#rpvs17) contains the `Pages/_ValidationScriptsPartial.cshtml`, which hooks up client-side validation.

For more information on partial views, see [Partial views in ASP.NET Core](../mvc/views/partial?view=aspnetcore-10.0).

[]()

## URL generation for Pages

The `Create` page, shown previously, uses `RedirectToPage`:

```
public class CreateModel : PageModel
{
    private readonly CustomerDbContext _context;

    public CreateModel(CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Customers.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
```

The app has the following file/folder structure:

* */Pages*

  * `Index.cshtml`

  * `Privacy.cshtml`

  * */Customers*

    * `Create.cshtml`
    * `Edit.cshtml`
    * `Index.cshtml`

The `Pages/Customers/Create.cshtml` and `Pages/Customers/Edit.cshtml` pages redirect to `Pages/Customers/Index.cshtml` after success. The string `./Index` is a relative page name used to access the preceding page. It is used to generate URLs to the `Pages/Customers/Index.cshtml` page. For example:

* `Url.Page("./Index", ...)`
* `<a asp-page="./Index">Customers Index Page</a>`
* `RedirectToPage("./Index")`

The absolute page name `/Index` is used to generate URLs to the `Pages/Index.cshtml` page. For example:

* `Url.Page("/Index", ...)`
* `<a asp-page="/Index">Home Index Page</a>`
* `RedirectToPage("/Index")`

The page name is the path to the page from the root */Pages* folder including a leading `/` (for example, `/Index`). The preceding URL generation samples offer enhanced options and functional capabilities over hard-coding a URL. URL generation uses [routing](../mvc/controllers/routing?view=aspnetcore-10.0) and can generate and encode parameters according to how the route is defined in the destination path.

URL generation for pages supports relative names. The following table shows which Index page is selected using different `RedirectToPage` parameters in `Pages/Customers/Create.cshtml`.

| RedirectToPage(x)          | Page                    |
| -------------------------- | ----------------------- |
| RedirectToPage("/Index")   | *Pages/Index*           |
| RedirectToPage("./Index"); | *Pages/Customers/Index* |
| RedirectToPage("../Index") | *Pages/Index*           |
| RedirectToPage("Index")    | *Pages/Customers/Index* |

`RedirectToPage("Index")`, `RedirectToPage("./Index")`, and `RedirectToPage("../Index")` are *relative names*. The `RedirectToPage` parameter is *combined* with the path of the current page to compute the name of the destination page.

Relative name linking is useful when building sites with a complex structure. When relative names are used to link between pages in a folder:

* Renaming a folder doesn't break the relative links.
* Links are not broken because they don't include the folder name.

To redirect to a page in a different [Area](../mvc/controllers/areas?view=aspnetcore-10.0), specify the area:

```
RedirectToPage("/Index", new { area = "Services" });
```

For more information, see [Areas in ASP.NET Core](../mvc/controllers/areas?view=aspnetcore-10.0) and [Razor Pages route and app conventions in ASP.NET Core](razor-pages-conventions?view=aspnetcore-10.0).

## ViewData attribute

Data can be passed to a page with [ViewDataAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewdataattribute). Properties with the `[ViewData]` attribute have their values stored and loaded from the [ViewDataDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.viewdatadictionary).

In the following example, the `AboutModel` applies the `[ViewData]` attribute to the `Title` property:

```
public class AboutModel : PageModel
{
    [ViewData]
    public string Title { get; } = "About";

    public void OnGet()
    {
    }
}
```

In the About page, access the `Title` property as a model property:

```
<h1>@Model.Title</h1>
```

In the layout, the title is read from the ViewData dictionary:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...
```

## TempData

ASP.NET Core exposes the [TempData](/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller.tempdata#microsoft-aspnetcore-mvc-controller-tempdata). This property stores data until it's read. The [Keep](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.tempdatadictionary.keep) and [Peek](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.tempdatadictionary.peek) methods can be used to examine the data without deletion. `TempData` is useful for redirection, when data is needed for more than a single request.

The following code sets the value of `Message` using `TempData`:

```
public class CreateDotModel : PageModel
{
    private readonly AppDbContext _db;

    public CreateDotModel(AppDbContext db)
    {
        _db = db;
    }

    [TempData]
    public string Message { get; set; }

    [BindProperty]
    public Customer Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _db.Customers.Add(Customer);
        await _db.SaveChangesAsync();
        Message = $"Customer {Customer.Name} added";
        return RedirectToPage("./Index");
    }
}
```

The following markup in the `Pages/Customers/Index.cshtml` file displays the value of `Message` using `TempData`.

```
<h3>Msg: @Model.Message</h3>
```

The `Pages/Customers/Index.cshtml.cs` page model applies the `[TempData]` attribute to the `Message` property.

```
[TempData]
public string Message { get; set; }
```

For more information, see [TempData](../fundamentals/app-state?view=aspnetcore-10.0#tempdata).

[]()

## Multiple handlers per page

The following page generates markup for two handlers using the `asp-page-handler` Tag Helper:

```
@page
@model CreateFATHModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div><label>Name: <input asp-for="Customer.Name" /></label></div>
        <!-- <snippet_Handlers> -->
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
        <!-- </snippet_Handlers> -->
    </form>
</body>
</html>
```

The form in the preceding example has two submit buttons, each using the `FormActionTagHelper` to submit to a different URL. The `asp-page-handler` attribute is a companion to `asp-page`. `asp-page-handler` generates URLs that submit to each of the handler methods defined by a page. `asp-page` isn't specified because the sample is linking to the current page.

The page model:

```
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;

namespace RazorPagesContacts.Pages.Customers
{
    public class CreateFATHModel : PageModel
    {
        private readonly AppDbContext _db;

        public CreateFATHModel(AppDbContext db)
        {
            _db = db;
        }

        [BindProperty]
        public Customer Customer { get; set; }

        public async Task<IActionResult> OnPostJoinListAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _db.Customers.Add(Customer);
            await _db.SaveChangesAsync();
            return RedirectToPage("/Index");
        }

        public async Task<IActionResult> OnPostJoinListUCAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
            Customer.Name = Customer.Name?.ToUpperInvariant();
            return await OnPostJoinListAsync();
        }
    }
}
```

The preceding code uses *named handler methods*. Named handler methods are created by taking the text in the name after `On<HTTP Verb>` and before `Async` (if present). In the preceding example, the page methods are OnPost**JoinList**Async and OnPost**JoinListUC**Async. With *OnPost* and *Async* removed, the handler names are `JoinList` and `JoinListUC`.

```
<input type="submit" asp-page-handler="JoinList" value="Join" />
<input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
```

Using the preceding code, the URL path that submits to `OnPostJoinListAsync` is `https://localhost:5001/Customers/CreateFATH?handler=JoinList`. The URL path that submits to `OnPostJoinListUCAsync` is `https://localhost:5001/Customers/CreateFATH?handler=JoinListUC`.

## Custom routes

Use the `@page` directive to:

* Specify a custom route to a page. For example, the route to the About page can be set to `/Some/Other/Path` with `@page "/Some/Other/Path"`.
* Append segments to a page's default route. For example, an "item" segment can be added to a page's default route with `@page "item"`.
* Append parameters to a page's default route. For example, an ID parameter, `id`, can be required for a page with `@page "{id}"`.

A root-relative path designated by a tilde (`~`) at the beginning of the path is supported. For example, `@page "~/Some/Other/Path"` is the same as `@page "/Some/Other/Path"`.

If you don't like the query string `?handler=JoinList` in the URL, change the route to put the handler name in the path portion of the URL. The route can be customized by adding a route template enclosed in double quotes after the `@page` directive.

```
@page "{handler?}"
@model CreateRouteModel

<html>
<body>
    <p>
        Enter your name.
    </p>
    <div asp-validation-summary="All"></div>
    <form method="POST">
        <div><label>Name: <input asp-for="Customer.Name" /></label></div>
        <input type="submit" asp-page-handler="JoinList" value="Join" />
        <input type="submit" asp-page-handler="JoinListUC" value="JOIN UC" />
    </form>
</body>
</html>
```

Using the preceding code, the URL path that submits to `OnPostJoinListAsync` is `https://localhost:5001/Customers/CreateFATH/JoinList`. The URL path that submits to `OnPostJoinListUCAsync` is `https://localhost:5001/Customers/CreateFATH/JoinListUC`.

The `?` following `handler` means the route parameter is optional.

## Advanced configuration and settings

The configuration and settings in following sections is not required by most apps.

To configure advanced options, use the [AddRazorPages](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages) overload that configures [RazorPagesOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions):

```
public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
    {
        options.RootDirectory = "/MyPages";
        options.Conventions.AuthorizeFolder("/MyPages/Admin");
    });
}
```

Use the [RazorPagesOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions) to set the root directory for pages, or add application model conventions for pages. For more information on conventions, see [Razor Pages authorization conventions](security/authorization/conventions?view=aspnetcore-10.0).

To precompile views, see [Razor view compilation](../mvc/views/view-compilation?view=aspnetcore-10.0).

### Specify that Razor Pages are at the content root

By default, Razor Pages are rooted in the */Pages* directory. Add [WithRazorPagesAtContentRoot](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcrazorpagesmvcbuilderextensions.withrazorpagesatcontentroot) to specify that your Razor Pages are at the [content root](../fundamentals/?view=aspnetcore-10.0#content-root) ([ContentRootPath](/en-us/dotnet/api/microsoft.aspnetcore.hosting.ihostingenvironment.contentrootpath#microsoft-aspnetcore-hosting-ihostingenvironment-contentrootpath)) of the app:

```
public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesAtContentRoot();
}
```

### Specify that Razor Pages are at a custom root directory

Add [WithRazorPagesRoot](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.mvcrazorpagesmvccorebuilderextensions.withrazorpagesroot) to specify that Razor Pages are at a custom root directory in the app (provide a relative path):

```
public void ConfigureServices(IServiceCollection services)
{            
    services.AddRazorPages(options =>
        {
            options.Conventions.AuthorizeFolder("/MyPages/Admin");
        })
        .WithRazorPagesRoot("/path/to/razor/pages");
}
```

## Related content

* See [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0), which builds on this introduction.
* [`[Authorize]` attribute in Razor Pages apps](security/authorization/simple?view=aspnetcore-10.0#authorize-attribute-in-razor-pages-apps)
* [Download or view sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/index/samples/3.0)
* [Overview of ASP.NET Core](../overview?view=aspnetcore-10.0)
* [Razor syntax reference for ASP.NET Core](../mvc/views/razor?view=aspnetcore-10.0)
* [Areas in ASP.NET Core](../mvc/controllers/areas?view=aspnetcore-10.0)
* [Tutorial: Get started with Razor Pages in ASP.NET Core](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Razor Pages authorization conventions in ASP.NET Core](security/authorization/conventions?view=aspnetcore-10.0)
* [Razor Pages route and app conventions in ASP.NET Core](razor-pages-conventions?view=aspnetcore-10.0)
* [Razor Pages unit tests in ASP.NET Core](../test/razor-pages-tests?view=aspnetcore-10.0)
* [Partial views in ASP.NET Core](../mvc/views/partial?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/authentication-state
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/authentication-state.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor authentication state

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to create a custom [authentication state provider](./?view=aspnetcore-10.0#authenticationstateprovider-service) and receive user authentication state change notifications in code.

The general approaches taken for server-side and client-side Blazor apps are similar but differ in their exact implementations, so this article pivots between server-side Blazor apps and client-side Blazor apps. Use the pivot selector at the top of the article to change the article's pivot to match the type of Blazor project that you're working with:

* Server-side Blazor apps (**Server** pivot): Blazor Server for .NET 7 or earlier and the server project of a Blazor Web App for .NET 8 or later.
* Client-side Blazor apps (**Blazor WebAssembly** pivot): Blazor WebAssembly for all versions of .NET or the `.Client` project of a Blazor Web App for .NET 8 or later.

## Abstract `AuthenticationStateProvider` class

The Blazor framework includes an abstract [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) class to provide information about the authentication state of the current user with the following members:

* [GetAuthenticationStateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.getauthenticationstateasync): Asynchronously gets the authentication state of the current user.
* [AuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.authenticationstatechanged#microsoft-aspnetcore-components-authorization-authenticationstateprovider-authenticationstatechanged): An event that provides notification when the authentication state has changed. For example, this event may be raised if a user signs in or out of the app.
* [NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged): Raises an authentication state changed event.

## Implement a custom `AuthenticationStateProvider`

The app must reference the [`Microsoft.AspNetCore.Components.Authorization` NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization), which provides authentication and authorization support for Blazor apps.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Configure the following authentication, authorization, and cascading authentication state services in the `Program` file.

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app is preconfigured with the following service registrations, which includes exposing the authentication state as a cascading parameter. For more information, see [ASP.NET Core Blazor authentication and authorization](./?view=aspnetcore-10.0#expose-the-authentication-state-as-a-cascading-parameter) with additional information presented in the article's [Customize unauthorized content with the `Router` component](./?view=aspnetcore-10.0#customize-unauthorized-content-with-the-router-component) section.

```
using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
```

Configure authentication and authorization services in the `Program` file.

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app includes the following service registration.

```
using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddAuthorization();
```

Configure authentication and authorization services in `Startup.ConfigureServices` of `Startup.cs`.

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app includes the following service registration.

```
using Microsoft.AspNetCore.Components.Authorization;

...

services.AddAuthorization();
```

In Blazor WebAssembly apps (all .NET versions) or the `.Client` project of a Blazor Web App (.NET 8 or later), configure authentication, authorization, and cascading authentication state services in the `Program` file.

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app is preconfigured with the following service registrations, which includes exposing the authentication state as a cascading parameter. For more information, see [ASP.NET Core Blazor authentication and authorization](./?view=aspnetcore-10.0#expose-the-authentication-state-as-a-cascading-parameter) with additional information presented in the article's [Customize unauthorized content with the `Router` component](./?view=aspnetcore-10.0#customize-unauthorized-content-with-the-router-component) section.

```
using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
```

Configure authentication and authorization services in the `Program` file.

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app includes the following service registration.

```
using Microsoft.AspNetCore.Components.Authorization;

...

builder.Services.AddAuthorizationCore();
```

Subclass [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) and override [GetAuthenticationStateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.getauthenticationstateasync) to create the user's authentication state. In the following example, all users are authenticated with the username `mrfibuli`.

`CustomAuthStateProvider.cs`:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity(
        [
            new Claim(ClaimTypes.Name, "mrfibuli"),
        ], "Custom Authentication");

        var user = new ClaimsPrincipal(identity);

        return Task.FromResult(new AuthenticationState(user));
    }
}
```

Note

The preceding code that creates a new [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) uses simplified collection initialization introduced with C# 12 (.NET 8). For more information, see [Collection expressions - C# language reference](/en-us/dotnet/csharp/language-reference/operators/collection-expressions).

The `CustomAuthStateProvider` service is registered in the `Program` file. Register the service *scoped* with [AddScoped](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addscoped):

```
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
```

In a Blazor Server app, register the service *scoped* with [AddScoped](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addscoped) ***after*** the call to [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor):

```
builder.Services.AddServerSideBlazor();

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
```

In a Blazor Server app, register the service *scoped* with [AddScoped](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addscoped) ***after*** the call to [AddServerSideBlazor](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.componentservicecollectionextensions.addserversideblazor):

```
services.AddServerSideBlazor();

services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
```

The `CustomAuthStateProvider` service is registered in the `Program` file. Register the service *singleton* with [AddSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addsingleton):

```
builder.Services.AddSingleton<AuthenticationStateProvider, CustomAuthStateProvider>();
```

If it isn't present, add an [`@using`](../../mvc/views/razor?view=aspnetcore-10.0#using) statement to the imports file (`_Imports.razor`) to make the [Microsoft.AspNetCore.Components.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization) namespace available across components:

```
@using Microsoft.AspNetCore.Components.Authorization;
```

Confirm or change the route view component to an [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) in the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component definition. The location of the `Router` component differs depending on the type of app. Use search to locate the component if you're unaware of its location in the project.

```
<Router ...>
    <Found ...>
        <AuthorizeRouteView RouteData="routeData" 
            DefaultLayout="typeof(Layout.MainLayout)" />
        ...
    </Found>
</Router>
```

Note

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app includes the [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) component. For more information, see [ASP.NET Core Blazor authentication and authorization](./?view=aspnetcore-10.0#expose-the-authentication-state-as-a-cascading-parameter) with additional information presented in the article's [Customize unauthorized content with the `Router` component](./?view=aspnetcore-10.0#customize-unauthorized-content-with-the-router-component) section.

Where the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component is located:

* Confirm or change the route view component to an [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview).
* Confirm or add a [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) component around the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component.

The location of the `Router` component differs depending on the type of app. Use search to locate the component if you're unaware of its location in the project.

```
<CascadingAuthenticationState>
    <Router ...>
        <Found ...>
            <AuthorizeRouteView RouteData="routeData" 
                DefaultLayout="typeof(MainLayout)" />
            ...
        </Found>
    </Router>
</CascadingAuthenticationState>
```

Note

When you create a Blazor app from one of the Blazor project templates with authentication enabled, the app includes the [AuthorizeRouteView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizerouteview) and [CascadingAuthenticationState](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.cascadingauthenticationstate) components. For more information, see [ASP.NET Core Blazor authentication and authorization](./?view=aspnetcore-10.0#expose-the-authentication-state-as-a-cascading-parameter) with additional information presented in the article's [Customize unauthorized content with the `Router` component](./?view=aspnetcore-10.0#customize-unauthorized-content-with-the-router-component) section.

The following example [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component demonstrates the authenticated user's name:

```
<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>
```

For guidance on the use of [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview), see [ASP.NET Core Blazor authentication and authorization](./?view=aspnetcore-10.0#authorizeview-component).

## Authentication state change notifications

A [custom `AuthenticationStateProvider`](#implement-a-custom-authenticationstateprovider) can invoke [NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged) on the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) base class to notify consumers of the authentication state change to rerender.

The following example is based on implementing a custom [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) by following the guidance in the [Implement a custom `AuthenticationStateProvider`](#implement-a-custom-authenticationstateprovider) section earlier in this article. If you already followed the guidance in that section, the following `CustomAuthStateProvider` replaces the one shown in the section.

The following `CustomAuthStateProvider` implementation exposes a custom method, `AuthenticateUser`, to sign in a user and notify consumers of the authentication state change.

`CustomAuthStateProvider.cs`:

```
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var identity = new ClaimsIdentity();
        var user = new ClaimsPrincipal(identity);

        return Task.FromResult(new AuthenticationState(user));
    }

    public void AuthenticateUser(string userIdentifier)
    {
        var identity = new ClaimsIdentity(
        [
            new Claim(ClaimTypes.Name, userIdentifier),
        ], "Custom Authentication");

        var user = new ClaimsPrincipal(identity);

        NotifyAuthenticationStateChanged(
            Task.FromResult(new AuthenticationState(user)));
    }
}
```

Note

The preceding code that creates a new [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) uses simplified collection initialization introduced with C# 12 (.NET 8). For more information, see [Collection expressions - C# language reference](/en-us/dotnet/csharp/language-reference/operators/collection-expressions).

In a component:

* Inject [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider).
* Add a field to hold the user's identifier.
* Add a button and a method to cast the [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) to `CustomAuthStateProvider` and call `AuthenticateUser` with the user's identifier.

```
@inject AuthenticationStateProvider AuthenticationStateProvider

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        ((CustomAuthStateProvider)AuthenticationStateProvider)
            .AuthenticateUser(userIdentifier);
    }
}
```

The preceding approach can be enhanced to trigger notifications of authentication state changes via a custom service. The following `CustomAuthenticationService` class maintains the current user's claims principal in a backing field (`currentUser`) with an event (`UserChanged`) that the authentication state provider can subscribe to, where the event invokes [NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged). With the additional configuration later in this section, the `CustomAuthenticationService` can be injected into a component with logic that sets the `CurrentUser` to trigger the `UserChanged` event.

`CustomAuthenticationService.cs`:

```
using System.Security.Claims;

public class CustomAuthenticationService
{
    public event Action<ClaimsPrincipal>? UserChanged;
    private ClaimsPrincipal? currentUser;

    public ClaimsPrincipal CurrentUser
    {
        get { return currentUser ?? new(); }
        set
        {
            currentUser = value;

            if (UserChanged is not null)
            {
                UserChanged(currentUser);
            }
        }
    }
}
```

In the `Program` file, register the `CustomAuthenticationService` in the dependency injection container:

```
builder.Services.AddScoped<CustomAuthenticationService>();
```

In `Startup.ConfigureServices` of `Startup.cs`, register the `CustomAuthenticationService` in the dependency injection container:

```
services.AddScoped<CustomAuthenticationService>();
```

In the `Program` file, register the `CustomAuthenticationService` in the dependency injection container:

```
builder.Services.AddSingleton<CustomAuthenticationService>();
```

The following `CustomAuthStateProvider` subscribes to the `CustomAuthenticationService.UserChanged` event. The `GetAuthenticationStateAsync` method returns the user's authentication state. Initially, the authentication state is based on the value of the `CustomAuthenticationService.CurrentUser`. When the user changes, a new authentication state is created for the new user (`new AuthenticationState(newUser)`) for calls to `GetAuthenticationStateAsync`:

```
using Microsoft.AspNetCore.Components.Authorization;

public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private AuthenticationState authenticationState;

    public CustomAuthStateProvider(CustomAuthenticationService service)
    {
        authenticationState = new AuthenticationState(service.CurrentUser);

        service.UserChanged += (newUser) =>
        {
            authenticationState = new AuthenticationState(newUser);
            NotifyAuthenticationStateChanged(Task.FromResult(authenticationState));
        };
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync() =>
        Task.FromResult(authenticationState);
}
```

The following component's `SignIn` method creates a claims principal for the user's identifier to set on `CustomAuthenticationService.CurrentUser`:

```
@using System.Security.Claims
@inject CustomAuthenticationService AuthService

<input @bind="userIdentifier" />
<button @onclick="SignIn">Sign in</button>

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>You're not authorized.</p>
    </NotAuthorized>
</AuthorizeView>

@code {
    public string userIdentifier = string.Empty;

    private void SignIn()
    {
        var currentUser = AuthService.CurrentUser;

        var identity = new ClaimsIdentity(
            [
                new Claim(ClaimTypes.Name, userIdentifier),
            ],
            "Custom Authentication");

        var newUser = new ClaimsPrincipal(identity);

        AuthService.CurrentUser = newUser;
    }
}
```

Note

The preceding code that creates a new [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) uses simplified collection initialization introduced with C# 12 (.NET 8). For more information, see [Collection expressions - C# language reference](/en-us/dotnet/csharp/language-reference/operators/collection-expressions).

## Related content

* [Server-side unauthorized content display while prerendering with a custom `AuthenticationStateProvider`](./?view=aspnetcore-10.0#unauthorized-content-display-while-prerendering-with-a-custom-authenticationstateprovider)
* [How to access an `AuthenticationStateProvider` from a `DelegatingHandler` set up using an `IHttpClientFactory`](additional-scenarios?view=aspnetcore-10.0#access-authenticationstateprovider-in-outgoing-request-middleware)
* [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](blazor-web-app-with-oidc?view=aspnetcore-10.0)
* [Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](webassembly/standalone-with-identity/?view=aspnetcore-10.0)

- [Server-side unauthorized content display while prerendering with a custom `AuthenticationStateProvider`](./?view=aspnetcore-10.0#unauthorized-content-display-while-prerendering-with-a-custom-authenticationstateprovider)
- [How to access an `AuthenticationStateProvider` from a `DelegatingHandler` set up using an `IHttpClientFactory`](additional-scenarios?view=aspnetcore-10.0#access-authenticationstateprovider-in-outgoing-request-middleware)
- [Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)](blazor-web-app-with-oidc?view=aspnetcore-10.0)
- [Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](webassembly/standalone-with-identity/?view=aspnetcore-10.0) [Prerendering with authentication in hosted Blazor WebAssembly apps](webassembly/additional-scenarios?view=aspnetcore-10.0#prerendering-with-authentication)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/cookie.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use cookie authentication without ASP.NET Core Identity

By [Rick Anderson](https://twitter.com/RickAndMSFT)

[ASP.NET Core Identity](identity?view=aspnetcore-10.0) is a complete, full-featured authentication provider for creating and maintaining logins. However, a cookie-based authentication provider without ASP.NET Core Identity can be used. For more information, see [Introduction to Identity on ASP.NET Core](identity?view=aspnetcore-10.0).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/cookie/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

For demonstration purposes in the sample app, the user account for the hypothetical user, Maria Rodriguez, is hardcoded into the app. Use the **Email** address `maria.rodriguez@contoso.com` and any password to sign in the user. The user is authenticated in the `AuthenticateUser` method in the `Pages/Account/Login.cshtml.cs` file. In a real-world example, the user would be authenticated against a datastore.

Important

Starting with ASP.NET Core 10, known API endpoints no longer redirect to login pages when using cookie authentication. Instead, they return 401/403 status codes. For details, see [API endpoint authentication behavior in ASP.NET Core](api-endpoint-auth?view=aspnetcore-10.0).

## Add cookie authentication

* Add the authentication middleware services with the [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) and [AddCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie) methods.
* Call [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization) to set the `HttpContext.User` property and run the authorization middleware for requests. `UseAuthentication` and `UseAuthorization` must be called before `Map` methods such as [MapRazorPages](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorpagesendpointroutebuilderextensions.maprazorpages) and [MapDefaultControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapdefaultcontrollerroute)

```
using Microsoft.AspNetCore.Authentication.Cookies;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();

builder.Services.AddHttpContextAccessor();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();
```

[AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) passed to `AddAuthentication` sets the default authentication scheme for the app. `AuthenticationScheme` is useful when there are multiple instances of cookie authentication and the app needs to [authorize with a specific scheme](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0). Setting the `AuthenticationScheme` to [CookieAuthenticationDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) provides a value of `"Cookies"` for the scheme. Any string value can be used that distinguishes the scheme.

If [CookieAuthenticationDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) (default value: "`Cookies`") isn't used as the scheme, supply the scheme used when configuring the authentication provider. Otherwise, the default scheme is used. For example, if "`ContosoCookie`" is used as the scheme, supply the scheme used when configuring the authentication provider.

The authentication cookie's [IsEssential](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.isessential#microsoft-aspnetcore-http-cookiebuilder-isessential) property is set to `true` by default. Authentication cookies are allowed when a site visitor hasn't consented to data collection. For more information, see [General Data Protection Regulation (GDPR) support in ASP.NET Core](../gdpr?view=aspnetcore-10.0#essential-cookies).

The [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions) class is used to configure the authentication provider options.

Configure [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions) in the [AddCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie) method:

```
using Microsoft.AspNetCore.Authentication.Cookies;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
        options.SlidingExpiration = true;
        options.AccessDeniedPath = "/Forbidden/";
    });

builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();
```

## Cookie policy middleware

The [Cookie policy middleware (GitHub Source)](https://github.com/dotnet/aspnetcore/blob/main/src/Security/CookiePolicy/src/CookiePolicyMiddleware.cs) [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy) enables cookie policy capabilities. Middleware is processed in the order it's added, and cookie policy middleware should be added before cookie authentication middleware.

Use [CookiePolicyOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions) provided to the cookie policy middleware to control global characteristics of cookie processing and hook into cookie processing handlers when cookies are appended or deleted.

The default [MinimumSameSitePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.minimumsamesitepolicy#microsoft-aspnetcore-builder-cookiepolicyoptions-minimumsamesitepolicy) value is `SameSiteMode.Lax` to permit OAuth2 authentication. To strictly enforce a same-site policy of `SameSiteMode.Strict`, set the `MinimumSameSitePolicy`. Although this setting breaks OAuth2 and other cross-origin authentication schemes, it elevates the level of cookie security for other types of apps that don't rely on cross-origin request processing.

The following example shows how to configure cookie authentication with cookie policy middleware:

```
var cookiePolicyOptions = new CookiePolicyOptions
{
    MinimumSameSitePolicy = SameSiteMode.Strict,
};

app.UseCookiePolicy(cookiePolicyOptions);
```

The cookie policy middleware setting for `MinimumSameSitePolicy` can affect the setting of `Cookie.SameSite` in `CookieAuthenticationOptions` settings according to the matrix below.

| MinimumSameSitePolicy | Cookie.SameSite                                        | Resultant Cookie.SameSite setting                           |
| --------------------- | ------------------------------------------------------ | ----------------------------------------------------------- |
| SameSiteMode.None     | SameSiteMode.None SameSiteMode.Lax SameSiteMode.Strict | SameSiteMode.None SameSiteMode.Lax SameSiteMode.Strict      |
| SameSiteMode.Lax      | SameSiteMode.None SameSiteMode.Lax SameSiteMode.Strict | SameSiteMode.Lax SameSiteMode.Lax SameSiteMode.Strict       |
| SameSiteMode.Strict   | SameSiteMode.None SameSiteMode.Lax SameSiteMode.Strict | SameSiteMode.Strict SameSiteMode.Strict SameSiteMode.Strict |

## Create an authentication cookie

To create a cookie holding user information, construct a [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal). The user information is serialized and stored in the cookie.

Create a [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) with any required [Claim](/en-us/dotnet/api/system.security.claims.claim)s and call [SignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.signinasync) to sign in the user. `Login.cshtml.cs` in the sample app contains the following code:

```
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    ReturnUrl = returnUrl;

    if (ModelState.IsValid)
    {
        // Use Input.Email and Input.Password to authenticate the user
        // with your custom authentication logic.
        //
        // For demonstration purposes, the sample validates the user
        // on the email address maria.rodriguez@contoso.com with 
        // any password that passes model validation.

        var user = await AuthenticateUser(Input.Email, Input.Password);

        if (user == null)
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return Page();
        }

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, user.Email),
            new Claim("FullName", user.FullName),
            new Claim(ClaimTypes.Role, "Administrator"),
        };

        var claimsIdentity = new ClaimsIdentity(
            claims, CookieAuthenticationDefaults.AuthenticationScheme);

        var authProperties = new AuthenticationProperties
        {
            //AllowRefresh = <bool>,
            // Refreshing the authentication session should be allowed.

            //ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
            // The time at which the authentication ticket expires. A 
            // value set here overrides the ExpireTimeSpan option of 
            // CookieAuthenticationOptions set with AddCookie.

            //IsPersistent = true,
            // Whether the authentication session is persisted across 
            // multiple requests. When used with cookies, controls
            // whether the cookie's lifetime is absolute (matching the
            // lifetime of the authentication ticket) or session-based.

            //IssuedUtc = <DateTimeOffset>,
            // The time at which the authentication ticket was issued.

            //RedirectUri = <string>
            // The full path or absolute URI to be used as an http 
            // redirect response value.
        };

        await HttpContext.SignInAsync(
            CookieAuthenticationDefaults.AuthenticationScheme, 
            new ClaimsPrincipal(claimsIdentity), 
            authProperties);

        _logger.LogInformation("User {Email} logged in at {Time}.", 
            user.Email, DateTime.UtcNow);

        return LocalRedirect(Url.GetLocalUrl(returnUrl));
    }

    // Something failed. Redisplay the form.
    return Page();
}
```

`SignInAsync` creates an encrypted cookie and adds it to the current response. If `AuthenticationScheme` isn't specified, the default scheme is used.

[RedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.redirecturi#microsoft-aspnetcore-authentication-authenticationproperties-redirecturi) is only used on a few specific paths by default, for example, the login path and logout paths. For more information see the [CookieAuthenticationHandler source](https://github.com/dotnet/aspnetcore/blob/f2e6e6ff334176540ef0b3291122e359c2106d1a/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs#L334).

ASP.NET Core's [Data Protection](../data-protection/using-data-protection?view=aspnetcore-10.0) system is used for encryption. For an app hosted on multiple machines, load balancing across apps, or using a web farm, [configure data protection](../data-protection/configuration/overview?view=aspnetcore-10.0) to use the same key ring and app identifier.

## Sign out

To sign out the current user and delete their cookie, call [SignOutAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.signoutasync):

```
public async Task OnGetAsync(string returnUrl = null)
{
    if (!string.IsNullOrEmpty(ErrorMessage))
    {
        ModelState.AddModelError(string.Empty, ErrorMessage);
    }

    // Clear the existing external cookie
    await HttpContext.SignOutAsync(
        CookieAuthenticationDefaults.AuthenticationScheme);

    ReturnUrl = returnUrl;
}
```

If [CookieAuthenticationDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) (default value: "`Cookies`") isn't used as the scheme, supply the scheme used when configuring the authentication provider. Otherwise, the default scheme is used. For example, if "`ContosoCookie`" is used as the scheme, supply the scheme used when configuring the authentication provider.

When the browser closes it automatically deletes session based cookies (non-persistent cookies), but no cookies are cleared when an individual tab is closed. The server is not notified of tab or browser close events.

## React to back-end changes

Once a cookie is created, the cookie is the single source of identity. If a user account is disabled in back-end systems:

* The app's cookie authentication system continues to process requests based on the authentication cookie.
* The user remains signed into the app as long as the authentication cookie is valid.

The [ValidatePrincipal](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.validateprincipal) event can be used to intercept and override validation of the cookie identity. Validating the cookie on every request mitigates the risk of revoked users accessing the app.

One approach to cookie validation is based on keeping track of when the user database changes. If the database hasn't been changed since the user's cookie was issued, there's no need to re-authenticate the user if their cookie is still valid. In the sample app, the database is implemented in `IUserRepository` and stores a `LastChanged` value. When a user is updated in the database, the `LastChanged` value is set to the current time.

In order to invalidate a cookie when the database changes based on the `LastChanged` value, create the cookie with a `LastChanged` claim containing the current `LastChanged` value from the database:

```
var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, user.Email),
    new Claim("LastChanged", {Database Value})
};

var claimsIdentity = new ClaimsIdentity(
    claims,
    CookieAuthenticationDefaults.AuthenticationScheme);

await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme, 
    new ClaimsPrincipal(claimsIdentity));
```

To implement an override for the `ValidatePrincipal` event, write a method with the following signature in a class that derives from [CookieAuthenticationEvents](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents):

```
ValidatePrincipal(CookieValidatePrincipalContext)
```

The following is an example implementation of `CookieAuthenticationEvents`:

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
    private readonly IUserRepository _userRepository;

    public CustomCookieAuthenticationEvents(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
    {
        var userPrincipal = context.Principal;

        // Look for the LastChanged claim.
        var lastChanged = (from c in userPrincipal.Claims
                           where c.Type == "LastChanged"
                           select c.Value).FirstOrDefault();

        if (string.IsNullOrEmpty(lastChanged) ||
            !_userRepository.ValidateLastChanged(lastChanged))
        {
            context.RejectPrincipal();

            await context.HttpContext.SignOutAsync(
                CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}
```

Register the events instance during cookie service registration. Provide a [scoped service registration](../../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes) for your `CustomCookieAuthenticationEvents` class:

```
using Microsoft.AspNetCore.Authentication.Cookies;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.EventsType = typeof(CustomCookieAuthenticationEvents);
    });

builder.Services.AddScoped<CustomCookieAuthenticationEvents>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();
```

Consider a situation in which the user's name is updated—a decision that doesn't affect security in any way. If you want to non-destructively update the user principal, call `context.ReplacePrincipal` and set the `context.ShouldRenew` property to `true`.

Warning

The approach described here is triggered on every request. Validating authentication cookies for all users on every request can result in a large performance penalty for the app.

## Persistent cookies

You may want the cookie to persist across browser sessions. This persistence should only be enabled with explicit user consent with a "Remember Me" checkbox on sign in or a similar mechanism.

The following code snippet creates an identity and corresponding cookie that survives through browser closures. Any sliding expiration settings previously configured are honored. If the cookie expires while the browser is closed, the browser clears the cookie once it's restarted.

Set [IsPersistent](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.ispersistent#microsoft-aspnetcore-authentication-authenticationproperties-ispersistent) to `true` in [AuthenticationProperties](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties):

```
// using Microsoft.AspNetCore.Authentication;

await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(claimsIdentity),
    new AuthenticationProperties
    {
        IsPersistent = true
    });
```

## Absolute cookie expiration

An absolute expiration time can be set with [ExpiresUtc](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.expiresutc#microsoft-aspnetcore-authentication-authenticationproperties-expiresutc). To create a persistent cookie, `IsPersistent` must also be set. Otherwise, the cookie is created with a session-based lifetime and could expire either before or after the authentication ticket that it holds. When `ExpiresUtc` is set, it overrides the value of the [ExpireTimeSpan](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions.expiretimespan#microsoft-aspnetcore-authentication-cookies-cookieauthenticationoptions-expiretimespan) option of [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions), if set.

The following code snippet creates an identity and corresponding cookie that lasts for 20 minutes. This ignores any sliding expiration settings previously configured.

```
// using Microsoft.AspNetCore.Authentication;

await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(claimsIdentity),
    new AuthenticationProperties
    {
        IsPersistent = true,
        ExpiresUtc = DateTime.UtcNow.AddMinutes(20)
    });
```

[ASP.NET Core Identity](identity?view=aspnetcore-10.0) is a complete, full-featured authentication provider for creating and maintaining logins. However, a cookie-based authentication provider without ASP.NET Core Identity can be used. For more information, see [Introduction to Identity on ASP.NET Core](identity?view=aspnetcore-10.0).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authentication/cookie/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

For demonstration purposes in the sample app, the user account for the hypothetical user, Maria Rodriguez, is hardcoded into the app. Use the **Email** address `maria.rodriguez@contoso.com` and any password to sign in the user. The user is authenticated in the `AuthenticateUser` method in the `Pages/Account/Login.cshtml.cs` file. In a real-world example, the user would be authenticated against a database.

## Configuration

In the `Startup.ConfigureServices` method, create the authentication middleware services with the [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) and [AddCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie) methods:

```
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();
```

[AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) passed to `AddAuthentication` sets the default authentication scheme for the app. `AuthenticationScheme` is useful when there are multiple instances of cookie authentication and you want to [authorize with a specific scheme](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0). Setting the `AuthenticationScheme` to [CookieAuthenticationDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) provides a value of "Cookies" for the scheme. You can supply any string value that distinguishes the scheme.

The app's authentication scheme is different from the app's cookie authentication scheme. When a cookie authentication scheme isn't provided to [AddCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie), it uses `CookieAuthenticationDefaults.AuthenticationScheme` ("Cookies").

The authentication cookie's [IsEssential](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder.isessential#microsoft-aspnetcore-http-cookiebuilder-isessential) property is set to `true` by default. Authentication cookies are allowed when a site visitor hasn't consented to data collection. For more information, see [General Data Protection Regulation (GDPR) support in ASP.NET Core](../gdpr?view=aspnetcore-10.0#essential-cookies).

In `Startup.Configure`, call `UseAuthentication` and `UseAuthorization` to set the `HttpContext.User` property and run authorization middleware for requests. Call the `UseAuthentication` and `UseAuthorization` methods before calling `UseEndpoints`:

```
```

The [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions) class is used to configure the authentication provider options.

Set `CookieAuthenticationOptions` in the service configuration for authentication in the `Startup.ConfigureServices` method:

```
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        ...
    });
```

## Cookie policy middleware

[Cookie policy middleware](/en-us/dotnet/api/microsoft.aspnetcore.cookiepolicy.cookiepolicymiddleware) enables cookie policy capabilities. Adding the middleware to the app processing pipeline is order sensitive—it only affects downstream components registered in the pipeline, and cookie policy middleware should be added before cookie authentication middleware.

Use [CookiePolicyOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions) provided to the cookie policy middleware to control global characteristics of cookie processing and hook into cookie processing handlers when cookies are appended or deleted.

The default [MinimumSameSitePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.minimumsamesitepolicy#microsoft-aspnetcore-builder-cookiepolicyoptions-minimumsamesitepolicy) value is `SameSiteMode.Lax` to permit OAuth2 authentication. To strictly enforce a same-site policy of `SameSiteMode.Strict`, set the `MinimumSameSitePolicy`. Although this setting breaks OAuth2 and other cross-origin authentication schemes, it elevates the level of cookie security for other types of apps that don't rely on cross-origin request processing.

The following example shows how to configure cookie authentication with cookie policy middleware:

```
var cookiePolicyOptions = new CookiePolicyOptions
{
    MinimumSameSitePolicy = SameSiteMode.Strict,
};

app.UseCookiePolicy(cookiePolicyOptions);
```

The cookie policy middleware setting for `MinimumSameSitePolicy` can affect the setting of `Cookie.SameSite` in `CookieAuthenticationOptions` settings according to the matrix below.

| MinimumSameSitePolicy | Cookie.SameSite                                        | Resultant Cookie.SameSite setting                           |
| --------------------- | ------------------------------------------------------ | ----------------------------------------------------------- |
| SameSiteMode.None     | SameSiteMode.None SameSiteMode.Lax SameSiteMode.Strict | SameSiteMode.None SameSiteMode.Lax SameSiteMode.Strict      |
| SameSiteMode.Lax      | SameSiteMode.None SameSiteMode.Lax SameSiteMode.Strict | SameSiteMode.Lax SameSiteMode.Lax SameSiteMode.Strict       |
| SameSiteMode.Strict   | SameSiteMode.None SameSiteMode.Lax SameSiteMode.Strict | SameSiteMode.Strict SameSiteMode.Strict SameSiteMode.Strict |

## Create an authentication cookie

To create a cookie holding user information, construct a [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal). The user information is serialized and stored in the cookie.

Create a [ClaimsIdentity](/en-us/dotnet/api/system.security.claims.claimsidentity) with any required [Claim](/en-us/dotnet/api/system.security.claims.claim)s and call [SignInAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.signinasync) to sign in the user:

```
var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, user.Email),
    new Claim("FullName", user.FullName),
    new Claim(ClaimTypes.Role, "Administrator"),
};

var claimsIdentity = new ClaimsIdentity(
    claims, CookieAuthenticationDefaults.AuthenticationScheme);

var authProperties = new AuthenticationProperties
{
    //AllowRefresh = <bool>,
    // Refreshing the authentication session should be allowed.

    //ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
    // The time at which the authentication ticket expires. A 
    // value set here overrides the ExpireTimeSpan option of 
    // CookieAuthenticationOptions set with AddCookie.

    //IsPersistent = true,
    // Whether the authentication session is persisted across 
    // multiple requests. When used with cookies, controls
    // whether the cookie's lifetime is absolute (matching the
    // lifetime of the authentication ticket) or session-based.

    //IssuedUtc = <DateTimeOffset>,
    // The time at which the authentication ticket was issued.

    //RedirectUri = <string>
    // The full path or absolute URI to be used as an http 
    // redirect response value.
};

await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme, 
    new ClaimsPrincipal(claimsIdentity), 
    authProperties);
```

`SignInAsync` creates an encrypted cookie and adds it to the current response. If `AuthenticationScheme` isn't specified, the default scheme is used.

[RedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.redirecturi#microsoft-aspnetcore-authentication-authenticationproperties-redirecturi) is only used on a few specific paths by default, for example, the login path and logout paths. For more information see the [CookieAuthenticationHandler source](https://github.com/dotnet/aspnetcore/blob/f2e6e6ff334176540ef0b3291122e359c2106d1a/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs#L334).

ASP.NET Core's [Data Protection](../data-protection/using-data-protection?view=aspnetcore-10.0) system is used for encryption. For an app hosted on multiple machines, load balancing across apps, or using a web farm, [configure data protection](../data-protection/configuration/overview?view=aspnetcore-10.0) to use the same key ring and app identifier.

## Sign out

To sign out the current user and delete their cookie, call [SignOutAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.signoutasync):

```
await HttpContext.SignOutAsync(
    CookieAuthenticationDefaults.AuthenticationScheme);
```

If [CookieAuthenticationDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) (default value: "`Cookies`") isn't used as the scheme, supply the scheme used when configuring the authentication provider. Otherwise, the default scheme is used. For example, if "`ContosoCookie`" is used as the scheme, supply the scheme used when configuring the authentication provider.

When the browser closes it automatically deletes session based cookies (non-persistent cookies), but no cookies are cleared when an individual tab is closed. The server is not notified of tab or browser close events.

## React to back-end changes

Once a cookie is created, the cookie is the single source of identity. If a user account is disabled in back-end systems:

* The app's cookie authentication system continues to process requests based on the authentication cookie.
* The user remains signed into the app as long as the authentication cookie is valid.

The [ValidatePrincipal](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents.validateprincipal) event can be used to intercept and override validation of the cookie identity. Validating the cookie on every request mitigates the risk of revoked users accessing the app.

One approach to cookie validation is based on keeping track of when the user database changes. If the database hasn't been changed since the user's cookie was issued, there's no need to re-authenticate the user if their cookie is still valid. In the sample app, the database is implemented in `IUserRepository` and stores a `LastChanged` value. When a user is updated in the database, the `LastChanged` value is set to the current time.

In order to invalidate a cookie when the database changes based on the `LastChanged` value, create the cookie with a `LastChanged` claim containing the current `LastChanged` value from the database:

```
var claims = new List<Claim>
{
    new Claim(ClaimTypes.Name, user.Email),
    new Claim("LastChanged", {Database Value})
};

var claimsIdentity = new ClaimsIdentity(
    claims, 
    CookieAuthenticationDefaults.AuthenticationScheme);

await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme, 
    new ClaimsPrincipal(claimsIdentity));
```

To implement an override for the `ValidatePrincipal` event, write a method with the following signature in a class that derives from [CookieAuthenticationEvents](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationevents):

```
ValidatePrincipal(CookieValidatePrincipalContext)
```

The following is an example implementation of `CookieAuthenticationEvents`:

```
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
    private readonly IUserRepository _userRepository;

    public CustomCookieAuthenticationEvents(IUserRepository userRepository)
    {
        // Get the database from registered DI services.
        _userRepository = userRepository;
    }

    public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
    {
        var userPrincipal = context.Principal;

        // Look for the LastChanged claim.
        var lastChanged = (from c in userPrincipal.Claims
                           where c.Type == "LastChanged"
                           select c.Value).FirstOrDefault();

        if (string.IsNullOrEmpty(lastChanged) ||
            !_userRepository.ValidateLastChanged(lastChanged))
        {
            context.RejectPrincipal();

            await context.HttpContext.SignOutAsync(
                CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}
```

Register the events instance during cookie service registration in the `Startup.ConfigureServices` method. Provide a [scoped service registration](../../fundamentals/dependency-injection?view=aspnetcore-10.0#service-lifetimes) for your `CustomCookieAuthenticationEvents` class:

```
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.EventsType = typeof(CustomCookieAuthenticationEvents);
    });

services.AddScoped<CustomCookieAuthenticationEvents>();
```

Consider a situation in which the user's name is updated—a decision that doesn't affect security in any way. If you want to non-destructively update the user principal, call `context.ReplacePrincipal` and set the `context.ShouldRenew` property to `true`.

Warning

The approach described here is triggered on every request. Validating authentication cookies for all users on every request can result in a large performance penalty for the app.

## Persistent cookies

You may want the cookie to persist across browser sessions. This persistence should only be enabled with explicit user consent with a "Remember Me" checkbox on sign in or a similar mechanism.

The following code snippet creates an identity and corresponding cookie that survives through browser closures. Any sliding expiration settings previously configured are honored. If the cookie expires while the browser is closed, the browser clears the cookie once it's restarted.

Set [IsPersistent](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.ispersistent#microsoft-aspnetcore-authentication-authenticationproperties-ispersistent) to `true` in [AuthenticationProperties](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties):

```
// using Microsoft.AspNetCore.Authentication;

await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(claimsIdentity),
    new AuthenticationProperties
    {
        IsPersistent = true
    });
```

## Absolute cookie expiration

An absolute expiration time can be set with [ExpiresUtc](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.expiresutc#microsoft-aspnetcore-authentication-authenticationproperties-expiresutc). To create a persistent cookie, `IsPersistent` must also be set. Otherwise, the cookie is created with a session-based lifetime and could expire either before or after the authentication ticket that it holds. When `ExpiresUtc` is set, it overrides the value of the [ExpireTimeSpan](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions.expiretimespan#microsoft-aspnetcore-authentication-cookies-cookieauthenticationoptions-expiretimespan) option of [CookieAuthenticationOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationoptions), if set.

The following code snippet creates an identity and corresponding cookie that lasts for 20 minutes. This ignores any sliding expiration settings previously configured.

```
// using Microsoft.AspNetCore.Authentication;

await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(claimsIdentity),
    new AuthenticationProperties
    {
        IsPersistent = true,
        ExpiresUtc = DateTime.UtcNow.AddMinutes(20)
    });
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/working-with-forms.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tag Helpers in forms in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT), [N. Taylor Mullen](https://github.com/NTaylorMullen), [Dave Paquette](https://twitter.com/Dave_Paquette), and [Jerrie Pelser](https://github.com/jerriep)

This document demonstrates working with Forms and the HTML elements commonly used on a Form. The HTML [Form](https://www.w3.org/TR/html401/interact/forms.html) element provides the primary mechanism web apps use to post back data to the server. Most of this document describes [Tag Helpers](tag-helpers/intro?view=aspnetcore-10.0) and how they can help you productively create robust HTML forms. We recommend you read [Introduction to Tag Helpers](tag-helpers/intro?view=aspnetcore-10.0) before you read this document.

In many cases, HTML Helpers provide an alternative approach to a specific Tag Helper. However, it's important to recognize that Tag Helpers don't replace HTML Helpers, and there's not a Tag Helper for each HTML Helper. When an HTML Helper alternative exists, it's mentioned.

[]()

## The Form Tag Helper

The [Form Tag Helper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.formtaghelper):

* Generates the HTML [\<FORM>](https://www.w3.org/TR/html401/interact/forms.html) `action` attribute value for a MVC controller action or named route.

* Generates a hidden [Request Verification Token](/en-us/aspnet/mvc/overview/security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages) to prevent cross-site request forgery (when used with the `[ValidateAntiForgeryToken]` attribute in the HTTP Post action method).

* Provides the `asp-route-<Parameter Name>` attribute, where `<Parameter Name>` is added to the route values. The `routeValues` parameters to `Html.BeginForm` and `Html.BeginRouteForm` provide similar functionality.

* Has an HTML Helper alternative `Html.BeginForm` and `Html.BeginRouteForm`.

Sample:

```
<form asp-controller="Demo" asp-action="Register" method="post">
    <!-- Input and Submit elements -->
</form>
```

The previous Form Tag Helper generates the following HTML:

```
<form method="post" action="/Demo/Register">
    <!-- Input and Submit elements -->
    <input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>">
</form>
```

The MVC runtime generates the `action` attribute value from the Form Tag Helper attributes `asp-controller` and `asp-action`. The Form Tag Helper also generates a hidden [Request Verification Token](/en-us/aspnet/mvc/overview/security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages) to prevent cross-site request forgery (when used with the `[ValidateAntiForgeryToken]` attribute in the HTTP Post action method). Protecting a pure HTML Form from cross-site request forgery is difficult, but the Form Tag Helper provides this service for you.

### Using a named route

The `asp-route` Tag Helper attribute can also generate markup for the HTML `action` attribute. An app with a [route](../../fundamentals/routing?view=aspnetcore-10.0) named `register` could use the following markup for the registration page:

```
<form asp-route="register" method="post">
    <!-- Input and Submit elements -->
</form>
```

Many of the views in the *Views/Account* folder (generated when you create a new web app with *Individual Accounts*) contain the [asp-route-returnurl](working-with-forms?view=aspnetcore-10.0) attribute:

```
<form asp-controller="Account" asp-action="Login"
     asp-route-returnurl="@ViewData["ReturnUrl"]"
     method="post" class="form-horizontal" role="form">
```

Note

With the built in templates, `returnUrl` is only populated automatically when you try to access an authorized resource but aren't authenticated or authorized. When you attempt an unauthorized access, the security middleware redirects you to the login page with the `returnUrl` set.

## The Form Action Tag Helper

The Form Action Tag Helper generates the `formaction` attribute on the generated `<button ...>` or `<input type="image" ...>` tag. The `formaction` attribute controls where a form submits its data. It binds to [\<input>](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/input) elements of type `image` and [\<button>](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/button) elements. The Form Action Tag Helper enables the usage of several [AnchorTagHelper](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) `asp-` attributes to control what `formaction` link is generated for the corresponding element.

Supported [AnchorTagHelper](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0) attributes to control the value of `formaction`:

| Attribute                                                                                            | Description                                                   |
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [asp-controller](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0#asp-controller)         | The name of the controller.                                   |
| [asp-action](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0#asp-action)                 | The name of the action method.                                |
| [asp-area](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0#asp-area)                     | The name of the area.                                         |
| [asp-page](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0#asp-page)                     | The name of the Razor page.                                   |
| [asp-page-handler](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0#asp-page-handler)     | The name of the Razor page handler.                           |
| [asp-route](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0#asp-route)                   | The name of the route.                                        |
| [asp-route-{value}](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0#asp-route-value)     | A single URL route value. For example, `asp-route-id="1234"`. |
| [asp-all-route-data](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0#asp-all-route-data) | All route values.                                             |
| [asp-fragment](tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-10.0#asp-fragment)             | The URL fragment.                                             |

### Submit to controller example

The following markup submits the form to the `Index` action of `HomeController` when the input or button is selected:

```
<form method="post">
    <button asp-controller="Home" asp-action="Index">Click Me</button>
    <input type="image" src="..." alt="Or Click Me" asp-controller="Home" 
                                asp-action="Index">
</form>
```

The previous markup generates following HTML:

```
<form method="post">
    <button formaction="/Home">Click Me</button>
    <input type="image" src="..." alt="Or Click Me" formaction="/Home">
</form>
```

### Submit to page example

The following markup submits the form to the `About` Razor Page:

```
<form method="post">
    <button asp-page="About">Click Me</button>
    <input type="image" src="..." alt="Or Click Me" asp-page="About">
</form>
```

The previous markup generates following HTML:

```
<form method="post">
    <button formaction="/About">Click Me</button>
    <input type="image" src="..." alt="Or Click Me" formaction="/About">
</form>
```

### Submit to route example

Consider the `/Home/Test` endpoint:

```
public class HomeController : Controller
{
    [Route("/Home/Test", Name = "Custom")]
    public string Test()
    {
        return "This is the test page";
    }
}
```

The following markup submits the form to the `/Home/Test` endpoint.

```
<form method="post">
    <button asp-route="Custom">Click Me</button>
    <input type="image" src="..." alt="Or Click Me" asp-route="Custom">
</form>
```

The previous markup generates following HTML:

```
<form method="post">
    <button formaction="/Home/Test">Click Me</button>
    <input type="image" src="..." alt="Or Click Me" formaction="/Home/Test">
</form>
```

## The Input Tag Helper

The Input Tag Helper binds an HTML [\<input>](https://www.w3.org/wiki/HTML/Elements/input) element to a model expression in your razor view.

Syntax:

```
<input asp-for="<Expression Name>">
```

The Input Tag Helper:

* Generates the `id` and `name` HTML attributes for the expression name specified in the `asp-for` attribute. `asp-for="Property1.Property2"` is equivalent to `m => m.Property1.Property2`. The name of the expression is what is used for the `asp-for` attribute value. For more information, see the [Expression names](#expression-names) section.

* Sets the HTML `type` attribute value based on the model type and [data annotation](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.iattributeadapter) attributes applied to the model property.

* Won't overwrite the HTML `type` attribute value when one is specified.

* Generates [HTML5](https://developer.mozilla.org/docs/Glossary/HTML5) validation attributes from [data annotation](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.iattributeadapter) attributes applied to model properties.

* Has an HTML Helper feature overlap with `Html.TextBoxFor` and `Html.EditorFor`. See the [HTML Helper alternatives to Input Tag Helper](#html-helper-alternatives-to-input-tag-helper) section for details.

* Provides strong typing. If the name of the property changes and you don't update the Tag Helper, you'll get an error similar to the following:

  ```
  An error occurred during the compilation of a resource required to process
  this request. Please review the following specific error details and modify
  your source code appropriately.

  Type expected
  'RegisterViewModel' does not contain a definition for 'Email' and no
  extension method 'Email' accepting a first argument of type 'RegisterViewModel'
  could be found (are you missing a using directive or an assembly reference?)
  ```

The `Input` Tag Helper sets the HTML `type` attribute based on the .NET type. The following table lists some common .NET types and generated HTML type (not every .NET type is listed).

| .NET type      | Input Type                                                                                                   |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| Bool           | type="checkbox"                                                                                              |
| String         | type="text"                                                                                                  |
| DateTime       | type=["datetime-local"](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/input/datetime-local) |
| Byte           | type="number"                                                                                                |
| Int            | type="number"                                                                                                |
| Single, Double | type="number"                                                                                                |

The following table shows some common [data annotations](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.iattributeadapter) attributes that the input tag helper will map to specific input types (not every validation attribute is listed).

| Attribute                      | Input Type      |
| ------------------------------ | --------------- |
| \[EmailAddress]                | type="email"    |
| \[Url]                         | type="url"      |
| \[HiddenInput]                 | type="hidden"   |
| \[Phone]                       | type="tel"      |
| \[DataType(DataType.Password)] | type="password" |
| \[DataType(DataType.Date)]     | type="date"     |
| \[DataType(DataType.Time)]     | type="time"     |

Sample:

```
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
    public class RegisterViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email Address")]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}
```

```
@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterInput" method="post">
    <label>Email: <input asp-for="Email" /></label> <br />
    <label>Password: <input asp-for="Password" /></label><br />
    <button type="submit">Register</button>
</form>
```

The preceding code generates the following HTML:

```
<form method="post" action="/Demo/RegisterInput">
    Email:
    <input type="email" data-val="true"
            data-val-email="The Email Address field is not a valid email address."
            data-val-required="The Email Address field is required."
            id="Email" name="Email" value=""><br>
    Password:
    <input type="password" data-val="true"
            data-val-required="The Password field is required."
            id="Password" name="Password"><br>
    <button type="submit">Register</button>
    <input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>">
</form>
```

The data annotations applied to the `Email` and `Password` properties generate metadata on the model. The Input Tag Helper consumes the model metadata and produces [HTML5](https://developer.mozilla.org/docs/Web/Guide/HTML/HTML5) `data-val-*` attributes (see [Model Validation](../models/validation?view=aspnetcore-10.0)). These attributes describe the validators to attach to the input fields. This provides unobtrusive HTML5 and [jQuery](https://jquery.com/) validation. The unobtrusive attributes have the format `data-val-rule="Error Message"`, where rule is the name of the validation rule (such as `data-val-required`, `data-val-email`, `data-val-maxlength`, etc.). If an error message is provided in the attribute, it's displayed as the value for the `data-val-rule` attribute. There are also attributes of the form `data-val-ruleName-argumentName="argumentValue"` that provide additional details about the rule, for example, `data-val-maxlength-max="1024"` .

When binding multiple `input` controls to the same property, the generated controls share the same `id`, which makes the generated mark-up invalid. To prevent duplicates, specify the `id` attribute for each control explicitly.

### Checkbox hidden input rendering

Checkboxes in HTML5 don't submit a value when they're unchecked. To enable a default value to be sent for an unchecked checkbox, the Input Tag Helper generates an additional hidden input for checkboxes.

For example, consider the following Razor markup that uses the Input Tag Helper for a boolean model property `IsChecked`:

```
<form method="post">
    <input asp-for="@Model.IsChecked" />
    <button type="submit">Submit</button>
</form>
```

The preceding Razor markup generates HTML markup similar to the following:

```
<form method="post">
    <input name="IsChecked" type="checkbox" value="true" />
    <button type="submit">Submit</button>

    <input name="IsChecked" type="hidden" value="false" /> 
</form>
```

The preceding HTML markup shows an additional hidden input with a name of `IsChecked` and a value of `false`. By default, this hidden input is rendered at the end of the form. When the form is submitted:

* If the `IsChecked` checkbox input is checked, both `true` and `false` are submitted as values.
* If the `IsChecked` checkbox input is unchecked, only the hidden input value `false` is submitted.

The ASP.NET Core model-binding process reads only the first value when binding to a `bool` value, which results in `true` for checked checkboxes and `false` for unchecked checkboxes.

To configure the behavior of the hidden input rendering, set the [CheckBoxHiddenInputRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.htmlhelperoptions.checkboxhiddeninputrendermode#microsoft-aspnetcore-mvc-viewfeatures-htmlhelperoptions-checkboxhiddeninputrendermode) property on [MvcViewOptions.HtmlHelperOptions](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcviewoptions.htmlhelperoptions#microsoft-aspnetcore-mvc-mvcviewoptions-htmlhelperoptions). For example:

```
services.Configure<MvcViewOptions>(options =>
    options.HtmlHelperOptions.CheckBoxHiddenInputRenderMode =
        CheckBoxHiddenInputRenderMode.None);
```

The preceding code disables hidden input rendering for checkboxes by setting `CheckBoxHiddenInputRenderMode` to [CheckBoxHiddenInputRenderMode.None](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.checkboxhiddeninputrendermode#microsoft-aspnetcore-mvc-rendering-checkboxhiddeninputrendermode-none). For all available rendering modes, see the [CheckBoxHiddenInputRenderMode](/en-us/dotnet/api/microsoft.aspnetcore.mvc.rendering.checkboxhiddeninputrendermode) enum.

### HTML Helper alternatives to Input Tag Helper

`Html.TextBox`, `Html.TextBoxFor`, `Html.Editor`, and `Html.EditorFor` have overlapping features with the Input Tag Helper. The Input Tag Helper will automatically set the `type` attribute; `Html.TextBox` and `Html.TextBoxFor` won't. `Html.Editor` and `Html.EditorFor` handle collections, complex objects, and templates; the Input Tag Helper doesn't. The Input Tag Helper, `Html.EditorFor`, and `Html.TextBoxFor` are strongly typed (they use lambda expressions); `Html.TextBox` and `Html.Editor` aren't (they use expression names).

### HtmlAttributes

`@Html.Editor()` and `@Html.EditorFor()` use a special `ViewDataDictionary` entry named `htmlAttributes` when executing their default templates. This behavior is optionally augmented using `additionalViewData` parameters. The key "htmlAttributes" is case-insensitive. The key "htmlAttributes" is handled similarly to the `htmlAttributes` object passed to input helpers like `@Html.TextBox()`.

```
@Html.EditorFor(model => model.YourProperty, 
  new { htmlAttributes = new { @class="myCssClass", style="Width:100px" } })
```

### Expression names

The `asp-for` attribute value is a `ModelExpression` and the right hand side of a lambda expression. Therefore, `asp-for="Property1"` becomes `m => m.Property1` in the generated code, which is why you don't need to prefix with `Model`. You can use the "@" character to start an inline expression and move before the `m.`:

```
@{
  var joe = "Joe";
}

<input asp-for="@joe">
```

Generates the following:

```
<input type="text" id="joe" name="joe" value="Joe">
```

With collection properties, `asp-for="CollectionProperty[23].Member"` generates the same name as `asp-for="CollectionProperty[i].Member"` when `i` has the value `23`.

When ASP.NET Core MVC calculates the value of `ModelExpression`, it inspects several sources, including `ModelState`. Consider `<input type="text" asp-for="Name">`. The calculated `value` attribute is the first non-null value from:

* `ModelState` entry with key "Name".
* Result of the expression `Model.Name`.

### Navigating child properties

You can also navigate to child properties using the property path of the view model. Consider a more complex model class that contains a child `Address` property.

```
public class AddressViewModel
{
    public string AddressLine1 { get; set; }
}
```

```
public class RegisterAddressViewModel
{
    public string Email { get; set; }

    [DataType(DataType.Password)]
    public string Password { get; set; }

    public AddressViewModel Address { get; set; }
}
```

In the view, we bind to `Address.AddressLine1`:

```
@model RegisterAddressViewModel

<form asp-controller="Demo" asp-action="RegisterAddress" method="post">
    <label>Email: <input asp-for="Email" /></label> <br />
    <label>Password: <input asp-for="Password" /></label><br />
    <label>Address: <input asp-for="Address.AddressLine1" /></label><br />
    <button type="submit">Register</button>
</form>
```

The following HTML is generated for `Address.AddressLine1`:

```
<input type="text" id="Address_AddressLine1" name="Address.AddressLine1" value="">
```

### Expression names and Collections

Sample, a model containing an array of `Colors`:

```
public class Person
{
    public List<string> Colors { get; set; }

    public int Age { get; set; }
}
```

The action method:

```
public IActionResult Edit(int id, int colorIndex)
{
    ViewData["Index"] = colorIndex;
    return View(GetPerson(id));
}
```

The following Razor shows how you access a specific `Color` element:

```
@model Person
@{
    var index = (int)ViewData["index"];
}

<form asp-controller="ToDo" asp-action="Edit" method="post">
    @Html.EditorFor(m => m.Colors[index])
    <label asp-for="Age"></label>
    <input asp-for="Age" /><br />
    <button type="submit">Post</button>
</form>
```

The `Views/Shared/EditorTemplates/String.cshtml` template:

```
@model string

<label asp-for="@Model"></label>
<input asp-for="@Model" /> <br />
```

Sample using `List<T>`:

```
public class ToDoItem
{
    public string Name { get; set; }

    public bool IsDone { get; set; }
}
```

The following Razor shows how to iterate over a collection:

```
@model List<ToDoItem>

<form asp-controller="ToDo" asp-action="Edit" method="post">
    <table>
        <tr> <th>Name</th> <th>Is Done</th> </tr>

        @for (int i = 0; i < Model.Count; i++)
        {
            <tr>
                @Html.EditorFor(model => model[i])
            </tr>
        }

    </table>
    <button type="submit">Save</button>
</form>
```

The `Views/Shared/EditorTemplates/ToDoItem.cshtml` template:

```
@model ToDoItem

<td>
    <label asp-for="@Model.Name"></label>
    @Html.DisplayFor(model => model.Name)
</td>
<td>
    <input asp-for="@Model.IsDone" />
</td>

@*
    This template replaces the following Razor which evaluates the indexer three times.
    <td>
         <label asp-for="@Model[i].Name"></label>
         @Html.DisplayFor(model => model[i].Name)
     </td>
     <td>
         <input asp-for="@Model[i].IsDone" />
     </td>
*@
```

`foreach` should be used if possible when the value is going to be used in an `asp-for` or `Html.DisplayFor` equivalent context. In general, `for` is better than `foreach` (if the scenario allows it) because it doesn't need to allocate an enumerator; however, evaluating an indexer in a LINQ expression can be expensive and should be minimized.

 

Note

The commented sample code above shows how you would replace the lambda expression with the `@` operator to access each `ToDoItem` in the list.

## The Textarea Tag Helper

The `Textarea Tag Helper` tag helper is similar to the Input Tag Helper.

* Generates the `id` and `name` attributes, and the data validation attributes from the model for a [\<textarea>](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/textarea) element.

* Provides strong typing.

* HTML Helper alternative: `Html.TextAreaFor`.

Sample:

```
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
    public class DescriptionViewModel
    {
        [MinLength(5)]
        [MaxLength(1024)]
        public string Description { get; set; }
    }
}
```

```
@model DescriptionViewModel

<form asp-controller="Demo" asp-action="RegisterTextArea" method="post">
    <textarea asp-for="Description"></textarea>
    <button type="submit">Test</button>
</form>
```

The following HTML is generated:

```
<form method="post" action="/Demo/RegisterTextArea">
  <textarea data-val="true"
   data-val-maxlength="The field Description must be a string or array type with a maximum length of &#x27;1024&#x27;."
   data-val-maxlength-max="1024"
   data-val-minlength="The field Description must be a string or array type with a minimum length of &#x27;5&#x27;."
   data-val-minlength-min="5"
   id="Description" name="Description">
  </textarea>
  <button type="submit">Test</button>
  <input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>">
</form>
```

## The Label Tag Helper

* Generates the label caption and `for` attribute on a [\<label>](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/label) element for an expression name.

* HTML Helper alternative: `Html.LabelFor`.

The `Label Tag Helper` provides the following benefits over a pure HTML label element:

* You automatically get the descriptive label value from the `Display` attribute. The intended display name might change over time, and the combination of `Display` attribute and Label Tag Helper will apply the `Display` everywhere it's used.

* Less markup in source code.

* Strong typing with the model property.

Sample:

```
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
    public class SimpleViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email Address")]
        public string Email { get; set; }
    }
}
```

```
@model SimpleViewModel

<form asp-controller="Demo" asp-action="RegisterLabel" method="post">
    <label asp-for="Email"></label>
    <input asp-for="Email" /> <br />
</form>
```

The following HTML is generated for the `<label>` element:

```
<label for="Email">Email Address</label>
```

The Label Tag Helper generated the `for` attribute value of "Email", which is the ID associated with the `<input>` element. The Tag Helpers generate consistent `id` and `for` elements so they can be correctly associated. The caption in this sample comes from the `Display` attribute. If the model didn't contain a `Display` attribute, the caption would be the property name of the expression. To override the default caption, add a caption inside the label tag.

## The Validation Tag Helpers

There are two Validation Tag Helpers. The `Validation Message Tag Helper` (which displays a validation message for a single property on your model), and the `Validation Summary Tag Helper` (which displays a summary of validation errors). The `Input Tag Helper` adds HTML5 client side validation attributes to input elements based on data annotation attributes on your model classes. Validation is also performed on the server. The Validation Tag Helper displays these error messages when a validation error occurs.

### The Validation Message Tag Helper

* Adds the [HTML5](https://developer.mozilla.org/docs/Web/Guide/HTML/HTML5) `data-valmsg-for="property"` attribute to the [span](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/span) element, which attaches the validation error messages on the input field of the specified model property. When a client side validation error occurs, [jQuery](https://jquery.com/) displays the error message in the `<span>` element.

* Validation also takes place on the server. Clients might have JavaScript disabled and some validation can only be done on the server side.

* HTML Helper alternative: `Html.ValidationMessageFor`.

The `Validation Message Tag Helper` is used with the `asp-validation-for` attribute on an HTML [span](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/span) element.

```
<span asp-validation-for="Email"></span>
```

The Validation Message Tag Helper will generate the following HTML:

```
<span class="field-validation-valid"
  data-valmsg-for="Email"
  data-valmsg-replace="true"></span>
```

You generally use the `Validation Message Tag Helper` after an `Input` Tag Helper for the same property. Doing so displays any validation error messages near the input that caused the error.

Note

You must have a view with the correct JavaScript and [jQuery](https://jquery.com/) script references in place for client side validation. For more information, see [Model Validation](../models/validation?view=aspnetcore-10.0).

When a server side validation error occurs (for example when you have custom server side validation or client-side validation is disabled), MVC places that error message as the body of the `<span>` element.

```
<span class="field-validation-error" data-valmsg-for="Email"
            data-valmsg-replace="true">
   The Email Address field is required.
</span>
```

### The Validation Summary Tag Helper

* Targets `<div>` elements with the `asp-validation-summary` attribute.

* HTML Helper alternative: `@Html.ValidationSummary`.

The `Validation Summary Tag Helper` is used to display a summary of validation messages. The `asp-validation-summary` attribute value can be any of the following:

| asp-validation-summary | Validation messages displayed |
| ---------------------- | ----------------------------- |
| `All`                  | Property and model level      |
| `ModelOnly`            | Model                         |
| `None`                 | None                          |

### Sample

In the following example, the data model has `DataAnnotation` attributes, which generates validation error messages on the `<input>` element. When a validation error occurs, the Validation Tag Helper displays the error message:

```
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
    public class RegisterViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email Address")]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}
```

```
@model RegisterViewModel

<form asp-controller="Demo" asp-action="RegisterValidation" method="post">
    <div asp-validation-summary="ModelOnly"></div>
    <label>Email: <input asp-for="Email" /></label> <br />
    <span asp-validation-for="Email"></span><br />
    <label>Password: <input asp-for="Password" /></label><br />
    <span asp-validation-for="Password"></span><br />
    <button type="submit">Register</button>
</form>
```

The generated HTML (when the model is valid):

```
<form action="/DemoReg/Register" method="post">
  <label>Email: <input name="Email" id="Email" type="email" value=""
   data-val-required="The Email field is required."
   data-val-email="The Email field is not a valid email address."
   data-val="true"></label><br>
  <span class="field-validation-valid" data-valmsg-replace="true"
   data-valmsg-for="Email"></span><br>
  <label>Password: <input name="Password" id="Password" type="password"
   data-val-required="The Password field is required." data-val="true"></label><br>
  <span class="field-validation-valid" data-valmsg-replace="true"
   data-valmsg-for="Password"></span><br>
  <button type="submit">Register</button>
  <input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>">
</form>
```

## The Select Tag Helper

* Generates [select](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/select) and associated [option](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/option) elements for properties of your model.

* Has an HTML Helper alternative `Html.DropDownListFor` and `Html.ListBoxFor`.

The `Select Tag Helper` `asp-for` specifies the model property name for the [select](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/select) element and `asp-items` specifies the [option](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/option) elements. For example:

```
<select asp-for="Country" asp-items="Model.Countries"></select> 
```

Sample:

```
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
    public class CountryViewModel
    {
        public string Country { get; set; }

        public List<SelectListItem> Countries { get; } = new List<SelectListItem>
        {
            new SelectListItem { Value = "MX", Text = "Mexico" },
            new SelectListItem { Value = "CA", Text = "Canada" },
            new SelectListItem { Value = "US", Text = "USA" },
        };
    }
}
```

The `Index` method initializes the `CountryViewModel`, sets the selected country, and passes it to the `Index` view.

```
public IActionResult Index()
{
    var model = new CountryViewModel();
    model.Country = "CA";
    return View(model);
}
```

The HTTP POST `Index` method displays the selection:

```
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(CountryViewModel model)
{
    if (ModelState.IsValid)
    {
        var msg = model.Country + " selected";
        return RedirectToAction("IndexSuccess", new { message = msg });
    }

    // If we got this far, something failed; redisplay form.
    return View(model);
}
```

The `Index` view:

```
@model CountryViewModel

<form asp-controller="Home" asp-action="Index" method="post">
    <select asp-for="Country" asp-items="Model.Countries"></select> 
    <br /><button type="submit">Register</button>
</form>
```

Which generates the following HTML (with "CA" selected):

```
<form method="post" action="/">
     <select id="Country" name="Country">
       <option value="MX">Mexico</option>
       <option selected="selected" value="CA">Canada</option>
       <option value="US">USA</option>
     </select>
       <br /><button type="submit">Register</button>
     <input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>">
   </form>
```

Note

We don't recommend using `ViewBag` or `ViewData` with the Select Tag Helper. A view model is more robust at providing MVC metadata and less problematic.

The `asp-for` attribute value is a special case and doesn't require a `Model` prefix. The other Tag Helper attributes do (such as `asp-items`).

```
<select asp-for="Country" asp-items="Model.Countries"></select> 
```

### Enum binding

It's often convenient to use `<select>` with an `enum` property and generate the `SelectListItem` elements from the `enum` values.

Sample:

```
public class CountryEnumViewModel
{
    public CountryEnum EnumCountry { get; set; }
}
```

```
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
    public enum CountryEnum
    {
        [Display(Name = "United Mexican States")]
        Mexico,
        [Display(Name = "United States of America")]
        USA,
        Canada,
        France,
        Germany,
        Spain
    }
}
```

The `GetEnumSelectList` method generates a `SelectList` object for an enum.

```
@model CountryEnumViewModel

<form asp-controller="Home" asp-action="IndexEnum" method="post">
    <select asp-for="EnumCountry" 
            asp-items="Html.GetEnumSelectList<CountryEnum>()">
    </select> 
    <br /><button type="submit">Register</button>
</form>
```

You can mark your enumerator list with the `Display` attribute to get a richer UI:

```
using System.ComponentModel.DataAnnotations;

namespace FormsTagHelper.ViewModels
{
    public enum CountryEnum
    {
        [Display(Name = "United Mexican States")]
        Mexico,
        [Display(Name = "United States of America")]
        USA,
        Canada,
        France,
        Germany,
        Spain
    }
}
```

The following HTML is generated:

```
<form method="post" action="/Home/IndexEnum">
    <select data-val="true" data-val-required="The EnumCountry field is required."
            id="EnumCountry" name="EnumCountry">
        <option value="0">United Mexican States</option>
        <option value="1">United States of America</option>
        <option value="2">Canada</option>
        <option value="3">France</option>
        <option value="4">Germany</option>
        <option selected="selected" value="5">Spain</option>
    </select>
    <br /><button type="submit">Register</button>
    <input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>">
</form>
```

### Option Group

The HTML [\<optgroup>](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/optgroup) element is generated when the view model contains one or more `SelectListGroup` objects.

The `CountryViewModelGroup` groups the `SelectListItem` elements into the "North America" and "Europe" groups:

```
public class CountryViewModelGroup
{
    public CountryViewModelGroup()
    {
        var NorthAmericaGroup = new SelectListGroup { Name = "North America" };
        var EuropeGroup = new SelectListGroup { Name = "Europe" };

        Countries = new List<SelectListItem>
        {
            new SelectListItem
            {
                Value = "MEX",
                Text = "Mexico",
                Group = NorthAmericaGroup
            },
            new SelectListItem
            {
                Value = "CAN",
                Text = "Canada",
                Group = NorthAmericaGroup
            },
            new SelectListItem
            {
                Value = "US",
                Text = "USA",
                Group = NorthAmericaGroup
            },
            new SelectListItem
            {
                Value = "FR",
                Text = "France",
                Group = EuropeGroup
            },
            new SelectListItem
            {
                Value = "ES",
                Text = "Spain",
                Group = EuropeGroup
            },
            new SelectListItem
            {
                Value = "DE",
                Text = "Germany",
                Group = EuropeGroup
            }
      };
    }

    public string Country { get; set; }

    public List<SelectListItem> Countries { get; }
```

The two groups are shown as follows:

The generated HTML:

```
 <form method="post" action="/Home/IndexGroup">
      <select id="Country" name="Country">
          <optgroup label="North America">
              <option value="MEX">Mexico</option>
              <option value="CAN">Canada</option>
              <option value="US">USA</option>
          </optgroup>
          <optgroup label="Europe">
              <option value="FR">France</option>
              <option value="ES">Spain</option>
              <option value="DE">Germany</option>
          </optgroup>
      </select>
      <br /><button type="submit">Register</button>
      <input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>">
 </form>
```

### Multiple select

The Select Tag Helper will automatically generate the [multiple = "multiple"](https://w3c.github.io/html-reference/select.html) attribute if the property specified in the `asp-for` attribute is an `IEnumerable`. For example, given the following model:

```
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace FormsTagHelper.ViewModels
{
    public class CountryViewModelIEnumerable
    {
        public IEnumerable<string> CountryCodes { get; set; }

        public List<SelectListItem> Countries { get; } = new List<SelectListItem>
        {
            new SelectListItem { Value = "MX", Text = "Mexico" },
            new SelectListItem { Value = "CA", Text = "Canada" },
            new SelectListItem { Value = "US", Text = "USA"    },
            new SelectListItem { Value = "FR", Text = "France" },
            new SelectListItem { Value = "ES", Text = "Spain"  },
            new SelectListItem { Value = "DE", Text = "Germany"}
         };
    }
}
```

With the following view:

```
@model CountryViewModelIEnumerable

<form asp-controller="Home" asp-action="IndexMultiSelect" method="post">
    <select asp-for="CountryCodes" asp-items="Model.Countries"></select> 
    <br /><button type="submit">Register</button>
</form>
```

Generates the following HTML:

```
<form method="post" action="/Home/IndexMultiSelect">
    <select id="CountryCodes"
    multiple="multiple"
    name="CountryCodes"><option value="MX">Mexico</option>
<option value="CA">Canada</option>
<option value="US">USA</option>
<option value="FR">France</option>
<option value="ES">Spain</option>
<option value="DE">Germany</option>
</select>
    <br /><button type="submit">Register</button>
  <input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>">
</form>
```

### No selection

If you find yourself using the "not specified" option in multiple pages, you can create a template to eliminate repeating the HTML:

```
@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">
    @Html.EditorForModel()
    <br /><button type="submit">Register</button>
</form>
```

The `Views/Shared/EditorTemplates/CountryViewModel.cshtml` template:

```
@model CountryViewModel

<select asp-for="Country" asp-items="Model.Countries">
    <option value="">--none--</option>
</select>
```

Adding HTML [\<option>](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/option) elements isn't limited to the *No selection* case. For example, the following view and action method will generate HTML similar to the preceding code:

```
public IActionResult IndexNone()
{
    var model = new CountryViewModel();
    model.Countries.Insert(0, new SelectListItem("<none>", ""));
    return View(model);
}
```

```
@model CountryViewModel

<form asp-controller="Home" asp-action="IndexEmpty" method="post">
    <select asp-for="Country">
        <option value="">&lt;none&gt;</option>
        <option value="MX">Mexico</option>
        <option value="CA">Canada</option>
        <option value="US">USA</option>
    </select> 
    <br /><button type="submit">Register</button>
</form>
```

The correct `<option>` element will be selected (contain the `selected="selected"` attribute) depending on the current `Country` value.

```
public IActionResult IndexOption(int id)
{
    var model = new CountryViewModel();
    model.Country = "CA";
    return View(model);
}
```

```
 <form method="post" action="/Home/IndexEmpty">
      <select id="Country" name="Country">
          <option value="">&lt;none&gt;</option>
          <option value="MX">Mexico</option>
          <option value="CA" selected="selected">Canada</option>
          <option value="US">USA</option>
      </select>
      <br /><button type="submit">Register</button>
   <input name="__RequestVerificationToken" type="hidden" value="<removed for brevity>">
 </form>
```

## Related content

* [Tag Helpers in ASP.NET Core](tag-helpers/intro?view=aspnetcore-10.0)
* [HTML Form element](https://www.w3.org/TR/html401/interact/forms.html)
* [Request Verification Token](/en-us/aspnet/mvc/overview/security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages)
* [Model Binding in ASP.NET Core](../models/model-binding?view=aspnetcore-10.0)
* [Model validation in ASP.NET Core MVC](../models/validation?view=aspnetcore-10.0)
* [IAttributeAdapter Interface](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.iattributeadapter)
* [Code snippets for this document](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/working-with-forms/samples/final)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/troubleshoot
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/troubleshoot.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Troubleshoot gRPC on .NET

By [James Newton-King](https://twitter.com/jamesnk)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This document discusses commonly encountered problems when developing gRPC apps on .NET.

## Mismatch between client and service SSL/TLS configuration

The gRPC template and samples use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246) to secure gRPC services by default. gRPC clients need to use a secure connection to call secured gRPC services successfully.

You can verify the ASP.NET Core gRPC service is using TLS in the logs written on app start. The service will be listening on an HTTPS endpoint:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

The .NET client must use `https` in the server address to make calls with a secured connection:

```
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
```

All gRPC client implementations support TLS. gRPC clients from other languages typically require the channel configured with `SslCredentials`. `SslCredentials` specifies the certificate that the client will use, and it must be used instead of insecure credentials. For examples of configuring the different gRPC client implementations to use TLS, see [gRPC Authentication](https://www.grpc.io/docs/guides/auth/).

## Call a gRPC service with an untrusted/invalid certificate

The .NET gRPC client requires the service to have a trusted certificate. The following error message is returned when calling a gRPC service without a trusted certificate:

> Unhandled exception. System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.

You may see this error if you are testing your app locally and the ASP.NET Core HTTPS development certificate is not trusted. For instructions to fix this issue, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos).

If you are calling a gRPC service on another machine and are unable to trust the certificate then the gRPC client can be configured to ignore the invalid certificate. The following code uses [HttpClientHandler.ServerCertificateCustomValidationCallback](/en-us/dotnet/api/system.net.http.httpclienthandler.servercertificatecustomvalidationcallback) to allow calls without a trusted certificate:

```
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = 
    HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpHandler = handler });
var client = new Greeter.GreeterClient(channel);
```

The [gRPC client factory](clientfactory?view=aspnetcore-10.0) allows calls without a trusted certificate. Use the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method to configure the handler on the client:

```

var services = new ServiceCollection();

services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        var handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback =
            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

        return handler;
    });
```

Warning

Untrusted certificates should only be used during app development. Production apps should always use valid certificates.

## Call insecure gRPC services with .NET client

The .NET gRPC client can call insecure gRPC services by specifying `http` in the server address. For example, `GrpcChannel.ForAddress("http://localhost:5000")`.

There are some additional requirements to call insecure gRPC services depending on the .NET version an app is using:

* .NET 5 or later requires [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client) version 2.32.0 or later.
* .NET Core 3.x requires additional configuration. The app must set the `System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport` switch to `true`. For more information see [Asp.Net Core 3.x: Call insecure gRPC services with the .NET client](troubleshoot?view=aspnetcore-3.1#call-insecure-grpc-services-with-net-client-2):

Important

Insecure gRPC services must be hosted on a HTTP/2-only port. For more information, see [ASP.NET Core protocol negotiation](aspnetcore?view=aspnetcore-10.0#protocol-negotiation).

## Unable to start ASP.NET Core gRPC app on macOS

Kestrel doesn't support HTTP/2 with TLS on macOS before .NET 8. The ASP.NET Core gRPC template and samples use TLS by default. You'll see the following error message when you attempt to start the gRPC server:

> Unable to bind to https\://localhost:5001 on the IPv4 loopback interface: 'HTTP/2 over TLS is not supported on macOS due to missing ALPN support.'.

To work around this issue in .NET 7 or earlier, configure Kestrel and the gRPC client to use HTTP/2 *without* TLS. You should only do this during development. Not using TLS will result in gRPC messages being sent without encryption. For more information see [ASP.NET Core in .NET 7: Unable to start ASP.NET Core gRPC app on macOS](troubleshoot?view=aspnetcore-7.0#unable-to-start-aspnet-core-grpc-app-on-macos).

## gRPC C# assets are not code generated from `.proto` files

gRPC code generation of concrete clients and service base classes requires protobuf files and tooling to be referenced from a project. You must include:

* `.proto` files you want to use in the `<Protobuf>` item group. [Imported `.proto` files](https://developers.google.com/protocol-buffers/docs/proto3#importing-definitions) must be referenced by the project.
* Package reference to the gRPC tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/).

For more information on generating gRPC C# assets, see [gRPC Services with C#](basics?view=aspnetcore-10.0).

An ASP.NET Core web app hosting gRPC services only needs the service base class generated:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
```

A gRPC client app making gRPC calls only needs the concrete client generated:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
```

## WPF projects unable to generate gRPC C# assets from `.proto` files

WPF projects have a [known issue](https://github.com/dotnet/wpf/issues/810) that prevents gRPC code generation from working correctly. Any gRPC types generated in a WPF project by referencing `Grpc.Tools` and `.proto` files will create compilation errors when used:

> error CS0246: The type or namespace name 'MyGrpcServices' could not be found (are you missing a using directive or an assembly reference?)

You can workaround this issue by:

1. Create a new .NET class library project.

2. In the new project, add references to enable [C# code generation from `.proto` files](basics?view=aspnetcore-10.0#generated-c-assets):

   * Add the following package references:

     * [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/)
     * [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client/)
     * [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/)

   * Add `.proto` files to the `<Protobuf>` item group.

3. In the WPF application, add a reference to the new project.

The WPF application can use the gRPC generated types from the new class library project.

## Calling gRPC services hosted in a sub-directory

Warning

Many third-party gRPC tools don't support services hosted in subdirectories. Consider finding a way to host gRPC as the root directory.

The path component of a gRPC channel's address is ignored when making gRPC calls. For example, `GrpcChannel.ForAddress("https://localhost:5001/ignored_path")` won't use `ignored_path` when routing gRPC calls for the service.

The address path is ignored because gRPC has a standardized, prescriptive address structure. A gRPC address combines the package, service and method names: `https://localhost:5001/PackageName.ServiceName/MethodName`.

There are some scenarios when an app needs to include a path with gRPC calls. For example, when an ASP.NET Core gRPC app is hosted in an IIS directory and the directory needs to be included in the request. When a path is required, it can be added to the gRPC call using the custom `SubdirectoryHandler` specified below:

```
public class SubdirectoryHandler : DelegatingHandler
{
    private readonly string _subdirectory;

    public SubdirectoryHandler(HttpMessageHandler innerHandler, string subdirectory)
        : base(innerHandler)
    {
        _subdirectory = subdirectory;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var old = request.RequestUri;

        var url = $"{old.Scheme}://{old.Host}:{old.Port}";
        url += $"{_subdirectory}{request.RequestUri.AbsolutePath}";
        request.RequestUri = new Uri(url, UriKind.Absolute);

        return base.SendAsync(request, cancellationToken);
    }
}
```

`SubdirectoryHandler` is used when the gRPC channel is created.

```
var handler = new SubdirectoryHandler(new HttpClientHandler(), "/MyApp");

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { HttpHandler = handler });
var client = new Greeter.GreeterClient(channel);

var reply = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
```

The preceding code:

* Creates a `SubdirectoryHandler` with the path `/MyApp`.
* Configures a channel to use `SubdirectoryHandler`.
* Calls the gRPC service with `SayHelloAsync`. The gRPC call is sent to `https://localhost:5001/MyApp/greet.Greeter/SayHello`.

Alternatively, a client factory can be configured with `SubdirectoryHandler` by using [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler).

## Configure gRPC client to use HTTP/3

The .NET gRPC client supports HTTP/3 with .NET 6 or later. If the server sends an `alt-svc` response header to the client that indicates the server supports HTTP/3, the client will automatically upgrade its connection to HTTP/3. For more information, see [Use HTTP/3 with the ASP.NET Core Kestrel web server](../fundamentals/servers/kestrel/http3?view=aspnetcore-10.0).

A [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) can be used to force a gRPC client to use HTTP/3. Forcing HTTP/3 avoids the overhead of upgrading the request. Force HTTP/3 with code similar to the following:

```
public class Http3Handler : DelegatingHandler
{
    public Http3Handler() { }
    public Http3Handler(HttpMessageHandler innerHandler) : base(innerHandler) { }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Version = HttpVersion.Version30;
        request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;

        return base.SendAsync(request, cancellationToken);
    }
}
```

`Http3Handler` is used when the gRPC channel is created. The following code creates a channel configured to use `Http3Handler`.

```
var handler = new Http3Handler(new HttpClientHandler());

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { HttpHandler = handler });
var client = new Greeter.GreeterClient(channel);

var reply = await client.SayHelloAsync(
                  new HelloRequest { Name = "GreeterClient" });
```

Alternatively, a client factory can be configured with `Http3Handler` by using [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler).

## Building gRPC on Alpine Linux

The `Grpc.Tools` package [generates .NET types from `.proto` files](basics?view=aspnetcore-10.0#generated-c-assets) using a bundled native binary called `protoc`. Additional steps are required to build gRPC apps on platforms that aren't supported by the native binaries in `Grpc.Tools`, such as Alpine Linux.

### Generate code ahead of time

One solution is to generate code ahead of time.

1. Move `.proto` files and the `Grpc.Tools` package reference to a new project.
2. Publish the project as a NuGet package and upload it to a NuGet feed.
3. Update the app to reference the NuGet package.

With the preceding steps, the app no longer requires `Grpc.Tools` to build because code is generated ahead of time.

### Customize `Grpc.Tools` native binaries

`Grpc.Tools` supports using custom native binaries. This feature allows gRPC tooling to run in environments its bundled native binaries don't support.

Build or acquire `protoc` and `grpc_csharp_plugin` native binaries and configure `Grpc.Tools` to use them. Configure native binaries by setting the following environment variables:

* `PROTOBUF_PROTOC` - Full path to the protocol buffers compiler
* `GRPC_PROTOC_PLUGIN` - Full path to the grpc\_csharp\_plugin

For Alpine Linux, there are community-provided packages for the protocol buffers compiler and gRPC plugins at [https://pkgs.alpinelinux.org/](https://pkgs.alpinelinux.org/packages?name=grpc-plugins).

```
# Build or install the binaries for your architecture.

# For Alpine Linux, the grpc-plugins package can be used.
# See https://pkgs.alpinelinux.org/package/edge/community/x86_64/grpc-plugins
apk add grpc-plugins  # Alpine Linux specific package installer

# Set environment variables for the built/installed protoc
# and grpc_csharp_plugin binaries
export PROTOBUF_PROTOC=/usr/bin/protoc
export GRPC_PROTOC_PLUGIN=/usr/bin/grpc_csharp_plugin

# When dotnet build runs, the Grpc.Tools NuGet package
# uses the binaries pointed to by the environment variables.
dotnet build
```

For more information about using `Grpc.Tools` with unsupported architectures, see the [gRPC build integration documentation](https://github.com/grpc/grpc/blob/master/src/csharp/BUILD-INTEGRATION.md#using-grpctools-with-unsupported-architectures).

## gRPC call timeout from `HttpClient.Timeout`

[HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is configured with a 100 second timeout by default. If a `GrpcChannel` is configured to use an `HttpClient`, long-running gRPC streaming calls are canceled if they don't complete within the timeout limit.

```
System.OperationCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
```

There are a couple of ways to fix this error. The first is to configure [HttpClient.Timeout](/en-us/dotnet/api/system.net.http.httpclient.timeout#system-net-http-httpclient-timeout) to a larger value. [Timeout.InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan) disables the timeout:

```
var handler = new HttpClientHandler();

var httpClient = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpClient = httpClient });
var client = new Greeter.GreeterClient(channel);
```

Alternatively, avoid creating `HttpClient` and set `GrpcChannel.HttpHandler` instead:

```
var handler = new HttpClientHandler();

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpHandler = handler });
var client = new Greeter.GreeterClient(channel);
```

This document discusses commonly encountered problems when developing gRPC apps on .NET.

## Mismatch between client and service SSL/TLS configuration

The gRPC template and samples use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246) to secure gRPC services by default. gRPC clients need to use a secure connection to call secured gRPC services successfully.

You can verify the ASP.NET Core gRPC service is using TLS in the logs written on app start. The service will be listening on an HTTPS endpoint:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

The .NET client must use `https` in the server address to make calls with a secured connection:

```
static async Task Main(string[] args)
{
    // The port number(5001) must match the port of the gRPC server.
    var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greet.GreeterClient(channel);
}
```

All gRPC client implementations support TLS. gRPC clients from other languages typically require the channel configured with `SslCredentials`. `SslCredentials` specifies the certificate that the client will use, and it must be used instead of insecure credentials. For examples of configuring the different gRPC client implementations to use TLS, see [gRPC Authentication](https://www.grpc.io/docs/guides/auth/).

## Call a gRPC service with an untrusted/invalid certificate

The .NET gRPC client requires the service to have a trusted certificate. The following error message is returned when calling a gRPC service without a trusted certificate:

> Unhandled exception. System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.

You may see this error if you are testing your app locally and the ASP.NET Core HTTPS development certificate is not trusted. For instructions to fix this issue, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos).

If you are calling a gRPC service on another machine and are unable to trust the certificate then the gRPC client can be configured to ignore the invalid certificate. The following code uses [HttpClientHandler.ServerCertificateCustomValidationCallback](/en-us/dotnet/api/system.net.http.httpclienthandler.servercertificatecustomvalidationcallback) to allow calls without a trusted certificate:

```
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = 
    HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpHandler = handler });
var client = new Greet.GreeterClient(channel);
```

The [gRPC client factory](clientfactory?view=aspnetcore-10.0) allows calls without a trusted certificate. Use the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method to configure the handler on the client:

```
builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        var handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback = 
            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

        return handler;
    });
```

Warning

Untrusted certificates should only be used during app development. Production apps should always use valid certificates.

## Call insecure gRPC services with .NET client

The .NET gRPC client can call insecure gRPC services by specifying `http` in the server address. For example, `GrpcChannel.ForAddress("http://localhost:5000")`.

There are some additional requirements to call insecure gRPC services depending on the .NET version an app is using:

* .NET 5 or later requires [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client) version 2.32.0 or later.

* .NET Core 3.x requires additional configuration. The app must set the `System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport` switch to `true`:

  ```
  // This switch must be set before creating the GrpcChannel/HttpClient.
  AppContext.SetSwitch(
      "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

  // The port number(5000) must match the port of the gRPC server.
  var channel = GrpcChannel.ForAddress("http://localhost:5000");
  var client = new Greet.GreeterClient(channel);
  ```

The `System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport` switch is only required for .NET Core 3.x. It does nothing in .NET 5 and isn't required.

Important

Insecure gRPC services must be hosted on a HTTP/2-only port. For more information, see [ASP.NET Core protocol negotiation](aspnetcore?view=aspnetcore-10.0#protocol-negotiation).

## Unable to start ASP.NET Core gRPC app on macOS

Kestrel doesn't support HTTP/2 with TLS on macOS before .NET 8. The ASP.NET Core gRPC template and samples use TLS by default. You'll see the following error message when you attempt to start the gRPC server:

> Unable to bind to https\://localhost:5001 on the IPv4 loopback interface: 'HTTP/2 over TLS is not supported on macOS due to missing ALPN support.'.

To work around this issue in .NET 7 or earlier, configure Kestrel and the gRPC client to use HTTP/2 *without* TLS. You should only do this during development. Not using TLS will result in gRPC messages being sent without encryption.

Kestrel must configure an HTTP/2 endpoint without TLS in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    // Setup a HTTP/2 endpoint without TLS.
    options.ListenLocalhost(<5287>, o => o.Protocols =
        HttpProtocols.Http2);
});
```

* In the preceding code, replace the localhost port number `5287` with the `HTTP` (not `HTTPS`) port number specified in `Properties/launchSettings.json` within the gRPC service project.

When an HTTP/2 endpoint is configured without TLS, the endpoint's [ListenOptions.Protocols](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsprotocols) must be set to `HttpProtocols.Http2`. `HttpProtocols.Http1AndHttp2` can't be used because TLS is required to negotiate HTTP/2. Without TLS, all connections to the endpoint default to HTTP/1.1, and gRPC calls fail.

The gRPC client must also be configured to not use TLS. For more information, see [Call insecure gRPC services with .NET client](#call-insecure-grpc-services-with-net-client).

Warning

HTTP/2 without TLS should only be used during app development. Production apps should always use transport security. For more information, see [Security considerations in gRPC for ASP.NET Core](security?view=aspnetcore-10.0#transport-security).

## gRPC C# assets are not code generated from `.proto` files

gRPC code generation of concrete clients and service base classes requires protobuf files and tooling to be referenced from a project. You must include:

* `.proto` files you want to use in the `<Protobuf>` item group. [Imported `.proto` files](https://developers.google.com/protocol-buffers/docs/proto3#importing-definitions) must be referenced by the project.
* Package reference to the gRPC tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/).

For more information on generating gRPC C# assets, see [gRPC Services with C#](basics?view=aspnetcore-10.0).

An ASP.NET Core web app hosting gRPC services only needs the service base class generated:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
```

A gRPC client app making gRPC calls only needs the concrete client generated:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
```

## WPF projects unable to generate gRPC C# assets from `.proto` files

WPF projects have a [known issue](https://github.com/dotnet/wpf/issues/810) that prevents gRPC code generation from working correctly. Any gRPC types generated in a WPF project by referencing `Grpc.Tools` and `.proto` files will create compilation errors when used:

> error CS0246: The type or namespace name 'MyGrpcServices' could not be found (are you missing a using directive or an assembly reference?)

You can workaround this issue by:

1. Create a new .NET class library project.

2. In the new project, add references to enable [C# code generation from `.proto` files](basics?view=aspnetcore-10.0#generated-c-assets):

   * Add the following package references:

     * [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/)
     * [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client/)
     * [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/)

   * Add `.proto` files to the `<Protobuf>` item group.

3. In the WPF application, add a reference to the new project.

The WPF application can use the gRPC generated types from the new class library project.

## Calling gRPC services hosted in a sub-directory

Warning

Many third-party gRPC tools don't support services hosted in subdirectories. Consider finding a way to host gRPC as the root directory.

The path component of a gRPC channel's address is ignored when making gRPC calls. For example, `GrpcChannel.ForAddress("https://localhost:5001/ignored_path")` won't use `ignored_path` when routing gRPC calls for the service.

The address path is ignored because gRPC has a standardized, prescriptive address structure. A gRPC address combines the package, service and method names: `https://localhost:5001/PackageName.ServiceName/MethodName`.

There are some scenarios when an app needs to include a path with gRPC calls. For example, when an ASP.NET Core gRPC app is hosted in an IIS directory and the directory needs to be included in the request. When a path is required, it can be added to the gRPC call using the custom `SubdirectoryHandler` specified below:

```
/// <summary>
/// A delegating handler that adds a subdirectory to the URI of gRPC requests.
/// </summary>
public class SubdirectoryHandler : DelegatingHandler
{
    private readonly string _subdirectory;

    public SubdirectoryHandler(HttpMessageHandler innerHandler, string subdirectory)
        : base(innerHandler)
    {
        _subdirectory = subdirectory;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var old = request.RequestUri;

        var url = $"{old.Scheme}://{old.Host}:{old.Port}";
        url += $"{_subdirectory}{request.RequestUri.AbsolutePath}";
        request.RequestUri = new Uri(url, UriKind.Absolute);

        return base.SendAsync(request, cancellationToken);
    }
}
```

`SubdirectoryHandler` is used when the gRPC channel is created.

```
var handler = new SubdirectoryHandler(new HttpClientHandler(), "/MyApp");

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { HttpHandler = handler });
var client = new Greet.GreeterClient(channel);

var reply = await client.SayHelloAsync(new HelloRequest { Name = ".NET" });
```

The preceding code:

* Creates a `SubdirectoryHandler` with the path `/MyApp`.
* Configures a channel to use `SubdirectoryHandler`.
* Calls the gRPC service with `SayHelloAsync`. The gRPC call is sent to `https://localhost:5001/MyApp/greet.Greeter/SayHello`.

Alternatively, a client factory can be configured with `SubdirectoryHandler` by using [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler).

## Configure gRPC client to use HTTP/3

The .NET gRPC client supports HTTP/3 with .NET 6 or later. If the server sends an `alt-svc` response header to the client that indicates the server supports HTTP/3, the client will automatically upgrade its connection to HTTP/3. For information about how to enable HTTP/3 on the server, see [Use HTTP/3 with the ASP.NET Core Kestrel web server](../fundamentals/servers/kestrel/http3?view=aspnetcore-10.0).

HTTP/3 support in .NET 8 is enabled by default. HTTP/3 support in .NET 6 and .NET 7 needs to be enabled via a configuration flag in the project file:

```
<ItemGroup>
  <RuntimeHostConfigurationOption Include="System.Net.SocketsHttpHandler.Http3Support" Value="true" />
</ItemGroup>
```

`System.Net.SocketsHttpHandler.Http3Support` can also be set using [AppContext.SetSwitch](/en-us/dotnet/api/system.appcontext.setswitch).

A [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) can be used to force a gRPC client to use HTTP/3. Forcing HTTP/3 avoids the overhead of upgrading the request. Force HTTP/3 with code similar to the following:

```
/// <summary>
/// A delegating handler that changes the request HTTP version to HTTP/3.
/// </summary>
public class Http3Handler : DelegatingHandler
{
    public Http3Handler() { }
    public Http3Handler(HttpMessageHandler innerHandler) : base(innerHandler) { }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Version = HttpVersion.Version30;
        request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;

        return base.SendAsync(request, cancellationToken);
    }
}
```

`Http3Handler` is used when the gRPC channel is created. The following code creates a channel configured to use `Http3Handler`.

```
var handler = new Http3Handler(new HttpClientHandler());

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { HttpHandler = handler });
var client = new Greet.GreeterClient(channel);

var reply = await client.SayHelloAsync(new HelloRequest { Name = ".NET" });
```

Alternatively, a client factory can be configured with `Http3Handler` by using [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler).

## Building gRPC on Alpine Linux

The `Grpc.Tools` package [generates .NET types from `.proto` files](basics?view=aspnetcore-10.0#generated-c-assets) using a bundled native binary called `protoc`. Additional steps are required to build gRPC apps on platforms that aren't supported by the native binaries in `Grpc.Tools`, such as Alpine Linux.

### Generate code ahead of time

One solution is to generate code ahead of time.

1. Move `.proto` files and the `Grpc.Tools` package reference to a new project.
2. Publish the project as a NuGet package and upload it to a NuGet feed.
3. Update the app to reference the NuGet package.

With the preceding steps, the app no longer requires `Grpc.Tools` to build because code is generated ahead of time.

### Customize `Grpc.Tools` native binaries

`Grpc.Tools` supports using custom native binaries. This feature allows gRPC tooling to run in environments its bundled native binaries don't support.

Build or acquire `protoc` and `grpc_csharp_plugin` native binaries and configure `Grpc.Tools` to use them. Configure native binaries by setting the following environment variables:

* `PROTOBUF_PROTOC` - Full path to the protocol buffers compiler
* `GRPC_PROTOC_PLUGIN` - Full path to the grpc\_csharp\_plugin

For Alpine Linux, there are community-provided packages for the protocol buffers compiler and gRPC plugins at [https://pkgs.alpinelinux.org/](https://pkgs.alpinelinux.org/packages?name=grpc-plugins).

```
# Build or install the binaries for your architecture.

# For Alpine Linux, the grpc-plugins package can be used.
#  See https://pkgs.alpinelinux.org/package/edge/community/x86_64/grpc-plugins
apk add grpc-plugins  # Alpine Linux specific package installer

# Set environment variables for the built/installed protoc
# and grpc_csharp_plugin binaries
export PROTOBUF_PROTOC=/usr/bin/protoc
export GRPC_PROTOC_PLUGIN=/usr/bin/grpc_csharp_plugin

# When dotnet build runs, the Grpc.Tools NuGet package
# uses the binaries pointed to by the environment variables.
dotnet build
```

For more information about using `Grpc.Tools` with unsupported architectures, see the [gRPC build integration documentation](https://github.com/grpc/grpc/blob/master/src/csharp/BUILD-INTEGRATION.md#using-grpctools-with-unsupported-architectures).

## gRPC call timeout from `HttpClient.Timeout`

[HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is configured with a 100 second timeout by default. If a `GrpcChannel` is configured to use an `HttpClient`, long-running gRPC streaming calls are canceled if they don't complete within the timeout limit.

```
System.OperationCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
```

There are a couple of ways to fix this error. The first is to configure [HttpClient.Timeout](/en-us/dotnet/api/system.net.http.httpclient.timeout#system-net-http-httpclient-timeout) to a larger value. [Timeout.InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan) disables the timeout:

```
var handler = new HttpClientHandler();

var httpClient = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpClient = httpClient });
var client = new Greeter.GreeterClient(channel);
```

Alternatively, avoid creating `HttpClient` and set `GrpcChannel.HttpHandler` instead:

```
var handler = new HttpClientHandler();

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpHandler = handler });
var client = new Greeter.GreeterClient(channel);
```

This document discusses commonly encountered problems when developing gRPC apps on .NET.

## Mismatch between client and service SSL/TLS configuration

The gRPC template and samples use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246) to secure gRPC services by default. gRPC clients need to use a secure connection to call secured gRPC services successfully.

You can verify the ASP.NET Core gRPC service is using TLS in the logs written on app start. The service will be listening on an HTTPS endpoint:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

The .NET client must use `https` in the server address to make calls with a secured connection:

```
static async Task Main(string[] args)
{
    // The port number(5001) must match the port of the gRPC server.
    var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greet.GreeterClient(channel);
}
```

All gRPC client implementations support TLS. gRPC clients from other languages typically require the channel configured with `SslCredentials`. `SslCredentials` specifies the certificate that the client will use, and it must be used instead of insecure credentials. For examples of configuring the different gRPC client implementations to use TLS, see [gRPC Authentication](https://www.grpc.io/docs/guides/auth/).

## Call a gRPC service with an untrusted/invalid certificate

The .NET gRPC client requires the service to have a trusted certificate. The following error message is returned when calling a gRPC service without a trusted certificate:

> Unhandled exception. System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.

You may see this error if you are testing your app locally and the ASP.NET Core HTTPS development certificate is not trusted. For instructions to fix this issue, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos).

If you are calling a gRPC service on another machine and are unable to trust the certificate then the gRPC client can be configured to ignore the invalid certificate. The following code uses [HttpClientHandler.ServerCertificateCustomValidationCallback](/en-us/dotnet/api/system.net.http.httpclienthandler.servercertificatecustomvalidationcallback) to allow calls without a trusted certificate:

```
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = 
    HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpHandler = handler });
var client = new Greet.GreeterClient(channel);
```

The [gRPC client factory](clientfactory?view=aspnetcore-10.0) allows calls without a trusted certificate. Use the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method to configure the handler on the client:

```
services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        var handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback = 
            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

        return handler;
    });
```

Warning

Untrusted certificates should only be used during app development. Production apps should always use valid certificates.

## Call insecure gRPC services with .NET client

The .NET gRPC client can call insecure gRPC services by specifying `http` in the server address. For example, `GrpcChannel.ForAddress("http://localhost:5000")`.

There are some additional requirements to call insecure gRPC services depending on the .NET version an app is using:

* .NET 5 or later requires [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client) version 2.32.0 or later.

* .NET Core 3.x requires additional configuration. The app must set the `System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport` switch to `true`:

  ```
  // This switch must be set before creating the GrpcChannel/HttpClient.
  AppContext.SetSwitch(
      "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

  // The port number(5000) must match the port of the gRPC server.
  var channel = GrpcChannel.ForAddress("http://localhost:5000");
  var client = new Greet.GreeterClient(channel);
  ```

The `System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport` switch is only required for .NET Core 3.x. It does nothing in .NET 5 and isn't required.

Important

Insecure gRPC services must be hosted on a HTTP/2-only port. For more information, see [ASP.NET Core protocol negotiation](aspnetcore?view=aspnetcore-10.0#protocol-negotiation).

## Unable to start ASP.NET Core gRPC app on macOS

Kestrel doesn't support HTTP/2 with TLS on macOS before .NET 8. The ASP.NET Core gRPC template and samples use TLS by default. You'll see the following error message when you attempt to start the gRPC server:

> Unable to bind to https\://localhost:5001 on the IPv4 loopback interface: 'HTTP/2 over TLS is not supported on macOS due to missing ALPN support.'.

To work around this issue in .NET 7 or earlier, configure Kestrel and the gRPC client to use HTTP/2 *without* TLS. You should only do this during development. Not using TLS will result in gRPC messages being sent without encryption.

Kestrel must configure an HTTP/2 endpoint without TLS in `Program.cs`:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(options =>
            {
                // Setup a HTTP/2 endpoint without TLS.
                options.ListenLocalhost(5000, o => o.Protocols = 
                    HttpProtocols.Http2);
            });
            webBuilder.UseStartup<Startup>();
        });
```

When an HTTP/2 endpoint is configured without TLS, the endpoint's [ListenOptions.Protocols](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsprotocols) must be set to `HttpProtocols.Http2`. `HttpProtocols.Http1AndHttp2` can't be used because TLS is required to negotiate HTTP/2. Without TLS, all connections to the endpoint default to HTTP/1.1, and gRPC calls fail.

The gRPC client must also be configured to not use TLS. For more information, see [Call insecure gRPC services with .NET client](#call-insecure-grpc-services-with-net-client).

Warning

HTTP/2 without TLS should only be used during app development. Production apps should always use transport security. For more information, see [Security considerations in gRPC for ASP.NET Core](security?view=aspnetcore-10.0#transport-security).

## gRPC C# assets are not code generated from `.proto` files

gRPC code generation of concrete clients and service base classes requires protobuf files and tooling to be referenced from a project. You must include:

* `.proto` files you want to use in the `<Protobuf>` item group. [Imported `.proto` files](https://developers.google.com/protocol-buffers/docs/proto3#importing-definitions) must be referenced by the project.
* Package reference to the gRPC tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/).

For more information on generating gRPC C# assets, see [gRPC Services with C#](basics?view=aspnetcore-10.0).

An ASP.NET Core web app hosting gRPC services only needs the service base class generated:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
```

A gRPC client app making gRPC calls only needs the concrete client generated:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
```

## WPF projects unable to generate gRPC C# assets from `.proto` files

WPF projects have a [known issue](https://github.com/dotnet/wpf/issues/810) that prevents gRPC code generation from working correctly. Any gRPC types generated in a WPF project by referencing `Grpc.Tools` and `.proto` files will create compilation errors when used:

> error CS0246: The type or namespace name 'MyGrpcServices' could not be found (are you missing a using directive or an assembly reference?)

You can workaround this issue by:

1. Create a new .NET class library project.

2. In the new project, add references to enable [C# code generation from `.proto` files](basics?view=aspnetcore-10.0#generated-c-assets):

   * Add the following package references:

     * [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/)
     * [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client/)
     * [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/)

   * Add `.proto` files to the `<Protobuf>` item group.

3. In the WPF application, add a reference to the new project.

The WPF application can use the gRPC generated types from the new class library project.

## Calling gRPC services hosted in a sub-directory

Warning

Many third-party gRPC tools don't support services hosted in subdirectories. Consider finding a way to host gRPC as the root directory.

The path component of a gRPC channel's address is ignored when making gRPC calls. For example, `GrpcChannel.ForAddress("https://localhost:5001/ignored_path")` won't use `ignored_path` when routing gRPC calls for the service.

The address path is ignored because gRPC has a standardized, prescriptive address structure. A gRPC address combines the package, service and method names: `https://localhost:5001/PackageName.ServiceName/MethodName`.

There are some scenarios when an app needs to include a path with gRPC calls. For example, when an ASP.NET Core gRPC app is hosted in an IIS directory and the directory needs to be included in the request. When a path is required, it can be added to the gRPC call using the custom `SubdirectoryHandler` specified below:

```
/// <summary>
/// A delegating handler that adds a subdirectory to the URI of gRPC requests.
/// </summary>
public class SubdirectoryHandler : DelegatingHandler
{
    private readonly string _subdirectory;

    public SubdirectoryHandler(HttpMessageHandler innerHandler, string subdirectory)
        : base(innerHandler)
    {
        _subdirectory = subdirectory;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var old = request.RequestUri;

        var url = $"{old.Scheme}://{old.Host}:{old.Port}";
        url += $"{_subdirectory}{request.RequestUri.AbsolutePath}";
        request.RequestUri = new Uri(url, UriKind.Absolute);

        return base.SendAsync(request, cancellationToken);
    }
}
```

`SubdirectoryHandler` is used when the gRPC channel is created.

```
var handler = new SubdirectoryHandler(new HttpClientHandler(), "/MyApp");

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { HttpHandler = handler });
var client = new Greet.GreeterClient(channel);

var reply = await client.SayHelloAsync(new HelloRequest { Name = ".NET" });
```

The preceding code:

* Creates a `SubdirectoryHandler` with the path `/MyApp`.
* Configures a channel to use `SubdirectoryHandler`.
* Calls the gRPC service with `SayHelloAsync`. The gRPC call is sent to `https://localhost:5001/MyApp/greet.Greeter/SayHello`.

Alternatively, a client factory can be configured with `SubdirectoryHandler` by using [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler).

## gRPC call timeout from `HttpClient.Timeout`

[HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is configured with a 100 second timeout by default. If a `GrpcChannel` is configured to use an `HttpClient`, long-running gRPC streaming calls are canceled if they don't complete within the timeout limit.

```
System.OperationCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
```

There are a couple of ways to fix this error. The first is to configure [HttpClient.Timeout](/en-us/dotnet/api/system.net.http.httpclient.timeout#system-net-http-httpclient-timeout) to a larger value. [Timeout.InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan) disables the timeout:

```
var handler = new HttpClientHandler();

var httpClient = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpClient = httpClient });
var client = new Greeter.GreeterClient(channel);
```

Alternatively, avoid creating `HttpClient` and set `GrpcChannel.HttpHandler` instead:

```
var handler = new HttpClientHandler();

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpHandler = handler });
var client = new Greeter.GreeterClient(channel);
```

This document discusses commonly encountered problems when developing gRPC apps on .NET.

## Mismatch between client and service SSL/TLS configuration

The gRPC template and samples use [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246) to secure gRPC services by default. gRPC clients need to use a secure connection to call secured gRPC services successfully.

You can verify the ASP.NET Core gRPC service is using TLS in the logs written on app start. The service will be listening on an HTTPS endpoint:

```
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
```

The .NET client must use `https` in the server address to make calls with a secured connection:

```
static async Task Main(string[] args)
{
    // The port number(5001) must match the port of the gRPC server.
    var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greet.GreeterClient(channel);
}
```

All gRPC client implementations support TLS. gRPC clients from other languages typically require the channel configured with `SslCredentials`. `SslCredentials` specifies the certificate that the client will use, and it must be used instead of insecure credentials. For examples of configuring the different gRPC client implementations to use TLS, see [gRPC Authentication](https://www.grpc.io/docs/guides/auth/).

## Call a gRPC service with an untrusted/invalid certificate

The .NET gRPC client requires the service to have a trusted certificate. The following error message is returned when calling a gRPC service without a trusted certificate:

> Unhandled exception. System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.

You may see this error if you are testing your app locally and the ASP.NET Core HTTPS development certificate is not trusted. For instructions to fix this issue, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../security/enforcing-ssl?view=aspnetcore-10.0#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos).

If you are calling a gRPC service on another machine and are unable to trust the certificate then the gRPC client can be configured to ignore the invalid certificate. The following code uses [HttpClientHandler.ServerCertificateCustomValidationCallback](/en-us/dotnet/api/system.net.http.httpclienthandler.servercertificatecustomvalidationcallback) to allow calls without a trusted certificate:

```
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = 
    HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpHandler = handler });
var client = new Greet.GreeterClient(channel);
```

The [gRPC client factory](clientfactory?view=aspnetcore-10.0) allows calls without a trusted certificate. Use the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) extension method to configure the handler on the client:

```
services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        var handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback = 
            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;

        return handler;
    });
```

Warning

Untrusted certificates should only be used during app development. Production apps should always use valid certificates.

## Call insecure gRPC services with .NET client

The .NET gRPC client can call insecure gRPC services by specifying `http` in the server address. For example, `GrpcChannel.ForAddress("http://localhost:5000")`.

There are some additional requirements to call insecure gRPC services depending on the .NET version an app is using:

* .NET 5 or later requires [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client) version 2.32.0 or later.

* .NET Core 3.x requires additional configuration. The app must set the `System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport` switch to `true`:

  ```
  // This switch must be set before creating the GrpcChannel/HttpClient.
  AppContext.SetSwitch(
      "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

  // The port number(5000) must match the port of the gRPC server.
  var channel = GrpcChannel.ForAddress("http://localhost:5000");
  var client = new Greet.GreeterClient(channel);
  ```

The `System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport` switch is only required for .NET Core 3.x. It does nothing in .NET 5 and isn't required.

Important

Insecure gRPC services must be hosted on a HTTP/2-only port. For more information, see [ASP.NET Core protocol negotiation](aspnetcore?view=aspnetcore-10.0#protocol-negotiation).

## Unable to start ASP.NET Core gRPC app on macOS

Kestrel doesn't support HTTP/2 with TLS on macOS before .NET 8. The ASP.NET Core gRPC template and samples use TLS by default. You'll see the following error message when you attempt to start the gRPC server:

> Unable to bind to https\://localhost:5001 on the IPv4 loopback interface: 'HTTP/2 over TLS is not supported on macOS due to missing ALPN support.'.

To work around this issue in .NET 7 or earlier, configure Kestrel and the gRPC client to use HTTP/2 *without* TLS. You should only do this during development. Not using TLS will result in gRPC messages being sent without encryption.

Kestrel must configure an HTTP/2 endpoint without TLS in `Program.cs`:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(options =>
            {
                // Setup a HTTP/2 endpoint without TLS.
                options.ListenLocalhost(5000, o => o.Protocols = 
                    HttpProtocols.Http2);
            });
            webBuilder.UseStartup<Startup>();
        });
```

When an HTTP/2 endpoint is configured without TLS, the endpoint's [ListenOptions.Protocols](../fundamentals/servers/kestrel?view=aspnetcore-10.0#listenoptionsprotocols) must be set to `HttpProtocols.Http2`. `HttpProtocols.Http1AndHttp2` can't be used because TLS is required to negotiate HTTP/2. Without TLS, all connections to the endpoint default to HTTP/1.1, and gRPC calls fail.

The gRPC client must also be configured to not use TLS. For more information, see [Call insecure gRPC services with .NET client](#call-insecure-grpc-services-with-net-client).

Warning

HTTP/2 without TLS should only be used during app development. Production apps should always use transport security. For more information, see [Security considerations in gRPC for ASP.NET Core](security?view=aspnetcore-10.0#transport-security).

## gRPC C# assets are not code generated from `.proto` files

gRPC code generation of concrete clients and service base classes requires protobuf files and tooling to be referenced from a project. You must include:

* `.proto` files you want to use in the `<Protobuf>` item group. [Imported `.proto` files](https://developers.google.com/protocol-buffers/docs/proto3#importing-definitions) must be referenced by the project.
* Package reference to the gRPC tooling package [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/).

For more information on generating gRPC C# assets, see [gRPC Services with C#](basics?view=aspnetcore-10.0).

An ASP.NET Core web app hosting gRPC services only needs the service base class generated:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
```

A gRPC client app making gRPC calls only needs the concrete client generated:

```
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
```

## WPF projects unable to generate gRPC C# assets from `.proto` files

WPF projects have a [known issue](https://github.com/dotnet/wpf/issues/810) that prevents gRPC code generation from working correctly. Any gRPC types generated in a WPF project by referencing `Grpc.Tools` and `.proto` files will create compilation errors when used:

> error CS0246: The type or namespace name 'MyGrpcServices' could not be found (are you missing a using directive or an assembly reference?)

You can workaround this issue by:

1. Create a new .NET class library project.

2. In the new project, add references to enable [C# code generation from `.proto` files](basics?view=aspnetcore-10.0#generated-c-assets):

   * Add the following package references:

     * [Grpc.Tools](https://www.nuget.org/packages/Grpc.Tools/)
     * [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client/)
     * [Google.Protobuf](https://www.nuget.org/packages/Google.Protobuf/)

   * Add `.proto` files to the `<Protobuf>` item group.

3. In the WPF application, add a reference to the new project.

The WPF application can use the gRPC generated types from the new class library project.

## Calling gRPC services hosted in a sub-directory

Warning

Many third-party gRPC tools don't support services hosted in subdirectories. Consider finding a way to host gRPC as the root directory.

The path component of a gRPC channel's address is ignored when making gRPC calls. For example, `GrpcChannel.ForAddress("https://localhost:5001/ignored_path")` won't use `ignored_path` when routing gRPC calls for the service.

The address path is ignored because gRPC has a standardized, prescriptive address structure. A gRPC address combines the package, service and method names: `https://localhost:5001/PackageName.ServiceName/MethodName`.

There are some scenarios when an app needs to include a path with gRPC calls. For example, when an ASP.NET Core gRPC app is hosted in an IIS directory and the directory needs to be included in the request. When a path is required, it can be added to the gRPC call using the custom `SubdirectoryHandler` specified below:

```
/// <summary>
/// A delegating handler that adds a subdirectory to the URI of gRPC requests.
/// </summary>
public class SubdirectoryHandler : DelegatingHandler
{
    private readonly string _subdirectory;

    public SubdirectoryHandler(HttpMessageHandler innerHandler, string subdirectory)
        : base(innerHandler)
    {
        _subdirectory = subdirectory;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var old = request.RequestUri;

        var url = $"{old.Scheme}://{old.Host}:{old.Port}";
        url += $"{_subdirectory}{request.RequestUri.AbsolutePath}";
        request.RequestUri = new Uri(url, UriKind.Absolute);

        return base.SendAsync(request, cancellationToken);
    }
}
```

`SubdirectoryHandler` is used when the gRPC channel is created.

```
var handler = new SubdirectoryHandler(new HttpClientHandler(), "/MyApp");

var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { HttpHandler = handler });
var client = new Greet.GreeterClient(channel);

var reply = await client.SayHelloAsync(new HelloRequest { Name = ".NET" });
```

The preceding code:

* Creates a `SubdirectoryHandler` with the path `/MyApp`.
* Configures a channel to use `SubdirectoryHandler`.
* Calls the gRPC service with `SayHelloAsync`. The gRPC call is sent to `https://localhost:5001/MyApp/greet.Greeter/SayHello`.

Alternatively, a client factory can be configured with `SubdirectoryHandler` by using [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler).

## gRPC call timeout from `HttpClient.Timeout`

[HttpClient](/en-us/dotnet/api/system.net.http.httpclient) is configured with a 100 second timeout by default. If a `GrpcChannel` is configured to use an `HttpClient`, long-running gRPC streaming calls are canceled if they don't complete within the timeout limit.

```
System.OperationCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
```

There are a couple of ways to fix this error. The first is to configure [HttpClient.Timeout](/en-us/dotnet/api/system.net.http.httpclient.timeout#system-net-http-httpclient-timeout) to a larger value. [Timeout.InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan#system-threading-timeout-infinitetimespan) disables the timeout:

```
var handler = new HttpClientHandler();

var httpClient = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpClient = httpClient });
var client = new Greeter.GreeterClient(channel);
```

Alternatively, avoid creating `HttpClient` and set `GrpcChannel.HttpHandler` instead:

```
var handler = new HttpClientHandler();

var channel = GrpcChannel.ForAddress("https://localhost:5001",
    new GrpcChannelOptions { HttpHandler = handler });
var client = new Greeter.GreeterClient(channel);
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-identity/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/webassembly/standalone-with-identity/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Standalone Blazor WebAssembly apps can be secured with ASP.NET Core Identity by following the guidance in this article.

## Endpoints for registering, logging in, and logging out

Instead of using the default UI provided by ASP.NET Core Identity for SPA and Blazor apps, which is based on Razor Pages, call [MapIdentityApi](/en-us/dotnet/api/microsoft.aspnetcore.routing.identityapiendpointroutebuilderextensions.mapidentityapi) in a backend API to add JSON API endpoints for registering and logging in users with ASP.NET Core Identity. Identity API endpoints also support advanced features, such as two-factor authentication and email verification.

In the following examples, `httpClient` is an instance of [HttpClient](/en-us/dotnet/api/system.net.http.httpclient). For more information, see [Call a web API from an ASP.NET Core Blazor app](../../../call-web-api?view=aspnetcore-10.0).

On the client, call the `/register` endpoint to register a user with their email address and password:

```
using var result = await httpClient.PostAsJsonAsync(
    "register", new
    {
        email,
        password
    });
```

On the client, log in a user with cookie authentication using the `/login` endpoint with `useCookies` query string set to `true`:

```
using var result = await httpClient.PostAsJsonAsync(
    "login?useCookies=true", new
    {
        email,
        password
    });
```

The backend server API establishes cookie authentication with a call to [AddIdentityCookies](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitycookieauthenticationbuilderextensions.addidentitycookies) on the authentication builder:

```
builder.Services
    .AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddIdentityCookies();
```

## Token authentication

For native and mobile scenarios where some clients don't support cookies, the login API provides a parameter to request tokens.

Warning

We recommend using cookies for browser-based apps instead of tokens because the browser handles cookies without exposing them to JavaScript. If you opt to use token-based security in web apps, you're responsible for ensuring the tokens are kept secure.

A custom token (one that is proprietary to the ASP.NET Core Identity platform) is issued that can be used to authenticate subsequent requests. The token should be passed in the `Authorization` header as a bearer token. A refresh token is also provided. This token allows the app to request a new token when the old one expires without forcing the user to log in again.

The tokens are not standard JSON Web Tokens (JWTs). The use of custom tokens is intentional, as the built-in Identity API is meant primarily for simple scenarios. The token option is not intended to be a fully-featured identity service provider or token server, but instead an alternative to the cookie option for clients that can't use cookies.

The following guidance begins the process of implementing token-based authentication with the login API. Custom code is required to complete the implementation. For more information, see [Use Identity to secure a Web API backend for SPAs](../../../../security/authentication/identity-api-authorization?view=aspnetcore-10.0).

Instead of the backend server API establishing cookie authentication with a call to [AddIdentityCookies](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitycookieauthenticationbuilderextensions.addidentitycookies) on the authentication builder, the server API sets up bearer token auth with the [AddBearerToken](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.bearertokenextensions.addbearertoken) extension method. Specify the scheme for bearer authentication tokens with [IdentityConstants.BearerScheme](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityconstants.bearerscheme).

In `Backend/Program.cs`, change the authentication services and configuration to the following:

```
builder.Services
    .AddAuthentication()
    .AddBearerToken(IdentityConstants.BearerScheme);
```

In `BlazorWasmAuth/Identity/CookieAuthenticationStateProvider.cs`, remove the `useCookies` query string parameter in the `LoginAsync` method of the `CookieAuthenticationStateProvider`:

```
- login?useCookies=true
+ login
```

At this point, you must provide custom code to parse the [AccessTokenResponse](/en-us/dotnet/api/microsoft.aspnetcore.authentication.bearertoken.accesstokenresponse) on the client and manage the access and refresh tokens. For more information, see [Use Identity to secure a Web API backend for SPAs](../../../../security/authentication/identity-api-authorization?view=aspnetcore-10.0).

## Additional Identity scenarios

Scenarios covered by the Blazor documentation set:

* [Account confirmation, password management, and recovery codes](account-confirmation-and-password-recovery?view=aspnetcore-10.0)
* [Two-factor authentication (2FA) with a TOTP authenticator app](qrcodes-for-authenticator-apps?view=aspnetcore-10.0)

For information on additional Identity scenarios provided by the API, see [Use Identity to secure a Web API backend for SPAs](../../../../security/authentication/identity-api-authorization?view=aspnetcore-10.0):

* Secure selected endpoints
* User info management

## Use secure authentication flows to maintain sensitive data and credentials

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](../../?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

## Sample apps

In this article, sample apps serve as a reference for standalone Blazor WebAssembly apps that access ASP.NET Core Identity through a backend web API. The demonstration includes two apps:

* `Backend`: A backend web API app that maintains a user identity store for ASP.NET Core Identity.
* `BlazorWasmAuth`: A standalone Blazor WebAssembly frontend app with user authentication.

Access the sample apps through the latest version folder from the repository's root with the following link. The samples are provided for .NET 8 or later. See the `README` file in the `BlazorWebAssemblyStandaloneWithIdentity` folder for steps on how to run the sample apps.

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../../../fundamentals/?view=aspnetcore-10.0#sample-apps))

## Backend web API app packages and code

The backend web API app maintains a user identity store for ASP.NET Core Identity.

### Packages

The app uses the following NuGet packages:

* [`Microsoft.AspNetCore.Identity.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.EntityFrameworkCore)
* [`Microsoft.EntityFrameworkCore.InMemory`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory)
* [`NSwag.AspNetCore`](https://www.nuget.org/packages/NSwag.AspNetCore)

If your app is to use a different EF Core database provider than the in-memory provider, don't create a package reference in your app for `Microsoft.EntityFrameworkCore.InMemory`.

In the app's project file (`.csproj`), [invariant globalization](../../../globalization-localization?view=aspnetcore-10.0#invariant-globalization) is configured.

### Sample app code

[App settings](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/appsettings.json) configure backend and frontend URLs:

* `Backend` app (`BackendUrl`): `https://localhost:7211`
* `BlazorWasmAuth` app (`FrontendUrl`): `https://localhost:7171`

The [`Backend.http` file](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Backend.http) can be used for testing the weather data request. Note that the `BlazorWasmAuth` app must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see [Use .http files in Visual Studio 2022](../../../../test/http-files?view=aspnetcore-10.0).

The following setup and configuration is found in the app's [`Program` file](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/Program.cs).

User identity with cookie authentication is added by calling [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) and [AddIdentityCookies](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitycookieauthenticationbuilderextensions.addidentitycookies). Services for authorization checks are added by a call to [AddAuthorizationBuilder](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.policyservicecollectionextensions.addauthorizationbuilder).

Only recommended for demonstrations, the app uses the [EF Core in-memory database provider](/en-us/ef/core/providers/in-memory/) for the database context registration ([AddDbContext](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkservicecollectionextensions.adddbcontext)). The in-memory database provider makes it easy to restart the app and test the registration and login user flows. Each run starts with a fresh database, but the app includes [test user seeding demonstration code](#test-user-seeding-demonstration), which is described later in this article. If the database is changed to SQLite, users are saved between sessions, but the database must be created through [migrations](/en-us/ef/core/managing-schemas/migrations/), as shown in the [EF Core getting started tutorial](/en-us/ef/core/get-started/overview/first-app)†. You can use other relational providers such as SQL Server for your production code.

Note

†The EF Core getting started tutorial uses PowerShell commands to execute database migrations when using Visual Studio. An alternative approach in Visual Studio is to use the Connected Services UI:

In **Solution Explorer**, double-click **Connected Services**. In **Service Dependencies** > **SQL Server Express LocalDB**, select the ellipsis (`...`) followed by either **Add migration** to create a migration or **Update database** to update the database.

Configure Identity to use the EF Core database and expose the Identity endpoints via the calls to [AddIdentityCore](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentitycore), [AddEntityFrameworkStores](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityentityframeworkbuilderextensions.addentityframeworkstores), and [AddApiEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilderextensions.addapiendpoints).

A [Cross-Origin Resource Sharing (CORS)](../../../../security/cors?view=aspnetcore-10.0) policy is established to permit requests from the frontend and backend apps. Fallback URLs are configured for the CORS policy if app settings don't provide them:

* `Backend` app (`BackendUrl`): `https://localhost:5001`
* `BlazorWasmAuth` app (`FrontendUrl`): `https://localhost:5002`

The project includes packages and configuration to produce [OpenAPI documents](../../../../fundamentals/openapi/overview?view=aspnetcore-10.0).

Services and endpoints for [Swagger/OpenAPI](../../../../tutorials/web-api-help-pages-using-swagger?view=aspnetcore-10.0) are included for web API documentation and development testing. For more information on NSwag, see [Get started with NSwag and ASP.NET Core](../../../../tutorials/getting-started-with-nswag?view=aspnetcore-10.0).

User role claims are sent from a [Minimal API](../../../../fundamentals/apis?view=aspnetcore-10.0) at the `/roles` endpoint.

Routes are mapped for Identity endpoints by calling `MapIdentityApi<AppUser>()`.

A logout endpoint (`/Logout`) is configured in the middleware pipeline to sign users out.

To secure an endpoint, add the [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) extension method to the route definition. For a controller, add the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the controller or action.

For more information on basic patterns for initialization and configuration of a [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) instance, see [DbContext Lifetime, Configuration, and Initialization](/en-us/ef/core/dbcontext-configuration/) in the EF Core documentation.

## Frontend standalone Blazor WebAssembly app packages and code

A standalone Blazor WebAssembly frontend app demonstrates user authentication and authorization to access a private webpage.

### Packages

The app uses the following NuGet packages:

* [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication)
* [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http)
* [`Microsoft.AspNetCore.Components.WebAssembly`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly)
* [`Microsoft.AspNetCore.Components.Gateway`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Gateway)

- [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication)
- [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http)
- [`Microsoft.AspNetCore.Components.WebAssembly`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly)
- [`Microsoft.AspNetCore.Components.WebAssembly.DevServer`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.DevServer)

### Sample app code

The `Models` folder contains the app's models:

* [`FormResult` (`Identity/Models/FormResult.cs`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/Models/FormResult.cs): Response for login and registration.
* [`UserInfo` (`Identity/Models/UserInfo.cs`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/Models/UserInfo.cs): User info from identity endpoint to establish claims.

The [`IAccountManagement` interface (`Identity/CookieHandler.cs`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/IAccountManagement.cs) provides account management services.

The [`CookieAuthenticationStateProvider` class (`Identity/CookieAuthenticationStateProvider.cs`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/CookieAuthenticationStateProvider.cs) handles state for cookie-based authentication and provides account management service implementations described by the `IAccountManagement` interface. The `LoginAsync` method explicitly enables cookie authentication via the `useCookies` query string value of `true`. The class also manages creating role claims for authenticated users.

The [`CookieHandler` class (`Identity/CookieHandler.cs`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/CookieHandler.cs) ensures cookie credentials are sent with each request to the backend web API, which handles Identity and maintains the Identity data store.

The [`wwwroot/appsettings.file`](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/wwwroot/appsettings.json) provides backend and frontend URL endpoints.

The [`App` component](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/App.razor) exposes the authentication state as a cascading parameter. For more information, see [ASP.NET Core Blazor authentication and authorization](../../?view=aspnetcore-10.0#expose-the-authentication-state-as-a-cascading-parameter).

The [`MainLayout` component](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Layout/MainLayout.razor) and [`NavMenu` component](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Layout/NavMenu.razor) use the [`AuthorizeView` component](../../?view=aspnetcore-10.0#authorizeview-component) to selectively display content based on the user's authentication status.

The following components handle common user authentication tasks, making use of `IAccountManagement` services:

* [`Register` component (`Components/Identity/Register.razor`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Identity/Register.razor)
* [`Login` component (`Components/Identity/Login.razor`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Identity/Login.razor)
* [`Logout` component (`Components/Identity/Logout.razor`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Identity/Logout.razor)

The [`PrivatePage` component (`Components/Pages/PrivatePage.razor`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Components/Pages/PrivatePage.razor) requires authentication and shows the user's claims.

Services and configuration is provided in the [`Program` file (`Program.cs`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Program.cs):

* The cookie handler is registered as a scoped service.
* Authorization services are registered.
* The custom authentication state provider is registered as a scoped service.
* The account management interface (`IAccountManagement`) is registered.
* The base host URL is configured for a registered HTTP client instance.
* The base backend URL is configured for a registered HTTP client instance that's used for auth interactions with the backend web API. The HTTP client uses the cookie handler to ensure that cookie credentials are sent with each request.

Call [AuthenticationStateProvider.NotifyAuthenticationStateChanged](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider.notifyauthenticationstatechanged) when the user's authentication state changes. For an example, see the `LoginAsync` and `LogoutAsync` methods of the [`CookieAuthenticationStateProvider` class (`Identity/CookieAuthenticationStateProvider.cs`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/CookieAuthenticationStateProvider.cs).

Warning

The [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component selectively displays UI content depending on whether the user is authorized. All content within a Blazor WebAssembly app placed in an [AuthorizeView](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authorizeview) component is discoverable without authentication, so sensitive content should be obtained from a backend server-based web API after authentication succeeds. For more information, see the following resources:

* [ASP.NET Core Blazor authentication and authorization](../../?view=aspnetcore-10.0#authorizeview-component)
* [Call a web API from an ASP.NET Core Blazor app](../../../call-web-api?view=aspnetcore-10.0)
* [ASP.NET Core Blazor WebAssembly additional security scenarios](../additional-scenarios?view=aspnetcore-10.0)

## Test user seeding demonstration

The `SeedData` class ([`SeedData.cs`](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/Backend/SeedData.cs)) demonstrates how to create test users for development. The test user, named Leela, signs into the app with the email address `leela@contoso.com`. The user's password is set to `Passw0rd!`. Leela is given `Administrator` and `Manager` roles for authorization, which enables the user to access the manager page at `/private-manager-page` but not the editor page at `/private-editor-page`.

Warning

Never allow test user code to run in a production environment. `SeedData.InitializeAsync` is only called in the `Development` environment in the `Program` file:

```
if (builder.Environment.IsDevelopment())
{
    await using var scope = app.Services.CreateAsyncScope();
    await SeedData.InitializeAsync(scope.ServiceProvider);
}
```

## Roles

Role claims aren't sent back from the `manage/info` endpoint to create user claims for users of the `BlazorWasmAuth` app. Role claims are managed independently via a separate request in the `GetAuthenticationStateAsync` method of the [`CookieAuthenticationStateProvider` class (`Identity/CookieAuthenticationStateProvider.cs`)](https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAssemblyStandaloneWithIdentity/BlazorWasmAuth/Identity/CookieAuthenticationStateProvider.cs) after the user is authenticated in the `Backend` project.

In the `CookieAuthenticationStateProvider`, a roles request is made to the `/roles` endpoint of the `Backend` server API project. The response is read into a string by calling [ReadAsStringAsync()](/en-us/dotnet/api/system.net.http.httpcontent.readasstringasync#system-net-http-httpcontent-readasstringasync). [JsonSerializer.Deserialize](/en-us/dotnet/api/system.text.json.jsonserializer.deserialize) deserializes the string into a custom `RoleClaim` array. Finally, the claims are added to the user's claims collection.

In the `Backend` server API's `Program` file, a [Minimal API](../../../../fundamentals/apis?view=aspnetcore-10.0) manages the `/roles` endpoint. Claims of [RoleClaimType](/en-us/dotnet/api/system.security.claims.claimsidentity.roleclaimtype) are [selected](/en-us/dotnet/api/system.linq.enumerable.select) into an [anonymous type](/en-us/dotnet/csharp/fundamentals/types/anonymous-types) and serialized for return to the `BlazorWasmAuth` project with [TypedResults.Json](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.json).

The roles endpoint requires authorization by calling [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization). If you decide not to use Minimal APIs in favor of controllers for secure server API endpoints, be sure to set the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) on controllers or actions.

## Cross-domain hosting (same-site configuration)

The [sample apps](#sample-apps) are configured for hosting both apps at the same domain. If you host the `Backend` app at a different domain than the `BlazorWasmAuth` app, uncomment the code that configures the cookie ([ConfigureApplicationCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.configureapplicationcookie)) in the `Backend` app's `Program` file. The default values are:

* Same-site mode: [SameSiteMode.Lax](/en-us/dotnet/api/microsoft.aspnetcore.http.samesitemode#microsoft-aspnetcore-http-samesitemode-lax)
* Secure policy: [CookieSecurePolicy.SameAsRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy#microsoft-aspnetcore-http-cookiesecurepolicy-sameasrequest)

Change the values to:

* Same-site mode: [SameSiteMode.None](/en-us/dotnet/api/microsoft.aspnetcore.http.samesitemode#microsoft-aspnetcore-http-samesitemode-none)
* Secure policy: [CookieSecurePolicy.Always](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy#microsoft-aspnetcore-http-cookiesecurepolicy-always)

```
- options.Cookie.SameSite = SameSiteMode.Lax;
- options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
+ options.Cookie.SameSite = SameSiteMode.None;
+ options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
```

For more information on same-site cookie settings, see the following resources:

* [Set-Cookie: `SameSite=<samesite-value>` (MDN documentation)](https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value)
* [Cookies: HTTP State Management Mechanism (RFC Draft 6265, Section 4.1)](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1)

## Antiforgery support

Only the logout endpoint (`/logout`) in the `Backend` app requires attention to mitigate the threat of [Cross-Site Request Forgery (CSRF)](../../../../security/anti-request-forgery?view=aspnetcore-10.0).

The logout endpoint checks for an empty body to prevent CSRF attacks. By requiring a body, the request must be made from JavaScript, which is the only way to access the authentication cookie. The logout endpoint can't be accessed by a form-based POST. This prevents a malicious site from logging the user out.

Furthermore, the endpoint is protected by authorization ([RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization)) to prevent anonymous access.

The `BlazorWasmAuth` client app is simply required to pass an empty object `{}` in the body of the request.

Outside of the logout endpoint, [antiforgery mitigation](../../../../security/anti-request-forgery?view=aspnetcore-10.0) is only required when submitting form data to the server encoded as `application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain`. Blazor manages CSRF mitigation for forms in most cases. For more information, see [ASP.NET Core Blazor authentication and authorization](../../?view=aspnetcore-10.0#antiforgery-support) and [ASP.NET Core Blazor forms overview](../../../forms/?view=aspnetcore-10.0#antiforgery-support).

Requests to other server API endpoints (web API) with `application/json`-encoded content and [CORS](../../../../security/cors?view=aspnetcore-10.0) enabled doesn't require CSRF protection. This is why no CSRF protection is required for the `Backend` app's data processing (`/data-processing`) endpoint. The roles (`/roles`) endpoint doesn't need CSRF protection because it's a GET endpoint that doesn't modify any state.

## Troubleshoot

### Logging

To enable debug or trace logging for Blazor WebAssembly authentication, see [ASP.NET Core Blazor logging](../../../fundamentals/logging?view=aspnetcore-10.0).

### Common errors

Check each project's configuration. Confirm that the URLs are correct:

* `Backend` project

  * `appsettings.json`

    * `BackendUrl`
    * `FrontendUrl`

  * `Backend.http`: `Backend_HostAddress`

* `BlazorWasmAuth` project: `wwwroot/appsettings.json`

  * `BackendUrl`
  * `FrontendUrl`

If the configuration appears correct:

* Analyze application logs.

* Examine the network traffic between the `BlazorWasmAuth` app and `Backend` app with the browser's developer tools. Often, an exact error message or a message with a clue to what's causing the problem is returned to the client by the backend app after making a request. Developer tools guidance is found in the following articles:

* [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)

* [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)

* [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

The documentation team responds to document feedback and bugs in articles. Open an issue using the **Open a documentation issue** link at the bottom of the article. The team isn't able to provide product support. Several public support forums are available to assist with troubleshooting an app. We recommend the following:

* [Stack Overflow (tag: `blazor`)](https://stackoverflow.com/questions/tagged/blazor)
* [ASP.NET Core Slack Team](https://join.slack.com/t/aspnetcore/shared_invite/zt-1mv5487zb-EOZxJ1iqb0A0ajowEbxByQ)
* [Blazor Gitter](https://gitter.im/aspnet/Blazor)

*The preceding forums are not owned or controlled by Microsoft.*

For non-security, non-sensitive, and non-confidential reproducible framework bug reports, [open an issue with the ASP.NET Core product unit](https://github.com/dotnet/aspnetcore/issues). Don't open an issue with the product unit until you've thoroughly investigated the cause of a problem and can't resolve it on your own and with the help of the community on a public support forum. The product unit isn't able to troubleshoot individual apps that are broken due to simple misconfiguration or use cases involving third-party services. If a report is sensitive or confidential in nature or describes a potential security flaw in the product that cyberattackers may exploit, see [Reporting security issues and bugs (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md#reporting-security-issues-and-bugs).

### Cookies and site data

Cookies and site data can persist across app updates and interfere with testing and troubleshooting. Clear the following when making app code changes, user account changes, or app configuration changes:

* User sign-in cookies
* App cookies
* Cached and stored site data

One approach to prevent lingering cookies and site data from interfering with testing and troubleshooting is to:

* Configure a browser

  * Use a browser for testing that you can configure to delete all cookie and site data each time the browser is closed.
  * Make sure that the browser is closed manually or by the IDE for any change to the app, test user, or provider configuration.

* Use a custom command to open a browser in InPrivate or Incognito mode in Visual Studio:

  * Open **Browse With** dialog box from Visual Studio's **Run** button.

  * Select the **Add** button.

  * Provide the path to your browser in the **Program** field. The following executable paths are typical installation locations for Windows 10. If your browser is installed in a different location or you aren't using Windows 10, provide the path to the browser's executable.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

  * Provide a name in the **Friendly name** field. For example, `Firefox Auth Testing`.

  * Select the **OK** button.

  * To avoid having to select the browser profile for each iteration of testing with an app, set the profile as the default with the **Set as Default** button.

  * Make sure that the browser is closed by the IDE for any change to the app, test user, or provider configuration.

### App upgrades

A functioning app may fail immediately after upgrading either the .NET SDK on the development machine or changing package versions within the app. In some cases, incoherent packages may break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:

1. Clear the local system's NuGet package caches by executing [`dotnet nuget locals all --clear`](/en-us/dotnet/core/tools/dotnet-nuget-locals) from a command shell.
2. Delete the project's `bin` and `obj` folders.
3. Restore and rebuild the project.
4. Delete all of the files in the deployment folder on the server prior to redeploying the app.

Note

Use of package versions incompatible with the app's target framework isn't supported. For information on a package, use the [NuGet Gallery](https://www.nuget.org).

### Inspect the user's claims

To troubleshoot problems with user claims, the following `UserClaims` component can be used directly in apps or serve as the basis for further customization.

`UserClaims.razor`:

```
@page "/user-claims"
@using System.Security.Claims
@attribute [Authorize]

<PageTitle>User Claims</PageTitle>

<h1>User Claims</h1>

**Name**: @AuthenticatedUser?.Identity?.Name

<h2>Claims</h2>

@foreach (var claim in AuthenticatedUser?.Claims ?? Array.Empty<Claim>())
{
    <p class="claim">@(claim.Type): @claim.Value</p>
}

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? AuthenticationState { get; set; }

    public ClaimsPrincipal? AuthenticatedUser { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (AuthenticationState is not null)
        {
            var state = await AuthenticationState;
            AuthenticatedUser = state.User;
        }
    }
}
```

## Related content

* [`AuthenticationStateProvider` service](../../?view=aspnetcore-10.0#authenticationstateprovider-service)
* [Client-side SignalR cross-origin negotiation for authentication](../../../fundamentals/signalr?view=aspnetcore-10.0#client-side-signalr-cross-origin-negotiation-for-authentication)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/config-files
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/yarp/config-files.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# YARP Configuration Files

The reverse proxy can load configuration for routes and clusters from files by using the [IConfiguration](/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration) abstraction from Microsoft.Extensions.

This article provides example proxy configuration for YARP. Although the configuration example uses JSON, any `IConfiguration` source should work. Also, the configuration updates without restarting the proxy when the source file changes.

## Load the proxy configuration

To load the proxy configuration from the `IConfiguration` instance, add the following code in the *Program.cs* file:

```
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Add the reverse proxy capability to the server
builder.Services.AddReverseProxy()
    // Initialize the reverse proxy from the "ReverseProxy" section of configuration
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();

// Register the reverse proxy routes
app.MapReverseProxy();

app.Run();
```

Note

For details about middleware ordering, see [ASP.NET Core middleware > Middleware order](/en-us/aspnet/core/fundamentals/middleware/#middleware-order).

Configuration can be modified during the load sequence by using [YARP configuration filters](config-filters?view=aspnetcore-10.0).

## Load from multiple configuration sources

Starting in version 1.1, YARP supports loading the proxy configuration from multiple sources. You can call the `LoadFromConfig` method multiple times and reference different `IConfiguration` sections. You can also combine the call with a different config source, such as InMemory. Routes can reference clusters from other sources.

Note

Merging partial config from different sources for a given route or cluster isn't supported.

```
services.AddReverseProxy()
    .LoadFromConfig(Configuration.GetSection("ReverseProxy1"))
    .LoadFromConfig(Configuration.GetSection("ReverseProxy2"));
```

Another example:

```

services.AddReverseProxy()
    .LoadFromMemory(routes, clusters)
    .LoadFromConfig(Configuration.GetSection("ReverseProxy"));
```

## Review the configuration contract

File-based configuration is dynamically mapped to the types in the [Yarp.ReverseProxy.Configuration](/en-us/dotnet/api/yarp.reverseproxy.configuration) namespace by an [IProxyConfigProvider](/en-us/dotnet/api/yarp.reverseproxy.configuration.iproxyconfigprovider) implementation. The contract converts at application start and each time the configuration changes.

## Examine the configuration structure

The configuration consists of a named section and subsections for routes and clusters. In the previous example, the named section is `Configuration.GetSection("ReverseProxy")`.

```
{
  "ReverseProxy": {
    "Routes": {
      "route1" : {
        "ClusterId": "cluster1",
        "Match": {
          "Path": "{**catch-all}",
          "Hosts" : [ "www.aaaaa.com", "www.bbbbb.com"]
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "cluster1/destination1": {
            "Address": "https://example.com/"
          }
        }
      }
    }
  }
}
```

### Define configuration routes

The `Routes` section is an unordered collection of route matches and their associated configuration.

Each route requires at least the following fields:

* `RouteId`: A unique name for the route.
* `ClusterId`: The name of an entry in the `Clusters` section.
* `Match`: Either a `Hosts` array or a `Path` pattern string. You can define the path by using [Routing in ASP.NET Core > Route templates](/en-us/aspnet/core/fundamentals/routing#route-templates).

Route matching is based on the most specific routes having highest precedence, as described in [URL matching](/en-us/aspnet/core/fundamentals/routing#url-matching). Explicit ordering can be achieved by using the `order` field, where lower values take higher priority.

[YARP headers](header-routing?view=aspnetcore-10.0), [authentication and authorization](authn-authz?view=aspnetcore-10.0), [cross-origin requests (CORS)](cors?view=aspnetcore-10.0), and other route-based policies can be configured on each route entry. For other fields, see the [RouteConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.routeconfig) reference.

The proxy applies the given matching criteria and policies, and passes off the request to the specified cluster.

### Define configuration clusters

The `Clusters` section is an unordered collection of named clusters. A cluster primarily contains a collection of named destinations and their addresses, any of which is considered capable of handling requests for a given route. The proxy processes the request according to the route and cluster configuration to select a destination.

For other fields, see the [ClusterConfig](/en-us/dotnet/api/yarp.reverseproxy.configuration.clusterconfig) reference.

## Review all config properties

The following JSON example shows all properties available for the YARP proxy configuration.

```
{
  // Base URLs the server listens on, must be configured independently of the routes defined below
  "Urls": "http://localhost:5000;https://localhost:5001",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      // Uncomment to hide diagnostic messages from runtime and proxy
      // "Microsoft": "Warning",
      // "Yarp" : "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ReverseProxy": {
    // Routes inform the proxy which requests to forward
    "Routes": {
      "minimumroute" : {
        // Matches anything and routes to www.example.com
        "ClusterId": "minimumcluster",
        "Match": {
          "Path": "{**catch-all}"
        }
      },
      "allrouteprops" : {
        // Matches /something/* and routes to "allclusterprops"
        "ClusterId": "allclusterprops", // Name of a cluster
        "Order" : 100, // Lower numbers have higher precedence
        "MaxRequestBodySize" : 1000000, // In bytes. An optional override of the server's limit (30MB default). Set to -1 to disable.
        "AuthorizationPolicy" : "Anonymous", // Name of the policy or "Default", "Anonymous"
        "CorsPolicy" : "Default", // Name of the CorsPolicy to apply to this route or "Default", "Disable"
        "Match": {
          "Path": "/something/{**remainder}", // Path to match by using ASP.NET syntax
          "Hosts" : [ "www.aaaaa.com", "www.bbbbb.com"], // Host names to match, unspecified is any
          "Methods" : [ "GET", "PUT" ], // HTTP methods that match, unspecified is all
          "Headers": [ // Headers to match, unspecified is any
            {
              "Name": "MyCustomHeader", // Name of the header
              "Values": [ "value1", "value2", "another value" ], // Match any of these values
              "Mode": "ExactHeader", // Or, match "HeaderPrefix", "Exists" , "Contains", "NotContains", "NotExists"
              "IsCaseSensitive": true
            }
          ],
          "QueryParameters": [ // Query parameters to match, unspecified is any
            {
              "Name": "MyQueryParameter", // Name of the query parameter
              "Values": [ "value1", "value2", "another value" ], // Match any of these values
              "Mode": "Exact", // Or, match "Prefix", "Exists" , "Contains", "NotContains"
              "IsCaseSensitive": true
            }
          ]
        },
        "Metadata" : { // List of key value pairs that can be used by custom extensions
          "MyName" : "MyValue"
        },
        "Transforms" : [ // List of transforms. See the Transforms article for more details
          {
            "RequestHeader": "MyHeader",
            "Set": "MyValue"
          }
        ]
      }
    },
    // Clusters tell the proxy where and how to forward requests
    "Clusters": {
      "minimumcluster": {
        "Destinations": {
          "example.com": {
            "Address": "http://www.example.com/"
          }
        }
      },
      "allclusterprops": {
        "Destinations": {
          "first_destination": {
            "Address": "https://contoso.com"
          },
          "another_destination": {
            "Address": "https://10.20.30.40",
            "Health" : "https://10.20.30.40:12345/test" // Override for active health checks
          }
        },
        "LoadBalancingPolicy" : "PowerOfTwoChoices", // Alternatively "FirstAlphabetical", "Random", "RoundRobin", "LeastRequests"
        "SessionAffinity": {
          "Enabled": true, // Default is 'false'
          "Policy": "Cookie", // Default, alternatively "CustomHeader"
          "FailurePolicy": "Redistribute", // Default, alternatively "Return503Error"
          "Settings" : {
              "CustomHeaderName": "MySessionHeaderName" // Default is 'X-Yarp-Proxy-Affinity`
          }
        },
        "HealthCheck": {
          "Active": { // Makes API calls to validate the health
            "Enabled": "true",
            "Interval": "00:00:10",
            "Timeout": "00:00:10",
            "Policy": "ConsecutiveFailures",
            "Path": "/api/health", // API endpoint to query for health state
            "Query": "?foo=bar"
          },
          "Passive": { // Disables destinations based on HTTP response codes
            "Enabled": true, // Default is false
            "Policy" : "TransportFailureRateHealthPolicy", // Required
            "ReactivationPeriod" : "00:00:10" // 10s
          }
        },
        "HttpClient" : { // Configuration of HttpClient instance used to contact destinations
          "SSLProtocols" : "Tls13",
          "DangerousAcceptAnyServerCertificate" : false,
          "MaxConnectionsPerServer" : 1024,
          "EnableMultipleHttp2Connections" : true,
          "RequestHeaderEncoding" : "Latin1", // How to interpret non ASCII characters in request header values
          "ResponseHeaderEncoding" : "Latin1" // How to interpret non ASCII characters in response header values
        },
        "HttpRequest" : { // Options for sending request to destination
          "ActivityTimeout" : "00:02:00",
          "Version" : "2",
          "VersionPolicy" : "RequestVersionOrLower",
          "AllowResponseBuffering" : "false"
        },
        "Metadata" : { // Custom Key value pairs
          "TransportFailureRateHealthPolicy.RateLimit": "0.5", // Used by Passive health policy
          "MyKey" : "MyValue"
        }
      }
    }
  }
}
```

## Related content

* [Logging configuration](diagnosing-yarp-issues?view=aspnetcore-10.0#logging)
* [HTTP client configuration](http-client-config?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/interprocess-namedpipes
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/interprocess-namedpipes.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Inter-process communication with gRPC and Named pipes

By [James Newton-King](https://twitter.com/jamesnk)

.NET supports inter-process communication (IPC) using gRPC. For more information about getting started with using gRPC to communicate between processes, see [Inter-process communication with gRPC](interprocess?view=aspnetcore-10.0).

[Named pipes](https://wikipedia.org/wiki/Named_pipe) is an IPC transport that is supported on all versions of Windows. Named pipes integrate well with [Windows security](/en-us/windows/win32/ipc/named-pipe-security-and-access-rights) to control client access to the pipe. This article discusses how to configure gRPC communication over named pipes.

## Prerequisites

* .NET 8 or later
* Windows

## Server configuration

Named pipes are supported by [Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0), which is configured in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenNamedPipe("MyPipeName", listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
    });
});
```

The preceding example:

* Configures Kestrel's endpoints in [ConfigureKestrel](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderkestrelextensions.configurekestrel).
* Calls `ListenNamedPipe` to listen to a named pipe with the specified name.
* Creates a named pipe endpoint that isn't configured to use HTTPS. For information about enabling HTTPS, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsusehttps).

### Configuring `PipeSecurity` for named pipes

To control which users or groups can connect, use the [NamedPipeTransportOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.namedpipes.namedpipetransportoptions) class. This allows a custom [`PipeSecurity`](/en-us/dotnet/api/system.io.pipes.pipesecurity) object to be specified.

Example:

```
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes;
using System.IO.Pipes;
using System.Security.AccessControl;
using System.Security.Principal;

var builder = WebApplication.CreateBuilder(args);

// Configure PipeSecurity
builder.WebHost.UseNamedPipes(options =>
{
    using var serverIdentity = WindowsIdentity.GetCurrent();
    var pipeSecurity = new PipeSecurity();

    // Grant the account the server runs as the rights to create each instance
    // of the pipe that Kestrel listens on.
    pipeSecurity.AddAccessRule(new PipeAccessRule(
        serverIdentity.User!,
        PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance,
        AccessControlType.Allow));

    // Grant client accounts read/write access only. Replace {CLIENT GROUP}
    // with a security group containing only the accounts allowed to call
    // the service.
    pipeSecurity.AddAccessRule(new PipeAccessRule(
        "{CLIENT GROUP}",
        PipeAccessRights.ReadWrite,
        AccessControlType.Allow));
    // Add additional rules as needed

    options.PipeSecurity = pipeSecurity;
    options.CurrentUserOnly = false;
});
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ListenNamedPipe("MyPipeName", listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
    });
});
```

The preceding example:

* Calls `UseNamedPipes` on the [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) to access and configure [NamedPipeTransportOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.namedpipes.namedpipetransportoptions).
* Sets [CurrentUserOnly](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.namedpipes.namedpipetransportoptions.currentuseronly#microsoft-aspnetcore-server-kestrel-transport-namedpipes-namedpipetransportoptions-currentuseronly) to `false`, which is required when providing a custom [PipeSecurity](/en-us/dotnet/api/system.io.pipes.pipesecurity) object.
* Sets the [PipeSecurity](/en-us/dotnet/api/system.io.pipes.pipesecurity) property to control which users or groups can connect to the named pipe.
* Grants the account the server runs as read/write and `CreateNewInstance` access. Kestrel creates several instances of the pipe to accept connections in parallel, and Windows requires `CreateNewInstance` to create each instance after the first.
* Grants the client security group read/write access only, where the `{CLIENT GROUP}` placeholder is a group containing the accounts allowed to call the service. Additional security rules can be added as needed for the scenario.

Important

Unless the app also configures authentication and authorization, the pipe's security descriptor is the only thing that prevents other accounts from calling the server. Grant access to the narrowest principal the scenario requires. Avoid broad groups such as `Users`, which resolves to `BUILTIN\Users` and includes every authenticated and interactive account on the machine, plus `Domain Users` on a domain-joined machine. For information about authenticating callers, see [Authentication and authorization in gRPC for ASP.NET Core](authn-and-authz?view=aspnetcore-10.0).

Warning

Don't grant `PipeAccessRights.CreateNewInstance` to client accounts or to groups that contain untrusted users. `CreateNewInstance` corresponds to the Windows `FILE_CREATE_PIPE_INSTANCE` right, which authorizes a grantee to create *additional server instances* under the same pipe name. Windows distributes incoming client connections across all instances of a pipe, so a process holding that right can accept genuine client connections and impersonate the service. Grant `CreateNewInstance` only to the account the server runs as.

Inspecting the connected pipe's owner SID from the client doesn't detect an impostor created this way. Every instance of a named pipe shares the security descriptor supplied when the first instance was created, so an instance added by another process reports the same owner as the genuine server. To let clients verify which server they're talking to, authenticate the channel itself. For example, enable HTTPS on the endpoint and validate the server certificate, or require a secret that only the genuine server can present. For more information, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsusehttps).

Important

`CurrentUserOnly` defaults to `true`, which restricts the pipe to the account the server runs as. Setting a custom `PipeSecurity` requires `CurrentUserOnly` to be `false`, otherwise an `ArgumentException` is thrown. Setting it to `false` replaces that built-in restriction, so the supplied `PipeSecurity` becomes the only access control on the pipe.

Note

Account names are resolved to security identifiers (SIDs) when the access rule is added. Resolution is locale-dependent and can fail on a localized installation of Windows or on a domain-joined machine that can't reach a domain controller. Constructing a [SecurityIdentifier](/en-us/dotnet/api/system.security.principal.securityidentifier) directly from the group's SID avoids both problems.

### Customize Kestrel named pipe endpoints

Kestrel's named pipe support enables advanced customization, allowing you to configure different security settings for each endpoint using the `CreateNamedPipeServerStream` option. This approach is ideal for scenarios where multiple named pipe endpoints require unique access controls. The ability to customize pipes per endpoint is available starting with .NET 9.

An example of where this is useful is a Kestrel app that requires two pipe endpoints with different access security. The `CreateNamedPipeServerStream` option can be used to create pipes with custom security settings, depending on the pipe name.

```
using Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes;
using System.IO.Pipes;
using System.Security.AccessControl;
using System.Security.Principal;

var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenNamedPipe("pipe1");
    options.ListenNamedPipe("pipe2");
});

builder.WebHost.UseNamedPipes(options =>
{
    options.CurrentUserOnly = false;
    options.CreateNamedPipeServerStream = (context) =>
    {
        var pipeSecurity = CreatePipeSecurity(context.NamedPipeEndPoint.PipeName);

        return NamedPipeServerStreamAcl.Create(context.NamedPipeEndPoint.PipeName, PipeDirection.InOut,
            NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte,
            context.PipeOptions, inBufferSize: 0, outBufferSize: 0, pipeSecurity);
    };
});

static PipeSecurity CreatePipeSecurity(string pipeName)
{
    using var serverIdentity = WindowsIdentity.GetCurrent();
    var pipeSecurity = new PipeSecurity();

    // The server account creates every instance of both pipes.
    pipeSecurity.AddAccessRule(new PipeAccessRule(
        serverIdentity.User!,
        PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance,
        AccessControlType.Allow));

    // pipe1 and pipe2 allow different client groups to connect. Replace the
    // placeholders with groups containing the accounts allowed to connect.
    var clientGroup = pipeName == "pipe1"
        ? "{PIPE1 CLIENT GROUP}"
        : "{PIPE2 CLIENT GROUP}";

    pipeSecurity.AddAccessRule(new PipeAccessRule(
        clientGroup,
        PipeAccessRights.ReadWrite,
        AccessControlType.Allow));

    return pipeSecurity;
}
```

The preceding example applies the same principle per endpoint: only the server's own account is granted `CreateNewInstance`, and each pipe grants read/write access to a different set of callers. `CurrentUserOnly` is set to `false` because `context.PipeOptions` is passed to [Create](/en-us/dotnet/api/system.io.pipes.namedpipeserverstreamacl.create) along with a `PipeSecurity` object.

Warning

A pipe's security descriptor controls which accounts can *connect* to that pipe. It doesn't control which services a connected caller can invoke. Every endpoint mapped in the app is served on every named pipe endpoint, so a caller that connects to any pipe can call every mapped service. Don't rely on per-pipe access control to restrict access to individual services. Use authentication and authorization instead, or host the restricted services in a separate process with its own pipe. For more information, see [Authentication and authorization in gRPC for ASP.NET Core](authn-and-authz?view=aspnetcore-10.0).

## Client configuration

`GrpcChannel` supports making gRPC calls over custom transports. When a channel is created, it can be configured with a [SocketsHttpHandler](/en-us/dotnet/api/system.net.http.socketshttphandler) that has a custom [ConnectCallback](/en-us/dotnet/api/system.net.http.socketshttphandler.connectcallback#system-net-http-socketshttphandler-connectcallback). The callback allows the client to make connections over custom transports and then send HTTP requests over that transport.

Note

Some connectivity features of `GrpcChannel`, such as client side load balancing and channel status, can't be used together with named pipes.

Named pipes connection factory example:

```
public class NamedPipesConnectionFactory
{
    private readonly string pipeName;

    public NamedPipesConnectionFactory(string pipeName)
    {
        this.pipeName = pipeName;
    }

    public async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext _,
        CancellationToken cancellationToken = default)
    {
        var clientStream = new NamedPipeClientStream(
            serverName: ".",
            pipeName: this.pipeName,
            direction: PipeDirection.InOut,
            options: PipeOptions.WriteThrough | PipeOptions.Asynchronous,
            impersonationLevel: TokenImpersonationLevel.Anonymous);

        try
        {
            await clientStream.ConnectAsync(cancellationToken).ConfigureAwait(false);
            return clientStream;
        }
        catch
        {
            clientStream.Dispose();
            throw;
        }
    }
}
```

Using the custom connection factory to create a channel:

```
public static GrpcChannel CreateChannel()
{
    var connectionFactory = new NamedPipesConnectionFactory("MyPipeName");
    var socketsHttpHandler = new SocketsHttpHandler
    {
        ConnectCallback = connectionFactory.ConnectAsync
    };

    return GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
    {
        HttpHandler = socketsHttpHandler
    });
}
```

Channels created using the preceding code send gRPC calls over named pipes.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-29

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/blazor-web-app-with-oidc
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/blazor-web-app-with-oidc.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Note

Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. For more information, see [Azure AD B2C: Frequently asked questions (FAQ)](/en-us/azure/active-directory-b2c/faq).

This article describes how to secure a Blazor Web App with [OpenID Connect (OIDC)](https://openid.net/developers/how-connect-works/) using a sample app in the [`dotnet/blazor-samples` GitHub repository (.NET 8 or later)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps)).

For Microsoft Entra ID, you can use [AddMicrosoftIdentityWebApp](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapp) from [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) ([`Microsoft.Identity.Web` NuGet package](https://www.nuget.org/packages/Microsoft.Identity.Web), [API documentation](/en-us/dotnet/api/microsoft.identity.web)), which adds both the OIDC and Cookie authentication handlers with the appropriate defaults. The sample app and the guidance in this article don't use Microsoft Identity Web. The guidance demonstrates how to configure the OIDC handler *manually* for any OIDC provider. For more information on implementing Microsoft Identity Web, see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0).

This version of the article covers implementing OIDC with the [Backend for Frontend (BFF) pattern](/en-us/azure/architecture/patterns/backends-for-frontends) with [YARP](https://dotnet.github.io/yarp/) and [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview). Change the article version selector to either **Without YARP and Aspire (Interactive Auto)** (Interactive Auto rendering) or **Without YARP and Aspire (Interactive Server)** (Interactive Server rendering) if the app's specification doesn't call for adopting YARP and Aspire.

## Prerequisites

[Aspire](/en-us/dotnet/aspire/get-started/aspire-overview) requires [Visual Studio](https://visualstudio.microsoft.com/) version 17.10 or later.

Also, see the *Prerequisites* section of [Quickstart: Build your first Aspire solution](/en-us/dotnet/aspire/get-started/build-your-first-aspire-app?tabs=visual-studio#prerequisites).

## Sample solution

The sample app consists of the following projects:

* Aspire:

  * `Aspire.AppHost`: Used to manage the high-level orchestration concerns of the app.
  * `Aspire.ServiceDefaults`: Contains default Aspire app configurations that can be extended and customized as needed.

* `MinimalApiJwt`: Backend web API, containing an example [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.

* `BlazorWebAppOidc`: Server-side project of the Blazor Web App. The project uses [YARP](https://dotnet.github.io/yarp/) to proxy requests to a weather forecast endpoint in the backend web API project (`MinimalApiJwt`) with the `access_token` stored in the authentication cookie.

* `BlazorWebAppOidc.Client`: Client-side project of the Blazor Web App.

Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the `BlazorWebAppOidcBffAutoYarpAspire` folder for .NET 8 or later.

Start the solution from the ***`Aspire/Aspire.AppHost` project***.

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

The Blazor Web App uses [the Auto render mode with global interactivity](../components/render-modes?view=aspnetcore-10.0).

The server project calls [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization) to add a server-side authentication state provider that uses [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) to flow the authentication state to the client. The client calls [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization) to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application.

The `PersistingAuthenticationStateProvider` class (`PersistingAuthenticationStateProvider.cs`) is a server-side [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) that uses [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) to flow the authentication state to the client, which is then fixed for the lifetime of the WebAssembly application.

This app is a starting point for any OIDC authentication flow. OIDC is configured manually in the app and doesn't rely upon [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra) or [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) packages, nor does the sample app require [Microsoft Azure](https://azure.microsoft.com/) hosting. However, the sample app can be used with Entra, Microsoft Identity Web, and hosted in Azure.

Automatic non-interactive token refresh with the help of a custom cookie refresher (`CookieOidcRefresher.cs`).

The [Backend for Frontend (BFF) pattern](/en-us/azure/architecture/patterns/backends-for-frontends) is adopted using [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview) for service discovery and [YARP](https://dotnet.github.io/yarp/) for proxying requests to a weather forecast endpoint on the backend app.

The backend web API (`MinimalApiJwt`) uses JWT-bearer authentication to validate JWT tokens saved by the Blazor Web App in the sign-in cookie.

Aspire improves the experience of building .NET cloud-native apps. It provides a consistent, opinionated set of tools and patterns for building and running distributed apps.

YARP (Yet Another Reverse Proxy) is a library used to create a reverse proxy server. `MapForwarder` in the `Program` file of the server project adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using default configuration for the outgoing request, customized transforms, and default HTTP client:

* When rendering the `Weather` component on the server, the component uses the `ServerWeatherForecaster` class to proxy the request for weather data with the user's access token. [IHttpContextAccessor.HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor.httpcontext#microsoft-aspnetcore-http-ihttpcontextaccessor-httpcontext) determines if an [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is available for use by the `GetWeatherForecastAsync` method. For more information, see [ASP.NET Core Razor components](../components/?view=aspnetcore-10.0#ihttpcontextaccessorhttpcontext).
* When the component is rendered on the client, the component uses the `ClientWeatherForecaster` service implementation, which uses a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) (in the client project's `Program` file) to make a web API call to the server project. A Minimal API endpoint (`/weather-forecast`) defined in the server project's `Program` file transforms the request with the user's access token to obtain the weather data.

For more information on (web) API calls using a service abstractions in Blazor Web Apps, see [Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0#service-abstractions-for-web-api-calls).

## Microsoft Entra ID app registrations

We recommend using separate registrations for apps and web APIs, even when the apps and web APIs are in the same solution. The following guidance is for the `BlazorWebAppOidc` app and `MinimalApiJwt` web API of the sample solution, but the same guidance applies generally to any Entra-based registrations for apps and web APIs.

For app and web API registration guidance, see [Register an application in Microsoft Entra ID](/en-us/entra/identity-platform/quickstart-register-app).

Register the web API (`MinimalApiJwt`) first so that you can then grant access to the web API when registering the app. The web API's tenant ID and client ID are used to configure the web API in its `Program` file. After registering the web API, expose the web API in **App registrations** > **Expose an API** with a scope name of `Weather.Get`. Record the App ID URI for use in the app's configuration.

Next, register the app (`BlazorWebAppOidc`/`BlazorWebAppOidc.Client`) with a **Web** platform configuration and a **Redirect URI** of `https://localhost/signin-oidc` (a port isn't required). The app's tenant ID and client ID, along with the web API's base address, App ID URI, and weather scope name, are used to configure the app in its `Program` file. Grant API permission to access the web API in **App registrations** > **API permissions**. If the app's security specification calls for it, you can grant admin consent for the organization to access the web API. Authorized users and groups are assigned to the app's registration in **App registrations** > **Enterprise applications**.

In the Entra or Azure portal's **Implicit grant and hybrid flows** app registration configuration, don't select either checkbox for the authorization endpoint to return **Access tokens** or **ID tokens**. The OpenID Connect handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.

Create a client secret in the app's registration in the Entra or Azure portal (**Manage** > **Certificates & secrets** > **New client secret**). Hold on to the client secret **Value** for use the next section.

Additional Entra configuration guidance for specific settings is provided later in this article.

## Establish the client secret

*This section only applies to the server project of the Blazor Web App (`BlazorWebAppOidc` project).*

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](./?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

For local development testing, use the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) to store the Blazor server project's client secret under the configuration key `Authentication:Schemes:MicrosoftOidc:ClientSecret`.

The Blazor server project hasn't been initialized for the Secret Manager tool. Use a command shell, such as the Developer PowerShell command shell in Visual Studio, to execute the following command. Before executing the command, change the directory with the `cd` command to the server project's directory. The command establishes a user secrets identifier (`<UserSecretsId>` in the server app's project file):

```
dotnet user-secrets init
```

Execute the following command to set the client secret. The `{SECRET}` placeholder is the client secret obtained from the app's registration:

```
dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"
```

If using Visual Studio, you can confirm the secret is set by right-clicking the server project in **Solution Explorer** and selecting **Manage User Secrets**.

## Aspire projects

For more information on using Aspire and details on the `.AppHost` and `.ServiceDefaults` projects of the sample app, see the [Aspire documentation](/en-us/dotnet/aspire/).

Confirm that you've met the prerequisites for Aspire. For more information, see the *Prerequisites* section of [Quickstart: Build your first Aspire solution](/en-us/dotnet/aspire/get-started/build-your-first-aspire-app?tabs=visual-studio#prerequisites).

The sample app only configures an insecure HTTP launch profile (`http`) for use during development testing.

## `MinimalApiJwt` project

The `MinimalApiJwt` project is a backend web API for multiple frontend projects. The project configures a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data. Requests from the Blazor Web App server-side project (`BlazorWebAppOidc`) are proxied to the `MinimalApiJwt` project.

The `MinimalApiJwt.http` file can be used for testing the weather data request. Note that the `MinimalApiJwt` project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see [Use .http files in Visual Studio 2022](../../test/http-files?view=aspnetcore-10.0).

The project includes packages and configuration to produce [OpenAPI documents](../../fundamentals/openapi/overview?view=aspnetcore-10.0).

The project includes packages and configuration to produce [OpenAPI documents](../../fundamentals/openapi/overview?view=aspnetcore-10.0) and the [Swagger UI](https://swagger.io/api-hub/) in the `Development` environment. For more information, see [Use the generated OpenAPI documents](../../fundamentals/openapi/using-openapi-documents?view=aspnetcore-10.0#use-swagger-ui-for-local-ad-hoc-testing).

A secure weather forecast data endpoint is in the project's `Program` file:

```
app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();
```

The [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) extension method requires authorization for the route definition. For any controllers that you add to the project, add the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the controller or action.

Configure the project in the [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions) of the [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) call in the project's `Program` file.

The [Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.authority) sets the Authority for making OIDC calls. We recommend using a separate app registration for the `MinimalApiJwt` project. The authority matches the issurer (`iss`) of the JWT returned by the identity provider.

```
jwtOptions.Authority = "{AUTHORITY}";
```

The format of the Authority depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee`.

ME-ID tenant Authority example:

```
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
```

The preceding example uses the V1 STS token URL format. For guidance on V2 STS tokens, see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0#sts-token-version).

AAD B2C tenant Authority example:

```
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
```

The [Audience](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.audience) sets the Audience for any received OIDC token.

```
jwtOptions.Audience = "{APP ID URI}";
```

Note

When using Microsoft Entra ID, match the value to just the path of the **Application ID URI** configured when adding the `Weather.Get` scope under **Expose an API** in the Entra or Azure portal. Don't include the scope name, "`Weather.Get`," in the value.

The format of the Audience depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of `contoso` and a Client ID of `11112222-bbbb-3333-cccc-4444dddd5555`.

ME-ID tenant App ID URI example:

```
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";
```

AAD B2C tenant App ID URI example:

```
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";
```

## Server-side Blazor Web App project (`BlazorWebAppOidc`)

This section explains how to configure the server-side Blazor project.

The following [OpenIdConnectOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions) configuration is found in the project's `Program` file on the call to [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect).

[PushedAuthorizationBehavior](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.pushedauthorizationbehavior): Controls [Pushed Authorization Requests (PAR) support](../../release-notes/aspnetcore-9.0?view=aspnetcore-10.0#openidconnecthandler-adds-support-for-pushed-authorization-requests-par). By default, the setting is to use PAR if the identity provider's discovery document (usually found at `.well-known/openid-configuration`) advertises support for PAR. If you wish to require PAR support for the app, you can assign a value of [`PushedAuthorizationBehavior.Require`](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.pushedauthorizationbehavior). PAR isn't supported by Microsoft Entra, and there are no plans for Entra to ever support it in the future.

```
oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;
```

[SignInScheme](/en-us/dotnet/api/microsoft.aspnetcore.builder.remoteauthenticationoptions.signinscheme): Sets the authentication scheme corresponding to the middleware responsible of persisting user's identity after a successful authentication. The OIDC handler needs to use a sign-in scheme that's capable of persisting user credentials across requests. The following line is present merely for demonstration purposes. If omitted, [DefaultSignInScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultsigninscheme) is used as a fallback value.

```
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
```

Scopes for `openid` and `profile` ([Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.scope)) (Optional): The `openid` and `profile` scopes are also configured by default because they're required for the OIDC handler to work, but these may need to be re-added if scopes are included in the `Authentication:Schemes:MicrosoftOidc:Scope` configuration. For general configuration guidance, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0) and [ASP.NET Core Blazor configuration](../fundamentals/configuration?view=aspnetcore-10.0).

```
oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
```

[SaveTokens](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens): Defines whether access and refresh tokens should be stored in the [AuthenticationProperties](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties) after a successful authorization. This property is set to `true` so the refresh token gets stored for non-interactive token refresh.

```
oidcOptions.SaveTokens = true;
```

Scope for offline access ([Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.scope)): The `offline_access` scope is required for the refresh token.

```
oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
```

Scopes for obtaining weather data from the web API ([Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.scope)): Configure the `Weather.Get` scope for accessing the external web API for weather data. In the following example, the `{APP ID URI}` placeholder is found in the Entra or Azure portal where the web API is exposed. For any other identity provider, use the appropriate scope.

```
oidcOptions.Scope.Add("{APP ID URI}/Weather.Get");
```

The format of the scope depends on the type of tenant in use. In the following examples, the Tenant Domain is `contoso.onmicrosoft.com`, and the Client ID is `11112222-bbbb-3333-cccc-4444dddd5555`.

ME-ID tenant App ID URI example:

```
oidcOptions.Scope.Add("api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");
```

AAD B2C tenant App ID URI example:

```
oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");
```

[Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.authority) and [ClientId](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.clientid): Sets the Authority and Client ID for OIDC calls.

```
oidcOptions.Authority = "{AUTHORITY}";
oidcOptions.ClientId = "{CLIENT ID}";
```

The following example uses a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee` and a Client ID of `00001111-aaaa-2222-bbbb-3333cccc4444`:

```
oidcOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
oidcOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
```

For multi-tenant apps, the "common" authority should be used. You can also use the "common" authority for single-tenant apps, but a custom [IssuerValidator](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.issuervalidator) is required, as shown later in this section.

```
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0";
```

[ResponseType](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.responsetype): Configures the OIDC handler to only perform authorization code flow. Implicit grants and hybrid flows are unnecessary in this mode. The OIDC handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.

```
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
```

[MapInboundClaims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.mapinboundclaims) and configuration of [NameClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.nameclaimtype) and [RoleClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.roleclaimtype): Many OIDC servers use "`name`" and "`role`" rather than the SOAP/WS-Fed defaults in [ClaimTypes](/en-us/dotnet/api/system.security.claims.claimtypes). When [MapInboundClaims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.mapinboundclaims) is set to `false`, the handler doesn't perform claims mappings, and the claim names from the JWT are used directly by the app. The following example sets the role claim type to "`roles`," which is appropriate for [Microsoft Entra ID (ME-ID)](https://www.microsoft.com/security/business/microsoft-entra). Consult your identity provider's documentation for more information.

Note

[MapInboundClaims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.mapinboundclaims) must be set to `false` for most OIDC providers, which prevents renaming claims.

```
oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";
```

Path configuration: Paths must match the redirect URI (login callback path) and post logout redirect (signed-out callback path) paths configured when registering the application with the OIDC provider. In the Azure portal, paths are configured in the **Authentication** blade of the app's registration. Both the sign-in and sign-out paths must be registered as redirect URIs. The default values are `/signin-oidc` and `/signout-callback-oidc`.

Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost:{PORT}/signin-oidc

Note

A port isn't required for `localhost` addresses when using Microsoft Entra ID. Most other OIDC providers require the correct port.

[SignedOutCallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions.signedoutcallbackpath) (configuration key: "`SignedOutCallbackPath`"): The request path within the app's base path intercepted by the OIDC handler where the user agent is first returned after signing out from the identity provider. The sample app doesn't set a value for the path because the default value of "`/signout-callback-oidc`" is used. After intercepting the request, the OIDC handler redirects to the [SignedOutRedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutredirecturi) or [RedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.redirecturi), if specified.

Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost:{PORT}/signout-callback-oidc

Note

When using Microsoft Entra ID, set the path in the **Web** platform configuration's **Redirect URI** entries in the Entra or Azure portal. A port isn't required for `localhost` addresses when using Entra. Most other OIDC providers require the correct port. If you don't add the signed-out callback path URI to the app's registration in Entra, Entra refuses to redirect the user back to the app and merely asks them to close their browser window.

[RemoteSignOutPath](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions.remotesignoutpath): Requests received on this path cause the handler to invoke sign-out using the sign-out scheme.

In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost/signout-oidc

Note

When using Microsoft Entra ID, set the **Front-channel logout URL** in the Entra or Azure portal. A port isn't required for `localhost` addresses when using Entra. Most other OIDC providers require the correct port.

```
oidcOptions.CallbackPath = new PathString("{PATH}");
oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
oidcOptions.RemoteSignOutPath = new PathString("{PATH}");
```

Examples (default values):

```
oidcOptions.CallbackPath = new PathString("/signin-oidc");
oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");
```

(*Microsoft Azure only with the "common" endpoint*) [TokenValidationParameters.IssuerValidator](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.issuervalidator): Many OIDC providers work with the default issuer validator, but we need to account for the issuer parameterized with the Tenant ID (`{TENANT ID}`) returned by `https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration`. For more information, see [SecurityTokenInvalidIssuerException with OpenID Connect and the Azure AD "common" endpoint (`AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet` #1731)](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1731).

Only for apps using Microsoft Entra ID with the "common" endpoint:

```
var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;
```

## Client-side Blazor Web App project (`BlazorWebAppOidc.Client`)

The `BlazorWebAppOidc.Client` project is the client-side project of the Blazor Web App.

The client calls [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization) to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application.

The `PersistentAuthenticationStateProvider` class (`PersistentAuthenticationStateProvider.cs`) is a client-side [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) that determines the user's authentication state by looking for data persisted in the page when it was rendered on the server. The authentication state is fixed for the lifetime of the WebAssembly application.

If the user needs to log in or out, a full page reload is required.

The sample app only provides a user name and email for display purposes.

For Microsoft Entra ID, you can use [AddMicrosoftIdentityWebApp](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapp) from [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) ([`Microsoft.Identity.Web` NuGet package](https://www.nuget.org/packages/Microsoft.Identity.Web), [API documentation](/en-us/dotnet/api/microsoft.identity.web)), which adds both the OIDC and Cookie authentication handlers with the appropriate defaults. The sample app and the guidance in this article don't use Microsoft Identity Web. The guidance demonstrates how to configure the OIDC handler *manually* for any OIDC provider. For more information on implementing Microsoft Identity Web, see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0).

This version of the article covers implementing OIDC with the [Backend for Frontend (BFF) pattern](/en-us/azure/architecture/patterns/backends-for-frontends) without [YARP](https://dotnet.github.io/yarp/) and [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview). Change the article version selector to **With YARP and Aspire (Interactive Auto)** if the app's specification calls for adopting YARP and Aspire.

The following specification is adopted:

* The Blazor Web App uses [the Auto render mode with global interactivity](../components/render-modes?view=aspnetcore-10.0).
* Custom auth state provider services are used by the server and client apps to capture the user's authentication state and flow it between the server and client.
* This app is a starting point for any OIDC authentication flow. OIDC is configured manually in the app and doesn't rely upon [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra) or [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) packages, nor does the sample app require [Microsoft Azure](https://azure.microsoft.com/) hosting. However, the sample app can be used with Entra, Microsoft Identity Web, and hosted in Azure.
* Automatic non-interactive token refresh.
* A separate web API project demonstrates a secure web API call for weather data.

For an alternative experience using [Microsoft Authentication Library for .NET](/en-us/entra/msal/dotnet/), [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/), and [Microsoft Entra ID](https://www.microsoft.com/security/business/identity-access/microsoft-entra-id), see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0).

## Sample solution

The sample app consists of the following projects:

* `BlazorWebAppOidc`: Server-side project of the Blazor Web App, containing an example [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.
* `BlazorWebAppOidc.Client`: Client-side project of the Blazor Web App.
* `MinimalApiJwt`: Backend web API with a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.

Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the `BlazorWebAppOidcBffAuto` folder for .NET 8 or later.

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

Sample solution features:

* Automatic non-interactive token refresh with the help of a custom cookie refresher (`CookieOidcRefresher.cs`).

* Weather data is handled by a Minimal API endpoint (`/weather-forecast`) in the `Program` file (`Program.cs`) of the `MinimalApiJwt` project. The endpoint requires authorization by calling [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization). For any controllers that you add to the project, add the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to the controller or action. For more information on requiring authorization across the app via an [authorization policy](../../security/authorization/policies?view=aspnetcore-10.0) and opting out of authorization at a subset of public endpoints, see the [Razor Pages OIDC guidance](../../security/authentication/configure-oidc-web-authentication?view=aspnetcore-10.0#force-authorization).

* The app securely calls a web API for weather data:

  * When rendering the `Weather` component on the server, the component uses the `ServerWeatherForecaster` on the server to obtain weather data from the web API in the `MinimalApiJwt` project using a [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) (`TokenHandler`) that attaches the access token from the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) to the request. For more information on [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) instances, see [HTTP requests with IHttpClientFactory - ASP.NET Core](../../fundamentals/http-requests?view=aspnetcore-10.0#outgoing-request-middleware).
  * When the component is rendered on the client, the component uses the `ClientWeatherForecaster` service implementation, which uses a preconfigured [HttpClient](/en-us/dotnet/api/system.net.http.httpclient) (in the client project's `Program` file) to make the web API call from the server project's `ServerWeatherForecaster`.

- The server project calls [AddAuthenticationStateSerialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyrazorcomponentsbuilderextensions.addauthenticationstateserialization) to add a server-side authentication state provider that uses [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) to flow the authentication state to the client. The client calls [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization) to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application.

* The `PersistingAuthenticationStateProvider` class (`PersistingAuthenticationStateProvider.cs`) is a server-side [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) that uses [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) to flow the authentication state to the client, which is then fixed for the lifetime of the WebAssembly application.

For more information on (web) API calls using a service abstractions in Blazor Web Apps, see [Call a web API from an ASP.NET Core Blazor app](../call-web-api?view=aspnetcore-10.0#service-abstractions-for-web-api-calls).

## OIDC provider terminology and guidance

Although you aren't required to adopt [Microsoft Entra (ME-ID)](https://www.microsoft.com/security/business/microsoft-entra) as the OIDC provider to use the sample app and the guidance in this article, this article describes settings for ME-ID using names that are found in Microsoft documentation and the Azure/Entra portals. OIDC settings have similar naming across OIDC providers. When using a third-party OIDC provider, use the provider's documentation in conjunction with the guidance in this article for app and web API registrations.

## Microsoft Entra ID app registrations

We recommend using separate registrations for apps and web APIs, even when the apps and web APIs are in the same solution. The following guidance is for the `BlazorWebAppOidc` app and `MinimalApiJwt` web API of the sample solution, but the same guidance applies generally to any Entra-based registrations for apps and web APIs.

For app and web API registration guidance, see [Register an application in Microsoft Entra ID](/en-us/entra/identity-platform/quickstart-register-app).

Register the web API (`MinimalApiJwt`) first so that you can then grant access to the web API when registering the app. The web API's tenant ID and client ID are used to configure the web API in its `Program` file. After registering the web API, expose the web API in **App registrations** > **Expose an API** with a scope name of `Weather.Get`. Record the App ID URI for use in the app's configuration.

Next, register the app (`BlazorWebAppOidc`/`BlazorWebAppOidc.Client`) with a **Web** platform configuration and a **Redirect URI** of `https://localhost/signin-oidc` (a port isn't required). The app's tenant ID and client ID, along with the web API's base address, App ID URI, and weather scope name, are used to configure the app in its `Program` file. Grant API permission to access the web API in **App registrations** > **API permissions**. If the app's security specification calls for it, you can grant admin consent for the organization to access the web API. Authorized users and groups are assigned to the app's registration in **App registrations** > **Enterprise applications**.

In the Entra or Azure portal's **Implicit grant and hybrid flows** app registration configuration, don't select either checkbox for the authorization endpoint to return **Access tokens** or **ID tokens**. The OpenID Connect handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.

Create a client secret in the app's registration in the Entra or Azure portal (**Manage** > **Certificates & secrets** > **New client secret**). Hold on to the client secret **Value** for use the next section.

Additional Entra configuration guidance for specific settings is provided later in this article.

## Establish the client secret

*This section only applies to the server project of the Blazor Web App (`BlazorWebAppOidc` project).*

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](./?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

For local development testing, use the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) to store the Blazor server project's client secret under the configuration key `Authentication:Schemes:MicrosoftOidc:ClientSecret`.

The Blazor server project hasn't been initialized for the Secret Manager tool. Use a command shell, such as the Developer PowerShell command shell in Visual Studio, to execute the following command. Before executing the command, change the directory with the `cd` command to the server project's directory. The command establishes a user secrets identifier (`<UserSecretsId>` in the server app's project file):

```
dotnet user-secrets init
```

Execute the following command to set the client secret. The `{SECRET}` placeholder is the client secret obtained from the app's registration:

```
dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"
```

If using Visual Studio, you can confirm the secret is set by right-clicking the server project in **Solution Explorer** and selecting **Manage User Secrets**.

## `MinimalApiJwt` project

The `MinimalApiJwt` project is a backend web API for multiple frontend projects. The project configures a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.

The `MinimalApiJwt.http` file can be used for testing the weather data request. Note that the `MinimalApiJwt` project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see [Use .http files in Visual Studio 2022](../../test/http-files?view=aspnetcore-10.0).

The project includes packages and configuration to produce [OpenAPI documents](../../fundamentals/openapi/overview?view=aspnetcore-10.0).

The project includes packages and configuration to produce [OpenAPI documents](../../fundamentals/openapi/overview?view=aspnetcore-10.0) and the [Swagger UI](https://swagger.io/api-hub/) in the `Development` environment. For more information, see [Use the generated OpenAPI documents](../../fundamentals/openapi/using-openapi-documents?view=aspnetcore-10.0#use-swagger-ui-for-local-ad-hoc-testing).

The project creates a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data:

```
app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();
```

Configure the project in the [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions) of the [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) call in the project's `Program` file.

The [Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.authority) sets the Authority for making OIDC calls. We recommend using a separate app registration for the `MinimalApiJwt` project. The authority matches the issurer (`iss`) of the JWT returned by the identity provider.

```
jwtOptions.Authority = "{AUTHORITY}";
```

The format of the Authority depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee`.

ME-ID tenant Authority example:

```
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
```

The preceding example uses the V1 STS token URL format. For guidance on V2 STS tokens, see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0#sts-token-version).

AAD B2C tenant Authority example:

```
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
```

The [Audience](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.audience) sets the Audience for any received OIDC token.

```
jwtOptions.Audience = "{APP ID URI}";
```

Note

When using Microsoft Entra ID, match the value to just the path of the **Application ID URI** configured when adding the `Weather.Get` scope under **Expose an API** in the Entra or Azure portal. Don't include the scope name, "`Weather.Get`," in the value.

The format of the Audience depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of `contoso` and a Client ID of `11112222-bbbb-3333-cccc-4444dddd5555`.

ME-ID tenant App ID URI example:

```
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";
```

AAD B2C tenant App ID URI example:

```
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";
```

## Blazor Web App server project (`BlazorWebAppOidc`)

The `BlazorWebAppOidc` project is the server-side project of the Blazor Web App.

A [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) (`TokenHandler`) manages attaching a user's access token to outgoing requests. The token handler only executes during static server-side rendering (static SSR), so using [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is safe in this scenario. For more information, see [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../components/httpcontext?view=aspnetcore-10.0) and [ASP.NET Core Blazor additional server-side security scenarios](additional-scenarios?view=aspnetcore-10.0#use-a-token-handler-for-web-api-calls).

`TokenHandler.cs`:

```
public class TokenHandler(IHttpContextAccessor httpContextAccessor) : 
    DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (httpContextAccessor.HttpContext is null)
        {
            throw new Exception("HttpContext not available");
        }

        var accessToken = await httpContextAccessor.HttpContext
            .GetTokenAsync("access_token");

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);

        return await base.SendAsync(request, cancellationToken);
    }
}
```

In the project's `Program` file, the token handler (`TokenHandler`) is registered as a service and specified as the message handler with [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler) for making secure requests to the backend `MinimalApiJwt` web API using a [named HTTP client](../call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory) ("`ExternalApi`").

```
builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("ExternalApi",
      client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ?? 
          throw new Exception("Missing base address!")))
      .AddHttpMessageHandler<TokenHandler>();
```

In the project's `appsettings.json` file, configure the external API URI:

```
"ExternalApiUri": "{BASE ADDRESS}"
```

Example:

```
"ExternalApiUri": "https://localhost:7277"
```

The following [OpenIdConnectOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions) configuration is found in the project's `Program` file on the call to [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect):

[PushedAuthorizationBehavior](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.pushedauthorizationbehavior): Controls [Pushed Authorization Requests (PAR) support](../../release-notes/aspnetcore-9.0?view=aspnetcore-10.0#openidconnecthandler-adds-support-for-pushed-authorization-requests-par). By default, the setting is to use PAR if the identity provider's discovery document (usually found at `.well-known/openid-configuration`) advertises support for PAR. If you wish to require PAR support for the app, you can assign a value of [`PushedAuthorizationBehavior.Require`](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.pushedauthorizationbehavior). PAR isn't supported by Microsoft Entra, and there are no plans for Entra to ever support it in the future.

```
oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;
```

[SignInScheme](/en-us/dotnet/api/microsoft.aspnetcore.builder.remoteauthenticationoptions.signinscheme): Sets the authentication scheme corresponding to the middleware responsible of persisting user's identity after a successful authentication. The OIDC handler needs to use a sign-in scheme that's capable of persisting user credentials across requests. The following line is present merely for demonstration purposes. If omitted, [DefaultSignInScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultsigninscheme) is used as a fallback value.

```
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
```

Scopes for `openid` and `profile` ([Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.scope)) (Optional): The `openid` and `profile` scopes are also configured by default because they're required for the OIDC handler to work, but these may need to be re-added if scopes are included in the `Authentication:Schemes:MicrosoftOidc:Scope` configuration. For general configuration guidance, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0) and [ASP.NET Core Blazor configuration](../fundamentals/configuration?view=aspnetcore-10.0).

```
oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
```

[SaveTokens](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens): Defines whether access and refresh tokens should be stored in the [AuthenticationProperties](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties) after a successful authorization. This property is set to `true` so the refresh token gets stored for non-interactive token refresh.

```
oidcOptions.SaveTokens = true;
```

Scope for offline access ([Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.scope)): The `offline_access` scope is required for the refresh token.

```
oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
```

[Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.authority) and [ClientId](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.clientid): Sets the Authority and Client ID for OIDC calls.

```
oidcOptions.Authority = "{AUTHORITY}";
oidcOptions.ClientId = "{CLIENT ID}";
```

The following example uses a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee` and a Client ID of `00001111-aaaa-2222-bbbb-3333cccc4444`:

```
oidcOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
oidcOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
```

For multi-tenant apps, the "common" authority should be used. You can also use the "common" authority for single-tenant apps, but a custom [IssuerValidator](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.issuervalidator) is required, as shown later in this section.

```
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0";
```

[ResponseType](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.responsetype): Configures the OIDC handler to only perform authorization code flow. Implicit grants and hybrid flows are unnecessary in this mode. The OIDC handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.

```
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
```

[MapInboundClaims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.mapinboundclaims) and configuration of [NameClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.nameclaimtype) and [RoleClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.roleclaimtype): Many OIDC servers use "`name`" and "`role`" rather than the SOAP/WS-Fed defaults in [ClaimTypes](/en-us/dotnet/api/system.security.claims.claimtypes). When [MapInboundClaims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.mapinboundclaims) is set to `false`, the handler doesn't perform claims mappings, and the claim names from the JWT are used directly by the app. The following example sets the role claim type to "`roles`," which is appropriate for [Microsoft Entra ID (ME-ID)](https://www.microsoft.com/security/business/microsoft-entra). Consult your identity provider's documentation for more information.

Note

[MapInboundClaims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.mapinboundclaims) must be set to `false` for most OIDC providers, which prevents renaming claims.

```
oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";
```

Path configuration: Paths must match the redirect URI (login callback path) and post logout redirect (signed-out callback path) paths configured when registering the application with the OIDC provider. In the Azure portal, paths are configured in the **Authentication** blade of the app's registration. Both the sign-in and sign-out paths must be registered as redirect URIs. The default values are `/signin-oidc` and `/signout-callback-oidc`.

[CallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.builder.remoteauthenticationoptions.callbackpath#microsoft-aspnetcore-builder-remoteauthenticationoptions-callbackpath): The request path within the app's base path where the user-agent is returned.

Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost:{PORT}/signin-oidc

Note

A port isn't required for `localhost` addresses when using Microsoft Entra ID. Most other OIDC providers require the correct port.

[SignedOutCallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutcallbackpath) (configuration key: "`SignedOutCallbackPath`"): The request path within the app's base path intercepted by the OIDC handler where the user agent is first returned after signing out from the identity provider. The sample app doesn't set a value for the path because the default value of "`/signout-callback-oidc`" is used. After intercepting the request, the OIDC handler redirects to the [SignedOutRedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutredirecturi) or [RedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.redirecturi), if specified.

Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost:{PORT}/signout-callback-oidc

Note

When using Microsoft Entra ID, set the path in the **Web** platform configuration's **Redirect URI** entries in the Entra or Azure portal. A port isn't required for `localhost` addresses when using Entra. Most other OIDC providers require the correct port. If you don't add the signed-out callback path URI to the app's registration in Entra, Entra refuses to redirect the user back to the app and merely asks them to close their browser window.

[RemoteSignOutPath](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions.remotesignoutpath): Requests received on this path cause the handler to invoke sign-out using the sign-out scheme.

In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost/signout-oidc

Note

When using Microsoft Entra ID, set the **Front-channel logout URL** in the Entra or Azure portal. A port isn't required for `localhost` addresses when using Entra. Most other OIDC providers require the correct port.

```
oidcOptions.CallbackPath = new PathString("{PATH}");
oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
oidcOptions.RemoteSignOutPath = new PathString("{PATH}");
```

Examples (default values):

```
oidcOptions.CallbackPath = new PathString("/signin-oidc");
oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");
```

(*Microsoft Azure only with the "common" endpoint*) [TokenValidationParameters.IssuerValidator](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.issuervalidator): Many OIDC providers work with the default issuer validator, but we need to account for the issuer parameterized with the Tenant ID (`{TENANT ID}`) returned by `https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration`. For more information, see [SecurityTokenInvalidIssuerException with OpenID Connect and the Azure AD "common" endpoint (`AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet` #1731)](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1731).

Only for apps using Microsoft Entra ID with the "common" endpoint:

```
var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;
```

## Blazor Web App client project (`BlazorWebAppOidc.Client`)

The `BlazorWebAppOidc.Client` project is the client-side project of the Blazor Web App.

The client calls [AddAuthenticationStateDeserialization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.webassemblyauthenticationservicecollectionextensions.addauthenticationstatedeserialization) to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application.

The `PersistentAuthenticationStateProvider` class (`PersistentAuthenticationStateProvider.cs`) is a client-side [AuthenticationStateProvider](/en-us/dotnet/api/microsoft.aspnetcore.components.authorization.authenticationstateprovider) that determines the user's authentication state by looking for data persisted in the page when it was rendered on the server. The authentication state is fixed for the lifetime of the WebAssembly application.

If the user needs to log in or out, a full page reload is required.

The sample app only provides a user name and email for display purposes.

For Microsoft Entra ID or Azure AD B2C, you can use [AddMicrosoftIdentityWebApp](/en-us/dotnet/api/microsoft.identity.web.appbuilderextension.addmicrosoftidentitywebapp) from [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) ([`Microsoft.Identity.Web` NuGet package](https://www.nuget.org/packages/Microsoft.Identity.Web), [API documentation](/en-us/dotnet/api/microsoft.identity.web)), which adds both the OIDC and Cookie authentication handlers with the appropriate defaults. The sample app and the guidance in this article don't use Microsoft Identity Web. The guidance demonstrates how to configure the OIDC handler *manually* for any OIDC provider. For more information on implementing Microsoft Identity Web, see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0).

This version of the article covers implementing OIDC with the [Backend for Frontend (BFF) pattern](/en-us/azure/architecture/patterns/backends-for-frontends) without [YARP](https://dotnet.github.io/yarp/) and [Aspire](/en-us/dotnet/aspire/get-started/aspire-overview). Change the article version selector to **With YARP and Aspire (Interactive Auto)** if the app's specification calls for adopting YARP and Aspire.

The following specification is adopted:

* The Blazor Web App uses [the Server render mode with global interactivity](../components/render-modes?view=aspnetcore-10.0).
* This app is a starting point for any OIDC authentication flow. OIDC is configured manually in the app and doesn't rely upon [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra) or [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/) packages, nor does the sample app require [Microsoft Azure](https://azure.microsoft.com/) hosting. However, the sample app can be used with Entra, Microsoft Identity Web, and hosted in Azure.
* Automatic non-interactive token refresh.
* A separate web API project demonstrates a secure web API call for weather data.

For an alternative experience using [Microsoft Authentication Library for .NET](/en-us/entra/msal/dotnet/), [Microsoft Identity Web](/en-us/entra/msal/dotnet/microsoft-identity-web/), and [Microsoft Entra ID](https://www.microsoft.com/security/business/identity-access/microsoft-entra-id), see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0).

## Sample solution

The sample app consists of the following projects:

* `BlazorWebAppOidcServer`: Blazor Web App server-side project (global Interactive Server rendering).
* `MinimalApiJwt`: Backend web API with a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.

Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the `BlazorWebAppOidcBffServer` folder for .NET 8 or later.

[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

## Microsoft Entra ID app registrations

We recommend using separate registrations for apps and web APIs, even when the apps and web APIs are in the same solution. The following guidance is for the `BlazorWebAppOidcServer` app and `MinimalApiJwt` web API of the sample solution, but the same guidance applies generally to any Entra-based registrations for apps and web APIs.

Register the web API (`MinimalApiJwt`) first so that you can then grant access to the web API when registering the app. The web API's tenant ID and client ID are used to configure the web API in its `Program` file. After registering the web API, expose the web API in **App registrations** > **Expose an API** with a scope name of `Weather.Get`. Record the App ID URI for use in the app's configuration.

Next, register the app (`BlazorWebAppOidcServer`) with a **Web** platform configuration and a **Redirect URI** of `https://localhost/signin-oidc` (a port isn't required). The app's tenant ID and client ID, along with the web API's base address, App ID URI, and weather scope name, are used to configure the app in its `Program` file. Grant API permission to access the web API in **App registrations** > **API permissions**. If the app's security specification calls for it, you can grant admin consent for the organization to access the web API. Authorized users and groups are assigned to the app's registration in **App registrations** > **Enterprise applications**.

In the Entra or Azure portal's **Implicit grant and hybrid flows** app registration configuration, don't select either checkbox for the authorization endpoint to return **Access tokens** or **ID tokens**. The OpenID Connect handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.

Create a client secret in the app's registration in the Entra or Azure portal (**Manage** > **Certificates & secrets** > **New client secret**). Hold on to the client secret **Value** for use the next section.

Additional Entra configuration guidance for specific settings is provided later in this article.

## Establish the client secret

*This section only applies to the server project of the Blazor Web App (`BlazorWebAppOidcServer` project).*

Warning

Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is ***always insecure***. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) is recommended for securing sensitive data. For more information, see [Securely maintain sensitive data and credentials](./?view=aspnetcore-10.0#securely-maintain-sensitive-data-and-credentials).

For local development testing, use the [Secret Manager tool](../../security/app-secrets?view=aspnetcore-10.0) to store the Blazor server project's client secret under the configuration key `Authentication:Schemes:MicrosoftOidc:ClientSecret`.

The Blazor server project hasn't been initialized for the Secret Manager tool. Use a command shell, such as the Developer PowerShell command shell in Visual Studio, to execute the following command. Before executing the command, change the directory with the `cd` command to the server project's directory. The command establishes a user secrets identifier (`<UserSecretsId>` in the app's project file):

```
dotnet user-secrets init
```

Execute the following command to set the client secret. The `{SECRET}` placeholder is the client secret obtained from the app's registration:

```
dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"
```

If using Visual Studio, you can confirm the secret is set by right-clicking the project in **Solution Explorer** and selecting **Manage User Secrets**.

## `MinimalApiJwt` project

The `MinimalApiJwt` project is a backend web API for multiple frontend projects. The project configures a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data.

The `MinimalApiJwt.http` file can be used for testing the weather data request. Note that the `MinimalApiJwt` project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see [Use .http files in Visual Studio 2022](../../test/http-files?view=aspnetcore-10.0).

The project includes packages and configuration to produce [OpenAPI documents](../../fundamentals/openapi/overview?view=aspnetcore-10.0).

The project includes packages and configuration to produce [OpenAPI documents](../../fundamentals/openapi/overview?view=aspnetcore-10.0) and the [Swagger UI](https://swagger.io/api-hub/) in the `Development` environment. For more information, see [Use the generated OpenAPI documents](../../fundamentals/openapi/using-openapi-documents?view=aspnetcore-10.0#use-swagger-ui-for-local-ad-hoc-testing).

The project creates a [Minimal API](../../fundamentals/minimal-apis?view=aspnetcore-10.0) endpoint for weather data:

```
app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();
```

Configure the project in the [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions) of the [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) call in the project's `Program` file.

The [Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.authority) sets the Authority for making OIDC calls. We recommend using a separate app registration for the `MinimalApiJwt` project. The authority matches the issurer (`iss`) of the JWT returned by the identity provider.

```
jwtOptions.Authority = "{AUTHORITY}";
```

The format of the Authority depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee`.

ME-ID tenant Authority example:

```
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
```

The preceding example uses the V1 STS token URL format. For guidance on V2 STS tokens, see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0#sts-token-version).

AAD B2C tenant Authority example:

```
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
```

The [Audience](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.audience) sets the Audience for any received OIDC token.

```
jwtOptions.Audience = "{APP ID URI}";
```

Note

When using Microsoft Entra ID, match the value to just the path of the **Application ID URI** configured when adding the `Weather.Get` scope under **Expose an API** in the Entra or Azure portal. Don't include the scope name, "`Weather.Get`," in the value.

The format of the Audience depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of `contoso` and a Client ID of `11112222-bbbb-3333-cccc-4444dddd5555`.

ME-ID tenant App ID URI example:

```
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";
```

AAD B2C tenant App ID URI example:

```
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";
```

## `BlazorWebAppOidcServer` project

Automatic non-interactive token refresh is managed by a custom cookie refresher (`CookieOidcRefresher.cs`).

A [DelegatingHandler](/en-us/dotnet/api/system.net.http.delegatinghandler) (`TokenHandler`) manages attaching a user's access token to outgoing requests. The token handler only executes during static server-side rendering (static SSR), so using [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) is safe in this scenario. For more information, see [IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps](../components/httpcontext?view=aspnetcore-10.0) and [ASP.NET Core Blazor additional server-side security scenarios](additional-scenarios?view=aspnetcore-10.0#use-a-token-handler-for-web-api-calls).

`TokenHandler.cs`:

```
public class TokenHandler(IHttpContextAccessor httpContextAccessor) : 
    DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (httpContextAccessor.HttpContext is null)
        {
            throw new Exception("HttpContext not available");
        }

        var accessToken = await httpContextAccessor.HttpContext
            .GetTokenAsync("access_token");

        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);

        return await base.SendAsync(request, cancellationToken);
    }
}
```

In the project's `Program` file, the token handler (`TokenHandler`) is registered as a service and specified as the message handler with [AddHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addhttpmessagehandler) for making secure requests to the backend `MinimalApiJwt` web API using a [named HTTP client](../call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory) ("`ExternalApi`").

```
builder.Services.AddScoped<TokenHandler>();

builder.Services.AddHttpClient("ExternalApi",
      client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ?? 
          throw new Exception("Missing base address!")))
      .AddHttpMessageHandler<TokenHandler>();
```

The `Weather` component uses the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) to prevent unauthorized access. For more information on requiring authorization across the app via an [authorization policy](../../security/authorization/policies?view=aspnetcore-10.0) and opting out of authorization at a subset of public endpoints, see the [Razor Pages OIDC guidance](../../security/authentication/configure-oidc-web-authentication?view=aspnetcore-10.0#force-authorization).

The `ExternalApi` HTTP client is used to make a request for weather data to the secure web API. In the [`OnInitializedAsync` lifecycle event](../components/lifecycle?view=aspnetcore-10.0#component-initialization-oninitializedasync) of `Weather.razor`:

```
using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
var client = ClientFactory.CreateClient("ExternalApi");
using var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();

forecasts = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
    throw new IOException("No weather forecast!");
```

In the project's `appsettings.json` file, configure the external API URI:

```
"ExternalApiUri": "{BASE ADDRESS}"
```

Example:

```
"ExternalApiUri": "https://localhost:7277"
```

The following [OpenIdConnectOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions) configuration is found in the project's `Program` file on the call to [AddOpenIdConnect](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect):

[PushedAuthorizationBehavior](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.pushedauthorizationbehavior): Controls [Pushed Authorization Requests (PAR) support](../../release-notes/aspnetcore-9.0?view=aspnetcore-10.0#openidconnecthandler-adds-support-for-pushed-authorization-requests-par). By default, the setting is to use PAR if the identity provider's discovery document (usually found at `.well-known/openid-configuration`) advertises support for PAR. If you wish to require PAR support for the app, you can assign a value of [`PushedAuthorizationBehavior.Require`](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.pushedauthorizationbehavior). PAR isn't supported by Microsoft Entra, and there are no plans for Entra to ever support it in the future.

```
oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;
```

[SignInScheme](/en-us/dotnet/api/microsoft.aspnetcore.builder.remoteauthenticationoptions.signinscheme): Sets the authentication scheme corresponding to the middleware responsible of persisting user's identity after a successful authentication. The OIDC handler needs to use a sign-in scheme that's capable of persisting user credentials across requests. The following line is present merely for demonstration purposes. If omitted, [DefaultSignInScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultsigninscheme) is used as a fallback value.

```
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
```

Scopes for `openid` and `profile` ([Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.scope)) (Optional): The `openid` and `profile` scopes are also configured by default because they're required for the OIDC handler to work, but these may need to be re-added if scopes are included in the `Authentication:Schemes:MicrosoftOidc:Scope` configuration. For general configuration guidance, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0) and [ASP.NET Core Blazor configuration](../fundamentals/configuration?view=aspnetcore-10.0).

```
oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
```

Configure the `Weather.Get` scope for accessing the external web API for weather data. The following example is based on using Entra ID in an ME-ID tenant domain. In the following example, the `{APP ID URI}` placeholder is found in the Entra or Azure portal where the web API is exposed. For any other identity provider, use the appropriate scope.

```
oidcOptions.Scope.Add("{APP ID URI}/Weather.Get");
```

The format of the scope depends on the type of tenant in use. In the following examples, the Tenant Domain is `contoso.onmicrosoft.com`, and the Client ID is `11112222-bbbb-3333-cccc-4444dddd5555`.

ME-ID tenant App ID URI example:

```
oidcOptions.Scope.Add("api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");
```

AAD B2C tenant App ID URI example:

```
oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");
```

[SaveTokens](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.savetokens): Defines whether access and refresh tokens should be stored in the [AuthenticationProperties](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties) after a successful authorization. This property is set to `true` so the refresh token gets stored for non-interactive token refresh.

```
oidcOptions.SaveTokens = true;
```

Scope for offline access ([Scope](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.scope)): The `offline_access` scope is required for the refresh token.

```
oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
```

[Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.authority) and [ClientId](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.clientid): Sets the Authority and Client ID for OIDC calls.

```
oidcOptions.Authority = "{AUTHORITY}";
oidcOptions.ClientId = "{CLIENT ID}";
```

The following example uses a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee` and a Client ID of `00001111-aaaa-2222-bbbb-3333cccc4444`:

```
oidcOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
oidcOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
```

For multi-tenant apps, the "common" authority should be used. You can also use the "common" authority for single-tenant apps, but a custom [IssuerValidator](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.issuervalidator) is required, as shown later in this section.

```
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0";
```

[ResponseType](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.responsetype): Configures the OIDC handler to only perform authorization code flow. Implicit grants and hybrid flows are unnecessary in this mode. The OIDC handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.

```
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
```

[MapInboundClaims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.mapinboundclaims) and configuration of [NameClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.nameclaimtype) and [RoleClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.roleclaimtype): Many OIDC servers use "`name`" and "`role`" rather than the SOAP/WS-Fed defaults in [ClaimTypes](/en-us/dotnet/api/system.security.claims.claimtypes). When [MapInboundClaims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.mapinboundclaims) is set to `false`, the handler doesn't perform claims mappings, and the claim names from the JWT are used directly by the app. The following example sets the role claim type to "`roles`," which is appropriate for [Microsoft Entra ID (ME-ID)](https://www.microsoft.com/security/business/microsoft-entra). Consult your identity provider's documentation for more information.

Note

[MapInboundClaims](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.mapinboundclaims) must be set to `false` for most OIDC providers, which prevents renaming claims.

```
oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";
```

Path configuration: Paths must match the redirect URI (login callback path) and post logout redirect (signed-out callback path) paths configured when registering the application with the OIDC provider. In the Azure portal, paths are configured in the **Authentication** blade of the app's registration. Both the sign-in and sign-out paths must be registered as redirect URIs. The default values are `/signin-oidc` and `/signout-callback-oidc`.

[CallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.builder.remoteauthenticationoptions.callbackpath#microsoft-aspnetcore-builder-remoteauthenticationoptions-callbackpath): The request path within the app's base path where the user-agent is returned.

Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost:{PORT}/signin-oidc

Note

A port isn't required for `localhost` addresses when using Microsoft Entra ID. Most other OIDC providers require the correct port.

[SignedOutCallbackPath](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutcallbackpath) (configuration key: "`SignedOutCallbackPath`"): The request path within the app's base path intercepted by the OIDC handler where the user agent is first returned after signing out from the identity provider. The sample app doesn't set a value for the path because the default value of "`/signout-callback-oidc`" is used. After intercepting the request, the OIDC handler redirects to the [SignedOutRedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutredirecturi) or [RedirectUri](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationproperties.redirecturi), if specified.

Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost:{PORT}/signout-callback-oidc

Note

When using Microsoft Entra ID, set the path in the **Web** platform configuration's **Redirect URI** entries in the Entra or Azure portal. A port isn't required for `localhost` addresses when using Entra. Most other OIDC providers require the correct port. If you don't add the signed-out callback path URI to the app's registration in Entra, Entra refuses to redirect the user back to the app and merely asks them to close their browser window.

[RemoteSignOutPath](/en-us/dotnet/api/microsoft.aspnetcore.builder.openidconnectoptions.remotesignoutpath): Requests received on this path cause the handler to invoke sign-out using the sign-out scheme.

In the following example, the `{PORT}` placeholder is the app's port:

> https\://localhost/signout-oidc

Note

When using Microsoft Entra ID, set the **Front-channel logout URL** in the Entra or Azure portal. A port isn't required for `localhost` addresses when using Entra. Most other OIDC providers require the correct port.

```
oidcOptions.CallbackPath = new PathString("{PATH}");
oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
oidcOptions.RemoteSignOutPath = new PathString("{PATH}");
```

Examples (default values):

```
oidcOptions.CallbackPath = new PathString("/signin-oidc");
oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");
```

(*Microsoft Azure only with the "common" endpoint*) [TokenValidationParameters.IssuerValidator](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.issuervalidator): Many OIDC providers work with the default issuer validator, but we need to account for the issuer parameterized with the Tenant ID (`{TENANT ID}`) returned by `https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration`. For more information, see [SecurityTokenInvalidIssuerException with OpenID Connect and the Azure AD "common" endpoint (`AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet` #1731)](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1731).

Only for apps using Microsoft Entra ID with the "common" endpoint:

```
var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;
```

## Only serialize the name and role claims

*This section only applies to the Interactive Auto render mode sample apps.*

In the `Program` file, all claims are serialized by setting [SerializeAllClaims](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.server.authenticationstateserializationoptions.serializeallclaims) to `true`. If you only want the name and role claims serialized for CSR, remove the option or set it to `false`.

## Supply configuration with the JSON configuration provider (app settings)

The [sample solution projects](#sample-solution) configure OIDC and JWT bearer authentication in their `Program` files in order to make configuration settings discoverable using C# autocompletion. Professional apps usually use a *configuration provider* to configure OIDC options, such as the default [JSON configuration provider](../../fundamentals/configuration/?view=aspnetcore-10.0). The JSON configuration provider loads configuration from app settings files `appsettings.json`/`appsettings.{ENVIRONMENT}.json`, where the `{ENVIRONMENT}` placeholder is the app's [runtime environment](../../fundamentals/environments?view=aspnetcore-10.0). Follow the guidance in this section to use app settings files for configuration.

In the app settings file (`appsettings.json`) of the `BlazorWebAppOidc` or `BlazorWebAppOidcServer` project, add the following JSON configuration:

```
"Authentication": {
  "Schemes": {
    "MicrosoftOidc": {
      "Authority": "https://login.microsoftonline.com/{TENANT ID (BLAZOR APP)}/v2.0",
      "ClientId": "{CLIENT ID (BLAZOR APP)}",
      "CallbackPath": "/signin-oidc",
      "SignedOutCallbackPath": "/signout-callback-oidc",
      "RemoteSignOutPath": "/signout-oidc",
      "SignedOutRedirectUri": "/",
      "Scope": [
        "openid",
        "profile",
        "offline_access",
        "{APP ID URI (WEB API)}/Weather.Get"
      ]
    }
  }
},
```

Update the placeholders in the preceding configuration to match the values that the app uses in the `Program` file:

* `{TENANT ID (BLAZOR APP)}`: The Tenant Id of the Blazor app.
* `{CLIENT ID (BLAZOR APP)}`: The Client Id of the Blazor app.
* `{APP ID URI (WEB API)}`: The App ID URI of the web API.

The "common" Authority (`https://login.microsoftonline.com/common/v2.0`) should be used for multi-tenant apps. To use the "common" Authority for single-tenant apps, see the [Use the "common" Authority for single-tenant apps](#use-the-common-authority-for-single-tenant-apps) section.

Update any other values in the preceding configuration to match custom/non-default values used in the `Program` file.

The configuration is automatically picked up by the authentication builder.

Remove the following lines from the `Program` file:

```
- oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
- oidcOptions.Scope.Add("...");
- oidcOptions.CallbackPath = new PathString("...");
- oidcOptions.SignedOutCallbackPath = new PathString("...");
- oidcOptions.RemoteSignOutPath = new PathString("...");
- oidcOptions.Authority = "...";
- oidcOptions.ClientId = "...";
```

In the `ConfigureCookieOidc` method of `CookieOidcServiceCollectionExtensions.cs`, remove the following line:

```
- oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
```

In the `MinimalApiJwt` project, add the following app settings configuration to the `appsettings.json` file:

```
"Authentication": {
  "Schemes": {
    "Bearer": {
      "Authority": "https://sts.windows.net/{TENANT ID (WEB API)}/",
      "ValidAudiences": [ "{APP ID URI (WEB API)}" ]
    }
  }
},
```

The preceding example uses the V1 STS token URL format. For guidance on V2 STS tokens, see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0#sts-token-version).

The preceding example for the ME-ID tenant type uses the V1 STS token URL format. For guidance on V2 STS tokens, see [Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID](blazor-web-app-with-entra?view=aspnetcore-10.0#sts-token-version).

Audience formats adopt the following patterns (`{CLIENT ID}` is the Client Id of the web API; `{DIRECTORY NAME}` is the directory name, for example, `contoso`):

* ME-ID tenant type: `api://{CLIENT ID}`
* Microsoft Entra External ID: `{CLIENT ID}`
* B2C tenant type: `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}`

The configuration is automatically picked up by the JWT bearer authentication builder.

Remove the following lines from the `Program` file:

```
- jwtOptions.Authority = "...";
- jwtOptions.Audience = "...";
```

For more information on configuration, see the following resources:

* [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0)
* [ASP.NET Core Blazor configuration](../fundamentals/configuration?view=aspnetcore-10.0)

## Use the "common" Authority for single-tenant apps

You can use the "common" Authority for single-tenant apps, but you must take the following steps to implement a custom issuer validator.

Add the [`Microsoft.IdentityModel.Validators` NuGet package](https://www.nuget.org/packages/Microsoft.IdentityModel.Validators) to the server project.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

At the top of the `Program` file, make the [Microsoft.IdentityModel.Validators](/en-us/dotnet/api/microsoft.identitymodel.validators) namespace available:

```
using Microsoft.IdentityModel.Validators;
```

Use the following code in the `Program` file where OIDC options are configured:

```
var microsoftIssuerValidator = 
    AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = 
    microsoftIssuerValidator.Validate;
```

For more information, see [SecurityTokenInvalidIssuerException with OpenID Connect and the Azure AD "common" endpoint (`AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet` #1731)](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1731).

## Redirect to the home page on logout

The `LogInOrOut` component (`Layout/LogInOrOut.razor`) sets a hidden field for the return URL (`ReturnUrl`) to the current URL (`currentURL`). When the user signs out of the app, the identity provider returns the user to the page from which they logged out. If the user logs out from a secure page, they're returned to the same secure page and sent back through the authentication process. This authentication flow is reasonable when users need to change accounts regularly.

Alternatively, use the following `LogInOrOut` component, which doesn't supply a return URL when logging out.

`Layout/LogInOrOut.razor`:

```
<div class="nav-item px-3">
    <AuthorizeView>
        <Authorized>
            <form action="authentication/logout" method="post">
                <AntiforgeryToken />
                <button type="submit" class="nav-link">
                    <span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true">
                    </span> Logout
                </button>
            </form>
        </Authorized>
        <NotAuthorized>
            <a class="nav-link" href="authentication/login">
                <span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> 
                Login
            </a>
        </NotAuthorized>
    </AuthorizeView>
</div>
```

## Token refresh

The custom cookie refresher (`CookieOidcRefresher.cs`) implementation updates the user's claims automatically when they expire. The current implementation expects to receive an ID token from the token endpoint in exchange for the refresh token. The claims in this ID token are then used to overwrite the user's claims.

The sample implementation doesn't include code for requesting claims from the [UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) on token refresh. For more information, see [`BlazorWebAppOidc AddOpenIdConnect with GetClaimsFromUserInfoEndpoint = true doesn't propogate [sic] role claims to client` (`dotnet/aspnetcore` #58826)](https://github.com/dotnet/aspnetcore/issues/58826#issuecomment-2492738142).

Note

Some identity providers [only return an access token when using a refresh token](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse). The `CookieOidcRefresher` can be updated with additional logic to continue to use the prior set of claims stored in the authentication cookie or use the access token to request claims from the UserInfo endpoint.

## Cryptographic nonce

A *nonce* is a string value that associates a client's session with an ID token to mitigate [replay attacks](https://developer.mozilla.org/docs/Glossary/Replay_attack).

If you receive a nonce error during authentication development and testing, use a new InPrivate/incognito browser session for each test run, no matter how small the change made to the app or test user because stale cookie data can lead to a nonce error. For more information, see the [Cookies and site data](#cookies-and-site-data) section.

A nonce isn't required or used when a refresh token is exchanged for a new access token. In the sample app, the `CookieOidcRefresher` (`CookieOidcRefresher.cs`) deliberately sets [OpenIdConnectProtocolValidator.RequireNonce](/en-us/dotnet/api/microsoft.identitymodel.protocols.openidconnect.openidconnectprotocolvalidator.requirenonce#microsoft-identitymodel-protocols-openidconnect-openidconnectprotocolvalidator-requirenonce) to `false`.

## Application roles for apps not registered with Microsoft Entra (ME-ID)

*This section pertains to apps that don't use [Microsoft Entra ID (ME-ID)](https://www.microsoft.com/security/business/microsoft-entra) as the identity provider. For apps registered with ME-ID, see the [Application roles for apps registered with Microsoft Entra (ME-ID)](#application-roles-for-apps-registered-with-microsoft-entra-me-id) section.*

Configure the role claim type ([TokenValidationParameters.RoleClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.roleclaimtype#microsoft-identitymodel-tokens-tokenvalidationparameters-roleclaimtype)) in the [OpenIdConnectOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions) of `Program.cs`:

```
oidcOptions.TokenValidationParameters.RoleClaimType = "{ROLE CLAIM TYPE}";
```

For many OIDC identity providers, the role claim type is `role`. Check your identity provider's documentation for the correct value.

Replace the `UserInfo` class in the `BlazorWebAppOidc.Client` project with the following class.

`UserInfo.cs`:

```
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using System.Security.Claims;

namespace BlazorWebAppOidc.Client;

// Add properties to this class and update the server and client 
// AuthenticationStateProviders to expose more information about 
// the authenticated user to the client.
public sealed class UserInfo
{
    public required string UserId { get; init; }
    public required string Name { get; init; }
    public required string[] Roles { get; init; }

    public const string UserIdClaimType = "sub";
    public const string NameClaimType = "name";
    private const string RoleClaimType = "role";

    public static UserInfo FromClaimsPrincipal(ClaimsPrincipal principal) =>
        new()
        {
            UserId = GetRequiredClaim(principal, UserIdClaimType),
            Name = GetRequiredClaim(principal, NameClaimType),
            Roles = principal.FindAll(RoleClaimType).Select(c => c.Value)
                .ToArray(),
        };

    public ClaimsPrincipal ToClaimsPrincipal() =>
        new(new ClaimsIdentity(
            Roles.Select(role => new Claim(RoleClaimType, role))
                .Concat([
                    new Claim(UserIdClaimType, UserId),
                    new Claim(NameClaimType, Name),
                ]),
            authenticationType: nameof(UserInfo),
            nameType: NameClaimType,
            roleType: RoleClaimType));

    private static string GetRequiredClaim(ClaimsPrincipal principal,
        string claimType) =>
            principal.FindFirst(claimType)?.Value ??
            throw new InvalidOperationException(
                $"Could not find required '{claimType}' claim.");
}
```

At this point, Razor components can adopt [role-based and policy-based authorization](./?view=aspnetcore-10.0#role-based-and-policy-based-authorization). Application roles appear in `role` claims, one claim per role.

## Application roles for apps registered with Microsoft Entra (ME-ID)

Use the guidance in this section to implement application roles, ME-ID security groups, and ME-ID built-in administrator roles for apps using [Microsoft Entra ID (ME-ID)](https://www.microsoft.com/security/business/microsoft-entra).

The approach described in this section configures ME-ID to send groups and roles in the authentication cookie header. When users are only a member of a few security groups and roles, the following approach should work for most hosting platforms without running into a problem where headers are too long, for example with IIS hosting that has a default header length limit of 16 KB (`MaxRequestBytes`). If header length is a problem due to high group or role membership, we recommend not following the guidance in this section in favor of implementing [Microsoft Graph](/en-us/graph/sdks/sdks-overview) to obtain a user's groups and roles from ME-ID separately, an approach that doesn't inflate the size of the authentication cookie. For more information, see [Bad Request - Request Too Long - IIS Server (`dotnet/aspnetcore` #57545)](https://github.com/dotnet/aspnetcore/issues/57545).

Configure the role claim type ([TokenValidationParameters.RoleClaimType](/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.roleclaimtype#microsoft-identitymodel-tokens-tokenvalidationparameters-roleclaimtype)) in [OpenIdConnectOptions](/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions) of `Program.cs`. Set the value to `roles`:

```
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";
```

Although you can't [assign roles to groups](/en-us/entra/identity/role-based-access-control/groups-concept) without an ME-ID Premium account, you can assign roles to users and receive role claims for users with a standard Azure account. The guidance in this section doesn't require an ME-ID Premium account.

When working with the default directory, follow the guidance in [Add app roles to your application and receive them in the token (ME-ID documentation)](/en-us/entra/identity-platform/howto-add-app-roles-in-apps) to configure and assign roles. If you aren't working with the default directory, edit the app's manifest in the Azure portal to establish the app's roles manually in the `appRoles` entry of the manifest file. For more information, see [Configure the role claim (ME-ID documentation)](/en-us/entra/identity-platform/enterprise-app-role-management).

A user's Azure security groups arrive in `groups` claims, and a user's built-in ME-ID administrator role assignments arrive in [well-known IDs (`wids`) claims](/en-us/entra/identity-platform/access-tokens#payload-claims). Values for both claim types are GUIDs. When received by the app, these claims can be used to establish [role and policy authorization in Razor components](./?view=aspnetcore-10.0#role-based-and-policy-based-authorization).

In the app's manifest in the Azure portal, set the [`groupMembershipClaims` attribute](/en-us/entra/identity-platform/reference-app-manifest#groupmembershipclaims-attribute) to `All`. A value of `All` results in ME-ID sending all of the security/distribution groups (`groups` claims) and roles (`wids` claims) of the signed-in user. To set the `groupMembershipClaims` attribute:

1. Open the app's registration in the Azure portal.
2. Select **Manage** > **Manifest** in the sidebar.
3. Find the `groupMembershipClaims` attribute.
4. Set the value to `All` (`"groupMembershipClaims": "All"`).
5. Select the **Save** button.

Replace the `UserInfo` class in the `BlazorWebAppOidc.Client` project with the following class.

`UserInfo.cs`:

```
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using System.Security.Claims;

namespace BlazorWebAppOidc.Client;

// Add properties to this class and update the server and client 
// AuthenticationStateProviders to expose more information about 
// the authenticated user to the client.
public sealed class UserInfo
{
    public required string UserId { get; init; }
    public required string Name { get; init; }
    public required string[] Roles { get; init; }
    public required string[] Groups { get; init; }
    public required string[] Wids { get; init; }

    public const string UserIdClaimType = "sub";
    public const string NameClaimType = "name";
    private const string RoleClaimType = "roles";
    private const string GroupsClaimType = "groups";
    private const string WidsClaimType = "wids";

    public static UserInfo FromClaimsPrincipal(ClaimsPrincipal principal) =>
        new()
        {
            UserId = GetRequiredClaim(principal, UserIdClaimType),
            Name = GetRequiredClaim(principal, NameClaimType),
            Roles = principal.FindAll(RoleClaimType).Select(c => c.Value)
                .ToArray(),
            Groups = principal.FindAll(GroupsClaimType).Select(c => c.Value)
                .ToArray(),
            Wids = principal.FindAll(WidsClaimType).Select(c => c.Value)
                .ToArray(),
        };

    public ClaimsPrincipal ToClaimsPrincipal() =>
        new(new ClaimsIdentity(
            Roles.Select(role => new Claim(RoleClaimType, role))
                .Concat(Groups.Select(role => new Claim(GroupsClaimType, role)))
                .Concat(Wids.Select(role => new Claim(WidsClaimType, role)))
                .Concat([
                    new Claim(UserIdClaimType, UserId),
                    new Claim(NameClaimType, Name),
                ]),
            authenticationType: nameof(UserInfo),
            nameType: NameClaimType,
            roleType: RoleClaimType));

    private static string GetRequiredClaim(ClaimsPrincipal principal,
        string claimType) =>
            principal.FindFirst(claimType)?.Value ??
            throw new InvalidOperationException(
                $"Could not find required '{claimType}' claim.");
}
```

At this point, Razor components can adopt [role-based and policy-based authorization](./?view=aspnetcore-10.0#role-based-and-policy-based-authorization):

* Application roles appear in `roles` claims, one claim per role.
* Security groups appear in `groups` claims, one claim per group. The security group GUIDs appear in the Azure portal when you create a security group and are listed when selecting **Identity** > **Overview** > **Groups** > **View**.
* Built-in ME-ID administrator roles appear in `wids` claims, one claim per role. The `wids` claim with a value of `b79fbf4d-3ef9-4689-8143-76b194e85509` is always sent by ME-ID for non-guest accounts of the tenant and doesn't refer to an administrator role. Administrator role GUIDs (*role template IDs*) appear in the Azure portal when selecting **Roles & admins**, followed by the ellipsis (**…**) > **Description** for the listed role. The role template IDs are also listed in [Microsoft Entra built-in roles (Entra documentation)](/en-us/entra/identity/role-based-access-control/permissions-reference).

## Alternative: Duende Access Token Management

In the sample app, a custom cookie refresher (`CookieOidcRefresher.cs`) implementation is used to perform automatic non-interactive token refresh. An alternative solution can be found in the open source [`Duende.AccessTokenManagement.OpenIdConnect` package](https://docs.duendesoftware.com/accesstokenmanagement/web-apps/).

Duende Access Token Management provides automatic access token management features for .NET worker and ASP.NET Core web apps, including Blazor, without the need to add a custom cookie refresher.

After the package is installed, remove the `CookieOidcRefresher` and add access token management for the currently logged-in user in the `Program` file:

```
// Add services for token management
builder.Services.AddOpenIdConnectAccessTokenManagement();

// Register a typed HTTP client with token management support
builder.Services.AddHttpClient<InvoiceClient>(client =>
    {
        client.BaseAddress = new Uri("https://api.example.com/invoices/");
    })
    .AddUserAccessTokenHandler();
```

The [typed HTTP client](../call-web-api?view=aspnetcore-10.0#typed-httpclient) (or [named HTTP client](../call-web-api?view=aspnetcore-10.0#named-httpclient-with-ihttpclientfactory), if implemented) has automatic access token lifetime management on behalf of the currently logged-in user, including transparent refresh token management.

For more information, see the [Duende Access Token Management documentation for Blazor](https://docs.duendesoftware.com/accesstokenmanagement/blazor-server/).

## Host in a web farm or cluster

Server-side Blazor Web Apps hosted in a web farm or cluster of machines must adopt [*session affinity*](../fundamentals/signalr?view=aspnetcore-10.0#use-session-affinity-sticky-sessions-for-server-side-web-farm-hosting) to maintain Blazor circuits for users of the app.

We also recommend using a shared [Data Protection](../../security/data-protection/introduction?view=aspnetcore-10.0) key ring in production, even when the app uses the Interactive WebAssembly render mode exclusively for client-side rendering (no Blazor circuits). For more information, see the following articles:

* [Configure ASP.NET Core Data Protection](../../security/data-protection/configuration/overview?view=aspnetcore-10.0)
* [Key storage providers in ASP.NET Core](../../security/data-protection/implementation/key-storage-providers?view=aspnetcore-10.0)
* [Key encryption at rest in Windows and Azure using ASP.NET Core](../../security/data-protection/implementation/key-encryption-at-rest?view=aspnetcore-10.0)

  > Uncaught TypeError TypeError: Failed to execute 'postMessage' on 'Window': The provided value cannot be converted to a sequence.

    * [Google Chrome](https://developers.google.com/web/tools/chrome-devtools/network) (Google documentation)
    * [Microsoft Edge](/en-us/microsoft-edge/devtools-guide-chromium/network/)
    * [Mozilla Firefox](https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/index.html) (Mozilla documentation)

  > info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService\[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.

    * Microsoft Edge: `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`
    * Google Chrome: `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
    * Mozilla Firefox: `C:\Program Files\Mozilla Firefox\firefox.exe`

  * In the **Arguments** field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.

    * Microsoft Edge: Use `-inprivate`.
    * Google Chrome: Use `--incognito --new-window {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).
    * Mozilla Firefox: Use `-private -url {URL}`, where the `{URL}` placeholder is the URL to open (for example, `https://localhost:5001`).

```
@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

<PageTitle>User Claims</PageTitle>

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li><b>@claim.Type:</b> @claim.Value</li>
        }
    </ul>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    [CascadingParameter]
    private Task<AuthenticationState>? AuthState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (AuthState == null)
        {
            return;
        }

        var authState = await AuthState;
        claims = authState.User.Claims;
    }
}
```

### Inspect the access token

Obtaining the access token during development is often helpful when troubleshooting app and Azure configuration problems. In the following example for a weather forecast endpoint, the bearer token and token details are logged only when the app is compiled with the `DEBUG` symbol, which is typically a Debug build. You can decode the token using an online JWT token decoder, such as the [Microsoft JWT token decoder](https://jwt.ms/), or log details from the token in C#, as the following example demonstrates.

Caution

In production, avoid logging the token or its contents.

```
app.MapGet("/weather-forecast", (HttpContext context, ILogger<Program> logger) =>
{
#if DEBUG
    var authHeader = context.Request.Headers.Authorization.FirstOrDefault(v => 
        v != null && 
        v.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase));

    if (authHeader is not null)
    {
        var token = authHeader["Bearer ".Length..].Trim();
        logger.LogDebug("Token: {Token}", token);

        try
        {
                var handler = 
                    new Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler();
                var jwtToken = handler.ReadJsonWebToken(token);
                logger.LogDebug("Audience: {Audience}", 
                    string.Join(", ", jwtToken.Audiences));
                logger.LogDebug("Issuer: {Issuer}", jwtToken.Issuer);
            var jwtToken = handler.ReadJwtToken(token);
            logger.LogDebug("Audience: {Audience}", 
                string.Join(", ", jwtToken.Audiences));
            logger.LogDebug("Issuer: {Issuer}", jwtToken.Issuer);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to decode token.");
        }
    }
#endif

    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();
```

## Related content

* [`AzureAD/microsoft-identity-web` GitHub repository](https://github.com/AzureAD/microsoft-identity-web/wiki): Helpful guidance on implementing Microsoft Identity Web for Microsoft Entra ID for ASP.NET Core apps, including links to sample apps and related Azure documentation. Currently, Blazor Web Apps aren't explicitly addressed by the Azure documentation, but the setup and configuration of a Blazor Web App for ME-ID and Azure hosting is the same as it is for any ASP.NET Core web app.
* [`AuthenticationStateProvider` service](./?view=aspnetcore-10.0#authenticationstateprovider-service)
* [Manage authentication state in Blazor Web Apps](./?view=aspnetcore-10.0#manage-authentication-state-in-blazor-web-apps)
* [Refresh token during http request in Blazor Interactive Server with OIDC (`dotnet/aspnetcore` #55213)](https://github.com/dotnet/aspnetcore/issues/55213)
* [Secure data in Blazor Web Apps with Interactive Auto rendering](./?view=aspnetcore-10.0#secure-data-in-blazor-web-apps-with-interactive-auto-rendering)
* [How to access an `AuthenticationStateProvider` from a `DelegatingHandler`](additional-scenarios?view=aspnetcore-10.0#access-authenticationstateprovider-in-outgoing-request-middleware)
* [Opaque (reference) access token support](additional-scenarios?view=aspnetcore-10.0#opaque-reference-access-token-support)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/anchor-tag-helper
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/anchor-tag-helper.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Anchor Tag Helper in ASP.NET Core

By [Peter Kellner](https://peterkellner.net) and [Scott Addie](https://github.com/scottaddie)

The [Anchor Tag Helper](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper) enhances the standard HTML anchor (`<a ... ></a>`) tag by adding new attributes. By convention, the attribute names are prefixed with `asp-`. The rendered anchor element's `href` attribute value is determined by the values of the `asp-` attributes.

For an overview of Tag Helpers, see [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0).

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/views/tag-helpers/built-in/samples) ([how to download](../../../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

*SpeakerController* is used in samples throughout this document:

```
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;

public class SpeakerController : Controller
{
    private List<Speaker> Speakers =
        new List<Speaker>
        {
            new Speaker {SpeakerId = 10},
            new Speaker {SpeakerId = 11},
            new Speaker {SpeakerId = 12}
        };

    [Route("Speaker/{id:int}")]
    public IActionResult Detail(int id) =>
        View(Speakers.FirstOrDefault(a => a.SpeakerId == id));

    [Route("/Speaker/Evaluations", 
           Name = "speakerevals")]
    public IActionResult Evaluations() => View();

    [Route("/Speaker/EvaluationsCurrent",
           Name = "speakerevalscurrent")]
    public IActionResult Evaluations(
        int speakerId,
        bool currentYear) => View();

    public IActionResult Index() => View(Speakers);
}

public class Speaker
{
    public int SpeakerId { get; set; }
}
```

## Anchor Tag Helper attributes

### asp-controller

The [asp-controller](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.controller) attribute assigns the controller used for generating the URL. The following markup lists all speakers:

```
<a asp-controller="Speaker"
   asp-action="Index">All Speakers</a>
```

The generated HTML:

```
<a href="/Speaker">All Speakers</a>
```

If the `asp-controller` attribute is specified and `asp-action` isn't, the default `asp-action` value is the controller action associated with the currently executing view. If `asp-action` is omitted from the preceding markup, and the Anchor Tag Helper is used in *HomeController*'s *Index* view (*/Home*), the generated HTML is:

```
<a href="/Home">All Speakers</a>
```

### asp-action

The [asp-action](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.action) attribute value represents the controller action name included in the generated `href` attribute. The following markup sets the generated `href` attribute value to the speaker evaluations page:

```
<a asp-controller="Speaker"
   asp-action="Evaluations">Speaker Evaluations</a>
```

The generated HTML:

```
<a href="/Speaker/Evaluations">Speaker Evaluations</a>
```

If no `asp-controller` attribute is specified, the default controller calling the view executing the current view is used.

If the `asp-action` attribute value is `Index`, then no action is appended to the URL, leading to the invocation of the default `Index` action. The action specified (or defaulted), must exist in the controller referenced in `asp-controller`.

### asp-route-{value}

The [asp-route-{value}](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.routevalues) attribute enables a wildcard route prefix. Any value occupying the `{value}` placeholder is interpreted as a potential route parameter. If a default route isn't found, this route prefix is appended to the generated `href` attribute as a request parameter and value. Otherwise, it's substituted in the route template.

Consider the following controller action:

```
private List<Speaker> Speakers =
    new List<Speaker>
    {
        new Speaker {SpeakerId = 10},
        new Speaker {SpeakerId = 11},
        new Speaker {SpeakerId = 12}
    };

[Route("Speaker/{id:int}")]
public IActionResult Detail(int id) =>
    View(Speakers.FirstOrDefault(a => a.SpeakerId == id));
```

With a default route template defined in `Program.cs`:

```
app.MapControllerRoute(
  name: "default",
  pattern: "{controller=Home}/{action=Index}/{id?}");
```

The MVC view uses the model, provided by the action, as follows:

```
@model Speaker
<!DOCTYPE html>
<html>
<body>
    <a asp-controller="Speaker"
       asp-action="Detail" 
       asp-route-id="@Model.SpeakerId">SpeakerId: @Model.SpeakerId</a>
</body>
</html>
```

The default route's `{id?}` placeholder was matched. The generated HTML:

```
<a href="/Speaker/Detail/12">SpeakerId: 12</a>
```

Assume the route prefix isn't part of the matching routing template, as with the following MVC view:

```
@model Speaker
<!DOCTYPE html>
<html>
<body>
    <a asp-controller="Speaker"
       asp-action="Detail"
       asp-route-speakerid="@Model.SpeakerId">SpeakerId: @Model.SpeakerId</a>
<body>
</html>
```

The following HTML is generated because `speakerid` wasn't found in the matching route:

```
<a href="/Speaker/Detail?speakerid=12">SpeakerId: 12</a>
```

If either `asp-controller` or `asp-action` aren't specified, then the same default processing is followed as is in the `asp-route` attribute.

### asp-route

The [asp-route](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.route) attribute is used for creating a URL linking directly to a named route. Using [routing attributes](../../../controllers/routing?view=aspnetcore-10.0#ar), a route can be named as shown in the `SpeakerController` and used in its `Evaluations` action:

```
[Route("/Speaker/Evaluations", 
       Name = "speakerevals")]
```

In the following markup, the `asp-route` attribute references the named route:

```
<a asp-route="speakerevals">Speaker Evaluations</a>
```

The Anchor Tag Helper generates a route directly to that controller action using the URL */Speaker/Evaluations*. The generated HTML:

```
<a href="/Speaker/Evaluations">Speaker Evaluations</a>
```

If `asp-controller` or `asp-action` is specified in addition to `asp-route`, the route generated may not be what you expect. To avoid a route conflict, `asp-route` shouldn't be used with the `asp-controller` and `asp-action` attributes.

### asp-all-route-data

The [asp-all-route-data](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.routevalues) attribute supports the creation of a dictionary of key-value pairs. The key is the parameter name, and the value is the parameter value.

In the following example, a dictionary is initialized and passed to a Razor view. Alternatively, the data could be passed in with your model.

```
@{
var parms = new Dictionary<string, string>
            {
                { "speakerId", "11" },
                { "currentYear", "true" }
            };
}

<a asp-route="speakerevalscurrent"
   asp-all-route-data="parms">Speaker Evaluations</a>
```

The preceding code generates the following HTML:

```
<a href="/Speaker/EvaluationsCurrent?speakerId=11&currentYear=true">Speaker Evaluations</a>
```

The `asp-all-route-data` dictionary is flattened to produce a query string meeting the requirements of the overloaded `Evaluations` action:

```
public IActionResult Evaluations() => View();

[Route("/Speaker/EvaluationsCurrent",
       Name = "speakerevalscurrent")]
public IActionResult Evaluations(
```

If any keys in the dictionary match route parameters, those values are substituted in the route as appropriate. The other non-matching values are generated as request parameters.

### asp-fragment

The [asp-fragment](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.fragment) attribute defines a URL fragment to append to the URL. The Anchor Tag Helper adds the hash character (#). Consider the following markup:

```
<a asp-controller="Speaker"
   asp-action="Evaluations"
   asp-fragment="SpeakerEvaluations">Speaker Evaluations</a>
```

The generated HTML:

```
<a href="/Speaker/Evaluations#SpeakerEvaluations">Speaker Evaluations</a>
```

Hash tags are useful when building client-side apps. They can be used for easy marking and searching in JavaScript, for example.

### asp-area

The [asp-area](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.area) attribute sets the area name used to set the appropriate route. The following examples depict how the `asp-area` attribute causes a remapping of routes.

#### Usage in Razor Pages

Razor Pages areas are supported in ASP.NET Core 2.1 or later.

Consider the following directory hierarchy:

* **{Project name}**

  * **wwwroot**

  * **Areas**

    * **Sessions**

      * **Pages**

        * *\_ViewStart.cshtml*
        * `Index.cshtml`
        * `Index.cshtml.cs`

  * **Pages**

The markup to reference the *Sessions* area *Index* Razor Page is:

```
<a asp-area="Sessions"
   asp-page="/Index">View Sessions</a>
```

The generated HTML:

```
<a href="/Sessions">View Sessions</a>
```

Tip

To support areas in a Razor Pages app, ensure that area routing is enabled in your app's routing configuration. Use the minimal hosting model and configure area routes in *Program.cs* as needed.

#### Usage in MVC

Consider the following directory hierarchy:

* **{Project name}**

  * **wwwroot**

  * **Areas**

    * **Blogs**

      * **Controllers**
        * `HomeController.cs`

      * **Views**

        * **Home**

          * `AboutBlog.cshtml`
          * `Index.cshtml`

        * *\_ViewStart.cshtml*

  * **Controllers**

Setting `asp-area` to "Blogs" prefixes the directory *Areas/Blogs* to the routes of the associated controllers and views for this anchor tag. The markup to reference the *AboutBlog* view is:

```
<a asp-area="Blogs"
   asp-controller="Home"
   asp-action="AboutBlog">About Blog</a>
```

The generated HTML:

```
<a href="/Blogs/Home/AboutBlog">About Blog</a>
```

Tip

To support areas in an MVC app, the route template must include a reference to the area, if it exists. Configure area routes in *Program.cs* using `MapControllerRoute` with an area pattern, for example:

```
app.MapControllerRoute(
    name: "mvcAreaRoute",
    pattern: "{area:exists}/{controller=Home}/{action=Index}");
```

### asp-protocol

The [asp-protocol](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.protocol) attribute is for specifying a protocol (such as `https`) in your URL. For example:

```
<a asp-protocol="https"
   asp-controller="Home"
   asp-action="About">About</a>
```

The generated HTML:

```
<a href="https://localhost/Home/About">About</a>
```

The host name in the example is localhost. The Anchor Tag Helper uses the website's public domain when generating the URL.

### asp-host

The [asp-host](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.host) attribute is for specifying a host name in your URL. For example:

```
<a asp-protocol="https"
   asp-host="microsoft.com"
   asp-controller="Home"
   asp-action="About">About</a>
```

The generated HTML:

```
<a href="https://microsoft.com/Home/About">About</a>
```

### asp-page

The [asp-page](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.page) attribute is used with Razor Pages. Use it to set an anchor tag's `href` attribute value to a specific page. Prefixing the page name with `/` creates a URL for a matching page from the root of the app:

With the sample code, the following markup creates a link to the attendee Razor Page:

```
<a asp-page="/Attendee">All Attendees</a>
```

The generated HTML:

```
<a href="/Attendee">All Attendees</a>
```

The `asp-page` attribute is mutually exclusive with the `asp-route`, `asp-controller`, and `asp-action` attributes. However, `asp-page` can be used with `asp-route-{value}` to control routing, as the following markup demonstrates:

```
<a asp-page="/Attendee"
   asp-route-attendeeid="10">View Attendee</a>
```

The generated HTML:

```
<a href="/Attendee?attendeeid=10">View Attendee</a>
```

If the referenced page doesn't exist, a link to the current page is generated using an [ambient value](../../../../fundamentals/routing?view=aspnetcore-10.0#ambient-values-and-explicit-values) from the request. No warning is indicated, except at the debug log level.

### asp-page-handler

The [asp-page-handler](/en-us/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.pagehandler) attribute is used with Razor Pages. It's intended for linking to specific page handlers.

Consider the following page handler:

```
public void OnGetProfile(int attendeeId)
{
    ViewData["AttendeeId"] = attendeeId;

    // code omitted for brevity
}
```

The page model's associated markup links to the `OnGetProfile` page handler. Note the `On<Verb>` prefix of the page handler method name is omitted in the `asp-page-handler` attribute value. When the method is asynchronous, the `Async` suffix is omitted, too.

```
<a asp-page="/Attendee"
   asp-page-handler="Profile"
   asp-route-attendeeid="12">Attendee Profile</a>
```

The generated HTML:

```
<a href="/Attendee?attendeeid=12&handler=Profile">Attendee Profile</a>
```

## Related content

* [Areas in ASP.NET Core](../../../controllers/areas?view=aspnetcore-10.0)
* [Razor Pages architecture and concepts in ASP.NET Core](../../../../razor-pages/?view=aspnetcore-10.0)
* [Compatibility version for ASP.NET Core MVC](../../../compatibility-version?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/introduction.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Introduction to authorization in ASP.NET Core

Authorization refers to the process that determines what a user is able to do. For example, an administrative user is allowed to create a document library, add documents, edit documents, and delete them. A nonadministrative user working with the library is only authorized to read the documents.

Authorization is separate and distinct from authentication. However, authorization relies on an authentication mechanism. Authentication is the process of verifying a user's identity, which might result in the creation of one or more identity objects for the user.

Configuring authentication doesn't automatically restrict access to endpoints. To [require authenticated users by default](policies?view=aspnetcore-10.0#require-global-user-authentication) in a server-side app, configure a fallback authorization policy. For information about how authorization metadata selects named, default, and fallback policies, see [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0#default-and-fallback-policies).

For more information about authentication in ASP.NET Core, see [Overview of ASP.NET Core Authentication](../authentication/?view=aspnetcore-10.0).

## Authorization types

ASP.NET Core authorization provides a simple declarative [role](roles?view=aspnetcore-10.0) and a rich [policy-based](policies?view=aspnetcore-10.0) model. Authorization is expressed in requirements, and handlers evaluate a user's claims against requirements. Imperative checks can be based on simple policies or policies that evaluate both the user identity and properties of the resource that the user is attempting to access.

## Namespaces

Authorization components, including the [`[Authorize]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and [`[AllowAnonymous]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.allowanonymousattribute), are defined in the [Microsoft.AspNetCore.Authorization](/en-us/dotnet/api/microsoft.aspnetcore.authorization) namespace.

For more information, see [Simple authorization in ASP.NET Core](simple?view=aspnetcore-10.0).

## Related content

* [Authentication and authorization in Minimal APIs](../../fundamentals/minimal-apis/security?view=aspnetcore-10.0)

* [ASP.NET Core Blazor authentication and authorization](../../blazor/security/?view=aspnetcore-10.0)

* [Razor Pages authorization conventions in ASP.NET Core](../../razor-pages/security/authorization/conventions?view=aspnetcore-10.0)

* Blazor app authorization patterns

  * [Server-side Blazor (Blazor Web Apps, Blazor Server apps)](../../blazor/security/additional-scenarios?view=aspnetcore-10.0#server-side-blazor-app-authorization-patterns)
  * [Blazor WebAssembly](../../blazor/security/webassembly/?view=aspnetcore-10.0#blazor-webassembly-authorization-patterns)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/webassembly-event-pipe-diagnostics
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/performance/webassembly-event-pipe-diagnostics.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor WebAssembly Event Pipe diagnostics

This article describes diagnostic tools and how to use them in Blazor WebAssembly apps.

## Prerequisite for all scenarios

Install the [.NET WebAssembly build tools](../webassembly-build-tools-and-aot?view=aspnetcore-10.0#net-webassembly-build-tools):

```
dotnet workload install wasm-tools
```

## How a WebAssembly app uses memory and how to troubleshoot memory leaks

In the app's project file (`.csproj`), add following properties for the duration of the investigation:

```
<PropertyGroup>
  <EnableDiagnostics>true</EnableDiagnostics>
</PropertyGroup>
```

Warning

Don't enable diagnostics in production because it has a negative performance impact.

Build your app with the `wasm-tools` workload.

Open the app in a browser and navigate to problematic pages or components.

Take a managed memory dump by calling `collectGcDump` JavaScript API:

```
globalThis.getDotnetRuntime(0).collectGcDump();
```

Call the preceding API from either a browser developer tools console or JavaScript code of the app.

A `.nettrace` file is downloaded from the browser into a local folder, usually the `Downloads` folder on Windows.

Convert the dump to `.gcdump` format using the `dotnet-gcdump` tool. To view the converted `.gcdump` file, use Visual Studio or PrefView.

For more information, see [View the GC dump captured from dotnet-gcdump](/en-us/dotnet/core/diagnostics/dotnet-gcdump#view-the-gc-dump-captured-from-dotnet-gcdump).

## How a WebAssembly app uses CPU and how to find slow or hot methods

In the app's project file (`.csproj`), add following properties for the duration of the investigation:

```
<PropertyGroup>
  <EnableDiagnostics>true</EnableDiagnostics>
  <!-- Disable debugger -->
  <WasmDebugLevel>0</WasmDebugLevel>
  <!-- Sampling in all methods, see below for filtering options -->
  <WasmPerformanceInstrumentation>all</WasmPerformanceInstrumentation>
</PropertyGroup>
```

Warning

Don't enable diagnostics in production because it has a negative performance impact.

Build the app with the `wasm-tools` workload.

Open the app in a browser and navigate to problematic pages or components.

Start colllecting CPU samples for 60 seconds by calling the `collectCpuSamples` JavaScript API:

```
globalThis.getDotnetRuntime(0).collectCpuSamples({durationSeconds: 60});
```

Call the preceding API from either a browser developer tools console or JavaScript code of the app.

Start using the app to run problematic code.

After the predefined period, the browser downloads a `.nettrace` file into a local folder, usually the `Downloads` folder on Windows. To view the `.nettrace` file, use Visual Studio or PrefView.

For more information, see [Use EventPipe to trace your .NET application](/en-us/dotnet/core/diagnostics/eventpipe#use-eventpipe-to-trace-your-net-application).

The [`Timing-Allow-Origin` HTTP header](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Timing-Allow-Origin) allows for more precise time measurements.

## How to observe metrics emitted by a WebAssembly app

In the app's project file (`.csproj`), add following properties for the duration of the investigation:

```
<PropertyGroup>
  <EnableDiagnostics>true</EnableDiagnostics>
  <MetricsSupport>true</MetricsSupport>
  <EventSourceSupport>true</EventSourceSupport>
</PropertyGroup>
```

Warning

Don't enable diagnostics in production because it has a negative performance impact.

Build the app with the `wasm-tools` workload.

Open the app in a browser and navigate to problematic pages or components.

Start colllecting metrics for 60 seconds by calling the `collectMetrics` JavaScript API:

```
globalThis.getDotnetRuntime(0).collectMetrics({durationSeconds: 60});
```

Call the preceding API from either a browser developer tools console or JavaScript code of the app.

After the predefined period, the browser downloads a `.nettrace` file into a local folder, usually the `Downloads` folder on Windows. To view the `.nettrace` file, use Visual Studio or PrefView.

For more information, see [Use EventPipe to trace your .NET application](/en-us/dotnet/core/diagnostics/eventpipe#use-eventpipe-to-trace-your-net-application).

## MSBuild properties that enable diagnostic integration

| Property                           | Default  | Set value to… | Description                                                                                                                                                                                             |
| ---------------------------------- | -------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<EnableDiagnostics>`              | `false`  | `true`        | Enables support for WebAssembly performance tracing.                                                                                                                                                    |
| `<WasmPerformanceInstrumentation>` | No value | See table†    | Enables instrumentation necessary for the sampling profiler. The property follows the callspec syntax. †For permissible values, see the following table.                                                |
| `<MetricsSupport>`                 | `false`  | `true`        | Enables `System.Diagnostics.Metrics` support. For more information, see the [`System.Diagnostics.Metrics` namespace](/en-us/dotnet/api/system.diagnostics.metrics).                                     |
| `<EventSourceSupport>`             | `false`  | `true`        | Enables `EventPipe` support. For more information, see [Diagnostics and instrumentation: Observability and telemetry](/en-us/dotnet/core/deploying/native-aot/diagnostics#observability-and-telemetry). |

The following table describes permissable `<WasmPerformanceInstrumentation>` values.

| `<WasmPerformanceInstrumentation>` value | Description                           |
| ---------------------------------------- | ------------------------------------- |
| `all`                                    | All assemblies                        |
| `program`                                | Entry point assembly                  |
| `{ASSEMBLY}`                             | Specifies an assembly (`{ASSEMBLY}`)  |
| `M:Type:{METHOD}`                        | Specifies a method (`{METHOD}`)       |
| `N:{NAMESPACE}`                          | Specifies a namespace (`{NAMESPACE}`) |
| `T:{TYPE}`                               | Specifies a type (`{TYPE}`)           |
| `+EXPR`                                  | Includes expression                   |
| `-EXPR`                                  | Excludes expression                   |

Your code should yield to main browser loop often to allow the trace to be collected. When executing long running loops, the internal diagnostic buffers could overflow.

Caution

Enabling profilers and diagnostic tools has negative size and performance impacts, so don't publish an app for production with profilers enabled.

## Related content

* [EventPipe](/en-us/dotnet/core/diagnostics/eventpipe) is a runtime component used to collect tracing data, similar to [ETW](/en-us/windows/win32/etw/event-tracing-portal) and [perf\_events](https://wikipedia.org/wiki/Perf_%28Linux%29).
* [What diagnostic tools are available in .NET Core?](/en-us/dotnet/core/diagnostics/)
* [.NET diagnostic tools](/en-us/dotnet/core/diagnostics/tools-overview)
* [`dotnet/diagnostics` GitHub repository](https://github.com/dotnet/diagnostics)
* [`Microsoft.Diagnostics.NETCore.Client` NuGet package](https://www.nuget.org/packages/Microsoft.Diagnostics.NETCore.Client)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/json-transcoding-binding
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/json-transcoding-binding.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure HTTP and JSON for gRPC JSON transcoding

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [James Newton-King](https://twitter.com/jamesnk)

gRPC JSON transcoding creates RESTful JSON web APIs from gRPC methods. It uses annotations and options for customizing how a RESTful API maps to the gRPC methods.

## HTTP rules

gRPC methods must be annotated with an HTTP rule before they support transcoding. The HTTP rule includes information about calling the gRPC method as a RESTful API, such as the HTTP method and route.

```
import "google/api/annotations.proto";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      get: "/v1/greeter/{name}"
    };
  }
}
```

An HTTP rule is:

* An annotation on gRPC methods.
* Identified by the name `google.api.http`.
* Imported from the `google/api/annotations.proto` file. The [`google/api/http.proto`](https://github.com/dotnet/aspnetcore/blob/main/src/Grpc/JsonTranscoding/test/testassets/Sandbox/google/api/http.proto) and [`google/api/annotations.proto`](https://github.com/dotnet/aspnetcore/blob/main/src/Grpc/JsonTranscoding/test/testassets/Sandbox/google/api/annotations.proto) files need to be in the project.

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### HTTP method

The HTTP method is specified by setting the route to the matching HTTP method field name:

* `get`
* `put`
* `post`
* `delete`
* `patch`

The `custom` field allows for other HTTP methods.

In the following example, the `CreateAddress` method is mapped to `POST` with the specified route:

```
service Address {
  rpc CreateAddress (CreateAddressRequest) returns (CreateAddressReply) {
    option (google.api.http) = {
      post: "/v1/address",
      body: "*"
    };
  }
}
```

### Route

gRPC JSON transcoding routes support route parameters. For example, `{name}` in a route binds to the `name` field on the request message.

To bind a field on a nested message, specify the path to the field. In the following example, `{params.org}` binds to the `org` field on the `IssueParams` message:

```
service Repository {
  rpc GetIssue (GetIssueRequest) returns (GetIssueReply) {
    option (google.api.http) = {
      get: "/{apiVersion}/{params.org}/{params.repo}/issue/{params.issueId}"
    };
  }
}

message GetIssueRequest {
  int32 api_version = 1;
  IssueParams params = 2;
}
message IssueParams {
  string org = 1;
  string repo = 2;
  int32 issueId = 3;
}
```

Transcoding routes and [ASP.NET Core routes](../fundamentals/routing?view=aspnetcore-10.0) have a similar syntax and feature set. However, some ASP.NET Core routing features aren't supported by transcoding. These include:

* [Route constraints](../fundamentals/routing?view=aspnetcore-10.0#route-constraints)
* [Default values](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [Optional parameters](../fundamentals/routing?view=aspnetcore-10.0#route-templates)
* [Complex segments](../fundamentals/routing?view=aspnetcore-10.0#complex-segments)

### Request body

Transcoding deserializes the request body JSON to the request message. The `body` field specifies how the HTTP request body maps to the request message. The value is either the name of the request field whose value is mapped to the HTTP request body or `*` for mapping all request fields.

In the following example, the HTTP request body is deserialized to the `address` field:

```
service Address {
  rpc AddAddress (AddAddressRequest) returns (AddAddressReply) {
    option (google.api.http) = {
      post: "/{apiVersion}/address",
      body: "address"
    };
  }
}

message AddAddressRequest {
  int32 api_version = 1;
  Address address = 2;
}
message Address {
  string street = 1;
  string city = 2;
  string country = 3;
}
```

### Query parameters

Any fields in the request message that aren't bound by route parameters or the request body can be set using HTTP query parameters.

```
service Repository {
  rpc GetIssues (GetIssuesRequest) returns (GetIssuesReply) {
    option (google.api.http) = {
      get: "/v1/{org}/{repo}/issue"
    };
  }
}

message GetIssuesRequest {
  string org = 1;
  string repo = 2;
  string text = 3;
  PageParams page = 4;
}
message PageParams {
  int32 index = 1;
  int32 size = 2;
}
```

In the preceding example:

* `org` and `repo` fields are bound from route parameters.
* Other fields, such as `text` and the nested fields from `page`, can be bound from the query string: `?text=value&page.index=0&page.size=10`

### Response body

By default, transcoding serializes the entire response message as JSON. The `response_body` field allows serialization of a subset of the response message.

```
service Address {
  rpc GetAddress (GetAddressRequest) returns (GetAddressReply) {
    option (google.api.http) = {
      get: "/v1/address/{id}",
      response_body: "address"
    };
  }
}

message GetAddressReply {
  int32 version = 1;
  Address address = 2;
}
message Address {
  string street = 1;
  string city = 2;
  string country = 3;
}
```

In the preceding example, the `address` field is serialized to the response body as JSON.

### Specification

For more information about customizing gRPC transcoding, see the [HttpRule specification](https://cloud.google.com/service-infrastructure/docs/service-management/reference/rpc/google.api#google.api.HttpRule).

## Customize JSON

Messages are converted to and from JSON using the [JSON mapping in the Protobuf specification](https://developers.google.com/protocol-buffers/docs/proto3#json). Protobuf's JSON mapping is a standardized way to convert between JSON and Protobuf, and all serialization follows these rules.

However, gRPC JSON transcoding offers some limited options for customizing JSON with [GrpcJsonSettings](/en-us/dotnet/api/microsoft.aspnetcore.grpc.jsontranscoding.grpcjsonsettings), as shown in the following table.

| Option                                                                                                                                                                                                    | Default Value | Description                                                                                                                                                                    |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [IgnoreDefaultValues](/en-us/dotnet/api/microsoft.aspnetcore.grpc.jsontranscoding.grpcjsonsettings.ignoredefaultvalues#microsoft-aspnetcore-grpc-jsontranscoding-grpcjsonsettings-ignoredefaultvalues)    | `false`       | If set to `true`, fields with default values are ignored during serialization.                                                                                                 |
| [WriteEnumsAsIntegers](/en-us/dotnet/api/microsoft.aspnetcore.grpc.jsontranscoding.grpcjsonsettings.writeenumsasintegers#microsoft-aspnetcore-grpc-jsontranscoding-grpcjsonsettings-writeenumsasintegers) | `false`       | If set to `true`, enum values are written as integers instead of strings.                                                                                                      |
| [WriteInt64sAsStrings](/en-us/dotnet/api/microsoft.aspnetcore.grpc.jsontranscoding.grpcjsonsettings.writeint64sasstrings#microsoft-aspnetcore-grpc-jsontranscoding-grpcjsonsettings-writeint64sasstrings) | `false`       | If set to `true`, `Int64` and `UInt64` values are written as strings instead of numbers.                                                                                       |
| [WriteIndented](/en-us/dotnet/api/microsoft.aspnetcore.grpc.jsontranscoding.grpcjsonsettings.writeindented#microsoft-aspnetcore-grpc-jsontranscoding-grpcjsonsettings-writeindented)                      | `false`       | If set to `true`, JSON is written using pretty printing. This option doesn't affect streaming methods, which write line-delimited JSON messages and can't use pretty printing. |

```
builder.Services.AddGrpc().AddJsonTranscoding(o =>
{
    o.JsonSettings.WriteIndented = true;
});
```

In the `.proto` file, the `json_name` field option customizes a field's name when it's serialized as JSON, as in the following example:

```
message TestMessage {
  string my_field = 1 [json_name="customFieldName"];
}
```

Transcoding doesn't support advanced JSON customization. Apps requiring precise JSON structure control should consider using [ASP.NET Core Web API](../web-api/?view=aspnetcore-10.0).

## Related content

* [gRPC JSON transcoding in ASP.NET Core gRPC apps](json-transcoding?view=aspnetcore-10.0)
* [HttpRule specification](https://cloud.google.com/service-infrastructure/docs/service-management/reference/rpc/google.api#google.api.HttpRule)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-07-31

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/custom-authorization-policy-providers
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authorization/custom-authorization-policy-providers.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Custom authorization policy providers in ASP.NET Core

By [Mike Rousos](https://github.com/mjrousos)

This article describes how to implement the [IAuthorizationPolicyProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider) interface to create a custom authorization policy provider, including how to create an authorization attribute for the provider.

For a typical implementation of [policy-based authorization](policies?view=aspnetcore-10.0), policies are registered by calling [AuthorizationOptions.AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.addpolicy) during authorization service configuration. Sometimes, it isn't possible or desirable to register many authorization policies in this manner.

For example, an app might require policy-based checks for building room numbers or user ages, where it doesn't make sense to create policies for building room numbers or ages with many [AddPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationoptions.addpolicy) calls. These scenarios are best implemented by passing a parameter that represents room numbers or ages to a custom `[Authorize]` attribute backed by a custom authorization policy provider. The custom policy provider receives the parameter value and dynamically creates a single policy to determine if authorization requirements are met. Using this approach, you avoid creating dozens or even hundreds of individual, explicit authorization policies.

Other scenarios where a custom policy provider is useful include:

* To dynamically, flexibly determine authorization requirements based on complex logic.
* To create policies at runtime based on information from an external data source, such as a database.
* When an external service is used to provide policy evaluation.

## Sample app

The Blazor Web App sample for this article is the [`BlazorWebAppAuthorization` sample app (`dotnet/AspNetCore.Docs.Samples` GitHub repository)](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/security/authorization/BlazorWebAppAuthorization) ([how to download](../../overview?view=aspnetcore-10.0#how-to-download-a-sample)). The sample app uses seeded accounts to demonstrate the examples in this article. For more information, see the sample's README file (`README.md`).

Caution

This sample app uses an in-memory database to store user information, which isn't suitable for production scenarios. The sample app is intended for demonstration purposes only and shouldn't be used as a starting point for production apps.

For an MVC sample, see the [`CustomPolicyProvider` sample in the `dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore/tree/v3.1.3/src/Security/samples/CustomPolicyProvider).

Tip

Use the [`git sparse-checkout` command](https://git-scm.com/docs/git-sparse-checkout) to download a single folder from the main branch of a GitHub repository.

In the following example, the `security/authorization/BlazorWebAppAuthorization` subfolder is downloaded from the `dotnet/AspNetCore.Docs.Samples` repository. Replace `https://github.com/dotnet/AspNetCore.Docs.Samples.git` with the URL of the repository that you want to clone, and replace the `security/authorization/BlazorWebAppAuthorization` path with the path to the subfolder that you want to download:

```
git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.Samples.git --sparse
cd AspNetCore.Docs.Samples
git sparse-checkout init --cone
git sparse-checkout set security/authorization/BlazorWebAppAuthorization
```

## Customize policy retrieval

The developer decides in advance how custom policies are named by inventing a naming scheme in a string format that's easily parsed to meet one or more requirements for each policy evaluation:

* The custom authorization attribute adopts the policy naming scheme.
* The custom policy provider adopts the same policy naming scheme.

For example, consider a minimum age naming scheme in the format `MinimumAge{AGE}`, where the `{AGE}` placeholder is any given age, for example, `MinimumAge21`. This naming scheme is easily identified by its prefix "`MinimumAge`" with an easily parsed string-based age ("`21`").

The developer customizes how authorization policies are provided by implementing the following APIs in the custom policy provider:

* The [GetPolicyAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider.getpolicyasync) method returns an authorization policy for a given name.
* The [GetDefaultPolicyAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider.getdefaultpolicyasync) method returns the default authorization policy, which is selected when authorization is required but no specific policy is set.
* The [GetFallbackPolicyAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider.getfallbackpolicyasync) method returns the fallback authorization policy, which is selected when no policy is produced from authorization metadata. By default, the fallback policy is `null`.

For complete named, default, and fallback policy selection rules, see [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0#default-and-fallback-policies).

The general format of a custom policy provider that either returns a policy for a given matching name (represented by the `{IDENTIFY POLICY BY NAME}` placeholder) or returns `null` when no policy name matches is similar to the following.

Implementing the required default and fallback policies for a custom policy provider ([GetDefaultPolicyAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider.getdefaultpolicyasync), [GetFallbackPolicyAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider.getfallbackpolicyasync)) are omitted in the following example for brevity but are described and shown in a complete example later in this article.

```
// Omitted for brevity:
//   'GetDefaultPolicyAsync' (default policy)
//   'GetFallbackPolicyAsync' (fallback policy) 
internal class CustomPolicyProvider() : IAuthorizationPolicyProvider
{
    public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        if ({IDENTIFY POLICY BY NAME})
        {
            var policy = new AuthorizationPolicyBuilder(
                IdentityConstants.ApplicationScheme);
            policy.AddRequirements(...);

            return Task.FromResult<AuthorizationPolicy?>(policy.Build());
        }

        return Task.FromResult<AuthorizationPolicy?>(null);
    }
}
```

Note

Use of [IdentityConstants.ApplicationScheme](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityconstants.applicationscheme) in the preceding example represents the scheme used to identify application authentication cookies. An empty [AuthorizationPolicyBuilder.AuthenticationSchemes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.authenticationschemes) list evaluates requirements against the default schemes—it doesn't authenticate every registered scheme.

ASP.NET Core only uses one instance of [IAuthorizationPolicyProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider). [DefaultAuthorizationPolicyProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.defaultauthorizationpolicyprovider) is the framework's default [IAuthorizationPolicyProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider) implementation for retrieving authorization policies by name. Policy provider behavior is customized by registering a custom policy provider implementation in the app's service container to replace the default implementation.

If a custom policy provider is able to explicitly match and return all of the authorization policies that the app uses, the policy provider can return `Task.FromResult<AuthorizationPolicy>(null)` from the [GetPolicyAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider.getpolicyasync) method when no policy name matches. However, most apps that implement a custom provider defer traditional policy retrieval, for example to handle role-based and claim-based policies, to the default policy provider. Such an app typically uses a custom provider that:

* Attempts to parse policy names, returning an authorization policy for a matching name with one or more requirements or assertions.
* Uses the framework's [DefaultAuthorizationPolicyProvider](/en-us/dotnet/api/microsoft.aspnetcore.authorization.defaultauthorizationpolicyprovider) by implementing [GetDefaultPolicyAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider.getdefaultpolicyasync) to provide an authorization policy for `[Authorize]` attributes that don't specify a policy name:

```
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => 
    DefaultPolicyProvider.GetDefaultPolicyAsync();
```

Any of the following [AuthorizationPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder) methods can be used for the custom policy provider's requirements and assertions in its [GetPolicyAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider.getpolicyasync) method:

* [AddRequirements](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.addrequirements)
* [RequireClaim](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireclaim)
* [RequireRole](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requirerole)
* [RequireUserName](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireusername)
* [RequireAssertion](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireassertion)
* [RequireAuthenticatedUser](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.requireauthenticateduser)

The custom policy provider must also implement [GetFallbackPolicyAsync](/en-us/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationpolicyprovider.getfallbackpolicyasync). Choosing a non-`null` fallback is optional. The following example retrieves the fallback authorization policy by delegating to the default authorization policy provider's implementation:

```
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => 
    DefaultPolicyProvider.GetFallbackPolicyAsync();
```

The default policy or fallback policy can [combine policies](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicy.combine), which is useful, for example, when middleware-based authorization flows don't specify per-endpoint authorization. All combined requirements must succeed for the combined policy to succeed.

In the following example, the policy combines:

* A standard user policy that requires an authenticated user with a `Status` claim of `Active`.
* A manager policy that requires the `Manager` role with a `Department` claim of `Sales`.

```
var standardUserPolicy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .RequireClaim("Status", "Active")
    .Build();

var managerPolicy = new AuthorizationPolicyBuilder()
    .RequireRole("Manager")
    .RequireClaim("Department", "Sales")
    .Build();

var combinedPolicy = AuthorizationPolicy.Combine(standardUserPolicy, managerPolicy);

return Task.FromResult<AuthorizationPolicy?>(combinedPolicy);
```

## Custom authorization attribute

The recommended approach for applying policies in concert with a custom policy provider is to use a strongly-typed [AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute). A custom attribute implementation maps arguments into a string that's used to retrieve a corresponding authorization policy.

The following `MinimumAgeAuthorizeAttribute` example derives from [AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) and makes the `Age` property wrap the [AuthorizeAttribute.Policy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute.policy) property. The attribute type has a policy string based on the hard-coded prefix (`MinimumAge`) and an integer passed in via its constructor (`MinimumAge{AGE}`), where the `{AGE}` placeholder is the minimum age, for example, `MinimumAge21`. The following attribute is used with the custom policy provider shown later in this article in the [Minimum age custom policy provider example](#minimum-age-custom-policy-provider-example) section.

`Policies/Attributes/MinimumAgeAuthorizeAttribute.cs`:

```
using Microsoft.AspNetCore.Authorization;

namespace BlazorWebAppAuthorization.Policies.Attributes;

public class MinimumAgeAuthorizeAttribute : AuthorizeAttribute
{
    private const string PolicyPrefix = "MinimumAge";

    public MinimumAgeAuthorizeAttribute(int age) => Age = age;

    public int Age
    {
        get
        {
            if (!string.IsNullOrEmpty(Policy) &&
                Policy.StartsWith(PolicyPrefix, 
                    StringComparison.OrdinalIgnoreCase) &&
                int.TryParse(Policy.AsSpan(PolicyPrefix.Length), out var age))
            {
                return age;
            }

            return default;
        }
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegative(value);
            Policy = $"{PolicyPrefix}{value}";
        }
    }
}
```

You can apply the attribute for any given authorized minimum age with an integer parameter for the age. Examples are shown later in this article in the [Use policies from a custom policy provider](#use-policies-from-a-custom-policy-provider) section.

Important

As with all policy-based authorization scenarios, create a requirement and an authorization handler for the policy. Register the handler in the app's service container.

For examples that work with the custom policy provider in this article, see the parameterized `MinimumAgeRequirement` and `MinimumAgeHandler` code in the *Policy-based authorization* article, which work with the `MinimumAgePolicyProvider` demonstrated in this article:

* [`MinimumAgeRequirement(int)` parameterized class that accepts a minimum age](policies?view=aspnetcore-10.0#requirements-and-policy-registration)
* [`MinimumAgeHandler` class for one requirement](policies?view=aspnetcore-10.0#use-a-handler-for-one-requirement)
* [Register the `MinimumAgeHandler`](policies?view=aspnetcore-10.0#handler-registration)

## Minimum age custom policy provider example

Consider a situation where authorization is based on a user's minimum age and the authorization policy names follow the pattern `MinimumAge{AGE}`, where the `{AGE}` placeholder is a string representation of an integer age. This is the same naming scheme established for the `MinimumAgeAuthorizeAttribute` earlier in this article in the [Custom authorization attribute](#custom-authorization-attribute) section.

The custom policy provider should generate authorization policies by completing the following tasks:

* The age is parsed from the policy name.
* An authorization policy builder ([AuthorizationPolicyBuilder](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder)) creates a new [AuthorizationPolicy](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicy).
* The authorization policy builder is constructed with at least one authorization scheme name or always succeeds. Otherwise, there's no information on how to provide a challenge to the user and an exception is thrown.
* Set the list of authentication schemes in [AuthorizationPolicyBuilder.AuthenticationSchemes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.authenticationschemes) for the built policy. The following example passes [IdentityConstants.ApplicationScheme](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityconstants.applicationscheme), which represents the scheme used to identify application authentication cookies. An empty (unassigned) [AuthenticationSchemes](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.authenticationschemes) list evaluates requirements against the custom policy provider's default schemes—it doesn't authenticate every registered scheme.
* Add one or more requirements to the policy for user age evaluations with [AddRequirements](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder.addrequirements).

The following example demonstrates a minimum age custom policy provider.

`Policies/Providers/MinimumAgePolicyProvider.cs`:

```
using BlazorWebAppAuthorization.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

namespace BlazorWebAppAuthorization.Policies.Providers;

public class MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options) 
    : IAuthorizationPolicyProvider
{
    private const string PolicyPrefix = "MinimumAge";

    private DefaultAuthorizationPolicyProvider DefaultPolicyProvider { get; } = 
        new(options);

    public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        if (policyName.StartsWith(
                PolicyPrefix, StringComparison.OrdinalIgnoreCase) &&
            int.TryParse(policyName.AsSpan(PolicyPrefix.Length), out var age) &&
            age >= 0)
        {
            var policy = new AuthorizationPolicyBuilder(
                IdentityConstants.ApplicationScheme);
            policy.AddRequirements(new MinimumAgeRequirement(age));

            return Task.FromResult<AuthorizationPolicy?>(policy.Build());
        }

        return DefaultPolicyProvider.GetPolicyAsync(policyName);
    }

    public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => 
        DefaultPolicyProvider.GetDefaultPolicyAsync();

    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => 
        DefaultPolicyProvider.GetFallbackPolicyAsync();
}
```

## Use policies from a custom policy provider

To use custom policies:

* As with all policy-based authorization scenarios, create a requirement and an authorization handler for the policy. Register the handler in the app's service container.

  For examples that work with the custom policy provider in this article, see the parameterized `MinimumAgeRequirement` and `MinimumAgeHandler` code in the *Policy-based authorization* article, which work with the `MinimumAgePolicyProvider` demonstrated in this article:

  * [`MinimumAgeRequirement(int)` parameterized class that accepts a minimum age](policies?view=aspnetcore-10.0#requirements-and-policy-registration)
  * [`MinimumAgeHandler` class for one requirement](policies?view=aspnetcore-10.0#use-a-handler-for-one-requirement)
  * [Register the `MinimumAgeHandler`](policies?view=aspnetcore-10.0#handler-registration)

- Register the custom policy provider type to replace the default policy provider.

  In the app's `Program` file:

  ```
  builder.Services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
  ```

* Register the custom policy provider type to replace the default policy provider.

  In `Startup.ConfigureServices` of the `Startup.cs` file:

  ```
  services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
  ```

The [sample app](#sample-app) institutes application cookie redirect mapping with the following code:

* Anonymous Users: When an unauthenticated user requests a protected endpoint, the cookie handler issues a challenge and redirects to [CookieAuthenticationOptions.LoginPath](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieauthenticationoptions.loginpath) with a return URL parameter.
* Underage Users: When an authenticated user fails the custom minimum age policy, the handler issues a forbid response and redirects to [CookieAuthenticationOptions.AccessDeniedPath](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookieauthenticationoptions.accessdeniedpath) instead of the login page.

```
builder.Services.ConfigureApplicationCookie(options =>
{
    options.LoginPath = "/Account/Login";
    options.AccessDeniedPath = "/Account/AccessDenied";
});
```

For demonstration purposes, an `AuthorizeView` component can specify the weakly-typed `MinimumAge21` (`"MinimumAge" + Age`) policy, as the following sample Razor component demonstrates. Using a weakly-typed policy name isn't the best approach for applying a custom authorization policy. After the following example, a strongly-typed [AuthorizeAttribute](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) is demonstrated using the [`MinimumAgeAuthorizeAttribute` implementation](#custom-authorization-attribute) described in the [Custom authorization attribute](#custom-authorization-attribute) section.

`Components/Pages/PassMinimumAge21Policy.razor`:

```
@page "/pass-minimumage21-policy"

<h1>Pass 'MinimumAge21' policy (weakly-typed approach)</h1>

<p>
    Uses an AuthorizeView component to apply the policy using the policy's name. 
    This approach is shown for demonstration purposes and isn't recommended for 
    production code.
</p>

<AuthorizeView Policy="MinimumAge21">
    <Authorized>
        <p>You satisfy the 'MinimumAge21' policy.</p>
    </Authorized>
    <NotAuthorized>
        <p>You <b>don't</b> satisfy the 'MinimumAge21' policy.</p>
    </NotAuthorized>
</AuthorizeView>
```

The following component uses the strongly-typed [custom `MinimumAgeAuthorizeAttribute` implementation](#custom-authorization-attribute) described in the [Custom authorization attribute](#custom-authorization-attribute) section. Using a strongly-typed attribute is recommended for production apps.

`Components/Pages/PassMinimumAge21PolicyWithAttribute.razor`:

```
@page "/pass-minimumage21-policy-with-attribute"
@using BlazorWebAppAuthorization.Policies.Attributes
@attribute [MinimumAgeAuthorize(21)]

<h1>Pass 'MinimumAge21' policy (strongly-typed approach)</h1>

<p>
    Applies the policy to the Razor component with a custom 
    [MinimumAgeAuthorize] attribute (derived from AuthorizeAttribute).
    This approach is preferred for production code, as it's strongly-typed
    and avoids the use of a string to set the policy and minimum age.
</p>

<p>You satisfy the 'MinimumAge21' policy.</p>
```

The same approach is useful for securing [Minimal API endpoints](../../fundamentals/minimal-apis?view=aspnetcore-10.0#authorization):

```
app.MapGet("/must-be-21", [MinimumAgeAuthorize(21)] () => 
    "This endpoint requires a 21-year-old birthdate claim.");
```

## Related content

* [Complete custom 'IAuthorizationPolicyProvider' sample (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/tree/v3.1.3/src/Security/samples/CustomPolicyProvider)
* [Policy-based authorization in ASP.NET Core](policies?view=aspnetcore-10.0)
* [Policy-based authorization in ASP.NET Core Razor Pages](../../razor-pages/security/authorization/policies?view=aspnetcore-10.0)
* [Policy-based authorization in ASP.NET Core MVC](../../mvc/security/authorization/policies?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/razor-pages-start.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Get started with Razor Pages in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Rick Anderson](https://twitter.com/RickAndMSFT)

This tutorial is the first in a series that teaches the basics of building an ASP.NET Core Razor Pages web app.

For a more advanced introduction aimed at developers who are familiar with controllers and views, see [Introduction to Razor Pages](../../razor-pages/?view=aspnetcore-10.0). For a video introduction, see [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s).

If you're new to ASP.NET Core development and are unsure of which ASP.NET Core web UI solution will best fit your needs, see [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0).

At the end of this tutorial, you have a Razor Pages web app that manages a database of movies.

## Create a Razor Pages web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio and select **Create a new project**.

- In the **Create a new project** dialog, select **ASP.NET Core Web App (Razor Pages)** > **Next**.

- In the **Configure your new project** dialog, enter `RazorPagesMovie` for **Project name**. Name the project **RazorPagesMovie**, including matching the capitalization, so the namespaces match when you copy and paste example code.

- Select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 10.0**.
  * Verify: **Do not use top-level statements** is unchecked.

- Select **Create**.

  The following starter project is created:

For alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

This tutorial assumes you're familiar with Visual Studio Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that contains the project.

* Run the following commands:

  ```
  dotnet new webapp -o RazorPagesMovie
  code -r RazorPagesMovie
  ```

  The `dotnet new` command creates a new Razor Pages project in the *RazorPagesMovie* folder.

  The `code` command opens the *RazorPagesMovie* folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

## Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Select **RazorPagesMovie** in **Solution Explorer**, and then press `Ctrl`+`F5` to run the app without the debugger.

Visual Studio displays the following dialog when a project isn't yet configured to use SSL:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio:

* Runs the app, which launches the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).
* Launches the default browser at `https://localhost:<port>`, which displays the app's UI. `<port>` is the random port that is assigned when the app was created.

Close the browser window.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

In Visual Studio Code, press `Ctrl`+`F5` to run the app without debugging.

At the **Select debugger** prompt, select **C#**.

At the **Select Launch Configuration** prompt, select **C#: RazorPagesMovie \[https] RazorPagesMovie**.

The default browser opens with the following URL: `https://localhost:<port>` where `<port>` is the randomly generated port number.

Close the browser window.

In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

## Examine the project files

The following sections contain an overview of the main project folders and files that you work with in later tutorials.

### Pages folder

Contains Razor pages and supporting files. Each Razor page is a pair of files:

* A `.cshtml` file that has HTML markup with C# code by using Razor syntax.
* A `.cshtml.cs` file that has C# code that handles page events.

Supporting files have names that begin with an underscore. For example, the `_Layout.cshtml` file configures UI elements common to all pages. `_Layout.cshtml` sets up the navigation menu at the top of the page and the copyright notice at the bottom of the page. For more information, see [Layout in ASP.NET Core](../../mvc/views/layout?view=aspnetcore-10.0).

### wwwroot folder

Contains static assets, like HTML files, JavaScript files, and CSS files. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

### `appsettings.json`

Contains configuration data, like connection strings. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

### Program.cs

Contains the following code:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages()
   .WithStaticAssets();

app.Run();
```

The following lines of code in this file create a `WebApplicationBuilder` with preconfigured defaults, add Razor Pages support to the [Dependency Injection (DI) container](../../fundamentals/dependency-injection?view=aspnetcore-10.0), and build the app:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();
```

The developer exception page is enabled by default and provides helpful information on exceptions. Don't run production apps in development mode because the developer exception page can leak sensitive information.

The following code sets the exception endpoint to `/Error` and enables [HTTP Strict Transport Security (HSTS) protocol](../../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) when the app is ***not*** running in development mode:

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

For example, the preceding code runs when the app is in production or test mode. For more information, see [Use multiple environments in ASP.NET Core](../../fundamentals/environments?view=aspnetcore-10.0).

The following code enables various [Middleware](../../fundamentals/middleware/?view=aspnetcore-10.0):

* `app.UseHttpsRedirection();` : Redirects HTTP requests to HTTPS.
* `app.UseRouting();` : Adds route matching to the middleware pipeline. For more information, see [Routing in ASP.NET Core](../../fundamentals/routing?view=aspnetcore-10.0).
* `app.UseAuthorization();` : Authorizes a user to access secure resources. This app doesn't use authorization, so you can remove this line.
* `app.MapRazorPages();`: Configures endpoint routing for Razor Pages.
* `app.MapStaticAssets()` : Optimizes the delivery of static assets in an app, such as HTML, CSS, images, and JavaScript. For more information, see [What's new in ASP.NET Core in .NET 9](../../release-notes/aspnetcore-9.0?view=aspnetcore-10.0#optimizing-static-web-asset-delivery).
* `.WithStaticAssets();` : Ensures Razor Pages participate in the optimization system for static assets.
* `app.Run();` : Runs the app.

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie10) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Next: Add a model](model?view=aspnetcore-10.0)

This is the first tutorial of a series that teaches the basics of building an ASP.NET Core Razor Pages web app.

For a more advanced introduction aimed at developers who are familiar with controllers and views, see [Introduction to Razor Pages](../../razor-pages/?view=aspnetcore-10.0). For a video introduction, see [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s).

If you're new to ASP.NET Core development and are unsure of which ASP.NET Core web UI solution will best fit your needs, see [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0).

At the end of this tutorial, you'll have a Razor Pages web app that manages a database of movies.

## Create a Razor Pages web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio and select **New project**.

- In the **Create a new project** dialog, select **ASP.NET Core Web App (Razor Pages)** > **Next**.

- In the **Configure your new project** dialog, enter `RazorPagesMovie` for **Project name**. It's important to name the project **RazorPagesMovie**, including matching the capitalization, so the namespaces will match when you copy and paste example code.

- Select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 9.0**.
  * Verify: **Do not use top-level statements** is unchecked.

- Select **Create**.

  The following starter project is created:

For alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new webapp -o RazorPagesMovie
  code -r RazorPagesMovie
  ```

  The `dotnet new` command creates a new Razor Pages project in the *RazorPagesMovie* folder.

  The `code` command opens the *RazorPagesMovie* folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

## Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Select **RazorPagesMovie** in **Solution Explorer**, and then press `Ctrl`+`F5` to run the app without the debugger.

Visual Studio displays the following dialog when a project is not yet configured to use SSL:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio:

* Runs the app, which launches the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).
* Launches the default browser at `https://localhost:<port>`, which displays the apps UI. `<port>` is the random port that is assigned when the app was created.

Close the browser window.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

In Visual Studio Code, press `Ctrl`+`F5` to run the app without debugging.

At the **Select debugger** prompt, select **C#**.

At the **Select Launch Configuration** prompt, select **C#: RazorPagesMovie \[https] RazorPagesMovie**.

The default browser launched with the following URL: `https://localhost:<port>` where `<port>` is the randomly generated port number.

Close the browser window.

In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

## Examine the project files

The following sections contain an overview of the main project folders and files that you'll work with in later tutorials.

### Pages folder

Contains Razor pages and supporting files. Each Razor page is a pair of files:

* A `.cshtml` file that has HTML markup with C# code using Razor syntax.
* A `.cshtml.cs` file that has C# code that handles page events.

Supporting files have names that begin with an underscore. For example, the `_Layout.cshtml` file configures UI elements common to all pages. `_Layout.cshtml` sets up the navigation menu at the top of the page and the copyright notice at the bottom of the page. For more information, see [Layout in ASP.NET Core](../../mvc/views/layout?view=aspnetcore-10.0).

### wwwroot folder

Contains static assets, like HTML files, JavaScript files, and CSS files. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

### `appsettings.json`

Contains configuration data, like connection strings. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

### Program.cs

Contains the following code:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages();

app.Run();
```

The following lines of code in this file create a `WebApplicationBuilder` with preconfigured defaults, add Razor Pages support to the [Dependency Injection (DI) container](../../fundamentals/dependency-injection?view=aspnetcore-10.0), and builds the app:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();
```

The developer exception page is enabled by default and provides helpful information on exceptions. Production apps should not be run in development mode because the developer exception page can leak sensitive information.

The following code sets the exception endpoint to `/Error` and enables [HTTP Strict Transport Security (HSTS) protocol](../../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) when the app is ***not*** running in development mode:

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

For example, the preceding code runs when the app is in production or test mode. For more information, see [Use multiple environments in ASP.NET Core](../../fundamentals/environments?view=aspnetcore-10.0).

The following code enables various [Middleware](../../fundamentals/middleware/?view=aspnetcore-10.0):

* `app.UseHttpsRedirection();` : Redirects HTTP requests to HTTPS.
* `app.UseRouting();` : Adds route matching to the middleware pipeline. For more information, see [Routing in ASP.NET Core](../../fundamentals/routing?view=aspnetcore-10.0).
* `app.UseAuthorization();` : Authorizes a user to access secure resources. This app doesn't use authorization, therefore this line could be removed.
* `app.MapRazorPages();`: Configures endpoint routing for Razor Pages.
* `app.MapStaticAssets();` : Optimize the delivery of static assets in an app, such as HTML, CSS, images, and JavaScript. For more information, see [What's new in ASP.NET Core in .NET 9](../../release-notes/aspnetcore-9.0?view=aspnetcore-10.0#optimizing-static-web-asset-delivery).
* `app.Run();` : Runs the app.

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie90) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Next: Add a model](model?view=aspnetcore-10.0)

This is the first tutorial of a series that teaches the basics of building an ASP.NET Core Razor Pages web app.

For a more advanced introduction aimed at developers who are familiar with controllers and views, see [Introduction to Razor Pages](../../razor-pages/?view=aspnetcore-10.0). For a video introduction, see [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s).

If you're new to ASP.NET Core development and are unsure of which ASP.NET Core web UI solution will best fit your needs, see [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0).

At the end of this tutorial, you'll have a Razor Pages web app that manages a database of movies.

## Create a Razor Pages web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

- Start Visual Studio and select **New project**.

- In the **Create a new project** dialog, select **ASP.NET Core Web App (Razor Pages)** > **Next**.

- In the **Configure your new project** dialog, enter `RazorPagesMovie` for **Project name**. It's important to name the project **RazorPagesMovie**, including matching the capitalization, so the namespaces will match when you copy and paste example code.

- Select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 8.0 (Long Term Support)**.
  * Verify: **Do not use top-level statements** is unchecked.

- Select **Create**.

  The following starter project is created:

For alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new webapp -o RazorPagesMovie
  code -r RazorPagesMovie
  ```

  The `dotnet new` command creates a new Razor Pages project in the *RazorPagesMovie* folder.

  The `code` command opens the *RazorPagesMovie* folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

## Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

Select **RazorPagesMovie** in **Solution Explorer**, and then press `Ctrl`+`F5` to run the app without the debugger.

Visual Studio displays the following dialog when a project is not yet configured to use SSL:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio:

* Runs the app, which launches the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).
* Launches the default browser at `https://localhost:<port>`, which displays the apps UI. `<port>` is the random port that is assigned when the app was created.

Close the browser window.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

In Visual Studio Code, press `Ctrl`+`F5` to run the app without debugging.

At the **Select debugger** prompt, select **.NET 5+ and .NET Core**.

The default browser launched with the following URL: `https://localhost:<port>` where `<port>` is the randomly generated port number.

Close the browser window.

In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

## Examine the project files

The following sections contain an overview of the main project folders and files that you'll work with in later tutorials.

### Pages folder

Contains Razor pages and supporting files. Each Razor page is a pair of files:

* A `.cshtml` file that has HTML markup with C# code using Razor syntax.
* A `.cshtml.cs` file that has C# code that handles page events.

Supporting files have names that begin with an underscore. For example, the `_Layout.cshtml` file configures UI elements common to all pages. `_Layout.cshtml` sets up the navigation menu at the top of the page and the copyright notice at the bottom of the page. For more information, see [Layout in ASP.NET Core](../../mvc/views/layout?view=aspnetcore-10.0).

### wwwroot folder

Contains static assets, like HTML files, JavaScript files, and CSS files. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

### `appsettings.json`

Contains configuration data, like connection strings. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

### Program.cs

Contains the following code:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The following lines of code in this file create a `WebApplicationBuilder` with preconfigured defaults, add Razor Pages support to the [Dependency Injection (DI) container](../../fundamentals/dependency-injection?view=aspnetcore-10.0), and builds the app:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();
```

The developer exception page is enabled by default and provides helpful information on exceptions. Production apps should not be run in development mode because the developer exception page can leak sensitive information.

The following code sets the exception endpoint to `/Error` and enables [HTTP Strict Transport Security (HSTS) protocol](../../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) when the app is ***not*** running in development mode:

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

For example, the preceding code runs when the app is in production or test mode. For more information, see [Use multiple environments in ASP.NET Core](../../fundamentals/environments?view=aspnetcore-10.0).

The following code enables various [Middleware](../../fundamentals/middleware/?view=aspnetcore-10.0):

* `app.UseHttpsRedirection();` : Redirects HTTP requests to HTTPS.
* `app.UseStaticFiles();` : Enables static files, such as HTML, CSS, images, and JavaScript to be served. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).
* `app.UseRouting();` : Adds route matching to the middleware pipeline. For more information, see [Routing in ASP.NET Core](../../fundamentals/routing?view=aspnetcore-10.0)
* `app.MapRazorPages();`: Configures endpoint routing for Razor Pages.
* `app.UseAuthorization();` : Authorizes a user to access secure resources. This app doesn't use authorization, therefore this line could be removed.
* `app.Run();` : Runs the app.

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie80) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Next: Add a model](model?view=aspnetcore-10.0)

This is the first tutorial of a series that teaches the basics of building an ASP.NET Core Razor Pages web app.

For a more advanced introduction aimed at developers who are familiar with controllers and views, see [Introduction to Razor Pages](../../razor-pages/?view=aspnetcore-10.0). For a video introduction, see [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s).

If you're new to ASP.NET Core development and are unsure of which ASP.NET Core web UI solution will best fit your needs, see [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0).

At the end of this tutorial, you'll have a Razor Pages web app that manages a database of movies.

## Create a Razor Pages web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- Start Visual Studio and select **Create a new project**.

- In the **Create a new project** dialog, select **ASP.NET Core Web App** > **Next**.

- In the **Configure your new project** dialog, enter `RazorPagesMovie` for **Project name**. It's important to name the project **RazorPagesMovie**, including matching the capitalization, so the namespaces will match when you copy and paste example code.

- Select **Next**.

- In the **Additional information** dialog:

  * Select **.NET 7.0 (Standard Term Support)**.
  * Verify: **Do not use top-level statements** is unchecked.

- Select **Create**.

  The following starter project is created:

For alternative approaches to create the project, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new webapp -o RazorPagesMovie
  code -r RazorPagesMovie
  ```

  The `dotnet new` command creates a new Razor Pages project in the *RazorPagesMovie* folder.

  The `code` command opens the *RazorPagesMovie* project folder in the current instance of Visual Studio Code.

  * Select **Web and Console** > **App** > **Web Application**.
  * Select **Continue**.

* In the **Configure your new Web Application** dialog:

  * Verify: **Target framework** is set to **.NET 7.0** (or later).
  * Verify: **Authentication** is set to **No Authentication**.
  * Verify: **Do not use top-level statements** is unchecked.
  * Select **Continue**.

* In the **Configure your new Web Application** dialog:

  * Enter `RazorPagesMovie` for **Project name**. It's important to name the project **RazorPagesMovie**, including matching the capitalization, so the namespaces will match when you copy and paste example code.
  * Select **Create**.

## Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

Select **RazorPagesMovie** in **Solution Explorer**, and then press `Ctrl`+`F5` to run the app without the debugger.

Visual Studio displays the following dialog when a project is not yet configured to use SSL:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio:

* Runs the app, which launches the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).
* Launches the default browser at `https://localhost:<port>`, which displays the apps UI. `<port>` is the random port that is assigned when the app was created.

Close the browser window.

* Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

* Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

In Visual Studio Code, press `Ctrl`+`F5` to run the app without debugging.

At the **Select debugger** prompt, select **.NET 5+ and .NET Core**.

The default browser launched with the following URL: `https://localhost:<port>` where `<port>` is the randomly generated port number.

Close the browser window.

In Visual Studio Code, from the *Run* menu, select *Stop Debugging* or press `Shift`+`F5` to stop the app.

Select **Debug** > **Start Debugging** to launch the app.

Visual Studio for Mac launches a browser and navigates to `https://localhost:<port>`, where `<port>` is the port number randomly assigned at project creation and is set in `Properties/launchSettings.json`.

Close the browser window.

## Examine the project files

The following sections contain an overview of the main project folders and files that you'll work with in later tutorials.

### Pages folder

Contains Razor pages and supporting files. Each Razor page is a pair of files:

* A `.cshtml` file that has HTML markup with C# code using Razor syntax.
* A `.cshtml.cs` file that has C# code that handles page events.

Supporting files have names that begin with an underscore. For example, the `_Layout.cshtml` file configures UI elements common to all pages. `_Layout.cshtml` sets up the navigation menu at the top of the page and the copyright notice at the bottom of the page. For more information, see [Layout in ASP.NET Core](../../mvc/views/layout?view=aspnetcore-10.0).

### wwwroot folder

Contains static assets, like HTML files, JavaScript files, and CSS files. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

### `appsettings.json`

Contains configuration data, like connection strings. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

### Program.cs

Contains the following code:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The following lines of code in this file create a `WebApplicationBuilder` with preconfigured defaults, add Razor Pages support to the [Dependency Injection (DI) container](../../fundamentals/dependency-injection?view=aspnetcore-10.0), and builds the app:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();
```

The developer exception page is enabled by default and provides helpful information on exceptions. Production apps should not be run in development mode because the developer exception page can leak sensitive information.

The following code sets the exception endpoint to `/Error` and enables [HTTP Strict Transport Security (HSTS) protocol](../../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) when the app is ***not*** running in development mode:

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

For example, the preceding code runs when the app is in production or test mode. For more information, see [Use multiple environments in ASP.NET Core](../../fundamentals/environments?view=aspnetcore-10.0).

The following code enables various [Middleware](../../fundamentals/middleware/?view=aspnetcore-10.0):

* `app.UseHttpsRedirection();` : Redirects HTTP requests to HTTPS.
* `app.UseStaticFiles();` : Enables static files, such as HTML, CSS, images, and JavaScript to be served. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).
* `app.UseRouting();` : Adds route matching to the middleware pipeline. For more information, see [Routing in ASP.NET Core](../../fundamentals/routing?view=aspnetcore-10.0)
* `app.MapRazorPages();`: Configures endpoint routing for Razor Pages.
* `app.UseAuthorization();` : Authorizes a user to access secure resources. This app doesn't use authorization, therefore this line could be removed.
* `app.Run();` : Runs the app.

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie70) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Next: Add a model](model?view=aspnetcore-10.0)

This is the first tutorial of a series that teaches the basics of building an ASP.NET Core Razor Pages web app.

For a more advanced introduction aimed at developers who are familiar with controllers and views, see [Introduction to Razor Pages](../../razor-pages/?view=aspnetcore-10.0). For a video introduction, see [Entity Framework Core for Beginners](https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXCPdC3fTFA3Z79-eVH3K-s).

If you're new to ASP.NET Core development and are unsure of which ASP.NET Core web UI solution will best fit your needs, see [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0).

At the end of the series, you'll have an app that manages a database of movies.

In this tutorial, you:

* Create a Razor Pages web app.
* Run the app.
* Examine the project files.

At the end of this tutorial, you'll have a working Razor Pages web app that you'll enhance in later tutorials.

## Create a Razor Pages web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

1. Start Visual Studio 2022 and select **Create a new project**.

2. In the **Create a new project** dialog, select **ASP.NET Core Web App**, and then select **Next**.

3. In the **Configure your new project** dialog, enter `RazorPagesMovie` for **Project name**. It's important to name the project *RazorPagesMovie*, including matching the capitalization, so the namespaces will match when you copy and paste example code.

4. Select **Next**.

5. In the **Additional information** dialog, select **.NET 6.0 (Long-term support)** and then select **Create**.

The following starter project is created:

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new webapp -o RazorPagesMovie
  code -r RazorPagesMovie
  ```

  The `dotnet new` command creates a new Razor Pages project in the *RazorPagesMovie* folder.

  The `code` command opens the *RazorPagesMovie* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

1. Select **File** > **New Solution**.

2. In Visual Studio 2022 for Mac select **Web and Console** > **App** > **Web Application** > **Next**.

3. In the **Configure your new Web Application** dialog:

   1. Confirm that **Target framework** is set to the latest .NET 6.x version.
   2. Confirm that **Authentication** is set to **No Authentication**.
   3. Select **Next**.

4. Name the project *RazorPagesMovie* and select **Create**.

## Run the app

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)

Select **RazorPagesMovie** in **Solution Explorer**, and then press Ctrl+F5 to run without the debugger.

Visual Studio displays the following dialog when a project is not yet configured to use SSL:

Select **Yes** if you trust the IIS Express SSL certificate.

The following dialog is displayed:

Select **Yes** if you agree to trust the development certificate.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

Visual Studio:

* Runs the app, which launches the [Kestrel server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0).
* Launches the default browser at `https://localhost:5001`, which displays the apps UI.

- Trust the HTTPS development certificate by running the following command:

  ```
  dotnet dev-certs https --trust
  ```

  The preceding command requires .NET 9 or later SDK on Linux. For Linux on .NET 8.0.401 or earlier SDK, see your Linux distribution's documentation for trusting a certificate.

  The preceding command displays the following dialog, provided the certificate was not previously trusted:

- Select **Yes** if you agree to trust the development certificate.

  For more information, see the **Trust the ASP.NET Core HTTPS development certificate** section of the [Enforcing SSL](../../security/enforcing-ssl?view=aspnetcore-10.0) article.

For information on trusting the Firefox browser, see [Firefox SEC\_ERROR\_INADEQUATE\_KEY\_USAGE certificate error](../../security/enforcing-ssl?view=aspnetcore-10.0#trust-ff).

In Visual Studio Code, press Ctrl+F5 to run the app. At the **Select environment** prompt, select **.NET Core**.

The default browser launched with the following URL: `https://localhost:5001`

Select **Debug** > **Start Debugging** to launch the app. Visual Studio for Mac launches a browser and navigates to `https://localhost:<port>`, where `<port>` is the port number randomly assigned at project creation and is set in `Properties/launchSettings.json`.

## Examine the project files

The following sections contain an overview of the main project folders and files that you'll work with in later tutorials.

### Pages folder

Contains Razor pages and supporting files. Each Razor page is a pair of files:

* A `.cshtml` file that has HTML markup with C# code using Razor syntax.
* A `.cshtml.cs` file that has C# code that handles page events.

Supporting files have names that begin with an underscore. For example, the `_Layout.cshtml` file configures UI elements common to all pages. This file sets up the navigation menu at the top of the page and the copyright notice at the bottom of the page. For more information, see [Layout in ASP.NET Core](../../mvc/views/layout?view=aspnetcore-10.0).

### wwwroot folder

Contains static assets, like HTML files, JavaScript files, and CSS files. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

### `appsettings.json`

Contains configuration data, like connection strings. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

### Program.cs

Contains the following code:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The following lines of code in this file create a `WebApplicationBuilder` with preconfigured defaults, add Razor Pages support to the [Dependency Injection (DI) container](../../fundamentals/dependency-injection?view=aspnetcore-10.0), and build the app:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();
```

The developer exception page is enabled by default and provides helpful information on exceptions. Production apps should not be run in development mode because the developer exception page can leak sensitive information.

The following code sets the exception endpoint to `/Error` and enables [HTTP Strict Transport Security (HSTS) protocol](../../security/enforcing-ssl?view=aspnetcore-10.0#http-strict-transport-security-hsts-protocol) when the app is ***not*** running in development mode:

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

For example, the preceding code runs when the app is in production or test mode. For more information, see [Use multiple environments in ASP.NET Core](../../fundamentals/environments?view=aspnetcore-10.0).

The following code enables various [Middleware](../../fundamentals/middleware/?view=aspnetcore-10.0):

* `app.UseHttpsRedirection();` : Redirects HTTP requests to HTTPS.
* `app.UseStaticFiles();` : Enables static files, such as HTML, CSS, images, and JavaScript to be served. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).
* `app.UseRouting();` : Adds route matching to the middleware pipeline. For more information, see [Routing in ASP.NET Core](../../fundamentals/routing?view=aspnetcore-10.0)
* `app.MapRazorPages();`: Configures endpoint routing for Razor Pages.
* `app.UseAuthorization();` : Authorizes a user to access secure resources. This app doesn't use authorization, therefore this line could be removed.
* `app.Run();` : Runs the app.

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie60) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Next: Add a model](model?view=aspnetcore-10.0)

This is the first tutorial of a series that teaches the basics of building an ASP.NET Core Razor Pages web app.

For a more advanced introduction aimed at developers who are familiar with controllers and views, see [Introduction to Razor Pages](../../razor-pages/?view=aspnetcore-10.0).

If you're new to ASP.NET Core development and are unsure of which ASP.NET Core web UI solution will best fit your needs, see [Choose an ASP.NET Core UI](../choose-web-ui?view=aspnetcore-10.0).

At the end of the series, you'll have an app that manages a database of movies.

In this tutorial, you:

* Create a Razor Pages web app.
* Run the app.
* Examine the project files.

At the end of this tutorial, you'll have a working Razor Pages web app that you'll enhance in later tutorials.

## Create a Razor Pages web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

1. Start Visual Studio and select **Create a new project**. For more information, see [Create a new project in Visual Studio](/en-us/visualstudio/ide/create-new-project).

2. In the **Create a new project** dialog, select **ASP.NET Core Web Application**, and then select **Next**.

3. In the **Configure your new project** dialog, enter `RazorPagesMovie` for **Project name**. It's important to name the project *RazorPagesMovie*, including matching the capitalization, so the namespaces will match when you copy and paste example code.

4. Select **Create**.

5. In the **Create a new ASP.NET Core web application** dialog, select:

   1. **.NET Core** and **ASP.NET Core 5.0** in the dropdowns.
   2. **Web Application**.
   3. **Create**.

The following starter project is created:

The tutorial assumes familiarity with VS Code. For more information, see [Getting started with VS Code](https://code.visualstudio.com/docs).

* Select **New Terminal** from the **Terminal** menu to open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) that will contain the project.

* Run the following commands:

  ```
  dotnet new webapp -o RazorPagesMovie
  code -r RazorPagesMovie
  ```

  The `dotnet new` command creates a new Razor Pages project in the *RazorPagesMovie* folder.

  The `code` command opens the *RazorPagesMovie* project folder in the current instance of Visual Studio Code.

Visual Studio Code might display a dialog box that asks: **Do you trust the authors of the files in this folder?**

* If you trust all files in the parent folder, select **Trust the authors of all files in the parent folder**.
* Select **Yes, I trust the authors** since the project folder has files generated by .NET.
* When Visual Studio Code requests that you add assets to build and debug the project, select **Yes**. If Visual Studio Code doesn't offer to add build and debug assets, select **View** > **Command Palette** and type "`.NET`" into the search box. From the list of commands, select the `.NET: Generate Assets for Build and Debug` command.

Visual Studio Code adds a `.vscode` folder with generated `launch.json` and `tasks.json` files.

1. Select **File** > **New Solution**.

1) In Visual Studio for Mac earlier than version 8.6, select **.NET Core** > **App** > **Web Application** > **Next**. In version 8.6 or later, select **Web and Console** > **App** > **Web Application** > **Next**.

1. In the **Configure the new Web Application** dialog:

   1. Confirm that **Authentication** is set to **No Authentication**.
   2. If presented an option to select a **Target Framework**, select the latest .NET 5.x version.
   3. Select **Next**.

2. Name the project *RazorPagesMovie* and select **Create**.

## Run the app

  ```
  dotnet dev-certs https --trust
  ```

## Examine the project files

Here's an overview of the main project folders and files that you'll work with in later tutorials.

### Pages folder

Contains Razor pages and supporting files. Each Razor page is a pair of files:

* A `.cshtml` file that has HTML markup with C# code using Razor syntax.
* A `.cshtml.cs` file that has C# code that handles page events.

Supporting files have names that begin with an underscore. For example, the `_Layout.cshtml` file configures UI elements common to all pages. This file sets up the navigation menu at the top of the page and the copyright notice at the bottom of the page. For more information, see [Layout in ASP.NET Core](../../mvc/views/layout?view=aspnetcore-10.0).

### wwwroot folder

Contains static assets, like HTML files, JavaScript files, and CSS files. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

### `appsettings.json`

Contains configuration data, like connection strings. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

### Program.cs

Contains the entry point for the app. For more information, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0).

### Startup.cs

Contains code that configures app behavior. For more information, see [App startup in ASP.NET Core](../../fundamentals/startup?view=aspnetcore-10.0).

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Next: Add a model](model?view=aspnetcore-10.0)

This is the first tutorial of a series that teaches the basics of building an ASP.NET Core Razor Pages web app.

For a more advanced introduction aimed at developers who are familiar with controllers and views, see [Introduction to Razor Pages](../../razor-pages/?view=aspnetcore-10.0).

At the end of the series, you'll have an app that manages a database of movies.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie30) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

In this tutorial, you:

* Create a Razor Pages web app.
* Run the app.
* Examine the project files.

At the end of this tutorial, you'll have a working Razor Pages web app that you'll build on in later tutorials.

## Create a Razor Pages web app

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

- From the Visual Studio **File** menu, select **New** > **Project**.

- Create a new ASP.NET Core Web Application and select **Next**.

- Name the project **RazorPagesMovie**. It's important to name the project *RazorPagesMovie* so the namespaces will match when you copy and paste code.

- Select **ASP.NET Core 3.1** in the dropdown, **Web Application**, and then select **Create**.

The following starter project is created:

* Open the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change to the directory (`cd`) which will contain the project.

* Run the following commands:

  ```
  dotnet new webapp -o RazorPagesMovie
  code -r RazorPagesMovie
  ```

  * The `dotnet new` command creates a new Razor Pages project in the *RazorPagesMovie* folder.
  * The `code` command opens the *RazorPagesMovie* folder in the current instance of Visual Studio Code.

* After the status bar's OmniSharp flame icon turns green, a dialog asks **Required assets to build and debug are missing from 'RazorPagesMovie'. Add them?** Select **Yes**.

  A *.vscode* directory, containing `launch.json` and `tasks.json` files, is added to the project's root directory.

  If Visual Studio Code doesn't offer to add the assets automatically, see the **Linux** operating system guidance in [Tooling for ASP.NET Core Blazor](../../blazor/tooling?view=aspnetcore-10.0\&pivot=linux).

- Select **File** > **New Solution**.

- In Visual Studio for Mac earlier than version 8.6, select **.NET Core** > **App** > **Web Application** > **Next**. In version 8.6 or later, select **Web and Console** > **App** > **Web Application** > **Next**.

- In the **Configure the new Web Application** dialog:

  * Confirm that **Authentication** is set to **No Authentication**.
  * If presented an option to select a **Target Framework**, select the latest 3.x version.

  Select **Next**.

- Name the project **RazorPagesMovie**, and then select **Create**.

## Run the app

  ```
  dotnet dev-certs https --trust
  ```

## Examine the project files

Here's an overview of the main project folders and files that you'll work with in later tutorials.

### Pages folder

Contains Razor pages and supporting files. Each Razor page is a pair of files:

* A `.cshtml` file that has HTML markup with C# code using Razor syntax.
* A `.cshtml.cs` file that has C# code that handles page events.

Supporting files have names that begin with an underscore. For example, the `_Layout.cshtml` file configures UI elements common to all pages. This file sets up the navigation menu at the top of the page and the copyright notice at the bottom of the page. For more information, see [Layout in ASP.NET Core](../../mvc/views/layout?view=aspnetcore-10.0).

### wwwroot folder

Contains static files, like HTML files, JavaScript files, and CSS files. For more information, see [Serve static files in ASP.NET Core apps](../../fundamentals/static-files?view=aspnetcore-10.0).

### `appSettings.json`

Contains configuration data, like connection strings. For more information, see [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

### Program.cs

Contains the entry point for the program. For more information, see [.NET Generic Host in ASP.NET Core](../../fundamentals/host/generic-host?view=aspnetcore-10.0).

### Startup.cs

Contains code that configures app behavior. For more information, see [App startup in ASP.NET Core](../../fundamentals/startup?view=aspnetcore-10.0).

## Next steps

[Next: Add a model](model?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/introduction.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core data protection overview

ASP.NET Core provides a cryptographic API to protect data, including key management and rotation.

Web apps often need to store sensitive data. The Windows data protection API ([DPAPI](/en-us/dotnet/standard/security/how-to-use-data-protection)) isn't intended for use in web apps.

The ASP.NET Core data protection stack is designed to:

* Provide a built-in solution for most web scenarios.
* Address many deficiencies of the previous encryption system.
* Serve as the replacement for the `<machineKey>` element in ASP.NET 1.x - 4.x.

## Problem statement

*I need to persist trusted information for later retrieval, but I don't trust the persistence mechanism.* In web terms, this statement might be written as *I need to round-trip trusted state via an untrusted client.*

Authenticity, integrity, and tamper-proofing are requirements. The canonical example of this scenario is an authentication cookie or bearer token. The server generates an ***I am Groot and have xyz permissions*** token and sends it to the client. The client presents that token back to the server, but the server needs some kind of assurance that the client didn't forge the token.

Confidentiality is a requirement. Because the persisted state is trusted by the server, this state might contain information that shouldn't be disclosed to an untrusted client. For example:

* A file path
* A permission
* A handle or other indirect reference
* Some server-specific data

Isolation is a requirement. Because modern apps are componentized, individual components want to take advantage of this system without regard to other components in the system. For instance, consider a bearer token component using this stack. It should operate without any interference, for example, from an anti-CSRF mechanism also using the same stack.

Some common assumptions can narrow the scope of requirements:

* All services operating within the cryptosystem are equally trusted.
* The data doesn't need to be generated or consumed outside of the services under our direct control.
* Operations must be fast because each request to the web service might go through the cryptosystem one or more times. The speed requirement makes symmetric cryptography ideal. Asymmetric cryptography isn't used until it's required.

## Design philosophy

ASP.NET Core data protection is an [easy to use](using-data-protection?view=aspnetcore-10.0) data protection stack based on the following principles:

* **Configuration should be easy**. The system strives for zero configuration. In situations where developers need to configure a specific aspect, such as the key repository, those specific configurations aren't difficult.
* **Offer basic consumer-facing APIs**. The APIs are straight forward to use correctly and difficult to use incorrectly.
* **Don't require the developer to learn the principles of managing keys**. The system handles algorithm selection and key lifetime on behalf of the developer. Developers don't have access to the raw key material, so they don't need expert knowledge of the principles.
* **Protect keys at rest as much as possible**. The system figures out an appropriate default protection mechanism and applies it automatically.

The data protection APIs aren't primarily intended for indefinite persistence of confidential payloads. Other technologies, such as [Windows CNG DPAPI](/en-us/windows/win32/seccng/cng-dpapi) and [Azure Rights Management](/en-us/purview/azure-rights-management-learn-about) are more suited to the scenario of indefinite storage. They have correspondingly strong key management capabilities. That said, the ASP.NET Core data protection APIs can be used for long-term protection of confidential data.

## Target audience

The data protection system provides APIs that target three main audiences:

* The [consumer APIs](consumer-apis/overview?view=aspnetcore-10.0) target application and framework developers.

  *I don't want to learn about how the stack operates or about how it's configured. I just want to perform some operation with high probability of using the APIs successfully.*

* The [configuration APIs](configuration/overview?view=aspnetcore-10.0) target app developers and system administrators.

  *I need to tell the data protection system that my environment requires nondefault paths or settings.*

* The [extensibility APIs](extensibility/?view=aspnetcore-10.0) target developers in charge of implementing custom policy. Usage of these APIs is limited to rare situations and developers with security experience.

  *I need to replace an entire component within the system because I have truly unique behavioral requirements. I'm willing to learn uncommonly used parts of the API surface so I can build a plugin that fulfills my requirements.*

## Package layout

The data protection stack consists of five packages:

* [Microsoft.AspNetCore.DataProtection.Abstractions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Abstractions/) contains:

  * [IDataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotectionprovider) and [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector) interfaces to create data protection services.
  * Useful extension methods for working with these types, such as [IDataProtector.Protect](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector.protect).

  If the data protection system is instantiated elsewhere and you're consuming the API, reference `Microsoft.AspNetCore.DataProtection.Abstractions`.

* [Microsoft.AspNetCore.DataProtection](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection/) contains the core implementation of the data protection system, including:

  * Core cryptographic operations
  * Key management
  * Configuration and extensibility

  To instantiate the data protection system, reference `Microsoft.AspNetCore.DataProtection`. You might need to reference the data protection system when:

  * Adding it to an [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection).
  * Modifying or extending its behavior.

* [Microsoft.AspNetCore.DataProtection.Extensions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Extensions/) contains more APIs which developers might find useful but, which don't belong in the core package. For instance, this package contains:

  * Factory methods to instantiate the data protection system to store keys at a location on the file system without dependency injection. For more information, see [DataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionprovider).
  * Extension methods for limiting the lifetime of protected payloads. For more information, see [ITimeLimitedDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.itimelimiteddataprotector).

* [Microsoft.AspNetCore.DataProtection.SystemWeb](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.SystemWeb/) can be installed into an existing ASP.NET 4.x app to redirect its `<machineKey>` operations to use the new ASP.NET Core data protection stack. For more information, see [Replace the ASP.NET machineKey in ASP.NET Core](compatibility/replacing-machinekey?view=aspnetcore-10.0).

* [Microsoft.AspNetCore.Cryptography.KeyDerivation](https://www.nuget.org/packages/Microsoft.AspNetCore.Cryptography.KeyDerivation/) provides an implementation of the PBKDF2 password hashing routine. It's convenient for systems that must handle user passwords securely. For more information, see [Hash passwords in ASP.NET Core](consumer-apis/password-hashing?view=aspnetcore-10.0).

## Related content

* [Get started with the Data Protection APIs in ASP.NET Core](using-data-protection?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../host-and-deploy/web-farm?view=aspnetcore-10.0)

ASP.NET Core provides a cryptographic API to protect data, including key management and rotation.

Web apps often need to store sensitive data. The Windows data protection API ([DPAPI](/en-us/dotnet/standard/security/how-to-use-data-protection)) isn't intended for use in web apps.

The ASP.NET Core data protection stack was designed to:

* Provide a built in solution for most Web scenarios.
* Address many of the deficiencies of the previous encryption system.
* Serve as the replacement for the `<machineKey>` element in ASP.NET 1.x - 4.x.

## Problem statement

*I need to persist trusted information for later retrieval, but I don't trust the persistence mechanism.* In web terms, this might be written as *I need to round-trip trusted state via an untrusted client.*

Authenticity, integrity, and tamper-proofing is a requirement. The canonical example of this is an authentication cookie or bearer token. The server generates an ***I am Groot and have xyz permissions*** token and sends it to the client. The client presents that token back to the server, but the server needs some kind of assurance that the client hasn't forged the token.

Confidentiality is a requirement. Since the persisted state is trusted by the server, this state could contain information that shouldn't be disclosed to an untrusted client. For example:

* A file path.
* A permission.
* A handle or other indirect reference.
* Some server-specific data.

Isolation is a requirement. Since modern apps are componentized, individual components want to take advantage of this system without regard to other components in the system. For instance, consider a bearer token component using this stack. It should operate without any interference, for example, from an anti-CSRF mechanism also using the same stack.

Some common assumptions can narrow the scope of requirements:

* All services operating within the cryptosystem are equally trusted.
* The data doesn't need to be generated or consumed outside of the services under our direct control.
* Operations must be fast since each request to the web service might go through the cryptosystem one or more times. The speed requirement makes symmetric cryptography ideal. Asymmetric cryptography isn't used until it's required.

## Design philosophy

ASP.NET Core data protection is an [easy to use](using-data-protection?view=aspnetcore-10.0) data protection stack. It's based on the following principles:

* Ease of configuration. The system strives for zero configuration. In situations where developers need to configure a specific aspect, such as the key repository, those specific configurations aren't difficult.
* Offer a basic consumer-facing API. The APIs are straight forward to use correctly and difficult to use incorrectly.
* Developers don't have to learn key management principles. The system handles algorithm selection and key lifetime on behalf of the developer. The developer doesn't have access to the raw key material.
* Keys are protected at rest as much as possible. The system figures out an appropriate default protection mechanism and applies it automatically.

The data protection APIs aren't primarily intended for indefinite persistence of confidential payloads. Other technologies, such as [Windows CNG DPAPI](/en-us/windows/win32/seccng/cng-dpapi) and [Azure Rights Management](/en-us/rights-management/) are more suited to the scenario of indefinite storage. They have correspondingly strong key management capabilities. That said, the ASP.NET Core data protection APIs can be used for long-term protection of confidential data.

## Audience

The data protection system provides APIs that target three main audiences:

1. The [consumer APIs](consumer-apis/overview?view=aspnetcore-10.0) target application and framework developers.

   *I don't want to learn about how the stack operates or about how it's configured. I just want to perform some operation with high probability of using the APIs successfully.*

2. The [configuration APIs](configuration/overview?view=aspnetcore-10.0) target app developers and system administrators.

   *I need to tell the data protection system that my environment requires non-default paths or settings.*

3. The extensibility APIs target developers in charge of implementing custom policy. Usage of these APIs is limited to rare situations and developers with security experience.

   *I need to replace an entire component within the system because I have truly unique behavioral requirements. I'm willing to learn uncommonly used parts of the API surface in order to build a plugin that fulfills my requirements.*

## Package layout

The data protection stack consists of five packages:

* [Microsoft.AspNetCore.DataProtection.Abstractions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Abstractions/) contains:

  * [IDataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotectionprovider) and [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector) interfaces to create data protection services.
  * Useful extension methods for working with these types. for example, [IDataProtector.Protect](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector.protect)

  If the data protection system is instantiated elsewhere and you're consuming the API, reference `Microsoft.AspNetCore.DataProtection.Abstractions`.

* [Microsoft.AspNetCore.DataProtection](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection/) contains the core implementation of the data protection system, including:

  * Core cryptographic operations.
  * Key management.
  * Configuration and extensibility.

  To instantiate the data protection system, reference `Microsoft.AspNetCore.DataProtection`. You might need to reference the data protection system when:

  * Adding it to an [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection).
  * Modifying or extending its behavior.

* [Microsoft.AspNetCore.DataProtection.Extensions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Extensions/) contains additional APIs which developers might find useful but which don't belong in the core package. For instance, this package contains:

  * Factory methods to instantiate the data protection system to store keys at a location on the file system without dependency injection. See [DataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionprovider).
  * Extension methods for limiting the lifetime of protected payloads. See [ITimeLimitedDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.itimelimiteddataprotector).

* [Microsoft.AspNetCore.DataProtection.SystemWeb](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.SystemWeb/) can be installed into an existing ASP.NET 4.x app to redirect its `<machineKey>` operations to use the new ASP.NET Core data protection stack. For more information, see [Replace the ASP.NET machineKey in ASP.NET Core](compatibility/replacing-machinekey?view=aspnetcore-10.0).

* [Microsoft.AspNetCore.Cryptography.KeyDerivation](https://www.nuget.org/packages/Microsoft.AspNetCore.Cryptography.KeyDerivation/) provides an implementation of the PBKDF2 password hashing routine and can be used by systems that must handle user passwords securely. For more information, see [Hash passwords in ASP.NET Core](consumer-apis/password-hashing?view=aspnetcore-10.0).

## Related content

* [Get started with the Data Protection APIs in ASP.NET Core](using-data-protection?view=aspnetcore-10.0)
* [Host ASP.NET Core in a web farm](../../host-and-deploy/web-farm?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/validation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Model validation in ASP.NET Core MVC and Razor Pages

This article explains how to validate user input in an ASP.NET Core MVC or Razor Pages app.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Model state

Model state represents errors that come from two subsystems: model binding and model validation. Errors that originate from [model binding](model-binding?view=aspnetcore-10.0) are generally data conversion errors. For example, an "x" is entered in an integer field. Model validation occurs after model binding and reports errors where data doesn't conform to business rules. For example, a 0 is entered in a field that expects a rating between 1 and 5.

Both model binding and model validation occur before the execution of a controller action or a Razor Pages handler method. For web apps, it's the app's responsibility to inspect `ModelState.IsValid` and react appropriately. Web apps typically redisplay the page with an error message, as shown in the following Razor Pages example:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

For ASP.NET Core MVC with controllers and views, the following example shows how to check `ModelState.IsValid` inside of a controller action:

```
public async Task<IActionResult> Create(Movie movie)
{
    if (!ModelState.IsValid)
    {
        return View(movie);
    }

    _context.Movies.Add(movie);
    await _context.SaveChangesAsync();

    return RedirectToAction(nameof(Index));
}
```

Web API controllers don't have to check `ModelState.IsValid` if they have the [\[ApiController\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute. In that case, an automatic HTTP 400 response containing error details is returned when model state is invalid. For more information, see [Automatic HTTP 400 responses](../../web-api/?view=aspnetcore-10.0#automatic-http-400-responses).

## Rerun validation

Validation is automatic, but you might want to repeat it manually. For example, you might compute a value for a property and want to rerun validation after setting the property to the computed value. To rerun validation, call [ModelStateDictionary.ClearValidationState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.clearvalidationstate) to clear validation specific to the model being validated followed by `TryValidateModel`:

```
public async Task<IActionResult> OnPostTryValidateAsync()
{
    var modifiedReleaseDate = DateTime.Now.Date;
    Movie.ReleaseDate = modifiedReleaseDate;

    ModelState.ClearValidationState(nameof(Movie));
    if (!TryValidateModel(Movie, nameof(Movie)))
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

## Validation attributes

Validation attributes let you specify validation rules for model properties. The following example from the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples) shows a model class that is annotated with validation attributes. The `[ClassicMovie]` attribute is a custom validation attribute and the others are built in. Not shown is `[ClassicMovieWithClientValidator]`, which shows an alternative way to implement a custom attribute.

```
public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}
```

## Built-in attributes

For a complete list of validation attributes, see the [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace.

### Error messages

Validation attributes let you specify the error message to be displayed for invalid input. For example:

```
[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]
```

Internally, the attributes call [String.Format](/en-us/dotnet/api/system.string.format) with a placeholder for the field name and sometimes additional placeholders. For example:

```
[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]
```

When applied to a `Name` property, the error message created by the preceding code would be "Name length must be between 6 and 8.".

To find out which parameters are passed to `String.Format` for a particular attribute's error message, see the [DataAnnotations source code](https://github.com/dotnet/runtime/tree/main/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations).

### Use JSON property names in validation errors

By default, when a validation error occurs, model validation produces a [ModelStateDictionary](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary) with the property name as the error key. Some apps, such as single page apps, benefit from using JSON property names for validation errors generated from Web APIs. The following code configures validation to use the [`SystemTextJsonValidationMetadataProvider`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.systemtextjsonvalidationmetadataprovider) to use JSON property names:

```
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new SystemTextJsonValidationMetadataProvider());
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

The following code configures validation to use the [`NewtonsoftJsonValidationMetadataProvider`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.newtonsoftjson.newtonsoftjsonvalidationmetadataprovider) to use JSON property name when using [Json.NET](https://www.newtonsoft.com/json):

```
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new NewtonsoftJsonValidationMetadataProvider());
}).AddNewtonsoftJson();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

For an example of the policy to use camel-casing, see [`Program.cs` on GitHub](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/validation/samples/7.x/ValidationJSON/Program.cs).

## Non-nullable reference types and \[Required] attribute

The validation system treats non-nullable parameters or bound properties as if they had a `[Required(AllowEmptyStrings = true)]` attribute. By [enabling `Nullable` contexts](/en-us/dotnet/csharp/nullable-references#nullable-contexts), MVC implicitly starts validating non-nullable properties or parameters as if they had been attributed with the `[Required(AllowEmptyStrings = true)]` attribute. Consider the following code:

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

If the app was built with `<Nullable>enable</Nullable>`, a missing value for `Name` in a JSON or form post results in a validation error. This may seem contradictory since the `[Required(AllowEmptyStrings = true)]` attribute is implied, but this is expected behavior because [empty strings are converted to null by default](/en-us/dotnet/api/system.componentmodel.dataannotations.displayformatattribute.convertemptystringtonull). Use a nullable reference type to allow null or missing values to be specified for the `Name` property:

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

This behavior can be disabled by configuring [SuppressImplicitRequiredAttributeForNonNullableReferenceTypes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.suppressimplicitrequiredattributefornonnullablereferencetypes#microsoft-aspnetcore-mvc-mvcoptions-suppressimplicitrequiredattributefornonnullablereferencetypes) in `Program.cs`:

```
builder.Services.AddControllers(
    options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);
```

### \[Required] validation on the server

On the server, a required value is considered missing if the property is null. A non-nullable field is always valid, and the `[Required]` attribute's error message is never displayed.

However, model binding for a non-nullable property may fail, resulting in an error message such as `The value '' is invalid`. To specify a custom error message for server-side validation of non-nullable types, you have the following options:

* Make the field nullable (for example, `decimal?` instead of `decimal`). [Nullable\<T>](/en-us/dotnet/csharp/programming-guide/nullable-types/) value types are treated like standard nullable types.

* Specify the default error message to be used by model binding, as shown in the following example:

  ```
  builder.Services.AddRazorPages()
      .AddMvcOptions(options =>
      {
          options.MaxModelValidationErrors = 50;
          options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
              _ => "The field is required.");
      });

  builder.Services.AddSingleton
      <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
  ```

  For more information about model binding errors that you can set default messages for, see [DefaultModelBindingMessageProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.defaultmodelbindingmessageprovider#methods).

### \[Required] validation on the client

Non-nullable types and strings are handled differently on the client compared to the server. On the client:

* A value is considered present only if input is entered for it. Therefore, client-side validation handles non-nullable types the same as nullable types.
* Whitespace in a string field is considered valid input by the jQuery Validation [required](https://jqueryvalidation.org/required-method/) method. Server-side validation considers a required string field invalid if only whitespace is entered.

As noted earlier, non-nullable types are treated as though they had a `[Required(AllowEmptyStrings = true)]` attribute. That means you get client-side validation even if you don't apply the `[Required(AllowEmptyStrings = true)]` attribute. But if you don't use the attribute, you get a default error message. To specify a custom error message, use the attribute.

## \[Remote] attribute

The [\[Remote\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.remoteattribute) attribute implements client-side validation that requires calling a method on the server to determine whether field input is valid. For example, the app may need to verify whether a user name is already in use.

To implement remote validation:

1. Create an action method for JavaScript to call. The jQuery Validation [remote](https://jqueryvalidation.org/remote-method/) method expects a JSON response:

   * `true` means the input data is valid.
   * `false`, `undefined`, or `null` means the input is invalid. Display the default error message.
   * Any other string means the input is invalid. Display the string as a custom error message.

   Here's an example of an action method that returns a custom error message:

   ```
   [AcceptVerbs("GET", "POST")]
   public IActionResult VerifyEmail(string email)
   {
       if (!_userService.VerifyEmail(email))
       {
           return Json($"Email {email} is already in use.");
       }

       return Json(true);
   }
   ```

2. In the model class, annotate the property with a `[Remote]` attribute that points to the validation action method, as shown in the following example:

   ```
   [Remote(action: "VerifyEmail", controller: "Users")]
   public string Email { get; set; } = null!;
   ```

Remote validation:

* Doesn't perform server-side validation after the form is submitted.
* Doesn't perform client-side checks if the client has disabled JavaScript. If the client-side validation check is required for form processing on the server, always implement separate server-side validation.

### Additional fields

The [AdditionalFields](/en-us/dotnet/api/microsoft.aspnetcore.mvc.remoteattribute.additionalfields) property of the `[Remote]` attribute lets you validate combinations of fields against data on the server. For example, if the `User` model had `FirstName` and `LastName` properties, you might want to verify that no existing users already have that pair of names. The following example shows how to use `AdditionalFields`:

```
[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]
[Display(Name = "First Name")]
public string FirstName { get; set; } = null!;

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
[Display(Name = "Last Name")]
public string LastName { get; set; } = null!;
```

`AdditionalFields` could be set explicitly to the strings "FirstName" and "LastName", but using the [nameof](/en-us/dotnet/csharp/language-reference/keywords/nameof) operator simplifies later refactoring. The action method for this validation must accept both `firstName` and `lastName` arguments:

```
[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userService.VerifyName(firstName, lastName))
    {
        return Json($"A user named {firstName} {lastName} already exists.");
    }

    return Json(true);
}
```

When the user enters a first or last name, JavaScript makes a remote call to see if that pair of names has been taken.

To validate two or more additional fields, provide them as a comma-delimited list. For example, to add a `MiddleName` property to the model, set the `[Remote]` attribute as shown in the following example:

```
[Remote(action: "VerifyName", controller: "Users",
    AdditionalFields = nameof(FirstName) + "," + nameof(LastName))]
public string MiddleName { get; set; }
```

`AdditionalFields`, like all attribute arguments, must be a constant expression. Therefore, don't use an [interpolated string](/en-us/dotnet/csharp/language-reference/keywords/interpolated-strings) or call [Join](/en-us/dotnet/api/system.string.join) to initialize `AdditionalFields`.

## Alternatives to built-in attributes

If you need validation not provided by built-in attributes, you can:

* [Create custom attributes](#custom-attributes).
* [Implement IValidatableObject](#ivalidatableobject).

## Custom attributes

For scenarios that the built-in validation attributes don't handle, you can create custom validation attributes. Create a class that inherits from [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute), and override the [IsValid](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute.isvalid) method.

The `IsValid` method accepts an object named *value*, which is the input to be validated. An overload also accepts a [ValidationContext](/en-us/dotnet/api/system.componentmodel.dataannotations.validationcontext) object, which provides additional information, such as the model instance created by model binding.

The following example validates that the release date for a movie in the *Classic* genre isn't later than a specified year. The `[ClassicMovie]` attribute:

* Is only run on the server.
* For Classic movies, validates the release date:

```
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
        => Year = year;

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult? IsValid(
        object? value, ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value!).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}
```

The `movie` variable in the preceding example represents a `Movie` object that contains the data from the form submission. When validation fails, a [ValidationResult](/en-us/dotnet/api/system.componentmodel.dataannotations.validationresult) with an error message is returned.

## IValidatableObject

The preceding example works only with `Movie` types. Another option for class-level validation is to implement [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) in the model class, as shown in the following example:

```
public class ValidatableMovie : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
        {
            yield return new ValidationResult(
                $"Classic movies must have a release year no later than {_classicYear}.",
                new[] { nameof(ReleaseDate) });
        }
    }
}
```

## Custom validation

The following code shows how to add a model error after examining the model:

```
if (Contact.Name == Contact.ShortName)
{
    ModelState.AddModelError("Contact.ShortName", 
                             "Short name can't be the same as Name.");
}
```

The following code implements the validation test in a controller:

```
if (contact.Name == contact.ShortName)
{
    ModelState.AddModelError(nameof(contact.ShortName),
                             "Short name can't be the same as Name.");
}
```

The following code verifies the phone number and email are unique:

```
public async Task<IActionResult> OnPostAsync()
{
    // Attach Validation Error Message to the Model on validation failure.          

    if (Contact.Name == Contact.ShortName)
    {
        ModelState.AddModelError("Contact.ShortName", 
                                 "Short name can't be the same as Name.");
    }

    if (_context.Contact.Any(i => i.PhoneNumber == Contact.PhoneNumber))
    {
        ModelState.AddModelError("Contact.PhoneNumber",
                                  "The Phone number is already in use.");
    }
    if (_context.Contact.Any(i => i.Email == Contact.Email))
    {
        ModelState.AddModelError("Contact.Email", "The Email is already in use.");
    }

    if (!ModelState.IsValid || _context.Contact == null || Contact == null)
    {
        // if model is invalid, return the page with the model state errors.
        return Page();
    }
    _context.Contact.Add(Contact);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

The following code implements the validation test in a controller:

```
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Name,ShortName,Email,PhoneNumber")] Contact contact)
{
    // Attach Validation Error Message to the Model on validation failure.
    if (contact.Name == contact.ShortName)
    {
        ModelState.AddModelError(nameof(contact.ShortName),
                                 "Short name can't be the same as Name.");
    }

    if (_context.Contact.Any(i => i.PhoneNumber == contact.PhoneNumber))
    {
        ModelState.AddModelError(nameof(contact.PhoneNumber),
                                  "The Phone number is already in use.");
    }
    if (_context.Contact.Any(i => i.Email == contact.Email))
    {
        ModelState.AddModelError(nameof(contact.Email), "The Email is already in use.");
    }

    if (ModelState.IsValid)
    {
        _context.Add(contact);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(contact);
}
```

Checking for a unique phone number or email is typically also done with [remote validation](#remote-attribute).

## ValidationResult

Consider the following custom `ValidateNameAttribute`:

```
public class ValidateNameAttribute : ValidationAttribute
{
    public ValidateNameAttribute()
    {
        const string defaultErrorMessage = "Error with Name";
        ErrorMessage ??= defaultErrorMessage;
    }

    protected override ValidationResult? IsValid(object? value,
                                         ValidationContext validationContext)
    {
        if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
        {
            return new ValidationResult("Name is required.");
        }

        if (value.ToString()!.ToLower().Contains("zz"))
        {

            return new ValidationResult(
                        FormatErrorMessage(validationContext.DisplayName));
        }

        return ValidationResult.Success;
    }
}
```

In the following code, the custom `[ValidateName]` attribute is applied:

```
public class Contact
{
    public Guid Id { get; set; }

    [ValidateName(ErrorMessage = "Name must not contain `zz`")] 
    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? PhoneNumber { get; set; }
}
```

When the model contains `zz`, a new [ValidationResult](/en-us/dotnet/api/system.componentmodel.dataannotations.validationresult) is returned.

## Top-level node validation

Top-level nodes include:

* Action parameters
* Controller properties
* Page handler parameters
* Page model properties

Model-bound top-level nodes are validated in addition to validating model properties. In the following example from the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples), the `VerifyPhone` method uses the [RegularExpressionAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute) to validate the `phone` action parameter:

```
[AcceptVerbs("GET", "POST")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
{
    if (!ModelState.IsValid)
    {
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");
    }

    return Json(true);
}
```

Top-level nodes can use [BindRequiredAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindrequiredattribute) with validation attributes. In the following example from the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples), the `CheckAge` method specifies that the `age` parameter must be bound from the query string when the form is submitted:

```
[HttpPost]
public IActionResult CheckAge([BindRequired, FromQuery] int age)
{
```

In the Check Age page (`CheckAge.cshtml`), there are two forms. The first form submits an `Age` value of `99` as a query string parameter: `https://localhost:5001/Users/CheckAge?Age=99`.

When a properly formatted `age` parameter from the query string is submitted, the form validates.

The second form on the Check Age page submits the `Age` value in the body of the request, and validation fails. Binding fails because the `age` parameter must come from a query string.

## Maximum errors

Validation stops when the maximum number of errors is reached (200 by default). You can configure this number with the following code in `Program.cs`:

```
builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            _ => "The field is required.");
    });

builder.Services.AddSingleton
    <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
```

## Maximum recursion

[ValidationVisitor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.validationvisitor) traverses the object graph of the model being validated. For models that are deep or are infinitely recursive, validation may result in stack overflow. [MvcOptions.MaxValidationDepth](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.maxvalidationdepth) provides a way to stop validation early if the visitor recursion exceeds a configured depth. The default value of `MvcOptions.MaxValidationDepth` is 32.

## Automatic short-circuit

Validation is automatically short-circuited (skipped) if the model graph doesn't require validation. Objects that the runtime skips validation for include collections of primitives (such as `byte[]`, `string[]`, `Dictionary<string, string>`) and complex object graphs that don't have any validators.

## Client-side validation

Client-side validation prevents submission until the form is valid. The Submit button runs JavaScript that either submits the form or displays error messages.

Client-side validation avoids an unnecessary round trip to the server when there are input errors on a form. The following script references in `_Layout.cshtml` and `_ValidationScriptsPartial.cshtml` support client-side validation:

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
```

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>
```

The [jQuery Unobtrusive Validation](https://github.com/aspnet/jquery-validation-unobtrusive) script is a custom Microsoft front-end library that builds on the popular [jQuery Validation](https://jqueryvalidation.org/) plugin. Without jQuery Unobtrusive Validation, you would have to code the same validation logic in two places: once in the server-side validation attributes on model properties, and then again in client-side scripts. Instead, [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) and [HTML helpers](../views/overview?view=aspnetcore-10.0) use the validation attributes and type metadata from model properties to render HTML 5 `data-` attributes for the form elements that need validation. jQuery Unobtrusive Validation parses the `data-` attributes and passes the logic to jQuery Validation, effectively "copying" the server-side validation logic to the client. You can display validation errors on the client using tag helpers as shown here:

```
<div class="form-group">
    <label asp-for="Movie.ReleaseDate" class="control-label"></label>
    <input asp-for="Movie.ReleaseDate" class="form-control" />
    <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
```

The preceding tag helpers render the following HTML:

```
<div class="form-group">
    <label class="control-label" for="Movie_ReleaseDate">Release Date</label>
    <input class="form-control" type="date" data-val="true"
        data-val-required="The Release Date field is required."
        id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
    <span class="text-danger field-validation-valid"
        data-valmsg-for="Movie.ReleaseDate" data-valmsg-replace="true"></span>
</div>
```

Notice that the `data-` attributes in the HTML output correspond to the validation attributes for the `Movie.ReleaseDate` property. The `data-val-required` attribute contains an error message to display if the user doesn't fill in the release date field. jQuery Unobtrusive Validation passes this value to the jQuery Validation [required()](https://jqueryvalidation.org/required-method/) method, which then displays that message in the accompanying **\<span>** element.

Data type validation is based on the .NET type of a property, unless that is overridden by a [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute. Browsers have their own default error messages, but the jQuery Validation Unobtrusive Validation package can override those messages. `[DataType]` attributes and subclasses such as [\[EmailAddress\]](/en-us/dotnet/api/system.componentmodel.dataannotations.emailaddressattribute) let you specify the error message.

## Unobtrusive validation

For information on unobtrusive validation, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/1111).

### Add Validation to Dynamic Forms

jQuery Unobtrusive Validation passes validation logic and parameters to jQuery Validation when the page first loads. Therefore, validation doesn't work automatically on dynamically generated forms. To enable validation, tell jQuery Unobtrusive Validation to parse the dynamic form immediately after you create it. For example, the following code sets up client-side validation on a form added via AJAX.

```
$.get({
    url: "https://url/that/returns/a/form",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add form. " + errorThrown);
    },
    success: function(newFormHTML) {
        var container = document.getElementById("form-container");
        container.insertAdjacentHTML("beforeend", newFormHTML);
        var forms = container.getElementsByTagName("form");
        var newForm = forms[forms.length - 1];
        $.validator.unobtrusive.parse(newForm);
    }
})
```

The `$.validator.unobtrusive.parse()` method accepts a jQuery selector for its one argument. This method tells jQuery Unobtrusive Validation to parse the `data-` attributes of forms within that selector. The values of those attributes are then passed to the jQuery Validation plugin.

### Add Validation to Dynamic Controls

The `$.validator.unobtrusive.parse()` method works on an entire form, not on individual dynamically generated controls, such as `<input>` and `<select/>`. To reparse the form, remove the validation data that was added when the form was parsed earlier, as shown in the following example:

```
$.get({
    url: "https://url/that/returns/a/control",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add control. " + errorThrown);
    },
    success: function(newInputHTML) {
        var form = document.getElementById("my-form");
        form.insertAdjacentHTML("beforeend", newInputHTML);
        $(form).removeData("validator")    // Added by jQuery Validation
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})
```

## Custom client-side validation

Custom client-side validation is done by generating `data-` HTML attributes that work with a custom jQuery Validation adapter. The following sample adapter code was written for the `[ClassicMovie]` and `[ClassicMovieWithClientValidator]` attributes that were introduced earlier in this article:

```
$.validator.addMethod('classicmovie', function (value, element, params) {
    var genre = $(params[0]).val(), year = params[1], date = new Date(value);

    // The Classic genre has a value of '0'.
    if (genre && genre.length > 0 && genre[0] === '0') {
        // The release date for a Classic is valid if it's no greater than the given year.
        return date.getUTCFullYear() <= year;
    }

    return true;
});

$.validator.unobtrusive.adapters.add('classicmovie', ['year'], function (options) {
    var element = $(options.form).find('select#Movie_Genre')[0];

    options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
    options.messages['classicmovie'] = options.message;
});
```

For information about how to write adapters, see the [jQuery Validation documentation](https://jqueryvalidation.org/documentation/).

The use of an adapter for a given field is triggered by `data-` attributes that:

* Flag the field as being subject to validation (`data-val="true"`).
* Identify a validation rule name and error message text (for example, `data-val-rulename="Error message."`).
* Provide any additional parameters the validator needs (for example, `data-val-rulename-param1="value"`).

The following example shows the `data-` attributes for the [sample app's](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples) `ClassicMovie` attribute:

```
<input class="form-control" type="date"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year no later than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The Release Date field is required."
    id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
```

As noted earlier, [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) and [HTML helpers](../views/overview?view=aspnetcore-10.0) use information from validation attributes to render `data-` attributes. There are two options for writing code that results in the creation of custom `data-` HTML attributes:

* Create a class that derives from [AttributeAdapterBase\<TAttribute>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.attributeadapterbase-1) and a class that implements [IValidationAttributeAdapterProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.ivalidationattributeadapterprovider), and register your attribute and its adapter in DI. This method follows the [single responsibility principle](https://wikipedia.org/wiki/Single_responsibility_principle) in that server-related and client-related validation code is in separate classes. The adapter also has the advantage that since it's registered in DI, other services in DI are available to it if needed.
* Implement [IClientModelValidator](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.iclientmodelvalidator) in your [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) class. This method might be appropriate if the attribute doesn't do any server-side validation and doesn't need any services from DI.

### AttributeAdapter for client-side validation

This method of rendering `data-` attributes in HTML is used by the `ClassicMovie` attribute in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples). To add client validation by using this method:

1. Create an attribute adapter class for the custom validation attribute. Derive the class from [AttributeAdapterBase\<TAttribute>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.attributeadapterbase-1). Create an `AddValidation` method that adds `data-` attributes to the rendered output, as shown in this example:

   ```
   public class ClassicMovieAttributeAdapter : AttributeAdapterBase<ClassicMovieAttribute>
   {
       public ClassicMovieAttributeAdapter(
           ClassicMovieAttribute attribute, IStringLocalizer? stringLocalizer)
           : base(attribute, stringLocalizer)
       {

       }

       public override void AddValidation(ClientModelValidationContext context)
       {
           MergeAttribute(context.Attributes, "data-val", "true");
           MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage(context));

           var year = Attribute.Year.ToString(CultureInfo.InvariantCulture);
           MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
       }

       public override string GetErrorMessage(ModelValidationContextBase validationContext)
           => Attribute.GetErrorMessage();
   }
   ```

2. Create an adapter provider class that implements [IValidationAttributeAdapterProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.ivalidationattributeadapterprovider). In the [GetAttributeAdapter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.ivalidationattributeadapterprovider.getattributeadapter) method pass in the custom attribute to the adapter's constructor, as shown in this example:

   ```
   public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
   {
       private readonly IValidationAttributeAdapterProvider baseProvider =
           new ValidationAttributeAdapterProvider();

       public IAttributeAdapter? GetAttributeAdapter(
           ValidationAttribute attribute, IStringLocalizer? stringLocalizer)
       {
           if (attribute is ClassicMovieAttribute classicMovieAttribute)
           {
               return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
           }

           return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
       }
   }
   ```

3. Register the adapter provider for DI in `Program.cs`:

   ```
   builder.Services.AddRazorPages()
       .AddMvcOptions(options =>
       {
           options.MaxModelValidationErrors = 50;
           options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
               _ => "The field is required.");
       });

   builder.Services.AddSingleton
       <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
   ```

### IClientModelValidator for client-side validation

This method of rendering `data-` attributes in HTML is used by the `ClassicMovieWithClientValidator` attribute in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples). To add client validation by using this method:

* In the custom validation attribute, implement the [IClientModelValidator](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.iclientmodelvalidator) interface and create an [AddValidation](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.iclientmodelvalidator.addvalidation) method. In the `AddValidation` method, add `data-` attributes for validation, as shown in the following example:

  ```
  public class ClassicMovieWithClientValidatorAttribute :
      ValidationAttribute, IClientModelValidator
  {
      public ClassicMovieWithClientValidatorAttribute(int year)
          => Year = year;

      public int Year { get; }

      public void AddValidation(ClientModelValidationContext context)
      {
          MergeAttribute(context.Attributes, "data-val", "true");
          MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());

          var year = Year.ToString(CultureInfo.InvariantCulture);
          MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
      }

      public string GetErrorMessage() =>
          $"Classic movies must have a release year no later than {Year}.";

      protected override ValidationResult? IsValid(
          object? value, ValidationContext validationContext)
      {
          var movie = (Movie)validationContext.ObjectInstance;
          var releaseYear = ((DateTime)value!).Year;

          if (movie.Genre == Genre.Classic && releaseYear > Year)
          {
              return new ValidationResult(GetErrorMessage());
          }

          return ValidationResult.Success;
      }

      private static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
      {
          if (attributes.ContainsKey(key))
          {
              return false;
          }

          attributes.Add(key, value);
          return true;
      }
  }
  ```

## Disable client-side validation

The following code disables client validation in Razor Pages:

```
builder.Services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });
```

Other options to disable client-side validation:

* Comment out the reference to `_ValidationScriptsPartial` in all the `.cshtml` files.
* Remove the contents of the *Pages\Shared\_ValidationScriptsPartial.cshtml* file.

The preceding approach won't prevent client-side validation of ASP.NET Core Identity Razor class library. For more information, see [Scaffold Identity in ASP.NET Core projects](../../security/authentication/scaffold-identity?view=aspnetcore-10.0).

## Related content

* [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations)
* [Model Binding](model-binding?view=aspnetcore-10.0)
* [ASP.NET Core Blazor forms overview](../../blazor/forms/?view=aspnetcore-10.0)

This article explains how to validate user input in an ASP.NET Core MVC or Razor Pages app.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Model state

Model state represents errors that come from two subsystems: model binding and model validation. Errors that originate from [model binding](model-binding?view=aspnetcore-10.0) are generally data conversion errors. For example, an "x" is entered in an integer field. Model validation occurs after model binding and reports errors where data doesn't conform to business rules. For example, a 0 is entered in a field that expects a rating between 1 and 5.

Both model binding and model validation occur before the execution of a controller action or a Razor Pages handler method. For web apps, it's the app's responsibility to inspect `ModelState.IsValid` and react appropriately. Web apps typically redisplay the page with an error message, as shown in the following Razor Pages example:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

For ASP.NET Core MVC with controllers and views, the following example shows how to check `ModelState.IsValid` inside of a controller action:

```
public async Task<IActionResult> Create(Movie movie)
{
    if (!ModelState.IsValid)
    {
        return View(movie);
    }

    _context.Movies.Add(movie);
    await _context.SaveChangesAsync();

    return RedirectToAction(nameof(Index));
}
```

Web API controllers don't have to check `ModelState.IsValid` if they have the [\[ApiController\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute. In that case, an automatic HTTP 400 response containing error details is returned when model state is invalid. For more information, see [Automatic HTTP 400 responses](../../web-api/?view=aspnetcore-10.0#automatic-http-400-responses).

## Rerun validation

Validation is automatic, but you might want to repeat it manually. For example, you might compute a value for a property and want to rerun validation after setting the property to the computed value. To rerun validation, call [ModelStateDictionary.ClearValidationState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.clearvalidationstate) to clear validation specific to the model being validated followed by `TryValidateModel`:

```
public async Task<IActionResult> OnPostTryValidateAsync()
{
    var modifiedReleaseDate = DateTime.Now.Date;
    Movie.ReleaseDate = modifiedReleaseDate;

    ModelState.ClearValidationState(nameof(Movie));
    if (!TryValidateModel(Movie, nameof(Movie)))
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

## Validation attributes

Validation attributes let you specify validation rules for model properties. The following example from the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples) shows a model class that is annotated with validation attributes. The `[ClassicMovie]` attribute is a custom validation attribute and the others are built in. Not shown is `[ClassicMovieWithClientValidator]`, which shows an alternative way to implement a custom attribute.

```
public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}
```

## Built-in attributes

Here are some of the built-in validation attributes:

* [\[ValidateNever\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.validateneverattribute): Indicates that a property or parameter should be excluded from validation.
* [\[CreditCard\]](/en-us/dotnet/api/system.componentmodel.dataannotations.creditcardattribute): Validates that the property has a credit card format. Requires [jQuery Validation Additional Methods](https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/additional-methods.js).
* [\[Compare\]](/en-us/dotnet/api/system.componentmodel.dataannotations.compareattribute): Validates that two properties in a model match.
* [\[EmailAddress\]](/en-us/dotnet/api/system.componentmodel.dataannotations.emailaddressattribute): Validates that the property has an email format.
* [\[Phone\]](/en-us/dotnet/api/system.componentmodel.dataannotations.phoneattribute): Validates that the property has a telephone number format.
* [\[Range\]](/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute): Validates that the property value falls within a specified range.
* [\[RegularExpression\]](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute): Validates that the property value matches a specified regular expression.
* [\[Required\]](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute): Validates that the field isn't null. See [`[Required]` attribute](#non-nullable-reference-types-and-the-required-attribute) for details about this attribute's behavior.
* [\[StringLength\]](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute): Validates that a string property value doesn't exceed a specified length limit.
* [\[Url\]](/en-us/dotnet/api/system.componentmodel.dataannotations.urlattribute): Validates that the property has a URL format.
* [\[Remote\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.remoteattribute): Validates input on the client by calling an action method on the server. See [`[Remote]` attribute](#remote-attribute) for details about this attribute's behavior.

A complete list of validation attributes can be found in the [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace.

### Error messages

Validation attributes let you specify the error message to be displayed for invalid input. For example:

```
[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]
```

Internally, the attributes call [String.Format](/en-us/dotnet/api/system.string.format) with a placeholder for the field name and sometimes additional placeholders. For example:

```
[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]
```

When applied to a `Name` property, the error message created by the preceding code would be "Name length must be between 6 and 8.".

To find out which parameters are passed to `String.Format` for a particular attribute's error message, see the [DataAnnotations source code](https://github.com/dotnet/runtime/tree/main/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations).

## Non-nullable reference types and the \[Required] attribute

The validation system treats non-nullable parameters or bound properties as if they had a `[Required(AllowEmptyStrings = true)]` attribute. By [enabling `Nullable` contexts](/en-us/dotnet/csharp/nullable-references#nullable-contexts), MVC implicitly starts validating non-nullable properties [on non-generic types](#ngen) or parameters as if they had been attributed with the `[Required(AllowEmptyStrings = true)]` attribute. Consider the following code:

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

If the app was built with `<Nullable>enable</Nullable>`, a missing value for `Name` in a JSON or form post results in a validation error. Use a nullable reference type to allow null or missing values to be specified for the `Name` property:

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

This behavior can be disabled by configuring [SuppressImplicitRequiredAttributeForNonNullableReferenceTypes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.suppressimplicitrequiredattributefornonnullablereferencetypes#microsoft-aspnetcore-mvc-mvcoptions-suppressimplicitrequiredattributefornonnullablereferencetypes) in `Program.cs`:

```
builder.Services.AddControllers(
    options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);
```

[]()

## Non-nullable properties on generic types and \[Required] attribute

Non-nullable properties on generic types must include the `[Required]` attribute when the type is required. In the following code, `TestRequired` is not required:

```
public class WeatherForecast<T>
{
    public string TestRequired { get; set; } = null!;
    public T? Inner { get; set; }
}
```

In the following code, `TestRequired` is explicitly marked as required:

```
using System.ComponentModel.DataAnnotations;

public class WeatherForecast<T>
{
    [Required]
    public string TestRequired { get; set; } = null!;
    public T? Inner { get; set; }
}
```

### \[Required] validation on the server

On the server, a required value is considered missing if the property is null. A non-nullable field is always valid, and the `[Required]` attribute's error message is never displayed.

However, model binding for a non-nullable property may fail, resulting in an error message such as `The value '' is invalid`. To specify a custom error message for server-side validation of non-nullable types, you have the following options:

* Make the field nullable (for example, `decimal?` instead of `decimal`). [Nullable\<T>](/en-us/dotnet/csharp/programming-guide/nullable-types/) value types are treated like standard nullable types.

* Specify the default error message to be used by model binding, as shown in the following example:

  ```
  builder.Services.AddRazorPages()
      .AddMvcOptions(options =>
      {
          options.MaxModelValidationErrors = 50;
          options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
              _ => "The field is required.");
      });

  builder.Services.AddSingleton
      <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
  ```

  For more information about model binding errors that you can set default messages for, see [DefaultModelBindingMessageProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.defaultmodelbindingmessageprovider#methods).

### \[Required] validation on the client

Non-nullable types and strings are handled differently on the client compared to the server. On the client:

* A value is considered present only if input is entered for it. Therefore, client-side validation handles non-nullable types the same as nullable types.
* Whitespace in a string field is considered valid input by the jQuery Validation [required](https://jqueryvalidation.org/required-method/) method. Server-side validation considers a required string field invalid if only whitespace is entered.

As noted earlier, non-nullable types are treated as though they had a `[Required(AllowEmptyStrings = true)]` attribute. That means you get client-side validation even if you don't apply the `[Required(AllowEmptyStrings = true)]` attribute. But if you don't use the attribute, you get a default error message. To specify a custom error message, use the attribute.

## \[Remote] attribute

The [\[Remote\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.remoteattribute) attribute implements client-side validation that requires calling a method on the server to determine whether field input is valid. For example, the app may need to verify whether a user name is already in use.

To implement remote validation:

1. Create an action method for JavaScript to call. The jQuery Validation [remote](https://jqueryvalidation.org/remote-method/) method expects a JSON response:

   * `true` means the input data is valid.
   * `false`, `undefined`, or `null` means the input is invalid. Display the default error message.
   * Any other string means the input is invalid. Display the string as a custom error message.

   Here's an example of an action method that returns a custom error message:

   ```
   [AcceptVerbs("GET", "POST")]
   public IActionResult VerifyEmail(string email)
   {
       if (!_userService.VerifyEmail(email))
       {
           return Json($"Email {email} is already in use.");
       }

       return Json(true);
   }
   ```

2. In the model class, annotate the property with a `[Remote]` attribute that points to the validation action method, as shown in the following example:

   ```
   [Remote(action: "VerifyEmail", controller: "Users")]
   public string Email { get; set; } = null!;
   ```

### Additional fields

The [AdditionalFields](/en-us/dotnet/api/microsoft.aspnetcore.mvc.remoteattribute.additionalfields) property of the `[Remote]` attribute lets you validate combinations of fields against data on the server. For example, if the `User` model had `FirstName` and `LastName` properties, you might want to verify that no existing users already have that pair of names. The following example shows how to use `AdditionalFields`:

```
[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]
[Display(Name = "First Name")]
public string FirstName { get; set; } = null!;

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
[Display(Name = "Last Name")]
public string LastName { get; set; } = null!;
```

`AdditionalFields` could be set explicitly to the strings "FirstName" and "LastName", but using the [nameof](/en-us/dotnet/csharp/language-reference/keywords/nameof) operator simplifies later refactoring. The action method for this validation must accept both `firstName` and `lastName` arguments:

```
[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userService.VerifyName(firstName, lastName))
    {
        return Json($"A user named {firstName} {lastName} already exists.");
    }

    return Json(true);
}
```

When the user enters a first or last name, JavaScript makes a remote call to see if that pair of names has been taken.

To validate two or more additional fields, provide them as a comma-delimited list. For example, to add a `MiddleName` property to the model, set the `[Remote]` attribute as shown in the following example:

```
[Remote(action: "VerifyName", controller: "Users",
    AdditionalFields = nameof(FirstName) + "," + nameof(LastName))]
public string MiddleName { get; set; }
```

`AdditionalFields`, like all attribute arguments, must be a constant expression. Therefore, don't use an [interpolated string](/en-us/dotnet/csharp/language-reference/keywords/interpolated-strings) or call [Join](/en-us/dotnet/api/system.string.join) to initialize `AdditionalFields`.

## Alternatives to built-in attributes

If you need validation not provided by built-in attributes, you can:

* [Create custom attributes](#custom-attributes).
* [Implement IValidatableObject](#ivalidatableobject).

## Custom attributes

For scenarios that the built-in validation attributes don't handle, you can create custom validation attributes. Create a class that inherits from [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute), and override the [IsValid](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute.isvalid) method.

The `IsValid` method accepts an object named *value*, which is the input to be validated. An overload also accepts a [ValidationContext](/en-us/dotnet/api/system.componentmodel.dataannotations.validationcontext) object, which provides additional information, such as the model instance created by model binding.

The following example validates that the release date for a movie in the *Classic* genre isn't later than a specified year. The `[ClassicMovie]` attribute:

* Is only run on the server.
* For Classic movies, validates the release date:

```
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
        => Year = year;

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult? IsValid(
        object? value, ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value!).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}
```

The `movie` variable in the preceding example represents a `Movie` object that contains the data from the form submission. When validation fails, a [ValidationResult](/en-us/dotnet/api/system.componentmodel.dataannotations.validationresult) with an error message is returned.

## IValidatableObject

The preceding example works only with `Movie` types. Another option for class-level validation is to implement [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) in the model class, as shown in the following example:

```
public class ValidatableMovie : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
        {
            yield return new ValidationResult(
                $"Classic movies must have a release year no later than {_classicYear}.",
                new[] { nameof(ReleaseDate) });
        }
    }
}
```

## Top-level node validation

Top-level nodes include:

* Action parameters
* Controller properties
* Page handler parameters
* Page model properties

Model-bound top-level nodes are validated in addition to validating model properties. In the following example from the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples), the `VerifyPhone` method uses the [RegularExpressionAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute) to validate the `phone` action parameter:

```
[AcceptVerbs("GET", "POST")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
{
    if (!ModelState.IsValid)
    {
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");
    }

    return Json(true);
}
```

Top-level nodes can use [BindRequiredAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindrequiredattribute) with validation attributes. In the following example from the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples), the `CheckAge` method specifies that the `age` parameter must be bound from the query string when the form is submitted:

```
[HttpPost]
public IActionResult CheckAge([BindRequired, FromQuery] int age)
{
```

In the Check Age page (`CheckAge.cshtml`), there are two forms. The first form submits an `Age` value of `99` as a query string parameter: `https://localhost:5001/Users/CheckAge?Age=99`.

When a properly formatted `age` parameter from the query string is submitted, the form validates.

The second form on the Check Age page submits the `Age` value in the body of the request, and validation fails. Binding fails because the `age` parameter must come from a query string.

## Maximum errors

Validation stops when the maximum number of errors is reached (200 by default). You can configure this number with the following code in `Program.cs`:

```
builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            _ => "The field is required.");
    });

builder.Services.AddSingleton
    <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
```

## Maximum recursion

[ValidationVisitor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.validationvisitor) traverses the object graph of the model being validated. For models that are deep or are infinitely recursive, validation may result in stack overflow. [MvcOptions.MaxValidationDepth](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.maxvalidationdepth) provides a way to stop validation early if the visitor recursion exceeds a configured depth. The default value of `MvcOptions.MaxValidationDepth` is 32.

## Automatic short-circuit

Validation is automatically short-circuited (skipped) if the model graph doesn't require validation. Objects that the runtime skips validation for include collections of primitives (such as `byte[]`, `string[]`, `Dictionary<string, string>`) and complex object graphs that don't have any validators.

## Client-side validation

Client-side validation prevents submission until the form is valid. The Submit button runs JavaScript that either submits the form or displays error messages.

Client-side validation avoids an unnecessary round trip to the server when there are input errors on a form. The following script references in `_Layout.cshtml` and `_ValidationScriptsPartial.cshtml` support client-side validation:

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
```

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>
```

The [jQuery Unobtrusive Validation](https://github.com/aspnet/jquery-validation-unobtrusive) script is a custom Microsoft front-end library that builds on the popular [jQuery Validation](https://jqueryvalidation.org/) plugin. Without jQuery Unobtrusive Validation, you would have to code the same validation logic in two places: once in the server-side validation attributes on model properties, and then again in client-side scripts. Instead, [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) and [HTML helpers](../views/overview?view=aspnetcore-10.0) use the validation attributes and type metadata from model properties to render HTML 5 `data-` attributes for the form elements that need validation. jQuery Unobtrusive Validation parses the `data-` attributes and passes the logic to jQuery Validation, effectively "copying" the server-side validation logic to the client. You can display validation errors on the client using tag helpers as shown here:

```
<div class="form-group">
    <label asp-for="Movie.ReleaseDate" class="control-label"></label>
    <input asp-for="Movie.ReleaseDate" class="form-control" />
    <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
```

The preceding tag helpers render the following HTML:

```
<div class="form-group">
    <label class="control-label" for="Movie_ReleaseDate">Release Date</label>
    <input class="form-control" type="date" data-val="true"
        data-val-required="The Release Date field is required."
        id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
    <span class="text-danger field-validation-valid"
        data-valmsg-for="Movie.ReleaseDate" data-valmsg-replace="true"></span>
</div>
```

Notice that the `data-` attributes in the HTML output correspond to the validation attributes for the `Movie.ReleaseDate` property. The `data-val-required` attribute contains an error message to display if the user doesn't fill in the release date field. jQuery Unobtrusive Validation passes this value to the jQuery Validation [required()](https://jqueryvalidation.org/required-method/) method, which then displays that message in the accompanying **\<span>** element.

Data type validation is based on the .NET type of a property, unless that is overridden by a [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute. Browsers have their own default error messages, but the jQuery Validation Unobtrusive Validation package can override those messages. `[DataType]` attributes and subclasses such as [\[EmailAddress\]](/en-us/dotnet/api/system.componentmodel.dataannotations.emailaddressattribute) let you specify the error message.

## Unobtrusive validation

For information on unobtrusive validation, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/1111).

### Add Validation to Dynamic Forms

jQuery Unobtrusive Validation passes validation logic and parameters to jQuery Validation when the page first loads. Therefore, validation doesn't work automatically on dynamically generated forms. To enable validation, tell jQuery Unobtrusive Validation to parse the dynamic form immediately after you create it. For example, the following code sets up client-side validation on a form added via AJAX.

```
$.get({
    url: "https://url/that/returns/a/form",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add form. " + errorThrown);
    },
    success: function(newFormHTML) {
        var container = document.getElementById("form-container");
        container.insertAdjacentHTML("beforeend", newFormHTML);
        var forms = container.getElementsByTagName("form");
        var newForm = forms[forms.length - 1];
        $.validator.unobtrusive.parse(newForm);
    }
})
```

The `$.validator.unobtrusive.parse()` method accepts a jQuery selector for its one argument. This method tells jQuery Unobtrusive Validation to parse the `data-` attributes of forms within that selector. The values of those attributes are then passed to the jQuery Validation plugin.

### Add Validation to Dynamic Controls

The `$.validator.unobtrusive.parse()` method works on an entire form, not on individual dynamically generated controls, such as `<input>` and `<select/>`. To reparse the form, remove the validation data that was added when the form was parsed earlier, as shown in the following example:

```
$.get({
    url: "https://url/that/returns/a/control",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add control. " + errorThrown);
    },
    success: function(newInputHTML) {
        var form = document.getElementById("my-form");
        form.insertAdjacentHTML("beforeend", newInputHTML);
        $(form).removeData("validator")    // Added by jQuery Validation
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})
```

## Custom client-side validation

Custom client-side validation is done by generating `data-` HTML attributes that work with a custom jQuery Validation adapter. The following sample adapter code was written for the `[ClassicMovie]` and `[ClassicMovieWithClientValidator]` attributes that were introduced earlier in this article:

```
$.validator.addMethod('classicmovie', function (value, element, params) {
    var genre = $(params[0]).val(), year = params[1], date = new Date(value);

    // The Classic genre has a value of '0'.
    if (genre && genre.length > 0 && genre[0] === '0') {
        // The release date for a Classic is valid if it's no greater than the given year.
        return date.getUTCFullYear() <= year;
    }

    return true;
});

$.validator.unobtrusive.adapters.add('classicmovie', ['year'], function (options) {
    var element = $(options.form).find('select#Movie_Genre')[0];

    options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
    options.messages['classicmovie'] = options.message;
});
```

For information about how to write adapters, see the [jQuery Validation documentation](https://jqueryvalidation.org/documentation/).

The use of an adapter for a given field is triggered by `data-` attributes that:

* Flag the field as being subject to validation (`data-val="true"`).
* Identify a validation rule name and error message text (for example, `data-val-rulename="Error message."`).
* Provide any additional parameters the validator needs (for example, `data-val-rulename-param1="value"`).

The following example shows the `data-` attributes for the [sample app's](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples) `ClassicMovie` attribute:

```
<input class="form-control" type="date"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year no later than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The Release Date field is required."
    id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
```

As noted earlier, [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) and [HTML helpers](../views/overview?view=aspnetcore-10.0) use information from validation attributes to render `data-` attributes. There are two options for writing code that results in the creation of custom `data-` HTML attributes:

* Create a class that derives from [AttributeAdapterBase\<TAttribute>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.attributeadapterbase-1) and a class that implements [IValidationAttributeAdapterProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.ivalidationattributeadapterprovider), and register your attribute and its adapter in DI. This method follows the [single responsibility principle](https://wikipedia.org/wiki/Single_responsibility_principle) in that server-related and client-related validation code is in separate classes. The adapter also has the advantage that since it's registered in DI, other services in DI are available to it if needed.
* Implement [IClientModelValidator](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.iclientmodelvalidator) in your [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) class. This method might be appropriate if the attribute doesn't do any server-side validation and doesn't need any services from DI.

### AttributeAdapter for client-side validation

This method of rendering `data-` attributes in HTML is used by the `ClassicMovie` attribute in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples). To add client validation by using this method:

1. Create an attribute adapter class for the custom validation attribute. Derive the class from [AttributeAdapterBase\<TAttribute>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.attributeadapterbase-1). Create an `AddValidation` method that adds `data-` attributes to the rendered output, as shown in this example:

   ```
   public class ClassicMovieAttributeAdapter : AttributeAdapterBase<ClassicMovieAttribute>
   {
       public ClassicMovieAttributeAdapter(
           ClassicMovieAttribute attribute, IStringLocalizer? stringLocalizer)
           : base(attribute, stringLocalizer)
       {

       }

       public override void AddValidation(ClientModelValidationContext context)
       {
           MergeAttribute(context.Attributes, "data-val", "true");
           MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage(context));

           var year = Attribute.Year.ToString(CultureInfo.InvariantCulture);
           MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
       }

       public override string GetErrorMessage(ModelValidationContextBase validationContext)
           => Attribute.GetErrorMessage();
   }
   ```

2. Create an adapter provider class that implements [IValidationAttributeAdapterProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.ivalidationattributeadapterprovider). In the [GetAttributeAdapter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.ivalidationattributeadapterprovider.getattributeadapter) method pass in the custom attribute to the adapter's constructor, as shown in this example:

   ```
   public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
   {
       private readonly IValidationAttributeAdapterProvider baseProvider =
           new ValidationAttributeAdapterProvider();

       public IAttributeAdapter? GetAttributeAdapter(
           ValidationAttribute attribute, IStringLocalizer? stringLocalizer)
       {
           if (attribute is ClassicMovieAttribute classicMovieAttribute)
           {
               return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
           }

           return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
       }
   }
   ```

3. Register the adapter provider for DI in `Program.cs`:

   ```
   builder.Services.AddRazorPages()
       .AddMvcOptions(options =>
       {
           options.MaxModelValidationErrors = 50;
           options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
               _ => "The field is required.");
       });

   builder.Services.AddSingleton
       <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
   ```

### IClientModelValidator for client-side validation

This method of rendering `data-` attributes in HTML is used by the `ClassicMovieWithClientValidator` attribute in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples). To add client validation by using this method:

* In the custom validation attribute, implement the [IClientModelValidator](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.iclientmodelvalidator) interface and create an [AddValidation](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.iclientmodelvalidator.addvalidation) method. In the `AddValidation` method, add `data-` attributes for validation, as shown in the following example:

  ```
  public class ClassicMovieWithClientValidatorAttribute :
      ValidationAttribute, IClientModelValidator
  {
      public ClassicMovieWithClientValidatorAttribute(int year)
          => Year = year;

      public int Year { get; }

      public void AddValidation(ClientModelValidationContext context)
      {
          MergeAttribute(context.Attributes, "data-val", "true");
          MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());

          var year = Year.ToString(CultureInfo.InvariantCulture);
          MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
      }

      public string GetErrorMessage() =>
          $"Classic movies must have a release year no later than {Year}.";

      protected override ValidationResult? IsValid(
          object? value, ValidationContext validationContext)
      {
          var movie = (Movie)validationContext.ObjectInstance;
          var releaseYear = ((DateTime)value!).Year;

          if (movie.Genre == Genre.Classic && releaseYear > Year)
          {
              return new ValidationResult(GetErrorMessage());
          }

          return ValidationResult.Success;
      }

      private static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
      {
          if (attributes.ContainsKey(key))
          {
              return false;
          }

          attributes.Add(key, value);
          return true;
      }
  }
  ```

## Disable client-side validation

The following code disables client validation in Razor Pages:

```
builder.Services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });
```

Other options to disable client-side validation:

* Comment out the reference to `_ValidationScriptsPartial` in all the `.cshtml` files.
* Remove the contents of the *Pages\Shared\_ValidationScriptsPartial.cshtml* file.

The preceding approach won't prevent client-side validation of ASP.NET Core Identity Razor class library. For more information, see [Scaffold Identity in ASP.NET Core projects](../../security/authentication/scaffold-identity?view=aspnetcore-10.0).

## Additional resources

* [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations)
* [Model Binding](model-binding?view=aspnetcore-10.0)

This article explains how to validate user input in an ASP.NET Core MVC or Razor Pages app.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Model state

Model state represents errors that come from two subsystems: model binding and model validation. Errors that originate from [model binding](model-binding?view=aspnetcore-10.0) are generally data conversion errors. For example, an "x" is entered in an integer field. Model validation occurs after model binding and reports errors where data doesn't conform to business rules. For example, a 0 is entered in a field that expects a rating between 1 and 5.

Both model binding and model validation occur before the execution of a controller action or a Razor Pages handler method. For web apps, it's the app's responsibility to inspect `ModelState.IsValid` and react appropriately. Web apps typically redisplay the page with an error message:

```
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

Web API controllers don't have to check `ModelState.IsValid` if they have the [\[ApiController\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute) attribute. In that case, an automatic HTTP 400 response containing error details is returned when model state is invalid. For more information, see [Automatic HTTP 400 responses](../../web-api/?view=aspnetcore-10.0#automatic-http-400-responses).

## Rerun validation

Validation is automatic, but you might want to repeat it manually. For example, you might compute a value for a property and want to rerun validation after setting the property to the computed value. To rerun validation, call [ModelStateDictionary.ClearValidationState](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary.clearvalidationstate) to clear validation specific to the model being validated followed by `TryValidateModel`:

```
public async Task<IActionResult> OnPostTryValidateAsync()
{
    var modifiedReleaseDate = DateTime.Now.Date;
    Movie.ReleaseDate = modifiedReleaseDate;

    ModelState.ClearValidationState(nameof(Movie));
    if (!TryValidateModel(Movie, nameof(Movie)))
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

## Validation attributes

Validation attributes let you specify validation rules for model properties. The following example from the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples) shows a model class that is annotated with validation attributes. The `[ClassicMovie]` attribute is a custom validation attribute and the others are built in. Not shown is `[ClassicMovieWithClientValidator]`, which shows an alternative way to implement a custom attribute.

```
public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}
```

## Built-in attributes

Here are some of the built-in validation attributes:

* [\[ValidateNever\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.validateneverattribute): Indicates that a property or parameter should be excluded from validation.
* [\[CreditCard\]](/en-us/dotnet/api/system.componentmodel.dataannotations.creditcardattribute): Validates that the property has a credit card format. Requires [jQuery Validation Additional Methods](https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/additional-methods.js).
* [\[Compare\]](/en-us/dotnet/api/system.componentmodel.dataannotations.compareattribute): Validates that two properties in a model match.
* [\[EmailAddress\]](/en-us/dotnet/api/system.componentmodel.dataannotations.emailaddressattribute): Validates that the property has an email format.
* [\[Phone\]](/en-us/dotnet/api/system.componentmodel.dataannotations.phoneattribute): Validates that the property has a telephone number format.
* [\[Range\]](/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute): Validates that the property value falls within a specified range.
* [\[RegularExpression\]](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute): Validates that the property value matches a specified regular expression.
* [\[Required\]](/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute): Validates that the field isn't null. See [`[Required]` attribute](#non-nullable-reference-types-and-the-required-attribute) for details about this attribute's behavior.
* [\[StringLength\]](/en-us/dotnet/api/system.componentmodel.dataannotations.stringlengthattribute): Validates that a string property value doesn't exceed a specified length limit.
* [\[Url\]](/en-us/dotnet/api/system.componentmodel.dataannotations.urlattribute): Validates that the property has a URL format.
* [\[Remote\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.remoteattribute): Validates input on the client by calling an action method on the server. See [`[Remote]` attribute](#remote-attribute) for details about this attribute's behavior.

A complete list of validation attributes can be found in the [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) namespace.

### Error messages

Validation attributes let you specify the error message to be displayed for invalid input. For example:

```
[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]
```

Internally, the attributes call [String.Format](/en-us/dotnet/api/system.string.format) with a placeholder for the field name and sometimes additional placeholders. For example:

```
[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]
```

When applied to a `Name` property, the error message created by the preceding code would be "Name length must be between 6 and 8.".

To find out which parameters are passed to `String.Format` for a particular attribute's error message, see the [DataAnnotations source code](https://github.com/dotnet/runtime/tree/main/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations).

## Non-nullable reference types and \[Required] attribute

The validation system treats non-nullable parameters or bound properties as if they had a `[Required(AllowEmptyStrings = true)]` attribute. By [enabling `Nullable` contexts](/en-us/dotnet/csharp/nullable-references#nullable-contexts), MVC implicitly starts validating non-nullable properties or parameters as if they had been attributed with the `[Required(AllowEmptyStrings = true)]` attribute. Consider the following code:

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

If the app was built with `<Nullable>enable</Nullable>`, a missing value for `Name` in a JSON or form post results in a validation error. Use a nullable reference type to allow null or missing values to be specified for the `Name` property:

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

This behavior can be disabled by configuring [SuppressImplicitRequiredAttributeForNonNullableReferenceTypes](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.suppressimplicitrequiredattributefornonnullablereferencetypes#microsoft-aspnetcore-mvc-mvcoptions-suppressimplicitrequiredattributefornonnullablereferencetypes) in `Startup.ConfigureServices`:

```
services.AddControllers(options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);
```

### \[Required] validation on the server

On the server, a required value is considered missing if the property is null. A non-nullable field is always valid, and the `[Required]` attribute's error message is never displayed.

However, model binding for a non-nullable property may fail, resulting in an error message such as `The value '' is invalid`. To specify a custom error message for server-side validation of non-nullable types, you have the following options:

* Make the field nullable (for example, `decimal?` instead of `decimal`). [Nullable\<T>](/en-us/dotnet/csharp/programming-guide/nullable-types/) value types are treated like standard nullable types.

* Specify the default error message to be used by model binding, as shown in the following example:

  ```
  services.AddRazorPages()
      .AddMvcOptions(options =>
      {
          options.MaxModelValidationErrors = 50;
          options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
              _ => "The field is required.");
      });

  services.AddSingleton<IValidationAttributeAdapterProvider,
      CustomValidationAttributeAdapterProvider>();
  ```

  For more information about model binding errors that you can set default messages for, see [DefaultModelBindingMessageProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.metadata.defaultmodelbindingmessageprovider#methods).

### \[Required] validation on the client

Non-nullable types and strings are handled differently on the client compared to the server. On the client:

* A value is considered present only if input is entered for it. Therefore, client-side validation handles non-nullable types the same as nullable types.
* Whitespace in a string field is considered valid input by the jQuery Validation [required](https://jqueryvalidation.org/required-method/) method. Server-side validation considers a required string field invalid if only whitespace is entered.

As noted earlier, non-nullable types are treated as though they had a `[Required(AllowEmptyStrings = true)]` attribute. That means you get client-side validation even if you don't apply the `[Required(AllowEmptyStrings = true)]` attribute. But if you don't use the attribute, you get a default error message. To specify a custom error message, use the attribute.

## \[Remote] attribute

The [\[Remote\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.remoteattribute) attribute implements client-side validation that requires calling a method on the server to determine whether field input is valid. For example, the app may need to verify whether a user name is already in use.

To implement remote validation:

1. Create an action method for JavaScript to call. The jQuery Validation [remote](https://jqueryvalidation.org/remote-method/) method expects a JSON response:

   * `true` means the input data is valid.
   * `false`, `undefined`, or `null` means the input is invalid. Display the default error message.
   * Any other string means the input is invalid. Display the string as a custom error message.

   Here's an example of an action method that returns a custom error message:

   ```
   [AcceptVerbs("GET", "POST")]
   public IActionResult VerifyEmail(string email)
   {
       if (!_userService.VerifyEmail(email))
       {
           return Json($"Email {email} is already in use.");
       }

       return Json(true);
   }
   ```

2. In the model class, annotate the property with a `[Remote]` attribute that points to the validation action method, as shown in the following example:

   ```
   [Remote(action: "VerifyEmail", controller: "Users")]
   public string Email { get; set; }
   ```

### Additional fields

The [AdditionalFields](/en-us/dotnet/api/microsoft.aspnetcore.mvc.remoteattribute.additionalfields) property of the `[Remote]` attribute lets you validate combinations of fields against data on the server. For example, if the `User` model had `FirstName` and `LastName` properties, you might want to verify that no existing users already have that pair of names. The following example shows how to use `AdditionalFields`:

```
[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]
[Display(Name = "First Name")]
public string FirstName { get; set; }

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
[Display(Name = "Last Name")]
public string LastName { get; set; }
```

`AdditionalFields` could be set explicitly to the strings "FirstName" and "LastName", but using the [nameof](/en-us/dotnet/csharp/language-reference/keywords/nameof) operator simplifies later refactoring. The action method for this validation must accept both `firstName` and `lastName` arguments:

```
[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userService.VerifyName(firstName, lastName))
    {
        return Json($"A user named {firstName} {lastName} already exists.");
    }

    return Json(true);
}
```

When the user enters a first or last name, JavaScript makes a remote call to see if that pair of names has been taken.

To validate two or more additional fields, provide them as a comma-delimited list. For example, to add a `MiddleName` property to the model, set the `[Remote]` attribute as shown in the following example:

```
[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName) + "," + nameof(LastName))]
public string MiddleName { get; set; }
```

`AdditionalFields`, like all attribute arguments, must be a constant expression. Therefore, don't use an [interpolated string](/en-us/dotnet/csharp/language-reference/keywords/interpolated-strings) or call [Join](/en-us/dotnet/api/system.string.join) to initialize `AdditionalFields`.

## Alternatives to built-in attributes

If you need validation not provided by built-in attributes, you can:

* [Create custom attributes](#custom-attributes).
* [Implement IValidatableObject](#ivalidatableobject).

## Custom attributes

For scenarios that the built-in validation attributes don't handle, you can create custom validation attributes. Create a class that inherits from [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute), and override the [IsValid](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute.isvalid) method.

The `IsValid` method accepts an object named *value*, which is the input to be validated. An overload also accepts a [ValidationContext](/en-us/dotnet/api/system.componentmodel.dataannotations.validationcontext) object, which provides additional information, such as the model instance created by model binding.

The following example validates that the release date for a movie in the *Classic* genre isn't later than a specified year. The `[ClassicMovie]` attribute:

* Is only run on the server.
* For Classic movies, validates the release date:

```
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
    {
        Year = year;
    }

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult IsValid(object value,
        ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}
```

The `movie` variable in the preceding example represents a `Movie` object that contains the data from the form submission. When validation fails, a [ValidationResult](/en-us/dotnet/api/system.componentmodel.dataannotations.validationresult) with an error message is returned.

## IValidatableObject

The preceding example works only with `Movie` types. Another option for class-level validation is to implement [IValidatableObject](/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject) in the model class, as shown in the following example:

```
public class ValidatableMovie : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
        {
            yield return new ValidationResult(
                $"Classic movies must have a release year no later than {_classicYear}.",
                new[] { nameof(ReleaseDate) });
        }
    }
}
```

## Top-level node validation

Top-level nodes include:

* Action parameters
* Controller properties
* Page handler parameters
* Page model properties

Model-bound top-level nodes are validated in addition to validating model properties. In the following example from the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples), the `VerifyPhone` method uses the [RegularExpressionAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.regularexpressionattribute) to validate the `phone` action parameter:

```
[AcceptVerbs("GET", "POST")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
{
    if (!ModelState.IsValid)
    {
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");
    }

    return Json(true);
}
```

Top-level nodes can use [BindRequiredAttribute](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindrequiredattribute) with validation attributes. In the following example from the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples), the `CheckAge` method specifies that the `age` parameter must be bound from the query string when the form is submitted:

```
[HttpPost]
public IActionResult CheckAge([BindRequired, FromQuery] int age)
{
```

In the Check Age page (`CheckAge.cshtml`), there are two forms. The first form submits an `Age` value of `99` as a query string parameter: `https://localhost:5001/Users/CheckAge?Age=99`.

When a properly formatted `age` parameter from the query string is submitted, the form validates.

The second form on the Check Age page submits the `Age` value in the body of the request, and validation fails. Binding fails because the `age` parameter must come from a query string.

## Maximum errors

Validation stops when the maximum number of errors is reached (200 by default). You can configure this number with the following code in `Startup.ConfigureServices`:

```
services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            _ => "The field is required.");
    });

services.AddSingleton<IValidationAttributeAdapterProvider,
    CustomValidationAttributeAdapterProvider>();
```

## Maximum recursion

[ValidationVisitor](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.validationvisitor) traverses the object graph of the model being validated. For models that are deep or are infinitely recursive, validation may result in stack overflow. [MvcOptions.MaxValidationDepth](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.maxvalidationdepth) provides a way to stop validation early if the visitor recursion exceeds a configured depth. The default value of `MvcOptions.MaxValidationDepth` is 32.

## Automatic short-circuit

Validation is automatically short-circuited (skipped) if the model graph doesn't require validation. Objects that the runtime skips validation for include collections of primitives (such as `byte[]`, `string[]`, `Dictionary<string, string>`) and complex object graphs that don't have any validators.

## Client-side validation

Client-side validation prevents submission until the form is valid. The Submit button runs JavaScript that either submits the form or displays error messages.

Client-side validation avoids an unnecessary round trip to the server when there are input errors on a form. The following script references in `_Layout.cshtml` and `_ValidationScriptsPartial.cshtml` support client-side validation:

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js"></script>
```

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.js"></script>
```

The [jQuery Unobtrusive Validation](https://github.com/aspnet/jquery-validation-unobtrusive) script is a custom Microsoft front-end library that builds on the popular [jQuery Validation](https://jqueryvalidation.org/) plugin. Without jQuery Unobtrusive Validation, you would have to code the same validation logic in two places: once in the server-side validation attributes on model properties, and then again in client-side scripts. Instead, [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) and [HTML helpers](../views/overview?view=aspnetcore-10.0) use the validation attributes and type metadata from model properties to render HTML 5 `data-` attributes for the form elements that need validation. jQuery Unobtrusive Validation parses the `data-` attributes and passes the logic to jQuery Validation, effectively "copying" the server-side validation logic to the client. You can display validation errors on the client using tag helpers as shown here:

```
<div class="form-group">
    <label asp-for="Movie.ReleaseDate" class="control-label"></label>
    <input asp-for="Movie.ReleaseDate" class="form-control" />
    <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
```

The preceding tag helpers render the following HTML:

```
<div class="form-group">
    <label class="control-label" for="Movie_ReleaseDate">Release Date</label>
    <input class="form-control" type="date" data-val="true"
        data-val-required="The Release Date field is required."
        id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
    <span class="text-danger field-validation-valid"
        data-valmsg-for="Movie.ReleaseDate" data-valmsg-replace="true"></span>
</div>
```

Notice that the `data-` attributes in the HTML output correspond to the validation attributes for the `Movie.ReleaseDate` property. The `data-val-required` attribute contains an error message to display if the user doesn't fill in the release date field. jQuery Unobtrusive Validation passes this value to the jQuery Validation [required()](https://jqueryvalidation.org/required-method/) method, which then displays that message in the accompanying **\<span>** element.

Data type validation is based on the .NET type of a property, unless that is overridden by a [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatype) attribute. Browsers have their own default error messages, but the jQuery Validation Unobtrusive Validation package can override those messages. `[DataType]` attributes and subclasses such as [\[EmailAddress\]](/en-us/dotnet/api/system.componentmodel.dataannotations.emailaddressattribute) let you specify the error message.

## Unobtrusive validation

For information on unobtrusive validation, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/1111).

### Add Validation to Dynamic Forms

jQuery Unobtrusive Validation passes validation logic and parameters to jQuery Validation when the page first loads. Therefore, validation doesn't work automatically on dynamically generated forms. To enable validation, tell jQuery Unobtrusive Validation to parse the dynamic form immediately after you create it. For example, the following code sets up client-side validation on a form added via AJAX.

```
$.get({
    url: "https://url/that/returns/a/form",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add form. " + errorThrown);
    },
    success: function(newFormHTML) {
        var container = document.getElementById("form-container");
        container.insertAdjacentHTML("beforeend", newFormHTML);
        var forms = container.getElementsByTagName("form");
        var newForm = forms[forms.length - 1];
        $.validator.unobtrusive.parse(newForm);
    }
})
```

The `$.validator.unobtrusive.parse()` method accepts a jQuery selector for its one argument. This method tells jQuery Unobtrusive Validation to parse the `data-` attributes of forms within that selector. The values of those attributes are then passed to the jQuery Validation plugin.

### Add Validation to Dynamic Controls

The `$.validator.unobtrusive.parse()` method works on an entire form, not on individual dynamically generated controls, such as `<input>` and `<select/>`. To reparse the form, remove the validation data that was added when the form was parsed earlier, as shown in the following example:

```
$.get({
    url: "https://url/that/returns/a/control",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add control. " + errorThrown);
    },
    success: function(newInputHTML) {
        var form = document.getElementById("my-form");
        form.insertAdjacentHTML("beforeend", newInputHTML);
        $(form).removeData("validator")    // Added by jQuery Validation
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})
```

## Custom client-side validation

Custom client-side validation is done by generating `data-` HTML attributes that work with a custom jQuery Validation adapter. The following sample adapter code was written for the `[ClassicMovie]` and `[ClassicMovieWithClientValidator]` attributes that were introduced earlier in this article:

```
$.validator.addMethod('classicmovie', function (value, element, params) {
    var genre = $(params[0]).val(), year = params[1], date = new Date(value);

    // The Classic genre has a value of '0'.
    if (genre && genre.length > 0 && genre[0] === '0') {
        // The release date for a Classic is valid if it's no greater than the given year.
        return date.getUTCFullYear() <= year;
    }

    return true;
});

$.validator.unobtrusive.adapters.add('classicmovie', ['year'], function (options) {
    var element = $(options.form).find('select#Movie_Genre')[0];

    options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
    options.messages['classicmovie'] = options.message;
});
```

For information about how to write adapters, see the [jQuery Validation documentation](https://jqueryvalidation.org/documentation/).

The use of an adapter for a given field is triggered by `data-` attributes that:

* Flag the field as being subject to validation (`data-val="true"`).
* Identify a validation rule name and error message text (for example, `data-val-rulename="Error message."`).
* Provide any additional parameters the validator needs (for example, `data-val-rulename-param1="value"`).

The following example shows the `data-` attributes for the [sample app's](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples) `ClassicMovie` attribute:

```
<input class="form-control" type="date"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year no later than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The Release Date field is required."
    id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
```

As noted earlier, [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) and [HTML helpers](../views/overview?view=aspnetcore-10.0) use information from validation attributes to render `data-` attributes. There are two options for writing code that results in the creation of custom `data-` HTML attributes:

* Create a class that derives from [AttributeAdapterBase\<TAttribute>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.attributeadapterbase-1) and a class that implements [IValidationAttributeAdapterProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.ivalidationattributeadapterprovider), and register your attribute and its adapter in DI. This method follows the [single responsibility principle](https://wikipedia.org/wiki/Single_responsibility_principle) in that server-related and client-related validation code is in separate classes. The adapter also has the advantage that since it's registered in DI, other services in DI are available to it if needed.
* Implement [IClientModelValidator](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.iclientmodelvalidator) in your [ValidationAttribute](/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute) class. This method might be appropriate if the attribute doesn't do any server-side validation and doesn't need any services from DI.

### AttributeAdapter for client-side validation

This method of rendering `data-` attributes in HTML is used by the `ClassicMovie` attribute in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples). To add client validation by using this method:

1. Create an attribute adapter class for the custom validation attribute. Derive the class from [AttributeAdapterBase\<TAttribute>](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.attributeadapterbase-1). Create an `AddValidation` method that adds `data-` attributes to the rendered output, as shown in this example:

   ```
   public class ClassicMovieAttributeAdapter : AttributeAdapterBase<ClassicMovieAttribute>
   {
       public ClassicMovieAttributeAdapter(ClassicMovieAttribute attribute,
           IStringLocalizer stringLocalizer)
           : base(attribute, stringLocalizer)
       {

       }

       public override void AddValidation(ClientModelValidationContext context)
       {
           MergeAttribute(context.Attributes, "data-val", "true");
           MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage(context));

           var year = Attribute.Year.ToString(CultureInfo.InvariantCulture);
           MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
       }

       public override string GetErrorMessage(ModelValidationContextBase validationContext) =>
           Attribute.GetErrorMessage();
   }
   ```

2. Create an adapter provider class that implements [IValidationAttributeAdapterProvider](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.ivalidationattributeadapterprovider). In the [GetAttributeAdapter](/en-us/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.ivalidationattributeadapterprovider.getattributeadapter) method pass in the custom attribute to the adapter's constructor, as shown in this example:

   ```
   public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
   {
       private readonly IValidationAttributeAdapterProvider baseProvider =
           new ValidationAttributeAdapterProvider();

       public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute,
           IStringLocalizer stringLocalizer)
       {
           if (attribute is ClassicMovieAttribute classicMovieAttribute)
           {
               return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
           }

           return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
       }
   }
   ```

3. Register the adapter provider for DI in `Startup.ConfigureServices`:

   ```
   services.AddRazorPages()
       .AddMvcOptions(options =>
       {
           options.MaxModelValidationErrors = 50;
           options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
               _ => "The field is required.");
       });

   services.AddSingleton<IValidationAttributeAdapterProvider,
       CustomValidationAttributeAdapterProvider>();
   ```

### IClientModelValidator for client-side validation

This method of rendering `data-` attributes in HTML is used by the `ClassicMovieWithClientValidator` attribute in the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/validation/samples). To add client validation by using this method:

* In the custom validation attribute, implement the [IClientModelValidator](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.iclientmodelvalidator) interface and create an [AddValidation](/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.validation.iclientmodelvalidator.addvalidation) method. In the `AddValidation` method, add `data-` attributes for validation, as shown in the following example:

  ```
  public class ClassicMovieWithClientValidatorAttribute :
      ValidationAttribute, IClientModelValidator
  {
      public ClassicMovieWithClientValidatorAttribute(int year)
      {
          Year = year;
      }

      public int Year { get; }

      public void AddValidation(ClientModelValidationContext context)
      {
          MergeAttribute(context.Attributes, "data-val", "true");
          MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());

          var year = Year.ToString(CultureInfo.InvariantCulture);
          MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
      }

      public string GetErrorMessage() =>
          $"Classic movies must have a release year no later than {Year}.";

      protected override ValidationResult IsValid(object value,
          ValidationContext validationContext)
      {
          var movie = (Movie)validationContext.ObjectInstance;
          var releaseYear = ((DateTime)value).Year;

          if (movie.Genre == Genre.Classic && releaseYear > Year)
          {
              return new ValidationResult(GetErrorMessage());
          }

          return ValidationResult.Success;
      }

      private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
      {
          if (attributes.ContainsKey(key))
          {
              return false;
          }

          attributes.Add(key, value);
          return true;
      }
  }
  ```

## Disable client-side validation

The following code disables client validation in Razor Pages:

```
services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });
```

Other options to disable client-side validation:

* Comment out the reference to `_ValidationScriptsPartial` in all the `.cshtml` files.
* Remove the contents of the *Pages\Shared\_ValidationScriptsPartial.cshtml* file.

The preceding approach won't prevent client-side validation of ASP.NET Core Identity Razor class library. For more information, see [Scaffold Identity in ASP.NET Core projects](../../security/authentication/scaffold-identity?view=aspnetcore-10.0).

## Additional resources

* [System.ComponentModel.DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations)
* [Model Binding](model-binding?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/areas
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/controllers/areas.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Areas in ASP.NET Core

By [Dhananjay Kumar](https://twitter.com/debug_mode) and [Rick Anderson](https://twitter.com/RickAndMSFT)

Areas are an ASP.NET feature used to organize related functionality into a group as a separate:

* Namespace for routing.
* Folder structure for views and Razor Pages.

Using areas creates a hierarchy for the purpose of routing by adding another route parameter, `area`, to `controller` and `action` or a Razor Page `page`.

Areas provide a way to partition an ASP.NET Core Web app into smaller functional groups, each with its own set of Razor Pages, controllers, views, and models. An area is effectively a structure inside an app. In an ASP.NET Core web project, logical components like Pages, Model, Controller, and View are kept in different folders. The ASP.NET Core runtime uses naming conventions to create the relationship between these components. For a large app, it may be advantageous to partition the app into separate high level areas of functionality. For instance, an e-commerce app with multiple business units, such as checkout, billing, and search. Each of these units have their own area to contain views, controllers, Razor Pages, and models.

Consider using Areas in a project when:

* The app is made of multiple high-level functional components that can be logically separated.
* You want to partition the app so that each functional area can be worked on independently.

If you're using Razor Pages, see [Areas with Razor Pages](#areas-with-razor-pages) in this document.

## Areas for controllers with views

A typical ASP.NET Core web app using areas, controllers, and views contains the following:

* An [Area folder structure](#area-folder-structure).

* Controllers with the [`[Area]`](#attribute) attribute to associate the controller with the area:

  ```
  [Area("Products")]
  public class ManageController : Controller
  {
  ```

* The [area route added to `Program.cs`](#add-area-route):

  ```
  var builder = WebApplication.CreateBuilder(args);

  builder.Services.AddControllersWithViews();

  var app = builder.Build();

  if (!app.Environment.IsDevelopment())
  {
      app.UseExceptionHandler("/Home/Error");
      app.UseHsts();
  }

  app.UseHttpsRedirection();
  app.UseStaticFiles();

  app.UseRouting();

  app.UseAuthorization();

  app.MapControllerRoute(
      name: "MyArea",
      pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

  app.MapControllerRoute(
      name: "default",
      pattern: "{controller=Home}/{action=Index}/{id?}");

  app.Run();
  ```

### Area folder structure

Consider an app that has two logical groups, *Products* and *Services*. Using areas, the folder structure would be similar to the following:

* Project name

  * Areas

    * Products

      * Controllers

        * HomeController.cs
        * ManageController.cs

      * Views

        * Home
          * Index.cshtml

        * Manage

          * Index.cshtml
          * About.cshtml

    * Services

      * Controllers
        * HomeController.cs
      * Views
        * Home
          * Index.cshtml

While the preceding layout is typical when using Areas, only the view files are required to use this folder structure. View discovery searches for a matching area view file in the following order:

```
/Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml
/Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml
/Views/Shared/<Action-Name>.cshtml
/Pages/Shared/<Action-Name>.cshtml
```

[]()

### Associate the controller with an Area

Area controllers are designated with the [`[Area]`](/en-us/dotnet/api/microsoft.aspnetcore.mvc.areaattribute) attribute:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.Docs.Samples;

namespace MVCareas.Areas.Products.Controllers;

[Area("Products")]
public class ManageController : Controller
{
    public IActionResult Index()
    {
        ViewData["routeInfo"] = ControllerContext.MyDisplayRouteInfo();
        return View();
    }

    public IActionResult About()
    {
        ViewData["routeInfo"] = ControllerContext.MyDisplayRouteInfo();
        return View();
    }
}
```

### Add Area route

Area routes typically use [conventional routing](routing?view=aspnetcore-10.0#cr) rather than [attribute routing](routing?view=aspnetcore-10.0#ar). Conventional routing is order-dependent. In general, routes with areas should be placed earlier in the route table as they're more specific than routes without an area.

`{area:...}` can be used as a token in route templates if url space is uniform across all areas:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "MyArea",
    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

In the preceding code, `exists` applies a constraint that the route must match an area. Using `{area:...}` with `MapControllerRoute`:

* Is the least complicated mechanism to adding routing to areas.
* Matches all controllers with the `[Area("Area name")]` attribute.

The following code uses [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute) to create two named area routes:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapAreaControllerRoute(
    name: "MyAreaProducts",
    areaName: "Products",
    pattern: "Products/{controller=Home}/{action=Index}/{id?}");

app.MapAreaControllerRoute(
    name: "MyAreaServices",
    areaName: "Services",
    pattern: "Services/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

For more information, see [Area routing](routing?view=aspnetcore-10.0#areas).

### Link generation with MVC areas

The following code from the [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/6.0) shows link generation with the area specified:

```
<li>Anchor Tag Helper links</li>
<ul>
    <li>
        <a asp-area="Products" asp-controller="Home" asp-action="About">
            Products/Home/About
        </a>
    </li>
    <li>
        <a asp-area="Services" asp-controller="Home" asp-action="About">
            Services About
        </a>
    </li>
    <li>
        <a asp-area="" asp-controller="Home" asp-action="About">
            /Home/About
        </a>
    </li>
</ul>
<li>Html.ActionLink generated links</li>
<ul>
    <li>
        @Html.ActionLink("Product/Manage/About", "About", "Manage",
                                                new { area = "Products" })
    </li>
</ul>
<li>Url.Action generated links</li>
<ul>
    <li>
        <a href='@Url.Action("About", "Manage", new { area = "Products" })'>
            Products/Manage/About
        </a>
    </li>
</ul>
```

The sample download includes a [partial view](../views/partial?view=aspnetcore-10.0) that contains:

* The preceding links.
* Links similar to the preceding except `area` is not specified.

The partial view is referenced in the [layout file](../views/layout?view=aspnetcore-10.0), so every page in the app displays the generated links. The links generated without specifying the area are only valid when referenced from a page in the same area and controller.

When the area or controller is not specified, routing depends on the [ambient](routing?view=aspnetcore-10.0#ambient) values. The current route values of the current request are considered ambient values for link generation. In many cases for the sample app, using the ambient values generates incorrect links with the markup that doesn't specify the area.

For more information, see [Routing to controller actions](routing?view=aspnetcore-10.0).

### Shared layout for Areas using the \_ViewStart.cshtml file

To share a common layout for the entire app, keep the *\_ViewStart.cshtml* in the [application root folder](#arf). For more information, see [Layout in ASP.NET Core](../views/layout?view=aspnetcore-10.0)

[]()

### Application root folder

The application root folder is the folder containing the `Program.cs` file in a web app created with the ASP.NET Core templates.

### \_ViewImports.cshtml

*/Views/\_ViewImports.cshtml*, for MVC, and */Pages/\_ViewImports.cshtml* for Razor Pages, is not imported to views in areas. Use one of the following approaches to provide view imports to all views:

* Add *\_ViewImports.cshtml* to the [application root folder](#arf). A *\_ViewImports.cshtml* in the application root folder will apply to all views in the app.

* Copy the *\_ViewImports.cshtml* file to the appropriate view folder under areas. For example, a Razor Pages app created with individual accounts has a *\_ViewImports.cshtml* file in the following folders:

  * */Areas/Identity/Pages/\_ViewImports.cshtml*
  * */Pages/\_ViewImports.cshtml*

The *\_ViewImports.cshtml* file typically contains [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) imports, `@using`, and `@inject` statements. For more information, see [Importing Shared Directives](../views/layout?view=aspnetcore-10.0#importing-shared-directives).

[]()

### Change default area folder where views are stored

The following code changes the default area folder from `"Areas"` to `"MyAreas"`:

```
using Microsoft.AspNetCore.Mvc.Razor;

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<RazorViewEngineOptions>(options =>
{
    options.AreaViewLocationFormats.Clear();
    options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/{1}/{0}.cshtml");
    options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/Shared/{0}.cshtml");
    options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
});

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "MyArea",
    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```

[]()

## Areas with Razor Pages

Areas with Razor Pages require an `Areas/<area name>/Pages` folder in the root of the app. The following folder structure is used with the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/3.1):

* Project name

  * Areas

    * Products

      * Pages

        * \_ViewImports
        * About
        * Index

    * Services

      * Pages

        * Manage

          * About
          * Index

### Link generation with Razor Pages and areas

The following code from the [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/6.0/RPareas) shows link generation with the area specified (for example, `asp-area="Products"`):

```
<li>Anchor Tag Helper links</li>
<ul>
    <li>
        <a asp-area="Products" asp-page="/About">
            Products/About
        </a>
    </li>
    <li>
        <a asp-area="Services" asp-page="/Manage/About">
            Services/Manage/About
        </a>
    </li>
    <li>
        <a asp-area="" asp-page="/About">
            /About
        </a>
    </li>
</ul>
<li>Url.Page generated links</li>
<ul>
    <li>
        <a href='@Url.Page("/Manage/About", new { area = "Services" })'>
            Services/Manage/About
        </a>
    </li>
    <li>
        <a href='@Url.Page("/About", new { area = "Products" })'>
            Products/About
        </a>
    </li>
</ul>
```

The sample download includes a [partial view](../views/partial?view=aspnetcore-10.0) that contains the preceding links and the same links without specifying the area. The partial view is referenced in the [layout file](../views/layout?view=aspnetcore-10.0), so every page in the app displays the generated links. The links generated without specifying the area are only valid when referenced from a page in the same area.

When the area is not specified, routing depends on the *ambient* values. The current route values of the current request are considered ambient values for link generation. In many cases for the sample app, using the ambient values generates incorrect links. For example, consider the links generated from the following code:

```
<li>
    <a asp-page="/Manage/About">
        Services/Manage/About
    </a>
</li>
<li>
    <a asp-page="/About">
        /About
    </a>
</li>
```

For the preceding code:

* The link generated from `<a asp-page="/Manage/About">` is correct only when the last request was for a page in `Services` area. For example, `/Services/Manage/`, `/Services/Manage/Index`, or `/Services/Manage/About`.
* The link generated from `<a asp-page="/About">` is correct only when the last request was for a page in `/Home`.
* The code is from the [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/6.0/RPareas).

### Import namespace and Tag Helpers with \_ViewImports file

A *\_ViewImports.cshtml* file can be added to each area *Pages* folder to import the namespace and Tag Helpers to each Razor Page in the folder.

Consider the *Services* area of the sample code, which doesn't contain a *\_ViewImports.cshtml* file. The following markup shows the */Services/Manage/About* Razor Page:

```
@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model RPareas.Areas.Services.Pages.Manage.AboutModel
@{
    ViewData["Title"] = "Srv Mng About";
}

<div>
  ViewData["routeInfo"]:  @ViewData["routeInfo"]
</div>

<a asp-area="Products" asp-page="/Index">
    Products/Index
</a>
```

In the preceding markup:

* The fully qualified class name must be used to specify the model (`@model RPareas.Areas.Services.Pages.Manage.AboutModel`).
* [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) are enabled by `@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers`

In the sample download, the Products area contains the following *\_ViewImports.cshtml* file:

```
@namespace RPareas.Areas.Products.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

The following markup shows the */Products/About* Razor Page:

```
@page
@model AboutModel
@{
    ViewData["Title"] = "Prod About";
}
```

In the preceding file, the namespace and `@addTagHelper` directive is imported to the file by the *Areas/Products/Pages/\_ViewImports.cshtml* file.

For more information, see [Managing Tag Helper scope](../views/tag-helpers/intro?view=aspnetcore-2.2#managing-tag-helper-scope) and [Importing Shared Directives](../views/layout?view=aspnetcore-10.0#importing-shared-directives).

### Shared layout for Razor Pages Areas

To share a common layout for the entire app, move the *\_ViewStart.cshtml* to the application root folder.

### Publishing Areas

All \*.cshtml files and files within the *wwwroot* directory are published to output when `<Project Sdk="Microsoft.NET.Sdk.Web">` is included in the \*.csproj file.

## Add MVC Area with Visual Studio

In Solution Explorer, right click the project and select **ADD > New Scaffolded Item**, then select **MVC Area**.

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/6.0) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)). The download sample provides a basic app for testing areas.
* [MyDisplayRouteInfo and ToCtxString](https://github.com/Rick-Anderson/RouteInfo/blob/master/Microsoft.Docs.Samples.RouteInfo/ControllerContextExtensions.cs) are provided by the [Rick.Docs.Samples.RouteInfo](https://www.nuget.org/packages/Rick.Docs.Samples.RouteInfo) NuGet package. The methods display `Controller` and `Razor Page` route information.

Areas are an ASP.NET feature used to organize related functionality into a group as a separate:

* Namespace for routing.
* Folder structure for views and Razor Pages.

Using areas creates a hierarchy for the purpose of routing by adding another route parameter, `area`, to `controller` and `action` or a Razor Page `page`.

Areas provide a way to partition an ASP.NET Core Web app into smaller functional groups, each with its own set of Razor Pages, controllers, views, and models. An area is effectively a structure inside an app. In an ASP.NET Core web project, logical components like Pages, Model, Controller, and View are kept in different folders. The ASP.NET Core runtime uses naming conventions to create the relationship between these components. For a large app, it may be advantageous to partition the app into separate high level areas of functionality. For instance, an e-commerce app with multiple business units, such as checkout, billing, and search. Each of these units have their own area to contain views, controllers, Razor Pages, and models.

Consider using Areas in a project when:

* The app is made of multiple high-level functional components that can be logically separated.
* You want to partition the app so that each functional area can be worked on independently.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/3.1) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)). The download sample provides a basic app for testing areas.

If you're using Razor Pages, see [Areas with Razor Pages](#areas-with-razor-pages) in this document.

## Areas for controllers with views

A typical ASP.NET Core web app using areas, controllers, and views contains the following:

* An [Area folder structure](#area-folder-structure).

* Controllers with the [`[Area]`](#attribute) attribute to associate the controller with the area:

  ```
  [Area("Products")]
  public class ManageController : Controller
  {
  ```

* The [area route added to startup](#add-area-route):

  ```
  app.UseEndpoints(endpoints =>
  {
      endpoints.MapControllerRoute(
          name: "MyArea",
          pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

      endpoints.MapControllerRoute(
          name: "default",
          pattern: "{controller=Home}/{action=Index}/{id?}");
  });
  ```

### Area folder structure

Consider an app that has two logical groups, *Products* and *Services*. Using areas, the folder structure would be similar to the following:

* Project name

  * Areas

    * Products

      * Controllers

        * HomeController.cs
        * ManageController.cs

      * Views

        * Home
          * Index.cshtml

        * Manage

          * Index.cshtml
          * About.cshtml

    * Services

      * Controllers
        * HomeController.cs
      * Views
        * Home
          * Index.cshtml

While the preceding layout is typical when using Areas, only the view files are required to use this folder structure. View discovery searches for a matching area view file in the following order:

```
/Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml
/Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml
/Views/Shared/<Action-Name>.cshtml
/Pages/Shared/<Action-Name>.cshtml
```

[]()

### Associate the controller with an Area

Area controllers are designated with the [\[Area\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.areaattribute) attribute:

```
using Microsoft.AspNetCore.Mvc;
using Microsoft.Docs.Samples;

namespace MVCareas.Areas.Products.Controllers
{
    [Area("Products")]
    public class ManageController : Controller
    {
        public IActionResult Index()
        {
            ViewData["routeInfo"] = ControllerContext.MyDisplayRouteInfo();
            return View();
        }

        public IActionResult About()
        {
            ViewData["routeInfo"] = ControllerContext.MyDisplayRouteInfo();
            return View();
        }
    }
}
```

### Add Area route

Area routes typically use [conventional routing](routing?view=aspnetcore-10.0#cr) rather than [attribute routing](routing?view=aspnetcore-10.0#ar). Conventional routing is order-dependent. In general, routes with areas should be placed earlier in the route table as they're more specific than routes without an area.

`{area:...}` can be used as a token in route templates if url space is uniform across all areas:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "MyArea",
            pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

In the preceding code, `exists` applies a constraint that the route must match an area. Using `{area:...}` with `MapControllerRoute`:

* Is the least complicated mechanism to adding routing to areas.
* Matches all controllers with the `[Area("Area name")]` attribute.

The following code uses [MapAreaControllerRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.controllerendpointroutebuilderextensions.mapareacontrollerroute) to create two named area routes:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapAreaControllerRoute(
            name: "MyAreaProducts",
            areaName: "Products",
            pattern: "Products/{controller=Home}/{action=Index}/{id?}");

        endpoints.MapAreaControllerRoute(
            name: "MyAreaServices",
            areaName: "Services",
            pattern: "Services/{controller=Home}/{action=Index}/{id?}");

        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

For more information, see [Area routing](routing?view=aspnetcore-10.0#areas).

### Link generation with MVC areas

The following code from the [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/31samples) shows link generation with the area specified:

```
<li>Anchor Tag Helper links</li>
<ul>
    <li>
        <a asp-area="Products" asp-controller="Home" asp-action="About">
            Products/Home/About
        </a>
    </li>
    <li>
        <a asp-area="Services" asp-controller="Home" asp-action="About">
            Services About
        </a>
    </li>
    <li>
        <a asp-area="" asp-controller="Home" asp-action="About">
            /Home/About
        </a>
    </li>
</ul>
<li>Html.ActionLink generated links</li>
<ul>
    <li>
        @Html.ActionLink("Product/Manage/About", "About", "Manage",
                                                new { area = "Products" })
    </li>
</ul>
<li>Url.Action generated links</li>
<ul>
    <li>
        <a href='@Url.Action("About", "Manage", new { area = "Products" })'>
            Products/Manage/About
        </a>
    </li>
</ul>
```

The sample download includes a [partial view](../views/partial?view=aspnetcore-10.0) that contains:

* The preceding links.
* Links similar to the preceding except `area` is not specified.

The partial view is referenced in the [layout file](../views/layout?view=aspnetcore-10.0), so every page in the app displays the generated links. The links generated without specifying the area are only valid when referenced from a page in the same area and controller.

When the area or controller is not specified, routing depends on the [ambient](routing?view=aspnetcore-10.0#ambient) values. The current route values of the current request are considered ambient values for link generation. In many cases for the sample app, using the ambient values generates incorrect links with the markup that doesn't specify the area.

For more information, see [Routing to controller actions](routing?view=aspnetcore-10.0).

### Shared layout for Areas using the \_ViewStart.cshtml file

To share a common layout for the entire app, keep the `_ViewStart.cshtml` in the [application root folder](#arf). For more information, see [Layout in ASP.NET Core](../views/layout?view=aspnetcore-10.0)

[]()

### Application root folder

The application root folder is the folder containing `Startup.cs` in web app created with the ASP.NET Core templates.

### \_ViewImports.cshtml

`/Views/_ViewImports.cshtml`, for MVC, and `/Pages/_ViewImports.cshtml` for Razor Pages, is not imported to views in areas. Use one of the following approaches to provide view imports to all views:

* Add `_ViewImports.cshtml` to the [application root folder](#arf). A `_ViewImports.cshtml` in the application root folder will apply to all views in the app.
* Copy the `_ViewImports.cshtml` file to the appropriate view folder under areas.

The `_ViewImports.cshtml` file typically contains [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) imports, `@using`, and `@inject` statements. For more information, see [Importing Shared Directives](../views/layout?view=aspnetcore-10.0#importing-shared-directives).

[]()

### Change default area folder where views are stored

The following code changes the default area folder from `"Areas"` to `"MyAreas"`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.AreaViewLocationFormats.Clear();
        options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/{1}/{0}.cshtml");
        options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/Shared/{0}.cshtml");
        options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
    });

    services.AddControllersWithViews();
}
```

[]()

## Areas with Razor Pages

Areas with Razor Pages require an `Areas/<area name>/Pages` folder in the root of the app. The following folder structure is used with the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/3.1):

* Project name

  * Areas

    * Products

      * Pages

        * \_ViewImports
        * About
        * Index

    * Services

      * Pages

        * Manage

          * About
          * Index

### Link generation with Razor Pages and areas

The following code from the [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/RPareas) shows link generation with the area specified (for example, `asp-area="Products"`):

```
<li>Anchor Tag Helper links</li>
<ul>
    <li>
        <a asp-area="Products" asp-page="/About">
            Products/About
        </a>
    </li>
    <li>
        <a asp-area="Services" asp-page="/Manage/About">
            Services/Manage/About
        </a>
    </li>
    <li>
        <a asp-area="" asp-page="/About">
            /About
        </a>
    </li>
</ul>
<li>Url.Page generated links</li>
<ul>
    <li>
        <a href='@Url.Page("/Manage/About", new { area = "Services" })'>
            Services/Manage/About
        </a>
    </li>
    <li>
        <a href='@Url.Page("/About", new { area = "Products" })'>
            Products/About
        </a>
    </li>
</ul>
```

The sample download includes a [partial view](../views/partial?view=aspnetcore-10.0) that contains the preceding links and the same links without specifying the area. The partial view is referenced in the [layout file](../views/layout?view=aspnetcore-10.0), so every page in the app displays the generated links. The links generated without specifying the area are only valid when referenced from a page in the same area.

When the area is not specified, routing depends on the *ambient* values. The current route values of the current request are considered ambient values for link generation. In many cases for the sample app, using the ambient values generates incorrect links. For example, consider the links generated from the following code:

```
<li>
    <a asp-page="/Manage/About">
        Services/Manage/About
    </a>
</li>
<li>
    <a asp-page="/About">
        /About
    </a>
</li>
```

For the preceding code:

* The link generated from `<a asp-page="/Manage/About">` is correct only when the last request was for a page in `Services` area. For example, `/Services/Manage/`, `/Services/Manage/Index`, or `/Services/Manage/About`.
* The link generated from `<a asp-page="/About">` is correct only when the last request was for a page in `/Home`.
* The code is from the [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/3.1/RPareas).

### Import namespace and Tag Helpers with \_ViewImports file

A `_ViewImports.cshtml` file can be added to each area *Pages* folder to import the namespace and Tag Helpers to each Razor Page in the folder.

Consider the *Services* area of the sample code, which doesn't contain a `_ViewImports.cshtml` file. The following markup shows the */Services/Manage/About* Razor Page:

```
@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model RPareas.Areas.Services.Pages.Manage.AboutModel
@{
    ViewData["Title"] = "Srv Mng About";
}

<a asp-area="Products" asp-page="/Index">
    Products/Index
</a>
```

In the preceding markup:

* The fully qualified class name must be used to specify the model (`@model RPareas.Areas.Services.Pages.Manage.AboutModel`).
* [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) are enabled by `@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers`

In the sample download, the Products area contains the following `_ViewImports.cshtml` file:

```
@namespace RPareas.Areas.Products.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

The following markup shows the */Products/About* Razor Page:

```
@page
@model AboutModel
@{
    ViewData["Title"] = "Prod About";
}
```

In the preceding file, the namespace and `@addTagHelper` directive is imported to the file by the `Areas/Products/Pages/_ViewImports.cshtml` file.

For more information, see [Managing Tag Helper scope](../views/tag-helpers/intro?view=aspnetcore-2.2#managing-tag-helper-scope) and [Importing Shared Directives](../views/layout?view=aspnetcore-10.0#importing-shared-directives).

### Shared layout for Razor Pages Areas

To share a common layout for the entire app, move the `_ViewStart.cshtml` to the application root folder.

### Publishing Areas

All \*.cshtml files and files within the *wwwroot* directory are published to output when `<Project Sdk="Microsoft.NET.Sdk.Web">` is included in the \*.csproj file.

## Add MVC Area with Visual Studio

In Solution Explorer, right click the project and select **ADD > New Scaffolded Item**, then select **MVC Area**.

Areas are an ASP.NET feature used to organize related functionality into a group as a separate namespace (for routing) and folder structure (for views). Using areas creates a hierarchy for the purpose of routing by adding another route parameter, `area`, to `controller` and `action` or a Razor Page `page`.

Areas provide a way to partition an ASP.NET Core Web app into smaller functional groups, each with its own set of Razor Pages, controllers, views, and models. An area is effectively a structure inside an app. In an ASP.NET Core web project, logical components like Pages, Model, Controller, and View are kept in different folders. The ASP.NET Core runtime uses naming conventions to create the relationship between these components. For a large app, it may be advantageous to partition the app into separate high level areas of functionality. For instance, an e-commerce app with multiple business units, such as checkout, billing, and search. Each of these units have their own area to contain views, controllers, Razor Pages, and models.

Consider using Areas in a project when:

* The app is made of multiple high-level functional components that can be logically separated.
* You want to partition the app so that each functional area can be worked on independently.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)). The download sample provides a basic app for testing areas.

If you're using Razor Pages, see [Areas with Razor Pages](#areas-with-razor-pages) in this document.

## Areas for controllers with views

A typical ASP.NET Core web app using areas, controllers, and views contains the following:

* An [Area folder structure](#area-folder-structure).

* Controllers with the [`[Area]`](#attribute) attribute to associate the controller with the area:

  ```
  [Area("Products")]
  public class ManageController : Controller
  {
  ```

* The [area route added to startup](#add-area-route):

  ```
  app.UseMvc(routes =>
  {
      routes.MapRoute(
        name: "MyArea",
        template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

      routes.MapRoute(
         name: "default",
         template: "{controller=Home}/{action=Index}/{id?}");
  });
  ```

### Area folder structure

Consider an app that has two logical groups, *Products* and *Services*. Using areas, the folder structure would be similar to the following:

* Project name

  * Areas

    * Products

      * Controllers

        * HomeController.cs
        * ManageController.cs

      * Views

        * Home
          * Index.cshtml

        * Manage

          * Index.cshtml
          * About.cshtml

    * Services

      * Controllers
        * HomeController.cs
      * Views
        * Home
          * Index.cshtml

While the preceding layout is typical when using Areas, only the view files are required to use this folder structure. View discovery searches for a matching area view file in the following order:

```
/Areas/<Area-Name>/Views/<Controller-Name>/<Action-Name>.cshtml
/Areas/<Area-Name>/Views/Shared/<Action-Name>.cshtml
/Views/Shared/<Action-Name>.cshtml
/Pages/Shared/<Action-Name>.cshtml
```

[]()

### Associate the controller with an Area

Area controllers are designated with the [\[Area\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.areaattribute) attribute:

```
using Microsoft.AspNetCore.Mvc;

namespace MVCareas.Areas.Products.Controllers
{
    [Area("Products")]
    public class ManageController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult About()
        {
            return View();
        }
    }
}
```

### Add Area route

Area routes typically use conventional routing rather than attribute routing. Conventional routing is order-dependent. In general, routes with areas should be placed earlier in the route table as they're more specific than routes without an area.

`{area:...}` can be used as a token in route templates if url space is uniform across all areas:

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
          name: "MyArea",
          template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

        routes.MapRoute(
           name: "default",
           template: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

In the preceding code, `exists` applies a constraint that the route must match an area. Using `{area:...}` is the least complicated mechanism to adding routing to areas.

The following code uses [MapAreaRoute](/en-us/dotnet/api/microsoft.aspnetcore.builder.mvcarearoutebuilderextensions.maparearoute) to create two named area routes:

```
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapAreaRoute(
            name: "MyAreaProducts",
            areaName:"Products",
            template: "Products/{controller=Home}/{action=Index}/{id?}");

        routes.MapAreaRoute(
            name: "MyAreaServices",
            areaName: "Services",
            template: "Services/{controller=Home}/{action=Index}/{id?}");

        routes.MapRoute(
           name: "default",
           template: "{controller=Home}/{action=Index}/{id?}");
    });
}
```

When using `MapAreaRoute` with ASP.NET Core 2.2, see [this GitHub issue](https://github.com/dotnet/AspNetCore/issues/7772).

For more information, see [Area routing](routing?view=aspnetcore-10.0#areas).

### Link generation with MVC areas

The following code from the [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples) shows link generation with the area specified:

```
<li>Anchor Tag Helper links</li>
<ul>
    <li>
        <a asp-area="Products" asp-controller="Home" asp-action="About">
            Products/Home/About
        </a>
    </li>
    <li>
        <a asp-area="Services" asp-controller="Home" asp-action="About">
            Services About
        </a>
    </li>
    <li>
        <a asp-area="" asp-controller="Home" asp-action="About">
            /Home/About
        </a>
    </li>
</ul>
<li>Html.ActionLink generated links</li>
<ul>
    <li>
        @Html.ActionLink("Product/Manage/About", "About", "Manage", 
                                                new { area = "Products" })
    </li>
</ul>
<li>Url.Action generated links</li>
<ul>
    <li>
        <a href='@Url.Action("About", "Manage", new { area = "Products" })'>
            Products/Manage/About
        </a>
    </li>
</ul>
```

The links generated with the preceding code are valid anywhere in the app.

The sample download includes a [partial view](../views/partial?view=aspnetcore-10.0) that contains the preceding links and the same links without specifying the area. The partial view is referenced in the [layout file](../views/layout?view=aspnetcore-10.0), so every page in the app displays the generated links. The links generated without specifying the area are only valid when referenced from a page in the same area and controller.

When the area or controller is not specified, routing depends on the *ambient* values. The current route values of the current request are considered ambient values for link generation. In many cases for the sample app, using the ambient values generates incorrect links.

For more information, see [Routing to controller actions](routing?view=aspnetcore-10.0).

### Shared layout for Areas using the \_ViewStart.cshtml file

To share a common layout for the entire app, move the `_ViewStart.cshtml` to the application root folder.

### \_ViewImports.cshtml

In its standard location, `/Views/_ViewImports.cshtml` doesn't apply to areas. To use common [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0), `@using`, or `@inject` in your area, ensure a proper `_ViewImports.cshtml` file [applies to your area views](../views/layout?view=aspnetcore-10.0#importing-shared-directives). If you want the same behavior in all your views, move `/Views/_ViewImports.cshtml` to the application root.

[]()

### Change default area folder where views are stored

The following code changes the default area folder from `"Areas"` to `"MyAreas"`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.AreaViewLocationFormats.Clear();
        options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/{1}/{0}.cshtml");
        options.AreaViewLocationFormats.Add("/MyAreas/{2}/Views/Shared/{0}.cshtml");
        options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
    });

    services.AddMvc();
}
```

[]()

## Areas with Razor Pages

Areas with Razor Pages require an `Areas/<area name>/Pages` folder in the root of the app. The following folder structure is used with the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples):

* Project name

  * Areas

    * Products

      * Pages

        * \_ViewImports
        * About
        * Index

    * Services

      * Pages

        * Manage

          * About
          * Index

### Link generation with Razor Pages and areas

The following code from the [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/RPareas) shows link generation with the area specified (for example, `asp-area="Products"`):

```
<li>Anchor Tag Helper links</li>
<ul>
    <li>
        <a asp-area="Products" asp-page="/About">
            Products/About
        </a>
    </li>
    <li>
        <a asp-area="Services" asp-page="/Manage/About">
            Services/Manage/About
        </a>
    </li>
    <li>
        <a asp-area="" asp-page="/About">
            /About
        </a>
    </li>
</ul>
<li>Url.Page generated links</li>
<ul>
    <li>
        <a href='@Url.Page("/Manage/About", new { area = "Services" })'>
            Services/Manage/About
        </a>
    </li>
    <li>
        <a href='@Url.Page("/About", new { area = "Products" })'>
            Products/About
        </a>
    </li>
</ul>
```

The links generated with the preceding code are valid anywhere in the app.

The sample download includes a [partial view](../views/partial?view=aspnetcore-10.0) that contains the preceding links and the same links without specifying the area. The partial view is referenced in the [layout file](../views/layout?view=aspnetcore-10.0), so every page in the app displays the generated links. The links generated without specifying the area are only valid when referenced from a page in the same area.

When the area is not specified, routing depends on the *ambient* values. The current route values of the current request are considered ambient values for link generation. In many cases for the sample app, using the ambient values generates incorrect links. For example, consider the links generated from the following code:

```
<li>
    <a asp-page="/Manage/About">
        Services/Manage/About
    </a>
</li>
<li>
    <a asp-page="/About">
        /About
    </a>
</li>
```

For the preceding code:

* The link generated from `<a asp-page="/Manage/About">` is correct only when the last request was for a page in `Services` area. For example, `/Services/Manage/`, `/Services/Manage/Index`, or `/Services/Manage/About`.
* The link generated from `<a asp-page="/About">` is correct only when the last request was for a page in `/Home`.
* The code is from the [sample download](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/areas/samples/RPareas).

### Import namespace and Tag Helpers with \_ViewImports file

A `_ViewImports.cshtml` file can be added to each area *Pages* folder to import the namespace and Tag Helpers to each Razor Page in the folder.

Consider the *Services* area of the sample code, which doesn't contain a `_ViewImports.cshtml` file. The following markup shows the */Services/Manage/About* Razor Page:

```
@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model RPareas.Areas.Services.Pages.Manage.AboutModel
@{
    ViewData["Title"] = "Srv Mng About";
}

<h2>/Services/Manage/About</h2>

<a asp-area="Products" asp-page="/Index">
    Products/Index
</a>
```

In the preceding markup:

* The fully qualified domain name must be used to specify the model (`@model RPareas.Areas.Services.Pages.Manage.AboutModel`).
* [Tag Helpers](../views/tag-helpers/intro?view=aspnetcore-10.0) are enabled by `@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers`

In the sample download, the Products area contains the following `_ViewImports.cshtml` file:

```
@namespace RPareas.Areas.Products.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
```

The following markup shows the */Products/About* Razor Page:

```
@page
@model AboutModel
@{
    ViewData["Title"] = "Prod About";
}

<h2>Products/About</h2>

<a asp-area="Services" asp-page="/Manage/About">
    Services/Manage/About
</a>
```

In the preceding file, the namespace and `@addTagHelper` directive is imported to the file by the `Areas/Products/Pages/_ViewImports.cshtml` file.

For more information, see [Managing Tag Helper scope](../views/tag-helpers/intro?view=aspnetcore-2.2#managing-tag-helper-scope) and [Importing Shared Directives](../views/layout?view=aspnetcore-10.0#importing-shared-directives).

### Shared layout for Razor Pages Areas

To share a common layout for the entire app, move the `_ViewStart.cshtml` to the application root folder.

### Publishing Areas

All \*.cshtml files and files within the *wwwroot* directory are published to output when `<Project Sdk="Microsoft.NET.Sdk.Web">` is included in the \*.csproj file.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/webassembly-lazy-load-assemblies
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/webassembly-lazy-load-assemblies.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Lazy load assemblies in ASP.NET Core Blazor WebAssembly

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Blazor WebAssembly app startup performance can be improved by waiting to load developer-created app assemblies until the assemblies are required, which is called *lazy loading*.

This article's initial sections cover the app configuration. For a working demonstration, see the [Complete example](#complete-example) section at the end of this article.

*This article only applies to Blazor WebAssembly apps.* Assembly lazy loading doesn't benefit server-side apps because server-rendered apps don't download assemblies to the client.

Lazy loading shouldn't be used for core runtime assemblies, which might be trimmed on publish and unavailable on the client when the app loads.

## File extension placeholder (`{FILE EXTENSION}`) for assembly files

Assembly files use the [Webcil packaging format for .NET assemblies](host-and-deploy/webassembly/?view=aspnetcore-10.0#webcil-packaging-format-for-net-assemblies) with a `.wasm` file extension.

Throughout the article, the `{FILE EXTENSION}` placeholder represents "`wasm`".

Assembly files are based on Dynamic-Link Libraries (DLLs) with a `.dll` file extension.

Throughout the article, the `{FILE EXTENSION}` placeholder represents "`dll`".

## Project file configuration

Mark assemblies for lazy loading in the app's project file (`.csproj`) using the `BlazorWebAssemblyLazyLoad` item. Use the assembly name with file extension. The Blazor framework prevents the assembly from loading at app launch.

```
<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="{ASSEMBLY NAME}.{FILE EXTENSION}" />
</ItemGroup>
```

The `{ASSEMBLY NAME}` placeholder is the name of the assembly, and the `{FILE EXTENSION}` placeholder is the file extension. The file extension is required.

Include one `BlazorWebAssemblyLazyLoad` item for each assembly. If an assembly has dependencies, include a `BlazorWebAssemblyLazyLoad` entry for each dependency.

## `Router` component configuration

The Blazor framework automatically registers a singleton service for lazy loading assemblies in client-side Blazor WebAssembly apps, [LazyAssemblyLoader](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.services.lazyassemblyloader). The [LazyAssemblyLoader.LoadAssembliesAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.services.lazyassemblyloader.loadassembliesasync) method:

* Uses [JS interop](javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-10.0) to fetch assemblies via a network call.
* Loads assemblies into the runtime executing on WebAssembly in the browser.

Note

Guidance for *hosted* Blazor WebAssembly [solutions](tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln) is covered in the [Lazy load assemblies in a hosted Blazor WebAssembly solution](#lazy-load-assemblies-in-a-hosted-blazor-webassembly-solution) section.

Blazor's [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component designates the assemblies that Blazor searches for routable components and is also responsible for rendering the component for the route where the user navigates. The [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component's [`OnNavigateAsync` method](fundamentals/routing?view=aspnetcore-10.0#handle-asynchronous-navigation-events-with-onnavigateasync) is used in conjunction with lazy loading to load the correct assemblies for endpoints that a user requests.

Logic is implemented inside [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) to determine the assemblies to load with [LazyAssemblyLoader](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.services.lazyassemblyloader). Options for how to structure the logic include:

* Conditional checks inside the [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) method.
* A lookup table that maps routes to assembly names, either injected into the component or implemented within the component's code.

In the following example:

* The namespace for [Microsoft.AspNetCore.Components.WebAssembly.Services](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.services) is specified.
* The [LazyAssemblyLoader](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.services.lazyassemblyloader) service is injected (`AssemblyLoader`).
* The `{PATH}` placeholder is the path where the list of assemblies should load. The example uses a conditional check for a single path that loads a single set of assemblies.
* The `{LIST OF ASSEMBLIES}` placeholder is the comma-separated list of assembly file name strings, including their file extensions (for example, `"Assembly1.{FILE EXTENSION}", "Assembly2.{FILE EXTENSION}"`).

`App.razor`:

```
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="typeof(App).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       [ {LIST OF ASSEMBLIES} ]);
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}
```

```
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="typeof(App).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}
```

```
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="typeof(Program).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}
```

Note

The preceding example doesn't show the contents of the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component's Razor markup (`...`). For a demonstration with complete code, see the [Complete example](#complete-example) section of this article.

Note

With the release of .NET 5.0.1 and for any additional 5.x releases, the `Router` component includes the `PreferExactMatches` parameter set to `@true`. For more information, see [Migrate from ASP.NET Core 3.1 to .NET 5](../migration/31-to-50?view=aspnetcore-10.0#changes-to-blazor-app-routing-logic-in-501-and-further-5x-releases-up-to-60).

## Assemblies that include routable components

When the list of assemblies includes routable components, the assembly list for a given path is passed to the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component's [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies#microsoft-aspnetcore-components-routing-router-additionalassemblies) collection.

In the following example:

* The [List](/en-us/dotnet/api/system.collections.generic.list-1)<[Assembly](/en-us/dotnet/api/system.reflection.assembly)> in `lazyLoadedAssemblies` passes the assembly list to [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies#microsoft-aspnetcore-components-routing-router-additionalassemblies). The framework searches the assemblies for routes and updates the route collection if new routes are found. To access the [Assembly](/en-us/dotnet/api/system.reflection.assembly) type, the namespace for [System.Reflection](/en-us/dotnet/api/system.reflection) is included at the top of the `App.razor` file.
* The `{PATH}` placeholder is the path where the list of assemblies should load. The example uses a conditional check for a single path that loads a single set of assemblies.
* The `{LIST OF ASSEMBLIES}` placeholder is the comma-separated list of assembly file name strings, including their file extensions (for example, `"Assembly1.{FILE EXTENSION}", "Assembly2.{FILE EXTENSION}"`).

`App.razor`:

```
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(App).Assembly" 
    AdditionalAssemblies="lazyLoadedAssemblies" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = [];

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if (args.Path == "{PATH}")
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    [ {LIST OF ASSEMBLIES} ]);
                lazyLoadedAssemblies.AddRange(assemblies);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}
```

```
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(App).Assembly" 
    AdditionalAssemblies="lazyLoadedAssemblies" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if (args.Path == "{PATH}")
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { {LIST OF ASSEMBLIES} });
                lazyLoadedAssemblies.AddRange(assemblies);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}
```

```
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(Program).Assembly" 
    AdditionalAssemblies="lazyLoadedAssemblies" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
                   lazyLoadedAssemblies.AddRange(assemblies);
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}
```

Note

The preceding example doesn't show the contents of the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component's Razor markup (`...`). For a demonstration with complete code, see the [Complete example](#complete-example) section of this article.

Note

With the release of .NET 5.0.1 and for any additional 5.x releases, the `Router` component includes the `PreferExactMatches` parameter set to `@true`. For more information, see [Migrate from ASP.NET Core 3.1 to .NET 5](../migration/31-to-50?view=aspnetcore-10.0#changes-to-blazor-app-routing-logic-in-501-and-further-5x-releases-up-to-60).

For more information, see [ASP.NET Core Blazor routing](fundamentals/routing?view=aspnetcore-10.0#route-to-components-from-multiple-assemblies).

## User interaction with `<Navigating>` content

While loading assemblies, which can take several seconds, the [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component can indicate to the user that a page transition is occurring with the router's [Navigating](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.navigating#microsoft-aspnetcore-components-routing-router-navigating) property.

For more information, see [ASP.NET Core Blazor routing](fundamentals/routing?view=aspnetcore-10.0#user-interaction-with-navigating-content).

## Handle cancellations in `OnNavigateAsync`

The [NavigationContext](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationcontext) object passed to the [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) callback contains a [CancellationToken](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.navigationcontext.cancellationtoken#microsoft-aspnetcore-components-routing-navigationcontext-cancellationtoken) that's set when a new navigation event occurs. The [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) callback must throw when the cancellation token is set to avoid continuing to run the [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) callback on an outdated navigation.

For more information, see [ASP.NET Core Blazor routing](fundamentals/routing?view=aspnetcore-10.0#handle-cancellations-in-onnavigateasync).

## `OnNavigateAsync` events and renamed assembly files

The resource loader relies on the assembly names that are defined in the boot manifest file. If [assemblies are renamed](host-and-deploy/webassembly/?view=aspnetcore-10.0#change-the-file-name-extension-of-dll-files), the assembly names used in an [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) callback and the assembly names in the boot manifest file are out of sync.

To rectify this:

* Check to see if the app is running in the `Production` environment when determining which assembly names to use.
* Store the renamed assembly names in a separate file and read from that file to determine what assembly name to use with the [LazyAssemblyLoader](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.services.lazyassemblyloader) service and [OnNavigateAsync](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.onnavigateasync#microsoft-aspnetcore-components-routing-router-onnavigateasync) callback.

## Lazy load assemblies in a hosted Blazor WebAssembly solution

The framework's lazy loading implementation supports lazy loading with prerendering in a hosted Blazor WebAssembly [solution](tooling?view=aspnetcore-10.0#visual-studio-solution-file-sln). During prerendering, all assemblies, including those marked for lazy loading, are assumed to be loaded. Manually register the [LazyAssemblyLoader](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.services.lazyassemblyloader) service in the **Server** project.

At the top of the `Program.cs` file of the **Server** project, add the namespace for [Microsoft.AspNetCore.Components.WebAssembly.Services](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.services):

```
using Microsoft.AspNetCore.Components.WebAssembly.Services;
```

In `Program.cs` of the **Server** project, register the service:

```
builder.Services.AddScoped<LazyAssemblyLoader>();
```

At the top of the `Startup.cs` file of the **Server** project, add the namespace for [Microsoft.AspNetCore.Components.WebAssembly.Services](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.services):

```
using Microsoft.AspNetCore.Components.WebAssembly.Services;
```

In `Startup.ConfigureServices` (`Startup.cs`) of the **Server** project, register the service:

```
services.AddScoped<LazyAssemblyLoader>();
```

## Complete example

The demonstration in this section:

* Creates a robot controls assembly (`GrantImaharaRobotControls.{FILE EXTENSION}`) as a [Razor class library (RCL)](components/class-libraries?view=aspnetcore-10.0) that includes a `Robot` component (`Robot.razor` with a route template of `/robot`).
* Lazily loads the RCL's assembly to render its `Robot` component when the `/robot` URL is requested by the user.

Create a standalone Blazor WebAssembly app to demonstrate lazy loading of a Razor class library's assembly. Name the project `LazyLoadTest`.

Add an ASP.NET Core class library project to the solution:

* Visual Studio: Right-click the solution file in **Solution Explorer** and select **Add** > **New project**. From the dialog of new project types, select **Razor Class Library**. Name the project `GrantImaharaRobotControls`. Do **not** select the **Support pages and views** checkbox.
* Visual Studio Code/.NET CLI: Execute `dotnet new razorclasslib -o GrantImaharaRobotControls` from a command prompt. The `-o|--output` option creates a folder and names the project `GrantImaharaRobotControls`.

Create a `HandGesture` class in the RCL with a `ThumbUp` method that hypothetically makes a robot perform a thumbs-up gesture. The method accepts an argument for the axis, `Left` or `Right`, as an [`enum`](/en-us/dotnet/csharp/language-reference/builtin-types/enum). The method returns `true` on success.

`HandGesture.cs`:

```
using Microsoft.Extensions.Logging;

namespace GrantImaharaRobotControls;

public static class HandGesture
{
    public static bool ThumbUp(Axis axis, ILogger logger)
    {
        logger.LogInformation("Thumb up gesture. Axis: {Axis}", axis);

        // Code to make robot perform gesture

        return true;
    }
}

public enum Axis { Left, Right }
```

```
using Microsoft.Extensions.Logging;

namespace GrantImaharaRobotControls
{
    public static class HandGesture
    {
        public static bool ThumbUp(Axis axis, ILogger logger)
        {
            logger.LogInformation("Thumb up gesture. Axis: {Axis}", axis);

            // Code to make robot perform gesture

            return true;
        }
    }

    public enum Axis { Left, Right }
}
```

Add the following component to the root of the RCL project. The component permits the user to submit a left or right hand thumb-up gesture request.

`Robot.razor`:

```
@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm FormName="RobotForm" Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in Enum.GetValues<Axis>())
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

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

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new() { AxisSelection = Axis.Left };
    private string? message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}
```

```
@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in Enum.GetValues<Axis>())
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

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

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new() { AxisSelection = Axis.Left };
    private string? message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}
```

```
@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in (Axis[])Enum
            .GetValues(typeof(Axis)))
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

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

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new RobotModel() { AxisSelection = Axis.Left };
    private string message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}
```

In the `LazyLoadTest` project, create a project reference for the `GrantImaharaRobotControls` RCL:

* Visual Studio: Right-click the `LazyLoadTest` project and select **Add** > **Project Reference** to add a project reference for the `GrantImaharaRobotControls` RCL.
* Visual Studio Code/.NET CLI: Execute `dotnet add reference {PATH}` in a command shell from the project's folder. The `{PATH}` placeholder is the path to the RCL project.

Specify the RCL's assembly for lazy loading in the `LazyLoadTest` app's project file (`.csproj`):

```
<ItemGroup>
    <BlazorWebAssemblyLazyLoad Include="GrantImaharaRobotControls.{FILE EXTENSION}" />
</ItemGroup>
```

The following [Router](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router) component demonstrates loading the `GrantImaharaRobotControls.{FILE EXTENSION}` assembly when the user navigates to `/robot`. Replace the app's default `App` component with the following `App` component.

During page transitions, a styled message is displayed to the user with the `<Navigating>` element. For more information, see the [User interaction with `<Navigating>` content](#user-interaction-with-navigating-content) section.

The assembly is assigned to [AdditionalAssemblies](/en-us/dotnet/api/microsoft.aspnetcore.components.routing.router.additionalassemblies#microsoft-aspnetcore-components-routing-router-additionalassemblies), which results in the router searching the assembly for routable components, where it finds the `Robot` component. The `Robot` component's route is added to the app's route collection. For more information, see the [ASP.NET Core Blazor routing](fundamentals/routing?view=aspnetcore-10.0#route-to-components-from-multiple-assemblies) article and the [Assemblies that include routable components](#assemblies-that-include-routable-components) section of this article.

`App.razor`:

```
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(App).Assembly"
        AdditionalAssemblies="lazyLoadedAssemblies" 
        OnNavigateAsync="OnNavigateAsync">
    <Navigating>
        <div style="padding:20px;background-color:blue;color:white">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = [];
    private bool grantImaharaRobotControlsAssemblyLoaded;

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if ((args.Path == "robot") && !grantImaharaRobotControlsAssemblyLoaded)
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    [ "GrantImaharaRobotControls.{FILE EXTENSION}" ]);
                lazyLoadedAssemblies.AddRange(assemblies);
                grantImaharaRobotControlsAssemblyLoaded = true;
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}
```

```
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(App).Assembly"
        AdditionalAssemblies="lazyLoadedAssemblies" 
        OnNavigateAsync="OnNavigateAsync">
    <Navigating>
        <div style="padding:20px;background-color:blue;color:white">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new();
    private bool grantImaharaRobotControlsAssemblyLoaded;

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if ((args.Path == "robot") && !grantImaharaRobotControlsAssemblyLoaded)
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" });
                lazyLoadedAssemblies.AddRange(assemblies);
                grantImaharaRobotControlsAssemblyLoaded = true;
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}
```

```
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(Program).Assembly"
        AdditionalAssemblies="lazyLoadedAssemblies" 
        OnNavigateAsync="OnNavigateAsync">
    <Navigating>
        <div style="padding:20px;background-color:blue;color:white">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();
    private bool grantImaharaRobotControlsAssemblyLoaded;

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if ((args.Path == "robot") && !grantImaharaRobotControlsAssemblyLoaded)
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" });
                lazyLoadedAssemblies.AddRange(assemblies);
                grantImaharaRobotControlsAssemblyLoaded = true;
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}
```

Build and run the app.

When the `Robot` component from the RCL is requested at `/robot`, the `GrantImaharaRobotControls.{FILE EXTENSION}` assembly is loaded and the `Robot` component is rendered. You can inspect the assembly loading in the **Network** tab of the browser's developer tools.

## Troubleshoot

* If unexpected rendering occurs, such as rendering a component from a previous navigation, confirm that the code throws if the cancellation token is set.
* If assemblies configured for lazy loading unexpectedly load at app start, check that the assembly is marked for lazy loading in the project file.

Note

A known issue exists for loading types from a lazily-loaded assembly. For more information, see [Blazor WebAssembly lazy loading assemblies not working when using @ref attribute in the component (dotnet/aspnetcore #29342)](https://github.com/dotnet/aspnetcore/issues/29342).

## Related content

* [Handle asynchronous navigation events with `OnNavigateAsync`](fundamentals/routing?view=aspnetcore-10.0#handle-asynchronous-navigation-events-with-onnavigateasync)
* [ASP.NET Core Blazor performance best practices](performance/?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/11/response-compression-always-vary
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/breaking-changes/11/response-compression-always-vary.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Response compression always emits Vary: Accept-Encoding

The response compression middleware now appends `Vary: Accept-Encoding` to every response that passes through it, including responses for which it didn't actually compress the body. Downstream caches see more correct cache keys, at the cost of slightly more cache variants when caches don't normalize the `Accept-Encoding` request header.

## Version introduced

.NET 11

## Previous behavior

Previously, the response compression middleware only appended `Vary: Accept-Encoding` to responses it actually compressed. Responses for which compression was skipped—for example, because the content type wasn't in `MimeTypes`, or because the client preferred `identity` encoding—were sent without the `Vary` header added by the middleware.

A downstream cache could therefore serve a compressed response that was stored for `Accept-Encoding: gzip` to a client that only accepted `identity` (and vice versa), unless the cache or the origin already added the header by another means.

## New behavior

Starting in ASP.NET Core 11, the response compression middleware appends `Vary: Accept-Encoding` to the response header collection for every response it sees, whether or not it compressed the body. If the header already contains `Accept-Encoding`, the middleware doesn't add a duplicate token.

## Type of breaking change

This change is a [behavioral change](/en-us/dotnet/core/compatibility/categories#behavioral-change).

## Reason for change

Without the `Vary: Accept-Encoding` header on every response, a shared cache can serve a stored encoded response to a client that doesn't accept that encoding. Adding the header unconditionally makes the middleware's response correctly cacheable by HTTP-compliant intermediaries. For more information, see [dotnet/aspnetcore#55092](https://github.com/dotnet/aspnetcore/pull/55092) and the issue it fixes, [dotnet/aspnetcore#48008](https://github.com/dotnet/aspnetcore/issues/48008).

## Recommended action

No source change is required. Verify that any downstream cache (CDN, reverse proxy, browser cache) normalizes the `Accept-Encoding` request header so it doesn't store an unnecessarily large number of variants. Most modern CDNs already do this.

If you don't want the middleware to add the header to a specific response, remove it after the middleware runs:

```
app.UseResponseCompression();

app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        // Remove the Vary header added by UseResponseCompression for a specific path.
        if (context.Request.Path.StartsWithSegments("/no-vary"))
        {
            context.Response.Headers.Remove("Vary");
        }
        return Task.CompletedTask;
    });
    await next();
});
```

## Affected APIs

* [Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware](/en-us/dotnet/api/microsoft.aspnetcore.responsecompression.responsecompressionmiddleware)
* [Microsoft.AspNetCore.Builder.ResponseCompressionBuilderExtensions.UseResponseCompression](/en-us/dotnet/api/microsoft.aspnetcore.builder.responsecompressionbuilderextensions.useresponsecompression)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-06-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0015
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0015.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0015: Suggest using IHeaderDictionary properties

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0015      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

[IHeaderDictionary](/en-us/dotnet/api/system.collections.idictionary) properties are the recommended strategy for accessing headers.

## Rule description

`IHeaderDictionary` properties are recommended for accessing headers. Accessing headers using an indexer as in the example below is not recommended.

```
var app = WebApplication.Create();

app.MapGet("/", (HttpContext context) => context.Request.Headers[""content-type""]);

app.Run();
```

## How to fix violations

To fix a violation of this rule, use the property specified in the analyzer message to access the header specified in the message or apply the associated codefix.

```
var app = WebApplication.Create();
app.MapGet("/", (HttpContext context) => context.Request.Headers.ContentType);
app.Run();
```

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/certauth.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure certificate authentication in ASP.NET Core

`Microsoft.AspNetCore.Authentication.Certificate` contains an implementation similar to [Certificate Authentication](https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.4) for ASP.NET Core. Certificate authentication happens at the TLS level before it ever gets to ASP.NET Core. More accurately, this functionality is an authentication handler that validates the certificate and then gives you an event where you can resolve that certificate to a `ClaimsPrincipal`.

You **must** [configure your server](#configure-your-server-to-require-certificates) for certificate authentication with IIS, Kestrel, Azure Web Apps, or your preferred solution.

This article describes how to configure certificate authentication in ASP.NET Core for IIS and HTTP.sys, and provides examples for calling various methods and working with properties.

## Review proxy and load balancer scenarios

Certificate authentication is a stateful scenario primarily used where a proxy or load balancer doesn't handle traffic between clients and servers. If a proxy or load balancer is used, certificate authentication only works if the proxy or load balancer:

* Handles the authentication.
* Passes the user authentication information to the app (for example, in a request header), which acts on the authentication information.

An alternative to certificate authentication in environments where proxies and load balancers are used is Active Directory Federated Services (ADFS) with OpenID Connect (OIDC).

## Get started

Acquire an HTTPS certificate, apply it, and [configure your server](#configure-your-server-to-require-certificates) to require certificates.

In the web app:

* Add a reference to the [Microsoft.AspNetCore.Authentication.Certificate](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Certificate) NuGet package.

* In the *Program.cs* file, call the `builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);` method. Provide a delegate for the `OnCertificateValidated` event handler to complete any supplementary validation on the client certificate sent with requests. Turn that information into a `ClaimsPrincipal` value and set it on the `context.Principal` property.

If authentication fails, this handler returns a `403 (Forbidden)` response rather than a `401 (Unauthorized)`, as you might expect. The handler returns a different response because it expects authentication to occur during the initial TLS connection. By the time it reaches the handler, it's too late. There's no way to upgrade the connection from an anonymous connection to one with a certificate.

The `UseAuthentication` method is required to set `HttpContext.User` to a `ClaimsPrincipal` value created from the certificate. For example:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate();

var app = builder.Build();

app.UseAuthentication();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding example demonstrates the default way to add certificate authentication. The handler constructs a user principal by using the common certificate properties.

## Configure certificate validation

The `CertificateAuthenticationOptions` handler has some built-in validations that are the minimum validations you should perform on a certificate. Each of these settings is enabled by default. The following sections describe how to work with the settings.

### AllowedCertificateTypes = Chained, SelfSigned, or All (Chained | SelfSigned)

Default value: `CertificateTypes.Chained`

This check validates that only the appropriate certificate type is allowed. If the app is using self-signed certificates, this option needs to be set to `CertificateTypes.All` or `CertificateTypes.SelfSigned`.

### ChainTrustValidationMode

Default value: [X509ChainTrustMode.System](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509chaintrustmode#system-security-cryptography-x509certificates-x509chaintrustmode-system)

The certificate presented by the client must chain to a trusted root certificate. This check controls which trust store contains these root certificates.

By default, the handler uses the system trust store. If the presented client certificate needs to chain to a root certificate that doesn't appear in the system trust store, you can set the option to [X509ChainTrustMode.CustomRootTrust](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509chaintrustmode#system-security-cryptography-x509certificates-x509chaintrustmode-customroottrust) so the handler uses the `CustomTrustStore` property.

### CustomTrustStore

Default value: Empty [X509Certificate2Collection](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2collection)

If the handler's [ChainTrustValidationMode](/en-us/dotnet/api/microsoft.aspnetcore.authentication.certificate.certificateauthenticationoptions.chaintrustvalidationmode#microsoft-aspnetcore-authentication-certificate-certificateauthenticationoptions-chaintrustvalidationmode) property is set to `X509ChainTrustMode.CustomRootTrust`, this [X509Certificate2Collection](/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2collection) object contains every certificate used to validate the client certificate up to a trusted root, including the trusted root.

When the client presents a certificate that's part of a multi-level certificate chain, the `CustomTrustStore` property must contain every issuing certificate in the chain.

### ValidateCertificateUse

Default value: `true`

This check validates that the certificate presented by the client has the Client Authentication extended key use (EKU), or no EKUs at all. As the specifications say, if no EKU is specified, then all EKUs are deemed valid.

### ValidateValidityPeriod

Default value: `true`

This check validates that the certificate is within its validity period. On each request, the handler ensures that a certificate that was valid when it was presented hasn't expired during its current session.

### RevocationFlag

Default value: `X509RevocationFlag.ExcludeRoot`

A flag that specifies which certificates in the chain are checked for revocation.

Revocation checks are only performed when the certificate is chained to a root certificate.

### RevocationMode

Default value: `X509RevocationMode.Online`

A flag that specifies how revocation checks are performed.

Specifying an online check can result in a long delay while the certificate authority is contacted.

Revocation checks are performed only when the certificate is chained to a root certificate.

### FAQ: Can I configure my app to require a certificate only on certain paths?

This approach isn't possible. The certificate exchange is completed at the start of the HTTPS conversation. The operation is done by the server before the first request is received on that connection, so it isn't possible to scope based on any request fields.

## Process handler events

The handler has two events:

* `OnAuthenticationFailed`: Called if an exception happens during authentication and allows you to react.

* `OnCertificateValidated`: Called after the certificate is validated, passed validation, and a default principal is created. This event allows you to perform your own validation and augment or replace the principal. Examples include:

  * Determining if the certificate is known to your services.

  * Constructing your own principal, such as in the following example:

    ```
    builder.Services.AddAuthentication(
            CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate(options =>
        {
            options.Events = new CertificateAuthenticationEvents
            {
                OnCertificateValidated = context =>
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();

                    return Task.CompletedTask;
                }
            };
        });
    ```

If the inbound certificate doesn't satisfy your extra validation, call `context.Fail("failure reason")` with a failure reason.

For better functionality, call a service registered in dependency injection that connects to a database or other type of user store. Access the service by using the context passed into the delegate. Consider the following example:

```
builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService = context.HttpContext.RequestServices
                    .GetRequiredService<ICertificateValidationService>();

                if (validationService.ValidateCertificate(context.ClientCertificate))
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }

                return Task.CompletedTask;
            }
        };
    });
```

Conceptually, the validation of the certificate is an authorization concern. For example, you can add a check on an issuer or thumbprint in an authorization policy rather than inside the `OnCertificateValidated` handler.

## Configure your server to require certificates

The following sections describe how to configure your server to require certificates for a specific solution, including Kestrel, IIS, Azure, custom web proxies, and Azure Web Apps.

### Kestrel

In the *Program.cs* file, configure Kestrel as follows:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<KestrelServerOptions>(options =>
{
    options.ConfigureHttpsDefaults(options =>
        options.ClientCertificateMode = ClientCertificateMode.RequireCertificate);
});
```

Note

When an endpoint is created by calling the [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) method **before** calling the [ConfigureHttpsDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults) method, the endpoint doesn't have the defaults applied.

### IIS

Complete the following steps in IIS Manager:

1. In the **Connections** tab, select your site.
2. In the **Features View** window, double-click **SSL Settings**.
3. Select the **Require SSL** checkbox.
4. For the **Client certificates** option, select **Require**.

### Azure and custom web proxies

For more information on how to configure the certificate forwarding middleware, see the [host and deploy documentation](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0#certificate-forwarding).

### Certificate authentication in Azure Web Apps

No forwarding configuration is required for Azure. The certificate-forwarding middleware sets up the configuration.

Note

Certificate-forwarding middleware is required for this scenario.

For more information, see [Use TLS/SSL certificates in your app code (Azure documentation)](/en-us/azure/app-service/configure-ssl-certificate-in-code).

### Certificate authentication in custom web proxies

The `AddCertificateForwarding` method is used to specify:

* The client header name.
* How to load the certificate (via the `HeaderConverter` property).

In custom web proxies, the certificate is passed as a custom request header, for example `X-SSL-CERT`. To use the certificate, configure certificate forwarding in the *Program.cs* file:

```
builder.Services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "X-SSL-CERT";

    options.HeaderConverter = headerValue =>
    {
        X509Certificate2? clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            clientCertificate = new X509Certificate2(StringToByteArray(headerValue));
        }

        return clientCertificate!;

        static byte[] StringToByteArray(string hex)
        {
            var numberChars = hex.Length;
            var bytes = new byte[numberChars / 2];

            for (int i = 0; i < numberChars; i += 2)
            {
                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
            }

            return bytes;
        }
    };
});
```

If NGINX is used with the configuration `proxy_set_header ssl-client-cert $ssl_client_escaped_cert` to reverse proxy the app, or the app is deployed on Kubernetes by using NGINX Ingress, the client certificate is passed to the app in [URL-encoded form](https://developer.mozilla.org/docs/Glossary/percent-encoding). To use the certificate, decode it as follows:

```
builder.Services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";

    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2? clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            clientCertificate = X509Certificate2.CreateFromPem(
                WebUtility.UrlDecode(headerValue));
        }

        return clientCertificate!;
    };
});
```

Add the middleware in the *Program.cs* file. The `UseCertificateForwarding` method is called before the calls to the `UseAuthentication` and `UseAuthorization` methods:

```
var app = builder.Build();

app.UseCertificateForwarding();

app.UseAuthentication();
app.UseAuthorization();
```

A separate class can be used to implement validation logic. Because the same self-signed certificate is used in this example, ensure that only your certificate can be used. Validate that the thumbprints of both the client certificate and the server certificate match. Otherwise, any certificate can be used and be sufficient for authentication. The certificate is then used inside the `AddCertificate` method. You can also validate the subject or the issuer here if you use intermediate or child certificates.

```
using System.Security.Cryptography.X509Certificates;

namespace CertAuthSample.Snippets;

public class SampleCertificateValidationService : ICertificateValidationService
{
    public bool ValidateCertificate(X509Certificate2 clientCertificate)
    {
        // Don't hardcode passwords in production code.
        // Use a certificate thumbprint or Azure Key Vault.
        var expectedCertificate = new X509Certificate2(
            Path.Combine("/path/to/pfx"), "1234");

        return clientCertificate.Thumbprint == expectedCertificate.Thumbprint;
    }
}
```

#### Implement an HttpClient with a certificate and IHttpClientFactory

In the following example, a client certificate is added to a `HttpClientHandler` by using the `ClientCertificates` property from the handler. This handler can then be used in a named instance of an `HttpClient` with the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) method. This scenario is configured in the *Program.cs* file:

```
var clientCertificate =
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

builder.Services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});
```

The `IHttpClientFactory` can then be used to get the named instance with the handler and the certificate. The `CreateClient` method with the name of the client defined in the *Program.cs* file is used to get the instance. The HTTP request can be sent by using the client as required:

```
public class SampleHttpService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SampleHttpService(IHttpClientFactory httpClientFactory)
        => _httpClientFactory = httpClientFactory;

    public async Task<JsonDocument> GetAsync()
    {
        var httpClient = _httpClientFactory.CreateClient("namedClient");
        var httpResponseMessage = await httpClient.GetAsync("https://example.com");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            return JsonDocument.Parse(
                await httpResponseMessage.Content.ReadAsStringAsync());
        }

        throw new ApplicationException($"Status code: {httpResponseMessage.StatusCode}");
    }
}
```

If the correct certificate is sent to the server, the data is returned. If no certificate or the wrong certificate is sent, the server returns an HTTP 403 status code.

### Certificates in PowerShell

Creating the certificates is the hardest part in setting up this flow. You can create a root certificate by using the `New-SelfSignedCertificate` PowerShell cmdlet. When you create the certificate, use a strong password. It's important to add the `KeyUsageProperty` parameter and the `KeyUsage` parameter as shown in the examples.

#### Create root certificate authority

The following code shows how to create a certificate authority (CA) at the root:

```
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt
```

Note

The `-DnsName` parameter value must match the deployment target of the app. For example, "localhost" for development.

#### Install in the trusted root

The root certificate must be trusted on your host system. Only root certificates created by a certificate authority are trusted by default. For information on how to trust the root certificate on Windows, see [the Windows documentation](/en-us/windows-hardware/drivers/install/trusted-root-certification-authorities-certificate-store) or the [Import-Certificate](/en-us/powershell/module/pki/import-certificate) PowerShell cmdlet.

#### Use an intermediate certificate

An intermediate certificate can now be created from the root certificate. This approach isn't required for all use cases, but you might need to create many certificates or need to activate or disable groups of certificates. The `TextExtension` parameter is required to set the path length in the basic constraints of the certificate.

The intermediate certificate can then be added to the trusted intermediate certificate in the Windows host system.

```
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt
```

#### Create child certificate from intermediate certificate

A child certificate can be created from the intermediate certificate. This child certificate is the end entity. You don't need to create more child certificates.

```
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
```

#### Create child certificate from root certificate

A child certificate can also be created from the root certificate directly.

```
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
```

#### Example: root - intermediate certificate - certificate

The following example shows configuration of the root CA, the intermediate certificate, and the child certificate:

```
$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" 

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt
```

When you use the root, intermediate, or child certificates, the certificates can be validated by using the Thumbprint or PublicKey as required:

```
using System.Security.Cryptography.X509Certificates;

namespace CertAuthSample.Snippets;

public class SampleCertificateThumbprintsValidationService : ICertificateValidationService
{
    private readonly string[] validThumbprints = new[]
    {
        "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
        "0C89639E4E2998A93E423F919B36D4009A0F9991",
        "BA9BF91ED35538A01375EFC212A2F46104B33A44"
    };

    public bool ValidateCertificate(X509Certificate2 clientCertificate)
        => validThumbprints.Contains(clientCertificate.Thumbprint);
}
```

[]()

## Cache certificate validation results

.NET 5 and later versions support the ability to enable caching of validation results. The caching dramatically improves performance of certificate authentication because validation is an expensive operation.

By default, certificate authentication disables caching. To enable caching, call the `AddCertificateCache` method in the *Program.cs* file:

```
builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate()
    .AddCertificateCache(options =>
    {
        options.CacheSize = 1024;
        options.CacheEntryExpiration = TimeSpan.FromMinutes(2);
    });
```

When [AddCertificateCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.certificateauthenticationappbuilderextensions.addcertificatecache) is enabled, the result produced by [OnCertificateValidated](/en-us/dotnet/api/microsoft.aspnetcore.authentication.certificate.certificateauthenticationevents.oncertificatevalidated) must be a pure function of the authentication scheme and certificate. Don't base the result on request context because the cached result is reused for subsequent requests. Place request-context-dependent decisions in an authorization policy or provide a context-aware implementation of [ICertificateValidationCache](/en-us/dotnet/api/microsoft.aspnetcore.authentication.certificate.icertificatevalidationcache).

The cache stores both successful and failed validation results for the configured cache entry lifetime.

The default caching implementation stores results in memory. You can provide your own cache by implementing `ICertificateValidationCache` and registering it with dependency injection. For example, `services.AddSingleton<ICertificateValidationCache, YourCache>()`.

## Use optional client certificates

This section provides information for apps that must protect a subset of the app with a certificate. For example, a Razor Page or controller in the app might require client certificates. This scenario presents some challenges:

* Client certificates are a TLS feature, not an HTTP feature.
* Client certificates are negotiated per-connection and usually at the start of the connection before any HTTP data is available.

There are two approaches for implementing optional client certificates:

* Option 1: Use separate host names (SNI) and redirection. While this option involves more work to configure, the approach is recommended because it works in most environments and protocols.
* Option 2: Renegotiation during an HTTP request. This approach has several limitations and isn't recommended.

### Separate hosts (SNI)

At the start of the connection, only the Server Name Indication (SNI)† is known. Client certificates can be configured per host name so one host requires the certificates and another doesn't.

* Set up binding for the domain and subdomain.

  For example, set up bindings on `contoso.com` and `myClient.contoso.com`. The `contoso.com` host doesn't require a client certificate but `myClient.contoso.com` does.

  For more information, see the following resources:

  * [Kestrel web server](../../fundamentals/servers/kestrel?view=aspnetcore-10.0)

    * [Configure HTTPS in code (listenOptions.UseHttps)](../../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#configure-https-in-code)
    * [ClientCertificateMode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.clientcertificatemode#microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions-clientcertificatemode) property

  * IIS

    * [Create the IIS site](../../tutorials/publish-to-iis?view=aspnetcore-10.0#create-the-iis-site)
    * [Configure security on IIS](/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis#configure-ssl-settings-2)

  * HTTP.sys

    * [Configure Windows Server](../../fundamentals/servers/httpsys?view=aspnetcore-10.0#configure-windows-server)

  .NET 5 and later offers more convenient support for redirecting to acquire optional client certificates. For more information, see the [Optional certificates sample](https://github.com/dotnet/aspnetcore/tree/9ce4a970a21bace3fb262da9591ed52359309592/src/Security/Authentication/Certificate/samples/Certificate.Optional.Sample).

* For requests to the web app that require a client certificate and don't have one, redirect to the same page by using the client certificate protected subdomain.

  For example, redirect to `myClient.contoso.com/requestedPage`. Because the request to `myClient.contoso.com/requestedPage` is a different hostname than `contoso.com/requestedPage`, the client establishes a different connection and the client certificate is provided.

  For more information, see [Introduction to authorization in ASP.NET Core](../authorization/introduction?view=aspnetcore-10.0).

† Server Name Indication (SNI) is a TLS extension that's used to include a virtual domain as a part of SSL negotiation. This approach effectively means the virtual domain name, or a hostname, can be used to identify the network end point.

### TLS renegotiation

TLS renegotiation is a process by which the client and server can reassess the encryption requirements for an individual connection, including requesting a client certificate if not previously provided. TLS renegotiation is a security risk and isn't recommended because:

* In HTTP/1.1, the server must first buffer or consume any in-flight HTTP data such as POST request bodies to make sure the connection is clear for the renegotiation. Otherwise, the renegotiation can stop responding or fail.
* HTTP/2 and HTTP/3 [explicitly prohibit](https://datatracker.ietf.org/doc/html/rfc7540#section-9.2.1) renegotiation.
* There are security risks associated with renegotiation. TLS 1.3 removed renegotiation of the whole connection and replaced it with a new extension for requesting only the client certificate after the start of the connection. This mechanism is exposed via the same APIs and is still subject to the prior constraints of buffering and HTTP protocol versions.

The implementation and configuration of this feature varies by server and framework version, as described in the following sections.

#### IIS

IIS manages the client certificate negotiation on your behalf. A subsection of the application can enable the `SslRequireCert` option to negotiate the client certificate for those requests. For more information, see [Configuration in the IIS documentation](/en-us/iis/configuration/system.webserver/security/access#configuration).

IIS automatically buffers any request body data up to a configured size limit before renegotiating. Requests that exceed the limit are rejected with a 413 response. This limit defaults to 48 KB and is configurable by setting the [uploadReadAheadSize](/en-us/iis/configuration/system.webserver/serverruntime#attributes) property.

#### HttpSys

HttpSys has two settings that control the client certificate negotiation and both should be set. The first is in the *netsh.exe* file under `http add sslcert clientcertnegotiation=enable/disable`. This flag indicates whether to negotiate the client certificate at the start of a connection. Set the value to `disable` for optional client certificates. For more information, see the `http add sslcert` parameter usage in the [netsh docs](/en-us/windows-server/administration/windows-commands/netsh-http#parameters).

The other setting is the [ClientCertificateMethod](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.clientcertificatemethod#microsoft-aspnetcore-server-httpsys-httpsysoptions-clientcertificatemethod) property. When set to `AllowRenegotation`, the client certificate can be renegotiated during a request.

Note

The application should buffer or consume any request body data before attempting the renegotiation. Otherwise, the request might become unresponsive.

An application can first check the [ClientCertificate](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.clientcertificate#microsoft-aspnetcore-http-connectioninfo-clientcertificate) property to see if the certificate is available. If it isn't available, ensure the request body is consumed before calling the [GetClientCertificateAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.getclientcertificateasync) method to negotiate one. `GetClientCertificateAsync` can return a null certificate if the client declines to provide one.

Note

The behavior of the `ClientCertificate` property changed in .NET 6. For more information, see [GitHub issue #466](https://github.com/aspnet/Announcements/issues/466).

#### Kestrel

Kestrel controls client certificate negotiation with the [ClientCertificateMode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.clientcertificatemode#microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions-clientcertificatemode) property.

.NET 6 and later provides the `DelayCertificate` option for the [ClientCertificateMode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.clientcertificatemode) property. When this option is set, an app can check the [ClientCertificate](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.clientcertificate#microsoft-aspnetcore-http-connectioninfo-clientcertificate) property to see if the certificate is available. If it isn't available, ensure the request body is consumed before calling the [GetClientCertificateAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.getclientcertificateasync) method to negotiate one. `GetClientCertificateAsync` can return a null certificate if the client declines to provide one.

Note

The application should buffer or consume any request body data before attempting the renegotiation. Otherwise, `GetClientCertificateAsync` might throw the exception, *InvalidOperationException: Client stream needs to be drained before renegotiation*.

If you programmatically configure the TLS settings per SNI host name, call the [UseHttps](../../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#configure-https-in-code) overload (.NET 6 or later) that takes a [TlsHandshakeCallbackOptions](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.tlshandshakecallbackoptions) class object. This option controls client certificate renegotiation via the [AllowDelayedClientCertificateNegotation](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.tlshandshakecallbackcontext.allowdelayedclientcertificatenegotation) property. For more information, see the [ListenOptionsHttpsExtensions.UseHttps](/en-us/dotnet/api/microsoft.aspnetcore.hosting.listenoptionshttpsextensions.usehttps) method.

`Microsoft.AspNetCore.Authentication.Certificate` contains an implementation similar to [Certificate Authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) for ASP.NET Core. Certificate authentication happens at the TLS level, which occurs long before it ever gets to ASP.NET Core. More accurately, this authentication handler validates the certificate and gives you an event where you can resolve the certificate to a `ClaimsPrincipal` value.

[Configure your server](#configure-your-server-to-require-certificates) for certificate authentication, be it IIS, Kestrel, Azure Web Apps, or whatever else you're using.

## Proxy and load balancer scenarios

Certificate authentication is a stateful scenario primarily used where a proxy or load balancer doesn't handle traffic between clients and servers. If a proxy or load balancer is used, certificate authentication only works if the proxy or load balancer:

* Handles the authentication.
* Passes the user authentication information to the app (for example, in a request header), which acts on the authentication information.

An alternative to certificate authentication in environments where proxies and load balancers are used is Active Directory Federated Services (ADFS) with OpenID Connect (OIDC).

## Get started

Acquire an HTTPS certificate, apply it, and [configure your server](#configure-your-server-to-require-certificates) to require certificates.

In your web app, add a reference to the [Microsoft.AspNetCore.Authentication.Certificate](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Certificate) package. Then in the `Startup.ConfigureServices` method, call `services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);` with your options, providing a delegate for `OnCertificateValidated` to do any supplementary validation on the client certificate sent with requests. Turn that information into a `ClaimsPrincipal` and set it on the `context.Principal` property.

If authentication fails, this handler returns a `403 (Forbidden)` response rather a `401 (Unauthorized)`, as you might expect. The reasoning is that the authentication should happen during the initial TLS connection. By the time it reaches the handler, it's too late. There's no way to upgrade the connection from an anonymous connection to one with a certificate.

Also add `app.UseAuthentication();` in the `Startup.Configure` method. Otherwise, the `HttpContext.User` will not be set to `ClaimsPrincipal` created from the certificate. For example:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate()
        // Adding an ICertificateValidationCache results in certificate auth caching the results.
        // The default implementation uses a memory cache.
        .AddCertificateCache();

    // All other service configuration
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();

    // All other app configuration
}
```

The preceding example demonstrates the default way to add certificate authentication. The handler constructs a user principal using the common certificate properties.

## Configure certificate validation

The `CertificateAuthenticationOptions` handler has some built-in validations that are the minimum validations you should perform on a certificate. Each of these settings is enabled by default.

### AllowedCertificateTypes = Chained, SelfSigned, or All (Chained | SelfSigned)

Default value: `CertificateTypes.Chained`

This check validates that only the appropriate certificate type is allowed. If the app is using self-signed certificates, this option needs to be set to `CertificateTypes.All` or `CertificateTypes.SelfSigned`.

### ValidateCertificateUse

Default value: `true`

This check validates that the certificate presented by the client has the Client Authentication extended key use (EKU), or no EKUs at all. As the specifications say, if no EKU is specified, then all EKUs are deemed valid.

### ValidateValidityPeriod

Default value: `true`

This check validates that the certificate is within its validity period. On each request, the handler ensures that a certificate that was valid when it was presented hasn't expired during its current session.

### RevocationFlag

Default value: `X509RevocationFlag.ExcludeRoot`

A flag that specifies which certificates in the chain are checked for revocation.

Revocation checks are only performed when the certificate is chained to a root certificate.

### RevocationMode

Default value: `X509RevocationMode.Online`

A flag that specifies how revocation checks are performed.

Specifying an online check can result in a long delay while the certificate authority is contacted.

Revocation checks are only performed when the certificate is chained to a root certificate.

### Can I configure my app to require a certificate only on certain paths?

This isn't possible. Remember the certificate exchange is done at the start of the HTTPS conversation, it's done by the server before the first request is received on that connection so it's not possible to scope based on any request fields.

## Handler events

The handler has two events:

* `OnAuthenticationFailed`: Called if an exception happens during authentication and allows you to react.

* `OnCertificateValidated`: Called after the certificate has been validated, passed validation and a default principal has been created. This event allows you to perform your own validation and augment or replace the principal. For examples include:

  * Determining if the certificate is known to your services.

  * Constructing your own principal. Consider the following example in `Startup.ConfigureServices`:

    ```
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate(options =>
        {
            options.Events = new CertificateAuthenticationEvents
            {
                OnCertificateValidated = context =>
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier, 
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer),
                        new Claim(ClaimTypes.Name,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();

                    return Task.CompletedTask;
                }
            };
        });
    ```

If you find the inbound certificate doesn't meet your extra validation, call `context.Fail("failure reason")` with a failure reason.

For real functionality, you'll probably want to call a service registered in dependency injection that connects to a database or other type of user store. Access your service by using the context passed into your delegate. Consider the following example in `Startup.ConfigureServices`:

```
services.AddAuthentication(
    CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService =
                    context.HttpContext.RequestServices
                        .GetRequiredService<ICertificateValidationService>();

                if (validationService.ValidateCertificate(
                    context.ClientCertificate))
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }                     

                return Task.CompletedTask;
            }
        };
    });
```

Conceptually, the validation of the certificate is an authorization concern. Adding a check on, for example, an issuer or thumbprint in an authorization policy, rather than inside `OnCertificateValidated`, is perfectly acceptable.

## Configure your server to require certificates

### Kestrel

In `Program.cs`, configure Kestrel as follows:

```
public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(o =>
            {
                o.ConfigureHttpsDefaults(o => 
                    o.ClientCertificateMode =  ClientCertificateMode.RequireCertificate);
            });
        });
}
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureHttpsDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults) won't have the defaults applied.

### IIS

Complete the following steps in IIS Manager:

1. Select your site from the **Connections** tab.
2. Double-click the **SSL Settings** option in the **Features View** window.
3. Check the **Require SSL** checkbox, and select the **Require** radio button in the **Client certificates** section.

### Azure and custom web proxies

See the [host and deploy documentation](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0#certificate-forwarding) for how to configure the certificate forwarding middleware.

### Use certificate authentication in Azure Web Apps

No forwarding configuration is required for Azure. Forwarding configuration is set up by the certificate-forwarding middleware.

Note

Certificate-forwarding middleware is required for this scenario.

For more information, see [Use a TLS/SSL certificate in your code in Azure App Service (Azure documentation)](/en-us/azure/app-service/configure-ssl-certificate-in-code).

### Use certificate authentication in custom web proxies

The `AddCertificateForwarding` method is used to specify:

* The client header name.
* How the certificate is to be loaded (using the `HeaderConverter` property).

In custom web proxies, the certificate is passed as a custom request header, for example `X-SSL-CERT`. To use it, configure certificate forwarding in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options =>
    {
        options.CertificateHeader = "X-SSL-CERT";
        options.HeaderConverter = (headerValue) =>
        {
            X509Certificate2 clientCertificate = null;

            if(!string.IsNullOrWhiteSpace(headerValue))
            {
                byte[] bytes = StringToByteArray(headerValue);
                clientCertificate = new X509Certificate2(bytes);
            }

            return clientCertificate;
        };
    });
}

private static byte[] StringToByteArray(string hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];

    for (int i = 0; i < NumberChars; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }

    return bytes;
}
```

If the app is reverse proxied by NGINX with the configuration `proxy_set_header ssl-client-cert $ssl_client_escaped_cert` or deployed on Kubernetes using NGINX Ingress, the client certificate is passed to the app in [URL-encoded form](https://developer.mozilla.org/docs/Glossary/percent-encoding). To use the certificate, decode it as follows:

In `Startup.ConfigureServices` (`Startup.cs`):

```
services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";
    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2 clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            string certPem = WebUtility.UrlDecode(headerValue);
            clientCertificate = X509Certificate2.CreateFromPem(certPem);
        }

        return clientCertificate;
    };
});
```

The `Startup.Configure` method then adds the middleware. `UseCertificateForwarding` is called before the calls to `UseAuthentication` and `UseAuthorization`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseRouting();

    app.UseCertificateForwarding();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

A separate class can be used to implement validation logic. Because the same self-signed certificate is used in this example, ensure that only your certificate can be used. Validate that the thumbprints of both the client certificate and the server certificate match, otherwise any certificate can be used and will be enough to authenticate. This would be used inside the `AddCertificate` method. You could also validate the subject or the issuer here if you're using intermediate or child certificates.

```
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            // Do not hardcode passwords in production code
            // Use thumbprint or key vault
            var cert = new X509Certificate2(
                Path.Combine("sts_dev_cert.pfx"), "1234");

            if (clientCertificate.Thumbprint == cert.Thumbprint)
            {
                return true;
            }

            return false;
        }
    }
}
```

#### Implement an HttpClient using a certificate and the HttpClientHandler

The `HttpClientHandler` could be added directly in the constructor of the `HttpClient` class. Care should be taken when creating instances of the `HttpClient`. The `HttpClient` will then send the certificate with each request.

```
private async Task<JsonDocument> GetApiDataUsingHttpClientHandler()
{
    var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(cert);
    var client = new HttpClient(handler);

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
```

#### Implement an HttpClient using a certificate and a named HttpClient from IHttpClientFactory

In the following example, a client certificate is added to a `HttpClientHandler` using the `ClientCertificates` property from the handler. This handler can then be used in a named instance of an `HttpClient` using the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) method. This is setup in `Startup.ConfigureServices`:

```
var clientCertificate = 
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});
```

The `IHttpClientFactory` can then be used to get the named instance with the handler and the certificate. The `CreateClient` method with the name of the client defined in the `Startup` class is used to get the instance. The HTTP request can be sent using the client as required.

```
private readonly IHttpClientFactory _clientFactory;

public ApiService(IHttpClientFactory clientFactory)
{
    _clientFactory = clientFactory;
}

private async Task<JsonDocument> GetApiDataWithNamedClient()
{
    var client = _clientFactory.CreateClient("namedClient");

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
```

If the correct certificate is sent to the server, the data is returned. If no certificate or the wrong certificate is sent, an HTTP 403 status code is returned.

### Create certificates in PowerShell

Creating the certificates is the hardest part in setting up this flow. A root certificate can be created using the `New-SelfSignedCertificate` PowerShell cmdlet. When creating the certificate, use a strong password. It's important to add the `KeyUsageProperty` parameter and the `KeyUsage` parameter as shown.

#### Create root CA

```
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt
```

Note

The `-DnsName` parameter value must match the deployment target of the app. For example, "localhost" for development.

#### Install in the trusted root

The root certificate needs to be trusted on your host system. A root certificate which was not created by a certificate authority won't be trusted by default. For information on how to trust the root certificate on Windows, see [this question](https://social.msdn.microsoft.com/Forums/SqlServer/5ed119ef-1704-4be4-8a4f-ef11de7c8f34/a-certificate-chain-processed-but-terminated-in-a-root-certificate-which-is-not-trusted-by-the).

#### Intermediate certificate

An intermediate certificate can now be created from the root certificate. This isn't required for all use cases, but you might need to create many certificates or need to activate or disable groups of certificates. The `TextExtension` parameter is required to set the path length in the basic constraints of the certificate.

The intermediate certificate can then be added to the trusted intermediate certificate in the Windows host system.

```
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt
```

#### Create child certificate from intermediate certificate

A child certificate can be created from the intermediate certificate. This is the end entity and doesn't need to create more child certificates.

```
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
```

#### Create child certificate from root certificate

A child certificate can also be created from the root certificate directly.

```
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
```

#### Example root - intermediate certificate - certificate

```
$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" 

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt
```

When using the root, intermediate, or child certificates, the certificates can be validated using the Thumbprint or PublicKey as required.

```
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService 
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            return CheckIfThumbprintIsValid(clientCertificate);
        }

        private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
        {
            var listOfValidThumbprints = new List<string>
            {
                "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
                "0C89639E4E2998A93E423F919B36D4009A0F9991",
                "BA9BF91ED35538A01375EFC212A2F46104B33A44"
            };

            if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
            {
                return true;
            }

            return false;
        }
    }
}
```

[]()

## Certificate validation caching

.NET 5 or later versions support the ability to enable caching of validation results. The caching dramatically improves performance of certificate authentication, as validation is an expensive operation.

By default, certificate authentication disables caching. To enable caching, call `AddCertificateCache` in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
            .AddCertificate()
            .AddCertificateCache(options =>
            {
                options.CacheSize = 1024;
                options.CacheEntryExpiration = TimeSpan.FromMinutes(2);
            });
}
```

When [AddCertificateCache](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.certificateauthenticationappbuilderextensions.addcertificatecache) is enabled, the result produced by [OnCertificateValidated](/en-us/dotnet/api/microsoft.aspnetcore.authentication.certificate.certificateauthenticationevents.oncertificatevalidated) must be a pure function of the authentication scheme and certificate. Don't base the result on request context because the cached result is reused for subsequent requests. Place request-context-dependent decisions in an authorization policy or provide a context-aware implementation of [ICertificateValidationCache](/en-us/dotnet/api/microsoft.aspnetcore.authentication.certificate.icertificatevalidationcache).

The cache stores both successful and failed validation results for the configured cache entry lifetime.

The default caching implementation stores results in memory. You can provide your own cache by implementing `ICertificateValidationCache` and registering it with dependency injection. For example, `services.AddSingleton<ICertificateValidationCache, YourCache>()`.

## Optional client certificates

This section provides information for apps that must protect a subset of the app with a certificate. For example, a Razor Page or controller in the app might require client certificates. This presents challenges as client certificates:

* Are a TLS feature, not an HTTP feature.
* Are negotiated per-connection and usually at the start of the connection before any HTTP data is available.

There are two approaches to implementing optional client certificates:

1. Using separate host names (SNI) and redirecting. While more work to configure, this is recommended because it works in most environments and protocols.
2. Renegotiation during an HTTP request. This has several limitations and is not recommended.

### Separate Hosts (SNI)

At the start of the connection, only the Server Name Indication (SNI)† is known. Client certificates can be configured per host name so that one host requires them and another does not.

* Set up binding for the domain and subdomain:

  * For example, set up bindings on `contoso.com` and `myClient.contoso.com`. The `contoso.com` host doesn't require a client certificate but `myClient.contoso.com` does.

  * For more information, see:

    * [Kestrel web server in ASP.NET Core](../../fundamentals/servers/kestrel?view=aspnetcore-10.0):

      * [ListenOptions.UseHttps](../../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsusehttps)
      * [ClientCertificateMode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.clientcertificatemode#microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions-clientcertificatemode)

    * IIS

      * [Hosting IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0#create-the-iis-site)
      * [Configure security on IIS](/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis#configure-ssl-settings-2)

    * HTTP.sys: [Configure Windows Server](../../fundamentals/servers/httpsys?view=aspnetcore-10.0#configure-windows-server)

.NET 5 or later adds more convenient support for redirecting to acquire optional client certificates. For more information, see the [Optional certificates sample](https://github.com/dotnet/aspnetcore/tree/9ce4a970a21bace3fb262da9591ed52359309592/src/Security/Authentication/Certificate/samples/Certificate.Optional.Sample).

* For requests to the web app that require a client certificate and don't have one:

  * Redirect to the same page using the client certificate protected subdomain.
  * For example, redirect to `myClient.contoso.com/requestedPage`. Because the request to `myClient.contoso.com/requestedPage` is a different hostname than `contoso.com/requestedPage`, the client establishes a different connection and the client certificate is provided.
  * For more information, see [Introduction to authorization in ASP.NET Core](../authorization/introduction?view=aspnetcore-10.0).

† Server Name Indication (SNI) is a TLS extension to include a virtual domain as a part of SSL negotiation. This effectively means the virtual domain name, or a hostname, can be used to identify the network end point.

### Renegotiation

TLS renegotiation is a process by which the client and server can re-assess the encryption requirements for an individual connection, including requesting a client certificate if not previously provided. TLS renegotiation is a security risk and isn't recommended because:

* In HTTP/1.1 the server must first buffer or consume any HTTP data that is in flight such as POST request bodies to make sure the connection is clear for the renegotiation. Otherwise the renegotiation can stop responding or fail.
* HTTP/2 and HTTP/3 [explicitly prohibit](https://tools.ietf.org/html/rfc7540#section-9.2.1) renegotiation.
* There are security risks associated with renegotiation. TLS 1.3 removed renegotiation of the whole connection and replaced it with a new extension for requesting only the client certificate after the start of the connection. This mechanism is exposed via the same APIs and is still subject to the prior constraints of buffering and HTTP protocol versions.

The implementation and configuration of this feature varies by server and framework version.

#### IIS

IIS manages the client certificate negotiation on your behalf. A subsection of the application can enable the `SslRequireCert` option to negotiate the client certificate for those requests. See [Configuration in the IIS documentation](/en-us/iis/configuration/system.webserver/security/access#configuration) for details.

IIS will automatically buffer any request body data up to a configured size limit before renegotiating. Requests that exceed the limit are rejected with a 413 response. This limit defaults to 48KB and is configurable by setting the [uploadReadAheadSize](/en-us/iis/configuration/system.webserver/serverruntime).

#### HttpSys

HttpSys has two settings which control the client certificate negotiation and both should be set. The first is in netsh.exe under `http add sslcert clientcertnegotiation=enable/disable`. This flag indicates if the client certificate should be negotiated at the start of a connection and it should be set to `disable` for optional client certificates. See the [netsh docs](/en-us/windows-server/networking/technologies/netsh/netsh-http#add-sslcert) for details.

The other setting is [ClientCertificateMethod](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.clientcertificatemethod#microsoft-aspnetcore-server-httpsys-httpsysoptions-clientcertificatemethod). When set to `AllowRenegotation`, the client certificate can be renegotiated during a request.

*NOTE* The application should buffer or consume any request body data before attempting the renegotiation, otherwise the request may become unresponsive.

There is a [known issue](https://github.com/dotnet/aspnetcore/issues/33586) where enabling `AllowRenegotation` can cause the renegotiation to happen synchronously when accessing the [ClientCertificate](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.clientcertificate#microsoft-aspnetcore-http-connectioninfo-clientcertificate) property. Call the [GetClientCertificateAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.getclientcertificateasync) method to avoid this. This has been addressed in .NET 6. For more information, see [this GitHub issue](https://github.com/aspnet/Announcements/issues/466). Note `GetClientCertificateAsync` can return a null certificate if the client declines to provide one.

#### Kestrel

Kestrel controls client certificate negotiation with the [ClientCertificateMode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.clientcertificatemode#microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions-clientcertificatemode) option.

For .NET 5 or earlier Kestrel does not support renegotiating after the start of a connection to acquire a client certificate. This feature has been added in .NET 6.

`Microsoft.AspNetCore.Authentication.Certificate` contains an implementation similar to [Certificate Authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) for ASP.NET Core. Certificate authentication happens at the TLS level, long before it ever gets to ASP.NET Core. More accurately, this is an authentication handler that validates the certificate and then gives you an event where you can resolve that certificate to a `ClaimsPrincipal`.

[Configure your server](#configure-your-server-to-require-certificates) for certificate authentication, be it IIS, Kestrel, Azure Web Apps, or whatever else you're using.

## Proxy and load balancer scenarios

Certificate authentication is a stateful scenario primarily used where a proxy or load balancer doesn't handle traffic between clients and servers. If a proxy or load balancer is used, certificate authentication only works if the proxy or load balancer:

* Handles the authentication.
* Passes the user authentication information to the app (for example, in a request header), which acts on the authentication information.

An alternative to certificate authentication in environments where proxies and load balancers are used is Active Directory Federated Services (ADFS) with OpenID Connect (OIDC).

## Get started

Acquire an HTTPS certificate, apply it, and [configure your server](#configure-your-server-to-require-certificates) to require certificates.

In your web app, add a reference to the [Microsoft.AspNetCore.Authentication.Certificate](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Certificate) package. Then in the `Startup.ConfigureServices` method, call `services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);` with your options, providing a delegate for `OnCertificateValidated` to do any supplementary validation on the client certificate sent with requests. Turn that information into a `ClaimsPrincipal` and set it on the `context.Principal` property.

If authentication fails, this handler returns a `403 (Forbidden)` response rather a `401 (Unauthorized)`, as you might expect. The reasoning is that the authentication should happen during the initial TLS connection. By the time it reaches the handler, it's too late. There's no way to upgrade the connection from an anonymous connection to one with a certificate.

Also add `app.UseAuthentication();` in the `Startup.Configure` method. Otherwise, the `HttpContext.User` will not be set to `ClaimsPrincipal` created from the certificate. For example:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate();

    // All other service configuration
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();

    // All other app configuration
}
```

The preceding example demonstrates the default way to add certificate authentication. The handler constructs a user principal using the common certificate properties.

## Configure certificate validation

The `CertificateAuthenticationOptions` handler has some built-in validations that are the minimum validations you should perform on a certificate. Each of these settings is enabled by default.

### AllowedCertificateTypes = Chained, SelfSigned, or All (Chained | SelfSigned)

Default value: `CertificateTypes.Chained`

This check validates that only the appropriate certificate type is allowed. If the app is using self-signed certificates, this option needs to be set to `CertificateTypes.All` or `CertificateTypes.SelfSigned`.

### ValidateCertificateUse

Default value: `true`

This check validates that the certificate presented by the client has the Client Authentication extended key use (EKU), or no EKUs at all. As the specifications say, if no EKU is specified, then all EKUs are deemed valid.

### ValidateValidityPeriod

Default value: `true`

This check validates that the certificate is within its validity period. On each request, the handler ensures that a certificate that was valid when it was presented hasn't expired during its current session.

### RevocationFlag

Default value: `X509RevocationFlag.ExcludeRoot`

A flag that specifies which certificates in the chain are checked for revocation.

Revocation checks are only performed when the certificate is chained to a root certificate.

### RevocationMode

Default value: `X509RevocationMode.Online`

A flag that specifies how revocation checks are performed.

Specifying an online check can result in a long delay while the certificate authority is contacted.

Revocation checks are only performed when the certificate is chained to a root certificate.

### Can I configure my app to require a certificate only on certain paths?

This isn't possible. Remember the certificate exchange is done at the start of the HTTPS conversation, it's done by the server before the first request is received on that connection so it's not possible to scope based on any request fields.

## Handler events

The handler has two events:

* `OnAuthenticationFailed`: Called if an exception happens during authentication and allows you to react.

* `OnCertificateValidated`: Called after the certificate has been validated, passed validation and a default principal has been created. This event allows you to perform your own validation and augment or replace the principal. For examples include:

  * Determining if the certificate is known to your services.

  * Constructing your own principal. Consider the following example in `Startup.ConfigureServices`:

    ```
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate(options =>
        {
            options.Events = new CertificateAuthenticationEvents
            {
                OnCertificateValidated = context =>
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier, 
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer),
                        new Claim(ClaimTypes.Name,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();

                    return Task.CompletedTask;
                }
            };
        });
    ```

If you find the inbound certificate doesn't meet your extra validation, call `context.Fail("failure reason")` with a failure reason.

For real functionality, you'll probably want to call a service registered in dependency injection that connects to a database or other type of user store. Access your service by using the context passed into your delegate. Consider the following example in `Startup.ConfigureServices`:

```
services.AddAuthentication(
    CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService =
                    context.HttpContext.RequestServices
                        .GetRequiredService<ICertificateValidationService>();

                if (validationService.ValidateCertificate(
                    context.ClientCertificate))
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }                     

                return Task.CompletedTask;
            }
        };
    });
```

Conceptually, the validation of the certificate is an authorization concern. Adding a check on, for example, an issuer or thumbprint in an authorization policy, rather than inside `OnCertificateValidated`, is perfectly acceptable.

## Configure your server to require certificates

### Kestrel

In `Program.cs`, configure Kestrel as follows:

```
public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(o =>
            {
                o.ConfigureHttpsDefaults(o => 
                    o.ClientCertificateMode =  ClientCertificateMode.RequireCertificate);
            });
        });
}
```

Note

Endpoints created by calling [Listen](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.listen) **before** calling [ConfigureHttpsDefaults](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserveroptions.configurehttpsdefaults) won't have the defaults applied.

### IIS

Complete the following steps in IIS Manager:

1. Select your site from the **Connections** tab.
2. Double-click the **SSL Settings** option in the **Features View** window.
3. Check the **Require SSL** checkbox, and select the **Require** radio button in the **Client certificates** section.

### Azure and custom web proxies

See the [host and deploy documentation](../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0#certificate-forwarding) for how to configure the certificate forwarding middleware.

### Use certificate authentication in Azure Web Apps

No forwarding configuration is required for Azure. Forwarding configuration is set up by the certificate-forwarding middleware.

Note

Certificate-forwarding middleware is required for this scenario.

For more information, see [Use a TLS/SSL certificate in your code in Azure App Service (Azure documentation)](/en-us/azure/app-service/configure-ssl-certificate-in-code).

### Use certificate authentication in custom web proxies

The `AddCertificateForwarding` method is used to specify:

* The client header name.
* How the certificate is to be loaded (using the `HeaderConverter` property).

In custom web proxies, the certificate is passed as a custom request header, for example `X-SSL-CERT`. To use it, configure certificate forwarding in `Startup.ConfigureServices`:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options =>
    {
        options.CertificateHeader = "X-SSL-CERT";
        options.HeaderConverter = (headerValue) =>
        {
            X509Certificate2 clientCertificate = null;

            if(!string.IsNullOrWhiteSpace(headerValue))
            {
                byte[] bytes = StringToByteArray(headerValue);
                clientCertificate = new X509Certificate2(bytes);
            }

            return clientCertificate;
        };
    });
}

private static byte[] StringToByteArray(string hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];

    for (int i = 0; i < NumberChars; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }

    return bytes;
}
```

If the app is reverse proxied by NGINX with the configuration `proxy_set_header ssl-client-cert $ssl_client_escaped_cert` or deployed on Kubernetes using NGINX Ingress, the client certificate is passed to the app in [URL-encoded form](https://developer.mozilla.org/docs/Glossary/percent-encoding). To use the certificate, decode it as follows:

Add the namespace for [System.Net](/en-us/dotnet/api/system.net) to the top of `Startup.cs`:

```
using System.Net;
```

In `Startup.ConfigureServices`:

```
services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";
    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2 clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            var bytes = UrlEncodedPemToByteArray(headerValue);
            clientCertificate = new X509Certificate2(bytes);
        }

        return clientCertificate;
    };
});
```

Add the `UrlEncodedPemToByteArray` method:

```
private static byte[] UrlEncodedPemToByteArray(string urlEncodedBase64Pem)
{
    var base64Pem = WebUtility.UrlDecode(urlEncodedBase64Pem);
    var base64Cert = base64Pem
        .Replace("-----BEGIN CERTIFICATE-----", string.Empty)
        .Replace("-----END CERTIFICATE-----", string.Empty)
        .Trim();

    return Convert.FromBase64String(base64Cert);
}
```

The `Startup.Configure` method then adds the middleware. `UseCertificateForwarding` is called before the calls to `UseAuthentication` and `UseAuthorization`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseRouting();

    app.UseCertificateForwarding();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
```

A separate class can be used to implement validation logic. Because the same self-signed certificate is used in this example, ensure that only your certificate can be used. Validate that the thumbprints of both the client certificate and the server certificate match, otherwise any certificate can be used and will be enough to authenticate. This would be used inside the `AddCertificate` method. You could also validate the subject or the issuer here if you're using intermediate or child certificates.

```
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            // Do not hardcode passwords in production code
            // Use thumbprint or key vault
            var cert = new X509Certificate2(
                Path.Combine("sts_dev_cert.pfx"), "1234");

            if (clientCertificate.Thumbprint == cert.Thumbprint)
            {
                return true;
            }

            return false;
        }
    }
}
```

#### Implement an HttpClient using a certificate and the HttpClientHandler

The `HttpClientHandler` could be added directly in the constructor of the `HttpClient` class. Care should be taken when creating instances of the `HttpClient`. The `HttpClient` will then send the certificate with each request.

```
private async Task<JsonDocument> GetApiDataUsingHttpClientHandler()
{
    var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(cert);
    var client = new HttpClient(handler);

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
```

#### Implement an HttpClient using a certificate and a named HttpClient from IHttpClientFactory

In the following example, a client certificate is added to a `HttpClientHandler` using the `ClientCertificates` property from the handler. This handler can then be used in a named instance of an `HttpClient` using the [ConfigurePrimaryHttpMessageHandler](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler) method. This is setup in `Startup.ConfigureServices`:

```
var clientCertificate = 
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});
```

The `IHttpClientFactory` can then be used to get the named instance with the handler and the certificate. The `CreateClient` method with the name of the client defined in the `Startup` class is used to get the instance. The HTTP request can be sent using the client as required.

```
private readonly IHttpClientFactory _clientFactory;

public ApiService(IHttpClientFactory clientFactory)
{
    _clientFactory = clientFactory;
}

private async Task<JsonDocument> GetApiDataWithNamedClient()
{
    var client = _clientFactory.CreateClient("namedClient");

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
```

If the correct certificate is sent to the server, the data is returned. If no certificate or the wrong certificate is sent, an HTTP 403 status code is returned.

### Create certificates in PowerShell

Creating the certificates is the hardest part in setting up this flow. A root certificate can be created using the `New-SelfSignedCertificate` PowerShell cmdlet. When creating the certificate, use a strong password. It's important to add the `KeyUsageProperty` parameter and the `KeyUsage` parameter as shown.

#### Create root CA

```
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt
```

Note

The `-DnsName` parameter value must match the deployment target of the app. For example, "localhost" for development.

#### Install in the trusted root

The root certificate needs to be trusted on your host system. A root certificate which was not created by a certificate authority won't be trusted by default. For information on how to trust the root certificate on Windows, see [this question](https://social.msdn.microsoft.com/Forums/SqlServer/5ed119ef-1704-4be4-8a4f-ef11de7c8f34/a-certificate-chain-processed-but-terminated-in-a-root-certificate-which-is-not-trusted-by-the).

#### Intermediate certificate

An intermediate certificate can now be created from the root certificate. This isn't required for all use cases, but you might need to create many certificates or need to activate or disable groups of certificates. The `TextExtension` parameter is required to set the path length in the basic constraints of the certificate.

The intermediate certificate can then be added to the trusted intermediate certificate in the Windows host system.

```
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt
```

#### Create child certificate from intermediate certificate

A child certificate can be created from the intermediate certificate. This is the end entity and doesn't need to create more child certificates.

```
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
```

#### Create child certificate from root certificate

A child certificate can also be created from the root certificate directly.

```
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
```

#### Example root - intermediate certificate - certificate

```
$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" 

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt
```

When using the root, intermediate, or child certificates, the certificates can be validated using the Thumbprint or PublicKey as required.

```
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService 
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            return CheckIfThumbprintIsValid(clientCertificate);
        }

        private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
        {
            var listOfValidThumbprints = new List<string>
            {
                "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
                "0C89639E4E2998A93E423F919B36D4009A0F9991",
                "BA9BF91ED35538A01375EFC212A2F46104B33A44"
            };

            if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
            {
                return true;
            }

            return false;
        }
    }
}
```

[]()

## Optional client certificates

This section provides information for apps that must protect a subset of the app with a certificate. For example, a Razor Page or controller in the app might require client certificates. This presents challenges as client certificates:

* Are a TLS feature, not an HTTP feature.
* Are negotiated per-connection and usually at the start of the connection before any HTTP data is available.

There are two approaches to implementing optional client certificates:

1. Using separate host names (SNI) and redirecting. While more work to configure, this is recommended because it works in most environments and protocols.
2. Renegotiation during an HTTP request. This has several limitations and is not recommended.

### Separate Hosts (SNI)

At the start of the connection, only the Server Name Indication (SNI)† is known. Client certificates can be configured per host name so that one host requires them and another does not.

* Set up binding for the domain and subdomain:

  * For example, set up bindings on `contoso.com` and `myClient.contoso.com`. The `contoso.com` host doesn't require a client certificate but `myClient.contoso.com` does.

  * For more information, see:

    * [Kestrel web server in ASP.NET Core](../../fundamentals/servers/kestrel?view=aspnetcore-10.0):

      * [ListenOptions.UseHttps](../../fundamentals/servers/kestrel?view=aspnetcore-10.0#listenoptionsusehttps)
      * [ClientCertificateMode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.clientcertificatemode#microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions-clientcertificatemode)
      * Note Kestrel does not currently support multiple TLS configurations on one binding, you'll need two bindings with unique IPs or ports. For more information, see [this GitHub issue](https://github.com/dotnet/runtime/issues/31097).

    * IIS

      * [Hosting IIS](../../host-and-deploy/iis/?view=aspnetcore-10.0#create-the-iis-site)
      * [Configure security on IIS](/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis#configure-ssl-settings-2)

    * HTTP.sys: [Configure Windows Server](../../fundamentals/servers/httpsys?view=aspnetcore-10.0#configure-windows-server)

.NET 5 or later adds more convenient support for redirecting to acquire optional client certificates. For more information, see the [Optional certificates sample](https://github.com/dotnet/aspnetcore/tree/9ce4a970a21bace3fb262da9591ed52359309592/src/Security/Authentication/Certificate/samples/Certificate.Optional.Sample).

* For requests to the web app that require a client certificate and don't have one:

  * Redirect to the same page using the client certificate protected subdomain.
  * For example, redirect to `myClient.contoso.com/requestedPage`. Because the request to `myClient.contoso.com/requestedPage` is a different hostname than `contoso.com/requestedPage`, the client establishes a different connection and the client certificate is provided.
  * For more information, see [Introduction to authorization in ASP.NET Core](../authorization/introduction?view=aspnetcore-10.0).

† Server Name Indication (SNI) is a TLS extension to include a virtual domain as a part of SSL negotiation. This effectively means the virtual domain name, or a hostname, can be used to identify the network end point.

### Renegotiation

TLS renegotiation is a process by which the client and server can re-assess the encryption requirements for an individual connection, including requesting a client certificate if not previously provided. TLS renegotiation is a security risk and isn't recommended because:

* In HTTP/1.1 the server must first buffer or consume any HTTP data that is in flight such as POST request bodies to make sure the connection is clear for the renegotiation. Otherwise the renegotiation can stop responding or fail.
* HTTP/2 and HTTP/3 [explicitly prohibit](https://tools.ietf.org/html/rfc7540#section-9.2.1) renegotiation.
* There are security risks associated with renegotiation. TLS 1.3 removed renegotiation of the whole connection and replaced it with a new extension for requesting only the client certificate after the start of the connection. This mechanism is exposed via the same APIs and is still subject to the prior constraints of buffering and HTTP protocol versions.

The implementation and configuration of this feature varies by server and framework version.

#### IIS

IIS manages the client certificate negotiation on your behalf. A subsection of the application can enable the `SslRequireCert` option to negotiate the client certificate for those requests. See [Configuration in the IIS documentation](/en-us/iis/configuration/system.webserver/security/access#configuration) for details.

IIS will automatically buffer any request body data up to a configured size limit before renegotiating. Requests that exceed the limit are rejected with a 413 response. This limit defaults to 48KB and is configurable by setting the [uploadReadAheadSize](/en-us/iis/configuration/system.webserver/serverruntime).

#### HttpSys

HttpSys has two settings which control the client certificate negotiation and both should be set. The first is in netsh.exe under `http add sslcert clientcertnegotiation=enable/disable`. This flag indicates if the client certificate should be negotiated at the start of a connection and it should be set to `disable` for optional client certificates. See the [netsh docs](/en-us/windows-server/networking/technologies/netsh/netsh-http#add-sslcert) for details.

The other setting is [ClientCertificateMethod](/en-us/dotnet/api/microsoft.aspnetcore.server.httpsys.httpsysoptions.clientcertificatemethod#microsoft-aspnetcore-server-httpsys-httpsysoptions-clientcertificatemethod). When set to `AllowRenegotation`, the client certificate can be renegotiated during a request.

*NOTE* The application should buffer or consume any request body data before attempting the renegotiation, otherwise the request may become unresponsive.

There is a [known issue](https://github.com/dotnet/aspnetcore/issues/33586) where enabling `AllowRenegotation` can cause the renegotiation to happen synchronously when accessing the [ClientCertificate](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.clientcertificate#microsoft-aspnetcore-http-connectioninfo-clientcertificate) property. Call the [GetClientCertificateAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.connectioninfo.getclientcertificateasync) method to avoid this. This has been addressed in .NET 6. For more information, see [this GitHub issue](https://github.com/aspnet/Announcements/issues/466). Note `GetClientCertificateAsync` can return a null certificate if the client declines to provide one.

#### Kestrel

Kestrel controls client certificate negotiation with the [ClientCertificateMode](/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.https.httpsconnectionadapteroptions.clientcertificatemode#microsoft-aspnetcore-server-kestrel-https-httpsconnectionadapteroptions-clientcertificatemode) option.

For .NET 5 or earlier Kestrel does not support renegotiating after the start of a connection to acquire a client certificate. This feature has been added in .NET 6.

Leave questions, comments, and other feedback on optional client certificates in the discussion thread for [GitHub issue #18720](https://github.com/dotnet/AspNetCore.Docs/issues/18720).

## Related content

* [Overview of ASP.NET Core authentication](./?view=aspnetcore-10.0)
* [Introduction to authorization in ASP.NET Core](../authorization/introduction?view=aspnetcore-10.0)
* [Use TLS/SSL certificates in your app code (Azure documentation)](/en-us/azure/app-service/configure-ssl-certificate-in-code)
* [Important changes to App Service managed certificates (Blog, November 2025)](https://techcommunity.microsoft.com/blog/appsonazureblog/follow-up-to-%E2%80%98important-changes-to-app-service-managed-certificates%E2%80%99-november-20/4466120)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management/server
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/state-management/server.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor server-side state management

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article describes common approaches for maintaining a user's data (state) in server-side Blazor scenarios.

## Maintain user state

Server-side Blazor is a stateful app framework. Most of the time, the app maintains a connection to the server. The user's state is held in the server's memory in a *circuit*.

Examples of user state held in a circuit include:

* The hierarchy of component instances and their most recent render output in the rendered UI.
* The values of fields and properties in component instances.
* Data held in [dependency injection (DI)](../../fundamentals/dependency-injection?view=aspnetcore-10.0) service instances that are scoped to the circuit.

User state might also be found in JavaScript variables in the browser's memory set via [JavaScript interop](../javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-10.0) calls.

If a user experiences a temporary network connection loss, Blazor attempts to reconnect the user to their original circuit with their original state. However, reconnecting a user to their original circuit in the server's memory isn't always possible:

* The server can't retain a disconnected circuit forever. The server must release a disconnected circuit after a timeout or when the server is under memory pressure.
* In multi-server, load-balanced deployment environments, individual servers may fail or be automatically removed when no longer required to handle the overall volume of requests. The original server processing requests for a user may become unavailable when the user attempts to reconnect.
* The user might close and reopen their browser or reload the page, which removes any state held in the browser's memory. For example, JavaScript variable values set through JavaScript interop calls are lost.

When a user can't be reconnected to their original circuit, the user receives a new circuit with newly initialized state. This is equivalent to closing and reopening a desktop app.

## When to persist user state

State persistence isn't automatic. You must take steps when developing the app to implement stateful data persistence.

Generally, maintain state across circuits where users are actively creating data, not simply reading data that already exists.

Data persistence is typically only required for high-value state that users expended effort to create. Persisting state either saves time or aids in commercial activities:

* Multi-step web forms: It's time-consuming for a user to re-enter data for several completed steps of a multi-step web form if their state is lost. A user loses state in this scenario if they navigate away from the form and return later.
* Shopping carts: Any commercially important component of an app that represents potential revenue can be maintained. A user who loses their state, and thus their shopping cart, may purchase fewer products or services when they return to the website later.

An app can only persist *app state*. UIs can't be persisted, such as component instances and their render trees. Components and render trees aren't generally serializable. To persist UI state, such as the expanded nodes of a tree view control, the app must use custom code to model the behavior of the UI state as serializable app state.

## Circuit state persistence

During server-side rendering, Blazor Web Apps can persist a user's session (circuit) state when the connection to the server is lost for an extended period of time or proactively paused, as long as a full-page refresh isn't triggered. This allows users to resume their session without losing unsaved work in the following scenarios:

* Browser tab throttling
* Mobile device users switching apps
* Network interruptions
* Proactive resource management (pausing inactive circuits)
* [Enhanced navigation](../fundamentals/navigation?view=aspnetcore-10.0#enhanced-navigation-and-form-handling)

Server resources can be freed up if the circuit state can be persisted and then resumed later:

* Even if disconnected, a circuit might continue to perform work and consume CPU, memory, and other resources. Persisted state only consumes a fixed amount of memory that the developer controls.
* Persisted state represents a subset of the memory consumed by the app, so the server isn't required to keep track of the app's components and other server-side objects.

State is persisted for two scenarios:

* Component state: State that components use for Interactive Server rendering, for example, a list of items retrieved from the database or a form that the user is filling out.
* Scoped services: State held inside of a server-side service, for example, the current user.

Conditions:

* The feature is only effective for Interactive Server rendering.
* If the user refreshes the page (app), the persisted state is lost.
* The state must be JSON serializable. Cyclic references or ORM entities may not serialize correctly.
* Use `@key` for uniqueness when rendering components in a loop to avoid key conflicts.
* Persist only necessary state. Storing excessive data may impact performance.
* No automatic hibernation. You must opt-in and configure state persistence explicitly.
* No guarantee of recovery. If state persistence fails, the app falls back to the default disconnected experience.

State persistence is enabled by default when [AddInteractiveServerComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serverrazorcomponentsbuilderextensions.addinteractiveservercomponents) is called on [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) in the `Program` file. [MemoryCache](/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache) is the default storage implementation for single app instances and stores up to 1,000 persisted circuits for two hours, which are configurable.

Use the following options to change the default values of the in-memory provider:

* [PersistedCircuitInMemoryMaxRetained](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.persistedcircuitinmemorymaxretained) (`{CIRCUIT COUNT}` placeholder): The maximum number of circuits to retain. The default is 1,000 circuits. For example, use `2000` to retain state for up to 2,000 circuits.
* [PersistedCircuitInMemoryRetentionPeriod](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.persistedcircuitinmemoryretentionperiod) (`{RETENTION PERIOD}` placeholder): The maximum retention period as a [TimeSpan](/en-us/dotnet/api/system.timespan). The default is two hours. For example, use `TimeSpan.FromHours(3)` for a three-hour retention period.

```
services.Configure<CircuitOptions>(options =>
{
    options.PersistedCircuitInMemoryMaxRetained = {CIRCUIT COUNT};
    options.PersistedCircuitInMemoryRetentionPeriod = {RETENTION PERIOD};
});
```

Persisting component state across circuits is built on top of the existing [PersistentComponentState](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentcomponentstate) API, which continues to persist state for prerendered components that adopt an interactive render mode. For more information, see [ASP.NET Core Blazor prerendered state persistence](prerendered-state-persistence?view=aspnetcore-10.0).

> \[NOTE] Persisting component state for prerendering works for any interactive render mode, but circuit state persistence only works for the **Interactive Server** render mode.

Annotate component `public` properties with the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute) to enable circuit state persistence. The following example also keys the items with the [`@key` directive attribute](../components/element-component-model-relationships?view=aspnetcore-10.0) to provide a unique identifier for each component instance:

```
@foreach (var item in Items)
{
    <ItemDisplay @key="@($"unique-prefix-{item.Id}")" Item="item" />
}

@code {
    [PersistentState]
    public List<Item> Items { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Items ??= await LoadItemsAsync();
    }
}
```

To persist state for a scoped service:

* Annotate the `public` service property with the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute).
* Add the service to the service collection.
* Call the [RegisterPersistentService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsrazorcomponentbuilderextensions.registerpersistentservice) extension method with the service.

```
public class CustomUserService
{
    [PersistentState]
    public string UserData { get; set; }
}

services.AddScoped<CustomUserService>();

services.AddRazorComponents()
  .AddInteractiveServerComponents()
  .RegisterPersistentService<CustomUserService>(RenderMode.InteractiveAuto);
```

> \[NOTE] The preceding example persists `UserData` state when the service is used in component prerendering for both Interactive Server and Interactive WebAssembly rendering because `RenderMode.InteractiveAuto` is specified to [RegisterPersistentService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsrazorcomponentbuilderextensions.registerpersistentservice). However, circuit state persistence is only available for the **Interactive Server** render mode.

To handle distributed state persistence (and to act as the default state persistence mechanism when configured), assign a [`HybridCache`](../../performance/caching/overview?view=aspnetcore-10.0#hybridcache) (API: [HybridCache](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache)) to the app, which configures its own persistence period ([PersistedCircuitDistributedRetentionPeriod](/en-us/dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.persistedcircuitdistributedretentionperiod), eight hours by default). `HybridCache` is used because it provides a unified approach to distributed storage that doesn't require separate packages for each storage provider.

In the following example, a [HybridCache](/en-us/dotnet/api/microsoft.extensions.caching.hybrid.hybridcache) is implemented with the [Redis](https://redis.io/) storage provider:

```
services.AddHybridCache()
    .AddRedis("{CONNECTION STRING}");

services.AddRazorComponents()
    .AddInteractiveServerComponents();
```

In the preceding example, the `{CONNECTION STRING}` placeholder represents the Redis cache connection string, which should be provided using a secure approach, such as the [Secret Manager](../../security/app-secrets?view=aspnetcore-10.0#secret-manager) tool in the `Development` environment or [Azure Key Vault](/en-us/azure/key-vault/) with [Azure Managed Identities](/en-us/entra/identity/managed-identities-azure-resources/overview) for Azure-deployed apps in any environment.

For more information on the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute), see [ASP.NET Core Blazor prerendered state persistence](prerendered-state-persistence?view=aspnetcore-10.0).

## Pause and resume circuits

Pause and resume circuits to implement custom policies that improve the scalability of an app.

Pausing a circuit stores details about the circuit in client-side browser storage and evicts the circuit, which frees server resources. Resuming the circuit establishes a new circuit and initializes it using the persisted state.

From a JavaScript event handler:

* Call `Blazor.pauseCircuit()` to pause a circuit.
* Call `Blazor.resumeCircuit()` to resume a circuit.

The following example assumes that a circuit isn't required for an app that isn't visible:

```
window.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    Blazor.pauseCircuit();
  } else if (document.visibilityState === 'visible') {
    Blazor.resumeCircuit();
  }
});
```

## Automatic circuit pause on tab inactivity

Auto-pause can pause a circuit when the browser tab becomes hidden, freeing server memory and SignalR connections held by inactive users. It's an opt-in feature provided by the `Microsoft.AspNetCore.Components.Server.AutoPause` package. After adding a package reference, enable the feature by calling `AddAutoPause` when the app's root component is mapped:

```
app.MapRazorComponents<App>()
    .WithBrowserOptions(options => options.AddAutoPause(p => p.HiddenDelay = TimeSpan.FromSeconds(30)));
```

`AddAutoPause` configures an `AutoPauseBrowserOptions` instance with the following properties:

* `Enabled`: Whether auto-pause is enabled. Defaults to `true`, so calling `AddAutoPause` is what enables the feature.
* `HiddenDelay`: The delay after the tab becomes hidden before the circuit pauses. Defaults to two minutes (`TimeSpan.FromMinutes(2)`) and must be greater than `TimeSpan.Zero`.

After the tab is hidden for `HiddenDelay`, the circuit pauses. If the user returns before the delay elapses, the pause doesn't occur.

Note

Auto-pause triggers on the [Page Visibility API](https://developer.mozilla.org/docs/Web/API/Page_Visibility_API) `visibilitychange` event, whose meaning differs by platform:

* On desktop, the tab becomes hidden when the user switches tabs or minimizes the window. The pause timer runs reliably, and the circuit pauses gracefully after the delay.
* On mobile, the page also becomes hidden when the *whole app* is backgrounded (switching apps, returning to the home screen, or locking the screen), not just when switching browser tabs.

On mobile, the operating system suspends the page's JavaScript shortly after the app is backgrounded (within seconds on Android, up to about 30 seconds on iOS). If `HiddenDelay` is longer than that window, the pause timer never fires, and the circuit is dropped by the OS-initiated disconnect instead of pausing gracefully. The session is still preserved through the normal reconnection and [circuit state persistence](#circuit-state-persistence) path, but the client-side veto and deferral logic doesn't run. For this reason, graceful auto-pause isn't guaranteed and isn't a supported scenario on mobile when the app is backgrounded.

The package defers the pause while circuit-owned work is in progress (downloads, uploads, JS interop calls, Web Locks, Picture-in-Picture). It vetoes the pause entirely while focused text `<input>` elements with Blazor `@bind` bindings are edited or audio/video is playing.

For elements without Blazor bindings (for example, `<canvas>`, WebRTC connections, or custom elements), the app is responsible for handling state. Register a circuit handler with an `onCircuitPausing` callback in the [Blazor startup configuration](../fundamentals/startup?view=aspnetcore-10.0):

```
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      circuitHandlers: [
        {
          onCircuitPausing: async (signal /* AbortSignal */) => {
            // Example: save canvas state before the pause proceeds.
            const canvas = document.getElementById('drawing-canvas');
            if (canvas) {
              localStorage.setItem('canvasData', canvas.toDataURL());
            }

            // Example: close an active WebRTC connection gracefully.
            if (window.activePeerConnection && !signal.aborted) {
              window.activePeerConnection.close();
              await new Promise(resolve => {
                signal.addEventListener('abort', resolve);
                setTimeout(resolve, 100);
              });
            }
          }
        }
      ]
    }
  });
</script>
```

Note

The `<input type="file">` element can't have its value restored after pause/resume due to browser security restrictions. Using `[PersistentState]` on a property bound to a file `<input>` element causes an `InvalidStateError` that crashes the circuit. Instead, capture the file name in a separate property. Because the browser doesn't expose the file name through the native `change` event, read it with a small JS interop helper:

```
@inject IJSRuntime JS

<input id="persist-upload" type="file" @onchange="HandleFileSelected" />
<span>@SelectedFileName</span>

@code {
    [PersistentState]
    public string? SelectedFileName { get; set; }

    private async Task HandleFileSelected(ChangeEventArgs e)
    {
        SelectedFileName = await JS.InvokeAsync<string>("getFileName", "persist-upload");
    }
}
```

```
window.getFileName = (id) => document.getElementById(id)?.files?.[0]?.name ?? '';
```

## Server-triggered circuit pause

A server-side Blazor app that adopts the Interactive Server render mode can implement server-triggered circuit pause, which allows the app to gracefully pause client circuits, preserving client state for seamless reconnection.

This feature is useful in the following scenarios:

* Planned shutdowns and deployments.
* Instance draining.
* App maintenance windows.

`Circuit.RequestCircuitPauseAsync(CancellationToken)` is used to request that the connected client begin the graceful circuit-pause flow. The `CancellationToken` cancels the request before it is accepted by the framework. The method returns `true` if the request was accepted and the client was asked to begin pausing.

When a server-side Blazor application shuts down (for example, during deployment), connected clients lose their SignalR connection. The approach in this section:

* Detects shutdown before the server closes connections.
* Triggers a pause on connected circuits via `Microsoft.AspNetCore.Components.Server.Circuits.Circuit.RequestCircuitPauseAsync`.
* Preserves state using [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute) on component properties.

In the following example implementation, the following code files are placed in a `Shutdown` folder at the root of the app:

* `CircuitShutdownService.cs`: A singleton service that coordinates shutdown.
* `ShutdownCircuitHandler.cs`: A scoped service for each active circuit.
* `ShutdownCircuitOptions.cs`: Configuration options.

`Shutdown/ShutdownCircuitOptions.cs`:

```
namespace PauseResumeOnShutdown.Shutdown;

public class ShutdownCircuitOptions
{
    public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(10);
}
```

Using the following approach, the fact that the code sends the `RequestCircuitPauseAsync` asynchronously doesn't mean that upon returning the value that the client is already paused. It's only a request to pause the client, which the client can defer. That's why the code includes the [TaskCompletionSource](/en-us/dotnet/api/system.threading.tasks.taskcompletionsource) (`_shutdownTcs`), which is set when there aren't any circuits connected (all of them are successfully shut down). In case a client requests a deferral longer than the server allows, longer than `ShutdownTimeout`, the client doesn't persist state and experiences a normal connection loss. Other clients that don't defer the pause request have their connections re-established after the app goes back online with state persisted.

`Shutdown/CircuitShutdownService.cs`:

```
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.Options;

namespace PauseResumeOnShutdown.Shutdown;

public class CircuitShutdownService
{
    private readonly ConcurrentDictionary<string, Circuit> 
        _circuits = new();
    private readonly ShutdownCircuitOptions _options;
    private bool _isShuttingDown;
    private TaskCompletionSource _shutdownTcs = new();

    public CircuitShutdownService(IHostApplicationLifetime appLifetime, 
        IOptions<ShutdownCircuitOptions> options)
    {
        _options = options.Value;
        appLifetime.ApplicationStopping.Register(OnApplicationStopping);
    }

    private void OnApplicationStopping()
    {
        _isShuttingDown = true;

        if (_circuits.IsEmpty)
        {
            return;
        }

        var pauseTasks = _circuits.Values
            .Select(c => c.RequestCircuitPauseAsync().AsTask())
            .Append(_shutdownTcs.Task);

        Task.WhenAll(pauseTasks).Wait(_options.ShutdownTimeout);

        _shutdownTcs.Task.Wait(_options.ShutdownTimeout);
    }

    public void Register(string circuitId, Circuit circuit)
    {
        _circuits.TryAdd(circuitId, circuit);
    }

    public void Unregister(string circuitId)
    {
        _circuits.TryRemove(circuitId, out _);

        if (_isShuttingDown && _circuits.IsEmpty)
        {
            _shutdownTcs.TrySetResult();
        }
    }
}
```

`Shutdown/ShutdownCircuitHandler.cs`:

```
using Microsoft.AspNetCore.Components.Server.Circuits;

namespace PauseResumeOnShutdown.Shutdown;

public class ShutdownCircuitHandler(CircuitShutdownService shutdownService) 
    : CircuitHandler
{
    public override Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        shutdownService.Register(circuit.Id, circuit);

        return Task.CompletedTask;
    }

    public override Task OnConnectionDownAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        shutdownService.Unregister(circuit.Id);

        return Task.CompletedTask;
    }
}
```

`Program.cs`:

```
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
using PauseResumeOnShutdown.Components;
using PauseResumeOnShutdown.Shutdown;

var builder = WebApplication.CreateBuilder(args);

// Increase host shutdown timeout to allow time for pause operations
// Must be greater than `ShutdownTimeout` in `ShutdownCircuitOptions` 
// otherwise the host terminates connections before circuits finish 
// pausing
builder.Host.ConfigureHostOptions(options =>
    options.ShutdownTimeout = TimeSpan.FromSeconds(30));

builder.Services.Configure<ShutdownCircuitOptions>(options =>
    options.ShutdownTimeout = TimeSpan.FromSeconds(10));

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

// Register CircuitShutdownService as a singleton
builder.Services.AddSingleton<CircuitShutdownService>();

// Register ShutdownCircuitHandler as a scoped CircuitHandler
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Scoped<CircuitHandler, ShutdownCircuitHandler>());

var app = builder.Build();

// ... rest of pipeline
```

Optionally, to defer pause on the client until critical work completes (for example, an in-flight payment), configure the `onPauseRequested` callback in the [Blazor startup configuration](../fundamentals/startup?view=aspnetcore-10.0). Place the following after the [server-side Blazor script reference](../project-structure?view=aspnetcore-10.0#location-of-the-blazor-script):

```
<script> 
  Blazor.start({
    circuit: {
      onPauseRequested: async () => {
        // Perform any critical cleanup or wait for in-flight operations.
        // Return true to allow the pause or false to reject it.
        return true;
      }
    }
  });
</script>
```

Without the `onPauseRequested` callback, the client pauses immediately when the server requests it.

In a component, use the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute) to persist component state across pause/resume. In the following `Counter` component example, the current count (`CurrentCount`) is preserved across server restarts using the preceding approach:

```
@page "/counter"
@rendermode InteractiveServer

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [PersistentState]
    public int CurrentCount { get; set; }

    private void IncrementCount()
    {
        CurrentCount++;
    }
}
```

For more information on the [`[PersistentState]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.components.persistentstateattribute), see [ASP.NET Core Blazor prerendered state persistence](prerendered-state-persistence?view=aspnetcore-10.0).

\-->

## Persist state across circuits

Generally, maintain state across circuits where users are actively creating data, not simply reading data that already exists.

To preserve state across circuits, the app must persist the data to some other storage location than the server's memory. State persistence isn't automatic. You must take steps when developing the app to implement stateful data persistence.

Data persistence is typically only required for high-value state that users expended effort to create. In the following examples, persisting state either saves time or aids in commercial activities:

* Multi-step web forms: It's time-consuming for a user to re-enter data for several completed steps of a multi-step web form if their state is lost. A user loses state in this scenario if they navigate away from the form and return later.
* Shopping carts: Any commercially important component of an app that represents potential revenue can be maintained. A user who loses their state, and thus their shopping cart, may purchase fewer products or services when they return to the site later.

An app can only persist *app state*. UIs can't be persisted, such as component instances and their render trees. Components and render trees aren't generally serializable. To persist UI state, such as the expanded nodes of a tree view control, the app must use custom code to model the behavior of the UI state as serializable app state.

## Server-side storage

Data can be stored temporarily or permanently in server-side scenarios.

### Supported property types for temporary data and session

The `[SupplyParameterFromTempData]` and `[SupplyParameterFromSession]` attributes support the same stored-value types for component properties during static server-side rendering:

| Type            | Supported values                                                                                                                      |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Scalar          | `string`, `int`, `bool`, `Guid`, `DateTime`, and enums with an `int` underlying type                                                  |
| Nullable scalar | Nullable forms of the supported value types, such as `int?`                                                                           |
| Collection      | One-dimensional arrays (`T[]`), `List<T>`, `HashSet<T>`, and `Collection<T>`, where `T` is a supported scalar or nullable scalar type |
| Dictionary      | `Dictionary<string, T>`, where `T` is a supported scalar or nullable scalar type                                                      |
| Object array    | `object[]`, whose elements are serialized individually and must have supported types                                                  |

Null values are supported. An empty collection remains distinct from a null collection after a round trip. A nullable property explicitly set to null and one never set both read back as null; this alone doesn't establish whether a storage key was present.

Arbitrary JSON-serializable types aren't supported. For example, a user-defined class, a list of user-defined classes, or an enum backed by `byte` isn't supported. The supported types are determined by the stored-data serializer, not by whether [System.Text.Json](/en-us/dotnet/api/system.text.json) can serialize the type.

### Temporary data persistence

To persist temporary data between HTTP requests during static server-side rendering (static SSR), Blazor supports `TempData`. `TempData` is ideal for scenarios such as flash messages after form submissions, passing data during redirects (POST-Redirect-GET pattern), and one-time notifications.

Important

This feature is only available during static server-side rendering (static SSR). During interactive SSR and interactive client-side rendering (CSR), the `TempData` value isn't supplied, and the property retains its default value. `TempData`:

* Is available when [AddRazorComponents](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.razorcomponentsservicecollectionextensions.addrazorcomponents) is called in the app's `Program` file.
* Is provided as a cascading value with the [`[CascadingParameter]` attribute](../components/cascading-values-and-parameters?view=aspnetcore-10.0#cascadingparameter-attribute) or the `[SupplyParameterFromTempData]` parameter attribute.
* Is accessed by key (string).
* Supports the [stored-value types listed earlier](#supported-property-types-for-temporary-data-and-session).
* Stores `object?` values, requiring runtime casting (example: `var message = TempData["Message"] as string`). IntelliSense and type checking aren't supported.
* Uses case-insensitive keys, so `TempData["message"]` and `TempData["Message"]` retrieve the same value.

```
[CascadingParameter]
public ITempData? TempData { get; set; }
```

When supplied to a parameter for simple read/write of a single value, use the `[SupplyParameterFromTempData]` attribute without or with a key (string):

```
[SupplyParameterFromTempData]
public string? Message { get; set; }

[SupplyParameterFromTempData(Name = "flash_message")]
public string? FlashMessage { get; set; }
```

A key (string) is useful to distinguish multiple parameters because properties can't share a temporary data value.

The `ITempData` interface provides the following methods for controlling value lifecycle:

* `Get`: Gets the value associated with the specified key and schedules the data for deletion.
* `Peek`: Returns the value associated with the specified key without marking the data for deletion.
* `Keep`: Marks all keys in the dictionary for retention. Values are available on the next request.
* `Keep(string)`: Marks a specified key (string) for retention. The value is available on the next request.

Data stored in `TempData` is automatically removed after the data is read unless `Keep`/`Keep(string)` is called or the data is accessed via `Peek`.

The default cookie-based provider uses [Data Protection](../../security/data-protection/introduction?view=aspnetcore-10.0) for encryption. If the cookie is tampered with or can't be decrypted, `TempData` has no value for that request. The provider removes the unreadable cookie in the response rather than displaying an error page, so the next request starts without it. In a multi-server deployment, configure [Data Protection](../../security/data-protection/configuration/overview?view=aspnetcore-10.0) so instances can read each other's cookies. Normal key rotation doesn't make existing cookies unreadable when the previous keys are retained.

To change the Blazor `TempData` cookie settings in the following table, configure `TempDataCookie` when calling `AddRazorComponents` in the app's `Program` file.

| Parameter                                                                                                     | API            | Notes                                                                                                                                                                                                                                             |
| ------------------------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Name                                                                                                          | `Name`         | The default value is `.AspNetCore.Components.TempData`.                                                                                                                                                                                           |
| [HTTP Only](https://developer.mozilla.org/docs/Web/Security/Practical_implementation_guides/Cookies#httponly) | `HttpOnly`     | The default value is `true`.                                                                                                                                                                                                                      |
| [SameSite value](https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value)       | `SameSite`     | The default value is [SameSiteMode.Lax](/en-us/dotnet/api/microsoft.aspnetcore.http.samesitemode#microsoft-aspnetcore-http-samesitemode-lax).                                                                                                     |
| [Secure policy](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Set-Cookie#secure)              | `SecurePolicy` | The default value is [CookieSecurePolicy.SameAsRequest](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy#microsoft-aspnetcore-http-cookiesecurepolicy-sameasrequest), which marks the cookie as secure when the request uses HTTPS. |

Example (sets default values):

```
builder.Services.AddRazorComponents(options =>
{
    options.TempDataCookie.Name = ".AspNetCore.Components.TempData";
    options.TempDataCookie.HttpOnly = true;
    options.TempDataCookie.SameSite = SameSiteMode.Lax;
    options.TempDataCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
```

Note

Unsupported non-null `TempData` values fail when the response is persisted, which can occur after the component has rendered. Blazor WebAssembly and interactive server rendering aren't supported.

Browsers enforce a 4 KB cookie size limit. `TempData` automatically uses [ChunkingCookieManager](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.chunkingcookiemanager) to split cookies across multiple cookie headers, but developers storing a large amount of data must switch to session storage, which introduces session affinity requirements.

In the following example, a form displays a message that's retained in `TempData` after the form is submitted (a new request).

`Pages/TempDataExample1.razor`:

```
@page "/tempdata-example-1"
@inject NavigationManager NavigationManager

<p>@message</p>

<form method="post" @formname="SetMessage" @onsubmit="Submit">
    <AntiforgeryToken />
    <button type="submit">Submit</button>
</form>

@code {
    [CascadingParameter]
    public ITempData? TempData { get; set; }

    private string? message;

    protected override void OnInitialized()
    {
        // Get removes the value after reading (one-time use)
        message = TempData?.Get("Message") as string ?? "No message";
    }

    private void Submit()
    {
        TempData!["Message"] = "Form submitted successfully!";
        NavigationManager.NavigateTo("/tempdata-example-1", forceLoad: true);
    }
}
```

Reading without deleting (`Peek`):

```
protected override void OnInitialized()
{
    var notification = TempData?.Peek("Message") as string;
}
```

Keep a specific value for another request (`Keep(string)`):

```
protected override void OnInitialized()
{
    var message = TempData?.Get("Message") as string;
    TempData?.Keep("Message");
}
```

Keep all values for another request (`Keep`):

```
protected override void OnInitialized()
{
    TempData?.Keep();
}
```

Similar to the preceding example but when only simple read/write of a single value is required, the following example uses the `[SupplyParameterFromTempData]` attribute.

`Pages/TempDataExample2.razor`:

```
@page "/tempdata-example-2"
@inject NavigationManager NavigationManager

<p>@Message</p>

<form method="post" @formname="SetMessage" @onsubmit="Submit">
    <AntiforgeryToken />
    <button type="submit">Submit</button>
</form>

@code {
    [SupplyParameterFromTempData]
    public string? Message { get; set; }

    private void Submit()
    {
        Message = "Form submitted successfully!";
        NavigationManager.NavigateTo("/tempdata-example-2", forceLoad: true);
    }
}
```

#### Streaming server-side rendering

During streaming server-side rendering (streaming SSR), cookie-backed `TempData` values can't be saved after the response starts because the provider must write a cookie to the response headers. Setting a value before an `await` doesn't avoid this limitation if the response is flushed before `TempData` is persisted. To persist values during streaming SSR, use the session-storage provider.

Configure session services and middleware as described in [Session data persistence](#session-data-persistence). A new session needs its session cookie issued before the first response flush. After the session is established, session-backed values can be persisted during streaming SSR.

### Session data persistence

Session data persistence reads and writes cookie-based HTTP session values during static server-side rendering (static SSR), which is useful for scenarios such as shopping cart IDs or multi-step form progress. Unlike [temporary data persistence (`ITempData`)](#temporary-data-persistence), session values aren't cleared after reading. Values persist across multiple requests for the session lifetime.

Important

This feature is only available during static server-side rendering (static SSR). During interactive SSR and interactive client-side rendering (CSR), the session value isn't supplied, and the property retains its default value.

Session storage:

* Requires explicit session state configuration.
* Has no practical size limits (within session constraints).
* Serializes values with [System.Text.Json](/en-us/dotnet/api/system.text.json) with [System.Text.Json.JsonSerializerDefaults](/en-us/dotnet/api/system.text.json.jsonserializerdefaults).
* Requires session affinity (sticky sessions) in load-balanced environments. Without it, users may lose data. For more information, see [ASP.NET Core Blazor SignalR guidance](../fundamentals/signalr?view=aspnetcore-10.0#use-session-affinity-sticky-sessions-for-server-side-web-farm-hosting).

For streaming SSR, establish a new session before the first response flush. Place a `[SupplyParameterFromSession]` parameter on a component rendered before that flush or write to the session directly before an `await` that triggers it. If the first session-backed component is rendered only after streaming starts, the session cookie can't be issued for a new session and its values won't persist to the next request.

When supplied to a parameter, use the `[SupplyParameterFromSession]` attribute without or with a key (string):

```
[SupplyParameterFromSession]
public string? Message { get; set; }

[SupplyParameterFromSession(Name = "flash_message")]
public string? FlashMessage { get; set; }
```

A key (string) is useful to distinguish multiple parameters because properties can't share a session value.

The [supported property types](#supported-property-types-for-temporary-data-and-session) are the same as for `[SupplyParameterFromTempData]`. For an unsupported concrete property type, `[SupplyParameterFromSession]` throws when the component subscribes to the session value. The exception identifies the property, component, attribute, and property type. This differs from the later `TempData` persistence failure, which identifies the unsupported value type.

Call `AddSession` on the service collection in the app's `Program` file passing `SessionOptions` to change the cookie's parameters.

| Parameter                                                                                                     | API            | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------------------------------------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Name                                                                                                          | `Name`         | The default value is [SessionDefaults.CookieName](/en-us/dotnet/api/microsoft.aspnetcore.session.sessiondefaults.cookiename) (`.AspNetCore.Session`).                                                                                                                                                                                                                                                                                                                                                                |
| Path                                                                                                          | `Path`         | The default value is [SessionDefaults.CookiePath](/en-us/dotnet/api/microsoft.aspnetcore.session.sessiondefaults.cookiepath) (`/`).                                                                                                                                                                                                                                                                                                                                                                                  |
| [HTTP Only](https://developer.mozilla.org/docs/Web/Security/Practical_implementation_guides/Cookies#httponly) | `HttpOnly`     | The default value is `true`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| [SameSite value](https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value)       | `SameSite`     | The default value is [SameSiteMode.Lax](/en-us/dotnet/api/microsoft.aspnetcore.http.samesitemode#microsoft-aspnetcore-http-samesitemode-lax).                                                                                                                                                                                                                                                                                                                                                                        |
| [Secure policy](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Set-Cookie#secure)              | `SecurePolicy` | Some browsers don't allow insecure endpoints to set cookies with a 'secure' flag or overwrite cookies whose 'secure' flag is set. Since mixing secure and insecure endpoints is a common scenario in apps, the framework relaxes the restriction on secure policy on some cookies by setting them to 'None'. Cookies related to authentication or authorization use a stronger policy than 'None'. The default value is [`CookieSecurePolicy.None`](/en-us/dotnet/api/microsoft.aspnetcore.http.cookiesecurepolicy). |
| Is Essential                                                                                                  | `IsEssential`  | Session is considered non-essential, as it's designed for ephemeral data. The default value is `false`.                                                                                                                                                                                                                                                                                                                                                                                                              |
| Idle Timeout                                                                                                  | `IdleTimeout`  | Indicates how long the session can be idle before its contents are abandoned (default value: 20 minutes). Each session access resets the timeout. This setting only applies to the content of the session, not the cookie.                                                                                                                                                                                                                                                                                           |
| IO Timeout                                                                                                    | `IOTimeout`    | The maximum amount of time allowed to load a session from the store or to commit it back to the store (default value: 1 minute). This may only apply to asynchronous operations. The timeout is disabled using [Timeout.InfiniteTimeSpan](/en-us/dotnet/api/system.threading.timeout.infinitetimespan).                                                                                                                                                                                                              |

After configuring session services with `AddSession`, call `UseSession` in the request processing pipeline. The following example demonstrates default values:

```
builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.Name = ".AspNetCore.Session";
    options.Path = "/";
    options.HttpOnly = true;
    options.SecurePolicy = CookieSecurePolicy.None;
    options.SameSite = SameSiteMode.Lax;
    options.IsEssential = false;
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.IOTimeout =  TimeSpan.FromMinutes(1);
});

builder.Services.AddRazorComponents();

var app = builder.Build();

app.UseSession();
```

`Pages/Checkout.razor`:

```
@page "/checkout"

<p>Current step: @CurrentStep</p>

<EditForm Model="Input" FormName="checkout" OnSubmit="NextStep">
    <button type="submit">Next</button>
</EditForm>

@code {
    [SupplyParameterFromSession(Name = "checkout_step")]
    public int CurrentStep { get; set; }

    private object Input { get; } = new();

    private void NextStep() => CurrentStep++;
}
```

### Permanent data persistence

For permanent data persistence that spans multiple users and devices, the app can use server-side storage. Options include:

* Blob storage
* Key-value storage
* Relational database
* Table storage

After data is saved, the user's state is retained and available in any new circuit.

For more information on Azure data storage options, see the following:

* [Azure Databases](https://azure.microsoft.com/product-categories/databases/)
* [Azure Storage Documentation](/en-us/azure/storage/)

## Browser storage

For more information, see [ASP.NET Core Blazor state management using protected browser storage](protected-browser-storage?view=aspnetcore-10.0).

## Related content

* [State management using the URL](./?view=aspnetcore-10.0#url)
* [In-memory state container service](./?view=aspnetcore-10.0#in-memory-state-container-service)
* [Cascading values and parameters](./?view=aspnetcore-10.0#cascading-values-and-parameters)
* [Managing state via an external server API](../call-web-api?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-29

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0036
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0036.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0036: Validatable property or its type on an endpoint parameter type is not accessible

|              | Value   |
| ------------ | ------- |
| **Rule ID**  | ASP0036 |
| **Category** | Usage   |
| **Severity** | Warning |

## Cause

A property on a Minimal API endpoint parameter type declares validation, but the property, its getter, or its type isn't accessible to the validation source generator.

## Rule description

The validation source generator can only generate validation for public properties with public getters and accessible property types. An inaccessible property that has validation attributes, implements `IValidatableObject`, or contains a type that declares validation is silently skipped.

The following code produces this diagnostic because `Child` isn't public:

```
app.MapPost("/orders", (Order order) => Results.Ok(order));

public class Order
{
    internal Customer Child { get; } = new();

    public class Customer
    {
        [Required]
        public string? Name { get; set; }
    }
}
```

## How to fix violations

Make the property and its getter public. Also make the property type and each of its containing types public or internal and don't declare them as file-local:

```
public class Order
{
    public Customer Child { get; } = new();

    public class Customer
    {
        [Required]
        public string? Name { get; set; }
    }
}
```

If the property shouldn't be validated, apply [SkipValidationAttribute](/en-us/dotnet/api/microsoft.extensions.validation.skipvalidationattribute) to it. To opt out the whole endpoint, disable validation for the endpoint.

For more information, see [Validation in ASP.NET Core](../fundamentals/validation?view=aspnetcore-12.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-26

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Overview of ASP.NET Core authentication

By [Mike Rousos](https://github.com/mjrousos)

Authentication is the process of determining a user's identity. [Authorization](../authorization/introduction?view=aspnetcore-10.0) is the process of determining whether a user has access to a resource. In ASP.NET Core, authentication is handled by the authentication service, [IAuthenticationService](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationservice), which is used by authentication [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0). The authentication service uses registered authentication handlers to complete authentication-related actions. Examples of authentication-related actions include:

* Authenticating a user.
* Responding when an unauthenticated user tries to access a restricted resource.

Configuring authentication doesn't automatically restrict access to endpoints. To [require authenticated users by default](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication) in a server-side app, configure a fallback authorization policy.

The registered authentication handlers and their configuration options are called "schemes".

Authentication schemes are specified by registering authentication services in `Program.cs`:

* By calling a scheme-specific extension method after a call to [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), such as [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) or [AddCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie). These extension methods use [AuthenticationBuilder.AddScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder.addscheme) to register schemes with appropriate settings.
* Less commonly, by calling `AuthenticationBuilder.AddScheme` directly.

For example, the following code registers authentication services and handlers for cookie and JWT bearer authentication schemes:

```
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
        options => builder.Configuration.Bind("JwtSettings", options))
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
        options => builder.Configuration.Bind("CookieSettings", options));
```

The `AddAuthentication` parameter [JwtBearerDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerdefaults.authenticationscheme#microsoft-aspnetcore-authentication-jwtbearer-jwtbearerdefaults-authenticationscheme) is the name of the scheme to use by default when a specific scheme isn't requested.

If multiple schemes are used, authorization policies (or authorization attributes) can [specify the authentication scheme (or schemes)](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0) they depend on to authenticate the user. In the example above, the cookie authentication scheme could be used by specifying its name ([CookieAuthenticationDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) by default, though a different name could be provided when calling `AddCookie`).

In some cases, the call to `AddAuthentication` is automatically made by other extension methods. For example, when using [ASP.NET Core Identity](identity?view=aspnetcore-10.0), `AddAuthentication` is called internally.

The Authentication middleware is added in `Program.cs` by calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication). Calling `UseAuthentication` registers the middleware that uses the previously registered authentication schemes. Call `UseAuthentication` before any middleware that depends on users being authenticated.

## Authentication concepts

Authentication is responsible for providing the [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) for authorization to make permission decisions against. There are multiple authentication scheme approaches to select which authentication handler is responsible for generating the correct set of claims:

* [Authentication scheme](#authentication-scheme)
* The default authentication scheme, discussed in the next two sections.
* Directly set [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user).

When there is only a single authentication scheme registered, it becomes the default scheme. If multiple schemes are registered and the default scheme isn't specified, a scheme must be specified in the authorize attribute, otherwise, the following error is thrown:

> InvalidOperationException: No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action\<AuthenticationOptions> configureOptions).

### `DefaultScheme`

When there is only a single authentication scheme registered, the single authentication scheme:

* Is automatically used as the [DefaultScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationoptions.defaultscheme#microsoft-aspnetcore-authentication-authenticationoptions-defaultscheme).
* Eliminates the need to specify the `DefaultScheme` in [AddAuthentication(IServiceCollection)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication#microsoft-extensions-dependencyinjection-authenticationservicecollectionextensions-addauthentication\(microsoft-extensions-dependencyinjection-iservicecollection\)) or [AddAuthenticationCore(IServiceCollection)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationcoreservicecollectionextensions.addauthenticationcore#microsoft-extensions-dependencyinjection-authenticationcoreservicecollectionextensions-addauthenticationcore\(microsoft-extensions-dependencyinjection-iservicecollection\)).

To disable automatically using the single authentication scheme as the `DefaultScheme`, call `AppContext.SetSwitch("Microsoft.AspNetCore.Authentication.SuppressAutoDefaultScheme")`.

### Authentication scheme

The [authentication scheme](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0) can select which authentication handler is responsible for generating the correct set of claims. For more information, see [Authorize with a specific scheme](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0).

An authentication scheme is a name that corresponds to:

* An authentication handler.
* Options for configuring that specific instance of the handler.

Schemes are useful as a mechanism for referring to the authentication, challenge, and forbid behaviors of the associated handler. For example, an authorization policy can use scheme names to specify which authentication scheme (or schemes) should be used to authenticate the user. When configuring authentication, it's common to specify the default authentication scheme. The default scheme is used unless a resource requests a specific scheme. It's also possible to:

* Specify different default schemes to use for authenticate, challenge, and forbid actions.
* Combine multiple schemes into one using [policy schemes](policyschemes?view=aspnetcore-10.0).

### Authentication handler

An authentication handler:

* Is a type that implements the behavior of a scheme.
* Is derived from [IAuthenticationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationhandler) or [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1).
* Has the primary responsibility to authenticate users.

Based on the authentication scheme's configuration and the incoming request context, authentication handlers:

* Construct [AuthenticationTicket](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationticket) objects representing the user's identity if authentication is successful.

* Return 'no result' or 'failure' if authentication is unsuccessful.

* Have methods for challenge and forbid actions for when users attempt to access resources:

  * They're unauthorized to access (forbid).
  * When they're unauthenticated (challenge).

### `RemoteAuthenticationHandler<TOptions>` vs `AuthenticationHandler<TOptions>`

[RemoteAuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationhandler-1) is the class for authentication that requires a remote authentication step. When the remote authentication step is finished, the handler calls back to the `CallbackPath` set by the handler. The handler finishes the authentication step using the information passed to the [HandleRemoteAuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.twitter.twitterhandler.handleremoteauthenticateasync) callback path. [OAuth 2.0](https://oauth.net/2/) and [OIDC](https://openid.net/developers/how-connect-works/) both use this pattern. JWT and cookies don't since they can directly use the bearer header and cookie to authenticate. The remotely hosted provider in this case:

* Is the authentication provider.
* Examples include [Facebook](social/facebook-logins?view=aspnetcore-10.0), [Twitter](social/twitter-logins?view=aspnetcore-10.0), [Google](social/google-logins?view=aspnetcore-10.0), [Microsoft](social/microsoft-logins?view=aspnetcore-10.0), and any other OIDC provider that handles authenticating users using the handlers mechanism.

### Authenticate

An authentication scheme's authenticate action is responsible for constructing the user's identity based on request context. It returns an [AuthenticateResult](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticateresult) indicating whether authentication was successful and, if so, the user's identity in an authentication ticket. See [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.authenticateasync). Authenticate examples include:

* A cookie authentication scheme constructing the user's identity from cookies.
* A JWT bearer scheme deserializing and validating a JWT bearer token to construct the user's identity.

### Challenge

An authentication challenge is invoked by Authorization when an unauthenticated user requests an endpoint that requires authentication. An authentication challenge is issued, for example, when an anonymous user requests a restricted resource or follows a login link. Authorization invokes a challenge using the specified authentication schemes, or the default if none is specified. See [ChallengeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.challengeasync). Authentication challenge examples include:

* A cookie authentication scheme redirecting the user to a login page.
* A JWT bearer scheme returning a 401 result with a `www-authenticate: bearer` header.

A challenge action should let the user know what authentication mechanism to use to access the requested resource.

### Forbid

An authentication scheme's forbid action is called by Authorization when an authenticated user attempts to access a resource they're not permitted to access. See [ForbidAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.forbidasync). Authentication forbid examples include:

* A cookie authentication scheme redirecting the user to a page indicating access was forbidden.
* A JWT bearer scheme returning a 403 result.
* A custom authentication scheme redirecting to a page where the user can request access to the resource.

A forbid action can let the user know:

* They're authenticated.
* They're not permitted to access the requested resource.

See the following links for differences between challenge and forbid:

* [Resource-based authorization](../authorization/resource-based?view=aspnetcore-10.0).
* [Differences between challenge and forbid](../authorization/secure-data?view=aspnetcore-10.0#challenge).

## Authentication providers per tenant

ASP.NET Core doesn't have a built-in solution for multi-tenant authentication. While it's possible for customers to write one using the built-in features, we recommend customers consider [Orchard Core](https://www.orchardcore.net/), [ABP Framework](https://abp.io/), or [Finbuckle.MultiTenant](https://www.finbuckle.com/multitenant) for multi-tenant authentication.

Orchard Core is:

* An open-source, modular, and multi-tenant app framework built with ASP.NET Core.
* A content management system (CMS) built on top of that app framework.

See the [Orchard Core](https://github.com/OrchardCMS/OrchardCore) source for an example of authentication providers per tenant.

[ABP Framework](https://abp.io/) supports various architectural patterns including modularity, microservices, domain driven design, and multi-tenancy. See [ABP Framework source on GitHub](https://github.com/abpframework/abp).

Finbuckle.MultiTenant:

* Open source
* Provides tenant resolution
* Lightweight
* Provides data isolation
* Configure app behavior uniquely for each tenant

## Additional resources

* [Authorize with a specific scheme in ASP.NET Core](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0)
* [Policy schemes in ASP.NET Core](policyschemes?view=aspnetcore-10.0)
* [Create an ASP.NET Core app with user data protected by authorization](../authorization/secure-data?view=aspnetcore-10.0)
* [Globally require authenticated users](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication)
* [GitHub issue on using multiple authentication schemes](https://github.com/dotnet/aspnetcore/issues/26002)

By [Mike Rousos](https://github.com/mjrousos)

Authentication is the process of determining a user's identity. [Authorization](../authorization/introduction?view=aspnetcore-10.0) is the process of determining whether a user has access to a resource. In ASP.NET Core, authentication is handled by the authentication service, [IAuthenticationService](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationservice), which is used by authentication [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0). The authentication service uses registered authentication handlers to complete authentication-related actions. Examples of authentication-related actions include:

* Authenticating a user.
* Responding when an unauthenticated user tries to access a restricted resource.

Configuring authentication doesn't automatically restrict access to endpoints. To [require authenticated users by default](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication) in a server-side app, configure a fallback authorization policy.

The registered authentication handlers and their configuration options are called "schemes".

Authentication schemes are specified by registering authentication services in `Program.cs`:

* By calling a scheme-specific extension method after a call to [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication), such as [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) or [AddCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie). These extension methods use [AuthenticationBuilder.AddScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder.addscheme) to register schemes with appropriate settings.
* Less commonly, by calling `AuthenticationBuilder.AddScheme` directly.

For example, the following code registers authentication services and handlers for cookie and JWT bearer authentication schemes:

```
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
        options => builder.Configuration.Bind("JwtSettings", options))
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
        options => builder.Configuration.Bind("CookieSettings", options));
```

The `AddAuthentication` parameter [JwtBearerDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerdefaults.authenticationscheme#microsoft-aspnetcore-authentication-jwtbearer-jwtbearerdefaults-authenticationscheme) is the name of the scheme to use by default when a specific scheme isn't requested.

If multiple schemes are used, authorization policies (or authorization attributes) can [specify the authentication scheme (or schemes)](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0) they depend on to authenticate the user. In the example above, the cookie authentication scheme could be used by specifying its name ([CookieAuthenticationDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) by default, though a different name could be provided when calling `AddCookie`).

In some cases, the call to `AddAuthentication` is automatically made by other extension methods. For example, when using [ASP.NET Core Identity](identity?view=aspnetcore-10.0), `AddAuthentication` is called internally.

The Authentication middleware is added in `Program.cs` by calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication). Calling `UseAuthentication` registers the middleware that uses the previously registered authentication schemes. Call `UseAuthentication` before any middleware that depends on users being authenticated.

## Authentication concepts

Authentication is responsible for providing the [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) for authorization to make permission decisions against. There are multiple authentication scheme approaches to select which authentication handler is responsible for generating the correct set of claims:

* [Authentication scheme](#authentication-scheme)
* The default authentication scheme, discussed in the next section.
* Directly set [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user).

There's no automatic probing of schemes. If the default scheme isn't specified, the scheme must be specified in the authorize attribute, otherwise, the following error is thrown:

> InvalidOperationException: No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action\<AuthenticationOptions> configureOptions).

### Authentication scheme

The [authentication scheme](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0) can select which authentication handler is responsible for generating the correct set of claims. For more information, see [Authorize with a specific scheme](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0).

An authentication scheme is a name that corresponds to:

* An authentication handler.
* Options for configuring that specific instance of the handler.

Schemes are useful as a mechanism for referring to the authentication, challenge, and forbid behaviors of the associated handler. For example, an authorization policy can use scheme names to specify which authentication scheme (or schemes) should be used to authenticate the user. When configuring authentication, it's common to specify the default authentication scheme. The default scheme is used unless a resource requests a specific scheme. It's also possible to:

* Specify different default schemes to use for authenticate, challenge, and forbid actions.
* Combine multiple schemes into one using [policy schemes](policyschemes?view=aspnetcore-10.0).

### Authentication handler

An authentication handler:

* Is a type that implements the behavior of a scheme.
* Is derived from [IAuthenticationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationhandler) or [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1).
* Has the primary responsibility to authenticate users.

Based on the authentication scheme's configuration and the incoming request context, authentication handlers:

* Construct [AuthenticationTicket](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationticket) objects representing the user's identity if authentication is successful.

* Return 'no result' or 'failure' if authentication is unsuccessful.

* Have methods for challenge and forbid actions for when users attempt to access resources:

  * They're unauthorized to access (forbid).
  * When they're unauthenticated (challenge).

### `RemoteAuthenticationHandler<TOptions>` vs `AuthenticationHandler<TOptions>`

[RemoteAuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationhandler-1) is the class for authentication that requires a remote authentication step. When the remote authentication step is finished, the handler calls back to the `CallbackPath` set by the handler. The handler finishes the authentication step using the information passed to the [HandleRemoteAuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.twitter.twitterhandler.handleremoteauthenticateasync) callback path. [OAuth 2.0](https://oauth.net/2/) and [OIDC](https://openid.net/developers/how-connect-works/) both use this pattern. JWT and cookies don't since they can directly use the bearer header and cookie to authenticate. The remotely hosted provider in this case:

* Is the authentication provider.
* Examples include [Facebook](social/facebook-logins?view=aspnetcore-10.0), [Twitter](social/twitter-logins?view=aspnetcore-10.0), [Google](social/google-logins?view=aspnetcore-10.0), [Microsoft](social/microsoft-logins?view=aspnetcore-10.0), and any other OIDC provider that handles authenticating users using the handlers mechanism.

### Authenticate

An authentication scheme's authenticate action is responsible for constructing the user's identity based on request context. It returns an [AuthenticateResult](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticateresult) indicating whether authentication was successful and, if so, the user's identity in an authentication ticket. See [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.authenticateasync). Authenticate examples include:

* A cookie authentication scheme constructing the user's identity from cookies.
* A JWT bearer scheme deserializing and validating a JWT bearer token to construct the user's identity.

### Challenge

An authentication challenge is invoked by Authorization when an unauthenticated user requests an endpoint that requires authentication. An authentication challenge is issued, for example, when an anonymous user requests a restricted resource or follows a login link. Authorization invokes a challenge using the specified authentication schemes, or the default if none is specified. See [ChallengeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.challengeasync). Authentication challenge examples include:

* A cookie authentication scheme redirecting the user to a login page.
* A JWT bearer scheme returning a 401 result with a `www-authenticate: bearer` header.

A challenge action should let the user know what authentication mechanism to use to access the requested resource.

### Forbid

An authentication scheme's forbid action is called by Authorization when an authenticated user attempts to access a resource they're not permitted to access. See [ForbidAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.forbidasync). Authentication forbid examples include:

* A cookie authentication scheme redirecting the user to a page indicating access was forbidden.
* A JWT bearer scheme returning a 403 result.
* A custom authentication scheme redirecting to a page where the user can request access to the resource.

A forbid action can let the user know:

* They're authenticated.
* They're not permitted to access the requested resource.

See the following links for differences between challenge and forbid:

* [Challenge and forbid with an operational resource handler](../../razor-pages/security/authorization/resource-based?view=aspnetcore-10.0#challenge-and-forbid-with-an-operational-resource-handler).
* [Differences between challenge and forbid](../authorization/secure-data?view=aspnetcore-10.0#challenge).

## Authentication providers per tenant

ASP.NET Core doesn't have a built-in solution for multi-tenant authentication. While it's possible for customers to write one using the built-in features, we recommend customers consider [Orchard Core](https://www.orchardcore.net/) or [ABP Framework](https://abp.io/) for multi-tenant authentication.

Orchard Core is:

* An open-source, modular, and multi-tenant app framework built with ASP.NET Core.
* A content management system (CMS) built on top of that app framework.

See the [Orchard Core](https://github.com/OrchardCMS/OrchardCore) source for an example of authentication providers per tenant.

[ABP Framework](https://abp.io/) supports various architectural patterns including modularity, microservices, domain driven design, and multi-tenancy. See [ABP Framework source on GitHub](https://github.com/abpframework/abp).

## Additional resources

* [Authorize with a specific scheme in ASP.NET Core](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0)
* [Policy schemes in ASP.NET Core](policyschemes?view=aspnetcore-10.0)
* [Create an ASP.NET Core app with user data protected by authorization](../authorization/secure-data?view=aspnetcore-10.0)
* [Globally require authenticated users](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication)
* [GitHub issue on using multiple authentication schemes](https://github.com/dotnet/aspnetcore/issues/26002)

By [Mike Rousos](https://github.com/mjrousos)

Authentication is the process of determining a user's identity. [Authorization](../authorization/introduction?view=aspnetcore-10.0) is the process of determining whether a user has access to a resource. In ASP.NET Core, authentication is handled by the authentication service, [IAuthenticationService](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationservice), which is used by authentication [middleware](../../fundamentals/middleware/?view=aspnetcore-10.0). The authentication service uses registered authentication handlers to complete authentication-related actions. Examples of authentication-related actions include:

* Authenticating a user.
* Responding when an unauthenticated user tries to access a restricted resource.

Configuring authentication doesn't automatically restrict access to endpoints. To [require authenticated users by default](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication) in a server-side app, configure a fallback authorization policy.

The registered authentication handlers and their configuration options are called "schemes".

Authentication schemes are specified by registering authentication services in `Startup.ConfigureServices`:

* By calling a scheme-specific extension method after a call to [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication) (such as [AddJwtBearer](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer) or [AddCookie](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie), for example). These extension methods use [AuthenticationBuilder.AddScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationbuilder.addscheme) to register schemes with appropriate settings.
* Less commonly, by calling `AuthenticationBuilder.AddScheme` directly.

For example, the following code registers authentication services and handlers for cookie and JWT bearer authentication schemes:

```
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
        options => Configuration.Bind("JwtSettings", options))
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
        options => Configuration.Bind("CookieSettings", options));
```

The `AddAuthentication` parameter [JwtBearerDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerdefaults.authenticationscheme#microsoft-aspnetcore-authentication-jwtbearer-jwtbearerdefaults-authenticationscheme) is the name of the scheme to use by default when a specific scheme isn't requested.

If multiple schemes are used, authorization policies (or authorization attributes) can [specify the authentication scheme (or schemes)](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0) they depend on to authenticate the user. In the example above, the cookie authentication scheme could be used by specifying its name ([CookieAuthenticationDefaults.AuthenticationScheme](/en-us/dotnet/api/microsoft.aspnetcore.authentication.cookies.cookieauthenticationdefaults.authenticationscheme#microsoft-aspnetcore-authentication-cookies-cookieauthenticationdefaults-authenticationscheme) by default, though a different name could be provided when calling `AddCookie`).

In some cases, the call to `AddAuthentication` is automatically made by other extension methods. For example, when using [ASP.NET Core Identity](identity?view=aspnetcore-10.0), `AddAuthentication` is called internally.

The Authentication middleware is added in `Startup.Configure` by calling [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication). Calling `UseAuthentication` registers the middleware that uses the previously registered authentication schemes. Call `UseAuthentication` before any middleware that depends on users being authenticated. When using endpoint routing, the call to `UseAuthentication` must go:

* After [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting), so that route information is available for authentication decisions.
* Before [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints), so that users are authenticated before accessing the endpoints.

## Authentication concepts

Authentication is responsible for providing the [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) for authorization to make permission decisions against. There are multiple authentication scheme approaches to select which authentication handler is responsible for generating the correct set of claims:

* [Authentication scheme](#authentication-scheme)
* The default authentication scheme, discussed in the next section.
* Directly set [HttpContext.User](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.user#microsoft-aspnetcore-http-httpcontext-user).

There's no automatic probing of schemes. If the default scheme isn't specified, the scheme must be specified in the authorize attribute, otherwise, the following error is thrown:

> InvalidOperationException: No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action\<AuthenticationOptions> configureOptions).

### Authentication scheme

The [authentication scheme](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0) can select which authentication handler is responsible for generating the correct set of claims. For more information, see [Authorize with a specific scheme](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0).

An authentication scheme is a name that corresponds to:

* An authentication handler.
* Options for configuring that specific instance of the handler.

Schemes are useful as a mechanism for referring to the authentication, challenge, and forbid behaviors of the associated handler. For example, an authorization policy can use scheme names to specify which authentication scheme (or schemes) should be used to authenticate the user. When configuring authentication, it's common to specify the default authentication scheme. The default scheme is used unless a resource requests a specific scheme. It's also possible to:

* Specify different default schemes to use for authenticate, challenge, and forbid actions.
* Combine multiple schemes into one using [policy schemes](policyschemes?view=aspnetcore-10.0).

### Authentication handler

An authentication handler:

* Is a type that implements the behavior of a scheme.
* Is derived from [IAuthenticationHandler](/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationhandler) or [AuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1).
* Has the primary responsibility to authenticate users.

Based on the authentication scheme's configuration and the incoming request context, authentication handlers:

* Construct [AuthenticationTicket](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationticket) objects representing the user's identity if authentication is successful.

* Return 'no result' or 'failure' if authentication is unsuccessful.

* Have methods for challenge and forbid actions for when users attempt to access resources:

  * They're unauthorized to access (forbid).
  * When they're unauthenticated (challenge).

### `RemoteAuthenticationHandler<TOptions>` vs `AuthenticationHandler<TOptions>`

[RemoteAuthenticationHandler\<TOptions>](/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationhandler-1) is the class for authentication that requires a remote authentication step. When the remote authentication step is finished, the handler calls back to the `CallbackPath` set by the handler. The handler finishes the authentication step using the information passed to the [HandleRemoteAuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.twitter.twitterhandler.handleremoteauthenticateasync) callback path. [OAuth 2.0](https://oauth.net/2/) and [OIDC](https://openid.net/developers/how-connect-works/) both use this pattern. JWT and cookies don't since they can directly use the bearer header and cookie to authenticate. The remotely hosted provider in this case:

* Is the authentication provider.
* Examples include [Facebook](social/facebook-logins?view=aspnetcore-10.0), [Twitter](social/twitter-logins?view=aspnetcore-10.0), [Google](social/google-logins?view=aspnetcore-10.0), [Microsoft](social/microsoft-logins?view=aspnetcore-10.0), and any other OIDC provider that handles authenticating users using the handlers mechanism.

### Authenticate

An authentication scheme's authenticate action is responsible for constructing the user's identity based on request context. It returns an [AuthenticateResult](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticateresult) indicating whether authentication was successful and, if so, the user's identity in an authentication ticket. See [AuthenticateAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.authenticateasync). Authenticate examples include:

* A cookie authentication scheme constructing the user's identity from cookies.
* A JWT bearer scheme deserializing and validating a JWT bearer token to construct the user's identity.

### Challenge

An authentication challenge is invoked by Authorization when an unauthenticated user requests an endpoint that requires authentication. An authentication challenge is issued, for example, when an anonymous user requests a restricted resource or follows a login link. Authorization invokes a challenge using the specified authentication schemes, or the default if none is specified. See [ChallengeAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.challengeasync). Authentication challenge examples include:

* A cookie authentication scheme redirecting the user to a login page.
* A JWT bearer scheme returning a 401 result with a `www-authenticate: bearer` header.

A challenge action should let the user know what authentication mechanism to use to access the requested resource.

### Forbid

An authentication scheme's forbid action is called by Authorization when an authenticated user attempts to access a resource they're not permitted to access. See [ForbidAsync](/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.forbidasync). Authentication forbid examples include:

* A cookie authentication scheme redirecting the user to a page indicating access was forbidden.
* A JWT bearer scheme returning a 403 result.
* A custom authentication scheme redirecting to a page where the user can request access to the resource.

A forbid action can let the user know:

* They're authenticated.
* They're not permitted to access the requested resource.

See the following links for differences between challenge and forbid:

* [Resource-based authorization](../authorization/resource-based?view=aspnetcore-10.0).
* [Differences between challenge and forbid](../authorization/secure-data?view=aspnetcore-10.0#challenge).

## Authentication providers per tenant

ASP.NET Core framework doesn't have a built-in solution for multi-tenant authentication. While it's possible for customers to write an app with multi-tenant authentication, we recommend using one of the following ASP.NET Core application frameworks that support multi-tenant authentication.

[Orchard Core](https://www.orchardcore.net/) is an open-source, modular, and multi-tenant app framework built with ASP.NET Core that also provides a content management system (CMS). See the [Orchard Core](https://github.com/OrchardCMS/OrchardCore) source for an example of authentication providers per tenant.

[ABP Framework](https://abp.io/) supports various architectural patterns including modularity, microservices, domain-driven design, and multi-tenancy. See [ABP Framework source on GitHub](https://github.com/abpframework/abp).

## Additional resources

* [Authorize with a specific scheme in ASP.NET Core](../authorization/authorize-with-a-specific-scheme?view=aspnetcore-10.0)
* [Policy schemes in ASP.NET Core](policyschemes?view=aspnetcore-10.0)
* [Create an ASP.NET Core app with user data protected by authorization](../authorization/secure-data?view=aspnetcore-10.0)
* [Globally require authenticated users](../authorization/policies?view=aspnetcore-10.0#require-global-user-authentication)
* [GitHub issue on using multiple authentication schemes](https://github.com/dotnet/aspnetcore/issues/26002)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0000
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0000.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0000: Do not call 'IServiceCollection.BuildServiceProvider' in 'ConfigureServices'

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0000      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A call to [BuildServiceProvider](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectioncontainerbuilderextensions.buildserviceprovider) was detected in the application start up code.

## Rule description

Calling 'BuildServiceProvider' from application code results in more than one copy of singleton services being created which might result in incorrect application behavior. Consider alternatives such as dependency injecting services as parameters to 'Configure'.

## How to fix violations

Remove the call to `BuildServiceProvider` from the application startup code.

## When to suppress warnings

It is safe to suppress this rule if updating the application to remove the call to `BuildServiceProvider` is non-trivial and you have thoroughly tested the application to ensure multiple singleton services are not added.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/metrics/built-in
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/metrics/built-in.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core built-in metrics

This article is the entry point for the built-in metrics that ASP.NET Core produces using the [System.Diagnostics.Metrics](/en-us/dotnet/api/system.diagnostics.metrics) API. The metrics reference is organized into focused pages by topic. Use the following pages to find the instruments, attributes, and usage guidance for each area. For a listing of metrics based on the older [EventCounters](/en-us/dotnet/core/diagnostics/event-counters) API, see [Available counters](/en-us/dotnet/core/diagnostics/available-counters).

For information on how to collect, report, enrich, and test with ASP.NET Core metrics, see [ASP.NET Core metrics](overview?view=aspnetcore-10.0).

Note

Starting in .NET 11, the built-in HTTP server meters cataloged in these reference pages (most notably `Microsoft.AspNetCore.Hosting` and `Microsoft.AspNetCore.Server.Kestrel`) emit data compliant with the required parts of the [OpenTelemetry HTTP server semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/). The [`OpenTelemetry.Instrumentation.AspNetCore`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNetCore) package is optional for HTTP server metrics—register these meters directly with the OpenTelemetry SDK. For more information, see [ASP.NET Core metrics](overview?view=aspnetcore-10.0).

## Metrics reference pages

| Page                                                                      | Namespaces                                                                                                                                                                                                                          | Example metrics                                                                                                                        |
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| [HTTP metrics](http?view=aspnetcore-10.0)                                 | `Microsoft.AspNetCore.Hosting`, `Microsoft.AspNetCore.Routing`, `Microsoft.AspNetCore.RateLimiting`, `Microsoft.AspNetCore.HeaderParsing`, `Microsoft.AspNetCore.Server.Kestrel`, `Microsoft.AspNetCore.Http.Connections` (SignalR) | `http.server.request.duration`, `aspnetcore.routing.match_attempts`, `kestrel.active_connections`, `signalr.server.active_connections` |
| [Diagnostics metrics](diagnostics?view=aspnetcore-10.0)                   | `Microsoft.AspNetCore.Diagnostics`                                                                                                                                                                                                  | `aspnetcore.diagnostics.exceptions`                                                                                                    |
| [Blazor (Components) metrics](blazor?view=aspnetcore-10.0)                | `Microsoft.AspNetCore.Components`, `Microsoft.AspNetCore.Components.Lifecycle`, `Microsoft.AspNetCore.Components.Server.Circuits`                                                                                                   | `aspnetcore.components.navigation`, `aspnetcore.components.circuit.active`                                                             |
| [Authentication and authorization metrics](security?view=aspnetcore-10.0) | `Microsoft.AspNetCore.Authorization`, `Microsoft.AspNetCore.Authentication`                                                                                                                                                         | `aspnetcore.authorization.attempts`, `aspnetcore.authentication.challenges`                                                            |

The Blazor (Components) and authentication and authorization metrics pages describe metrics available in ASP.NET Core 10.0 or later. Select ASP.NET Core 10.0 (or a later version) with the version selector to view that content.

## How metrics are grouped

Metrics are grouped by audience and topic. Because some namespaces don't map cleanly to a single topic, the following placement rules apply:

* HTTP request handling, routing, rate limiting, header parsing, the Kestrel web server, and SignalR (`Microsoft.AspNetCore.Http.Connections`) are grouped together on the HTTP metrics page, because they all relate to serving HTTP and real-time connections.
* Error handling middleware metrics are on the diagnostics metrics page.
* Blazor (Components) metrics are grouped by their component, lifecycle, and server circuit namespaces.
* Authentication, authorization, and Identity metrics are grouped together as security-related metrics.

## How to read this reference

Each metric on the reference pages is documented with the following information:

* **Name**: The metric (instrument) name.
* **Instrument Type**: The kind of instrument, such as `Counter`, `UpDownCounter`, or `Histogram`.
* **Unit (UCUM)**: The unit of measurement, expressed using the [Unified Code for Units of Measure (UCUM)](https://ucum.org/), for example `s` for seconds.
* **Attribute**: The dimensions (tags) reported with the metric.
* **Presence**: When the attribute is present, for example `Always` or only under specific conditions.
* **Usage**: Example questions the metric helps answer.

For guidance on how to collect, report, enrich, and test with these metrics, see [ASP.NET Core metrics](overview?view=aspnetcore-10.0).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-02

----
url: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-2.0
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-2.0.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# What's new in ASP.NET Core 2.0

This article highlights the most significant changes in ASP.NET Core 2.0, with links to relevant documentation.

## Razor Pages

Razor Pages is a new feature of ASP.NET Core MVC that makes coding page-focused scenarios easier and more productive.

For more information, see the introduction and tutorial:

* [Introduction to Razor Pages](../razor-pages/?view=aspnetcore-10.0)
* [Get started with Razor Pages](../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)

## ASP.NET Core metapackage

A new ASP.NET Core metapackage includes all of the packages made and supported by the ASP.NET Core and Entity Framework Core teams, along with their internal and 3rd-party dependencies. You no longer need to choose individual ASP.NET Core features by package. All features are included in the [Microsoft.AspNetCore.All](https://www.nuget.org/packages/Microsoft.AspNetCore.All) package. The default templates use this package.

For more information, see [Microsoft.AspNetCore.All metapackage for ASP.NET Core 2.0](../fundamentals/metapackage?view=aspnetcore-10.0).

## Runtime Store

Applications that use the `Microsoft.AspNetCore.All` metapackage automatically take advantage of the new .NET Core Runtime Store. The Store contains all the runtime assets needed to run ASP.NET Core 2.0 applications. When you use the `Microsoft.AspNetCore.All` metapackage, no assets from the referenced ASP.NET Core NuGet packages are deployed with the application because they already reside on the target system. The assets in the Runtime Store are also precompiled to improve application startup time.

For more information, see [Runtime store](/en-us/dotnet/core/deploying/runtime-store)

## .NET Standard 2.0

The ASP.NET Core 2.0 packages target .NET Standard 2.0. The packages can be referenced by other .NET Standard 2.0 libraries, and they can run on .NET Standard 2.0-compliant implementations of .NET, including .NET Core 2.0 and .NET Framework 4.6.1.

The `Microsoft.AspNetCore.All` metapackage targets .NET Core 2.0 only, because it's intended to be used with the .NET Core 2.0 Runtime Store.

## Configuration update

An `IConfiguration` instance is added to the services container by default in ASP.NET Core 2.0. `IConfiguration` in the services container makes it easier for applications to retrieve configuration values from the container.

For information about the status of planned documentation, see the [GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/3387).

## Logging update

In ASP.NET Core 2.0, logging is incorporated into the dependency injection (DI) system by default. You add providers and configure filtering in the `Program.cs` file instead of in the `Startup.cs` file. And the default `ILoggerFactory` supports filtering in a way that lets you use one flexible approach for both cross-provider filtering and specific-provider filtering.

For more information, see [Introduction to Logging](../fundamentals/logging/?view=aspnetcore-10.0).

## Authentication update

A new authentication model makes it easier to configure authentication for an application using DI.

New templates are available for configuring authentication for web apps and web APIs using [Azure AD B2C](https://azure.microsoft.com/services/active-directory-b2c/).

For information about the status of planned documentation, see the [GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/3054).

## Identity update

We've made it easier to build secure web APIs using Identity in ASP.NET Core 2.0. You can acquire access tokens for accessing your web APIs using the [Microsoft Authentication Library (MSAL)](https://www.nuget.org/packages/Microsoft.Identity.Client).

For more information on authentication changes in 2.0, see the following resources:

* [Account confirmation and password recovery in ASP.NET Core](../security/authentication/accconfirm?view=aspnetcore-10.0)
* [Enable QR Code generation for authenticator apps in ASP.NET Core](../security/authentication/identity-enable-qrcodes?view=aspnetcore-10.0)
* [Migrate Authentication and Identity to ASP.NET Core 2.0](../migration/1x-to-2x/identity-2x?view=aspnetcore-10.0)

## SPA templates

Single Page Application (SPA) project templates for Angular, Aurelia, Knockout.js, React.js, and React.js with Redux are available. The Angular template has been updated to Angular 4. The Angular and React templates are available by default; for information about how to get the other templates, see [Create a new SPA project](../client-side/spa-services?view=aspnetcore-10.0#create-a-new-project). For information about how to build a SPA in ASP.NET Core, see [The features described in this article are obsolete as of ASP.NET Core 3.0](../client-side/spa-services?view=aspnetcore-10.0).

## Kestrel improvements

The Kestrel web server has new features that make it more suitable as an Internet-facing server. A number of server constraint configuration options are added in the `KestrelServerOptions` class's new `Limits` property. Add limits for the following:

* Maximum client connections
* Maximum request body size
* Minimum request body data rate

For more information, see [Kestrel web server implementation in ASP.NET Core](../fundamentals/servers/kestrel?view=aspnetcore-10.0).

## WebListener renamed to HTTP.sys

The packages `Microsoft.AspNetCore.Server.WebListener` and `Microsoft.Net.Http.Server` have been merged into a new package `Microsoft.AspNetCore.Server.HttpSys`. The namespaces have been updated to match.

For more information, see [HTTP.sys web server implementation in ASP.NET Core](../fundamentals/servers/httpsys?view=aspnetcore-10.0).

## Enhanced HTTP header support

When using MVC to transmit a `FileStreamResult` or a `FileContentResult`, you now have the option to set an `ETag` or a `LastModified` date on the content you transmit. You can set these values on the returned content with code similar to the following:

```
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array");
var entityTag = new EntityTagHeaderValue("\"MyCalculatedEtagValue\"");
return File(data, "text/plain", "downloadName.txt", lastModified: DateTime.UtcNow.AddSeconds(-5), entityTag: entityTag);
```

The file returned to your visitors has the appropriate HTTP headers for the `ETag` and `LastModified` values.

If an application visitor requests content with a Range Request header, ASP.NET Core recognizes the request and handles the header. If the requested content can be partially delivered, ASP.NET Core appropriately skips and returns just the requested set of bytes. You don't need to write any special handlers into your methods to adapt or handle this feature; it's automatically handled for you.

## Hosting startup and Application Insights

Hosting environments can now inject extra package dependencies and execute code during application startup, without the application needing to explicitly take a dependency or call any methods. This feature can be used to enable certain environments to "light-up" features unique to that environment without the application needing to know ahead of time.

In ASP.NET Core 2.0, this feature is used to automatically enable Application Insights diagnostics when debugging in Visual Studio and (after opting in) when running in Azure App Services. As a result, the project templates no longer add Application Insights packages and code by default.

For information about the status of planned documentation, see the [GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/3389).

## Automatic use of antiforgery tokens

ASP.NET Core has always helped HTML-encode content by default, but with the new version an extra step is taken to help prevent cross-site request forgery (XSRF) attacks. ASP.NET Core will now emit antiforgery tokens by default and validate them on form POST actions and pages without extra configuration.

For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../security/anti-request-forgery?view=aspnetcore-10.0).

## Automatic precompilation

Razor view pre-compilation is enabled during publish by default, reducing the publish output size and application startup time.

For more information, see [Razor view compilation and precompilation in ASP.NET Core](../mvc/views/view-compilation?view=aspnetcore-10.0).

## Razor support for C# 7.1

The Razor view engine has been updated to work with the new Roslyn compiler. That includes support for C# 7.1 features like Default Expressions, Inferred Tuple Names, and Pattern-Matching with Generics. To use C# 7.1 in your project, add the following property in your project file and then reload the solution:

```
<LangVersion>latest</LangVersion>
```

For information about the status of C# 7.1 features, see [the Roslyn GitHub repository](https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md).

## Other documentation updates for 2.0

* [Visual Studio publish profiles for ASP.NET Core app deployment](../host-and-deploy/visual-studio-publish-profiles?view=aspnetcore-10.0)
* [Key Management](../security/data-protection/implementation/key-management?view=aspnetcore-10.0)
* [Configure Facebook authentication](../security/authentication/social/facebook-logins?view=aspnetcore-10.0)
* [Configure Twitter authentication](../security/authentication/social/twitter-logins?view=aspnetcore-10.0)
* [Configure Google authentication](../security/authentication/social/google-logins?view=aspnetcore-10.0)
* [Configure Microsoft Account authentication](../security/authentication/social/microsoft-logins?view=aspnetcore-10.0)

## Migration guidance

For guidance on how to migrate ASP.NET Core 1.x applications to ASP.NET Core 2.0, see the following resources:

* [Migrate from ASP.NET Core 1.x to ASP.NET Core 2.0](../migration/1x-to-2x/?view=aspnetcore-10.0)
* [Migrate Authentication and Identity to ASP.NET Core 2.0](../migration/1x-to-2x/identity-2x?view=aspnetcore-10.0)

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

## Related content

For the complete list of changes, see the [ASP.NET Core 2.0 Release Notes](https://github.com/dotnet/aspnetcore/releases/tag/2.0.0).

To connect with the ASP.NET Core development team's progress and plans, tune in to the [ASP.NET Community Standup](https://live.asp.net/).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/gdpr
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/gdpr.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# EU General Data Protection Regulation (GDPR) support in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

ASP.NET Core provides APIs and templates to help meet some of the [EU General Data Protection Regulation (GDPR)](https://ec.europa.eu/info/law/law-topic/data-protection/reform/what-does-general-data-protection-regulation-gdpr-govern_en) requirements:

* The project templates include extension points and stubbed markup that you can replace with your privacy and cookie use policy.
* The `Pages/Privacy.cshtml` page or `Views/Home/Privacy.cshtml` view provides a page to detail your site's privacy policy.

For GDPR guidance that applies to Blazor apps, see [EU General Data Protection Regulation (GDPR) support in ASP.NET Core Blazor](../blazor/security/gdpr?view=aspnetcore-10.0).

To enable the default cookie consent feature like that found in the ASP.NET Core 2.2 templates in a current ASP.NET Core template generated app, add the following highlighted code to `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    // This lambda determines whether user consent for non-essential 
    // cookies is needed for a given request.
    options.CheckConsentNeeded = context => true;

    options.MinimumSameSitePolicy = SameSiteMode.None;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

In the preceding code, [CookiePolicyOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions) and [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy) are used.

* Add the cookie consent partial to the `_Layout.cshtml` file:

  ```
              @*Previous markup removed for brevity*@
      </header>
      <div class="container">
          <partial name="_CookieConsentPartial" />
          <main role="main" class="pb-3">
              @RenderBody()
          </main>
      </div>

      <footer class="border-top footer text-muted">
          <div class="container">
              &copy; 2022 - WebGDPR - <a asp-area="" asp-page="/Privacy">Privacy</a>
          </div>
      </footer>

      <script src="~/lib/jquery/dist/jquery.min.js"></script>
      <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
      <script src="~/js/site.js" asp-append-version="true"></script>

      @await RenderSectionAsync("Scripts", required: false)
  </body>
  </html>
  ```

* Add the `_CookieConsentPartial.cshtml` file to the project:

  ```
  @using Microsoft.AspNetCore.Http.Features

  @{
      var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
      var showBanner = !consentFeature?.CanTrack ?? false;
      var cookieString = consentFeature?.CreateConsentCookie();
  }

  @if (showBanner)
  {
      <div id="cookieConsent" class="alert alert-info alert-dismissible fade show" role="alert">
          Use this space to summarize your privacy and cookie use policy. <a asp-page="/Privacy">Learn More</a>.
          <button type="button" class="accept-policy close" data-bs-dismiss="alert" aria-label="Close" data-cookie-string="@cookieString">
              <span aria-hidden="true">Accept</span>
          </button>
      </div>
      <script>
          (function () {
              var button = document.querySelector("#cookieConsent button[data-cookie-string]");
              button.addEventListener("click", function (event) {
                  document.cookie = button.dataset.cookieString;
              }, false);
          })();
      </script>
  }
  ```

* Select the ASP.NET Core [2.2 version](gdpr?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-2.2) of this article to read about the cookie consent feature.

## Customize the cookie consent value

Specify the value used to track if the user consented to the cookie use policy using the [`CookiePolicyOptions.ConsentCookieValue`](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.consentcookievalue) property:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
    options.ConsentCookieValue = "true";
});

var app = builder.Build();
```

## Encryption at rest

Some databases and storage mechanisms allow for encryption at rest. Encryption at rest:

* Encrypts stored data automatically.
* Encrypts without configuration, programming, or other work for the software that accesses the data.
* Is the easiest and safest option.
* Allows the database to manage keys and encryption.

For example:

* Microsoft SQL and Azure SQL provide [Transparent Data Encryption](/en-us/sql/relational-databases/security/encryption/transparent-data-encryption) (TDE).
* [SQL Azure encrypts the database by default](https://azure.microsoft.com/updates/newly-created-azure-sql-databases-encrypted-by-default/)
* [Azure Blobs, Files, Table, and Queue Storage are encrypted by default](https://azure.microsoft.com/blog/announcing-default-encryption-for-azure-blobs-files-table-and-queue-storage/).

For databases that don't provide built-in encryption at rest, you may be able to use disk encryption to provide the same protection. For example:

* [BitLocker for Windows Server](/en-us/windows/security/information-protection/bitlocker/bitlocker-how-to-deploy-on-windows-server)

* Linux:

  * [eCryptfs](https://launchpad.net/ecryptfs)
  * [EncFS](https://github.com/vgough/encfs).

## Related content

* [Microsoft Trust Center: Safeguard individual privacy with cloud services from Microsoft: GDPR](https://www.microsoft.com/trust-center/privacy/gdpr-overview)
* [European Commission: Data protection explained](https://ec.europa.eu/info/law/law-topic/data-protection/reform/what-does-general-data-protection-regulation-gdpr-govern_en)

- The project templates include extension points and stubbed markup that you can replace with your privacy and cookie use policy.
- A cookie consent feature allows you to ask for (and track) consent from your users for storing personal information. If a user hasn't consented to data collection and the app has [CheckConsentNeeded](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions.checkconsentneeded) set to `true`, non-essential cookies aren't sent to the browser.
- Cookies can be marked as essential. Essential cookies are sent to the browser even when the user hasn't consented and tracking is disabled.
- [TempData and Session cookies](#tempdata) aren't functional when tracking is disabled.
- The [Identity manage](#pd) page provides a link to download and delete user data.

The [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/gdpr/sample) allows you to test most of the GDPR extension points and APIs added to the ASP.NET Core 2.1 templates. See the [ReadMe](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/gdpr/sample) file for testing instructions.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/live/aspnetcore/security/gdpr/sample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample))

## ASP.NET Core GDPR support in template-generated code

Razor Pages and MVC projects created with the project templates include the following GDPR support:

* [CookiePolicyOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions) and [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy) are set in the `Startup` class.
* The *\_CookieConsentPartial.cshtml* [partial view](../mvc/views/tag-helpers/built-in/partial-tag-helper?view=aspnetcore-10.0). An **Accept** button is included in this file. When the user clicks the **Accept** button, consent to store cookies is provided.
* The `Pages/Privacy.cshtml` page or `Views/Home/Privacy.cshtml` view provides a page to detail your site's privacy policy. The *\_CookieConsentPartial.cshtml* file generates a link to the Privacy page.
* For apps created with individual user accounts, the Manage page provides links to download and delete [personal user data](#pd).

### CookiePolicyOptions and UseCookiePolicy

[CookiePolicyOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions) are initialized in `Startup.ConfigureServices`:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services 
    // to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies 
            // is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
        services.AddDefaultIdentity<IdentityUser>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

        // If the app uses session state, call AddSession.
        // services.AddSession();

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

    // This method gets called by the runtime. Use this method to configure the 
    // HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();

        app.UseAuthentication();

        // If the app uses session state, call Session Middleware after Cookie 
        // Policy Middleware and before MVC Middleware.
        // app.UseSession();

        app.UseMvc();
    }
}
```

[UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy) is called in `Startup.Configure`:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services 
    // to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies 
            // is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
        services.AddDefaultIdentity<IdentityUser>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

        // If the app uses session state, call AddSession.
        // services.AddSession();

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

    // This method gets called by the runtime. Use this method to configure the 
    // HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();

        app.UseAuthentication();

        // If the app uses session state, call Session Middleware after Cookie 
        // Policy Middleware and before MVC Middleware.
        // app.UseSession();

        app.UseMvc();
    }
}
```

### \_CookieConsentPartial.cshtml partial view

The *\_CookieConsentPartial.cshtml* partial view:

```
@using Microsoft.AspNetCore.Http.Features

@{
    var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
    var showBanner = !consentFeature?.CanTrack ?? false;
    var cookieString = consentFeature?.CreateConsentCookie();
}

@if (showBanner)
{
    <nav id="cookieConsent" class="navbar navbar-default navbar-fixed-top" role="alert">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#cookieConsent .navbar-collapse">
                    <span class="sr-only">Toggle cookie consent banner</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <span class="navbar-brand"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></span>
            </div>
            <div class="collapse navbar-collapse">
                <p class="navbar-text">
                    Use this space to summarize your privacy and cookie use policy.
                </p>
                <div class="navbar-right">
                    <a asp-page="/Privacy" class="btn btn-info navbar-btn">Learn More</a>
                    <button type="button" class="btn btn-default navbar-btn" data-cookie-string="@cookieString">Accept</button>
                </div>
            </div>
        </div>
    </nav>
    <script>
        (function () {
            document.querySelector("#cookieConsent button[data-cookie-string]").addEventListener("click", function (el) {
                document.cookie = el.target.dataset.cookieString;
                document.querySelector("#cookieConsent").classList.add("hidden");
            }, false);
        })();
    </script>
}
```

This partial:

* Obtains the state of tracking for the user. If the app is configured to require consent, the user must consent before cookies can be tracked. If consent is required, the cookie consent panel is fixed at top of the navigation bar created by the *\_Layout.cshtml* file.
* Provides an HTML `<p>` element to summarize your privacy and cookie use policy.
* Provides a link to Privacy page or view where you can detail your site's privacy policy.

## Essential cookies

If consent to store cookies hasn't been provided, only cookies marked essential are sent to the browser. The following code makes a cookie essential:

```
public IActionResult OnPostCreateEssentialAsync()
{
    HttpContext.Response.Cookies.Append(Constants.EssentialSec, 
        DateTime.Now.Second.ToString(), 
        new CookieOptions() { IsEssential = true });

    ResponseCookies = Response.Headers[HeaderNames.SetCookie].ToString();

    return RedirectToPage("./Index");
}
```

[]()

### TempData provider and session state cookies aren't essential

The [TempData provider](../fundamentals/app-state?view=aspnetcore-10.0#tempdata) cookie isn't essential. If tracking is disabled, the TempData provider isn't functional. To enable the TempData provider when tracking is disabled, mark the TempData cookie as essential in `Startup.ConfigureServices`:

```
// The TempData provider cookie is not essential. Make it essential
// so TempData is functional when tracking is disabled.
services.Configure<CookieTempDataProviderOptions>(options => {
    options.Cookie.IsEssential = true;
});
```

[Session state](../fundamentals/app-state?view=aspnetcore-10.0) cookies are not essential. Session state isn't functional when tracking is disabled. The following code makes session cookies essential:

```
services.AddSession(options =>
{
    options.Cookie.IsEssential = true;
});
```

[]()

## Personal data

ASP.NET Core apps created with individual user accounts include code to download and delete personal data.

Select the user name and then select **Personal data**:

Notes:

* To generate the `Account/Manage` code, see [Scaffold Identity](authentication/scaffold-identity?view=aspnetcore-10.0).
* The **Delete** and **Download** links only act on the default identity data. Apps that create custom user data must be extended to delete/download the custom user data. For more information, see [Add, download, and delete custom user data to Identity](authentication/add-user-data?view=aspnetcore-10.0).
* Saved tokens for the user that are stored in the Identity database table `AspNetUserTokens` are deleted when the user is deleted via the cascading delete behavior due to the [foreign key](https://github.com/aspnet/Identity/blob/release/2.1/src/EF/IdentityUserContext.cs#L152).
* [External provider authentication](authentication/social/?view=aspnetcore-10.0), such as Facebook and Google, isn't available before the cookie policy is accepted.

## Encryption at rest

Some databases and storage mechanisms allow for encryption at rest. Encryption at rest:

* Encrypts stored data automatically.
* Encrypts without configuration, programming, or other work for the software that accesses the data.
* Is the easiest and safest option.
* Allows the database to manage keys and encryption.

For example:

* Microsoft SQL and Azure SQL provide [Transparent Data Encryption](/en-us/sql/relational-databases/security/encryption/transparent-data-encryption) (TDE).
* [SQL Azure encrypts the database by default](https://azure.microsoft.com/updates/newly-created-azure-sql-databases-encrypted-by-default/)
* [Azure Blobs, Files, Table, and Queue Storage are encrypted by default](https://azure.microsoft.com/blog/announcing-default-encryption-for-azure-blobs-files-table-and-queue-storage/).

For databases that don't provide built-in encryption at rest, you may be able to use disk encryption to provide the same protection. For example:

* [BitLocker for Windows Server](/en-us/windows/security/information-protection/bitlocker/bitlocker-how-to-deploy-on-windows-server)

* Linux:

  * [eCryptfs](https://launchpad.net/ecryptfs)
  * [EncFS](https://github.com/vgough/encfs).

## Related content

* [Microsoft.com/GDPR](https://www.microsoft.com/trustcenter/Privacy/GDPR)

- The project templates include extension points and stubbed markup that you can replace with your privacy and cookie use policy.
- The `Pages/Privacy.cshtml` page or `Views/Home/Privacy.cshtml` view provides a page to detail your site's privacy policy.

To enable the default cookie consent feature like that found in the ASP.NET Core 2.2 templates in a current ASP.NET Core template generated app:

* Add `using Microsoft.AspNetCore.Http` to the list of using directives.

* Add [CookiePolicyOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions) to `Startup.ConfigureServices` and [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy) to `Startup.Configure`:

  ```
  public class Startup
  {
      public Startup(IConfiguration configuration)
      {
          Configuration = configuration;
      }

      public IConfiguration Configuration { get; }

      public void ConfigureServices(IServiceCollection services)
      {
          services.Configure<CookiePolicyOptions>(options =>
          {
              // This lambda determines whether user consent for non-essential 
              // cookies is needed for a given request.
              options.CheckConsentNeeded = context => true;
              // requires using Microsoft.AspNetCore.Http;
              options.MinimumSameSitePolicy = SameSiteMode.None;
          });

          services.AddRazorPages();
      }

      public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
      {
          if (env.IsDevelopment())
          {
              app.UseDeveloperExceptionPage();
          }
          else
          {
              app.UseExceptionHandler("/Error");
              app.UseHsts();
          }

          app.UseHttpsRedirection();
          app.UseStaticFiles();
          app.UseCookiePolicy();

          app.UseRouting();

          app.UseAuthorization();

          app.UseEndpoints(endpoints =>
          {
              endpoints.MapRazorPages();
          });
      }
  }
  ```

* Add the cookie consent partial to the `_Layout.cshtml` file:

  ```
          @*Previous markup removed for brevity*@
      </header>
      <div class="container">
          <partial name="_CookieConsentPartial" />
          <main role="main" class="pb-3">
              @RenderBody()
          </main>
      </div>

      <footer class="border-top footer text-muted">
          <div class="container">
              &copy; 2019 - RPCC - <a asp-area="" asp-page="/Privacy">Privacy</a>
          </div>
      </footer>

      <script src="~/lib/jquery/dist/jquery.js"></script>
      <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
      <script src="~/js/site.js" asp-append-version="true"></script>

      @RenderSection("Scripts", required: false)
  </body>
  </html>
  ```

* Add the *\_CookieConsentPartial.cshtml* file to the project:

  ```
  @using Microsoft.AspNetCore.Http.Features

  @{
      var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
      var showBanner = !consentFeature?.CanTrack ?? false;
      var cookieString = consentFeature?.CreateConsentCookie();
  }

  @if (showBanner)
  {
      <div id="cookieConsent" class="alert alert-info alert-dismissible fade show" role="alert">
          Use this space to summarize your privacy and cookie use policy. <a asp-page="/Privacy">Learn More</a>.
          <button type="button" class="accept-policy close" data-dismiss="alert" aria-label="Close" data-cookie-string="@cookieString">
              <span aria-hidden="true">Accept</span>
          </button>
      </div>
      <script>
          (function () {
              var button = document.querySelector("#cookieConsent button[data-cookie-string]");
              button.addEventListener("click", function (event) {
                  document.cookie = button.dataset.cookieString;
              }, false);
          })();
      </script>
  }
  ```

* Select the ASP.NET Core 2.2 version of this article to read about the cookie consent feature.

- The project templates include extension points and stubbed markup that you can replace with your privacy and cookie use policy.
- The `Pages/Privacy.cshtml` page or `Views/Home/Privacy.cshtml` view provides a page to detail your site's privacy policy.

To enable the default cookie consent feature like that found in the ASP.NET Core 2.2 templates in a current ASP.NET Core template generated app, add the following highlighted code to `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    // This lambda determines whether user consent for non-essential 
    // cookies is needed for a given request.
    options.CheckConsentNeeded = context => true;

    options.MinimumSameSitePolicy = SameSiteMode.None;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

In the preceding code, [CookiePolicyOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyoptions) and [UseCookiePolicy](/en-us/dotnet/api/microsoft.aspnetcore.builder.cookiepolicyappbuilderextensions.usecookiepolicy) are used.

* Add the cookie consent partial to the `_Layout.cshtml` file:

  ```
              @*Previous markup removed for brevity*@
      </header>
      <div class="container">
          <partial name="_CookieConsentPartial" />
          <main role="main" class="pb-3">
              @RenderBody()
          </main>
      </div>

      <footer class="border-top footer text-muted">
          <div class="container">
              &copy; 2022 - WebGDPR - <a asp-area="" asp-page="/Privacy">Privacy</a>
          </div>
      </footer>

      <script src="~/lib/jquery/dist/jquery.min.js"></script>
      <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
      <script src="~/js/site.js" asp-append-version="true"></script>

      @await RenderSectionAsync("Scripts", required: false)
  </body>
  </html>
  ```

* Add the `_CookieConsentPartial.cshtml` file to the project:

  ```
  @using Microsoft.AspNetCore.Http.Features

  @{
      var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
      var showBanner = !consentFeature?.CanTrack ?? false;
      var cookieString = consentFeature?.CreateConsentCookie();
  }

  @if (showBanner)
  {
      <div id="cookieConsent" class="alert alert-info alert-dismissible fade show" role="alert">
          Use this space to summarize your privacy and cookie use policy. <a asp-page="/Privacy">Learn More</a>.
          <button type="button" class="accept-policy close" data-bs-dismiss="alert" aria-label="Close" data-cookie-string="@cookieString">
              <span aria-hidden="true">Accept</span>
          </button>
      </div>
      <script>
          (function () {
              var button = document.querySelector("#cookieConsent button[data-cookie-string]");
              button.addEventListener("click", function (event) {
                  document.cookie = button.dataset.cookieString;
              }, false);
          })();
      </script>
  }
  ```

* Select the ASP.NET Core [2.2 version](gdpr?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-2.2) of this article to read about the cookie consent feature.

## Encryption at rest

Some databases and storage mechanisms allow for encryption at rest. Encryption at rest:

* Encrypts stored data automatically.
* Encrypts without configuration, programming, or other work for the software that accesses the data.
* Is the easiest and safest option.
* Allows the database to manage keys and encryption.

For example:

* Microsoft SQL and Azure SQL provide [Transparent Data Encryption](/en-us/sql/relational-databases/security/encryption/transparent-data-encryption) (TDE).
* [SQL Azure encrypts the database by default](https://azure.microsoft.com/updates/newly-created-azure-sql-databases-encrypted-by-default/)
* [Azure Blobs, Files, Table, and Queue Storage are encrypted by default](https://azure.microsoft.com/blog/announcing-default-encryption-for-azure-blobs-files-table-and-queue-storage/).

For databases that don't provide built-in encryption at rest, you may be able to use disk encryption to provide the same protection. For example:

* [BitLocker for Windows Server](/en-us/windows/security/information-protection/bitlocker/bitlocker-how-to-deploy-on-windows-server)

* Linux:

  * [eCryptfs](https://launchpad.net/ecryptfs)
  * [EncFS](https://github.com/vgough/encfs).

## Related content

* [Microsoft.com/GDPR](https://www.microsoft.com/trustcenter/Privacy/GDPR)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/qrcodes-for-authenticator-apps
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/security/qrcodes-for-authenticator-apps.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Enable QR code generation for TOTP authenticator apps in an ASP.NET Core Blazor Web App

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to configure an ASP.NET Core Blazor Web App for two-factor authentication (2FA) with QR codes generated by Time-based One-time Password Algorithm (TOTP) authenticator apps.

For an introduction to 2FA with TOTP authenticator apps, see [Enable QR code generation for TOTP authentication](../../security/authentication/identity-enable-qrcodes?view=aspnetcore-10.0).

The guidance in this article relies upon either creating the app with **Individual Accounts** for the new app's **Authentication type** or [scaffolding Identity into an existing app](../../security/authentication/scaffold-identity?view=aspnetcore-10.0#scaffold-identity-into-a-blazor-project). For guidance on using the .NET CLI instead of Visual Studio for scaffolding Identity into an existing app, see [ASP.NET Core code generator tool (\`aspnet-codegenerator\`)](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

Warning

TOTP codes should be kept secret because they can be used to authenticate multiple times before they expire.

## Add QR codes to the 2FA configuration page

A QR code generated by the app to set up 2FA with an TOTP authenticator app must be generated by a QR code library.

The guidance in this article uses [`manuelbl/QrCodeGenerator`](https://github.com/manuelbl/QrCodeGenerator), but you can use any QR code generation library.

Add a package reference for the [`Net.Codecrete.QrCodeGenerator`](https://www.nuget.org/packages/Net.Codecrete.QrCodeGenerator) NuGet package.

Note

For guidance on adding packages to .NET apps, see the articles under *Install and manage packages* at [Package consumption workflow (NuGet documentation)](/en-us/nuget/consume-packages/overview-and-workflow). Confirm correct package versions at [NuGet.org](https://www.nuget.org).

Open the `EnableAuthenticator` component in the `Components/Account/Pages/Manage` folder. At the top of the file under the `@page` directive, add an `@using` directive for the QrCodeGenerator namespace:

```
@using Net.Codecrete.QrCodeGenerator
```

Delete the `<div>` element that contains the QR code instructions and the two `<div>` elements where the QR code should appear and where the QR code data is stored in the page:

```
- <div class="alert alert-info">
-     Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable 
-     QR code generation</a>.
- </div>
- <div></div>
- <div data-url="@authenticatorUri"></div>
```

Replace the deleted elements with the following markup:

```
<div>
    <svg xmlns="http://www.w3.org/2000/svg" height="300" width="300" stroke="none" 
            version="1.1" viewBox="0 0 50 50">
        <rect width="300" height="300" fill="#ffffff" />
        <path d="@svgGraphicsPath" fill="#000000" />
    </svg>
</div>
```

Just inside the opening `@code` block, change the variable declaration for `authenticatorUri` to `svgGraphicsPath`:

```
- private string? authenticatorUri;
+ private string? svgGraphicsPath;
```

Change the site name in the `GenerateQrCodeUri` method. The default value is `Microsoft.AspNetCore.Identity.UI`. Change the value to a meaningful site name that users can identify easily in their authenticator app. Developers usually set a site name that matches the company's name. We recommend limiting the site name length to 30 characters or less to allow the site name to display on narrow mobile device screens.

In the following example, the default value `Microsoft.AspNetCore.Identity.UI` is changed to the company name `Weyland-Yutani Corporation` (©1986 20th Century Studios [*Aliens*](https://www.20thcenturystudios.com/movies/aliens)).

In the `GenerateQrCodeUri` method:

```
- UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"),
+ UrlEncoder.Encode("Weyland-Yutani Corporation"),
```

At the bottom of the `LoadSharedKeyAndQrCodeUriAsync` method, add the [`var` keyword](/en-us/dotnet/csharp/programming-guide/classes-and-structs/implicitly-typed-local-variables) to the line that sets `authenticatorUri`, making it an implicitly-typed local variable:

```
- authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!);
+ var authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!);
```

Add the following lines of code at the bottom of the `LoadSharedKeyAndQrCodeUriAsync` method:

```
var qr = QrCode.EncodeText(authenticatorUri, QrCode.Ecc.Medium);
svgGraphicsPath = qr.ToGraphicsPath();
```

Run the app and ensure that the QR code is scannable and that the code validates.

Warning

An ASP.NET Core TOTP code should be kept secret because it can be used to authenticate successfully multiple times before it expires.

## `EnableAuthenticator` component in reference source

The `EnableAuthenticator` component (`Components/Account/Pages/Manage/EnableAuthenticator.razor` in the server project) can be inspected in the [Blazor Web App project template (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

## Failures due to TOTP time skew

TOTP authentication depends on accurate time keeping on the TOTP authenticator app device and the app's host. TOTP tokens are only valid for 30 seconds. If logins are failing due to rejected TOTP codes, confirm accurate time is maintained, preferably synchronized to an accurate NTP service.

## Related content

* [Using a different QR code library](../../security/authentication/identity-enable-qrcodes?view=aspnetcore-10.0#using-a-different-qr-code-library)
* [TOTP client and server time skew](../../security/authentication/identity-enable-qrcodes?view=aspnetcore-10.0#totp-client-and-server-time-skew)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/metrics/diagnostics
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/metrics/diagnostics.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core built-in diagnostics metrics

This article describes the built-in diagnostics metrics for ASP.NET Core produced using the [System.Diagnostics.Metrics](/en-us/dotnet/api/system.diagnostics.metrics) API. These metrics report diagnostics information from the ASP.NET Core error handling middleware.

For an overview of all built-in metrics reference pages and how to read this reference, see [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0). For information on how to collect, report, enrich, and test with ASP.NET Core metrics, see [ASP.NET Core metrics](overview?view=aspnetcore-10.0).

## `Microsoft.AspNetCore.Diagnostics`

The `Microsoft.AspNetCore.Diagnostics` metrics report diagnostics information from [ASP.NET Core error handling middleware](/en-us/aspnet/core/fundamentals/error-handling):

* [`aspnetcore.diagnostics.exceptions`](#metric-aspnetcorediagnosticsexceptions)

### Metric: `aspnetcore.diagnostics.exceptions`

| Name                                                                                                                                                        | Instrument Type | Unit (UCUM)   | Description                                                   |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------- | ------------------------------------------------------------- |
| [`aspnetcore.diagnostics.exceptions`](https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-aspnetcore-metrics/#metric-aspnetcorediagnosticsexceptions) | Counter         | `{exception}` | Number of exceptions caught by exception handling middleware. |

| Attribute                                 | Type   | Description                                                                                                                                                  | Examples                                                   | Presence                                      |
| ----------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | --------------------------------------------- |
| `aspnetcore.diagnostics.exception.result` | string | ASP.NET Core exception middleware handling result.                                                                                                           | `handled`; `unhandled`                                     | Always                                        |
| `aspnetcore.diagnostics.handler.type`     | string | Full type name of the [`IExceptionHandler`](/en-us/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) implementation that handled the exception. | `Contoso.MyHandler`                                        | If the exception was handled by this handler. |
| `exception.type`                          | string | The full name of exception type.                                                                                                                             | `System.OperationCanceledException`; `Contoso.MyException` | Always                                        |

## Related content

* [ASP.NET Core built-in metrics](built-in?view=aspnetcore-10.0)
* [ASP.NET Core built-in HTTP metrics](http?view=aspnetcore-10.0)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/grpc/aspnetcore
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/grpc/aspnetcore.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# gRPC services with ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This document shows how to get started with gRPC services using ASP.NET Core.

## Get started with gRPC service in ASP.NET Core

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

See [Get started with gRPC services](../tutorials/grpc/grpc-start?view=aspnetcore-10.0) for detailed instructions on how to create a gRPC project.

Run `dotnet new grpc -o GrpcGreeter` from the command line.

## Add gRPC services to an ASP.NET Core app

gRPC requires the [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore) package.

### Configure gRPC

In `Program.cs`:

* gRPC is enabled with the `AddGrpc` method.
* Each gRPC service is added to the routing pipeline through the `MapGrpcService` method.

```
using GrpcGreeter.Services;

var builder = WebApplication.CreateBuilder(args);

// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

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

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");

app.Run();
```

ASP.NET Core middleware and features share the routing pipeline, therefore an app can be configured to serve additional request handlers. The additional request handlers, such as MVC controllers, work in parallel with the configured gRPC services.

## Server options

gRPC services can be hosted by all built-in ASP.NET Core servers.

* Kestrel
* TestServer
* IIS†
* HTTP.sys†

†Requires .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later.

For more information about choosing the right server for an ASP.NET Core app, see [Web server implementations in ASP.NET Core](../fundamentals/servers/?view=aspnetcore-10.0).

## Kestrel

[Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) is a cross-platform web server for ASP.NET Core. Kestrel focuses on high performance and memory utilization, but it doesn't have some of the advanced features in HTTP.sys such as port sharing.

Kestrel gRPC endpoints:

* Require HTTP/2.
* Should be secured with [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246).

### HTTP/2

gRPC requires HTTP/2. gRPC for ASP.NET Core validates [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) is `HTTP/2`.

Kestrel [supports HTTP/2](../fundamentals/servers/kestrel/http2?view=aspnetcore-10.0) on most modern operating systems. Kestrel endpoints are configured to support HTTP/1.1 and HTTP/2 connections by default.

### TLS

Kestrel endpoints used for gRPC should be secured with TLS. In development, an endpoint secured with TLS is automatically created at `https://localhost:5001` when the ASP.NET Core development certificate is present. No configuration is required. An `https` prefix verifies the Kestrel endpoint is using TLS.

In production, TLS must be explicitly configured. In the following `appsettings.json` example, an HTTP/2 endpoint secured with TLS is provided:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Protocols": "Http2",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "<certificate password>"
        }
      }
    }
  }
}
```

Alternatively, Kestrel endpoints can be configured in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(IPAddress.Any, 5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
        listenOptions.UseHttps("<path to .pfx file>",
            "<certificate password>");
    });
});
```

For more information on enabling TLS with Kestrel, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsusehttps).

### Protocol negotiation

TLS is used for more than securing communication. The TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake is used to negotiate the connection protocol between the client and the server when an endpoint supports multiple protocols. This negotiation determines whether the connection uses HTTP/1.1 or HTTP/2.

If an HTTP/2 endpoint is configured without TLS, the endpoint's [ListenOptions.Protocols](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsprotocols) must be set to `HttpProtocols.Http2`. An endpoint with multiple protocols, such as `HttpProtocols.Http1AndHttp2` for example, can't be used without TLS because there's no negotiation. All connections to the unsecured endpoint default to HTTP/1.1, and gRPC calls fail.

For more information on enabling HTTP/2 and TLS with Kestrel, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

macOS doesn't support ASP.NET Core gRPC with TLS before .NET 8. Additional configuration is required to successfully run gRPC services on macOS when using .NET 7 or earlier. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

## IIS

[Internet Information Services (IIS)](../host-and-deploy/iis/?view=aspnetcore-10.0) is a flexible, secure and manageable Web Server for hosting web apps, including ASP.NET Core. .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later are required to host gRPC services with IIS.

IIS must be configured to use TLS and HTTP/2. For more information, see [Use ASP.NET Core with HTTP/2 on IIS](../host-and-deploy/iis/protocols?view=aspnetcore-10.0).

## HTTP.sys

[HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) is a web server for ASP.NET Core that only runs on Windows. .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later are required to host gRPC services with HTTP.sys.

HTTP.sys must be configured to use TLS and HTTP/2. For more information, see [HTTP.sys web server HTTP/2 support](../fundamentals/servers/httpsys?view=aspnetcore-10.0#http2-support).

## Host gRPC in non-ASP.NET Core projects

An ASP.NET Core gRPC server is typically created from the gRPC template. The project file created by the template uses `Microsoft.NET.SDK.Web` as the SDK:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>
```

The `Microsoft.NET.SDK.Web` SDK value automatically adds a reference to the ASP.NET Core framework. The reference allows the app to use ASP.NET Core types required to host a server.

You can add a gRPC server to non-ASP.NET Core projects with the following project file settings:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
    
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

The preceding project file:

* Doesn't use `Microsoft.NET.SDK.Web` as the SDK.

* Adds a framework reference to `Microsoft.AspNetCore.App`.

  * The framework reference allows non-ASP.NET Core apps, such as Windows Services, WPF apps, or WinForms apps to use ASP.NET Core APIs.
  * The app can now use ASP.NET Core APIs to start an ASP.NET Core server.

* Adds gRPC requirements:

  * NuGet package reference to [`Grpc.AspNetCore`](https://www.nuget.org/packages/Grpc.AspNetCore).
  * `.proto` file.

For more information about using the `Microsoft.AspNetCore.App` framework reference, see [Use the ASP.NET Core shared framework](../fundamentals/target-aspnetcore?view=aspnetcore-10.0#use-the-aspnet-core-shared-framework).

## Integration with ASP.NET Core APIs

gRPC services have full access to the ASP.NET Core features such as [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) and [logging](../fundamentals/logging/?view=aspnetcore-10.0). For example, the service implementation can resolve a logger service from the DI container.

Constructor injection:

```
public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }
}
```

Primary constructor injection (.NET 8 or later):

```
public class GreeterService(ILogger<GreeterService> logger) : Greeter.GreeterBase
{
    ...
}
```

By default, the gRPC service implementation can resolve other DI services with any lifetime (Singleton, Scoped, or Transient).

### Resolve HttpContext in gRPC methods

The gRPC API provides access to some HTTP/2 message data, such as the method, host, header, and trailers. Access is through the `ServerCallContext` argument passed to each gRPC method:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}
```

`ServerCallContext` doesn't provide full access to `HttpContext` in all ASP.NET APIs. The `GetHttpContext` extension method provides full access to the `HttpContext` representing the underlying HTTP/2 message in ASP.NET APIs:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        var httpContext = context.GetHttpContext();
        var clientCertificate = httpContext.Connection.ClientCertificate;

        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name + " from " + clientCertificate.Issuer
        });
    }
}
```

## Related content

* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [Kestrel web server in ASP.NET Core](../fundamentals/servers/kestrel?view=aspnetcore-10.0)

This document shows how to get started with gRPC services using ASP.NET Core.

## Get started with gRPC service in ASP.NET Core

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

See [Get started with gRPC services](../tutorials/grpc/grpc-start?view=aspnetcore-10.0) for detailed instructions on how to create a gRPC project.

Run `dotnet new grpc -o GrpcGreeter` from the command line.

## Add gRPC services to an ASP.NET Core app

gRPC requires the [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore) package.

### Configure gRPC

In `Program.cs`:

* gRPC is enabled with the `AddGrpc` method.
* Each gRPC service is added to the routing pipeline through the `MapGrpcService` method.

```
using GrpcGreeter.Services;

var builder = WebApplication.CreateBuilder(args);

// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

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

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");

app.Run();
```

ASP.NET Core middleware and features share the routing pipeline, therefore an app can be configured to serve additional request handlers. The additional request handlers, such as MVC controllers, work in parallel with the configured gRPC services.

## Server options

gRPC services can be hosted by all built-in ASP.NET Core servers.

* Kestrel
* TestServer
* IIS†
* HTTP.sys†

†Requires .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later.

For more information about choosing the right server for an ASP.NET Core app, see [Web server implementations in ASP.NET Core](../fundamentals/servers/?view=aspnetcore-10.0).

## Kestrel

[Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) is a cross-platform web server for ASP.NET Core. Kestrel focuses on high performance and memory utilization, but it doesn't have some of the advanced features in HTTP.sys such as port sharing.

Kestrel gRPC endpoints:

* Require HTTP/2.
* Should be secured with [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246).

### HTTP/2

gRPC requires HTTP/2. gRPC for ASP.NET Core validates [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) is `HTTP/2`.

Kestrel [supports HTTP/2](../fundamentals/servers/kestrel/http2?view=aspnetcore-10.0) on most modern operating systems. Kestrel endpoints are configured to support HTTP/1.1 and HTTP/2 connections by default.

### TLS

Kestrel endpoints used for gRPC should be secured with TLS. In development, an endpoint secured with TLS is automatically created at `https://localhost:5001` when the ASP.NET Core development certificate is present. No configuration is required. An `https` prefix verifies the Kestrel endpoint is using TLS.

In production, TLS must be explicitly configured. In the following `appsettings.json` example, an HTTP/2 endpoint secured with TLS is provided:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Protocols": "Http2",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "<certificate password>"
        }
      }
    }
  }
}
```

Alternatively, Kestrel endpoints can be configured in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(IPAddress.Any, 5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
        listenOptions.UseHttps("<path to .pfx file>",
            "<certificate password>");
    });
});
```

For more information on enabling TLS with Kestrel, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsusehttps).

### Protocol negotiation

TLS is used for more than securing communication. The TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake is used to negotiate the connection protocol between the client and the server when an endpoint supports multiple protocols. This negotiation determines whether the connection uses HTTP/1.1 or HTTP/2.

If an HTTP/2 endpoint is configured without TLS, the endpoint's [ListenOptions.Protocols](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsprotocols) must be set to `HttpProtocols.Http2`. An endpoint with multiple protocols, such as `HttpProtocols.Http1AndHttp2` for example, can't be used without TLS because there's no negotiation. All connections to the unsecured endpoint default to HTTP/1.1, and gRPC calls fail.

For more information on enabling HTTP/2 and TLS with Kestrel, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

macOS doesn't support ASP.NET Core gRPC with TLS before .NET 8. Additional configuration is required to successfully run gRPC services on macOS when using .NET 7 or earlier. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

## IIS

[Internet Information Services (IIS)](../host-and-deploy/iis/?view=aspnetcore-10.0) is a flexible, secure and manageable Web Server for hosting web apps, including ASP.NET Core. .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later are required to host gRPC services with IIS.

IIS must be configured to use TLS and HTTP/2. For more information, see [Use ASP.NET Core with HTTP/2 on IIS](../host-and-deploy/iis/protocols?view=aspnetcore-10.0).

## HTTP.sys

[HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) is a web server for ASP.NET Core that only runs on Windows. .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later are required to host gRPC services with HTTP.sys.

HTTP.sys must be configured to use TLS and HTTP/2. For more information, see [HTTP.sys web server HTTP/2 support](../fundamentals/servers/httpsys?view=aspnetcore-10.0#http2-support).

## Host gRPC in non-ASP.NET Core projects

An ASP.NET Core gRPC server is typically created from the gRPC template. The project file created by the template uses `Microsoft.NET.SDK.Web` as the SDK:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>
```

The `Microsoft.NET.SDK.Web` SDK value automatically adds a reference to the ASP.NET Core framework. The reference allows the app to use ASP.NET Core types required to host a server.

You can add a gRPC server to non-ASP.NET Core projects with the following project file settings:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
    
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

The preceding project file:

* Doesn't use `Microsoft.NET.SDK.Web` as the SDK.

* Adds a framework reference to `Microsoft.AspNetCore.App`.

  * The framework reference allows non-ASP.NET Core apps, such as Windows Services, WPF apps, or WinForms apps to use ASP.NET Core APIs.
  * The app can now use ASP.NET Core APIs to start an ASP.NET Core server.

* Adds gRPC requirements:

  * NuGet package reference to [`Grpc.AspNetCore`](https://www.nuget.org/packages/Grpc.AspNetCore).
  * `.proto` file.

For more information about using the `Microsoft.AspNetCore.App` framework reference, see [Use the ASP.NET Core shared framework](../fundamentals/target-aspnetcore?view=aspnetcore-10.0#use-the-aspnet-core-shared-framework).

## Integration with ASP.NET Core APIs

gRPC services have full access to the ASP.NET Core features such as [Dependency Injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) and [Logging](../fundamentals/logging/?view=aspnetcore-10.0). For example, the service implementation can resolve a logger service from the DI container via the constructor:

```
public class GreeterService : Greeter.GreeterBase
{
    public GreeterService(ILogger<GreeterService> logger)
    {
    }
}
```

By default, the gRPC service implementation can resolve other DI services with any lifetime (Singleton, Scoped, or Transient).

### Resolve HttpContext in gRPC methods

The gRPC API provides access to some HTTP/2 message data, such as the method, host, header, and trailers. Access is through the `ServerCallContext` argument passed to each gRPC method:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}
```

`ServerCallContext` doesn't provide full access to `HttpContext` in all ASP.NET APIs. The `GetHttpContext` extension method provides full access to the `HttpContext` representing the underlying HTTP/2 message in ASP.NET APIs:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        var httpContext = context.GetHttpContext();
        var clientCertificate = httpContext.Connection.ClientCertificate;

        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name + " from " + clientCertificate.Issuer
        });
    }
}
```

## Related content

* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [Kestrel web server in ASP.NET Core](../fundamentals/servers/kestrel?view=aspnetcore-10.0)

This document shows how to get started with gRPC services using ASP.NET Core.

## Get started with gRPC service in ASP.NET Core

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

See [Get started with gRPC services](../tutorials/grpc/grpc-start?view=aspnetcore-10.0) for detailed instructions on how to create a gRPC project.

Run `dotnet new grpc -o GrpcGreeter` from the command line.

## Add gRPC services to an ASP.NET Core app

gRPC requires the [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore) package.

### Configure gRPC

In `Program.cs`:

* gRPC is enabled with the `AddGrpc` method.
* Each gRPC service is added to the routing pipeline through the `MapGrpcService` method.

```
using GrpcGreeter.Services;

var builder = WebApplication.CreateBuilder(args);

// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

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

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");

app.Run();
```

ASP.NET Core middleware and features share the routing pipeline, therefore an app can be configured to serve additional request handlers. The additional request handlers, such as MVC controllers, work in parallel with the configured gRPC services.

## Server options

gRPC services can be hosted by all built-in ASP.NET Core servers.

* Kestrel
* TestServer
* IIS†
* HTTP.sys†

†Requires .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later.

For more information about choosing the right server for an ASP.NET Core app, see [Web server implementations in ASP.NET Core](../fundamentals/servers/?view=aspnetcore-10.0).

## Kestrel

[Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) is a cross-platform web server for ASP.NET Core. Kestrel focuses on high performance and memory utilization, but it doesn't have some of the advanced features in HTTP.sys such as port sharing.

Kestrel gRPC endpoints:

* Require HTTP/2.
* Should be secured with [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246).

### HTTP/2

gRPC requires HTTP/2. gRPC for ASP.NET Core validates [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) is `HTTP/2`.

Kestrel [supports HTTP/2](../fundamentals/servers/kestrel/http2?view=aspnetcore-10.0) on most modern operating systems. Kestrel endpoints are configured to support HTTP/1.1 and HTTP/2 connections by default.

### TLS

Kestrel endpoints used for gRPC should be secured with TLS. In development, an endpoint secured with TLS is automatically created at `https://localhost:5001` when the ASP.NET Core development certificate is present. No configuration is required. An `https` prefix verifies the Kestrel endpoint is using TLS.

In production, TLS must be explicitly configured. In the following `appsettings.json` example, an HTTP/2 endpoint secured with TLS is provided:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Protocols": "Http2",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "<certificate password>"
        }
      }
    }
  }
}
```

Alternatively, Kestrel endpoints can be configured in `Program.cs`:

```
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.Listen(IPAddress.Any, 5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
        listenOptions.UseHttps("<path to .pfx file>",
            "<certificate password>");
    });
});
```

For more information on enabling TLS with Kestrel, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsusehttps).

### Protocol negotiation

TLS is used for more than securing communication. The TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake is used to negotiate the connection protocol between the client and the server when an endpoint supports multiple protocols. This negotiation determines whether the connection uses HTTP/1.1 or HTTP/2.

If an HTTP/2 endpoint is configured without TLS, the endpoint's [ListenOptions.Protocols](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsprotocols) must be set to `HttpProtocols.Http2`. An endpoint with multiple protocols, such as `HttpProtocols.Http1AndHttp2` for example, can't be used without TLS because there's no negotiation. All connections to the unsecured endpoint default to HTTP/1.1, and gRPC calls fail.

For more information on enabling HTTP/2 and TLS with Kestrel, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

macOS doesn't support ASP.NET Core gRPC with TLS before .NET 8. Additional configuration is required to successfully run gRPC services on macOS when using .NET 7 or earlier. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

## IIS

[Internet Information Services (IIS)](../host-and-deploy/iis/?view=aspnetcore-10.0) is a flexible, secure and manageable Web Server for hosting web apps, including ASP.NET Core. .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later are required to host gRPC services with IIS.

IIS must be configured to use TLS and HTTP/2. For more information, see [Use ASP.NET Core with HTTP/2 on IIS](../host-and-deploy/iis/protocols?view=aspnetcore-10.0).

## HTTP.sys

[HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) is a web server for ASP.NET Core that only runs on Windows. .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later are required to host gRPC services with HTTP.sys.

HTTP.sys must be configured to use TLS and HTTP/2. For more information, see [HTTP.sys web server HTTP/2 support](../fundamentals/servers/httpsys?view=aspnetcore-10.0#http2-support).

## Host gRPC in non-ASP.NET Core projects

An ASP.NET Core gRPC server is typically created from the gRPC template. The project file created by the template uses `Microsoft.NET.SDK.Web` as the SDK:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>
```

The `Microsoft.NET.SDK.Web` SDK value automatically adds a reference to the ASP.NET Core framework. The reference allows the app to use ASP.NET Core types required to host a server.

You can add a gRPC server to non-ASP.NET Core projects with the following project file settings:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
    
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

The preceding project file:

* Doesn't use `Microsoft.NET.SDK.Web` as the SDK.

* Adds a framework reference to `Microsoft.AspNetCore.App`.

  * The framework reference allows non-ASP.NET Core apps, such as Windows Services, WPF apps, or WinForms apps to use ASP.NET Core APIs.
  * The app can now use ASP.NET Core APIs to start an ASP.NET Core server.

* Adds gRPC requirements:

  * NuGet package reference to [`Grpc.AspNetCore`](https://www.nuget.org/packages/Grpc.AspNetCore).
  * `.proto` file.

For more information about using the `Microsoft.AspNetCore.App` framework reference, see [Use the ASP.NET Core shared framework](../fundamentals/target-aspnetcore?view=aspnetcore-10.0#use-the-aspnet-core-shared-framework).

## Integration with ASP.NET Core APIs

gRPC services have full access to the ASP.NET Core features such as [Dependency Injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) and [Logging](../fundamentals/logging/?view=aspnetcore-10.0). For example, the service implementation can resolve a logger service from the DI container via the constructor:

```
public class GreeterService : Greeter.GreeterBase
{
    public GreeterService(ILogger<GreeterService> logger)
    {
    }
}
```

By default, the gRPC service implementation can resolve other DI services with any lifetime (Singleton, Scoped, or Transient).

### Resolve HttpContext in gRPC methods

The gRPC API provides access to some HTTP/2 message data, such as the method, host, header, and trailers. Access is through the `ServerCallContext` argument passed to each gRPC method:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}
```

`ServerCallContext` doesn't provide full access to `HttpContext` in all ASP.NET APIs. The `GetHttpContext` extension method provides full access to the `HttpContext` representing the underlying HTTP/2 message in ASP.NET APIs:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        var httpContext = context.GetHttpContext();
        var clientCertificate = httpContext.Connection.ClientCertificate;

        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name + " from " + clientCertificate.Issuer
        });
    }
}
```

## Related content

* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [Kestrel web server in ASP.NET Core](../fundamentals/servers/kestrel?view=aspnetcore-10.0)

This document shows how to get started with gRPC services using ASP.NET Core.

## Get started with gRPC service in ASP.NET Core

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

See [Get started with gRPC services](../tutorials/grpc/grpc-start?view=aspnetcore-10.0) for detailed instructions on how to create a gRPC project.

Run `dotnet new grpc -o GrpcGreeter` from the command line.

## Add gRPC services to an ASP.NET Core app

gRPC requires the [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore) package.

### Configure gRPC

In `Startup.cs`:

* gRPC is enabled with the `AddGrpc` method.
* Each gRPC service is added to the routing pipeline through the `MapGrpcService` method.

```
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            // Communication with gRPC endpoints must be made through a gRPC client.
            // To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909
            endpoints.MapGrpcService<GreeterService>();
        });
    }
}
```

ASP.NET Core middleware and features share the routing pipeline, therefore an app can be configured to serve additional request handlers. The additional request handlers, such as MVC controllers, work in parallel with the configured gRPC services.

## Server options

gRPC services can be hosted by all built-in ASP.NET Core servers.

* Kestrel
* TestServer
* IIS†
* HTTP.sys†

†Requires .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later.

For more information about choosing the right server for an ASP.NET Core app, see [Web server implementations in ASP.NET Core](../fundamentals/servers/?view=aspnetcore-10.0).

## Kestrel

[Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) is a cross-platform web server for ASP.NET Core. Kestrel focuses on high performance and memory utilization, but it doesn't have some of the advanced features in HTTP.sys such as port sharing.

Kestrel gRPC endpoints:

* Require HTTP/2.
* Should be secured with [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246).

### HTTP/2

gRPC requires HTTP/2. gRPC for ASP.NET Core validates [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) is `HTTP/2`.

Kestrel [supports HTTP/2](../fundamentals/servers/kestrel/http2?view=aspnetcore-10.0) on most modern operating systems. Kestrel endpoints are configured to support HTTP/1.1 and HTTP/2 connections by default.

### TLS

Kestrel endpoints used for gRPC should be secured with TLS. In development, an endpoint secured with TLS is automatically created at `https://localhost:5001` when the ASP.NET Core development certificate is present. No configuration is required. An `https` prefix verifies the Kestrel endpoint is using TLS.

In production, TLS must be explicitly configured. In the following `appsettings.json` example, an HTTP/2 endpoint secured with TLS is provided:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Protocols": "Http2",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "<certificate password>"
        }
      }
    }
  }
}
```

Alternatively, Kestrel endpoints can be configured in `Program.cs`:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(options =>
            {
                options.Listen(IPAddress.Any, 5001, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http2;
                    listenOptions.UseHttps("<path to .pfx file>", 
                        "<certificate password>");
                });
            });
            webBuilder.UseStartup<Startup>();
        });
```

For more information on enabling TLS with Kestrel, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsusehttps).

### Protocol negotiation

TLS is used for more than securing communication. The TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake is used to negotiate the connection protocol between the client and the server when an endpoint supports multiple protocols. This negotiation determines whether the connection uses HTTP/1.1 or HTTP/2.

If an HTTP/2 endpoint is configured without TLS, the endpoint's [ListenOptions.Protocols](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0#listenoptionsprotocols) must be set to `HttpProtocols.Http2`. An endpoint with multiple protocols, such as `HttpProtocols.Http1AndHttp2` for example, can't be used without TLS because there's no negotiation. All connections to the unsecured endpoint default to HTTP/1.1, and gRPC calls fail.

For more information on enabling HTTP/2 and TLS with Kestrel, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

macOS doesn't support ASP.NET Core gRPC with TLS before .NET 8. Additional configuration is required to successfully run gRPC services on macOS when using .NET 7 or earlier. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

## IIS

[Internet Information Services (IIS)](../host-and-deploy/iis/?view=aspnetcore-10.0) is a flexible, secure and manageable Web Server for hosting web apps, including ASP.NET Core. .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later are required to host gRPC services with IIS.

IIS must be configured to use TLS and HTTP/2. For more information, see [Use ASP.NET Core with HTTP/2 on IIS](../host-and-deploy/iis/protocols?view=aspnetcore-10.0).

## HTTP.sys

[HTTP.sys](../fundamentals/servers/httpsys?view=aspnetcore-10.0) is a web server for ASP.NET Core that only runs on Windows. .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later are required to host gRPC services with HTTP.sys.

HTTP.sys must be configured to use TLS and HTTP/2. For more information, see [HTTP.sys web server HTTP/2 support](../fundamentals/servers/httpsys?view=aspnetcore-10.0#http2-support).

## Host gRPC in non-ASP.NET Core projects

An ASP.NET Core gRPC server is typically created from the gRPC template. The project file created by the template uses `Microsoft.NET.SDK.Web` as the SDK:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>
```

The `Microsoft.NET.SDK.Web` SDK value automatically adds a reference to the ASP.NET Core framework. The reference allows the app to use ASP.NET Core types required to host a server.

You can add a gRPC server to non-ASP.NET Core projects with the following project file settings:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
    
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

The preceding project file:

* Doesn't use `Microsoft.NET.SDK.Web` as the SDK.

* Adds a framework reference to `Microsoft.AspNetCore.App`.

  * The framework reference allows non-ASP.NET Core apps, such as Windows Services, WPF apps, or WinForms apps to use ASP.NET Core APIs.
  * The app can now use ASP.NET Core APIs to start an ASP.NET Core server.

* Adds gRPC requirements:

  * NuGet package reference to [`Grpc.AspNetCore`](https://www.nuget.org/packages/Grpc.AspNetCore).
  * `.proto` file.

For more information about using the `Microsoft.AspNetCore.App` framework reference, see [Use the ASP.NET Core shared framework](../fundamentals/target-aspnetcore?view=aspnetcore-10.0#use-the-aspnet-core-shared-framework).

## Integration with ASP.NET Core APIs

gRPC services have full access to the ASP.NET Core features such as [Dependency Injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) and [Logging](../fundamentals/logging/?view=aspnetcore-10.0). For example, the service implementation can resolve a logger service from the DI container via the constructor:

```
public class GreeterService : Greeter.GreeterBase
{
    public GreeterService(ILogger<GreeterService> logger)
    {
    }
}
```

By default, the gRPC service implementation can resolve other DI services with any lifetime (Singleton, Scoped, or Transient).

### Resolve HttpContext in gRPC methods

The gRPC API provides access to some HTTP/2 message data, such as the method, host, header, and trailers. Access is through the `ServerCallContext` argument passed to each gRPC method:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}
```

`ServerCallContext` doesn't provide full access to `HttpContext` in all ASP.NET APIs. The `GetHttpContext` extension method provides full access to the `HttpContext` representing the underlying HTTP/2 message in ASP.NET APIs:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        var httpContext = context.GetHttpContext();
        var clientCertificate = httpContext.Connection.ClientCertificate;

        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name + " from " + clientCertificate.Issuer
        });
    }
}
```

## Related content

* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [Kestrel web server in ASP.NET Core](../fundamentals/servers/kestrel?view=aspnetcore-10.0)

This document shows how to get started with gRPC services using ASP.NET Core.

## Get started with gRPC service in ASP.NET Core

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

See [Get started with gRPC services](../tutorials/grpc/grpc-start?view=aspnetcore-10.0) for detailed instructions on how to create a gRPC project.

Run `dotnet new grpc -o GrpcGreeter` from the command line.

## Add gRPC services to an ASP.NET Core app

gRPC requires the [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore) package.

### Configure gRPC

In `Startup.cs`:

* gRPC is enabled with the `AddGrpc` method.
* Each gRPC service is added to the routing pipeline through the `MapGrpcService` method.

```
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            // Communication with gRPC endpoints must be made through a gRPC client.
            // To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909
            endpoints.MapGrpcService<GreeterService>();
        });
    }
}
```

ASP.NET Core middleware and features share the routing pipeline, therefore an app can be configured to serve additional request handlers. The additional request handlers, such as MVC controllers, work in parallel with the configured gRPC services.

## Server options

gRPC services can be hosted by all built-in ASP.NET Core servers.

* Kestrel
* TestServer
* IIS†
* HTTP.sys†

†Requires .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later.

For more information about choosing the right server for an ASP.NET Core app, see [Web server implementations in ASP.NET Core](../fundamentals/servers/?view=aspnetcore-10.0).

## Kestrel

[Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) is a cross-platform web server for ASP.NET Core. Kestrel focuses on high performance and memory utilization, but it doesn't have some of the advanced features in HTTP.sys such as port sharing.

Kestrel gRPC endpoints:

* Require HTTP/2.
* Should be secured with [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246).

### HTTP/2

gRPC requires HTTP/2. gRPC for ASP.NET Core validates [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) is `HTTP/2`.

Kestrel [supports HTTP/2](../fundamentals/servers/kestrel?view=aspnetcore-10.0#http2-support) on most modern operating systems. Kestrel endpoints are configured to support HTTP/1.1 and HTTP/2 connections by default.

### TLS

Kestrel endpoints used for gRPC should be secured with TLS. In development, an endpoint secured with TLS is automatically created at `https://localhost:5001` when the ASP.NET Core development certificate is present. No configuration is required. An `https` prefix verifies the Kestrel endpoint is using TLS.

In production, TLS must be explicitly configured. In the following `appsettings.json` example, an HTTP/2 endpoint secured with TLS is provided:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Protocols": "Http2",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "<certificate password>"
        }
      }
    }
  }
}
```

Alternatively, Kestrel endpoints can be configured in `Program.cs`:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(options =>
            {
                options.Listen(IPAddress.Any, 5001, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http2;
                    listenOptions.UseHttps("<path to .pfx file>", 
                        "<certificate password>");
                });
            });
            webBuilder.UseStartup<Startup>();
        });
```

For more information on enabling TLS with Kestrel, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#listenoptionsusehttps).

### Protocol negotiation

TLS is used for more than securing communication. The TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake is used to negotiate the connection protocol between the client and the server when an endpoint supports multiple protocols. This negotiation determines whether the connection uses HTTP/1.1 or HTTP/2.

If an HTTP/2 endpoint is configured without TLS, the endpoint's [ListenOptions.Protocols](../fundamentals/servers/kestrel?view=aspnetcore-10.0#listenoptionsprotocols) must be set to `HttpProtocols.Http2`. An endpoint with multiple protocols, such as `HttpProtocols.Http1AndHttp2` for example, can't be used without TLS because there's no negotiation. All connections to the unsecured endpoint default to HTTP/1.1, and gRPC calls fail.

For more information on enabling HTTP/2 and TLS with Kestrel, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration).

Note

macOS doesn't support ASP.NET Core gRPC with TLS before .NET 8. Additional configuration is required to successfully run gRPC services on macOS when using .NET 7 or earlier. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

## Host gRPC in non-ASP.NET Core projects

An ASP.NET Core gRPC server is typically created from the gRPC template. The project file created by the template uses `Microsoft.NET.SDK.Web` as the SDK:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>
```

The `Microsoft.NET.SDK.Web` SDK value automatically adds a reference to the ASP.NET Core framework. The reference allows the app to use ASP.NET Core types required to host a server.

You can add a gRPC server to non-ASP.NET Core projects with the following project file settings:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
    
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

The preceding project file:

* Doesn't use `Microsoft.NET.SDK.Web` as the SDK.

* Adds a framework reference to `Microsoft.AspNetCore.App`.

  * The framework reference allows non-ASP.NET Core apps, such as Windows Services, WPF apps, or WinForms apps to use ASP.NET Core APIs.
  * The app can now use ASP.NET Core APIs to start an ASP.NET Core server.

* Adds gRPC requirements:

  * NuGet package reference to [`Grpc.AspNetCore`](https://www.nuget.org/packages/Grpc.AspNetCore).
  * `.proto` file.

For more information about using the `Microsoft.AspNetCore.App` framework reference, see [Use the ASP.NET Core shared framework](../fundamentals/target-aspnetcore?view=aspnetcore-10.0#use-the-aspnet-core-shared-framework).

## Integration with ASP.NET Core APIs

gRPC services have full access to the ASP.NET Core features such as [Dependency Injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) and [Logging](../fundamentals/logging/?view=aspnetcore-10.0). For example, the service implementation can resolve a logger service from the DI container via the constructor:

```
public class GreeterService : Greeter.GreeterBase
{
    public GreeterService(ILogger<GreeterService> logger)
    {
    }
}
```

By default, the gRPC service implementation can resolve other DI services with any lifetime (Singleton, Scoped, or Transient).

### Resolve HttpContext in gRPC methods

The gRPC API provides access to some HTTP/2 message data, such as the method, host, header, and trailers. Access is through the `ServerCallContext` argument passed to each gRPC method:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}
```

`ServerCallContext` doesn't provide full access to `HttpContext` in all ASP.NET APIs. The `GetHttpContext` extension method provides full access to the `HttpContext` representing the underlying HTTP/2 message in ASP.NET APIs:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        var httpContext = context.GetHttpContext();
        var clientCertificate = httpContext.Connection.ClientCertificate;

        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name + " from " + clientCertificate.Issuer
        });
    }
}
```

## Related content

* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [Kestrel web server in ASP.NET Core](../fundamentals/servers/kestrel?view=aspnetcore-10.0)

This document shows how to get started with gRPC services using ASP.NET Core.

## Get started with gRPC service in ASP.NET Core

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/grpc/grpc-start/sample) ([how to download](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_2_visual-studio-code+visual-studio-mac)

See [Get started with gRPC services](../tutorials/grpc/grpc-start?view=aspnetcore-10.0) for detailed instructions on how to create a gRPC project.

Run `dotnet new grpc -o GrpcGreeter` from the command line.

## Add gRPC services to an ASP.NET Core app

gRPC requires the [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore) package.

### Configure gRPC

In `Startup.cs`:

* gRPC is enabled with the `AddGrpc` method.
* Each gRPC service is added to the routing pipeline through the `MapGrpcService` method.

```
public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            // Communication with gRPC endpoints must be made through a gRPC client.
            // To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909
            endpoints.MapGrpcService<GreeterService>();
        });
    }
}
```

ASP.NET Core middleware and features share the routing pipeline, therefore an app can be configured to serve additional request handlers. The additional request handlers, such as MVC controllers, work in parallel with the configured gRPC services.

## Server options

gRPC services can be hosted by all built-in ASP.NET Core servers.

* Kestrel
* TestServer
* IIS†
* HTTP.sys†

†Requires .NET 5 and Windows 11 Build 22000 or Windows Server 2022 Build 20348 or later.

For more information about choosing the right server for an ASP.NET Core app, see [Web server implementations in ASP.NET Core](../fundamentals/servers/?view=aspnetcore-10.0).

## Kestrel

[Kestrel](../fundamentals/servers/kestrel?view=aspnetcore-10.0) is a cross-platform web server for ASP.NET Core. Kestrel focuses on high performance and memory utilization, but it doesn't have some of the advanced features in HTTP.sys such as port sharing.

Kestrel gRPC endpoints:

* Require HTTP/2.
* Should be secured with [Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc5246).

### HTTP/2

gRPC requires HTTP/2. gRPC for ASP.NET Core validates [HttpRequest.Protocol](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.protocol) is `HTTP/2`.

Kestrel [supports HTTP/2](../fundamentals/servers/kestrel?view=aspnetcore-10.0#http2-support) on most modern operating systems. Kestrel endpoints are configured to support HTTP/1.1 and HTTP/2 connections by default.

### TLS

Kestrel endpoints used for gRPC should be secured with TLS. In development, an endpoint secured with TLS is automatically created at `https://localhost:5001` when the ASP.NET Core development certificate is present. No configuration is required. An `https` prefix verifies the Kestrel endpoint is using TLS.

In production, TLS must be explicitly configured. In the following `appsettings.json` example, an HTTP/2 endpoint secured with TLS is provided:

```
{
  "Kestrel": {
    "Endpoints": {
      "HttpsInlineCertFile": {
        "Url": "https://localhost:5001",
        "Protocols": "Http2",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "<certificate password>"
        }
      }
    }
  }
}
```

Alternatively, Kestrel endpoints can be configured in `Program.cs`:

```
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(options =>
            {
                options.Listen(IPAddress.Any, 5001, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http2;
                    listenOptions.UseHttps("<path to .pfx file>", 
                        "<certificate password>");
                });
            });
            webBuilder.UseStartup<Startup>();
        });
```

For more information on enabling TLS with Kestrel, see [Kestrel HTTPS endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#listenoptionsusehttps).

### Protocol negotiation

TLS is used for more than securing communication. The TLS [Application-Layer Protocol Negotiation (ALPN)](https://tools.ietf.org/html/rfc7301#section-3) handshake is used to negotiate the connection protocol between the client and the server when an endpoint supports multiple protocols. This negotiation determines whether the connection uses HTTP/1.1 or HTTP/2.

If an HTTP/2 endpoint is configured without TLS, the endpoint's [ListenOptions.Protocols](../fundamentals/servers/kestrel?view=aspnetcore-10.0#listenoptionsprotocols) must be set to `HttpProtocols.Http2`. An endpoint with multiple protocols, such as `HttpProtocols.Http1AndHttp2` for example, can't be used without TLS because there's no negotiation. All connections to the unsecured endpoint default to HTTP/1.1, and gRPC calls fail.

For more information on enabling HTTP/2 and TLS with Kestrel, see [Kestrel endpoint configuration](../fundamentals/servers/kestrel?view=aspnetcore-10.0#endpoint-configuration).

Note

macOS doesn't support ASP.NET Core gRPC with TLS before .NET 8. Additional configuration is required to successfully run gRPC services on macOS when using .NET 7 or earlier. For more information, see [Unable to start ASP.NET Core gRPC app on macOS](troubleshoot?view=aspnetcore-10.0#unable-to-start-aspnet-core-grpc-app-on-macos).

## Host gRPC in non-ASP.NET Core projects

An ASP.NET Core gRPC server is typically created from the gRPC template. The project file created by the template uses `Microsoft.NET.SDK.Web` as the SDK:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>
```

The `Microsoft.NET.SDK.Web` SDK value automatically adds a reference to the ASP.NET Core framework. The reference allows the app to use ASP.NET Core types required to host a server.

You can add a gRPC server to non-ASP.NET Core projects with the following project file settings:

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

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
    
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>
```

The preceding project file:

* Doesn't use `Microsoft.NET.SDK.Web` as the SDK.

* Adds a framework reference to `Microsoft.AspNetCore.App`.

  * The framework reference allows non-ASP.NET Core apps, such as Windows Services, WPF apps, or WinForms apps to use ASP.NET Core APIs.
  * The app can now use ASP.NET Core APIs to start an ASP.NET Core server.

* Adds gRPC requirements:

  * NuGet package reference to [`Grpc.AspNetCore`](https://www.nuget.org/packages/Grpc.AspNetCore).
  * `.proto` file.

For more information about using the `Microsoft.AspNetCore.App` framework reference, see [Use the ASP.NET Core shared framework](../fundamentals/target-aspnetcore?view=aspnetcore-10.0#use-the-aspnet-core-shared-framework).

## Integration with ASP.NET Core APIs

gRPC services have full access to the ASP.NET Core features such as [Dependency Injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) and [Logging](../fundamentals/logging/?view=aspnetcore-10.0). For example, the service implementation can resolve a logger service from the DI container via the constructor:

```
public class GreeterService : Greeter.GreeterBase
{
    public GreeterService(ILogger<GreeterService> logger)
    {
    }
}
```

By default, the gRPC service implementation can resolve other DI services with any lifetime (Singleton, Scoped, or Transient).

### Resolve HttpContext in gRPC methods

The gRPC API provides access to some HTTP/2 message data, such as the method, host, header, and trailers. Access is through the `ServerCallContext` argument passed to each gRPC method:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}
```

`ServerCallContext` doesn't provide full access to `HttpContext` in all ASP.NET APIs. The `GetHttpContext` extension method provides full access to the `HttpContext` representing the underlying HTTP/2 message in ASP.NET APIs:

```
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(
        HelloRequest request, ServerCallContext context)
    {
        var httpContext = context.GetHttpContext();
        var clientCertificate = httpContext.Connection.ClientCertificate;

        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name + " from " + clientCertificate.Issuer
        });
    }
}
```

## Related content

* [Create a .NET gRPC client and server in ASP.NET Core](../tutorials/grpc/grpc-start?view=aspnetcore-10.0)
* [Overview for gRPC on .NET](./?view=aspnetcore-10.0)
* [gRPC Services with C#](basics?view=aspnetcore-10.0)
* [Kestrel web server in ASP.NET Core](../fundamentals/servers/kestrel?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/messagepackhubprotocol.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use MessagePack Hub Protocol in SignalR for ASP.NET Core

This article assumes the reader is familiar with the topics covered in [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0).

## What is MessagePack?

[MessagePack](https://msgpack.org/index.html) is a fast and compact binary serialization format. It's useful when performance and bandwidth are a concern because it creates smaller messages than [JSON](https://www.json.org/). The binary messages are unreadable when looking at network traces and logs unless the bytes are passed through a MessagePack parser. SignalR has built-in support for the MessagePack format and provides APIs for the client and server to use.

## Configure MessagePack on the server

To enable the MessagePack Hub Protocol on the server, install the `Microsoft.AspNetCore.SignalR.Protocols.MessagePack` package in your app. In the `Startup.ConfigureServices` method, add `AddMessagePackProtocol` to the `AddSignalR` call to enable MessagePack support on the server.

```
services.AddSignalR()
    .AddMessagePackProtocol();
```

Note

JSON is enabled by default. Adding MessagePack enables support for both JSON and MessagePack clients.

To customize how MessagePack formats data, `AddMessagePackProtocol` takes a delegate for configuring options. In that delegate, the `SerializerOptions` property is used to configure MessagePack serialization options. For more information on how the resolvers work, visit the MessagePack library at [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp). Attributes can be used on the objects you want to serialize to define how they should be handled.

```
services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.SerializerOptions = MessagePackSerializerOptions.Standard
            .WithResolver(new CustomResolver())
            .WithSecurity(MessagePackSecurity.UntrustedData);
    });
```

Warning

We strongly recommend reviewing [CVE-2020-5234](https://github.com/neuecc/MessagePack-CSharp/security/advisories/GHSA-7q36-4xx7-xcxf) and applying the recommended patches. For example, calling `.WithSecurity(MessagePackSecurity.UntrustedData)` when replacing the `SerializerOptions`.

## Configure MessagePack on the client

Note

JSON is enabled by default for the supported clients. Clients can only support a single protocol. Adding MessagePack support replaces any previously configured protocols.

### .NET client

To enable MessagePack in the .NET Client, install the `Microsoft.AspNetCore.SignalR.Protocols.MessagePack` package and call `AddMessagePackProtocol` on `HubConnectionBuilder`.

```
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

var hubConnection = new HubConnectionBuilder()
                        .WithUrl("/chathub")
                        .AddMessagePackProtocol()
                        .Build();
```

Note

This `AddMessagePackProtocol` call takes a delegate for configuring options just like the server.

### JavaScript client

MessagePack support for the JavaScript client is provided by the [@microsoft/signalr-protocol-msgpack](https://www.npmjs.com/package/@microsoft/signalr-protocol-msgpack) npm package. Install the package by executing the following command in a command shell:

```
npm install @microsoft/signalr-protocol-msgpack
```

After installing the npm package, the module can be used directly via a JavaScript module loader or imported into the browser by referencing the following file:

*node\_modules\\@microsoft\signalr-protocol-msgpack\dist\browser\signalr-protocol-msgpack.js*

The following required javaScript files must be referenced in the order shown below:

```
<script src="~/lib/signalr/signalr.js"></script>
<script src="~/lib/signalr/signalr-protocol-msgpack.js"></script>
```

Adding `.withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())` to the `HubConnectionBuilder` configures the client to use the MessagePack protocol when connecting to a server.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
    .build();
```

At this time, there are no configuration options for the MessagePack protocol on the JavaScript client.

### Java client

To enable MessagePack with Java, install the `com.microsoft.signalr.messagepack` package. When using Gradle, add the following line to the `dependencies` section of the *build.gradle* file:

```
implementation 'com.microsoft.signalr.messagepack:signalr-messagepack:5.0.0'
```

When using Maven, add the following lines inside the `<dependencies>` element of the `pom.xml` file:

```
<dependency>
    <groupId>com.microsoft.signalr.messagepack</groupId>
    <artifactId>signalr</artifactId>
    <version>5.0.0</version>
</dependency>
```

Call `withHubProtocol(new MessagePackHubProtocol())` on `HubConnectionBuilder`.

```
HubConnection messagePackConnection = HubConnectionBuilder.create("YOUR HUB URL HERE")
    .withHubProtocol(new MessagePackHubProtocol())
    .build();
```

## MessagePack considerations

There are a few issues to be aware of when using the MessagePack Hub Protocol.

### MessagePack is case-sensitive

The MessagePack protocol is case-sensitive. For example, consider the following C# class:

```
public class ChatMessage
{
    public string Sender { get; }
    public string Message { get; }
}
```

When sending from the JavaScript client, you must use `PascalCased` property names, since the casing must match the C# class exactly. For example:

```
connection.invoke("SomeMethod", { Sender: "Sally", Message: "Hello!" });
```

Using `camelCased` names won't properly bind to the C# class. You can work around this by using the `Key` attribute to specify a different name for the MessagePack property. For more information, see [the MessagePack-CSharp documentation](https://github.com/neuecc/MessagePack-CSharp#object-serialization).

### DateTime.Kind is not preserved when serializing/deserializing

The MessagePack protocol doesn't provide a way to encode the `Kind` value of a `DateTime`. As a result, when deserializing a date, the MessagePack Hub Protocol will convert to the UTC format if the `DateTime.Kind` is `DateTimeKind.Local` otherwise it will not touch the time and pass it as is. If you're working with `DateTime` values, we recommend converting to UTC before sending them. Convert them from UTC to local time when you receive them.

### MessagePack support in "ahead-of-time" compilation environment

The [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp/tree/v2.1.90) library used by the .NET client and server uses code generation to optimize serialization. As a result, it isn't supported by default on environments that use "ahead-of-time" compilation, such as NET Multi-platform App UI (.NET MAUI) or Unity. It's possible to use MessagePack in these environments by "pre-generating" the serializer/deserializer code. For more information, see [the MessagePack-CSharp documentation](https://github.com/neuecc/MessagePack-CSharp/tree/v2.1.90#aot-code-generation-to-support-unityxamarin). Once you have pre-generated the serializers, you can register them using the configuration delegate passed to `AddMessagePackProtocol`:

```
services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        StaticCompositeResolver.Instance.Register(
            MessagePack.Resolvers.GeneratedResolver.Instance,
            MessagePack.Resolvers.StandardResolver.Instance
        );
        options.SerializerOptions = MessagePackSerializerOptions.Standard
            .WithResolver(StaticCompositeResolver.Instance)
            .WithSecurity(MessagePackSecurity.UntrustedData);
    });
```

### Type checks are more strict in MessagePack

The JSON Hub Protocol will perform type conversions during deserialization. For example, if the incoming object has a property value that is a number (`{ foo: 42 }`) but the property on the .NET class is of type `string`, the value will be converted. However, MessagePack doesn't perform this conversion and will throw an exception that can be seen in server-side logs (and in the console):

```
InvalidDataException: Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.
```

For more information on this limitation, see GitHub issue [aspnet/SignalR#2937](https://github.com/aspnet/SignalR/issues/2937).

### Chars and Strings in Java

In the java client, `char` objects will be serialized as one-character `String` objects. This is in contrast with the C# and JavaScript client, which serialize them as `short` objects. The MessagePack spec itself does not define behavior for `char` objects, so it is up to the library author to determine how to serialize them. The difference in behavior between our clients is a result of the libraries we used for our implementations.

### Android device support

Depending on the MessagePack version targeted by the .NET release, the MessagePack integration can increase the minimum Android API level. For example, `signalr-messagepack` 8.0.24 resolves `jackson-databind` 2.18, raising the minimum Android API level from 21 to 26. If you need to support older Android devices than a given MessagePack integration permits, use the default JSON protocol, which doesn't have this MessagePack dependency constraint. For more information, see [\[SignalR Java client\]: signalr-messagepack 8.0.24 drops Android 7 support in a patch release (`dotnet/aspnetcore` #68893)](https://github.com/dotnet/aspnetcore/issues/68893).

## Additional resources

* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)

This article assumes the reader is familiar with the topics covered in [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0).

## What is MessagePack?

[MessagePack](https://msgpack.org/index.html) is a fast and compact binary serialization format. It's useful when performance and bandwidth are a concern because it creates smaller messages compared to [JSON](https://www.json.org/). The binary messages are unreadable when looking at network traces and logs unless the bytes are passed through a MessagePack parser. SignalR has built-in support for the MessagePack format and provides APIs for the client and server to use.

## Configure MessagePack on the server

To enable the MessagePack Hub Protocol on the server, install the `Microsoft.AspNetCore.SignalR.Protocols.MessagePack` package in your app. In the `Startup.ConfigureServices` method, add `AddMessagePackProtocol` to the `AddSignalR` call to enable MessagePack support on the server.

Note

JSON is enabled by default. Adding MessagePack enables support for both JSON and MessagePack clients.

```
services.AddSignalR()
    .AddMessagePackProtocol();
```

To customize how MessagePack will format your data, `AddMessagePackProtocol` takes a delegate for configuring options. In that delegate, the `SerializerOptions` property can be used to configure MessagePack serialization options. For more information on how the resolvers work, visit the MessagePack library at [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp). Attributes can be used on the objects you want to serialize to define how they should be handled.

```
services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.SerializerOptions = MessagePackSerializerOptions.Standard
            .WithResolver(new CustomResolver())
            .WithSecurity(MessagePackSecurity.UntrustedData);
    });
```

Warning

We strongly recommend reviewing [CVE-2020-5234](https://github.com/neuecc/MessagePack-CSharp/security/advisories/GHSA-7q36-4xx7-xcxf) and applying the recommended patches. For example, calling `.WithSecurity(MessagePackSecurity.UntrustedData)` when replacing the `SerializerOptions`.

## Configure MessagePack on the client

Note

JSON is enabled by default for the supported clients. Clients can only support a single protocol. Adding MessagePack support will replace any previously configured protocols.

### .NET client

To enable MessagePack in the .NET Client, install the `Microsoft.AspNetCore.SignalR.Protocols.MessagePack` package and call `AddMessagePackProtocol` on `HubConnectionBuilder`.

```
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

var hubConnection = new HubConnectionBuilder()
                        .WithUrl("/chathub")
                        .AddMessagePackProtocol()
                        .Build();
```

Note

This `AddMessagePackProtocol` call takes a delegate for configuring options just like the server.

### JavaScript client

MessagePack support for the JavaScript client is provided by the [@microsoft/signalr-protocol-msgpack](https://www.npmjs.com/package/@microsoft/signalr-protocol-msgpack) npm package. Install the package by executing the following command in a command shell:

```
npm install @microsoft/signalr-protocol-msgpack
```

After installing the npm package, the module can be used directly via a JavaScript module loader or imported into the browser by referencing the following file:

*node\_modules\\@microsoft\signalr-protocol-msgpack\dist\browser\signalr-protocol-msgpack.js*

In a browser, the `msgpack5` library must also be referenced. Use a `<script>` tag to create a reference. The library can be found at *node\_modules\msgpack5\dist\msgpack5.js*.

Note

When using the `<script>` element, the order is important. If `signalr-protocol-msgpack.js` is referenced before `msgpack5.js`, an error occurs when trying to connect with MessagePack. `signalr.js` is also required before `signalr-protocol-msgpack.js`.

```
<script src="~/lib/signalr/signalr.js"></script>
<script src="~/lib/msgpack5/msgpack5.js"></script>
<script src="~/lib/signalr/signalr-protocol-msgpack.js"></script>
```

Adding `.withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())` to the `HubConnectionBuilder` will configure the client to use the MessagePack protocol when connecting to a server.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
    .build();
```

Note

At this time, there are no configuration options for the MessagePack protocol on the JavaScript client.

### Java client

To enable MessagePack with Java, install the `com.microsoft.signalr.messagepack` package. When using Gradle, add the following line to the `dependencies` section of the *build.gradle* file:

```
implementation 'com.microsoft.signalr.messagepack:signalr-messagepack:5.0.0'
```

When using Maven, add the following lines inside the `<dependencies>` element of the `pom.xml` file:

```
<dependency>
    <groupId>com.microsoft.signalr.messagepack</groupId>
    <artifactId>signalr</artifactId>
    <version>5.0.0</version>
</dependency>
```

Call `withHubProtocol(new MessagePackHubProtocol())` on `HubConnectionBuilder`.

```
HubConnection messagePackConnection = HubConnectionBuilder.create("YOUR HUB URL HERE")
    .withHubProtocol(new MessagePackHubProtocol())
    .build();
```

## MessagePack considerations

There are a few issues to be aware of when using the MessagePack Hub Protocol.

### MessagePack is case-sensitive

The MessagePack protocol is case-sensitive. For example, consider the following C# class:

```
public class ChatMessage
{
    public string Sender { get; }
    public string Message { get; }
}
```

When sending from the JavaScript client, you must use `PascalCased` property names, since the casing must match the C# class exactly. For example:

```
connection.invoke("SomeMethod", { Sender: "Sally", Message: "Hello!" });
```

Using `camelCased` names won't properly bind to the C# class. You can work around this by using the `Key` attribute to specify a different name for the MessagePack property. For more information, see [the MessagePack-CSharp documentation](https://github.com/neuecc/MessagePack-CSharp#object-serialization).

### DateTime.Kind is not preserved when serializing/deserializing

The MessagePack protocol doesn't provide a way to encode the `Kind` value of a `DateTime`. As a result, when deserializing a date, the MessagePack Hub Protocol will convert to the UTC format if the `DateTime.Kind` is `DateTimeKind.Local` otherwise it will not touch the time and pass it as is. If you're working with `DateTime` values, we recommend converting to UTC before sending them. Convert them from UTC to local time when you receive them.

### DateTime.MinValue is not supported by MessagePack in JavaScript

The [msgpack5](https://github.com/mcollina/msgpack5) library used by the SignalR JavaScript client doesn't support the `timestamp96` type in MessagePack. This type is used to encode very large date values (either very early in the past or very far in the future). The value of `DateTime.MinValue` is `January 1, 0001`, which must be encoded in a `timestamp96` value. Because of this, sending `DateTime.MinValue` to a JavaScript client isn't supported. When `DateTime.MinValue` is received by the JavaScript client, the following error is thrown:

```
Uncaught Error: unable to find ext type 255 at decoder.js:427
```

Usually, `DateTime.MinValue` is used to encode a "missing" or `null` value. If you need to encode that value in MessagePack, use a nullable `DateTime` value (`DateTime?`) or encode a separate `bool` value indicating if the date is present.

For more information on this limitation, see GitHub issue [aspnet/SignalR#2228](https://github.com/aspnet/SignalR/issues/2228).

### MessagePack support in "ahead-of-time" compilation environment

The [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp/tree/v2.1.90) library used by the .NET client and server uses code generation to optimize serialization. As a result, it isn't supported by default on environments that use "ahead-of-time" compilation, such as NET Multi-platform App UI (.NET MAUI) or Unity. It's possible to use MessagePack in these environments by "pre-generating" the serializer/deserializer code. For more information, see [the MessagePack-CSharp documentation](https://github.com/neuecc/MessagePack-CSharp/tree/v2.1.90#aot-code-generation-to-support-unityxamarin). Once you have pre-generated the serializers, you can register them using the configuration delegate passed to `AddMessagePackProtocol`:

```
services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        StaticCompositeResolver.Instance.Register(
            MessagePack.Resolvers.GeneratedResolver.Instance,
            MessagePack.Resolvers.StandardResolver.Instance
        );
        options.SerializerOptions = MessagePackSerializerOptions.Standard
            .WithResolver(StaticCompositeResolver.Instance)
            .WithSecurity(MessagePackSecurity.UntrustedData);
    });
```

### Type checks are more strict in MessagePack

The JSON Hub Protocol will perform type conversions during deserialization. For example, if the incoming object has a property value that is a number (`{ foo: 42 }`) but the property on the .NET class is of type `string`, the value will be converted. However, MessagePack doesn't perform this conversion and will throw an exception that can be seen in server-side logs (and in the console):

```
InvalidDataException: Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.
```

For more information on this limitation, see GitHub issue [aspnet/SignalR#2937](https://github.com/aspnet/SignalR/issues/2937).

### Chars and Strings in Java

In the java client, `char` objects will be serialized as one-character `String` objects. This is in contrast with the C# and JavaScript client, which serialize them as `short` objects. The MessagePack spec itself does not define behavior for `char` objects, so it is up to the library author to determine how to serialize them. The difference in behavior between our clients is a result of the libraries we used for our implementations.

### Android device support

Depending on the MessagePack version targeted by the .NET release, the MessagePack integration can increase the minimum Android API level. For example, `signalr-messagepack` 8.0.24 resolves `jackson-databind` 2.18, raising the minimum Android API level from 21 to 26. If you need to support older Android devices than a given MessagePack integration permits, use the default JSON protocol, which doesn't have this MessagePack dependency constraint. For more information, see [\[SignalR Java client\]: signalr-messagepack 8.0.24 drops Android 7 support in a patch release (`dotnet/aspnetcore` #68893)](https://github.com/dotnet/aspnetcore/issues/68893).

## Additional resources

* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)

This article assumes the reader is familiar with the topics covered in [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0).

## What is MessagePack?

[MessagePack](https://msgpack.org/index.html) is a fast and compact binary serialization format. It's useful when performance and bandwidth are a concern because it creates smaller messages compared to [JSON](https://www.json.org/). The binary messages are unreadable when looking at network traces and logs unless the bytes are passed through a MessagePack parser. SignalR has built-in support for the MessagePack format, and provides APIs for the client and server to use.

## Configure MessagePack on the server

To enable the MessagePack Hub Protocol on the server, install the `Microsoft.AspNetCore.SignalR.Protocols.MessagePack` package in your app. In the `Startup.ConfigureServices` method, add `AddMessagePackProtocol` to the `AddSignalR` call to enable MessagePack support on the server.

Note

JSON is enabled by default. Adding MessagePack enables support for both JSON and MessagePack clients.

```
services.AddSignalR()
    .AddMessagePackProtocol();
```

To customize how MessagePack will format your data, `AddMessagePackProtocol` takes a delegate for configuring options. In that delegate, the `FormatterResolvers` property can be used to configure MessagePack serialization options. For more information on how the resolvers work, visit the MessagePack library at [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp). Attributes can be used on the objects you want to serialize to define how they should be handled.

```
services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
        {
            MessagePack.Resolvers.StandardResolver.Instance
        };
    });
```

Warning

We strongly recommend reviewing [CVE-2020-5234](https://github.com/neuecc/MessagePack-CSharp/security/advisories/GHSA-7q36-4xx7-xcxf) and applying the recommended patches. For example, setting the `MessagePackSecurity.Active` static property to `MessagePackSecurity.UntrustedData`. Setting the `MessagePackSecurity.Active` requires manually installing a [1.9.x version of MessagePack](https://www.nuget.org/packages/MessagePack/1.9.3). Installing `MessagePack` 1.9.x upgrades the version SignalR uses. `MessagePack` version 2.x introduced breaking changes and is incompatible with SignalR versions 3.1 or earlier. When `MessagePackSecurity.Active` isn't set to `MessagePackSecurity.UntrustedData`, a malicious client could cause a denial of service. Set `MessagePackSecurity.Active` in `Program.Main`, as shown in the following code:

```
using MessagePack;

public static void Main(string[] args)
{
  MessagePackSecurity.Active = MessagePackSecurity.UntrustedData;

  CreateHostBuilder(args).Build().Run();
}
```

## Configure MessagePack on the client

Note

JSON is enabled by default for the supported clients. Clients can only support a single protocol. Adding MessagePack support will replace any previously configured protocols.

### .NET client

To enable MessagePack in the .NET Client, install the `Microsoft.AspNetCore.SignalR.Protocols.MessagePack` package and call `AddMessagePackProtocol` on `HubConnectionBuilder`.

```
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

var hubConnection = new HubConnectionBuilder()
                        .WithUrl("/chathub")
                        .AddMessagePackProtocol()
                        .Build();
```

Note

This `AddMessagePackProtocol` call takes a delegate for configuring options just like the server.

### JavaScript client

MessagePack support for the JavaScript client is provided by the [@microsoft/signalr-protocol-msgpack](https://www.npmjs.com/package/@microsoft/signalr-protocol-msgpack) npm package. Install the package by executing the following command in a command shell:

```
npm install @microsoft/signalr-protocol-msgpack
```

After installing the npm package, the module can be used directly via a JavaScript module loader or imported into the browser by referencing the following file:

*node\_modules\\@microsoft\signalr-protocol-msgpack\dist\browser\signalr-protocol-msgpack.js*

In a browser, the `msgpack5` library must also be referenced. Use a `<script>` tag to create a reference. The library can be found at *node\_modules\msgpack5\dist\msgpack5.js*.

Note

When using the `<script>` element, the order is important. If `signalr-protocol-msgpack.js` is referenced before `msgpack5.js`, an error occurs when trying to connect with MessagePack. `signalr.js` is also required before `signalr-protocol-msgpack.js`.

```
<script src="~/lib/signalr/signalr.js"></script>
<script src="~/lib/msgpack5/msgpack5.js"></script>
<script src="~/lib/signalr/signalr-protocol-msgpack.js"></script>
```

Adding `.withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())` to the `HubConnectionBuilder` will configure the client to use the MessagePack protocol when connecting to a server.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
    .build();
```

Note

At this time, there are no configuration options for the MessagePack protocol on the JavaScript client.

## MessagePack considerations

There are a few issues to be aware of when using the MessagePack Hub Protocol.

### MessagePack is case-sensitive

The MessagePack protocol is case-sensitive. For example, consider the following C# class:

```
public class ChatMessage
{
    public string Sender { get; }
    public string Message { get; }
}
```

When sending from the JavaScript client, you must use `PascalCased` property names, since the casing must match the C# class exactly. For example:

```
connection.invoke("SomeMethod", { Sender: "Sally", Message: "Hello!" });
```

Using `camelCased` names won't properly bind to the C# class. You can work around this by using the `Key` attribute to specify a different name for the MessagePack property. For more information, see [the MessagePack-CSharp documentation](https://github.com/neuecc/MessagePack-CSharp#object-serialization).

### DateTime.Kind is not preserved when serializing/deserializing

The MessagePack protocol doesn't provide a way to encode the `Kind` value of a `DateTime`. As a result, when deserializing a date, the MessagePack Hub Protocol assumes the incoming date is in UTC format. If you're working with `DateTime` values in local time, we recommend converting to UTC before sending them. Convert them from UTC to local time when you receive them.

For more information on this limitation, see GitHub issue [aspnet/SignalR#2632](https://github.com/aspnet/SignalR/issues/2632).

### DateTime.MinValue is not supported by MessagePack in JavaScript

The [msgpack5](https://github.com/mcollina/msgpack5) library used by the SignalR JavaScript client doesn't support the `timestamp96` type in MessagePack. This type is used to encode very large date values (either very early in the past or very far in the future). The value of `DateTime.MinValue` is `January 1, 0001`, which must be encoded in a `timestamp96` value. Because of this, sending `DateTime.MinValue` to a JavaScript client isn't supported. When `DateTime.MinValue` is received by the JavaScript client, the following error is thrown:

```
Uncaught Error: unable to find ext type 255 at decoder.js:427
```

Usually, `DateTime.MinValue` is used to encode a "missing" or `null` value. If you need to encode that value in MessagePack, use a nullable `DateTime` value (`DateTime?`) or encode a separate `bool` value indicating if the date is present.

For more information on this limitation, see GitHub issue [aspnet/SignalR#2228](https://github.com/aspnet/SignalR/issues/2228).

### MessagePack support in "ahead-of-time" compilation environment

The [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp/tree/v1.8.80) library used by the .NET client and server uses code generation to optimize serialization. As a result, it isn't supported by default on environments that use "ahead-of-time" compilation, such as NET Multi-platform App UI (.NET MAUI) or Unity. It's possible to use MessagePack in these environments by "pre-generating" the serializer/deserializer code. For more information, see [the MessagePack-CSharp documentation](https://github.com/neuecc/MessagePack-CSharp/tree/v1.8.80#pre-code-generationunityxamarin-supports). Once you have pre-generated the serializers, you can register them using the configuration delegate passed to `AddMessagePackProtocol`:

```
services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
        {
            MessagePack.Resolvers.GeneratedResolver.Instance,
            MessagePack.Resolvers.StandardResolver.Instance
        };
    });
```

### Type checks are more strict in MessagePack

The JSON Hub Protocol will perform type conversions during deserialization. For example, if the incoming object has a property value that is a number (`{ foo: 42 }`) but the property on the .NET class is of type `string`, the value will be converted. However, MessagePack doesn't perform this conversion and will throw an exception that can be seen in server-side logs (and in the console):

```
InvalidDataException: Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.
```

For more information on this limitation, see GitHub issue [aspnet/SignalR#2937](https://github.com/aspnet/SignalR/issues/2937).

### Android device support

Depending on the MessagePack version targeted by the .NET release, the MessagePack integration can increase the minimum Android API level. For example, `signalr-messagepack` 8.0.24 resolves `jackson-databind` 2.18, raising the minimum Android API level from 21 to 26. If you need to support older Android devices than a given MessagePack integration permits, use the default JSON protocol, which doesn't have this MessagePack dependency constraint. For more information, see [\[SignalR Java client\]: signalr-messagepack 8.0.24 drops Android 7 support in a patch release (`dotnet/aspnetcore` #68893)](https://github.com/dotnet/aspnetcore/issues/68893).

## Additional resources

* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)

This article assumes the reader is familiar with the topics covered in [Get started with ASP.NET Core SignalR](../tutorials/signalr?view=aspnetcore-10.0).

## What is MessagePack?

[MessagePack](https://msgpack.org/index.html) is a fast and compact binary serialization format. It's useful when performance and bandwidth are a concern because it creates smaller messages compared to [JSON](https://www.json.org/). The binary messages are unreadable when looking at network traces and logs unless the bytes are passed through a MessagePack parser. SignalR has built-in support for the MessagePack format, and provides APIs for the client and server to use.

## Configure MessagePack on the server

To enable the MessagePack Hub Protocol on the server, install the `Microsoft.AspNetCore.SignalR.Protocols.MessagePack` package in your app. In the `Startup.ConfigureServices` method, add `AddMessagePackProtocol` to the `AddSignalR` call to enable MessagePack support on the server.

Note

JSON is enabled by default. Adding MessagePack enables support for both JSON and MessagePack clients.

```
services.AddSignalR()
    .AddMessagePackProtocol();
```

To customize how MessagePack will format your data, `AddMessagePackProtocol` takes a delegate for configuring options. In that delegate, the `FormatterResolvers` property can be used to configure MessagePack serialization options. For more information on how the resolvers work, visit the MessagePack library at [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp). Attributes can be used on the objects you want to serialize to define how they should be handled.

```
services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
        {
            MessagePack.Resolvers.StandardResolver.Instance
        };
    });
```

Warning

We strongly recommend reviewing [CVE-2020-5234](https://github.com/neuecc/MessagePack-CSharp/security/advisories/GHSA-7q36-4xx7-xcxf) and applying the recommended patches. For example, setting the `MessagePackSecurity.Active` static property to `MessagePackSecurity.UntrustedData`. Setting the `MessagePackSecurity.Active` requires manually installing a [1.9.x version of MessagePack](https://www.nuget.org/packages/MessagePack/1.9.3). Installing `MessagePack` 1.9.x upgrades the version SignalR uses. When `MessagePackSecurity.Active` is not set to `MessagePackSecurity.UntrustedData`, a malicious client could cause a denial of service. Set `MessagePackSecurity.Active` in `Program.Main`, as shown in the following code:

```
using MessagePack;

public static void Main(string[] args)
{
  MessagePackSecurity.Active = MessagePackSecurity.UntrustedData;

  CreateHostBuilder(args).Build().Run();
}
```

## Configure MessagePack on the client

Note

JSON is enabled by default for the supported clients. Clients can only support a single protocol. Adding MessagePack support will replace any previously configured protocols.

### .NET client

To enable MessagePack in the .NET Client, install the `Microsoft.AspNetCore.SignalR.Protocols.MessagePack` package and call `AddMessagePackProtocol` on `HubConnectionBuilder`.

```
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

var hubConnection = new HubConnectionBuilder()
                        .WithUrl("/chathub")
                        .AddMessagePackProtocol()
                        .Build();
```

Note

This `AddMessagePackProtocol` call takes a delegate for configuring options just like the server.

### JavaScript client

MessagePack support for the JavaScript client is provided by the [@aspnet/signalr-protocol-msgpack](https://www.npmjs.com/package/@aspnet/signalr-protocol-msgpack) npm package. Install the package by executing the following command in a command shell:

```
npm install @aspnet/signalr-protocol-msgpack
```

After installing the npm package, the module can be used directly via a JavaScript module loader or imported into the browser by referencing the following file:

*node\_modules\\@aspnet\signalr-protocol-msgpack\dist\browser\signalr-protocol-msgpack.js*

In a browser, the `msgpack5` library must also be referenced. Use a `<script>` tag to create a reference. The library can be found at *node\_modules\msgpack5\dist\msgpack5.js*.

Note

When using the `<script>` element, the order is important. If `signalr-protocol-msgpack.js` is referenced before `msgpack5.js`, an error occurs when trying to connect with MessagePack. `signalr.js` is also required before `signalr-protocol-msgpack.js`.

```
<script src="~/lib/signalr/signalr.js"></script>
<script src="~/lib/msgpack5/msgpack5.js"></script>
<script src="~/lib/signalr/signalr-protocol-msgpack.js"></script>
```

Adding `.withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())` to the `HubConnectionBuilder` will configure the client to use the MessagePack protocol when connecting to a server.

```
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
    .build();
```

Note

At this time, there are no configuration options for the MessagePack protocol on the JavaScript client.

## MessagePack considerations

There are a few issues to be aware of when using the MessagePack Hub Protocol.

### MessagePack is case-sensitive

The MessagePack protocol is case-sensitive. For example, consider the following C# class:

```
public class ChatMessage
{
    public string Sender { get; }
    public string Message { get; }
}
```

When sending from the JavaScript client, you must use `PascalCased` property names, since the casing must match the C# class exactly. For example:

```
connection.invoke("SomeMethod", { Sender: "Sally", Message: "Hello!" });
```

Using `camelCased` names won't properly bind to the C# class. You can work around this by using the `Key` attribute to specify a different name for the MessagePack property. For more information, see [the MessagePack-CSharp documentation](https://github.com/neuecc/MessagePack-CSharp#object-serialization).

### DateTime.Kind is not preserved when serializing/deserializing

The MessagePack protocol doesn't provide a way to encode the `Kind` value of a `DateTime`. As a result, when deserializing a date, the MessagePack Hub Protocol assumes the incoming date is in UTC format. If you're working with `DateTime` values in local time, we recommend converting to UTC before sending them. Convert them from UTC to local time when you receive them.

For more information on this limitation, see GitHub issue [aspnet/SignalR#2632](https://github.com/aspnet/SignalR/issues/2632).

### DateTime.MinValue is not supported by MessagePack in JavaScript

The [msgpack5](https://github.com/mcollina/msgpack5) library used by the SignalR JavaScript client doesn't support the `timestamp96` type in MessagePack. This type is used to encode very large date values (either very early in the past or very far in the future). The value of `DateTime.MinValue` is `January 1, 0001` which must be encoded in a `timestamp96` value. Because of this, sending `DateTime.MinValue` to a JavaScript client isn't supported. When `DateTime.MinValue` is received by the JavaScript client, the following error is thrown:

```
Uncaught Error: unable to find ext type 255 at decoder.js:427
```

Usually, `DateTime.MinValue` is used to encode a "missing" or `null` value. If you need to encode that value in MessagePack, use a nullable `DateTime` value (`DateTime?`) or encode a separate `bool` value indicating if the date is present.

For more information on this limitation, see GitHub issue [aspnet/SignalR#2228](https://github.com/aspnet/SignalR/issues/2228).

### MessagePack support in "ahead-of-time" compilation environment

The [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp/tree/v1.8.80) library used by the .NET client and server uses code generation to optimize serialization. As a result, it isn't supported by default on environments that use "ahead-of-time" compilation, such as NET Multi-platform App UI (.NET MAUI) or Unity. It's possible to use MessagePack in these environments by "pre-generating" the serializer/deserializer code. For more information, see [the MessagePack-CSharp documentation](https://github.com/neuecc/MessagePack-CSharp/tree/v1.8.80#pre-code-generationunityxamarin-supports). Once you have pre-generated the serializers, you can register them using the configuration delegate passed to `AddMessagePackProtocol`:

```
services.AddSignalR()
    .AddMessagePackProtocol(options =>
    {
        options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
        {
            MessagePack.Resolvers.GeneratedResolver.Instance,
            MessagePack.Resolvers.StandardResolver.Instance
        };
    });
```

### Type checks are more strict in MessagePack

The JSON Hub Protocol will perform type conversions during deserialization. For example, if the incoming object has a property value that is a number (`{ foo: 42 }`) but the property on the .NET class is of type `string`, the value will be converted. However, MessagePack doesn't perform this conversion and will throw an exception that can be seen in server-side logs (and in the console):

```
InvalidDataException: Error binding arguments. Make sure that the types of the provided values match the types of the hub method being invoked.
```

For more information on this limitation, see GitHub issue [aspnet/SignalR#2937](https://github.com/aspnet/SignalR/issues/2937).

### Android device support

Depending on the MessagePack version targeted by the .NET release, the MessagePack integration can increase the minimum Android API level. For example, `signalr-messagepack` 8.0.24 resolves `jackson-databind` 2.18, raising the minimum Android API level from 21 to 26. If you need to support older Android devices than a given MessagePack integration permits, use the default JSON protocol, which doesn't have this MessagePack dependency constraint. For more information, see [\[SignalR Java client\]: signalr-messagepack 8.0.24 drops Android 7 support in a patch release (`dotnet/aspnetcore` #68893)](https://github.com/dotnet/aspnetcore/issues/68893).

## Additional resources

* [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/inheritance
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-mvc/inheritance.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Tutorial: Implement inheritance - ASP.NET MVC with EF Core

In the previous tutorial, you handled concurrency exceptions. This tutorial will show you how to implement inheritance in the data model.

In object-oriented programming, you can use inheritance to facilitate code reuse. In this tutorial, you'll change the `Instructor` and `Student` classes so that they derive from a `Person` base class which contains properties such as `LastName` that are common to both instructors and students. You won't add or change any web pages, but you'll change some of the code and those changes will be automatically reflected in the database.

In this tutorial, you:

* Map inheritance to database
* Create the Person class
* Update Instructor and Student
* Add Person to the model
* Create and update migrations
* Test the implementation

## Prerequisites

* [Handle Concurrency](concurrency?view=aspnetcore-10.0)

## Map inheritance to database

The `Instructor` and `Student` classes in the School data model have several properties that are identical:

Suppose you want to eliminate the redundant code for the properties that are shared by the `Instructor` and `Student` entities. Or you want to write a service that can format names without caring whether the name came from an instructor or a student. You could create a `Person` base class that contains only those shared properties, then make the `Instructor` and `Student` classes inherit from that base class, as shown in the following illustration:

There are several ways this inheritance structure could be represented in the database. You could have a `Person` table that includes information about both students and instructors in a single table. Some of the columns could apply only to instructors (HireDate), some only to students (EnrollmentDate), some to both (LastName, FirstName). Typically, you'd have a discriminator column to indicate which type each row represents. For example, the discriminator column might have "Instructor" for instructors and "Student" for students.

This pattern of generating an entity inheritance structure from a single database table is called *table-per-hierarchy (TPH)* inheritance.

An alternative is to make the database look more like the inheritance structure. For example, you could have only the name fields in the `Person` table and have separate `Instructor` and `Student` tables with the date fields.

Warning

Table-Per-Type (TPT) is not supported by EF Core 3.x, however it is has been implemented in [EF Core 5.0](/en-us/ef/core/what-is-new/ef-core-5.0/plan).

This pattern of making a database table for each entity class is called *table-per-type (TPT)* inheritance.

Yet another option is to map all non-abstract types to individual tables. All properties of a class, including inherited properties, map to columns of the corresponding table. This pattern is called *Table-per-Concrete Class (TPC)* inheritance. If you implemented TPC inheritance for the `Person`, `Student`, and `Instructor` classes as shown earlier, the `Student` and `Instructor` tables would look no different after implementing inheritance than they did before.

TPC and TPH inheritance patterns generally deliver better performance than TPT inheritance patterns, because TPT patterns can result in complex join queries.

This tutorial demonstrates how to implement TPH inheritance. TPH is the only inheritance pattern that the Entity Framework Core supports. What you'll do is create a `Person` class, change the `Instructor` and `Student` classes to derive from `Person`, add the new class to the `DbContext`, and create a migration.

Tip

Consider saving a copy of the project before making the following changes. Then if you run into problems and need to start over, it will be easier to start from the saved project instead of reversing steps done for this tutorial or going back to the beginning of the whole series.

## Create the Person class

In the Models folder, create Person.cs and replace the template code with the following code:

```
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public abstract class Person
    {
        public int ID { get; set; }

        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }
    }
}
```

## Update Instructor and Student

In `Instructor.cs`, derive the Instructor class from the Person class and remove the key and name fields. The code will look like the following example:

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}
```

Make the same changes in `Student.cs`.

```
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }


        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

## Add Person to the model

Add the Person entity type to `SchoolContext.cs`. The new lines are highlighted.

```
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }
        public DbSet<Person> People { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
            modelBuilder.Entity<Person>().ToTable("Person");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}
```

This is all that the Entity Framework needs in order to configure table-per-hierarchy inheritance. As you'll see, when the database is updated, it will have a Person table in place of the Student and Instructor tables.

## Create and update migrations

Save your changes and build the project. Then open the command window in the project folder and enter the following command:

```
dotnet ef migrations add Inheritance
```

Don't run the `database update` command yet. That command will result in lost data because it will drop the Instructor table and rename the Student table to Person. You need to provide custom code to preserve existing data.

Open `Migrations/<timestamp>_Inheritance.cs` and replace the `Up` method with the following code:

```
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropForeignKey(
        name: "FK_Enrollment_Student_StudentID",
        table: "Enrollment");

    migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table: "Enrollment");

    migrationBuilder.RenameTable(name: "Instructor", newName: "Person");
    migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<string>(name: "Discriminator", table: "Person", nullable: false, maxLength: 128, defaultValue: "Instructor");
    migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<int>(name: "OldId", table: "Person", nullable: true);

    // Copy existing Student data into new Person table.
    migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, ID AS OldId FROM dbo.Student");
    // Fix up existing relationships to match new PK's.
    migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");

    // Remove temporary key
    migrationBuilder.DropColumn(name: "OldID", table: "Person");

    migrationBuilder.DropTable(
        name: "Student");

    migrationBuilder.CreateIndex(
         name: "IX_Enrollment_StudentID",
         table: "Enrollment",
         column: "StudentID");

    migrationBuilder.AddForeignKey(
        name: "FK_Enrollment_Person_StudentID",
        table: "Enrollment",
        column: "StudentID",
        principalTable: "Person",
        principalColumn: "ID",
        onDelete: ReferentialAction.Cascade);
}
```

This code takes care of the following database update tasks:

* Removes foreign key constraints and indexes that point to the Student table.

* Renames the Instructor table as Person and makes changes needed for it to store Student data:

* Adds nullable EnrollmentDate for students.

* Adds Discriminator column to indicate whether a row is for a student or an instructor.

* Makes HireDate nullable since student rows won't have hire dates.

* Adds a temporary field that will be used to update foreign keys that point to students. When you copy students into the Person table they will get new primary key values.

* Copies data from the Student table into the Person table. This causes students to get assigned new primary key values.

* Fixes foreign key values that point to students.

* Re-creates foreign key constraints and indexes, now pointing them to the Person table.

(If you had used GUID instead of integer as the primary key type, the student primary key values wouldn't have to change, and several of these steps could have been omitted.)

Run the `database update` command:

```
dotnet ef database update
```

(In a production system you would make corresponding changes to the `Down` method in case you ever had to use that to go back to the previous database version. For this tutorial you won't be using the `Down` method.)

Note

It's possible to get other errors when making schema changes in a database that has existing data. If you get migration errors that you can't resolve, you can either change the database name in the connection string or delete the database. With a new database, there's no data to migrate, and the update-database command is more likely to complete without errors. To delete the database, use SSOX or run the `database drop` CLI command.

## Test the implementation

Run the app and try various pages. Everything works the same as it did before.

In **SQL Server Object Explorer**, expand **Data Connections/SchoolContext** and then **Tables**, and you see that the Student and Instructor tables have been replaced by a Person table. Open the Person table designer and you see that it has all of the columns that used to be in the Student and Instructor tables.

Right-click the Person table, and then click **Show Table Data** to see the discriminator column.

## Get the code

[Download or view the completed application.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final)

## Related content

For more information about inheritance in Entity Framework Core, see [Inheritance](/en-us/ef/core/modeling/inheritance).

## Next steps

In this tutorial, you:

* Mapped inheritance to database
* Created the Person class
* Updated Instructor and Student
* Added Person to the model
* Created and update migrations
* Tested the implementation

Advance to the next tutorial to learn how to handle a variety of relatively advanced Entity Framework scenarios.

[Next: Advanced topics](advanced?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/intro
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/data/ef-rp/intro.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1 of 8

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Tom Dykstra](https://github.com/tdykstra), [Jeremy Likness](https://twitter.com/jeremylikness), and [Jon P Smith](https://twitter.com/thereformedprog)

This is the first in a series of tutorials that show how to use Entity Framework (EF) Core in an [ASP.NET Core Razor Pages](../../razor-pages/?view=aspnetcore-10.0) app. The tutorials build a web site for a fictional Contoso University. The site includes functionality such as student admission, course creation, and instructor assignments. The tutorial uses the code first approach. For information on following this tutorial using the database first approach, see [this Github issue](https://github.com/dotnet/AspNetCore.Docs/issues/16897).

[Download or view the completed app.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu60) [Download instructions](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

## Prerequisites

* If you're new to Razor Pages, go through the [Get started with Razor Pages](../../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0) tutorial series before starting this one.

- [Visual Studio](#tabpanel_1_visual-studio)
- [Visual Studio Code](#tabpanel_1_visual-studio-code)

* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) with the **ASP.NET and web development** workload.

### Database engines

The Visual Studio instructions use [SQL Server LocalDB](/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb), a version of SQL Server Express that runs only on Windows.

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# for Visual Studio Code (latest version)](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
* [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)

The Visual Studio Code instructions use the .NET CLI for ASP.NET Core development functions such as project creation. You can follow these instructions on macOS, Linux, or Windows and with any code editor. Minor changes may be required if you use something other than Visual Studio Code.

Consider downloading and installing a third-party tool for managing and viewing a SQLite database, such as [DB Browser for SQLite](https://sqlitebrowser.org/).

### Database engines

The Visual Studio Code tab use [SQLite](https://www.sqlite.org/), a cross-platform database engine.

### Troubleshooting

If you run into a problem you can't resolve, compare your code to the [completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples). A good way to get help is by posting a question to StackOverflow\.com, using the [ASP.NET Core tag](https://stackoverflow.com/questions/tagged/asp.net-core) or the [EF Core tag](https://stackoverflow.com/questions/tagged/entity-framework-core).

### The sample app

The app built in these tutorials is a basic university web site. Users can view and update student, course, and instructor information. Here are a few of the screens created in the tutorial.

The UI style of this site is based on the built-in project templates. The tutorial's focus is on how to use EF Core with ASP.NET Core, not how to customize the UI.

[]()

### Optional: Build the sample download

This step is optional. Building the completed app is recommended when you have problems you can't solve. If you run into a problem you can't resolve, compare your code to the [completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu50). [Download instructions](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

Select `ContosoUniversity.csproj` to open the project.

* Build the project.

* In Package Manager Console (PMC) run the following command:

  ```
  Update-Database
  ```

- Remove the comments from the `ContosoUniversity.csproj` file so `SQLiteVersion` is defined:

  ```
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <DefineConstants>TRACE;SQLiteVersion</DefineConstants>
  </PropertyGroup>
  ```

- In a command window, update the database:

  ```
  dotnet tool install --global dotnet-ef
  dotnet ef database update
  ```

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Run the project to seed the database.

## Create the web app project

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

1. Start Visual Studio 2022 and select **Create a new project**.

2. In the **Create a new project** dialog, select **ASP.NET Core Web App**, and then select **Next**.

3. In the **Configure your new project** dialog, enter `ContosoUniversity` for **Project name**. It's important to name the project *ContosoUniversity*, including matching the capitalization, so the namespaces will match when you copy and paste example code.

4. Select **Next**.

5. In the **Additional information** dialog, select **.NET 6.0 (Long-term support)** and then select **Create**.

* In a terminal, navigate to the folder in which the project folder should be created.

* Run the following commands to create a Razor Pages project and `cd` into the new project folder:

  ```
  dotnet new webapp -o ContosoUniversity
  cd ContosoUniversity  
  ```

## Set up the site style

Copy and paste the following code into the `Pages/Shared/_Layout.cshtml` file:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Contoso University</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/ContosoUniversity.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">Contoso University</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">                        
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/About">About</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Students/Index">Students</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Courses/Index">Courses</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Instructors/Index">Instructors</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Departments/Index">Departments</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2021 - Contoso University - <a asp-area="" asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
```

The layout file sets the site header, footer, and menu. The preceding code makes the following changes:

* Each occurrence of "ContosoUniversity" to "Contoso University". There are three occurrences.
* The **Home** and **Privacy** menu entries are deleted.
* Entries are added for **About**, **Students**, **Courses**, **Instructors**, and **Departments**.

In `Pages/Index.cshtml`, replace the contents of the file with the following code:

```
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="row mb-auto">
    <div class="col-md-4">
        <div class="row no-gutters border mb-4">
            <div class="col p-4 mb-4 ">
                <p class="card-text">
                    Contoso University is a sample application that
                    demonstrates how to use Entity Framework Core in an
                    ASP.NET Core Razor Pages web app.
                </p>
            </div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="row no-gutters border mb-4">
            <div class="col p-4 d-flex flex-column position-static">
                <p class="card-text mb-auto">
                    You can build the application by following the steps in a series of tutorials.
                </p>
                <p>
@*                    <a href="https://learn.microsoft.com/aspnet/core/data/ef-rp/intro" class="stretched-link">See the tutorial</a>
*@                </p>
            </div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="row no-gutters border mb-4">
            <div class="col p-4 d-flex flex-column">
                <p class="card-text mb-auto">
                    You can download the completed project from GitHub.
                </p>
                <p>
@*                    <a href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples" class="stretched-link">See project source code</a>
*@                </p>
            </div>
        </div>
    </div>
</div>
```

The preceding code replaces the text about ASP.NET Core with text about this app.

Run the app to verify that the home page appears.

## The data model

The following sections create a data model:

A student can enroll in any number of courses, and a course can have any number of students enrolled in it.

## The Student entity

* Create a *Models* folder in the project folder.
* Create `Models/Student.cs` with the following code:
  ```
  namespace ContosoUniversity.Models
  {
      public class Student
      {
          public int ID { get; set; }
          public string LastName { get; set; }
          public string FirstMidName { get; set; }
          public DateTime EnrollmentDate { get; set; }

          public ICollection<Enrollment> Enrollments { get; set; }
      }
  }
  ```

The `ID` property becomes the primary key column of the database table that corresponds to this class. By default, EF Core interprets a property that's named `ID` or `classnameID` as the primary key. So the alternative automatically recognized name for the `Student` class primary key is `StudentID`. For more information, see [EF Core - Keys](/en-us/ef/core/modeling/keys?tabs=data-annotations).

The `Enrollments` property is a [navigation property](/en-us/ef/core/modeling/relationships). Navigation properties hold other entities that are related to this entity. In this case, the `Enrollments` property of a `Student` entity holds all of the `Enrollment` entities that are related to that Student. For example, if a Student row in the database has two related Enrollment rows, the `Enrollments` navigation property contains those two Enrollment entities.

In the database, an Enrollment row is related to a Student row if its `StudentID` column contains the student's ID value. For example, suppose a Student row has ID=1. Related Enrollment rows will have `StudentID` = 1. `StudentID` is a *foreign key* in the Enrollment table.

The `Enrollments` property is defined as `ICollection<Enrollment>` because there may be multiple related Enrollment entities. Other collection types can be used, such as `List<Enrollment>` or `HashSet<Enrollment>`. When `ICollection<Enrollment>` is used, EF Core creates a `HashSet<Enrollment>` collection by default.

## The Enrollment entity

Create `Models/Enrollment.cs` with the following code:

```
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}
```

The `EnrollmentID` property is the primary key; this entity uses the `classnameID` pattern instead of `ID` by itself. For a production data model, many developers choose one pattern and use it consistently. This tutorial uses both just to illustrate that both work. Using `ID` without `classname` makes it easier to implement some kinds of data model changes.

The `Grade` property is an `enum`. The question mark after the `Grade` type declaration indicates that the `Grade` property is [nullable](/en-us/dotnet/csharp/programming-guide/nullable-types/). A grade that's null is different from a zero grade—null means a grade isn't known or hasn't been assigned yet.

The `StudentID` property is a foreign key, and the corresponding navigation property is `Student`. An `Enrollment` entity is associated with one `Student` entity, so the property contains a single `Student` entity.

The `CourseID` property is a foreign key, and the corresponding navigation property is `Course`. An `Enrollment` entity is associated with one `Course` entity.

EF Core interprets a property as a foreign key if it's named `<navigation property name><primary key property name>`. For example,`StudentID` is the foreign key for the `Student` navigation property, since the `Student` entity's primary key is `ID`. Foreign key properties can also be named `<primary key property name>`. For example, `CourseID` since the `Course` entity's primary key is `CourseID`.

## The Course entity

Create `Models/Course.cs` with the following code:

```
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int CourseID { get; set; }
        public string Title { get; set; }
        public int Credits { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The `Enrollments` property is a navigation property. A `Course` entity can be related to any number of `Enrollment` entities.

The `DatabaseGenerated` attribute allows the app to specify the primary key rather than having the database generate it.

Build the app. The compiler generates several warnings about how `null` values are handled. See [this GitHub issue](https://github.com/dotnet/Scaffolding/issues/1594), [Nullable reference types](/en-us/dotnet/csharp/nullable-references), and [Tutorial: Express your design intent more clearly with nullable and non-nullable reference types](/en-us/dotnet/csharp/whats-new/tutorials/nullable-reference-types) for more information.

To eliminate the warnings from nullable reference types, remove the following line from the `ContosoUniversity.csproj` file:

```
<Nullable>enable</Nullable>
```

The scaffolding engine currently does not support [nullable reference types](/en-us/dotnet/csharp/nullable-references), therefore the models used in scaffold can't either.

Remove the `?` nullable reference type annotation from `public string? RequestId { get; set; }` in `Pages/Error.cshtml.cs` so the project builds without compiler warnings.

## Scaffold Student pages

In this section, the ASP.NET Core scaffolding tool is used to generate:

* An EF Core `DbContext` class. The context is the main class that coordinates Entity Framework functionality for a given data model. It derives from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.
* Razor pages that handle Create, Read, Update, and Delete (CRUD) operations for the `Student` entity.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)

* Create a *Pages/Students* folder.

* In **Solution Explorer**, right-click the *Pages/Students* folder and select **Add** > **New Scaffolded Item**.

* In the **Add New Scaffold Item** dialog:

  * In the left tab, select **Installed > Common > Razor Pages**
  * Select **Razor Pages using Entity Framework (CRUD)** > **ADD**.

* In the **Add Razor Pages using Entity Framework (CRUD)** dialog:

  * In the **Model class** drop-down, select **Student (ContosoUniversity.Models)**.

  * In the **Data context class** row, select the **+** (plus) sign.

    * Change the data context name to end in `SchoolContext` rather than `ContosoUniversityContext`. The updated context name: `ContosoUniversity.Data.SchoolContext`
    * Select **Add** to finish adding the data context class.
    * Select **Add** to finish the **Add Razor Pages** dialog.

The following packages are automatically installed:

* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.EntityFrameworkCore.Tools`
* `Microsoft.VisualStudio.Web.CodeGeneration.Design`

- Run the following .NET CLI commands to install required NuGet packages:

  ```
  dotnet add package Microsoft.EntityFrameworkCore.SQLite
  dotnet add package Microsoft.EntityFrameworkCore.SqlServer
  dotnet add package Microsoft.EntityFrameworkCore.Design
  dotnet add package Microsoft.EntityFrameworkCore.Tools
  dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
  dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
  ```

  The `Microsoft.VisualStudio.Web.CodeGeneration.Design` package is required for scaffolding. Although the app won't use SQL Server, the scaffolding tool needs the SQL Server package.

- Create a *Pages/Students* folder.

- Run the following command to install the [aspnet-codegenerator scaffolding tool](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

  ```
  dotnet tool install --global dotnet-aspnet-codegenerator
  ```

- Run the following command to scaffold Student pages.

  **On Windows**

  ```
  dotnet aspnet-codegenerator razorpage -m Student -dc ContosoUniversity.Data.SchoolContext -udl -outDir Pages\Students --referenceScriptLibraries -dbProvider sqlite 
  ```

  **On macOS or Linux**

  ```
  dotnet aspnet-codegenerator razorpage -m Student -dc ContosoUniversity.Data.SchoolContext -udl -outDir Pages/Students --referenceScriptLibraries -dbProvider sqlite
  ```

If the preceding step fails, build the project and retry the scaffold step.

The scaffolding process:

* Creates Razor pages in the *Pages/Students* folder:

  * `Create.cshtml` and `Create.cshtml.cs`
  * `Delete.cshtml` and `Delete.cshtml.cs`
  * `Details.cshtml` and `Details.cshtml.cs`
  * `Edit.cshtml` and `Edit.cshtml.cs`
  * `Index.cshtml` and `Index.cshtml.cs`

* Creates `Data/SchoolContext.cs`.

* Adds the context to dependency injection in `Program.cs`.

* Adds a database connection string to `appsettings.json`.

## Database connection string

The scaffolding tool generates a connection string in the `appsettings.json` file.

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

The connection string specifies [SQL Server LocalDB](/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb):

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=SchoolContext-0e9;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

LocalDB is a lightweight version of the SQL Server Express Database Engine and is intended for app development, not production use. By default, LocalDB creates *.mdf* files in the `C:/Users/<user>` directory.

Rename the connection string key to `SchoolContextSQLite` and shorten value to `CU.db`:

```
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "SchoolContextSQLite": "Data Source=CU.db"
  },
  "MyMaxModelBindingCollectionSize": "3"
}
```

Don't change the directory without making sure it's valid.

Renaming the connection string key to `SchoolContextSQLite` helps the author maintain one sample that supports both the SQLlite and SQL Server.

## Update the database context class

The main class that coordinates EF Core functionality for a given data model is the database context class. The context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext). The context specifies which entities are included in the data model. In this project, the class is named `SchoolContext`.

Update `Data/SchoolContext.cs` with the following code:

```
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext (DbContextOptions<SchoolContext> options)
            : base(options)
        {
        }

        public DbSet<Student> Students { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Course> Courses { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
        }
    }
}
```

The preceding code changes from the singular `DbSet<Student> Student` to the plural `DbSet<Student> Students`. To make the Razor Pages code match the new `DBSet` name, make a global change from: `_context.Student.` to: `_context.Students.`

There are 8 occurrences.

Because an entity set contains multiple entities, many developers prefer the `DBSet` property names should be plural.

The highlighted code:

* Creates a [DbSet\<TEntity>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for each entity set. In EF Core terminology:

  * An entity set typically corresponds to a database table.
  * An entity corresponds to a row in the table.

* Calls [OnModelCreating](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.onmodelcreating). `OnModelCreating`:

  * Is called when `SchoolContext` has been initialized but before the model has been secured and used to initialize the context.
  * Is required because later in the tutorial the `Student` entity will have references to the other entities.

We hope to [fix this issue](https://github.com/dotnet/Scaffolding/issues/1594) in a future release.

## Program.cs

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services such as the `SchoolContext` are registered with dependency injection during app startup. Components that require these services, such as Razor Pages, are provided these services via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically registered the context class with the dependency injection container.

* [Visual Studio](#tabpanel_6_visual-studio)
* [Visual Studio Code](#tabpanel_6_visual-studio-code)

The following highlighted lines were added by the scaffolder:

```
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>
  options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolContext")));
```

Verify the code added by the scaffolder calls [UseSqlite](/en-us/dotnet/api/microsoft.entityframeworkcore.sqlitedbcontextoptionsbuilderextensions.usesqlite).

```
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("SchoolContextSQLite")));
```

See [Use SQLite for development, SQL Server for production](../../tutorials/razor-pages/model?view=aspnetcore-10.0\&tabs=visual-studio-code#sqlite-ss-6) for information on using a production database.

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` or the `appsettings.Development.json` file.

[]()

### Add the database exception filter

Add [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) and [UseMigrationsEndPoint](/en-us/dotnet/api/microsoft.aspnetcore.builder.migrationsendpointextensions.usemigrationsendpoint) as shown in the following code:

* [Visual Studio](#tabpanel_7_visual-studio)
* [Visual Studio Code](#tabpanel_7_visual-studio-code)

```
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>
  options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolContext")));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
else
{
    app.UseDeveloperExceptionPage();
    app.UseMigrationsEndPoint();
}
```

Add the [Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore) NuGet package.

In the Package Manager Console, enter the following to add the NuGet package:

```
Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
```

```
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("SchoolContextSQLite")));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
else
{
    app.UseDeveloperExceptionPage();
    app.UseMigrationsEndPoint();
}
```

The `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` NuGet package provides ASP.NET Core middleware for Entity Framework Core error pages. This middleware helps to detect and diagnose errors with Entity Framework Core migrations.

The `AddDatabaseDeveloperPageExceptionFilter` provides helpful error information in the [development environment](../../fundamentals/environments?view=aspnetcore-10.0) for EF migrations errors.

## Create the database

Update `Program.cs` to create the database if it doesn't exist:

* [Visual Studio](#tabpanel_8_visual-studio)
* [Visual Studio Code](#tabpanel_8_visual-studio-code)

```
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>
  options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolContext")));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
else
{
    app.UseDeveloperExceptionPage();
    app.UseMigrationsEndPoint();
}

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<SchoolContext>();
    context.Database.EnsureCreated();
    // DbInitializer.Initialize(context);
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("SchoolContextSQLite")));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
else
{
    app.UseDeveloperExceptionPage();
    app.UseMigrationsEndPoint();
}

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<SchoolContext>();
    context.Database.EnsureCreated();
    // DbInitializer.Initialize(context);
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The [EnsureCreated](/en-us/dotnet/api/microsoft.entityframeworkcore.infrastructure.databasefacade.ensurecreated) method takes no action if a database for the context exists. If no database exists, it creates the database and schema. `EnsureCreated` enables the following workflow for handling data model changes:

* Delete the database. Any existing data is lost.
* Change the data model. For example, add an `EmailAddress` field.
* Run the app.
* `EnsureCreated` creates a database with the new schema.

This workflow works early in development when the schema is rapidly evolving, as long as data doesn't need to be preserved. The situation is different when data that has been entered into the database needs to be preserved. When that is the case, use migrations.

Later in the tutorial series, the database is deleted that was created by `EnsureCreated` and migrations is used. A database that is created by `EnsureCreated` can't be updated by using migrations.

### Test the app

* Run the app.
* Select the **Students** link and then **Create New**.
* Test the Edit, Details, and Delete links.

## Seed the database

The `EnsureCreated` method creates an empty database. This section adds code that populates the database with test data.

Create `Data/DbInitializer.cs` with the following code:

```
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2019-09-01")},
                new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2017-09-01")},
                new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2018-09-01")},
                new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2017-09-01")},
                new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2017-09-01")},
                new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2016-09-01")},
                new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2018-09-01")},
                new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2019-09-01")}
            };

            context.Students.AddRange(students);
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course{CourseID=1050,Title="Chemistry",Credits=3},
                new Course{CourseID=4022,Title="Microeconomics",Credits=3},
                new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
                new Course{CourseID=1045,Title="Calculus",Credits=4},
                new Course{CourseID=3141,Title="Trigonometry",Credits=4},
                new Course{CourseID=2021,Title="Composition",Credits=3},
                new Course{CourseID=2042,Title="Literature",Credits=4}
            };

            context.Courses.AddRange(courses);
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
                new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
                new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
                new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
                new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
                new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
                new Enrollment{StudentID=3,CourseID=1050},
                new Enrollment{StudentID=4,CourseID=1050},
                new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
                new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
                new Enrollment{StudentID=6,CourseID=1045},
                new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
            };

            context.Enrollments.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}
```

The code checks if there are any students in the database. If there are no students, it adds test data to the database. It creates the test data in arrays rather than `List<T>` collections to optimize performance.

* In `Program.cs`, remove `//` from the `DbInitializer.Initialize` line:

```
using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<SchoolContext>();
    context.Database.EnsureCreated();
    DbInitializer.Initialize(context);
}
```

* [Visual Studio](#tabpanel_9_visual-studio)
* [Visual Studio Code](#tabpanel_9_visual-studio-code)

- Stop the app if it's running, and run the following command in the **Package Manager Console** (PMC):

  ```
  Drop-Database -Confirm
  ```

- Respond with `Y` to delete the database.

* Stop the app if it's running, and delete the `CU.db` file.

- Restart the app.
- Select the Students page to see the seeded data.

## View the database

* [Visual Studio](#tabpanel_10_visual-studio)
* [Visual Studio Code](#tabpanel_10_visual-studio-code)

- Open **SQL Server Object Explorer** (SSOX) from the **View** menu in Visual Studio.
- In SSOX, select **(localdb)\MSSQLLocalDB > Databases > SchoolContext-{GUID}**. The database name is generated from the context name provided earlier plus a dash and a GUID.
- Expand the **Tables** node.
- Right-click the **Student** table and click **View Data** to see the columns created and the rows inserted into the table.
- Right-click the **Student** table and click **View Code** to see how the `Student` model maps to the `Student` table schema.

Use a SQLite tool to view the database schema and seeded data. The database file is named `CU.db` and is located in the project folder.

## Asynchronous EF methods in ASP.NET Core web apps

Asynchronous programming is the default mode for ASP.NET Core and EF Core.

A web server has a limited number of threads available, and in high load situations all of the available threads might be in use. When that happens, the server can't process new requests until the threads are freed up. With synchronous code, many threads may be tied up while they aren't doing work because they're waiting for I/O to complete. With asynchronous code, when a process is waiting for I/O to complete, its thread is freed up for the server to use for processing other requests. As a result, asynchronous code enables server resources to be used more efficiently, and the server can handle more traffic without delays.

Asynchronous code does introduce a small amount of overhead at run time. For low traffic situations, the performance hit is negligible, while for high traffic situations, the potential performance improvement is substantial.

In the following code, the [async](/en-us/dotnet/csharp/language-reference/keywords/async) keyword, `Task` return value, `await` keyword, and `ToListAsync` method make the code execute asynchronously.

```
public async Task OnGetAsync()
{
    Students = await _context.Students.ToListAsync();
}
```

* The `async` keyword tells the compiler to:

  * Generate callbacks for parts of the method body.
  * Create the [Task](/en-us/dotnet/csharp/programming-guide/concepts/async/async-return-types#BKMK_TaskReturnType) object that's returned.

* The `Task` return type represents ongoing work.

* The `await` keyword causes the compiler to split the method into two parts. The first part ends with the operation that's started asynchronously. The second part is put into a callback method that's called when the operation completes.

* `ToListAsync` is the asynchronous version of the `ToList` extension method.

Some things to be aware of when writing asynchronous code that uses EF Core:

* Only statements that cause queries or commands to be sent to the database are executed asynchronously. That includes `ToListAsync`, `SingleOrDefaultAsync`, `FirstOrDefaultAsync`, and `SaveChangesAsync`. It doesn't include statements that just change an `IQueryable`, such as `var students = context.Students.Where(s => s.LastName == "Davolio")`.
* An EF Core context isn't thread safe: don't try to do multiple operations in parallel.
* To take advantage of the performance benefits of async code, verify that library packages (such as for paging) use async if they call EF Core methods that send queries to the database.

For more information about asynchronous programming in .NET, see [Async Overview](/en-us/dotnet/standard/async) and [Asynchronous programming with async and await](/en-us/dotnet/csharp/programming-guide/concepts/async/).

Warning

The async implementation of [Microsoft.Data.SqlClient](https://github.com/dotnet/SqlClient) has some known issues ([#593](https://github.com/dotnet/SqlClient/issues/593), [#601](https://github.com/dotnet/SqlClient/issues/601), and others). If you're seeing unexpected performance problems, try using sync command execution instead, especially when dealing with large text or binary values.

## Performance considerations

In general, a web page shouldn't be loading an arbitrary number of rows. A query should use paging or a limiting approach. For example, the preceding query could use `Take` to limit the rows returned:

```
public async Task OnGetAsync()
{
    Student = await _context.Students.Take(10).ToListAsync();
}
```

Enumerating a large table in a view could return a partially constructed HTTP 200 response if a database exception occurs part way through the enumeration.

Paging is covered later in the tutorial.

For more information, see [Performance considerations (EF)](/en-us/ef/core/performance).

## Next steps

[Use SQLite for development, SQL Server for production](../../tutorials/razor-pages/model?view=aspnetcore-10.0\&tabs=visual-studio-code#use-sqlite-for-development-sql-server-for-production)

[Next tutorial](crud?view=aspnetcore-10.0)

This is the first in a series of tutorials that show how to use Entity Framework (EF) Core in an [ASP.NET Core Razor Pages](../../razor-pages/?view=aspnetcore-10.0) app. The tutorials build a web site for a fictional Contoso University. The site includes functionality such as student admission, course creation, and instructor assignments. The tutorial uses the code first approach. For information on following this tutorial using the database first approach, see [this Github issue](https://github.com/dotnet/AspNetCore.Docs/issues/16897).

[Download or view the completed app.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu50) [Download instructions](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

## Prerequisites

* If you're new to Razor Pages, go through the [Get started with Razor Pages](../../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0) tutorial series before starting this one.

- [Visual Studio](#tabpanel_11_visual-studio)
- [Visual Studio Code](#tabpanel_11_visual-studio-code)

* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) with the **ASP.NET and web development** workload.

### Database engines

The Visual Studio instructions use [SQL Server LocalDB](/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb), a version of SQL Server Express that runs only on Windows.

* [Visual Studio Code](https://code.visualstudio.com/download)
* [C# Dev Kit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit)
* [Latest released .NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core)

You can follow the Visual Studio Code instructions on macOS, Linux, or Windows. Changes may be required if you use an integrated development environment (IDE) other than Visual Studio Code.

Consider downloading and installing a third-party tool for managing and viewing a SQLite database, such as [DB Browser for SQLite](https://sqlitebrowser.org/).

### Database engines

The Visual Studio Code tab use [SQLite](https://www.sqlite.org/), a cross-platform database engine.

### Troubleshooting

If you run into a problem you can't resolve, compare your code to the [completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples). A good way to get help is by posting a question to StackOverflow\.com, using the [ASP.NET Core tag](https://stackoverflow.com/questions/tagged/asp.net-core) or the [EF Core tag](https://stackoverflow.com/questions/tagged/entity-framework-core).

### The sample app

The app built in these tutorials is a basic university web site. Users can view and update student, course, and instructor information. Here are a few of the screens created in the tutorial.

The UI style of this site is based on the built-in project templates. The tutorial's focus is on how to use EF Core with ASP.NET Core, not how to customize the UI.

[]()

### Optional: Build the sample download

This step is optional. Building the completed app is recommended when you have problems you can't solve. If you run into a problem you can't resolve, compare your code to the [completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu50). [Download instructions](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

* [Visual Studio](#tabpanel_12_visual-studio)
* [Visual Studio Code](#tabpanel_12_visual-studio-code)

Select `ContosoUniversity.csproj` to open the project.

* Build the project.
* In Package Manager Console (PMC) run the following command:

```
Update-Database
```

* Remove the comments from the `ContosoUniversity.csproj` file so `SQLiteVersion` is defined:

  ```
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <DefineConstants>TRACE;SQLiteVersion</DefineConstants>
  </PropertyGroup>
  ```

* In a command window, update the database:

  ```
  dotnet tool install --global dotnet-ef
  dotnet ef database update
  ```

Run the project to seed the database.

## Create the web app project

* [Visual Studio](#tabpanel_13_visual-studio)
* [Visual Studio Code](#tabpanel_13_visual-studio-code)

1. Start Visual Studio and select **Create a new project**.

2. In the **Create a new project** dialog, select **ASP.NET Core Web Application** > **Next**.

3. In the **Configure your new project** dialog, enter `ContosoUniversity` for **Project name**. It's important to use this exact name including capitalization, so each `namespace` matches when code is copied.

4. Select **Create**.

5. In the **Create a new ASP.NET Core web application** dialog, select:

   1. **.NET Core** and **ASP.NET Core 5.0** in the dropdowns.
   2. **ASP.NET Core Web App**.
   3. **Create**

* In a terminal, navigate to the folder in which the project folder should be created.

* Run the following commands to create a Razor Pages project and `cd` into the new project folder:

  ```
  dotnet new webapp -o ContosoUniversity
  cd ContosoUniversity  
  ```

## Set up the site style

Copy and paste the following code into the `Pages/Shared/_Layout.cshtml` file:

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Contoso University</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">Contoso University</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/About">About</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Students/Index">Students</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Courses/Index">Courses</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Instructors/Index">Instructors</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Departments/Index">Departments</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2021 - Contoso University - <a asp-area="" asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>
```

The layout file sets the site header, footer, and menu. The preceding code makes the following changes:

* Each occurrence of "ContosoUniversity" to "Contoso University". There are three occurrences.
* The **Home** and **Privacy** menu entries are deleted.
* Entries are added for **About**, **Students**, **Courses**, **Instructors**, and **Departments**.

In `Pages/Index.cshtml`, replace the contents of the file with the following code:

```
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="row mb-auto">
    <div class="col-md-4">
        <div class="row no-gutters border mb-4">
            <div class="col p-4 mb-4 ">
                <p class="card-text">
                    Contoso University is a sample application that
                    demonstrates how to use Entity Framework Core in an
                    ASP.NET Core Razor Pages web app.
                </p>
            </div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="row no-gutters border mb-4">
            <div class="col p-4 d-flex flex-column position-static">
                <p class="card-text mb-auto">
                    You can build the application by following the steps in a series of tutorials.
                </p>
                <p>
                    <a href="https://learn.microsoft.com/aspnet/core/data/ef-rp/intro" class="stretched-link">See the tutorial</a>
                </p>
            </div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="row no-gutters border mb-4">
            <div class="col p-4 d-flex flex-column">
                <p class="card-text mb-auto">
                    You can download the completed project from GitHub.
                </p>
                <p>
                    <a href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples" class="stretched-link">See project source code</a>
                </p>
            </div>
        </div>
    </div>
</div>
```

The preceding code replaces the text about ASP.NET Core with text about this app.

Run the app to verify that the home page appears.

## The data model

The following sections create a data model:

A student can enroll in any number of courses, and a course can have any number of students enrolled in it.

## The Student entity

* Create a *Models* folder in the project folder.

* Create `Models/Student.cs` with the following code:

  ```
  using System;
  using System.Collections.Generic;

  namespace ContosoUniversity.Models
  {
      public class Student
      {
          public int ID { get; set; }
          public string LastName { get; set; }
          public string FirstMidName { get; set; }
          public DateTime EnrollmentDate { get; set; }

          public ICollection<Enrollment> Enrollments { get; set; }
      }
  }
  ```

The `ID` property becomes the primary key column of the database table that corresponds to this class. By default, EF Core interprets a property that's named `ID` or `classnameID` as the primary key. So the alternative automatically recognized name for the `Student` class primary key is `StudentID`. For more information, see [EF Core - Keys](/en-us/ef/core/modeling/keys?tabs=data-annotations).

The `Enrollments` property is a [navigation property](/en-us/ef/core/modeling/relationships). Navigation properties hold other entities that are related to this entity. In this case, the `Enrollments` property of a `Student` entity holds all of the `Enrollment` entities that are related to that Student. For example, if a Student row in the database has two related Enrollment rows, the `Enrollments` navigation property contains those two Enrollment entities.

In the database, an Enrollment row is related to a Student row if its `StudentID` column contains the student's ID value. For example, suppose a Student row has ID=1. Related Enrollment rows will have `StudentID` = 1. `StudentID` is a *foreign key* in the Enrollment table.

The `Enrollments` property is defined as `ICollection<Enrollment>` because there may be multiple related Enrollment entities. Other collection types can be used, such as `List<Enrollment>` or `HashSet<Enrollment>`. When `ICollection<Enrollment>` is used, EF Core creates a `HashSet<Enrollment>` collection by default.

## The Enrollment entity

Create `Models/Enrollment.cs` with the following code:

```
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}
```

The `EnrollmentID` property is the primary key; this entity uses the `classnameID` pattern instead of `ID` by itself. For a production data model, many developers choose one pattern and use it consistently. This tutorial uses both just to illustrate that both work. Using `ID` without `classname` makes it easier to implement some kinds of data model changes.

The `Grade` property is an `enum`. The question mark after the `Grade` type declaration indicates that the `Grade` property is [nullable](/en-us/dotnet/csharp/programming-guide/nullable-types/). A grade that's null is different from a zero grade—null means a grade isn't known or hasn't been assigned yet.

The `StudentID` property is a foreign key, and the corresponding navigation property is `Student`. An `Enrollment` entity is associated with one `Student` entity, so the property contains a single `Student` entity.

The `CourseID` property is a foreign key, and the corresponding navigation property is `Course`. An `Enrollment` entity is associated with one `Course` entity.

EF Core interprets a property as a foreign key if it's named `<navigation property name><primary key property name>`. For example,`StudentID` is the foreign key for the `Student` navigation property, since the `Student` entity's primary key is `ID`. Foreign key properties can also be named `<primary key property name>`. For example, `CourseID` since the `Course` entity's primary key is `CourseID`.

## The Course entity

Create `Models/Course.cs` with the following code:

```
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int CourseID { get; set; }
        public string Title { get; set; }
        public int Credits { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The `Enrollments` property is a navigation property. A `Course` entity can be related to any number of `Enrollment` entities.

The `DatabaseGenerated` attribute allows the app to specify the primary key rather than having the database generate it.

Build the project to validate that there are no compiler errors.

## Scaffold Student pages

In this section, the ASP.NET Core scaffolding tool is used to generate:

* An EF Core `DbContext` class. The context is the main class that coordinates Entity Framework functionality for a given data model. It derives from the [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) class.
* Razor pages that handle Create, Read, Update, and Delete (CRUD) operations for the `Student` entity.

- [Visual Studio](#tabpanel_14_visual-studio)
- [Visual Studio Code](#tabpanel_14_visual-studio-code)

* Create a *Pages/Students* folder.

* In **Solution Explorer**, right-click the *Pages/Students* folder and select **Add** > **New Scaffolded Item**.

* In the **Add New Scaffold Item** dialog:

  * In the left tab, select **Installed > Common > Razor Pages**
  * Select **Razor Pages using Entity Framework (CRUD)** > **ADD**.

* In the **Add Razor Pages using Entity Framework (CRUD)** dialog:

  * In the **Model class** drop-down, select **Student (ContosoUniversity.Models)**.

  * In the **Data context class** row, select the **+** (plus) sign.

    * Change the data context name to end in `SchoolContext` rather than `ContosoUniversityContext`. The updated context name: `ContosoUniversity.Data.SchoolContext`
    * Select **Add** to finish adding the data context class.
    * Select **Add** to finish the **Add Razor Pages** dialog.

If scaffolding fails with the error `'Install the package Microsoft.VisualStudio.Web.CodeGeneration.Design and try again.'`, run the scaffold tool again or see [this GitHub issue](https://github.com/dotnet/Scaffolding/issues/1540).

The following packages are automatically installed:

* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.EntityFrameworkCore.Tools`
* `Microsoft.VisualStudio.Web.CodeGeneration.Design`

- Run the following .NET CLI commands to install required NuGet packages:

  ```
  dotnet add package Microsoft.EntityFrameworkCore.SQLite -v 5.0.0-*
  dotnet add package Microsoft.EntityFrameworkCore.SqlServer -v 5.0.0-*
  dotnet add package Microsoft.EntityFrameworkCore.Design -v 5.0.0-*
  dotnet add package Microsoft.EntityFrameworkCore.Tools -v 5.0.0-*
  dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design -v 5.0.0-*
  dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore -v 5.0.0-*  
  ```

  The `Microsoft.VisualStudio.Web.CodeGeneration.Design` package is required for scaffolding. Although the app won't use SQL Server, the scaffolding tool needs the SQL Server package.

- Create a *Pages/Students* folder.

- Run the following command to install the [aspnet-codegenerator scaffolding tool](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

  ```
  dotnet tool install --global dotnet-aspnet-codegenerator
  ```

- Run the following command to scaffold Student pages.

  **On Windows**

  ```
  dotnet aspnet-codegenerator razorpage -m Student -dc ContosoUniversity.Data.SchoolContext -udl -outDir Pages\Students --referenceScriptLibraries -sqlite  
  ```

  **On macOS or Linux**

  ```
  dotnet aspnet-codegenerator razorpage -m Student -dc ContosoUniversity.Data.SchoolContext -udl -outDir Pages/Students --referenceScriptLibraries -dbProvider sqlite 
  ```

If the preceding step fails, build the project and retry the scaffold step.

The scaffolding process:

* Creates Razor pages in the *Pages/Students* folder:

  * `Create.cshtml` and `Create.cshtml.cs`
  * `Delete.cshtml` and `Delete.cshtml.cs`
  * `Details.cshtml` and `Details.cshtml.cs`
  * `Edit.cshtml` and `Edit.cshtml.cs`
  * `Index.cshtml` and `Index.cshtml.cs`

* Creates `Data/SchoolContext.cs`.

* Adds the context to dependency injection in `Startup.cs`.

* Adds a database connection string to `appsettings.json`.

## Database connection string

The scaffolding tool generates a connection string in the `appsettings.json` file.

* [Visual Studio](#tabpanel_15_visual-studio)
* [Visual Studio Code](#tabpanel_15_visual-studio-code)

The connection string specifies [SQL Server LocalDB](/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb):

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

LocalDB is a lightweight version of the SQL Server Express Database Engine and is intended for app development, not production use. By default, LocalDB creates *.mdf* files in the `C:/Users/<user>` directory.

Shorten the SQLite connection string to `CU.db`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Data Source=CU.db"
  }
}
```

Don't change the directory without making sure it's valid.

## Update the database context class

The main class that coordinates EF Core functionality for a given data model is the database context class. The context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext). The context specifies which entities are included in the data model. In this project, the class is named `SchoolContext`.

Update `Data/SchoolContext.cs` with the following code:

```
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext (DbContextOptions<SchoolContext> options)
            : base(options)
        {
        }

        public DbSet<Student> Students { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Course> Courses { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
        }
    }
}
```

The preceding code changes from the singular `DbSet<Student> Student` to the plural `DbSet<Student> Students`. To make the Razor Pages code match the new `DBSet` name, make a global change from: `_context.Student.` to: `_context.Students.`

There are 8 occurrences.

Because an entity set contains multiple entities, many developers prefer the `DBSet` property names should be plural.

The highlighted code:

* Creates a [DbSet\<TEntity>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for each entity set. In EF Core terminology:

  * An entity set typically corresponds to a database table.
  * An entity corresponds to a row in the table.

* Calls [OnModelCreating](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.onmodelcreating). `OnModelCreating`:

  * Is called when `SchoolContext` has been initialized but before the model has been secured and used to initialize the context.
  * Is required because later in the tutorial the `Student` entity will have references to the other entities.

Build the project to verify there are no compiler errors.

## Startup.cs

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services such as the `SchoolContext` are registered with dependency injection during app startup. Components that require these services, such as Razor Pages, are provided these services via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically registered the context class with the dependency injection container.

* [Visual Studio](#tabpanel_16_visual-studio)
* [Visual Studio Code](#tabpanel_16_visual-studio-code)

The following highlighted lines were added by the scaffolder:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));
}
```

Verify the code added by the scaffolder calls `UseSqlite`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
         options.UseSqlite(Configuration.GetConnectionString("SchoolContext")));
}
```

See [Use SQLite for development, SQL Server for production](../../tutorials/razor-pages/model?view=aspnetcore-10.0\&tabs=visual-studio-code#use-sqlite-for-development-sql-server-for-production) for information on using a production database.

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

[]()

### Add the database exception filter

Add [AddDatabaseDeveloperPageExceptionFilter](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.databasedeveloperpageexceptionfilterserviceextensions.adddatabasedeveloperpageexceptionfilter) and [UseMigrationsEndPoint](/en-us/dotnet/api/microsoft.aspnetcore.builder.migrationsendpointextensions.usemigrationsendpoint) as shown in the following code:

* [Visual Studio](#tabpanel_17_visual-studio)
* [Visual Studio Code](#tabpanel_17_visual-studio-code)

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
       options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));

    services.AddDatabaseDeveloperPageExceptionFilter();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseMigrationsEndPoint();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

Add the [Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore) NuGet package.

In the Package Manager Console, enter the following to add the NuGet package:

```
Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
```

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
       options.UseSqlite(Configuration.GetConnectionString("SchoolContext")));

    services.AddDatabaseDeveloperPageExceptionFilter();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseMigrationsEndPoint();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}
```

The `Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore` NuGet package provides ASP.NET Core middleware for Entity Framework Core error pages. This middleware helps to detect and diagnose errors with Entity Framework Core migrations.

The `AddDatabaseDeveloperPageExceptionFilter` provides helpful error information in the [development environment](../../fundamentals/environments?view=aspnetcore-10.0) for EF migrations errors.

## Create the database

Update `Program.cs` to create the database if it doesn't exist:

```
using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            CreateDbIfNotExists(host);

            host.Run();
        }

        private static void CreateDbIfNotExists(IHost host)
        {
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    var context = services.GetRequiredService<SchoolContext>();
                    context.Database.EnsureCreated();
                    // DbInitializer.Initialize(context);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

The [EnsureCreated](/en-us/dotnet/api/microsoft.entityframeworkcore.infrastructure.databasefacade.ensurecreated) method takes no action if a database for the context exists. If no database exists, it creates the database and schema. `EnsureCreated` enables the following workflow for handling data model changes:

* Delete the database. Any existing data is lost.
* Change the data model. For example, add an `EmailAddress` field.
* Run the app.
* `EnsureCreated` creates a database with the new schema.

This workflow works early in development when the schema is rapidly evolving, as long as data doesn't need to be preserved. The situation is different when data that has been entered into the database needs to be preserved. When that is the case, use migrations.

Later in the tutorial series, the database is deleted that was created by `EnsureCreated` and migrations is used. A database that is created by `EnsureCreated` can't be updated by using migrations.

### Test the app

* Run the app.
* Select the **Students** link and then **Create New**.
* Test the Edit, Details, and Delete links.

## Seed the database

The `EnsureCreated` method creates an empty database. This section adds code that populates the database with test data.

Create `Data/DbInitializer.cs` with the following code:

```
using ContosoUniversity.Models;
using System;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2019-09-01")},
                new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2017-09-01")},
                new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2018-09-01")},
                new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2017-09-01")},
                new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2017-09-01")},
                new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2016-09-01")},
                new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2018-09-01")},
                new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2019-09-01")}
            };

            context.Students.AddRange(students);
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course{CourseID=1050,Title="Chemistry",Credits=3},
                new Course{CourseID=4022,Title="Microeconomics",Credits=3},
                new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
                new Course{CourseID=1045,Title="Calculus",Credits=4},
                new Course{CourseID=3141,Title="Trigonometry",Credits=4},
                new Course{CourseID=2021,Title="Composition",Credits=3},
                new Course{CourseID=2042,Title="Literature",Credits=4}
            };

            context.Courses.AddRange(courses);
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
                new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
                new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
                new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
                new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
                new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
                new Enrollment{StudentID=3,CourseID=1050},
                new Enrollment{StudentID=4,CourseID=1050},
                new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
                new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
                new Enrollment{StudentID=6,CourseID=1045},
                new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
            };

            context.Enrollments.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}
```

The code checks if there are any students in the database. If there are no students, it adds test data to the database. It creates the test data in arrays rather than `List<T>` collections to optimize performance.

* In `Program.cs`, remove `//` from the `DbInitializer.Initialize` line:

  ```
    context.Database.EnsureCreated();
    DbInitializer.Initialize(context);
  ```

- [Visual Studio](#tabpanel_18_visual-studio)
- [Visual Studio Code](#tabpanel_18_visual-studio-code)

* Stop the app if it's running, and run the following command in the **Package Manager Console** (PMC):

  ```
  Drop-Database -Confirm
  ```

* Respond with `Y` to delete the database.

- Stop the app if it's running, and delete the `CU.db` file.

* Restart the app.
* Select the Students page to see the seeded data.

## View the database

* [Visual Studio](#tabpanel_19_visual-studio)
* [Visual Studio Code](#tabpanel_19_visual-studio-code)

- Open **SQL Server Object Explorer** (SSOX) from the **View** menu in Visual Studio.
- In SSOX, select **(localdb)\MSSQLLocalDB > Databases > SchoolContext-{GUID}**. The database name is generated from the context name provided earlier plus a dash and a GUID.
- Expand the **Tables** node.
- Right-click the **Student** table and click **View Data** to see the columns created and the rows inserted into the table.
- Right-click the **Student** table and click **View Code** to see how the `Student` model maps to the `Student` table schema.

Use a SQLite tool to view the database schema and seeded data. The database file is named `CU.db` and is located in the project folder.

## Asynchronous code

Asynchronous programming is the default mode for ASP.NET Core and EF Core.

A web server has a limited number of threads available, and in high load situations all of the available threads might be in use. When that happens, the server can't process new requests until the threads are freed up. With synchronous code, many threads may be tied up while they aren't doing work because they're waiting for I/O to complete. With asynchronous code, when a process is waiting for I/O to complete, its thread is freed up for the server to use for processing other requests. As a result, asynchronous code enables server resources to be used more efficiently, and the server can handle more traffic without delays.

Asynchronous code does introduce a small amount of overhead at run time. For low traffic situations, the performance hit is negligible, while for high traffic situations, the potential performance improvement is substantial.

In the following code, the [async](/en-us/dotnet/csharp/language-reference/keywords/async) keyword, `Task` return value, `await` keyword, and `ToListAsync` method make the code execute asynchronously.

```
public async Task OnGetAsync()
{
    Students = await _context.Students.ToListAsync();
}
```

* The `async` keyword tells the compiler to:

  * Generate callbacks for parts of the method body.
  * Create the [Task](/en-us/dotnet/csharp/programming-guide/concepts/async/async-return-types#BKMK_TaskReturnType) object that's returned.

* The `Task` return type represents ongoing work.

* The `await` keyword causes the compiler to split the method into two parts. The first part ends with the operation that's started asynchronously. The second part is put into a callback method that's called when the operation completes.

* `ToListAsync` is the asynchronous version of the `ToList` extension method.

Some things to be aware of when writing asynchronous code that uses EF Core:

* Only statements that cause queries or commands to be sent to the database are executed asynchronously. That includes `ToListAsync`, `SingleOrDefaultAsync`, `FirstOrDefaultAsync`, and `SaveChangesAsync`. It doesn't include statements that just change an `IQueryable`, such as `var students = context.Students.Where(s => s.LastName == "Davolio")`.
* An EF Core context isn't thread safe: don't try to do multiple operations in parallel.
* To take advantage of the performance benefits of async code, verify that library packages (such as for paging) use async if they call EF Core methods that send queries to the database.

For more information about asynchronous programming in .NET, see [Async Overview](/en-us/dotnet/standard/async) and [Asynchronous programming with async and await](/en-us/dotnet/csharp/programming-guide/concepts/async/).

## Performance considerations

In general, a web page shouldn't be loading an arbitrary number of rows. A query should use paging or a limiting approach. For example, the preceding query could use `Take` to limit the rows returned:

```
public async Task OnGetAsync()
{
    Student = await _context.Students.Take(10).ToListAsync();
}
```

Enumerating a large table in a view could return a partially constructed HTTP 200 response if a database exception occurs part way through the enumeration.

[MaxModelBindingCollectionSize](/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions.maxmodelbindingcollectionsize#microsoft-aspnetcore-mvc-mvcoptions-maxmodelbindingcollectionsize) defaults to 1024. The following code sets `MaxModelBindingCollectionSize`:

```
public void ConfigureServices(IServiceCollection services)
{
    var myMaxModelBindingCollectionSize = Convert.ToInt32(
                Configuration["MyMaxModelBindingCollectionSize"] ?? "100");

    services.Configure<MvcOptions>(options =>
           options.MaxModelBindingCollectionSize = myMaxModelBindingCollectionSize);

    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
          options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));

    services.AddDatabaseDeveloperPageExceptionFilter();
}
```

See [Configuration](../../fundamentals/configuration/?view=aspnetcore-10.0) for information on configuration settings like `MyMaxModelBindingCollectionSize`.

Paging is covered later in the tutorial.

For more information, see [Performance considerations (EF)](/en-us/dotnet/framework/data/adonet/ef/performance-considerations).

## SQL Logging of Entity Framework Core

Logging configuration is commonly provided by the `Logging` section of `appsettings.{Environment}.json` files. To log SQL statements, add `"Microsoft.EntityFrameworkCore.Database.Command": "Information"` to the `appsettings.Development.json` file:

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDB-2;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
     ,"Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

With the preceding JSON, SQL statements are displayed on the command line and in the Visual Studio output window.

For more information, see the following resources:

* [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0#configuration)
* [ASP.NET template disables EF Core SQL logging by default (`dotnet/aspnetcore` #32977)](https://github.com/dotnet/aspnetcore/issues/32977)

## Next steps

[Use SQLite for development, SQL Server for production](../../tutorials/razor-pages/model?view=aspnetcore-10.0\&tabs=visual-studio-code#use-sqlite-for-development-sql-server-for-production)

[Next tutorial](crud?view=aspnetcore-10.0)

This is the first in a series of tutorials that show how to use Entity Framework (EF) Core in an [ASP.NET Core Razor Pages](../../razor-pages/?view=aspnetcore-10.0) app. The tutorials build a web site for a fictional Contoso University. The site includes functionality such as student admission, course creation, and instructor assignments. The tutorial uses the code first approach. For information on following this tutorial using the database first approach, see [this Github issue](https://github.com/dotnet/AspNetCore.Docs/issues/16897).

[Download or view the completed app.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) [Download instructions](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample).

## Prerequisites

* If you're new to Razor Pages, go through the [Get started with Razor Pages](../../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0) tutorial series before starting this one.

- [Visual Studio](#tabpanel_20_visual-studio)
- [Visual Studio Code](#tabpanel_20_visual-studio-code)

* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) with the **ASP.NET and web development** workload.

- [Visual Studio Code](https://code.visualstudio.com/download)
- [C# Dev Kit for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit)
- [Latest released .NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core)

You can follow the Visual Studio Code instructions on macOS, Linux, or Windows. Changes may be required if you use an integrated development environment (IDE) other than Visual Studio Code.

## Database engines

The Visual Studio instructions use [SQL Server LocalDB](/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb), a version of SQL Server Express that runs only on Windows.

The Visual Studio Code instructions use [SQLite](https://www.sqlite.org/), a cross-platform database engine.

If you choose to use SQLite, download and install a third-party tool for managing and viewing a SQLite database, such as [DB Browser for SQLite](https://sqlitebrowser.org/).

## Troubleshooting

If you run into a problem you can't resolve, compare your code to the [completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples). A good way to get help is by posting a question to StackOverflow\.com, using the [ASP.NET Core tag](https://stackoverflow.com/questions/tagged/asp.net-core) or the [EF Core tag](https://stackoverflow.com/questions/tagged/entity-framework-core).

## The sample app

The app built in these tutorials is a basic university web site. Users can view and update student, course, and instructor information. Here are a few of the screens created in the tutorial.

The UI style of this site is based on the built-in project templates. The tutorial's focus is on how to use EF Core, not how to customize the UI.

Follow the link at the top of the page to get the source code for the completed project. The *cu30* folder has the code for the ASP.NET Core 3.0 version of the tutorial. Files that reflect the state of the code for tutorials 1-7 can be found in the *cu30snapshots* folder.

* [Visual Studio](#tabpanel_21_visual-studio)
* [Visual Studio Code](#tabpanel_21_visual-studio-code)

To run the app after downloading the completed project:

* Build the project.

* In Package Manager Console (PMC) run the following command:

  ```
  Update-Database
  ```

* Run the project to seed the database.

To run the app after downloading the completed project:

* Delete `ContosoUniversity.csproj`, and rename `ContosoUniversitySQLite.csproj` to `ContosoUniversity.csproj`.

* In `Program.cs`, comment out `#define Startup` so `StartupSQLite` is used.

* Delete `appsettings.json`, and rename `appSettingsSQLite.json` to `appsettings.json`.

* Delete the *Migrations* folder, and rename *MigrationsSQL* to *Migrations*.

* Do a global search for `#if SQLiteVersion` and remove `#if SQLiteVersion` and the associated `#endif` statement.

* Build the project.

* At a command prompt in the project folder, run the following commands:

  ```
  dotnet tool uninstall --global dotnet-ef
  dotnet tool install --global dotnet-ef --version 5.0.0-*
  dotnet ef database update
  ```

* In your SQLite tool, run the following SQL statement:

  ```
  UPDATE Department SET RowVersion = randomblob(8)
  ```

* Run the project to seed the database.

## Create the web app project

* [Visual Studio](#tabpanel_22_visual-studio)
* [Visual Studio Code](#tabpanel_22_visual-studio-code)

- From the Visual Studio **File** menu, select **New** > **Project**.
- Select **ASP.NET Core Web Application**.
- Name the project *ContosoUniversity*. It's important to use this exact name including capitalization, so the namespaces match when code is copied and pasted.
- Select **.NET Core** and **ASP.NET Core 3.0** in the dropdowns, and then select **Web Application**.

* In a terminal, navigate to the folder in which the project folder should be created.

* Run the following commands to create a Razor Pages project and `cd` into the new project folder:

  ```
  dotnet new webapp -o ContosoUniversity
  cd ContosoUniversity
  ```

## Set up the site style

Set up the site header, footer, and menu by updating `Pages/Shared/_Layout.cshtml`:

* Change each occurrence of "ContosoUniversity" to "Contoso University". There are three occurrences.

* Delete the **Home** and **Privacy** menu entries, and add entries for **About**, **Students**, **Courses**, **Instructors**, and **Departments**.

The changes are highlighted.

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Contoso University</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">Contoso University</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/About">About</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Students/Index">Students</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Courses/Index">Courses</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Instructors/Index">Instructors</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Departments/Index">Departments</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2019 - Contoso University - <a asp-area="" asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>
```

In `Pages/Index.cshtml`, replace the contents of the file with the following code to replace the text about ASP.NET Core with text about this app:

```
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="row mb-auto">
    <div class="col-md-4">
        <div class="row no-gutters border mb-4">
            <div class="col p-4 mb-4 ">
                <p class="card-text">
                    Contoso University is a sample application that
                    demonstrates how to use Entity Framework Core in an
                    ASP.NET Core Razor Pages web app.
                </p>
            </div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="row no-gutters border mb-4">
            <div class="col p-4 d-flex flex-column position-static">
                <p class="card-text mb-auto">
                    You can build the application by following the steps in a series of tutorials.
                </p>
                <p>
                    <a href="https://learn.microsoft.com/aspnet/core/data/ef-rp/intro" class="stretched-link">See the tutorial</a>
                </p>
            </div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="row no-gutters border mb-4">
            <div class="col p-4 d-flex flex-column">
                <p class="card-text mb-auto">
                    You can download the completed project from GitHub.
                </p>
                <p>
                    <a href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples" class="stretched-link">See project source code</a>
                </p>
            </div>
        </div>
    </div>
</div>
```

Run the app to verify that the home page appears.

## The data model

The following sections create a data model:

A student can enroll in any number of courses, and a course can have any number of students enrolled in it.

## The Student entity

* Create a *Models* folder in the project folder.

* Create `Models/Student.cs` with the following code:

  ```
  using System;
  using System.Collections.Generic;

  namespace ContosoUniversity.Models
  {
      public class Student
      {
          public int ID { get; set; }
          public string LastName { get; set; }
          public string FirstMidName { get; set; }
          public DateTime EnrollmentDate { get; set; }

          public ICollection<Enrollment> Enrollments { get; set; }
      }
  }
  ```

The `ID` property becomes the primary key column of the database table that corresponds to this class. By default, EF Core interprets a property that's named `ID` or `classnameID` as the primary key. So the alternative automatically recognized name for the `Student` class primary key is `StudentID`. For more information, see [EF Core - Keys](/en-us/ef/core/modeling/keys?tabs=data-annotations).

The `Enrollments` property is a [navigation property](/en-us/ef/core/modeling/relationships). Navigation properties hold other entities that are related to this entity. In this case, the `Enrollments` property of a `Student` entity holds all of the `Enrollment` entities that are related to that Student. For example, if a Student row in the database has two related Enrollment rows, the `Enrollments` navigation property contains those two Enrollment entities.

In the database, an Enrollment row is related to a Student row if its StudentID column contains the student's ID value. For example, suppose a Student row has ID=1. Related Enrollment rows will have StudentID = 1. StudentID is a *foreign key* in the Enrollment table.

The `Enrollments` property is defined as `ICollection<Enrollment>` because there may be multiple related Enrollment entities. You can use other collection types, such as `List<Enrollment>` or `HashSet<Enrollment>`. When `ICollection<Enrollment>` is used, EF Core creates a `HashSet<Enrollment>` collection by default.

## The Enrollment entity

Create `Models/Enrollment.cs` with the following code:

```
namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}
```

The `EnrollmentID` property is the primary key; this entity uses the `classnameID` pattern instead of `ID` by itself. For a production data model, choose one pattern and use it consistently. This tutorial uses both just to illustrate that both work. Using `ID` without `classname` makes it easier to implement some kinds of data model changes.

The `Grade` property is an `enum`. The question mark after the `Grade` type declaration indicates that the `Grade` property is [nullable](/en-us/dotnet/csharp/programming-guide/nullable-types/). A grade that's null is different from a zero grade—null means a grade isn't known or hasn't been assigned yet.

The `StudentID` property is a foreign key, and the corresponding navigation property is `Student`. An `Enrollment` entity is associated with one `Student` entity, so the property contains a single `Student` entity.

The `CourseID` property is a foreign key, and the corresponding navigation property is `Course`. An `Enrollment` entity is associated with one `Course` entity.

EF Core interprets a property as a foreign key if it's named `<navigation property name><primary key property name>`. For example,`StudentID` is the foreign key for the `Student` navigation property, since the `Student` entity's primary key is `ID`. Foreign key properties can also be named `<primary key property name>`. For example, `CourseID` since the `Course` entity's primary key is `CourseID`.

## The Course entity

Create `Models/Course.cs` with the following code:

```
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int CourseID { get; set; }
        public string Title { get; set; }
        public int Credits { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}
```

The `Enrollments` property is a navigation property. A `Course` entity can be related to any number of `Enrollment` entities.

The `DatabaseGenerated` attribute allows the app to specify the primary key rather than having the database generate it.

Build the project to validate that there are no compiler errors.

## Scaffold Student pages

In this section, you use the ASP.NET Core scaffolding tool to generate:

* An EF Core *context* class. The context is the main class that coordinates Entity Framework functionality for a given data model. It derives from the `Microsoft.EntityFrameworkCore.DbContext` class.
* Razor pages that handle Create, Read, Update, and Delete (CRUD) operations for the `Student` entity.

- [Visual Studio](#tabpanel_23_visual-studio)
- [Visual Studio Code](#tabpanel_23_visual-studio-code)

* Create a *Students* folder in the *Pages* folder.

* In **Solution Explorer**, right-click the *Pages/Students* folder and select **Add** > **New Scaffolded Item**.

* In the **Add Scaffold** dialog, select **Razor Pages using Entity Framework (CRUD)** > **ADD**.

* In the **Add Razor Pages using Entity Framework (CRUD)** dialog:

  * In the **Model class** drop-down, select **Student (ContosoUniversity.Models)**.
  * In the **Data context class** row, select the **+** (plus) sign.
  * Change the data context name from *ContosoUniversity.Models.ContosoUniversityContext* to *ContosoUniversity.Data.SchoolContext*.
  * Select **Add**.

The following packages are automatically installed:

* `Microsoft.VisualStudio.Web.CodeGeneration.Design`
* `Microsoft.EntityFrameworkCore.SqlServer`
* `Microsoft.Extensions.Logging.Debug`
* `Microsoft.EntityFrameworkCore.Tools`

- Run the following .NET CLI commands to install required NuGet packages:

```
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.Extensions.Logging.Debug
```

The Microsoft.VisualStudio.Web.CodeGeneration.Design package is required for scaffolding. Although the app won't use SQL Server, the scaffolding tool needs the SQL Server package.

* Create a *Pages/Students* folder.

* Run the following command to install the [aspnet-codegenerator scaffolding tool](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

  ```
  dotnet tool install --global dotnet-aspnet-codegenerator
  ```

* Run the following command to scaffold Student pages.

  **On Windows**

  ```
  dotnet aspnet-codegenerator razorpage -m Student -dc ContosoUniversity.Data.SchoolContext -udl -outDir Pages\Students --referenceScriptLibraries
  ```

  **On macOS or Linux**

  ```
  dotnet aspnet-codegenerator razorpage -m Student -dc ContosoUniversity.Data.SchoolContext -udl -outDir Pages/Students --referenceScriptLibraries
  ```

If you have a problem with the preceding step, build the project and retry the scaffold step.

The scaffolding process:

* Creates Razor pages in the *Pages/Students* folder:

  * `Create.cshtml` and `Create.cshtml.cs`
  * `Delete.cshtml` and `Delete.cshtml.cs`
  * `Details.cshtml` and `Details.cshtml.cs`
  * `Edit.cshtml` and `Edit.cshtml.cs`
  * `Index.cshtml` and `Index.cshtml.cs`

* Creates `Data/SchoolContext.cs`.

* Adds the context to dependency injection in `Startup.cs`.

* Adds a database connection string to `appsettings.json`.

## Database connection string

* [Visual Studio](#tabpanel_24_visual-studio)
* [Visual Studio Code](#tabpanel_24_visual-studio-code)

The `appsettings.json` file specifies the connection string [SQL Server LocalDB](/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb).

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=SchoolContext6;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
```

LocalDB is a lightweight version of the SQL Server Express Database Engine and is intended for app development, not production use. By default, LocalDB creates *.mdf* files in the `C:/Users/<user>` directory.

Change the connection string to point to a SQLite database file named `CU.db`:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Data Source=CU.db"
  }
}
```

## Update the database context class

The main class that coordinates EF Core functionality for a given data model is the database context class. The context is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext). The context specifies which entities are included in the data model. In this project, the class is named `SchoolContext`.

Update `Data/SchoolContext.cs` with the following code:

```
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext (DbContextOptions<SchoolContext> options)
            : base(options)
        {
        }

        public DbSet<Student> Students { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Course> Courses { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
        }
    }
}
```

The highlighted code creates a [DbSet\<TEntity>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for each entity set. In EF Core terminology:

* An entity set typically corresponds to a database table.
* An entity corresponds to a row in the table.

Since an entity set contains multiple entities, the DBSet properties should be plural names. Since the scaffolding tool created a`Student` DBSet, this step changes it to plural `Students`.

To make the Razor Pages code match the new DBSet name, make a global change across the whole project of `_context.Student` to `_context.Students`. There are 8 occurrences.

Build the project to verify there are no compiler errors.

## Startup.cs

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services (such as the EF Core database context) are registered with dependency injection during application startup. Components that require these services (such as Razor Pages) are provided these services via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically registered the context class with the dependency injection container.

* [Visual Studio](#tabpanel_25_visual-studio)
* [Visual Studio Code](#tabpanel_25_visual-studio-code)

- In `ConfigureServices`, the highlighted lines were added by the scaffolder:

  ```
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddRazorPages();

      services.AddDbContext<SchoolContext>(options =>
              options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));
  }
  ```

* In `ConfigureServices`, make sure the code added by the scaffolder calls `UseSqlite`.

  ```
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddRazorPages();

      services.AddDbContext<SchoolContext>(options =>
           options.UseSqlite(Configuration.GetConnectionString("SchoolContext")));
  }
  ```

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [ASP.NET Core configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

## Create the database

Update `Program.cs` to create the database if it doesn't exist:

```
using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            CreateDbIfNotExists(host);

            host.Run();
        }

        private static void CreateDbIfNotExists(IHost host)
        {
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    var context = services.GetRequiredService<SchoolContext>();
                    context.Database.EnsureCreated();
                    // DbInitializer.Initialize(context);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

The [EnsureCreated](/en-us/dotnet/api/microsoft.entityframeworkcore.infrastructure.databasefacade.ensurecreated) method takes no action if a database for the context exists. If no database exists, it creates the database and schema. `EnsureCreated` enables the following workflow for handling data model changes:

* Delete the database. Any existing data is lost.
* Change the data model. For example, add an `EmailAddress` field.
* Run the app.
* `EnsureCreated` creates a database with the new schema.

This workflow works well early in development when the schema is rapidly evolving, as long as you don't need to preserve data. The situation is different when data that has been entered into the database needs to be preserved. When that is the case, use migrations.

Later in the tutorial series, you delete the database that was created by `EnsureCreated` and use migrations instead. A database that is created by `EnsureCreated` can't be updated by using migrations.

### Test the app

* Run the app.
* Select the **Students** link and then **Create New**.
* Test the Edit, Details, and Delete links.

## Seed the database

The `EnsureCreated` method creates an empty database. This section adds code that populates the database with test data.

Create `Data/DbInitializer.cs` with the following code:

```
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using System;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2019-09-01")},
                new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2017-09-01")},
                new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2018-09-01")},
                new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2017-09-01")},
                new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2017-09-01")},
                new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2016-09-01")},
                new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2018-09-01")},
                new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2019-09-01")}
            };

            context.Students.AddRange(students);
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course{CourseID=1050,Title="Chemistry",Credits=3},
                new Course{CourseID=4022,Title="Microeconomics",Credits=3},
                new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
                new Course{CourseID=1045,Title="Calculus",Credits=4},
                new Course{CourseID=3141,Title="Trigonometry",Credits=4},
                new Course{CourseID=2021,Title="Composition",Credits=3},
                new Course{CourseID=2042,Title="Literature",Credits=4}
            };

            context.Courses.AddRange(courses);
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
                new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
                new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
                new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
                new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
                new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
                new Enrollment{StudentID=3,CourseID=1050},
                new Enrollment{StudentID=4,CourseID=1050},
                new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
                new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
                new Enrollment{StudentID=6,CourseID=1045},
                new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
            };

            context.Enrollments.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}
```

The code checks if there are any students in the database. If there are no students, it adds test data to the database. It creates the test data in arrays rather than `List<T>` collections to optimize performance.

* In `Program.cs`, replace the `EnsureCreated` call with a `DbInitializer.Initialize` call:

  ```
  // context.Database.EnsureCreated();
  DbInitializer.Initialize(context);
  ```

- [Visual Studio](#tabpanel_26_visual-studio)
- [Visual Studio Code](#tabpanel_26_visual-studio-code)

Stop the app if it's running, and run the following command in the **Package Manager Console** (PMC):

```
Drop-Database
```

* Stop the app if it's running, and delete the `CU.db` file.

- Restart the app.

- Select the Students page to see the seeded data.

## View the database

* [Visual Studio](#tabpanel_27_visual-studio)
* [Visual Studio Code](#tabpanel_27_visual-studio-code)

- Open **SQL Server Object Explorer** (SSOX) from the **View** menu in Visual Studio.
- In SSOX, select **(localdb)\MSSQLLocalDB > Databases > SchoolContext-{GUID}**. The database name is generated from the context name you provided earlier plus a dash and a GUID.
- Expand the **Tables** node.
- Right-click the **Student** table and click **View Data** to see the columns created and the rows inserted into the table.
- Right-click the **Student** table and click **View Code** to see how the `Student` model maps to the `Student` table schema.

Use your SQLite tool to view the database schema and seeded data. The database file is named `CU.db` and is located in the project folder.

## Asynchronous code

Asynchronous programming is the default mode for ASP.NET Core and EF Core.

A web server has a limited number of threads available, and in high load situations all of the available threads might be in use. When that happens, the server can't process new requests until the threads are freed up. With synchronous code, many threads may be tied up while they aren't actually doing any work because they're waiting for I/O to complete. With asynchronous code, when a process is waiting for I/O to complete, its thread is freed up for the server to use for processing other requests. As a result, asynchronous code enables server resources to be used more efficiently, and the server can handle more traffic without delays.

Asynchronous code does introduce a small amount of overhead at run time. For low traffic situations, the performance hit is negligible, while for high traffic situations, the potential performance improvement is substantial.

In the following code, the [async](/en-us/dotnet/csharp/language-reference/keywords/async) keyword, `Task<T>` return value, `await` keyword, and `ToListAsync` method make the code execute asynchronously.

```
public async Task OnGetAsync()
{
    Students = await _context.Students.ToListAsync();
}
```

* The `async` keyword tells the compiler to:

  * Generate callbacks for parts of the method body.
  * Create the [Task](/en-us/dotnet/csharp/programming-guide/concepts/async/async-return-types#BKMK_TaskReturnType) object that's returned.

* The `Task<T>` return type represents ongoing work.

* The `await` keyword causes the compiler to split the method into two parts. The first part ends with the operation that's started asynchronously. The second part is put into a callback method that's called when the operation completes.

* `ToListAsync` is the asynchronous version of the `ToList` extension method.

Some things to be aware of when writing asynchronous code that uses EF Core:

* Only statements that cause queries or commands to be sent to the database are executed asynchronously. That includes `ToListAsync`, `SingleOrDefaultAsync`, `FirstOrDefaultAsync`, and `SaveChangesAsync`. It doesn't include statements that just change an `IQueryable`, such as `var students = context.Students.Where(s => s.LastName == "Davolio")`.
* An EF Core context isn't thread safe: don't try to do multiple operations in parallel.
* To take advantage of the performance benefits of async code, verify that library packages (such as for paging) use async if they call EF Core methods that send queries to the database.

For more information about asynchronous programming in .NET, see [Async Overview](/en-us/dotnet/standard/async) and [Asynchronous programming with async and await](/en-us/dotnet/csharp/programming-guide/concepts/async/).

## Next steps

[Next tutorial](crud?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/migration/50-to-60
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/migration/50-to-60.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Migrate from ASP.NET Core in .NET 5 to .NET 6

This article explains how to update an existing ASP.NET Core in .NET 5 project to .NET 6. For instructions on how to migrate from ASP.NET Core 3.1 to .NET 6, see [Migrate from ASP.NET Core 3.1 to .NET 6](31-to-60?view=aspnetcore-10.0).

## Update .NET SDK version in global.json

If you rely upon a [`global.json`](/en-us/dotnet/core/tools/global-json) file to target a specific .NET SDK version, update the `version` property to the .NET 6 SDK version that's installed. For example:

```
{
  "sdk": {
-    "version": "5.0.100"
+    "version": "6.0.100"
  }
}
```

## Update the target framework

Update the project file's [Target Framework Moniker (TFM)](/en-us/dotnet/standard/frameworks) to `net6.0`:

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

  <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

</Project>
```

## Update package references

In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.org/packages?q=Microsoft.AspNetCore.*) and [`Microsoft.Extensions.*`](https://www.nuget.org/packages?q=Microsoft.Extensions.*) package reference's `Version` attribute to 6.0.0 or later. For example:

```
<ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="5.0.3" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
+    <PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="6.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
</ItemGroup>
```

## New hosting model

The new .NET 6 minimal hosting model for ASP.NET Core apps requires only one file and a few lines of code. **Apps migrating to .NET 6 don't need to use the new minimal hosting model.** For more information, see [Apps migrating to .NET 6 don't need to use the new minimal hosting model](#apps-migrating-to-or-using-60-or-later-dont-need-to-use-the-new-minimal-hosting-model) in the following section.

The following code from the ASP.NET Core empty template creates an app using the new minimal hosting model:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The minimal hosting model:

* Significantly reduces the number of files and lines of code required to create an app. Only one file is needed with four lines of code.
* Unifies `Startup.cs` and `Program.cs` into a single `Program.cs` file.
* Uses [top-level statements](/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) to minimize the code required for an app.
* Uses [global `using` directives](/en-us/dotnet/csharp/whats-new/csharp-10#global-using-directives) to eliminate or minimize the number of [`using` statement](/en-us/dotnet/csharp/language-reference/keywords/using-statement) lines required.

The following code displays the `Startup.cs` and `Program.cs` files from an .NET 5 Web App template (Razor Pages) with unused `using` statements removed:

```
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
// Unused usings removed.

namespace WebAppRPv5
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // 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.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}
```

```
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
// Unused usings removed.

namespace WebAppRPv5
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```

In ASP.NET Core in .NET 6, the preceding code is replaced by the following:

```
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding ASP.NET Core in .NET 6 sample shows how:

* [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configureservices) is replaced with [`WebApplication.Services`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.services#microsoft-aspnetcore-builder-webapplication-services).
* `builder.Build()` returns a configured [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) to the variable `app`. [Configure](/en-us/dotnet/api/microsoft.aspnetcore.hosting.startupbase.configure) is replaced with configuration calls to same services using `app`.

Detailed examples of migrating ASP.NET Core in .NET 5 `Startup` code to .NET 6 using the minimal hosting model are provided later in this document.

There are a few changes to the other files generated for the Web App template:

* `Index.cshtml` and `Privacy.cshtml` have the unused `using` statements removed.
* `RequestId` in `Error.cshtml` is declared as a [nullable reference type (NRT)](/en-us/dotnet/csharp/language-reference/builtin-types/nullable-reference-types):

```
- public string RequestId { get; set; }
+ public string? RequestId { get; set; }
```

* Log level defaults have changed in `appsettings.json` and `appsettings.Development.json`:

```
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
+ "Microsoft.AspNetCore": "Warning"
```

In the preceding ASP.NET Core template code, `"Microsoft": "Warning"` has been changed to `"Microsoft.AspNetCore": "Warning"`. This change results in logging all informational messages from the `Microsoft` namespace ***except*** `Microsoft.AspNetCore`. For example, `Microsoft.EntityFrameworkCore` is now logged at the informational level.

For more details on the new hosting model, see the [Frequently asked questions](#faq) section. For more information on the adoption of NRTs and .NET compiler null-state analysis, see the [Nullable reference types (NRTs) and .NET compiler null-state static analysis](#nullable-reference-types-nrts-and-net-compiler-null-state-static-analysis) section.

### Apps migrating to or using 6.0 or later don't need to use the new minimal hosting model

Using [`Startup`](../fundamentals/startup?view=aspnetcore-10.0\&preserve-view=true\&view=aspnetcore-5.0) and the [Generic Host](../fundamentals/host/generic-host?view=aspnetcore-10.0) used by the ASP.NET Core 3.1 and 5.0 templates is fully supported.

### Use a `Startup` class with the new minimal hosting model

ASP.NET Core 3.1 and 5.0 apps can use their `Startup` code with the new minimal hosting model. Using `Startup` with the minimal hosting model has the following advantages:

* No hidden reflection is used to call the `Startup` class.
* Asynchronous code can be written because the developer controls the call to `Startup`.
* Code can be written that interleaves `ConfigureServices` and `Configure`.

One minor limitation in using `Startup` code with the new minimal hosting model is that to inject a dependency into `Configure`, the service in `Program.cs` must be manually resolved.

Consider the following code generated by the ASP.NET Core 3.1 or 5.0 Razor Pages template:

```
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
```

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

The preceding code migrated to the new minimal hosting model:

```
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);

var startup = new Startup(builder.Configuration);

startup.ConfigureServices(builder.Services);

var app = builder.Build();

startup.Configure(app, app.Environment);

app.Run();
```

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (!env.IsDevelopment())
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

In the preceding code, the `if (env.IsDevelopment())` block is removed because in [development mode](../fundamentals/environments?view=aspnetcore-10.0), the developer exception page middleware is enabled by default. For more information, see [Differences between the ASP.NET Core in .NET 5 and .NET 6 hosting models](#differences-between-the-aspnet-core-in-net-5-and-net-6-hosting-models) in the next section.

When using a custom dependency injection (DI) container, add the following highlighted code:

```
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

var startup = new Startup(builder.Configuration);

startup.ConfigureServices(builder.Services);

// Using a custom DI container.
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(startup.ConfigureContainer);

var app = builder.Build();

startup.Configure(app, app.Environment);

app.Run();
```

```
using Autofac;
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    //  Using a custom DI container
    public void ConfigureContainer(ContainerBuilder builder)
    {
        // Configure custom container.
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (!env.IsDevelopment())
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

When using the minimal hosting model, the endpoint routing middleware wraps the entire middleware pipeline, therefore there's no need to have explicit calls to `UseRouting` or `UseEndpoints` to register routes. `UseRouting` can still be used to specify where route matching happens, but `UseRouting` doesn't need to be explicitly called if routes should be matched at the beginning of the middleware pipeline.

In the following code, the calls to `UseRouting` and `UseEndpoints` are removed from `Startup`. `MapRazorPages` is called in `Program.cs`:

```
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (!env.IsDevelopment())
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        //app.UseRouting();

        //app.UseEndpoints(endpoints =>
        //{
        //    endpoints.MapRazorPages();
        //});
    }
}
```

```
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);

var startup = new Startup(builder.Configuration);

startup.ConfigureServices(builder.Services);

var app = builder.Build();

startup.Configure(app, app.Environment);

app.MapRazorPages();

app.Run();
```

When using `Startup` with the new minimal hosting model, keep in mind the following difference:

* `Program.cs` controls the instantiation and lifetime of the `Startup` class.
* Any additional services injected into the `Configure` method need to be manually resolved by the `Program` class.

### Differences between the ASP.NET Core in .NET 5 and .NET 6 hosting models

* In [development mode](../fundamentals/environments?view=aspnetcore-10.0), the developer exception page middleware is enabled by default.
* The app name defaults to the entry point assembly's name: `Assembly.GetEntryAssembly().GetName().FullName`. When using the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) in a library, explicitly change the app name to the library's assembly to allow MVC's [application part discovery](../mvc/advanced/app-parts?view=aspnetcore-10.0) to work. See [Change the content root, app name, and environment](50-to-60-samples?view=aspnetcore-10.0#ccr) in this document for detailed instructions.
* The endpoint routing middleware wraps the entire middleware pipeline, therefore there's no need to have explicit calls to `UseRouting` or `UseEndpoints` to register routes. `UseRouting` can still be used to specify where route matching happens, but `UseRouting` doesn't need to be explicitly called if routes should be matched at the beginning of the middleware pipeline.
* The [pipeline](../fundamentals/middleware/?view=aspnetcore-10.0) is created before any [IStartupFilter](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartupfilter) runs, therefore exceptions caused while building the pipeline aren't visible to the `IStartupFilter` call chain.
* Some tools, such as EF migrations, use [`Program.CreateHostBuilder`](../fundamentals/host/generic-host?view=aspnetcore-10.0) to access the app's `IServiceProvider` to execute custom logic in the context of the app. These tools have been updated to use a new technique to execute custom logic in the context of the app. [Entity Framework Migrations](/en-us/ef/core/managing-schemas/migrations/) is an example of a tool that uses `Program.CreateHostBuilder` in this way. We're working to make sure tools are updated to use the new model.
* Unlike the `Startup` class, the minimal host doesn't automatically configure a DI scope when instantiating the service provider. For contexts where a scope is required, it is necessary to invoke [IServiceScope](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicescope) with [IServiceScopeFactory.CreateScope](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicescopefactory.createscope) to instantiate a new scope. For more information, see [how to resolve a service at app startup](../fundamentals/dependency-injection?view=aspnetcore-10.0#resolve-a-service-at-app-start-up).
* It's ***not*** possible to [change any host settings such as app name, environment, or the content root](50-to-60-samples?view=aspnetcore-10.0#ccr) after the creation of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder). For detailed instructions on changing host settings, see [Customize `IHostBuilder` or `IWebHostBuilder`](50-to-60-samples?view=aspnetcore-10.0#cii). The following highlighted APIs throw an exception:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

// WebHost

try
{
    builder.WebHost.UseContentRoot(Directory.GetCurrentDirectory());
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

try
{
    builder.WebHost.UseEnvironment(Environments.Staging);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

try
{
    builder.WebHost.UseSetting(WebHostDefaults.ApplicationKey, "ApplicationName2");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

try
{
    builder.WebHost.UseSetting(WebHostDefaults.ContentRootKey, Directory.GetCurrentDirectory());
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

try
{
    builder.WebHost.UseSetting(WebHostDefaults.EnvironmentKey, Environments.Staging);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

// Host
try
{
    builder.Host.UseEnvironment(Environments.Staging);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

try
{
    // TODO: This does not throw
    builder.Host.UseContentRoot(Directory.GetCurrentDirectory());
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

* The `Startup` class can't be used from [`WebApplicationBuilder.Host`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.host#microsoft-aspnetcore-builder-webapplicationbuilder-host) or [`WebApplicationBuilder.WebHost`](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost#microsoft-aspnetcore-builder-webapplicationbuilder-webhost). The following highlighted code throws an exception:

  ```
  var builder = WebApplication.CreateBuilder(args);

  try
  {
      builder.Host.ConfigureWebHostDefaults(webHostBuilder =>
      {
          webHostBuilder.UseStartup<Startup>();
      });
  }
  catch (Exception ex)
  {
      Console.WriteLine(ex.Message);
      throw;    
  }

  builder.Services.AddRazorPages();

  var app = builder.Build();
  ```

  ```
  var builder = WebApplication.CreateBuilder(args);

  try
  {
      builder.WebHost.UseStartup<Startup>();
  }
  catch (Exception ex)
  {
      Console.WriteLine(ex.Message);
      throw;    
  }

  builder.Services.AddRazorPages();

  var app = builder.Build();
  ```

* The [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) implementation on [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) (`WebApplicationBuilder.Host`), doesn't defer execution of the [ConfigureServices](/en-us/dotnet/api/microsoft.aspnetcore.hosting.istartup.configureservices), [ConfigureAppConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureappconfiguration), or [ConfigureHostConfiguration](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.configurehostconfiguration) methods. Not deferring execution allows code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) to observe changes made to the `IServiceCollection` and `IConfiguration`. The following example only adds `Service1` as an `IService`:

  ```
  using Microsoft.Extensions.DependencyInjection.Extensions;

  var builder = WebApplication.CreateBuilder(args);

  builder.Host.ConfigureServices(services =>
  {
      services.TryAddSingleton<IService, Service1>();
  });

  builder.Services.TryAddSingleton<IService, Service2>();

  var app = builder.Build();

  // Displays Service1 only.
  Console.WriteLine(app.Services.GetRequiredService<IService>());

  app.Run();

  class Service1 : IService
  {
  }

  class Service2 : IService
  {
  }

  interface IService
  {
  }
  ```

In the preceding code, the `builder.Host.ConfigureServices` callback gets called inline rather than being deferred until `builder.Build` is called. This means that `Service1` gets added to the `IServiceCollection` before `Service2` and results in `Service1` being resolved for `IService`.

## Building libraries for ASP.NET Core in .NET 6

The existing .NET ecosystem built extensibility around [IServiceCollection](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection), [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder), and [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder). These properties are available on [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) as `Services`, `Host`, and `WebHost`.

`WebApplication` implements both [Microsoft.AspNetCore.Builder.IApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder) and [Microsoft.AspNetCore.Routing.IEndpointRouteBuilder](/en-us/dotnet/api/microsoft.aspnetcore.routing.iendpointroutebuilder).

We expect library authors to continue targeting `IHostBuilder`, `IWebHostBuilder`, `IApplicationBuilder`, and `IEndpointRouteBuilder` when building ASP.NET Core specific components. This ensures that your middleware, route handler, or other extensibility points continue to work across different hosting models.

## Frequently asked questions (FAQ)

* **Is the new minimal hosting model less capable?**

  No. The new hosting model is functionally equivalent for 98% of scenarios supported by `IHostBuilder` and the `IWebHostBuilder`. There are some advanced scenarios that require specific workarounds on `IHostBuilder`, but we expect those to be extremely rare.

* **Is the generic hosting model deprecated?**

  No. The generic hosting model is an alternative model that is supported indefinitely. The generic host underpins the new hosting model and is still the primary way to host worker-based applications.

* **Do I have to migrate to the new hosting model?**

  No. The new hosting model is the preferred way to host new apps using .NET 6 or later, but you aren't forced to change the project layout in existing apps. This means apps can upgrade from .NET 5 to .NET 6 by changing the target framework in the project file from `net5.0` to `net6.0`. For more information, see the [Update the target framework](#update-the-target-framework) section in this article. However, we recommend apps migrate to the new hosting model to take advantage of new features only available to the new hosting model.

* **Do I have to use top-level statements?**

  No. The new project templates all use [top-level statements](/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements), but the new hosting APIs can be used in any .NET 6 app to host a webserver or web app.

* **Where do I put state that was stored as fields in my `Program` or `Startup` class?**

  We strongly recommend using [dependency injection](../fundamentals/dependency-injection?view=aspnetcore-10.0) (DI) to flow state in ASP.NET Core apps.

  There are two approaches to storing state outside of DI:

  * Store the state in another class. Storing in a class assumes a static state that can be accessed from anywhere in the app.

  * Use the `Program` class generated by top level statements to store state. Using `Program` to store state is the semantic approach:

    ```
    var builder = WebApplication.CreateBuilder(args);

    ConfigurationValue = builder.Configuration["SomeKey"] ?? "Hello";

    var app = builder.Build();

    app.MapGet("/", () => ConfigurationValue);

    app.Run();

    partial class Program
    {
        public static string? ConfigurationValue { get; private set; }
    }
    ```

* **What if I was using a custom dependency injection container?**

  Custom DI containers are supported. For an example, see [Custom dependency injection (DI) container](50-to-60-samples?view=aspnetcore-10.0#cdi).

* **Do `WebApplicationFactory` and `TestServer` still work?**

  Yes. `WebApplicationFactory<TEntryPoint>` is the way to test the new hosting model. For an example, see [Test with `WebApplicationFactory` or `TestServer`](50-to-60-samples?view=aspnetcore-10.0#twa).

## Blazor

After following the guidance earlier in this article to update an app to .NET 6, adopt specific features by following the links in [What's new in ASP.NET Core in .NET 6](../release-notes/aspnetcore-6.0?view=aspnetcore-10.0#blazor).

To adopt all of the [new 6.0 features for Blazor apps](../release-notes/aspnetcore-6.0?view=aspnetcore-10.0#blazor), we recommend the following process:

* Create a new 6.0 Blazor project from one of the Blazor project templates. For more information, see [Tooling for ASP.NET Core Blazor](../blazor/tooling?view=aspnetcore-10.0).
* Move the app's components and code to the 6.0 app making modifications to adopt the new .NET 6 features.

## Migrating SPA projects

### Migrating Angular apps from SPA extensions

See [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/44600)

### Migrating React apps from SPA extensions

See **Migrating React applications from Spa Extensions** in [this GitHub issue](https://github.com/dotnet/aspnetcore/issues/44600)

## Update Docker images

For apps using Docker, update your *Dockerfile* `FROM` statements and scripts. Use a base image that includes the ASP.NET Core in .NET 6 runtime. Consider the following `docker pull` command difference between ASP.NET Core in .NET 5 and .NET 6:

```
- docker pull mcr.microsoft.com/dotnet/aspnet:5.0
+ docker pull mcr.microsoft.com/dotnet/aspnet:6.0
```

See GitHub issue [Breaking Change: Default console logger format set to JSON](https://github.com/dotnet/dotnet-docker/issues/3274).

## Changes to the ASP.NET Core Razor SDK

The Razor compiler now leverages the new [source generators feature](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) to generate compiled C# files from the Razor views and pages in a project. In previous versions:

* The compilation relied on the `RazorGenerate` and `RazorCompile` targets to produce the generated code. These targets are no longer valid. In .NET 6, both code generation and compilation are supported by a single call to the compiler. `RazorComponentGenerateDependsOn` is still supported to specify dependencies that are required before the build runs.
* A separate Razor assembly, `AppName.Views.dll`, was generated that contained the compiled view types in an application. This behavior has been deprecated and a single assembly `AppName.dll` is produced that contains both the app types and the generated views.
* The app types in `AppName.Views.dll` were public. In .NET 6, the app types are in `AppName.dll` but are `internal sealed`. Apps doing type discover on `AppName.Views.dll` won't be able to do type discover on `AppName.dll`. The following shows the API change:

```
- public class Views_Home_Index : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<dynamic>
+ internal sealed class Views_Home_Index : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<dynamic>
```

Make the following changes:

* The following properties are no longer applicable with the single-step compilation model.

  * `RazorTargetAssemblyAttribute`
  * `RazorTargetName`
  * `EnableDefaultRazorTargetAssemblyInfoAttributes`
  * `UseRazorBuildServer`
  * `GenerateRazorTargetAssemblyInfo`
  * `GenerateMvcApplicationPartsAssemblyAttributes`

For more information, see [Razor compiler no longer produces a Views assembly](https://github.com/aspnet/Announcements/issues/459).

## Project templates use Duende Identity Server

Project templates now use [Duende Identity Server](https://docs.duendesoftware.com).

Important

Duende Identity Server is an open source product with a reciprocal license agreement. If you plan to use Duende Identity Server in production, you might be required to obtain a commercial licence from [Duende Software](https://duendesoftware.com/) and pay a license fee. For more information, see [Duende Software: Licenses](https://aka.ms/identityserverlicense).

To learn how to use [Microsoft Azure Active Directory](https://azure.microsoft.com/services/active-directory/) for ASP.NET Core Identity, see [Identity (dotnet/aspnetcore GitHub repository)](https://aka.ms/aspnetidentityserver).

Add a `DbSet<Key>` property named `Keys` to every `IdentityDbContext` to satisfy a new requirement from the updated version of `IPersistedGrantDbContext`. The keys are required as part of the contract with Duende Identity Server's stores.

```
public DbSet<Key> Keys { get; set; }
```

Note

Existing migrations must be recreated for Duende Identity Server.

## Code samples migrated to ASP.NET Core in .NET 6

[Code samples migrated to the new minimal hosting model in 6.0](50-to-60-samples?view=aspnetcore-10.0)

## Breaking changes

Use the articles in [Breaking changes in .NET](/en-us/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET.

For more information, see [Announcements GitHub repository (`aspnet/Announcements`, `6.0.0` label)](https://github.com/aspnet/Announcements/issues?q=is%3Aissue+label%3A6.0.0+is%3Aopen): Includes breaking and non-breaking information.

## Nullable reference types (NRTs) and .NET compiler null-state static analysis

ASP.NET Core project templates use nullable reference types (NRTs), and the .NET compiler performs null-state static analysis. These features were released with C# 8 and are enabled by default for apps generated using ASP.NET Core in .NET 6 (C# 10) or later.

The .NET compiler's null-state static analysis warnings can either serve as a guide for updating a documentation example or sample app locally or be ignored. Null-state static analysis can be disabled by [setting `Nullable` to `disable`](/en-us/dotnet/csharp/language-reference/builtin-types/nullable-reference-types#setting-the-nullable-context) in the app's project file, which we only recommend for documentation examples and sample apps if the compiler warnings are distracting while learning about .NET. ***We don't recommend disabling null-state checking in production projects.***

For more information on NRTs, the MSBuild `Nullable` property, and updating apps (including `#pragma` guidance), see the following resources in the C# documentation:

* [Nullable reference types](/en-us/dotnet/csharp/nullable-references)
* [Nullable reference types (C# reference)](/en-us/dotnet/csharp/language-reference/builtin-types/nullable-reference-types)
* [Learn techniques to resolve nullable warnings](/en-us/dotnet/csharp/nullable-warnings)
* [Update a codebase with nullable reference types to improve null diagnostic warnings](/en-us/dotnet/csharp/nullable-migration-strategies)
* [Attributes for null-state static analysis](/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis)
* [! (null-forgiving) operator (C# reference)](/en-us/dotnet/csharp/language-reference/operators/null-forgiving)

## ASP.NET Core Module (ANCM)

If the [ASP.NET Core Module (ANCM)](../host-and-deploy/aspnet-core-module?view=aspnetcore-10.0) wasn't a selected component when Visual Studio was installed or if a prior version of the ANCM was installed on the system, download the latest [.NET Core Hosting Bundle Installer (direct download)](https://dotnet.microsoft.com/permalink/dotnetcore-current-windows-runtime-bundle-installer) and run the installer. For more information, see [Hosting Bundle](../host-and-deploy/iis/hosting-bundle?view=aspnetcore-10.0).

## Application name change

In .NET 6, [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) normalizes the content root path to end with a [DirectorySeparatorChar](/en-us/dotnet/api/system.io.path.directoryseparatorchar#system-io-path-directoryseparatorchar). Most apps migrating from [HostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder) or [WebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder) won't have the same app name because they aren't normalized. For more information, see [SetApplicationName](../security/data-protection/configuration/overview?view=aspnetcore-10.0#set-the-application-name-setapplicationname)

## Related content

* [Code samples migrated to the new minimal hosting model in 6.0](50-to-60-samples?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-24

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/webapplication
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/webapplication.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# WebApplication and WebApplicationBuilder in ASP.NET Core apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

## `WebApplication`

The following code is generated by an ASP.NET Core template:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code can be created via `dotnet new web` on the command line or selecting the Empty Web template in Visual Studio.

The following code creates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`) without explicitly creating a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder):

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults:

[`WebApplication`](webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should be run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case as it is automatically added as described previously:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.
* The app needs to call `UseRouting` and `UseEndpoints` so that the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

### Working with ports

When a web app is created with Visual Studio or `dotnet new`, a `Properties/launchSettings.json` file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog `Unable to connect to web server 'AppName'`. Visual Studio returns an error because it's expecting the port specified in `Properties/launchSettings.json`, but the app is using the port specified by `app.Run("http://localhost:3000")`. Run the following port changing samples from the command line.

The following sections set the app's port.

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");
```

In the preceding code, the app responds to port `3000`.

#### Multiple ports

In the following code, the app responds to port `3000` and `4000`:

```
var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Set the port from the command line

The following command makes the app respond to port `7777`:

```
dotnet run --urls="https://localhost:7777"
```

If the Kestrel endpoint is also configured in the `appsettings.json` file, the `appsettings.json` file specified URL is used. For more information, see [Kestrel endpoint configuration](../configuration/?view=aspnetcore-10.0#kestrel-endpoint-configuration)

#### Read the port from environment

The following code reads the port from the environment:

```
var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");
```

The preferred way to set the port from the environment is to use the `ASPNETCORE_URLS` environment variable, which is shown in the following section.

#### Set the ports via the `ASPNETCORE_URLS` environment variable

The `ASPNETCORE_URLS` environment variable is available to set the port:

```
ASPNETCORE_URLS=http://localhost:3000
```

`ASPNETCORE_URLS` supports multiple URLs:

```
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
```

For more information using the environment, see [ASP.NET Core runtime environments](../environments?view=aspnetcore-10.0).

### Listen on all interfaces

The following samples demonstrate listening on all interfaces

#### `http://*:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://+:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://0.0.0.0:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Listen on all interfaces using `ASPNETCORE_URLS`

The preceding samples can use `ASPNETCORE_URLS`:

```
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
```

### Specify HTTPS with development certificate

```
var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information on the development certificate, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../../security/enforcing-ssl?view=aspnetcore-10.0#trust).

### Specify HTTPS using a custom certificate

The following sections show how to specify the custom certificate using the `appsettings.json` file and via configuration.

#### Specify the custom certificate with `appsettings.json`

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}
```

#### Specify the custom certificate via configuration

```
var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Use the certificate APIs

```
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Configuration

The following code reads from the configuration system:

```
var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();
```

For more information, see [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

### Logging

The following code writes a message to the log on application startup:

```
var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information, see [Logging in .NET and ASP.NET Core](../logging/?view=aspnetcore-10.0).

### Access the Dependency Injection (DI) container

The following code shows how to get services from the DI container during application startup:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();
```

For more information, see [Dependency injection in ASP.NET Core](../dependency-injection?view=aspnetcore-10.0).

## `WebApplicationBuilder`

This section contains sample code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

### Change the content root, application name, and environment

The following code sets the content root, application name, and environment:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

[WebApplication.Create](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.create) initializes a new instance of the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) class with preconfigured defaults.

For more information, see [ASP.NET Core fundamentals overview](../?view=aspnetcore-10.0).

### Change the content root, app name, and environment by environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| Feature          | Environment variable         | Command-line argument |
| ---------------- | ---------------------------- | --------------------- |
| Application name | `ASPNETCORE_APPLICATIONNAME` | `--applicationName`   |
| Environment name | `ASPNETCORE_ENVIRONMENT`     | `--environment`       |
| Content root     | `ASPNETCORE_CONTENTROOT`     | `--contentRoot`       |

### Add configuration providers

The following sample adds the INI configuration provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](../configuration/?view=aspnetcore-10.0#file-configuration-provider) in [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

### Read configuration

By default the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) reads configuration from multiple sources, including:

* `appSettings.json` and `appSettings.{environment}.json`
* Environment variables
* The command line

The following code reads `HelloKey` from configuration and displays the value at the `/` endpoint. If the configuration value is null, "Hello" is assigned to `message`:

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

For a complete list of configuration sources read, see [Default configuration](../configuration/?view=aspnetcore-10.0#default-configuration) in [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

### Add logging providers

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();
```

### Add services

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

### Customize the `IHostBuilder`

Existing extension methods on [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) can be accessed using the [Host property](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.properties#microsoft-extensions-hosting-ihostbuilder-properties):

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Customize the `IWebHostBuilder`

Extension methods on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) can be accessed using the [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost) property.

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();
```

### Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed with `WebHostOptions`, the command line, or with the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();
```

### Custom dependency injection (DI) container

The following example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

### Add middleware

Any existing ASP.NET Core middleware can be configured on the `WebApplication`:

```
var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information, see [ASP.NET Core middleware](../middleware/?view=aspnetcore-10.0).

### Developer exception page

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the [development environment](../environments?view=aspnetcore-10.0), navigating to `/` renders a friendly page that shows the exception.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();
```

## `WebApplication`

The following code is generated by an ASP.NET Core template:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code can be created via `dotnet new web` on the command line or selecting the Empty Web template in Visual Studio.

The following code creates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`) without explicitly creating a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder):

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults:

[WebApplication](webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// User middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// End user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case because it's automatically added as described earlier:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// Other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.

* The app needs to call `UseRouting` and `UseEndpoints` so the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

For information on antiforgery middleware in Minimal APIs, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0#afwma).

### Working with ports

When a web app is created with Visual Studio or `dotnet new`, a `Properties/launchSettings.json` file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog `Unable to connect to web server 'AppName'`. Visual Studio returns an error because it's expecting the port specified in `Properties/launchSettings.json`, but the app is using the port specified by `app.Run("http://localhost:3000")`. Run the following port changing samples from the command line.

The following sections set the app's port.

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");
```

In the preceding code, the app responds to port `3000`.

#### Multiple ports

In the following code, the app responds to port `3000` and `4000`:

```
var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Set the port from the command line

The following command makes the app respond to port `7777`:

```
dotnet run --urls="https://localhost:7777"
```

If the Kestrel endpoint is also configured in the `appsettings.json` file, the `appsettings.json` file specified URL is used. For more information, see [Kestrel endpoint configuration](../configuration/?view=aspnetcore-10.0#kestrel-endpoint-configuration)

#### Read the port from environment

The following code reads the port from the environment:

```
var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");
```

The preferred way to set the port from the environment is to use the `ASPNETCORE_URLS` environment variable, which is shown in the following section.

#### Set the ports via the `ASPNETCORE_URLS` environment variable

The `ASPNETCORE_URLS` environment variable is available to set the port:

```
ASPNETCORE_URLS=http://localhost:3000
```

`ASPNETCORE_URLS` supports multiple URLs:

```
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
```

### Listen on all interfaces

The following samples demonstrate listening on all interfaces.

#### `http://*:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://+:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://0.0.0.0:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Listen on all interfaces using `ASPNETCORE_URLS`

The preceding samples can use `ASPNETCORE_URLS`:

```
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
```

### Listen on all interfaces using `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`

The preceding samples can also use `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`, but `ASPNETCORE_URLS` (when set) overrides the following settings:

```
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
```

For more information, see [Configure endpoints for Kestrel web server](../servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### Specify HTTPS with development certificate

```
var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information on the development certificate, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../../security/enforcing-ssl?view=aspnetcore-10.0#trust).

### Specify HTTPS using a custom certificate

The following sections show how to specify the custom certificate using the `appsettings.json` file and via configuration.

#### Specify the custom certificate with `appsettings.json`

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}
```

#### Specify the custom certificate via configuration

```
var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Use the certificate APIs

```
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Read the environment

```
var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();
```

For more information using the environment, see [ASP.NET Core runtime environments](../environments?view=aspnetcore-10.0).

### Configuration

The following code reads from the configuration system:

```
var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();
```

For more information, see [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

### Logging

The following code writes a message to the log on application startup:

```
var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information, see [Logging in .NET and ASP.NET Core](../logging/?view=aspnetcore-10.0).

### Access the Dependency Injection (DI) container

The following code shows how to get services from the DI container during application startup:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();
```

The following code shows how to access keys from the DI container using the [`[FromKeyedServices]` attribute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}
```

For more information on DI, see [Dependency injection in ASP.NET Core](../dependency-injection?view=aspnetcore-10.0).

## `WebApplicationBuilder`

This section contains sample code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

### Change the content root, application name, and environment

The following code sets the content root, application name, and environment:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

[WebApplication.Create](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.create) initializes a new instance of the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) class with preconfigured defaults.

For more information, see [ASP.NET Core fundamentals overview](../?view=aspnetcore-10.0)

### Change the content root, app name, and environment by using environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| Feature          | Environment variable         | Command-line argument |
| ---------------- | ---------------------------- | --------------------- |
| Application name | `ASPNETCORE_APPLICATIONNAME` | `--applicationName`   |
| Environment name | `ASPNETCORE_ENVIRONMENT`     | `--environment`       |
| Content root     | `ASPNETCORE_CONTENTROOT`     | `--contentRoot`       |

### Add configuration providers

The following sample adds the INI configuration provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](../configuration/?view=aspnetcore-10.0#file-configuration-provider) in [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

### Read configuration

By default the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) reads configuration from multiple sources, including:

* `appSettings.json` and `appSettings.{environment}.json`
* Environment variables
* The command line

For a complete list of configuration sources read, see [Default configuration](../configuration/?view=aspnetcore-10.0#default-configuration) in [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

The following code reads `HelloKey` from configuration and displays the value at the `/` endpoint. If the configuration value is null, "Hello" is assigned to `message`:

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

### Read the environment

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Add logging providers

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();
```

### Add services

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

### Customize the `IHostBuilder`

Existing extension methods on [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) can be accessed using the [Host property](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.properties#microsoft-extensions-hosting-ihostbuilder-properties):

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Customize the `IWebHostBuilder`

Extension methods on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) can be accessed using the [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost) property.

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();
```

### Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed with `WebHostOptions`, the command line, or with the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();
```

### Custom dependency injection (DI) container

The following example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

### Add middleware

Any existing ASP.NET Core middleware can be configured on the `WebApplication`:

```
var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information, see [ASP.NET Core middleware](../middleware/?view=aspnetcore-10.0).

### Developer exception page

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the [development environment](../environments?view=aspnetcore-10.0), navigating to `/` renders a friendly page that shows the exception.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();
```

## `WebApplication`

The following code is generated by an ASP.NET Core template:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code can be created via `dotnet new web` on the command line or selecting the Empty Web template in Visual Studio.

The following code creates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`) without explicitly creating a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder):

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults:

[WebApplication](webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// User middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// End user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case because it's automatically added as described earlier:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// Other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.

* The app needs to call `UseRouting` and `UseEndpoints` so the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

For information on antiforgery middleware in Minimal APIs, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0#afwma).

### Working with ports

When a web app is created with Visual Studio or `dotnet new`, a `Properties/launchSettings.json` file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog `Unable to connect to web server 'AppName'`. Visual Studio returns an error because it's expecting the port specified in `Properties/launchSettings.json`, but the app is using the port specified by `app.Run("http://localhost:3000")`. Run the following port changing samples from the command line.

The following sections set the app's port.

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");
```

In the preceding code, the app responds to port `3000`.

#### Multiple ports

In the following code, the app responds to port `3000` and `4000`:

```
var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Set the port from the command line

The following command makes the app respond to port `7777`:

```
dotnet run --urls="https://localhost:7777"
```

If the Kestrel endpoint is also configured in the `appsettings.json` file, the `appsettings.json` file specified URL is used. For more information, see [Kestrel endpoint configuration](../configuration/?view=aspnetcore-10.0#kestrel-endpoint-configuration)

#### Read the port from environment

The following code reads the port from the environment:

```
var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");
```

The preferred way to set the port from the environment is to use the `ASPNETCORE_URLS` environment variable, which is shown in the following section.

#### Set the ports via the `ASPNETCORE_URLS` environment variable

The `ASPNETCORE_URLS` environment variable is available to set the port:

```
ASPNETCORE_URLS=http://localhost:3000
```

`ASPNETCORE_URLS` supports multiple URLs:

```
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
```

### Listen on all interfaces

The following samples demonstrate listening on all interfaces

#### `http://*:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://+:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://0.0.0.0:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Listen on all interfaces using `ASPNETCORE_URLS`

The preceding samples can use `ASPNETCORE_URLS`:

```
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
```

### Listen on all interfaces using `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`

The preceding samples can also use `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`, but `ASPNETCORE_URLS` (when set) overrides the following settings:

```
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
```

For more information, see [Configure endpoints for Kestrel web server](../servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### Specify HTTPS with development certificate

```
var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information on the development certificate, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../../security/enforcing-ssl?view=aspnetcore-10.0#trust).

### Specify HTTPS using a custom certificate

The following sections show how to specify the custom certificate using the `appsettings.json` file and via configuration.

#### Specify the custom certificate with `appsettings.json`

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}
```

#### Specify the custom certificate via configuration

```
var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Use the certificate APIs

```
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Read the environment

```
var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();
```

For more information using the environment, see [ASP.NET Core runtime environments](../environments?view=aspnetcore-10.0).

### Configuration

The following code reads from the configuration system:

```
var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();
```

For more information, see [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

### Logging

The following code writes a message to the log on application startup:

```
var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information, see [Logging in .NET and ASP.NET Core](../logging/?view=aspnetcore-10.0).

### Access the Dependency Injection (DI) container

The following code shows how to get services from the DI container during application startup:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();
```

The following code shows how to access keys from the DI container using the [`[FromKeyedServices]` attribute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}
```

For more information on DI, see [Dependency injection in ASP.NET Core](../dependency-injection?view=aspnetcore-10.0).

## `WebApplicationBuilder`

This section contains sample code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

### Change the content root, application name, and environment

The following code sets the content root, application name, and environment:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

[WebApplication.Create](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.create) initializes a new instance of the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) class with preconfigured defaults.

For more information, see [ASP.NET Core fundamentals overview](../?view=aspnetcore-10.0)

### Change the content root, app name, and environment by using environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| Feature          | Environment variable         | Command-line argument |
| ---------------- | ---------------------------- | --------------------- |
| Application name | `ASPNETCORE_APPLICATIONNAME` | `--applicationName`   |
| Environment name | `ASPNETCORE_ENVIRONMENT`     | `--environment`       |
| Content root     | `ASPNETCORE_CONTENTROOT`     | `--contentRoot`       |

### Add configuration providers

The following sample adds the INI configuration provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](../configuration/?view=aspnetcore-10.0#file-configuration-provider) in [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

### Read configuration

By default the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) reads configuration from multiple sources, including:

* `appSettings.json` and `appSettings.{environment}.json`
* Environment variables
* The command line

For a complete list of configuration sources read, see [Default configuration](../configuration/?view=aspnetcore-10.0#default-configuration) in [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

The following code reads `HelloKey` from configuration and displays the value at the `/` endpoint. If the configuration value is null, "Hello" is assigned to `message`:

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

### Read the environment

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Add logging providers

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();
```

### Add services

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

### Customize the `IHostBuilder`

Existing extension methods on [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) can be accessed using the [Host property](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.properties#microsoft-extensions-hosting-ihostbuilder-properties):

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Customize the `IWebHostBuilder`

Extension methods on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) can be accessed using the [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost) property.

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();
```

### Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed with `WebHostOptions`, the command line, or with the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();
```

### Custom dependency injection (DI) container

The following example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

### Add middleware

Any existing ASP.NET Core middleware can be configured on the `WebApplication`:

```
var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information, see [ASP.NET Core middleware](../middleware/?view=aspnetcore-10.0).

### Developer exception page

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the [development environment](../environments?view=aspnetcore-10.0), navigating to `/` renders a friendly page that shows the exception.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();
```

## `WebApplication`

The following code is generated by an ASP.NET Core template:

```
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

The preceding code can be created via `dotnet new web` on the command line or selecting the Empty Web template in Visual Studio.

The following code creates a [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) (`app`) without explicitly creating a [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder):

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();
```

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults:

[WebApplication](webapplication?view=aspnetcore-10.0) automatically adds the following middleware in ASP.NET Core apps depending on certain conditions:

```
if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// User middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// End user middleware/endpoints

app.UseEndpoints(e => {});
```

In some cases, the default middleware configuration isn't correct for the app and requires modification. For example, [UseCors](/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors) should be called before [UseAuthentication](/en-us/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication) and [UseAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationappbuilderextensions.useauthorization). The app needs to call `UseAuthentication` and `UseAuthorization` if `UseCors` is called:

```
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
```

If middleware should run before route matching occurs, [UseRouting](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.userouting) should be called and the middleware should be placed before the call to `UseRouting`. [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) isn't required in this case because it's automatically added as described earlier:

```
app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// Other middleware and endpoints
```

When adding a terminal middleware:

* The middleware must be added after `UseEndpoints`.

* The app needs to call `UseRouting` and `UseEndpoints` so the terminal middleware can be placed at the correct location.

```
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});
```

Terminal middleware is middleware that runs if no endpoint handles the request.

For information on antiforgery middleware in Minimal APIs, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0#afwma).

### Working with ports

When a web app is created with Visual Studio or `dotnet new`, a `Properties/launchSettings.json` file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog `Unable to connect to web server 'AppName'`. Visual Studio returns an error because it's expecting the port specified in `Properties/launchSettings.json`, but the app is using the port specified by `app.Run("http://localhost:3000")`. Run the following port changing samples from the command line.

The following sections set the app's port.

```
var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");
```

In the preceding code, the app responds to port `3000`.

#### Multiple ports

In the following code, the app responds to port `3000` and `4000`:

```
var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Set the port from the command line

The following command makes the app respond to port `7777`:

```
dotnet run --urls="https://localhost:7777"
```

If the Kestrel endpoint is also configured in the `appsettings.json` file, the `appsettings.json` file specified URL is used. For more information, see [Kestrel endpoint configuration](../configuration/?view=aspnetcore-10.0#kestrel-endpoint-configuration)

#### Read the port from environment

The following code reads the port from the environment:

```
var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");
```

The preferred way to set the port from the environment is to use the `ASPNETCORE_URLS` environment variable, which is shown in the following section.

#### Set the ports via the `ASPNETCORE_URLS` environment variable

The `ASPNETCORE_URLS` environment variable is available to set the port:

```
ASPNETCORE_URLS=http://localhost:3000
```

`ASPNETCORE_URLS` supports multiple URLs:

```
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
```

### Listen on all interfaces

The following samples demonstrate listening on all interfaces.

#### `http://*:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://+:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### `http://0.0.0.0:3000`

```
var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Listen on all interfaces using `ASPNETCORE_URLS`

The preceding samples can use `ASPNETCORE_URLS`:

```
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
```

### Listen on all interfaces using `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`

The preceding samples can also use `ASPNETCORE_HTTP_PORTS` and `ASPNETCORE_HTTPS_PORTS`, but `ASPNETCORE_URLS` (when set) overrides the following settings:

```
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
```

For more information, see [Configure endpoints for Kestrel web server](../servers/kestrel/endpoints?view=aspnetcore-10.0).

Note

Don't confuse the `HTTPS_PORTS` configuration key and `ASPNETCORE_HTTPS_PORTS` environment variable, which set the ports for Kestrel/HTTP.sys endpoint configuration, with the `HTTPS_PORT` configuration key and `ASPNETCORE_HTTPS_PORT` environment variable, which set the port for [HTTPS redirection middleware](../../security/enforcing-ssl?view=aspnetcore-10.0#https-redirection-middleware-usehttpsredirection).

### Specify HTTPS with development certificate

```
var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information on the development certificate, see [Trust the ASP.NET Core HTTPS development certificate on Windows and macOS](../../security/enforcing-ssl?view=aspnetcore-10.0#trust).

### Specify HTTPS using a custom certificate

The following sections show how to specify the custom certificate using the `appsettings.json` file and via configuration.

#### Specify the custom certificate with `appsettings.json`

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}
```

#### Specify the custom certificate via configuration

```
var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

#### Use the certificate APIs

```
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();
```

### Read the environment

```
var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();
```

For more information using the environment, see [ASP.NET Core runtime environments](../environments?view=aspnetcore-10.0).

### Configuration

The following code reads from the configuration system:

```
var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();
```

For more information, see [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

### Logging

The following code writes a message to the log on application startup:

```
var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();
```

For more information, see [Logging in .NET and ASP.NET Core](../logging/?view=aspnetcore-10.0).

### Access the Dependency Injection (DI) container

The following code shows how to get services from the DI container during application startup:

```

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();
```

The following code shows how to access keys from the DI container using the [`[FromKeyedServices]` attribute](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}
```

For more information on DI, see [Dependency injection in ASP.NET Core](../dependency-injection?view=aspnetcore-10.0).

## `WebApplicationBuilder`

This section contains sample code using [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder).

### Change the content root, application name, and environment

The following code sets the content root, application name, and environment:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();
```

[WebApplication.Create](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.create) initializes a new instance of the [WebApplication](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication) class with preconfigured defaults.

For more information, see [ASP.NET Core fundamentals overview](../?view=aspnetcore-10.0)

### Change the content root, app name, and environment by using environment variables or command line

The following table shows the environment variable and command-line argument used to change the content root, app name, and environment.

| Feature          | Environment variable         | Command-line argument |
| ---------------- | ---------------------------- | --------------------- |
| Application name | `ASPNETCORE_APPLICATIONNAME` | `--applicationName`   |
| Environment name | `ASPNETCORE_ENVIRONMENT`     | `--environment`       |
| Content root     | `ASPNETCORE_CONTENTROOT`     | `--contentRoot`       |

### Add configuration providers

The following sample adds the INI configuration provider:

```
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();
```

For detailed information, see [File configuration providers](../configuration/?view=aspnetcore-10.0#file-configuration-provider) in [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

### Read configuration

By default the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) reads configuration from multiple sources, including:

* `appSettings.json` and `appSettings.{environment}.json`
* Environment variables
* The command line

For a complete list of configuration sources read, see [Default configuration](../configuration/?view=aspnetcore-10.0#default-configuration) in [Configuration in ASP.NET Core](../configuration/?view=aspnetcore-10.0).

The following code reads `HelloKey` from configuration and displays the value at the `/` endpoint. If the configuration value is null, "Hello" is assigned to `message`:

```
var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();
```

### Read the environment

```
var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Add logging providers

```
var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();
```

### Add services

```
var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
```

### Customize the `IHostBuilder`

Existing extension methods on [IHostBuilder](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder) can be accessed using the [Host property](/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder.properties#microsoft-extensions-hosting-ihostbuilder-properties):

```
var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

### Customize the `IWebHostBuilder`

Extension methods on [IWebHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostbuilder) can be accessed using the [WebApplicationBuilder.WebHost](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder.webhost) property.

```
var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();
```

### Change the web root

By default, the web root is relative to the content root in the `wwwroot` folder. Web root is where the static file middleware looks for static files. Web root can be changed with `WebHostOptions`, the command line, or with the [UseWebRoot](/en-us/dotnet/api/microsoft.aspnetcore.hosting.hostingabstractionswebhostbuilderextensions.usewebroot) method:

```
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();
```

### Custom dependency injection (DI) container

The following example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html):

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();
```

### Add middleware

Any existing ASP.NET Core middleware can be configured on the `WebApplication`:

```
var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();
```

For more information, see [ASP.NET Core middleware](../middleware/?view=aspnetcore-10.0).

### Developer exception page

[WebApplication.CreateBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication.createbuilder) initializes a new instance of the [WebApplicationBuilder](/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplicationbuilder) class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the [development environment](../environments?view=aspnetcore-10.0), navigating to `/` renders a friendly page that shows the exception.

```
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();
```

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-19

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/apache
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/apache.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host and deploy ASP.NET Core Blazor WebAssembly with Apache

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to host and deploy Blazor WebAssembly using [Apache](https://www.apache.org/).

To deploy a Blazor WebAssembly app to Apache:

Create the Apache configuration file. The following example is a simplified configuration file (`blazorapp.config`):

```
<VirtualHost *:80>
    ServerName www.example.com
    ServerAlias *.example.com

    DocumentRoot "/var/www/blazorapp"
    ErrorDocument 404 /index.html

    AddType application/wasm .wasm

    <Directory "/var/www/blazorapp">
        Options -Indexes
        AllowOverride None
    </Directory>

    <IfModule mod_deflate.c>
        AddOutputFilterByType DEFLATE text/css
        AddOutputFilterByType DEFLATE application/javascript
        AddOutputFilterByType DEFLATE text/html
        AddOutputFilterByType DEFLATE application/octet-stream
        AddOutputFilterByType DEFLATE application/wasm
        <IfModule mod_setenvif.c>
            BrowserMatch ^Mozilla/4 gzip-only-text/html
            BrowserMatch ^Mozilla/4.0[678] no-gzip
            BrowserMatch bMSIE !no-gzip !gzip-only-text/html
        </IfModule>
    </IfModule>

    ErrorLog /var/log/httpd/blazorapp-error.log
    CustomLog /var/log/httpd/blazorapp-access.log common
</VirtualHost>
```

Create the Apache configuration file. The following example is a simplified configuration file (`blazorapp.config`):

```
<VirtualHost *:80>
    ServerName www.example.com
    ServerAlias *.example.com

    DocumentRoot "/var/www/blazorapp"
    ErrorDocument 404 /index.html

    AddType application/wasm .wasm
    AddType application/octet-stream .dll

    <Directory "/var/www/blazorapp">
        Options -Indexes
        AllowOverride None
    </Directory>

    <IfModule mod_deflate.c>
        AddOutputFilterByType DEFLATE text/css
        AddOutputFilterByType DEFLATE application/javascript
        AddOutputFilterByType DEFLATE text/html
        AddOutputFilterByType DEFLATE application/octet-stream
        AddOutputFilterByType DEFLATE application/wasm
        <IfModule mod_setenvif.c>
            BrowserMatch ^Mozilla/4 gzip-only-text/html
            BrowserMatch ^Mozilla/4.0[678] no-gzip
            BrowserMatch bMSIE !no-gzip !gzip-only-text/html
        </IfModule>
    </IfModule>

    ErrorLog /var/log/httpd/blazorapp-error.log
    CustomLog /var/log/httpd/blazorapp-access.log common
</VirtualHost>
```

Place the Apache configuration file into the `/etc/httpd/conf.d/` directory.

Place the app's published assets (`/bin/Release/{TARGET FRAMEWORK}/publish/wwwroot`, where the `{TARGET FRAMEWORK}` placeholder is the target framework) into the `/var/www/blazorapp` directory (the location specified to `DocumentRoot` in the configuration file).

Restart the Apache service.

For more information, see [`mod_mime`](https://httpd.apache.org/docs/2.4/mod/mod_mime.html) and [`mod_deflate`](https://httpd.apache.org/docs/current/mod/mod_deflate.html).

## Hosted deployment on Linux (Apache)

Configure the app with [ForwardedHeadersOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions) to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers by following the guidance in [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

For more information on setting the app's base path, including sub-app path configuration, see [ASP.NET Core Blazor app base path](../app-base-path?view=aspnetcore-10.0).

The following example hosts the app at a root URL (no sub-app path):

```
<VirtualHost *:*>
    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>

<VirtualHost *:80>
    ProxyPreserveHost On
    ProxyPass         / http://localhost:5000/
    ProxyPassReverse  / http://localhost:5000/
    ProxyPassMatch    ^/_blazor/(.*) http://localhost:5000/_blazor/$1
    ProxyPass         /_blazor ws://localhost:5000/_blazor
    ServerName        www.example.com
    ServerAlias       *.example.com
    ErrorLog          ${APACHE_LOG_DIR}helloapp-error.log
    CustomLog         ${APACHE_LOG_DIR}helloapp-access.log common
</VirtualHost>
```

To configure the server to host the app at a sub-app path, the `{PATH}` placeholder in the following entries is the sub-app path:

```
<VirtualHost *:*>
    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>

<VirtualHost *:80>
    ProxyPreserveHost On
    ProxyPass         / http://localhost:5000/{PATH}
    ProxyPassReverse  / http://localhost:5000/{PATH}
    ProxyPassMatch    ^/_blazor/(.*) http://localhost:5000/{PATH}/_blazor/$1
    ProxyPass         /_blazor ws://localhost:5000/{PATH}/_blazor
    ServerName        www.example.com
    ServerAlias       *.example.com
    ErrorLog          ${APACHE_LOG_DIR}helloapp-error.log
    CustomLog         ${APACHE_LOG_DIR}helloapp-access.log common
</VirtualHost>
```

For an app that responds to requests at `/blazor`:

```
<VirtualHost *:*>
    RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost>

<VirtualHost *:80>
    ProxyPreserveHost On
    ProxyPass         / http://localhost:5000/blazor
    ProxyPassReverse  / http://localhost:5000/blazor
    ProxyPassMatch    ^/_blazor/(.*) http://localhost:5000/blazor/_blazor/$1
    ProxyPass         /_blazor ws://localhost:5000/blazor/_blazor
    ServerName        www.example.com
    ServerAlias       *.example.com
    ErrorLog          ${APACHE_LOG_DIR}helloapp-error.log
    CustomLog         ${APACHE_LOG_DIR}helloapp-access.log common
</VirtualHost>
```

## Related content

* [Apache documentation](https://httpd.apache.org/docs/current/mod/mod_proxy.html)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0021
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0021.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0021: The return type of the BindAsync method must be `ValueTask<T>`.

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0021      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

An implementation of the [BindAsync](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1.bindasync) method has a return type that isn't [ValueTask\<TResult>](/en-us/dotnet/api/system.threading.tasks.valuetask-1).

## Rule description

This diagnostic is emitted when an implementation of the `BindAsync` method has a return type that isn't `ValueTask<T>`.

## How to fix violations

To fix a violation of this rule, define a `ValueTask<T>` return type for `BindAsync` and consider implementing [IBindableFromHttpContext\<TSelf>](/en-us/dotnet/api/microsoft.aspnetcore.http.ibindablefromhttpcontext-1) to enforce implementation.

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/non-di-scenarios
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/data-protection/configuration/non-di-scenarios.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Non-DI aware scenarios for Data Protection in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT)

The ASP.NET Core Data Protection system is normally [added to a service container](../consumer-apis/overview?view=aspnetcore-10.0) and consumed by dependent components via dependency injection (DI). However, there are cases where this isn't feasible or desired, especially when importing the system into an existing app.

To support these scenarios, the [Microsoft.AspNetCore.DataProtection.Extensions](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.Extensions/) package provides a concrete type, [DataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionprovider), which offers a simple way to use Data Protection without relying on DI. The `DataProtectionProvider` type implements [IDataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotectionprovider). Constructing `DataProtectionProvider` only requires providing a [DirectoryInfo](/en-us/dotnet/api/system.io.directoryinfo) instance to indicate where the provider's cryptographic keys should be stored, as seen in the following code sample:

```
using System;
using System.IO;
using Microsoft.AspNetCore.DataProtection;

public class Program
{
    public static void Main(string[] args)
    {
        // Get the path to %LOCALAPPDATA%\myapp-keys
        var destFolder = Path.Combine(
            System.Environment.GetEnvironmentVariable("LOCALAPPDATA"),
            "myapp-keys");

        // Instantiate the data protection system at this folder
        var dataProtectionProvider = DataProtectionProvider.Create(
            new DirectoryInfo(destFolder));

        var protector = dataProtectionProvider.CreateProtector("Program.No-DI");
        Console.Write("Enter input: ");
        var input = Console.ReadLine();

        // Protect the payload
        var protectedPayload = protector.Protect(input);
        Console.WriteLine($"Protect returned: {protectedPayload}");

        // Unprotect the payload
        var unprotectedPayload = protector.Unprotect(protectedPayload);
        Console.WriteLine($"Unprotect returned: {unprotectedPayload}");

        Console.WriteLine();
        Console.WriteLine("Press any key...");
        Console.ReadKey();
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Enter input: Hello world!
 * Protect returned: CfDJ8FWbAn6...ch3hAPm1NJA
 * Unprotect returned: Hello world!
 *
 * Press any key...
*/
```

By default, the `DataProtectionProvider` concrete type doesn't encrypt raw key material before persisting it to the file system. This is to support scenarios where the developer points to a network share and the Data Protection system can't automatically deduce an appropriate at-rest key encryption mechanism.

Additionally, the `DataProtectionProvider` concrete type doesn't [isolate apps](overview?view=aspnetcore-10.0#per-application-isolation) by default. All apps using the same key directory can share payloads as long as their [purpose parameters](../consumer-apis/purpose-strings?view=aspnetcore-10.0) match.

The [DataProtectionProvider](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionprovider) constructor accepts an optional configuration callback that can be used to adjust the behaviors of the system. The sample below demonstrates restoring isolation with an explicit call to [SetApplicationName](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.setapplicationname). The sample also demonstrates configuring the system to automatically encrypt persisted keys using Windows DPAPI. If the directory points to a UNC share, you may wish to distribute a shared certificate across all relevant machines and to configure the system to use certificate-based encryption with a call to [ProtectKeysWithCertificate](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.dataprotectionbuilderextensions.protectkeyswithcertificate).

```
using System;
using System.IO;
using Microsoft.AspNetCore.DataProtection;

public class Program
{
    public static void Main(string[] args)
    {
        // Get the path to %LOCALAPPDATA%\myapp-keys
        var destFolder = Path.Combine(
            System.Environment.GetEnvironmentVariable("LOCALAPPDATA"),
            "myapp-keys");

        // Instantiate the data protection system at this folder
        var dataProtectionProvider = DataProtectionProvider.Create(
            new DirectoryInfo(destFolder),
            configuration =>
            {
                configuration.SetApplicationName("my app name");
                configuration.ProtectKeysWithDpapi();
            });

        var protector = dataProtectionProvider.CreateProtector("Program.No-DI");
        Console.Write("Enter input: ");
        var input = Console.ReadLine();

        // Protect the payload
        var protectedPayload = protector.Protect(input);
        Console.WriteLine($"Protect returned: {protectedPayload}");

        // Unprotect the payload
        var unprotectedPayload = protector.Unprotect(protectedPayload);
        Console.WriteLine($"Unprotect returned: {unprotectedPayload}");

        Console.WriteLine();
        Console.WriteLine("Press any key...");
        Console.ReadKey();
    }
}
```

Tip

Instances of the `DataProtectionProvider` concrete type are expensive to create. If an app maintains multiple instances of this type and if they're all using the same key storage directory, app performance might degrade. If you use the `DataProtectionProvider` type, we recommend that you create this type once and reuse it as much as possible. The `DataProtectionProvider` type and all [IDataProtector](/en-us/dotnet/api/microsoft.aspnetcore.dataprotection.idataprotector) instances created from it are thread-safe for multiple callers.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2024-06-17

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0005
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0005.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0005: Do not place attribute on method called by route handler lambda

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0005      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

An attribute was applied to a method definition instead of the route handler in a route handler endpoint.

## Rule description

When an endpoint is declared, attributes should be applied to the delegate parameter in order to be effective. For example, the [Authorize](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute in the following code sample isn't set on the registered endpoint:

```
app.MapGet("/todos/{id}", GetTodoById);

[Authorize]
Todo GetTodoById(int id)
{
  ...
}
```

The attribute must be placed on the route handler parameter as shown in the following code:

```
app.MapGet("/todos/{id}", [Authorize] GetTodoById);

Todo GetTodoById(int id)
{
  ...
}
```

## How to fix violations

To fix a violation of this rule, make sure that endpoint attributes are applied to the route handler parameter:

```
app.MapGet("/todos/{id}", [Authorize] (int id) => {});
app.MapGet("/users/{id}", [Authorize] GetUserById);
```

## When to suppress warnings

Do not suppress a warning from this rule. Misplaced attributes can result in unexpected behavior at runtime.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/performance/rate-limit.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Rate limiting middleware in ASP.NET Core

By [Arvin Kahbazi](https://github.com/Kahbazi), [Maarten Balliauw](https://github.com/maartenba), and [Rick Anderson](https://twitter.com/RickAndMSFT)

The `Microsoft.AspNetCore.RateLimiting` middleware provides rate limiting middleware. Apps configure rate limiting policies and then attach the policies to endpoints. Apps using rate limiting should be carefully load tested and reviewed before deploying. See [Testing endpoints with rate limiting](#test7) in this article for more information.

For an introduction to rate limiting, see [Rate limiting middleware](https://blog.maartenballiauw.be/posts/2022-09-26-aspnet-core-rate-limiting-middleware).

## Why use rate limiting

Rate limiting can be used for managing the flow of incoming requests to an app. Key reasons to implement rate limiting:

* **Preventing Abuse**: Rate limiting helps protect an app from abuse by limiting the number of requests a user or client can make in a given time period. This protection is particularly important for public APIs.
* **Ensuring Fair Usage**: By setting limits that prevent users from monopolizing the system, you ensure that all users have fair access to resources.
* **Protecting Resources**: Rate limiting helps prevent server overload by controlling the number of requests that can be processed. It protects the backend resources from being overwhelmed.
* **Enhancing Security**: It can mitigate the risk of Denial of Service (DoS) attacks by limiting the rate at which requests are processed. It makes it harder for attackers to flood a system.
* **Improving Performance**: By controlling the rate of incoming requests, you can maintain optimal performance and responsiveness of an app, ensuring a better user experience.
* **Cost Management**: For services that incur costs based on usage, rate limiting can help manage and predict expenses by controlling the volume of requests processed.

Implementing rate limiting in an ASP.NET Core app can help maintain stability, security, and performance. The result is a reliable and efficient service for all users.

## Prevent DDoS attacks

While rate limiting can help mitigate the risk of Denial of Service (DoS) attacks by limiting the rate at which requests are processed, it's not a comprehensive solution for Distributed Denial of Service (DDoS) attacks. DDoS attacks involve multiple systems overwhelming an app with a flood of requests, making it difficult to handle with rate limiting alone.

For robust DDoS protection, consider using a commercial DDoS protection service. These services offer advanced features such as:

* **Traffic analysis**: Continuous monitoring and analysis of incoming traffic to detect and mitigate DDoS attacks in real time.
* **Scalability**: The ability to handle large-scale attacks by distributing traffic across multiple servers and data centers.
* **Automated mitigation**: Automated response mechanisms to quickly block malicious traffic without manual intervention.
* **Global network**: A global network of servers to absorb and mitigate attacks closer to the source.
* **Constant updates**: Commercial services continuously track and update their protection mechanisms to adapt to new and evolving threats.

When using a cloud hosting service, DDoS protection is usually available as part of the hosting solution, such as [Azure Web Application Firewall](https://azure.microsoft.com/products/web-application-firewall/), [AWS Shield](https://aws.amazon.com/shield/) or [Google Cloud Armor](https://cloud.google.com/armor/docs). Dedicated protections are available as Web Application Firewalls (WAF) or as part of a CDN solution such as [Cloudflare](https://www.cloudflare.com/ddos/) or [Akamai Kona Site Defender](https://www.akamai.com/us/en/products/security/kona-site-defender.jsp)

Implementing a commercial DDoS protection service in conjunction with rate limiting can provide a comprehensive defense strategy, ensuring the stability, security, and performance of an app.

## Use rate-limiting middleware

The following steps show how to use the rate limiting middleware in an ASP.NET Core app:

1. Configure rate limiting services.

In the `Program.cs` file, configure the rate limiting services by adding the appropriate rate limiting policies. Define policies as either global or named policies. The following example permits 10 requests per minute by user (identity) or globally:

```
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 10,
                QueueLimit = 0,
                Window = TimeSpan.FromMinutes(1)
            }));
});
```

Named policies need to be explicitly applied to the pages or endpoints. The following example adds a fixed window limiter policy named `"fixed"` which you add to an endpoint later:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", opt =>
    {
        opt.PermitLimit = 4;
        opt.Window = TimeSpan.FromSeconds(12);
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 2;
    });
});

var app = builder.Build();
```

The global limiter applies to all endpoints automatically when you configure it via [options.GlobalLimiter](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.ratelimiteroptions.globallimiter).

2. Enable rate limiting middleware

   In the `Program.cs` file, enable the rate limiting middleware by calling [UseRateLimiter](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterapplicationbuilderextensions.useratelimiter):

```
app.UseRouting();

app.UseRateLimiter();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

app.Run();
```

### Apply rate limiting policies to endpoints or pages

#### Apply rate limiting to Web API endpoints

Apply a named policy to the endpoint or group, for example:

```

app.MapGet("/api/resource", () => "This endpoint is rate limited")
   .RequireRateLimiting("fixed"); // Apply specific policy to an endpoint
```

#### Apply rate limiting to MVC controllers

Apply the configured rate limiting policies to specific endpoints or globally. For example, to apply the "fixed" policy to all controller endpoints:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers().RequireRateLimiting("fixed");
});
```

#### Apply rate limiting to server-side Blazor apps

To set rate limiting for all of the app's routable Razor components, specify [RequireRateLimiting](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterendpointconventionbuilderextensions.requireratelimiting) with the rate limiting policy name on the [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents) call in the `Program` file. In the following example, the rate limiting policy named "`policy`" is applied:

```
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .RequireRateLimiting("policy");
```

To set a policy for a single routable Razor component or a folder of components via an imports file (`_Imports.razor`), apply the [`[EnableRateLimiting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.enableratelimitingattribute) with the policy name. In the following example, the rate limiting policy named "`override`" is applied. The policy replaces any policies currently applied to the endpoint. The global limiter still runs on the endpoint with this attribute applied.

```
@page "/counter"
@using Microsoft.AspNetCore.RateLimiting
@attribute [EnableRateLimiting("override")]

<h1>Counter</h1>
```

Apply the [`[EnableRateLimiting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.enableratelimitingattribute) only to a routable component or a folder of components via an imports file if [RequireRateLimiting](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterendpointconventionbuilderextensions.requireratelimiting) is ***not*** called on [MapRazorComponents](/en-us/dotnet/api/microsoft.aspnetcore.builder.razorcomponentsendpointroutebuilderextensions.maprazorcomponents).

Use the [`[DisableRateLimiting]` attribute](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.disableratelimitingattribute) to disable rate limiting for a routable component or a folder of components via an imports file.

## Rate limiter algorithms

The [`RateLimiterOptionsExtensions`](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.ratelimiteroptionsextensions) class provides the following extension methods for rate limiting:

* [Fixed window](#fixed)
* [Sliding window](#slide)
* [Token bucket](#token)
* [Concurrency](#concur)

The fixed, sliding, and token limiters all limit the maximum number of requests in a time period. The concurrency limiter limits only the number of concurrent requests and doesn't cap the number of requests in a time period. Consider the cost of an endpoint when you select a limiter. The cost of an endpoint includes the resources used, such as time, data access, CPU, and I/O.

[]()

### Fixed window limiter

The [`AddFixedWindowLimiter`](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.ratelimiteroptionsextensions.addfixedwindowlimiter#microsoft-aspnetcore-ratelimiting-ratelimiteroptionsextensions-addfixedwindowlimiter\(microsoft-aspnetcore-ratelimiting-ratelimiteroptions-system-string-system-threading-ratelimiting-fixedwindowratelimiteroptions\)) method uses a fixed time window to limit requests. When the time window expires, a new time window starts and the request limit resets.

Consider the following code:

```
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter(policyName: "fixed", options =>
    {
        options.PermitLimit = 4;
        options.Window = TimeSpan.FromSeconds(12);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = 2;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
                           .RequireRateLimiting("fixed");

app.Run();
```

The preceding code:

* Calls [AddRateLimiter](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterservicecollectionextensions.addratelimiter) to add a rate limiting service to the service collection.
* Calls `AddFixedWindowLimiter` to create a fixed window limiter with a policy name of `"fixed"` and sets:
* [PermitLimit](/en-us/dotnet/api/system.threading.ratelimiting.fixedwindowratelimiteroptions.permitlimit#system-threading-ratelimiting-fixedwindowratelimiteroptions-permitlimit) to 4 and the time [Window](/en-us/dotnet/api/system.threading.ratelimiting.fixedwindowratelimiteroptions.window#system-threading-ratelimiting-fixedwindowratelimiteroptions-window) to 12. A maximum of 4 requests per each 12-second window are allowed.
* [QueueProcessingOrder](/en-us/dotnet/api/system.threading.ratelimiting.fixedwindowratelimiteroptions.queueprocessingorder#system-threading-ratelimiting-fixedwindowratelimiteroptions-queueprocessingorder) to [OldestFirst](/en-us/dotnet/api/system.threading.ratelimiting.queueprocessingorder#system-threading-ratelimiting-queueprocessingorder-oldestfirst).
* [QueueLimit](/en-us/dotnet/api/system.threading.ratelimiting.fixedwindowratelimiteroptions.queuelimit#system-threading-ratelimiting-fixedwindowratelimiteroptions-queuelimit) to 2 (set this to 0 to disable the queueing mechanism).
* Calls [UseRateLimiter](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterapplicationbuilderextensions.useratelimiter) to enable rate limiting.

Apps should use [Configuration](../fundamentals/configuration/?view=aspnetcore-10.0) to set limiter options. The following code updates the preceding code using [`MyRateLimitOptions`](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/main/fundamentals/middleware/rate-limit/WebRateLimitAuth/Models/MyRateLimitOptions.cs) for configuration:

```
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MyRateLimitOptions>(
    builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter(policyName: fixedPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Fixed Window Limiter {GetTicks()}"))
                           .RequireRateLimiting(fixedPolicy);

app.Run();
```

[UseRateLimiter](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterapplicationbuilderextensions.useratelimiter) must be called after `UseRouting` when rate limiting endpoint specific APIs are used. For example, if the [`[EnableRateLimiting]`](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.enableratelimitingattribute) attribute is used, `UseRateLimiter` must be called after `UseRouting`. When calling only global limiters, `UseRateLimiter` can be called before `UseRouting`.

[]()

### Sliding window limiter

A sliding window algorithm:

* Is similar to the fixed window limiter but adds segments per window. The window slides one segment each segment interval. The segment interval is (window time)/(segments per window).
* Limits the requests for a window to `permitLimit` requests.
* Each time window is divided in `n` segments per window.
* Requests taken from the expired time segment one window back (`n` segments prior to the current segment) are added to the current segment. We refer to the most expired time segment one window back as the expired segment.

Consider the following table that shows a sliding window limiter with a 30-second window, three segments per window, and a limit of 100 requests:

* The top row and first column shows the time segment.
* The second row shows the remaining requests available. The remaining requests are calculated as the available requests minus the processed requests plus the recycled requests.
* Requests at each time moves along the diagonal blue line.
* From time 30 on, the request taken from the expired time segment are added back to the request limit, as shown in the red lines.

The following table shows the data in the previous graph in a different format. The **Available** column shows the requests available from the previous segment (The **Carry over** from the previous row). The first row shows 100 available requests because there's no previous segment.

| Time | Available | Taken | Recycled from expired | Carry over |
| ---- | --------- | ----- | --------------------- | ---------- |
| 0    | 100       | 20    | 0                     | 80         |
| 10   | 80        | 30    | 0                     | 50         |
| 20   | 50        | 40    | 0                     | 10         |
| 30   | 10        | 30    | 20                    | 0          |
| 40   | 0         | 10    | 30                    | 20         |
| 50   | 20        | 10    | 40                    | 50         |
| 60   | 50        | 35    | 30                    | 45         |

The following code uses the sliding window rate limiter:

```
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var slidingPolicy = "sliding";

builder.Services.AddRateLimiter(_ => _
    .AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Sliding Window Limiter {GetTicks()}"))
                           .RequireRateLimiting(slidingPolicy);

app.Run();
```

[]()

### Token bucket limiter

The token bucket limiter is similar to the sliding window limiter, but rather than adding back the requests taken from the expired segment, a fixed number of tokens are added each replenishment period. The tokens added each segment can't increase the available tokens to a number higher than the token bucket limit. The following table shows a token bucket limiter with a limit of 100 tokens and a 10-second replenishment period.

| Time | Available | Taken | Added | Carry over |
| ---- | --------- | ----- | ----- | ---------- |
| 0    | 100       | 20    | 0     | 80         |
| 10   | 80        | 10    | 20    | 90         |
| 20   | 90        | 5     | 15    | 100        |
| 30   | 100       | 30    | 20    | 90         |
| 40   | 90        | 6     | 16    | 100        |
| 50   | 100       | 40    | 20    | 80         |
| 60   | 80        | 50    | 20    | 50         |

The following code uses the token bucket limiter:

```
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var tokenPolicy = "token";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

builder.Services.AddRateLimiter(_ => _
    .AddTokenBucketLimiter(policyName: tokenPolicy, options =>
    {
        options.TokenLimit = myOptions.TokenLimit;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
        options.ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod);
        options.TokensPerPeriod = myOptions.TokensPerPeriod;
        options.AutoReplenishment = myOptions.AutoReplenishment;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}"))
                           .RequireRateLimiting(tokenPolicy);

app.Run();
```

When [AutoReplenishment](/en-us/dotnet/api/system.threading.ratelimiting.tokenbucketratelimiteroptions.autoreplenishment) is set to `true`, an internal timer replenishes the tokens every [ReplenishmentPeriod](/en-us/dotnet/api/system.threading.ratelimiting.tokenbucketratelimiter.replenishmentperiod); when set to `false`, the app must call [TryReplenish](/en-us/dotnet/api/system.threading.ratelimiting.tokenbucketratelimiter.tryreplenish) on the limiter.

[]()

### Concurrency limiter

The concurrency limiter limits the number of concurrent requests. Each request reduces the concurrency limit by one. When a request completes, the limit is increased by one. Unlike the other requests limiters that limit the total number of requests for a specified period, the concurrency limiter limits only the number of concurrent requests and doesn't cap the number of requests in a time period.

The following code uses the concurrency limiter:

```
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

var concurrencyPolicy = "Concurrency";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);

builder.Services.AddRateLimiter(_ => _
    .AddConcurrencyLimiter(policyName: concurrencyPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();

app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", async () =>
{
    await Task.Delay(500);
    return Results.Ok($"Concurrency Limiter {GetTicks()}");
                              
}).RequireRateLimiting(concurrencyPolicy);

app.Run();
```

## Rate limiting partitions

Rate limiting partitions divide the traffic into separate buckets that each get their own rate limit counters. This approach provides more granular control than a single global counter. Different keys, such as user ID, IP address, or API key, define the partition buckets.

### Benefits of partitioning

* **Fairness**: One user can't consume the entire rate limit for everyone.
* **Granularity**: Different limits for different users and resources.
* **Security**: Better protection against targeted abuse.
* **Tiered service**: Support for service tiers with different limits.

Partitioned rate limiting gives you fine-grained control over how you manage API traffic while ensuring fair resource allocation.

### By IP address

```
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 50,
            Window = TimeSpan.FromMinutes(1)
        }));
```

### By user identity

```
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.User.Identity?.Name ?? "anonymous",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100,
            Window = TimeSpan.FromMinutes(1)
        }));
```

### By API key

```
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
    string apiKey = httpContext.Request.Headers["X-API-Key"].ToString() ?? "no-key";

    // Different limits based on key tier
    return apiKey switch
    {
        "premium-key" => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: apiKey,
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 1000,
                Window = TimeSpan.FromMinutes(1)
            }),

        _ => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: apiKey,
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1)
            }),
    };
});
```

### By endpoint path

```
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
    string path = httpContext.Request.Path.ToString();

    // Different limits for different paths
    if (path.StartsWith("/api/public"))
    {
        return RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: $"{httpContext.Connection.RemoteIpAddress}-public",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 30,
                Window = TimeSpan.FromSeconds(10)
            });
    }

    return RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100,
            Window = TimeSpan.FromMinutes(1)
        });
});
```

### Create chained limiters

The [CreateChained](/en-us/dotnet/api/system.threading.ratelimiting.partitionedratelimiter.createchained) API accepts multiple [PartitionedRateLimiter](/en-us/dotnet/api/system.threading.ratelimiting.partitionedratelimiter) instances and combines them into one `PartitionedRateLimiter`. The combined limiter runs all the input limiters in sequence. Because the result is a `PartitionedRateLimiter` assigned to `GlobalLimiter`, the chain applies to every endpoint. To chain limiters for a specific endpoint instead, use a named policy, as shown in [Chain limiters in a named policy](#chain-limiters-in-a-named-policy).

The following code uses `CreateChained`:

```
using System.Globalization;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(_ =>
{
    _.OnRejected = async (context, cancellationToken) =>
    {
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
        }

        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", cancellationToken);
    };
    _.GlobalLimiter = PartitionedRateLimiter.CreateChained(
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();

            return RateLimitPartition.GetFixedWindowLimiter
            (userAgent, _ =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 4,
                    Window = TimeSpan.FromSeconds(2)
                });
        }),
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var userAgent = httpContext.Request.Headers.UserAgent.ToString();
            
            return RateLimitPartition.GetFixedWindowLimiter
            (userAgent, _ =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 20,    
                    Window = TimeSpan.FromSeconds(30)
                });
        }));
});

var app = builder.Build();
app.UseRateLimiter();

static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");

app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"));

app.Run();
```

For more information, see the [CreateChained source code](https://github.com/dotnet/runtime/blob/79874806d246670ee5fe76e73ce566578fe675c0/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/PartitionedRateLimiter.cs#L52-L64).

### Chain limiters in a named policy

[CreateChained](/en-us/dotnet/api/system.threading.ratelimiting.partitionedratelimiter.createchained) chains *global* limiters that apply to every endpoint. To combine multiple limiter types and scope them to specific endpoints, chain the limiters inside a named policy with [CreateChained](/en-us/dotnet/api/system.threading.ratelimiting.ratelimiter.createchained). This overload returns a single [RateLimiter](/en-us/dotnet/api/system.threading.ratelimiting.ratelimiter) that runs each limiter in sequence, which is the return type a named policy's partition factory requires.

The following `"combined"` policy chains a token bucket limiter and a concurrency limiter with `RateLimiter.CreateChained`, then applies the policy to a single endpoint with [RequireRateLimiting](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterendpointconventionbuilderextensions.requireratelimiting):

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("combined", httpContext =>
    {
        // Partition on the authenticated identity name when available. Each distinct key creates and
        // caches its own limiter, so partitioning on unbounded user-controlled
        // input can exhaust memory (a DoS risk).
        string partitionKey = httpContext.User.Identity?.Name ?? "anonymous";

        return RateLimitPartition.Get(partitionKey, _ =>
            RateLimiter.CreateChained(
                new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 100,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 5,
                    ReplenishmentPeriod = TimeSpan.FromSeconds(10),
                    TokensPerPeriod = 10,
                    AutoReplenishment = true
                }),
                new ConcurrencyLimiter(new ConcurrencyLimiterOptions
                {
                    PermitLimit = 5,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 2
                })));
    });
});

var app = builder.Build();

app.MapGet("/api/resource", () => "This endpoint uses multiple limiters")
   .RequireRateLimiting("combined");
```

A request must acquire a lease from every limiter in the chain to proceed, and the limiters run in the order passed to `RateLimiter.CreateChained`. If a limiter rejects the request, the request is rejected and the leases already acquired from earlier limiters in the chain are disposed in reverse order.

When chaining limiters in a named policy, keep the following in mind:

* Disposing a lease returns the permit for a concurrency limiter. The time-based limiters (token bucket, fixed window, and sliding window) don't return a permit that was already acquired when a later limiter in the chain rejects the request, so take this into account when ordering the limiters in the chain.
* Constructing a `TokenBucketRateLimiter` directly with `AutoReplenishment` set to `true` gives each limiter instance its own timer. The `AddTokenBucketLimiter` and `RateLimitPartition.GetTokenBucketLimiter` helpers instead set `AutoReplenishment` to `false` and replenish all of their limiters from a single shared timer.
* `RateLimiter.CreateChained` doesn't dispose the limiters passed to it. In the preceding example, the partition caches the chained limiter and the framework manages its lifetime. If you create chained limiters outside of a partition, dispose the inner limiters when they're no longer in use.
* Prefer the global `PartitionedRateLimiter.CreateChained` approach when the chain should apply to every endpoint. Use a named policy with `RateLimiter.CreateChained` only when the chain must be scoped to specific endpoints.

## Choosing what happens when a request is rate limited

For simple cases, you can just set the status code:

```
builder.Services.AddRateLimiter(options =>
{
    // Set a custom status code for rejections
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // Rate limiter configuration...
});
```

The most common approach is to register an `OnRejected` callback when configuring rate limiting:

```
builder.Services.AddRateLimiter(options =>
{
    // Rate limiter configuration...

    options.OnRejected = async (context, cancellationToken) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;

        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((long)Math.Ceiling(retryAfter.TotalSeconds)).ToString(NumberFormatInfo.InvariantInfo);
        }

        await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Please try again later.", cancellationToken);

        // Optional logging
        logger.LogWarning("Rate limit exceeded for IP: {IpAddress}",
            context.HttpContext.Connection.RemoteIpAddress);
    };
});
```

The example requires `using System.Globalization;` and `using System.Threading.RateLimiting;`.

Note

[FixedWindowRateLimiter](/en-us/dotnet/api/system.threading.ratelimiting.fixedwindowratelimiter) reports `MetadataName.RetryAfter` based on the next fixed-window boundary. Apps that copy this metadata to the `Retry-After` response header in `OnRejected` return an accurate retry interval automatically. Limiters that can't estimate when permits are available, such as [ConcurrencyLimiter](/en-us/dotnet/api/system.threading.ratelimiting.concurrencylimiter), don't provide `RetryAfter` metadata.

Another option is to queue the request:

### Request queuing

When you enable queuing, if a request exceeds the rate limit, the system places it in a queue. The request waits in the queue until a permit becomes available or a timeout occurs. The system processes requests according to a configurable queue order.

```
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", options =>
    {
        options.PermitLimit = 10;           // Allow 10 requests
        options.Window = TimeSpan.FromSeconds(10);  // Per 10-second window
        options.QueueLimit = 5;             // Queue up to 5 additional requests
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; // Process oldest requests first
        options.AutoReplenishment = true; // Default: automatically replenish permits
    });
});
```

## `EnableRateLimiting` and `DisableRateLimiting` attributes

Apply the [`[EnableRateLimiting]`](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.enableratelimitingattribute) and [`[DisableRateLimiting]`](/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.disableratelimitingattribute) attributes to a controller, action method, or Razor Page. For Razor Pages, apply the attribute to the Razor Page and not the page handlers. For example, you can't apply `[EnableRateLimiting]` to `OnGet`, `OnPost`, or any other page handler.

The `[DisableRateLimiting]` attribute ***disables*** rate limiting for the controller, action method, or Razor Page, regardless of named rate limiters or global limiters applied. For example, consider the following code which calls [RequireRateLimiting](/en-us/dotnet/api/microsoft.aspnetcore.builder.ratelimiterendpointconventionbuilderextensions.requireratelimiting) to apply the `fixedPolicy` rate limiting to all controller endpoints:

```
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.Configure<MyRateLimitOptions>(
    builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter(policyName: fixedPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var slidingPolicy = "sliding";

builder.Services.AddRateLimiter(_ => _
    .AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
    {
        options.PermitLimit = myOptions.SlidingPermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();
app.UseRateLimiter();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.MapRazorPages().RequireRateLimiting(slidingPolicy);
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy);

app.Run();
```

In the following code, `[DisableRateLimiting]` disables rate limiting and overrides `[EnableRateLimiting("fixed")]` applied to the `Home2Controller` and `app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)` called in `Program.cs`:

```
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
    private readonly ILogger<Home2Controller> _logger;

    public Home2Controller(ILogger<Home2Controller> logger)
    {
        _logger = logger;
    }

    public ActionResult Index()
    {
        return View();
    }

    [EnableRateLimiting("sliding")]
    public ActionResult Privacy()
    {
        return View();
    }

    [DisableRateLimiting]
    public ActionResult NoLimit()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}
```

In the preceding code, the `[EnableRateLimiting("sliding")]` is ***not*** applied to the `Privacy` action method because `Program.cs` called `app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)`.

Consider the following code which doesn't call `RequireRateLimiting` on `MapRazorPages` or `MapDefaultControllerRoute`:

```
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

builder.Services.Configure<MyRateLimitOptions>(
    builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));

var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";

builder.Services.AddRateLimiter(_ => _
    .AddFixedWindowLimiter(policyName: fixedPolicy, options =>
    {
        options.PermitLimit = myOptions.PermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var slidingPolicy = "sliding";

builder.Services.AddRateLimiter(_ => _
    .AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
    {
        options.PermitLimit = myOptions.SlidingPermitLimit;
        options.Window = TimeSpan.FromSeconds(myOptions.Window);
        options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = myOptions.QueueLimit;
    }));

var app = builder.Build();

app.UseRateLimiter();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.MapRazorPages();
app.MapDefaultControllerRoute();  // RequireRateLimiting not called

app.Run();
```

Consider the following controller:

```
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
    private readonly ILogger<Home2Controller> _logger;

    public Home2Controller(ILogger<Home2Controller> logger)
    {
        _logger = logger;
    }

    public ActionResult Index()
    {
        return View();
    }

    [EnableRateLimiting("sliding")]
    public ActionResult Privacy()
    {
        return View();
    }

    [DisableRateLimiting]
    public ActionResult NoLimit()
    {
        return View();
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }
}
```

In the preceding controller:

* The `"fixed"` policy rate limiter is applied to all action methods that don't have `EnableRateLimiting` and `DisableRateLimiting` attributes.
* The `"sliding"` policy rate limiter is applied to the `Privacy` action.
* Rate limiting is disabled on the `NoLimit` action method.

## Rate limiting metrics

The rate limiting middleware provides [built-in metrics and monitoring](/en-us/aspnet/core/metrics/overview) capabilities to help you understand how rate limits affect app performance and user experience. For a list of metrics, see [`Microsoft.AspNetCore.RateLimiting`](/en-us/dotnet/core/diagnostics/built-in-metrics-aspnetcore#microsoftaspnetcoreratelimiting).

[]()

## Testing endpoints with rate limiting

Before deploying an app that uses rate limiting to production, stress test the app to validate the rate limiters and options you used. For example, create a [JMeter script](https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html) by using a tool like [BlazeMeter](https://www.blazemeter.com/blog/jmeter-tutorial) or [Apache JMeter HTTP(S) Test Script Recorder](https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html) and load the script to [Azure Load Testing](/en-us/azure/load-testing/overview-what-is-azure-load-testing).

If you create partitions by using user input, your app becomes vulnerable to [Denial of Service](https://www.cisa.gov/uscert/ncas/tips/ST04-015) (DoS) attacks. For example, if you create partitions by using client IP addresses, your app becomes vulnerable to Denial of Service attacks that use IP Source Address Spoofing. For more information, see [BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service Attacks that employ IP Source Address Spoofing](https://www.rfc-editor.org/info/bcp38).

## Related content

* [Rate limiting middleware](https://blog.maartenballiauw.be/posts/2022-09-26-aspnet-core-rate-limiting-middleware) by Maarten Balliauw provides an excellent introduction and overview to rate limiting.
* [Rate limit an HTTP handler in .NET](/en-us/dotnet/core/extensions/http-ratelimiter)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-09

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/overview
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/overview.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Overview of ASP.NET Core MVC

By [Steve Smith](https://ardalis.com/)

ASP.NET Core MVC is a rich framework for building web apps and APIs using the Model-View-Controller design pattern.

## MVC pattern

The Model-View-Controller (MVC) architectural pattern separates an application into three main groups of components: Models, Views, and Controllers. This pattern helps to achieve [separation of concerns](/en-us/dotnet/architecture/modern-web-apps-azure/architectural-principles#separation-of-concerns). Using this pattern, user requests are routed to a Controller, which is responsible for working with the Model to perform user actions and/or retrieve results of queries. The Controller chooses the View to display to the user, and provides it with any Model data it requires.

The following diagram shows the three main components and which ones reference the others:

This delineation of responsibilities helps you scale the application in terms of complexity because it's easier to code, debug, and test something (model, view, or controller) that has a single job. It's more difficult to update, test, and debug code that has dependencies spread across two or more of these three areas. For example, user interface logic tends to change more frequently than business logic. If presentation code and business logic are combined in a single object, an object containing business logic must be modified every time the user interface is changed. This often introduces errors and requires the retesting of business logic after every minimal user interface change.

Note

Both the view and the controller depend on the model. However, the model doesn't depend on the view or the controller. This is one of the key benefits of the separation. This separation allows the model to be built and tested independent of the visual presentation.

### Model Responsibilities

The Model in an MVC application represents the state of the application and any business logic or operations that should be performed by it. Business logic should be encapsulated in the model, along with any implementation logic for persisting the state of the application. Strongly-typed views typically use ViewModel types designed to contain the data to display on that view. The controller creates and populates these ViewModel instances from the model.

### View Responsibilities

Views are responsible for presenting content through the user interface. They use the [Razor view engine](#razor-view-engine) to embed .NET code in HTML markup. There should be minimal logic within views, and any logic in them should relate to presenting content. If you find the need to perform a great deal of logic in view files in order to display data from a complex model, consider using a [View Component](views/view-components?view=aspnetcore-10.0), ViewModel, or view template to simplify the view.

### Controller Responsibilities

Controllers are the components that handle user interaction, work with the model, and ultimately select a view to render. In an MVC application, the view only displays information; the controller handles and responds to user input and interaction. In the MVC pattern, the controller is the initial entry point, and is responsible for selecting which model types to work with and which view to render (hence its name - it controls how the app responds to a given request).

Note

Controllers shouldn't be overly complicated by too many responsibilities. To keep controller logic from becoming overly complex, push business logic out of the controller and into the domain model.

Tip

If you find that your controller actions frequently perform the same kinds of actions, move these common actions into [filters](#filters).

## ASP.NET Core MVC

The ASP.NET Core MVC framework is a lightweight, open source, highly testable presentation framework optimized for use with ASP.NET Core.

ASP.NET Core MVC provides a patterns-based way to build dynamic websites that enables a clean separation of concerns. It gives you full control over markup, supports TDD-friendly development and uses the latest web standards.

## Routing

ASP.NET Core MVC is built on top of [ASP.NET Core's routing](../fundamentals/routing?view=aspnetcore-10.0), a powerful URL-mapping component that lets you build applications that have comprehensible and searchable URLs. This enables you to define your application's URL naming patterns that work well for search engine optimization (SEO) and for link generation, without regard for how the files on your web server are organized. You can define your routes using a convenient route template syntax that supports route value constraints, defaults and optional values.

*Convention-based routing* enables you to globally define the URL formats that your application accepts and how each of those formats maps to a specific action method on a given controller. When an incoming request is received, the routing engine parses the URL and matches it to one of the defined URL formats, and then calls the associated controller's action method.

```
routes.MapRoute(name: "Default", template: "{controller=Home}/{action=Index}/{id?}");
```

*Attribute routing* enables you to specify routing information by decorating your controllers and actions with attributes that define your application's routes. This means that your route definitions are placed next to the controller and action with which they're associated.

```
[Route("api/[controller]")]
public class ProductsController : Controller
{
    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
      ...
    }
}
```

## Model binding

ASP.NET Core MVC [model binding](models/model-binding?view=aspnetcore-10.0) converts client request data (form values, route data, query string parameters, HTTP headers) into objects that the controller can handle. As a result, your controller logic doesn't have to do the work of figuring out the incoming request data; it simply has the data as parameters to its action methods.

```
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) { ... }
```

## Model validation

ASP.NET Core MVC supports [validation](models/validation?view=aspnetcore-10.0) by decorating your model object with data annotation validation attributes. The validation attributes are checked on the client side before values are posted to the server, as well as on the server before the controller action is called.

```
using System.ComponentModel.DataAnnotations;
public class LoginViewModel
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Display(Name = "Remember me?")]
    public bool RememberMe { get; set; }
}
```

A controller action:

```
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    if (ModelState.IsValid)
    {
      // work with the model
    }
    // At this point, something failed, redisplay form
    return View(model);
}
```

The framework handles validating request data both on the client and on the server. Validation logic specified on model types is added to the rendered views as unobtrusive annotations and is enforced in the browser with [jQuery Validation](https://github.com/jquery-validation/jquery-validation).

## Dependency injection

ASP.NET Core has built-in support for [dependency injection (DI)](../fundamentals/dependency-injection?view=aspnetcore-10.0). In ASP.NET Core MVC, [controllers](controllers/dependency-injection?view=aspnetcore-10.0) can request needed services through their constructors, allowing them to follow the [Explicit Dependencies Principle](/en-us/dotnet/architecture/modern-web-apps-azure/architectural-principles#explicit-dependencies).

Your app can also use [dependency injection in view files](views/dependency-injection?view=aspnetcore-10.0), using the `@inject` directive:

```
@inject SomeService ServiceName

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ServiceName.GetTitle</title>
</head>
<body>
    <h1>@ServiceName.GetTitle</h1>
</body>
</html>
```

## Filters

[Filters](controllers/filters?view=aspnetcore-10.0) help developers encapsulate cross-cutting concerns, like exception handling or authorization. Filters enable running custom pre- and post-processing logic for action methods, and can be configured to run at certain points within the execution pipeline for a given request. Filters can be applied to controllers or actions as attributes (or can be run globally). Several filters (such as `Authorize`) are included in the framework. `[Authorize]` is the attribute that is used to create MVC authorization filters.

```
[Authorize]
public class AccountController : Controller
```

## Areas

[Areas](controllers/areas?view=aspnetcore-10.0) provide a way to partition a large ASP.NET Core MVC Web app into smaller functional groupings. An area is an MVC structure inside an application. In an MVC project, logical components like Model, Controller, and View are kept in different folders, and MVC uses naming conventions to create the relationship between these components. For a large app, it might be advantageous to partition the app into separate high level areas of functionality. For instance, an e-commerce app with multiple business units, such as checkout, billing, and search etc. Each of these units have their own logical component views, controllers, and models.

## Web APIs

In addition to being a great platform for building web sites, ASP.NET Core MVC has great support for building Web APIs. You can build services that reach a broad range of clients including browsers and mobile devices.

The framework includes support for HTTP content-negotiation with built-in support to [format data](../web-api/advanced/formatting?view=aspnetcore-10.0) as JSON or XML. Write [custom formatters](../web-api/advanced/custom-formatters?view=aspnetcore-10.0) to add support for your own formats.

Use link generation to enable support for hypermedia. Easily enable support for [Cross-Origin Resource Sharing (CORS)](https://fetch.spec.whatwg.org/#http-cors-protocol) so that your Web APIs can be shared across multiple Web applications.

## Testability

The framework's use of interfaces and dependency injection make it well-suited to unit testing, and the framework includes features (like a TestHost and InMemory provider for Entity Framework) that make [integration tests](../test/integration-tests?view=aspnetcore-10.0) quick and easy as well. Learn more about [how to test controller logic](controllers/testing?view=aspnetcore-10.0).

## Razor view engine

[ASP.NET Core MVC views](views/overview?view=aspnetcore-10.0) use the [Razor view engine](views/razor?view=aspnetcore-10.0) to render views. Razor is a compact, expressive, and fluid template markup language for defining views using embedded C# code. Razor is used to dynamically generate web content on the server. You can cleanly mix server code with client side content and code.

```
<ul>
    @for (int i = 0; i < 5; i++) {
        <li>List item @i</li>
    }
</ul>
```

Using the Razor view engine you can define [layouts](views/layout?view=aspnetcore-10.0), [partial views](views/partial?view=aspnetcore-10.0) and replaceable sections.

## Strongly typed views

Razor views in MVC can be strongly typed based on your model. Controllers can pass a strongly typed model to views enabling your views to have type checking and IntelliSense support.

For example, the following view renders a model of type `IEnumerable<Product>`:

```
@model IEnumerable<Product>
<ul>
    @foreach (Product p in Model)
    {
        <li>@p.Name</li>
    }
</ul>
```

## Tag Helpers

[Tag Helpers](views/tag-helpers/intro?view=aspnetcore-10.0) enable server side code to participate in creating and rendering HTML elements in Razor files. You can use tag helpers to define custom tags (for example, `<environment>`) or to modify the behavior of existing tags (for example, `<label>`). Tag Helpers bind to specific elements based on the element name and its attributes. They provide the benefits of server-side rendering while still preserving an HTML editing experience.

There are many built-in Tag Helpers for common tasks - such as creating forms, links, loading assets and more - and even more available in public GitHub repositories and as NuGet packages. Tag Helpers are authored in C#, and they target HTML elements based on element name, attribute name, or parent tag. For example, the built-in LinkTagHelper can be used to create a link to the `Login` action of the `AccountsController`:

```
<p>
    Thank you for confirming your email.
    Please <a asp-controller="Account" asp-action="Login">Click here to Log in</a>.
</p>
```

The `EnvironmentTagHelper` can be used to include different scripts in your views (for example, raw or minified) based on the runtime environment, such as Development, Staging, or Production:

```
<environment names="Development">
    <script src="~/lib/jquery/dist/jquery.js"></script>
</environment>
<environment names="Staging,Production">
    <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.js"
            asp-fallback-src="~/lib/jquery/dist/jquery.js"
            asp-fallback-test="window.jQuery">
    </script>
</environment>
```

Tag Helpers provide an HTML-friendly development experience and a rich IntelliSense environment for creating HTML and Razor markup. Most of the built-in Tag Helpers target existing HTML elements and provide server-side attributes for the element.

## View Components

[View Components](views/view-components?view=aspnetcore-10.0) allow you to package rendering logic and reuse it throughout the application. They're similar to [partial views](views/partial?view=aspnetcore-10.0), but with associated logic.

## Compatibility version

The `SetCompatibilityVersion` method allowed an app to opt in or opt out of potentially breaking behavior changes introduced in ASP.NET Core MVC 2.1 or later. The method is a no-op for ASP.NET Core 3.0 and later apps, and it's removed in ASP.NET Core 11. For more information, see [Compatibility version for ASP.NET Core MVC](compatibility-version?view=aspnetcore-10.0) and [MVC compatibility options removed](/en-us/aspnet/core/breaking-changes/11/mvc-compatibility-options-removed).

## Related content

* [MyTested.AspNetCore.Mvc - Fluent Testing Library for ASP.NET Core MVC](https://github.com/ivaylokenov/MyTested.AspNetCore.Mvc): Strongly-typed unit testing library, providing a fluent interface for testing MVC and web API apps. (*Not maintained or supported by Microsoft.*)
* [Dependency injection in ASP.NET Core](../fundamentals/dependency-injection?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization-extensibility
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/localization-extensibility.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Localization Extensibility

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Hisham Bin Ateya](https://github.com/hishamco)

This article:

* Lists the extensibility points on the localization APIs.
* Provides instructions on how to extend ASP.NET Core app localization.

## Extensible Points in Localization APIs

ASP.NET Core localization APIs are built to be extensible. Extensibility allows developers to customize the localization according to their needs. For instance, [OrchardCore](https://github.com/orchardCMS/OrchardCore/) has a `POStringLocalizer`. `POStringLocalizer` describes in detail using [Portable Object localization](portable-object-localization?view=aspnetcore-10.0) to use `PO` files to store localization resources.

This article lists the two main extensibility points that localization APIs provide:

* [RequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.requestcultureprovider)
* [IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer)

## Localization Culture Providers

ASP.NET Core localization APIs have four default providers that can determine the current culture of an executing request:

* [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider)
* [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider)
* [AcceptLanguageHeaderRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.acceptlanguageheaderrequestcultureprovider)
* [CustomRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.customrequestcultureprovider)

The preceding providers are described in detail in the [localization middleware](localization?view=aspnetcore-10.0) documentation. If the default providers don't meet your needs, build a custom provider using one of the following approaches:

### Use CustomRequestCultureProvider

[CustomRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.customrequestcultureprovider) provides a custom [RequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.requestcultureprovider) that uses a simple delegate to determine the current localization culture:

```
options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context =>
{
    var currentCulture = "en";
    var segments = context.Request.Path.Value.Split(new char[] { '/' }, 
        StringSplitOptions.RemoveEmptyEntries);

    if (segments.Length > 1 && segments[0].Length == 2)
    {
        currentCulture = segments[0];
    }

    var requestCulture = new ProviderCultureResult(currentCulture);

    return Task.FromResult(requestCulture);
}));
```

```
options.RequestCultureProviders.Insert(0, new CustomRequestCultureProvider(async context =>
{
    var currentCulture = "en";
    var segments = context.Request.Path.Value.Split(new char[] { '/' }, 
        StringSplitOptions.RemoveEmptyEntries);

    if (segments.Length > 1 && segments[0].Length == 2)
    {
        currentCulture = segments[0];
    }

    var requestCulture = new ProviderCultureResult(currentCulture);

    return Task.FromResult(requestCulture);
}));
```

### Use a new implementation of RequestCultureProvider

A new implementation of [RequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.requestcultureprovider) can be created that determines the request culture information from a custom source. For example, the custom source can be a configuration file or database.

The following example shows `AppSettingsRequestCultureProvider`, which extends the [RequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.requestcultureprovider) to determine the request culture information from `appsettings.json`:

```
public class AppSettingsRequestCultureProvider : RequestCultureProvider
{
    public string CultureKey { get; set; } = "culture";

    public string UICultureKey { get; set; } = "ui-culture";

    public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException();
        }

        var configuration = httpContext.RequestServices.GetService<IConfigurationRoot>();
        var culture = configuration[CultureKey];
        var uiCulture = configuration[UICultureKey];

        if (culture == null && uiCulture == null)
        {
            return Task.FromResult((ProviderCultureResult)null);
        }

        if (culture != null && uiCulture == null)
        {
            uiCulture = culture;
        }

        if (culture == null && uiCulture != null)
        {
            culture = uiCulture;
        }
        
        var providerResultCulture = new ProviderCultureResult(culture, uiCulture);

        return Task.FromResult(providerResultCulture);
    }
}
```

## Localization resources

ASP.NET Core localization provides [ResourceManagerStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.resourcemanagerstringlocalizer). [ResourceManagerStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.resourcemanagerstringlocalizer) is an implementation of [IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer) that uses `resx` to store localization resources.

You aren't limited to using `resx` files. By implementing `IStringLocalizer`, any data source can be used.

The following example projects implement [IStringLocalizer](/en-us/dotnet/api/microsoft.extensions.localization.istringlocalizer):

* [EFStringLocalizer](https://github.com/aspnet/Entropy/tree/master/samples/Localization.EntityFramework)
* [JsonStringLocalizer](https://github.com/hishamco/My.Extensions.Localization.Json)
* [SqlLocalizer](https://github.com/damienbod/AspNetCoreLocalization)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/model
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/razor-pages/model.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 2, add a model to a Razor Pages app in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

In this tutorial, you add classes for managing movies in a database. The app's model classes use [Entity Framework Core (EF Core)](/en-us/ef/core) to work with the database. EF Core is an object-relational mapper (O/RM) that simplifies data access. You write the model classes first, and EF Core creates the database.

The model classes are known as POCO classes (from "**P**lain-**O**ld **C**LR **O**bjects") because they don't have a dependency on EF Core. They define the properties of the data that are stored in the database.

## Add a data model

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

1. In **Solution Explorer**, right-click the *RazorPagesMovie* project. Select **Add** > **New Folder**. Name the folder `Models`.

2. Right-click the `Models` folder. Select **Add** > **Class**. Name the class *Movie*.

3. Add the following properties to the `Movie` class:

   ```
   using System.ComponentModel.DataAnnotations;

   namespace RazorPagesMovie.Models;

   public class Movie
   {
       public int Id { get; set; }
       public string? Title { get; set; }
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string? Genre { get; set; }
       public decimal Price { get; set; }
   }
   ```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute that specifies the type of data in the `ReleaseDate` property. By using this attribute:

  * The user isn't required to enter time information in the date field.
  * Only the date is displayed, without any time information.

* A question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

Build the project to verify there are no compilation errors.

1. Add a folder named `Models`.
2. Add a class to the `Models` folder named `Movie.cs`.

Add the following properties to the `Movie` class:

```
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}
```

The `Movie` class contains:

* An `ID` field that acts as the primary key for the database.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute that specifies the type of data in the `ReleaseDate` field. By using this attribute:

  * The user doesn't need to enter time information in the date field.
  * Only the date is displayed, without any time information.

* A question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

### Add NuGet packages and EF tools

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
```

In Visual Studio Code, press `Ctrl`+`F5` to run the app without debugging.

In the *Panel* below the editor region, select the *PROBLEMS* tab, or from the *View* menu, select *Problems* if it's not currently in view. Verify there are no compilation errors.

## Scaffold the movie model

In this section, you scaffold the movie model. The scaffolding tool generates pages for Create, Read, Update, and Delete (CRUD) operations for the movie model.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

1. Right-click on the **Pages** folder. Select **Add** > **New Scaffolded Item**.

1) In the **Add New Scaffold** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Add**.

1. Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

   1. In the **Model class** drop down, select **Movie (RazorPagesMovie.Models)**.

   2. In the **Data context class** row, select the **+** (plus) sign.

      1. In the **Add Data Context** dialog, the class name `RazorPagesMovie.Data.RazorPagesMovieContext` is generated.
      2. In the **Database provider** drop down, select **SQL Server**.

   3. Select **Add**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

* Open a command shell to the project directory, which contains the `Program.cs` and `.csproj` files. Run the following command:

  ```
  dotnet aspnet-codegenerator razorpage -m Movie -dc RazorPagesMovie.Data.RazorPagesMovieContext -udl -outDir Pages/MoviePages --referenceScriptLibraries --databaseProvider sqlite
  ```

The following table details the ASP.NET Core code generator options.

| Option                       | Description                                               |
| ---------------------------- | --------------------------------------------------------- |
| `-m`                         | The name of the model.                                    |
| `-dc`                        | The `DbContext` class to use including namespace.         |
| `-udl`                       | Use the default layout.                                   |
| `-outDir`                    | The relative output folder path to create the views.      |
| `--referenceScriptLibraries` | Adds `_ValidationScriptsPartial` to Edit and Create pages |

Use the `-h` option to get help on the `dotnet aspnet-codegenerator razorpage` command:

```
dotnet aspnet-codegenerator razorpage -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to select the SQLite connection string in development and SQL Server in production.

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext")));
}
else
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMovieContext")));
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code doesn't call `UseDeveloperExceptionPage` in development because `WebApplication` calls `UseDeveloperExceptionPage` in development mode.

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

### Files created and updated

The scaffold process creates the following files:

* *Pages/MoviePages*: Create, Delete, Details, Edit, and Index.
* `Data/RazorPagesMovieContext.cs`

The next tutorial explains the created files.

The scaffold process adds the following highlighted code to the `Program.cs` file:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages()
   .WithStaticAssets();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages()
   .WithStaticAssets();
   
app.Run();
```

Later in this tutorial, you learn about the `Program.cs` changes.

## Create the initial database schema by using EF's migration feature

The migrations feature in Entity Framework Core provides a way to:

* Create the initial database schema.
* Incrementally update the database schema to keep it in sync with the app's data model. This approach preserves existing data in the database.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)

In this section, use the **Package Manager Console** (PMC) window to:

* Add an initial migration.

* Update the database with the initial migration.

* From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console**.

* In the PMC, enter the following command:

  ```
  Add-Migration InitialCreate
  ```

* The `Add-Migration` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument names the migration. You can use any name, but by convention choose a name that describes the migration.

The following warning is displayed, which you address in a later step:

> No store type was specified for the decimal column 'Price' on entity type 'Movie'. This condition causes values to be silently truncated if they don't fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating' by using 'HasColumnType', specify precision and scale using 'HasPrecision', or configure a value converter using 'HasConversion'.

* In the PMC, enter the following command:

  ```
  Update-Database
  ```

  The `Update-Database` command runs the `Up` method in migrations that aren't applied. In this case, the command runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

- Right-click the *RazorPagesMovie.csproj* project, and then select **Open in Integrated Terminal**.

  The **Terminal** window opens with the command prompt at the project directory, which contains the Program.cs and .csproj files.

- Run the following .NET CLI command:

  ```
  dotnet ef migrations add InitialCreate
  ```

  The `migrations` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migrations. You can use any name, but by convention, choose a name that describes the migration.

- Run the following .NET CLI command:

  ```
  dotnet ef database update
  ```

  The `update` command runs the `Up` method in migrations that aren't applied. In this case, `update` runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

Note

For SQLite, the column type for the `Price` field is set to `TEXT`. This issue is resolved in a later step.

The data context `RazorPagesMovieContext`:

* Derives from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext).
* Specifies which entities are included in the data model.
* Coordinates EF Core functionality, such as Create, Read, Update, and Delete, for the `Movie` model.

The `RazorPagesMovieContext` class in the generated file `Data/RazorPagesMovieContext.cs`:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Data
{
    public class RazorPagesMovieContext : DbContext
    {
        public RazorPagesMovieContext (DbContextOptions<RazorPagesMovieContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; } = default!;
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

The name of the connection string is passed to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [Configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

## Test the app

1. Run the app and append `/MoviePages` to the URL in the browser (`http://localhost:{PORT}/MoviePages`, where the `{PORT}` placeholder is the port).

   If you receive the following error:

   ```
   SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
   Login failed for user 'User-name'.
   ```

   You missed the [migrations step](#create-the-initial-database-schema-by-using-efs-migration-feature).

2. Test the **Create New** link.

Note

You might not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

1. Test the **Edit**, **Details**, and **Delete** links.

The next tutorial explains the files created by scaffolding.

### Examine the context registered with dependency injection

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Register services, such as the EF Core database context, with dependency injection during application startup. Provide components that require these services (such as Razor Pages) via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically creates a database context and registers it with the dependency injection container. The following highlighted code is added to the `Program.cs` file by the scaffolder:

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages()
   .WithStaticAssets();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages()
   .WithStaticAssets();
   
app.Run();
```

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie10) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Previous: Get Started](razor-pages-start?view=aspnetcore-10.0) [Next: Scaffolded Razor Pages](page?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. The app's model classes use [Entity Framework Core (EF Core)](/en-us/ef/core) to work with the database. EF Core is an object-relational mapper (O/RM) that simplifies data access. You write the model classes first, and EF Core creates the database.

The model classes are known as POCO classes (from "**P**lain-**O**ld **C**LR **O**bjects") because they don't have a dependency on EF Core. They define the properties of the data that are stored in the database.

## Add a data model

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

1. In **Solution Explorer**, right-click the *RazorPagesMovie* project > **Add** > **New Folder**. Name the folder `Models`.

2. Right-click the `Models` folder. Select **Add** > **Class**. Name the class *Movie*.

3. Add the following properties to the `Movie` class:

   ```
   using System.ComponentModel.DataAnnotations;

   namespace RazorPagesMovie.Models;

   public class Movie
   {
       public int Id { get; set; }
       public string? Title { get; set; }
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string? Genre { get; set; }
       public decimal Price { get; set; }
   }
   ```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute that specifies the type of data in the `ReleaseDate` property. With this attribute:

  * The user isn't required to enter time information in the date field.
  * Only the date is displayed, not time information.

* The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

Build the project to verify there are no compilation errors.

1. Add a folder named `Models`.
2. Add a class to the `Models` folder named `Movie.cs`.

Add the following properties to the `Movie` class:

```
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}
```

The `Movie` class contains:

* An `ID` field to provide a primary key for the database.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute to specify the type of data in the `ReleaseDate` field. With this attribute:

  * The user is not required to enter time information in the date field.
  * Only the date is displayed, not time information.

* The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

[]()

### Add NuGet packages and EF tools

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
```

## Scaffold the movie model

In this section, the movie model is scaffolded. That is, the scaffolding tool produces pages for Create, Read, Update, and Delete (CRUD) operations for the movie model.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

1. Create the *Pages/Movies* folder:

   1. Right-click on the *Pages* folder > **Add** > **New Folder**.
   2. Name the folder *Movies*.

2. Right-click on the *Pages/Movies* folder > **Add** > **New Scaffolded Item**.

3. In the **Add New Scaffold** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Add**.

4. Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

   1. In the **Model class** drop down, select **Movie (RazorPagesMovie.Models)**.

   2. In the **Data context class** row, select the **+** (plus) sign.

      1. In the **Add Data Context** dialog, the class name `RazorPagesMovie.Data.RazorPagesMovieContext` is generated.
      2. In the **Database provider** drop down, select **SQL Server**.

   3. Select **Add**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

* Open a command shell to the project directory, which contains the `Program.cs` and `.csproj` files. Run the following command:

  ```
  dotnet aspnet-codegenerator razorpage -m Movie -dc RazorPagesMovie.Data.RazorPagesMovieContext -udl -outDir Pages/Movies --referenceScriptLibraries --databaseProvider sqlite
  ```

[]()The following table details the ASP.NET Core code generator options.

| Option                       | Description                                               |
| ---------------------------- | --------------------------------------------------------- |
| `-m`                         | The name of the model.                                    |
| `-dc`                        | The `DbContext` class to use including namespace.         |
| `-udl`                       | Use the default layout.                                   |
| `-outDir`                    | The relative output folder path to create the views.      |
| `--referenceScriptLibraries` | Adds `_ValidationScriptsPartial` to Edit and Create pages |

Use the `-h` option to get help on the `dotnet aspnet-codegenerator razorpage` command:

```
dotnet aspnet-codegenerator razorpage -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to select the SQLite connection string in development and SQL Server in production.

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext")));
}
else
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMovieContext")));
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code doesn't call `UseDeveloperExceptionPage` in development because `WebApplication` calls `UseDeveloperExceptionPage` in development mode.

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

### Files created and updated

The scaffold process creates the following files:

* *Pages/Movies*: Create, Delete, Details, Edit, and Index.
* `Data/RazorPagesMovieContext.cs`

The created files are explained in the next tutorial.

The scaffold process adds the following highlighted code to the `Program.cs` file:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages();

app.Run();
```

The `Program.cs` changes are explained later in this tutorial.

[]()

## Create the initial database schema using EF's migration feature

The migrations feature in Entity Framework Core provides a way to:

* Create the initial database schema.
* Incrementally update the database schema to keep it in sync with the app's data model. Existing data in the database is preserved.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)

In this section, the **Package Manager Console** (PMC) window is used to:

* Add an initial migration.

* Update the database with the initial migration.

* From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console**.

* In the PMC, enter the following command:

  ```
  Add-Migration InitialCreate
  ```

* The `Add-Migration` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migration. Any name can be used, but by convention a name is selected that describes the migration.

The following warning is displayed, which is addressed in a later step:

> No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

* In the PMC, enter the following command:

  ```
  Update-Database
  ```

  The `Update-Database` command runs the `Up` method in migrations that have not been applied. In this case, the command runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

- Right-click the *RazorPagesMovie.csproj* project, and then select **Open in Integrated Terminal**.

  The **Terminal** window opens with the command prompt at the project directory, which contains the Program.cs and .csproj files.

- Run the following .NET CLI command:

  ```
  dotnet ef migrations add InitialCreate
  ```

  The `migrations` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migrations. Any name can be used, but by convention a name is selected that describes the migration.

- Run the following .NET CLI command:

  ```
  dotnet ef database update
  ```

  The `update` command runs the `Up` method in migrations that have not been applied. In this case, `update` runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

Note

For SQLite, column type for the `Price` field is set to `TEXT`. This is resolved in a later step.

The data context `RazorPagesMovieContext`:

* Derives from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext).
* Specifies which entities are included in the data model.
* Coordinates EF Core functionality, such as Create, Read, Update and Delete, for the `Movie` model.

The `RazorPagesMovieContext` class in the generated file `Data/RazorPagesMovieContext.cs`:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Data
{
    public class RazorPagesMovieContext : DbContext
    {
        public RazorPagesMovieContext (DbContextOptions<RazorPagesMovieContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; } = default!;
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [Configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

[]()

## Test the app

1. Run the app and append `/Movies` to the URL in the browser (`http://localhost:port/movies`).

   If you receive the following error:

   ```
   SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
   Login failed for user 'User-name'.
   ```

   You missed the [migrations step](#pmc6).

2. Test the **Create New** link.

   Note

   You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

3. Test the **Edit**, **Details**, and **Delete** links.

The next tutorial explains the files created by scaffolding.

### Examine the context registered with dependency injection

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the EF Core database context, are registered with dependency injection during application startup. Components that require these services (such as Razor Pages) are provided via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically created a database context and registered it with the dependency injection container. The following highlighted code is added to the `Program.cs` file by the scaffolder:

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages();

app.Run();
```

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie90) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Previous: Get Started](razor-pages-start?view=aspnetcore-10.0) [Next: Scaffolded Razor Pages](page?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. The app's model classes use [Entity Framework Core (EF Core)](/en-us/ef/core) to work with the database. EF Core is an object-relational mapper (O/RM) that simplifies data access. You write the model classes first, and EF Core creates the database.

The model classes are known as POCO classes (from "**P**lain-**O**ld **C**LR **O**bjects") because they don't have a dependency on EF Core. They define the properties of the data that are stored in the database.

## Add a data model

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

1. In **Solution Explorer**, right-click the *RazorPagesMovie* project > **Add** > **New Folder**. Name the folder `Models`.

2. Right-click the `Models` folder. Select **Add** > **Class**. Name the class *Movie*.

3. Add the following properties to the `Movie` class:

   ```
   using System.ComponentModel.DataAnnotations;

   namespace RazorPagesMovie.Models;

   public class Movie
   {
       public int Id { get; set; }
       public string? Title { get; set; }
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string? Genre { get; set; }
       public decimal Price { get; set; }
   }
   ```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute that specifies the type of data in the `ReleaseDate` property. With this attribute:

  * The user isn't required to enter time information in the date field.
  * Only the date is displayed, not time information.

* The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

Build the project to verify there are no compilation errors.

1. Add a folder named `Models`.
2. Add a class to the `Models` folder named `Movie.cs`.

Add the following properties to the `Movie` class:

```
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}
```

The `Movie` class contains:

* An `ID` field to provide a primary key for the database.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute to specify the type of data in the `ReleaseDate` field. With this attribute:

  * The user is not required to enter time information in the date field.
  * Only the date is displayed, not time information.

* The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

[]()

### Add NuGet packages and EF tools

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
```

## Scaffold the movie model

In this section, the movie model is scaffolded. That is, the scaffolding tool produces pages for Create, Read, Update, and Delete (CRUD) operations for the movie model.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)

1. Create the *Pages/Movies* folder:

   1. Right-click on the *Pages* folder > **Add** > **New Folder**.
   2. Name the folder *Movies*.

2. Right-click on the *Pages/Movies* folder > **Add** > **New Scaffolded Item**.

3. In the **Add New Scaffold** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Add**.

4. Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

   1. In the **Model class** drop down, select **Movie (RazorPagesMovie.Models)**.

   2. In the **Data context class** row, select the **+** (plus) sign.

      1. In the **Add Data Context** dialog, the class name `RazorPagesMovie.Data.RazorPagesMovieContext` is generated.
      2. In the **Database provider** drop down, select **SQL Server**.

   3. Select **Add**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

* Open a command shell to the project directory, which contains the `Program.cs` and `.csproj` files. Run the following command:

  ```
  dotnet aspnet-codegenerator razorpage -m Movie -dc RazorPagesMovie.Data.RazorPagesMovieContext -udl -outDir Pages/Movies --referenceScriptLibraries --databaseProvider sqlite
  ```

[]()The following table details the ASP.NET Core code generator options.

| Option                       | Description                                               |
| ---------------------------- | --------------------------------------------------------- |
| `-m`                         | The name of the model.                                    |
| `-dc`                        | The `DbContext` class to use including namespace.         |
| `-udl`                       | Use the default layout.                                   |
| `-outDir`                    | The relative output folder path to create the views.      |
| `--referenceScriptLibraries` | Adds `_ValidationScriptsPartial` to Edit and Create pages |

Use the `-h` option to get help on the `dotnet aspnet-codegenerator razorpage` command:

```
dotnet aspnet-codegenerator razorpage -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to select the SQLite connection string in development and SQL Server in production.

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext")));
}
else
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMovieContext")));
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code doesn't call `UseDeveloperExceptionPage` in development because `WebApplication` calls `UseDeveloperExceptionPage` in development mode.

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

### Files created and updated

The scaffold process creates the following files:

* *Pages/Movies*: Create, Delete, Details, Edit, and Index.
* `Data/RazorPagesMovieContext.cs`

The created files are explained in the next tutorial.

The scaffold process adds the following highlighted code to the `Program.cs` file:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The `Program.cs` changes are explained later in this tutorial.

[]()

## Create the initial database schema using EF's migration feature

The migrations feature in Entity Framework Core provides a way to:

* Create the initial database schema.
* Incrementally update the database schema to keep it in sync with the app's data model. Existing data in the database is preserved.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)

In this section, the **Package Manager Console** (PMC) window is used to:

* Add an initial migration.

* Update the database with the initial migration.

* From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console**.

* In the PMC, enter the following command:

  ```
  Add-Migration InitialCreate
  ```

* The `Add-Migration` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migration. Any name can be used, but by convention a name is selected that describes the migration.

The following warning is displayed, which is addressed in a later step:

> No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

* In the PMC, enter the following command:

  ```
  Update-Database
  ```

  The `Update-Database` command runs the `Up` method in migrations that have not been applied. In this case, the command runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

- Right-click the *RazorPagesMovie.csproj* project, and then select **Open in Integrated Terminal**.

  The **Terminal** window opens with the command prompt at the project directory, which contains the Program.cs and .csproj files.

- Run the following .NET CLI command:

  ```
  dotnet ef migrations add InitialCreate
  ```

  The `migrations` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migrations. Any name can be used, but by convention a name is selected that describes the migration.

- Run the following .NET CLI command:

  ```
  dotnet ef database update
  ```

The `update` command runs the `Up` method in migrations that have not been applied. In this case, `update` runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

Note

For SQLite, column type for the `Price` field is set to `TEXT`. This is resolved in a later step.

The data context `RazorPagesMovieContext`:

* Derives from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext).
* Specifies which entities are included in the data model.
* Coordinates EF Core functionality, such as Create, Read, Update and Delete, for the `Movie` model.

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Data
{
    public class RazorPagesMovieContext : DbContext
    {
        public RazorPagesMovieContext (DbContextOptions<RazorPagesMovieContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; } = default!;
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [Configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

[]()

## Test the app

1. Run the app and append `/Movies` to the URL in the browser (`http://localhost:port/movies`).

   If you receive the following error:

   ```
   SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
   Login failed for user 'User-name'.
   ```

   You missed the [migrations step](#pmc6).

2. Test the **Create New** link.

   Note

   You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

3. Test the **Edit**, **Details**, and **Delete** links.

The next tutorial explains the files created by scaffolding.

### Examine the context registered with dependency injection

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the EF Core database context, are registered with dependency injection during application startup. Components that require these services (such as Razor Pages) are provided via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically created a database context and registered it with the dependency injection container. The following highlighted code is added to the `Program.cs` file by the scaffolder:

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code](#tabpanel_5_visual-studio-code)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie80) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Previous: Get Started](razor-pages-start?view=aspnetcore-10.0) [Next: Scaffolded Razor Pages](page?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. The app's model classes use [Entity Framework Core (EF Core)](/en-us/ef/core) to work with the database. EF Core is an object-relational mapper (O/RM) that simplifies data access. You write the model classes first, and EF Core creates the database.

The model classes are known as POCO classes (from "**P**lain-**O**ld **C**LR **O**bjects") because they don't have a dependency on EF Core. They define the properties of the data that are stored in the database.

## Add a data model

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

1. In **Solution Explorer**, right-click the *RazorPagesMovie* project > **Add** > **New Folder**. Name the folder `Models`.

2. Right-click the `Models` folder. Select **Add** > **Class**. Name the class *Movie*.

3. Add the following properties to the `Movie` class:

   ```
   using System.ComponentModel.DataAnnotations;

   namespace RazorPagesMovie.Models;

   public class Movie
   {
       public int Id { get; set; }
       public string? Title { get; set; }
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string? Genre { get; set; }
       public decimal Price { get; set; }
   }
   ```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute that specifies the type of data in the `ReleaseDate` property. With this attribute:

  * The user isn't required to enter time information in the date field.
  * Only the date is displayed, not time information.

* The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

Build the project to verify there are no compilation errors.

1. Add a folder named `Models`.
2. Add a class to the `Models` folder named `Movie.cs`.

Add the following properties to the `Movie` class:

```
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}
```

The `Movie` class contains:

* An `ID` field to provide a primary key for the database.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute to specify the type of data in the `ReleaseDate` field. With this attribute:

  * The user is not required to enter time information in the date field.
  * Only the date is displayed, not time information.

* The question mark after `string` indicates that the property is nullable. For more information, see [Nullable reference types](/en-us/dotnet/csharp/nullable-references).

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

[]()

### Add NuGet packages and EF tools

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
```

1. In the **Solution Tool Window**, control-click the *RazorPagesMovie* project, and then select **Add** > **New Folder...**. Name the folder `Models`.

2. Control-click the `Models` folder, and then select **Add** > **New Class...**.

3. In the **New File** dialog:

   1. Select **General** in the left pane.
   2. Select **Empty Class** in the center pane.
   3. Name the class **Movie** and select **Create**.

4. Add the following properties to the `Movie` class:

   ```
   using System.ComponentModel.DataAnnotations;

   namespace RazorPagesMovie.Models;

   public class Movie
   {
       public int Id { get; set; }
       public string? Title { get; set; }
       [DataType(DataType.Date)]
       public DateTime ReleaseDate { get; set; }
       public string? Genre { get; set; }
       public decimal Price { get; set; }
   }
   ```

The `Movie` class contains:

* An `ID` field to provide a primary key for the database.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute to specify the type of data in the `ReleaseDate` field. With this attribute:

  * The user isn't required to enter time information in the date field.
  * Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

Build the project to verify there are no compilation errors.

## Scaffold the movie model

In this section, the movie model is scaffolded. That is, the scaffolding tool produces pages for Create, Read, Update, and Delete (CRUD) operations for the movie model.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

1. Create the *Pages/Movies* folder:

   1. Right-click on the *Pages* folder > **Add** > **New Folder**.
   2. Name the folder *Movies*.

2. Right-click on the *Pages/Movies* folder > **Add** > **New Scaffolded Item**.

3. In the **Add New Scaffold** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Add**.

4. Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

   1. In the **Model class** drop down, select **Movie (RazorPagesMovie.Models)**.

   2. In the **Data context class** row, select the **+** (plus) sign.

      1. In the **Add Data Context** dialog, the class name `RazorPagesMovie.Data.RazorPagesMovieContext` is generated.
      2. In the **Database provider** drop down, select **SQL Server**.

   3. Select **Add**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

* Open a command shell to the project directory, which contains the `Program.cs` and `.csproj` files. Run the following command:

  ```
  dotnet aspnet-codegenerator razorpage -m Movie -dc RazorPagesMovie.Data.RazorPagesMovieContext -udl -outDir Pages/Movies --referenceScriptLibraries --databaseProvider sqlite
  ```

[]()The following table details the ASP.NET Core code generator options.

| Option                       | Description                                               |
| ---------------------------- | --------------------------------------------------------- |
| `-m`                         | The name of the model.                                    |
| `-dc`                        | The `DbContext` class to use including namespace.         |
| `-udl`                       | Use the default layout.                                   |
| `-outDir`                    | The relative output folder path to create the views.      |
| `--referenceScriptLibraries` | Adds `_ValidationScriptsPartial` to Edit and Create pages |

Use the `-h` option to get help on the `dotnet aspnet-codegenerator razorpage` command:

```
dotnet aspnet-codegenerator razorpage -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to select the SQLite connection string in development and SQL Server in production.

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext")));
}
else
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMovieContext")));
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code doesn't call `UseDeveloperExceptionPage` in development because `WebApplication` calls `UseDeveloperExceptionPage` in development mode.

1. Add the NuGet package `Microsoft.EntityFrameworkCore.Sqlite`, which is required for the scaffolding tool.

   1. In the **Solution Tool Window**, control-click the *RazorPagesMovie* project, and then select **Open in Terminal**.

   The Terminal window opens with the command prompt at the project directory, which contains the Program.cs and .csproj files.

   1. Run the following .NET CLI command:

   ```
     dotnet add package Microsoft.EntityFrameworkCore.SQLite
   ```

2. Create a *Pages/Movies* folder:

   1. Control-click on the *Pages* folder > **Add** > **New Folder**.
   2. Name the folder *Movies*.

3. Control-click on the *Pages/Movies* folder > **Add** > **New Scaffolding...**.

4. In the **New Scaffolding** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Next**.

5. Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

   1. In the **Model class to use:** row, enter `Movie`.
   2. In the **DbContext Class to use:** row, name the class `RazorPagesMovie.Data.RazorPagesMovieContext`.
   3. Select **Finish**.

The scaffolding process may take some time to complete as required packages are automatically downloaded and added to the project.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to select the SQLite connection string in development and SQL Server in production.

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext")));
}
else
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMovieContext")));
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code doesn't call `UseDeveloperExceptionPage` in development because `WebApplication` calls `UseDeveloperExceptionPage` in development mode.

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

### Files created and updated

The scaffold process creates the following files:

* *Pages/Movies*: Create, Delete, Details, Edit, and Index.
* `Data/RazorPagesMovieContext.cs`

The created files are explained in the next tutorial.

The scaffold process adds the following highlighted code to the `Program.cs` file:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The `Program.cs` changes are explained later in this tutorial.

[]()

## Create the initial database schema using EF's migration feature

The migrations feature in Entity Framework Core provides a way to:

* Create the initial database schema.
* Incrementally update the database schema to keep it in sync with the app's data model. Existing data in the database is preserved.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code](#tabpanel_4_visual-studio-code)
- [Visual Studio for Mac](#tabpanel_4_visual-studio-mac)

In this section, the **Package Manager Console** (PMC) window is used to:

* Add an initial migration.
* Update the database with the initial migration.

1. From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console**.

2. In the PMC, enter the following commands:

   ```
   Add-Migration InitialCreate
   Update-Database
   ```

* The `Add-Migration` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migration. Any name can be used, but by convention a name is selected that describes the migration.

* The `Update-Database` command runs the `Up` method in migrations that have not been applied. In this case, the command runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

- Right-click the *RazorPagesMovie.csproj* project, and then select **Open in Integrated Terminal**.

  The **Terminal** window opens with the command prompt at the project directory, which contains the Program.cs and .csproj files.

- Run the following .NET CLI commands:

  ```
  dotnet ef migrations add InitialCreate
  dotnet ef database update
  ```

- The `migrations` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migrations. Any name can be used, but by convention a name is selected that describes the migration.

- The `update` command runs the `Up` method in migrations that have not been applied. In this case, `update` runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

Note

For SQLite, column type for the `Price` field is set to `TEXT`. This is resolved in a later step.

* Control-click the *RazorPagesMovie* project, and then select **Open in Terminal**.

  The **Terminal** window opens with the command prompt at the project directory, which contains the Program.cs and .csproj files.

* Run the following .NET CLI commands:

  ```
  dotnet tool uninstall --global dotnet-ef
  dotnet tool install --global dotnet-ef
  dotnet ef migrations add InitialCreate
  dotnet ef database update
  ```

* The `tool` commands install the latest [Entity Framework Core tools](/en-us/ef/core/get-started/overview/install#get-the-entity-framework-core-tools) after uninstalling any previous version, if one exists.

  Note

  By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

* The `migrations` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migrations. Any name can be used, but by convention a name is selected that describes the migration.

* The `update` command runs the `Up` method in migrations that have not been applied. In this case, `update` runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

Note

For SQLite, column type for the `Price` field is set to `TEXT`. This is resolved in a later step.

The following warning is displayed, which is addressed in a later step:

> No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

The data context `RazorPagesMovieContext`:

* Derives from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext).
* Specifies which entities are included in the data model.
* Coordinates EF Core functionality, such as Create, Read, Update and Delete, for the `Movie` model.

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Data
{
    public class RazorPagesMovieContext : DbContext
    {
        public RazorPagesMovieContext (DbContextOptions<RazorPagesMovieContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; } = default!;
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [Configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

[]()

## Test the app

1. Run the app and append `/Movies` to the URL in the browser (`http://localhost:port/movies`).

   If you receive the following error:

   ```
   SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
   Login failed for user 'User-name'.
   ```

   You missed the [migrations step](#pmc6).

2. Test the **Create New** link.

   Note

   You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

3. Test the **Edit**, **Details**, and **Delete** links.

The next tutorial explains the files created by scaffolding.

### Examine the context registered with dependency injection

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the EF Core database context, are registered with dependency injection during application startup. Components that require these services (such as Razor Pages) are provided via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically created a database context and registered it with the dependency injection container. The following highlighted code is added to the `Program.cs` file by the scaffolder:

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_5_visual-studio-code+visual-studio-mac)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie70) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Previous: Get Started](razor-pages-start?view=aspnetcore-10.0) [Next: Scaffolded Razor Pages](page?view=aspnetcore-10.0)

In this tutorial, classes are added for managing movies in a database. The app's model classes use [Entity Framework Core (EF Core)](/en-us/ef/core) to work with the database. EF Core is an object-relational mapper (O/RM) that simplifies data access. You write the model classes first, and EF Core creates the database.

The model classes are known as POCO classes (from "**P**lain-**O**ld **C**LR **O**bjects") because they don't have a dependency on EF Core. They define the properties of the data that are stored in the database.

## Add a data model

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

1. In **Solution Explorer**, right-click the *RazorPagesMovie* project > **Add** > **New Folder**. Name the folder `Models`.

2. Right-click the `Models` folder. Select **Add** > **Class**. Name the class *Movie*.

3. Add the following properties to the `Movie` class:

   ```
   using System.ComponentModel.DataAnnotations;

   namespace RazorPagesMovie.Models
   {
       public class Movie
       {
           public int ID { get; set; }
           public string Title { get; set; } = string.Empty;

           [DataType(DataType.Date)]
           public DateTime ReleaseDate { get; set; }
           public string Genre { get; set; } = string.Empty;
           public decimal Price { get; set; }
       }
   }
   ```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute that specifies the type of data in the `ReleaseDate` property. With this attribute:

  * The user isn't required to enter time information in the date field.
  * Only the date is displayed, not time information.

1. Add a folder named `Models`.
2. Add a class to the `Models` folder named `Movie.cs`.

Add the following properties to the `Movie` class:

```
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; } = string.Empty;

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; } = string.Empty;
        public decimal Price { get; set; }
    }
}
```

The `Movie` class contains:

* An `ID` field to provide a primary key for the database.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute to specify the type of data in the `ReleaseDate` field. With this attribute:

  * The user is not required to enter time information in the date field.
  * Only the date is displayed, not time information.

[]()

### Add NuGet packages and EF tools

Run the following .NET CLI commands:

```
dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```

1. In the **Solution Tool Window**, right-click the *RazorPagesMovie* project, and then select **Add** > **New Folder...**. Name the folder `Models`.

2. Right-click the `Models` folder, and then select **Add** > **New File...**.

3. In the **New File** dialog:

   1. Select **General** in the left pane.
   2. Select **Empty Class** in the center pane.
   3. Name the class **Movie** and select **Create**.

4. Add the following properties to the `Movie` class:

   ```
   using System.ComponentModel.DataAnnotations;

   namespace RazorPagesMovie.Models
   {
       public class Movie
       {
           public int ID { get; set; }
           public string Title { get; set; } = string.Empty;

           [DataType(DataType.Date)]
           public DateTime ReleaseDate { get; set; }
           public string Genre { get; set; } = string.Empty;
           public decimal Price { get; set; }
       }
   }
   ```

The `Movie` class contains:

* An `ID` field to provide a primary key for the database.

* A [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute to specify the type of data in the `ReleaseDate` field. With this attribute:

  * The user isn't required to enter time information in the date field.
  * Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

Build the project to verify there are no compilation errors.

## Scaffold the movie model

In this section, the movie model is scaffolded. That is, the scaffolding tool produces pages for Create, Read, Update, and Delete (CRUD) operations for the movie model.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

1. Add the NuGet package `Microsoft.EntityFrameworkCore.Design`, which is required for the scaffolding tool.

   1. From the **Tools** menu, select **NuGet Package Manager** > **Manage NuGet Packages for Solution**
   2. Select the **Browse** tab.
   3. Enter `Microsoft.EntityFrameworkCore.Design` and select it from the list.
   4. Check **Project** and then Select **Install**
   5. Select **I Accept** in the **License Acceptance** dialog.

2. Create the *Pages/Movies* folder:

   1. Right-click on the *Pages* folder > **Add** > **New Folder**.
   2. Name the folder *Movies*.

3. Right-click on the *Pages/Movies* folder > **Add** > **New Scaffolded Item**.

4. In the **Add New Scaffold** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Add**.

5. Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

   1. In the **Model class** drop down, select **Movie (RazorPagesMovie.Models)**.
   2. In the **Data context class** row, select the **+** (plus) sign.
      1. In the **Add Data Context** dialog, the class name `RazorPagesMovie.Data.RazorPagesMovieContext` is generated.
   3. Select **Add**.

   If you get an error message that says you need to install the `Microsoft.EntityFrameworkCore.SqlServer` package, repeat the steps starting with **Add** > **New Scaffolded Item**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

* Open a command shell to the project directory, which contains the `Program.cs` and `.csproj` files. Run the following command:

  ```
  dotnet aspnet-codegenerator razorpage -m Movie -dc RazorPagesMovieContext -udl -outDir Pages/Movies --referenceScriptLibraries -sqlite
  ```

[]()The following table details the ASP.NET Core code generator options.

| Option                       | Description                                               |
| ---------------------------- | --------------------------------------------------------- |
| `-m`                         | The name of the model.                                    |
| `-dc`                        | The `DbContext` class to use.                             |
| `-udl`                       | Use the default layout.                                   |
| `-outDir`                    | The relative output folder path to create the views.      |
| `--referenceScriptLibraries` | Adds `_ValidationScriptsPartial` to Edit and Create pages |

Use the `-h` option to get help on the `dotnet aspnet-codegenerator razorpage` command:

```
dotnet aspnet-codegenerator razorpage -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to select the SQLite connection string in development and SQL Server in production.

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext")));
}
else
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMovieContext")));
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code doesn't call `UseDeveloperExceptionPage` in development because `WebApplication` calls `UseDeveloperExceptionPage` in development mode.

1. Create a *Pages/Movies* folder:

   1. Right-click on the *Pages* folder > **Add** > **New Folder**.
   2. Name the folder *Movies*.

2. Right-click on the *Pages/Movies* folder > **Add** > **New Scaffolding...**.

3. In the **New Scaffolding** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Next**.

4. Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

   1. In the **Model class to use:** row, enter `Movie`.
   2. In the **DbContext Class to use:** row, name the class `RazorPagesMovie.Data.RazorPagesMovieContext`.
   3. Select **Finish**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to select the SQLite connection string in development and SQL Server in production.

```
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext")));
}
else
{
    builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("ProductionMovieContext")));
}

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The preceding code doesn't call `UseDeveloperExceptionPage` in development because `WebApplication` calls `UseDeveloperExceptionPage` in development mode.

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

### Files created and updated

The scaffold process creates the following files:

* *Pages/Movies*: Create, Delete, Details, Edit, and Index.
* `Data/RazorPagesMovieContext.cs`

The created files are explained in the next tutorial.

The scaffold process adds the following highlighted code to the `Program.cs` file:

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The `Program.cs` changes are explained later in this tutorial.

[]()

## Create the initial database schema using EF's migration feature

The migrations feature in Entity Framework Core provides a way to:

* Create the initial database schema.
* Incrementally update the database schema to keep it in sync with the app's data model. Existing data in the database is preserved.

- [Visual Studio](#tabpanel_4_visual-studio)
- [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

In this section, the **Package Manager Console** (PMC) window is used to:

* Add an initial migration.
* Update the database with the initial migration.

1. From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console**.

2. In the PMC, enter the following commands:

   ```
   Add-Migration InitialCreate
   Update-Database
   ```

* In the **Solution Tool Window**, right-click the *RazorPagesMovie* project, and then select **Open in Terminal**.

  The **Terminal** window opens with the command prompt at the project directory, which contains the Program.cs and .csproj files.

* Run the following .NET CLI commands:

  ```
  dotnet tool install --global dotnet-ef
  dotnet ef migrations add InitialCreate
  dotnet ef database update
  ```

  Note

  By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Note

For SQLite, column type for the `Price` field is set to `TEXT`. This is resolved in a later step.

The preceding commands install [the Entity Framework Core tools](/en-us/ef/core/get-started/overview/install#get-the-entity-framework-core-tools) and run the `migrations` command to generate code that creates the initial database schema.

The following warning is displayed, which is addressed in a later step:

> No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

The `migrations` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migrations. Any name can be used, but by convention a name is selected that describes the migration.

The `update` command runs the `Up` method in migrations that have not been applied. In this case, `update` runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

### Examine the context registered with dependency injection

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the EF Core database context, are registered with dependency injection during application startup. Components that require these services (such as Razor Pages) are provided via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically created a database context and registered it with the dependency injection container. The following highlighted code is added to the `Program.cs` file by the scaffolder:

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_5_visual-studio-code+visual-studio-mac)

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

```
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("RazorPagesMovieContext") ?? throw new InvalidOperationException("Connection string 'RazorPagesMovieContext' not found.")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

The data context `RazorPagesMovieContext`:

* Derives from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext).
* Specifies which entities are included in the data model.
* Coordinates EF Core functionality, such as Create, Read, Update and Delete, for the `Movie` model.

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Data
{
    public class RazorPagesMovieContext : DbContext
    {
        public RazorPagesMovieContext (DbContextOptions<RazorPagesMovieContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesMovie.Models.Movie>? Movie { get; set; }
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [Configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

[]()

## Test the app

1. Run the app and append `/Movies` to the URL in the browser (`http://localhost:port/movies`).

   If you receive the following error:

   ```
   SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
   Login failed for user 'User-name'.
   ```

   You missed the [migrations step](#pmc6).

2. Test the **Create New** link.

   Note

   You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

3. Test the **Edit**, **Details**, and **Delete** links.

The next tutorial explains the files created by scaffolding.

## Troubleshooting with the completed sample

If you run into a problem you can't resolve, compare your code to the completed project. [View or download completed project](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie60) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Next steps

[Previous: Get Started](razor-pages-start?view=aspnetcore-10.0) [Next: Scaffolded Razor Pages](page?view=aspnetcore-10.0)

In this section, classes are added for managing movies in a database. The app's model classes use [Entity Framework Core (EF Core)](/en-us/ef/core) to work with the database. EF Core is an object-relational mapper (O/RM) that simplifies data access. You write the model classes first, and EF Core creates the database.

The model classes are known as POCO classes (from "**P**lain-**O**ld **C**LR **O**bjects") because they don't have a dependency on EF Core. They define the properties of the data that are stored in the database.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie50) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

## Add a data model

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

1. In **Solution Explorer**, right-click the *RazorPagesMovie* project > **Add** > **New Folder**. Name the folder `Models`.

2. Right-click the `Models` folder. Select **Add** > **Class**. Name the class *Movie*.

3. Add the following properties to the `Movie` class:

   ```
   using System;
   using System.ComponentModel.DataAnnotations;

   namespace RazorPagesMovie.Models
   {
       public class Movie
       {
           public int ID { get; set; }
           public string Title { get; set; }

           [DataType(DataType.Date)]
           public DateTime ReleaseDate { get; set; }
           public string Genre { get; set; }
           public decimal Price { get; set; }
       }
   }
   ```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* `[DataType(DataType.Date)]`: The [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). With this attribute:

  * The user isn't required to enter time information in the date field.
  * Only the date is displayed, not time information.

1. Add a folder named `Models`.
2. Add a class to the `Models` folder named `Movie.cs`.

Add the following properties to the `Movie` class:

```
using System;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}
```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* `[DataType(DataType.Date)]`: The [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). With this attribute:

  * The user is not required to enter time information in the date field.
  * Only the date is displayed, not time information.

[]()

### Add NuGet packages and EF tools

Run the following .NET CLI commands:

```
dotnet tool install --global dotnet-ef
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```

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

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0-*" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.0-*" />
  </ItemGroup>

</Project>
```

1. In the **Solution Tool Window**, control-click the *RazorPagesMovie* project, and then select **Add** > **New Folder...**. Name the folder `Models`.

2. Control-click the `Models` folder, and then select **Add** > **New File...**.

3. In the **New File** dialog:

   1. Select **General** in the left pane.
   2. Select **Empty Class** in the center pane.
   3. Name the class **Movie** and select **New**.

4. Add the following properties to the `Movie` class:

   ```
   using System;
   using System.ComponentModel.DataAnnotations;

   namespace RazorPagesMovie.Models
   {
       public class Movie
       {
           public int ID { get; set; }
           public string Title { get; set; }

           [DataType(DataType.Date)]
           public DateTime ReleaseDate { get; set; }
           public string Genre { get; set; }
           public decimal Price { get; set; }
       }
   }
   ```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* `[DataType(DataType.Date)]`: The [\[DataType\]](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). With this attribute:

  * The user is not required to enter time information in the date field.
  * Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

Build the project to verify there are no compilation errors.

## Scaffold the movie model

In this section, the movie model is scaffolded. That is, the scaffolding tool produces pages for Create, Read, Update, and Delete (CRUD) operations for the movie model.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

1. Create a *Pages/Movies* folder:

   1. Right-click on the *Pages* folder > **Add** > **New Folder**.
   2. Name the folder *Movies*.

2. Right-click on the *Pages/Movies* folder > **Add** > **New Scaffolded Item**.

3. In the **Add Scaffold** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Add**.

4. Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

   1. In the **Model class** drop down, select **Movie (RazorPagesMovie.Models)**.
   2. In the **Data context class** row, select the **+** (plus) sign.
      1. In the **Add Data Context** dialog, the class name `RazorPagesMovie.Data.RazorPagesMovieContext` is generated.
   3. Select **Add**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

* Open a command shell to the project directory, which contains the `Program.cs`, `Startup.cs`, and `.csproj` files. Run the following command:

  ```
  dotnet aspnet-codegenerator razorpage -m Movie -dc RazorPagesMovieContext -udl -outDir Pages/Movies --referenceScriptLibraries -sqlite
  ```

[]()The following table details the ASP.NET Core code generator options.

| Option                       | Description                                               |
| ---------------------------- | --------------------------------------------------------- |
| `-m`                         | The name of the model.                                    |
| `-dc`                        | The `DbContext` class to use.                             |
| `-udl`                       | Use the default layout.                                   |
| `-outDir`                    | The relative output folder path to create the views.      |
| `--referenceScriptLibraries` | Adds `_ValidationScriptsPartial` to Edit and Create pages |

Use the `-h` option to get help on the `dotnet aspnet-codegenerator razorpage` command:

```
dotnet aspnet-codegenerator razorpage -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

[]()

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to inject [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into `Startup`. `IWebHostEnvironment` is injected so `ConfigureServices` can use SQLite in development and SQL Server in production.

```
public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Environment = env;
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Environment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        if (Environment.IsDevelopment())
        {
            services.AddDbContext<RazorPagesMovieContext>(options =>
            options.UseSqlite(
                Configuration.GetConnectionString("RazorPagesMovieContext")));
        }
        else
        {
            services.AddDbContext<RazorPagesMovieContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("MovieContext")));
        }

        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app)
    {
        if (Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

1. Create a *Pages/Movies* folder:

   1. Control-click on the *Pages* folder > **Add** > **New Folder**.
   2. Name the folder *Movies*.

2. Control-click on the *Pages/Movies* folder > **Add** > **New Scaffolding...**.

3. In the **New Scaffolding** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Next**.

4. Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

   1. In the **DbContext Class to use:** row, name the class `RazorPagesMovie.Data.RazorPagesMovieContext`.
   2. Select **Finish**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

[]()

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to inject [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into `Startup`. `IWebHostEnvironment` is injected so `ConfigureServices` can use SQLite in development and SQL Server in production.

```
public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Environment = env;
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Environment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        if (Environment.IsDevelopment())
        {
            services.AddDbContext<RazorPagesMovieContext>(options =>
            options.UseSqlite(
                Configuration.GetConnectionString("RazorPagesMovieContext")));
        }
        else
        {
            services.AddDbContext<RazorPagesMovieContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("MovieContext")));
        }

        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app)
    {
        if (Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

### Files created and updated

The scaffold process creates the following files:

* *Pages/Movies*: Create, Delete, Details, Edit, and Index.
* `Data/RazorPagesMovieContext.cs`

#### Updated files

* `Startup.cs`

The created and updated files are explained in the next section.

[]()

## Create the initial database schema using EF's migration feature

The migrations feature in Entity Framework Core provides a way to:

* Create the initial database schema.
* Incrementally update the database schema to keep it in sync with the application's data model. Existing data in the database is preserved.

- [Visual Studio](#tabpanel_3_visual-studio)
- [Visual Studio Code / Visual Studio for Mac](#tabpanel_3_visual-studio-code+visual-studio-mac)

In this section, the **Package Manager Console** (PMC) window is used to:

* Add an initial migration.
* Update the database with the initial migration.

1. From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console**.

2. In the PMC, enter the following commands:

   ```
   Add-Migration InitialCreate
   Update-Database
   ```

* Run the following .NET CLI commands:

  ```
  dotnet ef migrations add InitialCreate
  dotnet ef database update
  ```

Note

For SQLite, column type for the `Price` field is set to `TEXT`. This is resolved in a later step.

For SQL Server, the preceding commands generate the following warning: "No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'."

Ignore the warning, as it will be addressed in a later step.

The `migrations` command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migrations. Any name can be used, but by convention a name is selected that describes the migration.

The `update` command runs the `Up` method in migrations that have not been applied. In this case, `update` runs the `Up` method in the `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

### Examine the context registered with dependency injection

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the EF Core database context, are registered with dependency injection during application startup. Components that require these services (such as Razor Pages) are provided via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically created a database context and registered it with the dependency injection container.

Examine the `Startup.ConfigureServices` method. The highlighted line was added by the scaffolder:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<RazorPagesMovieContext>(options =>
      options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
```

The `RazorPagesMovieContext` coordinates EF Core functionality, such as Create, Read, Update and Delete, for the `Movie` model. The data context (`RazorPagesMovieContext`) is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext). The data context specifies which entities are included in the data model.

```
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie.Data
{
    public class RazorPagesMovieContext : DbContext
    {
        public RazorPagesMovieContext (
            DbContextOptions<RazorPagesMovieContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; }
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [Configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

Examine the `Up` method.

[]()

## Test the app

1. Run the app and append `/Movies` to the URL in the browser (`http://localhost:port/movies`).

   If you receive the following error:

   ```
   SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
   Login failed for user 'User-name'.
   ```

   You missed the [migrations step](#pmc).

2. Test the **Create** link.

   Note

   You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

3. Test the **Edit**, **Details**, and **Delete** links.

## SQL Logging of Entity Framework Core

Logging configuration is commonly provided by the `Logging` section of `appsettings.{Environment}.json` files. To log SQL statements, add `"Microsoft.EntityFrameworkCore.Database.Command": "Information"` to the `appsettings.Development.json` file:

```
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDB-2;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
     ,"Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
  "AllowedHosts": "*"
}
```

With the preceding JSON, SQL statements are displayed on the command line and in the Visual Studio output window.

For more information, see the following resources:

* [Logging in .NET and ASP.NET Core](../../fundamentals/logging/?view=aspnetcore-10.0#configuration)
* [ASP.NET template disables EF Core SQL logging by default (`dotnet/aspnetcore` #32977)](https://github.com/dotnet/aspnetcore/issues/32977)

The next tutorial explains the files created by scaffolding.

## Next steps

[Previous: Get Started](razor-pages-start?view=aspnetcore-10.0) [Next: Scaffolded Razor Pages](page?view=aspnetcore-10.0)

In this section, classes are added for managing movies. The app's model classes use [Entity Framework Core (EF Core)](/en-us/ef/core) to work with the database. EF Core is an object-relational mapper (O/RM) that simplifies data access.

The model classes are known as POCO classes (from "plain-old CLR objects") because they don't have any dependency on EF Core. They define the properties of the data that are stored in the database.

[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/razor-pages/razor-pages-start/samples/RazorPagesMovie30) ([how to download](../../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)).

Warning

This article uses a local database that doesn't require the user to be authenticated. Production apps should use the most secure authentication flow available. For more information on authentication for deployed test and production apps, see [Secure authentication flows](../../security/?view=aspnetcore-10.0#secure-authentication-flows).

## Add a data model

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_1_visual-studio-mac)

Right-click the **RazorPagesMovie** project > **Add** > **New Folder**. Name the folder `Models`.

Right-click the `Models` folder. Select **Add** > **Class**. Name the class **Movie**.

Add the following properties to the `Movie` class:

```
using System;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}
```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* `[DataType(DataType.Date)]`: The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). With this attribute:

  * The user is not required to enter time information in the date field.
  * Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

* Add a folder named `Models`.
* Add a class to the `Models` folder named `Movie.cs`.

Add the following properties to the `Movie` class:

```
using System;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}
```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* `[DataType(DataType.Date)]`: The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). With this attribute:

  * The user is not required to enter time information in the date field.
  * Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

[]()

### Add NuGet packages and EF tools

Run the following .NET CLI commands:

```
dotnet tool install --global dotnet-ef --version 3.1.9
dotnet tool install --global dotnet-aspnet-codegenerator --version 3.1.4
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 3.1.9
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore --version 3.1.9
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design --version 3.1.4
dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 3.1.9
dotnet add package Microsoft.Extensions.Logging.Debug --version 3.1.9
```

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
  </ItemGroup>

</Project>
```

### Add a database context class

* In the *RazorPagesMovie* project, create a new folder named *Data*.

* Add the following `RazorPagesMovieContext` class to the *Data* folder:

  ```
  using Microsoft.EntityFrameworkCore;

  namespace RazorPagesMovie.Data
  {
      public class RazorPagesMovieContext : DbContext
      {
          public RazorPagesMovieContext (
              DbContextOptions<RazorPagesMovieContext> options)
              : base(options)
          {
          }

          public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; }
      }
  }
  ```

The preceding code creates a `DbSet` property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table, and an entity corresponds to a row in the table. The code won't compile until dependencies are added in a later step.

[]()

### Add a database connection string

Add a connection string to the `appsettings.json` file as shown in the following highlighted code:

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RazorPagesMovieContext": "Data Source=MvcMovie.db"
  }
}
```

[]()

### Register the database context

Add the following `using` statements at the top of `Startup.cs`:

```
using RazorPagesMovie.Data;
using Microsoft.EntityFrameworkCore;
```

Register the database context with the [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0) container in `Startup.ConfigureServices`.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<RazorPagesMovieContext>(options =>
        options.UseSqlite(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
```

* In the **Solution Tool Window**, control-click the **RazorPagesMovie** project, and then select **Add** > **New Folder...**. Name the folder `Models`.

* Right-click the `Models` folder, and then select **Add** > **New File...**.

* In the **New File** dialog:

  * Select **General** in the left pane.
  * Select **Empty Class** in the center pane.
  * Name the class **Movie** and select **New**.

Add the following properties to the `Movie` class:

```
using System;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}
```

The `Movie` class contains:

* The `ID` field is required by the database for the primary key.

* `[DataType(DataType.Date)]`: The [DataType](/en-us/dotnet/api/system.componentmodel.dataannotations.datatypeattribute) attribute specifies the type of the data (`Date`). With this attribute:

  * The user is not required to enter time information in the date field.
  * Only the date is displayed, not time information.

[DataAnnotations](/en-us/dotnet/api/system.componentmodel.dataannotations) are covered in a later tutorial.

Build the project to verify there are no compilation errors.

## Scaffold the movie model

In this section, the movie model is scaffolded. That is, the scaffolding tool produces pages for Create, Read, Update, and Delete (CRUD) operations for the movie model.

* [Visual Studio](#tabpanel_2_visual-studio)
* [Visual Studio Code](#tabpanel_2_visual-studio-code)
* [Visual Studio for Mac](#tabpanel_2_visual-studio-mac)

Create a *Pages/Movies* folder:

* Right-click on the *Pages* folder > **Add** > **New Folder**.
* Name the folder *Movies*.

Right-click on the *Pages/Movies* folder > **Add** > **New Scaffolded Item**.

In the **Add Scaffold** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Add**.

Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

* In the **Model class** drop down, select **Movie (RazorPagesMovie.Models)**.
* In the **Data context class** row, select the **+** (plus) sign and change the generated name from RazorPagesMovie.*`Models`*.RazorPagesMovieContext to RazorPagesMovie.**Data**.RazorPagesMovieContext. [This change](https://developercommunity.visualstudio.com/content/problem/652166/aspnet-core-ef-scaffolder-uses-incorrect-namespace.html) is not required. It creates the database context class with the correct namespace.
* Select **Add**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

* Open a command window in the project directory, which contains the `Program.cs`, `Startup.cs`, and `.csproj` files.

* **For Windows**: Run the following command:

  ```
  dotnet aspnet-codegenerator razorpage -m Movie -dc RazorPagesMovieContext -udl -outDir Pages\Movies --referenceScriptLibraries
  ```

* **For macOS and Linux**: Run the following command:

  ```
  dotnet aspnet-codegenerator razorpage -m Movie -dc RazorPagesMovieContext -udl -outDir Pages/Movies --referenceScriptLibraries
  ```

[]()The following table details the ASP.NET Core code generator options:

| Option                       | Description                                               |
| ---------------------------- | --------------------------------------------------------- |
| `-m`                         | The name of the model.                                    |
| `-dc`                        | The `DbContext` class to use.                             |
| `-udl`                       | Use the default layout.                                   |
| `-outDir`                    | The relative output folder path to create the views.      |
| `--referenceScriptLibraries` | Adds `_ValidationScriptsPartial` to Edit and Create pages |

Use the `-h` option to get help on the `dotnet aspnet-codegenerator razorpage` command:

```
dotnet aspnet-codegenerator razorpage -h
```

For more information, see [dotnet aspnet-codegenerator](../../fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-10.0).

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to inject [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into Startup. `IWebHostEnvironment` is injected so `ConfigureServices` can use SQLite in development and SQL Server in production.

```
public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Environment = env;
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Environment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        if (Environment.IsDevelopment())
        {
            services.AddDbContext<RazorPagesMovieContext>(options =>
            options.UseSqlite(
                Configuration.GetConnectionString("RazorPagesMovieContext")));
        }
        else
        {
            services.AddDbContext<RazorPagesMovieContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("MovieContext")));
        }

        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app)
    {
        if (Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

Create a *Pages/Movies* folder:

* Right-click on the *Pages* folder > **Add** > **New Folder**.
* Name the folder *Movies*.

Right-click on the *Pages/Movies* folder > **Add** > **New Scaffolding...**.

In the **New Scaffolding** dialog, select **Razor Pages using Entity Framework (CRUD)** > **Next**.

Complete the **Add Razor Pages using Entity Framework (CRUD)** dialog:

* In the **Model class** drop down, select, or type, **Movie (RazorPagesMovie.Models)**.
* In the **Data context class** row, type the name for the new class, RazorPagesMovie.**Data**.RazorPagesMovieContext. [This change](https://developercommunity.visualstudio.com/content/problem/652166/aspnet-core-ef-scaffolder-uses-incorrect-namespace.html) is not required. It creates the database context class with the correct namespace.
* Select **Add**.

The `appsettings.json` file is updated with the connection string used to connect to a local database.

### Add EF tools

Run the following .NET CLI command:

```
dotnet tool install --global dotnet-ef
```

The preceding command adds the Entity Framework Core Tools for the .NET CLI. For more information, see [Entity Framework Core tools reference - .NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

### Use SQLite for development, SQL Server for production

When SQLite is selected, the template generated code is ready for development. The following code shows how to inject [IWebHostEnvironment](/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment) into Startup. `IWebHostEnvironment` is injected so `ConfigureServices` can use SQLite in development and SQL Server in production.

```
public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Environment = env;
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Environment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        if (Environment.IsDevelopment())
        {
            services.AddDbContext<RazorPagesMovieContext>(options =>
            options.UseSqlite(
                Configuration.GetConnectionString("RazorPagesMovieContext")));
        }
        else
        {
            services.AddDbContext<RazorPagesMovieContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("MovieContext")));
        }

        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app)
    {
        if (Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}
```

### Files created

* [Visual Studio](#tabpanel_3_visual-studio)
* [Visual Studio for Mac](#tabpanel_3_visual-studio-mac)
* [Visual Studio Code](#tabpanel_3_visual-studio-code)

The scaffold process creates and updates the following files:

* *Pages/Movies*: Create, Delete, Details, Edit, and Index.
* `Data/RazorPagesMovieContext.cs`

### Updated

* `Startup.cs`

The created and updated files are explained in the next section.

The scaffold process creates and updates the following files:

* *Pages/Movies*: Create, Delete, Details, Edit, and Index.
* `Data/RazorPagesMovieContext.cs`

### Updated

* `Startup.cs`

The created and updated files are explained in the next section.

The scaffold process creates the following files:

* *Pages/Movies*: Create, Delete, Details, Edit, and Index.

The created files are explained in the next section.

[]()

## Initial migration

* [Visual Studio](#tabpanel_4_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_4_visual-studio-code+visual-studio-mac)

In this section, the Package Manager Console (PMC) is used to:

* Add an initial migration.
* Update the database with the initial migration.

From the **Tools** menu, select **NuGet Package Manager** > **Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration InitialCreate
Update-Database
```

If `dotnet ef` has not been installed, install it as a global tool:

```
  dotnet tool install --global dotnet-ef
```

For more information on the CLI for EF Core, see [EF Core tools reference for the .NET CLI](/en-us/ef/core/miscellaneous/cli/dotnet).

Note

By default, the architecture of the .NET binaries to install represents the currently running operating system architecture. To specify a different architecture, review how to use the `dotnet tool install` command with the ['--arch' option](/en-us/dotnet/core/tools/dotnet-tool-install#options). For more information, see [GitHub dotnet/aspnetcore.docs issue #29262](https://github.com/dotnet/AspNetCore.Docs/issues/29262) - *Add '-a arm64' on Apple Silicon*.

Run the following .NET CLI commands:

```
dotnet ef migrations add InitialCreate
dotnet ef database update
```

The preceding commands generate the following warning: "No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'."

Ignore the warning, as it will be addressed in a later step.

The migrations command generates code to create the initial database schema. The schema is based on the model specified in `DbContext`. The `InitialCreate` argument is used to name the migrations. Any name can be used, but by convention a name is selected that describes the migration.

The `update` command runs the `Up` method in migrations that have not been applied. In this case, `update` runs the `Up` method in `Migrations/<time-stamp>_InitialCreate.cs` file, which creates the database.

* [Visual Studio](#tabpanel_5_visual-studio)
* [Visual Studio Code / Visual Studio for Mac](#tabpanel_5_visual-studio-code+visual-studio-mac)

### Examine the context registered with dependency injection

ASP.NET Core is built with [dependency injection](../../fundamentals/dependency-injection?view=aspnetcore-10.0). Services, such as the EF Core database context, are registered with dependency injection during application startup. Components that require these services, such as Razor Pages, are provided via constructor parameters. The constructor code that gets a database context instance is shown later in the tutorial.

The scaffolding tool automatically created a database context and registered it with the dependency injection container.

Examine the `Startup.ConfigureServices` method. The highlighted line was added by the scaffolder:

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<RazorPagesMovieContext>(options =>
      options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext")));
}
```

The `RazorPagesMovieContext` coordinates EF Core functionality, such as Create, Read, Update and Delete, for the `Movie` model. The data context (`RazorPagesMovieContext`) is derived from [Microsoft.EntityFrameworkCore.DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext). The data context specifies which entities are included in the data model.

```
using Microsoft.EntityFrameworkCore;

namespace RazorPagesMovie.Data
{
    public class RazorPagesMovieContext : DbContext
    {
        public RazorPagesMovieContext (
            DbContextOptions<RazorPagesMovieContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; }
    }
}
```

The preceding code creates a [DbSet\<Movie>](/en-us/dotnet/api/microsoft.entityframeworkcore.dbset-1) property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

The name of the connection string is passed in to the context by calling a method on a [DbContextOptions](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptions) object. For local development, the [Configuration system](../../fundamentals/configuration/?view=aspnetcore-10.0) reads the connection string from the `appsettings.json` file.

Examine the `Up` method.

[]()

### Test the app

* Run the app and append `/Movies` to the URL in the browser (`http://localhost:port/movies`).

If you get the error:

```
SqlException: Cannot open database "RazorPagesMovieContext-GUID" requested by the login. The login failed.
Login failed for user 'User-name'.
```

You missed the [migrations step](#pmc).

* Test the **Create** link.

  Note

  You may not be able to enter decimal commas in the `Price` field. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point and for non US-English date formats, the app must be globalized. For globalization instructions, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-326590420).

* Test the **Edit**, **Details**, and **Delete** links.

The next tutorial explains the files created by scaffolding.

## Next steps

[Previous: Get Started](razor-pages-start?view=aspnetcore-10.0) [Next: Scaffolded Razor Pages](page?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-20

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/social/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# External provider authentication in ASP.NET Core Identity

By [Valeriy Novytskyy](https://github.com/01binary) and [Rick Anderson](https://x.com/RickAndMSFT)

This article explains how to build an ASP.NET Core app that enables users to sign in using OAuth 2.0 with credentials from external authentication providers.

[Facebook](facebook-logins?view=aspnetcore-10.0), [X (formerly Twitter)](twitter-logins?view=aspnetcore-10.0), [Google](google-logins?view=aspnetcore-10.0), and [Microsoft](microsoft-logins?view=aspnetcore-10.0) providers are covered in the following sections and use the starter project created in this article. Other providers are available in third-party packages such as [OpenIddict](https://documentation.openiddict.com/integrations/web-providers), [AspNet.Security.OAuth.Providers](https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers) and [AspNet.Security.OpenId.Providers](https://github.com/aspnet-contrib/AspNet.Security.OpenId.Providers).

Enabling users to sign in with their existing credentials is convenient for the users and shifts many of the complexities of managing the sign-in process onto a third party.

## Create a New ASP.NET Core Project

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code / .NET CLI](#tabpanel_1_visual-studio-code+net-cli)

- Select the **ASP.NET Core Web App** template and select **Next**.
- Name the project and select **Next**.
- In the **Authentication type** dropdown, select **Individual Accounts** and select **Create**.

* Open a command shell. For Visual Studio Code, you can use the [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal).

* Change directories (`cd`) to a folder for the sample app.

* For Windows, run the following command:

  ```
  dotnet new webapp -o WebApp1 -au Individual -uld
  ```

  For macOS and Linux, run the following command:

  ```
  dotnet new webapp -o WebApp1 -au Individual
  ```

  * The `dotnet new` command uses the `-o|--output` option to create a new Razor Pages project in the `WebApp1` folder.
  * `-au Individual` creates the code for Individual authentication.
  * `-uld` uses LocalDB, a lightweight version of SQL Server Express for Windows. Omit `-uld` to use SQLite, which is the default across all platforms.

  For more information, see [`dotnet new <TEMPLATE>`](/en-us/dotnet/core/tools/dotnet-new).

## Apply migrations

* Run the app and select the **Register** link.
* Enter the email and password for the new account, and then select **Register**.
* Follow the instructions to apply migrations.

## Forward request information with a proxy or load balancer

If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. This information usually includes the secure request scheme (`https`), host, and client IP address. Apps don't automatically read these request headers to discover and use the original request information.

The scheme is used in link generation that affects the authentication flow with external providers. Losing the secure scheme (`https`) results in the app generating incorrect insecure redirect URLs.

Use forwarded headers middleware to make the original request information available to the app for request processing.

For more information, see [Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0).

## Use Secret Manager to store tokens assigned by login providers

Social login providers assign **Application Id** and **Application Secret** tokens during the registration process. The exact token names vary by provider. These tokens represent the credentials that the app uses to access the provider's API. The tokens constitute *user secrets* that can be linked to your app configuration with the help of [Secret Manager](../../app-secrets?view=aspnetcore-10.0#secret-manager). User secrets are a more secure alternative to storing the tokens in a configuration file, such as `appsettings.json`.

Important

Secret Manager is only for local development and testing. Protect staging and production secrets with the [Azure Key Vault configuration provider](../../key-vault-configuration?view=aspnetcore-10.0), which can also be used for local development and testing if you prefer not to use the Secret Manager locally.

For guidance on storing the tokens assigned by each login provider, see [Safe storage of app secrets in development](../../app-secrets?view=aspnetcore-10.0).

## Configure login providers

Use the following articles to configure login providers and the app:

* [Facebook](facebook-logins?view=aspnetcore-10.0) instructions
* [X (formerly Twitter)](twitter-logins?view=aspnetcore-10.0) instructions
* [Google](google-logins?view=aspnetcore-10.0) instructions
* [Microsoft](microsoft-logins?view=aspnetcore-10.0) instructions
* [Other provider](other-logins?view=aspnetcore-10.0) instructions

## Multiple authentication providers

When the app requires multiple providers, chain the provider extension methods on [AddAuthentication](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authenticationservicecollectionextensions.addauthentication):

```
builder.Services.AddAuthentication()
    .AddGoogle(options =>
    {
        // Google configuration options
    })
    .AddFacebook(options =>
    {
        // Facebook configuration options
    })
    .AddMicrosoftAccount(options =>
    {
        // Microsoft Account configuration options
    })
    .AddTwitter(options =>
    {
        // X (formerly Twitter) configuration options
    });
```

For detailed configuration guidance on each provider, see their respective articles.

## Optionally set a password

When you register with an external login provider, you don't have a password registered with the app. This alleviates you from creating and remembering a password for the site, but it also makes you completely dependent on the external login provider for site access. If the external login provider is unavailable, you won't be able to sign in to the app.

To create a password and sign in using your email that you set during the sign-in process with external providers:

* Select the **Hello \<email alias>** link at the top-right corner to navigate to the **Manage** view:
* Select **Create**:
* Set a valid password, and you can use this credential to sign in with your email address.

## Additional information

* [Sign in with Apple Example Integration](https://github.com/martincostello/SignInWithAppleSample)
* [How to customize the login buttons (`dotnet/AspNetCore.Docs` #10563)](https://github.com/dotnet/AspNetCore.Docs/issues/10563)
* [Persist additional data about the user and their access and refresh tokens](additional-claims?view=aspnetcore-10.0)
* [Passkeys (FIDO2/WebAuthn) in ASP.NET Core Identity](../passkeys/?view=aspnetcore-10.0): A passwordless alternative to social logins introduced in .NET 10.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/control-head-content
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/components/control-head-content.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Control `<head>` content in ASP.NET Core Blazor apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Razor components can modify the HTML `<head>` element content of a page, including setting the page's title (`<title>` element) and modifying metadata (`<meta>` elements).

## Control `<head>` content in a Razor component

Specify the page's title with the [PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) component, which enables rendering an HTML `<title>` element to a [`HeadOutlet` component](#headoutlet-component).

Specify `<head>` element content with the [HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) component, which provides content to a [`HeadOutlet` component](#headoutlet-component).

The following example sets the page's title and description using Razor.

`ControlHeadContent.razor`:

```
@page "/control-head-content"

<PageTitle>@title</PageTitle>

<h1>Control <head> Content Example</h1>

<p>
    Title: @title
</p>

<p>
    Description: @description
</p>

<HeadContent>
    <meta name="description" content="@description">
</HeadContent>

@code {
    private string description = "This description is set by the component.";
    private string title = "Control <head> Content";
}
```

```
@page "/control-head-content"

<PageTitle>@title</PageTitle>

<h1>Control <head> Content Example</h1>

<p>
    Title: @title
</p>

<p>
    Description: @description
</p>

<HeadContent>
    <meta name="description" content="@description">
</HeadContent>

@code {
    private string description = "This description is set by the component.";
    private string title = "Control <head> Content";
}
```

```
@page "/control-head-content"

<h1>Control <head> content</h1>

<p>
    Title: @title
</p>

<p>
    Description: @description
</p>

<PageTitle>@title</PageTitle>

<HeadContent>
    <meta name="description" content="@description">
</HeadContent>

@code {
    private string description = "Description set by component";
    private string title = "Title set by component";
}
```

```
@page "/control-head-content"

<h1>Control <head> content</h1>

<p>
    Title: @title
</p>

<p>
    Description: @description
</p>

<PageTitle>@title</PageTitle>

<HeadContent>
    <meta name="description" content="@description">
</HeadContent>

@code {
    private string description = "Description set by component";
    private string title = "Title set by component";
}
```

## Control `<head>` content during prerendering

*This section applies to prerendered Blazor WebAssembly apps and Blazor Server apps.*

When Razor components are prerendered, the use of a layout page (`_Layout.cshtml`) is required to control `<head>` content with the [PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) and [HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) components. The reason for this requirement is that components that control `<head>` content must be rendered before the layout with the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component. **This order of rendering is required to control head content.**

If the shared `_Layout.cshtml` file doesn't have a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) for a [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component, add it to the `<head>` elements.

In a **required**, shared `_Layout.cshtml` file of a Blazor Server app or Razor Pages/MVC app that embeds components into pages or views:

```
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
```

In a **required**, shared `_Layout.cshtml` file of a prerendered hosted Blazor WebAssembly app:

```
<component type="typeof(HeadOutlet)" render-mode="WebAssemblyPrerendered" />
```

## Set a page title for components via a layout

Set the page title in a [layout component](layouts?view=aspnetcore-10.0):

```
@inherits LayoutComponentBase

<PageTitle>Page Title</PageTitle>

<div class="page">
    ...  
</div>
```

## `HeadOutlet` component

The [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component renders content provided by [PageTitle](/en-us/dotnet/api/microsoft.aspnetcore.components.web.pagetitle) and [HeadContent](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headcontent) components.

In a Blazor Web App created from the project template, the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component in `App.razor` renders `<head>` content:

```
<head>
    ...
    <HeadOutlet />
</head>
```

In Blazor Server apps created from the Blazor Server project template, a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) renders `<head>` content for the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component in `Pages/_Host.cshtml`:

```
<head>
    ...
    <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
```

In Blazor Server apps created from the Blazor Server project template, a [Component Tag Helper](../../mvc/views/tag-helpers/built-in/component-tag-helper?view=aspnetcore-10.0) renders `<head>` content for the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component in `Pages/_Layout.cshtml`:

```
<head>
    ...
    <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
```

In an app created from the Blazor WebAssembly project template, the [HeadOutlet](/en-us/dotnet/api/microsoft.aspnetcore.components.web.headoutlet) component is added to the [RootComponents](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder.rootcomponents#microsoft-aspnetcore-components-webassembly-hosting-webassemblyhostbuilder-rootcomponents) collection of the [WebAssemblyHostBuilder](/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.webassemblyhostbuilder) in the client-side `Program` file:

```
builder.RootComponents.Add<HeadOutlet>("head::after");
```

When the [`::after` pseudo-selector](https://developer.mozilla.org/docs/Web/CSS/::after) is specified, the contents of the root component are appended to the existing head contents instead of replacing the content. This allows the app to retain static head content in `wwwroot/index.html` without having to repeat the content in the app's Razor components.

## Set a default page title in a Blazor Web App

Set the page title in the `App` component (`App.razor`):

```
<head>
    ...
    <HeadOutlet />
    <PageTitle>Page Title</PageTitle>
</head>
```

## Not found page title in a Blazor WebAssembly app

In Blazor apps created from the Blazor WebAssembly Standalone App project template, the `NotFound` component template in the `App` component (`App.razor`) sets the page title to `Not found`.

In Blazor apps created from a Blazor project template, the `NotFound` component template in the `App` component (`App.razor`) sets the page title to `Not found`.

`App.razor`:

```
<NotFound>
    <PageTitle>Not found</PageTitle>
    ...
</NotFound>
```

## Related content

* [Control headers in C# code at startup](../fundamentals/startup?view=aspnetcore-10.0#control-headers-in-c-code)
* [Blazor samples GitHub repository (`dotnet/blazor-samples`)](https://github.com/dotnet/blazor-samples) ([how to download](../fundamentals/?view=aspnetcore-10.0#sample-apps))

Mozilla MDN Web Docs documentation:

* [What's in the head? Metadata in HTML](https://developer.mozilla.org/docs/Learn/HTML/Introduction_to_HTML/The_head_metadata_in_HTML)
* [`<head>`: The Document Metadata (Header) element](https://developer.mozilla.org/docs/Web/HTML/Element/head)
* [`<title>`: The Document Title element](https://developer.mozilla.org/docs/Web/HTML/Element/title)
* [`<meta>`: The metadata element](https://developer.mozilla.org/docs/Web/HTML/Element/meta)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/cache-tag-helper
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/views/tag-helpers/built-in/cache-tag-helper.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Cache Tag Helper in ASP.NET Core MVC

By [Peter Kellner](https://peterkellner.net)

The Cache Tag Helper provides the ability to improve the performance of your ASP.NET Core app by caching its content to the internal ASP.NET Core cache provider.

For an overview of Tag Helpers, see [Tag Helpers in ASP.NET Core](../intro?view=aspnetcore-10.0).

The following Razor markup caches the current date:

```
<cache>@DateTime.Now</cache>
```

The first request to the page that contains the Tag Helper displays the current date. Additional requests show the cached value until the cache expires (default 20 minutes) or until the cached date is evicted from the cache.

## Cache Tag Helper Attributes

### `expires-after`

| Attribute type | Example                      | Default |
| -------------- | ---------------------------- | ------- |
| `TimeSpan`     | `@TimeSpan.FromSeconds(120)` | —       |

`expires-after` sets the length of time from the first request time to cache the contents.

Example:

```
<cache expires-after="@TimeSpan.FromSeconds(120)">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

### `enabled`

| Attribute type | Examples        | Default |
| -------------- | --------------- | ------- |
| Boolean        | `true`, `false` | `true`  |

`enabled` determines if the content enclosed by the Cache Tag Helper is cached. The default is `true`. If set to `false`, the rendered output is **not** cached.

Example:

```
<cache enabled="true">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

### `expires-on`

| Attribute type   | Example                            |
| ---------------- | ---------------------------------- |
| `DateTimeOffset` | `@new DateTime(2025,1,29,17,02,0)` |

`expires-on` sets an absolute expiration date for the cached item.

The following example caches the contents of the Cache Tag Helper until 5:02 PM on January 29, 2025:

```
<cache expires-on="@new DateTime(2025,1,29,17,02,0)">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

### `expires-sliding`

| Attribute type | Example                     |
| -------------- | --------------------------- |
| `TimeSpan`     | `@TimeSpan.FromSeconds(60)` |

Sets the time that a cache entry should be evicted if its value hasn't been accessed.

Example:

```
<cache expires-sliding="@TimeSpan.FromSeconds(60)">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

Defaults to 30 seconds if [`expires-after`](#expires-after) and [`expires-on`](#expires-on) aren't defined.

### `priority`

| Attribute type      | Examples                               | Default  |
| ------------------- | -------------------------------------- | -------- |
| `CacheItemPriority` | `High`, `Low`, `NeverRemove`, `Normal` | `Normal` |

`priority` provides cache eviction guidance to the built-in cache provider. The web server evicts `Low` cache entries first when it's under memory pressure.

Example:

```
<cache priority="High">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

The `priority` attribute doesn't guarantee a specific level of cache retention. `CacheItemPriority` is only a suggestion. Setting this attribute to `NeverRemove` doesn't guarantee that cached items are always retained. See the topics in the [Related content](#related-content) section for more information.

The Cache Tag Helper is dependent on the [memory cache service](../../../../performance/caching/memory?view=aspnetcore-10.0). The Cache Tag Helper adds the service if it hasn't been added.

### `vary-by`

| Attribute type | Example  |
| -------------- | -------- |
| String         | `@Model` |

`vary-by` allows for customization of what data is cached. When the object referenced by the attribute's string value changes, the content of the Cache Tag Helper is updated. Often, a string-concatenation of model values are assigned to this attribute. Effectively, this results in a scenario where an update to any of the concatenated values invalidates the cache.

The following example assumes the controller method rendering the view sums the integer value of the two route parameters, `myParam1` and `myParam2`, and returns the sum as the single model property. When this sum changes, the content of the Cache Tag Helper is rendered and cached again.

Action:

```
public IActionResult Index(string myParam1, string myParam2, string myParam3)
{
    int num1;
    int num2;
    int.TryParse(myParam1, out num1);
    int.TryParse(myParam2, out num2);
    return View(viewName, num1 + num2);
}
```

`Index.cshtml`:

```
<cache vary-by="@Model">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

### `vary-by-cookie`

| Attribute type | Examples                                                                         |
| -------------- | -------------------------------------------------------------------------------- |
| String         | `.AspNetCore.Identity.Application`, `.AspNetCore.Identity.Application,HairColor` |

`vary-by-cookie` accepts a comma-delimited list of cookie names that trigger a cache refresh when the cookie values change.

The following example monitors the cookie associated with ASP.NET Core Identity. When a user is authenticated, a change in the Identity cookie triggers a cache refresh:

```
<cache vary-by-cookie=".AspNetCore.Identity.Application">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

### `vary-by-culture`

| Attribute type | Examples        | Default |
| -------------- | --------------- | ------- |
| Boolean        | `true`, `false` | `false` |

`vary-by-culture` varys the cached result by request culture. Setting the attribute to `true` means the result is varied by [CultureInfo.CurrentCulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentculture) and [CultureInfo.CurrentUICulture](/en-us/dotnet/api/system.globalization.cultureinfo.currentuiculture).

Example:

```
<cache vary-by-culture="true">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

### `vary-by-header`

| Attribute type | Examples                                    |
| -------------- | ------------------------------------------- |
| String         | `User-Agent`, `User-Agent,content-encoding` |

`vary-by-header` accepts a comma-delimited list of header values that trigger a cache refresh when they change.

The following example monitors the header value `User-Agent`. The example caches the content for every different `User-Agent` presented to the web server:

```
<cache vary-by-header="User-Agent">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

### `vary-by-query`

| Attribute type | Examples             |
| -------------- | -------------------- |
| String         | `Make`, `Make,Model` |

`vary-by-query` accepts a comma-delimited list of [Keys](/en-us/dotnet/api/microsoft.aspnetcore.http.iquerycollection.keys) in a query string ([Query](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.query)) that trigger a cache refresh when the value of any listed key changes.

The following example monitors the values of `Make` and `Model`. The example caches the content for every different `Make` and `Model` presented to the web server:

```
<cache vary-by-query="Make,Model">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

### `vary-by-route`

| Attribute type | Examples             |
| -------------- | -------------------- |
| String         | `Make`, `Make,Model` |

`vary-by-route` accepts a comma-delimited list of route parameter names that trigger a cache refresh when the route data parameter value changes.

Example:

`Startup.cs`:

```
routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{Make?}/{Model?}");
```

`Index.cshtml`:

```
<cache vary-by-route="Make,Model">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

### `vary-by-user`

| Attribute type | Examples        | Default |
| -------------- | --------------- | ------- |
| Boolean        | `true`, `false` | `false` |

`vary-by-user` specifies whether or not the cache resets when the signed-in user (or Context Principal) changes. The current user is also known as the Request Context Principal and can be viewed in a Razor view by referencing `@User.Identity.Name`.

The following example monitors the current logged in user to trigger a cache refresh:

```
<cache vary-by-user="true">
    Current Time Inside Cache Tag Helper: @DateTime.Now
</cache>
```

Using this attribute maintains the contents in cache through a sign-in and sign-out cycle. When the value is `true`, an authentication cycle invalidates the cache for the authenticated user. The cache is invalidated because a new unique cookie value is generated when a user is authenticated. Cache is maintained for the anonymous state when no cookie is present or the cookie has expired. If the user is **not** authenticated, the cache is maintained.

## Related content

* [Cache in-memory in ASP.NET Core](../../../../performance/caching/memory?view=aspnetcore-10.0)
* [Introduction to Identity on ASP.NET Core](../../../../security/authentication/identity?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/configure-linker
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/configure-linker.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Configure the Linker for ASP.NET Core Blazor

This article explains how to control the Intermediate Language (IL) Linker for client-side scenarios when building a Blazor app.

Blazor WebAssembly performs [Intermediate Language (IL)](/en-us/dotnet/standard/glossary#il) linking during a build to trim unnecessary IL from the app's output assemblies. The linker is disabled when building in Debug configuration. Apps must build in Release configuration to enable the linker. We recommend building in Release when deploying your Blazor WebAssembly apps.

Linking an app optimizes for size but may have detrimental effects. Apps that use [reflection](/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/) or related dynamic features may break when trimmed because the linker doesn't know about this dynamic behavior and can't determine in general which types are required for reflection at runtime. To trim such apps, the linker must be informed about any types required by reflection in the code and in packages or frameworks that the app depends on.

To ensure the trimmed app works correctly once deployed, it's important to test Release builds of the app frequently while developing.

Linking for Blazor apps can be configured using these MSBuild features:

* Configure linking globally with a [MSBuild property](#control-linking-with-an-msbuild-property).
* Control linking on a per-assembly basis with a [configuration file](#control-linking-with-a-configuration-file).

## Control linking with an MSBuild property

By default, linking is enabled when an app is built in `Release` configuration. To disable linking, configure the `BlazorWebAssemblyEnableLinking` MSBuild property in the project file:

```
<PropertyGroup>
  <BlazorWebAssemblyEnableLinking>false</BlazorWebAssemblyEnableLinking>
</PropertyGroup>
```

## Control linking with a configuration file

Control linking on a per-assembly basis by providing an XML configuration file and specifying the file as a MSBuild item in the project file:

```
<ItemGroup>
  <BlazorLinkerDescriptor Include="LinkerConfig.xml" />
</ItemGroup>
```

`LinkerConfig.xml`:

```
<?xml version="1.0" encoding="UTF-8" ?>
<!--
  This file specifies which parts of the BCL or Blazor packages must not be
  stripped by the IL Linker even if they aren't referenced by user code.
-->
<linker>
  <assembly fullname="mscorlib">
    <!--
      Preserve the methods in WasmRuntime because its methods are called by 
      JavaScript client-side code to implement timers.
      Fixes: https://github.com/dotnet/blazor/issues/239
    -->
    <type fullname="System.Threading.WasmRuntime" />
  </assembly>
  <assembly fullname="System.Core">
    <!--
      System.Linq.Expressions* is required by Json.NET and any 
      expression.Compile caller. The assembly isn't stripped.
    -->
    <type fullname="System.Linq.Expressions*" />
  </assembly>
  <!--
    In this example, the app's entry point assembly is listed. The assembly
    isn't stripped by the IL Linker.
  -->
  <assembly fullname="MyCoolBlazorApp" />
</linker>
```

For more information and examples, see [Data Formats (`dotnet/runtime` GitHub repository)](https://github.com/dotnet/runtime/blob/main/docs/tools/illink/data-formats.md).

## Add an XML linker configuration file to a library

To configure the linker for a specific library, add an XML linker configuration file into the library as an embedded resource. The embedded resource must have the same name as the assembly.

In the following example, the `LinkerConfig.xml` file is specified as an embedded resource that has the same name as the library's assembly:

```
<ItemGroup>
  <EmbeddedResource Include="LinkerConfig.xml">
    <LogicalName>$(MSBuildProjectName).xml</LogicalName>
  </EmbeddedResource>
</ItemGroup>
```

### Configure the linker for internationalization

Blazor's linker configuration for Blazor WebAssembly apps strips out internationalization information except for locales explicitly requested. Removing these assemblies minimizes the app's size.

To control which I18N assemblies are retained, set the `<BlazorWebAssemblyI18NAssemblies>` MSBuild property in the project file:

```
<PropertyGroup>
  <BlazorWebAssemblyI18NAssemblies>{all|none|REGION1,REGION2,...}</BlazorWebAssemblyI18NAssemblies>
</PropertyGroup>
```

| Region Value     | Mono region assembly    |
| ---------------- | ----------------------- |
| `all`            | All assemblies included |
| `cjk`            | `I18N.CJK.dll`          |
| `mideast`        | `I18N.MidEast.dll`      |
| `none` (default) | None                    |
| `other`          | `I18N.Other.dll`        |
| `rare`           | `I18N.Rare.dll`         |
| `west`           | `I18N.West.dll`         |

Use a comma to separate multiple values (for example, `mideast,west`).

For more information, see [I18N: Pnetlib Internationalization Framework Library (mono/mono GitHub repository)](https://github.com/mono/mono/tree/main/mcs/class/I18N).

## Related content

* [ASP.NET Core Blazor app download size performance best practices](../performance/app-download-size?view=aspnetcore-3.1#intermediate-language-il-linking)
* [IL trimmer concepts and related tools (`dotnet/runtime` GitHub repository)](https://github.com/dotnet/runtime/tree/main/docs/tools/illink)
* [Trim self-contained deployments and executables (server-side Blazor apps only)](/en-us/dotnet/core/deploying/trimming/trim-self-contained)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-11-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/host-and-deploy/docker/index.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host ASP.NET Core in Docker containers

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

The following articles are available for learning about hosting ASP.NET Core apps in Docker:

[Introduction to Containers and Docker](/en-us/dotnet/standard/microservices-architecture/container-docker-introduction/index)\
See how containerization is an approach to software development in which an application or service, its dependencies, and its configuration are packaged together as a container image. The image can be tested and then deployed to a host.

[What is Docker](/en-us/dotnet/standard/microservices-architecture/container-docker-introduction/docker-defined)\
Discover how Docker is an open-source project for automating the deployment of apps as portable, self-sufficient containers that can run on the cloud or on-premises.

[Docker Terminology](/en-us/dotnet/standard/microservices-architecture/container-docker-introduction/docker-terminology)\
Learn terms and definitions for Docker technology.

[Docker containers, images, and registries](/en-us/dotnet/standard/microservices-architecture/container-docker-introduction/docker-containers-images-registries)\
Find out how Docker container images are stored in an image registry for consistent deployment across environments.

[Run an ASP.NET Core app in Docker containers](building-net-docker-images?view=aspnetcore-10.0) Learn how to build and dockerize an ASP.NET Core app. Explore Docker images maintained by Microsoft and examine use cases.

[.NET Docker samples](https://github.com/dotnet/dotnet-docker/tree/main/samples) Samples and guidance that demonstrate how to use .NET and Docker for development, testing and production.

[Visual Studio Container Tools](/en-us/visualstudio/containers/add-container-support)\
Discover how Visual Studio supports building, debugging, and running containerized apps, including an ASP.NET Core example. Both Windows and Linux containers are supported.

[Publish to Azure Container Registry](/en-us/azure/vs-azure-tools-docker-hosting-web-apps-in-docker)\
Find out how to use the Visual Studio Container Tools extension to deploy an ASP.NET Core app to a Docker host on Azure using PowerShell.

[Configure ASP.NET Core to work with proxy servers and load balancers](../proxy-load-balancer?view=aspnetcore-10.0)\
Additional configuration might be required for apps hosted behind proxy servers and load balancers. Passing requests through a proxy often obscures information about the original request, such as the scheme and client IP. It might be necessary to forward some information about the request manually to the app.

[GC using Docker and small containers](../../performance/memory?view=aspnetcore-10.0#gc-using-docker-and-small-containers) Discusses GC selection with small containers.

[]()

## System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached

Disabling `reloadOnChange` can significantly reduce the number of opened files. To disable reloading configuration files, set the environment variable `DOTNET_HOSTBUILDER__RELOADCONFIGONCHANGE=false`

For alternative approaches or to leave feedback on this problem, see [this GitHub issue](https://github.com/dotnet/AspNetCore.Docs/issues/19814).

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-08-05

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/web-api-help-pages-using-swagger.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core web API documentation with Swagger / OpenAPI

By [Rico Suter](https://blog.rsuter.com/)

This article covers using **Swagger** tooling — provided by the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) and [NSwag](https://github.com/RicoSuter/NSwag) packages — to generate OpenAPI documentation and interactive help pages for ASP.NET Core web APIs.

In .NET 9 and later, ASP.NET Core includes built-in OpenAPI support that replaces Swashbuckle as the default. Swashbuckle is no longer included in project templates, but it remains available as a community package you can add manually.

* To understand the built‑in OpenAPI features, see [Overview of OpenAPI support in ASP.NET Core API apps](../fundamentals/openapi/overview?view=aspnetcore-8.0\&preserve-view=true\&view=aspnetcore-9.0).
* To add Swagger UI for interactive exploration or local ad‑hoc testing alongside the built-in OpenAPI support, see [Use the generated OpenAPI documents](../fundamentals/openapi/using-openapi-documents?view=aspnetcore-8.0#use-swagger-ui-for-local-ad-hoc-testing).

The following instructions apply to projects using Swashbuckle or NSwag with ASP.NET Core 8.0 and earlier.

[Swagger](https://swagger.io/) ([OpenAPI](https://www.openapis.org/)) is a language-agnostic specification for describing REST APIs. It allows both computers and humans to understand the capabilities of a REST API without direct access to the source code. Its main goals are to:

* Minimize the amount of work needed to connect decoupled services.
* Reduce the amount of time needed to accurately document a service.

The two main OpenAPI implementations for .NET are [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) and [NSwag](https://github.com/RicoSuter/NSwag), see:

* [Getting Started with Swashbuckle](getting-started-with-swashbuckle?view=aspnetcore-8.0)
* [Getting Started with NSwag](getting-started-with-nswag?view=aspnetcore-8.0)

## OpenAPI vs. Swagger

The Swagger project was donated to the OpenAPI Initiative in 2015 and has since been referred to as OpenAPI. Both names are used interchangeably. However, "OpenAPI" refers to the specification. "Swagger" refers to the family of open-source and commercial products from SmartBear that work with the OpenAPI Specification. Subsequent open-source products, such as [OpenAPIGenerator](https://github.com/OpenAPITools/openapi-generator), also fall under the Swagger family name, despite not being released by SmartBear.

In short:

* OpenAPI is a specification.
* Swagger is tooling that uses the OpenAPI specification. For example, OpenAPIGenerator and SwaggerUI.

## OpenAPI specification (`openapi.json`)

The OpenAPI specification is a document that describes the capabilities of your API. The document is based on the [XML](getting-started-with-swashbuckle?view=aspnetcore-8.0#xml-comments) and attribute annotations within the controllers and models. It's the core part of the OpenAPI flow and is used to drive tooling such as SwaggerUI. By default, it's named `openapi.json`. Here's an example of an OpenAPI specification, reduced for brevity:

```
{
  "openapi": "3.0.1",
  "info": {
    "title": "API V1",
    "version": "v1"
  },
  "paths": {
    "/api/Todo": {
      "get": {
        "tags": [
          "Todo"
        ],
        "operationId": "ApiTodoGet",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToDoItem"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToDoItem"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToDoItem"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        …
      }
    },
    "/api/Todo/{id}": {
      "get": {
        …
      },
      "put": {
        …
      },
      "delete": {
        …
      }
    }
  },
  "components": {
    "schemas": {
      "ToDoItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "isCompleted": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      }
    }
  }
}
```

## Swagger UI

[Swagger UI](https://swagger.io/swagger-ui/) offers a web-based UI that provides information about the service, using the generated OpenAPI specification. Both Swashbuckle and NSwag include an embedded version of Swagger UI, so that it can be hosted in your ASP.NET Core app using a middleware registration call. The web UI looks like this:

Each public action method in your controllers can be tested from the UI. Select a method name to expand the section. Add any necessary parameters, and select **Try it out!**.

Note

The Swagger UI version used for the screenshots is version 2. For a version 3 example, see [Petstore example](https://petstore.swagger.io/).

## Securing Swagger UI endpoints

Call [`MapSwagger().RequireAuthorization`](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) to secure the Swagger UI endpoints. The following example secures the swagger endpoints:

```
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();

var app = builder.Build();

  if (app.Environment.IsDevelopment())
  {
    app.UseSwagger();
    app.UseSwaggerUI();
  }

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapSwagger().RequireAuthorization();

app.MapGet("/", () => "Hello, World!");
app.MapGet("/secret", (ClaimsPrincipal user) => $"Hello {user.Identity?.Name}. My secret")
    .RequireAuthorization();

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

In the preceding code, the `/weatherforecast` endpoint doesn't need authorization, but the Swagger endpoints do.

The following Curl passes a JWT token to test the Swagger UI endpoint:

```
curl -i -H "Authorization: Bearer {TOKEN}" https://localhost:{PORT}/swagger/v1/swagger.json
```

where the `{TOKEN}` placeholder is the JWT bearer token and the `{PORT}` placeholder is the port number.

For more information on testing with JWT tokens, see [Generate tokens with dotnet user-jwts](../security/authentication/jwt-authn?view=aspnetcore-8.0).

## Generate an XML documentation file at compile time

See [GenerateDocumentationFile](/en-us/dotnet/core/project-sdk/msbuild-props#generatedocumentationfile) for more information.

## Next steps

* [Get started with Swashbuckle](getting-started-with-swashbuckle?view=aspnetcore-8.0)
* [Get started with NSwag](getting-started-with-nswag?view=aspnetcore-8.0)

By [Rico Suter](https://blog.rsuter.com/)

Swagger (OpenAPI) is a language-agnostic specification for describing REST APIs. It allows both computers and humans to understand the capabilities of a REST API without direct access to the source code. Its main goals are to:

* Minimize the amount of work needed to connect decoupled services.
* Reduce the amount of time needed to accurately document a service.

The two main OpenAPI implementations for .NET are [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) and [NSwag](https://github.com/RicoSuter/NSwag), see:

* [Getting Started with Swashbuckle](getting-started-with-swashbuckle?view=aspnetcore-8.0)
* [Getting Started with NSwag](getting-started-with-nswag?view=aspnetcore-8.0)

## OpenAPI vs. Swagger

The Swagger project was donated to the OpenAPI Initiative in 2015 and has since been referred to as OpenAPI. Both names are used interchangeably. However, "OpenAPI" refers to the specification. "Swagger" refers to the family of open-source and commercial products from SmartBear that work with the OpenAPI Specification. Subsequent open-source products, such as [OpenAPIGenerator](https://github.com/OpenAPITools/openapi-generator), also fall under the Swagger family name, despite not being released by SmartBear.

In short:

* OpenAPI is a specification.
* Swagger is tooling that uses the OpenAPI specification. For example, OpenAPIGenerator and SwaggerUI.

## OpenAPI specification (`openapi.json`)

The OpenAPI specification is a document that describes the capabilities of your API. The document is based on the [XML](getting-started-with-swashbuckle?view=aspnetcore-8.0#xml-comments) and attribute annotations within the controllers and models. It's the core part of the OpenAPI flow and is used to drive tooling such as SwaggerUI. By default, it's named `openapi.json`. Here's an example of an OpenAPI specification, reduced for brevity:

```
{
  "openapi": "3.0.1",
  "info": {
    "title": "API V1",
    "version": "v1"
  },
  "paths": {
    "/api/Todo": {
      "get": {
        "tags": [
          "Todo"
        ],
        "operationId": "ApiTodoGet",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToDoItem"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToDoItem"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ToDoItem"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        …
      }
    },
    "/api/Todo/{id}": {
      "get": {
        …
      },
      "put": {
        …
      },
      "delete": {
        …
      }
    }
  },
  "components": {
    "schemas": {
      "ToDoItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "isCompleted": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      }
    }
  }
}
```

## Swagger UI

[Swagger UI](https://swagger.io/swagger-ui/) offers a web-based UI that provides information about the service, using the generated OpenAPI specification. Both Swashbuckle and NSwag include an embedded version of Swagger UI, so that it can be hosted in your ASP.NET Core app using a middleware registration call. The web UI looks like this:

Each public action method in your controllers can be tested from the UI. Select a method name to expand the section. Add any necessary parameters, and select **Try it out!**.

Note

The Swagger UI version used for the screenshots is version 2. For a version 3 example, see [Petstore example](https://petstore.swagger.io/).

## Next steps

* [Get started with Swashbuckle](getting-started-with-swashbuckle?view=aspnetcore-8.0)
* [Get started with NSwag](getting-started-with-nswag?view=aspnetcore-8.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-23

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0016
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0016.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0016: Do not return a value from RequestDelegate

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0016      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

A method used to create a [RequestDelegate](/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate) returns `Task<T>`. `RequestDelegate` discards this value.

## Rule description

Do not return a value `Delegate`s provided to APIs that expect `RequestDelegate`. For example, the following sample returns a `Task<string>` where the `string` value of the `Task` will be discarded.

```
var app = WebApplication.Create();
app.Use(next =>
{
    return new RequestDelegate((HttpContext context) =>
    {
        return Task.FromResult(""hello world"");
    });
});
```

## How to fix violations

To fix a violation of this rule, change the return type to non-generic `Task` or, if the delegate is a route handler, cast it to `Delegate` so the return value is written to the response.

## When to suppress warnings

Do not suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-07-15

----
url: https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/breaking-changes/11/blazor-server-side-rendering-deferred-cross-site-request-forgery-protection.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Blazor server-side rendering defers antiforgery validation to middleware

In ASP.NET Core 11, Blazor static server-side rendering (SSR) endpoints no longer validate antiforgery tokens themselves. They rely on the antiforgery middleware to record a validation result, and they generate antiforgery tokens only when the token-based antiforgery middleware is present in the pipeline.

## Version introduced

.NET 11

## Previous behavior

Previously, when a Blazor SSR endpoint handled a form `POST`, the Razor Components endpoint validated the request itself. If the upstream antiforgery middleware hadn't already recorded a result, the endpoint called `IAntiforgery` directly to validate the request's antiforgery token. The endpoint also always generated and stored antiforgery tokens for rendered forms, whether or not `app.UseAntiforgery()` was in the pipeline.

As a result, a Blazor SSR app that didn't call `app.UseAntiforgery()` still had its form posts validated against antiforgery tokens by the endpoint, and still emitted tokens for its forms.

## New behavior

Starting in ASP.NET Core 11, the Razor Components endpoint trusts the antiforgery verdict recorded on the request's `IAntiforgeryValidationFeature` by upstream middleware. For a form `POST`, it returns `400 Bad Request` only when the recorded verdict is invalid, and it no longer calls `IAntiforgery` to validate the request itself. The endpoint generates antiforgery tokens only when the token-based antiforgery middleware ran for the request; when that middleware isn't present, the endpoint skips token generation.

The verdict can be recorded by either of two middleware components:

* The token-based antiforgery middleware that `app.UseAntiforgery()` adds.
* The automatic cross-origin CSRF protection middleware that's injected by default in apps built with `WebApplication.CreateBuilder` (new in .NET 11).

The impact depends on the app's configuration:

* Apps that call `app.UseAntiforgery()` are unaffected. Requests are validated against antiforgery tokens, and tokens are generated for forms, exactly as before.
* Apps that don't call `app.UseAntiforgery()` are now protected by the automatic CSRF protection middleware instead of by token validation in the endpoint. These apps no longer emit antiforgery tokens for their forms.

## Type of breaking change

This change is a [behavioral change](/en-us/dotnet/core/compatibility/categories#behavioral-change).

## Reason for change

The token-based antiforgery system and the new cross-origin CSRF protection now record a single validation result on the shared `IAntiforgeryValidationFeature`, which form-consuming components read to decide whether to reject a request. Having the Razor Components endpoint validate the request a second time duplicated that work and could produce a result that differed from the middleware's. Generating tokens when no antiforgery middleware was present produced tokens that nothing validated.

For more information, see [dotnet/aspnetcore#67082](https://github.com/dotnet/aspnetcore/pull/67082).

## Recommended action

If your Blazor SSR app relies on antiforgery tokens — for example, to validate form posts or to render tokens into forms — make sure it calls `app.UseAntiforgery()`:

```
var app = builder.Build();

app.UseAntiforgery();
```

Apps that call `app.UseAntiforgery()`, directly or implicitly through `AddRazorComponents`, require no changes.

If you intentionally removed `app.UseAntiforgery()` and want to rely on the automatic cross-origin CSRF protection instead, no action is required. Be aware that antiforgery tokens are no longer generated for your forms, and cross-origin form posts are rejected based on `Sec-Fetch-Site` and `Origin` rather than tokens. For more information, see [Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery?view=aspnetcore-10.0) and [Migrate from ASP.NET Core in .NET 10 to ASP.NET Core in .NET 11](../../migration/100-to-110?view=aspnetcore-10.0).

## Affected APIs

None. No public API surface changed. The change affects the behavior of Blazor static server-side rendering endpoints and the antiforgery state provider that generates tokens.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-21

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/mvc1004
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/mvc1004.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# MVC1004: Rename model bound parameter

|                                     | Value    |
| ----------------------------------- | -------- |
| **Rule ID**                         | MVC1004  |
| **Fix is breaking or non-breaking** | Breaking |

## Cause

A model bound parameter has the same name as one of its properties.

### Rule description

Model binding a complex parameter with a property that has the same name may result in unexpected binding behavior. Consider renaming the parameter, or using a binding attribute to specify a different name.

Consider the following code:

```
public class HomeController : Controller
{
    public IActionResult Get(SearchModel search)
    {
        ...
    }
}

public class SearcModel
{
    public string Search { get; set; }
}
```

In this model, the parameter and its property are both named `Search`, which results in model binding attempting to bind the property as `search.Search`. Naming a parameter and its property the same prevents binding to a value without a prefix such as a query that looks like `?search=MySearchTerm`.

## How to fix violations

* Rename the parameter if its prefix is not used during binding:

```
public IActionResult Get(SearchModel model)
{
    ...
}
```

Renaming a parameter on a public type could be considered a breaking change since it changes a library's public API surface.

* If this is problematic, consider using a model binding attribute such as `Bind` to specify the model binding prefix:

```
public IActionResult Get([Bind(Prefix = "")] SearchModel search)
{
    ...
}
```

## When to suppress warnings

Warnings can be suppressed if you intend to use the parameter name as a prefix during model binding.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2025-08-08

----
url: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/validation
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/tutorials/first-mvc-app/validation.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Part 9, add validation to an ASP.NET Core MVC app

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

In this section:

* Validation logic is added to the `Movie` model.
* You ensure that the validation rules are enforced any time a user creates or edits a movie.

## Keeping things DRY

One of the design tenets of MVC is [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("Don't Repeat Yourself"). ASP.NET Core MVC encourages you to specify functionality or behavior only once, and then have it be reflected everywhere in an app. This reduces the amount of code you need to write and makes the code you do write less error prone, easier to test, and easier to maintain.

The validation support provided by MVC and Entity Framework Core is a good example of the DRY principle in action. You can declaratively specify validation rules in one place (in the model class) and the rules are enforced everywhere in the app.

## Delete the previously edited data

In the next step, validation rules are added that don't allow null values. Run the app, navigate to `/Movies/Index`, delete all listed movies, and stop the app. The app will use the seed data the next time it is run.

## Add validation rules to the movie model

The DataAnnotations namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like `DataType` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in validation attributes `Required`, `StringLength`, `RegularExpression`, `Range` and the `DataType` formatting attribute.

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string? Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }    

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
    [Required]
    [StringLength(30)]
    public string? Genre { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string? Rating { get; set; }
}
```

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:

* The `Required` and `MinimumLength` attributes indicate that a property must have a value; but nothing prevents a user from entering white space to satisfy this validation.

* The `RegularExpression` attribute is used to limit what characters can be input. In the preceding code, "Genre":

  * Must only use letters.
  * The first letter is required to be uppercase. White spaces are allowed while numbers, and special characters are not allowed.

* The `RegularExpression` "Rating":

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a "Genre".

* The `Range` attribute constrains a value to within a specified range.

* The `StringLength` attribute lets you set the maximum length of a string property, and optionally its minimum length.

* Value types (such as `decimal`, `int`, `float`, `DateTime`) are inherently required and don't need the `[Required]` attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures that you can't forget to validate something and inadvertently let bad data into the database.

## Validation Error UI

Run the app and navigate to the Movies controller.

Select the **Create New** link to add a new movie. Fill out the form with some invalid values. As soon as jQuery client side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered an appropriate validation error message in each field containing an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a user has JavaScript disabled).

A significant benefit is that you didn't need to change a single line of code in the `MoviesController` class or in the `Create.cshtml` view in order to enable this validation UI. The controller and views you created earlier in this tutorial automatically picked up the validation rules that you specified by using validation attributes on the properties of the `Movie` model class. Test validation using the `Edit` action method, and the same validation is applied.

The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a break point in the `HTTP Post` method, by using the [Fiddler tool](https://www.telerik.com/fiddler) , or the [F12 Developer tools](/en-us/microsoft-edge/devtools-guide).

## How validation works

You might wonder how the validation UI was generated without any updates to the code in the controller or views. The following code shows the two `Create` methods.

```
// GET: Movies/Create
public IActionResult Create()
{
    return View();
}

// POST: Movies/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (ModelState.IsValid)
    {
        _context.Add(movie);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The first (HTTP GET) `Create` action method displays the initial Create form. The second (`[HttpPost]`) version handles the form post. The second `Create` method (The `[HttpPost]` version) calls `ModelState.IsValid` to check whether the movie has any validation errors. Calling this method evaluates any validation attributes that have been applied to the object. If the object has validation errors, the `Create` method re-displays the form. If there are no errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the server when there are validation errors detected on the client side; the second `Create` method is never called when there are client side validation errors. If you disable JavaScript in your browser, client validation is disabled and you can test the HTTP POST `Create` method `ModelState.IsValid` detecting any validation errors.

You can set a break point in the `[HttpPost] Create` method and verify the method is never called, client side validation won't submit the form data when validation errors are detected. If you disable JavaScript in your browser, then submit the form with errors, the break point will be hit. You still get full validation without JavaScript.

The following image shows how to disable JavaScript in the Chrome browser. Search in the settings for how to disable JavaScript in the browser that you use.

After you disable JavaScript, post invalid data and step through the debugger.

A portion of the `Create.cshtml` view template is shown in the following markup:

```
<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>

            @*Markup removed for brevity.*@
```

The preceding markup is used by the action methods to display the initial form and to redisplay it in the event of an error.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

What's really nice about this approach is that neither the controller nor the `Create` view template knows anything about the actual validation rules being enforced or about the specific error messages displayed. The validation rules and the error strings are specified only in the `Movie` class. These same validation rules are automatically applied to the `Edit` view and any other views templates you might create that edit your model.

When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the model (in this example, the `Movie` class). You won't have to worry about different parts of the application being inconsistent with how the rules are enforced — all validation logic will be defined in one place and used everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be fully honoring the DRY principle.

## Using DataType Attributes

Open the `Movie.cs` file and examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. We've already applied a `DataType` enumeration value to the release date and to the price fields. The following code shows the `ReleaseDate` and `Price` properties with the appropriate `DataType` attribute.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }    
```

The `DataType` attributes only provide hints for the view engine to format the data and supplies elements/attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email. You can use the `RegularExpression` attribute to validate the format of the data. The `DataType` attribute is used to specify a data type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want to keep track of the date, not the time. The `DataType` Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress and more. The `DataType` attribute can also enable the application to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`, and a date selector can be provided for `DataType.Date` in browsers that support HTML5. The `DataType` attributes emit HTML 5 `data-` (pronounced data dash) attributes that HTML 5 browsers can understand. The `DataType` attributes do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `DisplayFormat` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied when the value is displayed in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably don't want the currency symbol in the text box for editing.)

You can use the `DisplayFormat` attribute by itself, but it's generally a good idea to use the `DataType` attribute. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the following benefits that you don't get with DisplayFormat:

* The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.)

* By default, the browser will render data using the correct format based on your locale.

* The `DataType` attribute can enable MVC to choose the right field template to render the data (the `DisplayFormat` if used by itself uses the string template).

## Apply migrations

Some validation attributes affect the database schema. For example, `[Required]` makes a column `NOT NULL` and `[StringLength(60)]` limits the column type to `nvarchar(60)`. Run a migration to keep the database schema consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

For more information, see [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations).

## Related content

* [Part 8, Add a new field (EF Core migrations)](new-field?view=aspnetcore-10.0)
* [Working with Forms](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

[Previous](new-field?view=aspnetcore-10.0) [Next](details?view=aspnetcore-10.0)

In this section:

* Validation logic is added to the `Movie` model.
* You ensure that the validation rules are enforced any time a user creates or edits a movie.

## Keeping things DRY

One of the design tenets of MVC is [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("Don't Repeat Yourself"). ASP.NET Core MVC encourages you to specify functionality or behavior only once, and then have it be reflected everywhere in an app. This reduces the amount of code you need to write and makes the code you do write less error prone, easier to test, and easier to maintain.

The validation support provided by MVC and Entity Framework Core is a good example of the DRY principle in action. You can declaratively specify validation rules in one place (in the model class) and the rules are enforced everywhere in the app.

## Delete the previously edited data

In the next step, validation rules are added that don't allow null values. Run the app, navigate to `/Movies/Index`, delete all listed movies, and stop the app. The app will use the seed data the next time it is run.

## Add validation rules to the movie model

The DataAnnotations namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like `DataType` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in validation attributes `Required`, `StringLength`, `RegularExpression`, `Range` and the `DataType` formatting attribute.

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string? Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }    

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
    [Required]
    [StringLength(30)]
    public string? Genre { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string? Rating { get; set; }
}
```

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:

* The `Required` and `MinimumLength` attributes indicate that a property must have a value; but nothing prevents a user from entering white space to satisfy this validation.

* The `RegularExpression` attribute is used to limit what characters can be input. In the preceding code, "Genre":

  * Must only use letters.
  * The first letter is required to be uppercase. White spaces are allowed while numbers, and special characters are not allowed.

* The `RegularExpression` "Rating":

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a "Genre".

* The `Range` attribute constrains a value to within a specified range.

* The `StringLength` attribute lets you set the maximum length of a string property, and optionally its minimum length.

* Value types (such as `decimal`, `int`, `float`, `DateTime`) are inherently required and don't need the `[Required]` attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures that you can't forget to validate something and inadvertently let bad data into the database.

## Validation Error UI

Run the app and navigate to the Movies controller.

Select the **Create New** link to add a new movie. Fill out the form with some invalid values. As soon as jQuery client side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered an appropriate validation error message in each field containing an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a user has JavaScript disabled).

A significant benefit is that you didn't need to change a single line of code in the `MoviesController` class or in the `Create.cshtml` view in order to enable this validation UI. The controller and views you created earlier in this tutorial automatically picked up the validation rules that you specified by using validation attributes on the properties of the `Movie` model class. Test validation using the `Edit` action method, and the same validation is applied.

The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a break point in the `HTTP Post` method, by using the [Fiddler tool](https://www.telerik.com/fiddler) , or the [F12 Developer tools](/en-us/microsoft-edge/devtools-guide).

## How validation works

You might wonder how the validation UI was generated without any updates to the code in the controller or views. The following code shows the two `Create` methods.

```
// GET: Movies/Create
public IActionResult Create()
{
    return View();
}

// POST: Movies/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (ModelState.IsValid)
    {
        _context.Add(movie);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The first (HTTP GET) `Create` action method displays the initial Create form. The second (`[HttpPost]`) version handles the form post. The second `Create` method (The `[HttpPost]` version) calls `ModelState.IsValid` to check whether the movie has any validation errors. Calling this method evaluates any validation attributes that have been applied to the object. If the object has validation errors, the `Create` method re-displays the form. If there are no errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the server when there are validation errors detected on the client side; the second `Create` method is never called when there are client side validation errors. If you disable JavaScript in your browser, client validation is disabled and you can test the HTTP POST `Create` method `ModelState.IsValid` detecting any validation errors.

You can set a break point in the `[HttpPost] Create` method and verify the method is never called, client side validation won't submit the form data when validation errors are detected. If you disable JavaScript in your browser, then submit the form with errors, the break point will be hit. You still get full validation without JavaScript.

The following image shows how to disable JavaScript in the Firefox browser.

The following image shows how to disable JavaScript in the Chrome browser.

After you disable JavaScript, post invalid data and step through the debugger.

A portion of the `Create.cshtml` view template is shown in the following markup:

```
<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>

            @*Markup removed for brevity.*@
```

The preceding markup is used by the action methods to display the initial form and to redisplay it in the event of an error.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

What's really nice about this approach is that neither the controller nor the `Create` view template knows anything about the actual validation rules being enforced or about the specific error messages displayed. The validation rules and the error strings are specified only in the `Movie` class. These same validation rules are automatically applied to the `Edit` view and any other views templates you might create that edit your model.

When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the model (in this example, the `Movie` class). You won't have to worry about different parts of the application being inconsistent with how the rules are enforced — all validation logic will be defined in one place and used everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be fully honoring the DRY principle.

## Using DataType Attributes

Open the `Movie.cs` file and examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. We've already applied a `DataType` enumeration value to the release date and to the price fields. The following code shows the `ReleaseDate` and `Price` properties with the appropriate `DataType` attribute.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }    
```

The `DataType` attributes only provide hints for the view engine to format the data and supplies elements/attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email. You can use the `RegularExpression` attribute to validate the format of the data. The `DataType` attribute is used to specify a data type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want to keep track of the date, not the time. The `DataType` Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress and more. The `DataType` attribute can also enable the application to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`, and a date selector can be provided for `DataType.Date` in browsers that support HTML5. The `DataType` attributes emit HTML 5 `data-` (pronounced data dash) attributes that HTML 5 browsers can understand. The `DataType` attributes do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `DisplayFormat` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied when the value is displayed in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably don't want the currency symbol in the text box for editing.)

You can use the `DisplayFormat` attribute by itself, but it's generally a good idea to use the `DataType` attribute. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the following benefits that you don't get with DisplayFormat:

* The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.)

* By default, the browser will render data using the correct format based on your locale.

* The `DataType` attribute can enable MVC to choose the right field template to render the data (the `DisplayFormat` if used by itself uses the string template).

Note

jQuery validation doesn't work with the `Range` attribute and `DateTime`. For example, the following code will always display a client side validation error, even when the date is in the specified range:

`[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]`

You will need to disable jQuery date validation to use the `Range` attribute with `DateTime`. It's generally not a good practice to compile hard dates in your models, so using the `Range` attribute and `DateTime` is discouraged.

The following code shows combining attributes on one line:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    [StringLength(60, MinimumLength = 3)]
    public string? Title { get; set; }
    [Display(Name = "Release Date"), DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
    public string? Genre { get; set; }
    [Range(1, 100), DataType(DataType.Currency), Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string? Rating { get; set; }
}
```

## Apply migrations

Some validation attributes affect the database schema. For example, `[Required]` makes a column `NOT NULL` and `[StringLength(60)]` limits the column type to `nvarchar(60)`. Run a migration to keep the database schema consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

For more information, see [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations).

In the next part of the series, we review the app and make some improvements to the automatically generated `Details` and `Delete` methods.

## Related content

* [Part 8, Add a new field (EF Core migrations)](new-field?view=aspnetcore-10.0)
* [Working with Forms](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

[Previous](new-field?view=aspnetcore-10.0) [Next](details?view=aspnetcore-10.0)

In this section:

* Validation logic is added to the `Movie` model.
* You ensure that the validation rules are enforced any time a user creates or edits a movie.

## Keeping things DRY

One of the design tenets of MVC is [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("Don't Repeat Yourself"). ASP.NET Core MVC encourages you to specify functionality or behavior only once, and then have it be reflected everywhere in an app. This reduces the amount of code you need to write and makes the code you do write less error prone, easier to test, and easier to maintain.

The validation support provided by MVC and Entity Framework Core Code First is a good example of the DRY principle in action. You can declaratively specify validation rules in one place (in the model class) and the rules are enforced everywhere in the app.

## Delete the previously edited data

In the next step, validation rules are added that don't allow null values. Run the app, navigate to `/Movies/Index`, delete all listed movies, and stop the app. The app will use the seed data the next time it is run.

## Add validation rules to the movie model

The DataAnnotations namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like `DataType` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in validation attributes `Required`, `StringLength`, `RegularExpression`, `Range` and the `DataType` formatting attribute.

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string? Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }    

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
    [Required]
    [StringLength(30)]
    public string? Genre { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string? Rating { get; set; }
}
```

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:

* The `Required` and `MinimumLength` attributes indicate that a property must have a value; but nothing prevents a user from entering white space to satisfy this validation.

* The `RegularExpression` attribute is used to limit what characters can be input. In the preceding code, "Genre":

  * Must only use letters.
  * The first letter is required to be uppercase. White spaces are allowed while numbers, and special characters are not allowed.

* The `RegularExpression` "Rating":

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a "Genre".

* The `Range` attribute constrains a value to within a specified range.

* The `StringLength` attribute lets you set the maximum length of a string property, and optionally its minimum length.

* Value types (such as `decimal`, `int`, `float`, `DateTime`) are inherently required and don't need the `[Required]` attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures that you can't forget to validate something and inadvertently let bad data into the database.

## Validation Error UI

Run the app and navigate to the Movies controller.

Select the **Create New** link to add a new movie. Fill out the form with some invalid values. As soon as jQuery client side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered an appropriate validation error message in each field containing an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a user has JavaScript disabled).

A significant benefit is that you didn't need to change a single line of code in the `MoviesController` class or in the `Create.cshtml` view in order to enable this validation UI. The controller and views you created earlier in this tutorial automatically picked up the validation rules that you specified by using validation attributes on the properties of the `Movie` model class. Test validation using the `Edit` action method, and the same validation is applied.

The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a break point in the `HTTP Post` method, by using the [Fiddler tool](https://www.telerik.com/fiddler) , or the [F12 Developer tools](/en-us/microsoft-edge/devtools-guide).

## How validation works

You might wonder how the validation UI was generated without any updates to the code in the controller or views. The following code shows the two `Create` methods.

```
// GET: Movies/Create
public IActionResult Create()
{
    return View();
}

// POST: Movies/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (ModelState.IsValid)
    {
        _context.Add(movie);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The first (HTTP GET) `Create` action method displays the initial Create form. The second (`[HttpPost]`) version handles the form post. The second `Create` method (The `[HttpPost]` version) calls `ModelState.IsValid` to check whether the movie has any validation errors. Calling this method evaluates any validation attributes that have been applied to the object. If the object has validation errors, the `Create` method re-displays the form. If there are no errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the server when there are validation errors detected on the client side; the second `Create` method is never called when there are client side validation errors. If you disable JavaScript in your browser, client validation is disabled and you can test the HTTP POST `Create` method `ModelState.IsValid` detecting any validation errors.

You can set a break point in the `[HttpPost] Create` method and verify the method is never called, client side validation won't submit the form data when validation errors are detected. If you disable JavaScript in your browser, then submit the form with errors, the break point will be hit. You still get full validation without JavaScript.

The following image shows how to disable JavaScript in the Firefox browser.

The following image shows how to disable JavaScript in the Chrome browser.

After you disable JavaScript, post invalid data and step through the debugger.

A portion of the `Create.cshtml` view template is shown in the following markup:

```
<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>

            @*Markup removed for brevity.*@
```

The preceding markup is used by the action methods to display the initial form and to redisplay it in the event of an error.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

What's really nice about this approach is that neither the controller nor the `Create` view template knows anything about the actual validation rules being enforced or about the specific error messages displayed. The validation rules and the error strings are specified only in the `Movie` class. These same validation rules are automatically applied to the `Edit` view and any other views templates you might create that edit your model.

When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the model (in this example, the `Movie` class). You won't have to worry about different parts of the application being inconsistent with how the rules are enforced — all validation logic will be defined in one place and used everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be fully honoring the DRY principle.

## Using DataType Attributes

Open the `Movie.cs` file and examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. We've already applied a `DataType` enumeration value to the release date and to the price fields. The following code shows the `ReleaseDate` and `Price` properties with the appropriate `DataType` attribute.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }    
```

The `DataType` attributes only provide hints for the view engine to format the data and supplies elements/attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email. You can use the `RegularExpression` attribute to validate the format of the data. The `DataType` attribute is used to specify a data type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want to keep track of the date, not the time. The `DataType` Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress and more. The `DataType` attribute can also enable the application to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`, and a date selector can be provided for `DataType.Date` in browsers that support HTML5. The `DataType` attributes emit HTML 5 `data-` (pronounced data dash) attributes that HTML 5 browsers can understand. The `DataType` attributes do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `DisplayFormat` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied when the value is displayed in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably don't want the currency symbol in the text box for editing.)

You can use the `DisplayFormat` attribute by itself, but it's generally a good idea to use the `DataType` attribute. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the following benefits that you don't get with DisplayFormat:

* The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.)

* By default, the browser will render data using the correct format based on your locale.

* The `DataType` attribute can enable MVC to choose the right field template to render the data (the `DisplayFormat` if used by itself uses the string template).

Note

jQuery validation doesn't work with the `Range` attribute and `DateTime`. For example, the following code will always display a client side validation error, even when the date is in the specified range:

`[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]`

You will need to disable jQuery date validation to use the `Range` attribute with `DateTime`. It's generally not a good practice to compile hard dates in your models, so using the `Range` attribute and `DateTime` is discouraged.

The following code shows combining attributes on one line:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; }
    [Display(Name = "Release Date"), DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
    public string Genre { get; set; }
    [Range(1, 100), DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string Rating { get; set; }
}
```

## Apply migrations

Some validation attributes affect the database schema. For example, `[Required]` makes a column `NOT NULL` and `[StringLength(60)]` limits the column type to `nvarchar(60)`. Run a migration to keep the database schema consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

For more information, see [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations).

In the next part of the series, we review the app and make some improvements to the automatically generated `Details` and `Delete` methods.

## Related content

* [Part 8, Add a new field (EF Core migrations)](new-field?view=aspnetcore-10.0)
* [Working with Forms](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

[Previous](new-field?view=aspnetcore-10.0) [Next](details?view=aspnetcore-10.0)

In this section:

* Validation logic is added to the `Movie` model.
* You ensure that the validation rules are enforced any time a user creates or edits a movie.

## Keeping things DRY

One of the design tenets of MVC is [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("Don't Repeat Yourself"). ASP.NET Core MVC encourages you to specify functionality or behavior only once, and then have it be reflected everywhere in an app. This reduces the amount of code you need to write and makes the code you do write less error prone, easier to test, and easier to maintain.

The validation support provided by MVC and Entity Framework Core Code First is a good example of the DRY principle in action. You can declaratively specify validation rules in one place (in the model class) and the rules are enforced everywhere in the app.

## Add validation rules to the movie model

The DataAnnotations namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like `DataType` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in validation attributes `Required`, `StringLength`, `RegularExpression`, `Range` and the `DataType` formatting attribute.

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string? Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }    

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
    [Required]
    [StringLength(30)]
    public string? Genre { get; set; }
    
    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string? Rating { get; set; }
}
```

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:

* The `Required` and `MinimumLength` attributes indicate that a property must have a value; but nothing prevents a user from entering white space to satisfy this validation.

* The `RegularExpression` attribute is used to limit what characters can be input. In the preceding code, "Genre":

  * Must only use letters.
  * The first letter is required to be uppercase. White spaces are allowed while numbers, and special characters are not allowed.

* The `RegularExpression` "Rating":

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a "Genre".

* The `Range` attribute constrains a value to within a specified range.

* The `StringLength` attribute lets you set the maximum length of a string property, and optionally its minimum length.

* Value types (such as `decimal`, `int`, `float`, `DateTime`) are inherently required and don't need the `[Required]` attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures that you can't forget to validate something and inadvertently let bad data into the database.

## Validation Error UI

Run the app and navigate to the Movies controller.

Select the **Create New** link to add a new movie. Fill out the form with some invalid values. As soon as jQuery client side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered an appropriate validation error message in each field containing an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a user has JavaScript disabled).

A significant benefit is that you didn't need to change a single line of code in the `MoviesController` class or in the `Create.cshtml` view in order to enable this validation UI. The controller and views you created earlier in this tutorial automatically picked up the validation rules that you specified by using validation attributes on the properties of the `Movie` model class. Test validation using the `Edit` action method, and the same validation is applied.

The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a break point in the `HTTP Post` method, by using the [Fiddler tool](https://www.telerik.com/fiddler) , or the [F12 Developer tools](/en-us/microsoft-edge/devtools-guide).

## How validation works

You might wonder how the validation UI was generated without any updates to the code in the controller or views. The following code shows the two `Create` methods.

```
// GET: Movies/Create
public IActionResult Create()
{
    return View();
}

// POST: Movies/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (ModelState.IsValid)
    {
        _context.Add(movie);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The first (HTTP GET) `Create` action method displays the initial Create form. The second (`[HttpPost]`) version handles the form post. The second `Create` method (The `[HttpPost]` version) calls `ModelState.IsValid` to check whether the movie has any validation errors. Calling this method evaluates any validation attributes that have been applied to the object. If the object has validation errors, the `Create` method re-displays the form. If there are no errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the server when there are validation errors detected on the client side; the second `Create` method is never called when there are client side validation errors. If you disable JavaScript in your browser, client validation is disabled and you can test the HTTP POST `Create` method `ModelState.IsValid` detecting any validation errors.

You can set a break point in the `[HttpPost] Create` method and verify the method is never called, client side validation won't submit the form data when validation errors are detected. If you disable JavaScript in your browser, then submit the form with errors, the break point will be hit. You still get full validation without JavaScript.

The following image shows how to disable JavaScript in the Firefox browser.

The following image shows how to disable JavaScript in the Chrome browser.

After you disable JavaScript, post invalid data and step through the debugger.

A portion of the `Create.cshtml` view template is shown in the following markup:

```
<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>

            @*Markup removed for brevity.*@
```

The preceding markup is used by the action methods to display the initial form and to redisplay it in the event of an error.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

What's really nice about this approach is that neither the controller nor the `Create` view template knows anything about the actual validation rules being enforced or about the specific error messages displayed. The validation rules and the error strings are specified only in the `Movie` class. These same validation rules are automatically applied to the `Edit` view and any other views templates you might create that edit your model.

When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the model (in this example, the `Movie` class). You won't have to worry about different parts of the application being inconsistent with how the rules are enforced — all validation logic will be defined in one place and used everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be fully honoring the DRY principle.

## Using DataType Attributes

Open the `Movie.cs` file and examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. We've already applied a `DataType` enumeration value to the release date and to the price fields. The following code shows the `ReleaseDate` and `Price` properties with the appropriate `DataType` attribute.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }    
```

The `DataType` attributes only provide hints for the view engine to format the data and supplies elements/attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email. You can use the `RegularExpression` attribute to validate the format of the data. The `DataType` attribute is used to specify a data type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want to keep track of the date, not the time. The `DataType` Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress and more. The `DataType` attribute can also enable the application to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`, and a date selector can be provided for `DataType.Date` in browsers that support HTML5. The `DataType` attributes emit HTML 5 `data-` (pronounced data dash) attributes that HTML 5 browsers can understand. The `DataType` attributes do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `DisplayFormat` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied when the value is displayed in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably don't want the currency symbol in the text box for editing.)

You can use the `DisplayFormat` attribute by itself, but it's generally a good idea to use the `DataType` attribute. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the following benefits that you don't get with DisplayFormat:

* The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.)

* By default, the browser will render data using the correct format based on your locale.

* The `DataType` attribute can enable MVC to choose the right field template to render the data (the `DisplayFormat` if used by itself uses the string template).

Note

jQuery validation doesn't work with the `Range` attribute and `DateTime`. For example, the following code will always display a client side validation error, even when the date is in the specified range:

`[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]`

You will need to disable jQuery date validation to use the `Range` attribute with `DateTime`. It's generally not a good practice to compile hard dates in your models, so using the `Range` attribute and `DateTime` is discouraged.

The following code shows combining attributes on one line:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; }
    [Display(Name = "Release Date"), DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
    public string Genre { get; set; }
    [Range(1, 100), DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }
    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string Rating { get; set; }
}
```

## Apply migrations

Some validation attributes affect the database schema. For example, `[Required]` makes a column `NOT NULL` and `[StringLength(60)]` limits the column type to `nvarchar(60)`. Run a migration to keep the database schema consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

## For more information, see [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations).

In the next part of the series, we review the app and make some improvements to the automatically generated `Details` and `Delete` methods.

## Related content

* [Part 8, Add a new field (EF Core migrations)](new-field?view=aspnetcore-10.0)
* [Working with Forms](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

[Previous](new-field?view=aspnetcore-10.0) [Next](details?view=aspnetcore-10.0)

In this section:

* Validation logic is added to the `Movie` model.
* You ensure that the validation rules are enforced any time a user creates or edits a movie.

## Keeping things DRY

One of the design tenets of MVC is [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("Don't Repeat Yourself"). ASP.NET Core MVC encourages you to specify functionality or behavior only once, and then have it be reflected everywhere in an app. This reduces the amount of code you need to write and makes the code you do write less error prone, easier to test, and easier to maintain.

The validation support provided by MVC and Entity Framework Core Code First is a good example of the DRY principle in action. You can declaratively specify validation rules in one place (in the model class) and the rules are enforced everywhere in the app.

## Add validation rules to the movie model

The DataAnnotations namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like `DataType` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in `Required`, `StringLength`, `RegularExpression`, and `Range` validation attributes.

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string? Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string? Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string? Rating { get; set; }
    }
}
```

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:

* The `Required` and `MinimumLength` attributes indicate that a property must have a value; but nothing prevents a user from entering white space to satisfy this validation.

* The `RegularExpression` attribute is used to limit what characters can be input. In the preceding code, "Genre":

  * Must only use letters.
  * The first letter is required to be uppercase. White spaces are allowed while numbers, and special characters are not allowed.

* The `RegularExpression` "Rating":

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a "Genre".

* The `Range` attribute constrains a value to within a specified range.

* The `StringLength` attribute lets you set the maximum length of a string property, and optionally its minimum length.

* Value types (such as `decimal`, `int`, `float`, `DateTime`) are inherently required and don't need the `[Required]` attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures that you can't forget to validate something and inadvertently let bad data into the database.

## Validation Error UI

Run the app and navigate to the Movies controller.

Select the **Create New** link to add a new movie. Fill out the form with some invalid values. As soon as jQuery client side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered an appropriate validation error message in each field containing an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a user has JavaScript disabled).

A significant benefit is that you didn't need to change a single line of code in the `MoviesController` class or in the `Create.cshtml` view in order to enable this validation UI. The controller and views you created earlier in this tutorial automatically picked up the validation rules that you specified by using validation attributes on the properties of the `Movie` model class. Test validation using the `Edit` action method, and the same validation is applied.

The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a break point in the `HTTP Post` method, by using the [Fiddler tool](https://www.telerik.com/fiddler) , or the [F12 Developer tools](/en-us/microsoft-edge/devtools-guide).

## How validation works

You might wonder how the validation UI was generated without any updates to the code in the controller or views. The following code shows the two `Create` methods.

```
// GET: Movies/Create
public IActionResult Create()
{
    return View();
}

// POST: Movies/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (ModelState.IsValid)
    {
        _context.Add(movie);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}
```

The first (HTTP GET) `Create` action method displays the initial Create form. The second (`[HttpPost]`) version handles the form post. The second `Create` method (The `[HttpPost]` version) calls `ModelState.IsValid` to check whether the movie has any validation errors. Calling this method evaluates any validation attributes that have been applied to the object. If the object has validation errors, the `Create` method re-displays the form. If there are no errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the server when there are validation errors detected on the client side; the second `Create` method is never called when there are client side validation errors. If you disable JavaScript in your browser, client validation is disabled and you can test the HTTP POST `Create` method `ModelState.IsValid` detecting any validation errors.

You can set a break point in the `[HttpPost] Create` method and verify the method is never called, client side validation won't submit the form data when validation errors are detected. If you disable JavaScript in your browser, then submit the form with errors, the break point will be hit. You still get full validation without JavaScript.

The following image shows how to disable JavaScript in the Firefox browser.

The following image shows how to disable JavaScript in the Chrome browser.

After you disable JavaScript, post invalid data and step through the debugger.

A portion of the `Create.cshtml` view template is shown in the following markup:

```
<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>

            @*Markup removed for brevity.*@
```

The preceding markup is used by the action methods to display the initial form and to redisplay it in the event of an error.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

What's really nice about this approach is that neither the controller nor the `Create` view template knows anything about the actual validation rules being enforced or about the specific error messages displayed. The validation rules and the error strings are specified only in the `Movie` class. These same validation rules are automatically applied to the `Edit` view and any other views templates you might create that edit your model.

When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the model (in this example, the `Movie` class). You won't have to worry about different parts of the application being inconsistent with how the rules are enforced — all validation logic will be defined in one place and used everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be fully honoring the DRY principle.

## Using DataType Attributes

Open the `Movie.cs` file and examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. We've already applied a `DataType` enumeration value to the release date and to the price fields. The following code shows the `ReleaseDate` and `Price` properties with the appropriate `DataType` attribute.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
```

The `DataType` attributes only provide hints for the view engine to format the data and supplies elements/attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email. You can use the `RegularExpression` attribute to validate the format of the data. The `DataType` attribute is used to specify a data type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want to keep track of the date, not the time. The `DataType` Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress and more. The `DataType` attribute can also enable the application to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`, and a date selector can be provided for `DataType.Date` in browsers that support HTML5. The `DataType` attributes emit HTML 5 `data-` (pronounced data dash) attributes that HTML 5 browsers can understand. The `DataType` attributes do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `DisplayFormat` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied when the value is displayed in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably don't want the currency symbol in the text box for editing.)

You can use the `DisplayFormat` attribute by itself, but it's generally a good idea to use the `DataType` attribute. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the following benefits that you don't get with DisplayFormat:

* The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.)

* By default, the browser will render data using the correct format based on your locale.

* The `DataType` attribute can enable MVC to choose the right field template to render the data (the `DisplayFormat` if used by itself uses the string template).

Note

jQuery validation doesn't work with the `Range` attribute and `DateTime`. For example, the following code will always display a client side validation error, even when the date is in the specified range:

`[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]`

You will need to disable jQuery date validation to use the `Range` attribute with `DateTime`. It's generally not a good practice to compile hard dates in your models, so using the `Range` attribute and `DateTime` is discouraged.

The following code shows combining attributes on one line:

```
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [StringLength(60, MinimumLength = 3)]
        public string Title { get; set; }

        [Display(Name = "Release Date"), DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
        public string Genre { get; set; }

        [Range(1, 100), DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
        public string Rating { get; set; }
    }
}
```

## Apply migrations

Some validation attributes affect the database schema. For example, `[Required]` makes a column `NOT NULL` and `[StringLength(60)]` limits the column type to `nvarchar(60)`. Run a migration to keep the database schema consistent with the model.

* [Visual Studio](#tabpanel_1_visual-studio)
* [Visual Studio Code](#tabpanel_1_visual-studio-code)

From the **Tools** menu, select **NuGet Package Manager > Package Manager Console**.

In the PMC, enter the following commands:

```
Add-Migration New_DataAnnotations
Update-Database
```

```
dotnet ef migrations add InitialCreate
```

```
dotnet ef database update
```

For more information, see [Resetting all migrations](/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#resetting-all-migrations).

In the next part of the series, we review the app and make some improvements to the automatically generated `Details` and `Delete` methods.

## Related content

* [Part 8, Add a new field (EF Core migrations)](new-field?view=aspnetcore-10.0)
* [Working with Forms](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

[Previous](new-field?view=aspnetcore-10.0) [Next](details?view=aspnetcore-10.0)

In this section:

* Validation logic is added to the `Movie` model.
* You ensure that the validation rules are enforced any time a user creates or edits a movie.

## Keeping things DRY

One of the design tenets of MVC is [DRY](https://wikipedia.org/wiki/Don%27t_repeat_yourself) ("Don't Repeat Yourself"). ASP.NET Core MVC encourages you to specify functionality or behavior only once, and then have it be reflected everywhere in an app. This reduces the amount of code you need to write and makes the code you do write less error prone, easier to test, and easier to maintain.

The validation support provided by MVC and Entity Framework Core Code First is a good example of the DRY principle in action. You can declaratively specify validation rules in one place (in the model class) and the rules are enforced everywhere in the app.

## Add validation rules to the movie model

The DataAnnotations namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like `DataType` that help with formatting and don't provide any validation.

Update the `Movie` class to take advantage of the built-in `Required`, `StringLength`, `RegularExpression`, and `Range` validation attributes.

```
public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
    [Required]
    [StringLength(30)]
    public string Genre { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
    [StringLength(5)]
    [Required]
    public string Rating { get; set; }
}
```

The validation attributes specify behavior that you want to enforce on the model properties they're applied to:

* The `Required` and `MinimumLength` attributes indicate that a property must have a value; but nothing prevents a user from entering white space to satisfy this validation.

* The `RegularExpression` attribute is used to limit what characters can be input. In the preceding code, "Genre":

  * Must only use letters.
  * The first letter must be uppercase. White spaces are allowed, while numbers and special characters aren't allowed.

* The `RegularExpression` "Rating":

  * Requires that the first character be an uppercase letter.
  * Allows special characters and numbers in subsequent spaces. "PG-13" is valid for a rating, but fails for a "Genre".

* The `Range` attribute constrains a value to within a specified range.

* The `StringLength` attribute lets you set the maximum length of a string property, and optionally its minimum length.

* Value types (such as `decimal`, `int`, `float`, `DateTime`) are inherently required and don't need the `[Required]` attribute.

Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures that you can't forget to validate something and inadvertently let bad data into the database.

## Validation Error UI

Run the app and navigate to the Movies controller.

Tap the **Create New** link to add a new movie. Fill out the form with some invalid values. As soon as jQuery client side validation detects the error, it displays an error message.

Note

You may not be able to enter decimal commas in decimal fields. To support [jQuery validation](https://jqueryvalidation.org/) for non-English locales that use a comma (",") for a decimal point, and non US-English date formats, you must take steps to globalize your app. [See this GitHub comment 4076](https://github.com/dotnet/AspNetCore.Docs/issues/4076#issuecomment-1153254062) for instructions on adding decimal comma.

Notice how the form has automatically rendered an appropriate validation error message in each field containing an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a user has JavaScript disabled).

A significant benefit is that you didn't need to change a single line of code in the `MoviesController` class or in the `Create.cshtml` view in order to enable this validation UI. The controller and views you created earlier in this tutorial automatically picked up the validation rules that you specified by using validation attributes on the properties of the `Movie` model class. Test validation using the `Edit` action method, and the same validation is applied.

The form data isn't sent to the server until there are no client side validation errors. You can verify this by putting a break point in the `HTTP Post` method, by using the [Fiddler tool](https://www.telerik.com/fiddler) , or the [F12 Developer tools](/en-us/microsoft-edge/devtools-guide).

## How validation works

You might wonder how the validation UI was generated without any updates to the code in the controller or views. The following code shows the two `Create` methods.

```
// GET: Movies/Create
public IActionResult Create()
{
    return View();
}

// POST: Movies/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
    [Bind("ID,Title,ReleaseDate,Genre,Price, Rating")] Movie movie)
{
    if (ModelState.IsValid)
    {
        _context.Add(movie);
        await _context.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    return View(movie);
}
```

The first (HTTP GET) `Create` action method displays the initial Create form. The second (`[HttpPost]`) version handles the form post. The second `Create` method (The `[HttpPost]` version) calls `ModelState.IsValid` to check whether the movie has any validation errors. Calling this method evaluates any validation attributes that have been applied to the object. If the object has validation errors, the `Create` method re-displays the form. If there are no errors, the method saves the new movie in the database. In our movie example, the form isn't posted to the server when there are validation errors detected on the client side; the second `Create` method is never called when there are client side validation errors. If you disable JavaScript in your browser, client validation is disabled and you can test the HTTP POST `Create` method `ModelState.IsValid` detecting any validation errors.

You can set a break point in the `[HttpPost] Create` method and verify the method is never called, client side validation won't submit the form data when validation errors are detected. If you disable JavaScript in your browser, then submit the form with errors, the break point will be hit. You still get full validation without JavaScript.

The following image shows how to disable JavaScript in the Firefox browser.

The following image shows how to disable JavaScript in the Chrome browser.

After you disable JavaScript, post invalid data and step through the debugger.

The portion of the `Create.cshtml` view template is shown in the following markup:

```

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>           
       
        @*Markup removed for brevity.*@
```

The preceding markup is used by the action methods to display the initial form and to redisplay it in the event of an error.

The [Input Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0) uses the [DataAnnotations](/en-us/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-6) attributes and produces HTML attributes needed for jQuery Validation on the client side. The [Validation Tag Helper](../../mvc/views/working-with-forms?view=aspnetcore-10.0#the-validation-tag-helpers) displays validation errors. See [Validation](../../mvc/models/validation?view=aspnetcore-10.0) for more information.

What's really nice about this approach is that neither the controller nor the `Create` view template knows anything about the actual validation rules being enforced or about the specific error messages displayed. The validation rules and the error strings are specified only in the `Movie` class. These same validation rules are automatically applied to the `Edit` view and any other views templates you might create that edit your model.

When you need to change validation logic, you can do so in exactly one place by adding validation attributes to the model (in this example, the `Movie` class). You won't have to worry about different parts of the application being inconsistent with how the rules are enforced — all validation logic will be defined in one place and used everywhere. This keeps the code very clean, and makes it easy to maintain and evolve. And it means that you'll be fully honoring the DRY principle.

## Using DataType Attributes

Open the `Movie.cs` file and examine the `Movie` class. The `System.ComponentModel.DataAnnotations` namespace provides formatting attributes in addition to the built-in set of validation attributes. We've already applied a `DataType` enumeration value to the release date and to the price fields. The following code shows the `ReleaseDate` and `Price` properties with the appropriate `DataType` attribute.

```
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
```

The `DataType` attributes only provide hints for the view engine to format the data (and supplies elements/attributes such as `<a>` for URL's and `<a href="mailto:EmailAddress.com">` for email. You can use the `RegularExpression` attribute to validate the format of the data. The `DataType` attribute is used to specify a data type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want to keep track of the date, not the time. The `DataType` Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress and more. The `DataType` attribute can also enable the application to automatically provide type-specific features. For example, a `mailto:` link can be created for `DataType.EmailAddress`, and a date selector can be provided for `DataType.Date` in browsers that support HTML5. The `DataType` attributes emit HTML 5 `data-` (pronounced data dash) attributes that HTML 5 browsers can understand. The `DataType` attributes do **not** provide any validation.

`DataType.Date` doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's `CultureInfo`.

The `DisplayFormat` attribute is used to explicitly specify the date format:

```
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
```

The `ApplyFormatInEditMode` setting specifies that the formatting should also be applied when the value is displayed in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably don't want the currency symbol in the text box for editing.)

You can use the `DisplayFormat` attribute by itself, but it's generally a good idea to use the `DataType` attribute. The `DataType` attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the following benefits that you don't get with DisplayFormat:

* The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.)

* By default, the browser will render data using the correct format based on your locale.

* The `DataType` attribute can enable MVC to choose the right field template to render the data (the `DisplayFormat` if used by itself uses the string template).

Note

jQuery validation doesn't work with the `Range` attribute and `DateTime`. For example, the following code will always display a client side validation error, even when the date is in the specified range:

`[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]`

You will need to disable jQuery date validation to use the `Range` attribute with `DateTime`. It's generally not a good practice to compile hard dates in your models, so using the `Range` attribute and `DateTime` is discouraged.

The following code shows combining attributes on one line:

```
public class Movie
{
    public int Id { get; set; }

    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; }

    [Display(Name = "Release Date"), DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
    public string Genre { get; set; }

    [Range(1, 100), DataType(DataType.Currency)]
    [Column(TypeName = "decimal(18, 2)")]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
    public string Rating { get; set; }
}
```

In the next part of the series, we review the app and make some improvements to the automatically generated `Details` and `Delete` methods.

## Related content

* [Working with Forms](../../mvc/views/working-with-forms?view=aspnetcore-10.0)
* [Globalization and localization](../../fundamentals/localization?view=aspnetcore-10.0)
* [Introduction to Tag Helpers](../../mvc/views/tag-helpers/intro?view=aspnetcore-10.0)
* [Author Tag Helpers](../../mvc/views/tag-helpers/authoring?view=aspnetcore-10.0)

[Previous](new-field?view=aspnetcore-10.0) [Next](details?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-09

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/troubleshoot-aspnet-core-localization
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/troubleshoot-aspnet-core-localization.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Troubleshoot ASP.NET Core localization

By [Hisham Bin Ateya](https://github.com/hishamco)

This article provides instructions on how to diagnose ASP.NET Core app localization issues.

## Localization configuration issues

**Localization middleware order**\
The app may not localize because the localization middleware isn't ordered as expected.

To resolve this issue, ensure that localization middleware is registered before MVC middleware. Otherwise, the localization middleware isn't applied.

```
public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization(options => options.ResourcesPath = "Resources");

    services.AddMvc();
}
```

**Localization resources path not found**

**Supported Cultures in RequestCultureProvider don't match with registered once**

## Resource file naming issues

ASP.NET Core has predefined rules and guidelines for localization resources file naming, which are described in [Globalization and localization in ASP.NET Core](localization?view=aspnetcore-10.0#resource-file-naming).

## Missing resources

Common causes of resources not being found include:

* Resource names are misspelled in either the .NET XML resource file (`.resx`) or the localizer request.
* The resource is missing from the resource file for some languages, but exists in others.
* If you're still having trouble, check the localization log messages (logged at the `Debug` log level) for more details about the missing resources.

Tip

When using [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider), verify single quotes aren't used with the cultures inside the localization cookie value. For example, `c='en-UK'|uic='en-US'` is an invalid cookie value, while `c=en-UK|uic=en-US` is valid.

## Resources and class libraries issues

ASP.NET Core by default provides a way to allow the class libraries to find their resource files via [ResourceLocationAttribute](/en-us/dotnet/api/microsoft.extensions.localization.resourcelocationattribute).

Common issues with class libraries include:

* Missing the [ResourceLocationAttribute](/en-us/dotnet/api/microsoft.extensions.localization.resourcelocationattribute) in a class library prevents [ResourceManagerStringLocalizerFactory](/en-us/dotnet/api/microsoft.extensions.localization.resourcemanagerstringlocalizerfactory) from discovering the resources.
* Resource file naming. For more information, see the [Resource file naming issues](#resource-file-naming-issues) section.
* Changing the root namespace of the class library. For more information, see the [Root namespace issues](#root-namespace-issues) section.

## `CustomRequestCultureProvider` doesn't work as expected

The [RequestLocalizationOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions) class has three default providers:

* [QueryStringRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.querystringrequestcultureprovider)
* [CookieRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.cookierequestcultureprovider)
* [AcceptLanguageHeaderRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.acceptlanguageheaderrequestcultureprovider)

The [CustomRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.customrequestcultureprovider) allows you to customize how the localization culture is provided. The [CustomRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.localization.customrequestcultureprovider) is used when the default providers don't meet your requirements.

A common reason for a custom provider not working properly is that it isn't the first provider in the [RequestCultureProviders](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.requestcultureproviders#microsoft-aspnetcore-builder-requestlocalizationoptions-requestcultureproviders) list. To resolve this issue:

* Insert the custom provider at position zero in the [RequestCultureProviders](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.requestcultureproviders#microsoft-aspnetcore-builder-requestlocalizationoptions-requestcultureproviders) list:

  ```
  options.AddInitialRequestCultureProvider(
      new CustomRequestCultureProvider(async context =>
      {
          // My custom request culture logic
          return new ProviderCultureResult("en");
      }));
  ```

- Insert the custom provider at position zero in the [RequestCultureProviders](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptions.requestcultureproviders#microsoft-aspnetcore-builder-requestlocalizationoptions-requestcultureproviders) list:

  ```
  options.RequestCultureProviders.Insert(0, 
      new CustomRequestCultureProvider(async context =>
      {
          // My custom request culture logic
          return new ProviderCultureResult("en");
      }));
  ```

* Use the [AddInitialRequestCultureProvider](/en-us/dotnet/api/microsoft.aspnetcore.builder.requestlocalizationoptionsextensions.addinitialrequestcultureprovider) extension method to set the custom provider as the initial provider.

## Root namespace issues

When the root namespace of an assembly is different than the assembly name, localization doesn't work by default. To avoid this issue use the [`RootNamespace` attribute](/en-us/dotnet/api/microsoft.extensions.localization.rootnamespaceattribute), which is described in [Globalization and localization in ASP.NET Core](localization?view=aspnetcore-10.0#resource-file-naming).

Warning

A root namespace issue can occur when a project's name isn't a valid .NET identifier. For instance, `my-project-name.csproj` uses the root namespace `my_project_name` and the assembly name `my-project-name`, which results in this error.

## Resources and build action

If you use resource files for localization, it's important that they have an appropriate build action. Use **Embedded Resource**; otherwise, the `ResourceStringLocalizer` isn't able to find these resources.

## Location override using "Sensors" pane in developer tools

When using the location override using the **Sensors** pane in Google Chrome or Microsoft Edge developer tools, the fallback language is reset after prerendering. Avoid setting the language using the **Sensors** pane when testing. Set the language using the browser's language settings.

For more information, see [Blazor Localization does not work with InteractiveServer (`dotnet/aspnetcore` #53707)](https://github.com/dotnet/aspnetcore/issues/53707).

## GitHub issues with helpful problem solving tips

* [Please add more info about shared files (`dotnet/AspNetCore.Docs` #28674)](https://github.com/dotnet/AspNetCore.Docs/issues/28674)
* [Blazor Localization does not work with InteractiveServer (`dotnet/aspnetcore` #53707)](https://github.com/dotnet/aspnetcore/issues/53707) ([Location override using "Sensors" pane](#location-override-using-sensors-pane-in-developer-tools))

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-06-11

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/supported-platforms
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/supported-platforms.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP.NET Core Blazor supported platforms

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Blazor is supported in the browsers shown in the following table on both mobile and desktop platforms.

| Browser         | Version  |
| --------------- | -------- |
| Apple Safari    | Current† |
| Google Chrome   | Current† |
| Microsoft Edge  | Current† |
| Mozilla Firefox | Current† |

†*Current* refers to the latest version of the browser.

## Blazor WebAssembly

| Browser                     | Version       |
| --------------------------- | ------------- |
| Apple Safari                | Current†      |
| Google Chrome               | Current†      |
| Microsoft Edge              | Current†      |
| Microsoft Internet Explorer | Not Supported |
| Mozilla Firefox             | Current†      |

†*Current* refers to the latest version of the browser.

## Blazor Server

| Browser                     | Version       |
| --------------------------- | ------------- |
| Apple Safari                | Current†      |
| Google Chrome               | Current†      |
| Microsoft Edge              | Current†      |
| Microsoft Internet Explorer | Not Supported |
| Mozilla Firefox             | Current†      |

†*Current* refers to the latest version of the browser.

For [Blazor Hybrid apps](hybrid/?view=aspnetcore-10.0), we test on and support the latest platform Web View control versions:

* [Microsoft Edge `WebView2` on Windows](/en-us/microsoft-edge/webview2/)
* [Chrome on Android](https://play.google.com/store/apps/details?id=com.android.chrome)
* [Safari on iOS and macOS](https://www.apple.com/safari/)

## Related content

* [ASP.NET Core Blazor hosting models](hosting-models?view=aspnetcore-10.0)
* [ASP.NET Core SignalR supported platforms](../signalr/supported-platforms?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/when-to-use-a-reverse-proxy
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/servers/kestrel/when-to-use-a-reverse-proxy.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# When to use Kestrel with a reverse proxy

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

When Kestrel is used as an edge server without a reverse proxy server, sharing of the same IP address and port among multiple processes is unsupported. When Kestrel is configured to listen on a port, Kestrel handles all traffic for that port regardless of requests' `Host` headers. A reverse proxy that can share ports can forward requests to Kestrel on a unique IP and port.

Even if a reverse proxy server isn't required, using a reverse proxy server might be a good choice.

A reverse proxy:

* Can limit the exposed public surface area of the apps that it hosts.
* Provides an additional layer of configuration and defense-in-depth cybersecurity.
* Might integrate better with existing infrastructure.
* Simplifies load balancing and secure communication (HTTPS) configuration. Only the reverse proxy server requires the X.509 certificate for the public domain. That server can communicate with the app's servers on the internal network using plain HTTP or HTTPS with locally managed certificates. Internal HTTPS increases security but adds significant overhead.

Warning

Hosting in a reverse proxy configuration requires [host filtering](host-filtering?view=aspnetcore-10.0).

## Related content

[Configure ASP.NET Core to work with proxy servers and load balancers](../../../host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-13

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/accconfirm
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/accconfirm.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Account confirmation and password recovery in ASP.NET Core

By [Rick Anderson](https://twitter.com/RickAndMSFT), [Ponant](https://github.com/Ponant), and [Joe Audette](https://twitter.com/joeaudette)

This tutorial shows how to build an ASP.NET Core app with email confirmation and password reset. This tutorial is **not** a beginning article. You should be familiar with:

* [Razor Pages in ASP.NET Core](../../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Authentication](identity?view=aspnetcore-10.0)
* [Entity Framework Core](../../data/ef-mvc/intro?view=aspnetcore-10.0)

For Blazor guidance, which adds to or supersedes the guidance in this article, see the following resources:

* [Account confirmation and password recovery in ASP.NET Core Blazor](../../blazor/security/account-confirmation-and-password-recovery?view=aspnetcore-10.0)
* [Account confirmation and password recovery in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity](../../blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery?view=aspnetcore-10.0)

## Prerequisites

* [.NET 6 or later SDK](https://dotnet.microsoft.com/download/dotnet/6.0)
* Successfully [send email from a C# console app by using the SendGrid API (Twilio)](https://www.twilio.com/blog/send-emails-using-the-sendgrid-api-with-dotnetnet-6-and-csharp)

## Create and test a web app with authentication

Run the following commands to create a web app with authentication:

```
dotnet new webapp -au Individual -o WebPWrecover
cd WebPWrecover
dotnet run
```

### Register a user with simulated email confirmation

Run the app, select the **Register** link, and register a user.

After registration completes, you're redirected to the `/Identity/Account/RegisterConfirmation` page, which contains a link to simulate email confirmation.

1. Select the `Click here to confirm your account` link.

2. Select the **Login** link and sign-in with the same credentials.

3. Select the `Hello YourEmail@provider.com!` link, which redirects to the `/Identity/Account/Manage/PersonalData` page.

4. Select the **Personal data** tab, and then select **Delete**.

The `Click here to confirm your account` link displays because the [IEmailSender](https://github.com/dotnet/aspnetcore/blob/1dcf7acfacf0fe154adcc23270cb0da11ff44ace/src/Identity/UI/src/Areas/Identity/Services/EmailSender.cs) interface isn't yet implemented and registered with the [dependency injection container](../../fundamentals/dependency-injection?view=aspnetcore-10.0). For more information, see the [RegisterConfirmation source](https://github.com/dotnet/aspnetcore/blob/main/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/RegisterConfirmation.cshtml.cs#L71-L74).

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list.

### Configure an email provider

In this tutorial, Twilio [SendGrid](https://www.twilio.com/en-us/sendgrid) is used to send email. A SendGrid account and key are required to send email. We recommend using SendGrid or another email service to send email rather than SMTP. SMTP is difficult to secure and set up correctly.

The SendGrid account might require [adding a Sender](https://sendgrid.com/docs/ui/sending-email/senders/).

Create a class to fetch the secure email key. For this sample, create the *Services/AuthMessageSenderOptions.cs* file:

```
namespace WebPWrecover.Services;

public class AuthMessageSenderOptions
{
    public string? SendGridKey { get; set; }
}
```

#### Configure SendGrid user secrets

Set the `SendGridKey` value with the [secret-manager tool](../app-secrets?view=aspnetcore-10.0). For example:

```
dotnet user-secrets set SendGridKey <key>

Successfully saved SendGridKey to the secret store.
```

On Windows, Secret Manager stores keys/value pairs in a *secrets.json* file in the `%APPDATA%/Microsoft/UserSecrets/<WebAppName-userSecretsId>` directory.

The contents of the *secrets.json* file aren't encrypted. The following markup shows the *secrets.json* file. The `SendGridKey` value is removed from the example.

```
{
  "SendGridKey": "<key removed>"
}
```

For more information, see the [Options pattern](../../fundamentals/configuration/options?view=aspnetcore-10.0) and [Configuration in ASP.NET Core](../../fundamentals/configuration/?view=aspnetcore-10.0).

### Install SendGrid

This tutorial shows how to add email notifications through [SendGrid](https://sendgrid.com/), but other email providers can be used.

Install the `SendGrid` NuGet package:

* [Visual Studio](#tabpanel_1_visual-studio)
* [.NET CLI](#tabpanel_1_net-cli)

From the Package Manager Console, enter the following command:

```
Install-Package SendGrid
```

From the console, enter the following command:

```
dotnet add package SendGrid
```

To register for a free SendGrid account, [start sending with a free SendGrid Email API trial](https://www.twilio.com/en-us/products/email-api/pricing).

### Implement IEmailSender

To implement the `IEmailSender` interface, create the *Services/EmailSender.cs* file with code similar to the following example:

```
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;

namespace WebPWrecover.Services;

public class EmailSender : IEmailSender
{
    private readonly ILogger _logger;

    public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
                       ILogger<EmailSender> logger)
    {
        Options = optionsAccessor.Value;
        _logger = logger;
    }

    public AuthMessageSenderOptions Options { get; } //Set with Secret Manager.

    public async Task SendEmailAsync(string toEmail, string subject, string message)
    {
        if (string.IsNullOrEmpty(Options.SendGridKey))
        {
            throw new Exception("Null SendGridKey");
        }
        await Execute(Options.SendGridKey, subject, message, toEmail);
    }

    public async Task Execute(string apiKey, string subject, string message, string toEmail)
    {
        var client = new SendGridClient(apiKey);
        var msg = new SendGridMessage()
        {
            From = new EmailAddress("Joe@contoso.com", "Password Recovery"),
            Subject = subject,
            PlainTextContent = message,
            HtmlContent = message
        };
        msg.AddTo(new EmailAddress(toEmail));

        // Disable click tracking.
        // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
        msg.SetClickTracking(false, false);
        var response = await client.SendEmailAsync(msg);
        _logger.LogInformation(response.IsSuccessStatusCode 
                               ? $"Email to {toEmail} queued successfully!"
                               : $"Failure Email to {toEmail}");
    }
}
```

### Configure the app to support email

Add the following code to the *Program.cs* file, which performs the following tasks:

* Adds the `EmailSender` instance as a transient service.
* Registers the `AuthMessageSenderOptions` configuration instance.

```
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using WebPWrecover.Data;
using WebPWrecover.Services;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(options =>
{
    options.SendGridKey = builder.Configuration["SendGridKey"];
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();
```

### Disable default account verification when Account.RegisterConfirmation is scaffolded

If `Account.RegisterConfirmation` is scaffolded, complete the instructions in this section.

Important

If `Account.RegisterConfirmation` is **not** scaffolded, skip the following instructions and continue to the next section.

The user is redirected to the `/Identity/Account/RegisterConfirmation` page where they can select a link to have the account confirmed. The default `Account.RegisterConfirmation` is used ***only*** for testing. Automatic account verification should be disabled in a production app.

To require a confirmed account and prevent immediate sign in at registration, set `DisplayConfirmAccountLink = false` in the scaffolded */Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs* file:

```
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable

using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;

namespace WebPWrecover.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterConfirmationModel : PageModel
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly IEmailSender _sender;

        public RegisterConfirmationModel(UserManager<IdentityUser> userManager, IEmailSender sender)
        {
            _userManager = userManager;
            _sender = sender;
        }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public string Email { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public bool DisplayConfirmAccountLink { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public string EmailConfirmationUrl { get; set; }

        public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
        {
            if (email == null)
            {
                return RedirectToPage("/Index");
            }
            returnUrl = returnUrl ?? Url.Content("~/");

            var user = await _userManager.FindByEmailAsync(email);
            if (user == null)
            {
                return NotFound($"Unable to load user with email '{email}'.");
            }

            Email = email;
            // Once you add a real email sender, you should remove this code that lets you confirm the account
            DisplayConfirmAccountLink = false;
            if (DisplayConfirmAccountLink)
            {
                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                EmailConfirmationUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                    protocol: Request.Scheme);
            }

            return Page();
        }
    }
}
```

This step is necessary only when `Account.RegisterConfirmation` is scaffolded.

The non-scaffolded [RegisterConfirmation](https://github.com/dotnet/aspnetcore/blob/1dcf7acfacf0fe154adcc23270cb0da11ff44ace/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/RegisterConfirmation.cshtml.cs#L74-L87) automatically detects when an [IEmailSender](https://github.com/dotnet/aspnetcore/blob/1dcf7acfacf0fe154adcc23270cb0da11ff44ace/src/Identity/UI/src/Areas/Identity/Services/EmailSender.cs) is implemented and registered with the [dependency injection container](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

## Register, confirm email, and reset password

Run the web app, and test the account confirmation and password recovery flow.

1. Run the app and register a new user.

2. Check your email for the account confirmation link. If you don't receive the email, see the [Debug email](#debug-email) section for troubleshooting.

3. Select the link and confirm your email.

4. Sign in with your email and password.

5. Sign out.

### Test password reset

1. If you're signed in, select **Logout**.

2. Select the **Log in** link, and then select the **Forgot your password?** link.

3. Enter the email you used to register the account. The app sends an email with a link to reset your password.

4. Go to the sent email message.

5. Select the link and reset your password.

After your password successfully resets, you can sign in with your email and new password.

## Resend email confirmation

This section describes the code that supports the email confirmation process and related tasks.

* Start by selecting the **Resend email confirmation** link on the **Login** page.

### Change email and activity timeout

The default inactivity timeout is 14 days. The following code sets the inactivity timeout to five days:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using WebPWrecover.Data;
using WebPWrecover.Services;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

builder.Services.ConfigureApplicationCookie(o => {
    o.ExpireTimeSpan = TimeSpan.FromDays(5);
    o.SlidingExpiration = true;
});

var app = builder.Build();

// Code removed for brevity
```

### Change all data protection token lifespans

The following code changes the timeout period for all data protection tokens to three hours:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using WebPWrecover.Data;
using WebPWrecover.Services;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

builder.Services.Configure<DataProtectionTokenProviderOptions>(o =>
       o.TokenLifespan = TimeSpan.FromHours(3));

var app = builder.Build();

// Code removed for brevity.
```

The built-in Identity user tokens (see the [AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs](https://github.com/dotnet/AspNetCore/blob/v2.2.2/src/Identity/Extensions.Core/src/TokenOptions.cs) source) have a [one day timeout](https://github.com/dotnet/AspNetCore/blob/v2.2.2/src/Identity/Core/src/DataProtectionTokenProviderOptions.cs).

### Change the email token lifespan

The default token lifespan of [the Identity user tokens](https://github.com/dotnet/AspNetCore/blob/v2.2.2/src/Identity/Extensions.Core/src/TokenOptions.cs) is [one day](https://github.com/dotnet/AspNetCore/blob/v2.2.2/src/Identity/Core/src/DataProtectionTokenProviderOptions.cs).

The following code shows how to change the email token lifespan.

Add a custom [DataProtectorTokenProvider\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.dataprotectortokenprovider-1) class and [DataProtectionTokenProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.dataprotectiontokenprovideroptions) class:

```
public class CustomEmailConfirmationTokenProvider<TUser>
                              :  DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomEmailConfirmationTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<EmailConfirmationTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
                                       : base(dataProtectionProvider, options, logger)
    {

    }
}
public class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions
{
    public EmailConfirmationTokenProviderOptions()
    {
        Name = "EmailDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(4);
    }
}
```

Add the custom provider to the service container:

```
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using WebPWrecover.Data;
using WebPWrecover.Services;
using WebPWrecover.TokenProviders;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(config =>
{
    config.SignIn.RequireConfirmedEmail = true;
    config.Tokens.ProviderMap.Add("CustomEmailConfirmation",
        new TokenProviderDescriptor(
            typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
    config.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
}).AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddTransient<CustomEmailConfirmationTokenProvider<IdentityUser>>();

builder.Services.AddRazorPages();

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

var app = builder.Build();

// Code removed for brevity.
```

### Debug email

If the email process isn't working as expected, try these troubleshooting steps:

* Set a breakpoint in the `EmailSender.Execute` method and verify the `SendGridClient.SendEmailAsync` method is called.

* Create a [console app to send email](https://www.twilio.com/docs/sendgrid/for-developers/sending-email/v2-csharp-code-example) by using similar code to `EmailSender.Execute`.

* Review the [Email Activity](https://www.twilio.com/docs/sendgrid/ui/analytics-and-reporting/email-activity-feed) page.

* Check your spam folder.

* Try another email alias on a different email provider, such as Microsoft, Yahoo, Gmail, and so on.

* Try sending to different email accounts.

Tip

**A security best practice** is to **not** use production secrets in test and development. If you publish the app to Azure, set the SendGrid secrets as application settings in the Azure Web App portal. The configuration system is set up to read keys from environment variables.

## Combine social and local login accounts

To complete this section, you must first enable an external authentication provider. For more information, see [Using external login providers with Identity in ASP.NET Core](social/?view=aspnetcore-10.0).

In this sequence, the email address `RickAndMSFT@gmail.com` is first created as a local login. However, you can create the account as a social login first, and then add a local login.

1. To combine local and social accounts, select the email address link.

2. In the **Manage your account** page, select the **Manage** link.

   Notice there are currently zero (0) external (social logins) associated with the authenticated account.

3. In the **Manage your external logins** page, select the link to another login service. Follow the service prompts and accept the app requests.

   In the following image, Facebook is added as an external authentication provider:

Authentication for the user email address now combines local and external (social) accounts. The user can sign in with either account.

Tip

It's a good practice to recommend your users to add a local account to your app. This approach can help ensure continued access in case their social login authentication service is down, or they lose access to their social account.

## Enable account confirmation after a site has users

If you enable account confirmation on a site with existing users, you lock them out because their accounts aren't confirmed.

To work around the issue of existing user lockout, use one of the following approaches:

* Update the database to mark all existing users as confirmed.

* Confirm existing users. For example, batch-send emails with confirmation links.

## Prerequisites

[.NET Core 3.0 or later SDK](https://dotnet.microsoft.com/download/dotnet-core/3.0)

## Create and test a web app with authentication

Run the following commands to create a web app with authentication.

```
dotnet new webapp -au Individual -uld -o WebPWrecover
cd WebPWrecover
dotnet run
```

Run the app, select the **Register** link, and register a user. Once registered, you are redirected to the to `/Identity/Account/RegisterConfirmation` page which contains a link to simulate email confirmation:

* Select the `Click here to confirm your account` link.
* Select the **Login** link and sign-in with the same credentials.
* Select the `Hello YourEmail@provider.com!` link, which redirects you to the `/Identity/Account/Manage/PersonalData` page.
* Select the **Personal data** tab on the left, and then select **Delete**.

### Configure an email provider

In this tutorial, [SendGrid](https://sendgrid.com) is used to send email. You can use other email providers. We recommend you use SendGrid or another email service to send email. SMTP is difficult to configure so mail is not marked as spam.

The SendGrid account may require [adding a Sender](https://sendgrid.com/docs/ui/sending-email/senders/).

Create a class to fetch the secure email key. For this sample, create `Services/AuthMessageSenderOptions.cs`:

```
namespace WebPWrecover.Services;

public class AuthMessageSenderOptions
{
    public string? SendGridKey { get; set; }
}
```

#### Configure SendGrid user secrets

Set the `SendGridKey` with the [secret-manager tool](../app-secrets?view=aspnetcore-10.0). For example:

```
dotnet user-secrets set SendGridKey <SG.key>

Successfully saved SendGridKey = SG.keyVal to the secret store.
```

On Windows, Secret Manager stores keys/value pairs in a `secrets.json` file in the `%APPDATA%/Microsoft/UserSecrets/<WebAppName-userSecretsId>` directory.

The contents of the `secrets.json` file aren't encrypted. The following markup shows the `secrets.json` file. The `SendGridKey` value has been removed.

```
{
  "SendGridKey": "<key removed>"
}
```

For more information, see the [Options pattern](../../fundamentals/configuration/options?view=aspnetcore-10.0) and [configuration](../../fundamentals/configuration/?view=aspnetcore-10.0).

### Install SendGrid

This tutorial shows how to add email notifications through [SendGrid](https://sendgrid.com/), but you can send email using SMTP and other mechanisms.

Install the `SendGrid` NuGet package:

* [Visual Studio](#tabpanel_2_visual-studio)
* [.NET CLI](#tabpanel_2_net-cli)

From the Package Manager Console, enter the following command:

```
Install-Package SendGrid
```

From the console, enter the following command:

```
dotnet add package SendGrid
```

See [Get Started with SendGrid for Free](https://sendgrid.com/free/) to register for a free SendGrid account.

### Implement IEmailSender

To Implement `IEmailSender`, create `Services/EmailSender.cs` with code similar to the following:

```
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;

namespace WebPWrecover.Services;

public class EmailSender : IEmailSender
{
    private readonly ILogger _logger;

    public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
                       ILogger<EmailSender> logger)
    {
        Options = optionsAccessor.Value;
        _logger = logger;
    }

    public AuthMessageSenderOptions Options { get; } //Set with Secret Manager.

    public async Task SendEmailAsync(string toEmail, string subject, string message)
    {
        if (string.IsNullOrEmpty(Options.SendGridKey))
        {
            throw new Exception("Null SendGridKey");
        }
        await Execute(Options.SendGridKey, subject, message, toEmail);
    }

    public async Task Execute(string apiKey, string subject, string message, string toEmail)
    {
        var client = new SendGridClient(apiKey);
        var msg = new SendGridMessage()
        {
            From = new EmailAddress("Joe@contoso.com", "Password Recovery"),
            Subject = subject,
            PlainTextContent = message,
            HtmlContent = message
        };
        msg.AddTo(new EmailAddress(toEmail));

        // Disable click tracking.
        // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
        msg.SetClickTracking(false, false);
        var response = await client.SendEmailAsync(msg);
        _logger.LogInformation(response.IsSuccessStatusCode 
                               ? $"Email to {toEmail} queued successfully!"
                               : $"Failure Email to {toEmail}");
    }
}
```

### Configure startup to support email

Add the following code to the `ConfigureServices` method in the `Startup.cs` file:

* Add `EmailSender` as a transient service.
* Register the `AuthMessageSenderOptions` configuration instance.

```
public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
        config.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
        config.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
      }).AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddTransient<CustomEmailConfirmationTokenProvider<IdentityUser>>();

    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddRazorPages();
}
```

## Scaffold RegisterConfirmation

Follow the instructions for [Scaffold Identity](scaffold-identity?view=aspnetcore-10.0) and scaffold `Account\RegisterConfirmation`.

### Disable default account verification when Account.RegisterConfirmation is scaffolded

If `Account.RegisterConfirmation` is scaffolded, complete the instructions in this section.

Important

If `Account.RegisterConfirmation` is **not** scaffolded, skip the following instructions and continue to the next section.

The user is redirected to the `/Identity/Account/RegisterConfirmation` page where they can select a link to have the account confirmed. The default `Account.RegisterConfirmation` is used ***only*** for testing. Automatic account verification should be disabled in a production app.

To require a confirmed account and prevent immediate sign in at registration, set `DisplayConfirmAccountLink = false` in the scaffolded */Areas/Identity/Pages/Account/RegisterConfirmation.cshtml.cs* file:

```
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable

using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.WebUtilities;

namespace WebPWrecover.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class RegisterConfirmationModel : PageModel
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly IEmailSender _sender;

        public RegisterConfirmationModel(UserManager<IdentityUser> userManager, IEmailSender sender)
        {
            _userManager = userManager;
            _sender = sender;
        }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public string Email { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public bool DisplayConfirmAccountLink { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public string EmailConfirmationUrl { get; set; }

        public async Task<IActionResult> OnGetAsync(string email, string returnUrl = null)
        {
            if (email == null)
            {
                return RedirectToPage("/Index");
            }
            returnUrl = returnUrl ?? Url.Content("~/");

            var user = await _userManager.FindByEmailAsync(email);
            if (user == null)
            {
                return NotFound($"Unable to load user with email '{email}'.");
            }

            Email = email;
            // Once you add a real email sender, you should remove this code that lets you confirm the account
            DisplayConfirmAccountLink = false;
            if (DisplayConfirmAccountLink)
            {
                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                EmailConfirmationUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl },
                    protocol: Request.Scheme);
            }

            return Page();
        }
    }
}
```

This step is necessary only when `Account.RegisterConfirmation` is scaffolded.

The non-scaffolded [RegisterConfirmation](https://github.com/dotnet/aspnetcore/blob/1dcf7acfacf0fe154adcc23270cb0da11ff44ace/src/Identity/UI/src/Areas/Identity/Pages/V4/Account/RegisterConfirmation.cshtml.cs#L74-L87) automatically detects when an [IEmailSender](https://github.com/dotnet/aspnetcore/blob/1dcf7acfacf0fe154adcc23270cb0da11ff44ace/src/Identity/UI/src/Areas/Identity/Services/EmailSender.cs) is implemented and registered with the [dependency injection container](../../fundamentals/dependency-injection?view=aspnetcore-10.0).

## Register, confirm email, and reset password

Run the web app, and test the account confirmation and password recovery flow.

* Run the app and register a new user
* Check your email for the account confirmation link. See [Debug email](#debug) if you don't get the email.
* Click the link to confirm your email.
* Sign in with your email and password.
* Sign out.

### Test password reset

* If you're signed in, select **Logout**.
* Select the **Log in** link and select the **Forgot your password?** link.
* Enter the email you used to register the account.
* An email with a link to reset your password is sent. Check your email and click the link to reset your password. After your password has been successfully reset, you can sign in with your email and new password.

[]()

## Resend email confirmation

In .NET 5 or later, select the **Resend email confirmation** link on the **Login** page.

### Change email and activity timeout

The default inactivity timeout is 14 days. The following code sets the inactivity timeout to 5 days:

```
services.ConfigureApplicationCookie(o => {
    o.ExpireTimeSpan = TimeSpan.FromDays(5);
    o.SlidingExpiration = true;
});
```

### Change all data protection token lifespans

The following code changes all data protection tokens timeout period to 3 hours:

```
public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
                  options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.Configure<DataProtectionTokenProviderOptions>(o =>
       o.TokenLifespan = TimeSpan.FromHours(3));

    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddRazorPages();
}
```

The built in Identity user tokens (see [AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs](https://github.com/dotnet/AspNetCore/blob/v2.2.2/src/Identity/Extensions.Core/src/TokenOptions.cs) )have a [one day timeout](https://github.com/dotnet/AspNetCore/blob/v2.2.2/src/Identity/Core/src/DataProtectionTokenProviderOptions.cs).

### Change the email token lifespan

The default token lifespan of [the Identity user tokens](https://github.com/dotnet/AspNetCore/blob/v2.2.2/src/Identity/Extensions.Core/src/TokenOptions.cs) is [one day](https://github.com/dotnet/AspNetCore/blob/v2.2.2/src/Identity/Core/src/DataProtectionTokenProviderOptions.cs). This section shows how to change the email token lifespan.

Add a custom [DataProtectorTokenProvider\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.dataprotectortokenprovider-1) and [DataProtectionTokenProviderOptions](/en-us/dotnet/api/microsoft.aspnetcore.identity.dataprotectiontokenprovideroptions):

```
public class CustomEmailConfirmationTokenProvider<TUser>
                                       : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomEmailConfirmationTokenProvider(IDataProtectionProvider dataProtectionProvider,
        IOptions<EmailConfirmationTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
                                          : base(dataProtectionProvider, options, logger)
    {

    }
}
public class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions
{
    public EmailConfirmationTokenProviderOptions()
    {
        Name = "EmailDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(4);
    }
}
```

Add the custom provider to the service container:

```
public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(config =>
    {
        config.SignIn.RequireConfirmedEmail = true;
        config.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
        config.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
      }).AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddTransient<CustomEmailConfirmationTokenProvider<IdentityUser>>();

    services.AddTransient<IEmailSender, EmailSender>();
    services.Configure<AuthMessageSenderOptions>(Configuration);

    services.AddRazorPages();
}
```

[]()

### Debug email

If you can't get email working:

* Set a breakpoint in `EmailSender.Execute` to verify `SendGridClient.SendEmailAsync` is called.
* Create a [console app to send email](https://sendgrid.com/docs/Integrate/Code_Examples/v2_Mail/csharp.html) using similar code to `EmailSender.Execute`.
* Review the [Email Activity](https://sendgrid.com/docs/User_Guide/email_activity.html) page.
* Check your spam folder.
* Try another email alias on a different email provider (Microsoft, Yahoo, Gmail, etc.)
* Try sending to different email accounts.

**A security best practice** is to **not** use production secrets in test and development. If you publish the app to Azure, set the SendGrid secrets as application settings in the Azure Web App portal. The configuration system is set up to read keys from environment variables.

## Combine social and local login accounts

To complete this section, you must first enable an external authentication provider. See [Facebook, Google, and external provider authentication](social/?view=aspnetcore-10.0).

You can combine local and social accounts by clicking on your email link. In the following sequence, "RickAndMSFT\@gmail.com" is first created as a local login; however, you can create the account as a social login first, then add a local login.

Click on the **Manage** link. Note the 0 external (social logins) associated with this account.

Click the link to another login service and accept the app requests. In the following image, Facebook is the external authentication provider:

The two accounts have been combined. You are able to sign in with either account. You might want your users to add local accounts in case their social login authentication service is down, or more likely they've lost access to their social account.

## Enable account confirmation after a site has users

Enabling account confirmation on a site with users locks out all the existing users. Existing users are locked out because their accounts aren't confirmed. To work around existing user lockout, use one of the following approaches:

* Update the database to mark all existing users as being confirmed.
* Confirm existing users. For example, batch-send emails with confirmation links.

## Related content

* [Razor Pages in ASP.NET Core](../../tutorials/razor-pages/razor-pages-start?view=aspnetcore-10.0)
* [Authentication](identity?view=aspnetcore-10.0)
* [Entity Framework Core](../../data/ef-mvc/intro?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-05-12

----
url: https://learn.microsoft.com/en-us/aspnet/core/diagnostics/asp0011
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/diagnostics/asp0011.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# ASP0011: Suggest using builder.Logging over Host.ConfigureLogging or WebHost.ConfigureLogging

|                                     | Value        |
| ----------------------------------- | ------------ |
| **Rule ID**                         | ASP0011      |
| **Category**                        | Usage        |
| **Fix is breaking or non-breaking** | Non-breaking |

## Cause

`ConfigureLogging` isn't the recommended strategy for configuring logging in a Minimal API application.

## Rule description

`ConfigureLogging` isn't the recommended strategy for configuring logging in a Minimal API application.

```
var builder = WebApplication.CreateBuilder(args);

builder.Host.ConfigureLogging(logging =>
{
    logging.AddJsonConsole();
})

var app = builder.Build();

app.Run();
```

## How to fix violations

To fix a violation of this rule, use the `Logging` property on the `WebApplicationBuilder` to modify the logging configuration directly without the need for an additional `ConfigureLogging` call.

```
var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddJsonConsole();

var app = builder.Build();

app.Run();
```

## When to suppress warnings

Do ***not*** suppress a warning from this rule.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-02-03

----
url: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly/azure-static-web-apps
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/azure-static-web-apps.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Host and deploy ASP.NET Core standalone Blazor WebAssembly with Azure Static Web Apps

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

This article explains how to host and deploy standalone Blazor WebAssembly with [Microsoft Azure Static Web Apps](https://azure.microsoft.com/products/app-service/static).

## App configuration

To ensure that requests for any path return `index.html`, set a navigation fallback route.

Create a file named `staticwebapp.config.json` in the project's root folder with the following content:

```
{
  "navigationFallback": {
    "rewrite": "/index.html"
  }
}
```

## Deploy from Visual Studio

To deploy a standalone Blazor WebAssembly app from Visual Studio:

* Save any unsaved work in the project, as a Visual Studio restart might be required during the process.
* Follow the guidance at [Deploy a Blazor app on Azure Static Web Apps: Deploy from Visual Studio (Azure Static Web Apps documentation)](/en-us/azure/static-web-apps/deploy-blazor#deploy-from-visual-studio).

## GitHub deployment scenarios

* Visual Studio Code: [Quickstart: Build your first static site with Azure Static Web Apps](/en-us/azure/static-web-apps/getting-started?tabs=blazor)
* .NET CLI: [Deploy Blazor websites to the cloud with Azure Static Web Apps (Video)](/en-us/shows/deploy-websites-to-the-cloud-with-azure-static-web-apps/deploy-blazor-websites-to-the-cloud-with-azure-static-web-apps)
* Deploy from GitHub: [Tutorial: Building a static web app with Blazor in Azure Static Web Apps](/en-us/azure/static-web-apps/deploy-blazor)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/request-response
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/middleware/request-response.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Request and response operations in ASP.NET Core

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

By [Justin Kotalik](https://github.com/jkotalik)

This article explains how to read from the request body and write to the response body. Code for these operations might be required when writing middleware. Outside of writing middleware, custom code isn't generally required because the operations are handled by MVC and Razor Pages.

There are two abstractions for the request and response bodies: [Stream](/en-us/dotnet/api/system.io.stream) and [Pipe](/en-us/dotnet/api/system.io.pipelines.pipe). For request reading, [HttpRequest.Body](/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.body#microsoft-aspnetcore-http-httprequest-body) is a [Stream](/en-us/dotnet/api/system.io.stream), and `HttpRequest.BodyReader` is a [PipeReader](/en-us/dotnet/api/system.io.pipelines.pipereader). For response writing, [HttpResponse.Body](/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse.body#microsoft-aspnetcore-http-httpresponse-body) is a [Stream](/en-us/dotnet/api/system.io.stream), and `HttpResponse.BodyWriter` is a [PipeWriter](/en-us/dotnet/api/system.io.pipelines.pipewriter).

[Pipelines](/en-us/dotnet/standard/io/pipelines) are recommended over streams. Streams can be easier to use for some simple operations, but pipelines have a performance advantage and are easier to use in most scenarios. ASP.NET Core is starting to use pipelines instead of streams internally. Examples include:

* `FormReader`
* `TextReader`
* `TextWriter`
* `HttpResponse.WriteAsync`

Streams aren't being removed from the framework. Streams continue to be used throughout .NET:

* Many stream types don't have pipe equivalents, such as `FileStreams` and `ResponseCompression`.
* It's straightforward adding compression to a stream.

## Stream examples

Suppose the goal is to create a middleware that reads the entire request body as a list of strings, splitting on new lines. A simple stream implementation might look like the following example:

Warning

The following code:

* Is used to demonstrate the problems with not using a pipe to read the request body.
* Is not intended to be used in production apps.

```
private async Task<List<string>> GetListOfStringsFromStream(Stream requestBody)
{
    // Build up the request body in a string builder.
    StringBuilder builder = new StringBuilder();

    // Rent a shared buffer to write the request body into.
    byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

    while (true)
    {
        var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0, buffer.Length);
        if (bytesRemaining == 0)
        {
            break;
        }

        // Append the encoded string into the string builder.
        var encodedString = Encoding.UTF8.GetString(buffer, 0, bytesRemaining);
        builder.Append(encodedString);
    }

    ArrayPool<byte>.Shared.Return(buffer);

    var entireRequestBody = builder.ToString();

    // Split on \n in the string.
    return new List<string>(entireRequestBody.Split("\n"));
}
```

This code works, but there are some issues:

* Before appending to the `StringBuilder`, the example creates another string (`encodedString`) that is thrown away immediately. This process occurs for all bytes in the stream, so the result is extra memory allocation the size of the entire request body.
* The example reads the entire string before splitting on new lines. It's more efficient to check for new lines in the byte array.

Here's an example that fixes some of the preceding issues:

Warning

The following code:

* Is used to demonstrate the solutions to some problems in the preceding code while not solving all the problems.
* Is not intended to be used in production apps.

```
private async Task<List<string>> GetListOfStringsFromStreamMoreEfficient(Stream requestBody)
{
    StringBuilder builder = new StringBuilder();
    byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
    List<string> results = new List<string>();

    while (true)
    {
        var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0, buffer.Length);

        if (bytesRemaining == 0)
        {
            results.Add(builder.ToString());
            break;
        }

        // Instead of adding the entire buffer into the StringBuilder
        // only add the remainder after the last \n in the array.
        var prevIndex = 0;
        int index;
        while (true)
        {
            index = Array.IndexOf(buffer, (byte)'\n', prevIndex);
            if (index == -1)
            {
                break;
            }

            var encodedString = Encoding.UTF8.GetString(buffer, prevIndex, index - prevIndex);

            if (builder.Length > 0)
            {
                // If there was a remainder in the string buffer, include it in the next string.
                results.Add(builder.Append(encodedString).ToString());
                builder.Clear();
            }
            else
            {
                results.Add(encodedString);
            }

            // Skip past last \n
            prevIndex = index + 1;
        }

        var remainingString = Encoding.UTF8.GetString(buffer, prevIndex, bytesRemaining - prevIndex);
        builder.Append(remainingString);
    }

    ArrayPool<byte>.Shared.Return(buffer);

    return results;
}
```

This preceding example:

* Doesn't buffer the entire request body in a `StringBuilder` unless there aren't any newline characters.
* Doesn't call `Split` on the string.

However, there are still a few issues:

* If newline characters are sparse, much of the request body is buffered in the string.
* The code continues to create strings (`remainingString`) and adds them to the string buffer, which results in an extra allocation.

These issues are fixable, but the code is becoming progressively more complicated with little improvement. Pipelines provide a way to solve these problems with minimal code complexity.

## Pipelines

The following example shows how the preceding stream scenario can be handled using a [PipeReader](/en-us/dotnet/standard/io/pipelines#pipe):

```
private async Task<List<string>> GetListOfStringFromPipe(PipeReader reader)
{
    List<string> results = new List<string>();

    while (true)
    {
        ReadResult readResult = await reader.ReadAsync();
        var buffer = readResult.Buffer;

        SequencePosition? position = null;

        do
        {
            // Look for a EOL in the buffer
            position = buffer.PositionOf((byte)'\n');

            if (position != null)
            {
                var readOnlySequence = buffer.Slice(0, position.Value);
                AddStringToList(results, in readOnlySequence);

                // Skip the line + the \n character (basically position)
                buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
            }
        }
        while (position != null);


        if (readResult.IsCompleted && buffer.Length > 0)
        {
            AddStringToList(results, in buffer);
        }

        reader.AdvanceTo(buffer.Start, buffer.End);

        // At this point, buffer will be updated to point one byte after the last
        // \n character.
        if (readResult.IsCompleted)
        {
            break;
        }
    }

    return results;
}

private static void AddStringToList(List<string> results, in ReadOnlySequence<byte> readOnlySequence)
{
    // Separate method because Span/ReadOnlySpan cannot be used in async methods
    ReadOnlySpan<byte> span = readOnlySequence.IsSingleSegment ? readOnlySequence.First.Span : readOnlySequence.ToArray().AsSpan();
    results.Add(Encoding.UTF8.GetString(span));
}
```

This example fixes many issues that the streams implementations had:

* There's no need for a string buffer because the `PipeReader` handles bytes that haven't been used.
* Encoded strings are directly added to the list of returned strings.
* Other than the `ToArray` call, and the memory used by the string, string creation is allocation free.

When writing directly to `HttpResponse.BodyWriter`, call `PipeWriter.FlushAsync` manually to ensure the data is flushed to the underlying response response body. Here's why:

* `HttpResponse.BodyWriter` is a `PipeWriter` that buffers data until a flush operation is triggered.
* Calling `FlushAsync` writes the buffered data to the underlying response body.

It's up to the developer to decide when to call `FlushAsync`, balancing factors such as buffer size, network write overhead, and whether the data should be sent in discrete chunks. For more information, see [System.IO.Pipelines in .NET](/en-us/dotnet/standard/io/pipelines).

## Adapters

The `Body`, `BodyReader`, and `BodyWriter` properties are available for `HttpRequest` and `HttpResponse`. When you set `Body` to a different stream, a new set of adapters automatically adapt each type to the other. If you set `HttpRequest.Body` to a new stream, `HttpRequest.BodyReader` is automatically set to a new `PipeReader` that wraps `HttpRequest.Body`.

## StartAsync

`HttpResponse.StartAsync` is used to indicate that headers are unmodifiable and to run `OnStarting` callbacks. When using Kestrel as a server, calling `StartAsync` before using the `PipeReader` guarantees that memory returned by `GetMemory` belongs to Kestrel's internal [Pipe](/en-us/dotnet/api/system.io.pipelines.pipe) rather than an external buffer.

## Related content

* [System.IO.Pipelines in .NET](/en-us/dotnet/standard/io/pipelines)
* [Write custom ASP.NET Core middleware](write?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-07-22

----
url: https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/signalr/hubs.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# Use hubs in ASP.NET Core SignalR

By [Rachel Appel](https://twitter.com/rachelappel) and [Kevin Griffin](https://twitter.com/1kevgriff)

The SignalR Hubs API enables connected clients to call methods on the server, facilitating real-time communication. The server defines methods that are called by the client, and the client defines methods that are called by the server. SignalR also enables indirect client-to-client communication, where the SignalR Hub provides the mediation. This approach allows sending messages between individual clients, groups, or to all connected clients. SignalR takes care of everything required to make real-time client-to-server and server-to-client communication possible.

This article describes how to configure hubs, send messages to clients, and allow servers to handle results from clients.

## Configure SignalR hubs

Register the services required by SignalR hubs by calling the [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr) method in the `Program` file:

```
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();
```

Configure SignalR endpoints by calling the [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.hubendpointroutebuilderextensions.maphub) method in the `Program` file:

```
app.MapRazorPages();
app.MapHub<ChatHub>("/Chat");

app.Run();
```

The SignalR middleware requires some services, which are configured by calling [AddSignalR](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.signalrdependencyinjectionextensions.addsignalr):

```
services.AddSignalR();
```

When adding SignalR functionality to an ASP.NET Core app, setup SignalR routes by calling [MapHub](/en-us/dotnet/api/microsoft.aspnetcore.builder.hubendpointroutebuilderextensions.maphub) in the `Startup.Configure` method's [UseEndpoints](/en-us/dotnet/api/microsoft.aspnetcore.builder.endpointroutingapplicationbuilderextensions.useendpoints) callback:

```
app.UseEndpoints(endpoints =>
{
    endpoints.MapHub<ChatHub>("/chathub");
});
```

When adding SignalR functionality to an ASP.NET Core app, setup SignalR routes by calling [UseSignalR](/en-us/dotnet/api/microsoft.aspnetcore.builder.signalrappbuilderextensions.usesignalr) in the `Startup.Configure` method:

```
app.UseSignalR(route =>
{
    route.MapHub<ChatHub>("/chathub");
});
```

Note

ASP.NET Core SignalR server-side assemblies are now installed with the .NET Core SDK. For more information, see [SignalR assemblies in shared framework](../migration/22-to-30?view=aspnetcore-10.0#signalr-assemblies-in-shared-framework).

## Create and use hubs

Create a hub by declaring a class that inherits from [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub). Add `public` methods to the class to make them callable from clients:

```
public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
        => await Clients.All.SendAsync("ReceiveMessage", user, message);
}
```

You can specify a return type and parameters, including complex types and arrays, as you would in any C# method. SignalR handles the serialization and deserialization of complex objects and arrays in your parameters and return values. Hub method parameters, return values, and stream items can be [C# union types](/en-us/dotnet/csharp/language-reference/builtin-types/union) only with the default `JsonHubProtocol`. The MessagePack and `Newtonsoft.Json` hub protocols don't support unions.

### Accept cancellation in a hub method

Any hub method can declare a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken). The server supplies the token, so clients don't send an argument for it. The token is canceled when the client cancels the invocation or when the connection closes:

```
public class ReportHub : Hub
{
    public async Task<Report> GenerateReport(
        int reportId, CancellationToken cancellationToken)
    {
        // Pass cancellationToken to the work being performed.
    }
}
```

Cancellation is cooperative. The hub method decides whether and how quickly to stop, and SignalR sends a completion message for the invocation either way. Letting an [OperationCanceledException](/en-us/dotnet/api/system.operationcanceledexception) propagate out of the hub method completes the invocation with an error.

A client can cancel any invocation that has an invocation ID. For how a client requests cancellation, see [ASP.NET Core SignalR .NET client](dotnet-client?view=aspnetcore-10.0#cancel-a-hub-method-invocation).

You can specify a return type and parameters, including complex types and arrays, as you would in any C# method. SignalR handles the serialization and deserialization of complex objects and arrays in your parameters and return values.

## Use 'Context' object properties and methods

The [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub) class includes a [Context](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub.context) property that contains the following properties with information about the connection.

| Property                                                                                               | Description                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ConnectionId](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext.connectionid)           | Gets the unique ID for the connection, assigned by SignalR. There's one connection ID for each connection.                                                                                                                                                                                                                                                                                  |
| [UserIdentifier](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext.useridentifier)       | Gets the [user identifier](groups?view=aspnetcore-10.0). By default, SignalR uses the [ClaimTypes.NameIdentifier](/en-us/dotnet/api/system.security.claims.claimtypes.nameidentifier#system-security-claims-claimtypes-nameidentifier) property from the [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) associated with the connection as the user identifier. |
| [User](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext.user)                           | Gets the [ClaimsPrincipal](/en-us/dotnet/api/system.security.claims.claimsprincipal) associated with the current user.                                                                                                                                                                                                                                                                      |
| [Items](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext.items)                         | Gets a key/value collection that can be used to share data within the scope of this connection. Data can be stored in this collection and it persists for the connection across different hub method invocations.                                                                                                                                                                           |
| [Features](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext.features)                   | Gets the collection of features available on the connection. This collection isn't currently needed in most scenarios, so detailed documentation isn't yet available.                                                                                                                                                                                                                       |
| [ConnectionAborted](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext.connectionaborted) | Gets a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) that notifies when the connection is aborted.                                                                                                                                                                                                                                                              |

The [Hub.Context](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub.context) property also contains the following methods:

| Method                                                                                                   | Description                                                                                                                                                                                                                                                                |
| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [GetHttpContext](/en-us/dotnet/api/microsoft.aspnetcore.signalr.gethttpcontextextensions.gethttpcontext) | Returns the [HttpContext](/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) for the connection, or `null` if the connection isn't associated with an HTTP request. For HTTP connections, use this method to get information such as HTTP headers and query strings. |
| [Abort](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubcallercontext.abort)                           | Aborts the connection.                                                                                                                                                                                                                                                     |

## Use 'Clients' object properties and methods

The [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub) class includes a [Clients](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub.clients) property that contains the following properties for communication between server and client:

| Property                                                                            | Description                                                                        |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| [All](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.all)             | Calls a method on all connected clients.                                           |
| [Caller](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubcallerclients-1.caller) | Calls a method on the client that invoked the hub method.                          |
| [Others](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubcallerclients-1.others) | Calls a method on all connected clients except the client that invoked the method. |

The [Hub.Clients](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub.clients) property also contains the following methods:

| Method                                                                                            | Description                                                                                 |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| [AllExcept](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.allexcept)               | Calls a method on all connected clients except for the specified connections.               |
| [Client](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.client)                     | Calls a method on a specific connected client.                                              |
| [Clients](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.clients)                   | Calls a method on specific connected clients.                                               |
| [Group](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.group)                       | Calls a method on all connections in the specified group.                                   |
| [GroupExcept](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.groupexcept)           | Calls a method on all connections in the specified group, except the specified connections. |
| [Groups](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.groups)                     | Calls a method on multiple groups of connections.                                           |
| [OthersInGroup](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubcallerclients-1.othersingroup) | Calls a method on a group of connections, excluding the client that invoked the hub method. |
| [User](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.user)                         | Calls a method on all connections associated with a specific user.                          |
| [Users](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubclients-1.users)                       | Calls a method on all connections associated with the specified users.                      |

Each property or method returns an object with a `SendAsync` method. The `SendAsync` method receives the name of the client method to call and any parameters.

The object returned by the `Client` and `Caller` methods also contain an `InvokeAsync` method, which can be used to wait for a [result from the client](hubs?view=aspnetcore-10.0#request-client-results).

## Send messages to clients

To make calls to specific clients, use the properties of the `Clients` object. In the following example, there are three hub methods:

* The `SendMessage` method sends a message to all connected clients by using the `Clients.All` property.
* The `SendMessageToCaller` method sends a message back to the caller by using the `Clients.Caller` property.
* The `SendMessageToGroup` method sends a message to all clients in the `SignalR Users` group.

```
public async Task SendMessage(string user, string message)
    => await Clients.All.SendAsync("ReceiveMessage", user, message);

public async Task SendMessageToCaller(string user, string message)
    => await Clients.Caller.SendAsync("ReceiveMessage", user, message);

public async Task SendMessageToGroup(string user, string message)
    => await Clients.Group("SignalR Users").SendAsync("ReceiveMessage", user, message);
```

```
public Task SendMessage(string user, string message)
{
    return Clients.All.SendAsync("ReceiveMessage", user, message);
}

public Task SendMessageToCaller(string user, string message)
{
    return Clients.Caller.SendAsync("ReceiveMessage", user, message);
}

public Task SendMessageToGroup(string user, string message)
{
    return Clients.Group("SignalR Users").SendAsync("ReceiveMessage", user, message);
}
```

## Use strongly typed hubs

A drawback of using the `SendAsync` method is that it relies on a string to specify the client method to call. This design leaves code open to runtime errors if the method name is misspelled or missing from the client.

An alternative to using the `SendAsync` method is to strongly type the [Hub](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub) class with [Hub\<T>](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub-1). In the following example, the `ChatHub` client method is extracted into an interface named `IChatClient`:

```
public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
}
```

The interface can be used to refactor the preceding `ChatHub` example to make it strongly typed:

```
public class StronglyTypedChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
        => await Clients.All.ReceiveMessage(user, message);

    public async Task SendMessageToCaller(string user, string message)
        => await Clients.Caller.ReceiveMessage(user, message);

    public async Task SendMessageToGroup(string user, string message)
        => await Clients.Group("SignalR Users").ReceiveMessage(user, message);
}
```

Using `Hub<IChatClient>` enables compile-time checking of the client methods. This approach prevents issues caused by using strings because `Hub<T>` can only provide access to the methods defined in the interface. Using a strongly typed `Hub<T>` disables the ability to use the `SendAsync` method.

Note

The `Async` suffix isn't stripped from method names. Unless a client method is defined with `.on('MyMethodAsync')`, don't use `MyMethodAsync` as the name.

## Request client results

In addition to making calls to clients, the server can request a result from a client. In this scenario, the server uses the `ISingleClientProxy.InvokeAsync` method and the client returns a result from its `.On` handler. Pass a [CancellationToken](/en-us/dotnet/api/system.threading.cancellationtoken) that limits how long the server waits for a result. The following examples use a 30-second cancellation token.

There are two ways to use the API on the server.

You can call `Client(...)` or `Caller` on the `Clients` property in a `Hub` method:

```
public class ChatHub : Hub
{
    public async Task<string> WaitForMessage(string connectionId)
    {
        using var cancellationTokenSource =
            new CancellationTokenSource(TimeSpan.FromSeconds(30));

        var message = await Clients.Client(connectionId).InvokeAsync<string>(
            "GetMessage", cancellationTokenSource.Token);
        return message;
    }
}
```

Or, you can call `Client(...)` on an instance of [IHubContext\<T>](hubcontext?view=aspnetcore-10.0):

```
async Task SomeMethod(IHubContext<MyHub> context)
{
    using var cancellationTokenSource =
        new CancellationTokenSource(TimeSpan.FromSeconds(30));

    string result = await context.Clients.Client(connectionID).InvokeAsync<string>(
        "GetMessage", cancellationTokenSource.Token);
}
```

Strongly typed hubs can also return values from interface methods:

```
public interface IClient
{
    Task<string> GetMessage();
}

public class ChatHub : Hub<IClient>
{
    public async Task<string> WaitForMessage(string connectionId)
    {
        string message = await Clients.Client(connectionId).GetMessage();
        return message;
    }
}
```

Clients return results in their `.On(...)` handlers, as shown in the following sections.

#### .NET client

```
hubConnection.On("GetMessage", async () =>
{
    Console.WriteLine("Enter message:");
    var message = await Console.In.ReadLineAsync();
    return message;
});
```

#### TypeScript client

```
hubConnection.on("GetMessage", async () => {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("message");
        }, 100);
    });
    return promise;
});
```

#### Java client

```
hubConnection.onWithResult("GetMessage", () -> {
    return Single.just("message");
});
```

## Change the name of a hub method

By default, a server hub method name is the name of the .NET method. To change this default behavior for a specific method, use the [HubMethodName](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubmethodnameattribute) attribute. The client should use this name instead of the .NET method name when invoking the method:

```
[HubMethodName("SendMessageToUser")]
public async Task DirectMessage(string user, string message)
    => await Clients.User(user).SendAsync("ReceiveMessage", user, message);
```

## Inject services into a hub

SignalR registers an [IHubActivator\<THub>](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubactivator-1) and creates an unregistered hub with [ActivatorUtilities](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.activatorutilities) for each invocation. Singleton services injected into a hub outlive the hub instance, and injected transient services have a lifetime that matches the lifetime of the hub instance. Scoped service instances are created for each hub invocation and also match the lifetime of the hub; therefore, scoped and transient services exhibit equivalent lifetimes.

Hub constructor service injection is supported. In the following example, [IDbContextFactory\<TContext>](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1) is injected into a hub using a primary constructor to save messages to a database when `SendMessage` is called.

In the app's `Program` file using SQL Server as the example database provider and a connection string from configuration:

```
var connectionString = 
    builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
```

An example hub class:

```
public class ChatHub(IDbContextFactory<ApplicationDbContext> contextFactory) : Hub
{
    public async Task SendMessage(string user, string message)
    {
        using var context = await contextFactory.CreateDbContextAsync();

        var msgEntity = new Message { User = user, Content = message };
        context.Messages.Add(msgEntity);
        await context.SaveChangesAsync();

        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

If you're unable to use a factory and must inject a scoped [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) directly, the framework automatically creates a dependency injection (DI) scope for the hub method invocation. The framework disposes the context as soon as the invocation/stream completes. ***However, you must ensure that the hub's methods don't execute concurrent database operations on the same context instance because [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) isn't thread-safe.***

Hub method service injection is also supported. In the following example, a scoped [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) (`ApplicationDbContext`) only used for a quick write operation is injected into the specific method that requires it:

```
public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message, 
        ApplicationDbContext context)
    {
        context.Messages.Add(new Message { User = user, Content = message });
        await context.SaveChangesAsync();

        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

To explicitly specify which parameters are resolved from DI in hub methods, specify the `[FromServices]` attribute or a custom attribute that implements [IFromServiceMetadata](/en-us/dotnet/api/microsoft.aspnetcore.http.metadata.ifromservicemetadata) on the hub method parameters that should be resolved from DI.

Set the [DisableImplicitFromServicesParameters](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.disableimplicitfromservicesparameters#microsoft-aspnetcore-signalr-huboptions-disableimplicitfromservicesparameters) property ([ASP.NET Core documentation](configuration?view=aspnetcore-10.0#configure-server-options)) in case some parameters might come from DI and you instead want them to come from the client.

In the app's `Program` file:

```
builder.Services.AddSingleton<SomeCustomType>();
builder.Services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();

builder.Services.AddSignalR(options =>
{
    options.DisableImplicitFromServicesParameters = true;
});
```

In the following hub method, only `IDatabaseService` is resolved from DI:

```
public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message,
        SomeCustomType type,
        [FromServices] IDatabaseService dbService)
    {
        await dbService.SaveMessageAsync(user, message, type);
        await Clients.All.SendAsync("ReceiveMessage", user, message, type);
    }
}
```

Note

Implicit parameter inference makes use of [IServiceProviderIsService](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iserviceproviderisservice), which is optionally implemented in DI configurations. If the app's DI container doesn't support this feature, injecting services into hub methods using implicit parameter inference isn't supported.

For database operations, adopting the factory pattern is preferred for overlapping operations within one invocation:

* A direct scoped context can run into concurrency issues. Using a factory completely isolates each factory operation.
* For server-side Blazor apps, the factory pattern is recommended. For more information, see [ASP.NET Core Blazor with Entity Framework Core (EF Core)](../blazor/blazor-ef-core?view=aspnetcore-10.0).

SignalR registers an [IHubActivator\<THub>](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubactivator-1) and creates an unregistered hub with [ActivatorUtilities](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.activatorutilities) for each invocation. Singleton services injected into a hub outlive the hub instance, and injected transient services have a lifetime that matches the lifetime of the hub instance. Scoped service instances are created for each hub invocation and also match the lifetime of the hub; therefore, scoped and transient services exhibit equivalent lifetimes.

Hub constructor service injection is supported with the factory pattern for creating database contexts using [IDbContextFactory\<TContext>](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1). In the following example, [IDbContextFactory\<TContext>](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1) is injected into a hub's constructor and used to save messages to a database when `SendMessage` is called.

In `Startup.ConfigureServices` using SQL Server as the example database provider and a connection string from configuration:

```
var connectionString = Configuration.GetConnectionString("DefaultConnection");

services.AddDbContextFactory<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
```

An example hub class:

```
public class ChatHub : Hub
{
    private readonly IDbContextFactory<ApplicationDbContext> contextFactory;

    public ChatHub(IDbContextFactory<ApplicationDbContext> contextFactory)
    {
        this.contextFactory = contextFactory;
    }

    public async Task SendMessage(string user, string message)
    {
        using var context = contextFactory.CreateDbContext();

        context.Messages.Add(new Message { User = user, Content = message });
        await context.SaveChangesAsync();

        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

If you're unable to use a factory and must inject a scoped [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) directly, the framework automatically creates a dependency injection (DI) scope for the hub method invocation. The framework disposes the context as soon as the invocation/stream completes. ***However, you must ensure that the hub's methods don't execute concurrent database operations on the same context instance because [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) isn't thread-safe.***

When injecting a scoped [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext), the framework automatically creates a dependency injection (DI) scope for the hub method invocation. The framework disposes the context as soon as the invocation/stream completes. ***However, you must ensure that the hub's methods don't execute concurrent database operations on the same context instance because [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) isn't thread-safe.***

When injecting a scoped [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext), the framework automatically creates a dependency injection (DI) scope for the hub method invocation. The framework disposes the context as soon as the invocation completes. ***However, you must ensure that the hub's methods don't execute concurrent database operations on the same context instance because [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) isn't thread-safe.***

Note

Upgrade the app to target .NET 5 or later to use the factory pattern for creating database contexts using [IDbContextFactory\<TContext>](/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1).

For database operations, adopting the factory pattern is preferred for overlapping operations within one invocation:

* A direct scoped context can run into concurrency issues. Using a factory completely isolates each factory operation.
* For server-side Blazor apps, the factory pattern is recommended. For more information, see [ASP.NET Core Blazor with Entity Framework Core (EF Core)](../blazor/blazor-ef-core?view=aspnetcore-10.0).

SignalR registers an [IHubActivator\<THub>](/en-us/dotnet/api/microsoft.aspnetcore.signalr.ihubactivator-1) and creates an unregistered hub with [ActivatorUtilities](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.activatorutilities) for each invocation. Singleton services injected into a hub outlive the hub instance, and injected transient services have a lifetime that matches the lifetime of the hub instance. Scoped service instances are created for each hub invocation and also match the lifetime of the hub; therefore, scoped and transient services exhibit equivalent lifetimes.

Hub constructor service injection is supported. The framework automatically creates a dependency injection (DI) scope for the hub method invocation. The framework disposes the context as soon as the invocation completes. ***However, you must ensure that the hub's methods don't execute concurrent database operations on the same context instance because [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) isn't thread-safe.***

In the following example, [DbContext](/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext) is injected into a hub's constructor and used to save messages to a database when `SendMessage` is called.

In `Startup.ConfigureServices` using SQL Server as the example database provider and a connection string from configuration:

```
var connectionString = Configuration.GetConnectionString("DefaultConnection");

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
```

In a hub class:

```
public class ChatHub : Hub
{
    private readonly ApplicationDbContext context;

    public ChatHub(ApplicationDbContext context)
    {
        this.context = context;
    }

    public async Task SendMessage(string user, string message)
    {
        context.Messages.Add(new Message { User = user, Content = message });
        await context.SaveChangesAsync();

        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}
```

Because each hub method call is executed on a new hub instance, don't store state in a property of the hub class.

Don't instantiate a hub directly via DI. To send messages to a client from elsewhere in your app, use an [`IHubContext`](hubcontext?view=aspnetcore-10.0).

Use the [`await` operator](/en-us/dotnet/csharp/language-reference/operators/await) when calling an asynchronous method that depends on the hub staying alive. If you call an asynchronous method without `await`, the call can fail with the hub method completing before the asynchronous method finishes.

✔️Supported: `await Clients.All.SendAsync(...);`\
❌Not supported: `Clients.All.SendAsync(...);` (missing `await`)

For general guidance on DI, see [Dependency injection in ASP.NET Core](../fundamentals/dependency-injection?view=aspnetcore-10.0) and its linked additional resources.

## Keyed services support in dependency injection

The keyed services mechanism allows you to register and retrieve dependency injection services by using keys. A service is associated with a key by calling the [AddKeyedSingleton](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addkeyedsingleton) method to register it. As an alternative, you can call the `AddKeyedScoped` or `AddKeyedTransient` method.

You access a registered service by specifying the key with the \[[FromKeyedServices](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute)] attribute. The following code shows how to use keyed services:

```
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

var app = builder.Build();

app.MapRazorPages();
app.MapHub<MyHub>("/myHub");

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

public class MyHub : Hub
{
    public void SmallCacheMethod([FromKeyedServices("small")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }

    public void BigCacheMethod([FromKeyedServices("big")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }
}
```

## Limit per-connection streaming invocations

[MaximumParallelInvocationsPerClient](/en-us/dotnet/api/microsoft.aspnetcore.signalr.huboptions.maximumparallelinvocationsperclient#microsoft-aspnetcore-signalr-huboptions-maximumparallelinvocationsperclient) controls the number of non-streaming hub method invocations a client can run in parallel before they are queued. It does **not** apply to streaming hub invocations. Streaming invocations are intentionally excluded because they are expected to be long-running and concurrent, so a client can start any number of concurrent streams regardless of that setting.

To enforce a per-connection limit on streaming invocations, wrap the stream inside the hub method itself using a private helper that increments a counter before yielding items and decrements it in a `finally` block:

```
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;

public class StreamingHub : Hub
{
    private static readonly ConcurrentDictionary<string, int> _activeStreams = new();
    private const int MaxConcurrentStreams = 2;

    public IAsyncEnumerable<int> Counter(
        int count,
        int delay,
        CancellationToken cancellationToken)
    {
        return WithLimit(Context.ConnectionId, GetCounter(count, delay, cancellationToken));
    }

    private async IAsyncEnumerable<int> GetCounter(
        int count,
        int delay,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        for (var i = 0; i < count; i++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            yield return i;
            await Task.Delay(delay, cancellationToken);
        }
    }

    private async IAsyncEnumerable<T> WithLimit(
        string connectionId,
        IAsyncEnumerable<T> stream,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        var current = _activeStreams.AddOrUpdate(
            connectionId,
            addValue: 1,
            updateValueFactory: (_, count) => count + 1);

        if (current > MaxConcurrentStreams)
        {
            Decrement(connectionId);
            throw new HubException(
                $"The connection is limited to {MaxConcurrentStreams} concurrent streaming invocations.");
        }

        try
        {
            await foreach (var item in stream.WithCancellation(cancellationToken))
            {
                yield return item;
            }
        }
        finally
        {
            Decrement(connectionId);
        }
    }

    private static void Decrement(string connectionId)
    {
        while (_activeStreams.TryGetValue(connectionId, out var current))
        {
            if (current <= 1)
            {
                if (_activeStreams.TryRemove(new KeyValuePair<string, int>(connectionId, current)))
                {
                    return;
                }
            }
            else if (_activeStreams.TryUpdate(connectionId, current - 1, current))
            {
                return;
            }
        }
    }
}
```

The key point is that `WithLimit` wraps the original `IAsyncEnumerable<T>` and holds the counter elevated for the *full lifetime of the stream*, not just until the first item is yielded. The `finally` block runs only when the client finishes consuming the stream, cancels it, or the connection drops.

If your streaming hub methods return `ChannelReader<T>` instead of `IAsyncEnumerable<T>`, a similar wrapper can be applied. It should use the same `_activeStreams` dictionary so both stream types share a single connection-level limit rather than each maintaining their own independent count.

Note

The `_activeStreams` dictionary is `static` so it is shared across all hub instances. If you prefer DI-managed state, register a singleton service that owns the dictionary and inject it into the hub constructor.

## Handle events for a connection

The SignalR Hubs API provides the [OnConnectedAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub.onconnectedasync) and [OnDisconnectedAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub.ondisconnectedasync) virtual methods to manage and track connections. Override the `OnConnectedAsync` virtual method to perform actions when a client connects to the hub, such as adding it to a group:

```
public override async Task OnConnectedAsync()
{
    await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
    await base.OnConnectedAsync();
}
```

Override the `OnDisconnectedAsync` virtual method to perform actions when a client disconnects. If the client disconnects intentionally, such as by calling `connection.stop()`, the `exception` parameter is set to `null`. However, if the client disconnects due to an error, such as a network failure, the `exception` parameter contains an exception that describes the failure:

```
public override async Task OnDisconnectedAsync(Exception? exception)
{
    await base.OnDisconnectedAsync(exception);
}
```

```
public override async Task OnDisconnectedAsync(Exception exception)
{
    await Clients.Group("SignalR Users").SendAsync("ReceiveMessage", "I", "disconnect");
    await base.OnDisconnectedAsync(exception);
}
```

The [RemoveFromGroupAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.igroupmanager.removefromgroupasync) method doesn't need to be called within the [OnDisconnectedAsync](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hub.ondisconnectedasync) method because it's handled automatically.

Warning

Security warning: Exposing `ConnectionId` can lead to malicious impersonation if the SignalR server or client version is ASP.NET Core 2.2 or earlier.

A third virtual method, `OnAuthenticationRefreshedAsync`, runs when a connected client refreshes its authentication without reconnecting. Override it to perform actions after the refreshed principal is applied to the connection. `Context.User` reflects the refreshed principal when the method runs:

```
public class ChatHub : Hub
{
    public override Task OnAuthenticationRefreshedAsync()
    {
        return Clients.Caller.SendAsync(
            "AuthenticationRefreshed", Context.UserIdentifier);
    }
}
```

The method runs only for hubs that enable authentication refresh. A refresh doesn't change `Context.UserIdentifier` or reroute messages sent with `Clients.User`.

Two behaviors differ from the other connection events:

* [Client results](hubs?view=aspnetcore-10.0#request-client-results) aren't supported from this method, the same as in `OnConnectedAsync`. Send messages with `SendAsync` instead of requesting a result with `InvokeAsync`.
* An exception thrown by this method is logged and then discarded. The connection stays open, the client isn't notified, and the refresh isn't rejected.

For the full feature, including how to enable it and how clients request a refresh, see [SignalR authentication and authorization](authn-and-authz?view=aspnetcore-10.0#authentication-refresh).

## Handle errors

Exceptions thrown in your hub methods are sent to the client that invoked the method. On the JavaScript client, the `invoke` method returns a [JavaScript `Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Using_promises). When the client receives an error with a handler attached to the promise using `catch`, it's invoked and passed as a JavaScript `Error` object:

```
try {
  await connection.invoke("SendMessage", user, message);
} catch (err) {
  console.error(err);
}
```

```
connection.invoke("SendMessage", user, message).catch(err => console.error(err));
```

If your Hub throws an exception, connections aren't closed. By default, SignalR returns a generic error message to the client. For example:

```
Microsoft.AspNetCore.SignalR.HubException: An unexpected error occurred invoking 'MethodName' on the server.
```

Unexpected exceptions often contain sensitive information, such as the name of a database server in an exception triggered when the database connection fails. SignalR doesn't expose these detailed error messages by default as a security measure. For more information on why exception details are suppressed, see [Security considerations in ASP.NET Core SignalR](security?view=aspnetcore-10.0#exceptions).

If you have an exceptional condition you *do* want to propagate to the client, you can use the [HubException](/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubexception) class. If you throw a `HubException` from your hub method, SignalR **will** send the entire message to the client, unmodified:

```
public Task ThrowException()
    => throw new HubException("This error will be sent to the client!");
```

Note

SignalR only sends the `Message` property of the exception to the client. The stack trace and other properties on the exception aren't available to the client.

## Related content

* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/signalr/hubs/samples/) [(how to download)](../fundamentals/?view=aspnetcore-10.0#how-to-download-a-sample)
* [Overview of ASP.NET Core SignalR](introduction?view=aspnetcore-10.0)
* [ASP.NET Core SignalR JavaScript client](javascript-client?view=aspnetcore-10.0)
* [Publish an ASP.NET Core SignalR app to Azure App Service](publish-to-azure-web-app?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-09-25

----
url: https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/10/openapi-analyzers-deprecated
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/breaking-changes/10/openapi-analyzers-deprecated.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# IncludeOpenAPIAnalyzers property and MVC API analyzers are deprecated

The `IncludeOpenAPIAnalyzers` MSBuild property and its associated MVC API analyzers are deprecated and will be removed in a future release. When `IncludeOpenAPIAnalyzers` is set to `true`, the build now emits warning `ASPDEPR007`.

## Version introduced

.NET 10 Preview 7

## Previous behavior

Previously, you could set `<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>` in your Web SDK projects to enable MVC API analyzers without any warnings or deprecation notices.

```
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
  </PropertyGroup>
</Project>
```

Such a project built successfully without any deprecation warnings.

## New behavior

Starting in .NET 10, when `IncludeOpenAPIAnalyzers` is set to `true`, the build emits warning `ASPDEPR007`:

> warning ASPDEPR007: The IncludeOpenAPIAnalyzers property and its associated MVC API analyzers are deprecated and will be removed in a future release.

The analyzers continue to function, but developers receive this deprecation warning during compilation.

## Type of breaking change

This change can affect [source compatibility](/en-us/dotnet/core/compatibility/categories#source-compatibility).

## Reason for change

The MVC API analyzers were originally introduced to help keep return types and attributes in sync for API controllers, ensuring consistency between method signatures and their corresponding OpenAPI documentation. These analyzers provided compile-time validation to catch mismatches between declared return types and the actual types returned by controller actions.

However, with the introduction of Minimal APIs and the [TypedResults](/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults) pattern, the .NET ecosystem has evolved toward a more type-safe approach for API development. `TypedResults` provides compile-time guarantees about response types without requiring additional analyzers, making the MVC API analyzers redundant for modern .NET applications. In .NET 10, `TypedResults` are supported in controller-based APIs.

Previous approach with MVC API analyzers:

```
[HttpGet]
[ProducesResponseType<Product>(200)]
[ProducesResponseType(404)]
public async Task<ActionResult> GetProduct(int id)
{
    var product = await _productService.GetByIdAsync(id);
    if (product == null)
        return NotFound(); // Analyzer ensures this matches ProducesResponseType(404)

    return Ok(product); // Analyzer ensures this matches ProducesResponseType<Product>(200)
}
```

Modern approach with `TypedResults`:

```
[HttpGet("{id}")]
public async Task<Results<Ok<Product>, NotFound>> GetProduct(int id)
{
    var product = await _productService.GetByIdAsync(id);
    return product == null
        ? TypedResults.NotFound()
        : TypedResults.Ok(product);
}
```

The `TypedResults` pattern eliminates the need for separate analyzers because the return type itself (`Results<Ok<Product>, NotFound>`) explicitly declares all possible response types at compile time. This approach is more maintainable, provides better IntelliSense support, and automatically generates accurate OpenAPI documentation without additional tooling.

As the .NET ecosystem continues to embrace `TypedResults` as the recommended pattern for API development, the MVC API analyzers have become obsolete and are being deprecated to streamline the development experience.

## Recommended action

Developers should:

* Remove the deprecated property: Remove `<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>` from your project files to eliminate the warning.
* Migrate to `TypedResults`: Migrate to the `TypedResults` pattern to ensure better consistency between application behavior and OpenAPI documentation.

If you need to continue using the deprecated analyzers temporarily, you can suppress the warning:

```
<PropertyGroup>
  <NoWarn>$(NoWarn);ASPDEPR007</NoWarn>
</PropertyGroup>
```

## Affected APIs

* MSBuild property: `IncludeOpenAPIAnalyzers`.
* Associated MVC API analyzers included when `IncludeOpenAPIAnalyzers` is `true`.

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-01-28

----
url: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization
----

[Read in English ](#)[Edit](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/security/authentication/identity-api-authorization.md)

***

***

Note

Access to this page requires authorization. You can try [signing in](#) or [changing directories]().

Access to this page requires authorization. You can try [changing directories]().

# How to use Identity to secure a Web API backend for SPAs

Note

This isn't the latest version of this article. For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

Warning

This version of ASP.NET Core is no longer supported. For more information, see the [.NET and .NET Core Support Policy](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). For the current release, see the [.NET 10 version of this article](?view=aspnetcore-10.0\&preserve-view=true).

[ASP.NET Core Identity](identity?view=aspnetcore-10.0) provides APIs that handle authentication, authorization, and identity management. The APIs make it possible to secure endpoints of a Web API backend with cookie-based authentication. A token-based option is available for clients that can't use cookies, but in using this you are responsible for ensuring the tokens are kept secure. We recommend using cookies for browser-based applications, because, by default, the browser automatically handles them without exposing them to JavaScript.

This article shows how to use Identity to secure a Web API backend for SPAs such as Angular, React, and Vue apps. The same backend APIs can be used to secure [Blazor WebAssembly apps](../../blazor/security/webassembly/standalone-with-identity/?view=aspnetcore-10.0).

## Prerequisites

The steps shown in this article add authentication and authorization to an ASP.NET Core Web API app that:

* Isn't already configured for authentication.
* Targets `net8.0` or later.
* Can be either Minimal API or controller-based API.

Some of the testing instructions in this article use the [Swagger UI](/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger) that's included with the project template. The Swagger UI isn't required to use Identity with a Web API backend.

## Install NuGet packages

Install the following NuGet packages:

* [`Microsoft.AspNetCore.Identity.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.EntityFrameworkCore) - Enables Identity to work with Entity Framework Core (EF Core).

* One that enables EF Core to work with a database, such as one of the following packages:

  * [`Microsoft.EntityFrameworkCore.SqlServer`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer) or
  * [`Microsoft.EntityFrameworkCore.Sqlite`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite) or
  * [`Microsoft.EntityFrameworkCore.InMemory`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory).

For the quickest way to get started, use the in-memory database.

Change the database later to SQLite or SQL Server to save user data between sessions when testing or for production use. That introduces some complexity compared to in-memory, as it requires the database to be created through [migrations](/en-us/ef/core/managing-schemas/migrations/), as shown in the [EF Core getting started tutorial](/en-us/ef/core/get-started/overview/first-app).

Install these packages by using the [NuGet package manager in Visual Studio](/en-us/nuget/consume-packages/install-use-packages-visual-studio) or the [dotnet add package](/en-us/dotnet/core/tools/dotnet-add-package) CLI command.

## Create an `IdentityDbContext`

Add a class named `ApplicationDbContext` that inherits from [IdentityDbContext\<TUser>](/en-us/dotnet/api/microsoft.aspnetcore.identity.entityframeworkcore.identitydbcontext-1):

```
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) :
        base(options)
    { }
}
```

The code shown provides a special constructor that makes it possible to configure the database for different environments.

Add one or more of the following `using` directives as needed when adding the code shown in these steps.

```
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
```

## Configure the EF Core context

As noted earlier, the simplest way to get started is to use the in-memory database. With in-memory each run starts with a fresh database, and there's no need to use migrations. After the call to `WebApplication.CreateBuilder(args)`, add the following code to configure Identity to use an in-memory database:

```
builder.Services.AddDbContext<ApplicationDbContext>(
    options => options.UseInMemoryDatabase("AppDb"));
```

To save user data between sessions when testing or for production use, change the database later to SQLite or SQL Server.

## Add Identity services to the container

After the call to `WebApplication.CreateBuilder(args)`, call [AddAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.authorizationservicecollectionextensions.addauthorization) to add services to the dependency injection (DI) container:

```
builder.Services.AddAuthorization();
```

## Activate Identity APIs

After the call to `WebApplication.CreateBuilder(args)`, call [AddIdentityApiEndpoints\<TUser>(IServiceCollection)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityservicecollectionextensions.addidentityapiendpoints#microsoft-extensions-dependencyinjection-identityservicecollectionextensions-addidentityapiendpoints-1\(microsoft-extensions-dependencyinjection-iservicecollection\)) and [AddEntityFrameworkStores\<TContext>(IdentityBuilder)](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityentityframeworkbuilderextensions.addentityframeworkstores#microsoft-extensions-dependencyinjection-identityentityframeworkbuilderextensions-addentityframeworkstores-1\(microsoft-aspnetcore-identity-identitybuilder\)).

```
builder.Services.AddIdentityApiEndpoints<IdentityUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();
```

By default, both cookies and proprietary tokens are activated. Cookies are issued at login if either the `useCookies` or `useSessionCookies` query string parameter in the login endpoint is `true`.

## Map Identity routes

After the call to `builder.Build()`, call [MapIdentityApi\<TUser>(IEndpointRouteBuilder)](/en-us/dotnet/api/microsoft.aspnetcore.routing.identityapiendpointroutebuilderextensions.mapidentityapi#microsoft-aspnetcore-routing-identityapiendpointroutebuilderextensions-mapidentityapi-1\(microsoft-aspnetcore-routing-iendpointroutebuilder\)) to map the Identity endpoints:

```
app.MapIdentityApi<IdentityUser>();
```

## Secure selected endpoints

To secure an endpoint, use the [RequireAuthorization](/en-us/dotnet/api/microsoft.aspnetcore.builder.authorizationendpointconventionbuilderextensions.requireauthorization) extension method on the `Map{Method}` call that defines the route. For example:

```
app.MapGet("/weatherforecast", (HttpContext httpContext) =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = summaries[Random.Shared.Next(summaries.Length)]
        })
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi()
.RequireAuthorization();
```

The `RequireAuthorization` method can also be used to:

* Secure Swagger UI endpoints, as shown in the following example:

  ```
  app.MapSwagger().RequireAuthorization();
  ```

* Secure with a specific claim or permission, as shown in the following example:

  ```
  .RequireAuthorization("Admin");
  ```

In a controller-based web API project, secure endpoints by applying the [\[`Authorize`\]](/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizeattribute) attribute to a controller or action.

## Test the API

A quick way to test authentication is to use the in-memory database and the Swagger UI that's included with the project template. The following steps show how to test the API with the Swagger UI. Make sure that the [Swagger UI endpoints aren't secured](#secure-selected-endpoints).

### Attempt to access a secured endpoint

* Run the app and navigate to the Swagger UI.
* Expand a secured endpoint, such as `/weatherforecast` in a project created by the web API template.
* Select **Try it out**.
* Select **Execute**. The response is `401 - not authorized`.

### Test registration

* Expand `/register` and select **Try it out**.

* In the **Parameters** section of the UI, a sample request body is shown:

  ```
  {
    "email": "string",
    "password": "string"
  }
  ```

* Replace "string" with a valid email address and password, and then select **Execute**.

  To comply with the default password validation rules, the password must be at least six characters long and contain at least one of each of the following characters:

  * Uppercase letter
  * Lowercase letter
  * Numeric digit
  * Nonalphanumeric character

  If you enter an invalid email address or a bad password, the result includes the validation errors. Here's an example of a response body with validation errors:

  ```
  {
    "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "errors": {
      "PasswordTooShort": [
        "Passwords must be at least 6 characters."
      ],
      "PasswordRequiresNonAlphanumeric": [
        "Passwords must have at least one non alphanumeric character."
      ],
      "PasswordRequiresDigit": [
        "Passwords must have at least one digit ('0'-'9')."
      ],
      "PasswordRequiresLower": [
        "Passwords must have at least one lowercase ('a'-'z')."
      ]
    }
  }
  ```

  The errors are returned in the [ProblemDetails](/en-us/aspnet/core/web-api/handle-errors#validation-failure-error-response) format so the client can parse them and display validation errors as needed.

  A successful registration results in a `200 - OK` response.

### Test login

* Expand `/login` and select **Try it out**. The example request body shows two additional parameters:

  ```
  {
    "email": "string",
    "password": "string",
    "twoFactorCode": "string",
    "twoFactorRecoveryCode": "string"
  }
  ```

  The extra JSON properties aren't needed for this example and can be deleted. Set `useCookies` to `true`.

* Replace "string" with the email address and password that you used to register, and then select **Execute**.

  A successful login results in a `200 - OK` response with a cookie in the response header.

### Retest the secured endpoint

After a successful login, rerun the secured endpoint. The authentication cookie is automatically sent with the request, and the endpoint is authorized. Cookie-based authentication is securely built-in to the browser and "just works."

### Testing with nonbrowser clients

Some web clients might not include cookies in the header by default:

* If you're using a tool for testing APIs, you might need to enable cookies in the settings.

* The JavaScript `fetch` API doesn't include cookies by default. Enable them by setting `credentials` to the value `include` in the options.

* An `HttpClient` running in a Blazor WebAssembly app needs the `HttpRequestMessage` to include credentials, like the following example:

  ```
  request.SetBrowserRequestCredential(BrowserRequestCredentials.Include);
  ```

## Use token-based authentication

We recommend using cookies in browser-based applications, because, by default, the browser automatically handles them without exposing them to JavaScript.

A custom token (one that is proprietary to the ASP.NET Core identity platform) is issued that can be used to authenticate subsequent requests. The short-lived access token is passed in the `Authorization` header as a bearer token. A longer-lived refresh token is also provided. This refresh token allows the application to request a new access token when the old one expires without forcing the user to log in again.

The tokens aren't standard JSON Web Tokens (JWTs). The use of custom tokens is intentional, as the built-in Identity API is meant primarily for simple scenarios. The token option isn't intended to be a full-featured identity service provider or token server, but instead an alternative to the cookie option for clients that can't use cookies.

To use token-based authentication, set `useCookies` and `useSessionCookies` to `false` (or omit them) when calling the `/login` endpoint. Tokens use the *Bearer* authentication scheme. Using the token returned from the call to `/login`, subsequent calls to protected endpoints should add the header `Authorization: Bearer {TOKEN}`, where the `{TOKEN}` placeholder is the access token. For more information, see the [Use the `POST /login` endpoint](#use-the-post-login-endpoint) section.

## Log out

To provide a way for the user to log out, define a `/logout` endpoint like the following example:

```
app.MapPost("/logout", async (SignInManager<IdentityUser> signInManager,
    [FromBody] object empty) =>
{
    if (empty != null)
    {
        await signInManager.SignOutAsync();
        return Results.Ok();
    }
    return Results.Unauthorized();
})
.WithOpenApi()
.RequireAuthorization();
```

Provide an empty JSON object (`{}`) in the request body when calling this endpoint. The following code is an example of a call to the logout endpoint:

```
public signOut() {
  return this.http.post('/logout', {}, {
    withCredentials: true,
    observe: 'response',
    responseType: 'text'
```

## SignOut everywhere

Apps need to react to events involving security-sensitive actions such as password changes, or other security-sensitive events. This is achieved using the [security stamp](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser-1.securitystamp) feature of Identity.

Apps need to react to security-sensitive actions such as password changes. Identity achieves this using the [security stamp](/en-us/dotnet/api/microsoft.aspnetcore.identity.identityuser-1.securitystamp) feature:

* For cookie-based authentication, the security stamp is periodically revalidated based on [SecurityStampValidatorOptions.ValidationInterval](/en-us/dotnet/api/microsoft.aspnetcore.identity.securitystampvalidatoroptions.validationinterval).
* For token-based authentication, the access token lifetime set by [BearerTokenOptions.BearerTokenExpiration](/en-us/dotnet/api/microsoft.aspnetcore.authentication.bearertoken.bearertokenoptions.bearertokenexpiration) limits how long a session remains active after a security-sensitive change.

The validation interval is a balance between immediate session invalidation and database performance. A shorter interval requires more frequent database hits, while a longer one leaves a small window where an old, potentially compromised session might remain active.

## The `MapIdentityApi<TUser>` endpoints

The call to `MapIdentityApi<TUser>` adds the following endpoints to the app:

* [`POST /register`](#use-the-post-register-endpoint)
* [`POST /login`](#use-the-post-login-endpoint)
* [`POST /refresh`](#use-the-post-refresh-endpoint)
* [`GET /confirmEmail`](#use-the-get-confirmemail-endpoint)
* [`POST /resendConfirmationEmail`](#use-the-post-resendconfirmationemail-endpoint)
* [`POST /forgotPassword`](#use-the-post-forgotpassword-endpoint)
* [`POST /resetPassword`](#use-the-post-resetpassword-endpoint)
* [`POST /manage/2fa`](#use-the-post-manage2fa-endpoint)
* [`GET /manage/info`](#use-the-get-manageinfo-endpoint)
* [`POST /manage/info`](#use-the-post-manageinfo-endpoint)

## Use the `POST /register` endpoint

The request body must have [Email](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.loginrequest.email#microsoft-aspnetcore-identity-data-loginrequest-email) and [Password](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.loginrequest.password#microsoft-aspnetcore-identity-data-loginrequest-password) properties:

```
{
  "email": "string",
  "password": "string",
}
```

For more information, see:

* [Test registration](#test-registration) earlier in this article.
* [RegisterRequest](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.registerrequest).

## Use the `POST /login` endpoint

In the request body, [Email](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.loginrequest.email#microsoft-aspnetcore-identity-data-loginrequest-email) and [Password](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.loginrequest.password#microsoft-aspnetcore-identity-data-loginrequest-password) are required. If two-factor authentication (2FA) is enabled, either [TwoFactorCode](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.loginrequest.twofactorcode#microsoft-aspnetcore-identity-data-loginrequest-twofactorcode) or [TwoFactorRecoveryCode](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.loginrequest.twofactorrecoverycode#microsoft-aspnetcore-identity-data-loginrequest-twofactorrecoverycode) is required. If 2FA isn't enabled, omit both `twoFactorCode` and `twoFactorRecoveryCode`. For more information, see [Use the `POST /manage/2fa` endpoint](#use-the-post-manage2fa-endpoint) later in this article.

Here's a request body example with 2FA not enabled:

```
{
  "email": "string",
  "password": "string"
}
```

Here are request body examples with 2FA enabled:

* ```
  {
    "email": "string",
    "password": "string",
    "twoFactorCode": "string",
  }
  ```
* ```
  {
    "email": "string",
    "password": "string",
    "twoFactorRecoveryCode": "string"
  }
  ```

The endpoint supports the following query string parameters:

* `useCookies`: Set to `true` to issue a persistent authentication cookie when `useSessionCookies` isn't `true`.
* `useSessionCookies`: Set to `true` to issue a session (non-persistent) authentication cookie.

If both `useCookies` and `useSessionCookies` are set to `true`, a session cookie is issued. If both parameters are `false` or omitted, token-based authentication is used.

For more information about cookie-based authentication, see [Test login](#test-login) earlier in this article.

### Token-based authentication

If both `useCookies` and `useSessionCookies` are `false` or omitted, token-based authentication is enabled. The response body includes the following properties:

```
{
  "tokenType": "string",
  "accessToken": "string",
  "expiresIn": 0,
  "refreshToken": "string"
}
```

For more information about these properties, see [AccessTokenResponse](/en-us/dotnet/api/microsoft.aspnetcore.authentication.bearertoken.accesstokenresponse).

Use the [BearerTokenOptions.BearerTokenExpiration](/en-us/dotnet/api/microsoft.aspnetcore.authentication.bearertoken.bearertokenoptions.bearertokenexpiration) property to set how long the access token remains valid.

Put the access token in a header to make authenticated requests, as shown in the following example

```
Authorization: Bearer {access token}
```

When the access token is about to expire, call the [/refresh](#use-the-post-refresh-endpoint) endpoint.

## Use the `POST /refresh` endpoint

For use only with token-based authentication. Gets a new access token without forcing the user to log in again. Call this endpoint when the access token is about to expire.

The request body contains only the [RefreshToken](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.refreshrequest.refreshtoken#microsoft-aspnetcore-identity-data-refreshrequest-refreshtoken). Here's a request body example:

```
{
  "refreshToken": "string"
}
```

If the call is successful, the response body is a new [AccessTokenResponse](/en-us/dotnet/api/microsoft.aspnetcore.authentication.bearertoken.accesstokenresponse), as shown in the following example:

```
{
  "tokenType": "string",
  "accessToken": "string",
  "expiresIn": 0,
  "refreshToken": "string"
}
```

Use the [BearerTokenOptions.RefreshTokenExpiration](/en-us/dotnet/api/microsoft.aspnetcore.authentication.bearertoken.bearertokenoptions.refreshtokenexpiration) property to set how long the refresh token remains valid.

## Use the `GET /confirmEmail` endpoint

If Identity is set up for email confirmation, a successful call to the `/register` endpoint sends an email that contains a link to the `/confirmEmail` endpoint. The link contains the following query string parameters:

* `userId`
* `code`
* `changedEmail` - Included only if the user changed the email address during registration.

Identity provides default text for the confirmation email. By default, the email subject is "Confirm your email" and the email body looks like the following example:

```
 Please confirm your account by <a href='https://contoso.com/confirmEmail?userId={user ID}&code={generated code}&changedEmail={new email address}'>clicking here</a>.
```

If the [RequireConfirmedEmail](/en-us/dotnet/api/microsoft.aspnetcore.identity.signinoptions.requireconfirmedemail#microsoft-aspnetcore-identity-signinoptions-requireconfirmedemail) property is set to `true`, the user can't log in until the email address is confirmed by clicking the link in the email. The `/confirmEmail` endpoint:

* Confirms the email address and enables the user to log in.
* Returns the text "Thank you for confirming your email." in the response body.

To set up Identity for email confirmation, add code in `Program.cs` to set `RequireConfirmedEmail` to `true` and add a class that implements [IEmailSender](/en-us/dotnet/api/microsoft.aspnetcore.identity.ui.services.iemailsender) to the DI container. For example:

```
builder.Services.Configure<IdentityOptions>(options =>
{
    options.SignIn.RequireConfirmedEmail = true;
});

builder.Services.AddTransient<IEmailSender, EmailSender>();
```

For more information, see [Account confirmation and password recovery](accconfirm?view=aspnetcore-10.0).

Identity provides default text for the other emails that need to be sent as well, such as for 2FA and password reset. To customize these emails, provide a custom implementation of the `IEmailSender` interface. In the preceding example, `EmailSender` is a class that implements `IEmailSender`. For more information, including an example of a class that implements `IEmailSender`, see [Account confirmation and password recovery](accconfirm?view=aspnetcore-10.0).

## Use the `POST /resendConfirmationEmail` endpoint

Sends an email only if the address is valid for a registered user.

The request body contains only the [Email](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.resendconfirmationemailrequest.email#microsoft-aspnetcore-identity-data-resendconfirmationemailrequest-email). Here's a request body example:

```
{
  "email": "string"
}
```

For more information, see [Use the `GET /confirmEmail` endpoint](#use-the-get-confirmemail-endpoint) earlier in this article.

## Use the `POST /forgotPassword` endpoint

Generates an email that contains a password reset code. Send that code to [`/resetPassword`](#use-the-post-resetpassword-endpoint) with a new password.

The request body contains only the [Email](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.forgotpasswordrequest.email#microsoft-aspnetcore-identity-data-forgotpasswordrequest-email). Here's an example:

```
{
  "email": "string"
}
```

For information about how to enable Identity to send emails, see [Use the `GET /confirmEmail` endpoint](#use-the-get-confirmemail-endpoint).

## Use the `POST /resetPassword` endpoint

Call this endpoint after getting a reset code by calling the `/forgotPassword` endpoint.

The request body requires [Email](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.resetpasswordrequest.email#microsoft-aspnetcore-identity-data-resetpasswordrequest-email), [ResetCode](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.resetpasswordrequest.resetcode#microsoft-aspnetcore-identity-data-resetpasswordrequest-resetcode), and [NewPassword](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.resetpasswordrequest.newpassword#microsoft-aspnetcore-identity-data-resetpasswordrequest-newpassword). Here's an example:

```
{
  "email": "string",
  "resetCode": "string",
  "newPassword": "string"
}
```

## Use the `POST /manage/2fa` endpoint

Configures two-factor authentication (2FA) for the user. When 2FA is enabled, successful login requires a code produced by an authenticator app in addition to the email address and password.

### Enable 2FA

To enable 2FA for the currently authenticated user:

* Call the `/manage/2fa` endpoint, sending an empty JSON object (`{}`) in the request body.

* The response body provides the [SharedKey](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.twofactorresponse.sharedkey#microsoft-aspnetcore-identity-data-twofactorresponse-sharedkey) along with some other properties that aren't needed at this point. The shared key is used to set up the authenticator app. Response body example:

  ```
  {
    "sharedKey": "string",
    "recoveryCodesLeft": 0,
    "recoveryCodes": null,
    "isTwoFactorEnabled": false,
    "isMachineRemembered": false
  }
  ```

* Use the shared key to get a Time-based one-time password (TOTP). For more information, see [Enable QR code generation for TOTP authentication](identity-enable-qrcodes?view=aspnetcore-10.0).

* Call the `/manage/2fa` endpoint, sending the TOTP and `"enable": true` in the request body. For example:

  ```
  {
    "enable": true,
    "twoFactorCode": "string"
  }
  ```

* The response body confirms that [IsTwoFactorEnabled](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.twofactorresponse.istwofactorenabled#microsoft-aspnetcore-identity-data-twofactorresponse-istwofactorenabled) is true and provides the [RecoveryCodes](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.twofactorresponse.recoverycodes#microsoft-aspnetcore-identity-data-twofactorresponse-recoverycodes). The recovery codes are used to log in when the authenticator app isn't available. Response body example after successfully enabling 2FA:

  ```
  {
    "sharedKey": "string",
    "recoveryCodesLeft": 10,
    "recoveryCodes": [
      "string",
      "string",
      "string",
      "string",
      "string",
      "string",
      "string",
      "string",
      "string",
      "string"
    ],
    "isTwoFactorEnabled": true,
    "isMachineRemembered": false
  }
  ```

### Log in with 2FA

Call the `/login` endpoint, sending the email address, password, and TOTP in the request body. For example:

```
{
  "email": "string",
  "password": "string",
  "twoFactorCode": "string"
}
```

If the user doesn't have access to the authenticator app, log in by calling the `/login` endpoint with one of the recovery codes that was provided when 2FA was enabled. The request body looks like the following example:

```
{
  "email": "string",
  "password": "string",
  "twoFactorRecoveryCode": "string"
}
```

### Reset the recovery codes

To get a new collection of recovery codes, call this endpoint with [ResetRecoveryCodes](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.twofactorrequest.resetrecoverycodes#microsoft-aspnetcore-identity-data-twofactorrequest-resetrecoverycodes) set to `true`. Here's a request body example:

```
{
  "resetRecoveryCodes": true
}
```

### Reset the shared key

To get a new random shared key, call this endpoint with [ResetSharedKey](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.twofactorrequest.resetsharedkey#microsoft-aspnetcore-identity-data-twofactorrequest-resetsharedkey) set to `true`. Here's a request body example:

```
{
  "resetSharedKey": true
}
```

Resetting the key automatically disables the two-factor login requirement for the authenticated user until it's re-enabled by a later request.

### Forget the machine

To clear the cookie "remember me flag" if present, call this endpoint with [ForgetMachine](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.twofactorrequest.forgetmachine#microsoft-aspnetcore-identity-data-twofactorrequest-forgetmachine) set to true. Here's a request body example:

```
{
  "forgetMachine": true
}
```

This endpoint has no impact on token-based authentication.

## Use the `GET /manage/info` endpoint

Gets email address and email confirmation status of the logged-in user. Claims were omitted from this endpoint for security reasons. If claims are needed, use the server-side APIs to set up an endpoint for claims. Or instead of sharing all of the users' claims, provide a validation endpoint that accepts a claim and responds whether the user has it.

The request doesn't require any parameters. The response body includes the [Email](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.inforesponse.email#microsoft-aspnetcore-identity-data-inforesponse-email) and [IsEmailConfirmed](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.inforesponse.isemailconfirmed#microsoft-aspnetcore-identity-data-inforesponse-isemailconfirmed) properties, as in the following example:

```
{
  "email": "string",
  "isEmailConfirmed": true
}
```

## Use the `POST /manage/info` endpoint

Updates the email address and password of the logged-in user. Send [NewEmail](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.inforequest.newemail#microsoft-aspnetcore-identity-data-inforequest-newemail), [NewPassword](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.inforequest.newpassword#microsoft-aspnetcore-identity-data-inforequest-newpassword), and [OldPassword](/en-us/dotnet/api/microsoft.aspnetcore.identity.data.inforequest.oldpassword#microsoft-aspnetcore-identity-data-inforequest-oldpassword) in the request body, as shown in the following example:

```
{
  "newEmail": "string",
  "newPassword": "string",
  "oldPassword": "string"
}
```

Here's an example of the response body:

```
{
  "email": "string",
  "isEmailConfirmed": false
}
```

## See also

For more information, see the following resources:

* [Choose an identity management solution](../how-to-choose-identity-solution?view=aspnetcore-10.0)

* [Identity management solutions for .NET web apps](../identity-management-solutions?view=aspnetcore-10.0)

* [Simple authorization in ASP.NET Core](../authorization/simple?view=aspnetcore-10.0)

* [Add, download, and delete user data to Identity in an ASP.NET Core project](add-user-data?view=aspnetcore-10.0)

* [Create an ASP.NET Core app with user data protected by authorization](../authorization/secure-data?view=aspnetcore-10.0)

* [Account confirmation and password recovery](accconfirm?view=aspnetcore-10.0)

* [Enable QR code generation for TOTP authentication](identity-enable-qrcodes?view=aspnetcore-10.0)

* [Sample Web API backend for SPAs](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/samples/SimpleAuthCookiesAndTokens/SimpleAuthCookiesAndTokens) The .http file shows token-based authentication. For example:

  * Doesn't set `useCookies`
  * Uses the Authorization header to pass the token
  * Shows refresh to extend session without forcing the user to login again

* [Sample Angular app that uses Identity to secure a Web API backend](https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/samples/ngIdentity)

The ASP.NET Core templates offer authentication in Single Page Apps (SPAs) using the support for API authorization. ASP.NET Core Identity for authenticating and storing users is combined with [Duende Identity Server](https://docs.duendesoftware.com) for implementing OpenID Connect.

Important

[Duende Software](https://duendesoftware.com/) might require you to pay a license fee for production use of Duende Identity Server. For more information, see [Migrate from ASP.NET Core in .NET 5 to .NET 6](../../migration/50-to-60?view=aspnetcore-10.0#project-templates-use-duende-identity-server).

An authentication parameter was added to the **Angular** and **React** project templates that is similar to the authentication parameter in the **Web Application (Model-View-Controller)** (MVC) and **Web Application** (Razor Pages) project templates. The allowed parameter values are **None** and **Individual**. The **React.js and Redux** project template doesn't support the authentication parameter at this time.

## Create an app with API authorization support

User authentication and authorization can be used with both Angular and React SPAs. Open a command shell, and run the following command:

**Angular**:

```
dotnet new angular -au Individual
```

**React**:

```
dotnet new react -au Individual
```

The preceding command creates an ASP.NET Core app with a *ClientApp* directory containing the SPA.

## General description of the ASP.NET Core components of the app

The following sections describe additions to the project when authentication support is included:

### `Program.cs`

The following code examples rely on the [Microsoft.AspNetCore.ApiAuthorization.IdentityServer](https://www.nuget.org/packages/Microsoft.AspNetCore.ApiAuthorization.IdentityServer) NuGet package. The examples configure API authentication and authorization using the [AddApiAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityserverbuilderconfigurationextensions.addapiauthorization) and [AddIdentityServerJwt](/en-us/dotnet/api/microsoft.aspnetcore.apiauthorization.identityserver.apiresourcecollection.addidentityserverjwt) extension methods. Projects using the React or Angular SPA project templates with authentication include a reference to this package.

`dotnet new angular -au Individual` generates the following `Program.cs` file:

```
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using output_directory_name.Data;
using output_directory_name.Models;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

builder.Services.AddAuthentication()
    .AddIdentityServerJwt();

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action=Index}/{id?}");
app.MapRazorPages();

app.MapFallbackToFile("index.html");

app.Run();
```

The preceding code configures:

* Identity with the default UI:

  ```
  builder.Services.AddDbContext<ApplicationDbContext>(options =>
      options.UseSqlite(connectionString));
  builder.Services.AddDatabaseDeveloperPageExceptionFilter();

  builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
      .AddEntityFrameworkStores<ApplicationDbContext>();
  ```

* IdentityServer with an additional `AddApiAuthorization` helper method that sets up some default ASP.NET Core conventions on top of IdentityServer:

  ```
  builder.Services.AddIdentityServer()
      .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
  ```

* Authentication with an additional `AddIdentityServerJwt` helper method that configures the app to validate JWT tokens produced by IdentityServer:

  ```
  builder.Services.AddAuthentication()
  .AddIdentityServerJwt();
  ```

* The authentication middleware that is responsible for validating the request credentials and setting the user on the request context:

  ```
  app.UseAuthentication();
  ```

* The IdentityServer middleware that exposes the OpenID Connect endpoints:

  ```
  app.UseIdentityServer();
  ```

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../?view=aspnetcore-10.0#secure-authentication-flows).

### Azure App Service on Linux

For Azure App Service deployments on Linux, specify the issuer explicitly:

```
builder.Services.Configure<JwtBearerOptions>(
    IdentityServerJwtConstants.IdentityServerJwtBearerScheme, 
    options =>
    {
        options.Authority = "{AUTHORITY}";
    });
```

In the preceding code, the `{AUTHORITY}` placeholder is the [Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.authority#microsoft-aspnetcore-authentication-jwtbearer-jwtbeareroptions-authority) to use when making OpenID Connect calls.

Example:

```
options.Authority = "https://contoso-service.azurewebsites.net";
```

### `AddApiAuthorization`

This helper method configures IdentityServer to use our supported configuration. IdentityServer is a powerful and extensible framework for handling app security concerns. At the same time, that exposes unnecessary complexity for the most common scenarios. Consequently, a set of conventions and configuration options is provided to you that are considered a good starting point. Once your authentication needs change, the full power of IdentityServer is still available to customize authentication to suit your needs.

### `AddIdentityServerJwt`

This helper method configures a policy scheme for the app as the default authentication handler. The policy is configured to let Identity handle all requests routed to any subpath in the Identity URL space "/Identity". The `JwtBearerHandler` handles all other requests. Additionally, this method registers an `<<ApplicationName>>API` API resource with IdentityServer with a default scope of `<<ApplicationName>>API` and configures the JWT bearer token middleware to validate tokens issued by IdentityServer for the app.

### `WeatherForecastController`

In the file, notice the `[Authorize]` attribute applied to the class that indicates that the user needs to be authorized based on the default policy to access the resource. The default authorization policy happens to be configured to use the default authentication scheme, which is set up by `AddIdentityServerJwt` to the policy scheme that was mentioned above, making the `JwtBearerHandler` configured by such helper method the default handler for requests to the app.

### `ApplicationDbContext`

In the file, notice the same `DbContext` is used in Identity with the exception that it extends `ApiAuthorizationDbContext` (a more derived class from `IdentityDbContext`) to include the schema for IdentityServer.

To gain full control of the database schema, inherit from one of the available Identity `DbContext` classes and configure the context to include the Identity schema by calling `builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value)` on the `OnModelCreating` method.

### `OidcConfigurationController`

In the file, notice the endpoint that's provisioned to serve the OIDC parameters that the client needs to use.

### `appsettings.json`

In the `appsettings.json` file of the project root, there's a new `IdentityServer` section that describes the list of configured clients. In the following example, there's a single client. The client name corresponds to the app name and is mapped by convention to the OAuth `ClientId` parameter. The profile indicates the app type being configured. It's used internally to drive conventions that simplify the configuration process for the server. There are several profiles available, as explained in the [Application profiles](#application-profiles) section.

```
"IdentityServer": {
  "Clients": {
    "angularindividualpreview3final": {
      "Profile": "IdentityServerSPA"
    }
  }
}
```

### `appsettings.Development.json`

In the `appsettings.Development.json` file of the project root, there's an `IdentityServer` section that describes the key used to sign tokens. When deploying to production, a key needs to be provisioned and deployed alongside the app, as explained in the [Deploy to production](#deploy-to-production) section.

```
"IdentityServer": {
  "Key": {
    "Type": "Development"
  }
}
```

## General description of the Angular app

The authentication and API authorization support in the Angular template resides in its own Angular module in the *ClientApp/src/api-authorization* directory. The module is composed of the following elements:

* 3 components:

  * `login.component.ts`: Handles the app's login flow.

  * `logout.component.ts`: Handles the app's logout flow.

  * `login-menu.component.ts`: A widget that displays one of the following sets of links:

    * User profile management and log out links when the user is authenticated.
    * Registration and log in links when the user isn't authenticated.

* A route safeguard `AuthorizeGuard` that can be added to routes and requires a user to be authenticated before visiting the route.

* An HTTP interceptor `AuthorizeInterceptor` that attaches the access token to outgoing HTTP requests targeting the API when the user is authenticated.

* A service `AuthorizeService` that handles the lower-level details of the authentication process and exposes information about the authenticated user to the rest of the app for consumption.

* An Angular module that defines routes associated with the authentication parts of the app. It exposes the login menu component, the interceptor, the safeguard, and the service for consumption from the rest of the app.

## General description of the React app

The support for authentication and API authorization in the React template resides in the *ClientApp/src/components/api-authorization* directory. It's composed of the following elements:

* 4 components:

  * `Login.js`: Handles the app's login flow.

  * `Logout.js`: Handles the app's logout flow.

  * `LoginMenu.js`: A widget that displays one of the following sets of links:

    * User profile management and log out links when the user is authenticated.
    * Registration and log in links when the user isn't authenticated.

  * `AuthorizeRoute.js`: A route component that requires a user to be authenticated before rendering the component indicated in the `Component` parameter.

* An exported `authService` instance of class `AuthorizeService` that handles the lower-level details of the authentication process and exposes information about the authenticated user to the rest of the app for consumption.

Now that you've seen the main components of the solution, you can take a deeper look at individual scenarios for the app.

## Require authorization on a new API

By default, the system is configured to easily require authorization for new APIs. To do so, create a new controller and add the `[Authorize]` attribute to the controller class or to any action within the controller.

## Customize the API authentication handler

To customize the configuration of the API's JWT handler, configure its [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.jwtbeareroptions) instance:

```
builder.Services.AddAuthentication()
    .AddIdentityServerJwt();

builder.Services.Configure<JwtBearerOptions>(
    IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
    options =>
    {
        ...
    });
```

The API's JWT handler raises events that enable control over the authentication process using `JwtBearerEvents`. To provide support for API authorization, `AddIdentityServerJwt` registers its own event handlers.

To customize the handling of an event, wrap the existing event handler with additional logic as required. For example:

```
builder.Services.Configure<JwtBearerOptions>(
    IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
    options =>
    {
        var onTokenValidated = options.Events.OnTokenValidated;       

        options.Events.OnTokenValidated = async context =>
        {
            await onTokenValidated(context);
            ...
        }
    });
```

In the preceding code, the `OnTokenValidated` event handler is replaced with a custom implementation. This implementation:

1. Calls the original implementation provided by the API authorization support.
2. Run its own custom logic.

## Protect a client-side route (Angular)

Protecting a client-side route is done by adding the authorize safeguard to the list of safeguards to run when configuring a route. As an example, you can see how the `fetch-data` route is configured within the main app Angular module:

```
RouterModule.forRoot([
  // ...
  { path: 'fetch-data', component: FetchDataComponent, canActivate: [AuthorizeGuard] },
])
```

It's important to mention that protecting a route doesn't protect the actual endpoint (which still requires an `[Authorize]` attribute applied to it) but that it only prevents the user from navigating to the given client-side route when it isn't authenticated.

## Authenticate API requests (Angular)

Authenticating requests to APIs hosted alongside the app is done automatically through the use of the HTTP client interceptor defined by the app.

## Protect a client-side route (React)

Protect a client-side route by using the `AuthorizeRoute` component instead of the plain `Route` component. For example, notice how the `fetch-data` route is configured within the `App` component:

```
<AuthorizeRoute path='/fetch-data' component={FetchData} />
```

Protecting a route:

* Doesn't protect the actual endpoint (which still requires an `[Authorize]` attribute applied to it).
* Only prevents the user from navigating to the given client-side route when it isn't authenticated.

## Authenticate API requests (React)

Authenticating requests with React is done by first importing the `authService` instance from the `AuthorizeService`. The access token is retrieved from the `authService` and is attached to the request as shown below. In React components, this work is typically done in the `componentDidMount` lifecycle method or as the result from some user interaction.

### Import the `authService` into a component

```
import authService from './api-authorization/AuthorizeService'
```

### Retrieve and attach the access token to the response

```
async populateWeatherData() {
  const token = await authService.getAccessToken();
  const response = await fetch('api/SampleData/WeatherForecasts', {
    headers: !token ? {} : { 'Authorization': `Bearer ${token}` }
  });
  const data = await response.json();
  this.setState({ forecasts: data, loading: false });
}
```

## Deploy to production

To deploy the app to production, the following resources need to be provisioned:

* A database to store the Identity user accounts and the IdentityServer grants.

* A production certificate to use for signing tokens.

  * There are no specific requirements for this certificate; it can be a self-signed certificate or a certificate provisioned through a CA authority.
  * It can be generated through standard tools like PowerShell or OpenSSL.
  * It can be installed into the certificate store on the target machines or deployed as a *.pfx* file with a strong password.

### Example: Deploy to a non-Azure web hosting provider

In your web hosting panel, create or load your certificate. Then in the app's `appsettings.json` file, modify the `IdentityServer` section to include the key details. For example:

```
"IdentityServer": {
  "Key": {
    "Type": "Store",
    "StoreName": "WebHosting",
    "StoreLocation": "CurrentUser",
    "Name": "CN=MyApplication"
  }
}
```

In the preceding example:

* `StoreName` represents the name of the certificate store where the certificate is stored. In this case, it points to the web hosting store.
* `StoreLocation` represents where to load the certificate from (`CurrentUser` in this case).
* `Name` corresponds with the distinguished subject for the certificate.

### Example: Deploy to Azure App Service

This section describes deploying the app to Azure App Service using a certificate stored in the certificate store. To modify the app to load a certificate from the certificate store, a Standard tier service plan or better is required when you configure the app in the Azure portal in a later step.

In the app's `appsettings.json` file, modify the `IdentityServer` section to include the key details:

```
"IdentityServer": {
  "Key": {
    "Type": "Store",
    "StoreName": "My",
    "StoreLocation": "CurrentUser",
    "Name": "CN=MyApplication"
  }
}
```

* The store name represents the name of the certificate store where the certificate is stored. In this case, it points to the personal user store.
* The store location represents where to load the certificate from (`CurrentUser` or `LocalMachine`).
* The name property on certificate corresponds with the distinguished subject for the certificate.

To deploy to Azure App Service, follow the steps in [Deploy the app to Azure](../../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0#deploy-the-app-to-azure), which explains how to create the necessary Azure resources and deploy the app to production.

After following the preceding instructions, the app is deployed to Azure but isn't yet functional. The certificate used by the app must be configured in the Azure portal. Locate the thumbprint for the certificate and follow the steps described in [Load your certificates](/en-us/azure/app-service/app-service-web-ssl-cert-load#load-the-certificate-in-code).

While these steps mention SSL, there's a **Private certificates** section in the Azure portal where you can upload the provisioned certificate to use with the app.

After configuring the app and the app's settings in the Azure portal, restart the app in the portal.

## Other configuration options

The support for API authorization builds on top of IdentityServer with a set of conventions, default values, and enhancements to simplify the experience for SPAs. Needless to say, the full power of IdentityServer is available behind the scenes if the ASP.NET Core integrations don't cover your scenario. The ASP.NET Core support is focused on "first-party" apps, where all the apps are created and deployed by our organization. As such, support isn't offered for things like consent or federation. For those scenarios, use IdentityServer and follow their documentation.

### Application profiles

Application profiles are predefined configurations for apps that further define their parameters. At this time, the following profiles are supported:

* `IdentityServerSPA`: Represents a SPA hosted alongside IdentityServer as a single unit.

  * The `redirect_uri` defaults to `/authentication/login-callback`.
  * The `post_logout_redirect_uri` defaults to `/authentication/logout-callback`.
  * The set of scopes includes the `openid`, `profile`, and every scope defined for the APIs in the app.
  * The set of allowed OIDC response types is `id_token token` or each of them individually (`id_token`, `token`).
  * The allowed response mode is `fragment`.

* `SPA`: Represents a SPA that isn't hosted with IdentityServer.

  * The set of scopes includes the `openid`, `profile`, and every scope defined for the APIs in the app.
  * The set of allowed OIDC response types is `id_token token` or each of them individually (`id_token`, `token`).
  * The allowed response mode is `fragment`.

* `IdentityServerJwt`: Represents an API that is hosted alongside with IdentityServer.
  * The app is configured to have a single scope that defaults to the app name.

* `API`: Represents an API that isn't hosted with IdentityServer.
  * The app is configured to have a single scope that defaults to the app name.

### Configuration through `AppSettings`

Configure the apps through the configuration system by adding them to the list of `Clients` or `Resources`.

Configure each client's `redirect_uri` and `post_logout_redirect_uri` property, as shown in the following example:

```
"IdentityServer": {
  "Clients": {
    "MySPA": {
      "Profile": "SPA",
      "RedirectUri": "https://www.example.com/authentication/login-callback",
      "LogoutUri": "https://www.example.com/authentication/logout-callback"
    }
  }
}
```

When configuring resources, you can configure the scopes for the resource as shown below:

```
"IdentityServer": {
  "Resources": {
    "MyExternalApi": {
      "Profile": "API",
      "Scopes": "a b c"
    }
  }
}
```

### Configuration through code

You can also configure the clients and resources through code using an overload of `AddApiAuthorization` that takes an action to configure options.

```
AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
    options.Clients.AddSPA(
        "My SPA", spa =>
        spa.WithRedirectUri("http://www.example.com/authentication/login-callback")
           .WithLogoutRedirectUri(
               "http://www.example.com/authentication/logout-callback"));

    options.ApiResources.AddApiResource("MyExternalApi", resource =>
        resource.WithScopes("a", "b", "c"));
});
```

## Additional resources

* [Use Angular with ASP.NET Core](../../client-side/spa/angular?view=aspnetcore-10.0)
* [Use React with ASP.NET Core](../../client-side/spa/react?view=aspnetcore-10.0)
* [Scaffold Identity in ASP.NET Core projects](scaffold-identity?view=aspnetcore-10.0)

The ASP.NET Core 3.1 or later templates offer authentication in Single Page Apps (SPAs) using the support for API authorization. ASP.NET Core Identity for authenticating and storing users is combined with [IdentityServer](https://identityserver.io/) for implementing OpenID Connect.

An authentication parameter was added to the **Angular** and **React** project templates that is similar to the authentication parameter in the **Web Application (Model-View-Controller)** (MVC) and **Web Application** (Razor Pages) project templates. The allowed parameter values are **None** and **Individual**. The **React.js and Redux** project template doesn't support the authentication parameter at this time.

## Create an app with API authorization support

User authentication and authorization can be used with both Angular and React SPAs. Open a command shell, and run the following command:

**Angular**:

```
dotnet new angular -o <output_directory_name> 
```

**React**:

```
dotnet new react -o <output_directory_name> -au Individual
```

The preceding command creates an ASP.NET Core app with a *ClientApp* directory containing the SPA.

## General description of the ASP.NET Core components of the app

The following sections describe additions to the project when authentication support is included:

### `Startup` class

The following code examples rely on the [Microsoft.AspNetCore.ApiAuthorization.IdentityServer](https://www.nuget.org/packages/Microsoft.AspNetCore.ApiAuthorization.IdentityServer) NuGet package. The examples configure API authentication and authorization using the [AddApiAuthorization](/en-us/dotnet/api/microsoft.extensions.dependencyinjection.identityserverbuilderconfigurationextensions.addapiauthorization) and [AddIdentityServerJwt](/en-us/dotnet/api/microsoft.aspnetcore.apiauthorization.identityserver.apiresourcecollection.addidentityserverjwt) extension methods. Projects using the React or Angular SPA project templates with authentication include a reference to this package.

The `Startup` class has the following additions:

* Inside the `Startup.ConfigureServices` method:

  * Identity with the default UI:

    ```
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<ApplicationUser>()
        .AddEntityFrameworkStores<ApplicationDbContext>();
    ```

  * IdentityServer with an additional `AddApiAuthorization` helper method that sets up some default ASP.NET Core conventions on top of IdentityServer:

    ```
    services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
    ```

  * Authentication with an additional `AddIdentityServerJwt` helper method that configures the app to validate JWT tokens produced by IdentityServer:

    ```
    services.AddAuthentication()
        .AddIdentityServerJwt();
    ```

* Inside the `Startup.Configure` method:

  * The authentication middleware that is responsible for validating the request credentials and setting the user on the request context:

    ```
    app.UseAuthentication();
    ```

  * The IdentityServer middleware that exposes the OpenID Connect endpoints:

    ```
    app.UseIdentityServer();
    ```

Warning

This article shows the use of connection strings. When a local database is used for development and testing, database user authentication via the connection string isn't required. In production environments, connection strings sometimes include a password for authenticating database access or database operations. A resource owner password credential (ROPC) in a connection string is a security risk that should be avoided in production apps. Production apps should use the most secure authentication flow available. For more information on authentication for apps deployed to test or production environments, see [ASP.NET Core security topics](../?view=aspnetcore-10.0#secure-authentication-flows).

### Azure App Service on Linux

For Azure App Service deployments on Linux, specify the issuer explicitly in `Startup.ConfigureServices`:

```
services.Configure<JwtBearerOptions>(
    IdentityServerJwtConstants.IdentityServerJwtBearerScheme, 
    options =>
    {
        options.Authority = "{AUTHORITY}";
    });
```

In the preceding code, the `{AUTHORITY}` placeholder is the [Authority](/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.authority#microsoft-aspnetcore-authentication-jwtbearer-jwtbeareroptions-authority) to use when making OpenID Connect calls.

Example:

```
options.Authority = "https://contoso-service.azurewebsites.net";
```

### `AddApiAuthorization`

This helper method configures IdentityServer to use our supported configuration. IdentityServer is a powerful and extensible framework for handling app security concerns. At the same time, that exposes unnecessary complexity for the most common scenarios. Consequently, a set of conventions and configuration options is provided to you that are considered a good starting point. Once your authentication needs change, the full power of IdentityServer is still available to customize authentication to suit your needs.

### `AddIdentityServerJwt`

This helper method configures a policy scheme for the app as the default authentication handler. The policy is configured to let Identity handle all requests routed to any subpath in the Identity URL space "/Identity". The `JwtBearerHandler` handles all other requests. Additionally, this method registers an `<<ApplicationName>>API` API resource with IdentityServer with a default scope of `<<ApplicationName>>API` and configures the JWT bearer token middleware to validate tokens issued by IdentityServer for the app.

### `WeatherForecastController`

In the file, notice the `[Authorize]` attribute applied to the class that indicates that the user needs to be authorized based on the default policy to access the resource. The default authorization policy happens to be configured to use the default authentication scheme, which is set up by `AddIdentityServerJwt` to the policy scheme that was mentioned above, making the `JwtBearerHandler` configured by such helper method the default handler for requests to the app.

### `ApplicationDbContext`

In the file, notice the same `DbContext` is used in Identity with the exception that it extends `ApiAuthorizationDbContext` (a more derived class from `IdentityDbContext`) to include the schema for IdentityServer.

To gain full control of the database schema, inherit from one of the available Identity `DbContext` classes and configure the context to include the Identity schema by calling `builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value)` on the `OnModelCreating` method.

### `OidcConfigurationController`

In the file, notice the endpoint that's provisioned to serve the OIDC parameters that the client needs to use.

### `appsettings.json`

In the `appsettings.json` file of the project root, there's a new `IdentityServer` section that describes the list of configured clients. In the following example, there's a single client. The client name corresponds to the app name and is mapped by convention to the OAuth `ClientId` parameter. The profile indicates the app type being configured. It's used internally to drive conventions that simplify the configuration process for the server. There are several profiles available, as explained in the [Application profiles](#application-profiles) section.

```
"IdentityServer": {
  "Clients": {
    "angularindividualpreview3final": {
      "Profile": "IdentityServerSPA"
    }
  }
}
```

### `appsettings.Development.json`

In the `appsettings.Development.json` file of the project root, there's an `IdentityServer` section that describes the key used to sign tokens. When deploying to production, a key needs to be provisioned and deployed alongside the app, as explained in the [Deploy to production](#deploy-to-production) section.

```
"IdentityServer": {
  "Key": {
    "Type": "Development"
  }
}
```

## General description of the Angular app

The authentication and API authorization support in the Angular template resides in its own Angular module in the *ClientApp/src/api-authorization* directory. The module is composed of the following elements:

* 3 components:

  * `login.component.ts`: Handles the app's login flow.

  * `logout.component.ts`: Handles the app's logout flow.

  * `login-menu.component.ts`: A widget that displays one of the following sets of links:

    * User profile management and log out links when the user is authenticated.
    * Registration and log in links when the user isn't authenticated.

* A route safeguard `AuthorizeGuard` that can be added to routes and requires a user to be authenticated before visiting the route.

* An HTTP interceptor `AuthorizeInterceptor` that attaches the access token to outgoing HTTP requests targeting the API when the user is authenticated.

* A service `AuthorizeService` that handles the lower-level details of the authentication process and exposes information about the authenticated user to the rest of the app for consumption.

* An Angular module that defines routes associated with the authentication parts of the app. It exposes the login menu component, the interceptor, the safeguard, and the service for consumption from the rest of the app.

## General description of the React app

The support for authentication and API authorization in the React template resides in the *ClientApp/src/components/api-authorization* directory. It's composed of the following elements:

* 4 components:

  * `Login.js`: Handles the app's login flow.

  * `Logout.js`: Handles the app's logout flow.

  * `LoginMenu.js`: A widget that displays one of the following sets of links:

    * User profile management and log out links when the user is authenticated.
    * Registration and log in links when the user isn't authenticated.

  * `AuthorizeRoute.js`: A route component that requires a user to be authenticated before rendering the component indicated in the `Component` parameter.

* An exported `authService` instance of class `AuthorizeService` that handles the lower-level details of the authentication process and exposes information about the authenticated user to the rest of the app for consumption.

Now that you've seen the main components of the solution, you can take a deeper look at individual scenarios for the app.

## Require authorization on a new API

By default, the system is configured to easily require authorization for new APIs. To do so, create a new controller and add the `[Authorize]` attribute to the controller class or to any action within the controller.

## Customize the API authentication handler

To customize the configuration of the API's JWT handler, configure its [JwtBearerOptions](/en-us/dotnet/api/microsoft.aspnetcore.builder.jwtbeareroptions) instance:

```
services.AddAuthentication()
    .AddIdentityServerJwt();

services.Configure<JwtBearerOptions>(
    IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
    options =>
    {
        ...
    });
```

The API's JWT handler raises events that enable control over the authentication process using `JwtBearerEvents`. To provide support for API authorization, `AddIdentityServerJwt` registers its own event handlers.

To customize the handling of an event, wrap the existing event handler with additional logic as required. For example:

```
services.Configure<JwtBearerOptions>(
    IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
    options =>
    {
        var onTokenValidated = options.Events.OnTokenValidated;       

        options.Events.OnTokenValidated = async context =>
        {
            await onTokenValidated(context);
            ...
        }
    });
```

In the preceding code, the `OnTokenValidated` event handler is replaced with a custom implementation. This implementation:

1. Calls the original implementation provided by the API authorization support.
2. Run its own custom logic.

## Protect a client-side route (Angular)

Protecting a client-side route is done by adding the authorize safeguard to the list of safeguards to run when configuring a route. As an example, you can see how the `fetch-data` route is configured within the main app Angular module:

```
RouterModule.forRoot([
  // ...
  { path: 'fetch-data', component: FetchDataComponent, canActivate: [AuthorizeGuard] },
])
```

It's important to mention that protecting a route doesn't protect the actual endpoint (which still requires an `[Authorize]` attribute applied to it) but that it only prevents the user from navigating to the given client-side route when it isn't authenticated.

## Authenticate API requests (Angular)

Authenticating requests to APIs hosted alongside the app is done automatically through the use of the HTTP client interceptor defined by the app.

## Protect a client-side route (React)

Protect a client-side route by using the `AuthorizeRoute` component instead of the plain `Route` component. For example, notice how the `fetch-data` route is configured within the `App` component:

```
<AuthorizeRoute path='/fetch-data' component={FetchData} />
```

Protecting a route:

* Doesn't protect the actual endpoint (which still requires an `[Authorize]` attribute applied to it).
* Only prevents the user from navigating to the given client-side route when it isn't authenticated.

## Authenticate API requests (React)

Authenticating requests with React is done by first importing the `authService` instance from the `AuthorizeService`. The access token is retrieved from the `authService` and is attached to the request as shown below. In React components, this work is typically done in the `componentDidMount` lifecycle method or as the result from some user interaction.

### Import the `authService` into a component

```
import authService from './api-authorization/AuthorizeService'
```

### Retrieve and attach the access token to the response

```
async populateWeatherData() {
  const token = await authService.getAccessToken();
  const response = await fetch('api/SampleData/WeatherForecasts', {
    headers: !token ? {} : { 'Authorization': `Bearer ${token}` }
  });
  const data = await response.json();
  this.setState({ forecasts: data, loading: false });
}
```

## Deploy to production

To deploy the app to production, the following resources need to be provisioned:

* A database to store the Identity user accounts and the IdentityServer grants.

* A production certificate to use for signing tokens.

  * There are no specific requirements for this certificate; it can be a self-signed certificate or a certificate provisioned through a CA authority.
  * It can be generated through standard tools like PowerShell or OpenSSL.
  * It can be installed into the certificate store on the target machines or deployed as a *.pfx* file with a strong password.

### Example: Deploy to a non-Azure web hosting provider

In your web hosting panel, create or load your certificate. Then in the app's `appsettings.json` file, modify the `IdentityServer` section to include the key details. For example:

```
"IdentityServer": {
  "Key": {
    "Type": "Store",
    "StoreName": "WebHosting",
    "StoreLocation": "CurrentUser",
    "Name": "CN=MyApplication"
  }
}
```

In the preceding example:

* `StoreName` represents the name of the certificate store where the certificate is stored. In this case, it points to the web hosting store.
* `StoreLocation` represents where to load the certificate from (`CurrentUser` in this case).
* `Name` corresponds with the distinguished subject for the certificate.

### Example: Deploy to Azure App Service

This section describes deploying the app to Azure App Service using a certificate stored in the certificate store. To modify the app to load a certificate from the certificate store, a Standard tier service plan or better is required when you configure the app in the Azure portal in a later step.

In the app's `appsettings.json` file, modify the `IdentityServer` section to include the key details:

```
"IdentityServer": {
  "Key": {
    "Type": "Store",
    "StoreName": "My",
    "StoreLocation": "CurrentUser",
    "Name": "CN=MyApplication"
  }
}
```

* The store name represents the name of the certificate store where the certificate is stored. In this case, it points to the personal user store.
* The store location represents where to load the certificate from (`CurrentUser` or `LocalMachine`).
* The name property on certificate corresponds with the distinguished subject for the certificate.

To deploy to Azure App Service, follow the steps in [Deploy the app to Azure](../../tutorials/publish-to-azure-webapp-using-vs?view=aspnetcore-10.0#deploy-the-app-to-azure), which explains how to create the necessary Azure resources and deploy the app to production.

After following the preceding instructions, the app is deployed to Azure but isn't yet functional. The certificate used by the app must be configured in the Azure portal. Locate the thumbprint for the certificate and follow the steps described in [Load your certificates](/en-us/azure/app-service/app-service-web-ssl-cert-load#load-the-certificate-in-code).

While these steps mention SSL, there's a **Private certificates** section in the Azure portal where you can upload the provisioned certificate to use with the app.

After configuring the app and the app's settings in the Azure portal, restart the app in the portal.

## Other configuration options

The support for API authorization builds on top of IdentityServer with a set of conventions, default values, and enhancements to simplify the experience for SPAs. Needless to say, the full power of IdentityServer is available behind the scenes if the ASP.NET Core integrations don't cover your scenario. The ASP.NET Core support is focused on "first-party" apps, where all the apps are created and deployed by our organization. As such, support isn't offered for things like consent or federation. For those scenarios, use IdentityServer and follow their documentation.

### Application profiles

Application profiles are predefined configurations for apps that further define their parameters. At this time, the following profiles are supported:

* `IdentityServerSPA`: Represents a SPA hosted alongside IdentityServer as a single unit.

  * The `redirect_uri` defaults to `/authentication/login-callback`.
  * The `post_logout_redirect_uri` defaults to `/authentication/logout-callback`.
  * The set of scopes includes the `openid`, `profile`, and every scope defined for the APIs in the app.
  * The set of allowed OIDC response types is `id_token token` or each of them individually (`id_token`, `token`).
  * The allowed response mode is `fragment`.

* `SPA`: Represents a SPA that isn't hosted with IdentityServer.

  * The set of scopes includes the `openid`, `profile`, and every scope defined for the APIs in the app.
  * The set of allowed OIDC response types is `id_token token` or each of them individually (`id_token`, `token`).
  * The allowed response mode is `fragment`.

* `IdentityServerJwt`: Represents an API that is hosted alongside with IdentityServer.
  * The app is configured to have a single scope that defaults to the app name.

* `API`: Represents an API that isn't hosted with IdentityServer.
  * The app is configured to have a single scope that defaults to the app name.

### Configuration through `AppSettings`

Configure the apps through the configuration system by adding them to the list of `Clients` or `Resources`.

Configure each client's `redirect_uri` and `post_logout_redirect_uri` property, as shown in the following example:

```
"IdentityServer": {
  "Clients": {
    "MySPA": {
      "Profile": "SPA",
      "RedirectUri": "https://www.example.com/authentication/login-callback",
      "LogoutUri": "https://www.example.com/authentication/logout-callback"
    }
  }
}
```

When configuring resources, you can configure the scopes for the resource as shown below:

```
"IdentityServer": {
  "Resources": {
    "MyExternalApi": {
      "Profile": "API",
      "Scopes": "a b c"
    }
  }
}
```

### Configuration through code

You can also configure the clients and resources through code using an overload of `AddApiAuthorization` that takes an action to configure options.

```
AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
    options.Clients.AddSPA(
        "My SPA", spa =>
        spa.WithRedirectUri("http://www.example.com/authentication/login-callback")
           .WithLogoutRedirectUri(
               "http://www.example.com/authentication/logout-callback"));

    options.ApiResources.AddApiResource("MyExternalApi", resource =>
        resource.WithScopes("a", "b", "c"));
});
```

## Additional resources

* [Use Angular with ASP.NET Core](../../client-side/spa/angular?view=aspnetcore-10.0)
* [Use React with ASP.NET Core](../../client-side/spa/react?view=aspnetcore-10.0)
* [Scaffold Identity in ASP.NET Core projects](scaffold-identity?view=aspnetcore-10.0)

Collaborate with us on GitHub

The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see [our contributor guide](https://learn.microsoft.com/contribute/content/dotnet/dotnet-contribute).

ASP.NET Core

[Open a documentation issue ](#)[Provide product feedback](https://github.com/dotnet/aspnetcore/blob/main/CONTRIBUTING.md)

***

## Feedback

Was this page helpful?

No

Need help with this topic?

Want to try using Ask Learn to clarify or guide you through this topic?

***

## Additional resources

***

* Last updated on 2026-10-07